From 5283e1a39fefde9a7b4953d0675e25ca84665edc Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Fri, 28 Mar 2025 15:38:16 -0400 Subject: [PATCH] Handle all firmware types for ZBT-1 and Yellow update entities (#141674) Handle other firmware types --- .../homeassistant_hardware/update.py | 2 +- .../homeassistant_sky_connect/update.py | 35 +++++++++++-- .../components/homeassistant_yellow/update.py | 35 +++++++++++-- .../homeassistant_sky_connect/test_update.py | 46 ++++++++++++++++ .../homeassistant_yellow/test_update.py | 52 +++++++++++++++++++ 5 files changed, 163 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/homeassistant_hardware/update.py b/homeassistant/components/homeassistant_hardware/update.py index e835286238f..960facc81f8 100644 --- a/homeassistant/components/homeassistant_hardware/update.py +++ b/homeassistant/components/homeassistant_hardware/update.py @@ -199,7 +199,7 @@ class BaseFirmwareUpdateEntity( # This entity is not currently associated with a device so we must manually # give it a name self._attr_name = f"{self._config_entry.title} Update" - self._attr_title = self.entity_description.firmware_name or "unknown" + self._attr_title = self.entity_description.firmware_name or "Unknown" if ( self._current_firmware_info is None diff --git a/homeassistant/components/homeassistant_sky_connect/update.py b/homeassistant/components/homeassistant_sky_connect/update.py index 96978eb4562..5eaa1e220be 100644 --- a/homeassistant/components/homeassistant_sky_connect/update.py +++ b/homeassistant/components/homeassistant_sky_connect/update.py @@ -64,6 +64,28 @@ FIRMWARE_ENTITY_DESCRIPTIONS: dict[ expected_firmware_type=ApplicationType.SPINEL, firmware_name="OpenThread RCP", ), + ApplicationType.CPC: FirmwareUpdateEntityDescription( + key="firmware", + display_precision=0, + device_class=UpdateDeviceClass.FIRMWARE, + entity_category=EntityCategory.CONFIG, + version_parser=lambda fw: fw, + fw_type="skyconnect_multipan", + version_key="cpc_version", + expected_firmware_type=ApplicationType.CPC, + firmware_name="Multiprotocol", + ), + ApplicationType.GECKO_BOOTLOADER: FirmwareUpdateEntityDescription( + key="firmware", + display_precision=0, + device_class=UpdateDeviceClass.FIRMWARE, + entity_category=EntityCategory.CONFIG, + version_parser=lambda fw: fw, + fw_type=None, # We don't want to update the bootloader + version_key="gecko_bootloader_version", + expected_firmware_type=ApplicationType.GECKO_BOOTLOADER, + firmware_name="Gecko Bootloader", + ), None: FirmwareUpdateEntityDescription( key="firmware", display_precision=0, @@ -86,9 +108,16 @@ def _async_create_update_entity( ) -> FirmwareUpdateEntity: """Create an update entity that handles firmware type changes.""" firmware_type = config_entry.data[FIRMWARE] - entity_description = FIRMWARE_ENTITY_DESCRIPTIONS[ - ApplicationType(firmware_type) if firmware_type is not None else None - ] + + try: + entity_description = FIRMWARE_ENTITY_DESCRIPTIONS[ + ApplicationType(firmware_type) + ] + except (KeyError, ValueError): + _LOGGER.debug( + "Unknown firmware type %r, using default entity description", firmware_type + ) + entity_description = FIRMWARE_ENTITY_DESCRIPTIONS[None] entity = FirmwareUpdateEntity( device=config_entry.data["device"], diff --git a/homeassistant/components/homeassistant_yellow/update.py b/homeassistant/components/homeassistant_yellow/update.py index 71913dc9923..94989d5c6b6 100644 --- a/homeassistant/components/homeassistant_yellow/update.py +++ b/homeassistant/components/homeassistant_yellow/update.py @@ -64,6 +64,28 @@ FIRMWARE_ENTITY_DESCRIPTIONS: dict[ expected_firmware_type=ApplicationType.SPINEL, firmware_name="OpenThread RCP", ), + ApplicationType.CPC: FirmwareUpdateEntityDescription( + key="firmware", + display_precision=0, + device_class=UpdateDeviceClass.FIRMWARE, + entity_category=EntityCategory.CONFIG, + version_parser=lambda fw: fw, + fw_type="yellow_multipan", + version_key="cpc_version", + expected_firmware_type=ApplicationType.CPC, + firmware_name="Multiprotocol", + ), + ApplicationType.GECKO_BOOTLOADER: FirmwareUpdateEntityDescription( + key="firmware", + display_precision=0, + device_class=UpdateDeviceClass.FIRMWARE, + entity_category=EntityCategory.CONFIG, + version_parser=lambda fw: fw, + fw_type=None, # We don't want to update the bootloader + version_key="gecko_bootloader_version", + expected_firmware_type=ApplicationType.GECKO_BOOTLOADER, + firmware_name="Gecko Bootloader", + ), None: FirmwareUpdateEntityDescription( key="radio_firmware", display_precision=0, @@ -86,9 +108,16 @@ def _async_create_update_entity( ) -> FirmwareUpdateEntity: """Create an update entity that handles firmware type changes.""" firmware_type = config_entry.data[FIRMWARE] - entity_description = FIRMWARE_ENTITY_DESCRIPTIONS[ - ApplicationType(firmware_type) if firmware_type is not None else None - ] + + try: + entity_description = FIRMWARE_ENTITY_DESCRIPTIONS[ + ApplicationType(firmware_type) + ] + except (KeyError, ValueError): + _LOGGER.debug( + "Unknown firmware type %r, using default entity description", firmware_type + ) + entity_description = FIRMWARE_ENTITY_DESCRIPTIONS[None] entity = FirmwareUpdateEntity( device=RADIO_DEVICE, diff --git a/tests/components/homeassistant_sky_connect/test_update.py b/tests/components/homeassistant_sky_connect/test_update.py index 7ad0099785b..b6c7291e0af 100644 --- a/tests/components/homeassistant_sky_connect/test_update.py +++ b/tests/components/homeassistant_sky_connect/test_update.py @@ -1,5 +1,7 @@ """Test SkyConnect firmware update entity.""" +import pytest + from homeassistant.components.homeassistant_hardware.helpers import ( async_notify_firmware_info, ) @@ -84,3 +86,47 @@ async def test_zbt1_update_entity(hass: HomeAssistant) -> None: assert state_spinel.attributes["title"] == "OpenThread RCP" assert state_spinel.attributes["installed_version"] == "2.4.4.0" assert state_spinel.attributes["latest_version"] is None + + +@pytest.mark.parametrize( + ("firmware", "version", "expected"), + [ + ("ezsp", "7.3.1.0 build 0", "EmberZNet Zigbee 7.3.1.0"), + ("spinel", "SL-OPENTHREAD/2.4.4.0_GitHub-7074a43e4", "OpenThread RCP 2.4.4.0"), + ("bootloader", "2.4.2", "Gecko Bootloader 2.4.2"), + ("cpc", "4.3.2", "Multiprotocol 4.3.2"), + ("router", "1.2.3.4", "Unknown 1.2.3.4"), # Not supported but still shown + ], +) +async def test_zbt1_update_entity_state( + hass: HomeAssistant, firmware: str, version: str, expected: str +) -> None: + """Test the ZBT-1 firmware update entity with different firmware types.""" + await async_setup_component(hass, "homeassistant", {}) + + zbt1_config_entry = MockConfigEntry( + domain="homeassistant_sky_connect", + data={ + "firmware": firmware, + "firmware_version": version, + "device": USB_DATA_ZBT1.device, + "manufacturer": USB_DATA_ZBT1.manufacturer, + "pid": USB_DATA_ZBT1.pid, + "product": USB_DATA_ZBT1.description, + "serial_number": USB_DATA_ZBT1.serial_number, + "vid": USB_DATA_ZBT1.vid, + }, + version=1, + minor_version=3, + ) + zbt1_config_entry.add_to_hass(hass) + + assert await hass.config_entries.async_setup(zbt1_config_entry.entry_id) + await hass.async_block_till_done() + + state = hass.states.get(UPDATE_ENTITY_ID) + assert state is not None + assert ( + f"{state.attributes['title']} {state.attributes['installed_version']}" + == expected + ) diff --git a/tests/components/homeassistant_yellow/test_update.py b/tests/components/homeassistant_yellow/test_update.py index 2ce66b95137..2cc7b51836c 100644 --- a/tests/components/homeassistant_yellow/test_update.py +++ b/tests/components/homeassistant_yellow/test_update.py @@ -2,6 +2,8 @@ from unittest.mock import patch +import pytest + from homeassistant.components.homeassistant_hardware.helpers import ( async_notify_firmware_info, ) @@ -90,3 +92,53 @@ async def test_yellow_update_entity(hass: HomeAssistant) -> None: assert state_spinel.attributes["title"] == "OpenThread RCP" assert state_spinel.attributes["installed_version"] == "2.4.4.0" assert state_spinel.attributes["latest_version"] is None + + +@pytest.mark.parametrize( + ("firmware", "version", "expected"), + [ + ("ezsp", "7.3.1.0 build 0", "EmberZNet Zigbee 7.3.1.0"), + ("spinel", "SL-OPENTHREAD/2.4.4.0_GitHub-7074a43e4", "OpenThread RCP 2.4.4.0"), + ("bootloader", "2.4.2", "Gecko Bootloader 2.4.2"), + ("cpc", "4.3.2", "Multiprotocol 4.3.2"), + ("router", "1.2.3.4", "Unknown 1.2.3.4"), # Not supported but still shown + ], +) +async def test_yellow_update_entity_state( + hass: HomeAssistant, firmware: str, version: str, expected: str +) -> None: + """Test the Yellow firmware update entity with different firmware types.""" + await async_setup_component(hass, "homeassistant", {}) + + # Set up the Yellow integration + yellow_config_entry = MockConfigEntry( + title="Home Assistant Yellow", + domain="homeassistant_yellow", + data={ + "firmware": firmware, + "firmware_version": version, + "device": RADIO_DEVICE, + }, + version=1, + minor_version=3, + ) + yellow_config_entry.add_to_hass(hass) + + with ( + patch( + "homeassistant.components.homeassistant_yellow.is_hassio", return_value=True + ), + patch( + "homeassistant.components.homeassistant_yellow.get_os_info", + return_value={"board": "yellow"}, + ), + ): + assert await hass.config_entries.async_setup(yellow_config_entry.entry_id) + await hass.async_block_till_done() + + state = hass.states.get(UPDATE_ENTITY_ID) + assert state is not None + assert ( + f"{state.attributes['title']} {state.attributes['installed_version']}" + == expected + )