171 lines
5.5 KiB
Python
171 lines
5.5 KiB
Python
"""Tuya Home Assistant Base Device Model."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from typing import Any, Literal, overload
|
|
|
|
from tuya_sharing import CustomerDevice, Manager
|
|
|
|
from homeassistant.helpers.device_registry import DeviceInfo
|
|
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
|
from homeassistant.helpers.entity import Entity
|
|
|
|
from .const import DOMAIN, LOGGER, TUYA_HA_SIGNAL_UPDATE_ENTITY, DPCode, DPType
|
|
from .models import EnumTypeData, IntegerTypeData
|
|
|
|
_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,
|
|
}
|
|
|
|
|
|
class TuyaEntity(Entity):
|
|
"""Tuya base device."""
|
|
|
|
_attr_has_entity_name = True
|
|
_attr_should_poll = False
|
|
|
|
def __init__(self, device: CustomerDevice, device_manager: Manager) -> None:
|
|
"""Init TuyaHaEntity."""
|
|
self._attr_unique_id = f"tuya.{device.id}"
|
|
# TuyaEntity initialize mq can subscribe
|
|
device.set_up = True
|
|
self.device = device
|
|
self.device_manager = device_manager
|
|
|
|
@property
|
|
def device_info(self) -> DeviceInfo:
|
|
"""Return a device description for device registry."""
|
|
return DeviceInfo(
|
|
identifiers={(DOMAIN, self.device.id)},
|
|
manufacturer="Tuya",
|
|
name=self.device.name,
|
|
model=self.device.product_name,
|
|
model_id=self.device.product_id,
|
|
)
|
|
|
|
@property
|
|
def available(self) -> bool:
|
|
"""Return if the device is available."""
|
|
return self.device.online
|
|
|
|
@overload
|
|
def find_dpcode(
|
|
self,
|
|
dpcodes: str | DPCode | tuple[DPCode, ...] | None,
|
|
*,
|
|
prefer_function: bool = False,
|
|
dptype: Literal[DPType.ENUM],
|
|
) -> EnumTypeData | None: ...
|
|
|
|
@overload
|
|
def find_dpcode(
|
|
self,
|
|
dpcodes: str | DPCode | tuple[DPCode, ...] | None,
|
|
*,
|
|
prefer_function: bool = False,
|
|
dptype: Literal[DPType.INTEGER],
|
|
) -> IntegerTypeData | None: ...
|
|
|
|
def find_dpcode(
|
|
self,
|
|
dpcodes: str | DPCode | tuple[DPCode, ...] | None,
|
|
*,
|
|
prefer_function: bool = False,
|
|
dptype: DPType,
|
|
) -> EnumTypeData | IntegerTypeData | None:
|
|
"""Find type information for a matching DP code available for this device."""
|
|
if dptype not in (DPType.ENUM, DPType.INTEGER):
|
|
raise NotImplementedError("Only ENUM and INTEGER types are supported")
|
|
|
|
if dpcodes is None:
|
|
return None
|
|
|
|
if isinstance(dpcodes, str):
|
|
dpcodes = (DPCode(dpcodes),)
|
|
elif not isinstance(dpcodes, tuple):
|
|
dpcodes = (dpcodes,)
|
|
|
|
order = ["status_range", "function"]
|
|
if prefer_function:
|
|
order = ["function", "status_range"]
|
|
|
|
for dpcode in dpcodes:
|
|
for key in order:
|
|
if dpcode not in getattr(self.device, key):
|
|
continue
|
|
if (
|
|
dptype == DPType.ENUM
|
|
and getattr(self.device, key)[dpcode].type == DPType.ENUM
|
|
):
|
|
if not (
|
|
enum_type := EnumTypeData.from_json(
|
|
dpcode, getattr(self.device, key)[dpcode].values
|
|
)
|
|
):
|
|
continue
|
|
return enum_type
|
|
|
|
if (
|
|
dptype == DPType.INTEGER
|
|
and getattr(self.device, key)[dpcode].type == DPType.INTEGER
|
|
):
|
|
if not (
|
|
integer_type := IntegerTypeData.from_json(
|
|
dpcode, getattr(self.device, key)[dpcode].values
|
|
)
|
|
):
|
|
continue
|
|
return integer_type
|
|
|
|
return None
|
|
|
|
def get_dptype(
|
|
self, dpcode: DPCode | None, *, prefer_function: bool = False
|
|
) -> DPType | None:
|
|
"""Find a matching DPCode data type available on for this device."""
|
|
if dpcode is None:
|
|
return None
|
|
|
|
order = ["status_range", "function"]
|
|
if prefer_function:
|
|
order = ["function", "status_range"]
|
|
for key in order:
|
|
if dpcode in getattr(self.device, key):
|
|
current_type = getattr(self.device, key)[dpcode].type
|
|
try:
|
|
return DPType(current_type)
|
|
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(current_type)
|
|
|
|
return None
|
|
|
|
async def async_added_to_hass(self) -> None:
|
|
"""Call when entity is added to hass."""
|
|
self.async_on_remove(
|
|
async_dispatcher_connect(
|
|
self.hass,
|
|
f"{TUYA_HA_SIGNAL_UPDATE_ENTITY}_{self.device.id}",
|
|
self._handle_state_update,
|
|
)
|
|
)
|
|
|
|
async def _handle_state_update(
|
|
self,
|
|
updated_status_properties: list[str] | None,
|
|
dp_timestamps: dict | None = None,
|
|
) -> None:
|
|
self.async_write_ha_state()
|
|
|
|
def _send_command(self, commands: list[dict[str, Any]]) -> None:
|
|
"""Send command to the device."""
|
|
LOGGER.debug("Sending commands for device %s: %s", self.device.id, commands)
|
|
self.device_manager.send_commands(self.device.id, commands)
|