Skip to content

Commit

Permalink
Add Homeassistant Analytics Insights integration (#107634)
Browse files Browse the repository at this point in the history
* Add Homeassistant Analytics integration

* Add Homeassistant Analytics integration

* Add Homeassistant Analytics integration

* Fix feedback

* Fix test

* Update conftest.py

* Add some testcases

* Make code clear

* log exception

* Bump python-homeassistant-analytics to 0.2.1

* Bump python-homeassistant-analytics to 0.3.0

* Change domain to homeassistant_analytics_consumer

* Add integration name to config flow selector

* Update homeassistant/components/homeassistant_analytics_consumer/manifest.json

Co-authored-by: Sid <27780930+autinerd@users.noreply.github.com>

* Fix hassfest

* Apply suggestions from code review

Co-authored-by: Robert Resch <robert@resch.dev>

* Bump python-homeassistant-analytics to 0.4.0

* Rename to Home Assistant Analytics Insights

* Update homeassistant/components/analytics_insights/config_flow.py

Co-authored-by: Robert Resch <robert@resch.dev>

* Update homeassistant/components/analytics_insights/manifest.json

Co-authored-by: Robert Resch <robert@resch.dev>

* Rename to Home Assistant Analytics Insights

* add test

* Fallback to 0 when there is no data found

* Allow to select any integration

* Fix tests

* Fix tests

* Update tests/components/analytics_insights/conftest.py

Co-authored-by: Robert Resch <robert@resch.dev>

* Update tests/components/analytics_insights/test_sensor.py

Co-authored-by: Robert Resch <robert@resch.dev>

* Fix format

* Fix tests

---------

Co-authored-by: Sid <27780930+autinerd@users.noreply.github.com>
Co-authored-by: Robert Resch <robert@resch.dev>
  • Loading branch information
3 people authored Jan 23, 2024
1 parent 52ede95 commit d9f1450
Show file tree
Hide file tree
Showing 22 changed files with 3,226 additions and 0 deletions.
1 change: 1 addition & 0 deletions .strict-typing
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ homeassistant.components.ambient_station.*
homeassistant.components.amcrest.*
homeassistant.components.ampio.*
homeassistant.components.analytics.*
homeassistant.components.analytics_insights.*
homeassistant.components.android_ip_webcam.*
homeassistant.components.androidtv.*
homeassistant.components.androidtv_remote.*
Expand Down
2 changes: 2 additions & 0 deletions CODEOWNERS
Validating CODEOWNERS rules …
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,8 @@ build.json @home-assistant/supervisor
/homeassistant/components/amcrest/ @flacjacket
/homeassistant/components/analytics/ @home-assistant/core @ludeeus
/tests/components/analytics/ @home-assistant/core @ludeeus
/homeassistant/components/analytics_insights/ @joostlek
/tests/components/analytics_insights/ @joostlek
/homeassistant/components/android_ip_webcam/ @engrbm87
/tests/components/android_ip_webcam/ @engrbm87
/homeassistant/components/androidtv/ @JeffLIrion @ollo69
Expand Down
58 changes: 58 additions & 0 deletions homeassistant/components/analytics_insights/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
"""The Homeassistant Analytics integration."""
from __future__ import annotations

from dataclasses import dataclass

from python_homeassistant_analytics import HomeassistantAnalyticsClient

from homeassistant.config_entries import ConfigEntry
from homeassistant.const import Platform
from homeassistant.core import HomeAssistant
from homeassistant.helpers.aiohttp_client import async_get_clientsession

from .const import CONF_TRACKED_INTEGRATIONS, DOMAIN
from .coordinator import HomeassistantAnalyticsDataUpdateCoordinator

PLATFORMS: list[Platform] = [Platform.SENSOR]


@dataclass(frozen=True)
class AnalyticsData:
"""Analytics data class."""

coordinator: HomeassistantAnalyticsDataUpdateCoordinator
names: dict[str, str]


async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up Homeassistant Analytics from a config entry."""
client = HomeassistantAnalyticsClient(session=async_get_clientsession(hass))

integrations = await client.get_integrations()

names = {}
for integration in entry.options[CONF_TRACKED_INTEGRATIONS]:
if integration not in integrations:
names[integration] = integration
continue
names[integration] = integrations[integration].title

coordinator = HomeassistantAnalyticsDataUpdateCoordinator(hass, client)

await coordinator.async_config_entry_first_refresh()

hass.data.setdefault(DOMAIN, {})[entry.entry_id] = AnalyticsData(
coordinator=coordinator, names=names
)

await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)

return True


async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Unload a config entry."""
if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS):
hass.data[DOMAIN].pop(entry.entry_id)

return unload_ok
74 changes: 74 additions & 0 deletions homeassistant/components/analytics_insights/config_flow.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
"""Config flow for Homeassistant Analytics integration."""
from __future__ import annotations

from typing import Any

from python_homeassistant_analytics import (
HomeassistantAnalyticsClient,
HomeassistantAnalyticsConnectionError,
)
from python_homeassistant_analytics.models import IntegrationType
import voluptuous as vol

from homeassistant.config_entries import ConfigFlow
from homeassistant.data_entry_flow import FlowResult
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.selector import (
SelectOptionDict,
SelectSelector,
SelectSelectorConfig,
)

from .const import CONF_TRACKED_INTEGRATIONS, DOMAIN, LOGGER

INTEGRATION_TYPES_WITHOUT_ANALYTICS = (
IntegrationType.BRAND,
IntegrationType.ENTITY,
IntegrationType.VIRTUAL,
)


class HomeassistantAnalyticsConfigFlow(ConfigFlow, domain=DOMAIN):
"""Handle a config flow for Homeassistant Analytics."""

async def async_step_user(
self, user_input: dict[str, Any] | None = None
) -> FlowResult:
"""Handle the initial step."""
self._async_abort_entries_match()
if user_input:
return self.async_create_entry(
title="Home Assistant Analytics Insights", data={}, options=user_input
)

client = HomeassistantAnalyticsClient(
session=async_get_clientsession(self.hass)
)
try:
integrations = await client.get_integrations()
except HomeassistantAnalyticsConnectionError:
LOGGER.exception("Error connecting to Home Assistant analytics")
return self.async_abort(reason="cannot_connect")

options = [
SelectOptionDict(
value=domain,
label=integration.title,
)
for domain, integration in integrations.items()
if integration.integration_type not in INTEGRATION_TYPES_WITHOUT_ANALYTICS
]
return self.async_show_form(
step_id="user",
data_schema=vol.Schema(
{
vol.Required(CONF_TRACKED_INTEGRATIONS): SelectSelector(
SelectSelectorConfig(
options=options,
multiple=True,
sort=True,
)
),
}
),
)
8 changes: 8 additions & 0 deletions homeassistant/components/analytics_insights/const.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
"""Constants for the Homeassistant Analytics integration."""
import logging

DOMAIN = "analytics_insights"

CONF_TRACKED_INTEGRATIONS = "tracked_integrations"

LOGGER = logging.getLogger(__package__)
53 changes: 53 additions & 0 deletions homeassistant/components/analytics_insights/coordinator.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
"""DataUpdateCoordinator for the Homeassistant Analytics integration."""
from __future__ import annotations

from datetime import timedelta

from python_homeassistant_analytics import (
HomeassistantAnalyticsClient,
HomeassistantAnalyticsConnectionError,
HomeassistantAnalyticsNotModifiedError,
)

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


class HomeassistantAnalyticsDataUpdateCoordinator(
DataUpdateCoordinator[dict[str, int]]
):
"""A Homeassistant Analytics Data Update Coordinator."""

config_entry: ConfigEntry

def __init__(
self, hass: HomeAssistant, client: HomeassistantAnalyticsClient
) -> None:
"""Initialize the Homeassistant Analytics data coordinator."""
super().__init__(
hass,
LOGGER,
name=DOMAIN,
update_interval=timedelta(hours=12),
)
self._client = client
self._tracked_integrations = self.config_entry.options[
CONF_TRACKED_INTEGRATIONS
]

async def _async_update_data(self) -> dict[str, int]:
try:
data = await self._client.get_current_analytics()
except HomeassistantAnalyticsConnectionError as err:
raise UpdateFailed(
"Error communicating with Homeassistant Analytics"
) from err
except HomeassistantAnalyticsNotModifiedError:
return self.data
return {
integration: data.integrations.get(integration, 0)
for integration in self._tracked_integrations
}
11 changes: 11 additions & 0 deletions homeassistant/components/analytics_insights/manifest.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"domain": "analytics_insights",
"name": "Home Assistant Analytics Insights",
"codeowners": ["@joostlek"],
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/analytics_insights",
"integration_type": "service",
"iot_class": "cloud_polling",
"loggers": ["python_homeassistant_analytics"],
"requirements": ["python-homeassistant-analytics==0.5.0"]
}
62 changes: 62 additions & 0 deletions homeassistant/components/analytics_insights/sensor.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
"""Sensor for Home Assistant analytics."""
from __future__ import annotations

from homeassistant.components.sensor import SensorEntity, SensorStateClass
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.update_coordinator import CoordinatorEntity

from . import AnalyticsData
from .const import DOMAIN
from .coordinator import HomeassistantAnalyticsDataUpdateCoordinator


async def async_setup_entry(
hass: HomeAssistant,
entry: ConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Initialize the entries."""

analytics_data: AnalyticsData = hass.data[DOMAIN][entry.entry_id]
async_add_entities(
HomeassistantAnalyticsSensor(
analytics_data.coordinator,
integration_domain,
analytics_data.names[integration_domain],
)
for integration_domain in analytics_data.coordinator.data
)


class HomeassistantAnalyticsSensor(
CoordinatorEntity[HomeassistantAnalyticsDataUpdateCoordinator], SensorEntity
):
"""Home Assistant Analytics Sensor."""

_attr_has_entity_name = True
_attr_state_class = SensorStateClass.TOTAL
_attr_native_unit_of_measurement = "active installations"

def __init__(
self,
coordinator: HomeassistantAnalyticsDataUpdateCoordinator,
integration_domain: str,
name: str,
) -> None:
"""Initialize the sensor."""
super().__init__(coordinator)
self._attr_name = name
self._attr_unique_id = f"core_{integration_domain}_active_installations"
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, DOMAIN)},
entry_type=DeviceEntryType.SERVICE,
)
self._integration_domain = integration_domain

@property
def native_value(self) -> int | None:
"""Return the state of the sensor."""
return self.coordinator.data.get(self._integration_domain)
18 changes: 18 additions & 0 deletions homeassistant/components/analytics_insights/strings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
{
"config": {
"step": {
"user": {
"data": {
"tracked_integrations": "Integrations"
}
}
},
"error": {
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
"unknown": "[%key:common::config_flow::error::unknown%]"
},
"abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_service%]"
}
}
}
1 change: 1 addition & 0 deletions homeassistant/generated/config_flows.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
"amberelectric",
"ambiclimate",
"ambient_station",
"analytics_insights",
"android_ip_webcam",
"androidtv",
"androidtv_remote",
Expand Down
6 changes: 6 additions & 0 deletions homeassistant/generated/integrations.json
Original file line number Diff line number Diff line change
Expand Up @@ -255,6 +255,12 @@
"config_flow": false,
"iot_class": "cloud_polling"
},
"analytics_insights": {
"name": "Home Assistant Analytics Insights",
"integration_type": "service",
"config_flow": true,
"iot_class": "cloud_polling"
},
"android_ip_webcam": {
"name": "Android IP Webcam",
"integration_type": "hub",
Expand Down
10 changes: 10 additions & 0 deletions mypy.ini
Original file line number Diff line number Diff line change
Expand Up @@ -450,6 +450,16 @@ disallow_untyped_defs = true
warn_return_any = true
warn_unreachable = true

[mypy-homeassistant.components.analytics_insights.*]
check_untyped_defs = true
disallow_incomplete_defs = true
disallow_subclassing_any = true
disallow_untyped_calls = true
disallow_untyped_decorators = true
disallow_untyped_defs = true
warn_return_any = true
warn_unreachable = true

[mypy-homeassistant.components.android_ip_webcam.*]
check_untyped_defs = true
disallow_incomplete_defs = true
Expand Down
3 changes: 3 additions & 0 deletions requirements_all.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2197,6 +2197,9 @@ python-gc100==1.0.3a0
# homeassistant.components.gitlab_ci
python-gitlab==1.6.0

# homeassistant.components.analytics_insights
python-homeassistant-analytics==0.5.0

# homeassistant.components.homewizard
python-homewizard-energy==4.1.0

Expand Down
3 changes: 3 additions & 0 deletions requirements_test_all.txt
Original file line number Diff line number Diff line change
Expand Up @@ -1673,6 +1673,9 @@ python-ecobee-api==0.2.17
# homeassistant.components.fully_kiosk
python-fullykiosk==0.0.12

# homeassistant.components.analytics_insights
python-homeassistant-analytics==0.5.0

# homeassistant.components.homewizard
python-homewizard-energy==4.1.0

Expand Down
11 changes: 11 additions & 0 deletions tests/components/analytics_insights/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
"""Tests for the Homeassistant Analytics integration."""
from homeassistant.core import HomeAssistant

from tests.common import MockConfigEntry


async def setup_integration(hass: HomeAssistant, config_entry: MockConfigEntry) -> None:
"""Fixture for setting up the component."""
config_entry.add_to_hass(hass)

await hass.config_entries.async_setup(config_entry.entry_id)
Loading

0 comments on commit d9f1450

Please sign in to comment.