From 3fcdc244d71348ac0b4df96922c47b0eae6de089 Mon Sep 17 00:00:00 2001 From: Patrick Morrison Date: Sat, 9 May 2026 14:18:19 +0000 Subject: [PATCH 1/2] Add native clean segment support --- custom_components/robovac/config_flow.py | 22 +++++- custom_components/robovac/const.py | 2 + custom_components/robovac/strings.json | 8 +- custom_components/robovac/vacuum.py | 98 +++++++++++++++++++++++- 4 files changed, 126 insertions(+), 4 deletions(-) diff --git a/custom_components/robovac/config_flow.py b/custom_components/robovac/config_flow.py index 6eb342ad..716ef794 100644 --- a/custom_components/robovac/config_flow.py +++ b/custom_components/robovac/config_flow.py @@ -45,7 +45,13 @@ from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError -from .const import CONF_AUTODISCOVERY, CONF_VACS, DOMAIN +from .const import ( + CONF_AUTODISCOVERY, + CONF_ROOM_SEGMENT_MAP_ID, + CONF_ROOM_SEGMENTS, + CONF_VACS, + DOMAIN, +) from .countries import ( get_phone_code_by_country_code, get_phone_code_by_region, @@ -283,6 +289,12 @@ async def async_step_edit(self, user_input: dict[str, Any] | None = None) -> Con updated_vacuums[self.selected_vacuum][CONF_IP_ADDRESS] = user_input[ CONF_IP_ADDRESS ] + updated_vacuums[self.selected_vacuum][CONF_ROOM_SEGMENT_MAP_ID] = ( + user_input[CONF_ROOM_SEGMENT_MAP_ID] + ) + updated_vacuums[self.selected_vacuum][CONF_ROOM_SEGMENTS] = user_input[ + CONF_ROOM_SEGMENTS + ] self.hass.config_entries.async_update_entry( self.config_entry, @@ -301,6 +313,14 @@ async def async_step_edit(self, user_input: dict[str, Any] | None = None) -> Con CONF_IP_ADDRESS, default=vacuums[self.selected_vacuum].get(CONF_IP_ADDRESS), ): str, + vol.Required( + CONF_ROOM_SEGMENT_MAP_ID, + default=vacuums[self.selected_vacuum].get(CONF_ROOM_SEGMENT_MAP_ID, 1), + ): int, + vol.Optional( + CONF_ROOM_SEGMENTS, + default=vacuums[self.selected_vacuum].get(CONF_ROOM_SEGMENTS, ""), + ): str, } ) diff --git a/custom_components/robovac/const.py b/custom_components/robovac/const.py index 944c7dca..a0edeb97 100644 --- a/custom_components/robovac/const.py +++ b/custom_components/robovac/const.py @@ -3,6 +3,8 @@ DOMAIN = "robovac" CONF_VACS = "vacuums" CONF_AUTODISCOVERY = "autodiscovery" +CONF_ROOM_SEGMENT_MAP_ID = "room_segment_map_id" +CONF_ROOM_SEGMENTS = "room_segments" REFRESH_RATE = 60 PING_RATE = 10 TIMEOUT = 5 diff --git a/custom_components/robovac/strings.json b/custom_components/robovac/strings.json index 79ecd0ab..34f36c03 100644 --- a/custom_components/robovac/strings.json +++ b/custom_components/robovac/strings.json @@ -38,11 +38,15 @@ "title": "Edit vacuum", "data": { "autodiscovery": "Enable autodiscovery", - "ip_address": "IP Address" + "ip_address": "IP Address", + "room_segment_map_id": "Room segment map ID", + "room_segments": "Room segments" }, "data_description": { "autodiscovery": "Automatically find the vacuum on the network.", - "ip_address": "The static IP address of your vacuum on your local network (optional if autodiscovery is enabled)." + "ip_address": "The static IP address of your vacuum on your local network (optional if autodiscovery is enabled).", + "room_segment_map_id": "The map ID used when cleaning configured room segments.", + "room_segments": "Comma-separated room segments in id:name format, for example 1:Kitchen,2:Living Room." }, "description": "Autodiscovery will automatically update the IP address" } diff --git a/custom_components/robovac/vacuum.py b/custom_components/robovac/vacuum.py index 093ec874..35b7ea38 100644 --- a/custom_components/robovac/vacuum.py +++ b/custom_components/robovac/vacuum.py @@ -19,6 +19,7 @@ from __future__ import annotations import asyncio import base64 +from dataclasses import dataclass from datetime import timedelta from enum import StrEnum import json @@ -27,6 +28,7 @@ from typing import Any, cast from homeassistant.components.vacuum import ( + Segment, StateVacuumEntity, VacuumActivity, VacuumEntityFeature, @@ -46,7 +48,15 @@ from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback -from .const import CONF_VACS, DOMAIN, PING_RATE, REFRESH_RATE, TIMEOUT +from .const import ( + CONF_ROOM_SEGMENT_MAP_ID, + CONF_ROOM_SEGMENTS, + CONF_VACS, + DOMAIN, + PING_RATE, + REFRESH_RATE, + TIMEOUT, +) from .errors import getErrorMessage from .vacuums.base import RobovacCommand, RoboVacEntityFeature, TuyaCodes, TUYA_CONSUMABLES_CODES from .robovac import ModelNotSupportedException, RoboVac @@ -76,6 +86,50 @@ VACUUM_ACTIVITY_VALUES = {activity.value for activity in VacuumActivity} +@dataclass(frozen=True) +class RoomSegment: + """Cleanable room segment for a RoboVac map.""" + + id: int + name: str + + +@dataclass(frozen=True) +class RoomSegmentMap: + """Cleanable room segments and map id for a RoboVac.""" + + map_id: int + segments: tuple[RoomSegment, ...] + + +def _parse_room_segments(raw_segments: str | None) -> tuple[RoomSegment, ...]: + """Parse configured room segments from 'id:name' comma-separated text.""" + if not raw_segments: + return () + + segments: list[RoomSegment] = [] + for raw_segment in raw_segments.split(","): + segment = raw_segment.strip() + if not segment: + continue + raw_id, separator, name = segment.partition(":") + if not separator: + _LOGGER.warning("Ignoring room segment without ':' separator: %s", segment) + continue + try: + segment_id = int(raw_id.strip()) + except ValueError: + _LOGGER.warning("Ignoring room segment with invalid id: %s", segment) + continue + name = name.strip() + if not name: + _LOGGER.warning("Ignoring room segment without name: %s", segment) + continue + segments.append(RoomSegment(segment_id, name)) + + return tuple(segments) + + async def async_setup_entry( hass: HomeAssistant, config_entry: ConfigEntry, @@ -373,6 +427,11 @@ def __init__(self, item: dict[str, Any]) -> None: self._attr_model_code = item[CONF_MODEL] self._attr_ip_address = item[CONF_IP_ADDRESS] self._attr_access_token = item[CONF_ACCESS_TOKEN] + configured_segments = _parse_room_segments(item.get(CONF_ROOM_SEGMENTS)) + self._room_segment_map = RoomSegmentMap( + map_id=int(item.get(CONF_ROOM_SEGMENT_MAP_ID, 1)), + segments=configured_segments, + ) self.vacuum: RoboVac | None = None self.update_failures = 0 self.tuyastatus: dict[str, Any] | None = None @@ -415,6 +474,8 @@ def __init__(self, item: dict[str, Any]) -> None: if self.vacuum is not None: # Get the supported features from the vacuum features = int(self.vacuum.getHomeAssistantFeatures()) + if self._room_segment_map.segments: + features |= int(VacuumEntityFeature.CLEAN_AREA) self._attr_supported_features = VacuumEntityFeature(features) self._attr_robovac_supported = self.vacuum.getRoboVacFeatures() self._attr_activity_mapping = self.vacuum.getRoboVacActivityMapping() @@ -450,6 +511,41 @@ def __init__(self, item: dict[str, Any]) -> None: }, ) + async def async_get_segments(self) -> list[Segment]: + """Return cleanable segments for Home Assistant clean-area mapping.""" + return [ + Segment(id=str(segment.id), name=segment.name) + for segment in self._room_segment_map.segments + ] + + async def async_clean_segments(self, segment_ids: list[str], **kwargs: Any) -> None: + """Clean Home Assistant native clean-area segments.""" + known_room_ids = {segment.id for segment in self._room_segment_map.segments} + room_ids: list[int] = [] + for segment_id in segment_ids: + try: + room_id = int(segment_id) + except (TypeError, ValueError): + _LOGGER.warning("Ignoring invalid segment id for %s: %s", self.name, segment_id) + continue + if room_id not in known_room_ids: + _LOGGER.warning("Ignoring unknown segment id for %s: %s", self.name, segment_id) + continue + room_ids.append(room_id) + + if not room_ids: + _LOGGER.warning("No valid segment ids supplied for %s: %s", self.name, segment_ids) + return + + await self.async_send_command( + "roomClean", + { + "room_ids": room_ids, + "map_id": self._room_segment_map.map_id, + "count": int(kwargs.get("count", kwargs.get("repeats", 1))), + }, + ) + async def async_update(self) -> None: """Synchronize state from the vacuum. From e6e08b22a5e552ca6078c25bbe83f509911b3102 Mon Sep 17 00:00:00 2001 From: Patrick Morrison Date: Sat, 9 May 2026 23:11:10 +0000 Subject: [PATCH 2/2] Address clean segment review feedback --- .../robovac/translations/cy.json | 8 +- .../robovac/translations/de.json | 8 +- .../robovac/translations/en.json | 8 +- .../robovac/translations/es.json | 8 +- .../robovac/translations/fr.json | 8 +- .../robovac/translations/it.json | 8 +- .../robovac/translations/nl.json | 8 +- .../robovac/translations/pt.json | 8 +- custom_components/robovac/vacuum.py | 6 ++ tests/test_options_flow.py | 16 +++- tests/test_vacuum/test_vacuum_commands.py | 30 +++++++ tests/test_vacuum/test_vacuum_entity.py | 85 ++++++++++++++++++- 12 files changed, 181 insertions(+), 20 deletions(-) diff --git a/custom_components/robovac/translations/cy.json b/custom_components/robovac/translations/cy.json index 60f60435..422441e2 100644 --- a/custom_components/robovac/translations/cy.json +++ b/custom_components/robovac/translations/cy.json @@ -38,11 +38,15 @@ "title": "Golygu sugnwr llwch", "data": { "autodiscovery": "Galluogi awto-ddarganfod", - "ip_address": "Cyfeiriad IP" + "ip_address": "Cyfeiriad IP", + "room_segment_map_id": "Room segment map ID", + "room_segments": "Room segments" }, "data_description": { "autodiscovery": "Dod o hyd i'r sugnwr llwch yn awtomatig ar y rhwydwaith.", - "ip_address": "Cyfeiriad IP statig eich sugnwr llwch ar eich rhwydwaith lleol (dewisol os yw awto-ddarganfod wedi'i alluogi)." + "ip_address": "Cyfeiriad IP statig eich sugnwr llwch ar eich rhwydwaith lleol (dewisol os yw awto-ddarganfod wedi'i alluogi).", + "room_segment_map_id": "The map ID used when cleaning configured room segments.", + "room_segments": "Comma-separated room segments in id:name format, for example 1:Kitchen,2:Living Room." }, "description": "Bydd awto-ddarganfod yn diweddaru'r cyfeiriad IP yn awtomatig" } diff --git a/custom_components/robovac/translations/de.json b/custom_components/robovac/translations/de.json index ef5210bf..2751cc3a 100644 --- a/custom_components/robovac/translations/de.json +++ b/custom_components/robovac/translations/de.json @@ -38,11 +38,15 @@ "title": "Staubsauger bearbeiten", "data": { "autodiscovery": "Autodiscovery aktivieren", - "ip_address": "IP-Adresse" + "ip_address": "IP-Adresse", + "room_segment_map_id": "Room segment map ID", + "room_segments": "Room segments" }, "data_description": { "autodiscovery": "Den Staubsauger automatisch im Netzwerk finden.", - "ip_address": "Die statische IP-Adresse Ihres Staubsaugers in Ihrem lokalen Netzwerk (optional, wenn Autodiscovery aktiviert ist)." + "ip_address": "Die statische IP-Adresse Ihres Staubsaugers in Ihrem lokalen Netzwerk (optional, wenn Autodiscovery aktiviert ist).", + "room_segment_map_id": "The map ID used when cleaning configured room segments.", + "room_segments": "Comma-separated room segments in id:name format, for example 1:Kitchen,2:Living Room." }, "description": "Autodiscovery aktualisiert die IP-Adresse automatisch" } diff --git a/custom_components/robovac/translations/en.json b/custom_components/robovac/translations/en.json index 79ecd0ab..34f36c03 100644 --- a/custom_components/robovac/translations/en.json +++ b/custom_components/robovac/translations/en.json @@ -38,11 +38,15 @@ "title": "Edit vacuum", "data": { "autodiscovery": "Enable autodiscovery", - "ip_address": "IP Address" + "ip_address": "IP Address", + "room_segment_map_id": "Room segment map ID", + "room_segments": "Room segments" }, "data_description": { "autodiscovery": "Automatically find the vacuum on the network.", - "ip_address": "The static IP address of your vacuum on your local network (optional if autodiscovery is enabled)." + "ip_address": "The static IP address of your vacuum on your local network (optional if autodiscovery is enabled).", + "room_segment_map_id": "The map ID used when cleaning configured room segments.", + "room_segments": "Comma-separated room segments in id:name format, for example 1:Kitchen,2:Living Room." }, "description": "Autodiscovery will automatically update the IP address" } diff --git a/custom_components/robovac/translations/es.json b/custom_components/robovac/translations/es.json index f543083e..955abe0b 100644 --- a/custom_components/robovac/translations/es.json +++ b/custom_components/robovac/translations/es.json @@ -38,11 +38,15 @@ "title": "Editar aspiradora", "data": { "autodiscovery": "Habilitar autodescubrimiento", - "ip_address": "Dirección IP" + "ip_address": "Dirección IP", + "room_segment_map_id": "Room segment map ID", + "room_segments": "Room segments" }, "data_description": { "autodiscovery": "Encontrar automáticamente la aspiradora en la red.", - "ip_address": "La dirección IP estática de su aspiradora en su red local (opcional si el autodescubrimiento está habilitado)." + "ip_address": "La dirección IP estática de su aspiradora en su red local (opcional si el autodescubrimiento está habilitado).", + "room_segment_map_id": "The map ID used when cleaning configured room segments.", + "room_segments": "Comma-separated room segments in id:name format, for example 1:Kitchen,2:Living Room." }, "description": "El autodescubrimiento actualizará automáticamente la dirección IP" } diff --git a/custom_components/robovac/translations/fr.json b/custom_components/robovac/translations/fr.json index d0d5b910..df1112b5 100644 --- a/custom_components/robovac/translations/fr.json +++ b/custom_components/robovac/translations/fr.json @@ -38,11 +38,15 @@ "title": "Modifier l'aspirateur", "data": { "autodiscovery": "Activer la découverte automatique", - "ip_address": "Adresse IP" + "ip_address": "Adresse IP", + "room_segment_map_id": "Room segment map ID", + "room_segments": "Room segments" }, "data_description": { "autodiscovery": "Trouver automatiquement l'aspirateur sur le réseau.", - "ip_address": "L'adresse IP statique de votre aspirateur sur votre réseau local (optionnel si la découverte automatique est activée)." + "ip_address": "L'adresse IP statique de votre aspirateur sur votre réseau local (optionnel si la découverte automatique est activée).", + "room_segment_map_id": "The map ID used when cleaning configured room segments.", + "room_segments": "Comma-separated room segments in id:name format, for example 1:Kitchen,2:Living Room." }, "description": "La découverte automatique mettra à jour automatiquement l'adresse IP" } diff --git a/custom_components/robovac/translations/it.json b/custom_components/robovac/translations/it.json index 7fb03976..17cb385e 100644 --- a/custom_components/robovac/translations/it.json +++ b/custom_components/robovac/translations/it.json @@ -38,11 +38,15 @@ "title": "Modifica aspirapolvere", "data": { "autodiscovery": "Abilita rilevamento automatico", - "ip_address": "Indirizzo IP" + "ip_address": "Indirizzo IP", + "room_segment_map_id": "Room segment map ID", + "room_segments": "Room segments" }, "data_description": { "autodiscovery": "Trova automaticamente l'aspirapolvere sulla rete.", - "ip_address": "L'indirizzo IP statico del tuo aspirapolvere sulla tua rete locale (opzionale se il rilevamento automatico è abilitato)." + "ip_address": "L'indirizzo IP statico del tuo aspirapolvere sulla tua rete locale (opzionale se il rilevamento automatico è abilitato).", + "room_segment_map_id": "The map ID used when cleaning configured room segments.", + "room_segments": "Comma-separated room segments in id:name format, for example 1:Kitchen,2:Living Room." }, "description": "Il rilevamento automatico aggiornerà automaticamente l'indirizzo IP" } diff --git a/custom_components/robovac/translations/nl.json b/custom_components/robovac/translations/nl.json index a7bf8051..8a144520 100644 --- a/custom_components/robovac/translations/nl.json +++ b/custom_components/robovac/translations/nl.json @@ -38,11 +38,15 @@ "title": "Stofzuiger bewerken", "data": { "autodiscovery": "Automatische detectie inschakelen", - "ip_address": "IP-adres" + "ip_address": "IP-adres", + "room_segment_map_id": "Room segment map ID", + "room_segments": "Room segments" }, "data_description": { "autodiscovery": "Vind de stofzuiger automatisch op het netwerk.", - "ip_address": "Het statische IP-adres van uw stofzuiger op uw lokale netwerk (optioneel als automatische detectie is ingeschakeld)." + "ip_address": "Het statische IP-adres van uw stofzuiger op uw lokale netwerk (optioneel als automatische detectie is ingeschakeld).", + "room_segment_map_id": "The map ID used when cleaning configured room segments.", + "room_segments": "Comma-separated room segments in id:name format, for example 1:Kitchen,2:Living Room." }, "description": "Automatische detectie zal het IP-adres automatisch bijwerken" } diff --git a/custom_components/robovac/translations/pt.json b/custom_components/robovac/translations/pt.json index ddbe7c8a..58c65444 100644 --- a/custom_components/robovac/translations/pt.json +++ b/custom_components/robovac/translations/pt.json @@ -38,11 +38,15 @@ "title": "Editar aspirador", "data": { "autodiscovery": "Ativar descoberta automática", - "ip_address": "Endereço IP" + "ip_address": "Endereço IP", + "room_segment_map_id": "Room segment map ID", + "room_segments": "Room segments" }, "data_description": { "autodiscovery": "Encontrar automaticamente o aspirador na rede.", - "ip_address": "O endereço IP estático do seu aspirador na sua rede local (opcional se a descoberta automática estiver ativada)." + "ip_address": "O endereço IP estático do seu aspirador na sua rede local (opcional se a descoberta automática estiver ativada).", + "room_segment_map_id": "The map ID used when cleaning configured room segments.", + "room_segments": "Comma-separated room segments in id:name format, for example 1:Kitchen,2:Living Room." }, "description": "A descoberta automática atualizará automaticamente o endereço IP" } diff --git a/custom_components/robovac/vacuum.py b/custom_components/robovac/vacuum.py index 35b7ea38..b9eaced4 100644 --- a/custom_components/robovac/vacuum.py +++ b/custom_components/robovac/vacuum.py @@ -1028,7 +1028,13 @@ async def async_send_command( elif command in ("roomClean", "room_clean") and params is not None and isinstance(params, dict): room_ids = params.get("roomIds") or params.get("room_ids", [1]) count = params.get("count", 1) + map_id = params.get("mapId") or params.get("map_id") + if not isinstance(room_ids, list): + room_ids = [room_ids] + clean_request = {"roomIds": room_ids, "cleanTimes": count} + if map_id is not None: + clean_request["mapId"] = map_id method_call = { "method": "selectRoomsClean", "data": clean_request, diff --git a/tests/test_options_flow.py b/tests/test_options_flow.py index c1b7edb7..611e7e28 100644 --- a/tests/test_options_flow.py +++ b/tests/test_options_flow.py @@ -21,7 +21,13 @@ from homeassistant.core import HomeAssistant from custom_components.robovac.config_flow import OptionsFlowHandler -from custom_components.robovac.const import DOMAIN, CONF_AUTODISCOVERY, CONF_VACS +from custom_components.robovac.const import ( + CONF_AUTODISCOVERY, + CONF_ROOM_SEGMENT_MAP_ID, + CONF_ROOM_SEGMENTS, + CONF_VACS, + DOMAIN, +) from custom_components.robovac.robovac import RoboVac @@ -115,6 +121,8 @@ async def test_options_flow_edit_default_values(hass: HomeAssistant, mock_config # Verify that the form contains the expected fields assert CONF_AUTODISCOVERY in result["data_schema"].schema assert CONF_IP_ADDRESS in result["data_schema"].schema + assert CONF_ROOM_SEGMENT_MAP_ID in result["data_schema"].schema + assert CONF_ROOM_SEGMENTS in result["data_schema"].schema @pytest.mark.asyncio @@ -146,6 +154,8 @@ async def test_options_flow_edit_custom_values(hass: HomeAssistant) -> None: # Verify that the form contains the expected fields assert CONF_AUTODISCOVERY in result["data_schema"].schema assert CONF_IP_ADDRESS in result["data_schema"].schema + assert CONF_ROOM_SEGMENT_MAP_ID in result["data_schema"].schema + assert CONF_ROOM_SEGMENTS in result["data_schema"].schema @pytest.mark.asyncio @@ -182,6 +192,8 @@ async def test_options_flow_edit_submit_with_ip(hass: HomeAssistant, mock_config { CONF_AUTODISCOVERY: False, CONF_IP_ADDRESS: "192.168.1.100", + CONF_ROOM_SEGMENT_MAP_ID: 3, + CONF_ROOM_SEGMENTS: "1:Kitchen,2:Living Room", } ) @@ -229,6 +241,8 @@ async def test_options_flow_edit_submit_without_ip( { CONF_AUTODISCOVERY: True, CONF_IP_ADDRESS: "", + CONF_ROOM_SEGMENT_MAP_ID: 1, + CONF_ROOM_SEGMENTS: "", } ) diff --git a/tests/test_vacuum/test_vacuum_commands.py b/tests/test_vacuum/test_vacuum_commands.py index 76048678..325f32e6 100644 --- a/tests/test_vacuum/test_vacuum_commands.py +++ b/tests/test_vacuum/test_vacuum_commands.py @@ -1,5 +1,8 @@ """Tests for the RoboVac vacuum entity commands.""" +import base64 +import json + import pytest from typing import Any from unittest.mock import patch, MagicMock, AsyncMock, call @@ -268,6 +271,33 @@ async def test_async_send_command(mock_robovac, mock_vacuum_data) -> None: mock_robovac.async_set.assert_called_once_with({"118": False}) +@pytest.mark.asyncio +async def test_room_clean_includes_map_id(mock_robovac, mock_vacuum_data) -> None: + """Test roomClean forwards the configured map ID when provided.""" + with patch("custom_components.robovac.vacuum.RoboVac", return_value=mock_robovac): + entity = RoboVacEntity(mock_vacuum_data) + + await entity.async_send_command( + "roomClean", + { + "room_ids": [2], + "map_id": 3, + "count": 2, + }, + ) + + first_call = mock_robovac.async_set.await_args_list[0] + payload = first_call.args[0]["124"] + method_call = json.loads(base64.b64decode(payload).decode("utf8")) + + assert method_call["method"] == "selectRoomsClean" + assert method_call["data"] == { + "roomIds": [2], + "cleanTimes": 2, + "mapId": 3, + } + + @pytest.mark.asyncio async def test_async_update(mock_robovac, mock_vacuum_data) -> None: """Test the async_update method.""" diff --git a/tests/test_vacuum/test_vacuum_entity.py b/tests/test_vacuum/test_vacuum_entity.py index d76155c0..cf314aa1 100644 --- a/tests/test_vacuum/test_vacuum_entity.py +++ b/tests/test_vacuum/test_vacuum_entity.py @@ -2,10 +2,13 @@ import pytest from typing import Any -from unittest.mock import patch, MagicMock +from unittest.mock import AsyncMock, patch, MagicMock -from homeassistant.components.vacuum import VacuumActivity -from custom_components.robovac.vacuum import RoboVacEntity +from homeassistant.components.vacuum import VacuumActivity, VacuumEntityFeature +from homeassistant.const import CONF_ID + +from custom_components.robovac.const import CONF_ROOM_SEGMENT_MAP_ID, CONF_ROOM_SEGMENTS +from custom_components.robovac.vacuum import RoboVacEntity, _parse_room_segments from custom_components.robovac.vacuums.base import TuyaCodes @@ -168,6 +171,82 @@ async def test_update_entity_values(mock_robovac, mock_vacuum_data) -> None: assert entity._attr_fan_speed == "Standard" +def test_parse_room_segments() -> None: + """Test configured room segment parsing.""" + segments = _parse_room_segments("1:Kitchen, bad, nope:Study, 2: Living Room, 3:") + + assert [(segment.id, segment.name) for segment in segments] == [ + (1, "Kitchen"), + (2, "Living Room"), + ] + + +def test_clean_area_feature_enabled_only_with_segments( + mock_robovac, mock_vacuum_data +) -> None: + """Test CLEAN_AREA is enabled only when segments are configured.""" + with patch("custom_components.robovac.vacuum.RoboVac", return_value=mock_robovac): + entity = RoboVacEntity(mock_vacuum_data) + assert not entity.supported_features & VacuumEntityFeature.CLEAN_AREA + + segmented_data = { + **mock_vacuum_data, + CONF_ROOM_SEGMENT_MAP_ID: 3, + CONF_ROOM_SEGMENTS: "1:Kitchen,2:Living Room", + } + with patch("custom_components.robovac.vacuum.RoboVac", return_value=mock_robovac): + entity = RoboVacEntity(segmented_data) + assert entity.supported_features & VacuumEntityFeature.CLEAN_AREA + + +@pytest.mark.asyncio +async def test_async_get_segments(mock_robovac, mock_vacuum_data) -> None: + """Test Home Assistant segment metadata is returned from configured rooms.""" + data = { + **mock_vacuum_data, + CONF_ROOM_SEGMENT_MAP_ID: 3, + CONF_ROOM_SEGMENTS: "1:Kitchen,2:Living Room", + } + + with patch("custom_components.robovac.vacuum.RoboVac", return_value=mock_robovac): + entity = RoboVacEntity(data) + + segments = await entity.async_get_segments() + + assert [(segment.id, segment.name) for segment in segments] == [ + ("1", "Kitchen"), + ("2", "Living Room"), + ] + + +@pytest.mark.asyncio +async def test_async_clean_segments_maps_to_room_clean( + mock_robovac, mock_vacuum_data +) -> None: + """Test segment cleaning validates IDs and calls roomClean.""" + data = { + **mock_vacuum_data, + CONF_ID: "test_robovac_id", + CONF_ROOM_SEGMENT_MAP_ID: 3, + CONF_ROOM_SEGMENTS: "1:Kitchen,2:Living Room", + } + + with patch("custom_components.robovac.vacuum.RoboVac", return_value=mock_robovac): + entity = RoboVacEntity(data) + + entity.async_send_command = AsyncMock() + await entity.async_clean_segments(["2", "bad", "99"], repeats=2) + + entity.async_send_command.assert_awaited_once_with( + "roomClean", + { + "room_ids": [2], + "map_id": 3, + "count": 2, + }, + ) + + @pytest.mark.asyncio async def test_fan_speed_formatting(mock_robovac, mock_vacuum_data) -> None: """Test fan speed formatting in update_entity_values."""