mirror of
https://github.com/sudoxnym/roomba_rest980.git
synced 2026-04-14 11:37:46 +00:00
move to runtime_data and work on bronze
This commit is contained in:
parent
d310a8441f
commit
b035324c2f
13 changed files with 394 additions and 119 deletions
|
|
@ -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)
|
||||
|
||||
|
||||
"""
|
||||
|
||||
|
|
|
|||
|
|
@ -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"),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 = {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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.",
|
||||
|
|
|
|||
19
custom_components/roomba_rest980/quality_scale.yaml
Normal file
19
custom_components/roomba_rest980/quality_scale.yaml
Normal 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
|
||||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in a new issue