Compare commits

..

17 Commits

Author SHA1 Message Date
bittles 1decadee2e Merge pull request #2 from bittles/dev
mostly code cleanup
2023-01-03 14:15:03 -05:00
bittles 6ef44ddb9b add check for childxml to avoid errors
lot of code cleanup and getting rid of trailing whitespace, try to make it a little more uniform.  document some changes in comments
2023-01-03 14:09:44 -05:00
bittles 724e167114 change ping schedule from 30s to 300s
decrease calls to robot, see if this is contributing to battery use
2023-01-02 22:56:34 -05:00
bittles cf817145a0 more code cleanup, no need to handle differences if server_address defined or not
mustve fixed init at some point, whatever, it works
2023-01-02 22:54:48 -05:00
bittles 066cc5bf7b code cleanup and more searching 2023-01-02 22:41:04 -05:00
bittles aea75338f6 some init logging to catch whether bumper or sucks is handling pings 2023-01-02 22:28:22 -05:00
bittles cd4c8069bc typo 2023-01-02 22:18:08 -05:00
bittles 43a9cef3b7 try ping? unsure how to register handlers 2023-01-02 22:14:28 -05:00
bittles 42356582cc remove what i think are some unneeded changes 2023-01-02 20:19:37 -05:00
bittles 0d95ef7fac Merge pull request #1 from bittles/master
change xmpp ping from 30 to 300s to not spam robot, add ifs for mqtt …
2023-01-02 19:55:04 -05:00
bittles a9bd4d2d48 change xmpp ping from 30 to 300s to not spam robot, add ifs for mqtt or xmpp robot 2023-01-02 19:54:11 -05:00
bittles f9899bbf0d Delete sucksbumper.py.save 2023-01-02 19:32:51 -05:00
bittles c6a11b47b6 more hacs stuff 2023-01-02 19:27:03 -05:00
bittles cbac201d0d Update manifest.json 2023-01-02 19:20:58 -05:00
bittles 8d7aa5c582 add to do 2023-01-02 19:14:31 -05:00
bittles 0a2c296ef2 formatting 2023-01-02 19:12:44 -05:00
bittles d4afaceb62 add hacs support, bump version number, update readme 2023-01-02 19:11:56 -05:00
5 changed files with 130 additions and 1449 deletions
+32 -9
View File
@@ -1,16 +1,16 @@
# Home Assistant Ecovacs Custom Component with Bumper Support # Home Assistant Ecovacs Custom Component with Bumper Support
Replaces built in ecovacs component. 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. Replaces built in ecovacs component. Designed to work with bumper, https://github.com/bmartin5692/bumper, a replacement for Ecovacs servers to truly get local control.
Works with bumper with my N79 and should work with at least other XMPP based ecovacs. Don't know if changes will work with MQTT based ones. Works with bumper with my N79 and should work with at least other XMPP based ecovacs. Don't know if changes will work with MQTT based ones.
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. Should work as regular if bumper is false in config but haven't tested yet, goal was to get it all local. Maybe mess around and test it in future.
With bumper and my N79 commands would work but some queries had responses that included errno='', which bumper would flag as an error even though the full response was there. If your debug logs are throwing errors and the errno is '' then my small fork of bumper may help https://github.com/bittles/bumper-fork With bumper, my N79 commands would work but some queries had responses that included errno='', which bumper would flag as an error even though the full response was there. It never created attributes for the filters as one of the results. If your debug logs are throwing errors and the errno is '' then my small fork of bumper may help https://github.com/bittles/bumper-fork
Should work as regular if bumper isn't used in config but haven't tested yet, goal was to get it all local. Maybe mess around and test it in future. Based off the regular home assistant ecovacs config and bmartin's fork of sucks, https://github.com/bmartin5692/sucks.
I'm using the docker-compose example from my bumper forked from bmartin5692's, https://github.com/bmartin5692/bumper on an odroid-n2+.
I'm using the docker-compose example for bumper by bmartin5692, https://github.com/bmartin5692/bumper on an odroid-n2+.
### DNS
For DNS routing I have an Asus AX88u with asus-merlin installed running Adguard. DNS rewrites in AdGuard for domains: For DNS routing I have an Asus AX88u with asus-merlin installed running Adguard. DNS rewrites in AdGuard for domains:
``` ```
*.ecouser.net *.ecouser.net
@@ -19,12 +19,20 @@ For DNS routing I have an Asus AX88u with asus-merlin installed running Adguard.
``` ```
pointing to my bumper server. pointing to my bumper server.
Big credits to bmartin5692 for his fork of sucks to base this off of as well.
## Home Assistant Install & Config ## Home Assistant Install & Config
Drop the ecovacs folder into your custom_components folder. If I polish this up I'll add hacs support. ### HACS Install
You can add this repository to your HACS: https://github.com/bittles/ha_ecovacs_bumper
Then download with HACS, HACS -> Integrations -> Explore & Download Repositories -> EcovacsBumper
Restart HASS. Restart HASS.
### Manually Install
Drop the ecovacs folder into your custom_components folder.
Restart HASS.
### Config
In your configuration.yaml: In your configuration.yaml:
``` ```
ecovacs: ecovacs:
@@ -50,3 +58,18 @@ ecovacs:
verify_ssl: false verify_ssl: false
``` ```
Just finished getting this working late 12/13/22 so not sure if everything works yet but will commit changes here if I update it or at least document issues. Just finished getting this working late 12/13/22 so not sure if everything works yet but will commit changes here if I update it or at least document issues.
### Logging
```
logger:
logs:
custom_components.ecovacs.sucksbumper: debug # or whatever level you want
```
### To-Do:
Make component async, use config_flow, create device and clean up some of the hass integration stuff.
### 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.
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.
+4 -3
View File
@@ -1,10 +1,11 @@
{ {
"domain": "ecovacs", "domain": "ecovacs",
"name": "Ecovacs Bumper", "name": "Ecovacs Bumper",
"version": "1.3.0", "version": "1.3.4",
"documentation": "https://www.home-assistant.io/integrations/ecovacs", "documentation": "https://github.com/bittles/ha_ecovacs_bumper",
"issue_tracker": "https://github.com/bittles/ha_ecovacs_bumper/issues",
"requirements": ["sleekxmppfs==1.4.1", "click>=6", "requests>=2.18", "pycryptodome>=3.4", "pycountry-convert>=0.5", "paho-mqtt>=1.4", "stringcase>=1.2"], "requirements": ["sleekxmppfs==1.4.1", "click>=6", "requests>=2.18", "pycryptodome>=3.4", "pycountry-convert>=0.5", "paho-mqtt>=1.4", "stringcase>=1.2"],
"codeowners": ["@OverloadUT", "@mib1185"], "codeowners": ["bittles"],
"iot_class": "local_polling", "iot_class": "local_polling",
"loggers": ["sleekxmppfs", "sucksbumper"] "loggers": ["sleekxmppfs", "sucksbumper"]
} }
+27 -255
View File
@@ -108,11 +108,11 @@ CHARGE_MODE_TO_ECOVACS = {
CHARGE_MODE_FROM_ECOVACS = { CHARGE_MODE_FROM_ECOVACS = {
'going': CHARGE_MODE_RETURNING, 'going': CHARGE_MODE_RETURNING,
'Going': CHARGE_MODE_RETURNING, # 'Going': CHARGE_MODE_RETURNING,
'slot_charging': CHARGE_MODE_CHARGING, 'slot_charging': CHARGE_MODE_CHARGING,
'SlotCharging': CHARGE_MODE_CHARGING, # 'SlotCharging': CHARGE_MODE_CHARGING,
'idle': CHARGE_MODE_IDLE, 'idle': CHARGE_MODE_IDLE,
'Idle': CHARGE_MODE_IDLE, # 'Idle': CHARGE_MODE_IDLE,
} }
COMPONENT_TO_ECOVACS = { COMPONENT_TO_ECOVACS = {
@@ -123,11 +123,11 @@ COMPONENT_TO_ECOVACS = {
COMPONENT_FROM_ECOVACS = { COMPONENT_FROM_ECOVACS = {
'brush': COMPONENT_MAIN_BRUSH, 'brush': COMPONENT_MAIN_BRUSH,
'Brush': COMPONENT_MAIN_BRUSH, # 'Brush': COMPONENT_MAIN_BRUSH,
'side_brush': COMPONENT_SIDE_BRUSH, 'side_brush': COMPONENT_SIDE_BRUSH,
'SideBrush': COMPONENT_SIDE_BRUSH, # 'SideBrush': COMPONENT_SIDE_BRUSH,
'dust_case_heap': COMPONENT_FILTER, 'dust_case_heap': COMPONENT_FILTER,
'DustCaseHeap': COMPONENT_FILTER, # 'DustCaseHeap': COMPONENT_FILTER,
} }
def str_to_bool_or_cert(s): def str_to_bool_or_cert(s):
@@ -142,10 +142,8 @@ def str_to_bool_or_cert(s):
return s return s
else: else:
raise ValueError("Certificate path provided is not a file - {}".format(s)) raise ValueError("Certificate path provided is not a file - {}".format(s))
raise ValueError("Cannot covert {} to a bool or certificate path".format(s)) raise ValueError("Cannot covert {} to a bool or certificate path".format(s))
class EcoVacsAPI: class EcoVacsAPI:
CLIENT_KEY = "eJUWrzRv34qFSaYk" CLIENT_KEY = "eJUWrzRv34qFSaYk"
SECRET = "Cyu5jcR4zyK6QEPn1hdIGXB5QIDAQABMA0GC" SECRET = "Cyu5jcR4zyK6QEPn1hdIGXB5QIDAQABMA0GC"
@@ -153,12 +151,9 @@ class EcoVacsAPI:
MAIN_URL_FORMAT = 'https://eco-{country}-api.ecovacs.com/v1/private/{country}/{lang}/{deviceId}/{appCode}/{appVersion}/{channel}/{deviceType}' MAIN_URL_FORMAT = 'https://eco-{country}-api.ecovacs.com/v1/private/{country}/{lang}/{deviceId}/{appCode}/{appVersion}/{channel}/{deviceType}'
USER_URL_FORMAT = 'https://users-{continent}.ecouser.net:8000/user.do' USER_URL_FORMAT = 'https://users-{continent}.ecouser.net:8000/user.do'
PORTAL_URL_FORMAT = 'https://portal-{continent}.ecouser.net/api' PORTAL_URL_FORMAT = 'https://portal-{continent}.ecouser.net/api'
USERSAPI = 'users/user.do' USERSAPI = 'users/user.do'
IOTDEVMANAGERAPI = 'iot/devmanager.do' # IOT Device Manager - This provides control of "IOT" products via RestAPI, some bots use this instead of XMPP IOTDEVMANAGERAPI = 'iot/devmanager.do' # IOT Device Manager - This provides control of "IOT" products via RestAPI, some bots use this instead of XMPP
PRODUCTAPI = 'pim/product' # Leaving this open, the only endpoint known currently is "Product IOT Map" - pim/product/getProductIotMap - This provides a list of "IOT" products. Not sure what this provides the app. PRODUCTAPI = 'pim/product' # Leaving this open, the only endpoint known currently is "Product IOT Map" - pim/product/getProductIotMap - This provides a list of "IOT" products. Not sure what this provides the app.
REALM = 'ecouser.net' REALM = 'ecouser.net'
def __init__(self, device_id, account_id, password_hash, country, continent, verify_ssl=True): def __init__(self, device_id, account_id, password_hash, country, continent, verify_ssl=True):
@@ -175,7 +170,6 @@ class EcoVacsAPI:
'deviceType': '1' 'deviceType': '1'
#'deviceType': '2' - iphone #'deviceType': '2' - iphone
} }
self.verify_ssl = str_to_bool_or_cert(verify_ssl) self.verify_ssl = str_to_bool_or_cert(verify_ssl)
_LOGGER.debug("Setting up EcoVacsAPI") _LOGGER.debug("Setting up EcoVacsAPI")
self.resource = device_id[0:8] self.resource = device_id[0:8]
@@ -200,12 +194,10 @@ class EcoVacsAPI:
result = params.copy() result = params.copy()
result['authTimespan'] = int(time.time() * 1000) result['authTimespan'] = int(time.time() * 1000)
result['authTimeZone'] = 'GMT-8' result['authTimeZone'] = 'GMT-8'
sign_on = self.meta.copy() sign_on = self.meta.copy()
sign_on.update(result) sign_on.update(result)
sign_on_text = EcoVacsAPI.CLIENT_KEY + ''.join( sign_on_text = EcoVacsAPI.CLIENT_KEY + ''.join(
[k + '=' + str(sign_on[k]) for k in sorted(sign_on.keys())]) + EcoVacsAPI.SECRET [k + '=' + str(sign_on[k]) for k in sorted(sign_on.keys())]) + EcoVacsAPI.SECRET
result['authAppkey'] = EcoVacsAPI.CLIENT_KEY result['authAppkey'] = EcoVacsAPI.CLIENT_KEY
result['authSign'] = self.md5(sign_on_text) result['authSign'] = self.md5(sign_on_text)
return result return result
@@ -243,24 +235,18 @@ class EcoVacsAPI:
"failure {} ({}) for call {} and parameters {}".format(json['error'], json['errno'], function, params)) "failure {} ({}) for call {} and parameters {}".format(json['error'], json['errno'], function, params))
def __call_portal_api(self, api, function, args, verify_ssl=True, **kwargs): def __call_portal_api(self, api, function, args, verify_ssl=True, **kwargs):
if api == self.USERSAPI: if api == self.USERSAPI:
params = {'todo': function} params = {'todo': function}
params.update(args) params.update(args)
else: else:
params = {} params = {}
params.update(args) params.update(args)
_LOGGER.debug("calling portal api {} function {} with {}".format(api, function, params)) _LOGGER.debug("calling portal api {} function {} with {}".format(api, function, params))
continent = self.continent continent = self.continent
if 'continent' in kwargs: if 'continent' in kwargs:
continent = kwargs.get('continent') continent = kwargs.get('continent')
url = (EcoVacsAPI.PORTAL_URL_FORMAT + "/" + api).format(continent=continent, **self.meta) url = (EcoVacsAPI.PORTAL_URL_FORMAT + "/" + api).format(continent=continent, **self.meta)
response = requests.post(url, json=params, verify=verify_ssl) response = requests.post(url, json=params, verify=verify_ssl)
json = response.json() json = response.json()
_LOGGER.debug("got {}".format(json)) _LOGGER.debug("got {}".format(json))
if api == self.USERSAPI: if api == self.USERSAPI:
@@ -276,7 +262,6 @@ class EcoVacsAPI:
return self.__call_portal_api(self.USERSAPI, function, args, verify_ssl=verify_ssl, set_token=2, continent="ww") return self.__call_portal_api(self.USERSAPI, function, args, verify_ssl=verify_ssl, set_token=2, continent="ww")
else: else:
_LOGGER.debug("loginByItToken set token error, failed after 3 attempts") _LOGGER.debug("loginByItToken set token error, failed after 3 attempts")
if api.startswith(self.PRODUCTAPI): if api.startswith(self.PRODUCTAPI):
if json['code'] == 0: if json['code'] == 0:
return json return json
@@ -354,7 +339,6 @@ class EcoVacsAPI:
result = cipher.encrypt(bytes(text, 'utf8')) result = cipher.encrypt(bytes(text, 'utf8'))
return str(b64encode(result), 'utf8') return str(b64encode(result), 'utf8')
class EventEmitter(object): class EventEmitter(object):
"""A very simple event emitting system.""" """A very simple event emitting system."""
def __init__(self): def __init__(self):
@@ -372,7 +356,6 @@ class EventEmitter(object):
for subscriber in self._subscribers: for subscriber in self._subscribers:
subscriber.callback(event) subscriber.callback(event)
class EventListener(object): class EventListener(object):
"""Object that allows event consumers to easily unsubscribe from events.""" """Object that allows event consumers to easily unsubscribe from events."""
def __init__(self, emitter, callback): def __init__(self, emitter, callback):
@@ -385,54 +368,35 @@ class EventListener(object):
class VacBot(): class VacBot():
# switched verify and monitor just to be consistent # switched verify and monitor just to be consistent
def __init__(self, user, domain, resource, secret, vacuum, continent, server_address=None, verify_ssl=True, monitor=False): def __init__(self, user, domain, resource, secret, vacuum, continent, server_address=None, verify_ssl=True, monitor=False):
self.vacuum = vacuum self.vacuum = vacuum
self.server_address = server_address self.server_address = server_address
# If True, the VacBot object will handle keeping track of all statuses, # If True, the VacBot object will handle keeping track of all statuses,
# including the initial request for statuses, and new requests after the # including the initial request for statuses, and new requests after the
# VacBot returns from being offline. It will also cause it to regularly # VacBot returns from being offline. It will also cause it to regularly
# request component lifespans # request component lifespans
self._monitor = monitor self._monitor = monitor
self._failed_pings = 0 self._failed_pings = 0
# These three are representations of the vacuum state as reported by the API # These three are representations of the vacuum state as reported by the API
self.clean_status = None self.clean_status = None
self.charge_status = None self.charge_status = None
self.battery_status = None self.battery_status = None
# This is an aggregate state managed by the sucks library, combining the clean and charge events to a single state # This is an aggregate state managed by the sucks library, combining the clean and charge events to a single state
self.vacuum_status = None self.vacuum_status = None
self.fan_speed = None self.fan_speed = None
# Populated by component Lifespan reports # Populated by component Lifespan reports
self.components = {} self.components = {}
self.statusEvents = EventEmitter() self.statusEvents = EventEmitter()
self.batteryEvents = EventEmitter() self.batteryEvents = EventEmitter()
self.lifespanEvents = EventEmitter() self.lifespanEvents = EventEmitter()
self.errorEvents = EventEmitter() self.errorEvents = EventEmitter()
#Set none for clients to start #Set none for clients to start
self.xmpp = None self.xmpp = None
self.iotmq = None self.iotmq = None
if not vacuum['iotmq']: if not vacuum['iotmq']:
# if server is defined then use bmartins init example for using sucks library in his docs; couldnt get this to work in hass with code he had here though, maybe not referencing everything right in component init
if self.server_address is not None:
vacuum = {"did": "none", "class": "none"}
# super().__init__("sucks", "ecouser.net", "", "", vacuum, "")
self.xmpp = EcoVacsXMPP("sucks", "ecouser.net", "", "", "", vacuum, server_address)
self.xmpp.subscribe_to_ctls(self._handle_ctl)
# should work with ecovacs servers but 1) havent tested with my changes and 2) havent tested with bmartins changes
else:
self.xmpp = EcoVacsXMPP(user, domain, resource, secret, continent, vacuum, server_address) self.xmpp = EcoVacsXMPP(user, domain, resource, secret, continent, vacuum, server_address)
#Uncomment line to allow unencrypted plain auth #Uncomment line to allow unencrypted plain auth
#self.xmpp['feature_mechanisms'].unencrypted_plain = True #self.xmpp['feature_mechanisms'].unencrypted_plain = True
self.xmpp.subscribe_to_ctls(self._handle_ctl) self.xmpp.subscribe_to_ctls(self._handle_ctl)
else: else:
self.iotmq = EcoVacsIOTMQ(user, domain, resource, secret, continent, vacuum, server_address, verify_ssl=verify_ssl) self.iotmq = EcoVacsIOTMQ(user, domain, resource, secret, continent, vacuum, server_address, verify_ssl=verify_ssl)
self.iotmq.subscribe_to_ctls(self._handle_ctl) self.iotmq.subscribe_to_ctls(self._handle_ctl)
@@ -444,23 +408,13 @@ class VacBot():
#self.xmpp.subscribe_to_ctls(self._handle_ctl) #self.xmpp.subscribe_to_ctls(self._handle_ctl)
def connect_and_wait_until_ready(self): def connect_and_wait_until_ready(self):
# use bmartins exmaple if defining our own server, couldn't get this to work without defining, probably xmpp port but idk
if self.server_address:
logging.info("connecting")
# self.xmpp.connect(self.server_address)
# self.xmpp.process()
self.xmpp.connect_and_wait_until_ready()
self.xmpp.schedule('Ping', 30, lambda: self.send_ping(), repeat=True)
# keep rest of bmartins fork intact
else:
if not self.vacuum['iotmq']: if not self.vacuum['iotmq']:
self.xmpp.connect_and_wait_until_ready() self.xmpp.connect_and_wait_until_ready()
self.xmpp.schedule('Ping', 30, lambda: self.send_ping(), repeat=True) self.xmpp.schedule('Ping', 300, lambda: self.send_ping(), repeat=True)
else: else:
self.iotmq.connect_and_wait_until_ready() self.iotmq.connect_and_wait_until_ready()
self.iotmq.schedule(30, self.send_ping) self.iotmq.schedule(30, self.send_ping)
#self.xmpp.connect_and_wait_until_ready() #Leaving in case xmpp is given to iotmq in the future #self.xmpp.connect_and_wait_until_ready() #Leaving in case xmpp is given to iotmq in the future
if self._monitor: if self._monitor:
# Do a first ping, which will also fetch initial statuses if the ping succeeds # Do a first ping, which will also fetch initial statuses if the ping succeeds
self.send_ping() self.send_ping()
@@ -470,11 +424,7 @@ class VacBot():
self.iotmq.schedule(3600,self.refresh_components) self.iotmq.schedule(3600,self.refresh_components)
def _handle_ctl(self, ctl): def _handle_ctl(self, ctl):
# _LOGGER.debug("super handle_ctl called with ctl:")
# _LOGGER.debug(ctl)
method = '_handle_' + ctl['event'] method = '_handle_' + ctl['event']
# _LOGGER.debug("method assigned:")
# _LOGGER.debug(method)
if hasattr(self, method): if hasattr(self, method):
getattr(self, method)(ctl) getattr(self, method)(ctl)
@@ -483,35 +433,22 @@ class VacBot():
error = event['error'] error = event['error']
elif 'errs' in event: elif 'errs' in event:
error = event['errs'] error = event['errs']
if not error == '': if not error == '':
self.errorEvents.notify(error) self.errorEvents.notify(error)
_LOGGER.debug("*** error = " + error) _LOGGER.debug("*** error = " + error)
def _handle_life_span(self, event): def _handle_life_span(self, event):
# _LOGGER.debug("_handle_life_span called, event is: ")
# _LOGGER.debug(event)
# _LOGGER.debug("event shown now continue with handle life span")
type = event['type'] type = event['type']
# _LOGGER.debug("type in handle life span: ")
# _LOGGER.debug(type)
# _LOGGER.debug("type shown now continue with handle life span")
try: try:
type = COMPONENT_FROM_ECOVACS[type] type = COMPONENT_FROM_ECOVACS[type]
except KeyError: except KeyError:
_LOGGER.warning("Unknown component type: '" + type + "'") _LOGGER.warning("Unknown component type: '" + type + "'")
if 'val' in event: if 'val' in event:
lifespan = int(event['val']) / 100 lifespan = int(event['val']) / 100
_LOGGER.debug("**********Component " + type + " has lifespan of " + str(lifespan) + ".") _LOGGER.debug("**********Component " + type + " has lifespan of " + str(lifespan) + ".")
else: else:
lifespan = int(event['left']) / 60 #This works for a D901 lifespan = int(event['left']) / 60 #This works for a D901
self.components[type] = lifespan self.components[type] = lifespan
lifespan_event = {'type': type, 'lifespan': lifespan} lifespan_event = {'type': type, 'lifespan': lifespan}
self.lifespanEvents.notify(lifespan_event) self.lifespanEvents.notify(lifespan_event)
_LOGGER.debug("*** life_span " + type + " = " + str(lifespan)) _LOGGER.debug("*** life_span " + type + " = " + str(lifespan))
@@ -529,7 +466,6 @@ class VacBot():
_LOGGER.warning("Unknown cleaning status '" + type + "'") _LOGGER.warning("Unknown cleaning status '" + type + "'")
self.clean_status = type self.clean_status = type
self.vacuum_status = type self.vacuum_status = type
fan = event.get('speed', None) fan = event.get('speed', None)
if fan is not None: if fan is not None:
try: try:
@@ -565,12 +501,10 @@ class VacBot():
else: else:
status = 'idle' #Fall back to Idle status status = 'idle' #Fall back to Idle status
_LOGGER.error("Unknown charging status '" + event['errno'] + "'") #Log this so we can identify more errors _LOGGER.error("Unknown charging status '" + event['errno'] + "'") #Log this so we can identify more errors
try: try:
status = CHARGE_MODE_FROM_ECOVACS[status] status = CHARGE_MODE_FROM_ECOVACS[status]
except KeyError: except KeyError:
_LOGGER.warning("Unknown charging status '" + status + "'") _LOGGER.warning("Unknown charging status '" + status + "'")
self.charge_status = status self.charge_status = status
if status != 'idle' or self.vacuum_status == 'charging': if status != 'idle' or self.vacuum_status == 'charging':
# We have to ignore the idle messages, because all it means is that it's not # We have to ignore the idle messages, because all it means is that it's not
@@ -601,7 +535,6 @@ class VacBot():
elif self.vacuum['iotmq']: elif self.vacuum['iotmq']:
if not self.iotmq.send_ping(): if not self.iotmq.send_ping():
raise RuntimeError() raise RuntimeError()
except XMPPError as err: except XMPPError as err:
_LOGGER.warning("Ping did not reach VacBot. Will retry.") _LOGGER.warning("Ping did not reach VacBot. Will retry.")
_LOGGER.debug("*** Error type: " + err.etype) _LOGGER.debug("*** Error type: " + err.etype)
@@ -610,14 +543,12 @@ class VacBot():
if self._failed_pings >= 4: if self._failed_pings >= 4:
self.vacuum_status = 'offline' self.vacuum_status = 'offline'
self.statusEvents.notify(self.vacuum_status) self.statusEvents.notify(self.vacuum_status)
except RuntimeError as err: except RuntimeError as err:
_LOGGER.warning("Ping did not reach VacBot. Will retry.") _LOGGER.warning("Ping did not reach VacBot. Will retry.")
self._failed_pings += 1 self._failed_pings += 1
if self._failed_pings >= 4: if self._failed_pings >= 4:
self.vacuum_status = 'offline' self.vacuum_status = 'offline'
self.statusEvents.notify(self.vacuum_status) self.statusEvents.notify(self.vacuum_status)
else: else:
self._failed_pings = 0 self._failed_pings = 0
if self._monitor: if self._monitor:
@@ -692,7 +623,6 @@ class EcoVacsIOTMQ(ClientMQTT):
self.scheduler = sched.scheduler(time.time, time.sleep) self.scheduler = sched.scheduler(time.time, time.sleep)
self.scheduler_thread = threading.Thread(target=self.scheduler.run, daemon=True, name="mqtt_schedule_thread") self.scheduler_thread = threading.Thread(target=self.scheduler.run, daemon=True, name="mqtt_schedule_thread")
self.verify_ssl = str_to_bool_or_cert(verify_ssl) self.verify_ssl = str_to_bool_or_cert(verify_ssl)
if server_address is None: if server_address is None:
self.hostname = ('mq-{}.ecouser.net'.format(self.continent)) self.hostname = ('mq-{}.ecouser.net'.format(self.continent))
self.port = 8883 self.port = 8883
@@ -704,24 +634,20 @@ class EcoVacsIOTMQ(ClientMQTT):
self.port = int(saddress[1]) self.port = int(saddress[1])
else: else:
self.port = 8883 self.port = 8883
self._client_id = self.user + '@' + self.domain.split(".")[0] + '/' + self.resource self._client_id = self.user + '@' + self.domain.split(".")[0] + '/' + self.resource
self.username_pw_set(self.user + '@' + self.domain, secret) self.username_pw_set(self.user + '@' + self.domain, secret)
self.ready_flag = Event() self.ready_flag = Event()
def connect_and_wait_until_ready(self): def connect_and_wait_until_ready(self):
#self._on_log = self.on_log #This provides more logging than needed, even for debug #self._on_log = self.on_log #This provides more logging than needed, even for debug
self._on_message = self._handle_ctl_mqtt self._on_message = self._handle_ctl_mqtt
self._on_connect = self.on_connect self._on_connect = self.on_connect
#TODO: This is pretty insecure and accepts any cert, maybe actually check? #TODO: This is pretty insecure and accepts any cert, maybe actually check?
ssl_ctx = ssl.create_default_context() ssl_ctx = ssl.create_default_context()
ssl_ctx.check_hostname = False ssl_ctx.check_hostname = False
ssl_ctx.verify_mode = ssl.CERT_NONE ssl_ctx.verify_mode = ssl.CERT_NONE
self.tls_set_context(ssl_ctx) self.tls_set_context(ssl_ctx)
self.tls_insecure_set(True) self.tls_insecure_set(True)
self.connect(self.hostname, self.port) self.connect(self.hostname, self.port)
self.loop_start() self.loop_start()
self.wait_until_ready() self.wait_until_ready()
@@ -749,11 +675,9 @@ class EcoVacsIOTMQ(ClientMQTT):
if rc != 0: if rc != 0:
_LOGGER.error("EcoVacsMQTT - error connecting with MQTT Return {}".format(rc)) _LOGGER.error("EcoVacsMQTT - error connecting with MQTT Return {}".format(rc))
raise RuntimeError("EcoVacsMQTT - error connecting with MQTT Return {}".format(rc)) raise RuntimeError("EcoVacsMQTT - error connecting with MQTT Return {}".format(rc))
else: else:
_LOGGER.debug("EcoVacsMQTT - Connected with result code "+str(rc)) _LOGGER.debug("EcoVacsMQTT - Connected with result code "+str(rc))
_LOGGER.debug("EcoVacsMQTT - Subscribing to all") _LOGGER.debug("EcoVacsMQTT - Subscribing to all")
self.subscribe('iot/atr/+/' + self.vacuum['did'] + '/' + self.vacuum['class'] + '/' + self.vacuum['resource'] + '/+', qos=0) self.subscribe('iot/atr/+/' + self.vacuum['did'] + '/' + self.vacuum['class'] + '/' + self.vacuum['resource'] + '/+', qos=0)
self.ready_flag.set() self.ready_flag.set()
@@ -781,7 +705,6 @@ class EcoVacsIOTMQ(ClientMQTT):
#Remove the td from ctl xml for RestAPI #Remove the td from ctl xml for RestAPI
payloadxml = cmd.to_xml() payloadxml = cmd.to_xml()
payloadxml.attrib.pop("td") payloadxml.attrib.pop("td")
return { return {
'auth': { 'auth': {
'realm': EcoVacsAPI.REALM, 'realm': EcoVacsAPI.REALM,
@@ -804,7 +727,6 @@ class EcoVacsIOTMQ(ClientMQTT):
_LOGGER.debug("calling iotdevmanager api with {}".format(args)) _LOGGER.debug("calling iotdevmanager api with {}".format(args))
params = {} params = {}
params.update(args) params.update(args)
url = (EcoVacsAPI.PORTAL_URL_FORMAT + "/iot/devmanager.do").format(continent=self.continent) url = (EcoVacsAPI.PORTAL_URL_FORMAT + "/iot/devmanager.do").format(continent=self.continent)
response = None response = None
try: #The RestAPI sometimes doesnt provide a response depending on command, reduce timeout to 3 to accomodate and make requests faster try: #The RestAPI sometimes doesnt provide a response depending on command, reduce timeout to 3 to accomodate and make requests faster
@@ -812,7 +734,6 @@ class EcoVacsIOTMQ(ClientMQTT):
except requests.exceptions.ReadTimeout: except requests.exceptions.ReadTimeout:
_LOGGER.debug("call to iotdevmanager failed with ReadTimeout") _LOGGER.debug("call to iotdevmanager failed with ReadTimeout")
return {} return {}
json = response.json() json = response.json()
if json['ret'] == 'ok': if json['ret'] == 'ok':
return json return json
@@ -838,7 +759,6 @@ class EcoVacsIOTMQ(ClientMQTT):
def _ctl_to_dict_api(self, action, xmlstring): def _ctl_to_dict_api(self, action, xmlstring):
xml = ET.fromstring(xmlstring) xml = ET.fromstring(xmlstring)
xmlchild = xml.getchildren() xmlchild = xml.getchildren()
if len(xmlchild) > 0: if len(xmlchild) > 0:
result = xmlchild[0].attrib.copy() result = xmlchild[0].attrib.copy()
@@ -852,7 +772,6 @@ class EcoVacsIOTMQ(ClientMQTT):
result['event'] = "BatteryInfo" result['event'] = "BatteryInfo"
else: #Default back to replacing Get from the api cmdName else: #Default back to replacing Get from the api cmdName
result['event'] = action.name.replace("Get","",1) result['event'] = action.name.replace("Get","",1)
else: else:
result = xml.attrib.copy() result = xml.attrib.copy()
result['event'] = action.name.replace("Get","",1) result['event'] = action.name.replace("Get","",1)
@@ -860,11 +779,9 @@ class EcoVacsIOTMQ(ClientMQTT):
if result['ret'] == 'fail': if result['ret'] == 'fail':
if action.name == "Charge": #So far only seen this with Charge, when already docked if action.name == "Charge": #So far only seen this with Charge, when already docked
result['event'] = "ChargeState" result['event'] = "ChargeState"
for key in result: for key in result:
if not RepresentsInt(result[key]): #Fix to handle negative int values if not RepresentsInt(result[key]): #Fix to handle negative int values
result[key] = stringcase.snakecase(result[key]) result[key] = stringcase.snakecase(result[key])
return result return result
def _handle_ctl_mqtt(self, client, userdata, message): def _handle_ctl_mqtt(self, client, userdata, message):
@@ -877,16 +794,13 @@ class EcoVacsIOTMQ(ClientMQTT):
def _ctl_to_dict_mqtt(self, topic, xmlstring): def _ctl_to_dict_mqtt(self, topic, xmlstring):
#I haven't seen the need to fall back to data within the topic (like we do with IOT rest call actions), but it is here in case of future need #I haven't seen the need to fall back to data within the topic (like we do with IOT rest call actions), but it is here in case of future need
xml = ET.fromstring(xmlstring) #Convert from string to xml (like IOT rest calls), other than this it is similar to XMPP xml = ET.fromstring(xmlstring) #Convert from string to xml (like IOT rest calls), other than this it is similar to XMPP
#Including changes from jasonarends @ 28da7c2 below #Including changes from jasonarends @ 28da7c2 below
result = xml.attrib.copy() result = xml.attrib.copy()
if 'td' not in result: if 'td' not in result:
# This happens for commands with no response data, such as PlaySound # This happens for commands with no response data, such as PlaySound
# Handle response data with no 'td' # Handle response data with no 'td'
if 'type' in result: # single element with type and val if 'type' in result: # single element with type and val
result['event'] = "LifeSpan" # seems to always be LifeSpan type result['event'] = "LifeSpan" # seems to always be LifeSpan type
else: else:
if len(xml) > 0: # case where there is child element if len(xml) > 0: # case where there is child element
if 'clean' in xml[0].tag: if 'clean' in xml[0].tag:
@@ -904,12 +818,10 @@ class EcoVacsIOTMQ(ClientMQTT):
result['event'] = result.pop('td') result['event'] = result.pop('td')
if xml: if xml:
result.update(xml[0].attrib) result.update(xml[0].attrib)
for key in result: for key in result:
#Check for RepresentInt to handle negative int values, and ',' for ignoring position updates #Check for RepresentInt to handle negative int values, and ',' for ignoring position updates
if not RepresentsInt(result[key]) and ',' not in result[key]: if not RepresentsInt(result[key]) and ',' not in result[key]:
result[key] = stringcase.snakecase(result[key]) result[key] = stringcase.snakecase(result[key])
return result return result
@@ -930,7 +842,6 @@ class EcoVacsXMPP(ClientXMPP):
self.ctl_subscribers = [] self.ctl_subscribers = []
self.ready_flag = Event() self.ready_flag = Event()
def wait_until_ready(self): def wait_until_ready(self):
self.ready_flag.wait() self.ready_flag.wait()
@@ -940,189 +851,58 @@ class EcoVacsXMPP(ClientXMPP):
self.register_handler(Callback("general", self.register_handler(Callback("general",
MatchXPath('{jabber:client}iq/{com:ctl}query/{com:ctl}'), MatchXPath('{jabber:client}iq/{com:ctl}query/{com:ctl}'),
self._handle_ctl)) self._handle_ctl))
# register a ping handler, not really needed but keeps from errors being thrown
self.register_handler(Callback("Ping",
MatchXPath('{jabber:client}iq/{urn:xmpp:ping}ping/{urn:xmpp:ping}'),
self._handle_ping))
self.ready_flag.set() self.ready_flag.set()
def subscribe_to_ctls(self, function): def subscribe_to_ctls(self, function):
self.ctl_subscribers.append(function) self.ctl_subscribers.append(function)
def _handle_ctl(self, message): def _handle_ctl(self, message):
# _LOGGER.debug("message in handle_ctl is:")
# _LOGGER.debug(message)
# the_good_part = str(message.payload.decode("utf-8"))
# the_good_part = message.get_payload()[0][0]
the_good_part = message.get_payload()[0][0] the_good_part = message.get_payload()[0][0]
# _LOGGER.debug("the_good_part in handle_ctl is :")
# _LOGGER.debug(the_good_part)
# the_other_part = None
# try:
# the_other_part = message.get_payload()[0][0][0]
# _LOGGER.debug("Other payload found:")
# _LOGGER.debug(the_other_part)
# except IndexError:
# _LOGGER.debug("No extra payload")
as_dict = self._ctl_to_dict(the_good_part) as_dict = self._ctl_to_dict(the_good_part)
# _LOGGER.debug("handle ctl called with as_dict:")
# _LOGGER.debug(as_dict)
if as_dict is not None: if as_dict is not None:
for s in self.ctl_subscribers: for s in self.ctl_subscribers:
s(as_dict) s(as_dict)
# if as_dict is None:
# try:
# other_part = message.get_payload()[0][0][0]
# # _LOGGER.debug("handle_ctl called with get_payload()[0][0][0], the other part:")
# #_LOGGER.debug(other_part)
# other_dict = self._ctl_to_dict(other_part)
# #_LOGGER.debug("other dict in query:")
# #_LOGGER.debug(other_dict)
# if other_dict is not None:
# for s in self.ctl_subscribers:
# s(other_dict)
# except IndexError:
# _LOGGER.debug("No extra payload")
def _ctl_to_dict(self, xml): def _ctl_to_dict(self, xml):
#Including changes from jasonarends @ 28da7c2 below #Including changes from jasonarends @ 28da7c2 below
result = xml.attrib.copy() result = xml.attrib.copy()
# _LOGGER.debug("result is:") childxml = None
# _LOGGER.debug(result) try: # check for child xml
# if other_xml is not None: childxml = xml[0]
# other_result = other_xml.attrib.copy() except IndexError:
# _LOGGER.debug("other result:") _LOGGER.debug("No child xml")
# _LOGGER.debug(other_result)
# _LOGGER.debug(xml[0].tag)
if 'td' not in result: if 'td' not in result:
# _LOGGER.debug("td not in result:")
# _LOGGER.debug(result)
# This happens for commands with no response data, such as PlaySound
# Handle response data with no 'td' # Handle response data with no 'td'
if 'type' in result: # single element with type and val if 'type' in result: # single element with type and val
# _LOGGER.debug("type detected in result, result before event handling:")
# _LOGGER.debug(result)
result['event'] = "LifeSpan" # seems to always be LifeSpan type result['event'] = "LifeSpan" # seems to always be LifeSpan type
# result['event'] = "life_span" # seems to always be LifeSpan type
# _LOGGER.debug("result after event LifeSpan handling:")
# _LOGGER.debug(result)
else: else:
if xml[0] is not None: if childxml is not None:
# if other_xml is not None: # case where there is child element if 'clean' in childxml.tag:
# _LOGGER.debug("child xml detected, [0] tag is")
# _LOGGER.debug(xml[0].tag)
if 'clean' in xml[0].tag:
# _LOGGER.debug("clean detected in xml[0].tag, result before event handling:")
# _LOGGER.debug(result)
result['event'] = "CleanReport" result['event'] = "CleanReport"
# result['event'] = "clean_report" elif 'charge' in childxml.tag:
# _LOGGER.debug("result after event clean handling:")
# _LOGGER.debug(result)
elif 'charge' in xml[0].tag:
# _LOGGER.debug("charge detected in xml[0].tag, result before event handling:")
# _LOGGER.debug(result)
result['event'] = "ChargeState" result['event'] = "ChargeState"
# result['event'] = "charge_state" elif 'battery' in childxml.tag:
# _LOGGER.debug("result after event charge handling:")
# _LOGGER.debug(result)
elif 'battery' in xml[0].tag:
# _LOGGER.debug("battery detected in xml[0].tag, result before event handling:")
# _LOGGER.debug(result)
result['event'] = "BatteryInfo" result['event'] = "BatteryInfo"
# result['event'] = "battery_info"
# _LOGGER.debug("result after event battery handling:")
# _LOGGER.debug(result)
else: else:
# _LOGGER.warning("other payload detected but didn't catch on any checks, result is: ")
# _LOGGER.debug(result)
return return
result.update(xml[0].attrib) result.update(childxml.attrib)
# _LOGGER.debug("result after xml update attrib:")
# _LOGGER.debug(result)
else: # for non-'type' result with no child element, e.g., result of PlaySound else: # for non-'type' result with no child element, e.g., result of PlaySound
# _LOGGER.warning("payload didn't catch on any checks, result is: ")
# _LOGGER.debug(result)
return return
else: # response includes 'td' else: # response includes 'td'
# _LOGGER.debug("td detected in result, result before event handling:")
# _LOGGER.debug(result)
result['event'] = result.pop('td') result['event'] = result.pop('td')
# _LOGGER.debug("result after event td handling:")
# _LOGGER.debug(result)
if xml: if xml:
result.update(xml[0].attrib) result.update(xml[0].attrib) # reponses with td seem to always have child component
# _LOGGER.debug("IF XML sub-check result after xml update attrib:")
# _LOGGER.debug(result)
for key in result: for key in result:
#Check for RepresentInt to handle negative int values, and ',' for ignoring position updates #Check for RepresentInt to handle negative int values, and ',' for ignoring position updates
if not RepresentsInt(result[key]) and ',' not in result[key]: if not RepresentsInt(result[key]) and ',' not in result[key]:
result[key] = stringcase.snakecase(result[key]) result[key] = stringcase.snakecase(result[key])
return result return result
#
# result = xml.attrib.copy()
# other_result = other_xml.attrib.copy()
# _LOGGER.debug("result from xml is :")
# _LOGGER.debug(result)
# _LOGGER.debug("end of result")
# _LOGGER.debug("other_result from xml is :")
# _LOGGER.debug(other_result)
# _LOGGER.debug("end of other_result")
# if 'td' in result:
# result['event'] = result.pop('td')
# if xml:
# result.update(xml[0].attrib)
# for key in result:
# if not RepresentsInt(result[key]): #Fix to handle negative int values
# result[key] = stringcase.snakecase(result[key])
# _LOGGER.debug("td detected in result and result is:")
# _LOGGER.debug(result)
# _LOGGER.debug("end of td detect result")
# return result
# elif 'type' in result:
# result['event'] = result.pop('type')
# if 'errno' in result:
# if result['errno'] == '':
# result['errno'] = 'life_span'
# result['event'] = result.pop('errno')
# if xml:
# result.update(xml[0].attrib)
# else:
# result['event'] = result.pop('type')
# if xml:
# result.update(xml[0].attrib)
# for key in result:
# if not RepresentsInt(result[key]): #Fix to handle negative int values
# result[key] = stringcase.snakecase(result[key])
# _LOGGER.debug("type detected in result and result is:")
# _LOGGER.debug(result)
# _LOGGER.debug("end of type detect result")
# return result
# elif 'type' in other_result:
# if len(xmlchild) > 0:
# result = xmlchild[0].attrib.copy()
# #Fix for difference in XMPP vs API response
# #Depending on the report will use the tag and add "report" to fit the mold of sucks library
# if xmlchild[0].tag == "clean":
# result['event'] = "CleanReport"
# elif xmlchild[0].tag == "charge":
# result['event'] = "ChargeState"
# elif xmlchild[0].tag == "battery":
# result['event'] = "BatteryInfo"
# else:
# # This happens for commands with no response data, such as PlaySound
# _LOGGER.debug("neither type nor td in result:")
# _LOGGER.debug(result)
# _LOGGER.debug("end of no td or type detect result")
# return
def register_callback(self, userdata, message): def register_callback(self, userdata, message):
self.register_handler(Callback(kind, self.register_handler(Callback(kind,
MatchXPath('{jabber:client}iq/{com:ctl}query/{com:ctl}ctl[@td="' + kind + '"]'), MatchXPath('{jabber:client}iq/{com:ctl}query/{com:ctl}ctl[@td="' + kind + '"]'),
function)) function))
@@ -1149,7 +929,6 @@ class EcoVacsXMPP(ClientXMPP):
rtnval = str(random.randint(1,50)) rtnval = str(random.randint(1,50))
while len(str(rtnval)) <= 8: while len(str(rtnval)) <= 8:
rtnval = "{}{}".format(rtnval,random.randint(0,50)) rtnval = "{}{}".format(rtnval,random.randint(0,50))
return "{}".format(rtnval) #return as string return "{}".format(rtnval) #return as string
def _my_address(self): def _my_address(self):
@@ -1158,13 +937,17 @@ class EcoVacsXMPP(ClientXMPP):
else: else:
return self.user + '@' + self.domain + '/' + self.resource return self.user + '@' + self.domain + '/' + self.resource
def send_ping(self, to): def send_ping(self, to):
q = self.make_iq_get(ito=to, ifrom=self._my_address()) q = self.make_iq_get(ito=to, ifrom=self._my_address())
q.xml.append(ET.Element('ping', {'xmlns': 'urn:xmpp:ping'})) q.xml.append(ET.Element('ping', {'xmlns': 'urn:xmpp:ping'}))
_LOGGER.debug("*** sending ping ***") _LOGGER.debug("*** sending ping ***")
q.send() q.send()
# used some code from a sleekxmppfs plugin, seems to work fine
def _handle_ping(self, iq):
_LOGGER.debug("Pinged by %s", iq['from'])
iq.reply().send()
def connect_and_wait_until_ready(self): def connect_and_wait_until_ready(self):
self.connect(self.server_address) self.connect(self.server_address)
self.process() self.process()
@@ -1198,7 +981,6 @@ class VacBotCommand:
ctl.append(ixml) ctl.append(ixml)
else: else:
ctl.set(key, value) ctl.set(key, value)
return ctl return ctl
def __str__(self, *args, **kwargs): def __str__(self, *args, **kwargs):
@@ -1231,12 +1013,10 @@ class Edge(Clean):
def __init__(self): def __init__(self):
super().__init__('edge', 'high') super().__init__('edge', 'high')
class Spot(Clean): class Spot(Clean):
def __init__(self): def __init__(self):
super().__init__('spot', 'high') super().__init__('spot', 'high')
class Stop(Clean): class Stop(Clean):
def __init__(self): def __init__(self):
super().__init__('stop', 'normal') super().__init__('stop', 'normal')
@@ -1255,38 +1035,30 @@ class Charge(VacBotCommand):
def __init__(self): def __init__(self):
super().__init__('Charge', {'charge': {'type': CHARGE_MODE_TO_ECOVACS['return']}}) super().__init__('Charge', {'charge': {'type': CHARGE_MODE_TO_ECOVACS['return']}})
class Move(VacBotCommand): class Move(VacBotCommand):
def __init__(self, action): def __init__(self, action):
super().__init__('Move', {'move': {'action': self.ACTION[action]}}) super().__init__('Move', {'move': {'action': self.ACTION[action]}})
class PlaySound(VacBotCommand): class PlaySound(VacBotCommand):
def __init__(self, sid="0"): def __init__(self, sid="0"):
super().__init__('PlaySound', {'sid': sid}) super().__init__('PlaySound', {'sid': sid})
class GetCleanState(VacBotCommand): class GetCleanState(VacBotCommand):
def __init__(self): def __init__(self):
super().__init__('GetCleanState') super().__init__('GetCleanState')
class GetChargeState(VacBotCommand): class GetChargeState(VacBotCommand):
def __init__(self): def __init__(self):
super().__init__('GetChargeState') super().__init__('GetChargeState')
class GetBatteryState(VacBotCommand): class GetBatteryState(VacBotCommand):
def __init__(self): def __init__(self):
super().__init__('GetBatteryInfo') super().__init__('GetBatteryInfo')
class GetLifeSpan(VacBotCommand): class GetLifeSpan(VacBotCommand):
def __init__(self, component): def __init__(self, component):
# _LOGGER.debug("GetLifeSpan called by VacBot**************")
super().__init__('GetLifeSpan', {'type': COMPONENT_TO_ECOVACS[component]}) super().__init__('GetLifeSpan', {'type': COMPONENT_TO_ECOVACS[component]})
class SetTime(VacBotCommand): class SetTime(VacBotCommand):
def __init__(self, timestamp, timezone): def __init__(self, timestamp, timezone):
super().__init__('SetTime', {'time': {'t': timestamp, 'tz': timezone}}) super().__init__('SetTime', {'time': {'t': timestamp, 'tz': timezone}})
File diff suppressed because it is too large Load Diff
+5
View File
@@ -0,0 +1,5 @@
{
"name": "EcovacsBumper",
"domains": ["vacuum"],
"render_readme": true
}