mirror of
https://github.com/home-assistant/core.git
synced 2026-10-06 14:29:21 -04:00
Move vacuum service registration to services module (#183417)
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -7,7 +7,6 @@ from functools import partial
|
||||
import logging
|
||||
from typing import Any, final, override
|
||||
|
||||
import probatio
|
||||
from propcache.api import cached_property
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
@@ -18,25 +17,32 @@ from homeassistant.const import ( # noqa: F401 # STATE_PAUSED/IDLE are API
|
||||
SERVICE_TURN_ON,
|
||||
STATE_ON,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant, ServiceCall, callback
|
||||
from homeassistant.exceptions import ServiceValidationError
|
||||
from homeassistant.helpers import (
|
||||
config_validation as cv,
|
||||
issue_registry as ir,
|
||||
service as service_helper,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers import config_validation as cv, issue_registry as ir
|
||||
from homeassistant.helpers.entity import Entity, EntityDescription
|
||||
from homeassistant.helpers.entity_component import EntityComponent
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
|
||||
from .const import (
|
||||
from .const import ( # noqa: F401
|
||||
ATTR_FAN_SPEED,
|
||||
ATTR_PARAMS,
|
||||
DATA_COMPONENT,
|
||||
DOMAIN,
|
||||
SERVICE_CLEAN_AREA,
|
||||
SERVICE_CLEAN_SPOT,
|
||||
SERVICE_LOCATE,
|
||||
SERVICE_PAUSE,
|
||||
SERVICE_RETURN_TO_BASE,
|
||||
SERVICE_SEND_COMMAND,
|
||||
SERVICE_SET_FAN_SPEED,
|
||||
SERVICE_START,
|
||||
SERVICE_STOP,
|
||||
VacuumActivity,
|
||||
VacuumEntityCapabilityAttribute,
|
||||
VacuumEntityFeature,
|
||||
VacuumEntityStateAttribute,
|
||||
)
|
||||
from .services import async_setup_services
|
||||
from .websocket import async_register_websocket_handlers
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
@@ -47,21 +53,10 @@ PLATFORM_SCHEMA_BASE = cv.PLATFORM_SCHEMA_BASE
|
||||
SCAN_INTERVAL = timedelta(seconds=20)
|
||||
|
||||
ATTR_CLEANED_AREA = "cleaned_area"
|
||||
ATTR_FAN_SPEED = "fan_speed"
|
||||
ATTR_FAN_SPEED_LIST = "fan_speed_list"
|
||||
ATTR_PARAMS = "params"
|
||||
ATTR_STATUS = "status"
|
||||
|
||||
SERVICE_CLEAN_SPOT = "clean_spot"
|
||||
SERVICE_CLEAN_AREA = "clean_area"
|
||||
SERVICE_LOCATE = "locate"
|
||||
SERVICE_RETURN_TO_BASE = "return_to_base"
|
||||
SERVICE_SEND_COMMAND = "send_command"
|
||||
SERVICE_SET_FAN_SPEED = "set_fan_speed"
|
||||
SERVICE_START_PAUSE = "start_pause"
|
||||
SERVICE_START = "start"
|
||||
SERVICE_PAUSE = "pause"
|
||||
SERVICE_STOP = "stop"
|
||||
|
||||
DEFAULT_NAME = "Vacuum cleaner robot"
|
||||
|
||||
@@ -86,65 +81,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
|
||||
async_register_websocket_handlers(hass)
|
||||
|
||||
component.async_register_entity_service(
|
||||
SERVICE_START,
|
||||
None,
|
||||
"async_start",
|
||||
[VacuumEntityFeature.START],
|
||||
)
|
||||
component.async_register_entity_service(
|
||||
SERVICE_PAUSE,
|
||||
None,
|
||||
"async_pause",
|
||||
[VacuumEntityFeature.PAUSE],
|
||||
)
|
||||
component.async_register_entity_service(
|
||||
SERVICE_RETURN_TO_BASE,
|
||||
None,
|
||||
"async_return_to_base",
|
||||
[VacuumEntityFeature.RETURN_HOME],
|
||||
)
|
||||
component.async_register_entity_service(
|
||||
SERVICE_CLEAN_SPOT,
|
||||
None,
|
||||
"async_clean_spot",
|
||||
[VacuumEntityFeature.CLEAN_SPOT],
|
||||
)
|
||||
component.async_register_batched_entity_service(
|
||||
SERVICE_CLEAN_AREA,
|
||||
{
|
||||
probatio.Required("cleaning_area_id"): probatio.All(cv.ensure_list, [str]),
|
||||
},
|
||||
StateVacuumEntity.async_internal_clean_area,
|
||||
[VacuumEntityFeature.CLEAN_AREA],
|
||||
)
|
||||
component.async_register_entity_service(
|
||||
SERVICE_LOCATE,
|
||||
None,
|
||||
"async_locate",
|
||||
[VacuumEntityFeature.LOCATE],
|
||||
)
|
||||
component.async_register_entity_service(
|
||||
SERVICE_STOP,
|
||||
None,
|
||||
"async_stop",
|
||||
[VacuumEntityFeature.STOP],
|
||||
)
|
||||
component.async_register_entity_service(
|
||||
SERVICE_SET_FAN_SPEED,
|
||||
{probatio.Required(ATTR_FAN_SPEED): cv.string},
|
||||
"async_set_fan_speed",
|
||||
[VacuumEntityFeature.FAN_SPEED],
|
||||
)
|
||||
component.async_register_entity_service(
|
||||
SERVICE_SEND_COMMAND,
|
||||
{
|
||||
probatio.Required(ATTR_COMMAND): cv.string,
|
||||
probatio.Optional(ATTR_PARAMS): probatio.Any(dict, cv.ensure_list),
|
||||
},
|
||||
"async_send_command",
|
||||
[VacuumEntityFeature.SEND_COMMAND],
|
||||
)
|
||||
async_setup_services(hass)
|
||||
|
||||
return True
|
||||
|
||||
@@ -313,69 +250,6 @@ class StateVacuumEntity(
|
||||
|
||||
return [Segment(**segment) for segment in last_seen_segments]
|
||||
|
||||
@final
|
||||
@staticmethod
|
||||
async def async_internal_clean_area(
|
||||
entities: list[StateVacuumEntity], call: ServiceCall
|
||||
) -> None:
|
||||
"""Perform an area clean.
|
||||
|
||||
Calls async_clean_segments for each entity.
|
||||
"""
|
||||
data = dict(call.data)
|
||||
cleaning_area_id: list[str] = data.pop("cleaning_area_id")
|
||||
|
||||
entity_data: list[tuple[StateVacuumEntity, dict[str, Any]]] = []
|
||||
handled_areas: set[str] = set()
|
||||
for entity in entities:
|
||||
if entity.registry_entry is None:
|
||||
raise RuntimeError(
|
||||
"Cannot perform area clean, registry entry is not set for"
|
||||
f" {entity.entity_id}"
|
||||
)
|
||||
|
||||
options: Mapping[str, Any] = entity.registry_entry.options.get(DOMAIN, {})
|
||||
area_mapping: dict[str, list[str]] | None = options.get("area_mapping")
|
||||
|
||||
if area_mapping is None:
|
||||
raise ServiceValidationError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="area_mapping_not_configured",
|
||||
translation_placeholders={"entity_id": entity.entity_id},
|
||||
)
|
||||
|
||||
# We use a dict to preserve the order of segments.
|
||||
segment_ids: dict[str, None] = {}
|
||||
for area_id in cleaning_area_id:
|
||||
if (segments := area_mapping.get(area_id)) is None:
|
||||
continue
|
||||
handled_areas.add(area_id)
|
||||
for segment_id in segments:
|
||||
segment_ids[segment_id] = None
|
||||
|
||||
if not segment_ids:
|
||||
_LOGGER.debug(
|
||||
"No segments found for cleaning_area_id %s on vacuum %s",
|
||||
cleaning_area_id,
|
||||
entity.entity_id,
|
||||
)
|
||||
continue
|
||||
|
||||
entity_data.append((entity, {"segment_ids": list(segment_ids), **data}))
|
||||
|
||||
if entity_data:
|
||||
await service_helper.async_handle_entity_calls(
|
||||
"async_clean_segments", entity_data, context=call.context
|
||||
)
|
||||
|
||||
unhandled_areas = set(cleaning_area_id) - handled_areas
|
||||
if unhandled_areas:
|
||||
raise ServiceValidationError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="areas_not_mapped",
|
||||
translation_placeholders={"areas": ", ".join(sorted(unhandled_areas))},
|
||||
)
|
||||
|
||||
def clean_segments(self, segment_ids: list[str], **kwargs: Any) -> None:
|
||||
"""Perform an area clean."""
|
||||
raise NotImplementedError
|
||||
|
||||
@@ -11,6 +11,19 @@ if TYPE_CHECKING:
|
||||
|
||||
DOMAIN: Final = "vacuum"
|
||||
|
||||
ATTR_FAN_SPEED: Final = "fan_speed"
|
||||
ATTR_PARAMS: Final = "params"
|
||||
|
||||
SERVICE_CLEAN_AREA: Final = "clean_area"
|
||||
SERVICE_CLEAN_SPOT: Final = "clean_spot"
|
||||
SERVICE_LOCATE: Final = "locate"
|
||||
SERVICE_PAUSE: Final = "pause"
|
||||
SERVICE_RETURN_TO_BASE: Final = "return_to_base"
|
||||
SERVICE_SEND_COMMAND: Final = "send_command"
|
||||
SERVICE_SET_FAN_SPEED: Final = "set_fan_speed"
|
||||
SERVICE_START: Final = "start"
|
||||
SERVICE_STOP: Final = "stop"
|
||||
|
||||
DATA_COMPONENT: HassKey[EntityComponent[StateVacuumEntity]] = HassKey(DOMAIN)
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,162 @@
|
||||
"""Services for the vacuum integration."""
|
||||
|
||||
from collections.abc import Mapping
|
||||
import logging
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
import probatio
|
||||
|
||||
from homeassistant.const import ATTR_COMMAND
|
||||
from homeassistant.core import HomeAssistant, ServiceCall, callback
|
||||
from homeassistant.exceptions import ServiceValidationError
|
||||
from homeassistant.helpers import config_validation as cv, service as service_helper
|
||||
|
||||
from .const import (
|
||||
ATTR_FAN_SPEED,
|
||||
ATTR_PARAMS,
|
||||
DATA_COMPONENT,
|
||||
DOMAIN,
|
||||
SERVICE_CLEAN_AREA,
|
||||
SERVICE_CLEAN_SPOT,
|
||||
SERVICE_LOCATE,
|
||||
SERVICE_PAUSE,
|
||||
SERVICE_RETURN_TO_BASE,
|
||||
SERVICE_SEND_COMMAND,
|
||||
SERVICE_SET_FAN_SPEED,
|
||||
SERVICE_START,
|
||||
SERVICE_STOP,
|
||||
VacuumEntityFeature,
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from . import StateVacuumEntity
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
async def _async_clean_area(
|
||||
entities: list[StateVacuumEntity], call: ServiceCall
|
||||
) -> None:
|
||||
"""Perform an area clean.
|
||||
|
||||
Calls async_clean_segments for each entity.
|
||||
"""
|
||||
data = dict(call.data)
|
||||
cleaning_area_id: list[str] = data.pop("cleaning_area_id")
|
||||
|
||||
entity_data: list[tuple[StateVacuumEntity, dict[str, Any]]] = []
|
||||
handled_areas: set[str] = set()
|
||||
for entity in entities:
|
||||
if entity.registry_entry is None:
|
||||
raise RuntimeError(
|
||||
"Cannot perform area clean, registry entry is not set for"
|
||||
f" {entity.entity_id}"
|
||||
)
|
||||
|
||||
options: Mapping[str, Any] = entity.registry_entry.options.get(DOMAIN, {})
|
||||
area_mapping: dict[str, list[str]] | None = options.get("area_mapping")
|
||||
|
||||
if area_mapping is None:
|
||||
raise ServiceValidationError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="area_mapping_not_configured",
|
||||
translation_placeholders={"entity_id": entity.entity_id},
|
||||
)
|
||||
|
||||
# We use a dict to preserve the order of segments.
|
||||
segment_ids: dict[str, None] = {}
|
||||
for area_id in cleaning_area_id:
|
||||
if (segments := area_mapping.get(area_id)) is None:
|
||||
continue
|
||||
handled_areas.add(area_id)
|
||||
for segment_id in segments:
|
||||
segment_ids[segment_id] = None
|
||||
|
||||
if not segment_ids:
|
||||
_LOGGER.debug(
|
||||
"No segments found for cleaning_area_id %s on vacuum %s",
|
||||
cleaning_area_id,
|
||||
entity.entity_id,
|
||||
)
|
||||
continue
|
||||
|
||||
entity_data.append((entity, {"segment_ids": list(segment_ids), **data}))
|
||||
|
||||
if entity_data:
|
||||
await service_helper.async_handle_entity_calls(
|
||||
"async_clean_segments", entity_data, context=call.context
|
||||
)
|
||||
|
||||
unhandled_areas = set(cleaning_area_id) - handled_areas
|
||||
if unhandled_areas:
|
||||
raise ServiceValidationError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="areas_not_mapped",
|
||||
translation_placeholders={"areas": ", ".join(sorted(unhandled_areas))},
|
||||
)
|
||||
|
||||
|
||||
@callback
|
||||
def async_setup_services(hass: HomeAssistant) -> None:
|
||||
"""Register the vacuum services."""
|
||||
component = hass.data[DATA_COMPONENT]
|
||||
|
||||
component.async_register_entity_service(
|
||||
SERVICE_START,
|
||||
None,
|
||||
"async_start",
|
||||
[VacuumEntityFeature.START],
|
||||
)
|
||||
component.async_register_entity_service(
|
||||
SERVICE_PAUSE,
|
||||
None,
|
||||
"async_pause",
|
||||
[VacuumEntityFeature.PAUSE],
|
||||
)
|
||||
component.async_register_entity_service(
|
||||
SERVICE_RETURN_TO_BASE,
|
||||
None,
|
||||
"async_return_to_base",
|
||||
[VacuumEntityFeature.RETURN_HOME],
|
||||
)
|
||||
component.async_register_entity_service(
|
||||
SERVICE_CLEAN_SPOT,
|
||||
None,
|
||||
"async_clean_spot",
|
||||
[VacuumEntityFeature.CLEAN_SPOT],
|
||||
)
|
||||
component.async_register_batched_entity_service(
|
||||
SERVICE_CLEAN_AREA,
|
||||
{
|
||||
probatio.Required("cleaning_area_id"): probatio.All(cv.ensure_list, [str]),
|
||||
},
|
||||
_async_clean_area,
|
||||
[VacuumEntityFeature.CLEAN_AREA],
|
||||
)
|
||||
component.async_register_entity_service(
|
||||
SERVICE_LOCATE,
|
||||
None,
|
||||
"async_locate",
|
||||
[VacuumEntityFeature.LOCATE],
|
||||
)
|
||||
component.async_register_entity_service(
|
||||
SERVICE_STOP,
|
||||
None,
|
||||
"async_stop",
|
||||
[VacuumEntityFeature.STOP],
|
||||
)
|
||||
component.async_register_entity_service(
|
||||
SERVICE_SET_FAN_SPEED,
|
||||
{probatio.Required(ATTR_FAN_SPEED): cv.string},
|
||||
"async_set_fan_speed",
|
||||
[VacuumEntityFeature.FAN_SPEED],
|
||||
)
|
||||
component.async_register_entity_service(
|
||||
SERVICE_SEND_COMMAND,
|
||||
{
|
||||
probatio.Required(ATTR_COMMAND): cv.string,
|
||||
probatio.Optional(ATTR_PARAMS): probatio.Any(dict, cv.ensure_list),
|
||||
},
|
||||
"async_send_command",
|
||||
[VacuumEntityFeature.SEND_COMMAND],
|
||||
)
|
||||
@@ -20,6 +20,7 @@ from homeassistant.components.vacuum import (
|
||||
VacuumActivity,
|
||||
VacuumEntityFeature,
|
||||
)
|
||||
from homeassistant.components.vacuum.services import _async_clean_area
|
||||
from homeassistant.core import Context, HomeAssistant, ServiceCall
|
||||
from homeassistant.exceptions import ServiceValidationError
|
||||
from homeassistant.helpers import entity_registry as er, issue_registry as ir
|
||||
@@ -449,7 +450,7 @@ async def test_clean_area_no_registry_entry(hass: HomeAssistant) -> None:
|
||||
RuntimeError,
|
||||
match="Cannot perform area clean, registry entry is not set",
|
||||
):
|
||||
await StateVacuumEntity.async_internal_clean_area([mock_vacuum], call)
|
||||
await _async_clean_area([mock_vacuum], call)
|
||||
|
||||
with pytest.raises(
|
||||
RuntimeError,
|
||||
|
||||
Reference in New Issue
Block a user