From 69ee3a15b6d989b82fae452d14f62a25cae2f8d7 Mon Sep 17 00:00:00 2001 From: AlCalzone Date: Tue, 27 Jan 2026 09:58:36 +0100 Subject: [PATCH] Display Z-Wave home IDs as hexadecimal (#161624) --- homeassistant/components/zwave_js/__init__.py | 5 ++ .../components/zwave_js/config_flow.py | 7 +- homeassistant/components/zwave_js/const.py | 1 + homeassistant/components/zwave_js/helpers.py | 11 ++- homeassistant/components/zwave_js/repairs.py | 21 ++++- tests/components/zwave_js/test_helpers.py | 16 ++++ tests/components/zwave_js/test_init.py | 12 +-- tests/components/zwave_js/test_repairs.py | 83 ++++++++++++++++++- 8 files changed, 138 insertions(+), 18 deletions(-) diff --git a/homeassistant/components/zwave_js/__init__.py b/homeassistant/components/zwave_js/__init__.py index e14fd0757f6..2f28c61c48b 100644 --- a/homeassistant/components/zwave_js/__init__.py +++ b/homeassistant/components/zwave_js/__init__.py @@ -71,6 +71,7 @@ from .const import ( ATTR_EVENT_TYPE, ATTR_EVENT_TYPE_LABEL, ATTR_HOME_ID, + ATTR_HOME_ID_HEX, ATTR_LABEL, ATTR_NODE_ID, ATTR_PARAMETERS, @@ -123,6 +124,7 @@ from .helpers import ( async_disable_server_logging_if_needed, async_enable_server_logging_if_needed, async_enable_statistics, + format_home_id_for_display, get_device_id, get_device_id_ext, get_network_identifier_for_notification, @@ -955,6 +957,7 @@ class NodeEvents: ATTR_DOMAIN: DOMAIN, ATTR_NODE_ID: notification.node.node_id, ATTR_HOME_ID: driver.controller.home_id, + ATTR_HOME_ID_HEX: format_home_id_for_display(driver.controller.home_id), ATTR_ENDPOINT: notification.endpoint, ATTR_DEVICE_ID: device.id, ATTR_COMMAND_CLASS: notification.command_class, @@ -992,6 +995,7 @@ class NodeEvents: ATTR_DOMAIN: DOMAIN, ATTR_NODE_ID: notification.node.node_id, ATTR_HOME_ID: driver.controller.home_id, + ATTR_HOME_ID_HEX: format_home_id_for_display(driver.controller.home_id), ATTR_ENDPOINT: notification.endpoint_idx, ATTR_DEVICE_ID: device.id, ATTR_COMMAND_CLASS: notification.command_class, @@ -1077,6 +1081,7 @@ class NodeEvents: { ATTR_NODE_ID: value.node.node_id, ATTR_HOME_ID: driver.controller.home_id, + ATTR_HOME_ID_HEX: format_home_id_for_display(driver.controller.home_id), ATTR_DEVICE_ID: device.id, ATTR_ENTITY_ID: entity_id, ATTR_COMMAND_CLASS: value.command_class, diff --git a/homeassistant/components/zwave_js/config_flow.py b/homeassistant/components/zwave_js/config_flow.py index dd855cd4a6c..81603392730 100644 --- a/homeassistant/components/zwave_js/config_flow.py +++ b/homeassistant/components/zwave_js/config_flow.py @@ -73,6 +73,7 @@ from .helpers import ( CannotConnect, async_get_version_info, async_wait_for_driver_ready_event, + format_home_id_for_display, ) from .models import ZwaveJSConfigEntry @@ -467,7 +468,8 @@ class ZWaveJSConfigFlow(ConfigFlow, domain=DOMAIN): await self.async_set_unique_id(home_id) self._abort_if_unique_id_configured() self.ws_address = f"ws://{discovery_info.host}:{discovery_info.port}" - self.context.update({"title_placeholders": {CONF_NAME: home_id}}) + home_id_display = format_home_id_for_display(int(home_id)) + self.context.update({"title_placeholders": {CONF_NAME: home_id_display}}) return await self.async_step_zeroconf_confirm() async def async_step_zeroconf_confirm( @@ -479,10 +481,11 @@ class ZWaveJSConfigFlow(ConfigFlow, domain=DOMAIN): assert self.ws_address assert self.unique_id + home_id_display = format_home_id_for_display(int(self.unique_id)) return self.async_show_form( step_id="zeroconf_confirm", description_placeholders={ - "home_id": self.unique_id, + "home_id": home_id_display, CONF_URL: self.ws_address[5:], }, ) diff --git a/homeassistant/components/zwave_js/const.py b/homeassistant/components/zwave_js/const.py index 951f312516d..781be2af987 100644 --- a/homeassistant/components/zwave_js/const.py +++ b/homeassistant/components/zwave_js/const.py @@ -57,6 +57,7 @@ ZWAVE_JS_NOTIFICATION_EVENT = f"{DOMAIN}_notification" ZWAVE_JS_VALUE_UPDATED_EVENT = f"{DOMAIN}_value_updated" ATTR_NODE_ID = "node_id" ATTR_HOME_ID = "home_id" +ATTR_HOME_ID_HEX = "home_id_hex" ATTR_ENDPOINT = "endpoint" ATTR_LABEL = "label" ATTR_VALUE = "value" diff --git a/homeassistant/components/zwave_js/helpers.py b/homeassistant/components/zwave_js/helpers.py index dc415c157b6..d4c8ff7cbb1 100644 --- a/homeassistant/components/zwave_js/helpers.py +++ b/homeassistant/components/zwave_js/helpers.py @@ -184,6 +184,13 @@ async def async_disable_server_logging_if_needed( LOGGER.info("Zwave-js-server logging is enabled") +def format_home_id_for_display(home_id: int | None) -> str: + """Format home ID as hexadecimal string for display.""" + if home_id is None: + return "Unknown" + return f"0x{home_id:08x}" + + def get_valueless_base_unique_id(driver: Driver, node: ZwaveNode) -> str: """Return the base unique ID for an entity that is not based on a value.""" return f"{driver.controller.home_id}.{node.node_id}" @@ -555,9 +562,9 @@ def get_network_identifier_for_notification( hass: HomeAssistant, config_entry: ZwaveJSConfigEntry, controller: Controller ) -> str: """Return the network identifier string for persistent notifications.""" - home_id = str(controller.home_id) + home_id = format_home_id_for_display(controller.home_id) if len(hass.config_entries.async_entries(DOMAIN)) > 1: - if str(home_id) != config_entry.title: + if home_id != config_entry.title: return f"`{config_entry.title}`, with the home ID `{home_id}`," return f"with the home ID `{home_id}`" return "" diff --git a/homeassistant/components/zwave_js/repairs.py b/homeassistant/components/zwave_js/repairs.py index 072a330a7bd..7982065cb0d 100644 --- a/homeassistant/components/zwave_js/repairs.py +++ b/homeassistant/components/zwave_js/repairs.py @@ -8,7 +8,7 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers import issue_registry as ir from .const import DOMAIN -from .helpers import async_get_node_from_device_id +from .helpers import async_get_node_from_device_id, format_home_id_for_display class DeviceConfigFileChangedFlow(RepairsFlow): @@ -62,13 +62,26 @@ class MigrateUniqueIDFlow(RepairsFlow): def __init__(self, data: dict[str, str]) -> None: """Initialize.""" + # Format IDs for display + + try: + new_unique_id_hex = format_home_id_for_display(int(data["new_unique_id"])) + except (ValueError, TypeError): + new_unique_id_hex = data["new_unique_id"] + + try: + old_unique_id_hex = format_home_id_for_display(int(data["old_unique_id"])) + except (ValueError, TypeError): + old_unique_id_hex = data["old_unique_id"] + self.description_placeholders: dict[str, str] = { "config_entry_title": data["config_entry_title"], "controller_model": data["controller_model"], - "new_unique_id": data["new_unique_id"], - "old_unique_id": data["old_unique_id"], + "new_unique_id": new_unique_id_hex, + "old_unique_id": old_unique_id_hex, } self._config_entry_id: str = data["config_entry_id"] + self._new_unique_id: str = data["new_unique_id"] async def async_step_init( self, user_input: dict[str, str] | None = None @@ -88,7 +101,7 @@ class MigrateUniqueIDFlow(RepairsFlow): if config_entry is not None: self.hass.config_entries.async_update_entry( config_entry, - unique_id=self.description_placeholders["new_unique_id"], + unique_id=self._new_unique_id, ) self.hass.config_entries.async_schedule_reload(config_entry.entry_id) return self.async_create_entry(data={}) diff --git a/tests/components/zwave_js/test_helpers.py b/tests/components/zwave_js/test_helpers.py index c163b8e8c75..544b5126567 100644 --- a/tests/components/zwave_js/test_helpers.py +++ b/tests/components/zwave_js/test_helpers.py @@ -12,6 +12,7 @@ from homeassistant.components.zwave_js.helpers import ( async_get_node_status_sensor_entity_id, async_get_nodes_from_area_id, async_get_provisioning_entry_from_device_id, + format_home_id_for_display, get_value_state_schema, ) from homeassistant.config_entries import ConfigEntryState @@ -138,3 +139,18 @@ async def test_async_get_provisioning_entry_from_device_id( ): result = await async_get_provisioning_entry_from_device_id(hass, device.id) assert result == provisioning_entry + + +def test_format_home_id_for_display() -> None: + """Test format_home_id_for_display.""" + # Test with standard home ID + assert format_home_id_for_display(3245146787) == "0xc16d02a3" + + # Test with zero + assert format_home_id_for_display(0) == "0x00000000" + + # Test with max 32-bit value + assert format_home_id_for_display(4294967295) == "0xffffffff" + + # Test with None + assert format_home_id_for_display(None) == "Unknown" diff --git a/tests/components/zwave_js/test_init.py b/tests/components/zwave_js/test_init.py index 8faf2a28ce6..908f57e54d2 100644 --- a/tests/components/zwave_js/test_init.py +++ b/tests/components/zwave_js/test_init.py @@ -2077,18 +2077,18 @@ async def test_identify_event( assert len(notifications) == 1 assert list(notifications)[0] == msg_id assert ( - "network `Mock Title`, with the home ID `3245146787`" + "network `Mock Title`, with the home ID `0xc16d02a3`" in notifications[msg_id]["message"] ) async_dismiss(hass, msg_id) # Test case where config entry title and home ID do match - hass.config_entries.async_update_entry(integration, title="3245146787") + hass.config_entries.async_update_entry(integration, title="0xc16d02a3") client.driver.controller.receive_event(event) notifications = async_get_persistent_notifications(hass) assert len(notifications) == 1 assert list(notifications)[0] == msg_id - assert "network with the home ID `3245146787`" in notifications[msg_id]["message"] + assert "network with the home ID `0xc16d02a3`" in notifications[msg_id]["message"] async def test_server_logging( @@ -2241,13 +2241,13 @@ async def test_factory_reset_node( assert len(notifications) == 1 assert list(notifications)[0] == msg_id assert ( - "network `Mock Title`, with the home ID `3245146787`" + "network `Mock Title`, with the home ID `0xc16d02a3`" in notifications[msg_id]["message"] ) async_dismiss(hass, msg_id) # Test case where config entry title and home ID do match - hass.config_entries.async_update_entry(integration, title="3245146787") + hass.config_entries.async_update_entry(integration, title="0xc16d02a3") add_event = Event( type="node added", data={ @@ -2264,7 +2264,7 @@ async def test_factory_reset_node( notifications = async_get_persistent_notifications(hass) assert len(notifications) == 1 assert list(notifications)[0] == msg_id - assert "network with the home ID `3245146787`" in notifications[msg_id]["message"] + assert "network with the home ID `0xc16d02a3`" in notifications[msg_id]["message"] async def test_entity_available_when_node_dead( diff --git a/tests/components/zwave_js/test_repairs.py b/tests/components/zwave_js/test_repairs.py index cb2c5a846c7..4bd26ac74a1 100644 --- a/tests/components/zwave_js/test_repairs.py +++ b/tests/components/zwave_js/test_repairs.py @@ -379,8 +379,8 @@ async def test_migrate_unique_id( assert data["description_placeholders"] == { "config_entry_title": "Z-Wave JS", "controller_model": "ZW090", - "new_unique_id": "3245146787", - "old_unique_id": old_unique_id, + "new_unique_id": "0xc16d02a3", + "old_unique_id": "0x075bcd15", } # Apply fix @@ -446,8 +446,8 @@ async def test_migrate_unique_id_missing_config_entry( assert data["description_placeholders"] == { "config_entry_title": "Z-Wave JS", "controller_model": "ZW090", - "new_unique_id": "3245146787", - "old_unique_id": old_unique_id, + "new_unique_id": "0xc16d02a3", + "old_unique_id": "0x075bcd15", } # Apply fix @@ -459,3 +459,78 @@ async def test_migrate_unique_id_missing_config_entry( msg = await ws_client.receive_json() assert msg["success"] assert len(msg["result"]["issues"]) == 0 + + +@pytest.mark.usefixtures("client") +async def test_migrate_unique_id_non_integer_ids( + hass: HomeAssistant, + hass_client: ClientSessionGenerator, + hass_ws_client: WebSocketGenerator, +) -> None: + """Test the migrate unique id flow with non-integer unique IDs.""" + old_unique_id = "non_numeric_id" + new_unique_id = "also_invalid" + config_entry = MockConfigEntry( + domain=DOMAIN, + title="Z-Wave JS", + data={ + "url": "ws://test.org", + }, + unique_id=old_unique_id, + ) + config_entry.add_to_hass(hass) + + await hass.config_entries.async_setup(config_entry.entry_id) + + # Manually create the repair issue with non-integer unique IDs + ir.async_create_issue( + hass, + DOMAIN, + f"migrate_unique_id.{config_entry.entry_id}", + data={ + "config_entry_id": config_entry.entry_id, + "config_entry_title": "Z-Wave JS", + "controller_model": "ZW090", + "new_unique_id": new_unique_id, + "old_unique_id": old_unique_id, + }, + is_fixable=True, + severity=ir.IssueSeverity.ERROR, + translation_key="migrate_unique_id", + ) + + await async_process_repairs_platforms(hass) + ws_client = await hass_ws_client(hass) + http_client = await hass_client() + + # Assert the issue is present + await ws_client.send_json_auto_id({"type": "repairs/list_issues"}) + msg = await ws_client.receive_json() + assert msg["success"] + assert len(msg["result"]["issues"]) == 1 + issue = msg["result"]["issues"][0] + issue_id = issue["issue_id"] + assert issue_id == f"migrate_unique_id.{config_entry.entry_id}" + + data = await start_repair_fix_flow(http_client, DOMAIN, issue_id) + + flow_id = data["flow_id"] + assert data["step_id"] == "confirm" + # The non-integer IDs should be displayed as-is + assert data["description_placeholders"] == { + "config_entry_title": "Z-Wave JS", + "controller_model": "ZW090", + "new_unique_id": new_unique_id, + "old_unique_id": old_unique_id, + } + + # Apply fix + data = await process_repair_fix_flow(http_client, flow_id) + + assert data["type"] == "create_entry" + assert config_entry.unique_id == new_unique_id + + await ws_client.send_json_auto_id({"type": "repairs/list_issues"}) + msg = await ws_client.receive_json() + assert msg["success"] + assert len(msg["result"]["issues"]) == 0