Events now flow to Vacbot as reasonably sane dicts.

This commit is contained in:
William Pietri
2017-12-12 11:49:10 -08:00
parent fbd3c3d85a
commit d6e4f7d5c5
4 changed files with 154 additions and 22 deletions
+2 -1
View File
@@ -67,7 +67,8 @@ setup(
'click>=6', 'click>=6',
'requests>=2.18', 'requests>=2.18',
'pycryptodome>=3.4', 'pycryptodome>=3.4',
'pycountry-convert>=0.5' 'pycountry-convert>=0.5',
'stringcase>=1.2'
], ],
# List additional groups of dependencies here (e.g. development # List additional groups of dependencies here (e.g. development
+45 -15
View File
@@ -7,6 +7,7 @@ from threading import Event
import click import click
import requests import requests
import stringcase
from sleekxmpp import ClientXMPP, Callback, MatchXPath from sleekxmpp import ClientXMPP, Callback, MatchXPath
from sleekxmpp.xmlstream import ET from sleekxmpp.xmlstream import ET
@@ -135,34 +136,38 @@ class VacBot():
self.battery_status = None self.battery_status = None
self.xmpp = EcoVacsXMPP(user, domain, resource, secret, continent) self.xmpp = EcoVacsXMPP(user, domain, resource, secret, continent)
self.xmpp.register_callback("CleanReport", self._handle_clean_report)
self.xmpp.register_callback("ChargeState", self._handle_charge_report)
self.xmpp.register_callback("BatteryInfo", self._handle_battery_report)
self.xmpp.register_callback("error", self._handle_error) self.xmpp.register_callback("error", self._handle_error)
self.xmpp.subscribe_to_ctls(self._handle_ctl)
def connect_and_wait_until_ready(self): def connect_and_wait_until_ready(self):
self.xmpp.connect_and_wait_until_ready() self.xmpp.connect_and_wait_until_ready()
self.xmpp.schedule('Ping', 30, lambda: self.xmpp.send_ping(self._vacuum_adress()), repeat=True) self.xmpp.schedule('Ping', 30, lambda: self.xmpp.send_ping(self._vacuum_address()), repeat=True)
def _handle_clean_report(self, iq): def _handle_ctl(self, ctl):
self.clean_status = iq.find('{com:ctl}query/{com:ctl}ctl/{com:ctl}clean').get('type') method = '_handle_' + ctl['event']
if hasattr(self, method):
getattr(self, method)(ctl)
def _handle_clean_report(self, event):
self.clean_status = event['type']
logging.debug("*** clean_status = " + self.clean_status) logging.debug("*** clean_status = " + self.clean_status)
def _handle_battery_report(self, iq): def _handle_battery_info(self, iq):
try: try:
self.battery_status = float(iq.find('{com:ctl}query/{com:ctl}ctl/{com:ctl}battery').get('power')) / 100 self.battery_status = float(iq['power']) / 100
logging.debug("*** battery_status = {:.0%}".format(self.battery_status)) logging.debug("*** battery_status = {:.0%}".format(self.battery_status))
except ValueError: except ValueError:
logging.warning("couldn't parse battery status " + ET.tostring(iq)) logging.warning("couldn't parse battery status " + ET.tostring(iq))
def _handle_charge_report(self, iq): def _handle_charge_state(self, event):
report = iq.find('{com:ctl}query/{com:ctl}ctl/{com:ctl}charge').get('type') report = event['type']
if report.lower() == 'going': if report == 'going':
self.charge_status = 'returning' self.charge_status = 'returning'
elif report.lower() == 'slotcharging': elif report == 'slot_charging':
self.charge_status = 'charging' self.charge_status = 'charging'
elif report.lower() == 'idle': elif report == 'idle':
self.charge_status = 'idle' self.charge_status = 'idle'
else: else:
logging.warning("Unknown charging status '" + report + "'") logging.warning("Unknown charging status '" + report + "'")
@@ -173,11 +178,11 @@ class VacBot():
error_no = iq.find('{com:ctl}query/{com:ctl}ctl').get('errno') error_no = iq.find('{com:ctl}query/{com:ctl}ctl').get('errno')
logging.debug("*** error = " + error_no + " " + error) logging.debug("*** error = " + error_no + " " + error)
def _vacuum_adress(self): def _vacuum_address(self):
return self.vacuum['did'] + '@' + self.vacuum['class'] + '.ecorobot.net/atom' return self.vacuum['did'] + '@' + self.vacuum['class'] + '.ecorobot.net/atom'
def send_command(self, xml): def send_command(self, xml):
self.xmpp.send_command(xml, self._vacuum_adress()) self.xmpp.send_command(xml, self._vacuum_address())
def run(self, action): def run(self, action):
self.send_command(action.to_xml()) self.send_command(action.to_xml())
@@ -198,6 +203,7 @@ class EcoVacsXMPP(ClientXMPP):
self.credentials['authzid'] = user self.credentials['authzid'] = user
self.add_event_handler("session_start", self.session_start) self.add_event_handler("session_start", self.session_start)
self.ctl_subscribers = []
self.ready_flag = Event() self.ready_flag = Event()
def wait_until_ready(self): def wait_until_ready(self):
@@ -206,8 +212,32 @@ class EcoVacsXMPP(ClientXMPP):
def session_start(self, event): def session_start(self, event):
logging.debug("----------------- starting session ----------------") logging.debug("----------------- starting session ----------------")
logging.debug("event = {}".format(event)) logging.debug("event = {}".format(event))
self.register_handler(Callback("general",
MatchXPath('{jabber:client}iq/{com:ctl}query/{com:ctl}'),
self._handle_ctl))
self.ready_flag.set() self.ready_flag.set()
def subscribe_to_ctls(self, function):
self.ctl_subscribers.append(function)
def _handle_ctl(self, message):
the_good_part = message.get_payload()[0][0]
as_dict = self._ctl_to_dict(the_good_part)
for s in self.ctl_subscribers:
s(as_dict)
def _ctl_to_dict(self, xml):
result = xml.attrib.copy()
result['event'] = result.pop('td')
if xml:
result.update(xml[0].attrib)
for key in result:
result[key] = stringcase.snakecase(result[key])
return result
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 + '"]'),
+46 -1
View File
@@ -9,7 +9,52 @@ from sucks import *
# the library's design and its multithreaded nature and lack of explicit testing support. # the library's design and its multithreaded nature and lack of explicit testing support.
def test_wrap_command(): def test_wrap_command():
x = EcoVacsXMPP('20170101abcdefabcdefa', 'ecouser.net', 'abcdef12', 'A1b2C3d4efghijklmNOPQrstuvwxyz12', 'na') x = make_ecovacs_xmpp()
c = str(x._wrap_command(Clean(1).to_xml(), 'E0000000001234567890@126.ecorobot.net/atom')) c = str(x._wrap_command(Clean(1).to_xml(), 'E0000000001234567890@126.ecorobot.net/atom'))
assert_true(search(r'from="20170101abcdefabcdefa@ecouser.net/abcdef12"', c)) assert_true(search(r'from="20170101abcdefabcdefa@ecouser.net/abcdef12"', c))
assert_true(search(r'to="E0000000001234567890@126.ecorobot.net/atom"', c)) assert_true(search(r'to="E0000000001234567890@126.ecorobot.net/atom"', c))
def test_subscribe_to_ctls():
response = None
def save_response(value):
nonlocal response
response = value
x = make_ecovacs_xmpp()
query = x.make_iq_query()
query.set_payload(
ET.fromstring('<query xmlns="com:ctl"><ctl td="CleanReport"> <clean type="auto" /> </ctl></query>'))
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_xmpp()
assert_dict_equal(
x._ctl_to_dict(make_ctl('<ctl td="CleanReport"> <clean type="auto" /> </ctl>')),
{'event': 'clean_report', 'type': 'auto'})
assert_dict_equal(
x._ctl_to_dict(make_ctl('<ctl td="CleanReport"> <clean type="auto" speed="strong" /> </ctl>')),
{'event': 'clean_report', 'type': 'auto', 'speed': 'strong'})
assert_dict_equal(
x._ctl_to_dict(make_ctl('<ctl td="BatteryInfo"><battery power="095"/></ctl>')),
{'event': 'battery_info', 'power': '095'})
assert_dict_equal(
x._ctl_to_dict(make_ctl('# <ctl td="LifeSpan" type="Brush" val="099" total="365"/>')),
{'event': 'life_span', 'type': 'brush', 'val': '099', 'total': '365'})
def make_ecovacs_xmpp():
return EcoVacsXMPP('20170101abcdefabcdefa', 'ecouser.net', 'abcdef12', 'A1b2C3d4efghijklmNOPQrstuvwxyz12', 'na')
def make_ctl(string):
return ET.fromstring('<query xmlns="com:ctl">' + string + '</query>')[0]
+61 -5
View File
@@ -3,16 +3,72 @@ from nose.tools import *
from sucks import * from sucks import *
def test_handle_clean_report():
v = a_vacbot()
assert_equals(None, v.clean_status)
v._handle_ctl({'event': 'clean_report', 'type': 'auto', 'speed': 'strong'})
assert_equals('auto', v.clean_status)
def test_handle_charge_state():
v = a_vacbot()
assert_equals(None, v.clean_status)
v._handle_ctl({'event': 'charge_state', 'type': 'going'})
assert_equals('returning', v.charge_status)
v._handle_ctl({'event': 'charge_state', 'type': 'slot_charging'})
assert_equals('charging', v.charge_status)
v._handle_ctl({'event': 'charge_state', 'type': 'idle'})
assert_equals('idle', v.charge_status)
def test_handle_battery_info():
v = a_vacbot()
assert_equals(None, v.battery_status)
v._handle_ctl({'event': 'battery_info', 'power': '100'})
assert_equals(1.0, v.battery_status)
v._handle_ctl({'event': 'battery_info', 'power': '095'})
assert_equals(0.95, v.battery_status)
v._handle_ctl({'event': 'battery_info', 'power': '000'})
assert_equals(0.0, v.battery_status)
def test_handle_unknown_ctl():
v = a_vacbot()
v._handle_ctl({'event': 'weird_and_unknown_event', 'type': 'pretty_weird'})
# as long as it doesn't blow up, that's fine
# as-yet unhandled messages:
#
# <ctl td="LifeSpan" type="Brush" val="099" total="365"/>
# <ctl td="LifeSpan" type="DustCaseHeap" val="098" total="365"/>
# <ctl td="LifeSpan" type="SideBrush" val="098" total="365"/>
# <ctl td="Sched2"/>
# <ctl td="Sched2" id="30800321"/>
#
# plus errors!
def test_bot_address(): def test_bot_address():
v = vacbot_for_bot({"did": "E0000000001234567890", "class": "126", "nick": "bob"}) v = a_vacbot(bot={"did": "E0000000001234567890", "class": "126", "nick": "bob"})
assert_equals('E0000000001234567890@126.ecorobot.net/atom', v._vacuum_adress()) assert_equals('E0000000001234567890@126.ecorobot.net/atom', v._vacuum_address())
def test_model_variation(): def test_model_variation():
v = vacbot_for_bot({"did": "E0000000001234567890", "class": "141", "nick": "bob"}) v = a_vacbot(bot={"did": "E0000000001234567890", "class": "141", "nick": "bob"})
assert_equals('E0000000001234567890@141.ecorobot.net/atom', v._vacuum_adress()) assert_equals('E0000000001234567890@141.ecorobot.net/atom', v._vacuum_address())
def vacbot_for_bot(bot):
def a_vacbot(bot=None):
if bot is None:
bot = {"did": "E0000000001234567890", "class": "126", "nick": "bob"}
return VacBot('20170101abcdefabcdefa', 'ecouser.net', 'abcdef12', 'A1b2C3d4efghijklmNOPQrstuvwxyz12', return VacBot('20170101abcdefabcdefa', 'ecouser.net', 'abcdef12', 'A1b2C3d4efghijklmNOPQrstuvwxyz12',
bot, 'na') bot, 'na')