From d7d2b7ad761883a77bd887700edf2418b223285d Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 18 Oct 2024 23:11:27 -0400 Subject: [PATCH 1/3] Add method to generate devices analytics payload --- .../components/analytics/analytics.py | 68 +++++++++++- tests/components/analytics/test_analytics.py | 104 +++++++++++++++++- 2 files changed, 170 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/analytics/analytics.py b/homeassistant/components/analytics/analytics.py index b63475c80a4..6f53f81acae 100644 --- a/homeassistant/components/analytics/analytics.py +++ b/homeassistant/components/analytics/analytics.py @@ -27,8 +27,8 @@ from homeassistant.config_entries import SOURCE_IGNORE from homeassistant.const import ATTR_DOMAIN, __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 -import homeassistant.helpers.entity_registry as er from homeassistant.helpers.hassio import is_hassio from homeassistant.helpers.storage import Store from homeassistant.helpers.system_info import async_get_system_info @@ -370,3 +370,69 @@ class Analytics: for entry in entries if entry.source != SOURCE_IGNORE and entry.disabled_by is None ) + + +@callback +def async_devices_payload(hass: HomeAssistant) -> dict: + """Return the devices payload.""" + integrations_without_model_id: set[str] = set() + devices: list[dict[str, Any]] = [] + dev_reg = dr.async_get(hass) + ignored_integrations = { + "bluetooth", + "esphome", + "hassio", + "mqtt", + } + # Devices that need via device info set + new_indexes: dict[str, int] = {} + via_devices: dict[str, str] = {} + + for device in dev_reg.devices.values(): + # Ignore services + if device.entry_type: + continue + + if not device.primary_config_entry: + continue + + config_entry = hass.config_entries.async_get_entry(device.primary_config_entry) + + if config_entry is None: + continue + + if config_entry.domain in ignored_integrations: + continue + + if not device.model_id: + integrations_without_model_id.add(config_entry.domain) + continue + + if not device.manufacturer: + continue + + new_indexes[device.id] = len(devices) + devices.append( + { + "manufacturer": device.manufacturer, + "model_id": device.model_id, + "model": device.model, + "sw_version": device.sw_version, + "hw_version": device.hw_version, + "has_suggested_area": device.suggested_area is not None, + "has_configuration_url": device.configuration_url is not None, + "via_device": None, + } + ) + if device.via_device_id: + via_devices[device.id] = device.via_device_id + + for from_device, via_device in via_devices.items(): + if via_device not in new_indexes: + continue + devices[new_indexes[from_device]]["via_device"] = new_indexes[via_device] + + return { + "no_model_id": sorted(integrations_without_model_id), + "devices": devices, + } diff --git a/tests/components/analytics/test_analytics.py b/tests/components/analytics/test_analytics.py index ba7e46bdde7..6d7a614ca98 100644 --- a/tests/components/analytics/test_analytics.py +++ b/tests/components/analytics/test_analytics.py @@ -10,7 +10,10 @@ import pytest from syrupy import SnapshotAssertion from syrupy.matchers import path_type -from homeassistant.components.analytics.analytics import Analytics +from homeassistant.components.analytics.analytics import ( + Analytics, + async_devices_payload, +) from homeassistant.components.analytics.const import ( ANALYTICS_ENDPOINT_URL, ANALYTICS_ENDPOINT_URL_DEV, @@ -22,6 +25,7 @@ from homeassistant.components.analytics.const import ( from homeassistant.config_entries import ConfigEntryDisabler, ConfigEntryState from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers import device_registry as dr from homeassistant.loader import IntegrationNotFound from homeassistant.setup import async_setup_component @@ -920,3 +924,101 @@ async def test_not_check_config_entries_if_yaml( assert submitted_data["integrations"] == ["default_config"] assert submitted_data == logged_data assert snapshot == submitted_data + + +async def test_devices_payload( + hass: HomeAssistant, + device_registry: dr.DeviceRegistry, +) -> None: + """Test devices payload.""" + assert async_devices_payload(hass) == { + "no_model_id": [], + "devices": [], + } + + mock_config_entry = MockConfigEntry() + mock_config_entry.add_to_hass(hass) + + # Normal entry + device_registry.async_get_or_create( + config_entry_id=mock_config_entry.entry_id, + identifiers={("device", "1")}, + sw_version="test-sw-version", + hw_version="test-hw-version", + name="test-name", + manufacturer="test-manufacturer", + model="test-model", + model_id="test-model-id", + suggested_area="Game Room", + configuration_url="http://example.com/config", + ) + + # Ignored because service type + device_registry.async_get_or_create( + config_entry_id=mock_config_entry.entry_id, + identifiers={("device", "2")}, + manufacturer="test-manufacturer", + model_id="test-model-id", + entry_type=dr.DeviceEntryType.SERVICE, + ) + + # Ignored because config entry is ignore list + ignored_config_entry = MockConfigEntry(domain="esphome") + ignored_config_entry.add_to_hass(hass) + device_registry.async_get_or_create( + config_entry_id=ignored_config_entry.entry_id, + identifiers={("device", "3")}, + manufacturer="test-manufacturer", + model_id="test-model-id", + ) + + # Ignored because no model id + no_model_id_config_entry = MockConfigEntry(domain="no_model_id") + no_model_id_config_entry.add_to_hass(hass) + device_registry.async_get_or_create( + config_entry_id=no_model_id_config_entry.entry_id, + identifiers={("device", "4")}, + manufacturer="test-manufacturer", + ) + + # Ignored because no manufacturer + device_registry.async_get_or_create( + config_entry_id=mock_config_entry.entry_id, + identifiers={("device", "5")}, + model_id="test-model-id", + ) + + # Entry with via device + device_registry.async_get_or_create( + config_entry_id=mock_config_entry.entry_id, + identifiers={("device", "6")}, + manufacturer="test-manufacturer6", + model_id="test-model-id6", + via_device=("device", "1"), + ) + + assert async_devices_payload(hass) == { + "no_model_id": ["no_model_id"], + "devices": [ + { + "manufacturer": "test-manufacturer", + "model_id": "test-model-id", + "model": "test-model", + "sw_version": "test-sw-version", + "hw_version": "test-hw-version", + "has_suggested_area": True, + "has_configuration_url": True, + "via_device": None, + }, + { + "manufacturer": "test-manufacturer6", + "model_id": "test-model-id6", + "model": None, + "sw_version": None, + "hw_version": None, + "has_suggested_area": False, + "has_configuration_url": False, + "via_device": 0, + }, + ], + } From b3a32b5f59ce2c11fe31b0b4e41ca07174a2cdef Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 24 Oct 2024 17:33:00 +0000 Subject: [PATCH 2/3] Update format --- homeassistant/components/analytics/analytics.py | 1 + tests/components/analytics/test_analytics.py | 2 ++ 2 files changed, 3 insertions(+) diff --git a/homeassistant/components/analytics/analytics.py b/homeassistant/components/analytics/analytics.py index 6f53f81acae..736d7b010ca 100644 --- a/homeassistant/components/analytics/analytics.py +++ b/homeassistant/components/analytics/analytics.py @@ -433,6 +433,7 @@ def async_devices_payload(hass: HomeAssistant) -> dict: devices[new_indexes[from_device]]["via_device"] = new_indexes[via_device] return { + "version": "home-assistant:1", "no_model_id": sorted(integrations_without_model_id), "devices": devices, } diff --git a/tests/components/analytics/test_analytics.py b/tests/components/analytics/test_analytics.py index 6d7a614ca98..ef258394e12 100644 --- a/tests/components/analytics/test_analytics.py +++ b/tests/components/analytics/test_analytics.py @@ -932,6 +932,7 @@ async def test_devices_payload( ) -> None: """Test devices payload.""" assert async_devices_payload(hass) == { + "version": "home-assistant:1", "no_model_id": [], "devices": [], } @@ -998,6 +999,7 @@ async def test_devices_payload( ) assert async_devices_payload(hass) == { + "version": "home-assistant:1", "no_model_id": ["no_model_id"], "devices": [ { From 98cff0bd74140dfabb742c44d7950b69540d876a Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 24 Oct 2024 15:17:17 -0400 Subject: [PATCH 3/3] Add integration back --- homeassistant/components/analytics/analytics.py | 1 + tests/components/analytics/test_analytics.py | 2 ++ 2 files changed, 3 insertions(+) diff --git a/homeassistant/components/analytics/analytics.py b/homeassistant/components/analytics/analytics.py index 736d7b010ca..13eadab221c 100644 --- a/homeassistant/components/analytics/analytics.py +++ b/homeassistant/components/analytics/analytics.py @@ -414,6 +414,7 @@ def async_devices_payload(hass: HomeAssistant) -> dict: new_indexes[device.id] = len(devices) devices.append( { + "integration": config_entry.domain, "manufacturer": device.manufacturer, "model_id": device.model_id, "model": device.model, diff --git a/tests/components/analytics/test_analytics.py b/tests/components/analytics/test_analytics.py index ef258394e12..f2b4b30d4f9 100644 --- a/tests/components/analytics/test_analytics.py +++ b/tests/components/analytics/test_analytics.py @@ -1008,6 +1008,7 @@ async def test_devices_payload( "model": "test-model", "sw_version": "test-sw-version", "hw_version": "test-hw-version", + "integration": "test", "has_suggested_area": True, "has_configuration_url": True, "via_device": None, @@ -1018,6 +1019,7 @@ async def test_devices_payload( "model": None, "sw_version": None, "hw_version": None, + "integration": "test", "has_suggested_area": False, "has_configuration_url": False, "via_device": 0,