Home Assistant Unofficial Reference 2024.12.1
helpers.py
Go to the documentation of this file.
1 """Helper classes for Google Assistant integration."""
2 
3 from __future__ import annotations
4 
5 from abc import ABC, abstractmethod
6 from asyncio import gather
7 from collections.abc import Callable, Collection, Mapping
8 from datetime import datetime, timedelta
9 from functools import lru_cache
10 from http import HTTPStatus
11 import logging
12 import pprint
13 from typing import Any
14 
15 from aiohttp.web import json_response
16 from awesomeversion import AwesomeVersion
17 from yarl import URL
18 
19 from homeassistant.components import webhook
20 from homeassistant.const import (
21  ATTR_DEVICE_CLASS,
22  ATTR_SUPPORTED_FEATURES,
23  CLOUD_NEVER_EXPOSED_ENTITIES,
24  CONF_NAME,
25  STATE_UNAVAILABLE,
26 )
27 from homeassistant.core import CALLBACK_TYPE, Context, HomeAssistant, State, callback
28 from homeassistant.helpers import (
29  area_registry as ar,
30  device_registry as dr,
31  entity_registry as er,
32  start,
33 )
34 from homeassistant.helpers.event import async_call_later
35 from homeassistant.helpers.network import get_url
36 from homeassistant.helpers.redact import partial_redact
37 from homeassistant.util.dt import utcnow
38 
39 from . import trait
40 from .const import (
41  CONF_ALIASES,
42  CONF_ROOM_HINT,
43  DEVICE_CLASS_TO_GOOGLE_TYPES,
44  DOMAIN,
45  DOMAIN_TO_GOOGLE_TYPES,
46  ERR_FUNCTION_NOT_SUPPORTED,
47  NOT_EXPOSE_LOCAL,
48  SOURCE_LOCAL,
49 )
50 from .data_redaction import async_redact_msg
51 from .error import SmartHomeError
52 
53 SYNC_DELAY = 15
54 _LOGGER = logging.getLogger(__name__)
55 LOCAL_SDK_VERSION_HEADER = "HA-Cloud-Version"
56 LOCAL_SDK_MIN_VERSION = AwesomeVersion("2.1.5")
57 
58 
59 @callback
61  hass: HomeAssistant, entity_id: str
62 ) -> tuple[
63  er.RegistryEntry | None,
64  dr.DeviceEntry | None,
65  ar.AreaEntry | None,
66 ]:
67  """Get registry entries."""
68  ent_reg = er.async_get(hass)
69  dev_reg = dr.async_get(hass)
70  area_reg = ar.async_get(hass)
71 
72  if (entity_entry := ent_reg.async_get(entity_id)) and entity_entry.device_id:
73  device_entry = dev_reg.devices.get(entity_entry.device_id)
74  else:
75  device_entry = None
76 
77  if entity_entry and entity_entry.area_id:
78  area_id = entity_entry.area_id
79  elif device_entry and device_entry.area_id:
80  area_id = device_entry.area_id
81  else:
82  area_id = None
83 
84  if area_id is not None:
85  area_entry = area_reg.async_get_area(area_id)
86  else:
87  area_entry = None
88 
89  return entity_entry, device_entry, area_entry
90 
91 
92 class AbstractConfig(ABC):
93  """Hold the configuration for Google Assistant."""
94 
95  _unsub_report_state: Callable[[], None] | None = None
96 
97  def __init__(self, hass: HomeAssistant) -> None:
98  """Initialize abstract config."""
99  self.hasshass = hass
100  self._google_sync_unsub: dict[str, CALLBACK_TYPE] = {}
101  self._local_sdk_active_local_sdk_active = False
102  self._local_last_active_local_last_active: datetime | None = None
103  self._local_sdk_version_warn_local_sdk_version_warn = False
104  self.is_supported_cache: dict[str, tuple[int | None, bool]] = {}
105  self._on_deinitialize: list[CALLBACK_TYPE] = []
106 
107  async def async_initialize(self) -> None:
108  """Perform async initialization of config."""
109  if not self.enabledenabled:
110  return
111 
112  async def sync_google(_):
113  """Sync entities to Google."""
114  await self.async_sync_entities_allasync_sync_entities_all()
115 
116  self._on_deinitialize.append(start.async_at_start(self.hasshass, sync_google))
117 
118  @callback
119  def async_deinitialize(self) -> None:
120  """Remove listeners."""
121  _LOGGER.debug("async_deinitialize")
122  while self._on_deinitialize:
123  self._on_deinitialize.pop()()
124 
125  @property
126  @abstractmethod
127  def enabled(self):
128  """Return if Google is enabled."""
129 
130  @property
131  @abstractmethod
132  def entity_config(self):
133  """Return entity config."""
134 
135  @property
136  @abstractmethod
138  """Return entity config."""
139 
140  @property
142  """Return if we're actively reporting states."""
143  return self._unsub_report_state_unsub_report_state is not None
144 
145  @property
147  """Return if we're actively accepting local messages."""
148  return self._local_sdk_active_local_sdk_active
149 
150  @property
151  @abstractmethod
153  """Return if states should be proactively reported."""
154 
155  @property
156  def is_local_connected(self) -> bool:
157  """Return if local is connected."""
158  return (
159  self._local_last_active_local_last_active is not None
160  # We get a reachable devices intent every minute.
161  and self._local_last_active_local_last_active > utcnow() - timedelta(seconds=70)
162  )
163 
164  @abstractmethod
165  def get_local_user_id(self, webhook_id):
166  """Map webhook ID to a Home Assistant user ID.
167 
168  Any action initiated by Google Assistant via the local SDK will be attributed
169  to the returned user ID.
170 
171  Return None if no user id is found for the webhook_id.
172  """
173 
174  @abstractmethod
175  def get_local_webhook_id(self, agent_user_id):
176  """Return the webhook ID to be used for actions for a given agent user id via the local SDK."""
177 
178  @abstractmethod
179  def get_agent_user_id_from_context(self, context):
180  """Get agent user ID from context."""
181 
182  @abstractmethod
183  def get_agent_user_id_from_webhook(self, webhook_id):
184  """Map webhook ID to a Google agent user ID.
185 
186  Return None if no agent user id is found for the webhook_id.
187  """
188 
189  @abstractmethod
190  def should_expose(self, state) -> bool:
191  """Return if entity should be exposed."""
192 
193  @abstractmethod
194  def should_2fa(self, state):
195  """If an entity should have 2FA checked."""
196 
197  @abstractmethod
199  self, message: dict[str, Any], agent_user_id: str, event_id: str | None = None
200  ) -> HTTPStatus | None:
201  """Send a state report to Google."""
202 
203  async def async_report_state_all(self, message):
204  """Send a state report to Google for all previously synced users."""
205  jobs = [
206  self.async_report_stateasync_report_state(message, agent_user_id)
207  for agent_user_id in self.async_get_agent_usersasync_get_agent_users()
208  ]
209  await gather(*jobs)
210 
211  @callback
212  def async_enable_report_state(self) -> None:
213  """Enable proactive mode."""
214  # Circular dep
215  # pylint: disable-next=import-outside-toplevel
216  from .report_state import async_enable_report_state
217 
218  if self._unsub_report_state_unsub_report_state is None:
219  self._unsub_report_state_unsub_report_state = async_enable_report_state(self.hasshass, self)
220 
221  @callback
222  def async_disable_report_state(self) -> None:
223  """Disable report state."""
224  if self._unsub_report_state_unsub_report_state is not None:
225  self._unsub_report_state_unsub_report_state()
226  self._unsub_report_state_unsub_report_state = None
227 
228  async def async_sync_entities(self, agent_user_id: str):
229  """Sync all entities to Google."""
230  # Remove any pending sync
231  self._google_sync_unsub.pop(agent_user_id, lambda: None)()
232  status = await self._async_request_sync_devices_async_request_sync_devices(agent_user_id)
233  if status == HTTPStatus.NOT_FOUND:
234  await self.async_disconnect_agent_userasync_disconnect_agent_user(agent_user_id)
235  return status
236 
237  async def async_sync_entities_all(self) -> int:
238  """Sync all entities to Google for all registered agents."""
239  if not self.async_get_agent_usersasync_get_agent_users():
240  return 204
241 
242  res = await gather(
243  *(
244  self.async_sync_entitiesasync_sync_entities(agent_user_id)
245  for agent_user_id in self.async_get_agent_usersasync_get_agent_users()
246  )
247  )
248  return max(res, default=204)
249 
251  self, agent_user_id: str, event_id: str, payload: dict[str, Any]
252  ) -> HTTPStatus:
253  """Sync notifications to Google."""
254  # Remove any pending sync
255  self._google_sync_unsub.pop(agent_user_id, lambda: None)()
256  status = await self.async_report_stateasync_report_state(payload, agent_user_id, event_id)
257  assert status is not None
258  if status == HTTPStatus.NOT_FOUND:
259  await self.async_disconnect_agent_userasync_disconnect_agent_user(agent_user_id)
260  return status
261 
263  self, event_id: str, payload: dict[str, Any]
264  ) -> HTTPStatus:
265  """Sync notification to Google for all registered agents."""
266  if not self.async_get_agent_usersasync_get_agent_users():
267  return HTTPStatus.NO_CONTENT
268 
269  res = await gather(
270  *(
271  self.async_sync_notificationasync_sync_notification(agent_user_id, event_id, payload)
272  for agent_user_id in self.async_get_agent_usersasync_get_agent_users()
273  )
274  )
275  return max(res, default=HTTPStatus.NO_CONTENT)
276 
277  @callback
278  def async_schedule_google_sync(self, agent_user_id: str):
279  """Schedule a sync."""
280 
281  async def _schedule_callback(_now):
282  """Handle a scheduled sync callback."""
283  self._google_sync_unsub.pop(agent_user_id, None)
284  await self.async_sync_entitiesasync_sync_entities(agent_user_id)
285 
286  self._google_sync_unsub.pop(agent_user_id, lambda: None)()
287 
288  self._google_sync_unsub[agent_user_id] = async_call_later(
289  self.hasshass, SYNC_DELAY, _schedule_callback
290  )
291 
292  @callback
294  """Schedule a sync for all registered agents."""
295  for agent_user_id in self.async_get_agent_usersasync_get_agent_users():
296  self.async_schedule_google_syncasync_schedule_google_sync(agent_user_id)
297 
298  async def _async_request_sync_devices(self, agent_user_id: str) -> int:
299  """Trigger a sync with Google.
300 
301  Return value is the HTTP status code of the sync request.
302  """
303  raise NotImplementedError
304 
305  @abstractmethod
306  async def async_connect_agent_user(self, agent_user_id: str):
307  """Add a synced and known agent_user_id.
308 
309  Called before sending a sync response to Google.
310  """
311 
312  @abstractmethod
313  async def async_disconnect_agent_user(self, agent_user_id: str):
314  """Turn off report state and disable further state reporting.
315 
316  Called when:
317  - The user disconnects their account from Google.
318  - When the cloud configuration is initialized
319  - When sync entities fails with 404
320  """
321 
322  @callback
323  @abstractmethod
324  def async_get_agent_users(self) -> Collection[str]:
325  """Return known agent users."""
326 
327  @callback
328  def async_enable_local_sdk(self) -> None:
329  """Enable the local SDK."""
330  _LOGGER.debug("async_enable_local_sdk")
331  setup_successful = True
332  setup_webhook_ids = []
333 
334  # Don't enable local SDK if ssl is enabled
335  if self.hasshass.config.api and self.hasshass.config.api.use_ssl:
336  self._local_sdk_active_local_sdk_active = False
337  return
338 
339  for user_agent_id in self.async_get_agent_usersasync_get_agent_users():
340  if (webhook_id := self.get_local_webhook_idget_local_webhook_id(user_agent_id)) is None:
341  setup_successful = False
342  break
343 
344  _LOGGER.debug(
345  "Register webhook handler %s for agent user id %s",
346  partial_redact(webhook_id),
347  partial_redact(user_agent_id),
348  )
349  try:
350  webhook.async_register(
351  self.hasshass,
352  DOMAIN,
353  "Local Support for " + user_agent_id,
354  webhook_id,
355  self._handle_local_webhook_handle_local_webhook,
356  local_only=True,
357  )
358  setup_webhook_ids.append(webhook_id)
359  except ValueError:
360  _LOGGER.warning(
361  "Webhook handler %s for agent user id %s is already defined!",
362  partial_redact(webhook_id),
363  partial_redact(user_agent_id),
364  )
365  setup_successful = False
366  break
367 
368  if not setup_successful:
369  _LOGGER.warning(
370  "Local fulfillment failed to setup, falling back to cloud fulfillment"
371  )
372  for setup_webhook_id in setup_webhook_ids:
373  webhook.async_unregister(self.hasshass, setup_webhook_id)
374 
375  self._local_sdk_active_local_sdk_active = setup_successful
376 
377  @callback
378  def async_disable_local_sdk(self) -> None:
379  """Disable the local SDK."""
380  _LOGGER.debug("async_disable_local_sdk")
381  if not self._local_sdk_active_local_sdk_active:
382  return
383 
384  for agent_user_id in self.async_get_agent_usersasync_get_agent_users():
385  webhook_id = self.get_local_webhook_idget_local_webhook_id(agent_user_id)
386  _LOGGER.debug(
387  "Unregister webhook handler %s for agent user id %s",
388  partial_redact(webhook_id),
389  partial_redact(agent_user_id),
390  )
391  webhook.async_unregister(self.hasshass, webhook_id)
392 
393  self._local_sdk_active_local_sdk_active = False
394 
395  async def _handle_local_webhook(self, hass, webhook_id, request):
396  """Handle an incoming local SDK message."""
397  # Circular dep
398  # pylint: disable-next=import-outside-toplevel
399  from . import smart_home
400 
401  self._local_last_active_local_last_active = utcnow()
402 
403  # Check version local SDK.
404  version = request.headers.get("HA-Cloud-Version")
405  if not self._local_sdk_version_warn_local_sdk_version_warn and (
406  not version or AwesomeVersion(version) < LOCAL_SDK_MIN_VERSION
407  ):
408  _LOGGER.warning(
409  (
410  "Local SDK version is too old (%s), check documentation on how to"
411  " update to the latest version"
412  ),
413  version,
414  )
415  self._local_sdk_version_warn_local_sdk_version_warn = True
416 
417  payload = await request.json()
418 
419  if _LOGGER.isEnabledFor(logging.DEBUG):
420  msgid = "<UNKNOWN>"
421  if isinstance(payload, dict):
422  msgid = payload.get("requestId")
423  _LOGGER.debug(
424  "Received local message %s from %s (JS %s)",
425  msgid,
426  request.remote,
427  request.headers.get("HA-Cloud-Version", "unknown"),
428  )
429 
430  if (agent_user_id := self.get_agent_user_id_from_webhookget_agent_user_id_from_webhook(webhook_id)) is None:
431  # No agent user linked to this webhook, means that the user has somehow unregistered
432  # removing webhook and stopping processing of this request.
433  _LOGGER.error(
434  (
435  "Cannot process request for webhook %s as no linked agent user is"
436  " found:\n%s\n"
437  ),
438  partial_redact(webhook_id),
439  pprint.pformat(async_redact_msg(payload, agent_user_id)),
440  )
441  webhook.async_unregister(self.hasshass, webhook_id)
442  return None
443 
444  if not self.enabledenabled:
445  return json_response(
446  smart_home.api_disabled_response(payload, agent_user_id)
447  )
448 
449  result = await smart_home.async_handle_message(
450  self.hasshass,
451  self,
452  agent_user_id,
453  self.get_local_user_idget_local_user_id(webhook_id),
454  payload,
455  SOURCE_LOCAL,
456  )
457 
458  if _LOGGER.isEnabledFor(logging.DEBUG):
459  if isinstance(payload, dict):
460  _LOGGER.debug("Responding to local message %s", msgid)
461  else:
462  _LOGGER.debug("Empty response to local message %s", msgid)
463 
464  return json_response(result)
465 
466 
468  """Hold data associated with a particular request."""
469 
470  def __init__(
471  self,
472  config: AbstractConfig,
473  user_id: str,
474  source: str,
475  request_id: str,
476  devices: list[dict] | None,
477  ) -> None:
478  """Initialize the request data."""
479  self.configconfig = config
480  self.sourcesource = source
481  self.request_idrequest_id = request_id
482  self.contextcontext = Context(user_id=user_id)
483  self.devicesdevices = devices
484 
485  @property
486  def is_local_request(self):
487  """Return if this is a local request."""
488  return self.sourcesource == SOURCE_LOCAL
489 
490 
491 def get_google_type(domain, device_class):
492  """Google type based on domain and device class."""
493  typ = DEVICE_CLASS_TO_GOOGLE_TYPES.get((domain, device_class))
494 
495  return typ if typ is not None else DOMAIN_TO_GOOGLE_TYPES[domain]
496 
497 
498 @lru_cache(maxsize=4096)
499 def supported_traits_for_state(state: State) -> list[type[trait._Trait]]:
500  """Return all supported traits for state."""
501  domain = state.domain
502  attributes = state.attributes
503  features = attributes.get(ATTR_SUPPORTED_FEATURES, 0)
504 
505  if not isinstance(features, int):
506  _LOGGER.warning(
507  "Entity %s contains invalid supported_features value %s",
508  state.entity_id,
509  features,
510  )
511  return []
512 
513  device_class = state.attributes.get(ATTR_DEVICE_CLASS)
514  return [
515  Trait
516  for Trait in trait.TRAITS
517  if Trait.supported(domain, features, device_class, attributes)
518  ]
519 
520 
522  """Adaptation of Entity expressed in Google's terms."""
523 
524  __slots__ = ("hass", "config", "state", "entity_id", "_traits")
525 
526  def __init__(
527  self, hass: HomeAssistant, config: AbstractConfig, state: State
528  ) -> None:
529  """Initialize a Google entity."""
530  self.hasshass = hass
531  self.configconfig = config
532  self.statestate = state
533  self.entity_identity_id = state.entity_id
534  self._traits_traits: list[trait._Trait] | None = None
535 
536  def __repr__(self) -> str:
537  """Return the representation."""
538  return f"<GoogleEntity {self.state.entity_id}: {self.state.name}>"
539 
540  @callback
541  def traits(self) -> list[trait._Trait]:
542  """Return traits for entity."""
543  if self._traits_traits is not None:
544  return self._traits_traits
545  state = self.statestate
546  self._traits_traits = [
547  Trait(self.hasshass, state, self.configconfig)
548  for Trait in supported_traits_for_state(state)
549  ]
550  return self._traits_traits
551 
552  @callback
553  def should_expose(self):
554  """If entity should be exposed."""
555  return self.configconfig.should_expose(self.statestate)
556 
557  @callback
558  def should_expose_local(self) -> bool:
559  """Return if the entity should be exposed locally."""
560  return (
561  self.should_exposeshould_expose()
562  and get_google_type(
563  self.statestate.domain, self.statestate.attributes.get(ATTR_DEVICE_CLASS)
564  )
565  not in NOT_EXPOSE_LOCAL
566  and not self.might_2famight_2fa()
567  )
568 
569  @callback
570  def is_supported(self) -> bool:
571  """Return if entity is supported."""
572  return bool(self.traitstraits())
573 
574  @callback
575  def might_2fa(self) -> bool:
576  """Return if the entity might encounter 2FA."""
577  if not self.configconfig.should_2fa(self.statestate):
578  return False
579 
580  return self.might_2fa_traitsmight_2fa_traits()
581 
582  @callback
583  def might_2fa_traits(self) -> bool:
584  """Return if the entity might encounter 2FA based on just traits."""
585  state = self.statestate
586  domain = state.domain
587  features = state.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
588  device_class = state.attributes.get(ATTR_DEVICE_CLASS)
589 
590  return any(
591  trait.might_2fa(domain, features, device_class) for trait in self.traitstraits()
592  )
593 
594  def sync_serialize(self, agent_user_id, instance_uuid):
595  """Serialize entity for a SYNC response.
596 
597  https://developers.google.com/actions/smarthome/create-app#actiondevicessync
598  """
599  state = self.statestate
600  traits = self.traitstraits()
601  entity_config = self.configconfig.entity_config.get(state.entity_id, {})
602  name = (entity_config.get(CONF_NAME) or state.name).strip()
603 
604  # Find entity/device/area registry entries
605  entity_entry, device_entry, area_entry = _get_registry_entries(
606  self.hasshass, self.entity_identity_id
607  )
608 
609  # Build the device info
610  device = {
611  "id": state.entity_id,
612  "name": {"name": name},
613  "attributes": {},
614  "traits": [trait.name for trait in traits],
615  "willReportState": self.configconfig.should_report_state,
616  "type": get_google_type(
617  state.domain, state.attributes.get(ATTR_DEVICE_CLASS)
618  ),
619  }
620  # Add aliases
621  if (config_aliases := entity_config.get(CONF_ALIASES, [])) or (
622  entity_entry and entity_entry.aliases
623  ):
624  device["name"]["nicknames"] = [name, *config_aliases]
625  if entity_entry:
626  device["name"]["nicknames"].extend(entity_entry.aliases)
627 
628  # Add local SDK info if enabled
629  if self.configconfig.is_local_sdk_active and self.should_expose_localshould_expose_local():
630  device["otherDeviceIds"] = [{"deviceId": self.entity_identity_id}]
631  device["customData"] = {
632  "webhookId": self.configconfig.get_local_webhook_id(agent_user_id),
633  "httpPort": URL(get_url(self.hasshass, allow_external=False)).port,
634  "uuid": instance_uuid,
635  }
636 
637  # Add trait sync attributes
638  for trt in traits:
639  device["attributes"].update(trt.sync_attributes())
640 
641  # Add trait options
642  for trt in traits:
643  device.update(trt.sync_options())
644 
645  # Add roomhint
646  if room := entity_config.get(CONF_ROOM_HINT):
647  device["roomHint"] = room
648  elif area_entry and area_entry.name:
649  device["roomHint"] = area_entry.name
650 
651  if not device_entry:
652  return device
653 
654  # Add Matter info
655  if "matter" in self.hasshass.config.components and any(
656  x for x in device_entry.identifiers if x[0] == "matter"
657  ):
658  # pylint: disable-next=import-outside-toplevel
659  from homeassistant.components.matter import get_matter_device_info
660 
661  # Import matter can block the event loop for multiple seconds
662  # so we import it here to avoid blocking the event loop during
663  # setup since google_assistant is imported from cloud.
664  if matter_info := get_matter_device_info(self.hasshass, device_entry.id):
665  device["matterUniqueId"] = matter_info["unique_id"]
666  device["matterOriginalVendorId"] = matter_info["vendor_id"]
667  device["matterOriginalProductId"] = matter_info["product_id"]
668 
669  # Add deviceInfo
670  device_info = {}
671 
672  if device_entry.manufacturer:
673  device_info["manufacturer"] = device_entry.manufacturer
674  if device_entry.model:
675  device_info["model"] = device_entry.model
676  if device_entry.sw_version:
677  device_info["swVersion"] = device_entry.sw_version
678 
679  if device_info:
680  device["deviceInfo"] = device_info
681 
682  return device
683 
684  @callback
685  def query_serialize(self):
686  """Serialize entity for a QUERY response.
687 
688  https://developers.google.com/actions/smarthome/create-app#actiondevicesquery
689  """
690  state = self.statestate
691 
692  if state.state == STATE_UNAVAILABLE:
693  return {"online": False}
694 
695  attrs = {"online": True}
696 
697  for trt in self.traitstraits():
698  deep_update(attrs, trt.query_attributes())
699 
700  return attrs
701 
702  @callback
703  def notifications_serialize(self) -> dict[str, Any] | None:
704  """Serialize the payload for notifications to be sent."""
705  notifications: dict[str, Any] = {}
706 
707  for trt in self.traitstraits():
708  deep_update(notifications, trt.query_notifications() or {})
709 
710  return notifications or None
711 
712  @callback
714  """Serialize entity for a REACHABLE_DEVICE response."""
715  return {"verificationId": self.entity_identity_id}
716 
717  async def execute(self, data, command_payload):
718  """Execute a command.
719 
720  https://developers.google.com/actions/smarthome/create-app#actiondevicesexecute
721  """
722  command = command_payload["command"]
723  params = command_payload.get("params", {})
724  challenge = command_payload.get("challenge", {})
725  executed = False
726  for trt in self.traitstraits():
727  if trt.can_execute(command, params):
728  await trt.execute(command, data, params, challenge)
729  executed = True
730  break
731 
732  if not executed:
733  raise SmartHomeError(
734  ERR_FUNCTION_NOT_SUPPORTED,
735  f"Unable to execute {command} for {self.state.entity_id}",
736  )
737 
738  @callback
739  def async_update(self):
740  """Update the entity with latest info from Home Assistant."""
741  self.statestate = self.hasshass.states.get(self.entity_identity_id)
742 
743  if self._traits_traits is None:
744  return
745 
746  for trt in self._traits_traits:
747  trt.state = self.statestate
748 
749 
750 def deep_update(target, source):
751  """Update a nested dictionary with another nested dictionary."""
752  for key, value in source.items():
753  if isinstance(value, Mapping):
754  target[key] = deep_update(target.get(key, {}), value)
755  else:
756  target[key] = value
757  return target
758 
759 
760 @callback
762  hass: HomeAssistant, config: AbstractConfig, state: State
763 ) -> GoogleEntity | None:
764  """Return a GoogleEntity if entity is supported checking the cache first.
765 
766  This function will check the cache, and call async_get_google_entity_if_supported
767  if the entity is not in the cache, which will update the cache.
768  """
769  entity_id = state.entity_id
770  is_supported_cache = config.is_supported_cache
771  features: int | None = state.attributes.get(ATTR_SUPPORTED_FEATURES)
772  if result := is_supported_cache.get(entity_id):
773  cached_features, supported = result
774  if cached_features == features:
775  return GoogleEntity(hass, config, state) if supported else None
776  # Cache miss, check if entity is supported
777  return async_get_google_entity_if_supported(hass, config, state)
778 
779 
780 @callback
782  hass: HomeAssistant, config: AbstractConfig, state: State
783 ) -> GoogleEntity | None:
784  """Return a GoogleEntity if entity is supported.
785 
786  This function will update the cache, but it does not check the cache first.
787  """
788  features: int | None = state.attributes.get(ATTR_SUPPORTED_FEATURES)
789  entity = GoogleEntity(hass, config, state)
790  is_supported = bool(entity.traits())
791  config.is_supported_cache[state.entity_id] = (features, is_supported)
792  return entity if is_supported else None
793 
794 
795 @callback
797  hass: HomeAssistant, config: AbstractConfig
798 ) -> list[GoogleEntity]:
799  """Return all entities that are supported by Google."""
800  entities: list[GoogleEntity] = []
801  is_supported_cache = config.is_supported_cache
802  for state in hass.states.async_all():
803  entity_id = state.entity_id
804  if entity_id in CLOUD_NEVER_EXPOSED_ENTITIES:
805  continue
806  # Check check inlined for performance to avoid
807  # function calls for every entity since we enumerate
808  # the entire state machine here
809  features: int | None = state.attributes.get(ATTR_SUPPORTED_FEATURES)
810  if result := is_supported_cache.get(entity_id):
811  cached_features, supported = result
812  if cached_features == features:
813  if supported:
814  entities.append(GoogleEntity(hass, config, state))
815  continue
816  # Cached features don't match, fall through to check
817  # if the entity is supported and update the cache.
818  if entity := async_get_google_entity_if_supported(hass, config, state):
819  entities.append(entity)
820  return entities
HTTPStatus|None async_report_state(self, dict[str, Any] message, str agent_user_id, str|None event_id=None)
Definition: helpers.py:200
HTTPStatus async_sync_notification(self, str agent_user_id, str event_id, dict[str, Any] payload)
Definition: helpers.py:252
HTTPStatus async_sync_notification_all(self, str event_id, dict[str, Any] payload)
Definition: helpers.py:264
def _handle_local_webhook(self, hass, webhook_id, request)
Definition: helpers.py:395
None __init__(self, HomeAssistant hass, AbstractConfig config, State state)
Definition: helpers.py:528
def sync_serialize(self, agent_user_id, instance_uuid)
Definition: helpers.py:594
None __init__(self, AbstractConfig config, str user_id, str source, str request_id, list[dict]|None devices)
Definition: helpers.py:477
dict[str, Any] async_redact_msg(dict[str, Any] msg, str agent_user_id)
GoogleEntity|None async_get_google_entity_if_supported_cached(HomeAssistant hass, AbstractConfig config, State state)
Definition: helpers.py:763
list[GoogleEntity] async_get_entities(HomeAssistant hass, AbstractConfig config)
Definition: helpers.py:798
GoogleEntity|None async_get_google_entity_if_supported(HomeAssistant hass, AbstractConfig config, State state)
Definition: helpers.py:783
def get_google_type(domain, device_class)
Definition: helpers.py:491
tuple[ er.RegistryEntry|None, dr.DeviceEntry|None, ar.AreaEntry|None,] _get_registry_entries(HomeAssistant hass, str entity_id)
Definition: helpers.py:66
list[type[trait._Trait]] supported_traits_for_state(State state)
Definition: helpers.py:499
IssData update(pyiss.ISS iss)
Definition: __init__.py:33
MatterDeviceInfo|None get_matter_device_info(HomeAssistant hass, str device_id)
Definition: __init__.py:52
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
str get_url(HomeAssistant hass, *bool require_current_request=False, bool require_ssl=False, bool require_standard_port=False, bool require_cloud=False, bool allow_internal=True, bool allow_external=True, bool allow_cloud=True, bool|None allow_ip=None, bool|None prefer_external=None, bool prefer_cloud=False)
Definition: network.py:131
str partial_redact(str|Any x, int unmasked_prefix=4, int unmasked_suffix=4)
Definition: redact.py:15