From 284c4f0f6f0fcf9e05da605dcf4bf79a6e8571ff Mon Sep 17 00:00:00 2001 From: Ludeeus Date: Tue, 23 Mar 2021 14:06:41 +0000 Subject: [PATCH 01/16] Add analytics integration --- CODEOWNERS | 1 + .../components/analytics/__init__.py | 73 ++++++ .../components/analytics/analytics.py | 207 ++++++++++++++++++ homeassistant/components/analytics/const.py | 86 ++++++++ .../components/analytics/manifest.json | 8 + .../components/default_config/manifest.json | 1 + homeassistant/components/hassio/handler.py | 10 + .../components/onboarding/__init__.py | 13 +- homeassistant/components/onboarding/const.py | 3 +- .../components/onboarding/manifest.json | 1 + homeassistant/components/onboarding/views.py | 24 ++ tests/components/analytics/__init__.py | 1 + tests/components/analytics/test_analytics.py | 190 ++++++++++++++++ tests/components/analytics/test_init.py | 51 +++++ tests/components/hassio/conftest.py | 17 +- tests/components/onboarding/test_views.py | 18 +- tests/conftest.py | 16 ++ 17 files changed, 701 insertions(+), 19 deletions(-) create mode 100644 homeassistant/components/analytics/__init__.py create mode 100644 homeassistant/components/analytics/analytics.py create mode 100644 homeassistant/components/analytics/const.py create mode 100644 homeassistant/components/analytics/manifest.json create mode 100644 tests/components/analytics/__init__.py create mode 100644 tests/components/analytics/test_analytics.py create mode 100644 tests/components/analytics/test_init.py diff --git a/CODEOWNERS b/CODEOWNERS index bece933d9c8f8..59769a7953c44 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -36,6 +36,7 @@ homeassistant/components/alpha_vantage/* @fabaff homeassistant/components/ambiclimate/* @danielhiversen homeassistant/components/ambient_station/* @bachya homeassistant/components/amcrest/* @pnbruckner +homeassistant/components/analytics/* @home-assistant/core homeassistant/components/androidtv/* @JeffLIrion homeassistant/components/apache_kafka/* @bachya homeassistant/components/api/* @home-assistant/core diff --git a/homeassistant/components/analytics/__init__.py b/homeassistant/components/analytics/__init__.py new file mode 100644 index 0000000000000..2daca380c2e2c --- /dev/null +++ b/homeassistant/components/analytics/__init__.py @@ -0,0 +1,73 @@ +"""Send instance and usage analytics.""" +import voluptuous as vol + +from homeassistant.components import websocket_api +from homeassistant.core import HomeAssistant +from homeassistant.helpers import config_validation as cv +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 + + +async def async_setup(hass: HomeAssistant, _): + """Set up the analytics integration.""" + huuid = await hass.helpers.instance_id.async_get() + analytics = Analytics(hass, huuid) + + # Load stored data + await analytics.load() + + # Wait 15 min with first send + async_call_later(hass, 900, analytics.send_analytics) + + # Send every day + async_track_time_interval(hass, analytics.send_analytics, INTERVAL) + + websocket_api.async_register_command(hass, websocket_analytics_preferences) + websocket_api.async_register_command(hass, websocket_analytics_preferences_update) + + 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_preferences( + hass: HomeAssistant, + connection: websocket_api.connection.ActiveConnection, + msg: dict, +) -> None: + """Return analytics preferences.""" + analytics: Analytics = hass.data[DOMAIN] + connection.send_result( + msg["id"], + {ATTR_PREFERENCES: analytics.preferences, ATTR_HUUID: analytics.huuid}, + ) + + +@websocket_api.require_admin +@websocket_api.async_response +@websocket_api.websocket_command( + { + vol.Required("type"): "analytics/preferences", + vol.Required("preferences"): cv.ensure_list, + } +) +async def websocket_analytics_preferences_update( + 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}, + ) diff --git a/homeassistant/components/analytics/analytics.py b/homeassistant/components/analytics/analytics.py new file mode 100644 index 0000000000000..f916c2d4e71c0 --- /dev/null +++ b/homeassistant/components/analytics/analytics.py @@ -0,0 +1,207 @@ +"""Analytics helper class for the analytics integration.""" +import asyncio +from typing import List, Optional + +import aiohttp +import async_timeout + +from homeassistant.components.api import ATTR_INSTALLATION_TYPE +from homeassistant.components.automation.const import DOMAIN as AUTOMATION_DOMAIN +from homeassistant.components.hassio.const import DOMAIN as HASSIO_DOMAIN +from homeassistant.components.hassio.handler import HassIO +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_ENPOINT_URL, + ATTR_ADDON_COUNT, + ATTR_ADDONS, + ATTR_AUTO_UPDATE, + ATTR_AUTOMATION_COUNT, + ATTR_DIAGNOSTICS, + ATTR_HEALTHY, + ATTR_HUUID, + ATTR_INTEGRATION_COUNT, + ATTR_INTEGRATIONS, + ATTR_ONBOARDED, + ATTR_PREFERENCES, + ATTR_PROTECTED, + ATTR_SLUG, + ATTR_STATE_COUNT, + ATTR_SUPERVISOR, + ATTR_SUPPORTED, + ATTR_USER_COUNT, + ATTR_VERSION, + INGORED_DOMAINS, + LOGGER, + STORAGE_KEY, + STORAGE_VERSION, + AnalyticsPreference, +) + +DEFAULT_DATA = {ATTR_ONBOARDED: False, ATTR_PREFERENCES: []} + + +class Analytics: + """Analytics helper class for the analytics integration.""" + + def __init__(self, hass: HomeAssistant, huuid: str) -> None: + """Initialize the Analytics class.""" + self.hass: HomeAssistant = hass + self.huuid = huuid + self.session = async_get_clientsession(hass) + self._data = DEFAULT_DATA + self._store: Store = hass.helpers.storage.Store(STORAGE_VERSION, STORAGE_KEY) + self._supervisor: Optional[HassIO] = self.hass.data.get(HASSIO_DOMAIN) + + @property + def preferences(self) -> List[AnalyticsPreference]: + """Return the current active preferences.""" + return self._data[ATTR_PREFERENCES] + + @property + def onboarded(self) -> List[AnalyticsPreference]: + """Return bool if the user has made a choice.""" + return self._data[ATTR_ONBOARDED] + + async def load(self) -> None: + """Load preferences.""" + self._data = await self._store.async_load() or DEFAULT_DATA + if self._supervisor: + supervisor_info = await self._supervisor.get_supervisor_info() + if not self.onboarded: + # User have not configured analytics, get this setting from the supervisor + if ( + supervisor_info[ATTR_DIAGNOSTICS] + and AnalyticsPreference.DIAGNOSTICS + not in self._data[ATTR_PREFERENCES] + ): + self._data[ATTR_PREFERENCES].append(ATTR_DIAGNOSTICS) + elif ( + not supervisor_info[ATTR_DIAGNOSTICS] + and AnalyticsPreference.DIAGNOSTICS in self._data[ATTR_PREFERENCES] + ): + self._data[ATTR_PREFERENCES].remove(ATTR_DIAGNOSTICS) + + async def save_preferences(self, preferences: List[AnalyticsPreference]) -> None: + """Save preferences.""" + self._data[ATTR_PREFERENCES] = preferences + self._data[ATTR_ONBOARDED] = True + await self._store.async_save(self._data) + + if self._supervisor: + await self._supervisor.update_diagnostics( + AnalyticsPreference.DIAGNOSTICS in self._data[ATTR_PREFERENCES] + ) + + async def send_analytics(self, _=None) -> None: + """Send analytics.""" + supervisor_info = None + if self._supervisor: + supervisor_info = await self._supervisor.get_supervisor_info() + + if not self.onboarded or AnalyticsPreference.BASE not in self.preferences: + LOGGER.debug("Nothing to submit") + return + + system_info = await async_get_system_info(self.hass) + integrations = [] + addons = [] + payload: dict = { + ATTR_HUUID: self.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 ( + AnalyticsPreference.USAGE in self.preferences + or AnalyticsPreference.STATISTICS in self.preferences + ): + 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 integration.domain in INGORED_DOMAINS + or not integration.is_built_in + ): + continue + + integrations.append(integration.domain) + + if supervisor_info is not None: + installed_addons = await asyncio.gather( + *[ + self._supervisor.get_addon_info(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 AnalyticsPreference.USAGE in self.preferences: + payload[ATTR_INTEGRATIONS] = integrations + if supervisor_info is not None: + payload[ATTR_ADDONS] = addons + + if AnalyticsPreference.STATISTICS in self.preferences: + 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_ENPOINT_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_ENPOINT_URL) + except aiohttp.ClientError as err: + LOGGER.error( + "Error sending analytics to %s: %r", ANALYTICS_ENPOINT_URL, err + ) diff --git a/homeassistant/components/analytics/const.py b/homeassistant/components/analytics/const.py new file mode 100644 index 0000000000000..eb490dc850833 --- /dev/null +++ b/homeassistant/components/analytics/const.py @@ -0,0 +1,86 @@ +"""Constants for the analytics integration.""" +from datetime import timedelta +from enum import Enum +import logging + +ANALYTICS_ENPOINT_URL = "https://floral-resonance-f63b.ludeeus.workers.dev" +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_AUTOMATION_COUNT = "automation_count" +ATTR_AUTO_UPDATE = "auto_update" +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_SUPERVISOR = "supervisor" +ATTR_SUPPORTED = "supported" +ATTR_USER_COUNT = "user_count" +ATTR_VERSION = "version" + + +class AnalyticsPreference(str, Enum): + """Analytics prefrences.""" + + BASE = "base" + DIAGNOSTICS = "diagnostics" + STATISTICS = "statistics" + USAGE = "usage" + + +INGORED_DOMAINS = [ + "air_quality", + "alarm_control_panel", + "analytics", + "api", + "auth", + "binary_sensor", + "calendar", + "camera", + "climate", + "config", + "cover", + "demo", + "device_automation", + "device_tracker", + "fan", + "hassio", + "homeassistant", + "http", + "humidifier", + "image_processing", + "image", + "light", + "lock", + "logger", + "lovelace", + "media_player", + "notify", + "number", + "onboarding", + "persistent_notification", + "recorder", + "search", + "sensor", + "switch", + "system_log", + "tts", + "vacuum", + "water_heater", + "weather", + "websocket_api", +] diff --git a/homeassistant/components/analytics/manifest.json b/homeassistant/components/analytics/manifest.json new file mode 100644 index 0000000000000..de3fdfbdccdae --- /dev/null +++ b/homeassistant/components/analytics/manifest.json @@ -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" +} diff --git a/homeassistant/components/default_config/manifest.json b/homeassistant/components/default_config/manifest.json index 0f4b940cc3681..fa7f547869df1 100644 --- a/homeassistant/components/default_config/manifest.json +++ b/homeassistant/components/default_config/manifest.json @@ -3,6 +3,7 @@ "name": "Default Config", "documentation": "https://www.home-assistant.io/integrations/default_config", "dependencies": [ + "analytics", "automation", "cloud", "counter", diff --git a/homeassistant/components/hassio/handler.py b/homeassistant/components/hassio/handler.py index 6bc3cb345a516..484853ff77d80 100644 --- a/homeassistant/components/hassio/handler.py +++ b/homeassistant/components/hassio/handler.py @@ -181,6 +181,16 @@ def update_hass_timezone(self, 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): """Send API command to Hass.io. diff --git a/homeassistant/components/onboarding/__init__.py b/homeassistant/components/onboarding/__init__.py index bedfa703a9b0d..e383e4e32c459 100644 --- a/homeassistant/components/onboarding/__init__.py +++ b/homeassistant/components/onboarding/__init__.py @@ -4,10 +4,17 @@ from homeassistant.loader import bind_hass 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_VERSION = 3 +STORAGE_VERSION = 4 class OnboadingStorage(Store): @@ -20,6 +27,8 @@ async def _async_migrate_func(self, old_version, old_data): old_data["done"].append(STEP_INTEGRATION) if old_version < 3: old_data["done"].append(STEP_CORE_CONFIG) + if old_version < 4: + old_data["done"].append(STEP_ANALYTICS) return old_data diff --git a/homeassistant/components/onboarding/const.py b/homeassistant/components/onboarding/const.py index bf350a200dee6..5a771f524ac4a 100644 --- a/homeassistant/components/onboarding/const.py +++ b/homeassistant/components/onboarding/const.py @@ -3,7 +3,8 @@ STEP_USER = "user" STEP_CORE_CONFIG = "core_config" 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") diff --git a/homeassistant/components/onboarding/manifest.json b/homeassistant/components/onboarding/manifest.json index e2fb8e084b83b..06c9946b5c9b6 100644 --- a/homeassistant/components/onboarding/manifest.json +++ b/homeassistant/components/onboarding/manifest.json @@ -6,6 +6,7 @@ "hassio" ], "dependencies": [ + "analytics", "auth", "http", "person" diff --git a/homeassistant/components/onboarding/views.py b/homeassistant/components/onboarding/views.py index 1d5528688dd63..dec806428455d 100644 --- a/homeassistant/components/onboarding/views.py +++ b/homeassistant/components/onboarding/views.py @@ -14,6 +14,7 @@ from .const import ( DEFAULT_AREAS, DOMAIN, + STEP_ANALYTICS, STEP_CORE_CONFIG, STEP_INTEGRATION, STEP_USER, @@ -27,6 +28,7 @@ async def async_setup(hass, data, store): hass.http.register_view(UserOnboardingView(data, store)) hass.http.register_view(CoreConfigOnboardingView(data, store)) hass.http.register_view(IntegrationOnboardingView(data, store)) + hass.http.register_view(AnalyticsOnboardingView(data, store)) class OnboardingView(HomeAssistantView): @@ -217,6 +219,28 @@ async def post(self, request, data): 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 def _async_get_hass_provider(hass): """Get the Home Assistant auth provider.""" diff --git a/tests/components/analytics/__init__.py b/tests/components/analytics/__init__.py new file mode 100644 index 0000000000000..7cf0ac9f7baf2 --- /dev/null +++ b/tests/components/analytics/__init__.py @@ -0,0 +1 @@ +"""Tests for the analytics integration.""" diff --git a/tests/components/analytics/test_analytics.py b/tests/components/analytics/test_analytics.py new file mode 100644 index 0000000000000..ff2ed3ccdae5f --- /dev/null +++ b/tests/components/analytics/test_analytics.py @@ -0,0 +1,190 @@ +"""The tests for the analytics .""" +from homeassistant.components.analytics.analytics import Analytics +from homeassistant.components.analytics.const import ( + ANALYTICS_ENPOINT_URL, + AnalyticsPreference, +) +from homeassistant.components.hassio.const import DOMAIN as HASSIO_DOMAIN +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_ENPOINT_URL, status=200) + analytics = Analytics(hass, MOCK_HUUID) + await analytics.load() + assert analytics.preferences == [] + + await analytics.send_analytics() + assert "Nothing to submit" in caplog.text + assert len(aioclient_mock.mock_calls) == 0 + + +async def test_failed_to_send(hass, caplog, aioclient_mock): + """Test send base prefrences are defined.""" + aioclient_mock.post(ANALYTICS_ENPOINT_URL, status=400) + analytics = Analytics(hass, MOCK_HUUID) + await analytics.save_preferences([AnalyticsPreference.BASE]) + assert analytics.preferences == ["base"] + + await analytics.send_analytics() + assert "Sending analytics failed with statuscode 400" in caplog.text + + +async def test_send_base(hass, caplog, aioclient_mock): + """Test send base prefrences are defined.""" + aioclient_mock.post(ANALYTICS_ENPOINT_URL, status=200) + analytics = Analytics(hass, MOCK_HUUID) + await analytics.save_preferences([AnalyticsPreference.BASE]) + assert analytics.preferences == ["base"] + + 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, hassio_handler): + """Test send base prefrences are defined.""" + aioclient_mock.post(ANALYTICS_ENPOINT_URL, status=200) + aioclient_mock.post("http://127.0.0.1/supervisor/options", json={"result": "ok"}) + aioclient_mock.get( + "http://127.0.0.1/supervisor/info", + json={"result": "ok", "data": {"supported": True, "healthy": True}}, + ) + hass.data[HASSIO_DOMAIN] = hassio_handler + + analytics = Analytics(hass, MOCK_HUUID) + await analytics.save_preferences([AnalyticsPreference.BASE]) + assert analytics.preferences == ["base"] + + 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_ENPOINT_URL, status=200) + analytics = Analytics(hass, MOCK_HUUID) + await analytics.save_preferences( + [AnalyticsPreference.BASE, AnalyticsPreference.USAGE] + ) + assert analytics.preferences == ["base", "usage"] + hass.config.components = ["default_config"] + + 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, hassio_handler): + """Test send usage with supervisor prefrences are defined.""" + aioclient_mock.post(ANALYTICS_ENPOINT_URL, status=200) + aioclient_mock.post("http://127.0.0.1/supervisor/options", json={"result": "ok"}) + aioclient_mock.get( + "http://127.0.0.1/supervisor/info", + json={ + "result": "ok", + "data": { + "healthy": True, + "supported": True, + "addons": [{"slug": "test_addon"}], + }, + }, + ) + aioclient_mock.get( + "http://127.0.0.1/addons/test_addon/info", + json={ + "result": "ok", + "data": { + "slug": "test_addon", + "protected": True, + "version": "1", + "auto_update": False, + }, + }, + ) + hass.data[HASSIO_DOMAIN] = hassio_handler + + analytics = Analytics(hass, MOCK_HUUID) + await analytics.save_preferences( + [AnalyticsPreference.BASE, AnalyticsPreference.USAGE] + ) + assert analytics.preferences == ["base", "usage"] + hass.config.components = ["default_config"] + + 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_ENPOINT_URL, status=200) + analytics = Analytics(hass, MOCK_HUUID) + await analytics.save_preferences( + [AnalyticsPreference.BASE, AnalyticsPreference.STATISTICS] + ) + assert analytics.preferences == ["base", "statistics"] + hass.config.components = ["default_config"] + + 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, hassio_handler +): + """Test send statistics prefrences are defined.""" + aioclient_mock.post(ANALYTICS_ENPOINT_URL, status=200) + aioclient_mock.post("http://127.0.0.1/supervisor/options", json={"result": "ok"}) + aioclient_mock.get( + "http://127.0.0.1/supervisor/info", + json={ + "result": "ok", + "data": { + "healthy": True, + "supported": True, + "addons": [{"slug": "test_addon"}], + }, + }, + ) + aioclient_mock.get( + "http://127.0.0.1/addons/test_addon/info", + json={ + "result": "ok", + "data": { + "slug": "test_addon", + "protected": True, + "version": "1", + "auto_update": False, + }, + }, + ) + hass.data[HASSIO_DOMAIN] = hassio_handler + + analytics = Analytics(hass, MOCK_HUUID) + await analytics.save_preferences( + [AnalyticsPreference.BASE, AnalyticsPreference.STATISTICS] + ) + assert analytics.preferences == ["base", "statistics"] + + await analytics.send_analytics() + assert "'addon_count': 1" in caplog.text + assert "'integrations':" not in caplog.text diff --git a/tests/components/analytics/test_init.py b/tests/components/analytics/test_init.py new file mode 100644 index 0000000000000..3a117ecd0819f --- /dev/null +++ b/tests/components/analytics/test_init.py @@ -0,0 +1,51 @@ +"""The tests for the analytics .""" +from unittest.mock import patch + +import pytest + +from homeassistant.components.analytics.const import ANALYTICS_ENPOINT_URL, DOMAIN +from homeassistant.setup import async_setup_component + +MOCK_HUUID = "abcdefg" + + +@pytest.fixture(name="mock_get_huuid", autouse=True) +def mock_get_huuid_fixture(): + """Fixture to mock get huuid.""" + with patch("homeassistant.helpers.instance_id.async_get") as mock: + yield mock + + +async def test_setup(hass, mock_get_huuid): + """Test setup of the integration.""" + mock_get_huuid.return_value = MOCK_HUUID + assert await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) + await hass.async_block_till_done() + + assert hass.data[DOMAIN].huuid == MOCK_HUUID + + +async def test_websocket(hass, mock_get_huuid, hass_ws_client, aioclient_mock): + """Test websocekt commands.""" + aioclient_mock.post(ANALYTICS_ENPOINT_URL, status=200) + mock_get_huuid.return_value = MOCK_HUUID + 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"}) + response = await ws_client.receive_json() + + assert response["success"] + assert response["result"]["huuid"] == MOCK_HUUID + + await ws_client.send_json( + {"id": 2, "type": "analytics/preferences", "preferences": ["base"]} + ) + 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"}) + response = await ws_client.receive_json() + assert response["result"]["preferences"] == ["base"] diff --git a/tests/components/hassio/conftest.py b/tests/components/hassio/conftest.py index 1442d133f1e0e..ed4022a4b55cc 100644 --- a/tests/components/hassio/conftest.py +++ b/tests/components/hassio/conftest.py @@ -4,7 +4,7 @@ import pytest -from homeassistant.components.hassio.handler import HassIO, HassioAPIError +from homeassistant.components.hassio.handler import HassioAPIError from homeassistant.core import CoreState from homeassistant.setup import async_setup_component @@ -17,7 +17,7 @@ def hassio_env(): with patch.dict(os.environ, {"HASSIO": "127.0.0.1"}), patch( "homeassistant.components.hassio.HassIO.is_connected", 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", Mock(side_effect=HassioAPIError()), ): @@ -63,16 +63,3 @@ async def hassio_client_supervisor(hass, aiohttp_client, hassio_stubs): hass.http.app, headers={"Authorization": f"Bearer {access_token}"}, ) - - -@pytest.fixture -def hassio_handler(hass, aioclient_mock): - """Create mock hassio handler.""" - - async def get_client_session(): - return hass.helpers.aiohttp_client.async_get_clientsession() - - websession = hass.loop.run_until_complete(get_client_session()) - - with patch.dict(os.environ, {"HASSIO_TOKEN": HASSIO_TOKEN}): - yield HassIO(hass.loop, websession, "127.0.0.1") diff --git a/tests/components/onboarding/test_views.py b/tests/components/onboarding/test_views.py index 558cab7ee99e7..345285243c942 100644 --- a/tests/components/onboarding/test_views.py +++ b/tests/components/onboarding/test_views.py @@ -75,7 +75,7 @@ async def mock_supervisor_fixture(hass, aioclient_mock): return_value={}, ), patch( "homeassistant.components.hassio.HassIO.get_supervisor_info", - return_value={}, + return_value={"diagnostics": True}, ), patch( "homeassistant.components.hassio.HassIO.get_os_info", return_value={}, @@ -418,3 +418,19 @@ async def test_onboarding_core_no_rpi_power( rpi_power_state = hass.states.get("binary_sensor.rpi_power_status") 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"] diff --git a/tests/conftest.py b/tests/conftest.py index 3fc2dc748cbd1..dba56b130976a 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -3,6 +3,7 @@ import datetime import functools import logging +import os import ssl import threading from unittest.mock import MagicMock, patch @@ -17,6 +18,7 @@ from homeassistant.auth.models import Credentials from homeassistant.auth.providers import homeassistant, legacy_api_password from homeassistant.components import mqtt +from homeassistant.components.hassio.handler import HassIO from homeassistant.components.websocket_api.auth import ( TYPE_AUTH, TYPE_AUTH_OK, @@ -29,6 +31,7 @@ from homeassistant.setup import async_setup_component from homeassistant.util import location +from tests.components.hassio import HASSIO_TOKEN from tests.ignore_uncaught_exceptions import IGNORE_UNCAUGHT_EXCEPTIONS pytest.register_assert_rewrite("tests.common") @@ -595,3 +598,16 @@ def pattern_time_change_listener(ev) -> None: def enable_custom_integrations(hass): """Enable custom integrations defined in the test dir.""" hass.data.pop(loader.DATA_CUSTOM_COMPONENTS) + + +@pytest.fixture +def hassio_handler(hass, aioclient_mock): + """Create mock hassio handler.""" + + async def get_client_session(): + return hass.helpers.aiohttp_client.async_get_clientsession() + + websession = hass.loop.run_until_complete(get_client_session()) + + with patch.dict(os.environ, {"HASSIO_TOKEN": HASSIO_TOKEN}): + yield HassIO(hass.loop, websession, "127.0.0.1") From afc025ecdd949ce0b93fce8d89512613d40f0e9f Mon Sep 17 00:00:00 2001 From: Ludeeus Date: Tue, 23 Mar 2021 14:52:46 +0000 Subject: [PATCH 02/16] Add hass_storage --- tests/components/onboarding/test_init.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/components/onboarding/test_init.py b/tests/components/onboarding/test_init.py index e347a07e73f04..44f3920dd2828 100644 --- a/tests/components/onboarding/test_init.py +++ b/tests/components/onboarding/test_init.py @@ -23,7 +23,7 @@ async def test_not_setup_views_if_onboarded(hass, hass_storage): assert onboarding.async_is_onboarded(hass) -async def test_setup_views_if_not_onboarded(hass): +async def test_setup_views_if_not_onboarded(hass, hass_storage): """Test if onboarding is not done, we setup views.""" with patch( "homeassistant.components.onboarding.views.async_setup", From fe63a9091e16a4aba41996133c0e8adb53b5494d Mon Sep 17 00:00:00 2001 From: Ludeeus Date: Tue, 23 Mar 2021 15:04:38 +0000 Subject: [PATCH 03/16] Review comments --- homeassistant/components/analytics/__init__.py | 13 +++++++++---- .../components/analytics/analytics.py | 10 +++++----- homeassistant/components/analytics/const.py | 4 +++- tests/components/analytics/test_analytics.py | 18 +++++++++--------- tests/components/analytics/test_init.py | 4 ++-- 5 files changed, 28 insertions(+), 21 deletions(-) diff --git a/homeassistant/components/analytics/__init__.py b/homeassistant/components/analytics/__init__.py index 2daca380c2e2c..ec77062fc2eec 100644 --- a/homeassistant/components/analytics/__init__.py +++ b/homeassistant/components/analytics/__init__.py @@ -2,6 +2,7 @@ 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 import config_validation as cv from homeassistant.helpers.event import async_call_later, async_track_time_interval @@ -18,11 +19,15 @@ async def async_setup(hass: HomeAssistant, _): # Load stored data await analytics.load() - # Wait 15 min with first send - async_call_later(hass, 900, analytics.send_analytics) + 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) + # 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_preferences) websocket_api.async_register_command(hass, websocket_analytics_preferences_update) diff --git a/homeassistant/components/analytics/analytics.py b/homeassistant/components/analytics/analytics.py index f916c2d4e71c0..7a05fb542cbd3 100644 --- a/homeassistant/components/analytics/analytics.py +++ b/homeassistant/components/analytics/analytics.py @@ -17,7 +17,7 @@ from homeassistant.loader import async_get_integration from .const import ( - ANALYTICS_ENPOINT_URL, + ANALYTICS_ENDPOINT_URL, ATTR_ADDON_COUNT, ATTR_ADDONS, ATTR_AUTO_UPDATE, @@ -64,7 +64,7 @@ def preferences(self) -> List[AnalyticsPreference]: return self._data[ATTR_PREFERENCES] @property - def onboarded(self) -> List[AnalyticsPreference]: + def onboarded(self) -> bool: """Return bool if the user has made a choice.""" return self._data[ATTR_ONBOARDED] @@ -186,7 +186,7 @@ async def send_analytics(self, _=None) -> None: try: with async_timeout.timeout(30): - response = await self.session.post(ANALYTICS_ENPOINT_URL, json=payload) + response = await self.session.post(ANALYTICS_ENDPOINT_URL, json=payload) if response.status == 200: LOGGER.info( ( @@ -200,8 +200,8 @@ async def send_analytics(self, _=None) -> None: "Sending analytics failed with statuscode %s", response.status ) except asyncio.TimeoutError: - LOGGER.error("Timeout sending analytics to %s", ANALYTICS_ENPOINT_URL) + LOGGER.error("Timeout sending analytics to %s", ANALYTICS_ENDPOINT_URL) except aiohttp.ClientError as err: LOGGER.error( - "Error sending analytics to %s: %r", ANALYTICS_ENPOINT_URL, err + "Error sending analytics to %s: %r", ANALYTICS_ENDPOINT_URL, err ) diff --git a/homeassistant/components/analytics/const.py b/homeassistant/components/analytics/const.py index eb490dc850833..9eae2833963f0 100644 --- a/homeassistant/components/analytics/const.py +++ b/homeassistant/components/analytics/const.py @@ -3,7 +3,7 @@ from enum import Enum import logging -ANALYTICS_ENPOINT_URL = "https://floral-resonance-f63b.ludeeus.workers.dev" +ANALYTICS_ENDPOINT_URL = "https://floral-resonance-f63b.ludeeus.workers.dev" DOMAIN = "analytics" INTERVAL = timedelta(days=1) STORAGE_KEY = "core.analytics" @@ -53,6 +53,7 @@ class AnalyticsPreference(str, Enum): "camera", "climate", "config", + "conversation", "cover", "demo", "device_automation", @@ -76,6 +77,7 @@ class AnalyticsPreference(str, Enum): "recorder", "search", "sensor", + "stt", "switch", "system_log", "tts", diff --git a/tests/components/analytics/test_analytics.py b/tests/components/analytics/test_analytics.py index ff2ed3ccdae5f..7f92433c0ad09 100644 --- a/tests/components/analytics/test_analytics.py +++ b/tests/components/analytics/test_analytics.py @@ -1,7 +1,7 @@ """The tests for the analytics .""" from homeassistant.components.analytics.analytics import Analytics from homeassistant.components.analytics.const import ( - ANALYTICS_ENPOINT_URL, + ANALYTICS_ENDPOINT_URL, AnalyticsPreference, ) from homeassistant.components.hassio.const import DOMAIN as HASSIO_DOMAIN @@ -12,7 +12,7 @@ async def test_no_send(hass, caplog, aioclient_mock): """Test send when no prefrences are defined.""" - aioclient_mock.post(ANALYTICS_ENPOINT_URL, status=200) + aioclient_mock.post(ANALYTICS_ENDPOINT_URL, status=200) analytics = Analytics(hass, MOCK_HUUID) await analytics.load() assert analytics.preferences == [] @@ -24,7 +24,7 @@ async def test_no_send(hass, caplog, aioclient_mock): async def test_failed_to_send(hass, caplog, aioclient_mock): """Test send base prefrences are defined.""" - aioclient_mock.post(ANALYTICS_ENPOINT_URL, status=400) + aioclient_mock.post(ANALYTICS_ENDPOINT_URL, status=400) analytics = Analytics(hass, MOCK_HUUID) await analytics.save_preferences([AnalyticsPreference.BASE]) assert analytics.preferences == ["base"] @@ -35,7 +35,7 @@ async def test_failed_to_send(hass, caplog, aioclient_mock): async def test_send_base(hass, caplog, aioclient_mock): """Test send base prefrences are defined.""" - aioclient_mock.post(ANALYTICS_ENPOINT_URL, status=200) + aioclient_mock.post(ANALYTICS_ENDPOINT_URL, status=200) analytics = Analytics(hass, MOCK_HUUID) await analytics.save_preferences([AnalyticsPreference.BASE]) assert analytics.preferences == ["base"] @@ -50,7 +50,7 @@ async def test_send_base(hass, caplog, aioclient_mock): async def test_send_base_with_supervisor(hass, caplog, aioclient_mock, hassio_handler): """Test send base prefrences are defined.""" - aioclient_mock.post(ANALYTICS_ENPOINT_URL, status=200) + aioclient_mock.post(ANALYTICS_ENDPOINT_URL, status=200) aioclient_mock.post("http://127.0.0.1/supervisor/options", json={"result": "ok"}) aioclient_mock.get( "http://127.0.0.1/supervisor/info", @@ -73,7 +73,7 @@ async def test_send_base_with_supervisor(hass, caplog, aioclient_mock, hassio_ha async def test_send_usage(hass, caplog, aioclient_mock): """Test send usage prefrences are defined.""" - aioclient_mock.post(ANALYTICS_ENPOINT_URL, status=200) + aioclient_mock.post(ANALYTICS_ENDPOINT_URL, status=200) analytics = Analytics(hass, MOCK_HUUID) await analytics.save_preferences( [AnalyticsPreference.BASE, AnalyticsPreference.USAGE] @@ -88,7 +88,7 @@ async def test_send_usage(hass, caplog, aioclient_mock): async def test_send_usage_with_supervisor(hass, caplog, aioclient_mock, hassio_handler): """Test send usage with supervisor prefrences are defined.""" - aioclient_mock.post(ANALYTICS_ENPOINT_URL, status=200) + aioclient_mock.post(ANALYTICS_ENDPOINT_URL, status=200) aioclient_mock.post("http://127.0.0.1/supervisor/options", json={"result": "ok"}) aioclient_mock.get( "http://127.0.0.1/supervisor/info", @@ -132,7 +132,7 @@ async def test_send_usage_with_supervisor(hass, caplog, aioclient_mock, hassio_h async def test_send_statistics(hass, caplog, aioclient_mock): """Test send statistics prefrences are defined.""" - aioclient_mock.post(ANALYTICS_ENPOINT_URL, status=200) + aioclient_mock.post(ANALYTICS_ENDPOINT_URL, status=200) analytics = Analytics(hass, MOCK_HUUID) await analytics.save_preferences( [AnalyticsPreference.BASE, AnalyticsPreference.STATISTICS] @@ -152,7 +152,7 @@ async def test_send_statistics_with_supervisor( hass, caplog, aioclient_mock, hassio_handler ): """Test send statistics prefrences are defined.""" - aioclient_mock.post(ANALYTICS_ENPOINT_URL, status=200) + aioclient_mock.post(ANALYTICS_ENDPOINT_URL, status=200) aioclient_mock.post("http://127.0.0.1/supervisor/options", json={"result": "ok"}) aioclient_mock.get( "http://127.0.0.1/supervisor/info", diff --git a/tests/components/analytics/test_init.py b/tests/components/analytics/test_init.py index 3a117ecd0819f..15ed1ae9f5a32 100644 --- a/tests/components/analytics/test_init.py +++ b/tests/components/analytics/test_init.py @@ -3,7 +3,7 @@ import pytest -from homeassistant.components.analytics.const import ANALYTICS_ENPOINT_URL, DOMAIN +from homeassistant.components.analytics.const import ANALYTICS_ENDPOINT_URL, DOMAIN from homeassistant.setup import async_setup_component MOCK_HUUID = "abcdefg" @@ -27,7 +27,7 @@ async def test_setup(hass, mock_get_huuid): async def test_websocket(hass, mock_get_huuid, hass_ws_client, aioclient_mock): """Test websocekt commands.""" - aioclient_mock.post(ANALYTICS_ENPOINT_URL, status=200) + aioclient_mock.post(ANALYTICS_ENDPOINT_URL, status=200) mock_get_huuid.return_value = MOCK_HUUID assert await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) await hass.async_block_till_done() From 1291d55676dc17ae00bc8fd0efeb6bd704d66ad6 Mon Sep 17 00:00:00 2001 From: Ludeeus Date: Tue, 23 Mar 2021 17:27:26 +0000 Subject: [PATCH 04/16] adjust --- tests/components/analytics/test_init.py | 11 ------- tests/components/onboarding/test_init.py | 8 ++--- tests/components/onboarding/test_views.py | 38 +++++++++++++++-------- tests/conftest.py | 7 +++++ 4 files changed, 36 insertions(+), 28 deletions(-) diff --git a/tests/components/analytics/test_init.py b/tests/components/analytics/test_init.py index 15ed1ae9f5a32..8a85079c536e2 100644 --- a/tests/components/analytics/test_init.py +++ b/tests/components/analytics/test_init.py @@ -1,21 +1,10 @@ """The tests for the analytics .""" -from unittest.mock import patch - -import pytest - from homeassistant.components.analytics.const import ANALYTICS_ENDPOINT_URL, DOMAIN from homeassistant.setup import async_setup_component MOCK_HUUID = "abcdefg" -@pytest.fixture(name="mock_get_huuid", autouse=True) -def mock_get_huuid_fixture(): - """Fixture to mock get huuid.""" - with patch("homeassistant.helpers.instance_id.async_get") as mock: - yield mock - - async def test_setup(hass, mock_get_huuid): """Test setup of the integration.""" mock_get_huuid.return_value = MOCK_HUUID diff --git a/tests/components/onboarding/test_init.py b/tests/components/onboarding/test_init.py index 44f3920dd2828..fe184d46ce776 100644 --- a/tests/components/onboarding/test_init.py +++ b/tests/components/onboarding/test_init.py @@ -11,7 +11,7 @@ # Temporarily: if auth not active, always set onboarded=True -async def test_not_setup_views_if_onboarded(hass, hass_storage): +async def test_not_setup_views_if_onboarded(hass, hass_storage, mock_get_huuid): """Test if onboarding is done, we don't setup views.""" mock_storage(hass_storage, {"done": onboarding.STEPS}) @@ -23,7 +23,7 @@ async def test_not_setup_views_if_onboarded(hass, hass_storage): assert onboarding.async_is_onboarded(hass) -async def test_setup_views_if_not_onboarded(hass, hass_storage): +async def test_setup_views_if_not_onboarded(hass, mock_get_huuid): """Test if onboarding is not done, we setup views.""" with patch( "homeassistant.components.onboarding.views.async_setup", @@ -65,7 +65,7 @@ async def test_is_user_onboarded(): assert not onboarding.async_is_user_onboarded(hass) -async def test_having_owner_finishes_user_step(hass, hass_storage): +async def test_having_owner_finishes_user_step(hass, hass_storage, mock_get_huuid): """If owner user already exists, mark user step as complete.""" MockUser(is_owner=True).add_to_hass(hass) @@ -82,7 +82,7 @@ async def test_having_owner_finishes_user_step(hass, hass_storage): assert onboarding.STEP_USER in done -async def test_migration(hass, hass_storage): +async def test_migration(hass, hass_storage, mock_get_huuid): """Test migrating onboarding to new version.""" hass_storage[onboarding.STORAGE_KEY] = {"version": 1, "data": {"done": ["user"]}} assert await async_setup_component(hass, "onboarding", {}) diff --git a/tests/components/onboarding/test_views.py b/tests/components/onboarding/test_views.py index 345285243c942..8f4203f9b78a6 100644 --- a/tests/components/onboarding/test_views.py +++ b/tests/components/onboarding/test_views.py @@ -88,7 +88,7 @@ async def mock_supervisor_fixture(hass, aioclient_mock): yield -async def test_onboarding_progress(hass, hass_storage, aiohttp_client): +async def test_onboarding_progress(hass, hass_storage, aiohttp_client, mock_get_huuid): """Test fetching progress.""" mock_storage(hass_storage, {"done": ["hello"]}) @@ -107,7 +107,9 @@ async def test_onboarding_progress(hass, hass_storage, aiohttp_client): assert data[1] == {"step": "world", "done": False} -async def test_onboarding_user_already_done(hass, hass_storage, aiohttp_client): +async def test_onboarding_user_already_done( + hass, hass_storage, aiohttp_client, mock_get_huuid +): """Test creating a new user when user step already done.""" mock_storage(hass_storage, {"done": [views.STEP_USER]}) @@ -131,7 +133,7 @@ async def test_onboarding_user_already_done(hass, hass_storage, aiohttp_client): assert resp.status == HTTP_FORBIDDEN -async def test_onboarding_user(hass, hass_storage, aiohttp_client): +async def test_onboarding_user(hass, hass_storage, aiohttp_client, mock_get_huuid): """Test creating a new user.""" assert await async_setup_component(hass, "person", {}) assert await async_setup_component(hass, "onboarding", {}) @@ -191,7 +193,9 @@ async def test_onboarding_user(hass, hass_storage, aiohttp_client): ] -async def test_onboarding_user_invalid_name(hass, hass_storage, aiohttp_client): +async def test_onboarding_user_invalid_name( + hass, hass_storage, aiohttp_client, mock_get_huuid +): """Test not providing name.""" mock_storage(hass_storage, {"done": []}) @@ -213,7 +217,7 @@ async def test_onboarding_user_invalid_name(hass, hass_storage, aiohttp_client): assert resp.status == 400 -async def test_onboarding_user_race(hass, hass_storage, aiohttp_client): +async def test_onboarding_user_race(hass, hass_storage, aiohttp_client, mock_get_huuid): """Test race condition on creating new user.""" mock_storage(hass_storage, {"done": ["hello"]}) @@ -248,7 +252,9 @@ async def test_onboarding_user_race(hass, hass_storage, aiohttp_client): assert sorted([res1.status, res2.status]) == [200, HTTP_FORBIDDEN] -async def test_onboarding_integration(hass, hass_storage, hass_client, hass_admin_user): +async def test_onboarding_integration( + hass, hass_storage, hass_client, hass_admin_user, mock_get_huuid +): """Test finishing integration step.""" mock_storage(hass_storage, {"done": [const.STEP_USER]}) @@ -290,7 +296,7 @@ async def test_onboarding_integration(hass, hass_storage, hass_client, hass_admi async def test_onboarding_integration_missing_credential( - hass, hass_storage, hass_client, hass_access_token + hass, hass_storage, hass_client, hass_access_token, mock_get_huuid ): """Test that we fail integration step if user is missing credentials.""" mock_storage(hass_storage, {"done": [const.STEP_USER]}) @@ -312,7 +318,7 @@ async def test_onboarding_integration_missing_credential( async def test_onboarding_integration_invalid_redirect_uri( - hass, hass_storage, hass_client + hass, hass_storage, hass_client, mock_get_huuid ): """Test finishing integration step.""" mock_storage(hass_storage, {"done": [const.STEP_USER]}) @@ -337,7 +343,9 @@ async def test_onboarding_integration_invalid_redirect_uri( assert len(user.refresh_tokens) == 1, user -async def test_onboarding_integration_requires_auth(hass, hass_storage, aiohttp_client): +async def test_onboarding_integration_requires_auth( + hass, hass_storage, aiohttp_client, mock_get_huuid +): """Test finishing integration step.""" mock_storage(hass_storage, {"done": [const.STEP_USER]}) @@ -353,7 +361,9 @@ async def test_onboarding_integration_requires_auth(hass, hass_storage, aiohttp_ assert resp.status == 401 -async def test_onboarding_core_sets_up_met(hass, hass_storage, hass_client): +async def test_onboarding_core_sets_up_met( + hass, hass_storage, hass_client, mock_get_huuid +): """Test finishing the core step.""" mock_storage(hass_storage, {"done": [const.STEP_USER]}) @@ -371,7 +381,7 @@ async def test_onboarding_core_sets_up_met(hass, hass_storage, hass_client): async def test_onboarding_core_sets_up_rpi_power( - hass, hass_storage, hass_client, aioclient_mock, rpi + hass, hass_storage, hass_client, aioclient_mock, rpi, mock_get_huuid ): """Test that the core step sets up rpi_power on RPi.""" mock_storage(hass_storage, {"done": [const.STEP_USER]}) @@ -396,7 +406,7 @@ async def test_onboarding_core_sets_up_rpi_power( async def test_onboarding_core_no_rpi_power( - hass, hass_storage, hass_client, aioclient_mock, no_rpi + hass, hass_storage, hass_client, aioclient_mock, no_rpi, mock_get_huuid ): """Test that the core step do not set up rpi_power on non RPi.""" mock_storage(hass_storage, {"done": [const.STEP_USER]}) @@ -420,7 +430,9 @@ async def test_onboarding_core_no_rpi_power( assert not rpi_power_state -async def test_onboarding_analytics(hass, hass_storage, hass_client, hass_admin_user): +async def test_onboarding_analytics( + hass, hass_storage, hass_client, hass_admin_user, mock_get_huuid +): """Test finishing analytics step.""" mock_storage(hass_storage, {"done": [const.STEP_USER]}) diff --git a/tests/conftest.py b/tests/conftest.py index dba56b130976a..783dfb0d96326 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -600,6 +600,13 @@ def enable_custom_integrations(hass): hass.data.pop(loader.DATA_CUSTOM_COMPONENTS) +@pytest.fixture(name="mock_get_huuid") +def mock_get_huuid_fixture(): + """Fixture to mock get huuid.""" + with patch("homeassistant.helpers.instance_id.async_get") as mock: + yield mock + + @pytest.fixture def hassio_handler(hass, aioclient_mock): """Create mock hassio handler.""" From b516394069f43acfce51484bffe678bf182eb567 Mon Sep 17 00:00:00 2001 From: Ludeeus Date: Tue, 23 Mar 2021 18:31:49 +0000 Subject: [PATCH 05/16] Revert "adjust" This reverts commit 1291d55676dc17ae00bc8fd0efeb6bd704d66ad6. --- tests/components/analytics/test_init.py | 11 +++++++ tests/components/onboarding/test_init.py | 8 ++--- tests/components/onboarding/test_views.py | 38 ++++++++--------------- tests/conftest.py | 7 ----- 4 files changed, 28 insertions(+), 36 deletions(-) diff --git a/tests/components/analytics/test_init.py b/tests/components/analytics/test_init.py index 8a85079c536e2..15ed1ae9f5a32 100644 --- a/tests/components/analytics/test_init.py +++ b/tests/components/analytics/test_init.py @@ -1,10 +1,21 @@ """The tests for the analytics .""" +from unittest.mock import patch + +import pytest + from homeassistant.components.analytics.const import ANALYTICS_ENDPOINT_URL, DOMAIN from homeassistant.setup import async_setup_component MOCK_HUUID = "abcdefg" +@pytest.fixture(name="mock_get_huuid", autouse=True) +def mock_get_huuid_fixture(): + """Fixture to mock get huuid.""" + with patch("homeassistant.helpers.instance_id.async_get") as mock: + yield mock + + async def test_setup(hass, mock_get_huuid): """Test setup of the integration.""" mock_get_huuid.return_value = MOCK_HUUID diff --git a/tests/components/onboarding/test_init.py b/tests/components/onboarding/test_init.py index fe184d46ce776..44f3920dd2828 100644 --- a/tests/components/onboarding/test_init.py +++ b/tests/components/onboarding/test_init.py @@ -11,7 +11,7 @@ # Temporarily: if auth not active, always set onboarded=True -async def test_not_setup_views_if_onboarded(hass, hass_storage, mock_get_huuid): +async def test_not_setup_views_if_onboarded(hass, hass_storage): """Test if onboarding is done, we don't setup views.""" mock_storage(hass_storage, {"done": onboarding.STEPS}) @@ -23,7 +23,7 @@ async def test_not_setup_views_if_onboarded(hass, hass_storage, mock_get_huuid): assert onboarding.async_is_onboarded(hass) -async def test_setup_views_if_not_onboarded(hass, mock_get_huuid): +async def test_setup_views_if_not_onboarded(hass, hass_storage): """Test if onboarding is not done, we setup views.""" with patch( "homeassistant.components.onboarding.views.async_setup", @@ -65,7 +65,7 @@ async def test_is_user_onboarded(): assert not onboarding.async_is_user_onboarded(hass) -async def test_having_owner_finishes_user_step(hass, hass_storage, mock_get_huuid): +async def test_having_owner_finishes_user_step(hass, hass_storage): """If owner user already exists, mark user step as complete.""" MockUser(is_owner=True).add_to_hass(hass) @@ -82,7 +82,7 @@ async def test_having_owner_finishes_user_step(hass, hass_storage, mock_get_huui assert onboarding.STEP_USER in done -async def test_migration(hass, hass_storage, mock_get_huuid): +async def test_migration(hass, hass_storage): """Test migrating onboarding to new version.""" hass_storage[onboarding.STORAGE_KEY] = {"version": 1, "data": {"done": ["user"]}} assert await async_setup_component(hass, "onboarding", {}) diff --git a/tests/components/onboarding/test_views.py b/tests/components/onboarding/test_views.py index 8f4203f9b78a6..345285243c942 100644 --- a/tests/components/onboarding/test_views.py +++ b/tests/components/onboarding/test_views.py @@ -88,7 +88,7 @@ async def mock_supervisor_fixture(hass, aioclient_mock): yield -async def test_onboarding_progress(hass, hass_storage, aiohttp_client, mock_get_huuid): +async def test_onboarding_progress(hass, hass_storage, aiohttp_client): """Test fetching progress.""" mock_storage(hass_storage, {"done": ["hello"]}) @@ -107,9 +107,7 @@ async def test_onboarding_progress(hass, hass_storage, aiohttp_client, mock_get_ assert data[1] == {"step": "world", "done": False} -async def test_onboarding_user_already_done( - hass, hass_storage, aiohttp_client, mock_get_huuid -): +async def test_onboarding_user_already_done(hass, hass_storage, aiohttp_client): """Test creating a new user when user step already done.""" mock_storage(hass_storage, {"done": [views.STEP_USER]}) @@ -133,7 +131,7 @@ async def test_onboarding_user_already_done( assert resp.status == HTTP_FORBIDDEN -async def test_onboarding_user(hass, hass_storage, aiohttp_client, mock_get_huuid): +async def test_onboarding_user(hass, hass_storage, aiohttp_client): """Test creating a new user.""" assert await async_setup_component(hass, "person", {}) assert await async_setup_component(hass, "onboarding", {}) @@ -193,9 +191,7 @@ async def test_onboarding_user(hass, hass_storage, aiohttp_client, mock_get_huui ] -async def test_onboarding_user_invalid_name( - hass, hass_storage, aiohttp_client, mock_get_huuid -): +async def test_onboarding_user_invalid_name(hass, hass_storage, aiohttp_client): """Test not providing name.""" mock_storage(hass_storage, {"done": []}) @@ -217,7 +213,7 @@ async def test_onboarding_user_invalid_name( assert resp.status == 400 -async def test_onboarding_user_race(hass, hass_storage, aiohttp_client, mock_get_huuid): +async def test_onboarding_user_race(hass, hass_storage, aiohttp_client): """Test race condition on creating new user.""" mock_storage(hass_storage, {"done": ["hello"]}) @@ -252,9 +248,7 @@ async def test_onboarding_user_race(hass, hass_storage, aiohttp_client, mock_get assert sorted([res1.status, res2.status]) == [200, HTTP_FORBIDDEN] -async def test_onboarding_integration( - hass, hass_storage, hass_client, hass_admin_user, mock_get_huuid -): +async def test_onboarding_integration(hass, hass_storage, hass_client, hass_admin_user): """Test finishing integration step.""" mock_storage(hass_storage, {"done": [const.STEP_USER]}) @@ -296,7 +290,7 @@ async def test_onboarding_integration( async def test_onboarding_integration_missing_credential( - hass, hass_storage, hass_client, hass_access_token, mock_get_huuid + hass, hass_storage, hass_client, hass_access_token ): """Test that we fail integration step if user is missing credentials.""" mock_storage(hass_storage, {"done": [const.STEP_USER]}) @@ -318,7 +312,7 @@ async def test_onboarding_integration_missing_credential( async def test_onboarding_integration_invalid_redirect_uri( - hass, hass_storage, hass_client, mock_get_huuid + hass, hass_storage, hass_client ): """Test finishing integration step.""" mock_storage(hass_storage, {"done": [const.STEP_USER]}) @@ -343,9 +337,7 @@ async def test_onboarding_integration_invalid_redirect_uri( assert len(user.refresh_tokens) == 1, user -async def test_onboarding_integration_requires_auth( - hass, hass_storage, aiohttp_client, mock_get_huuid -): +async def test_onboarding_integration_requires_auth(hass, hass_storage, aiohttp_client): """Test finishing integration step.""" mock_storage(hass_storage, {"done": [const.STEP_USER]}) @@ -361,9 +353,7 @@ async def test_onboarding_integration_requires_auth( assert resp.status == 401 -async def test_onboarding_core_sets_up_met( - hass, hass_storage, hass_client, mock_get_huuid -): +async def test_onboarding_core_sets_up_met(hass, hass_storage, hass_client): """Test finishing the core step.""" mock_storage(hass_storage, {"done": [const.STEP_USER]}) @@ -381,7 +371,7 @@ async def test_onboarding_core_sets_up_met( async def test_onboarding_core_sets_up_rpi_power( - hass, hass_storage, hass_client, aioclient_mock, rpi, mock_get_huuid + hass, hass_storage, hass_client, aioclient_mock, rpi ): """Test that the core step sets up rpi_power on RPi.""" mock_storage(hass_storage, {"done": [const.STEP_USER]}) @@ -406,7 +396,7 @@ async def test_onboarding_core_sets_up_rpi_power( async def test_onboarding_core_no_rpi_power( - hass, hass_storage, hass_client, aioclient_mock, no_rpi, mock_get_huuid + hass, hass_storage, hass_client, aioclient_mock, no_rpi ): """Test that the core step do not set up rpi_power on non RPi.""" mock_storage(hass_storage, {"done": [const.STEP_USER]}) @@ -430,9 +420,7 @@ async def test_onboarding_core_no_rpi_power( assert not rpi_power_state -async def test_onboarding_analytics( - hass, hass_storage, hass_client, hass_admin_user, mock_get_huuid -): +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]}) diff --git a/tests/conftest.py b/tests/conftest.py index 783dfb0d96326..dba56b130976a 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -600,13 +600,6 @@ def enable_custom_integrations(hass): hass.data.pop(loader.DATA_CUSTOM_COMPONENTS) -@pytest.fixture(name="mock_get_huuid") -def mock_get_huuid_fixture(): - """Fixture to mock get huuid.""" - with patch("homeassistant.helpers.instance_id.async_get") as mock: - yield mock - - @pytest.fixture def hassio_handler(hass, aioclient_mock): """Create mock hassio handler.""" From f7454a8a8ce3bce1fe2310a25435546c94525c49 Mon Sep 17 00:00:00 2001 From: Ludeeus Date: Tue, 23 Mar 2021 18:32:03 +0000 Subject: [PATCH 06/16] Revert "Add hass_storage" This reverts commit afc025ecdd949ce0b93fce8d89512613d40f0e9f. --- tests/components/onboarding/test_init.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/components/onboarding/test_init.py b/tests/components/onboarding/test_init.py index 44f3920dd2828..e347a07e73f04 100644 --- a/tests/components/onboarding/test_init.py +++ b/tests/components/onboarding/test_init.py @@ -23,7 +23,7 @@ async def test_not_setup_views_if_onboarded(hass, hass_storage): assert onboarding.async_is_onboarded(hass) -async def test_setup_views_if_not_onboarded(hass, hass_storage): +async def test_setup_views_if_not_onboarded(hass): """Test if onboarding is not done, we setup views.""" with patch( "homeassistant.components.onboarding.views.async_setup", From 6fd4e7bdd185122199c1268c9cf8cbb324b8d487 Mon Sep 17 00:00:00 2001 From: Ludeeus Date: Tue, 23 Mar 2021 18:38:19 +0000 Subject: [PATCH 07/16] Adjust --- tests/components/analytics/test_init.py | 26 +++++++------------------ 1 file changed, 7 insertions(+), 19 deletions(-) diff --git a/tests/components/analytics/test_init.py b/tests/components/analytics/test_init.py index 15ed1ae9f5a32..aa9bd7061dbbe 100644 --- a/tests/components/analytics/test_init.py +++ b/tests/components/analytics/test_init.py @@ -1,34 +1,22 @@ """The tests for the analytics .""" -from unittest.mock import patch - -import pytest - from homeassistant.components.analytics.const import ANALYTICS_ENDPOINT_URL, DOMAIN from homeassistant.setup import async_setup_component -MOCK_HUUID = "abcdefg" - - -@pytest.fixture(name="mock_get_huuid", autouse=True) -def mock_get_huuid_fixture(): - """Fixture to mock get huuid.""" - with patch("homeassistant.helpers.instance_id.async_get") as mock: - yield mock - -async def test_setup(hass, mock_get_huuid): +async def test_setup(hass, hass_storage): """Test setup of the integration.""" - mock_get_huuid.return_value = MOCK_HUUID + uuid = await hass.helpers.instance_id.async_get() assert await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) await hass.async_block_till_done() - assert hass.data[DOMAIN].huuid == MOCK_HUUID + assert hass.data[DOMAIN].huuid == uuid + assert hass_storage["core.uuid"]["data"]["uuid"] == uuid -async def test_websocket(hass, mock_get_huuid, hass_ws_client, aioclient_mock): +async def test_websocket(hass, hass_storage, hass_ws_client, aioclient_mock): """Test websocekt commands.""" + uuid = await hass.helpers.instance_id.async_get() aioclient_mock.post(ANALYTICS_ENDPOINT_URL, status=200) - mock_get_huuid.return_value = MOCK_HUUID assert await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) await hass.async_block_till_done() @@ -37,7 +25,7 @@ async def test_websocket(hass, mock_get_huuid, hass_ws_client, aioclient_mock): response = await ws_client.receive_json() assert response["success"] - assert response["result"]["huuid"] == MOCK_HUUID + assert response["result"]["huuid"] == uuid await ws_client.send_json( {"id": 2, "type": "analytics/preferences", "preferences": ["base"]} From 9c671b619e69bf8c02f684e2a2f11b2a91a5ce4d Mon Sep 17 00:00:00 2001 From: Ludeeus Date: Tue, 23 Mar 2021 19:09:51 +0000 Subject: [PATCH 08/16] adjust --- .../components/analytics/__init__.py | 6 +-- .../components/analytics/analytics.py | 12 +++--- tests/components/analytics/test_analytics.py | 42 ++++++++++++------- tests/components/analytics/test_init.py | 21 ++++++---- 4 files changed, 48 insertions(+), 33 deletions(-) diff --git a/homeassistant/components/analytics/__init__.py b/homeassistant/components/analytics/__init__.py index ec77062fc2eec..7eec59c5e6893 100644 --- a/homeassistant/components/analytics/__init__.py +++ b/homeassistant/components/analytics/__init__.py @@ -13,8 +13,7 @@ async def async_setup(hass: HomeAssistant, _): """Set up the analytics integration.""" - huuid = await hass.helpers.instance_id.async_get() - analytics = Analytics(hass, huuid) + analytics = Analytics(hass) # Load stored data await analytics.load() @@ -46,9 +45,10 @@ async def websocket_analytics_preferences( ) -> 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: analytics.huuid}, + {ATTR_PREFERENCES: analytics.preferences, ATTR_HUUID: huuid}, ) diff --git a/homeassistant/components/analytics/analytics.py b/homeassistant/components/analytics/analytics.py index 7a05fb542cbd3..ba438b84fb523 100644 --- a/homeassistant/components/analytics/analytics.py +++ b/homeassistant/components/analytics/analytics.py @@ -49,10 +49,9 @@ class Analytics: """Analytics helper class for the analytics integration.""" - def __init__(self, hass: HomeAssistant, huuid: str) -> None: + def __init__(self, hass: HomeAssistant) -> None: """Initialize the Analytics class.""" self.hass: HomeAssistant = hass - self.huuid = huuid self.session = async_get_clientsession(hass) self._data = DEFAULT_DATA self._store: Store = hass.helpers.storage.Store(STORAGE_VERSION, STORAGE_KEY) @@ -101,18 +100,21 @@ async def save_preferences(self, preferences: List[AnalyticsPreference]) -> None async def send_analytics(self, _=None) -> None: """Send analytics.""" supervisor_info = None - if self._supervisor: - supervisor_info = await self._supervisor.get_supervisor_info() if not self.onboarded or AnalyticsPreference.BASE not in self.preferences: LOGGER.debug("Nothing to submit") return + huuid = await self.hass.helpers.instance_id.async_get() + + if self._supervisor: + supervisor_info = await self._supervisor.get_supervisor_info() + system_info = await async_get_system_info(self.hass) integrations = [] addons = [] payload: dict = { - ATTR_HUUID: self.huuid, + ATTR_HUUID: huuid, ATTR_VERSION: HA_VERSION, ATTR_INSTALLATION_TYPE: system_info[ATTR_INSTALLATION_TYPE], } diff --git a/tests/components/analytics/test_analytics.py b/tests/components/analytics/test_analytics.py index 7f92433c0ad09..5cb060695de2e 100644 --- a/tests/components/analytics/test_analytics.py +++ b/tests/components/analytics/test_analytics.py @@ -1,4 +1,6 @@ """The tests for the analytics .""" +from unittest.mock import patch + from homeassistant.components.analytics.analytics import Analytics from homeassistant.components.analytics.const import ( ANALYTICS_ENDPOINT_URL, @@ -13,11 +15,12 @@ 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, MOCK_HUUID) + analytics = Analytics(hass) await analytics.load() assert analytics.preferences == [] - await analytics.send_analytics() + with patch("homeassistant.helpers.instance_id.async_get", return_value=MOCK_HUUID): + await analytics.send_analytics() assert "Nothing to submit" in caplog.text assert len(aioclient_mock.mock_calls) == 0 @@ -25,22 +28,24 @@ async def test_no_send(hass, caplog, aioclient_mock): async def test_failed_to_send(hass, caplog, aioclient_mock): """Test send base prefrences are defined.""" aioclient_mock.post(ANALYTICS_ENDPOINT_URL, status=400) - analytics = Analytics(hass, MOCK_HUUID) + analytics = Analytics(hass) await analytics.save_preferences([AnalyticsPreference.BASE]) assert analytics.preferences == ["base"] - await analytics.send_analytics() + 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_send_base(hass, caplog, aioclient_mock): """Test send base prefrences are defined.""" aioclient_mock.post(ANALYTICS_ENDPOINT_URL, status=200) - analytics = Analytics(hass, MOCK_HUUID) + analytics = Analytics(hass) await analytics.save_preferences([AnalyticsPreference.BASE]) assert analytics.preferences == ["base"] - await analytics.send_analytics() + 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 @@ -58,11 +63,12 @@ async def test_send_base_with_supervisor(hass, caplog, aioclient_mock, hassio_ha ) hass.data[HASSIO_DOMAIN] = hassio_handler - analytics = Analytics(hass, MOCK_HUUID) + analytics = Analytics(hass) await analytics.save_preferences([AnalyticsPreference.BASE]) assert analytics.preferences == ["base"] - await analytics.send_analytics() + 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 "'supervisor': {'healthy': True, 'supported': True}}" in caplog.text @@ -74,14 +80,15 @@ async def test_send_base_with_supervisor(hass, caplog, aioclient_mock, hassio_ha 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, MOCK_HUUID) + analytics = Analytics(hass) await analytics.save_preferences( [AnalyticsPreference.BASE, AnalyticsPreference.USAGE] ) assert analytics.preferences == ["base", "usage"] hass.config.components = ["default_config"] - await analytics.send_analytics() + 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 @@ -115,14 +122,15 @@ async def test_send_usage_with_supervisor(hass, caplog, aioclient_mock, hassio_h ) hass.data[HASSIO_DOMAIN] = hassio_handler - analytics = Analytics(hass, MOCK_HUUID) + analytics = Analytics(hass) await analytics.save_preferences( [AnalyticsPreference.BASE, AnalyticsPreference.USAGE] ) assert analytics.preferences == ["base", "usage"] hass.config.components = ["default_config"] - await analytics.send_analytics() + with 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 @@ -133,14 +141,15 @@ async def test_send_usage_with_supervisor(hass, caplog, aioclient_mock, hassio_h 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, MOCK_HUUID) + analytics = Analytics(hass) await analytics.save_preferences( [AnalyticsPreference.BASE, AnalyticsPreference.STATISTICS] ) assert analytics.preferences == ["base", "statistics"] hass.config.components = ["default_config"] - await analytics.send_analytics() + 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 @@ -179,12 +188,13 @@ async def test_send_statistics_with_supervisor( ) hass.data[HASSIO_DOMAIN] = hassio_handler - analytics = Analytics(hass, MOCK_HUUID) + analytics = Analytics(hass) await analytics.save_preferences( [AnalyticsPreference.BASE, AnalyticsPreference.STATISTICS] ) assert analytics.preferences == ["base", "statistics"] - await analytics.send_analytics() + with 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 diff --git a/tests/components/analytics/test_init.py b/tests/components/analytics/test_init.py index aa9bd7061dbbe..564015950a7be 100644 --- a/tests/components/analytics/test_init.py +++ b/tests/components/analytics/test_init.py @@ -1,31 +1,32 @@ """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, hass_storage): +async def test_setup(hass): """Test setup of the integration.""" - uuid = await hass.helpers.instance_id.async_get() assert await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) await hass.async_block_till_done() - assert hass.data[DOMAIN].huuid == uuid - assert hass_storage["core.uuid"]["data"]["uuid"] == uuid + assert DOMAIN in hass.data -async def test_websocket(hass, hass_storage, hass_ws_client, aioclient_mock): +async def test_websocket(hass, hass_ws_client, aioclient_mock): """Test websocekt commands.""" - uuid = await hass.helpers.instance_id.async_get() 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"}) - response = await ws_client.receive_json() + + with patch("homeassistant.helpers.instance_id.async_get", return_value="abcdef"): + response = await ws_client.receive_json() assert response["success"] - assert response["result"]["huuid"] == uuid + assert response["result"]["huuid"] == "abcdef" await ws_client.send_json( {"id": 2, "type": "analytics/preferences", "preferences": ["base"]} @@ -35,5 +36,7 @@ async def test_websocket(hass, hass_storage, hass_ws_client, aioclient_mock): assert response["result"]["preferences"] == ["base"] await ws_client.send_json({"id": 3, "type": "analytics"}) - response = await ws_client.receive_json() + 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" From babd2fa4b080e0cdb55d39745423c14d5c539105 Mon Sep 17 00:00:00 2001 From: Ludeeus Date: Wed, 24 Mar 2021 12:29:16 +0000 Subject: [PATCH 09/16] Add more tests --- tests/components/analytics/test_analytics.py | 54 +++++++++++++++++++- tests/components/onboarding/test_views.py | 3 ++ 2 files changed, 55 insertions(+), 2 deletions(-) diff --git a/tests/components/analytics/test_analytics.py b/tests/components/analytics/test_analytics.py index 5cb060695de2e..e665df21171ba 100644 --- a/tests/components/analytics/test_analytics.py +++ b/tests/components/analytics/test_analytics.py @@ -1,9 +1,12 @@ """The tests for the analytics .""" from unittest.mock import patch +import aiohttp + from homeassistant.components.analytics.analytics import Analytics from homeassistant.components.analytics.const import ( ANALYTICS_ENDPOINT_URL, + ATTR_PREFERENCES, AnalyticsPreference, ) from homeassistant.components.hassio.const import DOMAIN as HASSIO_DOMAIN @@ -25,8 +28,43 @@ async def test_no_send(hass, caplog, aioclient_mock): assert len(aioclient_mock.mock_calls) == 0 +async def test_load_with_supervisor_diagnostics(hass, aioclient_mock, hassio_handler): + """Test loading with a supervisor that has diagnostics enabled.""" + aioclient_mock.get( + "http://127.0.0.1/supervisor/info", + json={"result": "ok", "data": {"diagnostics": True}}, + ) + hass.data[HASSIO_DOMAIN] = hassio_handler + + analytics = Analytics(hass) + assert AnalyticsPreference.DIAGNOSTICS not in analytics.preferences + await analytics.load() + assert AnalyticsPreference.DIAGNOSTICS in analytics.preferences + + +async def test_load_with_supervisor_without_diagnostics( + hass, aioclient_mock, hassio_handler +): + """Test loading with a supervisor that has not diagnostics enabled.""" + aioclient_mock.get( + "http://127.0.0.1/supervisor/info", + json={"result": "ok", "data": {"diagnostics": False}}, + ) + aioclient_mock.post("http://127.0.0.1/supervisor/options", json={"result": "ok"}) + hass.data[HASSIO_DOMAIN] = hassio_handler + + analytics = Analytics(hass) + analytics._data[ATTR_PREFERENCES] = [AnalyticsPreference.DIAGNOSTICS] + + assert AnalyticsPreference.DIAGNOSTICS in analytics.preferences + + await analytics.load() + + assert AnalyticsPreference.DIAGNOSTICS not in analytics.preferences + + async def test_failed_to_send(hass, caplog, aioclient_mock): - """Test send base prefrences are defined.""" + """Test failed to send payload.""" aioclient_mock.post(ANALYTICS_ENDPOINT_URL, status=400) analytics = Analytics(hass) await analytics.save_preferences([AnalyticsPreference.BASE]) @@ -37,6 +75,18 @@ async def test_failed_to_send(hass, caplog, aioclient_mock): 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([AnalyticsPreference.BASE]) + assert analytics.preferences == ["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) @@ -85,7 +135,7 @@ async def test_send_usage(hass, caplog, aioclient_mock): [AnalyticsPreference.BASE, AnalyticsPreference.USAGE] ) assert analytics.preferences == ["base", "usage"] - hass.config.components = ["default_config"] + hass.config.components = ["default_config", "api"] with patch("homeassistant.helpers.instance_id.async_get", return_value=MOCK_HUUID): await analytics.send_analytics() diff --git a/tests/components/onboarding/test_views.py b/tests/components/onboarding/test_views.py index 345285243c942..d8ae50b851fc8 100644 --- a/tests/components/onboarding/test_views.py +++ b/tests/components/onboarding/test_views.py @@ -434,3 +434,6 @@ async def test_onboarding_analytics(hass, hass_storage, hass_client, hass_admin_ 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 From f02716378dff5ab875c370ad3b8919739ac6870f Mon Sep 17 00:00:00 2001 From: Ludeeus Date: Wed, 24 Mar 2021 13:49:42 +0000 Subject: [PATCH 10/16] adjust part 1 --- .../components/analytics/__init__.py | 6 +++--- .../components/analytics/analytics.py | 21 ++++++++----------- 2 files changed, 12 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/analytics/__init__.py b/homeassistant/components/analytics/__init__.py index 7eec59c5e6893..6d35bdf5d5d79 100644 --- a/homeassistant/components/analytics/__init__.py +++ b/homeassistant/components/analytics/__init__.py @@ -28,8 +28,8 @@ async def start_schedule(_event): 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) - websocket_api.async_register_command(hass, websocket_analytics_preferences_update) hass.data[DOMAIN] = analytics return True @@ -38,7 +38,7 @@ async def start_schedule(_event): @websocket_api.require_admin @websocket_api.async_response @websocket_api.websocket_command({vol.Required("type"): "analytics"}) -async def websocket_analytics_preferences( +async def websocket_analytics( hass: HomeAssistant, connection: websocket_api.connection.ActiveConnection, msg: dict, @@ -60,7 +60,7 @@ async def websocket_analytics_preferences( vol.Required("preferences"): cv.ensure_list, } ) -async def websocket_analytics_preferences_update( +async def websocket_analytics_preferences( hass: HomeAssistant, connection: websocket_api.connection.ActiveConnection, msg: dict, diff --git a/homeassistant/components/analytics/analytics.py b/homeassistant/components/analytics/analytics.py index ba438b84fb523..995e36879048a 100644 --- a/homeassistant/components/analytics/analytics.py +++ b/homeassistant/components/analytics/analytics.py @@ -43,8 +43,6 @@ AnalyticsPreference, ) -DEFAULT_DATA = {ATTR_ONBOARDED: False, ATTR_PREFERENCES: []} - class Analytics: """Analytics helper class for the analytics integration.""" @@ -53,38 +51,37 @@ def __init__(self, hass: HomeAssistant) -> None: """Initialize the Analytics class.""" self.hass: HomeAssistant = hass self.session = async_get_clientsession(hass) - self._data = DEFAULT_DATA + self._data = {} self._store: Store = hass.helpers.storage.Store(STORAGE_VERSION, STORAGE_KEY) self._supervisor: Optional[HassIO] = self.hass.data.get(HASSIO_DOMAIN) @property def preferences(self) -> List[AnalyticsPreference]: """Return the current active preferences.""" - return self._data[ATTR_PREFERENCES] + return self._data.get(ATTR_PREFERENCES, []) @property def onboarded(self) -> bool: """Return bool if the user has made a choice.""" - return self._data[ATTR_ONBOARDED] + return self._data.get(ATTR_ONBOARDED, False) async def load(self) -> None: """Load preferences.""" - self._data = await self._store.async_load() or DEFAULT_DATA + self._data = await self._store.async_load() or {} if self._supervisor: supervisor_info = await self._supervisor.get_supervisor_info() if not self.onboarded: # User have not configured analytics, get this setting from the supervisor if ( supervisor_info[ATTR_DIAGNOSTICS] - and AnalyticsPreference.DIAGNOSTICS - not in self._data[ATTR_PREFERENCES] + and AnalyticsPreference.DIAGNOSTICS not in self.preferences ): - self._data[ATTR_PREFERENCES].append(ATTR_DIAGNOSTICS) + self._data.setdefault(ATTR_PREFERENCES, []).append(ATTR_DIAGNOSTICS) elif ( not supervisor_info[ATTR_DIAGNOSTICS] - and AnalyticsPreference.DIAGNOSTICS in self._data[ATTR_PREFERENCES] + and AnalyticsPreference.DIAGNOSTICS in self.preferences ): - self._data[ATTR_PREFERENCES].remove(ATTR_DIAGNOSTICS) + self._data.setdefault(ATTR_PREFERENCES, []).remove(ATTR_DIAGNOSTICS) async def save_preferences(self, preferences: List[AnalyticsPreference]) -> None: """Save preferences.""" @@ -94,7 +91,7 @@ async def save_preferences(self, preferences: List[AnalyticsPreference]) -> None if self._supervisor: await self._supervisor.update_diagnostics( - AnalyticsPreference.DIAGNOSTICS in self._data[ATTR_PREFERENCES] + AnalyticsPreference.DIAGNOSTICS in self.preferences ) async def send_analytics(self, _=None) -> None: From dcfdd900d438cd1070231ef5991c33fb2e52b020 Mon Sep 17 00:00:00 2001 From: Ludeeus Date: Wed, 24 Mar 2021 14:38:47 +0000 Subject: [PATCH 11/16] change tests --- .../components/analytics/analytics.py | 27 +-- homeassistant/components/hassio/__init__.py | 10 ++ tests/components/analytics/test_analytics.py | 166 +++++++++--------- tests/components/hassio/conftest.py | 15 +- tests/conftest.py | 16 -- 5 files changed, 119 insertions(+), 115 deletions(-) diff --git a/homeassistant/components/analytics/analytics.py b/homeassistant/components/analytics/analytics.py index 995e36879048a..fc7e262d70d20 100644 --- a/homeassistant/components/analytics/analytics.py +++ b/homeassistant/components/analytics/analytics.py @@ -1,14 +1,13 @@ """Analytics helper class for the analytics integration.""" import asyncio -from typing import List, Optional +from typing import List 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.components.hassio.const import DOMAIN as HASSIO_DOMAIN -from homeassistant.components.hassio.handler import HassIO from homeassistant.const import __version__ as HA_VERSION from homeassistant.core import HomeAssistant from homeassistant.helpers.aiohttp_client import async_get_clientsession @@ -53,7 +52,6 @@ def __init__(self, hass: HomeAssistant) -> None: self.session = async_get_clientsession(hass) self._data = {} self._store: Store = hass.helpers.storage.Store(STORAGE_VERSION, STORAGE_KEY) - self._supervisor: Optional[HassIO] = self.hass.data.get(HASSIO_DOMAIN) @property def preferences(self) -> List[AnalyticsPreference]: @@ -65,11 +63,16 @@ def onboarded(self) -> bool: """Return bool if the user has made a choice.""" return self._data.get(ATTR_ONBOARDED, False) + @property + def supervisor(self) -> bool: + """Return bool if a supervisor is present.""" + return hassio.DOMAIN in self.hass.data + async def load(self) -> None: """Load preferences.""" self._data = await self._store.async_load() or {} - if self._supervisor: - supervisor_info = await self._supervisor.get_supervisor_info() + 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 ( @@ -89,9 +92,9 @@ async def save_preferences(self, preferences: List[AnalyticsPreference]) -> None self._data[ATTR_ONBOARDED] = True await self._store.async_save(self._data) - if self._supervisor: - await self._supervisor.update_diagnostics( - AnalyticsPreference.DIAGNOSTICS in self.preferences + if self.supervisor: + await hassio.async_update_diagnostics( + self.hass, AnalyticsPreference.DIAGNOSTICS in self.preferences ) async def send_analytics(self, _=None) -> None: @@ -104,8 +107,8 @@ async def send_analytics(self, _=None) -> None: huuid = await self.hass.helpers.instance_id.async_get() - if self._supervisor: - supervisor_info = await self._supervisor.get_supervisor_info() + if self.supervisor: + supervisor_info = hassio.get_supervisor_info(self.hass) system_info = await async_get_system_info(self.hass) integrations = [] @@ -148,7 +151,7 @@ async def send_analytics(self, _=None) -> None: if supervisor_info is not None: installed_addons = await asyncio.gather( *[ - self._supervisor.get_addon_info(addon[ATTR_SLUG]) + hassio.async_get_addon_info(self.hass, addon[ATTR_SLUG]) for addon in supervisor_info[ATTR_ADDONS] ] ) diff --git a/homeassistant/components/hassio/__init__.py b/homeassistant/components/hassio/__init__.py index c09927fa7d2f3..7957cb67a027c 100644 --- a/homeassistant/components/hassio/__init__.py +++ b/homeassistant/components/hassio/__init__.py @@ -159,6 +159,16 @@ async def async_get_addon_info(hass: HomeAssistantType, slug: str) -> dict: 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 @api_data async def async_install_addon(hass: HomeAssistantType, slug: str) -> dict: diff --git a/tests/components/analytics/test_analytics.py b/tests/components/analytics/test_analytics.py index e665df21171ba..eb563bc94cef3 100644 --- a/tests/components/analytics/test_analytics.py +++ b/tests/components/analytics/test_analytics.py @@ -1,5 +1,5 @@ """The tests for the analytics .""" -from unittest.mock import patch +from unittest.mock import AsyncMock, Mock, PropertyMock, patch import aiohttp @@ -9,7 +9,6 @@ ATTR_PREFERENCES, AnalyticsPreference, ) -from homeassistant.components.hassio.const import DOMAIN as HASSIO_DOMAIN from homeassistant.const import __version__ as HA_VERSION MOCK_HUUID = "abcdefg" @@ -28,37 +27,36 @@ async def test_no_send(hass, caplog, aioclient_mock): assert len(aioclient_mock.mock_calls) == 0 -async def test_load_with_supervisor_diagnostics(hass, aioclient_mock, hassio_handler): +async def test_load_with_supervisor_diagnostics(hass): """Test loading with a supervisor that has diagnostics enabled.""" - aioclient_mock.get( - "http://127.0.0.1/supervisor/info", - json={"result": "ok", "data": {"diagnostics": True}}, - ) - hass.data[HASSIO_DOMAIN] = hassio_handler - analytics = Analytics(hass) assert AnalyticsPreference.DIAGNOSTICS not in analytics.preferences - await analytics.load() + with patch( + "homeassistant.components.hassio.get_supervisor_info", + side_effect=Mock(return_value={"diagnostics": True}), + ), patch( + "homeassistant.components.analytics.analytics.Analytics.supervisor", + PropertyMock(return_value=True), + ): + await analytics.load() assert AnalyticsPreference.DIAGNOSTICS in analytics.preferences -async def test_load_with_supervisor_without_diagnostics( - hass, aioclient_mock, hassio_handler -): +async def test_load_with_supervisor_without_diagnostics(hass): """Test loading with a supervisor that has not diagnostics enabled.""" - aioclient_mock.get( - "http://127.0.0.1/supervisor/info", - json={"result": "ok", "data": {"diagnostics": False}}, - ) - aioclient_mock.post("http://127.0.0.1/supervisor/options", json={"result": "ok"}) - hass.data[HASSIO_DOMAIN] = hassio_handler - analytics = Analytics(hass) analytics._data[ATTR_PREFERENCES] = [AnalyticsPreference.DIAGNOSTICS] assert AnalyticsPreference.DIAGNOSTICS in analytics.preferences - await analytics.load() + with patch( + "homeassistant.components.hassio.get_supervisor_info", + side_effect=Mock(return_value={"diagnostics": False}), + ), patch( + "homeassistant.components.analytics.analytics.Analytics.supervisor", + PropertyMock(return_value=True), + ): + await analytics.load() assert AnalyticsPreference.DIAGNOSTICS not in analytics.preferences @@ -103,21 +101,22 @@ async def test_send_base(hass, caplog, aioclient_mock): assert "'integrations':" not in caplog.text -async def test_send_base_with_supervisor(hass, caplog, aioclient_mock, hassio_handler): +async def test_send_base_with_supervisor(hass, caplog): """Test send base prefrences are defined.""" - aioclient_mock.post(ANALYTICS_ENDPOINT_URL, status=200) - aioclient_mock.post("http://127.0.0.1/supervisor/options", json={"result": "ok"}) - aioclient_mock.get( - "http://127.0.0.1/supervisor/info", - json={"result": "ok", "data": {"supported": True, "healthy": True}}, - ) - hass.data[HASSIO_DOMAIN] = hassio_handler analytics = Analytics(hass) await analytics.save_preferences([AnalyticsPreference.BASE]) assert analytics.preferences == ["base"] - with patch("homeassistant.helpers.instance_id.async_get", return_value=MOCK_HUUID): + with patch( + "homeassistant.components.hassio.get_supervisor_info", + side_effect=Mock(return_value={"supported": True, "healthy": True}), + ), patch( + "homeassistant.components.analytics.analytics.Analytics.supervisor", + PropertyMock(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 @@ -143,34 +142,9 @@ async def test_send_usage(hass, caplog, aioclient_mock): assert "'integration_count':" not in caplog.text -async def test_send_usage_with_supervisor(hass, caplog, aioclient_mock, hassio_handler): +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) - aioclient_mock.post("http://127.0.0.1/supervisor/options", json={"result": "ok"}) - aioclient_mock.get( - "http://127.0.0.1/supervisor/info", - json={ - "result": "ok", - "data": { - "healthy": True, - "supported": True, - "addons": [{"slug": "test_addon"}], - }, - }, - ) - aioclient_mock.get( - "http://127.0.0.1/addons/test_addon/info", - json={ - "result": "ok", - "data": { - "slug": "test_addon", - "protected": True, - "version": "1", - "auto_update": False, - }, - }, - ) - hass.data[HASSIO_DOMAIN] = hassio_handler analytics = Analytics(hass) await analytics.save_preferences( @@ -179,7 +153,31 @@ async def test_send_usage_with_supervisor(hass, caplog, aioclient_mock, hassio_h assert analytics.preferences == ["base", "usage"] hass.config.components = ["default_config"] - with patch("homeassistant.helpers.instance_id.async_get", return_value=MOCK_HUUID): + 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.async_get_addon_info", + side_effect=AsyncMock( + return_value={ + "slug": "test_addon", + "protected": True, + "version": "1", + "auto_update": False, + } + ), + ), patch( + "homeassistant.components.analytics.analytics.Analytics.supervisor", + PropertyMock(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}]" @@ -207,44 +205,40 @@ async def test_send_statistics(hass, caplog, aioclient_mock): assert "'integrations':" not in caplog.text -async def test_send_statistics_with_supervisor( - hass, caplog, aioclient_mock, hassio_handler -): +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) - aioclient_mock.post("http://127.0.0.1/supervisor/options", json={"result": "ok"}) - aioclient_mock.get( - "http://127.0.0.1/supervisor/info", - json={ - "result": "ok", - "data": { + analytics = Analytics(hass) + await analytics.save_preferences( + [AnalyticsPreference.BASE, AnalyticsPreference.STATISTICS] + ) + assert analytics.preferences == ["base", "statistics"] + + with patch( + "homeassistant.components.hassio.get_supervisor_info", + side_effect=Mock( + return_value={ "healthy": True, "supported": True, "addons": [{"slug": "test_addon"}], - }, - }, - ) - aioclient_mock.get( - "http://127.0.0.1/addons/test_addon/info", - json={ - "result": "ok", - "data": { + } + ), + ), patch( + "homeassistant.components.hassio.async_get_addon_info", + side_effect=AsyncMock( + return_value={ "slug": "test_addon", "protected": True, "version": "1", "auto_update": False, - }, - }, - ) - hass.data[HASSIO_DOMAIN] = hassio_handler - - analytics = Analytics(hass) - await analytics.save_preferences( - [AnalyticsPreference.BASE, AnalyticsPreference.STATISTICS] - ) - assert analytics.preferences == ["base", "statistics"] - - with patch("homeassistant.helpers.instance_id.async_get", return_value=MOCK_HUUID): + } + ), + ), patch( + "homeassistant.components.analytics.analytics.Analytics.supervisor", + PropertyMock(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 diff --git a/tests/components/hassio/conftest.py b/tests/components/hassio/conftest.py index ed4022a4b55cc..efa983c344032 100644 --- a/tests/components/hassio/conftest.py +++ b/tests/components/hassio/conftest.py @@ -4,7 +4,7 @@ import pytest -from homeassistant.components.hassio.handler import HassioAPIError +from homeassistant.components.hassio.handler import HassIO, HassioAPIError from homeassistant.core import CoreState from homeassistant.setup import async_setup_component @@ -63,3 +63,16 @@ async def hassio_client_supervisor(hass, aiohttp_client, hassio_stubs): hass.http.app, headers={"Authorization": f"Bearer {access_token}"}, ) + + +@pytest.fixture +def hassio_handler(hass, aioclient_mock): + """Create mock hassio handler.""" + + async def get_client_session(): + return hass.helpers.aiohttp_client.async_get_clientsession() + + websession = hass.loop.run_until_complete(get_client_session()) + + with patch.dict(os.environ, {"HASSIO_TOKEN": HASSIO_TOKEN}): + yield HassIO(hass.loop, websession, "127.0.0.1") diff --git a/tests/conftest.py b/tests/conftest.py index dba56b130976a..3fc2dc748cbd1 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -3,7 +3,6 @@ import datetime import functools import logging -import os import ssl import threading from unittest.mock import MagicMock, patch @@ -18,7 +17,6 @@ from homeassistant.auth.models import Credentials from homeassistant.auth.providers import homeassistant, legacy_api_password from homeassistant.components import mqtt -from homeassistant.components.hassio.handler import HassIO from homeassistant.components.websocket_api.auth import ( TYPE_AUTH, TYPE_AUTH_OK, @@ -31,7 +29,6 @@ from homeassistant.setup import async_setup_component from homeassistant.util import location -from tests.components.hassio import HASSIO_TOKEN from tests.ignore_uncaught_exceptions import IGNORE_UNCAUGHT_EXCEPTIONS pytest.register_assert_rewrite("tests.common") @@ -598,16 +595,3 @@ def pattern_time_change_listener(ev) -> None: def enable_custom_integrations(hass): """Enable custom integrations defined in the test dir.""" hass.data.pop(loader.DATA_CUSTOM_COMPONENTS) - - -@pytest.fixture -def hassio_handler(hass, aioclient_mock): - """Create mock hassio handler.""" - - async def get_client_session(): - return hass.helpers.aiohttp_client.async_get_clientsession() - - websession = hass.loop.run_until_complete(get_client_session()) - - with patch.dict(os.environ, {"HASSIO_TOKEN": HASSIO_TOKEN}): - yield HassIO(hass.loop, websession, "127.0.0.1") From 5a9f738798acc4b0fade45907e83392e19a69b3f Mon Sep 17 00:00:00 2001 From: Ludeeus Date: Wed, 24 Mar 2021 15:10:23 +0000 Subject: [PATCH 12/16] Fix missing aioclient_mock --- tests/components/analytics/test_analytics.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/components/analytics/test_analytics.py b/tests/components/analytics/test_analytics.py index eb563bc94cef3..cd37e9b2d7c2a 100644 --- a/tests/components/analytics/test_analytics.py +++ b/tests/components/analytics/test_analytics.py @@ -101,8 +101,9 @@ async def test_send_base(hass, caplog, aioclient_mock): assert "'integrations':" not in caplog.text -async def test_send_base_with_supervisor(hass, caplog): +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([AnalyticsPreference.BASE]) From 41a1511c7fbd63623fb09b7b5183bf866389cd1b Mon Sep 17 00:00:00 2001 From: Ludeeus Date: Wed, 24 Mar 2021 19:41:24 +0000 Subject: [PATCH 13/16] Update endpoint URL --- homeassistant/components/analytics/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/analytics/const.py b/homeassistant/components/analytics/const.py index 9eae2833963f0..cbed4e76c467a 100644 --- a/homeassistant/components/analytics/const.py +++ b/homeassistant/components/analytics/const.py @@ -3,7 +3,7 @@ from enum import Enum import logging -ANALYTICS_ENDPOINT_URL = "https://floral-resonance-f63b.ludeeus.workers.dev" +ANALYTICS_ENDPOINT_URL = "https://analytics-api.home-assistant.io" DOMAIN = "analytics" INTERVAL = timedelta(days=1) STORAGE_KEY = "core.analytics" From 322288c3b409a1fbb513bbfc93a63402084b174e Mon Sep 17 00:00:00 2001 From: Ludeeus Date: Thu, 25 Mar 2021 10:31:57 +0000 Subject: [PATCH 14/16] Address comments --- .../components/analytics/__init__.py | 5 +- .../components/analytics/analytics.py | 66 ++++++----- homeassistant/components/analytics/const.py | 69 +++-------- tests/components/analytics/test_analytics.py | 111 +++++++++++------- tests/components/analytics/test_init.py | 6 +- 5 files changed, 120 insertions(+), 137 deletions(-) diff --git a/homeassistant/components/analytics/__init__.py b/homeassistant/components/analytics/__init__.py index 6d35bdf5d5d79..3a06c56add585 100644 --- a/homeassistant/components/analytics/__init__.py +++ b/homeassistant/components/analytics/__init__.py @@ -4,11 +4,10 @@ from homeassistant.components import websocket_api from homeassistant.const import EVENT_HOMEASSISTANT_STARTED from homeassistant.core import HomeAssistant -from homeassistant.helpers import config_validation as cv 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 +from .const import ATTR_HUUID, ATTR_PREFERENCES, DOMAIN, INTERVAL, PREFERENCE_SCHEMA async def async_setup(hass: HomeAssistant, _): @@ -57,7 +56,7 @@ async def websocket_analytics( @websocket_api.websocket_command( { vol.Required("type"): "analytics/preferences", - vol.Required("preferences"): cv.ensure_list, + vol.Required("preferences", default={}): PREFERENCE_SCHEMA, } ) async def websocket_analytics_preferences( diff --git a/homeassistant/components/analytics/analytics.py b/homeassistant/components/analytics/analytics.py index fc7e262d70d20..69e3d63a0617b 100644 --- a/homeassistant/components/analytics/analytics.py +++ b/homeassistant/components/analytics/analytics.py @@ -1,6 +1,5 @@ """Analytics helper class for the analytics integration.""" import asyncio -from typing import List import aiohttp import async_timeout @@ -21,6 +20,7 @@ ATTR_ADDONS, ATTR_AUTO_UPDATE, ATTR_AUTOMATION_COUNT, + ATTR_BASE, ATTR_DIAGNOSTICS, ATTR_HEALTHY, ATTR_HUUID, @@ -31,15 +31,16 @@ ATTR_PROTECTED, ATTR_SLUG, ATTR_STATE_COUNT, + ATTR_STATISTICS, ATTR_SUPERVISOR, ATTR_SUPPORTED, + ATTR_USAGE, ATTR_USER_COUNT, ATTR_VERSION, - INGORED_DOMAINS, LOGGER, + PREFERENCE_SCHEMA, STORAGE_KEY, STORAGE_VERSION, - AnalyticsPreference, ) @@ -50,58 +51,66 @@ def __init__(self, hass: HomeAssistant) -> None: """Initialize the Analytics class.""" self.hass: HomeAssistant = hass self.session = async_get_clientsession(hass) - self._data = {} + self._data = {ATTR_PREFERENCES: {}, ATTR_ONBOARDED: False} self._store: Store = hass.helpers.storage.Store(STORAGE_VERSION, STORAGE_KEY) @property - def preferences(self) -> List[AnalyticsPreference]: + def preferences(self) -> dict: """Return the current active preferences.""" - return self._data.get(ATTR_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.get(ATTR_ONBOARDED, False) + return self._data[ATTR_ONBOARDED] @property def supervisor(self) -> bool: """Return bool if a supervisor is present.""" - return hassio.DOMAIN in self.hass.data + return hassio.is_hassio(self.hass) async def load(self) -> None: """Load preferences.""" - self._data = await self._store.async_load() or {} + 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 AnalyticsPreference.DIAGNOSTICS not in self.preferences + if supervisor_info[ATTR_DIAGNOSTICS] and not self.preferences.get( + ATTR_DIAGNOSTICS, False ): - self._data.setdefault(ATTR_PREFERENCES, []).append(ATTR_DIAGNOSTICS) - elif ( - not supervisor_info[ATTR_DIAGNOSTICS] - and AnalyticsPreference.DIAGNOSTICS in self.preferences + self._data[ATTR_PREFERENCES][ATTR_DIAGNOSTICS] = True + elif not supervisor_info[ATTR_DIAGNOSTICS] and self.preferences.get( + ATTR_DIAGNOSTICS, False ): - self._data.setdefault(ATTR_PREFERENCES, []).remove(ATTR_DIAGNOSTICS) + self._data[ATTR_PREFERENCES][ATTR_DIAGNOSTICS] = False - async def save_preferences(self, preferences: List[AnalyticsPreference]) -> None: + async def save_preferences(self, preferences: dict) -> None: """Save preferences.""" - self._data[ATTR_PREFERENCES] = preferences + preferences = PREFERENCE_SCHEMA(preferences) + for key in preferences: + self._data[ATTR_PREFERENCES][key] = preferences[key] self._data[ATTR_ONBOARDED] = True await self._store.async_save(self._data) if self.supervisor: await hassio.async_update_diagnostics( - self.hass, AnalyticsPreference.DIAGNOSTICS in self.preferences + 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 AnalyticsPreference.BASE not in self.preferences: + if not self.onboarded or not self.preferences.get(ATTR_BASE, False): LOGGER.debug("Nothing to submit") return @@ -125,9 +134,8 @@ async def send_analytics(self, _=None) -> None: ATTR_SUPPORTED: supervisor_info[ATTR_SUPPORTED], } - if ( - AnalyticsPreference.USAGE in self.preferences - or AnalyticsPreference.STATISTICS in self.preferences + if self.preferences.get(ATTR_USAGE, False) or self.preferences.get( + ATTR_STATISTICS, False ): configured_integrations = await asyncio.gather( *[ @@ -139,11 +147,7 @@ async def send_analytics(self, _=None) -> None: ) for integration in configured_integrations: - if ( - integration.disabled - or integration.domain in INGORED_DOMAINS - or not integration.is_built_in - ): + if integration.disabled or not integration.is_built_in: continue integrations.append(integration.domain) @@ -165,12 +169,12 @@ async def send_analytics(self, _=None) -> None: } ) - if AnalyticsPreference.USAGE in self.preferences: + if self.preferences.get(ATTR_USAGE, False): payload[ATTR_INTEGRATIONS] = integrations if supervisor_info is not None: payload[ATTR_ADDONS] = addons - if AnalyticsPreference.STATISTICS in self.preferences: + 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) diff --git a/homeassistant/components/analytics/const.py b/homeassistant/components/analytics/const.py index cbed4e76c467a..2a501b0c42079 100644 --- a/homeassistant/components/analytics/const.py +++ b/homeassistant/components/analytics/const.py @@ -1,8 +1,9 @@ """Constants for the analytics integration.""" from datetime import timedelta -from enum import Enum import logging +import voluptuous as vol + ANALYTICS_ENDPOINT_URL = "https://analytics-api.home-assistant.io" DOMAIN = "analytics" INTERVAL = timedelta(days=1) @@ -14,8 +15,9 @@ ATTR_ADDON_COUNT = "addon_count" ATTR_ADDONS = "addons" -ATTR_AUTOMATION_COUNT = "automation_count" ATTR_AUTO_UPDATE = "auto_update" +ATTR_AUTOMATION_COUNT = "automation_count" +ATTR_BASE = "base" ATTR_DIAGNOSTICS = "diagnostics" ATTR_HEALTHY = "healthy" ATTR_HUUID = "huuid" @@ -27,62 +29,19 @@ 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" -class AnalyticsPreference(str, Enum): - """Analytics prefrences.""" - - BASE = "base" - DIAGNOSTICS = "diagnostics" - STATISTICS = "statistics" - USAGE = "usage" - - -INGORED_DOMAINS = [ - "air_quality", - "alarm_control_panel", - "analytics", - "api", - "auth", - "binary_sensor", - "calendar", - "camera", - "climate", - "config", - "conversation", - "cover", - "demo", - "device_automation", - "device_tracker", - "fan", - "hassio", - "homeassistant", - "http", - "humidifier", - "image_processing", - "image", - "light", - "lock", - "logger", - "lovelace", - "media_player", - "notify", - "number", - "onboarding", - "persistent_notification", - "recorder", - "search", - "sensor", - "stt", - "switch", - "system_log", - "tts", - "vacuum", - "water_heater", - "weather", - "websocket_api", -] +PREFERENCE_SCHEMA = vol.Schema( + { + vol.Optional(ATTR_BASE): bool, + vol.Optional(ATTR_DIAGNOSTICS): bool, + vol.Optional(ATTR_STATISTICS): bool, + vol.Optional(ATTR_USAGE): bool, + } +) diff --git a/tests/components/analytics/test_analytics.py b/tests/components/analytics/test_analytics.py index cd37e9b2d7c2a..d4692b4fcc068 100644 --- a/tests/components/analytics/test_analytics.py +++ b/tests/components/analytics/test_analytics.py @@ -1,13 +1,16 @@ """The tests for the analytics .""" -from unittest.mock import AsyncMock, Mock, PropertyMock, patch +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, - AnalyticsPreference, + ATTR_STATISTICS, + ATTR_USAGE, ) from homeassistant.const import __version__ as HA_VERSION @@ -18,11 +21,15 @@ 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) - await analytics.load() - assert analytics.preferences == [] + 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] - with patch("homeassistant.helpers.instance_id.async_get", return_value=MOCK_HUUID): await analytics.send_analytics() + assert "Nothing to submit" in caplog.text assert len(aioclient_mock.mock_calls) == 0 @@ -30,43 +37,43 @@ async def test_no_send(hass, caplog, aioclient_mock): async def test_load_with_supervisor_diagnostics(hass): """Test loading with a supervisor that has diagnostics enabled.""" analytics = Analytics(hass) - assert AnalyticsPreference.DIAGNOSTICS not in analytics.preferences + assert not analytics.preferences[ATTR_DIAGNOSTICS] with patch( "homeassistant.components.hassio.get_supervisor_info", side_effect=Mock(return_value={"diagnostics": True}), ), patch( - "homeassistant.components.analytics.analytics.Analytics.supervisor", - PropertyMock(return_value=True), + "homeassistant.components.hassio.is_hassio", + side_effect=Mock(return_value=True), ): await analytics.load() - assert AnalyticsPreference.DIAGNOSTICS in analytics.preferences + 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] = [AnalyticsPreference.DIAGNOSTICS] + analytics._data[ATTR_PREFERENCES][ATTR_DIAGNOSTICS] = True - assert AnalyticsPreference.DIAGNOSTICS in analytics.preferences + assert analytics.preferences[ATTR_DIAGNOSTICS] with patch( "homeassistant.components.hassio.get_supervisor_info", side_effect=Mock(return_value={"diagnostics": False}), ), patch( - "homeassistant.components.analytics.analytics.Analytics.supervisor", - PropertyMock(return_value=True), + "homeassistant.components.hassio.is_hassio", + side_effect=Mock(return_value=True), ): await analytics.load() - assert AnalyticsPreference.DIAGNOSTICS not in analytics.preferences + 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([AnalyticsPreference.BASE]) - assert analytics.preferences == ["base"] + 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() @@ -77,8 +84,8 @@ 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([AnalyticsPreference.BASE]) - assert analytics.preferences == ["base"] + 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() @@ -89,8 +96,8 @@ 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([AnalyticsPreference.BASE]) - assert analytics.preferences == ["base"] + 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() @@ -106,15 +113,21 @@ async def test_send_base_with_supervisor(hass, caplog, aioclient_mock): aioclient_mock.post(ANALYTICS_ENDPOINT_URL, status=200) analytics = Analytics(hass) - await analytics.save_preferences([AnalyticsPreference.BASE]) - assert analytics.preferences == ["base"] + 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.analytics.analytics.Analytics.supervisor", - PropertyMock(return_value=True), + "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 ): @@ -131,11 +144,10 @@ 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( - [AnalyticsPreference.BASE, AnalyticsPreference.USAGE] - ) - assert analytics.preferences == ["base", "usage"] - hass.config.components = ["default_config", "api"] + 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() @@ -148,10 +160,9 @@ async def test_send_usage_with_supervisor(hass, caplog, aioclient_mock): aioclient_mock.post(ANALYTICS_ENDPOINT_URL, status=200) analytics = Analytics(hass) - await analytics.save_preferences( - [AnalyticsPreference.BASE, AnalyticsPreference.USAGE] - ) - assert analytics.preferences == ["base", "usage"] + 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( @@ -163,6 +174,12 @@ async def test_send_usage_with_supervisor(hass, caplog, aioclient_mock): "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( @@ -174,8 +191,8 @@ async def test_send_usage_with_supervisor(hass, caplog, aioclient_mock): } ), ), patch( - "homeassistant.components.analytics.analytics.Analytics.supervisor", - PropertyMock(return_value=True), + "homeassistant.components.hassio.is_hassio", + side_effect=Mock(return_value=True), ), patch( "homeassistant.helpers.instance_id.async_get", return_value=MOCK_HUUID ): @@ -191,10 +208,9 @@ 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( - [AnalyticsPreference.BASE, AnalyticsPreference.STATISTICS] - ) - assert analytics.preferences == ["base", "statistics"] + 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): @@ -210,10 +226,9 @@ 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( - [AnalyticsPreference.BASE, AnalyticsPreference.STATISTICS] - ) - assert analytics.preferences == ["base", "statistics"] + 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", @@ -224,6 +239,12 @@ async def test_send_statistics_with_supervisor(hass, caplog, aioclient_mock): "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( @@ -235,8 +256,8 @@ async def test_send_statistics_with_supervisor(hass, caplog, aioclient_mock): } ), ), patch( - "homeassistant.components.analytics.analytics.Analytics.supervisor", - PropertyMock(return_value=True), + "homeassistant.components.hassio.is_hassio", + side_effect=Mock(return_value=True), ), patch( "homeassistant.helpers.instance_id.async_get", return_value=MOCK_HUUID ): diff --git a/tests/components/analytics/test_init.py b/tests/components/analytics/test_init.py index 564015950a7be..4f8c95bc6b413 100644 --- a/tests/components/analytics/test_init.py +++ b/tests/components/analytics/test_init.py @@ -29,14 +29,14 @@ async def test_websocket(hass, hass_ws_client, aioclient_mock): assert response["result"]["huuid"] == "abcdef" await ws_client.send_json( - {"id": 2, "type": "analytics/preferences", "preferences": ["base"]} + {"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"] + 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"]["preferences"]["base"] assert response["result"]["huuid"] == "abcdef" From 5ace01bb50c0db8608747f11c31c55b50fe8b554 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Thu, 25 Mar 2021 14:08:36 +0100 Subject: [PATCH 15/16] Update homeassistant/components/analytics/analytics.py Co-authored-by: Martin Hjelmare --- homeassistant/components/analytics/analytics.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/homeassistant/components/analytics/analytics.py b/homeassistant/components/analytics/analytics.py index 69e3d63a0617b..2963e41f90b67 100644 --- a/homeassistant/components/analytics/analytics.py +++ b/homeassistant/components/analytics/analytics.py @@ -96,8 +96,7 @@ async def load(self) -> None: async def save_preferences(self, preferences: dict) -> None: """Save preferences.""" preferences = PREFERENCE_SCHEMA(preferences) - for key in preferences: - self._data[ATTR_PREFERENCES][key] = preferences[key] + self._data[ATTR_PREFERENCES].update(preferences) self._data[ATTR_ONBOARDED] = True await self._store.async_save(self._data) From c3c4010e792243534ce010454199d60207a4d2d9 Mon Sep 17 00:00:00 2001 From: Ludeeus Date: Fri, 26 Mar 2021 11:11:20 +0000 Subject: [PATCH 16/16] Update endpoint URL --- homeassistant/components/analytics/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/analytics/const.py b/homeassistant/components/analytics/const.py index 2a501b0c42079..ba56ba265a715 100644 --- a/homeassistant/components/analytics/const.py +++ b/homeassistant/components/analytics/const.py @@ -4,7 +4,7 @@ import voluptuous as vol -ANALYTICS_ENDPOINT_URL = "https://analytics-api.home-assistant.io" +ANALYTICS_ENDPOINT_URL = "https://analytics-api.home-assistant.io/v1" DOMAIN = "analytics" INTERVAL = timedelta(days=1) STORAGE_KEY = "core.analytics"