From cbdfff25ca510a5d7dff9a1cb39ce2da8afc7d65 Mon Sep 17 00:00:00 2001 From: rforro Date: Thu, 24 Feb 2022 14:48:15 +0100 Subject: [PATCH] Presets for single ZONNSMART TRV (#67157) * Presets for single ZONNSMART TRV * added zonnsmart climate tests * black8 fix --- homeassistant/components/zha/climate.py | 65 ++++++++++++ tests/components/zha/test_climate.py | 126 ++++++++++++++++++++++++ 2 files changed, 191 insertions(+) diff --git a/homeassistant/components/zha/climate.py b/homeassistant/components/zha/climate.py index d7e36c52517..4d91b84ec53 100644 --- a/homeassistant/components/zha/climate.py +++ b/homeassistant/components/zha/climate.py @@ -771,3 +771,68 @@ class StelproFanHeater(Thermostat): def hvac_modes(self) -> tuple[str, ...]: """Return only the heat mode, because the device can't be turned off.""" return (HVAC_MODE_HEAT,) + + +@STRICT_MATCH( + channel_names=CHANNEL_THERMOSTAT, + manufacturers={ + "_TZE200_hue3yfsn", + }, +) +class ZONNSMARTThermostat(Thermostat): + """ + ZONNSMART Thermostat implementation. + + Notice that this device uses two holiday presets (2: HolidayMode, + 3: HolidayModeTemp), but only one of them can be set. + """ + + PRESET_HOLIDAY = "holiday" + PRESET_FROST = "frost protect" + + def __init__(self, unique_id, zha_device, channels, **kwargs): + """Initialize ZHA Thermostat instance.""" + super().__init__(unique_id, zha_device, channels, **kwargs) + self._presets = [ + PRESET_NONE, + self.PRESET_HOLIDAY, + PRESET_SCHEDULE, + self.PRESET_FROST, + ] + self._supported_flags |= SUPPORT_PRESET_MODE + + async def async_attribute_updated(self, record): + """Handle attribute update from device.""" + if record.attr_name == "operation_preset": + if record.value == 0: + self._preset = PRESET_SCHEDULE + if record.value == 1: + self._preset = PRESET_NONE + if record.value == 2: + self._preset = self.PRESET_HOLIDAY + if record.value == 3: + self._preset = self.PRESET_HOLIDAY + if record.value == 4: + self._preset = self.PRESET_FROST + await super().async_attribute_updated(record) + + async def async_preset_handler(self, preset: str, enable: bool = False) -> bool: + """Set the preset mode.""" + mfg_code = self._zha_device.manufacturer_code + if not enable: + return await self._thrm.write_attributes( + {"operation_preset": 1}, manufacturer=mfg_code + ) + if preset == PRESET_SCHEDULE: + return await self._thrm.write_attributes( + {"operation_preset": 0}, manufacturer=mfg_code + ) + if preset == self.PRESET_HOLIDAY: + return await self._thrm.write_attributes( + {"operation_preset": 3}, manufacturer=mfg_code + ) + if preset == self.PRESET_FROST: + return await self._thrm.write_attributes( + {"operation_preset": 4}, manufacturer=mfg_code + ) + return False diff --git a/tests/components/zha/test_climate.py b/tests/components/zha/test_climate.py index 8ff787af7bd..9f856ca1df6 100644 --- a/tests/components/zha/test_climate.py +++ b/tests/components/zha/test_climate.py @@ -137,9 +137,25 @@ CLIMATE_MOES = { SIG_EP_OUTPUT: [zigpy.zcl.clusters.general.Ota.cluster_id], } } + +CLIMATE_ZONNSMART = { + 1: { + SIG_EP_PROFILE: zigpy.profiles.zha.PROFILE_ID, + SIG_EP_TYPE: zigpy.profiles.zha.DeviceType.THERMOSTAT, + SIG_EP_INPUT: [ + zigpy.zcl.clusters.general.Basic.cluster_id, + zigpy.zcl.clusters.hvac.Thermostat.cluster_id, + zigpy.zcl.clusters.hvac.UserInterface.cluster_id, + 61148, + ], + SIG_EP_OUTPUT: [zigpy.zcl.clusters.general.Ota.cluster_id], + } +} + MANUF_SINOPE = "Sinope Technologies" MANUF_ZEN = "Zen Within" MANUF_MOES = "_TZE200_ckud7u2l" +MANUF_ZONNSMART = "_TZE200_hue3yfsn" ZCL_ATTR_PLUG = { "abs_min_heat_setpoint_limit": 800, @@ -232,6 +248,17 @@ async def device_climate_moes(device_climate_mock): ) +@pytest.fixture +async def device_climate_zonnsmart(device_climate_mock): + """ZONNSMART thermostat.""" + + return await device_climate_mock( + CLIMATE_ZONNSMART, + manuf=MANUF_ZONNSMART, + quirk=zhaquirks.tuya.ts0601_trv.ZonnsmartTV01_ZG, + ) + + def test_sequence_mappings(): """Test correct mapping between control sequence -> HVAC Mode -> Sysmode.""" @@ -1326,3 +1353,102 @@ async def test_set_moes_operation_mode(hass, device_climate_moes): state = hass.states.get(entity_id) assert state.attributes[ATTR_PRESET_MODE] == PRESET_COMPLEX + + +async def test_set_zonnsmart_preset(hass, device_climate_zonnsmart): + """Test setting preset from homeassistant for zonnsmart trv.""" + + entity_id = await find_entity_id(Platform.CLIMATE, device_climate_zonnsmart, hass) + thrm_cluster = device_climate_zonnsmart.device.endpoints[1].thermostat + + state = hass.states.get(entity_id) + assert state.attributes[ATTR_PRESET_MODE] == PRESET_NONE + + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_PRESET_MODE, + {ATTR_ENTITY_ID: entity_id, ATTR_PRESET_MODE: PRESET_SCHEDULE}, + blocking=True, + ) + + assert thrm_cluster.write_attributes.await_count == 1 + assert thrm_cluster.write_attributes.call_args_list[0][0][0] == { + "operation_preset": 0 + } + + thrm_cluster.write_attributes.reset_mock() + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_PRESET_MODE, + {ATTR_ENTITY_ID: entity_id, ATTR_PRESET_MODE: "holiday"}, + blocking=True, + ) + + assert thrm_cluster.write_attributes.await_count == 2 + assert thrm_cluster.write_attributes.call_args_list[0][0][0] == { + "operation_preset": 1 + } + assert thrm_cluster.write_attributes.call_args_list[1][0][0] == { + "operation_preset": 3 + } + + thrm_cluster.write_attributes.reset_mock() + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_PRESET_MODE, + {ATTR_ENTITY_ID: entity_id, ATTR_PRESET_MODE: "frost protect"}, + blocking=True, + ) + + assert thrm_cluster.write_attributes.await_count == 2 + assert thrm_cluster.write_attributes.call_args_list[0][0][0] == { + "operation_preset": 1 + } + assert thrm_cluster.write_attributes.call_args_list[1][0][0] == { + "operation_preset": 4 + } + + thrm_cluster.write_attributes.reset_mock() + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_PRESET_MODE, + {ATTR_ENTITY_ID: entity_id, ATTR_PRESET_MODE: PRESET_NONE}, + blocking=True, + ) + + assert thrm_cluster.write_attributes.await_count == 1 + assert thrm_cluster.write_attributes.call_args_list[0][0][0] == { + "operation_preset": 1 + } + + +async def test_set_zonnsmart_operation_mode(hass, device_climate_zonnsmart): + """Test setting preset from trv for zonnsmart trv.""" + + entity_id = await find_entity_id(Platform.CLIMATE, device_climate_zonnsmart, hass) + thrm_cluster = device_climate_zonnsmart.device.endpoints[1].thermostat + + await send_attributes_report(hass, thrm_cluster, {"operation_preset": 0}) + + state = hass.states.get(entity_id) + assert state.attributes[ATTR_PRESET_MODE] == PRESET_SCHEDULE + + await send_attributes_report(hass, thrm_cluster, {"operation_preset": 1}) + + state = hass.states.get(entity_id) + assert state.attributes[ATTR_PRESET_MODE] == PRESET_NONE + + await send_attributes_report(hass, thrm_cluster, {"operation_preset": 2}) + + state = hass.states.get(entity_id) + assert state.attributes[ATTR_PRESET_MODE] == "holiday" + + await send_attributes_report(hass, thrm_cluster, {"operation_preset": 3}) + + state = hass.states.get(entity_id) + assert state.attributes[ATTR_PRESET_MODE] == "holiday" + + await send_attributes_report(hass, thrm_cluster, {"operation_preset": 4}) + + state = hass.states.get(entity_id) + assert state.attributes[ATTR_PRESET_MODE] == "frost protect"