Skip to content

Commit e8e9914

Browse files
gustavakerstromarturpragaczCopilot
authored
Template vacuum segments (#167805)
Co-authored-by: Artur Pragacz <49985303+arturpragacz@users.noreply.github.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
1 parent 77c7225 commit e8e9914

3 files changed

Lines changed: 450 additions & 10 deletions

File tree

homeassistant/components/template/vacuum.py

Lines changed: 117 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
from __future__ import annotations
44

5+
from collections.abc import Callable
56
import logging
67
from typing import TYPE_CHECKING, Any
78

@@ -10,13 +11,15 @@
1011
from homeassistant.components.vacuum import (
1112
ATTR_FAN_SPEED,
1213
DOMAIN as VACUUM_DOMAIN,
14+
SERVICE_CLEAN_AREA,
1315
SERVICE_CLEAN_SPOT,
1416
SERVICE_LOCATE,
1517
SERVICE_PAUSE,
1618
SERVICE_RETURN_TO_BASE,
1719
SERVICE_SET_FAN_SPEED,
1820
SERVICE_START,
1921
SERVICE_STOP,
22+
Segment,
2023
StateVacuumEntity,
2124
VacuumActivity,
2225
VacuumEntityFeature,
@@ -65,6 +68,7 @@
6568
CONF_FAN_SPEED_LIST = "fan_speeds"
6669
CONF_FAN_SPEED = "fan_speed"
6770
CONF_FAN_SPEED_TEMPLATE = "fan_speed_template"
71+
CONF_SEGMENTS_TEMPLATE = "segments_template"
6872

6973
DEFAULT_NAME = "Template Vacuum"
7074

@@ -77,6 +81,7 @@
7781
}
7882

7983
SCRIPT_FIELDS = (
84+
SERVICE_CLEAN_AREA,
8085
SERVICE_CLEAN_SPOT,
8186
SERVICE_LOCATE,
8287
SERVICE_PAUSE,
@@ -86,28 +91,43 @@
8691
SERVICE_STOP,
8792
)
8893

94+
CLEAN_AREA_GROUP = "clean_area_group"
95+
8996
VACUUM_COMMON_SCHEMA = vol.Schema(
9097
{
9198
vol.Optional(CONF_BATTERY_LEVEL): cv.template,
9299
vol.Optional(CONF_FAN_SPEED_LIST, default=[]): cv.ensure_list,
93100
vol.Optional(CONF_FAN_SPEED): cv.template,
94101
vol.Optional(CONF_STATE): cv.template,
102+
vol.Inclusive(
103+
CONF_SEGMENTS_TEMPLATE,
104+
CLEAN_AREA_GROUP,
105+
f"Options `{CONF_SEGMENTS_TEMPLATE}` and `{SERVICE_CLEAN_AREA}` must both exist",
106+
): cv.template,
95107
vol.Optional(SERVICE_CLEAN_SPOT): cv.SCRIPT_SCHEMA,
96108
vol.Optional(SERVICE_LOCATE): cv.SCRIPT_SCHEMA,
97109
vol.Optional(SERVICE_PAUSE): cv.SCRIPT_SCHEMA,
98110
vol.Optional(SERVICE_RETURN_TO_BASE): cv.SCRIPT_SCHEMA,
99111
vol.Optional(SERVICE_SET_FAN_SPEED): cv.SCRIPT_SCHEMA,
100112
vol.Required(SERVICE_START): cv.SCRIPT_SCHEMA,
101113
vol.Optional(SERVICE_STOP): cv.SCRIPT_SCHEMA,
114+
vol.Inclusive(
115+
SERVICE_CLEAN_AREA,
116+
CLEAN_AREA_GROUP,
117+
f"Options `{CONF_SEGMENTS_TEMPLATE}` and `{SERVICE_CLEAN_AREA}` must both exist",
118+
): cv.SCRIPT_SCHEMA,
102119
}
103120
)
104121

105-
VACUUM_YAML_SCHEMA = VACUUM_COMMON_SCHEMA.extend(
106-
TEMPLATE_ENTITY_OPTIMISTIC_SCHEMA
107-
).extend(
108-
make_template_entity_common_modern_attributes_schema(
109-
VACUUM_DOMAIN, DEFAULT_NAME
110-
).schema
122+
123+
VACUUM_YAML_SCHEMA = vol.All(
124+
VACUUM_COMMON_SCHEMA.extend(TEMPLATE_ENTITY_OPTIMISTIC_SCHEMA).extend(
125+
make_template_entity_common_modern_attributes_schema(
126+
VACUUM_DOMAIN, DEFAULT_NAME
127+
).schema
128+
),
129+
cv.key_dependency(CONF_SEGMENTS_TEMPLATE, CONF_UNIQUE_ID),
130+
cv.key_dependency(SERVICE_CLEAN_AREA, CONF_UNIQUE_ID),
111131
)
112132

113133
VACUUM_LEGACY_YAML_SCHEMA = vol.All(
@@ -214,6 +234,59 @@ def create_issue(
214234
)
215235

216236

237+
def validate_segments(
238+
entity: AbstractTemplateVacuum,
239+
option: str,
240+
) -> Callable[[Any], list[Segment] | None]:
241+
"""Parse segment template to list of segments."""
242+
243+
def parse(result: Any) -> list[Segment] | None:
244+
if template_validators.check_result_for_none(result):
245+
return None
246+
247+
segments: list[Segment] = []
248+
249+
if not isinstance(result, list):
250+
template_validators.log_validation_result_error(
251+
entity,
252+
option,
253+
result,
254+
"expected a list of dictionaries",
255+
)
256+
return None
257+
258+
for item in result:
259+
if not isinstance(item, dict):
260+
template_validators.log_validation_result_error(
261+
entity,
262+
option,
263+
item,
264+
"expected dictionary with keys id, name and optional group"
265+
" and string values",
266+
)
267+
return None
268+
269+
if (
270+
not isinstance(item.get("id"), str)
271+
or not isinstance(item.get("name"), str)
272+
or ("group" in item and not isinstance(item["group"], str))
273+
or not set(item).issubset({"id", "name", "group"})
274+
):
275+
template_validators.log_validation_result_error(
276+
entity,
277+
option,
278+
item,
279+
"expected dictionary with keys id, name and optional group"
280+
" and string values",
281+
)
282+
return None
283+
284+
segments.append(Segment(**item))
285+
return segments
286+
287+
return parse
288+
289+
217290
class AbstractTemplateVacuum(AbstractTemplateEntity, StateVacuumEntity):
218291
"""Representation of a template vacuum features."""
219292

@@ -228,6 +301,7 @@ def __init__(self, name: str, config: dict[str, Any]) -> None: # pylint: disabl
228301

229302
# List of valid fan speeds
230303
self._attr_fan_speed_list = config[CONF_FAN_SPEED_LIST]
304+
self._segments: list[Segment] = []
231305
self.setup_state_template(
232306
"_attr_activity",
233307
template_validators.strenum(self, CONF_STATE, VacuumActivity),
@@ -245,6 +319,13 @@ def __init__(self, name: str, config: dict[str, Any]) -> None: # pylint: disabl
245319
template_validators.number(self, CONF_BATTERY_LEVEL, 0.0, 100.0),
246320
)
247321

322+
self.setup_template(
323+
CONF_SEGMENTS_TEMPLATE,
324+
"_segments",
325+
validate_segments(self, CONF_SEGMENTS_TEMPLATE),
326+
self._update_segments,
327+
)
328+
248329
self._attr_supported_features = (
249330
VacuumEntityFeature.START | VacuumEntityFeature.STATE
250331
)
@@ -260,11 +341,41 @@ def __init__(self, name: str, config: dict[str, Any]) -> None: # pylint: disabl
260341
(SERVICE_CLEAN_SPOT, VacuumEntityFeature.CLEAN_SPOT),
261342
(SERVICE_LOCATE, VacuumEntityFeature.LOCATE),
262343
(SERVICE_SET_FAN_SPEED, VacuumEntityFeature.FAN_SPEED),
344+
(SERVICE_CLEAN_AREA, VacuumEntityFeature.CLEAN_AREA),
263345
):
264346
if (action_config := config.get(action_id)) is not None:
265347
self.add_script(action_id, action_config, name, DOMAIN)
266348
self._attr_supported_features |= supported_feature
267349

350+
@callback
351+
def _update_segments(self, result: list[Segment] | None) -> None:
352+
"""Save segment templates and create issue when segments changed."""
353+
if result is None:
354+
return
355+
356+
self._segments = result
357+
358+
if (last_seen := self.last_seen_segments) is not None and {
359+
s.id: s for s in last_seen
360+
} != {s.id: s for s in self._segments}:
361+
self.async_create_segments_issue()
362+
363+
async def async_get_segments(self) -> list[Segment]:
364+
"""Return the available segments."""
365+
return self._segments
366+
367+
async def async_clean_segments(self, segment_ids: list[str], **kwargs: Any) -> None:
368+
"""Perform an area clean."""
369+
if self._attr_assumed_state:
370+
self._attr_activity = VacuumActivity.CLEANING
371+
self.async_write_ha_state()
372+
if script := self._action_scripts.get(SERVICE_CLEAN_AREA):
373+
await self.async_run_script(
374+
script,
375+
run_variables={"segment_ids": segment_ids},
376+
context=self._context,
377+
)
378+
268379
async def async_start(self) -> None:
269380
"""Start or resume the cleaning task."""
270381
if self._attr_assumed_state:

tests/components/template/test_helpers.py

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@
5959
from homeassistant.components.template.vacuum import (
6060
LEGACY_FIELDS as VACUUM_LEGACY_FIELDS,
6161
SCRIPT_FIELDS as VACUUM_SCRIPT_FIELDS,
62+
SERVICE_CLEAN_AREA as VACUUM_SERVICE_CLEAN_AREA,
6263
)
6364
from homeassistant.core import HomeAssistant
6465
from homeassistant.exceptions import PlatformNotReady
@@ -578,8 +579,14 @@ async def _setup_and_test_yaml_device_action(
578579
),
579580
(
580581
"vacuum",
581-
VACUUM_SCRIPT_FIELDS,
582-
{"fan_speeds": ["low", "medium", "high"]},
582+
[
583+
service
584+
for service in VACUUM_SCRIPT_FIELDS
585+
if service != VACUUM_SERVICE_CLEAN_AREA
586+
],
587+
{
588+
"fan_speeds": ["low", "medium", "high"],
589+
},
583590
(
584591
("start", {}),
585592
("pause", {}),
@@ -773,7 +780,11 @@ async def test_yaml_device_actions_modern_config(
773780
),
774781
(
775782
"vacuum",
776-
VACUUM_SCRIPT_FIELDS,
783+
[
784+
service
785+
for service in VACUUM_SCRIPT_FIELDS
786+
if service != VACUUM_SERVICE_CLEAN_AREA
787+
],
777788
{
778789
"fan_speeds": ["low", "medium", "high"],
779790
"state": "{{ 'on' }}",

0 commit comments

Comments
 (0)