From 87cbb9235630b3b66d5a550b9e80028022fadec8 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 1 Oct 2025 12:28:42 +0000 Subject: [PATCH] Add time_total to lfkr93x0ukp5gaia --- .../components/tuya/quirks/__init__.py | 6 +- .../components/tuya/quirks/device_quirk.py | 46 +- .../tuya/quirks/device_quirks/cl.py | 13 +- .../components/tuya/quirks/homeassistant.py | 433 ++++++++++++++++++ homeassistant/components/tuya/sensor.py | 25 +- 5 files changed, 513 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/tuya/quirks/__init__.py b/homeassistant/components/tuya/quirks/__init__.py index 635704f6c423..fd8fa0dbfe81 100644 --- a/homeassistant/components/tuya/quirks/__init__.py +++ b/homeassistant/components/tuya/quirks/__init__.py @@ -8,8 +8,8 @@ import pathlib import pkgutil import sys -from .device_quirk import TuyaCoverDefinition, TuyaCoverDeviceClass, TuyaDeviceQuirk -from .homeassistant import parse_enum +from .device_quirk import TuyaCoverDefinition, TuyaDeviceQuirk, TuyaSensorDefinition +from .homeassistant import TuyaCoverDeviceClass, TuyaSensorDeviceClass, parse_enum from .registry import QuirksRegistry __all__ = [ @@ -18,6 +18,8 @@ __all__ = [ "TuyaCoverDefinition", "TuyaCoverDeviceClass", "TuyaDeviceQuirk", + "TuyaSensorDefinition", + "TuyaSensorDeviceClass", "parse_enum", ] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/tuya/quirks/device_quirk.py b/homeassistant/components/tuya/quirks/device_quirk.py index 1dc876d1892e..b876ea469d55 100644 --- a/homeassistant/components/tuya/quirks/device_quirk.py +++ b/homeassistant/components/tuya/quirks/device_quirk.py @@ -7,7 +7,11 @@ import inspect import pathlib from typing import TYPE_CHECKING, Self -from .homeassistant import TuyaCoverDeviceClass +from .homeassistant import ( + TuyaCoverDeviceClass, + TuyaEntityCategory, + TuyaSensorDeviceClass, +) if TYPE_CHECKING: from .registry import QuirksRegistry @@ -15,16 +19,18 @@ if TYPE_CHECKING: @dataclass class BaseTuyaDefinition: - """Definition for a Tuya device.""" + """Definition for a Tuya entity.""" key: str translation_key: str translation_string: str + device_class: str | None = None + entity_category: str | None = None @dataclass(kw_only=True) class TuyaCoverDefinition(BaseTuyaDefinition): - """Definition for a cover device.""" + """Definition for a cover entity.""" device_class: TuyaCoverDeviceClass | None = None @@ -33,16 +39,25 @@ class TuyaCoverDefinition(BaseTuyaDefinition): set_position_dp_code: str | None = None +@dataclass(kw_only=True) +class TuyaSensorDefinition(BaseTuyaDefinition): + """Definition for a sensor entity.""" + + device_class: TuyaSensorDeviceClass | None = None + + class TuyaDeviceQuirk: """Quirk for Tuya device.""" _applies_to: list[tuple[str, str]] cover_definitions: list[TuyaCoverDefinition] + sensor_definitions: list[TuyaSensorDefinition] def __init__(self) -> None: """Initialize the quirk.""" self._applies_to = [] self.cover_definitions = [] + self.sensor_definitions = [] current_frame = inspect.currentframe() if TYPE_CHECKING: @@ -70,11 +85,12 @@ class TuyaDeviceQuirk: translation_key: str, translation_string: str, device_class: TuyaCoverDeviceClass | None = None, + # Cover specific 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.""" + """Add cover definition.""" self.cover_definitions.append( TuyaCoverDefinition( key=key, @@ -87,3 +103,25 @@ class TuyaDeviceQuirk: ) ) return self + + def add_sensor( + self, + *, + key: str, + 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 diff --git a/homeassistant/components/tuya/quirks/device_quirks/cl.py b/homeassistant/components/tuya/quirks/device_quirks/cl.py index 63ced79de652..5251640e296f 100644 --- a/homeassistant/components/tuya/quirks/device_quirks/cl.py +++ b/homeassistant/components/tuya/quirks/device_quirks/cl.py @@ -3,7 +3,8 @@ from __future__ import annotations from .. import TUYA_QUIRKS_REGISTRY -from ..device_quirk import TuyaCoverDeviceClass, TuyaDeviceQuirk +from ..device_quirk import TuyaDeviceQuirk +from ..homeassistant import TuyaCoverDeviceClass, TuyaEntityCategory ( # This model has percent_state and percent_control but percent_state never @@ -13,7 +14,7 @@ from ..device_quirk import TuyaCoverDeviceClass, TuyaDeviceQuirk .add_cover( key="control", translation_key="curtain", - translation_string="curtain", + translation_string="[%key:component::cover::entity_component::curtain::name%]", current_state_dp_code="control", current_position_dp_code="percent_control", set_position_dp_code="percent_control", @@ -29,9 +30,15 @@ from ..device_quirk import TuyaCoverDeviceClass, TuyaDeviceQuirk .add_cover( key="control", translation_key="curtain", - translation_string="curtain", + translation_string="[%key:component::cover::entity_component::curtain::name%]", current_state_dp_code="control", device_class=TuyaCoverDeviceClass.CURTAIN, ) + .add_sensor( + key="time_total", + translation_key="last_operation_duration", + translation_string="Last operation duration", + entity_category=TuyaEntityCategory.DIAGNOSTIC, + ) .register(TUYA_QUIRKS_REGISTRY) ) diff --git a/homeassistant/components/tuya/quirks/homeassistant.py b/homeassistant/components/tuya/quirks/homeassistant.py index 0d17c5cb94cd..0f2e9c371797 100644 --- a/homeassistant/components/tuya/quirks/homeassistant.py +++ b/homeassistant/components/tuya/quirks/homeassistant.py @@ -15,6 +15,22 @@ def parse_enum[T: StrEnum](enum_class: type[T], value: str | None) -> T | None: return None +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 TuyaCoverDeviceClass(StrEnum): """Device class for cover.""" @@ -29,3 +45,420 @@ class TuyaCoverDeviceClass(StrEnum): 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` + """ diff --git a/homeassistant/components/tuya/sensor.py b/homeassistant/components/tuya/sensor.py index 06707e34a112..89c61cd3892c 100644 --- a/homeassistant/components/tuya/sensor.py +++ b/homeassistant/components/tuya/sensor.py @@ -44,6 +44,7 @@ from .const import ( ) from .entity import TuyaEntity from .models import ComplexValue, ElectricityValue, EnumTypeData, IntegerTypeData +from .quirks import TUYA_QUIRKS_REGISTRY, TuyaSensorDefinition, parse_enum from .util import get_dptype _WIND_DIRECTIONS = { @@ -1635,6 +1636,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 +1661,18 @@ 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.function + or definition.key in device.status_range + ) + ) + elif descriptions := SENSORS.get(device.category): entities.extend( TuyaSensorEntity(device, manager, description) for description in descriptions