Add ZHA HVAC Action sensor (#57021)
* WIP * Refactor multi-entity matching Eliminate the notion on primary channel. * Cleanup climate tests * Refactor multi-entity match Remove the "primary channel" in multiple entity matches * Cleanup * Add HVAC Action sensor * Add a "stop_on_match" option for multi entities matches Nominally working HVAC state sensors * Add id_suffix for HVAC action sensor * Fix Zen HVAC action sensor * Pylint
This commit is contained in:
parent
69875cbd11
commit
723596076d
7 changed files with 266 additions and 61 deletions
|
@ -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."""
|
||||
|
|
|
@ -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."""
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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__
|
||||
}
|
||||
|
|
|
@ -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",
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue