diff --git a/custom_components/powercalc/discovery.py b/custom_components/powercalc/discovery.py index 3025d8601..ee167d7a1 100644 --- a/custom_components/powercalc/discovery.py +++ b/custom_components/powercalc/discovery.py @@ -194,13 +194,15 @@ async def autodiscover_model(self, entity_entry: er.RegistryEntry | None) -> Mod model_info = ModelInfo( model_info.manufacturer, model_info.model.replace("/", "#slash#"), + model_info.model_id, ) _LOGGER.debug( - "%s: Auto discovered model (manufacturer=%s, model=%s)", + "%s: Auto discovered model (manufacturer=%s, model=%s, model_id=%s)", entity_entry.entity_id, model_info.manufacturer, model_info.model, + model_info.model_id, ) return model_info @@ -215,11 +217,12 @@ async def get_model_information(self, entity_entry: er.RegistryEntry) -> ModelIn manufacturer = str(device_entry.manufacturer) model = str(device_entry.model) + model_id = device_entry.model_id if hasattr(device_entry, "model_id") else None if len(manufacturer) == 0 or len(model) == 0: return None - return ModelInfo(manufacturer, model) + return ModelInfo(manufacturer, model, model_id) @callback def _init_entity_discovery( diff --git a/custom_components/powercalc/power_profile/factory.py b/custom_components/powercalc/power_profile/factory.py index 099fa9b04..be47a399f 100644 --- a/custom_components/powercalc/power_profile/factory.py +++ b/custom_components/powercalc/power_profile/factory.py @@ -22,9 +22,11 @@ async def get_power_profile( ) -> PowerProfile | None: manufacturer = config.get(CONF_MANUFACTURER) model = config.get(CONF_MODEL) + model_id = None if (manufacturer is None or model is None) and model_info: manufacturer = config.get(CONF_MANUFACTURER) or model_info.manufacturer model = config.get(CONF_MODEL) or model_info.model + model_id = model_info.model_id if not manufacturer or not model: return None @@ -38,7 +40,7 @@ async def get_power_profile( library = await ProfileLibrary.factory(hass) profile = await library.get_profile( - ModelInfo(manufacturer, model), + ModelInfo(manufacturer, model, model_id), custom_model_directory, ) if profile is None: diff --git a/custom_components/powercalc/power_profile/library.py b/custom_components/powercalc/power_profile/library.py index e6d1b2db6..746047568 100644 --- a/custom_components/powercalc/power_profile/library.py +++ b/custom_components/powercalc/power_profile/library.py @@ -98,7 +98,7 @@ async def get_profile( sub_profile = None if "/" in model_info.model: (model, sub_profile) = model_info.model.split("/", 1) - model_info = ModelInfo(model_info.manufacturer, model) + model_info = ModelInfo(model_info.manufacturer, model, model_info.model_id) profile = await self.create_power_profile(model_info, custom_directory) @@ -125,9 +125,14 @@ async def create_power_profile( if not resolved_manufacturer: return None - resolved_model: str | None = model_info.model + resolved_model: str | None = model_info.model_id or model_info.model if not custom_directory: - resolved_model = await self.find_model(resolved_manufacturer, model_info.model) + for model_identifier in (model_info.model_id, model_info.model): + if not model_identifier: + continue + resolved_model = await self.find_model(resolved_manufacturer, model_identifier) + if resolved_model: + break if not resolved_model: return None @@ -187,3 +192,5 @@ def get_loader(self) -> Loader: class ModelInfo(NamedTuple): manufacturer: str model: str + # Starting from HA 2024.8 we can use model_id to identify the model + model_id: str | None = None diff --git a/tests/conftest.py b/tests/conftest.py index 7fec4d1b2..cf2f3f3d7 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -105,6 +105,7 @@ def __call__( entity_id: str, manufacturer: str = "signify", model: str = "LCT010", + model_id: str | None = None, **entity_reg_kwargs: Any, # noqa: ANN401 ) -> None: ... @@ -115,6 +116,7 @@ def _mock_entity_with_model_information( entity_id: str, manufacturer: str = "signify", model: str = "LCT010", + model_id: str | None = None, **entity_reg_kwargs: Any, # noqa: ANN401 ) -> None: device_id = str(uuid.uuid4()) @@ -151,6 +153,7 @@ def _mock_entity_with_model_information( id=device_id, manufacturer=manufacturer, model=model, + model_id=model_id, ), }, ) diff --git a/tests/power_profile/test_library.py b/tests/power_profile/test_library.py index 83f3f240e..d40e54189 100644 --- a/tests/power_profile/test_library.py +++ b/tests/power_profile/test_library.py @@ -58,37 +58,28 @@ async def test_non_existing_manufacturer_returns_empty_model_list( assert not await library.get_model_listing("foo") -async def test_get_profile(hass: HomeAssistant) -> None: - library = await ProfileLibrary.factory(hass) - profile = await library.get_profile(ModelInfo("signify", "LCT010")) - assert profile - assert profile.manufacturer == "signify" - assert profile.model == "LCT010" - assert profile.get_model_directory().endswith("signify/LCT010") - - -async def test_get_profile_with_full_model_name(hass: HomeAssistant) -> None: - library = await ProfileLibrary.factory(hass) - profile = await library.get_profile(ModelInfo("signify", "LCA001")) - assert profile - assert profile.manufacturer == "signify" - assert profile.get_model_directory().endswith("signify/LCA001") - - -async def test_get_profile_with_full_manufacturer_name(hass: HomeAssistant) -> None: +@pytest.mark.parametrize( + "model_info,expected_manufacturer,expected_model", + [ + (ModelInfo("signify", "LCT010"), "signify", "LCT010"), + (ModelInfo("signify", "LCA001"), "signify", "LCA001"), + (ModelInfo("signify", "Hue go (LLC020)"), "signify", "LLC020"), + (ModelInfo("ikea", "TRADFRI bulb E14 WS opal 400lm"), "ikea", "LED1536G5"), + (ModelInfo("signify", "Hue go", "LLC020"), "signify", "LLC020"), + ], +) +async def test_get_profile( + hass: HomeAssistant, + model_info: ModelInfo, + expected_manufacturer: str, + expected_model: str, +) -> None: library = await ProfileLibrary.factory(hass) - profile = await library.get_profile(ModelInfo("signify", "Hue go (LLC020)")) + profile = await library.get_profile(model_info) assert profile - assert profile.manufacturer == "signify" - assert profile.get_model_directory().endswith("signify/LLC020") - - -async def test_get_profile_with_model_alias(hass: HomeAssistant) -> None: - library = await ProfileLibrary.factory(hass) - profile = await library.get_profile( - ModelInfo("ikea", "TRADFRI bulb E14 WS opal 400lm"), - ) - assert profile.get_model_directory().endswith("ikea/LED1536G5") + assert profile.manufacturer == expected_manufacturer + assert profile.model == expected_model + assert profile.get_model_directory().endswith(f"{expected_manufacturer}/{expected_model}") async def test_get_non_existing_profile(hass: HomeAssistant) -> None: diff --git a/tests/test_discovery.py b/tests/test_discovery.py index 0693b3a67..97a3ca015 100644 --- a/tests/test_discovery.py +++ b/tests/test_discovery.py @@ -288,36 +288,53 @@ async def test_load_model_with_slashes( @pytest.mark.parametrize( - "manufacturer,model,expected_manufacturer,expected_model", + "model_info,expected_manufacturer,expected_model", [ ( - "ikea", - "IKEA FLOALT LED light panel, dimmable, white spectrum (30x90 cm) (L1528)", + ModelInfo("ikea", "IKEA FLOALT LED light panel, dimmable, white spectrum (30x90 cm) (L1528)"), "ikea", "L1528", ), - ("IKEA", "LED1649C5", "ikea", "LED1649C5"), ( - "IKEA", - "TRADFRI LED bulb GU10 400 lumen, dimmable (LED1650R5)", + ModelInfo("IKEA", "LED1649C5"), + "ikea", + "LED1649C5", + ), + ( + ModelInfo("IKEA", "TRADFRI LED bulb GU10 400 lumen, dimmable (LED1650R5)"), "ikea", "LED1650R5", ), ( + ModelInfo("ikea", "TRADFRI bulb E14 W op/ch 400lm"), "ikea", - "TRADFRI bulb E14 W op/ch 400lm", + "LED1649C5", + ), + ( + ModelInfo("MLI", "45317"), + "mueller-licht", + "45317", + ), + ( + ModelInfo("TP-Link", "KP115(AU)"), + "tp-link", + "KP115", + ), + ( + ModelInfo("Apple", "HomePod (gen 2)"), + "apple", + "MQJ83", + ), + ( + ModelInfo("IKEA", "bladiebla", "LED1649C5"), "ikea", "LED1649C5", ), - ("MLI", 45317, "mueller-licht", "45317"), - ("TP-Link", "KP115(AU)", "tp-link", "KP115"), - ("Apple", "HomePod (gen 2)", "apple", "MQJ83"), ], ) async def test_autodiscover_model_from_entity_entry( hass: HomeAssistant, - manufacturer: str, - model: str, + model_info: ModelInfo, expected_manufacturer: str, expected_model: str, mock_entity_with_model_information: MockEntityWithModel, @@ -326,7 +343,7 @@ async def test_autodiscover_model_from_entity_entry( Test the autodiscovery lookup from the library by manufacturer and model information A given entity_entry is trying to be matched in the library and a PowerProfile instance returned when it is matched """ - mock_entity_with_model_information("light.testa", manufacturer, model) + mock_entity_with_model_information("light.testa", model_info.manufacturer, model_info.model, model_info.model_id) source_entity = await create_source_entity("light.testa", hass) power_profile = await get_power_profile_by_source_entity(hass, source_entity) @@ -407,7 +424,7 @@ async def test_no_power_sensors_are_created_for_ignored_config_entries( device_id="a", ), DeviceEntry(id="a", manufacturer="foo", model="bar"), - ModelInfo("foo", "bar"), + ModelInfo("foo", "bar", None), ), ( RegistryEntry( @@ -419,6 +436,16 @@ async def test_no_power_sensors_are_created_for_ignored_config_entries( DeviceEntry(id="b", manufacturer="foo", model="bar"), None, ), + ( + RegistryEntry( + entity_id="switch.test", + unique_id=uuid.uuid4(), + platform="switch", + device_id="a", + ), + DeviceEntry(id="a", manufacturer="foo", model="bar", model_id="barry"), + ModelInfo("foo", "bar", "barry"), + ), ], ) async def test_get_model_information(