Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 28 additions & 9 deletions zha/application/platforms/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -142,16 +142,35 @@ def __post_init__(self) -> None:
)


def register_entity[T: type[PlatformEntity]](
cluster_id: ClusterId | int,
) -> Callable[[T], T]:
"""Register an entity class for discovery."""

def inner(cls: T) -> T:
ENTITY_REGISTRY[cluster_id].append(cls)
return cls
def register_entity[T: type[PlatformEntity]](cls: T) -> T:
"""Register an entity class for discovery.

The registry is keyed by cluster ID purely to speed up discovery lookups
(see `discover_entities_for_endpoint`): for each cluster on an endpoint we
only evaluate the entities filed under that cluster. The actual matching is
done entirely by the class's `_cluster_match`.

The index key is therefore not independent information — it is derived from
the match. Every mandatory cluster (`server_clusters | client_clusters`)
must be present for a match to succeed, so any single one of them is a
sufficient discovery key: if the entity can match at all, that cluster is on
the endpoint and the lookup finds it. We index under exactly one mandatory
cluster (preferring a server cluster) to keep each class in a single bucket,
so discovery evaluates it at most once per endpoint.
"""
match = cls._cluster_match
if match is None:
raise TypeError(f"{cls.__name__} has no `_cluster_match` to register")

mandatory = match.server_clusters or match.client_clusters
if not mandatory:
raise ValueError(
f"{cls.__name__} declares no mandatory clusters; it cannot be "
"discovered and must not use @register_entity"
)

return inner
ENTITY_REGISTRY[min(mandatory)].append(cls)
return cls


def register_group_entity(cls: type[GroupEntity]) -> type[GroupEntity]:
Expand Down
2 changes: 1 addition & 1 deletion zha/application/platforms/alarm_control_panel/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,7 @@ async def async_alarm_trigger(self, code: str | None = None) -> None:
"""Send alarm trigger command."""


@register_entity(AceCluster.cluster_id)
@register_entity
class AlarmControlPanel(BaseAlarmControlPanel):
"""Entity for ZHA alarm control devices."""

Expand Down
44 changes: 22 additions & 22 deletions zha/application/platforms/binary_sensor/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -186,7 +186,7 @@ def parse(value: bool | int) -> bool:
return bool(value)


@register_entity(SMARTTHINGS_ACCELERATION_CLUSTER)
@register_entity
class Accelerometer(BinarySensor):
"""ZHA BinarySensor."""

Expand Down Expand Up @@ -232,7 +232,7 @@ class Accelerometer(BinarySensor):
}


@register_entity(OccupancySensing.cluster_id)
@register_entity
class Occupancy(BinarySensor):
"""ZHA BinarySensor."""

Expand Down Expand Up @@ -266,7 +266,7 @@ class Occupancy(BinarySensor):
}


@register_entity(OnOff.cluster_id)
@register_entity
class Opening(BinarySensor):
"""ZHA OnOff BinarySensor."""

Expand Down Expand Up @@ -300,7 +300,7 @@ class Opening(BinarySensor):
)


@register_entity(BinaryInputCluster.cluster_id)
@register_entity
class BinaryInputWithDescription(BinarySensor):
"""ZHA BinarySensor."""

Expand Down Expand Up @@ -342,7 +342,7 @@ def _is_supported(self) -> bool:
return super()._is_supported()


@register_entity(BinaryInputCluster.cluster_id)
@register_entity
class BinaryInput(BinarySensor):
"""ZHA BinarySensor."""

Expand Down Expand Up @@ -382,7 +382,7 @@ def _is_supported(self) -> bool:
return super()._is_supported()


@register_entity(OnOff.cluster_id)
@register_entity
class IkeaMotion(BinarySensor):
"""ZHA OnOff BinarySensor with motion device class for IKEA devices."""

Expand All @@ -398,7 +398,7 @@ class IkeaMotion(BinarySensor):
)


@register_entity(OnOff.cluster_id)
@register_entity
class PhilipsMotion(BinarySensor):
"""ZHA OnOff BinarySensor with motion device class for Philips devices."""

Expand All @@ -414,7 +414,7 @@ class PhilipsMotion(BinarySensor):
)


@register_entity(IasZone.cluster_id)
@register_entity
class IASZone(BinarySensor):
"""ZHA IAS BinarySensor."""

Expand Down Expand Up @@ -478,7 +478,7 @@ async def async_update(self) -> None:
self.maybe_emit_state_changed_event()


@register_entity(IasZone.cluster_id)
@register_entity
class SinopeLeakStatus(BinarySensor):
"""Sinope water leak sensor."""

Expand All @@ -493,7 +493,7 @@ class SinopeLeakStatus(BinarySensor):
)


@register_entity(TUYA_MANUFACTURER_CLUSTER)
@register_entity
class FrostLock(BinarySensor):
"""ZHA BinarySensor."""

Expand All @@ -509,7 +509,7 @@ class FrostLock(BinarySensor):
)


@register_entity(IKEA_AIR_PURIFIER_CLUSTER)
@register_entity
class ReplaceFilter(BinarySensor):
"""ZHA BinarySensor."""

Expand All @@ -525,7 +525,7 @@ class ReplaceFilter(BinarySensor):
)


@register_entity(AQARA_OPPLE_CLUSTER)
@register_entity
class AqaraPetFeederErrorDetected(BinarySensor):
"""ZHA aqara pet feeder error detected binary sensor."""

Expand All @@ -540,7 +540,7 @@ class AqaraPetFeederErrorDetected(BinarySensor):
)


@register_entity(AQARA_OPPLE_CLUSTER)
@register_entity
class XiaomiPlugConsumerConnected(BinarySensor):
"""ZHA Xiaomi plug consumer connected binary sensor."""

Expand All @@ -556,7 +556,7 @@ class XiaomiPlugConsumerConnected(BinarySensor):
)


@register_entity(AQARA_OPPLE_CLUSTER)
@register_entity
class AqaraThermostatWindowOpen(BinarySensor):
"""ZHA Aqara thermostat window open binary sensor."""

Expand All @@ -571,7 +571,7 @@ class AqaraThermostatWindowOpen(BinarySensor):
)


@register_entity(AQARA_OPPLE_CLUSTER)
@register_entity
class AqaraThermostatValveAlarm(BinarySensor):
"""ZHA Aqara thermostat valve alarm binary sensor."""

Expand All @@ -587,7 +587,7 @@ class AqaraThermostatValveAlarm(BinarySensor):
)


@register_entity(AQARA_OPPLE_CLUSTER)
@register_entity
class AqaraThermostatCalibrated(BinarySensor):
"""ZHA Aqara thermostat calibrated binary sensor."""

Expand All @@ -603,7 +603,7 @@ class AqaraThermostatCalibrated(BinarySensor):
)


@register_entity(AQARA_OPPLE_CLUSTER)
@register_entity
class AqaraThermostatExternalSensor(BinarySensor):
"""ZHA Aqara thermostat external sensor binary sensor."""

Expand All @@ -619,7 +619,7 @@ class AqaraThermostatExternalSensor(BinarySensor):
)


@register_entity(AQARA_OPPLE_CLUSTER)
@register_entity
class AqaraLinkageAlarmState(BinarySensor):
"""ZHA Aqara linkage alarm state binary sensor."""

Expand All @@ -635,7 +635,7 @@ class AqaraLinkageAlarmState(BinarySensor):
)


@register_entity(AQARA_OPPLE_CLUSTER)
@register_entity
class AqaraE1CurtainMotorOpenedByHandBinarySensor(BinarySensor):
"""Opened by hand binary sensor."""

Expand All @@ -651,7 +651,7 @@ class AqaraE1CurtainMotorOpenedByHandBinarySensor(BinarySensor):
)


@register_entity(Thermostat.cluster_id)
@register_entity
class DanfossMountingModeActive(BinarySensor):
"""Danfoss TRV proprietary attribute exposing whether in mounting mode."""

Expand Down Expand Up @@ -682,7 +682,7 @@ class DanfossMountingModeActive(BinarySensor):
}


@register_entity(Thermostat.cluster_id)
@register_entity
class DanfossHeatRequired(BinarySensor):
"""Danfoss TRV proprietary attribute exposing whether heat is required."""

Expand Down Expand Up @@ -711,7 +711,7 @@ class DanfossHeatRequired(BinarySensor):
}


@register_entity(Thermostat.cluster_id)
@register_entity
class DanfossPreheatStatus(BinarySensor):
"""Danfoss TRV proprietary attribute exposing whether in pre-heating mode."""

Expand Down
10 changes: 5 additions & 5 deletions zha/application/platforms/button/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@ async def async_press(self) -> None:
await command(*arguments, **kwargs)


@register_entity(Identify.cluster_id)
@register_entity
class IdentifyButton(Button):
"""Defines a ZHA identify button."""

Expand Down Expand Up @@ -184,7 +184,7 @@ async def async_press(self) -> None:
)


@register_entity(TUYA_MANUFACTURER_CLUSTER)
@register_entity
class FrostLockResetButton(WriteAttributeButton):
"""Defines a ZHA frost lock reset button."""

Expand All @@ -202,7 +202,7 @@ class FrostLockResetButton(WriteAttributeButton):
)


@register_entity(AQARA_OPPLE_CLUSTER)
@register_entity
class NoPresenceStatusResetButton(WriteAttributeButton):
"""Defines a ZHA no presence status reset button."""

Expand All @@ -220,7 +220,7 @@ class NoPresenceStatusResetButton(WriteAttributeButton):
)


@register_entity(AQARA_OPPLE_CLUSTER)
@register_entity
class AqaraPetFeederFeedButton(WriteAttributeButton):
"""Defines a feed button for the aqara c1 pet feeder."""

Expand All @@ -236,7 +236,7 @@ class AqaraPetFeederFeedButton(WriteAttributeButton):
)


@register_entity(AQARA_OPPLE_CLUSTER)
@register_entity
class AqaraSelfTestButton(WriteAttributeButton):
"""Defines a ZHA self-test button for Aqara smoke sensors."""

Expand Down
18 changes: 9 additions & 9 deletions zha/application/platforms/climate/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -195,7 +195,7 @@ async def async_set_temperature(
"""Set new target temperature."""


@register_entity(ThermostatCluster.cluster_id)
@register_entity
class Thermostat(BaseThermostat):
"""Representation of a ZHA Thermostat device."""

Expand Down Expand Up @@ -919,7 +919,7 @@ async def async_preset_handler(self, preset: str, enable: bool = False) -> None:
SINOPE_MANUFACTURER_CLUSTER = 0xFF01


@register_entity(ThermostatCluster.cluster_id)
@register_entity
class SinopeTechnologiesThermostat(Thermostat):
"""Sinope Technologies Thermostat."""

Expand Down Expand Up @@ -1034,7 +1034,7 @@ async def async_preset_handler_away(self, is_away: bool = False) -> None:
)


@register_entity(ThermostatCluster.cluster_id)
@register_entity
class ZenWithinThermostat(Thermostat):
"""Zen Within Thermostat implementation."""

Expand All @@ -1046,7 +1046,7 @@ class ZenWithinThermostat(Thermostat):
)


@register_entity(ThermostatCluster.cluster_id)
@register_entity
class ZehnderThermostat(Thermostat):
"""Zehnder thermostat to adapt AUTO mode behavior."""

Expand Down Expand Up @@ -1118,7 +1118,7 @@ def hvac_mode(self) -> HVACMode | None:
return ZehnderThermostat.ZEHNDER_SYSTEM_MODE_2_HVAC.get(self._system_mode)


@register_entity(ThermostatCluster.cluster_id)
@register_entity
class CentralitePearl(Thermostat):
"""Centralite Pearl Thermostat implementation."""

Expand Down Expand Up @@ -1150,7 +1150,7 @@ class CentralitePearl(Thermostat):
)


@register_entity(ThermostatCluster.cluster_id)
@register_entity
class MoesThermostat(Thermostat):
"""Moes Thermostat implementation."""

Expand Down Expand Up @@ -1237,7 +1237,7 @@ async def async_preset_handler(self, preset: str, enable: bool = False) -> None:
)


@register_entity(ThermostatCluster.cluster_id)
@register_entity
class BecaThermostat(Thermostat):
"""Beca Thermostat implementation."""

Expand Down Expand Up @@ -1317,7 +1317,7 @@ async def async_preset_handler(self, preset: str, enable: bool = False) -> None:
)


@register_entity(ThermostatCluster.cluster_id)
@register_entity
class StelproFanHeater(Thermostat):
"""Stelpro Fan Heater implementation."""

Expand Down Expand Up @@ -1348,7 +1348,7 @@ def hvac_modes(self) -> list[HVACMode]:
)


@register_entity(ThermostatCluster.cluster_id)
@register_entity
class ZONNSMARTThermostat(Thermostat):
"""ZONNSMART Thermostat implementation.

Expand Down
Loading
Loading