Home Assistant Unofficial Reference 2024.12.1
light.py
Go to the documentation of this file.
1 """Support for Hyperion-NG remotes."""
2 
3 from __future__ import annotations
4 
5 from collections.abc import Callable, Mapping, Sequence
6 import functools
7 import logging
8 from types import MappingProxyType
9 from typing import Any
10 
11 from hyperion import client, const
12 
14  ATTR_BRIGHTNESS,
15  ATTR_EFFECT,
16  ATTR_HS_COLOR,
17  ColorMode,
18  LightEntity,
19  LightEntityFeature,
20 )
21 from homeassistant.config_entries import ConfigEntry
22 from homeassistant.core import HomeAssistant, callback
23 from homeassistant.helpers.device_registry import DeviceInfo
25  async_dispatcher_connect,
26  async_dispatcher_send,
27 )
28 from homeassistant.helpers.entity_platform import AddEntitiesCallback
29 import homeassistant.util.color as color_util
30 
31 from . import (
32  get_hyperion_device_id,
33  get_hyperion_unique_id,
34  listen_for_instance_updates,
35 )
36 from .const import (
37  CONF_EFFECT_HIDE_LIST,
38  CONF_INSTANCE_CLIENTS,
39  CONF_PRIORITY,
40  DEFAULT_ORIGIN,
41  DEFAULT_PRIORITY,
42  DOMAIN,
43  HYPERION_MANUFACTURER_NAME,
44  HYPERION_MODEL_NAME,
45  SIGNAL_ENTITY_REMOVE,
46  TYPE_HYPERION_LIGHT,
47 )
48 
49 _LOGGER = logging.getLogger(__name__)
50 
51 CONF_DEFAULT_COLOR = "default_color"
52 CONF_HDMI_PRIORITY = "hdmi_priority"
53 CONF_EFFECT_LIST = "effect_list"
54 
55 # As we want to preserve brightness control for effects (e.g. to reduce the
56 # brightness), we need to persist the effect that is in flight, so
57 # subsequent calls to turn_on will know to keep the effect enabled.
58 # Unfortunately the Home Assistant UI does not easily expose a way to remove a
59 # selected effect (there is no 'No Effect' option by default). Instead, we
60 # create a new fake effect ("Solid") that is always selected by default for
61 # showing a solid color. This is the same method used by WLED.
62 KEY_EFFECT_SOLID = "Solid"
63 
64 DEFAULT_COLOR = [255, 255, 255]
65 DEFAULT_BRIGHTNESS = 255
66 DEFAULT_EFFECT = KEY_EFFECT_SOLID
67 DEFAULT_NAME = "Hyperion"
68 DEFAULT_PORT = const.DEFAULT_PORT_JSON
69 DEFAULT_HDMI_PRIORITY = 880
70 DEFAULT_EFFECT_LIST: list[str] = []
71 
72 ICON_LIGHTBULB = "mdi:lightbulb"
73 ICON_EFFECT = "mdi:lava-lamp"
74 
75 
77  hass: HomeAssistant,
78  config_entry: ConfigEntry,
79  async_add_entities: AddEntitiesCallback,
80 ) -> None:
81  """Set up a Hyperion platform from config entry."""
82 
83  entry_data = hass.data[DOMAIN][config_entry.entry_id]
84  server_id = config_entry.unique_id
85 
86  @callback
87  def instance_add(instance_num: int, instance_name: str) -> None:
88  """Add entities for a new Hyperion instance."""
89  assert server_id
90  args = (
91  server_id,
92  instance_num,
93  instance_name,
94  config_entry.options,
95  entry_data[CONF_INSTANCE_CLIENTS][instance_num],
96  )
98  [
99  HyperionLight(*args),
100  ]
101  )
102 
103  @callback
104  def instance_remove(instance_num: int) -> None:
105  """Remove entities for an old Hyperion instance."""
106  assert server_id
108  hass,
109  SIGNAL_ENTITY_REMOVE.format(
110  get_hyperion_unique_id(server_id, instance_num, TYPE_HYPERION_LIGHT)
111  ),
112  )
113 
114  listen_for_instance_updates(hass, config_entry, instance_add, instance_remove)
115 
116 
118  """A Hyperion light that acts as a client for the configured priority."""
119 
120  _attr_has_entity_name = True
121  _attr_name = None
122  _attr_color_mode = ColorMode.HS
123  _attr_should_poll = False
124  _attr_supported_color_modes = {ColorMode.HS}
125  _attr_supported_features = LightEntityFeature.EFFECT
126 
127  def __init__(
128  self,
129  server_id: str,
130  instance_num: int,
131  instance_name: str,
132  options: MappingProxyType[str, Any],
133  hyperion_client: client.HyperionClient,
134  ) -> None:
135  """Initialize the light."""
136  self._attr_unique_id_attr_unique_id = self._compute_unique_id_compute_unique_id(server_id, instance_num)
137  self._device_id_device_id = get_hyperion_device_id(server_id, instance_num)
138  self._instance_name_instance_name = instance_name
139  self._options_options = options
140  self._client_client = hyperion_client
141 
142  # Active state representing the Hyperion instance.
143  self._brightness_brightness: int = 255
144  self._rgb_color_rgb_color: Sequence[int] = DEFAULT_COLOR
145  self._effect_effect: str = KEY_EFFECT_SOLID
146 
147  self._static_effect_list: list[str] = [KEY_EFFECT_SOLID]
148  self._effect_list_effect_list: list[str] = self._static_effect_list[:]
149 
150  self._client_callbacks: Mapping[str, Callable[[dict[str, Any]], None]] = {
151  f"{const.KEY_ADJUSTMENT}-{const.KEY_UPDATE}": self._update_adjustment_update_adjustment,
152  f"{const.KEY_COMPONENTS}-{const.KEY_UPDATE}": self._update_components_update_components,
153  f"{const.KEY_EFFECTS}-{const.KEY_UPDATE}": self._update_effect_list_update_effect_list,
154  f"{const.KEY_PRIORITIES}-{const.KEY_UPDATE}": self._update_priorities_update_priorities,
155  f"{const.KEY_CLIENT}-{const.KEY_UPDATE}": self._update_client_update_client,
156  }
157  self._attr_device_info_attr_device_info = DeviceInfo(
158  identifiers={(DOMAIN, self._device_id_device_id)},
159  manufacturer=HYPERION_MANUFACTURER_NAME,
160  model=HYPERION_MODEL_NAME,
161  name=self._instance_name_instance_name,
162  configuration_url=self._client_client.remote_url,
163  )
164 
165  def _compute_unique_id(self, server_id: str, instance_num: int) -> str:
166  """Compute a unique id for this instance."""
167  return get_hyperion_unique_id(server_id, instance_num, TYPE_HYPERION_LIGHT)
168 
169  @property
170  def brightness(self) -> int:
171  """Return the brightness of this light between 0..255."""
172  return self._brightness_brightness
173 
174  @property
175  def hs_color(self) -> tuple[float, float]:
176  """Return last color value set."""
177  return color_util.color_RGB_to_hs(*self._rgb_color_rgb_color)
178 
179  @property
180  def icon(self) -> str:
181  """Return state specific icon."""
182  if self.is_onis_onis_on:
183  if self.effecteffecteffect != KEY_EFFECT_SOLID:
184  return ICON_EFFECT
185  return ICON_LIGHTBULB
186 
187  @property
188  def effect(self) -> str:
189  """Return the current effect."""
190  return self._effect_effect
191 
192  @property
193  def effect_list(self) -> list[str]:
194  """Return the list of supported effects."""
195  return self._effect_list_effect_list
196 
197  @property
198  def available(self) -> bool:
199  """Return server availability."""
200  return bool(self._client_client.has_loaded_state)
201 
202  def _get_option(self, key: str) -> Any:
203  """Get a value from the provided options."""
204  defaults = {
205  CONF_PRIORITY: DEFAULT_PRIORITY,
206  CONF_EFFECT_HIDE_LIST: [],
207  }
208  return self._options_options.get(key, defaults[key])
209 
210  @property
211  def is_on(self) -> bool:
212  """Return true if light is on. Light is considered on when there is a source at the configured HA priority."""
213  return self._get_priority_entry_that_dictates_state_get_priority_entry_that_dictates_state() is not None
214 
215  async def async_turn_on(self, **kwargs: Any) -> None:
216  """Turn on the light."""
217  # == Get key parameters ==
218  if ATTR_EFFECT not in kwargs and ATTR_HS_COLOR in kwargs:
219  effect = KEY_EFFECT_SOLID
220  else:
221  effect = kwargs.get(ATTR_EFFECT, self._effect_effect)
222  rgb_color: Sequence[int]
223  if ATTR_HS_COLOR in kwargs:
224  rgb_color = color_util.color_hs_to_RGB(*kwargs[ATTR_HS_COLOR])
225  else:
226  rgb_color = self._rgb_color_rgb_color
227 
228  # == Set brightness ==
229  if ATTR_BRIGHTNESS in kwargs:
230  brightness = kwargs[ATTR_BRIGHTNESS]
231  for item in self._client_client.adjustment or []:
232  if (
233  const.KEY_ID in item
234  and not await self._client_client.async_send_set_adjustment(
235  **{
236  const.KEY_ADJUSTMENT: {
237  const.KEY_BRIGHTNESS: int(
238  round((float(brightness) * 100) / 255)
239  ),
240  const.KEY_ID: item[const.KEY_ID],
241  }
242  }
243  )
244  ):
245  return
246 
247  # == Set an effect
248  if effect and effect != KEY_EFFECT_SOLID:
249  if not await self._client_client.async_send_set_effect(
250  **{
251  const.KEY_PRIORITY: self._get_option_get_option(CONF_PRIORITY),
252  const.KEY_EFFECT: {const.KEY_NAME: effect},
253  const.KEY_ORIGIN: DEFAULT_ORIGIN,
254  }
255  ):
256  return
257 
258  # == Set a color
259  elif not await self._client_client.async_send_set_color(
260  **{
261  const.KEY_PRIORITY: self._get_option_get_option(CONF_PRIORITY),
262  const.KEY_COLOR: rgb_color,
263  const.KEY_ORIGIN: DEFAULT_ORIGIN,
264  }
265  ):
266  return
267 
268  async def async_turn_off(self, **kwargs: Any) -> None:
269  """Turn off the light i.e. clear the configured priority."""
270  if not await self._client_client.async_send_clear(
271  **{const.KEY_PRIORITY: self._get_option_get_option(CONF_PRIORITY)}
272  ):
273  return
274 
276  self,
277  brightness: int | None = None,
278  rgb_color: Sequence[int] | None = None,
279  effect: str | None = None,
280  ) -> None:
281  """Set the internal state."""
282  if brightness is not None:
283  self._brightness_brightness = brightness
284  if rgb_color is not None:
285  self._rgb_color_rgb_color = rgb_color
286  if effect is not None:
287  self._effect_effect = effect
288 
289  @callback
290  def _update_components(self, _: dict[str, Any] | None = None) -> None:
291  """Update Hyperion components."""
292  self.async_write_ha_stateasync_write_ha_state()
293 
294  @callback
295  def _update_adjustment(self, _: dict[str, Any] | None = None) -> None:
296  """Update Hyperion adjustments."""
297  if self._client_client.adjustment:
298  brightness_pct = self._client_client.adjustment[0].get(
299  const.KEY_BRIGHTNESS, DEFAULT_BRIGHTNESS
300  )
301  if brightness_pct < 0 or brightness_pct > 100:
302  return
303  self._set_internal_state_set_internal_state(
304  brightness=int(round((brightness_pct * 255) / float(100)))
305  )
306  self.async_write_ha_stateasync_write_ha_state()
307 
308  @callback
309  def _update_priorities(self, _: dict[str, Any] | None = None) -> None:
310  """Update Hyperion priorities."""
311  priority = self._get_priority_entry_that_dictates_state_get_priority_entry_that_dictates_state()
312  if priority:
313  component_id = priority.get(const.KEY_COMPONENTID)
314  if component_id == const.KEY_COMPONENTID_EFFECT:
315  # Owner is the effect name.
316  # See: https://docs.hyperion-project.org/en/json/ServerInfo.html#priorities
317  self._set_internal_state_set_internal_state(
318  rgb_color=DEFAULT_COLOR, effect=priority[const.KEY_OWNER]
319  )
320  elif component_id == const.KEY_COMPONENTID_COLOR:
321  self._set_internal_state_set_internal_state(
322  rgb_color=priority[const.KEY_VALUE][const.KEY_RGB],
323  effect=KEY_EFFECT_SOLID,
324  )
325  self.async_write_ha_stateasync_write_ha_state()
326 
327  @callback
328  def _update_effect_list(self, _: dict[str, Any] | None = None) -> None:
329  """Update Hyperion effects."""
330  if not self._client_client.effects:
331  return
332  effect_list: list[str] = []
333  hide_effects = self._get_option_get_option(CONF_EFFECT_HIDE_LIST)
334 
335  for effect in self._client_client.effects or []:
336  if const.KEY_NAME in effect:
337  effect_name = effect[const.KEY_NAME]
338  if effect_name not in hide_effects:
339  effect_list.append(effect_name)
340 
341  self._effect_list_effect_list = [
342  effect for effect in self._static_effect_list if effect not in hide_effects
343  ] + effect_list
344  self.async_write_ha_stateasync_write_ha_state()
345 
346  @callback
347  def _update_full_state(self) -> None:
348  """Update full Hyperion state."""
349  self._update_adjustment_update_adjustment()
350  self._update_priorities_update_priorities()
351  self._update_effect_list_update_effect_list()
352 
353  _LOGGER.debug(
354  (
355  "Hyperion full state update: On=%s,Brightness=%i,Effect=%s "
356  "(%i effects total),Color=%s"
357  ),
358  self.is_onis_onis_on,
359  self._brightness_brightness,
360  self._effect_effect,
361  len(self._effect_list_effect_list),
362  self._rgb_color_rgb_color,
363  )
364 
365  @callback
366  def _update_client(self, _: dict[str, Any] | None = None) -> None:
367  """Update client connection state."""
368  self.async_write_ha_stateasync_write_ha_state()
369 
370  async def async_added_to_hass(self) -> None:
371  """Register callbacks when entity added to hass."""
372  self.async_on_removeasync_on_remove(
374  self.hasshass,
375  SIGNAL_ENTITY_REMOVE.format(self.unique_idunique_id),
376  functools.partial(self.async_removeasync_remove, force_remove=True),
377  )
378  )
379 
380  self._client_client.add_callbacks(self._client_callbacks)
381 
382  # Load initial state.
383  self._update_full_state_update_full_state()
384 
385  async def async_will_remove_from_hass(self) -> None:
386  """Cleanup prior to hass removal."""
387  self._client_client.remove_callbacks(self._client_callbacks)
388 
389  def _get_priority_entry_that_dictates_state(self) -> dict[str, Any] | None:
390  """Get the relevant Hyperion priority entry to consider."""
391  # Return whether or not the HA priority is among the active priorities.
392  for priority in self._client_client.priorities or []:
393  if priority.get(const.KEY_PRIORITY) == self._get_option_get_option(CONF_PRIORITY):
394  return priority
395  return None
None _set_internal_state(self, int|None brightness=None, Sequence[int]|None rgb_color=None, str|None effect=None)
Definition: light.py:280
str _compute_unique_id(self, str server_id, int instance_num)
Definition: light.py:165
None __init__(self, str server_id, int instance_num, str instance_name, MappingProxyType[str, Any] options, client.HyperionClient hyperion_client)
Definition: light.py:134
None _update_effect_list(self, dict[str, Any]|None _=None)
Definition: light.py:328
None _update_adjustment(self, dict[str, Any]|None _=None)
Definition: light.py:295
None _update_priorities(self, dict[str, Any]|None _=None)
Definition: light.py:309
dict[str, Any]|None _get_priority_entry_that_dictates_state(self)
Definition: light.py:389
None _update_client(self, dict[str, Any]|None _=None)
Definition: light.py:366
None _update_components(self, dict[str, Any]|None _=None)
Definition: light.py:290
None async_on_remove(self, CALLBACK_TYPE func)
Definition: entity.py:1331
None async_remove(self, *bool force_remove=False)
Definition: entity.py:1387
web.Response get(self, web.Request request, str config_key)
Definition: view.py:88
None async_setup_entry(HomeAssistant hass, ConfigEntry config_entry, AddEntitiesCallback async_add_entities)
Definition: light.py:80
str get_hyperion_device_id(str server_id, int instance)
Definition: __init__.py:71
str get_hyperion_unique_id(str server_id, int instance, str name)
Definition: __init__.py:66
None listen_for_instance_updates(HomeAssistant hass, ConfigEntry config_entry, Callable add_func, Callable remove_func)
Definition: __init__.py:113
Callable[[], None] async_dispatcher_connect(HomeAssistant hass, str signal, Callable[..., Any] target)
Definition: dispatcher.py:103
None async_dispatcher_send(HomeAssistant hass, str signal, *Any args)
Definition: dispatcher.py:193