move to runtime_data and work on bronze

This commit is contained in:
ia74 2025-08-09 00:49:29 -05:00
parent d310a8441f
commit b035324c2f
13 changed files with 394 additions and 119 deletions

View file

@ -444,6 +444,9 @@ class iRobotCloudApi:
umf_data = await self._aws_request(url, params)
# Save UMF data for debugging/camera development
await self._save_umf_data_for_debug(pmap_id, umf_data)
return umf_data
async def get_favorites(self) -> dict[str, Any]:
@ -499,6 +502,49 @@ class iRobotCloudApi:
all_data["favorites"] = await self.get_favorites()
return all_data
async def _save_umf_data_for_debug(
self, pmap_id: str, umf_data: dict[str, Any]
) -> None:
"""Save UMF data to file for debugging purposes."""
if not DEBUG_SAVE_UMF:
return
try:
# Create debug data structure
debug_data = {
"timestamp": datetime.now(UTC).isoformat(),
"pmap_id": pmap_id,
"umf_data": umf_data,
}
# Load existing data if file exists
existing_data = []
if DEBUG_UMF_PATH.exists():
try:
async with aiofiles.open(DEBUG_UMF_PATH) as f:
content = await f.read()
existing_data = json.loads(content)
if not isinstance(existing_data, list):
existing_data = [existing_data]
except (json.JSONDecodeError, OSError):
existing_data = []
# Add new data
existing_data.append(debug_data)
# Keep only the latest 10 entries to avoid huge files
if len(existing_data) > 10:
existing_data = existing_data[-10:]
# Save back to file
async with aiofiles.open(DEBUG_UMF_PATH, "w") as f:
await f.write(json.dumps(existing_data, indent=2, default=str))
_LOGGER.debug("Saved UMF data for pmap %s to %s", pmap_id, DEBUG_UMF_PATH)
except (OSError, json.JSONEncodeError) as e:
_LOGGER.warning("Failed to save UMF debug data: %s", e)
"""

View file

@ -5,7 +5,8 @@ import logging
from homeassistant.components.sensor import SensorEntity
from homeassistant.helpers.entity import EntityCategory
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers import device_registry as dr
from .const import DOMAIN
_LOGGER = logging.getLogger(__name__)
@ -14,6 +15,7 @@ _LOGGER = logging.getLogger(__name__)
class RoombaSensor(CoordinatorEntity, SensorEntity):
"""Generic Roomba sensor to provide coordinator."""
_attr_has_entity_name = True
_rs_given_info: tuple[str, str] = ("Sensor", "sensor")
def __init__(self, coordinator, entry) -> None:
@ -25,11 +27,19 @@ class RoombaSensor(CoordinatorEntity, SensorEntity):
self._attr_has_entity_name = True
self._attr_name = self._rs_given_info[0]
self._attr_unique_id = f"{entry.unique_id}_{self._rs_given_info[1]}"
self._attr_device_info = {
"identifiers": {(DOMAIN, entry.unique_id)},
"name": entry.title,
"manufacturer": "iRobot",
}
@property
def device_info(self) -> DeviceInfo:
"""Return the Roomba's device information."""
data = self.coordinator.data or {}
return DeviceInfo(
identifiers={DOMAIN, self._entry.unique_id},
name=data.get("name", "Roomba"),
manufacturer="iRobot",
model="Roomba",
model_id=data.get("sku"),
sw_version=data.get("softwareVer"),
)
def isMissionActive(self) -> bool:
"""Return whether or not there is a mission in progress."""
@ -56,6 +66,7 @@ class RoombaSensor(CoordinatorEntity, SensorEntity):
class RoombaCloudSensor(CoordinatorEntity, SensorEntity):
"""Generic Roomba sensor to provide coordinator."""
_attr_has_entity_name = True
_rs_given_info: tuple[str, str] = ("Sensor", "sensor")
def __init__(self, coordinator, entry) -> None:
@ -67,10 +78,18 @@ class RoombaCloudSensor(CoordinatorEntity, SensorEntity):
self._attr_has_entity_name = True
self._attr_name = self._rs_given_info[0]
self._attr_unique_id = f"{entry.unique_id}_{self._rs_given_info[1]}"
self._attr_device_info = {
"identifiers": {(DOMAIN, entry.unique_id)},
"name": entry.title,
"manufacturer": "iRobot",
}
if not entry.data["cloud_api"]:
self._attr_available = False
@property
def device_info(self) -> DeviceInfo:
"""Return the Roomba's device information."""
data = self.coordinator.data or {}
return DeviceInfo(
identifiers={DOMAIN, self._entry.unique_id},
name=data.get("name", "Roomba"),
manufacturer="iRobot",
model="Roomba",
model_id=data.get("sku"),
sw_version=data.get("softwareVer"),
)

View file

@ -11,30 +11,66 @@ from homeassistant.helpers.aiohttp_client import async_get_clientsession
from .const import DOMAIN
from .coordinator import RoombaCloudCoordinator, RoombaDataCoordinator
from .switch import RoomSwitch
CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN)
_LOGGER = logging.getLogger(__name__)
class RoombaRuntimeData:
"""Setup the runtime data structure."""
local_coordinator: RoombaDataCoordinator = None
robot_blid: str = None
cloud_enabled: bool = False
cloud_coordinator: RoombaCloudCoordinator = None
switched_rooms: dict[str, RoomSwitch] = {}
def __init__(
self,
local_coordinator: RoombaDataCoordinator,
robot_blid: str,
cloud_enabled: bool,
cloud_coordinator: RoombaCloudCoordinator,
) -> None:
"""Initialize the class with given data."""
self.local_coordinator = local_coordinator
self.robot_blid = robot_blid
self.cloud_enabled = cloud_enabled
self.cloud_coordinator = cloud_coordinator
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Setup Roombas with the Rest980 base url."""
coordinator = RoombaDataCoordinator(hass, entry)
cloud_coordinator: RoombaCloudCoordinator = None
await coordinator.async_config_entry_first_refresh()
hass.data.setdefault(DOMAIN, {})
hass.data[DOMAIN][entry.entry_id + "_coordinator"] = coordinator
hass.data[DOMAIN][entry.entry_id + "_blid"] = "unknown"
entry.runtime_data = RoombaRuntimeData(
local_coordinator=coordinator,
robot_blid=None,
cloud_enabled=entry.data["cloud_api"],
cloud_coordinator=cloud_coordinator,
)
# Set up cloud coordinator if enabled
if entry.data["cloud_api"]:
cc = RoombaCloudCoordinator(hass, entry)
hass.data[DOMAIN][entry.entry_id + "_cloud"] = cc
cloud_coordinator = RoombaCloudCoordinator(hass, entry)
await cloud_coordinator.async_config_entry_first_refresh()
# Start background task for cloud setup and BLID matching
hass.async_create_task(_async_setup_cloud(hass, entry, coordinator, cc))
hass.async_create_task(
_async_setup_cloud(hass, entry, coordinator, cloud_coordinator)
)
# Update runtime data with cloud coordinator
entry.runtime_data.cloud_coordinator = cloud_coordinator
else:
hass.data[DOMAIN][entry.entry_id + "_cloud"] = {}
cloud_coordinator = None
# Register services
await _async_register_services(hass)
@ -107,7 +143,6 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
await hass.config_entries.async_unload_platforms(
entry, ["vacuum", "sensor", "switch", "button", "camera"]
)
hass.data[DOMAIN].pop(entry.entry_id + "_coordinator")
return True
@ -122,9 +157,20 @@ async def _async_setup_cloud(
# 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)
# Perform BLID matching only if not already stored in config entry
if "robot_blid" not in entry.data:
matched_blid = await _async_match_blid(
hass, entry, coordinator, cloud_coordinator
)
if matched_blid:
# Store the matched BLID permanently in config entry data
hass.config_entries.async_update_entry(
entry, data={**entry.data, "robot_blid": matched_blid}
)
entry.runtime_data.robot_blid = matched_blid
else:
# Use stored BLID from config entry
entry.runtime_data.robot_blid = entry.data["robot_blid"]
await hass.config_entries.async_forward_entry_setups(
entry, ["switch", "button", "camera"]
@ -132,8 +178,7 @@ async def _async_setup_cloud(
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"] = {}
cloud_coordinator = None
async def _async_match_blid(
@ -164,7 +209,7 @@ async def _async_match_blid(
and cloud_sw_ver == local_sw_ver
and cloud_name == local_name
):
hass.data[DOMAIN][entry.entry_id + "_blid"] = blid
entry.runtime_data.robot_blid = blid
_LOGGER.info("Matched local Roomba with cloud robot %s", blid)
break

View file

@ -13,10 +13,10 @@ _LOGGER = logging.getLogger(__name__)
async def async_setup_entry(hass: HomeAssistant, entry, async_add_entities):
"""Create the switches to identify cleanable rooms."""
cloudCoordinator = hass.data[DOMAIN][entry.entry_id + "_cloud"]
cloudCoordinator = entry.runtime_data.cloud_coordinator
entities = []
if cloudCoordinator and cloudCoordinator.data:
blid = hass.data[DOMAIN][entry.entry_id + "_blid"]
blid = entry.runtime_data.robot_blid
# Get cloud data for the specific robot
if blid in cloudCoordinator.data:
cloud_data = cloudCoordinator.data
@ -35,7 +35,7 @@ class FavoriteButton(ButtonEntity):
"""Creates a button entity for entries."""
self._attr_name = f"{data['name']}"
self._entry = entry
self._attr_unique_id = f"{entry.entry_id}_{data['favorite_id']}"
self._attr_unique_id = f"{entry.unique_id}_{data['favorite_id']}"
self._attr_entity_category = EntityCategory.CONFIG
self._attr_extra_state_attributes = data
self._data = data

View file

@ -46,9 +46,9 @@ async def async_setup_entry(
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
) -> None:
"""Set up Roomba map camera."""
_LOGGER.debug("Setting up camera platform for entry %s", entry.entry_id)
_LOGGER.debug("Setting up camera platform for entry %s", entry.unique_id)
cloudCoordinator = hass.data[DOMAIN].get(entry.entry_id + "_cloud")
cloudCoordinator = entry.runtime_data.cloud_coordinator
if not cloudCoordinator:
_LOGGER.warning("No cloud coordinator found for camera setup")
@ -59,7 +59,7 @@ async def async_setup_entry(
return
entities = []
blid = hass.data[DOMAIN].get(entry.entry_id + "_blid", "unknown")
blid = entry.runtime_data.robot_blid
_LOGGER.debug("Using BLID: %s for camera setup", blid)
if blid != "unknown" and blid in cloudCoordinator.data:
@ -131,7 +131,7 @@ class RoombaMapCamera(Camera):
# Camera attributes
self._attr_name = f"Roomba Map - {self._map_header.get('name', 'Unknown')}"
self._attr_unique_id = f"{entry.entry_id}_map_{pmap_id}"
self._attr_unique_id = f"{entry.unique_id}_map_{pmap_id}"
# Device info
self._attr_device_info = {

View file

@ -1,21 +1,32 @@
"""The configuration flow for the robot."""
import asyncio
import hashlib
import logging
from aiohttp import ClientConnectorError, ClientError
import voluptuous as vol
from homeassistant import config_entries
from homeassistant.config_entries import ConfigFlowResult
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from .CloudApi import iRobotCloudApi
from .CloudApi import AuthenticationError, iRobotCloudApi
from .const import DOMAIN
SCHEMA = vol.Schema(
_LOGGER = logging.getLogger(__name__)
USER_SCHEMA = vol.Schema(
{
vol.Required("base_url"): str,
vol.Required("cloud_api", default=True): bool,
vol.Optional("irobot_username"): str,
vol.Optional("irobot_password"): str,
}
)
CLOUD_SCHEMA = vol.Schema(
{
vol.Required("irobot_username"): str,
vol.Required("irobot_password"): str,
}
)
@ -23,46 +34,90 @@ SCHEMA = vol.Schema(
class RoombaConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
"""Config flow."""
async def async_step_user(self, user_input=None):
VERSION = 2
_proposed_name: str
_user_data: dict[str, any]
async def async_step_cloud(self, user_input=None) -> ConfigFlowResult:
"""Show user the setup for the cloud API."""
if user_input is not None:
errors = {}
async with iRobotCloudApi(
user_input["irobot_username"], user_input["irobot_password"]
) as api:
try:
await api.authenticate()
except AuthenticationError:
errors["base"] = "cloud_authentication_error"
except Exception: # Allowed in config flow for robustness
errors["base"] = "unknown"
if errors:
return self.async_show_form(
step_id="cloud",
data_schema=CLOUD_SCHEMA,
errors=errors,
)
if hasattr(self, "_user_data"):
data = {**self._user_data, **user_input}
return self.async_create_entry(
title=self._proposed_name,
data=data,
)
return self.async_abort(reason="missing_user_data")
return self.async_show_form(step_id="cloud", data_schema=CLOUD_SCHEMA)
async def test_local(self, user_input):
"""Test connection to local rest980 API."""
session = async_get_clientsession(self.hass)
async with session.get(
f"{user_input['base_url']}/api/local/info/state"
) as resp:
data = await resp.json() or {}
if data == {}:
raise ValueError("No data returned from device")
unique_id = hashlib.md5(user_input["base_url"].encode()).hexdigest()[:8]
await self.async_set_unique_id(unique_id)
self._abort_if_unique_id_configured()
return data
async def async_step_user(self, user_input=None) -> ConfigFlowResult:
"""Show the user the input for the base url."""
if user_input is not None:
session = async_get_clientsession(self.hass)
data = {}
async with asyncio.timeout(10):
try:
async with session.get(
f"{user_input['base_url']}/api/local/info/state"
) as resp:
data = await resp.json() or {}
if data == {}:
return self.async_show_form(
step_id="user",
data_schema=SCHEMA,
errors=["cannot_connect"],
)
if user_input["cloud_api"]:
async with iRobotCloudApi(
user_input["irobot_username"], user_input["irobot_password"]
) as api:
await api.authenticate()
except Exception:
return self.async_show_form(
step_id="user",
data_schema=SCHEMA,
errors=["cannot_connect"],
)
errors = {}
try:
async with asyncio.timeout(10):
device_data = await self.test_local(user_input)
except TimeoutError:
errors["base"] = "local_cannot_connect"
except (ClientError, ClientConnectorError, OSError):
errors["base"] = "local_cannot_connect"
except ValueError:
errors["base"] = "local_connected_no_data"
except Exception: # Allowed in config flow for robustness
errors["base"] = "unknown"
return self.async_create_entry(
title=data.get("name", "Roomba"),
data=user_input,
)
if errors:
return self.async_show_form(
step_id="user",
data_schema=USER_SCHEMA,
errors=errors,
)
return self.async_show_form(step_id="user", data_schema=SCHEMA)
# Use device name if available, otherwise fall back to URL
device_name = device_data.get("name", "Roomba")
self._proposed_name = f"{device_name}"
async def async_step_options(self, user_input=None):
"""I dont know."""
return self.async_create_entry(
title="Room Switches Configured via UI",
data=self.options,
options=self.options,
)
if not user_input["cloud_api"]:
return self.async_create_entry(
title=self._proposed_name,
data=user_input,
)
# Store user data for use in cloud step
self._user_data = user_input
return self.async_show_form(step_id="cloud", data_schema=CLOUD_SCHEMA)
return self.async_show_form(step_id="user", data_schema=USER_SCHEMA)

View file

@ -14,11 +14,88 @@ notReadyMappings = {
68: "Updating Map",
}
## Some mappings thanks to https://github.com/NickWaterton/Roomba980-Python/blob/master/roomba/roomba.py
errorMappings = {
0: "n-a",
15: "Reboot Required",
18: "Docking Issue",
68: "Updating Map",
1: "Left wheel off floor",
2: "Main brushes stuck",
3: "Right wheel off floor",
4: "Left wheel stuck",
5: "Right wheel stuck",
6: "Stuck near a cliff",
7: "Left wheel error",
8: "Bin error",
9: "Bumper stuck",
10: "Right wheel error",
11: "Bin error",
12: "Cliff sensor issue",
13: "Both wheels off floor",
14: "Bin missing",
16: "Bumped unexpectedly",
17: "Path blocked",
19: "Undocking issue",
20: "Docking issue",
21: "Navigation problem",
22: "Navigation problem",
23: "Battery issue",
24: "Navigation problem",
25: "Reboot required",
26: "Vacuum problem",
27: "Vacuum problem",
29: "Software update needed",
30: "Vacuum problem",
31: "Reboot required",
32: "Smart map problem",
33: "Path blocked",
34: "Reboot required",
35: "Unrecognised cleaning pad",
36: "Bin full",
37: "Tank needed refilling",
38: "Vacuum problem",
39: "Reboot required",
40: "Navigation problem",
41: "Timed out",
42: "Localization problem",
43: "Navigation problem",
44: "Pump issue",
45: "Lid open",
46: "Low battery",
47: "Reboot required",
48: "Path blocked",
52: "Pad required attention",
53: "Software update required",
65: "Hardware problem detected",
66: "Low memory",
73: "Pad type changed",
74: "Max area reached",
75: "Navigation problem",
76: "Hardware problem detected",
88: "Back-up refused",
89: "Mission runtime too long",
101: "Battery isn't connected",
102: "Charging error",
103: "Charging error",
104: "No charge current",
105: "Charging current too low",
106: "Battery too warm",
107: "Battery temperature incorrect",
108: "Battery communication failure",
109: "Battery error",
110: "Battery cell imbalance",
111: "Battery communication failure",
112: "Invalid charging load",
114: "Internal battery failure",
115: "Cell failure during charging",
116: "Charging error of Home Base",
118: "Battery communication failure",
119: "Charging timeout",
120: "Battery not initialized",
122: "Charging system error",
123: "Battery not initialized",
216: "Charging base bag full",
}
cycleMappings = {
@ -31,15 +108,23 @@ cycleMappings = {
"none": "Ready",
}
## Some mappings thanks to https://github.com/NickWaterton/Roomba980-Python/blob/master/roomba/roomba.py
phaseMappings = {
"new": "New Mission",
"resume": "Resumed",
"recharge": "Recharging",
"completed": "Mission Completed",
"cancelled": "Cancelled",
"pause": "Paused",
"chargingerror": "Base Unplugged",
"charge": "Charge",
"run": "Run",
"evac": "Empty",
"stop": "Paused",
"stuck": "Stuck",
"hmUsrDock": "Sent Home",
"hmMidMsn": "Mid Dock",
"hmPostMsn": "Final Dock",
"hmMidMsn": "Docking",
"hmPostMsn": "Docking - Ending Job",
"idle": "Idle", # Added for RoombaPhase
"stopped": "Stopped", # Added for RoombaPhase
}
@ -57,7 +142,7 @@ cleanBaseMappings = {
351: "Clogged",
352: "Sealing Problem",
353: "Bag Full",
360: "Comms Problem",
360: "IR Comms Problem",
364: "Bin Full Sensors Not Cleared",
}

View file

@ -4,7 +4,6 @@
"codeowners": [
"@ia74"
],
"version": "1.9.0",
"config_flow": true,
"dependencies": [],
"dhcp": [
@ -27,11 +26,11 @@
],
"documentation": "https://github.com/ia74/roomba_rest980",
"iot_class": "local_polling",
"quality_scale": "bronze",
"requirements": [
"aiohttp",
"Pillow",
"aiofiles"
"aiofiles==24.1.0"
],
"version": "1.10.0",
"zeroconf": [
{
"type": "_amzn-alexa._tcp.local.",

View file

@ -0,0 +1,19 @@
rules:
# Bronze
action-setup: todo
appropriate-polling: done
brands: done
common-modules: done
config-flow-test-coverage: todo
config-flow: done
dependency-transparency: done
docs-actions: todo
docs-installation-instructions: todo
docs-removal-instructions: todo
entity-event-setup: done
entity-unique-id: done
has-entity-name: todo
runtime-data: todo
test-before-configure: todo
test-before-setup: todo
unique-config-entry: todo

View file

@ -16,13 +16,13 @@ _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"]
coordinator = entry.runtime_data.local_coordinator
cloudCoordinator = entry.runtime_data.cloud_coordinator
# Create cloud pmap entities if cloud data is available
cloud_entities = []
if cloudCoordinator and cloudCoordinator.data:
blid = hass.data[DOMAIN][entry.entry_id + "_blid"]
blid = entry.runtime_data.robot_blid
# Get cloud data for the specific robot
if blid in cloudCoordinator.data:
cloud_data = cloudCoordinator.data[blid]
@ -136,12 +136,7 @@ class RoombaCloudAttributes(RoombaCloudSensor):
@property
def extra_state_attributes(self):
"""Return all the attributes returned by iRobot's cloud."""
return (
self.coordinator.data.get(
self.hass.data[DOMAIN][self._entry.entry_id + "_blid"]
)
or {}
)
return self.coordinator.data.get(self._entry.runtime_data.robot_blid) or {}
class RoombaCloudPmap(RoombaCloudSensor):

View file

@ -13,10 +13,10 @@ _LOGGER = logging.getLogger(__name__)
async def async_setup_entry(hass: HomeAssistant, entry, async_add_entities):
"""Create the switches to identify cleanable rooms."""
cloudCoordinator = hass.data[DOMAIN][entry.entry_id + "_cloud"]
cloudCoordinator = entry.runtime_data.cloud_coordinator
entities = []
if cloudCoordinator and cloudCoordinator.data:
blid = hass.data[DOMAIN][entry.entry_id + "_blid"]
blid = entry.runtime_data.robot_blid
# Get cloud data for the specific robot
if blid in cloudCoordinator.data:
cloud_data = cloudCoordinator.data[blid]
@ -37,7 +37,7 @@ async def async_setup_entry(hass: HomeAssistant, entry, async_add_entities):
e,
)
for ent in entities:
hass.data[DOMAIN][f"switch.{ent.unique_id}"] = ent
entry.runtime_data.switched_rooms[f"switch.{ent.unique_id}"] = ent
async_add_entities(entities)
@ -47,7 +47,7 @@ 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}_{data['id']}"
self._attr_unique_id = f"{entry.unique_id}_{data['id']}"
self._is_on = False
self._room_json = {"region_id": data["id"], "type": "rid"}
self._attr_entity_category = EntityCategory.CONFIG

View file

@ -1,26 +1,34 @@
{
"common": {},
"config": {
"step": {
"user": {
"title": "Setup Roomba for integration",
"data": {
"base_url": "rest980 Base URL (ex: http://localhost:3000)",
"cloud_api": "Enable cloud features?",
"irobot_username": "iRobot Username",
"irobot_password": "iRobot Password"
"cloud_api": "Enable cloud features?"
},
"data_description": {
"cloud_api": "Use iRobot's API to add extra features like dynamic rooms and maps."
}
},
"cloud": {
"title": "Enter iRobot Home credentials",
"data": {
"irobot_username": "iRobot Username",
"irobot_password": "iRobot Password"
}
}
},
"error": {
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
"unknown": "[%key:common::config_flow::error::unknown%]"
"cloud_authentication_error": "Could not login to the iRobot Home API.",
"local_cannot_connect": "Could not connect to rest980 on that URL.",
"local_connected_no_data": "Rest980 did not output valid Roomba data.",
"unknown": "Unknown error occurred.",
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]"
},
"abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]"
"missing_user_data": "Forgot the local Rest980 data."
}
}
}

View file

@ -8,6 +8,7 @@ from homeassistant.components.vacuum import (
VacuumEntityFeature,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.core import HomeAssistant
from homeassistant.helpers.update_coordinator import CoordinatorEntity
@ -28,11 +29,13 @@ SUPPORT_ROBOT = (
)
async def async_setup_entry(hass: HomeAssistant, entry, async_add_entities):
async def async_setup_entry(
hass: HomeAssistant, entry: ConfigEntry, async_add_entities
):
"""Create the vacuum."""
coordinator = hass.data[DOMAIN][entry.entry_id + "_coordinator"]
await coordinator.async_config_entry_first_refresh()
async_add_entities([RoombaVacuum(hass, coordinator, entry)])
async_add_entities(
[RoombaVacuum(hass, entry.runtime_data.local_coordinator, entry)]
)
class RoombaVacuum(CoordinatorEntity, StateVacuumEntity):
@ -44,13 +47,8 @@ class RoombaVacuum(CoordinatorEntity, StateVacuumEntity):
self.hass = hass
self._entry: ConfigEntry = entry
self._attr_supported_features = SUPPORT_ROBOT
self._attr_unique_id = f"{entry.unique_id}_vacuum"
self._attr_unique_id = f"{entry.entry_id}_vacuum"
self._attr_name = entry.title
self._attr_device_info = {
"identifiers": {(DOMAIN, entry.unique_id)},
"name": entry.title,
"manufacturer": "iRobot",
}
def _handle_coordinator_update(self):
"""Update all attributes."""
@ -78,15 +76,21 @@ class RoombaVacuum(CoordinatorEntity, StateVacuumEntity):
self._attr_available = data != {}
self._attr_extra_state_attributes = createExtendedAttributes(self)
self._attr_device_info = {
"identifiers": self._attr_device_info.get("identifiers"),
"name": self._attr_device_info.get("name"),
"manufacturer": self._attr_device_info.get("manufacturer"),
"model": f"Roomba {data.get('sku')}",
"sw_version": data.get("softwareVer"),
}
self._async_write_ha_state()
@property
def device_info(self) -> DeviceInfo:
"""Return the Roomba's device information."""
data = self.coordinator.data or {}
return DeviceInfo(
identifiers={DOMAIN, self._entry.unique_id},
name=data.get("name", "Roomba"),
manufacturer="iRobot",
model="Roomba",
model_id=data.get("sku"),
sw_version=data.get("softwareVer"),
)
async def async_clean_spot(self, **kwargs):
"""Spot clean."""
@ -111,7 +115,7 @@ class RoombaVacuum(CoordinatorEntity, StateVacuumEntity):
regions = []
# Check if we have room selection switches available
domain_data = self.hass.data.get(DOMAIN, {})
domain_data = self._entry.runtime_data.switched_rooms
selected_rooms = []
# Find all room switches that are turned on