diff --git a/homeassistant/components/zha/climate.py b/homeassistant/components/zha/climate.py index 3aa11d85516..e734c9cb415 100644 --- a/homeassistant/components/zha/climate.py +++ b/homeassistant/components/zha/climate.py @@ -60,8 +60,6 @@ from .core.const import ( from .core.registries import ZHA_ENTITIES from .entity import ZhaEntity -DEPENDENCIES = ["zha"] - ATTR_SYS_MODE = "system_mode" ATTR_RUNNING_MODE = "running_mode" ATTR_SETPT_CHANGE_SRC = "setpoint_change_source" @@ -76,6 +74,7 @@ ATTR_UNOCCP_COOL_SETPT = "unoccupied_cooling_setpoint" STRICT_MATCH = functools.partial(ZHA_ENTITIES.strict_match, DOMAIN) +MULTI_MATCH = functools.partial(ZHA_ENTITIES.multipass_match, DOMAIN) RUNNING_MODE = {0x00: HVAC_MODE_OFF, 0x03: HVAC_MODE_COOL, 0x04: HVAC_MODE_HEAT} @@ -164,16 +163,13 @@ async def async_setup_entry(hass, config_entry, async_add_entities): hass.data[DATA_ZHA][DATA_ZHA_DISPATCHERS].append(unsub) -@STRICT_MATCH(channel_names=CHANNEL_THERMOSTAT, aux_channels=CHANNEL_FAN) +@MULTI_MATCH(channel_names=CHANNEL_THERMOSTAT, aux_channels=CHANNEL_FAN) class Thermostat(ZhaEntity, ClimateEntity): """Representation of a ZHA Thermostat device.""" DEFAULT_MAX_TEMP = 35 DEFAULT_MIN_TEMP = 7 - _domain = DOMAIN - value_attribute = 0x0000 - def __init__(self, unique_id, zha_device, channels, **kwargs): """Initialize ZHA Thermostat instance.""" super().__init__(unique_id, zha_device, channels, **kwargs) @@ -519,9 +515,10 @@ class Thermostat(ZhaEntity, ClimateEntity): return await handler(enable) -@STRICT_MATCH( +@MULTI_MATCH( channel_names={CHANNEL_THERMOSTAT, "sinope_manufacturer_specific"}, manufacturers="Sinope Technologies", + stop_on_match=True, ) class SinopeTechnologiesThermostat(Thermostat): """Sinope Technologies Thermostat.""" @@ -570,10 +567,11 @@ class SinopeTechnologiesThermostat(Thermostat): return res -@STRICT_MATCH( +@MULTI_MATCH( channel_names=CHANNEL_THERMOSTAT, aux_channels=CHANNEL_FAN, manufacturers="Zen Within", + stop_on_match=True, ) class ZenWithinThermostat(Thermostat): """Zen Within Thermostat implementation.""" @@ -599,11 +597,12 @@ class ZenWithinThermostat(Thermostat): return CURRENT_HVAC_OFF -@STRICT_MATCH( +@MULTI_MATCH( channel_names=CHANNEL_THERMOSTAT, aux_channels=CHANNEL_FAN, manufacturers="Centralite", models="3157100", + stop_on_match=True, ) class CentralitePearl(ZenWithinThermostat): """Centralite Pearl Thermostat implementation.""" diff --git a/homeassistant/components/zha/core/discovery.py b/homeassistant/components/zha/core/discovery.py index 4d70c7aea96..43a0186d88b 100644 --- a/homeassistant/components/zha/core/discovery.py +++ b/homeassistant/components/zha/core/discovery.py @@ -63,8 +63,8 @@ class ProbeEndpoint: def discover_entities(self, channel_pool: zha_typing.ChannelPoolType) -> None: """Process an endpoint on a zigpy device.""" self.discover_by_device_type(channel_pool) - self.discover_by_cluster_id(channel_pool) self.discover_multi_entities(channel_pool) + self.discover_by_cluster_id(channel_pool) @callback def discover_by_device_type(self, channel_pool: zha_typing.ChannelPoolType) -> None: @@ -166,25 +166,42 @@ class ProbeEndpoint: def discover_multi_entities(channel_pool: zha_typing.ChannelPoolType) -> None: """Process an endpoint on and discover multiple entities.""" + ep_profile_id = channel_pool.endpoint.profile_id + ep_device_type = channel_pool.endpoint.device_type + cmpt_by_dev_type = zha_regs.DEVICE_CLASS[ep_profile_id].get(ep_device_type) remaining_channels = channel_pool.unclaimed_channels() - for channel in remaining_channels: - unique_id = f"{channel_pool.unique_id}-{channel.cluster.cluster_id}" - matches, claimed = zha_regs.ZHA_ENTITIES.get_multi_entity( - channel_pool.manufacturer, - channel_pool.model, - channel, - remaining_channels, - ) - if not claimed: - continue + matches, claimed = zha_regs.ZHA_ENTITIES.get_multi_entity( + channel_pool.manufacturer, channel_pool.model, remaining_channels + ) - channel_pool.claim_channels(claimed) - for component, ent_classes_list in matches.items(): - for entity_class in ent_classes_list: + channel_pool.claim_channels(claimed) + for component, ent_n_chan_list in matches.items(): + for entity_and_channel in ent_n_chan_list: + _LOGGER.debug( + "'%s' component -> '%s' using %s", + component, + entity_and_channel.entity_class.__name__, + [ch.name for ch in entity_and_channel.claimed_channel], + ) + for component, ent_n_chan_list in matches.items(): + for entity_and_channel in ent_n_chan_list: + if component == cmpt_by_dev_type: + # for well known device types, like thermostats we'll take only 1st class channel_pool.async_new_entity( - component, entity_class, unique_id, claimed + component, + entity_and_channel.entity_class, + channel_pool.unique_id, + entity_and_channel.claimed_channel, ) + break + first_ch = entity_and_channel.claimed_channel[0] + channel_pool.async_new_entity( + component, + entity_and_channel.entity_class, + f"{channel_pool.unique_id}-{first_ch.cluster.cluster_id}", + entity_and_channel.claimed_channel, + ) def initialize(self, hass: HomeAssistant) -> None: """Update device overrides config.""" diff --git a/homeassistant/components/zha/core/registries.py b/homeassistant/components/zha/core/registries.py index 2bf324e3007..1e41c313836 100644 --- a/homeassistant/components/zha/core/registries.py +++ b/homeassistant/components/zha/core/registries.py @@ -3,7 +3,9 @@ from __future__ import annotations import collections from collections.abc import Callable -from typing import Dict +import dataclasses +import logging +from typing import Dict, List import attr from zigpy import zcl @@ -27,6 +29,7 @@ from . import channels as zha_channels # noqa: F401 pylint: disable=unused-impo from .decorators import CALLABLE_T, DictRegistry, SetRegistry from .typing import ChannelType +_LOGGER = logging.getLogger(__name__) GROUP_ENTITY_DOMAINS = [LIGHT, SWITCH, FAN] PHILLIPS_REMOTE_CLUSTER = 0xFC00 @@ -157,6 +160,8 @@ class MatchRule: aux_channels: Callable | set[str] | str = attr.ib( factory=frozenset, converter=set_or_callable ) + # for multi entities, stop further processing on a match for a component + stop_on_match: bool = attr.ib(default=False) @property def weight(self) -> int: @@ -234,8 +239,16 @@ class MatchRule: return matches -RegistryDictType = Dict[str, Dict[MatchRule, CALLABLE_T]] +@dataclasses.dataclass +class EntityClassAndChannels: + """Container for entity class and corresponding channels.""" + entity_class: CALLABLE_T + claimed_channel: list[ChannelType] + + +RegistryDictType = Dict[str, Dict[MatchRule, CALLABLE_T]] +MultiRegistryDictType = Dict[str, Dict[MatchRule, List[CALLABLE_T]]] GroupRegistryDictType = Dict[str, CALLABLE_T] @@ -245,7 +258,7 @@ class ZHAEntityRegistry: def __init__(self): """Initialize Registry instance.""" self._strict_registry: RegistryDictType = collections.defaultdict(dict) - self._multi_entity_registry: RegistryDictType = collections.defaultdict( + self._multi_entity_registry: MultiRegistryDictType = collections.defaultdict( lambda: collections.defaultdict(list) ) self._group_registry: GroupRegistryDictType = {} @@ -271,22 +284,26 @@ class ZHAEntityRegistry: self, manufacturer: str, model: str, - primary_channel: ChannelType, - aux_channels: list[ChannelType], + channels: list[ChannelType], components: set | None = None, - ) -> tuple[dict[str, list[CALLABLE_T]], list[ChannelType]]: + ) -> tuple[dict[str, list[EntityClassAndChannels]], list[ChannelType]]: """Match ZHA Channels to potentially multiple ZHA Entity classes.""" - result: dict[str, list[CALLABLE_T]] = collections.defaultdict(list) - claimed: set[ChannelType] = set() + result: dict[str, list[EntityClassAndChannels]] = collections.defaultdict(list) + all_claimed: set[ChannelType] = set() for component in components or self._multi_entity_registry: matches = self._multi_entity_registry[component] - for match in sorted(matches, key=lambda x: x.weight, reverse=True): - if match.strict_matched(manufacturer, model, [primary_channel]): - claimed |= set(match.claim_channels(aux_channels)) - ent_classes = self._multi_entity_registry[component][match] - result[component].extend(ent_classes) + sorted_matches = sorted(matches, key=lambda x: x.weight, reverse=True) + for match in sorted_matches: + if match.strict_matched(manufacturer, model, channels): + claimed = match.claim_channels(channels) + for ent_class in self._multi_entity_registry[component][match]: + ent_n_channels = EntityClassAndChannels(ent_class, claimed) + result[component].append(ent_n_channels) + all_claimed |= set(claimed) + if match.stop_on_match: + break - return result, list(claimed) + return result, list(all_claimed) def get_group_entity(self, component: str) -> CALLABLE_T: """Match a ZHA group to a ZHA Entity class.""" @@ -325,11 +342,17 @@ class ZHAEntityRegistry: manufacturers: Callable | set[str] | str = None, models: Callable | set[str] | str = None, aux_channels: Callable | set[str] | str = None, + stop_on_match: bool = False, ) -> Callable[[CALLABLE_T], CALLABLE_T]: """Decorate a loose match rule.""" rule = MatchRule( - channel_names, generic_ids, manufacturers, models, aux_channels + channel_names, + generic_ids, + manufacturers, + models, + aux_channels, + stop_on_match, ) def decorator(zha_entity: CALLABLE_T) -> CALLABLE_T: diff --git a/homeassistant/components/zha/sensor.py b/homeassistant/components/zha/sensor.py index b2cc414ad5f..18df552986d 100644 --- a/homeassistant/components/zha/sensor.py +++ b/homeassistant/components/zha/sensor.py @@ -5,6 +5,13 @@ import functools import numbers from typing import Any +from homeassistant.components.climate.const import ( + CURRENT_HVAC_COOL, + CURRENT_HVAC_FAN, + CURRENT_HVAC_HEAT, + CURRENT_HVAC_IDLE, + CURRENT_HVAC_OFF, +) from homeassistant.components.sensor import ( DEVICE_CLASS_BATTERY, DEVICE_CLASS_CO, @@ -57,6 +64,7 @@ from .core.const import ( CHANNEL_PRESSURE, CHANNEL_SMARTENERGY_METERING, CHANNEL_TEMPERATURE, + CHANNEL_THERMOSTAT, DATA_ZHA, DATA_ZHA_DISPATCHERS, SIGNAL_ADD_ENTITIES, @@ -482,3 +490,120 @@ class FormaldehydeConcentration(Sensor): _decimals = 0 _multiplier = 1e6 _unit = CONCENTRATION_PARTS_PER_MILLION + + +@MULTI_MATCH(channel_names=CHANNEL_THERMOSTAT) +class ThermostatHVACAction(Sensor, id_suffix="hvac_action"): + """Thermostat HVAC action sensor.""" + + @classmethod + def create_entity( + cls, + unique_id: str, + zha_device: ZhaDeviceType, + channels: list[ChannelType], + **kwargs, + ) -> ZhaEntity | None: + """Entity Factory. + + Return entity if it is a supported configuration, otherwise return None + """ + + return cls(unique_id, zha_device, channels, **kwargs) + + @property + def native_value(self) -> str | None: + """Return the current HVAC action.""" + if ( + self._channel.pi_heating_demand is None + and self._channel.pi_cooling_demand is None + ): + return self._rm_rs_action + return self._pi_demand_action + + @property + def _rm_rs_action(self) -> str | None: + """Return the current HVAC action based on running mode and running state.""" + + running_mode = self._channel.running_mode + if running_mode == self._channel.RunningMode.Heat: + return CURRENT_HVAC_HEAT + if running_mode == self._channel.RunningMode.Cool: + return CURRENT_HVAC_COOL + + running_state = self._channel.running_state + if running_state and running_state & ( + self._channel.RunningState.Fan_State_On + | self._channel.RunningState.Fan_2nd_Stage_On + | self._channel.RunningState.Fan_3rd_Stage_On + ): + return CURRENT_HVAC_FAN + if ( + self._channel.system_mode != self._channel.SystemMode.Off + and running_mode == self._channel.SystemMode.Off + ): + return CURRENT_HVAC_IDLE + return CURRENT_HVAC_OFF + + @property + def _pi_demand_action(self) -> str | None: + """Return the current HVAC action based on pi_demands.""" + + heating_demand = self._channel.pi_heating_demand + if heating_demand is not None and heating_demand > 0: + return CURRENT_HVAC_HEAT + cooling_demand = self._channel.pi_cooling_demand + if cooling_demand is not None and cooling_demand > 0: + return CURRENT_HVAC_COOL + + if self._channel.system_mode != self._channel.SystemMode.Off: + return CURRENT_HVAC_IDLE + return CURRENT_HVAC_OFF + + @callback + def async_set_state(self, *args, **kwargs) -> None: + """Handle state update from channel.""" + self.async_write_ha_state() + + +@MULTI_MATCH( + channel_names=CHANNEL_THERMOSTAT, + manufacturers="Zen Within", + stop_on_match=True, +) +class ZenHVACAction(ThermostatHVACAction): + """Zen Within Thermostat HVAC Action.""" + + @property + def _rm_rs_action(self) -> str | None: + """Return the current HVAC action based on running mode and running state.""" + + running_state = self._channel.running_state + if running_state is None: + return None + + rs_heat = ( + self._channel.RunningState.Heat_State_On + | self._channel.RunningState.Heat_2nd_Stage_On + ) + if running_state & rs_heat: + return CURRENT_HVAC_HEAT + + rs_cool = ( + self._channel.RunningState.Cool_State_On + | self._channel.RunningState.Cool_2nd_Stage_On + ) + if running_state & rs_cool: + return CURRENT_HVAC_COOL + + running_state = self._channel.running_state + if running_state and running_state & ( + self._channel.RunningState.Fan_State_On + | self._channel.RunningState.Fan_2nd_Stage_On + | self._channel.RunningState.Fan_3rd_Stage_On + ): + return CURRENT_HVAC_FAN + + if self._channel.system_mode != self._channel.SystemMode.Off: + return CURRENT_HVAC_IDLE + return CURRENT_HVAC_OFF diff --git a/tests/components/zha/test_climate.py b/tests/components/zha/test_climate.py index e452d90d60f..1784813250f 100644 --- a/tests/components/zha/test_climate.py +++ b/tests/components/zha/test_climate.py @@ -45,6 +45,7 @@ from homeassistant.components.climate.const import ( SERVICE_SET_PRESET_MODE, SERVICE_SET_TEMPERATURE, ) +from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN from homeassistant.components.zha.climate import ( DOMAIN, HVAC_MODE_2_SYSTEM, @@ -174,6 +175,7 @@ def device_climate_mock(hass, zigpy_device_mock, zha_device_joined): plugged_attrs = {**ZCL_ATTR_PLUG, **plug} zigpy_device = zigpy_device_mock(clusters, manufacturer=manuf, quirk=quirk) + zigpy_device.node_desc.mac_capability_flags |= 0b_0000_0100 zigpy_device.endpoints[1].thermostat.PLUGGED_ATTR_READS = plugged_attrs zha_device = await zha_device_joined(zigpy_device) await async_enable_traffic(hass, [zha_device]) @@ -257,45 +259,60 @@ async def test_climate_hvac_action_running_state(hass, device_climate): thrm_cluster = device_climate.device.endpoints[1].thermostat entity_id = await find_entity_id(DOMAIN, device_climate, hass) + sensor_entity_id = await find_entity_id(SENSOR_DOMAIN, device_climate, hass) state = hass.states.get(entity_id) assert state.attributes[ATTR_HVAC_ACTION] == CURRENT_HVAC_OFF + hvac_sensor_state = hass.states.get(sensor_entity_id) + assert hvac_sensor_state.state == CURRENT_HVAC_OFF await send_attributes_report( hass, thrm_cluster, {0x001E: Thermostat.RunningMode.Off} ) state = hass.states.get(entity_id) assert state.attributes[ATTR_HVAC_ACTION] == CURRENT_HVAC_OFF + hvac_sensor_state = hass.states.get(sensor_entity_id) + assert hvac_sensor_state.state == CURRENT_HVAC_OFF await send_attributes_report( hass, thrm_cluster, {0x001C: Thermostat.SystemMode.Auto} ) state = hass.states.get(entity_id) assert state.attributes[ATTR_HVAC_ACTION] == CURRENT_HVAC_IDLE + hvac_sensor_state = hass.states.get(sensor_entity_id) + assert hvac_sensor_state.state == CURRENT_HVAC_IDLE await send_attributes_report( hass, thrm_cluster, {0x001E: Thermostat.RunningMode.Cool} ) state = hass.states.get(entity_id) assert state.attributes[ATTR_HVAC_ACTION] == CURRENT_HVAC_COOL + hvac_sensor_state = hass.states.get(sensor_entity_id) + assert hvac_sensor_state.state == CURRENT_HVAC_COOL await send_attributes_report( hass, thrm_cluster, {0x001E: Thermostat.RunningMode.Heat} ) state = hass.states.get(entity_id) assert state.attributes[ATTR_HVAC_ACTION] == CURRENT_HVAC_HEAT + hvac_sensor_state = hass.states.get(sensor_entity_id) + assert hvac_sensor_state.state == CURRENT_HVAC_HEAT await send_attributes_report( hass, thrm_cluster, {0x001E: Thermostat.RunningMode.Off} ) state = hass.states.get(entity_id) assert state.attributes[ATTR_HVAC_ACTION] == CURRENT_HVAC_IDLE + hvac_sensor_state = hass.states.get(sensor_entity_id) + assert hvac_sensor_state.state == CURRENT_HVAC_IDLE await send_attributes_report( hass, thrm_cluster, {0x0029: Thermostat.RunningState.Fan_State_On} ) state = hass.states.get(entity_id) assert state.attributes[ATTR_HVAC_ACTION] == CURRENT_HVAC_FAN + hvac_sensor_state = hass.states.get(sensor_entity_id) + assert hvac_sensor_state.state == CURRENT_HVAC_FAN async def test_climate_hvac_action_running_state_zen(hass, device_climate_zen): @@ -303,63 +320,84 @@ async def test_climate_hvac_action_running_state_zen(hass, device_climate_zen): thrm_cluster = device_climate_zen.device.endpoints[1].thermostat entity_id = await find_entity_id(DOMAIN, device_climate_zen, hass) + sensor_entity_id = await find_entity_id(SENSOR_DOMAIN, device_climate_zen, hass) state = hass.states.get(entity_id) assert ATTR_HVAC_ACTION not in state.attributes + hvac_sensor_state = hass.states.get(sensor_entity_id) + assert hvac_sensor_state.state == "unknown" await send_attributes_report( hass, thrm_cluster, {0x0029: Thermostat.RunningState.Cool_2nd_Stage_On} ) state = hass.states.get(entity_id) assert state.attributes[ATTR_HVAC_ACTION] == CURRENT_HVAC_COOL + hvac_sensor_state = hass.states.get(sensor_entity_id) + assert hvac_sensor_state.state == CURRENT_HVAC_COOL await send_attributes_report( hass, thrm_cluster, {0x0029: Thermostat.RunningState.Fan_State_On} ) state = hass.states.get(entity_id) assert state.attributes[ATTR_HVAC_ACTION] == CURRENT_HVAC_FAN + hvac_sensor_state = hass.states.get(sensor_entity_id) + assert hvac_sensor_state.state == CURRENT_HVAC_FAN await send_attributes_report( hass, thrm_cluster, {0x0029: Thermostat.RunningState.Heat_2nd_Stage_On} ) state = hass.states.get(entity_id) assert state.attributes[ATTR_HVAC_ACTION] == CURRENT_HVAC_HEAT + hvac_sensor_state = hass.states.get(sensor_entity_id) + assert hvac_sensor_state.state == CURRENT_HVAC_HEAT await send_attributes_report( hass, thrm_cluster, {0x0029: Thermostat.RunningState.Fan_2nd_Stage_On} ) state = hass.states.get(entity_id) assert state.attributes[ATTR_HVAC_ACTION] == CURRENT_HVAC_FAN + hvac_sensor_state = hass.states.get(sensor_entity_id) + assert hvac_sensor_state.state == CURRENT_HVAC_FAN await send_attributes_report( hass, thrm_cluster, {0x0029: Thermostat.RunningState.Cool_State_On} ) state = hass.states.get(entity_id) assert state.attributes[ATTR_HVAC_ACTION] == CURRENT_HVAC_COOL + hvac_sensor_state = hass.states.get(sensor_entity_id) + assert hvac_sensor_state.state == CURRENT_HVAC_COOL await send_attributes_report( hass, thrm_cluster, {0x0029: Thermostat.RunningState.Fan_3rd_Stage_On} ) state = hass.states.get(entity_id) assert state.attributes[ATTR_HVAC_ACTION] == CURRENT_HVAC_FAN + hvac_sensor_state = hass.states.get(sensor_entity_id) + assert hvac_sensor_state.state == CURRENT_HVAC_FAN await send_attributes_report( hass, thrm_cluster, {0x0029: Thermostat.RunningState.Heat_State_On} ) state = hass.states.get(entity_id) assert state.attributes[ATTR_HVAC_ACTION] == CURRENT_HVAC_HEAT + hvac_sensor_state = hass.states.get(sensor_entity_id) + assert hvac_sensor_state.state == CURRENT_HVAC_HEAT await send_attributes_report( hass, thrm_cluster, {0x0029: Thermostat.RunningState.Idle} ) state = hass.states.get(entity_id) assert state.attributes[ATTR_HVAC_ACTION] == CURRENT_HVAC_OFF + hvac_sensor_state = hass.states.get(sensor_entity_id) + assert hvac_sensor_state.state == CURRENT_HVAC_OFF await send_attributes_report( hass, thrm_cluster, {0x001C: Thermostat.SystemMode.Heat} ) state = hass.states.get(entity_id) assert state.attributes[ATTR_HVAC_ACTION] == CURRENT_HVAC_IDLE + hvac_sensor_state = hass.states.get(sensor_entity_id) + assert hvac_sensor_state.state == CURRENT_HVAC_IDLE async def test_climate_hvac_action_pi_demand(hass, device_climate): diff --git a/tests/components/zha/test_registries.py b/tests/components/zha/test_registries.py index d202c7256dd..01b2a074187 100644 --- a/tests/components/zha/test_registries.py +++ b/tests/components/zha/test_registries.py @@ -332,27 +332,13 @@ def test_multi_sensor_match(channel, entity_registry): ch_illuminati = channel("illuminance", 0x0401) match, claimed = entity_registry.get_multi_entity( - "manufacturer", - "model", - primary_channel=ch_illuminati, - aux_channels=[ch_se, ch_illuminati], - ) - - assert s.binary_sensor not in match - assert s.component not in match - assert set(claimed) == set() - - match, claimed = entity_registry.get_multi_entity( - "manufacturer", - "model", - primary_channel=ch_se, - aux_channels=[ch_se, ch_illuminati], + "manufacturer", "model", channels=[ch_se, ch_illuminati] ) assert s.binary_sensor in match assert s.component not in match assert set(claimed) == {ch_se} - assert {cls.__name__ for cls in match[s.binary_sensor]} == { + assert {cls.entity_class.__name__ for cls in match[s.binary_sensor]} == { SmartEnergySensor2.__name__ } @@ -371,17 +357,16 @@ def test_multi_sensor_match(channel, entity_registry): pass match, claimed = entity_registry.get_multi_entity( - "manufacturer", - "model", - primary_channel=ch_se, - aux_channels={ch_se, ch_illuminati}, + "manufacturer", "model", channels={ch_se, ch_illuminati} ) assert s.binary_sensor in match assert s.component in match assert set(claimed) == {ch_se, ch_illuminati} - assert {cls.__name__ for cls in match[s.binary_sensor]} == { + assert {cls.entity_class.__name__ for cls in match[s.binary_sensor]} == { SmartEnergySensor2.__name__, SmartEnergySensor3.__name__, } - assert {cls.__name__ for cls in match[s.component]} == {SmartEnergySensor1.__name__} + assert {cls.entity_class.__name__ for cls in match[s.component]} == { + SmartEnergySensor1.__name__ + } diff --git a/tests/components/zha/zha_devices_list.py b/tests/components/zha/zha_devices_list.py index 6276aa12068..e85f4c270d5 100644 --- a/tests/components/zha/zha_devices_list.py +++ b/tests/components/zha/zha_devices_list.py @@ -3174,6 +3174,7 @@ DEVICES = [ "sensor.sinope_technologies_th1123zb_77665544_electrical_measurement_rms_current", "sensor.sinope_technologies_th1123zb_77665544_electrical_measurement_rms_voltage", "sensor.sinope_technologies_th1123zb_77665544_temperature", + "sensor.sinope_technologies_th1123zb_77665544_thermostat_hvac_action", ], DEV_SIG_ENT_MAP: { ("climate", "00:11:22:33:44:55:66:77-1"): { @@ -3201,6 +3202,11 @@ DEVICES = [ DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurementRMSVoltage", DEV_SIG_ENT_MAP_ID: "sensor.sinope_technologies_th1123zb_77665544_electrical_measurement_rms_voltage", }, + ("sensor", "00:11:22:33:44:55:66:77-1-513-hvac_action"): { + DEV_SIG_CHANNELS: ["thermostat"], + DEV_SIG_ENT_MAP_CLASS: "ThermostatHVACAction", + DEV_SIG_ENT_MAP_ID: "sensor.sinope_technologies_th1123zb_77665544_thermostat_hvac_action", + }, }, DEV_SIG_EVT_CHANNELS: ["1:0x0019"], SIG_MANUFACTURER: "Sinope Technologies", @@ -3231,6 +3237,7 @@ DEVICES = [ "sensor.sinope_technologies_th1124zb_77665544_electrical_measurement_rms_current", "sensor.sinope_technologies_th1124zb_77665544_electrical_measurement_rms_voltage", "sensor.sinope_technologies_th1124zb_77665544_temperature", + "sensor.sinope_technologies_th1124zb_77665544_thermostat_hvac_action", "climate.sinope_technologies_th1124zb_77665544_thermostat", ], DEV_SIG_ENT_MAP: { @@ -3239,6 +3246,11 @@ DEVICES = [ DEV_SIG_ENT_MAP_CLASS: "Thermostat", DEV_SIG_ENT_MAP_ID: "climate.sinope_technologies_th1124zb_77665544_thermostat", }, + ("sensor", "00:11:22:33:44:55:66:77-1-513-hvac_action"): { + DEV_SIG_CHANNELS: ["thermostat"], + DEV_SIG_ENT_MAP_CLASS: "ThermostatHVACAction", + DEV_SIG_ENT_MAP_ID: "sensor.sinope_technologies_th1124zb_77665544_thermostat_hvac_action", + }, ("sensor", "00:11:22:33:44:55:66:77-1-1026"): { DEV_SIG_CHANNELS: ["temperature"], DEV_SIG_ENT_MAP_CLASS: "Temperature", @@ -3454,6 +3466,7 @@ DEVICES = [ }, DEV_SIG_ENTITIES: [ "climate.zen_within_zen_01_77665544_fan_thermostat", + "sensor.zen_within_zen_01_77665544_thermostat_hvac_action", "sensor.zen_within_zen_01_77665544_power", ], DEV_SIG_ENT_MAP: { @@ -3467,6 +3480,11 @@ DEVICES = [ DEV_SIG_ENT_MAP_CLASS: "ZenWithinThermostat", DEV_SIG_ENT_MAP_ID: "climate.zen_within_zen_01_77665544_fan_thermostat", }, + ("sensor", "00:11:22:33:44:55:66:77-1-513-hvac_action"): { + DEV_SIG_CHANNELS: ["thermostat"], + DEV_SIG_ENT_MAP_CLASS: "ZenHVACAction", + DEV_SIG_ENT_MAP_ID: "sensor.zen_within_zen_01_77665544_thermostat_hvac_action", + }, }, DEV_SIG_EVT_CHANNELS: ["1:0x0019"], SIG_MANUFACTURER: "Zen Within",