mirror of
https://github.com/home-assistant/core
synced 2024-10-05 09:12:11 +00:00
Add support for custom integrations in Analytics Insights (#109110)
This commit is contained in:
parent
d752ab3aa4
commit
360697836f
|
@ -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,
|
||||
|
|
|
@ -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__)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -3,6 +3,9 @@
|
|||
"sensor": {
|
||||
"core_integrations": {
|
||||
"default": "mdi:puzzle"
|
||||
},
|
||||
"custom_integrations": {
|
||||
"default": "mdi:puzzle-edit"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -23,5 +23,12 @@
|
|||
"abort": {
|
||||
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]"
|
||||
}
|
||||
},
|
||||
"entity": {
|
||||
"sensor": {
|
||||
"custom_integrations": {
|
||||
"name": "{custom_integration_domain} (custom)"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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"],
|
||||
},
|
||||
)
|
||||
|
|
|
@ -0,0 +1,10 @@
|
|||
{
|
||||
"hacs": {
|
||||
"total": 157481,
|
||||
"versions": {
|
||||
"1.33.0": 123794,
|
||||
"1.30.1": 1684,
|
||||
"1.14.1": 23
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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({
|
||||
|
|
|
@ -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()
|
||||
|
|
Loading…
Reference in a new issue