Add support for custom integrations in Analytics Insights (#109110)

This commit is contained in:
Joost Lekkerkerker 2024-01-30 17:52:28 +01:00 committed by GitHub
parent d752ab3aa4
commit 360697836f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 177 additions and 13 deletions

View file

@ -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,

View file

@ -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__)

View file

@ -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

View file

@ -3,6 +3,9 @@
"sensor": {
"core_integrations": {
"default": "mdi:puzzle"
},
"custom_integrations": {
"default": "mdi:puzzle-edit"
}
}
}

View file

@ -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(

View file

@ -23,5 +23,12 @@
"abort": {
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]"
}
},
"entity": {
"sensor": {
"custom_integrations": {
"name": "{custom_integration_domain} (custom)"
}
}
}
}

View file

@ -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"],
},
)

View file

@ -0,0 +1,10 @@
{
"hacs": {
"total": 157481,
"versions": {
"1.33.0": 123794,
"1.30.1": 1684,
"1.14.1": 23
}
}
}

View file

@ -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': <SensorStateClass.TOTAL: 'total'>,
}),
'config_entry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'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': <ANY>,
'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': <SensorStateClass.TOTAL: 'total'>,
'unit_of_measurement': 'active installations',
}),
'context': <ANY>,
'entity_id': 'sensor.homeassistant_analytics_hacs_custom',
'last_changed': <ANY>,
'last_updated': <ANY>,
'state': '157481',
})
# ---
# name: test_all_entities[sensor.homeassistant_analytics_myq-entry]
EntityRegistryEntrySnapshot({
'aliases': set({

View file

@ -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()