Compare commits

...

3 Commits

Author SHA1 Message Date
epenet
a76c3a6864 Adjust patch 2025-10-03 07:36:32 +00:00
epenet
3ae8f5d955 Use lazy formatting
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-10-03 09:19:37 +02:00
epenet
bbd21876e6 Add workaround for partial local_strategy in Tuya 2025-10-03 07:13:19 +00:00
4 changed files with 143 additions and 4 deletions

View File

@@ -51,7 +51,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: TuyaConfigEntry) -> bool
raise ConfigEntryAuthFailed("Authentication failed. Please re-authenticate.")
token_listener = TokenListener(hass, entry)
manager = Manager(
manager = CustomManager(
TUYA_CLIENT_ID,
entry.data[CONF_USER_CODE],
entry.data[CONF_TERMINAL_ID],
@@ -243,3 +243,32 @@ class TokenListener(SharingTokenListener):
self.hass.config_entries.async_update_entry(self.entry, data=data)
self.hass.add_job(async_update_entry)
# Workaround for https://github.com/home-assistant/core/issues/151239
# When a status update is received with a dpId not present in the
# local_strategy, all subsequent dpId are ignored resulting in
# missing update in Home Assistant.
# Can be removed when fix is implemented in library
# - https://github.com/tuya/tuya-device-sharing-sdk/pull/39
class CustomManager(Manager):
"""Workaround for device status update issue #151239.
Can be removed when fix is implemented in library
https://github.com/tuya/tuya-device-sharing-sdk/pull/39
"""
def _on_device_report(self, device_id: str, status: list[dict[str, Any]]) -> None:
# Patch start
if (device := self.device_map.get(device_id)) and device.support_local:
new_status = []
for item in status:
if (dpId := item.get("dpId")) and dpId not in device.local_strategy:
LOGGER.debug("Ignoring dpId %s missing from local_strategy", dpId)
continue
new_status.append(item)
status = new_status
# Patch end
super()._on_device_report(device_id, status)

View File

@@ -319,6 +319,8 @@ async def initialize_entry(
mock_config_entry.add_to_hass(hass)
# Initialize the component
with patch("homeassistant.components.tuya.Manager", return_value=mock_manager):
with patch(
"homeassistant.components.tuya.CustomManager", return_value=mock_manager
):
await hass.config_entries.async_setup(mock_config_entry.entry_id)
await hass.async_block_till_done()

View File

@@ -73,8 +73,8 @@ async def mock_loaded_entry(
mock_config_entry.add_to_hass(hass)
# Initialize the component
with (
patch("homeassistant.components.tuya.Manager", return_value=mock_manager),
with patch(
"homeassistant.components.tuya.CustomManager", return_value=mock_manager
):
await hass.config_entries.async_setup(mock_config_entry.entry_id)
await hass.async_block_till_done()

View File

@@ -2,9 +2,12 @@
from __future__ import annotations
from unittest.mock import patch
from syrupy.assertion import SnapshotAssertion
from tuya_sharing import CustomerDevice, Manager
from homeassistant.components.tuya import CustomManager
from homeassistant.components.tuya.const import DOMAIN
from homeassistant.core import HomeAssistant
from homeassistant.helpers import device_registry as dr, entity_registry as er
@@ -65,3 +68,108 @@ async def test_fixtures_valid(hass: HomeAssistant) -> None:
assert key not in details, (
f"Please remove data[`'{key}']` from {device_code}.json"
)
async def test_manager_fix() -> None:
"""Test manager fix for Tuya SDK.
See https://github.com/home-assistant/core/issues/151239
dpId `101` is present in the status update but not in the local_strategy, causing
all subsequent dpId (`3`, `15`, `5`, `9`) in the message to be ignored.
"""
with patch("tuya_sharing.manager.CustomerTokenInfo"):
manager = CustomManager("test", "test", "test", "test")
device = CustomerDevice(
support_local=True,
local_strategy={
3: {
"value_convert": "default",
"status_code": "humidity",
"config_item": {
"statusFormat": '{"humidity":"$"}',
"valueDesc": '{"unit":"%","min":0,"max":100,"scale":0,"step":1}',
"valueType": "Integer",
"enumMappingMap": {},
"pid": "rknwi0ctbbghzgla",
},
},
5: {
"value_convert": "default",
"status_code": "temp_current",
"config_item": {
"statusFormat": '{"temp_current":"$"}',
"valueDesc": '{"unit":"","min":0,"max":1000,"scale":1,"step":1}',
"valueType": "Integer",
"enumMappingMap": {},
"pid": "rknwi0ctbbghzgla",
},
},
9: {
"value_convert": "default",
"status_code": "temp_unit_convert",
"config_item": {
"statusFormat": '{"temp_unit_convert":"$"}',
"valueDesc": '{"range":["c","f"]}',
"valueType": "Enum",
"enumMappingMap": {},
"pid": "rknwi0ctbbghzgla",
},
},
15: {
"value_convert": "default",
"status_code": "battery_percentage",
"config_item": {
"statusFormat": '{"battery_percentage":"$"}',
"valueDesc": '{"unit":"%","min":0,"max":100,"scale":0,"step":1}',
"valueType": "Integer",
"enumMappingMap": {},
"pid": "rknwi0ctbbghzgla",
},
},
},
status={
"humidity": 321,
"temp_current": 321,
"temp_unit_convert": "f",
"battery_percentage": 321,
},
)
manager.device_map = {"bfe251cfa8882fae3cpvup": device}
manager.on_message(
{
"protocol": 20,
"data": {
"bizCode": "online",
"bizData": {"devId": "bfe251cfa8882fae3cpvup", "time": 1759226706159},
"ts": 1759226706408,
},
"t": 1759226706408,
}
)
manager.on_message(
{
"protocol": 4,
"data": {
"devId": "bfe251cfa8882fae3cpvup",
"dataId": "2711cd0c-c215-48f4-b535-243640e3b5b3",
"productKey": "rknwi0ctbbghzgla",
"status": [
{"dpId": 101, "t": 1759226706567, "value": 456},
{"dpId": 3, "t": 1759226706567, "value": 87},
{"dpId": 15, "t": 1759226706567, "value": 40},
{"dpId": 5, "t": 1759226706567, "value": 76},
{"dpId": 9, "t": 1759226706567, "value": "c"},
],
},
"t": 1759226707104,
}
)
assert device.status == {
"humidity": 87,
"temp_current": 76,
"temp_unit_convert": "c",
"battery_percentage": 40,
}