Compare commits
No commits in common. "main" and "v1.1.0.beta.5" have entirely different histories.
main
...
v1.1.0.bet
|
|
@ -4,28 +4,38 @@ 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"
|
||||
# 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.
|
||||
|
|
|
|||
|
|
@ -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 +0,0 @@
|
|||
/node_modules
|
||||
20
.releaserc
20
.releaserc
|
|
@ -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"
|
||||
]
|
||||
}
|
||||
|
|
@ -24,12 +24,12 @@ from .const import CONF_VACS, DOMAIN
|
|||
|
||||
from .tuyalocaldiscovery import TuyaLocalDiscovery
|
||||
|
||||
PLATFORMS = [Platform.VACUUM, Platform.SENSOR]
|
||||
PLATFORM = Platform.VACUUM
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
async def async_setup(hass, entry) -> bool:
|
||||
hass.data.setdefault(DOMAIN, {CONF_VACS:{}})
|
||||
hass.data.setdefault(DOMAIN, {})
|
||||
|
||||
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_setups(entry, PLATFORMS)
|
||||
await hass.config_entries.async_forward_entry_setup(entry, PLATFORM)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Unload a config entry."""
|
||||
if unload_ok := await hass.config_entries.async_unload_platforms(
|
||||
entry, PLATFORMS
|
||||
if unload_ok := await hass.config_entries.async_forward_entry_unload(
|
||||
entry, PLATFORM
|
||||
):
|
||||
"""Nothing"""
|
||||
return unload_ok
|
||||
|
|
|
|||
|
|
@ -14,7 +14,6 @@
|
|||
|
||||
"""Config flow for Eufy Robovac integration."""
|
||||
from __future__ import annotations
|
||||
import json
|
||||
|
||||
import logging
|
||||
from typing import Any, Optional
|
||||
|
|
@ -40,17 +39,10 @@ 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
|
||||
|
|
@ -96,41 +88,15 @@ def get_eufy_vacuums(self):
|
|||
settings_response = response.json()
|
||||
|
||||
self[CONF_CLIENT_ID] = user_response["user_info"]["id"]
|
||||
if (
|
||||
"tuya_home" in settings_response["setting"]["home_setting"]
|
||||
and "tuya_region_code"
|
||||
in settings_response["setting"]["home_setting"]["tuya_home"]
|
||||
):
|
||||
self[CONF_REGION] = settings_response["setting"]["home_setting"]["tuya_home"][
|
||||
"tuya_region_code"
|
||||
]
|
||||
if user_response["user_info"]["phone_code"]:
|
||||
self[CONF_COUNTRY_CODE] = user_response["user_info"]["phone_code"]
|
||||
else:
|
||||
self[CONF_COUNTRY_CODE] = get_phone_code_by_region(self[CONF_REGION])
|
||||
elif user_response["user_info"]["phone_code"]:
|
||||
self[CONF_REGION] = get_region_by_phone_code(
|
||||
user_response["user_info"]["phone_code"]
|
||||
)
|
||||
self[CONF_COUNTRY_CODE] = user_response["user_info"]["phone_code"]
|
||||
elif user_response["user_info"]["country"]:
|
||||
self[CONF_REGION] = get_region_by_country_code(
|
||||
user_response["user_info"]["country"]
|
||||
)
|
||||
self[CONF_COUNTRY_CODE] = get_phone_code_by_country_code(
|
||||
user_response["user_info"]["country"]
|
||||
)
|
||||
else:
|
||||
self[CONF_REGION] = "EU"
|
||||
self[CONF_COUNTRY_CODE] = "44"
|
||||
|
||||
self[CONF_REGION] = settings_response["setting"]["home_setting"]["tuya_home"][
|
||||
"tuya_region_code"
|
||||
]
|
||||
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"]
|
||||
|
|
@ -139,6 +105,7 @@ 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"],
|
||||
|
|
@ -153,11 +120,10 @@ def get_eufy_vacuums(self):
|
|||
self[CONF_VACS][item["device"]["id"]] = vac_details
|
||||
except:
|
||||
_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"]
|
||||
)
|
||||
)
|
||||
_LOGGER.debug(json.dumps(item["device"], indent=2))
|
||||
|
||||
return response
|
||||
|
||||
|
|
@ -187,8 +153,8 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
|||
errors["base"] = "cannot_connect"
|
||||
except InvalidAuth:
|
||||
errors["base"] = "invalid_auth"
|
||||
except Exception as e: # pylint: disable=broad-except
|
||||
_LOGGER.exception("Unexpected exception: {}".format(e))
|
||||
except Exception: # pylint: disable=broad-except
|
||||
_LOGGER.exception("Unexpected exception")
|
||||
errors["base"] = "unknown"
|
||||
else:
|
||||
await self.async_set_unique_id(unique_id)
|
||||
|
|
|
|||
|
|
@ -3,6 +3,3 @@
|
|||
DOMAIN = "robovac"
|
||||
CONF_VACS = "vacuums"
|
||||
CONF_AUTODISCOVERY = "autodiscovery"
|
||||
REFRESH_RATE = 60
|
||||
PING_RATE = 10
|
||||
TIMEOUT = 5
|
||||
|
|
|
|||
|
|
@ -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"]
|
||||
|
|
@ -37,7 +37,6 @@ ROBOVAC_SERIES = {
|
|||
"T2251",
|
||||
"T2252",
|
||||
"T2253",
|
||||
"T2254",
|
||||
"T2150",
|
||||
"T2255",
|
||||
"T2256",
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
@ -1,40 +1,21 @@
|
|||
{
|
||||
"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"
|
||||
}
|
||||
"config": {
|
||||
"step": {
|
||||
"user": {
|
||||
"data": {
|
||||
"username": "[%key:common::config_flow::data::username%]",
|
||||
"password": "[%key:common::config_flow::data::password%]"
|
||||
}
|
||||
}
|
||||
},
|
||||
"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"
|
||||
}
|
||||
}
|
||||
"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%]"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -46,17 +46,13 @@ 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"
|
||||
|
|
@ -355,10 +351,6 @@ 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."""
|
||||
|
||||
|
|
@ -452,16 +444,7 @@ class Message:
|
|||
SET_COMMAND = 0x07
|
||||
GRATUITOUS_UPDATE = 0x08
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
command,
|
||||
payload=None,
|
||||
sequence=None,
|
||||
encrypt=False,
|
||||
device=None,
|
||||
expect_response=True,
|
||||
ttl=5,
|
||||
):
|
||||
def __init__(self, command, payload=None, sequence=None, encrypt_for=None):
|
||||
if payload is None:
|
||||
payload = b""
|
||||
self.payload = payload
|
||||
|
|
@ -471,15 +454,11 @@ class Message:
|
|||
self.set_sequence()
|
||||
else:
|
||||
self.sequence = sequence
|
||||
self.encrypt = encrypt
|
||||
self.device = device
|
||||
self.expiry = int(time.time()) + ttl
|
||||
self.expect_response = expect_response
|
||||
self.listener = None
|
||||
if expect_response is True:
|
||||
self.listener = asyncio.Semaphore(0)
|
||||
if device is not None:
|
||||
device._listeners[self.sequence] = self.listener
|
||||
self.encrypt = False
|
||||
self.device = None
|
||||
if encrypt_for is not None:
|
||||
self.device = encrypt_for
|
||||
self.encrypt = True
|
||||
|
||||
def __repr__(self):
|
||||
return "{}({}, {!r}, {!r}, {})".format(
|
||||
|
|
@ -524,11 +503,37 @@ class Message:
|
|||
|
||||
__bytes__ = bytes
|
||||
|
||||
async def async_send(self):
|
||||
await self.device._async_send(self)
|
||||
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)
|
||||
|
||||
@classmethod
|
||||
def from_bytes(cls, device, data, cipher=None):
|
||||
def from_bytes(cls, data, cipher=None):
|
||||
try:
|
||||
prefix, sequence, command, payload_size = struct.unpack_from(
|
||||
MESSAGE_PREFIX_FORMAT, data
|
||||
|
|
@ -585,23 +590,41 @@ class Message:
|
|||
try:
|
||||
payload_text = payload_data.decode("utf8")
|
||||
except UnicodeDecodeError as e:
|
||||
device._LOGGER.debug(payload_data.hex())
|
||||
device._LOGGER.error(e)
|
||||
_LOGGER.debug(payload_data.hex())
|
||||
_LOGGER.error(e)
|
||||
raise MessageDecodeFailed() from e
|
||||
try:
|
||||
payload = json.loads(payload_text)
|
||||
except json.decoder.JSONDecodeError as e:
|
||||
# data may be encrypted
|
||||
device._LOGGER.debug(payload_data.hex())
|
||||
device._LOGGER.error(e)
|
||||
_LOGGER.debug(payload_data.hex())
|
||||
_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,
|
||||
|
|
@ -615,7 +638,6 @@ 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
|
||||
|
|
@ -633,23 +655,13 @@ class TuyaDevice:
|
|||
|
||||
self.cipher = TuyaCipher(local_key, self.version)
|
||||
self.writer = None
|
||||
self._response_task = None
|
||||
self._recieve_task = None
|
||||
self._ping_task = None
|
||||
self._handlers: dict[int, Callable[[Message], Coroutine]] = {
|
||||
Message.GRATUITOUS_UPDATE: self.async_gratuitous_update_state,
|
||||
Message.PING_COMMAND: self._async_pong_received,
|
||||
self._handlers = {
|
||||
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(
|
||||
|
|
@ -663,149 +675,69 @@ class TuyaDevice:
|
|||
def __str__(self):
|
||||
return "{} ({}:{})".format(self.device_id, self.host, self.port)
|
||||
|
||||
async def process_queue(self):
|
||||
if self._enabled is False:
|
||||
async def async_connect(self, callback=None):
|
||||
if self._connected:
|
||||
return
|
||||
|
||||
self.clean_queue()
|
||||
|
||||
if len(self._queue) > 0:
|
||||
self._LOGGER.debug(
|
||||
"Processing queue. Current length: {}".format(len(self._queue))
|
||||
)
|
||||
try:
|
||||
message = self._queue.pop(0)
|
||||
await message.async_send()
|
||||
self._failures = 0
|
||||
self._queue_interval = INITIAL_QUEUE_TIME
|
||||
self._backoff = False
|
||||
except Exception as e:
|
||||
self._failures += 1
|
||||
self._LOGGER.debug(
|
||||
"{} failures. Most recent: {}".format(self._failures, e)
|
||||
)
|
||||
if self._failures > 3:
|
||||
self._backoff = True
|
||||
self._queue_interval = min(
|
||||
INITIAL_BACKOFF * (BACKOFF_MULTIPLIER ** (self._failures - 4)),
|
||||
600,
|
||||
)
|
||||
self._LOGGER.warn(
|
||||
"{} failures, backing off for {} seconds".format(
|
||||
self._failures, self._queue_interval
|
||||
)
|
||||
)
|
||||
|
||||
await asyncio.sleep(self._queue_interval)
|
||||
asyncio.create_task(self.process_queue())
|
||||
|
||||
def clean_queue(self):
|
||||
cleaned_queue = []
|
||||
now = int(time.time())
|
||||
for item in self._queue:
|
||||
if item.expiry > now:
|
||||
cleaned_queue.append(item)
|
||||
self._queue = cleaned_queue
|
||||
|
||||
async def async_connect(self):
|
||||
if self._connected is True or self._enabled is False:
|
||||
return
|
||||
|
||||
sock = socket.socket(family=socket.AF_INET, type=socket.SOCK_STREAM)
|
||||
sock.settimeout(self.timeout)
|
||||
self._LOGGER.debug("Connecting to {}".format(self))
|
||||
_LOGGER.debug("Connecting to {}".format(self))
|
||||
try:
|
||||
sock.connect((self.host, self.port))
|
||||
except (socket.timeout, TimeoutError) as e:
|
||||
except socket.timeout as e:
|
||||
self._dps["106"] = "CONNECTION_FAILED"
|
||||
raise ConnectionTimeoutException("Connection timed out")
|
||||
loop = asyncio.get_running_loop()
|
||||
loop.create_connection
|
||||
raise ConnectionTimeoutException("Connection timed out") from e
|
||||
self.reader, self.writer = await asyncio.open_connection(sock=sock)
|
||||
self._connected = True
|
||||
|
||||
if self._ping_task is None:
|
||||
self._ping_task = asyncio.create_task(self.async_ping(self.ping_interval))
|
||||
|
||||
asyncio.create_task(self._async_handle_message())
|
||||
|
||||
async def async_disable(self):
|
||||
self._enabled = False
|
||||
|
||||
await self.async_disconnect()
|
||||
asyncio.ensure_future(self._async_ping(self.ping_interval))
|
||||
asyncio.ensure_future(self._async_handle_message())
|
||||
|
||||
async def async_disconnect(self):
|
||||
if self._connected is False:
|
||||
return
|
||||
|
||||
self._LOGGER.debug("Disconnected from {}".format(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}
|
||||
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))
|
||||
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)
|
||||
|
||||
async def async_set(self, dps):
|
||||
t = int(time.time())
|
||||
payload = {"devId": self.device_id, "uid": "", "t": t, "dps": dps}
|
||||
message = Message(
|
||||
Message.SET_COMMAND,
|
||||
payload,
|
||||
encrypt=True,
|
||||
device=self,
|
||||
expect_response=False,
|
||||
)
|
||||
self._queue.append(message)
|
||||
message = Message(Message.SET_COMMAND, payload, encrypt_for=self)
|
||||
await message.async_send(self)
|
||||
|
||||
async def async_ping(self, ping_interval):
|
||||
if self._enabled is False:
|
||||
def set(self, dps):
|
||||
_call_async(self.async_set, dps)
|
||||
|
||||
async def _async_ping(self, ping_interval):
|
||||
if not self._connected:
|
||||
return
|
||||
|
||||
if self._backoff is True:
|
||||
self._LOGGER.debug("Currently in backoff, not adding ping to queue")
|
||||
else:
|
||||
self.last_ping = time.time()
|
||||
encrypt = False if self.version < (3, 3) else True
|
||||
message = Message(
|
||||
Message.PING_COMMAND,
|
||||
sequence=0,
|
||||
encrypt=encrypt,
|
||||
device=self,
|
||||
expect_response=False,
|
||||
)
|
||||
self._queue.append(message)
|
||||
|
||||
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)
|
||||
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):
|
||||
async def _async_pong_received(self, message, device):
|
||||
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):
|
||||
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))
|
||||
_LOGGER.info("Received updated state {}: {}".format(self, self._dps))
|
||||
self._dps.update(state_message.payload["dps"])
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
|
|
@ -813,59 +745,39 @@ class TuyaDevice:
|
|||
|
||||
@state.setter
|
||||
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):
|
||||
if self._enabled is False or self._connected is False:
|
||||
return
|
||||
|
||||
try:
|
||||
self._response_task = asyncio.create_task(
|
||||
self.reader.readuntil(MAGIC_SUFFIX_BYTES)
|
||||
)
|
||||
await self._response_task
|
||||
response_data = self._response_task.result()
|
||||
message = Message.from_bytes(self, response_data, self.cipher)
|
||||
except Exception as e:
|
||||
if isinstance(e, InvalidMessage):
|
||||
self._LOGGER.debug("Invalid message from {}: {}".format(self, e))
|
||||
elif isinstance(e, MessageDecodeFailed):
|
||||
self._LOGGER.debug("Failed to decrypt message from {}".format(self))
|
||||
elif isinstance(e, asyncio.IncompleteReadError):
|
||||
if self._connected:
|
||||
self._LOGGER.debug("Incomplete read")
|
||||
elif isinstance(e, ConnectionResetError):
|
||||
self._LOGGER.debug(
|
||||
"Connection reset: {}\n{}".format(e, traceback.format_exc())
|
||||
)
|
||||
await self.async_disconnect()
|
||||
|
||||
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))
|
||||
else:
|
||||
self._LOGGER.debug("Received message from {}: {}".format(self, message))
|
||||
_LOGGER.debug("Received message from {}: {}".format(self, message))
|
||||
if message.sequence in self._listeners:
|
||||
sem = self._listeners[message.sequence]
|
||||
if isinstance(sem, asyncio.Semaphore):
|
||||
self._listeners[message.sequence] = message
|
||||
sem.release()
|
||||
else:
|
||||
handler = self._handlers.get(message.command, None)
|
||||
if handler is not None:
|
||||
asyncio.create_task(handler(message))
|
||||
for c in self._handlers.get(message.command, []):
|
||||
asyncio.ensure_future(c(message, self))
|
||||
|
||||
self._response_task = None
|
||||
asyncio.create_task(self._async_handle_message())
|
||||
asyncio.ensure_future(self._async_handle_message())
|
||||
|
||||
async def _async_send(self, message, retries=2):
|
||||
self._LOGGER.debug("Sending to {}: {}".format(self, message))
|
||||
async def _async_send(self, message, retries=4):
|
||||
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):
|
||||
await self.async_disconnect()
|
||||
|
||||
asyncio.ensure_future(self.async_disconnect())
|
||||
raise ConnectionException(
|
||||
"Connection to {} failed: {}".format(self, e)
|
||||
)
|
||||
|
|
@ -877,49 +789,19 @@ class TuyaDevice:
|
|||
raise TuyaException("Failed to send data to {}".format(self))
|
||||
|
||||
if isinstance(e, socket.error):
|
||||
self._LOGGER.debug(
|
||||
_LOGGER.debug(
|
||||
"Retrying send due to error. Connection to {} failed: {}".format(
|
||||
self, e
|
||||
)
|
||||
)
|
||||
elif isinstance(e, asyncio.IncompleteReadError):
|
||||
self._LOGGER.debug(
|
||||
_LOGGER.debug(
|
||||
"Retrying send due to error. Incomplete read from: {} : {}. Partial data recieved: {}".format(
|
||||
self, e, e.partial
|
||||
)
|
||||
)
|
||||
else:
|
||||
self._LOGGER.debug(
|
||||
_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
|
||||
|
|
|
|||
|
|
@ -90,20 +90,17 @@ class TuyaAPISession:
|
|||
country_code = None
|
||||
session_id = None
|
||||
|
||||
def __init__(self, username, region, timezone, phone_code):
|
||||
def __init__(self, username, region, timezone):
|
||||
self.session = requests.session()
|
||||
self.session.headers = DEFAULT_TUYA_HEADERS.copy()
|
||||
self.default_query_params = DEFAULT_TUYA_QUERY_PARAMS.copy()
|
||||
self.default_query_params["deviceId"] = self.generate_new_device_id()
|
||||
self.username = username
|
||||
self.country_code = phone_code
|
||||
self.country_code = self.getCountryCode(region)
|
||||
self.base_url = {
|
||||
"AZ": "https://a1.tuyaus.com",
|
||||
"AY": "https://a1.tuyacn.com",
|
||||
"IN": "https://a1.tuyain.com",
|
||||
"EU": "https://a1.tuyaeu.com",
|
||||
}.get(region, "https://a1.tuyaeu.com")
|
||||
|
||||
"AY": "https://a1.tuyacn.com",
|
||||
}.get(region, "https://a1.tuyaus.com")
|
||||
DEFAULT_TUYA_QUERY_PARAMS["timeZoneId"] = timezone
|
||||
|
||||
@staticmethod
|
||||
|
|
@ -238,5 +235,10 @@ class TuyaAPISession:
|
|||
|
||||
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")
|
||||
|
|
|
|||
|
|
@ -56,7 +56,7 @@ from homeassistant.const import (
|
|||
)
|
||||
|
||||
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 .robovac import (
|
||||
|
|
@ -84,6 +84,7 @@ ATTR_CONSUMABLES = "consumables"
|
|||
ATTR_MODE = "mode"
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
REFRESH_RATE = 20
|
||||
SCAN_INTERVAL = timedelta(seconds=REFRESH_RATE)
|
||||
UPDATE_RETRIES = 3
|
||||
|
||||
|
|
@ -114,7 +115,6 @@ 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=TIMEOUT,
|
||||
ping_interval=PING_RATE,
|
||||
timeout=2,
|
||||
ping_interval=REFRESH_RATE / 2,
|
||||
model_code=self.model_code[0:5],
|
||||
update_entity_state=self.pushed_update_handler,
|
||||
)
|
||||
|
|
@ -316,13 +316,15 @@ class RoboVacEntity(StateVacuumEntity):
|
|||
self.update_entity_values()
|
||||
except TuyaException as e:
|
||||
self.update_failures += 1
|
||||
_LOGGER.warn(
|
||||
"Update errored. Current update failure count: {}. Reason: {}".format(
|
||||
_LOGGER.debug(
|
||||
"Update errored. Current failure count: {}. Reason: {}".format(
|
||||
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"
|
||||
raise e
|
||||
|
||||
async def pushed_update_handler(self):
|
||||
self.update_entity_values()
|
||||
|
|
@ -357,16 +359,11 @@ class RoboVacEntity(StateVacuumEntity):
|
|||
CONSUMABLE_CODE in self.tuyastatus
|
||||
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(
|
||||
"ascii"
|
||||
)
|
||||
)
|
||||
if (
|
||||
"consumable" in consumables
|
||||
and "duration" in consumables["consumable"]
|
||||
):
|
||||
self._attr_consumables = consumables["consumable"]["duration"]
|
||||
)["consumable"]["duration"]
|
||||
|
||||
async def async_locate(self, **kwargs):
|
||||
"""Locate the vacuum cleaner."""
|
||||
|
|
@ -450,4 +447,4 @@ class RoboVacEntity(StateVacuumEntity):
|
|||
await self.vacuum.async_set({"124": base64_str})
|
||||
|
||||
async def async_will_remove_from_hass(self):
|
||||
await self.vacuum.async_disable()
|
||||
await self.vacuum.async_disconnect()
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -1,6 +0,0 @@
|
|||
{
|
||||
"devDependencies": {
|
||||
"conventional-changelog-conventionalcommits": "^7.0.2",
|
||||
"semantic-release": "^23.0.2"
|
||||
}
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
cryptography==41.0.4
|
||||
cryptography==41.0.3
|
||||
homeassistant==2023.8.4
|
||||
Requests==2.31.0
|
||||
setuptools==68.0.0
|
||||
|
|
|
|||
18
setup.py
18
setup.py
|
|
@ -24,18 +24,18 @@ import warnings
|
|||
dynamic_requires = []
|
||||
|
||||
setup(
|
||||
name="robovac",
|
||||
name='robovac',
|
||||
version="1.0",
|
||||
author="Luke Morrigan",
|
||||
url="http://github.com/codefoodpixels/robovac",
|
||||
author='Luke Bonaccorsi',
|
||||
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',
|
||||
],
|
||||
)
|
||||
|
|
|
|||
Loading…
Reference in New Issue