Add snapshot device analytics url config option (#156984)

This commit is contained in:
Artur Pragacz
2025-11-23 13:47:33 +01:00
committed by GitHub
parent 80151b205d
commit bd0ab4d1fe
5 changed files with 191 additions and 135 deletions
+25 -4
View File
@@ -7,7 +7,6 @@ import voluptuous as vol
from homeassistant.components import websocket_api
from homeassistant.const import EVENT_HOMEASSISTANT_STARTED
from homeassistant.core import Event, HomeAssistant, callback
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.typing import ConfigType
from homeassistant.util.hass_dict import HassKey
@@ -30,14 +29,36 @@ __all__ = [
"async_devices_payload",
]
CONFIG_SCHEMA = cv.empty_config_schema(DOMAIN)
CONF_SNAPSHOTS_URL = "snapshots_url"
CONFIG_SCHEMA = vol.Schema(
{
DOMAIN: vol.Schema(
{
vol.Optional(CONF_SNAPSHOTS_URL): vol.Any(str, None),
}
)
},
extra=vol.ALLOW_EXTRA,
)
DATA_COMPONENT: HassKey[Analytics] = HassKey(DOMAIN)
async def async_setup(hass: HomeAssistant, _: ConfigType) -> bool:
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Set up the analytics integration."""
analytics = Analytics(hass)
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
snapshots_url = analytics_config[CONF_SNAPSHOTS_URL]
else:
disable_snapshots = True
snapshots_url = None
analytics = Analytics(hass, snapshots_url, disable_snapshots)
# Load stored data
await analytics.load()
+53 -40
View File
@@ -59,9 +59,6 @@ from homeassistant.loader import (
from homeassistant.setup import async_get_loaded_integrations
from .const import (
ANALYTICS_ENDPOINT_URL,
ANALYTICS_ENDPOINT_URL_DEV,
ANALYTICS_SNAPSHOT_ENDPOINT_URL,
ATTR_ADDON_COUNT,
ATTR_ADDONS,
ATTR_ARCH,
@@ -91,10 +88,14 @@ from .const import (
ATTR_USER_COUNT,
ATTR_UUID,
ATTR_VERSION,
BASIC_ENDPOINT_URL,
BASIC_ENDPOINT_URL_DEV,
DOMAIN,
INTERVAL,
LOGGER,
PREFERENCE_SCHEMA,
SNAPSHOT_DEFAULT_URL,
SNAPSHOT_URL_PATH,
SNAPSHOT_VERSION,
STORAGE_KEY,
STORAGE_VERSION,
@@ -236,10 +237,18 @@ class AnalyticsData:
class Analytics:
"""Analytics helper class for the analytics integration."""
def __init__(self, hass: HomeAssistant) -> None:
def __init__(
self,
hass: HomeAssistant,
snapshots_url: str | None = None,
disable_snapshots: bool = False,
) -> None:
"""Initialize the Analytics class."""
self.hass: HomeAssistant = hass
self.session = async_get_clientsession(hass)
self._hass: HomeAssistant = hass
self._snapshots_url = snapshots_url
self._disable_snapshots = disable_snapshots
self._session = async_get_clientsession(hass)
self._data = AnalyticsData(False, {})
self._store = Store[dict[str, Any]](hass, STORAGE_VERSION, STORAGE_KEY)
self._basic_scheduled: CALLBACK_TYPE | None = None
@@ -249,13 +258,15 @@ class Analytics:
def preferences(self) -> dict:
"""Return the current active preferences."""
preferences = self._data.preferences
return {
result = {
ATTR_BASE: preferences.get(ATTR_BASE, False),
ATTR_SNAPSHOTS: preferences.get(ATTR_SNAPSHOTS, False),
ATTR_DIAGNOSTICS: preferences.get(ATTR_DIAGNOSTICS, False),
ATTR_USAGE: preferences.get(ATTR_USAGE, False),
ATTR_STATISTICS: preferences.get(ATTR_STATISTICS, False),
}
if not self._disable_snapshots:
result[ATTR_SNAPSHOTS] = preferences.get(ATTR_SNAPSHOTS, False)
return result
@property
def onboarded(self) -> bool:
@@ -272,13 +283,13 @@ class Analytics:
"""Return the endpoint that will receive the payload."""
if RELEASE_CHANNEL is ReleaseChannel.DEV:
# dev installations will contact the dev analytics environment
return ANALYTICS_ENDPOINT_URL_DEV
return ANALYTICS_ENDPOINT_URL
return BASIC_ENDPOINT_URL_DEV
return BASIC_ENDPOINT_URL
@property
def supervisor(self) -> bool:
"""Return bool if a supervisor is present."""
return is_hassio(self.hass)
return is_hassio(self._hass)
async def load(self) -> None:
"""Load preferences."""
@@ -288,7 +299,7 @@ class Analytics:
if (
self.supervisor
and (supervisor_info := hassio.get_supervisor_info(self.hass)) is not None
and (supervisor_info := hassio.get_supervisor_info(self._hass)) is not None
):
if not self.onboarded:
# User have not configured analytics, get this setting from the supervisor
@@ -315,7 +326,7 @@ class Analytics:
if self.supervisor:
await hassio.async_update_diagnostics(
self.hass, self.preferences.get(ATTR_DIAGNOSTICS, False)
self._hass, self.preferences.get(ATTR_DIAGNOSTICS, False)
)
async def send_analytics(self, _: datetime | None = None) -> None:
@@ -323,7 +334,7 @@ class Analytics:
if not self.onboarded or not self.preferences.get(ATTR_BASE, False):
return
hass = self.hass
hass = self._hass
supervisor_info = None
operating_system_info: dict[str, Any] = {}
@@ -463,7 +474,7 @@ class Analytics:
try:
async with timeout(30):
response = await self.session.post(self.endpoint_basic, json=payload)
response = await self._session.post(self.endpoint_basic, json=payload)
if response.status == 200:
LOGGER.info(
(
@@ -479,11 +490,9 @@ class Analytics:
self.endpoint_basic,
)
except TimeoutError:
LOGGER.error("Timeout sending analytics to %s", ANALYTICS_ENDPOINT_URL)
LOGGER.error("Timeout sending analytics to %s", BASIC_ENDPOINT_URL)
except aiohttp.ClientError as err:
LOGGER.error(
"Error sending analytics to %s: %r", ANALYTICS_ENDPOINT_URL, err
)
LOGGER.error("Error sending analytics to %s: %r", BASIC_ENDPOINT_URL, err)
@callback
def _async_should_report_integration(
@@ -507,7 +516,7 @@ class Analytics:
if not integration.config_flow:
return False
entries = self.hass.config_entries.async_entries(integration.domain)
entries = self._hass.config_entries.async_entries(integration.domain)
# Filter out ignored and disabled entries
return any(
@@ -521,7 +530,7 @@ class Analytics:
if not self.onboarded or not self.preferences.get(ATTR_SNAPSHOTS, False):
return
payload = await _async_snapshot_payload(self.hass)
payload = await _async_snapshot_payload(self._hass)
headers = {
"Content-Type": "application/json",
@@ -532,11 +541,16 @@ class Analytics:
self._data.submission_identifier
)
url = (
self._snapshots_url
if self._snapshots_url is not None
else SNAPSHOT_DEFAULT_URL
)
url += SNAPSHOT_URL_PATH
try:
async with timeout(30):
response = await self.session.post(
ANALYTICS_SNAPSHOT_ENDPOINT_URL, json=payload, headers=headers
)
response = await self._session.post(url, json=payload, headers=headers)
if response.status == 200: # OK
response_data = await response.json()
@@ -562,7 +576,7 @@ class Analytics:
# Clear the invalid identifier and retry on next cycle
LOGGER.warning(
"Invalid submission identifier to %s, clearing: %s",
ANALYTICS_SNAPSHOT_ENDPOINT_URL,
url,
error_message,
)
self._data.submission_identifier = None
@@ -571,7 +585,7 @@ class Analytics:
LOGGER.warning(
"Malformed snapshot analytics submission (%s) to %s: %s",
error_kind,
ANALYTICS_SNAPSHOT_ENDPOINT_URL,
url,
error_message,
)
@@ -579,7 +593,7 @@ class Analytics:
response_text = await response.text()
LOGGER.warning(
"Snapshot analytics service %s unavailable: %s",
ANALYTICS_SNAPSHOT_ENDPOINT_URL,
url,
response_text,
)
@@ -587,18 +601,18 @@ class Analytics:
LOGGER.warning(
"Unexpected status code %s when submitting snapshot analytics to %s",
response.status,
ANALYTICS_SNAPSHOT_ENDPOINT_URL,
url,
)
except TimeoutError:
LOGGER.error(
"Timeout sending snapshot analytics to %s",
ANALYTICS_SNAPSHOT_ENDPOINT_URL,
url,
)
except aiohttp.ClientError as err:
LOGGER.error(
"Error sending snapshot analytics to %s: %r",
ANALYTICS_SNAPSHOT_ENDPOINT_URL,
url,
err,
)
@@ -622,7 +636,7 @@ class Analytics:
elif self._basic_scheduled is None:
# Wait 15 min after started for basic analytics
self._basic_scheduled = async_call_later(
self.hass,
self._hass,
900,
HassJob(
self._async_schedule_basic,
@@ -631,10 +645,7 @@ class Analytics:
),
)
if not self.preferences.get(ATTR_SNAPSHOTS, False) or RELEASE_CHANNEL not in (
ReleaseChannel.DEV,
ReleaseChannel.NIGHTLY,
):
if not self.preferences.get(ATTR_SNAPSHOTS, False) or self._disable_snapshots:
LOGGER.debug("Snapshot analytics not scheduled")
if self._snapshot_scheduled:
self._snapshot_scheduled()
@@ -642,9 +653,11 @@ class Analytics:
elif self._snapshot_scheduled is None:
snapshot_submission_time = self._data.snapshot_submission_time
interval_seconds = INTERVAL.total_seconds()
if snapshot_submission_time is None:
# Randomize the submission time within the 24 hours
snapshot_submission_time = random.uniform(0, 86400)
snapshot_submission_time = random.uniform(0, interval_seconds)
self._data.snapshot_submission_time = snapshot_submission_time
await self._save()
LOGGER.debug(
@@ -654,10 +667,10 @@ class Analytics:
# Calculate delay until next submission
current_time = time.time()
delay = (snapshot_submission_time - current_time) % 86400
delay = (snapshot_submission_time - current_time) % interval_seconds
self._snapshot_scheduled = async_call_later(
self.hass,
self._hass,
delay,
HassJob(
self._async_schedule_snapshots,
@@ -672,7 +685,7 @@ class Analytics:
# Send basic analytics every day
self._basic_scheduled = async_track_time_interval(
self.hass,
self._hass,
self.send_analytics,
INTERVAL,
name="basic analytics daily",
@@ -685,7 +698,7 @@ class Analytics:
# Send snapshot analytics every day
self._snapshot_scheduled = async_track_time_interval(
self.hass,
self._hass,
self.send_snapshot,
INTERVAL,
name="snapshot analytics daily",
+6 -4
View File
@@ -5,15 +5,17 @@ import logging
import voluptuous as vol
ANALYTICS_ENDPOINT_URL = "https://analytics-api.home-assistant.io/v1"
ANALYTICS_ENDPOINT_URL_DEV = "https://analytics-api-dev.home-assistant.io/v1"
SNAPSHOT_VERSION = "1"
ANALYTICS_SNAPSHOT_ENDPOINT_URL = f"https://device-database.eco-dev-aws.openhomefoundation.com/api/v1/snapshot/{SNAPSHOT_VERSION}"
DOMAIN = "analytics"
INTERVAL = timedelta(days=1)
STORAGE_KEY = "core.analytics"
STORAGE_VERSION = 1
BASIC_ENDPOINT_URL = "https://analytics-api.home-assistant.io/v1"
BASIC_ENDPOINT_URL_DEV = "https://analytics-api-dev.home-assistant.io/v1"
SNAPSHOT_VERSION = 1
SNAPSHOT_DEFAULT_URL = "https://device-database.eco-dev-aws.openhomefoundation.com"
SNAPSHOT_URL_PATH = f"/api/v1/snapshot/{SNAPSHOT_VERSION}"
LOGGER: logging.Logger = logging.getLogger(__package__)
+105 -85
View File
@@ -21,14 +21,15 @@ from homeassistant.components.analytics.analytics import (
async_devices_payload,
)
from homeassistant.components.analytics.const import (
ANALYTICS_ENDPOINT_URL,
ANALYTICS_ENDPOINT_URL_DEV,
ANALYTICS_SNAPSHOT_ENDPOINT_URL,
ATTR_BASE,
ATTR_DIAGNOSTICS,
ATTR_SNAPSHOTS,
ATTR_STATISTICS,
ATTR_USAGE,
BASIC_ENDPOINT_URL,
BASIC_ENDPOINT_URL_DEV,
SNAPSHOT_DEFAULT_URL,
SNAPSHOT_URL_PATH,
)
from homeassistant.components.number import NumberDeviceClass
from homeassistant.components.sensor import SensorDeviceClass
@@ -51,6 +52,8 @@ from tests.common import (
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"
@@ -179,14 +182,14 @@ async def test_failed_to_send(
aioclient_mock: AiohttpClientMocker,
) -> None:
"""Test failed to send payload."""
aioclient_mock.post(ANALYTICS_ENDPOINT_URL, status=400)
aioclient_mock.post(BASIC_ENDPOINT_URL, status=400)
analytics = Analytics(hass)
await analytics.save_preferences({ATTR_BASE: True})
assert analytics.preferences[ATTR_BASE]
await analytics.send_analytics()
assert (
f"Sending analytics failed with statuscode 400 from {ANALYTICS_ENDPOINT_URL}"
f"Sending analytics failed with statuscode 400 from {BASIC_ENDPOINT_URL}"
in caplog.text
)
@@ -198,7 +201,7 @@ async def test_failed_to_send_raises(
aioclient_mock: AiohttpClientMocker,
) -> None:
"""Test raises when failed to send payload."""
aioclient_mock.post(ANALYTICS_ENDPOINT_URL, exc=aiohttp.ClientError())
aioclient_mock.post(BASIC_ENDPOINT_URL, exc=aiohttp.ClientError())
analytics = Analytics(hass)
await analytics.save_preferences({ATTR_BASE: True})
assert analytics.preferences[ATTR_BASE]
@@ -215,7 +218,7 @@ async def test_send_base(
snapshot: SnapshotAssertion,
) -> None:
"""Test send base preferences are defined."""
aioclient_mock.post(ANALYTICS_ENDPOINT_URL, status=200)
aioclient_mock.post(BASIC_ENDPOINT_URL, status=200)
analytics = Analytics(hass)
await analytics.save_preferences({ATTR_BASE: True})
@@ -238,7 +241,7 @@ async def test_send_base_with_supervisor(
snapshot: SnapshotAssertion,
) -> None:
"""Test send base preferences are defined."""
aioclient_mock.post(ANALYTICS_ENDPOINT_URL, status=200)
aioclient_mock.post(BASIC_ENDPOINT_URL, status=200)
analytics = Analytics(hass)
await analytics.save_preferences({ATTR_BASE: True})
@@ -291,7 +294,7 @@ async def test_send_usage(
snapshot: SnapshotAssertion,
) -> None:
"""Test send usage preferences are defined."""
aioclient_mock.post(ANALYTICS_ENDPOINT_URL, status=200)
aioclient_mock.post(BASIC_ENDPOINT_URL, status=200)
analytics = Analytics(hass)
hass.http = Mock(ssl_certificate=None)
await analytics.save_preferences({ATTR_BASE: True, ATTR_USAGE: True})
@@ -327,7 +330,7 @@ async def test_send_usage_with_supervisor(
supervisor_client: AsyncMock,
) -> None:
"""Test send usage with supervisor preferences are defined."""
aioclient_mock.post(ANALYTICS_ENDPOINT_URL, status=200)
aioclient_mock.post(BASIC_ENDPOINT_URL, status=200)
analytics = Analytics(hass)
hass.http = Mock(ssl_certificate=None)
await analytics.save_preferences({ATTR_BASE: True, ATTR_USAGE: True})
@@ -388,7 +391,7 @@ async def test_send_statistics(
snapshot: SnapshotAssertion,
) -> None:
"""Test send statistics preferences are defined."""
aioclient_mock.post(ANALYTICS_ENDPOINT_URL, status=200)
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]
@@ -414,7 +417,7 @@ async def test_send_statistics_one_integration_fails(
aioclient_mock: AiohttpClientMocker,
) -> None:
"""Test send statistics preferences are defined."""
aioclient_mock.post(ANALYTICS_ENDPOINT_URL, status=200)
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]
@@ -442,7 +445,7 @@ async def test_send_statistics_disabled_integration(
snapshot: SnapshotAssertion,
) -> None:
"""Test send statistics with disabled integration."""
aioclient_mock.post(ANALYTICS_ENDPOINT_URL, status=200)
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]
@@ -481,7 +484,7 @@ async def test_send_statistics_ignored_integration(
snapshot: SnapshotAssertion,
) -> None:
"""Test send statistics with ignored integration."""
aioclient_mock.post(ANALYTICS_ENDPOINT_URL, status=200)
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]
@@ -522,7 +525,7 @@ async def test_send_statistics_async_get_integration_unknown_exception(
aioclient_mock: AiohttpClientMocker,
) -> None:
"""Test send statistics preferences are defined."""
aioclient_mock.post(ANALYTICS_ENDPOINT_URL, status=200)
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]
@@ -548,7 +551,7 @@ async def test_send_statistics_with_supervisor(
supervisor_client: AsyncMock,
) -> None:
"""Test send statistics preferences are defined."""
aioclient_mock.post(ANALYTICS_ENDPOINT_URL, status=200)
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]
@@ -605,7 +608,7 @@ async def test_reusing_uuid(
aioclient_mock: AiohttpClientMocker,
) -> None:
"""Test reusing the stored UUID."""
aioclient_mock.post(ANALYTICS_ENDPOINT_URL, status=200)
aioclient_mock.post(BASIC_ENDPOINT_URL, status=200)
analytics = Analytics(hass)
analytics._data.uuid = "NOT_MOCK_UUID"
@@ -627,7 +630,7 @@ async def test_custom_integrations(
snapshot: SnapshotAssertion,
) -> None:
"""Test sending custom integrations."""
aioclient_mock.post(ANALYTICS_ENDPOINT_URL, status=200)
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": {}})
@@ -652,14 +655,14 @@ async def test_dev_url(
aioclient_mock: AiohttpClientMocker,
) -> None:
"""Test sending payload to dev url."""
aioclient_mock.post(ANALYTICS_ENDPOINT_URL_DEV, status=200)
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]) == ANALYTICS_ENDPOINT_URL_DEV
assert str(payload[1]) == BASIC_ENDPOINT_URL_DEV
@pytest.mark.usefixtures("ha_dev_version_mock", "supervisor_client")
@@ -669,17 +672,16 @@ async def test_dev_url_error(
caplog: pytest.LogCaptureFixture,
) -> None:
"""Test sending payload to dev url that returns error."""
aioclient_mock.post(ANALYTICS_ENDPOINT_URL_DEV, status=400)
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]) == ANALYTICS_ENDPOINT_URL_DEV
assert str(payload[1]) == BASIC_ENDPOINT_URL_DEV
assert (
"Sending analytics failed with statuscode 400 from"
f" {ANALYTICS_ENDPOINT_URL_DEV}"
f"Sending analytics failed with statuscode 400 from {BASIC_ENDPOINT_URL_DEV}"
) in caplog.text
@@ -689,7 +691,7 @@ async def test_nightly_endpoint(
aioclient_mock: AiohttpClientMocker,
) -> None:
"""Test sending payload to production url when running nightly."""
aioclient_mock.post(ANALYTICS_ENDPOINT_URL, status=200)
aioclient_mock.post(BASIC_ENDPOINT_URL, status=200)
analytics = Analytics(hass)
await analytics.save_preferences({ATTR_BASE: True})
@@ -699,7 +701,7 @@ async def test_nightly_endpoint(
await analytics.send_analytics()
payload = aioclient_mock.mock_calls[0]
assert str(payload[1]) == ANALYTICS_ENDPOINT_URL
assert str(payload[1]) == BASIC_ENDPOINT_URL
@pytest.mark.usefixtures(
@@ -712,7 +714,7 @@ async def test_send_with_no_energy(
snapshot: SnapshotAssertion,
) -> None:
"""Test send base preferences are defined."""
aioclient_mock.post(ANALYTICS_ENDPOINT_URL, status=200)
aioclient_mock.post(BASIC_ENDPOINT_URL, status=200)
analytics = Analytics(hass)
hass.http = Mock(ssl_certificate=None)
@@ -750,7 +752,7 @@ async def test_send_with_no_energy_config(
snapshot: SnapshotAssertion,
) -> None:
"""Test send base preferences are defined."""
aioclient_mock.post(ANALYTICS_ENDPOINT_URL, status=200)
aioclient_mock.post(BASIC_ENDPOINT_URL, status=200)
analytics = Analytics(hass)
await analytics.save_preferences({ATTR_BASE: True, ATTR_USAGE: True})
@@ -783,7 +785,7 @@ async def test_send_with_energy_config(
snapshot: SnapshotAssertion,
) -> None:
"""Test send base preferences are defined."""
aioclient_mock.post(ANALYTICS_ENDPOINT_URL, status=200)
aioclient_mock.post(BASIC_ENDPOINT_URL, status=200)
analytics = Analytics(hass)
await analytics.save_preferences({ATTR_BASE: True, ATTR_USAGE: True})
@@ -816,7 +818,7 @@ async def test_send_usage_with_certificate(
snapshot: SnapshotAssertion,
) -> None:
"""Test send usage preferences with certificate."""
aioclient_mock.post(ANALYTICS_ENDPOINT_URL, status=200)
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})
@@ -842,7 +844,7 @@ async def test_send_with_recorder(
snapshot: SnapshotAssertion,
) -> None:
"""Test recorder information."""
aioclient_mock.post(ANALYTICS_ENDPOINT_URL, status=200)
aioclient_mock.post(BASIC_ENDPOINT_URL, status=200)
analytics = Analytics(hass)
hass.http = Mock(ssl_certificate="/some/path/to/cert.pem")
@@ -893,7 +895,7 @@ async def test_timeout_while_sending(
) -> None:
"""Test timeout error while sending analytics."""
analytics = Analytics(hass)
aioclient_mock.post(ANALYTICS_ENDPOINT_URL_DEV, exc=TimeoutError())
aioclient_mock.post(BASIC_ENDPOINT_URL_DEV, exc=TimeoutError())
await analytics.save_preferences({ATTR_BASE: True})
await analytics.send_analytics()
@@ -909,7 +911,7 @@ async def test_not_check_config_entries_if_yaml(
snapshot: SnapshotAssertion,
) -> None:
"""Test skip config entry check if defined in yaml."""
aioclient_mock.post(ANALYTICS_ENDPOINT_URL, status=200)
aioclient_mock.post(BASIC_ENDPOINT_URL, status=200)
analytics = Analytics(hass)
hass.http = Mock(ssl_certificate="/some/path/to/cert.pem")
@@ -967,7 +969,7 @@ async def test_submitting_legacy_integrations(
) -> None:
"""Test submitting legacy integrations."""
hass.http = Mock(ssl_certificate=None)
aioclient_mock.post(ANALYTICS_ENDPOINT_URL, status=200)
aioclient_mock.post(BASIC_ENDPOINT_URL, status=200)
analytics = Analytics(hass)
await analytics.save_preferences({ATTR_BASE: True, ATTR_USAGE: True})
@@ -1472,7 +1474,7 @@ async def test_send_snapshot_success(
) -> None:
"""Test successful snapshot submission."""
aioclient_mock.post(
ANALYTICS_SNAPSHOT_ENDPOINT_URL,
SNAPSHOT_ENDPOINT_URL,
status=200,
json={"submission_identifier": "test-identifier-123"},
)
@@ -1496,7 +1498,7 @@ async def test_send_snapshot_with_existing_identifier(
) -> None:
"""Test snapshot submission with existing identifier."""
aioclient_mock.post(
ANALYTICS_SNAPSHOT_ENDPOINT_URL,
SNAPSHOT_ENDPOINT_URL,
status=200,
json={"submission_identifier": "test-identifier-123"},
)
@@ -1531,7 +1533,7 @@ async def test_send_snapshot_invalid_identifier(
) -> None:
"""Test snapshot submission with invalid identifier."""
aioclient_mock.post(
ANALYTICS_SNAPSHOT_ENDPOINT_URL,
SNAPSHOT_ENDPOINT_URL,
status=400,
json={
"kind": "invalid-submission-identifier",
@@ -1575,7 +1577,7 @@ async def test_send_snapshot_invalid_identifier(
),
(
{"status": 503, "text": "Service Unavailable"},
f"Snapshot analytics service {ANALYTICS_SNAPSHOT_ENDPOINT_URL} unavailable",
f"Snapshot analytics service {SNAPSHOT_ENDPOINT_URL} unavailable",
),
(
{"status": 500},
@@ -1606,7 +1608,7 @@ async def test_send_snapshot_error(
expected_log: str,
) -> None:
"""Test snapshot submission error."""
aioclient_mock.post(ANALYTICS_SNAPSHOT_ENDPOINT_URL, **post_kwargs)
aioclient_mock.post(SNAPSHOT_ENDPOINT_URL, **post_kwargs)
analytics = Analytics(hass)
with patch(
@@ -1624,14 +1626,13 @@ async def test_send_snapshot_error(
assert expected_log in caplog.text
@pytest.mark.usefixtures("ha_dev_version_mock", "supervisor_client")
async def test_async_schedule(
hass: HomeAssistant,
aioclient_mock: AiohttpClientMocker,
) -> None:
"""Test scheduling."""
aioclient_mock.post(ANALYTICS_ENDPOINT_URL_DEV, status=200)
aioclient_mock.post(ANALYTICS_SNAPSHOT_ENDPOINT_URL, status=200, json={})
aioclient_mock.post(BASIC_ENDPOINT_URL, status=200)
aioclient_mock.post(SNAPSHOT_ENDPOINT_URL, status=200, json={})
analytics = Analytics(hass)
@@ -1651,12 +1652,9 @@ async def test_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]) == ANALYTICS_ENDPOINT_URL_DEV for call in aioclient_mock.mock_calls
)
assert any(
str(call[1]) == ANALYTICS_SNAPSHOT_ENDPOINT_URL
for call in aioclient_mock.mock_calls
str(call[1]) == SNAPSHOT_ENDPOINT_URL for call in aioclient_mock.mock_calls
)
preferences = await analytics._store.async_load()
@@ -1664,7 +1662,6 @@ async def test_async_schedule(
assert 0 <= preferences["snapshot_submission_time"] <= 86400
@pytest.mark.usefixtures("ha_dev_version_mock")
async def test_async_schedule_disabled(
hass: HomeAssistant,
aioclient_mock: AiohttpClientMocker,
@@ -1689,42 +1686,13 @@ async def test_async_schedule_disabled(
assert len(aioclient_mock.mock_calls) == 0
@pytest.mark.usefixtures("supervisor_client")
async def test_async_schedule_snapshots_not_dev(
hass: HomeAssistant,
aioclient_mock: AiohttpClientMocker,
) -> None:
"""Test that snapshots are not scheduled on non-dev versions."""
aioclient_mock.post(ANALYTICS_ENDPOINT_URL, status=200)
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()
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]) == ANALYTICS_ENDPOINT_URL
@pytest.mark.usefixtures("ha_dev_version_mock", "supervisor_client")
async def test_async_schedule_already_scheduled(
hass: HomeAssistant,
aioclient_mock: AiohttpClientMocker,
) -> None:
"""Test not rescheduled if already scheduled."""
aioclient_mock.post(ANALYTICS_ENDPOINT_URL_DEV, status=200)
aioclient_mock.post(ANALYTICS_SNAPSHOT_ENDPOINT_URL, status=200, json={})
aioclient_mock.post(BASIC_ENDPOINT_URL, status=200)
aioclient_mock.post(SNAPSHOT_ENDPOINT_URL, status=200, json={})
analytics = Analytics(hass)
with patch(
@@ -1745,17 +1713,13 @@ async def test_async_schedule_already_scheduled(
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]) == ANALYTICS_ENDPOINT_URL_DEV for call in aioclient_mock.mock_calls
)
assert any(
str(call[1]) == ANALYTICS_SNAPSHOT_ENDPOINT_URL
for call in aioclient_mock.mock_calls
str(call[1]) == SNAPSHOT_ENDPOINT_URL for call in aioclient_mock.mock_calls
)
@pytest.mark.parametrize(("onboarded"), [True, False])
@pytest.mark.usefixtures("ha_dev_version_mock")
async def test_async_schedule_cancel_when_disabled(
hass: HomeAssistant,
aioclient_mock: AiohttpClientMocker,
@@ -1791,3 +1755,59 @@ async def test_async_schedule_cancel_when_disabled(
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
+2 -2
View File
@@ -4,7 +4,7 @@ from unittest.mock import patch
import pytest
from homeassistant.components.analytics.const import ANALYTICS_ENDPOINT_URL, DOMAIN
from homeassistant.components.analytics.const import BASIC_ENDPOINT_URL, DOMAIN
from homeassistant.core import HomeAssistant
from homeassistant.setup import async_setup_component
@@ -29,7 +29,7 @@ async def test_websocket(
aioclient_mock: AiohttpClientMocker,
) -> None:
"""Test WebSocket commands."""
aioclient_mock.post(ANALYTICS_ENDPOINT_URL, status=200)
aioclient_mock.post(BASIC_ENDPOINT_URL, status=200)
assert await async_setup_component(hass, DOMAIN, {DOMAIN: {}})
await hass.async_block_till_done()