-
-
Notifications
You must be signed in to change notification settings - Fork 37.4k
Add analytics integration #48256
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Add analytics integration #48256
Changes from all commits
Commits
Show all changes
16 commits
Select commit
Hold shift + click to select a range
284c4f0
Add analytics integration
ludeeus afc025e
Add hass_storage
ludeeus fe63a90
Review comments
ludeeus 1291d55
adjust
ludeeus b516394
Revert "adjust"
ludeeus f7454a8
Revert "Add hass_storage"
ludeeus 6fd4e7b
Adjust
ludeeus 9c671b6
adjust
ludeeus babd2fa
Add more tests
ludeeus f027163
adjust part 1
ludeeus dcfdd90
change tests
ludeeus 5a9f738
Fix missing aioclient_mock
ludeeus 41a1511
Update endpoint URL
ludeeus 322288c
Address comments
ludeeus 5ace01b
Update homeassistant/components/analytics/analytics.py
ludeeus c3c4010
Update endpoint URL
ludeeus File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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", | ||
|
ludeeus marked this conversation as resolved.
|
||
| 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}, | ||
| ) | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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) | ||
|
ludeeus marked this conversation as resolved.
|
||
|
|
||
| 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 | ||
| ) | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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, | ||
| } | ||
| ) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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" | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.