mirror of
https://github.com/home-assistant/core.git
synced 2026-04-01 21:16:34 +00:00
Compare commits
24 Commits
remove_dev
...
epenet-202
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0d78e7459b | ||
|
|
3246692998 | ||
|
|
c3ade123be | ||
|
|
14bd55426d | ||
|
|
9fa0974c8c | ||
|
|
1f2af23dff | ||
|
|
69580d8023 | ||
|
|
4d60d1f2ab | ||
|
|
d26bb6eeba | ||
|
|
0a31ca153f | ||
|
|
2bb2153b9c | ||
|
|
e201adf4e0 | ||
|
|
29282ed2ec | ||
|
|
87cbb92356 | ||
|
|
c728910cd7 | ||
|
|
a84b10f905 | ||
|
|
9f09c137cd | ||
|
|
04c9d55e03 | ||
|
|
378a3151d4 | ||
|
|
b169bf0bf0 | ||
|
|
9369cf82fb | ||
|
|
51d7b8e898 | ||
|
|
4abb397fd3 | ||
|
|
546f089eae |
@@ -5,6 +5,7 @@ from __future__ import annotations
|
||||
import logging
|
||||
from typing import Any, NamedTuple
|
||||
|
||||
from tuya_device_handlers.devices import register_tuya_quirks
|
||||
from tuya_sharing import (
|
||||
CustomerDevice,
|
||||
Manager,
|
||||
@@ -103,6 +104,10 @@ async def async_setup_entry(hass: HomeAssistant, entry: TuyaConfigEntry) -> bool
|
||||
model_id=device.product_id,
|
||||
)
|
||||
|
||||
# Should be loaded from configuration.yaml
|
||||
# but for now, we can use a hardcoded path for testing
|
||||
quirks_path = "/config/tuya_quirks/"
|
||||
await hass.async_add_executor_job(register_tuya_quirks, quirks_path)
|
||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||
# If the device does not register any entities, the device does not need to subscribe
|
||||
# So the subscription is here
|
||||
|
||||
@@ -5,6 +5,11 @@ from __future__ import annotations
|
||||
from dataclasses import dataclass
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
from tuya_device_handlers import TUYA_QUIRKS_REGISTRY
|
||||
from tuya_device_handlers.builder import (
|
||||
TuyaClimateDefinition,
|
||||
TuyaIntegerConversionFunction,
|
||||
)
|
||||
from tuya_sharing import CustomerDevice, Manager
|
||||
|
||||
from homeassistant.components.climate import (
|
||||
@@ -46,6 +51,9 @@ class TuyaClimateEntityDescription(ClimateEntityDescription):
|
||||
"""Describe an Tuya climate entity."""
|
||||
|
||||
switch_only_hvac_mode: HVACMode
|
||||
current_temperature_state_conversion: TuyaIntegerConversionFunction | None = None
|
||||
target_temperature_state_conversion: TuyaIntegerConversionFunction | None = None
|
||||
target_temperature_command_conversion: TuyaIntegerConversionFunction | None = None
|
||||
|
||||
|
||||
CLIMATE_DESCRIPTIONS: dict[DeviceCategory, TuyaClimateEntityDescription] = {
|
||||
@@ -76,6 +84,18 @@ CLIMATE_DESCRIPTIONS: dict[DeviceCategory, TuyaClimateEntityDescription] = {
|
||||
}
|
||||
|
||||
|
||||
def _create_quirk_description(
|
||||
definition: TuyaClimateDefinition,
|
||||
) -> TuyaClimateEntityDescription:
|
||||
return TuyaClimateEntityDescription(
|
||||
key=definition.key,
|
||||
switch_only_hvac_mode=HVACMode(definition.switch_only_hvac_mode),
|
||||
current_temperature_state_conversion=definition.current_temperature_state_conversion,
|
||||
target_temperature_state_conversion=definition.target_temperature_state_conversion,
|
||||
target_temperature_command_conversion=definition.target_temperature_command_conversion,
|
||||
)
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: TuyaConfigEntry,
|
||||
@@ -90,7 +110,17 @@ async def async_setup_entry(
|
||||
entities: list[TuyaClimateEntity] = []
|
||||
for device_id in device_ids:
|
||||
device = manager.device_map[device_id]
|
||||
if device and device.category in CLIMATE_DESCRIPTIONS:
|
||||
if quirk := TUYA_QUIRKS_REGISTRY.get_quirk_for_device(device):
|
||||
entities.extend(
|
||||
TuyaClimateEntity(
|
||||
device,
|
||||
manager,
|
||||
_create_quirk_description(definition),
|
||||
hass.config.units.temperature_unit,
|
||||
)
|
||||
for definition in quirk.climate_definitions
|
||||
)
|
||||
elif device.category in CLIMATE_DESCRIPTIONS:
|
||||
entities.append(
|
||||
TuyaClimateEntity(
|
||||
device,
|
||||
@@ -194,8 +224,16 @@ class TuyaClimateEntity(TuyaEntity, ClimateEntity):
|
||||
# it to define min, max & step temperatures
|
||||
if self._set_temperature:
|
||||
self._attr_supported_features |= ClimateEntityFeature.TARGET_TEMPERATURE
|
||||
self._attr_max_temp = self._set_temperature.max_scaled
|
||||
self._attr_min_temp = self._set_temperature.min_scaled
|
||||
if convert := self.entity_description.target_temperature_state_conversion:
|
||||
self._attr_max_temp = convert(
|
||||
self.device, self._set_temperature, self._set_temperature.max
|
||||
)
|
||||
self._attr_min_temp = convert(
|
||||
self.device, self._set_temperature, self._set_temperature.min
|
||||
)
|
||||
else:
|
||||
self._attr_max_temp = self._set_temperature.max_scaled
|
||||
self._attr_min_temp = self._set_temperature.min_scaled
|
||||
self._attr_target_temperature_step = self._set_temperature.step_scaled
|
||||
|
||||
# Determine HVAC modes
|
||||
@@ -347,13 +385,20 @@ class TuyaClimateEntity(TuyaEntity, ClimateEntity):
|
||||
# guarded by ClimateEntityFeature.TARGET_TEMPERATURE
|
||||
assert self._set_temperature is not None
|
||||
|
||||
if convert := self.entity_description.target_temperature_command_conversion:
|
||||
value = convert(
|
||||
self.device, self._set_temperature, kwargs[ATTR_TEMPERATURE]
|
||||
)
|
||||
else:
|
||||
value = round(
|
||||
self._set_temperature.scale_value_back(kwargs[ATTR_TEMPERATURE])
|
||||
)
|
||||
|
||||
self._send_command(
|
||||
[
|
||||
{
|
||||
"code": self._set_temperature.dpcode,
|
||||
"value": round(
|
||||
self._set_temperature.scale_value_back(kwargs[ATTR_TEMPERATURE])
|
||||
),
|
||||
"value": value,
|
||||
}
|
||||
]
|
||||
)
|
||||
@@ -368,6 +413,9 @@ class TuyaClimateEntity(TuyaEntity, ClimateEntity):
|
||||
if temperature is None:
|
||||
return None
|
||||
|
||||
if convert := self.entity_description.current_temperature_state_conversion:
|
||||
return convert(self.device, self._current_temperature, temperature)
|
||||
|
||||
if self._current_temperature.scale == 0 and self._current_temperature.step != 1:
|
||||
# The current temperature can have a scale of 0 or 1 and is used for
|
||||
# rounding, Home Assistant doesn't need to round but we will always
|
||||
@@ -399,6 +447,9 @@ class TuyaClimateEntity(TuyaEntity, ClimateEntity):
|
||||
if temperature is None:
|
||||
return None
|
||||
|
||||
if convert := self.entity_description.target_temperature_state_conversion:
|
||||
return convert(self.device, self._set_temperature, temperature)
|
||||
|
||||
return self._set_temperature.scale_value(temperature)
|
||||
|
||||
@property
|
||||
|
||||
@@ -5,6 +5,9 @@ from __future__ import annotations
|
||||
from dataclasses import dataclass
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
from tuya_device_handlers import TUYA_QUIRKS_REGISTRY
|
||||
from tuya_device_handlers.builder import TuyaCoverDefinition
|
||||
from tuya_device_handlers.helpers import parse_enum
|
||||
from tuya_sharing import CustomerDevice, Manager
|
||||
|
||||
from homeassistant.components.cover import (
|
||||
@@ -143,6 +146,19 @@ COVERS: dict[DeviceCategory, tuple[TuyaCoverEntityDescription, ...]] = {
|
||||
}
|
||||
|
||||
|
||||
def _create_quirk_description(
|
||||
definition: TuyaCoverDefinition,
|
||||
) -> TuyaCoverEntityDescription:
|
||||
return TuyaCoverEntityDescription(
|
||||
key=DPCode(definition.key),
|
||||
translation_key=definition.translation_key,
|
||||
current_state=parse_enum(DPCode, definition.current_state_dp_code),
|
||||
current_position=parse_enum(DPCode, definition.current_position_dp_code),
|
||||
set_position=parse_enum(DPCode, definition.set_position_dp_code),
|
||||
device_class=parse_enum(CoverDeviceClass, definition.device_class),
|
||||
)
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: TuyaConfigEntry,
|
||||
@@ -157,7 +173,18 @@ async def async_setup_entry(
|
||||
entities: list[TuyaCoverEntity] = []
|
||||
for device_id in device_ids:
|
||||
device = manager.device_map[device_id]
|
||||
if descriptions := COVERS.get(device.category):
|
||||
if quirk := TUYA_QUIRKS_REGISTRY.get_quirk_for_device(device):
|
||||
entities.extend(
|
||||
TuyaCoverEntity(
|
||||
device, manager, _create_quirk_description(definition)
|
||||
)
|
||||
for definition in quirk.cover_definitions
|
||||
if (
|
||||
definition.key in device.function
|
||||
or definition.key in device.status_range
|
||||
)
|
||||
)
|
||||
elif descriptions := COVERS.get(device.category):
|
||||
entities.extend(
|
||||
TuyaCoverEntity(device, manager, description)
|
||||
for description in descriptions
|
||||
|
||||
@@ -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"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -8,45 +8,16 @@ import json
|
||||
import struct
|
||||
from typing import Self
|
||||
|
||||
from tuya_device_handlers.helpers import TuyaIntegerTypeDefinition
|
||||
|
||||
from .const import DPCode
|
||||
from .util import remap_value
|
||||
|
||||
|
||||
@dataclass
|
||||
class IntegerTypeData:
|
||||
class IntegerTypeData(TuyaIntegerTypeDefinition):
|
||||
"""Integer Type Data."""
|
||||
|
||||
dpcode: DPCode
|
||||
min: int
|
||||
max: int
|
||||
scale: float
|
||||
step: float
|
||||
unit: str | None = None
|
||||
type: str | None = None
|
||||
|
||||
@property
|
||||
def max_scaled(self) -> float:
|
||||
"""Return the max scaled."""
|
||||
return self.scale_value(self.max)
|
||||
|
||||
@property
|
||||
def min_scaled(self) -> float:
|
||||
"""Return the min scaled."""
|
||||
return self.scale_value(self.min)
|
||||
|
||||
@property
|
||||
def step_scaled(self) -> float:
|
||||
"""Return the step scaled."""
|
||||
return self.step / (10**self.scale)
|
||||
|
||||
def scale_value(self, value: float) -> float:
|
||||
"""Scale a value."""
|
||||
return value / (10**self.scale)
|
||||
|
||||
def scale_value_back(self, value: float) -> int:
|
||||
"""Return raw value for scaled."""
|
||||
return int(value * (10**self.scale))
|
||||
|
||||
def remap_value_to(
|
||||
self,
|
||||
value: float,
|
||||
|
||||
@@ -2,6 +2,9 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from tuya_device_handlers import TUYA_QUIRKS_REGISTRY
|
||||
from tuya_device_handlers.builder import TuyaSelectDefinition
|
||||
from tuya_device_handlers.helpers import parse_enum
|
||||
from tuya_sharing import CustomerDevice, Manager
|
||||
|
||||
from homeassistant.components.select import SelectEntity, SelectEntityDescription
|
||||
@@ -343,6 +346,16 @@ SELECTS[DeviceCategory.DGHSXJ] = SELECTS[DeviceCategory.SP]
|
||||
SELECTS[DeviceCategory.PC] = SELECTS[DeviceCategory.KG]
|
||||
|
||||
|
||||
def _create_quirk_description(
|
||||
definition: TuyaSelectDefinition,
|
||||
) -> SelectEntityDescription:
|
||||
return SelectEntityDescription(
|
||||
key=DPCode(definition.key),
|
||||
translation_key=definition.translation_key,
|
||||
entity_category=parse_enum(EntityCategory, definition.entity_category),
|
||||
)
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: TuyaConfigEntry,
|
||||
@@ -357,7 +370,15 @@ async def async_setup_entry(
|
||||
entities: list[TuyaSelectEntity] = []
|
||||
for device_id in device_ids:
|
||||
device = manager.device_map[device_id]
|
||||
if descriptions := SELECTS.get(device.category):
|
||||
if quirk := TUYA_QUIRKS_REGISTRY.get_quirk_for_device(device):
|
||||
entities.extend(
|
||||
TuyaSelectEntity(
|
||||
device, manager, _create_quirk_description(definition)
|
||||
)
|
||||
for definition in quirk.select_definitions
|
||||
if definition.key in device.status
|
||||
)
|
||||
elif descriptions := SELECTS.get(device.category):
|
||||
entities.extend(
|
||||
TuyaSelectEntity(device, manager, description)
|
||||
for description in descriptions
|
||||
|
||||
@@ -6,6 +6,9 @@ from collections.abc import Callable
|
||||
from dataclasses import dataclass
|
||||
from typing import Any
|
||||
|
||||
from tuya_device_handlers import TUYA_QUIRKS_REGISTRY
|
||||
from tuya_device_handlers.builder import TuyaSensorDefinition
|
||||
from tuya_device_handlers.helpers import parse_enum
|
||||
from tuya_sharing import CustomerDevice, Manager
|
||||
from tuya_sharing.device import DeviceStatusRange
|
||||
|
||||
@@ -1635,6 +1638,17 @@ SENSORS[DeviceCategory.DGHSXJ] = SENSORS[DeviceCategory.SP]
|
||||
SENSORS[DeviceCategory.PC] = SENSORS[DeviceCategory.KG]
|
||||
|
||||
|
||||
def _create_quirk_description(
|
||||
definition: TuyaSensorDefinition,
|
||||
) -> TuyaSensorEntityDescription:
|
||||
return TuyaSensorEntityDescription(
|
||||
key=DPCode(definition.key),
|
||||
translation_key=definition.translation_key,
|
||||
device_class=parse_enum(SensorDeviceClass, definition.device_class),
|
||||
entity_category=parse_enum(EntityCategory, definition.entity_category),
|
||||
)
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: TuyaConfigEntry,
|
||||
@@ -1649,7 +1663,15 @@ async def async_setup_entry(
|
||||
entities: list[TuyaSensorEntity] = []
|
||||
for device_id in device_ids:
|
||||
device = manager.device_map[device_id]
|
||||
if descriptions := SENSORS.get(device.category):
|
||||
if quirk := TUYA_QUIRKS_REGISTRY.get_quirk_for_device(device):
|
||||
entities.extend(
|
||||
TuyaSensorEntity(
|
||||
device, manager, _create_quirk_description(definition)
|
||||
)
|
||||
for definition in quirk.sensor_definitions
|
||||
if definition.key in device.status
|
||||
)
|
||||
elif descriptions := SENSORS.get(device.category):
|
||||
entities.extend(
|
||||
TuyaSensorEntity(device, manager, description)
|
||||
for description in descriptions
|
||||
|
||||
@@ -5,6 +5,9 @@ from __future__ import annotations
|
||||
from dataclasses import dataclass
|
||||
from typing import Any
|
||||
|
||||
from tuya_device_handlers import TUYA_QUIRKS_REGISTRY
|
||||
from tuya_device_handlers.builder import TuyaSwitchDefinition
|
||||
from tuya_device_handlers.helpers import parse_enum
|
||||
from tuya_sharing import CustomerDevice, Manager
|
||||
|
||||
from homeassistant.components.switch import (
|
||||
@@ -921,6 +924,17 @@ SWITCHES[DeviceCategory.CZ] = SWITCHES[DeviceCategory.PC]
|
||||
SWITCHES[DeviceCategory.DGHSXJ] = SWITCHES[DeviceCategory.SP]
|
||||
|
||||
|
||||
def _create_quirk_description(
|
||||
definition: TuyaSwitchDefinition,
|
||||
) -> SwitchEntityDescription:
|
||||
return SwitchEntityDescription(
|
||||
key=DPCode(definition.key),
|
||||
translation_key=definition.translation_key,
|
||||
device_class=parse_enum(SwitchDeviceClass, definition.device_class),
|
||||
entity_category=parse_enum(EntityCategory, definition.entity_category),
|
||||
)
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: TuyaConfigEntry,
|
||||
@@ -936,7 +950,15 @@ async def async_setup_entry(
|
||||
entities: list[TuyaSwitchEntity] = []
|
||||
for device_id in device_ids:
|
||||
device = manager.device_map[device_id]
|
||||
if descriptions := SWITCHES.get(device.category):
|
||||
if quirk := TUYA_QUIRKS_REGISTRY.get_quirk_for_device(device):
|
||||
entities.extend(
|
||||
TuyaSwitchEntity(
|
||||
device, manager, _create_quirk_description(definition)
|
||||
)
|
||||
for definition in quirk.switch_definitions
|
||||
if definition.key in device.status
|
||||
)
|
||||
elif descriptions := SWITCHES.get(device.category):
|
||||
entities.extend(
|
||||
TuyaSwitchEntity(device, manager, description)
|
||||
for description in descriptions
|
||||
|
||||
3
requirements_all.txt
generated
3
requirements_all.txt
generated
@@ -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
requirements_test_all.txt
generated
3
requirements_test_all.txt
generated
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
91
tests/components/tuya/test_quirks.py
Normal file
91
tests/components/tuya/test_quirks.py
Normal file
@@ -0,0 +1,91 @@
|
||||
"""Ensure quirks are correctly aligned with Home Assistant."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from enum import StrEnum
|
||||
|
||||
import pytest
|
||||
from tuya_device_handlers import TUYA_QUIRKS_REGISTRY
|
||||
from tuya_device_handlers.builder import TuyaDeviceQuirk
|
||||
from tuya_device_handlers.builder.base_quirk import BaseTuyaDefinition
|
||||
from tuya_device_handlers.devices import register_tuya_quirks
|
||||
from tuya_device_handlers.registry import QuirksRegistry
|
||||
|
||||
from homeassistant.components.cover import CoverDeviceClass
|
||||
from homeassistant.components.sensor import SensorDeviceClass
|
||||
from homeassistant.const import EntityCategory, Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import translation
|
||||
from homeassistant.util.json import load_json
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def filled_quirks_registry() -> QuirksRegistry:
|
||||
"""Mock an old config entry that can be migrated."""
|
||||
register_tuya_quirks()
|
||||
return TUYA_QUIRKS_REGISTRY
|
||||
|
||||
|
||||
_PLATFORM_DEVICE_CLASS: dict[Platform, type[StrEnum]] = {
|
||||
Platform.COVER: CoverDeviceClass,
|
||||
Platform.SENSOR: SensorDeviceClass,
|
||||
}
|
||||
|
||||
|
||||
def _validate_quirk_platform(
|
||||
quirk: TuyaDeviceQuirk, strings: dict[str, str], platform: str
|
||||
) -> None:
|
||||
"""Validate entity translations exist in strings.json."""
|
||||
definitions: list[BaseTuyaDefinition] = getattr(quirk, f"{platform}_definitions")
|
||||
for definition in definitions:
|
||||
# Validate entity translations exist in strings.json.
|
||||
if definition.translation_key:
|
||||
full_key = f"entity.{platform}.{definition.translation_key}.name"
|
||||
assert definition.translation_string == strings.get(full_key), (
|
||||
f"Incorrect or missing translation string for {full_key} in "
|
||||
"homeassistant/components/tuya/strings.json"
|
||||
)
|
||||
# Validate entity state translations exist in strings.json.
|
||||
if state_translations := definition.state_translations:
|
||||
for state, state_translation in state_translations.items():
|
||||
full_key = (
|
||||
f"entity.{platform}.{definition.translation_key}.state.{state}"
|
||||
)
|
||||
assert state_translation == strings.get(full_key), (
|
||||
f"Incorrect or missing translation string for {full_key} in "
|
||||
"homeassistant/components/tuya/strings.json"
|
||||
)
|
||||
# Validate device class is valid for platform.
|
||||
if definition.device_class is not None:
|
||||
device_class_enum = _PLATFORM_DEVICE_CLASS[platform]
|
||||
assert definition.device_class in device_class_enum.__members__.values(), (
|
||||
f"Invalid quirk device class {definition.device_class} for "
|
||||
f"{definition.key} {platform} in {quirk.quirk_file} {quirk.quirk_file_line}"
|
||||
)
|
||||
# Validate entity category is valid for platform.
|
||||
if definition.entity_category is not None:
|
||||
assert definition.entity_category in EntityCategory.__members__.values(), (
|
||||
f"Invalid quirk entity category {definition.entity_category} for "
|
||||
f"{definition.key} {platform} in {quirk.quirk_file} {quirk.quirk_file_line}"
|
||||
)
|
||||
|
||||
|
||||
async def test_quirks_validation(
|
||||
hass: HomeAssistant, filled_quirks_registry: QuirksRegistry
|
||||
) -> None:
|
||||
"""Test that quirks are valid.
|
||||
|
||||
- ensures that all translation strings exist in strings.json
|
||||
- ensures that all device classes are valid for the platform
|
||||
- ensures that all entity categories are valid for the platform
|
||||
"""
|
||||
json_strings = await hass.async_add_executor_job(
|
||||
load_json, "homeassistant/components/tuya/strings.json"
|
||||
)
|
||||
strings = translation.recursive_flatten("", json_strings)
|
||||
|
||||
for category_devices in filled_quirks_registry._quirks.values():
|
||||
for quirk in category_devices.values():
|
||||
_validate_quirk_platform(quirk, strings, Platform.COVER)
|
||||
_validate_quirk_platform(quirk, strings, Platform.SELECT)
|
||||
_validate_quirk_platform(quirk, strings, Platform.SENSOR)
|
||||
Reference in New Issue
Block a user