Move group service registration to services module (#184305)

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
epenet
2026-10-05 15:10:42 +02:00
committed by GitHub
co-authored by Claude Opus 5
parent 91fabb4644
commit 8bbc10b9f1
3 changed files with 256 additions and 207 deletions
+10 -207
View File
@@ -1,7 +1,5 @@
"""Provide the functionality to group entities."""
import asyncio
from collections.abc import Collection
import logging
from typing import Any
@@ -10,22 +8,18 @@ import probatio
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (
ATTR_ENTITY_ID, # noqa: F401
ATTR_ICON,
ATTR_NAME,
CONF_ENTITIES,
CONF_ICON,
CONF_NAME,
SERVICE_RELOAD,
Platform,
SERVICE_RELOAD, # noqa: F401
)
from homeassistant.core import HomeAssistant, ServiceCall
from homeassistant.core import HomeAssistant
from homeassistant.helpers import config_validation as cv, entity_registry as er
from homeassistant.helpers.group import (
expand_entity_ids as _expand_entity_ids,
get_entity_ids as _get_entity_ids,
get_group_entities,
)
from homeassistant.helpers.reload import async_reload_integration_platforms
from homeassistant.helpers.typing import ConfigType
#
@@ -45,33 +39,19 @@ from .const import ( # noqa: F401
ATTR_OBJECT_ID,
ATTR_ORDER,
ATTR_REMOVE_ENTITIES,
CONF_ALL,
CONF_HIDE_MEMBERS,
DATA_COMPONENT,
DOMAIN,
GROUP_ORDER,
PLATFORMS,
REG_KEY,
SERVICE_REMOVE,
SERVICE_SET,
)
from .entity import Group, async_get_component
from .entity import Group # noqa: F401
from .registry import async_setup as async_setup_registry
CONF_ALL = "all"
SERVICE_SET = "set"
SERVICE_REMOVE = "remove"
PLATFORMS = [
Platform.BINARY_SENSOR,
Platform.COVER,
Platform.FAN,
Platform.LIGHT,
Platform.LOCK,
Platform.MEDIA_PLAYER,
Platform.NOTIFY,
Platform.SENSOR,
Platform.SWITCH,
Platform.VALVE,
]
from .services import async_process_config, async_setup_services
_LOGGER = logging.getLogger(__name__)
@@ -201,187 +181,10 @@ async def async_remove_entry(hass: HomeAssistant, entry: ConfigEntry) -> None:
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Set up all groups found defined in the configuration."""
component = async_get_component(hass)
await async_setup_registry(hass)
await _async_process_config(hass, config)
await async_process_config(hass, config)
async def reload_service_handler(service: ServiceCall) -> None:
"""Group reload handler.
- Remove group.group entities not created by service calls and set them up again
- Reload xxx.group platforms
"""
conf = await component.async_prepare_reload(skip_reset=True)
# Simplified + modified version of EntityPlatform.async_reset:
# - group.group never retries setup
# - group.group never polls
# - We don't need to reset EntityPlatform._setup_complete
# - Only remove entities which were not created by service calls
tasks = [
entity.async_remove()
for entity in component.entities
if entity.entity_id.startswith("group.") and not entity.created_by_service
]
if tasks:
await asyncio.gather(*tasks)
component.config = None
await _async_process_config(hass, conf)
await async_reload_integration_platforms(hass, DOMAIN, PLATFORMS)
hass.services.async_register(
DOMAIN, SERVICE_RELOAD, reload_service_handler, schema=probatio.Schema({})
)
service_lock = asyncio.Lock()
async def locked_service_handler(service: ServiceCall) -> None:
"""Handle a service with an async lock."""
async with service_lock:
await groups_service_handler(service)
async def groups_service_handler(service: ServiceCall) -> None:
"""Handle dynamic group service functions."""
object_id = service.data[ATTR_OBJECT_ID]
entity_id = f"{DOMAIN}.{object_id}"
group = component.get_entity(entity_id)
# new group
if service.service == SERVICE_SET and group is None:
entity_ids = (
service.data.get(ATTR_ENTITIES)
or service.data.get(ATTR_ADD_ENTITIES)
or None
)
await Group.async_create_group(
hass,
service.data.get(ATTR_NAME, object_id),
created_by_service=True,
entity_ids=entity_ids,
icon=service.data.get(ATTR_ICON),
mode=service.data.get(ATTR_ALL),
object_id=object_id,
order=None,
context=service.context,
)
return
if group is None:
_LOGGER.warning("%s:Group '%s' doesn't exist!", service.service, object_id)
return
group.async_set_context(service.context)
# update group
if service.service == SERVICE_SET:
need_update = False
if ATTR_ADD_ENTITIES in service.data:
delta = service.data[ATTR_ADD_ENTITIES]
entity_ids = set(group.tracking) | set(delta)
group.async_update_tracked_entity_ids(entity_ids)
if ATTR_REMOVE_ENTITIES in service.data:
delta = service.data[ATTR_REMOVE_ENTITIES]
entity_ids = set(group.tracking) - set(delta)
group.async_update_tracked_entity_ids(entity_ids)
if ATTR_ENTITIES in service.data:
entity_ids = service.data[ATTR_ENTITIES]
group.async_update_tracked_entity_ids(entity_ids)
if ATTR_NAME in service.data:
group.set_name(service.data[ATTR_NAME])
need_update = True
if ATTR_ICON in service.data:
group.set_icon(service.data[ATTR_ICON])
need_update = True
if ATTR_ALL in service.data:
group.mode = all if service.data[ATTR_ALL] else any
need_update = True
if need_update:
group.async_write_ha_state()
return
# remove group
if service.service == SERVICE_REMOVE:
await component.async_remove_entity(entity_id)
hass.services.async_register(
DOMAIN,
SERVICE_SET,
locked_service_handler,
schema=probatio.All(
probatio.Schema(
{
probatio.Required(ATTR_OBJECT_ID): cv.slug,
probatio.Optional(ATTR_NAME): cv.string,
probatio.Optional(ATTR_ICON): cv.string,
probatio.Optional(ATTR_ALL): cv.boolean,
probatio.Exclusive(ATTR_ENTITIES, "entities"): cv.entity_ids,
probatio.Exclusive(ATTR_ADD_ENTITIES, "entities"): cv.entity_ids,
probatio.Exclusive(ATTR_REMOVE_ENTITIES, "entities"): cv.entity_ids,
}
)
),
)
hass.services.async_register(
DOMAIN,
SERVICE_REMOVE,
groups_service_handler,
schema=probatio.Schema({probatio.Required(ATTR_OBJECT_ID): cv.slug}),
)
async_setup_services(hass)
return True
async def _async_process_config(hass: HomeAssistant, config: ConfigType) -> None:
"""Process group configuration."""
hass.data.setdefault(GROUP_ORDER, 0)
entities = []
domain_config: dict[str, dict[str, Any]] = config.get(DOMAIN, {})
for object_id, conf in domain_config.items():
name: str = conf.get(CONF_NAME, object_id)
entity_ids: Collection[str] = conf.get(CONF_ENTITIES) or []
icon: str | None = conf.get(CONF_ICON)
mode = bool(conf.get(CONF_ALL))
order = hass.data[GROUP_ORDER]
# We keep track of the order when we are creating the tasks
# in the same way that async_create_group does to make
# sure we use the same ordering system. This overcomes
# the problem with concurrently creating the groups
entities.append(
Group.async_create_group_entity(
hass,
name,
created_by_service=False,
entity_ids=entity_ids,
icon=icon,
object_id=object_id,
mode=mode,
order=order,
)
)
# Keep track of the group order without iterating
# every state in the state machine every time
# we setup a new group
hass.data[GROUP_ORDER] += 1
# If called before the platform async_setup is called (test cases)
await async_get_component(hass).async_add_entities(entities)
+19
View File
@@ -2,6 +2,7 @@
from typing import TYPE_CHECKING
from homeassistant.const import Platform
from homeassistant.util.hass_dict import HassKey
if TYPE_CHECKING:
@@ -22,6 +23,24 @@ ATTR_ADD_ENTITIES = "add_entities"
ATTR_REMOVE_ENTITIES = "remove_entities"
ATTR_AUTO = "auto"
ATTR_ENTITIES = "entities"
SERVICE_SET = "set"
SERVICE_REMOVE = "remove"
CONF_ALL = "all"
PLATFORMS = [
Platform.BINARY_SENSOR,
Platform.COVER,
Platform.FAN,
Platform.LIGHT,
Platform.LOCK,
Platform.MEDIA_PLAYER,
Platform.NOTIFY,
Platform.SENSOR,
Platform.SWITCH,
Platform.VALVE,
]
ATTR_OBJECT_ID = "object_id"
ATTR_ORDER = "order"
ATTR_ALL = "all"
+227
View File
@@ -0,0 +1,227 @@
"""Services for the group integration."""
import asyncio
from collections.abc import Collection
from functools import partial
import logging
from typing import Any
import probatio
from homeassistant.const import (
ATTR_ICON,
ATTR_NAME,
CONF_ENTITIES,
CONF_ICON,
CONF_NAME,
SERVICE_RELOAD,
)
from homeassistant.core import HomeAssistant, ServiceCall, callback
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.reload import async_reload_integration_platforms
from homeassistant.helpers.typing import ConfigType
from .const import (
ATTR_ADD_ENTITIES,
ATTR_ALL,
ATTR_ENTITIES,
ATTR_OBJECT_ID,
ATTR_REMOVE_ENTITIES,
CONF_ALL,
DOMAIN,
GROUP_ORDER,
PLATFORMS,
SERVICE_REMOVE,
SERVICE_SET,
)
from .entity import Group, async_get_component
_LOGGER = logging.getLogger(__name__)
async def async_process_config(hass: HomeAssistant, config: ConfigType) -> None:
"""Process group configuration."""
hass.data.setdefault(GROUP_ORDER, 0)
entities = []
domain_config: dict[str, dict[str, Any]] = config.get(DOMAIN, {})
for object_id, conf in domain_config.items():
name: str = conf.get(CONF_NAME, object_id)
entity_ids: Collection[str] = conf.get(CONF_ENTITIES) or []
icon: str | None = conf.get(CONF_ICON)
mode = bool(conf.get(CONF_ALL))
order = hass.data[GROUP_ORDER]
# We keep track of the order when we are creating the tasks
# in the same way that async_create_group does to make
# sure we use the same ordering system. This overcomes
# the problem with concurrently creating the groups
entities.append(
Group.async_create_group_entity(
hass,
name,
created_by_service=False,
entity_ids=entity_ids,
icon=icon,
object_id=object_id,
mode=mode,
order=order,
)
)
# Keep track of the group order without iterating
# every state in the state machine every time
# we setup a new group
hass.data[GROUP_ORDER] += 1
# If called before the platform async_setup is called (test cases)
await async_get_component(hass).async_add_entities(entities)
async def _async_reload_service(service: ServiceCall) -> None:
"""Group reload handler.
- Remove group.group entities not created by service calls and set them up again
- Reload xxx.group platforms
"""
hass = service.hass
component = async_get_component(hass)
conf = await component.async_prepare_reload(skip_reset=True)
# Simplified + modified version of EntityPlatform.async_reset:
# - group.group never retries setup
# - group.group never polls
# - We don't need to reset EntityPlatform._setup_complete
# - Only remove entities which were not created by service calls
tasks = [
entity.async_remove()
for entity in component.entities
if entity.entity_id.startswith("group.") and not entity.created_by_service
]
if tasks:
await asyncio.gather(*tasks)
component.config = None
await async_process_config(hass, conf)
await async_reload_integration_platforms(hass, DOMAIN, PLATFORMS)
async def _async_groups_service(service: ServiceCall) -> None:
"""Handle dynamic group service functions."""
hass = service.hass
component = async_get_component(hass)
object_id = service.data[ATTR_OBJECT_ID]
entity_id = f"{DOMAIN}.{object_id}"
group = component.get_entity(entity_id)
# new group
if service.service == SERVICE_SET and group is None:
entity_ids = (
service.data.get(ATTR_ENTITIES)
or service.data.get(ATTR_ADD_ENTITIES)
or None
)
await Group.async_create_group(
hass,
service.data.get(ATTR_NAME, object_id),
created_by_service=True,
entity_ids=entity_ids,
icon=service.data.get(ATTR_ICON),
mode=service.data.get(ATTR_ALL),
object_id=object_id,
order=None,
context=service.context,
)
return
if group is None:
_LOGGER.warning("%s:Group '%s' doesn't exist!", service.service, object_id)
return
group.async_set_context(service.context)
# update group
if service.service == SERVICE_SET:
need_update = False
if ATTR_ADD_ENTITIES in service.data:
delta = service.data[ATTR_ADD_ENTITIES]
entity_ids = set(group.tracking) | set(delta)
group.async_update_tracked_entity_ids(entity_ids)
if ATTR_REMOVE_ENTITIES in service.data:
delta = service.data[ATTR_REMOVE_ENTITIES]
entity_ids = set(group.tracking) - set(delta)
group.async_update_tracked_entity_ids(entity_ids)
if ATTR_ENTITIES in service.data:
entity_ids = service.data[ATTR_ENTITIES]
group.async_update_tracked_entity_ids(entity_ids)
if ATTR_NAME in service.data:
group.set_name(service.data[ATTR_NAME])
need_update = True
if ATTR_ICON in service.data:
group.set_icon(service.data[ATTR_ICON])
need_update = True
if ATTR_ALL in service.data:
group.mode = all if service.data[ATTR_ALL] else any
need_update = True
if need_update:
group.async_write_ha_state()
return
# remove group
if service.service == SERVICE_REMOVE:
await component.async_remove_entity(entity_id)
async def _async_locked_groups_service(
service_lock: asyncio.Lock, service: ServiceCall
) -> None:
"""Handle a service with an async lock."""
async with service_lock:
await _async_groups_service(service)
@callback
def async_setup_services(hass: HomeAssistant) -> None:
"""Register the group services."""
service_lock = asyncio.Lock()
hass.services.async_register(
DOMAIN, SERVICE_RELOAD, _async_reload_service, schema=probatio.Schema({})
)
hass.services.async_register(
DOMAIN,
SERVICE_SET,
partial(_async_locked_groups_service, service_lock),
schema=probatio.All(
probatio.Schema(
{
probatio.Required(ATTR_OBJECT_ID): cv.slug,
probatio.Optional(ATTR_NAME): cv.string,
probatio.Optional(ATTR_ICON): cv.string,
probatio.Optional(ATTR_ALL): cv.boolean,
probatio.Exclusive(ATTR_ENTITIES, "entities"): cv.entity_ids,
probatio.Exclusive(ATTR_ADD_ENTITIES, "entities"): cv.entity_ids,
probatio.Exclusive(ATTR_REMOVE_ENTITIES, "entities"): cv.entity_ids,
}
)
),
)
hass.services.async_register(
DOMAIN,
SERVICE_REMOVE,
_async_groups_service,
schema=probatio.Schema({probatio.Required(ATTR_OBJECT_ID): cv.slug}),
)