From c1b197419d722e33053b346d66c4f4547fdd426f Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Thu, 19 Oct 2017 10:56:25 +0200 Subject: [PATCH] Fix async probs (#9924) * Update entity.py * Update entity_component.py * Update entity_component.py * Update __init__.py * Update entity_component.py * Update entity_component.py * Update entity.py * cleanup entity * Update entity_component.py * Update entity_component.py * Fix names & comments / fix tests * Revert deadlock protection * Add tests for entity * Add test fix name * Update other code * Fix lint * Remove restore state from template entities * Lint --- .../alarm_control_panel/__init__.py | 11 +- .../components/binary_sensor/template.py | 7 +- homeassistant/components/camera/__init__.py | 11 +- homeassistant/components/climate/__init__.py | 78 ++++++--- homeassistant/components/cover/__init__.py | 13 +- homeassistant/components/cover/template.py | 7 +- homeassistant/components/fan/__init__.py | 12 +- homeassistant/components/light/__init__.py | 12 +- homeassistant/components/light/template.py | 8 +- homeassistant/components/lock/__init__.py | 12 +- .../components/media_player/__init__.py | 9 +- homeassistant/components/remote/__init__.py | 11 +- homeassistant/components/sensor/template.py | 5 - homeassistant/components/switch/__init__.py | 11 +- homeassistant/components/switch/template.py | 7 +- homeassistant/components/vacuum/__init__.py | 8 +- homeassistant/helpers/entity.py | 23 ++- homeassistant/helpers/entity_component.py | 56 +++--- .../components/binary_sensor/test_template.py | 40 +---- tests/components/light/test_template.py | 52 +----- tests/components/sensor/test_template.py | 42 +---- tests/components/switch/test_template.py | 48 +----- tests/helpers/test_entity.py | 159 ++++++++++++++++++ tests/helpers/test_entity_component.py | 76 +++++++++ 24 files changed, 356 insertions(+), 362 deletions(-) diff --git a/homeassistant/components/alarm_control_panel/__init__.py b/homeassistant/components/alarm_control_panel/__init__.py index 005048ba8c1..1141e42f9ef 100644 --- a/homeassistant/components/alarm_control_panel/__init__.py +++ b/homeassistant/components/alarm_control_panel/__init__.py @@ -124,20 +124,13 @@ def async_setup(hass, config): method = "async_{}".format(SERVICE_TO_METHOD[service.service]) + update_tasks = [] for alarm in target_alarms: yield from getattr(alarm, method)(code) - update_tasks = [] - for alarm in target_alarms: if not alarm.should_poll: continue - - update_coro = hass.async_add_job( - alarm.async_update_ha_state(True)) - if hasattr(alarm, 'async_update'): - update_tasks.append(update_coro) - else: - yield from update_coro + update_tasks.append(alarm.async_update_ha_state(True)) if update_tasks: yield from asyncio.wait(update_tasks, loop=hass.loop) diff --git a/homeassistant/components/binary_sensor/template.py b/homeassistant/components/binary_sensor/template.py index 9356d87d7ea..16167a93b82 100644 --- a/homeassistant/components/binary_sensor/template.py +++ b/homeassistant/components/binary_sensor/template.py @@ -15,13 +15,12 @@ from homeassistant.components.binary_sensor import ( DEVICE_CLASSES_SCHEMA) from homeassistant.const import ( ATTR_FRIENDLY_NAME, ATTR_ENTITY_ID, CONF_VALUE_TEMPLATE, - CONF_SENSORS, CONF_DEVICE_CLASS, EVENT_HOMEASSISTANT_START, STATE_ON) + CONF_SENSORS, CONF_DEVICE_CLASS, EVENT_HOMEASSISTANT_START) from homeassistant.exceptions import TemplateError import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import async_generate_entity_id from homeassistant.helpers.event import ( async_track_state_change, async_track_same_state) -from homeassistant.helpers.restore_state import async_get_last_state _LOGGER = logging.getLogger(__name__) @@ -94,10 +93,6 @@ class BinarySensorTemplate(BinarySensorDevice): @asyncio.coroutine def async_added_to_hass(self): """Register callbacks.""" - state = yield from async_get_last_state(self.hass, self.entity_id) - if state: - self._state = state.state == STATE_ON - @callback def template_bsensor_state_listener(entity, old_state, new_state): """Handle the target device state changes.""" diff --git a/homeassistant/components/camera/__init__.py b/homeassistant/components/camera/__init__.py index a7d778d99aa..c509d582e11 100644 --- a/homeassistant/components/camera/__init__.py +++ b/homeassistant/components/camera/__init__.py @@ -126,23 +126,16 @@ def async_setup(hass, config): """Handle calls to the camera services.""" target_cameras = component.async_extract_from_service(service) + update_tasks = [] for camera in target_cameras: if service.service == SERVICE_EN_MOTION: yield from camera.async_enable_motion_detection() elif service.service == SERVICE_DISEN_MOTION: yield from camera.async_disable_motion_detection() - update_tasks = [] - for camera in target_cameras: if not camera.should_poll: continue - - update_coro = hass.async_add_job( - camera.async_update_ha_state(True)) - if hasattr(camera, 'async_update'): - update_tasks.append(update_coro) - else: - yield from update_coro + update_tasks.append(camera.async_update_ha_state(True)) if update_tasks: yield from asyncio.wait(update_tasks, loop=hass.loop) diff --git a/homeassistant/components/climate/__init__.py b/homeassistant/components/climate/__init__.py index 53e60380a38..61f5773356f 100644 --- a/homeassistant/components/climate/__init__.py +++ b/homeassistant/components/climate/__init__.py @@ -236,24 +236,6 @@ def async_setup(hass, config): load_yaml_config_file, os.path.join(os.path.dirname(__file__), 'services.yaml')) - @asyncio.coroutine - def _async_update_climate(target_climate): - """Update climate entity after service stuff.""" - update_tasks = [] - for climate in target_climate: - if not climate.should_poll: - continue - - update_coro = hass.async_add_job( - climate.async_update_ha_state(True)) - if hasattr(climate, 'async_update'): - update_tasks.append(update_coro) - else: - yield from update_coro - - if update_tasks: - yield from asyncio.wait(update_tasks, loop=hass.loop) - @asyncio.coroutine def async_away_mode_set_service(service): """Set away mode on target climate devices.""" @@ -261,13 +243,19 @@ def async_setup(hass, config): away_mode = service.data.get(ATTR_AWAY_MODE) + update_tasks = [] for climate in target_climate: if away_mode: yield from climate.async_turn_away_mode_on() else: yield from climate.async_turn_away_mode_off() - yield from _async_update_climate(target_climate) + if not climate.should_poll: + continue + update_tasks.append(climate.async_update_ha_state(True)) + + if update_tasks: + yield from asyncio.wait(update_tasks, loop=hass.loop) hass.services.async_register( DOMAIN, SERVICE_SET_AWAY_MODE, async_away_mode_set_service, @@ -281,10 +269,16 @@ def async_setup(hass, config): hold_mode = service.data.get(ATTR_HOLD_MODE) + update_tasks = [] for climate in target_climate: yield from climate.async_set_hold_mode(hold_mode) - yield from _async_update_climate(target_climate) + if not climate.should_poll: + continue + update_tasks.append(climate.async_update_ha_state(True)) + + if update_tasks: + yield from asyncio.wait(update_tasks, loop=hass.loop) hass.services.async_register( DOMAIN, SERVICE_SET_HOLD_MODE, async_hold_mode_set_service, @@ -298,13 +292,19 @@ def async_setup(hass, config): aux_heat = service.data.get(ATTR_AUX_HEAT) + update_tasks = [] for climate in target_climate: if aux_heat: yield from climate.async_turn_aux_heat_on() else: yield from climate.async_turn_aux_heat_off() - yield from _async_update_climate(target_climate) + if not climate.should_poll: + continue + update_tasks.append(climate.async_update_ha_state(True)) + + if update_tasks: + yield from asyncio.wait(update_tasks, loop=hass.loop) hass.services.async_register( DOMAIN, SERVICE_SET_AUX_HEAT, async_aux_heat_set_service, @@ -316,6 +316,7 @@ def async_setup(hass, config): """Set temperature on the target climate devices.""" target_climate = component.async_extract_from_service(service) + update_tasks = [] for climate in target_climate: kwargs = {} for value, temp in service.data.items(): @@ -330,7 +331,12 @@ def async_setup(hass, config): yield from climate.async_set_temperature(**kwargs) - yield from _async_update_climate(target_climate) + if not climate.should_poll: + continue + update_tasks.append(climate.async_update_ha_state(True)) + + if update_tasks: + yield from asyncio.wait(update_tasks, loop=hass.loop) hass.services.async_register( DOMAIN, SERVICE_SET_TEMPERATURE, async_temperature_set_service, @@ -344,10 +350,15 @@ def async_setup(hass, config): humidity = service.data.get(ATTR_HUMIDITY) + update_tasks = [] for climate in target_climate: yield from climate.async_set_humidity(humidity) + if not climate.should_poll: + continue + update_tasks.append(climate.async_update_ha_state(True)) - yield from _async_update_climate(target_climate) + if update_tasks: + yield from asyncio.wait(update_tasks, loop=hass.loop) hass.services.async_register( DOMAIN, SERVICE_SET_HUMIDITY, async_humidity_set_service, @@ -361,10 +372,15 @@ def async_setup(hass, config): fan = service.data.get(ATTR_FAN_MODE) + update_tasks = [] for climate in target_climate: yield from climate.async_set_fan_mode(fan) + if not climate.should_poll: + continue + update_tasks.append(climate.async_update_ha_state(True)) - yield from _async_update_climate(target_climate) + if update_tasks: + yield from asyncio.wait(update_tasks, loop=hass.loop) hass.services.async_register( DOMAIN, SERVICE_SET_FAN_MODE, async_fan_mode_set_service, @@ -378,10 +394,15 @@ def async_setup(hass, config): operation_mode = service.data.get(ATTR_OPERATION_MODE) + update_tasks = [] for climate in target_climate: yield from climate.async_set_operation_mode(operation_mode) + if not climate.should_poll: + continue + update_tasks.append(climate.async_update_ha_state(True)) - yield from _async_update_climate(target_climate) + if update_tasks: + yield from asyncio.wait(update_tasks, loop=hass.loop) hass.services.async_register( DOMAIN, SERVICE_SET_OPERATION_MODE, async_operation_set_service, @@ -395,10 +416,15 @@ def async_setup(hass, config): swing_mode = service.data.get(ATTR_SWING_MODE) + update_tasks = [] for climate in target_climate: yield from climate.async_set_swing_mode(swing_mode) + if not climate.should_poll: + continue + update_tasks.append(climate.async_update_ha_state(True)) - yield from _async_update_climate(target_climate) + if update_tasks: + yield from asyncio.wait(update_tasks, loop=hass.loop) hass.services.async_register( DOMAIN, SERVICE_SET_SWING_MODE, async_swing_set_service, diff --git a/homeassistant/components/cover/__init__.py b/homeassistant/components/cover/__init__.py index 23c0be1a43e..ba60382ae64 100644 --- a/homeassistant/components/cover/__init__.py +++ b/homeassistant/components/cover/__init__.py @@ -169,21 +169,12 @@ def async_setup(hass, config): params.pop(ATTR_ENTITY_ID, None) # call method + update_tasks = [] for cover in covers: yield from getattr(cover, method['method'])(**params) - - update_tasks = [] - - for cover in covers: if not cover.should_poll: continue - - update_coro = hass.async_add_job( - cover.async_update_ha_state(True)) - if hasattr(cover, 'async_update'): - update_tasks.append(update_coro) - else: - yield from update_coro + update_tasks.append(cover.async_update_ha_state(True)) if update_tasks: yield from asyncio.wait(update_tasks, loop=hass.loop) diff --git a/homeassistant/components/cover/template.py b/homeassistant/components/cover/template.py index 2e3ad7fff16..4c79d19d38d 100644 --- a/homeassistant/components/cover/template.py +++ b/homeassistant/components/cover/template.py @@ -24,7 +24,6 @@ from homeassistant.exceptions import TemplateError import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import async_generate_entity_id from homeassistant.helpers.event import async_track_state_change -from homeassistant.helpers.restore_state import async_get_last_state from homeassistant.helpers.script import Script _LOGGER = logging.getLogger(__name__) @@ -134,7 +133,7 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): _LOGGER.error("No covers added") return False - async_add_devices(covers, True) + async_add_devices(covers) return True @@ -190,10 +189,6 @@ class CoverTemplate(CoverDevice): @asyncio.coroutine def async_added_to_hass(self): """Register callbacks.""" - state = yield from async_get_last_state(self.hass, self.entity_id) - if state: - self._position = 100 if state.state == STATE_OPEN else 0 - @callback def template_cover_state_listener(entity, old_state, new_state): """Handle target device state changes.""" diff --git a/homeassistant/components/fan/__init__.py b/homeassistant/components/fan/__init__.py index fd12529cb48..7710040ae99 100644 --- a/homeassistant/components/fan/__init__.py +++ b/homeassistant/components/fan/__init__.py @@ -215,20 +215,12 @@ def async_setup(hass, config: dict): target_fans = component.async_extract_from_service(service) params.pop(ATTR_ENTITY_ID, None) + update_tasks = [] for fan in target_fans: yield from getattr(fan, method['method'])(**params) - - update_tasks = [] - - for fan in target_fans: if not fan.should_poll: continue - - update_coro = hass.async_add_job(fan.async_update_ha_state(True)) - if hasattr(fan, 'async_update'): - update_tasks.append(update_coro) - else: - yield from update_coro + update_tasks.append(fan.async_update_ha_state(True)) if update_tasks: yield from asyncio.wait(update_tasks, loop=hass.loop) diff --git a/homeassistant/components/light/__init__.py b/homeassistant/components/light/__init__.py index 4e9fbbf81ab..d69d6991ff0 100644 --- a/homeassistant/components/light/__init__.py +++ b/homeassistant/components/light/__init__.py @@ -274,6 +274,7 @@ def async_setup(hass, config): preprocess_turn_on_alternatives(params) + update_tasks = [] for light in target_lights: if service.service == SERVICE_TURN_ON: yield from light.async_turn_on(**params) @@ -282,18 +283,9 @@ def async_setup(hass, config): else: yield from light.async_toggle(**params) - update_tasks = [] - - for light in target_lights: if not light.should_poll: continue - - update_coro = hass.async_add_job( - light.async_update_ha_state(True)) - if hasattr(light, 'async_update'): - update_tasks.append(update_coro) - else: - yield from update_coro + update_tasks.append(light.async_update_ha_state(True)) if update_tasks: yield from asyncio.wait(update_tasks, loop=hass.loop) diff --git a/homeassistant/components/light/template.py b/homeassistant/components/light/template.py index ce004d994b2..b2a9e97f11e 100644 --- a/homeassistant/components/light/template.py +++ b/homeassistant/components/light/template.py @@ -20,7 +20,6 @@ from homeassistant.exceptions import TemplateError import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import async_generate_entity_id from homeassistant.helpers.event import async_track_state_change -from homeassistant.helpers.restore_state import async_get_last_state from homeassistant.helpers.script import Script _LOGGER = logging.getLogger(__name__) @@ -87,7 +86,7 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): _LOGGER.error("No lights added") return False - async_add_devices(lights, True) + async_add_devices(lights) return True @@ -150,10 +149,6 @@ class LightTemplate(Light): @asyncio.coroutine def async_added_to_hass(self): """Register callbacks.""" - state = yield from async_get_last_state(self.hass, self.entity_id) - if state: - self._state = state.state == STATE_ON - @callback def template_light_state_listener(entity, old_state, new_state): """Handle target device state changes.""" @@ -207,6 +202,7 @@ class LightTemplate(Light): @asyncio.coroutine def async_update(self): """Update the state from the template.""" + print("ASYNC UPDATE") if self._template is not None: try: state = self._template.async_render().lower() diff --git a/homeassistant/components/lock/__init__.py b/homeassistant/components/lock/__init__.py index c64f77b3bd6..a1ad3a83b50 100644 --- a/homeassistant/components/lock/__init__.py +++ b/homeassistant/components/lock/__init__.py @@ -90,24 +90,16 @@ def async_setup(hass, config): code = service.data.get(ATTR_CODE) + update_tasks = [] for entity in target_locks: if service.service == SERVICE_LOCK: yield from entity.async_lock(code=code) else: yield from entity.async_unlock(code=code) - update_tasks = [] - - for entity in target_locks: if not entity.should_poll: continue - - update_coro = hass.async_add_job( - entity.async_update_ha_state(True)) - if hasattr(entity, 'async_update'): - update_tasks.append(update_coro) - else: - yield from update_coro + update_tasks.append(entity.async_update_ha_state(True)) if update_tasks: yield from asyncio.wait(update_tasks, loop=hass.loop) diff --git a/homeassistant/components/media_player/__init__.py b/homeassistant/components/media_player/__init__.py index 2ff957186ba..d12c634884f 100644 --- a/homeassistant/components/media_player/__init__.py +++ b/homeassistant/components/media_player/__init__.py @@ -406,16 +406,9 @@ def async_setup(hass, config): update_tasks = [] for player in target_players: yield from getattr(player, method['method'])(**params) - - for player in target_players: if not player.should_poll: continue - - update_coro = player.async_update_ha_state(True) - if hasattr(player, 'async_update'): - update_tasks.append(update_coro) - else: - yield from update_coro + update_tasks.append(player.async_update_ha_state(True)) if update_tasks: yield from asyncio.wait(update_tasks, loop=hass.loop) diff --git a/homeassistant/components/remote/__init__.py b/homeassistant/components/remote/__init__.py index e975460be58..41dbec851b5 100755 --- a/homeassistant/components/remote/__init__.py +++ b/homeassistant/components/remote/__init__.py @@ -148,6 +148,7 @@ def async_setup(hass, config): num_repeats = service.data.get(ATTR_NUM_REPEATS) delay_secs = service.data.get(ATTR_DELAY_SECS) + update_tasks = [] for remote in target_remotes: if service.service == SERVICE_TURN_ON: yield from remote.async_turn_on(activity=activity_id) @@ -160,17 +161,9 @@ def async_setup(hass, config): else: yield from remote.async_turn_off(activity=activity_id) - update_tasks = [] - for remote in target_remotes: if not remote.should_poll: continue - - update_coro = hass.async_add_job( - remote.async_update_ha_state(True)) - if hasattr(remote, 'async_update'): - update_tasks.append(update_coro) - else: - yield from update_coro + update_tasks.append(remote.async_update_ha_state(True)) if update_tasks: yield from asyncio.wait(update_tasks, loop=hass.loop) diff --git a/homeassistant/components/sensor/template.py b/homeassistant/components/sensor/template.py index e59864dea2b..ff426951d3f 100644 --- a/homeassistant/components/sensor/template.py +++ b/homeassistant/components/sensor/template.py @@ -19,7 +19,6 @@ from homeassistant.exceptions import TemplateError import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity, async_generate_entity_id from homeassistant.helpers.event import async_track_state_change -from homeassistant.helpers.restore_state import async_get_last_state _LOGGER = logging.getLogger(__name__) @@ -93,10 +92,6 @@ class SensorTemplate(Entity): @asyncio.coroutine def async_added_to_hass(self): """Register callbacks.""" - state = yield from async_get_last_state(self.hass, self.entity_id) - if state: - self._state = state.state - @callback def template_sensor_state_listener(entity, old_state, new_state): """Handle device state changes.""" diff --git a/homeassistant/components/switch/__init__.py b/homeassistant/components/switch/__init__.py index a53c6c5c01f..5bfea4eff0e 100644 --- a/homeassistant/components/switch/__init__.py +++ b/homeassistant/components/switch/__init__.py @@ -107,6 +107,7 @@ def async_setup(hass, config): """Handle calls to the switch services.""" target_switches = component.async_extract_from_service(service) + update_tasks = [] for switch in target_switches: if service.service == SERVICE_TURN_ON: yield from switch.async_turn_on() @@ -115,17 +116,9 @@ def async_setup(hass, config): else: yield from switch.async_turn_off() - update_tasks = [] - for switch in target_switches: if not switch.should_poll: continue - - update_coro = hass.async_add_job( - switch.async_update_ha_state(True)) - if hasattr(switch, 'async_update'): - update_tasks.append(update_coro) - else: - yield from update_coro + update_tasks.append(switch.async_update_ha_state(True)) if update_tasks: yield from asyncio.wait(update_tasks, loop=hass.loop) diff --git a/homeassistant/components/switch/template.py b/homeassistant/components/switch/template.py index 9b73d668c8c..2d50363bb2b 100644 --- a/homeassistant/components/switch/template.py +++ b/homeassistant/components/switch/template.py @@ -19,7 +19,6 @@ from homeassistant.exceptions import TemplateError import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import async_generate_entity_id from homeassistant.helpers.event import async_track_state_change -from homeassistant.helpers.restore_state import async_get_last_state from homeassistant.helpers.script import Script _LOGGER = logging.getLogger(__name__) @@ -71,7 +70,7 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): _LOGGER.error("No switches added") return False - async_add_devices(switches, True) + async_add_devices(switches) return True @@ -96,10 +95,6 @@ class SwitchTemplate(SwitchDevice): @asyncio.coroutine def async_added_to_hass(self): """Register callbacks.""" - state = yield from async_get_last_state(self.hass, self.entity_id) - if state: - self._state = state.state == STATE_ON - @callback def template_switch_state_listener(entity, old_state, new_state): """Handle target device state changes.""" diff --git a/homeassistant/components/vacuum/__init__.py b/homeassistant/components/vacuum/__init__.py index d3a521d8661..32839c08115 100644 --- a/homeassistant/components/vacuum/__init__.py +++ b/homeassistant/components/vacuum/__init__.py @@ -200,13 +200,7 @@ def async_setup(hass, config): yield from getattr(vacuum, method['method'])(**params) if not vacuum.should_poll: continue - - update_coro = hass.async_add_job( - vacuum.async_update_ha_state(True)) - if hasattr(vacuum, 'async_update'): - update_tasks.append(update_coro) - else: - yield from update_coro + update_tasks.append(vacuum.async_update_ha_state(True)) if update_tasks: yield from asyncio.wait(update_tasks, loop=hass.loop) diff --git a/homeassistant/helpers/entity.py b/homeassistant/helpers/entity.py index d45c3c6b2f9..930c76f9779 100644 --- a/homeassistant/helpers/entity.py +++ b/homeassistant/helpers/entity.py @@ -71,8 +71,11 @@ class Entity(object): # If we reported if this entity was slow _slow_reported = False - # protect for multiple updates - _update_warn = None + # Protect for multiple updates + _update_staged = False + + # Process updates pararell + parallel_updates = None @property def should_poll(self) -> bool: @@ -197,11 +200,15 @@ class Entity(object): # update entity data if force_refresh: - if self._update_warn: - # Update is already in progress. + if self._update_staged: return + self._update_staged = True - self._update_warn = self.hass.loop.call_later( + # Process update sequential + if self.parallel_updates: + yield from self.parallel_updates.acquire() + + update_warn = self.hass.loop.call_later( SLOW_UPDATE_WARNING, _LOGGER.warning, "Update of %s is taking over %s seconds", self.entity_id, SLOW_UPDATE_WARNING @@ -217,8 +224,10 @@ class Entity(object): _LOGGER.exception("Update for %s fails", self.entity_id) return finally: - self._update_warn.cancel() - self._update_warn = None + self._update_staged = False + update_warn.cancel() + if self.parallel_updates: + self.parallel_updates.release() start = timer() diff --git a/homeassistant/helpers/entity_component.py b/homeassistant/helpers/entity_component.py index 2833010789e..8a3026c49e5 100644 --- a/homeassistant/helpers/entity_component.py +++ b/homeassistant/helpers/entity_component.py @@ -44,7 +44,7 @@ class EntityComponent(object): self.config = None self._platforms = { - 'core': EntityPlatform(self, domain, self.scan_interval, None), + 'core': EntityPlatform(self, domain, self.scan_interval, 0, None), } self.async_add_entities = self._platforms['core'].async_add_entities self.add_entities = self._platforms['core'].add_entities @@ -128,17 +128,23 @@ class EntityComponent(object): return # Config > Platform > Component - scan_interval = (platform_config.get(CONF_SCAN_INTERVAL) or - getattr(platform, 'SCAN_INTERVAL', None) or - self.scan_interval) + scan_interval = ( + platform_config.get(CONF_SCAN_INTERVAL) or + getattr(platform, 'SCAN_INTERVAL', None) or self.scan_interval) + parallel_updates = getattr( + platform, 'PARALLEL_UPDATES', + int(not hasattr(platform, 'async_setup_platform'))) + entity_namespace = platform_config.get(CONF_ENTITY_NAMESPACE) key = (platform_type, scan_interval, entity_namespace) if key not in self._platforms: - self._platforms[key] = EntityPlatform( - self, platform_type, scan_interval, entity_namespace) - entity_platform = self._platforms[key] + entity_platform = self._platforms[key] = EntityPlatform( + self, platform_type, scan_interval, parallel_updates, + entity_namespace) + else: + entity_platform = self._platforms[key] self.logger.info("Setting up %s.%s", self.domain, platform_type) warn_task = self.hass.loop.call_later( @@ -204,13 +210,6 @@ class EntityComponent(object): entity.hass = self.hass - # update/init entity data - if update_before_add: - if hasattr(entity, 'async_update'): - yield from entity.async_update() - else: - yield from self.hass.async_add_job(entity.update) - if getattr(entity, 'entity_id', None) is None: object_id = entity.name or DEVICE_DEFAULT_NAME @@ -235,7 +234,7 @@ class EntityComponent(object): if hasattr(entity, 'async_added_to_hass'): yield from entity.async_added_to_hass() - yield from entity.async_update_ha_state() + yield from entity.async_update_ha_state(update_before_add) return True @@ -316,17 +315,23 @@ class EntityComponent(object): class EntityPlatform(object): """Keep track of entities for a single platform and stay in loop.""" - def __init__(self, component, platform, scan_interval, entity_namespace): + def __init__(self, component, platform, scan_interval, parallel_updates, + entity_namespace): """Initialize the entity platform.""" self.component = component self.platform = platform self.scan_interval = scan_interval + self.parallel_updates = None self.entity_namespace = entity_namespace self.platform_entities = [] self._tasks = [] self._async_unsub_polling = None self._process_updates = asyncio.Lock(loop=component.hass.loop) + if parallel_updates: + self.parallel_updates = asyncio.Semaphore( + parallel_updates, loop=component.hass.loop) + @asyncio.coroutine def async_block_entities_done(self): """Wait until all entities add to hass.""" @@ -377,6 +382,7 @@ class EntityPlatform(object): @asyncio.coroutine def async_process_entity(new_entity): """Add entities to StateMachine.""" + new_entity.parallel_updates = self.parallel_updates ret = yield from self.component.async_add_entity( new_entity, self, update_before_add=update_before_add ) @@ -432,26 +438,10 @@ class EntityPlatform(object): with (yield from self._process_updates): tasks = [] - to_update = [] - for entity in self.platform_entities: if not entity.should_poll: continue - - update_coro = entity.async_update_ha_state(True) - if hasattr(entity, 'async_update'): - tasks.append( - self.component.hass.async_add_job(update_coro)) - else: - to_update.append(update_coro) - - for update_coro in to_update: - try: - yield from update_coro - except Exception: # pylint: disable=broad-except - self.component.logger.exception( - "Error while update entity from %s in %s", - self.platform, self.component.domain) + tasks.append(entity.async_update_ha_state(True)) if tasks: yield from asyncio.wait(tasks, loop=self.component.hass.loop) diff --git a/tests/components/binary_sensor/test_template.py b/tests/components/binary_sensor/test_template.py index 11163d42ab5..481226c4f73 100644 --- a/tests/components/binary_sensor/test_template.py +++ b/tests/components/binary_sensor/test_template.py @@ -4,7 +4,6 @@ from datetime import timedelta import unittest from unittest import mock -from homeassistant.core import CoreState, State from homeassistant.const import MATCH_ALL from homeassistant import setup from homeassistant.components.binary_sensor import template @@ -12,11 +11,9 @@ from homeassistant.exceptions import TemplateError from homeassistant.helpers import template as template_hlpr from homeassistant.util.async import run_callback_threadsafe import homeassistant.util.dt as dt_util -from homeassistant.helpers.restore_state import DATA_RESTORE_CACHE from tests.common import ( - get_test_home_assistant, assert_setup_component, mock_component, - async_fire_time_changed) + get_test_home_assistant, assert_setup_component, async_fire_time_changed) class TestBinarySensorTemplate(unittest.TestCase): @@ -169,41 +166,6 @@ class TestBinarySensorTemplate(unittest.TestCase): run_callback_threadsafe(self.hass.loop, vs.async_check_state).result() -@asyncio.coroutine -def test_restore_state(hass): - """Ensure states are restored on startup.""" - hass.data[DATA_RESTORE_CACHE] = { - 'binary_sensor.test': State('binary_sensor.test', 'on'), - } - - hass.state = CoreState.starting - mock_component(hass, 'recorder') - - config = { - 'binary_sensor': { - 'platform': 'template', - 'sensors': { - 'test': { - 'friendly_name': 'virtual thingy', - 'value_template': - "{{ states.sensor.test_state.state == 'on' }}", - 'device_class': 'motion', - }, - }, - }, - } - yield from setup.async_setup_component(hass, 'binary_sensor', config) - - state = hass.states.get('binary_sensor.test') - assert state.state == 'on' - - yield from hass.async_start() - yield from hass.async_block_till_done() - - state = hass.states.get('binary_sensor.test') - assert state.state == 'off' - - @asyncio.coroutine def test_template_delay_on(hass): """Test binary sensor template delay on.""" diff --git a/tests/components/light/test_template.py b/tests/components/light/test_template.py index 0e741cc7ee1..5c32a1050a2 100644 --- a/tests/components/light/test_template.py +++ b/tests/components/light/test_template.py @@ -1,16 +1,14 @@ """The tests for the Template light platform.""" import logging -import asyncio -from homeassistant.core import callback, State, CoreState +from homeassistant.core import callback from homeassistant import setup import homeassistant.components as core from homeassistant.components.light import ATTR_BRIGHTNESS from homeassistant.const import STATE_ON, STATE_OFF -from homeassistant.helpers.restore_state import DATA_RESTORE_CACHE from tests.common import ( - get_test_home_assistant, assert_setup_component, mock_component) + get_test_home_assistant, assert_setup_component) _LOGGER = logging.getLogger(__name__) @@ -627,49 +625,3 @@ class TestTemplateLight: assert state is not None assert state.attributes.get('friendly_name') == 'Template light' - - -@asyncio.coroutine -def test_restore_state(hass): - """Ensure states are restored on startup.""" - hass.data[DATA_RESTORE_CACHE] = { - 'light.test_template_light': - State('light.test_template_light', 'on'), - } - - hass.state = CoreState.starting - mock_component(hass, 'recorder') - yield from setup.async_setup_component(hass, 'light', { - 'light': { - 'platform': 'template', - 'lights': { - 'test_template_light': { - 'value_template': - "{{states.light.test_state.state}}", - 'turn_on': { - 'service': 'test.automation', - }, - 'turn_off': { - 'service': 'light.turn_off', - 'entity_id': 'light.test_state' - }, - 'set_level': { - 'service': 'test.automation', - 'data_template': { - 'entity_id': 'light.test_state', - 'brightness': '{{brightness}}' - } - } - } - } - } - }) - - state = hass.states.get('light.test_template_light') - assert state.state == 'on' - - yield from hass.async_start() - yield from hass.async_block_till_done() - - state = hass.states.get('light.test_template_light') - assert state.state == 'off' diff --git a/tests/components/sensor/test_template.py b/tests/components/sensor/test_template.py index efff5186854..5e6a4957c04 100644 --- a/tests/components/sensor/test_template.py +++ b/tests/components/sensor/test_template.py @@ -1,12 +1,7 @@ """The test for the Template sensor platform.""" -import asyncio +from homeassistant.setup import setup_component -from homeassistant.core import CoreState, State -from homeassistant.setup import setup_component, async_setup_component -from homeassistant.helpers.restore_state import DATA_RESTORE_CACHE - -from tests.common import ( - get_test_home_assistant, assert_setup_component, mock_component) +from tests.common import get_test_home_assistant, assert_setup_component class TestTemplateSensor: @@ -188,36 +183,3 @@ class TestTemplateSensor: self.hass.block_till_done() assert self.hass.states.all() == [] - - -@asyncio.coroutine -def test_restore_state(hass): - """Ensure states are restored on startup.""" - hass.data[DATA_RESTORE_CACHE] = { - 'sensor.test_template_sensor': - State('sensor.test_template_sensor', 'It Test.'), - } - - hass.state = CoreState.starting - mock_component(hass, 'recorder') - - yield from async_setup_component(hass, 'sensor', { - 'sensor': { - 'platform': 'template', - 'sensors': { - 'test_template_sensor': { - 'value_template': - "It {{ states.sensor.test_state.state }}." - } - } - } - }) - - state = hass.states.get('sensor.test_template_sensor') - assert state.state == 'It Test.' - - yield from hass.async_start() - yield from hass.async_block_till_done() - - state = hass.states.get('sensor.test_template_sensor') - assert state.state == 'It .' diff --git a/tests/components/switch/test_template.py b/tests/components/switch/test_template.py index f7e9b7d730c..e4a1a1af558 100644 --- a/tests/components/switch/test_template.py +++ b/tests/components/switch/test_template.py @@ -1,14 +1,11 @@ """The tests for the Template switch platform.""" -import asyncio - -from homeassistant.core import callback, State, CoreState +from homeassistant.core import callback from homeassistant import setup import homeassistant.components as core from homeassistant.const import STATE_ON, STATE_OFF -from homeassistant.helpers.restore_state import DATA_RESTORE_CACHE from tests.common import ( - get_test_home_assistant, assert_setup_component, mock_component) + get_test_home_assistant, assert_setup_component) class TestTemplateSwitch: @@ -410,44 +407,3 @@ class TestTemplateSwitch: self.hass.block_till_done() assert len(self.calls) == 1 - - -@asyncio.coroutine -def test_restore_state(hass): - """Ensure states are restored on startup.""" - hass.data[DATA_RESTORE_CACHE] = { - 'switch.test_template_switch': - State('switch.test_template_switch', 'on'), - } - - hass.state = CoreState.starting - mock_component(hass, 'recorder') - - yield from setup.async_setup_component(hass, 'switch', { - 'switch': { - 'platform': 'template', - 'switches': { - 'test_template_switch': { - 'value_template': - "{{ states.switch.test_state.state }}", - 'turn_on': { - 'service': 'switch.turn_on', - 'entity_id': 'switch.test_state' - }, - 'turn_off': { - 'service': 'switch.turn_off', - 'entity_id': 'switch.test_state' - }, - } - } - } - }) - - state = hass.states.get('switch.test_template_switch') - assert state.state == 'on' - - yield from hass.async_start() - yield from hass.async_block_till_done() - - state = hass.states.get('switch.test_template_switch') - assert state.state == 'unavailable' diff --git a/tests/helpers/test_entity.py b/tests/helpers/test_entity.py index cf73e066072..56a696e1f1b 100644 --- a/tests/helpers/test_entity.py +++ b/tests/helpers/test_entity.py @@ -213,3 +213,162 @@ def test_async_schedule_update_ha_state(hass): yield from hass.async_block_till_done() assert update_call is True + + +@asyncio.coroutine +def test_async_pararell_updates_with_zero(hass): + """Test pararell updates with 0 (disabled).""" + updates = [] + test_lock = asyncio.Event(loop=hass.loop) + + class AsyncEntity(entity.Entity): + + def __init__(self, entity_id, count): + """Initialize Async test entity.""" + self.entity_id = entity_id + self.hass = hass + self._count = count + + @asyncio.coroutine + def async_update(self): + """Test update.""" + updates.append(self._count) + yield from test_lock.wait() + + ent_1 = AsyncEntity("sensor.test_1", 1) + ent_2 = AsyncEntity("sensor.test_2", 2) + + ent_1.async_schedule_update_ha_state(True) + ent_2.async_schedule_update_ha_state(True) + + while True: + if len(updates) == 2: + break + yield from asyncio.sleep(0, loop=hass.loop) + + assert len(updates) == 2 + assert updates == [1, 2] + + test_lock.set() + + +@asyncio.coroutine +def test_async_pararell_updates_with_one(hass): + """Test pararell updates with 1 (sequential).""" + updates = [] + test_lock = asyncio.Lock(loop=hass.loop) + test_semephore = asyncio.Semaphore(1, loop=hass.loop) + + yield from test_lock.acquire() + + class AsyncEntity(entity.Entity): + + def __init__(self, entity_id, count): + """Initialize Async test entity.""" + self.entity_id = entity_id + self.hass = hass + self._count = count + self.parallel_updates = test_semephore + + @asyncio.coroutine + def async_update(self): + """Test update.""" + updates.append(self._count) + yield from test_lock.acquire() + + ent_1 = AsyncEntity("sensor.test_1", 1) + ent_2 = AsyncEntity("sensor.test_2", 2) + ent_3 = AsyncEntity("sensor.test_3", 3) + + ent_1.async_schedule_update_ha_state(True) + ent_2.async_schedule_update_ha_state(True) + ent_3.async_schedule_update_ha_state(True) + + while True: + if len(updates) == 1: + break + yield from asyncio.sleep(0, loop=hass.loop) + + assert len(updates) == 1 + assert updates == [1] + + test_lock.release() + + while True: + if len(updates) == 2: + break + yield from asyncio.sleep(0, loop=hass.loop) + + assert len(updates) == 2 + assert updates == [1, 2] + + test_lock.release() + + while True: + if len(updates) == 3: + break + yield from asyncio.sleep(0, loop=hass.loop) + + assert len(updates) == 3 + assert updates == [1, 2, 3] + + test_lock.release() + + +@asyncio.coroutine +def test_async_pararell_updates_with_two(hass): + """Test pararell updates with 2 (pararell).""" + updates = [] + test_lock = asyncio.Lock(loop=hass.loop) + test_semephore = asyncio.Semaphore(2, loop=hass.loop) + + yield from test_lock.acquire() + + class AsyncEntity(entity.Entity): + + def __init__(self, entity_id, count): + """Initialize Async test entity.""" + self.entity_id = entity_id + self.hass = hass + self._count = count + self.parallel_updates = test_semephore + + @asyncio.coroutine + def async_update(self): + """Test update.""" + updates.append(self._count) + yield from test_lock.acquire() + + ent_1 = AsyncEntity("sensor.test_1", 1) + ent_2 = AsyncEntity("sensor.test_2", 2) + ent_3 = AsyncEntity("sensor.test_3", 3) + ent_4 = AsyncEntity("sensor.test_4", 4) + + ent_1.async_schedule_update_ha_state(True) + ent_2.async_schedule_update_ha_state(True) + ent_3.async_schedule_update_ha_state(True) + ent_4.async_schedule_update_ha_state(True) + + while True: + if len(updates) == 2: + break + yield from asyncio.sleep(0, loop=hass.loop) + + assert len(updates) == 2 + assert updates == [1, 2] + + test_lock.release() + yield from asyncio.sleep(0, loop=hass.loop) + test_lock.release() + + while True: + if len(updates) == 4: + break + yield from asyncio.sleep(0, loop=hass.loop) + + assert len(updates) == 4 + assert updates == [1, 2, 3, 4] + + test_lock.release() + yield from asyncio.sleep(0, loop=hass.loop) + test_lock.release() diff --git a/tests/helpers/test_entity_component.py b/tests/helpers/test_entity_component.py index efa079a7e4a..462d57160c9 100644 --- a/tests/helpers/test_entity_component.py +++ b/tests/helpers/test_entity_component.py @@ -578,3 +578,79 @@ def test_platform_not_ready(hass): yield from hass.async_block_till_done() assert len(platform1_setup.mock_calls) == 3 assert 'test_domain.mod1' in hass.config.components + + +@asyncio.coroutine +def test_pararell_updates_async_platform(hass): + """Warn we log when platform setup takes a long time.""" + platform = MockPlatform() + + @asyncio.coroutine + def mock_update(*args, **kwargs): + pass + + platform.async_setup_platform = mock_update + + loader.set_component('test_domain.platform', platform) + + component = EntityComponent(_LOGGER, DOMAIN, hass) + component._platforms = {} + + yield from component.async_setup({ + DOMAIN: { + 'platform': 'platform', + } + }) + + handle = list(component._platforms.values())[-1] + + assert handle.parallel_updates is None + + +@asyncio.coroutine +def test_pararell_updates_async_platform_with_constant(hass): + """Warn we log when platform setup takes a long time.""" + platform = MockPlatform() + + @asyncio.coroutine + def mock_update(*args, **kwargs): + pass + + platform.async_setup_platform = mock_update + platform.PARALLEL_UPDATES = 1 + + loader.set_component('test_domain.platform', platform) + + component = EntityComponent(_LOGGER, DOMAIN, hass) + component._platforms = {} + + yield from component.async_setup({ + DOMAIN: { + 'platform': 'platform', + } + }) + + handle = list(component._platforms.values())[-1] + + assert handle.parallel_updates is not None + + +@asyncio.coroutine +def test_pararell_updates_sync_platform(hass): + """Warn we log when platform setup takes a long time.""" + platform = MockPlatform() + + loader.set_component('test_domain.platform', platform) + + component = EntityComponent(_LOGGER, DOMAIN, hass) + component._platforms = {} + + yield from component.async_setup({ + DOMAIN: { + 'platform': 'platform', + } + }) + + handle = list(component._platforms.values())[-1] + + assert handle.parallel_updates is not None