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.