Home Assistant Unofficial Reference 2024.12.1
__init__.py
Go to the documentation of this file.
1 """Support to turn on lights based on the states."""
2 
3 from datetime import timedelta
4 from functools import partial
5 import logging
6 
7 import voluptuous as vol
8 
10  DOMAIN as DOMAIN_DEVICE_TRACKER,
11  is_on as device_tracker_is_on,
12 )
13 from homeassistant.components.group import get_entity_ids as group_get_entity_ids
15  ATTR_PROFILE,
16  ATTR_TRANSITION,
17  DOMAIN as DOMAIN_LIGHT,
18  is_on as light_is_on,
19 )
20 from homeassistant.components.person import DOMAIN as DOMAIN_PERSON
21 from homeassistant.const import (
22  ATTR_ENTITY_ID,
23  EVENT_HOMEASSISTANT_START,
24  SERVICE_TURN_OFF,
25  SERVICE_TURN_ON,
26  STATE_HOME,
27  STATE_NOT_HOME,
28  SUN_EVENT_SUNRISE,
29  SUN_EVENT_SUNSET,
30 )
31 from homeassistant.core import Event, EventStateChangedData, HomeAssistant, callback
33 from homeassistant.helpers.event import (
34  async_track_point_in_utc_time,
35  async_track_state_change_event,
36 )
37 from homeassistant.helpers.sun import get_astral_event_next, is_up
38 from homeassistant.helpers.typing import ConfigType
39 import homeassistant.util.dt as dt_util
40 
41 DOMAIN = "device_sun_light_trigger"
42 CONF_DEVICE_GROUP = "device_group"
43 CONF_DISABLE_TURN_OFF = "disable_turn_off"
44 CONF_LIGHT_GROUP = "light_group"
45 CONF_LIGHT_PROFILE = "light_profile"
46 
47 DEFAULT_DISABLE_TURN_OFF = False
48 DEFAULT_LIGHT_PROFILE = "relax"
49 
50 LIGHT_TRANSITION_TIME = timedelta(minutes=15)
51 
52 CONFIG_SCHEMA = vol.Schema(
53  {
54  DOMAIN: vol.Schema(
55  {
56  vol.Optional(CONF_DEVICE_GROUP): cv.entity_id,
57  vol.Optional(
58  CONF_DISABLE_TURN_OFF, default=DEFAULT_DISABLE_TURN_OFF
59  ): cv.boolean,
60  vol.Optional(CONF_LIGHT_GROUP): cv.string,
61  vol.Optional(
62  CONF_LIGHT_PROFILE, default=DEFAULT_LIGHT_PROFILE
63  ): cv.string,
64  }
65  )
66  },
67  extra=vol.ALLOW_EXTRA,
68 )
69 
70 
71 async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
72  """Set up the triggers to control lights based on device presence."""
73  conf = config[DOMAIN]
74  disable_turn_off = conf[CONF_DISABLE_TURN_OFF]
75  light_group = conf.get(CONF_LIGHT_GROUP)
76  light_profile = conf[CONF_LIGHT_PROFILE]
77  device_group = conf.get(CONF_DEVICE_GROUP)
78 
79  async def activate_on_start(_):
80  """Activate automation."""
81  await activate_automation(
82  hass, device_group, light_group, light_profile, disable_turn_off
83  )
84 
85  if hass.is_running:
86  await activate_on_start(None)
87  else:
88  hass.bus.async_listen_once(EVENT_HOMEASSISTANT_START, activate_on_start)
89 
90  return True
91 
92 
93 async def activate_automation( # noqa: C901
94  hass, device_group, light_group, light_profile, disable_turn_off
95 ):
96  """Activate the automation."""
97  logger = logging.getLogger(__name__)
98 
99  if device_group is None:
100  device_entity_ids = hass.states.async_entity_ids(DOMAIN_DEVICE_TRACKER)
101  else:
102  device_entity_ids = group_get_entity_ids(
103  hass, device_group, DOMAIN_DEVICE_TRACKER
104  )
105  device_entity_ids.extend(
106  group_get_entity_ids(hass, device_group, DOMAIN_PERSON)
107  )
108 
109  if not device_entity_ids:
110  logger.error("No devices found to track")
111  return
112 
113  # Get the light IDs from the specified group
114  if light_group is None:
115  light_ids = hass.states.async_entity_ids(DOMAIN_LIGHT)
116  else:
117  light_ids = group_get_entity_ids(hass, light_group, DOMAIN_LIGHT)
118 
119  if not light_ids:
120  logger.error("No lights found to turn on")
121  return
122 
123  @callback
124  def anyone_home():
125  """Test if anyone is home."""
126  return any(device_tracker_is_on(hass, dt_id) for dt_id in device_entity_ids)
127 
128  @callback
129  def any_light_on():
130  """Test if any light on."""
131  return any(light_is_on(hass, light_id) for light_id in light_ids)
132 
133  def calc_time_for_light_when_sunset():
134  """Calculate the time when to start fading lights in when sun sets.
135 
136  Returns None if no next_setting data available.
137 
138  Async friendly.
139  """
140  next_setting = get_astral_event_next(hass, SUN_EVENT_SUNSET)
141  if not next_setting:
142  return None
143  return next_setting - LIGHT_TRANSITION_TIME * len(light_ids)
144 
145  async def async_turn_on_before_sunset(light_id):
146  """Turn on lights."""
147  if not anyone_home() or light_is_on(hass, light_id):
148  return
149  await hass.services.async_call(
150  DOMAIN_LIGHT,
151  SERVICE_TURN_ON,
152  {
153  ATTR_ENTITY_ID: light_id,
154  ATTR_TRANSITION: LIGHT_TRANSITION_TIME.total_seconds(),
155  ATTR_PROFILE: light_profile,
156  },
157  )
158 
159  @callback
160  def async_turn_on_factory(light_id):
161  """Generate turn on callbacks as factory."""
162 
163  async def async_turn_on_light(now):
164  """Turn on specific light."""
165  await async_turn_on_before_sunset(light_id)
166 
167  return async_turn_on_light
168 
169  # Track every time sun rises so we can schedule a time-based
170  # pre-sun set event
171  @callback
172  def schedule_light_turn_on(now):
173  """Turn on all the lights at the moment sun sets.
174 
175  We will schedule to have each light start after one another
176  and slowly transition in.
177  """
178  start_point = calc_time_for_light_when_sunset()
179  if not start_point:
180  return
181 
182  for index, light_id in enumerate(light_ids):
184  hass,
185  async_turn_on_factory(light_id),
186  start_point + index * LIGHT_TRANSITION_TIME,
187  )
188 
190  hass, schedule_light_turn_on, get_astral_event_next(hass, SUN_EVENT_SUNRISE)
191  )
192 
193  # If the sun is already above horizon schedule the time-based pre-sun set
194  # event.
195  if is_up(hass):
196  schedule_light_turn_on(None)
197 
198  @callback
199  def check_light_on_dev_state_change(
200  from_state: str, to_state: str, event: Event[EventStateChangedData]
201  ) -> None:
202  """Handle tracked device state changes."""
203  event_data = event.data
204  if (
205  (old_state := event_data["old_state"]) is None
206  or (new_state := event_data["new_state"]) is None
207  or old_state.state != from_state
208  or new_state.state != to_state
209  ):
210  return
211 
212  entity = event_data["entity_id"]
213  lights_are_on = any_light_on()
214  light_needed = not (lights_are_on or is_up(hass))
215 
216  # These variables are needed for the elif check
217  now = dt_util.utcnow()
218  start_point = calc_time_for_light_when_sunset()
219 
220  # Do we need lights?
221  if light_needed:
222  logger.info("Home coming event for %s. Turning lights on", entity)
223  hass.async_create_task(
224  hass.services.async_call(
225  DOMAIN_LIGHT,
226  SERVICE_TURN_ON,
227  {ATTR_ENTITY_ID: light_ids, ATTR_PROFILE: light_profile},
228  )
229  )
230 
231  # Are we in the time span were we would turn on the lights
232  # if someone would be home?
233  # Check this by seeing if current time is later then the point
234  # in time when we would start putting the lights on.
235  elif start_point and start_point < now < get_astral_event_next(
236  hass, SUN_EVENT_SUNSET
237  ):
238  # Check for every light if it would be on if someone was home
239  # when the fading in started and turn it on if so
240  for index, light_id in enumerate(light_ids):
241  if now > start_point + index * LIGHT_TRANSITION_TIME:
242  hass.async_create_task(
243  hass.services.async_call(
244  DOMAIN_LIGHT, SERVICE_TURN_ON, {ATTR_ENTITY_ID: light_id}
245  )
246  )
247 
248  else:
249  # If this light didn't happen to be turned on yet so
250  # will all the following then, break.
251  break
252 
254  hass,
255  device_entity_ids,
256  partial(check_light_on_dev_state_change, STATE_NOT_HOME, STATE_HOME),
257  )
258 
259  if disable_turn_off:
260  return
261 
262  @callback
263  def turn_off_lights_when_all_leave(entity, old_state, new_state):
264  """Handle device group state change."""
265  # Make sure there is not someone home
266  if anyone_home():
267  return
268 
269  # Check if any light is on
270  if not any_light_on():
271  return
272 
273  logger.info("Everyone has left but there are lights on. Turning them off")
274  hass.async_create_task(
275  hass.services.async_call(
276  DOMAIN_LIGHT, SERVICE_TURN_OFF, {ATTR_ENTITY_ID: light_ids}
277  )
278  )
279 
281  hass,
282  device_entity_ids,
283  partial(turn_off_lights_when_all_leave, STATE_HOME, STATE_NOT_HOME),
284  )
285 
286  return
def activate_automation(hass, device_group, light_group, light_profile, disable_turn_off)
Definition: __init__.py:95
bool async_setup(HomeAssistant hass, ConfigType config)
Definition: __init__.py:71
CALLBACK_TYPE async_track_state_change_event(HomeAssistant hass, str|Iterable[str] entity_ids, Callable[[Event[EventStateChangedData]], Any] action, HassJobType|None job_type=None)
Definition: event.py:314
CALLBACK_TYPE async_track_point_in_utc_time(HomeAssistant hass, HassJob[[datetime], Coroutine[Any, Any, None]|None]|Callable[[datetime], Coroutine[Any, Any, None]|None] action, datetime point_in_time)
Definition: event.py:1542
bool is_up(HomeAssistant hass, datetime.datetime|None utc_point_in_time=None)
Definition: sun.py:142
datetime.datetime get_astral_event_next(HomeAssistant hass, str event, datetime.datetime|None utc_point_in_time=None, datetime.timedelta|None offset=None)
Definition: sun.py:60