Fixed race condition in Generic Thermostat (#15784)
* Fixed race condition in Generic Thermostat * Added a comment to clarify the meaning of the `time` argument.pull/15949/head
parent
e8218c4b29
commit
b7486e5605
|
@ -123,6 +123,7 @@ class GenericThermostat(ClimateDevice):
|
|||
self._enabled = True
|
||||
self._active = False
|
||||
self._cur_temp = None
|
||||
self._temp_lock = asyncio.Lock()
|
||||
self._min_temp = min_temp
|
||||
self._max_temp = max_temp
|
||||
self._target_temp = target_temp
|
||||
|
@ -140,7 +141,7 @@ class GenericThermostat(ClimateDevice):
|
|||
|
||||
if self._keep_alive:
|
||||
async_track_time_interval(
|
||||
hass, self._async_keep_alive, self._keep_alive)
|
||||
hass, self._async_control_heating, self._keep_alive)
|
||||
|
||||
sensor_state = hass.states.get(sensor_entity_id)
|
||||
if sensor_state and sensor_state.state != STATE_UNKNOWN:
|
||||
|
@ -234,31 +235,30 @@ class GenericThermostat(ClimateDevice):
|
|||
if operation_mode == STATE_HEAT:
|
||||
self._current_operation = STATE_HEAT
|
||||
self._enabled = True
|
||||
self._async_control_heating()
|
||||
await self._async_control_heating()
|
||||
elif operation_mode == STATE_COOL:
|
||||
self._current_operation = STATE_COOL
|
||||
self._enabled = True
|
||||
self._async_control_heating()
|
||||
await self._async_control_heating()
|
||||
elif operation_mode == STATE_OFF:
|
||||
self._current_operation = STATE_OFF
|
||||
self._enabled = False
|
||||
if self._is_device_active:
|
||||
self._heater_turn_off()
|
||||
await self._async_heater_turn_off()
|
||||
else:
|
||||
_LOGGER.error("Unrecognized operation mode: %s", operation_mode)
|
||||
return
|
||||
# Ensure we update the current operation after changing the mode
|
||||
self.schedule_update_ha_state()
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_set_temperature(self, **kwargs):
|
||||
async def async_set_temperature(self, **kwargs):
|
||||
"""Set new target temperature."""
|
||||
temperature = kwargs.get(ATTR_TEMPERATURE)
|
||||
if temperature is None:
|
||||
return
|
||||
self._target_temp = temperature
|
||||
self._async_control_heating()
|
||||
yield from self.async_update_ha_state()
|
||||
await self._async_control_heating()
|
||||
await self.async_update_ha_state()
|
||||
|
||||
@property
|
||||
def min_temp(self):
|
||||
|
@ -278,15 +278,14 @@ class GenericThermostat(ClimateDevice):
|
|||
# Get default temp from super class
|
||||
return super().max_temp
|
||||
|
||||
@asyncio.coroutine
|
||||
def _async_sensor_changed(self, entity_id, old_state, new_state):
|
||||
async def _async_sensor_changed(self, entity_id, old_state, new_state):
|
||||
"""Handle temperature changes."""
|
||||
if new_state is None:
|
||||
return
|
||||
|
||||
self._async_update_temp(new_state)
|
||||
self._async_control_heating()
|
||||
yield from self.async_update_ha_state()
|
||||
await self._async_control_heating()
|
||||
await self.async_update_ha_state()
|
||||
|
||||
@callback
|
||||
def _async_switch_changed(self, entity_id, old_state, new_state):
|
||||
|
@ -295,14 +294,6 @@ class GenericThermostat(ClimateDevice):
|
|||
return
|
||||
self.async_schedule_update_ha_state()
|
||||
|
||||
@callback
|
||||
def _async_keep_alive(self, time):
|
||||
"""Call at constant intervals for keep-alive purposes."""
|
||||
if self._is_device_active:
|
||||
self._heater_turn_on()
|
||||
else:
|
||||
self._heater_turn_off()
|
||||
|
||||
@callback
|
||||
def _async_update_temp(self, state):
|
||||
"""Update thermostat with latest state from sensor."""
|
||||
|
@ -314,62 +305,51 @@ class GenericThermostat(ClimateDevice):
|
|||
except ValueError as ex:
|
||||
_LOGGER.error("Unable to update from sensor: %s", ex)
|
||||
|
||||
@callback
|
||||
def _async_control_heating(self):
|
||||
async def _async_control_heating(self, time=None):
|
||||
"""Check if we need to turn heating on or off."""
|
||||
if not self._active and None not in (self._cur_temp,
|
||||
self._target_temp):
|
||||
self._active = True
|
||||
_LOGGER.info("Obtained current and target temperature. "
|
||||
"Generic thermostat active. %s, %s",
|
||||
self._cur_temp, self._target_temp)
|
||||
async with self._temp_lock:
|
||||
if not self._active and None not in (self._cur_temp,
|
||||
self._target_temp):
|
||||
self._active = True
|
||||
_LOGGER.info("Obtained current and target temperature. "
|
||||
"Generic thermostat active. %s, %s",
|
||||
self._cur_temp, self._target_temp)
|
||||
|
||||
if not self._active:
|
||||
return
|
||||
|
||||
if not self._enabled:
|
||||
return
|
||||
|
||||
if self.min_cycle_duration:
|
||||
if self._is_device_active:
|
||||
current_state = STATE_ON
|
||||
else:
|
||||
current_state = STATE_OFF
|
||||
long_enough = condition.state(
|
||||
self.hass, self.heater_entity_id, current_state,
|
||||
self.min_cycle_duration)
|
||||
if not long_enough:
|
||||
if not self._active or not self._enabled:
|
||||
return
|
||||
|
||||
if self.ac_mode:
|
||||
is_cooling = self._is_device_active
|
||||
if is_cooling:
|
||||
too_cold = self._target_temp - self._cur_temp >= \
|
||||
self._cold_tolerance
|
||||
if too_cold:
|
||||
_LOGGER.info("Turning off AC %s", self.heater_entity_id)
|
||||
self._heater_turn_off()
|
||||
else:
|
||||
too_hot = self._cur_temp - self._target_temp >= \
|
||||
self._hot_tolerance
|
||||
if too_hot:
|
||||
_LOGGER.info("Turning on AC %s", self.heater_entity_id)
|
||||
self._heater_turn_on()
|
||||
else:
|
||||
is_heating = self._is_device_active
|
||||
if is_heating:
|
||||
too_hot = self._cur_temp - self._target_temp >= \
|
||||
self._hot_tolerance
|
||||
if too_hot:
|
||||
if self.min_cycle_duration:
|
||||
if self._is_device_active:
|
||||
current_state = STATE_ON
|
||||
else:
|
||||
current_state = STATE_OFF
|
||||
long_enough = condition.state(
|
||||
self.hass, self.heater_entity_id, current_state,
|
||||
self.min_cycle_duration)
|
||||
if not long_enough:
|
||||
return
|
||||
|
||||
too_cold = \
|
||||
self._target_temp - self._cur_temp >= self._cold_tolerance
|
||||
too_hot = \
|
||||
self._cur_temp - self._target_temp >= self._hot_tolerance
|
||||
if self._is_device_active:
|
||||
if (self.ac_mode and too_cold) or \
|
||||
(not self.ac_mode and too_hot):
|
||||
_LOGGER.info("Turning off heater %s",
|
||||
self.heater_entity_id)
|
||||
self._heater_turn_off()
|
||||
await self._async_heater_turn_off()
|
||||
elif time is not None:
|
||||
# The time argument is passed only in keep-alive case
|
||||
await self._async_heater_turn_on()
|
||||
else:
|
||||
too_cold = self._target_temp - self._cur_temp >= \
|
||||
self._cold_tolerance
|
||||
if too_cold:
|
||||
if (self.ac_mode and too_hot) or \
|
||||
(not self.ac_mode and too_cold):
|
||||
_LOGGER.info("Turning on heater %s", self.heater_entity_id)
|
||||
self._heater_turn_on()
|
||||
await self._async_heater_turn_on()
|
||||
elif time is not None:
|
||||
# The time argument is passed only in keep-alive case
|
||||
await self._async_heater_turn_off()
|
||||
|
||||
@property
|
||||
def _is_device_active(self):
|
||||
|
@ -381,36 +361,32 @@ class GenericThermostat(ClimateDevice):
|
|||
"""Return the list of supported features."""
|
||||
return self._support_flags
|
||||
|
||||
@callback
|
||||
def _heater_turn_on(self):
|
||||
async def _async_heater_turn_on(self):
|
||||
"""Turn heater toggleable device on."""
|
||||
data = {ATTR_ENTITY_ID: self.heater_entity_id}
|
||||
self.hass.async_add_job(
|
||||
self.hass.services.async_call(HA_DOMAIN, SERVICE_TURN_ON, data))
|
||||
await self.hass.services.async_call(HA_DOMAIN, SERVICE_TURN_ON, data)
|
||||
|
||||
@callback
|
||||
def _heater_turn_off(self):
|
||||
async def _async_heater_turn_off(self):
|
||||
"""Turn heater toggleable device off."""
|
||||
data = {ATTR_ENTITY_ID: self.heater_entity_id}
|
||||
self.hass.async_add_job(
|
||||
self.hass.services.async_call(HA_DOMAIN, SERVICE_TURN_OFF, data))
|
||||
await self.hass.services.async_call(HA_DOMAIN, SERVICE_TURN_OFF, data)
|
||||
|
||||
@property
|
||||
def is_away_mode_on(self):
|
||||
"""Return true if away mode is on."""
|
||||
return self._is_away
|
||||
|
||||
def turn_away_mode_on(self):
|
||||
async def async_turn_away_mode_on(self):
|
||||
"""Turn away mode on by setting it on away hold indefinitely."""
|
||||
self._is_away = True
|
||||
self._saved_target_temp = self._target_temp
|
||||
self._target_temp = self._away_temp
|
||||
self._async_control_heating()
|
||||
self.schedule_update_ha_state()
|
||||
await self._async_control_heating()
|
||||
await self.async_update_ha_state()
|
||||
|
||||
def turn_away_mode_off(self):
|
||||
async def async_turn_away_mode_off(self):
|
||||
"""Turn away off."""
|
||||
self._is_away = False
|
||||
self._target_temp = self._saved_target_temp
|
||||
self._async_control_heating()
|
||||
self.schedule_update_ha_state()
|
||||
await self._async_control_heating()
|
||||
await self.async_update_ha_state()
|
||||
|
|
Loading…
Reference in New Issue