forked from github.com/eufy_robovac
Compare commits
1 Commits
e08db60e0c
...
v1.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
504edf23e9
|
5
.gitignore
vendored
Normal file
5
.gitignore
vendored
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
build
|
||||||
|
dist
|
||||||
|
__pycache__
|
||||||
|
*.lock
|
||||||
|
*.spec
|
||||||
5
Dockerfile
Normal file
5
Dockerfile
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
FROM ubuntu:latest
|
||||||
|
ARG DEBIAN_FRONTEND=noninteractive
|
||||||
|
RUN apt update && apt upgrade -y
|
||||||
|
RUN apt-get -qq install python3.12-full pipenv -y
|
||||||
|
|
||||||
13
LICENSE
13
LICENSE
@@ -1,13 +0,0 @@
|
|||||||
Copyright 2019 Richard Mitchell
|
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
you may not use this file except in compliance with the License.
|
|
||||||
You may obtain a copy of the License at
|
|
||||||
|
|
||||||
https://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
|
|
||||||
Unless required by applicable law or agreed to in writing, software
|
|
||||||
distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
See the License for the specific language governing permissions and
|
|
||||||
limitations under the License.
|
|
||||||
13
Pipfile
Normal file
13
Pipfile
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
[[source]]
|
||||||
|
url = "https://pypi.org/simple"
|
||||||
|
verify_ssl = true
|
||||||
|
name = "pypi"
|
||||||
|
|
||||||
|
[packages]
|
||||||
|
cryptograpy = "*"
|
||||||
|
pyinstaller = "*"
|
||||||
|
|
||||||
|
[dev-packages]
|
||||||
|
|
||||||
|
[requires]
|
||||||
|
python_version = "3.12"
|
||||||
44
README.md
44
README.md
@@ -1,44 +0,0 @@
|
|||||||
# Eufy Robovac control for Python
|
|
||||||
|
|
||||||
Work in progress!
|
|
||||||
|
|
||||||
## Installation
|
|
||||||
Pre-requisites:
|
|
||||||
* openssl (used for encryption)
|
|
||||||
|
|
||||||
```
|
|
||||||
git clone https://github.com/mitchellrj/eufy_robovac.git
|
|
||||||
cd eufy_robovac
|
|
||||||
python3 -m venv .
|
|
||||||
bin/pip install -e .
|
|
||||||
```
|
|
||||||
|
|
||||||
## Demo usage
|
|
||||||
```
|
|
||||||
bin/demo DEVICE_ID IP LOCAL_KEY
|
|
||||||
```
|
|
||||||
|
|
||||||
The demo:
|
|
||||||
* connects to your device,
|
|
||||||
* prints its state out,
|
|
||||||
* starts cleaning,
|
|
||||||
* waits 30 seconds,
|
|
||||||
* sends the device home,
|
|
||||||
* waits 10 seconds,
|
|
||||||
* disconnects & exits
|
|
||||||
|
|
||||||
## Home Assistant integration
|
|
||||||
|
|
||||||
**EXPERIMENTAL!**
|
|
||||||
|
|
||||||
Copy the contents of the `eufy_robovac` folder to `custom_components/eufy_vacuum` in your home assistant configuration directory. Then add the following to your configuration file:
|
|
||||||
|
|
||||||
```
|
|
||||||
eufy_vacuum:
|
|
||||||
devices:
|
|
||||||
- name: Robovac
|
|
||||||
address: 192.168.1.80
|
|
||||||
access_token: YOUR LOCAL KEY HERE
|
|
||||||
id: YOUR DEVICE ID HERE
|
|
||||||
type: T2118
|
|
||||||
```
|
|
||||||
8
compile
Executable file
8
compile
Executable file
@@ -0,0 +1,8 @@
|
|||||||
|
#!/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"
|
||||||
@@ -1,58 +0,0 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
|
|
||||||
# Copyright 2019 Richard Mitchell
|
|
||||||
#
|
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
# you may not use this file except in compliance with the License.
|
|
||||||
# You may obtain a copy of the License at
|
|
||||||
#
|
|
||||||
# https://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
#
|
|
||||||
# Unless required by applicable law or agreed to in writing, software
|
|
||||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
# See the License for the specific language governing permissions and
|
|
||||||
# limitations under the License.
|
|
||||||
|
|
||||||
import asyncio
|
|
||||||
import logging
|
|
||||||
import pprint
|
|
||||||
import sys
|
|
||||||
|
|
||||||
from eufy_robovac.robovac import Robovac
|
|
||||||
|
|
||||||
|
|
||||||
logging.basicConfig(level=logging.DEBUG)
|
|
||||||
|
|
||||||
|
|
||||||
async def connected_callback(message, device):
|
|
||||||
print("Connected. Current device state:")
|
|
||||||
pprint.pprint(device.state)
|
|
||||||
|
|
||||||
|
|
||||||
async def cleaning_started_callback(message, device):
|
|
||||||
print("Cleaning started.")
|
|
||||||
|
|
||||||
|
|
||||||
async def async_main(device_id, ip, local_key=None, *args, **kwargs):
|
|
||||||
r = Robovac(device_id, ip, local_key, *args, **kwargs)
|
|
||||||
await r.async_connect(connected_callback)
|
|
||||||
await asyncio.sleep(1)
|
|
||||||
print("Starting cleaning...")
|
|
||||||
await r.async_start_cleaning(cleaning_started_callback)
|
|
||||||
await asyncio.sleep(5)
|
|
||||||
print("Pausing...")
|
|
||||||
r.play_pause = False
|
|
||||||
await asyncio.sleep(1)
|
|
||||||
print("Disconnecting...")
|
|
||||||
await r.async_disconnect()
|
|
||||||
|
|
||||||
|
|
||||||
def main(*args, **kwargs):
|
|
||||||
if not args:
|
|
||||||
args = sys.argv[1:]
|
|
||||||
asyncio.run(async_main(*args, **kwargs))
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
main(*sys.argv[1:])
|
|
||||||
@@ -1,45 +0,0 @@
|
|||||||
### HA support
|
|
||||||
|
|
||||||
"""Support for Eufy devices."""
|
|
||||||
import logging
|
|
||||||
|
|
||||||
import voluptuous as vol
|
|
||||||
|
|
||||||
from homeassistant.const import (
|
|
||||||
CONF_ACCESS_TOKEN, CONF_ADDRESS, CONF_DEVICES, CONF_NAME,
|
|
||||||
CONF_ID, CONF_TYPE)
|
|
||||||
from homeassistant.helpers import discovery
|
|
||||||
import homeassistant.helpers.config_validation as cv
|
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
DOMAIN = 'eufy_vacuum'
|
|
||||||
|
|
||||||
DEVICE_SCHEMA = vol.Schema({
|
|
||||||
vol.Required(CONF_ADDRESS): cv.string,
|
|
||||||
vol.Optional(CONF_ACCESS_TOKEN): cv.string,
|
|
||||||
vol.Required(CONF_ID): cv.string,
|
|
||||||
vol.Required(CONF_TYPE): cv.string,
|
|
||||||
vol.Optional(CONF_NAME): cv.string
|
|
||||||
})
|
|
||||||
|
|
||||||
CONFIG_SCHEMA = vol.Schema({
|
|
||||||
DOMAIN: vol.Schema({
|
|
||||||
vol.Optional(CONF_DEVICES, default=[]):
|
|
||||||
vol.All(cv.ensure_list, [DEVICE_SCHEMA]),
|
|
||||||
}),
|
|
||||||
}, extra=vol.ALLOW_EXTRA)
|
|
||||||
|
|
||||||
|
|
||||||
def setup(hass, config):
|
|
||||||
"""Set up Eufy devices."""
|
|
||||||
for device_info in config.get(DOMAIN, {}).get(CONF_DEVICES, []):
|
|
||||||
device = {}
|
|
||||||
device['address'] = device_info[CONF_ADDRESS]
|
|
||||||
device['local_key'] = device_info.get(CONF_ACCESS_TOKEN)
|
|
||||||
device['device_id'] = device_info[CONF_ID]
|
|
||||||
device['name'] = device_info.get(CONF_NAME)
|
|
||||||
device['model'] = device_info[CONF_TYPE]
|
|
||||||
discovery.load_platform(hass, 'vacuum', DOMAIN, device, config)
|
|
||||||
|
|
||||||
return True
|
|
||||||
@@ -1,181 +0,0 @@
|
|||||||
"""Support for Eufy vacuum cleaners."""
|
|
||||||
import logging
|
|
||||||
|
|
||||||
from homeassistant.components.vacuum import (
|
|
||||||
PLATFORM_SCHEMA,
|
|
||||||
STATE_CLEANING, STATE_DOCKED, STATE_IDLE, STATE_PAUSED, STATE_RETURNING,
|
|
||||||
STATE_ERROR,
|
|
||||||
SUPPORT_BATTERY, SUPPORT_CLEAN_SPOT, SUPPORT_FAN_SPEED, SUPPORT_LOCATE,
|
|
||||||
SUPPORT_PAUSE, SUPPORT_RETURN_HOME, SUPPORT_STATUS, SUPPORT_START,
|
|
||||||
SUPPORT_TURN_ON, SUPPORT_TURN_OFF,
|
|
||||||
VacuumEntity)
|
|
||||||
|
|
||||||
|
|
||||||
from . import robovac
|
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
FAN_SPEED_OFF = 'Off'
|
|
||||||
FAN_SPEED_STANDARD = 'Standard'
|
|
||||||
FAN_SPEED_BOOST_IQ = 'Boost IQ'
|
|
||||||
FAN_SPEED_MAX = 'Max'
|
|
||||||
FAN_SPEEDS = {
|
|
||||||
robovac.CleanSpeed.NO_SUCTION: FAN_SPEED_OFF,
|
|
||||||
robovac.CleanSpeed.STANDARD: FAN_SPEED_STANDARD,
|
|
||||||
robovac.CleanSpeed.BOOST_IQ: FAN_SPEED_BOOST_IQ,
|
|
||||||
robovac.CleanSpeed.MAX: FAN_SPEED_MAX,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
SUPPORT_ROBOVAC_T2118 = (
|
|
||||||
SUPPORT_BATTERY | SUPPORT_CLEAN_SPOT | SUPPORT_FAN_SPEED | SUPPORT_LOCATE |
|
|
||||||
SUPPORT_PAUSE | SUPPORT_RETURN_HOME | SUPPORT_START | SUPPORT_STATUS |
|
|
||||||
SUPPORT_TURN_OFF | SUPPORT_TURN_ON
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
MODEL_CONFIG = {
|
|
||||||
'T2118': {
|
|
||||||
'fan_speeds': FAN_SPEEDS,
|
|
||||||
'support': SUPPORT_ROBOVAC_T2118
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def setup_platform(hass, config, add_entities, device_config=None):
|
|
||||||
"""Set up Eufy vacuum cleaners."""
|
|
||||||
if device_config is None:
|
|
||||||
return
|
|
||||||
add_entities([EufyVacuum(device_config)], True)
|
|
||||||
|
|
||||||
|
|
||||||
class EufyVacuum(VacuumEntity):
|
|
||||||
"""Representation of a Eufy vacuum cleaner."""
|
|
||||||
|
|
||||||
def __init__(self, device_config):
|
|
||||||
"""Initialize the light."""
|
|
||||||
|
|
||||||
try:
|
|
||||||
self._config = MODEL_CONFIG[device_config['model'].upper()]
|
|
||||||
except KeyError:
|
|
||||||
raise RuntimeError("Unsupported model {}".format(
|
|
||||||
device_config['model']))
|
|
||||||
|
|
||||||
self._fan_speed_reverse_mapping = {
|
|
||||||
v: k for k, v in self._config['fan_speeds'].items()}
|
|
||||||
self._device_id = device_config['device_id']
|
|
||||||
self.robovac = robovac.Robovac(
|
|
||||||
device_config['device_id'], device_config['address'],
|
|
||||||
device_config['local_key'])
|
|
||||||
self._name = device_config['name']
|
|
||||||
|
|
||||||
async def async_update(self):
|
|
||||||
"""Synchronise state from the vacuum."""
|
|
||||||
await self.robovac.async_get()
|
|
||||||
|
|
||||||
@property
|
|
||||||
def unique_id(self):
|
|
||||||
"""Return the ID of this vacuum."""
|
|
||||||
return self._device_id
|
|
||||||
|
|
||||||
@property
|
|
||||||
def name(self):
|
|
||||||
"""Return the name of the device if any."""
|
|
||||||
return self._name
|
|
||||||
|
|
||||||
@property
|
|
||||||
def is_on(self):
|
|
||||||
"""Return true if device is on."""
|
|
||||||
return self.robovac.work_status == robovac.WorkStatus.RUNNING
|
|
||||||
|
|
||||||
@property
|
|
||||||
def supported_features(self):
|
|
||||||
"""Flag vacuum cleaner robot features that are supported."""
|
|
||||||
return self._config['support']
|
|
||||||
|
|
||||||
@property
|
|
||||||
def fan_speed(self):
|
|
||||||
"""Return the fan speed of the vacuum cleaner."""
|
|
||||||
return self._config['fan_speeds'].get(
|
|
||||||
self.robovac.clean_speed, FAN_SPEED_OFF)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def fan_speed_list(self):
|
|
||||||
"""Get the list of available fan speed steps of the vacuum cleaner."""
|
|
||||||
return list(self._config['fan_speeds'].values())
|
|
||||||
|
|
||||||
@property
|
|
||||||
def battery_level(self):
|
|
||||||
"""Return the battery level of the vacuum cleaner."""
|
|
||||||
return self.robovac.battery_level
|
|
||||||
|
|
||||||
@property
|
|
||||||
def status(self):
|
|
||||||
"""Return the status of the vacuum cleaner."""
|
|
||||||
if self.robovac.error_code != robovac.ErrorCode.NO_ERROR:
|
|
||||||
return STATE_ERROR
|
|
||||||
elif self.robovac.go_home:
|
|
||||||
return STATE_RETURNING
|
|
||||||
elif self.robovac.work_status == robovac.WorkStatus.RUNNING:
|
|
||||||
return STATE_CLEANING
|
|
||||||
elif self.robovac.work_status == robovac.WorkStatus.CHARGING:
|
|
||||||
return STATE_DOCKED
|
|
||||||
elif self.robovac.work_status == robovac.WorkStatus.RECHARGE_NEEDED:
|
|
||||||
# Should be captured by `go_home` above, but just in case
|
|
||||||
return STATE_RETURNING
|
|
||||||
elif self.robovac.work_status == robovac.WorkStatus.SLEEPING:
|
|
||||||
return STATE_IDLE
|
|
||||||
elif self.robovac.work_status == robovac.WorkStatus.STAND_BY:
|
|
||||||
return STATE_IDLE
|
|
||||||
elif self.robovac.work_status == robovac.WorkStatus.COMPLETED:
|
|
||||||
return STATE_DOCKED
|
|
||||||
|
|
||||||
@property
|
|
||||||
def available(self) -> bool:
|
|
||||||
"""Return True if entity is available."""
|
|
||||||
return True
|
|
||||||
|
|
||||||
async def async_return_to_base(self, **kwargs):
|
|
||||||
"""Set the vacuum cleaner to return to the dock."""
|
|
||||||
await self.robovac.async_go_home()
|
|
||||||
|
|
||||||
async def async_clean_spot(self, **kwargs):
|
|
||||||
"""Perform a spot clean-up."""
|
|
||||||
await self.robovac.async_set_work_mode(robovac.WorkMode.SPOT)
|
|
||||||
|
|
||||||
async def async_locate(self, **kwargs):
|
|
||||||
"""Locate the vacuum cleaner."""
|
|
||||||
await self.robovac.async_find_robot()
|
|
||||||
|
|
||||||
async def async_set_fan_speed(self, fan_speed, **kwargs):
|
|
||||||
"""Set fan speed."""
|
|
||||||
clean_speed = self._fan_speed_reverse_mapping[fan_speed]
|
|
||||||
await self.robovac.async_set_clean_speed(clean_speed)
|
|
||||||
|
|
||||||
async def async_turn_on(self, **kwargs):
|
|
||||||
"""Turn the vacuum on."""
|
|
||||||
await self.robovac.async_set_work_mode(robovac.WorkMode.AUTO)
|
|
||||||
|
|
||||||
async def async_turn_off(self, **kwargs):
|
|
||||||
"""Turn the vacuum off and return to home."""
|
|
||||||
await self.async_return_to_base()
|
|
||||||
|
|
||||||
async def async_start(self, **kwargs):
|
|
||||||
"""Resume the cleaning cycle."""
|
|
||||||
await self.async_turn_on()
|
|
||||||
|
|
||||||
async def async_resume(self, **kwargs):
|
|
||||||
"""Resume the cleaning cycle."""
|
|
||||||
await self.robovac.async_play()
|
|
||||||
|
|
||||||
async def async_pause(self, **kwargs):
|
|
||||||
"""Pause the cleaning cycle."""
|
|
||||||
await self.robovac.async_pause()
|
|
||||||
|
|
||||||
async def async_start_pause(self, **kwargs):
|
|
||||||
"""Pause the cleaning task or resume it."""
|
|
||||||
if self.robovac.play_pause:
|
|
||||||
await self.async_pause()
|
|
||||||
else:
|
|
||||||
await self.async_play()
|
|
||||||
48
setup.py
48
setup.py
@@ -1,48 +0,0 @@
|
|||||||
#!/usr/bin/env python
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
|
|
||||||
# Copyright 2019 Richard Mitchell
|
|
||||||
#
|
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
# you may not use this file except in compliance with the License.
|
|
||||||
# You may obtain a copy of the License at
|
|
||||||
#
|
|
||||||
# https://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
#
|
|
||||||
# Unless required by applicable law or agreed to in writing, software
|
|
||||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
# See the License for the specific language governing permissions and
|
|
||||||
# limitations under the License.
|
|
||||||
|
|
||||||
|
|
||||||
import re
|
|
||||||
from setuptools import setup, find_packages
|
|
||||||
import sys
|
|
||||||
import warnings
|
|
||||||
|
|
||||||
dynamic_requires = []
|
|
||||||
|
|
||||||
setup(
|
|
||||||
name='eufy_robovac',
|
|
||||||
version="0.0",
|
|
||||||
author='Richard Mitchell',
|
|
||||||
author_email='eufy-robovac@mitch.org.uk',
|
|
||||||
url='http://github.com/mitchellrj/eufy_robovac',
|
|
||||||
packages=find_packages(),
|
|
||||||
scripts=[],
|
|
||||||
description='Python API for controlling Eufy robotic vacuum cleaners',
|
|
||||||
classifiers=[
|
|
||||||
'Development Status :: 4 - Beta',
|
|
||||||
'Intended Audience :: Developers',
|
|
||||||
'License :: OSI Approved :: Apache Software License',
|
|
||||||
'Operating System :: OS Independent',
|
|
||||||
'Programming Language :: Python',
|
|
||||||
],
|
|
||||||
install_requires=[
|
|
||||||
'cryptography'
|
|
||||||
],
|
|
||||||
entry_points = {
|
|
||||||
'console_scripts': ['demo=eufy_robovac.demo:main'],
|
|
||||||
}
|
|
||||||
)
|
|
||||||
@@ -14,9 +14,4 @@
|
|||||||
# See the License for the specific language governing permissions and
|
# See the License for the specific language governing permissions and
|
||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
|
|
||||||
from .robovac import Robovac
|
from .robovac import Robovac
|
||||||
|
|
||||||
try:
|
|
||||||
from .platform import *
|
|
||||||
except ImportError:
|
|
||||||
pass
|
|
||||||
@@ -15,6 +15,7 @@
|
|||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
import asyncio
|
||||||
|
|
||||||
from .property import DeviceProperty, StringEnum
|
from .property import DeviceProperty, StringEnum
|
||||||
from .tuya import TuyaDevice
|
from .tuya import TuyaDevice
|
||||||
@@ -86,6 +87,7 @@ class Robovac(TuyaDevice):
|
|||||||
BATTERY_LEVEL = '104'
|
BATTERY_LEVEL = '104'
|
||||||
ERROR_CODE = '106'
|
ERROR_CODE = '106'
|
||||||
|
|
||||||
|
|
||||||
power = DeviceProperty(POWER)
|
power = DeviceProperty(POWER)
|
||||||
play_pause = DeviceProperty(PLAY_PAUSE)
|
play_pause = DeviceProperty(PLAY_PAUSE)
|
||||||
direction = DeviceProperty(DIRECTION)
|
direction = DeviceProperty(DIRECTION)
|
||||||
@@ -117,3 +119,6 @@ class Robovac(TuyaDevice):
|
|||||||
|
|
||||||
async def async_set_clean_speed(self, clean_speed, callback=None):
|
async def async_set_clean_speed(self, clean_speed, callback=None):
|
||||||
await self.async_set({self.CLEAN_SPEED: str(clean_speed)}, callback)
|
await self.async_set({self.CLEAN_SPEED: str(clean_speed)}, callback)
|
||||||
|
|
||||||
|
async def async_move(self, direction, callback=None):
|
||||||
|
await self.async_set({self.DIRECTION: str(direction)}, callback)
|
||||||
187
src/vac.py
Executable file
187
src/vac.py
Executable file
@@ -0,0 +1,187 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
import asyncio
|
||||||
|
import signal
|
||||||
|
import logging
|
||||||
|
import pprint
|
||||||
|
import sys
|
||||||
|
import argparse
|
||||||
|
from enum import Enum
|
||||||
|
import os
|
||||||
|
|
||||||
|
|
||||||
|
from robovac.robovac import Robovac
|
||||||
|
|
||||||
|
|
||||||
|
stop_event = asyncio.Event()
|
||||||
|
|
||||||
|
|
||||||
|
def signal_handler():
|
||||||
|
print(f'\nExiting...')
|
||||||
|
stop_event.set()
|
||||||
|
|
||||||
|
|
||||||
|
def parse_robovac_config(path="/home/freyja/Desktop/robovac.conf"):
|
||||||
|
config = {}
|
||||||
|
|
||||||
|
|
||||||
|
if not os.path.exists(path):
|
||||||
|
raise FileNotFoundError(f"Config file not found at {path}")
|
||||||
|
|
||||||
|
|
||||||
|
with open(path, 'r') as f:
|
||||||
|
for line in f:
|
||||||
|
line = line.strip()
|
||||||
|
if line and not line.startswith("#"): # Skip empty lines and comments
|
||||||
|
if '=' in line:
|
||||||
|
key, value = map(str.strip, line.split('=', 1))
|
||||||
|
config[key] = value
|
||||||
|
|
||||||
|
|
||||||
|
required_keys = ['device_id', 'ip', 'local_code']
|
||||||
|
if not all(key in config for key in required_keys):
|
||||||
|
missing = [key for key in required_keys if key not in config]
|
||||||
|
raise ValueError(f"Missing keys in config: {', '.join(missing)}")
|
||||||
|
|
||||||
|
|
||||||
|
class credentials(Enum):
|
||||||
|
device_id = config['device_id']
|
||||||
|
ip = config['ip']
|
||||||
|
local_code = config['local_code']
|
||||||
|
|
||||||
|
|
||||||
|
return credentials
|
||||||
|
|
||||||
|
|
||||||
|
async def callback(message,device):
|
||||||
|
print(message)
|
||||||
|
pprint.pprint(device.state)
|
||||||
|
|
||||||
|
|
||||||
|
async def stepper(time):
|
||||||
|
total_seconds = time * 60
|
||||||
|
step = 1
|
||||||
|
elapsed = 0
|
||||||
|
while elapsed < total_seconds and not stop_event.is_set():
|
||||||
|
await asyncio.sleep(step)
|
||||||
|
elapsed += step
|
||||||
|
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_go_home(r: Robovac):
|
||||||
|
await r.async_go_home(callback)
|
||||||
|
|
||||||
|
|
||||||
|
async def async_pause(r: Robovac):
|
||||||
|
await r.async_pause(callback)
|
||||||
|
|
||||||
|
|
||||||
|
async def async_main(device_id,ip,local_code,time,go_home,debug,pause):
|
||||||
|
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 asyncio.sleep(2)
|
||||||
|
|
||||||
|
|
||||||
|
if debug:
|
||||||
|
while not stop_event.is_set():
|
||||||
|
if not go_home:
|
||||||
|
print("Auto cleaning")
|
||||||
|
await stepper(time)
|
||||||
|
if go_home:
|
||||||
|
print("Go home")
|
||||||
|
if pause:
|
||||||
|
print("Pause")
|
||||||
|
else:
|
||||||
|
while not stop_event.is_set():
|
||||||
|
if not go_home:
|
||||||
|
await async_auto_clean(r, time = int(time))
|
||||||
|
await async_go_home(r)
|
||||||
|
if go_home:
|
||||||
|
await async_go_home(r)
|
||||||
|
if pause:
|
||||||
|
await async_pause(r)
|
||||||
|
|
||||||
|
|
||||||
|
if not debug:
|
||||||
|
if stop_event.is_set() and not r.go_home and not pause: await async_go_home(r)
|
||||||
|
if r._connected: await r.async_disconnect()
|
||||||
|
|
||||||
|
|
||||||
|
def main(*args, **kwargs):
|
||||||
|
early_parser = argparse.ArgumentParser(add_help=False)
|
||||||
|
early_parser.add_argument('-c', '--config', help="Path to config file", default="/etc/robovac.conf")
|
||||||
|
early_parser.add_argument('--device_id')
|
||||||
|
early_parser.add_argument('--ip')
|
||||||
|
early_parser.add_argument('--local_code')
|
||||||
|
|
||||||
|
|
||||||
|
early_args, remaining_args = early_parser.parse_known_args()
|
||||||
|
|
||||||
|
|
||||||
|
use_config = not (early_args.device_id or early_args.ip or early_args.local_code)
|
||||||
|
|
||||||
|
|
||||||
|
defaults = {}
|
||||||
|
if use_config:
|
||||||
|
try:
|
||||||
|
creds = parse_robovac_config(early_args.config)
|
||||||
|
defaults = {
|
||||||
|
"device_id": creds.device_id.value,
|
||||||
|
"ip": creds.ip.value,
|
||||||
|
"local_code": creds.local_code.value
|
||||||
|
}
|
||||||
|
except Exception:
|
||||||
|
defaults = {}
|
||||||
|
|
||||||
|
|
||||||
|
if not use_config:
|
||||||
|
print("Configuration skipped")
|
||||||
|
|
||||||
|
|
||||||
|
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=20, help="Cleaning time in minutes")
|
||||||
|
parser.add_argument('--pause','-p', action='store_true', dest="pause", default=False, help="Pause vacuum")
|
||||||
|
parser.add_argument('--home', '-b', action='store_true', dest="go_home", default=False, help="Go home")
|
||||||
|
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")
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
|
||||||
|
if args.quiet and args.verbose:
|
||||||
|
parser.error("Cannot set quiet and verbose mode simultaneously.")
|
||||||
|
elif args.verbose:
|
||||||
|
logging.basicConfig(level=logging.DEBUG)
|
||||||
|
elif args.quiet:
|
||||||
|
logging.basicConfig(level=logging.CRITICAL)
|
||||||
|
else:
|
||||||
|
logging.basicConfig(level=logging.INFO)
|
||||||
|
|
||||||
|
|
||||||
|
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.debug,args.pause))
|
||||||
|
except Exception as e:
|
||||||
|
if args.debug or args.verbose:
|
||||||
|
print(e)
|
||||||
|
else:
|
||||||
|
print("An error occured.")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main(*sys.argv[1:])
|
||||||
Reference in New Issue
Block a user