Home Assistant Unofficial Reference 2024.12.1
entity.py
Go to the documentation of this file.
1 """Support for repeating alerts when conditions are met."""
2 
3 from __future__ import annotations
4 
5 from collections.abc import Callable
6 from datetime import timedelta
7 from typing import Any
8 
10  ATTR_DATA,
11  ATTR_MESSAGE,
12  ATTR_TITLE,
13  DOMAIN as DOMAIN_NOTIFY,
14 )
15 from homeassistant.const import STATE_IDLE, STATE_OFF, STATE_ON
16 from homeassistant.core import Event, EventStateChangedData, HassJob, HomeAssistant
17 from homeassistant.exceptions import ServiceNotFound
18 from homeassistant.helpers.entity import Entity
19 from homeassistant.helpers.event import (
20  async_track_point_in_time,
21  async_track_state_change_event,
22 )
23 from homeassistant.helpers.template import Template
24 from homeassistant.util.dt import now
25 
26 from .const import DOMAIN, LOGGER
27 
28 
30  """Representation of an alert."""
31 
32  _attr_should_poll = False
33 
34  def __init__(
35  self,
36  hass: HomeAssistant,
37  entity_id: str,
38  name: str,
39  watched_entity_id: str,
40  state: str,
41  repeat: list[float],
42  skip_first: bool,
43  message_template: Template | None,
44  done_message_template: Template | None,
45  notifiers: list[str],
46  can_ack: bool,
47  title_template: Template | None,
48  data: dict[Any, Any],
49  ) -> None:
50  """Initialize the alert."""
51  self.hasshasshass = hass
52  self._attr_name_attr_name = name
53  self._alert_state_alert_state = state
54  self._skip_first_skip_first = skip_first
55  self._data_data = data
56 
57  self._message_template_message_template = message_template
58  self._done_message_template_done_message_template = done_message_template
59  self._title_template_title_template = title_template
60 
61  self._notifiers_notifiers = notifiers
62  self._can_ack_can_ack = can_ack
63 
64  self._delay_delay = [timedelta(minutes=val) for val in repeat]
65  self._next_delay_next_delay = 0
66 
67  self._firing_firing = False
68  self._ack_ack = False
69  self._cancel_cancel: Callable[[], None] | None = None
70  self._send_done_message_send_done_message = False
71  self.entity_identity_identity_id = f"{DOMAIN}.{entity_id}"
72 
74  hass, [watched_entity_id], self.watched_entity_changewatched_entity_change
75  )
76 
77  @property
78  def state(self) -> str:
79  """Return the alert status."""
80  if self._firing_firing:
81  if self._ack_ack:
82  return STATE_OFF
83  return STATE_ON
84  return STATE_IDLE
85 
86  async def watched_entity_change(self, event: Event[EventStateChangedData]) -> None:
87  """Determine if the alert should start or stop."""
88  if (to_state := event.data["new_state"]) is None:
89  return
90  LOGGER.debug("Watched entity (%s) has changed", event.data["entity_id"])
91  if to_state.state == self._alert_state_alert_state and not self._firing_firing:
92  await self.begin_alertingbegin_alerting()
93  if to_state.state != self._alert_state_alert_state and self._firing_firing:
94  await self.end_alertingend_alerting()
95 
96  async def begin_alerting(self) -> None:
97  """Begin the alert procedures."""
98  LOGGER.debug("Beginning Alert: %s", self._attr_name_attr_name)
99  self._ack_ack = False
100  self._firing_firing = True
101  self._next_delay_next_delay = 0
102 
103  if not self._skip_first_skip_first:
104  await self._notify_notify()
105  else:
106  await self._schedule_notify_schedule_notify()
107 
108  self.async_write_ha_stateasync_write_ha_state()
109 
110  async def end_alerting(self) -> None:
111  """End the alert procedures."""
112  LOGGER.debug("Ending Alert: %s", self._attr_name_attr_name)
113  if self._cancel_cancel is not None:
114  self._cancel_cancel()
115  self._cancel_cancel = None
116 
117  self._ack_ack = False
118  self._firing_firing = False
119  if self._send_done_message_send_done_message:
120  await self._notify_done_message_notify_done_message()
121  self.async_write_ha_stateasync_write_ha_state()
122 
123  async def _schedule_notify(self) -> None:
124  """Schedule a notification."""
125  delay = self._delay_delay[self._next_delay_next_delay]
126  next_msg = now() + delay
127  self._cancel_cancel = async_track_point_in_time(
128  self.hasshasshass,
129  HassJob(
130  self._notify_notify, name="Schedule notify alert", cancel_on_shutdown=True
131  ),
132  next_msg,
133  )
134  self._next_delay_next_delay = min(self._next_delay_next_delay + 1, len(self._delay_delay) - 1)
135 
136  async def _notify(self, *args: Any) -> None:
137  """Send the alert notification."""
138  if not self._firing_firing:
139  return
140 
141  if not self._ack_ack:
142  LOGGER.info("Alerting: %s", self._attr_name_attr_name)
143  self._send_done_message_send_done_message = True
144 
145  if self._message_template_message_template is not None:
146  message = self._message_template_message_template.async_render(parse_result=False)
147  else:
148  message = self._attr_name_attr_name
149 
150  await self._send_notification_message_send_notification_message(message)
151  await self._schedule_notify_schedule_notify()
152 
153  async def _notify_done_message(self) -> None:
154  """Send notification of complete alert."""
155  LOGGER.info("Alerting: %s", self._done_message_template_done_message_template)
156  self._send_done_message_send_done_message = False
157 
158  if self._done_message_template_done_message_template is None:
159  return
160 
161  message = self._done_message_template_done_message_template.async_render(parse_result=False)
162 
163  await self._send_notification_message_send_notification_message(message)
164 
165  async def _send_notification_message(self, message: Any) -> None:
166  if not self._notifiers_notifiers:
167  return
168 
169  msg_payload = {ATTR_MESSAGE: message}
170 
171  if self._title_template_title_template is not None:
172  title = self._title_template_title_template.async_render(parse_result=False)
173  msg_payload[ATTR_TITLE] = title
174  if self._data_data:
175  msg_payload[ATTR_DATA] = self._data_data
176 
177  LOGGER.debug(msg_payload)
178 
179  for target in self._notifiers_notifiers:
180  try:
181  await self.hasshasshass.services.async_call(
182  DOMAIN_NOTIFY, target, msg_payload, context=self._context_context
183  )
184  except ServiceNotFound:
185  LOGGER.error(
186  "Failed to call notify.%s, retrying at next notification interval",
187  target,
188  )
189 
190  async def async_turn_on(self, **kwargs: Any) -> None:
191  """Async Unacknowledge alert."""
192  LOGGER.debug("Reset Alert: %s", self._attr_name_attr_name)
193  self._ack_ack = False
194  self.async_write_ha_stateasync_write_ha_state()
195 
196  async def async_turn_off(self, **kwargs: Any) -> None:
197  """Async Acknowledge alert."""
198  LOGGER.debug("Acknowledged Alert: %s", self._attr_name_attr_name)
199  self._ack_ack = True
200  self.async_write_ha_stateasync_write_ha_state()
201 
202  async def async_toggle(self, **kwargs: Any) -> None:
203  """Async toggle alert."""
204  if self._ack_ack:
205  return await self.async_turn_onasync_turn_on()
206  return await self.async_turn_offasync_turn_off()
None _send_notification_message(self, Any message)
Definition: entity.py:165
None watched_entity_change(self, Event[EventStateChangedData] event)
Definition: entity.py:86
None __init__(self, HomeAssistant hass, str entity_id, str name, str watched_entity_id, str state, list[float] repeat, bool skip_first, Template|None message_template, Template|None done_message_template, list[str] notifiers, bool can_ack, Template|None title_template, dict[Any, Any] data)
Definition: entity.py:49
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_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:1462
datetime now(HomeAssistant hass)
Definition: template.py:1890