mirror of
https://github.com/home-assistant/core.git
synced 2026-02-15 07:36:16 +00:00
Enable snapshot analytics as labs feature (#160068)
Co-authored-by: Steven Travers <steven.travers20@gmail.com> Co-authored-by: Norbert Rittel <norbert@rittel.de> Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
This commit is contained in:
@@ -4,7 +4,7 @@ from typing import Any
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components import websocket_api
|
||||
from homeassistant.components import labs, websocket_api
|
||||
from homeassistant.const import EVENT_HOMEASSISTANT_STARTED
|
||||
from homeassistant.core import Event, HomeAssistant, callback
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
@@ -18,7 +18,13 @@ from .analytics import (
|
||||
EntityAnalyticsModifications,
|
||||
async_devices_payload,
|
||||
)
|
||||
from .const import ATTR_ONBOARDED, ATTR_PREFERENCES, DOMAIN, PREFERENCE_SCHEMA
|
||||
from .const import (
|
||||
ATTR_ONBOARDED,
|
||||
ATTR_PREFERENCES,
|
||||
ATTR_SNAPSHOTS,
|
||||
DOMAIN,
|
||||
PREFERENCE_SCHEMA,
|
||||
)
|
||||
from .http import AnalyticsDevicesView
|
||||
|
||||
__all__ = [
|
||||
@@ -44,29 +50,55 @@ CONFIG_SCHEMA = vol.Schema(
|
||||
|
||||
DATA_COMPONENT: HassKey[Analytics] = HassKey(DOMAIN)
|
||||
|
||||
LABS_SNAPSHOT_FEATURE = "snapshots"
|
||||
|
||||
|
||||
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
"""Set up the analytics integration."""
|
||||
analytics_config = config.get(DOMAIN, {})
|
||||
|
||||
# For now we want to enable device analytics only if the url option
|
||||
# is explicitly listed in YAML.
|
||||
if CONF_SNAPSHOTS_URL in analytics_config:
|
||||
disable_snapshots = False
|
||||
await labs.async_update_preview_feature(
|
||||
hass, DOMAIN, LABS_SNAPSHOT_FEATURE, enabled=True
|
||||
)
|
||||
snapshots_url = analytics_config[CONF_SNAPSHOTS_URL]
|
||||
else:
|
||||
disable_snapshots = True
|
||||
snapshots_url = None
|
||||
|
||||
analytics = Analytics(hass, snapshots_url, disable_snapshots)
|
||||
analytics = Analytics(hass, snapshots_url)
|
||||
|
||||
# Load stored data
|
||||
await analytics.load()
|
||||
|
||||
started = False
|
||||
|
||||
async def _async_handle_labs_update(
|
||||
event: Event[labs.EventLabsUpdatedData],
|
||||
) -> None:
|
||||
"""Handle labs feature toggle."""
|
||||
await analytics.save_preferences({ATTR_SNAPSHOTS: event.data["enabled"]})
|
||||
if started:
|
||||
await analytics.async_schedule()
|
||||
|
||||
@callback
|
||||
def _async_labs_event_filter(event_data: labs.EventLabsUpdatedData) -> bool:
|
||||
"""Filter labs events for this integration's snapshot feature."""
|
||||
return (
|
||||
event_data["domain"] == DOMAIN
|
||||
and event_data["preview_feature"] == LABS_SNAPSHOT_FEATURE
|
||||
)
|
||||
|
||||
async def start_schedule(_event: Event) -> None:
|
||||
"""Start the send schedule after the started event."""
|
||||
nonlocal started
|
||||
started = True
|
||||
await analytics.async_schedule()
|
||||
|
||||
hass.bus.async_listen(
|
||||
labs.EVENT_LABS_UPDATED,
|
||||
_async_handle_labs_update,
|
||||
event_filter=_async_labs_event_filter,
|
||||
)
|
||||
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STARTED, start_schedule)
|
||||
|
||||
websocket_api.async_register_command(hass, websocket_analytics)
|
||||
|
||||
@@ -22,6 +22,7 @@ from homeassistant.components.energy import (
|
||||
DOMAIN as ENERGY_DOMAIN,
|
||||
is_configured as energy_is_configured,
|
||||
)
|
||||
from homeassistant.components.labs import async_is_preview_feature_enabled
|
||||
from homeassistant.components.recorder import (
|
||||
DOMAIN as RECORDER_DOMAIN,
|
||||
get_instance as get_recorder_instance,
|
||||
@@ -241,12 +242,10 @@ class Analytics:
|
||||
self,
|
||||
hass: HomeAssistant,
|
||||
snapshots_url: str | None = None,
|
||||
disable_snapshots: bool = False,
|
||||
) -> None:
|
||||
"""Initialize the Analytics class."""
|
||||
self._hass: HomeAssistant = hass
|
||||
self._snapshots_url = snapshots_url
|
||||
self._disable_snapshots = disable_snapshots
|
||||
|
||||
self._session = async_get_clientsession(hass)
|
||||
self._data = AnalyticsData(False, {})
|
||||
@@ -258,15 +257,13 @@ class Analytics:
|
||||
def preferences(self) -> dict:
|
||||
"""Return the current active preferences."""
|
||||
preferences = self._data.preferences
|
||||
result = {
|
||||
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),
|
||||
ATTR_SNAPSHOTS: preferences.get(ATTR_SNAPSHOTS, False),
|
||||
}
|
||||
if not self._disable_snapshots:
|
||||
result[ATTR_SNAPSHOTS] = preferences.get(ATTR_SNAPSHOTS, False)
|
||||
return result
|
||||
|
||||
@property
|
||||
def onboarded(self) -> bool:
|
||||
@@ -291,6 +288,11 @@ class Analytics:
|
||||
"""Return bool if a supervisor is present."""
|
||||
return is_hassio(self._hass)
|
||||
|
||||
@property
|
||||
def _snapshots_enabled(self) -> bool:
|
||||
"""Check if snapshots feature is enabled via labs."""
|
||||
return async_is_preview_feature_enabled(self._hass, DOMAIN, "snapshots")
|
||||
|
||||
async def load(self) -> None:
|
||||
"""Load preferences."""
|
||||
stored = await self._store.async_load()
|
||||
@@ -645,7 +647,10 @@ class Analytics:
|
||||
),
|
||||
)
|
||||
|
||||
if not self.preferences.get(ATTR_SNAPSHOTS, False) or self._disable_snapshots:
|
||||
if (
|
||||
not self.preferences.get(ATTR_SNAPSHOTS, False)
|
||||
or not self._snapshots_enabled
|
||||
):
|
||||
LOGGER.debug("Snapshot analytics not scheduled")
|
||||
if self._snapshot_scheduled:
|
||||
self._snapshot_scheduled()
|
||||
|
||||
@@ -7,5 +7,11 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/analytics",
|
||||
"integration_type": "system",
|
||||
"iot_class": "cloud_push",
|
||||
"preview_features": {
|
||||
"snapshots": {
|
||||
"feedback_url": "https://forms.gle/GqvRmgmghSDco8M46",
|
||||
"report_issue_url": "https://github.com/OHF-Device-Database/device-database/issues/new"
|
||||
}
|
||||
},
|
||||
"quality_scale": "internal"
|
||||
}
|
||||
|
||||
10
homeassistant/components/analytics/strings.json
Normal file
10
homeassistant/components/analytics/strings.json
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"preview_features": {
|
||||
"snapshots": {
|
||||
"description": "This free, open source device database of the Open Home Foundation helps users find useful information about smart home devices used in real installations.\n\nYou can help build it by anonymously sharing data about your devices. Only device-specific details (like model or manufacturer) are shared — never personally identifying information (like the names you assign).\n\nLearn more about the device database and how we process your data in our [Data Use Statement](https://www.openhomefoundation.org/device-database-data-use-statement), which you accept by opting in.",
|
||||
"disable_confirmation": "Your data will no longer be shared with the Open Home Foundation's device database.",
|
||||
"enable_confirmation": "This feature is still in development and may change. The device database is being refined based on user feedback and is not yet complete.",
|
||||
"name": "Device database"
|
||||
}
|
||||
}
|
||||
}
|
||||
7
homeassistant/generated/labs.py
generated
7
homeassistant/generated/labs.py
generated
@@ -4,6 +4,13 @@ To update, run python3 -m script.hassfest
|
||||
"""
|
||||
|
||||
LABS_PREVIEW_FEATURES = {
|
||||
"analytics": {
|
||||
"snapshots": {
|
||||
"feedback_url": "https://forms.gle/GqvRmgmghSDco8M46",
|
||||
"learn_more_url": "",
|
||||
"report_issue_url": "https://github.com/OHF-Device-Database/device-database/issues/new",
|
||||
},
|
||||
},
|
||||
"automation": {
|
||||
"new_triggers_conditions": {
|
||||
"feedback_url": "https://forms.gle/fWFZqf5MzuwWTsCH8",
|
||||
|
||||
@@ -111,6 +111,16 @@ def installation_type_mock() -> Generator[None]:
|
||||
yield
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def labs_snapshots_enabled() -> Generator[None]:
|
||||
"""Mock the labs feature to enable snapshots."""
|
||||
with patch(
|
||||
"homeassistant.components.analytics.analytics.async_is_preview_feature_enabled",
|
||||
return_value=True,
|
||||
):
|
||||
yield
|
||||
|
||||
|
||||
def _last_call_payload(aioclient: AiohttpClientMocker) -> dict[str, Any]:
|
||||
"""Return the payload of the last call."""
|
||||
return aioclient.mock_calls[-1][2]
|
||||
@@ -1454,6 +1464,7 @@ async def test_analytics_platforms(
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("labs_snapshots_enabled")
|
||||
async def test_send_snapshot_disabled(
|
||||
hass: HomeAssistant,
|
||||
aioclient_mock: AiohttpClientMocker,
|
||||
@@ -1469,6 +1480,7 @@ async def test_send_snapshot_disabled(
|
||||
assert len(aioclient_mock.mock_calls) == 0
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("labs_snapshots_enabled")
|
||||
async def test_send_snapshot_success(
|
||||
hass: HomeAssistant,
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
@@ -1493,6 +1505,7 @@ async def test_send_snapshot_success(
|
||||
assert "Submitted snapshot analytics to Home Assistant servers" in caplog.text
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("labs_snapshots_enabled")
|
||||
async def test_send_snapshot_with_existing_identifier(
|
||||
hass: HomeAssistant,
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
@@ -1528,6 +1541,7 @@ async def test_send_snapshot_with_existing_identifier(
|
||||
assert "Submitted snapshot analytics to Home Assistant servers" in caplog.text
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("labs_snapshots_enabled")
|
||||
async def test_send_snapshot_invalid_identifier(
|
||||
hass: HomeAssistant,
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
@@ -1564,6 +1578,7 @@ async def test_send_snapshot_invalid_identifier(
|
||||
assert "Invalid submission identifier" in caplog.text
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("labs_snapshots_enabled")
|
||||
@pytest.mark.parametrize(
|
||||
("post_kwargs", "expected_log"),
|
||||
[
|
||||
@@ -1628,6 +1643,7 @@ async def test_send_snapshot_error(
|
||||
assert expected_log in caplog.text
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("labs_snapshots_enabled")
|
||||
async def test_async_schedule(
|
||||
hass: HomeAssistant,
|
||||
aioclient_mock: AiohttpClientMocker,
|
||||
@@ -1664,6 +1680,7 @@ async def test_async_schedule(
|
||||
assert 0 <= preferences["snapshot_submission_time"] <= 86400
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("labs_snapshots_enabled")
|
||||
async def test_async_schedule_disabled(
|
||||
hass: HomeAssistant,
|
||||
aioclient_mock: AiohttpClientMocker,
|
||||
@@ -1688,6 +1705,7 @@ async def test_async_schedule_disabled(
|
||||
assert len(aioclient_mock.mock_calls) == 0
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("labs_snapshots_enabled")
|
||||
async def test_async_schedule_already_scheduled(
|
||||
hass: HomeAssistant,
|
||||
aioclient_mock: AiohttpClientMocker,
|
||||
@@ -1721,6 +1739,7 @@ async def test_async_schedule_already_scheduled(
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("labs_snapshots_enabled")
|
||||
@pytest.mark.parametrize(("onboarded"), [True, False])
|
||||
async def test_async_schedule_cancel_when_disabled(
|
||||
hass: HomeAssistant,
|
||||
@@ -1759,6 +1778,7 @@ async def test_async_schedule_cancel_when_disabled(
|
||||
assert len(aioclient_mock.mock_calls) == 0
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("labs_snapshots_enabled")
|
||||
async def test_async_schedule_snapshots_url(
|
||||
hass: HomeAssistant,
|
||||
aioclient_mock: AiohttpClientMocker,
|
||||
@@ -1787,29 +1807,3 @@ async def test_async_schedule_snapshots_url(
|
||||
|
||||
assert len(aioclient_mock.mock_calls) == 1
|
||||
assert str(aioclient_mock.mock_calls[0][1]) == endpoint
|
||||
|
||||
|
||||
async def test_async_schedule_snapshots_disabled(
|
||||
hass: HomeAssistant,
|
||||
aioclient_mock: AiohttpClientMocker,
|
||||
) -> None:
|
||||
"""Test that snapshots are disabled when configured."""
|
||||
aioclient_mock.post(SNAPSHOT_ENDPOINT_URL, status=200, json={})
|
||||
|
||||
analytics = Analytics(hass, disable_snapshots=True)
|
||||
with patch(
|
||||
"homeassistant.helpers.storage.Store.async_load",
|
||||
return_value={
|
||||
"onboarded": True,
|
||||
"preferences": {ATTR_BASE: False, ATTR_SNAPSHOTS: True},
|
||||
"uuid": "12345",
|
||||
},
|
||||
):
|
||||
await analytics.load()
|
||||
|
||||
await analytics.async_schedule()
|
||||
|
||||
async_fire_time_changed(hass, dt_util.utcnow() + timedelta(hours=25))
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert len(aioclient_mock.mock_calls) == 0
|
||||
|
||||
@@ -1,18 +1,33 @@
|
||||
"""The tests for the analytics ."""
|
||||
|
||||
from datetime import timedelta
|
||||
from typing import Any
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
|
||||
from homeassistant.components.analytics.const import BASIC_ENDPOINT_URL, DOMAIN
|
||||
from homeassistant.components.analytics import LABS_SNAPSHOT_FEATURE
|
||||
from homeassistant.components.analytics.const import (
|
||||
BASIC_ENDPOINT_URL,
|
||||
DOMAIN,
|
||||
SNAPSHOT_DEFAULT_URL,
|
||||
SNAPSHOT_URL_PATH,
|
||||
STORAGE_KEY,
|
||||
)
|
||||
from homeassistant.components.labs import async_update_preview_feature
|
||||
from homeassistant.const import EVENT_HOMEASSISTANT_STARTED
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.setup import async_setup_component
|
||||
from homeassistant.util import dt as dt_util
|
||||
|
||||
from tests.common import async_fire_time_changed
|
||||
from tests.test_util.aiohttp import AiohttpClientMocker
|
||||
from tests.typing import WebSocketGenerator
|
||||
|
||||
MOCK_VERSION = "1970.1.0"
|
||||
|
||||
SNAPSHOT_ENDPOINT_URL = SNAPSHOT_DEFAULT_URL + SNAPSHOT_URL_PATH
|
||||
|
||||
|
||||
async def test_setup(hass: HomeAssistant) -> None:
|
||||
"""Test setup of the integration."""
|
||||
@@ -22,6 +37,48 @@ async def test_setup(hass: HomeAssistant) -> None:
|
||||
assert DOMAIN in hass.data
|
||||
|
||||
|
||||
async def test_labs_feature_toggle(
|
||||
hass: HomeAssistant,
|
||||
hass_storage: dict[str, Any],
|
||||
aioclient_mock: AiohttpClientMocker,
|
||||
) -> None:
|
||||
"""Test that snapshots can be toggled via labs feature."""
|
||||
aioclient_mock.post(SNAPSHOT_ENDPOINT_URL, status=200, json={})
|
||||
|
||||
assert await async_setup_component(hass, "labs", {})
|
||||
assert await async_setup_component(hass, DOMAIN, {DOMAIN: {}})
|
||||
|
||||
hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
async_fire_time_changed(hass, dt_util.utcnow() + timedelta(hours=25))
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert len(aioclient_mock.mock_calls) == 0
|
||||
|
||||
await async_update_preview_feature(hass, DOMAIN, LABS_SNAPSHOT_FEATURE, True)
|
||||
|
||||
assert hass_storage[STORAGE_KEY]["data"]["preferences"]["snapshots"] is True
|
||||
|
||||
async_fire_time_changed(hass, dt_util.utcnow() + timedelta(hours=25))
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert any(
|
||||
str(call[1]) == SNAPSHOT_ENDPOINT_URL for call in aioclient_mock.mock_calls
|
||||
)
|
||||
|
||||
aioclient_mock.clear_requests()
|
||||
|
||||
await async_update_preview_feature(hass, DOMAIN, LABS_SNAPSHOT_FEATURE, False)
|
||||
|
||||
assert hass_storage[STORAGE_KEY]["data"]["preferences"]["snapshots"] is False
|
||||
|
||||
async_fire_time_changed(hass, dt_util.utcnow() + timedelta(hours=25))
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert len(aioclient_mock.mock_calls) == 0
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("supervisor_client")
|
||||
async def test_websocket(
|
||||
hass: HomeAssistant,
|
||||
|
||||
Reference in New Issue
Block a user