diff --git a/homeassistant/components/automation/__init__.py b/homeassistant/components/automation/__init__.py index 355e19f7fa0..81944d6ec57 100644 --- a/homeassistant/components/automation/__init__.py +++ b/homeassistant/components/automation/__init__.py @@ -11,6 +11,7 @@ import os import voluptuous as vol +from homeassistant.core import callback from homeassistant.bootstrap import prepare_setup_platform from homeassistant import config as conf_util from homeassistant.const import ( @@ -157,24 +158,24 @@ def setup(hass, config): descriptions = conf_util.load_yaml_config_file( os.path.join(os.path.dirname(__file__), 'services.yaml')) - @asyncio.coroutine + @callback def trigger_service_handler(service_call): """Handle automation triggers.""" - for entity in component.extract_from_service(service_call): + for entity in component.async_extract_from_service(service_call): hass.loop.create_task(entity.async_trigger( service_call.data.get(ATTR_VARIABLES), True)) - @asyncio.coroutine + @callback def turn_onoff_service_handler(service_call): """Handle automation turn on/off service calls.""" method = 'async_{}'.format(service_call.service) - for entity in component.extract_from_service(service_call): + for entity in component.async_extract_from_service(service_call): hass.loop.create_task(getattr(entity, method)()) - @asyncio.coroutine + @callback def toggle_service_handler(service_call): """Handle automation toggle service calls.""" - for entity in component.extract_from_service(service_call): + for entity in component.async_extract_from_service(service_call): if entity.is_on: hass.loop.create_task(entity.async_turn_off()) else: @@ -183,8 +184,7 @@ def setup(hass, config): @asyncio.coroutine def reload_service_handler(service_call): """Remove all automations and load new ones from config.""" - conf = yield from hass.loop.run_in_executor( - None, component.prepare_reload) + conf = yield from component.async_prepare_reload() if conf is None: return hass.loop.create_task(_async_process_config(hass, conf, component)) @@ -271,7 +271,9 @@ class AutomationEntity(ToggleEntity): self._async_detach_triggers() self._async_detach_triggers = None self._enabled = False - self.hass.loop.create_task(self.async_update_ha_state()) + # It's important that the update is finished before this method + # ends because async_remove depends on it. + yield from self.async_update_ha_state() @asyncio.coroutine def async_trigger(self, variables, skip_condition=False): @@ -284,11 +286,11 @@ class AutomationEntity(ToggleEntity): self._last_triggered = utcnow() self.hass.loop.create_task(self.async_update_ha_state()) - def remove(self): + @asyncio.coroutine + def async_remove(self): """Remove automation from HASS.""" - run_coroutine_threadsafe(self.async_turn_off(), - self.hass.loop).result() - super().remove() + yield from self.async_turn_off() + yield from super().async_remove() @asyncio.coroutine def async_enable(self): @@ -341,12 +343,11 @@ def _async_process_config(hass, config, component): entity = AutomationEntity(name, async_attach_triggers, cond_func, action, hidden) if config_block[CONF_INITIAL_STATE]: - tasks.append(hass.loop.create_task(entity.async_enable())) + tasks.append(entity.async_enable()) entities.append(entity) yield from asyncio.gather(*tasks, loop=hass.loop) - yield from hass.loop.run_in_executor( - None, component.add_entities, entities) + hass.loop.create_task(component.async_add_entities(entities)) return len(entities) > 0 diff --git a/homeassistant/components/demo.py b/homeassistant/components/demo.py index a2eb40e21e8..9f3042320c9 100644 --- a/homeassistant/components/demo.py +++ b/homeassistant/components/demo.py @@ -67,31 +67,33 @@ def setup(hass, config): lights = sorted(hass.states.entity_ids('light')) switches = sorted(hass.states.entity_ids('switch')) media_players = sorted(hass.states.entity_ids('media_player')) - group.Group(hass, 'living room', [ + + group.Group.create_group(hass, 'living room', [ lights[1], switches[0], 'input_select.living_room_preset', 'rollershutter.living_room_window', media_players[1], 'scene.romantic_lights']) - group.Group(hass, 'bedroom', [ + group.Group.create_group(hass, 'bedroom', [ lights[0], switches[1], media_players[0], 'input_slider.noise_allowance']) - group.Group(hass, 'kitchen', [ + group.Group.create_group(hass, 'kitchen', [ lights[2], 'rollershutter.kitchen_window', 'lock.kitchen_door']) - group.Group(hass, 'doors', [ + group.Group.create_group(hass, 'doors', [ 'lock.front_door', 'lock.kitchen_door', 'garage_door.right_garage_door', 'garage_door.left_garage_door']) - group.Group(hass, 'automations', [ + group.Group.create_group(hass, 'automations', [ 'input_select.who_cooks', 'input_boolean.notify', ]) - group.Group(hass, 'people', [ + group.Group.create_group(hass, 'people', [ 'device_tracker.demo_anne_therese', 'device_tracker.demo_home_boy', 'device_tracker.demo_paulus']) - group.Group(hass, 'thermostats', [ + group.Group.create_group(hass, 'thermostats', [ 'thermostat.nest', 'thermostat.thermostat']) - group.Group(hass, 'downstairs', [ + group.Group.create_group(hass, 'downstairs', [ 'group.living_room', 'group.kitchen', 'scene.romantic_lights', 'rollershutter.kitchen_window', - 'rollershutter.living_room_window', 'group.doors', 'thermostat.nest', + 'rollershutter.living_room_window', 'group.doors', + 'thermostat.nest', ], view=True) - group.Group(hass, 'Upstairs', [ + group.Group.create_group(hass, 'Upstairs', [ 'thermostat.thermostat', 'group.bedroom', ], view=True) diff --git a/homeassistant/components/device_tracker/__init__.py b/homeassistant/components/device_tracker/__init__.py index 3fa8361a44d..72698e189ff 100644 --- a/homeassistant/components/device_tracker/__init__.py +++ b/homeassistant/components/device_tracker/__init__.py @@ -6,6 +6,7 @@ https://home-assistant.io/components/device_tracker/ """ # pylint: disable=too-many-instance-attributes, too-many-arguments # pylint: disable=too-many-locals +import asyncio from datetime import timedelta import logging import os @@ -25,6 +26,7 @@ from homeassistant.helpers.entity import Entity from homeassistant.helpers.typing import GPSType, ConfigType, HomeAssistantType import homeassistant.helpers.config_validation as cv import homeassistant.util as util +from homeassistant.util.async import run_coroutine_threadsafe import homeassistant.util.dt as dt_util from homeassistant.helpers.event import track_utc_time_change @@ -252,9 +254,18 @@ class DeviceTracker(object): def setup_group(self): """Initialize group for all tracked devices.""" + run_coroutine_threadsafe( + self.async_setup_group(), self.hass.loop).result() + + @asyncio.coroutine + def async_setup_group(self): + """Initialize group for all tracked devices. + + This method must be run in the event loop. + """ entity_ids = (dev.entity_id for dev in self.devices.values() if dev.track) - self.group = group.Group( + self.group = yield from group.Group.async_create_group( self.hass, GROUP_NAME_ALL_DEVICES, entity_ids, False) def update_stale(self, now: dt_util.dt.datetime): diff --git a/homeassistant/components/group.py b/homeassistant/components/group.py index 41901d87e86..915254bd618 100644 --- a/homeassistant/components/group.py +++ b/homeassistant/components/group.py @@ -4,9 +4,9 @@ Provides functionality to group entities. For more details about this component, please refer to the documentation at https://home-assistant.io/components/group/ """ +import asyncio import logging import os -import threading import voluptuous as vol @@ -15,10 +15,13 @@ from homeassistant.const import ( ATTR_ENTITY_ID, CONF_ICON, CONF_NAME, STATE_CLOSED, STATE_HOME, STATE_NOT_HOME, STATE_OFF, STATE_ON, STATE_OPEN, STATE_LOCKED, STATE_UNLOCKED, STATE_UNKNOWN, ATTR_ASSUMED_STATE) -from homeassistant.helpers.entity import Entity, generate_entity_id +from homeassistant.core import callback +from homeassistant.helpers.entity import Entity, async_generate_entity_id from homeassistant.helpers.entity_component import EntityComponent -from homeassistant.helpers.event import track_state_change +from homeassistant.helpers.event import async_track_state_change import homeassistant.helpers.config_validation as cv +from homeassistant.util.async import ( + run_callback_threadsafe, run_coroutine_threadsafe) DOMAIN = 'group' @@ -87,7 +90,10 @@ def reload(hass): def expand_entity_ids(hass, entity_ids): - """Return entity_ids with group entity ids replaced by their members.""" + """Return entity_ids with group entity ids replaced by their members. + + Async friendly. + """ found_ids = [] for entity_id in entity_ids: @@ -118,7 +124,10 @@ def expand_entity_ids(hass, entity_ids): def get_entity_ids(hass, entity_id, domain_filter=None): - """Get members of this group.""" + """Get members of this group. + + Async friendly. + """ group = hass.states.get(entity_id) if not group or ATTR_ENTITY_ID not in group.attributes: @@ -139,20 +148,19 @@ def setup(hass, config): """Setup all groups found definded in the configuration.""" component = EntityComponent(_LOGGER, DOMAIN, hass) - success = _process_config(hass, config, component) - - if not success: - return False + run_coroutine_threadsafe( + _async_process_config(hass, config, component), hass.loop).result() descriptions = conf_util.load_yaml_config_file( os.path.join(os.path.dirname(__file__), 'services.yaml')) + @asyncio.coroutine def reload_service_handler(service_call): """Remove all groups and load new ones from config.""" - conf = component.prepare_reload() + conf = yield from component.async_prepare_reload() if conf is None: return - _process_config(hass, conf, component) + hass.loop.create_task(_async_process_config(hass, conf, component)) hass.services.register(DOMAIN, SERVICE_RELOAD, reload_service_handler, descriptions[DOMAIN][SERVICE_RELOAD], @@ -161,48 +169,82 @@ def setup(hass, config): return True -def _process_config(hass, config, component): +@asyncio.coroutine +def _async_process_config(hass, config, component): """Process group configuration.""" + groups = [] for object_id, conf in config.get(DOMAIN, {}).items(): name = conf.get(CONF_NAME, object_id) entity_ids = conf.get(CONF_ENTITIES) or [] icon = conf.get(CONF_ICON) view = conf.get(CONF_VIEW) - group = Group(hass, name, entity_ids, icon=icon, view=view, - object_id=object_id) - component.add_entities((group,)) + # This order is important as groups get a number based on creation + # order. + group = yield from Group.async_create_group( + hass, name, entity_ids, icon=icon, view=view, object_id=object_id) + groups.append(group) - return True + yield from component.async_add_entities(groups) class Group(Entity): """Track a group of entity ids.""" # pylint: disable=too-many-instance-attributes, too-many-arguments - def __init__(self, hass, name, entity_ids=None, user_defined=True, - icon=None, view=False, object_id=None): - """Initialize a group.""" + def __init__(self, hass, name, order=None, user_defined=True, icon=None, + view=False): + """Initialize a group. + + This Object has factory function for creation. + """ self.hass = hass self._name = name self._state = STATE_UNKNOWN - self._order = len(hass.states.entity_ids(DOMAIN)) self._user_defined = user_defined + self._order = order self._icon = icon self._view = view - self.entity_id = generate_entity_id( - ENTITY_ID_FORMAT, object_id or name, hass=hass) self.tracking = [] self.group_on = None self.group_off = None self._assumed_state = False - self._lock = threading.Lock() - self._unsub_state_changed = None + self._async_unsub_state_changed = None + @staticmethod + # pylint: disable=too-many-arguments + def create_group(hass, name, entity_ids=None, user_defined=True, + icon=None, view=False, object_id=None): + """Initialize a group.""" + return run_coroutine_threadsafe( + Group.async_create_group(hass, name, entity_ids, user_defined, + icon, view, object_id), + hass.loop).result() + + @staticmethod + @asyncio.coroutine + # pylint: disable=too-many-arguments + def async_create_group(hass, name, entity_ids=None, user_defined=True, + icon=None, view=False, object_id=None): + """Initialize a group. + + This method must be run in the event loop. + """ + group = Group( + hass, name, + order=len(hass.states.async_entity_ids(DOMAIN)), + user_defined=user_defined, icon=icon, view=view) + + group.entity_id = async_generate_entity_id( + ENTITY_ID_FORMAT, object_id or name, hass=hass) + + # run other async stuff if entity_ids is not None: - self.update_tracked_entity_ids(entity_ids) + yield from group.async_update_tracked_entity_ids(entity_ids) else: - self.update_ha_state(True) + yield from group.async_update_ha_state(True) + + return group @property def should_poll(self): @@ -249,40 +291,74 @@ class Group(Entity): def update_tracked_entity_ids(self, entity_ids): """Update the member entity IDs.""" - self.stop() + run_coroutine_threadsafe( + self.async_update_tracked_entity_ids(entity_ids), self.hass.loop + ).result() + + @asyncio.coroutine + def async_update_tracked_entity_ids(self, entity_ids): + """Update the member entity IDs. + + This method must be run in the event loop. + """ + yield from self.async_stop() self.tracking = tuple(ent_id.lower() for ent_id in entity_ids) self.group_on, self.group_off = None, None - self.update_ha_state(True) - - self.start() + yield from self.async_update_ha_state(True) + self.async_start() def start(self): """Start tracking members.""" - self._unsub_state_changed = track_state_change( - self.hass, self.tracking, self._state_changed_listener) + run_callback_threadsafe(self.hass.loop, self.async_start).result() + + def async_start(self): + """Start tracking members. + + This method must be run in the event loop. + """ + self._async_unsub_state_changed = async_track_state_change( + self.hass, self.tracking, self._state_changed_listener + ) def stop(self): """Unregister the group from Home Assistant.""" - self.remove() + run_coroutine_threadsafe(self.async_stop(), self.hass.loop).result() - def update(self): + @asyncio.coroutine + def async_stop(self): + """Unregister the group from Home Assistant. + + This method must be run in the event loop. + """ + yield from self.async_remove() + + @asyncio.coroutine + def async_update(self): """Query all members and determine current group state.""" self._state = STATE_UNKNOWN - self._update_group_state() + self._async_update_group_state() - def remove(self): - """Remove group from HASS.""" - super().remove() + @asyncio.coroutine + def async_remove(self): + """Remove group from HASS. - if self._unsub_state_changed: - self._unsub_state_changed() - self._unsub_state_changed = None + This method must be run in the event loop. + """ + yield from super().async_remove() + if self._async_unsub_state_changed: + self._async_unsub_state_changed() + self._async_unsub_state_changed = None + + @callback def _state_changed_listener(self, entity_id, old_state, new_state): - """Respond to a member state changing.""" - self._update_group_state(new_state) - self.update_ha_state() + """Respond to a member state changing. + + This method must be run in the event loop. + """ + self._async_update_group_state(new_state) + self.hass.loop.create_task(self.async_update_ha_state()) @property def _tracking_states(self): @@ -297,62 +373,64 @@ class Group(Entity): return states - def _update_group_state(self, tr_state=None): + @callback + def _async_update_group_state(self, tr_state=None): """Update group state. Optionally you can provide the only state changed since last update allowing this method to take shortcuts. + + This method must be run in the event loop. """ # pylint: disable=too-many-branches # To store current states of group entities. Might not be needed. - with self._lock: - states = None - gr_state = self._state - gr_on = self.group_on - gr_off = self.group_off + states = None + gr_state = self._state + gr_on = self.group_on + gr_off = self.group_off - # We have not determined type of group yet - if gr_on is None: - if tr_state is None: - states = self._tracking_states + # We have not determined type of group yet + if gr_on is None: + if tr_state is None: + states = self._tracking_states - for state in states: - gr_on, gr_off = \ - _get_group_on_off(state.state) - if gr_on is not None: - break - else: - gr_on, gr_off = _get_group_on_off(tr_state.state) + for state in states: + gr_on, gr_off = \ + _get_group_on_off(state.state) + if gr_on is not None: + break + else: + gr_on, gr_off = _get_group_on_off(tr_state.state) - if gr_on is not None: - self.group_on, self.group_off = gr_on, gr_off + if gr_on is not None: + self.group_on, self.group_off = gr_on, gr_off - # We cannot determine state of the group - if gr_on is None: - return + # We cannot determine state of the group + if gr_on is None: + return - if tr_state is None or ((gr_state == gr_on and - tr_state.state == gr_off) or - tr_state.state not in (gr_on, gr_off)): - if states is None: - states = self._tracking_states + if tr_state is None or ((gr_state == gr_on and + tr_state.state == gr_off) or + tr_state.state not in (gr_on, gr_off)): + if states is None: + states = self._tracking_states - if any(state.state == gr_on for state in states): - self._state = gr_on - else: - self._state = gr_off + if any(state.state == gr_on for state in states): + self._state = gr_on + else: + self._state = gr_off - elif tr_state.state in (gr_on, gr_off): - self._state = tr_state.state + elif tr_state.state in (gr_on, gr_off): + self._state = tr_state.state - if tr_state is None or self._assumed_state and \ - not tr_state.attributes.get(ATTR_ASSUMED_STATE): - if states is None: - states = self._tracking_states + if tr_state is None or self._assumed_state and \ + not tr_state.attributes.get(ATTR_ASSUMED_STATE): + if states is None: + states = self._tracking_states - self._assumed_state = any( - state.attributes.get(ATTR_ASSUMED_STATE) for state - in states) + self._assumed_state = any( + state.attributes.get(ATTR_ASSUMED_STATE) for state + in states) - elif tr_state.attributes.get(ATTR_ASSUMED_STATE): - self._assumed_state = True + elif tr_state.attributes.get(ATTR_ASSUMED_STATE): + self._assumed_state = True diff --git a/homeassistant/helpers/discovery.py b/homeassistant/helpers/discovery.py index b0cf8af0747..eb36fc9e1d5 100644 --- a/homeassistant/helpers/discovery.py +++ b/homeassistant/helpers/discovery.py @@ -1,8 +1,9 @@ """Helper methods to help with platform discovery.""" -from homeassistant import bootstrap +from homeassistant import bootstrap, core from homeassistant.const import ( ATTR_DISCOVERED, ATTR_SERVICE, EVENT_PLATFORM_DISCOVERED) +from homeassistant.util.async import run_callback_threadsafe EVENT_LOAD_PLATFORM = 'load_platform.{}' ATTR_PLATFORM = 'platform' @@ -43,8 +44,19 @@ def discover(hass, service, discovered=None, component=None, hass_config=None): def listen_platform(hass, component, callback): """Register a platform loader listener.""" + run_callback_threadsafe( + hass.loop, async_listen_platform, hass, component, callback + ).result() + + +def async_listen_platform(hass, component, callback): + """Register a platform loader listener. + + This method must be run in the event loop. + """ service = EVENT_LOAD_PLATFORM.format(component) + @core.callback def discovery_platform_listener(event): """Listen for platform discovery events.""" if event.data.get(ATTR_SERVICE) != service: @@ -55,9 +67,12 @@ def listen_platform(hass, component, callback): if not platform: return - callback(platform, event.data.get(ATTR_DISCOVERED)) + hass.async_run_job( + callback, platform, event.data.get(ATTR_DISCOVERED) + ) - hass.bus.listen(EVENT_PLATFORM_DISCOVERED, discovery_platform_listener) + hass.bus.async_listen( + EVENT_PLATFORM_DISCOVERED, discovery_platform_listener) def load_platform(hass, component, platform, discovered=None, diff --git a/homeassistant/helpers/entity.py b/homeassistant/helpers/entity.py index 99384764b5b..08f93b3697b 100644 --- a/homeassistant/helpers/entity.py +++ b/homeassistant/helpers/entity.py @@ -12,7 +12,8 @@ from homeassistant.const import ( from homeassistant.core import HomeAssistant from homeassistant.exceptions import NoEntitySpecifiedError from homeassistant.util import ensure_unique_string, slugify -from homeassistant.util.async import run_coroutine_threadsafe +from homeassistant.util.async import ( + run_coroutine_threadsafe, run_callback_threadsafe) # Entity attributes that we will overwrite _OVERWRITE = {} # type: Dict[str, Any] @@ -27,15 +28,27 @@ def generate_entity_id(entity_id_format: str, name: Optional[str], if current_ids is None: if hass is None: raise ValueError("Missing required parameter currentids or hass") + else: + return run_callback_threadsafe( + hass.loop, async_generate_entity_id, entity_id_format, name, + current_ids, hass + ).result() - current_ids = hass.states.entity_ids() + name = (name or DEVICE_DEFAULT_NAME).lower() - return async_generate_entity_id(entity_id_format, name, current_ids) + return ensure_unique_string( + entity_id_format.format(slugify(name)), current_ids) def async_generate_entity_id(entity_id_format: str, name: Optional[str], - current_ids: Optional[List[str]]=None) -> str: + current_ids: Optional[List[str]]=None, + hass: Optional[HomeAssistant]=None) -> str: """Generate a unique entity ID based on given entity IDs or used IDs.""" + if current_ids is None: + if hass is None: + raise ValueError("Missing required parameter currentids or hass") + + current_ids = hass.states.async_entity_ids() name = (name or DEVICE_DEFAULT_NAME).lower() return ensure_unique_string( @@ -238,7 +251,17 @@ class Entity(object): def remove(self) -> None: """Remove entitiy from HASS.""" - self.hass.states.remove(self.entity_id) + run_coroutine_threadsafe( + self.async_remove(), self.hass.loop + ).result() + + @asyncio.coroutine + def async_remove(self) -> None: + """Remove entitiy from async HASS. + + This method must be run in the event loop. + """ + self.hass.states.async_remove(self.entity_id) def _attr_setter(self, name, typ, attr, attrs): """Helper method to populate attributes based on properties.""" diff --git a/homeassistant/helpers/entity_component.py b/homeassistant/helpers/entity_component.py index 3146d703d19..e2e25bcfbd3 100644 --- a/homeassistant/helpers/entity_component.py +++ b/homeassistant/helpers/entity_component.py @@ -1,5 +1,5 @@ """Helpers for components that manage entities.""" -from threading import Lock +import asyncio from homeassistant import config as conf_util from homeassistant.bootstrap import (prepare_setup_platform, @@ -7,12 +7,15 @@ from homeassistant.bootstrap import (prepare_setup_platform, from homeassistant.const import ( ATTR_ENTITY_ID, CONF_SCAN_INTERVAL, CONF_ENTITY_NAMESPACE, DEVICE_DEFAULT_NAME) +from homeassistant.core import callback from homeassistant.exceptions import HomeAssistantError from homeassistant.loader import get_component from homeassistant.helpers import config_per_platform, discovery -from homeassistant.helpers.entity import generate_entity_id -from homeassistant.helpers.event import track_utc_time_change +from homeassistant.helpers.entity import async_generate_entity_id +from homeassistant.helpers.event import async_track_utc_time_change from homeassistant.helpers.service import extract_entity_ids +from homeassistant.util.async import ( + run_callback_threadsafe, run_coroutine_threadsafe) DEFAULT_SCAN_INTERVAL = 15 @@ -37,11 +40,11 @@ class EntityComponent(object): self.group = None self.config = None - self.lock = Lock() self._platforms = { 'core': EntityPlatform(self, self.scan_interval, None), } + self.async_add_entities = self._platforms['core'].async_add_entities self.add_entities = self._platforms['core'].add_entities def setup(self, config): @@ -50,20 +53,38 @@ class EntityComponent(object): Loads the platforms from the config and will listen for supported discovered platforms. """ + run_coroutine_threadsafe( + self.async_setup(config), self.hass.loop + ).result() + + @asyncio.coroutine + def async_setup(self, config): + """Set up a full entity component. + + Loads the platforms from the config and will listen for supported + discovered platforms. + + This method must be run in the event loop. + """ self.config = config # Look in config for Domain, Domain 2, Domain 3 etc and load them + tasks = [] for p_type, p_config in config_per_platform(config, self.domain): - self._setup_platform(p_type, p_config) + tasks.append(self._async_setup_platform(p_type, p_config)) + + yield from asyncio.gather(*tasks, loop=self.hass.loop) # Generic discovery listener for loading platform dynamically # Refer to: homeassistant.components.discovery.load_platform() + @callback def component_platform_discovered(platform, info): """Callback to load a platform.""" - self._setup_platform(platform, {}, info) + self.hass.loop.create_task( + self._async_setup_platform(platform, {}, info)) - discovery.listen_platform(self.hass, self.domain, - component_platform_discovered) + discovery.async_listen_platform( + self.hass, self.domain, component_platform_discovered) def extract_from_service(self, service): """Extract all known entities from a service call. @@ -71,19 +92,36 @@ class EntityComponent(object): Will return all entities if no entities specified in call. Will return an empty list if entities specified but unknown. """ - with self.lock: - if ATTR_ENTITY_ID not in service.data: - return list(self.entities.values()) + return run_callback_threadsafe( + self.hass.loop, self.async_extract_from_service, service + ).result() - return [self.entities[entity_id] for entity_id - in extract_entity_ids(self.hass, service) - if entity_id in self.entities] + def async_extract_from_service(self, service): + """Extract all known entities from a service call. - def _setup_platform(self, platform_type, platform_config, - discovery_info=None): - """Setup a platform for this component.""" - platform = prepare_setup_platform( - self.hass, self.config, self.domain, platform_type) + Will return all entities if no entities specified in call. + Will return an empty list if entities specified but unknown. + + This method must be run in the event loop. + """ + if ATTR_ENTITY_ID not in service.data: + return list(self.entities.values()) + + return [self.entities[entity_id] for entity_id + in extract_entity_ids(self.hass, service) + if entity_id in self.entities] + + @asyncio.coroutine + def _async_setup_platform(self, platform_type, platform_config, + discovery_info=None): + """Setup a platform for this component. + + This method must be run in the event loop. + """ + platform = yield from self.hass.loop.run_in_executor( + None, prepare_setup_platform, self.hass, self.config, self.domain, + platform_type + ) if platform is None: return @@ -102,9 +140,16 @@ class EntityComponent(object): entity_platform = self._platforms[key] try: - platform.setup_platform(self.hass, platform_config, - entity_platform.add_entities, - discovery_info) + if getattr(platform, 'async_setup_platform', None): + yield from platform.async_setup_platform( + self.hass, platform_config, + entity_platform.async_add_entities, discovery_info + ) + else: + yield from self.hass.loop.run_in_executor( + None, platform.setup_platform, self.hass, platform_config, + entity_platform.add_entities, discovery_info + ) self.hass.config.components.append( '{}.{}'.format(self.domain, platform_type)) @@ -114,6 +159,16 @@ class EntityComponent(object): def add_entity(self, entity, platform=None): """Add entity to component.""" + return run_coroutine_threadsafe( + self.async_add_entity(entity, platform), self.hass.loop + ).result() + + @asyncio.coroutine + def async_add_entity(self, entity, platform=None): + """Add entity to component. + + This method must be run in the event loop. + """ if entity is None or entity in self.entities.values(): return False @@ -126,40 +181,60 @@ class EntityComponent(object): object_id = '{} {}'.format(platform.entity_namespace, object_id) - entity.entity_id = generate_entity_id( + entity.entity_id = async_generate_entity_id( self.entity_id_format, object_id, self.entities.keys()) self.entities[entity.entity_id] = entity - entity.update_ha_state() + yield from entity.async_update_ha_state() return True def update_group(self): """Set up and/or update component group.""" + run_callback_threadsafe( + self.hass.loop, self.async_update_group).result() + + @asyncio.coroutine + def async_update_group(self): + """Set up and/or update component group. + + This method must be run in the event loop. + """ if self.group is None and self.group_name is not None: group = get_component('group') - self.group = group.Group(self.hass, self.group_name, - user_defined=False) - - if self.group is not None: - self.group.update_tracked_entity_ids(self.entities.keys()) + self.group = yield from group.Group.async_create_group( + self.hass, self.group_name, self.entities.keys(), + user_defined=False + ) + elif self.group is not None: + yield from self.group.async_update_tracked_entity_ids( + self.entities.keys()) def reset(self): """Remove entities and reset the entity component to initial values.""" - with self.lock: - for platform in self._platforms.values(): - platform.reset() + run_coroutine_threadsafe(self.async_reset(), self.hass.loop).result() - self._platforms = { - 'core': self._platforms['core'] - } - self.entities = {} - self.config = None + @asyncio.coroutine + def async_reset(self): + """Remove entities and reset the entity component to initial values. - if self.group is not None: - self.group.stop() - self.group = None + This method must be run in the event loop. + """ + tasks = [platform.async_reset() for platform + in self._platforms.values()] + + yield from asyncio.gather(*tasks, loop=self.hass.loop) + + self._platforms = { + 'core': self._platforms['core'] + } + self.entities = {} + self.config = None + + if self.group is not None: + yield from self.group.async_stop() + self.group = None def prepare_reload(self): """Prepare reloading this entity component.""" @@ -178,9 +253,20 @@ class EntityComponent(object): self.reset() return conf + @asyncio.coroutine + def async_prepare_reload(self): + """Prepare reloading this entity component. + + This method must be run in the event loop. + """ + conf = yield from self.hass.loop.run_in_executor( + None, self.prepare_reload + ) + return conf + class EntityPlatform(object): - """Keep track of entities for a single platform.""" + """Keep track of entities for a single platform and stay in loop.""" # pylint: disable=too-few-public-methods def __init__(self, component, scan_interval, entity_namespace): @@ -189,41 +275,58 @@ class EntityPlatform(object): self.scan_interval = scan_interval self.entity_namespace = entity_namespace self.platform_entities = [] - self._unsub_polling = None + self._async_unsub_polling = None def add_entities(self, new_entities): """Add entities for a single platform.""" - with self.component.lock: - for entity in new_entities: - if self.component.add_entity(entity, self): - self.platform_entities.append(entity) + run_coroutine_threadsafe( + self.async_add_entities(new_entities), self.component.hass.loop + ).result() - self.component.update_group() + @asyncio.coroutine + def async_add_entities(self, new_entities): + """Add entities for a single platform async. - if self._unsub_polling is not None or \ - not any(entity.should_poll for entity - in self.platform_entities): - return + This method must be run in the event loop. + """ + for entity in new_entities: + ret = yield from self.component.async_add_entity(entity, self) + if ret: + self.platform_entities.append(entity) - self._unsub_polling = track_utc_time_change( - self.component.hass, self._update_entity_states, - second=range(0, 60, self.scan_interval)) + yield from self.component.async_update_group() - def reset(self): - """Remove all entities and reset data.""" - for entity in self.platform_entities: - entity.remove() - if self._unsub_polling is not None: - self._unsub_polling() - self._unsub_polling = None + if self._async_unsub_polling is not None or \ + not any(entity.should_poll for entity + in self.platform_entities): + return + self._async_unsub_polling = async_track_utc_time_change( + self.component.hass, self._update_entity_states, + second=range(0, 60, self.scan_interval)) + + @asyncio.coroutine + def async_reset(self): + """Remove all entities and reset data. + + This method must be run in the event loop. + """ + tasks = [entity.async_remove() for entity in self.platform_entities] + + yield from asyncio.gather(*tasks, loop=self.component.hass.loop) + + if self._async_unsub_polling is not None: + self._async_unsub_polling() + self._async_unsub_polling = None + + @callback def _update_entity_states(self, now): - """Update the states of all the polling entities.""" - with self.component.lock: - # We copy the entities because new entities might be detected - # during state update causing deadlocks. - entities = list(entity for entity in self.platform_entities - if entity.should_poll) + """Update the states of all the polling entities. - for entity in entities: - entity.update_ha_state(True) + This method must be run in the event loop. + """ + for entity in self.platform_entities: + if entity.should_poll: + self.component.hass.loop.create_task( + entity.async_update_ha_state(True) + ) diff --git a/homeassistant/helpers/service.py b/homeassistant/helpers/service.py index 06df2eb992d..ccfeb707fea 100644 --- a/homeassistant/helpers/service.py +++ b/homeassistant/helpers/service.py @@ -98,6 +98,8 @@ def extract_entity_ids(hass, service_call): """Helper method to extract a list of entity ids from a service call. Will convert group entity ids to the entity ids it represents. + + Async friendly. """ if not (service_call.data and ATTR_ENTITY_ID in service_call.data): return [] diff --git a/tests/components/binary_sensor/test_nx584.py b/tests/components/binary_sensor/test_nx584.py index ea4d997c2c3..f56d3967ba4 100644 --- a/tests/components/binary_sensor/test_nx584.py +++ b/tests/components/binary_sensor/test_nx584.py @@ -37,12 +37,6 @@ class TestNX584SensorSetup(unittest.TestCase): """Stop everything that was started.""" self._mock_client.stop() - def test_setup_no_config(self): - """Test the setup with no configuration.""" - hass = mock.MagicMock() - hass.pool.worker_count = 2 - assert setup_component(hass, 'binary_sensor', {'nx584': {}}) - @mock.patch('homeassistant.components.binary_sensor.nx584.NX584Watcher') @mock.patch('homeassistant.components.binary_sensor.nx584.NX584ZoneSensor') def test_setup_defaults(self, mock_nx, mock_watcher): diff --git a/tests/components/camera/test_uvc.py b/tests/components/camera/test_uvc.py index 01ce1cec518..769ba457dc5 100644 --- a/tests/components/camera/test_uvc.py +++ b/tests/components/camera/test_uvc.py @@ -9,11 +9,22 @@ from uvcclient import nvr from homeassistant.bootstrap import setup_component from homeassistant.components.camera import uvc +from tests.common import get_test_home_assistant class TestUVCSetup(unittest.TestCase): """Test the UVC camera platform.""" + def setUp(self): + """Setup things to be run when tests are started.""" + self.hass = get_test_home_assistant() + self.hass.wsgi = mock.MagicMock() + self.hass.config.components = ['http'] + + def tearDown(self): + """Stop everything that was started.""" + self.hass.stop() + @mock.patch('uvcclient.nvr.UVCRemote') @mock.patch.object(uvc, 'UnifiVideoCamera') def test_setup_full_config(self, mock_uvc, mock_remote): @@ -37,14 +48,11 @@ class TestUVCSetup(unittest.TestCase): else: return {'model': 'UVC'} - hass = mock.MagicMock() - hass.pool.worker_count = 2 - hass.config.components = ['http'] mock_remote.return_value.index.return_value = fake_cameras mock_remote.return_value.get_camera.side_effect = fake_get_camera mock_remote.return_value.server_version = (3, 2, 0) - assert setup_component(hass, 'camera', {'camera': config}) + assert setup_component(self.hass, 'camera', {'camera': config}) mock_remote.assert_called_once_with('foo', 123, 'secret') mock_uvc.assert_has_calls([ @@ -65,14 +73,11 @@ class TestUVCSetup(unittest.TestCase): {'uuid': 'one', 'name': 'Front', 'id': 'id1'}, {'uuid': 'two', 'name': 'Back', 'id': 'id2'}, ] - hass = mock.MagicMock() - hass.pool.worker_count = 2 - hass.config.components = ['http'] mock_remote.return_value.index.return_value = fake_cameras mock_remote.return_value.get_camera.return_value = {'model': 'UVC'} mock_remote.return_value.server_version = (3, 2, 0) - assert setup_component(hass, 'camera', {'camera': config}) + assert setup_component(self.hass, 'camera', {'camera': config}) mock_remote.assert_called_once_with('foo', 7080, 'secret') mock_uvc.assert_has_calls([ @@ -93,14 +98,11 @@ class TestUVCSetup(unittest.TestCase): {'uuid': 'one', 'name': 'Front', 'id': 'id1'}, {'uuid': 'two', 'name': 'Back', 'id': 'id2'}, ] - hass = mock.MagicMock() - hass.pool.worker_count = 2 - hass.config.components = ['http'] mock_remote.return_value.index.return_value = fake_cameras mock_remote.return_value.get_camera.return_value = {'model': 'UVC'} mock_remote.return_value.server_version = (3, 1, 3) - assert setup_component(hass, 'camera', {'camera': config}) + assert setup_component(self.hass, 'camera', {'camera': config}) mock_remote.assert_called_once_with('foo', 7080, 'secret') mock_uvc.assert_has_calls([ @@ -111,18 +113,14 @@ class TestUVCSetup(unittest.TestCase): @mock.patch.object(uvc, 'UnifiVideoCamera') def test_setup_incomplete_config(self, mock_uvc): """"Test the setup with incomplete configuration.""" - hass = mock.MagicMock() - hass.pool.worker_count = 2 - hass.config.components = ['http'] - assert setup_component( - hass, 'camera', {'platform': 'uvc', 'nvr': 'foo'}) + self.hass, 'camera', {'platform': 'uvc', 'nvr': 'foo'}) assert not mock_uvc.called assert setup_component( - hass, 'camera', {'platform': 'uvc', 'key': 'secret'}) + self.hass, 'camera', {'platform': 'uvc', 'key': 'secret'}) assert not mock_uvc.called assert setup_component( - hass, 'camera', {'platform': 'uvc', 'port': 'invalid'}) + self.hass, 'camera', {'platform': 'uvc', 'port': 'invalid'}) assert not mock_uvc.called @mock.patch.object(uvc, 'UnifiVideoCamera') @@ -136,13 +134,9 @@ class TestUVCSetup(unittest.TestCase): 'nvr': 'foo', 'key': 'secret', } - hass = mock.MagicMock() - hass.pool.worker_count = 2 - hass.config.components = ['http'] - for error in errors: mock_remote.return_value.index.side_effect = error - assert setup_component(hass, 'camera', config) + assert setup_component(self.hass, 'camera', config) assert not mock_uvc.called diff --git a/tests/components/test_group.py b/tests/components/test_group.py index 9a2de824e90..5fe14c6377e 100644 --- a/tests/components/test_group.py +++ b/tests/components/test_group.py @@ -4,7 +4,7 @@ from collections import OrderedDict import unittest from unittest.mock import patch -from homeassistant.bootstrap import _setup_component +from homeassistant.bootstrap import setup_component from homeassistant.const import ( STATE_ON, STATE_OFF, STATE_HOME, STATE_UNKNOWN, ATTR_ICON, ATTR_HIDDEN, ATTR_ASSUMED_STATE, STATE_NOT_HOME, ) @@ -28,7 +28,7 @@ class TestComponentsGroup(unittest.TestCase): """Try to setup a group with mixed groupable states.""" self.hass.states.set('light.Bowl', STATE_ON) self.hass.states.set('device_tracker.Paulus', STATE_HOME) - group.Group( + group.Group.create_group( self.hass, 'person_and_light', ['light.Bowl', 'device_tracker.Paulus']) @@ -41,7 +41,7 @@ class TestComponentsGroup(unittest.TestCase): """Try to setup a group with a non existing state.""" self.hass.states.set('light.Bowl', STATE_ON) - grp = group.Group( + grp = group.Group.create_group( self.hass, 'light_and_nothing', ['light.Bowl', 'non.existing']) @@ -52,7 +52,7 @@ class TestComponentsGroup(unittest.TestCase): self.hass.states.set('cast.living_room', "Plex") self.hass.states.set('cast.bedroom', "Netflix") - grp = group.Group( + grp = group.Group.create_group( self.hass, 'chromecasts', ['cast.living_room', 'cast.bedroom']) @@ -60,7 +60,7 @@ class TestComponentsGroup(unittest.TestCase): def test_setup_empty_group(self): """Try to setup an empty group.""" - grp = group.Group(self.hass, 'nothing', []) + grp = group.Group.create_group(self.hass, 'nothing', []) self.assertEqual(STATE_UNKNOWN, grp.state) @@ -68,7 +68,7 @@ class TestComponentsGroup(unittest.TestCase): """Test if the group keeps track of states.""" self.hass.states.set('light.Bowl', STATE_ON) self.hass.states.set('light.Ceiling', STATE_OFF) - test_group = group.Group( + test_group = group.Group.create_group( self.hass, 'init_group', ['light.Bowl', 'light.Ceiling'], False) # Test if group setup in our init mode is ok @@ -82,7 +82,7 @@ class TestComponentsGroup(unittest.TestCase): """Test if turn off if the last device that was on turns off.""" self.hass.states.set('light.Bowl', STATE_OFF) self.hass.states.set('light.Ceiling', STATE_OFF) - test_group = group.Group( + test_group = group.Group.create_group( self.hass, 'init_group', ['light.Bowl', 'light.Ceiling'], False) self.hass.block_till_done() @@ -94,7 +94,7 @@ class TestComponentsGroup(unittest.TestCase): """Test if turn on if all devices were turned off and one turns on.""" self.hass.states.set('light.Bowl', STATE_OFF) self.hass.states.set('light.Ceiling', STATE_OFF) - test_group = group.Group( + test_group = group.Group.create_group( self.hass, 'init_group', ['light.Bowl', 'light.Ceiling'], False) # Turn one on @@ -108,7 +108,7 @@ class TestComponentsGroup(unittest.TestCase): """Test is_on method.""" self.hass.states.set('light.Bowl', STATE_ON) self.hass.states.set('light.Ceiling', STATE_OFF) - test_group = group.Group( + test_group = group.Group.create_group( self.hass, 'init_group', ['light.Bowl', 'light.Ceiling'], False) self.assertTrue(group.is_on(self.hass, test_group.entity_id)) @@ -123,7 +123,7 @@ class TestComponentsGroup(unittest.TestCase): """Test expand_entity_ids method.""" self.hass.states.set('light.Bowl', STATE_ON) self.hass.states.set('light.Ceiling', STATE_OFF) - test_group = group.Group( + test_group = group.Group.create_group( self.hass, 'init_group', ['light.Bowl', 'light.Ceiling'], False) self.assertEqual(sorted(['light.ceiling', 'light.bowl']), @@ -134,7 +134,7 @@ class TestComponentsGroup(unittest.TestCase): """Test that expand_entity_ids does not return duplicates.""" self.hass.states.set('light.Bowl', STATE_ON) self.hass.states.set('light.Ceiling', STATE_OFF) - test_group = group.Group( + test_group = group.Group.create_group( self.hass, 'init_group', ['light.Bowl', 'light.Ceiling'], False) self.assertEqual( @@ -155,7 +155,7 @@ class TestComponentsGroup(unittest.TestCase): """Test get_entity_ids method.""" self.hass.states.set('light.Bowl', STATE_ON) self.hass.states.set('light.Ceiling', STATE_OFF) - test_group = group.Group( + test_group = group.Group.create_group( self.hass, 'init_group', ['light.Bowl', 'light.Ceiling'], False) self.assertEqual( @@ -166,7 +166,7 @@ class TestComponentsGroup(unittest.TestCase): """Test if get_entity_ids works with a domain_filter.""" self.hass.states.set('switch.AC', STATE_OFF) - mixed_group = group.Group( + mixed_group = group.Group.create_group( self.hass, 'mixed_group', ['light.Bowl', 'switch.AC'], False) self.assertEqual( @@ -188,7 +188,7 @@ class TestComponentsGroup(unittest.TestCase): If no states existed and now a state it is tracking is being added as ON. """ - test_group = group.Group( + test_group = group.Group.create_group( self.hass, 'test group', ['light.not_there_1']) self.hass.states.set('light.not_there_1', STATE_ON) @@ -204,7 +204,7 @@ class TestComponentsGroup(unittest.TestCase): If no states existed and now a state it is tracking is being added as OFF. """ - test_group = group.Group( + test_group = group.Group.create_group( self.hass, 'test group', ['light.not_there_1']) self.hass.states.set('light.not_there_1', STATE_OFF) @@ -218,7 +218,7 @@ class TestComponentsGroup(unittest.TestCase): """Test setup method.""" self.hass.states.set('light.Bowl', STATE_ON) self.hass.states.set('light.Ceiling', STATE_OFF) - test_group = group.Group( + test_group = group.Group.create_group( self.hass, 'init_group', ['light.Bowl', 'light.Ceiling'], False) group_conf = OrderedDict() @@ -230,7 +230,7 @@ class TestComponentsGroup(unittest.TestCase): group_conf['test_group'] = 'hello.world,sensor.happy' group_conf['empty_group'] = {'name': 'Empty Group', 'entities': None} - _setup_component(self.hass, 'group', {'group': group_conf}) + setup_component(self.hass, 'group', {'group': group_conf}) group_state = self.hass.states.get( group.ENTITY_ID_FORMAT.format('second_group')) @@ -257,17 +257,19 @@ class TestComponentsGroup(unittest.TestCase): def test_groups_get_unique_names(self): """Two groups with same name should both have a unique entity id.""" - grp1 = group.Group(self.hass, 'Je suis Charlie') - grp2 = group.Group(self.hass, 'Je suis Charlie') + grp1 = group.Group.create_group(self.hass, 'Je suis Charlie') + grp2 = group.Group.create_group(self.hass, 'Je suis Charlie') self.assertNotEqual(grp1.entity_id, grp2.entity_id) def test_expand_entity_ids_expands_nested_groups(self): """Test if entity ids epands to nested groups.""" - group.Group(self.hass, 'light', ['light.test_1', 'light.test_2']) - group.Group(self.hass, 'switch', ['switch.test_1', 'switch.test_2']) - group.Group(self.hass, 'group_of_groups', ['group.light', - 'group.switch']) + group.Group.create_group( + self.hass, 'light', ['light.test_1', 'light.test_2']) + group.Group.create_group( + self.hass, 'switch', ['switch.test_1', 'switch.test_2']) + group.Group.create_group(self.hass, 'group_of_groups', ['group.light', + 'group.switch']) self.assertEqual( ['light.test_1', 'light.test_2', 'switch.test_1', 'switch.test_2'], @@ -278,7 +280,7 @@ class TestComponentsGroup(unittest.TestCase): """Test assumed state.""" self.hass.states.set('light.Bowl', STATE_ON) self.hass.states.set('light.Ceiling', STATE_OFF) - test_group = group.Group( + test_group = group.Group.create_group( self.hass, 'init_group', ['light.Bowl', 'light.Ceiling', 'sensor.no_exist']) @@ -304,7 +306,7 @@ class TestComponentsGroup(unittest.TestCase): self.hass.states.set('device_tracker.Adam', STATE_HOME) self.hass.states.set('device_tracker.Eve', STATE_NOT_HOME) self.hass.block_till_done() - group.Group( + group.Group.create_group( self.hass, 'peeps', ['device_tracker.Adam', 'device_tracker.Eve']) self.hass.states.set('device_tracker.Adam', 'cool_state_not_home') @@ -315,7 +317,7 @@ class TestComponentsGroup(unittest.TestCase): def test_reloading_groups(self): """Test reloading the group config.""" - _setup_component(self.hass, 'group', {'group': { + assert setup_component(self.hass, 'group', {'group': { 'second_group': { 'entities': 'light.Bowl', 'icon': 'mdi:work', @@ -342,3 +344,11 @@ class TestComponentsGroup(unittest.TestCase): assert self.hass.states.entity_ids() == ['group.hello'] assert self.hass.bus.listeners['state_changed'] == 1 + + def test_stopping_a_group(self): + """Test that a group correctly removes itself.""" + grp = group.Group.create_group( + self.hass, 'light', ['light.test_1', 'light.test_2']) + assert self.hass.states.entity_ids() == ['group.light'] + grp.stop() + assert self.hass.states.entity_ids() == [] diff --git a/tests/helpers/test_entity_component.py b/tests/helpers/test_entity_component.py index 0ab87c57452..6f658a70518 100644 --- a/tests/helpers/test_entity_component.py +++ b/tests/helpers/test_entity_component.py @@ -68,46 +68,46 @@ class TestHelpersEntityComponent(unittest.TestCase): group_name='everyone') # No group after setup - assert 0 == len(self.hass.states.entity_ids()) + assert len(self.hass.states.entity_ids()) == 0 component.add_entities([EntityTest(name='hello')]) # group exists - assert 2 == len(self.hass.states.entity_ids()) - assert ['group.everyone'] == self.hass.states.entity_ids('group') + assert len(self.hass.states.entity_ids()) == 2 + assert self.hass.states.entity_ids('group') == ['group.everyone'] group = self.hass.states.get('group.everyone') - assert ('test_domain.hello',) == group.attributes.get('entity_id') + assert group.attributes.get('entity_id') == ('test_domain.hello',) # group extended component.add_entities([EntityTest(name='hello2')]) - assert 3 == len(self.hass.states.entity_ids()) + assert len(self.hass.states.entity_ids()) == 3 group = self.hass.states.get('group.everyone') - assert ['test_domain.hello', 'test_domain.hello2'] == \ - sorted(group.attributes.get('entity_id')) + assert sorted(group.attributes.get('entity_id')) == \ + ['test_domain.hello', 'test_domain.hello2'] def test_polling_only_updates_entities_it_should_poll(self): """Test the polling of only updated entities.""" component = EntityComponent(_LOGGER, DOMAIN, self.hass, 20) no_poll_ent = EntityTest(should_poll=False) - no_poll_ent.update_ha_state = Mock() + no_poll_ent.async_update = Mock() poll_ent = EntityTest(should_poll=True) - poll_ent.update_ha_state = Mock() + poll_ent.async_update = Mock() component.add_entities([no_poll_ent, poll_ent]) - no_poll_ent.update_ha_state.reset_mock() - poll_ent.update_ha_state.reset_mock() + no_poll_ent.async_update.reset_mock() + poll_ent.async_update.reset_mock() fire_time_changed(self.hass, dt_util.utcnow().replace(second=0)) self.hass.block_till_done() - assert not no_poll_ent.update_ha_state.called - assert poll_ent.update_ha_state.called + assert not no_poll_ent.async_update.called + assert poll_ent.async_update.called def test_update_state_adds_entities(self): """Test if updating poll entities cause an entity to be added works.""" @@ -118,7 +118,7 @@ class TestHelpersEntityComponent(unittest.TestCase): component.add_entities([ent2]) assert 1 == len(self.hass.states.entity_ids()) - ent2.update_ha_state = lambda *_: component.add_entities([ent1]) + ent2.update = lambda *_: component.add_entities([ent1]) fire_time_changed(self.hass, dt_util.utcnow().replace(second=0)) self.hass.block_till_done() @@ -225,7 +225,7 @@ class TestHelpersEntityComponent(unittest.TestCase): assert platform2_setup.called @patch('homeassistant.helpers.entity_component.EntityComponent' - '._setup_platform') + '._async_setup_platform') @patch('homeassistant.bootstrap.setup_component', return_value=True) def test_setup_does_discovery(self, mock_setup_component, mock_setup): """Test setup for discovery.""" @@ -242,7 +242,8 @@ class TestHelpersEntityComponent(unittest.TestCase): assert ('platform_test', {}, {'msg': 'discovery_info'}) == \ mock_setup.call_args[0] - @patch('homeassistant.helpers.entity_component.track_utc_time_change') + @patch('homeassistant.helpers.entity_component.' + 'async_track_utc_time_change') def test_set_scan_interval_via_config(self, mock_track): """Test the setting of the scan interval via configuration.""" def platform_setup(hass, config, add_devices, discovery_info=None): @@ -264,7 +265,8 @@ class TestHelpersEntityComponent(unittest.TestCase): assert mock_track.called assert [0, 30] == list(mock_track.call_args[1]['second']) - @patch('homeassistant.helpers.entity_component.track_utc_time_change') + @patch('homeassistant.helpers.entity_component.' + 'async_track_utc_time_change') def test_set_scan_interval_via_platform(self, mock_track): """Test the setting of the scan interval via platform.""" def platform_setup(hass, config, add_devices, discovery_info=None): diff --git a/tests/helpers/test_service.py b/tests/helpers/test_service.py index 38af2178340..efe21f95d9b 100644 --- a/tests/helpers/test_service.py +++ b/tests/helpers/test_service.py @@ -139,7 +139,7 @@ class TestServiceHelpers(unittest.TestCase): self.hass.states.set('light.Ceiling', STATE_OFF) self.hass.states.set('light.Kitchen', STATE_OFF) - loader.get_component('group').Group( + loader.get_component('group').Group.create_group( self.hass, 'test', ['light.Ceiling', 'light.Kitchen']) call = ha.ServiceCall('light', 'turn_on', diff --git a/tests/helpers/test_template.py b/tests/helpers/test_template.py index 527d99df39e..f59c5405683 100644 --- a/tests/helpers/test_template.py +++ b/tests/helpers/test_template.py @@ -402,7 +402,8 @@ class TestHelpersTemplate(unittest.TestCase): 'longitude': self.hass.config.longitude, }) - group.Group(self.hass, 'location group', ['test_domain.object']) + group.Group.create_group( + self.hass, 'location group', ['test_domain.object']) self.assertEqual( 'test_domain.object', @@ -422,7 +423,8 @@ class TestHelpersTemplate(unittest.TestCase): 'longitude': self.hass.config.longitude, }) - group.Group(self.hass, 'location group', ['test_domain.object']) + group.Group.create_group( + self.hass, 'location group', ['test_domain.object']) self.assertEqual( 'test_domain.object',