33from __future__ import annotations
44
55from enum import IntEnum
6- from typing import TYPE_CHECKING , Any
6+ from typing import TYPE_CHECKING , Any , cast
77
88from chip .clusters import Objects as clusters
9+ from chip .clusters .Objects import NullValue
910from matter_server .client .models import device_types
11+ import voluptuous as vol
1012
1113from homeassistant .components .vacuum import (
1214 StateVacuumEntity ,
1618)
1719from homeassistant .config_entries import ConfigEntry
1820from homeassistant .const import Platform
19- from homeassistant .core import HomeAssistant , callback
21+ from homeassistant .core import (
22+ HomeAssistant ,
23+ ServiceResponse ,
24+ SupportsResponse ,
25+ callback ,
26+ )
2027from homeassistant .exceptions import HomeAssistantError
28+ from homeassistant .helpers import config_validation as cv , entity_platform
2129from homeassistant .helpers .entity_platform import AddConfigEntryEntitiesCallback
2230
31+ from .const import SERVICE_CLEAN_AREAS , SERVICE_GET_AREAS , SERVICE_SELECT_AREAS
2332from .entity import MatterEntity
2433from .helpers import get_matter
2534from .models import MatterDiscoverySchema
2635
36+ ATTR_CURRENT_AREA = "current_area"
37+ ATTR_CURRENT_AREA_NAME = "current_area_name"
38+ ATTR_SELECTED_AREAS = "selected_areas"
39+
2740
2841class OperationalState (IntEnum ):
2942 """Operational State of the vacuum cleaner.
@@ -56,6 +69,33 @@ async def async_setup_entry(
5669 """Set up Matter vacuum platform from Config Entry."""
5770 matter = get_matter (hass )
5871 matter .register_platform_handler (Platform .VACUUM , async_add_entities )
72+ platform = entity_platform .async_get_current_platform ()
73+
74+ # This will call Entity.async_handle_get_areas
75+ platform .async_register_entity_service (
76+ SERVICE_GET_AREAS ,
77+ schema = None ,
78+ func = "async_handle_get_areas" ,
79+ supports_response = SupportsResponse .ONLY ,
80+ )
81+ # This will call Entity.async_handle_clean_areas
82+ platform .async_register_entity_service (
83+ SERVICE_CLEAN_AREAS ,
84+ schema = {
85+ vol .Required ("areas" ): vol .All (cv .ensure_list , [cv .positive_int ]),
86+ },
87+ func = "async_handle_clean_areas" ,
88+ supports_response = SupportsResponse .ONLY ,
89+ )
90+ # This will call Entity.async_handle_select_areas
91+ platform .async_register_entity_service (
92+ SERVICE_SELECT_AREAS ,
93+ schema = {
94+ vol .Required ("areas" ): vol .All (cv .ensure_list , [cv .positive_int ]),
95+ },
96+ func = "async_handle_select_areas" ,
97+ supports_response = SupportsResponse .ONLY ,
98+ )
5999
60100
61101class MatterVacuum (MatterEntity , StateVacuumEntity ):
@@ -65,9 +105,23 @@ class MatterVacuum(MatterEntity, StateVacuumEntity):
65105 _supported_run_modes : (
66106 dict [int , clusters .RvcRunMode .Structs .ModeOptionStruct ] | None
67107 ) = None
108+ _attr_matter_areas : dict [str , Any ] | None = None
109+ _attr_current_area : int | None = None
110+ _attr_current_area_name : str | None = None
111+ _attr_selected_areas : list [int ] | None = None
112+ _attr_supported_maps : list [dict [str , Any ]] | None = None
68113 entity_description : StateVacuumEntityDescription
69114 _platform_translation_key = "vacuum"
70115
116+ @property
117+ def extra_state_attributes (self ) -> dict [str , Any ] | None :
118+ """Return the state attributes of the entity."""
119+ return {
120+ ATTR_CURRENT_AREA : self ._attr_current_area ,
121+ ATTR_CURRENT_AREA_NAME : self ._attr_current_area_name ,
122+ ATTR_SELECTED_AREAS : self ._attr_selected_areas ,
123+ }
124+
71125 def _get_run_mode_by_tag (
72126 self , tag : ModeTag
73127 ) -> clusters .RvcRunMode .Structs .ModeOptionStruct | None :
@@ -136,10 +190,160 @@ async def async_pause(self) -> None:
136190 """Pause the cleaning task."""
137191 await self .send_device_command (clusters .RvcOperationalState .Commands .Pause ())
138192
193+ def async_get_areas (self , ** kwargs : Any ) -> dict [str , Any ]:
194+ """Get available area and map IDs from vacuum appliance."""
195+
196+ supported_areas = self .get_matter_attribute_value (
197+ clusters .ServiceArea .Attributes .SupportedAreas
198+ )
199+ if not supported_areas :
200+ raise HomeAssistantError ("Can't get areas from the device." )
201+
202+ # Group by area_id: {area_id: {"map_id": ..., "name": ...}}
203+ areas = {}
204+ for area in supported_areas :
205+ area_id = getattr (area , "areaID" , None )
206+ map_id = getattr (area , "mapID" , None )
207+ location_name = None
208+ area_info = getattr (area , "areaInfo" , None )
209+ if area_info is not None :
210+ location_info = getattr (area_info , "locationInfo" , None )
211+ if location_info is not None :
212+ location_name = getattr (location_info , "locationName" , None )
213+ if area_id is not None :
214+ areas [area_id ] = {"map_id" : map_id , "name" : location_name }
215+
216+ # Optionally, also extract supported maps if available
217+ supported_maps = self .get_matter_attribute_value (
218+ clusters .ServiceArea .Attributes .SupportedMaps
219+ )
220+ maps = []
221+ if supported_maps :
222+ maps = [
223+ {
224+ "map_id" : getattr (m , "mapID" , None ),
225+ "name" : getattr (m , "name" , None ),
226+ }
227+ for m in supported_maps
228+ ]
229+
230+ return {
231+ "areas" : areas ,
232+ "maps" : maps ,
233+ }
234+
235+ async def async_handle_get_areas (self , ** kwargs : Any ) -> ServiceResponse :
236+ """Get available area and map IDs from vacuum appliance."""
237+ # Group by area_id: {area_id: {"map_id": ..., "name": ...}}
238+ areas = {}
239+ if self ._attr_matter_areas is not None :
240+ for area in self ._attr_matter_areas :
241+ area_id = getattr (area , "areaID" , None )
242+ map_id = getattr (area , "mapID" , None )
243+ location_name = None
244+ area_info = getattr (area , "areaInfo" , None )
245+ if area_info is not None :
246+ location_info = getattr (area_info , "locationInfo" , None )
247+ if location_info is not None :
248+ location_name = getattr (location_info , "locationName" , None )
249+ if area_id is not None :
250+ if map_id is NullValue :
251+ areas [area_id ] = {"name" : location_name }
252+ else :
253+ areas [area_id ] = {"map_id" : map_id , "name" : location_name }
254+
255+ # Optionally, also extract supported maps if available
256+ supported_maps = self .get_matter_attribute_value (
257+ clusters .ServiceArea .Attributes .SupportedMaps
258+ )
259+ maps = []
260+ if supported_maps != NullValue : # chip.clusters.Types.Nullable
261+ maps = [
262+ {
263+ "map_id" : getattr (m , "mapID" , None )
264+ if getattr (m , "mapID" , None ) != NullValue
265+ else None ,
266+ "name" : getattr (m , "name" , None ),
267+ }
268+ for m in supported_maps
269+ ]
270+
271+ return cast (
272+ ServiceResponse ,
273+ {
274+ "areas" : areas ,
275+ "maps" : maps ,
276+ },
277+ )
278+ return None
279+
280+ async def async_handle_select_areas (
281+ self , areas : list [int ], ** kwargs : Any
282+ ) -> ServiceResponse :
283+ """Select areas to clean."""
284+ selected_areas = areas
285+ # Matter command to the vacuum cleaner to select the areas.
286+ await self .send_device_command (
287+ clusters .ServiceArea .Commands .SelectAreas (newAreas = selected_areas )
288+ )
289+ # Return response indicating selected areas.
290+ return cast (
291+ ServiceResponse , {"status" : "areas selected" , "areas" : selected_areas }
292+ )
293+
294+ async def async_handle_clean_areas (
295+ self , areas : list [int ], ** kwargs : Any
296+ ) -> ServiceResponse :
297+ """Start cleaning the specified areas."""
298+ # Matter command to the vacuum cleaner to select the areas.
299+ await self .send_device_command (
300+ clusters .ServiceArea .Commands .SelectAreas (newAreas = areas )
301+ )
302+ # Start the vacuum cleaner after selecting areas.
303+ await self .async_start ()
304+ # Return response indicating selected areas.
305+ return cast (
306+ ServiceResponse , {"status" : "cleaning areas selected" , "areas" : areas }
307+ )
308+
139309 @callback
140310 def _update_from_device (self ) -> None :
141311 """Update from device."""
142312 self ._calculate_features ()
313+ # ServiceArea: get areas from the device
314+ self ._attr_matter_areas = self .get_matter_attribute_value (
315+ clusters .ServiceArea .Attributes .SupportedAreas
316+ )
317+ # optional CurrentArea attribute
318+ # pylint: disable=too-many-nested-blocks
319+ if self .get_matter_attribute_value (clusters .ServiceArea .Attributes .CurrentArea ):
320+ current_area = self .get_matter_attribute_value (
321+ clusters .ServiceArea .Attributes .CurrentArea
322+ )
323+ # get areaInfo.locationInfo.locationName for current_area in SupportedAreas list
324+ area_name = None
325+ if self ._attr_matter_areas :
326+ for area in self ._attr_matter_areas :
327+ if getattr (area , "areaID" , None ) == current_area :
328+ area_info = getattr (area , "areaInfo" , None )
329+ if area_info is not None :
330+ location_info = getattr (area_info , "locationInfo" , None )
331+ if location_info is not None :
332+ area_name = getattr (location_info , "locationName" , None )
333+ break
334+ self ._attr_current_area = current_area
335+ self ._attr_current_area_name = area_name
336+ else :
337+ self ._attr_current_area = None
338+ self ._attr_current_area_name = None
339+
340+ # optional SelectedAreas attribute
341+ if self .get_matter_attribute_value (
342+ clusters .ServiceArea .Attributes .SelectedAreas
343+ ):
344+ self ._attr_selected_areas = self .get_matter_attribute_value (
345+ clusters .ServiceArea .Attributes .SelectedAreas
346+ )
143347 # derive state from the run mode + operational state
144348 run_mode_raw : int = self .get_matter_attribute_value (
145349 clusters .RvcRunMode .Attributes .CurrentMode
@@ -220,6 +424,10 @@ def _calculate_features(self) -> None:
220424 clusters .RvcRunMode .Attributes .CurrentMode ,
221425 clusters .RvcOperationalState .Attributes .OperationalState ,
222426 ),
427+ optional_attributes = (
428+ clusters .ServiceArea .Attributes .SelectedAreas ,
429+ clusters .ServiceArea .Attributes .CurrentArea ,
430+ ),
223431 device_type = (device_types .RoboticVacuumCleaner ,),
224432 allow_none_value = True ,
225433 ),
0 commit comments