Home Assistant Unofficial Reference 2024.12.1
analytics.py
Go to the documentation of this file.
1 """Analytics helper class for the analytics integration."""
2 
3 from __future__ import annotations
4 
5 import asyncio
6 from asyncio import timeout
7 from dataclasses import asdict as dataclass_asdict, dataclass
8 from datetime import datetime
9 from typing import Any
10 import uuid
11 
12 import aiohttp
13 
14 from homeassistant.components import hassio
15 from homeassistant.components.api import ATTR_INSTALLATION_TYPE
16 from homeassistant.components.automation import DOMAIN as AUTOMATION_DOMAIN
18  DOMAIN as ENERGY_DOMAIN,
19  is_configured as energy_is_configured,
20 )
22  DOMAIN as RECORDER_DOMAIN,
23  get_instance as get_recorder_instance,
24 )
25 import homeassistant.config as conf_util
26 from homeassistant.config_entries import SOURCE_IGNORE
27 from homeassistant.const import ATTR_DOMAIN, __version__ as HA_VERSION
28 from homeassistant.core import HomeAssistant, callback
29 from homeassistant.exceptions import HomeAssistantError
30 from homeassistant.helpers.aiohttp_client import async_get_clientsession
32 from homeassistant.helpers.hassio import is_hassio
33 from homeassistant.helpers.storage import Store
34 from homeassistant.helpers.system_info import async_get_system_info
35 from homeassistant.loader import (
36  Integration,
37  IntegrationNotFound,
38  async_get_integrations,
39 )
40 from homeassistant.setup import async_get_loaded_integrations
41 
42 from .const import (
43  ANALYTICS_ENDPOINT_URL,
44  ANALYTICS_ENDPOINT_URL_DEV,
45  ATTR_ADDON_COUNT,
46  ATTR_ADDONS,
47  ATTR_ARCH,
48  ATTR_AUTO_UPDATE,
49  ATTR_AUTOMATION_COUNT,
50  ATTR_BASE,
51  ATTR_BOARD,
52  ATTR_CERTIFICATE,
53  ATTR_CONFIGURED,
54  ATTR_CUSTOM_INTEGRATIONS,
55  ATTR_DIAGNOSTICS,
56  ATTR_ENERGY,
57  ATTR_ENGINE,
58  ATTR_HEALTHY,
59  ATTR_INTEGRATION_COUNT,
60  ATTR_INTEGRATIONS,
61  ATTR_OPERATING_SYSTEM,
62  ATTR_PROTECTED,
63  ATTR_RECORDER,
64  ATTR_SLUG,
65  ATTR_STATE_COUNT,
66  ATTR_STATISTICS,
67  ATTR_SUPERVISOR,
68  ATTR_SUPPORTED,
69  ATTR_USAGE,
70  ATTR_USER_COUNT,
71  ATTR_UUID,
72  ATTR_VERSION,
73  LOGGER,
74  PREFERENCE_SCHEMA,
75  STORAGE_KEY,
76  STORAGE_VERSION,
77 )
78 
79 
80 @dataclass
82  """Analytics data."""
83 
84  onboarded: bool
85  preferences: dict[str, bool]
86  uuid: str | None
87 
88  @classmethod
89  def from_dict(cls, data: dict[str, Any]) -> AnalyticsData:
90  """Initialize analytics data from a dict."""
91  return cls(
92  data["onboarded"],
93  data["preferences"],
94  data["uuid"],
95  )
96 
97 
98 class Analytics:
99  """Analytics helper class for the analytics integration."""
100 
101  def __init__(self, hass: HomeAssistant) -> None:
102  """Initialize the Analytics class."""
103  self.hass: HomeAssistant = hass
104  self.sessionsession = async_get_clientsession(hass)
105  self._data_data = AnalyticsData(False, {}, None)
106  self._store_store = Store[dict[str, Any]](hass, STORAGE_VERSION, STORAGE_KEY)
107 
108  @property
109  def preferences(self) -> dict:
110  """Return the current active preferences."""
111  preferences = self._data_data.preferences
112  return {
113  ATTR_BASE: preferences.get(ATTR_BASE, False),
114  ATTR_DIAGNOSTICS: preferences.get(ATTR_DIAGNOSTICS, False),
115  ATTR_USAGE: preferences.get(ATTR_USAGE, False),
116  ATTR_STATISTICS: preferences.get(ATTR_STATISTICS, False),
117  }
118 
119  @property
120  def onboarded(self) -> bool:
121  """Return bool if the user has made a choice."""
122  return self._data_data.onboarded
123 
124  @property
125  def uuid(self) -> str | None:
126  """Return the uuid for the analytics integration."""
127  return self._data_data.uuid
128 
129  @property
130  def endpoint(self) -> str:
131  """Return the endpoint that will receive the payload."""
132  if HA_VERSION.endswith("0.dev0"):
133  # dev installations will contact the dev analytics environment
134  return ANALYTICS_ENDPOINT_URL_DEV
135  return ANALYTICS_ENDPOINT_URL
136 
137  @property
138  def supervisor(self) -> bool:
139  """Return bool if a supervisor is present."""
140  return is_hassio(self.hass)
141 
142  async def load(self) -> None:
143  """Load preferences."""
144  stored = await self._store_store.async_load()
145  if stored:
146  self._data_data = AnalyticsData.from_dict(stored)
147 
148  if (
149  self.supervisorsupervisor
150  and (supervisor_info := hassio.get_supervisor_info(self.hass)) is not None
151  ):
152  if not self.onboardedonboarded:
153  # User have not configured analytics, get this setting from the supervisor
154  if supervisor_info[ATTR_DIAGNOSTICS] and not self.preferencespreferences.get(
155  ATTR_DIAGNOSTICS, False
156  ):
157  self._data_data.preferences[ATTR_DIAGNOSTICS] = True
158  elif not supervisor_info[ATTR_DIAGNOSTICS] and self.preferencespreferences.get(
159  ATTR_DIAGNOSTICS, False
160  ):
161  self._data_data.preferences[ATTR_DIAGNOSTICS] = False
162 
163  async def save_preferences(self, preferences: dict) -> None:
164  """Save preferences."""
165  preferences = PREFERENCE_SCHEMA(preferences)
166  self._data_data.preferences.update(preferences)
167  self._data_data.onboarded = True
168 
169  await self._store_store.async_save(dataclass_asdict(self._data_data))
170 
171  if self.supervisorsupervisor:
172  await hassio.async_update_diagnostics(
173  self.hass, self.preferencespreferences.get(ATTR_DIAGNOSTICS, False)
174  )
175 
176  async def send_analytics(self, _: datetime | None = None) -> None:
177  """Send analytics."""
178  hass = self.hass
179  supervisor_info = None
180  operating_system_info: dict[str, Any] = {}
181 
182  if not self.onboardedonboarded or not self.preferencespreferences.get(ATTR_BASE, False):
183  LOGGER.debug("Nothing to submit")
184  return
185 
186  if self._data_data.uuid is None:
187  self._data_data.uuid = uuid.uuid4().hex
188  await self._store_store.async_save(dataclass_asdict(self._data_data))
189 
190  if self.supervisorsupervisor:
191  supervisor_info = hassio.get_supervisor_info(hass)
192  operating_system_info = hassio.get_os_info(hass) or {}
193 
194  system_info = await async_get_system_info(hass)
195  integrations = []
196  custom_integrations = []
197  addons: list[dict[str, Any]] = []
198  payload: dict = {
199  ATTR_UUID: self.uuiduuid,
200  ATTR_VERSION: HA_VERSION,
201  ATTR_INSTALLATION_TYPE: system_info[ATTR_INSTALLATION_TYPE],
202  }
203 
204  if supervisor_info is not None:
205  payload[ATTR_SUPERVISOR] = {
206  ATTR_HEALTHY: supervisor_info[ATTR_HEALTHY],
207  ATTR_SUPPORTED: supervisor_info[ATTR_SUPPORTED],
208  ATTR_ARCH: supervisor_info[ATTR_ARCH],
209  }
210 
211  if operating_system_info.get(ATTR_BOARD) is not None:
212  payload[ATTR_OPERATING_SYSTEM] = {
213  ATTR_BOARD: operating_system_info[ATTR_BOARD],
214  ATTR_VERSION: operating_system_info[ATTR_VERSION],
215  }
216 
217  if self.preferencespreferences.get(ATTR_USAGE, False) or self.preferencespreferences.get(
218  ATTR_STATISTICS, False
219  ):
220  ent_reg = er.async_get(hass)
221 
222  try:
223  yaml_configuration = await conf_util.async_hass_config_yaml(hass)
224  except HomeAssistantError as err:
225  LOGGER.error(err)
226  return
227 
228  configuration_set = set(yaml_configuration)
229  er_platforms = {
230  entity.platform
231  for entity in ent_reg.entities.values()
232  if not entity.disabled
233  }
234 
235  domains = async_get_loaded_integrations(hass)
236  configured_integrations = await async_get_integrations(hass, domains)
237  enabled_domains = set(configured_integrations)
238 
239  for integration in configured_integrations.values():
240  if isinstance(integration, IntegrationNotFound):
241  continue
242 
243  if isinstance(integration, BaseException):
244  raise integration
245 
246  if not self._async_should_report_integration_async_should_report_integration(
247  integration=integration,
248  yaml_domains=configuration_set,
249  entity_registry_platforms=er_platforms,
250  ):
251  continue
252 
253  if not integration.is_built_in:
254  custom_integrations.append(
255  {
256  ATTR_DOMAIN: integration.domain,
257  ATTR_VERSION: integration.version,
258  }
259  )
260  continue
261 
262  integrations.append(integration.domain)
263 
264  if supervisor_info is not None:
265  supervisor_client = hassio.get_supervisor_client(hass)
266  installed_addons = await asyncio.gather(
267  *(
268  supervisor_client.addons.addon_info(addon[ATTR_SLUG])
269  for addon in supervisor_info[ATTR_ADDONS]
270  )
271  )
272  addons.extend(
273  {
274  ATTR_SLUG: addon.slug,
275  ATTR_PROTECTED: addon.protected,
276  ATTR_VERSION: addon.version,
277  ATTR_AUTO_UPDATE: addon.auto_update,
278  }
279  for addon in installed_addons
280  )
281 
282  if self.preferencespreferences.get(ATTR_USAGE, False):
283  payload[ATTR_CERTIFICATE] = hass.http.ssl_certificate is not None
284  payload[ATTR_INTEGRATIONS] = integrations
285  payload[ATTR_CUSTOM_INTEGRATIONS] = custom_integrations
286  if supervisor_info is not None:
287  payload[ATTR_ADDONS] = addons
288 
289  if ENERGY_DOMAIN in enabled_domains:
290  payload[ATTR_ENERGY] = {
291  ATTR_CONFIGURED: await energy_is_configured(hass)
292  }
293 
294  if RECORDER_DOMAIN in enabled_domains:
295  instance = get_recorder_instance(hass)
296  engine = instance.database_engine
297  if engine and engine.version is not None:
298  payload[ATTR_RECORDER] = {
299  ATTR_ENGINE: engine.dialect.value,
300  ATTR_VERSION: engine.version,
301  }
302 
303  if self.preferencespreferences.get(ATTR_STATISTICS, False):
304  payload[ATTR_STATE_COUNT] = hass.states.async_entity_ids_count()
305  payload[ATTR_AUTOMATION_COUNT] = hass.states.async_entity_ids_count(
306  AUTOMATION_DOMAIN
307  )
308  payload[ATTR_INTEGRATION_COUNT] = len(integrations)
309  if supervisor_info is not None:
310  payload[ATTR_ADDON_COUNT] = len(addons)
311  payload[ATTR_USER_COUNT] = len(
312  [
313  user
314  for user in await hass.auth.async_get_users()
315  if not user.system_generated
316  ]
317  )
318 
319  try:
320  async with timeout(30):
321  response = await self.sessionsession.post(self.endpointendpoint, json=payload)
322  if response.status == 200:
323  LOGGER.info(
324  (
325  "Submitted analytics to Home Assistant servers. "
326  "Information submitted includes %s"
327  ),
328  payload,
329  )
330  else:
331  LOGGER.warning(
332  "Sending analytics failed with statuscode %s from %s",
333  response.status,
334  self.endpointendpoint,
335  )
336  except TimeoutError:
337  LOGGER.error("Timeout sending analytics to %s", ANALYTICS_ENDPOINT_URL)
338  except aiohttp.ClientError as err:
339  LOGGER.error(
340  "Error sending analytics to %s: %r", ANALYTICS_ENDPOINT_URL, err
341  )
342 
343  @callback
345  self,
346  integration: Integration,
347  yaml_domains: set[str],
348  entity_registry_platforms: set[str],
349  ) -> bool:
350  """Return a bool to indicate if this integration should be reported."""
351  if integration.disabled:
352  return False
353 
354  # Check if the integration is defined in YAML or in the entity registry
355  if (
356  integration.domain in yaml_domains
357  or integration.domain in entity_registry_platforms
358  ):
359  return True
360 
361  # Check if the integration provide a config flow
362  if not integration.config_flow:
363  return False
364 
365  entries = self.hass.config_entries.async_entries(integration.domain)
366 
367  # Filter out ignored and disabled entries
368  return any(
369  entry
370  for entry in entries
371  if entry.source != SOURCE_IGNORE and entry.disabled_by is None
372  )
AnalyticsData from_dict(cls, dict[str, Any] data)
Definition: analytics.py:89
None send_analytics(self, datetime|None _=None)
Definition: analytics.py:176
bool _async_should_report_integration(self, Integration integration, set[str] yaml_domains, set[str] entity_registry_platforms)
Definition: analytics.py:349
web.Response post(self, web.Request request, str config_key)
Definition: view.py:101
web.Response get(self, web.Request request, str config_key)
Definition: view.py:88
bool is_hassio(HomeAssistant hass)
Definition: __init__.py:302
aiohttp.ClientSession async_get_clientsession(HomeAssistant hass, bool verify_ssl=True, socket.AddressFamily family=socket.AF_UNSPEC, ssl_util.SSLCipherList ssl_cipher=ssl_util.SSLCipherList.PYTHON_DEFAULT)
None async_load(HomeAssistant hass)
None async_save(self, _T data)
Definition: storage.py:424
dict[str, Any] async_get_system_info(HomeAssistant hass)
Definition: system_info.py:44
dict[str, Integration|Exception] async_get_integrations(HomeAssistant hass, Iterable[str] domains)
Definition: loader.py:1368
set[str] async_get_loaded_integrations(core.HomeAssistant hass)
Definition: setup.py:651