mirror of
https://github.com/home-assistant/core.git
synced 2026-09-24 07:25:52 -05:00
Add snapshot device analytics url config option (#156984)
This commit is contained in:
@@ -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()
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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__)
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user