mirror of
https://github.com/home-assistant/core.git
synced 2025-04-25 01:38:02 +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_heat_setpoint_limit.name: True,
|
||||
Thermostat.AttributeDefs.local_temperature_calibration.name: True,
|
||||
Thermostat.AttributeDefs.setpoint_change_source.name: True,
|
||||
}
|
||||
|
||||
@property
|
||||
@ -341,3 +342,5 @@ class ThermostatClusterHandler(ClusterHandler):
|
||||
@registries.ZIGBEE_CLUSTER_HANDLER_REGISTRY.register(UserInterface.cluster_id)
|
||||
class UserInterfaceClusterHandler(ClusterHandler):
|
||||
"""User interface (thermostat) cluster handler."""
|
||||
|
||||
ZCL_INIT_ATTRS = {UserInterface.AttributeDefs.keypad_lockout.name: True}
|
||||
|
@ -5,6 +5,8 @@ import functools
|
||||
import logging
|
||||
from typing import TYPE_CHECKING, Any, Self
|
||||
|
||||
from zigpy.zcl.clusters.hvac import Thermostat
|
||||
|
||||
from homeassistant.components.number import NumberEntity, NumberMode
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import EntityCategory, Platform, UnitOfMass, UnitOfTemperature
|
||||
@ -985,3 +987,69 @@ class SonoffPresenceSenorTimeout(ZHANumberConfigurationEntity):
|
||||
|
||||
_attr_mode: NumberMode = NumberMode.BOX
|
||||
_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"
|
||||
_enum = SonoffPresenceDetectionSensitivityEnum
|
||||
_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)
|
||||
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(
|
||||
@ -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(
|
||||
cluster_handler_names=CLUSTER_HANDLER_ANALOG_INPUT,
|
||||
manufacturers="Digi",
|
||||
@ -1254,3 +1270,45 @@ class SonoffPresenceSenorIlluminationStatus(Sensor):
|
||||
def formatter(self, value: int) -> int | float | None:
|
||||
"""Numeric pass-through formatter."""
|
||||
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": {
|
||||
"name": "Presence detection timeout"
|
||||
},
|
||||
"max_heat_setpoint_limit": {
|
||||
"name": "Max heat setpoint limit"
|
||||
},
|
||||
"min_heat_setpoint_limit": {
|
||||
"name": "Min heat setpoint limit"
|
||||
}
|
||||
},
|
||||
"select": {
|
||||
@ -798,6 +804,9 @@
|
||||
},
|
||||
"detection_sensitivity": {
|
||||
"name": "Detection Sensitivity"
|
||||
},
|
||||
"keypad_lockout": {
|
||||
"name": "Keypad lockout"
|
||||
}
|
||||
},
|
||||
"sensor": {
|
||||
@ -881,6 +890,12 @@
|
||||
},
|
||||
"last_illumination_state": {
|
||||
"name": "Last illumination state"
|
||||
},
|
||||
"pi_heating_demand": {
|
||||
"name": "Pi heating demand"
|
||||
},
|
||||
"setpoint_change_source": {
|
||||
"name": "Setpoint change source"
|
||||
}
|
||||
},
|
||||
"switch": {
|
||||
|
@ -400,7 +400,9 @@ async def test_climate_hvac_action_running_state_zen(
|
||||
|
||||
thrm_cluster = device_climate_zen.device.endpoints[1].thermostat
|
||||
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)
|
||||
assert ATTR_HVAC_ACTION not in state.attributes
|
||||
|
@ -5,7 +5,8 @@ from unittest.mock import MagicMock, patch
|
||||
|
||||
import pytest
|
||||
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.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)
|
||||
|
||||
|
||||
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(
|
||||
(
|
||||
"cluster_id",
|
||||
@ -502,6 +520,22 @@ async def async_test_device_temperature(hass: HomeAssistant, cluster, entity_id)
|
||||
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(
|
||||
|
@ -4434,6 +4434,16 @@ DEVICES = [
|
||||
DEV_SIG_ENT_MAP_CLASS: "SinopeHVACAction",
|
||||
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_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_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