Skip to content
This repository was archived by the owner on Apr 5, 2021. It is now read-only.
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
66 commits
Select commit Hold shift + click to select a range
58ace66
Update .gitignore
bmartin5692 Jan 11, 2019
e408157
Basic support for D90X
bmartin5692 Jan 12, 2019
0da4532
Add spotarea
bmartin5692 Jan 13, 2019
c1f6814
Add clean_status for IOT
bmartin5692 Jan 13, 2019
ef0bd2e
Add backward action
bmartin5692 Jan 14, 2019
d317e4b
Fix tests & Clean
bmartin5692 Jan 14, 2019
304c576
Add spotarea command tests
bmartin5692 Jan 14, 2019
62185d1
Add spotclean to cli
bmartin5692 Jan 14, 2019
484502b
Cleanup init code
bmartin5692 Jan 14, 2019
205214c
Update .gitignore
bmartin5692 Jan 14, 2019
d070ab7
Delete launch.json
bmartin5692 Jan 14, 2019
ef467ac
Remove egg & update gitignore for vscode files
bmartin5692 Jan 14, 2019
4ca9750
Fix xmpp
bmartin5692 Jan 15, 2019
335ac6a
Handle already charging
bmartin5692 Jan 15, 2019
3c01cd2
Set timeout for IOT api calls
bmartin5692 Jan 15, 2019
ca7d37c
WIP: Initial MQTT work
bmartin5692 Jan 16, 2019
c72be55
MQTT Plumbing
bmartin5692 Jan 17, 2019
e2b0ea7
Add test MQTTPing
bmartin5692 Jan 17, 2019
c0eda3a
Fix clean from CLI
bmartin5692 Jan 17, 2019
6ff7173
Update __init__.py
bmartin5692 Jan 18, 2019
5a590d6
Merge pull request #1 from bmartin5692/MQTT
bmartin5692 Jan 18, 2019
605b1b5
Add to protocol.md
bmartin5692 Jan 18, 2019
10993e6
Fix tables in protocol
bmartin5692 Jan 18, 2019
f3bcfb4
Add more iot tests
bmartin5692 Jan 20, 2019
1812a21
Add mqtt tests
bmartin5692 Jan 21, 2019
2cfa5ec
Add ignore ssl to requests
bmartin5692 Jan 31, 2019
57979c3
Make ssl verification optional
bmartin5692 Feb 3, 2019
5d6df85
Fix custom commands
bmartin5692 Feb 8, 2019
b0bf467
stop printing positiion updates
bmartin5692 Feb 8, 2019
94a7844
Fix spot area clean
bmartin5692 Feb 16, 2019
409b04b
add tests
bmartin5692 Feb 16, 2019
9919baa
more tests
bmartin5692 Feb 16, 2019
597129d
Combine MQTT and IOT into IOTMQ
bmartin5692 Feb 18, 2019
ab30371
Update protocol.md
bmartin5692 Feb 19, 2019
6b23d85
Add SpotArea details
bmartin5692 Feb 19, 2019
f1257b8
format updates
bmartin5692 Feb 19, 2019
6c31e07
add comments and cleanup
bmartin5692 Feb 19, 2019
7fac965
Update setup.py
ecpunk Feb 19, 2019
bfca7e9
Merge pull request #2 from ecpunk/patch-1
bmartin5692 Feb 19, 2019
b7341b0
Update SpotArea
bmartin5692 Feb 20, 2019
0fa5fe4
Update protocol.md
bmartin5692 Feb 21, 2019
c37ebdd
Ozmo930 working
bmartin5692 Feb 25, 2019
f0aef13
Merge pull request #3 from bmartin5692/Ozmo930
bmartin5692 Feb 28, 2019
3dbaccd
Update test_ecovacs_xmpp.py
bmartin5692 Feb 28, 2019
91e3f3d
fix failing tests?
bmartin5692 Feb 28, 2019
e4848cb
Fix failing test
bmartin5692 Feb 28, 2019
3942e22
Add D600 to iotmqdevices
bmartin5692 Mar 14, 2019
70ecd77
Removing test made on assumptions
bmartin5692 Mar 14, 2019
79f9d67
Update protocol.md with mopping setting
bmartin5692 Mar 19, 2019
6da9a96
Update README API example
bmartin5692 Mar 19, 2019
5fda81e
Add verify_ssl to cli login
bmartin5692 Mar 19, 2019
3e1efae
Merge pull request #4 from bmartin5692/D901
bmartin5692 Mar 19, 2019
4b47e8e
Merge branch 'master' into pr/6
bmartin5692 Jun 14, 2019
813c620
Merge pull request #7 from bmartin5692/pr/6
bmartin5692 Jun 14, 2019
3c9c834
change str_to_bool func
bmartin5692 Jun 14, 2019
054f401
Merge branch 'master' into bumper-certs
bmartin5692 Jun 14, 2019
a14a294
Merge pull request #9 from bmartin5692/bumper-certs
bmartin5692 Jun 14, 2019
9d9ebf3
Fix de and setIOT
bmartin5692 Jun 15, 2019
56308ac
Merge pull request #10 from bmartin5692/fix-de
bmartin5692 Jun 15, 2019
25f806d
fix lookup tests
bmartin5692 Jun 15, 2019
ded517b
Merge pull request #12 from bmartin5692/fix-de
bmartin5692 Jun 15, 2019
dba6f56
add retry for set token
bmartin5692 Jun 17, 2019
eafccb6
Merge pull request #14 from bmartin5692/fix_setToken
bmartin5692 Jun 17, 2019
2a2b0c4
clean until bot returns to charger
bmartin5692 Jul 3, 2019
33d864b
Merge pull request #15 from bmartin5692/add-CleanAuto
bmartin5692 Jul 3, 2019
2ee455a
Added pause, resume, fixed stop
Wandersalamander Jan 19, 2020
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,11 @@ dist
# Nosetests files
cover/
.coverage

# Ignore Vscode files
.vscode/

# Ignore sucks.egg-info
sucks.egg-info/
.noseids
nosetests.xml
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -99,8 +99,8 @@ shaping the API.

A simple usage might go something like this:

```
import sucks
```python
from sucks import *

config = ...

Expand Down
Binary file added dist/sucks-0.9.3-py3.7.egg
Binary file not shown.
293 changes: 245 additions & 48 deletions protocol.md

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@
'requests>=2.18',
'pycryptodome>=3.4',
'pycountry-convert>=0.5',
'paho-mqtt>=1.4',
'stringcase>=1.2'
],

Expand Down
620 changes: 567 additions & 53 deletions sucks/__init__.py

Large diffs are not rendered by default.

64 changes: 52 additions & 12 deletions sucks/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,9 @@
import click
from pycountry_convert import country_alpha2_to_continent_code

import sucks
from sucks import *

print(sucks.__file__)
_LOGGER = logging.getLogger(__name__)


Expand Down Expand Up @@ -67,12 +68,15 @@ def __init__(self, wait_on, wait_for):

def wait(self, bot):
if not hasattr(bot, self.wait_on):
raise ValueError("object " + bot + " does not have method " + self.wait_on)
_LOGGER.debug("waiting on " + self.wait_on + " for value " + self.wait_for)
raise ValueError("object " + bot +
" does not have method " + self.wait_on)
_LOGGER.debug("waiting on " + self.wait_on +
" for value " + self.wait_for)

while getattr(bot, self.wait_on) != self.wait_for:
time.sleep(0.5)
_LOGGER.debug("wait complete; " + self.wait_on + " is now " + self.wait_for)
_LOGGER.debug("wait complete; " + self.wait_on +
" is now " + self.wait_for)


class CliAction:
Expand All @@ -96,7 +100,8 @@ def config_file_exists():
def read_config():
parser = configparser.ConfigParser()
with open(config_file()) as fp:
parser.read_file(itertools.chain(['[global]'], fp), source=config_file())
parser.read_file(itertools.chain(
['[global]'], fp), source=config_file())
return parser['global']


Expand Down Expand Up @@ -124,7 +129,8 @@ def should_run(frequency):
return True
n = random.random()
result = n <= frequency
_LOGGER.debug("tossing coin: {:0.3f} <= {:0.3f}: {}".format(n, frequency, result))
_LOGGER.debug("tossing coin: {:0.3f} <= {:0.3f}: {}".format(
n, frequency, result))
return result


Expand All @@ -141,15 +147,17 @@ def cli(debug):
@click.option('--country-code', prompt='your two-letter country code', default=lambda: current_country())
@click.option('--continent-code', prompt='your two-letter continent code',
default=lambda: continent_for_country(click.get_current_context().params['country_code']))
def login(email, password, country_code, continent_code):
@click.option('--verify-ssl', prompt='Verify SSL for API requests', default=True)
def login(email, password, country_code, continent_code, verify_ssl):
if config_file_exists() and not click.confirm('overwrite existing config?'):
click.echo("Skipping login.")
exit(0)
config = OrderedDict()
password_hash = EcoVacsAPI.md5(password)
device_id = EcoVacsAPI.md5(str(time.time()))
try:
EcoVacsAPI(device_id, email, password_hash, country_code, continent_code)
EcoVacsAPI(device_id, email, password_hash,
country_code, continent_code, verify_ssl)
except ValueError as e:
click.echo(e.args[0])
exit(1)
Expand All @@ -158,17 +166,22 @@ def login(email, password, country_code, continent_code):
config['device_id'] = device_id
config['country'] = country_code.lower()
config['continent'] = continent_code.lower()
config['verify_ssl'] = verify_ssl
write_config(config)
click.echo("Config saved.")
exit(0)


@cli.command(help='auto-cleans for the specified number of minutes')
@cli.command(help='auto-cleans for the specified number of minutes, if minutes is 0 auto clean until bot returns to charger by itself')
@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)
def clean(frequency, minutes):
waiter = StatusWait('charge_status', 'charging')
if minutes > 0:
waiter = TimeWait(minutes * 60)

if should_run(frequency):
return CliAction(Clean(), wait=TimeWait(minutes * 60))
return CliAction(Clean(), wait=waiter)


@cli.command(help='cleans room edges for the specified number of minutes')
Expand All @@ -179,6 +192,17 @@ def edge(frequency, minutes):
return CliAction(Edge(), wait=TimeWait(minutes * 60))


# ignore_unknown for map coordinates with negatives
@cli.command(help='cleans provided area(s), ex: "0,1"', context_settings={"ignore_unknown_options": True})
@click.option("--map-position", "-p", is_flag=True, help='clean provided map position instead of area, ex: "-602,1812,800,723"')
@click.argument('area', type=click.STRING, required=True)
def area(area, map_position):
if map_position:
return CliAction(SpotArea('start', map_position=area), wait=StatusWait('charge_status', 'returning'))
else:
return CliAction(SpotArea('start', area=area), wait=StatusWait('charge_status', 'returning'))


@cli.command(help='returns to charger')
def charge():
return charge_action()
Expand All @@ -193,6 +217,21 @@ def stop():
return CliAction(Stop(), terminal=True, wait=StatusWait('clean_status', 'stop'))


@cli.command(help='pause the robot in its current position')
def pause():
return CliAction(Pause(), terminal=True, wait=StatusWait('clean_status', 'pause'))


@cli.command(help='Resume job')
def resume():
return CliAction(Resume(), terminal=True, wait=StatusWait('charge_status', 'charging'))


@cli.command(help='get the current state of the robot')
def state():
return CliAction(GetCleanState(), terminal=True, wait=TimeWait(10))


@cli.resultcallback()
def run(actions, debug):
actions = list(filter(None.__ne__, actions))
Expand All @@ -209,9 +248,10 @@ def run(actions, debug):
if actions:
config = read_config()
api = EcoVacsAPI(config['device_id'], config['email'], config['password_hash'],
config['country'], config['continent'])
config['country'], config['continent'], verify_ssl=config['verify_ssl'])
vacuum = api.devices()[0]
vacbot = VacBot(api.uid, api.REALM, api.resource, api.user_access_token, vacuum, config['continent'])
vacbot = VacBot(api.uid, api.REALM, api.resource, api.user_access_token,
vacuum, config['continent'], verify_ssl=config['verify_ssl'])
vacbot.connect_and_wait_until_ready()

for action in actions:
Expand Down
76 changes: 70 additions & 6 deletions tests/test_commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,20 @@ def test_custom_command_inner_tag():
b'<ctl td="CustomCommand"><customtag customvar="customvalue" /></ctl>')


def test_custom_command_multiple_inner_tag():
# Ensure a custom-built command with multiple inner tags generates the expected XML payload
c = VacBotCommand('CustomCommand', {"customtag":[{"customvar":"customvalue1"},{"customvar":"customvalue2"}]})
logging.info(ElementTree.tostring(c.to_xml()))
assert_equals(ElementTree.tostring(c.to_xml()),
b'<ctl td="CustomCommand"><customtag customvar="customvalue1" /><customtag customvar="customvalue2" /></ctl>')

def test_custom_command_args_multiple_inner_tag():
# Ensure a custom-built command with args and multiple inner tags generates the expected XML payload
c = VacBotCommand('CustomCommand', {"arg1":"value1","customtag":[{"customvar":"customvalue1"},{"customvar":"customvalue2"}]})
assert_equals(ElementTree.tostring(c.to_xml()),
b'<ctl arg1="value1" td="CustomCommand"><customtag customvar="customvalue1" /><customtag customvar="customvalue2" /></ctl>')


def test_custom_command_noargs():
# Ensure a custom-built command with no args generates XML without an args element
c = VacBotCommand('CustomCommand')
Expand All @@ -29,22 +43,63 @@ def test_custom_command_noargs():
def test_clean_command():
c = Clean()
assert_equals(ElementTree.tostring(c.to_xml()),
b'<ctl td="Clean"><clean speed="standard" type="auto" /></ctl>') # protocol has attribs in other order
b'<ctl td="Clean"><clean act="s" speed="standard" type="auto" /></ctl>') # protocol has attribs in other order

c = Clean('edge', 'high')
assert_equals(ElementTree.tostring(c.to_xml()),
b'<ctl td="Clean"><clean speed="strong" type="border" /></ctl>') # protocol has attribs in other order
b'<ctl td="Clean"><clean act="s" speed="strong" type="border" /></ctl>') # protocol has attribs in other order

c = Clean(iotmq=True)
assert_equals(ElementTree.tostring(c.to_xml()),
b'<ctl td="Clean"><clean act="s" speed="standard" type="auto" /></ctl>') # test for iot act is added


def test_spotarea_command():
assert_raises(ValueError, SpotArea, 'start') #Value error if SpotArea doesn't include a mid or p

c = SpotArea('start', '0')
assert_equals(ElementTree.tostring(c.to_xml()),
b'<ctl td="Clean"><clean act="s" mid="0" speed="standard" type="SpotArea" /></ctl>') #Test namedarea clean

c = SpotArea('start', area='0')
assert_equals(ElementTree.tostring(c.to_xml()),
b'<ctl td="Clean"><clean act="s" mid="0" speed="standard" type="SpotArea" /></ctl>') #Test namedarea keyword clean

c = SpotArea('start', '', '-602,1812,800,723')
assert_equals(ElementTree.tostring(c.to_xml()),
b'<ctl td="Clean"><clean act="s" deep="1" p="-602,1812,800,723" speed="standard" type="SpotArea" /></ctl>') #Test customarea clean

c = SpotArea('start', '', '-602,1812,800,723', '2')
assert_equals(ElementTree.tostring(c.to_xml()),
b'<ctl td="Clean"><clean act="s" deep="2" p="-602,1812,800,723" speed="standard" type="SpotArea" /></ctl>') #Test customarea clean with deep 2

c = SpotArea('start', '', map_position='-602,1812,800,723')
assert_equals(ElementTree.tostring(c.to_xml()),
b'<ctl td="Clean"><clean act="s" deep="1" p="-602,1812,800,723" speed="standard" type="SpotArea" /></ctl>') #Test customarea keyword clean with deep default

c = SpotArea('start', map_position='-602,1812,800,723', cleanings='2')
assert_equals(ElementTree.tostring(c.to_xml()),
b'<ctl td="Clean"><clean act="s" deep="2" p="-602,1812,800,723" speed="standard" type="SpotArea" /></ctl>') #Test customarea keyword and cleanings keyword clean with deep default

c = SpotArea('start', area='0', map_position='-602,1812,800,723', cleanings='2')
assert_equals(ElementTree.tostring(c.to_xml()),
b'<ctl td="Clean"><clean act="s" mid="0" speed="standard" type="SpotArea" /></ctl>') #Test all keywords specified, should default to only mid

c = SpotArea('start', '0', '-602,1812,800,723','2')
assert_equals(ElementTree.tostring(c.to_xml()),
b'<ctl td="Clean"><clean act="s" mid="0" speed="standard" type="SpotArea" /></ctl>') #Test all keywords specified, should default to only mid


def test_edge_command():
c = Edge()
assert_equals(ElementTree.tostring(c.to_xml()),
b'<ctl td="Clean"><clean speed="strong" type="border" /></ctl>') # protocol has attribs in other order
b'<ctl td="Clean"><clean act="s" speed="strong" type="border" /></ctl>') # protocol has attribs in other order


def test_spot_command():
c = Spot()
assert_equals(ElementTree.tostring(c.to_xml()),
b'<ctl td="Clean"><clean speed="strong" type="spot" /></ctl>') # protocol has attribs in other order
b'<ctl td="Clean"><clean act="s" speed="strong" type="spot" /></ctl>') # protocol has attribs in other order


def test_charge_command():
Expand All @@ -56,7 +111,7 @@ def test_charge_command():
def test_stop_command():
c = Stop()
assert_equals(ElementTree.tostring(c.to_xml()),
b'<ctl td="Clean"><clean speed="standard" type="stop" /></ctl>')
b'<ctl td="Clean"><clean act="s" speed="standard" type="stop" /></ctl>')


def test_play_sound_command():
Expand Down Expand Up @@ -89,7 +144,6 @@ def test_get_battery_state_command():
b'<ctl td="GetBatteryInfo" />')



def test_move_command():
c = Move(action='left')
assert_equals(ElementTree.tostring(c.to_xml()),
Expand All @@ -103,6 +157,9 @@ def test_move_command():
c = Move(action='forward')
assert_equals(ElementTree.tostring(c.to_xml()),
b'<ctl td="Move"><move action="forward" /></ctl>')
c = Move(action='backward')
assert_equals(ElementTree.tostring(c.to_xml()),
b'<ctl td="Move"><move action="backward" /></ctl>')
c = Move(action='stop')
assert_equals(ElementTree.tostring(c.to_xml()),
b'<ctl td="Move"><move action="stop" /></ctl>')
Expand All @@ -112,9 +169,16 @@ def test_get_lifepsan_command():
c = GetLifeSpan('main_brush')
assert_equals(ElementTree.tostring(c.to_xml()),
b'<ctl td="GetLifeSpan" type="Brush" />')

c = GetLifeSpan('side_brush')
assert_equals(ElementTree.tostring(c.to_xml()),
b'<ctl td="GetLifeSpan" type="SideBrush" />')

c = GetLifeSpan('filter')
assert_equals(ElementTree.tostring(c.to_xml()),
b'<ctl td="GetLifeSpan" type="DustCaseHeap" />')

def test_set_time_command():
c = SetTime('1234', 'GMT-5')
assert_equals(ElementTree.tostring(c.to_xml()),
b'<ctl td="SetTime"><time t="1234" tz="GMT-5" /></ctl>')
Loading