diff --git a/homeassistant/components/homeassistant_connect_zbt2/strings.json b/homeassistant/components/homeassistant_connect_zbt2/strings.json index 2a3128023aedbf..20d340216e95d7 100644 --- a/homeassistant/components/homeassistant_connect_zbt2/strings.json +++ b/homeassistant/components/homeassistant_connect_zbt2/strings.json @@ -53,11 +53,15 @@ "description": "[%key:component::homeassistant_hardware::firmware_picker::options::step::pick_firmware::description%]", "menu_options": { "pick_firmware_zigbee": "[%key:component::homeassistant_hardware::firmware_picker::options::step::pick_firmware::menu_options::pick_firmware_zigbee%]", - "pick_firmware_thread": "[%key:component::homeassistant_hardware::firmware_picker::options::step::pick_firmware::menu_options::pick_firmware_thread%]" + "pick_firmware_thread": "[%key:component::homeassistant_hardware::firmware_picker::options::step::pick_firmware::menu_options::pick_firmware_thread%]", + "pick_firmware_zigbee_migrate": "[%key:component::homeassistant_hardware::firmware_picker::options::step::pick_firmware::menu_options::pick_firmware_zigbee_migrate%]", + "pick_firmware_thread_migrate": "[%key:component::homeassistant_hardware::firmware_picker::options::step::pick_firmware::menu_options::pick_firmware_thread_migrate%]" }, "menu_option_descriptions": { "pick_firmware_zigbee": "[%key:component::homeassistant_hardware::firmware_picker::options::step::pick_firmware::menu_option_descriptions::pick_firmware_zigbee%]", - "pick_firmware_thread": "[%key:component::homeassistant_hardware::firmware_picker::options::step::pick_firmware::menu_option_descriptions::pick_firmware_thread%]" + "pick_firmware_thread": "[%key:component::homeassistant_hardware::firmware_picker::options::step::pick_firmware::menu_option_descriptions::pick_firmware_thread%]", + "pick_firmware_zigbee_migrate": "[%key:component::homeassistant_hardware::firmware_picker::options::step::pick_firmware::menu_option_descriptions::pick_firmware_zigbee_migrate%]", + "pick_firmware_thread_migrate": "[%key:component::homeassistant_hardware::firmware_picker::options::step::pick_firmware::menu_option_descriptions::pick_firmware_thread_migrate%]" } }, "confirm_zigbee": { @@ -138,11 +142,15 @@ "description": "[%key:component::homeassistant_hardware::firmware_picker::options::step::pick_firmware::description%]", "menu_options": { "pick_firmware_zigbee": "[%key:component::homeassistant_hardware::firmware_picker::options::step::pick_firmware::menu_options::pick_firmware_zigbee%]", - "pick_firmware_thread": "[%key:component::homeassistant_hardware::firmware_picker::options::step::pick_firmware::menu_options::pick_firmware_thread%]" + "pick_firmware_thread": "[%key:component::homeassistant_hardware::firmware_picker::options::step::pick_firmware::menu_options::pick_firmware_thread%]", + "pick_firmware_zigbee_migrate": "[%key:component::homeassistant_hardware::firmware_picker::options::step::pick_firmware::menu_options::pick_firmware_zigbee_migrate%]", + "pick_firmware_thread_migrate": "[%key:component::homeassistant_hardware::firmware_picker::options::step::pick_firmware::menu_options::pick_firmware_thread_migrate%]" }, "menu_option_descriptions": { "pick_firmware_zigbee": "[%key:component::homeassistant_hardware::firmware_picker::options::step::pick_firmware::menu_option_descriptions::pick_firmware_zigbee%]", - "pick_firmware_thread": "[%key:component::homeassistant_hardware::firmware_picker::options::step::pick_firmware::menu_option_descriptions::pick_firmware_thread%]" + "pick_firmware_thread": "[%key:component::homeassistant_hardware::firmware_picker::options::step::pick_firmware::menu_option_descriptions::pick_firmware_thread%]", + "pick_firmware_zigbee_migrate": "[%key:component::homeassistant_hardware::firmware_picker::options::step::pick_firmware::menu_option_descriptions::pick_firmware_zigbee_migrate%]", + "pick_firmware_thread_migrate": "[%key:component::homeassistant_hardware::firmware_picker::options::step::pick_firmware::menu_option_descriptions::pick_firmware_thread_migrate%]" } }, "confirm_zigbee": { diff --git a/homeassistant/components/homeassistant_hardware/firmware_config_flow.py b/homeassistant/components/homeassistant_hardware/firmware_config_flow.py index ac89ebad0e90cd..69f290982087b9 100644 --- a/homeassistant/components/homeassistant_hardware/firmware_config_flow.py +++ b/homeassistant/components/homeassistant_hardware/firmware_config_flow.py @@ -50,6 +50,8 @@ STEP_PICK_FIRMWARE_THREAD = "pick_firmware_thread" STEP_PICK_FIRMWARE_ZIGBEE = "pick_firmware_zigbee" +STEP_PICK_FIRMWARE_THREAD_MIGRATE = "pick_firmware_thread_migrate" +STEP_PICK_FIRMWARE_ZIGBEE_MIGRATE = "pick_firmware_zigbee_migrate" class PickedFirmwareType(StrEnum): @@ -124,11 +126,23 @@ async def async_step_pick_firmware( self, user_input: dict[str, Any] | None = None ) -> ConfigFlowResult: """Pick Thread or Zigbee firmware.""" + # Determine if ZHA or Thread are already configured to present migrate options + zha_entries = self.hass.config_entries.async_entries(ZHA_DOMAIN) + otbr_entries = self.hass.config_entries.async_entries(OTBR_DOMAIN) + return self.async_show_menu( step_id="pick_firmware", menu_options=[ - STEP_PICK_FIRMWARE_ZIGBEE, - STEP_PICK_FIRMWARE_THREAD, + ( + STEP_PICK_FIRMWARE_ZIGBEE_MIGRATE + if zha_entries + else STEP_PICK_FIRMWARE_ZIGBEE + ), + ( + STEP_PICK_FIRMWARE_THREAD_MIGRATE + if otbr_entries + else STEP_PICK_FIRMWARE_THREAD + ), ], description_placeholders=self._get_translation_placeholders(), ) @@ -374,6 +388,12 @@ async def async_step_pick_firmware_zigbee( self._picked_firmware_type = PickedFirmwareType.ZIGBEE return await self.async_step_zigbee_installation_type() + async def async_step_pick_firmware_zigbee_migrate( + self, user_input: dict[str, Any] | None = None + ) -> ConfigFlowResult: + """Pick Zigbee firmware. Migration is automatic.""" + return await self.async_step_pick_firmware_zigbee() + async def async_step_install_zigbee_firmware( self, user_input: dict[str, Any] | None = None ) -> ConfigFlowResult: @@ -476,6 +496,12 @@ async def async_step_pick_firmware_thread( self._picked_firmware_type = PickedFirmwareType.THREAD return await self._async_continue_picked_firmware() + async def async_step_pick_firmware_thread_migrate( + self, user_input: dict[str, Any] | None = None + ) -> ConfigFlowResult: + """Pick Thread firmware. Migration is automatic.""" + return await self.async_step_pick_firmware_thread() + async def async_step_install_thread_firmware( self, user_input: dict[str, Any] | None = None ) -> ConfigFlowResult: diff --git a/homeassistant/components/homeassistant_hardware/strings.json b/homeassistant/components/homeassistant_hardware/strings.json index 0cc4dbc8afe358..a33dae15377981 100644 --- a/homeassistant/components/homeassistant_hardware/strings.json +++ b/homeassistant/components/homeassistant_hardware/strings.json @@ -7,11 +7,15 @@ "description": "You can use your {model} for a Zigbee or Thread network. Please check what type of devices you want to add to Home Assistant. You can always change this later.", "menu_options": { "pick_firmware_zigbee": "Use as Zigbee adapter", - "pick_firmware_thread": "Use as Thread adapter" + "pick_firmware_thread": "Use as Thread adapter", + "pick_firmware_zigbee_migrate": "Migrate Zigbee to a new adapter", + "pick_firmware_thread_migrate": "Migrate Thread to a new adapter" }, "menu_option_descriptions": { "pick_firmware_zigbee": "Most common protocol.", - "pick_firmware_thread": "Often used for Matter over Thread devices." + "pick_firmware_thread": "Often used for Matter over Thread devices.", + "pick_firmware_zigbee_migrate": "This will move your Zigbee network to the new adapter.", + "pick_firmware_thread_migrate": "This will migrate your Thread Border Router to the new adapter." } }, "confirm_zigbee": { diff --git a/homeassistant/components/homeassistant_sky_connect/strings.json b/homeassistant/components/homeassistant_sky_connect/strings.json index 2a3128023aedbf..20d340216e95d7 100644 --- a/homeassistant/components/homeassistant_sky_connect/strings.json +++ b/homeassistant/components/homeassistant_sky_connect/strings.json @@ -53,11 +53,15 @@ "description": "[%key:component::homeassistant_hardware::firmware_picker::options::step::pick_firmware::description%]", "menu_options": { "pick_firmware_zigbee": "[%key:component::homeassistant_hardware::firmware_picker::options::step::pick_firmware::menu_options::pick_firmware_zigbee%]", - "pick_firmware_thread": "[%key:component::homeassistant_hardware::firmware_picker::options::step::pick_firmware::menu_options::pick_firmware_thread%]" + "pick_firmware_thread": "[%key:component::homeassistant_hardware::firmware_picker::options::step::pick_firmware::menu_options::pick_firmware_thread%]", + "pick_firmware_zigbee_migrate": "[%key:component::homeassistant_hardware::firmware_picker::options::step::pick_firmware::menu_options::pick_firmware_zigbee_migrate%]", + "pick_firmware_thread_migrate": "[%key:component::homeassistant_hardware::firmware_picker::options::step::pick_firmware::menu_options::pick_firmware_thread_migrate%]" }, "menu_option_descriptions": { "pick_firmware_zigbee": "[%key:component::homeassistant_hardware::firmware_picker::options::step::pick_firmware::menu_option_descriptions::pick_firmware_zigbee%]", - "pick_firmware_thread": "[%key:component::homeassistant_hardware::firmware_picker::options::step::pick_firmware::menu_option_descriptions::pick_firmware_thread%]" + "pick_firmware_thread": "[%key:component::homeassistant_hardware::firmware_picker::options::step::pick_firmware::menu_option_descriptions::pick_firmware_thread%]", + "pick_firmware_zigbee_migrate": "[%key:component::homeassistant_hardware::firmware_picker::options::step::pick_firmware::menu_option_descriptions::pick_firmware_zigbee_migrate%]", + "pick_firmware_thread_migrate": "[%key:component::homeassistant_hardware::firmware_picker::options::step::pick_firmware::menu_option_descriptions::pick_firmware_thread_migrate%]" } }, "confirm_zigbee": { @@ -138,11 +142,15 @@ "description": "[%key:component::homeassistant_hardware::firmware_picker::options::step::pick_firmware::description%]", "menu_options": { "pick_firmware_zigbee": "[%key:component::homeassistant_hardware::firmware_picker::options::step::pick_firmware::menu_options::pick_firmware_zigbee%]", - "pick_firmware_thread": "[%key:component::homeassistant_hardware::firmware_picker::options::step::pick_firmware::menu_options::pick_firmware_thread%]" + "pick_firmware_thread": "[%key:component::homeassistant_hardware::firmware_picker::options::step::pick_firmware::menu_options::pick_firmware_thread%]", + "pick_firmware_zigbee_migrate": "[%key:component::homeassistant_hardware::firmware_picker::options::step::pick_firmware::menu_options::pick_firmware_zigbee_migrate%]", + "pick_firmware_thread_migrate": "[%key:component::homeassistant_hardware::firmware_picker::options::step::pick_firmware::menu_options::pick_firmware_thread_migrate%]" }, "menu_option_descriptions": { "pick_firmware_zigbee": "[%key:component::homeassistant_hardware::firmware_picker::options::step::pick_firmware::menu_option_descriptions::pick_firmware_zigbee%]", - "pick_firmware_thread": "[%key:component::homeassistant_hardware::firmware_picker::options::step::pick_firmware::menu_option_descriptions::pick_firmware_thread%]" + "pick_firmware_thread": "[%key:component::homeassistant_hardware::firmware_picker::options::step::pick_firmware::menu_option_descriptions::pick_firmware_thread%]", + "pick_firmware_zigbee_migrate": "[%key:component::homeassistant_hardware::firmware_picker::options::step::pick_firmware::menu_option_descriptions::pick_firmware_zigbee_migrate%]", + "pick_firmware_thread_migrate": "[%key:component::homeassistant_hardware::firmware_picker::options::step::pick_firmware::menu_option_descriptions::pick_firmware_thread_migrate%]" } }, "confirm_zigbee": { diff --git a/homeassistant/components/homeassistant_yellow/strings.json b/homeassistant/components/homeassistant_yellow/strings.json index a51bd3b3ed7607..3d5da55bb92bb6 100644 --- a/homeassistant/components/homeassistant_yellow/strings.json +++ b/homeassistant/components/homeassistant_yellow/strings.json @@ -76,11 +76,15 @@ "description": "[%key:component::homeassistant_hardware::firmware_picker::options::step::pick_firmware::description%]", "menu_options": { "pick_firmware_zigbee": "[%key:component::homeassistant_hardware::firmware_picker::options::step::pick_firmware::menu_options::pick_firmware_zigbee%]", - "pick_firmware_thread": "[%key:component::homeassistant_hardware::firmware_picker::options::step::pick_firmware::menu_options::pick_firmware_thread%]" + "pick_firmware_thread": "[%key:component::homeassistant_hardware::firmware_picker::options::step::pick_firmware::menu_options::pick_firmware_thread%]", + "pick_firmware_zigbee_migrate": "[%key:component::homeassistant_hardware::firmware_picker::options::step::pick_firmware::menu_options::pick_firmware_zigbee_migrate%]", + "pick_firmware_thread_migrate": "[%key:component::homeassistant_hardware::firmware_picker::options::step::pick_firmware::menu_options::pick_firmware_thread_migrate%]" }, "menu_option_descriptions": { "pick_firmware_zigbee": "[%key:component::homeassistant_hardware::firmware_picker::options::step::pick_firmware::menu_option_descriptions::pick_firmware_zigbee%]", - "pick_firmware_thread": "[%key:component::homeassistant_hardware::firmware_picker::options::step::pick_firmware::menu_option_descriptions::pick_firmware_thread%]" + "pick_firmware_thread": "[%key:component::homeassistant_hardware::firmware_picker::options::step::pick_firmware::menu_option_descriptions::pick_firmware_thread%]", + "pick_firmware_zigbee_migrate": "[%key:component::homeassistant_hardware::firmware_picker::options::step::pick_firmware::menu_option_descriptions::pick_firmware_zigbee_migrate%]", + "pick_firmware_thread_migrate": "[%key:component::homeassistant_hardware::firmware_picker::options::step::pick_firmware::menu_option_descriptions::pick_firmware_thread_migrate%]" } }, "confirm_zigbee": { diff --git a/homeassistant/components/opower/manifest.json b/homeassistant/components/opower/manifest.json index aa9e66d3841a31..cd24da920870fe 100644 --- a/homeassistant/components/opower/manifest.json +++ b/homeassistant/components/opower/manifest.json @@ -8,5 +8,5 @@ "iot_class": "cloud_polling", "loggers": ["opower"], "quality_scale": "bronze", - "requirements": ["opower==0.15.4"] + "requirements": ["opower==0.15.5"] } diff --git a/homeassistant/components/reolink/manifest.json b/homeassistant/components/reolink/manifest.json index 52b46089537d23..a509a79eaa1b8a 100644 --- a/homeassistant/components/reolink/manifest.json +++ b/homeassistant/components/reolink/manifest.json @@ -19,5 +19,5 @@ "iot_class": "local_push", "loggers": ["reolink_aio"], "quality_scale": "platinum", - "requirements": ["reolink-aio==0.15.0"] + "requirements": ["reolink-aio==0.15.1"] } diff --git a/homeassistant/components/reolink/number.py b/homeassistant/components/reolink/number.py index 1904cb7abbdc9e..e7575c207e9936 100644 --- a/homeassistant/components/reolink/number.py +++ b/homeassistant/components/reolink/number.py @@ -502,7 +502,7 @@ class ReolinkChimeNumberEntityDescription( ReolinkNumberEntityDescription( key="image_brightness", cmd_key="GetImage", - cmd_id=26, + cmd_id=[26, 78], translation_key="image_brightness", entity_category=EntityCategory.CONFIG, entity_registry_enabled_default=False, @@ -516,7 +516,7 @@ class ReolinkChimeNumberEntityDescription( ReolinkNumberEntityDescription( key="image_contrast", cmd_key="GetImage", - cmd_id=26, + cmd_id=[26, 78], translation_key="image_contrast", entity_category=EntityCategory.CONFIG, entity_registry_enabled_default=False, @@ -530,7 +530,7 @@ class ReolinkChimeNumberEntityDescription( ReolinkNumberEntityDescription( key="image_saturation", cmd_key="GetImage", - cmd_id=26, + cmd_id=[26, 78], translation_key="image_saturation", entity_category=EntityCategory.CONFIG, entity_registry_enabled_default=False, @@ -544,7 +544,7 @@ class ReolinkChimeNumberEntityDescription( ReolinkNumberEntityDescription( key="image_sharpness", cmd_key="GetImage", - cmd_id=26, + cmd_id=[26, 78], translation_key="image_sharpness", entity_category=EntityCategory.CONFIG, entity_registry_enabled_default=False, @@ -558,7 +558,7 @@ class ReolinkChimeNumberEntityDescription( ReolinkNumberEntityDescription( key="image_hue", cmd_key="GetImage", - cmd_id=26, + cmd_id=[26, 78], translation_key="image_hue", entity_category=EntityCategory.CONFIG, entity_registry_enabled_default=False, diff --git a/homeassistant/components/reolink/sensor.py b/homeassistant/components/reolink/sensor.py index 9b9a78c8ce7771..d832bf10e28e90 100644 --- a/homeassistant/components/reolink/sensor.py +++ b/homeassistant/components/reolink/sensor.py @@ -140,6 +140,7 @@ class ReolinkHostSensorEntityDescription( HOST_SENSORS = ( ReolinkHostSensorEntityDescription( key="wifi_signal", + cmd_id=464, cmd_key="115", translation_key="wifi_signal", device_class=SensorDeviceClass.SIGNAL_STRENGTH, diff --git a/homeassistant/components/universal/media_player.py b/homeassistant/components/universal/media_player.py index 25188eb3a5dcb6..47079a1eef59ea 100644 --- a/homeassistant/components/universal/media_player.py +++ b/homeassistant/components/universal/media_player.py @@ -365,7 +365,7 @@ def media_duration(self): @property def media_image_url(self): """Image url of current playing media.""" - return self._child_attr(ATTR_ENTITY_PICTURE) + return self._override_or_child_attr(ATTR_ENTITY_PICTURE) @property def entity_picture(self): diff --git a/homeassistant/helpers/target.py b/homeassistant/helpers/target.py index 0ccc4e2cec3064..79e84a2dccf536 100644 --- a/homeassistant/helpers/target.py +++ b/homeassistant/helpers/target.py @@ -96,10 +96,10 @@ def has_any_selector(self) -> bool: class SelectedEntities: """Class to hold the selected entities.""" - # Entities that were explicitly mentioned. + # Entity IDs of entities that were explicitly mentioned. referenced: set[str] = dataclasses.field(default_factory=set) - # Entities that were referenced via device/area/floor/label ID. + # Entity IDs of entities that were referenced via device/area/floor/label ID. # Should not trigger a warning when they don't exist. indirectly_referenced: set[str] = dataclasses.field(default_factory=set) diff --git a/homeassistant/util/unit_conversion.py b/homeassistant/util/unit_conversion.py index 75e515cd95c5d3..be4372573f1b34 100644 --- a/homeassistant/util/unit_conversion.py +++ b/homeassistant/util/unit_conversion.py @@ -174,7 +174,9 @@ class CarbonMonoxideConcentrationConverter(BaseUnitConverter): UNIT_CLASS = "carbon_monoxide" _UNIT_CONVERSION: dict[str | None, float] = { CONCENTRATION_PARTS_PER_MILLION: 1, - CONCENTRATION_MILLIGRAMS_PER_CUBIC_METER: 1.145609, + # concentration (mg/m3) = 0.0409 x concentration (ppm) x molecular weight + # Carbon monoxide molecular weight: 28.01 g/mol + CONCENTRATION_MILLIGRAMS_PER_CUBIC_METER: 0.0409 * 28.01, } VALID_UNITS = { CONCENTRATION_PARTS_PER_MILLION, diff --git a/requirements_all.txt b/requirements_all.txt index f10af188d66065..5c595ccc5adf42 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1634,7 +1634,7 @@ openwrt-luci-rpc==1.1.17 openwrt-ubus-rpc==0.0.2 # homeassistant.components.opower -opower==0.15.4 +opower==0.15.5 # homeassistant.components.oralb oralb-ble==0.17.6 @@ -2682,7 +2682,7 @@ renault-api==0.4.0 renson-endura-delta==1.7.2 # homeassistant.components.reolink -reolink-aio==0.15.0 +reolink-aio==0.15.1 # homeassistant.components.idteck_prox rfk101py==0.0.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index fb3aa461fb5063..e73818fee6345a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1393,7 +1393,7 @@ openhomedevice==2.2.0 openwebifpy==4.3.1 # homeassistant.components.opower -opower==0.15.4 +opower==0.15.5 # homeassistant.components.oralb oralb-ble==0.17.6 @@ -2231,7 +2231,7 @@ renault-api==0.4.0 renson-endura-delta==1.7.2 # homeassistant.components.reolink -reolink-aio==0.15.0 +reolink-aio==0.15.1 # homeassistant.components.rflink rflink==0.0.67 diff --git a/tests/components/homeassistant_hardware/test_config_flow.py b/tests/components/homeassistant_hardware/test_config_flow.py index 5268a0d143753a..4040386562dd18 100644 --- a/tests/components/homeassistant_hardware/test_config_flow.py +++ b/tests/components/homeassistant_hardware/test_config_flow.py @@ -976,3 +976,166 @@ async def test_options_flow_thread_to_zigbee(hass: HomeAssistant) -> None: # The firmware type has been updated assert config_entry.data["firmware"] == "ezsp" + + +async def test_config_flow_pick_firmware_shows_migrate_options_with_existing_zha( + hass: HomeAssistant, +) -> None: + """Test that migrate options are shown when ZHA entries exist.""" + # Create a ZHA config entry + zha_entry = MockConfigEntry( + domain="zha", + data={"device": {"path": "/dev/ttyUSB1"}}, + title="ZHA", + ) + zha_entry.add_to_hass(hass) + + init_result = await hass.config_entries.flow.async_init( + TEST_DOMAIN, context={"source": "hardware"} + ) + + assert init_result["type"] is FlowResultType.MENU + assert init_result["step_id"] == "pick_firmware" + + # Should show migrate option for Zigbee since ZHA exists (migrating from ZHA to Zigbee) + menu_options = init_result["menu_options"] + assert "pick_firmware_zigbee_migrate" in menu_options + assert "pick_firmware_thread" in menu_options # Normal option for Thread + + +async def test_config_flow_pick_firmware_shows_migrate_options_with_existing_otbr( + hass: HomeAssistant, +) -> None: + """Test that migrate options are shown when OTBR entries exist.""" + # Create an OTBR config entry + otbr_entry = MockConfigEntry( + domain="otbr", + data={"url": "http://192.168.1.100:8081"}, + title="OpenThread Border Router", + ) + otbr_entry.add_to_hass(hass) + + init_result = await hass.config_entries.flow.async_init( + TEST_DOMAIN, context={"source": "hardware"} + ) + + assert init_result["type"] is FlowResultType.MENU + assert init_result["step_id"] == "pick_firmware" + + # Should show migrate option for Thread since OTBR exists (migrating from OTBR to Thread) + menu_options = init_result["menu_options"] + assert "pick_firmware_thread_migrate" in menu_options + assert "pick_firmware_zigbee" in menu_options # Normal option for Zigbee + + +async def test_config_flow_pick_firmware_shows_migrate_options_with_both_existing( + hass: HomeAssistant, +) -> None: + """Test that migrate options are shown when both ZHA and OTBR entries exist.""" + # Create both ZHA and OTBR config entries + zha_entry = MockConfigEntry( + domain="zha", + data={"device": {"path": "/dev/ttyUSB1"}}, + title="ZHA", + ) + zha_entry.add_to_hass(hass) + + otbr_entry = MockConfigEntry( + domain="otbr", + data={"url": "http://192.168.1.100:8081"}, + title="OpenThread Border Router", + ) + otbr_entry.add_to_hass(hass) + + init_result = await hass.config_entries.flow.async_init( + TEST_DOMAIN, context={"source": "hardware"} + ) + + assert init_result["type"] is FlowResultType.MENU + assert init_result["step_id"] == "pick_firmware" + + # Should show migrate options for both since both exist + menu_options = init_result["menu_options"] + assert "pick_firmware_zigbee_migrate" in menu_options + assert "pick_firmware_thread_migrate" in menu_options + + +async def test_config_flow_pick_firmware_shows_normal_options_without_existing( + hass: HomeAssistant, +) -> None: + """Test that normal options are shown when no ZHA or OTBR entries exist.""" + init_result = await hass.config_entries.flow.async_init( + TEST_DOMAIN, context={"source": "hardware"} + ) + + assert init_result["type"] is FlowResultType.MENU + assert init_result["step_id"] == "pick_firmware" + + # Should show normal options since no existing entries + menu_options = init_result["menu_options"] + assert "pick_firmware_zigbee" in menu_options + assert "pick_firmware_thread" in menu_options + assert "pick_firmware_zigbee_migrate" not in menu_options + assert "pick_firmware_thread_migrate" not in menu_options + + +async def test_config_flow_zigbee_migrate_handler(hass: HomeAssistant) -> None: + """Test that the Zigbee migrate handler works correctly.""" + # Ensure Zigbee migrate option is available by adding a ZHA entry + zha_entry = MockConfigEntry( + domain="zha", + data={"device": {"path": "/dev/ttyUSB1"}}, + title="ZHA", + ) + zha_entry.add_to_hass(hass) + + init_result = await hass.config_entries.flow.async_init( + TEST_DOMAIN, context={"source": "hardware"} + ) + + with mock_firmware_info( + hass, + probe_app_type=ApplicationType.SPINEL, + flash_app_type=ApplicationType.EZSP, + ): + # Test the migrate handler directly + result = await hass.config_entries.flow.async_configure( + init_result["flow_id"], + user_input={"next_step_id": "pick_firmware_zigbee_migrate"}, + ) + + # Should proceed to zigbee installation type (same as normal zigbee flow) + assert result["type"] is FlowResultType.MENU + assert result["step_id"] == "zigbee_installation_type" + + +@pytest.mark.usefixtures("addon_store_info") +async def test_config_flow_thread_migrate_handler(hass: HomeAssistant) -> None: + """Test that the Thread migrate handler works correctly.""" + # Ensure Thread migrate option is available by adding an OTBR entry + otbr_entry = MockConfigEntry( + domain="otbr", + data={"url": "http://192.168.1.100:8081"}, + title="OpenThread Border Router", + ) + otbr_entry.add_to_hass(hass) + + init_result = await hass.config_entries.flow.async_init( + TEST_DOMAIN, context={"source": "hardware"} + ) + + with mock_firmware_info( + hass, + probe_app_type=ApplicationType.EZSP, + flash_app_type=ApplicationType.SPINEL, + ) as (_, _): + # Test the migrate handler directly + result = await hass.config_entries.flow.async_configure( + init_result["flow_id"], + user_input={"next_step_id": "pick_firmware_thread_migrate"}, + ) + + # Should proceed to OTBR addon installation (same as normal thread flow) + assert result["type"] is FlowResultType.SHOW_PROGRESS + assert result["progress_action"] == "install_addon" + assert result["step_id"] == "install_otbr_addon" diff --git a/tests/components/systemmonitor/test_diagnostics.py b/tests/components/systemmonitor/test_diagnostics.py index f9bde984399979..fa4376fc13ff69 100644 --- a/tests/components/systemmonitor/test_diagnostics.py +++ b/tests/components/systemmonitor/test_diagnostics.py @@ -2,7 +2,7 @@ from unittest.mock import Mock -from freezegun.api import FrozenDateTimeFactory +import pytest from syrupy.assertion import SnapshotAssertion from syrupy.filters import props @@ -13,6 +13,7 @@ from tests.typing import ClientSessionGenerator +@pytest.mark.freeze_time("2024-02-24 15:00:00", tz_offset=0) async def test_diagnostics( hass: HomeAssistant, hass_client: ClientSessionGenerator, @@ -27,6 +28,7 @@ async def test_diagnostics( ) == snapshot(exclude=props("last_update", "entry_id", "created_at", "modified_at")) +@pytest.mark.freeze_time("2024-02-24 15:00:00", tz_offset=0) async def test_diagnostics_missing_items( hass: HomeAssistant, hass_client: ClientSessionGenerator, @@ -34,7 +36,6 @@ async def test_diagnostics_missing_items( mock_os: Mock, mock_config_entry: MockConfigEntry, snapshot: SnapshotAssertion, - freezer: FrozenDateTimeFactory, ) -> None: """Test diagnostics.""" mock_psutil.net_if_addrs.return_value = None diff --git a/tests/components/universal/fixtures/configuration.yaml b/tests/components/universal/fixtures/configuration.yaml index c3e445615f19ba..2614c9b27fdc01 100644 --- a/tests/components/universal/fixtures/configuration.yaml +++ b/tests/components/universal/fixtures/configuration.yaml @@ -7,3 +7,4 @@ media_player: state: remote.alexander_master_bedroom source_list: remote.alexander_master_bedroom|activity_list source: remote.alexander_master_bedroom|current_activity + entity_picture: remote.alexander_master_bedroom|entity_picture diff --git a/tests/components/universal/test_media_player.py b/tests/components/universal/test_media_player.py index 1418a5b7dacbe4..c04145ad25fec4 100644 --- a/tests/components/universal/test_media_player.py +++ b/tests/components/universal/test_media_player.py @@ -266,6 +266,8 @@ async def mock_states(hass: HomeAssistant) -> Mock: result.mock_repeat_switch_id = switch.ENTITY_ID_FORMAT.format("repeat") hass.states.async_set(result.mock_repeat_switch_id, STATE_OFF) + result.mock_media_image_url_id = f"{input_select.DOMAIN}.entity_picture" + hass.states.async_set(result.mock_media_image_url_id, "/local/picture.png") return result @@ -289,6 +291,7 @@ def config_children_and_attr(mock_states): "repeat": mock_states.mock_repeat_switch_id, "sound_mode_list": mock_states.mock_sound_mode_list_id, "sound_mode": mock_states.mock_sound_mode_id, + "entity_picture": mock_states.mock_media_image_url_id, }, } @@ -598,6 +601,22 @@ async def test_sound_mode_list_children_and_attr( assert ump.sound_mode_list == "['music', 'movie', 'game']" +async def test_entity_picture_children_and_attr( + hass: HomeAssistant, config_children_and_attr, mock_states +) -> None: + """Test entity picture property w/ children and attrs.""" + config = validate_config(config_children_and_attr) + + ump = universal.UniversalMediaPlayer(hass, config) + + assert ump.entity_picture == "/local/picture.png" + + hass.states.async_set( + mock_states.mock_sound_mode_list_id, "/local/other_picture.png" + ) + assert ump.sound_mode_list == "/local/other_picture.png" + + async def test_source_list_children_and_attr( hass: HomeAssistant, config_children_and_attr, mock_states ) -> None: @@ -774,6 +793,7 @@ async def test_overrides(hass: HomeAssistant, config_children_and_attr) -> None: "clear_playlist": excmd, "play_media": excmd, "toggle": excmd, + "entity_picture": excmd, } await async_setup_component(hass, "media_player", {"media_player": config}) await hass.async_block_till_done() @@ -1364,7 +1384,11 @@ async def test_reload(hass: HomeAssistant) -> None: hass.states.async_set( "remote.alexander_master_bedroom", STATE_ON, - {"activity_list": ["act1", "act2"], "current_activity": "act2"}, + { + "activity_list": ["act1", "act2"], + "current_activity": "act2", + "entity_picture": "/local/picture_remote.png", + }, ) yaml_path = get_fixture_path("configuration.yaml", "universal") @@ -1382,6 +1406,10 @@ async def test_reload(hass: HomeAssistant) -> None: assert hass.states.get("media_player.tv") is None assert hass.states.get("media_player.master_bed_tv").state == "on" assert hass.states.get("media_player.master_bed_tv").attributes["source"] == "act2" + assert ( + hass.states.get("media_player.master_bed_tv").attributes["entity_picture"] + == "/local/picture_remote.png" + ) assert ( "device_class" not in hass.states.get("media_player.master_bed_tv").attributes )