Compare commits

...

3 Commits

Author SHA1 Message Date
bittles 99726d56a3 start config flow setup based on shark 2023-01-06 22:22:50 -05:00
bittles 67fef3862b shouldnt edit on phone, fixed now 2023-01-06 02:00:11 -05:00
bittles 0e7923103c forgot to update module name 2023-01-06 01:47:19 -05:00
5 changed files with 136 additions and 38 deletions
+15 -13
View File
@@ -13,18 +13,19 @@ from homeassistant.const import (
) )
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers import discovery from homeassistant.helpers import discovery
from homeassistant.helpers.typing import ConfigType #from homeassistant.helpers.typing import ConfigType
import homeassistant.helpers.config_validation as cv #import homeassistant.helpers.config_validation as cv
import voluptuous as vol #import voluptuous as vol
#use local sucks #use local sucks
from .sucks import EcoVacsAPI, VacBot from .sucks import EcoVacsAPI, VacBot
from .const import ( from .const import (
ECOVACS_DEVICES, # ECOVACS_DEVICES,
DOMAIN, # DOMAIN,
CONF_CONTINENT, CONF_CONTINENT,
LOGGER LOGGER
) )
"""
CONFIG_SCHEMA = vol.Schema( CONFIG_SCHEMA = vol.Schema(
{ {
DOMAIN: vol.Schema( DOMAIN: vol.Schema(
@@ -39,6 +40,7 @@ CONFIG_SCHEMA = vol.Schema(
}, },
extra=vol.ALLOW_EXTRA, extra=vol.ALLOW_EXTRA,
) )
"""
# Generate a random device ID on each bootup # Generate a random device ID on each bootup
ECOVACS_API_DEVICEID = "".join( ECOVACS_API_DEVICEID = "".join(
@@ -48,16 +50,16 @@ ECOVACS_API_DEVICEID = "".join(
def setup(hass: HomeAssistant, config: ConfigType) -> bool: def setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Set up the Ecovacs component.""" """Set up the Ecovacs component."""
LOGGER.debug("Creating new Ecovacs component") LOGGER.debug("Creating new Ecovacs component")
hass.data[ECOVACS_DEVICES] = [] # hass.data[ECOVACS_DEVICES] = []
SERVER_ADDRESS = None SERVER_ADDRESS = None
ecovacs_api = EcoVacsAPI( ecovacs_api = EcoVacsAPI(
ECOVACS_API_DEVICEID, ECOVACS_API_DEVICEID,
config[DOMAIN].get(CONF_USERNAME), config_entry.data(CONF_USERNAME),
EcoVacsAPI.md5(config[DOMAIN].get(CONF_PASSWORD)), EcoVacsAPI.md5(config_entry.data(CONF_PASSWORD)),
config[DOMAIN].get(CONF_COUNTRY), config_entry.data(CONF_COUNTRY),
config[DOMAIN].get(CONF_CONTINENT), config_entry.data(CONF_CONTINENT),
config[DOMAIN].get(CONF_VERIFY_SSL), # add to class call config_entry.data(CONF_VERIFY_SSL), # add to class call
) )
devices = ecovacs_api.devices() devices = ecovacs_api.devices()
@@ -75,9 +77,9 @@ def setup(hass: HomeAssistant, config: ConfigType) -> bool:
ecovacs_api.resource, ecovacs_api.resource,
ecovacs_api.user_access_token, ecovacs_api.user_access_token,
device, 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 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 monitor=True
) )
hass.data[ECOVACS_DEVICES].append(vacbot) hass.data[ECOVACS_DEVICES].append(vacbot)
+2 -2
View File
@@ -3,6 +3,6 @@ LOGGER = logging.getLogger(__name__)
#ecovacs constants #ecovacs constants
#init constants #init constants
ECOVACS_DEVICES = "ecovacs_devices" #ECOVACS_DEVICES = "ecovacs_devices"
DOMAIN = "ecovacs" #DOMAIN = "ecovacs"
CONF_CONTINENT = "continent" CONF_CONTINENT = "continent"
+84
View File
@@ -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
+1 -1
View File
@@ -1,7 +1,7 @@
{ {
"domain": "ecovacs", "domain": "ecovacs",
"name": "Ecovacs Bumper", "name": "Ecovacs Bumper",
"version": "1.4.0", "version": "1.4.2",
"documentation": "https://github.com/bittles/ha_ecovacs_bumper", "documentation": "https://github.com/bittles/ha_ecovacs_bumper",
"issue_tracker": "https://github.com/bittles/ha_ecovacs_bumper/issues", "issue_tracker": "https://github.com/bittles/ha_ecovacs_bumper/issues",
"requirements": ["sleekxmppfs==1.4.1", "requests>=2.18", "pycryptodome>=3.4", "pycountry-convert>=0.5", "paho-mqtt>=1.4", "stringcase>=1.2"], "requirements": ["sleekxmppfs==1.4.1", "requests>=2.18", "pycryptodome>=3.4", "pycountry-convert>=0.5", "paho-mqtt>=1.4", "stringcase>=1.2"],
+34 -22
View File
@@ -5,9 +5,12 @@ import logging
from typing import Any from typing import Any
#sucks #sucks
from . import sucksbumper from . import sucks
from homeassistant.components.vacuum import VacuumEntity, VacuumEntityFeature from homeassistant.components.vacuum import (
VacuumEntity,
VacuumEntityFeature,
)
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.icon import icon_for_battery_level from homeassistant.helpers.icon import icon_for_battery_level
@@ -20,7 +23,7 @@ _LOGGER = logging.getLogger(__name__)
ATTR_ERROR = "error" ATTR_ERROR = "error"
ATTR_COMPONENT_PREFIX = "component_" ATTR_COMPONENT_PREFIX = "component_"
"""
def setup_platform( def setup_platform(
hass: HomeAssistant, hass: HomeAssistant,
config: ConfigType, config: ConfigType,
@@ -33,15 +36,33 @@ def setup_platform(
vacuums.append(EcovacsVacuum(device)) vacuums.append(EcovacsVacuum(device))
_LOGGER.debug("Adding Ecovacs Vacuums to Home Assistant: %s", vacuums) _LOGGER.debug("Adding Ecovacs Vacuums to Home Assistant: %s", vacuums)
add_entities(vacuums, True) 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): class EcovacsVacuum(VacuumEntity):
"""Ecovacs Vacuums such as Deebot.""" """Ecovacs Vacuums such as Deebot."""
_attr_fan_speed_list = [sucksbumper.FAN_SPEED_NORMAL, sucksbumper.FAN_SPEED_HIGH] _attr_fan_speed_list = [sucks.FAN_SPEED_NORMAL, sucks.FAN_SPEED_HIGH]
_attr_should_poll = False _attr_should_poll = False
_attr_supported_features = ( _attr_supported_features = (
VacuumEntityFeature.BATTERY VacuumEntityFeature.BATTERY
| VacuumEntityFeature.FAN_SPEED
| VacuumEntityFeature.RETURN_HOME | VacuumEntityFeature.RETURN_HOME
| VacuumEntityFeature.CLEAN_SPOT | VacuumEntityFeature.CLEAN_SPOT
| VacuumEntityFeature.STOP | VacuumEntityFeature.STOP
@@ -50,10 +71,10 @@ class EcovacsVacuum(VacuumEntity):
| VacuumEntityFeature.LOCATE | VacuumEntityFeature.LOCATE
| VacuumEntityFeature.STATUS | VacuumEntityFeature.STATUS
| VacuumEntityFeature.SEND_COMMAND | VacuumEntityFeature.SEND_COMMAND
| VacuumEntityFeature.FAN_SPEED
) )
def __init__(self, device: sucksbumper.VacBot) -> None: def __init__(self, device: sucks.VacBot) -> None:
"""Initialize the Ecovacs Vacuum.""" """Initialize the Ecovacs Vacuum."""
self.device = device self.device = device
self.device.connect_and_wait_until_ready() self.device.connect_and_wait_until_ready()
@@ -75,7 +96,6 @@ class EcovacsVacuum(VacuumEntity):
def on_error(self, error): def on_error(self, error):
"""Handle an error event from the robot. """Handle an error event from the robot.
This will not change the entity's state. If the error caused the state 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 to change, that will come through as a separate on_status event
""" """
@@ -112,7 +132,7 @@ class EcovacsVacuum(VacuumEntity):
def return_to_base(self, **kwargs: Any) -> None: def return_to_base(self, **kwargs: Any) -> None:
"""Set the vacuum cleaner to return to the dock.""" """Set the vacuum cleaner to return to the dock."""
self.device.run(sucksbumper.Charge()) self.device.run(sucks.Charge())
@property @property
def battery_icon(self) -> str: def battery_icon(self) -> str:
@@ -126,7 +146,6 @@ class EcovacsVacuum(VacuumEntity):
"""Return the battery level of the vacuum cleaner.""" """Return the battery level of the vacuum cleaner."""
if self.device.battery_status is not None: if self.device.battery_status is not None:
return self.device.battery_status * 100 return self.device.battery_status * 100
return super().battery_level return super().battery_level
@property @property
@@ -136,8 +155,7 @@ class EcovacsVacuum(VacuumEntity):
def turn_on(self, **kwargs: Any) -> None: def turn_on(self, **kwargs: Any) -> None:
"""Turn the vacuum on and start cleaning.""" """Turn the vacuum on and start cleaning."""
self.device.run(sucks.Clean())
self.device.run(sucksbumper.Clean())
def turn_off(self, **kwargs: Any) -> None: def turn_off(self, **kwargs: Any) -> None:
"""Turn the vacuum off stopping the cleaning and returning home.""" """Turn the vacuum off stopping the cleaning and returning home."""
@@ -145,24 +163,20 @@ class EcovacsVacuum(VacuumEntity):
def stop(self, **kwargs: Any) -> None: def stop(self, **kwargs: Any) -> None:
"""Stop the vacuum cleaner.""" """Stop the vacuum cleaner."""
self.device.run(sucks.Stop())
self.device.run(sucksbumper.Stop())
def clean_spot(self, **kwargs: Any) -> None: def clean_spot(self, **kwargs: Any) -> None:
"""Perform a spot clean-up.""" """Perform a spot clean-up."""
self.device.run(sucks.Spot())
self.device.run(sucksbumper.Spot())
def locate(self, **kwargs: Any) -> None: def locate(self, **kwargs: Any) -> None:
"""Locate the vacuum cleaner.""" """Locate the vacuum cleaner."""
self.device.run(sucks.PlaySound())
self.device.run(sucksbumper.PlaySound())
def set_fan_speed(self, fan_speed: str, **kwargs: Any) -> None: def set_fan_speed(self, fan_speed: str, **kwargs: Any) -> None:
"""Set fan speed.""" """Set fan speed."""
if self.is_on: if self.is_on:
self.device.run(sucks.Clean(mode=self.device.clean_status, speed=fan_speed))
self.device.run(sucksbumper.Clean(mode=self.device.clean_status, speed=fan_speed))
def send_command( def send_command(
self, self,
@@ -171,16 +185,14 @@ class EcovacsVacuum(VacuumEntity):
**kwargs: Any, **kwargs: Any,
) -> None: ) -> None:
"""Send a command to a vacuum cleaner.""" """Send a command to a vacuum cleaner."""
self.device.run(sucksbumper.VacBotCommand(command, params)) self.device.run(sucks.VacBotCommand(command, params))
@property @property
def extra_state_attributes(self) -> dict[str, Any]: def extra_state_attributes(self) -> dict[str, Any]:
"""Return the device-specific state attributes of this vacuum.""" """Return the device-specific state attributes of this vacuum."""
data: dict[str, Any] = {} data: dict[str, Any] = {}
data[ATTR_ERROR] = self._error data[ATTR_ERROR] = self._error
for key, val in self.device.components.items(): for key, val in self.device.components.items():
attr_name = ATTR_COMPONENT_PREFIX + key attr_name = ATTR_COMPONENT_PREFIX + key
data[attr_name] = int(val * 100) data[attr_name] = int(val * 100)
return data return data