Home Assistant Unofficial Reference 2024.12.1
switch.py
Go to the documentation of this file.
1 """Flux for Home-Assistant.
2 
3 The idea was taken from https://github.com/KpaBap/hue-flux/
4 """
5 
6 from __future__ import annotations
7 
8 import datetime
9 import logging
10 from typing import Any
11 
12 import voluptuous as vol
13 
15  ATTR_BRIGHTNESS,
16  ATTR_COLOR_TEMP,
17  ATTR_RGB_COLOR,
18  ATTR_TRANSITION,
19  ATTR_XY_COLOR,
20  DOMAIN as LIGHT_DOMAIN,
21  VALID_TRANSITION,
22  is_on,
23 )
24 from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN, SwitchEntity
25 from homeassistant.const import (
26  ATTR_ENTITY_ID,
27  CONF_BRIGHTNESS,
28  CONF_LIGHTS,
29  CONF_MODE,
30  CONF_NAME,
31  CONF_PLATFORM,
32  SERVICE_TURN_ON,
33  STATE_ON,
34  SUN_EVENT_SUNRISE,
35  SUN_EVENT_SUNSET,
36 )
37 from homeassistant.core import HomeAssistant, ServiceCall
38 from homeassistant.helpers import config_validation as cv, event
39 from homeassistant.helpers.entity_platform import AddEntitiesCallback
40 from homeassistant.helpers.restore_state import RestoreEntity
41 from homeassistant.helpers.sun import get_astral_event_date
42 from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
43 from homeassistant.util import slugify
44 from homeassistant.util.color import (
45  color_RGB_to_xy_brightness,
46  color_temperature_kelvin_to_mired,
47  color_temperature_to_rgb,
48 )
49 from homeassistant.util.dt import as_local, utcnow as dt_utcnow
50 
51 _LOGGER = logging.getLogger(__name__)
52 
53 ATTR_UNIQUE_ID = "unique_id"
54 
55 CONF_START_TIME = "start_time"
56 CONF_STOP_TIME = "stop_time"
57 CONF_START_CT = "start_colortemp"
58 CONF_SUNSET_CT = "sunset_colortemp"
59 CONF_STOP_CT = "stop_colortemp"
60 CONF_DISABLE_BRIGHTNESS_ADJUST = "disable_brightness_adjust"
61 CONF_INTERVAL = "interval"
62 
63 MODE_XY = "xy"
64 MODE_MIRED = "mired"
65 MODE_RGB = "rgb"
66 DEFAULT_MODE = MODE_XY
67 
68 PLATFORM_SCHEMA = vol.Schema(
69  {
70  vol.Required(CONF_PLATFORM): "flux",
71  vol.Required(CONF_LIGHTS): cv.entity_ids,
72  vol.Optional(CONF_NAME, default="Flux"): cv.string,
73  vol.Optional(CONF_START_TIME): cv.time,
74  vol.Optional(CONF_STOP_TIME): cv.time,
75  vol.Optional(CONF_START_CT, default=4000): vol.All(
76  vol.Coerce(int), vol.Range(min=1000, max=40000)
77  ),
78  vol.Optional(CONF_SUNSET_CT, default=3000): vol.All(
79  vol.Coerce(int), vol.Range(min=1000, max=40000)
80  ),
81  vol.Optional(CONF_STOP_CT, default=1900): vol.All(
82  vol.Coerce(int), vol.Range(min=1000, max=40000)
83  ),
84  vol.Optional(CONF_BRIGHTNESS): vol.All(
85  vol.Coerce(int), vol.Range(min=0, max=255)
86  ),
87  vol.Optional(CONF_DISABLE_BRIGHTNESS_ADJUST): cv.boolean,
88  vol.Optional(CONF_MODE, default=DEFAULT_MODE): vol.Any(
89  MODE_XY, MODE_MIRED, MODE_RGB
90  ),
91  vol.Optional(CONF_INTERVAL, default=30): cv.positive_int,
92  vol.Optional(ATTR_TRANSITION, default=30): VALID_TRANSITION,
93  vol.Optional(ATTR_UNIQUE_ID): cv.string,
94  }
95 )
96 
97 
98 async def async_set_lights_xy(hass, lights, x_val, y_val, brightness, transition):
99  """Set color of array of lights."""
100  for light in lights:
101  if is_on(hass, light):
102  service_data = {ATTR_ENTITY_ID: light}
103  if x_val is not None and y_val is not None:
104  service_data[ATTR_XY_COLOR] = [x_val, y_val]
105  if brightness is not None:
106  service_data[ATTR_BRIGHTNESS] = brightness
107  if transition is not None:
108  service_data[ATTR_TRANSITION] = transition
109  await hass.services.async_call(LIGHT_DOMAIN, SERVICE_TURN_ON, service_data)
110 
111 
112 async def async_set_lights_temp(hass, lights, mired, brightness, transition):
113  """Set color of array of lights."""
114  for light in lights:
115  if is_on(hass, light):
116  service_data = {ATTR_ENTITY_ID: light}
117  if mired is not None:
118  service_data[ATTR_COLOR_TEMP] = int(mired)
119  if brightness is not None:
120  service_data[ATTR_BRIGHTNESS] = brightness
121  if transition is not None:
122  service_data[ATTR_TRANSITION] = transition
123  await hass.services.async_call(LIGHT_DOMAIN, SERVICE_TURN_ON, service_data)
124 
125 
126 async def async_set_lights_rgb(hass, lights, rgb, transition):
127  """Set color of array of lights."""
128  for light in lights:
129  if is_on(hass, light):
130  service_data = {ATTR_ENTITY_ID: light}
131  if rgb is not None:
132  service_data[ATTR_RGB_COLOR] = rgb
133  if transition is not None:
134  service_data[ATTR_TRANSITION] = transition
135  await hass.services.async_call(LIGHT_DOMAIN, SERVICE_TURN_ON, service_data)
136 
137 
139  hass: HomeAssistant,
140  config: ConfigType,
141  async_add_entities: AddEntitiesCallback,
142  discovery_info: DiscoveryInfoType | None = None,
143 ) -> None:
144  """Set up the Flux switches."""
145  name = config.get(CONF_NAME)
146  lights = config.get(CONF_LIGHTS)
147  start_time = config.get(CONF_START_TIME)
148  stop_time = config.get(CONF_STOP_TIME)
149  start_colortemp = config.get(CONF_START_CT)
150  sunset_colortemp = config.get(CONF_SUNSET_CT)
151  stop_colortemp = config.get(CONF_STOP_CT)
152  brightness = config.get(CONF_BRIGHTNESS)
153  disable_brightness_adjust = config.get(CONF_DISABLE_BRIGHTNESS_ADJUST)
154  mode = config.get(CONF_MODE)
155  interval = config.get(CONF_INTERVAL)
156  transition = config.get(ATTR_TRANSITION)
157  unique_id = config.get(ATTR_UNIQUE_ID)
158  flux = FluxSwitch(
159  name,
160  hass,
161  lights,
162  start_time,
163  stop_time,
164  start_colortemp,
165  sunset_colortemp,
166  stop_colortemp,
167  brightness,
168  disable_brightness_adjust,
169  mode,
170  interval,
171  transition,
172  unique_id,
173  )
174  async_add_entities([flux])
175 
176  async def async_update(call: ServiceCall | None = None) -> None:
177  """Update lights."""
178  await flux.async_flux_update()
179 
180  service_name = slugify(f"{name} update")
181  hass.services.async_register(SWITCH_DOMAIN, service_name, async_update)
182 
183 
184 class FluxSwitch(SwitchEntity, RestoreEntity):
185  """Representation of a Flux switch."""
186 
187  def __init__(
188  self,
189  name,
190  hass,
191  lights,
192  start_time,
193  stop_time,
194  start_colortemp,
195  sunset_colortemp,
196  stop_colortemp,
197  brightness,
198  disable_brightness_adjust,
199  mode,
200  interval,
201  transition,
202  unique_id,
203  ):
204  """Initialize the Flux switch."""
205  self._name_name = name
206  self.hasshasshass = hass
207  self._lights_lights = lights
208  self._start_time_start_time = start_time
209  self._stop_time_stop_time = stop_time
210  self._start_colortemp_start_colortemp = start_colortemp
211  self._sunset_colortemp_sunset_colortemp = sunset_colortemp
212  self._stop_colortemp_stop_colortemp = stop_colortemp
213  self._brightness_brightness = brightness
214  self._disable_brightness_adjust_disable_brightness_adjust = disable_brightness_adjust
215  self._mode_mode = mode
216  self._interval_interval = interval
217  self._transition_transition = transition
218  self._attr_unique_id_attr_unique_id = unique_id
219  self.unsub_trackerunsub_tracker = None
220 
221  @property
222  def name(self):
223  """Return the name of the device if any."""
224  return self._name_name
225 
226  @property
227  def is_on(self):
228  """Return true if switch is on."""
229  return self.unsub_trackerunsub_tracker is not None
230 
231  async def async_added_to_hass(self) -> None:
232  """Call when entity about to be added to hass."""
233  last_state = await self.async_get_last_stateasync_get_last_state()
234  if last_state and last_state.state == STATE_ON:
235  await self.async_turn_onasync_turn_on()
236 
237  async def async_will_remove_from_hass(self) -> None:
238  """Run when entity will be removed from hass."""
239  if self.unsub_trackerunsub_tracker:
240  self.unsub_trackerunsub_tracker()
241  return await super().async_will_remove_from_hass()
242 
243  async def async_turn_on(self, **kwargs: Any) -> None:
244  """Turn on flux."""
245  if self.is_onis_on:
246  return
247 
248  self.unsub_trackerunsub_tracker = event.async_track_time_interval(
249  self.hasshasshass,
250  self.async_flux_updateasync_flux_update,
251  datetime.timedelta(seconds=self._interval_interval),
252  )
253 
254  # Make initial update
255  await self.async_flux_updateasync_flux_update()
256 
257  self.async_write_ha_stateasync_write_ha_state()
258 
259  async def async_turn_off(self, **kwargs: Any) -> None:
260  """Turn off flux."""
261  if self.is_onis_on:
262  self.unsub_trackerunsub_tracker()
263  self.unsub_trackerunsub_tracker = None
264 
265  self.async_write_ha_stateasync_write_ha_state()
266 
267  async def async_flux_update(self, utcnow=None):
268  """Update all the lights using flux."""
269  if utcnow is None:
270  utcnow = dt_utcnow()
271 
272  now = as_local(utcnow)
273 
274  sunset = get_astral_event_date(self.hasshasshass, SUN_EVENT_SUNSET, now.date())
275  start_time = self.find_start_timefind_start_time(now)
276  stop_time = self.find_stop_timefind_stop_time(now)
277 
278  if stop_time <= start_time:
279  # stop_time does not happen in the same day as start_time
280  if start_time < now:
281  # stop time is tomorrow
282  stop_time += datetime.timedelta(days=1)
283  elif now < start_time:
284  # stop_time was yesterday since the new start_time is not reached
285  stop_time -= datetime.timedelta(days=1)
286 
287  if start_time < now < sunset:
288  # Daytime
289  time_state = "day"
290  temp_range = abs(self._start_colortemp_start_colortemp - self._sunset_colortemp_sunset_colortemp)
291  day_length = int(sunset.timestamp() - start_time.timestamp())
292  seconds_from_start = int(now.timestamp() - start_time.timestamp())
293  percentage_complete = seconds_from_start / day_length
294  temp_offset = temp_range * percentage_complete
295  if self._start_colortemp_start_colortemp > self._sunset_colortemp_sunset_colortemp:
296  temp = self._start_colortemp_start_colortemp - temp_offset
297  else:
298  temp = self._start_colortemp_start_colortemp + temp_offset
299  else:
300  # Night time
301  time_state = "night"
302 
303  if now < stop_time:
304  if stop_time < start_time and stop_time.day == sunset.day:
305  # we need to use yesterday's sunset time
306  sunset_time = sunset - datetime.timedelta(days=1)
307  else:
308  sunset_time = sunset
309 
310  night_length = int(stop_time.timestamp() - sunset_time.timestamp())
311  seconds_from_sunset = int(now.timestamp() - sunset_time.timestamp())
312  percentage_complete = seconds_from_sunset / night_length
313  else:
314  percentage_complete = 1
315 
316  temp_range = abs(self._sunset_colortemp_sunset_colortemp - self._stop_colortemp_stop_colortemp)
317  temp_offset = temp_range * percentage_complete
318  if self._sunset_colortemp_sunset_colortemp > self._stop_colortemp_stop_colortemp:
319  temp = self._sunset_colortemp_sunset_colortemp - temp_offset
320  else:
321  temp = self._sunset_colortemp_sunset_colortemp + temp_offset
322  rgb = color_temperature_to_rgb(temp)
323  x_val, y_val, b_val = color_RGB_to_xy_brightness(*rgb)
324  brightness = self._brightness_brightness if self._brightness_brightness else b_val
325  if self._disable_brightness_adjust_disable_brightness_adjust:
326  brightness = None
327  if self._mode_mode == MODE_XY:
328  await async_set_lights_xy(
329  self.hasshasshass, self._lights_lights, x_val, y_val, brightness, self._transition_transition
330  )
331  _LOGGER.debug(
332  (
333  "Lights updated to x:%s y:%s brightness:%s, %s%% "
334  "of %s cycle complete at %s"
335  ),
336  x_val,
337  y_val,
338  brightness,
339  round(percentage_complete * 100),
340  time_state,
341  now,
342  )
343  elif self._mode_mode == MODE_RGB:
344  await async_set_lights_rgb(self.hasshasshass, self._lights_lights, rgb, self._transition_transition)
345  _LOGGER.debug(
346  "Lights updated to rgb:%s, %s%% of %s cycle complete at %s",
347  rgb,
348  round(percentage_complete * 100),
349  time_state,
350  now,
351  )
352  else:
353  # Convert to mired and clamp to allowed values
355  await async_set_lights_temp(
356  self.hasshasshass, self._lights_lights, mired, brightness, self._transition_transition
357  )
358  _LOGGER.debug(
359  (
360  "Lights updated to mired:%s brightness:%s, %s%% "
361  "of %s cycle complete at %s"
362  ),
363  mired,
364  brightness,
365  round(percentage_complete * 100),
366  time_state,
367  now,
368  )
369 
370  def find_start_time(self, now):
371  """Return sunrise or start_time if given."""
372  if self._start_time_start_time:
373  sunrise = now.replace(
374  hour=self._start_time_start_time.hour, minute=self._start_time_start_time.minute, second=0
375  )
376  else:
377  sunrise = get_astral_event_date(self.hasshasshass, SUN_EVENT_SUNRISE, now.date())
378  return sunrise
379 
380  def find_stop_time(self, now):
381  """Return dusk or stop_time if given."""
382  if self._stop_time_stop_time:
383  dusk = now.replace(
384  hour=self._stop_time_stop_time.hour, minute=self._stop_time_stop_time.minute, second=0
385  )
386  else:
387  dusk = get_astral_event_date(self.hasshasshass, "dusk", now.date())
388  return dusk
None async_turn_on(self, **Any kwargs)
Definition: switch.py:243
def async_flux_update(self, utcnow=None)
Definition: switch.py:267
None async_turn_off(self, **Any kwargs)
Definition: switch.py:259
def __init__(self, name, hass, lights, start_time, stop_time, start_colortemp, sunset_colortemp, stop_colortemp, brightness, disable_brightness_adjust, mode, interval, transition, unique_id)
Definition: switch.py:203
def async_set_lights_xy(hass, lights, x_val, y_val, brightness, transition)
Definition: switch.py:98
def async_set_lights_rgb(hass, lights, rgb, transition)
Definition: switch.py:126
None async_setup_platform(HomeAssistant hass, ConfigType config, AddEntitiesCallback async_add_entities, DiscoveryInfoType|None discovery_info=None)
Definition: switch.py:143
def async_set_lights_temp(hass, lights, mired, brightness, transition)
Definition: switch.py:112
datetime.datetime|None get_astral_event_date(HomeAssistant hass, str event, datetime.date|datetime.datetime|None date=None)
Definition: sun.py:117
tuple[float, float, int] color_RGB_to_xy_brightness(int iR, int iG, int iB, GamutType|None Gamut=None)
Definition: color.py:223
tuple[float, float, float] color_temperature_to_rgb(float color_temperature_kelvin)
Definition: color.py:512
int color_temperature_kelvin_to_mired(float kelvin_temperature)
Definition: color.py:636
dt.datetime as_local(dt.datetime dattim)
Definition: dt.py:157