Compare commits

..

17 Commits

Author SHA1 Message Date
bittles 0c130226a1 Update README.md 2026-02-09 14:44:53 -06:00
bittles 590772ec2c Update README.md 2024-04-27 17:49:07 -05:00
bittles 4f92ce5eba async changes put back in to push for 1.7.0 2024-04-27 17:40:15 -05:00
bittles ba0a2f9ed8 revert async to push minimal change update for release
has StateVacuumEntity but no async, will push release like this that should work and push another release with async
2024-04-27 17:35:41 -05:00
bittles d1bb42c769 change VacuumEntity to StateVacuumEntity 2024-04-27 17:32:39 -05:00
bittles 4ac0dc84d1 Make setup of Ecovacs async (#96200) * make setup async * apply suggestions
Bring code up to date with ecovacs component from commit cce9d93 on 7/24/23
2024-04-27 17:16:45 -05:00
bittles cacb0b40bb Merge pull request #17 from bittles/dev
cant comment block inside class call
2023-01-19 18:44:55 -05:00
bittles dc032f6719 cant comment block inside class call 2023-01-19 18:44:33 -05:00
bittles 9355f86e2d Merge pull request #16 from bittles/dev
Update README.md
2023-01-19 16:57:54 -05:00
bittles d2a0537b37 Update README.md 2023-01-19 16:57:35 -05:00
bittles f8ba0dd6da Merge pull request #15 from bittles/dev
fix longstanding error handling i hope
2023-01-19 16:54:47 -05:00
bittles d5370a5152 Update sucks.py 2023-01-19 16:53:49 -05:00
bittles a483d30625 fix error handling, bump to 1.5.2 2023-01-19 16:52:06 -05:00
bittles 64d62abf93 fix readme 2023-01-19 11:26:37 -05:00
bittles 6e5c0c170b readme* 2023-01-18 00:30:47 -05:00
bittles 9b52bc8e1d update readme 2023-01-18 00:28:36 -05:00
bittles 9652c47d42 Update sucks.py 2023-01-18 00:03:42 -05:00
5 changed files with 90 additions and 52 deletions
+6 -1
View File
@@ -1,3 +1,8 @@
# Builtin HASS integration has options to not verify self-signed certificate now, this integration no longer needed.
## Latest version is 1.7.0 that includes updates that SHOULD at least let the component work with newer HASS versions. This version includes async updates so the comoponent won't crap out on setup if it's unable to reach the vacuum. I am unable to tset these changes since I don't have HASS currently setup and I don't have my old Ecovacs vacuum anymore (handed down to younger sis).
# If 1.7.0 doesn't work, use version 1.6.0. I just made the bare minimum changes to the component that again SHOULD work, but I'm unable to test it. HASS ditched the VacuumEntity import for the vacuum component at some point and change it to StateVacuumEntity. That's all that's included in this update. Thanks @guillaume042 for making it easy for me by pointing me straight to a an issue with another component using VacuumEntity (though theirs was just leftover code that wasn't actually being used).
# Home Assistant Ecovacs Custom Component with Bumper Support # Home Assistant Ecovacs Custom Component with Bumper Support
Based off the regular home assistant ecovacs components and bmartin's fork of sucks, https://github.com/bmartin5692/sucks. Replaces built in ecovacs component, with some upgrades and fixes. Allows SSL verification to be set to false to work with a self-hosted bumper server, https://github.com/bmartin5692/bumper, a replacement for Ecovacs servers to truly get local control. Based off the regular home assistant ecovacs components and bmartin's fork of sucks, https://github.com/bmartin5692/sucks. Replaces built in ecovacs component, with some upgrades and fixes. Allows SSL verification to be set to false to work with a self-hosted bumper server, https://github.com/bmartin5692/bumper, a replacement for Ecovacs servers to truly get local control.
@@ -78,4 +83,4 @@ Make component async, use config_flow, create device and clean up some of the ha
### Misc Info From Making This ### Misc Info From Making This
Commit history is a bit of a mess. master branch shows changes from bmartins fork of sucks to v1.3.0 of this custom component. dev branch shows commits from my attempts at testing and getting this to work. Commit history is a bit of a mess. master branch shows changes from bmartins fork of sucks to v1.3.0 of this custom component. dev branch shows commits from my attempts at testing and getting this to work.
Added additional catches to sucks because my N79 sends some weird payloads, but attributes all pull in now for brush life spans. Couple initial queries it also sends weird that I'm in process of catching atm. As of version 1.3.0 (in the manifest.json) these initial queries and all attributes are working. Was using an implementation completely mine but saw in the MQTT class there were already catches for child payloads without the main payload having the expected td in its payload. Kept comments in giving credit and adapted them to work with xmpp. Added additional catches to sucks because my N79 sends some weird payloads, but attributes all pull in now for brush life spans. Couple initial queries it also sends weird that I'm in process of catching atm. As of version 1.3.0 (in the manifest.json) these initial queries and all attributes are working. Was using an implementation completely mine but saw in the MQTT class there were already catches for child payloads without the main payload having the expected td in its payload. Kept comments in giving credit and adapted them to work with xmpp.
+41 -36
View File
@@ -44,55 +44,60 @@ ECOVACS_API_DEVICEID = "".join(
random.choice(string.ascii_uppercase + string.digits) for _ in range(8) random.choice(string.ascii_uppercase + string.digits) for _ in range(8)
) )
def setup(hass: HomeAssistant, config: ConfigType) -> bool: async def async_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] = [] def get_devices() -> list[VacBot]:
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),
)
ecovacs_devices = ecovacs_api.devices()
_LOGGER.debug("Ecobot devices: %s", ecovacs_devices)
SERVER_ADDRESS = None SERVER_ADDRESS = None
ecovacs_api = EcoVacsAPI( devices: list[VacBot] = []
ECOVACS_API_DEVICEID, for device in ecovacs_devices:
config[DOMAIN].get(CONF_USERNAME), _LOGGER.info(
EcoVacsAPI.md5(config[DOMAIN].get(CONF_PASSWORD)), "Discovered Ecovacs device on account: %s with nickname %s",
config[DOMAIN].get(CONF_COUNTRY), device.get("did"),
config[DOMAIN].get(CONF_CONTINENT), device.get("nick"),
config[DOMAIN].get(CONF_VERIFY_SSL), # add to class call )
) vacbot = VacBot(
ecovacs_api.uid,
ecovacs_api.REALM,
ecovacs_api.resource,
ecovacs_api.user_access_token,
device,
config[DOMAIN].get(CONF_CONTINENT).lower(),
config[DOMAIN].get(CONF_VERIFY_SSL), # add to class call
monitor=True,
)
devices = ecovacs_api.devices() devices.append(vacbot)
LOGGER.debug("Ecobot devices: %s", devices) return devices
for device in devices: hass.data[ECOVACS_DEVICES] = await hass.async_add_executor_job(get_devices)
LOGGER.info(
"Discovered Ecovacs device on account: %s with nickname %s",
device.get("did"),
device.get("nick"),
)
vacbot = VacBot(
ecovacs_api.uid,
ecovacs_api.REALM,
ecovacs_api.resource,
ecovacs_api.user_access_token,
device,
config[DOMAIN].get(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
monitor=True
)
hass.data[ECOVACS_DEVICES].append(vacbot)
def stop(event: object) -> None: async def async_stop(event: object) -> None:
"""Shut down open connections to Ecovacs XMPP server.""" """Shut down open connections to Ecovacs XMPP server."""
for device in hass.data[ECOVACS_DEVICES]: devices: list[VacBot] = hass.data[ECOVACS_DEVICES]
for device in devices:
LOGGER.info( LOGGER.info(
"Shutting down connection to Ecovacs device %s", "Shutting down connection to Ecovacs device %s",
device.vacuum.get("did"), device.vacuum.get("did"),
) )
device.disconnect() await hass.async_add_executor_job(device.disconnect)
# Listen for HA stop to disconnect. # Listen for HA stop to disconnect.
hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, stop) hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, async_stop)
if hass.data[ECOVACS_DEVICES]: if hass.data[ECOVACS_DEVICES]:
LOGGER.debug("Starting vacuum components") LOGGER.debug("Starting vacuum components")
discovery.load_platform(hass, Platform.VACUUM, DOMAIN, {}, config) hass.async_create_task(
return True discovery.async_load_platform(hass, Platform.VACUUM, DOMAIN, {}, config)
)
return True
+1 -1
View File
@@ -1,7 +1,7 @@
{ {
"domain": "ecovacs", "domain": "ecovacs",
"name": "Ecovacs Bumper", "name": "Ecovacs Bumper",
"version": "1.5.1", "version": "1.5.3",
"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"],
+32 -5
View File
@@ -7,6 +7,7 @@ import os
from sleekxmppfs.xmlstream import ET from sleekxmppfs.xmlstream import ET
from sleekxmppfs.exceptions import XMPPError from sleekxmppfs.exceptions import XMPPError
#from . import sucks_api
from .sucks_mqtt import EcoVacsIOTMQ from .sucks_mqtt import EcoVacsIOTMQ
from .sucks_xmpp import EcoVacsXMPP from .sucks_xmpp import EcoVacsXMPP
@@ -105,14 +106,40 @@ class VacBot():
getattr(self, method)(ctl) getattr(self, method)(ctl)
def _handle_error(self, event): def _handle_error(self, event):
if 'error' in event: if 'error' in event or 'errs' in event:
error = event['error'] error = '' # init error var so it's available outside of first if loop
elif 'errs' in event: if 'error' in event:
error = event['errs'] error = event['error']
if not error == '': elif 'errs' in event:
error = event['errs']
self.errorEvents.notify(error) self.errorEvents.notify(error)
LOGGER.error("*** error = " + error) LOGGER.error("*** error = " + error)
# if not error == '':
# Errors
# The bot broadcasts error codes for a number of cases.
# <ctl td="error" error="BatteryLow" errno="101"></ctl>
# The latest error can be requested like so:
# Request <ctl td="GetError" />
# Response <ctl ret="ok" errs="100"/>
# However in some cases the robot sends to code 100 shortly after an error has occurred, meaning that we cannot trust the GetError request to contain the last relevant error. For example, if the robot gets stuck it broadcasts 102 HostHang, then proceeds to stop and broadcasts 100 NoError.
# Known error codes
# 100 NoError: Robot is operational
# 101 BatteryLow: Low battery
# 102 HostHang: Robot is stuck
# 103 WheelAbnormal: Wheels are not moving as expected
# 104 DownSensorAbnormal: Down sensor is getting abnormal values
# 110 NoDustBox: Dust Bin Not installed
# These codes are taken from model M81 Pro. Error codes may differ between models.
def _handle_life_span(self, event): def _handle_life_span(self, event):
type = event['type'] type = event['type']
try: try:
+10 -9
View File
@@ -7,7 +7,7 @@ from typing import Any
#sucks #sucks
from . import sucks from . import sucks
from homeassistant.components.vacuum import VacuumEntity, VacuumEntityFeature from homeassistant.components.vacuum import StateVacuumEntity, 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
@@ -21,21 +21,23 @@ ATTR_ERROR = "error"
ATTR_COMPONENT_PREFIX = "component_" ATTR_COMPONENT_PREFIX = "component_"
def setup_platform( async def async_setup_platform(
hass: HomeAssistant, hass: HomeAssistant,
config: ConfigType, config: ConfigType,
add_entities: AddEntitiesCallback, async_add_entities: AddEntitiesCallback,
discovery_info: DiscoveryInfoType | None = None, discovery_info: DiscoveryInfoType | None = None,
) -> None: ) -> None:
"""Set up the Ecovacs vacuums.""" """Set up the Ecovacs vacuums."""
vacuums = [] vacuums = []
for device in hass.data[ECOVACS_DEVICES]: devices: list[sucks.VacBot] = hass.data[ECOVACS_DEVICES]
for device in devices:
await hass.async_add_executor_job(device.connect_and_wait_until_ready)
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) async_add_entities(vacuums)
class EcovacsVacuum(VacuumEntity): class EcovacsVacuum(StateVacuumEntity):
"""Ecovacs Vacuums such as Deebot.""" """Ecovacs Vacuums such as Deebot."""
_attr_fan_speed_list = [sucks.FAN_SPEED_NORMAL, sucks.FAN_SPEED_HIGH] _attr_fan_speed_list = [sucks.FAN_SPEED_NORMAL, sucks.FAN_SPEED_HIGH]
@@ -56,7 +58,7 @@ class EcovacsVacuum(VacuumEntity):
def __init__(self, device: sucks.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()
if self.device.vacuum.get("nick") is not None: if self.device.vacuum.get("nick") is not None:
self._attr_name = str(self.device.vacuum["nick"]) self._attr_name = str(self.device.vacuum["nick"])
else: else:
@@ -64,7 +66,6 @@ class EcovacsVacuum(VacuumEntity):
self._attr_name = str(format(self.device.vacuum["did"])) self._attr_name = str(format(self.device.vacuum["did"]))
self._error = None self._error = None
_LOGGER.debug("Vacuum initialized: %s", self.name)
async def async_added_to_hass(self) -> None: async def async_added_to_hass(self) -> None:
"""Set up the event listeners now that hass is ready.""" """Set up the event listeners now that hass is ready."""
@@ -183,4 +184,4 @@ class EcovacsVacuum(VacuumEntity):
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