Async EntitiesComponent (#3820)

* first version

* First draft component entities

* Change add_entities to callback from coroutine

* Fix bug add async_prepare_reload

* Group draft v1

* group async

* bugfix

* bugfix v2

* fix lint

* fix extract_entity_ids

* fix other things

* move get_component out of executor

* bugfix

* Address minor changes

* lint

* bugfix - should work now

* make group init async only

* change update handling to old stuff

* fix group handling, remove generator from init

* fix lint

* protect loop for spaming with updates

* fix lint

* update test_group

* fix

* update group handling

* fix __init__ async trouble

* move device_tracker to new layout

* lint

* fix group unittest

* Test with coroutine

* fix bug

* now it works 💯

* ups

* first part of suggestion

* add_entities to coroutine

* change group

* convert add async_add_entity to coroutine

* fix unit tests

* fix lint

* fix lint part 2

* fix wrong import delete

* change async_update_tracked_entity_ids to coroutine

* fix

* revert last change

* fix unittest entity id

* fix unittest

* fix unittest

* fix unittest entity_component

* fix group

* fix group_test

* try part 2 to fix test_group

* fix all entity_component

* rename _process_config

* Change Group to init with factory

* fix lint

* fix lint

* fix callback

* Tweak entity component and group

* More fixes

* Final fixes

* No longer needed blocks

* Address @bbangert comments

* Add test for group.stop

* More callbacks for automation
This commit is contained in:
Pascal Vizeli 2016-10-16 18:35:46 +02:00 committed by Paulus Schoutsen
parent a0fdb2778d
commit 0b8b9ecb94
14 changed files with 503 additions and 266 deletions

View File

@ -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

View File

@ -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)

View File

@ -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):

View File

@ -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

View File

@ -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,

View File

@ -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."""

View File

@ -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)
)

View File

@ -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 []

View File

@ -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):

View File

@ -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

View File

@ -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() == []

View File

@ -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):

View File

@ -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',

View File

@ -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',