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
|
# 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.
|
||||||
|
|
|
||||||
|
|
@ -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
|
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
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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
|
|
||||||
|
|
|
||||||
|
|
@ -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",
|
"T2251",
|
||||||
"T2252",
|
"T2252",
|
||||||
"T2253",
|
"T2253",
|
||||||
"T2254",
|
|
||||||
"T2150",
|
"T2150",
|
||||||
"T2255",
|
"T2255",
|
||||||
"T2256",
|
"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": {
|
"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"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
|
||||||
|
|
|
||||||
|
|
@ -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")
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
|
|
|
||||||
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
|
homeassistant==2023.8.4
|
||||||
Requests==2.31.0
|
Requests==2.31.0
|
||||||
setuptools==68.0.0
|
setuptools==68.0.0
|
||||||
|
|
|
||||||
18
setup.py
18
setup.py
|
|
@ -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',
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue