* ZHA Entity registry. Match a zha_device and channels to a ZHA entity. * Refactor ZHA sensor to use registry. * Remove sensor_types registry. * Fix ZHA device tracker battery remaining. * Remove should_poll/force_update attributes. * Fix binary_sensor regression. * isort. * Pylint. * Don't access protected members. * Address comments and fix spelling. * Make pylint happy again.
283 lines
10 KiB
Python
283 lines
10 KiB
Python
"""
|
|
Mapping registries for Zigbee Home Automation.
|
|
|
|
For more details about this component, please refer to the documentation at
|
|
https://home-assistant.io/integrations/zha/
|
|
"""
|
|
import collections
|
|
from typing import Callable, Set
|
|
|
|
import attr
|
|
import bellows.ezsp
|
|
import bellows.zigbee.application
|
|
import zigpy.profiles.zha
|
|
import zigpy.profiles.zll
|
|
import zigpy.zcl as zcl
|
|
import zigpy_deconz.api
|
|
import zigpy_deconz.zigbee.application
|
|
import zigpy_xbee.api
|
|
import zigpy_xbee.zigbee.application
|
|
import zigpy_zigate.api
|
|
import zigpy_zigate.zigbee.application
|
|
|
|
from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR
|
|
from homeassistant.components.device_tracker import DOMAIN as DEVICE_TRACKER
|
|
from homeassistant.components.fan import DOMAIN as FAN
|
|
from homeassistant.components.light import DOMAIN as LIGHT
|
|
from homeassistant.components.lock import DOMAIN as LOCK
|
|
from homeassistant.components.sensor import DOMAIN as SENSOR
|
|
from homeassistant.components.switch import DOMAIN as SWITCH
|
|
|
|
# importing channels updates registries
|
|
from . import channels # noqa: F401 pylint: disable=unused-import
|
|
from .const import (
|
|
CONTROLLER,
|
|
SENSOR_ACCELERATION,
|
|
SENSOR_OCCUPANCY,
|
|
SENSOR_OPENING,
|
|
ZHA_GW_RADIO,
|
|
ZHA_GW_RADIO_DESCRIPTION,
|
|
ZONE,
|
|
RadioType,
|
|
)
|
|
from .decorators import CALLABLE_T, DictRegistry, SetRegistry
|
|
|
|
BINARY_SENSOR_CLUSTERS = SetRegistry()
|
|
BINARY_SENSOR_TYPES = {}
|
|
BINDABLE_CLUSTERS = SetRegistry()
|
|
CHANNEL_ONLY_CLUSTERS = SetRegistry()
|
|
CLUSTER_REPORT_CONFIGS = {}
|
|
CUSTOM_CLUSTER_MAPPINGS = {}
|
|
DEVICE_CLASS = collections.defaultdict(dict)
|
|
DEVICE_TRACKER_CLUSTERS = SetRegistry()
|
|
EVENT_RELAY_CLUSTERS = SetRegistry()
|
|
LIGHT_CLUSTERS = SetRegistry()
|
|
OUTPUT_CHANNEL_ONLY_CLUSTERS = SetRegistry()
|
|
RADIO_TYPES = {}
|
|
REMOTE_DEVICE_TYPES = collections.defaultdict(list)
|
|
SINGLE_INPUT_CLUSTER_DEVICE_CLASS = {}
|
|
SINGLE_OUTPUT_CLUSTER_DEVICE_CLASS = {}
|
|
SWITCH_CLUSTERS = SetRegistry()
|
|
SMARTTHINGS_ACCELERATION_CLUSTER = 0xFC02
|
|
SMARTTHINGS_ARRIVAL_SENSOR_DEVICE_TYPE = 0x8000
|
|
SMARTTHINGS_HUMIDITY_CLUSTER = 0xFC45
|
|
|
|
COMPONENT_CLUSTERS = {
|
|
BINARY_SENSOR: BINARY_SENSOR_CLUSTERS,
|
|
DEVICE_TRACKER: DEVICE_TRACKER_CLUSTERS,
|
|
LIGHT: LIGHT_CLUSTERS,
|
|
SWITCH: SWITCH_CLUSTERS,
|
|
}
|
|
|
|
ZIGBEE_CHANNEL_REGISTRY = DictRegistry()
|
|
|
|
|
|
def establish_device_mappings():
|
|
"""Establish mappings between ZCL objects and HA ZHA objects.
|
|
|
|
These cannot be module level, as importing bellows must be done in a
|
|
in a function.
|
|
"""
|
|
RADIO_TYPES[RadioType.ezsp.name] = {
|
|
ZHA_GW_RADIO: bellows.ezsp.EZSP,
|
|
CONTROLLER: bellows.zigbee.application.ControllerApplication,
|
|
ZHA_GW_RADIO_DESCRIPTION: "EZSP",
|
|
}
|
|
|
|
RADIO_TYPES[RadioType.deconz.name] = {
|
|
ZHA_GW_RADIO: zigpy_deconz.api.Deconz,
|
|
CONTROLLER: zigpy_deconz.zigbee.application.ControllerApplication,
|
|
ZHA_GW_RADIO_DESCRIPTION: "Deconz",
|
|
}
|
|
|
|
RADIO_TYPES[RadioType.xbee.name] = {
|
|
ZHA_GW_RADIO: zigpy_xbee.api.XBee,
|
|
CONTROLLER: zigpy_xbee.zigbee.application.ControllerApplication,
|
|
ZHA_GW_RADIO_DESCRIPTION: "XBee",
|
|
}
|
|
|
|
RADIO_TYPES[RadioType.zigate.name] = {
|
|
ZHA_GW_RADIO: zigpy_zigate.api.ZiGate,
|
|
CONTROLLER: zigpy_zigate.zigbee.application.ControllerApplication,
|
|
ZHA_GW_RADIO_DESCRIPTION: "ZiGate",
|
|
}
|
|
|
|
BINARY_SENSOR_CLUSTERS.add(SMARTTHINGS_ACCELERATION_CLUSTER)
|
|
|
|
BINARY_SENSOR_TYPES.update(
|
|
{
|
|
SMARTTHINGS_ACCELERATION_CLUSTER: SENSOR_ACCELERATION,
|
|
zcl.clusters.general.OnOff.cluster_id: SENSOR_OPENING,
|
|
zcl.clusters.measurement.OccupancySensing.cluster_id: SENSOR_OCCUPANCY,
|
|
zcl.clusters.security.IasZone.cluster_id: ZONE,
|
|
}
|
|
)
|
|
|
|
DEVICE_CLASS[zigpy.profiles.zha.PROFILE_ID].update(
|
|
{
|
|
SMARTTHINGS_ARRIVAL_SENSOR_DEVICE_TYPE: DEVICE_TRACKER,
|
|
zigpy.profiles.zha.DeviceType.COLOR_DIMMABLE_LIGHT: LIGHT,
|
|
zigpy.profiles.zha.DeviceType.COLOR_TEMPERATURE_LIGHT: LIGHT,
|
|
zigpy.profiles.zha.DeviceType.DIMMABLE_BALLAST: LIGHT,
|
|
zigpy.profiles.zha.DeviceType.DIMMABLE_LIGHT: LIGHT,
|
|
zigpy.profiles.zha.DeviceType.DIMMABLE_PLUG_IN_UNIT: LIGHT,
|
|
zigpy.profiles.zha.DeviceType.EXTENDED_COLOR_LIGHT: LIGHT,
|
|
zigpy.profiles.zha.DeviceType.LEVEL_CONTROLLABLE_OUTPUT: LIGHT,
|
|
zigpy.profiles.zha.DeviceType.ON_OFF_BALLAST: SWITCH,
|
|
zigpy.profiles.zha.DeviceType.ON_OFF_LIGHT: LIGHT,
|
|
zigpy.profiles.zha.DeviceType.ON_OFF_LIGHT_SWITCH: SWITCH,
|
|
zigpy.profiles.zha.DeviceType.ON_OFF_PLUG_IN_UNIT: SWITCH,
|
|
zigpy.profiles.zha.DeviceType.SMART_PLUG: SWITCH,
|
|
}
|
|
)
|
|
|
|
DEVICE_CLASS[zigpy.profiles.zll.PROFILE_ID].update(
|
|
{
|
|
zigpy.profiles.zll.DeviceType.COLOR_LIGHT: LIGHT,
|
|
zigpy.profiles.zll.DeviceType.COLOR_TEMPERATURE_LIGHT: LIGHT,
|
|
zigpy.profiles.zll.DeviceType.DIMMABLE_LIGHT: LIGHT,
|
|
zigpy.profiles.zll.DeviceType.DIMMABLE_PLUGIN_UNIT: LIGHT,
|
|
zigpy.profiles.zll.DeviceType.EXTENDED_COLOR_LIGHT: LIGHT,
|
|
zigpy.profiles.zll.DeviceType.ON_OFF_LIGHT: LIGHT,
|
|
zigpy.profiles.zll.DeviceType.ON_OFF_PLUGIN_UNIT: SWITCH,
|
|
}
|
|
)
|
|
|
|
SINGLE_INPUT_CLUSTER_DEVICE_CLASS.update(
|
|
{
|
|
# this works for now but if we hit conflicts we can break it out to
|
|
# a different dict that is keyed by manufacturer
|
|
SMARTTHINGS_ACCELERATION_CLUSTER: BINARY_SENSOR,
|
|
SMARTTHINGS_HUMIDITY_CLUSTER: SENSOR,
|
|
zcl.clusters.closures.DoorLock: LOCK,
|
|
zcl.clusters.general.AnalogInput.cluster_id: SENSOR,
|
|
zcl.clusters.general.MultistateInput.cluster_id: SENSOR,
|
|
zcl.clusters.general.OnOff: SWITCH,
|
|
zcl.clusters.general.PowerConfiguration: SENSOR,
|
|
zcl.clusters.homeautomation.ElectricalMeasurement: SENSOR,
|
|
zcl.clusters.hvac.Fan: FAN,
|
|
zcl.clusters.measurement.IlluminanceMeasurement: SENSOR,
|
|
zcl.clusters.measurement.OccupancySensing: BINARY_SENSOR,
|
|
zcl.clusters.measurement.PressureMeasurement: SENSOR,
|
|
zcl.clusters.measurement.RelativeHumidity: SENSOR,
|
|
zcl.clusters.measurement.TemperatureMeasurement: SENSOR,
|
|
zcl.clusters.security.IasZone: BINARY_SENSOR,
|
|
zcl.clusters.smartenergy.Metering: SENSOR,
|
|
}
|
|
)
|
|
|
|
SINGLE_OUTPUT_CLUSTER_DEVICE_CLASS.update(
|
|
{zcl.clusters.general.OnOff: BINARY_SENSOR}
|
|
)
|
|
|
|
zha = zigpy.profiles.zha
|
|
REMOTE_DEVICE_TYPES[zha.PROFILE_ID].append(zha.DeviceType.COLOR_CONTROLLER)
|
|
REMOTE_DEVICE_TYPES[zha.PROFILE_ID].append(zha.DeviceType.COLOR_DIMMER_SWITCH)
|
|
REMOTE_DEVICE_TYPES[zha.PROFILE_ID].append(zha.DeviceType.COLOR_SCENE_CONTROLLER)
|
|
REMOTE_DEVICE_TYPES[zha.PROFILE_ID].append(zha.DeviceType.DIMMER_SWITCH)
|
|
REMOTE_DEVICE_TYPES[zha.PROFILE_ID].append(zha.DeviceType.NON_COLOR_CONTROLLER)
|
|
REMOTE_DEVICE_TYPES[zha.PROFILE_ID].append(
|
|
zha.DeviceType.NON_COLOR_SCENE_CONTROLLER
|
|
)
|
|
REMOTE_DEVICE_TYPES[zha.PROFILE_ID].append(zha.DeviceType.REMOTE_CONTROL)
|
|
REMOTE_DEVICE_TYPES[zha.PROFILE_ID].append(zha.DeviceType.SCENE_SELECTOR)
|
|
|
|
zll = zigpy.profiles.zll
|
|
REMOTE_DEVICE_TYPES[zll.PROFILE_ID].append(zll.DeviceType.COLOR_CONTROLLER)
|
|
REMOTE_DEVICE_TYPES[zll.PROFILE_ID].append(zll.DeviceType.COLOR_SCENE_CONTROLLER)
|
|
REMOTE_DEVICE_TYPES[zll.PROFILE_ID].append(zll.DeviceType.CONTROL_BRIDGE)
|
|
REMOTE_DEVICE_TYPES[zll.PROFILE_ID].append(zll.DeviceType.CONTROLLER)
|
|
REMOTE_DEVICE_TYPES[zll.PROFILE_ID].append(zll.DeviceType.SCENE_CONTROLLER)
|
|
|
|
|
|
@attr.s(frozen=True)
|
|
class MatchRule:
|
|
"""Match a ZHA Entity to a channel name or generic id."""
|
|
|
|
channel_names: Set[str] = attr.ib(factory=frozenset, converter=frozenset)
|
|
generic_ids: Set[str] = attr.ib(factory=frozenset, converter=frozenset)
|
|
manufacturer: str = attr.ib(default=None)
|
|
model: str = attr.ib(default=None)
|
|
|
|
|
|
class ZHAEntityRegistry:
|
|
"""Channel to ZHA Entity mapping."""
|
|
|
|
def __init__(self):
|
|
"""Initialize Registry instance."""
|
|
self._strict_registry = collections.defaultdict(dict)
|
|
self._loose_registry = collections.defaultdict(dict)
|
|
|
|
def get_entity(
|
|
self, component: str, zha_device, chnls: list, default: CALLABLE_T = None
|
|
) -> CALLABLE_T:
|
|
"""Match a ZHA Channels to a ZHA Entity class."""
|
|
for match in self._strict_registry[component]:
|
|
if self._strict_matched(zha_device, chnls, match):
|
|
return self._strict_registry[component][match]
|
|
|
|
return default
|
|
|
|
def strict_match(
|
|
self, component: str, rule: MatchRule
|
|
) -> Callable[[CALLABLE_T], CALLABLE_T]:
|
|
"""Decorate a strict match rule."""
|
|
|
|
def decorator(zha_ent: CALLABLE_T) -> CALLABLE_T:
|
|
"""Register a strict match rule.
|
|
|
|
All non empty fields of a match rule must match.
|
|
"""
|
|
self._strict_registry[component][rule] = zha_ent
|
|
return zha_ent
|
|
|
|
return decorator
|
|
|
|
def loose_match(
|
|
self, component: str, rule: MatchRule
|
|
) -> Callable[[CALLABLE_T], CALLABLE_T]:
|
|
"""Decorate a loose match rule."""
|
|
|
|
def decorator(zha_entity: CALLABLE_T) -> CALLABLE_T:
|
|
"""Register a loose match rule.
|
|
|
|
All non empty fields of a match rule must match.
|
|
"""
|
|
self._loose_registry[component][rule] = zha_entity
|
|
return zha_entity
|
|
|
|
return decorator
|
|
|
|
def _strict_matched(self, zha_device, chnls: dict, rule: MatchRule) -> bool:
|
|
"""Return True if this device matches the criteria."""
|
|
return all(self._matched(zha_device, chnls, rule))
|
|
|
|
def _loose_matched(self, zha_device, chnls: dict, rule: MatchRule) -> bool:
|
|
"""Return True if this device matches the criteria."""
|
|
return any(self._matched(zha_device, chnls, rule))
|
|
|
|
@staticmethod
|
|
def _matched(zha_device, chnls: list, rule: MatchRule) -> bool:
|
|
"""Return a list of field matches."""
|
|
if not any(attr.asdict(rule).values()):
|
|
return [False]
|
|
|
|
matches = []
|
|
if rule.channel_names:
|
|
channel_names = {ch.name for ch in chnls}
|
|
matches.append(rule.channel_names.issubset(channel_names))
|
|
|
|
if rule.generic_ids:
|
|
all_generic_ids = {ch.generic_id for ch in chnls}
|
|
matches.append(rule.generic_ids.issubset(all_generic_ids))
|
|
|
|
if rule.manufacturer:
|
|
matches.append(zha_device.manufacturer == rule.manufacturer)
|
|
|
|
if rule.model:
|
|
matches.append(zha_device.model == rule.model)
|
|
|
|
return matches
|
|
|
|
|
|
ZHA_ENTITIES = ZHAEntityRegistry()
|