initial commit

This commit is contained in:
Zach Graham 2024-09-19 21:51:39 -05:00
parent 5266322dfa
commit 24e7b0e228
65 changed files with 11550 additions and 1 deletions

5
CONTRIBUTION.md Normal file
View File

@ -0,0 +1,5 @@
# Contributions
Contributions are welcome. If you use HA and a Eufy Vacuum and want to make improvmenets, get in touch.

13
LICENSE Normal file
View File

@ -0,0 +1,13 @@
Copyright 2022 Brendan McCluskey
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.

266
README.md
View File

@ -1,2 +1,266 @@
# eufy-robovac-hass [![hacs_badge](https://img.shields.io/badge/HACS-Custom-orange.svg?style=for-the-badge)](https://github.com/custom-components/hacs)
[![Sponsor me on Github Sponsors](https://img.shields.io/badge/Sponsor-ea4aaa?style=for-the-badge&logo=github-sponsors&logoColor=%23EA4AAA&labelColor=white)](https://github.com/sponsors/CodeFoodPixels)
[![Tip me through ko-fi](https://img.shields.io/badge/KoFi-FF5E5B?style=for-the-badge&logo=kofi&logoColor=%23FF5E5B&labelColor=white)](https://ko-fi.com/O5O3O08PA)
[![Tip me through PayPal](https://img.shields.io/badge/Paypal.me-00457C?style=for-the-badge&logo=paypal&logoColor=%2300457C&labelColor=white)](https://paypal.me/CodeFoodPixels)
[![Tip me through Monzo](https://img.shields.io/badge/Monzo.me-14233C?style=for-the-badge&logo=monzo&logoColor=%2314233C&labelColor=white)](https://monzo.me/codefoodpixels)
# Eufy RobovVac control for Home Assistant
A brand new version Eufy RoboVac integration for Home Assistant that includes a Config Flow to add your RoboVac(s) and the local key and ID required. All you need to do is enter your Eufy app credentials and the Config Flow will look up the details for you. After the initial config use the configuration button on the Integration to enter the RoboVac IP address when prompted.
This work has evovled from the original work by Richard Mitchell https://github.com/mitchellrj and the countless others who have contributed over the last couple of years. It also builds on the work done by Andre Borie https://gitlab.com/Rjevski/eufy-device-id-and-local-key-grabber to get the required local ID and key.
This project has been forked many times since the I am building upon the original work done by Richard and attempting to simplfy the operation and number of files involved.
## Installation ##
Couple of Pre-reqs
1. Make sure your Home Assistant Core is up to date
2. Remove any previous Eufy or RoboVac installation including entries in the configuration.yaml
If you want you can clone this repo manually, oterwise use HACS (Recommended).
### Using HACS
1. In HACS add this repo as an integration additional repository.
2. Then install it.
3. Restart Home Assistant
4. Go to the Integrations Page and Click +Add Integration button
5. Search for Eufy Robovac and select it
6. Enter your Eufy username and password (The ones you use to login to the add with) and submit
7. If youve done it correctly you should get a success dialoge and option to enter an Area for each RoboVac you have
8. Click Finish
9. On the Integrations Screen Locate your Eufy Robovac card and click the configure button
10. Select the Radio button beside the Vacuum name and type its IP addess in the box and press Submit
(You need to repeat steps 9 and 10 for each RoboVac you have)
11. Enjoy
Please note: You may have to get a new version of the access key for your vacuum from time to time if Eufy change it. Worst case you have to Delete the integration and re add it to get the new key.
### Optional 1: Scripts
The integration is designed to work with the standard Home Assistant Lovelace card but that doesnt support all the options of your Robovac. I have created some scripts to send the relevant commands to the Robovac.
Add the below text to your scripts.yaml file for a xxC RoboVAC. It should be in the same folder as your configuration.yaml
```
15c_smallroomclean:
alias: 15C_smallRoomClean
sequence:
- service: vacuum.send_command
data:
command: smallRoomClean
target:
entity_id: vacuum.15c
mode: single
15c_edgeclean:
alias: 15C_edgeClean
sequence:
- service: vacuum.send_command
data:
command: edgeClean
target:
entity_id: vacuum.15c
mode: single
15c_dock:
alias: 15C_dock
sequence:
- service: vacuum.return_to_base
target:
entity_id: vacuum.15c
mode: single
```
If you have a Gxx add this to your scripts.yaml
```
g30_autoclean:
alias: G30_autoClean
sequence:
- service: vacuum.send_command
data:
command: autoClean
target:
entity_id: vacuum.g30
mode: single
g30_autoreturn:
alias: G30_autoReturn
sequence:
- service: vacuum.send_command
data:
command: autoReturn
target:
entity_id: vacuum.g30
mode: single
g30_donotdisturb:
alias: G30_do_Not_Disturb
sequence:
- service: vacuum.send_command
data:
command: doNotDisturb
target:
entity_id: vacuum.g30
mode: single
g30_dock:
alias: G30_dock
sequence:
- service: vacuum.return_to_base
target:
entity_id: vacuum.g30
mode: single
```
If you have an X8 add this to your scripts.yaml
```
x8_boostiq:
alias: x8_boostIQ
sequence:
- service: vacuum.send_command
data:
command: boostIQ
target:
entity_id: vacuum.x8
mode: single
x8_autoclean:
alias: x8_autoClean
sequence:
- service: vacuum.send_command
data:
command: autoClean
target:
entity_id: vacuum.x8
mode: single
x8_autoreturn:
alias: X8_autoReturn
sequence:
- service: vacuum.send_command
data:
command: autoReturn
target:
entity_id: vacuum.x8
mode: single
x8_donotdisturb:
alias: X8_do_Not_Disturb
sequence:
- service: vacuum.send_command
data:
command: doNotDisturb
target:
entity_id: vacuum.x8
mode: single
x8_dock:
alias: X8_dock
sequence:
- service: vacuum.return_to_base
target:
entity_id: vacuum.x8
mode: single
```
The facilities in the script options above only work on the those model series. i.e. You cant do edge cleaning on the G30 and you cant do the autoreturn on the 15C.
### Optional 2 : Lovelace Card
Search in HACS for the Vacuum Card by Denys Dovhan and install it and configure it in lovelace to use you vacuum. Note there is a minor "feature" in the vacuum card where it doesnt show the correct values in toolbar when they update and there is a template adjusting what is being displayed. A screen refresh shows the correct vaules. Hopefully this will be fixed soon.
Edit the lovelace vaccum card and add the following to the cards yaml if you have a xxC.
```
type: custom:vacuum-card
entity: vacuum.15c
image: default
show_name: true
show_status: true
show_toolbar: true
shortcuts:
- name: Dock
service: script.15c_dock
icon: mdi:home-map-marker
- name: Edge Cleaning
service: script.15c_edgeclean
icon: mdi:square-outline
- name: Small Room
service: script.15c_smallroomclean
icon: mdi:timer-cog-outline
```
Again if you have the Gxx you will add these lines to the cards yaml.
```
type: custom:vacuum-card
entity: vacuum.g30
image: default
shortcuts:
- name: Dock
service: script.g30_dock
icon: mdi:home-map-marker
- name: Auto Clean
service: script.g30_autoclean
icon: mdi:caps-lock
- name: Auto Return
service: script.g30_autoreturn
icon: mdi:arrow-u-down-left-bold
- name: Do Not Disturb
service: script.g30_donotdisturb
icon: mdi:volume-off
stats:
default:
- attribute: cleaning_area
unit: sq meters
subtitle: Cleaning Area
- attribute: cleaning_time
value_template: '{{ (value | float(0) / 60) | round(1) }}'
unit: minutes
subtitle: Cleaning time
- attribute: auto_return
subtitle: Auto Ret
value_template: '{% if (value == true) %}On{% else %}Off{% endif %}'
- attribute: do_not_disturb
subtitle: Dnd
value_template: '{% if (value == true) %}On{% else %}Off{% endif %}'
```
Again if you have the X8 you will add these lines to the cards yaml.
```
type: custom:vacuum-card
entity: vacuum.x8
image: default
stats:
default:
- attribute: cleaning_area
unit: sq meters
subtitle: Cleaning Area
- attribute: cleaning_time
value_template: '{{ (value | float(0) / 60) | round(1) }}'
unit: minutes
subtitle: Cleaning time
- attribute: boost_iq
subtitle: Boost IQ
value_template: '{% if (value == true) %}On{% else %}Off{% endif %}'
- attribute: auto_return
subtitle: Auto Ret
value_template: '{% if (value == true) %}On{% else %}Off{% endif %}'
- attribute: do_not_disturb
subtitle: Dnd
value_template: '{% if (value == true) %}On{% else %}Off{% endif %}'
shortcuts:
- name: Dock
service: script.x8_dock
icon: mdi:home-map-marker
- name: Auto Clean
service: script.x8_autoclean
icon: mdi:caps-lock
- name: Boost IQ
service: script.x8_boostiq
icon: mdi:bootstrap
- name: Auto Return
service: script.x8_autoreturn
icon: mdi:arrow-u-down-left-bold
- name: Do Not Disturb
service: script.x8_donotdisturb
icon: mdi:volume-off
```
## Debugging ##
I have left quite a few debug statements in the code and they may be useful to see whats happening by looking in the System Log files. The Log Viewer Addon available in the Home Assistance store can be very useful to watch the logs being updated in real time. To get the debugging to add to the logs you need to add the below text to your configuration.yaml
```
logger:
default: warning
logs:
custom_components.robovac.vacuum: debug
custom_components.robovac.tuyalocalapi: debug
```
---

View File

@ -0,0 +1,97 @@
# Copyright 2022 Brendan McCluskey
#
# 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.
"""The Eufy Robovac integration."""
from __future__ import annotations
import logging
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import EVENT_HOMEASSISTANT_STOP, Platform, CONF_IP_ADDRESS
from homeassistant.core import HomeAssistant
from .const import CONF_VACS, DOMAIN
from .tuyalocaldiscovery import TuyaLocalDiscovery
PLATFORMS = [Platform.VACUUM, Platform.SENSOR]
_LOGGER = logging.getLogger(__name__)
async def async_setup(hass, entry) -> bool:
hass.data.setdefault(DOMAIN, {CONF_VACS:{}})
async def update_device(device):
entry = async_get_config_entry_for_device(hass, device["gwId"])
if entry == None:
return
if not entry.state.recoverable:
return
hass_data = entry.data.copy()
if (
device["gwId"] in hass_data[CONF_VACS]
and device.get("ip") is not None
and hass_data[CONF_VACS][device["gwId"]].get("autodiscovery", True)
):
if hass_data[CONF_VACS][device["gwId"]][CONF_IP_ADDRESS] != device["ip"]:
hass_data[CONF_VACS][device["gwId"]][CONF_IP_ADDRESS] = device["ip"]
hass.config_entries.async_update_entry(entry, data=hass_data)
await hass.config_entries.async_reload(entry.entry_id)
_LOGGER.debug(
"Updated ip address of {} to {}".format(
device["gwId"], device["ip"]
)
)
tuyalocaldiscovery = TuyaLocalDiscovery(update_device)
try:
await tuyalocaldiscovery.start()
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, tuyalocaldiscovery.close)
except Exception:
_LOGGER.exception("failed to set up discovery")
return True
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up Eufy Robovac from a config entry."""
entry.async_on_unload(entry.add_update_listener(update_listener))
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
return True
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Unload a config entry."""
if unload_ok := await hass.config_entries.async_unload_platforms(
entry, PLATFORMS
):
"""Nothing"""
return unload_ok
async def update_listener(hass, entry):
"""Handle options update."""
await hass.config_entries.async_reload(entry.entry_id)
def async_get_config_entry_for_device(hass, device_id):
current_entries = hass.config_entries.async_entries(DOMAIN)
for entry in current_entries:
if device_id in entry.data[CONF_VACS]:
return entry
return None

View File

@ -0,0 +1,281 @@
# Copyright 2022 Brendan McCluskey
#
# 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.
"""Config flow for Eufy Robovac integration."""
from __future__ import annotations
import logging
from typing import Any, Optional
from copy import deepcopy
import voluptuous as vol
import homeassistant.helpers.config_validation as cv
from homeassistant import config_entries
from homeassistant.core import callback
from homeassistant.core import HomeAssistant
from homeassistant.data_entry_flow import FlowResult
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.selector import selector
from homeassistant.const import (
CONF_ACCESS_TOKEN,
CONF_NAME,
CONF_ID,
CONF_MODEL,
CONF_USERNAME,
CONF_PASSWORD,
CONF_IP_ADDRESS,
CONF_DESCRIPTION,
CONF_MAC,
CONF_CLIENT_ID,
CONF_REGION,
CONF_TIME_ZONE,
CONF_COUNTRY_CODE,
)
from .countries import (
get_phone_code_by_country_code,
get_phone_code_by_region,
get_region_by_country_code,
get_region_by_phone_code,
)
from .const import CONF_AUTODISCOVERY, DOMAIN, CONF_VACS
from .tuyawebapi import TuyaAPISession
from .eufywebapi import EufyLogon
_LOGGER = logging.getLogger(__name__)
USER_SCHEMA = vol.Schema(
{
vol.Required(CONF_USERNAME): cv.string,
vol.Required(CONF_PASSWORD): cv.string,
}
)
def get_eufy_vacuums(self):
"""Login to Eufy and get the vacuum details"""
eufy_session = EufyLogon(self["username"], self["password"])
response = eufy_session.get_user_info()
if response.status_code != 200:
raise CannotConnect
user_response = response.json()
if user_response["res_code"] != 1:
raise InvalidAuth
response = eufy_session.get_device_info(
user_response["user_info"]["request_host"],
user_response["user_info"]["id"],
user_response["access_token"],
)
device_response = response.json()
response = eufy_session.get_user_settings(
user_response["user_info"]["request_host"],
user_response["user_info"]["id"],
user_response["access_token"],
)
settings_response = response.json()
self[CONF_CLIENT_ID] = user_response["user_info"]["id"]
if (
"tuya_home" in settings_response["setting"]["home_setting"]
and "tuya_region_code"
in settings_response["setting"]["home_setting"]["tuya_home"]
):
self[CONF_REGION] = settings_response["setting"]["home_setting"]["tuya_home"][
"tuya_region_code"
]
if user_response["user_info"]["phone_code"]:
self[CONF_COUNTRY_CODE] = user_response["user_info"]["phone_code"]
else:
self[CONF_COUNTRY_CODE] = get_phone_code_by_region(self[CONF_REGION])
elif user_response["user_info"]["phone_code"]:
self[CONF_REGION] = get_region_by_phone_code(
user_response["user_info"]["phone_code"]
)
self[CONF_COUNTRY_CODE] = user_response["user_info"]["phone_code"]
elif user_response["user_info"]["country"]:
self[CONF_REGION] = get_region_by_country_code(
user_response["user_info"]["country"]
)
self[CONF_COUNTRY_CODE] = get_phone_code_by_country_code(
user_response["user_info"]["country"]
)
else:
self[CONF_REGION] = "EU"
self[CONF_COUNTRY_CODE] = "44"
self[CONF_TIME_ZONE] = user_response["user_info"]["timezone"]
tuya_client = TuyaAPISession(
username="eh-" + self[CONF_CLIENT_ID],
region=self[CONF_REGION],
timezone=self[CONF_TIME_ZONE],
phone_code=self[CONF_COUNTRY_CODE],
)
items = device_response["devices"]
self[CONF_VACS] = {}
for item in items:
if item["product"]["appliance"] == "Cleaning":
try:
device = tuya_client.get_device(item["id"])
_LOGGER.debug("Robovac schema: {}".format(device["schema"]))
vac_details = {
CONF_ID: item["id"],
CONF_MODEL: item["product"]["product_code"],
CONF_NAME: item["alias_name"],
CONF_DESCRIPTION: item["name"],
CONF_MAC: item["wifi"]["mac"],
CONF_IP_ADDRESS: "",
CONF_AUTODISCOVERY: True,
CONF_ACCESS_TOKEN: device["localKey"],
}
self[CONF_VACS][item["id"]] = vac_details
except:
_LOGGER.debug(
"Vacuum {} found on Eufy, but not on Tuya. Skipping.".format(
item["id"]
)
)
return response
async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> dict[str, Any]:
"""Validate the user input allows us to connect."""
await hass.async_add_executor_job(get_eufy_vacuums, data)
return data
class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
"""Handle a config flow for Eufy Robovac."""
data: Optional[dict[str, Any]]
async def async_step_user(
self, user_input: dict[str, Any] | None = None
) -> FlowResult:
"""Handle the initial step."""
if user_input is None:
return self.async_show_form(step_id="user", data_schema=USER_SCHEMA)
errors = {}
try:
unique_id = user_input[CONF_USERNAME]
valid_data = await validate_input(self.hass, user_input)
except CannotConnect:
errors["base"] = "cannot_connect"
except InvalidAuth:
errors["base"] = "invalid_auth"
except Exception as e: # pylint: disable=broad-except
_LOGGER.exception("Unexpected exception: {}".format(e))
errors["base"] = "unknown"
else:
await self.async_set_unique_id(unique_id)
self._abort_if_unique_id_configured()
# return await self.async_step_repo(valid_data)
return self.async_create_entry(title=unique_id, data=valid_data)
return self.async_show_form(
step_id="user", data_schema=USER_SCHEMA, errors=errors
)
@staticmethod
@callback
def async_get_options_flow(config_entry):
"""Get the options flow for this handler."""
return OptionsFlowHandler(config_entry)
class CannotConnect(HomeAssistantError):
"""Error to indicate we cannot connect."""
class InvalidAuth(HomeAssistantError):
"""Error to indicate there is invalid auth."""
class OptionsFlowHandler(config_entries.OptionsFlow):
"""Handles options flow for the component."""
def __init__(self, config_entry: config_entries.ConfigEntry) -> None:
self.config_entry = config_entry
self.selected_vacuum = None
async def async_step_init(self, user_input=None):
errors = {}
if user_input is not None:
self.selected_vacuum = user_input["selected_vacuum"]
return await self.async_step_edit()
vacuums_config = self.config_entry.data[CONF_VACS]
vacuum_list = {}
for vacuum_id in vacuums_config:
vacuum_list[vacuum_id] = vacuums_config[vacuum_id]["name"]
devices_schema = vol.Schema(
{vol.Required("selected_vacuum"): vol.In(vacuum_list)}
)
return self.async_show_form(
step_id="init", data_schema=devices_schema, errors=errors
)
async def async_step_edit(self, user_input=None):
"""Manage the options for the custom component."""
errors = {}
vacuums = self.config_entry.data[CONF_VACS]
if user_input is not None:
updated_vacuums = deepcopy(vacuums)
updated_vacuums[self.selected_vacuum][CONF_AUTODISCOVERY] = user_input[
CONF_AUTODISCOVERY
]
if user_input[CONF_IP_ADDRESS]:
updated_vacuums[self.selected_vacuum][CONF_IP_ADDRESS] = user_input[
CONF_IP_ADDRESS
]
self.hass.config_entries.async_update_entry(
self.config_entry,
data={CONF_VACS: updated_vacuums},
)
return self.async_create_entry(title="", data={})
options_schema = vol.Schema(
{
vol.Required(
CONF_AUTODISCOVERY,
default=vacuums[self.selected_vacuum].get(CONF_AUTODISCOVERY, True),
): bool,
vol.Optional(
CONF_IP_ADDRESS,
default=vacuums[self.selected_vacuum].get(CONF_IP_ADDRESS),
): str,
}
)
return self.async_show_form(
step_id="edit", data_schema=options_schema, errors=errors
)

View File

@ -0,0 +1,8 @@
"""Constants for the Eufy Robovac integration."""
DOMAIN = "robovac"
CONF_VACS = "vacuums"
CONF_AUTODISCOVERY = "autodiscovery"
REFRESH_RATE = 60
PING_RATE = 10
TIMEOUT = 5

View File

@ -0,0 +1,228 @@
COUNTRIES = [
{"country_code": "AF", "phone_code": "93", "tuya_region": "EU"},
{"country_code": "AL", "phone_code": "355", "tuya_region": "EU"},
{"country_code": "DZ", "phone_code": "213", "tuya_region": "EU"},
{"country_code": "AO", "phone_code": "244", "tuya_region": "EU"},
{"country_code": "AR", "phone_code": "54", "tuya_region": "AZ"},
{"country_code": "AM", "phone_code": "374", "tuya_region": "EU"},
{"country_code": "AU", "phone_code": "61", "tuya_region": "AZ"},
{"country_code": "AT", "phone_code": "43", "tuya_region": "EU"},
{"country_code": "AZ", "phone_code": "994", "tuya_region": "EU"},
{"country_code": "BH", "phone_code": "973", "tuya_region": "EU"},
{"country_code": "BD", "phone_code": "880", "tuya_region": "EU"},
{"country_code": "BY", "phone_code": "375", "tuya_region": "EU"},
{"country_code": "BE", "phone_code": "32", "tuya_region": "EU"},
{"country_code": "BZ", "phone_code": "501", "tuya_region": "EU"},
{"country_code": "BJ", "phone_code": "229", "tuya_region": "EU"},
{"country_code": "BT", "phone_code": "975", "tuya_region": "EU"},
{"country_code": "BO", "phone_code": "591", "tuya_region": "AZ"},
{"country_code": "BA", "phone_code": "387", "tuya_region": "EU"},
{"country_code": "BW", "phone_code": "267", "tuya_region": "EU"},
{"country_code": "BR", "phone_code": "55", "tuya_region": "AZ"},
{"country_code": "VG", "phone_code": "1284", "tuya_region": "EU"},
{"country_code": "BN", "phone_code": "673", "tuya_region": "EU"},
{"country_code": "BG", "phone_code": "359", "tuya_region": "EU"},
{"country_code": "BF", "phone_code": "226", "tuya_region": "EU"},
{"country_code": "BI", "phone_code": "257", "tuya_region": "EU"},
{"country_code": "KH", "phone_code": "855", "tuya_region": "EU"},
{"country_code": "CM", "phone_code": "237", "tuya_region": "EU"},
{"country_code": "US", "phone_code": "1", "tuya_region": "AZ"},
{"country_code": "CA", "phone_code": "1", "tuya_region": "AZ"},
{"country_code": "CV", "phone_code": "238", "tuya_region": "EU"},
{"country_code": "KY", "phone_code": "1345", "tuya_region": "EU"},
{"country_code": "CF", "phone_code": "236", "tuya_region": "EU"},
{"country_code": "TD", "phone_code": "235", "tuya_region": "EU"},
{"country_code": "CL", "phone_code": "56", "tuya_region": "AZ"},
{"country_code": "CN", "phone_code": "86", "tuya_region": "AY"},
{"country_code": "CO", "phone_code": "57", "tuya_region": "AZ"},
{"country_code": "KM", "phone_code": "269", "tuya_region": "EU"},
{"country_code": "CG", "phone_code": "242", "tuya_region": "EU"},
{"country_code": "CD", "phone_code": "243", "tuya_region": "EU"},
{"country_code": "CR", "phone_code": "506", "tuya_region": "EU"},
{"country_code": "HR", "phone_code": "385", "tuya_region": "EU"},
{"country_code": "CY", "phone_code": "357", "tuya_region": "EU"},
{"country_code": "CZ", "phone_code": "420", "tuya_region": "EU"},
{"country_code": "DK", "phone_code": "45", "tuya_region": "EU"},
{"country_code": "DJ", "phone_code": "253", "tuya_region": "EU"},
{"country_code": "DO", "phone_code": "1809", "tuya_region": "EU"},
{"country_code": "DO", "phone_code": "1829", "tuya_region": "EU"},
{"country_code": "DO", "phone_code": "1849", "tuya_region": "EU"},
{"country_code": "EC", "phone_code": "593", "tuya_region": "AZ"},
{"country_code": "EG", "phone_code": "20", "tuya_region": "EU"},
{"country_code": "SV", "phone_code": "503", "tuya_region": "EU"},
{"country_code": "GQ", "phone_code": "240", "tuya_region": "EU"},
{"country_code": "ER", "phone_code": "291", "tuya_region": "EU"},
{"country_code": "EE", "phone_code": "372", "tuya_region": "EU"},
{"country_code": "ET", "phone_code": "251", "tuya_region": "EU"},
{"country_code": "FJ", "phone_code": "679", "tuya_region": "EU"},
{"country_code": "FI", "phone_code": "358", "tuya_region": "EU"},
{"country_code": "FR", "phone_code": "33", "tuya_region": "EU"},
{"country_code": "GA", "phone_code": "241", "tuya_region": "EU"},
{"country_code": "GM", "phone_code": "220", "tuya_region": "EU"},
{"country_code": "GE", "phone_code": "995", "tuya_region": "EU"},
{"country_code": "DE", "phone_code": "49", "tuya_region": "EU"},
{"country_code": "GH", "phone_code": "233", "tuya_region": "EU"},
{"country_code": "GR", "phone_code": "30", "tuya_region": "EU"},
{"country_code": "GL", "phone_code": "299", "tuya_region": "EU"},
{"country_code": "GT", "phone_code": "502", "tuya_region": "AZ"},
{"country_code": "GN", "phone_code": "224", "tuya_region": "EU"},
{"country_code": "GY", "phone_code": "592", "tuya_region": "EU"},
{"country_code": "HT", "phone_code": "509", "tuya_region": "EU"},
{"country_code": "HN", "phone_code": "504", "tuya_region": "EU"},
{"country_code": "HK", "phone_code": "852", "tuya_region": "AZ"},
{"country_code": "HU", "phone_code": "36", "tuya_region": "EU"},
{"country_code": "IS", "phone_code": "354", "tuya_region": "EU"},
{"country_code": "IN", "phone_code": "91", "tuya_region": "IN"},
{"country_code": "ID", "phone_code": "62", "tuya_region": "AZ"},
{"country_code": "IR", "phone_code": "98", "tuya_region": "EU"},
{"country_code": "IQ", "phone_code": "964", "tuya_region": "EU"},
{"country_code": "IE", "phone_code": "353", "tuya_region": "EU"},
{"country_code": "IM", "phone_code": "44", "tuya_region": "EU"},
{"country_code": "IL", "phone_code": "972", "tuya_region": "EU"},
{"country_code": "IT", "phone_code": "39", "tuya_region": "AZ"},
{"country_code": "CI", "phone_code": "225", "tuya_region": "EU"},
{"country_code": "JM", "phone_code": "1876", "tuya_region": "EU"},
{"country_code": "JP", "phone_code": "81", "tuya_region": "AZ"},
{"country_code": "JO", "phone_code": "962", "tuya_region": "EU"},
{"country_code": "KZ", "phone_code": "7", "tuya_region": "EU"},
{"country_code": "KE", "phone_code": "254", "tuya_region": "EU"},
{"country_code": "KR", "phone_code": "82", "tuya_region": "AZ"},
{"country_code": "KW", "phone_code": "965", "tuya_region": "EU"},
{"country_code": "KG", "phone_code": "996", "tuya_region": "EU"},
{"country_code": "LA", "phone_code": "856", "tuya_region": "EU"},
{"country_code": "LV", "phone_code": "371", "tuya_region": "EU"},
{"country_code": "LB", "phone_code": "961", "tuya_region": "EU"},
{"country_code": "LS", "phone_code": "266", "tuya_region": "EU"},
{"country_code": "LR", "phone_code": "231", "tuya_region": "EU"},
{"country_code": "LY", "phone_code": "218", "tuya_region": "EU"},
{"country_code": "LT", "phone_code": "370", "tuya_region": "EU"},
{"country_code": "LU", "phone_code": "352", "tuya_region": "EU"},
{"country_code": "MO", "phone_code": "853", "tuya_region": "AZ"},
{"country_code": "MK", "phone_code": "389", "tuya_region": "EU"},
{"country_code": "MG", "phone_code": "261", "tuya_region": "EU"},
{"country_code": "MW", "phone_code": "265", "tuya_region": "EU"},
{"country_code": "MY", "phone_code": "60", "tuya_region": "AZ"},
{"country_code": "MV", "phone_code": "960", "tuya_region": "EU"},
{"country_code": "ML", "phone_code": "223", "tuya_region": "EU"},
{"country_code": "MT", "phone_code": "356", "tuya_region": "EU"},
{"country_code": "MR", "phone_code": "222", "tuya_region": "EU"},
{"country_code": "MU", "phone_code": "230", "tuya_region": "EU"},
{"country_code": "MX", "phone_code": "52", "tuya_region": "AZ"},
{"country_code": "MD", "phone_code": "373", "tuya_region": "EU"},
{"country_code": "MC", "phone_code": "377", "tuya_region": "EU"},
{"country_code": "MN", "phone_code": "976", "tuya_region": "EU"},
{"country_code": "ME", "phone_code": "382", "tuya_region": "EU"},
{"country_code": "MA", "phone_code": "212", "tuya_region": "EU"},
{"country_code": "MZ", "phone_code": "258", "tuya_region": "EU"},
{"country_code": "MM", "phone_code": "95", "tuya_region": "AZ"},
{"country_code": "NA", "phone_code": "264", "tuya_region": "EU"},
{"country_code": "NP", "phone_code": "977", "tuya_region": "EU"},
{"country_code": "NL", "phone_code": "31", "tuya_region": "EU"},
{"country_code": "NZ", "phone_code": "64", "tuya_region": "AZ"},
{"country_code": "NI", "phone_code": "505", "tuya_region": "AZ"},
{"country_code": "NE", "phone_code": "227", "tuya_region": "EU"},
{"country_code": "NG", "phone_code": "234", "tuya_region": "EU"},
{"country_code": "KP", "phone_code": "850", "tuya_region": "EU"},
{"country_code": "NO", "phone_code": "47", "tuya_region": "EU"},
{"country_code": "OM", "phone_code": "968", "tuya_region": "EU"},
{"country_code": "PK", "phone_code": "92", "tuya_region": "EU"},
{"country_code": "PA", "phone_code": "507", "tuya_region": "EU"},
{"country_code": "PY", "phone_code": "595", "tuya_region": "AZ"},
{"country_code": "PE", "phone_code": "51", "tuya_region": "AZ"},
{"country_code": "PH", "phone_code": "63", "tuya_region": "AZ"},
{"country_code": "PL", "phone_code": "48", "tuya_region": "EU"},
{"country_code": "PF", "phone_code": "689", "tuya_region": "EU"},
{"country_code": "PT", "phone_code": "351", "tuya_region": "EU"},
{"country_code": "PR", "phone_code": "1787", "tuya_region": "AZ"},
{"country_code": "QA", "phone_code": "974", "tuya_region": "EU"},
{"country_code": "RE", "phone_code": "262", "tuya_region": "EU"},
{"country_code": "RO", "phone_code": "40", "tuya_region": "EU"},
{"country_code": "RU", "phone_code": "7", "tuya_region": "EU"},
{"country_code": "RW", "phone_code": "250", "tuya_region": "EU"},
{"country_code": "SM", "phone_code": "378", "tuya_region": "EU"},
{"country_code": "SA", "phone_code": "966", "tuya_region": "EU"},
{"country_code": "SN", "phone_code": "221", "tuya_region": "EU"},
{"country_code": "RS", "phone_code": "381", "tuya_region": "EU"},
{"country_code": "SL", "phone_code": "232", "tuya_region": "EU"},
{"country_code": "SG", "phone_code": "65", "tuya_region": "EU"},
{"country_code": "SK", "phone_code": "421", "tuya_region": "EU"},
{"country_code": "SI", "phone_code": "386", "tuya_region": "EU"},
{"country_code": "SO", "phone_code": "252", "tuya_region": "EU"},
{"country_code": "ZA", "phone_code": "27", "tuya_region": "EU"},
{"country_code": "ES", "phone_code": "34", "tuya_region": "EU"},
{"country_code": "LK", "phone_code": "94", "tuya_region": "EU"},
{"country_code": "SD", "phone_code": "249", "tuya_region": "EU"},
{"country_code": "SR", "phone_code": "597", "tuya_region": "AZ"},
{"country_code": "SZ", "phone_code": "268", "tuya_region": "EU"},
{"country_code": "SE", "phone_code": "46", "tuya_region": "EU"},
{"country_code": "CH", "phone_code": "41", "tuya_region": "EU"},
{"country_code": "SY", "phone_code": "963", "tuya_region": "EU"},
{"country_code": "TW", "phone_code": "886", "tuya_region": "AZ"},
{"country_code": "TJ", "phone_code": "992", "tuya_region": "EU"},
{"country_code": "TZ", "phone_code": "255", "tuya_region": "EU"},
{"country_code": "TH", "phone_code": "66", "tuya_region": "AZ"},
{"country_code": "TG", "phone_code": "228", "tuya_region": "EU"},
{"country_code": "TO", "phone_code": "676", "tuya_region": "EU"},
{"country_code": "TT", "phone_code": "1868", "tuya_region": "EU"},
{"country_code": "TN", "phone_code": "216", "tuya_region": "EU"},
{"country_code": "TR", "phone_code": "90", "tuya_region": "EU"},
{"country_code": "TM", "phone_code": "993", "tuya_region": "EU"},
{"country_code": "VI", "phone_code": "1340", "tuya_region": "EU"},
{"country_code": "UG", "phone_code": "256", "tuya_region": "EU"},
{"country_code": "UA", "phone_code": "380", "tuya_region": "EU"},
{"country_code": "AE", "phone_code": "971", "tuya_region": "EU"},
{"country_code": "GB", "phone_code": "44", "tuya_region": "EU"},
{"country_code": "UY", "phone_code": "598", "tuya_region": "AZ"},
{"country_code": "UZ", "phone_code": "998", "tuya_region": "EU"},
{"country_code": "VA", "phone_code": "379", "tuya_region": "EU"},
{"country_code": "VE", "phone_code": "58", "tuya_region": "AZ"},
{"country_code": "VN", "phone_code": "84", "tuya_region": "AZ"},
{"country_code": "YE", "phone_code": "967", "tuya_region": "EU"},
{"country_code": "ZR", "phone_code": "243", "tuya_region": "EU"},
{"country_code": "ZM", "phone_code": "260", "tuya_region": "EU"},
{"country_code": "ZW", "phone_code": "263", "tuya_region": "EU"},
{"country_code": "NCL", "phone_code": "687", "tuya_region": "EU"},
{"country_code": "MQ", "phone_code": "596", "tuya_region": "EU"},
]
def get_region_by_country_code(country_code):
country = next(
(item for item in COUNTRIES if item["country_code"] == country_code), None
)
if country is None:
return "EU"
return country["tuya_region"]
def get_region_by_phone_code(phone_code):
country = next(
(item for item in COUNTRIES if item["phone_code"] == phone_code), None
)
if country is None:
return "EU"
return country["tuya_region"]
def get_phone_code_by_region(region):
country = next((item for item in COUNTRIES if item["tuya_region"] == region), None)
if country is None:
return "44"
return country["phone_code"]
def get_phone_code_by_country_code(country_code):
country = next(
(item for item in COUNTRIES if item["country_code"] == country_code), None
)
if country is None:
return "44"
return country["phone_code"]

View File

@ -0,0 +1,40 @@
ERROR_MESSAGES = {
"IP_ADDRESS": "IP Address not set",
"CONNECTION_FAILED": "Connection to the vacuum failed",
"UNSUPPORTED_MODEL": "This model is not supported",
"no_error": "None",
1:"Front bumper stuck",
2:"Wheel stuck",
3:"Side brush",
4:"Rolling brush bar stuck",
5:"Device trapped",
6:"Device trapped",
7:"Wheel suspended",
8:"Low battery",
9:"Magnetic boundary",
12:"Right wall sensor",
13:"Device tilted",
14:"Insert dust collector",
17:"Restricted area detected",
18:"Laser cover stuck",
19:"Laser sesor stuck",
20:"Laser sensor blocked",
21:"Base blocked",
"S1":"Battery",
"S2":"Wheel Module",
"S3":"Side Brush",
"S4":"Suction Fan",
"S5":"Rolling Brush",
"S8":"Path Tracking Sensor",
"Wheel_stuck":"Wheel stuck",
"R_brush_stuck":"Rolling brush stuck",
"Crash_bar_stuck":"Front bumper stuck",
"sensor_dirty":"Sensor dirty",
"N_enough_pow":"Low battery",
"Stuck_5_min":"Device trapped",
"Fan_stuck":"Fan stuck",
"S_brush_stuck":"Side brush stuck",
}
def getErrorMessage(code):
return ERROR_MESSAGES.get(code, code)

View File

@ -0,0 +1,45 @@
"""Original Work from here: Andre Borie https://gitlab.com/Rjevski/eufy-device-id-and-local-key-grabber"""
import requests
eufyheaders = {
"User-Agent": "EufyHome-Android-2.4.0",
"timezone": "Europe/London",
"category": "Home",
"token": "",
"uid": "",
"openudid": "sdk_gphone64_arm64",
"clientType": "2",
"language": "en",
"country": "US",
"Accept-Encoding": "gzip",
}
class EufyLogon:
def __init__(self, username, password):
self.username = username
self.password = password
def get_user_info(self):
login_url = "https://home-api.eufylife.com/v1/user/email/login"
login_auth = {
"client_Secret": "GQCpr9dSp3uQpsOMgJ4xQ",
"client_id": "eufyhome-app",
"email": self.username,
"password": self.password,
}
return requests.post(login_url, json=login_auth, headers=eufyheaders)
def get_user_settings(self, url, userid, token):
setting_url = url + "/v1/user/setting"
eufyheaders["token"] = token
eufyheaders["id"] = userid
return requests.request("GET", setting_url, headers=eufyheaders, timeout=1.5)
def get_device_info(self, url, userid, token):
device_url = url + "/v1/device/v2"
eufyheaders["token"] = token
eufyheaders["id"] = userid
return requests.request("GET", device_url, headers=eufyheaders)

View File

@ -0,0 +1,13 @@
{
"domain": "robovac",
"name": "Eufy Robovac",
"codeowners": ["@codefoodpixels"],
"config_flow": true,
"dependencies": [],
"documentation": "https://github.com/codefoodpixels/robovac",
"integration_type": "device",
"iot_class": "local_polling",
"issue_tracker": "https://github.com/codefoodpixels/robovac/issues",
"requirements": [],
"version": "1.0.0"
}

View File

@ -0,0 +1,45 @@
from .vacuums.base import RobovacCommand
from .tuyalocalapi import TuyaDevice
from .vacuums import ROBOVAC_MODELS
class ModelNotSupportedException(Exception):
"""This model is not supported"""
class RoboVac(TuyaDevice):
""""""
def __init__(self, model_code, *args, **kwargs):
if model_code not in ROBOVAC_MODELS:
raise ModelNotSupportedException(
"Model {} is not supported".format(model_code)
)
self.model_details = ROBOVAC_MODELS[model_code]
super().__init__(self.model_details, *args, **kwargs)
def getHomeAssistantFeatures(self):
return self.model_details.homeassistant_features
def getRoboVacFeatures(self):
return self.model_details.robovac_features
def getFanSpeeds(self):
return self.model_details.commands[RobovacCommand.FAN_SPEED]["values"]
def getModes(self):
return self.model_details.commands[RobovacCommand.MODE]["values"]
def getSupportedCommands(self):
return list(self.model_details.commands.keys())
def getCommandCodes(self):
command_codes = {}
for key, value in self.model_details.commands.items():
if isinstance(value, dict):
command_codes[key] = str(value["code"])
else:
command_codes[key] = str(value)
return command_codes

View File

@ -0,0 +1,63 @@
import logging
from datetime import timedelta
from homeassistant.components.sensor import SensorDeviceClass, SensorEntity
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import PERCENTAGE, EntityCategory, CONF_NAME, CONF_ID
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.device_registry import DeviceInfo
from .const import CONF_VACS, DOMAIN, REFRESH_RATE
_LOGGER = logging.getLogger(__name__)
BATTERY = "Battery"
SCAN_INTERVAL = timedelta(seconds=REFRESH_RATE)
async def async_setup_entry(
hass: HomeAssistant,
config_entry: ConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Initialize my test integration 2 config entry."""
vacuums = config_entry.data[CONF_VACS]
for item in vacuums:
item = vacuums[item]
entity = RobovacSensorEntity(item)
async_add_entities([entity])
class RobovacSensorEntity(SensorEntity):
_attr_has_entity_name = True
_attr_device_class = SensorDeviceClass.BATTERY
_attr_entity_category = EntityCategory.DIAGNOSTIC
_attr_native_unit_of_measurement = PERCENTAGE
_attr_available = False
def __init__(self, item):
self.robovac = item
self.robovac_id = item[CONF_ID]
self._attr_unique_id = item[CONF_ID]
self._battery_level = None
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, item[CONF_ID])},
name=item[CONF_NAME]
)
def update(self):
try:
self._battery_level = self.hass.data[DOMAIN][CONF_VACS][self.robovac_id].battery_level
self._attr_available = True
except:
_LOGGER.debug("Failed to get battery level for {}".format(self.robovac_id))
self._battery_level = None
self._attr_available = False
@property
def native_value(self) -> str | None:
"""Return the state."""
if self._battery_level is not None:
return self._battery_level
return None

View File

@ -0,0 +1,40 @@
{
"config": {
"abort": {
"already_configured": "Device is already configured"
},
"error": {
"cannot_connect": "Failed to connect",
"invalid_auth": "Invalid authentication",
"unknown": "Unexpected error"
},
"step": {
"user": {
"data": {
"host": "Host",
"password": "Password",
"username": "Username"
},
"description": "Enter your Eufy account details"
}
}
},
"options": {
"step": {
"init": {
"title": "Manage vacuums",
"data": {
"selected_vacuum": "Select the Vacuum to edit"
}
},
"edit": {
"title": "Edit vacuum",
"data": {
"autodiscovery": "Enable autodiscovery",
"ip_address": "IP Address"
},
"description": "Autodiscovery will automatically update the IP address"
}
}
}
}

View File

@ -0,0 +1,40 @@
{
"config": {
"abort": {
"already_configured": "Device is already configured"
},
"error": {
"cannot_connect": "Failed to connect",
"invalid_auth": "Invalid authentication",
"unknown": "Unexpected error"
},
"step": {
"user": {
"data": {
"host": "Host",
"password": "Password",
"username": "Username"
},
"description": "Enter your Eufy account details"
}
}
},
"options": {
"step": {
"init": {
"title": "Manage vacuums",
"data": {
"selected_vacuum": "Select the Vacuum to edit"
}
},
"edit": {
"title": "Edit vacuum",
"data": {
"autodiscovery": "Enable autodiscovery",
"ip_address": "IP Address"
},
"description": "Autodiscovery will automatically update the IP address"
}
}
}
}

View File

@ -0,0 +1,929 @@
# -*- 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.
# Based on portions of https://github.com/codetheweb/tuyapi/
#
# MIT License
#
# Copyright (c) 2017 Max Isom
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
import asyncio
import base64
import json
import logging
import socket
import struct
import sys
import time
import traceback
from typing import Callable, Coroutine
from cryptography.hazmat.backends.openssl import backend as openssl_backend
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
from cryptography.hazmat.primitives.hashes import Hash, MD5
from cryptography.hazmat.primitives.padding import PKCS7
from .vacuums.base import RobovacCommand
INITIAL_BACKOFF = 5
INITIAL_QUEUE_TIME = 0.1
BACKOFF_MULTIPLIER = 1.70224
_LOGGER = logging.getLogger(__name__)
MESSAGE_PREFIX_FORMAT = ">IIII"
MESSAGE_SUFFIX_FORMAT = ">II"
MAGIC_PREFIX = 0x000055AA
MAGIC_SUFFIX = 0x0000AA55
MAGIC_SUFFIX_BYTES = struct.pack(">I", MAGIC_SUFFIX)
CRC_32_TABLE = [
0x00000000,
0x77073096,
0xEE0E612C,
0x990951BA,
0x076DC419,
0x706AF48F,
0xE963A535,
0x9E6495A3,
0x0EDB8832,
0x79DCB8A4,
0xE0D5E91E,
0x97D2D988,
0x09B64C2B,
0x7EB17CBD,
0xE7B82D07,
0x90BF1D91,
0x1DB71064,
0x6AB020F2,
0xF3B97148,
0x84BE41DE,
0x1ADAD47D,
0x6DDDE4EB,
0xF4D4B551,
0x83D385C7,
0x136C9856,
0x646BA8C0,
0xFD62F97A,
0x8A65C9EC,
0x14015C4F,
0x63066CD9,
0xFA0F3D63,
0x8D080DF5,
0x3B6E20C8,
0x4C69105E,
0xD56041E4,
0xA2677172,
0x3C03E4D1,
0x4B04D447,
0xD20D85FD,
0xA50AB56B,
0x35B5A8FA,
0x42B2986C,
0xDBBBC9D6,
0xACBCF940,
0x32D86CE3,
0x45DF5C75,
0xDCD60DCF,
0xABD13D59,
0x26D930AC,
0x51DE003A,
0xC8D75180,
0xBFD06116,
0x21B4F4B5,
0x56B3C423,
0xCFBA9599,
0xB8BDA50F,
0x2802B89E,
0x5F058808,
0xC60CD9B2,
0xB10BE924,
0x2F6F7C87,
0x58684C11,
0xC1611DAB,
0xB6662D3D,
0x76DC4190,
0x01DB7106,
0x98D220BC,
0xEFD5102A,
0x71B18589,
0x06B6B51F,
0x9FBFE4A5,
0xE8B8D433,
0x7807C9A2,
0x0F00F934,
0x9609A88E,
0xE10E9818,
0x7F6A0DBB,
0x086D3D2D,
0x91646C97,
0xE6635C01,
0x6B6B51F4,
0x1C6C6162,
0x856530D8,
0xF262004E,
0x6C0695ED,
0x1B01A57B,
0x8208F4C1,
0xF50FC457,
0x65B0D9C6,
0x12B7E950,
0x8BBEB8EA,
0xFCB9887C,
0x62DD1DDF,
0x15DA2D49,
0x8CD37CF3,
0xFBD44C65,
0x4DB26158,
0x3AB551CE,
0xA3BC0074,
0xD4BB30E2,
0x4ADFA541,
0x3DD895D7,
0xA4D1C46D,
0xD3D6F4FB,
0x4369E96A,
0x346ED9FC,
0xAD678846,
0xDA60B8D0,
0x44042D73,
0x33031DE5,
0xAA0A4C5F,
0xDD0D7CC9,
0x5005713C,
0x270241AA,
0xBE0B1010,
0xC90C2086,
0x5768B525,
0x206F85B3,
0xB966D409,
0xCE61E49F,
0x5EDEF90E,
0x29D9C998,
0xB0D09822,
0xC7D7A8B4,
0x59B33D17,
0x2EB40D81,
0xB7BD5C3B,
0xC0BA6CAD,
0xEDB88320,
0x9ABFB3B6,
0x03B6E20C,
0x74B1D29A,
0xEAD54739,
0x9DD277AF,
0x04DB2615,
0x73DC1683,
0xE3630B12,
0x94643B84,
0x0D6D6A3E,
0x7A6A5AA8,
0xE40ECF0B,
0x9309FF9D,
0x0A00AE27,
0x7D079EB1,
0xF00F9344,
0x8708A3D2,
0x1E01F268,
0x6906C2FE,
0xF762575D,
0x806567CB,
0x196C3671,
0x6E6B06E7,
0xFED41B76,
0x89D32BE0,
0x10DA7A5A,
0x67DD4ACC,
0xF9B9DF6F,
0x8EBEEFF9,
0x17B7BE43,
0x60B08ED5,
0xD6D6A3E8,
0xA1D1937E,
0x38D8C2C4,
0x4FDFF252,
0xD1BB67F1,
0xA6BC5767,
0x3FB506DD,
0x48B2364B,
0xD80D2BDA,
0xAF0A1B4C,
0x36034AF6,
0x41047A60,
0xDF60EFC3,
0xA867DF55,
0x316E8EEF,
0x4669BE79,
0xCB61B38C,
0xBC66831A,
0x256FD2A0,
0x5268E236,
0xCC0C7795,
0xBB0B4703,
0x220216B9,
0x5505262F,
0xC5BA3BBE,
0xB2BD0B28,
0x2BB45A92,
0x5CB36A04,
0xC2D7FFA7,
0xB5D0CF31,
0x2CD99E8B,
0x5BDEAE1D,
0x9B64C2B0,
0xEC63F226,
0x756AA39C,
0x026D930A,
0x9C0906A9,
0xEB0E363F,
0x72076785,
0x05005713,
0x95BF4A82,
0xE2B87A14,
0x7BB12BAE,
0x0CB61B38,
0x92D28E9B,
0xE5D5BE0D,
0x7CDCEFB7,
0x0BDBDF21,
0x86D3D2D4,
0xF1D4E242,
0x68DDB3F8,
0x1FDA836E,
0x81BE16CD,
0xF6B9265B,
0x6FB077E1,
0x18B74777,
0x88085AE6,
0xFF0F6A70,
0x66063BCA,
0x11010B5C,
0x8F659EFF,
0xF862AE69,
0x616BFFD3,
0x166CCF45,
0xA00AE278,
0xD70DD2EE,
0x4E048354,
0x3903B3C2,
0xA7672661,
0xD06016F7,
0x4969474D,
0x3E6E77DB,
0xAED16A4A,
0xD9D65ADC,
0x40DF0B66,
0x37D83BF0,
0xA9BCAE53,
0xDEBB9EC5,
0x47B2CF7F,
0x30B5FFE9,
0xBDBDF21C,
0xCABAC28A,
0x53B39330,
0x24B4A3A6,
0xBAD03605,
0xCDD70693,
0x54DE5729,
0x23D967BF,
0xB3667A2E,
0xC4614AB8,
0x5D681B02,
0x2A6F2B94,
0xB40BBE37,
0xC30C8EA1,
0x5A05DF1B,
0x2D02EF8D,
]
class TuyaException(Exception):
"""Base for Tuya exceptions."""
class InvalidKey(TuyaException):
"""The local key is invalid."""
class InvalidMessage(TuyaException):
"""The message received is invalid."""
class MessageDecodeFailed(TuyaException):
"""The message received cannot be decoded as JSON."""
class ConnectionException(TuyaException):
"""The socket connection failed."""
class ConnectionTimeoutException(ConnectionException):
"""The socket connection timed out."""
class RequestResponseCommandMismatch(TuyaException):
"""The command in the response didn't match the one from the request."""
class ResponseTimeoutException(TuyaException):
"""Did not recieve a response to the request within the timeout"""
class BackoffException(TuyaException):
"""Backoff time not reached"""
class TuyaCipher:
"""Tuya cryptographic helpers."""
def __init__(self, key, version):
"""Initialize the cipher."""
self.version = version
self.key = key
self.cipher = Cipher(
algorithms.AES(key.encode("ascii")), modes.ECB(), backend=openssl_backend
)
def get_prefix_size_and_validate(self, command, encrypted_data):
try:
version = tuple(map(int, encrypted_data[:3].decode("utf8").split(".")))
except ValueError:
version = (0, 0)
if version != self.version:
return 0
if version < (3, 3):
hash = encrypted_data[3:19].decode("ascii")
expected_hash = self.hash(encrypted_data[19:])
if hash != expected_hash:
return 0
return 19
else:
if command in (Message.SET_COMMAND, Message.GRATUITOUS_UPDATE):
_, sequence, __, ___ = struct.unpack_from(">IIIH", encrypted_data, 3)
return 15
return 0
def decrypt(self, command, data):
prefix_size = self.get_prefix_size_and_validate(command, data)
data = data[prefix_size:]
decryptor = self.cipher.decryptor()
if self.version < (3, 3):
data = base64.b64decode(data)
decrypted_data = decryptor.update(data)
decrypted_data += decryptor.finalize()
unpadder = PKCS7(128).unpadder()
unpadded_data = unpadder.update(decrypted_data)
unpadded_data += unpadder.finalize()
return unpadded_data
def encrypt(self, command, data):
encrypted_data = b""
if data:
padder = PKCS7(128).padder()
padded_data = padder.update(data)
padded_data += padder.finalize()
encryptor = self.cipher.encryptor()
encrypted_data = encryptor.update(padded_data)
encrypted_data += encryptor.finalize()
prefix = ".".join(map(str, self.version)).encode("utf8")
if self.version < (3, 3):
payload = base64.b64encode(encrypted_data)
hash = self.hash(payload)
prefix += hash.encode("utf8")
else:
payload = encrypted_data
if command in (Message.SET_COMMAND, Message.GRATUITOUS_UPDATE):
prefix += b"\x00" * 12
else:
prefix = b""
return prefix + payload
def hash(self, data):
digest = Hash(MD5(), backend=openssl_backend)
to_hash = "data={}||lpv={}||{}".format(
data.decode("ascii"), ".".join(map(str, self.version)), self.key
)
digest.update(to_hash.encode("utf8"))
intermediate = digest.finalize().hex()
return intermediate[8:24]
def crc(data):
"""Calculate the Tuya-flavored CRC of some data."""
c = 0xFFFFFFFF
for b in data:
c = (c >> 8) ^ CRC_32_TABLE[(c ^ b) & 255]
return c ^ 0xFFFFFFFF
class Message:
PING_COMMAND = 0x09
GET_COMMAND = 0x0A
SET_COMMAND = 0x07
GRATUITOUS_UPDATE = 0x08
def __init__(
self,
command,
payload=None,
sequence=None,
encrypt=False,
device=None,
expect_response=True,
ttl=5,
):
if payload is None:
payload = b""
self.payload = payload
self.command = command
self.original_sequence = sequence
if sequence is None:
self.set_sequence()
else:
self.sequence = sequence
self.encrypt = encrypt
self.device = device
self.expiry = int(time.time()) + ttl
self.expect_response = expect_response
self.listener = None
if expect_response is True:
self.listener = asyncio.Semaphore(0)
if device is not None:
device._listeners[self.sequence] = self.listener
def __repr__(self):
return "{}({}, {!r}, {!r}, {})".format(
self.__class__.__name__,
hex(self.command),
self.payload,
self.sequence,
"<Device {}>".format(self.device) if self.device else None,
)
def set_sequence(self):
self.sequence = int(time.perf_counter() * 1000) & 0xFFFFFFFF
def hex(self):
return self.bytes().hex()
def bytes(self):
payload_data = self.payload
if isinstance(payload_data, dict):
payload_data = json.dumps(payload_data, separators=(",", ":"))
if not isinstance(payload_data, bytes):
payload_data = payload_data.encode("utf8")
if self.encrypt:
payload_data = self.device.cipher.encrypt(self.command, payload_data)
payload_size = len(payload_data) + struct.calcsize(MESSAGE_SUFFIX_FORMAT)
header = struct.pack(
MESSAGE_PREFIX_FORMAT,
MAGIC_PREFIX,
self.sequence,
self.command,
payload_size,
)
if self.device and self.device.version >= (3, 3):
checksum = crc(header + payload_data)
else:
checksum = crc(payload_data)
footer = struct.pack(MESSAGE_SUFFIX_FORMAT, checksum, MAGIC_SUFFIX)
return header + payload_data + footer
__bytes__ = bytes
async def async_send(self):
await self.device._async_send(self)
@classmethod
def from_bytes(cls, device, data, cipher=None):
try:
prefix, sequence, command, payload_size = struct.unpack_from(
MESSAGE_PREFIX_FORMAT, data
)
except struct.error as e:
raise InvalidMessage("Invalid message header format.") from e
if prefix != MAGIC_PREFIX:
raise InvalidMessage("Magic prefix missing from message.")
# check for an optional return code
header_size = struct.calcsize(MESSAGE_PREFIX_FORMAT)
try:
(return_code,) = struct.unpack_from(">I", data, header_size)
except struct.error as e:
raise InvalidMessage("Unable to unpack return code.") from e
if return_code >> 8:
payload_data = data[
header_size : header_size
+ payload_size
- struct.calcsize(MESSAGE_SUFFIX_FORMAT)
]
return_code = None
else:
payload_data = data[
header_size
+ struct.calcsize(">I") : header_size
+ payload_size
- struct.calcsize(MESSAGE_SUFFIX_FORMAT)
]
try:
expected_crc, suffix = struct.unpack_from(
MESSAGE_SUFFIX_FORMAT,
data,
header_size + payload_size - struct.calcsize(MESSAGE_SUFFIX_FORMAT),
)
except struct.error as e:
raise InvalidMessage("Invalid message suffix format.") from e
if suffix != MAGIC_SUFFIX:
raise InvalidMessage("Magic suffix missing from message")
actual_crc = crc(
data[: header_size + payload_size - struct.calcsize(MESSAGE_SUFFIX_FORMAT)]
)
if expected_crc != actual_crc:
raise InvalidMessage("CRC check failed")
payload = None
if payload_data:
try:
payload_data = cipher.decrypt(command, payload_data)
except ValueError as e:
pass
try:
payload_text = payload_data.decode("utf8")
except UnicodeDecodeError as e:
device._LOGGER.debug(payload_data.hex())
device._LOGGER.error(e)
raise MessageDecodeFailed() from e
try:
payload = json.loads(payload_text)
except json.decoder.JSONDecodeError as e:
# data may be encrypted
device._LOGGER.debug(payload_data.hex())
device._LOGGER.error(e)
raise MessageDecodeFailed() from e
return cls(command, payload, sequence)
class TuyaDevice:
"""Represents a generic Tuya device."""
def __init__(
self,
model_details,
device_id,
host,
timeout,
ping_interval,
update_entity_state,
local_key=None,
port=6668,
gateway_id=None,
version=(3, 3),
):
"""Initialize the device."""
self._LOGGER = _LOGGER.getChild(device_id)
self.model_details = model_details
self.device_id = device_id
self.host = host
self.port = port
if not gateway_id:
gateway_id = self.device_id
self.gateway_id = gateway_id
self.version = version
self.timeout = timeout
self.last_pong = 0
self.ping_interval = ping_interval
self.update_entity_state_cb = update_entity_state
if len(local_key) != 16:
raise InvalidKey("Local key should be a 16-character string")
self.cipher = TuyaCipher(local_key, self.version)
self.writer = None
self._response_task = None
self._recieve_task = None
self._ping_task = None
self._handlers: dict[int, Callable[[Message], Coroutine]] = {
Message.GRATUITOUS_UPDATE: self.async_gratuitous_update_state,
Message.PING_COMMAND: self._async_pong_received,
}
self._dps = {}
self._connected = False
self._enabled = True
self._queue = []
self._listeners = {}
self._backoff = False
self._queue_interval = INITIAL_QUEUE_TIME
self._failures = 0
asyncio.create_task(self.process_queue())
def __repr__(self):
return "{}({!r}, {!r}, {!r}, {!r})".format(
self.__class__.__name__,
self.device_id,
self.host,
self.port,
self.cipher.key,
)
def __str__(self):
return "{} ({}:{})".format(self.device_id, self.host, self.port)
async def process_queue(self):
if self._enabled is False:
return
self.clean_queue()
if len(self._queue) > 0:
self._LOGGER.debug(
"Processing queue. Current length: {}".format(len(self._queue))
)
try:
message = self._queue.pop(0)
await message.async_send()
self._failures = 0
self._queue_interval = INITIAL_QUEUE_TIME
self._backoff = False
except Exception as e:
self._failures += 1
self._LOGGER.debug(
"{} failures. Most recent: {}".format(self._failures, e)
)
if self._failures > 3:
self._backoff = True
self._queue_interval = min(
INITIAL_BACKOFF * (BACKOFF_MULTIPLIER ** (self._failures - 4)),
600,
)
self._LOGGER.warn(
"{} failures, backing off for {} seconds".format(
self._failures, self._queue_interval
)
)
await asyncio.sleep(self._queue_interval)
asyncio.create_task(self.process_queue())
def clean_queue(self):
cleaned_queue = []
now = int(time.time())
for item in self._queue:
if item.expiry > now:
cleaned_queue.append(item)
self._queue = cleaned_queue
async def async_connect(self):
if self._connected is True or self._enabled is False:
return
sock = socket.socket(family=socket.AF_INET, type=socket.SOCK_STREAM)
sock.settimeout(self.timeout)
self._LOGGER.debug("Connecting to {}".format(self))
try:
sock.connect((self.host, self.port))
except (socket.timeout, TimeoutError) as e:
self._dps[self.model_details.commands[RobovacCommand.ERROR]] = (
"CONNECTION_FAILED"
)
raise ConnectionTimeoutException("Connection timed out")
loop = asyncio.get_running_loop()
loop.create_connection
self.reader, self.writer = await asyncio.open_connection(sock=sock)
self._connected = True
if self._ping_task is None:
self._ping_task = asyncio.create_task(self.async_ping(self.ping_interval))
asyncio.create_task(self._async_handle_message())
async def async_disable(self):
self._enabled = False
await self.async_disconnect()
async def async_disconnect(self):
if self._connected is False:
return
self._LOGGER.debug("Disconnected from {}".format(self))
self._connected = False
self.last_pong = 0
if self.writer is not None:
self.writer.close()
await self.writer.wait_closed()
if self.reader is not None and not self.reader.at_eof():
self.reader.feed_eof()
async def async_get(self):
payload = {"gwId": self.gateway_id, "devId": self.device_id}
encrypt = False if self.version < (3, 3) else True
message = Message(Message.GET_COMMAND, payload, encrypt=encrypt, device=self)
self._queue.append(message)
response = await self.async_recieve(message)
await self.async_update_state(response)
async def async_set(self, dps):
t = int(time.time())
payload = {"devId": self.device_id, "uid": "", "t": t, "dps": dps}
message = Message(
Message.SET_COMMAND,
payload,
encrypt=True,
device=self,
expect_response=False,
)
self._queue.append(message)
async def async_ping(self, ping_interval):
if self._enabled is False:
return
if self._backoff is True:
self._LOGGER.debug("Currently in backoff, not adding ping to queue")
else:
self.last_ping = time.time()
encrypt = False if self.version < (3, 3) else True
message = Message(
Message.PING_COMMAND,
sequence=0,
encrypt=encrypt,
device=self,
expect_response=False,
)
self._queue.append(message)
await asyncio.sleep(ping_interval)
self._ping_task = asyncio.create_task(self.async_ping(self.ping_interval))
if self.last_pong < self.last_ping:
await self.async_disconnect()
async def _async_pong_received(self, message):
self.last_pong = time.time()
async def async_gratuitous_update_state(self, state_message):
await self.async_update_state(state_message)
await self.update_entity_state_cb()
async def async_update_state(self, state_message, _=None):
if (
state_message is not None
and state_message.payload
and state_message.payload["dps"]
):
self._dps.update(state_message.payload["dps"])
self._LOGGER.debug("Received updated state {}: {}".format(self, self._dps))
@property
def state(self):
return dict(self._dps)
@state.setter
def state_setter(self, new_values):
asyncio.create_task(self.async_set(new_values))
async def _async_handle_message(self):
if self._enabled is False or self._connected is False:
return
try:
self._response_task = asyncio.create_task(
self.reader.readuntil(MAGIC_SUFFIX_BYTES)
)
await self._response_task
response_data = self._response_task.result()
message = Message.from_bytes(self, response_data, self.cipher)
except Exception as e:
if isinstance(e, InvalidMessage):
self._LOGGER.debug("Invalid message from {}: {}".format(self, e))
elif isinstance(e, MessageDecodeFailed):
self._LOGGER.debug("Failed to decrypt message from {}".format(self))
elif isinstance(e, asyncio.IncompleteReadError):
if self._connected:
self._LOGGER.debug("Incomplete read")
elif isinstance(e, ConnectionResetError):
self._LOGGER.debug(
"Connection reset: {}\n{}".format(e, traceback.format_exc())
)
await self.async_disconnect()
else:
self._LOGGER.debug("Received message from {}: {}".format(self, message))
if message.sequence in self._listeners:
sem = self._listeners[message.sequence]
if isinstance(sem, asyncio.Semaphore):
self._listeners[message.sequence] = message
sem.release()
else:
handler = self._handlers.get(message.command, None)
if handler is not None:
asyncio.create_task(handler(message))
self._response_task = None
asyncio.create_task(self._async_handle_message())
async def _async_send(self, message, retries=2):
self._LOGGER.debug("Sending to {}: {}".format(self, message))
try:
await self.async_connect()
self.writer.write(message.bytes())
await self.writer.drain()
except Exception as e:
if retries == 0:
if isinstance(e, socket.error):
await self.async_disconnect()
raise ConnectionException(
"Connection to {} failed: {}".format(self, e)
)
elif isinstance(e, asyncio.IncompleteReadError):
raise InvalidMessage(
"Incomplete read from: {} : {}".format(self, e)
)
else:
raise TuyaException("Failed to send data to {}".format(self))
if isinstance(e, socket.error):
self._LOGGER.debug(
"Retrying send due to error. Connection to {} failed: {}".format(
self, e
)
)
elif isinstance(e, asyncio.IncompleteReadError):
self._LOGGER.debug(
"Retrying send due to error. Incomplete read from: {} : {}. Partial data recieved: {}".format(
self, e, e.partial
)
)
else:
self._LOGGER.debug(
"Retrying send due to error. Failed to send data to {}".format(self)
)
await asyncio.sleep(0.25)
await self._async_send(message, retries=retries - 1)
async def async_recieve(self, message):
if message.expect_response is True:
try:
self._recieve_task = asyncio.create_task(
asyncio.wait_for(message.listener.acquire(), timeout=self.timeout)
)
await self._recieve_task
response = self._listeners.pop(message.sequence)
if isinstance(response, Exception):
raise response
return response
except Exception as e:
del self._listeners[message.sequence]
await self.async_disconnect()
if isinstance(e, TimeoutError):
raise ResponseTimeoutException(
"Timed out waiting for response to sequence number {}".format(
message.sequence
)
)
raise e

View File

@ -0,0 +1,57 @@
import asyncio
import json
import logging
from hashlib import md5
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
_LOGGER = logging.getLogger(__name__)
UDP_KEY = md5(b"yGAdlopoPVldABfn").digest()
class DiscoveryPortsNotAvailableException(Exception):
"""This model is not supported"""
class TuyaLocalDiscovery(asyncio.DatagramProtocol):
def __init__(self, callback):
self.devices = {}
self._listeners = []
self.discovered_callback = callback
async def start(self):
loop = asyncio.get_running_loop()
listener = loop.create_datagram_endpoint(
lambda: self, local_addr=("0.0.0.0", 6666), reuse_port=True
)
encrypted_listener = loop.create_datagram_endpoint(
lambda: self, local_addr=("0.0.0.0", 6667), reuse_port=True
)
try:
self._listeners = await asyncio.gather(listener, encrypted_listener)
_LOGGER.debug("Listening to broadcasts on UDP port 6666 and 6667")
except Exception as e:
raise DiscoveryPortsNotAvailableException(
"Ports 6666 and 6667 are needed for autodiscovery but are unavailable. This may be due to having the localtuya integration installed and it not allowing other integrations to use the same ports. A pull request has been raised to address this: https://github.com/rospogrigio/localtuya/pull/1481"
)
def close(self, *args, **kwargs):
for transport, _ in self._listeners:
transport.close()
def datagram_received(self, data, addr):
data = data[20:-8]
try:
cipher = Cipher(algorithms.AES(UDP_KEY), modes.ECB(), default_backend())
decryptor = cipher.decryptor()
padded_data = decryptor.update(data) + decryptor.finalize()
data = padded_data[: -ord(padded_data[len(padded_data) - 1 :])]
except Exception:
data = data.decode()
decoded = json.loads(data)
asyncio.ensure_future(self.discovered_callback(decoded))

View File

@ -0,0 +1,242 @@
"""Original Work from here: Andre Borie https://gitlab.com/Rjevski/eufy-device-id-and-local-key-grabber"""
from hashlib import md5, sha256
import hmac
import json
import math
import random
import string
import time
import uuid
from cryptography.hazmat.backends.openssl import backend as openssl_backend
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
import requests
TUYA_INITIAL_BASE_URL = "https://a1.tuyaeu.com"
EUFY_HMAC_KEY = (
"A_cepev5pfnhua4dkqkdpmnrdxx378mpjr_s8x78u7xwymasd9kqa7a73pjhxqsedaj".encode()
)
def unpadded_rsa(key_exponent: int, key_n: int, plaintext: bytes) -> bytes:
keylength = math.ceil(key_n.bit_length() / 8)
input_nr = int.from_bytes(plaintext, byteorder="big")
crypted_nr = pow(input_nr, key_exponent, key_n)
return crypted_nr.to_bytes(keylength, byteorder="big")
def shuffled_md5(value: str) -> str:
_hash = md5(value.encode("utf-8")).hexdigest()
return _hash[8:16] + _hash[0:8] + _hash[24:32] + _hash[16:24]
TUYA_PASSWORD_INNER_CIPHER = Cipher(
algorithms.AES(
bytearray(
[36, 78, 109, 138, 86, 172, 135, 145, 36, 67, 45, 139, 108, 188, 162, 196]
)
),
modes.CBC(
bytearray(
[119, 36, 86, 242, 167, 102, 76, 243, 57, 44, 53, 151, 233, 62, 87, 71]
)
),
backend=openssl_backend,
)
DEFAULT_TUYA_HEADERS = {"User-Agent": "TY-UA=APP/Android/2.4.0/SDK/null"}
SIGNATURE_RELEVANT_PARAMETERS = {
"a",
"v",
"lat",
"lon",
"lang",
"deviceId",
"appVersion",
"ttid",
"isH5",
"h5Token",
"os",
"clientId",
"postData",
"time",
"requestId",
"et",
"n4h5",
"sid",
"sp",
}
DEFAULT_TUYA_QUERY_PARAMS = {
"appVersion": "2.4.0",
"deviceId": "",
"platform": "sdk_gphone64_arm64",
"clientId": "yx5v9uc3ef9wg3v9atje",
"lang": "en",
"osSystem": "12",
"os": "Android",
"timeZoneId": "",
"ttid": "android",
"et": "0.0.1",
"sdkVersion": "3.0.8cAnker",
}
class TuyaAPISession:
username = None
country_code = None
session_id = None
def __init__(self, username, region, timezone, phone_code):
self.session = requests.session()
self.session.headers = DEFAULT_TUYA_HEADERS.copy()
self.default_query_params = DEFAULT_TUYA_QUERY_PARAMS.copy()
self.default_query_params["deviceId"] = self.generate_new_device_id()
self.username = username
self.country_code = phone_code
self.base_url = {
"AZ": "https://a1.tuyaus.com",
"AY": "https://a1.tuyacn.com",
"IN": "https://a1.tuyain.com",
"EU": "https://a1.tuyaeu.com",
}.get(region, "https://a1.tuyaeu.com")
DEFAULT_TUYA_QUERY_PARAMS["timeZoneId"] = timezone
@staticmethod
def generate_new_device_id():
expected_length = 44
base64_characters = string.ascii_letters + string.digits
device_id_dependent_part = "8534c8ec0ed0"
return device_id_dependent_part + "".join(
random.choice(base64_characters)
for _ in range(expected_length - len(device_id_dependent_part))
)
@staticmethod
def get_signature(query_params: dict, encoded_post_data: str):
query_params = query_params.copy()
if encoded_post_data:
query_params["postData"] = encoded_post_data
sorted_pairs = sorted(query_params.items())
filtered_pairs = filter(
lambda p: p[0] and p[0] in SIGNATURE_RELEVANT_PARAMETERS, sorted_pairs
)
mapped_pairs = map(
# postData is pre-emptively hashed (for performance reasons?), everything else is included as-is
lambda p: p[0] + "=" + (shuffled_md5(p[1]) if p[0] == "postData" else p[1]),
filtered_pairs,
)
message = "||".join(mapped_pairs)
return hmac.HMAC(
key=EUFY_HMAC_KEY, msg=message.encode("utf-8"), digestmod=sha256
).hexdigest()
def _request(
self,
action: str,
version="1.0",
data: dict = None,
query_params: dict = None,
_requires_session=True,
):
if not self.session_id and _requires_session:
self.acquire_session()
current_time = time.time()
request_id = uuid.uuid4()
extra_query_params = {
"time": str(int(current_time)),
"requestId": str(request_id),
"a": action,
"v": version,
**(query_params or {}),
}
query_params = {**self.default_query_params, **extra_query_params}
encoded_post_data = json.dumps(data, separators=(",", ":")) if data else ""
resp = self.session.post(
self.base_url + "/api.json",
params={
**query_params,
"sign": self.get_signature(query_params, encoded_post_data),
},
data={"postData": encoded_post_data} if encoded_post_data else None,
)
resp.raise_for_status()
data = resp.json()
if "result" not in data:
raise Exception(
f"No 'result' key in the response - the entire response is {data}."
)
return data["result"]
def request_token(self, username, country_code):
return self._request(
action="tuya.m.user.uid.token.create",
data={"uid": username, "countryCode": country_code},
_requires_session=False,
)
def determine_password(self, username: str):
new_uid = username
padded_size = 16 * math.ceil(len(new_uid) / 16)
password_uid = new_uid.zfill(padded_size)
encryptor = TUYA_PASSWORD_INNER_CIPHER.encryptor()
encrypted_uid = encryptor.update(password_uid.encode("utf8"))
encrypted_uid += encryptor.finalize()
return md5(encrypted_uid.hex().upper().encode("utf-8")).hexdigest()
def request_session(self, username, password, country_code):
token_response = self.request_token(username, country_code)
encrypted_password = unpadded_rsa(
key_exponent=int(token_response["exponent"]),
key_n=int(token_response["publicKey"]),
plaintext=password.encode("utf-8"),
)
data = {
"uid": username,
"createGroup": True,
"ifencrypt": 1,
"passwd": encrypted_password.hex(),
"countryCode": country_code,
"options": '{"group": 1}',
"token": token_response["token"],
}
try:
return self._request(
action="tuya.m.user.uid.password.login.reg",
data=data,
_requires_session=False,
)
except Exception as e:
error_password = md5("12345678".encode("utf8")).hexdigest()
if password != error_password:
return self.request_session(username, error_password, country_code)
else:
raise e
def acquire_session(self):
password = self.determine_password(self.username)
session_response = self.request_session(
self.username, password, self.country_code
)
self.session_id = self.default_query_params["sid"] = session_response["sid"]
self.base_url = session_response["domain"]["mobileApiUrl"]
self.country_code = (
session_response["phoneCode"]
if session_response["phoneCode"]
else self.getCountryCode(session_response["domain"]["regionCode"])
)
def list_homes(self):
return self._request(action="tuya.m.location.list", version="2.1")
def get_device(self, devId):
return self._request(
action="tuya.m.device.get", version="1.0", data={"devId": devId}
)

View File

@ -0,0 +1,591 @@
# Copyright 2022 Brendan McCluskey
#
# 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.
"""Eufy Robovac sensor platform."""
from __future__ import annotations
from collections.abc import Mapping
from datetime import timedelta
import logging
import asyncio
import base64
import json
import time
import ast
from typing import Any
from enum import IntEnum, StrEnum
from homeassistant.loader import bind_hass
from homeassistant.components.vacuum import (
StateVacuumEntity,
STATE_CLEANING,
STATE_DOCKED,
STATE_ERROR,
STATE_IDLE,
STATE_RETURNING,
STATE_PAUSED
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.device_registry import (
CONNECTION_NETWORK_MAC,
)
from homeassistant.helpers.entity import DeviceInfo
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.const import (
CONF_ACCESS_TOKEN,
CONF_MODEL,
CONF_NAME,
CONF_ID,
CONF_IP_ADDRESS,
CONF_DESCRIPTION,
CONF_MAC,
STATE_UNAVAILABLE,
)
from .vacuums.base import RoboVacEntityFeature, RobovacCommand
from .tuyalocalapi import TuyaException
from .const import CONF_VACS, DOMAIN, REFRESH_RATE, PING_RATE, TIMEOUT
from .errors import getErrorMessage
from .robovac import (
ModelNotSupportedException,
RoboVac,
)
from homeassistant.const import ATTR_BATTERY_LEVEL
ATTR_BATTERY_ICON = "battery_icon"
ATTR_ERROR = "error"
ATTR_FAN_SPEED = "fan_speed"
ATTR_FAN_SPEED_LIST = "fan_speed_list"
ATTR_STATUS = "status"
ATTR_ERROR_CODE = "error_code"
ATTR_MODEL_CODE = "model_code"
ATTR_CLEANING_AREA = "cleaning_area"
ATTR_CLEANING_TIME = "cleaning_time"
ATTR_AUTO_RETURN = "auto_return"
ATTR_DO_NOT_DISTURB = "do_not_disturb"
ATTR_BOOST_IQ = "boost_iq"
ATTR_CONSUMABLES = "consumables"
ATTR_MODE = "mode"
MODE_MAPPING = { #152
"AggO": "Auto cleaning",
"BBoCCAE=": "Start auto",
"AggN": "Pause",
"AggG": "Stop / Go to charge",
"AA==": "Standby"
}
EMPTY_MAPPING = { #173
"BBICGAE=": "Empty dust",
"BBICIAE=": "Wash mop",
"BBICEAE=": "Dry mop"
}
TUYA_STATUS_MAPPING = { #153
"BgoAEAUyAA==": "AUTO",
"BgoAEAVSAA==": "POSITION",
"CAoAEAUyAggB": "PAUSE",
"CAoCCAEQBTIA": "ROOM",
"CAoCCAEQBVIA": "ROOM_POSITION",
"CgoCCAEQBTICCAE=": "ROOM_PAUSE",
"CAoCCAIQBTIA": "SPOT",
"CAoCCAIQBVIA": "SPOT_POSITION",
"CgoCCAIQBTICCAE=": "SPOT_PAUSE",
"BAoAEAY=": "START_MANUAL",
"BBAHQgA=": "GOING_TO_CHARGE",
"BBADGgA=": "CHARGING",
"BhADGgIIAQ==": "COMPLETED",
"AA==": "STANDBY",
"AhAB": "SLEEPING",
}
STATUS_MAPPING = {
"AUTO" : "Auto cleaning",
"POSITION": "Positioning",
"PAUSE": "Cleaning paused",
"ROOM": "Cleaning room",
"ROOM_POSITION": "Positioning room",
"ROOM_PAUSE": "Cleaning room paused",
"SPOT": "Spot cleaning",
"SPOT_POSITION": "Positioning spot",
"SPOT_PAUSE": "Cleaning spot paused",
"START_MANUAL": "Manual mode",
"GOING_TO_CHARGE": "Recharge",
"CHARGING": "Charging",
"COMPLETED": "Completed",
"STANDBY": "Standby",
"SLEEPING": "Sleeping",
}
ERROR_MAPPING = { #177
"DAiI6suO9dXszgFSAA==": "no_error",
"FAjwudWorOPszgEaAqURUgQSAqUR": "Sidebrush stuck",
"FAj+nMu7zuPszgEaAtg2UgQSAtg2": "Robot stuck",
"DAjtzbfps+XszgFSAA==": "no_error",
"DAiom9rd6eTszgFSAA==": "no_error",
"DAia8JTV5OPszgFSAA==": "no_error",
"DAj489bWsePszgFSAA==": "no_error",
# DAjH1er4vtbszgFSAA==
# DAi73bTN+uLszgFSAA==
# DAj489bWsePszgFSAA==
# DAia8JTV5OPszgFSAA==
# DAiom9rd6eTszgFSAA==
# DAjtzbfps+XszgFSAA==
}
_LOGGER = logging.getLogger(__name__)
SCAN_INTERVAL = timedelta(seconds=REFRESH_RATE)
UPDATE_RETRIES = 3
async def async_setup_entry(
hass: HomeAssistant,
config_entry: ConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Initialize my test integration 2 config entry."""
vacuums = config_entry.data[CONF_VACS]
for item in vacuums:
item = vacuums[item]
entity = RoboVacEntity(item)
hass.data[DOMAIN][CONF_VACS][item[CONF_ID]] = entity
async_add_entities([entity])
class RoboVacEntity(StateVacuumEntity):
"""Eufy Robovac version of a Vacuum entity"""
_attr_should_poll = True
_attr_access_token: str | None = None
_attr_ip_address: str | None = None
_attr_model_code: str | None = None
_attr_cleaning_area: str | None = None
_attr_cleaning_time: str | None = None
_attr_auto_return: str | None = None
_attr_do_not_disturb: str | None = None
_attr_boost_iq: str | None = None
_attr_consumables: str | None = None
_attr_mode: str | None = None
_attr_robovac_supported: str | None = None
@property
def robovac_supported(self) -> str | None:
"""Return the supported features of the vacuum cleaner."""
return self._attr_robovac_supported
@property
def mode(self) -> str | None:
"""Return the cleaning mode of the vacuum cleaner."""
return self._attr_mode
@property
def consumables(self) -> str | None:
"""Return the consumables status of the vacuum cleaner."""
return self._attr_consumables
@property
def cleaning_area(self) -> str | None:
"""Return the cleaning area of the vacuum cleaner."""
return self._attr_cleaning_area
@property
def cleaning_time(self) -> str | None:
"""Return the cleaning time of the vacuum cleaner."""
return self._attr_cleaning_time
@property
def auto_return(self) -> str | None:
"""Return the auto_return mode of the vacuum cleaner."""
return self._attr_auto_return
@property
def do_not_disturb(self) -> str | None:
"""Return the do not disturb mode of the vacuum cleaner."""
return self._attr_do_not_disturb
@property
def boost_iq(self) -> str | None:
"""Return the boost iq mode of the vacuum cleaner."""
return self._attr_boost_iq
@property
def model_code(self) -> str | None:
"""Return the model code of the vacuum cleaner."""
return self._attr_model_code
@property
def access_token(self) -> str | None:
"""Return the fan speed of the vacuum cleaner."""
return self._attr_access_token
@property
def ip_address(self) -> str | None:
"""Return the ip address of the vacuum cleaner."""
return self._attr_ip_address
@property
def state(self) -> str | None:
if self.tuya_state is None:
return STATE_UNAVAILABLE
elif (
type(self.error_code) is not None
and self.error_code
and self.error_code
not in [
0,
"no_error",
]
):
_LOGGER.debug(
"State changed to error. Error message: {}".format(
getErrorMessage(self.error_code)
)
)
return STATE_ERROR
elif self.tuya_state == "Charging" or self.tuya_state == "Completed":
return STATE_DOCKED
elif self.tuya_state == "Recharge":
return STATE_RETURNING
elif self.tuya_state == "Sleeping" or self.tuya_state == "Standby":
return STATE_IDLE
else:
return STATE_CLEANING
@property
def extra_state_attributes(self) -> dict[str, Any]:
"""Return the device-specific state attributes of this vacuum."""
data: dict[str, Any] = {}
if type(self.error_code) is not None and self.error_code not in [0, "no_error"]:
data[ATTR_ERROR] = getErrorMessage(self.error_code)
if (
self.robovac_supported & RoboVacEntityFeature.CLEANING_AREA
and self.cleaning_area
):
data[ATTR_CLEANING_AREA] = self.cleaning_area
if (
self.robovac_supported & RoboVacEntityFeature.CLEANING_TIME
and self.cleaning_time
):
data[ATTR_CLEANING_TIME] = self.cleaning_time
if (
self.robovac_supported & RoboVacEntityFeature.AUTO_RETURN
and self.auto_return
):
data[ATTR_AUTO_RETURN] = self.auto_return
if (
self.robovac_supported & RoboVacEntityFeature.DO_NOT_DISTURB
and self.do_not_disturb
):
data[ATTR_DO_NOT_DISTURB] = self.do_not_disturb
if self.robovac_supported & RoboVacEntityFeature.BOOST_IQ and self.boost_iq:
data[ATTR_BOOST_IQ] = self.boost_iq
if (
self.robovac_supported & RoboVacEntityFeature.CONSUMABLES
and self.consumables
):
data[ATTR_CONSUMABLES] = self.consumables
if self.mode:
data[ATTR_MODE] = self.mode
return data
def __init__(self, item) -> None:
"""Initialize Eufy Robovac"""
super().__init__()
self._attr_battery_level = 0
self._attr_name = item[CONF_NAME]
self._attr_unique_id = item[CONF_ID]
self._attr_model_code = item[CONF_MODEL]
self._attr_ip_address = item[CONF_IP_ADDRESS]
self._attr_access_token = item[CONF_ACCESS_TOKEN]
self.update_failures = 0
try:
self.vacuum = RoboVac(
device_id=self.unique_id,
host=self.ip_address,
local_key=self.access_token,
timeout=TIMEOUT,
ping_interval=PING_RATE,
model_code=self.model_code[0:5],
update_entity_state=self.pushed_update_handler,
)
except ModelNotSupportedException:
self.error_code = "UNSUPPORTED_MODEL"
self._attr_supported_features = self.vacuum.getHomeAssistantFeatures()
self._attr_robovac_supported = self.vacuum.getRoboVacFeatures()
fan_speeds = self.vacuum.getFanSpeeds()
self.fan_speed_map = {}
for speed in fan_speeds:
self.fan_speed_map[friendly_text(speed)] = speed
self._attr_fan_speed_list = list(self.fan_speed_map.keys())
_LOGGER.debug(self._attr_fan_speed_list)
self._tuya_command_codes = self.vacuum.getCommandCodes()
self._attr_mode = None
self._attr_consumables = None
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, item[CONF_ID])},
name=item[CONF_NAME],
manufacturer="Eufy",
model=item[CONF_DESCRIPTION],
connections=[
(CONNECTION_NETWORK_MAC, item[CONF_MAC]),
],
)
self.error_code = None
self.tuya_state = None
self.tuyastatus = None
async def async_added_to_hass(self):
await self.async_forced_update()
async def async_update(self):
"""Synchronise state from the vacuum."""
try:
await self.async_update_vacuum()
self.update_failures = 0
except TuyaException as e:
self.update_failures += 1
_LOGGER.warn(
"Update errored. Current update failure count: {}. Reason: {}".format(
self.update_failures, e
)
)
if self.update_failures >= UPDATE_RETRIES:
self.error_code = "CONNECTION_FAILED"
async def async_update_vacuum(self):
if self.error_code == "UNSUPPORTED_MODEL":
return
if self.ip_address == "":
self.error_code = "IP_ADDRESS"
return
await self.vacuum.async_get()
self.update_entity_values()
async def async_forced_update(self):
await self.async_update_vacuum()
self.async_write_ha_state()
async def pushed_update_handler(self):
self.update_entity_values()
self.async_write_ha_state()
def update_entity_values(self):
self.tuyastatus = self.vacuum._dps
_LOGGER.debug("tuyastatus %s", self.tuyastatus)
self._attr_battery_level = self.tuyastatus.get(
self._tuya_command_codes[RobovacCommand.BATTERY]
)
_LOGGER.debug("_attr_battery_level %s", self._attr_battery_level)
self.tuya_state = STATUS_MAPPING.get(
TUYA_STATUS_MAPPING.get(
self.tuyastatus.get(
self._tuya_command_codes[RobovacCommand.STATUS]
), None
), None
)
_LOGGER.debug("tuya_state %s", self.tuya_state)
self.error_code = ERROR_MAPPING.get(
self.tuyastatus.get(
self._tuya_command_codes[RobovacCommand.ERROR]
), None
)
_LOGGER.debug("error_code %s", self.error_code)
self._attr_mode = self.tuyastatus.get(
self._tuya_command_codes[RobovacCommand.MODE]
)
_LOGGER.debug("_attr_mode %s", self._attr_mode)
self._attr_fan_speed = friendly_text(
self.tuyastatus.get(self._tuya_command_codes[RobovacCommand.FAN_SPEED], "")
)
_LOGGER.debug("_attr_fan_speed %s", self._attr_fan_speed)
if self.robovac_supported & RoboVacEntityFeature.CLEANING_AREA:
self._attr_cleaning_area = self.tuyastatus.get(
self._tuya_command_codes[RobovacCommand.CLEANING_AREA]
)
_LOGGER.debug("_attr_cleaning_area %s", self._attr_cleaning_area)
if self.robovac_supported & RoboVacEntityFeature.CLEANING_TIME:
self._attr_cleaning_time = self.tuyastatus.get(
self._tuya_command_codes[RobovacCommand.CLEANING_TIME]
)
_LOGGER.debug("_attr_cleaning_time %s", self._attr_cleaning_time)
if self.robovac_supported & RoboVacEntityFeature.AUTO_RETURN:
self._attr_auto_return = self.tuyastatus.get(
self._tuya_command_codes[RobovacCommand.AUTO_RETURN]
)
_LOGGER.debug("_attr_auto_return %s", self._attr_auto_return)
if self.robovac_supported & RoboVacEntityFeature.DO_NOT_DISTURB:
self._attr_do_not_disturb = self.tuyastatus.get(
self._tuya_command_codes[RobovacCommand.DO_NOT_DISTURB]
)
_LOGGER.debug("_attr_do_not_disturb %s", self._attr_do_not_disturb)
if self.robovac_supported & RoboVacEntityFeature.BOOST_IQ:
self._attr_boost_iq = self.tuyastatus.get(
self._tuya_command_codes[RobovacCommand.BOOST_IQ]
)
_LOGGER.debug("_attr_boost_iq %s", self._attr_boost_iq)
if self.robovac_supported & RoboVacEntityFeature.CONSUMABLES:
consumables = ast.literal_eval(
base64.b64decode(
self.tuyastatus.get(
self._tuya_command_codes[RobovacCommand.CONSUMABLES]
)
).decode("ascii")
)
_LOGGER.debug("Consumables decoded value is: {}".format(consumables))
if "consumable" in consumables and "duration" in consumables["consumable"]:
_LOGGER.debug(
"Consumables encoded value is: {}".format(
consumables["consumable"]["duration"]
)
)
self._attr_consumables = consumables["consumable"]["duration"]
_LOGGER.debug("_attr_consumables %s", self._attr_consumables)
async def async_locate(self, **kwargs):
"""Locate the vacuum cleaner."""
_LOGGER.info("Locate Pressed")
code = self._tuya_command_codes[RobovacCommand.LOCATE]
if self.tuyastatus.get(code):
await self.vacuum.async_set({code: False})
else:
await self.vacuum.async_set({code: True})
asyncio.create_task(self.async_forced_update())
async def async_return_to_base(self, **kwargs):
"""Set the vacuum cleaner to return to the dock."""
_LOGGER.info("Return home Pressed")
await self.vacuum.async_set(
{self._tuya_command_codes[RobovacCommand.MODE]: "AggG"}
)
asyncio.create_task(self.async_forced_update())
async def async_start(self, **kwargs):
await self.vacuum.async_set(
{self._tuya_command_codes[RobovacCommand.MODE]: "BBoCCAE="}
)
asyncio.create_task(self.async_forced_update())
async def async_pause(self, **kwargs):
await self.vacuum.async_set(
{self._tuya_command_codes[RobovacCommand.MODE]: "AggN"}
)
asyncio.create_task(self.async_forced_update())
async def async_stop(self, **kwargs):
await self.async_return_to_base()
asyncio.create_task(self.async_forced_update())
async def async_clean_spot(self, **kwargs):
"""Perform a spot clean-up."""
_LOGGER.info("Spot Clean Pressed")
await self.vacuum.async_set(
{self._tuya_command_codes[RobovacCommand.MODE]: "Spot"}
)
asyncio.create_task(self.async_forced_update())
async def async_set_fan_speed(self, fan_speed, **kwargs):
"""Set fan speed."""
_LOGGER.info("Fan Speed Selected")
await self.vacuum.async_set(
{
self._tuya_command_codes[RobovacCommand.FAN_SPEED]: self.fan_speed_map[
fan_speed
]
}
)
asyncio.create_task(self.async_forced_update())
async def async_send_command(
self, command: str, params: dict | list | None = None, **kwargs
) -> None:
"""Send a command to a vacuum cleaner."""
_LOGGER.info("Send Command %s Pressed", command)
if command == "edgeClean":
await self.vacuum.async_set({"5": "Edge"})
elif command == "smallRoomClean":
await self.vacuum.async_set({"5": "SmallRoom"})
elif command == "autoClean":
await self.vacuum.async_set({"152": "BBoCCAE="})
elif command == "autoReturn":
if self.auto_return:
await self.vacuum.async_set({"135": False})
else:
await self.vacuum.async_set({"135": True})
elif command == "doNotDisturb":
if self.do_not_disturb:
await self.vacuum.async_set({"139": "MEQ4MDAwMDAw"})
await self.vacuum.async_set({"107": False})
else:
await self.vacuum.async_set({"139": "MTAwMDAwMDAw"})
await self.vacuum.async_set({"107": True})
elif command == "boostIQ":
if self.boost_iq:
await self.vacuum.async_set({"118": False})
else:
await self.vacuum.async_set({"118": True})
elif command == "roomClean":
roomIds = params.get("roomIds", [1])
count = params.get("count", 1)
clean_request = {"roomIds": roomIds, "cleanTimes": count}
method_call = {
"method": "selectRoomsClean",
"data": clean_request,
"timestamp": round(time.time() * 1000),
}
json_str = json.dumps(method_call, separators=(",", ":"))
base64_str = base64.b64encode(json_str.encode("utf8")).decode("utf8")
_LOGGER.info("roomClean call %s", json_str)
await self.vacuum.async_set({"124": base64_str})
else:
await self.vacuum.async_set({command: params.get("value", "")})
asyncio.create_task(self.async_forced_update())
async def async_will_remove_from_hass(self):
await self.vacuum.async_disable()
def friendly_text(input):
return " ".join(
word[0].upper() + word[1:] for word in input.replace("_", " ").split()
)

View File

@ -0,0 +1,44 @@
from homeassistant.components.vacuum import VacuumEntityFeature
from .base import RoboVacEntityFeature, RobovacCommand
class T1250:
homeassistant_features = (
VacuumEntityFeature.BATTERY
| VacuumEntityFeature.CLEAN_SPOT
| VacuumEntityFeature.FAN_SPEED
| VacuumEntityFeature.LOCATE
| VacuumEntityFeature.PAUSE
| VacuumEntityFeature.RETURN_HOME
| VacuumEntityFeature.SEND_COMMAND
| VacuumEntityFeature.START
| VacuumEntityFeature.STATE
| VacuumEntityFeature.STOP
)
robovac_features = RoboVacEntityFeature.CLEANING_TIME | RoboVacEntityFeature.CLEANING_AREA | RoboVacEntityFeature.DO_NOT_DISTURB | RoboVacEntityFeature.AUTO_RETURN | RoboVacEntityFeature.CONSUMABLES
commands = {
RobovacCommand.START_PAUSE: 2,
RobovacCommand.DIRECTION: {
"code": 3,
"values": ["forward", "back", "left", "right"],
},
RobovacCommand.MODE: {
"code": 5,
"values": ["auto", "SmallRoom", "Spot", "Edge", "Nosweep"],
},
RobovacCommand.STATUS: 15,
RobovacCommand.RETURN_HOME: 101,
RobovacCommand.FAN_SPEED: {
"code": 102,
"values": ["Standard","Turbo","Max","Boost_IQ"],
},
RobovacCommand.LOCATE: 103,
RobovacCommand.BATTERY: 104,
RobovacCommand.ERROR: 106,
# These commands need codes adding
# RobovacCommand.CLEANING_AREA: 0,
# RobovacCommand.CLEANING_TIME: 0,
# RobovacCommand.AUTO_RETURN: 0,
# RobovacCommand.DO_NOT_DISTURB: 0,
# RobovacCommand.CONSUMABLES: 0,
}

View File

@ -0,0 +1,38 @@
from homeassistant.components.vacuum import VacuumEntityFeature
from .base import RoboVacEntityFeature, RobovacCommand
class T2103:
homeassistant_features = (
VacuumEntityFeature.BATTERY
| VacuumEntityFeature.CLEAN_SPOT
| VacuumEntityFeature.FAN_SPEED
| VacuumEntityFeature.LOCATE
| VacuumEntityFeature.PAUSE
| VacuumEntityFeature.RETURN_HOME
| VacuumEntityFeature.SEND_COMMAND
| VacuumEntityFeature.START
| VacuumEntityFeature.STATE
| VacuumEntityFeature.STOP
)
robovac_features = RoboVacEntityFeature.EDGE | RoboVacEntityFeature.SMALL_ROOM
commands = {
RobovacCommand.START_PAUSE: 2,
RobovacCommand.DIRECTION: {
"code": 3,
"values": ["forward", "back", "left", "right"],
},
RobovacCommand.MODE: {
"code": 5,
"values": ["auto", "SmallRoom", "Spot", "Edge", "Nosweep"],
},
RobovacCommand.STATUS: 15,
RobovacCommand.RETURN_HOME: 101,
RobovacCommand.FAN_SPEED: {
"code": 102,
"values": ["No_suction","Standard","Boost_IQ","Max"],
},
RobovacCommand.LOCATE: 103,
RobovacCommand.BATTERY: 104,
RobovacCommand.ERROR: 106,
}

View File

@ -0,0 +1,38 @@
from homeassistant.components.vacuum import VacuumEntityFeature
from .base import RoboVacEntityFeature, RobovacCommand
class T2117:
homeassistant_features = (
VacuumEntityFeature.BATTERY
| VacuumEntityFeature.CLEAN_SPOT
| VacuumEntityFeature.FAN_SPEED
| VacuumEntityFeature.LOCATE
| VacuumEntityFeature.PAUSE
| VacuumEntityFeature.RETURN_HOME
| VacuumEntityFeature.SEND_COMMAND
| VacuumEntityFeature.START
| VacuumEntityFeature.STATE
| VacuumEntityFeature.STOP
)
robovac_features = RoboVacEntityFeature.EDGE | RoboVacEntityFeature.SMALL_ROOM
commands = {
RobovacCommand.START_PAUSE: 2,
RobovacCommand.DIRECTION: {
"code": 3,
"values": ["forward", "back", "left", "right"],
},
RobovacCommand.MODE: {
"code": 5,
"values": ["auto", "SmallRoom", "Spot", "Edge", "Nosweep"],
},
RobovacCommand.STATUS: 15,
RobovacCommand.RETURN_HOME: 101,
RobovacCommand.FAN_SPEED: {
"code": 102,
"values": ["No_suction","Standard","Boost_IQ","Max"],
},
RobovacCommand.LOCATE: 103,
RobovacCommand.BATTERY: 104,
RobovacCommand.ERROR: 106,
}

View File

@ -0,0 +1,38 @@
from homeassistant.components.vacuum import VacuumEntityFeature
from .base import RoboVacEntityFeature, RobovacCommand
class T2118:
homeassistant_features = (
VacuumEntityFeature.BATTERY
| VacuumEntityFeature.CLEAN_SPOT
| VacuumEntityFeature.FAN_SPEED
| VacuumEntityFeature.LOCATE
| VacuumEntityFeature.PAUSE
| VacuumEntityFeature.RETURN_HOME
| VacuumEntityFeature.SEND_COMMAND
| VacuumEntityFeature.START
| VacuumEntityFeature.STATE
| VacuumEntityFeature.STOP
)
robovac_features = RoboVacEntityFeature.EDGE | RoboVacEntityFeature.SMALL_ROOM
commands = {
RobovacCommand.START_PAUSE: 2,
RobovacCommand.DIRECTION: {
"code": 3,
"values": ["forward", "back", "left", "right"],
},
RobovacCommand.MODE: {
"code": 5,
"values": ["auto", "SmallRoom", "Spot", "Edge", "Nosweep"],
},
RobovacCommand.STATUS: 15,
RobovacCommand.RETURN_HOME: 101,
RobovacCommand.FAN_SPEED: {
"code": 102,
"values": ["No_suction","Standard","Boost_IQ","Max"],
},
RobovacCommand.LOCATE: 103,
RobovacCommand.BATTERY: 104,
RobovacCommand.ERROR: 106,
}

View File

@ -0,0 +1,38 @@
from homeassistant.components.vacuum import VacuumEntityFeature
from .base import RoboVacEntityFeature, RobovacCommand
class T2119:
homeassistant_features = (
VacuumEntityFeature.BATTERY
| VacuumEntityFeature.CLEAN_SPOT
| VacuumEntityFeature.FAN_SPEED
| VacuumEntityFeature.LOCATE
| VacuumEntityFeature.PAUSE
| VacuumEntityFeature.RETURN_HOME
| VacuumEntityFeature.SEND_COMMAND
| VacuumEntityFeature.START
| VacuumEntityFeature.STATE
| VacuumEntityFeature.STOP
)
robovac_features = RoboVacEntityFeature.EDGE | RoboVacEntityFeature.SMALL_ROOM
commands = {
RobovacCommand.START_PAUSE: 2,
RobovacCommand.DIRECTION: {
"code": 3,
"values": ["forward", "back", "left", "right"],
},
RobovacCommand.MODE: {
"code": 5,
"values": ["auto", "SmallRoom", "Spot", "Edge", "Nosweep"],
},
RobovacCommand.STATUS: 15,
RobovacCommand.RETURN_HOME: 101,
RobovacCommand.FAN_SPEED: {
"code": 102,
"values": ["No_suction","Standard","Boost_IQ","Max"],
},
RobovacCommand.LOCATE: 103,
RobovacCommand.BATTERY: 104,
RobovacCommand.ERROR: 106,
}

View File

@ -0,0 +1,38 @@
from homeassistant.components.vacuum import VacuumEntityFeature
from .base import RoboVacEntityFeature, RobovacCommand
class T2120:
homeassistant_features = (
VacuumEntityFeature.BATTERY
| VacuumEntityFeature.CLEAN_SPOT
| VacuumEntityFeature.FAN_SPEED
| VacuumEntityFeature.LOCATE
| VacuumEntityFeature.PAUSE
| VacuumEntityFeature.RETURN_HOME
| VacuumEntityFeature.SEND_COMMAND
| VacuumEntityFeature.START
| VacuumEntityFeature.STATE
| VacuumEntityFeature.STOP
)
robovac_features = RoboVacEntityFeature.EDGE | RoboVacEntityFeature.SMALL_ROOM
commands = {
RobovacCommand.START_PAUSE: 2,
RobovacCommand.DIRECTION: {
"code": 3,
"values": ["forward", "back", "left", "right"],
},
RobovacCommand.MODE: {
"code": 5,
"values": ["auto", "SmallRoom", "Spot", "Edge", "Nosweep"],
},
RobovacCommand.STATUS: 15,
RobovacCommand.RETURN_HOME: 101,
RobovacCommand.FAN_SPEED: {
"code": 102,
"values": ["No_suction","Standard","Boost_IQ","Max"],
},
RobovacCommand.LOCATE: 103,
RobovacCommand.BATTERY: 104,
RobovacCommand.ERROR: 106,
}

View File

@ -0,0 +1,38 @@
from homeassistant.components.vacuum import VacuumEntityFeature
from .base import RoboVacEntityFeature, RobovacCommand
class T2123:
homeassistant_features = (
VacuumEntityFeature.BATTERY
| VacuumEntityFeature.CLEAN_SPOT
| VacuumEntityFeature.FAN_SPEED
| VacuumEntityFeature.LOCATE
| VacuumEntityFeature.PAUSE
| VacuumEntityFeature.RETURN_HOME
| VacuumEntityFeature.SEND_COMMAND
| VacuumEntityFeature.START
| VacuumEntityFeature.STATE
| VacuumEntityFeature.STOP
)
robovac_features = RoboVacEntityFeature.EDGE | RoboVacEntityFeature.SMALL_ROOM
commands = {
RobovacCommand.START_PAUSE: 2,
RobovacCommand.DIRECTION: {
"code": 3,
"values": ["forward", "back", "left", "right"],
},
RobovacCommand.MODE: {
"code": 5,
"values": ["auto", "SmallRoom", "Spot", "Edge", "Nosweep"],
},
RobovacCommand.STATUS: 15,
RobovacCommand.RETURN_HOME: 101,
RobovacCommand.FAN_SPEED: {
"code": 102,
"values": ["No_suction","Standard","Boost_IQ","Max"],
},
RobovacCommand.LOCATE: 103,
RobovacCommand.BATTERY: 104,
RobovacCommand.ERROR: 106,
}

View File

@ -0,0 +1,38 @@
from homeassistant.components.vacuum import VacuumEntityFeature
from .base import RoboVacEntityFeature, RobovacCommand
class T2128:
homeassistant_features = (
VacuumEntityFeature.BATTERY
| VacuumEntityFeature.CLEAN_SPOT
| VacuumEntityFeature.FAN_SPEED
| VacuumEntityFeature.LOCATE
| VacuumEntityFeature.PAUSE
| VacuumEntityFeature.RETURN_HOME
| VacuumEntityFeature.SEND_COMMAND
| VacuumEntityFeature.START
| VacuumEntityFeature.STATE
| VacuumEntityFeature.STOP
)
robovac_features = RoboVacEntityFeature.EDGE | RoboVacEntityFeature.SMALL_ROOM
commands = {
RobovacCommand.START_PAUSE: 2,
RobovacCommand.DIRECTION: {
"code": 3,
"values": ["forward", "back", "left", "right"],
},
RobovacCommand.MODE: {
"code": 5,
"values": ["auto", "SmallRoom", "Spot", "Edge", "Nosweep"],
},
RobovacCommand.STATUS: 15,
RobovacCommand.RETURN_HOME: 101,
RobovacCommand.FAN_SPEED: {
"code": 102,
"values": ["No_suction","Standard","Boost_IQ","Max"],
},
RobovacCommand.LOCATE: 103,
RobovacCommand.BATTERY: 104,
RobovacCommand.ERROR: 106,
}

View File

@ -0,0 +1,38 @@
from homeassistant.components.vacuum import VacuumEntityFeature
from .base import RoboVacEntityFeature, RobovacCommand
class T2130:
homeassistant_features = (
VacuumEntityFeature.BATTERY
| VacuumEntityFeature.CLEAN_SPOT
| VacuumEntityFeature.FAN_SPEED
| VacuumEntityFeature.LOCATE
| VacuumEntityFeature.PAUSE
| VacuumEntityFeature.RETURN_HOME
| VacuumEntityFeature.SEND_COMMAND
| VacuumEntityFeature.START
| VacuumEntityFeature.STATE
| VacuumEntityFeature.STOP
)
robovac_features = RoboVacEntityFeature.EDGE | RoboVacEntityFeature.SMALL_ROOM
commands = {
RobovacCommand.START_PAUSE: 2,
RobovacCommand.DIRECTION: {
"code": 3,
"values": ["forward", "back", "left", "right"],
},
RobovacCommand.MODE: {
"code": 5,
"values": ["auto", "SmallRoom", "Spot", "Edge", "Nosweep"],
},
RobovacCommand.STATUS: 15,
RobovacCommand.RETURN_HOME: 101,
RobovacCommand.FAN_SPEED: {
"code": 102,
"values": ["No_suction","Standard","Boost_IQ","Max"],
},
RobovacCommand.LOCATE: 103,
RobovacCommand.BATTERY: 104,
RobovacCommand.ERROR: 106,
}

View File

@ -0,0 +1,38 @@
from homeassistant.components.vacuum import VacuumEntityFeature
from .base import RoboVacEntityFeature, RobovacCommand
class T2132:
homeassistant_features = (
VacuumEntityFeature.BATTERY
| VacuumEntityFeature.CLEAN_SPOT
| VacuumEntityFeature.FAN_SPEED
| VacuumEntityFeature.LOCATE
| VacuumEntityFeature.PAUSE
| VacuumEntityFeature.RETURN_HOME
| VacuumEntityFeature.SEND_COMMAND
| VacuumEntityFeature.START
| VacuumEntityFeature.STATE
| VacuumEntityFeature.STOP
)
robovac_features = RoboVacEntityFeature.EDGE | RoboVacEntityFeature.SMALL_ROOM
commands = {
RobovacCommand.START_PAUSE: 2,
RobovacCommand.DIRECTION: {
"code": 3,
"values": ["forward", "back", "left", "right"],
},
RobovacCommand.MODE: {
"code": 5,
"values": ["auto", "SmallRoom", "Spot", "Edge", "Nosweep"],
},
RobovacCommand.STATUS: 15,
RobovacCommand.RETURN_HOME: 101,
RobovacCommand.FAN_SPEED: {
"code": 102,
"values": ["No_suction","Standard","Boost_IQ","Max"],
},
RobovacCommand.LOCATE: 103,
RobovacCommand.BATTERY: 104,
RobovacCommand.ERROR: 106,
}

View File

@ -0,0 +1,43 @@
from homeassistant.components.vacuum import VacuumEntityFeature
from .base import RoboVacEntityFeature, RobovacCommand
class T2150:
homeassistant_features = (
VacuumEntityFeature.BATTERY
| VacuumEntityFeature.CLEAN_SPOT
| VacuumEntityFeature.FAN_SPEED
| VacuumEntityFeature.LOCATE
| VacuumEntityFeature.PAUSE
| VacuumEntityFeature.RETURN_HOME
| VacuumEntityFeature.SEND_COMMAND
| VacuumEntityFeature.START
| VacuumEntityFeature.STATE
| VacuumEntityFeature.STOP
)
robovac_features = RoboVacEntityFeature.CLEANING_TIME | RoboVacEntityFeature.CLEANING_AREA | RoboVacEntityFeature.DO_NOT_DISTURB | RoboVacEntityFeature.AUTO_RETURN
commands = {
RobovacCommand.START_PAUSE: 2,
RobovacCommand.DIRECTION: {
"code": 3,
"values": ["forward", "back", "left", "right"],
},
RobovacCommand.MODE: {
"code": 5,
"values": ["auto", "SmallRoom", "Spot", "Edge", "Nosweep"],
},
RobovacCommand.STATUS: 15,
RobovacCommand.RETURN_HOME: 101,
RobovacCommand.FAN_SPEED: {
"code": 102,
"values": ["Standard","Turbo","Max","Boost_IQ"],
},
RobovacCommand.LOCATE: 103,
RobovacCommand.BATTERY: 104,
RobovacCommand.ERROR: 106,
# These commands need codes adding
# RobovacCommand.CLEANING_AREA: 0,
# RobovacCommand.CLEANING_TIME: 0,
# RobovacCommand.AUTO_RETURN: 0,
# RobovacCommand.DO_NOT_DISTURB: 0,
}

View File

@ -0,0 +1,46 @@
from homeassistant.components.vacuum import VacuumEntityFeature
from .base import RoboVacEntityFeature, RobovacCommand
class T2181:
homeassistant_features = (
VacuumEntityFeature.BATTERY
| VacuumEntityFeature.CLEAN_SPOT
| VacuumEntityFeature.FAN_SPEED
| VacuumEntityFeature.LOCATE
| VacuumEntityFeature.PAUSE
| VacuumEntityFeature.RETURN_HOME
| VacuumEntityFeature.SEND_COMMAND
| VacuumEntityFeature.START
| VacuumEntityFeature.STATE
| VacuumEntityFeature.STOP
| VacuumEntityFeature.MAP
)
robovac_features = RoboVacEntityFeature.CLEANING_TIME | RoboVacEntityFeature.CLEANING_AREA | RoboVacEntityFeature.DO_NOT_DISTURB | RoboVacEntityFeature.AUTO_RETURN | RoboVacEntityFeature.ROOM | RoboVacEntityFeature.ZONE | RoboVacEntityFeature.BOOST_IQ | RoboVacEntityFeature.MAP | RoboVacEntityFeature.CONSUMABLES
commands = {
RobovacCommand.START_PAUSE: 2,
RobovacCommand.DIRECTION: {
"code": 3,
"values": ["forward", "back", "left", "right"],
},
RobovacCommand.MODE: {
"code": 5,
"values": ["auto", "SmallRoom", "Spot", "Edge", "Nosweep"],
},
RobovacCommand.STATUS: 15,
RobovacCommand.RETURN_HOME: 101,
RobovacCommand.FAN_SPEED: {
"code": 102,
"values": ["Quiet","Standard","Turbo","Max"],
},
RobovacCommand.LOCATE: 103,
RobovacCommand.BATTERY: 104,
RobovacCommand.ERROR: 106,
# These commands need codes adding
# RobovacCommand.CLEANING_AREA: 0,
# RobovacCommand.CLEANING_TIME: 0,
# RobovacCommand.AUTO_RETURN: 0,
# RobovacCommand.DO_NOT_DISTURB: 0,
# RobovacCommand.BOOST_IQ: 0,
# RobovacCommand.CONSUMABLES: 0,
}

View File

@ -0,0 +1,46 @@
from homeassistant.components.vacuum import VacuumEntityFeature
from .base import RoboVacEntityFeature, RobovacCommand
class T2182:
homeassistant_features = (
VacuumEntityFeature.BATTERY
| VacuumEntityFeature.CLEAN_SPOT
| VacuumEntityFeature.FAN_SPEED
| VacuumEntityFeature.LOCATE
| VacuumEntityFeature.PAUSE
| VacuumEntityFeature.RETURN_HOME
| VacuumEntityFeature.SEND_COMMAND
| VacuumEntityFeature.START
| VacuumEntityFeature.STATE
| VacuumEntityFeature.STOP
| VacuumEntityFeature.MAP
)
robovac_features = RoboVacEntityFeature.CLEANING_TIME | RoboVacEntityFeature.CLEANING_AREA | RoboVacEntityFeature.DO_NOT_DISTURB | RoboVacEntityFeature.AUTO_RETURN | RoboVacEntityFeature.ROOM | RoboVacEntityFeature.ZONE | RoboVacEntityFeature.BOOST_IQ | RoboVacEntityFeature.MAP | RoboVacEntityFeature.CONSUMABLES
commands = {
RobovacCommand.START_PAUSE: 2,
RobovacCommand.DIRECTION: {
"code": 3,
"values": ["forward", "back", "left", "right"],
},
RobovacCommand.MODE: {
"code": 5,
"values": ["auto", "SmallRoom", "Spot", "Edge", "Nosweep"],
},
RobovacCommand.STATUS: 15,
RobovacCommand.RETURN_HOME: 101,
RobovacCommand.FAN_SPEED: {
"code": 102,
"values": ["Quiet","Standard","Turbo","Max"],
},
RobovacCommand.LOCATE: 103,
RobovacCommand.BATTERY: 104,
RobovacCommand.ERROR: 106,
# These commands need codes adding
# RobovacCommand.CLEANING_AREA: 0,
# RobovacCommand.CLEANING_TIME: 0,
# RobovacCommand.AUTO_RETURN: 0,
# RobovacCommand.DO_NOT_DISTURB: 0,
# RobovacCommand.BOOST_IQ: 0,
# RobovacCommand.CONSUMABLES: 0,
}

View File

@ -0,0 +1,46 @@
from homeassistant.components.vacuum import VacuumEntityFeature
from .base import RoboVacEntityFeature, RobovacCommand
class T2190:
homeassistant_features = (
VacuumEntityFeature.BATTERY
| VacuumEntityFeature.CLEAN_SPOT
| VacuumEntityFeature.FAN_SPEED
| VacuumEntityFeature.LOCATE
| VacuumEntityFeature.PAUSE
| VacuumEntityFeature.RETURN_HOME
| VacuumEntityFeature.SEND_COMMAND
| VacuumEntityFeature.START
| VacuumEntityFeature.STATE
| VacuumEntityFeature.STOP
| VacuumEntityFeature.MAP
)
robovac_features = RoboVacEntityFeature.CLEANING_TIME | RoboVacEntityFeature.CLEANING_AREA | RoboVacEntityFeature.DO_NOT_DISTURB | RoboVacEntityFeature.AUTO_RETURN | RoboVacEntityFeature.ROOM | RoboVacEntityFeature.ZONE | RoboVacEntityFeature.BOOST_IQ | RoboVacEntityFeature.MAP | RoboVacEntityFeature.CONSUMABLES
commands = {
RobovacCommand.START_PAUSE: 2,
RobovacCommand.DIRECTION: {
"code": 3,
"values": ["forward", "back", "left", "right"],
},
RobovacCommand.MODE: {
"code": 5,
"values": ["auto", "SmallRoom", "Spot", "Edge", "Nosweep"],
},
RobovacCommand.STATUS: 15,
RobovacCommand.RETURN_HOME: 101,
RobovacCommand.FAN_SPEED: {
"code": 102,
"values": ["Quiet","Standard","Turbo","Max"],
},
RobovacCommand.LOCATE: 103,
RobovacCommand.BATTERY: 104,
RobovacCommand.ERROR: 106,
# These commands need codes adding
# RobovacCommand.CLEANING_AREA: 0,
# RobovacCommand.CLEANING_TIME: 0,
# RobovacCommand.AUTO_RETURN: 0,
# RobovacCommand.DO_NOT_DISTURB: 0,
# RobovacCommand.BOOST_IQ: 0,
# RobovacCommand.CONSUMABLES: 0,
}

View File

@ -0,0 +1,45 @@
from homeassistant.components.vacuum import VacuumEntityFeature
from .base import RoboVacEntityFeature, RobovacCommand
class T2192:
homeassistant_features = (
VacuumEntityFeature.BATTERY
| VacuumEntityFeature.CLEAN_SPOT
| VacuumEntityFeature.FAN_SPEED
| VacuumEntityFeature.LOCATE
| VacuumEntityFeature.PAUSE
| VacuumEntityFeature.RETURN_HOME
| VacuumEntityFeature.SEND_COMMAND
| VacuumEntityFeature.START
| VacuumEntityFeature.STATE
| VacuumEntityFeature.STOP
| VacuumEntityFeature.MAP
)
robovac_features = RoboVacEntityFeature.CLEANING_TIME | RoboVacEntityFeature.CLEANING_AREA | RoboVacEntityFeature.DO_NOT_DISTURB | RoboVacEntityFeature.AUTO_RETURN | RoboVacEntityFeature.ROOM | RoboVacEntityFeature.ZONE | RoboVacEntityFeature.BOOST_IQ | RoboVacEntityFeature.MAP
commands = {
RobovacCommand.START_PAUSE: 2,
RobovacCommand.DIRECTION: {
"code": 3,
"values": ["forward", "back", "left", "right"],
},
RobovacCommand.MODE: {
"code": 5,
"values": ["auto", "SmallRoom", "Spot", "Edge", "Nosweep"],
},
RobovacCommand.STATUS: 15,
RobovacCommand.RETURN_HOME: 101,
RobovacCommand.FAN_SPEED: {
"code": 102,
"values": ["Quiet","Standard","Turbo","Max"],
},
RobovacCommand.LOCATE: 103,
RobovacCommand.BATTERY: 104,
RobovacCommand.ERROR: 106,
# These commands need codes adding
# RobovacCommand.CLEANING_AREA: 0,
# RobovacCommand.CLEANING_TIME: 0,
# RobovacCommand.AUTO_RETURN: 0,
# RobovacCommand.DO_NOT_DISTURB: 0,
# RobovacCommand.BOOST_IQ: 0,
}

View File

@ -0,0 +1,46 @@
from homeassistant.components.vacuum import VacuumEntityFeature
from .base import RoboVacEntityFeature, RobovacCommand
class T2193:
homeassistant_features = (
VacuumEntityFeature.BATTERY
| VacuumEntityFeature.CLEAN_SPOT
| VacuumEntityFeature.FAN_SPEED
| VacuumEntityFeature.LOCATE
| VacuumEntityFeature.PAUSE
| VacuumEntityFeature.RETURN_HOME
| VacuumEntityFeature.SEND_COMMAND
| VacuumEntityFeature.START
| VacuumEntityFeature.STATE
| VacuumEntityFeature.STOP
| VacuumEntityFeature.MAP
)
robovac_features = RoboVacEntityFeature.CLEANING_TIME | RoboVacEntityFeature.CLEANING_AREA | RoboVacEntityFeature.DO_NOT_DISTURB | RoboVacEntityFeature.AUTO_RETURN | RoboVacEntityFeature.ROOM | RoboVacEntityFeature.ZONE | RoboVacEntityFeature.BOOST_IQ | RoboVacEntityFeature.MAP | RoboVacEntityFeature.CONSUMABLES
commands = {
RobovacCommand.START_PAUSE: 2,
RobovacCommand.DIRECTION: {
"code": 3,
"values": ["forward", "back", "left", "right"],
},
RobovacCommand.MODE: {
"code": 5,
"values": ["auto", "SmallRoom", "Spot", "Edge", "Nosweep"],
},
RobovacCommand.STATUS: 15,
RobovacCommand.RETURN_HOME: 101,
RobovacCommand.FAN_SPEED: {
"code": 102,
"values": ["Quiet","Standard","Turbo","Max"],
},
RobovacCommand.LOCATE: 103,
RobovacCommand.BATTERY: 104,
RobovacCommand.ERROR: 106,
# These commands need codes adding
# RobovacCommand.CLEANING_AREA: 0,
# RobovacCommand.CLEANING_TIME: 0,
# RobovacCommand.AUTO_RETURN: 0,
# RobovacCommand.DO_NOT_DISTURB: 0,
# RobovacCommand.BOOST_IQ: 0,
# RobovacCommand.CONSUMABLES: 0,
}

View File

@ -0,0 +1,46 @@
from homeassistant.components.vacuum import VacuumEntityFeature
from .base import RoboVacEntityFeature, RobovacCommand
class T2194:
homeassistant_features = (
VacuumEntityFeature.BATTERY
| VacuumEntityFeature.CLEAN_SPOT
| VacuumEntityFeature.FAN_SPEED
| VacuumEntityFeature.LOCATE
| VacuumEntityFeature.PAUSE
| VacuumEntityFeature.RETURN_HOME
| VacuumEntityFeature.SEND_COMMAND
| VacuumEntityFeature.START
| VacuumEntityFeature.STATE
| VacuumEntityFeature.STOP
| VacuumEntityFeature.MAP
)
robovac_features = RoboVacEntityFeature.CLEANING_TIME | RoboVacEntityFeature.CLEANING_AREA | RoboVacEntityFeature.DO_NOT_DISTURB | RoboVacEntityFeature.AUTO_RETURN | RoboVacEntityFeature.ROOM | RoboVacEntityFeature.ZONE | RoboVacEntityFeature.BOOST_IQ | RoboVacEntityFeature.MAP | RoboVacEntityFeature.CONSUMABLES
commands = {
RobovacCommand.START_PAUSE: 2,
RobovacCommand.DIRECTION: {
"code": 3,
"values": ["forward", "back", "left", "right"],
},
RobovacCommand.MODE: {
"code": 5,
"values": ["auto", "SmallRoom", "Spot", "Edge", "Nosweep"],
},
RobovacCommand.STATUS: 15,
RobovacCommand.RETURN_HOME: 101,
RobovacCommand.FAN_SPEED: {
"code": 102,
"values": ["Quiet","Standard","Turbo","Max"],
},
RobovacCommand.LOCATE: 103,
RobovacCommand.BATTERY: 104,
RobovacCommand.ERROR: 106,
# These commands need codes adding
# RobovacCommand.CLEANING_AREA: 0,
# RobovacCommand.CLEANING_TIME: 0,
# RobovacCommand.AUTO_RETURN: 0,
# RobovacCommand.DO_NOT_DISTURB: 0,
# RobovacCommand.BOOST_IQ: 0,
# RobovacCommand.CONSUMABLES: 0,
}

View File

@ -0,0 +1,43 @@
from homeassistant.components.vacuum import VacuumEntityFeature
from .base import RoboVacEntityFeature, RobovacCommand
class T2250:
homeassistant_features = (
VacuumEntityFeature.BATTERY
| VacuumEntityFeature.CLEAN_SPOT
| VacuumEntityFeature.FAN_SPEED
| VacuumEntityFeature.LOCATE
| VacuumEntityFeature.PAUSE
| VacuumEntityFeature.RETURN_HOME
| VacuumEntityFeature.SEND_COMMAND
| VacuumEntityFeature.START
| VacuumEntityFeature.STATE
| VacuumEntityFeature.STOP
)
robovac_features = RoboVacEntityFeature.CLEANING_TIME | RoboVacEntityFeature.CLEANING_AREA | RoboVacEntityFeature.DO_NOT_DISTURB | RoboVacEntityFeature.AUTO_RETURN
commands = {
RobovacCommand.START_PAUSE: 2,
RobovacCommand.DIRECTION: {
"code": 3,
"values": ["forward", "back", "left", "right"],
},
RobovacCommand.MODE: {
"code": 5,
"values": ["auto", "SmallRoom", "Spot", "Edge", "Nosweep"],
},
RobovacCommand.STATUS: 15,
RobovacCommand.RETURN_HOME: 101,
RobovacCommand.FAN_SPEED: {
"code": 102,
"values": ["Standard","Turbo","Max","Boost_IQ"],
},
RobovacCommand.LOCATE: 103,
RobovacCommand.BATTERY: 104,
RobovacCommand.ERROR: 106,
# These commands need codes adding
# RobovacCommand.CLEANING_AREA: 0,
# RobovacCommand.CLEANING_TIME: 0,
# RobovacCommand.AUTO_RETURN: 0,
# RobovacCommand.DO_NOT_DISTURB: 0,
}

View File

@ -0,0 +1,43 @@
from homeassistant.components.vacuum import VacuumEntityFeature
from .base import RoboVacEntityFeature, RobovacCommand
class T2251:
homeassistant_features = (
VacuumEntityFeature.BATTERY
| VacuumEntityFeature.CLEAN_SPOT
| VacuumEntityFeature.FAN_SPEED
| VacuumEntityFeature.LOCATE
| VacuumEntityFeature.PAUSE
| VacuumEntityFeature.RETURN_HOME
| VacuumEntityFeature.SEND_COMMAND
| VacuumEntityFeature.START
| VacuumEntityFeature.STATE
| VacuumEntityFeature.STOP
)
robovac_features = RoboVacEntityFeature.CLEANING_TIME | RoboVacEntityFeature.CLEANING_AREA | RoboVacEntityFeature.DO_NOT_DISTURB | RoboVacEntityFeature.AUTO_RETURN
commands = {
RobovacCommand.START_PAUSE: 2,
RobovacCommand.DIRECTION: {
"code": 3,
"values": ["forward", "back", "left", "right"],
},
RobovacCommand.MODE: {
"code": 5,
"values": ["auto", "SmallRoom", "Spot", "Edge", "Nosweep"],
},
RobovacCommand.STATUS: 15,
RobovacCommand.RETURN_HOME: 101,
RobovacCommand.FAN_SPEED: {
"code": 102,
"values": ["Standard","Turbo","Max","Boost_IQ"],
},
RobovacCommand.LOCATE: 103,
RobovacCommand.BATTERY: 104,
RobovacCommand.ERROR: 106,
# These commands need codes adding
# RobovacCommand.CLEANING_AREA: 0,
# RobovacCommand.CLEANING_TIME: 0,
# RobovacCommand.AUTO_RETURN: 0,
# RobovacCommand.DO_NOT_DISTURB: 0,
}

View File

@ -0,0 +1,43 @@
from homeassistant.components.vacuum import VacuumEntityFeature
from .base import RoboVacEntityFeature, RobovacCommand
class T2252:
homeassistant_features = (
VacuumEntityFeature.BATTERY
| VacuumEntityFeature.CLEAN_SPOT
| VacuumEntityFeature.FAN_SPEED
| VacuumEntityFeature.LOCATE
| VacuumEntityFeature.PAUSE
| VacuumEntityFeature.RETURN_HOME
| VacuumEntityFeature.SEND_COMMAND
| VacuumEntityFeature.START
| VacuumEntityFeature.STATE
| VacuumEntityFeature.STOP
)
robovac_features = RoboVacEntityFeature.CLEANING_TIME | RoboVacEntityFeature.CLEANING_AREA | RoboVacEntityFeature.DO_NOT_DISTURB | RoboVacEntityFeature.AUTO_RETURN
commands = {
RobovacCommand.START_PAUSE: 2,
RobovacCommand.DIRECTION: {
"code": 3,
"values": ["forward", "back", "left", "right"],
},
RobovacCommand.MODE: {
"code": 5,
"values": ["auto", "SmallRoom", "Spot", "Edge", "Nosweep"],
},
RobovacCommand.STATUS: 15,
RobovacCommand.RETURN_HOME: 101,
RobovacCommand.FAN_SPEED: {
"code": 102,
"values": ["Standard","Turbo","Max","Boost_IQ"],
},
RobovacCommand.LOCATE: 103,
RobovacCommand.BATTERY: 104,
RobovacCommand.ERROR: 106,
# These commands need codes adding
# RobovacCommand.CLEANING_AREA: 0,
# RobovacCommand.CLEANING_TIME: 0,
# RobovacCommand.AUTO_RETURN: 0,
# RobovacCommand.DO_NOT_DISTURB: 0,
}

View File

@ -0,0 +1,45 @@
from homeassistant.components.vacuum import VacuumEntityFeature
from .base import RoboVacEntityFeature, RobovacCommand
class T2253:
homeassistant_features = (
VacuumEntityFeature.BATTERY
| VacuumEntityFeature.CLEAN_SPOT
| VacuumEntityFeature.FAN_SPEED
| VacuumEntityFeature.LOCATE
| VacuumEntityFeature.PAUSE
| VacuumEntityFeature.RETURN_HOME
| VacuumEntityFeature.SEND_COMMAND
| VacuumEntityFeature.START
| VacuumEntityFeature.STATE
| VacuumEntityFeature.STOP
| VacuumEntityFeature.MAP
)
robovac_features = RoboVacEntityFeature.CLEANING_TIME | RoboVacEntityFeature.CLEANING_AREA | RoboVacEntityFeature.DO_NOT_DISTURB | RoboVacEntityFeature.AUTO_RETURN | RoboVacEntityFeature.MAP | RoboVacEntityFeature.CONSUMABLES
commands = {
RobovacCommand.START_PAUSE: 2,
RobovacCommand.DIRECTION: {
"code": 3,
"values": ["forward", "back", "left", "right"],
},
RobovacCommand.MODE: {
"code": 5,
"values": ["auto", "SmallRoom", "Spot", "Edge", "Nosweep"],
},
RobovacCommand.STATUS: 15,
RobovacCommand.RETURN_HOME: 101,
RobovacCommand.FAN_SPEED: {
"code": 102,
"values": ["Standard","Turbo","Max","Boost_IQ"],
},
RobovacCommand.LOCATE: 103,
RobovacCommand.BATTERY: 104,
RobovacCommand.ERROR: 106,
# These commands need codes adding
# RobovacCommand.CLEANING_AREA: 0,
# RobovacCommand.CLEANING_TIME: 0,
# RobovacCommand.AUTO_RETURN: 0,
# RobovacCommand.DO_NOT_DISTURB: 0,
# RobovacCommand.CONSUMABLES: 0,
}

View File

@ -0,0 +1,43 @@
from homeassistant.components.vacuum import VacuumEntityFeature
from .base import RoboVacEntityFeature, RobovacCommand
class T2254:
homeassistant_features = (
VacuumEntityFeature.BATTERY
| VacuumEntityFeature.CLEAN_SPOT
| VacuumEntityFeature.FAN_SPEED
| VacuumEntityFeature.LOCATE
| VacuumEntityFeature.PAUSE
| VacuumEntityFeature.RETURN_HOME
| VacuumEntityFeature.SEND_COMMAND
| VacuumEntityFeature.START
| VacuumEntityFeature.STATE
| VacuumEntityFeature.STOP
)
robovac_features = RoboVacEntityFeature.CLEANING_TIME | RoboVacEntityFeature.CLEANING_AREA | RoboVacEntityFeature.DO_NOT_DISTURB | RoboVacEntityFeature.AUTO_RETURN
commands = {
RobovacCommand.START_PAUSE: 2,
RobovacCommand.DIRECTION: {
"code": 3,
"values": ["forward", "back", "left", "right"],
},
RobovacCommand.MODE: {
"code": 5,
"values": ["auto", "SmallRoom", "Spot", "Edge", "Nosweep"],
},
RobovacCommand.STATUS: 15,
RobovacCommand.RETURN_HOME: 101,
RobovacCommand.FAN_SPEED: {
"code": 102,
"values": ["Standard","Turbo","Max","Boost_IQ"],
},
RobovacCommand.LOCATE: 103,
RobovacCommand.BATTERY: 104,
RobovacCommand.ERROR: 106,
# These commands need codes adding
# RobovacCommand.CLEANING_AREA: 0,
# RobovacCommand.CLEANING_TIME: 0,
# RobovacCommand.AUTO_RETURN: 0,
# RobovacCommand.DO_NOT_DISTURB: 0,
}

View File

@ -0,0 +1,43 @@
from homeassistant.components.vacuum import VacuumEntityFeature
from .base import RoboVacEntityFeature, RobovacCommand
class T2255:
homeassistant_features = (
VacuumEntityFeature.BATTERY
| VacuumEntityFeature.CLEAN_SPOT
| VacuumEntityFeature.FAN_SPEED
| VacuumEntityFeature.LOCATE
| VacuumEntityFeature.PAUSE
| VacuumEntityFeature.RETURN_HOME
| VacuumEntityFeature.SEND_COMMAND
| VacuumEntityFeature.START
| VacuumEntityFeature.STATE
| VacuumEntityFeature.STOP
)
robovac_features = RoboVacEntityFeature.CLEANING_TIME | RoboVacEntityFeature.CLEANING_AREA | RoboVacEntityFeature.DO_NOT_DISTURB | RoboVacEntityFeature.AUTO_RETURN
commands = {
RobovacCommand.START_PAUSE: 2,
RobovacCommand.DIRECTION: {
"code": 3,
"values": ["forward", "back", "left", "right"],
},
RobovacCommand.MODE: {
"code": 5,
"values": ["auto", "SmallRoom", "Spot", "Edge", "Nosweep"],
},
RobovacCommand.STATUS: 15,
RobovacCommand.RETURN_HOME: 101,
RobovacCommand.FAN_SPEED: {
"code": 102,
"values": ["Standard","Turbo","Max","Boost_IQ"],
},
RobovacCommand.LOCATE: 103,
RobovacCommand.BATTERY: 104,
RobovacCommand.ERROR: 106,
# These commands need codes adding
# RobovacCommand.CLEANING_AREA: 0,
# RobovacCommand.CLEANING_TIME: 0,
# RobovacCommand.AUTO_RETURN: 0,
# RobovacCommand.DO_NOT_DISTURB: 0,
}

View File

@ -0,0 +1,44 @@
from homeassistant.components.vacuum import VacuumEntityFeature
from .base import RoboVacEntityFeature, RobovacCommand
class T2256:
homeassistant_features = (
VacuumEntityFeature.BATTERY
| VacuumEntityFeature.CLEAN_SPOT
| VacuumEntityFeature.FAN_SPEED
| VacuumEntityFeature.LOCATE
| VacuumEntityFeature.PAUSE
| VacuumEntityFeature.RETURN_HOME
| VacuumEntityFeature.SEND_COMMAND
| VacuumEntityFeature.START
| VacuumEntityFeature.STATE
| VacuumEntityFeature.STOP
)
robovac_features = RoboVacEntityFeature.CLEANING_TIME | RoboVacEntityFeature.CLEANING_AREA | RoboVacEntityFeature.DO_NOT_DISTURB | RoboVacEntityFeature.AUTO_RETURN | RoboVacEntityFeature.CONSUMABLES
commands = {
RobovacCommand.START_PAUSE: 2,
RobovacCommand.DIRECTION: {
"code": 3,
"values": ["forward", "back", "left", "right"],
},
RobovacCommand.MODE: {
"code": 5,
"values": ["auto", "SmallRoom", "Spot", "Edge", "Nosweep"],
},
RobovacCommand.STATUS: 15,
RobovacCommand.RETURN_HOME: 101,
RobovacCommand.FAN_SPEED: {
"code": 102,
"values": ["Standard","Turbo","Max","Boost_IQ"],
},
RobovacCommand.LOCATE: 103,
RobovacCommand.BATTERY: 104,
RobovacCommand.ERROR: 106,
# These commands need codes adding
# RobovacCommand.CLEANING_AREA: 0,
# RobovacCommand.CLEANING_TIME: 0,
# RobovacCommand.AUTO_RETURN: 0,
# RobovacCommand.DO_NOT_DISTURB: 0,
# RobovacCommand.CONSUMABLES: 0,
}

View File

@ -0,0 +1,43 @@
from homeassistant.components.vacuum import VacuumEntityFeature
from .base import RoboVacEntityFeature, RobovacCommand
class T2257:
homeassistant_features = (
VacuumEntityFeature.BATTERY
| VacuumEntityFeature.CLEAN_SPOT
| VacuumEntityFeature.FAN_SPEED
| VacuumEntityFeature.LOCATE
| VacuumEntityFeature.PAUSE
| VacuumEntityFeature.RETURN_HOME
| VacuumEntityFeature.SEND_COMMAND
| VacuumEntityFeature.START
| VacuumEntityFeature.STATE
| VacuumEntityFeature.STOP
)
robovac_features = RoboVacEntityFeature.CLEANING_TIME | RoboVacEntityFeature.CLEANING_AREA | RoboVacEntityFeature.DO_NOT_DISTURB | RoboVacEntityFeature.AUTO_RETURN
commands = {
RobovacCommand.START_PAUSE: 2,
RobovacCommand.DIRECTION: {
"code": 3,
"values": ["forward", "back", "left", "right"],
},
RobovacCommand.MODE: {
"code": 5,
"values": ["auto", "SmallRoom", "Spot", "Edge", "Nosweep"],
},
RobovacCommand.STATUS: 15,
RobovacCommand.RETURN_HOME: 101,
RobovacCommand.FAN_SPEED: {
"code": 102,
"values": ["Standard","Turbo","Max","Boost_IQ"],
},
RobovacCommand.LOCATE: 103,
RobovacCommand.BATTERY: 104,
RobovacCommand.ERROR: 106,
# These commands need codes adding
# RobovacCommand.CLEANING_AREA: 0,
# RobovacCommand.CLEANING_TIME: 0,
# RobovacCommand.AUTO_RETURN: 0,
# RobovacCommand.DO_NOT_DISTURB: 0,
}

View File

@ -0,0 +1,44 @@
from homeassistant.components.vacuum import VacuumEntityFeature
from .base import RoboVacEntityFeature, RobovacCommand
class T2258:
homeassistant_features = (
VacuumEntityFeature.BATTERY
| VacuumEntityFeature.CLEAN_SPOT
| VacuumEntityFeature.FAN_SPEED
| VacuumEntityFeature.LOCATE
| VacuumEntityFeature.PAUSE
| VacuumEntityFeature.RETURN_HOME
| VacuumEntityFeature.SEND_COMMAND
| VacuumEntityFeature.START
| VacuumEntityFeature.STATE
| VacuumEntityFeature.STOP
)
robovac_features = RoboVacEntityFeature.CLEANING_TIME | RoboVacEntityFeature.CLEANING_AREA | RoboVacEntityFeature.DO_NOT_DISTURB | RoboVacEntityFeature.AUTO_RETURN | RoboVacEntityFeature.CONSUMABLES
commands = {
RobovacCommand.START_PAUSE: 2,
RobovacCommand.DIRECTION: {
"code": 3,
"values": ["forward", "back", "left", "right"],
},
RobovacCommand.MODE: {
"code": 5,
"values": ["auto", "SmallRoom", "Spot", "Edge", "Nosweep"],
},
RobovacCommand.STATUS: 15,
RobovacCommand.RETURN_HOME: 101,
RobovacCommand.FAN_SPEED: {
"code": 102,
"values": ["Standard","Turbo","Max","Boost_IQ"],
},
RobovacCommand.LOCATE: 103,
RobovacCommand.BATTERY: 104,
RobovacCommand.ERROR: 106,
# These commands need codes adding
# RobovacCommand.CLEANING_AREA: 0,
# RobovacCommand.CLEANING_TIME: 0,
# RobovacCommand.AUTO_RETURN: 0,
# RobovacCommand.DO_NOT_DISTURB: 0,
# RobovacCommand.CONSUMABLES: 0,
}

View File

@ -0,0 +1,43 @@
from homeassistant.components.vacuum import VacuumEntityFeature
from .base import RoboVacEntityFeature, RobovacCommand
class T2259:
homeassistant_features = (
VacuumEntityFeature.BATTERY
| VacuumEntityFeature.CLEAN_SPOT
| VacuumEntityFeature.FAN_SPEED
| VacuumEntityFeature.LOCATE
| VacuumEntityFeature.PAUSE
| VacuumEntityFeature.RETURN_HOME
| VacuumEntityFeature.SEND_COMMAND
| VacuumEntityFeature.START
| VacuumEntityFeature.STATE
| VacuumEntityFeature.STOP
)
robovac_features = RoboVacEntityFeature.CLEANING_TIME | RoboVacEntityFeature.CLEANING_AREA | RoboVacEntityFeature.DO_NOT_DISTURB | RoboVacEntityFeature.AUTO_RETURN
commands = {
RobovacCommand.START_PAUSE: 2,
RobovacCommand.DIRECTION: {
"code": 3,
"values": ["forward", "back", "left", "right"],
},
RobovacCommand.MODE: {
"code": 5,
"values": ["auto", "SmallRoom", "Spot", "Edge", "Nosweep"],
},
RobovacCommand.STATUS: 15,
RobovacCommand.RETURN_HOME: 101,
RobovacCommand.FAN_SPEED: {
"code": 102,
"values": ["Standard","Turbo","Max","Boost_IQ"],
},
RobovacCommand.LOCATE: 103,
RobovacCommand.BATTERY: 104,
RobovacCommand.ERROR: 106,
# These commands need codes adding
# RobovacCommand.CLEANING_AREA: 0,
# RobovacCommand.CLEANING_TIME: 0,
# RobovacCommand.AUTO_RETURN: 0,
# RobovacCommand.DO_NOT_DISTURB: 0,
}

View File

@ -0,0 +1,46 @@
from homeassistant.components.vacuum import VacuumEntityFeature
from .base import RoboVacEntityFeature, RobovacCommand
class T2261:
homeassistant_features = (
VacuumEntityFeature.BATTERY
| VacuumEntityFeature.CLEAN_SPOT
| VacuumEntityFeature.FAN_SPEED
| VacuumEntityFeature.LOCATE
| VacuumEntityFeature.PAUSE
| VacuumEntityFeature.RETURN_HOME
| VacuumEntityFeature.SEND_COMMAND
| VacuumEntityFeature.START
| VacuumEntityFeature.STATE
| VacuumEntityFeature.STOP
| VacuumEntityFeature.MAP
)
robovac_features = RoboVacEntityFeature.CLEANING_TIME | RoboVacEntityFeature.CLEANING_AREA | RoboVacEntityFeature.DO_NOT_DISTURB | RoboVacEntityFeature.AUTO_RETURN | RoboVacEntityFeature.ROOM | RoboVacEntityFeature.ZONE | RoboVacEntityFeature.BOOST_IQ | RoboVacEntityFeature.MAP | RoboVacEntityFeature.CONSUMABLES
commands = {
RobovacCommand.START_PAUSE: 2,
RobovacCommand.DIRECTION: {
"code": 3,
"values": ["forward", "back", "left", "right"],
},
RobovacCommand.MODE: {
"code": 5,
"values": ["auto", "SmallRoom", "Spot", "Edge", "Nosweep"],
},
RobovacCommand.STATUS: 15,
RobovacCommand.RETURN_HOME: 101,
RobovacCommand.FAN_SPEED: {
"code": 102,
"values": ["Pure","Standard","Turbo","Max"],
},
RobovacCommand.LOCATE: 103,
RobovacCommand.BATTERY: 104,
RobovacCommand.ERROR: 106,
# These commands need codes adding
# RobovacCommand.CLEANING_AREA: 0,
# RobovacCommand.CLEANING_TIME: 0,
# RobovacCommand.AUTO_RETURN: 0,
# RobovacCommand.DO_NOT_DISTURB: 0,
# RobovacCommand.BOOST_IQ: 0,
# RobovacCommand.CONSUMABLES: 0,
}

View File

@ -0,0 +1,45 @@
from homeassistant.components.vacuum import VacuumEntityFeature
from .base import RoboVacEntityFeature, RobovacCommand
class T2262:
homeassistant_features = (
VacuumEntityFeature.BATTERY
| VacuumEntityFeature.CLEAN_SPOT
| VacuumEntityFeature.FAN_SPEED
| VacuumEntityFeature.LOCATE
| VacuumEntityFeature.PAUSE
| VacuumEntityFeature.RETURN_HOME
| VacuumEntityFeature.SEND_COMMAND
| VacuumEntityFeature.START
| VacuumEntityFeature.STATE
| VacuumEntityFeature.STOP
| VacuumEntityFeature.MAP
)
robovac_features = RoboVacEntityFeature.CLEANING_TIME | RoboVacEntityFeature.CLEANING_AREA | RoboVacEntityFeature.DO_NOT_DISTURB | RoboVacEntityFeature.AUTO_RETURN | RoboVacEntityFeature.ROOM | RoboVacEntityFeature.ZONE | RoboVacEntityFeature.BOOST_IQ | RoboVacEntityFeature.MAP
commands = {
RobovacCommand.START_PAUSE: 2,
RobovacCommand.DIRECTION: {
"code": 3,
"values": ["forward", "back", "left", "right"],
},
RobovacCommand.MODE: {
"code": 5,
"values": ["auto", "SmallRoom", "Spot", "Edge", "Nosweep"],
},
RobovacCommand.STATUS: 15,
RobovacCommand.RETURN_HOME: 101,
RobovacCommand.FAN_SPEED: {
"code": 102,
"values": ["Pure","Standard","Turbo","Max"],
},
RobovacCommand.LOCATE: 103,
RobovacCommand.BATTERY: 104,
RobovacCommand.ERROR: 106,
# These commands need codes adding
# RobovacCommand.CLEANING_AREA: 0,
# RobovacCommand.CLEANING_TIME: 0,
# RobovacCommand.AUTO_RETURN: 0,
# RobovacCommand.DO_NOT_DISTURB: 0,
# RobovacCommand.BOOST_IQ: 0,
}

View File

@ -0,0 +1,134 @@
from homeassistant.components.vacuum import VacuumEntityFeature
from .base import RobovacCommand, RoboVacEntityFeature
class T2266:
homeassistant_features = (
VacuumEntityFeature.BATTERY
# | VacuumEntityFeature.CLEAN_SPOT
| VacuumEntityFeature.FAN_SPEED
| VacuumEntityFeature.LOCATE
| VacuumEntityFeature.PAUSE
| VacuumEntityFeature.RETURN_HOME
| VacuumEntityFeature.SEND_COMMAND
| VacuumEntityFeature.START
| VacuumEntityFeature.STATE
| VacuumEntityFeature.STOP
# | VacuumEntityFeature.MAP
)
robovac_features = (
# RoboVacEntityFeature.CLEANING_TIME
# | RoboVacEntityFeature.CLEANING_AREA
RoboVacEntityFeature.DO_NOT_DISTURB
# | RoboVacEntityFeature.AUTO_RETURN
# | RoboVacEntityFeature.ROOM
# | RoboVacEntityFeature.ZONE
| RoboVacEntityFeature.BOOST_IQ
# | RoboVacEntityFeature.MAP
# | RoboVacEntityFeature.CONSUMABLES
)
commands = {
RobovacCommand.MODE: { # works (Start Auto and Return dock commands tested)
"code": 152,
"values": ["AggN", "AA==", "AggG", "BBoCCAE=", "AggO"],
},
RobovacCommand.STATUS: { # works (status only)
"code": 153,
"values": [
"BgoAEAUyAA===",
"BgoAEAVSAA===",
"CAoAEAUyAggB",
"CAoCCAEQBTIA",
"CAoCCAEQBVIA",
"CgoCCAEQBTICCAE=",
"CAoCCAIQBTIA",
"CAoCCAIQBVIA",
"CgoCCAIQBTICCAE=",
"BAoAEAY=",
"BBAHQgA=",
"BBADGgA=",
"BhADGgIIAQ==",
"AA==",
"AhAB",
],
},
RobovacCommand.DIRECTION: { # untested
"code": 155,
"values": ["Brake", "Forward", "Back", "Left", "Right"],
},
RobovacCommand.START_PAUSE: 156, # True, False #works (status only)
RobovacCommand.DO_NOT_DISTURB: 157, # DgoAEgoKABICCBYaAggI #untested
RobovacCommand.FAN_SPEED: { # works (status and update)
"code": 158,
"values": ["Quiet", "Standard", "Turbo", "Max"],
},
RobovacCommand.BOOST_IQ: 159, # True, False #works (status and update)
RobovacCommand.LOCATE: 160, # True, False #works (status)
# Speaker volume: 161 #works, not yet implemented
RobovacCommand.BATTERY: 163, # int #works (status)
RobovacCommand.CONSUMABLES: 168, # encrypted, not usable
RobovacCommand.RETURN_HOME: 173, # encrypted, not usable
# FgoQMggKAggBEgIQAToECgIIARICCAE=
# FgoQMg4KAggBEggIARj/////DxICCAE=
# FAoQMggKAggBEgIQAToECgIIARIA
# GAoQMggKAggBEgIQAToECgIIARIECAE4AQ==
# GgoQMggKAggBEgIQAToECgIIARIGCAEYATgB
RobovacCommand.ERROR: 177, # #encrypted, few known values
# SIDEBRUSH_STUCK: "FAjwudWorOPszgEaAqURUgQSAqUR"
# ROBOT_STUCK: "FAj+nMu7zuPszgEaAtg2UgQSAtg2"
# IQofCgIIAhICCAIaAggCKgIIAjoCCBugAe7Pqs6M1+zOAQ==
# IQofCgIIAhICCAIaAggCKgIIAjoCCBqgAYPx0a331uzOAQ==
# IQofCgIIBBICCAQaAggEKgIIBDoCCCmgAcSfs6Lo5uzOAQ==
# These commands need codes adding
# RobovacCommand.CLEANING_AREA: 0,
# RobovacCommand.CLEANING_TIME: 0,
# RobovacCommand.AUTO_RETURN: 0,
# Unknown: 151 (true/false)
# Unknown: 154
# DgoKCgAaAggBIgIIARIA
# DAoICgAaAggBIgASAA==
# Unknown: 164
# MBoAIiwKBgi4y/q0BhIECAEQARoMCAESBBgJIB4aAgg+Kg4aDBIKCgIIARIAGgAiAA==
# NAgGEAYaACIsCgYIuMv6tAYSBAgBEAEaDAgBEgQYCSAeGgIIPioOGgwSCgoCCAESABoAIgA=
# Unknown: 167
# FAoAEgcIiEoQbhgEGgcI1EgQbBgC
# FgoCEAESBwiIShBuGAQaBwjUSBBsGAI=
# GAoECDwQARIHCIhKEG4YBBoHCNRIEGwYAg==
# GQoFCLQBEAQSBwiIShBuGAQaBwjUSBBsGAI=
# GwoFCKApEDgSCAiocxCmARgFGggI9HEQpAEYAw==
# Unknown: 171
# AhAB
# Unknown: 176
# MQoAGgBSCBoAIgIIASoAWDJiHwodChFBIG5ldHdvcmsgZm9yIHlvdRABGgYQ4/7/tAY=
# LwoAGgBSCBoAIgIIASoAWFZiHQobChFBIG5ldHdvcmsgZm9yIHlvdRoGEPvagrUG
# LwoAGgBSCBoAIgIIASoAWCJiHQobChFBIG5ldHdvcmsgZm9yIHlvdRoGEK2YgLUG
# LwoAGgBSCBoAIgIIASoAWDBiHQobChFBIG5ldHdvcmsgZm9yIHlvdRoGEK2YgLUG
# Unknown: 178
# DQjRidjfv9bszgESAR8=
# DQiMrPbd+eLszgESAVU=
# DQiW0dXL+uLszgESAR8=
# Cgiv6NbWsePszgE=
# DQjPuorb6eTszgESAR8=
# DQjayd7nsOXszgESASg=
# Unknown: 179
# EBIOKgwIBRACGAEgwYyAtQY=
# FhIUEhIIBRABIFsowYyAtQYw74yAtQY=
# DhIMKgoIBhgCIPvagrUG
# DhIMKgoIBxgCIJLbgrUG
# EBIOKgwIBxADGAIg3eyCtQY=
# EBIOKgwIBxAEGAIgrPGCtQY=
# DhIMKgoICBgCILHxgrUG
# DhIMKgoICBADIIj6grUG
# DhIMKgoICBADIOqMg7UG
# DhIMKgoICBAEIOuMg7UG
# DBIKKggICSCljYO1Bg==
# DhIMKgoICRACIJmcg7UG
# FhIUEhIICRABIBoomZyDtQYw6pyDtQY=
# DhIMIgoICRABGO+cg7UG
# DhIMIgoICRABGLedg7UG
# IRIfCh0ICRgBMPvagrUGOMmdg7UGQKApSDhQO1gBYAdqAA==
# Unknown: 169
# cwoSZXVmeSBDbGVhbiBMNjAgU0VTGhFDODpGRTowRjo3Nzo5NDo5QyIGMS4zLjI0KAVCKDM2NGFjOGNkNjQzZjllMDczZjg4NzlmNGFhOTdkZGE5OGUzMjg5NTRiFggBEgQIAhABGgQIAhABIgIIASoCCAE=
# s \x12eufy Clean L60 SES\x1a\x11C8:FE:0F:77:94:9C"\x061.3.24(\x05B(364ac8cd643f9e073f8879f4aa97dda98e328954b\x16\x08\x01\x12\x04\x08\x02\x10\x01\x1a\x04\x08\x02\x10\x01"\x02\x08\x01*\x02\x08\x01
}

View File

@ -0,0 +1,120 @@
from homeassistant.components.vacuum import VacuumEntityFeature
from .base import RoboVacEntityFeature, RobovacCommand
class T2267:
homeassistant_features = (
VacuumEntityFeature.BATTERY
# | VacuumEntityFeature.CLEAN_SPOT
| VacuumEntityFeature.FAN_SPEED
| VacuumEntityFeature.LOCATE
| VacuumEntityFeature.PAUSE
| VacuumEntityFeature.RETURN_HOME
| VacuumEntityFeature.SEND_COMMAND
| VacuumEntityFeature.START
| VacuumEntityFeature.STATE
| VacuumEntityFeature.STOP
# | VacuumEntityFeature.MAP
)
robovac_features = (
# RoboVacEntityFeature.CLEANING_TIME
# | RoboVacEntityFeature.CLEANING_AREA
RoboVacEntityFeature.DO_NOT_DISTURB
# | RoboVacEntityFeature.AUTO_RETURN
# | RoboVacEntityFeature.ROOM
# | RoboVacEntityFeature.ZONE
| RoboVacEntityFeature.BOOST_IQ
# | RoboVacEntityFeature.MAP
# | RoboVacEntityFeature.CONSUMABLES
)
commands = {
RobovacCommand.MODE: { #works (Start Auto and Return dock commands tested)
"code": 152,
"values": ["AggN","AA==","AggG","BBoCCAE=","AggO"],
},
RobovacCommand.STATUS: { #works (status only)
"code": 153,
"values": ["BgoAEAUyAA===","BgoAEAVSAA===","CAoAEAUyAggB","CAoCCAEQBTIA","CAoCCAEQBVIA","CgoCCAEQBTICCAE=","CAoCCAIQBTIA","CAoCCAIQBVIA","CgoCCAIQBTICCAE=","BAoAEAY=","BBAHQgA=","BBADGgA=","BhADGgIIAQ==","AA==","AhAB"],
},
RobovacCommand.DIRECTION: { #untested
"code": 155,
"values": ["Brake", "Forward", "Back", "Left", "Right"],
},
RobovacCommand.START_PAUSE: 156, # True, False #works (status only)
RobovacCommand.DO_NOT_DISTURB: 157, # DgoAEgoKABICCBYaAggI #untested
RobovacCommand.FAN_SPEED: { #works (status and update)
"code": 158,
"values": ["Quiet", "Standard", "Turbo", "Max"],
},
RobovacCommand.BOOST_IQ: 159, # True, False #works (status and update)
RobovacCommand.LOCATE: 160, # True, False #works (status)
# Speaker volume: 161 #works, not yet implemented
RobovacCommand.BATTERY: 163, # int #works (status)
RobovacCommand.CONSUMABLES: 168, #encrypted, not usable
RobovacCommand.RETURN_HOME: 173, #encrypted, not usable
# FgoQMggKAggBEgIQAToECgIIARICCAE=
# FgoQMg4KAggBEggIARj/////DxICCAE=
# FAoQMggKAggBEgIQAToECgIIARIA
# GAoQMggKAggBEgIQAToECgIIARIECAE4AQ==
# GgoQMggKAggBEgIQAToECgIIARIGCAEYATgB
RobovacCommand.ERROR: 177, # #encrypted, few known values
# SIDEBRUSH_STUCK: "FAjwudWorOPszgEaAqURUgQSAqUR"
# ROBOT_STUCK: "FAj+nMu7zuPszgEaAtg2UgQSAtg2"
# IQofCgIIAhICCAIaAggCKgIIAjoCCBugAe7Pqs6M1+zOAQ==
# IQofCgIIAhICCAIaAggCKgIIAjoCCBqgAYPx0a331uzOAQ==
# IQofCgIIBBICCAQaAggEKgIIBDoCCCmgAcSfs6Lo5uzOAQ==
# These commands need codes adding
# RobovacCommand.CLEANING_AREA: 0,
# RobovacCommand.CLEANING_TIME: 0,
# RobovacCommand.AUTO_RETURN: 0,
# Unknown: 151 (true/false)
# Unknown: 154
# DgoKCgAaAggBIgIIARIA
# DAoICgAaAggBIgASAA==
# Unknown: 164
# MBoAIiwKBgi4y/q0BhIECAEQARoMCAESBBgJIB4aAgg+Kg4aDBIKCgIIARIAGgAiAA==
# NAgGEAYaACIsCgYIuMv6tAYSBAgBEAEaDAgBEgQYCSAeGgIIPioOGgwSCgoCCAESABoAIgA=
# Unknown: 167
# FAoAEgcIiEoQbhgEGgcI1EgQbBgC
# FgoCEAESBwiIShBuGAQaBwjUSBBsGAI=
# GAoECDwQARIHCIhKEG4YBBoHCNRIEGwYAg==
# GQoFCLQBEAQSBwiIShBuGAQaBwjUSBBsGAI=
# GwoFCKApEDgSCAiocxCmARgFGggI9HEQpAEYAw==
# Unknown: 171
# AhAB
# Unknown: 176
# MQoAGgBSCBoAIgIIASoAWDJiHwodChFBIG5ldHdvcmsgZm9yIHlvdRABGgYQ4/7/tAY=
# LwoAGgBSCBoAIgIIASoAWFZiHQobChFBIG5ldHdvcmsgZm9yIHlvdRoGEPvagrUG
# LwoAGgBSCBoAIgIIASoAWCJiHQobChFBIG5ldHdvcmsgZm9yIHlvdRoGEK2YgLUG
# LwoAGgBSCBoAIgIIASoAWDBiHQobChFBIG5ldHdvcmsgZm9yIHlvdRoGEK2YgLUG
# Unknown: 178
# DQjRidjfv9bszgESAR8=
# DQiMrPbd+eLszgESAVU=
# DQiW0dXL+uLszgESAR8=
# Cgiv6NbWsePszgE=
# DQjPuorb6eTszgESAR8=
# DQjayd7nsOXszgESASg=
# Unknown: 179
# EBIOKgwIBRACGAEgwYyAtQY=
# FhIUEhIIBRABIFsowYyAtQYw74yAtQY=
# DhIMKgoIBhgCIPvagrUG
# DhIMKgoIBxgCIJLbgrUG
# EBIOKgwIBxADGAIg3eyCtQY=
# EBIOKgwIBxAEGAIgrPGCtQY=
# DhIMKgoICBgCILHxgrUG
# DhIMKgoICBADIIj6grUG
# DhIMKgoICBADIOqMg7UG
# DhIMKgoICBAEIOuMg7UG
# DBIKKggICSCljYO1Bg==
# DhIMKgoICRACIJmcg7UG
# FhIUEhIICRABIBoomZyDtQYw6pyDtQY=
# DhIMIgoICRABGO+cg7UG
# DhIMIgoICRABGLedg7UG
# IRIfCh0ICRgBMPvagrUGOMmdg7UGQKApSDhQO1gBYAdqAA==
# Unknown: 169
# cwoSZXVmeSBDbGVhbiBMNjAgU0VTGhFDODpGRTowRjo3Nzo5NDo5QyIGMS4zLjI0KAVCKDM2NGFjOGNkNjQzZjllMDczZjg4NzlmNGFhOTdkZGE5OGUzMjg5NTRiFggBEgQIAhABGgQIAhABIgIIASoCCAE=
# s \x12eufy Clean L60 SES\x1a\x11C8:FE:0F:77:94:9C"\x061.3.24(\x05B(364ac8cd643f9e073f8879f4aa97dda98e328954b\x16\x08\x01\x12\x04\x08\x02\x10\x01\x1a\x04\x08\x02\x10\x01"\x02\x08\x01*\x02\x08\x01
}

View File

@ -0,0 +1,43 @@
from homeassistant.components.vacuum import VacuumEntityFeature
from .base import RoboVacEntityFeature, RobovacCommand
class T2270:
homeassistant_features = (
VacuumEntityFeature.BATTERY
| VacuumEntityFeature.CLEAN_SPOT
| VacuumEntityFeature.FAN_SPEED
| VacuumEntityFeature.LOCATE
| VacuumEntityFeature.PAUSE
| VacuumEntityFeature.RETURN_HOME
| VacuumEntityFeature.SEND_COMMAND
| VacuumEntityFeature.START
| VacuumEntityFeature.STATE
| VacuumEntityFeature.STOP
)
robovac_features = RoboVacEntityFeature.CLEANING_TIME | RoboVacEntityFeature.CLEANING_AREA | RoboVacEntityFeature.DO_NOT_DISTURB | RoboVacEntityFeature.AUTO_RETURN
commands = {
RobovacCommand.START_PAUSE: 2,
RobovacCommand.DIRECTION: {
"code": 3,
"values": ["forward", "back", "left", "right"],
},
RobovacCommand.MODE: {
"code": 5,
"values": ["auto", "SmallRoom", "Spot", "Edge", "Nosweep"],
},
RobovacCommand.STATUS: 15,
RobovacCommand.RETURN_HOME: 101,
RobovacCommand.FAN_SPEED: {
"code": 102,
"values": ["Standard","Turbo","Max","Boost_IQ"],
},
RobovacCommand.LOCATE: 103,
RobovacCommand.BATTERY: 104,
RobovacCommand.ERROR: 106,
# These commands need codes adding
# RobovacCommand.CLEANING_AREA: 0,
# RobovacCommand.CLEANING_TIME: 0,
# RobovacCommand.AUTO_RETURN: 0,
# RobovacCommand.DO_NOT_DISTURB: 0,
}

View File

@ -0,0 +1,43 @@
from homeassistant.components.vacuum import VacuumEntityFeature
from .base import RoboVacEntityFeature, RobovacCommand
class T2272:
homeassistant_features = (
VacuumEntityFeature.BATTERY
| VacuumEntityFeature.CLEAN_SPOT
| VacuumEntityFeature.FAN_SPEED
| VacuumEntityFeature.LOCATE
| VacuumEntityFeature.PAUSE
| VacuumEntityFeature.RETURN_HOME
| VacuumEntityFeature.SEND_COMMAND
| VacuumEntityFeature.START
| VacuumEntityFeature.STATE
| VacuumEntityFeature.STOP
)
robovac_features = RoboVacEntityFeature.CLEANING_TIME | RoboVacEntityFeature.CLEANING_AREA | RoboVacEntityFeature.DO_NOT_DISTURB | RoboVacEntityFeature.AUTO_RETURN
commands = {
RobovacCommand.START_PAUSE: 2,
RobovacCommand.DIRECTION: {
"code": 3,
"values": ["forward", "back", "left", "right"],
},
RobovacCommand.MODE: {
"code": 5,
"values": ["auto", "SmallRoom", "Spot", "Edge", "Nosweep"],
},
RobovacCommand.STATUS: 15,
RobovacCommand.RETURN_HOME: 101,
RobovacCommand.FAN_SPEED: {
"code": 102,
"values": ["Standard","Turbo","Max","Boost_IQ"],
},
RobovacCommand.LOCATE: 103,
RobovacCommand.BATTERY: 104,
RobovacCommand.ERROR: 106,
# These commands need codes adding
# RobovacCommand.CLEANING_AREA: 0,
# RobovacCommand.CLEANING_TIME: 0,
# RobovacCommand.AUTO_RETURN: 0,
# RobovacCommand.DO_NOT_DISTURB: 0,
}

View File

@ -0,0 +1,44 @@
from homeassistant.components.vacuum import VacuumEntityFeature
from .base import RoboVacEntityFeature, RobovacCommand
class T2273:
homeassistant_features = (
VacuumEntityFeature.BATTERY
| VacuumEntityFeature.CLEAN_SPOT
| VacuumEntityFeature.FAN_SPEED
| VacuumEntityFeature.LOCATE
| VacuumEntityFeature.PAUSE
| VacuumEntityFeature.RETURN_HOME
| VacuumEntityFeature.SEND_COMMAND
| VacuumEntityFeature.START
| VacuumEntityFeature.STATE
| VacuumEntityFeature.STOP
)
robovac_features = RoboVacEntityFeature.CLEANING_TIME | RoboVacEntityFeature.CLEANING_AREA | RoboVacEntityFeature.DO_NOT_DISTURB | RoboVacEntityFeature.AUTO_RETURN | RoboVacEntityFeature.CONSUMABLES
commands = {
RobovacCommand.START_PAUSE: 2,
RobovacCommand.DIRECTION: {
"code": 3,
"values": ["forward", "back", "left", "right"],
},
RobovacCommand.MODE: {
"code": 5,
"values": ["auto", "SmallRoom", "Spot", "Edge", "Nosweep"],
},
RobovacCommand.STATUS: 15,
RobovacCommand.RETURN_HOME: 101,
RobovacCommand.FAN_SPEED: {
"code": 102,
"values": ["Standard","Turbo","Max","Boost_IQ"],
},
RobovacCommand.LOCATE: 103,
RobovacCommand.BATTERY: 104,
RobovacCommand.ERROR: 106,
# These commands need codes adding
# RobovacCommand.CLEANING_AREA: 0,
# RobovacCommand.CLEANING_TIME: 0,
# RobovacCommand.AUTO_RETURN: 0,
# RobovacCommand.DO_NOT_DISTURB: 0,
# RobovacCommand.CONSUMABLES: 0,
}

View File

@ -0,0 +1,120 @@
from homeassistant.components.vacuum import VacuumEntityFeature
from .base import RoboVacEntityFeature, RobovacCommand
class T2275:
homeassistant_features = (
VacuumEntityFeature.BATTERY
# | VacuumEntityFeature.CLEAN_SPOT
| VacuumEntityFeature.FAN_SPEED
| VacuumEntityFeature.LOCATE
| VacuumEntityFeature.PAUSE
| VacuumEntityFeature.RETURN_HOME
| VacuumEntityFeature.SEND_COMMAND
| VacuumEntityFeature.START
| VacuumEntityFeature.STATE
| VacuumEntityFeature.STOP
# | VacuumEntityFeature.MAP
)
robovac_features = (
# RoboVacEntityFeature.CLEANING_TIME
# | RoboVacEntityFeature.CLEANING_AREA
RoboVacEntityFeature.DO_NOT_DISTURB
# | RoboVacEntityFeature.AUTO_RETURN
# | RoboVacEntityFeature.ROOM
# | RoboVacEntityFeature.ZONE
| RoboVacEntityFeature.BOOST_IQ
# | RoboVacEntityFeature.MAP
# | RoboVacEntityFeature.CONSUMABLES
)
commands = {
RobovacCommand.MODE: { #works (Start Auto and Return dock commands tested)
"code": 152,
"values": ["AggN","AA==","AggG","BBoCCAE=","AggO"],
},
RobovacCommand.STATUS: { #works (status only)
"code": 153,
"values": ["BgoAEAUyAA===","BgoAEAVSAA===","CAoAEAUyAggB","CAoCCAEQBTIA","CAoCCAEQBVIA","CgoCCAEQBTICCAE=","CAoCCAIQBTIA","CAoCCAIQBVIA","CgoCCAIQBTICCAE=","BAoAEAY=","BBAHQgA=","BBADGgA=","BhADGgIIAQ==","AA==","AhAB"],
},
RobovacCommand.DIRECTION: { #untested
"code": 155,
"values": ["Brake", "Forward", "Back", "Left", "Right"],
},
RobovacCommand.START_PAUSE: 156, # True, False #works (status only)
RobovacCommand.DO_NOT_DISTURB: 157, # DgoAEgoKABICCBYaAggI #untested
RobovacCommand.FAN_SPEED: { #works (status and update)
"code": 158,
"values": ["Quiet", "Standard", "Turbo", "Max"],
},
RobovacCommand.BOOST_IQ: 159, # True, False #works (status and update)
RobovacCommand.LOCATE: 160, # True, False #works (status)
# Speaker volume: 161 #works, not yet implemented
RobovacCommand.BATTERY: 163, # int #works (status)
RobovacCommand.CONSUMABLES: 168, #encrypted, not usable
RobovacCommand.RETURN_HOME: 173, #encrypted, not usable
# FgoQMggKAggBEgIQAToECgIIARICCAE=
# FgoQMg4KAggBEggIARj/////DxICCAE=
# FAoQMggKAggBEgIQAToECgIIARIA
# GAoQMggKAggBEgIQAToECgIIARIECAE4AQ==
# GgoQMggKAggBEgIQAToECgIIARIGCAEYATgB
RobovacCommand.ERROR: 177, # #encrypted, few known values
# SIDEBRUSH_STUCK: "FAjwudWorOPszgEaAqURUgQSAqUR"
# ROBOT_STUCK: "FAj+nMu7zuPszgEaAtg2UgQSAtg2"
# IQofCgIIAhICCAIaAggCKgIIAjoCCBugAe7Pqs6M1+zOAQ==
# IQofCgIIAhICCAIaAggCKgIIAjoCCBqgAYPx0a331uzOAQ==
# IQofCgIIBBICCAQaAggEKgIIBDoCCCmgAcSfs6Lo5uzOAQ==
# These commands need codes adding
# RobovacCommand.CLEANING_AREA: 0,
# RobovacCommand.CLEANING_TIME: 0,
# RobovacCommand.AUTO_RETURN: 0,
# Unknown: 151 (true/false)
# Unknown: 154
# DgoKCgAaAggBIgIIARIA
# DAoICgAaAggBIgASAA==
# Unknown: 164
# MBoAIiwKBgi4y/q0BhIECAEQARoMCAESBBgJIB4aAgg+Kg4aDBIKCgIIARIAGgAiAA==
# NAgGEAYaACIsCgYIuMv6tAYSBAgBEAEaDAgBEgQYCSAeGgIIPioOGgwSCgoCCAESABoAIgA=
# Unknown: 167
# FAoAEgcIiEoQbhgEGgcI1EgQbBgC
# FgoCEAESBwiIShBuGAQaBwjUSBBsGAI=
# GAoECDwQARIHCIhKEG4YBBoHCNRIEGwYAg==
# GQoFCLQBEAQSBwiIShBuGAQaBwjUSBBsGAI=
# GwoFCKApEDgSCAiocxCmARgFGggI9HEQpAEYAw==
# Unknown: 171
# AhAB
# Unknown: 176
# MQoAGgBSCBoAIgIIASoAWDJiHwodChFBIG5ldHdvcmsgZm9yIHlvdRABGgYQ4/7/tAY=
# LwoAGgBSCBoAIgIIASoAWFZiHQobChFBIG5ldHdvcmsgZm9yIHlvdRoGEPvagrUG
# LwoAGgBSCBoAIgIIASoAWCJiHQobChFBIG5ldHdvcmsgZm9yIHlvdRoGEK2YgLUG
# LwoAGgBSCBoAIgIIASoAWDBiHQobChFBIG5ldHdvcmsgZm9yIHlvdRoGEK2YgLUG
# Unknown: 178
# DQjRidjfv9bszgESAR8=
# DQiMrPbd+eLszgESAVU=
# DQiW0dXL+uLszgESAR8=
# Cgiv6NbWsePszgE=
# DQjPuorb6eTszgESAR8=
# DQjayd7nsOXszgESASg=
# Unknown: 179
# EBIOKgwIBRACGAEgwYyAtQY=
# FhIUEhIIBRABIFsowYyAtQYw74yAtQY=
# DhIMKgoIBhgCIPvagrUG
# DhIMKgoIBxgCIJLbgrUG
# EBIOKgwIBxADGAIg3eyCtQY=
# EBIOKgwIBxAEGAIgrPGCtQY=
# DhIMKgoICBgCILHxgrUG
# DhIMKgoICBADIIj6grUG
# DhIMKgoICBADIOqMg7UG
# DhIMKgoICBAEIOuMg7UG
# DBIKKggICSCljYO1Bg==
# DhIMKgoICRACIJmcg7UG
# FhIUEhIICRABIBoomZyDtQYw6pyDtQY=
# DhIMIgoICRABGO+cg7UG
# DhIMIgoICRABGLedg7UG
# IRIfCh0ICRgBMPvagrUGOMmdg7UGQKApSDhQO1gBYAdqAA==
# Unknown: 169
# cwoSZXVmeSBDbGVhbiBMNjAgU0VTGhFDODpGRTowRjo3Nzo5NDo5QyIGMS4zLjI0KAVCKDM2NGFjOGNkNjQzZjllMDczZjg4NzlmNGFhOTdkZGE5OGUzMjg5NTRiFggBEgQIAhABGgQIAhABIgIIASoCCAE=
# s \x12eufy Clean L60 SES\x1a\x11C8:FE:0F:77:94:9C"\x061.3.24(\x05B(364ac8cd643f9e073f8879f4aa97dda98e328954b\x16\x08\x01\x12\x04\x08\x02\x10\x01\x1a\x04\x08\x02\x10\x01"\x02\x08\x01*\x02\x08\x01
}

View File

@ -0,0 +1,120 @@
from homeassistant.components.vacuum import VacuumEntityFeature
from .base import RoboVacEntityFeature, RobovacCommand
class T2276:
homeassistant_features = (
VacuumEntityFeature.BATTERY
# | VacuumEntityFeature.CLEAN_SPOT
| VacuumEntityFeature.FAN_SPEED
| VacuumEntityFeature.LOCATE
| VacuumEntityFeature.PAUSE
| VacuumEntityFeature.RETURN_HOME
| VacuumEntityFeature.SEND_COMMAND
| VacuumEntityFeature.START
| VacuumEntityFeature.STATE
| VacuumEntityFeature.STOP
# | VacuumEntityFeature.MAP
)
robovac_features = (
# RoboVacEntityFeature.CLEANING_TIME
# | RoboVacEntityFeature.CLEANING_AREA
RoboVacEntityFeature.DO_NOT_DISTURB
# | RoboVacEntityFeature.AUTO_RETURN
# | RoboVacEntityFeature.ROOM
# | RoboVacEntityFeature.ZONE
| RoboVacEntityFeature.BOOST_IQ
# | RoboVacEntityFeature.MAP
# | RoboVacEntityFeature.CONSUMABLES
)
commands = {
RobovacCommand.MODE: { #works (Start Auto and Return dock commands tested)
"code": 152,
"values": ["AggN","AA==","AggG","BBoCCAE=","AggO"],
},
RobovacCommand.STATUS: { #works (status only)
"code": 153,
"values": ["BgoAEAUyAA===","BgoAEAVSAA===","CAoAEAUyAggB","CAoCCAEQBTIA","CAoCCAEQBVIA","CgoCCAEQBTICCAE=","CAoCCAIQBTIA","CAoCCAIQBVIA","CgoCCAIQBTICCAE=","BAoAEAY=","BBAHQgA=","BBADGgA=","BhADGgIIAQ==","AA==","AhAB"],
},
RobovacCommand.DIRECTION: { #untested
"code": 155,
"values": ["Brake", "Forward", "Back", "Left", "Right"],
},
RobovacCommand.START_PAUSE: 156, # True, False #works (status only)
RobovacCommand.DO_NOT_DISTURB: 157, # DgoAEgoKABICCBYaAggI #untested
RobovacCommand.FAN_SPEED: { #works (status and update)
"code": 158,
"values": ["Quiet", "Standard", "Turbo", "Max"],
},
RobovacCommand.BOOST_IQ: 159, # True, False #works (status and update)
RobovacCommand.LOCATE: 160, # True, False #works (status)
# Speaker volume: 161 #works, not yet implemented
RobovacCommand.BATTERY: 163, # int #works (status)
RobovacCommand.CONSUMABLES: 168, #encrypted, not usable
RobovacCommand.RETURN_HOME: 173, #encrypted, not usable
# FgoQMggKAggBEgIQAToECgIIARICCAE=
# FgoQMg4KAggBEggIARj/////DxICCAE=
# FAoQMggKAggBEgIQAToECgIIARIA
# GAoQMggKAggBEgIQAToECgIIARIECAE4AQ==
# GgoQMggKAggBEgIQAToECgIIARIGCAEYATgB
RobovacCommand.ERROR: 177, # #encrypted, few known values
# SIDEBRUSH_STUCK: "FAjwudWorOPszgEaAqURUgQSAqUR"
# ROBOT_STUCK: "FAj+nMu7zuPszgEaAtg2UgQSAtg2"
# IQofCgIIAhICCAIaAggCKgIIAjoCCBugAe7Pqs6M1+zOAQ==
# IQofCgIIAhICCAIaAggCKgIIAjoCCBqgAYPx0a331uzOAQ==
# IQofCgIIBBICCAQaAggEKgIIBDoCCCmgAcSfs6Lo5uzOAQ==
# These commands need codes adding
# RobovacCommand.CLEANING_AREA: 0,
# RobovacCommand.CLEANING_TIME: 0,
# RobovacCommand.AUTO_RETURN: 0,
# Unknown: 151 (true/false)
# Unknown: 154
# DgoKCgAaAggBIgIIARIA
# DAoICgAaAggBIgASAA==
# Unknown: 164
# MBoAIiwKBgi4y/q0BhIECAEQARoMCAESBBgJIB4aAgg+Kg4aDBIKCgIIARIAGgAiAA==
# NAgGEAYaACIsCgYIuMv6tAYSBAgBEAEaDAgBEgQYCSAeGgIIPioOGgwSCgoCCAESABoAIgA=
# Unknown: 167
# FAoAEgcIiEoQbhgEGgcI1EgQbBgC
# FgoCEAESBwiIShBuGAQaBwjUSBBsGAI=
# GAoECDwQARIHCIhKEG4YBBoHCNRIEGwYAg==
# GQoFCLQBEAQSBwiIShBuGAQaBwjUSBBsGAI=
# GwoFCKApEDgSCAiocxCmARgFGggI9HEQpAEYAw==
# Unknown: 171
# AhAB
# Unknown: 176
# MQoAGgBSCBoAIgIIASoAWDJiHwodChFBIG5ldHdvcmsgZm9yIHlvdRABGgYQ4/7/tAY=
# LwoAGgBSCBoAIgIIASoAWFZiHQobChFBIG5ldHdvcmsgZm9yIHlvdRoGEPvagrUG
# LwoAGgBSCBoAIgIIASoAWCJiHQobChFBIG5ldHdvcmsgZm9yIHlvdRoGEK2YgLUG
# LwoAGgBSCBoAIgIIASoAWDBiHQobChFBIG5ldHdvcmsgZm9yIHlvdRoGEK2YgLUG
# Unknown: 178
# DQjRidjfv9bszgESAR8=
# DQiMrPbd+eLszgESAVU=
# DQiW0dXL+uLszgESAR8=
# Cgiv6NbWsePszgE=
# DQjPuorb6eTszgESAR8=
# DQjayd7nsOXszgESASg=
# Unknown: 179
# EBIOKgwIBRACGAEgwYyAtQY=
# FhIUEhIIBRABIFsowYyAtQYw74yAtQY=
# DhIMKgoIBhgCIPvagrUG
# DhIMKgoIBxgCIJLbgrUG
# EBIOKgwIBxADGAIg3eyCtQY=
# EBIOKgwIBxAEGAIgrPGCtQY=
# DhIMKgoICBgCILHxgrUG
# DhIMKgoICBADIIj6grUG
# DhIMKgoICBADIOqMg7UG
# DhIMKgoICBAEIOuMg7UG
# DBIKKggICSCljYO1Bg==
# DhIMKgoICRACIJmcg7UG
# FhIUEhIICRABIBoomZyDtQYw6pyDtQY=
# DhIMIgoICRABGO+cg7UG
# DhIMIgoICRABGLedg7UG
# IRIfCh0ICRgBMPvagrUGOMmdg7UGQKApSDhQO1gBYAdqAA==
# Unknown: 169
# cwoSZXVmeSBDbGVhbiBMNjAgU0VTGhFDODpGRTowRjo3Nzo5NDo5QyIGMS4zLjI0KAVCKDM2NGFjOGNkNjQzZjllMDczZjg4NzlmNGFhOTdkZGE5OGUzMjg5NTRiFggBEgQIAhABGgQIAhABIgIIASoCCAE=
# s \x12eufy Clean L60 SES\x1a\x11C8:FE:0F:77:94:9C"\x061.3.24(\x05B(364ac8cd643f9e073f8879f4aa97dda98e328954b\x16\x08\x01\x12\x04\x08\x02\x10\x01\x1a\x04\x08\x02\x10\x01"\x02\x08\x01*\x02\x08\x01
}

View File

@ -0,0 +1,120 @@
from homeassistant.components.vacuum import VacuumEntityFeature
from .base import RoboVacEntityFeature, RobovacCommand
class T2277:
homeassistant_features = (
VacuumEntityFeature.BATTERY
# | VacuumEntityFeature.CLEAN_SPOT
| VacuumEntityFeature.FAN_SPEED
| VacuumEntityFeature.LOCATE
| VacuumEntityFeature.PAUSE
| VacuumEntityFeature.RETURN_HOME
| VacuumEntityFeature.SEND_COMMAND
| VacuumEntityFeature.START
| VacuumEntityFeature.STATE
| VacuumEntityFeature.STOP
# | VacuumEntityFeature.MAP
)
robovac_features = (
# RoboVacEntityFeature.CLEANING_TIME
# | RoboVacEntityFeature.CLEANING_AREA
RoboVacEntityFeature.DO_NOT_DISTURB
# | RoboVacEntityFeature.AUTO_RETURN
# | RoboVacEntityFeature.ROOM
# | RoboVacEntityFeature.ZONE
| RoboVacEntityFeature.BOOST_IQ
# | RoboVacEntityFeature.MAP
# | RoboVacEntityFeature.CONSUMABLES
)
commands = {
RobovacCommand.MODE: { #works (Start Auto and Return dock commands tested)
"code": 152,
"values": ["AggN","AA==","AggG","BBoCCAE=","AggO"],
},
RobovacCommand.STATUS: { #works (status only)
"code": 153,
"values": ["BgoAEAUyAA===","BgoAEAVSAA===","CAoAEAUyAggB","CAoCCAEQBTIA","CAoCCAEQBVIA","CgoCCAEQBTICCAE=","CAoCCAIQBTIA","CAoCCAIQBVIA","CgoCCAIQBTICCAE=","BAoAEAY=","BBAHQgA=","BBADGgA=","BhADGgIIAQ==","AA==","AhAB"],
},
RobovacCommand.DIRECTION: { #untested
"code": 155,
"values": ["Brake", "Forward", "Back", "Left", "Right"],
},
RobovacCommand.START_PAUSE: 156, # True, False #works (status only)
RobovacCommand.DO_NOT_DISTURB: 157, # DgoAEgoKABICCBYaAggI #untested
RobovacCommand.FAN_SPEED: { #works (status and update)
"code": 158,
"values": ["Quiet", "Standard", "Turbo", "Max"],
},
RobovacCommand.BOOST_IQ: 159, # True, False #works (status and update)
RobovacCommand.LOCATE: 160, # True, False #works (status)
# Speaker volume: 161 #works, not yet implemented
RobovacCommand.BATTERY: 163, # int #works (status)
RobovacCommand.CONSUMABLES: 168, #encrypted, not usable
RobovacCommand.RETURN_HOME: 173, #encrypted, not usable
# FgoQMggKAggBEgIQAToECgIIARICCAE=
# FgoQMg4KAggBEggIARj/////DxICCAE=
# FAoQMggKAggBEgIQAToECgIIARIA
# GAoQMggKAggBEgIQAToECgIIARIECAE4AQ==
# GgoQMggKAggBEgIQAToECgIIARIGCAEYATgB
RobovacCommand.ERROR: 177, # #encrypted, few known values
# SIDEBRUSH_STUCK: "FAjwudWorOPszgEaAqURUgQSAqUR"
# ROBOT_STUCK: "FAj+nMu7zuPszgEaAtg2UgQSAtg2"
# IQofCgIIAhICCAIaAggCKgIIAjoCCBugAe7Pqs6M1+zOAQ==
# IQofCgIIAhICCAIaAggCKgIIAjoCCBqgAYPx0a331uzOAQ==
# IQofCgIIBBICCAQaAggEKgIIBDoCCCmgAcSfs6Lo5uzOAQ==
# These commands need codes adding
# RobovacCommand.CLEANING_AREA: 0,
# RobovacCommand.CLEANING_TIME: 0,
# RobovacCommand.AUTO_RETURN: 0,
# Unknown: 151 (true/false)
# Unknown: 154
# DgoKCgAaAggBIgIIARIA
# DAoICgAaAggBIgASAA==
# Unknown: 164
# MBoAIiwKBgi4y/q0BhIECAEQARoMCAESBBgJIB4aAgg+Kg4aDBIKCgIIARIAGgAiAA==
# NAgGEAYaACIsCgYIuMv6tAYSBAgBEAEaDAgBEgQYCSAeGgIIPioOGgwSCgoCCAESABoAIgA=
# Unknown: 167
# FAoAEgcIiEoQbhgEGgcI1EgQbBgC
# FgoCEAESBwiIShBuGAQaBwjUSBBsGAI=
# GAoECDwQARIHCIhKEG4YBBoHCNRIEGwYAg==
# GQoFCLQBEAQSBwiIShBuGAQaBwjUSBBsGAI=
# GwoFCKApEDgSCAiocxCmARgFGggI9HEQpAEYAw==
# Unknown: 171
# AhAB
# Unknown: 176
# MQoAGgBSCBoAIgIIASoAWDJiHwodChFBIG5ldHdvcmsgZm9yIHlvdRABGgYQ4/7/tAY=
# LwoAGgBSCBoAIgIIASoAWFZiHQobChFBIG5ldHdvcmsgZm9yIHlvdRoGEPvagrUG
# LwoAGgBSCBoAIgIIASoAWCJiHQobChFBIG5ldHdvcmsgZm9yIHlvdRoGEK2YgLUG
# LwoAGgBSCBoAIgIIASoAWDBiHQobChFBIG5ldHdvcmsgZm9yIHlvdRoGEK2YgLUG
# Unknown: 178
# DQjRidjfv9bszgESAR8=
# DQiMrPbd+eLszgESAVU=
# DQiW0dXL+uLszgESAR8=
# Cgiv6NbWsePszgE=
# DQjPuorb6eTszgESAR8=
# DQjayd7nsOXszgESASg=
# Unknown: 179
# EBIOKgwIBRACGAEgwYyAtQY=
# FhIUEhIIBRABIFsowYyAtQYw74yAtQY=
# DhIMKgoIBhgCIPvagrUG
# DhIMKgoIBxgCIJLbgrUG
# EBIOKgwIBxADGAIg3eyCtQY=
# EBIOKgwIBxAEGAIgrPGCtQY=
# DhIMKgoICBgCILHxgrUG
# DhIMKgoICBADIIj6grUG
# DhIMKgoICBADIOqMg7UG
# DhIMKgoICBAEIOuMg7UG
# DBIKKggICSCljYO1Bg==
# DhIMKgoICRACIJmcg7UG
# FhIUEhIICRABIBoomZyDtQYw6pyDtQY=
# DhIMIgoICRABGO+cg7UG
# DhIMIgoICRABGLedg7UG
# IRIfCh0ICRgBMPvagrUGOMmdg7UGQKApSDhQO1gBYAdqAA==
# Unknown: 169
# cwoSZXVmeSBDbGVhbiBMNjAgU0VTGhFDODpGRTowRjo3Nzo5NDo5QyIGMS4zLjI0KAVCKDM2NGFjOGNkNjQzZjllMDczZjg4NzlmNGFhOTdkZGE5OGUzMjg5NTRiFggBEgQIAhABGgQIAhABIgIIASoCCAE=
# s \x12eufy Clean L60 SES\x1a\x11C8:FE:0F:77:94:9C"\x061.3.24(\x05B(364ac8cd643f9e073f8879f4aa97dda98e328954b\x16\x08\x01\x12\x04\x08\x02\x10\x01\x1a\x04\x08\x02\x10\x01"\x02\x08\x01*\x02\x08\x01
}

View File

@ -0,0 +1,120 @@
from homeassistant.components.vacuum import VacuumEntityFeature
from .base import RoboVacEntityFeature, RobovacCommand
class T2278:
homeassistant_features = (
VacuumEntityFeature.BATTERY
# | VacuumEntityFeature.CLEAN_SPOT
| VacuumEntityFeature.FAN_SPEED
| VacuumEntityFeature.LOCATE
| VacuumEntityFeature.PAUSE
| VacuumEntityFeature.RETURN_HOME
| VacuumEntityFeature.SEND_COMMAND
| VacuumEntityFeature.START
| VacuumEntityFeature.STATE
| VacuumEntityFeature.STOP
# | VacuumEntityFeature.MAP
)
robovac_features = (
# RoboVacEntityFeature.CLEANING_TIME
# | RoboVacEntityFeature.CLEANING_AREA
RoboVacEntityFeature.DO_NOT_DISTURB
# | RoboVacEntityFeature.AUTO_RETURN
# | RoboVacEntityFeature.ROOM
# | RoboVacEntityFeature.ZONE
| RoboVacEntityFeature.BOOST_IQ
# | RoboVacEntityFeature.MAP
# | RoboVacEntityFeature.CONSUMABLES
)
commands = {
RobovacCommand.MODE: { #works (Start Auto and Return dock commands tested)
"code": 152,
"values": ["AggN","AA==","AggG","BBoCCAE=","AggO"],
},
RobovacCommand.STATUS: { #works (status only)
"code": 153,
"values": ["BgoAEAUyAA===","BgoAEAVSAA===","CAoAEAUyAggB","CAoCCAEQBTIA","CAoCCAEQBVIA","CgoCCAEQBTICCAE=","CAoCCAIQBTIA","CAoCCAIQBVIA","CgoCCAIQBTICCAE=","BAoAEAY=","BBAHQgA=","BBADGgA=","BhADGgIIAQ==","AA==","AhAB"],
},
RobovacCommand.DIRECTION: { #untested
"code": 155,
"values": ["Brake", "Forward", "Back", "Left", "Right"],
},
RobovacCommand.START_PAUSE: 156, # True, False #works (status only)
RobovacCommand.DO_NOT_DISTURB: 157, # DgoAEgoKABICCBYaAggI #untested
RobovacCommand.FAN_SPEED: { #works (status and update)
"code": 158,
"values": ["Quiet", "Standard", "Turbo", "Max"],
},
RobovacCommand.BOOST_IQ: 159, # True, False #works (status and update)
RobovacCommand.LOCATE: 160, # True, False #works (status)
# Speaker volume: 161 #works, not yet implemented
RobovacCommand.BATTERY: 163, # int #works (status)
RobovacCommand.CONSUMABLES: 168, #encrypted, not usable
RobovacCommand.RETURN_HOME: 173, #encrypted, not usable
# FgoQMggKAggBEgIQAToECgIIARICCAE=
# FgoQMg4KAggBEggIARj/////DxICCAE=
# FAoQMggKAggBEgIQAToECgIIARIA
# GAoQMggKAggBEgIQAToECgIIARIECAE4AQ==
# GgoQMggKAggBEgIQAToECgIIARIGCAEYATgB
RobovacCommand.ERROR: 177, # #encrypted, few known values
# SIDEBRUSH_STUCK: "FAjwudWorOPszgEaAqURUgQSAqUR"
# ROBOT_STUCK: "FAj+nMu7zuPszgEaAtg2UgQSAtg2"
# IQofCgIIAhICCAIaAggCKgIIAjoCCBugAe7Pqs6M1+zOAQ==
# IQofCgIIAhICCAIaAggCKgIIAjoCCBqgAYPx0a331uzOAQ==
# IQofCgIIBBICCAQaAggEKgIIBDoCCCmgAcSfs6Lo5uzOAQ==
# These commands need codes adding
# RobovacCommand.CLEANING_AREA: 0,
# RobovacCommand.CLEANING_TIME: 0,
# RobovacCommand.AUTO_RETURN: 0,
# Unknown: 151 (true/false)
# Unknown: 154
# DgoKCgAaAggBIgIIARIA
# DAoICgAaAggBIgASAA==
# Unknown: 164
# MBoAIiwKBgi4y/q0BhIECAEQARoMCAESBBgJIB4aAgg+Kg4aDBIKCgIIARIAGgAiAA==
# NAgGEAYaACIsCgYIuMv6tAYSBAgBEAEaDAgBEgQYCSAeGgIIPioOGgwSCgoCCAESABoAIgA=
# Unknown: 167
# FAoAEgcIiEoQbhgEGgcI1EgQbBgC
# FgoCEAESBwiIShBuGAQaBwjUSBBsGAI=
# GAoECDwQARIHCIhKEG4YBBoHCNRIEGwYAg==
# GQoFCLQBEAQSBwiIShBuGAQaBwjUSBBsGAI=
# GwoFCKApEDgSCAiocxCmARgFGggI9HEQpAEYAw==
# Unknown: 171
# AhAB
# Unknown: 176
# MQoAGgBSCBoAIgIIASoAWDJiHwodChFBIG5ldHdvcmsgZm9yIHlvdRABGgYQ4/7/tAY=
# LwoAGgBSCBoAIgIIASoAWFZiHQobChFBIG5ldHdvcmsgZm9yIHlvdRoGEPvagrUG
# LwoAGgBSCBoAIgIIASoAWCJiHQobChFBIG5ldHdvcmsgZm9yIHlvdRoGEK2YgLUG
# LwoAGgBSCBoAIgIIASoAWDBiHQobChFBIG5ldHdvcmsgZm9yIHlvdRoGEK2YgLUG
# Unknown: 178
# DQjRidjfv9bszgESAR8=
# DQiMrPbd+eLszgESAVU=
# DQiW0dXL+uLszgESAR8=
# Cgiv6NbWsePszgE=
# DQjPuorb6eTszgESAR8=
# DQjayd7nsOXszgESASg=
# Unknown: 179
# EBIOKgwIBRACGAEgwYyAtQY=
# FhIUEhIIBRABIFsowYyAtQYw74yAtQY=
# DhIMKgoIBhgCIPvagrUG
# DhIMKgoIBxgCIJLbgrUG
# EBIOKgwIBxADGAIg3eyCtQY=
# EBIOKgwIBxAEGAIgrPGCtQY=
# DhIMKgoICBgCILHxgrUG
# DhIMKgoICBADIIj6grUG
# DhIMKgoICBADIOqMg7UG
# DhIMKgoICBAEIOuMg7UG
# DBIKKggICSCljYO1Bg==
# DhIMKgoICRACIJmcg7UG
# FhIUEhIICRABIBoomZyDtQYw6pyDtQY=
# DhIMIgoICRABGO+cg7UG
# DhIMIgoICRABGLedg7UG
# IRIfCh0ICRgBMPvagrUGOMmdg7UGQKApSDhQO1gBYAdqAA==
# Unknown: 169
# cwoSZXVmeSBDbGVhbiBMNjAgU0VTGhFDODpGRTowRjo3Nzo5NDo5QyIGMS4zLjI0KAVCKDM2NGFjOGNkNjQzZjllMDczZjg4NzlmNGFhOTdkZGE5OGUzMjg5NTRiFggBEgQIAhABGgQIAhABIgIIASoCCAE=
# s \x12eufy Clean L60 SES\x1a\x11C8:FE:0F:77:94:9C"\x061.3.24(\x05B(364ac8cd643f9e073f8879f4aa97dda98e328954b\x16\x08\x01\x12\x04\x08\x02\x10\x01\x1a\x04\x08\x02\x10\x01"\x02\x08\x01*\x02\x08\x01
}

View File

@ -0,0 +1,120 @@
from homeassistant.components.vacuum import VacuumEntityFeature
from .base import RoboVacEntityFeature, RobovacCommand
class T2320:
homeassistant_features = (
VacuumEntityFeature.BATTERY
# | VacuumEntityFeature.CLEAN_SPOT
| VacuumEntityFeature.FAN_SPEED
| VacuumEntityFeature.LOCATE
| VacuumEntityFeature.PAUSE
| VacuumEntityFeature.RETURN_HOME
| VacuumEntityFeature.SEND_COMMAND
| VacuumEntityFeature.START
| VacuumEntityFeature.STATE
| VacuumEntityFeature.STOP
# | VacuumEntityFeature.MAP
)
robovac_features = (
# RoboVacEntityFeature.CLEANING_TIME
# | RoboVacEntityFeature.CLEANING_AREA
RoboVacEntityFeature.DO_NOT_DISTURB
# | RoboVacEntityFeature.AUTO_RETURN
# | RoboVacEntityFeature.ROOM
# | RoboVacEntityFeature.ZONE
| RoboVacEntityFeature.BOOST_IQ
# | RoboVacEntityFeature.MAP
# | RoboVacEntityFeature.CONSUMABLES
)
commands = {
RobovacCommand.MODE: { #works (Start Auto and Return dock commands tested)
"code": 152,
"values": ["AggN","AA==","AggG","BBoCCAE=","AggO"],
},
RobovacCommand.STATUS: { #works (status only)
"code": 153,
"values": ["BgoAEAUyAA===","BgoAEAVSAA===","CAoAEAUyAggB","CAoCCAEQBTIA","CAoCCAEQBVIA","CgoCCAEQBTICCAE=","CAoCCAIQBTIA","CAoCCAIQBVIA","CgoCCAIQBTICCAE=","BAoAEAY=","BBAHQgA=","BBADGgA=","BhADGgIIAQ==","AA==","AhAB"],
},
RobovacCommand.DIRECTION: { #untested
"code": 155,
"values": ["Brake", "Forward", "Back", "Left", "Right"],
},
RobovacCommand.START_PAUSE: 156, # True, False #works (status only)
RobovacCommand.DO_NOT_DISTURB: 157, # DgoAEgoKABICCBYaAggI #untested
RobovacCommand.FAN_SPEED: { #works (status and update)
"code": 158,
"values": ["Quiet", "Standard", "Turbo", "Max"],
},
RobovacCommand.BOOST_IQ: 159, # True, False #works (status and update)
RobovacCommand.LOCATE: 160, # True, False #works (status)
# Speaker volume: 161 #works, not yet implemented
RobovacCommand.BATTERY: 163, # int #works (status)
RobovacCommand.CONSUMABLES: 168, #encrypted, not usable
RobovacCommand.RETURN_HOME: 173, #encrypted, not usable
# FgoQMggKAggBEgIQAToECgIIARICCAE=
# FgoQMg4KAggBEggIARj/////DxICCAE=
# FAoQMggKAggBEgIQAToECgIIARIA
# GAoQMggKAggBEgIQAToECgIIARIECAE4AQ==
# GgoQMggKAggBEgIQAToECgIIARIGCAEYATgB
RobovacCommand.ERROR: 177, # #encrypted, few known values
# SIDEBRUSH_STUCK: "FAjwudWorOPszgEaAqURUgQSAqUR"
# ROBOT_STUCK: "FAj+nMu7zuPszgEaAtg2UgQSAtg2"
# IQofCgIIAhICCAIaAggCKgIIAjoCCBugAe7Pqs6M1+zOAQ==
# IQofCgIIAhICCAIaAggCKgIIAjoCCBqgAYPx0a331uzOAQ==
# IQofCgIIBBICCAQaAggEKgIIBDoCCCmgAcSfs6Lo5uzOAQ==
# These commands need codes adding
# RobovacCommand.CLEANING_AREA: 0,
# RobovacCommand.CLEANING_TIME: 0,
# RobovacCommand.AUTO_RETURN: 0,
# Unknown: 151 (true/false)
# Unknown: 154
# DgoKCgAaAggBIgIIARIA
# DAoICgAaAggBIgASAA==
# Unknown: 164
# MBoAIiwKBgi4y/q0BhIECAEQARoMCAESBBgJIB4aAgg+Kg4aDBIKCgIIARIAGgAiAA==
# NAgGEAYaACIsCgYIuMv6tAYSBAgBEAEaDAgBEgQYCSAeGgIIPioOGgwSCgoCCAESABoAIgA=
# Unknown: 167
# FAoAEgcIiEoQbhgEGgcI1EgQbBgC
# FgoCEAESBwiIShBuGAQaBwjUSBBsGAI=
# GAoECDwQARIHCIhKEG4YBBoHCNRIEGwYAg==
# GQoFCLQBEAQSBwiIShBuGAQaBwjUSBBsGAI=
# GwoFCKApEDgSCAiocxCmARgFGggI9HEQpAEYAw==
# Unknown: 171
# AhAB
# Unknown: 176
# MQoAGgBSCBoAIgIIASoAWDJiHwodChFBIG5ldHdvcmsgZm9yIHlvdRABGgYQ4/7/tAY=
# LwoAGgBSCBoAIgIIASoAWFZiHQobChFBIG5ldHdvcmsgZm9yIHlvdRoGEPvagrUG
# LwoAGgBSCBoAIgIIASoAWCJiHQobChFBIG5ldHdvcmsgZm9yIHlvdRoGEK2YgLUG
# LwoAGgBSCBoAIgIIASoAWDBiHQobChFBIG5ldHdvcmsgZm9yIHlvdRoGEK2YgLUG
# Unknown: 178
# DQjRidjfv9bszgESAR8=
# DQiMrPbd+eLszgESAVU=
# DQiW0dXL+uLszgESAR8=
# Cgiv6NbWsePszgE=
# DQjPuorb6eTszgESAR8=
# DQjayd7nsOXszgESASg=
# Unknown: 179
# EBIOKgwIBRACGAEgwYyAtQY=
# FhIUEhIIBRABIFsowYyAtQYw74yAtQY=
# DhIMKgoIBhgCIPvagrUG
# DhIMKgoIBxgCIJLbgrUG
# EBIOKgwIBxADGAIg3eyCtQY=
# EBIOKgwIBxAEGAIgrPGCtQY=
# DhIMKgoICBgCILHxgrUG
# DhIMKgoICBADIIj6grUG
# DhIMKgoICBADIOqMg7UG
# DhIMKgoICBAEIOuMg7UG
# DBIKKggICSCljYO1Bg==
# DhIMKgoICRACIJmcg7UG
# FhIUEhIICRABIBoomZyDtQYw6pyDtQY=
# DhIMIgoICRABGO+cg7UG
# DhIMIgoICRABGLedg7UG
# IRIfCh0ICRgBMPvagrUGOMmdg7UGQKApSDhQO1gBYAdqAA==
# Unknown: 169
# cwoSZXVmeSBDbGVhbiBMNjAgU0VTGhFDODpGRTowRjo3Nzo5NDo5QyIGMS4zLjI0KAVCKDM2NGFjOGNkNjQzZjllMDczZjg4NzlmNGFhOTdkZGE5OGUzMjg5NTRiFggBEgQIAhABGgQIAhABIgIIASoCCAE=
# s \x12eufy Clean L60 SES\x1a\x11C8:FE:0F:77:94:9C"\x061.3.24(\x05B(364ac8cd643f9e073f8879f4aa97dda98e328954b\x16\x08\x01\x12\x04\x08\x02\x10\x01\x1a\x04\x08\x02\x10\x01"\x02\x08\x01*\x02\x08\x01
}

View File

@ -0,0 +1,120 @@
from homeassistant.components.vacuum import VacuumEntityFeature
from .base import RoboVacEntityFeature, RobovacCommand
class T2351:
homeassistant_features = (
VacuumEntityFeature.BATTERY
# | VacuumEntityFeature.CLEAN_SPOT
| VacuumEntityFeature.FAN_SPEED
| VacuumEntityFeature.LOCATE
| VacuumEntityFeature.PAUSE
| VacuumEntityFeature.RETURN_HOME
| VacuumEntityFeature.SEND_COMMAND
| VacuumEntityFeature.START
| VacuumEntityFeature.STATE
| VacuumEntityFeature.STOP
# | VacuumEntityFeature.MAP
)
robovac_features = (
# RoboVacEntityFeature.CLEANING_TIME
# | RoboVacEntityFeature.CLEANING_AREA
RoboVacEntityFeature.DO_NOT_DISTURB
# | RoboVacEntityFeature.AUTO_RETURN
# | RoboVacEntityFeature.ROOM
# | RoboVacEntityFeature.ZONE
| RoboVacEntityFeature.BOOST_IQ
# | RoboVacEntityFeature.MAP
# | RoboVacEntityFeature.CONSUMABLES
)
commands = {
RobovacCommand.MODE: { #works (Start Auto and Return dock commands tested)
"code": 152,
"values": ["AggN","AA==","AggG","BBoCCAE=","AggO"],
},
RobovacCommand.STATUS: { #works (status only)
"code": 153,
"values": ["BgoAEAUyAA===","BgoAEAVSAA===","CAoAEAUyAggB","CAoCCAEQBTIA","CAoCCAEQBVIA","CgoCCAEQBTICCAE=","CAoCCAIQBTIA","CAoCCAIQBVIA","CgoCCAIQBTICCAE=","BAoAEAY=","BBAHQgA=","BBADGgA=","BhADGgIIAQ==","AA==","AhAB"],
},
RobovacCommand.DIRECTION: { #untested
"code": 155,
"values": ["Brake", "Forward", "Back", "Left", "Right"],
},
RobovacCommand.START_PAUSE: 156, # True, False #works (status only)
RobovacCommand.DO_NOT_DISTURB: 157, # DgoAEgoKABICCBYaAggI #untested
RobovacCommand.FAN_SPEED: { #works (status and update)
"code": 158,
"values": ["Quiet", "Standard", "Turbo", "Max"],
},
RobovacCommand.BOOST_IQ: 159, # True, False #works (status and update)
RobovacCommand.LOCATE: 160, # True, False #works (status)
# Speaker volume: 161 #works, not yet implemented
RobovacCommand.BATTERY: 163, # int #works (status)
RobovacCommand.CONSUMABLES: 168, #encrypted, not usable
RobovacCommand.RETURN_HOME: 173, #encrypted, not usable
# FgoQMggKAggBEgIQAToECgIIARICCAE=
# FgoQMg4KAggBEggIARj/////DxICCAE=
# FAoQMggKAggBEgIQAToECgIIARIA
# GAoQMggKAggBEgIQAToECgIIARIECAE4AQ==
# GgoQMggKAggBEgIQAToECgIIARIGCAEYATgB
RobovacCommand.ERROR: 177, # #encrypted, few known values
# SIDEBRUSH_STUCK: "FAjwudWorOPszgEaAqURUgQSAqUR"
# ROBOT_STUCK: "FAj+nMu7zuPszgEaAtg2UgQSAtg2"
# IQofCgIIAhICCAIaAggCKgIIAjoCCBugAe7Pqs6M1+zOAQ==
# IQofCgIIAhICCAIaAggCKgIIAjoCCBqgAYPx0a331uzOAQ==
# IQofCgIIBBICCAQaAggEKgIIBDoCCCmgAcSfs6Lo5uzOAQ==
# These commands need codes adding
# RobovacCommand.CLEANING_AREA: 0,
# RobovacCommand.CLEANING_TIME: 0,
# RobovacCommand.AUTO_RETURN: 0,
# Unknown: 151 (true/false)
# Unknown: 154
# DgoKCgAaAggBIgIIARIA
# DAoICgAaAggBIgASAA==
# Unknown: 164
# MBoAIiwKBgi4y/q0BhIECAEQARoMCAESBBgJIB4aAgg+Kg4aDBIKCgIIARIAGgAiAA==
# NAgGEAYaACIsCgYIuMv6tAYSBAgBEAEaDAgBEgQYCSAeGgIIPioOGgwSCgoCCAESABoAIgA=
# Unknown: 167
# FAoAEgcIiEoQbhgEGgcI1EgQbBgC
# FgoCEAESBwiIShBuGAQaBwjUSBBsGAI=
# GAoECDwQARIHCIhKEG4YBBoHCNRIEGwYAg==
# GQoFCLQBEAQSBwiIShBuGAQaBwjUSBBsGAI=
# GwoFCKApEDgSCAiocxCmARgFGggI9HEQpAEYAw==
# Unknown: 171
# AhAB
# Unknown: 176
# MQoAGgBSCBoAIgIIASoAWDJiHwodChFBIG5ldHdvcmsgZm9yIHlvdRABGgYQ4/7/tAY=
# LwoAGgBSCBoAIgIIASoAWFZiHQobChFBIG5ldHdvcmsgZm9yIHlvdRoGEPvagrUG
# LwoAGgBSCBoAIgIIASoAWCJiHQobChFBIG5ldHdvcmsgZm9yIHlvdRoGEK2YgLUG
# LwoAGgBSCBoAIgIIASoAWDBiHQobChFBIG5ldHdvcmsgZm9yIHlvdRoGEK2YgLUG
# Unknown: 178
# DQjRidjfv9bszgESAR8=
# DQiMrPbd+eLszgESAVU=
# DQiW0dXL+uLszgESAR8=
# Cgiv6NbWsePszgE=
# DQjPuorb6eTszgESAR8=
# DQjayd7nsOXszgESASg=
# Unknown: 179
# EBIOKgwIBRACGAEgwYyAtQY=
# FhIUEhIIBRABIFsowYyAtQYw74yAtQY=
# DhIMKgoIBhgCIPvagrUG
# DhIMKgoIBxgCIJLbgrUG
# EBIOKgwIBxADGAIg3eyCtQY=
# EBIOKgwIBxAEGAIgrPGCtQY=
# DhIMKgoICBgCILHxgrUG
# DhIMKgoICBADIIj6grUG
# DhIMKgoICBADIOqMg7UG
# DhIMKgoICBAEIOuMg7UG
# DBIKKggICSCljYO1Bg==
# DhIMKgoICRACIJmcg7UG
# FhIUEhIICRABIBoomZyDtQYw6pyDtQY=
# DhIMIgoICRABGO+cg7UG
# DhIMIgoICRABGLedg7UG
# IRIfCh0ICRgBMPvagrUGOMmdg7UGQKApSDhQO1gBYAdqAA==
# Unknown: 169
# cwoSZXVmeSBDbGVhbiBMNjAgU0VTGhFDODpGRTowRjo3Nzo5NDo5QyIGMS4zLjI0KAVCKDM2NGFjOGNkNjQzZjllMDczZjg4NzlmNGFhOTdkZGE5OGUzMjg5NTRiFggBEgQIAhABGgQIAhABIgIIASoCCAE=
# s \x12eufy Clean L60 SES\x1a\x11C8:FE:0F:77:94:9C"\x061.3.24(\x05B(364ac8cd643f9e073f8879f4aa97dda98e328954b\x16\x08\x01\x12\x04\x08\x02\x10\x01\x1a\x04\x08\x02\x10\x01"\x02\x08\x01*\x02\x08\x01
}

View File

@ -0,0 +1,83 @@
from .T1250 import T1250
from .T2103 import T2103
from .T2117 import T2117
from .T2118 import T2118
from .T2119 import T2119
from .T2120 import T2120
from .T2123 import T2123
from .T2128 import T2128
from .T2130 import T2130
from .T2132 import T2132
from .T2150 import T2150
from .T2181 import T2181
from .T2182 import T2182
from .T2190 import T2190
from .T2192 import T2192
from .T2193 import T2193
from .T2194 import T2194
from .T2250 import T2250
from .T2251 import T2251
from .T2252 import T2252
from .T2253 import T2253
from .T2254 import T2254
from .T2255 import T2255
from .T2256 import T2256
from .T2257 import T2257
from .T2258 import T2258
from .T2259 import T2259
from .T2261 import T2261
from .T2262 import T2262
from .T2266 import T2266
from .T2267 import T2267
from .T2270 import T2270
from .T2272 import T2272
from .T2273 import T2273
from .T2275 import T2275
from .T2276 import T2276
from .T2277 import T2277
from .T2278 import T2278
from .T2320 import T2320
from .T2351 import T2351
ROBOVAC_MODELS = {
"T2103": T2103,
"T2117": T2117,
"T2118": T2118,
"T2119": T2119,
"T2120": T2120,
"T2123": T2123,
"T2128": T2128,
"T2130": T2130,
"T2132": T2132,
"T1250": T1250,
"T2250": T2250,
"T2251": T2251,
"T2252": T2252,
"T2253": T2253,
"T2254": T2254,
"T2150": T2150,
"T2255": T2255,
"T2256": T2256,
"T2257": T2257,
"T2258": T2258,
"T2259": T2259,
"T2270": T2270,
"T2272": T2272,
"T2273": T2273,
"T2181": T2181,
"T2182": T2182,
"T2190": T2190,
"T2192": T2192,
"T2193": T2193,
"T2194": T2194,
"T2267": T2267,
"T2277": T2277,
"T2278": T2278,
"T2261": T2261,
"T2262": T2262,
"T2266": T2266,
"T2320": T2320,
"T2351": T2351,
"T2275": T2275,
"T2276": T2276,
}

View File

@ -0,0 +1,35 @@
from enum import IntEnum, StrEnum
class RoboVacEntityFeature(IntEnum):
"""Supported features of the RoboVac entity."""
EDGE = 1
SMALL_ROOM = 2
CLEANING_TIME = 4
CLEANING_AREA = 8
DO_NOT_DISTURB = 16
AUTO_RETURN = 32
CONSUMABLES = 64
ROOM = 128
ZONE = 256
MAP = 512
BOOST_IQ = 1024
class RobovacCommand(StrEnum):
START_PAUSE = "start_pause"
DIRECTION = "direction"
MODE = "mode"
STATUS = "status"
RETURN_HOME = "return_home"
FAN_SPEED = "fan_speed"
LOCATE = "locate"
BATTERY = "battery"
ERROR = "error"
CLEANING_AREA = "cleaning_area"
CLEANING_TIME = "cleaning_time"
AUTO_RETURN = "auto_return"
DO_NOT_DISTURB = "do_not_disturb"
BOOST_IQ = "boost_iq"
CONSUMABLES = "consumables"

4
hacs.json Normal file
View File

@ -0,0 +1,4 @@
{
"name": "Eufy RoboVac",
"render_readme": true
}

6041
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

6
package.json Normal file
View File

@ -0,0 +1,6 @@
{
"devDependencies": {
"conventional-changelog-conventionalcommits": "^7.0.2",
"semantic-release": "^23.0.2"
}
}

5
requirements.txt Normal file
View File

@ -0,0 +1,5 @@
cryptography==41.0.4
homeassistant==2023.8.4
Requests==2.31.0
setuptools==68.0.0
voluptuous==0.13.1

41
setup.py Normal file
View File

@ -0,0 +1,41 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# Copyright 2022 Brendan McCluskey
#
# 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="robovac",
version="1.0",
author="Luke Morrigan",
url="http://github.com/codefoodpixels/robovac",
packages=find_packages(),
scripts=[],
description="Python API for controlling Eufy Robovac vacuum cleaners",
classifiers=[
"Development Status :: 4 - Beta",
"Intended Audience :: Developers",
"License :: OSI Approved :: Apache Software License",
"Operating System :: OS Independent",
"Programming Language :: Python",
],
)