Skip to content

Commit

Permalink
Add support for device model_id added in HA 2024.8 (#2392)
Browse files Browse the repository at this point in the history
* feat: add support for device model_id added in HA 2024.8

* chore: add tests and parameterize

* fix: enable tests
  • Loading branch information
bramstroker authored Aug 16, 2024
1 parent 41f9918 commit 25af19a
Show file tree
Hide file tree
Showing 6 changed files with 82 additions and 49 deletions.
7 changes: 5 additions & 2 deletions custom_components/powercalc/discovery.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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(
Expand Down
4 changes: 3 additions & 1 deletion custom_components/powercalc/power_profile/factory.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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:
Expand Down
13 changes: 10 additions & 3 deletions custom_components/powercalc/power_profile/library.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand All @@ -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
Expand Down Expand Up @@ -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
3 changes: 3 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -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: ...

Expand All @@ -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())
Expand Down Expand Up @@ -151,6 +153,7 @@ def _mock_entity_with_model_information(
id=device_id,
manufacturer=manufacturer,
model=model,
model_id=model_id,
),
},
)
Expand Down
49 changes: 20 additions & 29 deletions tests/power_profile/test_library.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
55 changes: 41 additions & 14 deletions tests/test_discovery.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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)
Expand Down Expand Up @@ -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(
Expand All @@ -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(
Expand Down

0 comments on commit 25af19a

Please sign in to comment.