Home Assistant Unofficial Reference 2024.12.1
entity.py
Go to the documentation of this file.
1 """Tuya Home Assistant Base Device Model."""
2 
3 from __future__ import annotations
4 
5 import base64
6 from dataclasses import dataclass
7 import json
8 import struct
9 from typing import Any, Literal, Self, overload
10 
11 from tuya_sharing import CustomerDevice, Manager
12 
13 from homeassistant.helpers.device_registry import DeviceInfo
14 from homeassistant.helpers.dispatcher import async_dispatcher_connect
15 from homeassistant.helpers.entity import Entity
16 
17 from .const import DOMAIN, LOGGER, TUYA_HA_SIGNAL_UPDATE_ENTITY, DPCode, DPType
18 from .util import remap_value
19 
20 _DPTYPE_MAPPING: dict[str, DPType] = {
21  "Bitmap": DPType.RAW,
22  "bitmap": DPType.RAW,
23  "bool": DPType.BOOLEAN,
24  "enum": DPType.ENUM,
25  "json": DPType.JSON,
26  "raw": DPType.RAW,
27  "string": DPType.STRING,
28  "value": DPType.INTEGER,
29 }
30 
31 
32 @dataclass
34  """Integer Type Data."""
35 
36  dpcode: DPCode
37  min: int
38  max: int
39  scale: float
40  step: float
41  unit: str | None = None
42  type: str | None = None
43 
44  @property
45  def max_scaled(self) -> float:
46  """Return the max scaled."""
47  return self.scale_valuescale_value(self.max)
48 
49  @property
50  def min_scaled(self) -> float:
51  """Return the min scaled."""
52  return self.scale_valuescale_value(self.min)
53 
54  @property
55  def step_scaled(self) -> float:
56  """Return the step scaled."""
57  return self.step / (10**self.scale)
58 
59  def scale_value(self, value: float) -> float:
60  """Scale a value."""
61  return value / (10**self.scale)
62 
63  def scale_value_back(self, value: float) -> int:
64  """Return raw value for scaled."""
65  return int(value * (10**self.scale))
66 
68  self,
69  value: float,
70  to_min: float = 0,
71  to_max: float = 255,
72  reverse: bool = False,
73  ) -> float:
74  """Remap a value from this range to a new range."""
75  return remap_value(value, self.min, self.max, to_min, to_max, reverse)
76 
78  self,
79  value: float,
80  from_min: float = 0,
81  from_max: float = 255,
82  reverse: bool = False,
83  ) -> float:
84  """Remap a value from its current range to this range."""
85  return remap_value(value, from_min, from_max, self.min, self.max, reverse)
86 
87  @classmethod
88  def from_json(cls, dpcode: DPCode, data: str) -> IntegerTypeData | None:
89  """Load JSON string and return a IntegerTypeData object."""
90  if not (parsed := json.loads(data)):
91  return None
92 
93  return cls(
94  dpcode,
95  min=int(parsed["min"]),
96  max=int(parsed["max"]),
97  scale=float(parsed["scale"]),
98  step=max(float(parsed["step"]), 1),
99  unit=parsed.get("unit"),
100  type=parsed.get("type"),
101  )
102 
103 
104 @dataclass
106  """Enum Type Data."""
107 
108  dpcode: DPCode
109  range: list[str]
110 
111  @classmethod
112  def from_json(cls, dpcode: DPCode, data: str) -> EnumTypeData | None:
113  """Load JSON string and return a EnumTypeData object."""
114  if not (parsed := json.loads(data)):
115  return None
116  return cls(dpcode, **parsed)
117 
118 
119 @dataclass
121  """Electricity Type Data."""
122 
123  electriccurrent: str | None = None
124  power: str | None = None
125  voltage: str | None = None
126 
127  @classmethod
128  def from_json(cls, data: str) -> Self:
129  """Load JSON string and return a ElectricityTypeData object."""
130  return cls(**json.loads(data.lower()))
131 
132  @classmethod
133  def from_raw(cls, data: str) -> Self:
134  """Decode base64 string and return a ElectricityTypeData object."""
135  raw = base64.b64decode(data)
136  voltage = struct.unpack(">H", raw[0:2])[0] / 10.0
137  electriccurrent = struct.unpack(">L", b"\x00" + raw[2:5])[0] / 1000.0
138  power = struct.unpack(">L", b"\x00" + raw[5:8])[0] / 1000.0
139  return cls(
140  electriccurrent=str(electriccurrent), power=str(power), voltage=str(voltage)
141  )
142 
143 
145  """Tuya base device."""
146 
147  _attr_has_entity_name = True
148  _attr_should_poll = False
149 
150  def __init__(self, device: CustomerDevice, device_manager: Manager) -> None:
151  """Init TuyaHaEntity."""
152  self._attr_unique_id_attr_unique_id = f"tuya.{device.id}"
153  # TuyaEntity initialize mq can subscribe
154  device.set_up = True
155  self.devicedevice = device
156  self.device_managerdevice_manager = device_manager
157 
158  @property
159  def device_info(self) -> DeviceInfo:
160  """Return a device description for device registry."""
161  return DeviceInfo(
162  identifiers={(DOMAIN, self.devicedevice.id)},
163  manufacturer="Tuya",
164  name=self.devicedevice.name,
165  model=self.devicedevice.product_name,
166  model_id=self.devicedevice.product_id,
167  )
168 
169  @property
170  def available(self) -> bool:
171  """Return if the device is available."""
172  return self.devicedevice.online
173 
174  @overload
176  self,
177  dpcodes: str | DPCode | tuple[DPCode, ...] | None,
178  *,
179  prefer_function: bool = False,
180  dptype: Literal[DPType.ENUM],
181  ) -> EnumTypeData | None: ...
182 
183  @overload
185  self,
186  dpcodes: str | DPCode | tuple[DPCode, ...] | None,
187  *,
188  prefer_function: bool = False,
189  dptype: Literal[DPType.INTEGER],
190  ) -> IntegerTypeData | None: ...
191 
192  @overload
194  self,
195  dpcodes: str | DPCode | tuple[DPCode, ...] | None,
196  *,
197  prefer_function: bool = False,
198  ) -> DPCode | None: ...
199 
201  self,
202  dpcodes: str | DPCode | tuple[DPCode, ...] | None,
203  *,
204  prefer_function: bool = False,
205  dptype: DPType | None = None,
206  ) -> DPCode | EnumTypeData | IntegerTypeData | None:
207  """Find a matching DP code available on for this device."""
208  if dpcodes is None:
209  return None
210 
211  if isinstance(dpcodes, str):
212  dpcodes = (DPCode(dpcodes),)
213  elif not isinstance(dpcodes, tuple):
214  dpcodes = (dpcodes,)
215 
216  order = ["status_range", "function"]
217  if prefer_function:
218  order = ["function", "status_range"]
219 
220  # When we are not looking for a specific datatype, we can append status for
221  # searching
222  if not dptype:
223  order.append("status")
224 
225  for dpcode in dpcodes:
226  for key in order:
227  if dpcode not in getattr(self.devicedevice, key):
228  continue
229  if (
230  dptype == DPType.ENUM
231  and getattr(self.devicedevice, key)[dpcode].type == DPType.ENUM
232  ):
233  if not (
234  enum_type := EnumTypeData.from_json(
235  dpcode, getattr(self.devicedevice, key)[dpcode].values
236  )
237  ):
238  continue
239  return enum_type
240 
241  if (
242  dptype == DPType.INTEGER
243  and getattr(self.devicedevice, key)[dpcode].type == DPType.INTEGER
244  ):
245  if not (
246  integer_type := IntegerTypeData.from_json(
247  dpcode, getattr(self.devicedevice, key)[dpcode].values
248  )
249  ):
250  continue
251  return integer_type
252 
253  if dptype not in (DPType.ENUM, DPType.INTEGER):
254  return dpcode
255 
256  return None
257 
259  self, dpcode: DPCode | None, prefer_function: bool = False
260  ) -> DPType | None:
261  """Find a matching DPCode data type available on for this device."""
262  if dpcode is None:
263  return None
264 
265  order = ["status_range", "function"]
266  if prefer_function:
267  order = ["function", "status_range"]
268  for key in order:
269  if dpcode in getattr(self.devicedevice, key):
270  current_type = getattr(self.devicedevice, key)[dpcode].type
271  try:
272  return DPType(current_type)
273  except ValueError:
274  # Sometimes, we get ill-formed DPTypes from the cloud,
275  # this fixes them and maps them to the correct DPType.
276  return _DPTYPE_MAPPING.get(current_type)
277 
278  return None
279 
280  async def async_added_to_hass(self) -> None:
281  """Call when entity is added to hass."""
282  self.async_on_removeasync_on_remove(
284  self.hasshass,
285  f"{TUYA_HA_SIGNAL_UPDATE_ENTITY}_{self.device.id}",
286  self._handle_state_update_handle_state_update,
287  )
288  )
289 
291  self, updated_status_properties: list[str] | None
292  ) -> None:
293  self.async_write_ha_stateasync_write_ha_state()
294 
295  def _send_command(self, commands: list[dict[str, Any]]) -> None:
296  """Send command to the device."""
297  LOGGER.debug("Sending commands for device %s: %s", self.devicedevice.id, commands)
298  self.device_managerdevice_manager.send_commands(self.devicedevice.id, commands)
EnumTypeData|None from_json(cls, DPCode dpcode, str data)
Definition: entity.py:112
IntegerTypeData|None from_json(cls, DPCode dpcode, str data)
Definition: entity.py:88
float remap_value_from(self, float value, float from_min=0, float from_max=255, bool reverse=False)
Definition: entity.py:83
float remap_value_to(self, float value, float to_min=0, float to_max=255, bool reverse=False)
Definition: entity.py:73
None __init__(self, CustomerDevice device, Manager device_manager)
Definition: entity.py:150
None _send_command(self, list[dict[str, Any]] commands)
Definition: entity.py:295
None _handle_state_update(self, list[str]|None updated_status_properties)
Definition: entity.py:292
EnumTypeData|None find_dpcode(self, str|DPCode|tuple[DPCode,...]|None dpcodes, *bool prefer_function=False, Literal[DPType.ENUM] dptype)
Definition: entity.py:181
DPType|None get_dptype(self, DPCode|None dpcode, bool prefer_function=False)
Definition: entity.py:260
None async_on_remove(self, CALLBACK_TYPE func)
Definition: entity.py:1331
float remap_value(float value, float from_min=0, float from_max=255, float to_min=0, float to_max=255, bool reverse=False)
Definition: util.py:13
Callable[[], None] async_dispatcher_connect(HomeAssistant hass, str signal, Callable[..., Any] target)
Definition: dispatcher.py:103