Compare commits

...

2 Commits

Author SHA1 Message Date
epenet
e118e4d7e4 Move Tuya helpers to external library 2026-01-23 08:37:50 +00:00
epenet
c2508d506f Add reference to external library 2026-01-23 08:28:12 +00:00
28 changed files with 247 additions and 1119 deletions

View File

@@ -5,6 +5,12 @@ from __future__ import annotations
from base64 import b64decode
from typing import Any
from tuya_device_handlers.device_wrapper.base import DeviceWrapper
from tuya_device_handlers.device_wrapper.common import (
DPCodeEnumWrapper,
DPCodeRawWrapper,
)
from tuya_device_handlers.type_information import EnumTypeInformation
from tuya_sharing import CustomerDevice, Manager
from homeassistant.components.alarm_control_panel import (
@@ -20,8 +26,6 @@ from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from . import TuyaConfigEntry
from .const import TUYA_DISCOVERY_NEW, DeviceCategory, DPCode
from .entity import TuyaEntity
from .models import DeviceWrapper, DPCodeEnumWrapper, DPCodeRawWrapper
from .type_information import EnumTypeInformation
ALARM: dict[DeviceCategory, tuple[AlarmControlPanelEntityDescription, ...]] = {
DeviceCategory.MAL: (
@@ -39,7 +43,7 @@ class _AlarmChangedByWrapper(DPCodeRawWrapper):
Decode base64 to utf-16be string, but only if alarm has been triggered.
"""
def read_device_status(self, device: CustomerDevice) -> str | None:
def read_device_status(self, device: CustomerDevice) -> str | None: # type: ignore[override]
"""Read the device status."""
if (
device.status.get(DPCode.MASTER_STATE) != "alarm"

View File

@@ -4,6 +4,12 @@ from __future__ import annotations
from dataclasses import dataclass
from tuya_device_handlers.device_wrapper.base import DeviceWrapper
from tuya_device_handlers.device_wrapper.binary_sensor import DPCodeBitmapBitWrapper
from tuya_device_handlers.device_wrapper.common import (
DPCodeBooleanWrapper,
DPCodeWrapper,
)
from tuya_sharing import CustomerDevice, Manager
from homeassistant.components.binary_sensor import (
@@ -19,12 +25,6 @@ from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from . import TuyaConfigEntry
from .const import TUYA_DISCOVERY_NEW, DeviceCategory, DPCode
from .entity import TuyaEntity
from .models import (
DeviceWrapper,
DPCodeBitmapBitWrapper,
DPCodeBooleanWrapper,
DPCodeWrapper,
)
@dataclass(frozen=True)

View File

@@ -2,6 +2,8 @@
from __future__ import annotations
from tuya_device_handlers.device_wrapper.base import DeviceWrapper
from tuya_device_handlers.device_wrapper.common import DPCodeBooleanWrapper
from tuya_sharing import CustomerDevice, Manager
from homeassistant.components.button import ButtonEntity, ButtonEntityDescription
@@ -13,7 +15,6 @@ from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from . import TuyaConfigEntry
from .const import TUYA_DISCOVERY_NEW, DeviceCategory, DPCode
from .entity import TuyaEntity
from .models import DeviceWrapper, DPCodeBooleanWrapper
BUTTONS: dict[DeviceCategory, tuple[ButtonEntityDescription, ...]] = {
DeviceCategory.HXD: (

View File

@@ -2,6 +2,8 @@
from __future__ import annotations
from tuya_device_handlers.device_wrapper.base import DeviceWrapper
from tuya_device_handlers.device_wrapper.common import DPCodeBooleanWrapper
from tuya_sharing import CustomerDevice, Manager
from homeassistant.components import ffmpeg
@@ -13,7 +15,6 @@ from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from . import TuyaConfigEntry
from .const import TUYA_DISCOVERY_NEW, DeviceCategory, DPCode
from .entity import TuyaEntity
from .models import DeviceWrapper, DPCodeBooleanWrapper
CAMERAS: tuple[DeviceCategory, ...] = (
DeviceCategory.DGHSXJ,

View File

@@ -6,6 +6,13 @@ import collections
from dataclasses import dataclass
from typing import Any, Self
from tuya_device_handlers.device_wrapper.base import DeviceWrapper
from tuya_device_handlers.device_wrapper.common import (
DPCodeBooleanWrapper,
DPCodeEnumWrapper,
DPCodeIntegerWrapper,
)
from tuya_device_handlers.type_information import EnumTypeInformation
from tuya_sharing import CustomerDevice, Manager
from homeassistant.components.climate import (
@@ -33,13 +40,6 @@ from .const import (
DPCode,
)
from .entity import TuyaEntity
from .models import (
DeviceWrapper,
DPCodeBooleanWrapper,
DPCodeEnumWrapper,
DPCodeIntegerWrapper,
)
from .type_information import EnumTypeInformation
TUYA_HVAC_TO_HA = {
"auto": HVACMode.HEAT_COOL,
@@ -176,8 +176,10 @@ class _HvacModeWrapper(DPCodeEnumWrapper):
return None
return TUYA_HVAC_TO_HA[raw]
def _convert_value_to_raw_value(
self, device: CustomerDevice, value: HVACMode
def _convert_value_to_raw_value( # type: ignore[override]
self,
device: CustomerDevice,
value: HVACMode,
) -> Any:
"""Convert value to raw value."""
return next(

View File

@@ -82,18 +82,6 @@ class WorkMode(StrEnum):
WHITE = "white"
class DPType(StrEnum):
"""Data point types."""
BITMAP = "Bitmap"
BOOLEAN = "Boolean"
ENUM = "Enum"
INTEGER = "Integer"
JSON = "Json"
RAW = "Raw"
STRING = "String"
class DeviceCategory(StrEnum):
"""Tuya device categories.

View File

@@ -5,6 +5,17 @@ from __future__ import annotations
from dataclasses import dataclass
from typing import Any
from tuya_device_handlers.device_wrapper.base import DeviceWrapper
from tuya_device_handlers.device_wrapper.common import (
DPCodeBooleanWrapper,
DPCodeEnumWrapper,
DPCodeIntegerWrapper,
)
from tuya_device_handlers.type_information import (
EnumTypeInformation,
IntegerTypeInformation,
)
from tuya_device_handlers.utils import RemapHelper
from tuya_sharing import CustomerDevice, Manager
from homeassistant.components.cover import (
@@ -22,14 +33,6 @@ from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from . import TuyaConfigEntry
from .const import TUYA_DISCOVERY_NEW, DeviceCategory, DPCode
from .entity import TuyaEntity
from .models import (
DeviceWrapper,
DPCodeBooleanWrapper,
DPCodeEnumWrapper,
DPCodeIntegerWrapper,
)
from .type_information import EnumTypeInformation, IntegerTypeInformation
from .util import RemapHelper
class _DPCodePercentageMappingWrapper(DPCodeIntegerWrapper):
@@ -84,7 +87,7 @@ class _InstructionBooleanWrapper(DPCodeBooleanWrapper):
options = ["open", "close"]
_ACTION_MAPPINGS = {"open": True, "close": False}
def _convert_value_to_raw_value(self, device: CustomerDevice, value: str) -> bool:
def _convert_value_to_raw_value(self, device: CustomerDevice, value: str) -> bool: # type: ignore[override]
return self._ACTION_MAPPINGS[value]
@@ -130,7 +133,7 @@ class _IsClosedEnumWrapper(DPCodeEnumWrapper):
"fully_open": False,
}
def read_device_status(self, device: CustomerDevice) -> bool | None:
def read_device_status(self, device: CustomerDevice) -> bool | None: # type: ignore[override]
if (value := super().read_device_status(device)) is None:
return None
return self._MAPPINGS.get(value)

View File

@@ -4,6 +4,7 @@ from __future__ import annotations
from typing import Any
from tuya_device_handlers.device_wrapper import DEVICE_WARNINGS
from tuya_sharing import CustomerDevice
from homeassistant.components.diagnostics import REDACTED
@@ -14,7 +15,6 @@ from homeassistant.util import dt as dt_util
from . import TuyaConfigEntry
from .const import DOMAIN, DPCode
from .type_information import DEVICE_WARNINGS
_REDACTED_DPCODES = {
DPCode.ALARM_MESSAGE,

View File

@@ -4,6 +4,7 @@ from __future__ import annotations
from typing import Any
from tuya_device_handlers.device_wrapper import DeviceWrapper
from tuya_sharing import CustomerDevice, Manager
from homeassistant.helpers.device_registry import DeviceInfo
@@ -11,7 +12,6 @@ from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entity import Entity
from .const import DOMAIN, LOGGER, TUYA_HA_SIGNAL_UPDATE_ENTITY
from .models import DeviceWrapper
class TuyaEntity(Entity):

View File

@@ -6,6 +6,13 @@ from base64 import b64decode
from dataclasses import dataclass
from typing import Any
from tuya_device_handlers.device_wrapper.base import DeviceWrapper
from tuya_device_handlers.device_wrapper.common import (
DPCodeEnumWrapper,
DPCodeRawWrapper,
DPCodeStringWrapper,
DPCodeTypeInformationWrapper,
)
from tuya_sharing import CustomerDevice, Manager
from homeassistant.components.event import (
@@ -20,19 +27,14 @@ from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from . import TuyaConfigEntry
from .const import TUYA_DISCOVERY_NEW, DeviceCategory, DPCode
from .entity import TuyaEntity
from .models import (
DeviceWrapper,
DPCodeEnumWrapper,
DPCodeRawWrapper,
DPCodeStringWrapper,
DPCodeTypeInformationWrapper,
)
class _EventEnumWrapper(DPCodeEnumWrapper):
"""Wrapper for event enum DP codes."""
def read_device_status(self, device: CustomerDevice) -> tuple[str, None] | None:
def read_device_status( # type: ignore[override]
self, device: CustomerDevice
) -> tuple[str, None] | None:
"""Return the event details."""
if (raw_value := super().read_device_status(device)) is None:
return None
@@ -67,7 +69,7 @@ class _DoorbellPicWrapper(DPCodeRawWrapper):
super().__init__(dpcode, type_information)
self.options = ["triggered"]
def read_device_status(
def read_device_status( # type: ignore[override]
self, device: CustomerDevice
) -> tuple[str, dict[str, Any]] | None:
"""Return the event attributes for the doorbell picture."""

View File

@@ -4,6 +4,14 @@ from __future__ import annotations
from typing import Any
from tuya_device_handlers.device_wrapper.base import DeviceWrapper
from tuya_device_handlers.device_wrapper.common import (
DPCodeBooleanWrapper,
DPCodeEnumWrapper,
DPCodeIntegerWrapper,
)
from tuya_device_handlers.type_information import IntegerTypeInformation
from tuya_device_handlers.utils import RemapHelper
from tuya_sharing import CustomerDevice, Manager
from homeassistant.components.fan import (
@@ -23,14 +31,7 @@ from homeassistant.util.percentage import (
from . import TuyaConfigEntry
from .const import TUYA_DISCOVERY_NEW, DeviceCategory, DPCode
from .entity import TuyaEntity
from .models import (
DeviceWrapper,
DPCodeBooleanWrapper,
DPCodeEnumWrapper,
DPCodeIntegerWrapper,
)
from .type_information import IntegerTypeInformation
from .util import RemapHelper, get_dpcode
from .util import get_dpcode
_DIRECTION_DPCODES = (DPCode.FAN_DIRECTION,)
_MODE_DPCODES = (DPCode.FAN_MODE, DPCode.MODE)
@@ -82,7 +83,7 @@ def _has_a_valid_dpcode(device: CustomerDevice) -> bool:
class _FanSpeedEnumWrapper(DPCodeEnumWrapper):
"""Wrapper for fan speed DP code (from an enum)."""
def read_device_status(self, device: CustomerDevice) -> int | None:
def read_device_status(self, device: CustomerDevice) -> int | None: # type: ignore[override]
"""Get the current speed as a percentage."""
if (value := super().read_device_status(device)) is None:
return None

View File

@@ -5,6 +5,12 @@ from __future__ import annotations
from dataclasses import dataclass
from typing import Any
from tuya_device_handlers.device_wrapper.base import DeviceWrapper
from tuya_device_handlers.device_wrapper.common import (
DPCodeBooleanWrapper,
DPCodeEnumWrapper,
DPCodeIntegerWrapper,
)
from tuya_sharing import CustomerDevice, Manager
from homeassistant.components.humidifier import (
@@ -20,12 +26,6 @@ from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from . import TuyaConfigEntry
from .const import TUYA_DISCOVERY_NEW, DeviceCategory, DPCode
from .entity import TuyaEntity
from .models import (
DeviceWrapper,
DPCodeBooleanWrapper,
DPCodeEnumWrapper,
DPCodeIntegerWrapper,
)
from .util import ActionDPCodeNotFoundError, get_dpcode

View File

@@ -7,6 +7,15 @@ from enum import StrEnum
import json
from typing import Any, cast
from tuya_device_handlers.device_wrapper.base import DeviceWrapper
from tuya_device_handlers.device_wrapper.common import (
DPCodeBooleanWrapper,
DPCodeEnumWrapper,
DPCodeIntegerWrapper,
DPCodeJsonWrapper,
)
from tuya_device_handlers.type_information import IntegerTypeInformation
from tuya_device_handlers.utils import RemapHelper
from tuya_sharing import CustomerDevice, Manager
from homeassistant.components.light import (
@@ -30,15 +39,6 @@ from homeassistant.util.json import json_loads_object
from . import TuyaConfigEntry
from .const import TUYA_DISCOVERY_NEW, DeviceCategory, DPCode, WorkMode
from .entity import TuyaEntity
from .models import (
DeviceWrapper,
DPCodeBooleanWrapper,
DPCodeEnumWrapper,
DPCodeIntegerWrapper,
DPCodeJsonWrapper,
)
from .type_information import IntegerTypeInformation
from .util import RemapHelper
class _BrightnessWrapper(DPCodeIntegerWrapper):
@@ -174,7 +174,7 @@ class _ColorDataWrapper(DPCodeJsonWrapper):
s_type = DEFAULT_S_TYPE
v_type = DEFAULT_V_TYPE
def read_device_status(
def read_device_status( # type: ignore[override]
self, device: CustomerDevice
) -> tuple[float, float, float] | None:
"""Return a tuple (H, S, V) from this color data."""

View File

@@ -43,5 +43,8 @@
"integration_type": "hub",
"iot_class": "cloud_push",
"loggers": ["tuya_sharing"],
"requirements": ["tuya-device-sharing-sdk==0.2.8"]
"requirements": [
"tuya-device-handlers==0.0.10",
"tuya-device-sharing-sdk==0.2.8"
]
}

View File

@@ -1,320 +0,0 @@
"""Tuya Home Assistant Base Device Model."""
from __future__ import annotations
import logging
from typing import Any, Self
from tuya_sharing import CustomerDevice
from homeassistant.components.sensor import SensorStateClass
from .type_information import (
BitmapTypeInformation,
BooleanTypeInformation,
EnumTypeInformation,
IntegerTypeInformation,
JsonTypeInformation,
RawTypeInformation,
StringTypeInformation,
TypeInformation,
)
_LOGGER = logging.getLogger(__name__)
class DeviceWrapper[T]:
"""Base device wrapper."""
native_unit: str | None = None
suggested_unit: str | None = None
state_class: SensorStateClass | None = None
max_value: float
min_value: float
value_step: float
options: list[str]
def initialize(self, device: CustomerDevice) -> None:
"""Initialize the wrapper with device data.
Called when the entity is added to Home Assistant.
Override in subclasses to perform initialization logic.
"""
def skip_update(
self,
device: CustomerDevice,
updated_status_properties: list[str] | None,
dp_timestamps: dict[str, int] | None,
) -> bool:
"""Determine if the wrapper should skip an update.
The default is to always skip, unless overridden in subclasses.
"""
return True
def read_device_status(self, device: CustomerDevice) -> T | None:
"""Read device status and convert to a Home Assistant value."""
raise NotImplementedError
def get_update_commands(
self, device: CustomerDevice, value: T
) -> list[dict[str, Any]]:
"""Generate update commands for a Home Assistant action."""
raise NotImplementedError
class DPCodeWrapper(DeviceWrapper):
"""Base device wrapper for a single DPCode.
Used as a common interface for referring to a DPCode, and
access read conversion routines.
"""
def __init__(self, dpcode: str) -> None:
"""Init DPCodeWrapper."""
self.dpcode = dpcode
def skip_update(
self,
device: CustomerDevice,
updated_status_properties: list[str] | None,
dp_timestamps: dict[str, int] | None,
) -> bool:
"""Determine if the wrapper should skip an update.
By default, skip if updated_status_properties is given and
does not include this dpcode.
"""
return (
updated_status_properties is None
or self.dpcode not in updated_status_properties
)
def _convert_value_to_raw_value(self, device: CustomerDevice, value: Any) -> Any:
"""Convert a Home Assistant value back to a raw device value.
This is called by `get_update_commands` to prepare the value for sending
back to the device, and should be implemented in concrete classes if needed.
"""
raise NotImplementedError
def get_update_commands(
self, device: CustomerDevice, value: Any
) -> list[dict[str, Any]]:
"""Get the update commands for the dpcode.
The Home Assistant value is converted back to a raw device value.
"""
return [
{
"code": self.dpcode,
"value": self._convert_value_to_raw_value(device, value),
}
]
class DPCodeTypeInformationWrapper[T: TypeInformation](DPCodeWrapper):
"""Base DPCode wrapper with Type Information."""
_DPTYPE: type[T]
type_information: T
def __init__(self, dpcode: str, type_information: T) -> None:
"""Init DPCodeWrapper."""
super().__init__(dpcode)
self.type_information = type_information
def read_device_status(self, device: CustomerDevice) -> Any | None:
"""Read the device value for the dpcode."""
return self.type_information.process_raw_value(
device.status.get(self.dpcode), device
)
@classmethod
def find_dpcode(
cls,
device: CustomerDevice,
dpcodes: str | tuple[str, ...] | None,
*,
prefer_function: bool = False,
) -> Self | None:
"""Find and return a DPCodeTypeInformationWrapper for the given DP codes."""
if type_information := cls._DPTYPE.find_dpcode(
device, dpcodes, prefer_function=prefer_function
):
return cls(
dpcode=type_information.dpcode, type_information=type_information
)
return None
class DPCodeBooleanWrapper(DPCodeTypeInformationWrapper[BooleanTypeInformation]):
"""Simple wrapper for boolean values.
Supports True/False only.
"""
_DPTYPE = BooleanTypeInformation
def _convert_value_to_raw_value(
self, device: CustomerDevice, value: Any
) -> Any | None:
"""Convert a Home Assistant value back to a raw device value."""
if value in (True, False):
return value
# Currently only called with boolean values
# Safety net in case of future changes
raise ValueError(f"Invalid boolean value `{value}`")
class DPCodeJsonWrapper(DPCodeTypeInformationWrapper[JsonTypeInformation]):
"""Wrapper to extract information from a JSON value."""
_DPTYPE = JsonTypeInformation
class DPCodeEnumWrapper(DPCodeTypeInformationWrapper[EnumTypeInformation]):
"""Simple wrapper for EnumTypeInformation values."""
_DPTYPE = EnumTypeInformation
def __init__(self, dpcode: str, type_information: EnumTypeInformation) -> None:
"""Init DPCodeEnumWrapper."""
super().__init__(dpcode, type_information)
self.options = type_information.range
def _convert_value_to_raw_value(self, device: CustomerDevice, value: Any) -> Any:
"""Convert a Home Assistant value back to a raw device value."""
if value in self.type_information.range:
return value
# Guarded by select option validation
# Safety net in case of future changes
raise ValueError(
f"Enum value `{value}` out of range: {self.type_information.range}"
)
class DPCodeIntegerWrapper(DPCodeTypeInformationWrapper[IntegerTypeInformation]):
"""Simple wrapper for IntegerTypeInformation values."""
_DPTYPE = IntegerTypeInformation
def __init__(self, dpcode: str, type_information: IntegerTypeInformation) -> None:
"""Init DPCodeIntegerWrapper."""
super().__init__(dpcode, type_information)
self.native_unit = type_information.unit
self.min_value = self.type_information.scale_value(type_information.min)
self.max_value = self.type_information.scale_value(type_information.max)
self.value_step = self.type_information.scale_value(type_information.step)
def _convert_value_to_raw_value(self, device: CustomerDevice, value: Any) -> Any:
"""Convert a Home Assistant value back to a raw device value."""
new_value = round(value * (10**self.type_information.scale))
if self.type_information.min <= new_value <= self.type_information.max:
return new_value
# Guarded by number validation
# Safety net in case of future changes
raise ValueError(
f"Value `{new_value}` (converted from `{value}`) out of range:"
f" ({self.type_information.min}-{self.type_information.max})"
)
class DPCodeDeltaIntegerWrapper(DPCodeIntegerWrapper):
"""Wrapper for integer values with delta report accumulation.
This wrapper handles sensors that report incremental (delta) values
instead of cumulative totals. It accumulates the delta values locally
to provide a running total.
"""
_accumulated_value: float = 0
_last_dp_timestamp: int | None = None
def __init__(self, dpcode: str, type_information: IntegerTypeInformation) -> None:
"""Init DPCodeDeltaIntegerWrapper."""
super().__init__(dpcode, type_information)
# Delta reports use TOTAL_INCREASING state class
self.state_class = SensorStateClass.TOTAL_INCREASING
def skip_update(
self,
device: CustomerDevice,
updated_status_properties: list[str] | None,
dp_timestamps: dict[str, int] | None,
) -> bool:
"""Override skip_update to process delta updates.
Processes delta accumulation before determining if update should be skipped.
"""
if (
super().skip_update(device, updated_status_properties, dp_timestamps)
or dp_timestamps is None
or (current_timestamp := dp_timestamps.get(self.dpcode)) is None
or current_timestamp == self._last_dp_timestamp
or (raw_value := super().read_device_status(device)) is None
):
return True
delta = float(raw_value)
self._accumulated_value += delta
_LOGGER.debug(
"Delta update for %s: +%s, total: %s",
self.dpcode,
delta,
self._accumulated_value,
)
self._last_dp_timestamp = current_timestamp
return False
def read_device_status(self, device: CustomerDevice) -> float | None:
"""Read device status, returning accumulated value for delta reports."""
return self._accumulated_value
class DPCodeRawWrapper(DPCodeTypeInformationWrapper[RawTypeInformation]):
"""Wrapper to extract information from a RAW/binary value."""
_DPTYPE = RawTypeInformation
class DPCodeStringWrapper(DPCodeTypeInformationWrapper[StringTypeInformation]):
"""Wrapper to extract information from a STRING value."""
_DPTYPE = StringTypeInformation
class DPCodeBitmapBitWrapper(DPCodeWrapper):
"""Simple wrapper for a specific bit in bitmap values."""
def __init__(self, dpcode: str, mask: int) -> None:
"""Init DPCodeBitmapWrapper."""
super().__init__(dpcode)
self._mask = mask
def read_device_status(self, device: CustomerDevice) -> bool | None:
"""Read the device value for the dpcode."""
if (raw_value := device.status.get(self.dpcode)) is None:
return None
return (raw_value & (1 << self._mask)) != 0
@classmethod
def find_dpcode(
cls,
device: CustomerDevice,
dpcodes: str | tuple[str, ...],
*,
bitmap_key: str,
) -> Self | None:
"""Find and return a DPCodeBitmapBitWrapper for the given DP codes."""
if (
type_information := BitmapTypeInformation.find_dpcode(device, dpcodes)
) and bitmap_key in type_information.label:
return cls(
type_information.dpcode, type_information.label.index(bitmap_key)
)
return None

View File

@@ -2,6 +2,8 @@
from __future__ import annotations
from tuya_device_handlers.device_wrapper.base import DeviceWrapper
from tuya_device_handlers.device_wrapper.common import DPCodeIntegerWrapper
from tuya_sharing import CustomerDevice, Manager
from homeassistant.components.number import (
@@ -25,7 +27,6 @@ from .const import (
DPCode,
)
from .entity import TuyaEntity
from .models import DeviceWrapper, DPCodeIntegerWrapper
NUMBERS: dict[DeviceCategory, tuple[NumberEntityDescription, ...]] = {
DeviceCategory.BH: (

View File

@@ -1,60 +0,0 @@
"""Parsers for RAW (base64-encoded bytes) values."""
from dataclasses import dataclass
import struct
from typing import Self
@dataclass(kw_only=True)
class ElectricityData:
"""Electricity RAW value."""
current: float
power: float
voltage: float
@classmethod
def from_bytes(cls, raw: bytes) -> Self | None:
"""Parse bytes and return an ElectricityValue object."""
# Format:
# - legacy: 8 bytes
# - v01: [ver=0x01][len=0x0F][data(15 bytes)]
# - v02: [ver=0x02][len=0x0F][data(15 bytes)][sign_bitmap(1 byte)]
# Data layout (big-endian):
# - voltage: 2B, unit 0.1 V
# - current: 3B, unit 0.001 A (i.e., mA)
# - active power: 3B, unit 0.001 kW (i.e., W)
# - reactive power: 3B, unit 0.001 kVar
# - apparent power: 3B, unit 0.001 kVA
# - power factor: 1B, unit 0.01
# Sign bitmap (v02 only, 1 bit means negative):
# - bit0 current
# - bit1 active power
# - bit2 reactive
# - bit3 power factor
is_v1 = len(raw) == 17 and raw[0:2] == b"\x01\x0f"
is_v2 = len(raw) == 18 and raw[0:2] == b"\x02\x0f"
if is_v1 or is_v2:
data = raw[2:17]
voltage = struct.unpack(">H", data[0:2])[0] / 10.0
current = struct.unpack(">L", b"\x00" + data[2:5])[0]
power = struct.unpack(">L", b"\x00" + data[5:8])[0]
if is_v2:
sign_bitmap = raw[17]
if sign_bitmap & 0x01:
current = -current
if sign_bitmap & 0x02:
power = -power
return cls(current=current, power=power, voltage=voltage)
if len(raw) >= 8:
voltage = struct.unpack(">H", raw[0:2])[0] / 10.0
current = struct.unpack(">L", b"\x00" + raw[2:5])[0]
power = struct.unpack(">L", b"\x00" + raw[5:8])[0]
return cls(current=current, power=power, voltage=voltage)
return None

View File

@@ -2,6 +2,8 @@
from __future__ import annotations
from tuya_device_handlers.device_wrapper.base import DeviceWrapper
from tuya_device_handlers.device_wrapper.common import DPCodeEnumWrapper
from tuya_sharing import CustomerDevice, Manager
from homeassistant.components.select import SelectEntity, SelectEntityDescription
@@ -13,7 +15,6 @@ from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from . import TuyaConfigEntry
from .const import TUYA_DISCOVERY_NEW, DeviceCategory, DPCode
from .entity import TuyaEntity
from .models import DeviceWrapper, DPCodeEnumWrapper
# All descriptions can be found here. Mostly the Enum data types in the
# default instructions set of each category end up being a select.

View File

@@ -4,6 +4,24 @@ from __future__ import annotations
from dataclasses import dataclass
from tuya_device_handlers.device_wrapper.base import DeviceWrapper
from tuya_device_handlers.device_wrapper.common import (
DPCodeEnumWrapper,
DPCodeIntegerWrapper,
DPCodeTypeInformationWrapper,
DPCodeWrapper,
)
from tuya_device_handlers.device_wrapper.sensor import (
DeltaIntegerWrapper,
ElectricityCurrentJsonWrapper,
ElectricityCurrentRawWrapper,
ElectricityPowerJsonWrapper,
ElectricityPowerRawWrapper,
ElectricityVoltageJsonWrapper,
ElectricityVoltageRawWrapper,
WindDirectionEnumWrapper,
)
from tuya_device_handlers.type_information import IntegerTypeInformation
from tuya_sharing import CustomerDevice, Manager
from homeassistant.components.sensor import (
@@ -38,138 +56,10 @@ from .const import (
DPCode,
)
from .entity import TuyaEntity
from .models import (
DeviceWrapper,
DPCodeDeltaIntegerWrapper,
DPCodeEnumWrapper,
DPCodeIntegerWrapper,
DPCodeJsonWrapper,
DPCodeRawWrapper,
DPCodeTypeInformationWrapper,
DPCodeWrapper,
)
from .raw_data_models import ElectricityData
from .type_information import EnumTypeInformation, IntegerTypeInformation
class _WindDirectionWrapper(DPCodeTypeInformationWrapper[EnumTypeInformation]):
"""Custom DPCode Wrapper for converting enum to wind direction."""
_DPTYPE = EnumTypeInformation
_WIND_DIRECTIONS = {
"north": 0.0,
"north_north_east": 22.5,
"north_east": 45.0,
"east_north_east": 67.5,
"east": 90.0,
"east_south_east": 112.5,
"south_east": 135.0,
"south_south_east": 157.5,
"south": 180.0,
"south_south_west": 202.5,
"south_west": 225.0,
"west_south_west": 247.5,
"west": 270.0,
"west_north_west": 292.5,
"north_west": 315.0,
"north_north_west": 337.5,
}
def read_device_status(self, device: CustomerDevice) -> float | None:
"""Read the device value for the dpcode."""
if (raw_value := device.status.get(self.dpcode)) in self.type_information.range:
return self._WIND_DIRECTIONS.get(raw_value)
return None
class _JsonElectricityCurrentWrapper(DPCodeJsonWrapper):
"""Custom DPCode Wrapper for extracting electricity current from JSON."""
native_unit = UnitOfElectricCurrent.AMPERE
def read_device_status(self, device: CustomerDevice) -> float | None:
"""Read the device value for the dpcode."""
if (status := super().read_device_status(device)) is None:
return None
return status.get("electricCurrent")
class _JsonElectricityPowerWrapper(DPCodeJsonWrapper):
"""Custom DPCode Wrapper for extracting electricity power from JSON."""
native_unit = UnitOfPower.KILO_WATT
def read_device_status(self, device: CustomerDevice) -> float | None:
"""Read the device value for the dpcode."""
if (status := super().read_device_status(device)) is None:
return None
return status.get("power")
class _JsonElectricityVoltageWrapper(DPCodeJsonWrapper):
"""Custom DPCode Wrapper for extracting electricity voltage from JSON."""
native_unit = UnitOfElectricPotential.VOLT
def read_device_status(self, device: CustomerDevice) -> float | None:
"""Read the device value for the dpcode."""
if (status := super().read_device_status(device)) is None:
return None
return status.get("voltage")
class _RawElectricityDataWrapper(DPCodeRawWrapper):
"""Custom DPCode Wrapper for extracting ElectricityData from base64."""
def _convert(self, value: ElectricityData) -> float:
"""Extract specific value from T."""
raise NotImplementedError
def read_device_status(self, device: CustomerDevice) -> float | None:
"""Read the device value for the dpcode."""
if (raw_value := super().read_device_status(device)) is None or (
value := ElectricityData.from_bytes(raw_value)
) is None:
return None
return self._convert(value)
class _RawElectricityCurrentWrapper(_RawElectricityDataWrapper):
"""Custom DPCode Wrapper for extracting electricity current from base64."""
native_unit = UnitOfElectricCurrent.MILLIAMPERE
suggested_unit = UnitOfElectricCurrent.AMPERE
def _convert(self, value: ElectricityData) -> float:
"""Extract specific value from ElectricityData."""
return value.current
class _RawElectricityPowerWrapper(_RawElectricityDataWrapper):
"""Custom DPCode Wrapper for extracting electricity power from base64."""
native_unit = UnitOfPower.WATT
suggested_unit = UnitOfPower.KILO_WATT
def _convert(self, value: ElectricityData) -> float:
"""Extract specific value from ElectricityData."""
return value.power
class _RawElectricityVoltageWrapper(_RawElectricityDataWrapper):
"""Custom DPCode Wrapper for extracting electricity voltage from base64."""
native_unit = UnitOfElectricPotential.VOLT
def _convert(self, value: ElectricityData) -> float:
"""Extract specific value from ElectricityData."""
return value.voltage
CURRENT_WRAPPER = (_RawElectricityCurrentWrapper, _JsonElectricityCurrentWrapper)
POWER_WRAPPER = (_RawElectricityPowerWrapper, _JsonElectricityPowerWrapper)
VOLTAGE_WRAPPER = (_RawElectricityVoltageWrapper, _JsonElectricityVoltageWrapper)
CURRENT_WRAPPER = (ElectricityCurrentRawWrapper, ElectricityCurrentJsonWrapper)
POWER_WRAPPER = (ElectricityPowerRawWrapper, ElectricityPowerJsonWrapper)
VOLTAGE_WRAPPER = (ElectricityVoltageRawWrapper, ElectricityVoltageJsonWrapper)
@dataclass(frozen=True)
@@ -1069,7 +959,7 @@ SENSORS: dict[DeviceCategory, tuple[TuyaSensorEntityDescription, ...]] = {
translation_key="wind_direction",
device_class=SensorDeviceClass.WIND_DIRECTION,
state_class=SensorStateClass.MEASUREMENT,
wrapper_class=(_WindDirectionWrapper,),
wrapper_class=(WindDirectionEnumWrapper,),
),
TuyaSensorEntityDescription(
key=DPCode.DEW_POINT_TEMP,
@@ -1743,7 +1633,7 @@ def _get_dpcode_wrapper(
# Check for integer type first, using delta wrapper only for sum report_type
if type_information := IntegerTypeInformation.find_dpcode(device, dpcode):
if type_information.report_type == "sum":
return DPCodeDeltaIntegerWrapper(type_information.dpcode, type_information)
return DeltaIntegerWrapper(type_information.dpcode, type_information)
return DPCodeIntegerWrapper(type_information.dpcode, type_information)
return DPCodeEnumWrapper.find_dpcode(device, dpcode)

View File

@@ -4,6 +4,8 @@ from __future__ import annotations
from typing import Any
from tuya_device_handlers.device_wrapper.base import DeviceWrapper
from tuya_device_handlers.device_wrapper.common import DPCodeBooleanWrapper
from tuya_sharing import CustomerDevice, Manager
from homeassistant.components.siren import (
@@ -19,7 +21,6 @@ from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from . import TuyaConfigEntry
from .const import TUYA_DISCOVERY_NEW, DeviceCategory, DPCode
from .entity import TuyaEntity
from .models import DeviceWrapper, DPCodeBooleanWrapper
SIRENS: dict[DeviceCategory, tuple[SirenEntityDescription, ...]] = {
DeviceCategory.CO2BJ: (

View File

@@ -5,6 +5,8 @@ from __future__ import annotations
from dataclasses import dataclass
from typing import Any
from tuya_device_handlers.device_wrapper.base import DeviceWrapper
from tuya_device_handlers.device_wrapper.common import DPCodeBooleanWrapper
from tuya_sharing import CustomerDevice, Manager
from homeassistant.components.switch import (
@@ -27,7 +29,6 @@ from homeassistant.helpers.issue_registry import (
from . import TuyaConfigEntry
from .const import DOMAIN, TUYA_DISCOVERY_NEW, DeviceCategory, DPCode
from .entity import TuyaEntity
from .models import DeviceWrapper, DPCodeBooleanWrapper
@dataclass(frozen=True, kw_only=True)

View File

@@ -1,302 +0,0 @@
"""Type information classes for the Tuya integration."""
from __future__ import annotations
import base64
from dataclasses import dataclass
from typing import Any, ClassVar, Self, cast
from tuya_sharing import CustomerDevice
from homeassistant.util.json import json_loads_object
from .const import LOGGER, DPType
from .util import parse_dptype
# Dictionary to track logged warnings to avoid spamming logs
# Keyed by device ID
DEVICE_WARNINGS: dict[str, set[str]] = {}
def _should_log_warning(device_id: str, warning_key: str) -> bool:
"""Check if a warning has already been logged for a device and add it if not.
Returns: True if the warning should be logged, False if it was already logged.
"""
if (device_warnings := DEVICE_WARNINGS.get(device_id)) is None:
device_warnings = set()
DEVICE_WARNINGS[device_id] = device_warnings
if warning_key in device_warnings:
return False
DEVICE_WARNINGS[device_id].add(warning_key)
return True
@dataclass(kw_only=True)
class TypeInformation[T]:
"""Type information.
As provided by the SDK, from `device.function` / `device.status_range`.
"""
_DPTYPE: ClassVar[DPType]
dpcode: str
type_data: str
def process_raw_value(
self, raw_value: Any | None, device: CustomerDevice
) -> T | None:
"""Read and process raw value against this type information.
Base implementation does no validation, subclasses may override to provide
specific validation.
"""
return raw_value
@classmethod
def _from_json(
cls, dpcode: str, type_data: str, *, report_type: str | None
) -> Self | None:
"""Load JSON string and return a TypeInformation object."""
return cls(dpcode=dpcode, type_data=type_data)
@classmethod
def find_dpcode(
cls,
device: CustomerDevice,
dpcodes: str | tuple[str, ...] | None,
*,
prefer_function: bool = False,
) -> Self | None:
"""Find type information for a matching DP code available for this device."""
if dpcodes is None:
return None
if not isinstance(dpcodes, tuple):
dpcodes = (dpcodes,)
lookup_tuple = (
(device.function, device.status_range)
if prefer_function
else (device.status_range, device.function)
)
for dpcode in dpcodes:
report_type = (
sr.report_type if (sr := device.status_range.get(dpcode)) else None
)
for device_specs in lookup_tuple:
if (
(current_definition := device_specs.get(dpcode))
and parse_dptype(current_definition.type) is cls._DPTYPE
and (
type_information := cls._from_json(
dpcode=dpcode,
type_data=current_definition.values,
report_type=report_type,
)
)
):
return type_information
return None
@dataclass(kw_only=True)
class BitmapTypeInformation(TypeInformation[int]):
"""Bitmap type information."""
_DPTYPE = DPType.BITMAP
label: list[str]
@classmethod
def _from_json(
cls, dpcode: str, type_data: str, *, report_type: str | None
) -> Self | None:
"""Load JSON string and return a BitmapTypeInformation object."""
if not (parsed := cast(dict[str, Any] | None, json_loads_object(type_data))):
return None
return cls(
dpcode=dpcode,
type_data=type_data,
label=parsed["label"],
)
@dataclass(kw_only=True)
class BooleanTypeInformation(TypeInformation[bool]):
"""Boolean type information."""
_DPTYPE = DPType.BOOLEAN
def process_raw_value(
self, raw_value: Any | None, device: CustomerDevice
) -> bool | None:
"""Read and process raw value against this type information."""
if raw_value is None:
return None
# Validate input against defined range
if raw_value not in (True, False):
if _should_log_warning(
device.id, f"boolean_out_range|{self.dpcode}|{raw_value}"
):
LOGGER.warning(
"Found invalid boolean value `%s` for datapoint `%s` in product "
"id `%s`, expected one of `%s`; please report this defect to "
"Tuya support",
raw_value,
self.dpcode,
device.product_id,
(True, False),
)
return None
return raw_value
@dataclass(kw_only=True)
class EnumTypeInformation(TypeInformation[str]):
"""Enum type information."""
_DPTYPE = DPType.ENUM
range: list[str]
def process_raw_value(
self, raw_value: Any | None, device: CustomerDevice
) -> str | None:
"""Read and process raw value against this type information."""
if raw_value is None:
return None
# Validate input against defined range
if raw_value not in self.range:
if _should_log_warning(
device.id, f"enum_out_range|{self.dpcode}|{raw_value}"
):
LOGGER.warning(
"Found invalid enum value `%s` for datapoint `%s` in product "
"id `%s`, expected one of `%s`; please report this defect to "
"Tuya support",
raw_value,
self.dpcode,
device.product_id,
self.range,
)
return None
return raw_value
@classmethod
def _from_json(
cls, dpcode: str, type_data: str, *, report_type: str | None
) -> Self | None:
"""Load JSON string and return an EnumTypeInformation object."""
if not (parsed := json_loads_object(type_data)):
return None
return cls(
dpcode=dpcode,
type_data=type_data,
**cast(dict[str, list[str]], parsed),
)
@dataclass(kw_only=True)
class IntegerTypeInformation(TypeInformation[float]):
"""Integer type information."""
_DPTYPE = DPType.INTEGER
min: int
max: int
scale: int
step: int
unit: str | None = None
report_type: str | None
def scale_value(self, value: int) -> float:
"""Scale a value."""
return value / (10**self.scale)
def scale_value_back(self, value: float) -> int:
"""Return raw value for scaled."""
return round(value * (10**self.scale))
def process_raw_value(
self, raw_value: Any | None, device: CustomerDevice
) -> float | None:
"""Read and process raw value against this type information."""
if raw_value is None:
return None
# Validate input against defined range
if not isinstance(raw_value, int) or not (self.min <= raw_value <= self.max):
if _should_log_warning(
device.id, f"integer_out_range|{self.dpcode}|{raw_value}"
):
LOGGER.warning(
"Found invalid integer value `%s` for datapoint `%s` in product "
"id `%s`, expected integer value between %s and %s; please report "
"this defect to Tuya support",
raw_value,
self.dpcode,
device.product_id,
self.min,
self.max,
)
return None
return raw_value / (10**self.scale)
@classmethod
def _from_json(
cls, dpcode: str, type_data: str, *, report_type: str | None
) -> Self | None:
"""Load JSON string and return an IntegerTypeInformation object."""
if not (parsed := cast(dict[str, Any] | None, json_loads_object(type_data))):
return None
return cls(
dpcode=dpcode,
type_data=type_data,
min=int(parsed["min"]),
max=int(parsed["max"]),
scale=int(parsed["scale"]),
step=int(parsed["step"]),
unit=parsed.get("unit"),
report_type=report_type,
)
@dataclass(kw_only=True)
class JsonTypeInformation(TypeInformation[dict[str, Any]]):
"""Json type information."""
_DPTYPE = DPType.JSON
def process_raw_value(
self, raw_value: Any | None, device: CustomerDevice
) -> dict[str, Any] | None:
"""Read and process raw value against this type information."""
if raw_value is None:
return None
return json_loads_object(raw_value)
@dataclass(kw_only=True)
class RawTypeInformation(TypeInformation[bytes]):
"""Raw type information."""
_DPTYPE = DPType.RAW
def process_raw_value(
self, raw_value: Any | None, device: CustomerDevice
) -> bytes | None:
"""Read and process raw value against this type information."""
if raw_value is None:
return None
return base64.b64decode(raw_value)
@dataclass(kw_only=True)
class StringTypeInformation(TypeInformation[str]):
"""String type information."""
_DPTYPE = DPType.STRING

View File

@@ -2,27 +2,11 @@
from __future__ import annotations
from dataclasses import dataclass
from typing import TYPE_CHECKING, Any
from tuya_sharing import CustomerDevice
from homeassistant.exceptions import ServiceValidationError
from .const import DOMAIN, DPCode, DPType
if TYPE_CHECKING:
from .type_information import IntegerTypeInformation
_DPTYPE_MAPPING: dict[str, DPType] = {
"bitmap": DPType.BITMAP,
"bool": DPType.BOOLEAN,
"enum": DPType.ENUM,
"json": DPType.JSON,
"raw": DPType.RAW,
"string": DPType.STRING,
"value": DPType.INTEGER,
}
from .const import DOMAIN, DPCode
def get_dpcode(
@@ -46,90 +30,6 @@ def get_dpcode(
return None
def parse_dptype(dptype: str) -> DPType | None:
"""Parse DPType from device DPCode information."""
try:
return DPType(dptype)
except ValueError:
# Sometimes, we get ill-formed DPTypes from the cloud,
# this fixes them and maps them to the correct DPType.
return _DPTYPE_MAPPING.get(dptype)
@dataclass(kw_only=True)
class RemapHelper:
"""Helper class for remapping values."""
source_min: int
source_max: int
target_min: int
target_max: int
@classmethod
def from_type_information(
cls,
type_information: IntegerTypeInformation,
target_min: int,
target_max: int,
) -> RemapHelper:
"""Create RemapHelper from IntegerTypeInformation."""
return cls(
source_min=type_information.min,
source_max=type_information.max,
target_min=target_min,
target_max=target_max,
)
@classmethod
def from_function_data(
cls, function_data: dict[str, Any], target_min: int, target_max: int
) -> RemapHelper:
"""Create RemapHelper from function_data."""
return cls(
source_min=function_data["min"],
source_max=function_data["max"],
target_min=target_min,
target_max=target_max,
)
def remap_value_to(self, value: float, *, reverse: bool = False) -> float:
"""Remap a value from this range to a new range."""
return self.remap_value(
value,
self.source_min,
self.source_max,
self.target_min,
self.target_max,
reverse=reverse,
)
def remap_value_from(self, value: float, *, reverse: bool = False) -> float:
"""Remap a value from its current range to this range."""
return self.remap_value(
value,
self.target_min,
self.target_max,
self.source_min,
self.source_max,
reverse=reverse,
)
@staticmethod
def remap_value(
value: float,
from_min: float,
from_max: float,
to_min: float,
to_max: float,
*,
reverse: bool = False,
) -> float:
"""Remap a value from its current range, to a new range."""
if reverse:
value = from_max - value + from_min
return ((value - from_min) / (from_max - from_min)) * (to_max - to_min) + to_min
class ActionDPCodeNotFoundError(ServiceValidationError):
"""Custom exception for action DP code not found errors."""

View File

@@ -4,6 +4,11 @@ from __future__ import annotations
from typing import Any, Self
from tuya_device_handlers.device_wrapper.base import DeviceWrapper
from tuya_device_handlers.device_wrapper.common import (
DPCodeBooleanWrapper,
DPCodeEnumWrapper,
)
from tuya_sharing import CustomerDevice, Manager
from homeassistant.components.vacuum import (
@@ -18,7 +23,6 @@ from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from . import TuyaConfigEntry
from .const import TUYA_DISCOVERY_NEW, DeviceCategory, DPCode
from .entity import TuyaEntity
from .models import DeviceWrapper, DPCodeBooleanWrapper, DPCodeEnumWrapper
class _VacuumActivityWrapper(DeviceWrapper):

View File

@@ -2,6 +2,8 @@
from __future__ import annotations
from tuya_device_handlers.device_wrapper.base import DeviceWrapper
from tuya_device_handlers.device_wrapper.common import DPCodeBooleanWrapper
from tuya_sharing import CustomerDevice, Manager
from homeassistant.components.valve import (
@@ -17,7 +19,6 @@ from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from . import TuyaConfigEntry
from .const import TUYA_DISCOVERY_NEW, DeviceCategory, DPCode
from .entity import TuyaEntity
from .models import DeviceWrapper, DPCodeBooleanWrapper
VALVES: dict[DeviceCategory, tuple[ValveEntityDescription, ...]] = {
DeviceCategory.SFKZQ: (

3
requirements_all.txt generated
View File

@@ -3061,6 +3061,9 @@ ttls==1.8.3
# homeassistant.components.thethingsnetwork
ttn_client==1.2.3
# homeassistant.components.tuya
tuya-device-handlers==0.0.10
# homeassistant.components.tuya
tuya-device-sharing-sdk==0.2.8

View File

@@ -2558,6 +2558,9 @@ ttls==1.8.3
# homeassistant.components.thethingsnetwork
ttn_client==1.2.3
# homeassistant.components.tuya
tuya-device-handlers==0.0.10
# homeassistant.components.tuya
tuya-device-sharing-sdk==0.2.8

View File

@@ -5704,7 +5704,7 @@
'suggested_display_precision': 2,
}),
'sensor.private': dict({
'suggested_unit_of_measurement': <UnitOfElectricCurrent.AMPERE: 'A'>,
'suggested_unit_of_measurement': 'A',
}),
}),
'original_device_class': <SensorDeviceClass.CURRENT: 'current'>,
@@ -5716,7 +5716,7 @@
'supported_features': 0,
'translation_key': 'phase_a_current',
'unique_id': 'tuya.qi94v9dmdx4fkpncqldphase_aelectriccurrent',
'unit_of_measurement': <UnitOfElectricCurrent.AMPERE: 'A'>,
'unit_of_measurement': 'A',
})
# ---
# name: test_platform_setup_and_discovery[sensor.duan_lu_qi_ha_phase_a_current-state]
@@ -5725,7 +5725,7 @@
'device_class': 'current',
'friendly_name': '断路器HA Phase A current',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': <UnitOfElectricCurrent.AMPERE: 'A'>,
'unit_of_measurement': 'A',
}),
'context': <ANY>,
'entity_id': 'sensor.duan_lu_qi_ha_phase_a_current',
@@ -5764,7 +5764,7 @@
'suggested_display_precision': 2,
}),
'sensor.private': dict({
'suggested_unit_of_measurement': <UnitOfPower.KILO_WATT: 'kW'>,
'suggested_unit_of_measurement': 'kW',
}),
}),
'original_device_class': <SensorDeviceClass.POWER: 'power'>,
@@ -5776,7 +5776,7 @@
'supported_features': 0,
'translation_key': 'phase_a_power',
'unique_id': 'tuya.qi94v9dmdx4fkpncqldphase_apower',
'unit_of_measurement': <UnitOfPower.KILO_WATT: 'kW'>,
'unit_of_measurement': 'kW',
})
# ---
# name: test_platform_setup_and_discovery[sensor.duan_lu_qi_ha_phase_a_power-state]
@@ -5785,7 +5785,7 @@
'device_class': 'power',
'friendly_name': '断路器HA Phase A power',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': <UnitOfPower.KILO_WATT: 'kW'>,
'unit_of_measurement': 'kW',
}),
'context': <ANY>,
'entity_id': 'sensor.duan_lu_qi_ha_phase_a_power',
@@ -5833,7 +5833,7 @@
'supported_features': 0,
'translation_key': 'phase_a_voltage',
'unique_id': 'tuya.qi94v9dmdx4fkpncqldphase_avoltage',
'unit_of_measurement': <UnitOfElectricPotential.VOLT: 'V'>,
'unit_of_measurement': 'V',
})
# ---
# name: test_platform_setup_and_discovery[sensor.duan_lu_qi_ha_phase_a_voltage-state]
@@ -5842,7 +5842,7 @@
'device_class': 'voltage',
'friendly_name': '断路器HA Phase A voltage',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': <UnitOfElectricPotential.VOLT: 'V'>,
'unit_of_measurement': 'V',
}),
'context': <ANY>,
'entity_id': 'sensor.duan_lu_qi_ha_phase_a_voltage',
@@ -6225,7 +6225,7 @@
'suggested_display_precision': 2,
}),
'sensor.private': dict({
'suggested_unit_of_measurement': <UnitOfElectricCurrent.AMPERE: 'A'>,
'suggested_unit_of_measurement': 'A',
}),
}),
'original_device_class': <SensorDeviceClass.CURRENT: 'current'>,
@@ -6237,7 +6237,7 @@
'supported_features': 0,
'translation_key': 'phase_a_current',
'unique_id': 'tuya.vcrfgwvbuybgnj3zqldphase_aelectriccurrent',
'unit_of_measurement': <UnitOfElectricCurrent.AMPERE: 'A'>,
'unit_of_measurement': 'A',
})
# ---
# name: test_platform_setup_and_discovery[sensor.edesanya_energy_phase_a_current-state]
@@ -6246,7 +6246,7 @@
'device_class': 'current',
'friendly_name': 'Edesanya Energy Phase A current',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': <UnitOfElectricCurrent.AMPERE: 'A'>,
'unit_of_measurement': 'A',
}),
'context': <ANY>,
'entity_id': 'sensor.edesanya_energy_phase_a_current',
@@ -6285,7 +6285,7 @@
'suggested_display_precision': 2,
}),
'sensor.private': dict({
'suggested_unit_of_measurement': <UnitOfPower.KILO_WATT: 'kW'>,
'suggested_unit_of_measurement': 'kW',
}),
}),
'original_device_class': <SensorDeviceClass.POWER: 'power'>,
@@ -6297,7 +6297,7 @@
'supported_features': 0,
'translation_key': 'phase_a_power',
'unique_id': 'tuya.vcrfgwvbuybgnj3zqldphase_apower',
'unit_of_measurement': <UnitOfPower.KILO_WATT: 'kW'>,
'unit_of_measurement': 'kW',
})
# ---
# name: test_platform_setup_and_discovery[sensor.edesanya_energy_phase_a_power-state]
@@ -6306,7 +6306,7 @@
'device_class': 'power',
'friendly_name': 'Edesanya Energy Phase A power',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': <UnitOfPower.KILO_WATT: 'kW'>,
'unit_of_measurement': 'kW',
}),
'context': <ANY>,
'entity_id': 'sensor.edesanya_energy_phase_a_power',
@@ -6354,7 +6354,7 @@
'supported_features': 0,
'translation_key': 'phase_a_voltage',
'unique_id': 'tuya.vcrfgwvbuybgnj3zqldphase_avoltage',
'unit_of_measurement': <UnitOfElectricPotential.VOLT: 'V'>,
'unit_of_measurement': 'V',
})
# ---
# name: test_platform_setup_and_discovery[sensor.edesanya_energy_phase_a_voltage-state]
@@ -6363,7 +6363,7 @@
'device_class': 'voltage',
'friendly_name': 'Edesanya Energy Phase A voltage',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': <UnitOfElectricPotential.VOLT: 'V'>,
'unit_of_measurement': 'V',
}),
'context': <ANY>,
'entity_id': 'sensor.edesanya_energy_phase_a_voltage',
@@ -6402,7 +6402,7 @@
'suggested_display_precision': 2,
}),
'sensor.private': dict({
'suggested_unit_of_measurement': <UnitOfElectricCurrent.AMPERE: 'A'>,
'suggested_unit_of_measurement': 'A',
}),
}),
'original_device_class': <SensorDeviceClass.CURRENT: 'current'>,
@@ -6414,7 +6414,7 @@
'supported_features': 0,
'translation_key': 'phase_b_current',
'unique_id': 'tuya.vcrfgwvbuybgnj3zqldphase_belectriccurrent',
'unit_of_measurement': <UnitOfElectricCurrent.AMPERE: 'A'>,
'unit_of_measurement': 'A',
})
# ---
# name: test_platform_setup_and_discovery[sensor.edesanya_energy_phase_b_current-state]
@@ -6423,7 +6423,7 @@
'device_class': 'current',
'friendly_name': 'Edesanya Energy Phase B current',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': <UnitOfElectricCurrent.AMPERE: 'A'>,
'unit_of_measurement': 'A',
}),
'context': <ANY>,
'entity_id': 'sensor.edesanya_energy_phase_b_current',
@@ -6462,7 +6462,7 @@
'suggested_display_precision': 2,
}),
'sensor.private': dict({
'suggested_unit_of_measurement': <UnitOfPower.KILO_WATT: 'kW'>,
'suggested_unit_of_measurement': 'kW',
}),
}),
'original_device_class': <SensorDeviceClass.POWER: 'power'>,
@@ -6474,7 +6474,7 @@
'supported_features': 0,
'translation_key': 'phase_b_power',
'unique_id': 'tuya.vcrfgwvbuybgnj3zqldphase_bpower',
'unit_of_measurement': <UnitOfPower.KILO_WATT: 'kW'>,
'unit_of_measurement': 'kW',
})
# ---
# name: test_platform_setup_and_discovery[sensor.edesanya_energy_phase_b_power-state]
@@ -6483,7 +6483,7 @@
'device_class': 'power',
'friendly_name': 'Edesanya Energy Phase B power',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': <UnitOfPower.KILO_WATT: 'kW'>,
'unit_of_measurement': 'kW',
}),
'context': <ANY>,
'entity_id': 'sensor.edesanya_energy_phase_b_power',
@@ -6531,7 +6531,7 @@
'supported_features': 0,
'translation_key': 'phase_b_voltage',
'unique_id': 'tuya.vcrfgwvbuybgnj3zqldphase_bvoltage',
'unit_of_measurement': <UnitOfElectricPotential.VOLT: 'V'>,
'unit_of_measurement': 'V',
})
# ---
# name: test_platform_setup_and_discovery[sensor.edesanya_energy_phase_b_voltage-state]
@@ -6540,7 +6540,7 @@
'device_class': 'voltage',
'friendly_name': 'Edesanya Energy Phase B voltage',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': <UnitOfElectricPotential.VOLT: 'V'>,
'unit_of_measurement': 'V',
}),
'context': <ANY>,
'entity_id': 'sensor.edesanya_energy_phase_b_voltage',
@@ -6579,7 +6579,7 @@
'suggested_display_precision': 2,
}),
'sensor.private': dict({
'suggested_unit_of_measurement': <UnitOfElectricCurrent.AMPERE: 'A'>,
'suggested_unit_of_measurement': 'A',
}),
}),
'original_device_class': <SensorDeviceClass.CURRENT: 'current'>,
@@ -6591,7 +6591,7 @@
'supported_features': 0,
'translation_key': 'phase_c_current',
'unique_id': 'tuya.vcrfgwvbuybgnj3zqldphase_celectriccurrent',
'unit_of_measurement': <UnitOfElectricCurrent.AMPERE: 'A'>,
'unit_of_measurement': 'A',
})
# ---
# name: test_platform_setup_and_discovery[sensor.edesanya_energy_phase_c_current-state]
@@ -6600,7 +6600,7 @@
'device_class': 'current',
'friendly_name': 'Edesanya Energy Phase C current',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': <UnitOfElectricCurrent.AMPERE: 'A'>,
'unit_of_measurement': 'A',
}),
'context': <ANY>,
'entity_id': 'sensor.edesanya_energy_phase_c_current',
@@ -6639,7 +6639,7 @@
'suggested_display_precision': 2,
}),
'sensor.private': dict({
'suggested_unit_of_measurement': <UnitOfPower.KILO_WATT: 'kW'>,
'suggested_unit_of_measurement': 'kW',
}),
}),
'original_device_class': <SensorDeviceClass.POWER: 'power'>,
@@ -6651,7 +6651,7 @@
'supported_features': 0,
'translation_key': 'phase_c_power',
'unique_id': 'tuya.vcrfgwvbuybgnj3zqldphase_cpower',
'unit_of_measurement': <UnitOfPower.KILO_WATT: 'kW'>,
'unit_of_measurement': 'kW',
})
# ---
# name: test_platform_setup_and_discovery[sensor.edesanya_energy_phase_c_power-state]
@@ -6660,7 +6660,7 @@
'device_class': 'power',
'friendly_name': 'Edesanya Energy Phase C power',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': <UnitOfPower.KILO_WATT: 'kW'>,
'unit_of_measurement': 'kW',
}),
'context': <ANY>,
'entity_id': 'sensor.edesanya_energy_phase_c_power',
@@ -6708,7 +6708,7 @@
'supported_features': 0,
'translation_key': 'phase_c_voltage',
'unique_id': 'tuya.vcrfgwvbuybgnj3zqldphase_cvoltage',
'unit_of_measurement': <UnitOfElectricPotential.VOLT: 'V'>,
'unit_of_measurement': 'V',
})
# ---
# name: test_platform_setup_and_discovery[sensor.edesanya_energy_phase_c_voltage-state]
@@ -6717,7 +6717,7 @@
'device_class': 'voltage',
'friendly_name': 'Edesanya Energy Phase C voltage',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': <UnitOfElectricPotential.VOLT: 'V'>,
'unit_of_measurement': 'V',
}),
'context': <ANY>,
'entity_id': 'sensor.edesanya_energy_phase_c_voltage',
@@ -12443,7 +12443,7 @@
'suggested_display_precision': 2,
}),
'sensor.private': dict({
'suggested_unit_of_measurement': <UnitOfElectricCurrent.AMPERE: 'A'>,
'suggested_unit_of_measurement': 'A',
}),
}),
'original_device_class': <SensorDeviceClass.CURRENT: 'current'>,
@@ -12455,7 +12455,7 @@
'supported_features': 0,
'translation_key': 'phase_a_current',
'unique_id': 'tuya.6pd3bkidqldphase_aelectriccurrent',
'unit_of_measurement': <UnitOfElectricCurrent.AMPERE: 'A'>,
'unit_of_measurement': 'A',
})
# ---
# name: test_platform_setup_and_discovery[sensor.medidor_de_energia_phase_a_current-state]
@@ -12464,7 +12464,7 @@
'device_class': 'current',
'friendly_name': 'Medidor de Energia Phase A current',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': <UnitOfElectricCurrent.AMPERE: 'A'>,
'unit_of_measurement': 'A',
}),
'context': <ANY>,
'entity_id': 'sensor.medidor_de_energia_phase_a_current',
@@ -12503,7 +12503,7 @@
'suggested_display_precision': 2,
}),
'sensor.private': dict({
'suggested_unit_of_measurement': <UnitOfPower.KILO_WATT: 'kW'>,
'suggested_unit_of_measurement': 'kW',
}),
}),
'original_device_class': <SensorDeviceClass.POWER: 'power'>,
@@ -12515,7 +12515,7 @@
'supported_features': 0,
'translation_key': 'phase_a_power',
'unique_id': 'tuya.6pd3bkidqldphase_apower',
'unit_of_measurement': <UnitOfPower.KILO_WATT: 'kW'>,
'unit_of_measurement': 'kW',
})
# ---
# name: test_platform_setup_and_discovery[sensor.medidor_de_energia_phase_a_power-state]
@@ -12524,7 +12524,7 @@
'device_class': 'power',
'friendly_name': 'Medidor de Energia Phase A power',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': <UnitOfPower.KILO_WATT: 'kW'>,
'unit_of_measurement': 'kW',
}),
'context': <ANY>,
'entity_id': 'sensor.medidor_de_energia_phase_a_power',
@@ -12572,7 +12572,7 @@
'supported_features': 0,
'translation_key': 'phase_a_voltage',
'unique_id': 'tuya.6pd3bkidqldphase_avoltage',
'unit_of_measurement': <UnitOfElectricPotential.VOLT: 'V'>,
'unit_of_measurement': 'V',
})
# ---
# name: test_platform_setup_and_discovery[sensor.medidor_de_energia_phase_a_voltage-state]
@@ -12581,7 +12581,7 @@
'device_class': 'voltage',
'friendly_name': 'Medidor de Energia Phase A voltage',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': <UnitOfElectricPotential.VOLT: 'V'>,
'unit_of_measurement': 'V',
}),
'context': <ANY>,
'entity_id': 'sensor.medidor_de_energia_phase_a_voltage',
@@ -12620,7 +12620,7 @@
'suggested_display_precision': 2,
}),
'sensor.private': dict({
'suggested_unit_of_measurement': <UnitOfElectricCurrent.AMPERE: 'A'>,
'suggested_unit_of_measurement': 'A',
}),
}),
'original_device_class': <SensorDeviceClass.CURRENT: 'current'>,
@@ -12632,7 +12632,7 @@
'supported_features': 0,
'translation_key': 'phase_b_current',
'unique_id': 'tuya.6pd3bkidqldphase_belectriccurrent',
'unit_of_measurement': <UnitOfElectricCurrent.AMPERE: 'A'>,
'unit_of_measurement': 'A',
})
# ---
# name: test_platform_setup_and_discovery[sensor.medidor_de_energia_phase_b_current-state]
@@ -12641,7 +12641,7 @@
'device_class': 'current',
'friendly_name': 'Medidor de Energia Phase B current',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': <UnitOfElectricCurrent.AMPERE: 'A'>,
'unit_of_measurement': 'A',
}),
'context': <ANY>,
'entity_id': 'sensor.medidor_de_energia_phase_b_current',
@@ -12680,7 +12680,7 @@
'suggested_display_precision': 2,
}),
'sensor.private': dict({
'suggested_unit_of_measurement': <UnitOfPower.KILO_WATT: 'kW'>,
'suggested_unit_of_measurement': 'kW',
}),
}),
'original_device_class': <SensorDeviceClass.POWER: 'power'>,
@@ -12692,7 +12692,7 @@
'supported_features': 0,
'translation_key': 'phase_b_power',
'unique_id': 'tuya.6pd3bkidqldphase_bpower',
'unit_of_measurement': <UnitOfPower.KILO_WATT: 'kW'>,
'unit_of_measurement': 'kW',
})
# ---
# name: test_platform_setup_and_discovery[sensor.medidor_de_energia_phase_b_power-state]
@@ -12701,7 +12701,7 @@
'device_class': 'power',
'friendly_name': 'Medidor de Energia Phase B power',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': <UnitOfPower.KILO_WATT: 'kW'>,
'unit_of_measurement': 'kW',
}),
'context': <ANY>,
'entity_id': 'sensor.medidor_de_energia_phase_b_power',
@@ -12749,7 +12749,7 @@
'supported_features': 0,
'translation_key': 'phase_b_voltage',
'unique_id': 'tuya.6pd3bkidqldphase_bvoltage',
'unit_of_measurement': <UnitOfElectricPotential.VOLT: 'V'>,
'unit_of_measurement': 'V',
})
# ---
# name: test_platform_setup_and_discovery[sensor.medidor_de_energia_phase_b_voltage-state]
@@ -12758,7 +12758,7 @@
'device_class': 'voltage',
'friendly_name': 'Medidor de Energia Phase B voltage',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': <UnitOfElectricPotential.VOLT: 'V'>,
'unit_of_measurement': 'V',
}),
'context': <ANY>,
'entity_id': 'sensor.medidor_de_energia_phase_b_voltage',
@@ -12797,7 +12797,7 @@
'suggested_display_precision': 2,
}),
'sensor.private': dict({
'suggested_unit_of_measurement': <UnitOfElectricCurrent.AMPERE: 'A'>,
'suggested_unit_of_measurement': 'A',
}),
}),
'original_device_class': <SensorDeviceClass.CURRENT: 'current'>,
@@ -12809,7 +12809,7 @@
'supported_features': 0,
'translation_key': 'phase_c_current',
'unique_id': 'tuya.6pd3bkidqldphase_celectriccurrent',
'unit_of_measurement': <UnitOfElectricCurrent.AMPERE: 'A'>,
'unit_of_measurement': 'A',
})
# ---
# name: test_platform_setup_and_discovery[sensor.medidor_de_energia_phase_c_current-state]
@@ -12818,7 +12818,7 @@
'device_class': 'current',
'friendly_name': 'Medidor de Energia Phase C current',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': <UnitOfElectricCurrent.AMPERE: 'A'>,
'unit_of_measurement': 'A',
}),
'context': <ANY>,
'entity_id': 'sensor.medidor_de_energia_phase_c_current',
@@ -12857,7 +12857,7 @@
'suggested_display_precision': 2,
}),
'sensor.private': dict({
'suggested_unit_of_measurement': <UnitOfPower.KILO_WATT: 'kW'>,
'suggested_unit_of_measurement': 'kW',
}),
}),
'original_device_class': <SensorDeviceClass.POWER: 'power'>,
@@ -12869,7 +12869,7 @@
'supported_features': 0,
'translation_key': 'phase_c_power',
'unique_id': 'tuya.6pd3bkidqldphase_cpower',
'unit_of_measurement': <UnitOfPower.KILO_WATT: 'kW'>,
'unit_of_measurement': 'kW',
})
# ---
# name: test_platform_setup_and_discovery[sensor.medidor_de_energia_phase_c_power-state]
@@ -12878,7 +12878,7 @@
'device_class': 'power',
'friendly_name': 'Medidor de Energia Phase C power',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': <UnitOfPower.KILO_WATT: 'kW'>,
'unit_of_measurement': 'kW',
}),
'context': <ANY>,
'entity_id': 'sensor.medidor_de_energia_phase_c_power',
@@ -12926,7 +12926,7 @@
'supported_features': 0,
'translation_key': 'phase_c_voltage',
'unique_id': 'tuya.6pd3bkidqldphase_cvoltage',
'unit_of_measurement': <UnitOfElectricPotential.VOLT: 'V'>,
'unit_of_measurement': 'V',
})
# ---
# name: test_platform_setup_and_discovery[sensor.medidor_de_energia_phase_c_voltage-state]
@@ -12935,7 +12935,7 @@
'device_class': 'voltage',
'friendly_name': 'Medidor de Energia Phase C voltage',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': <UnitOfElectricPotential.VOLT: 'V'>,
'unit_of_measurement': 'V',
}),
'context': <ANY>,
'entity_id': 'sensor.medidor_de_energia_phase_c_voltage',
@@ -13145,7 +13145,7 @@
'suggested_display_precision': 2,
}),
'sensor.private': dict({
'suggested_unit_of_measurement': <UnitOfElectricCurrent.AMPERE: 'A'>,
'suggested_unit_of_measurement': 'A',
}),
}),
'original_device_class': <SensorDeviceClass.CURRENT: 'current'>,
@@ -13157,7 +13157,7 @@
'supported_features': 0,
'translation_key': 'phase_a_current',
'unique_id': 'tuya.nnqlg0rxryraf8ezbdnzphase_aelectriccurrent',
'unit_of_measurement': <UnitOfElectricCurrent.AMPERE: 'A'>,
'unit_of_measurement': 'A',
})
# ---
# name: test_platform_setup_and_discovery[sensor.meter_phase_a_current-state]
@@ -13166,7 +13166,7 @@
'device_class': 'current',
'friendly_name': 'Meter Phase A current',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': <UnitOfElectricCurrent.AMPERE: 'A'>,
'unit_of_measurement': 'A',
}),
'context': <ANY>,
'entity_id': 'sensor.meter_phase_a_current',
@@ -13205,7 +13205,7 @@
'suggested_display_precision': 2,
}),
'sensor.private': dict({
'suggested_unit_of_measurement': <UnitOfPower.KILO_WATT: 'kW'>,
'suggested_unit_of_measurement': 'kW',
}),
}),
'original_device_class': <SensorDeviceClass.POWER: 'power'>,
@@ -13217,7 +13217,7 @@
'supported_features': 0,
'translation_key': 'phase_a_power',
'unique_id': 'tuya.nnqlg0rxryraf8ezbdnzphase_apower',
'unit_of_measurement': <UnitOfPower.KILO_WATT: 'kW'>,
'unit_of_measurement': 'kW',
})
# ---
# name: test_platform_setup_and_discovery[sensor.meter_phase_a_power-state]
@@ -13226,7 +13226,7 @@
'device_class': 'power',
'friendly_name': 'Meter Phase A power',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': <UnitOfPower.KILO_WATT: 'kW'>,
'unit_of_measurement': 'kW',
}),
'context': <ANY>,
'entity_id': 'sensor.meter_phase_a_power',
@@ -13274,7 +13274,7 @@
'supported_features': 0,
'translation_key': 'phase_a_voltage',
'unique_id': 'tuya.nnqlg0rxryraf8ezbdnzphase_avoltage',
'unit_of_measurement': <UnitOfElectricPotential.VOLT: 'V'>,
'unit_of_measurement': 'V',
})
# ---
# name: test_platform_setup_and_discovery[sensor.meter_phase_a_voltage-state]
@@ -13283,7 +13283,7 @@
'device_class': 'voltage',
'friendly_name': 'Meter Phase A voltage',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': <UnitOfElectricPotential.VOLT: 'V'>,
'unit_of_measurement': 'V',
}),
'context': <ANY>,
'entity_id': 'sensor.meter_phase_a_voltage',
@@ -13436,7 +13436,7 @@
'suggested_display_precision': 2,
}),
'sensor.private': dict({
'suggested_unit_of_measurement': <UnitOfElectricCurrent.AMPERE: 'A'>,
'suggested_unit_of_measurement': 'A',
}),
}),
'original_device_class': <SensorDeviceClass.CURRENT: 'current'>,
@@ -13448,7 +13448,7 @@
'supported_features': 0,
'translation_key': 'phase_a_current',
'unique_id': 'tuya.obb7p55c0us6rdxkqldphase_aelectriccurrent',
'unit_of_measurement': <UnitOfElectricCurrent.AMPERE: 'A'>,
'unit_of_measurement': 'A',
})
# ---
# name: test_platform_setup_and_discovery[sensor.metering_3pn_wifi_stable_phase_a_current-state]
@@ -13457,7 +13457,7 @@
'device_class': 'current',
'friendly_name': 'Metering_3PN_WiFi_stable Phase A current',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': <UnitOfElectricCurrent.AMPERE: 'A'>,
'unit_of_measurement': 'A',
}),
'context': <ANY>,
'entity_id': 'sensor.metering_3pn_wifi_stable_phase_a_current',
@@ -13496,7 +13496,7 @@
'suggested_display_precision': 2,
}),
'sensor.private': dict({
'suggested_unit_of_measurement': <UnitOfPower.KILO_WATT: 'kW'>,
'suggested_unit_of_measurement': 'kW',
}),
}),
'original_device_class': <SensorDeviceClass.POWER: 'power'>,
@@ -13508,7 +13508,7 @@
'supported_features': 0,
'translation_key': 'phase_a_power',
'unique_id': 'tuya.obb7p55c0us6rdxkqldphase_apower',
'unit_of_measurement': <UnitOfPower.KILO_WATT: 'kW'>,
'unit_of_measurement': 'kW',
})
# ---
# name: test_platform_setup_and_discovery[sensor.metering_3pn_wifi_stable_phase_a_power-state]
@@ -13517,7 +13517,7 @@
'device_class': 'power',
'friendly_name': 'Metering_3PN_WiFi_stable Phase A power',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': <UnitOfPower.KILO_WATT: 'kW'>,
'unit_of_measurement': 'kW',
}),
'context': <ANY>,
'entity_id': 'sensor.metering_3pn_wifi_stable_phase_a_power',
@@ -13565,7 +13565,7 @@
'supported_features': 0,
'translation_key': 'phase_a_voltage',
'unique_id': 'tuya.obb7p55c0us6rdxkqldphase_avoltage',
'unit_of_measurement': <UnitOfElectricPotential.VOLT: 'V'>,
'unit_of_measurement': 'V',
})
# ---
# name: test_platform_setup_and_discovery[sensor.metering_3pn_wifi_stable_phase_a_voltage-state]
@@ -13574,7 +13574,7 @@
'device_class': 'voltage',
'friendly_name': 'Metering_3PN_WiFi_stable Phase A voltage',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': <UnitOfElectricPotential.VOLT: 'V'>,
'unit_of_measurement': 'V',
}),
'context': <ANY>,
'entity_id': 'sensor.metering_3pn_wifi_stable_phase_a_voltage',
@@ -13613,7 +13613,7 @@
'suggested_display_precision': 2,
}),
'sensor.private': dict({
'suggested_unit_of_measurement': <UnitOfElectricCurrent.AMPERE: 'A'>,
'suggested_unit_of_measurement': 'A',
}),
}),
'original_device_class': <SensorDeviceClass.CURRENT: 'current'>,
@@ -13625,7 +13625,7 @@
'supported_features': 0,
'translation_key': 'phase_b_current',
'unique_id': 'tuya.obb7p55c0us6rdxkqldphase_belectriccurrent',
'unit_of_measurement': <UnitOfElectricCurrent.AMPERE: 'A'>,
'unit_of_measurement': 'A',
})
# ---
# name: test_platform_setup_and_discovery[sensor.metering_3pn_wifi_stable_phase_b_current-state]
@@ -13634,7 +13634,7 @@
'device_class': 'current',
'friendly_name': 'Metering_3PN_WiFi_stable Phase B current',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': <UnitOfElectricCurrent.AMPERE: 'A'>,
'unit_of_measurement': 'A',
}),
'context': <ANY>,
'entity_id': 'sensor.metering_3pn_wifi_stable_phase_b_current',
@@ -13673,7 +13673,7 @@
'suggested_display_precision': 2,
}),
'sensor.private': dict({
'suggested_unit_of_measurement': <UnitOfPower.KILO_WATT: 'kW'>,
'suggested_unit_of_measurement': 'kW',
}),
}),
'original_device_class': <SensorDeviceClass.POWER: 'power'>,
@@ -13685,7 +13685,7 @@
'supported_features': 0,
'translation_key': 'phase_b_power',
'unique_id': 'tuya.obb7p55c0us6rdxkqldphase_bpower',
'unit_of_measurement': <UnitOfPower.KILO_WATT: 'kW'>,
'unit_of_measurement': 'kW',
})
# ---
# name: test_platform_setup_and_discovery[sensor.metering_3pn_wifi_stable_phase_b_power-state]
@@ -13694,7 +13694,7 @@
'device_class': 'power',
'friendly_name': 'Metering_3PN_WiFi_stable Phase B power',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': <UnitOfPower.KILO_WATT: 'kW'>,
'unit_of_measurement': 'kW',
}),
'context': <ANY>,
'entity_id': 'sensor.metering_3pn_wifi_stable_phase_b_power',
@@ -13742,7 +13742,7 @@
'supported_features': 0,
'translation_key': 'phase_b_voltage',
'unique_id': 'tuya.obb7p55c0us6rdxkqldphase_bvoltage',
'unit_of_measurement': <UnitOfElectricPotential.VOLT: 'V'>,
'unit_of_measurement': 'V',
})
# ---
# name: test_platform_setup_and_discovery[sensor.metering_3pn_wifi_stable_phase_b_voltage-state]
@@ -13751,7 +13751,7 @@
'device_class': 'voltage',
'friendly_name': 'Metering_3PN_WiFi_stable Phase B voltage',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': <UnitOfElectricPotential.VOLT: 'V'>,
'unit_of_measurement': 'V',
}),
'context': <ANY>,
'entity_id': 'sensor.metering_3pn_wifi_stable_phase_b_voltage',
@@ -13790,7 +13790,7 @@
'suggested_display_precision': 2,
}),
'sensor.private': dict({
'suggested_unit_of_measurement': <UnitOfElectricCurrent.AMPERE: 'A'>,
'suggested_unit_of_measurement': 'A',
}),
}),
'original_device_class': <SensorDeviceClass.CURRENT: 'current'>,
@@ -13802,7 +13802,7 @@
'supported_features': 0,
'translation_key': 'phase_c_current',
'unique_id': 'tuya.obb7p55c0us6rdxkqldphase_celectriccurrent',
'unit_of_measurement': <UnitOfElectricCurrent.AMPERE: 'A'>,
'unit_of_measurement': 'A',
})
# ---
# name: test_platform_setup_and_discovery[sensor.metering_3pn_wifi_stable_phase_c_current-state]
@@ -13811,7 +13811,7 @@
'device_class': 'current',
'friendly_name': 'Metering_3PN_WiFi_stable Phase C current',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': <UnitOfElectricCurrent.AMPERE: 'A'>,
'unit_of_measurement': 'A',
}),
'context': <ANY>,
'entity_id': 'sensor.metering_3pn_wifi_stable_phase_c_current',
@@ -13850,7 +13850,7 @@
'suggested_display_precision': 2,
}),
'sensor.private': dict({
'suggested_unit_of_measurement': <UnitOfPower.KILO_WATT: 'kW'>,
'suggested_unit_of_measurement': 'kW',
}),
}),
'original_device_class': <SensorDeviceClass.POWER: 'power'>,
@@ -13862,7 +13862,7 @@
'supported_features': 0,
'translation_key': 'phase_c_power',
'unique_id': 'tuya.obb7p55c0us6rdxkqldphase_cpower',
'unit_of_measurement': <UnitOfPower.KILO_WATT: 'kW'>,
'unit_of_measurement': 'kW',
})
# ---
# name: test_platform_setup_and_discovery[sensor.metering_3pn_wifi_stable_phase_c_power-state]
@@ -13871,7 +13871,7 @@
'device_class': 'power',
'friendly_name': 'Metering_3PN_WiFi_stable Phase C power',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': <UnitOfPower.KILO_WATT: 'kW'>,
'unit_of_measurement': 'kW',
}),
'context': <ANY>,
'entity_id': 'sensor.metering_3pn_wifi_stable_phase_c_power',
@@ -13919,7 +13919,7 @@
'supported_features': 0,
'translation_key': 'phase_c_voltage',
'unique_id': 'tuya.obb7p55c0us6rdxkqldphase_cvoltage',
'unit_of_measurement': <UnitOfElectricPotential.VOLT: 'V'>,
'unit_of_measurement': 'V',
})
# ---
# name: test_platform_setup_and_discovery[sensor.metering_3pn_wifi_stable_phase_c_voltage-state]
@@ -13928,7 +13928,7 @@
'device_class': 'voltage',
'friendly_name': 'Metering_3PN_WiFi_stable Phase C voltage',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': <UnitOfElectricPotential.VOLT: 'V'>,
'unit_of_measurement': 'V',
}),
'context': <ANY>,
'entity_id': 'sensor.metering_3pn_wifi_stable_phase_c_voltage',
@@ -15238,7 +15238,7 @@
'suggested_display_precision': 2,
}),
'sensor.private': dict({
'suggested_unit_of_measurement': <UnitOfElectricCurrent.AMPERE: 'A'>,
'suggested_unit_of_measurement': 'A',
}),
}),
'original_device_class': <SensorDeviceClass.CURRENT: 'current'>,
@@ -15250,7 +15250,7 @@
'supported_features': 0,
'translation_key': 'phase_a_current',
'unique_id': 'tuya.bcyciyhhu1g2gk9rqldphase_aelectriccurrent',
'unit_of_measurement': <UnitOfElectricCurrent.AMPERE: 'A'>,
'unit_of_measurement': 'A',
})
# ---
# name: test_platform_setup_and_discovery[sensor.p1_energia_elettrica_phase_a_current-state]
@@ -15259,7 +15259,7 @@
'device_class': 'current',
'friendly_name': 'P1 Energia Elettrica Phase A current',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': <UnitOfElectricCurrent.AMPERE: 'A'>,
'unit_of_measurement': 'A',
}),
'context': <ANY>,
'entity_id': 'sensor.p1_energia_elettrica_phase_a_current',
@@ -15298,7 +15298,7 @@
'suggested_display_precision': 2,
}),
'sensor.private': dict({
'suggested_unit_of_measurement': <UnitOfPower.KILO_WATT: 'kW'>,
'suggested_unit_of_measurement': 'kW',
}),
}),
'original_device_class': <SensorDeviceClass.POWER: 'power'>,
@@ -15310,7 +15310,7 @@
'supported_features': 0,
'translation_key': 'phase_a_power',
'unique_id': 'tuya.bcyciyhhu1g2gk9rqldphase_apower',
'unit_of_measurement': <UnitOfPower.KILO_WATT: 'kW'>,
'unit_of_measurement': 'kW',
})
# ---
# name: test_platform_setup_and_discovery[sensor.p1_energia_elettrica_phase_a_power-state]
@@ -15319,7 +15319,7 @@
'device_class': 'power',
'friendly_name': 'P1 Energia Elettrica Phase A power',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': <UnitOfPower.KILO_WATT: 'kW'>,
'unit_of_measurement': 'kW',
}),
'context': <ANY>,
'entity_id': 'sensor.p1_energia_elettrica_phase_a_power',
@@ -15367,7 +15367,7 @@
'supported_features': 0,
'translation_key': 'phase_a_voltage',
'unique_id': 'tuya.bcyciyhhu1g2gk9rqldphase_avoltage',
'unit_of_measurement': <UnitOfElectricPotential.VOLT: 'V'>,
'unit_of_measurement': 'V',
})
# ---
# name: test_platform_setup_and_discovery[sensor.p1_energia_elettrica_phase_a_voltage-state]
@@ -15376,7 +15376,7 @@
'device_class': 'voltage',
'friendly_name': 'P1 Energia Elettrica Phase A voltage',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': <UnitOfElectricPotential.VOLT: 'V'>,
'unit_of_measurement': 'V',
}),
'context': <ANY>,
'entity_id': 'sensor.p1_energia_elettrica_phase_a_voltage',
@@ -15415,7 +15415,7 @@
'suggested_display_precision': 2,
}),
'sensor.private': dict({
'suggested_unit_of_measurement': <UnitOfElectricCurrent.AMPERE: 'A'>,
'suggested_unit_of_measurement': 'A',
}),
}),
'original_device_class': <SensorDeviceClass.CURRENT: 'current'>,
@@ -15427,7 +15427,7 @@
'supported_features': 0,
'translation_key': 'phase_b_current',
'unique_id': 'tuya.bcyciyhhu1g2gk9rqldphase_belectriccurrent',
'unit_of_measurement': <UnitOfElectricCurrent.AMPERE: 'A'>,
'unit_of_measurement': 'A',
})
# ---
# name: test_platform_setup_and_discovery[sensor.p1_energia_elettrica_phase_b_current-state]
@@ -15436,7 +15436,7 @@
'device_class': 'current',
'friendly_name': 'P1 Energia Elettrica Phase B current',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': <UnitOfElectricCurrent.AMPERE: 'A'>,
'unit_of_measurement': 'A',
}),
'context': <ANY>,
'entity_id': 'sensor.p1_energia_elettrica_phase_b_current',
@@ -15475,7 +15475,7 @@
'suggested_display_precision': 2,
}),
'sensor.private': dict({
'suggested_unit_of_measurement': <UnitOfPower.KILO_WATT: 'kW'>,
'suggested_unit_of_measurement': 'kW',
}),
}),
'original_device_class': <SensorDeviceClass.POWER: 'power'>,
@@ -15487,7 +15487,7 @@
'supported_features': 0,
'translation_key': 'phase_b_power',
'unique_id': 'tuya.bcyciyhhu1g2gk9rqldphase_bpower',
'unit_of_measurement': <UnitOfPower.KILO_WATT: 'kW'>,
'unit_of_measurement': 'kW',
})
# ---
# name: test_platform_setup_and_discovery[sensor.p1_energia_elettrica_phase_b_power-state]
@@ -15496,7 +15496,7 @@
'device_class': 'power',
'friendly_name': 'P1 Energia Elettrica Phase B power',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': <UnitOfPower.KILO_WATT: 'kW'>,
'unit_of_measurement': 'kW',
}),
'context': <ANY>,
'entity_id': 'sensor.p1_energia_elettrica_phase_b_power',
@@ -15544,7 +15544,7 @@
'supported_features': 0,
'translation_key': 'phase_b_voltage',
'unique_id': 'tuya.bcyciyhhu1g2gk9rqldphase_bvoltage',
'unit_of_measurement': <UnitOfElectricPotential.VOLT: 'V'>,
'unit_of_measurement': 'V',
})
# ---
# name: test_platform_setup_and_discovery[sensor.p1_energia_elettrica_phase_b_voltage-state]
@@ -15553,7 +15553,7 @@
'device_class': 'voltage',
'friendly_name': 'P1 Energia Elettrica Phase B voltage',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': <UnitOfElectricPotential.VOLT: 'V'>,
'unit_of_measurement': 'V',
}),
'context': <ANY>,
'entity_id': 'sensor.p1_energia_elettrica_phase_b_voltage',
@@ -15592,7 +15592,7 @@
'suggested_display_precision': 2,
}),
'sensor.private': dict({
'suggested_unit_of_measurement': <UnitOfElectricCurrent.AMPERE: 'A'>,
'suggested_unit_of_measurement': 'A',
}),
}),
'original_device_class': <SensorDeviceClass.CURRENT: 'current'>,
@@ -15604,7 +15604,7 @@
'supported_features': 0,
'translation_key': 'phase_c_current',
'unique_id': 'tuya.bcyciyhhu1g2gk9rqldphase_celectriccurrent',
'unit_of_measurement': <UnitOfElectricCurrent.AMPERE: 'A'>,
'unit_of_measurement': 'A',
})
# ---
# name: test_platform_setup_and_discovery[sensor.p1_energia_elettrica_phase_c_current-state]
@@ -15613,7 +15613,7 @@
'device_class': 'current',
'friendly_name': 'P1 Energia Elettrica Phase C current',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': <UnitOfElectricCurrent.AMPERE: 'A'>,
'unit_of_measurement': 'A',
}),
'context': <ANY>,
'entity_id': 'sensor.p1_energia_elettrica_phase_c_current',
@@ -15652,7 +15652,7 @@
'suggested_display_precision': 2,
}),
'sensor.private': dict({
'suggested_unit_of_measurement': <UnitOfPower.KILO_WATT: 'kW'>,
'suggested_unit_of_measurement': 'kW',
}),
}),
'original_device_class': <SensorDeviceClass.POWER: 'power'>,
@@ -15664,7 +15664,7 @@
'supported_features': 0,
'translation_key': 'phase_c_power',
'unique_id': 'tuya.bcyciyhhu1g2gk9rqldphase_cpower',
'unit_of_measurement': <UnitOfPower.KILO_WATT: 'kW'>,
'unit_of_measurement': 'kW',
})
# ---
# name: test_platform_setup_and_discovery[sensor.p1_energia_elettrica_phase_c_power-state]
@@ -15673,7 +15673,7 @@
'device_class': 'power',
'friendly_name': 'P1 Energia Elettrica Phase C power',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': <UnitOfPower.KILO_WATT: 'kW'>,
'unit_of_measurement': 'kW',
}),
'context': <ANY>,
'entity_id': 'sensor.p1_energia_elettrica_phase_c_power',
@@ -15721,7 +15721,7 @@
'supported_features': 0,
'translation_key': 'phase_c_voltage',
'unique_id': 'tuya.bcyciyhhu1g2gk9rqldphase_cvoltage',
'unit_of_measurement': <UnitOfElectricPotential.VOLT: 'V'>,
'unit_of_measurement': 'V',
})
# ---
# name: test_platform_setup_and_discovery[sensor.p1_energia_elettrica_phase_c_voltage-state]
@@ -15730,7 +15730,7 @@
'device_class': 'voltage',
'friendly_name': 'P1 Energia Elettrica Phase C voltage',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': <UnitOfElectricPotential.VOLT: 'V'>,
'unit_of_measurement': 'V',
}),
'context': <ANY>,
'entity_id': 'sensor.p1_energia_elettrica_phase_c_voltage',
@@ -24155,7 +24155,7 @@
'supported_features': 0,
'translation_key': 'phase_a_current',
'unique_id': 'tuya.9oh1h1uyalfykgg4bdnzphase_aelectriccurrent',
'unit_of_measurement': <UnitOfElectricCurrent.AMPERE: 'A'>,
'unit_of_measurement': 'A',
})
# ---
# name: test_platform_setup_and_discovery[sensor.xoca_dac212xc_v2_s1_phase_a_current-state]
@@ -24164,7 +24164,7 @@
'device_class': 'current',
'friendly_name': 'XOCA-DAC212XC V2-S1 Phase A current',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': <UnitOfElectricCurrent.AMPERE: 'A'>,
'unit_of_measurement': 'A',
}),
'context': <ANY>,
'entity_id': 'sensor.xoca_dac212xc_v2_s1_phase_a_current',
@@ -24212,7 +24212,7 @@
'supported_features': 0,
'translation_key': 'phase_a_power',
'unique_id': 'tuya.9oh1h1uyalfykgg4bdnzphase_apower',
'unit_of_measurement': <UnitOfPower.KILO_WATT: 'kW'>,
'unit_of_measurement': 'kW',
})
# ---
# name: test_platform_setup_and_discovery[sensor.xoca_dac212xc_v2_s1_phase_a_power-state]
@@ -24221,7 +24221,7 @@
'device_class': 'power',
'friendly_name': 'XOCA-DAC212XC V2-S1 Phase A power',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': <UnitOfPower.KILO_WATT: 'kW'>,
'unit_of_measurement': 'kW',
}),
'context': <ANY>,
'entity_id': 'sensor.xoca_dac212xc_v2_s1_phase_a_power',
@@ -24269,7 +24269,7 @@
'supported_features': 0,
'translation_key': 'phase_a_voltage',
'unique_id': 'tuya.9oh1h1uyalfykgg4bdnzphase_avoltage',
'unit_of_measurement': <UnitOfElectricPotential.VOLT: 'V'>,
'unit_of_measurement': 'V',
})
# ---
# name: test_platform_setup_and_discovery[sensor.xoca_dac212xc_v2_s1_phase_a_voltage-state]
@@ -24278,7 +24278,7 @@
'device_class': 'voltage',
'friendly_name': 'XOCA-DAC212XC V2-S1 Phase A voltage',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': <UnitOfElectricPotential.VOLT: 'V'>,
'unit_of_measurement': 'V',
}),
'context': <ANY>,
'entity_id': 'sensor.xoca_dac212xc_v2_s1_phase_a_voltage',