Log warning for incorrect Tuya enum values (#156541)

This commit is contained in:
epenet
2025-11-22 15:04:01 +01:00
committed by GitHub
parent 0b96da3b24
commit c0772f3957
4 changed files with 202 additions and 7 deletions

View File

@@ -14,6 +14,7 @@ from homeassistant.util import dt as dt_util
from . import TuyaConfigEntry
from .const import DOMAIN, DPCode
from .models import DEVICE_WARNINGS
_REDACTED_DPCODES = {
DPCode.ALARM_MESSAGE,
@@ -97,6 +98,7 @@ def _async_device_as_dict(
"home_assistant": {},
"set_up": device.set_up,
"support_local": device.support_local,
"warnings": DEVICE_WARNINGS.get(device.id),
}
# Gather Tuya states

View File

@@ -11,9 +11,27 @@ from tuya_sharing import CustomerDevice
from homeassistant.util.json import json_loads, json_loads_object
from .const import DPCode, DPType
from .const import LOGGER, DPCode, DPType
from .util import parse_dptype, remap_value
# Dictionary to track logged warnings to avoid spamming logs
# Keyed by device ID
DEVICE_WARNINGS: dict[str, set[str]] = {}
def _should_log_warning(device_id: str, warning_key: str) -> bool:
"""Check if a warning has already been logged for a device and add it if not.
Returns: False if the warning was already logged, True if it was added.
"""
if (device_warnings := DEVICE_WARNINGS.get(device_id)) is None:
device_warnings = set()
DEVICE_WARNINGS[device_id] = device_warnings
if warning_key in device_warnings:
return False
DEVICE_WARNINGS[device_id].add(warning_key)
return True
@dataclass(kw_only=True)
class TypeInformation:
@@ -285,11 +303,22 @@ class DPCodeEnumWrapper(DPCodeTypeInformationWrapper[EnumTypeData]):
Values outside of the list defined by the Enum type information will
return None.
"""
if (
raw_value := self._read_device_status_raw(device)
) in self.type_information.range:
return raw_value
return None
if (raw_value := self._read_device_status_raw(device)) is None:
return None
if raw_value not in self.type_information.range:
if _should_log_warning(
device.id, f"enum_out_range|{self.dpcode}|{raw_value}"
):
LOGGER.warning(
"Found invalid enum value `%s` for datapoint `%s` in product id `%s`,"
" expected one of `%s`; please report this defect to Tuya support",
raw_value,
self.dpcode,
device.product_id,
self.type_information.range,
)
return None
return raw_value
def _convert_value_to_raw_value(self, device: CustomerDevice, value: Any) -> Any:
"""Convert a Home Assistant value back to a raw device value."""

View File

@@ -298,6 +298,7 @@
'terminal_id': '7cd96aff-6ec8-4006-b093-3dbff7947591',
'time_zone': '+02:00',
'update_time': '2024-12-02T20:08:56+00:00',
'warnings': None,
})
# ---
# name: test_device_diagnostics[rqbj_4iqe2hsfyd86kwwc]
@@ -413,6 +414,167 @@
'terminal_id': '7cd96aff-6ec8-4006-b093-3dbff7947591',
'time_zone': '-04:00',
'update_time': '2025-06-24T20:33:10+00:00',
'warnings': None,
})
# ---
# name: test_device_diagnostics[tdq_9htyiowaf5rtdhrv]
dict({
'active_time': '2024-09-08T13:46:46+00:00',
'category': 'tdq',
'create_time': '2024-09-08T13:46:46+00:00',
'disabled_by': None,
'disabled_polling': False,
'endpoint': 'https://apigw.tuyaeu.com',
'function': dict({
'countdown_1': dict({
'type': 'Integer',
'value': '{"unit":"s","min":0,"max":86400,"scale":0,"step":1}',
}),
'cycle_time': dict({
'type': 'String',
'value': '{"maxlen":255}',
}),
'random_time': dict({
'type': 'String',
'value': '{"maxlen":255}',
}),
'relay_status': dict({
'type': 'Enum',
'value': '{"range":["0","1","2"]}',
}),
'remote_add': dict({
'type': 'Raw',
'value': '{}',
}),
'remote_list': dict({
'type': 'Raw',
'value': '{}',
}),
'switch_1': dict({
'type': 'Boolean',
'value': '{}',
}),
'switch_inching': dict({
'type': 'String',
'value': '{"maxlen":255}',
}),
'switch_type': dict({
'type': 'Enum',
'value': '{"range":["flip","sync","button"]}',
}),
}),
'home_assistant': dict({
'disabled': False,
'disabled_by': None,
'entities': list([
dict({
'device_class': None,
'disabled': False,
'disabled_by': None,
'entity_category': 'config',
'icon': None,
'original_device_class': None,
'original_icon': None,
'state': dict({
'attributes': dict({
'friendly_name': 'Framboisiers Power on behavior',
'options': list([
'0',
'1',
'2',
]),
}),
'entity_id': 'select.framboisiers_power_on_behavior',
'state': 'unknown',
}),
'unit_of_measurement': None,
}),
dict({
'device_class': None,
'disabled': False,
'disabled_by': None,
'entity_category': None,
'icon': None,
'original_device_class': 'outlet',
'original_icon': None,
'state': dict({
'attributes': dict({
'device_class': 'outlet',
'friendly_name': 'Framboisiers Switch 1',
}),
'entity_id': 'switch.framboisiers_switch_1',
'state': 'off',
}),
'unit_of_measurement': None,
}),
]),
'name': 'Framboisiers',
'name_by_user': None,
}),
'id': 'vrhdtr5fawoiyth9qdt',
'mqtt_connected': True,
'name': 'Framboisiers',
'online': True,
'product_id': '9htyiowaf5rtdhrv',
'product_name': '1-433',
'set_up': True,
'status': dict({
'countdown_1': 0,
'cycle_time': '',
'random_time': '',
'relay_status': 2,
'remote_add': '',
'remote_list': 'AA==',
'switch_1': False,
'switch_inching': 'AAAC',
'switch_type': 'button',
}),
'status_range': dict({
'countdown_1': dict({
'type': 'Integer',
'value': '{"unit":"s","min":0,"max":86400,"scale":0,"step":1}',
}),
'cycle_time': dict({
'type': 'String',
'value': '{"maxlen":255}',
}),
'random_time': dict({
'type': 'String',
'value': '{"maxlen":255}',
}),
'relay_status': dict({
'type': 'Enum',
'value': '{"range":["0","1","2"]}',
}),
'remote_add': dict({
'type': 'Raw',
'value': '{}',
}),
'remote_list': dict({
'type': 'Raw',
'value': '{}',
}),
'switch_1': dict({
'type': 'Boolean',
'value': '{}',
}),
'switch_inching': dict({
'type': 'String',
'value': '{"maxlen":255}',
}),
'switch_type': dict({
'type': 'Enum',
'value': '{"range":["flip","sync","button"]}',
}),
}),
'sub': False,
'support_local': True,
'terminal_id': '7cd96aff-6ec8-4006-b093-3dbff7947591',
'time_zone': '+02:00',
'update_time': '2024-09-08T13:46:46+00:00',
'warnings': list([
'enum_out_range|relay_status|2',
]),
})
# ---
# name: test_entry_diagnostics[rqbj_4iqe2hsfyd86kwwc]
@@ -525,6 +687,7 @@
'support_local': True,
'time_zone': '-04:00',
'update_time': '2025-06-24T20:33:10+00:00',
'warnings': None,
}),
]),
'disabled_by': None,

View File

@@ -45,8 +45,9 @@ async def test_entry_diagnostics(
@pytest.mark.parametrize(
"mock_device_code",
[
"mal_gyitctrjj1kefxp2",
"rqbj_4iqe2hsfyd86kwwc",
"mal_gyitctrjj1kefxp2", # with redacted dpcodes
"tdq_9htyiowaf5rtdhrv", # with bad enum warnings
],
)
async def test_device_diagnostics(