Compare commits

...

17 Commits

Author SHA1 Message Date
G Johansson 7659eba376 multiple domains 2026-06-07 19:11:14 +00:00
G Johansson 2f083190fc Fix comments 2026-06-07 19:05:15 +00:00
G Johansson 640ec2bf34 Mod binary test 2026-06-07 18:14:23 +00:00
G Johansson fb24beccfc Migrate test 2026-06-07 18:08:39 +00:00
G Johansson 690d73b97e Fix light test 2026-06-07 18:01:31 +00:00
G Johansson 510485b711 group member 2026-06-07 18:01:19 +00:00
G Johansson 3b15efab36 entity_ids is a list 2026-06-07 18:00:33 +00:00
G Johansson 3a4d9ba425 Mods 2026-06-07 11:13:02 +00:00
G Johansson 6276525780 Fix group member 2026-06-07 11:09:21 +00:00
G Johansson ee9af3d1d3 Fix event 2026-06-07 11:01:07 +00:00
G Johansson 07c4fbf7c3 prop 2026-06-07 10:47:15 +00:00
G Johansson 46043165f1 Fix group members 2026-06-07 10:40:03 +00:00
G Johansson 158c0211f7 Fix migration 2026-06-07 10:31:29 +00:00
G Johansson 9160f2b42d Mod tests 2026-06-07 10:29:10 +00:00
G Johansson b30d702d3f entities 2026-06-07 10:28:57 +00:00
G Johansson fdaa3175fb Hide members 2026-06-07 10:28:00 +00:00
G Johansson 9e541fc872 Migrate group helper to use target selector 2026-06-06 20:14:19 +00:00
27 changed files with 535 additions and 303 deletions
+27 -1
View File
@@ -13,6 +13,7 @@ from homeassistant.const import (
ATTR_ICON,
ATTR_NAME,
CONF_ENTITIES,
CONF_ENTITY_ID,
CONF_ICON,
CONF_NAME,
SERVICE_RELOAD,
@@ -140,6 +141,30 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
return True
async def async_migrate_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Migrate a config entry."""
if entry.version > 2:
# This means the user has downgraded from a future version
return False
if entry.version == 1:
# Migrate Entity selector to Target selector
new_options = dict(entry.options)
current_entities = new_options[CONF_ENTITIES]
new_options[CONF_ENTITIES] = {CONF_ENTITY_ID: current_entities}
_LOGGER.debug(
"Migrating from version 1 to version 2: %s -> %s",
entry.options,
new_options,
)
hass.config_entries.async_update_entry(entry, version=2, options=new_options)
return True
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Unload a config entry."""
return await hass.config_entries.async_unload_platforms(
@@ -155,7 +180,8 @@ async def async_remove_entry(hass: HomeAssistant, entry: ConfigEntry) -> None:
if not entry.options[CONF_HIDE_MEMBERS]:
return
for member in entry.options[CONF_ENTITIES]:
entity_ids = entry.options[CONF_ENTITIES].get(CONF_ENTITY_ID, [])
for member in entity_ids:
if not (entity_id := er.async_resolve_entity_id(registry, member)):
continue
if (entity_entry := registry.async_get(entity_id)) is None:
+12 -10
View File
@@ -13,7 +13,6 @@ from homeassistant.components.binary_sensor import (
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (
ATTR_ENTITY_ID,
CONF_DEVICE_CLASS,
CONF_ENTITIES,
CONF_NAME,
@@ -55,13 +54,14 @@ async def async_setup_platform(
discovery_info: DiscoveryInfoType | None = None,
) -> None:
"""Set up the Binary Sensor Group platform."""
entities = {"entity_id": config[CONF_ENTITIES]}
async_add_entities(
[
BinarySensorGroup(
config.get(CONF_UNIQUE_ID),
config[CONF_NAME],
config.get(CONF_DEVICE_CLASS),
config[CONF_ENTITIES],
entities,
config.get(CONF_ALL),
)
]
@@ -74,16 +74,18 @@ async def async_setup_entry(
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Initialize Binary Sensor Group config entry."""
registry = er.async_get(hass)
entities = er.async_validate_entity_ids(
registry, config_entry.options[CONF_ENTITIES]
)
target_config = dict(config_entry.options[CONF_ENTITIES])
entity_ids = target_config.get("entity_id", [])
if entity_ids:
registry = er.async_get(hass)
entities = er.async_validate_entity_ids(registry, entity_ids)
target_config["entity_id"] = entities
mode = config_entry.options[CONF_ALL]
async_add_entities(
[
BinarySensorGroup(
config_entry.entry_id, config_entry.title, None, entities, mode
config_entry.entry_id, config_entry.title, None, target_config, mode
)
]
)
@@ -113,14 +115,14 @@ class BinarySensorGroup(GroupEntity, BinarySensorEntity):
unique_id: str | None,
name: str,
device_class: BinarySensorDeviceClass | None,
entity_ids: list[str],
target_config: dict[str, Any],
mode: bool | None,
) -> None:
"""Initialize a BinarySensorGroup entity."""
super().__init__()
self._entity_ids = entity_ids
self._target_config = target_config
self._domains = [BINARY_SENSOR_DOMAIN]
self._attr_name = name
self._attr_extra_state_attributes = {ATTR_ENTITY_ID: entity_ids}
self._attr_unique_id = unique_id
self._device_class = device_class
self.mode = any
+13 -9
View File
@@ -49,12 +49,13 @@ async def async_setup_platform(
__: DiscoveryInfoType | None = None,
) -> None:
"""Set up the button group platform."""
entities = {"entity_id": config[CONF_ENTITIES]}
async_add_entities(
[
ButtonGroup(
config.get(CONF_UNIQUE_ID),
config[CONF_NAME],
config[CONF_ENTITIES],
entities,
)
]
)
@@ -66,16 +67,18 @@ async def async_setup_entry(
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Initialize button group config entry."""
registry = er.async_get(hass)
entities = er.async_validate_entity_ids(
registry, config_entry.options[CONF_ENTITIES]
)
target_config = dict(config_entry.options[CONF_ENTITIES])
entity_ids = target_config.get("entity_id", [])
if entity_ids:
registry = er.async_get(hass)
entities = er.async_validate_entity_ids(registry, entity_ids)
target_config["entity_id"] = entities
async_add_entities(
[
ButtonGroup(
config_entry.entry_id,
config_entry.title,
entities,
target_config,
)
]
)
@@ -103,12 +106,13 @@ class ButtonGroup(GroupEntity, ButtonEntity):
self,
unique_id: str | None,
name: str,
entity_ids: list[str],
target_config: dict[str, Any],
) -> None:
"""Initialize a button group."""
self._entity_ids = entity_ids
super().__init__()
self._target_config = target_config
self._domains = [BUTTON_DOMAIN]
self._attr_name = name
self._attr_extra_state_attributes = {ATTR_ENTITY_ID: entity_ids}
self._attr_unique_id = unique_id
async def async_press(self) -> None:
+17 -20
View File
@@ -7,7 +7,7 @@ from typing import Any, cast
import voluptuous as vol
from homeassistant.components import websocket_api
from homeassistant.const import CONF_ENTITIES, CONF_TYPE
from homeassistant.const import CONF_ENTITIES, CONF_ENTITY_ID, CONF_TYPE
from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import entity_registry as er, selector
@@ -16,8 +16,6 @@ from homeassistant.helpers.schema_config_entry_flow import (
SchemaConfigFlowHandler,
SchemaFlowFormStep,
SchemaFlowMenuStep,
SchemaOptionsFlowHandler,
entity_selector_without_own_entities,
)
from .binary_sensor import CONF_ALL, async_create_preview_binary_sensor
@@ -53,20 +51,14 @@ async def basic_group_options_schema(
domain: str | list[str], handler: SchemaCommonFlowHandler | None
) -> vol.Schema:
"""Generate options schema."""
entity_selector: selector.Selector[Any] | vol.Schema
if handler is None:
entity_selector = selector.selector(
{"entity": {"domain": domain, "multiple": True, "reorder": True}}
)
else:
entity_selector = entity_selector_without_own_entities(
cast(SchemaOptionsFlowHandler, handler.parent_handler),
selector.EntitySelectorConfig(domain=domain, multiple=True, reorder=True),
)
return vol.Schema(
{
vol.Required(CONF_ENTITIES): entity_selector,
vol.Required(CONF_ENTITIES): selector.TargetSelector(
selector.TargetSelectorConfig(
entity=selector.EntityFilterSelectorConfig(domain=domain)
)
),
vol.Required(CONF_HIDE_MEMBERS, default=False): selector.BooleanSelector(),
}
)
@@ -77,10 +69,10 @@ def basic_group_config_schema(domain: str | list[str]) -> vol.Schema:
return vol.Schema(
{
vol.Required("name"): selector.TextSelector(),
vol.Required(CONF_ENTITIES): selector.EntitySelector(
selector.EntitySelectorConfig(
domain=domain, multiple=True, reorder=True
),
vol.Required(CONF_ENTITIES): selector.TargetSelector(
selector.TargetSelectorConfig(
entity=selector.EntityFilterSelectorConfig(domain=domain)
)
),
vol.Required(CONF_HIDE_MEMBERS, default=False): selector.BooleanSelector(),
}
@@ -338,6 +330,8 @@ CREATE_PREVIEW_ENTITY: dict[
class GroupConfigFlowHandler(SchemaConfigFlowHandler, domain=DOMAIN):
"""Handle a config or options flow for groups."""
VERSION = 2
config_flow = CONFIG_FLOW
options_flow = OPTIONS_FLOW
options_flow_reloads = True
@@ -387,11 +381,14 @@ class GroupConfigFlowHandler(SchemaConfigFlowHandler, domain=DOMAIN):
def _async_hide_members(
hass: HomeAssistant, members: list[str], hidden_by: er.RegistryEntryHider | None
hass: HomeAssistant,
members: dict[str, Any],
hidden_by: er.RegistryEntryHider | None,
) -> None:
"""Hide or unhide group members."""
registry = er.async_get(hass)
for member in members:
entity_ids = members.get(CONF_ENTITY_ID, [])
for member in entity_ids:
if not (entity_id := er.async_resolve_entity_id(registry, member)):
continue
if entity_id not in registry.entities:
+15 -14
View File
@@ -69,12 +69,9 @@ async def async_setup_platform(
discovery_info: DiscoveryInfoType | None = None,
) -> None:
"""Set up the Cover Group platform."""
entities = {"entity_id": config[CONF_ENTITIES]}
async_add_entities(
[
CoverGroup(
config.get(CONF_UNIQUE_ID), config[CONF_NAME], config[CONF_ENTITIES]
)
]
[CoverGroup(config.get(CONF_UNIQUE_ID), config[CONF_NAME], entities)]
)
@@ -84,13 +81,14 @@ async def async_setup_entry(
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Initialize Cover Group config entry."""
registry = er.async_get(hass)
entities = er.async_validate_entity_ids(
registry, config_entry.options[CONF_ENTITIES]
)
target_config = dict(config_entry.options[CONF_ENTITIES])
entity_ids = target_config.get("entity_id", [])
if entity_ids:
registry = er.async_get(hass)
entities = er.async_validate_entity_ids(registry, entity_ids)
target_config["entity_id"] = entities
async_add_entities(
[CoverGroup(config_entry.entry_id, config_entry.title, entities)]
[CoverGroup(config_entry.entry_id, config_entry.title, target_config)]
)
@@ -115,9 +113,13 @@ class CoverGroup(GroupEntity, CoverEntity):
_attr_is_closing: bool | None = False
_attr_current_cover_position: int | None = 100
def __init__(self, unique_id: str | None, name: str, entities: list[str]) -> None:
def __init__(
self, unique_id: str | None, name: str, target_config: dict[str, Any]
) -> None:
"""Initialize a CoverGroup entity."""
self._entity_ids = entities
super().__init__()
self._target_config = target_config
self._domains = [COVER_DOMAIN]
self._covers: dict[str, set[str]] = {
KEY_OPEN_CLOSE: set(),
KEY_STOP: set(),
@@ -130,7 +132,6 @@ class CoverGroup(GroupEntity, CoverEntity):
}
self._attr_name = name
self._attr_extra_state_attributes = {ATTR_ENTITY_ID: entities}
self._attr_unique_id = unique_id
@callback
+87 -3
View File
@@ -25,6 +25,12 @@ from homeassistant.helpers import start
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.target import (
TargetSelection,
TargetStateChangedData,
async_extract_referenced_entity_ids,
async_track_target_selector_state_change_event,
)
from .const import ATTR_AUTO, ATTR_ORDER, DATA_COMPONENT, DOMAIN, GROUP_ORDER, REG_KEY
from .registry import GroupIntegrationRegistry, SingleStateType
@@ -43,6 +49,8 @@ class GroupEntity(Entity):
_attr_should_poll = False
_entity_ids: list[str]
_target_config: dict[str, Any]
_domains: list[str]
@callback
def async_start_preview(
@@ -51,6 +59,7 @@ class GroupEntity(Entity):
) -> CALLBACK_TYPE:
"""Render a preview."""
self.update_entities(False)
for entity_id in self._entity_ids:
if (state := self.hass.states.get(entity_id)) is None:
continue
@@ -74,8 +83,51 @@ class GroupEntity(Entity):
self.hass, self._entity_ids, async_state_changed_listener
)
@callback
def filter_entities_by_domain(self, entity_ids: set[str]) -> set[str]:
"""Filter entities by domain."""
return {
entity_id
for entity_id in entity_ids
if split_entity_id(entity_id)[0] in self._domains
and entity_id != self.entity_id
}
@callback
def update_entities(self, update_group_members: bool = True) -> None:
"""Update the entities in the group."""
selected = async_extract_referenced_entity_ids(
self.hass,
TargetSelection(self._target_config),
expand_group=True,
primary_entities_only=False,
)
self._entity_ids = []
# Prepend entities from config to ensure order for explicitly configured entities
if entity_list := self._target_config.get("entity_id"):
self._entity_ids = list(entity_list)
self._entity_ids.extend(
[
entity
for entity in self.filter_entities_by_domain(
selected.referenced | selected.indirectly_referenced
)
if entity not in self._entity_ids
]
)
self._attr_extra_state_attributes = {ATTR_ENTITY_ID: self._entity_ids}
if update_group_members:
self.update_group_member(self._entity_ids)
def update_group_member(self, entities: list[str]) -> None:
"""Update the group member."""
async def async_added_to_hass(self) -> None:
"""Register listeners."""
self.update_entities()
for entity_id in self._entity_ids:
if (state := self.hass.states.get(entity_id)) is None:
continue
@@ -83,18 +135,50 @@ class GroupEntity(Entity):
@callback
def async_state_changed_listener(
event: Event[EventStateChangedData],
target_state_change_data: TargetStateChangedData,
) -> None:
"""Handle child updates."""
event = target_state_change_data.state_change_event
self.async_set_context(event.context)
self.async_update_supported_features(
event.data["entity_id"], event.data["new_state"]
)
self.async_defer_or_update_ha_state()
@callback
def async_update_entities(added: set[str], removed: set[str]) -> None:
"""Handle entity changes."""
for entity_id in added:
if entity_id not in self._entity_ids:
self._entity_ids.append(entity_id)
for entity_id in removed:
if entity_id in self._entity_ids:
self._entity_ids.remove(entity_id)
# Ensure the group does not include itself as member
if self.entity_id in self._entity_ids:
self._entity_ids.remove(self.entity_id)
self._attr_extra_state_attributes = {
ATTR_ENTITY_ID: sorted(self._entity_ids)
}
for entity_id in self._entity_ids:
if (state := self.hass.states.get(entity_id)) is None:
continue
self.async_update_supported_features(entity_id, state)
self.update_group_member(self._entity_ids)
self.async_defer_or_update_ha_state()
self.async_on_remove(
async_track_state_change_event(
self.hass, self._entity_ids, async_state_changed_listener
async_track_target_selector_state_change_event(
self.hass,
self._target_config,
async_state_changed_listener,
self.filter_entities_by_domain,
on_entities_update=async_update_entities,
primary_entities_only=False,
)
)
self.async_on_remove(start.async_at_start(self.hass, self._update_at_start))
+35 -24
View File
@@ -15,7 +15,6 @@ from homeassistant.components.event import (
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (
ATTR_DEVICE_CLASS,
ATTR_ENTITY_ID,
ATTR_FRIENDLY_NAME,
CONF_ENTITIES,
CONF_NAME,
@@ -23,13 +22,16 @@ from homeassistant.const import (
STATE_UNAVAILABLE,
STATE_UNKNOWN,
)
from homeassistant.core import Event, EventStateChangedData, HomeAssistant, callback
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers import config_validation as cv, entity_registry as er
from homeassistant.helpers.entity_platform import (
AddConfigEntryEntitiesCallback,
AddEntitiesCallback,
)
from homeassistant.helpers.event import async_track_state_change_event
from homeassistant.helpers.target import (
TargetStateChangedData,
async_track_target_selector_state_change_event,
)
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
from .entity import GroupEntity
@@ -55,12 +57,13 @@ async def async_setup_platform(
__: DiscoveryInfoType | None = None,
) -> None:
"""Set up the event group platform."""
entities = {"entity_id": config[CONF_ENTITIES]}
async_add_entities(
[
EventGroup(
config.get(CONF_UNIQUE_ID),
config[CONF_NAME],
config[CONF_ENTITIES],
entities,
)
]
)
@@ -72,16 +75,18 @@ async def async_setup_entry(
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Initialize event group config entry."""
registry = er.async_get(hass)
entities = er.async_validate_entity_ids(
registry, config_entry.options[CONF_ENTITIES]
)
target_config = dict(config_entry.options[CONF_ENTITIES])
entity_ids = target_config.get("entity_id", [])
if entity_ids:
registry = er.async_get(hass)
entities = er.async_validate_entity_ids(registry, entity_ids)
target_config["entity_id"] = entities
async_add_entities(
[
EventGroup(
config_entry.entry_id,
config_entry.title,
entities,
target_config,
)
]
)
@@ -109,12 +114,13 @@ class EventGroup(GroupEntity, EventEntity):
self,
unique_id: str | None,
name: str,
entity_ids: list[str],
target_config: dict[str, Any],
) -> None:
"""Initialize an event group."""
self._entity_ids = entity_ids
super().__init__()
self._target_config = target_config
self._domains = [EVENT_DOMAIN]
self._attr_name = name
self._attr_extra_state_attributes = {ATTR_ENTITY_ID: entity_ids}
self._attr_unique_id = unique_id
self._attr_event_types = []
@@ -123,23 +129,26 @@ class EventGroup(GroupEntity, EventEntity):
@callback
def async_state_changed_listener(
event: Event[EventStateChangedData],
target_state_change_data: TargetStateChangedData,
) -> None:
"""Handle child updates."""
if not self.hass.is_running:
return
self.async_set_context(event.context)
# Update all properties of the group
self.async_update_group_state()
# Re-fire if one of the members fires an event, but only
# if the original state was not unavailable or unknown.
if (
(old_state := event.data["old_state"])
(
old_state := target_state_change_data.state_change_event.data[
"old_state"
]
)
and old_state.state not in (STATE_UNAVAILABLE, STATE_UNKNOWN)
and (new_state := event.data["new_state"])
and (
new_state := target_state_change_data.state_change_event.data[
"new_state"
]
)
and new_state.state not in (STATE_UNAVAILABLE, STATE_UNKNOWN)
and (event_type := new_state.attributes.get(ATTR_EVENT_TYPE))
):
@@ -155,11 +164,13 @@ class EventGroup(GroupEntity, EventEntity):
# Fire the group event
self._trigger_event(event_type, event_attributes)
self.async_write_ha_state()
self.async_on_remove(
async_track_state_change_event(
self.hass, self._entity_ids, async_state_changed_listener
async_track_target_selector_state_change_event(
self.hass,
self._target_config,
async_state_changed_listener,
self.filter_entities_by_domain,
primary_entities_only=False,
)
)
+16 -9
View File
@@ -75,8 +75,9 @@ async def async_setup_platform(
discovery_info: DiscoveryInfoType | None = None,
) -> None:
"""Set up the Fan Group platform."""
entities = {"entity_id": config[CONF_ENTITIES]}
async_add_entities(
[FanGroup(config.get(CONF_UNIQUE_ID), config[CONF_NAME], config[CONF_ENTITIES])]
[FanGroup(config.get(CONF_UNIQUE_ID), config[CONF_NAME], entities)]
)
@@ -86,13 +87,16 @@ async def async_setup_entry(
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Initialize Fan Group config entry."""
registry = er.async_get(hass)
entities = er.async_validate_entity_ids(
registry, config_entry.options[CONF_ENTITIES]
target_config = dict(config_entry.options[CONF_ENTITIES])
entity_ids = target_config.get("entity_id", [])
if entity_ids:
registry = er.async_get(hass)
entities = er.async_validate_entity_ids(registry, entity_ids)
target_config["entity_id"] = entities
async_add_entities(
[FanGroup(config_entry.entry_id, config_entry.title, target_config)]
)
async_add_entities([FanGroup(config_entry.entry_id, config_entry.title, entities)])
@callback
def async_create_preview_fan(
@@ -111,9 +115,13 @@ class FanGroup(GroupEntity, FanEntity):
_attr_available: bool = False
def __init__(self, unique_id: str | None, name: str, entities: list[str]) -> None:
def __init__(
self, unique_id: str | None, name: str, target_config: dict[str, Any]
) -> None:
"""Initialize a FanGroup entity."""
self._entity_ids = entities
super().__init__()
self._target_config = target_config
self._domains = [FAN_DOMAIN]
self._fans: dict[int, set[str]] = {flag: set() for flag in SUPPORTED_FLAGS}
self._percentage = None
self._oscillating = None
@@ -121,7 +129,6 @@ class FanGroup(GroupEntity, FanEntity):
self._speed_count = 100
self._is_on: bool | None = False
self._attr_name = name
self._attr_extra_state_attributes = {ATTR_ENTITY_ID: entities}
self._attr_unique_id = unique_id
@property
+18 -10
View File
@@ -25,6 +25,7 @@ from homeassistant.components.light import (
ATTR_TRANSITION,
ATTR_WHITE,
ATTR_XY_COLOR,
DOMAIN as LIGHT_DOMAIN,
PLATFORM_SCHEMA as LIGHT_PLATFORM_SCHEMA,
ColorMode,
LightEntity,
@@ -84,12 +85,13 @@ async def async_setup_platform(
discovery_info: DiscoveryInfoType | None = None,
) -> None:
"""Initialize light.group platform."""
entities = {"entity_id": config[CONF_ENTITIES]}
async_add_entities(
[
LightGroup(
config.get(CONF_UNIQUE_ID),
config[CONF_NAME],
config[CONF_ENTITIES],
entities,
config.get(CONF_ALL),
)
]
@@ -102,14 +104,16 @@ async def async_setup_entry(
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Initialize Light Group config entry."""
registry = er.async_get(hass)
entities = er.async_validate_entity_ids(
registry, config_entry.options[CONF_ENTITIES]
)
target_config = dict(config_entry.options[CONF_ENTITIES])
entity_ids = target_config.get("entity_id", [])
if entity_ids:
registry = er.async_get(hass)
entities = er.async_validate_entity_ids(registry, entity_ids)
target_config["entity_id"] = entities
mode = config_entry.options.get(CONF_ALL, False)
async_add_entities(
[LightGroup(config_entry.entry_id, config_entry.title, entities, mode)]
[LightGroup(config_entry.entry_id, config_entry.title, target_config, mode)]
)
@@ -153,13 +157,17 @@ class LightGroup(GroupEntity, LightEntity):
_attr_should_poll = False
def __init__(
self, unique_id: str | None, name: str, entity_ids: list[str], mode: bool | None
self,
unique_id: str | None,
name: str,
target_config: dict[str, Any],
mode: bool | None,
) -> None:
"""Initialize a light group."""
self._entity_ids = entity_ids
super().__init__()
self._target_config = target_config
self._domains = [LIGHT_DOMAIN]
self._attr_name = name
self._attr_extra_state_attributes = {ATTR_ENTITY_ID: entity_ids}
self._attr_unique_id = unique_id
self.mode = any
if mode:
+19 -15
View File
@@ -14,7 +14,6 @@ from homeassistant.components.lock import (
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (
ATTR_ENTITY_ID,
CONF_ENTITIES,
CONF_NAME,
CONF_UNIQUE_ID,
@@ -55,12 +54,13 @@ async def async_setup_platform(
discovery_info: DiscoveryInfoType | None = None,
) -> None:
"""Set up the Lock Group platform."""
entities = {"entity_id": config[CONF_ENTITIES]}
async_add_entities(
[
LockGroup(
config.get(CONF_UNIQUE_ID),
config[CONF_NAME],
config[CONF_ENTITIES],
entities,
)
]
)
@@ -72,16 +72,18 @@ async def async_setup_entry(
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Initialize Lock Group config entry."""
registry = er.async_get(hass)
entities = er.async_validate_entity_ids(
registry, config_entry.options[CONF_ENTITIES]
)
target_config = dict(config_entry.options[CONF_ENTITIES])
entity_ids = target_config.get("entity_id", [])
if entity_ids:
registry = er.async_get(hass)
entities = er.async_validate_entity_ids(registry, entity_ids)
target_config["entity_id"] = entities
async_add_entities(
[
LockGroup(
config_entry.entry_id,
config_entry.title,
entities,
target_config,
)
]
)
@@ -104,22 +106,24 @@ class LockGroup(GroupEntity, LockEntity):
_attr_available = False
_attr_should_poll = False
group: GenericGroup
def __init__(
self,
unique_id: str | None,
name: str,
entity_ids: list[str],
self, unique_id: str | None, name: str, target_config: dict[str, Any]
) -> None:
"""Initialize a lock group."""
self._entity_ids = entity_ids
self.group = GenericGroup(self, entity_ids)
super().__init__()
self._target_config = target_config
self._domains = [LOCK_DOMAIN]
self.group = GenericGroup(self, target_config.get("entity_id", []))
self._attr_supported_features = LockEntityFeature.OPEN
self._attr_name = name
self._attr_extra_state_attributes = {ATTR_ENTITY_ID: entity_ids}
self._attr_unique_id = unique_id
def update_group_member(self, entities: list[str]) -> None:
"""Update the group member."""
self.group._member_entity_ids = entities # noqa: SLF001
@callback
def async_update_group_state(self) -> None:
"""Query all members and determine the lock group state."""
+20 -76
View File
@@ -1,6 +1,5 @@
"""Platform allowing several media players to be grouped into one media player."""
from collections.abc import Callable, Mapping
from contextlib import suppress
from typing import Any
@@ -43,22 +42,16 @@ from homeassistant.const import (
STATE_UNAVAILABLE,
STATE_UNKNOWN,
)
from homeassistant.core import (
CALLBACK_TYPE,
Event,
EventStateChangedData,
HomeAssistant,
State,
callback,
)
from homeassistant.core import HomeAssistant, State, callback
from homeassistant.helpers import config_validation as cv, entity_registry as er
from homeassistant.helpers.entity_platform import (
AddConfigEntryEntitiesCallback,
AddEntitiesCallback,
)
from homeassistant.helpers.event import async_track_state_change_event
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
from .entity import GroupEntity
KEY_ANNOUNCE = "announce"
KEY_CLEAR_PLAYLIST = "clear_playlist"
KEY_ENQUEUE = "enqueue"
@@ -88,12 +81,9 @@ async def async_setup_platform(
discovery_info: DiscoveryInfoType | None = None,
) -> None:
"""Set up the MediaPlayer Group platform."""
entities = {"entity_id": config[CONF_ENTITIES]}
async_add_entities(
[
MediaPlayerGroup(
config.get(CONF_UNIQUE_ID), config[CONF_NAME], config[CONF_ENTITIES]
)
]
[MediaPlayerGroup(config.get(CONF_UNIQUE_ID), config[CONF_NAME], entities)]
)
@@ -103,13 +93,14 @@ async def async_setup_entry(
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Initialize MediaPlayer Group config entry."""
registry = er.async_get(hass)
entities = er.async_validate_entity_ids(
registry, config_entry.options[CONF_ENTITIES]
)
target_config = dict(config_entry.options[CONF_ENTITIES])
entity_ids = target_config.get("entity_id", [])
if entity_ids:
registry = er.async_get(hass)
entities = er.async_validate_entity_ids(registry, entity_ids)
target_config["entity_id"] = entities
async_add_entities(
[MediaPlayerGroup(config_entry.entry_id, config_entry.title, entities)]
[MediaPlayerGroup(config_entry.entry_id, config_entry.title, target_config)]
)
@@ -125,20 +116,20 @@ def async_create_preview_media_player(
)
class MediaPlayerGroup(MediaPlayerEntity):
class MediaPlayerGroup(GroupEntity, MediaPlayerEntity):
"""Representation of a Media Group."""
_unrecorded_attributes = frozenset({ATTR_ENTITY_ID})
_attr_available: bool = False
_attr_should_poll = False
def __init__(self, unique_id: str | None, name: str, entities: list[str]) -> None:
def __init__(
self, unique_id: str | None, name: str, target_config: dict[str, Any]
) -> None:
"""Initialize a Media Group entity."""
super().__init__()
self._target_config = target_config
self._domains = [MEDIA_PLAYER_DOMAIN]
self._name = name
self._attr_unique_id = unique_id
self._entities = entities
self._features: dict[str, set[str]] = {
KEY_ANNOUNCE: set(),
KEY_CLEAR_PLAYLIST: set(),
@@ -152,16 +143,6 @@ class MediaPlayerGroup(MediaPlayerEntity):
KEY_VOLUME: set(),
}
@callback
def async_on_state_change(self, event: Event[EventStateChangedData]) -> None:
"""Update supported features and state when a new state is received."""
self.async_set_context(event.context)
self.async_update_supported_features(
event.data["entity_id"], event.data["new_state"]
)
self.async_update_group_state()
self.async_write_ha_state()
@callback
def async_update_supported_features(
self,
@@ -229,48 +210,11 @@ class MediaPlayerGroup(MediaPlayerEntity):
else:
self._features[KEY_ENQUEUE].discard(entity_id)
@callback
def async_start_preview(
self,
preview_callback: Callable[[str, Mapping[str, Any]], None],
) -> CALLBACK_TYPE:
"""Render a preview."""
@callback
def async_state_changed_listener(
event: Event[EventStateChangedData] | None,
) -> None:
"""Handle child updates."""
self.async_update_group_state()
calculated_state = self._async_calculate_state()
preview_callback(calculated_state.state, calculated_state.attributes)
async_state_changed_listener(None)
return async_track_state_change_event(
self.hass, self._entities, async_state_changed_listener
)
async def async_added_to_hass(self) -> None:
"""Register listeners."""
for entity_id in self._entities:
new_state = self.hass.states.get(entity_id)
self.async_update_supported_features(entity_id, new_state)
async_track_state_change_event(
self.hass, self._entities, self.async_on_state_change
)
self.async_update_group_state()
self.async_write_ha_state()
@property
def name(self) -> str:
"""Return the name of the entity."""
return self._name
@property
def extra_state_attributes(self) -> Mapping[str, Any]:
"""Return the state attributes for the media group."""
return {ATTR_ENTITY_ID: self._entities}
async def async_clear_playlist(self) -> None:
"""Clear players playlist."""
data = {ATTR_ENTITY_ID: self._features[KEY_CLEAR_PLAYLIST]}
@@ -440,7 +384,7 @@ class MediaPlayerGroup(MediaPlayerEntity):
"""Query all members and determine the media group state."""
states = [
state.state
for entity_id in self._entities
for entity_id in self._entity_ids
if (state := self.hass.states.get(entity_id)) is not None
]
+11 -12
View File
@@ -132,13 +132,14 @@ async def async_setup_entry(
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Initialize Notify Group config entry."""
registry = er.async_get(hass)
entities = er.async_validate_entity_ids(
registry, config_entry.options[CONF_ENTITIES]
)
target_config = dict(config_entry.options[CONF_ENTITIES])
entity_ids = target_config.get("entity_id", [])
if entity_ids:
registry = er.async_get(hass)
entities = er.async_validate_entity_ids(registry, entity_ids)
target_config["entity_id"] = entities
async_add_entities(
[NotifyGroup(config_entry.entry_id, config_entry.title, entities)]
[NotifyGroup(config_entry.entry_id, config_entry.title, target_config)]
)
@@ -160,15 +161,13 @@ class NotifyGroup(GroupEntity, NotifyEntity):
_attr_available: bool = False
def __init__(
self,
unique_id: str | None,
name: str,
entity_ids: list[str],
self, unique_id: str | None, name: str, target_config: dict[str, Any]
) -> None:
"""Initialize a NotifyGroup."""
self._entity_ids = entity_ids
super().__init__()
self._target_config = target_config
self._domains = [NOTIFY_DOMAIN]
self._attr_name = name
self._attr_extra_state_attributes = {ATTR_ENTITY_ID: entity_ids}
self._attr_unique_id = unique_id
async def async_send_message(self, message: str, title: str | None = None) -> None:
+17 -10
View File
@@ -27,7 +27,6 @@ from homeassistant.components.sensor import (
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (
ATTR_ENTITY_ID,
CONF_DEVICE_CLASS,
CONF_ENTITIES,
CONF_NAME,
@@ -117,13 +116,14 @@ async def async_setup_platform(
discovery_info: DiscoveryInfoType | None = None,
) -> None:
"""Set up the Switch Group platform."""
entities = {"entity_id": config[CONF_ENTITIES]}
async_add_entities(
[
SensorGroup(
hass,
config.get(CONF_UNIQUE_ID),
config[CONF_NAME],
config[CONF_ENTITIES],
entities,
config[CONF_IGNORE_NON_NUMERIC],
config[CONF_TYPE],
config.get(CONF_UNIT_OF_MEASUREMENT),
@@ -140,17 +140,19 @@ async def async_setup_entry(
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Initialize Switch Group config entry."""
registry = er.async_get(hass)
entities = er.async_validate_entity_ids(
registry, config_entry.options[CONF_ENTITIES]
)
target_config = dict(config_entry.options[CONF_ENTITIES])
entity_ids = target_config.get("entity_id", [])
if entity_ids:
registry = er.async_get(hass)
entities = er.async_validate_entity_ids(registry, entity_ids)
target_config["entity_id"] = entities
async_add_entities(
[
SensorGroup(
hass,
config_entry.entry_id,
config_entry.title,
entities,
target_config,
config_entry.options.get(CONF_IGNORE_NON_NUMERIC, True),
config_entry.options[CONF_TYPE],
None,
@@ -345,7 +347,7 @@ class SensorGroup(GroupEntity, SensorEntity):
hass: HomeAssistant,
unique_id: str | None,
name: str,
entity_ids: list[str],
target_config: dict[str, Any],
ignore_non_numeric: bool,
sensor_type: str,
unit_of_measurement: str | None,
@@ -353,8 +355,10 @@ class SensorGroup(GroupEntity, SensorEntity):
device_class: SensorDeviceClass | None,
) -> None:
"""Initialize a sensor group."""
super().__init__()
self._target_config = target_config
self._domains = [SENSOR_DOMAIN, NUMBER_DOMAIN, INPUT_NUMBER_DOMAIN]
self.hass = hass
self._entity_ids = entity_ids
self._sensor_type = sensor_type
self._configured_state_class = state_class
self._configured_device_class = device_class
@@ -482,7 +486,10 @@ class SensorGroup(GroupEntity, SensorEntity):
@property
def extra_state_attributes(self) -> dict[str, Any]:
"""Return the state attributes of the sensor."""
return {ATTR_ENTITY_ID: self._entity_ids, **self._extra_state_attribute}
return {
**self._extra_state_attribute,
**self._attr_extra_state_attributes,
}
@property
def icon(self) -> str | None:
+13 -10
View File
@@ -57,12 +57,13 @@ async def async_setup_platform(
discovery_info: DiscoveryInfoType | None = None,
) -> None:
"""Set up the Switch Group platform."""
entities = {"entity_id": config[CONF_ENTITIES]}
async_add_entities(
[
SwitchGroup(
config.get(CONF_UNIQUE_ID),
config[CONF_NAME],
config[CONF_ENTITIES],
entities,
config.get(CONF_ALL, False),
)
]
@@ -75,16 +76,18 @@ async def async_setup_entry(
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Initialize Switch Group config entry."""
registry = er.async_get(hass)
entities = er.async_validate_entity_ids(
registry, config_entry.options[CONF_ENTITIES]
)
target_config = dict(config_entry.options[CONF_ENTITIES])
entity_ids = target_config.get("entity_id", [])
if entity_ids:
registry = er.async_get(hass)
entities = er.async_validate_entity_ids(registry, entity_ids)
target_config["entity_id"] = entities
async_add_entities(
[
SwitchGroup(
config_entry.entry_id,
config_entry.title,
entities,
target_config,
config_entry.options.get(CONF_ALL),
)
]
@@ -114,14 +117,14 @@ class SwitchGroup(GroupEntity, SwitchEntity):
self,
unique_id: str | None,
name: str,
entity_ids: list[str],
target_config: dict[str, Any],
mode: bool | None,
) -> None:
"""Initialize a switch group."""
self._entity_ids = entity_ids
super().__init__()
self._target_config = target_config
self._domains = [SWITCH_DOMAIN]
self._attr_name = name
self._attr_extra_state_attributes = {ATTR_ENTITY_ID: entity_ids}
self._attr_unique_id = unique_id
self.mode = any
if mode:
+15 -15
View File
@@ -63,12 +63,9 @@ async def async_setup_platform(
discovery_info: DiscoveryInfoType | None = None,
) -> None:
"""Set up the Valve Group platform."""
entities = {"entity_id": config[CONF_ENTITIES]}
async_add_entities(
[
ValveGroup(
config.get(CONF_UNIQUE_ID), config[CONF_NAME], config[CONF_ENTITIES]
)
]
[ValveGroup(config.get(CONF_UNIQUE_ID), config[CONF_NAME], entities)]
)
@@ -78,13 +75,14 @@ async def async_setup_entry(
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Initialize Valve Group config entry."""
registry = er.async_get(hass)
entities = er.async_validate_entity_ids(
registry, config_entry.options[CONF_ENTITIES]
)
target_config = dict(config_entry.options[CONF_ENTITIES])
entity_ids = target_config.get("entity_id", [])
if entity_ids:
registry = er.async_get(hass)
entities = er.async_validate_entity_ids(registry, entity_ids)
target_config["entity_id"] = entities
async_add_entities(
[ValveGroup(config_entry.entry_id, config_entry.title, entities)]
[ValveGroup(config_entry.entry_id, config_entry.title, target_config)]
)
@@ -110,17 +108,19 @@ class ValveGroup(GroupEntity, ValveEntity):
_attr_is_opening: bool | None = False
_attr_reports_position: bool = False
def __init__(self, unique_id: str | None, name: str, entities: list[str]) -> None:
def __init__(
self, unique_id: str | None, name: str, target_config: dict[str, Any]
) -> None:
"""Initialize a ValveGroup entity."""
self._entity_ids = entities
super().__init__()
self._target_config = target_config
self._domains = [VALVE_DOMAIN]
self._valves: dict[str, set[str]] = {
KEY_OPEN_CLOSE: set(),
KEY_STOP: set(),
KEY_SET_POSITION: set(),
}
self._attr_name = name
self._attr_extra_state_attributes = {ATTR_ENTITY_ID: entities}
self._attr_unique_id = unique_id
@callback
+1 -1
View File
@@ -56,7 +56,7 @@ class GenericGroup(Group):
super().__init__(entity)
self._member_entity_ids = member_entity_ids
@cached_property
@property
def member_entity_ids(self) -> list[str]:
"""Return the list of member entity IDs."""
return self._member_entity_ids
@@ -512,22 +512,3 @@ def wrapped_entity_config_entry_title(
if state:
return state.name or object_id
return object_id
@callback
def entity_selector_without_own_entities(
handler: SchemaOptionsFlowHandler,
entity_selector_config: selector.EntitySelectorConfig,
) -> selector.EntitySelector:
"""Return an entity selector which excludes own entities."""
entity_registry = er.async_get(handler.hass)
entities = er.async_entries_for_config_entry(
entity_registry,
handler.config_entry.entry_id,
)
entity_ids = [ent.entity_id for ent in entities]
final_selector_config = entity_selector_config.copy()
final_selector_config["exclude_entities"] = entity_ids
return selector.EntitySelector(final_selector_config)
+2
View File
@@ -376,6 +376,8 @@ class TargetStateChangeTracker(TargetEntityChangeTracker):
"""Handle the tracked entities."""
previous_entities = self._tracked_entities
self._tracked_entities = tracked_entities
if previous_entities == tracked_entities:
return
if self._on_entities_update is not None:
added = tracked_entities - previous_entities
+95 -2
View File
@@ -10,9 +10,11 @@ from homeassistant.const import (
STATE_UNKNOWN,
)
from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_registry as er
from homeassistant.helpers import entity_registry as er, label_registry as lr
from homeassistant.setup import async_setup_component
from tests.common import MockConfigEntry
async def test_default_state(
hass: HomeAssistant, entity_registry: er.EntityRegistry
@@ -41,8 +43,8 @@ async def test_default_state(
assert state is not None
assert state.state == STATE_ON
assert state.attributes.get(ATTR_ENTITY_ID) == [
"binary_sensor.kitchen",
"binary_sensor.bedroom",
"binary_sensor.kitchen",
]
entry = entity_registry.async_get("binary_sensor.bedroom_group")
@@ -52,6 +54,97 @@ async def test_default_state(
assert entry.original_device_class == "presence"
async def test_multiple_targets(
hass: HomeAssistant,
label_registry: lr.LabelRegistry,
entity_registry: er.EntityRegistry,
) -> None:
"""Test binary sensor from config entry with multiple targets."""
hass.states.async_set("binary_sensor.kitchen", "on")
hass.states.async_set("binary_sensor.bedroom", "on")
group_config_entry = MockConfigEntry(
data={},
domain=DOMAIN,
options={
"entities": {
"area_id": ["bedroom"],
"entity_id": [
"binary_sensor.kitchen",
"binary_sensor.bedroom",
"binary_sensor.not_exist",
],
"label_id": ["test"],
},
"group_type": "binary_sensor",
"name": "Bedroom Group",
"all": False,
},
title="Bedroom Group",
version=2,
)
group_config_entry.add_to_hass(hass)
label_registry.async_create("Test")
entity_registry.async_get_or_create(
"binary_sensor",
"test",
"in_a_label",
suggested_object_id="in_a_label",
config_entry=group_config_entry,
)
entity_registry.async_update_entity("binary_sensor.in_a_label", labels={"test"})
hass.states.async_set("binary_sensor.in_a_label", "on")
assert await hass.config_entries.async_setup(group_config_entry.entry_id)
await hass.async_block_till_done()
state = hass.states.get("binary_sensor.bedroom_group")
assert state is not None
assert state.state == STATE_ON
assert state.attributes.get(ATTR_ENTITY_ID) == [
"binary_sensor.bedroom",
"binary_sensor.in_a_label",
"binary_sensor.kitchen",
"binary_sensor.not_exist",
]
entity_registry.async_get_or_create(
"binary_sensor",
"test",
"added_to_a_label",
suggested_object_id="added_to_a_label",
config_entry=group_config_entry,
)
entity_registry.async_update_entity(
"binary_sensor.added_to_a_label", labels={"test"}
)
hass.states.async_set("binary_sensor.added_to_a_label", "on")
entity_registry.async_get_or_create(
"test",
"test",
"not_to_be_included",
suggested_object_id="not_to_be_included",
config_entry=group_config_entry,
)
entity_registry.async_update_entity("test.not_to_be_included", labels={"test"})
hass.states.async_set("test.not_to_be_included", "on")
await hass.async_block_till_done()
state = hass.states.get("binary_sensor.bedroom_group")
assert state is not None
assert state.state == STATE_ON
assert state.attributes.get(ATTR_ENTITY_ID) == [
"binary_sensor.added_to_a_label",
"binary_sensor.bedroom",
"binary_sensor.in_a_label",
"binary_sensor.kitchen",
"binary_sensor.not_exist",
]
async def test_state_reporting_all(hass: HomeAssistant) -> None:
"""Test the state reporting in 'all' mode.
+35 -26
View File
@@ -98,7 +98,7 @@ async def test_config_flow(
result["flow_id"],
{
"name": "Living Room",
"entities": members,
"entities": {"entity_id": members},
**extra_input,
},
)
@@ -108,7 +108,7 @@ async def test_config_flow(
assert result["title"] == "Living Room"
assert result["data"] == {}
assert result["options"] == {
"entities": members,
"entities": {"entity_id": members},
"group_type": group_type,
"hide_members": False,
"name": "Living Room",
@@ -119,7 +119,7 @@ async def test_config_flow(
config_entry = hass.config_entries.async_entries(DOMAIN)[0]
assert config_entry.data == {}
assert config_entry.options == {
"entities": members,
"entities": {"entity_id": members},
"group_type": group_type,
"hide_members": False,
"name": "Living Room",
@@ -192,7 +192,7 @@ async def test_config_flow_hides_members(
result["flow_id"],
{
"name": "Living Room",
"entities": members,
"entities": {"entity_id": members},
"hide_members": hide_members,
**extra_input,
},
@@ -232,7 +232,7 @@ async def test_options(
) -> None:
"""Test reconfiguring."""
members1 = [f"{group_type}.one", f"{group_type}.two"]
members2 = [f"{group_type}.four", f"{group_type}.five"]
members2 = [f"{group_type}.five", f"{group_type}.four"]
for member in members1:
hass.states.async_set(member, member_state, {})
@@ -243,12 +243,13 @@ async def test_options(
data={},
domain=DOMAIN,
options={
"entities": members1,
"entities": {"entity_id": members1},
"group_type": group_type,
"name": "Bed Room",
**extra_options,
},
title="Bed Room",
version=2,
)
group_config_entry.add_to_hass(hass)
@@ -263,21 +264,18 @@ async def test_options(
result = await hass.config_entries.options.async_init(config_entry.entry_id)
assert result["type"] is FlowResultType.FORM
assert result["step_id"] == group_type
assert (
get_schema_suggested_value(result["data_schema"].schema, "entities") == members1
)
assert get_schema_suggested_value(result["data_schema"].schema, "entities") == {
"entity_id": members1
}
assert "name" not in result["data_schema"].schema
assert result["data_schema"].schema["entities"].config["exclude_entities"] == [
f"{group_type}.bed_room"
]
result = await hass.config_entries.options.async_configure(
result["flow_id"],
user_input={"entities": members2, **options_options},
user_input={"entities": {"entity_id": members2}, **options_options},
)
assert result["type"] is FlowResultType.CREATE_ENTRY
assert result["data"] == {
"entities": members2,
"entities": {"entity_id": members2},
"group_type": group_type,
"hide_members": False,
"name": "Bed Room",
@@ -285,7 +283,7 @@ async def test_options(
}
assert config_entry.data == {}
assert config_entry.options == {
"entities": members2,
"entities": {"entity_id": members2},
"group_type": group_type,
"hide_members": False,
"name": "Bed Room",
@@ -336,12 +334,13 @@ async def test_all_options(
data={},
domain=DOMAIN,
options={
"entities": members1,
"entities": {"entity_id": members1},
"group_type": group_type,
"name": "Bed Room",
**extra_options,
},
title="Bed Room",
version=2,
)
group_config_entry.add_to_hass(hass)
@@ -359,12 +358,12 @@ async def test_all_options(
result = await hass.config_entries.options.async_configure(
result["flow_id"],
user_input={
"entities": members2,
"entities": {"entity_id": members2},
},
)
assert result["type"] is FlowResultType.CREATE_ENTRY
assert result["data"] == {
"entities": members2,
"entities": {"entity_id": members2},
"group_type": group_type,
"hide_members": False,
"name": "Bed Room",
@@ -372,7 +371,7 @@ async def test_all_options(
}
assert config_entry.data == {}
assert config_entry.options == {
"entities": members2,
"entities": {"entity_id": members2},
"group_type": group_type,
"hide_members": False,
"name": "Bed Room",
@@ -439,13 +438,14 @@ async def test_options_flow_hides_members(
data={},
domain=DOMAIN,
options={
"entities": members,
"entities": {"entity_id": members},
"group_type": group_type,
"hide_members": False,
"name": "Bed Room",
**extra_input,
},
title="Bed Room",
version=2,
)
group_config_entry.add_to_hass(hass)
@@ -458,7 +458,7 @@ async def test_options_flow_hides_members(
result = await hass.config_entries.options.async_configure(
result["flow_id"],
user_input={
"entities": members,
"entities": {"entity_id": members},
"hide_members": hide_members,
},
)
@@ -539,7 +539,10 @@ async def test_config_flow_preview(
"type": "group/start_preview",
"flow_id": result["flow_id"],
"flow_type": "config_flow",
"user_input": {"name": "My group", "entities": input_entities}
"user_input": {
"name": "My group",
"entities": {"entity_id": input_entities},
}
| extra_user_input,
}
)
@@ -571,7 +574,10 @@ async def test_config_flow_preview(
"type": "group/start_preview",
"flow_id": result["flow_id"],
"flow_type": "config_flow",
"user_input": {"name": "My group", "entities": input_entities}
"user_input": {
"name": "My group",
"entities": {"entity_id": input_entities},
}
| extra_user_input,
}
)
@@ -642,13 +648,14 @@ async def test_option_flow_preview(
data={},
domain=DOMAIN,
options={
"entities": input_entities,
"entities": {"entity_id": input_entities},
"group_type": domain,
"hide_members": False,
"name": "My group",
}
| extra_config_flow_data,
title="My group",
version=2,
)
config_entry.add_to_hass(hass)
assert await hass.config_entries.async_setup(config_entry.entry_id)
@@ -669,7 +676,8 @@ async def test_option_flow_preview(
"type": "group/start_preview",
"flow_id": result["flow_id"],
"flow_type": "options_flow",
"user_input": {"entities": input_entities} | extra_user_input,
"user_input": {"entities": {"entity_id": input_entities}}
| extra_user_input,
}
)
msg = await client.receive_json()
@@ -699,13 +707,14 @@ async def test_option_flow_sensor_preview_config_entry_removed(
data={},
domain=DOMAIN,
options={
"entities": input_entities,
"entities": {"entity_id": input_entities},
"group_type": "sensor",
"hide_members": False,
"name": "My sensor group",
"type": "min",
},
title="My min_max",
version=2,
)
config_entry.add_to_hass(hass)
assert await hass.config_entries.async_setup(config_entry.entry_id)
+2 -2
View File
@@ -126,8 +126,8 @@ async def test_state(hass: HomeAssistant) -> None:
await hass.async_block_till_done()
state = hass.states.get(COVER_GROUP)
assert state.attributes[ATTR_ENTITY_ID] == [
DEMO_COVER,
DEMO_COVER_POS,
DEMO_COVER,
DEMO_COVER_TILT,
DEMO_TILT,
]
@@ -287,8 +287,8 @@ async def test_attributes(
state = hass.states.get(COVER_GROUP)
assert state.state == CoverState.CLOSED
assert state.attributes[ATTR_ENTITY_ID] == [
DEMO_COVER,
DEMO_COVER_POS,
DEMO_COVER,
DEMO_COVER_TILT,
DEMO_TILT,
]
+12 -8
View File
@@ -135,10 +135,12 @@ async def test_state(hass: HomeAssistant, entity_registry: er.EntityRegistry) ->
hass.states.async_set(CEILING_FAN_ENTITY_ID, STATE_UNKNOWN, {})
await hass.async_block_till_done()
state = hass.states.get(FAN_GROUP)
assert state.attributes[ATTR_ENTITY_ID] == [
*FULL_FAN_ENTITY_IDS,
*LIMITED_FAN_ENTITY_IDS,
]
assert state.attributes[ATTR_ENTITY_ID] == sorted(
[
*FULL_FAN_ENTITY_IDS,
*LIMITED_FAN_ENTITY_IDS,
]
)
# All group members unavailable -> unavailable
hass.states.async_set(CEILING_FAN_ENTITY_ID, STATE_UNAVAILABLE)
@@ -228,10 +230,12 @@ async def test_attributes(hass: HomeAssistant) -> None:
await hass.async_block_till_done()
state = hass.states.get(FAN_GROUP)
assert state.state == STATE_ON
assert state.attributes[ATTR_ENTITY_ID] == [
*FULL_FAN_ENTITY_IDS,
*LIMITED_FAN_ENTITY_IDS,
]
assert state.attributes[ATTR_ENTITY_ID] == sorted(
[
*FULL_FAN_ENTITY_IDS,
*LIMITED_FAN_ENTITY_IDS,
]
)
# Add Entity that supports speed
hass.states.async_set(
+47 -2
View File
@@ -1973,12 +1973,13 @@ async def test_setup_and_remove_config_entry(
data={},
domain=group.DOMAIN,
options={
"entities": members1,
"entities": {"entity_id": members1},
"group_type": group_type,
"name": "Bed Room",
**extra_options,
},
title="Bed Room",
version=2,
)
group_config_entry.add_to_hass(hass)
assert await hass.config_entries.async_setup(group_config_entry.entry_id)
@@ -2062,13 +2063,14 @@ async def test_unhide_members_on_remove(
data={},
domain=group.DOMAIN,
options={
"entities": members,
"entities": {"entity_id": members},
"group_type": group_type,
"hide_members": hide_members,
"name": "Bed Room",
**extra_options,
},
title="Bed Room",
version=2,
)
group_config_entry.add_to_hass(hass)
assert await hass.config_entries.async_setup(group_config_entry.entry_id)
@@ -2301,3 +2303,46 @@ async def test_entity_platforms_with_multiple_on_states_with_state_match(
group_state2,
grouped_groups,
)
async def test_migrate_from_version_1_to_2(hass: HomeAssistant) -> None:
"""Test migrating from version 1 to 2."""
hass.states.async_set("binary_sensor.kitchen", "on")
hass.states.async_set("binary_sensor.bedroom", "on")
group_config_entry = MockConfigEntry(
data={},
domain="group",
options={
"entities": [
"binary_sensor.kitchen",
"binary_sensor.bedroom",
],
"group_type": "binary_sensor",
"name": "Fancy Group",
"all": False,
},
title="Fancy Group",
)
group_config_entry.add_to_hass(hass)
assert await hass.config_entries.async_setup(group_config_entry.entry_id)
await hass.async_block_till_done()
state = hass.states.get("binary_sensor.fancy_group")
assert state is not None
assert state.state == STATE_ON
entry = hass.config_entries.async_entries("group")[0]
assert entry.version == 2
assert entry.options == {
"all": False,
"entities": {
"entity_id": [
"binary_sensor.kitchen",
"binary_sensor.bedroom",
],
},
"group_type": "binary_sensor",
"name": "Fancy Group",
}
+2 -1
View File
@@ -77,7 +77,7 @@ async def test_default_state(
assert state is not None
assert state.state == STATE_ON
assert state.attributes[ATTR_SUPPORTED_FEATURES] == 0
assert state.attributes.get(ATTR_ENTITY_ID) == ["light.kitchen", "light.bedroom"]
assert state.attributes.get(ATTR_ENTITY_ID) == ["light.bedroom", "light.kitchen"]
assert state.attributes.get(ATTR_BRIGHTNESS) is None
assert state.attributes.get(ATTR_HS_COLOR) is None
assert state.attributes.get(ATTR_COLOR_TEMP_KELVIN) is None
@@ -1473,6 +1473,7 @@ async def test_invalid_service_calls(hass: HomeAssistant) -> None:
assert add_entities.call_count == 1
grouped_light = add_entities.call_args[0][0][0]
grouped_light.hass = hass
grouped_light._entity_ids = ["light.test1", "light.test2"]
service_call_events = async_capture_events(hass, EVENT_CALL_SERVICE)
+1 -1
View File
@@ -47,7 +47,7 @@ async def test_default_state(
state = hass.states.get("lock.door_group")
assert state is not None
assert state.state == LockState.LOCKED
assert state.attributes.get(ATTR_ENTITY_ID) == ["lock.front", "lock.back"]
assert state.attributes.get(ATTR_ENTITY_ID) == ["lock.back", "lock.front"]
entry = entity_registry.async_get("lock.door_group")
assert entry
+1 -1
View File
@@ -53,7 +53,7 @@ async def test_default_state(
state = hass.states.get("switch.multimedia_group")
assert state is not None
assert state.state == STATE_ON
assert state.attributes.get(ATTR_ENTITY_ID) == ["switch.tv", "switch.soundbar"]
assert state.attributes.get(ATTR_ENTITY_ID) == ["switch.soundbar", "switch.tv"]
entry = entity_registry.async_get("switch.multimedia_group")
assert entry
+2 -2
View File
@@ -117,9 +117,9 @@ async def test_state(hass: HomeAssistant) -> None:
await hass.async_block_till_done()
state = hass.states.get(VALVE_GROUP)
assert state.attributes[ATTR_ENTITY_ID] == [
DEMO_VALVE_POS1,
DEMO_VALVE1,
DEMO_VALVE2,
DEMO_VALVE_POS1,
DEMO_VALVE_POS2,
]
@@ -277,9 +277,9 @@ async def test_attributes(
state = hass.states.get(VALVE_GROUP)
assert state.state == ValveState.CLOSED
assert state.attributes[ATTR_ENTITY_ID] == [
DEMO_VALVE_POS1,
DEMO_VALVE1,
DEMO_VALVE2,
DEMO_VALVE_POS1,
DEMO_VALVE_POS2,
]