diff --git a/homeassistant/components/air_quality/group.py b/homeassistant/components/air_quality/group.py new file mode 100644 index 00000000000..4741f8a3b54 --- /dev/null +++ b/homeassistant/components/air_quality/group.py @@ -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() diff --git a/homeassistant/components/alarm_control_panel/group.py b/homeassistant/components/alarm_control_panel/group.py new file mode 100644 index 00000000000..6645f12245d --- /dev/null +++ b/homeassistant/components/alarm_control_panel/group.py @@ -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, + ) diff --git a/homeassistant/components/binary_sensor/group.py b/homeassistant/components/binary_sensor/group.py new file mode 100644 index 00000000000..1636054663d --- /dev/null +++ b/homeassistant/components/binary_sensor/group.py @@ -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) diff --git a/homeassistant/components/climate/group.py b/homeassistant/components/climate/group.py new file mode 100644 index 00000000000..87674da414b --- /dev/null +++ b/homeassistant/components/climate/group.py @@ -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, + ) diff --git a/homeassistant/components/config/group.py b/homeassistant/components/config/group.py index e26b2b80bc1..22a9bf4f02a 100644 --- a/homeassistant/components/config/group.py +++ b/homeassistant/components/config/group.py @@ -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 diff --git a/homeassistant/components/cover/group.py b/homeassistant/components/cover/group.py new file mode 100644 index 00000000000..d031b7cf693 --- /dev/null +++ b/homeassistant/components/cover/group.py @@ -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) diff --git a/homeassistant/components/device_tracker/group.py b/homeassistant/components/device_tracker/group.py new file mode 100644 index 00000000000..07ec2cfe985 --- /dev/null +++ b/homeassistant/components/device_tracker/group.py @@ -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) diff --git a/homeassistant/components/fan/group.py b/homeassistant/components/fan/group.py new file mode 100644 index 00000000000..1636054663d --- /dev/null +++ b/homeassistant/components/fan/group.py @@ -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) diff --git a/homeassistant/components/group/__init__.py b/homeassistant/components/group/__init__.py index 87eb2cd615b..ed7e7d0bff0 100644 --- a/homeassistant/components/group/__init__.py +++ b/homeassistant/components/group/__init__.py @@ -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] diff --git a/homeassistant/components/humidifier/group.py b/homeassistant/components/humidifier/group.py new file mode 100644 index 00000000000..1636054663d --- /dev/null +++ b/homeassistant/components/humidifier/group.py @@ -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) diff --git a/homeassistant/components/light/group.py b/homeassistant/components/light/group.py new file mode 100644 index 00000000000..1636054663d --- /dev/null +++ b/homeassistant/components/light/group.py @@ -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) diff --git a/homeassistant/components/lock/group.py b/homeassistant/components/lock/group.py new file mode 100644 index 00000000000..d64b2172750 --- /dev/null +++ b/homeassistant/components/lock/group.py @@ -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) diff --git a/homeassistant/components/media_player/group.py b/homeassistant/components/media_player/group.py new file mode 100644 index 00000000000..b612165fa19 --- /dev/null +++ b/homeassistant/components/media_player/group.py @@ -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) diff --git a/homeassistant/components/person/group.py b/homeassistant/components/person/group.py new file mode 100644 index 00000000000..07ec2cfe985 --- /dev/null +++ b/homeassistant/components/person/group.py @@ -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) diff --git a/homeassistant/components/remote/group.py b/homeassistant/components/remote/group.py new file mode 100644 index 00000000000..1636054663d --- /dev/null +++ b/homeassistant/components/remote/group.py @@ -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) diff --git a/homeassistant/components/sensor/group.py b/homeassistant/components/sensor/group.py new file mode 100644 index 00000000000..4741f8a3b54 --- /dev/null +++ b/homeassistant/components/sensor/group.py @@ -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() diff --git a/homeassistant/components/switch/group.py b/homeassistant/components/switch/group.py new file mode 100644 index 00000000000..1636054663d --- /dev/null +++ b/homeassistant/components/switch/group.py @@ -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) diff --git a/homeassistant/components/vacuum/group.py b/homeassistant/components/vacuum/group.py new file mode 100644 index 00000000000..0219ecdf795 --- /dev/null +++ b/homeassistant/components/vacuum/group.py @@ -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 + ) diff --git a/homeassistant/components/water_heater/group.py b/homeassistant/components/water_heater/group.py new file mode 100644 index 00000000000..f4ec0ecbc26 --- /dev/null +++ b/homeassistant/components/water_heater/group.py @@ -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, + ) diff --git a/homeassistant/components/weather/group.py b/homeassistant/components/weather/group.py new file mode 100644 index 00000000000..4741f8a3b54 --- /dev/null +++ b/homeassistant/components/weather/group.py @@ -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() diff --git a/tests/components/device_sun_light_trigger/test_init.py b/tests/components/device_sun_light_trigger/test_init.py index 19715577e2a..4879be9d18c 100644 --- a/tests/components/device_sun_light_trigger/test_init.py +++ b/tests/components/device_sun_light_trigger/test_init.py @@ -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( diff --git a/tests/components/group/test_init.py b/tests/components/group/test_init.py index 9684c107bb7..b0aba957494 100644 --- a/tests/components/group/test_init.py +++ b/tests/components/group/test_init.py @@ -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" diff --git a/tests/helpers/test_service.py b/tests/helpers/test_service.py index 1a0cdb79bbc..929df2a32e0 100644 --- a/tests/helpers/test_service.py +++ b/tests/helpers/test_service.py @@ -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"] ) diff --git a/tests/helpers/test_template.py b/tests/helpers/test_template.py index e69b78bbb36..20401f6fbdf 100644 --- a/tests/helpers/test_template.py +++ b/tests/helpers/test_template.py @@ -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(