Refactor group to extend domains that can be grouped (#40607)

Co-authored-by: Paulus Schoutsen <paulus@home-assistant.io>
This commit is contained in:
J. Nick Koston 2020-09-30 09:13:53 -05:00 committed by GitHub
parent e7d8742771
commit 9ccebdb8d5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
24 changed files with 1006 additions and 210 deletions

View File

@ -0,0 +1,14 @@
"""Describe group states."""
from homeassistant.components.group import GroupIntegrationRegistry
from homeassistant.core import callback
from homeassistant.helpers.typing import HomeAssistantType
@callback
def async_describe_on_off_states(
hass: HomeAssistantType, registry: GroupIntegrationRegistry
) -> None:
"""Describe group on off states."""
registry.exclude_domain()

View File

@ -0,0 +1,31 @@
"""Describe group states."""
from homeassistant.components.group import GroupIntegrationRegistry
from homeassistant.const import (
STATE_ALARM_ARMED_AWAY,
STATE_ALARM_ARMED_CUSTOM_BYPASS,
STATE_ALARM_ARMED_HOME,
STATE_ALARM_ARMED_NIGHT,
STATE_ALARM_TRIGGERED,
STATE_OFF,
)
from homeassistant.core import callback
from homeassistant.helpers.typing import HomeAssistantType
@callback
def async_describe_on_off_states(
hass: HomeAssistantType, registry: GroupIntegrationRegistry
) -> None:
"""Describe group on off states."""
registry.on_off_states(
{
STATE_ALARM_ARMED_AWAY,
STATE_ALARM_ARMED_CUSTOM_BYPASS,
STATE_ALARM_ARMED_HOME,
STATE_ALARM_ARMED_NIGHT,
STATE_ALARM_TRIGGERED,
},
STATE_OFF,
)

View File

@ -0,0 +1,15 @@
"""Describe group states."""
from homeassistant.components.group import GroupIntegrationRegistry
from homeassistant.const import STATE_OFF, STATE_ON
from homeassistant.core import callback
from homeassistant.helpers.typing import HomeAssistantType
@callback
def async_describe_on_off_states(
hass: HomeAssistantType, registry: GroupIntegrationRegistry
) -> None:
"""Describe group on off states."""
registry.on_off_states({STATE_ON}, STATE_OFF)

View File

@ -0,0 +1,20 @@
"""Describe group states."""
from homeassistant.components.group import GroupIntegrationRegistry
from homeassistant.const import STATE_OFF
from homeassistant.core import callback
from homeassistant.helpers.typing import HomeAssistantType
from .const import HVAC_MODE_OFF, HVAC_MODES
@callback
def async_describe_on_off_states(
hass: HomeAssistantType, registry: GroupIntegrationRegistry
) -> None:
"""Describe group on off states."""
registry.on_off_states(
set(HVAC_MODES) - {HVAC_MODE_OFF},
STATE_OFF,
)

View File

@ -1,8 +1,14 @@
"""Provide configuration end points for Groups."""
from homeassistant.components.group import DOMAIN, GROUP_SCHEMA
from homeassistant.components.group import (
DOMAIN,
GROUP_SCHEMA,
GroupIntegrationRegistry,
)
from homeassistant.config import GROUP_CONFIG_PATH
from homeassistant.const import SERVICE_RELOAD
from homeassistant.core import callback
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.typing import HomeAssistantType
from . import EditKeyBasedConfigView
@ -25,3 +31,11 @@ async def async_setup(hass):
)
)
return True
@callback
def async_describe_on_off_states(
hass: HomeAssistantType, registry: GroupIntegrationRegistry
) -> None:
"""Describe group on off states."""
return

View File

@ -0,0 +1,16 @@
"""Describe group states."""
from homeassistant.components.group import GroupIntegrationRegistry
from homeassistant.const import STATE_CLOSED, STATE_OPEN
from homeassistant.core import callback
from homeassistant.helpers.typing import HomeAssistantType
@callback
def async_describe_on_off_states(
hass: HomeAssistantType, registry: GroupIntegrationRegistry
) -> None:
"""Describe group on off states."""
# On means open, Off means closed
registry.on_off_states({STATE_OPEN}, STATE_CLOSED)

View File

@ -0,0 +1,15 @@
"""Describe group states."""
from homeassistant.components.group import GroupIntegrationRegistry
from homeassistant.const import STATE_HOME, STATE_NOT_HOME
from homeassistant.core import callback
from homeassistant.helpers.typing import HomeAssistantType
@callback
def async_describe_on_off_states(
hass: HomeAssistantType, registry: GroupIntegrationRegistry
) -> None:
"""Describe group on off states."""
registry.on_off_states({STATE_HOME}, STATE_NOT_HOME)

View File

@ -0,0 +1,15 @@
"""Describe group states."""
from homeassistant.components.group import GroupIntegrationRegistry
from homeassistant.const import STATE_OFF, STATE_ON
from homeassistant.core import callback
from homeassistant.helpers.typing import HomeAssistantType
@callback
def async_describe_on_off_states(
hass: HomeAssistantType, registry: GroupIntegrationRegistry
) -> None:
"""Describe group on off states."""
registry.on_off_states({STATE_ON}, STATE_OFF)

View File

@ -1,7 +1,8 @@
"""Provide the functionality to group entities."""
import asyncio
from contextvars import ContextVar
import logging
from typing import Any, Iterable, List, Optional, cast
from typing import Any, Dict, Iterable, List, Optional, Set, cast
import voluptuous as vol
@ -17,23 +18,18 @@ from homeassistant.const import (
ENTITY_MATCH_NONE,
EVENT_HOMEASSISTANT_START,
SERVICE_RELOAD,
STATE_CLOSED,
STATE_HOME,
STATE_LOCKED,
STATE_NOT_HOME,
STATE_OFF,
STATE_OK,
STATE_ON,
STATE_OPEN,
STATE_PROBLEM,
STATE_UNKNOWN,
STATE_UNLOCKED,
)
from homeassistant.core import CoreState, callback
from homeassistant.core import CoreState, callback, split_entity_id
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity import Entity, async_generate_entity_id
from homeassistant.helpers.entity_component import EntityComponent
from homeassistant.helpers.event import async_track_state_change_event
from homeassistant.helpers.integration_platform import (
async_process_integration_platforms,
)
from homeassistant.helpers.reload import async_reload_integration_platforms
from homeassistant.helpers.typing import HomeAssistantType
from homeassistant.loader import bind_hass
@ -60,8 +56,12 @@ SERVICE_REMOVE = "remove"
PLATFORMS = ["light", "cover", "notify"]
REG_KEY = f"{DOMAIN}_registry"
_LOGGER = logging.getLogger(__name__)
current_domain: ContextVar[str] = ContextVar("current_domain")
def _conf_preprocess(value):
"""Preprocess alternative configuration formats."""
@ -87,35 +87,38 @@ CONFIG_SCHEMA = vol.Schema(
extra=vol.ALLOW_EXTRA,
)
# List of ON/OFF state tuples for groupable states
_GROUP_TYPES = [
(STATE_ON, STATE_OFF),
(STATE_HOME, STATE_NOT_HOME),
(STATE_OPEN, STATE_CLOSED),
(STATE_LOCKED, STATE_UNLOCKED),
(STATE_PROBLEM, STATE_OK),
]
class GroupIntegrationRegistry:
"""Class to hold a registry of integrations."""
def _get_group_on_off(state):
"""Determine the group on/off states based on a state."""
for states in _GROUP_TYPES:
if state in states:
return states
on_off_mapping: Dict[str, str] = {STATE_ON: STATE_OFF}
on_states_by_domain: Dict[str, Set] = {}
exclude_domains: Set = set()
return None, None
def exclude_domain(self) -> None:
"""Exclude the current domain."""
self.exclude_domains.add(current_domain.get())
def on_off_states(self, on_states: Set, off_state: str) -> None:
"""Registry on and off states for the current domain."""
for on_state in on_states:
if on_state not in self.on_off_mapping:
self.on_off_mapping[on_state] = off_state
self.on_states_by_domain[current_domain.get()] = set(on_states)
@bind_hass
def is_on(hass, entity_id):
"""Test if the group state is in its ON-state."""
if REG_KEY not in hass.data:
# Integration not setup yet, it cannot be on
return False
state = hass.states.get(entity_id)
if state:
group_on, _ = _get_group_on_off(state.state)
# If we found a group_type, compare to ON-state
return group_on is not None and state.state == group_on
if state is not None:
return state.state in hass.data[REG_KEY].on_off_mapping
return False
@ -209,6 +212,10 @@ async def async_setup(hass, config):
if component is None:
component = hass.data[DOMAIN] = EntityComponent(_LOGGER, DOMAIN, hass)
hass.data[REG_KEY] = GroupIntegrationRegistry()
await async_process_integration_platforms(hass, DOMAIN, _process_group_platform)
await _async_process_config(hass, config, component)
async def reload_service_handler(service):
@ -332,6 +339,13 @@ async def async_setup(hass, config):
return True
async def _process_group_platform(hass, domain, platform):
"""Process a group platform."""
current_domain.set(domain)
platform.async_describe_on_off_states(hass, hass.data[REG_KEY])
async def _async_process_config(hass, config, component):
"""Process group configuration."""
hass.data.setdefault(GROUP_ORDER, 0)
@ -416,12 +430,10 @@ class Group(Entity):
self._name = name
self._state = STATE_UNKNOWN
self._icon = icon
if entity_ids:
self.tracking = tuple(ent_id.lower() for ent_id in entity_ids)
else:
self.tracking = ()
self.group_on = None
self.group_off = None
self._set_tracked(entity_ids)
self._on_off = None
self._assumed = None
self._on_states = None
self.user_defined = user_defined
self.mode = any
if mode:
@ -492,7 +504,7 @@ class Group(Entity):
if component is None:
component = hass.data[DOMAIN] = EntityComponent(_LOGGER, DOMAIN, hass)
await component.async_add_entities([group], True)
await component.async_add_entities([group])
return group
@ -550,25 +562,55 @@ class Group(Entity):
This method must be run in the event loop.
"""
await self.async_stop()
self.tracking = tuple(ent_id.lower() for ent_id in entity_ids)
self.group_on, self.group_off = None, None
self._async_stop()
self._set_tracked(entity_ids)
self._reset_tracked_state()
self._async_start()
await self.async_update_ha_state(True)
self.async_start()
def _set_tracked(self, entity_ids):
"""Tuple of entities to be tracked."""
# tracking are the entities we want to track
# trackable are the entities we actually watch
if not entity_ids:
self.tracking = ()
self.trackable = ()
return
excluded_domains = self.hass.data[REG_KEY].exclude_domains
tracking = []
trackable = []
for ent_id in entity_ids:
ent_id_lower = ent_id.lower()
domain = split_entity_id(ent_id_lower)[0]
tracking.append(ent_id_lower)
if domain not in excluded_domains:
trackable.append(ent_id_lower)
self.trackable = tuple(trackable)
self.tracking = tuple(tracking)
@callback
def async_start(self):
def _async_start(self, *_):
"""Start tracking members and write state."""
self._async_start_tracking()
self.async_write_ha_state()
@callback
def _async_start_tracking(self):
"""Start tracking members.
This method must be run in the event loop.
"""
if self._async_unsub_state_changed is None:
if self.trackable and self._async_unsub_state_changed is None:
self._async_unsub_state_changed = async_track_state_change_event(
self.hass, self.tracking, self._async_state_changed_listener
self.hass, self.trackable, self._async_state_changed_listener
)
async def async_stop(self):
self._async_update_group_state()
@callback
def _async_stop(self):
"""Unregister the group from Home Assistant.
This method must be run in the event loop.
@ -585,13 +627,19 @@ class Group(Entity):
async def async_added_to_hass(self):
"""Handle addition to Home Assistant."""
if self.tracking:
self.async_start()
self._reset_tracked_state()
if self.hass.state != CoreState.running:
self.hass.bus.async_listen_once(
EVENT_HOMEASSISTANT_START, self._async_start
)
return
self._async_start_tracking()
async def async_will_remove_from_hass(self):
"""Handle removal from Home Assistant."""
if self._async_unsub_state_changed:
self._async_unsub_state_changed()
self._async_unsub_state_changed = None
self._async_stop()
async def _async_state_changed_listener(self, event):
"""Respond to a member state changing.
@ -603,21 +651,40 @@ class Group(Entity):
return
self.async_set_context(event.context)
self._async_update_group_state(event.data.get("new_state"))
new_state = event.data.get("new_state")
if new_state is None:
# The state was removed from the state machine
self._reset_tracked_state()
self._async_update_group_state(new_state)
self.async_write_ha_state()
@property
def _tracking_states(self):
"""Return the states that the group is tracking."""
states = []
def _reset_tracked_state(self):
"""Reset tracked state."""
self._on_off = {}
self._assumed = {}
self._on_states = set()
for entity_id in self.tracking:
for entity_id in self.trackable:
state = self.hass.states.get(entity_id)
if state is not None:
states.append(state)
self._see_state(state)
return states
def _see_state(self, state):
"""Keep track of the the state."""
entity_id = state.entity_id
domain = state.domain
domain_on_state = self.hass.data[REG_KEY].on_states_by_domain.get(
domain, {STATE_ON}
)
self._on_off[entity_id] = state.state in domain_on_state
self._assumed[entity_id] = state.attributes.get(ATTR_ASSUMED_STATE)
if domain in self.hass.data[REG_KEY].on_states_by_domain:
self._on_states.update(domain_on_state)
@callback
def _async_update_group_state(self, tr_state=None):
@ -629,57 +696,40 @@ class Group(Entity):
This method must be run in the event loop.
"""
# To store current states of group entities. Might not be needed.
states = None
gr_state = self._state
gr_on = self.group_on
gr_off = self.group_off
if tr_state:
self._see_state(tr_state)
# 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)
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:
if not self._on_off:
return
if tr_state is None or (
(gr_state == gr_on and tr_state.state == gr_off)
or (gr_state == gr_off and tr_state.state == gr_on)
or tr_state.state not in (gr_on, gr_off)
):
if states is None:
states = self._tracking_states
if self.mode(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
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 = self.mode(
state.attributes.get(ATTR_ASSUMED_STATE) for state in states
)
self._assumed_state = self.mode(self._assumed.values())
elif tr_state.attributes.get(ATTR_ASSUMED_STATE):
self._assumed_state = True
num_on_states = len(self._on_states)
# If all the entity domains we are tracking
# have the same on state we use this state
# and its hass.data[REG_KEY].on_off_mapping to off
if num_on_states == 1:
on_state = list(self._on_states)[0]
# If we do not have an on state for any domains
# we use STATE_UNKNOWN
elif num_on_states == 0:
self._state = STATE_UNKNOWN
return
# If the entity domains have more than one
# on state, we use STATE_ON/STATE_OFF
else:
on_state = STATE_ON
group_is_on = self.mode(self._on_off.values())
if group_is_on:
self._state = on_state
else:
self._state = self.hass.data[REG_KEY].on_off_mapping[on_state]

View File

@ -0,0 +1,15 @@
"""Describe group states."""
from homeassistant.components.group import GroupIntegrationRegistry
from homeassistant.const import STATE_OFF, STATE_ON
from homeassistant.core import callback
from homeassistant.helpers.typing import HomeAssistantType
@callback
def async_describe_on_off_states(
hass: HomeAssistantType, registry: GroupIntegrationRegistry
) -> None:
"""Describe group on off states."""
registry.on_off_states({STATE_ON}, STATE_OFF)

View File

@ -0,0 +1,15 @@
"""Describe group states."""
from homeassistant.components.group import GroupIntegrationRegistry
from homeassistant.const import STATE_OFF, STATE_ON
from homeassistant.core import callback
from homeassistant.helpers.typing import HomeAssistantType
@callback
def async_describe_on_off_states(
hass: HomeAssistantType, registry: GroupIntegrationRegistry
) -> None:
"""Describe group on off states."""
registry.on_off_states({STATE_ON}, STATE_OFF)

View File

@ -0,0 +1,15 @@
"""Describe group states."""
from homeassistant.components.group import GroupIntegrationRegistry
from homeassistant.const import STATE_LOCKED, STATE_UNLOCKED
from homeassistant.core import callback
from homeassistant.helpers.typing import HomeAssistantType
@callback
def async_describe_on_off_states(
hass: HomeAssistantType, registry: GroupIntegrationRegistry
) -> None:
"""Describe group on off states."""
registry.on_off_states({STATE_LOCKED}, STATE_UNLOCKED)

View File

@ -0,0 +1,17 @@
"""Describe group states."""
from homeassistant.components.group import GroupIntegrationRegistry
from homeassistant.const import STATE_OFF
from homeassistant.core import callback
from homeassistant.helpers.typing import HomeAssistantType
from . import STATE_IDLE, STATE_PLAYING
@callback
def async_describe_on_off_states(
hass: HomeAssistantType, registry: GroupIntegrationRegistry
) -> None:
"""Describe group on off states."""
registry.on_off_states({STATE_PLAYING, STATE_IDLE}, STATE_OFF)

View File

@ -0,0 +1,15 @@
"""Describe group states."""
from homeassistant.components.group import GroupIntegrationRegistry
from homeassistant.const import STATE_HOME, STATE_NOT_HOME
from homeassistant.core import callback
from homeassistant.helpers.typing import HomeAssistantType
@callback
def async_describe_on_off_states(
hass: HomeAssistantType, registry: GroupIntegrationRegistry
) -> None:
"""Describe group on off states."""
registry.on_off_states({STATE_HOME}, STATE_NOT_HOME)

View File

@ -0,0 +1,15 @@
"""Describe group states."""
from homeassistant.components.group import GroupIntegrationRegistry
from homeassistant.const import STATE_OFF, STATE_ON
from homeassistant.core import callback
from homeassistant.helpers.typing import HomeAssistantType
@callback
def async_describe_on_off_states(
hass: HomeAssistantType, registry: GroupIntegrationRegistry
) -> None:
"""Describe group on off states."""
registry.on_off_states({STATE_ON}, STATE_OFF)

View File

@ -0,0 +1,14 @@
"""Describe group states."""
from homeassistant.components.group import GroupIntegrationRegistry
from homeassistant.core import callback
from homeassistant.helpers.typing import HomeAssistantType
@callback
def async_describe_on_off_states(
hass: HomeAssistantType, registry: GroupIntegrationRegistry
) -> None:
"""Describe group on off states."""
registry.exclude_domain()

View File

@ -0,0 +1,15 @@
"""Describe group states."""
from homeassistant.components.group import GroupIntegrationRegistry
from homeassistant.const import STATE_OFF, STATE_ON
from homeassistant.core import callback
from homeassistant.helpers.typing import HomeAssistantType
@callback
def async_describe_on_off_states(
hass: HomeAssistantType, registry: GroupIntegrationRegistry
) -> None:
"""Describe group on off states."""
registry.on_off_states({STATE_ON}, STATE_OFF)

View File

@ -0,0 +1,19 @@
"""Describe group states."""
from homeassistant.components.group import GroupIntegrationRegistry
from homeassistant.const import STATE_OFF, STATE_ON
from homeassistant.core import callback
from homeassistant.helpers.typing import HomeAssistantType
from . import STATE_CLEANING, STATE_ERROR, STATE_RETURNING
@callback
def async_describe_on_off_states(
hass: HomeAssistantType, registry: GroupIntegrationRegistry
) -> None:
"""Describe group on off states."""
registry.on_off_states(
{STATE_CLEANING, STATE_ON, STATE_RETURNING, STATE_ERROR}, STATE_OFF
)

View File

@ -0,0 +1,34 @@
"""Describe group states."""
from homeassistant.components.group import GroupIntegrationRegistry
from homeassistant.const import STATE_OFF
from homeassistant.core import callback
from homeassistant.helpers.typing import HomeAssistantType
from . import (
STATE_ECO,
STATE_ELECTRIC,
STATE_GAS,
STATE_HEAT_PUMP,
STATE_HIGH_DEMAND,
STATE_PERFORMANCE,
)
@callback
def async_describe_on_off_states(
hass: HomeAssistantType, registry: GroupIntegrationRegistry
) -> None:
"""Describe group on off states."""
registry.on_off_states(
{
STATE_ECO,
STATE_ELECTRIC,
STATE_PERFORMANCE,
STATE_HIGH_DEMAND,
STATE_HEAT_PUMP,
STATE_GAS,
},
STATE_OFF,
)

View File

@ -0,0 +1,14 @@
"""Describe group states."""
from homeassistant.components.group import GroupIntegrationRegistry
from homeassistant.core import callback
from homeassistant.helpers.typing import HomeAssistantType
@callback
def async_describe_on_off_states(
hass: HomeAssistantType, registry: GroupIntegrationRegistry
) -> None:
"""Describe group on off states."""
registry.exclude_domain()

View File

@ -176,6 +176,8 @@ async def test_lights_turn_on_when_coming_home_after_sun_set_person(hass, scanne
{"person": [{"id": "me", "name": "Me", "device_trackers": [device_1]}]},
)
assert await async_setup_component(hass, "group", {})
await hass.async_block_till_done()
await group.Group.async_create_group(hass, "person_me", ["person.me"])
assert await async_setup_component(

View File

@ -8,12 +8,15 @@ from homeassistant.const import (
ATTR_ASSUMED_STATE,
ATTR_FRIENDLY_NAME,
ATTR_ICON,
EVENT_HOMEASSISTANT_START,
SERVICE_RELOAD,
STATE_HOME,
STATE_NOT_HOME,
STATE_OFF,
STATE_ON,
STATE_UNKNOWN,
)
from homeassistant.core import CoreState
from homeassistant.helpers.event import TRACK_STATE_CHANGE_CALLBACKS
from homeassistant.setup import async_setup_component, setup_component
@ -29,6 +32,8 @@ class TestComponentsGroup(unittest.TestCase):
def setUp(self):
"""Set up things to be run when tests are started."""
self.hass = get_test_home_assistant()
for domain in ["device_tracker", "light", "group", "sensor"]:
setup_component(self.hass, domain, {})
self.addCleanup(self.hass.stop)
def test_setup_group_with_mixed_groupable_states(self):
@ -143,22 +148,6 @@ class TestComponentsGroup(unittest.TestCase):
group_state = self.hass.states.get(test_group.entity_id)
assert STATE_ON == group_state.state
def test_is_on(self):
"""Test is_on method."""
self.hass.states.set("light.Bowl", STATE_ON)
self.hass.states.set("light.Ceiling", STATE_OFF)
test_group = group.Group.create_group(
self.hass, "init_group", ["light.Bowl", "light.Ceiling"], False
)
assert group.is_on(self.hass, test_group.entity_id)
self.hass.states.set("light.Bowl", STATE_OFF)
self.hass.block_till_done()
assert not group.is_on(self.hass, test_group.entity_id)
# Try on non existing state
assert not group.is_on(self.hass, "non.existing")
def test_expand_entity_ids(self):
"""Test expand_entity_ids method."""
self.hass.states.set("light.Bowl", STATE_ON)
@ -272,42 +261,6 @@ class TestComponentsGroup(unittest.TestCase):
group_state = self.hass.states.get(test_group.entity_id)
assert STATE_OFF == group_state.state
def test_setup(self):
"""Test setup method."""
self.hass.states.set("light.Bowl", STATE_ON)
self.hass.states.set("light.Ceiling", STATE_OFF)
test_group = group.Group.create_group(
self.hass, "init_group", ["light.Bowl", "light.Ceiling"], False
)
group_conf = OrderedDict()
group_conf["second_group"] = {
"entities": f"light.Bowl, {test_group.entity_id}",
"icon": "mdi:work",
}
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})
group_state = self.hass.states.get(f"{group.DOMAIN}.second_group")
assert STATE_ON == group_state.state
assert {test_group.entity_id, "light.bowl"} == set(
group_state.attributes["entity_id"]
)
assert group_state.attributes.get(group.ATTR_AUTO) is None
assert "mdi:work" == group_state.attributes.get(ATTR_ICON)
assert 1 == group_state.attributes.get(group.ATTR_ORDER)
group_state = self.hass.states.get(f"{group.DOMAIN}.test_group")
assert STATE_UNKNOWN == group_state.state
assert {"sensor.happy", "hello.world"} == set(
group_state.attributes["entity_id"]
)
assert group_state.attributes.get(group.ATTR_AUTO) is None
assert group_state.attributes.get(ATTR_ICON) is None
assert 2 == group_state.attributes.get(group.ATTR_ORDER)
def test_groups_get_unique_names(self):
"""Two groups with same name should both have a unique entity id."""
grp1 = group.Group.create_group(self.hass, "Je suis Charlie")
@ -367,72 +320,150 @@ class TestComponentsGroup(unittest.TestCase):
self.hass.block_till_done()
assert STATE_NOT_HOME == self.hass.states.get(f"{group.DOMAIN}.peeps").state
def test_reloading_groups(self):
"""Test reloading the group config."""
assert setup_component(
self.hass,
"group",
{
"group": {
"second_group": {"entities": "light.Bowl", "icon": "mdi:work"},
"test_group": "hello.world,sensor.happy",
"empty_group": {"name": "Empty Group", "entities": None},
}
},
)
group.Group.create_group(
self.hass, "all tests", ["test.one", "test.two"], user_defined=False
)
async def test_is_on(hass):
"""Test is_on method."""
hass.states.async_set("light.Bowl", STATE_ON)
hass.states.async_set("light.Ceiling", STATE_OFF)
assert sorted(self.hass.states.entity_ids()) == [
"group.all_tests",
"group.empty_group",
"group.second_group",
"group.test_group",
]
assert self.hass.bus.listeners["state_changed"] == 1
assert len(self.hass.data[TRACK_STATE_CHANGE_CALLBACKS]["hello.world"]) == 1
assert len(self.hass.data[TRACK_STATE_CHANGE_CALLBACKS]["sensor.happy"]) == 1
assert len(self.hass.data[TRACK_STATE_CHANGE_CALLBACKS]["light.bowl"]) == 1
assert len(self.hass.data[TRACK_STATE_CHANGE_CALLBACKS]["test.one"]) == 1
assert len(self.hass.data[TRACK_STATE_CHANGE_CALLBACKS]["test.two"]) == 1
assert group.is_on(hass, "group.none") is False
assert await async_setup_component(hass, "light", {})
assert await async_setup_component(hass, "group", {})
await hass.async_block_till_done()
with patch(
"homeassistant.config.load_yaml_config_file",
return_value={
"group": {"hello": {"entities": "light.Bowl", "icon": "mdi:work"}}
},
):
common.reload(self.hass)
self.hass.block_till_done()
test_group = await group.Group.async_create_group(
hass, "init_group", ["light.Bowl", "light.Ceiling"], False
)
await hass.async_block_till_done()
assert sorted(self.hass.states.entity_ids()) == [
"group.all_tests",
"group.hello",
]
assert self.hass.bus.listeners["state_changed"] == 1
assert len(self.hass.data[TRACK_STATE_CHANGE_CALLBACKS]["light.bowl"]) == 1
assert len(self.hass.data[TRACK_STATE_CHANGE_CALLBACKS]["test.one"]) == 1
assert len(self.hass.data[TRACK_STATE_CHANGE_CALLBACKS]["test.two"]) == 1
assert group.is_on(hass, test_group.entity_id) is True
hass.states.async_set("light.Bowl", STATE_OFF)
await hass.async_block_till_done()
assert group.is_on(hass, test_group.entity_id) is False
def test_modify_group(self):
"""Test modifying a group."""
group_conf = OrderedDict()
group_conf["modify_group"] = {"name": "friendly_name", "icon": "mdi:work"}
# Try on non existing state
assert not group.is_on(hass, "non.existing")
assert setup_component(self.hass, "group", {"group": group_conf})
# The old way would create a new group modify_group1 because
# internally it didn't know anything about those created in the config
common.set_group(self.hass, "modify_group", icon="mdi:play")
self.hass.block_till_done()
async def test_reloading_groups(hass):
"""Test reloading the group config."""
assert await async_setup_component(
hass,
"group",
{
"group": {
"second_group": {"entities": "light.Bowl", "icon": "mdi:work"},
"test_group": "hello.world,sensor.happy",
"empty_group": {"name": "Empty Group", "entities": None},
}
},
)
await hass.async_block_till_done()
group_state = self.hass.states.get(f"{group.DOMAIN}.modify_group")
await group.Group.async_create_group(
hass, "all tests", ["test.one", "test.two"], user_defined=False
)
assert self.hass.states.entity_ids() == ["group.modify_group"]
assert group_state.attributes.get(ATTR_ICON) == "mdi:play"
assert group_state.attributes.get(ATTR_FRIENDLY_NAME) == "friendly_name"
await hass.async_block_till_done()
assert sorted(hass.states.async_entity_ids()) == [
"group.all_tests",
"group.empty_group",
"group.second_group",
"group.test_group",
]
assert hass.bus.async_listeners()["state_changed"] == 1
assert len(hass.data[TRACK_STATE_CHANGE_CALLBACKS]["hello.world"]) == 1
assert len(hass.data[TRACK_STATE_CHANGE_CALLBACKS]["light.bowl"]) == 1
assert len(hass.data[TRACK_STATE_CHANGE_CALLBACKS]["test.one"]) == 1
assert len(hass.data[TRACK_STATE_CHANGE_CALLBACKS]["test.two"]) == 1
with patch(
"homeassistant.config.load_yaml_config_file",
return_value={
"group": {"hello": {"entities": "light.Bowl", "icon": "mdi:work"}}
},
):
await hass.services.async_call(group.DOMAIN, SERVICE_RELOAD)
await hass.async_block_till_done()
assert sorted(hass.states.async_entity_ids()) == [
"group.all_tests",
"group.hello",
]
assert hass.bus.async_listeners()["state_changed"] == 1
assert len(hass.data[TRACK_STATE_CHANGE_CALLBACKS]["light.bowl"]) == 1
assert len(hass.data[TRACK_STATE_CHANGE_CALLBACKS]["test.one"]) == 1
assert len(hass.data[TRACK_STATE_CHANGE_CALLBACKS]["test.two"]) == 1
async def test_modify_group(hass):
"""Test modifying a group."""
group_conf = OrderedDict()
group_conf["modify_group"] = {
"name": "friendly_name",
"icon": "mdi:work",
"entities": None,
}
assert await async_setup_component(hass, "group", {"group": group_conf})
await hass.async_block_till_done()
assert hass.states.get(f"{group.DOMAIN}.modify_group")
# The old way would create a new group modify_group1 because
# internally it didn't know anything about those created in the config
common.async_set_group(hass, "modify_group", icon="mdi:play")
await hass.async_block_till_done()
group_state = hass.states.get(f"{group.DOMAIN}.modify_group")
assert group_state
assert hass.states.async_entity_ids() == ["group.modify_group"]
assert group_state.attributes.get(ATTR_ICON) == "mdi:play"
assert group_state.attributes.get(ATTR_FRIENDLY_NAME) == "friendly_name"
async def test_setup(hass):
"""Test setup method."""
hass.states.async_set("light.Bowl", STATE_ON)
hass.states.async_set("light.Ceiling", STATE_OFF)
group_conf = OrderedDict()
group_conf["test_group"] = "hello.world,sensor.happy"
group_conf["empty_group"] = {"name": "Empty Group", "entities": None}
assert await async_setup_component(hass, "light", {})
await hass.async_block_till_done()
assert await async_setup_component(hass, "group", {"group": group_conf})
await hass.async_block_till_done()
test_group = await group.Group.async_create_group(
hass, "init_group", ["light.Bowl", "light.Ceiling"], False
)
await group.Group.async_create_group(
hass,
"created_group",
["light.Bowl", f"{test_group.entity_id}"],
True,
"mdi:work",
)
await hass.async_block_till_done()
group_state = hass.states.get(f"{group.DOMAIN}.created_group")
assert STATE_ON == group_state.state
assert {test_group.entity_id, "light.bowl"} == set(
group_state.attributes["entity_id"]
)
assert group_state.attributes.get(group.ATTR_AUTO) is None
assert "mdi:work" == group_state.attributes.get(ATTR_ICON)
assert 3 == group_state.attributes.get(group.ATTR_ORDER)
group_state = hass.states.get(f"{group.DOMAIN}.test_group")
assert STATE_UNKNOWN == group_state.state
assert {"sensor.happy", "hello.world"} == set(group_state.attributes["entity_id"])
assert group_state.attributes.get(group.ATTR_AUTO) is None
assert group_state.attributes.get(ATTR_ICON) is None
assert 0 == group_state.attributes.get(group.ATTR_ORDER)
async def test_service_group_services(hass):
@ -496,6 +527,7 @@ async def test_group_order(hass):
"""Test that order gets incremented when creating a new group."""
hass.states.async_set("light.bowl", STATE_ON)
assert await async_setup_component(hass, "light", {})
assert await async_setup_component(
hass,
"group",
@ -518,6 +550,7 @@ async def test_group_order_with_dynamic_creation(hass):
"""Test that order gets incremented when creating a new group."""
hass.states.async_set("light.bowl", STATE_ON)
assert await async_setup_component(hass, "light", {})
assert await async_setup_component(
hass,
"group",
@ -563,3 +596,372 @@ async def test_group_order_with_dynamic_creation(hass):
await hass.async_block_till_done()
assert hass.states.get("group.new_group2").attributes["order"] == 4
async def test_group_persons(hass):
"""Test group of persons."""
hass.states.async_set("person.one", "Work")
hass.states.async_set("person.two", "Work")
hass.states.async_set("person.three", "home")
assert await async_setup_component(hass, "person", {})
assert await async_setup_component(
hass,
"group",
{
"group": {
"group_zero": {"entities": "person.one, person.two, person.three"},
}
},
)
await hass.async_block_till_done()
assert hass.states.get("group.group_zero").state == "home"
async def test_group_persons_and_device_trackers(hass):
"""Test group of persons and device_tracker."""
hass.states.async_set("person.one", "Work")
hass.states.async_set("person.two", "Work")
hass.states.async_set("person.three", "Work")
hass.states.async_set("device_tracker.one", "home")
assert await async_setup_component(hass, "person", {})
assert await async_setup_component(hass, "device_tracker", {})
assert await async_setup_component(
hass,
"group",
{
"group": {
"group_zero": {
"entities": "device_tracker.one, person.one, person.two, person.three"
},
}
},
)
await hass.async_block_till_done()
assert hass.states.get("group.group_zero").state == "home"
async def test_group_mixed_domains_on(hass):
"""Test group of mixed domains that is on."""
hass.states.async_set("lock.alexander_garage_exit_door", "locked")
hass.states.async_set("binary_sensor.alexander_garage_side_door_open", "on")
hass.states.async_set("cover.small_garage_door", "open")
for domain in ["lock", "binary_sensor", "cover"]:
assert await async_setup_component(hass, domain, {})
assert await async_setup_component(
hass,
"group",
{
"group": {
"group_zero": {
"all": "true",
"entities": "lock.alexander_garage_exit_door, binary_sensor.alexander_garage_side_door_open, cover.small_garage_door",
},
}
},
)
await hass.async_block_till_done()
assert hass.states.get("group.group_zero").state == "on"
async def test_group_mixed_domains_off(hass):
"""Test group of mixed domains that is off."""
hass.states.async_set("lock.alexander_garage_exit_door", "unlocked")
hass.states.async_set("binary_sensor.alexander_garage_side_door_open", "off")
hass.states.async_set("cover.small_garage_door", "closed")
for domain in ["lock", "binary_sensor", "cover"]:
assert await async_setup_component(hass, domain, {})
assert await async_setup_component(
hass,
"group",
{
"group": {
"group_zero": {
"all": "true",
"entities": "lock.alexander_garage_exit_door, binary_sensor.alexander_garage_side_door_open, cover.small_garage_door",
},
}
},
)
await hass.async_block_till_done()
assert hass.states.get("group.group_zero").state == "off"
async def test_group_locks(hass):
"""Test group of locks."""
hass.states.async_set("lock.one", "locked")
hass.states.async_set("lock.two", "locked")
hass.states.async_set("lock.three", "unlocked")
assert await async_setup_component(hass, "lock", {})
assert await async_setup_component(
hass,
"group",
{
"group": {
"group_zero": {"entities": "lock.one, lock.two, lock.three"},
}
},
)
await hass.async_block_till_done()
assert hass.states.get("group.group_zero").state == "locked"
async def test_group_sensors(hass):
"""Test group of sensors."""
hass.states.async_set("sensor.one", "locked")
hass.states.async_set("sensor.two", "on")
hass.states.async_set("sensor.three", "closed")
assert await async_setup_component(hass, "sensor", {})
assert await async_setup_component(
hass,
"group",
{
"group": {
"group_zero": {"entities": "sensor.one, sensor.two, sensor.three"},
}
},
)
await hass.async_block_till_done()
assert hass.states.get("group.group_zero").state == "unknown"
async def test_group_climate_mixed(hass):
"""Test group of climate with mixed states."""
hass.states.async_set("climate.one", "off")
hass.states.async_set("climate.two", "cool")
hass.states.async_set("climate.three", "heat")
assert await async_setup_component(hass, "climate", {})
assert await async_setup_component(
hass,
"group",
{
"group": {
"group_zero": {"entities": "climate.one, climate.two, climate.three"},
}
},
)
await hass.async_block_till_done()
assert hass.states.get("group.group_zero").state == STATE_ON
async def test_group_climate_all_cool(hass):
"""Test group of climate all set to cool."""
hass.states.async_set("climate.one", "cool")
hass.states.async_set("climate.two", "cool")
hass.states.async_set("climate.three", "cool")
assert await async_setup_component(hass, "climate", {})
assert await async_setup_component(
hass,
"group",
{
"group": {
"group_zero": {"entities": "climate.one, climate.two, climate.three"},
}
},
)
await hass.async_block_till_done()
assert hass.states.get("group.group_zero").state == STATE_ON
async def test_group_climate_all_off(hass):
"""Test group of climate all set to off."""
hass.states.async_set("climate.one", "off")
hass.states.async_set("climate.two", "off")
hass.states.async_set("climate.three", "off")
assert await async_setup_component(hass, "climate", {})
assert await async_setup_component(
hass,
"group",
{
"group": {
"group_zero": {"entities": "climate.one, climate.two, climate.three"},
}
},
)
await hass.async_block_till_done()
assert hass.states.get("group.group_zero").state == STATE_OFF
async def test_group_alarm(hass):
"""Test group of alarm control panels."""
hass.states.async_set("alarm_control_panel.one", "armed_away")
hass.states.async_set("alarm_control_panel.two", "armed_home")
hass.states.async_set("alarm_control_panel.three", "armed_away")
assert await async_setup_component(hass, "alarm_control_panel", {})
assert await async_setup_component(
hass,
"group",
{
"group": {
"group_zero": {
"entities": "alarm_control_panel.one, alarm_control_panel.two, alarm_control_panel.three"
},
}
},
)
await hass.async_block_till_done()
assert hass.states.get("group.group_zero").state == STATE_ON
async def test_group_alarm_disarmed(hass):
"""Test group of alarm control panels disarmed."""
hass.states.async_set("alarm_control_panel.one", "disarmed")
hass.states.async_set("alarm_control_panel.two", "disarmed")
hass.states.async_set("alarm_control_panel.three", "disarmed")
assert await async_setup_component(hass, "alarm_control_panel", {})
assert await async_setup_component(
hass,
"group",
{
"group": {
"group_zero": {
"entities": "alarm_control_panel.one, alarm_control_panel.two, alarm_control_panel.three"
},
}
},
)
await hass.async_block_till_done()
assert hass.states.get("group.group_zero").state == STATE_OFF
async def test_group_vacuum_off(hass):
"""Test group of vacuums."""
hass.states.async_set("vacuum.one", "docked")
hass.states.async_set("vacuum.two", "off")
hass.states.async_set("vacuum.three", "off")
assert await async_setup_component(hass, "vacuum", {})
assert await async_setup_component(
hass,
"group",
{
"group": {
"group_zero": {"entities": "vacuum.one, vacuum.two, vacuum.three"},
}
},
)
await hass.async_block_till_done()
assert hass.states.get("group.group_zero").state == STATE_OFF
async def test_group_vacuum_on(hass):
"""Test group of vacuums."""
hass.states.async_set("vacuum.one", "cleaning")
hass.states.async_set("vacuum.two", "off")
hass.states.async_set("vacuum.three", "off")
assert await async_setup_component(hass, "vacuum", {})
assert await async_setup_component(
hass,
"group",
{
"group": {
"group_zero": {"entities": "vacuum.one, vacuum.two, vacuum.three"},
}
},
)
await hass.async_block_till_done()
assert hass.states.get("group.group_zero").state == STATE_ON
async def test_device_tracker_not_home(hass):
"""Test group of device_tracker not_home."""
hass.states.async_set("device_tracker.one", "not_home")
hass.states.async_set("device_tracker.two", "not_home")
hass.states.async_set("device_tracker.three", "not_home")
assert await async_setup_component(hass, "device_tracker", {})
assert await async_setup_component(
hass,
"group",
{
"group": {
"group_zero": {
"entities": "device_tracker.one, device_tracker.two, device_tracker.three"
},
}
},
)
await hass.async_block_till_done()
assert hass.states.get("group.group_zero").state == "not_home"
async def test_light_removed(hass):
"""Test group of lights when one is removed."""
hass.states.async_set("light.one", "off")
hass.states.async_set("light.two", "off")
hass.states.async_set("light.three", "on")
assert await async_setup_component(hass, "light", {})
assert await async_setup_component(
hass,
"group",
{
"group": {
"group_zero": {"entities": "light.one, light.two, light.three"},
}
},
)
await hass.async_block_till_done()
assert hass.states.get("group.group_zero").state == "on"
hass.states.async_remove("light.three")
await hass.async_block_till_done()
assert hass.states.get("group.group_zero").state == "off"
async def test_switch_removed(hass):
"""Test group of switches when one is removed."""
hass.states.async_set("switch.one", "off")
hass.states.async_set("switch.two", "off")
hass.states.async_set("switch.three", "on")
hass.state = CoreState.stopped
assert await async_setup_component(hass, "switch", {})
assert await async_setup_component(
hass,
"group",
{
"group": {
"group_zero": {"entities": "switch.one, switch.two, switch.three"},
}
},
)
await hass.async_block_till_done()
assert hass.states.get("group.group_zero").state == "unknown"
hass.bus.async_fire(EVENT_HOMEASSISTANT_START)
await hass.async_block_till_done()
assert hass.states.get("group.group_zero").state == "on"
hass.states.async_remove("switch.three")
await hass.async_block_till_done()
assert hass.states.get("group.group_zero").state == "off"

View File

@ -267,6 +267,8 @@ async def test_extract_entity_ids(hass):
hass.states.async_set("light.Ceiling", STATE_OFF)
hass.states.async_set("light.Kitchen", STATE_OFF)
assert await async_setup_component(hass, "group", {})
await hass.async_block_till_done()
await hass.components.group.Group.async_create_group(
hass, "test", ["light.Ceiling", "light.Kitchen"]
)

View File

@ -18,6 +18,7 @@ from homeassistant.const import (
)
from homeassistant.exceptions import TemplateError
from homeassistant.helpers import template
from homeassistant.setup import async_setup_component
import homeassistant.util.dt as dt_util
from homeassistant.util.unit_system import UnitSystem
@ -1333,6 +1334,8 @@ async def test_closest_function_home_vs_group_entity_id(hass):
{"latitude": hass.config.latitude, "longitude": hass.config.longitude},
)
assert await async_setup_component(hass, "group", {})
await hass.async_block_till_done()
await group.Group.async_create_group(hass, "location group", ["test_domain.object"])
info = render_to_info(hass, '{{ closest("group.location_group").entity_id }}')
@ -1358,6 +1361,8 @@ async def test_closest_function_home_vs_group_state(hass):
{"latitude": hass.config.latitude, "longitude": hass.config.longitude},
)
assert await async_setup_component(hass, "group", {})
await hass.async_block_till_done()
await group.Group.async_create_group(hass, "location group", ["test_domain.object"])
info = render_to_info(hass, '{{ closest("group.location_group").entity_id }}')
@ -1397,6 +1402,8 @@ async def test_expand(hass):
)
assert_result_info(info, "", [], ["group"])
assert await async_setup_component(hass, "group", {})
await hass.async_block_till_done()
await group.Group.async_create_group(hass, "new group", ["test.object"])
info = render_to_info(
@ -1429,6 +1436,9 @@ async def test_expand(hass):
hass.states.async_set("sensor.power_1", 0)
hass.states.async_set("sensor.power_2", 200.2)
hass.states.async_set("sensor.power_3", 400.4)
assert await async_setup_component(hass, "group", {})
await hass.async_block_till_done()
await group.Group.async_create_group(
hass, "power sensors", ["sensor.power_1", "sensor.power_2", "sensor.power_3"]
)
@ -2095,6 +2105,8 @@ states.sensor.pick_humidity.state ~ " %"
)
)
assert await async_setup_component(hass, "group", {})
await hass.async_block_till_done()
await group.Group.async_create_group(hass, "empty group", [])
assert ["group.empty_group"] == template.extract_entities(