603 lines
19 KiB
Python
603 lines
19 KiB
Python
"""Test ZHA registries."""
|
|
from __future__ import annotations
|
|
|
|
import typing
|
|
from unittest import mock
|
|
|
|
import pytest
|
|
import zigpy.quirks as zigpy_quirks
|
|
|
|
from homeassistant.components.zha.binary_sensor import IASZone
|
|
from homeassistant.components.zha.core.const import ATTR_QUIRK_ID
|
|
import homeassistant.components.zha.core.registries as registries
|
|
from homeassistant.helpers import entity_registry as er
|
|
|
|
if typing.TYPE_CHECKING:
|
|
from homeassistant.components.zha.core.entity import ZhaEntity
|
|
|
|
MANUFACTURER = "mock manufacturer"
|
|
MODEL = "mock model"
|
|
QUIRK_CLASS = "mock.test.quirk.class"
|
|
QUIRK_ID = "quirk_id"
|
|
|
|
|
|
@pytest.fixture
|
|
def zha_device():
|
|
"""Return a mock of ZHA device."""
|
|
dev = mock.MagicMock()
|
|
dev.manufacturer = MANUFACTURER
|
|
dev.model = MODEL
|
|
dev.quirk_class = QUIRK_CLASS
|
|
dev.quirk_id = QUIRK_ID
|
|
return dev
|
|
|
|
|
|
@pytest.fixture
|
|
def cluster_handlers(cluster_handler):
|
|
"""Return a mock of cluster_handlers."""
|
|
|
|
return [cluster_handler("level", 8), cluster_handler("on_off", 6)]
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("rule", "matched"),
|
|
[
|
|
(registries.MatchRule(), False),
|
|
(registries.MatchRule(cluster_handler_names={"level"}), True),
|
|
(registries.MatchRule(cluster_handler_names={"level", "no match"}), False),
|
|
(registries.MatchRule(cluster_handler_names={"on_off"}), True),
|
|
(registries.MatchRule(cluster_handler_names={"on_off", "no match"}), False),
|
|
(registries.MatchRule(cluster_handler_names={"on_off", "level"}), True),
|
|
(
|
|
registries.MatchRule(cluster_handler_names={"on_off", "level", "no match"}),
|
|
False,
|
|
),
|
|
# test generic_id matching
|
|
(registries.MatchRule(generic_ids={"cluster_handler_0x0006"}), True),
|
|
(registries.MatchRule(generic_ids={"cluster_handler_0x0008"}), True),
|
|
(
|
|
registries.MatchRule(
|
|
generic_ids={"cluster_handler_0x0006", "cluster_handler_0x0008"}
|
|
),
|
|
True,
|
|
),
|
|
(
|
|
registries.MatchRule(
|
|
generic_ids={
|
|
"cluster_handler_0x0006",
|
|
"cluster_handler_0x0008",
|
|
"cluster_handler_0x0009",
|
|
}
|
|
),
|
|
False,
|
|
),
|
|
(
|
|
registries.MatchRule(
|
|
generic_ids={"cluster_handler_0x0006", "cluster_handler_0x0008"},
|
|
cluster_handler_names={"on_off", "level"},
|
|
),
|
|
True,
|
|
),
|
|
# manufacturer matching
|
|
(registries.MatchRule(manufacturers="no match"), False),
|
|
(registries.MatchRule(manufacturers=MANUFACTURER), True),
|
|
(
|
|
registries.MatchRule(
|
|
manufacturers="no match", aux_cluster_handlers="aux_cluster_handler"
|
|
),
|
|
False,
|
|
),
|
|
(
|
|
registries.MatchRule(
|
|
manufacturers=MANUFACTURER, aux_cluster_handlers="aux_cluster_handler"
|
|
),
|
|
True,
|
|
),
|
|
(registries.MatchRule(models=MODEL), True),
|
|
(registries.MatchRule(models="no match"), False),
|
|
(
|
|
registries.MatchRule(
|
|
models=MODEL, aux_cluster_handlers="aux_cluster_handler"
|
|
),
|
|
True,
|
|
),
|
|
(
|
|
registries.MatchRule(
|
|
models="no match", aux_cluster_handlers="aux_cluster_handler"
|
|
),
|
|
False,
|
|
),
|
|
(registries.MatchRule(quirk_ids=QUIRK_ID), True),
|
|
(registries.MatchRule(quirk_ids="no match"), False),
|
|
(
|
|
registries.MatchRule(
|
|
quirk_ids=QUIRK_ID, aux_cluster_handlers="aux_cluster_handler"
|
|
),
|
|
True,
|
|
),
|
|
(
|
|
registries.MatchRule(
|
|
quirk_ids="no match", aux_cluster_handlers="aux_cluster_handler"
|
|
),
|
|
False,
|
|
),
|
|
# match everything
|
|
(
|
|
registries.MatchRule(
|
|
generic_ids={"cluster_handler_0x0006", "cluster_handler_0x0008"},
|
|
cluster_handler_names={"on_off", "level"},
|
|
manufacturers=MANUFACTURER,
|
|
models=MODEL,
|
|
quirk_ids=QUIRK_ID,
|
|
),
|
|
True,
|
|
),
|
|
(
|
|
registries.MatchRule(
|
|
cluster_handler_names="on_off",
|
|
manufacturers={"random manuf", MANUFACTURER},
|
|
),
|
|
True,
|
|
),
|
|
(
|
|
registries.MatchRule(
|
|
cluster_handler_names="on_off",
|
|
manufacturers={"random manuf", "Another manuf"},
|
|
),
|
|
False,
|
|
),
|
|
(
|
|
registries.MatchRule(
|
|
cluster_handler_names="on_off",
|
|
manufacturers=lambda x: x == MANUFACTURER,
|
|
),
|
|
True,
|
|
),
|
|
(
|
|
registries.MatchRule(
|
|
cluster_handler_names="on_off",
|
|
manufacturers=lambda x: x != MANUFACTURER,
|
|
),
|
|
False,
|
|
),
|
|
(
|
|
registries.MatchRule(
|
|
cluster_handler_names="on_off", models={"random model", MODEL}
|
|
),
|
|
True,
|
|
),
|
|
(
|
|
registries.MatchRule(
|
|
cluster_handler_names="on_off", models={"random model", "Another model"}
|
|
),
|
|
False,
|
|
),
|
|
(
|
|
registries.MatchRule(
|
|
cluster_handler_names="on_off", models=lambda x: x == MODEL
|
|
),
|
|
True,
|
|
),
|
|
(
|
|
registries.MatchRule(
|
|
cluster_handler_names="on_off", models=lambda x: x != MODEL
|
|
),
|
|
False,
|
|
),
|
|
(
|
|
registries.MatchRule(
|
|
cluster_handler_names="on_off",
|
|
quirk_ids={"random quirk", QUIRK_ID},
|
|
),
|
|
True,
|
|
),
|
|
(
|
|
registries.MatchRule(
|
|
cluster_handler_names="on_off",
|
|
quirk_ids={"random quirk", "another quirk"},
|
|
),
|
|
False,
|
|
),
|
|
(
|
|
registries.MatchRule(
|
|
cluster_handler_names="on_off", quirk_ids=lambda x: x == QUIRK_ID
|
|
),
|
|
True,
|
|
),
|
|
(
|
|
registries.MatchRule(
|
|
cluster_handler_names="on_off", quirk_ids=lambda x: x != QUIRK_ID
|
|
),
|
|
False,
|
|
),
|
|
(
|
|
registries.MatchRule(cluster_handler_names="on_off", quirk_ids=QUIRK_ID),
|
|
True,
|
|
),
|
|
],
|
|
)
|
|
def test_registry_matching(rule, matched, cluster_handlers) -> None:
|
|
"""Test strict rule matching."""
|
|
assert (
|
|
rule.strict_matched(MANUFACTURER, MODEL, cluster_handlers, QUIRK_ID) is matched
|
|
)
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("rule", "matched"),
|
|
[
|
|
(registries.MatchRule(), False),
|
|
(registries.MatchRule(cluster_handler_names={"level"}), True),
|
|
(registries.MatchRule(cluster_handler_names={"level", "no match"}), False),
|
|
(registries.MatchRule(cluster_handler_names={"on_off"}), True),
|
|
(registries.MatchRule(cluster_handler_names={"on_off", "no match"}), False),
|
|
(registries.MatchRule(cluster_handler_names={"on_off", "level"}), True),
|
|
(
|
|
registries.MatchRule(cluster_handler_names={"on_off", "level", "no match"}),
|
|
False,
|
|
),
|
|
(
|
|
registries.MatchRule(
|
|
cluster_handler_names={"on_off", "level"}, models="no match"
|
|
),
|
|
True,
|
|
),
|
|
(
|
|
registries.MatchRule(
|
|
cluster_handler_names={"on_off", "level"},
|
|
models="no match",
|
|
manufacturers="no match",
|
|
),
|
|
True,
|
|
),
|
|
(
|
|
registries.MatchRule(
|
|
cluster_handler_names={"on_off", "level"},
|
|
models="no match",
|
|
manufacturers=MANUFACTURER,
|
|
),
|
|
True,
|
|
),
|
|
# test generic_id matching
|
|
(registries.MatchRule(generic_ids={"cluster_handler_0x0006"}), True),
|
|
(registries.MatchRule(generic_ids={"cluster_handler_0x0008"}), True),
|
|
(
|
|
registries.MatchRule(
|
|
generic_ids={"cluster_handler_0x0006", "cluster_handler_0x0008"}
|
|
),
|
|
True,
|
|
),
|
|
(
|
|
registries.MatchRule(
|
|
generic_ids={
|
|
"cluster_handler_0x0006",
|
|
"cluster_handler_0x0008",
|
|
"cluster_handler_0x0009",
|
|
}
|
|
),
|
|
False,
|
|
),
|
|
(
|
|
registries.MatchRule(
|
|
generic_ids={
|
|
"cluster_handler_0x0006",
|
|
"cluster_handler_0x0008",
|
|
"cluster_handler_0x0009",
|
|
},
|
|
models="mo match",
|
|
),
|
|
False,
|
|
),
|
|
(
|
|
registries.MatchRule(
|
|
generic_ids={
|
|
"cluster_handler_0x0006",
|
|
"cluster_handler_0x0008",
|
|
"cluster_handler_0x0009",
|
|
},
|
|
models=MODEL,
|
|
),
|
|
True,
|
|
),
|
|
(
|
|
registries.MatchRule(
|
|
generic_ids={"cluster_handler_0x0006", "cluster_handler_0x0008"},
|
|
cluster_handler_names={"on_off", "level"},
|
|
),
|
|
True,
|
|
),
|
|
# manufacturer matching
|
|
(registries.MatchRule(manufacturers="no match"), False),
|
|
(registries.MatchRule(manufacturers=MANUFACTURER), True),
|
|
(registries.MatchRule(models=MODEL), True),
|
|
(registries.MatchRule(models="no match"), False),
|
|
(registries.MatchRule(quirk_ids=QUIRK_ID), True),
|
|
(registries.MatchRule(quirk_ids="no match"), False),
|
|
# match everything
|
|
(
|
|
registries.MatchRule(
|
|
generic_ids={"cluster_handler_0x0006", "cluster_handler_0x0008"},
|
|
cluster_handler_names={"on_off", "level"},
|
|
manufacturers=MANUFACTURER,
|
|
models=MODEL,
|
|
quirk_ids=QUIRK_ID,
|
|
),
|
|
True,
|
|
),
|
|
],
|
|
)
|
|
def test_registry_loose_matching(rule, matched, cluster_handlers) -> None:
|
|
"""Test loose rule matching."""
|
|
assert (
|
|
rule.loose_matched(MANUFACTURER, MODEL, cluster_handlers, QUIRK_ID) is matched
|
|
)
|
|
|
|
|
|
def test_match_rule_claim_cluster_handlers_color(cluster_handler) -> None:
|
|
"""Test cluster handler claiming."""
|
|
ch_color = cluster_handler("color", 0x300)
|
|
ch_level = cluster_handler("level", 8)
|
|
ch_onoff = cluster_handler("on_off", 6)
|
|
|
|
rule = registries.MatchRule(
|
|
cluster_handler_names="on_off", aux_cluster_handlers={"color", "level"}
|
|
)
|
|
claimed = rule.claim_cluster_handlers([ch_color, ch_level, ch_onoff])
|
|
assert {"color", "level", "on_off"} == {ch.name for ch in claimed}
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("rule", "match"),
|
|
[
|
|
(registries.MatchRule(cluster_handler_names={"level"}), {"level"}),
|
|
(registries.MatchRule(cluster_handler_names={"level", "no match"}), {"level"}),
|
|
(registries.MatchRule(cluster_handler_names={"on_off"}), {"on_off"}),
|
|
(registries.MatchRule(generic_ids="cluster_handler_0x0000"), {"basic"}),
|
|
(
|
|
registries.MatchRule(
|
|
cluster_handler_names="level", generic_ids="cluster_handler_0x0000"
|
|
),
|
|
{"basic", "level"},
|
|
),
|
|
(
|
|
registries.MatchRule(cluster_handler_names={"level", "power"}),
|
|
{"level", "power"},
|
|
),
|
|
(
|
|
registries.MatchRule(
|
|
cluster_handler_names={"level", "on_off"},
|
|
aux_cluster_handlers={"basic", "power"},
|
|
),
|
|
{"basic", "level", "on_off", "power"},
|
|
),
|
|
(registries.MatchRule(cluster_handler_names={"color"}), set()),
|
|
],
|
|
)
|
|
def test_match_rule_claim_cluster_handlers(
|
|
rule, match, cluster_handler, cluster_handlers
|
|
) -> None:
|
|
"""Test cluster handler claiming."""
|
|
ch_basic = cluster_handler("basic", 0)
|
|
cluster_handlers.append(ch_basic)
|
|
ch_power = cluster_handler("power", 1)
|
|
cluster_handlers.append(ch_power)
|
|
|
|
claimed = rule.claim_cluster_handlers(cluster_handlers)
|
|
assert match == {ch.name for ch in claimed}
|
|
|
|
|
|
@pytest.fixture
|
|
def entity_registry():
|
|
"""Registry fixture."""
|
|
return registries.ZHAEntityRegistry()
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("manufacturer", "model", "quirk_id", "match_name"),
|
|
(
|
|
("random manufacturer", "random model", "random.class", "OnOff"),
|
|
("random manufacturer", MODEL, "random.class", "OnOffModel"),
|
|
(MANUFACTURER, "random model", "random.class", "OnOffManufacturer"),
|
|
("random manufacturer", "random model", QUIRK_ID, "OnOffQuirk"),
|
|
(MANUFACTURER, MODEL, "random.class", "OnOffModelManufacturer"),
|
|
(MANUFACTURER, "some model", "random.class", "OnOffMultimodel"),
|
|
),
|
|
)
|
|
def test_weighted_match(
|
|
cluster_handler,
|
|
entity_registry: er.EntityRegistry,
|
|
manufacturer,
|
|
model,
|
|
quirk_id,
|
|
match_name,
|
|
) -> None:
|
|
"""Test weightedd match."""
|
|
|
|
s = mock.sentinel
|
|
|
|
@entity_registry.strict_match(
|
|
s.component,
|
|
cluster_handler_names="on_off",
|
|
models={MODEL, "another model", "some model"},
|
|
)
|
|
class OnOffMultimodel:
|
|
pass
|
|
|
|
@entity_registry.strict_match(s.component, cluster_handler_names="on_off")
|
|
class OnOff:
|
|
pass
|
|
|
|
@entity_registry.strict_match(
|
|
s.component, cluster_handler_names="on_off", manufacturers=MANUFACTURER
|
|
)
|
|
class OnOffManufacturer:
|
|
pass
|
|
|
|
@entity_registry.strict_match(
|
|
s.component, cluster_handler_names="on_off", models=MODEL
|
|
)
|
|
class OnOffModel:
|
|
pass
|
|
|
|
@entity_registry.strict_match(
|
|
s.component,
|
|
cluster_handler_names="on_off",
|
|
models=MODEL,
|
|
manufacturers=MANUFACTURER,
|
|
)
|
|
class OnOffModelManufacturer:
|
|
pass
|
|
|
|
@entity_registry.strict_match(
|
|
s.component, cluster_handler_names="on_off", quirk_ids=QUIRK_ID
|
|
)
|
|
class OnOffQuirk:
|
|
pass
|
|
|
|
ch_on_off = cluster_handler("on_off", 6)
|
|
ch_level = cluster_handler("level", 8)
|
|
|
|
match, claimed = entity_registry.get_entity(
|
|
s.component, manufacturer, model, [ch_on_off, ch_level], quirk_id
|
|
)
|
|
|
|
assert match.__name__ == match_name
|
|
assert claimed == [ch_on_off]
|
|
|
|
|
|
def test_multi_sensor_match(
|
|
cluster_handler, entity_registry: er.EntityRegistry
|
|
) -> None:
|
|
"""Test multi-entity match."""
|
|
|
|
s = mock.sentinel
|
|
|
|
@entity_registry.multipass_match(
|
|
s.binary_sensor,
|
|
cluster_handler_names="smartenergy_metering",
|
|
)
|
|
class SmartEnergySensor2:
|
|
pass
|
|
|
|
ch_se = cluster_handler("smartenergy_metering", 0x0702)
|
|
ch_illuminati = cluster_handler("illuminance", 0x0401)
|
|
|
|
match, claimed = entity_registry.get_multi_entity(
|
|
"manufacturer",
|
|
"model",
|
|
cluster_handlers=[ch_se, ch_illuminati],
|
|
quirk_id="quirk_id",
|
|
)
|
|
|
|
assert s.binary_sensor in match
|
|
assert s.component not in match
|
|
assert set(claimed) == {ch_se}
|
|
assert {cls.entity_class.__name__ for cls in match[s.binary_sensor]} == {
|
|
SmartEnergySensor2.__name__
|
|
}
|
|
|
|
@entity_registry.multipass_match(
|
|
s.component,
|
|
cluster_handler_names="smartenergy_metering",
|
|
aux_cluster_handlers="illuminance",
|
|
)
|
|
class SmartEnergySensor1:
|
|
pass
|
|
|
|
@entity_registry.multipass_match(
|
|
s.binary_sensor,
|
|
cluster_handler_names="smartenergy_metering",
|
|
aux_cluster_handlers="illuminance",
|
|
)
|
|
class SmartEnergySensor3:
|
|
pass
|
|
|
|
match, claimed = entity_registry.get_multi_entity(
|
|
"manufacturer",
|
|
"model",
|
|
cluster_handlers={ch_se, ch_illuminati},
|
|
quirk_id="quirk_id",
|
|
)
|
|
|
|
assert s.binary_sensor in match
|
|
assert s.component in match
|
|
assert set(claimed) == {ch_se, ch_illuminati}
|
|
assert {cls.entity_class.__name__ for cls in match[s.binary_sensor]} == {
|
|
SmartEnergySensor2.__name__,
|
|
SmartEnergySensor3.__name__,
|
|
}
|
|
assert {cls.entity_class.__name__ for cls in match[s.component]} == {
|
|
SmartEnergySensor1.__name__
|
|
}
|
|
|
|
|
|
def iter_all_rules() -> typing.Iterable[registries.MatchRule, list[type[ZhaEntity]]]:
|
|
"""Iterate over all match rules and their corresponding entities."""
|
|
|
|
for rules in registries.ZHA_ENTITIES._strict_registry.values():
|
|
for rule, entity in rules.items():
|
|
yield rule, [entity]
|
|
|
|
for rules in registries.ZHA_ENTITIES._multi_entity_registry.values():
|
|
for multi in rules.values():
|
|
for rule, entities in multi.items():
|
|
yield rule, entities
|
|
|
|
for rules in registries.ZHA_ENTITIES._config_diagnostic_entity_registry.values():
|
|
for multi in rules.values():
|
|
for rule, entities in multi.items():
|
|
yield rule, entities
|
|
|
|
|
|
def test_quirk_classes() -> None:
|
|
"""Make sure that all quirk IDs in components matches exist."""
|
|
|
|
def quirk_class_validator(value):
|
|
"""Validate quirk IDs during self test."""
|
|
if callable(value):
|
|
# Callables cannot be tested
|
|
return
|
|
|
|
if isinstance(value, (frozenset, set, list)):
|
|
for v in value:
|
|
# Unpack the value if needed
|
|
quirk_class_validator(v)
|
|
return
|
|
|
|
if value not in all_quirk_ids:
|
|
raise ValueError(f"Quirk ID '{value}' does not exist.")
|
|
|
|
# get all quirk ID from zigpy quirks registry
|
|
all_quirk_ids = []
|
|
for manufacturer in zigpy_quirks._DEVICE_REGISTRY._registry.values():
|
|
for model_quirk_list in manufacturer.values():
|
|
for quirk in model_quirk_list:
|
|
quirk_id = getattr(quirk, ATTR_QUIRK_ID, None)
|
|
if quirk_id is not None and quirk_id not in all_quirk_ids:
|
|
all_quirk_ids.append(quirk_id)
|
|
del quirk, model_quirk_list, manufacturer
|
|
|
|
# validate all quirk IDs used in component match rules
|
|
for rule, _ in iter_all_rules():
|
|
quirk_class_validator(rule.quirk_ids)
|
|
|
|
|
|
def test_entity_names() -> None:
|
|
"""Make sure that all handlers expose entities with valid names."""
|
|
|
|
for _, entities in iter_all_rules():
|
|
for entity in entities:
|
|
if hasattr(entity, "_attr_name"):
|
|
# The entity has a name
|
|
assert isinstance(entity._attr_name, str) and entity._attr_name
|
|
elif hasattr(entity, "_attr_translation_key"):
|
|
assert (
|
|
isinstance(entity._attr_translation_key, str)
|
|
and entity._attr_translation_key
|
|
)
|
|
elif hasattr(entity, "_attr_device_class"):
|
|
assert entity._attr_device_class
|
|
else:
|
|
# The only exception (for now) is IASZone
|
|
assert entity is IASZone
|