diff --git a/CODEOWNERS b/CODEOWNERS index 50680453b36..bbce7367477 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 00000000000..3a06c56add5 --- /dev/null +++ b/homeassistant/components/analytics/__init__.py @@ -0,0 +1,77 @@ +"""Send instance and usage analytics.""" +import voluptuous as vol + +from homeassistant.components import websocket_api +from homeassistant.const import EVENT_HOMEASSISTANT_STARTED +from homeassistant.core import HomeAssistant +from homeassistant.helpers.event import async_call_later, async_track_time_interval + +from .analytics import Analytics +from .const import ATTR_HUUID, ATTR_PREFERENCES, DOMAIN, INTERVAL, PREFERENCE_SCHEMA + + +async def async_setup(hass: HomeAssistant, _): + """Set up the analytics integration.""" + analytics = Analytics(hass) + + # Load stored data + await analytics.load() + + async def start_schedule(_event): + """Start the send schedule after the started event.""" + # Wait 15 min after started + async_call_later(hass, 900, analytics.send_analytics) + + # Send every day + async_track_time_interval(hass, analytics.send_analytics, INTERVAL) + + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STARTED, start_schedule) + + websocket_api.async_register_command(hass, websocket_analytics) + websocket_api.async_register_command(hass, websocket_analytics_preferences) + + hass.data[DOMAIN] = analytics + return True + + +@websocket_api.require_admin +@websocket_api.async_response +@websocket_api.websocket_command({vol.Required("type"): "analytics"}) +async def websocket_analytics( + hass: HomeAssistant, + connection: websocket_api.connection.ActiveConnection, + msg: dict, +) -> None: + """Return analytics preferences.""" + analytics: Analytics = hass.data[DOMAIN] + huuid = await hass.helpers.instance_id.async_get() + connection.send_result( + msg["id"], + {ATTR_PREFERENCES: analytics.preferences, ATTR_HUUID: huuid}, + ) + + +@websocket_api.require_admin +@websocket_api.async_response +@websocket_api.websocket_command( + { + vol.Required("type"): "analytics/preferences", + vol.Required("preferences", default={}): PREFERENCE_SCHEMA, + } +) +async def websocket_analytics_preferences( + hass: HomeAssistant, + connection: websocket_api.connection.ActiveConnection, + msg: dict, +) -> None: + """Update analytics preferences.""" + preferences = msg[ATTR_PREFERENCES] + analytics: Analytics = hass.data[DOMAIN] + + await analytics.save_preferences(preferences) + await analytics.send_analytics() + + connection.send_result( + msg["id"], + {ATTR_PREFERENCES: analytics.preferences}, + ) diff --git a/homeassistant/components/analytics/analytics.py b/homeassistant/components/analytics/analytics.py new file mode 100644 index 00000000000..2963e41f90b --- /dev/null +++ b/homeassistant/components/analytics/analytics.py @@ -0,0 +1,212 @@ +"""Analytics helper class for the analytics integration.""" +import asyncio + +import aiohttp +import async_timeout + +from homeassistant.components import hassio +from homeassistant.components.api import ATTR_INSTALLATION_TYPE +from homeassistant.components.automation.const import DOMAIN as AUTOMATION_DOMAIN +from homeassistant.const import __version__ as HA_VERSION +from homeassistant.core import HomeAssistant +from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers.storage import Store +from homeassistant.helpers.system_info import async_get_system_info +from homeassistant.loader import async_get_integration + +from .const import ( + ANALYTICS_ENDPOINT_URL, + ATTR_ADDON_COUNT, + ATTR_ADDONS, + ATTR_AUTO_UPDATE, + ATTR_AUTOMATION_COUNT, + ATTR_BASE, + ATTR_DIAGNOSTICS, + ATTR_HEALTHY, + ATTR_HUUID, + ATTR_INTEGRATION_COUNT, + ATTR_INTEGRATIONS, + ATTR_ONBOARDED, + ATTR_PREFERENCES, + ATTR_PROTECTED, + ATTR_SLUG, + ATTR_STATE_COUNT, + ATTR_STATISTICS, + ATTR_SUPERVISOR, + ATTR_SUPPORTED, + ATTR_USAGE, + ATTR_USER_COUNT, + ATTR_VERSION, + LOGGER, + PREFERENCE_SCHEMA, + STORAGE_KEY, + STORAGE_VERSION, +) + + +class Analytics: + """Analytics helper class for the analytics integration.""" + + def __init__(self, hass: HomeAssistant) -> None: + """Initialize the Analytics class.""" + self.hass: HomeAssistant = hass + self.session = async_get_clientsession(hass) + self._data = {ATTR_PREFERENCES: {}, ATTR_ONBOARDED: False} + self._store: Store = hass.helpers.storage.Store(STORAGE_VERSION, STORAGE_KEY) + + @property + def preferences(self) -> dict: + """Return the current active preferences.""" + preferences = self._data[ATTR_PREFERENCES] + return { + ATTR_BASE: preferences.get(ATTR_BASE, False), + ATTR_DIAGNOSTICS: preferences.get(ATTR_DIAGNOSTICS, False), + ATTR_USAGE: preferences.get(ATTR_USAGE, False), + ATTR_STATISTICS: preferences.get(ATTR_STATISTICS, False), + } + + @property + def onboarded(self) -> bool: + """Return bool if the user has made a choice.""" + return self._data[ATTR_ONBOARDED] + + @property + def supervisor(self) -> bool: + """Return bool if a supervisor is present.""" + return hassio.is_hassio(self.hass) + + async def load(self) -> None: + """Load preferences.""" + stored = await self._store.async_load() + if stored: + self._data = stored + if self.supervisor: + supervisor_info = hassio.get_supervisor_info(self.hass) + if not self.onboarded: + # User have not configured analytics, get this setting from the supervisor + if supervisor_info[ATTR_DIAGNOSTICS] and not self.preferences.get( + ATTR_DIAGNOSTICS, False + ): + self._data[ATTR_PREFERENCES][ATTR_DIAGNOSTICS] = True + elif not supervisor_info[ATTR_DIAGNOSTICS] and self.preferences.get( + ATTR_DIAGNOSTICS, False + ): + self._data[ATTR_PREFERENCES][ATTR_DIAGNOSTICS] = False + + async def save_preferences(self, preferences: dict) -> None: + """Save preferences.""" + preferences = PREFERENCE_SCHEMA(preferences) + self._data[ATTR_PREFERENCES].update(preferences) + self._data[ATTR_ONBOARDED] = True + await self._store.async_save(self._data) + + if self.supervisor: + await hassio.async_update_diagnostics( + self.hass, self.preferences.get(ATTR_DIAGNOSTICS, False) + ) + + async def send_analytics(self, _=None) -> None: + """Send analytics.""" + supervisor_info = None + + if not self.onboarded or not self.preferences.get(ATTR_BASE, False): + LOGGER.debug("Nothing to submit") + return + + huuid = await self.hass.helpers.instance_id.async_get() + + if self.supervisor: + supervisor_info = hassio.get_supervisor_info(self.hass) + + system_info = await async_get_system_info(self.hass) + integrations = [] + addons = [] + payload: dict = { + ATTR_HUUID: huuid, + ATTR_VERSION: HA_VERSION, + ATTR_INSTALLATION_TYPE: system_info[ATTR_INSTALLATION_TYPE], + } + + if supervisor_info is not None: + payload[ATTR_SUPERVISOR] = { + ATTR_HEALTHY: supervisor_info[ATTR_HEALTHY], + ATTR_SUPPORTED: supervisor_info[ATTR_SUPPORTED], + } + + if self.preferences.get(ATTR_USAGE, False) or self.preferences.get( + ATTR_STATISTICS, False + ): + configured_integrations = await asyncio.gather( + *[ + async_get_integration(self.hass, domain) + for domain in self.hass.config.components + # Filter out platforms. + if "." not in domain + ] + ) + + for integration in configured_integrations: + if integration.disabled or not integration.is_built_in: + continue + + integrations.append(integration.domain) + + if supervisor_info is not None: + installed_addons = await asyncio.gather( + *[ + hassio.async_get_addon_info(self.hass, addon[ATTR_SLUG]) + for addon in supervisor_info[ATTR_ADDONS] + ] + ) + for addon in installed_addons: + addons.append( + { + ATTR_SLUG: addon[ATTR_SLUG], + ATTR_PROTECTED: addon[ATTR_PROTECTED], + ATTR_VERSION: addon[ATTR_VERSION], + ATTR_AUTO_UPDATE: addon[ATTR_AUTO_UPDATE], + } + ) + + if self.preferences.get(ATTR_USAGE, False): + payload[ATTR_INTEGRATIONS] = integrations + if supervisor_info is not None: + payload[ATTR_ADDONS] = addons + + if self.preferences.get(ATTR_STATISTICS, False): + payload[ATTR_STATE_COUNT] = len(self.hass.states.async_all()) + payload[ATTR_AUTOMATION_COUNT] = len( + self.hass.states.async_all(AUTOMATION_DOMAIN) + ) + payload[ATTR_INTEGRATION_COUNT] = len(integrations) + if supervisor_info is not None: + payload[ATTR_ADDON_COUNT] = len(addons) + payload[ATTR_USER_COUNT] = len( + [ + user + for user in await self.hass.auth.async_get_users() + if not user.system_generated + ] + ) + + try: + with async_timeout.timeout(30): + response = await self.session.post(ANALYTICS_ENDPOINT_URL, json=payload) + if response.status == 200: + LOGGER.info( + ( + "Submitted analytics to Home Assistant servers. " + "Information submitted includes %s" + ), + payload, + ) + else: + LOGGER.warning( + "Sending analytics failed with statuscode %s", response.status + ) + except asyncio.TimeoutError: + LOGGER.error("Timeout sending analytics to %s", ANALYTICS_ENDPOINT_URL) + except aiohttp.ClientError as err: + LOGGER.error( + "Error sending analytics to %s: %r", ANALYTICS_ENDPOINT_URL, err + ) diff --git a/homeassistant/components/analytics/const.py b/homeassistant/components/analytics/const.py new file mode 100644 index 00000000000..ba56ba265a7 --- /dev/null +++ b/homeassistant/components/analytics/const.py @@ -0,0 +1,47 @@ +"""Constants for the analytics integration.""" +from datetime import timedelta +import logging + +import voluptuous as vol + +ANALYTICS_ENDPOINT_URL = "https://analytics-api.home-assistant.io/v1" +DOMAIN = "analytics" +INTERVAL = timedelta(days=1) +STORAGE_KEY = "core.analytics" +STORAGE_VERSION = 1 + + +LOGGER: logging.Logger = logging.getLogger(__package__) + +ATTR_ADDON_COUNT = "addon_count" +ATTR_ADDONS = "addons" +ATTR_AUTO_UPDATE = "auto_update" +ATTR_AUTOMATION_COUNT = "automation_count" +ATTR_BASE = "base" +ATTR_DIAGNOSTICS = "diagnostics" +ATTR_HEALTHY = "healthy" +ATTR_HUUID = "huuid" +ATTR_INSTALLATION_TYPE = "installation_type" +ATTR_INTEGRATION_COUNT = "integration_count" +ATTR_INTEGRATIONS = "integrations" +ATTR_ONBOARDED = "onboarded" +ATTR_PREFERENCES = "preferences" +ATTR_PROTECTED = "protected" +ATTR_SLUG = "slug" +ATTR_STATE_COUNT = "state_count" +ATTR_STATISTICS = "statistics" +ATTR_SUPERVISOR = "supervisor" +ATTR_SUPPORTED = "supported" +ATTR_USAGE = "usage" +ATTR_USER_COUNT = "user_count" +ATTR_VERSION = "version" + + +PREFERENCE_SCHEMA = vol.Schema( + { + vol.Optional(ATTR_BASE): bool, + vol.Optional(ATTR_DIAGNOSTICS): bool, + vol.Optional(ATTR_STATISTICS): bool, + vol.Optional(ATTR_USAGE): bool, + } +) diff --git a/homeassistant/components/analytics/manifest.json b/homeassistant/components/analytics/manifest.json new file mode 100644 index 00000000000..de3fdfbdccd --- /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 0f4b940cc36..fa7f547869d 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/__init__.py b/homeassistant/components/hassio/__init__.py index 9fcb73884b3..a5a2a1886d7 100644 --- a/homeassistant/components/hassio/__init__.py +++ b/homeassistant/components/hassio/__init__.py @@ -161,6 +161,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/homeassistant/components/hassio/handler.py b/homeassistant/components/hassio/handler.py index 303d6770255..90077261185 100644 --- a/homeassistant/components/hassio/handler.py +++ b/homeassistant/components/hassio/handler.py @@ -185,6 +185,16 @@ class HassIO: """ 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 bedfa703a9b..e383e4e32c4 100644 --- a/homeassistant/components/onboarding/__init__.py +++ b/homeassistant/components/onboarding/__init__.py @@ -4,10 +4,17 @@ from homeassistant.helpers.storage import Store 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 @@ class OnboadingStorage(Store): 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 bf350a200de..5a771f524ac 100644 --- a/homeassistant/components/onboarding/const.py +++ b/homeassistant/components/onboarding/const.py @@ -3,7 +3,8 @@ DOMAIN = "onboarding" 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 e2fb8e084b8..06c9946b5c9 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 1d5528688dd..dec80642845 100644 --- a/homeassistant/components/onboarding/views.py +++ b/homeassistant/components/onboarding/views.py @@ -14,6 +14,7 @@ from homeassistant.core import callback 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 @@ class IntegrationOnboardingView(_BaseOnboardingView): 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 00000000000..7cf0ac9f7ba --- /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 00000000000..d4692b4fcc0 --- /dev/null +++ b/tests/components/analytics/test_analytics.py @@ -0,0 +1,266 @@ +"""The tests for the analytics .""" +from unittest.mock import AsyncMock, Mock, patch + +import aiohttp + +from homeassistant.components.analytics.analytics import Analytics +from homeassistant.components.analytics.const import ( + ANALYTICS_ENDPOINT_URL, + ATTR_BASE, + ATTR_DIAGNOSTICS, + ATTR_PREFERENCES, + ATTR_STATISTICS, + ATTR_USAGE, +) +from homeassistant.const import __version__ as HA_VERSION + +MOCK_HUUID = "abcdefg" + + +async def test_no_send(hass, caplog, aioclient_mock): + """Test send when no prefrences are defined.""" + aioclient_mock.post(ANALYTICS_ENDPOINT_URL, status=200) + analytics = Analytics(hass) + with patch( + "homeassistant.components.hassio.is_hassio", + side_effect=Mock(return_value=False), + ), patch("homeassistant.helpers.instance_id.async_get", return_value=MOCK_HUUID): + await analytics.load() + assert not analytics.preferences[ATTR_BASE] + + await analytics.send_analytics() + + assert "Nothing to submit" in caplog.text + assert len(aioclient_mock.mock_calls) == 0 + + +async def test_load_with_supervisor_diagnostics(hass): + """Test loading with a supervisor that has diagnostics enabled.""" + analytics = Analytics(hass) + assert not analytics.preferences[ATTR_DIAGNOSTICS] + with patch( + "homeassistant.components.hassio.get_supervisor_info", + side_effect=Mock(return_value={"diagnostics": True}), + ), patch( + "homeassistant.components.hassio.is_hassio", + side_effect=Mock(return_value=True), + ): + await analytics.load() + assert analytics.preferences[ATTR_DIAGNOSTICS] + + +async def test_load_with_supervisor_without_diagnostics(hass): + """Test loading with a supervisor that has not diagnostics enabled.""" + analytics = Analytics(hass) + analytics._data[ATTR_PREFERENCES][ATTR_DIAGNOSTICS] = True + + assert analytics.preferences[ATTR_DIAGNOSTICS] + + with patch( + "homeassistant.components.hassio.get_supervisor_info", + side_effect=Mock(return_value={"diagnostics": False}), + ), patch( + "homeassistant.components.hassio.is_hassio", + side_effect=Mock(return_value=True), + ): + await analytics.load() + + assert not analytics.preferences[ATTR_DIAGNOSTICS] + + +async def test_failed_to_send(hass, caplog, aioclient_mock): + """Test failed to send payload.""" + aioclient_mock.post(ANALYTICS_ENDPOINT_URL, status=400) + analytics = Analytics(hass) + await analytics.save_preferences({ATTR_BASE: True}) + assert analytics.preferences[ATTR_BASE] + + with patch("homeassistant.helpers.instance_id.async_get", return_value=MOCK_HUUID): + await analytics.send_analytics() + assert "Sending analytics failed with statuscode 400" in caplog.text + + +async def test_failed_to_send_raises(hass, caplog, aioclient_mock): + """Test raises when failed to send payload.""" + aioclient_mock.post(ANALYTICS_ENDPOINT_URL, exc=aiohttp.ClientError()) + analytics = Analytics(hass) + await analytics.save_preferences({ATTR_BASE: True}) + assert analytics.preferences[ATTR_BASE] + + with patch("homeassistant.helpers.instance_id.async_get", return_value=MOCK_HUUID): + await analytics.send_analytics() + assert "Error sending analytics" in caplog.text + + +async def test_send_base(hass, caplog, aioclient_mock): + """Test send base prefrences are defined.""" + aioclient_mock.post(ANALYTICS_ENDPOINT_URL, status=200) + analytics = Analytics(hass) + await analytics.save_preferences({ATTR_BASE: True}) + assert analytics.preferences[ATTR_BASE] + + with patch("homeassistant.helpers.instance_id.async_get", return_value=MOCK_HUUID): + await analytics.send_analytics() + assert f"'huuid': '{MOCK_HUUID}'" in caplog.text + assert f"'version': '{HA_VERSION}'" in caplog.text + assert "'installation_type':" in caplog.text + assert "'integration_count':" not in caplog.text + assert "'integrations':" not in caplog.text + + +async def test_send_base_with_supervisor(hass, caplog, aioclient_mock): + """Test send base prefrences are defined.""" + aioclient_mock.post(ANALYTICS_ENDPOINT_URL, status=200) + + analytics = Analytics(hass) + await analytics.save_preferences({ATTR_BASE: True}) + assert analytics.preferences[ATTR_BASE] + + with patch( + "homeassistant.components.hassio.get_supervisor_info", + side_effect=Mock(return_value={"supported": True, "healthy": True}), + ), patch( + "homeassistant.components.hassio.get_info", + side_effect=Mock(return_value={}), + ), patch( + "homeassistant.components.hassio.get_host_info", + side_effect=Mock(return_value={}), + ), patch( + "homeassistant.components.hassio.is_hassio", + side_effect=Mock(return_value=True), + ), patch( + "homeassistant.helpers.instance_id.async_get", return_value=MOCK_HUUID + ): + await analytics.send_analytics() + assert f"'huuid': '{MOCK_HUUID}'" in caplog.text + assert f"'version': '{HA_VERSION}'" in caplog.text + assert "'supervisor': {'healthy': True, 'supported': True}}" in caplog.text + assert "'installation_type':" in caplog.text + assert "'integration_count':" not in caplog.text + assert "'integrations':" not in caplog.text + + +async def test_send_usage(hass, caplog, aioclient_mock): + """Test send usage prefrences are defined.""" + aioclient_mock.post(ANALYTICS_ENDPOINT_URL, status=200) + analytics = Analytics(hass) + await analytics.save_preferences({ATTR_BASE: True, ATTR_USAGE: True}) + assert analytics.preferences[ATTR_BASE] + assert analytics.preferences[ATTR_USAGE] + hass.config.components = ["default_config"] + + with patch("homeassistant.helpers.instance_id.async_get", return_value=MOCK_HUUID): + await analytics.send_analytics() + assert "'integrations': ['default_config']" in caplog.text + assert "'integration_count':" not in caplog.text + + +async def test_send_usage_with_supervisor(hass, caplog, aioclient_mock): + """Test send usage with supervisor prefrences are defined.""" + aioclient_mock.post(ANALYTICS_ENDPOINT_URL, status=200) + + analytics = Analytics(hass) + await analytics.save_preferences({ATTR_BASE: True, ATTR_USAGE: True}) + assert analytics.preferences[ATTR_BASE] + assert analytics.preferences[ATTR_USAGE] + hass.config.components = ["default_config"] + + with patch( + "homeassistant.components.hassio.get_supervisor_info", + side_effect=Mock( + return_value={ + "healthy": True, + "supported": True, + "addons": [{"slug": "test_addon"}], + } + ), + ), patch( + "homeassistant.components.hassio.get_info", + side_effect=Mock(return_value={}), + ), patch( + "homeassistant.components.hassio.get_host_info", + side_effect=Mock(return_value={}), + ), patch( + "homeassistant.components.hassio.async_get_addon_info", + side_effect=AsyncMock( + return_value={ + "slug": "test_addon", + "protected": True, + "version": "1", + "auto_update": False, + } + ), + ), patch( + "homeassistant.components.hassio.is_hassio", + side_effect=Mock(return_value=True), + ), patch( + "homeassistant.helpers.instance_id.async_get", return_value=MOCK_HUUID + ): + await analytics.send_analytics() + assert ( + "'addons': [{'slug': 'test_addon', 'protected': True, 'version': '1', 'auto_update': False}]" + in caplog.text + ) + assert "'addon_count':" not in caplog.text + + +async def test_send_statistics(hass, caplog, aioclient_mock): + """Test send statistics prefrences are defined.""" + aioclient_mock.post(ANALYTICS_ENDPOINT_URL, status=200) + analytics = Analytics(hass) + await analytics.save_preferences({ATTR_BASE: True, ATTR_STATISTICS: True}) + assert analytics.preferences[ATTR_BASE] + assert analytics.preferences[ATTR_STATISTICS] + hass.config.components = ["default_config"] + + with patch("homeassistant.helpers.instance_id.async_get", return_value=MOCK_HUUID): + await analytics.send_analytics() + assert ( + "'state_count': 0, 'automation_count': 0, 'integration_count': 1, 'user_count': 0" + in caplog.text + ) + assert "'integrations':" not in caplog.text + + +async def test_send_statistics_with_supervisor(hass, caplog, aioclient_mock): + """Test send statistics prefrences are defined.""" + aioclient_mock.post(ANALYTICS_ENDPOINT_URL, status=200) + analytics = Analytics(hass) + await analytics.save_preferences({ATTR_BASE: True, ATTR_STATISTICS: True}) + assert analytics.preferences[ATTR_BASE] + assert analytics.preferences[ATTR_STATISTICS] + + with patch( + "homeassistant.components.hassio.get_supervisor_info", + side_effect=Mock( + return_value={ + "healthy": True, + "supported": True, + "addons": [{"slug": "test_addon"}], + } + ), + ), patch( + "homeassistant.components.hassio.get_info", + side_effect=Mock(return_value={}), + ), patch( + "homeassistant.components.hassio.get_host_info", + side_effect=Mock(return_value={}), + ), patch( + "homeassistant.components.hassio.async_get_addon_info", + side_effect=AsyncMock( + return_value={ + "slug": "test_addon", + "protected": True, + "version": "1", + "auto_update": False, + } + ), + ), patch( + "homeassistant.components.hassio.is_hassio", + side_effect=Mock(return_value=True), + ), patch( + "homeassistant.helpers.instance_id.async_get", return_value=MOCK_HUUID + ): + await analytics.send_analytics() + assert "'addon_count': 1" in caplog.text + assert "'integrations':" not in caplog.text diff --git a/tests/components/analytics/test_init.py b/tests/components/analytics/test_init.py new file mode 100644 index 00000000000..4f8c95bc6b4 --- /dev/null +++ b/tests/components/analytics/test_init.py @@ -0,0 +1,42 @@ +"""The tests for the analytics .""" +from unittest.mock import patch + +from homeassistant.components.analytics.const import ANALYTICS_ENDPOINT_URL, DOMAIN +from homeassistant.setup import async_setup_component + + +async def test_setup(hass): + """Test setup of the integration.""" + assert await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) + await hass.async_block_till_done() + + assert DOMAIN in hass.data + + +async def test_websocket(hass, hass_ws_client, aioclient_mock): + """Test websocekt commands.""" + aioclient_mock.post(ANALYTICS_ENDPOINT_URL, status=200) + assert await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) + await hass.async_block_till_done() + + ws_client = await hass_ws_client(hass) + await ws_client.send_json({"id": 1, "type": "analytics"}) + + with patch("homeassistant.helpers.instance_id.async_get", return_value="abcdef"): + response = await ws_client.receive_json() + + assert response["success"] + assert response["result"]["huuid"] == "abcdef" + + await ws_client.send_json( + {"id": 2, "type": "analytics/preferences", "preferences": {"base": True}} + ) + response = await ws_client.receive_json() + assert len(aioclient_mock.mock_calls) == 1 + assert response["result"]["preferences"]["base"] + + await ws_client.send_json({"id": 3, "type": "analytics"}) + with patch("homeassistant.helpers.instance_id.async_get", return_value="abcdef"): + response = await ws_client.receive_json() + assert response["result"]["preferences"]["base"] + assert response["result"]["huuid"] == "abcdef" diff --git a/tests/components/hassio/conftest.py b/tests/components/hassio/conftest.py index 1442d133f1e..efa983c3440 100644 --- a/tests/components/hassio/conftest.py +++ b/tests/components/hassio/conftest.py @@ -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()), ): diff --git a/tests/components/onboarding/test_views.py b/tests/components/onboarding/test_views.py index 558cab7ee99..d8ae50b851f 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,22 @@ 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"] + + resp = await client.post("/api/onboarding/analytics") + assert resp.status == 403