From e798f415a490fbe09ff5d17baa9ce93fc8130dd7 Mon Sep 17 00:00:00 2001 From: javicalle <31999997+javicalle@users.noreply.github.com> Date: Fri, 19 Mar 2021 15:42:45 +0100 Subject: [PATCH] Wait for switch startup in generic_thermostat (#45253) * Better status control on restore * Better status control on restore * fix code coverage * Rollback hvac_mode initialization I think I have better understood the handling of the `hvac_mode`. I change the approach. Now the thermostat doesn't initialize until the switch is available. * fix pyupgrade * fix black * Delete test_turn_on_while_restarting HVAC mode should not be modified by the switch. IMHO, this test does not make sense because if the switch is turned on the thermostat is not turning on (and not changing HVAC_MODE) * Re add turn off if HVAC is off If HVAC_MODE is off thermostat will not control heater switch. This can be because `initial_hvac_mode`, because state defaults to or because old_state. IMHO it is preferable to be excessively cautious. * Update climate.py * Change warning message * Fix black * Fix black --- .../components/generic_thermostat/climate.py | 17 +++- .../generic_thermostat/test_climate.py | 86 +++++++++++++++++++ 2 files changed, 102 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/generic_thermostat/climate.py b/homeassistant/components/generic_thermostat/climate.py index 7062267de19..3c7959dbf4d 100644 --- a/homeassistant/components/generic_thermostat/climate.py +++ b/homeassistant/components/generic_thermostat/climate.py @@ -266,6 +266,14 @@ class GenericThermostat(ClimateEntity, RestoreEntity): if not self._hvac_mode: self._hvac_mode = HVAC_MODE_OFF + # Prevent the device from keep running if HVAC_MODE_OFF + if self._hvac_mode == HVAC_MODE_OFF and self._is_device_active: + await self._async_heater_turn_off() + _LOGGER.warning( + "The climate mode is OFF, but the switch device is ON. Turning off device %s", + self.heater_entity_id, + ) + @property def should_poll(self): """Return the polling state.""" @@ -418,7 +426,11 @@ class GenericThermostat(ClimateEntity, RestoreEntity): async def _async_control_heating(self, time=None, force=False): """Check if we need to turn heating on or off.""" async with self._temp_lock: - if not self._active and None not in (self._cur_temp, self._target_temp): + if not self._active and None not in ( + self._cur_temp, + self._target_temp, + self._is_device_active, + ): self._active = True _LOGGER.info( "Obtained current and target temperature. " @@ -480,6 +492,9 @@ class GenericThermostat(ClimateEntity, RestoreEntity): @property def _is_device_active(self): """If the toggleable device is currently active.""" + if not self.hass.states.get(self.heater_entity_id): + return None + return self.hass.states.is_state(self.heater_entity_id, STATE_ON) @property diff --git a/tests/components/generic_thermostat/test_climate.py b/tests/components/generic_thermostat/test_climate.py index b83828c86c7..dc5353971b7 100644 --- a/tests/components/generic_thermostat/test_climate.py +++ b/tests/components/generic_thermostat/test_climate.py @@ -1249,6 +1249,92 @@ async def test_no_restore_state(hass): assert state.state == HVAC_MODE_OFF +async def test_initial_hvac_off_force_heater_off(hass): + """Ensure that restored state is coherent with real situation. + + 'initial_hvac_mode: off' will force HVAC status, but we must be sure + that heater don't keep on. + """ + # switch is on + calls = _setup_switch(hass, True) + assert hass.states.get(ENT_SWITCH).state == STATE_ON + + _setup_sensor(hass, 16) + + await async_setup_component( + hass, + DOMAIN, + { + "climate": { + "platform": "generic_thermostat", + "name": "test_thermostat", + "heater": ENT_SWITCH, + "target_sensor": ENT_SENSOR, + "target_temp": 20, + "initial_hvac_mode": HVAC_MODE_OFF, + } + }, + ) + await hass.async_block_till_done() + state = hass.states.get("climate.test_thermostat") + # 'initial_hvac_mode' will force state but must prevent heather keep working + assert state.state == HVAC_MODE_OFF + # heater must be switched off + assert len(calls) == 1 + call = calls[0] + assert call.domain == HASS_DOMAIN + assert call.service == SERVICE_TURN_OFF + assert call.data["entity_id"] == ENT_SWITCH + + +async def test_restore_will_turn_off_(hass): + """Ensure that restored state is coherent with real situation. + + Thermostat status must trigger heater event if temp raises the target . + """ + heater_switch = "input_boolean.test" + mock_restore_cache( + hass, + ( + State( + "climate.test_thermostat", + HVAC_MODE_HEAT, + {ATTR_TEMPERATURE: "18", ATTR_PRESET_MODE: PRESET_NONE}, + ), + State(heater_switch, STATE_ON, {}), + ), + ) + + hass.state = CoreState.starting + + assert await async_setup_component( + hass, input_boolean.DOMAIN, {"input_boolean": {"test": None}} + ) + await hass.async_block_till_done() + assert hass.states.get(heater_switch).state == STATE_ON + + _setup_sensor(hass, 22) + + await async_setup_component( + hass, + DOMAIN, + { + "climate": { + "platform": "generic_thermostat", + "name": "test_thermostat", + "heater": heater_switch, + "target_sensor": ENT_SENSOR, + "target_temp": 20, + } + }, + ) + await hass.async_block_till_done() + state = hass.states.get("climate.test_thermostat") + assert state.attributes[ATTR_TEMPERATURE] == 20 + assert state.state == HVAC_MODE_HEAT + assert hass.states.get(heater_switch).state == STATE_ON + + async def test_restore_state_uncoherence_case(hass): """ Test restore from a strange state.