22
33from __future__ import annotations
44
5+ from collections .abc import Callable
56import logging
67from typing import TYPE_CHECKING , Any
78
1011from 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 ,
6568CONF_FAN_SPEED_LIST = "fan_speeds"
6669CONF_FAN_SPEED = "fan_speed"
6770CONF_FAN_SPEED_TEMPLATE = "fan_speed_template"
71+ CONF_SEGMENTS_TEMPLATE = "segments_template"
6872
6973DEFAULT_NAME = "Template Vacuum"
7074
7781}
7882
7983SCRIPT_FIELDS = (
84+ SERVICE_CLEAN_AREA ,
8085 SERVICE_CLEAN_SPOT ,
8186 SERVICE_LOCATE ,
8287 SERVICE_PAUSE ,
8691 SERVICE_STOP ,
8792)
8893
94+ CLEAN_AREA_GROUP = "clean_area_group"
95+
8996VACUUM_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
113133VACUUM_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+
217290class 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 :
0 commit comments