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:
Alexei Chetroi 2021-10-04 12:57:58 -04:00 committed by GitHub
parent 69875cbd11
commit 723596076d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 266 additions and 61 deletions

View file

@ -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."""

View file

@ -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."""

View file

@ -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:

View file

@ -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

View file

@ -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):

View file

@ -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__
}

View file

@ -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",