Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for custom integrations in Analytics Insights #109110

Merged
merged 1 commit into from
Jan 30, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 22 additions & 1 deletion homeassistant/components/analytics_insights/config_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,12 @@
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,
Expand Down Expand Up @@ -58,6 +63,7 @@ async def async_step_user(
)
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")
Expand All @@ -81,6 +87,13 @@ async def async_step_user(
sort=True,
)
),
vol.Required(CONF_TRACKED_CUSTOM_INTEGRATIONS): SelectSelector(
SelectSelectorConfig(
options=list(custom_integrations),
multiple=True,
sort=True,
)
),
}
),
)
Expand All @@ -101,6 +114,7 @@ async def async_step_init(
)
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")
Expand All @@ -125,6 +139,13 @@ async def async_step_init(
sort=True,
)
),
vol.Required(CONF_TRACKED_CUSTOM_INTEGRATIONS): SelectSelector(
SelectSelectorConfig(
options=list(custom_integrations),
multiple=True,
sort=True,
)
),
},
),
self.options,
Expand Down
1 change: 1 addition & 0 deletions homeassistant/components/analytics_insights/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,6 @@
DOMAIN = "analytics_insights"

CONF_TRACKED_INTEGRATIONS = "tracked_integrations"
CONF_TRACKED_CUSTOM_INTEGRATIONS = "tracked_custom_integrations"

LOGGER = logging.getLogger(__package__)
30 changes: 27 additions & 3 deletions homeassistant/components/analytics_insights/coordinator.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from datetime import timedelta

from python_homeassistant_analytics import (
CustomIntegration,
HomeassistantAnalyticsClient,
HomeassistantAnalyticsConnectionError,
HomeassistantAnalyticsNotModifiedError,
Expand All @@ -14,14 +15,20 @@
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]):
Expand All @@ -43,10 +50,14 @@
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"
Expand All @@ -57,4 +68,17 @@
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

Check warning on line 84 in homeassistant/components/analytics_insights/coordinator.py

View check run for this annotation

Codecov / codecov/patch

homeassistant/components/analytics_insights/coordinator.py#L84

Added line #L84 was not covered by tests
3 changes: 3 additions & 0 deletions homeassistant/components/analytics_insights/icons.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@
"sensor": {
"core_integrations": {
"default": "mdi:puzzle"
},
"custom_integrations": {
"default": "mdi:puzzle-edit"
}
}
}
Expand Down
32 changes: 29 additions & 3 deletions homeassistant/components/analytics_insights/sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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(
Expand Down
7 changes: 7 additions & 0 deletions homeassistant/components/analytics_insights/strings.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,5 +23,12 @@
"abort": {
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]"
}
},
"entity": {
"sensor": {
"custom_integrations": {
"name": "{custom_integration_domain} (custom)"
}
}
}
}
19 changes: 16 additions & 3 deletions tests/components/analytics_insights/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,13 @@

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

Expand Down Expand Up @@ -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


Expand All @@ -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"],
},
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"hacs": {
"total": 157481,
"versions": {
"1.33.0": 123794,
"1.30.1": 1684,
"1.14.1": 23
}
}
}
47 changes: 47 additions & 0 deletions tests/components/analytics_insights/snapshots/test_sensor.ambr
Original file line number Diff line number Diff line change
@@ -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({
Expand Down
18 changes: 15 additions & 3 deletions tests/components/analytics_insights/test_config_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

from homeassistant import config_entries
from homeassistant.components.analytics_insights.const import (
CONF_TRACKED_CUSTOM_INTEGRATIONS,
CONF_TRACKED_INTEGRATIONS,
DOMAIN,
)
Expand All @@ -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


Expand All @@ -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)

Expand All @@ -87,13 +97,15 @@ 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()

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()
Expand Down
Loading