make better

This commit is contained in:
ia74 2025-07-21 17:44:58 -05:00
parent e77e353890
commit 74b3b6e478
10 changed files with 527 additions and 83 deletions

View file

@ -1,16 +1,26 @@
# roomba_rest980
Drop-in native integration/replacement for [jerrywillans/ha-rest980-roomba](https://github.com/jeremywillans/ha-rest980-roomba).
Drop-in native integration/replacement for [jeremywillans/ha-rest980-roomba](https://github.com/jeremywillans/ha-rest980-roomba).
Still work in progress, but the vacuum entity has been fully ported over.
## Roadmap
- [x] Feature parity (minus actions) with vacuum entity
- [x] Attribute parity with jeremywillans' YAML entry
- [x] Cloud API connection
- [ ] Cloud MQTT connection
- [ ] Actions
- [ ] Dynamically grab rooms and add them to the UI
- [x] Actions
- [x] Start
- [ ] Clean all rooms by default
- [x] Selective room cleaning
- [ ] Two pass feature
- [x] Pause
- [ ] Unpause (Requires further testing)
- [x] Return Home
- [x] Stop
- [x] Spot Clean
- [ ] Mapping Run
- [x] Dynamically grab rooms and add them to the UI
- [x] Grab room data (optional, cloud only)
- [ ] Create map image
@ -162,14 +172,24 @@ If all has gone right, checking the device will show something like this:
![Added the config!](img/fin.png)
## Step 4: Rooms! (Cloud)
## Step 3.5: Cloud issues.. (Cloud)
> As of current, this text is a placeholder for when this feature actually gets added. Still work in progress, but when this banner is removed, the below will be true.
iRobot does some quite interesting things with their API. As of current, my implementation does not use the cloud MQTT server (yet). However, even with the iRobot app and every instance of a connection closed, you may get ratelimited (*sometimes*) with the error "No mqtt slot available". There isn't a workaround for this that I know of, except to restart HA/reload the robot's config entry in ~5 minutes.
## Step 4: Rooms! (Cloud)
Your rooms will be auto-imported, alongside a clean map view, much like the one from the app.
This allows you to selectively clean rooms, and control it by automation (tutorial later).
Rooms you select will be cleaned in the order you select. Two-pass functionailty coming soon as well.
![Added the config!](img/rooms.png)
Room types and names are also dynamically imported as to be expected.
To work with this, switch the "Clean (room)" switches on in the order you like, then press the Clean button from the vacuum's entity!
![Room selection](img/clean.png)
## Step 4: Rooms! (rest980 ONLY)
TO BE CONTINUED...
@ -177,7 +197,7 @@ Rooms are not given to us easily when we're fully local, but a fix is in progres
## Important Note
From this part on, the guide will not diverge unless required and will assume you are using Cloud features, but most of it should be generically implemented.
From this part on, the guide will not diverge into Cloud/Local unless required and will assume you are using Cloud features, but most of it should be generically implemented.
## Step 5: Robot Maintenance / Done!
@ -185,18 +205,22 @@ From this part on, the guide will not diverge unless required and will assume yo
This integration will eventually support the maintenance function of the Roomba, but still is not implemented
## Cleaning a room using the Roomba from HA
> Unfortunately, this is not implemented yet, alongside any other action.
In any configuration you'd like, you may lay the switches on the dashboard and switch them in the order you want them cleaned. After that, press Start on the native Vacuum!
## Note:
Unfortunately, this is about where my current progress ends. We gather all the possible data and display it. I will be working on this integration however and eventually these features will be fully supported.
## Backwards Compatibility
Minus the actions (currently!), the integration adds all the attributes that you would expect from `jeremywillans` implementation, even adding an `extendedState` attribute that gives you "Ready", "Training", "Spot", etc. since HA doesnt do that natively for some odd reason.
The integration adds all the attributes that you would expect from [jeremywillans implementation](https://github.com/jeremywillans/ha-rest980-roomba), making it compatible with [the lovelace-roomba-vacuum-card](https://github.com/jeremywillans/lovelace-roomba-vacuum-card).
![Compatibility](img/compat.png)
One minor issue is that the Vacuum entity only supports these states:
```
Cleaning: The vacuum is currently cleaning.
Docked: The vacuum is currently docked. It is assumed that docked can also mean charging.
Error: The vacuum encountered an error while cleaning.
Idle: The vacuum is not paused, not docked, and does not have any errors.
Paused: The vacuum was cleaning but was paused without returning to the dock.
Returning: The vacuum is done cleaning and is currently returning to the dock, but not yet docked.
Unavailable: The entity is currently unavailable.
Unknown: The state is not yet known.
```
even adding an `extendedState` attribute that gives you "Ready", "Training", "Spot", etc. since HA doesnt do that natively for some odd reason.

View file

@ -404,6 +404,10 @@ class iRobotCloudApi:
async with self.session.get(final_url, headers=signed_headers) as response:
if response.status != 200:
if response.status == 403:
await self.authenticate()
_LOGGER.info("Reauthenticating API")
return await self._aws_request(url, params)
raise CloudApiError(f"AWS request failed: {response.status}")
return await response.json()
@ -460,10 +464,16 @@ class iRobotCloudApi:
return robot_data
retry_count = 0
async def get_all_robots_data(self) -> dict[str, dict[str, Any]]:
"""Get data for all authenticated robots."""
if not self.robots:
raise CloudApiError("No robots found. Authenticate first.")
if self.retry_count == 3:
raise CloudApiError("No robots found. Authenticate first.")
self.retry_count += 1
await self.authenticate()
return await self.get_all_robots_data()
all_data = {}
for blid in self.robots:
@ -475,3 +485,115 @@ class iRobotCloudApi:
all_data[blid] = {"error": str(e)}
return all_data
"""
active_pmapv_details:
active_pmapv:
pmap_id: BGQxV6zGTmCsalWFHr-S5g
pmapv_id: 250720T215523
create_time: 1753048538
proc_state: OK_Processed
creator: robot
nMssn: 1182
mission_id: 01K0MC4XWG0DKT67MCSGGG4924
learning_percentage: 100
last_user_pmapv_id: 250718T074805
last_user_ts: 1752824885
shareability: 1
robot_cap:
maps: 3
pmaps: 10
robot_id: B61489C9D5104793AFEA1F26C91B61DF
map_header:
id: BGQxV6zGTmCsalWFHr-S5g
version: 250720T215523
name: Main Floor
learning_percentage: 100
create_time: 1753048538
resolution: 0.105
user_orientation_rad: 1.5634
robot_orientation_rad: 3.1457
area: 38.1418
nmssn: 1182
mission_id: 01K0MC4XWG0DKT67MCSGGG4924
regions:
- id: '11'
name: Kitchen
region_type: kitchen
policies:
odoa_mode: 0
odoa_feats: {}
disabled_operating_modes: 0
override_operating_modes: 0
time_estimates:
- unit: seconds
estimate: 420
confidence: GOOD_CONFIDENCE
params:
noAutoPasses: true
twoPass: true
- unit: seconds
estimate: 210
confidence: GOOD_CONFIDENCE
params:
noAutoPasses: true
twoPass: false
- id: '15'
name: ''
region_type: unspecified
policies:
odoa_mode: 0
odoa_feats: {}
disabled_operating_modes: 0
override_operating_modes: 0
time_estimates:
- unit: seconds
estimate: 458
confidence: GOOD_CONFIDENCE
params:
noAutoPasses: true
twoPass: true
- unit: seconds
estimate: 229
confidence: GOOD_CONFIDENCE
params:
noAutoPasses: true
twoPass: false
- id: '10'
name: Hallway
region_type: hallway
policies:
odoa_mode: 0
odoa_feats: {}
disabled_operating_modes: 0
override_operating_modes: 0
time_estimates:
- unit: seconds
estimate: 1282
confidence: GOOD_CONFIDENCE
params:
noAutoPasses: true
twoPass: true
- unit: seconds
estimate: 641
confidence: GOOD_CONFIDENCE
params:
noAutoPasses: true
twoPass: false
zones: []
keepoutzones: []
observed_zones:
- id: '1449649421'
extent_type: rug
quality:
confidence: 70
related_objects:
- '1449649421'
- id: '1048295640'
extent_type: rug
quality:
confidence: 70
related_objects:
- '10482956"""

View file

@ -3,10 +3,12 @@
import logging
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.core import HomeAssistant, ServiceCall
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from .const import DOMAIN
from .coordinator import RoombaDataCoordinator, RoombaCloudCoordinator
import voluptuous as vol
CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN)
@ -21,34 +23,83 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
hass.data.setdefault(DOMAIN, {})
hass.data[DOMAIN][entry.entry_id + "_coordinator"] = coordinator
hass.data[DOMAIN][entry.entry_id + "_blid"] = "unknown"
# Set up cloud coordinator if enabled
if entry.data["cloud_api"]:
cc = RoombaCloudCoordinator(hass, entry)
await cc.async_config_entry_first_refresh()
hass.data[DOMAIN][entry.entry_id + "_cloud"] = cc
if "blid" not in entry.data:
for blid, robo in cc.data.items():
try:
ifo = robo["robot_info"] or {}
ifosku = ifo.get("sku")
ifoswv = ifo.get("softwareVer")
ifoname = ifo.get("name")
thisname = coordinator.data.get("name", "Roomba")
thisswv = coordinator.data.get("softwareVer")
thissku = coordinator.data.get("sku")
if ifosku == thissku and ifoswv == thisswv and ifoname == thisname:
hass.data[DOMAIN][entry.entry_id + "_blid"] = blid
except Exception as e:
_LOGGER.debug(e)
# Start background task for cloud setup and BLID matching
hass.async_create_task(_async_setup_cloud(hass, entry, coordinator, cc))
else:
hass.data[DOMAIN][entry.entry_id + "_cloud"] = {}
# Register services
await _async_register_services(hass)
# Forward platforms; create tasks but await to ensure no failure?
await hass.config_entries.async_forward_entry_setups(
entry, ["vacuum", "sensor", "switch"]
)
await hass.config_entries.async_forward_entry_setups(entry, ["vacuum", "sensor"])
return True
async def _async_register_services(hass: HomeAssistant) -> None:
"""Register integration services."""
async def handle_vacuum_clean(call: ServiceCall) -> None:
"""Handle vacuum clean service call."""
try:
payload = call.data["payload"]
base_url = call.data["base_url"]
session = async_get_clientsession(hass)
async with session.post(
f"{base_url}/api/local/action/cleanRoom",
headers={"content-type": "application/json"},
json=payload,
) as response:
if response.status != 200:
_LOGGER.error("Failed to send clean command: %s", response.status)
else:
_LOGGER.debug("Clean command sent successfully")
except Exception as e: # pylint: disable=broad-except
_LOGGER.error("Error sending clean command: %s", e)
async def handle_action(call: ServiceCall) -> None:
"""Handle action service call."""
try:
action = call.data["action"]
base_url = call.data["base_url"]
session = async_get_clientsession(hass)
async with session.get(
f"{base_url}/api/local/action/{action}",
) as response:
if response.status != 200:
_LOGGER.error("Failed to send clean command: %s", response.status)
else:
_LOGGER.debug("Action sent successfully")
except Exception as e: # pylint: disable=broad-except
_LOGGER.error("Error sending clean command: %s", e)
# Register services if not already registered
if not hass.services.has_service(DOMAIN, "rest980_clean"):
hass.services.async_register(
DOMAIN,
"rest980_clean",
handle_vacuum_clean,
vol.Schema({vol.Required("payload"): dict, vol.Required("base_url"): str}),
)
if not hass.services.has_service(DOMAIN, "rest980_action"):
hass.services.async_register(
DOMAIN,
"rest980_action",
handle_action,
vol.Schema({vol.Required("action"): str, vol.Required("base_url"): str}),
)
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Safely remove Roombas."""
await hass.config_entries.async_unload_platforms(
@ -56,3 +107,67 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
)
hass.data[DOMAIN].pop(entry.entry_id + "_coordinator")
return True
async def _async_setup_cloud(
hass: HomeAssistant,
entry: ConfigEntry,
coordinator: RoombaDataCoordinator,
cloud_coordinator: RoombaCloudCoordinator,
) -> None:
"""Set up cloud coordinator and perform BLID matching in background."""
try:
# Refresh cloud data
await cloud_coordinator.async_config_entry_first_refresh()
# Perform BLID matching if not already stored
if "blid" not in entry.data:
await _async_match_blid(hass, entry, coordinator, cloud_coordinator)
await hass.config_entries.async_forward_entry_setups(entry, ["switch"])
except Exception as e: # pylint: disable=broad-except
_LOGGER.error("Failed to set up cloud coordinator: %s", e)
# Set empty cloud data so entities can still be created
hass.data[DOMAIN][entry.entry_id + "_cloud"] = {}
async def _async_match_blid(
hass: HomeAssistant,
entry: ConfigEntry,
coordinator: RoombaDataCoordinator,
cloud_coordinator: RoombaCloudCoordinator,
) -> None:
"""Match local Roomba with cloud robot by comparing device info."""
try:
for blid, robo in cloud_coordinator.data.items():
try:
# Get cloud robot info
robot_info = robo.get("robot_info") or {}
cloud_sku = robot_info.get("sku")
cloud_sw_ver = robot_info.get("softwareVer")
cloud_name = robot_info.get("name")
# Get local robot info
local_data = coordinator.data or {}
local_name = local_data.get("name", "Roomba")
local_sw_ver = local_data.get("softwareVer")
local_sku = local_data.get("sku")
# Match robots by SKU, software version, and name
if (
cloud_sku == local_sku
and cloud_sw_ver == local_sw_ver
and cloud_name == local_name
):
hass.data[DOMAIN][entry.entry_id + "_blid"] = blid
_LOGGER.info("Matched local Roomba with cloud robot %s", blid)
break
except (KeyError, TypeError) as e:
_LOGGER.debug("Error matching robot %s: %s", blid, e)
else:
_LOGGER.warning("Could not match local Roomba with any cloud robot")
except Exception as e: # pylint: disable=broad-except
_LOGGER.error("Error during BLID matching: %s", e)

View file

@ -11,8 +11,15 @@ notReadyMappings = {
15: "Low Battery",
39: "Pending",
48: "Path Blocked",
68: "Updating Map",
}
errorMappings = {
0: "n-a",
15: "Reboot Required",
18: "Docking Issue",
68: "Updating Map",
}
errorMappings = {0: "n-a", 15: "Reboot Required", 18: "Docking Issue"}
cycleMappings = {
"clean": "Clean",
@ -61,3 +68,41 @@ jobInitiatorMappings = {
"localApp": "HA",
"none": "None", # Added for RoombaJobInitiator
}
regionTypeMappings = {
"default": "mdi:map-marker",
"custom": "mdi:face-agent",
"basement": "mdi:home-floor-b",
"bathroom": "mdi:shower",
"bedroom": "mdi:bed-king",
"breakfast_room": "mdi:silverware-fork-knief",
"closet": "mdi:hanger",
"den": "mdi:sofa-single",
"dining_room": "mdi:silverware-fork-knife",
"entryway": "mdi:door-open",
"family_room": "mdi:sofa-single",
"foyer": "mdi:door-open",
"garage": "mdi:garage",
"guest_bathroom": "mdi:shower",
"guest_bedroom": "mdi:bed-king",
"hallway": "mdi:shoe-print",
"kitchen": "mdi:fridge",
"kids_room": "mdi:teddy-bear",
"laundry_room": "mdi:washing-machine",
"living_room": "mdi:sofa",
"lounge": "mdi:sofa",
"media_room": "mdi:television",
"mud_room": "mdi:landslide",
"office": "mdi:chair-rolling",
"outside": "mdi:asterisk",
"pantry": "mdi:archive",
"playroom": "mdi:teddy-bear",
"primary_bathroom": "mdi:shower",
"primary_bedroom": "mdi:bed-king",
"recreation_room": "mdi:sofa",
"storage_room": "mdi:archive",
"study": "mdi:bookshelf",
"sun_room": "mdi:sun-angle",
"unfinished_basement": "mdi:home-floor-b",
"workshop": "mdi:toolbox",
}

View file

@ -1,5 +1,7 @@
"""Create sensors that poll Roomba's data."""
import logging
from homeassistant.components.sensor import SensorDeviceClass
from homeassistant.const import PERCENTAGE, UnitOfArea, UnitOfTime
from homeassistant.core import HomeAssistant
@ -9,11 +11,37 @@ from homeassistant.util import dt as dt_util
from .const import DOMAIN, cleanBaseMappings, jobInitiatorMappings, phaseMappings
from .RoombaSensor import RoombaSensor, RoombaCloudSensor
_LOGGER = logging.getLogger(__name__)
async def async_setup_entry(hass: HomeAssistant, entry, async_add_entities):
"""Create the sensors needed to poll Roomba's data."""
coordinator = hass.data[DOMAIN][entry.entry_id + "_coordinator"]
cloudCoordinator = hass.data[DOMAIN][entry.entry_id + "_cloud"]
# Create cloud pmap entities if cloud data is available
cloud_entities = []
if cloudCoordinator and cloudCoordinator.data:
blid = hass.data[DOMAIN][entry.entry_id + "_blid"]
# Get cloud data for the specific robot
if blid in cloudCoordinator.data:
cloud_data = cloudCoordinator.data[blid]
# Create pmap entities from cloud data
if "pmaps" in cloud_data:
for pmap in cloud_data["pmaps"]:
try:
cloud_entities.append(
RoombaCloudPmap(cloudCoordinator, entry, pmap)
)
except (KeyError, TypeError) as e:
_LOGGER.warning(
"Failed to create pmap entity for %s: %s",
pmap.get("pmap_id", "unknown"),
e,
)
if cloud_entities:
async_add_entities(cloud_entities)
async_add_entities(
[
RoombaAttributes(coordinator, entry),
@ -116,6 +144,21 @@ class RoombaCloudAttributes(RoombaCloudSensor):
)
class RoombaCloudPmap(RoombaCloudSensor):
"""Sensor for Roomba persistent map (pmap) data from cloud."""
def __init__(self, coordinator, entry, pmap) -> None:
"""Initialize the pmap sensor with data from cloud API."""
# Handle different pmap data structures
header = pmap["active_pmapv_details"]["map_header"]
pmap_name = header.get("name", "Unknown Map")
pmap_id = header.get("id", "unknown")
self._rs_given_info = (pmap_name, pmap_id)
super().__init__(coordinator, entry)
self._attr_extra_state_attributes = pmap
class RoombaPhase(RoombaSensor):
"""A simple sensor that returns the phase of the Roomba."""

View file

@ -1,11 +1,11 @@
# services.yaml
vacuum_action:
action:
fields:
command:
required: true
example: "dock"
vacuum_clean:
clean:
fields:
payload:
required: true

View file

@ -3,21 +3,37 @@
from homeassistant.components.switch import SwitchEntity
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity import EntityCategory
import logging
from .const import DOMAIN, regionTypeMappings
from .const import DOMAIN
ROOMS = {
"Kitchen": {"region_id": "11", "type": "rid"},
"Living Room": {"region_id": "9", "type": "rid"},
"Dining Room": {"region_id": "1", "type": "rid"},
"Hallway": {"region_id": "10", "type": "rid"},
"Office": {"region_id": "12", "type": "rid"},
}
_LOGGER = logging.getLogger(__name__)
async def async_setup_entry(hass: HomeAssistant, entry, async_add_entities):
"""Create the switches to identify cleanable rooms."""
entities = [RoomSwitch(entry, name, data) for name, data in ROOMS.items()]
cloudCoordinator = hass.data[DOMAIN][entry.entry_id + "_cloud"]
entities = []
if cloudCoordinator and cloudCoordinator.data:
blid = hass.data[DOMAIN][entry.entry_id + "_blid"]
# Get cloud data for the specific robot
if blid in cloudCoordinator.data:
cloud_data = cloudCoordinator.data[blid]
# Create pmap entities from cloud data
if "pmaps" in cloud_data:
for pmap in cloud_data["pmaps"]:
try:
for region in pmap["active_pmapv_details"]["regions"]:
entities.append(
RoomSwitch(
entry, region["name"] or "Unnamed Room", region
)
)
except (KeyError, TypeError) as e:
_LOGGER.warning(
"Failed to create pmap entity for %s: %s",
pmap.get("pmap_id", "unknown"),
e,
)
for ent in entities:
hass.data[DOMAIN][f"switch.{ent.unique_id}"] = ent
async_add_entities(entities)
@ -29,15 +45,21 @@ class RoomSwitch(SwitchEntity):
def __init__(self, entry, name, data) -> None:
"""Creates a switch entity for rooms."""
self._attr_name = f"Clean {name}"
self._attr_unique_id = f"{entry.entry_id}_{name.lower().replace(' ', '_')}"
self._attr_unique_id = f"{entry.entry_id}_{data['id']}"
self._is_on = False
self._room_json = data
self._room_json = {"region_id": data["id"], "type": "rid"}
self._attr_entity_category = EntityCategory.CONFIG
self._attr_extra_state_attributes = data
self._attr_device_info = {
"identifiers": {(DOMAIN, entry.unique_id)},
"name": entry.title,
"manufacturer": "iRobot",
}
# autodetect icon
icon = regionTypeMappings.get(
data["region_type"], regionTypeMappings.get("default")
)
self._attr_icon = icon
@property
def is_on(self):
@ -47,13 +69,20 @@ class RoomSwitch(SwitchEntity):
async def async_turn_on(self, **kwargs):
"""Yes."""
self._is_on = True
if self not in order_switched:
order_switched.append(self)
self.async_write_ha_state()
async def async_turn_off(self, **kwargs):
"""No."""
self._is_on = False
if self in order_switched:
order_switched.remove(self)
self.async_write_ha_state()
def get_region_json(self):
"""I'm not sure what this does to be honest."""
"""Return robot-readable JSON to identify the room to start cleaning it."""
return self._room_json
order_switched: list[RoomSwitch] = []

View file

@ -1,8 +1,5 @@
"""The vacuum."""
from datetime import datetime
import json
import logging
from homeassistant.components.vacuum import (
@ -15,7 +12,6 @@ from homeassistant.core import HomeAssistant
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from .const import DOMAIN
from .LegacyCompatibility import createExtendedAttributes
_LOGGER = logging.getLogger(__name__)
@ -27,7 +23,8 @@ SUPPORT_ROBOT = (
| VacuumEntityFeature.MAP
| VacuumEntityFeature.SEND_COMMAND
| VacuumEntityFeature.STATE
| VacuumEntityFeature.STATUS
| VacuumEntityFeature.STOP
| VacuumEntityFeature.PAUSE
)
@ -60,6 +57,7 @@ class RoombaVacuum(CoordinatorEntity, StateVacuumEntity):
data = self.coordinator.data or {}
status = data.get("cleanMissionStatus", {})
cycle = status.get("cycle")
phase = status.get("phase")
not_ready = status.get("notReady")
self._attr_activity = VacuumActivity.IDLE
@ -69,8 +67,14 @@ class RoombaVacuum(CoordinatorEntity, StateVacuumEntity):
self._attr_activity = VacuumActivity.ERROR
if cycle in ["clean", "quick", "spot", "train"]:
self._attr_activity = VacuumActivity.CLEANING
if cycle in ["evac", "dock"]:
if cycle in ["evac", "dock"]: # Emptying Roomba Bin to Dock, Entering Dock
self._attr_activity = VacuumActivity.DOCKED
if phase in {
"hmUsrDock",
"hwMidMsn",
"hmPostMsn",
}: # Sent Home, Mid Dock, Final Dock
self._attr_activity = VacuumActivity.RETURNING
self._attr_available = data != {}
self._attr_extra_state_attributes = createExtendedAttributes(self)
@ -88,37 +92,99 @@ class RoombaVacuum(CoordinatorEntity, StateVacuumEntity):
async def async_start(self):
"""Start cleaning floors, check if any are selected or just clean everything."""
payload = []
for entity in self.hass.states.async_all("switch"):
if entity.entity_id.startswith("switch.clean_") and entity.state == "on":
switch_obj = self.hass.data[DOMAIN].get(entity.entity_id)
if switch_obj:
payload.append(switch_obj.get_region_json())
if payload:
# TODO: FIX THIS FIX THIS IT NEEDS TO BE DYNAMIC NOT THIS GARBAGE
# TODO: FIX THIS FIX THIS IT NEEDS TO BE DYNAMIC NOT THIS GARBAGE
# TODO: FIX THIS FIX THIS IT NEEDS TO BE DYNAMIC NOT THIS GARBAGE
data = self.coordinator.data or {}
if data.get("phase") == "stop":
await self.hass.services.async_call(
DOMAIN,
"vacuum_clean",
"rest980_action",
service_data={
"payload": json.dumps(
{
"ordered": 1,
"pmap_id": "BGQxV6zGTmCsalWFHr-S5g",
"regions": payload,
}
)
"action": "resume",
"base_url": self._entry.data["base_url"],
},
blocking=True,
)
else:
_LOGGER.warning("No rooms selected for cleaning")
return
try:
# Get selected rooms from switches (if available)
payload = []
regions = []
# Check if we have room selection switches available
if hasattr(self, "_selected_rooms") and self._selected_rooms:
# Build regions list from selected rooms
regions = [
room.get_region_json()
for room in self._selected_rooms
if hasattr(room, "get_region_json")
]
# If we have specific regions selected, use targeted cleaning
if regions:
payload = {
"ordered": 1,
"pmap_id": self._attr_extra_state_attributes.get("pmap0_id", ""),
"regions": regions,
}
await self.hass.services.async_call(
DOMAIN,
"rest980_clean",
service_data={
"payload": payload,
"base_url": self._entry.data["base_url"],
},
blocking=True,
)
else:
# No specific rooms selected, start general clean
_LOGGER.info("Starting general cleaning (no specific rooms selected)")
await self.hass.services.async_call(
DOMAIN,
"rest980_clean",
service_data={
"payload": {"action": "start"},
"base_url": self._entry.data["base_url"],
},
blocking=True,
)
except (KeyError, AttributeError, ValueError) as e:
_LOGGER.error("Failed to start cleaning due to configuration error: %s", e)
except Exception as e: # pylint: disable=broad-except
_LOGGER.error("Failed to start cleaning: %s", e)
async def async_stop(self):
"""Stop the action."""
await self.hass.services.async_call(
DOMAIN,
"rest980_action",
service_data={
"action": "stop",
"base_url": self._entry.data["base_url"],
},
blocking=True,
)
async def async_pause(self):
"""Pause the current action."""
await self.hass.services.async_call(
DOMAIN,
"rest980_action",
service_data={
"action": "pause",
"base_url": self._entry.data["base_url"],
},
blocking=True,
)
async def async_return_to_base(self):
"""Calls the Roomba back to its dock."""
await self.hass.services.async_call(
DOMAIN, "vacuum_action", service_data={"command": "dock"}, blocking=True
DOMAIN,
"rest980_action",
service_data={
"action": "dock",
"base_url": self._entry.data["base_url"],
},
blocking=True,
)

BIN
img/clean.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

BIN
img/rooms.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 91 KiB