From 9739707f62cc394154a4ec407a863d17792d6d8e Mon Sep 17 00:00:00 2001 From: xonestonex <51967236+xonestonex@users.noreply.github.com> Date: Sun, 21 Mar 2021 23:03:23 +0100 Subject: [PATCH] Preset support for MOES thermostat valves (#48178) --- homeassistant/components/zha/climate.py | 90 ++++++++++ homeassistant/components/zha/core/const.py | 3 + tests/components/zha/test_climate.py | 183 +++++++++++++++++++++ 3 files changed, 276 insertions(+) diff --git a/homeassistant/components/zha/climate.py b/homeassistant/components/zha/climate.py index e2c4faa8539..8292335ce23 100644 --- a/homeassistant/components/zha/climate.py +++ b/homeassistant/components/zha/climate.py @@ -31,6 +31,9 @@ from homeassistant.components.climate.const import ( HVAC_MODE_HEAT_COOL, HVAC_MODE_OFF, PRESET_AWAY, + PRESET_BOOST, + PRESET_COMFORT, + PRESET_ECO, PRESET_NONE, SUPPORT_FAN_MODE, SUPPORT_PRESET_MODE, @@ -49,6 +52,8 @@ from .core.const import ( CHANNEL_THERMOSTAT, DATA_ZHA, DATA_ZHA_DISPATCHERS, + PRESET_COMPLEX, + PRESET_SCHEDULE, SIGNAL_ADD_ENTITIES, SIGNAL_ATTR_UPDATED, ) @@ -595,3 +600,88 @@ class ZenWithinThermostat(Thermostat): ) class CentralitePearl(ZenWithinThermostat): """Centralite Pearl Thermostat implementation.""" + + +@STRICT_MATCH( + channel_names=CHANNEL_THERMOSTAT, + manufacturers={ + "_TZE200_ckud7u2l", + "_TZE200_ywdxldoj", + "_TYST11_ckud7u2l", + "_TYST11_ywdxldoj", + }, +) +class MoesThermostat(Thermostat): + """Moes Thermostat implementation.""" + + 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, + PRESET_AWAY, + PRESET_SCHEDULE, + PRESET_COMFORT, + PRESET_ECO, + PRESET_BOOST, + PRESET_COMPLEX, + ] + self._supported_flags |= SUPPORT_PRESET_MODE + + @property + def hvac_modes(self) -> tuple[str, ...]: + """Return only the heat mode, because the device can't be turned off.""" + return (HVAC_MODE_HEAT,) + + 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_AWAY + if record.value == 1: + self._preset = PRESET_SCHEDULE + if record.value == 2: + self._preset = PRESET_NONE + if record.value == 3: + self._preset = PRESET_COMFORT + if record.value == 4: + self._preset = PRESET_ECO + if record.value == 5: + self._preset = PRESET_BOOST + if record.value == 6: + self._preset = PRESET_COMPLEX + 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": 2}, manufacturer=mfg_code + ) + if preset == PRESET_AWAY: + return await self._thrm.write_attributes( + {"operation_preset": 0}, manufacturer=mfg_code + ) + if preset == PRESET_SCHEDULE: + return await self._thrm.write_attributes( + {"operation_preset": 1}, manufacturer=mfg_code + ) + if preset == PRESET_COMFORT: + return await self._thrm.write_attributes( + {"operation_preset": 3}, manufacturer=mfg_code + ) + if preset == PRESET_ECO: + return await self._thrm.write_attributes( + {"operation_preset": 4}, manufacturer=mfg_code + ) + if preset == PRESET_BOOST: + return await self._thrm.write_attributes( + {"operation_preset": 5}, manufacturer=mfg_code + ) + if preset == PRESET_COMPLEX: + return await self._thrm.write_attributes( + {"operation_preset": 6}, manufacturer=mfg_code + ) + + return False diff --git a/homeassistant/components/zha/core/const.py b/homeassistant/components/zha/core/const.py index 45453ba7545..ed45400861a 100644 --- a/homeassistant/components/zha/core/const.py +++ b/homeassistant/components/zha/core/const.py @@ -173,6 +173,9 @@ MFG_CLUSTER_ID_START = 0xFC00 POWER_MAINS_POWERED = "Mains" POWER_BATTERY_OR_UNKNOWN = "Battery or Unknown" +PRESET_SCHEDULE = "schedule" +PRESET_COMPLEX = "complex" + class RadioType(enum.Enum): # pylint: disable=invalid-name diff --git a/tests/components/zha/test_climate.py b/tests/components/zha/test_climate.py index 05412ddb64d..ea2d6dfb7e3 100644 --- a/tests/components/zha/test_climate.py +++ b/tests/components/zha/test_climate.py @@ -33,6 +33,9 @@ from homeassistant.components.climate.const import ( HVAC_MODE_HEAT_COOL, HVAC_MODE_OFF, PRESET_AWAY, + PRESET_BOOST, + PRESET_COMFORT, + PRESET_ECO, PRESET_NONE, SERVICE_SET_FAN_MODE, SERVICE_SET_HVAC_MODE, @@ -44,6 +47,7 @@ from homeassistant.components.zha.climate import ( HVAC_MODE_2_SYSTEM, SEQ_OF_OPERATION, ) +from homeassistant.components.zha.core.const import PRESET_COMPLEX, PRESET_SCHEDULE from homeassistant.const import ATTR_ENTITY_ID, ATTR_TEMPERATURE, STATE_UNKNOWN from .common import async_enable_traffic, find_entity_id, send_attributes_report @@ -103,8 +107,23 @@ CLIMATE_ZEN = { "out_clusters": [zigpy.zcl.clusters.general.Ota.cluster_id], } } + +CLIMATE_MOES = { + 1: { + "device_type": zigpy.profiles.zha.DeviceType.THERMOSTAT, + "in_clusters": [ + zigpy.zcl.clusters.general.Basic.cluster_id, + zigpy.zcl.clusters.general.Identify.cluster_id, + zigpy.zcl.clusters.hvac.Thermostat.cluster_id, + zigpy.zcl.clusters.hvac.UserInterface.cluster_id, + 61148, + ], + "out_clusters": [zigpy.zcl.clusters.general.Ota.cluster_id], + } +} MANUF_SINOPE = "Sinope Technologies" MANUF_ZEN = "Zen Within" +MANUF_MOES = "_TZE200_ckud7u2l" ZCL_ATTR_PLUG = { "abs_min_heat_setpoint_limit": 800, @@ -183,6 +202,13 @@ async def device_climate_zen(device_climate_mock): return await device_climate_mock(CLIMATE_ZEN, manuf=MANUF_ZEN) +@pytest.fixture +async def device_climate_moes(device_climate_mock): + """MOES thermostat.""" + + return await device_climate_mock(CLIMATE_MOES, manuf=MANUF_MOES) + + def test_sequence_mappings(): """Test correct mapping between control sequence -> HVAC Mode -> Sysmode.""" @@ -1106,3 +1132,160 @@ async def test_set_fan_mode(hass, device_climate_fan): ) assert fan_cluster.write_attributes.await_count == 1 assert fan_cluster.write_attributes.call_args[0][0] == {"fan_mode": 5} + + +async def test_set_moes_preset(hass, device_climate_moes): + """Test setting preset for moes trv.""" + + entity_id = await find_entity_id(DOMAIN, device_climate_moes, hass) + thrm_cluster = device_climate_moes.device.endpoints[1].thermostat + + state = hass.states.get(entity_id) + assert state.attributes[ATTR_PRESET_MODE] == PRESET_NONE + + await hass.services.async_call( + DOMAIN, + SERVICE_SET_PRESET_MODE, + {ATTR_ENTITY_ID: entity_id, ATTR_PRESET_MODE: PRESET_AWAY}, + 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( + DOMAIN, + SERVICE_SET_PRESET_MODE, + {ATTR_ENTITY_ID: entity_id, ATTR_PRESET_MODE: PRESET_SCHEDULE}, + blocking=True, + ) + + assert thrm_cluster.write_attributes.await_count == 2 + assert thrm_cluster.write_attributes.call_args_list[0][0][0] == { + "operation_preset": 2 + } + assert thrm_cluster.write_attributes.call_args_list[1][0][0] == { + "operation_preset": 1 + } + + thrm_cluster.write_attributes.reset_mock() + await hass.services.async_call( + DOMAIN, + SERVICE_SET_PRESET_MODE, + {ATTR_ENTITY_ID: entity_id, ATTR_PRESET_MODE: PRESET_COMFORT}, + blocking=True, + ) + + assert thrm_cluster.write_attributes.await_count == 2 + assert thrm_cluster.write_attributes.call_args_list[0][0][0] == { + "operation_preset": 2 + } + 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( + DOMAIN, + SERVICE_SET_PRESET_MODE, + {ATTR_ENTITY_ID: entity_id, ATTR_PRESET_MODE: PRESET_ECO}, + blocking=True, + ) + + assert thrm_cluster.write_attributes.await_count == 2 + assert thrm_cluster.write_attributes.call_args_list[0][0][0] == { + "operation_preset": 2 + } + 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( + DOMAIN, + SERVICE_SET_PRESET_MODE, + {ATTR_ENTITY_ID: entity_id, ATTR_PRESET_MODE: PRESET_BOOST}, + blocking=True, + ) + + assert thrm_cluster.write_attributes.await_count == 2 + assert thrm_cluster.write_attributes.call_args_list[0][0][0] == { + "operation_preset": 2 + } + assert thrm_cluster.write_attributes.call_args_list[1][0][0] == { + "operation_preset": 5 + } + + thrm_cluster.write_attributes.reset_mock() + await hass.services.async_call( + DOMAIN, + SERVICE_SET_PRESET_MODE, + {ATTR_ENTITY_ID: entity_id, ATTR_PRESET_MODE: PRESET_COMPLEX}, + blocking=True, + ) + + assert thrm_cluster.write_attributes.await_count == 2 + assert thrm_cluster.write_attributes.call_args_list[0][0][0] == { + "operation_preset": 2 + } + assert thrm_cluster.write_attributes.call_args_list[1][0][0] == { + "operation_preset": 6 + } + + thrm_cluster.write_attributes.reset_mock() + await hass.services.async_call( + 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": 2 + } + + +async def test_set_moes_operation_mode(hass, device_climate_moes): + """Test setting preset for moes trv.""" + + entity_id = await find_entity_id(DOMAIN, device_climate_moes, hass) + thrm_cluster = device_climate_moes.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_AWAY + + await send_attributes_report(hass, thrm_cluster, {"operation_preset": 1}) + + state = hass.states.get(entity_id) + assert state.attributes[ATTR_PRESET_MODE] == PRESET_SCHEDULE + + await send_attributes_report(hass, thrm_cluster, {"operation_preset": 2}) + + state = hass.states.get(entity_id) + assert state.attributes[ATTR_PRESET_MODE] == PRESET_NONE + + await send_attributes_report(hass, thrm_cluster, {"operation_preset": 3}) + + state = hass.states.get(entity_id) + assert state.attributes[ATTR_PRESET_MODE] == PRESET_COMFORT + + await send_attributes_report(hass, thrm_cluster, {"operation_preset": 4}) + + state = hass.states.get(entity_id) + assert state.attributes[ATTR_PRESET_MODE] == PRESET_ECO + + await send_attributes_report(hass, thrm_cluster, {"operation_preset": 5}) + + state = hass.states.get(entity_id) + assert state.attributes[ATTR_PRESET_MODE] == PRESET_BOOST + + await send_attributes_report(hass, thrm_cluster, {"operation_preset": 6}) + + state = hass.states.get(entity_id) + assert state.attributes[ATTR_PRESET_MODE] == PRESET_COMPLEX