diff --git a/custom_components/roomba_rest980/__init__.py b/custom_components/roomba_rest980/__init__.py index 38aee9f..e830b0f 100644 --- a/custom_components/roomba_rest980/__init__.py +++ b/custom_components/roomba_rest980/__init__.py @@ -4,7 +4,7 @@ import logging import voluptuous as vol -from homeassistant.config_entries import ConfigEntry +from homeassistant.config_entries import ConfigEntry, ConfigEntryState from homeassistant.core import HomeAssistant, ServiceCall from homeassistant.helpers import config_validation as cv from homeassistant.helpers.aiohttp_client import async_get_clientsession @@ -172,13 +172,18 @@ async def _async_setup_cloud( # 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"] - ) + # Only forward setups if the entry is in LOADED state + if entry.state == ConfigEntryState.LOADED: + await hass.config_entries.async_forward_entry_setups( + entry, ["switch", "button", "camera"] + ) + else: + _LOGGER.warning("Config entry not in LOADED state, skipping cloud platform setup") except Exception as e: # pylint: disable=broad-except _LOGGER.error("Failed to set up cloud coordinator: %s", e) - cloud_coordinator = None + # Don't let cloud coordinator failure prevent main integration from working + entry.runtime_data.cloud_coordinator = None async def _async_match_blid( @@ -219,4 +224,4 @@ async def _async_match_blid( _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) + _LOGGER.error("Error during BLID matching: %s", e) \ No newline at end of file diff --git a/custom_components/roomba_rest980/vacuum.py b/custom_components/roomba_rest980/vacuum.py index fb4cbc2..fdc0f3f 100644 --- a/custom_components/roomba_rest980/vacuum.py +++ b/custom_components/roomba_rest980/vacuum.py @@ -28,6 +28,57 @@ SUPPORT_ROBOT = ( | VacuumEntityFeature.PAUSE ) +# Enhanced status mapping for i3 model support +I3_STATUS_MAPPING = { + # Idle/Ready states + 0: VacuumActivity.IDLE, # Ready + 1: VacuumActivity.IDLE, # Idle + 2: VacuumActivity.IDLE, # Run + + # Cleaning states + 3: VacuumActivity.CLEANING, # Clean + 4: VacuumActivity.CLEANING, # Spot cleaning + 5: VacuumActivity.CLEANING, # Edge cleaning + + # Docking/Charging states + 6: VacuumActivity.RETURNING, # Seeking dock + 7: VacuumActivity.DOCKED, # Charging + 8: VacuumActivity.DOCKED, # Docked + + # Error states + 9: VacuumActivity.ERROR, # Error + 10: VacuumActivity.ERROR, # Stuck + 11: VacuumActivity.ERROR, # Picked up + + # Additional i3 specific states + 12: VacuumActivity.IDLE, # Stopped + 13: VacuumActivity.PAUSED, # Paused + 14: VacuumActivity.CLEANING, # Training + 15: VacuumActivity.CLEANING, # Mapping + 16: VacuumActivity.IDLE, # Manual + 17: VacuumActivity.RETURNING,# Recharging + 18: VacuumActivity.DOCKED, # Evacuating + 19: VacuumActivity.CLEANING, # Smart cleaning + 20: VacuumActivity.CLEANING, # Room cleaning +} + +# Phase to activity mapping for better status detection +PHASE_MAPPING = { + "run": VacuumActivity.CLEANING, + "stop": VacuumActivity.IDLE, + "pause": VacuumActivity.PAUSED, + "charge": VacuumActivity.DOCKED, + "stuck": VacuumActivity.ERROR, + "hmUsrDock": VacuumActivity.RETURNING, + "hmPostMsn": VacuumActivity.RETURNING, + "hwMidMsn": VacuumActivity.CLEANING, + "evac": VacuumActivity.DOCKED, + "dock": VacuumActivity.DOCKED, + "charging": VacuumActivity.DOCKED, + "train": VacuumActivity.CLEANING, + "spot": VacuumActivity.CLEANING, +} + async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities @@ -39,7 +90,7 @@ async def async_setup_entry( class RoombaVacuum(CoordinatorEntity, StateVacuumEntity): - """The Rest980 controlled vacuum.""" + """The Rest980 controlled vacuum with enhanced i3 support.""" def __init__(self, hass: HomeAssistant, coordinator, entry: ConfigEntry) -> None: """Setup the robot.""" @@ -51,29 +102,70 @@ class RoombaVacuum(CoordinatorEntity, StateVacuumEntity): self._attr_name = entry.title def _handle_coordinator_update(self): - """Update all attributes.""" + """Update all attributes with enhanced i3 status handling.""" data = self.coordinator.data or {} status = data.get("cleanMissionStatus", {}) + + # Get all relevant status fields cycle = status.get("cycle") phase = status.get("phase") - not_ready = status.get("notReady") + not_ready = status.get("notReady", 0) + mission_state = status.get("mssnStrtTm") # Mission start time + error_code = status.get("error", 0) + # Get robot model info for model-specific handling + robot_name = data.get("name", "").lower() + is_i3_model = "i3" in robot_name or data.get("sku", "").startswith("R3") + + # Default to IDLE self._attr_activity = VacuumActivity.IDLE - if cycle == "none" and not_ready == 39: + + # Enhanced status determination logic + try: + # Check for error conditions first + if error_code > 0 or not_ready > 0: + self._attr_activity = VacuumActivity.ERROR + _LOGGER.debug(f"Vacuum in error state: error={error_code}, not_ready={not_ready}") + + # Check phase mapping first (most reliable for i3) + elif phase and phase in PHASE_MAPPING: + self._attr_activity = PHASE_MAPPING[phase] + _LOGGER.debug(f"Status from phase mapping: phase={phase}, activity={self._attr_activity}") + + # Check cycle-based status + elif cycle: + if cycle == "none": + if not_ready == 39: + self._attr_activity = VacuumActivity.IDLE + else: + self._attr_activity = VacuumActivity.IDLE + elif cycle in ["clean", "quick", "spot", "train"]: + self._attr_activity = VacuumActivity.CLEANING + elif cycle in ["evac", "dock"]: + self._attr_activity = VacuumActivity.DOCKED + else: + _LOGGER.debug(f"Unknown cycle: {cycle}") + + # For i3 models, check additional status fields + if is_i3_model: + # i3 models might have different status reporting + battery_percent = data.get("batPct", 100) + if battery_percent < 20 and phase in ["charge", "charging"]: + self._attr_activity = VacuumActivity.DOCKED + + # Check if mission is active + if mission_state and cycle in ["clean", "spot", "quick"]: + self._attr_activity = VacuumActivity.CLEANING + + # Final fallback - if we still don't have a proper state + if self._attr_activity == VacuumActivity.IDLE and cycle and cycle != "none": + _LOGGER.warning(f"Unknown status combination: cycle={cycle}, phase={phase}, not_ready={not_ready}") + # Log for debugging to help identify new status combinations + _LOGGER.warning(f"Full status data: {status}") + + except Exception as e: + _LOGGER.error(f"Error determining vacuum activity: {e}") self._attr_activity = VacuumActivity.IDLE - if not_ready and not_ready > 0: - self._attr_activity = VacuumActivity.ERROR - if cycle in ["clean", "quick", "spot", "train"] or phase in {"hwMidMsn"}: - self._attr_activity = VacuumActivity.CLEANING - if cycle in ["evac", "dock"] or phase in { - "charge", - }: # Emptying Roomba Bin to Dock, Entering Dock - self._attr_activity = VacuumActivity.DOCKED - if phase in { - "hmUsrDock", - "hmPostMsn", - }: # Sent Home, Mid Dock, Final Dock - self._attr_activity = VacuumActivity.RETURNING self._attr_available = data != {} self._attr_extra_state_attributes = createExtendedAttributes(self) @@ -83,11 +175,21 @@ class RoombaVacuum(CoordinatorEntity, StateVacuumEntity): def device_info(self) -> DeviceInfo: """Return the Roomba's device information.""" data = self.coordinator.data or {} + model = data.get("sku", "Roomba") + + # Enhanced model detection for i3 + if model.startswith("R3") or "i3" in data.get("name", "").lower(): + model = "Roomba i3" + elif model.startswith("R7"): + model = "Roomba i7" + elif model.startswith("R9"): + model = "Roomba s9" + return DeviceInfo( identifiers={(DOMAIN, self._entry.unique_id)}, name=data.get("name", "Roomba"), manufacturer="iRobot", - model="Roomba", + model=model, model_id=data.get("sku"), sw_version=data.get("softwareVer"), ) @@ -203,4 +305,4 @@ class RoombaVacuum(CoordinatorEntity, StateVacuumEntity): "base_url": self._entry.data["base_url"], }, blocking=True, - ) + ) \ No newline at end of file