diff --git a/.gitignore b/.gitignore index cd52c5d..4832bbf 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,4 @@ -build +build/ dist __pycache__ *.lock diff --git a/CHANGELOG.md b/CHANGELOG.md index b96169a..07e9986 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,22 @@ +# v1.2 + +- Major changes to default behavior: + - If time is not specified, whatever mode is selected will run until the vacuum gets low on battery. Then it will go home. + - If a time is specified, vacuum will run until the end of elapsed time then it will stop in place. +- Addition of a mode flag: + - `-m quiet`: Automatic direction, low suction + - `-m default`: Automatic direction, medium suction + - `-m max`: Automatic direction, maximum suction + - `-m edge`: Stick to the edges of the room + - `-m spot`: "Spot clean" - goes in goes in a spiral from one point +- Addition of a status flag: + - `-s or --status` will connect to robovac and print status. +- Improve tuya logs and callbacks by associating status names with their values. + - whereas before, status indicators such as "power" and "work mode" were reported as numerical keys, the keys are now just called "power" and "work mode". +- add a return to base flag + - `--return or -r`: when combined with `--time or -t`, will instruct robovac to return to base after time has elapsed. (default is to pause in place when time elapses). + - This is semantically different from `-b or --home` because it only impacts a timed job. + # v1.1 - Fix bugs and improve code efficiency diff --git a/README.md b/README.md index 7019318..9f4800b 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,7 @@ - [Command-line](#command-line) - [Config file](#config-file) - [Usage](#usage) + - [Examples](#examples-1) - [Obtaining device credentials](#obtaining-device-credentials) - [tl;dr](#tldr) - [Instructions:](#instructions-1) @@ -77,7 +78,7 @@ local_code=LOCAL_CODE # Usage ``` -usage: vac [-h] [-c CONFIG] [--device_id DEVICE_ID] [--ip IP] [--local_code LOCAL_CODE] [--time TIME] [--pause] [--home] [--debug] [--verbose] [--quiet] +usage: vac.py [-h] [-c CONFIG] [--device_id DEVICE_ID] [--ip IP] [--local_code LOCAL_CODE] [--time TIME] [--home] [--pause] [--mode MODE] [--verbose] [--quiet] [--status] [--return] Control a Robovac device. @@ -91,11 +92,33 @@ options: --local_code LOCAL_CODE Secret key obtained from eufy --time TIME, -t TIME Cleaning time in minutes - --pause, -p Pause vacuum --home, -b Go home - --debug, -d Enter debugging mode (won't send commands to vacuum) + --pause, -p Pause vacuum + --mode MODE, -m MODE Options: default, max, edge, spot, quiet --verbose, -v Enable verbose logs --quiet, -q Quiet logs + --status, -s Print status + --return, -r Return to base upon completion (requires -t) +``` + +## Examples + +__Clean for 5 minutes, then go home__ + +``` +robovac -t 5 -r +``` + +__Clean the edge of the room until the battery runs out, then go home__ + +``` +robovac -m edge +``` + +__Clean for 20 minutes then go home. Quiet all logs and run process in the background__ + +``` +robovac -t 20 -r -q > /dev/null 2>&1 & ``` # Obtaining device credentials diff --git a/VERSION b/VERSION index 69613ab..9bd7467 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -version=1.1 \ No newline at end of file +version=1.2 \ No newline at end of file diff --git a/build b/build new file mode 100755 index 0000000..f41985d --- /dev/null +++ b/build @@ -0,0 +1,8 @@ +#!/bin/bash + + +NAME="robovac-build" + + +docker buildx build . -t $NAME +docker run -v $PWD:/opt/build --rm -w "/opt/build" $NAME bash -c "pipenv install;pipenv run pyinstaller -F src/vac.py" \ No newline at end of file diff --git a/compile b/compile deleted file mode 100755 index ed31389..0000000 --- a/compile +++ /dev/null @@ -1,7 +0,0 @@ -#!/bin/bash - - -NAME="compile" - -docker buildx build . -t $NAME -docker run -v $PWD:/opt/compile --rm -w "/opt/compile" $NAME bash -c "pipenv install;pipenv run pyinstaller -F src/vac.py" \ No newline at end of file diff --git a/src/robovac/tuya.py b/src/robovac/tuya.py index 3d3a2ca..fd82b3e 100644 --- a/src/robovac/tuya.py +++ b/src/robovac/tuya.py @@ -185,7 +185,7 @@ class TuyaCipher: '>IIIH', encrypted_data, 3) return 15 return 0 - + def decrypt(self, command, data): prefix_size = self.get_prefix_size_and_validate(command, data) @@ -536,7 +536,22 @@ class TuyaDevice: async def async_update_state(self, state_message, _): self._dps.update(state_message.payload["dps"]) - _LOGGER.info("Received updated state {}: {}".format(self, self._dps)) + dict1 = { + 'POWER': '1', + 'PLAY_PAUSE': '2', + 'DIRECTION': '3', + 'WORK_MODE': '5', + 'WORK_STATUS': '15', + 'GO_HOME': '101', + 'CLEAN_SPEED': '102', + 'FIND_ROBOT': '103', + 'BATTERY_LEVEL': '104', + 'ERROR_CODE': '106' + } + dict2 = self._dps + combined_dict = {key: dict2[value] for key, value in dict1.items() if value in dict2} + + _LOGGER.info("Received updated state {}: {}".format(self, combined_dict)) @property def state(self): diff --git a/src/vac.py b/src/vac.py index 32dc90b..86fd549 100755 --- a/src/vac.py +++ b/src/vac.py @@ -2,22 +2,44 @@ import asyncio import signal import logging -import pprint import sys import argparse from enum import Enum import os +from pprint import pprint -from robovac.robovac import Robovac +from robovac.robovac import Robovac, CleanSpeed, WorkMode, WorkStatus -DEFAULT_TIME = 20 +DEFAULT_TIME = False stop_event = asyncio.Event() +device_status = { + 'POWER': '1', + 'PLAY_PAUSE': '2', + 'DIRECTION': '3', + 'WORK_MODE': '5', + 'WORK_STATUS': '15', + 'GO_HOME': '101', + 'CLEAN_SPEED': '102', + 'FIND_ROBOT': '103', + 'BATTERY_LEVEL': '104', + 'ERROR_CODE': '106' +} + + +class Modes: + DEFAULT = CleanSpeed.BOOST_IQ, WorkMode.AUTO + QUIET = CleanSpeed.STANDARD, WorkMode.AUTO + MAX = CleanSpeed.MAX, WorkMode.AUTO + EDGE = CleanSpeed.MAX, WorkMode.EDGE + SPOT = CleanSpeed.MAX, WorkMode.SPOT + + def signal_handler(): print(f'\nExiting...') stop_event.set() @@ -55,12 +77,23 @@ def parse_robovac_config(path="/home/freyja/Desktop/robovac.conf"): return credentials +async def ordered_callback(message,device): + # print(message) + dict1 = device_status + dict2 = device.state + combined_dict = {key: dict2[value] for key, value in dict1.items() if value in dict2} + pprint(combined_dict) + + async def callback(message,device): - print(message) - pprint.pprint(device.state) + # print(message) + dict1 = device_status + dict2 = device.state + combined_dict = {key: dict2[value] for key, value in dict1.items() if value in dict2} + print(combined_dict) -async def stepper(time): +async def stepper(time) -> bool: total_seconds = time * 60 step = 1 elapsed = 0 @@ -70,18 +103,25 @@ async def stepper(time): return True -async def async_auto_clean(r: Robovac,time): - await r.async_start_cleaning(callback) - await stepper(time) - await r.async_pause() +async def async_clean(r: Robovac, mode: Modes): + if r.work_mode != mode[1]: + await r.async_set_work_mode(mode[1],callback) + await asyncio.sleep(1) + await r.async_set_clean_speed(mode[0],callback) + await asyncio.sleep(1) async def async_go_home(r: Robovac): - await r.async_go_home(callback) + if r.work_status != WorkStatus.CHARGING: + await r.async_go_home(callback) + await asyncio.sleep(1) + async def async_pause(r: Robovac): - await r.async_pause(callback) + if r.work_status != WorkStatus.CHARGING: + await r.async_pause(callback) + await asyncio.sleep(1) async def async_main( @@ -90,19 +130,27 @@ async def async_main( local_code, time=DEFAULT_TIME, go_home=False, - pause=False + pause=False, + mode=None, + status=False, + rtb=False ): asyncio.get_event_loop().add_signal_handler(signal.SIGTERM, signal_handler) asyncio.get_event_loop().add_signal_handler(signal.SIGINT, signal_handler) r = Robovac(device_id,ip,local_code) - await r.async_connect(callback) + await r.async_connect(ordered_callback if status else callback) await asyncio.sleep(2) - if not go_home and not pause: + if status: + stop_event.set() + + + if mode and not go_home and not pause: while not stop_event.is_set(): - await async_auto_clean(r, time = int(time)) - await async_go_home(r) + await async_clean(r, mode) + if time: + await stepper(time) stop_event.set() @@ -118,12 +166,16 @@ async def async_main( stop_event.set() - if not r.go_home and not pause: - await async_go_home(r) + if time and not pause and not go_home and not status: + if rtb: + await async_go_home(r) + elif not rtb: + await async_pause(r) if r._connected: await r.async_disconnect() + return def main(*args, **kwargs): @@ -154,18 +206,25 @@ def main(*args, **kwargs): parser = argparse.ArgumentParser(description="Control a Robovac device.") - parser.add_argument('-c', '--config', help="Path to config file", default=early_args.config) - parser.add_argument('--device_id', help="Device ID", default=None if early_args.device_id else defaults.get('device_id')) - parser.add_argument('--ip', help="Device IP address", default=None if early_args.ip else defaults.get('ip')) - parser.add_argument('--local_code', help="Secret key obtained from eufy", default=None if early_args.local_code else defaults.get('local_code')) - parser.add_argument('--time', '-t', type=int, default=DEFAULT_TIME, help="Cleaning time in minutes") - parser.add_argument('--home', '-b', action='store_true', dest="go_home", default=False, help="Go home") - parser.add_argument('--pause','-p', action='store_true', dest="pause", default=False, help="Pause vacuum") - # parser.add_argument('--debug','-d', action='store_true', dest="debug", default=False, help="Enter debugging mode (won't send commands to vacuum)") - parser.add_argument('--verbose','-v', action='store_true', dest="verbose", default=False, help="Enable verbose logs") - parser.add_argument('--quiet','-q', action='store_true', dest="quiet", default=False, help="Quiet logs") + parser.add_argument('-c', '--config',help="Path to config file",default=early_args.config) + parser.add_argument('--device_id',help="Device ID",default=None if early_args.device_id else defaults.get('device_id')) + parser.add_argument('--ip',help="Device IP address",default=None if early_args.ip else defaults.get('ip')) + parser.add_argument('--local_code',help="Secret key obtained from eufy",default=None if early_args.local_code else defaults.get('local_code')) + parser.add_argument('--time', '-t',type=int,help="Cleaning time in minutes",default=DEFAULT_TIME) + parser.add_argument('--home', '-b',action='store_true',help="Go home",default=False,dest="go_home") + parser.add_argument('--pause','-p',action='store_true',help="Pause vacuum",default=False,dest="pause") + parser.add_argument('--mode', '-m',dest="mode",default="",help = "Options: default, max, edge, spot, quiet") + parser.add_argument('--verbose','-v',action='store_true',dest="verbose",default=False,help="Enable verbose logs") + parser.add_argument('--quiet','-q',action='store_true',dest="quiet",default=False,help="Quiet logs") + parser.add_argument('--status','-s',action='store_true',dest="status",default=False,help="Print status") + parser.add_argument('--return','-r',action='store_true',dest="rtb",default=False,help="Return to base upon completion (requires -t)") + + args = parser.parse_args() + if args.rtb and not args.time: + parser.error("Return to base may only be set if a duration is specified.") + if args.quiet and args.verbose: parser.error("Cannot set quiet and verbose mode simultaneously.") @@ -174,22 +233,48 @@ def main(*args, **kwargs): elif args.quiet: logging.basicConfig(level=logging.CRITICAL) sys.stdout = open(os.devnull, 'w') + elif args.status: + logging.basicConfig(level=logging.CRITICAL) else: logging.basicConfig(level=logging.INFO) - if not use_config: print("Configuration skipped") + if args.mode != "" and (args.pause or args.go_home): + print("Mode will be overridden.") + args.mode = None + elif args.mode == "" and (args.pause or args.go_home): + args.mode = None + elif args.mode.lower() == "" and not (args.pause or args.go_home): + args.mode = Modes.DEFAULT + elif args.mode.lower() == "default": + args.mode = Modes.DEFAULT + elif args.mode.lower() == "max": + args.mode = Modes.MAX + elif args.mode.lower() == "spot": + args.mode = Modes.SPOT + elif args.mode.lower() == "edge": + args.mode = Modes.EDGE + elif args.mode.lower() == "quiet": + args.mode = Modes.QUIET + else: + parser.error(f'Invalid mode selection') + + + if not use_config: + print("Configuration skipped") missing = [key for key in ['device_id', 'ip', 'local_code'] if getattr(args, key) is None] + + if missing: parser.error(f"Missing required argument(s): {', '.join(missing)}") try: - asyncio.run(async_main(args.device_id, args.ip, args.local_code, args.time, args.go_home,args.pause)) + asyncio.run(async_main(args.device_id, args.ip, args.local_code, args.time, args.go_home,args.pause, args.mode, args.status,args.rtb)) except Exception as e: - if args.debug or args.verbose: + if args.verbose: print(e) else: print("An error occured.")