mirror of
https://github.com/home-assistant/core.git
synced 2025-04-23 00:37:53 +00:00
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:
parent
a0fdb2778d
commit
0b8b9ecb94
@ -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
|
||||
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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):
|
||||
|
@ -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
|
||||
|
@ -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,
|
||||
|
@ -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."""
|
||||
|
@ -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)
|
||||
)
|
||||
|
@ -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 []
|
||||
|
@ -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):
|
||||
|
@ -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
|
||||
|
||||
|
||||
|
@ -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() == []
|
||||
|
@ -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):
|
||||
|
@ -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',
|
||||
|
@ -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',
|
||||
|
Loading…
x
Reference in New Issue
Block a user