Fix xmpp to work with iot
This commit is contained in:
Brian Martin
2019-01-15 02:24:49 -05:00
parent ef467acaf8
commit 4ca9750738
3 changed files with 87 additions and 63 deletions
+73 -57
View File
@@ -127,9 +127,13 @@ class EcoVacsAPI:
'lang': 'en', 'lang': 'en',
'deviceId': device_id, 'deviceId': device_id,
'appCode': 'i_eco_e', 'appCode': 'i_eco_e',
#'appCode': 'i_eco_a' - iphone
'appVersion': '1.3.5', 'appVersion': '1.3.5',
#'appVersion': '1.4.6' - iphone
'channel': 'c_googleplay', 'channel': 'c_googleplay',
#'channel': 'c_iphone', - iphone
'deviceType': '1' 'deviceType': '1'
#'deviceType': '2' - iphone
} }
_LOGGER.debug("Setting up EcoVacsAPI") _LOGGER.debug("Setting up EcoVacsAPI")
self.resource = device_id[0:8] self.resource = device_id[0:8]
@@ -216,11 +220,18 @@ class EcoVacsAPI:
if api == self.IOTDEVMANAGERAPI: if api == self.IOTDEVMANAGERAPI:
if json['ret'] == 'ok': if json['ret'] == 'ok':
return json return json
elif json['ret'] == 'fail' and json['debug'] == 'wait for response timed out': #Maybe handle timeout for IOT better in the future elif json['ret'] == 'fail':
_LOGGER.error("call to {} failed with {}".format(function, json)) if 'debug' in json:
return {} if json['debug'] == 'wait for response timed out':
#raise RuntimeError( #TODO - Maybe handle timeout for IOT better in the future
# "failure {} ({}) for call {} and parameters {}".format(json['error'], json['errno'], function, params)) _LOGGER.error("call to {} failed with {}".format(function, json))
return {}
else:
#TODO - Not sure if we want to raise an error yet, just return empty for now
_LOGGER.error("call to {} failed with {}".format(function, json))
return {}
#raise RuntimeError(
#"failure {} ({}) for call {} and parameters {}".format(json['error'], json['errno'], function, params))
if api.startswith(self.PRODUCTAPI): if api.startswith(self.PRODUCTAPI):
if json['code'] == 0: if json['code'] == 0:
@@ -353,17 +364,16 @@ class VacBot():
if vacuum['iot']: if vacuum['iot']:
self.iot = EcoVacsIOT(user, domain, resource, secret, continent, vacuum) self.iot = EcoVacsIOT(user, domain, resource, secret, continent, vacuum)
#TODO: How to handle subscriptions for IOT self.iot.subscribe_to_ctls(self._handle_ctl)
#self.iot.subscribe_to_ctls(self._handle_ctl)
else: self.xmpp = EcoVacsXMPP(user, domain, resource, secret, continent, vacuum, server_address )
self.xmpp = EcoVacsXMPP(user, domain, resource, secret, continent, server_address) 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):
if not self.vacuum['iot']: #if not self.vacuum['iot']:
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', 30, lambda: self.send_ping(), repeat=True)
#else: #ToDo identify the best way to handle similar for IOT devices #else: #ToDo identify the best way to handle similar for IOT devices
#self.iot.connect_and_wait_until_ready() #self.iot.connect_and_wait_until_ready()
@@ -372,11 +382,11 @@ class VacBot():
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()
if not self.vacuum['iot']: #if not self.vacuum['iot']:
self.xmpp.schedule('Components', 3600, lambda: self.refresh_components(), repeat=True) self.xmpp.schedule('Components', 3600, lambda: self.refresh_components(), repeat=True)
else: #else:
#For IOT go ahead and refresh components #For IOT go ahead and refresh components
self.refresh_components() # self.refresh_components()
def _handle_ctl(self, ctl): def _handle_ctl(self, ctl):
method = '_handle_' + ctl['event'] method = '_handle_' + ctl['event']
@@ -418,6 +428,8 @@ class VacBot():
cleaning = event.get('st', None) cleaning = event.get('st', None)
if cleaning == 'p': if cleaning == 'p':
self.clean_status = 'paused' self.clean_status = 'paused'
elif cleaning == 'h':
self.clean_status = 'standby'
else: else:
self.clean_status = 'cleaning' self.clean_status = 'cleaning'
@@ -474,30 +486,30 @@ class VacBot():
return self.vacuum_status in CLEANING_STATES return self.vacuum_status in CLEANING_STATES
def send_ping(self): def send_ping(self):
if not self.vacuum['iot']: try:
try: if not self.vacuum['iot']:
self.xmpp.send_ping(self._vacuum_address()) self.xmpp.send_ping(self._vacuum_address())
except XMPPError as err:
_LOGGER.warning("Ping did not reach VacBot. Will retry.")
_LOGGER.debug("*** Error type: " + err.etype)
_LOGGER.debug("*** Error condition: " + err.condition)
self._failed_pings += 1
if self._failed_pings >= 4:
self.vacuum_status = 'offline'
self.statusEvents.notify(self.vacuum_status)
else: else:
self._failed_pings = 0 self.xmpp.send_ping(EcoVacsAPI.REALM) #IOT vacuums are using the realm instead
if self._monitor: except XMPPError as err:
# If we don't yet have a vacuum status, request initial statuses again now that the ping succeeded _LOGGER.warning("Ping did not reach VacBot. Will retry.")
if self.vacuum_status == 'offline' or self.vacuum_status is None: _LOGGER.debug("*** Error type: " + err.etype)
self.request_all_statuses() _LOGGER.debug("*** Error condition: " + err.condition)
else: self._failed_pings += 1
# If we're not auto-monitoring the status, then just reset the status to None, which indicates unknown if self._failed_pings >= 4:
if self.vacuum_status == 'offline': self.vacuum_status = 'offline'
self.vacuum_status = None self.statusEvents.notify(self.vacuum_status)
self.statusEvents.notify(self.vacuum_status) else:
#else: #TODO determine how to handle send_ping for IOT device self._failed_pings = 0
#print("IOT Ping") if self._monitor:
# If we don't yet have a vacuum status, request initial statuses again now that the ping succeeded
if self.vacuum_status == 'offline' or self.vacuum_status is None:
self.request_all_statuses()
else:
# If we're not auto-monitoring the status, then just reset the status to None, which indicates unknown
if self.vacuum_status == 'offline':
self.vacuum_status = None
self.statusEvents.notify(self.vacuum_status)
def refresh_components(self): def refresh_components(self):
try: try:
@@ -537,6 +549,14 @@ class VacBot():
if not self.vacuum['iot']: if not self.vacuum['iot']:
self.xmpp.disconnect(wait=wait) self.xmpp.disconnect(wait=wait)
#This is used by EcoVacsIOT and EcoVacsXMPP for _ctl_to_dict
def RepresentsInt(stringvar):
try:
int(stringvar)
return True
except ValueError:
return False
class EcoVacsIOT(): class EcoVacsIOT():
def __init__(self, user, domain, resource, secret, continent, vacuum): def __init__(self, user, domain, resource, secret, continent, vacuum):
self.uid = user self.uid = user
@@ -552,7 +572,7 @@ class EcoVacsIOT():
self.ctl_subscribers = [] self.ctl_subscribers = []
self.ready_flag = Event() self.ready_flag = Event()
#TODO: Determine what to do with IOT connect and wait #TODO: Determine what to do with IOT connect and wait, or scrap
# def connect_and_wait_until_ready(self): # def connect_and_wait_until_ready(self):
# self.connect(EcoVacsAPI._EcoVacsAPI__call_portal_api()) # self.connect(EcoVacsAPI._EcoVacsAPI__call_portal_api())
# self.process() # self.process()
@@ -583,8 +603,8 @@ class EcoVacsIOT():
} }
# 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, action, message): def _handle_ctl(self, action, message):
@@ -613,19 +633,22 @@ class EcoVacsIOT():
result['event'] = action.name.replace("Get","",1) result['event'] = action.name.replace("Get","",1)
for key in result: for key in result:
result[key] = stringcase.snakecase(result[key]) if not RepresentsInt(result[key]): #Fix to handle negative int values
result[key] = stringcase.snakecase(result[key])
return result return result
class EcoVacsXMPP(ClientXMPP): class EcoVacsXMPP(ClientXMPP):
def __init__(self, user, domain, resource, secret, continent, server_address=None): def __init__(self, user, domain, resource, secret, continent, vacuum, server_address=None ):
ClientXMPP.__init__(self, user + '@' + domain, '0/' + resource + '/' + secret) ClientXMPP.__init__(self, user + '@' + domain, '0/' + resource + '/' + secret)
self.user = user self.user = user
self.domain = domain self.domain = domain
self.resource = resource self.resource = resource
self.apiresource = resource
self.continent = continent self.continent = continent
self.vacuum = vacuum
self.credentials['authzid'] = user self.credentials['authzid'] = user
if server_address is None: if server_address is None:
self.server_address = ('msg-{}.ecouser.net'.format(self.continent), '5223') self.server_address = ('msg-{}.ecouser.net'.format(self.continent), '5223')
@@ -667,18 +690,11 @@ class EcoVacsXMPP(ClientXMPP):
result.update(xml[0].attrib) result.update(xml[0].attrib)
for key in result: for key in result:
if not self.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 RepresentsInt(self, stringvar):
try:
int(stringvar)
return True
except ValueError:
return False
def register_callback(self, kind, function): def register_callback(self, kind, function):
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 + '"]'),
@@ -698,7 +714,11 @@ class EcoVacsXMPP(ClientXMPP):
return q return q
def _my_address(self): def _my_address(self):
return self.user + '@' + self.domain + '/' + self.boundjid.resource if not self.vacuum['iot']:
return self.user + '@' + self.domain + '/' + self.boundjid.resource
else:
return self.user + '@' + self.domain + '/' + self.apiresource
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())
@@ -711,7 +731,6 @@ class EcoVacsXMPP(ClientXMPP):
self.process() self.process()
self.wait_until_ready() self.wait_until_ready()
class VacBotCommand: class VacBotCommand:
ACTION = { ACTION = {
'forward': 'forward', 'forward': 'forward',
@@ -757,7 +776,6 @@ class VacBotCommand:
class Clean(VacBotCommand): class Clean(VacBotCommand):
#def __init__(self, mode='auto', speed='normal', terminal=False): - Keeping original in case
def __init__(self, mode='auto', speed='normal', terminal=False, **kwargs): def __init__(self, mode='auto', speed='normal', terminal=False, **kwargs):
if kwargs is None: if kwargs is None:
super().__init__('Clean', {'clean': {'type': CLEAN_MODE_TO_ECOVACS[mode], 'speed': FAN_SPEED_TO_ECOVACS[speed]}}) super().__init__('Clean', {'clean': {'type': CLEAN_MODE_TO_ECOVACS[mode], 'speed': FAN_SPEED_TO_ECOVACS[speed]}})
@@ -783,8 +801,6 @@ class Stop(Clean):
class SpotArea(Clean): class SpotArea(Clean):
def __init__(self, action='start', namedarea='', customarea='', cleanings='1'): def __init__(self, action='start', namedarea='', customarea='', cleanings='1'):
if namedarea != '': #For cleaning specified map area if namedarea != '': #For cleaning specified map area
super().__init__('spotarea', 'normal', act=CLEAN_ACTION_TO_ECOVACS[action], mid=namedarea) super().__init__('spotarea', 'normal', act=CLEAN_ACTION_TO_ECOVACS[action], mid=namedarea)
elif customarea != '': #For cleaning custom map area, and specify deep amount 1x/2x elif customarea != '': #For cleaning custom map area, and specify deep amount 1x/2x
+9 -3
View File
@@ -21,6 +21,13 @@ def test_wrap_command():
def test_is_iot(): def test_is_iot():
x = make_ecovacs_iot() x = make_ecovacs_iot()
# TODO - Error response from command
#'cmdName': 'Charge', 'payload': '<ctl><charge type="go" /></ctl>', 'payloadType': 'x', 'td': 'q', 'toId': '0e084f6c-0846-4342-a947-fe14c293301f', 'toRes': 'wC3g', 'toType': 'ls1ok3'}
# - Already charging on dock
#{'ret': 'ok', 'resp': "<ctl ret='fail' errno='8'/>", 'id': 'NLQy'}
#Timeout
# {'ret': 'fail', 'errno': 500, 'debug': 'wait for response timed out'}
# def test_subscribe_to_ctls(): # def test_subscribe_to_ctls():
# response = None # response = None
@@ -63,11 +70,10 @@ def make_ecovacs_iot():
eapi = make_api() eapi = make_api()
with requests_mock.mock() as m: with requests_mock.mock() as m:
device_id = 'E0000001234567890123'
device_resource = 'test_resource' device_resource = 'test_resource'
device_class = 'ls1ok3' device_class = 'ls1ok3' #this is for a D900 series
r = m.post(compile('user.do'), r = m.post(compile('user.do'),
text='{"todo": "result", "devices": [{"did": "%s", "class": "%s", "nick": "bob"}], "result": "ok"}' % (device_id, device_class)) text='{"todo": "result", "devices": [{"did": "E0000000001234567890", "class": "%s", "nick": "bob"}], "result": "ok"}' % (device_class))
r = m.post(compile('pim/product/getProductIotMap'), r = m.post(compile('pim/product/getProductIotMap'),
text='{"code":0,"data":[{"classid":"dl8fht","product":{"_id":"5acb0fa87c295c0001876ecf","name":"DEEBOT 600 Series","icon":"5acc32067c295c0001876eea","UILogicId":"dl8fht","ota":false,"iconUrl":"https://portal-ww.ecouser.net/api/pim/file/get/5acc32067c295c0001876eea"}},{"classid":"02uwxm","product":{"_id":"5ae1481e7ccd1a0001e1f69e","name":"DEEBOT OZMO Slim10 Series","icon":"5b1dddc48bc45700014035a1","UILogicId":"02uwxm","ota":false,"iconUrl":"https://portal-ww.ecouser.net/api/pim/file/get/5b1dddc48bc45700014035a1"}},{"classid":"y79a7u","product":{"_id":"5b04c0227ccd1a0001e1f6a8","name":"DEEBOT OZMO 900","icon":"5b04c0217ccd1a0001e1f6a7","UILogicId":"y79a7u","ota":true,"iconUrl":"https://portal-ww.ecouser.net/api/pim/file/get/5b04c0217ccd1a0001e1f6a7"}},{"classid":"jr3pqa","product":{"_id":"5b43077b8bc457000140363e","name":"DEEBOT 711","icon":"5b5ac4cc8d5a56000111e769","UILogicId":"jr3pqa","ota":true,"iconUrl":"https://portal-ww.ecouser.net/api/pim/file/get/5b5ac4cc8d5a56000111e769"}},{"classid":"uv242z","product":{"_id":"5b5149b4ac0b87000148c128","name":"DEEBOT 710","icon":"5b5ac4e45f21100001882bb9","UILogicId":"uv242z","ota":true,"iconUrl":"https://portal-ww.ecouser.net/api/pim/file/get/5b5ac4e45f21100001882bb9"}},{"classid":"ls1ok3","product":{"_id":"5b6561060506b100015c8868","name":"DEEBOT 900 Series","icon":"5ba4a2cb6c2f120001c32839","UILogicId":"ls1ok3","ota":true,"iconUrl":"https://portal-ww.ecouser.net/api/pim/file/get/5ba4a2cb6c2f120001c32839"}}]}') text='{"code":0,"data":[{"classid":"dl8fht","product":{"_id":"5acb0fa87c295c0001876ecf","name":"DEEBOT 600 Series","icon":"5acc32067c295c0001876eea","UILogicId":"dl8fht","ota":false,"iconUrl":"https://portal-ww.ecouser.net/api/pim/file/get/5acc32067c295c0001876eea"}},{"classid":"02uwxm","product":{"_id":"5ae1481e7ccd1a0001e1f69e","name":"DEEBOT OZMO Slim10 Series","icon":"5b1dddc48bc45700014035a1","UILogicId":"02uwxm","ota":false,"iconUrl":"https://portal-ww.ecouser.net/api/pim/file/get/5b1dddc48bc45700014035a1"}},{"classid":"y79a7u","product":{"_id":"5b04c0227ccd1a0001e1f6a8","name":"DEEBOT OZMO 900","icon":"5b04c0217ccd1a0001e1f6a7","UILogicId":"y79a7u","ota":true,"iconUrl":"https://portal-ww.ecouser.net/api/pim/file/get/5b04c0217ccd1a0001e1f6a7"}},{"classid":"jr3pqa","product":{"_id":"5b43077b8bc457000140363e","name":"DEEBOT 711","icon":"5b5ac4cc8d5a56000111e769","UILogicId":"jr3pqa","ota":true,"iconUrl":"https://portal-ww.ecouser.net/api/pim/file/get/5b5ac4cc8d5a56000111e769"}},{"classid":"uv242z","product":{"_id":"5b5149b4ac0b87000148c128","name":"DEEBOT 710","icon":"5b5ac4e45f21100001882bb9","UILogicId":"uv242z","ota":true,"iconUrl":"https://portal-ww.ecouser.net/api/pim/file/get/5b5ac4e45f21100001882bb9"}},{"classid":"ls1ok3","product":{"_id":"5b6561060506b100015c8868","name":"DEEBOT 900 Series","icon":"5ba4a2cb6c2f120001c32839","UILogicId":"ls1ok3","ota":true,"iconUrl":"https://portal-ww.ecouser.net/api/pim/file/get/5ba4a2cb6c2f120001c32839"}}]}')
d = eapi.devices() d = eapi.devices()
+4 -2
View File
@@ -56,8 +56,10 @@ def test_xml_to_dict():
{'event': 'life_span', 'type': 'dust_case_heap', 'val': '-050', 'total': '365'}) {'event': 'life_span', 'type': 'dust_case_heap', 'val': '-050', 'total': '365'})
def make_ecovacs_xmpp(): def make_ecovacs_xmpp(bot=None):
return EcoVacsXMPP('20170101abcdefabcdefa', 'ecouser.net', 'abcdef12', 'A1b2C3d4efghijklmNOPQrstuvwxyz12', 'na') if bot is None:
bot = bot = {"did": "E0000000001234567890", "class": "126", "nick": "bob", "iot": False}
return EcoVacsXMPP('20170101abcdefabcdefa', 'ecouser.net', 'abcdef12', 'A1b2C3d4efghijklmNOPQrstuvwxyz12', 'na', bot)
def make_ctl(string): def make_ctl(string):