diff --git a/homeassistant/components/analytics_insights/config_flow.py b/homeassistant/components/analytics_insights/config_flow.py index eb6d0f87079..b409a9c0fb9 100644 --- a/homeassistant/components/analytics_insights/config_flow.py +++ b/homeassistant/components/analytics_insights/config_flow.py @@ -25,7 +25,12 @@ from homeassistant.helpers.selector import ( SelectSelectorConfig, ) -from .const import CONF_TRACKED_INTEGRATIONS, DOMAIN, LOGGER +from .const import ( + CONF_TRACKED_CUSTOM_INTEGRATIONS, + CONF_TRACKED_INTEGRATIONS, + DOMAIN, + LOGGER, +) INTEGRATION_TYPES_WITHOUT_ANALYTICS = ( IntegrationType.BRAND, @@ -58,6 +63,7 @@ class HomeassistantAnalyticsConfigFlow(ConfigFlow, domain=DOMAIN): ) try: integrations = await client.get_integrations() + custom_integrations = await client.get_custom_integrations() except HomeassistantAnalyticsConnectionError: LOGGER.exception("Error connecting to Home Assistant analytics") return self.async_abort(reason="cannot_connect") @@ -81,6 +87,13 @@ class HomeassistantAnalyticsConfigFlow(ConfigFlow, domain=DOMAIN): sort=True, ) ), + vol.Required(CONF_TRACKED_CUSTOM_INTEGRATIONS): SelectSelector( + SelectSelectorConfig( + options=list(custom_integrations), + multiple=True, + sort=True, + ) + ), } ), ) @@ -101,6 +114,7 @@ class HomeassistantAnalyticsOptionsFlowHandler(OptionsFlowWithConfigEntry): ) try: integrations = await client.get_integrations() + custom_integrations = await client.get_custom_integrations() except HomeassistantAnalyticsConnectionError: LOGGER.exception("Error connecting to Home Assistant analytics") return self.async_abort(reason="cannot_connect") @@ -125,6 +139,13 @@ class HomeassistantAnalyticsOptionsFlowHandler(OptionsFlowWithConfigEntry): sort=True, ) ), + vol.Required(CONF_TRACKED_CUSTOM_INTEGRATIONS): SelectSelector( + SelectSelectorConfig( + options=list(custom_integrations), + multiple=True, + sort=True, + ) + ), }, ), self.options, diff --git a/homeassistant/components/analytics_insights/const.py b/homeassistant/components/analytics_insights/const.py index 3b9bf01d11e..745c05302a1 100644 --- a/homeassistant/components/analytics_insights/const.py +++ b/homeassistant/components/analytics_insights/const.py @@ -4,5 +4,6 @@ import logging DOMAIN = "analytics_insights" CONF_TRACKED_INTEGRATIONS = "tracked_integrations" +CONF_TRACKED_CUSTOM_INTEGRATIONS = "tracked_custom_integrations" LOGGER = logging.getLogger(__package__) diff --git a/homeassistant/components/analytics_insights/coordinator.py b/homeassistant/components/analytics_insights/coordinator.py index 0c2a0f16aa9..c646288cbe0 100644 --- a/homeassistant/components/analytics_insights/coordinator.py +++ b/homeassistant/components/analytics_insights/coordinator.py @@ -5,6 +5,7 @@ from dataclasses import dataclass from datetime import timedelta from python_homeassistant_analytics import ( + CustomIntegration, HomeassistantAnalyticsClient, HomeassistantAnalyticsConnectionError, HomeassistantAnalyticsNotModifiedError, @@ -14,14 +15,20 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed -from .const import CONF_TRACKED_INTEGRATIONS, DOMAIN, LOGGER +from .const import ( + CONF_TRACKED_CUSTOM_INTEGRATIONS, + CONF_TRACKED_INTEGRATIONS, + DOMAIN, + LOGGER, +) -@dataclass(frozen=True, kw_only=True) +@dataclass(frozen=True) class AnalyticsData: """Analytics data class.""" core_integrations: dict[str, int] + custom_integrations: dict[str, int] class HomeassistantAnalyticsDataUpdateCoordinator(DataUpdateCoordinator[AnalyticsData]): @@ -43,10 +50,14 @@ class HomeassistantAnalyticsDataUpdateCoordinator(DataUpdateCoordinator[Analytic self._tracked_integrations = self.config_entry.options[ CONF_TRACKED_INTEGRATIONS ] + self._tracked_custom_integrations = self.config_entry.options[ + CONF_TRACKED_CUSTOM_INTEGRATIONS + ] async def _async_update_data(self) -> AnalyticsData: try: data = await self._client.get_current_analytics() + custom_data = await self._client.get_custom_integrations() except HomeassistantAnalyticsConnectionError as err: raise UpdateFailed( "Error communicating with Homeassistant Analytics" @@ -57,4 +68,17 @@ class HomeassistantAnalyticsDataUpdateCoordinator(DataUpdateCoordinator[Analytic integration: data.integrations.get(integration, 0) for integration in self._tracked_integrations } - return AnalyticsData(core_integrations=core_integrations) + custom_integrations = { + integration: get_custom_integration_value(custom_data, integration) + for integration in self._tracked_custom_integrations + } + return AnalyticsData(core_integrations, custom_integrations) + + +def get_custom_integration_value( + data: dict[str, CustomIntegration], domain: str +) -> int: + """Get custom integration value.""" + if domain in data: + return data[domain].total + return 0 diff --git a/homeassistant/components/analytics_insights/icons.json b/homeassistant/components/analytics_insights/icons.json index b1358e478b4..705578dbc6b 100644 --- a/homeassistant/components/analytics_insights/icons.json +++ b/homeassistant/components/analytics_insights/icons.json @@ -3,6 +3,9 @@ "sensor": { "core_integrations": { "default": "mdi:puzzle" + }, + "custom_integrations": { + "default": "mdi:puzzle-edit" } } } diff --git a/homeassistant/components/analytics_insights/sensor.py b/homeassistant/components/analytics_insights/sensor.py index ae24abd8b07..e0fe2c79413 100644 --- a/homeassistant/components/analytics_insights/sensor.py +++ b/homeassistant/components/analytics_insights/sensor.py @@ -42,6 +42,20 @@ def get_core_integration_entity_description( ) +def get_custom_integration_entity_description( + domain: str, +) -> AnalyticsSensorEntityDescription: + """Get custom integration entity description.""" + return AnalyticsSensorEntityDescription( + key=f"custom_{domain}_active_installations", + translation_key="custom_integrations", + translation_placeholders={"custom_integration_domain": domain}, + state_class=SensorStateClass.TOTAL, + native_unit_of_measurement="active installations", + value_fn=lambda data: data.custom_integrations.get(domain), + ) + + async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, @@ -50,15 +64,27 @@ async def async_setup_entry( """Initialize the entries.""" analytics_data: AnalyticsInsightsData = hass.data[DOMAIN][entry.entry_id] - async_add_entities( + coordinator: HomeassistantAnalyticsDataUpdateCoordinator = ( + analytics_data.coordinator + ) + entities: list[HomeassistantAnalyticsSensor] = [] + entities.extend( HomeassistantAnalyticsSensor( - analytics_data.coordinator, + coordinator, get_core_integration_entity_description( integration_domain, analytics_data.names[integration_domain] ), ) - for integration_domain in analytics_data.coordinator.data.core_integrations + for integration_domain in coordinator.data.core_integrations ) + entities.extend( + HomeassistantAnalyticsSensor( + coordinator, + get_custom_integration_entity_description(integration_domain), + ) + for integration_domain in coordinator.data.custom_integrations + ) + async_add_entities(entities) class HomeassistantAnalyticsSensor( diff --git a/homeassistant/components/analytics_insights/strings.json b/homeassistant/components/analytics_insights/strings.json index 5c249a1cd5a..96ec59f299b 100644 --- a/homeassistant/components/analytics_insights/strings.json +++ b/homeassistant/components/analytics_insights/strings.json @@ -23,5 +23,12 @@ "abort": { "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]" } + }, + "entity": { + "sensor": { + "custom_integrations": { + "name": "{custom_integration_domain} (custom)" + } + } } } diff --git a/tests/components/analytics_insights/conftest.py b/tests/components/analytics_insights/conftest.py index a1a32cb3f74..6ca98d294e6 100644 --- a/tests/components/analytics_insights/conftest.py +++ b/tests/components/analytics_insights/conftest.py @@ -4,10 +4,13 @@ from unittest.mock import AsyncMock, patch import pytest from python_homeassistant_analytics import CurrentAnalytics -from python_homeassistant_analytics.models import Integration +from python_homeassistant_analytics.models import CustomIntegration, Integration from homeassistant.components.analytics_insights import DOMAIN -from homeassistant.components.analytics_insights.const import CONF_TRACKED_INTEGRATIONS +from homeassistant.components.analytics_insights.const import ( + CONF_TRACKED_CUSTOM_INTEGRATIONS, + CONF_TRACKED_INTEGRATIONS, +) from tests.common import MockConfigEntry, load_fixture, load_json_object_fixture @@ -40,6 +43,13 @@ def mock_analytics_client() -> Generator[AsyncMock, None, None]: client.get_integrations.return_value = { key: Integration.from_dict(value) for key, value in integrations.items() } + custom_integrations = load_json_object_fixture( + "analytics_insights/custom_integrations.json" + ) + client.get_custom_integrations.return_value = { + key: CustomIntegration.from_dict(value) + for key, value in custom_integrations.items() + } yield client @@ -50,5 +60,8 @@ def mock_config_entry() -> MockConfigEntry: domain=DOMAIN, title="Homeassistant Analytics", data={}, - options={CONF_TRACKED_INTEGRATIONS: ["youtube", "spotify", "myq"]}, + options={ + CONF_TRACKED_INTEGRATIONS: ["youtube", "spotify", "myq"], + CONF_TRACKED_CUSTOM_INTEGRATIONS: ["hacs"], + }, ) diff --git a/tests/components/analytics_insights/fixtures/custom_integrations.json b/tests/components/analytics_insights/fixtures/custom_integrations.json new file mode 100644 index 00000000000..5777c8f1d06 --- /dev/null +++ b/tests/components/analytics_insights/fixtures/custom_integrations.json @@ -0,0 +1,10 @@ +{ + "hacs": { + "total": 157481, + "versions": { + "1.33.0": 123794, + "1.30.1": 1684, + "1.14.1": 23 + } + } +} diff --git a/tests/components/analytics_insights/snapshots/test_sensor.ambr b/tests/components/analytics_insights/snapshots/test_sensor.ambr index 404850baa4e..474263f68e9 100644 --- a/tests/components/analytics_insights/snapshots/test_sensor.ambr +++ b/tests/components/analytics_insights/snapshots/test_sensor.ambr @@ -1,4 +1,51 @@ # serializer version: 1 +# name: test_all_entities[sensor.homeassistant_analytics_hacs_custom-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.homeassistant_analytics_hacs_custom', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'hacs (custom)', + 'platform': 'analytics_insights', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'custom_integrations', + 'unique_id': 'custom_hacs_active_installations', + 'unit_of_measurement': 'active installations', + }) +# --- +# name: test_all_entities[sensor.homeassistant_analytics_hacs_custom-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Homeassistant Analytics hacs (custom)', + 'state_class': , + 'unit_of_measurement': 'active installations', + }), + 'context': , + 'entity_id': 'sensor.homeassistant_analytics_hacs_custom', + 'last_changed': , + 'last_updated': , + 'state': '157481', + }) +# --- # name: test_all_entities[sensor.homeassistant_analytics_myq-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ diff --git a/tests/components/analytics_insights/test_config_flow.py b/tests/components/analytics_insights/test_config_flow.py index a93290745f2..8cefa29ee7b 100644 --- a/tests/components/analytics_insights/test_config_flow.py +++ b/tests/components/analytics_insights/test_config_flow.py @@ -5,6 +5,7 @@ from python_homeassistant_analytics import HomeassistantAnalyticsConnectionError from homeassistant import config_entries from homeassistant.components.analytics_insights.const import ( + CONF_TRACKED_CUSTOM_INTEGRATIONS, CONF_TRACKED_INTEGRATIONS, DOMAIN, ) @@ -26,14 +27,20 @@ async def test_form( result = await hass.config_entries.flow.async_configure( result["flow_id"], - {CONF_TRACKED_INTEGRATIONS: ["youtube"]}, + { + CONF_TRACKED_INTEGRATIONS: ["youtube"], + CONF_TRACKED_CUSTOM_INTEGRATIONS: ["hacs"], + }, ) await hass.async_block_till_done() assert result["type"] == FlowResultType.CREATE_ENTRY assert result["title"] == "Home Assistant Analytics Insights" assert result["data"] == {} - assert result["options"] == {CONF_TRACKED_INTEGRATIONS: ["youtube"]} + assert result["options"] == { + CONF_TRACKED_INTEGRATIONS: ["youtube"], + CONF_TRACKED_CUSTOM_INTEGRATIONS: ["hacs"], + } assert len(mock_setup_entry.mock_calls) == 1 @@ -60,7 +67,10 @@ async def test_form_already_configured( entry = MockConfigEntry( domain=DOMAIN, data={}, - options={CONF_TRACKED_INTEGRATIONS: ["youtube", "spotify"]}, + options={ + CONF_TRACKED_INTEGRATIONS: ["youtube", "spotify"], + CONF_TRACKED_CUSTOM_INTEGRATIONS: [], + }, ) entry.add_to_hass(hass) @@ -87,6 +97,7 @@ async def test_options_flow( result["flow_id"], user_input={ CONF_TRACKED_INTEGRATIONS: ["youtube", "hue"], + CONF_TRACKED_CUSTOM_INTEGRATIONS: ["hacs"], }, ) await hass.async_block_till_done() @@ -94,6 +105,7 @@ async def test_options_flow( assert result["type"] == FlowResultType.CREATE_ENTRY assert result["data"] == { CONF_TRACKED_INTEGRATIONS: ["youtube", "hue"], + CONF_TRACKED_CUSTOM_INTEGRATIONS: ["hacs"], } await hass.async_block_till_done() mock_analytics_client.get_integrations.assert_called_once()