Refactor group to extend domains that can be grouped (#40607)
Co-authored-by: Paulus Schoutsen <paulus@home-assistant.io>
This commit is contained in:
parent
e7d8742771
commit
9ccebdb8d5
24 changed files with 1006 additions and 210 deletions
14
homeassistant/components/air_quality/group.py
Normal file
14
homeassistant/components/air_quality/group.py
Normal 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()
|
31
homeassistant/components/alarm_control_panel/group.py
Normal file
31
homeassistant/components/alarm_control_panel/group.py
Normal 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,
|
||||
)
|
15
homeassistant/components/binary_sensor/group.py
Normal file
15
homeassistant/components/binary_sensor/group.py
Normal 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)
|
20
homeassistant/components/climate/group.py
Normal file
20
homeassistant/components/climate/group.py
Normal 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,
|
||||
)
|
|
@ -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
|
||||
|
|
16
homeassistant/components/cover/group.py
Normal file
16
homeassistant/components/cover/group.py
Normal 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)
|
15
homeassistant/components/device_tracker/group.py
Normal file
15
homeassistant/components/device_tracker/group.py
Normal 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)
|
15
homeassistant/components/fan/group.py
Normal file
15
homeassistant/components/fan/group.py
Normal 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)
|
|
@ -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]
|
||||
|
|
15
homeassistant/components/humidifier/group.py
Normal file
15
homeassistant/components/humidifier/group.py
Normal 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)
|
15
homeassistant/components/light/group.py
Normal file
15
homeassistant/components/light/group.py
Normal 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)
|
15
homeassistant/components/lock/group.py
Normal file
15
homeassistant/components/lock/group.py
Normal 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)
|
17
homeassistant/components/media_player/group.py
Normal file
17
homeassistant/components/media_player/group.py
Normal 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)
|
15
homeassistant/components/person/group.py
Normal file
15
homeassistant/components/person/group.py
Normal 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)
|
15
homeassistant/components/remote/group.py
Normal file
15
homeassistant/components/remote/group.py
Normal 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)
|
14
homeassistant/components/sensor/group.py
Normal file
14
homeassistant/components/sensor/group.py
Normal 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()
|
15
homeassistant/components/switch/group.py
Normal file
15
homeassistant/components/switch/group.py
Normal 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)
|
19
homeassistant/components/vacuum/group.py
Normal file
19
homeassistant/components/vacuum/group.py
Normal 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
|
||||
)
|
34
homeassistant/components/water_heater/group.py
Normal file
34
homeassistant/components/water_heater/group.py
Normal 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,
|
||||
)
|
14
homeassistant/components/weather/group.py
Normal file
14
homeassistant/components/weather/group.py
Normal 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()
|
|
@ -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(
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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"]
|
||||
)
|
||||
|
|
|
@ -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(
|
||||
|
|
Loading…
Add table
Reference in a new issue