Add analytics integration (#48256)
Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
This commit is contained in:
parent
6b7bcbbc96
commit
09c51da3a2
17 changed files with 734 additions and 5 deletions
|
@ -36,6 +36,7 @@ homeassistant/components/alpha_vantage/* @fabaff
|
||||||
homeassistant/components/ambiclimate/* @danielhiversen
|
homeassistant/components/ambiclimate/* @danielhiversen
|
||||||
homeassistant/components/ambient_station/* @bachya
|
homeassistant/components/ambient_station/* @bachya
|
||||||
homeassistant/components/amcrest/* @pnbruckner
|
homeassistant/components/amcrest/* @pnbruckner
|
||||||
|
homeassistant/components/analytics/* @home-assistant/core
|
||||||
homeassistant/components/androidtv/* @JeffLIrion
|
homeassistant/components/androidtv/* @JeffLIrion
|
||||||
homeassistant/components/apache_kafka/* @bachya
|
homeassistant/components/apache_kafka/* @bachya
|
||||||
homeassistant/components/api/* @home-assistant/core
|
homeassistant/components/api/* @home-assistant/core
|
||||||
|
|
77
homeassistant/components/analytics/__init__.py
Normal file
77
homeassistant/components/analytics/__init__.py
Normal file
|
@ -0,0 +1,77 @@
|
||||||
|
"""Send instance and usage analytics."""
|
||||||
|
import voluptuous as vol
|
||||||
|
|
||||||
|
from homeassistant.components import websocket_api
|
||||||
|
from homeassistant.const import EVENT_HOMEASSISTANT_STARTED
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.helpers.event import async_call_later, async_track_time_interval
|
||||||
|
|
||||||
|
from .analytics import Analytics
|
||||||
|
from .const import ATTR_HUUID, ATTR_PREFERENCES, DOMAIN, INTERVAL, PREFERENCE_SCHEMA
|
||||||
|
|
||||||
|
|
||||||
|
async def async_setup(hass: HomeAssistant, _):
|
||||||
|
"""Set up the analytics integration."""
|
||||||
|
analytics = Analytics(hass)
|
||||||
|
|
||||||
|
# Load stored data
|
||||||
|
await analytics.load()
|
||||||
|
|
||||||
|
async def start_schedule(_event):
|
||||||
|
"""Start the send schedule after the started event."""
|
||||||
|
# Wait 15 min after started
|
||||||
|
async_call_later(hass, 900, analytics.send_analytics)
|
||||||
|
|
||||||
|
# Send every day
|
||||||
|
async_track_time_interval(hass, analytics.send_analytics, INTERVAL)
|
||||||
|
|
||||||
|
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STARTED, start_schedule)
|
||||||
|
|
||||||
|
websocket_api.async_register_command(hass, websocket_analytics)
|
||||||
|
websocket_api.async_register_command(hass, websocket_analytics_preferences)
|
||||||
|
|
||||||
|
hass.data[DOMAIN] = analytics
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
@websocket_api.require_admin
|
||||||
|
@websocket_api.async_response
|
||||||
|
@websocket_api.websocket_command({vol.Required("type"): "analytics"})
|
||||||
|
async def websocket_analytics(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
connection: websocket_api.connection.ActiveConnection,
|
||||||
|
msg: dict,
|
||||||
|
) -> None:
|
||||||
|
"""Return analytics preferences."""
|
||||||
|
analytics: Analytics = hass.data[DOMAIN]
|
||||||
|
huuid = await hass.helpers.instance_id.async_get()
|
||||||
|
connection.send_result(
|
||||||
|
msg["id"],
|
||||||
|
{ATTR_PREFERENCES: analytics.preferences, ATTR_HUUID: huuid},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@websocket_api.require_admin
|
||||||
|
@websocket_api.async_response
|
||||||
|
@websocket_api.websocket_command(
|
||||||
|
{
|
||||||
|
vol.Required("type"): "analytics/preferences",
|
||||||
|
vol.Required("preferences", default={}): PREFERENCE_SCHEMA,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
async def websocket_analytics_preferences(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
connection: websocket_api.connection.ActiveConnection,
|
||||||
|
msg: dict,
|
||||||
|
) -> None:
|
||||||
|
"""Update analytics preferences."""
|
||||||
|
preferences = msg[ATTR_PREFERENCES]
|
||||||
|
analytics: Analytics = hass.data[DOMAIN]
|
||||||
|
|
||||||
|
await analytics.save_preferences(preferences)
|
||||||
|
await analytics.send_analytics()
|
||||||
|
|
||||||
|
connection.send_result(
|
||||||
|
msg["id"],
|
||||||
|
{ATTR_PREFERENCES: analytics.preferences},
|
||||||
|
)
|
212
homeassistant/components/analytics/analytics.py
Normal file
212
homeassistant/components/analytics/analytics.py
Normal file
|
@ -0,0 +1,212 @@
|
||||||
|
"""Analytics helper class for the analytics integration."""
|
||||||
|
import asyncio
|
||||||
|
|
||||||
|
import aiohttp
|
||||||
|
import async_timeout
|
||||||
|
|
||||||
|
from homeassistant.components import hassio
|
||||||
|
from homeassistant.components.api import ATTR_INSTALLATION_TYPE
|
||||||
|
from homeassistant.components.automation.const import DOMAIN as AUTOMATION_DOMAIN
|
||||||
|
from homeassistant.const import __version__ as HA_VERSION
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||||
|
from homeassistant.helpers.storage import Store
|
||||||
|
from homeassistant.helpers.system_info import async_get_system_info
|
||||||
|
from homeassistant.loader import async_get_integration
|
||||||
|
|
||||||
|
from .const import (
|
||||||
|
ANALYTICS_ENDPOINT_URL,
|
||||||
|
ATTR_ADDON_COUNT,
|
||||||
|
ATTR_ADDONS,
|
||||||
|
ATTR_AUTO_UPDATE,
|
||||||
|
ATTR_AUTOMATION_COUNT,
|
||||||
|
ATTR_BASE,
|
||||||
|
ATTR_DIAGNOSTICS,
|
||||||
|
ATTR_HEALTHY,
|
||||||
|
ATTR_HUUID,
|
||||||
|
ATTR_INTEGRATION_COUNT,
|
||||||
|
ATTR_INTEGRATIONS,
|
||||||
|
ATTR_ONBOARDED,
|
||||||
|
ATTR_PREFERENCES,
|
||||||
|
ATTR_PROTECTED,
|
||||||
|
ATTR_SLUG,
|
||||||
|
ATTR_STATE_COUNT,
|
||||||
|
ATTR_STATISTICS,
|
||||||
|
ATTR_SUPERVISOR,
|
||||||
|
ATTR_SUPPORTED,
|
||||||
|
ATTR_USAGE,
|
||||||
|
ATTR_USER_COUNT,
|
||||||
|
ATTR_VERSION,
|
||||||
|
LOGGER,
|
||||||
|
PREFERENCE_SCHEMA,
|
||||||
|
STORAGE_KEY,
|
||||||
|
STORAGE_VERSION,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class Analytics:
|
||||||
|
"""Analytics helper class for the analytics integration."""
|
||||||
|
|
||||||
|
def __init__(self, hass: HomeAssistant) -> None:
|
||||||
|
"""Initialize the Analytics class."""
|
||||||
|
self.hass: HomeAssistant = hass
|
||||||
|
self.session = async_get_clientsession(hass)
|
||||||
|
self._data = {ATTR_PREFERENCES: {}, ATTR_ONBOARDED: False}
|
||||||
|
self._store: Store = hass.helpers.storage.Store(STORAGE_VERSION, STORAGE_KEY)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def preferences(self) -> dict:
|
||||||
|
"""Return the current active preferences."""
|
||||||
|
preferences = self._data[ATTR_PREFERENCES]
|
||||||
|
return {
|
||||||
|
ATTR_BASE: preferences.get(ATTR_BASE, False),
|
||||||
|
ATTR_DIAGNOSTICS: preferences.get(ATTR_DIAGNOSTICS, False),
|
||||||
|
ATTR_USAGE: preferences.get(ATTR_USAGE, False),
|
||||||
|
ATTR_STATISTICS: preferences.get(ATTR_STATISTICS, False),
|
||||||
|
}
|
||||||
|
|
||||||
|
@property
|
||||||
|
def onboarded(self) -> bool:
|
||||||
|
"""Return bool if the user has made a choice."""
|
||||||
|
return self._data[ATTR_ONBOARDED]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def supervisor(self) -> bool:
|
||||||
|
"""Return bool if a supervisor is present."""
|
||||||
|
return hassio.is_hassio(self.hass)
|
||||||
|
|
||||||
|
async def load(self) -> None:
|
||||||
|
"""Load preferences."""
|
||||||
|
stored = await self._store.async_load()
|
||||||
|
if stored:
|
||||||
|
self._data = stored
|
||||||
|
if self.supervisor:
|
||||||
|
supervisor_info = hassio.get_supervisor_info(self.hass)
|
||||||
|
if not self.onboarded:
|
||||||
|
# User have not configured analytics, get this setting from the supervisor
|
||||||
|
if supervisor_info[ATTR_DIAGNOSTICS] and not self.preferences.get(
|
||||||
|
ATTR_DIAGNOSTICS, False
|
||||||
|
):
|
||||||
|
self._data[ATTR_PREFERENCES][ATTR_DIAGNOSTICS] = True
|
||||||
|
elif not supervisor_info[ATTR_DIAGNOSTICS] and self.preferences.get(
|
||||||
|
ATTR_DIAGNOSTICS, False
|
||||||
|
):
|
||||||
|
self._data[ATTR_PREFERENCES][ATTR_DIAGNOSTICS] = False
|
||||||
|
|
||||||
|
async def save_preferences(self, preferences: dict) -> None:
|
||||||
|
"""Save preferences."""
|
||||||
|
preferences = PREFERENCE_SCHEMA(preferences)
|
||||||
|
self._data[ATTR_PREFERENCES].update(preferences)
|
||||||
|
self._data[ATTR_ONBOARDED] = True
|
||||||
|
await self._store.async_save(self._data)
|
||||||
|
|
||||||
|
if self.supervisor:
|
||||||
|
await hassio.async_update_diagnostics(
|
||||||
|
self.hass, self.preferences.get(ATTR_DIAGNOSTICS, False)
|
||||||
|
)
|
||||||
|
|
||||||
|
async def send_analytics(self, _=None) -> None:
|
||||||
|
"""Send analytics."""
|
||||||
|
supervisor_info = None
|
||||||
|
|
||||||
|
if not self.onboarded or not self.preferences.get(ATTR_BASE, False):
|
||||||
|
LOGGER.debug("Nothing to submit")
|
||||||
|
return
|
||||||
|
|
||||||
|
huuid = await self.hass.helpers.instance_id.async_get()
|
||||||
|
|
||||||
|
if self.supervisor:
|
||||||
|
supervisor_info = hassio.get_supervisor_info(self.hass)
|
||||||
|
|
||||||
|
system_info = await async_get_system_info(self.hass)
|
||||||
|
integrations = []
|
||||||
|
addons = []
|
||||||
|
payload: dict = {
|
||||||
|
ATTR_HUUID: huuid,
|
||||||
|
ATTR_VERSION: HA_VERSION,
|
||||||
|
ATTR_INSTALLATION_TYPE: system_info[ATTR_INSTALLATION_TYPE],
|
||||||
|
}
|
||||||
|
|
||||||
|
if supervisor_info is not None:
|
||||||
|
payload[ATTR_SUPERVISOR] = {
|
||||||
|
ATTR_HEALTHY: supervisor_info[ATTR_HEALTHY],
|
||||||
|
ATTR_SUPPORTED: supervisor_info[ATTR_SUPPORTED],
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.preferences.get(ATTR_USAGE, False) or self.preferences.get(
|
||||||
|
ATTR_STATISTICS, False
|
||||||
|
):
|
||||||
|
configured_integrations = await asyncio.gather(
|
||||||
|
*[
|
||||||
|
async_get_integration(self.hass, domain)
|
||||||
|
for domain in self.hass.config.components
|
||||||
|
# Filter out platforms.
|
||||||
|
if "." not in domain
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
for integration in configured_integrations:
|
||||||
|
if integration.disabled or not integration.is_built_in:
|
||||||
|
continue
|
||||||
|
|
||||||
|
integrations.append(integration.domain)
|
||||||
|
|
||||||
|
if supervisor_info is not None:
|
||||||
|
installed_addons = await asyncio.gather(
|
||||||
|
*[
|
||||||
|
hassio.async_get_addon_info(self.hass, addon[ATTR_SLUG])
|
||||||
|
for addon in supervisor_info[ATTR_ADDONS]
|
||||||
|
]
|
||||||
|
)
|
||||||
|
for addon in installed_addons:
|
||||||
|
addons.append(
|
||||||
|
{
|
||||||
|
ATTR_SLUG: addon[ATTR_SLUG],
|
||||||
|
ATTR_PROTECTED: addon[ATTR_PROTECTED],
|
||||||
|
ATTR_VERSION: addon[ATTR_VERSION],
|
||||||
|
ATTR_AUTO_UPDATE: addon[ATTR_AUTO_UPDATE],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
if self.preferences.get(ATTR_USAGE, False):
|
||||||
|
payload[ATTR_INTEGRATIONS] = integrations
|
||||||
|
if supervisor_info is not None:
|
||||||
|
payload[ATTR_ADDONS] = addons
|
||||||
|
|
||||||
|
if self.preferences.get(ATTR_STATISTICS, False):
|
||||||
|
payload[ATTR_STATE_COUNT] = len(self.hass.states.async_all())
|
||||||
|
payload[ATTR_AUTOMATION_COUNT] = len(
|
||||||
|
self.hass.states.async_all(AUTOMATION_DOMAIN)
|
||||||
|
)
|
||||||
|
payload[ATTR_INTEGRATION_COUNT] = len(integrations)
|
||||||
|
if supervisor_info is not None:
|
||||||
|
payload[ATTR_ADDON_COUNT] = len(addons)
|
||||||
|
payload[ATTR_USER_COUNT] = len(
|
||||||
|
[
|
||||||
|
user
|
||||||
|
for user in await self.hass.auth.async_get_users()
|
||||||
|
if not user.system_generated
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
with async_timeout.timeout(30):
|
||||||
|
response = await self.session.post(ANALYTICS_ENDPOINT_URL, json=payload)
|
||||||
|
if response.status == 200:
|
||||||
|
LOGGER.info(
|
||||||
|
(
|
||||||
|
"Submitted analytics to Home Assistant servers. "
|
||||||
|
"Information submitted includes %s"
|
||||||
|
),
|
||||||
|
payload,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
LOGGER.warning(
|
||||||
|
"Sending analytics failed with statuscode %s", response.status
|
||||||
|
)
|
||||||
|
except asyncio.TimeoutError:
|
||||||
|
LOGGER.error("Timeout sending analytics to %s", ANALYTICS_ENDPOINT_URL)
|
||||||
|
except aiohttp.ClientError as err:
|
||||||
|
LOGGER.error(
|
||||||
|
"Error sending analytics to %s: %r", ANALYTICS_ENDPOINT_URL, err
|
||||||
|
)
|
47
homeassistant/components/analytics/const.py
Normal file
47
homeassistant/components/analytics/const.py
Normal file
|
@ -0,0 +1,47 @@
|
||||||
|
"""Constants for the analytics integration."""
|
||||||
|
from datetime import timedelta
|
||||||
|
import logging
|
||||||
|
|
||||||
|
import voluptuous as vol
|
||||||
|
|
||||||
|
ANALYTICS_ENDPOINT_URL = "https://analytics-api.home-assistant.io/v1"
|
||||||
|
DOMAIN = "analytics"
|
||||||
|
INTERVAL = timedelta(days=1)
|
||||||
|
STORAGE_KEY = "core.analytics"
|
||||||
|
STORAGE_VERSION = 1
|
||||||
|
|
||||||
|
|
||||||
|
LOGGER: logging.Logger = logging.getLogger(__package__)
|
||||||
|
|
||||||
|
ATTR_ADDON_COUNT = "addon_count"
|
||||||
|
ATTR_ADDONS = "addons"
|
||||||
|
ATTR_AUTO_UPDATE = "auto_update"
|
||||||
|
ATTR_AUTOMATION_COUNT = "automation_count"
|
||||||
|
ATTR_BASE = "base"
|
||||||
|
ATTR_DIAGNOSTICS = "diagnostics"
|
||||||
|
ATTR_HEALTHY = "healthy"
|
||||||
|
ATTR_HUUID = "huuid"
|
||||||
|
ATTR_INSTALLATION_TYPE = "installation_type"
|
||||||
|
ATTR_INTEGRATION_COUNT = "integration_count"
|
||||||
|
ATTR_INTEGRATIONS = "integrations"
|
||||||
|
ATTR_ONBOARDED = "onboarded"
|
||||||
|
ATTR_PREFERENCES = "preferences"
|
||||||
|
ATTR_PROTECTED = "protected"
|
||||||
|
ATTR_SLUG = "slug"
|
||||||
|
ATTR_STATE_COUNT = "state_count"
|
||||||
|
ATTR_STATISTICS = "statistics"
|
||||||
|
ATTR_SUPERVISOR = "supervisor"
|
||||||
|
ATTR_SUPPORTED = "supported"
|
||||||
|
ATTR_USAGE = "usage"
|
||||||
|
ATTR_USER_COUNT = "user_count"
|
||||||
|
ATTR_VERSION = "version"
|
||||||
|
|
||||||
|
|
||||||
|
PREFERENCE_SCHEMA = vol.Schema(
|
||||||
|
{
|
||||||
|
vol.Optional(ATTR_BASE): bool,
|
||||||
|
vol.Optional(ATTR_DIAGNOSTICS): bool,
|
||||||
|
vol.Optional(ATTR_STATISTICS): bool,
|
||||||
|
vol.Optional(ATTR_USAGE): bool,
|
||||||
|
}
|
||||||
|
)
|
8
homeassistant/components/analytics/manifest.json
Normal file
8
homeassistant/components/analytics/manifest.json
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
{
|
||||||
|
"domain": "analytics",
|
||||||
|
"name": "Analytics",
|
||||||
|
"documentation": "https://www.home-assistant.io/integrations/analytics",
|
||||||
|
"codeowners": ["@home-assistant/core"],
|
||||||
|
"dependencies": ["api", "websocket_api"],
|
||||||
|
"quality_scale": "internal"
|
||||||
|
}
|
|
@ -3,6 +3,7 @@
|
||||||
"name": "Default Config",
|
"name": "Default Config",
|
||||||
"documentation": "https://www.home-assistant.io/integrations/default_config",
|
"documentation": "https://www.home-assistant.io/integrations/default_config",
|
||||||
"dependencies": [
|
"dependencies": [
|
||||||
|
"analytics",
|
||||||
"automation",
|
"automation",
|
||||||
"cloud",
|
"cloud",
|
||||||
"counter",
|
"counter",
|
||||||
|
|
|
@ -161,6 +161,16 @@ async def async_get_addon_info(hass: HomeAssistantType, slug: str) -> dict:
|
||||||
return await hassio.get_addon_info(slug)
|
return await hassio.get_addon_info(slug)
|
||||||
|
|
||||||
|
|
||||||
|
@bind_hass
|
||||||
|
async def async_update_diagnostics(hass: HomeAssistantType, diagnostics: bool) -> dict:
|
||||||
|
"""Update Supervisor diagnostics toggle.
|
||||||
|
|
||||||
|
The caller of the function should handle HassioAPIError.
|
||||||
|
"""
|
||||||
|
hassio = hass.data[DOMAIN]
|
||||||
|
return await hassio.update_diagnostics(diagnostics)
|
||||||
|
|
||||||
|
|
||||||
@bind_hass
|
@bind_hass
|
||||||
@api_data
|
@api_data
|
||||||
async def async_install_addon(hass: HomeAssistantType, slug: str) -> dict:
|
async def async_install_addon(hass: HomeAssistantType, slug: str) -> dict:
|
||||||
|
|
|
@ -185,6 +185,16 @@ class HassIO:
|
||||||
"""
|
"""
|
||||||
return self.send_command("/supervisor/options", payload={"timezone": timezone})
|
return self.send_command("/supervisor/options", payload={"timezone": timezone})
|
||||||
|
|
||||||
|
@_api_bool
|
||||||
|
def update_diagnostics(self, diagnostics: bool):
|
||||||
|
"""Update Supervisor diagnostics setting.
|
||||||
|
|
||||||
|
This method return a coroutine.
|
||||||
|
"""
|
||||||
|
return self.send_command(
|
||||||
|
"/supervisor/options", payload={"diagnostics": diagnostics}
|
||||||
|
)
|
||||||
|
|
||||||
async def send_command(self, command, method="post", payload=None, timeout=10):
|
async def send_command(self, command, method="post", payload=None, timeout=10):
|
||||||
"""Send API command to Hass.io.
|
"""Send API command to Hass.io.
|
||||||
|
|
||||||
|
|
|
@ -4,10 +4,17 @@ from homeassistant.helpers.storage import Store
|
||||||
from homeassistant.loader import bind_hass
|
from homeassistant.loader import bind_hass
|
||||||
|
|
||||||
from . import views
|
from . import views
|
||||||
from .const import DOMAIN, STEP_CORE_CONFIG, STEP_INTEGRATION, STEP_USER, STEPS
|
from .const import (
|
||||||
|
DOMAIN,
|
||||||
|
STEP_ANALYTICS,
|
||||||
|
STEP_CORE_CONFIG,
|
||||||
|
STEP_INTEGRATION,
|
||||||
|
STEP_USER,
|
||||||
|
STEPS,
|
||||||
|
)
|
||||||
|
|
||||||
STORAGE_KEY = DOMAIN
|
STORAGE_KEY = DOMAIN
|
||||||
STORAGE_VERSION = 3
|
STORAGE_VERSION = 4
|
||||||
|
|
||||||
|
|
||||||
class OnboadingStorage(Store):
|
class OnboadingStorage(Store):
|
||||||
|
@ -20,6 +27,8 @@ class OnboadingStorage(Store):
|
||||||
old_data["done"].append(STEP_INTEGRATION)
|
old_data["done"].append(STEP_INTEGRATION)
|
||||||
if old_version < 3:
|
if old_version < 3:
|
||||||
old_data["done"].append(STEP_CORE_CONFIG)
|
old_data["done"].append(STEP_CORE_CONFIG)
|
||||||
|
if old_version < 4:
|
||||||
|
old_data["done"].append(STEP_ANALYTICS)
|
||||||
return old_data
|
return old_data
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -3,7 +3,8 @@ DOMAIN = "onboarding"
|
||||||
STEP_USER = "user"
|
STEP_USER = "user"
|
||||||
STEP_CORE_CONFIG = "core_config"
|
STEP_CORE_CONFIG = "core_config"
|
||||||
STEP_INTEGRATION = "integration"
|
STEP_INTEGRATION = "integration"
|
||||||
|
STEP_ANALYTICS = "analytics"
|
||||||
|
|
||||||
STEPS = [STEP_USER, STEP_CORE_CONFIG, STEP_INTEGRATION]
|
STEPS = [STEP_USER, STEP_CORE_CONFIG, STEP_ANALYTICS, STEP_INTEGRATION]
|
||||||
|
|
||||||
DEFAULT_AREAS = ("living_room", "kitchen", "bedroom")
|
DEFAULT_AREAS = ("living_room", "kitchen", "bedroom")
|
||||||
|
|
|
@ -6,6 +6,7 @@
|
||||||
"hassio"
|
"hassio"
|
||||||
],
|
],
|
||||||
"dependencies": [
|
"dependencies": [
|
||||||
|
"analytics",
|
||||||
"auth",
|
"auth",
|
||||||
"http",
|
"http",
|
||||||
"person"
|
"person"
|
||||||
|
|
|
@ -14,6 +14,7 @@ from homeassistant.core import callback
|
||||||
from .const import (
|
from .const import (
|
||||||
DEFAULT_AREAS,
|
DEFAULT_AREAS,
|
||||||
DOMAIN,
|
DOMAIN,
|
||||||
|
STEP_ANALYTICS,
|
||||||
STEP_CORE_CONFIG,
|
STEP_CORE_CONFIG,
|
||||||
STEP_INTEGRATION,
|
STEP_INTEGRATION,
|
||||||
STEP_USER,
|
STEP_USER,
|
||||||
|
@ -27,6 +28,7 @@ async def async_setup(hass, data, store):
|
||||||
hass.http.register_view(UserOnboardingView(data, store))
|
hass.http.register_view(UserOnboardingView(data, store))
|
||||||
hass.http.register_view(CoreConfigOnboardingView(data, store))
|
hass.http.register_view(CoreConfigOnboardingView(data, store))
|
||||||
hass.http.register_view(IntegrationOnboardingView(data, store))
|
hass.http.register_view(IntegrationOnboardingView(data, store))
|
||||||
|
hass.http.register_view(AnalyticsOnboardingView(data, store))
|
||||||
|
|
||||||
|
|
||||||
class OnboardingView(HomeAssistantView):
|
class OnboardingView(HomeAssistantView):
|
||||||
|
@ -217,6 +219,28 @@ class IntegrationOnboardingView(_BaseOnboardingView):
|
||||||
return self.json({"auth_code": auth_code})
|
return self.json({"auth_code": auth_code})
|
||||||
|
|
||||||
|
|
||||||
|
class AnalyticsOnboardingView(_BaseOnboardingView):
|
||||||
|
"""View to finish analytics onboarding step."""
|
||||||
|
|
||||||
|
url = "/api/onboarding/analytics"
|
||||||
|
name = "api:onboarding:analytics"
|
||||||
|
step = STEP_ANALYTICS
|
||||||
|
|
||||||
|
async def post(self, request):
|
||||||
|
"""Handle finishing analytics step."""
|
||||||
|
hass = request.app["hass"]
|
||||||
|
|
||||||
|
async with self._lock:
|
||||||
|
if self._async_is_done():
|
||||||
|
return self.json_message(
|
||||||
|
"Analytics config step already done", HTTP_FORBIDDEN
|
||||||
|
)
|
||||||
|
|
||||||
|
await self._async_mark_done(hass)
|
||||||
|
|
||||||
|
return self.json({})
|
||||||
|
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
def _async_get_hass_provider(hass):
|
def _async_get_hass_provider(hass):
|
||||||
"""Get the Home Assistant auth provider."""
|
"""Get the Home Assistant auth provider."""
|
||||||
|
|
1
tests/components/analytics/__init__.py
Normal file
1
tests/components/analytics/__init__.py
Normal file
|
@ -0,0 +1 @@
|
||||||
|
"""Tests for the analytics integration."""
|
266
tests/components/analytics/test_analytics.py
Normal file
266
tests/components/analytics/test_analytics.py
Normal file
|
@ -0,0 +1,266 @@
|
||||||
|
"""The tests for the analytics ."""
|
||||||
|
from unittest.mock import AsyncMock, Mock, patch
|
||||||
|
|
||||||
|
import aiohttp
|
||||||
|
|
||||||
|
from homeassistant.components.analytics.analytics import Analytics
|
||||||
|
from homeassistant.components.analytics.const import (
|
||||||
|
ANALYTICS_ENDPOINT_URL,
|
||||||
|
ATTR_BASE,
|
||||||
|
ATTR_DIAGNOSTICS,
|
||||||
|
ATTR_PREFERENCES,
|
||||||
|
ATTR_STATISTICS,
|
||||||
|
ATTR_USAGE,
|
||||||
|
)
|
||||||
|
from homeassistant.const import __version__ as HA_VERSION
|
||||||
|
|
||||||
|
MOCK_HUUID = "abcdefg"
|
||||||
|
|
||||||
|
|
||||||
|
async def test_no_send(hass, caplog, aioclient_mock):
|
||||||
|
"""Test send when no prefrences are defined."""
|
||||||
|
aioclient_mock.post(ANALYTICS_ENDPOINT_URL, status=200)
|
||||||
|
analytics = Analytics(hass)
|
||||||
|
with patch(
|
||||||
|
"homeassistant.components.hassio.is_hassio",
|
||||||
|
side_effect=Mock(return_value=False),
|
||||||
|
), patch("homeassistant.helpers.instance_id.async_get", return_value=MOCK_HUUID):
|
||||||
|
await analytics.load()
|
||||||
|
assert not analytics.preferences[ATTR_BASE]
|
||||||
|
|
||||||
|
await analytics.send_analytics()
|
||||||
|
|
||||||
|
assert "Nothing to submit" in caplog.text
|
||||||
|
assert len(aioclient_mock.mock_calls) == 0
|
||||||
|
|
||||||
|
|
||||||
|
async def test_load_with_supervisor_diagnostics(hass):
|
||||||
|
"""Test loading with a supervisor that has diagnostics enabled."""
|
||||||
|
analytics = Analytics(hass)
|
||||||
|
assert not analytics.preferences[ATTR_DIAGNOSTICS]
|
||||||
|
with patch(
|
||||||
|
"homeassistant.components.hassio.get_supervisor_info",
|
||||||
|
side_effect=Mock(return_value={"diagnostics": True}),
|
||||||
|
), patch(
|
||||||
|
"homeassistant.components.hassio.is_hassio",
|
||||||
|
side_effect=Mock(return_value=True),
|
||||||
|
):
|
||||||
|
await analytics.load()
|
||||||
|
assert analytics.preferences[ATTR_DIAGNOSTICS]
|
||||||
|
|
||||||
|
|
||||||
|
async def test_load_with_supervisor_without_diagnostics(hass):
|
||||||
|
"""Test loading with a supervisor that has not diagnostics enabled."""
|
||||||
|
analytics = Analytics(hass)
|
||||||
|
analytics._data[ATTR_PREFERENCES][ATTR_DIAGNOSTICS] = True
|
||||||
|
|
||||||
|
assert analytics.preferences[ATTR_DIAGNOSTICS]
|
||||||
|
|
||||||
|
with patch(
|
||||||
|
"homeassistant.components.hassio.get_supervisor_info",
|
||||||
|
side_effect=Mock(return_value={"diagnostics": False}),
|
||||||
|
), patch(
|
||||||
|
"homeassistant.components.hassio.is_hassio",
|
||||||
|
side_effect=Mock(return_value=True),
|
||||||
|
):
|
||||||
|
await analytics.load()
|
||||||
|
|
||||||
|
assert not analytics.preferences[ATTR_DIAGNOSTICS]
|
||||||
|
|
||||||
|
|
||||||
|
async def test_failed_to_send(hass, caplog, aioclient_mock):
|
||||||
|
"""Test failed to send payload."""
|
||||||
|
aioclient_mock.post(ANALYTICS_ENDPOINT_URL, status=400)
|
||||||
|
analytics = Analytics(hass)
|
||||||
|
await analytics.save_preferences({ATTR_BASE: True})
|
||||||
|
assert analytics.preferences[ATTR_BASE]
|
||||||
|
|
||||||
|
with patch("homeassistant.helpers.instance_id.async_get", return_value=MOCK_HUUID):
|
||||||
|
await analytics.send_analytics()
|
||||||
|
assert "Sending analytics failed with statuscode 400" in caplog.text
|
||||||
|
|
||||||
|
|
||||||
|
async def test_failed_to_send_raises(hass, caplog, aioclient_mock):
|
||||||
|
"""Test raises when failed to send payload."""
|
||||||
|
aioclient_mock.post(ANALYTICS_ENDPOINT_URL, exc=aiohttp.ClientError())
|
||||||
|
analytics = Analytics(hass)
|
||||||
|
await analytics.save_preferences({ATTR_BASE: True})
|
||||||
|
assert analytics.preferences[ATTR_BASE]
|
||||||
|
|
||||||
|
with patch("homeassistant.helpers.instance_id.async_get", return_value=MOCK_HUUID):
|
||||||
|
await analytics.send_analytics()
|
||||||
|
assert "Error sending analytics" in caplog.text
|
||||||
|
|
||||||
|
|
||||||
|
async def test_send_base(hass, caplog, aioclient_mock):
|
||||||
|
"""Test send base prefrences are defined."""
|
||||||
|
aioclient_mock.post(ANALYTICS_ENDPOINT_URL, status=200)
|
||||||
|
analytics = Analytics(hass)
|
||||||
|
await analytics.save_preferences({ATTR_BASE: True})
|
||||||
|
assert analytics.preferences[ATTR_BASE]
|
||||||
|
|
||||||
|
with patch("homeassistant.helpers.instance_id.async_get", return_value=MOCK_HUUID):
|
||||||
|
await analytics.send_analytics()
|
||||||
|
assert f"'huuid': '{MOCK_HUUID}'" in caplog.text
|
||||||
|
assert f"'version': '{HA_VERSION}'" in caplog.text
|
||||||
|
assert "'installation_type':" in caplog.text
|
||||||
|
assert "'integration_count':" not in caplog.text
|
||||||
|
assert "'integrations':" not in caplog.text
|
||||||
|
|
||||||
|
|
||||||
|
async def test_send_base_with_supervisor(hass, caplog, aioclient_mock):
|
||||||
|
"""Test send base prefrences are defined."""
|
||||||
|
aioclient_mock.post(ANALYTICS_ENDPOINT_URL, status=200)
|
||||||
|
|
||||||
|
analytics = Analytics(hass)
|
||||||
|
await analytics.save_preferences({ATTR_BASE: True})
|
||||||
|
assert analytics.preferences[ATTR_BASE]
|
||||||
|
|
||||||
|
with patch(
|
||||||
|
"homeassistant.components.hassio.get_supervisor_info",
|
||||||
|
side_effect=Mock(return_value={"supported": True, "healthy": True}),
|
||||||
|
), patch(
|
||||||
|
"homeassistant.components.hassio.get_info",
|
||||||
|
side_effect=Mock(return_value={}),
|
||||||
|
), patch(
|
||||||
|
"homeassistant.components.hassio.get_host_info",
|
||||||
|
side_effect=Mock(return_value={}),
|
||||||
|
), patch(
|
||||||
|
"homeassistant.components.hassio.is_hassio",
|
||||||
|
side_effect=Mock(return_value=True),
|
||||||
|
), patch(
|
||||||
|
"homeassistant.helpers.instance_id.async_get", return_value=MOCK_HUUID
|
||||||
|
):
|
||||||
|
await analytics.send_analytics()
|
||||||
|
assert f"'huuid': '{MOCK_HUUID}'" in caplog.text
|
||||||
|
assert f"'version': '{HA_VERSION}'" in caplog.text
|
||||||
|
assert "'supervisor': {'healthy': True, 'supported': True}}" in caplog.text
|
||||||
|
assert "'installation_type':" in caplog.text
|
||||||
|
assert "'integration_count':" not in caplog.text
|
||||||
|
assert "'integrations':" not in caplog.text
|
||||||
|
|
||||||
|
|
||||||
|
async def test_send_usage(hass, caplog, aioclient_mock):
|
||||||
|
"""Test send usage prefrences are defined."""
|
||||||
|
aioclient_mock.post(ANALYTICS_ENDPOINT_URL, status=200)
|
||||||
|
analytics = Analytics(hass)
|
||||||
|
await analytics.save_preferences({ATTR_BASE: True, ATTR_USAGE: True})
|
||||||
|
assert analytics.preferences[ATTR_BASE]
|
||||||
|
assert analytics.preferences[ATTR_USAGE]
|
||||||
|
hass.config.components = ["default_config"]
|
||||||
|
|
||||||
|
with patch("homeassistant.helpers.instance_id.async_get", return_value=MOCK_HUUID):
|
||||||
|
await analytics.send_analytics()
|
||||||
|
assert "'integrations': ['default_config']" in caplog.text
|
||||||
|
assert "'integration_count':" not in caplog.text
|
||||||
|
|
||||||
|
|
||||||
|
async def test_send_usage_with_supervisor(hass, caplog, aioclient_mock):
|
||||||
|
"""Test send usage with supervisor prefrences are defined."""
|
||||||
|
aioclient_mock.post(ANALYTICS_ENDPOINT_URL, status=200)
|
||||||
|
|
||||||
|
analytics = Analytics(hass)
|
||||||
|
await analytics.save_preferences({ATTR_BASE: True, ATTR_USAGE: True})
|
||||||
|
assert analytics.preferences[ATTR_BASE]
|
||||||
|
assert analytics.preferences[ATTR_USAGE]
|
||||||
|
hass.config.components = ["default_config"]
|
||||||
|
|
||||||
|
with patch(
|
||||||
|
"homeassistant.components.hassio.get_supervisor_info",
|
||||||
|
side_effect=Mock(
|
||||||
|
return_value={
|
||||||
|
"healthy": True,
|
||||||
|
"supported": True,
|
||||||
|
"addons": [{"slug": "test_addon"}],
|
||||||
|
}
|
||||||
|
),
|
||||||
|
), patch(
|
||||||
|
"homeassistant.components.hassio.get_info",
|
||||||
|
side_effect=Mock(return_value={}),
|
||||||
|
), patch(
|
||||||
|
"homeassistant.components.hassio.get_host_info",
|
||||||
|
side_effect=Mock(return_value={}),
|
||||||
|
), patch(
|
||||||
|
"homeassistant.components.hassio.async_get_addon_info",
|
||||||
|
side_effect=AsyncMock(
|
||||||
|
return_value={
|
||||||
|
"slug": "test_addon",
|
||||||
|
"protected": True,
|
||||||
|
"version": "1",
|
||||||
|
"auto_update": False,
|
||||||
|
}
|
||||||
|
),
|
||||||
|
), patch(
|
||||||
|
"homeassistant.components.hassio.is_hassio",
|
||||||
|
side_effect=Mock(return_value=True),
|
||||||
|
), patch(
|
||||||
|
"homeassistant.helpers.instance_id.async_get", return_value=MOCK_HUUID
|
||||||
|
):
|
||||||
|
await analytics.send_analytics()
|
||||||
|
assert (
|
||||||
|
"'addons': [{'slug': 'test_addon', 'protected': True, 'version': '1', 'auto_update': False}]"
|
||||||
|
in caplog.text
|
||||||
|
)
|
||||||
|
assert "'addon_count':" not in caplog.text
|
||||||
|
|
||||||
|
|
||||||
|
async def test_send_statistics(hass, caplog, aioclient_mock):
|
||||||
|
"""Test send statistics prefrences are defined."""
|
||||||
|
aioclient_mock.post(ANALYTICS_ENDPOINT_URL, status=200)
|
||||||
|
analytics = Analytics(hass)
|
||||||
|
await analytics.save_preferences({ATTR_BASE: True, ATTR_STATISTICS: True})
|
||||||
|
assert analytics.preferences[ATTR_BASE]
|
||||||
|
assert analytics.preferences[ATTR_STATISTICS]
|
||||||
|
hass.config.components = ["default_config"]
|
||||||
|
|
||||||
|
with patch("homeassistant.helpers.instance_id.async_get", return_value=MOCK_HUUID):
|
||||||
|
await analytics.send_analytics()
|
||||||
|
assert (
|
||||||
|
"'state_count': 0, 'automation_count': 0, 'integration_count': 1, 'user_count': 0"
|
||||||
|
in caplog.text
|
||||||
|
)
|
||||||
|
assert "'integrations':" not in caplog.text
|
||||||
|
|
||||||
|
|
||||||
|
async def test_send_statistics_with_supervisor(hass, caplog, aioclient_mock):
|
||||||
|
"""Test send statistics prefrences are defined."""
|
||||||
|
aioclient_mock.post(ANALYTICS_ENDPOINT_URL, status=200)
|
||||||
|
analytics = Analytics(hass)
|
||||||
|
await analytics.save_preferences({ATTR_BASE: True, ATTR_STATISTICS: True})
|
||||||
|
assert analytics.preferences[ATTR_BASE]
|
||||||
|
assert analytics.preferences[ATTR_STATISTICS]
|
||||||
|
|
||||||
|
with patch(
|
||||||
|
"homeassistant.components.hassio.get_supervisor_info",
|
||||||
|
side_effect=Mock(
|
||||||
|
return_value={
|
||||||
|
"healthy": True,
|
||||||
|
"supported": True,
|
||||||
|
"addons": [{"slug": "test_addon"}],
|
||||||
|
}
|
||||||
|
),
|
||||||
|
), patch(
|
||||||
|
"homeassistant.components.hassio.get_info",
|
||||||
|
side_effect=Mock(return_value={}),
|
||||||
|
), patch(
|
||||||
|
"homeassistant.components.hassio.get_host_info",
|
||||||
|
side_effect=Mock(return_value={}),
|
||||||
|
), patch(
|
||||||
|
"homeassistant.components.hassio.async_get_addon_info",
|
||||||
|
side_effect=AsyncMock(
|
||||||
|
return_value={
|
||||||
|
"slug": "test_addon",
|
||||||
|
"protected": True,
|
||||||
|
"version": "1",
|
||||||
|
"auto_update": False,
|
||||||
|
}
|
||||||
|
),
|
||||||
|
), patch(
|
||||||
|
"homeassistant.components.hassio.is_hassio",
|
||||||
|
side_effect=Mock(return_value=True),
|
||||||
|
), patch(
|
||||||
|
"homeassistant.helpers.instance_id.async_get", return_value=MOCK_HUUID
|
||||||
|
):
|
||||||
|
await analytics.send_analytics()
|
||||||
|
assert "'addon_count': 1" in caplog.text
|
||||||
|
assert "'integrations':" not in caplog.text
|
42
tests/components/analytics/test_init.py
Normal file
42
tests/components/analytics/test_init.py
Normal file
|
@ -0,0 +1,42 @@
|
||||||
|
"""The tests for the analytics ."""
|
||||||
|
from unittest.mock import patch
|
||||||
|
|
||||||
|
from homeassistant.components.analytics.const import ANALYTICS_ENDPOINT_URL, DOMAIN
|
||||||
|
from homeassistant.setup import async_setup_component
|
||||||
|
|
||||||
|
|
||||||
|
async def test_setup(hass):
|
||||||
|
"""Test setup of the integration."""
|
||||||
|
assert await async_setup_component(hass, DOMAIN, {DOMAIN: {}})
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
assert DOMAIN in hass.data
|
||||||
|
|
||||||
|
|
||||||
|
async def test_websocket(hass, hass_ws_client, aioclient_mock):
|
||||||
|
"""Test websocekt commands."""
|
||||||
|
aioclient_mock.post(ANALYTICS_ENDPOINT_URL, status=200)
|
||||||
|
assert await async_setup_component(hass, DOMAIN, {DOMAIN: {}})
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
ws_client = await hass_ws_client(hass)
|
||||||
|
await ws_client.send_json({"id": 1, "type": "analytics"})
|
||||||
|
|
||||||
|
with patch("homeassistant.helpers.instance_id.async_get", return_value="abcdef"):
|
||||||
|
response = await ws_client.receive_json()
|
||||||
|
|
||||||
|
assert response["success"]
|
||||||
|
assert response["result"]["huuid"] == "abcdef"
|
||||||
|
|
||||||
|
await ws_client.send_json(
|
||||||
|
{"id": 2, "type": "analytics/preferences", "preferences": {"base": True}}
|
||||||
|
)
|
||||||
|
response = await ws_client.receive_json()
|
||||||
|
assert len(aioclient_mock.mock_calls) == 1
|
||||||
|
assert response["result"]["preferences"]["base"]
|
||||||
|
|
||||||
|
await ws_client.send_json({"id": 3, "type": "analytics"})
|
||||||
|
with patch("homeassistant.helpers.instance_id.async_get", return_value="abcdef"):
|
||||||
|
response = await ws_client.receive_json()
|
||||||
|
assert response["result"]["preferences"]["base"]
|
||||||
|
assert response["result"]["huuid"] == "abcdef"
|
|
@ -17,7 +17,7 @@ def hassio_env():
|
||||||
with patch.dict(os.environ, {"HASSIO": "127.0.0.1"}), patch(
|
with patch.dict(os.environ, {"HASSIO": "127.0.0.1"}), patch(
|
||||||
"homeassistant.components.hassio.HassIO.is_connected",
|
"homeassistant.components.hassio.HassIO.is_connected",
|
||||||
return_value={"result": "ok", "data": {}},
|
return_value={"result": "ok", "data": {}},
|
||||||
), patch.dict(os.environ, {"HASSIO_TOKEN": "123456"}), patch(
|
), patch.dict(os.environ, {"HASSIO_TOKEN": HASSIO_TOKEN}), patch(
|
||||||
"homeassistant.components.hassio.HassIO.get_info",
|
"homeassistant.components.hassio.HassIO.get_info",
|
||||||
Mock(side_effect=HassioAPIError()),
|
Mock(side_effect=HassioAPIError()),
|
||||||
):
|
):
|
||||||
|
|
|
@ -75,7 +75,7 @@ async def mock_supervisor_fixture(hass, aioclient_mock):
|
||||||
return_value={},
|
return_value={},
|
||||||
), patch(
|
), patch(
|
||||||
"homeassistant.components.hassio.HassIO.get_supervisor_info",
|
"homeassistant.components.hassio.HassIO.get_supervisor_info",
|
||||||
return_value={},
|
return_value={"diagnostics": True},
|
||||||
), patch(
|
), patch(
|
||||||
"homeassistant.components.hassio.HassIO.get_os_info",
|
"homeassistant.components.hassio.HassIO.get_os_info",
|
||||||
return_value={},
|
return_value={},
|
||||||
|
@ -418,3 +418,22 @@ async def test_onboarding_core_no_rpi_power(
|
||||||
|
|
||||||
rpi_power_state = hass.states.get("binary_sensor.rpi_power_status")
|
rpi_power_state = hass.states.get("binary_sensor.rpi_power_status")
|
||||||
assert not rpi_power_state
|
assert not rpi_power_state
|
||||||
|
|
||||||
|
|
||||||
|
async def test_onboarding_analytics(hass, hass_storage, hass_client, hass_admin_user):
|
||||||
|
"""Test finishing analytics step."""
|
||||||
|
mock_storage(hass_storage, {"done": [const.STEP_USER]})
|
||||||
|
|
||||||
|
assert await async_setup_component(hass, "onboarding", {})
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
client = await hass_client()
|
||||||
|
|
||||||
|
resp = await client.post("/api/onboarding/analytics")
|
||||||
|
|
||||||
|
assert resp.status == 200
|
||||||
|
|
||||||
|
assert const.STEP_ANALYTICS in hass_storage[const.DOMAIN]["data"]["done"]
|
||||||
|
|
||||||
|
resp = await client.post("/api/onboarding/analytics")
|
||||||
|
assert resp.status == 403
|
||||||
|
|
Loading…
Add table
Reference in a new issue