Home Assistant Unofficial Reference 2024.12.1
report_state.py
Go to the documentation of this file.
1 """Google Report State implementation."""
2 
3 from __future__ import annotations
4 
5 from collections import deque
6 import logging
7 from typing import TYPE_CHECKING, Any
8 from uuid import uuid4
9 
10 from homeassistant.const import EVENT_STATE_CHANGED
11 from homeassistant.core import (
12  CALLBACK_TYPE,
13  Event,
14  EventStateChangedData,
15  HassJob,
16  HomeAssistant,
17  callback,
18 )
19 from homeassistant.helpers.event import async_call_later
20 from homeassistant.helpers.significant_change import create_checker
21 
22 from .const import DOMAIN
23 from .error import SmartHomeError
24 from .helpers import (
25  AbstractConfig,
26  async_get_entities,
27  async_get_google_entity_if_supported_cached,
28 )
29 
30 # Time to wait until the homegraph updates
31 # https://github.com/actions-on-google/smart-home-nodejs/issues/196#issuecomment-439156639
32 INITIAL_REPORT_DELAY = 60
33 
34 # Seconds to wait to group states
35 REPORT_STATE_WINDOW = 1
36 
37 _LOGGER = logging.getLogger(__name__)
38 
39 
40 @callback
42  hass: HomeAssistant, google_config: AbstractConfig
43 ) -> CALLBACK_TYPE:
44  """Enable state and notification reporting."""
45  checker = None
46  unsub_pending: CALLBACK_TYPE | None = None
47  pending: deque[dict[str, Any]] = deque([{}])
48 
49  async def report_states(now=None):
50  """Report the states."""
51  nonlocal pending
52  nonlocal unsub_pending
53 
54  pending.append({})
55 
56  # We will report all batches except last one because those are finalized.
57  while len(pending) > 1:
58  await google_config.async_report_state_all(
59  {"devices": {"states": pending.popleft()}}
60  )
61 
62  # If things got queued up in last batch while we were reporting, schedule ourselves again
63  if pending[0]:
64  unsub_pending = async_call_later(
65  hass, REPORT_STATE_WINDOW, report_states_job
66  )
67  else:
68  unsub_pending = None
69 
70  report_states_job = HassJob(report_states)
71 
72  @callback
73  def _async_entity_state_filter(data: EventStateChangedData) -> bool:
74  return bool(
75  hass.is_running
76  and (new_state := data["new_state"])
77  and google_config.should_expose(new_state)
79  hass, google_config, new_state
80  )
81  )
82 
83  async def _async_entity_state_listener(event: Event[EventStateChangedData]) -> None:
84  """Handle state changes."""
85  nonlocal unsub_pending, checker
86  data = event.data
87  new_state = data["new_state"]
88  if TYPE_CHECKING:
89  assert new_state is not None # verified in filter
91  hass, google_config, new_state
92  )
93  if TYPE_CHECKING:
94  assert entity is not None # verified in filter
95 
96  # We only trigger notifications on changes in the state value, not attributes.
97  # This is mainly designed for our event entity types
98  # We need to synchronize notifications using a `SYNC` response,
99  # together with other state changes.
100  if (
101  (old_state := data["old_state"])
102  and old_state.state != new_state.state
103  and (notifications := entity.notifications_serialize()) is not None
104  ):
105  event_id = uuid4().hex
106  payload = {
107  "devices": {"notifications": {entity.state.entity_id: notifications}}
108  }
109  _LOGGER.info(
110  "Sending event notification for entity %s",
111  entity.state.entity_id,
112  )
113  result = await google_config.async_sync_notification_all(event_id, payload)
114  if result != 200:
115  _LOGGER.error(
116  "Unable to send notification with result code: %s, check log for more"
117  " info",
118  result,
119  )
120 
121  changed_entity = data["entity_id"]
122  try:
123  entity_data = entity.query_serialize()
124  except SmartHomeError as err:
125  _LOGGER.debug("Not reporting state for %s: %s", changed_entity, err.code)
126  return
127 
128  assert checker is not None
129  if not checker.async_is_significant_change(new_state, extra_arg=entity_data):
130  return
131 
132  _LOGGER.debug("Scheduling report state for %s: %s", changed_entity, entity_data)
133 
134  # If a significant change is already scheduled and we have another significant one,
135  # let's create a new batch of changes
136  if changed_entity in pending[-1]:
137  pending.append({})
138 
139  pending[-1][changed_entity] = entity_data
140 
141  if unsub_pending is None:
142  unsub_pending = async_call_later(
143  hass, REPORT_STATE_WINDOW, report_states_job
144  )
145 
146  @callback
147  def extra_significant_check(
148  hass: HomeAssistant,
149  old_state: str,
150  old_attrs: dict,
151  old_extra_arg: dict,
152  new_state: str,
153  new_attrs: dict,
154  new_extra_arg: dict,
155  ):
156  """Check if the serialized data has changed."""
157  return old_extra_arg != new_extra_arg
158 
159  async def initial_report(_now):
160  """Report initially all states."""
161  nonlocal unsub, checker
162  entities = {}
163 
164  checker = await create_checker(hass, DOMAIN, extra_significant_check)
165 
166  for entity in async_get_entities(hass, google_config):
167  if not entity.should_expose():
168  continue
169 
170  try:
171  entity_data = entity.query_serialize()
172  except SmartHomeError:
173  continue
174 
175  # Tell our significant change checker that we're reporting
176  # So it knows with subsequent changes what was already reported.
177  if not checker.async_is_significant_change(
178  entity.state, extra_arg=entity_data
179  ):
180  continue
181 
182  entities[entity.entity_id] = entity_data
183 
184  if not entities:
185  return
186 
187  await google_config.async_report_state_all({"devices": {"states": entities}})
188 
189  unsub = hass.bus.async_listen(
190  EVENT_STATE_CHANGED,
191  _async_entity_state_listener,
192  event_filter=_async_entity_state_filter,
193  )
194 
195  unsub = async_call_later(
196  hass, INITIAL_REPORT_DELAY, HassJob(initial_report, cancel_on_shutdown=True)
197  )
198 
199  @callback
200  def unsub_all():
201  unsub()
202  if unsub_pending:
203  unsub_pending()
204 
205  return unsub_all
list[AlexaEntity] async_get_entities(HomeAssistant hass, AbstractConfig config)
Definition: entities.py:374
GoogleEntity|None async_get_google_entity_if_supported_cached(HomeAssistant hass, AbstractConfig config, State state)
Definition: helpers.py:763
CALLBACK_TYPE async_enable_report_state(HomeAssistant hass, AbstractConfig google_config)
Definition: report_state.py:43
CALLBACK_TYPE async_call_later(HomeAssistant hass, float|timedelta delay, HassJob[[datetime], Coroutine[Any, Any, None]|None]|Callable[[datetime], Coroutine[Any, Any, None]|None] action)
Definition: event.py:1597
SignificantlyChangedChecker create_checker(HomeAssistant hass, str _domain, ExtraCheckTypeFunc|None extra_significant_check=None)