Adding probabilistic options and improving docs.

This commit is contained in:
William Pietri
2017-11-04 10:26:07 -07:00
parent e633c2be22
commit 78dbaa1525
3 changed files with 162 additions and 32 deletions
+62 -14
View File
@@ -8,6 +8,16 @@ 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, 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. send email to my first name at williampietri.com.
If you're curious about the protocol, I have [a very rough
doc](protocol.md) started. I'll happily accept pull requests for it.
Why the project name? Well, a) it's ridiculous that I needed to MITM
my own vacuum. This is not the future I signed up for. There should
be a nice, tidy RESTful API. That would be easy enough to make. And b),
it's a vacuum.
## Usage
If you do try to use it, you'll need to create ~/.config/sucks.conf. It If you do try to use it, you'll need to create ~/.config/sucks.conf. It
should look something like this: should look something like this:
@@ -20,23 +30,61 @@ vacuum=[robot id]@126.ecorobot.net
``` ```
I got these values by using I got these values by using
[xmppeek](https://www.beneaththewaves.net/Software/XMPPPeek.html) [xmppeek](https://www.beneaththewaves.net/Software/XMPPPeek.html) to do
to do a man-in-the-middle attack on a man-in-the-middle attack on the android app. You can use the included
the android app. You can use the included log_clean.py script to generate log_clean.py script to generate a config from a captured session. (I
a config from a captured session. (I suspect that the Android app re-keys suspect that the Android app re-keys the connection on a regular basis,
the connection on a regular basis, as the secret was changing regularly as the secret was changing regularly up until I cleared the Android
up until I cleared the Android app's data from my phone.) app's data from my phone.)
If you're curious about the protocol, I have [a very rough With that set up, you could have it clean in auto mode for 10 minutes
doc](protocol.md) started. I'll happily accept pull requests for it. and return to its charger:
```
% sucks clean 10
```
You could have it clean for 15 minutes and then do an extra 10 minutes
of edging:
```
% sucks clean 15 edge 10
```
If you wanted it to clean for 5 minutes and then stop where it was,
either of these would work:
```
% sucks clean 5 stop
% sucks --no-charge clean 5
```
If it's running amok and you'd just like it to stop where it is:
```
% sucks stop
```
To tell it to go find plug in:
```
% sucks charge
```
I run mine from my crontab, but I didn't want it to clean every day,
so it also has a mode where it randomly decide to run or not based on
a frequency you give it. My crontab entry looks like this:
```
0 10 * * * /home/william/projects/sucks/sucks clean -f 4/7 15 edge -f 1/14 10
```
This means that every day at 10 am, it might do something. 4 days out
of 7, it will do 15 minutes of automatic cleaning. 1 day out of 14,
it will do another 10 minutes of edging. And afterward it will always
go back to charge.
Why the project name? Well, a) it's ridiculous that I needed to MITM
my own vacuum. This is not the future I signed up for. There should
be a nice, tidy RESTful API. That would be easy enough to make. And b),
it's a vacuum.
## To Do ## To Do
* add probabilistic cleaning options
* implement more commands
* add a status commmand * add a status commmand
+71 -16
View File
@@ -2,6 +2,8 @@ import configparser
import itertools import itertools
import logging import logging
import os import os
import random
import re
import time import time
from threading import Event from threading import Event
@@ -74,7 +76,6 @@ class VacBot(ClientXMPP):
self.wait_until_ready() self.wait_until_ready()
def run(self, action): def run(self, action):
click.echo("performing " + str(action))
self.send_command(action.to_xml()) self.send_command(action.to_xml())
action.wait_for_completion(self) action.wait_for_completion(self)
@@ -88,7 +89,7 @@ class VacBotCommand():
def wait_for_completion(self, bot): def wait_for_completion(self, bot):
if self.wait: if self.wait:
click.echo("waiting in " + self.name + " for " + str(self.wait) + "s") click.echo("waiting in " + self.command_name() + " for " + str(self.wait) + "s")
time.sleep(self.wait) time.sleep(self.wait)
def to_xml(self): def to_xml(self):
@@ -98,13 +99,17 @@ class VacBotCommand():
return ctl return ctl
def __str__(self, *args, **kwargs): def __str__(self, *args, **kwargs):
return self.name + " command" return self.command_name() + " command"
def command_name(self):
return self.__class__.__name__.lower()
class Clean(VacBotCommand): class Clean(VacBotCommand):
def __init__(self, wait): def __init__(self, wait):
super().__init__('clean', {'type': 'auto', 'speed': 'standard'}, wait) super().__init__('clean', {'type': 'auto', 'speed': 'standard'}, wait)
class Edge(VacBotCommand): class Edge(VacBotCommand):
def __init__(self, wait): def __init__(self, wait):
super().__init__('clean', {'type': 'border', 'speed': 'strong'}, wait) super().__init__('clean', {'type': 'border', 'speed': 'strong'}, wait)
@@ -133,6 +138,35 @@ class Stop(VacBotCommand):
logging.debug("done waiting in " + self.name) logging.debug("done waiting in " + self.name)
class FrequencyParamType(click.ParamType):
name = 'frequency'
RATIONAL_PATTERN = re.compile(r'([.0-9])/([.0-9])')
def convert(self, value, param, ctx):
result = None
try:
search = self.RATIONAL_PATTERN.search(value)
if search:
result = float(search.group(1)) / float(search.group(2))
else:
try:
result = float(value)
except ValueError:
pass
except ValueError:
pass
if result is None:
self.fail('%s is not a valid frequency' % value, param, ctx)
if 0 <= result <= 1:
return result
self.fail('%s is not between 0 and 1' % value, param, ctx)
FREQUENCY = FrequencyParamType()
def read_config(filename): def read_config(filename):
parser = configparser.ConfigParser() parser = configparser.ConfigParser()
with open(filename) as fp: with open(filename) as fp:
@@ -140,6 +174,15 @@ def read_config(filename):
return parser['global'] return parser['global']
def should_run(frequency):
if frequency is None:
return
n = random.random()
result = n <= frequency
logging.debug("tossing coin: {:0.3f} <= {:0.3f}: {}".format( n, frequency, result))
return result
@click.group(chain=True) @click.group(chain=True)
@click.option('--charge/--no-charge', default=True, help='Return to charge after running. Defaults to yes.') @click.option('--charge/--no-charge', default=True, help='Return to charge after running. Defaults to yes.')
@click.option('--debug/--no-debug', default=False) @click.option('--debug/--no-debug', default=False)
@@ -149,14 +192,19 @@ def cli(charge, debug):
@cli.command(help='auto-cleans for the specified number of minutes') @cli.command(help='auto-cleans for the specified number of minutes')
@click.option('--frequency', '-f', type=FREQUENCY, help='frequency with which to run; e.g. 0.5 or 3/7')
@click.argument('minutes', type=click.FLOAT) @click.argument('minutes', type=click.FLOAT)
def clean(minutes): def clean(frequency, minutes):
return Clean(minutes * 60) if should_run(frequency):
return Clean(minutes * 60)
@cli.command(help='cleans room edges for the specified number of minutes') @cli.command(help='cleans room edges for the specified number of minutes')
@click.option('--frequency', '-f', type=FREQUENCY, help='frequency with which to run; e.g. 0.5 or 3/7')
@click.argument('minutes', type=click.FLOAT) @click.argument('minutes', type=click.FLOAT)
def edge(minutes): def edge(frequency, minutes):
return Edge(minutes * 60) if should_run(frequency):
return Edge(minutes * 60)
@cli.command(help='returns to charger') @cli.command(help='returns to charger')
@@ -171,15 +219,22 @@ def stop():
@cli.resultcallback() @cli.resultcallback()
def run(actions, charge, debug): def run(actions, charge, debug):
config = read_config(os.path.expanduser('~/.config/sucks.conf')) actions = list(filter(None.__ne__, actions))
vacbot = VacBot(config['user'], config['domain'], config['resource'], config['secret'], if actions and charge and not actions[-1].terminal:
config['vacuum']) actions.append(Charge())
vacbot.connect_and_wait_until_ready()
for action in actions: if actions:
vacbot.run(action) config = read_config(os.path.expanduser('~/.config/sucks.conf'))
if charge and not actions[-1].terminal: vacbot = VacBot(config['user'], config['domain'], config['resource'], config['secret'],
vacbot.run(Charge()) config['vacuum'])
vacbot.disconnect(wait=True) vacbot.connect_and_wait_until_ready()
for action in actions:
click.echo("performing " + str(action))
vacbot.run(action)
vacbot.disconnect(wait=True)
click.echo("done") click.echo("done")
+29 -2
View File
@@ -1,9 +1,10 @@
from xml.etree import ElementTree from xml.etree import ElementTree
from nose.tools import assert_equals from nose.tools import *
from sucks import * from sucks import *
# There are no tests for the XMPP stuff here because a) it's relatively complicated to test given # There are no tests for the XMPP stuff here because a) it's relatively complicated to test given
# the library's design and its multithreaded nature, and b) I'm manually testing every change anyhow, # the library's design and its multithreaded nature, and b) I'm manually testing every change anyhow,
# as it's not clear how the robot really behaves. # as it's not clear how the robot really behaves.
@@ -16,7 +17,6 @@ def test_clean_command():
b'<ctl td="Clean"><clean speed="standard" type="auto" /></ctl>') # protocol has attribs in other order b'<ctl td="Clean"><clean speed="standard" type="auto" /></ctl>') # protocol has attribs in other order
def test_edge_command(): def test_edge_command():
# called Edge because that's what the UI uses, even though the protocol is different # called Edge because that's what the UI uses, even though the protocol is different
c = Edge(10) c = Edge(10)
@@ -39,3 +39,30 @@ def test_stop_command():
assert_equals(ElementTree.tostring(c.to_xml()), assert_equals(ElementTree.tostring(c.to_xml()),
b'<ctl td="Clean"><clean speed="standard" type="stop" /></ctl>') b'<ctl td="Clean"><clean speed="standard" type="stop" /></ctl>')
def test_frequency_param_type():
t = FREQUENCY
assert_equals(t.convert('0', None, None), 0)
assert_equals(t.convert('1', None, None), 1.0)
assert_equals(t.convert('1/2', None, None), 0.5)
assert_equals(t.convert('1/7', None, None), 1.0 / 7.0)
with assert_raises(click.exceptions.BadParameter):
t.convert('bob', None, None)
with assert_raises(click.exceptions.BadParameter):
t.convert('2', None, None)
with assert_raises(click.exceptions.BadParameter):
t.convert('7/5', None, None)
def test_should_run():
count = 0
for _ in range(10000):
if should_run(0.1):
count += 1
assert_almost_equal(count, 1000, delta=200)
count = 0
for _ in range(10000):
if should_run(0.9):
count += 1
assert_almost_equal(count, 9000, delta=200)