Home Assistant Unofficial Reference 2024.12.1
light.py
Go to the documentation of this file.
1 """Support for Tasmota lights."""
2 
3 from __future__ import annotations
4 
5 from typing import Any
6 
7 from hatasmota import light as tasmota_light
8 from hatasmota.entity import TasmotaEntity as HATasmotaEntity, TasmotaEntityConfig
9 from hatasmota.light import (
10  LIGHT_TYPE_COLDWARM,
11  LIGHT_TYPE_NONE,
12  LIGHT_TYPE_RGB,
13  LIGHT_TYPE_RGBCW,
14  LIGHT_TYPE_RGBW,
15 )
16 from hatasmota.models import DiscoveryHashType
17 
18 from homeassistant.components import light
20  ATTR_BRIGHTNESS,
21  ATTR_COLOR_TEMP,
22  ATTR_EFFECT,
23  ATTR_HS_COLOR,
24  ATTR_TRANSITION,
25  ATTR_WHITE,
26  ColorMode,
27  LightEntity,
28  LightEntityFeature,
29  brightness_supported,
30 )
31 from homeassistant.config_entries import ConfigEntry
32 from homeassistant.core import HomeAssistant, callback
33 from homeassistant.helpers.dispatcher import async_dispatcher_connect
34 from homeassistant.helpers.entity_platform import AddEntitiesCallback
35 
36 from .const import DATA_REMOVE_DISCOVER_COMPONENT
37 from .discovery import TASMOTA_DISCOVERY_ENTITY_NEW
38 from .entity import TasmotaAvailability, TasmotaDiscoveryUpdate, TasmotaOnOffEntity
39 
40 DEFAULT_BRIGHTNESS_MAX = 255
41 TASMOTA_BRIGHTNESS_MAX = 100
42 
43 
45  hass: HomeAssistant,
46  config_entry: ConfigEntry,
47  async_add_entities: AddEntitiesCallback,
48 ) -> None:
49  """Set up Tasmota light dynamically through discovery."""
50 
51  @callback
52  def async_discover(
53  tasmota_entity: HATasmotaEntity, discovery_hash: DiscoveryHashType
54  ) -> None:
55  """Discover and add a Tasmota light."""
57  [TasmotaLight(tasmota_entity=tasmota_entity, discovery_hash=discovery_hash)]
58  )
59 
60  hass.data[DATA_REMOVE_DISCOVER_COMPONENT.format(light.DOMAIN)] = (
62  hass,
63  TASMOTA_DISCOVERY_ENTITY_NEW.format(light.DOMAIN),
64  async_discover,
65  )
66  )
67 
68 
69 def clamp(value: float) -> float:
70  """Clamp value to the range 0..255."""
71  return min(max(value, 0), 255)
72 
73 
74 def scale_brightness(brightness: float) -> float:
75  """Scale brightness from 0..255 to 1..100."""
76  brightness_normalized = brightness / DEFAULT_BRIGHTNESS_MAX
77  device_brightness = min(
78  round(brightness_normalized * TASMOTA_BRIGHTNESS_MAX),
79  TASMOTA_BRIGHTNESS_MAX,
80  )
81  # Make sure the brightness is not rounded down to 0
82  return max(device_brightness, 1)
83 
84 
86  TasmotaAvailability,
87  TasmotaDiscoveryUpdate,
88  TasmotaOnOffEntity,
89  LightEntity,
90 ):
91  """Representation of a Tasmota light."""
92 
93  _tasmota_entity: tasmota_light.TasmotaLight
94 
95  def __init__(self, **kwds: Any) -> None:
96  """Initialize Tasmota light."""
97  self._supported_color_modes_supported_color_modes: set[str] | None = None
98 
99  self._brightness_brightness: int | None = None
100  self._color_mode_color_mode: str | None = None
101  self._color_temp_color_temp: int | None = None
102  self._effect_effect: str | None = None
103  self._white_value_white_value: int | None = None
104  self._flash_times_flash_times = None
105  self._hs_hs: tuple[float, float] | None = None
106 
107  super().__init__(
108  **kwds,
109  )
110 
111  self._setup_from_entity_setup_from_entity()
112 
113  async def discovery_update(
114  self, update: TasmotaEntityConfig, write_state: bool = True
115  ) -> None:
116  """Handle updated discovery message."""
117  await super().discovery_update(update, write_state=False)
118  self._setup_from_entity_setup_from_entity()
119  self.async_write_ha_stateasync_write_ha_state()
120 
121  def _setup_from_entity(self) -> None:
122  """(Re)Setup the entity."""
123  self._supported_color_modes_supported_color_modes = set()
124  supported_features = LightEntityFeature(0)
125  light_type = self._tasmota_entity_tasmota_entity.light_type
126 
127  if light_type in [LIGHT_TYPE_RGB, LIGHT_TYPE_RGBW, LIGHT_TYPE_RGBCW]:
128  # Mark HS support for RGBW light because we don't have direct
129  # control over the white channel, so the base component's RGB->RGBW
130  # translation does not work
131  self._supported_color_modes_supported_color_modes.add(ColorMode.HS)
132  self._color_mode_color_mode = ColorMode.HS
133 
134  if light_type == LIGHT_TYPE_RGBW:
135  self._supported_color_modes_supported_color_modes.add(ColorMode.WHITE)
136 
137  if light_type in [LIGHT_TYPE_COLDWARM, LIGHT_TYPE_RGBCW]:
138  self._supported_color_modes_supported_color_modes.add(ColorMode.COLOR_TEMP)
139  self._color_mode_color_mode = ColorMode.COLOR_TEMP
140 
141  if light_type != LIGHT_TYPE_NONE and not self._supported_color_modes_supported_color_modes:
142  self._supported_color_modes_supported_color_modes.add(ColorMode.BRIGHTNESS)
143  self._color_mode_color_mode = ColorMode.BRIGHTNESS
144 
145  if not self._supported_color_modes_supported_color_modes:
146  self._supported_color_modes_supported_color_modes.add(ColorMode.ONOFF)
147  self._color_mode_color_mode = ColorMode.ONOFF
148 
149  if light_type in [LIGHT_TYPE_RGB, LIGHT_TYPE_RGBW, LIGHT_TYPE_RGBCW]:
150  supported_features |= LightEntityFeature.EFFECT
151 
152  if self._tasmota_entity_tasmota_entity.supports_transition:
153  supported_features |= LightEntityFeature.TRANSITION
154 
155  self._attr_supported_features_attr_supported_features = supported_features
156 
157  @callback
158  def state_updated(self, state: bool, **kwargs: Any) -> None:
159  """Handle state updates."""
160  self._on_off_state_on_off_state_on_off_state = state
161  if attributes := kwargs.get("attributes"):
162  if "brightness" in attributes:
163  brightness = float(attributes["brightness"])
164  percent_bright = brightness / TASMOTA_BRIGHTNESS_MAX
165  self._brightness_brightness = round(percent_bright * 255)
166  if "color_hs" in attributes:
167  self._hs_hs = attributes["color_hs"]
168  if "color_temp" in attributes:
169  self._color_temp_color_temp = attributes["color_temp"]
170  if "effect" in attributes:
171  self._effect_effect = attributes["effect"]
172  if "white_value" in attributes:
173  white_value = float(attributes["white_value"])
174  percent_white = white_value / TASMOTA_BRIGHTNESS_MAX
175  self._white_value_white_value = round(percent_white * 255)
176  if self._tasmota_entity_tasmota_entity.light_type == LIGHT_TYPE_RGBW:
177  # Tasmota does not support RGBW mode, set mode to white or hs
178  if self._white_value_white_value == 0:
179  self._color_mode_color_mode = ColorMode.HS
180  else:
181  self._color_mode_color_mode = ColorMode.WHITE
182  elif self._tasmota_entity_tasmota_entity.light_type == LIGHT_TYPE_RGBCW:
183  # Tasmota does not support RGBWW mode, set mode to ct or hs
184  if self._white_value_white_value == 0:
185  self._color_mode_color_mode = ColorMode.HS
186  else:
187  self._color_mode_color_mode = ColorMode.COLOR_TEMP
188 
189  self.async_write_ha_stateasync_write_ha_state()
190 
191  @property
192  def brightness(self) -> int | None:
193  """Return the brightness of this light between 0..255."""
194  return self._brightness_brightness
195 
196  @property
197  def color_mode(self) -> str | None:
198  """Return the color mode of the light."""
199  return self._color_mode_color_mode
200 
201  @property
202  def color_temp(self) -> int | None:
203  """Return the color temperature in mired."""
204  return self._color_temp_color_temp
205 
206  @property
207  def min_mireds(self) -> int:
208  """Return the coldest color_temp that this light supports."""
209  return self._tasmota_entity_tasmota_entity.min_mireds
210 
211  @property
212  def max_mireds(self) -> int:
213  """Return the warmest color_temp that this light supports."""
214  return self._tasmota_entity_tasmota_entity.max_mireds
215 
216  @property
217  def effect(self) -> str | None:
218  """Return the current effect."""
219  return self._effect_effect
220 
221  @property
222  def effect_list(self) -> list[str] | None:
223  """Return the list of supported effects."""
224  return self._tasmota_entity_tasmota_entity.effect_list
225 
226  @property
227  def hs_color(self) -> tuple[float, float] | None:
228  """Return the hs color value."""
229  if self._hs_hs is None:
230  return None
231  hs_color = self._hs_hs
232  return (hs_color[0], hs_color[1])
233 
234  @property
235  def supported_color_modes(self) -> set[str] | None:
236  """Flag supported color modes."""
237  return self._supported_color_modes_supported_color_modes
238 
239  async def async_turn_on(self, **kwargs: Any) -> None:
240  """Turn the entity on."""
241  supported_color_modes = self._supported_color_modes_supported_color_modes or set()
242 
243  attributes: dict[str, Any] = {}
244 
245  if ATTR_HS_COLOR in kwargs and ColorMode.HS in supported_color_modes:
246  hs_color = kwargs[ATTR_HS_COLOR]
247  attributes["color_hs"] = [hs_color[0], hs_color[1]]
248 
249  if ATTR_WHITE in kwargs and ColorMode.WHITE in supported_color_modes:
250  attributes["white_value"] = scale_brightness(kwargs[ATTR_WHITE])
251 
252  if ATTR_TRANSITION in kwargs:
253  attributes["transition"] = kwargs[ATTR_TRANSITION]
254 
255  if ATTR_BRIGHTNESS in kwargs and brightness_supported(supported_color_modes):
256  attributes["brightness"] = scale_brightness(kwargs[ATTR_BRIGHTNESS])
257 
258  if ATTR_COLOR_TEMP in kwargs and ColorMode.COLOR_TEMP in supported_color_modes:
259  attributes["color_temp"] = int(kwargs[ATTR_COLOR_TEMP])
260 
261  if ATTR_EFFECT in kwargs:
262  attributes["effect"] = kwargs[ATTR_EFFECT]
263 
264  await self._tasmota_entity_tasmota_entity.set_state(True, attributes)
265 
266  async def async_turn_off(self, **kwargs: Any) -> None:
267  """Turn the entity off."""
268  attributes = {"state": "OFF"}
269 
270  if ATTR_TRANSITION in kwargs:
271  attributes["transition"] = kwargs[ATTR_TRANSITION]
272 
273  await self._tasmota_entity_tasmota_entity.set_state(False, attributes)
tuple[float, float]|None hs_color(self)
Definition: light.py:227
None discovery_update(self, TasmotaEntityConfig update, bool write_state=True)
Definition: light.py:115
None state_updated(self, bool state, **Any kwargs)
Definition: light.py:158
bool add(self, _T matcher)
Definition: match.py:185
bool brightness_supported(Iterable[ColorMode|str]|None color_modes)
Definition: __init__.py:155
None async_discover(DiscoveryInfo discovery_info)
Definition: sensor.py:217
None async_setup_entry(HomeAssistant hass, ConfigEntry config_entry, AddEntitiesCallback async_add_entities)
Definition: light.py:48
float scale_brightness(float brightness)
Definition: light.py:74
Callable[[], None] async_dispatcher_connect(HomeAssistant hass, str signal, Callable[..., Any] target)
Definition: dispatcher.py:103