This commit is contained in:
epenet
2025-11-04 14:09:16 +00:00
parent 3246692998
commit 0d78e7459b
26 changed files with 35 additions and 1137 deletions
+1 -1
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,
@@ -31,7 +32,6 @@ from .const import (
TUYA_DISCOVERY_NEW,
TUYA_HA_SIGNAL_UPDATE_ENTITY,
)
from .tuya_device_handlers.devices import register_tuya_quirks
# Suppress logs from the library, it logs unneeded on error
logging.getLogger("tuya_sharing").setLevel(logging.CRITICAL)
+5 -5
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 (
@@ -27,11 +32,6 @@ from . import TuyaConfigEntry
from .const import TUYA_DISCOVERY_NEW, DeviceCategory, DPCode, DPType
from .entity import TuyaEntity
from .models import IntegerTypeData
from .tuya_device_handlers import TUYA_QUIRKS_REGISTRY
from .tuya_device_handlers.builder import (
TuyaClimateDefinition,
TuyaIntegerConversionFunction,
)
from .util import get_dpcode
TUYA_HVAC_TO_HA = {
+3 -3
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 (
@@ -23,9 +26,6 @@ from . import TuyaConfigEntry
from .const import TUYA_DISCOVERY_NEW, DeviceCategory, DPCode, DPType
from .entity import TuyaEntity
from .models import EnumTypeData, IntegerTypeData
from .tuya_device_handlers import TUYA_QUIRKS_REGISTRY
from .tuya_device_handlers.builder import TuyaCoverDefinition
from .tuya_device_handlers.helpers import parse_enum
from .util import get_dpcode
+4 -1
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"
]
}
+2 -1
View File
@@ -8,8 +8,9 @@ import json
import struct
from typing import Self
from tuya_device_handlers.helpers import TuyaIntegerTypeDefinition
from .const import DPCode
from .tuya_device_handlers.helpers import TuyaIntegerTypeDefinition
from .util import remap_value
+3 -3
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
@@ -13,9 +16,6 @@ from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from . import TuyaConfigEntry
from .const import TUYA_DISCOVERY_NEW, DeviceCategory, DPCode, DPType
from .entity import TuyaEntity
from .tuya_device_handlers import TUYA_QUIRKS_REGISTRY
from .tuya_device_handlers.builder import TuyaSelectDefinition
from .tuya_device_handlers.helpers import parse_enum
# All descriptions can be found here. Mostly the Enum data types in the
# default instructions set of each category end up being a select.
+3 -3
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
@@ -44,9 +47,6 @@ from .const import (
)
from .entity import TuyaEntity
from .models import ComplexValue, ElectricityValue, EnumTypeData, IntegerTypeData
from .tuya_device_handlers import TUYA_QUIRKS_REGISTRY
from .tuya_device_handlers.builder import TuyaSensorDefinition
from .tuya_device_handlers.helpers import parse_enum
from .util import get_dptype
_WIND_DIRECTIONS = {
+3 -3
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 (
@@ -27,9 +30,6 @@ from homeassistant.helpers.issue_registry import (
from . import TuyaConfigEntry
from .const import DOMAIN, TUYA_DISCOVERY_NEW, DeviceCategory, DPCode
from .entity import TuyaEntity
from .tuya_device_handlers import TUYA_QUIRKS_REGISTRY
from .tuya_device_handlers.builder import TuyaSwitchDefinition
from .tuya_device_handlers.helpers import parse_enum
@dataclass(frozen=True, kw_only=True)
@@ -1,11 +0,0 @@
"""Tuya device handler."""
from __future__ import annotations
from .registry import QuirksRegistry
__all__ = [
"TUYA_QUIRKS_REGISTRY",
]
TUYA_QUIRKS_REGISTRY = QuirksRegistry()
@@ -1,23 +0,0 @@
"""Tuya device handler."""
from __future__ import annotations
from .base_quirk import (
TuyaClimateDefinition,
TuyaCoverDefinition,
TuyaDeviceQuirk,
TuyaIntegerConversionFunction,
TuyaSelectDefinition,
TuyaSensorDefinition,
TuyaSwitchDefinition,
)
__all__ = [
"TuyaClimateDefinition",
"TuyaCoverDefinition",
"TuyaDeviceQuirk",
"TuyaIntegerConversionFunction",
"TuyaSelectDefinition",
"TuyaSensorDefinition",
"TuyaSwitchDefinition",
]
@@ -1,240 +0,0 @@
"""Base quirk definition."""
from __future__ import annotations
from collections.abc import Callable
from dataclasses import dataclass
import inspect
import pathlib
from typing import TYPE_CHECKING, Any, Self
from tuya_device_handlers.helpers import (
TuyaClimateHVACMode,
TuyaCoverDeviceClass,
TuyaDeviceCategory,
TuyaDPCode,
TuyaEntityCategory,
TuyaSensorDeviceClass,
)
if TYPE_CHECKING:
from tuya_sharing import CustomerDevice
from ..helpers import TuyaIntegerTypeDefinition
from ..registry import QuirksRegistry
type TuyaIntegerConversionFunction = Callable[
[CustomerDevice, TuyaIntegerTypeDefinition, Any], Any
]
"""Start conversion function:
Args:
device: The Tuya device instance (CustomerDevice).
dptype: The DP type definition (TuyaIntegerTypeDefinition).
value: The value to convert.
"""
@dataclass
class BaseTuyaDefinition:
"""Definition for a Tuya entity."""
key: str
translation_key: str | None = None
translation_string: str | None = None
state_translations: dict[str, str] | None = None
device_class: str | None = None
entity_category: str | None = None
@dataclass(kw_only=True)
class TuyaClimateDefinition(BaseTuyaDefinition):
"""Definition for a climate entity."""
switch_only_hvac_mode: TuyaClimateHVACMode
current_temperature_dp_code: TuyaDPCode | None = None
current_temperature_state_conversion: TuyaIntegerConversionFunction | None = None
target_temperature_dp_code: TuyaDPCode | None = None
target_temperature_state_conversion: TuyaIntegerConversionFunction | None = None
target_temperature_command_conversion: TuyaIntegerConversionFunction | None = None
@dataclass(kw_only=True)
class TuyaCoverDefinition(BaseTuyaDefinition):
"""Definition for a cover entity."""
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(kw_only=True)
class TuyaSelectDefinition(BaseTuyaDefinition):
"""Definition for a select entity."""
@dataclass(kw_only=True)
class TuyaSensorDefinition(BaseTuyaDefinition):
"""Definition for a sensor entity."""
device_class: TuyaSensorDeviceClass | None = None
@dataclass(kw_only=True)
class TuyaSwitchDefinition(BaseTuyaDefinition):
"""Definition for a switch entity."""
class TuyaDeviceQuirk:
"""Quirk for Tuya device."""
def __init__(self) -> None:
"""Initialize the quirk."""
self._applies_to: list[tuple[TuyaDeviceCategory, str]] = []
self.climate_definitions: list[TuyaClimateDefinition] = []
self.cover_definitions: list[TuyaCoverDefinition] = []
self.select_definitions: list[TuyaSelectDefinition] = []
self.sensor_definitions: list[TuyaSensorDefinition] = []
self.switch_definitions: list[TuyaSwitchDefinition] = []
current_frame = inspect.currentframe()
if TYPE_CHECKING:
assert current_frame is not None
caller = current_frame.f_back
if TYPE_CHECKING:
assert caller is not None
self.quirk_file = pathlib.Path(caller.f_code.co_filename)
self.quirk_file_line = caller.f_lineno
def applies_to(self, *, category: TuyaDeviceCategory, 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_climate(
self,
*,
key: str,
# Climate specific
switch_only_hvac_mode: TuyaClimateHVACMode,
current_temperature_dp_code: TuyaDPCode | None = None,
current_temperature_state_conversion: TuyaIntegerConversionFunction
| None = None,
target_temperature_dp_code: TuyaDPCode | None = None,
target_temperature_state_conversion: TuyaIntegerConversionFunction
| None = None,
target_temperature_command_conversion: TuyaIntegerConversionFunction
| None = None,
) -> Self:
"""Add climate definition."""
self.climate_definitions.append(
TuyaClimateDefinition(
key=key,
switch_only_hvac_mode=switch_only_hvac_mode,
current_temperature_dp_code=current_temperature_dp_code,
current_temperature_state_conversion=current_temperature_state_conversion,
target_temperature_dp_code=target_temperature_dp_code,
target_temperature_state_conversion=target_temperature_state_conversion,
target_temperature_command_conversion=target_temperature_command_conversion,
)
)
return self
def add_cover(
self,
*,
key: TuyaDPCode,
translation_key: str,
translation_string: str,
device_class: TuyaCoverDeviceClass | None = None,
# Cover specific
current_state_dp_code: TuyaDPCode | None = None,
current_position_dp_code: TuyaDPCode | None = None,
set_position_dp_code: TuyaDPCode | None = None,
) -> Self:
"""Add cover definition."""
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
def add_select(
self,
*,
key: TuyaDPCode,
translation_key: str,
translation_string: str,
entity_category: TuyaEntityCategory | None = None,
# Select specific
state_translations: dict[str, str] | None = None,
) -> Self:
"""Add select definition."""
self.select_definitions.append(
TuyaSelectDefinition(
key=key,
translation_key=translation_key,
translation_string=translation_string,
entity_category=entity_category,
state_translations=state_translations,
)
)
return self
def add_sensor(
self,
*,
key: TuyaDPCode,
translation_key: str,
translation_string: str,
device_class: TuyaSensorDeviceClass | None = None,
entity_category: TuyaEntityCategory | None = None,
# Sensor specific
) -> Self:
"""Add sensor definition."""
self.sensor_definitions.append(
TuyaSensorDefinition(
key=key,
translation_key=translation_key,
translation_string=translation_string,
device_class=device_class,
entity_category=entity_category,
)
)
return self
def add_switch(
self,
*,
key: TuyaDPCode,
translation_key: str,
translation_string: str,
entity_category: TuyaEntityCategory | None = None,
# Switch specific
) -> Self:
"""Add switch definition."""
self.switch_definitions.append(
TuyaSwitchDefinition(
key=key,
translation_key=translation_key,
translation_string=translation_string,
entity_category=entity_category,
)
)
return self
@@ -1,60 +0,0 @@
"""Quirks for Tuya."""
from __future__ import annotations
import importlib
import logging
import pathlib
import pkgutil
import sys
from .. import TUYA_QUIRKS_REGISTRY
_LOGGER = logging.getLogger(__name__)
def register_tuya_quirks(custom_quirks_path: str | None = None) -> None:
"""Register all available quirks.
- remove custom quirks from `custom_quirks_path`
- add quirks from `devices` subfolder
- add custom quirks from `custom_quirks_path`
"""
if custom_quirks_path is not None:
TUYA_QUIRKS_REGISTRY.purge_custom_quirks(custom_quirks_path)
# Import all quirks in the `tuya_device_handlers` package first
for _importer, modname, _ispkg in pkgutil.walk_packages(
path=__path__,
prefix=__name__ + ".",
):
_LOGGER.debug("Loading quirks module %r", modname)
importlib.import_module(modname)
if custom_quirks_path is None:
return
path = pathlib.Path(custom_quirks_path)
_LOGGER.debug("Loading custom quirks from %r", path)
loaded = False
# Treat the custom quirk path (e.g. `/config/tuya_quirks/`) itself as a module
for importer, modname, _ispkg in pkgutil.walk_packages(path=[str(path)]):
_LOGGER.debug("Loading custom quirk module %r", modname)
try:
spec = importer.find_spec(modname) # type: ignore[call-arg]
module = importlib.util.module_from_spec(spec) # type: ignore[arg-type]
sys.modules[modname] = module
spec.loader.exec_module(module) # type: ignore[union-attr]
except Exception:
_LOGGER.exception("Unexpected exception importing custom quirk %r", modname)
else:
loaded = True
if loaded:
_LOGGER.warning(
"Loaded custom quirks. Please contribute them to https://github.com/TBD"
)
@@ -1,4 +0,0 @@
"""Quirks for Tuya CL category (curtain).
https://developer.tuya.com/en/docs/iot/categorycl?id=Kaiuz1hnpo7df
"""
@@ -1,36 +0,0 @@
"""Quirks for Tuya."""
from __future__ import annotations
from ... import TUYA_QUIRKS_REGISTRY
from ...builder import TuyaDeviceQuirk
from ...helpers import (
TuyaCoverDeviceClass,
TuyaDeviceCategory,
TuyaDPCode,
TuyaEntityCategory,
)
(
# This model has percent_state and percent_control but percent_state never
# gets updated - force percent_control instead
TuyaDeviceQuirk()
.applies_to(category=TuyaDeviceCategory.CL, product_id="g1cp07dsqnbdbbki")
.add_cover(
key=TuyaDPCode.CONTROL,
translation_key="curtain",
translation_string="[%key:component::cover::entity_component::curtain::name%]",
current_state_dp_code=TuyaDPCode.CONTROL,
current_position_dp_code=TuyaDPCode.PERCENT_CONTROL,
set_position_dp_code=TuyaDPCode.PERCENT_CONTROL,
device_class=TuyaCoverDeviceClass.CURTAIN,
)
.add_select(
key=TuyaDPCode.CONTROL_BACK_MODE,
translation_key="curtain_motor_mode",
translation_string="Motor mode",
entity_category=TuyaEntityCategory.CONFIG,
state_translations={"forward": "Forward", "back": "Back"},
)
.register(TUYA_QUIRKS_REGISTRY)
)
@@ -1,40 +0,0 @@
"""Quirks for Tuya."""
from __future__ import annotations
from ... import TUYA_QUIRKS_REGISTRY
from ...builder import TuyaDeviceQuirk
from ...helpers import (
TuyaCoverDeviceClass,
TuyaDeviceCategory,
TuyaDPCode,
TuyaEntityCategory,
)
(
# This model has percent_control / percent_state / situation_set
# but they never get updated - use control instead to get the state
TuyaDeviceQuirk()
.applies_to(category=TuyaDeviceCategory.CL, product_id="lfkr93x0ukp5gaia")
.add_cover(
key=TuyaDPCode.CONTROL,
translation_key="curtain",
translation_string="[%key:component::cover::entity_component::curtain::name%]",
current_state_dp_code=TuyaDPCode.CONTROL,
device_class=TuyaCoverDeviceClass.CURTAIN,
)
.add_select(
key=TuyaDPCode.CONTROL_BACK_MODE,
translation_key="curtain_motor_mode",
translation_string="Motor mode",
entity_category=TuyaEntityCategory.CONFIG,
state_translations={"forward": "Forward", "back": "Back"},
)
.add_sensor(
key=TuyaDPCode.TIME_TOTAL,
translation_key="last_operation_duration",
translation_string="Last operation duration",
entity_category=TuyaEntityCategory.DIAGNOSTIC,
)
.register(TUYA_QUIRKS_REGISTRY)
)
@@ -1,4 +0,0 @@
"""Quirks for Tuya WK category (thermostat).
https://developer.tuya.com/en/docs/iot/f?id=K9gf45ld5l0t9
"""
@@ -1,35 +0,0 @@
"""Quirks for Tuya."""
from __future__ import annotations
from ... import TUYA_QUIRKS_REGISTRY
from ...builder import TuyaDeviceQuirk
from ...helpers import (
TuyaClimateHVACMode,
TuyaDeviceCategory,
TuyaDPCode,
TuyaEntityCategory,
)
(
TuyaDeviceQuirk()
.applies_to(category=TuyaDeviceCategory.WK, product_id="IAYz2WK1th0cMLmL")
.add_climate(
key="wk",
switch_only_hvac_mode=TuyaClimateHVACMode.HEAT_COOL,
current_temperature_dp_code=TuyaDPCode.UPPER_TEMP,
# UPPER_TEMP uses incorrect scale 1 / step 5 - convert to proper temperature
current_temperature_state_conversion=lambda _device, _def, value: value / 2,
# TEMP_SET uses incorrect scale 0 / step 5 - convert to proper temperature
target_temperature_dp_code=TuyaDPCode.TEMP_SET,
target_temperature_state_conversion=lambda _device, _def, value: value / 2,
target_temperature_command_conversion=lambda _device, _def, value: value * 2,
)
.add_switch(
key=TuyaDPCode.CHILD_LOCK,
translation_key="child_lock",
translation_string="Child lock",
entity_category=TuyaEntityCategory.CONFIG,
)
.register(TUYA_QUIRKS_REGISTRY)
)
@@ -1,24 +0,0 @@
"""Tuya models."""
from __future__ import annotations
from .const import TuyaDeviceCategory, TuyaDPCode
from .homeassistant import (
TuyaClimateHVACMode,
TuyaCoverDeviceClass,
TuyaEntityCategory,
TuyaSensorDeviceClass,
)
from .models import TuyaIntegerTypeDefinition
from .utils import parse_enum
__all__ = [
"TuyaClimateHVACMode",
"TuyaCoverDeviceClass",
"TuyaDPCode",
"TuyaDeviceCategory",
"TuyaEntityCategory",
"TuyaIntegerTypeDefinition",
"TuyaSensorDeviceClass",
"parse_enum",
]
@@ -1,32 +0,0 @@
"""Constants for Tuya device handlers."""
from __future__ import annotations
from enum import StrEnum
class TuyaDPCode(StrEnum):
"""Tuya data-point codes."""
CHILD_LOCK = "child_lock"
CONTROL = "control"
CONTROL_BACK_MODE = "control_back_mode"
PERCENT_CONTROL = "percent_control"
TEMP_SET = "temp_set"
TIME_TOTAL = "time_total"
UPPER_TEMP = "upper_temp"
class TuyaDeviceCategory(StrEnum):
"""Tuya device categories."""
CL = "cl"
"""Curtain
https://developer.tuya.com/en/docs/iot/categorycl?id=Kaiuz1hnpo7df
"""
WK = "wk"
"""Thermostat
https://developer.tuya.com/en/docs/iot/f?id=K9gf45ld5l0t9
"""
@@ -1,480 +0,0 @@
"""Copy of Home Assistant constants."""
from __future__ import annotations
from enum import StrEnum
class TuyaEntityCategory(StrEnum):
"""Category of an entity.
An entity with a category will:
- Not be exposed to cloud, Alexa, or Google Assistant components
- Not be included in indirect service calls to devices or areas
"""
# Config: An entity which allows changing the configuration of a device.
CONFIG = "config"
# Diagnostic: An entity exposing some configuration parameter,
# or diagnostics of a device.
DIAGNOSTIC = "diagnostic"
class TuyaClimateHVACMode(StrEnum):
"""HVAC mode for climate devices."""
# All activity disabled / Device is off/standby
OFF = "off"
# Heating
HEAT = "heat"
# Cooling
COOL = "cool"
# The device supports heating/cooling to a range
HEAT_COOL = "heat_cool"
# The temperature is set based on a schedule, learned behavior, AI or some
# other related mechanism. User is not able to adjust the temperature
AUTO = "auto"
# Device is in Dry/Humidity mode
DRY = "dry"
# Only the fan is on, not fan and another mode like cool
FAN_ONLY = "fan_only"
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"
class TuyaSensorDeviceClass(StrEnum):
"""Device class for sensors."""
# Non-numerical device classes
DATE = "date"
"""Date.
Unit of measurement: `None`
ISO8601 format: https://en.wikipedia.org/wiki/ISO_8601
"""
ENUM = "enum"
"""Enumeration.
Provides a fixed list of options the state of the sensor can be in.
Unit of measurement: `None`
"""
TIMESTAMP = "timestamp"
"""Timestamp.
Unit of measurement: `None`
ISO8601 format: https://en.wikipedia.org/wiki/ISO_8601
"""
# Numerical device classes, these should be aligned with NumberDeviceClass
ABSOLUTE_HUMIDITY = "absolute_humidity"
"""Absolute humidity.
Unit of measurement: `g/m³`, `mg/m³`
"""
APPARENT_POWER = "apparent_power"
"""Apparent power.
Unit of measurement: `mVA`, `VA`, `kVA`
"""
AQI = "aqi"
"""Air Quality Index.
Unit of measurement: `None`
"""
AREA = "area"
"""Area
Unit of measurement: `UnitOfArea` units
"""
ATMOSPHERIC_PRESSURE = "atmospheric_pressure"
"""Atmospheric pressure.
Unit of measurement: `UnitOfPressure` units
"""
BATTERY = "battery"
"""Percentage of battery that is left.
Unit of measurement: `%`
"""
BLOOD_GLUCOSE_CONCENTRATION = "blood_glucose_concentration"
"""Blood glucose concentration.
Unit of measurement: `mg/dL`, `mmol/L`
"""
CO = "carbon_monoxide"
"""Carbon Monoxide gas concentration.
Unit of measurement: `ppm` (parts per million)
"""
CO2 = "carbon_dioxide"
"""Carbon Dioxide gas concentration.
Unit of measurement: `ppm` (parts per million)
"""
CONDUCTIVITY = "conductivity"
"""Conductivity.
Unit of measurement: `S/cm`, `mS/cm`, `μS/cm`
"""
CURRENT = "current"
"""Current.
Unit of measurement: `A`, `mA`
"""
DATA_RATE = "data_rate"
"""Data rate.
Unit of measurement: UnitOfDataRate
"""
DATA_SIZE = "data_size"
"""Data size.
Unit of measurement: UnitOfInformation
"""
DISTANCE = "distance"
"""Generic distance.
Unit of measurement: `LENGTH_*` units
- SI /metric: `mm`, `cm`, `m`, `km`
- USCS / imperial: `in`, `ft`, `yd`, `mi`
"""
DURATION = "duration"
"""Fixed duration.
Unit of measurement: `d`, `h`, `min`, `s`, `ms`, `μs`
"""
ENERGY = "energy"
"""Energy.
Use this device class for sensors measuring energy consumption, for example
electric energy consumption.
Unit of measurement: `J`, `kJ`, `MJ`, `GJ`, `mWh`, `Wh`, `kWh`, `MWh`, `GWh`, `TWh`, `cal`, `kcal`, `Mcal`, `Gcal`
"""
ENERGY_DISTANCE = "energy_distance"
"""Energy distance.
Use this device class for sensors measuring energy by distance, for example the amount
of electric energy consumed by an electric car.
Unit of measurement: `kWh/100km`, `Wh/km`, `mi/kWh`, `km/kWh`
"""
ENERGY_STORAGE = "energy_storage"
"""Stored energy.
Use this device class for sensors measuring stored energy, for example the amount
of electric energy currently stored in a battery or the capacity of a battery.
Unit of measurement: `J`, `kJ`, `MJ`, `GJ`, `mWh`, `Wh`, `kWh`, `MWh`, `GWh`, `TWh`, `cal`, `kcal`, `Mcal`, `Gcal`
"""
FREQUENCY = "frequency"
"""Frequency.
Unit of measurement: `Hz`, `kHz`, `MHz`, `GHz`
"""
GAS = "gas"
"""Gas.
Unit of measurement:
- SI / metric: `L`, `m³`
- USCS / imperial: `ft³`, `CCF`, `MCF`
"""
HUMIDITY = "humidity"
"""Relative humidity.
Unit of measurement: `%`
"""
ILLUMINANCE = "illuminance"
"""Illuminance.
Unit of measurement: `lx`
"""
IRRADIANCE = "irradiance"
"""Irradiance.
Unit of measurement:
- SI / metric: `W/m²`
- USCS / imperial: `BTU/(h⋅ft²)`
"""
MOISTURE = "moisture"
"""Moisture.
Unit of measurement: `%`
"""
MONETARY = "monetary"
"""Amount of money.
Unit of measurement: ISO4217 currency code
See https://en.wikipedia.org/wiki/ISO_4217#Active_codes for active codes
"""
NITROGEN_DIOXIDE = "nitrogen_dioxide"
"""Amount of NO2.
Unit of measurement: `μg/m³`
"""
NITROGEN_MONOXIDE = "nitrogen_monoxide"
"""Amount of NO.
Unit of measurement: `μg/m³`
"""
NITROUS_OXIDE = "nitrous_oxide"
"""Amount of N2O.
Unit of measurement: `μg/m³`
"""
OZONE = "ozone"
"""Amount of O3.
Unit of measurement: `μg/m³`
"""
PH = "ph"
"""Potential hydrogen (acidity/alkalinity).
Unit of measurement: Unitless
"""
PM1 = "pm1"
"""Particulate matter <= 1 μm.
Unit of measurement: `μg/m³`
"""
PM10 = "pm10"
"""Particulate matter <= 10 μm.
Unit of measurement: `μg/m³`
"""
PM25 = "pm25"
"""Particulate matter <= 2.5 μm.
Unit of measurement: `μg/m³`
"""
PM4 = "pm4"
"""Particulate matter <= 4 μm.
Unit of measurement: `μg/m³`
"""
POWER_FACTOR = "power_factor"
"""Power factor.
Unit of measurement: `%`, `None`
"""
POWER = "power"
"""Power.
Unit of measurement: `mW`, `W`, `kW`, `MW`, `GW`, `TW`, `BTU/h`
"""
PRECIPITATION = "precipitation"
"""Accumulated precipitation.
Unit of measurement: UnitOfPrecipitationDepth
- SI / metric: `cm`, `mm`
- USCS / imperial: `in`
"""
PRECIPITATION_INTENSITY = "precipitation_intensity"
"""Precipitation intensity.
Unit of measurement: UnitOfVolumetricFlux
- SI /metric: `mm/d`, `mm/h`
- USCS / imperial: `in/d`, `in/h`
"""
PRESSURE = "pressure"
"""Pressure.
Unit of measurement:
- `mbar`, `cbar`, `bar`
- `Pa`, `hPa`, `kPa`
- `inHg`
- `psi`
- `inH₂O`
"""
REACTIVE_ENERGY = "reactive_energy"
"""Reactive energy.
Unit of measurement: `varh`, `kvarh`
"""
REACTIVE_POWER = "reactive_power"
"""Reactive power.
Unit of measurement: `mvar`, `var`, `kvar`
"""
SIGNAL_STRENGTH = "signal_strength"
"""Signal strength.
Unit of measurement: `dB`, `dBm`
"""
SOUND_PRESSURE = "sound_pressure"
"""Sound pressure.
Unit of measurement: `dB`, `dBA`
"""
SPEED = "speed"
"""Generic speed.
Unit of measurement: `SPEED_*` units or `UnitOfVolumetricFlux`
- SI /metric: `mm/d`, `mm/h`, `m/s`, `km/h`, `mm/s`
- USCS / imperial: `in/d`, `in/h`, `in/s`, `ft/s`, `mph`
- Nautical: `kn`
- Beaufort: `Beaufort`
"""
SULPHUR_DIOXIDE = "sulphur_dioxide"
"""Amount of SO2.
Unit of measurement: `μg/m³`
"""
TEMPERATURE = "temperature"
"""Temperature.
Unit of measurement: `°C`, `°F`, `K`
"""
VOLATILE_ORGANIC_COMPOUNDS = "volatile_organic_compounds"
"""Amount of VOC.
Unit of measurement: `μg/m³`, `mg/m³`
"""
VOLATILE_ORGANIC_COMPOUNDS_PARTS = "volatile_organic_compounds_parts"
"""Ratio of VOC.
Unit of measurement: `ppm`, `ppb`
"""
VOLTAGE = "voltage"
"""Voltage.
Unit of measurement: `V`, `mV`, `μV`, `kV`, `MV`
"""
VOLUME = "volume"
"""Generic volume.
Unit of measurement: `VOLUME_*` units
- SI / metric: `mL`, `L`, `m³`
- USCS / imperial: `ft³`, `CCF`, `MCF`, `fl. oz.`, `gal` (warning: volumes expressed in
USCS/imperial units are currently assumed to be US volumes)
"""
VOLUME_STORAGE = "volume_storage"
"""Generic stored volume.
Use this device class for sensors measuring stored volume, for example the amount
of fuel in a fuel tank.
Unit of measurement: `VOLUME_*` units
- SI / metric: `mL`, `L`, `m³`
- USCS / imperial: `ft³`, `CCF`, `MCF`, `fl. oz.`, `gal` (warning: volumes expressed in
USCS/imperial units are currently assumed to be US volumes)
"""
VOLUME_FLOW_RATE = "volume_flow_rate"
"""Generic flow rate
Unit of measurement: UnitOfVolumeFlowRate
- SI / metric: `m³/h`, `m³/min`, `m³/s`, `L/h`, `L/min`, `L/s`, `mL/s`
- USCS / imperial: `ft³/min`, `gal/min`
"""
WATER = "water"
"""Water.
Unit of measurement:
- SI / metric: `m³`, `L`
- USCS / imperial: `ft³`, `CCF`, `MCF`, `gal` (warning: volumes expressed in
USCS/imperial units are currently assumed to be US volumes)
"""
WEIGHT = "weight"
"""Generic weight, represents a measurement of an object's mass.
Weight is used instead of mass to fit with every day language.
Unit of measurement: `MASS_*` units
- SI / metric: `μg`, `mg`, `g`, `kg`
- USCS / imperial: `oz`, `lb`
"""
WIND_DIRECTION = "wind_direction"
"""Wind direction.
Unit of measurement: `°`
"""
WIND_SPEED = "wind_speed"
"""Wind speed.
Unit of measurement: `SPEED_*` units
- SI /metric: `m/s`, `km/h`
- USCS / imperial: `ft/s`, `mph`
- Nautical: `kn`
- Beaufort: `Beaufort`
"""
@@ -1,41 +0,0 @@
"""Tuya models."""
from __future__ import annotations
from dataclasses import dataclass
@dataclass
class TuyaIntegerTypeDefinition:
"""Definition of an integer type data."""
dpcode: str
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))
@@ -1,19 +0,0 @@
"""Common utility functions for Tuya quirks."""
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.
Return None if value is None or if the value does not correspond to any
enum member.
"""
if value is None:
return None
try:
return enum_class(value)
except ValueError:
return None
@@ -1,57 +0,0 @@
"""Quirks registry."""
from __future__ import annotations
import logging
from typing import TYPE_CHECKING, Self
if TYPE_CHECKING:
from tuya_sharing import CustomerDevice
from .builder import TuyaDeviceQuirk
from .helpers import TuyaDeviceCategory
_LOGGER = logging.getLogger(__name__)
class QuirksRegistry:
"""Registry for Tuya quirks."""
instance: Self
_quirks: dict[str, dict[str, TuyaDeviceQuirk]]
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: TuyaDeviceCategory,
product_id: str,
quirk: TuyaDeviceQuirk,
) -> None:
"""Register a quirk for a specific device type."""
self._quirks.setdefault(category, {})[product_id] = quirk
def get_quirk_for_device(self, device: CustomerDevice) -> TuyaDeviceQuirk | None:
"""Get the quirk for a specific device."""
return self._quirks.get(device.category, {}).get(device.product_id)
def purge_custom_quirks(self, custom_quirks_root: str) -> None:
"""Purge custom quirks from the registry."""
for category_quirks in self._quirks.values():
to_remove = []
for product_id, quirk in category_quirks.items():
if quirk.quirk_file.is_relative_to(custom_quirks_root):
to_remove.append(product_id)
for product_id in to_remove:
_LOGGER.debug("Removing stale custom quirk: %s", product_id)
category_quirks.pop(product_id)
+3
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
+3
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
+5 -11
View File
@@ -5,20 +5,14 @@ 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.components.tuya.tuya_device_handlers import (
TUYA_QUIRKS_REGISTRY,
QuirksRegistry,
)
from homeassistant.components.tuya.tuya_device_handlers.builder import TuyaDeviceQuirk
from homeassistant.components.tuya.tuya_device_handlers.builder.base_quirk import (
BaseTuyaDefinition,
)
from homeassistant.components.tuya.tuya_device_handlers.devices import (
register_tuya_quirks,
)
from homeassistant.const import EntityCategory, Platform
from homeassistant.core import HomeAssistant
from homeassistant.helpers import translation