mirror of
https://github.com/home-assistant/core.git
synced 2026-10-07 06:50:41 -04:00
Move group service registration to services module (#184305)
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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}),
|
||||
)
|
||||
Reference in New Issue
Block a user