From 4ca97507384e71b37a0f127a8969c7e790e2f9eb Mon Sep 17 00:00:00 2001 From: Brian Martin Date: Tue, 15 Jan 2019 02:24:49 -0500 Subject: [PATCH] Fix xmpp Fix xmpp to work with iot --- sucks/__init__.py | 132 +++++++++++++++++++++---------------- tests/test_ecovacs_iot.py | 12 +++- tests/test_ecovacs_xmpp.py | 6 +- 3 files changed, 87 insertions(+), 63 deletions(-) diff --git a/sucks/__init__.py b/sucks/__init__.py index a75c5eb..9c7e658 100644 --- a/sucks/__init__.py +++ b/sucks/__init__.py @@ -127,9 +127,13 @@ class EcoVacsAPI: '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 } _LOGGER.debug("Setting up EcoVacsAPI") self.resource = device_id[0:8] @@ -216,11 +220,18 @@ class EcoVacsAPI: if api == self.IOTDEVMANAGERAPI: if json['ret'] == 'ok': return json - elif json['ret'] == 'fail' and json['debug'] == 'wait for response timed out': #Maybe handle timeout for IOT better in the future - _LOGGER.error("call to {} failed with {}".format(function, json)) - return {} - #raise RuntimeError( - # "failure {} ({}) for call {} and parameters {}".format(json['error'], json['errno'], function, params)) + elif json['ret'] == 'fail': + if 'debug' in json: + if json['debug'] == 'wait for response timed out': + #TODO - Maybe handle timeout for IOT better in the future + _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 json['code'] == 0: @@ -352,18 +363,17 @@ class VacBot(): self.errorEvents = EventEmitter() if vacuum['iot']: - self.iot = EcoVacsIOT(user, domain, resource, secret, continent, vacuum) - #TODO: How to handle subscriptions for IOT - #self.iot.subscribe_to_ctls(self._handle_ctl) - else: - self.xmpp = EcoVacsXMPP(user, domain, resource, secret, continent, server_address) - self.xmpp.subscribe_to_ctls(self._handle_ctl) + self.iot = EcoVacsIOT(user, domain, resource, secret, continent, vacuum) + self.iot.subscribe_to_ctls(self._handle_ctl) + + self.xmpp = EcoVacsXMPP(user, domain, resource, secret, continent, vacuum, server_address ) + self.xmpp.subscribe_to_ctls(self._handle_ctl) def connect_and_wait_until_ready(self): - if not self.vacuum['iot']: - self.xmpp.connect_and_wait_until_ready() - self.xmpp.schedule('Ping', 30, lambda: self.send_ping(), repeat=True) + #if not self.vacuum['iot']: + self.xmpp.connect_and_wait_until_ready() + self.xmpp.schedule('Ping', 30, lambda: self.send_ping(), repeat=True) #else: #ToDo identify the best way to handle similar for IOT devices #self.iot.connect_and_wait_until_ready() @@ -372,11 +382,11 @@ class VacBot(): if self._monitor: # Do a first ping, which will also fetch initial statuses if the ping succeeds self.send_ping() - if not self.vacuum['iot']: - self.xmpp.schedule('Components', 3600, lambda: self.refresh_components(), repeat=True) - else: + #if not self.vacuum['iot']: + self.xmpp.schedule('Components', 3600, lambda: self.refresh_components(), repeat=True) + #else: #For IOT go ahead and refresh components - self.refresh_components() + # self.refresh_components() def _handle_ctl(self, ctl): method = '_handle_' + ctl['event'] @@ -418,6 +428,8 @@ class VacBot(): cleaning = event.get('st', None) if cleaning == 'p': self.clean_status = 'paused' + elif cleaning == 'h': + self.clean_status = 'standby' else: self.clean_status = 'cleaning' @@ -474,30 +486,30 @@ class VacBot(): return self.vacuum_status in CLEANING_STATES def send_ping(self): - if not self.vacuum['iot']: - try: + try: + if not self.vacuum['iot']: 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: - self._failed_pings = 0 - 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) - #else: #TODO determine how to handle send_ping for IOT device - #print("IOT Ping") + self.xmpp.send_ping(EcoVacsAPI.REALM) #IOT vacuums are using the realm instead + 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: + self._failed_pings = 0 + 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): try: @@ -537,6 +549,14 @@ class VacBot(): if not self.vacuum['iot']: 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(): def __init__(self, user, domain, resource, secret, continent, vacuum): self.uid = user @@ -552,7 +572,7 @@ class EcoVacsIOT(): self.ctl_subscribers = [] 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): # self.connect(EcoVacsAPI._EcoVacsAPI__call_portal_api()) # self.process() @@ -583,8 +603,8 @@ class EcoVacsIOT(): } - # def subscribe_to_ctls(self, function): - # self.ctl_subscribers.append(function) + def subscribe_to_ctls(self, function): + self.ctl_subscribers.append(function) def _handle_ctl(self, action, message): @@ -613,19 +633,22 @@ class EcoVacsIOT(): result['event'] = action.name.replace("Get","",1) 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 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) self.user = user self.domain = domain self.resource = resource + self.apiresource = resource self.continent = continent + self.vacuum = vacuum self.credentials['authzid'] = user if server_address is None: self.server_address = ('msg-{}.ecouser.net'.format(self.continent), '5223') @@ -667,18 +690,11 @@ class EcoVacsXMPP(ClientXMPP): result.update(xml[0].attrib) 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]) return result - def RepresentsInt(self, stringvar): - try: - int(stringvar) - return True - except ValueError: - return False - def register_callback(self, kind, function): self.register_handler(Callback(kind, MatchXPath('{jabber:client}iq/{com:ctl}query/{com:ctl}ctl[@td="' + kind + '"]'), @@ -698,7 +714,11 @@ class EcoVacsXMPP(ClientXMPP): return q 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): q = self.make_iq_get(ito=to, ifrom=self._my_address()) @@ -711,7 +731,6 @@ class EcoVacsXMPP(ClientXMPP): self.process() self.wait_until_ready() - class VacBotCommand: ACTION = { 'forward': 'forward', @@ -757,7 +776,6 @@ class 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): if kwargs is None: 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): def __init__(self, action='start', namedarea='', customarea='', cleanings='1'): - - if namedarea != '': #For cleaning specified map area 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 diff --git a/tests/test_ecovacs_iot.py b/tests/test_ecovacs_iot.py index 3cc949c..9f4ec1b 100644 --- a/tests/test_ecovacs_iot.py +++ b/tests/test_ecovacs_iot.py @@ -21,6 +21,13 @@ def test_wrap_command(): def test_is_iot(): x = make_ecovacs_iot() +# TODO - Error response from command +#'cmdName': 'Charge', 'payload': '', 'payloadType': 'x', 'td': 'q', 'toId': '0e084f6c-0846-4342-a947-fe14c293301f', 'toRes': 'wC3g', 'toType': 'ls1ok3'} +# - Already charging on dock +#{'ret': 'ok', 'resp': "", 'id': 'NLQy'} + +#Timeout +# {'ret': 'fail', 'errno': 500, 'debug': 'wait for response timed out'} # def test_subscribe_to_ctls(): # response = None @@ -63,11 +70,10 @@ def make_ecovacs_iot(): eapi = make_api() with requests_mock.mock() as m: - device_id = 'E0000001234567890123' device_resource = 'test_resource' - device_class = 'ls1ok3' + device_class = 'ls1ok3' #this is for a D900 series 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'), 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() diff --git a/tests/test_ecovacs_xmpp.py b/tests/test_ecovacs_xmpp.py index 5e15818..cdfc3c7 100644 --- a/tests/test_ecovacs_xmpp.py +++ b/tests/test_ecovacs_xmpp.py @@ -56,8 +56,10 @@ def test_xml_to_dict(): {'event': 'life_span', 'type': 'dust_case_heap', 'val': '-050', 'total': '365'}) -def make_ecovacs_xmpp(): - return EcoVacsXMPP('20170101abcdefabcdefa', 'ecouser.net', 'abcdef12', 'A1b2C3d4efghijklmNOPQrstuvwxyz12', 'na') +def make_ecovacs_xmpp(bot=None): + 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):