Compare commits

..

6 Commits

Author SHA1 Message Date
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
5 changed files with 119 additions and 223 deletions
+28 -104
View File
@@ -1,16 +1,7 @@
"""Support for Ecovacs Deebot vacuums.""" """Support for Ecovacs Deebot vacuums."""
import asyncio
from functools import partial
import async_timeout
import random import random
import string import string
import logging #import asyncio ## to do will need to convert to slixmpp to do this i believe
# Use local sucks
from .sucks import EcoVacsAPI, VacBot
from homeassistant import exceptions
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import ( from homeassistant.const import (
CONF_USERNAME, CONF_USERNAME,
@@ -18,39 +9,36 @@ from homeassistant.const import (
CONF_COUNTRY, CONF_COUNTRY,
CONF_VERIFY_SSL, CONF_VERIFY_SSL,
EVENT_HOMEASSISTANT_STOP, EVENT_HOMEASSISTANT_STOP,
Platform,
) )
from homeassistant.core import HomeAssistant 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
#use local sucks
from .sucks import EcoVacsAPI, VacBot
from .const import ( from .const import (
ECOVACS_DEVICES, ECOVACS_DEVICES,
DOMAIN, DOMAIN,
PLATFORMS,
CONF_CONTINENT, CONF_CONTINENT,
LOGGER
) )
# from homeassistant.core import HomeAssistant CONFIG_SCHEMA = vol.Schema(
# from homeassistant.helpers import discovery {
# from homeassistant.helpers.typing import ConfigType DOMAIN: vol.Schema(
# import homeassistant.helpers.config_validation as cv {
# import voluptuous as vol vol.Required(CONF_USERNAME): cv.string,
vol.Required(CONF_PASSWORD): cv.string,
_LOGGER = logging.getLogger(__name__) vol.Required(CONF_COUNTRY): vol.All(vol.Lower, cv.string),
vol.Required(CONF_CONTINENT): vol.All(vol.Lower, cv.string),
# CONFIG_SCHEMA = vol.Schema( vol.Optional(CONF_VERIFY_SSL, default=True): cv.boolean, # can probably get rid of this and set verify ssl false if
# { }
# DOMAIN: vol.Schema( )
# { },
# vol.Required(CONF_USERNAME): cv.string, extra=vol.ALLOW_EXTRA,
# vol.Required(CONF_PASSWORD): cv.string, )
# vol.Required(CONF_COUNTRY): vol.All(vol.Lower, cv.string),
# vol.Required(CONF_CONTINENT): vol.All(vol.Lower, cv.string),
# vol.Optional(CONF_VERIFY_SSL, default=True): cv.boolean, # can probably get rid of this and set verify ssl false if
# }
# )
# },
# 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(
@@ -59,7 +47,7 @@ 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
@@ -73,10 +61,10 @@ def setup(hass: HomeAssistant, config: ConfigType) -> bool:
) )
devices = ecovacs_api.devices() devices = ecovacs_api.devices()
_LOGGER.debug("Ecobot devices: %s", devices) LOGGER.debug("Ecobot devices: %s", devices)
for device in devices: for device in devices:
_LOGGER.info( LOGGER.info(
"Discovered Ecovacs device on account: %s with nickname %s", "Discovered Ecovacs device on account: %s with nickname %s",
device.get("did"), device.get("did"),
device.get("nick"), device.get("nick"),
@@ -97,7 +85,7 @@ def setup(hass: HomeAssistant, config: ConfigType) -> bool:
def stop(event: object) -> None: def 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]: for device in hass.data[ECOVACS_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"),
) )
@@ -106,70 +94,6 @@ def setup(hass: HomeAssistant, config: ConfigType) -> bool:
# Listen for HA stop to disconnect. # Listen for HA stop to disconnect.
hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, stop) hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, 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) discovery.load_platform(hass, Platform.VACUUM, DOMAIN, {}, config)
return True return True
async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool:
"""Set the config entry up."""
# Set up Ecovacs platforms with config entry
_LOGGER.debug("Creating new Ecovacs component")
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
)
if not config_entry.options:
hass.config_entries.async_update_entry(
config_entry,
options={
CONF_CONTINUOUS: config_entry.data[CONF_CONTINUOUS],
CONF_DELAY: config_entry.data[CONF_DELAY],
},
)
roomba = await hass.async_add_executor_job(
partial(
RoombaFactory.create_roomba,
address=config_entry.data[CONF_HOST],
blid=config_entry.data[CONF_BLID],
password=config_entry.data[CONF_PASSWORD],
continuous=config_entry.options[CONF_CONTINUOUS],
delay=config_entry.options[CONF_DELAY],
)
)
try:
if not await async_connect_or_timeout(hass, roomba):
return False
except CannotConnect as err:
raise exceptions.ConfigEntryNotReady from err
async def _async_disconnect_roomba(event):
await async_disconnect_or_timeout(hass, roomba)
cancel_stop = hass.bus.async_listen_once(
EVENT_HOMEASSISTANT_STOP, _async_disconnect_roomba
)
hass.data.setdefault(DOMAIN, {})
hass.data[DOMAIN][config_entry.entry_id] = {
ROOMBA_SESSION: roomba,
BLID: config_entry.data[CONF_BLID],
CANCEL_STOP: cancel_stop,
}
await hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS)
if not config_entry.update_listeners:
config_entry.add_update_listener(async_update_options)
return True
+5 -5
View File
@@ -1,8 +1,8 @@
""" Ecovacs constants. """ import logging
from homeassistant.const import Platform LOGGER = logging.getLogger(__name__)
DOMAIN = "ecovacs" #ecovacs constants
PLATFORMS = [Platform.Vacuum] #init constants
ECOVACS_DEVICES = "ecovacs_devices" ECOVACS_DEVICES = "ecovacs_devices"
DOMAIN = "ecovacs"
CONF_CONTINENT = "continent" CONF_CONTINENT = "continent"
+67 -90
View File
@@ -2,8 +2,6 @@ import hashlib
import time import time
import requests import requests
import os import os
import logging
import aiohttp
from base64 import b64decode, b64encode from base64 import b64decode, b64encode
from collections import OrderedDict from collections import OrderedDict
from sleekxmppfs.xmlstream import ET from sleekxmppfs.xmlstream import ET
@@ -12,11 +10,9 @@ from sleekxmppfs.exceptions import XMPPError
from .sucks_mqtt import EcoVacsIOTMQ from .sucks_mqtt import EcoVacsIOTMQ
from .sucks_xmpp import EcoVacsXMPP from .sucks_xmpp import EcoVacsXMPP
#from .const import LOGGER from .const import LOGGER
from .sucks_const import * from .sucks_const import *
_LOGGER = logging.getLogger(__name__)
def str_to_bool_or_cert(s): def str_to_bool_or_cert(s):
if s == 'True' or s == True: if s == 'True' or s == True:
return True return True
@@ -31,10 +27,6 @@ def str_to_bool_or_cert(s):
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))
def get_ecovacs_api(device_id: str, username: str, password: str, country: str, continent: str, verify_ssl: bool, websession: Optional[aiohttp.ClientSession] = None)
""" Get Ecovacs api object """
return EcoVacsAPI(device_id, username, password, country, continent, verify_ssl, websession)
class EcoVacsAPI: class EcoVacsAPI:
CLIENT_KEY = "eJUWrzRv34qFSaYk" CLIENT_KEY = "eJUWrzRv34qFSaYk"
SECRET = "Cyu5jcR4zyK6QEPn1hdIGXB5QIDAQABMA0GC" SECRET = "Cyu5jcR4zyK6QEPn1hdIGXB5QIDAQABMA0GC"
@@ -47,15 +39,7 @@ class EcoVacsAPI:
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__( def __init__(self, device_id, account_id, password_hash, country, continent, verify_ssl=True):
self,
device_id,
account_id,
password_hash,
country,
continent,
verify_ssl=True,
websession: Optional[aiohttp.ClientSession] = None):
self.meta = { self.meta = {
'country': country, 'country': country,
'lang': 'en', 'lang': 'en',
@@ -70,7 +54,7 @@ class EcoVacsAPI:
#'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]
self.country = country self.country = country
self.continent = continent self.continent = continent
@@ -85,16 +69,9 @@ class EcoVacsAPI:
login_response = self.__call_login_by_it_token() login_response = self.__call_login_by_it_token()
self.user_access_token = login_response['token'] self.user_access_token = login_response['token']
if login_response['userId'] != self.uid: if login_response['userId'] != self.uid:
_LOGGER.debug("Switching to shorter UID " + login_response['userId']) LOGGER.debug("Switching to shorter UID " + login_response['userId'])
self.uid = login_response['userId'] self.uid = login_response['userId']
self.websession = websession LOGGER.debug("EcoVacsAPI connection complete")
_LOGGER.debug("EcoVacsAPI connection complete")
def ensure_session(self) -> aiohttp.ClientSession:
"""Ensure that we have an aiohttp ClientSession"""
if self.websession is None:
self.websession = aiohttp.ClientSession()
return self.websession
def __sign(self, params): def __sign(self, params):
result = params.copy() result = params.copy()
@@ -109,34 +86,34 @@ class EcoVacsAPI:
return result return result
def __call_main_api(self, function, *args): def __call_main_api(self, function, *args):
_LOGGER.debug("calling main api {} with {}".format(function, args)) LOGGER.debug("calling main api {} with {}".format(function, args))
params = OrderedDict(args) params = OrderedDict(args)
params['requestId'] = self.md5(time.time()) params['requestId'] = self.md5(time.time())
url = (EcoVacsAPI.MAIN_URL_FORMAT + "/" + function).format(**self.meta) url = (EcoVacsAPI.MAIN_URL_FORMAT + "/" + function).format(**self.meta)
api_response = requests.get(url, self.__sign(params), verify=self.verify_ssl) api_response = requests.get(url, self.__sign(params), verify=self.verify_ssl)
json = api_response.json() json = api_response.json()
_LOGGER.debug("got {}".format(json)) LOGGER.debug("got {}".format(json))
if json['code'] == '0000': if json['code'] == '0000':
return json['data'] return json['data']
elif json['code'] == '1005': elif json['code'] == '1005':
_LOGGER.error("incorrect email or password") LOGGER.error("incorrect email or password")
raise ValueError("incorrect email or password") raise ValueError("incorrect email or password")
else: else:
_LOGGER.error("call to {} failed with {}".format(function, json)) LOGGER.error("call to {} failed with {}".format(function, json))
raise RuntimeError("failure code {} ({}) for call {} and parameters {}".format( raise RuntimeError("failure code {} ({}) for call {} and parameters {}".format(
json['code'], json['msg'], function, args)) json['code'], json['msg'], function, args))
def __call_user_api(self, function, args): def __call_user_api(self, function, args):
_LOGGER.debug("calling user api {} with {}".format(function, args)) LOGGER.debug("calling user api {} with {}".format(function, args))
params = {'todo': function} params = {'todo': function}
params.update(args) params.update(args)
response = requests.post(EcoVacsAPI.USER_URL_FORMAT.format(continent=self.continent), json=params, verify=self.verify_ssl) response = requests.post(EcoVacsAPI.USER_URL_FORMAT.format(continent=self.continent), json=params, verify=self.verify_ssl)
json = response.json() json = response.json()
_LOGGER.debug("got {}".format(json)) LOGGER.debug("got {}".format(json))
if json['result'] == 'ok': if json['result'] == 'ok':
return json return json
else: else:
_LOGGER.error("call to {} failed with {}".format(function, json)) LOGGER.error("call to {} failed with {}".format(function, json))
raise RuntimeError( raise RuntimeError(
"failure {} ({}) for call {} and parameters {}".format(json['error'], json['errno'], function, params)) "failure {} ({}) for call {} and parameters {}".format(json['error'], json['errno'], function, params))
@@ -147,33 +124,33 @@ class EcoVacsAPI:
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:
if json['result'] == 'ok': if json['result'] == 'ok':
return json return json
elif json['result'] == 'fail': elif json['result'] == 'fail':
if json['error'] == 'set token error.': # If it is a set token error try again if json['error'] == 'set token error.': # If it is a set token error try again
if not 'set_token' in kwargs: if not 'set_token' in kwargs:
_LOGGER.debug("loginByItToken set token error, trying again (2/3)") LOGGER.debug("loginByItToken set token error, trying again (2/3)")
return self.__call_portal_api(self.USERSAPI, function, args, verify_ssl=verify_ssl, set_token=1) return self.__call_portal_api(self.USERSAPI, function, args, verify_ssl=verify_ssl, set_token=1)
elif kwargs.get('set_token') == 1: elif kwargs.get('set_token') == 1:
_LOGGER.debug("loginByItToken set token error, trying again with ww (3/3)") LOGGER.debug("loginByItToken set token error, trying again with ww (3/3)")
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
else: else:
_LOGGER.error("call to {} failed with {}".format(function, json)) LOGGER.error("call to {} failed with {}".format(function, json))
raise RuntimeError( raise RuntimeError(
"failure {} ({}) for call {} and parameters {}".format(json['error'], json['errno'], function, params)) "failure {} ({}) for call {} and parameters {}".format(json['error'], json['errno'], function, params))
@@ -198,31 +175,6 @@ class EcoVacsAPI:
} }
}, verify_ssl=self.verify_ssl)['devices'] }, verify_ssl=self.verify_ssl)['devices']
def SetIOTMQDevices(self, devices):
#Added for devices that utilize MQTT instead of XMPP for communication
for device in devices:
device['iotmq'] = False
if device['company'] == 'eco-ng': #Check if the device is part of the list
device['iotmq'] = True
return devices
def devices(self):
return self.SetIOTMQDevices(self.getdevices())
@staticmethod
def md5(text):
return hashlib.md5(bytes(str(text), 'utf8')).hexdigest()
@staticmethod
def encrypt(text):
from Crypto.PublicKey import RSA
from Crypto.Cipher import PKCS1_v1_5
key = RSA.import_key(b64decode(EcoVacsAPI.PUBLIC_KEY))
cipher = PKCS1_v1_5.new(key)
result = cipher.encrypt(bytes(text, 'utf8'))
return str(b64encode(result), 'utf8')
""" older currently unused code from before app update
def getiotProducts(self): def getiotProducts(self):
return self.__call_portal_api(self.PRODUCTAPI + '/getProductIotMap','', { return self.__call_portal_api(self.PRODUCTAPI + '/getProductIotMap','', {
'channel': '', 'channel': '',
@@ -242,8 +194,33 @@ class EcoVacsAPI:
for iotProduct in iotproducts: for iotProduct in iotproducts:
if device['class'] in iotProduct['classid']: if device['class'] in iotProduct['classid']:
device['iot_product'] = True device['iot_product'] = True
return devices return devices
"""
def SetIOTMQDevices(self, devices):
#Added for devices that utilize MQTT instead of XMPP for communication
for device in devices:
device['iotmq'] = False
if device['company'] == 'eco-ng': #Check if the device is part of the list
device['iotmq'] = True
return devices
def devices(self):
return self.SetIOTMQDevices(self.getdevices())
@staticmethod
def md5(text):
return hashlib.md5(bytes(str(text), 'utf8')).hexdigest()
@staticmethod
def encrypt(text):
from Crypto.PublicKey import RSA
from Crypto.Cipher import PKCS1_v1_5
key = RSA.import_key(b64decode(EcoVacsAPI.PUBLIC_KEY))
cipher = PKCS1_v1_5.new(key)
result = cipher.encrypt(bytes(text, 'utf8'))
return str(b64encode(result), 'utf8')
class EventEmitter(object): class EventEmitter(object):
"""A very simple event emitting system.""" """A very simple event emitting system."""
@@ -341,23 +318,23 @@ class VacBot():
error = event['errs'] error = event['errs']
if not error == '': if not error == '':
self.errorEvents.notify(error) self.errorEvents.notify(error)
_LOGGER.error("*** error = " + error) LOGGER.error("*** error = " + error)
def _handle_life_span(self, event): def _handle_life_span(self, event):
type = event['type'] type = event['type']
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.info("**********Component " + type + " has lifespan of " + str(lifespan) + ".") LOGGER.info("**********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.info("*** life_span " + type + " = " + str(lifespan)) LOGGER.info("*** life_span " + type + " = " + str(lifespan))
def _handle_clean_report(self, event): def _handle_clean_report(self, event):
type = event['type'] type = event['type']
@@ -369,7 +346,7 @@ class VacBot():
if statustype == CLEAN_ACTION_STOP or statustype == CLEAN_ACTION_PAUSE: if statustype == CLEAN_ACTION_STOP or statustype == CLEAN_ACTION_PAUSE:
type = statustype type = statustype
except KeyError: except KeyError:
_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)
@@ -377,22 +354,22 @@ class VacBot():
try: try:
fan = FAN_SPEED_FROM_ECOVACS[fan] fan = FAN_SPEED_FROM_ECOVACS[fan]
except KeyError: except KeyError:
_LOGGER.warning("Unknown fan speed: '" + fan + "'") LOGGER.warning("Unknown fan speed: '" + fan + "'")
self.fan_speed = fan self.fan_speed = fan
self.statusEvents.notify(self.vacuum_status) self.statusEvents.notify(self.vacuum_status)
if self.fan_speed: if self.fan_speed:
_LOGGER.info("*** clean_status = " + self.clean_status + " fan_speed = " + self.fan_speed) LOGGER.info("*** clean_status = " + self.clean_status + " fan_speed = " + self.fan_speed)
else: else:
_LOGGER.info("*** clean_status = " + self.clean_status + " fan_speed = None") LOGGER.info("*** clean_status = " + self.clean_status + " fan_speed = None")
def _handle_battery_info(self, iq): def _handle_battery_info(self, iq):
try: try:
self.battery_status = float(iq['power']) / 100 self.battery_status = float(iq['power']) / 100
except ValueError: except ValueError:
_LOGGER.warning("couldn't parse battery status " + ET.tostring(iq)) LOGGER.warning("couldn't parse battery status " + ET.tostring(iq))
else: else:
self.batteryEvents.notify(self.battery_status) self.batteryEvents.notify(self.battery_status)
_LOGGER.info("*** battery_status = {:.0%}".format(self.battery_status)) LOGGER.info("*** battery_status = {:.0%}".format(self.battery_status))
def _handle_charge_state(self, event): def _handle_charge_state(self, event):
if 'type' in event: if 'type' in event:
@@ -406,11 +383,11 @@ class VacBot():
status = 'idle' status = 'idle'
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
@@ -418,7 +395,7 @@ class VacBot():
# of what the vacuum is currently up to. # of what the vacuum is currently up to.
self.vacuum_status = status self.vacuum_status = status
self.statusEvents.notify(self.vacuum_status) self.statusEvents.notify(self.vacuum_status)
_LOGGER.info("*** charge_status = " + self.charge_status) LOGGER.info("*** charge_status = " + self.charge_status)
def _vacuum_address(self): def _vacuum_address(self):
if not self.vacuum['iotmq']: if not self.vacuum['iotmq']:
@@ -442,15 +419,15 @@ class VacBot():
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.error("*** Error type: " + err.etype) LOGGER.error("*** Error type: " + err.etype)
_LOGGER.error("*** Error condition: " + err.condition) LOGGER.error("*** Error condition: " + err.condition)
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)
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'
@@ -473,9 +450,9 @@ class VacBot():
self.run(GetLifeSpan('side_brush')) self.run(GetLifeSpan('side_brush'))
self.run(GetLifeSpan('filter')) self.run(GetLifeSpan('filter'))
except XMPPError as err: except XMPPError as err:
_LOGGER.warning("Component refresh requests failed to reach VacBot. Will try again later.") LOGGER.warning("Component refresh requests failed to reach VacBot. Will try again later.")
_LOGGER.error("*** Error type: " + err.etype) LOGGER.error("*** Error type: " + err.etype)
_LOGGER.error("*** Error condition: " + err.condition) LOGGER.error("*** Error condition: " + err.condition)
def refresh_statuses(self): def refresh_statuses(self):
try: try:
@@ -483,9 +460,9 @@ class VacBot():
self.run(GetChargeState()) self.run(GetChargeState())
self.run(GetBatteryState()) self.run(GetBatteryState())
except XMPPError as err: except XMPPError as err:
_LOGGER.warning("Initial status requests failed to reach VacBot. Will try again on next ping.") LOGGER.warning("Initial status requests failed to reach VacBot. Will try again on next ping.")
_LOGGER.error("*** Error type: " + err.etype) LOGGER.error("*** Error type: " + err.etype)
_LOGGER.error("*** Error condition: " + err.condition) LOGGER.error("*** Error condition: " + err.condition)
def request_all_statuses(self): def request_all_statuses(self):
self.refresh_statuses() self.refresh_statuses()
+12 -15
View File
@@ -4,16 +4,13 @@ import threading
import ssl import ssl
import requests import requests
import stringcase import stringcase
import logging
from threading import Event from threading import Event
from paho.mqtt.client import Client as ClientMQTT from paho.mqtt.client import Client as ClientMQTT
from paho.mqtt import publish as MQTTPublish from paho.mqtt import publish as MQTTPublish
from paho.mqtt import subscribe as MQTTSubscribe from paho.mqtt import subscribe as MQTTSubscribe
from sleekxmppfs.xmlstream import ET from sleekxmppfs.xmlstream import ET
#from .const import LOGGER from .const import LOGGER
_LOGGER = logging.getLogger(__name__)
#This is used by EcoVacsIOTMQ and EcoVacsXMPP for _ctl_to_dict #This is used by EcoVacsIOTMQ and EcoVacsXMPP for _ctl_to_dict
def RepresentsInt(stringvar): def RepresentsInt(stringvar):
@@ -86,19 +83,19 @@ class EcoVacsIOTMQ(ClientMQTT):
def on_connect(self, client, userdata, flags, rc): def on_connect(self, client, userdata, flags, rc):
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()
#def on_log(self, client, userdata, level, buf): #This is very noisy and verbose #def on_log(self, client, userdata, level, buf): #This is very noisy and verbose
# _LOGGER.debug("EcoVacsMQTT Log: {} ".format(buf)) # LOGGER.debug("EcoVacsMQTT Log: {} ".format(buf))
def send_ping(self): def send_ping(self):
_LOGGER.debug("*** MQTT sending ping ***") LOGGER.debug("*** MQTT sending ping ***")
rc = self._send_simple_command(MQTTPublish.paho.PINGREQ) rc = self._send_simple_command(MQTTPublish.paho.PINGREQ)
if rc == MQTTPublish.paho.MQTT_ERR_SUCCESS: if rc == MQTTPublish.paho.MQTT_ERR_SUCCESS:
return True return True
@@ -109,7 +106,7 @@ class EcoVacsIOTMQ(ClientMQTT):
if action.name == "Clean": #For handling Clean when action not specified (i.e. CLI) if action.name == "Clean": #For handling Clean when action not specified (i.e. CLI)
action.args['clean']['act'] = CLEAN_ACTION_TO_ECOVACS['start'] #Inject a start action action.args['clean']['act'] = CLEAN_ACTION_TO_ECOVACS['start'] #Inject a start action
c = self._wrap_command(action, recipient) c = self._wrap_command(action, recipient)
_LOGGER.debug('Sending command {0}'.format(c)) LOGGER.debug('Sending command {0}'.format(c))
self._handle_ctl_api(action, self._handle_ctl_api(action,
self.__call_iotdevmanager_api(c ,verify_ssl=self.verify_ssl ) self.__call_iotdevmanager_api(c ,verify_ssl=self.verify_ssl )
) )
@@ -137,7 +134,7 @@ class EcoVacsIOTMQ(ClientMQTT):
} }
def __call_iotdevmanager_api(self, args, verify_ssl=True): def __call_iotdevmanager_api(self, args, verify_ssl=True):
_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)
@@ -145,7 +142,7 @@ class EcoVacsIOTMQ(ClientMQTT):
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
response = requests.post(url, json=params, timeout=3, verify=verify_ssl) #May think about having timeout as an arg that could be provided in the future response = requests.post(url, json=params, timeout=3, verify=verify_ssl) #May think about having timeout as an arg that could be provided in the future
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':
@@ -154,11 +151,11 @@ class EcoVacsIOTMQ(ClientMQTT):
if 'debug' in json: if 'debug' in json:
if json['debug'] == 'wait for response timed out': if json['debug'] == 'wait for response timed out':
#TODO - Maybe handle timeout for IOT better in the future #TODO - Maybe handle timeout for IOT better in the future
_LOGGER.error("call to iotdevmanager failed with {}".format(json)) LOGGER.error("call to iotdevmanager failed with {}".format(json))
return {} return {}
else: else:
#TODO - Not sure if we want to raise an error yet, just return empty for now #TODO - Not sure if we want to raise an error yet, just return empty for now
_LOGGER.error("call to iotdevmanager failed with {}".format(json)) LOGGER.error("call to iotdevmanager failed with {}".format(json))
return {} return {}
#raise RuntimeError( #raise RuntimeError(
#"failure {} ({}) for call {} and parameters {}".format(json['error'], json['errno'], function, params)) #"failure {} ({}) for call {} and parameters {}".format(json['error'], json['errno'], function, params))
@@ -198,7 +195,7 @@ class EcoVacsIOTMQ(ClientMQTT):
return result return result
def _handle_ctl_mqtt(self, client, userdata, message): def _handle_ctl_mqtt(self, client, userdata, message):
#_LOGGER.debug("EcoVacs MQTT Received Message on Topic: {} - Message: {}".format(message.topic, str(message.payload.decode("utf-8")))) #LOGGER.debug("EcoVacs MQTT Received Message on Topic: {} - Message: {}".format(message.topic, str(message.payload.decode("utf-8"))))
as_dict = self._ctl_to_dict_mqtt(message.topic, str(message.payload.decode("utf-8"))) as_dict = self._ctl_to_dict_mqtt(message.topic, str(message.payload.decode("utf-8")))
if as_dict is not None: if as_dict is not None:
for s in self.ctl_subscribers: for s in self.ctl_subscribers:
+7 -9
View File
@@ -1,13 +1,11 @@
import stringcase import stringcase
import random import random
import logging
from threading import Event from threading import Event
from sleekxmppfs import ClientXMPP, Callback, MatchXPath from sleekxmppfs import ClientXMPP, Callback, MatchXPath
from sleekxmppfs.xmlstream import ET from sleekxmppfs.xmlstream import ET
#from sleekxmppfs.exceptions import XMPPError #from sleekxmppfs.exceptions import XMPPError
#from .const import LOGGER from .const import LOGGER
_LOGGER = logging.getLogger(__name__)
#This is used by EcoVacsIOTMQ and EcoVacsXMPP for _ctl_to_dict #This is used by EcoVacsIOTMQ and EcoVacsXMPP for _ctl_to_dict
def RepresentsInt(stringvar): def RepresentsInt(stringvar):
try: try:
@@ -37,8 +35,8 @@ class EcoVacsXMPP(ClientXMPP):
self.ready_flag.wait() self.ready_flag.wait()
def session_start(self, event): def session_start(self, event):
_LOGGER.debug("----------------- starting session ----------------") LOGGER.debug("----------------- starting session ----------------")
_LOGGER.debug("event = {}".format(event)) LOGGER.debug("event = {}".format(event))
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))
@@ -65,7 +63,7 @@ class EcoVacsXMPP(ClientXMPP):
try: # check for child xml try: # check for child xml
childxml = xml[0] childxml = xml[0]
except IndexError: except IndexError:
_LOGGER.debug("No child xml") LOGGER.debug("No child xml")
if 'td' not in result: if 'td' not in result:
# 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
@@ -100,7 +98,7 @@ class EcoVacsXMPP(ClientXMPP):
def send_command(self, xml, recipient): def send_command(self, xml, recipient):
c = self._wrap_command(xml, recipient) c = self._wrap_command(xml, recipient)
_LOGGER.debug('Sending command {0}'.format(c)) LOGGER.debug('Sending command {0}'.format(c))
c.send() c.send()
def _wrap_command(self, ctl, recipient): def _wrap_command(self, ctl, recipient):
@@ -131,12 +129,12 @@ class EcoVacsXMPP(ClientXMPP):
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 # used some code from a sleekxmppfs plugin, seems to work fine
def _handle_ping(self, iq): def _handle_ping(self, iq):
_LOGGER.debug("Pinged by %s", iq['from']) LOGGER.debug("Pinged by %s", iq['from'])
iq.reply().send() iq.reply().send()
def connect_and_wait_until_ready(self): def connect_and_wait_until_ready(self):