From a95c232f11670fe04e0c518feb25e4038db11c94 Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Tue, 29 Oct 2024 20:13:56 +0100 Subject: [PATCH] Add addon support to Home Assistant Analytics Insights (#128806) --- .../analytics_insights/config_flow.py | 35 +++++++++++-- .../components/analytics_insights/const.py | 1 + .../analytics_insights/coordinator.py | 16 ++++++ .../components/analytics_insights/sensor.py | 21 ++++++++ .../analytics_insights/strings.json | 4 ++ .../components/analytics_insights/conftest.py | 8 ++- .../analytics_insights/fixtures/addons.json | 31 ++++++++++++ .../snapshots/test_sensor.ambr | 50 +++++++++++++++++++ .../analytics_insights/test_config_flow.py | 26 ++++++++++ 9 files changed, 187 insertions(+), 5 deletions(-) create mode 100644 tests/components/analytics_insights/fixtures/addons.json diff --git a/homeassistant/components/analytics_insights/config_flow.py b/homeassistant/components/analytics_insights/config_flow.py index 909290b1035..baf0190967d 100644 --- a/homeassistant/components/analytics_insights/config_flow.py +++ b/homeassistant/components/analytics_insights/config_flow.py @@ -27,6 +27,7 @@ from homeassistant.helpers.selector import ( ) from .const import ( + CONF_TRACKED_ADDONS, CONF_TRACKED_CUSTOM_INTEGRATIONS, CONF_TRACKED_INTEGRATIONS, DOMAIN, @@ -55,8 +56,12 @@ class HomeassistantAnalyticsConfigFlow(ConfigFlow, domain=DOMAIN): """Handle the initial step.""" errors: dict[str, str] = {} if user_input is not None: - if not user_input.get(CONF_TRACKED_INTEGRATIONS) and not user_input.get( - CONF_TRACKED_CUSTOM_INTEGRATIONS + if all( + [ + not user_input.get(CONF_TRACKED_ADDONS), + not user_input.get(CONF_TRACKED_INTEGRATIONS), + not user_input.get(CONF_TRACKED_CUSTOM_INTEGRATIONS), + ] ): errors["base"] = "no_integrations_selected" else: @@ -64,6 +69,7 @@ class HomeassistantAnalyticsConfigFlow(ConfigFlow, domain=DOMAIN): title="Home Assistant Analytics Insights", data={}, options={ + CONF_TRACKED_ADDONS: user_input.get(CONF_TRACKED_ADDONS, []), CONF_TRACKED_INTEGRATIONS: user_input.get( CONF_TRACKED_INTEGRATIONS, [] ), @@ -77,6 +83,7 @@ class HomeassistantAnalyticsConfigFlow(ConfigFlow, domain=DOMAIN): session=async_get_clientsession(self.hass) ) try: + addons = await client.get_addons() integrations = await client.get_integrations() custom_integrations = await client.get_custom_integrations() except HomeassistantAnalyticsConnectionError: @@ -99,6 +106,13 @@ class HomeassistantAnalyticsConfigFlow(ConfigFlow, domain=DOMAIN): errors=errors, data_schema=vol.Schema( { + vol.Optional(CONF_TRACKED_ADDONS): SelectSelector( + SelectSelectorConfig( + options=list(addons), + multiple=True, + sort=True, + ) + ), vol.Optional(CONF_TRACKED_INTEGRATIONS): SelectSelector( SelectSelectorConfig( options=options, @@ -127,14 +141,19 @@ class HomeassistantAnalyticsOptionsFlowHandler(OptionsFlowWithConfigEntry): """Manage the options.""" errors: dict[str, str] = {} if user_input is not None: - if not user_input.get(CONF_TRACKED_INTEGRATIONS) and not user_input.get( - CONF_TRACKED_CUSTOM_INTEGRATIONS + if all( + [ + not user_input.get(CONF_TRACKED_ADDONS), + not user_input.get(CONF_TRACKED_INTEGRATIONS), + not user_input.get(CONF_TRACKED_CUSTOM_INTEGRATIONS), + ] ): errors["base"] = "no_integrations_selected" else: return self.async_create_entry( title="", data={ + CONF_TRACKED_ADDONS: user_input.get(CONF_TRACKED_ADDONS, []), CONF_TRACKED_INTEGRATIONS: user_input.get( CONF_TRACKED_INTEGRATIONS, [] ), @@ -148,6 +167,7 @@ class HomeassistantAnalyticsOptionsFlowHandler(OptionsFlowWithConfigEntry): session=async_get_clientsession(self.hass) ) try: + addons = await client.get_addons() integrations = await client.get_integrations() custom_integrations = await client.get_custom_integrations() except HomeassistantAnalyticsConnectionError: @@ -168,6 +188,13 @@ class HomeassistantAnalyticsOptionsFlowHandler(OptionsFlowWithConfigEntry): data_schema=self.add_suggested_values_to_schema( vol.Schema( { + vol.Optional(CONF_TRACKED_ADDONS): SelectSelector( + SelectSelectorConfig( + options=list(addons), + multiple=True, + sort=True, + ) + ), vol.Optional(CONF_TRACKED_INTEGRATIONS): SelectSelector( SelectSelectorConfig( options=options, diff --git a/homeassistant/components/analytics_insights/const.py b/homeassistant/components/analytics_insights/const.py index 56ea3f59794..1a01755f9ed 100644 --- a/homeassistant/components/analytics_insights/const.py +++ b/homeassistant/components/analytics_insights/const.py @@ -4,6 +4,7 @@ import logging DOMAIN = "analytics_insights" +CONF_TRACKED_ADDONS = "tracked_addons" CONF_TRACKED_INTEGRATIONS = "tracked_integrations" CONF_TRACKED_CUSTOM_INTEGRATIONS = "tracked_custom_integrations" diff --git a/homeassistant/components/analytics_insights/coordinator.py b/homeassistant/components/analytics_insights/coordinator.py index 3a7c40dfa82..701f1a8dbd4 100644 --- a/homeassistant/components/analytics_insights/coordinator.py +++ b/homeassistant/components/analytics_insights/coordinator.py @@ -12,11 +12,13 @@ from python_homeassistant_analytics import ( HomeassistantAnalyticsConnectionError, HomeassistantAnalyticsNotModifiedError, ) +from python_homeassistant_analytics.models import Addon from homeassistant.core import HomeAssistant from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from .const import ( + CONF_TRACKED_ADDONS, CONF_TRACKED_CUSTOM_INTEGRATIONS, CONF_TRACKED_INTEGRATIONS, DOMAIN, @@ -33,6 +35,7 @@ class AnalyticsData: active_installations: int reports_integrations: int + addons: dict[str, int] core_integrations: dict[str, int] custom_integrations: dict[str, int] @@ -53,6 +56,7 @@ class HomeassistantAnalyticsDataUpdateCoordinator(DataUpdateCoordinator[Analytic update_interval=timedelta(hours=12), ) self._client = client + self._tracked_addons = self.config_entry.options.get(CONF_TRACKED_ADDONS, []) self._tracked_integrations = self.config_entry.options[ CONF_TRACKED_INTEGRATIONS ] @@ -62,6 +66,7 @@ class HomeassistantAnalyticsDataUpdateCoordinator(DataUpdateCoordinator[Analytic async def _async_update_data(self) -> AnalyticsData: try: + addons_data = await self._client.get_addons() data = await self._client.get_current_analytics() custom_data = await self._client.get_custom_integrations() except HomeassistantAnalyticsConnectionError as err: @@ -70,6 +75,9 @@ class HomeassistantAnalyticsDataUpdateCoordinator(DataUpdateCoordinator[Analytic ) from err except HomeassistantAnalyticsNotModifiedError: return self.data + addons = { + addon: get_addon_value(addons_data, addon) for addon in self._tracked_addons + } core_integrations = { integration: data.integrations.get(integration, 0) for integration in self._tracked_integrations @@ -81,11 +89,19 @@ class HomeassistantAnalyticsDataUpdateCoordinator(DataUpdateCoordinator[Analytic return AnalyticsData( data.active_installations, data.reports_integrations, + addons, core_integrations, custom_integrations, ) +def get_addon_value(data: dict[str, Addon], name_slug: str) -> int: + """Get addon value.""" + if name_slug in data: + return data[name_slug].total + return 0 + + def get_custom_integration_value( data: dict[str, CustomIntegration], domain: str ) -> int: diff --git a/homeassistant/components/analytics_insights/sensor.py b/homeassistant/components/analytics_insights/sensor.py index 264c34e75ef..324ca6991d2 100644 --- a/homeassistant/components/analytics_insights/sensor.py +++ b/homeassistant/components/analytics_insights/sensor.py @@ -29,6 +29,20 @@ class AnalyticsSensorEntityDescription(SensorEntityDescription): value_fn: Callable[[AnalyticsData], StateType] +def get_addon_entity_description( + name_slug: str, +) -> AnalyticsSensorEntityDescription: + """Get addon entity description.""" + return AnalyticsSensorEntityDescription( + key=f"addon_{name_slug}_active_installations", + translation_key="addons", + name=name_slug, + state_class=SensorStateClass.TOTAL, + native_unit_of_measurement="active installations", + value_fn=lambda data: data.addons.get(name_slug), + ) + + def get_core_integration_entity_description( domain: str, name: str ) -> AnalyticsSensorEntityDescription: @@ -89,6 +103,13 @@ async def async_setup_entry( analytics_data.coordinator ) entities: list[HomeassistantAnalyticsSensor] = [] + entities.extend( + HomeassistantAnalyticsSensor( + coordinator, + get_addon_entity_description(addon_name_slug), + ) + for addon_name_slug in coordinator.data.addons + ) entities.extend( HomeassistantAnalyticsSensor( coordinator, diff --git a/homeassistant/components/analytics_insights/strings.json b/homeassistant/components/analytics_insights/strings.json index b3445fdf47e..10d3c19a2f6 100644 --- a/homeassistant/components/analytics_insights/strings.json +++ b/homeassistant/components/analytics_insights/strings.json @@ -3,10 +3,12 @@ "step": { "user": { "data": { + "tracked_addons": "Addons", "tracked_integrations": "Integrations", "tracked_custom_integrations": "Custom integrations" }, "data_description": { + "tracked_addons": "Select the addons you want to track", "tracked_integrations": "Select the integrations you want to track", "tracked_custom_integrations": "Select the custom integrations you want to track" } @@ -24,10 +26,12 @@ "step": { "init": { "data": { + "tracked_addons": "[%key:component::analytics_insights::config::step::user::data::tracked_addons%]", "tracked_integrations": "[%key:component::analytics_insights::config::step::user::data::tracked_integrations%]", "tracked_custom_integrations": "[%key:component::analytics_insights::config::step::user::data::tracked_custom_integrations%]" }, "data_description": { + "tracked_addons": "[%key:component::analytics_insights::config::step::user::data_description::tracked_addons%]", "tracked_integrations": "[%key:component::analytics_insights::config::step::user::data_description::tracked_integrations%]", "tracked_custom_integrations": "[%key:component::analytics_insights::config::step::user::data_description::tracked_custom_integrations%]" } diff --git a/tests/components/analytics_insights/conftest.py b/tests/components/analytics_insights/conftest.py index fcdda95e9bd..a9c152b8ab9 100644 --- a/tests/components/analytics_insights/conftest.py +++ b/tests/components/analytics_insights/conftest.py @@ -5,9 +5,10 @@ from unittest.mock import AsyncMock, patch import pytest from python_homeassistant_analytics import CurrentAnalytics -from python_homeassistant_analytics.models import CustomIntegration, Integration +from python_homeassistant_analytics.models import Addon, CustomIntegration, Integration from homeassistant.components.analytics_insights.const import ( + CONF_TRACKED_ADDONS, CONF_TRACKED_CUSTOM_INTEGRATIONS, CONF_TRACKED_INTEGRATIONS, DOMAIN, @@ -43,6 +44,10 @@ def mock_analytics_client() -> Generator[AsyncMock]: client.get_current_analytics.return_value = CurrentAnalytics.from_json( load_fixture("analytics_insights/current_data.json") ) + addons = load_json_object_fixture("analytics_insights/addons.json") + client.get_addons.return_value = { + key: Addon.from_dict(value) for key, value in addons.items() + } integrations = load_json_object_fixture("analytics_insights/integrations.json") client.get_integrations.return_value = { key: Integration.from_dict(value) for key, value in integrations.items() @@ -65,6 +70,7 @@ def mock_config_entry() -> MockConfigEntry: title="Homeassistant Analytics", data={}, options={ + CONF_TRACKED_ADDONS: ["core_samba"], CONF_TRACKED_INTEGRATIONS: ["youtube", "spotify", "myq"], CONF_TRACKED_CUSTOM_INTEGRATIONS: ["hacs"], }, diff --git a/tests/components/analytics_insights/fixtures/addons.json b/tests/components/analytics_insights/fixtures/addons.json new file mode 100644 index 00000000000..cb7ae42c86b --- /dev/null +++ b/tests/components/analytics_insights/fixtures/addons.json @@ -0,0 +1,31 @@ +{ + "core_samba": { + "total": 76357, + "versions": { + "12.3.2": 65875, + "12.2.0": 1313, + "12.3.1": 5018, + "12.1.0": 211, + "10.0.0": 1139, + "9.4.0": 4, + "12.3.0": 704, + "9.3.1": 36, + "10.0.2": 1290, + "9.5.1": 379, + "9.6.1": 66, + "10.0.1": 200, + "9.3.0": 20, + "9.2.0": 9, + "9.5.0": 13, + "12.0.0": 39, + "9.7.0": 20, + "11.0.0": 13, + "3.0": 1, + "9.6.0": 2, + "8.1": 2, + "9.0": 3 + }, + "protected": 76345, + "auto_update": 32732 + } +} diff --git a/tests/components/analytics_insights/snapshots/test_sensor.ambr b/tests/components/analytics_insights/snapshots/test_sensor.ambr index 971ca6db86f..6e11b344b0b 100644 --- a/tests/components/analytics_insights/snapshots/test_sensor.ambr +++ b/tests/components/analytics_insights/snapshots/test_sensor.ambr @@ -1,4 +1,54 @@ # serializer version: 1 +# name: test_all_entities[sensor.homeassistant_analytics_core_samba-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': , + 'entity_id': 'sensor.homeassistant_analytics_core_samba', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'core_samba', + 'platform': 'analytics_insights', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'addons', + 'unique_id': 'addon_core_samba_active_installations', + 'unit_of_measurement': 'active installations', + }) +# --- +# name: test_all_entities[sensor.homeassistant_analytics_core_samba-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Homeassistant Analytics core_samba', + 'state_class': , + 'unit_of_measurement': 'active installations', + }), + 'context': , + 'entity_id': 'sensor.homeassistant_analytics_core_samba', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '76357', + }) +# --- # name: test_all_entities[sensor.homeassistant_analytics_hacs_custom-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 0c9d4c074f8..747f24930a4 100644 --- a/tests/components/analytics_insights/test_config_flow.py +++ b/tests/components/analytics_insights/test_config_flow.py @@ -7,6 +7,7 @@ import pytest from python_homeassistant_analytics import HomeassistantAnalyticsConnectionError from homeassistant.components.analytics_insights.const import ( + CONF_TRACKED_ADDONS, CONF_TRACKED_CUSTOM_INTEGRATIONS, CONF_TRACKED_INTEGRATIONS, DOMAIN, @@ -25,10 +26,12 @@ from tests.common import MockConfigEntry [ ( { + CONF_TRACKED_ADDONS: ["core_samba"], CONF_TRACKED_INTEGRATIONS: ["youtube"], CONF_TRACKED_CUSTOM_INTEGRATIONS: ["hacs"], }, { + CONF_TRACKED_ADDONS: ["core_samba"], CONF_TRACKED_INTEGRATIONS: ["youtube"], CONF_TRACKED_CUSTOM_INTEGRATIONS: ["hacs"], }, @@ -38,6 +41,7 @@ from tests.common import MockConfigEntry CONF_TRACKED_INTEGRATIONS: ["youtube"], }, { + CONF_TRACKED_ADDONS: [], CONF_TRACKED_INTEGRATIONS: ["youtube"], CONF_TRACKED_CUSTOM_INTEGRATIONS: [], }, @@ -47,6 +51,7 @@ from tests.common import MockConfigEntry CONF_TRACKED_CUSTOM_INTEGRATIONS: ["hacs"], }, { + CONF_TRACKED_ADDONS: [], CONF_TRACKED_INTEGRATIONS: [], CONF_TRACKED_CUSTOM_INTEGRATIONS: ["hacs"], }, @@ -83,6 +88,7 @@ async def test_form( "user_input", [ { + CONF_TRACKED_ADDONS: [], CONF_TRACKED_INTEGRATIONS: [], CONF_TRACKED_CUSTOM_INTEGRATIONS: [], }, @@ -113,6 +119,7 @@ async def test_submitting_empty_form( result = await hass.config_entries.flow.async_configure( result["flow_id"], { + CONF_TRACKED_ADDONS: ["core_samba"], CONF_TRACKED_INTEGRATIONS: ["youtube"], CONF_TRACKED_CUSTOM_INTEGRATIONS: ["hacs"], }, @@ -123,6 +130,7 @@ async def test_submitting_empty_form( assert result["title"] == "Home Assistant Analytics Insights" assert result["data"] == {} assert result["options"] == { + CONF_TRACKED_ADDONS: ["core_samba"], CONF_TRACKED_INTEGRATIONS: ["youtube"], CONF_TRACKED_CUSTOM_INTEGRATIONS: ["hacs"], } @@ -161,6 +169,7 @@ async def test_form_already_configured( domain=DOMAIN, data={}, options={ + CONF_TRACKED_ADDONS: [], CONF_TRACKED_INTEGRATIONS: ["youtube", "spotify"], CONF_TRACKED_CUSTOM_INTEGRATIONS: [], }, @@ -179,19 +188,32 @@ async def test_form_already_configured( [ ( { + CONF_TRACKED_ADDONS: ["core_samba"], CONF_TRACKED_INTEGRATIONS: ["youtube"], CONF_TRACKED_CUSTOM_INTEGRATIONS: ["hacs"], }, { + CONF_TRACKED_ADDONS: ["core_samba"], CONF_TRACKED_INTEGRATIONS: ["youtube"], CONF_TRACKED_CUSTOM_INTEGRATIONS: ["hacs"], }, ), + ( + { + CONF_TRACKED_ADDONS: ["core_samba"], + }, + { + CONF_TRACKED_ADDONS: ["core_samba"], + CONF_TRACKED_INTEGRATIONS: [], + CONF_TRACKED_CUSTOM_INTEGRATIONS: [], + }, + ), ( { CONF_TRACKED_INTEGRATIONS: ["youtube"], }, { + CONF_TRACKED_ADDONS: [], CONF_TRACKED_INTEGRATIONS: ["youtube"], CONF_TRACKED_CUSTOM_INTEGRATIONS: [], }, @@ -201,6 +223,7 @@ async def test_form_already_configured( CONF_TRACKED_CUSTOM_INTEGRATIONS: ["hacs"], }, { + CONF_TRACKED_ADDONS: [], CONF_TRACKED_INTEGRATIONS: [], CONF_TRACKED_CUSTOM_INTEGRATIONS: ["hacs"], }, @@ -237,6 +260,7 @@ async def test_options_flow( "user_input", [ { + CONF_TRACKED_ADDONS: [], CONF_TRACKED_INTEGRATIONS: [], CONF_TRACKED_CUSTOM_INTEGRATIONS: [], }, @@ -267,6 +291,7 @@ async def test_submitting_empty_options_flow( result = await hass.config_entries.options.async_configure( result["flow_id"], { + CONF_TRACKED_ADDONS: ["core_samba"], CONF_TRACKED_INTEGRATIONS: ["youtube", "hue"], CONF_TRACKED_CUSTOM_INTEGRATIONS: ["hacs"], }, @@ -275,6 +300,7 @@ async def test_submitting_empty_options_flow( assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"] == { + CONF_TRACKED_ADDONS: ["core_samba"], CONF_TRACKED_INTEGRATIONS: ["youtube", "hue"], CONF_TRACKED_CUSTOM_INTEGRATIONS: ["hacs"], }