Compare commits

..

No commits in common. "main" and "v1.1.0.beta.5" have entirely different histories.

19 changed files with 199 additions and 6744 deletions

View File

@ -4,28 +4,38 @@ name: CI
# Controls when the workflow will run # Controls when the workflow will run
on: on:
# Triggers the workflow on push or pull request events but only for the "main" branch # Triggers the workflow on push or pull request events but only for the "main" branch
push: push:
branches: ["main"] branches: [ "main" ]
pull_request: pull_request:
branches: ["main"] branches: [ "main" ]
# Allows you to run this workflow manually from the Actions tab # Allows you to run this workflow manually from the Actions tab
workflow_dispatch: workflow_dispatch:
# A workflow run is made up of one or more jobs that can run sequentially or in parallel # A workflow run is made up of one or more jobs that can run sequentially or in parallel
jobs: jobs:
# This workflow contains a single job called "build" # This workflow contains a single job called "build"
build: build:
# The type of runner that the job will run on # The type of runner that the job will run on
runs-on: ubuntu-latest runs-on: ubuntu-latest
# Steps represent a sequence of tasks that will be executed as part of the job # Steps represent a sequence of tasks that will be executed as part of the job
steps: steps:
# Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it
- uses: actions/checkout@v3 - uses: actions/checkout@v3
- uses: "home-assistant/actions/hassfest@master" - uses: "home-assistant/actions/hassfest@master"
- name: HACS Action - name: HACS Action
uses: "hacs/action@main" uses: "hacs/action@main"
with: with:
category: "integration" 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.

View File

@ -1,19 +0,0 @@
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
View File

@ -1 +0,0 @@
/node_modules

1
.nvmrc
View File

@ -1 +0,0 @@
20

View File

@ -1,20 +0,0 @@
{
"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 from .tuyalocaldiscovery import TuyaLocalDiscovery
PLATFORMS = [Platform.VACUUM, Platform.SENSOR] PLATFORM = Platform.VACUUM
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
async def async_setup(hass, entry) -> bool: async def async_setup(hass, entry) -> bool:
hass.data.setdefault(DOMAIN, {CONF_VACS:{}}) hass.data.setdefault(DOMAIN, {})
async def update_device(device): async def update_device(device):
entry = async_get_config_entry_for_device(hass, device["gwId"]) 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.""" """Set up Eufy Robovac from a config entry."""
entry.async_on_unload(entry.add_update_listener(update_listener)) entry.async_on_unload(entry.add_update_listener(update_listener))
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) await hass.config_entries.async_forward_entry_setup(entry, PLATFORM)
return True return True
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Unload a config entry.""" """Unload a config entry."""
if unload_ok := await hass.config_entries.async_unload_platforms( if unload_ok := await hass.config_entries.async_forward_entry_unload(
entry, PLATFORMS entry, PLATFORM
): ):
"""Nothing""" """Nothing"""
return unload_ok return unload_ok

View File

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

View File

@ -3,6 +3,3 @@
DOMAIN = "robovac" DOMAIN = "robovac"
CONF_VACS = "vacuums" CONF_VACS = "vacuums"
CONF_AUTODISCOVERY = "autodiscovery" CONF_AUTODISCOVERY = "autodiscovery"
REFRESH_RATE = 60
PING_RATE = 10
TIMEOUT = 5

View File

@ -1,228 +0,0 @@
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,7 +37,6 @@ ROBOVAC_SERIES = {
"T2251", "T2251",
"T2252", "T2252",
"T2253", "T2253",
"T2254",
"T2150", "T2150",
"T2255", "T2255",
"T2256", "T2256",

View File

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

View File

@ -46,17 +46,13 @@ import socket
import struct import struct
import sys import sys
import time import time
import traceback
from typing import Callable, Coroutine
from cryptography.hazmat.backends.openssl import backend as openssl_backend from cryptography.hazmat.backends.openssl import backend as openssl_backend
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
from cryptography.hazmat.primitives.hashes import Hash, MD5 from cryptography.hazmat.primitives.hashes import Hash, MD5
from cryptography.hazmat.primitives.padding import PKCS7 from cryptography.hazmat.primitives.padding import PKCS7
INITIAL_BACKOFF = 5
INITIAL_QUEUE_TIME = 0.1
BACKOFF_MULTIPLIER = 1.70224
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
MESSAGE_PREFIX_FORMAT = ">IIII" MESSAGE_PREFIX_FORMAT = ">IIII"
MESSAGE_SUFFIX_FORMAT = ">II" MESSAGE_SUFFIX_FORMAT = ">II"
@ -355,10 +351,6 @@ class ResponseTimeoutException(TuyaException):
"""Did not recieve a response to the request within the timeout""" """Did not recieve a response to the request within the timeout"""
class BackoffException(TuyaException):
"""Backoff time not reached"""
class TuyaCipher: class TuyaCipher:
"""Tuya cryptographic helpers.""" """Tuya cryptographic helpers."""
@ -452,16 +444,7 @@ class Message:
SET_COMMAND = 0x07 SET_COMMAND = 0x07
GRATUITOUS_UPDATE = 0x08 GRATUITOUS_UPDATE = 0x08
def __init__( def __init__(self, command, payload=None, sequence=None, encrypt_for=None):
self,
command,
payload=None,
sequence=None,
encrypt=False,
device=None,
expect_response=True,
ttl=5,
):
if payload is None: if payload is None:
payload = b"" payload = b""
self.payload = payload self.payload = payload
@ -471,15 +454,11 @@ class Message:
self.set_sequence() self.set_sequence()
else: else:
self.sequence = sequence self.sequence = sequence
self.encrypt = encrypt self.encrypt = False
self.device = device self.device = None
self.expiry = int(time.time()) + ttl if encrypt_for is not None:
self.expect_response = expect_response self.device = encrypt_for
self.listener = None self.encrypt = True
if expect_response is True:
self.listener = asyncio.Semaphore(0)
if device is not None:
device._listeners[self.sequence] = self.listener
def __repr__(self): def __repr__(self):
return "{}({}, {!r}, {!r}, {})".format( return "{}({}, {!r}, {!r}, {})".format(
@ -524,11 +503,37 @@ class Message:
__bytes__ = bytes __bytes__ = bytes
async def async_send(self): async def async_send(self, device, retries=4):
await self.device._async_send(self) 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)
@classmethod @classmethod
def from_bytes(cls, device, data, cipher=None): def from_bytes(cls, data, cipher=None):
try: try:
prefix, sequence, command, payload_size = struct.unpack_from( prefix, sequence, command, payload_size = struct.unpack_from(
MESSAGE_PREFIX_FORMAT, data MESSAGE_PREFIX_FORMAT, data
@ -585,23 +590,41 @@ class Message:
try: try:
payload_text = payload_data.decode("utf8") payload_text = payload_data.decode("utf8")
except UnicodeDecodeError as e: except UnicodeDecodeError as e:
device._LOGGER.debug(payload_data.hex()) _LOGGER.debug(payload_data.hex())
device._LOGGER.error(e) _LOGGER.error(e)
raise MessageDecodeFailed() from e raise MessageDecodeFailed() from e
try: try:
payload = json.loads(payload_text) payload = json.loads(payload_text)
except json.decoder.JSONDecodeError as e: except json.decoder.JSONDecodeError as e:
# data may be encrypted # data may be encrypted
device._LOGGER.debug(payload_data.hex()) _LOGGER.debug(payload_data.hex())
device._LOGGER.error(e) _LOGGER.error(e)
raise MessageDecodeFailed() from e raise MessageDecodeFailed() from e
return cls(command, payload, sequence) 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: class TuyaDevice:
"""Represents a generic Tuya device.""" """Represents a generic Tuya device."""
# PING_INTERVAL = 10
def __init__( def __init__(
self, self,
device_id, device_id,
@ -615,7 +638,6 @@ class TuyaDevice:
version=(3, 3), version=(3, 3),
): ):
"""Initialize the device.""" """Initialize the device."""
self._LOGGER = _LOGGER.getChild(device_id)
self.device_id = device_id self.device_id = device_id
self.host = host self.host = host
self.port = port self.port = port
@ -633,23 +655,13 @@ class TuyaDevice:
self.cipher = TuyaCipher(local_key, self.version) self.cipher = TuyaCipher(local_key, self.version)
self.writer = None self.writer = None
self._response_task = None self._handlers = {
self._recieve_task = None Message.GRATUITOUS_UPDATE: [self.async_gratuitous_update_state],
self._ping_task = None Message.PING_COMMAND: [self._async_pong_received],
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._dps = {}
self._connected = False self._connected = False
self._enabled = True
self._queue = []
self._listeners = {} self._listeners = {}
self._backoff = False
self._queue_interval = INITIAL_QUEUE_TIME
self._failures = 0
asyncio.create_task(self.process_queue())
def __repr__(self): def __repr__(self):
return "{}({!r}, {!r}, {!r}, {!r})".format( return "{}({!r}, {!r}, {!r}, {!r})".format(
@ -663,149 +675,69 @@ class TuyaDevice:
def __str__(self): def __str__(self):
return "{} ({}:{})".format(self.device_id, self.host, self.port) return "{} ({}:{})".format(self.device_id, self.host, self.port)
async def process_queue(self): async def async_connect(self, callback=None):
if self._enabled is False: if self._connected:
return 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 = socket.socket(family=socket.AF_INET, type=socket.SOCK_STREAM)
sock.settimeout(self.timeout) sock.settimeout(self.timeout)
self._LOGGER.debug("Connecting to {}".format(self)) _LOGGER.debug("Connecting to {}".format(self))
try: try:
sock.connect((self.host, self.port)) sock.connect((self.host, self.port))
except (socket.timeout, TimeoutError) as e: except socket.timeout as e:
self._dps["106"] = "CONNECTION_FAILED" self._dps["106"] = "CONNECTION_FAILED"
raise ConnectionTimeoutException("Connection timed out") raise ConnectionTimeoutException("Connection timed out") from e
loop = asyncio.get_running_loop()
loop.create_connection
self.reader, self.writer = await asyncio.open_connection(sock=sock) self.reader, self.writer = await asyncio.open_connection(sock=sock)
self._connected = True self._connected = True
asyncio.ensure_future(self._async_ping(self.ping_interval))
if self._ping_task is None: asyncio.ensure_future(self._async_handle_message())
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): async def async_disconnect(self):
if self._connected is False: _LOGGER.debug("Disconnected from {}".format(self))
return
self._LOGGER.debug("Disconnected from {}".format(self))
self._connected = False self._connected = False
self.last_pong = 0 self.last_pong = 0
if self.writer is not None: if self.writer is not None:
self.writer.close() self.writer.close()
if self.reader is not None and not self.reader.at_eof():
self.reader.feed_eof()
async def async_get(self): async def async_get(self):
payload = {"gwId": self.gateway_id, "devId": self.device_id} payload = {"gwId": self.gateway_id, "devId": self.device_id}
encrypt = False if self.version < (3, 3) else True maybe_self = None if self.version < (3, 3) else self
message = Message(Message.GET_COMMAND, payload, encrypt=encrypt, device=self) message = Message(Message.GET_COMMAND, payload, encrypt_for=maybe_self)
self._queue.append(message) response = await message.async_send(self)
response = await self.async_recieve(message) await self.async_update_state(response)
asyncio.create_task(self.async_update_state(response))
async def async_set(self, dps): async def async_set(self, dps):
t = int(time.time()) t = int(time.time())
payload = {"devId": self.device_id, "uid": "", "t": t, "dps": dps} payload = {"devId": self.device_id, "uid": "", "t": t, "dps": dps}
message = Message( message = Message(Message.SET_COMMAND, payload, encrypt_for=self)
Message.SET_COMMAND, await message.async_send(self)
payload,
encrypt=True,
device=self,
expect_response=False,
)
self._queue.append(message)
async def async_ping(self, ping_interval): def set(self, dps):
if self._enabled is False: _call_async(self.async_set, dps)
async def _async_ping(self, ping_interval):
if not self._connected:
return return
if self._backoff is True: self.last_ping = time.time()
self._LOGGER.debug("Currently in backoff, not adding ping to queue") maybe_self = None if self.version < (3, 3) else self
else: message = Message(Message.PING_COMMAND, sequence=0, encrypt_for=maybe_self)
self.last_ping = time.time() await self._async_send(message)
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) await asyncio.sleep(ping_interval)
self._ping_task = asyncio.create_task(self.async_ping(self.ping_interval))
if self.last_pong < self.last_ping: if self.last_pong < self.last_ping:
await self.async_disconnect() await self.async_disconnect()
else:
asyncio.ensure_future(self._async_ping(self.ping_interval))
async def _async_pong_received(self, message): async def _async_pong_received(self, message, device):
self.last_pong = time.time() 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.async_update_state(state_message)
await self.update_entity_state_cb() await self.update_entity_state_cb()
async def async_update_state(self, state_message, _=None): async def async_update_state(self, state_message, _=None):
if ( _LOGGER.info("Received updated state {}: {}".format(self, self._dps))
state_message is not None self._dps.update(state_message.payload["dps"])
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 @property
def state(self): def state(self):
@ -813,59 +745,39 @@ class TuyaDevice:
@state.setter @state.setter
def state_setter(self, new_values): def state_setter(self, new_values):
asyncio.create_task(self.async_set(new_values)) asyncio.ensure_future(self.async_set(new_values))
async def _async_handle_message(self): async def _async_handle_message(self):
if self._enabled is False or self._connected is False:
return
try: try:
self._response_task = asyncio.create_task( response_data = await self.reader.readuntil(MAGIC_SUFFIX_BYTES)
self.reader.readuntil(MAGIC_SUFFIX_BYTES) message = Message.from_bytes(response_data, self.cipher)
) except InvalidMessage as e:
await self._response_task _LOGGER.debug("Invalid message from {}: {}".format(self, e))
response_data = self._response_task.result() except MessageDecodeFailed as e:
message = Message.from_bytes(self, response_data, self.cipher) _LOGGER.debug("Failed to decrypt message from {}".format(self))
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: else:
self._LOGGER.debug("Received message from {}: {}".format(self, message)) _LOGGER.debug("Received message from {}: {}".format(self, message))
if message.sequence in self._listeners: if message.sequence in self._listeners:
sem = self._listeners[message.sequence] sem = self._listeners[message.sequence]
if isinstance(sem, asyncio.Semaphore): if isinstance(sem, asyncio.Semaphore):
self._listeners[message.sequence] = message self._listeners[message.sequence] = message
sem.release() sem.release()
else: else:
handler = self._handlers.get(message.command, None) for c in self._handlers.get(message.command, []):
if handler is not None: asyncio.ensure_future(c(message, self))
asyncio.create_task(handler(message))
self._response_task = None asyncio.ensure_future(self._async_handle_message())
asyncio.create_task(self._async_handle_message())
async def _async_send(self, message, retries=2): async def _async_send(self, message, retries=4):
self._LOGGER.debug("Sending to {}: {}".format(self, message))
try: try:
await self.async_connect() await self.async_connect()
_LOGGER.debug("Sending to {}: {}".format(self, message))
self.writer.write(message.bytes()) self.writer.write(message.bytes())
await self.writer.drain() await self.writer.drain()
except Exception as e: except Exception as e:
if retries == 0: if retries == 0:
if isinstance(e, socket.error): if isinstance(e, socket.error):
await self.async_disconnect() asyncio.ensure_future(self.async_disconnect())
raise ConnectionException( raise ConnectionException(
"Connection to {} failed: {}".format(self, e) "Connection to {} failed: {}".format(self, e)
) )
@ -877,49 +789,19 @@ class TuyaDevice:
raise TuyaException("Failed to send data to {}".format(self)) raise TuyaException("Failed to send data to {}".format(self))
if isinstance(e, socket.error): if isinstance(e, socket.error):
self._LOGGER.debug( _LOGGER.debug(
"Retrying send due to error. Connection to {} failed: {}".format( "Retrying send due to error. Connection to {} failed: {}".format(
self, e self, e
) )
) )
elif isinstance(e, asyncio.IncompleteReadError): elif isinstance(e, asyncio.IncompleteReadError):
self._LOGGER.debug( _LOGGER.debug(
"Retrying send due to error. Incomplete read from: {} : {}. Partial data recieved: {}".format( "Retrying send due to error. Incomplete read from: {} : {}. Partial data recieved: {}".format(
self, e, e.partial self, e, e.partial
) )
) )
else: else:
self._LOGGER.debug( _LOGGER.debug(
"Retrying send due to error. Failed to send data to {}".format(self) "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) 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,20 +90,17 @@ class TuyaAPISession:
country_code = None country_code = None
session_id = None session_id = None
def __init__(self, username, region, timezone, phone_code): def __init__(self, username, region, timezone):
self.session = requests.session() self.session = requests.session()
self.session.headers = DEFAULT_TUYA_HEADERS.copy() self.session.headers = DEFAULT_TUYA_HEADERS.copy()
self.default_query_params = DEFAULT_TUYA_QUERY_PARAMS.copy() self.default_query_params = DEFAULT_TUYA_QUERY_PARAMS.copy()
self.default_query_params["deviceId"] = self.generate_new_device_id() self.default_query_params["deviceId"] = self.generate_new_device_id()
self.username = username self.username = username
self.country_code = phone_code self.country_code = self.getCountryCode(region)
self.base_url = { self.base_url = {
"AZ": "https://a1.tuyaus.com",
"AY": "https://a1.tuyacn.com",
"IN": "https://a1.tuyain.com",
"EU": "https://a1.tuyaeu.com", "EU": "https://a1.tuyaeu.com",
}.get(region, "https://a1.tuyaeu.com") "AY": "https://a1.tuyacn.com",
}.get(region, "https://a1.tuyaus.com")
DEFAULT_TUYA_QUERY_PARAMS["timeZoneId"] = timezone DEFAULT_TUYA_QUERY_PARAMS["timeZoneId"] = timezone
@staticmethod @staticmethod
@ -235,8 +232,13 @@ class TuyaAPISession:
def list_homes(self): def list_homes(self):
return self._request(action="tuya.m.location.list", version="2.1") return self._request(action="tuya.m.location.list", version="2.1")
def get_device(self, devId): def get_device(self, devId):
return self._request( 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 .tuyalocalapi import TuyaException
from .const import CONF_VACS, DOMAIN, REFRESH_RATE, PING_RATE, TIMEOUT from .const import CONF_VACS, DOMAIN
from .errors import getErrorMessage from .errors import getErrorMessage
from .robovac import ( from .robovac import (
@ -84,6 +84,7 @@ ATTR_CONSUMABLES = "consumables"
ATTR_MODE = "mode" ATTR_MODE = "mode"
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
REFRESH_RATE = 20
SCAN_INTERVAL = timedelta(seconds=REFRESH_RATE) SCAN_INTERVAL = timedelta(seconds=REFRESH_RATE)
UPDATE_RETRIES = 3 UPDATE_RETRIES = 3
@ -114,7 +115,6 @@ async def async_setup_entry(
for item in vacuums: for item in vacuums:
item = vacuums[item] item = vacuums[item]
entity = RoboVacEntity(item) entity = RoboVacEntity(item)
hass.data[DOMAIN][CONF_VACS][item[CONF_ID]] = entity
async_add_entities([entity]) async_add_entities([entity])
@ -273,8 +273,8 @@ class RoboVacEntity(StateVacuumEntity):
device_id=self.unique_id, device_id=self.unique_id,
host=self.ip_address, host=self.ip_address,
local_key=self.access_token, local_key=self.access_token,
timeout=TIMEOUT, timeout=2,
ping_interval=PING_RATE, ping_interval=REFRESH_RATE / 2,
model_code=self.model_code[0:5], model_code=self.model_code[0:5],
update_entity_state=self.pushed_update_handler, update_entity_state=self.pushed_update_handler,
) )
@ -316,13 +316,15 @@ class RoboVacEntity(StateVacuumEntity):
self.update_entity_values() self.update_entity_values()
except TuyaException as e: except TuyaException as e:
self.update_failures += 1 self.update_failures += 1
_LOGGER.warn( _LOGGER.debug(
"Update errored. Current update failure count: {}. Reason: {}".format( "Update errored. Current failure count: {}. Reason: {}".format(
self.update_failures, e self.update_failures, e
) )
) )
if self.update_failures >= UPDATE_RETRIES: if self.update_failures == UPDATE_RETRIES:
self.update_failures = 0
self.error_code = "CONNECTION_FAILED" self.error_code = "CONNECTION_FAILED"
raise e
async def pushed_update_handler(self): async def pushed_update_handler(self):
self.update_entity_values() self.update_entity_values()
@ -357,16 +359,11 @@ class RoboVacEntity(StateVacuumEntity):
CONSUMABLE_CODE in self.tuyastatus CONSUMABLE_CODE in self.tuyastatus
and self.tuyastatus.get(CONSUMABLE_CODE) is not None and self.tuyastatus.get(CONSUMABLE_CODE) is not None
): ):
consumables = ast.literal_eval( self._attr_consumables = ast.literal_eval(
base64.b64decode(self.tuyastatus.get(CONSUMABLE_CODE)).decode( base64.b64decode(self.tuyastatus.get(CONSUMABLE_CODE)).decode(
"ascii" "ascii"
) )
) )["consumable"]["duration"]
if (
"consumable" in consumables
and "duration" in consumables["consumable"]
):
self._attr_consumables = consumables["consumable"]["duration"]
async def async_locate(self, **kwargs): async def async_locate(self, **kwargs):
"""Locate the vacuum cleaner.""" """Locate the vacuum cleaner."""
@ -450,4 +447,4 @@ class RoboVacEntity(StateVacuumEntity):
await self.vacuum.async_set({"124": base64_str}) await self.vacuum.async_set({"124": base64_str})
async def async_will_remove_from_hass(self): async def async_will_remove_from_hass(self):
await self.vacuum.async_disable() await self.vacuum.async_disconnect()

6041
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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