Skip to content

Commit 6794a64

Browse files
authored
Add Danfoss Ally thermostat and derivatives to ZHA #86907 on core (#57)
1 parent d971bab commit 6794a64

8 files changed

Lines changed: 597 additions & 6 deletions

File tree

tests/test_sensor.py

Lines changed: 67 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
from typing import Any, Optional
77

88
import pytest
9+
from zhaquirks.danfoss import thermostat as danfoss_thermostat
910
from zigpy.device import Device as ZigpyDevice
1011
import zigpy.profiles.zha
1112
from zigpy.quirks import CustomCluster, get_device
@@ -21,7 +22,7 @@
2122
from zha.application.const import ZHA_CLUSTER_HANDLER_READS_PER_REQ
2223
from zha.application.gateway import Gateway
2324
from zha.application.platforms import PlatformEntity, sensor
24-
from zha.application.platforms.sensor import UnitOfMass
25+
from zha.application.platforms.sensor import DanfossSoftwareErrorCode, UnitOfMass
2526
from zha.application.platforms.sensor.const import SensorDeviceClass
2627
from zha.units import PERCENTAGE, UnitOfEnergy, UnitOfPressure, UnitOfVolume
2728
from zha.zigbee.device import Device
@@ -330,7 +331,6 @@ async def async_test_powerconfiguration(
330331
"battery_voltage",
331332
"battery_quantity",
332333
"battery_size",
333-
"battery_voltage",
334334
}
335335
await send_attributes_report(zha_gateway, cluster, {33: 98})
336336
assert_state(entity, 49, "%")
@@ -1229,3 +1229,68 @@ async def test_device_unavailable_skips_entity_polling(
12291229
"00:0d:6f:00:0a:90:69:e7-1-0-rssi: skipping polling for updated state, "
12301230
"available: False, allow polled requests: True" in caplog.text
12311231
)
1232+
1233+
1234+
@pytest.fixture
1235+
async def zigpy_device_danfoss_thermostat(
1236+
zigpy_device_mock: Callable[..., ZigpyDevice], # pylint: disable=redefined-outer-name
1237+
device_joined: Callable[[ZigpyDevice], Awaitable[Device]],
1238+
) -> Device:
1239+
"""Danfoss thermostat device."""
1240+
1241+
zigpy_device = zigpy_device_mock(
1242+
{
1243+
1: {
1244+
SIG_EP_INPUT: [
1245+
general.Basic.cluster_id,
1246+
general.PowerConfiguration.cluster_id,
1247+
general.Identify.cluster_id,
1248+
general.Time.cluster_id,
1249+
general.PollControl.cluster_id,
1250+
hvac.Thermostat.cluster_id,
1251+
hvac.UserInterface.cluster_id,
1252+
homeautomation.Diagnostic.cluster_id,
1253+
],
1254+
SIG_EP_OUTPUT: [general.Basic.cluster_id, general.Ota.cluster_id],
1255+
SIG_EP_TYPE: zigpy.profiles.zha.DeviceType.THERMOSTAT,
1256+
}
1257+
},
1258+
manufacturer="Danfoss",
1259+
model="eTRV0100",
1260+
quirk=danfoss_thermostat.DanfossThermostat,
1261+
)
1262+
1263+
zha_device = await device_joined(zigpy_device)
1264+
return zha_device, zigpy_device
1265+
1266+
1267+
async def test_danfoss_thermostat_sw_error(
1268+
zha_gateway: Gateway,
1269+
zigpy_device_danfoss_thermostat, # pylint: disable=redefined-outer-name
1270+
) -> None:
1271+
"""Test quirks defined thermostat."""
1272+
1273+
zha_device, zigpy_device = zigpy_device_danfoss_thermostat
1274+
1275+
entity = get_entity(
1276+
zha_device,
1277+
platform=Platform.SENSOR,
1278+
exact_entity_type=DanfossSoftwareErrorCode,
1279+
qualifier="sw_error_code",
1280+
)
1281+
assert entity is not None
1282+
1283+
cluster = zigpy_device.endpoints[1].diagnostic
1284+
1285+
await send_attributes_report(
1286+
zha_gateway,
1287+
cluster,
1288+
{
1289+
danfoss_thermostat.DanfossDiagnosticCluster.AttributeDefs.sw_error_code.id: 0x0001
1290+
},
1291+
)
1292+
1293+
assert entity.state["state"] == "something"
1294+
assert entity.extra_state_attribute_names
1295+
assert "Top_pcb_sensor_error" in entity.extra_state_attribute_names
1296+
assert entity.state["Top_pcb_sensor_error"]

zha/application/platforms/binary_sensor/__init__.py

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import logging
88
from typing import TYPE_CHECKING
99

10+
from zhaquirks.quirk_ids import DANFOSS_ALLY_THERMOSTAT
1011
from zigpy.quirks.v2 import BinarySensorMetadata
1112

1213
from zha.application import Platform
@@ -25,6 +26,7 @@
2526
CLUSTER_HANDLER_HUE_OCCUPANCY,
2627
CLUSTER_HANDLER_OCCUPANCY,
2728
CLUSTER_HANDLER_ON_OFF,
29+
CLUSTER_HANDLER_THERMOSTAT,
2830
CLUSTER_HANDLER_ZONE,
2931
)
3032

@@ -358,3 +360,43 @@ class AqaraE1CurtainMotorOpenedByHandBinarySensor(BinarySensor):
358360
_attribute_name = "hand_open"
359361
_attr_translation_key = "hand_open"
360362
_attr_entity_category = EntityCategory.DIAGNOSTIC
363+
364+
365+
@CONFIG_DIAGNOSTIC_MATCH(
366+
cluster_handler_names=CLUSTER_HANDLER_THERMOSTAT,
367+
quirk_ids={DANFOSS_ALLY_THERMOSTAT},
368+
)
369+
class DanfossMountingModeActive(BinarySensor):
370+
"""Danfoss TRV proprietary attribute exposing whether in mounting mode."""
371+
372+
_unique_id_suffix = "mounting_mode_active"
373+
_attribute_name = "mounting_mode_active"
374+
_attr_translation_key: str = "mounting_mode_active"
375+
_attr_device_class: BinarySensorDeviceClass = BinarySensorDeviceClass.OPENING
376+
_attr_entity_category = EntityCategory.DIAGNOSTIC
377+
378+
379+
@MULTI_MATCH(
380+
cluster_handler_names=CLUSTER_HANDLER_THERMOSTAT,
381+
quirk_ids={DANFOSS_ALLY_THERMOSTAT},
382+
)
383+
class DanfossHeatRequired(BinarySensor):
384+
"""Danfoss TRV proprietary attribute exposing whether heat is required."""
385+
386+
_unique_id_suffix = "heat_required"
387+
_attribute_name = "heat_required"
388+
_attr_translation_key: str = "heat_required"
389+
390+
391+
@CONFIG_DIAGNOSTIC_MATCH(
392+
cluster_handler_names=CLUSTER_HANDLER_THERMOSTAT,
393+
quirk_ids={DANFOSS_ALLY_THERMOSTAT},
394+
)
395+
class DanfossPreheatStatus(BinarySensor):
396+
"""Danfoss TRV proprietary attribute exposing whether in pre-heating mode."""
397+
398+
_unique_id_suffix = "preheat_status"
399+
_attribute_name = "preheat_status"
400+
_attr_translation_key: str = "preheat_status"
401+
_attr_entity_registry_enabled_default = False
402+
_attr_entity_category = EntityCategory.DIAGNOSTIC

zha/application/platforms/number/__init__.py

Lines changed: 70 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import logging
88
from typing import TYPE_CHECKING, Any, Self
99

10+
from zhaquirks.quirk_ids import DANFOSS_ALLY_THERMOSTAT
1011
from zigpy.quirks.v2 import NumberMetadata
1112
from zigpy.zcl.clusters.hvac import Thermostat
1213

@@ -21,7 +22,7 @@
2122
NumberMode,
2223
)
2324
from zha.application.registries import PLATFORM_ENTITIES
24-
from zha.units import UnitOfMass, UnitOfTemperature, validate_unit
25+
from zha.units import UnitOfMass, UnitOfTemperature, UnitOfTime, validate_unit
2526
from zha.zigbee.cluster_handlers import ClusterAttributeUpdatedEvent
2627
from zha.zigbee.cluster_handlers.const import (
2728
CLUSTER_HANDLER_ANALOG_OUTPUT,
@@ -144,6 +145,7 @@ def native_step(self) -> float | None:
144145
def name(self) -> str | None:
145146
"""Return the name of the number entity."""
146147
description = self._analog_output_cluster_handler.description
148+
# TODO what happened here?
147149
if description is not None and len(description) > 0:
148150
return f"{super().name} {description}"
149151
return super().name
@@ -899,3 +901,70 @@ class MinHeatSetpointLimit(ZCLHeatSetpointLimitEntity):
899901
_attr_entity_category = EntityCategory.CONFIG
900902

901903
_max_source = Thermostat.AttributeDefs.max_heat_setpoint_limit.name
904+
905+
906+
@CONFIG_DIAGNOSTIC_MATCH(
907+
cluster_handler_names=CLUSTER_HANDLER_THERMOSTAT,
908+
quirk_ids={DANFOSS_ALLY_THERMOSTAT},
909+
)
910+
class DanfossExerciseTriggerTime(NumberConfigurationEntity):
911+
"""Danfoss proprietary attribute to set the time to exercise the valve."""
912+
913+
_unique_id_suffix = "exercise_trigger_time"
914+
_attribute_name: str = "exercise_trigger_time"
915+
_attr_translation_key: str = "exercise_trigger_time"
916+
_attr_native_min_value: int = 0
917+
_attr_native_max_value: int = 1439
918+
_attr_mode: NumberMode = NumberMode.BOX
919+
_attr_native_unit_of_measurement: str = UnitOfTime.MINUTES
920+
_attr_icon: str = "mdi:clock"
921+
922+
923+
@CONFIG_DIAGNOSTIC_MATCH(
924+
cluster_handler_names=CLUSTER_HANDLER_THERMOSTAT,
925+
quirk_ids={DANFOSS_ALLY_THERMOSTAT},
926+
)
927+
class DanfossExternalMeasuredRoomSensor(ZCLTemperatureEntity):
928+
"""Danfoss proprietary attribute to communicate the value of the external temperature sensor."""
929+
930+
_unique_id_suffix = "external_measured_room_sensor"
931+
_attribute_name: str = "external_measured_room_sensor"
932+
_attr_translation_key: str = "external_temperature_sensor"
933+
_attr_native_min_value: float = -80
934+
_attr_native_max_value: float = 35
935+
_attr_icon: str = "mdi:thermometer"
936+
937+
938+
@CONFIG_DIAGNOSTIC_MATCH(
939+
cluster_handler_names=CLUSTER_HANDLER_THERMOSTAT,
940+
quirk_ids={DANFOSS_ALLY_THERMOSTAT},
941+
)
942+
class DanfossLoadRoomMean(NumberConfigurationEntity):
943+
"""Danfoss proprietary attribute to set a value for the load."""
944+
945+
_unique_id_suffix = "load_room_mean"
946+
_attribute_name: str = "load_room_mean"
947+
_attr_translation_key: str = "load_room_mean"
948+
_attr_native_min_value: int = -8000
949+
_attr_native_max_value: int = 2000
950+
_attr_mode: NumberMode = NumberMode.BOX
951+
_attr_icon: str = "mdi:scale-balance"
952+
953+
954+
@CONFIG_DIAGNOSTIC_MATCH(
955+
cluster_handler_names=CLUSTER_HANDLER_THERMOSTAT,
956+
quirk_ids={DANFOSS_ALLY_THERMOSTAT},
957+
)
958+
class DanfossRegulationSetpointOffset(NumberConfigurationEntity):
959+
"""Danfoss proprietary attribute to set the regulation setpoint offset."""
960+
961+
_unique_id_suffix = "regulation_setpoint_offset"
962+
_attribute_name: str = "regulation_setpoint_offset"
963+
_attr_translation_key: str = "regulation_setpoint_offset"
964+
_attr_mode: NumberMode = NumberMode.BOX
965+
_attr_native_unit_of_measurement: str = UnitOfTemperature.CELSIUS
966+
_attr_icon: str = "mdi:thermostat"
967+
_attr_native_min_value: float = -2.5
968+
_attr_native_max_value: float = 2.5
969+
_attr_native_step: float = 0.1
970+
_attr_multiplier = 1 / 10

zha/application/platforms/select.py

Lines changed: 109 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,12 @@
88
import logging
99
from typing import TYPE_CHECKING, Any, Self
1010

11-
from zhaquirks.quirk_ids import TUYA_PLUG_MANUFACTURER, TUYA_PLUG_ONOFF
11+
from zhaquirks.danfoss import thermostat as danfoss_thermostat
12+
from zhaquirks.quirk_ids import (
13+
DANFOSS_ALLY_THERMOSTAT,
14+
TUYA_PLUG_MANUFACTURER,
15+
TUYA_PLUG_ONOFF,
16+
)
1217
from zhaquirks.xiaomi.aqara.magnet_ac01 import OppleCluster as MagnetAC01OppleCluster
1318
from zhaquirks.xiaomi.aqara.switch_acn047 import OppleCluster as T2RelayOppleCluster
1419
from zigpy import types
@@ -28,6 +33,7 @@
2833
CLUSTER_HANDLER_INOVELLI,
2934
CLUSTER_HANDLER_OCCUPANCY,
3035
CLUSTER_HANDLER_ON_OFF,
36+
CLUSTER_HANDLER_THERMOSTAT,
3137
)
3238

3339
if TYPE_CHECKING:
@@ -695,3 +701,105 @@ class KeypadLockout(ZCLEnumSelectEntity):
695701
_attribute_name: str = "keypad_lockout"
696702
_enum = KeypadLockoutEnum
697703
_attr_translation_key: str = "keypad_lockout"
704+
705+
706+
@CONFIG_DIAGNOSTIC_MATCH(
707+
cluster_handler_names=CLUSTER_HANDLER_THERMOSTAT,
708+
quirk_ids={DANFOSS_ALLY_THERMOSTAT},
709+
)
710+
class DanfossExerciseDayOfTheWeek(ZCLEnumSelectEntity):
711+
"""Danfoss proprietary attribute for setting the day of the week for exercising."""
712+
713+
_unique_id_suffix = "exercise_day_of_week"
714+
_attribute_name = "exercise_day_of_week"
715+
_attr_translation_key: str = "exercise_day_of_week"
716+
_enum = danfoss_thermostat.DanfossExerciseDayOfTheWeekEnum
717+
_attr_icon: str = "mdi:wrench-clock"
718+
719+
720+
class DanfossOrientationEnum(types.enum8):
721+
"""Vertical or Horizontal."""
722+
723+
Horizontal = 0x00
724+
Vertical = 0x01
725+
726+
727+
@CONFIG_DIAGNOSTIC_MATCH(
728+
cluster_handler_names=CLUSTER_HANDLER_THERMOSTAT,
729+
quirk_ids={DANFOSS_ALLY_THERMOSTAT},
730+
)
731+
class DanfossOrientation(ZCLEnumSelectEntity):
732+
"""Danfoss proprietary attribute for setting the orientation of the valve.
733+
734+
Needed for biasing the internal temperature sensor.
735+
This is implemented as an enum here, but is a boolean on the device.
736+
"""
737+
738+
_unique_id_suffix = "orientation"
739+
_attribute_name = "orientation"
740+
_attr_translation_key: str = "valve_orientation"
741+
_enum = DanfossOrientationEnum
742+
743+
744+
@CONFIG_DIAGNOSTIC_MATCH(
745+
cluster_handler_names=CLUSTER_HANDLER_THERMOSTAT,
746+
quirk_ids={DANFOSS_ALLY_THERMOSTAT},
747+
)
748+
class DanfossAdaptationRunControl(ZCLEnumSelectEntity):
749+
"""Danfoss proprietary attribute for controlling the current adaptation run."""
750+
751+
_unique_id_suffix = "adaptation_run_control"
752+
_attribute_name = "adaptation_run_control"
753+
_attr_translation_key: str = "adaptation_run_command"
754+
_enum = danfoss_thermostat.DanfossAdaptationRunControlEnum
755+
756+
757+
class DanfossControlAlgorithmScaleFactorEnum(types.enum8):
758+
"""The time scale factor for changing the opening of the valve.
759+
760+
Not all values are given, therefore there are some extrapolated values with a margin of error of about 5 minutes.
761+
This is implemented as an enum here, but is a number on the device.
762+
"""
763+
764+
quick_5min = 0x01
765+
766+
quick_10min = 0x02 # extrapolated
767+
quick_15min = 0x03 # extrapolated
768+
quick_25min = 0x04 # extrapolated
769+
770+
moderate_30min = 0x05
771+
772+
moderate_40min = 0x06 # extrapolated
773+
moderate_50min = 0x07 # extrapolated
774+
moderate_60min = 0x08 # extrapolated
775+
moderate_70min = 0x09 # extrapolated
776+
777+
slow_80min = 0x0A
778+
779+
quick_open_disabled = 0x11 # not sure what it does; also requires lower 4 bits to be in [1, 10] I assume
780+
781+
782+
@CONFIG_DIAGNOSTIC_MATCH(
783+
cluster_handler_names=CLUSTER_HANDLER_THERMOSTAT,
784+
quirk_ids={DANFOSS_ALLY_THERMOSTAT},
785+
)
786+
class DanfossControlAlgorithmScaleFactor(ZCLEnumSelectEntity):
787+
"""Danfoss proprietary attribute for setting the scale factor of the setpoint filter time constant."""
788+
789+
_unique_id_suffix = "control_algorithm_scale_factor"
790+
_attribute_name = "control_algorithm_scale_factor"
791+
_attr_translation_key: str = "setpoint_response_time"
792+
_enum = DanfossControlAlgorithmScaleFactorEnum
793+
794+
795+
@CONFIG_DIAGNOSTIC_MATCH(
796+
cluster_handler_names="thermostat_ui",
797+
quirk_ids={DANFOSS_ALLY_THERMOSTAT},
798+
)
799+
class DanfossViewingDirection(ZCLEnumSelectEntity):
800+
"""Danfoss proprietary attribute for setting the viewing direction of the screen."""
801+
802+
_unique_id_suffix = "viewing_direction"
803+
_attribute_name = "viewing_direction"
804+
_attr_translation_key: str = "viewing_direction"
805+
_enum = danfoss_thermostat.DanfossViewingDirectionEnum

0 commit comments

Comments
 (0)