Merge branch 'rospogrigio:master' into master
This commit is contained in:
26
.github/workflows/combined.yaml
vendored
26
.github/workflows/combined.yaml
vendored
@@ -1,26 +0,0 @@
|
|||||||
name: "Validation And Formatting"
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
pull_request:
|
|
||||||
schedule:
|
|
||||||
- cron: '0 0 * * *'
|
|
||||||
jobs:
|
|
||||||
ci:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v2
|
|
||||||
name: Download repo
|
|
||||||
with:
|
|
||||||
fetch-depth: 0
|
|
||||||
- uses: actions/setup-python@v2
|
|
||||||
name: Setup Python
|
|
||||||
- uses: actions/cache@v2
|
|
||||||
name: Cache
|
|
||||||
with:
|
|
||||||
path: |
|
|
||||||
~/.cache/pip
|
|
||||||
key: custom-component-ci
|
|
||||||
- uses: KTibow/ha-blueprint@stable
|
|
||||||
name: CI
|
|
||||||
with:
|
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
7
.github/workflows/tox.yaml
vendored
7
.github/workflows/tox.yaml
vendored
@@ -1,6 +1,8 @@
|
|||||||
name: Tox PR CI
|
name: Tox PR CI
|
||||||
|
|
||||||
on: [pull_request]
|
on:
|
||||||
|
pull_request:
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
@@ -14,8 +16,7 @@ jobs:
|
|||||||
platform:
|
platform:
|
||||||
- ubuntu-latest
|
- ubuntu-latest
|
||||||
python-version:
|
python-version:
|
||||||
- 3.7
|
- 3.9
|
||||||
- 3.8
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v2
|
||||||
- name: Set up Python ${{ matrix.python-version }}
|
- name: Set up Python ${{ matrix.python-version }}
|
||||||
|
20
.github/workflows/validate.yml
vendored
Normal file
20
.github/workflows/validate.yml
vendored
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
name: HACS Validate
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
pull_request:
|
||||||
|
schedule:
|
||||||
|
- cron: "0 0 * * *"
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
validate-hacs:
|
||||||
|
runs-on: "ubuntu-latest"
|
||||||
|
steps:
|
||||||
|
- uses: "actions/checkout@v3"
|
||||||
|
- name: HACS validation
|
||||||
|
uses: "hacs/action@main"
|
||||||
|
with:
|
||||||
|
category: "integration"
|
||||||
|
- name: Hassfest validation
|
||||||
|
uses: home-assistant/actions/hassfest@master
|
@@ -1,5 +1,6 @@
|
|||||||
"""Code shared between all platforms."""
|
"""Code shared between all platforms."""
|
||||||
import asyncio
|
import asyncio
|
||||||
|
import json.decoder
|
||||||
import logging
|
import logging
|
||||||
import time
|
import time
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
@@ -176,12 +177,13 @@ class TuyaDevice(pytuya.TuyaListener, pytuya.ContextualLogger):
|
|||||||
|
|
||||||
def async_connect(self):
|
def async_connect(self):
|
||||||
"""Connect to device if not already connected."""
|
"""Connect to device if not already connected."""
|
||||||
|
# self.info("async_connect: %d %r %r", self._is_closing, self._connect_task, self._interface)
|
||||||
if not self._is_closing and self._connect_task is None and not self._interface:
|
if not self._is_closing and self._connect_task is None and not self._interface:
|
||||||
self._connect_task = asyncio.create_task(self._make_connection())
|
self._connect_task = asyncio.create_task(self._make_connection())
|
||||||
|
|
||||||
async def _make_connection(self):
|
async def _make_connection(self):
|
||||||
"""Subscribe localtuya entity events."""
|
"""Subscribe localtuya entity events."""
|
||||||
self.debug("Connecting to %s", self._dev_config_entry[CONF_HOST])
|
self.info("Trying to connect to %s...", self._dev_config_entry[CONF_HOST])
|
||||||
|
|
||||||
try:
|
try:
|
||||||
self._interface = await pytuya.connect(
|
self._interface = await pytuya.connect(
|
||||||
@@ -193,13 +195,16 @@ class TuyaDevice(pytuya.TuyaListener, pytuya.ContextualLogger):
|
|||||||
self,
|
self,
|
||||||
)
|
)
|
||||||
self._interface.add_dps_to_request(self.dps_to_request)
|
self._interface.add_dps_to_request(self.dps_to_request)
|
||||||
except Exception: # pylint: disable=broad-except
|
except Exception as ex: # pylint: disable=broad-except
|
||||||
self.exception(f"Connect to {self._dev_config_entry[CONF_HOST]} failed")
|
self.warning(
|
||||||
|
f"Failed to connect to {self._dev_config_entry[CONF_HOST]}: %s", ex
|
||||||
|
)
|
||||||
if self._interface is not None:
|
if self._interface is not None:
|
||||||
await self._interface.close()
|
await self._interface.close()
|
||||||
self._interface = None
|
self._interface = None
|
||||||
|
|
||||||
if self._interface is not None:
|
if self._interface is not None:
|
||||||
|
try:
|
||||||
try:
|
try:
|
||||||
self.debug("Retrieving initial state")
|
self.debug("Retrieving initial state")
|
||||||
status = await self._interface.status()
|
status = await self._interface.status()
|
||||||
@@ -209,8 +214,7 @@ class TuyaDevice(pytuya.TuyaListener, pytuya.ContextualLogger):
|
|||||||
self._interface.start_heartbeat()
|
self._interface.start_heartbeat()
|
||||||
self.status_updated(status)
|
self.status_updated(status)
|
||||||
|
|
||||||
except Exception as ex: # pylint: disable=broad-except
|
except Exception as ex:
|
||||||
try:
|
|
||||||
if (self._default_reset_dpids is not None) and (
|
if (self._default_reset_dpids is not None) and (
|
||||||
len(self._default_reset_dpids) > 0
|
len(self._default_reset_dpids) > 0
|
||||||
):
|
):
|
||||||
@@ -228,21 +232,14 @@ class TuyaDevice(pytuya.TuyaListener, pytuya.ContextualLogger):
|
|||||||
|
|
||||||
self._interface.start_heartbeat()
|
self._interface.start_heartbeat()
|
||||||
self.status_updated(status)
|
self.status_updated(status)
|
||||||
|
else:
|
||||||
except UnicodeDecodeError as e: # pylint: disable=broad-except
|
self.error("Initial state update failed, giving up: %r", ex)
|
||||||
self.exception(
|
|
||||||
f"Connect to {self._dev_config_entry[CONF_HOST]} failed: %s",
|
|
||||||
type(e),
|
|
||||||
)
|
|
||||||
if self._interface is not None:
|
if self._interface is not None:
|
||||||
await self._interface.close()
|
await self._interface.close()
|
||||||
self._interface = None
|
self._interface = None
|
||||||
|
|
||||||
except Exception as e: # pylint: disable=broad-except
|
except (UnicodeDecodeError, json.decoder.JSONDecodeError) as ex:
|
||||||
self.exception(
|
self.warning("Initial state update failed (%s), trying key update", ex)
|
||||||
f"Connect to {self._dev_config_entry[CONF_HOST]} failed"
|
|
||||||
)
|
|
||||||
if "json.decode" in str(type(e)):
|
|
||||||
await self.update_local_key()
|
await self.update_local_key()
|
||||||
|
|
||||||
if self._interface is not None:
|
if self._interface is not None:
|
||||||
@@ -270,14 +267,16 @@ class TuyaDevice(pytuya.TuyaListener, pytuya.ContextualLogger):
|
|||||||
|
|
||||||
if (
|
if (
|
||||||
CONF_SCAN_INTERVAL in self._dev_config_entry
|
CONF_SCAN_INTERVAL in self._dev_config_entry
|
||||||
and self._dev_config_entry[CONF_SCAN_INTERVAL] > 0
|
and int(self._dev_config_entry[CONF_SCAN_INTERVAL]) > 0
|
||||||
):
|
):
|
||||||
self._unsub_interval = async_track_time_interval(
|
self._unsub_interval = async_track_time_interval(
|
||||||
self._hass,
|
self._hass,
|
||||||
self._async_refresh,
|
self._async_refresh,
|
||||||
timedelta(seconds=self._dev_config_entry[CONF_SCAN_INTERVAL]),
|
timedelta(seconds=int(self._dev_config_entry[CONF_SCAN_INTERVAL])),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
self.info(f"Successfully connected to {self._dev_config_entry[CONF_HOST]}")
|
||||||
|
|
||||||
self._connect_task = None
|
self._connect_task = None
|
||||||
|
|
||||||
async def update_local_key(self):
|
async def update_local_key(self):
|
||||||
@@ -310,7 +309,7 @@ class TuyaDevice(pytuya.TuyaListener, pytuya.ContextualLogger):
|
|||||||
await self._interface.close()
|
await self._interface.close()
|
||||||
if self._disconnect_task is not None:
|
if self._disconnect_task is not None:
|
||||||
self._disconnect_task()
|
self._disconnect_task()
|
||||||
self.debug(
|
self.info(
|
||||||
"Closed connection with device %s.",
|
"Closed connection with device %s.",
|
||||||
self._dev_config_entry[CONF_FRIENDLY_NAME],
|
self._dev_config_entry[CONF_FRIENDLY_NAME],
|
||||||
)
|
)
|
||||||
@@ -358,7 +357,11 @@ class TuyaDevice(pytuya.TuyaListener, pytuya.ContextualLogger):
|
|||||||
self._unsub_interval()
|
self._unsub_interval()
|
||||||
self._unsub_interval = None
|
self._unsub_interval = None
|
||||||
self._interface = None
|
self._interface = None
|
||||||
self.debug("Disconnected - waiting for discovery broadcast")
|
|
||||||
|
if self._connect_task is not None:
|
||||||
|
self._connect_task.cancel()
|
||||||
|
self._connect_task = None
|
||||||
|
self.warning("Disconnected - waiting for discovery broadcast")
|
||||||
|
|
||||||
|
|
||||||
class LocalTuyaEntity(RestoreEntity, pytuya.ContextualLogger):
|
class LocalTuyaEntity(RestoreEntity, pytuya.ContextualLogger):
|
||||||
|
@@ -43,6 +43,7 @@ from .const import (
|
|||||||
CONF_RESET_DPIDS,
|
CONF_RESET_DPIDS,
|
||||||
CONF_SETUP_CLOUD,
|
CONF_SETUP_CLOUD,
|
||||||
CONF_USER_ID,
|
CONF_USER_ID,
|
||||||
|
CONF_ENABLE_ADD_ENTITIES,
|
||||||
DATA_CLOUD,
|
DATA_CLOUD,
|
||||||
DATA_DISCOVERY,
|
DATA_DISCOVERY,
|
||||||
DOMAIN,
|
DOMAIN,
|
||||||
@@ -140,12 +141,13 @@ def options_schema(entities):
|
|||||||
["3.1", "3.2", "3.3", "3.4"]
|
["3.1", "3.2", "3.3", "3.4"]
|
||||||
),
|
),
|
||||||
vol.Required(CONF_ENABLE_DEBUG, default=False): bool,
|
vol.Required(CONF_ENABLE_DEBUG, default=False): bool,
|
||||||
vol.Optional(CONF_SCAN_INTERVAL): cv.string,
|
vol.Optional(CONF_SCAN_INTERVAL): int,
|
||||||
vol.Optional(CONF_MANUAL_DPS): cv.string,
|
vol.Optional(CONF_MANUAL_DPS): cv.string,
|
||||||
vol.Optional(CONF_RESET_DPIDS): cv.string,
|
vol.Optional(CONF_RESET_DPIDS): cv.string,
|
||||||
vol.Required(
|
vol.Required(
|
||||||
CONF_ENTITIES, description={"suggested_value": entity_names}
|
CONF_ENTITIES, description={"suggested_value": entity_names}
|
||||||
): cv.multi_select(entity_names),
|
): cv.multi_select(entity_names),
|
||||||
|
vol.Required(CONF_ENABLE_ADD_ENTITIES, default=False): bool,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -256,20 +258,21 @@ async def validate_input(hass: core.HomeAssistant, data):
|
|||||||
)
|
)
|
||||||
try:
|
try:
|
||||||
detected_dps = await interface.detect_available_dps()
|
detected_dps = await interface.detect_available_dps()
|
||||||
except Exception: # pylint: disable=broad-except
|
except Exception as ex:
|
||||||
try:
|
try:
|
||||||
_LOGGER.debug("Initial state update failed, trying reset command")
|
_LOGGER.debug(
|
||||||
|
"Initial state update failed (%s), trying reset command", ex
|
||||||
|
)
|
||||||
if len(reset_ids) > 0:
|
if len(reset_ids) > 0:
|
||||||
await interface.reset(reset_ids)
|
await interface.reset(reset_ids)
|
||||||
detected_dps = await interface.detect_available_dps()
|
detected_dps = await interface.detect_available_dps()
|
||||||
except Exception: # pylint: disable=broad-except
|
except Exception as ex:
|
||||||
_LOGGER.debug("No DPS able to be detected")
|
_LOGGER.debug("No DPS able to be detected: %s", ex)
|
||||||
detected_dps = {}
|
detected_dps = {}
|
||||||
|
|
||||||
# if manual DPs are set, merge these.
|
# if manual DPs are set, merge these.
|
||||||
_LOGGER.debug("Detected DPS: %s", detected_dps)
|
_LOGGER.debug("Detected DPS: %s", detected_dps)
|
||||||
if CONF_MANUAL_DPS in data:
|
if CONF_MANUAL_DPS in data:
|
||||||
|
|
||||||
manual_dps_list = [dps.strip() for dps in data[CONF_MANUAL_DPS].split(",")]
|
manual_dps_list = [dps.strip() for dps in data[CONF_MANUAL_DPS].split(",")]
|
||||||
_LOGGER.debug(
|
_LOGGER.debug(
|
||||||
"Manual DPS Setting: %s (%s)", data[CONF_MANUAL_DPS], manual_dps_list
|
"Manual DPS Setting: %s (%s)", data[CONF_MANUAL_DPS], manual_dps_list
|
||||||
@@ -493,8 +496,8 @@ class LocalTuyaOptionsFlowHandler(config_entries.OptionsFlow):
|
|||||||
errors["base"] = "address_in_use"
|
errors["base"] = "address_in_use"
|
||||||
else:
|
else:
|
||||||
errors["base"] = "discovery_failed"
|
errors["base"] = "discovery_failed"
|
||||||
except Exception: # pylint: disable= broad-except
|
except Exception as ex:
|
||||||
_LOGGER.exception("discovery failed")
|
_LOGGER.exception("discovery failed: %s", ex)
|
||||||
errors["base"] = "discovery_failed"
|
errors["base"] = "discovery_failed"
|
||||||
|
|
||||||
devices = {
|
devices = {
|
||||||
@@ -553,6 +556,17 @@ class LocalTuyaOptionsFlowHandler(config_entries.OptionsFlow):
|
|||||||
CONF_PRODUCT_NAME
|
CONF_PRODUCT_NAME
|
||||||
)
|
)
|
||||||
if self.editing_device:
|
if self.editing_device:
|
||||||
|
if user_input[CONF_ENABLE_ADD_ENTITIES]:
|
||||||
|
self.editing_device = False
|
||||||
|
user_input[CONF_DEVICE_ID] = dev_id
|
||||||
|
self.device_data.update(
|
||||||
|
{
|
||||||
|
CONF_DEVICE_ID: dev_id,
|
||||||
|
CONF_DPS_STRINGS: self.dps_strings,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
return await self.async_step_pick_entity_type()
|
||||||
|
|
||||||
self.device_data.update(
|
self.device_data.update(
|
||||||
{
|
{
|
||||||
CONF_DEVICE_ID: dev_id,
|
CONF_DEVICE_ID: dev_id,
|
||||||
@@ -586,16 +600,29 @@ class LocalTuyaOptionsFlowHandler(config_entries.OptionsFlow):
|
|||||||
errors["base"] = "invalid_auth"
|
errors["base"] = "invalid_auth"
|
||||||
except EmptyDpsList:
|
except EmptyDpsList:
|
||||||
errors["base"] = "empty_dps"
|
errors["base"] = "empty_dps"
|
||||||
except Exception: # pylint: disable=broad-except
|
except Exception as ex:
|
||||||
_LOGGER.exception("Unexpected exception")
|
_LOGGER.exception("Unexpected exception: %s", ex)
|
||||||
errors["base"] = "unknown"
|
errors["base"] = "unknown"
|
||||||
|
|
||||||
defaults = {}
|
defaults = {}
|
||||||
if self.editing_device:
|
if self.editing_device:
|
||||||
# If selected device exists as a config entry, load config from it
|
# If selected device exists as a config entry, load config from it
|
||||||
defaults = self.config_entry.data[CONF_DEVICES][dev_id].copy()
|
defaults = self.config_entry.data[CONF_DEVICES][dev_id].copy()
|
||||||
schema = schema_defaults(options_schema(self.entities), **defaults)
|
cloud_devs = self.hass.data[DOMAIN][DATA_CLOUD].device_list
|
||||||
placeholders = {"for_device": f" for device `{dev_id}`"}
|
placeholders = {"for_device": f" for device `{dev_id}`"}
|
||||||
|
if dev_id in cloud_devs:
|
||||||
|
cloud_local_key = cloud_devs[dev_id].get(CONF_LOCAL_KEY)
|
||||||
|
if defaults[CONF_LOCAL_KEY] != cloud_local_key:
|
||||||
|
_LOGGER.info(
|
||||||
|
"New local_key detected: new %s vs old %s",
|
||||||
|
cloud_local_key,
|
||||||
|
defaults[CONF_LOCAL_KEY],
|
||||||
|
)
|
||||||
|
defaults[CONF_LOCAL_KEY] = cloud_devs[dev_id].get(CONF_LOCAL_KEY)
|
||||||
|
note = "\nNOTE: a new local_key has been retrieved using cloud API"
|
||||||
|
placeholders = {"for_device": f" for device `{dev_id}`.{note}"}
|
||||||
|
defaults[CONF_ENABLE_ADD_ENTITIES] = False
|
||||||
|
schema = schema_defaults(options_schema(self.entities), **defaults)
|
||||||
else:
|
else:
|
||||||
defaults[CONF_PROTOCOL_VERSION] = "3.3"
|
defaults[CONF_PROTOCOL_VERSION] = "3.3"
|
||||||
defaults[CONF_HOST] = ""
|
defaults[CONF_HOST] = ""
|
||||||
@@ -634,17 +661,6 @@ class LocalTuyaOptionsFlowHandler(config_entries.OptionsFlow):
|
|||||||
}
|
}
|
||||||
|
|
||||||
dev_id = self.device_data.get(CONF_DEVICE_ID)
|
dev_id = self.device_data.get(CONF_DEVICE_ID)
|
||||||
if dev_id in self.config_entry.data[CONF_DEVICES]:
|
|
||||||
self.hass.config_entries.async_update_entry(
|
|
||||||
self.config_entry, data=config
|
|
||||||
)
|
|
||||||
return self.async_abort(
|
|
||||||
reason="device_success",
|
|
||||||
description_placeholders={
|
|
||||||
"dev_name": config.get(CONF_FRIENDLY_NAME),
|
|
||||||
"action": "updated",
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
new_data = self.config_entry.data.copy()
|
new_data = self.config_entry.data.copy()
|
||||||
new_data[ATTR_UPDATED_AT] = str(int(time.time() * 1000))
|
new_data[ATTR_UPDATED_AT] = str(int(time.time() * 1000))
|
||||||
@@ -727,7 +743,7 @@ class LocalTuyaOptionsFlowHandler(config_entries.OptionsFlow):
|
|||||||
new_data = self.config_entry.data.copy()
|
new_data = self.config_entry.data.copy()
|
||||||
entry_id = self.config_entry.entry_id
|
entry_id = self.config_entry.entry_id
|
||||||
# removing entities from registry (they will be recreated)
|
# removing entities from registry (they will be recreated)
|
||||||
ent_reg = await er.async_get_registry(self.hass)
|
ent_reg = er.async_get(self.hass)
|
||||||
reg_entities = {
|
reg_entities = {
|
||||||
ent.unique_id: ent.entity_id
|
ent.unique_id: ent.entity_id
|
||||||
for ent in er.async_entries_for_config_entry(ent_reg, entry_id)
|
for ent in er.async_entries_for_config_entry(ent_reg, entry_id)
|
||||||
|
@@ -35,6 +35,7 @@ CONF_MODEL = "model"
|
|||||||
CONF_PRODUCT_KEY = "product_key"
|
CONF_PRODUCT_KEY = "product_key"
|
||||||
CONF_PRODUCT_NAME = "product_name"
|
CONF_PRODUCT_NAME = "product_name"
|
||||||
CONF_USER_ID = "user_id"
|
CONF_USER_ID = "user_id"
|
||||||
|
CONF_ENABLE_ADD_ENTITIES = "add_entities"
|
||||||
|
|
||||||
|
|
||||||
CONF_ACTION = "action"
|
CONF_ACTION = "action"
|
||||||
|
@@ -1,14 +1,14 @@
|
|||||||
{
|
{
|
||||||
"domain": "localtuya",
|
"domain": "localtuya",
|
||||||
"name": "LocalTuya integration",
|
"name": "LocalTuya integration",
|
||||||
"version": "5.0.0",
|
|
||||||
"documentation": "https://github.com/rospogrigio/localtuya/",
|
|
||||||
"dependencies": [],
|
|
||||||
"codeowners": [
|
"codeowners": [
|
||||||
"@rospogrigio", "@postlund"
|
"@rospogrigio", "@postlund"
|
||||||
],
|
],
|
||||||
|
"config_flow": true,
|
||||||
|
"dependencies": [],
|
||||||
|
"documentation": "https://github.com/rospogrigio/localtuya/",
|
||||||
|
"iot_class": "local_push",
|
||||||
"issue_tracker": "https://github.com/rospogrigio/localtuya/issues",
|
"issue_tracker": "https://github.com/rospogrigio/localtuya/issues",
|
||||||
"requirements": [],
|
"requirements": [],
|
||||||
"config_flow": true,
|
"version": "5.2.1"
|
||||||
"iot_class": "local_push"
|
|
||||||
}
|
}
|
||||||
|
@@ -444,11 +444,14 @@ class MessageDispatcher(ContextualLogger):
|
|||||||
if seqno in self.listeners:
|
if seqno in self.listeners:
|
||||||
raise Exception(f"listener exists for {seqno}")
|
raise Exception(f"listener exists for {seqno}")
|
||||||
|
|
||||||
self.debug("Command %d waiting for sequence number %d", cmd, seqno)
|
self.debug("Command %d waiting for seq. number %d", cmd, seqno)
|
||||||
self.listeners[seqno] = asyncio.Semaphore(0)
|
self.listeners[seqno] = asyncio.Semaphore(0)
|
||||||
try:
|
try:
|
||||||
await asyncio.wait_for(self.listeners[seqno].acquire(), timeout=timeout)
|
await asyncio.wait_for(self.listeners[seqno].acquire(), timeout=timeout)
|
||||||
except asyncio.TimeoutError:
|
except asyncio.TimeoutError:
|
||||||
|
self.debug(
|
||||||
|
"Command %d timed out waiting for sequence number %d", cmd, seqno
|
||||||
|
)
|
||||||
del self.listeners[seqno]
|
del self.listeners[seqno]
|
||||||
raise
|
raise
|
||||||
|
|
||||||
@@ -478,8 +481,11 @@ class MessageDispatcher(ContextualLogger):
|
|||||||
if msg.seqno in self.listeners:
|
if msg.seqno in self.listeners:
|
||||||
# self.debug("Dispatching sequence number %d", msg.seqno)
|
# self.debug("Dispatching sequence number %d", msg.seqno)
|
||||||
sem = self.listeners[msg.seqno]
|
sem = self.listeners[msg.seqno]
|
||||||
|
if isinstance(sem, asyncio.Semaphore):
|
||||||
self.listeners[msg.seqno] = msg
|
self.listeners[msg.seqno] = msg
|
||||||
sem.release()
|
sem.release()
|
||||||
|
else:
|
||||||
|
self.debug("Got additional message without request - skipping: %s", sem)
|
||||||
elif msg.cmd == HEART_BEAT:
|
elif msg.cmd == HEART_BEAT:
|
||||||
self.debug("Got heartbeat response")
|
self.debug("Got heartbeat response")
|
||||||
if self.HEARTBEAT_SEQNO in self.listeners:
|
if self.HEARTBEAT_SEQNO in self.listeners:
|
||||||
@@ -511,7 +517,7 @@ class MessageDispatcher(ContextualLogger):
|
|||||||
if msg.cmd == CONTROL_NEW:
|
if msg.cmd == CONTROL_NEW:
|
||||||
self.debug("Got ACK message for command %d: will ignore it", msg.cmd)
|
self.debug("Got ACK message for command %d: will ignore it", msg.cmd)
|
||||||
else:
|
else:
|
||||||
self.error(
|
self.debug(
|
||||||
"Got message type %d for unknown listener %d: %s",
|
"Got message type %d for unknown listener %d: %s",
|
||||||
msg.cmd,
|
msg.cmd,
|
||||||
msg.seqno,
|
msg.seqno,
|
||||||
@@ -881,8 +887,10 @@ class TuyaProtocol(asyncio.Protocol, ContextualLogger):
|
|||||||
try:
|
try:
|
||||||
# self.debug("decrypting=%r", payload)
|
# self.debug("decrypting=%r", payload)
|
||||||
payload = cipher.decrypt(payload, False, decode_text=False)
|
payload = cipher.decrypt(payload, False, decode_text=False)
|
||||||
except Exception:
|
except Exception as ex:
|
||||||
self.debug("incomplete payload=%r (len:%d)", payload, len(payload))
|
self.debug(
|
||||||
|
"incomplete payload=%r with len:%d (%s)", payload, len(payload), ex
|
||||||
|
)
|
||||||
return self.error_json(ERR_PAYLOAD)
|
return self.error_json(ERR_PAYLOAD)
|
||||||
|
|
||||||
# self.debug("decrypted 3.x payload=%r", payload)
|
# self.debug("decrypted 3.x payload=%r", payload)
|
||||||
@@ -907,8 +915,13 @@ class TuyaProtocol(asyncio.Protocol, ContextualLogger):
|
|||||||
try:
|
try:
|
||||||
# self.debug("decrypting=%r", payload)
|
# self.debug("decrypting=%r", payload)
|
||||||
payload = cipher.decrypt(payload, False)
|
payload = cipher.decrypt(payload, False)
|
||||||
except Exception:
|
except Exception as ex:
|
||||||
self.debug("incomplete payload=%r (len:%d)", payload, len(payload))
|
self.debug(
|
||||||
|
"incomplete payload=%r with len:%d (%s)",
|
||||||
|
payload,
|
||||||
|
len(payload),
|
||||||
|
ex,
|
||||||
|
)
|
||||||
return self.error_json(ERR_PAYLOAD)
|
return self.error_json(ERR_PAYLOAD)
|
||||||
|
|
||||||
# self.debug("decrypted 3.x payload=%r", payload)
|
# self.debug("decrypted 3.x payload=%r", payload)
|
||||||
@@ -917,9 +930,11 @@ class TuyaProtocol(asyncio.Protocol, ContextualLogger):
|
|||||||
if not isinstance(payload, str):
|
if not isinstance(payload, str):
|
||||||
try:
|
try:
|
||||||
payload = payload.decode()
|
payload = payload.decode()
|
||||||
except Exception:
|
except Exception as ex:
|
||||||
self.debug("payload was not string type and decoding failed")
|
self.debug("payload was not string type and decoding failed")
|
||||||
return self.error_json(ERR_JSON, payload)
|
raise DecodeError("payload was not a string: %s" % ex)
|
||||||
|
# return self.error_json(ERR_JSON, payload)
|
||||||
|
|
||||||
if "data unvalid" in payload:
|
if "data unvalid" in payload:
|
||||||
self.dev_type = "type_0d"
|
self.dev_type = "type_0d"
|
||||||
self.debug(
|
self.debug(
|
||||||
@@ -936,8 +951,11 @@ class TuyaProtocol(asyncio.Protocol, ContextualLogger):
|
|||||||
self.debug("Deciphered data = %r", payload)
|
self.debug("Deciphered data = %r", payload)
|
||||||
try:
|
try:
|
||||||
json_payload = json.loads(payload)
|
json_payload = json.loads(payload)
|
||||||
except Exception:
|
except Exception as ex:
|
||||||
json_payload = self.error_json(ERR_JSON, payload)
|
raise DecodeError(
|
||||||
|
"could not decrypt data: wrong local_key? (exception: %s)" % ex
|
||||||
|
)
|
||||||
|
# json_payload = self.error_json(ERR_JSON, payload)
|
||||||
|
|
||||||
# v3.4 stuffs it into {"data":{"dps":{"1":true}}, ...}
|
# v3.4 stuffs it into {"data":{"dps":{"1":true}}, ...}
|
||||||
if (
|
if (
|
||||||
@@ -971,11 +989,12 @@ class TuyaProtocol(asyncio.Protocol, ContextualLogger):
|
|||||||
# self.debug("decrypting %r using %r", payload, self.real_local_key)
|
# self.debug("decrypting %r using %r", payload, self.real_local_key)
|
||||||
cipher = AESCipher(self.real_local_key)
|
cipher = AESCipher(self.real_local_key)
|
||||||
payload = cipher.decrypt(payload, False, decode_text=False)
|
payload = cipher.decrypt(payload, False, decode_text=False)
|
||||||
except Exception:
|
except Exception as ex:
|
||||||
self.debug(
|
self.debug(
|
||||||
"session key step 2 decrypt failed, payload=%r (len:%d)",
|
"session key step 2 decrypt failed, payload=%r with len:%d (%s)",
|
||||||
payload,
|
payload,
|
||||||
len(payload),
|
len(payload),
|
||||||
|
ex,
|
||||||
)
|
)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
@@ -99,6 +99,7 @@
|
|||||||
"enable_debug": "Enable debugging for this device (debug must be enabled also in configuration.yaml)",
|
"enable_debug": "Enable debugging for this device (debug must be enabled also in configuration.yaml)",
|
||||||
"scan_interval": "Scan interval (seconds, only when not updating automatically)",
|
"scan_interval": "Scan interval (seconds, only when not updating automatically)",
|
||||||
"entities": "Entities (uncheck an entity to remove it)",
|
"entities": "Entities (uncheck an entity to remove it)",
|
||||||
|
"add_entities": "Add more entities in 'edit device' mode",
|
||||||
"manual_dps_strings": "Manual DPS to add (separated by commas ',') - used when detection is not working (optional)",
|
"manual_dps_strings": "Manual DPS to add (separated by commas ',') - used when detection is not working (optional)",
|
||||||
"reset_dpids": "DPIDs to send in RESET command (separated by commas ',')- Used when device does not respond to status requests after turning on (optional)"
|
"reset_dpids": "DPIDs to send in RESET command (separated by commas ',')- Used when device does not respond to status requests after turning on (optional)"
|
||||||
}
|
}
|
||||||
|
@@ -1,6 +1,4 @@
|
|||||||
{
|
{
|
||||||
"name": "Local Tuya",
|
"name": "Local Tuya",
|
||||||
"domains": ["climate", "cover", "fan", "light", "number", "select", "sensor", "switch"],
|
"homeassistant": "0.116.0"
|
||||||
"homeassistant": "0.116.0",
|
|
||||||
"iot_class": ["Local Push"]
|
|
||||||
}
|
}
|
||||||
|
@@ -176,7 +176,8 @@ disable=line-too-long,
|
|||||||
dangerous-default-value,
|
dangerous-default-value,
|
||||||
unreachable,
|
unreachable,
|
||||||
unnecessary-pass,
|
unnecessary-pass,
|
||||||
broad-except
|
broad-except,
|
||||||
|
raise-missing-from
|
||||||
|
|
||||||
# Enable the message, report, category or checker with the given id(s). You can
|
# Enable the message, report, category or checker with the given id(s). You can
|
||||||
# either give multiple identifier separated by comma (,) or put this option
|
# either give multiple identifier separated by comma (,) or put this option
|
||||||
|
3
tox.ini
3
tox.ini
@@ -6,12 +6,11 @@ cs_exclude_words = hass,unvalid
|
|||||||
|
|
||||||
[gh-actions]
|
[gh-actions]
|
||||||
python =
|
python =
|
||||||
3.8: clean, py38, lint, typing
|
|
||||||
3.9: clean, py39, lint, typing
|
3.9: clean, py39, lint, typing
|
||||||
|
|
||||||
[testenv]
|
[testenv]
|
||||||
passenv = TOXENV,CI
|
passenv = TOXENV,CI
|
||||||
whitelist_externals =
|
allowlist_externals =
|
||||||
true
|
true
|
||||||
setenv =
|
setenv =
|
||||||
LANG=en_US.UTF-8
|
LANG=en_US.UTF-8
|
||||||
|
Reference in New Issue
Block a user