From b169bf0bf0befe750eeb25e55d6ece9f3f5c4d86 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 1 Oct 2025 10:01:12 +0000 Subject: [PATCH] Move quirks to sub-folder --- homeassistant/components/tuya/cover.py | 27 ++++++- .../components/tuya/quirks/__init__.py | 32 ++++++++ .../components/tuya/quirks/device_quirk.py | 79 +++++++++++++++++++ .../components/tuya/quirks/homeassistant.py | 31 ++++++++ .../components/tuya/quirks/registry.py | 36 +++++++++ 5 files changed, 204 insertions(+), 1 deletion(-) create mode 100644 homeassistant/components/tuya/quirks/__init__.py create mode 100644 homeassistant/components/tuya/quirks/device_quirk.py create mode 100644 homeassistant/components/tuya/quirks/homeassistant.py create mode 100644 homeassistant/components/tuya/quirks/registry.py diff --git a/homeassistant/components/tuya/cover.py b/homeassistant/components/tuya/cover.py index 77ca38acd395..89eebee869fe 100644 --- a/homeassistant/components/tuya/cover.py +++ b/homeassistant/components/tuya/cover.py @@ -23,6 +23,7 @@ from . import TuyaConfigEntry from .const import TUYA_DISCOVERY_NEW, DeviceCategory, DPCode, DPType from .entity import TuyaEntity from .models import EnumTypeData, IntegerTypeData +from .quirks import TUYA_QUIRKS_REGISTRY, TuyaCoverDefinition, parse_enum from .util import get_dpcode @@ -164,6 +165,19 @@ def _apply_quirk( return description +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, @@ -178,7 +192,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, _apply_quirk(device, description)) for description in descriptions diff --git a/homeassistant/components/tuya/quirks/__init__.py b/homeassistant/components/tuya/quirks/__init__.py new file mode 100644 index 000000000000..07c012c76caa --- /dev/null +++ b/homeassistant/components/tuya/quirks/__init__.py @@ -0,0 +1,32 @@ +"""Quirks for Tuya.""" + +from __future__ import annotations + +from .device_quirk import DeviceQuirk, TuyaCoverDefinition, TuyaCoverDeviceClass +from .homeassistant import parse_enum +from .registry import QuirksRegistry + +__all__ = [ + "QUIRKS_REGISTRY", + "QuirksRegistry", + "TuyaCoverDefinition", + "TuyaCoverDeviceClass", + "parse_enum", +] + +TUYA_QUIRKS_REGISTRY = QuirksRegistry() + +( + # This model has percent_control / percent_state / situation_set + # but they never get updated - use control instead to get the state + DeviceQuirk() + .applies_to(category="cl", product_id="lfkr93x0ukp5gaia") + .add_cover( + key="control", + translation_key="curtain", + translation_string="curtain", + current_state_dp_code="control", + device_class=TuyaCoverDeviceClass.CURTAIN, + ) + .register(TUYA_QUIRKS_REGISTRY) +) diff --git a/homeassistant/components/tuya/quirks/device_quirk.py b/homeassistant/components/tuya/quirks/device_quirk.py new file mode 100644 index 000000000000..5f664b1116af --- /dev/null +++ b/homeassistant/components/tuya/quirks/device_quirk.py @@ -0,0 +1,79 @@ +"""Quirks registry.""" + +from __future__ import annotations + +from dataclasses import dataclass +from typing import TYPE_CHECKING, Self + +from .homeassistant import TuyaCoverDeviceClass + +if TYPE_CHECKING: + from .registry import QuirksRegistry + + +@dataclass +class BaseTuyaDefinition: + """Definition for a Tuya device.""" + + key: str + translation_key: str + translation_string: str + + +@dataclass(kw_only=True) +class TuyaCoverDefinition(BaseTuyaDefinition): + """Definition for a cover device.""" + + device_class: TuyaCoverDeviceClass | None = None + + current_state_dp_code: str | None = None + current_position_dp_code: str | None = None + set_position_dp_code: str | None = None + + +@dataclass +class DeviceQuirk: + """Quirk for Tuya device.""" + + _applies_to: list[tuple[str, str]] + cover_definitions: list[TuyaCoverDefinition] + + def __init__(self) -> None: + """Initialize the quirk.""" + self._applies_to = [] + self.cover_definitions = [] + + def applies_to(self, *, category: str, product_id: str) -> Self: + """Set the device type the quirk applies to.""" + self._applies_to.append((category, product_id)) + return self + + def register(self, registry: QuirksRegistry) -> None: + """Register the quirk in the registry.""" + for category, product_id in self._applies_to: + registry.register(category, product_id, self) + + def add_cover( + self, + *, + key: str, + translation_key: str, + translation_string: str, + device_class: TuyaCoverDeviceClass | None = None, + current_state_dp_code: str | None = None, + current_position_dp_code: str | None = None, + set_position_dp_code: str | None = None, + ) -> Self: + """Add cover quirk.""" + self.cover_definitions.append( + TuyaCoverDefinition( + key=key, + translation_key=translation_key, + translation_string=translation_string, + device_class=device_class, + current_state_dp_code=current_state_dp_code, + current_position_dp_code=current_position_dp_code, + set_position_dp_code=set_position_dp_code, + ) + ) + return self diff --git a/homeassistant/components/tuya/quirks/homeassistant.py b/homeassistant/components/tuya/quirks/homeassistant.py new file mode 100644 index 000000000000..0d17c5cb94cd --- /dev/null +++ b/homeassistant/components/tuya/quirks/homeassistant.py @@ -0,0 +1,31 @@ +"""Quirks registry.""" + +from __future__ import annotations + +from enum import StrEnum + + +def parse_enum[T: StrEnum](enum_class: type[T], value: str | None) -> T | None: + """Parse a string to an enum member, or return None if value is None.""" + if value is None: + return None + try: + return enum_class(value) + except ValueError: + return None + + +class TuyaCoverDeviceClass(StrEnum): + """Device class for cover.""" + + # Keep synchronized with homeassistant + AWNING = "awning" + BLIND = "blind" + CURTAIN = "curtain" + DAMPER = "damper" + DOOR = "door" + GARAGE = "garage" + GATE = "gate" + SHADE = "shade" + SHUTTER = "shutter" + WINDOW = "window" diff --git a/homeassistant/components/tuya/quirks/registry.py b/homeassistant/components/tuya/quirks/registry.py new file mode 100644 index 000000000000..d767f13ca1ac --- /dev/null +++ b/homeassistant/components/tuya/quirks/registry.py @@ -0,0 +1,36 @@ +"""Quirks registry.""" + +from __future__ import annotations + +from typing import TYPE_CHECKING, Self + +from tuya_sharing import CustomerDevice + +if TYPE_CHECKING: + from .device_quirk import DeviceQuirk + + +class QuirksRegistry: + """Registry for Tuya quirks.""" + + instance: Self + + _quirks: dict[str, dict[str, DeviceQuirk]] + + def __new__(cls) -> Self: + """Create a new class.""" + if not hasattr(cls, "instance"): + cls.instance = super().__new__(cls) + return cls.instance + + def __init__(self) -> None: + """Initialize the registry.""" + self._quirks = {} + + def register(self, category: str, product_id: str, quirk: DeviceQuirk) -> None: + """Register a quirk for a specific device type.""" + self._quirks.setdefault(category, {})[product_id] = quirk + + def get_quirk_for_device(self, device: CustomerDevice) -> DeviceQuirk | None: + """Get the quirk for a specific device.""" + return self._quirks.get(device.category, {}).get(device.product_id)