Compare commits

..

26 Commits

Author SHA1 Message Date
Luke Bonaccorsi f397319070 fix: log eufy device info when device not found on tuya 2024-03-22 23:45:02 +00:00
Luke Morrigan 7e60ecd2b4
Revert "Fix the missing error key" (#69)
* Revert "Fix set fan speed"

This reverts commit 8676cb3fad.

* Revert "Add debug"

This reverts commit 94ae2b55ea.

* Revert "Add debug infos"

This reverts commit 4f1bdb3bac.

* Revert "Fix the missing error key"

This reverts commit 8e6e311bfc.

* fix: re-add consumables code
2024-03-22 00:36:38 +00:00
Luke Bonaccorsi c687f111eb fix: fix reference to incorrect key 2024-03-20 01:17:01 +00:00
Luke Bonaccorsi b180896a9c chore: fix releaserc 2024-03-08 14:30:54 +00:00
Luke Bonaccorsi 2968d722f8 fix: add more routes for trying to find the tuya region 2024-03-08 14:28:03 +00:00
Luke Bonaccorsi cdcad837b7 chore: add ability to create beta releases [skip ci] 2024-03-06 12:35:55 +00:00
Luke Bonaccorsi 2c741fe32e fix: print stack trace on connection reset 2024-03-04 01:07:44 +00:00
Luke Bonaccorsi 3dd4a7b0e0 fix: add extra logging 2024-03-03 04:18:14 +00:00
Luke Bonaccorsi c251501aed fix: force disconnect on connection reset 2024-03-01 16:53:48 +00:00
Luke Bonaccorsi 0b18494fb1 fix: send eof to reader when disconnecting 2024-03-01 14:28:23 +00:00
Luke Bonaccorsi ac0dbdd11a fix: missed underscores 🤦‍♂️ 2024-02-28 18:32:21 +00:00
Luke Bonaccorsi 96a155d378 fix: adjust times for refresh, ping and timeout 2024-02-27 14:39:01 +00:00
Luke Bonaccorsi 3e2b923255 fix: fixed issue with multiple ping loops running 2024-02-26 15:26:23 +00:00
Luke Bonaccorsi 79892aa98f fix: add better logging when unexpected error is recieved during config 2024-02-25 01:59:12 +00:00
Luke Bonaccorsi a8601bbe90 chore: add release workflow [skip ci] 2024-02-25 01:39:35 +00:00
Luke Bonaccorsi 88ef4a6e25 feat: rewrite network code to make it more linear and handle being offline better 2024-02-25 01:13:15 +00:00
Dennis Melzer 8676cb3fad Fix set fan speed 2024-01-13 23:10:17 +00:00
Dennis Melzer 94ae2b55ea Add debug 2024-01-13 23:10:17 +00:00
Dennis Melzer 4f1bdb3bac Add debug infos 2024-01-13 23:10:17 +00:00
Dennis Melzer 8e6e311bfc Fix the missing error key 2024-01-13 23:10:17 +00:00
Rusty Myers 19aefa8e65
Merge pull request #35 from jonathanrobichaud4/main
Update to support G35 (T2254)
2024-01-13 13:47:26 -05:00
Jonathan Robichaud 715be42d93 Update to support G35 (T2254)
The G35 is the same as the G35+ (T2270) Minus the automatic empty station. I Haven't added it to HAS_CONSUMABLES or HAS_MAP_FEATURE as I'm not sure if it supports them but it does support basic operation and has been working stably for me for around a month!
2023-10-16 20:04:39 -03:00
Luke Bonaccorsi ae189ce422 Add check for if payload is set on state message 2023-10-05 16:29:22 +01:00
dependabot[bot] abb8285e2f Bump cryptography from 41.0.3 to 41.0.4
Bumps [cryptography](https://github.com/pyca/cryptography) from 41.0.3 to 41.0.4.
- [Changelog](https://github.com/pyca/cryptography/blob/main/CHANGELOG.rst)
- [Commits](https://github.com/pyca/cryptography/compare/41.0.3...41.0.4)

---
updated-dependencies:
- dependency-name: cryptography
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-09-29 18:11:50 +01:00
Luke Bonaccorsi fa9009e43b Add sensor for battery 2023-09-29 18:10:20 +01:00
Luke Bonaccorsi c342eefc16 Ensure translations are in strings.json 2023-09-29 17:54:14 +01:00
19 changed files with 6744 additions and 199 deletions

View File

@ -4,38 +4,28 @@ name: CI
# Controls when the workflow will run
on:
# Triggers the workflow on push or pull request events but only for the "main" branch
push:
branches: [ "main" ]
pull_request:
branches: [ "main" ]
# Triggers the workflow on push or pull request events but only for the "main" branch
push:
branches: ["main"]
pull_request:
branches: ["main"]
# Allows you to run this workflow manually from the Actions tab
workflow_dispatch:
# Allows you to run this workflow manually from the Actions tab
workflow_dispatch:
# A workflow run is made up of one or more jobs that can run sequentially or in parallel
jobs:
# This workflow contains a single job called "build"
build:
# The type of runner that the job will run on
runs-on: ubuntu-latest
# This workflow contains a single job called "build"
build:
# The type of runner that the job will run on
runs-on: ubuntu-latest
# Steps represent a sequence of tasks that will be executed as part of the job
steps:
# Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it
- uses: actions/checkout@v3
- uses: "home-assistant/actions/hassfest@master"
- name: HACS Action
uses: "hacs/action@main"
with:
category: "integration"
# Runs a single command using the runners shell
- name: Run a one-line script
run: echo Hello, world!
# Runs a set of commands using the runners shell
- name: Run a multi-line script
run: |
echo Add other actions to build,
echo test, and deploy your project.
# Steps represent a sequence of tasks that will be executed as part of the job
steps:
# Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it
- uses: actions/checkout@v3
- uses: "home-assistant/actions/hassfest@master"
- name: HACS Action
uses: "hacs/action@main"
with:
category: "integration"

19
.github/workflows/release.yaml vendored Normal file
View File

@ -0,0 +1,19 @@
on:
push:
branches:
- main
workflow_dispatch:
jobs:
create-release:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version-file: ".nvmrc"
- run: npm ci
- name: Release
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: npx semantic-release

1
.gitignore vendored Normal file
View File

@ -0,0 +1 @@
/node_modules

1
.nvmrc Normal file
View File

@ -0,0 +1 @@
20

20
.releaserc Normal file
View File

@ -0,0 +1,20 @@
{
"branches": [
"main",
{
"name": "*",
"channel": "beta",
"prerelease": true
}
],
"plugins": [
[
"@semantic-release/commit-analyzer",
{
"preset": "conventionalcommits"
}
],
"@semantic-release/release-notes-generator",
"@semantic-release/github"
]
}

View File

@ -24,12 +24,12 @@ from .const import CONF_VACS, DOMAIN
from .tuyalocaldiscovery import TuyaLocalDiscovery
PLATFORM = Platform.VACUUM
PLATFORMS = [Platform.VACUUM, Platform.SENSOR]
_LOGGER = logging.getLogger(__name__)
async def async_setup(hass, entry) -> bool:
hass.data.setdefault(DOMAIN, {})
hass.data.setdefault(DOMAIN, {CONF_VACS:{}})
async def update_device(device):
entry = async_get_config_entry_for_device(hass, device["gwId"])
@ -70,15 +70,15 @@ 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_setup(entry, PLATFORM)
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_forward_entry_unload(
entry, PLATFORM
if unload_ok := await hass.config_entries.async_unload_platforms(
entry, PLATFORMS
):
"""Nothing"""
return unload_ok

View File

@ -14,6 +14,7 @@
"""Config flow for Eufy Robovac integration."""
from __future__ import annotations
import json
import logging
from typing import Any, Optional
@ -39,10 +40,17 @@ from homeassistant.const import (
CONF_IP_ADDRESS,
CONF_DESCRIPTION,
CONF_MAC,
CONF_LOCATION,
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
@ -88,15 +96,41 @@ def get_eufy_vacuums(self):
settings_response = response.json()
self[CONF_CLIENT_ID] = user_response["user_info"]["id"]
self[CONF_REGION] = settings_response["setting"]["home_setting"]["tuya_home"][
"tuya_region_code"
]
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["items"]
@ -105,7 +139,6 @@ def get_eufy_vacuums(self):
if item["device"]["product"]["appliance"] == "Cleaning":
try:
device = tuya_client.get_device(item["device"]["id"])
_LOGGER.debug("Robovac schema: {}".format(device["schema"]))
vac_details = {
CONF_ID: item["device"]["id"],
@ -120,10 +153,11 @@ def get_eufy_vacuums(self):
self[CONF_VACS][item["device"]["id"]] = vac_details
except:
_LOGGER.debug(
"Vacuum {} found on Eufy, but not on Tuya. Skipping.".format(
"Skipping vacuum {}: found on Eufy but not on Tuya. Eufy details:".format(
item["device"]["id"]
)
)
_LOGGER.debug(json.dumps(item["device"], indent=2))
return response
@ -153,8 +187,8 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
errors["base"] = "cannot_connect"
except InvalidAuth:
errors["base"] = "invalid_auth"
except Exception: # pylint: disable=broad-except
_LOGGER.exception("Unexpected exception")
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)

View File

@ -3,3 +3,6 @@
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

@ -37,6 +37,7 @@ ROBOVAC_SERIES = {
"T2251",
"T2252",
"T2253",
"T2254",
"T2150",
"T2255",
"T2256",

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

@ -1,21 +1,40 @@
{
"config": {
"step": {
"user": {
"data": {
"username": "[%key:common::config_flow::data::username%]",
"password": "[%key:common::config_flow::data::password%]"
"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"
}
}
}
},
"error": {
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
"unknown": "[%key:common::config_flow::error::unknown%]"
},
"abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]"
"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

@ -46,13 +46,17 @@ 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
INITIAL_BACKOFF = 5
INITIAL_QUEUE_TIME = 0.1
BACKOFF_MULTIPLIER = 1.70224
_LOGGER = logging.getLogger(__name__)
MESSAGE_PREFIX_FORMAT = ">IIII"
MESSAGE_SUFFIX_FORMAT = ">II"
@ -351,6 +355,10 @@ 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."""
@ -444,7 +452,16 @@ class Message:
SET_COMMAND = 0x07
GRATUITOUS_UPDATE = 0x08
def __init__(self, command, payload=None, sequence=None, encrypt_for=None):
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
@ -454,11 +471,15 @@ class Message:
self.set_sequence()
else:
self.sequence = sequence
self.encrypt = False
self.device = None
if encrypt_for is not None:
self.device = encrypt_for
self.encrypt = True
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(
@ -503,37 +524,11 @@ class Message:
__bytes__ = bytes
async def async_send(self, device, retries=4):
device._listeners[self.sequence] = asyncio.Semaphore(0)
await device._async_send(self)
try:
await asyncio.wait_for(
device._listeners[self.sequence].acquire(), timeout=device.timeout
)
except:
del device._listeners[self.sequence]
if retries == 0:
raise ResponseTimeoutException(
"Timed out waiting for response to sequence number {}".format(
self.sequence
)
)
_LOGGER.debug(
"Timed out waiting for response to sequence number {}. Retrying".format(
self.sequence
)
)
if self.original_sequence is None:
self.set_sequence()
return self.async_send(device, retries - 1)
return device._listeners.pop(self.sequence)
async def async_send(self):
await self.device._async_send(self)
@classmethod
def from_bytes(cls, data, cipher=None):
def from_bytes(cls, device, data, cipher=None):
try:
prefix, sequence, command, payload_size = struct.unpack_from(
MESSAGE_PREFIX_FORMAT, data
@ -590,41 +585,23 @@ class Message:
try:
payload_text = payload_data.decode("utf8")
except UnicodeDecodeError as e:
_LOGGER.debug(payload_data.hex())
_LOGGER.error(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
_LOGGER.debug(payload_data.hex())
_LOGGER.error(e)
device._LOGGER.debug(payload_data.hex())
device._LOGGER.error(e)
raise MessageDecodeFailed() from e
return cls(command, payload, sequence)
def _call_async(fn, *args):
loop = None
if sys.version_info >= (3, 7):
try:
loop = asyncio.get_running_loop()
except RuntimeError:
pass
loop = asyncio.get_event_loop()
def wrapper(fn, *args):
asyncio.ensure_future(fn(*args))
loop.call_soon(wrapper, fn, *args)
class TuyaDevice:
"""Represents a generic Tuya device."""
# PING_INTERVAL = 10
def __init__(
self,
device_id,
@ -638,6 +615,7 @@ class TuyaDevice:
version=(3, 3),
):
"""Initialize the device."""
self._LOGGER = _LOGGER.getChild(device_id)
self.device_id = device_id
self.host = host
self.port = port
@ -655,13 +633,23 @@ class TuyaDevice:
self.cipher = TuyaCipher(local_key, self.version)
self.writer = None
self._handlers = {
Message.GRATUITOUS_UPDATE: [self.async_gratuitous_update_state],
Message.PING_COMMAND: [self._async_pong_received],
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(
@ -675,69 +663,149 @@ class TuyaDevice:
def __str__(self):
return "{} ({}:{})".format(self.device_id, self.host, self.port)
async def async_connect(self, callback=None):
if self._connected:
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)
_LOGGER.debug("Connecting to {}".format(self))
self._LOGGER.debug("Connecting to {}".format(self))
try:
sock.connect((self.host, self.port))
except socket.timeout as e:
except (socket.timeout, TimeoutError) as e:
self._dps["106"] = "CONNECTION_FAILED"
raise ConnectionTimeoutException("Connection timed out") from e
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
asyncio.ensure_future(self._async_ping(self.ping_interval))
asyncio.ensure_future(self._async_handle_message())
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):
_LOGGER.debug("Disconnected from {}".format(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()
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}
maybe_self = None if self.version < (3, 3) else self
message = Message(Message.GET_COMMAND, payload, encrypt_for=maybe_self)
response = await message.async_send(self)
await self.async_update_state(response)
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)
asyncio.create_task(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_for=self)
await message.async_send(self)
message = Message(
Message.SET_COMMAND,
payload,
encrypt=True,
device=self,
expect_response=False,
)
self._queue.append(message)
def set(self, dps):
_call_async(self.async_set, dps)
async def _async_ping(self, ping_interval):
if not self._connected:
async def async_ping(self, ping_interval):
if self._enabled is False:
return
self.last_ping = time.time()
maybe_self = None if self.version < (3, 3) else self
message = Message(Message.PING_COMMAND, sequence=0, encrypt_for=maybe_self)
await self._async_send(message)
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()
else:
asyncio.ensure_future(self._async_ping(self.ping_interval))
async def _async_pong_received(self, message, device):
async def _async_pong_received(self, message):
self.last_pong = time.time()
async def async_gratuitous_update_state(self, state_message, _):
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):
_LOGGER.info("Received updated state {}: {}".format(self, self._dps))
self._dps.update(state_message.payload["dps"])
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):
@ -745,39 +813,59 @@ class TuyaDevice:
@state.setter
def state_setter(self, new_values):
asyncio.ensure_future(self.async_set(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:
response_data = await self.reader.readuntil(MAGIC_SUFFIX_BYTES)
message = Message.from_bytes(response_data, self.cipher)
except InvalidMessage as e:
_LOGGER.debug("Invalid message from {}: {}".format(self, e))
except MessageDecodeFailed as e:
_LOGGER.debug("Failed to decrypt message from {}".format(self))
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:
_LOGGER.debug("Received message from {}: {}".format(self, message))
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:
for c in self._handlers.get(message.command, []):
asyncio.ensure_future(c(message, self))
handler = self._handlers.get(message.command, None)
if handler is not None:
asyncio.create_task(handler(message))
asyncio.ensure_future(self._async_handle_message())
self._response_task = None
asyncio.create_task(self._async_handle_message())
async def _async_send(self, message, retries=4):
async def _async_send(self, message, retries=2):
self._LOGGER.debug("Sending to {}: {}".format(self, message))
try:
await self.async_connect()
_LOGGER.debug("Sending to {}: {}".format(self, message))
self.writer.write(message.bytes())
await self.writer.drain()
except Exception as e:
if retries == 0:
if isinstance(e, socket.error):
asyncio.ensure_future(self.async_disconnect())
await self.async_disconnect()
raise ConnectionException(
"Connection to {} failed: {}".format(self, e)
)
@ -789,19 +877,49 @@ class TuyaDevice:
raise TuyaException("Failed to send data to {}".format(self))
if isinstance(e, socket.error):
_LOGGER.debug(
self._LOGGER.debug(
"Retrying send due to error. Connection to {} failed: {}".format(
self, e
)
)
elif isinstance(e, asyncio.IncompleteReadError):
_LOGGER.debug(
self._LOGGER.debug(
"Retrying send due to error. Incomplete read from: {} : {}. Partial data recieved: {}".format(
self, e, e.partial
)
)
else:
_LOGGER.debug(
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 self._connected is False:
return
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

@ -90,17 +90,20 @@ class TuyaAPISession:
country_code = None
session_id = None
def __init__(self, username, region, timezone):
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 = self.getCountryCode(region)
self.country_code = phone_code
self.base_url = {
"EU": "https://a1.tuyaeu.com",
"AZ": "https://a1.tuyaus.com",
"AY": "https://a1.tuyacn.com",
}.get(region, "https://a1.tuyaus.com")
"IN": "https://a1.tuyain.com",
"EU": "https://a1.tuyaeu.com",
}.get(region, "https://a1.tuyaeu.com")
DEFAULT_TUYA_QUERY_PARAMS["timeZoneId"] = timezone
@staticmethod
@ -232,13 +235,8 @@ class TuyaAPISession:
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}
action="tuya.m.device.get", version="1.0", data={"devId": devId}
)
def getCountryCode(self, region_code):
return {"EU": "44", "AY": "86"}.get(region_code, "1")

View File

@ -56,7 +56,7 @@ from homeassistant.const import (
)
from .tuyalocalapi import TuyaException
from .const import CONF_VACS, DOMAIN
from .const import CONF_VACS, DOMAIN, REFRESH_RATE, PING_RATE, TIMEOUT
from .errors import getErrorMessage
from .robovac import (
@ -84,7 +84,6 @@ ATTR_CONSUMABLES = "consumables"
ATTR_MODE = "mode"
_LOGGER = logging.getLogger(__name__)
REFRESH_RATE = 20
SCAN_INTERVAL = timedelta(seconds=REFRESH_RATE)
UPDATE_RETRIES = 3
@ -115,6 +114,7 @@ async def async_setup_entry(
for item in vacuums:
item = vacuums[item]
entity = RoboVacEntity(item)
hass.data[DOMAIN][CONF_VACS][item[CONF_ID]] = entity
async_add_entities([entity])
@ -273,8 +273,8 @@ class RoboVacEntity(StateVacuumEntity):
device_id=self.unique_id,
host=self.ip_address,
local_key=self.access_token,
timeout=2,
ping_interval=REFRESH_RATE / 2,
timeout=TIMEOUT,
ping_interval=PING_RATE,
model_code=self.model_code[0:5],
update_entity_state=self.pushed_update_handler,
)
@ -316,15 +316,13 @@ class RoboVacEntity(StateVacuumEntity):
self.update_entity_values()
except TuyaException as e:
self.update_failures += 1
_LOGGER.debug(
"Update errored. Current failure count: {}. Reason: {}".format(
_LOGGER.warn(
"Update errored. Current update failure count: {}. Reason: {}".format(
self.update_failures, e
)
)
if self.update_failures == UPDATE_RETRIES:
self.update_failures = 0
if self.update_failures >= UPDATE_RETRIES:
self.error_code = "CONNECTION_FAILED"
raise e
async def pushed_update_handler(self):
self.update_entity_values()
@ -359,11 +357,16 @@ class RoboVacEntity(StateVacuumEntity):
CONSUMABLE_CODE in self.tuyastatus
and self.tuyastatus.get(CONSUMABLE_CODE) is not None
):
self._attr_consumables = ast.literal_eval(
consumables = ast.literal_eval(
base64.b64decode(self.tuyastatus.get(CONSUMABLE_CODE)).decode(
"ascii"
)
)["consumable"]["duration"]
)
if (
"consumable" in consumables
and "duration" in consumables["consumable"]
):
self._attr_consumables = consumables["consumable"]["duration"]
async def async_locate(self, **kwargs):
"""Locate the vacuum cleaner."""
@ -447,4 +450,4 @@ class RoboVacEntity(StateVacuumEntity):
await self.vacuum.async_set({"124": base64_str})
async def async_will_remove_from_hass(self):
await self.vacuum.async_disconnect()
await self.vacuum.async_disable()

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"
}
}

View File

@ -1,4 +1,4 @@
cryptography==41.0.3
cryptography==41.0.4
homeassistant==2023.8.4
Requests==2.31.0
setuptools==68.0.0

View File

@ -24,18 +24,18 @@ import warnings
dynamic_requires = []
setup(
name='robovac',
name="robovac",
version="1.0",
author='Luke Bonaccorsi',
url='http://github.com/codefoodpixels/robovac',
author="Luke Morrigan",
url="http://github.com/codefoodpixels/robovac",
packages=find_packages(),
scripts=[],
description='Python API for controlling Eufy Robovac vacuum cleaners',
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',
"Development Status :: 4 - Beta",
"Intended Audience :: Developers",
"License :: OSI Approved :: Apache Software License",
"Operating System :: OS Independent",
"Programming Language :: Python",
],
)