From 10a104271eb9f1282141bfed5e85c841f744ad21 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Thu, 2 Feb 2017 06:44:05 +0100 Subject: [PATCH] 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 --- homeassistant/components/climate/demo.py | 20 +++---- homeassistant/components/climate/ecobee.py | 4 +- .../components/climate/generic_thermostat.py | 58 +++++++++++-------- homeassistant/components/climate/mysensors.py | 6 +- homeassistant/components/climate/netatmo.py | 3 - homeassistant/components/climate/zwave.py | 4 +- homeassistant/components/switch/__init__.py | 26 ++++++++- homeassistant/helpers/condition.py | 7 ++- 8 files changed, 79 insertions(+), 49 deletions(-) diff --git a/homeassistant/components/climate/demo.py b/homeassistant/components/climate/demo.py index a66873cbc63..9830daff69c 100644 --- a/homeassistant/components/climate/demo.py +++ b/homeassistant/components/climate/demo.py @@ -135,27 +135,27 @@ class DemoClimate(ClimateDevice): kwargs.get(ATTR_TARGET_TEMP_LOW) is not None: self._target_temperature_high = kwargs.get(ATTR_TARGET_TEMP_HIGH) self._target_temperature_low = kwargs.get(ATTR_TARGET_TEMP_LOW) - self.update_ha_state() + self.schedule_update_ha_state() def set_humidity(self, humidity): """Set new target temperature.""" self._target_humidity = humidity - self.update_ha_state() + self.schedule_update_ha_state() def set_swing_mode(self, swing_mode): """Set new target temperature.""" self._current_swing_mode = swing_mode - self.update_ha_state() + self.schedule_update_ha_state() def set_fan_mode(self, fan): """Set new target temperature.""" self._current_fan_mode = fan - self.update_ha_state() + self.schedule_update_ha_state() def set_operation_mode(self, operation_mode): """Set new target temperature.""" self._current_operation = operation_mode - self.update_ha_state() + self.schedule_update_ha_state() @property def current_swing_mode(self): @@ -170,24 +170,24 @@ class DemoClimate(ClimateDevice): def turn_away_mode_on(self): """Turn away mode on.""" self._away = True - self.update_ha_state() + self.schedule_update_ha_state() def turn_away_mode_off(self): """Turn away mode off.""" self._away = False - self.update_ha_state() + self.schedule_update_ha_state() def set_hold_mode(self, hold): """Update hold mode on.""" self._hold = hold - self.update_ha_state() + self.schedule_update_ha_state() def turn_aux_heat_on(self): """Turn away auxillary heater on.""" self._aux = True - self.update_ha_state() + self.schedule_update_ha_state() def turn_aux_heat_off(self): """Turn auxillary heater off.""" self._aux = False - self.update_ha_state() + self.schedule_update_ha_state() diff --git a/homeassistant/components/climate/ecobee.py b/homeassistant/components/climate/ecobee.py index dcee6d9ce31..8f5e7f5bba5 100644 --- a/homeassistant/components/climate/ecobee.py +++ b/homeassistant/components/climate/ecobee.py @@ -69,7 +69,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): for thermostat in target_thermostats: 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): """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: thermostat.resume_program(resume_all) - thermostat.update_ha_state(True) + thermostat.schedule_update_ha_state(True) descriptions = load_yaml_config_file( path.join(path.dirname(__file__), 'services.yaml')) diff --git a/homeassistant/components/climate/generic_thermostat.py b/homeassistant/components/climate/generic_thermostat.py index 562847567a3..da746270197 100644 --- a/homeassistant/components/climate/generic_thermostat.py +++ b/homeassistant/components/climate/generic_thermostat.py @@ -4,17 +4,19 @@ Adds support for generic thermostat units. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/climate.generic_thermostat/ """ +import asyncio import logging import voluptuous as vol +from homeassistant.core import callback from homeassistant.components import switch from homeassistant.components.climate import ( STATE_HEAT, STATE_COOL, STATE_IDLE, ClimateDevice, PLATFORM_SCHEMA) from homeassistant.const import ( ATTR_UNIT_OF_MEASUREMENT, STATE_ON, STATE_OFF, ATTR_TEMPERATURE) 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 _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.""" name = config.get(CONF_NAME) 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) 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, target_temp, ac_mode, min_cycle_duration, tolerance)]) @@ -86,12 +89,14 @@ class GenericThermostat(ClimateDevice): self._target_temp = target_temp self._unit = hass.config.units.temperature_unit - track_state_change(hass, sensor_entity_id, self._sensor_changed) - track_state_change(hass, heater_entity_id, self._switch_changed) + async_track_state_change( + 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) if sensor_state: - self._update_temp(sensor_state) + self._async_update_temp(sensor_state) @property def should_poll(self): @@ -128,14 +133,15 @@ class GenericThermostat(ClimateDevice): """Return the temperature we try to reach.""" return self._target_temp - def set_temperature(self, **kwargs): + @asyncio.coroutine + 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._control_heating() - self.schedule_update_ha_state() + self._async_control_heating() + yield from self.async_update_ha_state() @property def min_temp(self): @@ -157,22 +163,25 @@ class GenericThermostat(ClimateDevice): # Get default temp from super class 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.""" if new_state is None: return - self._update_temp(new_state) - self._control_heating() - self.schedule_update_ha_state() + self._async_update_temp(new_state) + self._async_control_heating() + 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.""" if new_state is None: 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.""" unit = state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) @@ -182,7 +191,8 @@ class GenericThermostat(ClimateDevice): except ValueError as 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.""" if not self._active and None not in (self._cur_temp, self._target_temp): @@ -198,9 +208,9 @@ class GenericThermostat(ClimateDevice): 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) + long_enough = condition.state( + self.hass, self.heater_entity_id, current_state, + self.min_cycle_duration) if not long_enough: return @@ -210,12 +220,12 @@ class GenericThermostat(ClimateDevice): too_cold = self._target_temp - self._cur_temp > self._tolerance if too_cold: _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: too_hot = self._cur_temp - self._target_temp > self._tolerance if too_hot: _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: is_heating = self._is_device_active if is_heating: @@ -223,12 +233,12 @@ class GenericThermostat(ClimateDevice): if too_hot: _LOGGER.info('Turning off heater %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: too_cold = self._target_temp - self._cur_temp > self._tolerance if too_cold: _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 def _is_device_active(self): diff --git a/homeassistant/components/climate/mysensors.py b/homeassistant/components/climate/mysensors.py index 6c55b3b4451..02979e75f5f 100755 --- a/homeassistant/components/climate/mysensors.py +++ b/homeassistant/components/climate/mysensors.py @@ -135,7 +135,7 @@ class MySensorsHVAC(mysensors.MySensorsDeviceEntity, ClimateDevice): if self.gateway.optimistic: # optimistically assume that switch has changed state self._values[value_type] = value - self.update_ha_state() + self.schedule_update_ha_state() def set_fan_mode(self, fan): """Set new target temperature.""" @@ -145,7 +145,7 @@ class MySensorsHVAC(mysensors.MySensorsDeviceEntity, ClimateDevice): if self.gateway.optimistic: # optimistically assume that switch has changed state self._values[set_req.V_HVAC_SPEED] = fan - self.update_ha_state() + self.schedule_update_ha_state() def set_operation_mode(self, operation_mode): """Set new target temperature.""" @@ -156,7 +156,7 @@ class MySensorsHVAC(mysensors.MySensorsDeviceEntity, ClimateDevice): if self.gateway.optimistic: # optimistically assume that switch has changed state self._values[set_req.V_HVAC_FLOW_STATE] = operation_mode - self.update_ha_state() + self.schedule_update_ha_state() def update(self): """Update the controller with the latest value from a sensor.""" diff --git a/homeassistant/components/climate/netatmo.py b/homeassistant/components/climate/netatmo.py index 163054cd121..0afc8c29bd9 100755 --- a/homeassistant/components/climate/netatmo.py +++ b/homeassistant/components/climate/netatmo.py @@ -111,7 +111,6 @@ class NetatmoThermostat(ClimateDevice): temp = None self._data.thermostatdata.setthermpoint(mode, temp, endTimeOffset=None) self._away = True - self.update_ha_state() def turn_away_mode_off(self): """Turn away off.""" @@ -119,7 +118,6 @@ class NetatmoThermostat(ClimateDevice): temp = None self._data.thermostatdata.setthermpoint(mode, temp, endTimeOffset=None) self._away = False - self.update_ha_state() def set_temperature(self, endTimeOffset=DEFAULT_TIME_OFFSET, **kwargs): """Set new target temperature for 2 hours.""" @@ -131,7 +129,6 @@ class NetatmoThermostat(ClimateDevice): mode, temperature, endTimeOffset) self._target_temperature = temperature self._away = False - self.update_ha_state() @Throttle(MIN_TIME_BETWEEN_UPDATES) def update(self): diff --git a/homeassistant/components/climate/zwave.py b/homeassistant/components/climate/zwave.py index fc2e8736ee9..76459d4997a 100755 --- a/homeassistant/components/climate/zwave.py +++ b/homeassistant/components/climate/zwave.py @@ -223,10 +223,10 @@ class ZWaveClimate(ZWaveDeviceEntity, ClimateDevice): # ZXT-120 responds only to whole int value.data = round(temperature, 0) self._target_temperature = temperature - self.update_ha_state() + self.schedule_update_ha_state() else: value.data = temperature - self.update_ha_state() + self.schedule_update_ha_state() break def set_fan_mode(self, fan): diff --git a/homeassistant/components/switch/__init__.py b/homeassistant/components/switch/__init__.py index 56ad5ea8966..a5712fcbcbe 100644 --- a/homeassistant/components/switch/__init__.py +++ b/homeassistant/components/switch/__init__.py @@ -11,6 +11,7 @@ import os import voluptuous as vol +from homeassistant.core import callback from homeassistant.config import load_yaml_config_file from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.entity import ToggleEntity @@ -20,6 +21,7 @@ from homeassistant.const import ( STATE_ON, SERVICE_TURN_ON, SERVICE_TURN_OFF, SERVICE_TOGGLE, ATTR_ENTITY_ID) from homeassistant.components import group +from homeassistant.util.async import run_callback_threadsafe DOMAIN = 'switch' SCAN_INTERVAL = timedelta(seconds=30) @@ -47,21 +49,39 @@ _LOGGER = logging.getLogger(__name__) 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 return hass.states.is_state(entity_id, STATE_ON) 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.""" 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): + """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.""" 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): diff --git a/homeassistant/helpers/condition.py b/homeassistant/helpers/condition.py index 781ef37dc9d..24af8a26351 100644 --- a/homeassistant/helpers/condition.py +++ b/homeassistant/helpers/condition.py @@ -199,7 +199,10 @@ numeric_state_from_config = _threaded_factory(async_numeric_state_from_config) 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): entity = hass.states.get(entity) @@ -357,7 +360,7 @@ def time_from_config(config, config_validation=True): def zone(hass, zone_ent, entity): """Test if zone-condition matches. - Can be run async. + Async friendly. """ if isinstance(zone_ent, str): zone_ent = hass.states.get(zone_ent)