commit 20579d92ab439428d88f4af8ae1161bc0133386c Author: William Pietri Date: Tue Oct 31 10:55:18 2017 -0700 initial import diff --git a/Pipfile b/Pipfile new file mode 100644 index 0000000..73e3495 --- /dev/null +++ b/Pipfile @@ -0,0 +1,15 @@ +[[source]] + +url = "https://pypi.python.org/simple" +verify_ssl = true +name = "pypi" + + +[dev-packages] + + + +[packages] + +sleekxmpp = ">=1.3" +click = ">=6" \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..e034336 --- /dev/null +++ b/README.md @@ -0,0 +1,37 @@ +sucks +===== + +A simple command-line python script to drive a robot vacuum. Currently +works only with the Ecovacs Deebot N79, as that's what I have. + +Right now this code offered more as inspiration than something for other +people to just download and use. But if you'd like to help flesh it out, +send email to my first name at williampietri.com. + +If you do try to use it, you'll need to create ~/.config/sucks.conf. It +should look something like this: + +``` +email=user@example.org +user=2017010101abdef012345 +domain=ecouser.net +resource=abcdef01 +secret=[long base64 string] +vacuum=[robot id]@126.ecorobot.net +``` + +I got these values by using xmppeek to do a man-in-the-middle attack on +the android app. I suspect that the Android app re-keys the connection +on a regular basis, as the secret was changing regularly up until I +cleared the Android app's data from my phone. It should be possible to +give this tool a command something like "login" that would create the config +file automatically. + +## To Do + +* implement common commands +* stop sending back error messages in response to robot updates +* track robot state (e.g., cleaning, stopped) +* use tracked state to be smarter +* log activity to aid in debugging +* add probabilistic cleaning options diff --git a/sucks.py b/sucks.py new file mode 100644 index 0000000..52bef7c --- /dev/null +++ b/sucks.py @@ -0,0 +1,112 @@ +import configparser +import itertools +import logging +import os +import time +from threading import Event + +import click +from sleekxmpp import ClientXMPP +from sleekxmpp.xmlstream import ET + + +class VacBot(ClientXMPP): + def __init__(self, user, domain, resource, secret, vacuum): + ClientXMPP.__init__(self, user + '@' + domain, '0/' + resource + '/' + secret) + + self.user = user + self.domain = domain + self.resource = resource + self.vacuum = vacuum + self.credentials['authzid'] = user + self.add_event_handler("session_start", self.session_start) + + self.ready_flag = Event() + + def wait_until_ready(self): + self.ready_flag.wait() + + def session_start(self, event): + print("----------------- starting session ----------------") + self.ready_flag.set() + + def clean(self, speed='standard', type='auto'): + self.make_command('clean', {'speed': speed, 'type': type}).send() + + def charge(self): + self.make_command('charge', {'type': 'go'}).send() + + def make_command(self, name, args): + clean = ET.Element(name, args) + ctl = ET.Element('ctl', {'td': name.capitalize()}) + ctl.append(clean) + command = self.wrap_command(ctl) + return command + + def wrap_command(self, ctl): + q = self.make_iq_query(xmlns=u'com:ctl', ito=self.vacuum + '/atom', + ifrom=self.user + '@' + self.domain + '/' + self.resource) + q['type'] = 'set' + for child in q.xml: + if child.tag.endswith('query'): + child.append(ctl) + return q + + def run(self): + self.connect(('47.88.66.164', '5223')) # TODO: change to domain name + click.echo("starting") + self.process() + click.echo("done with process") + self.wait_until_ready() + + +def read_config(filename): + parser = configparser.ConfigParser() + with open(filename) as fp: + parser.read_file(itertools.chain(['[global]'], fp), source=filename) + return parser['global'] + + +@click.group(chain=True) +@click.option('--charge/--no-charge', default=True, help='Return to charge after running. Defaults to yes.') +@click.option('--debug/--no-debug', default=False) +def cli(charge, debug): + logging.basicConfig(level=logging.DEBUG, format='%(levelname)-8s %(message)s') + + +@cli.command(help='cleans for the specified number of minutes') +@click.argument('minutes', type=click.FLOAT) +def clean(minutes): + def action(vacbot): + click.echo('cleaning') + vacbot.clean() + time.sleep(minutes * 60) + + return action + + +@cli.command(help='returns to charger') +def charge(vacbot): # TODO convert + click.echo('charging') + vacbot.charge() + time.sleep(20) + + +@cli.resultcallback() +def run(actions, charge, debug): + config = read_config(os.path.expanduser('~/.config/sucks.conf')) + vacbot = VacBot(config['user'], config['domain'], config['resource'], config['secret'], + config['vacuum']) + vacbot.run() + for action in actions: + click.echo("running " + str(action)) + action(vacbot) + if charge: + vacbot.charge() + vacbot.disconnect(wait=True) + + +if __name__ == '__main__': + click.echo("starting commands") + cli() + click.echo("done with commands") diff --git a/sucks.sh b/sucks.sh new file mode 100755 index 0000000..c47d377 --- /dev/null +++ b/sucks.sh @@ -0,0 +1,6 @@ +#!/bin/bash + +DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" + +cd ${DIR} +pipenv run -- python ./sucks.py "$@"