Compare commits
13 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| dc032f6719 | |||
| d2a0537b37 | |||
| d5370a5152 | |||
| a483d30625 | |||
| 9b52bc8e1d | |||
| 9652c47d42 | |||
| 45244f3744 | |||
| 646ff4ecbb | |||
| 3d5fd6c024 | |||
| 6cb13ce8a4 | |||
| c7051bd51b | |||
| 2b66ffa426 | |||
| 2b252c0b22 |
@@ -17,13 +17,12 @@ from homeassistant.helpers.typing import ConfigType
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
import voluptuous as vol
|
||||
#use local sucks
|
||||
from .sucks import EcoVacsAPI, VacBot
|
||||
from .const import (
|
||||
ECOVACS_DEVICES,
|
||||
DOMAIN,
|
||||
CONF_CONTINENT,
|
||||
LOGGER
|
||||
)
|
||||
from .sucks import VacBot
|
||||
from .sucks_api import EcoVacsAPI
|
||||
from .const import *
|
||||
|
||||
import logging
|
||||
LOGGER = logging.getLogger(__name__)
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema(
|
||||
{
|
||||
|
||||
@@ -1,6 +1,3 @@
|
||||
import logging
|
||||
LOGGER = logging.getLogger(__name__)
|
||||
|
||||
#ecovacs constants
|
||||
#init constants
|
||||
ECOVACS_DEVICES = "ecovacs_devices"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"domain": "ecovacs",
|
||||
"name": "Ecovacs Bumper",
|
||||
"version": "1.4.4",
|
||||
"version": "1.5.3",
|
||||
"documentation": "https://github.com/bittles/ha_ecovacs_bumper",
|
||||
"issue_tracker": "https://github.com/bittles/ha_ecovacs_bumper/issues",
|
||||
"requirements": ["sleekxmppfs==1.4.1", "requests>=2.18", "pycryptodome>=3.4", "pycountry-convert>=0.5", "paho-mqtt>=1.4", "stringcase>=1.2"],
|
||||
|
||||
@@ -1,226 +1,20 @@
|
||||
import hashlib
|
||||
import time
|
||||
import requests
|
||||
#import hashlib
|
||||
#import time
|
||||
#import requests
|
||||
import os
|
||||
from base64 import b64decode, b64encode
|
||||
from collections import OrderedDict
|
||||
#from base64 import b64decode, b64encode
|
||||
#from collections import OrderedDict
|
||||
from sleekxmppfs.xmlstream import ET
|
||||
from sleekxmppfs.exceptions import XMPPError
|
||||
|
||||
#from . import sucks_api
|
||||
from .sucks_mqtt import EcoVacsIOTMQ
|
||||
from .sucks_xmpp import EcoVacsXMPP
|
||||
|
||||
from .const import LOGGER
|
||||
from .sucks_const import *
|
||||
|
||||
def str_to_bool_or_cert(s):
|
||||
if s == 'True' or s == True:
|
||||
return True
|
||||
elif s == 'False' or s == False:
|
||||
return False
|
||||
else:
|
||||
if not s == None:
|
||||
if os.path.exists(s): # User could provide a path to a CA Cert as well, which is useful for Bumper
|
||||
if os.path.isfile(s):
|
||||
return s
|
||||
else:
|
||||
raise ValueError("Certificate path provided is not a file - {}".format(s))
|
||||
raise ValueError("Cannot covert {} to a bool or certificate path".format(s))
|
||||
|
||||
class EcoVacsAPI:
|
||||
CLIENT_KEY = "eJUWrzRv34qFSaYk"
|
||||
SECRET = "Cyu5jcR4zyK6QEPn1hdIGXB5QIDAQABMA0GC"
|
||||
PUBLIC_KEY = 'MIIB/TCCAWYCCQDJ7TMYJFzqYDANBgkqhkiG9w0BAQUFADBCMQswCQYDVQQGEwJjbjEVMBMGA1UEBwwMRGVmYXVsdCBDaXR5MRwwGgYDVQQKDBNEZWZhdWx0IENvbXBhbnkgTHRkMCAXDTE3MDUwOTA1MTkxMFoYDzIxMTcwNDE1MDUxOTEwWjBCMQswCQYDVQQGEwJjbjEVMBMGA1UEBwwMRGVmYXVsdCBDaXR5MRwwGgYDVQQKDBNEZWZhdWx0IENvbXBhbnkgTHRkMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDb8V0OYUGP3Fs63E1gJzJh+7iqeymjFUKJUqSD60nhWReZ+Fg3tZvKKqgNcgl7EGXp1yNifJKUNC/SedFG1IJRh5hBeDMGq0m0RQYDpf9l0umqYURpJ5fmfvH/gjfHe3Eg/NTLm7QEa0a0Il2t3Cyu5jcR4zyK6QEPn1hdIGXB5QIDAQABMA0GCSqGSIb3DQEBBQUAA4GBANhIMT0+IyJa9SU8AEyaWZZmT2KEYrjakuadOvlkn3vFdhpvNpnnXiL+cyWy2oU1Q9MAdCTiOPfXmAQt8zIvP2JC8j6yRTcxJCvBwORDyv/uBtXFxBPEC6MDfzU2gKAaHeeJUWrzRv34qFSaYkYta8canK+PSInylQTjJK9VqmjQ'
|
||||
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'
|
||||
PORTAL_URL_FORMAT = 'https://portal-{continent}.ecouser.net/api'
|
||||
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
|
||||
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'
|
||||
|
||||
def __init__(self, device_id, account_id, password_hash, country, continent, verify_ssl=True):
|
||||
self.meta = {
|
||||
'country': country,
|
||||
'lang': 'en',
|
||||
'deviceId': device_id,
|
||||
'appCode': 'i_eco_e',
|
||||
#'appCode': 'i_eco_a' - iphone
|
||||
'appVersion': '1.3.5',
|
||||
#'appVersion': '1.4.6' - iphone
|
||||
'channel': 'c_googleplay',
|
||||
#'channel': 'c_iphone', - iphone
|
||||
'deviceType': '1'
|
||||
#'deviceType': '2' - iphone
|
||||
}
|
||||
self.verify_ssl = str_to_bool_or_cert(verify_ssl)
|
||||
LOGGER.debug("Setting up EcoVacsAPI")
|
||||
self.resource = device_id[0:8]
|
||||
self.country = country
|
||||
self.continent = continent
|
||||
login_info = self.__call_main_api('user/login',
|
||||
('account', self.encrypt(account_id)),
|
||||
('password', self.encrypt(password_hash)))
|
||||
self.uid = login_info['uid']
|
||||
self.login_access_token = login_info['accessToken']
|
||||
self.auth_code = self.__call_main_api('user/getAuthCode',
|
||||
('uid', self.uid),
|
||||
('accessToken', self.login_access_token))['authCode']
|
||||
login_response = self.__call_login_by_it_token()
|
||||
self.user_access_token = login_response['token']
|
||||
if login_response['userId'] != self.uid:
|
||||
LOGGER.debug("Switching to shorter UID " + login_response['userId'])
|
||||
self.uid = login_response['userId']
|
||||
LOGGER.debug("EcoVacsAPI connection complete")
|
||||
|
||||
def __sign(self, params):
|
||||
result = params.copy()
|
||||
result['authTimespan'] = int(time.time() * 1000)
|
||||
result['authTimeZone'] = 'GMT-8'
|
||||
sign_on = self.meta.copy()
|
||||
sign_on.update(result)
|
||||
sign_on_text = EcoVacsAPI.CLIENT_KEY + ''.join(
|
||||
[k + '=' + str(sign_on[k]) for k in sorted(sign_on.keys())]) + EcoVacsAPI.SECRET
|
||||
result['authAppkey'] = EcoVacsAPI.CLIENT_KEY
|
||||
result['authSign'] = self.md5(sign_on_text)
|
||||
return result
|
||||
|
||||
def __call_main_api(self, function, *args):
|
||||
LOGGER.debug("calling main api {} with {}".format(function, args))
|
||||
params = OrderedDict(args)
|
||||
params['requestId'] = self.md5(time.time())
|
||||
url = (EcoVacsAPI.MAIN_URL_FORMAT + "/" + function).format(**self.meta)
|
||||
api_response = requests.get(url, self.__sign(params), verify=self.verify_ssl)
|
||||
json = api_response.json()
|
||||
LOGGER.debug("got {}".format(json))
|
||||
if json['code'] == '0000':
|
||||
return json['data']
|
||||
elif json['code'] == '1005':
|
||||
LOGGER.error("incorrect email or password")
|
||||
raise ValueError("incorrect email or password")
|
||||
else:
|
||||
LOGGER.error("call to {} failed with {}".format(function, json))
|
||||
raise RuntimeError("failure code {} ({}) for call {} and parameters {}".format(
|
||||
json['code'], json['msg'], function, args))
|
||||
|
||||
def __call_user_api(self, function, args):
|
||||
LOGGER.debug("calling user api {} with {}".format(function, args))
|
||||
params = {'todo': function}
|
||||
params.update(args)
|
||||
response = requests.post(EcoVacsAPI.USER_URL_FORMAT.format(continent=self.continent), json=params, verify=self.verify_ssl)
|
||||
json = response.json()
|
||||
LOGGER.debug("got {}".format(json))
|
||||
if json['result'] == 'ok':
|
||||
return json
|
||||
else:
|
||||
LOGGER.error("call to {} failed with {}".format(function, json))
|
||||
raise RuntimeError(
|
||||
"failure {} ({}) for call {} and parameters {}".format(json['error'], json['errno'], function, params))
|
||||
|
||||
def __call_portal_api(self, api, function, args, verify_ssl=True, **kwargs):
|
||||
if api == self.USERSAPI:
|
||||
params = {'todo': function}
|
||||
params.update(args)
|
||||
else:
|
||||
params = {}
|
||||
params.update(args)
|
||||
LOGGER.debug("calling portal api {} function {} with {}".format(api, function, params))
|
||||
continent = self.continent
|
||||
if 'continent' in kwargs:
|
||||
continent = kwargs.get('continent')
|
||||
url = (EcoVacsAPI.PORTAL_URL_FORMAT + "/" + api).format(continent=continent, **self.meta)
|
||||
response = requests.post(url, json=params, verify=verify_ssl)
|
||||
json = response.json()
|
||||
LOGGER.debug("got {}".format(json))
|
||||
if api == self.USERSAPI:
|
||||
if json['result'] == 'ok':
|
||||
return json
|
||||
elif json['result'] == 'fail':
|
||||
if json['error'] == 'set token error.': # If it is a set token error try again
|
||||
if not 'set_token' in kwargs:
|
||||
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)
|
||||
elif kwargs.get('set_token') == 1:
|
||||
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")
|
||||
else:
|
||||
LOGGER.debug("loginByItToken set token error, failed after 3 attempts")
|
||||
if api.startswith(self.PRODUCTAPI):
|
||||
if json['code'] == 0:
|
||||
return json
|
||||
|
||||
else:
|
||||
LOGGER.error("call to {} failed with {}".format(function, json))
|
||||
raise RuntimeError(
|
||||
"failure {} ({}) for call {} and parameters {}".format(json['error'], json['errno'], function, params))
|
||||
|
||||
def __call_login_by_it_token(self):
|
||||
return self.__call_portal_api(self.USERSAPI,'loginByItToken',
|
||||
{'country': self.meta['country'].upper(),
|
||||
'resource': self.resource,
|
||||
'realm': EcoVacsAPI.REALM,
|
||||
'userId': self.uid,
|
||||
'token': self.auth_code}
|
||||
, verify_ssl=self.verify_ssl)
|
||||
|
||||
def getdevices(self):
|
||||
return self.__call_portal_api(self.USERSAPI,'GetDeviceList', {
|
||||
'userid': self.uid,
|
||||
'auth': {
|
||||
'with': 'users',
|
||||
'userid': self.uid,
|
||||
'realm': EcoVacsAPI.REALM,
|
||||
'token': self.user_access_token,
|
||||
'resource': self.resource
|
||||
}
|
||||
}, verify_ssl=self.verify_ssl)['devices']
|
||||
|
||||
def getiotProducts(self):
|
||||
return self.__call_portal_api(self.PRODUCTAPI + '/getProductIotMap','', {
|
||||
'channel': '',
|
||||
'auth': {
|
||||
'with': 'users',
|
||||
'userid': self.uid,
|
||||
'realm': EcoVacsAPI.REALM,
|
||||
'token': self.user_access_token,
|
||||
'resource': self.resource
|
||||
}
|
||||
}, verify_ssl=self.verify_ssl)['data']
|
||||
|
||||
def SetIOTDevices(self, devices, iotproducts):
|
||||
#Originally added for D900, and not actively used in code now - Not sure what the app checks the items in this list for
|
||||
for device in devices: #Check if the device is part of iotProducts
|
||||
device['iot_product'] = False
|
||||
for iotProduct in iotproducts:
|
||||
if device['class'] in iotProduct['classid']:
|
||||
device['iot_product'] = True
|
||||
|
||||
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')
|
||||
import logging
|
||||
LOGGER = logging.getLogger(__name__)
|
||||
|
||||
class EventEmitter(object):
|
||||
"""A very simple event emitting system."""
|
||||
@@ -312,14 +106,40 @@ class VacBot():
|
||||
getattr(self, method)(ctl)
|
||||
|
||||
def _handle_error(self, event):
|
||||
if 'error' in event:
|
||||
error = event['error']
|
||||
elif 'errs' in event:
|
||||
error = event['errs']
|
||||
if not error == '':
|
||||
if 'error' in event or 'errs' in event:
|
||||
error = '' # init error var so it's available outside of first if loop
|
||||
if 'error' in event:
|
||||
error = event['error']
|
||||
elif 'errs' in event:
|
||||
error = event['errs']
|
||||
self.errorEvents.notify(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):
|
||||
type = event['type']
|
||||
try:
|
||||
|
||||
@@ -0,0 +1,224 @@
|
||||
import hashlib
|
||||
import time
|
||||
import requests
|
||||
#import os
|
||||
from base64 import b64decode, b64encode
|
||||
from collections import OrderedDict
|
||||
#from sleekxmppfs.xmlstream import ET
|
||||
#from sleekxmppfs.exceptions import XMPPError
|
||||
|
||||
#from .sucks_mqtt import EcoVacsIOTMQ
|
||||
#from .sucks_xmpp import EcoVacsXMPP
|
||||
from .sucks_api_const import *
|
||||
|
||||
import logging
|
||||
LOGGER = logging.getLogger(__name__)
|
||||
|
||||
def str_to_bool_or_cert(s):
|
||||
if s == 'True' or s == True:
|
||||
return True
|
||||
elif s == 'False' or s == False:
|
||||
return False
|
||||
else:
|
||||
if not s == None:
|
||||
if os.path.exists(s): # User could provide a path to a CA Cert as well, which is useful for Bumper
|
||||
if os.path.isfile(s):
|
||||
return s
|
||||
else:
|
||||
raise ValueError("Certificate path provided is not a file - {}".format(s))
|
||||
raise ValueError("Cannot covert {} to a bool or certificate path".format(s))
|
||||
|
||||
class EcoVacsAPI:
|
||||
CLIENT_KEY = API_CLIENT_KEY
|
||||
SECRET = API_SECRET
|
||||
PUBLIC_KEY = API_PUBLIC_KEY
|
||||
MAIN_URL_FORMAT = API_MAIN_URL_FORMAT
|
||||
USER_URL_FORMAT = API_USER_URL_FORMAT
|
||||
PORTAL_URL_FORMAT = API_PORTAL_URL_FORMAT
|
||||
USERSAPI = API_USERSAPI
|
||||
IOTDEVMANAGERAPI = API_IOTDEVMANAGERAPI # IOT Device Manager - This provides control of "IOT" products via RestAPI, some bots use this instead of XMPP
|
||||
PRODUCTAPI = API_PRODUCTAPI # 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 = API_REALM
|
||||
|
||||
def __init__(self, device_id, account_id, password_hash, country, continent, verify_ssl=True):
|
||||
self.meta = {
|
||||
'country': country,
|
||||
'lang': 'en',
|
||||
'deviceId': device_id,
|
||||
'appCode': 'i_eco_e',
|
||||
#'appCode': 'i_eco_a' - iphone
|
||||
'appVersion': '1.3.5',
|
||||
#'appVersion': '1.4.6' - iphone
|
||||
'channel': 'c_googleplay',
|
||||
#'channel': 'c_iphone', - iphone
|
||||
'deviceType': '1'
|
||||
#'deviceType': '2' - iphone
|
||||
}
|
||||
self.verify_ssl = str_to_bool_or_cert(verify_ssl)
|
||||
LOGGER.debug("Setting up EcoVacsAPI")
|
||||
self.resource = device_id[0:8]
|
||||
self.country = country
|
||||
self.continent = continent
|
||||
login_info = self.__call_main_api('user/login',
|
||||
('account', self.encrypt(account_id)),
|
||||
('password', self.encrypt(password_hash)))
|
||||
self.uid = login_info['uid']
|
||||
self.login_access_token = login_info['accessToken']
|
||||
self.auth_code = self.__call_main_api('user/getAuthCode',
|
||||
('uid', self.uid),
|
||||
('accessToken', self.login_access_token))['authCode']
|
||||
login_response = self.__call_login_by_it_token()
|
||||
self.user_access_token = login_response['token']
|
||||
if login_response['userId'] != self.uid:
|
||||
LOGGER.debug("Switching to shorter UID " + login_response['userId'])
|
||||
self.uid = login_response['userId']
|
||||
LOGGER.debug("EcoVacsAPI connection complete")
|
||||
|
||||
def __sign(self, params):
|
||||
result = params.copy()
|
||||
result['authTimespan'] = int(time.time() * 1000)
|
||||
result['authTimeZone'] = 'GMT-8'
|
||||
sign_on = self.meta.copy()
|
||||
sign_on.update(result)
|
||||
sign_on_text = EcoVacsAPI.CLIENT_KEY + ''.join(
|
||||
[k + '=' + str(sign_on[k]) for k in sorted(sign_on.keys())]) + EcoVacsAPI.SECRET
|
||||
result['authAppkey'] = EcoVacsAPI.CLIENT_KEY
|
||||
result['authSign'] = self.md5(sign_on_text)
|
||||
return result
|
||||
|
||||
def __call_main_api(self, function, *args):
|
||||
LOGGER.debug("calling main api {} with {}".format(function, args))
|
||||
params = OrderedDict(args)
|
||||
params['requestId'] = self.md5(time.time())
|
||||
url = (EcoVacsAPI.MAIN_URL_FORMAT + "/" + function).format(**self.meta)
|
||||
api_response = requests.get(url, self.__sign(params), verify=self.verify_ssl)
|
||||
json = api_response.json()
|
||||
LOGGER.debug("got {}".format(json))
|
||||
if json['code'] == '0000':
|
||||
return json['data']
|
||||
elif json['code'] == '1005':
|
||||
LOGGER.error("incorrect email or password")
|
||||
raise ValueError("incorrect email or password")
|
||||
else:
|
||||
LOGGER.error("call to {} failed with {}".format(function, json))
|
||||
raise RuntimeError("failure code {} ({}) for call {} and parameters {}".format(
|
||||
json['code'], json['msg'], function, args))
|
||||
|
||||
def __call_user_api(self, function, args):
|
||||
LOGGER.debug("calling user api {} with {}".format(function, args))
|
||||
params = {'todo': function}
|
||||
params.update(args)
|
||||
response = requests.post(EcoVacsAPI.USER_URL_FORMAT.format(continent=self.continent), json=params, verify=self.verify_ssl)
|
||||
json = response.json()
|
||||
LOGGER.debug("got {}".format(json))
|
||||
if json['result'] == 'ok':
|
||||
return json
|
||||
else:
|
||||
LOGGER.error("call to {} failed with {}".format(function, json))
|
||||
raise RuntimeError(
|
||||
"failure {} ({}) for call {} and parameters {}".format(json['error'], json['errno'], function, params))
|
||||
|
||||
def __call_portal_api(self, api, function, args, verify_ssl=True, **kwargs):
|
||||
if api == self.USERSAPI:
|
||||
params = {'todo': function}
|
||||
params.update(args)
|
||||
else:
|
||||
params = {}
|
||||
params.update(args)
|
||||
LOGGER.debug("calling portal api {} function {} with {}".format(api, function, params))
|
||||
continent = self.continent
|
||||
if 'continent' in kwargs:
|
||||
continent = kwargs.get('continent')
|
||||
url = (EcoVacsAPI.PORTAL_URL_FORMAT + "/" + api).format(continent=continent, **self.meta)
|
||||
response = requests.post(url, json=params, verify=verify_ssl)
|
||||
json = response.json()
|
||||
LOGGER.debug("got {}".format(json))
|
||||
if api == self.USERSAPI:
|
||||
if json['result'] == 'ok':
|
||||
return json
|
||||
elif json['result'] == 'fail':
|
||||
if json['error'] == 'set token error.': # If it is a set token error try again
|
||||
if not 'set_token' in kwargs:
|
||||
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)
|
||||
elif kwargs.get('set_token') == 1:
|
||||
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")
|
||||
else:
|
||||
LOGGER.debug("loginByItToken set token error, failed after 3 attempts")
|
||||
if api.startswith(self.PRODUCTAPI):
|
||||
if json['code'] == 0:
|
||||
return json
|
||||
|
||||
else:
|
||||
LOGGER.error("call to {} failed with {}".format(function, json))
|
||||
raise RuntimeError(
|
||||
"failure {} ({}) for call {} and parameters {}".format(json['error'], json['errno'], function, params))
|
||||
|
||||
def __call_login_by_it_token(self):
|
||||
return self.__call_portal_api(self.USERSAPI,'loginByItToken',
|
||||
{'country': self.meta['country'].upper(),
|
||||
'resource': self.resource,
|
||||
'realm': EcoVacsAPI.REALM,
|
||||
'userId': self.uid,
|
||||
'token': self.auth_code}
|
||||
, verify_ssl=self.verify_ssl)
|
||||
|
||||
def getdevices(self):
|
||||
return self.__call_portal_api(self.USERSAPI,'GetDeviceList', {
|
||||
'userid': self.uid,
|
||||
'auth': {
|
||||
'with': 'users',
|
||||
'userid': self.uid,
|
||||
'realm': EcoVacsAPI.REALM,
|
||||
'token': self.user_access_token,
|
||||
'resource': self.resource
|
||||
}
|
||||
}, verify_ssl=self.verify_ssl)['devices']
|
||||
|
||||
def getiotProducts(self):
|
||||
return self.__call_portal_api(self.PRODUCTAPI + '/getProductIotMap','', {
|
||||
'channel': '',
|
||||
'auth': {
|
||||
'with': 'users',
|
||||
'userid': self.uid,
|
||||
'realm': EcoVacsAPI.REALM,
|
||||
'token': self.user_access_token,
|
||||
'resource': self.resource
|
||||
}
|
||||
}, verify_ssl=self.verify_ssl)['data']
|
||||
|
||||
def SetIOTDevices(self, devices, iotproducts):
|
||||
#Originally added for D900, and not actively used in code now - Not sure what the app checks the items in this list for
|
||||
for device in devices: #Check if the device is part of iotProducts
|
||||
device['iot_product'] = False
|
||||
for iotProduct in iotproducts:
|
||||
if device['class'] in iotProduct['classid']:
|
||||
device['iot_product'] = True
|
||||
|
||||
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')
|
||||
@@ -0,0 +1,11 @@
|
||||
# API Constants
|
||||
API_CLIENT_KEY = "eJUWrzRv34qFSaYk"
|
||||
API_SECRET = "Cyu5jcR4zyK6QEPn1hdIGXB5QIDAQABMA0GC"
|
||||
API_PUBLIC_KEY = 'MIIB/TCCAWYCCQDJ7TMYJFzqYDANBgkqhkiG9w0BAQUFADBCMQswCQYDVQQGEwJjbjEVMBMGA1UEBwwMRGVmYXVsdCBDaXR5MRwwGgYDVQQKDBNEZWZhdWx0IENvbXBhbnkgTHRkMCAXDTE3MDUwOTA1MTkxMFoYDzIxMTcwNDE1MDUxOTEwWjBCMQswCQYDVQQGEwJjbjEVMBMGA1UEBwwMRGVmYXVsdCBDaXR5MRwwGgYDVQQKDBNEZWZhdWx0IENvbXBhbnkgTHRkMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDb8V0OYUGP3Fs63E1gJzJh+7iqeymjFUKJUqSD60nhWReZ+Fg3tZvKKqgNcgl7EGXp1yNifJKUNC/SedFG1IJRh5hBeDMGq0m0RQYDpf9l0umqYURpJ5fmfvH/gjfHe3Eg/NTLm7QEa0a0Il2t3Cyu5jcR4zyK6QEPn1hdIGXB5QIDAQABMA0GCSqGSIb3DQEBBQUAA4GBANhIMT0+IyJa9SU8AEyaWZZmT2KEYrjakuadOvlkn3vFdhpvNpnnXiL+cyWy2oU1Q9MAdCTiOPfXmAQt8zIvP2JC8j6yRTcxJCvBwORDyv/uBtXFxBPEC6MDfzU2gKAaHeeJUWrzRv34qFSaYkYta8canK+PSInylQTjJK9VqmjQ'
|
||||
API_MAIN_URL_FORMAT = 'https://eco-{country}-api.ecovacs.com/v1/private/{country}/{lang}/{deviceId}/{appCode}/{appVersion}/{channel}/{deviceType}'
|
||||
API_USER_URL_FORMAT = 'https://users-{continent}.ecouser.net:8000/user.do'
|
||||
API_PORTAL_URL_FORMAT = 'https://portal-{continent}.ecouser.net/api'
|
||||
API_USERSAPI = 'users/user.do'
|
||||
API_IOTDEVMANAGERAPI = 'iot/devmanager.do'
|
||||
API_PRODUCTAPI = 'pim/product'
|
||||
API_REALM = 'ecouser.net'
|
||||
@@ -10,7 +10,10 @@ from paho.mqtt import publish as MQTTPublish
|
||||
from paho.mqtt import subscribe as MQTTSubscribe
|
||||
from sleekxmppfs.xmlstream import ET
|
||||
|
||||
from .const import LOGGER
|
||||
from .sucks_api_const import API_REALM, API_IOTDEVMANAGERAPI, API_PORTAL_URL_FORMAT
|
||||
|
||||
import logging
|
||||
LOGGER = logging.getLogger(__name__)
|
||||
|
||||
def str_to_bool_or_cert(s):
|
||||
if s == 'True' or s == True:
|
||||
@@ -131,7 +134,7 @@ class EcoVacsIOTMQ(ClientMQTT):
|
||||
payloadxml.attrib.pop("td")
|
||||
return {
|
||||
'auth': {
|
||||
'realm': EcoVacsAPI.REALM,
|
||||
'realm': API_REALM,
|
||||
'resource': self.resource,
|
||||
'token': self.secret,
|
||||
'userid': self.user,
|
||||
@@ -151,7 +154,7 @@ class EcoVacsIOTMQ(ClientMQTT):
|
||||
LOGGER.debug("calling iotdevmanager api with {}".format(args))
|
||||
params = {}
|
||||
params.update(args)
|
||||
url = (EcoVacsAPI.PORTAL_URL_FORMAT + "/iot/devmanager.do").format(continent=self.continent)
|
||||
url = (API_PORTAL_URL_FORMAT + "/" + API_IOTDEVMANAGERAPI).format(continent=self.continent)
|
||||
response = None
|
||||
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
|
||||
|
||||
@@ -4,7 +4,9 @@ from threading import Event
|
||||
from sleekxmppfs import ClientXMPP, Callback, MatchXPath
|
||||
from sleekxmppfs.xmlstream import ET
|
||||
#from sleekxmppfs.exceptions import XMPPError
|
||||
from .const import LOGGER
|
||||
|
||||
import logging
|
||||
LOGGER = logging.getLogger(__name__)
|
||||
|
||||
#This is used by EcoVacsIOTMQ and EcoVacsXMPP for _ctl_to_dict
|
||||
def RepresentsInt(stringvar):
|
||||
|
||||
Reference in New Issue
Block a user