diff --git a/homeassistant/components/zha/climate.py b/homeassistant/components/zha/climate.py index 20b52baf72e..3a0ff6455d2 100644 --- a/homeassistant/components/zha/climate.py +++ b/homeassistant/components/zha/climate.py @@ -103,6 +103,8 @@ SEQ_OF_OPERATION = { 0x04: (HVAC_MODE_OFF, HVAC_MODE_HEAT_COOL, HVAC_MODE_COOL, HVAC_MODE_HEAT), # cooling and heating 4-pipes 0x05: (HVAC_MODE_OFF, HVAC_MODE_HEAT_COOL, HVAC_MODE_COOL, HVAC_MODE_HEAT), + 0x06: (HVAC_MODE_COOL, HVAC_MODE_HEAT, HVAC_MODE_OFF), # centralite specific + 0x07: (HVAC_MODE_HEAT_COOL, HVAC_MODE_OFF), # centralite specific } @@ -234,24 +236,39 @@ class Thermostat(ZhaEntity, ClimateEntity): self._thrm.pi_heating_demand is None and self._thrm.pi_cooling_demand is None ): - running_state = self._thrm.running_state - if running_state is None: - return None - if running_state & (RunningState.HEAT | RunningState.HEAT_STAGE_2): - return CURRENT_HVAC_HEAT - if running_state & (RunningState.COOL | RunningState.COOL_STAGE_2): - return CURRENT_HVAC_COOL - if running_state & ( - RunningState.FAN | RunningState.FAN_STAGE_2 | RunningState.FAN_STAGE_3 - ): - return CURRENT_HVAC_FAN - else: - heating_demand = self._thrm.pi_heating_demand - if heating_demand is not None and heating_demand > 0: - return CURRENT_HVAC_HEAT - cooling_demand = self._thrm.pi_cooling_demand - if cooling_demand is not None and cooling_demand > 0: - return CURRENT_HVAC_COOL + return self._rm_rs_action + return self._pi_demand_action + + @property + def _rm_rs_action(self) -> Optional[str]: + """Return the current HVAC action based on running mode and running state.""" + + running_mode = self._thrm.running_mode + if running_mode == SystemMode.HEAT: + return CURRENT_HVAC_HEAT + if running_mode == SystemMode.COOL: + return CURRENT_HVAC_COOL + + running_state = self._thrm.running_state + if running_state and running_state & ( + RunningState.FAN | RunningState.FAN_STAGE_2 | RunningState.FAN_STAGE_3 + ): + return CURRENT_HVAC_FAN + if self.hvac_mode != HVAC_MODE_OFF and running_mode == SystemMode.OFF: + return CURRENT_HVAC_IDLE + return CURRENT_HVAC_OFF + + @property + def _pi_demand_action(self) -> Optional[str]: + """Return the current HVAC action based on pi_demands.""" + + heating_demand = self._thrm.pi_heating_demand + if heating_demand is not None and heating_demand > 0: + return CURRENT_HVAC_HEAT + cooling_demand = self._thrm.pi_cooling_demand + if cooling_demand is not None and cooling_demand > 0: + return CURRENT_HVAC_COOL + if self.hvac_mode != HVAC_MODE_OFF: return CURRENT_HVAC_IDLE return CURRENT_HVAC_OFF @@ -389,14 +406,11 @@ class Thermostat(ZhaEntity, ClimateEntity): if occupancy is True: self._preset = PRESET_NONE + self.debug("Attribute '%s' = %s update", record.attr_name, record.value) self.async_write_ha_state() async def async_set_fan_mode(self, fan_mode: str) -> None: """Set fan mode.""" - if self.fan_modes is None: - self.warning("Fan is not supported") - return - if fan_mode not in self.fan_modes: self.warning("Unsupported '%s' fan mode", fan_mode) return @@ -540,3 +554,32 @@ class SinopeTechnologiesThermostat(Thermostat): self.debug("set occupancy to %s. Status: %s", 0 if is_away else 1, res) return res + + +@STRICT_MATCH( + channel_names=CHANNEL_THERMOSTAT, + aux_channels=CHANNEL_FAN, + manufacturers="Zen Within", +) +class ZenWithinThermostat(Thermostat): + """Zen Within Thermostat implementation.""" + + @property + def _rm_rs_action(self) -> Optional[str]: + """Return the current HVAC action based on running mode and running state.""" + + running_state = self._thrm.running_state + if running_state is None: + return None + if running_state & (RunningState.HEAT | RunningState.HEAT_STAGE_2): + return CURRENT_HVAC_HEAT + if running_state & (RunningState.COOL | RunningState.COOL_STAGE_2): + return CURRENT_HVAC_COOL + if running_state & ( + RunningState.FAN | RunningState.FAN_STAGE_2 | RunningState.FAN_STAGE_3 + ): + return CURRENT_HVAC_FAN + + if self.hvac_mode != HVAC_MODE_OFF: + return CURRENT_HVAC_IDLE + return CURRENT_HVAC_OFF diff --git a/tests/components/zha/test_climate.py b/tests/components/zha/test_climate.py index c8cffef3fb9..8ca949cd44e 100644 --- a/tests/components/zha/test_climate.py +++ b/tests/components/zha/test_climate.py @@ -89,7 +89,22 @@ CLIMATE_SINOPE = { "profile_id": 260, }, } -SINOPE = "Sinope Technologies" + +CLIMATE_ZEN = { + 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.Fan.cluster_id, + zigpy.zcl.clusters.hvac.Thermostat.cluster_id, + zigpy.zcl.clusters.hvac.UserInterface.cluster_id, + ], + "out_clusters": [zigpy.zcl.clusters.general.Ota.cluster_id], + } +} +MANUF_SINOPE = "Sinope Technologies" +MANUF_ZEN = "Zen Within" ZCL_ATTR_PLUG = { "abs_min_heat_setpoint_limit": 800, @@ -169,7 +184,14 @@ async def device_climate_fan(device_climate_mock): async def device_climate_sinope(device_climate_mock): """Sinope thermostat.""" - return await device_climate_mock(CLIMATE_SINOPE, manuf=SINOPE) + return await device_climate_mock(CLIMATE_SINOPE, manuf=MANUF_SINOPE) + + +@pytest.fixture +async def device_climate_zen(device_climate_mock): + """Zen Within thermostat.""" + + return await device_climate_mock(CLIMATE_ZEN, manuf=MANUF_ZEN) def test_sequence_mappings(): @@ -201,6 +223,52 @@ async def test_climate_hvac_action_running_state(hass, device_climate): thrm_cluster = device_climate.device.endpoints[1].thermostat entity_id = await find_entity_id(DOMAIN, device_climate, hass) + state = hass.states.get(entity_id) + assert state.attributes[ATTR_HVAC_ACTION] == CURRENT_HVAC_OFF + + await send_attributes_report( + hass, thrm_cluster, {0x001E: Thermostat.RunningMode.Off} + ) + state = hass.states.get(entity_id) + assert state.attributes[ATTR_HVAC_ACTION] == CURRENT_HVAC_OFF + + await send_attributes_report( + hass, thrm_cluster, {0x001C: Thermostat.SystemMode.Auto} + ) + state = hass.states.get(entity_id) + assert state.attributes[ATTR_HVAC_ACTION] == CURRENT_HVAC_IDLE + + await send_attributes_report( + hass, thrm_cluster, {0x001E: Thermostat.RunningMode.Cool} + ) + state = hass.states.get(entity_id) + assert state.attributes[ATTR_HVAC_ACTION] == CURRENT_HVAC_COOL + + await send_attributes_report( + hass, thrm_cluster, {0x001E: Thermostat.RunningMode.Heat} + ) + state = hass.states.get(entity_id) + assert state.attributes[ATTR_HVAC_ACTION] == CURRENT_HVAC_HEAT + + await send_attributes_report( + hass, thrm_cluster, {0x001E: Thermostat.RunningMode.Off} + ) + state = hass.states.get(entity_id) + assert state.attributes[ATTR_HVAC_ACTION] == CURRENT_HVAC_IDLE + + await send_attributes_report( + hass, thrm_cluster, {0x0029: Thermostat.RunningState.Fan_State_On} + ) + state = hass.states.get(entity_id) + assert state.attributes[ATTR_HVAC_ACTION] == CURRENT_HVAC_FAN + + +async def test_climate_hvac_action_running_state_zen(hass, device_climate_zen): + """Test Zen hvac action via running state.""" + + thrm_cluster = device_climate_zen.device.endpoints[1].thermostat + entity_id = await find_entity_id(DOMAIN, device_climate_zen, hass) + state = hass.states.get(entity_id) assert ATTR_HVAC_ACTION not in state.attributes @@ -266,7 +334,7 @@ async def test_climate_hvac_action_pi_demand(hass, device_climate): entity_id = await find_entity_id(DOMAIN, device_climate, hass) state = hass.states.get(entity_id) - assert ATTR_HVAC_ACTION not in state.attributes + assert state.attributes[ATTR_HVAC_ACTION] == CURRENT_HVAC_OFF await send_attributes_report(hass, thrm_cluster, {0x0007: 10}) state = hass.states.get(entity_id) @@ -381,7 +449,7 @@ async def test_target_temperature( "unoccupied_heating_setpoint": 1600, "unoccupied_cooling_setpoint": 2700, }, - manuf=SINOPE, + manuf=MANUF_SINOPE, ) entity_id = await find_entity_id(DOMAIN, device_climate, hass) if preset: @@ -417,7 +485,7 @@ async def test_target_temperature_high( "system_mode": Thermostat.SystemMode.Auto, "unoccupied_cooling_setpoint": unoccupied, }, - manuf=SINOPE, + manuf=MANUF_SINOPE, ) entity_id = await find_entity_id(DOMAIN, device_climate, hass) if preset: @@ -453,7 +521,7 @@ async def test_target_temperature_low( "system_mode": Thermostat.SystemMode.Auto, "unoccupied_heating_setpoint": unoccupied, }, - manuf=SINOPE, + manuf=MANUF_SINOPE, ) entity_id = await find_entity_id(DOMAIN, device_climate, hass) if preset: @@ -665,7 +733,7 @@ async def test_set_temperature_heat_cool(hass, device_climate_mock): "unoccupied_heating_setpoint": 1600, "unoccupied_cooling_setpoint": 2700, }, - manuf=SINOPE, + manuf=MANUF_SINOPE, ) entity_id = await find_entity_id(DOMAIN, device_climate, hass) thrm_cluster = device_climate.device.endpoints[1].thermostat @@ -755,7 +823,7 @@ async def test_set_temperature_heat(hass, device_climate_mock): "unoccupied_heating_setpoint": 1600, "unoccupied_cooling_setpoint": 2700, }, - manuf=SINOPE, + manuf=MANUF_SINOPE, ) entity_id = await find_entity_id(DOMAIN, device_climate, hass) thrm_cluster = device_climate.device.endpoints[1].thermostat @@ -838,7 +906,7 @@ async def test_set_temperature_cool(hass, device_climate_mock): "unoccupied_cooling_setpoint": 1600, "unoccupied_heating_setpoint": 2700, }, - manuf=SINOPE, + manuf=MANUF_SINOPE, ) entity_id = await find_entity_id(DOMAIN, device_climate, hass) thrm_cluster = device_climate.device.endpoints[1].thermostat @@ -921,7 +989,7 @@ async def test_set_temperature_wrong_mode(hass, device_climate_mock): "unoccupied_cooling_setpoint": 1600, "unoccupied_heating_setpoint": 2700, }, - manuf=SINOPE, + manuf=MANUF_SINOPE, ) entity_id = await find_entity_id(DOMAIN, device_climate, hass) thrm_cluster = device_climate.device.endpoints[1].thermostat @@ -1000,19 +1068,6 @@ async def test_fan_mode(hass, device_climate_fan): assert state.attributes[ATTR_FAN_MODE] == FAN_ON -async def test_set_fan_mode_no_fan(hass, device_climate): - """Test setting fan mode on fun less climate.""" - - entity_id = await find_entity_id(DOMAIN, device_climate, hass) - - await hass.services.async_call( - DOMAIN, - SERVICE_SET_FAN_MODE, - {ATTR_ENTITY_ID: entity_id, ATTR_FAN_MODE: FAN_ON}, - blocking=True, - ) - - async def test_set_fan_mode_not_supported(hass, device_climate_fan): """Test fan setting unsupported mode.""" diff --git a/tests/components/zha/zha_devices_list.py b/tests/components/zha/zha_devices_list.py index 01144cde694..230cc5d2377 100644 --- a/tests/components/zha/zha_devices_list.py +++ b/tests/components/zha/zha_devices_list.py @@ -3349,7 +3349,7 @@ DEVICES = [ }, ("climate", "00:11:22:33:44:55:66:77-1"): { "channels": ["thermostat", "fan"], - "entity_class": "Thermostat", + "entity_class": "ZenWithinThermostat", "entity_id": "climate.zen_within_zen_01_77665544_fan_thermostat", }, },