diff --git a/sucks/__init__.py b/sucks/__init__.py index f715054..e535d29 100644 --- a/sucks/__init__.py +++ b/sucks/__init__.py @@ -240,8 +240,10 @@ class EcoVacsAPI: 'token': self.auth_code} ) - def devices(self): - devices = self.__call_portal_api(self.USERSAPI,'GetDeviceList', { + + + def getdevices(self): + return self.__call_portal_api(self.USERSAPI,'GetDeviceList', { 'userid': self.uid, 'auth': { 'with': 'users', @@ -252,7 +254,8 @@ class EcoVacsAPI: } })['devices'] - iotProducts = self.__call_portal_api(self.PRODUCTAPI + '/getProductIotMap','', { + def getiotProducts(self): + return self.__call_portal_api(self.PRODUCTAPI + '/getProductIotMap','', { 'channel': '', 'auth': { 'with': 'users', @@ -263,16 +266,18 @@ class EcoVacsAPI: } })['data'] + def SetIOTDevices(self, devices, iotproducts): for device in devices: #Check if the device is part of iotProducts and add an iot flag. - for iotProducts in iotProducts: - if device['class'] == iotProducts['classid']: + for iotProduct in iotproducts: + if device['class'] == iotProduct['classid']: device['iot'] = True else: device['iot'] = False - return devices + def devices(self): + return self.SetIOTDevices(self.getdevices(), self.getiotProducts()) @staticmethod def md5(text): @@ -386,11 +391,12 @@ class VacBot(): _LOGGER.warning("Unknown component type: '" + type + "'") if 'val' in event: + lifespan = int(event['val']) / 100 else: - lifespan = int(event['left'].replace("_","-")) / 60 #This works for a D901, I also ran into a negative number and had to replace _ with - + lifespan = int(event['left']) / 60 #This works for a D901 self.components[type] = lifespan - #lifespan = str(int(int(event['left']) / 60) + " (" + int(int(event['left'])/int(event['total'])*100) + "%)") #This works for a D901 + lifespan_event = {'type': type, 'lifespan': lifespan} self.lifespanEvents.notify(lifespan_event) _LOGGER.debug("*** life_span " + type + " = " + str(lifespan)) @@ -557,7 +563,7 @@ class EcoVacsIOT(): def _wrap_command(self, cmd, recipient): - q = { + return { 'auth': { 'realm': EcoVacsAPI.REALM, 'resource': self.resource, @@ -574,7 +580,7 @@ class EcoVacsIOT(): "toType": self.vacuum['class'] } - return q + def subscribe_to_ctls(self, function): @@ -671,9 +677,18 @@ class EcoVacsXMPP(ClientXMPP): result.update(xml[0].attrib) for key in result: - result[key] = stringcase.snakecase(result[key]) + if not self.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 + '"]'), @@ -717,7 +732,7 @@ class VacBotCommand: 'stop': 'stop' } - def __init__(self, name, args=None): + def __init__(self, name, args=None, **kwargs): if args is None: args = {} self.name = name @@ -753,8 +768,14 @@ 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', action='start', mid='',p='',deep='', terminal=False): - super().__init__('Clean', {'clean': {'type': CLEAN_MODE_TO_ECOVACS[mode], 'speed': FAN_SPEED_TO_ECOVACS[speed], 'act': CLEAN_ACTION_TO_ECOVACS[action], 'mid':mid, 'p':p, 'deep':deep}}) + 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]}}) + else: + initcmd = {'type': CLEAN_MODE_TO_ECOVACS[mode], 'speed': FAN_SPEED_TO_ECOVACS[speed]} + for kkey, kvalue in kwargs.items(): + initcmd[kkey] = kvalue + super().__init__('Clean', {'clean': initcmd}) class Edge(Clean): def __init__(self): @@ -771,8 +792,26 @@ class Stop(Clean): super().__init__('stop', 'normal') class SpotArea(Clean): - def __init__(self): - super().__init__('spotarea', 'normal', 'start') + def __init__(self, **kwargs): + self.action='start' + self.mid='' + self.p='' + self.deep='' + if kwargs is not None: + for kkey, kvalue in kwargs.items(): + if kkey == 'action': + self.action = kvalue + elif kkey == 'mid': + self.mid = kvalue + elif kkey == 'p': + self.p = kvalue + elif kkey == 'deep': + self.deep = kvalue + + if self.mid != '': #For cleaning specified map area + super().__init__('spotarea', 'normal', act=CLEAN_ACTION_TO_ECOVACS[self.action], mid=self.mid) + elif self.p != '': #For cleaning custom map area, and specify deep amount 1x/2x + super().__init__('spotarea' ,'normal',act=CLEAN_ACTION_TO_ECOVACS[self.action], p=self.p, deep=self.deep) class Charge(VacBotCommand): def __init__(self): diff --git a/sucks/cli.py b/sucks/cli.py index 7e49704..88b6e24 100644 --- a/sucks/cli.py +++ b/sucks/cli.py @@ -215,10 +215,9 @@ def run(actions, debug): vacbot = VacBot(api.uid, api.REALM, api.resource, api.user_access_token, vacuum, config['continent']) #vacbot.connect_and_wait_until_ready()( - vacbot.run(Move('backward')) - vacbot.run(Move('backward')) - vacbot.run(Move('stop')) - #vacbot.run(Clean('spotarea', 'normal', 'start', '0')) + + vacbot.run(SpotArea(action='start', mid='0')) + #vacbot.request_all_statuses() diff --git a/tests/test_ecovacs_api.py b/tests/test_ecovacs_api.py index e71b197..49d9d76 100644 --- a/tests/test_ecovacs_api.py +++ b/tests/test_ecovacs_api.py @@ -72,7 +72,10 @@ def test_device_lookup(): device_id = 'E0000001234567890123' r = m.post(compile('user.do'), - text='{"todo": "result", "devices": [{"did": "%s", "class": "126", "nick": "bob"}], "result": "ok"}' % device_id) + text='{"todo": "result", "devices": [{"did": "%s", "class": "126", "nick": "bob"}], "result": "ok"}' % device_id) + 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 = api.devices() assert_equals(r.call_count, 1) assert_equals(len(d), 1) @@ -80,6 +83,25 @@ def test_device_lookup(): assert_equals(vacuum['did'], device_id) assert_equals(vacuum['class'], '126') +def test_device_lookup_is_IOT(): + api = make_api() + with requests_mock.mock() as m: + device_id = 'E0000001234567890123' + device_class = 'ls1ok3' + + r = m.post(compile('user.do'), + text='{"todo": "result", "devices": [{"did": "%s", "class": "%s", "nick": "bob"}], "result": "ok"}' %(device_id, 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 = api.devices() + assert_equals(r.call_count, 1) + assert_equals(len(d), 1) + vacuum = d[0] + assert_equals(vacuum['did'], device_id) + assert_equals(vacuum['class'], device_class) + assert_equals(vacuum['iot'], True) + def make_api(): with requests_mock.mock() as m: @@ -88,5 +110,7 @@ def make_api(): m.get(compile('user/getAuthCode'), text='{"time": 1511200804607, "data": {"authCode": "abcdef01234567890abcdef012345678"}, "code": "0000", "msg": "X"}') m.post(compile('user.do'), - text='{"todo": "result", "token": "base64base64base64base64base64ba", "result": "ok", "userId": "20170101abcdefabcdefa", "resource": "abcdef12"}') + text='{"todo": "result", "token": "base64base64base64base64base64ba", "result": "ok", "userId": "20170101abcdefabcdefa", "resource": "abcdef12"}') + 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"}}]}') return EcoVacsAPI("long_device_id", "account_id", "password_hash", 'us', 'na') diff --git a/tests/test_ecovacs_iot.py b/tests/test_ecovacs_iot.py new file mode 100644 index 0000000..3cc949c --- /dev/null +++ b/tests/test_ecovacs_iot.py @@ -0,0 +1,80 @@ +from re import compile + +import requests_mock +from nose.tools import * + +from sucks import * +from test_ecovacs_api import make_api + + +# There are few tests for the IOT stuff here because it's relatively complicated to test given +# the library's design and its multithreaded nature and lack of explicit testing support. + +def test_wrap_command(): + x = make_ecovacs_iot() + + c = x._wrap_command(Charge(), 'E0000000001234567890') + assert_equal(c['cmdName'], Charge().name) + assert_equal(c['toId'], 'E0000000001234567890') + assert_equal(c['payload'], '') + +def test_is_iot(): + x = make_ecovacs_iot() + + +# def test_subscribe_to_ctls(): +# response = None + +# def save_response(value): +# nonlocal response +# response = value + +# x = make_ecovacs_iot() + +# query = x.make_iq_query() +# query.set_payload( +# ET.fromstring(' ')) + +# x.subscribe_to_ctls(save_response) +# x._handle_ctl(query) +# assert_dict_equal(response, {'event': 'clean_report', 'type': 'auto'}) + + +# def test_xml_to_dict(): +# x = make_ecovacs_iot() + +# assert_dict_equal( +# x._ctl_to_dict(make_ctl(' ')), +# {'event': 'clean_report', 'type': 'auto'}) +# assert_dict_equal( +# x._ctl_to_dict(make_ctl(' ')), +# {'event': 'clean_report', 'type': 'auto', 'speed': 'strong'}) + +# assert_dict_equal( +# x._ctl_to_dict(make_ctl('')), +# {'event': 'battery_info', 'power': '095'}) + +# assert_dict_equal( +# x._ctl_to_dict(make_ctl('# ')), +# {'event': 'life_span', 'type': 'brush', 'val': '099', 'total': '365'}) + + +def make_ecovacs_iot(): + eapi = make_api() + + with requests_mock.mock() as m: + device_id = 'E0000001234567890123' + device_resource = 'test_resource' + device_class = 'ls1ok3' + r = m.post(compile('user.do'), + text='{"todo": "result", "devices": [{"did": "%s", "class": "%s", "nick": "bob"}], "result": "ok"}' % (device_id, 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() + + eiotvacuum = d[0] + eiotvacuum['resource'] = device_resource + return EcoVacsIOT('20170101abcdefabcdefa', 'ecouser.net', 'abcdef12', 'base64base64base64base64base64ba', 'na', eiotvacuum) + +def make_ctl(string): + return ET.fromstring('' + string + '')[0] diff --git a/tests/test_ecovacs_xmpp.py b/tests/test_ecovacs_xmpp.py index ba79d4c..5e15818 100644 --- a/tests/test_ecovacs_xmpp.py +++ b/tests/test_ecovacs_xmpp.py @@ -51,6 +51,10 @@ def test_xml_to_dict(): x._ctl_to_dict(make_ctl('# ')), {'event': 'life_span', 'type': 'brush', 'val': '099', 'total': '365'}) + assert_dict_equal( + x._ctl_to_dict(make_ctl('')), + {'event': 'life_span', 'type': 'dust_case_heap', 'val': '-050', 'total': '365'}) + def make_ecovacs_xmpp(): return EcoVacsXMPP('20170101abcdefabcdefa', 'ecouser.net', 'abcdef12', 'A1b2C3d4efghijklmNOPQrstuvwxyz12', 'na') diff --git a/tests/test_vacbot.py b/tests/test_vacbot.py index 925168c..b89e3d4 100644 --- a/tests/test_vacbot.py +++ b/tests/test_vacbot.py @@ -270,18 +270,18 @@ def test_handle_unknown_ctl(): # plus errors! def test_bot_address(): - v = a_vacbot(bot={"did": "E0000000001234567890", "class": "126", "nick": "bob"}) + v = a_vacbot(bot={"did": "E0000000001234567890", "class": "126", "nick": "bob", "iot":False}) assert_equals('E0000000001234567890@126.ecorobot.net/atom', v._vacuum_address()) def test_model_variation(): - v = a_vacbot(bot={"did": "E0000000001234567890", "class": "141", "nick": "bob"}) + v = a_vacbot(bot={"did": "E0000000001234567890", "class": "141", "nick": "bob","iot":False}) assert_equals('E0000000001234567890@141.ecorobot.net/atom', v._vacuum_address()) def a_vacbot(bot=None, monitor=False): if bot is None: - bot = {"did": "E0000000001234567890", "class": "126", "nick": "bob"} + bot = {"did": "E0000000001234567890", "class": "126", "nick": "bob", "iot": False} return VacBot('20170101abcdefabcdefa', 'ecouser.net', 'abcdef12', 'A1b2C3d4efghijklmNOPQrstuvwxyz12', bot, 'na', monitor=monitor)