mirror of
https://github.com/home-assistant/core.git
synced 2025-07-16 17:57:11 +00:00
Add ZHA ZCL thermostat entities (#106563)
This commit is contained in:
parent
82e1ed43f8
commit
a7a41e54f6
@ -146,6 +146,7 @@ class ThermostatClusterHandler(ClusterHandler):
|
|||||||
Thermostat.AttributeDefs.min_cool_setpoint_limit.name: True,
|
Thermostat.AttributeDefs.min_cool_setpoint_limit.name: True,
|
||||||
Thermostat.AttributeDefs.min_heat_setpoint_limit.name: True,
|
Thermostat.AttributeDefs.min_heat_setpoint_limit.name: True,
|
||||||
Thermostat.AttributeDefs.local_temperature_calibration.name: True,
|
Thermostat.AttributeDefs.local_temperature_calibration.name: True,
|
||||||
|
Thermostat.AttributeDefs.setpoint_change_source.name: True,
|
||||||
}
|
}
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@ -341,3 +342,5 @@ class ThermostatClusterHandler(ClusterHandler):
|
|||||||
@registries.ZIGBEE_CLUSTER_HANDLER_REGISTRY.register(UserInterface.cluster_id)
|
@registries.ZIGBEE_CLUSTER_HANDLER_REGISTRY.register(UserInterface.cluster_id)
|
||||||
class UserInterfaceClusterHandler(ClusterHandler):
|
class UserInterfaceClusterHandler(ClusterHandler):
|
||||||
"""User interface (thermostat) cluster handler."""
|
"""User interface (thermostat) cluster handler."""
|
||||||
|
|
||||||
|
ZCL_INIT_ATTRS = {UserInterface.AttributeDefs.keypad_lockout.name: True}
|
||||||
|
@ -5,6 +5,8 @@ import functools
|
|||||||
import logging
|
import logging
|
||||||
from typing import TYPE_CHECKING, Any, Self
|
from typing import TYPE_CHECKING, Any, Self
|
||||||
|
|
||||||
|
from zigpy.zcl.clusters.hvac import Thermostat
|
||||||
|
|
||||||
from homeassistant.components.number import NumberEntity, NumberMode
|
from homeassistant.components.number import NumberEntity, NumberMode
|
||||||
from homeassistant.config_entries import ConfigEntry
|
from homeassistant.config_entries import ConfigEntry
|
||||||
from homeassistant.const import EntityCategory, Platform, UnitOfMass, UnitOfTemperature
|
from homeassistant.const import EntityCategory, Platform, UnitOfMass, UnitOfTemperature
|
||||||
@ -985,3 +987,69 @@ class SonoffPresenceSenorTimeout(ZHANumberConfigurationEntity):
|
|||||||
|
|
||||||
_attr_mode: NumberMode = NumberMode.BOX
|
_attr_mode: NumberMode = NumberMode.BOX
|
||||||
_attr_icon: str = "mdi:timer-edit"
|
_attr_icon: str = "mdi:timer-edit"
|
||||||
|
|
||||||
|
|
||||||
|
# pylint: disable-next=hass-invalid-inheritance # needs fixing
|
||||||
|
class ZCLTemperatureEntity(ZHANumberConfigurationEntity):
|
||||||
|
"""Common entity class for ZCL temperature input."""
|
||||||
|
|
||||||
|
_attr_native_unit_of_measurement: str = UnitOfTemperature.CELSIUS
|
||||||
|
_attr_mode: NumberMode = NumberMode.BOX
|
||||||
|
_attr_native_step: float = 0.01
|
||||||
|
_attr_multiplier: float = 0.01
|
||||||
|
|
||||||
|
|
||||||
|
# pylint: disable-next=hass-invalid-inheritance # needs fixing
|
||||||
|
class ZCLHeatSetpointLimitEntity(ZCLTemperatureEntity):
|
||||||
|
"""Min or max heat setpoint setting on thermostats."""
|
||||||
|
|
||||||
|
_attr_icon: str = "mdi:thermostat"
|
||||||
|
_attr_native_step: float = 0.5
|
||||||
|
|
||||||
|
_min_source = Thermostat.AttributeDefs.abs_min_heat_setpoint_limit.name
|
||||||
|
_max_source = Thermostat.AttributeDefs.abs_max_heat_setpoint_limit.name
|
||||||
|
|
||||||
|
@property
|
||||||
|
def native_min_value(self) -> float:
|
||||||
|
"""Return the minimum value."""
|
||||||
|
# The spec says 0x954D, which is a signed integer, therefore the value is in decimals
|
||||||
|
min_present_value = self._cluster_handler.cluster.get(self._min_source, -27315)
|
||||||
|
return min_present_value * self._attr_multiplier
|
||||||
|
|
||||||
|
@property
|
||||||
|
def native_max_value(self) -> float:
|
||||||
|
"""Return the maximum value."""
|
||||||
|
max_present_value = self._cluster_handler.cluster.get(self._max_source, 0x7FFF)
|
||||||
|
return max_present_value * self._attr_multiplier
|
||||||
|
|
||||||
|
|
||||||
|
@CONFIG_DIAGNOSTIC_MATCH(cluster_handler_names=CLUSTER_HANDLER_THERMOSTAT)
|
||||||
|
# pylint: disable-next=hass-invalid-inheritance # needs fixing
|
||||||
|
class MaxHeatSetpointLimit(ZCLHeatSetpointLimitEntity):
|
||||||
|
"""Max heat setpoint setting on thermostats.
|
||||||
|
|
||||||
|
Optional thermostat attribute.
|
||||||
|
"""
|
||||||
|
|
||||||
|
_unique_id_suffix = "max_heat_setpoint_limit"
|
||||||
|
_attribute_name: str = "max_heat_setpoint_limit"
|
||||||
|
_attr_translation_key: str = "max_heat_setpoint_limit"
|
||||||
|
_attr_entity_category = EntityCategory.CONFIG
|
||||||
|
|
||||||
|
_min_source = Thermostat.AttributeDefs.min_heat_setpoint_limit.name
|
||||||
|
|
||||||
|
|
||||||
|
@CONFIG_DIAGNOSTIC_MATCH(cluster_handler_names=CLUSTER_HANDLER_THERMOSTAT)
|
||||||
|
# pylint: disable-next=hass-invalid-inheritance # needs fixing
|
||||||
|
class MinHeatSetpointLimit(ZCLHeatSetpointLimitEntity):
|
||||||
|
"""Min heat setpoint setting on thermostats.
|
||||||
|
|
||||||
|
Optional thermostat attribute.
|
||||||
|
"""
|
||||||
|
|
||||||
|
_unique_id_suffix = "min_heat_setpoint_limit"
|
||||||
|
_attribute_name: str = "min_heat_setpoint_limit"
|
||||||
|
_attr_translation_key: str = "min_heat_setpoint_limit"
|
||||||
|
_attr_entity_category = EntityCategory.CONFIG
|
||||||
|
|
||||||
|
_max_source = Thermostat.AttributeDefs.max_heat_setpoint_limit.name
|
||||||
|
@ -673,3 +673,28 @@ class SonoffPresenceDetectionSensitivity(ZCLEnumSelectEntity):
|
|||||||
_attribute_name = "ultrasonic_u_to_o_threshold"
|
_attribute_name = "ultrasonic_u_to_o_threshold"
|
||||||
_enum = SonoffPresenceDetectionSensitivityEnum
|
_enum = SonoffPresenceDetectionSensitivityEnum
|
||||||
_attr_translation_key: str = "detection_sensitivity"
|
_attr_translation_key: str = "detection_sensitivity"
|
||||||
|
|
||||||
|
|
||||||
|
class KeypadLockoutEnum(types.enum8):
|
||||||
|
"""Keypad lockout options."""
|
||||||
|
|
||||||
|
Unlock = 0x00
|
||||||
|
Lock1 = 0x01
|
||||||
|
Lock2 = 0x02
|
||||||
|
Lock3 = 0x03
|
||||||
|
Lock4 = 0x04
|
||||||
|
|
||||||
|
|
||||||
|
@CONFIG_DIAGNOSTIC_MATCH(cluster_handler_names="thermostat_ui")
|
||||||
|
class KeypadLockout(ZCLEnumSelectEntity):
|
||||||
|
"""Mandatory attribute for thermostat_ui cluster.
|
||||||
|
|
||||||
|
Often only the first two are implemented, and Lock2 to Lock4 should map to Lock1 in the firmware.
|
||||||
|
This however covers all bases.
|
||||||
|
"""
|
||||||
|
|
||||||
|
_unique_id_suffix = "keypad_lockout"
|
||||||
|
_attribute_name: str = "keypad_lockout"
|
||||||
|
_enum = KeypadLockoutEnum
|
||||||
|
_attr_translation_key: str = "keypad_lockout"
|
||||||
|
_attr_icon: str = "mdi:lock"
|
||||||
|
@ -95,6 +95,9 @@ CLUSTER_HANDLER_ST_HUMIDITY_CLUSTER = (
|
|||||||
)
|
)
|
||||||
STRICT_MATCH = functools.partial(ZHA_ENTITIES.strict_match, Platform.SENSOR)
|
STRICT_MATCH = functools.partial(ZHA_ENTITIES.strict_match, Platform.SENSOR)
|
||||||
MULTI_MATCH = functools.partial(ZHA_ENTITIES.multipass_match, Platform.SENSOR)
|
MULTI_MATCH = functools.partial(ZHA_ENTITIES.multipass_match, Platform.SENSOR)
|
||||||
|
CONFIG_DIAGNOSTIC_MATCH = functools.partial(
|
||||||
|
ZHA_ENTITIES.config_diagnostic_match, Platform.SENSOR
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
async def async_setup_entry(
|
async def async_setup_entry(
|
||||||
@ -238,6 +241,19 @@ class PollableSensor(Sensor):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# pylint: disable-next=hass-invalid-inheritance # needs fixing
|
||||||
|
class EnumSensor(Sensor):
|
||||||
|
"""Sensor with value from enum."""
|
||||||
|
|
||||||
|
_attr_device_class: SensorDeviceClass = SensorDeviceClass.ENUM
|
||||||
|
_enum: type[enum.Enum]
|
||||||
|
|
||||||
|
def formatter(self, value: int) -> str | None:
|
||||||
|
"""Use name of enum."""
|
||||||
|
assert self._enum is not None
|
||||||
|
return self._enum(value).name
|
||||||
|
|
||||||
|
|
||||||
@MULTI_MATCH(
|
@MULTI_MATCH(
|
||||||
cluster_handler_names=CLUSTER_HANDLER_ANALOG_INPUT,
|
cluster_handler_names=CLUSTER_HANDLER_ANALOG_INPUT,
|
||||||
manufacturers="Digi",
|
manufacturers="Digi",
|
||||||
@ -1254,3 +1270,45 @@ class SonoffPresenceSenorIlluminationStatus(Sensor):
|
|||||||
def formatter(self, value: int) -> int | float | None:
|
def formatter(self, value: int) -> int | float | None:
|
||||||
"""Numeric pass-through formatter."""
|
"""Numeric pass-through formatter."""
|
||||||
return SonoffIlluminationStates(value).name
|
return SonoffIlluminationStates(value).name
|
||||||
|
|
||||||
|
|
||||||
|
@CONFIG_DIAGNOSTIC_MATCH(cluster_handler_names=CLUSTER_HANDLER_THERMOSTAT)
|
||||||
|
# pylint: disable-next=hass-invalid-inheritance # needs fixing
|
||||||
|
class PiHeatingDemand(Sensor):
|
||||||
|
"""Sensor that displays the percentage of heating power demanded.
|
||||||
|
|
||||||
|
Optional thermostat attribute.
|
||||||
|
"""
|
||||||
|
|
||||||
|
_unique_id_suffix = "pi_heating_demand"
|
||||||
|
_attribute_name = "pi_heating_demand"
|
||||||
|
_attr_translation_key: str = "pi_heating_demand"
|
||||||
|
_attr_icon: str = "mdi:radiator"
|
||||||
|
_attr_native_unit_of_measurement = PERCENTAGE
|
||||||
|
_decimals = 0
|
||||||
|
_attr_state_class: SensorStateClass = SensorStateClass.MEASUREMENT
|
||||||
|
_attr_entity_category = EntityCategory.DIAGNOSTIC
|
||||||
|
|
||||||
|
|
||||||
|
class SetpointChangeSourceEnum(types.enum8):
|
||||||
|
"""The source of the setpoint change."""
|
||||||
|
|
||||||
|
Manual = 0x00
|
||||||
|
Schedule = 0x01
|
||||||
|
External = 0x02
|
||||||
|
|
||||||
|
|
||||||
|
@CONFIG_DIAGNOSTIC_MATCH(cluster_handler_names=CLUSTER_HANDLER_THERMOSTAT)
|
||||||
|
# pylint: disable-next=hass-invalid-inheritance # needs fixing
|
||||||
|
class SetpointChangeSource(EnumSensor):
|
||||||
|
"""Sensor that displays the source of the setpoint change.
|
||||||
|
|
||||||
|
Optional thermostat attribute.
|
||||||
|
"""
|
||||||
|
|
||||||
|
_unique_id_suffix = "setpoint_change_source"
|
||||||
|
_attribute_name = "setpoint_change_source"
|
||||||
|
_attr_translation_key: str = "setpoint_change_source"
|
||||||
|
_attr_icon: str = "mdi:thermostat"
|
||||||
|
_attr_entity_category = EntityCategory.DIAGNOSTIC
|
||||||
|
_enum = SetpointChangeSourceEnum
|
||||||
|
@ -730,6 +730,12 @@
|
|||||||
},
|
},
|
||||||
"presence_detection_timeout": {
|
"presence_detection_timeout": {
|
||||||
"name": "Presence detection timeout"
|
"name": "Presence detection timeout"
|
||||||
|
},
|
||||||
|
"max_heat_setpoint_limit": {
|
||||||
|
"name": "Max heat setpoint limit"
|
||||||
|
},
|
||||||
|
"min_heat_setpoint_limit": {
|
||||||
|
"name": "Min heat setpoint limit"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"select": {
|
"select": {
|
||||||
@ -798,6 +804,9 @@
|
|||||||
},
|
},
|
||||||
"detection_sensitivity": {
|
"detection_sensitivity": {
|
||||||
"name": "Detection Sensitivity"
|
"name": "Detection Sensitivity"
|
||||||
|
},
|
||||||
|
"keypad_lockout": {
|
||||||
|
"name": "Keypad lockout"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"sensor": {
|
"sensor": {
|
||||||
@ -881,6 +890,12 @@
|
|||||||
},
|
},
|
||||||
"last_illumination_state": {
|
"last_illumination_state": {
|
||||||
"name": "Last illumination state"
|
"name": "Last illumination state"
|
||||||
|
},
|
||||||
|
"pi_heating_demand": {
|
||||||
|
"name": "Pi heating demand"
|
||||||
|
},
|
||||||
|
"setpoint_change_source": {
|
||||||
|
"name": "Setpoint change source"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"switch": {
|
"switch": {
|
||||||
|
@ -400,7 +400,9 @@ async def test_climate_hvac_action_running_state_zen(
|
|||||||
|
|
||||||
thrm_cluster = device_climate_zen.device.endpoints[1].thermostat
|
thrm_cluster = device_climate_zen.device.endpoints[1].thermostat
|
||||||
entity_id = find_entity_id(Platform.CLIMATE, device_climate_zen, hass)
|
entity_id = find_entity_id(Platform.CLIMATE, device_climate_zen, hass)
|
||||||
sensor_entity_id = find_entity_id(Platform.SENSOR, device_climate_zen, hass)
|
sensor_entity_id = find_entity_id(
|
||||||
|
Platform.SENSOR, device_climate_zen, hass, "hvac_action"
|
||||||
|
)
|
||||||
|
|
||||||
state = hass.states.get(entity_id)
|
state = hass.states.get(entity_id)
|
||||||
assert ATTR_HVAC_ACTION not in state.attributes
|
assert ATTR_HVAC_ACTION not in state.attributes
|
||||||
|
@ -5,7 +5,8 @@ from unittest.mock import MagicMock, patch
|
|||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
import zigpy.profiles.zha
|
import zigpy.profiles.zha
|
||||||
from zigpy.zcl.clusters import general, homeautomation, measurement, smartenergy
|
from zigpy.zcl.clusters import general, homeautomation, hvac, measurement, smartenergy
|
||||||
|
from zigpy.zcl.clusters.hvac import Thermostat
|
||||||
|
|
||||||
from homeassistant.components.sensor import SensorDeviceClass
|
from homeassistant.components.sensor import SensorDeviceClass
|
||||||
from homeassistant.components.zha.core.const import ZHA_CLUSTER_HANDLER_READS_PER_REQ
|
from homeassistant.components.zha.core.const import ZHA_CLUSTER_HANDLER_READS_PER_REQ
|
||||||
@ -342,6 +343,23 @@ async def async_test_device_temperature(hass: HomeAssistant, cluster, entity_id)
|
|||||||
assert_state(hass, entity_id, "29.0", UnitOfTemperature.CELSIUS)
|
assert_state(hass, entity_id, "29.0", UnitOfTemperature.CELSIUS)
|
||||||
|
|
||||||
|
|
||||||
|
async def async_test_setpoint_change_source(hass, cluster, entity_id):
|
||||||
|
"""Test the translation of numerical state into enum text."""
|
||||||
|
await send_attributes_report(
|
||||||
|
hass, cluster, {Thermostat.AttributeDefs.setpoint_change_source.id: 0x01}
|
||||||
|
)
|
||||||
|
hass_state = hass.states.get(entity_id)
|
||||||
|
assert hass_state.state == "Schedule"
|
||||||
|
|
||||||
|
|
||||||
|
async def async_test_pi_heating_demand(hass, cluster, entity_id):
|
||||||
|
"""Test pi heating demand is correctly returned."""
|
||||||
|
await send_attributes_report(
|
||||||
|
hass, cluster, {Thermostat.AttributeDefs.pi_heating_demand.id: 1}
|
||||||
|
)
|
||||||
|
assert_state(hass, entity_id, "1", "%")
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
(
|
(
|
||||||
"cluster_id",
|
"cluster_id",
|
||||||
@ -502,6 +520,22 @@ async def async_test_device_temperature(hass: HomeAssistant, cluster, entity_id)
|
|||||||
None,
|
None,
|
||||||
None,
|
None,
|
||||||
),
|
),
|
||||||
|
(
|
||||||
|
hvac.Thermostat.cluster_id,
|
||||||
|
"setpoint_change_source",
|
||||||
|
async_test_setpoint_change_source,
|
||||||
|
10,
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
hvac.Thermostat.cluster_id,
|
||||||
|
"pi_heating_demand",
|
||||||
|
async_test_pi_heating_demand,
|
||||||
|
10,
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
async def test_sensor(
|
async def test_sensor(
|
||||||
|
@ -4434,6 +4434,16 @@ DEVICES = [
|
|||||||
DEV_SIG_ENT_MAP_CLASS: "SinopeHVACAction",
|
DEV_SIG_ENT_MAP_CLASS: "SinopeHVACAction",
|
||||||
DEV_SIG_ENT_MAP_ID: "sensor.sinope_technologies_th1123zb_hvac_action",
|
DEV_SIG_ENT_MAP_ID: "sensor.sinope_technologies_th1123zb_hvac_action",
|
||||||
},
|
},
|
||||||
|
("sensor", "00:11:22:33:44:55:66:77-1-513-pi_heating_demand"): {
|
||||||
|
DEV_SIG_CLUSTER_HANDLERS: ["thermostat"],
|
||||||
|
DEV_SIG_ENT_MAP_CLASS: "PiHeatingDemand",
|
||||||
|
DEV_SIG_ENT_MAP_ID: "sensor.sinope_technologies_th1123zb_pi_heating_demand",
|
||||||
|
},
|
||||||
|
("sensor", "00:11:22:33:44:55:66:77-1-513-setpoint_change_source"): {
|
||||||
|
DEV_SIG_CLUSTER_HANDLERS: ["thermostat"],
|
||||||
|
DEV_SIG_ENT_MAP_CLASS: "SetpointChangeSource",
|
||||||
|
DEV_SIG_ENT_MAP_ID: "sensor.sinope_technologies_th1123zb_setpoint_change_source",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -4524,6 +4534,16 @@ DEVICES = [
|
|||||||
DEV_SIG_ENT_MAP_CLASS: "SinopeHVACAction",
|
DEV_SIG_ENT_MAP_CLASS: "SinopeHVACAction",
|
||||||
DEV_SIG_ENT_MAP_ID: "sensor.sinope_technologies_th1124zb_hvac_action",
|
DEV_SIG_ENT_MAP_ID: "sensor.sinope_technologies_th1124zb_hvac_action",
|
||||||
},
|
},
|
||||||
|
("sensor", "00:11:22:33:44:55:66:77-1-513-pi_heating_demand"): {
|
||||||
|
DEV_SIG_CLUSTER_HANDLERS: ["thermostat"],
|
||||||
|
DEV_SIG_ENT_MAP_CLASS: "PiHeatingDemand",
|
||||||
|
DEV_SIG_ENT_MAP_ID: "sensor.sinope_technologies_th1124zb_pi_heating_demand",
|
||||||
|
},
|
||||||
|
("sensor", "00:11:22:33:44:55:66:77-1-513-setpoint_change_source"): {
|
||||||
|
DEV_SIG_CLUSTER_HANDLERS: ["thermostat"],
|
||||||
|
DEV_SIG_ENT_MAP_CLASS: "SetpointChangeSource",
|
||||||
|
DEV_SIG_ENT_MAP_ID: "sensor.sinope_technologies_th1124zb_setpoint_change_source",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -4817,6 +4837,16 @@ DEVICES = [
|
|||||||
DEV_SIG_ENT_MAP_CLASS: "ThermostatHVACAction",
|
DEV_SIG_ENT_MAP_CLASS: "ThermostatHVACAction",
|
||||||
DEV_SIG_ENT_MAP_ID: "sensor.zen_within_zen_01_hvac_action",
|
DEV_SIG_ENT_MAP_ID: "sensor.zen_within_zen_01_hvac_action",
|
||||||
},
|
},
|
||||||
|
("sensor", "00:11:22:33:44:55:66:77-1-513-pi_heating_demand"): {
|
||||||
|
DEV_SIG_CLUSTER_HANDLERS: ["thermostat"],
|
||||||
|
DEV_SIG_ENT_MAP_CLASS: "PiHeatingDemand",
|
||||||
|
DEV_SIG_ENT_MAP_ID: "sensor.zen_within_zen_01_pi_heating_demand",
|
||||||
|
},
|
||||||
|
("sensor", "00:11:22:33:44:55:66:77-1-513-setpoint_change_source"): {
|
||||||
|
DEV_SIG_CLUSTER_HANDLERS: ["thermostat"],
|
||||||
|
DEV_SIG_ENT_MAP_CLASS: "SetpointChangeSource",
|
||||||
|
DEV_SIG_ENT_MAP_ID: "sensor.zen_within_zen_01_setpoint_change_source",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
Loading…
x
Reference in New Issue
Block a user