diff --git a/custom_components/ecovacs/__init__.py b/custom_components/ecovacs/__init__.py index 850b9a0..a0ea12c 100644 --- a/custom_components/ecovacs/__init__.py +++ b/custom_components/ecovacs/__init__.py @@ -13,18 +13,19 @@ from homeassistant.const import ( ) from homeassistant.core import HomeAssistant from homeassistant.helpers import discovery -from homeassistant.helpers.typing import ConfigType -import homeassistant.helpers.config_validation as cv -import voluptuous as vol +#from homeassistant.helpers.typing import ConfigType +#import homeassistant.helpers.config_validation as cv +#import voluptuous as vol #use local sucks from .sucks import EcoVacsAPI, VacBot from .const import ( - ECOVACS_DEVICES, - DOMAIN, + # ECOVACS_DEVICES, + # DOMAIN, CONF_CONTINENT, LOGGER ) +""" CONFIG_SCHEMA = vol.Schema( { DOMAIN: vol.Schema( @@ -39,6 +40,7 @@ CONFIG_SCHEMA = vol.Schema( }, extra=vol.ALLOW_EXTRA, ) +""" # Generate a random device ID on each bootup ECOVACS_API_DEVICEID = "".join( @@ -48,16 +50,16 @@ ECOVACS_API_DEVICEID = "".join( def setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the Ecovacs component.""" LOGGER.debug("Creating new Ecovacs component") - hass.data[ECOVACS_DEVICES] = [] +# hass.data[ECOVACS_DEVICES] = [] SERVER_ADDRESS = None ecovacs_api = EcoVacsAPI( ECOVACS_API_DEVICEID, - config[DOMAIN].get(CONF_USERNAME), - EcoVacsAPI.md5(config[DOMAIN].get(CONF_PASSWORD)), - config[DOMAIN].get(CONF_COUNTRY), - config[DOMAIN].get(CONF_CONTINENT), - config[DOMAIN].get(CONF_VERIFY_SSL), # add to class call + config_entry.data(CONF_USERNAME), + EcoVacsAPI.md5(config_entry.data(CONF_PASSWORD)), + config_entry.data(CONF_COUNTRY), + config_entry.data(CONF_CONTINENT), + config_entry.data(CONF_VERIFY_SSL), # add to class call ) devices = ecovacs_api.devices() @@ -75,9 +77,9 @@ def setup(hass: HomeAssistant, config: ConfigType) -> bool: ecovacs_api.resource, ecovacs_api.user_access_token, device, - config[DOMAIN].get(CONF_CONTINENT).lower(), + config_entry.data(CONF_CONTINENT).lower(), SERVER_ADDRESS, # include server address in class, if it's null should be no effect - config[DOMAIN].get(CONF_VERIFY_SSL), # add to class call + config_entry.data(CONF_VERIFY_SSL), # add to class call monitor=True ) hass.data[ECOVACS_DEVICES].append(vacbot) diff --git a/custom_components/ecovacs/const.py b/custom_components/ecovacs/const.py index c04ec8c..0813034 100644 --- a/custom_components/ecovacs/const.py +++ b/custom_components/ecovacs/const.py @@ -3,6 +3,6 @@ LOGGER = logging.getLogger(__name__) #ecovacs constants #init constants -ECOVACS_DEVICES = "ecovacs_devices" -DOMAIN = "ecovacs" +#ECOVACS_DEVICES = "ecovacs_devices" +#DOMAIN = "ecovacs" CONF_CONTINENT = "continent" \ No newline at end of file diff --git a/custom_components/ecovacs/coordinator.py b/custom_components/ecovacs/coordinator.py new file mode 100644 index 0000000..c85776e --- /dev/null +++ b/custom_components/ecovacs/coordinator.py @@ -0,0 +1,84 @@ +"""Data update coordinator for Ecovacs vacuums.""" +from __future__ import annotations + +import asyncio + +from async_timeout import timeout +from sharkiq import ( + AylaApi, + SharkIqAuthError, + SharkIqAuthExpiringError, + SharkIqNotAuthedError, + SharkIqVacuum, +) + +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ConfigEntryAuthFailed +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed + +from .const import API_TIMEOUT, DOMAIN, LOGGER, UPDATE_INTERVAL + + +class SharkIqUpdateCoordinator(DataUpdateCoordinator[bool]): + """Define a wrapper class to update Shark IQ data.""" + + def __init__( + self, + hass: HomeAssistant, + config_entry: ConfigEntry, + ayla_api: AylaApi, + shark_vacs: list[SharkIqVacuum], + ) -> None: + """Set up the SharkIqUpdateCoordinator class.""" + self.ayla_api = ayla_api + self.shark_vacs: dict[str, SharkIqVacuum] = { + sharkiq.serial_number: sharkiq for sharkiq in shark_vacs + } + self._config_entry = config_entry + self._online_dsns: set[str] = set() + + super().__init__(hass, LOGGER, name=DOMAIN, update_interval=UPDATE_INTERVAL) + + @property + def online_dsns(self) -> set[str]: + """Get the set of all online DSNs.""" + return self._online_dsns + + def device_is_online(self, dsn: str) -> bool: + """Return the online state of a given vacuum dsn.""" + return dsn in self._online_dsns + + @staticmethod + async def _async_update_vacuum(sharkiq: SharkIqVacuum) -> None: + """Asynchronously update the data for a single vacuum.""" + dsn = sharkiq.serial_number + LOGGER.debug("Updating sharkiq data for device DSN %s", dsn) + async with timeout(API_TIMEOUT): + await sharkiq.async_update() + + async def _async_update_data(self) -> bool: + """Update data device by device.""" + try: + all_vacuums = await self.ayla_api.async_list_devices() + self._online_dsns = { + v["dsn"] + for v in all_vacuums + if v["connection_status"] == "Online" and v["dsn"] in self.shark_vacs + } + + LOGGER.debug("Updating sharkiq data") + online_vacs = (self.shark_vacs[dsn] for dsn in self.online_dsns) + await asyncio.gather(*(self._async_update_vacuum(v) for v in online_vacs)) + except ( + SharkIqAuthError, + SharkIqNotAuthedError, + SharkIqAuthExpiringError, + ) as err: + LOGGER.debug("Bad auth state. Attempting re-auth", exc_info=err) + raise ConfigEntryAuthFailed from err + except Exception as err: + LOGGER.exception("Unexpected error updating SharkIQ") + raise UpdateFailed(err) from err + + return True \ No newline at end of file diff --git a/custom_components/ecovacs/vacuum.py b/custom_components/ecovacs/vacuum.py index 2892655..7819218 100644 --- a/custom_components/ecovacs/vacuum.py +++ b/custom_components/ecovacs/vacuum.py @@ -7,7 +7,10 @@ from typing import Any #sucks from . import sucks -from homeassistant.components.vacuum import VacuumEntity, VacuumEntityFeature +from homeassistant.components.vacuum import ( + VacuumEntity, + VacuumEntityFeature, +) from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.icon import icon_for_battery_level @@ -20,7 +23,7 @@ _LOGGER = logging.getLogger(__name__) ATTR_ERROR = "error" ATTR_COMPONENT_PREFIX = "component_" - +""" def setup_platform( hass: HomeAssistant, config: ConfigType, @@ -33,7 +36,24 @@ def setup_platform( vacuums.append(EcovacsVacuum(device)) _LOGGER.debug("Adding Ecovacs Vacuums to Home Assistant: %s", vacuums) add_entities(vacuums, True) +""" +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, + discovery_info: DiscoveryInfoType | None = None, +) -> None: + """Set up the Ecovacs vacuums.""" + coordinator: SharkIqUpdateCoordinator = hass.data[DOMAIN][config_entry.entry_id] + devices: Iterable[SharkIqVacuum] = coordinator.shark_vacs.values() + device_names = [d.name for d in devices] + LOGGER.debug( + "Found %d Shark IQ device(s): %s", + len(device_names), + ", ".join([d.name for d in devices]), + ) + async_add_entities([SharkVacuumEntity(d, coordinator) for d in devices]) class EcovacsVacuum(VacuumEntity): """Ecovacs Vacuums such as Deebot.""" @@ -42,6 +62,7 @@ class EcovacsVacuum(VacuumEntity): _attr_should_poll = False _attr_supported_features = ( VacuumEntityFeature.BATTERY + | VacuumEntityFeature.FAN_SPEED | VacuumEntityFeature.RETURN_HOME | VacuumEntityFeature.CLEAN_SPOT | VacuumEntityFeature.STOP @@ -50,7 +71,7 @@ class EcovacsVacuum(VacuumEntity): | VacuumEntityFeature.LOCATE | VacuumEntityFeature.STATUS | VacuumEntityFeature.SEND_COMMAND - | VacuumEntityFeature.FAN_SPEED + ) def __init__(self, device: sucks.VacBot) -> None: @@ -75,7 +96,6 @@ class EcovacsVacuum(VacuumEntity): def on_error(self, error): """Handle an error event from the robot. - This will not change the entity's state. If the error caused the state to change, that will come through as a separate on_status event """ @@ -126,7 +146,6 @@ class EcovacsVacuum(VacuumEntity): """Return the battery level of the vacuum cleaner.""" if self.device.battery_status is not None: return self.device.battery_status * 100 - return super().battery_level @property @@ -136,7 +155,6 @@ class EcovacsVacuum(VacuumEntity): def turn_on(self, **kwargs: Any) -> None: """Turn the vacuum on and start cleaning.""" - self.device.run(sucks.Clean()) def turn_off(self, **kwargs: Any) -> None: @@ -145,23 +163,19 @@ class EcovacsVacuum(VacuumEntity): def stop(self, **kwargs: Any) -> None: """Stop the vacuum cleaner.""" - self.device.run(sucks.Stop()) def clean_spot(self, **kwargs: Any) -> None: """Perform a spot clean-up.""" - self.device.run(sucks.Spot()) def locate(self, **kwargs: Any) -> None: """Locate the vacuum cleaner.""" - self.device.run(sucks.PlaySound()) def set_fan_speed(self, fan_speed: str, **kwargs: Any) -> None: """Set fan speed.""" if self.is_on: - self.device.run(sucks.Clean(mode=self.device.clean_status, speed=fan_speed)) def send_command( @@ -178,9 +192,7 @@ class EcovacsVacuum(VacuumEntity): """Return the device-specific state attributes of this vacuum.""" data: dict[str, Any] = {} data[ATTR_ERROR] = self._error - for key, val in self.device.components.items(): attr_name = ATTR_COMPONENT_PREFIX + key data[attr_name] = int(val * 100) - return data