mirror of
https://github.com/home-assistant/core.git
synced 2026-02-05 14:55:35 +01:00
Compare commits
7 Commits
python-3.1
...
epenet-202
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1c19c2a278 | ||
|
|
631480349e | ||
|
|
114d721973 | ||
|
|
c4148d723f | ||
|
|
5f314c40df | ||
|
|
65d45f0052 | ||
|
|
73a8375d2d |
@@ -31,6 +31,7 @@ from .const import (
|
||||
TUYA_DISCOVERY_NEW,
|
||||
TUYA_HA_SIGNAL_UPDATE_ENTITY,
|
||||
)
|
||||
from .xternal_tuya_device_quirks import register_tuya_quirks
|
||||
|
||||
# Suppress logs from the library, it logs unneeded on error
|
||||
logging.getLogger("tuya_sharing").setLevel(logging.CRITICAL)
|
||||
@@ -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
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from dataclasses import dataclass, replace
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
from tuya_sharing import CustomerDevice, Manager
|
||||
@@ -26,8 +26,10 @@ from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from . import TuyaConfigEntry
|
||||
from .const import TUYA_DISCOVERY_NEW, DeviceCategory, DPCode, DPType
|
||||
from .entity import TuyaEntity
|
||||
from .models import IntegerTypeData
|
||||
from .models import IntegerTypeData, StateConversionFunction
|
||||
from .util import get_dpcode
|
||||
from .xternal_tuya_quirks import TUYA_QUIRKS_REGISTRY
|
||||
from .xternal_tuya_quirks.climate import CommonClimateType, TuyaClimateDefinition
|
||||
|
||||
TUYA_HVAC_TO_HA = {
|
||||
"auto": HVACMode.HEAT_COOL,
|
||||
@@ -46,6 +48,9 @@ class TuyaClimateEntityDescription(ClimateEntityDescription):
|
||||
"""Describe an Tuya climate entity."""
|
||||
|
||||
switch_only_hvac_mode: HVACMode
|
||||
current_temperature_state_conversion: StateConversionFunction | None = None
|
||||
target_temperature_state_conversion: StateConversionFunction | None = None
|
||||
target_temperature_command_conversion: StateConversionFunction | None = None
|
||||
|
||||
|
||||
CLIMATE_DESCRIPTIONS: dict[DeviceCategory, TuyaClimateEntityDescription] = {
|
||||
@@ -75,6 +80,25 @@ CLIMATE_DESCRIPTIONS: dict[DeviceCategory, TuyaClimateEntityDescription] = {
|
||||
),
|
||||
}
|
||||
|
||||
COMMON_CLIMATE_DEFINITIONS: dict[CommonClimateType, TuyaClimateEntityDescription] = {
|
||||
CommonClimateType.SWITCH_ONLY_HEAT_COOL: TuyaClimateEntityDescription(
|
||||
key="tbc",
|
||||
switch_only_hvac_mode=HVACMode.HEAT_COOL,
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
def _create_quirk_description(
|
||||
definition: TuyaClimateDefinition,
|
||||
) -> TuyaClimateEntityDescription:
|
||||
return replace(
|
||||
COMMON_CLIMATE_DEFINITIONS[definition.common_type],
|
||||
key=definition.key,
|
||||
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,
|
||||
@@ -90,7 +114,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 +228,17 @@ 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 +390,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._current_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 +418,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 +452,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
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from dataclasses import dataclass, replace
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
from tuya_sharing import CustomerDevice, Manager
|
||||
@@ -24,6 +24,8 @@ from .const import TUYA_DISCOVERY_NEW, DeviceCategory, DPCode, DPType
|
||||
from .entity import TuyaEntity
|
||||
from .models import EnumTypeData, IntegerTypeData
|
||||
from .util import get_dpcode
|
||||
from .xternal_tuya_quirks import TUYA_QUIRKS_REGISTRY
|
||||
from .xternal_tuya_quirks.cover import CommonCoverType, TuyaCoverDefinition
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
@@ -142,6 +144,26 @@ COVERS: dict[DeviceCategory, tuple[TuyaCoverEntityDescription, ...]] = {
|
||||
),
|
||||
}
|
||||
|
||||
COMMON_COVER_DEFINITIONS: dict[CommonCoverType, TuyaCoverEntityDescription] = {
|
||||
CommonCoverType.CURTAIN: TuyaCoverEntityDescription(
|
||||
key="tbc",
|
||||
translation_key="curtain",
|
||||
device_class=CoverDeviceClass.CURTAIN,
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
def _create_quirk_description(
|
||||
definition: TuyaCoverDefinition,
|
||||
) -> TuyaCoverEntityDescription:
|
||||
return replace(
|
||||
COMMON_COVER_DEFINITIONS[definition.common_type],
|
||||
key=definition.key,
|
||||
current_state=definition.current_state_dp_code,
|
||||
current_position=definition.current_position_dp_code,
|
||||
set_position=definition.set_position_dp_code,
|
||||
)
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
@@ -157,7 +179,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
|
||||
|
||||
@@ -3,14 +3,21 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import base64
|
||||
from collections.abc import Callable
|
||||
from dataclasses import dataclass
|
||||
import json
|
||||
import struct
|
||||
from typing import Self
|
||||
from typing import Any, Self
|
||||
|
||||
from tuya_sharing import CustomerDevice
|
||||
|
||||
from .const import DPCode
|
||||
from .util import remap_value
|
||||
|
||||
type StateConversionFunction = Callable[
|
||||
[CustomerDevice, EnumTypeData | IntegerTypeData | None, Any], Any
|
||||
]
|
||||
|
||||
|
||||
@dataclass
|
||||
class IntegerTypeData:
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import replace
|
||||
|
||||
from tuya_sharing import CustomerDevice, Manager
|
||||
|
||||
from homeassistant.components.select import SelectEntity, SelectEntityDescription
|
||||
@@ -13,6 +15,8 @@ from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from . import TuyaConfigEntry
|
||||
from .const import TUYA_DISCOVERY_NEW, DeviceCategory, DPCode, DPType
|
||||
from .entity import TuyaEntity
|
||||
from .xternal_tuya_quirks import TUYA_QUIRKS_REGISTRY
|
||||
from .xternal_tuya_quirks.select import CommonSelectType, TuyaSelectDefinition
|
||||
|
||||
# All descriptions can be found here. Mostly the Enum data types in the
|
||||
# default instructions set of each category end up being a select.
|
||||
@@ -342,6 +346,23 @@ SELECTS[DeviceCategory.DGHSXJ] = SELECTS[DeviceCategory.SP]
|
||||
# Power Socket (duplicate of `kg`)
|
||||
SELECTS[DeviceCategory.PC] = SELECTS[DeviceCategory.KG]
|
||||
|
||||
COMMON_SELECT_DEFINITIONS: dict[CommonSelectType, SelectEntityDescription] = {
|
||||
CommonSelectType.CONTROL_BACK_MODE: SelectEntityDescription(
|
||||
key="tbc",
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
translation_key="curtain_motor_mode",
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
def _create_quirk_description(
|
||||
definition: TuyaSelectDefinition,
|
||||
) -> SelectEntityDescription:
|
||||
return replace(
|
||||
COMMON_SELECT_DEFINITIONS[definition.common_type],
|
||||
key=DPCode(definition.key),
|
||||
)
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
@@ -357,7 +378,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
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable
|
||||
from dataclasses import dataclass
|
||||
from dataclasses import dataclass, replace
|
||||
from typing import Any
|
||||
|
||||
from tuya_sharing import CustomerDevice, Manager
|
||||
@@ -44,6 +44,8 @@ from .const import (
|
||||
)
|
||||
from .entity import TuyaEntity
|
||||
from .models import ComplexValue, ElectricityValue, EnumTypeData, IntegerTypeData
|
||||
from .xternal_tuya_quirks import TUYA_QUIRKS_REGISTRY
|
||||
from .xternal_tuya_quirks.sensor import CommonSensorType, TuyaSensorDefinition
|
||||
|
||||
_WIND_DIRECTIONS = {
|
||||
"north": 0.0,
|
||||
@@ -1624,6 +1626,23 @@ SENSORS[DeviceCategory.DGHSXJ] = SENSORS[DeviceCategory.SP]
|
||||
# Power Socket (duplicate of `kg`)
|
||||
SENSORS[DeviceCategory.PC] = SENSORS[DeviceCategory.KG]
|
||||
|
||||
COMMON_SENSOR_DEFINITIONS: dict[CommonSensorType, TuyaSensorEntityDescription] = {
|
||||
CommonSensorType.TIME_TOTAL: TuyaSensorEntityDescription(
|
||||
key="tbc",
|
||||
translation_key="last_operation_duration",
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
def _create_quirk_description(
|
||||
definition: TuyaSensorDefinition,
|
||||
) -> TuyaSensorEntityDescription:
|
||||
return replace(
|
||||
COMMON_SENSOR_DEFINITIONS[definition.common_type],
|
||||
key=DPCode(definition.key),
|
||||
)
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
@@ -1639,7 +1658,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
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from dataclasses import dataclass, replace
|
||||
from typing import Any
|
||||
|
||||
from tuya_sharing import CustomerDevice, Manager
|
||||
@@ -27,6 +27,8 @@ from homeassistant.helpers.issue_registry import (
|
||||
from . import TuyaConfigEntry
|
||||
from .const import DOMAIN, TUYA_DISCOVERY_NEW, DeviceCategory, DPCode
|
||||
from .entity import TuyaEntity
|
||||
from .xternal_tuya_quirks import TUYA_QUIRKS_REGISTRY
|
||||
from .xternal_tuya_quirks.switch import CommonSwitchType, TuyaSwitchDefinition
|
||||
|
||||
|
||||
@dataclass(frozen=True, kw_only=True)
|
||||
@@ -898,6 +900,23 @@ SWITCHES[DeviceCategory.CZ] = SWITCHES[DeviceCategory.PC]
|
||||
# Smart Camera - Low power consumption camera (duplicate of `sp`)
|
||||
SWITCHES[DeviceCategory.DGHSXJ] = SWITCHES[DeviceCategory.SP]
|
||||
|
||||
COMMON_SWITCH_DEFINITIONS: dict[CommonSwitchType, SwitchEntityDescription] = {
|
||||
CommonSwitchType.CHILD_LOCK: SwitchEntityDescription(
|
||||
key="tbc",
|
||||
translation_key="child_lock",
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
def _create_quirk_description(
|
||||
definition: TuyaSwitchDefinition,
|
||||
) -> SwitchEntityDescription:
|
||||
return replace(
|
||||
COMMON_SWITCH_DEFINITIONS[definition.common_type],
|
||||
key=DPCode(definition.key),
|
||||
)
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
@@ -914,7 +933,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
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
"""Quirks for Tuya."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import importlib
|
||||
import logging
|
||||
import pathlib
|
||||
import pkgutil
|
||||
import sys
|
||||
|
||||
from ..xternal_tuya_quirks import TUYA_QUIRKS_REGISTRY
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def register_tuya_quirks(custom_quirks_path: str | None = None) -> None:
|
||||
"""Register all quirks with xternal_tuya_quirks.
|
||||
|
||||
- remove custom quirks from `custom_quirks_path`
|
||||
- add quirks from `xternal_tuya_device_quirks`
|
||||
- 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 `xternal_tuya_device_quirks` 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"
|
||||
)
|
||||
@@ -0,0 +1,47 @@
|
||||
"""Quirks for Tuya."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from ..const import DPCode
|
||||
from ..xternal_tuya_quirks import TUYA_QUIRKS_REGISTRY, TuyaDeviceQuirk
|
||||
from ..xternal_tuya_quirks.cover import CommonCoverType
|
||||
from ..xternal_tuya_quirks.select import CommonSelectType
|
||||
from ..xternal_tuya_quirks.sensor import CommonSensorType
|
||||
|
||||
(
|
||||
# This model has percent_state and percent_control but percent_state never
|
||||
# gets updated - force percent_control instead
|
||||
TuyaDeviceQuirk()
|
||||
.applies_to(category="cl", product_id="g1cp07dsqnbdbbki")
|
||||
.add_common_cover(
|
||||
key=DPCode.CONTROL,
|
||||
common_type=CommonCoverType.CURTAIN,
|
||||
current_position_dp_code=DPCode.PERCENT_CONTROL,
|
||||
set_position_dp_code=DPCode.PERCENT_CONTROL,
|
||||
)
|
||||
.add_common_select(
|
||||
key=DPCode.CONTROL_BACK_MODE,
|
||||
common_type=CommonSelectType.CONTROL_BACK_MODE,
|
||||
)
|
||||
.register(TUYA_QUIRKS_REGISTRY)
|
||||
)
|
||||
(
|
||||
# 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="cl", product_id="lfkr93x0ukp5gaia")
|
||||
.add_common_cover(
|
||||
key=DPCode.CONTROL,
|
||||
common_type=CommonCoverType.CURTAIN,
|
||||
current_state_dp_code=DPCode.CONTROL,
|
||||
)
|
||||
.add_common_select(
|
||||
key=DPCode.CONTROL_BACK_MODE,
|
||||
common_type=CommonSelectType.CONTROL_BACK_MODE,
|
||||
)
|
||||
.add_common_sensor(
|
||||
key=DPCode.TIME_TOTAL,
|
||||
common_type=CommonSensorType.TIME_TOTAL,
|
||||
)
|
||||
.register(TUYA_QUIRKS_REGISTRY)
|
||||
)
|
||||
@@ -0,0 +1,53 @@
|
||||
"""Quirks for Tuya."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
from tuya_sharing import CustomerDevice
|
||||
|
||||
from ..const import DPCode
|
||||
from ..models import EnumTypeData, IntegerTypeData
|
||||
from ..xternal_tuya_quirks import TUYA_QUIRKS_REGISTRY, TuyaDeviceQuirk
|
||||
from ..xternal_tuya_quirks.climate import CommonClimateType
|
||||
from ..xternal_tuya_quirks.switch import CommonSwitchType
|
||||
from ..xternal_tuya_quirks.utils import scale_value, scale_value_back
|
||||
|
||||
|
||||
def _scale_value_force_scale_1(
|
||||
_device: CustomerDevice, dptype: EnumTypeData | IntegerTypeData | None, value: Any
|
||||
) -> float:
|
||||
"""Scale value to scale 1."""
|
||||
if TYPE_CHECKING:
|
||||
assert isinstance(dptype, IntegerTypeData)
|
||||
assert isinstance(value, int)
|
||||
return scale_value(value, dptype.step, 1)
|
||||
|
||||
|
||||
def _scale_value_back_force_scale_1(
|
||||
_device: CustomerDevice, dptype: EnumTypeData | IntegerTypeData | None, value: Any
|
||||
) -> int:
|
||||
"""Unscale value to scale 1."""
|
||||
if TYPE_CHECKING:
|
||||
assert isinstance(dptype, IntegerTypeData)
|
||||
assert isinstance(value, float)
|
||||
return scale_value_back(value, dptype.step, 1)
|
||||
|
||||
|
||||
(
|
||||
# This model has invalid scale 0 for temperature dps - force scale 1
|
||||
TuyaDeviceQuirk()
|
||||
.applies_to(category="wk", product_id="IAYz2WK1th0cMLmL")
|
||||
.add_common_climate(
|
||||
key="wk", # to avoid breaking change
|
||||
common_type=CommonClimateType.SWITCH_ONLY_HEAT_COOL,
|
||||
current_temperature_state_conversion=_scale_value_force_scale_1,
|
||||
target_temperature_state_conversion=_scale_value_force_scale_1,
|
||||
target_temperature_command_conversion=_scale_value_back_force_scale_1,
|
||||
)
|
||||
.add_common_switch(
|
||||
key=DPCode.CHILD_LOCK,
|
||||
common_type=CommonSwitchType.CHILD_LOCK,
|
||||
)
|
||||
.register(TUYA_QUIRKS_REGISTRY)
|
||||
)
|
||||
@@ -0,0 +1,16 @@
|
||||
"""Quirks for Tuya."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from .device_quirk import TuyaDeviceQuirk
|
||||
from .homeassistant import parse_enum
|
||||
from .registry import QuirksRegistry
|
||||
|
||||
__all__ = [
|
||||
"TUYA_QUIRKS_REGISTRY",
|
||||
"QuirksRegistry",
|
||||
"TuyaDeviceQuirk",
|
||||
"parse_enum",
|
||||
]
|
||||
|
||||
TUYA_QUIRKS_REGISTRY = QuirksRegistry()
|
||||
27
homeassistant/components/tuya/xternal_tuya_quirks/climate.py
Normal file
27
homeassistant/components/tuya/xternal_tuya_quirks/climate.py
Normal file
@@ -0,0 +1,27 @@
|
||||
"""Common climate quirks for Tuya devices."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from enum import StrEnum
|
||||
|
||||
from ..models import StateConversionFunction
|
||||
|
||||
|
||||
class CommonClimateType(StrEnum):
|
||||
"""Common climate types."""
|
||||
|
||||
SWITCH_ONLY_HEAT_COOL = "switch_only_heat_cool"
|
||||
|
||||
|
||||
@dataclass(kw_only=True)
|
||||
class TuyaClimateDefinition:
|
||||
"""Definition for a climate entity."""
|
||||
|
||||
key: str
|
||||
|
||||
common_type: CommonClimateType
|
||||
|
||||
current_temperature_state_conversion: StateConversionFunction | None = None
|
||||
target_temperature_state_conversion: StateConversionFunction | None = None
|
||||
target_temperature_command_conversion: StateConversionFunction | None = None
|
||||
28
homeassistant/components/tuya/xternal_tuya_quirks/cover.py
Normal file
28
homeassistant/components/tuya/xternal_tuya_quirks/cover.py
Normal file
@@ -0,0 +1,28 @@
|
||||
"""Common cover quirks for Tuya devices."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from enum import StrEnum
|
||||
|
||||
from ..const import DPCode
|
||||
|
||||
|
||||
class CommonCoverType(StrEnum):
|
||||
"""Common cover types."""
|
||||
|
||||
CURTAIN = "curtain"
|
||||
|
||||
|
||||
@dataclass(kw_only=True)
|
||||
class TuyaCoverDefinition:
|
||||
"""Definition for a cover entity."""
|
||||
|
||||
key: str
|
||||
|
||||
common_type: CommonCoverType
|
||||
|
||||
current_position_dp_code: DPCode | None = None
|
||||
current_state_dp_code: DPCode | None = None
|
||||
set_position_dp_code: DPCode | None = None
|
||||
set_state_dp_code: DPCode
|
||||
@@ -0,0 +1,146 @@
|
||||
"""Quirks registry."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import inspect
|
||||
import pathlib
|
||||
from typing import TYPE_CHECKING, Self
|
||||
|
||||
from ..const import DPCode
|
||||
from ..models import StateConversionFunction
|
||||
from .climate import CommonClimateType, TuyaClimateDefinition
|
||||
from .cover import CommonCoverType, TuyaCoverDefinition
|
||||
from .select import CommonSelectType, TuyaSelectDefinition
|
||||
from .sensor import CommonSensorType, TuyaSensorDefinition
|
||||
from .switch import CommonSwitchType, TuyaSwitchDefinition
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .registry import QuirksRegistry
|
||||
|
||||
|
||||
class TuyaDeviceQuirk:
|
||||
"""Quirk for Tuya device."""
|
||||
|
||||
_applies_to: list[tuple[str, str]]
|
||||
climate_definitions: list[TuyaClimateDefinition]
|
||||
cover_definitions: list[TuyaCoverDefinition]
|
||||
select_definitions: list[TuyaSelectDefinition]
|
||||
sensor_definitions: list[TuyaSensorDefinition]
|
||||
switch_definitions: list[TuyaSwitchDefinition]
|
||||
|
||||
def __init__(self) -> None:
|
||||
"""Initialize the quirk."""
|
||||
self._applies_to = []
|
||||
self.climate_definitions = []
|
||||
self.cover_definitions = []
|
||||
self.select_definitions = []
|
||||
self.sensor_definitions = []
|
||||
self.switch_definitions = []
|
||||
|
||||
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: 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_common_climate(
|
||||
self,
|
||||
*,
|
||||
key: str,
|
||||
common_type: CommonClimateType,
|
||||
current_temperature_state_conversion: StateConversionFunction | None = None,
|
||||
target_temperature_state_conversion: StateConversionFunction | None = None,
|
||||
target_temperature_command_conversion: StateConversionFunction | None = None,
|
||||
) -> Self:
|
||||
"""Add climate definition."""
|
||||
self.climate_definitions.append(
|
||||
TuyaClimateDefinition(
|
||||
key=key,
|
||||
common_type=common_type,
|
||||
current_temperature_state_conversion=current_temperature_state_conversion,
|
||||
target_temperature_state_conversion=target_temperature_state_conversion,
|
||||
target_temperature_command_conversion=target_temperature_command_conversion,
|
||||
)
|
||||
)
|
||||
return self
|
||||
|
||||
def add_common_cover(
|
||||
self,
|
||||
*,
|
||||
key: DPCode,
|
||||
common_type: CommonCoverType,
|
||||
current_position_dp_code: DPCode | None = None,
|
||||
current_state_dp_code: DPCode | None = None,
|
||||
set_position_dp_code: DPCode | None = None,
|
||||
set_state_dp_code: DPCode | None = None,
|
||||
) -> Self:
|
||||
"""Add cover definition."""
|
||||
self.cover_definitions.append(
|
||||
TuyaCoverDefinition(
|
||||
key=key,
|
||||
common_type=common_type,
|
||||
current_position_dp_code=current_position_dp_code,
|
||||
current_state_dp_code=current_state_dp_code,
|
||||
set_position_dp_code=set_position_dp_code,
|
||||
set_state_dp_code=set_state_dp_code or key,
|
||||
)
|
||||
)
|
||||
return self
|
||||
|
||||
def add_common_select(
|
||||
self,
|
||||
*,
|
||||
key: DPCode,
|
||||
common_type: CommonSelectType,
|
||||
) -> Self:
|
||||
"""Add select definition."""
|
||||
self.select_definitions.append(
|
||||
TuyaSelectDefinition(
|
||||
key=key,
|
||||
common_type=common_type,
|
||||
)
|
||||
)
|
||||
return self
|
||||
|
||||
def add_common_sensor(
|
||||
self,
|
||||
*,
|
||||
key: DPCode,
|
||||
common_type: CommonSensorType,
|
||||
) -> Self:
|
||||
"""Add sensor definition."""
|
||||
self.sensor_definitions.append(
|
||||
TuyaSensorDefinition(
|
||||
key=key,
|
||||
common_type=common_type,
|
||||
)
|
||||
)
|
||||
return self
|
||||
|
||||
def add_common_switch(
|
||||
self,
|
||||
*,
|
||||
key: DPCode,
|
||||
common_type: CommonSwitchType,
|
||||
) -> Self:
|
||||
"""Add switch definition."""
|
||||
self.switch_definitions.append(
|
||||
TuyaSwitchDefinition(
|
||||
key=key,
|
||||
common_type=common_type,
|
||||
)
|
||||
)
|
||||
return self
|
||||
@@ -0,0 +1,15 @@
|
||||
"""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
|
||||
@@ -0,0 +1,51 @@
|
||||
"""Quirks registry."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from typing import TYPE_CHECKING, Self
|
||||
|
||||
from tuya_sharing import CustomerDevice
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .device_quirk import TuyaDeviceQuirk
|
||||
|
||||
_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: str, 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)
|
||||
23
homeassistant/components/tuya/xternal_tuya_quirks/select.py
Normal file
23
homeassistant/components/tuya/xternal_tuya_quirks/select.py
Normal file
@@ -0,0 +1,23 @@
|
||||
"""Common select quirks for Tuya devices."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from enum import StrEnum
|
||||
|
||||
from ..const import DPCode
|
||||
|
||||
|
||||
class CommonSelectType(StrEnum):
|
||||
"""Common select types."""
|
||||
|
||||
CONTROL_BACK_MODE = "control_back_mode"
|
||||
|
||||
|
||||
@dataclass(kw_only=True)
|
||||
class TuyaSelectDefinition:
|
||||
"""Definition for a select entity."""
|
||||
|
||||
key: DPCode
|
||||
|
||||
common_type: CommonSelectType
|
||||
23
homeassistant/components/tuya/xternal_tuya_quirks/sensor.py
Normal file
23
homeassistant/components/tuya/xternal_tuya_quirks/sensor.py
Normal file
@@ -0,0 +1,23 @@
|
||||
"""Common sensor quirks for Tuya devices."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from enum import StrEnum
|
||||
|
||||
from ..const import DPCode
|
||||
|
||||
|
||||
class CommonSensorType(StrEnum):
|
||||
"""Common sensor types."""
|
||||
|
||||
TIME_TOTAL = "time_total"
|
||||
|
||||
|
||||
@dataclass(kw_only=True)
|
||||
class TuyaSensorDefinition:
|
||||
"""Definition for a sensor entity."""
|
||||
|
||||
key: DPCode
|
||||
|
||||
common_type: CommonSensorType
|
||||
23
homeassistant/components/tuya/xternal_tuya_quirks/switch.py
Normal file
23
homeassistant/components/tuya/xternal_tuya_quirks/switch.py
Normal file
@@ -0,0 +1,23 @@
|
||||
"""Common switch quirks for Tuya devices."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from enum import StrEnum
|
||||
|
||||
from ..const import DPCode
|
||||
|
||||
|
||||
class CommonSwitchType(StrEnum):
|
||||
"""Common switch types."""
|
||||
|
||||
CHILD_LOCK = "child_lock"
|
||||
|
||||
|
||||
@dataclass(kw_only=True)
|
||||
class TuyaSwitchDefinition:
|
||||
"""Definition for a switch entity."""
|
||||
|
||||
key: DPCode
|
||||
|
||||
common_type: CommonSwitchType
|
||||
18
homeassistant/components/tuya/xternal_tuya_quirks/utils.py
Normal file
18
homeassistant/components/tuya/xternal_tuya_quirks/utils.py
Normal file
@@ -0,0 +1,18 @@
|
||||
"""Common utility functions for Tuya quirks."""
|
||||
|
||||
|
||||
def scale_value(value: int, step: float, scale: float) -> float:
|
||||
"""Official scaling function from Tuya.
|
||||
|
||||
See https://support.tuya.com/en/help/_detail/Kadi66s463e2q
|
||||
"""
|
||||
return step * value / (10**scale)
|
||||
|
||||
|
||||
def scale_value_back(value: float, step: float, scale: float) -> int:
|
||||
"""Official scaling function from Tuya.
|
||||
|
||||
See https://support.tuya.com/en/help/_detail/Kadi66s463e2q
|
||||
"""
|
||||
|
||||
return int(value * (10**scale) / step)
|
||||
@@ -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',
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user