Files
core/tests/components/analytics/test_analytics.py
T

Ignoring revisions in .git-blame-ignore-revs. Click here to bypass and see the normal blame view.

1814 lines
58 KiB
Python
Raw Normal View History

2021-03-30 02:20:11 +02:00
"""The tests for the analytics ."""
2024-02-16 15:47:41 +01:00
from collections.abc import Generator
from datetime import timedelta
from http import HTTPStatus
from typing import Any
from unittest.mock import AsyncMock, Mock, patch
2021-03-30 02:20:11 +02:00
import aiohttp
2024-02-16 15:47:41 +01:00
from awesomeversion import AwesomeVersion
import pytest
from syrupy.assertion import SnapshotAssertion
2024-02-16 15:47:41 +01:00
from syrupy.matchers import path_type
2021-03-30 02:20:11 +02:00
from homeassistant.components.analytics.analytics import (
Analytics,
2025-09-23 15:16:37 +02:00
AnalyticsInput,
AnalyticsModifications,
DeviceAnalyticsModifications,
EntityAnalyticsModifications,
async_devices_payload,
)
2021-03-30 02:20:11 +02:00
from homeassistant.components.analytics.const import (
ATTR_BASE,
ATTR_DIAGNOSTICS,
ATTR_SNAPSHOTS,
2021-03-30 02:20:11 +02:00
ATTR_STATISTICS,
ATTR_USAGE,
BASIC_ENDPOINT_URL,
BASIC_ENDPOINT_URL_DEV,
SNAPSHOT_DEFAULT_URL,
SNAPSHOT_URL_PATH,
2021-03-30 02:20:11 +02:00
)
from homeassistant.components.number import NumberDeviceClass
from homeassistant.components.sensor import SensorDeviceClass
from homeassistant.config_entries import ConfigEntryDisabler, ConfigEntryState
from homeassistant.const import ATTR_ASSUMED_STATE, EntityCategory
from homeassistant.core import HomeAssistant, ReleaseChannel
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import device_registry as dr, entity_registry as er
from homeassistant.loader import IntegrationNotFound
from homeassistant.setup import async_setup_component
from homeassistant.util import dt as dt_util
2021-03-30 02:20:11 +02:00
from tests.common import (
MockConfigEntry,
MockModule,
async_fire_time_changed,
mock_integration,
mock_platform,
)
from tests.test_util.aiohttp import AiohttpClientMocker
from tests.typing import ClientSessionGenerator
SNAPSHOT_ENDPOINT_URL = SNAPSHOT_DEFAULT_URL + SNAPSHOT_URL_PATH
MOCK_UUID = "abcdefg"
MOCK_VERSION = "1970.1.0"
MOCK_VERSION_DEV = "1970.1.0.dev0"
MOCK_VERSION_NIGHTLY = "1970.1.0.dev19700101"
2021-03-30 02:20:11 +02:00
2024-02-16 15:47:41 +01:00
@pytest.fixture(autouse=True)
def uuid_mock() -> Generator[None]:
2024-02-16 15:47:41 +01:00
"""Mock the UUID."""
with patch(
"homeassistant.components.analytics.analytics.gen_uuid", return_value=MOCK_UUID
):
2024-02-16 15:47:41 +01:00
yield
@pytest.fixture(autouse=True)
def ha_version_mock() -> Generator[None]:
2024-02-16 15:47:41 +01:00
"""Mock the core version."""
with (
patch(
"homeassistant.components.analytics.analytics.HA_VERSION",
MOCK_VERSION,
),
patch(
"homeassistant.components.analytics.analytics.RELEASE_CHANNEL",
ReleaseChannel.STABLE,
),
):
yield
@pytest.fixture
def ha_dev_version_mock() -> Generator[None]:
"""Mock the core version as a dev version."""
with (
patch(
"homeassistant.components.analytics.analytics.HA_VERSION",
MOCK_VERSION_DEV,
),
patch(
"homeassistant.components.analytics.analytics.RELEASE_CHANNEL",
ReleaseChannel.DEV,
),
2024-02-16 15:47:41 +01:00
):
yield
@pytest.fixture
def installation_type_mock() -> Generator[None]:
2024-02-16 15:47:41 +01:00
"""Mock the async_get_system_info."""
with patch(
"homeassistant.components.analytics.analytics.async_get_system_info",
return_value={"installation_type": "Home Assistant Tests"},
):
yield
def _last_call_payload(aioclient: AiohttpClientMocker) -> dict[str, Any]:
"""Return the payload of the last call."""
return aioclient.mock_calls[-1][2]
@pytest.mark.usefixtures("supervisor_client")
async def test_no_send(
hass: HomeAssistant,
caplog: pytest.LogCaptureFixture,
aioclient_mock: AiohttpClientMocker,
) -> None:
2022-01-17 11:36:59 -05:00
"""Test send when no preferences are defined."""
2021-03-30 02:20:11 +02:00
analytics = Analytics(hass)
with patch(
"homeassistant.components.analytics.analytics.is_hassio",
2021-03-30 02:20:11 +02:00
side_effect=Mock(return_value=False),
):
2021-03-30 02:20:11 +02:00
assert not analytics.preferences[ATTR_BASE]
await analytics.send_analytics()
assert len(aioclient_mock.mock_calls) == 0
async def test_load_with_supervisor_diagnostics(hass: HomeAssistant) -> None:
2021-03-30 02:20:11 +02:00
"""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.analytics.analytics.is_hassio",
2021-03-30 02:20:11 +02:00
side_effect=Mock(return_value=True),
2024-03-26 00:02:16 +01:00
),
2021-03-30 02:20:11 +02:00
):
await analytics.load()
assert analytics.preferences[ATTR_DIAGNOSTICS]
async def test_load_with_supervisor_without_diagnostics(hass: HomeAssistant) -> None:
2021-03-30 02:20:11 +02:00
"""Test loading with a supervisor that has not diagnostics enabled."""
analytics = Analytics(hass)
2022-12-02 14:05:08 +01:00
analytics._data.preferences[ATTR_DIAGNOSTICS] = True
2021-03-30 02:20:11 +02:00
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.is_hassio",
2021-03-30 02:20:11 +02:00
side_effect=Mock(return_value=True),
2024-03-26 00:02:16 +01:00
),
2021-03-30 02:20:11 +02:00
):
await analytics.load()
assert not analytics.preferences[ATTR_DIAGNOSTICS]
@pytest.mark.usefixtures("supervisor_client")
async def test_failed_to_send(
hass: HomeAssistant,
caplog: pytest.LogCaptureFixture,
aioclient_mock: AiohttpClientMocker,
) -> None:
2021-03-30 02:20:11 +02:00
"""Test failed to send payload."""
aioclient_mock.post(BASIC_ENDPOINT_URL, status=400)
2021-03-30 02:20:11 +02:00
analytics = Analytics(hass)
await analytics.save_preferences({ATTR_BASE: True})
assert analytics.preferences[ATTR_BASE]
2024-02-16 15:47:41 +01:00
await analytics.send_analytics()
assert (
f"Sending analytics failed with statuscode 400 from {BASIC_ENDPOINT_URL}"
in caplog.text
)
2021-03-30 02:20:11 +02:00
@pytest.mark.usefixtures("supervisor_client")
async def test_failed_to_send_raises(
hass: HomeAssistant,
caplog: pytest.LogCaptureFixture,
aioclient_mock: AiohttpClientMocker,
) -> None:
2021-03-30 02:20:11 +02:00
"""Test raises when failed to send payload."""
aioclient_mock.post(BASIC_ENDPOINT_URL, exc=aiohttp.ClientError())
2021-03-30 02:20:11 +02:00
analytics = Analytics(hass)
await analytics.save_preferences({ATTR_BASE: True})
assert analytics.preferences[ATTR_BASE]
2024-02-16 15:47:41 +01:00
await analytics.send_analytics()
2021-03-30 02:20:11 +02:00
assert "Error sending analytics" in caplog.text
@pytest.mark.usefixtures("installation_type_mock", "supervisor_client")
async def test_send_base(
hass: HomeAssistant,
caplog: pytest.LogCaptureFixture,
aioclient_mock: AiohttpClientMocker,
2024-02-16 15:47:41 +01:00
snapshot: SnapshotAssertion,
) -> None:
2022-01-17 11:36:59 -05:00
"""Test send base preferences are defined."""
aioclient_mock.post(BASIC_ENDPOINT_URL, status=200)
2021-03-30 02:20:11 +02:00
analytics = Analytics(hass)
2021-03-30 02:20:11 +02:00
await analytics.save_preferences({ATTR_BASE: True})
assert analytics.preferences[ATTR_BASE]
2024-02-16 15:47:41 +01:00
await analytics.send_analytics()
2024-02-16 15:47:41 +01:00
logged_data = caplog.records[-1].args
submitted_data = _last_call_payload(aioclient_mock)
assert submitted_data == logged_data
assert snapshot == submitted_data
2021-03-30 02:20:11 +02:00
@pytest.mark.usefixtures("supervisor_client")
async def test_send_base_with_supervisor(
hass: HomeAssistant,
caplog: pytest.LogCaptureFixture,
aioclient_mock: AiohttpClientMocker,
2024-02-16 15:47:41 +01:00
snapshot: SnapshotAssertion,
) -> None:
2022-01-17 11:36:59 -05:00
"""Test send base preferences are defined."""
aioclient_mock.post(BASIC_ENDPOINT_URL, status=200)
2021-03-30 02:20:11 +02:00
analytics = Analytics(hass)
await analytics.save_preferences({ATTR_BASE: True})
assert analytics.preferences[ATTR_BASE]
with (
patch(
"homeassistant.components.hassio.get_supervisor_info",
2021-06-01 15:09:23 +02:00
side_effect=Mock(
return_value={"supported": True, "healthy": True, "arch": "amd64"}
2024-03-26 00:02:16 +01:00
),
),
patch(
"homeassistant.components.hassio.get_os_info",
side_effect=Mock(return_value={"board": "blue", "version": "123"}),
2024-03-26 00:02:16 +01:00
),
patch(
2021-03-30 02:20:11 +02:00
"homeassistant.components.hassio.get_info",
side_effect=Mock(return_value={}),
2024-03-26 00:02:16 +01:00
),
patch(
2021-03-30 02:20:11 +02:00
"homeassistant.components.hassio.get_host_info",
side_effect=Mock(return_value={}),
2024-03-26 00:02:16 +01:00
),
patch(
"homeassistant.components.analytics.analytics.is_hassio",
2021-03-30 02:20:11 +02:00
side_effect=Mock(return_value=True),
) as is_hassio_mock,
patch(
"homeassistant.helpers.system_info.is_hassio",
new=is_hassio_mock,
2021-06-01 15:09:23 +02:00
),
):
await analytics.load()
2021-03-30 02:20:11 +02:00
await analytics.send_analytics()
2024-02-16 15:47:41 +01:00
logged_data = caplog.records[-1].args
submitted_data = _last_call_payload(aioclient_mock)
assert submitted_data == logged_data
assert snapshot == submitted_data
2021-03-30 02:20:11 +02:00
@pytest.mark.usefixtures("installation_type_mock", "supervisor_client")
async def test_send_usage(
hass: HomeAssistant,
caplog: pytest.LogCaptureFixture,
aioclient_mock: AiohttpClientMocker,
2024-02-16 15:47:41 +01:00
snapshot: SnapshotAssertion,
) -> None:
2022-01-17 11:36:59 -05:00
"""Test send usage preferences are defined."""
aioclient_mock.post(BASIC_ENDPOINT_URL, status=200)
2021-03-30 02:20:11 +02:00
analytics = Analytics(hass)
hass.http = Mock(ssl_certificate=None)
2021-03-30 02:20:11 +02:00
await analytics.save_preferences({ATTR_BASE: True, ATTR_USAGE: True})
2021-03-30 02:20:11 +02:00
assert analytics.preferences[ATTR_BASE]
assert analytics.preferences[ATTR_USAGE]
hass.config.components.add("default_config")
2021-03-30 02:20:11 +02:00
with patch(
"homeassistant.config.load_yaml_config_file",
return_value={"default_config": {}},
):
await analytics.send_analytics()
assert (
"Submitted analytics to Home Assistant servers. Information submitted includes"
in caplog.text
)
2024-02-16 15:47:41 +01:00
logged_data = caplog.records[-1].args
submitted_data = _last_call_payload(aioclient_mock)
assert submitted_data == logged_data
assert snapshot == submitted_data
2021-03-30 02:20:11 +02:00
@pytest.mark.usefixtures("mock_hass_config")
async def test_send_usage_with_supervisor(
hass: HomeAssistant,
caplog: pytest.LogCaptureFixture,
aioclient_mock: AiohttpClientMocker,
2024-02-16 15:47:41 +01:00
snapshot: SnapshotAssertion,
supervisor_client: AsyncMock,
) -> None:
2022-01-17 11:36:59 -05:00
"""Test send usage with supervisor preferences are defined."""
aioclient_mock.post(BASIC_ENDPOINT_URL, status=200)
2021-03-30 02:20:11 +02:00
analytics = Analytics(hass)
hass.http = Mock(ssl_certificate=None)
2021-03-30 02:20:11 +02:00
await analytics.save_preferences({ATTR_BASE: True, ATTR_USAGE: True})
assert analytics.preferences[ATTR_BASE]
assert analytics.preferences[ATTR_USAGE]
hass.config.components.add("default_config")
2021-03-30 02:20:11 +02:00
supervisor_client.addons.addon_info.return_value = Mock(
slug="test_addon", protected=True, version="1", auto_update=False
)
2021-03-30 02:20:11 +02:00
with (
patch(
"homeassistant.components.hassio.get_supervisor_info",
side_effect=Mock(
return_value={
"healthy": True,
"supported": True,
2021-06-01 15:09:23 +02:00
"arch": "amd64",
2021-03-30 02:20:11 +02:00
"addons": [{"slug": "test_addon"}],
}
2024-03-26 00:02:16 +01:00
),
2021-03-30 02:20:11 +02:00
),
patch(
"homeassistant.components.hassio.get_os_info",
side_effect=Mock(return_value={}),
2021-03-30 02:20:11 +02:00
),
patch(
"homeassistant.components.hassio.get_info",
side_effect=Mock(return_value={}),
),
patch(
"homeassistant.components.hassio.get_host_info",
side_effect=Mock(return_value={}),
),
2024-03-26 00:02:16 +01:00
patch(
"homeassistant.components.analytics.analytics.is_hassio",
2021-03-30 02:20:11 +02:00
side_effect=Mock(return_value=True),
) as is_hassio_mock,
patch(
"homeassistant.helpers.system_info.is_hassio",
new=is_hassio_mock,
2021-03-30 02:20:11 +02:00
),
):
await analytics.send_analytics()
2024-02-16 15:47:41 +01:00
logged_data = caplog.records[-1].args
submitted_data = _last_call_payload(aioclient_mock)
assert submitted_data == logged_data
assert snapshot == submitted_data
2021-03-30 02:20:11 +02:00
@pytest.mark.usefixtures("installation_type_mock", "supervisor_client")
async def test_send_statistics(
hass: HomeAssistant,
caplog: pytest.LogCaptureFixture,
aioclient_mock: AiohttpClientMocker,
2024-02-16 15:47:41 +01:00
snapshot: SnapshotAssertion,
) -> None:
2022-01-17 11:36:59 -05:00
"""Test send statistics preferences are defined."""
aioclient_mock.post(BASIC_ENDPOINT_URL, status=200)
2021-03-30 02:20:11 +02:00
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.add("default_config")
2021-03-30 02:20:11 +02:00
with patch(
"homeassistant.config.load_yaml_config_file",
return_value={"default_config": {}},
):
await analytics.send_analytics()
2024-02-16 15:47:41 +01:00
logged_data = caplog.records[-1].args
submitted_data = _last_call_payload(aioclient_mock)
assert submitted_data == logged_data
assert snapshot == submitted_data
2021-03-30 02:20:11 +02:00
@pytest.mark.usefixtures("mock_hass_config", "supervisor_client")
async def test_send_statistics_one_integration_fails(
hass: HomeAssistant,
aioclient_mock: AiohttpClientMocker,
) -> None:
2022-01-17 11:36:59 -05:00
"""Test send statistics preferences are defined."""
aioclient_mock.post(BASIC_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.components.analytics.analytics.async_get_integrations",
return_value={"any": IntegrationNotFound("any")},
2024-02-16 15:47:41 +01:00
):
await analytics.send_analytics()
post_call = aioclient_mock.mock_calls[0]
assert "uuid" in post_call[2]
assert post_call[2]["integration_count"] == 0
@pytest.mark.usefixtures(
"installation_type_mock", "mock_hass_config", "supervisor_client"
)
async def test_send_statistics_disabled_integration(
hass: HomeAssistant,
caplog: pytest.LogCaptureFixture,
aioclient_mock: AiohttpClientMocker,
2024-02-16 15:47:41 +01:00
snapshot: SnapshotAssertion,
) -> None:
"""Test send statistics with disabled integration."""
aioclient_mock.post(BASIC_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.components.analytics.analytics.async_get_integrations",
return_value={
"disabled_integration_manifest": mock_integration(
hass,
MockModule(
"disabled_integration",
async_setup=AsyncMock(return_value=True),
partial_manifest={"disabled": "system"},
),
)
},
2024-02-16 15:47:41 +01:00
):
await analytics.send_analytics()
2024-02-16 15:47:41 +01:00
logged_data = caplog.records[-1].args
submitted_data = _last_call_payload(aioclient_mock)
assert submitted_data == logged_data
assert snapshot == submitted_data
@pytest.mark.usefixtures(
"installation_type_mock", "mock_hass_config", "supervisor_client"
)
async def test_send_statistics_ignored_integration(
hass: HomeAssistant,
caplog: pytest.LogCaptureFixture,
aioclient_mock: AiohttpClientMocker,
2024-02-16 15:47:41 +01:00
snapshot: SnapshotAssertion,
) -> None:
"""Test send statistics with ignored integration."""
aioclient_mock.post(BASIC_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]
mock_config_entry = MockConfigEntry(
domain="ignored_integration",
state=ConfigEntryState.LOADED,
source="ignore",
)
mock_config_entry.add_to_hass(hass)
with patch(
"homeassistant.components.analytics.analytics.async_get_integrations",
return_value={
"ignored_integration": mock_integration(
hass,
MockModule(
"ignored_integration",
async_setup=AsyncMock(return_value=True),
partial_manifest={"config_flow": True},
),
),
},
2024-02-16 15:47:41 +01:00
):
await analytics.send_analytics()
2024-02-16 15:47:41 +01:00
logged_data = caplog.records[-1].args
submitted_data = _last_call_payload(aioclient_mock)
assert submitted_data == logged_data
assert snapshot == submitted_data
@pytest.mark.usefixtures("mock_hass_config", "supervisor_client")
async def test_send_statistics_async_get_integration_unknown_exception(
hass: HomeAssistant,
aioclient_mock: AiohttpClientMocker,
) -> None:
2022-01-17 11:36:59 -05:00
"""Test send statistics preferences are defined."""
aioclient_mock.post(BASIC_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 (
pytest.raises(ValueError),
patch(
"homeassistant.components.analytics.analytics.async_get_integrations",
return_value={"any": ValueError()},
2024-03-26 00:02:16 +01:00
),
2024-02-16 15:47:41 +01:00
):
await analytics.send_analytics()
@pytest.mark.usefixtures("mock_hass_config")
async def test_send_statistics_with_supervisor(
hass: HomeAssistant,
caplog: pytest.LogCaptureFixture,
aioclient_mock: AiohttpClientMocker,
2024-02-16 15:47:41 +01:00
snapshot: SnapshotAssertion,
supervisor_client: AsyncMock,
) -> None:
2022-01-17 11:36:59 -05:00
"""Test send statistics preferences are defined."""
aioclient_mock.post(BASIC_ENDPOINT_URL, status=200)
2021-03-30 02:20:11 +02:00
analytics = Analytics(hass)
await analytics.save_preferences({ATTR_BASE: True, ATTR_STATISTICS: True})
assert analytics.preferences[ATTR_BASE]
assert analytics.preferences[ATTR_STATISTICS]
supervisor_client.addons.addon_info.return_value = Mock(
slug="test_addon", protected=True, version="1", auto_update=False
)
2021-03-30 02:20:11 +02:00
with (
patch(
"homeassistant.components.hassio.get_supervisor_info",
side_effect=Mock(
return_value={
"healthy": True,
"supported": True,
2021-06-01 15:09:23 +02:00
"arch": "amd64",
2021-03-30 02:20:11 +02:00
"addons": [{"slug": "test_addon"}],
}
2024-03-26 00:02:16 +01:00
),
2021-03-30 02:20:11 +02:00
),
patch(
"homeassistant.components.hassio.get_os_info",
side_effect=Mock(return_value={}),
2021-03-30 02:20:11 +02:00
),
patch(
"homeassistant.components.hassio.get_info",
side_effect=Mock(return_value={}),
),
patch(
"homeassistant.components.hassio.get_host_info",
side_effect=Mock(return_value={}),
),
2024-03-26 00:02:16 +01:00
patch(
"homeassistant.components.analytics.analytics.is_hassio",
2021-03-30 02:20:11 +02:00
side_effect=Mock(return_value=True),
) as is_hassio_mock,
patch(
"homeassistant.helpers.system_info.is_hassio",
new=is_hassio_mock,
2021-03-30 02:20:11 +02:00
),
):
await analytics.send_analytics()
2024-02-16 15:47:41 +01:00
logged_data = caplog.records[-1].args
submitted_data = _last_call_payload(aioclient_mock)
assert submitted_data == logged_data
assert snapshot == submitted_data
@pytest.mark.usefixtures("supervisor_client")
async def test_reusing_uuid(
2024-02-16 15:47:41 +01:00
hass: HomeAssistant,
aioclient_mock: AiohttpClientMocker,
) -> None:
"""Test reusing the stored UUID."""
aioclient_mock.post(BASIC_ENDPOINT_URL, status=200)
analytics = Analytics(hass)
2022-12-02 14:05:08 +01:00
analytics._data.uuid = "NOT_MOCK_UUID"
await analytics.save_preferences({ATTR_BASE: True})
2024-02-16 15:47:41 +01:00
# This is not actually called but that in itself prove the test
await analytics.send_analytics()
assert analytics.uuid == "NOT_MOCK_UUID"
@pytest.mark.usefixtures(
"enable_custom_integrations", "installation_type_mock", "supervisor_client"
)
async def test_custom_integrations(
hass: HomeAssistant,
aioclient_mock: AiohttpClientMocker,
2024-02-16 15:47:41 +01:00
caplog: pytest.LogCaptureFixture,
snapshot: SnapshotAssertion,
) -> None:
"""Test sending custom integrations."""
aioclient_mock.post(BASIC_ENDPOINT_URL, status=200)
analytics = Analytics(hass)
hass.http = Mock(ssl_certificate=None)
assert await async_setup_component(hass, "test_package", {"test_package": {}})
await analytics.save_preferences({ATTR_BASE: True, ATTR_USAGE: True})
with patch(
"homeassistant.config.load_yaml_config_file",
return_value={"test_package": {}},
):
await analytics.send_analytics()
2024-02-16 15:47:41 +01:00
logged_data = caplog.records[-1].args
submitted_data = _last_call_payload(aioclient_mock)
assert submitted_data == logged_data
assert snapshot == submitted_data
@pytest.mark.usefixtures("ha_dev_version_mock", "supervisor_client")
async def test_dev_url(
hass: HomeAssistant,
aioclient_mock: AiohttpClientMocker,
) -> None:
"""Test sending payload to dev url."""
aioclient_mock.post(BASIC_ENDPOINT_URL_DEV, status=200)
analytics = Analytics(hass)
await analytics.save_preferences({ATTR_BASE: True})
await analytics.send_analytics()
payload = aioclient_mock.mock_calls[0]
assert str(payload[1]) == BASIC_ENDPOINT_URL_DEV
@pytest.mark.usefixtures("ha_dev_version_mock", "supervisor_client")
async def test_dev_url_error(
hass: HomeAssistant,
aioclient_mock: AiohttpClientMocker,
caplog: pytest.LogCaptureFixture,
) -> None:
"""Test sending payload to dev url that returns error."""
aioclient_mock.post(BASIC_ENDPOINT_URL_DEV, status=400)
analytics = Analytics(hass)
await analytics.save_preferences({ATTR_BASE: True})
await analytics.send_analytics()
payload = aioclient_mock.mock_calls[0]
assert str(payload[1]) == BASIC_ENDPOINT_URL_DEV
assert (
f"Sending analytics failed with statuscode 400 from {BASIC_ENDPOINT_URL_DEV}"
) in caplog.text
@pytest.mark.usefixtures("supervisor_client")
async def test_nightly_endpoint(
hass: HomeAssistant,
aioclient_mock: AiohttpClientMocker,
) -> None:
"""Test sending payload to production url when running nightly."""
aioclient_mock.post(BASIC_ENDPOINT_URL, status=200)
analytics = Analytics(hass)
await analytics.save_preferences({ATTR_BASE: True})
with patch(
"homeassistant.components.analytics.analytics.HA_VERSION", MOCK_VERSION_NIGHTLY
):
await analytics.send_analytics()
payload = aioclient_mock.mock_calls[0]
assert str(payload[1]) == BASIC_ENDPOINT_URL
2021-08-02 18:46:07 +02:00
@pytest.mark.usefixtures(
"installation_type_mock", "mock_hass_config", "supervisor_client"
)
async def test_send_with_no_energy(
hass: HomeAssistant,
aioclient_mock: AiohttpClientMocker,
2024-02-16 15:47:41 +01:00
caplog: pytest.LogCaptureFixture,
snapshot: SnapshotAssertion,
) -> None:
2022-01-17 11:36:59 -05:00
"""Test send base preferences are defined."""
aioclient_mock.post(BASIC_ENDPOINT_URL, status=200)
2021-08-02 18:46:07 +02:00
analytics = Analytics(hass)
hass.http = Mock(ssl_certificate=None)
2021-08-02 18:46:07 +02:00
await analytics.save_preferences({ATTR_BASE: True, ATTR_USAGE: True})
2024-02-16 15:47:41 +01:00
with (
patch(
2021-08-02 18:46:07 +02:00
"homeassistant.components.analytics.analytics.energy_is_configured",
AsyncMock(),
2024-02-16 15:47:41 +01:00
) as energy_is_configured,
patch(
"homeassistant.components.analytics.analytics.get_recorder_instance",
Mock(),
) as get_recorder_instance,
):
2021-08-02 18:46:07 +02:00
energy_is_configured.return_value = False
2024-02-16 15:47:41 +01:00
get_recorder_instance.return_value = Mock(database_engine=Mock())
2021-08-02 18:46:07 +02:00
await analytics.send_analytics()
2024-02-16 15:47:41 +01:00
logged_data = caplog.records[-1].args
submitted_data = _last_call_payload(aioclient_mock)
2021-08-02 18:46:07 +02:00
2024-02-16 15:47:41 +01:00
assert "energy" not in submitted_data
assert submitted_data == logged_data
assert snapshot == submitted_data
2021-08-02 18:46:07 +02:00
@pytest.mark.usefixtures(
"recorder_mock", "installation_type_mock", "mock_hass_config", "supervisor_client"
)
async def test_send_with_no_energy_config(
hass: HomeAssistant,
aioclient_mock: AiohttpClientMocker,
2024-02-16 15:47:41 +01:00
caplog: pytest.LogCaptureFixture,
snapshot: SnapshotAssertion,
) -> None:
2022-01-17 11:36:59 -05:00
"""Test send base preferences are defined."""
aioclient_mock.post(BASIC_ENDPOINT_URL, status=200)
2021-08-02 18:46:07 +02:00
analytics = Analytics(hass)
await analytics.save_preferences({ATTR_BASE: True, ATTR_USAGE: True})
assert await async_setup_component(hass, "energy", {})
2021-08-02 18:46:07 +02:00
2024-02-16 15:47:41 +01:00
with patch(
2021-08-02 18:46:07 +02:00
"homeassistant.components.analytics.analytics.energy_is_configured", AsyncMock()
) as energy_is_configured:
energy_is_configured.return_value = False
await analytics.send_analytics()
2024-02-16 15:47:41 +01:00
logged_data = caplog.records[-1].args
submitted_data = _last_call_payload(aioclient_mock)
2021-08-02 18:46:07 +02:00
2024-02-16 15:47:41 +01:00
assert submitted_data["energy"]["configured"] is False
assert submitted_data == logged_data
assert (
snapshot(matcher=path_type({"recorder.version": (AwesomeVersion,)}))
== submitted_data
)
2021-08-02 18:46:07 +02:00
@pytest.mark.usefixtures(
"recorder_mock", "installation_type_mock", "mock_hass_config", "supervisor_client"
)
async def test_send_with_energy_config(
hass: HomeAssistant,
aioclient_mock: AiohttpClientMocker,
2024-02-16 15:47:41 +01:00
caplog: pytest.LogCaptureFixture,
snapshot: SnapshotAssertion,
) -> None:
2022-01-17 11:36:59 -05:00
"""Test send base preferences are defined."""
aioclient_mock.post(BASIC_ENDPOINT_URL, status=200)
2021-08-02 18:46:07 +02:00
analytics = Analytics(hass)
await analytics.save_preferences({ATTR_BASE: True, ATTR_USAGE: True})
assert await async_setup_component(hass, "energy", {})
2021-08-02 18:46:07 +02:00
2024-02-16 15:47:41 +01:00
with patch(
2021-08-02 18:46:07 +02:00
"homeassistant.components.analytics.analytics.energy_is_configured", AsyncMock()
) as energy_is_configured:
energy_is_configured.return_value = True
await analytics.send_analytics()
2024-02-16 15:47:41 +01:00
logged_data = caplog.records[-1].args
submitted_data = _last_call_payload(aioclient_mock)
2021-08-02 18:46:07 +02:00
2024-02-16 15:47:41 +01:00
assert submitted_data["energy"]["configured"] is True
assert submitted_data == logged_data
assert (
snapshot(matcher=path_type({"recorder.version": (AwesomeVersion,)}))
== submitted_data
)
@pytest.mark.usefixtures(
"installation_type_mock", "mock_hass_config", "supervisor_client"
)
async def test_send_usage_with_certificate(
hass: HomeAssistant,
caplog: pytest.LogCaptureFixture,
aioclient_mock: AiohttpClientMocker,
2024-02-16 15:47:41 +01:00
snapshot: SnapshotAssertion,
) -> None:
"""Test send usage preferences with certificate."""
aioclient_mock.post(BASIC_ENDPOINT_URL, status=200)
analytics = Analytics(hass)
hass.http = Mock(ssl_certificate="/some/path/to/cert.pem")
await analytics.save_preferences({ATTR_BASE: True, ATTR_USAGE: True})
assert analytics.preferences[ATTR_BASE]
assert analytics.preferences[ATTR_USAGE]
2024-02-16 15:47:41 +01:00
await analytics.send_analytics()
2024-02-16 15:47:41 +01:00
logged_data = caplog.records[-1].args
submitted_data = _last_call_payload(aioclient_mock)
assert submitted_data["certificate"] is True
assert submitted_data == logged_data
assert snapshot == submitted_data
@pytest.mark.usefixtures("recorder_mock", "installation_type_mock", "supervisor_client")
async def test_send_with_recorder(
hass: HomeAssistant,
aioclient_mock: AiohttpClientMocker,
2024-02-16 15:47:41 +01:00
caplog: pytest.LogCaptureFixture,
snapshot: SnapshotAssertion,
) -> None:
"""Test recorder information."""
aioclient_mock.post(BASIC_ENDPOINT_URL, status=200)
analytics = Analytics(hass)
hass.http = Mock(ssl_certificate="/some/path/to/cert.pem")
await analytics.save_preferences({ATTR_BASE: True, ATTR_USAGE: True})
with patch(
"homeassistant.config.load_yaml_config_file",
return_value={"recorder": {}},
):
await analytics.send_analytics()
2024-02-16 15:47:41 +01:00
logged_data = caplog.records[-1].args
submitted_data = _last_call_payload(aioclient_mock)
assert submitted_data["recorder"]["engine"] == "sqlite"
assert submitted_data == logged_data
assert (
snapshot(matcher=path_type({"recorder.version": (AwesomeVersion,)}))
== submitted_data
)
@pytest.mark.usefixtures("supervisor_client")
async def test_send_with_problems_loading_yaml(
hass: HomeAssistant,
caplog: pytest.LogCaptureFixture,
aioclient_mock: AiohttpClientMocker,
) -> None:
"""Test error loading YAML configuration."""
analytics = Analytics(hass)
await analytics.save_preferences({ATTR_BASE: True, ATTR_USAGE: True})
with patch(
"homeassistant.config.load_yaml_config_file",
side_effect=HomeAssistantError("Error loading YAML file"),
):
await analytics.send_analytics()
assert "Error loading YAML file" in caplog.text
assert len(aioclient_mock.mock_calls) == 0
@pytest.mark.usefixtures("ha_dev_version_mock", "mock_hass_config", "supervisor_client")
async def test_timeout_while_sending(
hass: HomeAssistant,
caplog: pytest.LogCaptureFixture,
aioclient_mock: AiohttpClientMocker,
) -> None:
"""Test timeout error while sending analytics."""
analytics = Analytics(hass)
aioclient_mock.post(BASIC_ENDPOINT_URL_DEV, exc=TimeoutError())
await analytics.save_preferences({ATTR_BASE: True})
await analytics.send_analytics()
assert "Timeout sending analytics" in caplog.text
@pytest.mark.usefixtures("installation_type_mock", "supervisor_client")
async def test_not_check_config_entries_if_yaml(
hass: HomeAssistant,
caplog: pytest.LogCaptureFixture,
aioclient_mock: AiohttpClientMocker,
2024-02-16 15:47:41 +01:00
snapshot: SnapshotAssertion,
) -> None:
"""Test skip config entry check if defined in yaml."""
aioclient_mock.post(BASIC_ENDPOINT_URL, status=200)
analytics = Analytics(hass)
hass.http = Mock(ssl_certificate="/some/path/to/cert.pem")
await analytics.save_preferences(
{ATTR_BASE: True, ATTR_STATISTICS: True, ATTR_USAGE: True}
)
assert analytics.preferences[ATTR_BASE]
assert analytics.preferences[ATTR_STATISTICS]
hass.config.components = ["default_config"]
mock_config_entry = MockConfigEntry(
domain="ignored_integration",
state=ConfigEntryState.LOADED,
source="ignore",
disabled_by=ConfigEntryDisabler.USER,
)
mock_config_entry.add_to_hass(hass)
with (
patch(
"homeassistant.components.analytics.analytics.async_get_integrations",
return_value={
"default_config": mock_integration(
hass,
MockModule(
"default_config",
async_setup=AsyncMock(return_value=True),
partial_manifest={"config_flow": True},
2024-03-26 00:02:16 +01:00
),
),
},
),
patch(
"homeassistant.config.load_yaml_config_file",
return_value={"default_config": {}},
2024-03-26 00:02:16 +01:00
),
):
await analytics.send_analytics()
2024-02-16 15:47:41 +01:00
logged_data = caplog.records[-1].args
submitted_data = _last_call_payload(aioclient_mock)
assert submitted_data["integration_count"] == 1
assert submitted_data["integrations"] == ["default_config"]
assert submitted_data == logged_data
assert snapshot == submitted_data
@pytest.mark.usefixtures("installation_type_mock", "supervisor_client")
async def test_submitting_legacy_integrations(
hass: HomeAssistant,
caplog: pytest.LogCaptureFixture,
aioclient_mock: AiohttpClientMocker,
snapshot: SnapshotAssertion,
) -> None:
"""Test submitting legacy integrations."""
hass.http = Mock(ssl_certificate=None)
aioclient_mock.post(BASIC_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 = ["binary_sensor"]
with (
patch(
"homeassistant.components.analytics.analytics.async_get_integrations",
return_value={
"default_config": mock_integration(
hass,
MockModule(
"legacy_binary_sensor",
async_setup=AsyncMock(return_value=True),
partial_manifest={"config_flow": False},
),
),
},
),
patch(
"homeassistant.config.async_hass_config_yaml",
return_value={"binary_sensor": [{"platform": "legacy_binary_sensor"}]},
),
):
await analytics.send_analytics()
logged_data = caplog.records[-1].args
submitted_data = _last_call_payload(aioclient_mock)
assert submitted_data["integrations"] == ["legacy_binary_sensor"]
assert submitted_data == logged_data
assert snapshot == submitted_data
@pytest.mark.usefixtures("enable_custom_integrations")
async def test_devices_payload_no_entities(
hass: HomeAssistant,
hass_client: ClientSessionGenerator,
device_registry: dr.DeviceRegistry,
) -> None:
"""Test devices payload with no entities."""
assert await async_setup_component(hass, "analytics", {})
assert await async_devices_payload(hass) == {
"version": "home-assistant:1",
"home_assistant": MOCK_VERSION,
"integrations": {},
}
mock_config_entry = MockConfigEntry(domain="hue")
mock_config_entry.add_to_hass(hass)
# Normal device with all fields
device_registry.async_get_or_create(
config_entry_id=mock_config_entry.entry_id,
identifiers={("device", "1")},
sw_version="test-sw-version",
hw_version="test-hw-version",
name="test-name",
manufacturer="test-manufacturer",
model="test-model",
model_id="test-model-id",
suggested_area="Game Room",
configuration_url="http://example.com/config",
)
# Service type device
device_registry.async_get_or_create(
config_entry_id=mock_config_entry.entry_id,
identifiers={("device", "2")},
manufacturer="test-manufacturer",
model_id="test-model-id",
entry_type=dr.DeviceEntryType.SERVICE,
)
# Device without model_id
device_registry.async_get_or_create(
config_entry_id=mock_config_entry.entry_id,
identifiers={("device", "4")},
manufacturer="test-manufacturer",
)
# Device without manufacturer
device_registry.async_get_or_create(
config_entry_id=mock_config_entry.entry_id,
identifiers={("device", "5")},
model_id="test-model-id",
)
# Device with via_device reference
device_registry.async_get_or_create(
config_entry_id=mock_config_entry.entry_id,
identifiers={("device", "6")},
manufacturer="test-manufacturer6",
model_id="test-model-id6",
via_device=("device", "1"),
)
# Device from custom integration
mock_custom_config_entry = MockConfigEntry(domain="test")
mock_custom_config_entry.add_to_hass(hass)
device_registry.async_get_or_create(
config_entry_id=mock_custom_config_entry.entry_id,
identifiers={("device", "7")},
manufacturer="test-manufacturer7",
model_id="test-model-id7",
)
# Device from an integration with a service type
mock_service_config_entry = MockConfigEntry(domain="uptime")
mock_service_config_entry.add_to_hass(hass)
device_registry.async_get_or_create(
config_entry_id=mock_service_config_entry.entry_id,
identifiers={("device", "8")},
manufacturer="test-manufacturer8",
model_id="test-model-id8",
)
client = await hass_client()
response = await client.get("/api/analytics/devices")
assert response.status == HTTPStatus.OK
assert await response.json() == {
"version": "home-assistant:1",
"home_assistant": MOCK_VERSION,
"integrations": {
"hue": {
"devices": [
{
"entry_type": None,
"has_configuration_url": True,
"hw_version": "test-hw-version",
"manufacturer": "test-manufacturer",
"model": "test-model",
"model_id": "test-model-id",
"sw_version": "test-sw-version",
"via_device": None,
"entities": [],
},
{
"entry_type": None,
"has_configuration_url": False,
"hw_version": None,
"manufacturer": "test-manufacturer",
"model": None,
"model_id": None,
"sw_version": None,
"via_device": None,
"entities": [],
},
{
"entry_type": None,
"has_configuration_url": False,
"hw_version": None,
"manufacturer": None,
"model": None,
"model_id": "test-model-id",
"sw_version": None,
"via_device": None,
"entities": [],
},
{
"entry_type": None,
"has_configuration_url": False,
"hw_version": None,
"manufacturer": "test-manufacturer6",
"model": None,
"model_id": "test-model-id6",
"sw_version": None,
"via_device": ["hue", 0],
"entities": [],
},
],
"entities": [],
},
},
}
async def test_devices_payload_with_entities(
hass: HomeAssistant,
hass_client: ClientSessionGenerator,
device_registry: dr.DeviceRegistry,
entity_registry: er.EntityRegistry,
) -> None:
"""Test devices payload with entities."""
assert await async_setup_component(hass, "analytics", {})
mock_config_entry = MockConfigEntry(domain="hue")
mock_config_entry.add_to_hass(hass)
device_entry = device_registry.async_get_or_create(
config_entry_id=mock_config_entry.entry_id,
identifiers={("device", "1")},
manufacturer="test-manufacturer",
model_id="test-model-id",
)
device_entry_2 = device_registry.async_get_or_create(
config_entry_id=mock_config_entry.entry_id,
identifiers={("device", "2")},
manufacturer="test-manufacturer",
model_id="test-model-id",
)
device_entry_3 = device_registry.async_get_or_create(
config_entry_id=mock_config_entry.entry_id,
identifiers={("device", "3")},
manufacturer="test-manufacturer",
model_id="test-model-id",
entry_type=dr.DeviceEntryType.SERVICE,
)
# First device
# Entity with capabilities
entity_registry.async_get_or_create(
domain="light",
platform="hue",
unique_id="1",
capabilities={"min_color_temp_kelvin": 2000, "max_color_temp_kelvin": 6535},
device_id=device_entry.id,
has_entity_name=True,
)
# Entity with category and device class
entity_registry.async_get_or_create(
domain="number",
platform="hue",
unique_id="1",
device_id=device_entry.id,
entity_category=EntityCategory.CONFIG,
has_entity_name=True,
original_device_class=NumberDeviceClass.TEMPERATURE,
)
hass.states.async_set("number.hue_1", "2")
# Entity with assumed state
entity_registry.async_get_or_create(
domain="light",
platform="hue",
unique_id="2",
device_id=device_entry.id,
has_entity_name=True,
)
hass.states.async_set("light.hue_2", "on", {ATTR_ASSUMED_STATE: True})
# Entity from a different integration
entity_registry.async_get_or_create(
domain="light",
platform="shelly",
unique_id="1",
device_id=device_entry.id,
has_entity_name=True,
)
# Second device
entity_registry.async_get_or_create(
domain="light",
platform="hue",
unique_id="3",
device_id=device_entry_2.id,
)
# Third device (service type)
entity_registry.async_get_or_create(
domain="light",
platform="hue",
unique_id="4",
device_id=device_entry_3.id,
)
# Entity without device with unit of measurement and state class
entity_registry.async_get_or_create(
domain="sensor",
platform="hue",
unique_id="1",
capabilities={"state_class": "measurement"},
original_device_class=SensorDeviceClass.TEMPERATURE,
unit_of_measurement="°C",
)
client = await hass_client()
response = await client.get("/api/analytics/devices")
assert response.status == HTTPStatus.OK
assert await response.json() == {
"version": "home-assistant:1",
"home_assistant": MOCK_VERSION,
"integrations": {
"hue": {
"devices": [
{
"entry_type": None,
"has_configuration_url": False,
"hw_version": None,
"manufacturer": "test-manufacturer",
"model": None,
"model_id": "test-model-id",
"sw_version": None,
"via_device": None,
"entities": [
{
"assumed_state": None,
"domain": "light",
"entity_category": None,
"has_entity_name": True,
"original_device_class": None,
"unit_of_measurement": None,
},
{
"assumed_state": False,
"domain": "number",
"entity_category": "config",
"has_entity_name": True,
"original_device_class": "temperature",
"unit_of_measurement": None,
},
{
"assumed_state": True,
"domain": "light",
"entity_category": None,
"has_entity_name": True,
"original_device_class": None,
"unit_of_measurement": None,
},
],
},
{
"entry_type": None,
"has_configuration_url": False,
"hw_version": None,
"manufacturer": "test-manufacturer",
"model": None,
"model_id": "test-model-id",
"sw_version": None,
"via_device": None,
"entities": [
{
"assumed_state": None,
"domain": "light",
"entity_category": None,
"has_entity_name": False,
"original_device_class": None,
"unit_of_measurement": None,
},
],
},
],
"entities": [
{
"assumed_state": None,
"domain": "sensor",
"entity_category": None,
"has_entity_name": False,
"original_device_class": "temperature",
"unit_of_measurement": "°C",
},
],
},
"shelly": {
"devices": [],
"entities": [
{
"assumed_state": None,
"domain": "light",
"entity_category": None,
"has_entity_name": True,
2025-09-23 15:16:37 +02:00
"original_device_class": None,
"unit_of_measurement": None,
},
],
},
},
}
async def test_analytics_platforms(
hass: HomeAssistant,
hass_client: ClientSessionGenerator,
device_registry: dr.DeviceRegistry,
entity_registry: er.EntityRegistry,
) -> None:
"""Test analytics platforms."""
assert await async_setup_component(hass, "analytics", {})
mock_config_entry = MockConfigEntry(domain="test")
mock_config_entry.add_to_hass(hass)
device_registry.async_get_or_create(
config_entry_id=mock_config_entry.entry_id,
identifiers={("device", "1")},
manufacturer="test-manufacturer",
model_id="test-model-id",
)
device_registry.async_get_or_create(
config_entry_id=mock_config_entry.entry_id,
identifiers={("device", "2")},
manufacturer="test-manufacturer",
model_id="test-model-id-2",
)
entity_registry.async_get_or_create(
domain="sensor",
platform="test",
unique_id="1",
capabilities={"options": ["secret1", "secret2"]},
)
entity_registry.async_get_or_create(
domain="sensor",
platform="test",
unique_id="2",
capabilities={"options": ["secret1", "secret2"]},
)
async def async_modify_analytics(
hass: HomeAssistant,
analytics_input: AnalyticsInput,
) -> AnalyticsModifications:
first = True
devices_configs = {}
for device_id in analytics_input.device_ids:
device_config = DeviceAnalyticsModifications()
devices_configs[device_id] = device_config
if first:
first = False
else:
device_config.remove = True
first = True
entities_configs = {}
for entity_id in analytics_input.entity_ids:
entity_entry = entity_registry.async_get(entity_id)
entity_config = EntityAnalyticsModifications()
entities_configs[entity_id] = entity_config
if first:
first = False
entity_config.capabilities = dict(entity_entry.capabilities)
entity_config.capabilities["options"] = len(
entity_config.capabilities["options"]
)
else:
entity_config.remove = True
return AnalyticsModifications(
devices=devices_configs,
entities=entities_configs,
)
platform_mock = Mock(async_modify_analytics=async_modify_analytics)
mock_platform(hass, "test.analytics", platform_mock)
client = await hass_client()
response = await client.get("/api/analytics/devices")
assert response.status == HTTPStatus.OK
assert await response.json() == {
"version": "home-assistant:1",
"home_assistant": MOCK_VERSION,
"integrations": {
"test": {
"devices": [
{
"entry_type": None,
"has_configuration_url": False,
"hw_version": None,
"manufacturer": "test-manufacturer",
"model": None,
"model_id": "test-model-id",
"sw_version": None,
"via_device": None,
"entities": [],
2025-09-23 15:16:37 +02:00
},
],
"entities": [
{
"assumed_state": None,
"domain": "sensor",
"entity_category": None,
"has_entity_name": False,
"original_device_class": None,
"unit_of_measurement": None,
},
],
},
},
}
async def test_send_snapshot_disabled(
hass: HomeAssistant,
aioclient_mock: AiohttpClientMocker,
) -> None:
"""Test no snapshots are sent."""
analytics = Analytics(hass)
await analytics.send_snapshot()
await analytics.save_preferences({ATTR_SNAPSHOTS: False})
await analytics.send_snapshot()
assert len(aioclient_mock.mock_calls) == 0
async def test_send_snapshot_success(
hass: HomeAssistant,
caplog: pytest.LogCaptureFixture,
aioclient_mock: AiohttpClientMocker,
) -> None:
"""Test successful snapshot submission."""
aioclient_mock.post(
SNAPSHOT_ENDPOINT_URL,
status=200,
json={"submission_identifier": "test-identifier-123"},
)
analytics = Analytics(hass)
await analytics.save_preferences({ATTR_SNAPSHOTS: True})
await analytics.send_snapshot()
assert len(aioclient_mock.mock_calls) == 1
preferences = await analytics._store.async_load()
assert preferences["submission_identifier"] == "test-identifier-123"
assert "Submitted snapshot analytics to Home Assistant servers" in caplog.text
async def test_send_snapshot_with_existing_identifier(
hass: HomeAssistant,
caplog: pytest.LogCaptureFixture,
aioclient_mock: AiohttpClientMocker,
) -> None:
"""Test snapshot submission with existing identifier."""
aioclient_mock.post(
SNAPSHOT_ENDPOINT_URL,
status=200,
json={"submission_identifier": "test-identifier-123"},
)
analytics = Analytics(hass)
with patch(
"homeassistant.helpers.storage.Store.async_load",
return_value={
"onboarded": True,
"preferences": {ATTR_BASE: True, ATTR_SNAPSHOTS: True},
"uuid": "12345",
"submission_identifier": "old-identifier",
},
):
await analytics.load()
await analytics.send_snapshot()
assert len(aioclient_mock.mock_calls) == 1
call_headers = aioclient_mock.mock_calls[0][3]
assert call_headers["X-Device-Database-Submission-Identifier"] == "old-identifier"
preferences = await analytics._store.async_load()
assert preferences["submission_identifier"] == "test-identifier-123"
assert "Submitted snapshot analytics to Home Assistant servers" in caplog.text
async def test_send_snapshot_invalid_identifier(
hass: HomeAssistant,
caplog: pytest.LogCaptureFixture,
aioclient_mock: AiohttpClientMocker,
) -> None:
"""Test snapshot submission with invalid identifier."""
aioclient_mock.post(
SNAPSHOT_ENDPOINT_URL,
status=400,
json={
"kind": "invalid-submission-identifier",
"message": "The identifier is invalid",
},
)
analytics = Analytics(hass)
with patch(
"homeassistant.helpers.storage.Store.async_load",
return_value={
"onboarded": True,
"preferences": {ATTR_BASE: True, ATTR_SNAPSHOTS: True},
"uuid": "12345",
"submission_identifier": "invalid-identifier",
},
):
await analytics.load()
await analytics.send_snapshot()
assert len(aioclient_mock.mock_calls) == 1
preferences = await analytics._store.async_load()
assert preferences.get("submission_identifier") is None
assert "Invalid submission identifier" in caplog.text
@pytest.mark.parametrize(
("post_kwargs", "expected_log"),
[
(
{
"status": 400,
"json": {
"kind": "malformed-payload",
"message": "Invalid payload format",
},
},
"Malformed snapshot analytics submission",
),
(
{"status": 503, "text": "Service Unavailable"},
f"Snapshot analytics service {SNAPSHOT_ENDPOINT_URL} unavailable",
),
(
{"status": 500},
"Unexpected status code 500 when submitting snapshot analytics",
),
(
{"exc": TimeoutError()},
"Timeout sending snapshot analytics",
),
(
{"exc": aiohttp.ClientError()},
"Error sending snapshot analytics",
),
],
ids=[
"bad_request",
"service_unavailable",
"unexpected_status",
"timeout",
"client_error",
],
)
async def test_send_snapshot_error(
hass: HomeAssistant,
caplog: pytest.LogCaptureFixture,
aioclient_mock: AiohttpClientMocker,
post_kwargs: dict[str, Any],
expected_log: str,
) -> None:
"""Test snapshot submission error."""
aioclient_mock.post(SNAPSHOT_ENDPOINT_URL, **post_kwargs)
analytics = Analytics(hass)
with patch(
"homeassistant.helpers.storage.Store.async_load",
return_value={
"onboarded": True,
"preferences": {ATTR_BASE: True, ATTR_SNAPSHOTS: True},
"uuid": "12345",
},
):
await analytics.load()
await analytics.send_snapshot()
assert expected_log in caplog.text
async def test_async_schedule(
hass: HomeAssistant,
aioclient_mock: AiohttpClientMocker,
) -> None:
"""Test scheduling."""
aioclient_mock.post(BASIC_ENDPOINT_URL, status=200)
aioclient_mock.post(SNAPSHOT_ENDPOINT_URL, status=200, json={})
analytics = Analytics(hass)
# Schedule when not onboarded
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
# Onboard and enable both
await analytics.save_preferences({ATTR_BASE: True, ATTR_SNAPSHOTS: True})
await analytics.async_schedule()
async_fire_time_changed(hass, dt_util.utcnow() + timedelta(hours=25))
await hass.async_block_till_done()
assert any(str(call[1]) == BASIC_ENDPOINT_URL for call in aioclient_mock.mock_calls)
assert any(
str(call[1]) == SNAPSHOT_ENDPOINT_URL for call in aioclient_mock.mock_calls
)
preferences = await analytics._store.async_load()
assert preferences["snapshot_submission_time"] is not None
assert 0 <= preferences["snapshot_submission_time"] <= 86400
async def test_async_schedule_disabled(
hass: HomeAssistant,
aioclient_mock: AiohttpClientMocker,
) -> None:
"""Test scheduling when disabled."""
analytics = Analytics(hass)
with patch(
"homeassistant.helpers.storage.Store.async_load",
return_value={
"onboarded": True,
"preferences": {ATTR_BASE: False, ATTR_SNAPSHOTS: False},
"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
async def test_async_schedule_already_scheduled(
hass: HomeAssistant,
aioclient_mock: AiohttpClientMocker,
) -> None:
"""Test not rescheduled if already scheduled."""
aioclient_mock.post(BASIC_ENDPOINT_URL, status=200)
aioclient_mock.post(SNAPSHOT_ENDPOINT_URL, status=200, json={})
analytics = Analytics(hass)
with patch(
"homeassistant.helpers.storage.Store.async_load",
return_value={
"onboarded": True,
"preferences": {ATTR_BASE: True, ATTR_SNAPSHOTS: True},
"uuid": "12345",
},
):
await analytics.load()
await analytics.async_schedule()
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) == 2
assert any(str(call[1]) == BASIC_ENDPOINT_URL for call in aioclient_mock.mock_calls)
assert any(
str(call[1]) == SNAPSHOT_ENDPOINT_URL for call in aioclient_mock.mock_calls
)
@pytest.mark.parametrize(("onboarded"), [True, False])
async def test_async_schedule_cancel_when_disabled(
hass: HomeAssistant,
aioclient_mock: AiohttpClientMocker,
onboarded: bool,
) -> None:
"""Test that scheduled tasks are cancelled when disabled."""
analytics = Analytics(hass)
with patch(
"homeassistant.helpers.storage.Store.async_load",
return_value={
"onboarded": True,
"preferences": {ATTR_BASE: True, ATTR_SNAPSHOTS: True},
"uuid": "12345",
},
):
await analytics.load()
await analytics.async_schedule()
with patch(
"homeassistant.helpers.storage.Store.async_load",
return_value={
"onboarded": onboarded,
"preferences": {ATTR_BASE: False, ATTR_SNAPSHOTS: False},
"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
async def test_async_schedule_snapshots_url(
hass: HomeAssistant,
aioclient_mock: AiohttpClientMocker,
) -> None:
"""Test that snapshots use provided url."""
url = "https://custom-snapshot-url.example.com"
endpoint = f"{url}{SNAPSHOT_URL_PATH}"
aioclient_mock.post(endpoint, status=200, json={})
analytics = Analytics(hass, url)
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) == 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