Compare commits

...

24 Commits

Author SHA1 Message Date
epenet
0d78e7459b Improve 2025-11-04 14:09:16 +00:00
epenet
3246692998 Update 2025-11-03 14:33:51 +00:00
epenet
c3ade123be Improve 2025-11-03 14:32:56 +00:00
epenet
14bd55426d Adjust 2025-11-03 13:25:59 +00:00
epenet
9fa0974c8c Refactor 2025-11-03 12:00:10 +00:00
epenet
1f2af23dff Add comment 2025-11-03 08:31:07 +00:00
epenet
69580d8023 Improve 2025-11-03 08:31:07 +00:00
epenet
4d60d1f2ab Improve 2025-11-03 08:31:06 +00:00
epenet
d26bb6eeba Add validation 2025-11-03 08:31:06 +00:00
epenet
0a31ca153f Fix __all__ 2025-11-03 08:31:06 +00:00
epenet
2bb2153b9c Refactor split between quirks and device_quirks 2025-11-03 08:31:05 +00:00
epenet
e201adf4e0 Move quirks location 2025-11-03 08:30:49 +00:00
epenet
29282ed2ec Add control_back_mode 2025-11-03 08:30:19 +00:00
epenet
87cbb92356 Add time_total to lfkr93x0ukp5gaia 2025-11-03 08:30:19 +00:00
epenet
c728910cd7 Adjust snapshot 2025-11-03 08:29:41 +00:00
epenet
a84b10f905 Update quirks loader 2025-11-03 08:29:41 +00:00
epenet
9f09c137cd Move quirks to sub-folder 2025-11-03 08:29:40 +00:00
epenet
04c9d55e03 Add quirk for g1cp07dsqnbdbbki 2025-11-03 08:29:40 +00:00
epenet
378a3151d4 cleanup 2025-11-03 08:29:40 +00:00
epenet
b169bf0bf0 Move quirks to sub-folder 2025-11-03 08:29:40 +00:00
epenet
9369cf82fb Update snapshot 2025-11-03 08:29:39 +00:00
epenet
51d7b8e898 Comments 2025-11-03 08:29:39 +00:00
epenet
4abb397fd3 Add quirk 2025-11-03 08:29:39 +00:00
epenet
546f089eae Add specific test 2025-11-03 08:29:39 +00:00
14 changed files with 379 additions and 55 deletions

View File

@@ -5,6 +5,7 @@ from __future__ import annotations
import logging
from typing import Any, NamedTuple
from tuya_device_handlers.devices import register_tuya_quirks
from tuya_sharing import (
CustomerDevice,
Manager,
@@ -103,6 +104,10 @@ async def async_setup_entry(hass: HomeAssistant, entry: TuyaConfigEntry) -> bool
model_id=device.product_id,
)
# Should be loaded from configuration.yaml
# but for now, we can use a hardcoded path for testing
quirks_path = "/config/tuya_quirks/"
await hass.async_add_executor_job(register_tuya_quirks, quirks_path)
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
# If the device does not register any entities, the device does not need to subscribe
# So the subscription is here

View File

@@ -5,6 +5,11 @@ from __future__ import annotations
from dataclasses import dataclass
from typing import TYPE_CHECKING, Any
from tuya_device_handlers import TUYA_QUIRKS_REGISTRY
from tuya_device_handlers.builder import (
TuyaClimateDefinition,
TuyaIntegerConversionFunction,
)
from tuya_sharing import CustomerDevice, Manager
from homeassistant.components.climate import (
@@ -46,6 +51,9 @@ class TuyaClimateEntityDescription(ClimateEntityDescription):
"""Describe an Tuya climate entity."""
switch_only_hvac_mode: HVACMode
current_temperature_state_conversion: TuyaIntegerConversionFunction | None = None
target_temperature_state_conversion: TuyaIntegerConversionFunction | None = None
target_temperature_command_conversion: TuyaIntegerConversionFunction | None = None
CLIMATE_DESCRIPTIONS: dict[DeviceCategory, TuyaClimateEntityDescription] = {
@@ -76,6 +84,18 @@ CLIMATE_DESCRIPTIONS: dict[DeviceCategory, TuyaClimateEntityDescription] = {
}
def _create_quirk_description(
definition: TuyaClimateDefinition,
) -> TuyaClimateEntityDescription:
return TuyaClimateEntityDescription(
key=definition.key,
switch_only_hvac_mode=HVACMode(definition.switch_only_hvac_mode),
current_temperature_state_conversion=definition.current_temperature_state_conversion,
target_temperature_state_conversion=definition.target_temperature_state_conversion,
target_temperature_command_conversion=definition.target_temperature_command_conversion,
)
async def async_setup_entry(
hass: HomeAssistant,
entry: TuyaConfigEntry,
@@ -90,7 +110,17 @@ async def async_setup_entry(
entities: list[TuyaClimateEntity] = []
for device_id in device_ids:
device = manager.device_map[device_id]
if device and device.category in CLIMATE_DESCRIPTIONS:
if quirk := TUYA_QUIRKS_REGISTRY.get_quirk_for_device(device):
entities.extend(
TuyaClimateEntity(
device,
manager,
_create_quirk_description(definition),
hass.config.units.temperature_unit,
)
for definition in quirk.climate_definitions
)
elif device.category in CLIMATE_DESCRIPTIONS:
entities.append(
TuyaClimateEntity(
device,
@@ -194,8 +224,16 @@ class TuyaClimateEntity(TuyaEntity, ClimateEntity):
# it to define min, max & step temperatures
if self._set_temperature:
self._attr_supported_features |= ClimateEntityFeature.TARGET_TEMPERATURE
self._attr_max_temp = self._set_temperature.max_scaled
self._attr_min_temp = self._set_temperature.min_scaled
if convert := self.entity_description.target_temperature_state_conversion:
self._attr_max_temp = convert(
self.device, self._set_temperature, self._set_temperature.max
)
self._attr_min_temp = convert(
self.device, self._set_temperature, self._set_temperature.min
)
else:
self._attr_max_temp = self._set_temperature.max_scaled
self._attr_min_temp = self._set_temperature.min_scaled
self._attr_target_temperature_step = self._set_temperature.step_scaled
# Determine HVAC modes
@@ -347,13 +385,20 @@ class TuyaClimateEntity(TuyaEntity, ClimateEntity):
# guarded by ClimateEntityFeature.TARGET_TEMPERATURE
assert self._set_temperature is not None
if convert := self.entity_description.target_temperature_command_conversion:
value = convert(
self.device, self._set_temperature, kwargs[ATTR_TEMPERATURE]
)
else:
value = round(
self._set_temperature.scale_value_back(kwargs[ATTR_TEMPERATURE])
)
self._send_command(
[
{
"code": self._set_temperature.dpcode,
"value": round(
self._set_temperature.scale_value_back(kwargs[ATTR_TEMPERATURE])
),
"value": value,
}
]
)
@@ -368,6 +413,9 @@ class TuyaClimateEntity(TuyaEntity, ClimateEntity):
if temperature is None:
return None
if convert := self.entity_description.current_temperature_state_conversion:
return convert(self.device, self._current_temperature, temperature)
if self._current_temperature.scale == 0 and self._current_temperature.step != 1:
# The current temperature can have a scale of 0 or 1 and is used for
# rounding, Home Assistant doesn't need to round but we will always
@@ -399,6 +447,9 @@ class TuyaClimateEntity(TuyaEntity, ClimateEntity):
if temperature is None:
return None
if convert := self.entity_description.target_temperature_state_conversion:
return convert(self.device, self._set_temperature, temperature)
return self._set_temperature.scale_value(temperature)
@property

View File

@@ -5,6 +5,9 @@ from __future__ import annotations
from dataclasses import dataclass
from typing import TYPE_CHECKING, Any
from tuya_device_handlers import TUYA_QUIRKS_REGISTRY
from tuya_device_handlers.builder import TuyaCoverDefinition
from tuya_device_handlers.helpers import parse_enum
from tuya_sharing import CustomerDevice, Manager
from homeassistant.components.cover import (
@@ -143,6 +146,19 @@ COVERS: dict[DeviceCategory, tuple[TuyaCoverEntityDescription, ...]] = {
}
def _create_quirk_description(
definition: TuyaCoverDefinition,
) -> TuyaCoverEntityDescription:
return TuyaCoverEntityDescription(
key=DPCode(definition.key),
translation_key=definition.translation_key,
current_state=parse_enum(DPCode, definition.current_state_dp_code),
current_position=parse_enum(DPCode, definition.current_position_dp_code),
set_position=parse_enum(DPCode, definition.set_position_dp_code),
device_class=parse_enum(CoverDeviceClass, definition.device_class),
)
async def async_setup_entry(
hass: HomeAssistant,
entry: TuyaConfigEntry,
@@ -157,7 +173,18 @@ async def async_setup_entry(
entities: list[TuyaCoverEntity] = []
for device_id in device_ids:
device = manager.device_map[device_id]
if descriptions := COVERS.get(device.category):
if quirk := TUYA_QUIRKS_REGISTRY.get_quirk_for_device(device):
entities.extend(
TuyaCoverEntity(
device, manager, _create_quirk_description(definition)
)
for definition in quirk.cover_definitions
if (
definition.key in device.function
or definition.key in device.status_range
)
)
elif descriptions := COVERS.get(device.category):
entities.extend(
TuyaCoverEntity(device, manager, description)
for description in descriptions

View File

@@ -43,5 +43,8 @@
"integration_type": "hub",
"iot_class": "cloud_push",
"loggers": ["tuya_sharing"],
"requirements": ["tuya-device-sharing-sdk==0.2.4"]
"requirements": [
"tuya-device-handlers==0.0.1",
"tuya-device-sharing-sdk==0.2.4"
]
}

View File

@@ -8,45 +8,16 @@ import json
import struct
from typing import Self
from tuya_device_handlers.helpers import TuyaIntegerTypeDefinition
from .const import DPCode
from .util import remap_value
@dataclass
class IntegerTypeData:
class IntegerTypeData(TuyaIntegerTypeDefinition):
"""Integer Type Data."""
dpcode: DPCode
min: int
max: int
scale: float
step: float
unit: str | None = None
type: str | None = None
@property
def max_scaled(self) -> float:
"""Return the max scaled."""
return self.scale_value(self.max)
@property
def min_scaled(self) -> float:
"""Return the min scaled."""
return self.scale_value(self.min)
@property
def step_scaled(self) -> float:
"""Return the step scaled."""
return self.step / (10**self.scale)
def scale_value(self, value: float) -> float:
"""Scale a value."""
return value / (10**self.scale)
def scale_value_back(self, value: float) -> int:
"""Return raw value for scaled."""
return int(value * (10**self.scale))
def remap_value_to(
self,
value: float,

View File

@@ -2,6 +2,9 @@
from __future__ import annotations
from tuya_device_handlers import TUYA_QUIRKS_REGISTRY
from tuya_device_handlers.builder import TuyaSelectDefinition
from tuya_device_handlers.helpers import parse_enum
from tuya_sharing import CustomerDevice, Manager
from homeassistant.components.select import SelectEntity, SelectEntityDescription
@@ -343,6 +346,16 @@ SELECTS[DeviceCategory.DGHSXJ] = SELECTS[DeviceCategory.SP]
SELECTS[DeviceCategory.PC] = SELECTS[DeviceCategory.KG]
def _create_quirk_description(
definition: TuyaSelectDefinition,
) -> SelectEntityDescription:
return SelectEntityDescription(
key=DPCode(definition.key),
translation_key=definition.translation_key,
entity_category=parse_enum(EntityCategory, definition.entity_category),
)
async def async_setup_entry(
hass: HomeAssistant,
entry: TuyaConfigEntry,
@@ -357,7 +370,15 @@ async def async_setup_entry(
entities: list[TuyaSelectEntity] = []
for device_id in device_ids:
device = manager.device_map[device_id]
if descriptions := SELECTS.get(device.category):
if quirk := TUYA_QUIRKS_REGISTRY.get_quirk_for_device(device):
entities.extend(
TuyaSelectEntity(
device, manager, _create_quirk_description(definition)
)
for definition in quirk.select_definitions
if definition.key in device.status
)
elif descriptions := SELECTS.get(device.category):
entities.extend(
TuyaSelectEntity(device, manager, description)
for description in descriptions

View File

@@ -6,6 +6,9 @@ from collections.abc import Callable
from dataclasses import dataclass
from typing import Any
from tuya_device_handlers import TUYA_QUIRKS_REGISTRY
from tuya_device_handlers.builder import TuyaSensorDefinition
from tuya_device_handlers.helpers import parse_enum
from tuya_sharing import CustomerDevice, Manager
from tuya_sharing.device import DeviceStatusRange
@@ -1635,6 +1638,17 @@ SENSORS[DeviceCategory.DGHSXJ] = SENSORS[DeviceCategory.SP]
SENSORS[DeviceCategory.PC] = SENSORS[DeviceCategory.KG]
def _create_quirk_description(
definition: TuyaSensorDefinition,
) -> TuyaSensorEntityDescription:
return TuyaSensorEntityDescription(
key=DPCode(definition.key),
translation_key=definition.translation_key,
device_class=parse_enum(SensorDeviceClass, definition.device_class),
entity_category=parse_enum(EntityCategory, definition.entity_category),
)
async def async_setup_entry(
hass: HomeAssistant,
entry: TuyaConfigEntry,
@@ -1649,7 +1663,15 @@ async def async_setup_entry(
entities: list[TuyaSensorEntity] = []
for device_id in device_ids:
device = manager.device_map[device_id]
if descriptions := SENSORS.get(device.category):
if quirk := TUYA_QUIRKS_REGISTRY.get_quirk_for_device(device):
entities.extend(
TuyaSensorEntity(
device, manager, _create_quirk_description(definition)
)
for definition in quirk.sensor_definitions
if definition.key in device.status
)
elif descriptions := SENSORS.get(device.category):
entities.extend(
TuyaSensorEntity(device, manager, description)
for description in descriptions

View File

@@ -5,6 +5,9 @@ from __future__ import annotations
from dataclasses import dataclass
from typing import Any
from tuya_device_handlers import TUYA_QUIRKS_REGISTRY
from tuya_device_handlers.builder import TuyaSwitchDefinition
from tuya_device_handlers.helpers import parse_enum
from tuya_sharing import CustomerDevice, Manager
from homeassistant.components.switch import (
@@ -921,6 +924,17 @@ SWITCHES[DeviceCategory.CZ] = SWITCHES[DeviceCategory.PC]
SWITCHES[DeviceCategory.DGHSXJ] = SWITCHES[DeviceCategory.SP]
def _create_quirk_description(
definition: TuyaSwitchDefinition,
) -> SwitchEntityDescription:
return SwitchEntityDescription(
key=DPCode(definition.key),
translation_key=definition.translation_key,
device_class=parse_enum(SwitchDeviceClass, definition.device_class),
entity_category=parse_enum(EntityCategory, definition.entity_category),
)
async def async_setup_entry(
hass: HomeAssistant,
entry: TuyaConfigEntry,
@@ -936,7 +950,15 @@ async def async_setup_entry(
entities: list[TuyaSwitchEntity] = []
for device_id in device_ids:
device = manager.device_map[device_id]
if descriptions := SWITCHES.get(device.category):
if quirk := TUYA_QUIRKS_REGISTRY.get_quirk_for_device(device):
entities.extend(
TuyaSwitchEntity(
device, manager, _create_quirk_description(definition)
)
for definition in quirk.switch_definitions
if definition.key in device.status
)
elif descriptions := SWITCHES.get(device.category):
entities.extend(
TuyaSwitchEntity(device, manager, description)
for description in descriptions

3
requirements_all.txt generated
View File

@@ -3010,6 +3010,9 @@ ttls==1.8.3
# homeassistant.components.thethingsnetwork
ttn_client==1.2.3
# homeassistant.components.tuya
tuya-device-handlers==0.0.1
# homeassistant.components.tuya
tuya-device-sharing-sdk==0.2.4

View File

@@ -2483,6 +2483,9 @@ ttls==1.8.3
# homeassistant.components.thethingsnetwork
ttn_client==1.2.3
# homeassistant.components.tuya
tuya-device-handlers==0.0.1
# homeassistant.components.tuya
tuya-device-sharing-sdk==0.2.4

View File

@@ -383,8 +383,8 @@
<HVACMode.OFF: 'off'>,
<HVACMode.HEAT_COOL: 'heat_cool'>,
]),
'max_temp': 7.0,
'min_temp': 1.0,
'max_temp': 35.0,
'min_temp': 5.0,
'target_temp_step': 0.5,
}),
'config_entry_id': <ANY>,
@@ -419,17 +419,17 @@
# name: test_platform_setup_and_discovery[climate.el_termostato_de_la_cocina-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'current_temperature': 4.5,
'current_temperature': 22.5,
'friendly_name': 'El termostato de la cocina',
'hvac_modes': list([
<HVACMode.OFF: 'off'>,
<HVACMode.HEAT_COOL: 'heat_cool'>,
]),
'max_temp': 7.0,
'min_temp': 1.0,
'max_temp': 35.0,
'min_temp': 5.0,
'supported_features': <ClimateEntityFeature: 385>,
'target_temp_step': 0.5,
'temperature': 4.6,
'temperature': 23.0,
}),
'context': <ANY>,
'entity_id': 'climate.el_termostato_de_la_cocina',

View File

@@ -493,7 +493,7 @@
# name: test_platform_setup_and_discovery[cover.persiana_do_quarto_curtain-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'current_position': 100,
'current_position': 0,
'device_class': 'curtain',
'friendly_name': 'Persiana do Quarto Curtain',
'supported_features': <CoverEntityFeature: 15>,
@@ -503,7 +503,7 @@
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'open',
'state': 'closed',
})
# ---
# name: test_platform_setup_and_discovery[cover.projector_screen_curtain-entry]
@@ -535,7 +535,7 @@
'platform': 'tuya',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': <CoverEntityFeature: 15>,
'supported_features': <CoverEntityFeature: 11>,
'translation_key': 'curtain',
'unique_id': 'tuya.aiag5pku0x39rkfllccontrol',
'unit_of_measurement': None,
@@ -544,17 +544,16 @@
# name: test_platform_setup_and_discovery[cover.projector_screen_curtain-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'current_position': 100,
'device_class': 'curtain',
'friendly_name': 'Projector Screen Curtain',
'supported_features': <CoverEntityFeature: 15>,
'supported_features': <CoverEntityFeature: 11>,
}),
'context': <ANY>,
'entity_id': 'cover.projector_screen_curtain',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'open',
'state': 'closed',
})
# ---
# name: test_platform_setup_and_discovery[cover.roller_shutter_living_room_curtain-entry]

View File

@@ -2,6 +2,7 @@
from __future__ import annotations
from typing import Any
from unittest.mock import patch
import pytest
@@ -352,3 +353,108 @@ async def test_cl_n3xgr5pdmpinictg_state(
state = hass.states.get(entity_id)
assert state is not None, f"{entity_id} does not exist"
assert state.state == expected_state
@pytest.mark.parametrize(
"mock_device_code",
["cl_lfkr93x0ukp5gaia"],
)
@pytest.mark.parametrize(
("tuya_status", "expected_state"),
[
(
{
"control": "open",
"percent_control": 100,
"percent_state": 0,
"control_back_mode": "forward",
"work_state": "opening",
"countdown_left": 0,
"time_total": 0,
"situation_set": "fully_open",
"fault": 0,
"border": "down",
},
"open",
),
(
{
"control": "close",
"percent_control": 100,
"percent_state": 0,
"control_back_mode": "forward",
"work_state": "opening",
"countdown_left": 0,
"time_total": 0,
"situation_set": "fully_open",
"fault": 0,
"border": "down",
},
"closed",
),
],
)
@patch("homeassistant.components.tuya.PLATFORMS", [Platform.COVER])
async def test_cl_lfkr93x0ukp5gaia_state(
hass: HomeAssistant,
mock_manager: Manager,
mock_config_entry: MockConfigEntry,
mock_device: CustomerDevice,
tuya_status: dict[str, Any],
expected_state: str,
) -> None:
"""Test cover position for lfkr93x0ukp5gaia device.
See https://github.com/home-assistant/core/issues/152826
percent_control / percent_state / situation_set never change, regardless
of open or closed state
"""
entity_id = "cover.projector_screen_curtain"
mock_device.status.update(**tuya_status)
await initialize_entry(hass, mock_manager, mock_config_entry, mock_device)
state = hass.states.get(entity_id)
assert state is not None, f"{entity_id} does not exist"
assert state.state == expected_state
assert ATTR_CURRENT_POSITION not in state.attributes
@pytest.mark.parametrize(
"mock_device_code",
["cl_g1cp07dsqnbdbbki"],
)
@pytest.mark.parametrize(
("initial_percent_control", "expected_state", "expected_position"),
[
(0, "open", 100),
(25, "open", 75),
(50, "open", 50),
(75, "open", 25),
(100, "closed", 0),
],
)
@patch("homeassistant.components.tuya.PLATFORMS", [Platform.COVER])
async def test_cl_g1cp07dsqnbdbbki_state(
hass: HomeAssistant,
mock_manager: Manager,
mock_config_entry: MockConfigEntry,
mock_device: CustomerDevice,
initial_percent_control: int,
expected_state: str,
expected_position: int,
) -> None:
"""Test cover position for g1cp07dsqnbdbbki device.
See https://github.com/home-assistant/core/issues/139966
percent_state never changes, regardless of actual position
"""
entity_id = "cover.persiana_do_quarto_curtain"
mock_device.status["percent_control"] = initial_percent_control
await initialize_entry(hass, mock_manager, mock_config_entry, mock_device)
state = hass.states.get(entity_id)
assert state is not None, f"{entity_id} does not exist"
assert state.state == expected_state
assert state.attributes[ATTR_CURRENT_POSITION] == expected_position

View File

@@ -0,0 +1,91 @@
"""Ensure quirks are correctly aligned with Home Assistant."""
from __future__ import annotations
from enum import StrEnum
import pytest
from tuya_device_handlers import TUYA_QUIRKS_REGISTRY
from tuya_device_handlers.builder import TuyaDeviceQuirk
from tuya_device_handlers.builder.base_quirk import BaseTuyaDefinition
from tuya_device_handlers.devices import register_tuya_quirks
from tuya_device_handlers.registry import QuirksRegistry
from homeassistant.components.cover import CoverDeviceClass
from homeassistant.components.sensor import SensorDeviceClass
from homeassistant.const import EntityCategory, Platform
from homeassistant.core import HomeAssistant
from homeassistant.helpers import translation
from homeassistant.util.json import load_json
@pytest.fixture(scope="module")
def filled_quirks_registry() -> QuirksRegistry:
"""Mock an old config entry that can be migrated."""
register_tuya_quirks()
return TUYA_QUIRKS_REGISTRY
_PLATFORM_DEVICE_CLASS: dict[Platform, type[StrEnum]] = {
Platform.COVER: CoverDeviceClass,
Platform.SENSOR: SensorDeviceClass,
}
def _validate_quirk_platform(
quirk: TuyaDeviceQuirk, strings: dict[str, str], platform: str
) -> None:
"""Validate entity translations exist in strings.json."""
definitions: list[BaseTuyaDefinition] = getattr(quirk, f"{platform}_definitions")
for definition in definitions:
# Validate entity translations exist in strings.json.
if definition.translation_key:
full_key = f"entity.{platform}.{definition.translation_key}.name"
assert definition.translation_string == strings.get(full_key), (
f"Incorrect or missing translation string for {full_key} in "
"homeassistant/components/tuya/strings.json"
)
# Validate entity state translations exist in strings.json.
if state_translations := definition.state_translations:
for state, state_translation in state_translations.items():
full_key = (
f"entity.{platform}.{definition.translation_key}.state.{state}"
)
assert state_translation == strings.get(full_key), (
f"Incorrect or missing translation string for {full_key} in "
"homeassistant/components/tuya/strings.json"
)
# Validate device class is valid for platform.
if definition.device_class is not None:
device_class_enum = _PLATFORM_DEVICE_CLASS[platform]
assert definition.device_class in device_class_enum.__members__.values(), (
f"Invalid quirk device class {definition.device_class} for "
f"{definition.key} {platform} in {quirk.quirk_file} {quirk.quirk_file_line}"
)
# Validate entity category is valid for platform.
if definition.entity_category is not None:
assert definition.entity_category in EntityCategory.__members__.values(), (
f"Invalid quirk entity category {definition.entity_category} for "
f"{definition.key} {platform} in {quirk.quirk_file} {quirk.quirk_file_line}"
)
async def test_quirks_validation(
hass: HomeAssistant, filled_quirks_registry: QuirksRegistry
) -> None:
"""Test that quirks are valid.
- ensures that all translation strings exist in strings.json
- ensures that all device classes are valid for the platform
- ensures that all entity categories are valid for the platform
"""
json_strings = await hass.async_add_executor_job(
load_json, "homeassistant/components/tuya/strings.json"
)
strings = translation.recursive_flatten("", json_strings)
for category_devices in filled_quirks_registry._quirks.values():
for quirk in category_devices.values():
_validate_quirk_platform(quirk, strings, Platform.COVER)
_validate_quirk_platform(quirk, strings, Platform.SELECT)
_validate_quirk_platform(quirk, strings, Platform.SENSOR)