mirror of
https://github.com/home-assistant/core.git
synced 2025-10-06 02:09:28 +00:00
719 lines
24 KiB
Python
719 lines
24 KiB
Python
"""Analytics helper class for the analytics integration."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import asyncio
|
|
from asyncio import timeout
|
|
from collections.abc import Awaitable, Callable, Iterable, Mapping
|
|
from dataclasses import asdict as dataclass_asdict, dataclass, field
|
|
from datetime import datetime
|
|
from typing import Any, Protocol
|
|
import uuid
|
|
|
|
import aiohttp
|
|
|
|
from homeassistant import config as conf_util
|
|
from homeassistant.components import hassio
|
|
from homeassistant.components.api import ATTR_INSTALLATION_TYPE
|
|
from homeassistant.components.automation import DOMAIN as AUTOMATION_DOMAIN
|
|
from homeassistant.components.energy import (
|
|
DOMAIN as ENERGY_DOMAIN,
|
|
is_configured as energy_is_configured,
|
|
)
|
|
from homeassistant.components.recorder import (
|
|
DOMAIN as RECORDER_DOMAIN,
|
|
get_instance as get_recorder_instance,
|
|
)
|
|
from homeassistant.config_entries import SOURCE_IGNORE
|
|
from homeassistant.const import (
|
|
ATTR_ASSUMED_STATE,
|
|
ATTR_DOMAIN,
|
|
BASE_PLATFORMS,
|
|
__version__ as HA_VERSION,
|
|
)
|
|
from homeassistant.core import HomeAssistant, callback
|
|
from homeassistant.exceptions import HomeAssistantError
|
|
from homeassistant.helpers import device_registry as dr, entity_registry as er
|
|
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
|
from homeassistant.helpers.hassio import is_hassio
|
|
from homeassistant.helpers.singleton import singleton
|
|
from homeassistant.helpers.storage import Store
|
|
from homeassistant.helpers.system_info import async_get_system_info
|
|
from homeassistant.helpers.typing import UNDEFINED
|
|
from homeassistant.loader import (
|
|
Integration,
|
|
IntegrationNotFound,
|
|
async_get_integration,
|
|
async_get_integrations,
|
|
)
|
|
from homeassistant.setup import async_get_loaded_integrations
|
|
|
|
from .const import (
|
|
ANALYTICS_ENDPOINT_URL,
|
|
ANALYTICS_ENDPOINT_URL_DEV,
|
|
ATTR_ADDON_COUNT,
|
|
ATTR_ADDONS,
|
|
ATTR_ARCH,
|
|
ATTR_AUTO_UPDATE,
|
|
ATTR_AUTOMATION_COUNT,
|
|
ATTR_BASE,
|
|
ATTR_BOARD,
|
|
ATTR_CERTIFICATE,
|
|
ATTR_CONFIGURED,
|
|
ATTR_CUSTOM_INTEGRATIONS,
|
|
ATTR_DIAGNOSTICS,
|
|
ATTR_ENERGY,
|
|
ATTR_ENGINE,
|
|
ATTR_HEALTHY,
|
|
ATTR_INTEGRATION_COUNT,
|
|
ATTR_INTEGRATIONS,
|
|
ATTR_OPERATING_SYSTEM,
|
|
ATTR_PROTECTED,
|
|
ATTR_RECORDER,
|
|
ATTR_SLUG,
|
|
ATTR_STATE_COUNT,
|
|
ATTR_STATISTICS,
|
|
ATTR_SUPERVISOR,
|
|
ATTR_SUPPORTED,
|
|
ATTR_USAGE,
|
|
ATTR_USER_COUNT,
|
|
ATTR_UUID,
|
|
ATTR_VERSION,
|
|
DOMAIN,
|
|
LOGGER,
|
|
PREFERENCE_SCHEMA,
|
|
STORAGE_KEY,
|
|
STORAGE_VERSION,
|
|
)
|
|
|
|
DATA_ANALYTICS_MODIFIERS = "analytics_modifiers"
|
|
|
|
type AnalyticsModifier = Callable[
|
|
[HomeAssistant, AnalyticsInput], Awaitable[AnalyticsModifications]
|
|
]
|
|
|
|
|
|
@singleton(DATA_ANALYTICS_MODIFIERS)
|
|
def _async_get_modifiers(
|
|
hass: HomeAssistant,
|
|
) -> dict[str, AnalyticsModifier | None]:
|
|
"""Return the analytics modifiers."""
|
|
return {}
|
|
|
|
|
|
@dataclass
|
|
class AnalyticsInput:
|
|
"""Analytics input for a single integration.
|
|
|
|
This is sent to integrations that implement the platform.
|
|
"""
|
|
|
|
device_ids: Iterable[str] = field(default_factory=list)
|
|
entity_ids: Iterable[str] = field(default_factory=list)
|
|
|
|
|
|
@dataclass
|
|
class AnalyticsModifications:
|
|
"""Analytics config for a single integration.
|
|
|
|
This is used by integrations that implement the platform.
|
|
"""
|
|
|
|
remove: bool = False
|
|
devices: Mapping[str, DeviceAnalyticsModifications] | None = None
|
|
entities: Mapping[str, EntityAnalyticsModifications] | None = None
|
|
|
|
|
|
@dataclass
|
|
class DeviceAnalyticsModifications:
|
|
"""Analytics config for a single device.
|
|
|
|
This is used by integrations that implement the platform.
|
|
"""
|
|
|
|
remove: bool = False
|
|
|
|
|
|
@dataclass
|
|
class EntityAnalyticsModifications:
|
|
"""Analytics config for a single entity.
|
|
|
|
This is used by integrations that implement the platform.
|
|
"""
|
|
|
|
remove: bool = False
|
|
|
|
|
|
class AnalyticsPlatformProtocol(Protocol):
|
|
"""Define the format of analytics platforms."""
|
|
|
|
async def async_modify_analytics(
|
|
self,
|
|
hass: HomeAssistant,
|
|
analytics_input: AnalyticsInput,
|
|
) -> AnalyticsModifications:
|
|
"""Modify the analytics."""
|
|
|
|
|
|
async def _async_get_analytics_platform(
|
|
hass: HomeAssistant, domain: str
|
|
) -> AnalyticsPlatformProtocol | None:
|
|
"""Get analytics platform."""
|
|
try:
|
|
integration = await async_get_integration(hass, domain)
|
|
except IntegrationNotFound:
|
|
return None
|
|
try:
|
|
return await integration.async_get_platform(DOMAIN)
|
|
except ImportError:
|
|
return None
|
|
|
|
|
|
async def _async_get_modifier(
|
|
hass: HomeAssistant, domain: str
|
|
) -> AnalyticsModifier | None:
|
|
"""Get analytics modifier."""
|
|
modifiers = _async_get_modifiers(hass)
|
|
modifier = modifiers.get(domain, UNDEFINED)
|
|
|
|
if modifier is not UNDEFINED:
|
|
return modifier
|
|
|
|
platform = await _async_get_analytics_platform(hass, domain)
|
|
if platform is None:
|
|
modifiers[domain] = None
|
|
return None
|
|
|
|
modifier = getattr(platform, "async_modify_analytics", None)
|
|
modifiers[domain] = modifier
|
|
return modifier
|
|
|
|
|
|
def gen_uuid() -> str:
|
|
"""Generate a new UUID."""
|
|
return uuid.uuid4().hex
|
|
|
|
|
|
@dataclass
|
|
class AnalyticsData:
|
|
"""Analytics data."""
|
|
|
|
onboarded: bool
|
|
preferences: dict[str, bool]
|
|
uuid: str | None
|
|
|
|
@classmethod
|
|
def from_dict(cls, data: dict[str, Any]) -> AnalyticsData:
|
|
"""Initialize analytics data from a dict."""
|
|
return cls(
|
|
data["onboarded"],
|
|
data["preferences"],
|
|
data["uuid"],
|
|
)
|
|
|
|
|
|
class Analytics:
|
|
"""Analytics helper class for the analytics integration."""
|
|
|
|
def __init__(self, hass: HomeAssistant) -> None:
|
|
"""Initialize the Analytics class."""
|
|
self.hass: HomeAssistant = hass
|
|
self.session = async_get_clientsession(hass)
|
|
self._data = AnalyticsData(False, {}, None)
|
|
self._store = Store[dict[str, Any]](hass, STORAGE_VERSION, STORAGE_KEY)
|
|
|
|
@property
|
|
def preferences(self) -> dict:
|
|
"""Return the current active preferences."""
|
|
preferences = self._data.preferences
|
|
return {
|
|
ATTR_BASE: preferences.get(ATTR_BASE, False),
|
|
ATTR_DIAGNOSTICS: preferences.get(ATTR_DIAGNOSTICS, False),
|
|
ATTR_USAGE: preferences.get(ATTR_USAGE, False),
|
|
ATTR_STATISTICS: preferences.get(ATTR_STATISTICS, False),
|
|
}
|
|
|
|
@property
|
|
def onboarded(self) -> bool:
|
|
"""Return bool if the user has made a choice."""
|
|
return self._data.onboarded
|
|
|
|
@property
|
|
def uuid(self) -> str | None:
|
|
"""Return the uuid for the analytics integration."""
|
|
return self._data.uuid
|
|
|
|
@property
|
|
def endpoint(self) -> str:
|
|
"""Return the endpoint that will receive the payload."""
|
|
if HA_VERSION.endswith("0.dev0"):
|
|
# dev installations will contact the dev analytics environment
|
|
return ANALYTICS_ENDPOINT_URL_DEV
|
|
return ANALYTICS_ENDPOINT_URL
|
|
|
|
@property
|
|
def supervisor(self) -> bool:
|
|
"""Return bool if a supervisor is present."""
|
|
return is_hassio(self.hass)
|
|
|
|
async def load(self) -> None:
|
|
"""Load preferences."""
|
|
stored = await self._store.async_load()
|
|
if stored:
|
|
self._data = AnalyticsData.from_dict(stored)
|
|
|
|
if (
|
|
self.supervisor
|
|
and (supervisor_info := hassio.get_supervisor_info(self.hass)) is not None
|
|
):
|
|
if not self.onboarded:
|
|
# User have not configured analytics, get this setting from the supervisor
|
|
if supervisor_info[ATTR_DIAGNOSTICS] and not self.preferences.get(
|
|
ATTR_DIAGNOSTICS, False
|
|
):
|
|
self._data.preferences[ATTR_DIAGNOSTICS] = True
|
|
elif not supervisor_info[ATTR_DIAGNOSTICS] and self.preferences.get(
|
|
ATTR_DIAGNOSTICS, False
|
|
):
|
|
self._data.preferences[ATTR_DIAGNOSTICS] = False
|
|
|
|
async def save_preferences(self, preferences: dict) -> None:
|
|
"""Save preferences."""
|
|
preferences = PREFERENCE_SCHEMA(preferences)
|
|
self._data.preferences.update(preferences)
|
|
self._data.onboarded = True
|
|
|
|
await self._store.async_save(dataclass_asdict(self._data))
|
|
|
|
if self.supervisor:
|
|
await hassio.async_update_diagnostics(
|
|
self.hass, self.preferences.get(ATTR_DIAGNOSTICS, False)
|
|
)
|
|
|
|
async def send_analytics(self, _: datetime | None = None) -> None:
|
|
"""Send analytics."""
|
|
hass = self.hass
|
|
supervisor_info = None
|
|
operating_system_info: dict[str, Any] = {}
|
|
|
|
if not self.onboarded or not self.preferences.get(ATTR_BASE, False):
|
|
LOGGER.debug("Nothing to submit")
|
|
return
|
|
|
|
if self._data.uuid is None:
|
|
self._data.uuid = gen_uuid()
|
|
await self._store.async_save(dataclass_asdict(self._data))
|
|
|
|
if self.supervisor:
|
|
supervisor_info = hassio.get_supervisor_info(hass)
|
|
operating_system_info = hassio.get_os_info(hass) or {}
|
|
|
|
system_info = await async_get_system_info(hass)
|
|
integrations = []
|
|
custom_integrations = []
|
|
addons: list[dict[str, Any]] = []
|
|
payload: dict = {
|
|
ATTR_UUID: self.uuid,
|
|
ATTR_VERSION: HA_VERSION,
|
|
ATTR_INSTALLATION_TYPE: system_info[ATTR_INSTALLATION_TYPE],
|
|
}
|
|
|
|
if supervisor_info is not None:
|
|
payload[ATTR_SUPERVISOR] = {
|
|
ATTR_HEALTHY: supervisor_info[ATTR_HEALTHY],
|
|
ATTR_SUPPORTED: supervisor_info[ATTR_SUPPORTED],
|
|
ATTR_ARCH: supervisor_info[ATTR_ARCH],
|
|
}
|
|
|
|
if operating_system_info.get(ATTR_BOARD) is not None:
|
|
payload[ATTR_OPERATING_SYSTEM] = {
|
|
ATTR_BOARD: operating_system_info[ATTR_BOARD],
|
|
ATTR_VERSION: operating_system_info[ATTR_VERSION],
|
|
}
|
|
|
|
if self.preferences.get(ATTR_USAGE, False) or self.preferences.get(
|
|
ATTR_STATISTICS, False
|
|
):
|
|
ent_reg = er.async_get(hass)
|
|
|
|
try:
|
|
yaml_configuration = await conf_util.async_hass_config_yaml(hass)
|
|
except HomeAssistantError as err:
|
|
LOGGER.error(err)
|
|
return
|
|
|
|
configuration_set = _domains_from_yaml_config(yaml_configuration)
|
|
|
|
er_platforms = {
|
|
entity.platform
|
|
for entity in ent_reg.entities.values()
|
|
if not entity.disabled
|
|
}
|
|
|
|
domains = async_get_loaded_integrations(hass)
|
|
configured_integrations = await async_get_integrations(hass, domains)
|
|
enabled_domains = set(configured_integrations)
|
|
|
|
for integration in configured_integrations.values():
|
|
if isinstance(integration, IntegrationNotFound):
|
|
continue
|
|
|
|
if isinstance(integration, BaseException):
|
|
raise integration
|
|
|
|
if not self._async_should_report_integration(
|
|
integration=integration,
|
|
yaml_domains=configuration_set,
|
|
entity_registry_platforms=er_platforms,
|
|
):
|
|
continue
|
|
|
|
if not integration.is_built_in:
|
|
custom_integrations.append(
|
|
{
|
|
ATTR_DOMAIN: integration.domain,
|
|
ATTR_VERSION: integration.version,
|
|
}
|
|
)
|
|
continue
|
|
|
|
integrations.append(integration.domain)
|
|
|
|
if supervisor_info is not None:
|
|
supervisor_client = hassio.get_supervisor_client(hass)
|
|
installed_addons = await asyncio.gather(
|
|
*(
|
|
supervisor_client.addons.addon_info(addon[ATTR_SLUG])
|
|
for addon in supervisor_info[ATTR_ADDONS]
|
|
)
|
|
)
|
|
addons.extend(
|
|
{
|
|
ATTR_SLUG: addon.slug,
|
|
ATTR_PROTECTED: addon.protected,
|
|
ATTR_VERSION: addon.version,
|
|
ATTR_AUTO_UPDATE: addon.auto_update,
|
|
}
|
|
for addon in installed_addons
|
|
)
|
|
|
|
if self.preferences.get(ATTR_USAGE, False):
|
|
payload[ATTR_CERTIFICATE] = hass.http.ssl_certificate is not None
|
|
payload[ATTR_INTEGRATIONS] = integrations
|
|
payload[ATTR_CUSTOM_INTEGRATIONS] = custom_integrations
|
|
if supervisor_info is not None:
|
|
payload[ATTR_ADDONS] = addons
|
|
|
|
if ENERGY_DOMAIN in enabled_domains:
|
|
payload[ATTR_ENERGY] = {
|
|
ATTR_CONFIGURED: await energy_is_configured(hass)
|
|
}
|
|
|
|
if RECORDER_DOMAIN in enabled_domains:
|
|
instance = get_recorder_instance(hass)
|
|
engine = instance.database_engine
|
|
if engine and engine.version is not None:
|
|
payload[ATTR_RECORDER] = {
|
|
ATTR_ENGINE: engine.dialect.value,
|
|
ATTR_VERSION: engine.version,
|
|
}
|
|
|
|
if self.preferences.get(ATTR_STATISTICS, False):
|
|
payload[ATTR_STATE_COUNT] = hass.states.async_entity_ids_count()
|
|
payload[ATTR_AUTOMATION_COUNT] = hass.states.async_entity_ids_count(
|
|
AUTOMATION_DOMAIN
|
|
)
|
|
payload[ATTR_INTEGRATION_COUNT] = len(integrations)
|
|
if supervisor_info is not None:
|
|
payload[ATTR_ADDON_COUNT] = len(addons)
|
|
payload[ATTR_USER_COUNT] = len(
|
|
[
|
|
user
|
|
for user in await hass.auth.async_get_users()
|
|
if not user.system_generated
|
|
]
|
|
)
|
|
|
|
try:
|
|
async with timeout(30):
|
|
response = await self.session.post(self.endpoint, json=payload)
|
|
if response.status == 200:
|
|
LOGGER.info(
|
|
(
|
|
"Submitted analytics to Home Assistant servers. "
|
|
"Information submitted includes %s"
|
|
),
|
|
payload,
|
|
)
|
|
else:
|
|
LOGGER.warning(
|
|
"Sending analytics failed with statuscode %s from %s",
|
|
response.status,
|
|
self.endpoint,
|
|
)
|
|
except TimeoutError:
|
|
LOGGER.error("Timeout sending analytics to %s", ANALYTICS_ENDPOINT_URL)
|
|
except aiohttp.ClientError as err:
|
|
LOGGER.error(
|
|
"Error sending analytics to %s: %r", ANALYTICS_ENDPOINT_URL, err
|
|
)
|
|
|
|
@callback
|
|
def _async_should_report_integration(
|
|
self,
|
|
integration: Integration,
|
|
yaml_domains: set[str],
|
|
entity_registry_platforms: set[str],
|
|
) -> bool:
|
|
"""Return a bool to indicate if this integration should be reported."""
|
|
if integration.disabled:
|
|
return False
|
|
|
|
# Check if the integration is defined in YAML or in the entity registry
|
|
if (
|
|
integration.domain in yaml_domains
|
|
or integration.domain in entity_registry_platforms
|
|
):
|
|
return True
|
|
|
|
# Check if the integration provide a config flow
|
|
if not integration.config_flow:
|
|
return False
|
|
|
|
entries = self.hass.config_entries.async_entries(integration.domain)
|
|
|
|
# Filter out ignored and disabled entries
|
|
return any(
|
|
entry
|
|
for entry in entries
|
|
if entry.source != SOURCE_IGNORE and entry.disabled_by is None
|
|
)
|
|
|
|
|
|
def _domains_from_yaml_config(yaml_configuration: dict[str, Any]) -> set[str]:
|
|
"""Extract domains from the YAML configuration."""
|
|
domains = set(yaml_configuration)
|
|
for platforms in conf_util.extract_platform_integrations(
|
|
yaml_configuration, BASE_PLATFORMS
|
|
).values():
|
|
domains.update(platforms)
|
|
return domains
|
|
|
|
|
|
DEFAULT_ANALYTICS_CONFIG = AnalyticsModifications()
|
|
DEFAULT_DEVICE_ANALYTICS_CONFIG = DeviceAnalyticsModifications()
|
|
DEFAULT_ENTITY_ANALYTICS_CONFIG = EntityAnalyticsModifications()
|
|
|
|
|
|
async def async_devices_payload(hass: HomeAssistant) -> dict: # noqa: C901
|
|
"""Return detailed information about entities and devices."""
|
|
dev_reg = dr.async_get(hass)
|
|
ent_reg = er.async_get(hass)
|
|
|
|
integration_inputs: dict[str, tuple[list[str], list[str]]] = {}
|
|
integration_configs: dict[str, AnalyticsModifications] = {}
|
|
|
|
removed_devices: set[str] = set()
|
|
|
|
# Get device list
|
|
for device_entry in dev_reg.devices.values():
|
|
if not device_entry.primary_config_entry:
|
|
continue
|
|
|
|
config_entry = hass.config_entries.async_get_entry(
|
|
device_entry.primary_config_entry
|
|
)
|
|
|
|
if config_entry is None:
|
|
continue
|
|
|
|
if device_entry.entry_type is dr.DeviceEntryType.SERVICE:
|
|
removed_devices.add(device_entry.id)
|
|
continue
|
|
|
|
integration_domain = config_entry.domain
|
|
|
|
integration_input = integration_inputs.setdefault(integration_domain, ([], []))
|
|
integration_input[0].append(device_entry.id)
|
|
|
|
# Get entity list
|
|
for entity_entry in ent_reg.entities.values():
|
|
integration_domain = entity_entry.platform
|
|
|
|
integration_input = integration_inputs.setdefault(integration_domain, ([], []))
|
|
integration_input[1].append(entity_entry.entity_id)
|
|
|
|
integrations = {
|
|
domain: integration
|
|
for domain, integration in (
|
|
await async_get_integrations(hass, integration_inputs.keys())
|
|
).items()
|
|
if isinstance(integration, Integration)
|
|
}
|
|
|
|
# Filter out custom integrations and integrations that are not device or hub type
|
|
integration_inputs = {
|
|
domain: integration_info
|
|
for domain, integration_info in integration_inputs.items()
|
|
if (integration := integrations.get(domain)) is not None
|
|
and integration.is_built_in
|
|
and integration.manifest.get("integration_type") in ("device", "hub")
|
|
}
|
|
|
|
# Call integrations that implement the analytics platform
|
|
for integration_domain, integration_input in integration_inputs.items():
|
|
if (
|
|
modifier := await _async_get_modifier(hass, integration_domain)
|
|
) is not None:
|
|
try:
|
|
integration_config = await modifier(
|
|
hass, AnalyticsInput(*integration_input)
|
|
)
|
|
except Exception as err: # noqa: BLE001
|
|
LOGGER.exception(
|
|
"Calling async_modify_analytics for integration '%s' failed: %s",
|
|
integration_domain,
|
|
err,
|
|
)
|
|
integration_configs[integration_domain] = AnalyticsModifications(
|
|
remove=True
|
|
)
|
|
continue
|
|
|
|
if not isinstance(integration_config, AnalyticsModifications):
|
|
LOGGER.error( # type: ignore[unreachable]
|
|
"Calling async_modify_analytics for integration '%s' did not return an AnalyticsConfig",
|
|
integration_domain,
|
|
)
|
|
integration_configs[integration_domain] = AnalyticsModifications(
|
|
remove=True
|
|
)
|
|
continue
|
|
|
|
integration_configs[integration_domain] = integration_config
|
|
|
|
integrations_info: dict[str, dict[str, Any]] = {}
|
|
|
|
# We need to refer to other devices, for example in `via_device` field.
|
|
# We don't however send the original device ids outside of Home Assistant,
|
|
# instead we refer to devices by (integration_domain, index_in_integration_device_list).
|
|
device_id_mapping: dict[str, tuple[str, int]] = {}
|
|
|
|
# Fill out information about devices
|
|
for integration_domain, integration_input in integration_inputs.items():
|
|
integration_config = integration_configs.get(
|
|
integration_domain, DEFAULT_ANALYTICS_CONFIG
|
|
)
|
|
|
|
if integration_config.remove:
|
|
continue
|
|
|
|
integration_info = integrations_info.setdefault(
|
|
integration_domain, {"devices": [], "entities": []}
|
|
)
|
|
|
|
devices_info = integration_info["devices"]
|
|
|
|
for device_id in integration_input[0]:
|
|
device_config = DEFAULT_DEVICE_ANALYTICS_CONFIG
|
|
if integration_config.devices is not None:
|
|
device_config = integration_config.devices.get(device_id, device_config)
|
|
|
|
if device_config.remove:
|
|
removed_devices.add(device_id)
|
|
continue
|
|
|
|
device_entry = dev_reg.devices[device_id]
|
|
|
|
device_id_mapping[device_id] = (integration_domain, len(devices_info))
|
|
|
|
devices_info.append(
|
|
{
|
|
"entry_type": device_entry.entry_type,
|
|
"has_configuration_url": device_entry.configuration_url is not None,
|
|
"hw_version": device_entry.hw_version,
|
|
"manufacturer": device_entry.manufacturer,
|
|
"model": device_entry.model,
|
|
"model_id": device_entry.model_id,
|
|
"sw_version": device_entry.sw_version,
|
|
"via_device": device_entry.via_device_id,
|
|
"entities": [],
|
|
}
|
|
)
|
|
|
|
# Fill out via_device with new device ids
|
|
for integration_info in integrations_info.values():
|
|
for device_info in integration_info["devices"]:
|
|
if device_info["via_device"] is None:
|
|
continue
|
|
device_info["via_device"] = device_id_mapping.get(device_info["via_device"])
|
|
|
|
# Fill out information about entities
|
|
for integration_domain, integration_input in integration_inputs.items():
|
|
integration_config = integration_configs.get(
|
|
integration_domain, DEFAULT_ANALYTICS_CONFIG
|
|
)
|
|
|
|
if integration_config.remove:
|
|
continue
|
|
|
|
integration_info = integrations_info.setdefault(
|
|
integration_domain, {"devices": [], "entities": []}
|
|
)
|
|
|
|
devices_info = integration_info["devices"]
|
|
entities_info = integration_info["entities"]
|
|
|
|
for entity_id in integration_input[1]:
|
|
entity_config = DEFAULT_ENTITY_ANALYTICS_CONFIG
|
|
if integration_config.entities is not None:
|
|
entity_config = integration_config.entities.get(
|
|
entity_id, entity_config
|
|
)
|
|
|
|
if entity_config.remove:
|
|
continue
|
|
|
|
entity_entry = ent_reg.entities[entity_id]
|
|
|
|
entity_state = hass.states.get(entity_id)
|
|
|
|
entity_info = {
|
|
# LIMITATION: `assumed_state` can be overridden by users;
|
|
# we should replace it with the original value in the future.
|
|
# It is also not present, if entity is not in the state machine,
|
|
# which can happen for disabled entities.
|
|
"assumed_state": (
|
|
entity_state.attributes.get(ATTR_ASSUMED_STATE, False)
|
|
if entity_state is not None
|
|
else None
|
|
),
|
|
"domain": entity_entry.domain,
|
|
"entity_category": entity_entry.entity_category,
|
|
"has_entity_name": entity_entry.has_entity_name,
|
|
"original_device_class": entity_entry.original_device_class,
|
|
# LIMITATION: `unit_of_measurement` can be overridden by users;
|
|
# we should replace it with the original value in the future.
|
|
"unit_of_measurement": entity_entry.unit_of_measurement,
|
|
}
|
|
|
|
if (device_id_ := entity_entry.device_id) is not None:
|
|
if device_id_ in removed_devices:
|
|
# The device was removed, so we remove the entity too
|
|
continue
|
|
|
|
if (
|
|
new_device_id := device_id_mapping.get(device_id_)
|
|
) is not None and (new_device_id[0] == integration_domain):
|
|
device_info = devices_info[new_device_id[1]]
|
|
device_info["entities"].append(entity_info)
|
|
continue
|
|
|
|
entities_info.append(entity_info)
|
|
|
|
return {
|
|
"version": "home-assistant:1",
|
|
"home_assistant": HA_VERSION,
|
|
"integrations": integrations_info,
|
|
}
|