From 33250ee1cbd120858c628720e5547dbf0da84565 Mon Sep 17 00:00:00 2001 From: Maurizio Porrato Date: Mon, 11 Feb 2019 12:41:14 +0000 Subject: [PATCH 1/5] Get rid of click. Use cmd and shlex instead. --- eufy.py | 137 +++++++++++++++++++++++++++++++++++++++----------------- 1 file changed, 96 insertions(+), 41 deletions(-) diff --git a/eufy.py b/eufy.py index 85fb95e..aa4d8b0 100755 --- a/eufy.py +++ b/eufy.py @@ -1,7 +1,15 @@ #!/usr/bin/env python3 -import click -from datetime import datetime +import cmd +from datetime import datetime, time +from inspect import getargspec +from shlex import shlex +from time import sleep + + +def drive_ir(pulses): + # Debugging placeholder + print(','.join([str(x) for x in pulses])) class EufyRawIR: @@ -24,7 +32,7 @@ class EufyRawIR: return 3*signal def sendRaw(self, message): - print(self._modulate(message)) + drive_ir(self._modulate(message)) class EufyIR(EufyRawIR): @@ -122,47 +130,94 @@ class EufyIR(EufyRawIR): self.send(self.RETURN_BASE) -@click.group() -def cli(): - pass +def lexer(f): + def g(self, args): + argv = tuple(x.lower() for x in shlex(args)) + s = getargspec(f) + maxargs = len(s.args or []) + minargs = maxargs - len(s.defaults or []) + if minargs <= len(argv) + 1 <= maxargs: + return f(self, *argv) + else: + self.stdout.write("*** Invalid arguments for {}\n".format(f.__name__[3:])) + return g -@cli.command() -@click.argument('mode', required=False, - type=click.Choice(['auto', 'spot', 'edge', 'room'])) -def clean(mode): - eufy = EufyIR() - dict( - auto=eufy.cleanAuto, - spot=eufy.cleanSpot, - edge=eufy.cleanEdge, - room=eufy.cleanRoom - ).get(mode, eufy.start)() -@cli.command() -@click.argument('mode', required=True, - type=click.Choice(['standard', 'boostiq', 'max'])) -def power(mode): - eufy = EufyIR() - { - 'standard': eufy.powerStandard, - 'boostiq': eufy.powerBoostIQ, - 'max': eufy.powerMax - }.get(mode)() +class Eufy(cmd.Cmd): -@cli.command() -@click.argument('mode', required=True, - type=click.Choice(['forward', 'backward', 'ccw', 'cw', 'left', 'right'])) -def move(mode): - eufy = EufyIR() - dict( - forward=eufy.moveForward, - backward=eufy.moveBackward, - ccw=eufy.moveCCW, - cw=eufy.moveCW, - left=eufy.moveLeft, - right=eufy.moveRight - ).get(mode)() + prompt = 'eufy> ' + + def __init__(self, *args, **kwargs): + super(Eufy, self).__init__(*args, **kwargs) + self.ir = EufyIR() + + @lexer + def do_clean(self, mode='auto'): + { + 'auto': self.ir.cleanAuto, + 'spot': self.ir.cleanSpot, + 'edge': self.ir.cleanEdge, + 'room': self.ir.cleanRoom + }.get(mode)() + + @lexer + def do_start(self): + self.ir.cleanAuto() + + @lexer + def do_stop(self): + self.ir.stop() + + @lexer + def do_power(self, mode): + { + 'standard': self.ir.powerStandard, + 'boostiq': self.ir.powerBoostIQ, + 'max': self.ir.powerMax + }.get(mode)() + + @lexer + def do_move(self, direction): + { + 'forward': self.ir.moveForward, + 'backward': self.ir.moveBackward, + 'ccw': self.ir.moveCCW, + 'cw': self.ir.moveCW, + 'left': self.ir.moveCCW, + 'right': self.ir.moveCW + }.get(direction)() + + @lexer + def do_time(self): + self.ir.setTime() + + @lexer + def do_schedule(self, t): + newtime = datetime.now() + newtime.strptime(t, "%H:%M") + self.ir.setSchedule(newtime.time()) + + @lexer + def do_base(self): + self.ir.returnBase() + + @lexer + def do_pause(self, delay=1): + sleep(int(delay)) + + @lexer + def do_quit(self): + return True + + @lexer + def do_EOF(self): + return True if __name__ == '__main__': - cli() + import sys + + eufy = Eufy() + if len(sys.argv) > 1: + eufy.cmdqueue = ' '.join(sys.argv[1:]).split(',') + ['quit'] + eufy.cmdloop() From b43c7bae975f0fa417567fb0255bb60b5c76f20c Mon Sep 17 00:00:00 2001 From: Maurizio Porrato Date: Mon, 11 Feb 2019 13:46:20 +0000 Subject: [PATCH 2/5] Documentation cleanup --- protocol.txt | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/protocol.txt b/protocol.txt index e2c3f14..14a5cc1 100644 --- a/protocol.txt +++ b/protocol.txt @@ -1,3 +1,6 @@ +Eufy RoboVac 11s IR protocol +============================ + Preamble: H:3ms L:3ms Logical 0: H:400µs L:600µs Logical 1: H:400µs L:1.6ms @@ -9,11 +12,11 @@ Frame: (Preamble+48bits+Trailer)*3 Format: 68CCHHMMSSKK (Hex, MSB first) - CC: command - HH: hour - MM: minute - SS: schedule (in quarters of an hour from midnight, 0xff=off) - KK: checksum (sum of all bytes in the frame modulo 256) + CC: command + HH: hour + MM: minute + SS: schedule (in quarters of an hour from midnight, 0xff=off) + KK: checksum (sum of all bytes in the frame modulo 256) CC | Command ------+--------- From 110297ffb832f02cc8867a8534b149a6cdaf0945 Mon Sep 17 00:00:00 2001 From: Maurizio Porrato Date: Mon, 11 Feb 2019 14:35:51 +0000 Subject: [PATCH 3/5] Use argparse --- eufy.py | 23 ++++++++++++++++++----- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/eufy.py b/eufy.py index aa4d8b0..1535faf 100755 --- a/eufy.py +++ b/eufy.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 -import cmd +from cmd import Cmd from datetime import datetime, time from inspect import getargspec from shlex import shlex @@ -143,7 +143,7 @@ def lexer(f): return g -class Eufy(cmd.Cmd): +class Eufy(Cmd): prompt = 'eufy> ' @@ -215,9 +215,22 @@ class Eufy(cmd.Cmd): if __name__ == '__main__': - import sys + from argparse import ArgumentParser, FileType + + parser = ArgumentParser(description='Eufy RoboVac 11s CLI tool') + parser.add_argument('-f', '--file', type=FileType('r'), help='read commands from file') + parser.add_argument('command', nargs='*') + args = parser.parse_args() eufy = Eufy() - if len(sys.argv) > 1: - eufy.cmdqueue = ' '.join(sys.argv[1:]).split(',') + ['quit'] + + if args.file: + eufy.cmdqueue.extend(args.file.readlines()) + + if args.command: + eufy.cmdqueue.extend(' '.join(args.command).split(',')) + + if eufy.cmdqueue: + eufy.cmdqueue.append('quit') + eufy.cmdloop() From e05c892eaf2e1140205f8e6da843ceb992866408 Mon Sep 17 00:00:00 2001 From: Maurizio Porrato Date: Mon, 11 Feb 2019 21:08:21 +0000 Subject: [PATCH 4/5] Avoid using deprecated inspect.getargspec() --- eufy.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/eufy.py b/eufy.py index 1535faf..aa2a64d 100755 --- a/eufy.py +++ b/eufy.py @@ -2,7 +2,7 @@ from cmd import Cmd from datetime import datetime, time -from inspect import getargspec +from inspect import signature, Signature from shlex import shlex from time import sleep @@ -133,9 +133,9 @@ class EufyIR(EufyRawIR): def lexer(f): def g(self, args): argv = tuple(x.lower() for x in shlex(args)) - s = getargspec(f) - maxargs = len(s.args or []) - minargs = maxargs - len(s.defaults or []) + s = signature(f) + maxargs = len(s.parameters) + minargs = len([x for _,x in s.parameters.items() if x.default is not Signature.empty]) if minargs <= len(argv) + 1 <= maxargs: return f(self, *argv) else: From d5bfbc27b619121d4e1c4657afc99648d2348f07 Mon Sep 17 00:00:00 2001 From: Maurizio Porrato Date: Mon, 11 Feb 2019 22:16:43 +0000 Subject: [PATCH 5/5] Persist schedule. Drop shlex. --- eufy.py | 29 ++++++++++++++++++++--------- 1 file changed, 20 insertions(+), 9 deletions(-) diff --git a/eufy.py b/eufy.py index aa2a64d..fbf3b69 100755 --- a/eufy.py +++ b/eufy.py @@ -3,7 +3,7 @@ from cmd import Cmd from datetime import datetime, time from inspect import signature, Signature -from shlex import shlex +import os from time import sleep @@ -58,7 +58,12 @@ class EufyIR(EufyRawIR): RETURN_BASE = 0xef def __init__(self): - self._schedule = 0xff + self._schedule_file = os.path.expanduser('~/.eufy-schedule') + try: + with open(os.path.expanduser(self._SCHEDULE_FILE), 'r') as f: + self._schedule = int(f.read(5)) + except: + self._schedule = 0xff def _message(self, code): now = datetime.now() @@ -119,11 +124,17 @@ class EufyIR(EufyRawIR): self.send(self.SET_TIME) def setSchedule(self, t): - # FIXME: properly store schedule - self.send(self.SET_SCHEDULE, t) + self._schedule = t.hour * 4 + t.minute // 15 + with open(self._schedule_file, 'w') as f: + f.write(str(self._schedule)) + self.send(self.SET_SCHEDULE) def cancelSchedule(self): - # FIXME: properly store schedule + self._schedule = 0xff + try: + os.remove(self._schedule_file) + except: + pass self.send(self.CANCEL_SCHEDULE) def returnBase(self): @@ -132,11 +143,11 @@ class EufyIR(EufyRawIR): def lexer(f): def g(self, args): - argv = tuple(x.lower() for x in shlex(args)) + argv = tuple(x.lower() for x in args.split()) s = signature(f) - maxargs = len(s.parameters) - minargs = len([x for _,x in s.parameters.items() if x.default is not Signature.empty]) - if minargs <= len(argv) + 1 <= maxargs: + maxargs = len(s.parameters) - 1 + minargs = len([x for _,x in s.parameters.items() if x.default is Signature.empty]) - 1 + if minargs <= len(argv) <= maxargs: return f(self, *argv) else: self.stdout.write("*** Invalid arguments for {}\n".format(f.__name__[3:]))