Cleanup climate platform for async update_ha_state / migrate generic thermostat (#5679)

* Cleanup climate from blocking stuff / migrate generic

* Migrate generic thermostat

* fix tests

* fix lint
This commit is contained in:
Pascal Vizeli 2017-02-02 06:44:05 +01:00 committed by Paulus Schoutsen
parent 647a93801c
commit 10a104271e
8 changed files with 79 additions and 49 deletions

View File

@ -135,27 +135,27 @@ class DemoClimate(ClimateDevice):
kwargs.get(ATTR_TARGET_TEMP_LOW) is not None: kwargs.get(ATTR_TARGET_TEMP_LOW) is not None:
self._target_temperature_high = kwargs.get(ATTR_TARGET_TEMP_HIGH) self._target_temperature_high = kwargs.get(ATTR_TARGET_TEMP_HIGH)
self._target_temperature_low = kwargs.get(ATTR_TARGET_TEMP_LOW) self._target_temperature_low = kwargs.get(ATTR_TARGET_TEMP_LOW)
self.update_ha_state() self.schedule_update_ha_state()
def set_humidity(self, humidity): def set_humidity(self, humidity):
"""Set new target temperature.""" """Set new target temperature."""
self._target_humidity = humidity self._target_humidity = humidity
self.update_ha_state() self.schedule_update_ha_state()
def set_swing_mode(self, swing_mode): def set_swing_mode(self, swing_mode):
"""Set new target temperature.""" """Set new target temperature."""
self._current_swing_mode = swing_mode self._current_swing_mode = swing_mode
self.update_ha_state() self.schedule_update_ha_state()
def set_fan_mode(self, fan): def set_fan_mode(self, fan):
"""Set new target temperature.""" """Set new target temperature."""
self._current_fan_mode = fan self._current_fan_mode = fan
self.update_ha_state() self.schedule_update_ha_state()
def set_operation_mode(self, operation_mode): def set_operation_mode(self, operation_mode):
"""Set new target temperature.""" """Set new target temperature."""
self._current_operation = operation_mode self._current_operation = operation_mode
self.update_ha_state() self.schedule_update_ha_state()
@property @property
def current_swing_mode(self): def current_swing_mode(self):
@ -170,24 +170,24 @@ class DemoClimate(ClimateDevice):
def turn_away_mode_on(self): def turn_away_mode_on(self):
"""Turn away mode on.""" """Turn away mode on."""
self._away = True self._away = True
self.update_ha_state() self.schedule_update_ha_state()
def turn_away_mode_off(self): def turn_away_mode_off(self):
"""Turn away mode off.""" """Turn away mode off."""
self._away = False self._away = False
self.update_ha_state() self.schedule_update_ha_state()
def set_hold_mode(self, hold): def set_hold_mode(self, hold):
"""Update hold mode on.""" """Update hold mode on."""
self._hold = hold self._hold = hold
self.update_ha_state() self.schedule_update_ha_state()
def turn_aux_heat_on(self): def turn_aux_heat_on(self):
"""Turn away auxillary heater on.""" """Turn away auxillary heater on."""
self._aux = True self._aux = True
self.update_ha_state() self.schedule_update_ha_state()
def turn_aux_heat_off(self): def turn_aux_heat_off(self):
"""Turn auxillary heater off.""" """Turn auxillary heater off."""
self._aux = False self._aux = False
self.update_ha_state() self.schedule_update_ha_state()

View File

@ -69,7 +69,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
for thermostat in target_thermostats: for thermostat in target_thermostats:
thermostat.set_fan_min_on_time(str(fan_min_on_time)) thermostat.set_fan_min_on_time(str(fan_min_on_time))
thermostat.update_ha_state(True) thermostat.schedule_update_ha_state(True)
def resume_program_set_service(service): def resume_program_set_service(service):
"""Resume the program on the target thermostats.""" """Resume the program on the target thermostats."""
@ -85,7 +85,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
for thermostat in target_thermostats: for thermostat in target_thermostats:
thermostat.resume_program(resume_all) thermostat.resume_program(resume_all)
thermostat.update_ha_state(True) thermostat.schedule_update_ha_state(True)
descriptions = load_yaml_config_file( descriptions = load_yaml_config_file(
path.join(path.dirname(__file__), 'services.yaml')) path.join(path.dirname(__file__), 'services.yaml'))

View File

@ -4,17 +4,19 @@ Adds support for generic thermostat units.
For more details about this platform, please refer to the documentation at For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/climate.generic_thermostat/ https://home-assistant.io/components/climate.generic_thermostat/
""" """
import asyncio
import logging import logging
import voluptuous as vol import voluptuous as vol
from homeassistant.core import callback
from homeassistant.components import switch from homeassistant.components import switch
from homeassistant.components.climate import ( from homeassistant.components.climate import (
STATE_HEAT, STATE_COOL, STATE_IDLE, ClimateDevice, PLATFORM_SCHEMA) STATE_HEAT, STATE_COOL, STATE_IDLE, ClimateDevice, PLATFORM_SCHEMA)
from homeassistant.const import ( from homeassistant.const import (
ATTR_UNIT_OF_MEASUREMENT, STATE_ON, STATE_OFF, ATTR_TEMPERATURE) ATTR_UNIT_OF_MEASUREMENT, STATE_ON, STATE_OFF, ATTR_TEMPERATURE)
from homeassistant.helpers import condition from homeassistant.helpers import condition
from homeassistant.helpers.event import track_state_change from homeassistant.helpers.event import async_track_state_change
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -48,7 +50,8 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
}) })
def setup_platform(hass, config, add_devices, discovery_info=None): @asyncio.coroutine
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
"""Setup the generic thermostat.""" """Setup the generic thermostat."""
name = config.get(CONF_NAME) name = config.get(CONF_NAME)
heater_entity_id = config.get(CONF_HEATER) heater_entity_id = config.get(CONF_HEATER)
@ -60,7 +63,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
min_cycle_duration = config.get(CONF_MIN_DUR) min_cycle_duration = config.get(CONF_MIN_DUR)
tolerance = config.get(CONF_TOLERANCE) tolerance = config.get(CONF_TOLERANCE)
add_devices([GenericThermostat( yield from async_add_devices([GenericThermostat(
hass, name, heater_entity_id, sensor_entity_id, min_temp, max_temp, hass, name, heater_entity_id, sensor_entity_id, min_temp, max_temp,
target_temp, ac_mode, min_cycle_duration, tolerance)]) target_temp, ac_mode, min_cycle_duration, tolerance)])
@ -86,12 +89,14 @@ class GenericThermostat(ClimateDevice):
self._target_temp = target_temp self._target_temp = target_temp
self._unit = hass.config.units.temperature_unit self._unit = hass.config.units.temperature_unit
track_state_change(hass, sensor_entity_id, self._sensor_changed) async_track_state_change(
track_state_change(hass, heater_entity_id, self._switch_changed) hass, sensor_entity_id, self._async_sensor_changed)
async_track_state_change(
hass, heater_entity_id, self._async_switch_changed)
sensor_state = hass.states.get(sensor_entity_id) sensor_state = hass.states.get(sensor_entity_id)
if sensor_state: if sensor_state:
self._update_temp(sensor_state) self._async_update_temp(sensor_state)
@property @property
def should_poll(self): def should_poll(self):
@ -128,14 +133,15 @@ class GenericThermostat(ClimateDevice):
"""Return the temperature we try to reach.""" """Return the temperature we try to reach."""
return self._target_temp return self._target_temp
def set_temperature(self, **kwargs): @asyncio.coroutine
def async_set_temperature(self, **kwargs):
"""Set new target temperature.""" """Set new target temperature."""
temperature = kwargs.get(ATTR_TEMPERATURE) temperature = kwargs.get(ATTR_TEMPERATURE)
if temperature is None: if temperature is None:
return return
self._target_temp = temperature self._target_temp = temperature
self._control_heating() self._async_control_heating()
self.schedule_update_ha_state() yield from self.async_update_ha_state()
@property @property
def min_temp(self): def min_temp(self):
@ -157,22 +163,25 @@ class GenericThermostat(ClimateDevice):
# Get default temp from super class # Get default temp from super class
return ClimateDevice.max_temp.fget(self) return ClimateDevice.max_temp.fget(self)
def _sensor_changed(self, entity_id, old_state, new_state): @asyncio.coroutine
def _async_sensor_changed(self, entity_id, old_state, new_state):
"""Called when temperature changes.""" """Called when temperature changes."""
if new_state is None: if new_state is None:
return return
self._update_temp(new_state) self._async_update_temp(new_state)
self._control_heating() self._async_control_heating()
self.schedule_update_ha_state() yield from self.async_update_ha_state()
def _switch_changed(self, entity_id, old_state, new_state): @callback
def _async_switch_changed(self, entity_id, old_state, new_state):
"""Called when heater switch changes state.""" """Called when heater switch changes state."""
if new_state is None: if new_state is None:
return return
self.schedule_update_ha_state() self.hass.async_add_job(self.async_update_ha_state())
def _update_temp(self, state): @callback
def _async_update_temp(self, state):
"""Update thermostat with latest state from sensor.""" """Update thermostat with latest state from sensor."""
unit = state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) unit = state.attributes.get(ATTR_UNIT_OF_MEASUREMENT)
@ -182,7 +191,8 @@ class GenericThermostat(ClimateDevice):
except ValueError as ex: except ValueError as ex:
_LOGGER.error('Unable to update from sensor: %s', ex) _LOGGER.error('Unable to update from sensor: %s', ex)
def _control_heating(self): @callback
def _async_control_heating(self):
"""Check if we need to turn heating on or off.""" """Check if we need to turn heating on or off."""
if not self._active and None not in (self._cur_temp, if not self._active and None not in (self._cur_temp,
self._target_temp): self._target_temp):
@ -198,8 +208,8 @@ class GenericThermostat(ClimateDevice):
current_state = STATE_ON current_state = STATE_ON
else: else:
current_state = STATE_OFF current_state = STATE_OFF
long_enough = condition.state(self.hass, self.heater_entity_id, long_enough = condition.state(
current_state, self.hass, self.heater_entity_id, current_state,
self.min_cycle_duration) self.min_cycle_duration)
if not long_enough: if not long_enough:
return return
@ -210,12 +220,12 @@ class GenericThermostat(ClimateDevice):
too_cold = self._target_temp - self._cur_temp > self._tolerance too_cold = self._target_temp - self._cur_temp > self._tolerance
if too_cold: if too_cold:
_LOGGER.info('Turning off AC %s', self.heater_entity_id) _LOGGER.info('Turning off AC %s', self.heater_entity_id)
switch.turn_off(self.hass, self.heater_entity_id) switch.async_turn_off(self.hass, self.heater_entity_id)
else: else:
too_hot = self._cur_temp - self._target_temp > self._tolerance too_hot = self._cur_temp - self._target_temp > self._tolerance
if too_hot: if too_hot:
_LOGGER.info('Turning on AC %s', self.heater_entity_id) _LOGGER.info('Turning on AC %s', self.heater_entity_id)
switch.turn_on(self.hass, self.heater_entity_id) switch.async_turn_on(self.hass, self.heater_entity_id)
else: else:
is_heating = self._is_device_active is_heating = self._is_device_active
if is_heating: if is_heating:
@ -223,12 +233,12 @@ class GenericThermostat(ClimateDevice):
if too_hot: if too_hot:
_LOGGER.info('Turning off heater %s', _LOGGER.info('Turning off heater %s',
self.heater_entity_id) self.heater_entity_id)
switch.turn_off(self.hass, self.heater_entity_id) switch.async_turn_off(self.hass, self.heater_entity_id)
else: else:
too_cold = self._target_temp - self._cur_temp > self._tolerance too_cold = self._target_temp - self._cur_temp > self._tolerance
if too_cold: if too_cold:
_LOGGER.info('Turning on heater %s', self.heater_entity_id) _LOGGER.info('Turning on heater %s', self.heater_entity_id)
switch.turn_on(self.hass, self.heater_entity_id) switch.async_turn_on(self.hass, self.heater_entity_id)
@property @property
def _is_device_active(self): def _is_device_active(self):

View File

@ -135,7 +135,7 @@ class MySensorsHVAC(mysensors.MySensorsDeviceEntity, ClimateDevice):
if self.gateway.optimistic: if self.gateway.optimistic:
# optimistically assume that switch has changed state # optimistically assume that switch has changed state
self._values[value_type] = value self._values[value_type] = value
self.update_ha_state() self.schedule_update_ha_state()
def set_fan_mode(self, fan): def set_fan_mode(self, fan):
"""Set new target temperature.""" """Set new target temperature."""
@ -145,7 +145,7 @@ class MySensorsHVAC(mysensors.MySensorsDeviceEntity, ClimateDevice):
if self.gateway.optimistic: if self.gateway.optimistic:
# optimistically assume that switch has changed state # optimistically assume that switch has changed state
self._values[set_req.V_HVAC_SPEED] = fan self._values[set_req.V_HVAC_SPEED] = fan
self.update_ha_state() self.schedule_update_ha_state()
def set_operation_mode(self, operation_mode): def set_operation_mode(self, operation_mode):
"""Set new target temperature.""" """Set new target temperature."""
@ -156,7 +156,7 @@ class MySensorsHVAC(mysensors.MySensorsDeviceEntity, ClimateDevice):
if self.gateway.optimistic: if self.gateway.optimistic:
# optimistically assume that switch has changed state # optimistically assume that switch has changed state
self._values[set_req.V_HVAC_FLOW_STATE] = operation_mode self._values[set_req.V_HVAC_FLOW_STATE] = operation_mode
self.update_ha_state() self.schedule_update_ha_state()
def update(self): def update(self):
"""Update the controller with the latest value from a sensor.""" """Update the controller with the latest value from a sensor."""

View File

@ -111,7 +111,6 @@ class NetatmoThermostat(ClimateDevice):
temp = None temp = None
self._data.thermostatdata.setthermpoint(mode, temp, endTimeOffset=None) self._data.thermostatdata.setthermpoint(mode, temp, endTimeOffset=None)
self._away = True self._away = True
self.update_ha_state()
def turn_away_mode_off(self): def turn_away_mode_off(self):
"""Turn away off.""" """Turn away off."""
@ -119,7 +118,6 @@ class NetatmoThermostat(ClimateDevice):
temp = None temp = None
self._data.thermostatdata.setthermpoint(mode, temp, endTimeOffset=None) self._data.thermostatdata.setthermpoint(mode, temp, endTimeOffset=None)
self._away = False self._away = False
self.update_ha_state()
def set_temperature(self, endTimeOffset=DEFAULT_TIME_OFFSET, **kwargs): def set_temperature(self, endTimeOffset=DEFAULT_TIME_OFFSET, **kwargs):
"""Set new target temperature for 2 hours.""" """Set new target temperature for 2 hours."""
@ -131,7 +129,6 @@ class NetatmoThermostat(ClimateDevice):
mode, temperature, endTimeOffset) mode, temperature, endTimeOffset)
self._target_temperature = temperature self._target_temperature = temperature
self._away = False self._away = False
self.update_ha_state()
@Throttle(MIN_TIME_BETWEEN_UPDATES) @Throttle(MIN_TIME_BETWEEN_UPDATES)
def update(self): def update(self):

View File

@ -223,10 +223,10 @@ class ZWaveClimate(ZWaveDeviceEntity, ClimateDevice):
# ZXT-120 responds only to whole int # ZXT-120 responds only to whole int
value.data = round(temperature, 0) value.data = round(temperature, 0)
self._target_temperature = temperature self._target_temperature = temperature
self.update_ha_state() self.schedule_update_ha_state()
else: else:
value.data = temperature value.data = temperature
self.update_ha_state() self.schedule_update_ha_state()
break break
def set_fan_mode(self, fan): def set_fan_mode(self, fan):

View File

@ -11,6 +11,7 @@ import os
import voluptuous as vol import voluptuous as vol
from homeassistant.core import callback
from homeassistant.config import load_yaml_config_file from homeassistant.config import load_yaml_config_file
from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.entity_component import EntityComponent
from homeassistant.helpers.entity import ToggleEntity from homeassistant.helpers.entity import ToggleEntity
@ -20,6 +21,7 @@ from homeassistant.const import (
STATE_ON, SERVICE_TURN_ON, SERVICE_TURN_OFF, SERVICE_TOGGLE, STATE_ON, SERVICE_TURN_ON, SERVICE_TURN_OFF, SERVICE_TOGGLE,
ATTR_ENTITY_ID) ATTR_ENTITY_ID)
from homeassistant.components import group from homeassistant.components import group
from homeassistant.util.async import run_callback_threadsafe
DOMAIN = 'switch' DOMAIN = 'switch'
SCAN_INTERVAL = timedelta(seconds=30) SCAN_INTERVAL = timedelta(seconds=30)
@ -47,21 +49,39 @@ _LOGGER = logging.getLogger(__name__)
def is_on(hass, entity_id=None): def is_on(hass, entity_id=None):
"""Return if the switch is on based on the statemachine.""" """Return if the switch is on based on the statemachine.
Async friendly.
"""
entity_id = entity_id or ENTITY_ID_ALL_SWITCHES entity_id = entity_id or ENTITY_ID_ALL_SWITCHES
return hass.states.is_state(entity_id, STATE_ON) return hass.states.is_state(entity_id, STATE_ON)
def turn_on(hass, entity_id=None): def turn_on(hass, entity_id=None):
"""Turn all or specified switch on."""
run_callback_threadsafe(
hass.loop, async_turn_on, hass, entity_id).result()
@callback
def async_turn_on(hass, entity_id=None):
"""Turn all or specified switch on.""" """Turn all or specified switch on."""
data = {ATTR_ENTITY_ID: entity_id} if entity_id else None data = {ATTR_ENTITY_ID: entity_id} if entity_id else None
hass.services.call(DOMAIN, SERVICE_TURN_ON, data) hass.async_add_job(hass.services.async_call(DOMAIN, SERVICE_TURN_ON, data))
def turn_off(hass, entity_id=None): def turn_off(hass, entity_id=None):
"""Turn all or specified switch off."""
run_callback_threadsafe(
hass.loop, async_turn_off, hass, entity_id).result()
@callback
def async_turn_off(hass, entity_id=None):
"""Turn all or specified switch off.""" """Turn all or specified switch off."""
data = {ATTR_ENTITY_ID: entity_id} if entity_id else None data = {ATTR_ENTITY_ID: entity_id} if entity_id else None
hass.services.call(DOMAIN, SERVICE_TURN_OFF, data) hass.async_add_job(
hass.services.async_call(DOMAIN, SERVICE_TURN_OFF, data))
def toggle(hass, entity_id=None): def toggle(hass, entity_id=None):

View File

@ -199,7 +199,10 @@ numeric_state_from_config = _threaded_factory(async_numeric_state_from_config)
def state(hass, entity, req_state, for_period=None): def state(hass, entity, req_state, for_period=None):
"""Test if state matches requirements.""" """Test if state matches requirements.
Async friendly.
"""
if isinstance(entity, str): if isinstance(entity, str):
entity = hass.states.get(entity) entity = hass.states.get(entity)
@ -357,7 +360,7 @@ def time_from_config(config, config_validation=True):
def zone(hass, zone_ent, entity): def zone(hass, zone_ent, entity):
"""Test if zone-condition matches. """Test if zone-condition matches.
Can be run async. Async friendly.
""" """
if isinstance(zone_ent, str): if isinstance(zone_ent, str):
zone_ent = hass.states.get(zone_ent) zone_ent = hass.states.get(zone_ent)