Compare commits

..

15 Commits

Author SHA1 Message Date
bittles 455320c50e Merge pull request #12 from bittles/dev
import EcoVacsAPI back into mqtt library
2023-01-15 15:35:54 -05:00
bittles 2b66ffa426 bump version 2023-01-15 15:34:51 -05:00
bittles 2b252c0b22 import ecovacsapi back into mqtt library 2023-01-15 15:33:57 -05:00
bittles d7a7b2d185 Merge pull request #11 from bittles/dev
fixes for newer python versions
2023-01-15 15:11:04 -05:00
bittles 01165a4f85 version bump 2023-01-15 15:10:29 -05:00
bittles fa2c724333 fixes for newer python versions
isAlive and getchildren removed in python 3.9
2023-01-15 15:10:00 -05:00
bittles b21e2f95aa Merge pull request #10 from bittles/master
fix string to cert in mqtt
2023-01-15 14:11:04 -05:00
bittles 0e8c3ef0b6 fix string to cert in mqtt 2023-01-15 14:10:08 -05:00
bittles 4d8dc3fbe4 Merge pull request #9 from bittles/master
update dev to current master
2023-01-15 14:02:04 -05:00
bittles f1002d107f Merge pull request #7 from bittles/dev
shouldnt edit on phone, fixed now
2023-01-06 02:01:34 -05:00
bittles 24fc2ea74e Merge pull request #6 from bittles/dev
forgot to update module name
2023-01-06 01:51:24 -05:00
bittles 1fbe95c72f Merge pull request #5 from bittles/dev
1.4.0
2023-01-04 14:54:44 -05:00
bittles 3e59aa94aa Merge pull request #4 from bittles/dev
update docs
2023-01-04 13:31:06 -05:00
bittles e56266be86 Merge pull request #3 from bittles/dev
Dev
2023-01-03 23:02:16 -05:00
bittles 1decadee2e Merge pull request #2 from bittles/dev
mostly code cleanup
2023-01-03 14:15:03 -05:00
6 changed files with 45 additions and 128 deletions
+13 -15
View File
@@ -13,19 +13,18 @@ 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(
@@ -40,7 +39,6 @@ CONFIG_SCHEMA = vol.Schema(
},
extra=vol.ALLOW_EXTRA,
)
"""
# Generate a random device ID on each bootup
ECOVACS_API_DEVICEID = "".join(
@@ -50,16 +48,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_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
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
)
devices = ecovacs_api.devices()
@@ -77,9 +75,9 @@ def setup(hass: HomeAssistant, config: ConfigType) -> bool:
ecovacs_api.resource,
ecovacs_api.user_access_token,
device,
config_entry.data(CONF_CONTINENT).lower(),
config[DOMAIN].get(CONF_CONTINENT).lower(),
SERVER_ADDRESS, # include server address in class, if it's null should be no effect
config_entry.data(CONF_VERIFY_SSL), # add to class call
config[DOMAIN].get(CONF_VERIFY_SSL), # add to class call
monitor=True
)
hass.data[ECOVACS_DEVICES].append(vacbot)
+2 -2
View File
@@ -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"
-84
View File
@@ -1,84 +0,0 @@
"""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",
"name": "Ecovacs Bumper",
"version": "1.4.2",
"version": "1.4.5",
"documentation": "https://github.com/bittles/ha_ecovacs_bumper",
"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"],
+17 -2
View File
@@ -9,9 +9,24 @@ from paho.mqtt.client import Client as ClientMQTT
from paho.mqtt import publish as MQTTPublish
from paho.mqtt import subscribe as MQTTSubscribe
from sleekxmppfs.xmlstream import ET
from .sucks import EcoVacsAPI
from .const import LOGGER
def str_to_bool_or_cert(s):
if s == 'True' or s == True:
return True
elif s == 'False' or s == False:
return False
else:
if not s == None:
if os.path.exists(s): # User could provide a path to a CA Cert as well, which is useful for Bumper
if os.path.isfile(s):
return s
else:
raise ValueError("Certificate path provided is not a file - {}".format(s))
raise ValueError("Cannot covert {} to a bool or certificate path".format(s))
#This is used by EcoVacsIOTMQ and EcoVacsXMPP for _ctl_to_dict
def RepresentsInt(stringvar):
try:
@@ -75,7 +90,7 @@ class EcoVacsIOTMQ(ClientMQTT):
def schedule(self, timer_seconds, timer_function):
self.scheduler.enter(timer_seconds, 1, self._run_scheduled_func,(timer_seconds, timer_function))
if not self.scheduler_thread.isAlive():
if not self.scheduler_thread.is_alive():
self.scheduler_thread.start()
def wait_until_ready(self):
@@ -169,7 +184,7 @@ class EcoVacsIOTMQ(ClientMQTT):
def _ctl_to_dict_api(self, action, xmlstring):
xml = ET.fromstring(xmlstring)
xmlchild = xml.getchildren()
xmlchild = list(xml)
if len(xmlchild) > 0:
result = xmlchild[0].attrib.copy()
#Fix for difference in XMPP vs API response
+12 -24
View File
@@ -7,10 +7,7 @@ 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
@@ -23,7 +20,7 @@ _LOGGER = logging.getLogger(__name__)
ATTR_ERROR = "error"
ATTR_COMPONENT_PREFIX = "component_"
"""
def setup_platform(
hass: HomeAssistant,
config: ConfigType,
@@ -36,24 +33,7 @@ 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."""
@@ -62,7 +42,6 @@ class EcovacsVacuum(VacuumEntity):
_attr_should_poll = False
_attr_supported_features = (
VacuumEntityFeature.BATTERY
| VacuumEntityFeature.FAN_SPEED
| VacuumEntityFeature.RETURN_HOME
| VacuumEntityFeature.CLEAN_SPOT
| VacuumEntityFeature.STOP
@@ -71,7 +50,7 @@ class EcovacsVacuum(VacuumEntity):
| VacuumEntityFeature.LOCATE
| VacuumEntityFeature.STATUS
| VacuumEntityFeature.SEND_COMMAND
| VacuumEntityFeature.FAN_SPEED
)
def __init__(self, device: sucks.VacBot) -> None:
@@ -96,6 +75,7 @@ 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
"""
@@ -146,6 +126,7 @@ 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
@@ -155,6 +136,7 @@ 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:
@@ -163,19 +145,23 @@ 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(
@@ -192,7 +178,9 @@ 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