mirror of
https://github.com/home-assistant/core.git
synced 2025-07-23 13:17:32 +00:00
Add activity sensors to Withings (#102501)
Co-authored-by: J. Nick Koston <nick@koston.org>
This commit is contained in:
parent
164872e1af
commit
04b883a8e9
@ -53,6 +53,7 @@ from homeassistant.helpers.typing import ConfigType
|
|||||||
|
|
||||||
from .const import CONF_PROFILES, CONF_USE_WEBHOOK, DEFAULT_TITLE, DOMAIN, LOGGER
|
from .const import CONF_PROFILES, CONF_USE_WEBHOOK, DEFAULT_TITLE, DOMAIN, LOGGER
|
||||||
from .coordinator import (
|
from .coordinator import (
|
||||||
|
WithingsActivityDataUpdateCoordinator,
|
||||||
WithingsBedPresenceDataUpdateCoordinator,
|
WithingsBedPresenceDataUpdateCoordinator,
|
||||||
WithingsDataUpdateCoordinator,
|
WithingsDataUpdateCoordinator,
|
||||||
WithingsGoalsDataUpdateCoordinator,
|
WithingsGoalsDataUpdateCoordinator,
|
||||||
@ -131,6 +132,7 @@ class WithingsData:
|
|||||||
sleep_coordinator: WithingsSleepDataUpdateCoordinator
|
sleep_coordinator: WithingsSleepDataUpdateCoordinator
|
||||||
bed_presence_coordinator: WithingsBedPresenceDataUpdateCoordinator
|
bed_presence_coordinator: WithingsBedPresenceDataUpdateCoordinator
|
||||||
goals_coordinator: WithingsGoalsDataUpdateCoordinator
|
goals_coordinator: WithingsGoalsDataUpdateCoordinator
|
||||||
|
activity_coordinator: WithingsActivityDataUpdateCoordinator
|
||||||
coordinators: set[WithingsDataUpdateCoordinator] = field(default_factory=set)
|
coordinators: set[WithingsDataUpdateCoordinator] = field(default_factory=set)
|
||||||
|
|
||||||
def __post_init__(self) -> None:
|
def __post_init__(self) -> None:
|
||||||
@ -140,6 +142,7 @@ class WithingsData:
|
|||||||
self.sleep_coordinator,
|
self.sleep_coordinator,
|
||||||
self.bed_presence_coordinator,
|
self.bed_presence_coordinator,
|
||||||
self.goals_coordinator,
|
self.goals_coordinator,
|
||||||
|
self.activity_coordinator,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -172,6 +175,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
|||||||
sleep_coordinator=WithingsSleepDataUpdateCoordinator(hass, client),
|
sleep_coordinator=WithingsSleepDataUpdateCoordinator(hass, client),
|
||||||
bed_presence_coordinator=WithingsBedPresenceDataUpdateCoordinator(hass, client),
|
bed_presence_coordinator=WithingsBedPresenceDataUpdateCoordinator(hass, client),
|
||||||
goals_coordinator=WithingsGoalsDataUpdateCoordinator(hass, client),
|
goals_coordinator=WithingsGoalsDataUpdateCoordinator(hass, client),
|
||||||
|
activity_coordinator=WithingsActivityDataUpdateCoordinator(hass, client),
|
||||||
)
|
)
|
||||||
|
|
||||||
for coordinator in withings_data.coordinators:
|
for coordinator in withings_data.coordinators:
|
||||||
|
@ -1,9 +1,10 @@
|
|||||||
"""Withings coordinator."""
|
"""Withings coordinator."""
|
||||||
from abc import abstractmethod
|
from abc import abstractmethod
|
||||||
from datetime import datetime, timedelta
|
from datetime import date, datetime, timedelta
|
||||||
from typing import TypeVar
|
from typing import TypeVar
|
||||||
|
|
||||||
from aiowithings import (
|
from aiowithings import (
|
||||||
|
Activity,
|
||||||
Goals,
|
Goals,
|
||||||
MeasurementType,
|
MeasurementType,
|
||||||
NotificationCategory,
|
NotificationCategory,
|
||||||
@ -81,7 +82,6 @@ class WithingsMeasurementDataUpdateCoordinator(
|
|||||||
super().__init__(hass, client)
|
super().__init__(hass, client)
|
||||||
self.notification_categories = {
|
self.notification_categories = {
|
||||||
NotificationCategory.WEIGHT,
|
NotificationCategory.WEIGHT,
|
||||||
NotificationCategory.ACTIVITY,
|
|
||||||
NotificationCategory.PRESSURE,
|
NotificationCategory.PRESSURE,
|
||||||
}
|
}
|
||||||
self._previous_data: dict[MeasurementType, float] = {}
|
self._previous_data: dict[MeasurementType, float] = {}
|
||||||
@ -185,3 +185,41 @@ class WithingsGoalsDataUpdateCoordinator(WithingsDataUpdateCoordinator[Goals]):
|
|||||||
async def _internal_update_data(self) -> Goals:
|
async def _internal_update_data(self) -> Goals:
|
||||||
"""Retrieve goals data."""
|
"""Retrieve goals data."""
|
||||||
return await self._client.get_goals()
|
return await self._client.get_goals()
|
||||||
|
|
||||||
|
|
||||||
|
class WithingsActivityDataUpdateCoordinator(
|
||||||
|
WithingsDataUpdateCoordinator[Activity | None]
|
||||||
|
):
|
||||||
|
"""Withings activity coordinator."""
|
||||||
|
|
||||||
|
_previous_data: Activity | None = None
|
||||||
|
|
||||||
|
def __init__(self, hass: HomeAssistant, client: WithingsClient) -> None:
|
||||||
|
"""Initialize the Withings data coordinator."""
|
||||||
|
super().__init__(hass, client)
|
||||||
|
self.notification_categories = {
|
||||||
|
NotificationCategory.ACTIVITY,
|
||||||
|
}
|
||||||
|
|
||||||
|
async def _internal_update_data(self) -> Activity | None:
|
||||||
|
"""Retrieve latest activity."""
|
||||||
|
if self._last_valid_update is None:
|
||||||
|
now = dt_util.utcnow()
|
||||||
|
startdate = now - timedelta(days=14)
|
||||||
|
activities = await self._client.get_activities_in_period(
|
||||||
|
startdate.date(), now.date()
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
activities = await self._client.get_activities_since(
|
||||||
|
self._last_valid_update
|
||||||
|
)
|
||||||
|
|
||||||
|
today = date.today()
|
||||||
|
for activity in activities:
|
||||||
|
if activity.date == today:
|
||||||
|
self._previous_data = activity
|
||||||
|
self._last_valid_update = activity.modified
|
||||||
|
return activity
|
||||||
|
if self._previous_data and self._previous_data.date == today:
|
||||||
|
return self._previous_data
|
||||||
|
return None
|
||||||
|
@ -3,8 +3,9 @@ from __future__ import annotations
|
|||||||
|
|
||||||
from collections.abc import Callable
|
from collections.abc import Callable
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
from aiowithings import Goals, MeasurementType, SleepSummary
|
from aiowithings import Activity, Goals, MeasurementType, SleepSummary
|
||||||
|
|
||||||
from homeassistant.components.sensor import (
|
from homeassistant.components.sensor import (
|
||||||
SensorDeviceClass,
|
SensorDeviceClass,
|
||||||
@ -15,6 +16,7 @@ from homeassistant.components.sensor import (
|
|||||||
from homeassistant.config_entries import ConfigEntry
|
from homeassistant.config_entries import ConfigEntry
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
PERCENTAGE,
|
PERCENTAGE,
|
||||||
|
Platform,
|
||||||
UnitOfLength,
|
UnitOfLength,
|
||||||
UnitOfMass,
|
UnitOfMass,
|
||||||
UnitOfSpeed,
|
UnitOfSpeed,
|
||||||
@ -23,7 +25,9 @@ from homeassistant.const import (
|
|||||||
)
|
)
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
|
import homeassistant.helpers.entity_registry as er
|
||||||
from homeassistant.helpers.typing import StateType
|
from homeassistant.helpers.typing import StateType
|
||||||
|
from homeassistant.util import dt as dt_util
|
||||||
|
|
||||||
from . import WithingsData
|
from . import WithingsData
|
||||||
from .const import (
|
from .const import (
|
||||||
@ -35,6 +39,7 @@ from .const import (
|
|||||||
UOM_MMHG,
|
UOM_MMHG,
|
||||||
)
|
)
|
||||||
from .coordinator import (
|
from .coordinator import (
|
||||||
|
WithingsActivityDataUpdateCoordinator,
|
||||||
WithingsDataUpdateCoordinator,
|
WithingsDataUpdateCoordinator,
|
||||||
WithingsGoalsDataUpdateCoordinator,
|
WithingsGoalsDataUpdateCoordinator,
|
||||||
WithingsMeasurementDataUpdateCoordinator,
|
WithingsMeasurementDataUpdateCoordinator,
|
||||||
@ -396,6 +401,105 @@ SLEEP_SENSORS = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class WithingsActivitySensorEntityDescriptionMixin:
|
||||||
|
"""Mixin for describing withings data."""
|
||||||
|
|
||||||
|
value_fn: Callable[[Activity], StateType]
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class WithingsActivitySensorEntityDescription(
|
||||||
|
SensorEntityDescription, WithingsActivitySensorEntityDescriptionMixin
|
||||||
|
):
|
||||||
|
"""Immutable class for describing withings data."""
|
||||||
|
|
||||||
|
|
||||||
|
ACTIVITY_SENSORS = [
|
||||||
|
WithingsActivitySensorEntityDescription(
|
||||||
|
key="activity_steps_today",
|
||||||
|
value_fn=lambda activity: activity.steps,
|
||||||
|
translation_key="activity_steps_today",
|
||||||
|
icon="mdi:shoe-print",
|
||||||
|
native_unit_of_measurement="Steps",
|
||||||
|
state_class=SensorStateClass.TOTAL,
|
||||||
|
),
|
||||||
|
WithingsActivitySensorEntityDescription(
|
||||||
|
key="activity_distance_today",
|
||||||
|
value_fn=lambda activity: activity.distance,
|
||||||
|
translation_key="activity_distance_today",
|
||||||
|
suggested_display_precision=0,
|
||||||
|
icon="mdi:map-marker-distance",
|
||||||
|
native_unit_of_measurement=UnitOfLength.METERS,
|
||||||
|
device_class=SensorDeviceClass.DISTANCE,
|
||||||
|
state_class=SensorStateClass.TOTAL,
|
||||||
|
),
|
||||||
|
WithingsActivitySensorEntityDescription(
|
||||||
|
key="activity_floors_climbed_today",
|
||||||
|
value_fn=lambda activity: activity.floors_climbed,
|
||||||
|
translation_key="activity_floors_climbed_today",
|
||||||
|
icon="mdi:stairs-up",
|
||||||
|
native_unit_of_measurement="Floors",
|
||||||
|
state_class=SensorStateClass.TOTAL,
|
||||||
|
),
|
||||||
|
WithingsActivitySensorEntityDescription(
|
||||||
|
key="activity_soft_duration_today",
|
||||||
|
value_fn=lambda activity: activity.soft_activity,
|
||||||
|
translation_key="activity_soft_duration_today",
|
||||||
|
native_unit_of_measurement=UnitOfTime.SECONDS,
|
||||||
|
suggested_unit_of_measurement=UnitOfTime.MINUTES,
|
||||||
|
device_class=SensorDeviceClass.DURATION,
|
||||||
|
state_class=SensorStateClass.TOTAL,
|
||||||
|
entity_registry_enabled_default=False,
|
||||||
|
),
|
||||||
|
WithingsActivitySensorEntityDescription(
|
||||||
|
key="activity_moderate_duration_today",
|
||||||
|
value_fn=lambda activity: activity.moderate_activity,
|
||||||
|
translation_key="activity_moderate_duration_today",
|
||||||
|
native_unit_of_measurement=UnitOfTime.SECONDS,
|
||||||
|
suggested_unit_of_measurement=UnitOfTime.MINUTES,
|
||||||
|
device_class=SensorDeviceClass.DURATION,
|
||||||
|
state_class=SensorStateClass.TOTAL,
|
||||||
|
entity_registry_enabled_default=False,
|
||||||
|
),
|
||||||
|
WithingsActivitySensorEntityDescription(
|
||||||
|
key="activity_intense_duration_today",
|
||||||
|
value_fn=lambda activity: activity.intense_activity,
|
||||||
|
translation_key="activity_intense_duration_today",
|
||||||
|
native_unit_of_measurement=UnitOfTime.SECONDS,
|
||||||
|
suggested_unit_of_measurement=UnitOfTime.MINUTES,
|
||||||
|
device_class=SensorDeviceClass.DURATION,
|
||||||
|
state_class=SensorStateClass.TOTAL,
|
||||||
|
entity_registry_enabled_default=False,
|
||||||
|
),
|
||||||
|
WithingsActivitySensorEntityDescription(
|
||||||
|
key="activity_active_duration_today",
|
||||||
|
value_fn=lambda activity: activity.total_time_active,
|
||||||
|
translation_key="activity_active_duration_today",
|
||||||
|
native_unit_of_measurement=UnitOfTime.SECONDS,
|
||||||
|
suggested_unit_of_measurement=UnitOfTime.HOURS,
|
||||||
|
device_class=SensorDeviceClass.DURATION,
|
||||||
|
state_class=SensorStateClass.TOTAL,
|
||||||
|
),
|
||||||
|
WithingsActivitySensorEntityDescription(
|
||||||
|
key="activity_active_calories_burnt_today",
|
||||||
|
value_fn=lambda activity: activity.active_calories_burnt,
|
||||||
|
suggested_display_precision=1,
|
||||||
|
translation_key="activity_active_calories_burnt_today",
|
||||||
|
native_unit_of_measurement="Calories",
|
||||||
|
state_class=SensorStateClass.TOTAL,
|
||||||
|
),
|
||||||
|
WithingsActivitySensorEntityDescription(
|
||||||
|
key="activity_total_calories_burnt_today",
|
||||||
|
value_fn=lambda activity: activity.total_calories_burnt,
|
||||||
|
suggested_display_precision=1,
|
||||||
|
translation_key="activity_total_calories_burnt_today",
|
||||||
|
native_unit_of_measurement="Calories",
|
||||||
|
state_class=SensorStateClass.TOTAL,
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
STEP_GOAL = "steps"
|
STEP_GOAL = "steps"
|
||||||
SLEEP_GOAL = "sleep"
|
SLEEP_GOAL = "sleep"
|
||||||
WEIGHT_GOAL = "weight"
|
WEIGHT_GOAL = "weight"
|
||||||
@ -460,6 +564,8 @@ async def async_setup_entry(
|
|||||||
async_add_entities: AddEntitiesCallback,
|
async_add_entities: AddEntitiesCallback,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Set up the sensor config entry."""
|
"""Set up the sensor config entry."""
|
||||||
|
ent_reg = er.async_get(hass)
|
||||||
|
|
||||||
withings_data: WithingsData = hass.data[DOMAIN][entry.entry_id]
|
withings_data: WithingsData = hass.data[DOMAIN][entry.entry_id]
|
||||||
|
|
||||||
measurement_coordinator = withings_data.measurement_coordinator
|
measurement_coordinator = withings_data.measurement_coordinator
|
||||||
@ -512,6 +618,31 @@ async def async_setup_entry(
|
|||||||
|
|
||||||
goals_coordinator.async_add_listener(_async_goals_listener)
|
goals_coordinator.async_add_listener(_async_goals_listener)
|
||||||
|
|
||||||
|
activity_coordinator = withings_data.activity_coordinator
|
||||||
|
|
||||||
|
activity_callback: Callable[[], None] | None = None
|
||||||
|
|
||||||
|
activity_entities_setup_before = ent_reg.async_get_entity_id(
|
||||||
|
Platform.SENSOR, DOMAIN, f"withings_{entry.unique_id}_activity_steps_today"
|
||||||
|
)
|
||||||
|
|
||||||
|
def _async_add_activity_entities() -> None:
|
||||||
|
"""Add activity entities."""
|
||||||
|
if activity_coordinator.data is not None or activity_entities_setup_before:
|
||||||
|
async_add_entities(
|
||||||
|
WithingsActivitySensor(activity_coordinator, attribute)
|
||||||
|
for attribute in ACTIVITY_SENSORS
|
||||||
|
)
|
||||||
|
if activity_callback:
|
||||||
|
activity_callback()
|
||||||
|
|
||||||
|
if activity_coordinator.data is not None or activity_entities_setup_before:
|
||||||
|
_async_add_activity_entities()
|
||||||
|
else:
|
||||||
|
activity_callback = activity_coordinator.async_add_listener(
|
||||||
|
_async_add_activity_entities
|
||||||
|
)
|
||||||
|
|
||||||
sleep_coordinator = withings_data.sleep_coordinator
|
sleep_coordinator = withings_data.sleep_coordinator
|
||||||
|
|
||||||
entities.extend(
|
entities.extend(
|
||||||
@ -585,3 +716,23 @@ class WithingsGoalsSensor(WithingsSensor):
|
|||||||
"""Return the state of the entity."""
|
"""Return the state of the entity."""
|
||||||
assert self.coordinator.data
|
assert self.coordinator.data
|
||||||
return self.entity_description.value_fn(self.coordinator.data)
|
return self.entity_description.value_fn(self.coordinator.data)
|
||||||
|
|
||||||
|
|
||||||
|
class WithingsActivitySensor(WithingsSensor):
|
||||||
|
"""Implementation of a Withings activity sensor."""
|
||||||
|
|
||||||
|
coordinator: WithingsActivityDataUpdateCoordinator
|
||||||
|
|
||||||
|
entity_description: WithingsActivitySensorEntityDescription
|
||||||
|
|
||||||
|
@property
|
||||||
|
def native_value(self) -> StateType:
|
||||||
|
"""Return the state of the entity."""
|
||||||
|
if not self.coordinator.data:
|
||||||
|
return None
|
||||||
|
return self.entity_description.value_fn(self.coordinator.data)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def last_reset(self) -> datetime:
|
||||||
|
"""These values reset every day."""
|
||||||
|
return dt_util.start_of_local_day()
|
||||||
|
@ -143,6 +143,33 @@
|
|||||||
},
|
},
|
||||||
"weight_goal": {
|
"weight_goal": {
|
||||||
"name": "Weight goal"
|
"name": "Weight goal"
|
||||||
|
},
|
||||||
|
"activity_steps_today": {
|
||||||
|
"name": "Steps today"
|
||||||
|
},
|
||||||
|
"activity_distance_today": {
|
||||||
|
"name": "Distance travelled today"
|
||||||
|
},
|
||||||
|
"activity_floors_climbed_today": {
|
||||||
|
"name": "Floors climbed today"
|
||||||
|
},
|
||||||
|
"activity_soft_duration_today": {
|
||||||
|
"name": "Soft activity today"
|
||||||
|
},
|
||||||
|
"activity_moderate_duration_today": {
|
||||||
|
"name": "Moderate activity today"
|
||||||
|
},
|
||||||
|
"activity_intense_duration_today": {
|
||||||
|
"name": "Intense activity today"
|
||||||
|
},
|
||||||
|
"activity_active_duration_today": {
|
||||||
|
"name": "Active time today"
|
||||||
|
},
|
||||||
|
"activity_active_calories_burnt_today": {
|
||||||
|
"name": "Active calories burnt today"
|
||||||
|
},
|
||||||
|
"activity_total_calories_burnt_today": {
|
||||||
|
"name": "Total calories burnt today"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -5,7 +5,7 @@ from typing import Any
|
|||||||
from urllib.parse import urlparse
|
from urllib.parse import urlparse
|
||||||
|
|
||||||
from aiohttp.test_utils import TestClient
|
from aiohttp.test_utils import TestClient
|
||||||
from aiowithings import Goals, MeasurementGroup
|
from aiowithings import Activity, Goals, MeasurementGroup
|
||||||
from freezegun.api import FrozenDateTimeFactory
|
from freezegun.api import FrozenDateTimeFactory
|
||||||
|
|
||||||
from homeassistant.components.webhook import async_generate_url
|
from homeassistant.components.webhook import async_generate_url
|
||||||
@ -84,3 +84,11 @@ def load_measurements_fixture(
|
|||||||
"""Return measurement from fixture."""
|
"""Return measurement from fixture."""
|
||||||
meas_json = load_json_array_fixture(fixture)
|
meas_json = load_json_array_fixture(fixture)
|
||||||
return [MeasurementGroup.from_api(measurement) for measurement in meas_json]
|
return [MeasurementGroup.from_api(measurement) for measurement in meas_json]
|
||||||
|
|
||||||
|
|
||||||
|
def load_activity_fixture(
|
||||||
|
fixture: str = "withings/activity.json",
|
||||||
|
) -> list[Activity]:
|
||||||
|
"""Return measurement from fixture."""
|
||||||
|
activity_json = load_json_array_fixture(fixture)
|
||||||
|
return [Activity.from_api(activity) for activity in activity_json]
|
||||||
|
@ -16,7 +16,11 @@ from homeassistant.core import HomeAssistant
|
|||||||
from homeassistant.setup import async_setup_component
|
from homeassistant.setup import async_setup_component
|
||||||
|
|
||||||
from tests.common import MockConfigEntry, load_json_array_fixture
|
from tests.common import MockConfigEntry, load_json_array_fixture
|
||||||
from tests.components.withings import load_goals_fixture, load_measurements_fixture
|
from tests.components.withings import (
|
||||||
|
load_activity_fixture,
|
||||||
|
load_goals_fixture,
|
||||||
|
load_measurements_fixture,
|
||||||
|
)
|
||||||
|
|
||||||
CLIENT_ID = "1234"
|
CLIENT_ID = "1234"
|
||||||
CLIENT_SECRET = "5678"
|
CLIENT_SECRET = "5678"
|
||||||
@ -132,7 +136,7 @@ def mock_withings():
|
|||||||
devices_json = load_json_array_fixture("withings/devices.json")
|
devices_json = load_json_array_fixture("withings/devices.json")
|
||||||
devices = [Device.from_api(device) for device in devices_json]
|
devices = [Device.from_api(device) for device in devices_json]
|
||||||
|
|
||||||
measurement_groups = load_measurements_fixture("withings/measurements.json")
|
measurement_groups = load_measurements_fixture()
|
||||||
|
|
||||||
sleep_json = load_json_array_fixture("withings/sleep_summaries.json")
|
sleep_json = load_json_array_fixture("withings/sleep_summaries.json")
|
||||||
sleep_summaries = [
|
sleep_summaries = [
|
||||||
@ -144,12 +148,16 @@ def mock_withings():
|
|||||||
NotificationConfiguration.from_api(not_conf) for not_conf in notification_json
|
NotificationConfiguration.from_api(not_conf) for not_conf in notification_json
|
||||||
]
|
]
|
||||||
|
|
||||||
|
activities = load_activity_fixture()
|
||||||
|
|
||||||
mock = AsyncMock(spec=WithingsClient)
|
mock = AsyncMock(spec=WithingsClient)
|
||||||
mock.get_devices.return_value = devices
|
mock.get_devices.return_value = devices
|
||||||
mock.get_goals.return_value = load_goals_fixture("withings/goals.json")
|
mock.get_goals.return_value = load_goals_fixture()
|
||||||
mock.get_measurement_in_period.return_value = measurement_groups
|
mock.get_measurement_in_period.return_value = measurement_groups
|
||||||
mock.get_measurement_since.return_value = measurement_groups
|
mock.get_measurement_since.return_value = measurement_groups
|
||||||
mock.get_sleep_summary_since.return_value = sleep_summaries
|
mock.get_sleep_summary_since.return_value = sleep_summaries
|
||||||
|
mock.get_activities_since.return_value = activities
|
||||||
|
mock.get_activities_in_period.return_value = activities
|
||||||
mock.list_notification_configurations.return_value = notifications
|
mock.list_notification_configurations.return_value = notifications
|
||||||
|
|
||||||
with patch(
|
with patch(
|
||||||
|
282
tests/components/withings/fixtures/activity.json
Normal file
282
tests/components/withings/fixtures/activity.json
Normal file
@ -0,0 +1,282 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"steps": 1892,
|
||||||
|
"distance": 1607.93,
|
||||||
|
"elevation": 0,
|
||||||
|
"soft": 4981,
|
||||||
|
"moderate": 158,
|
||||||
|
"intense": 0,
|
||||||
|
"active": 158,
|
||||||
|
"calories": 204.796,
|
||||||
|
"totalcalories": 2454.481,
|
||||||
|
"deviceid": null,
|
||||||
|
"hash_deviceid": null,
|
||||||
|
"timezone": "Europe/Amsterdam",
|
||||||
|
"date": "2023-10-08",
|
||||||
|
"modified": 1697038118,
|
||||||
|
"brand": 18,
|
||||||
|
"modelid": 1055,
|
||||||
|
"model": "GoogleFit tracker",
|
||||||
|
"is_tracker": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"steps": 2576,
|
||||||
|
"distance": 2349.617,
|
||||||
|
"elevation": 0,
|
||||||
|
"soft": 1255,
|
||||||
|
"moderate": 1211,
|
||||||
|
"intense": 0,
|
||||||
|
"active": 1211,
|
||||||
|
"calories": 134.967,
|
||||||
|
"totalcalories": 2351.652,
|
||||||
|
"deviceid": null,
|
||||||
|
"hash_deviceid": null,
|
||||||
|
"timezone": "Europe/Amsterdam",
|
||||||
|
"date": "2023-10-09",
|
||||||
|
"modified": 1697038118,
|
||||||
|
"brand": 18,
|
||||||
|
"modelid": 1055,
|
||||||
|
"model": "GoogleFit tracker",
|
||||||
|
"is_tracker": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"steps": 1827,
|
||||||
|
"distance": 1595.537,
|
||||||
|
"elevation": 0,
|
||||||
|
"soft": 2194,
|
||||||
|
"moderate": 569,
|
||||||
|
"intense": 0,
|
||||||
|
"active": 569,
|
||||||
|
"calories": 110.223,
|
||||||
|
"totalcalories": 2313.98,
|
||||||
|
"deviceid": null,
|
||||||
|
"hash_deviceid": null,
|
||||||
|
"timezone": "Europe/Amsterdam",
|
||||||
|
"date": "2023-10-10",
|
||||||
|
"modified": 1697057517,
|
||||||
|
"brand": 18,
|
||||||
|
"modelid": 1055,
|
||||||
|
"model": "GoogleFit tracker",
|
||||||
|
"is_tracker": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"steps": 3801,
|
||||||
|
"distance": 3307.985,
|
||||||
|
"elevation": 0,
|
||||||
|
"soft": 5146,
|
||||||
|
"moderate": 963,
|
||||||
|
"intense": 0,
|
||||||
|
"active": 963,
|
||||||
|
"calories": 240.89,
|
||||||
|
"totalcalories": 2385.746,
|
||||||
|
"deviceid": null,
|
||||||
|
"hash_deviceid": null,
|
||||||
|
"timezone": "Europe/Amsterdam",
|
||||||
|
"date": "2023-10-11",
|
||||||
|
"modified": 1697842183,
|
||||||
|
"brand": 18,
|
||||||
|
"modelid": 1055,
|
||||||
|
"model": "GoogleFit tracker",
|
||||||
|
"is_tracker": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"steps": 2501,
|
||||||
|
"distance": 2158.186,
|
||||||
|
"elevation": 0,
|
||||||
|
"soft": 1854,
|
||||||
|
"moderate": 998,
|
||||||
|
"intense": 0,
|
||||||
|
"active": 998,
|
||||||
|
"calories": 113.123,
|
||||||
|
"totalcalories": 2317.396,
|
||||||
|
"deviceid": null,
|
||||||
|
"hash_deviceid": null,
|
||||||
|
"timezone": "Europe/Amsterdam",
|
||||||
|
"date": "2023-10-12",
|
||||||
|
"modified": 1697842183,
|
||||||
|
"brand": 18,
|
||||||
|
"modelid": 1055,
|
||||||
|
"model": "GoogleFit tracker",
|
||||||
|
"is_tracker": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"steps": 6787,
|
||||||
|
"distance": 6008.779,
|
||||||
|
"elevation": 0,
|
||||||
|
"soft": 3773,
|
||||||
|
"moderate": 2831,
|
||||||
|
"intense": 36,
|
||||||
|
"active": 2867,
|
||||||
|
"calories": 263.371,
|
||||||
|
"totalcalories": 2380.669,
|
||||||
|
"deviceid": null,
|
||||||
|
"hash_deviceid": null,
|
||||||
|
"timezone": "Europe/Amsterdam",
|
||||||
|
"date": "2023-10-13",
|
||||||
|
"modified": 1697842183,
|
||||||
|
"brand": 18,
|
||||||
|
"modelid": 1055,
|
||||||
|
"model": "GoogleFit tracker",
|
||||||
|
"is_tracker": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"steps": 1232,
|
||||||
|
"distance": 1050.925,
|
||||||
|
"elevation": 0,
|
||||||
|
"soft": 2950,
|
||||||
|
"moderate": 196,
|
||||||
|
"intense": 0,
|
||||||
|
"active": 196,
|
||||||
|
"calories": 124.754,
|
||||||
|
"totalcalories": 2311.674,
|
||||||
|
"deviceid": null,
|
||||||
|
"hash_deviceid": null,
|
||||||
|
"timezone": "Europe/Amsterdam",
|
||||||
|
"date": "2023-10-14",
|
||||||
|
"modified": 1697842183,
|
||||||
|
"brand": 18,
|
||||||
|
"modelid": 1055,
|
||||||
|
"model": "GoogleFit tracker",
|
||||||
|
"is_tracker": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"steps": 851,
|
||||||
|
"distance": 723.139,
|
||||||
|
"elevation": 0,
|
||||||
|
"soft": 1634,
|
||||||
|
"moderate": 83,
|
||||||
|
"intense": 0,
|
||||||
|
"active": 83,
|
||||||
|
"calories": 68.121,
|
||||||
|
"totalcalories": 2294.325,
|
||||||
|
"deviceid": null,
|
||||||
|
"hash_deviceid": null,
|
||||||
|
"timezone": "Europe/Amsterdam",
|
||||||
|
"date": "2023-10-15",
|
||||||
|
"modified": 1697842184,
|
||||||
|
"brand": 18,
|
||||||
|
"modelid": 1055,
|
||||||
|
"model": "GoogleFit tracker",
|
||||||
|
"is_tracker": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"steps": 654,
|
||||||
|
"distance": 557.509,
|
||||||
|
"elevation": 0,
|
||||||
|
"soft": 1558,
|
||||||
|
"moderate": 124,
|
||||||
|
"intense": 0,
|
||||||
|
"active": 124,
|
||||||
|
"calories": 66.707,
|
||||||
|
"totalcalories": 2292.897,
|
||||||
|
"deviceid": null,
|
||||||
|
"hash_deviceid": null,
|
||||||
|
"timezone": "Europe/Amsterdam",
|
||||||
|
"date": "2023-10-16",
|
||||||
|
"modified": 1697842184,
|
||||||
|
"brand": 18,
|
||||||
|
"modelid": 1055,
|
||||||
|
"model": "GoogleFit tracker",
|
||||||
|
"is_tracker": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"steps": 566,
|
||||||
|
"distance": 482.185,
|
||||||
|
"elevation": 0,
|
||||||
|
"soft": 1085,
|
||||||
|
"moderate": 52,
|
||||||
|
"intense": 0,
|
||||||
|
"active": 52,
|
||||||
|
"calories": 45.126,
|
||||||
|
"totalcalories": 2287.08,
|
||||||
|
"deviceid": null,
|
||||||
|
"hash_deviceid": null,
|
||||||
|
"timezone": "Europe/Amsterdam",
|
||||||
|
"date": "2023-10-17",
|
||||||
|
"modified": 1697842184,
|
||||||
|
"brand": 18,
|
||||||
|
"modelid": 1055,
|
||||||
|
"model": "GoogleFit tracker",
|
||||||
|
"is_tracker": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"steps": 2204,
|
||||||
|
"distance": 1901.651,
|
||||||
|
"elevation": 0,
|
||||||
|
"soft": 1393,
|
||||||
|
"moderate": 941,
|
||||||
|
"intense": 0,
|
||||||
|
"active": 941,
|
||||||
|
"calories": 92.585,
|
||||||
|
"totalcalories": 2302.971,
|
||||||
|
"deviceid": null,
|
||||||
|
"hash_deviceid": null,
|
||||||
|
"timezone": "Europe/Amsterdam",
|
||||||
|
"date": "2023-10-18",
|
||||||
|
"modified": 1697842185,
|
||||||
|
"brand": 18,
|
||||||
|
"modelid": 1055,
|
||||||
|
"model": "GoogleFit tracker",
|
||||||
|
"is_tracker": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"steps": 95,
|
||||||
|
"distance": 80.63,
|
||||||
|
"elevation": 0,
|
||||||
|
"soft": 543,
|
||||||
|
"moderate": 0,
|
||||||
|
"intense": 0,
|
||||||
|
"active": 0,
|
||||||
|
"calories": 21.541,
|
||||||
|
"totalcalories": 2277.668,
|
||||||
|
"deviceid": null,
|
||||||
|
"hash_deviceid": null,
|
||||||
|
"timezone": "Europe/Amsterdam",
|
||||||
|
"date": "2023-10-19",
|
||||||
|
"modified": 1697842185,
|
||||||
|
"brand": 18,
|
||||||
|
"modelid": 1055,
|
||||||
|
"model": "GoogleFit tracker",
|
||||||
|
"is_tracker": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"steps": 1209,
|
||||||
|
"distance": 1028.559,
|
||||||
|
"elevation": 0,
|
||||||
|
"soft": 1864,
|
||||||
|
"moderate": 292,
|
||||||
|
"intense": 0,
|
||||||
|
"active": 292,
|
||||||
|
"calories": 85.497,
|
||||||
|
"totalcalories": 2303.788,
|
||||||
|
"deviceid": null,
|
||||||
|
"hash_deviceid": null,
|
||||||
|
"timezone": "Europe/Amsterdam",
|
||||||
|
"date": "2023-10-20",
|
||||||
|
"modified": 1697884856,
|
||||||
|
"brand": 18,
|
||||||
|
"modelid": 1055,
|
||||||
|
"model": "GoogleFit tracker",
|
||||||
|
"is_tracker": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"steps": 1155,
|
||||||
|
"distance": 1020.121,
|
||||||
|
"elevation": 0,
|
||||||
|
"soft": 1516,
|
||||||
|
"moderate": 1487,
|
||||||
|
"intense": 420,
|
||||||
|
"active": 1907,
|
||||||
|
"calories": 221.132,
|
||||||
|
"totalcalories": 2444.149,
|
||||||
|
"deviceid": null,
|
||||||
|
"hash_deviceid": null,
|
||||||
|
"timezone": "Europe/Amsterdam",
|
||||||
|
"date": "2023-10-21",
|
||||||
|
"modified": 1697888004,
|
||||||
|
"brand": 18,
|
||||||
|
"modelid": 1055,
|
||||||
|
"model": "GoogleFit tracker",
|
||||||
|
"is_tracker": false
|
||||||
|
}
|
||||||
|
]
|
@ -1,4 +1,35 @@
|
|||||||
# serializer version: 1
|
# serializer version: 1
|
||||||
|
# name: test_all_entities[sensor.henk_active_calories_burnt_today]
|
||||||
|
StateSnapshot({
|
||||||
|
'attributes': ReadOnlyDict({
|
||||||
|
'friendly_name': 'henk Active calories burnt today',
|
||||||
|
'last_reset': '2023-10-20T00:00:00-07:00',
|
||||||
|
'state_class': <SensorStateClass.TOTAL: 'total'>,
|
||||||
|
'unit_of_measurement': 'Calories',
|
||||||
|
}),
|
||||||
|
'context': <ANY>,
|
||||||
|
'entity_id': 'sensor.henk_active_calories_burnt_today',
|
||||||
|
'last_changed': <ANY>,
|
||||||
|
'last_updated': <ANY>,
|
||||||
|
'state': '221.132',
|
||||||
|
})
|
||||||
|
# ---
|
||||||
|
# name: test_all_entities[sensor.henk_active_time_today]
|
||||||
|
StateSnapshot({
|
||||||
|
'attributes': ReadOnlyDict({
|
||||||
|
'device_class': 'duration',
|
||||||
|
'friendly_name': 'henk Active time today',
|
||||||
|
'last_reset': '2023-10-20T00:00:00-07:00',
|
||||||
|
'state_class': <SensorStateClass.TOTAL: 'total'>,
|
||||||
|
'unit_of_measurement': <UnitOfTime.SECONDS: 's'>,
|
||||||
|
}),
|
||||||
|
'context': <ANY>,
|
||||||
|
'entity_id': 'sensor.henk_active_time_today',
|
||||||
|
'last_changed': <ANY>,
|
||||||
|
'last_updated': <ANY>,
|
||||||
|
'state': '1907',
|
||||||
|
})
|
||||||
|
# ---
|
||||||
# name: test_all_entities[sensor.henk_average_heart_rate]
|
# name: test_all_entities[sensor.henk_average_heart_rate]
|
||||||
StateSnapshot({
|
StateSnapshot({
|
||||||
'attributes': ReadOnlyDict({
|
'attributes': ReadOnlyDict({
|
||||||
@ -102,6 +133,23 @@
|
|||||||
'state': '70',
|
'state': '70',
|
||||||
})
|
})
|
||||||
# ---
|
# ---
|
||||||
|
# name: test_all_entities[sensor.henk_distance_travelled_today]
|
||||||
|
StateSnapshot({
|
||||||
|
'attributes': ReadOnlyDict({
|
||||||
|
'device_class': 'distance',
|
||||||
|
'friendly_name': 'henk Distance travelled today',
|
||||||
|
'icon': 'mdi:map-marker-distance',
|
||||||
|
'last_reset': '2023-10-20T00:00:00-07:00',
|
||||||
|
'state_class': <SensorStateClass.TOTAL: 'total'>,
|
||||||
|
'unit_of_measurement': <UnitOfLength.METERS: 'm'>,
|
||||||
|
}),
|
||||||
|
'context': <ANY>,
|
||||||
|
'entity_id': 'sensor.henk_distance_travelled_today',
|
||||||
|
'last_changed': <ANY>,
|
||||||
|
'last_updated': <ANY>,
|
||||||
|
'state': '1020.121',
|
||||||
|
})
|
||||||
|
# ---
|
||||||
# name: test_all_entities[sensor.henk_extracellular_water]
|
# name: test_all_entities[sensor.henk_extracellular_water]
|
||||||
StateSnapshot({
|
StateSnapshot({
|
||||||
'attributes': ReadOnlyDict({
|
'attributes': ReadOnlyDict({
|
||||||
@ -161,6 +209,22 @@
|
|||||||
'state': '0.07',
|
'state': '0.07',
|
||||||
})
|
})
|
||||||
# ---
|
# ---
|
||||||
|
# name: test_all_entities[sensor.henk_floors_climbed_today]
|
||||||
|
StateSnapshot({
|
||||||
|
'attributes': ReadOnlyDict({
|
||||||
|
'friendly_name': 'henk Floors climbed today',
|
||||||
|
'icon': 'mdi:stairs-up',
|
||||||
|
'last_reset': '2023-10-20T00:00:00-07:00',
|
||||||
|
'state_class': <SensorStateClass.TOTAL: 'total'>,
|
||||||
|
'unit_of_measurement': 'Floors',
|
||||||
|
}),
|
||||||
|
'context': <ANY>,
|
||||||
|
'entity_id': 'sensor.henk_floors_climbed_today',
|
||||||
|
'last_changed': <ANY>,
|
||||||
|
'last_updated': <ANY>,
|
||||||
|
'state': '0',
|
||||||
|
})
|
||||||
|
# ---
|
||||||
# name: test_all_entities[sensor.henk_heart_pulse]
|
# name: test_all_entities[sensor.henk_heart_pulse]
|
||||||
StateSnapshot({
|
StateSnapshot({
|
||||||
'attributes': ReadOnlyDict({
|
'attributes': ReadOnlyDict({
|
||||||
@ -207,6 +271,22 @@
|
|||||||
'state': '0.95',
|
'state': '0.95',
|
||||||
})
|
})
|
||||||
# ---
|
# ---
|
||||||
|
# name: test_all_entities[sensor.henk_intense_activity_today]
|
||||||
|
StateSnapshot({
|
||||||
|
'attributes': ReadOnlyDict({
|
||||||
|
'device_class': 'duration',
|
||||||
|
'friendly_name': 'henk Intense activity today',
|
||||||
|
'last_reset': '2023-10-20T00:00:00-07:00',
|
||||||
|
'state_class': <SensorStateClass.TOTAL: 'total'>,
|
||||||
|
'unit_of_measurement': <UnitOfTime.SECONDS: 's'>,
|
||||||
|
}),
|
||||||
|
'context': <ANY>,
|
||||||
|
'entity_id': 'sensor.henk_intense_activity_today',
|
||||||
|
'last_changed': <ANY>,
|
||||||
|
'last_updated': <ANY>,
|
||||||
|
'state': '420',
|
||||||
|
})
|
||||||
|
# ---
|
||||||
# name: test_all_entities[sensor.henk_intracellular_water]
|
# name: test_all_entities[sensor.henk_intracellular_water]
|
||||||
StateSnapshot({
|
StateSnapshot({
|
||||||
'attributes': ReadOnlyDict({
|
'attributes': ReadOnlyDict({
|
||||||
@ -296,6 +376,22 @@
|
|||||||
'state': '10',
|
'state': '10',
|
||||||
})
|
})
|
||||||
# ---
|
# ---
|
||||||
|
# name: test_all_entities[sensor.henk_moderate_activity_today]
|
||||||
|
StateSnapshot({
|
||||||
|
'attributes': ReadOnlyDict({
|
||||||
|
'device_class': 'duration',
|
||||||
|
'friendly_name': 'henk Moderate activity today',
|
||||||
|
'last_reset': '2023-10-20T00:00:00-07:00',
|
||||||
|
'state_class': <SensorStateClass.TOTAL: 'total'>,
|
||||||
|
'unit_of_measurement': <UnitOfTime.SECONDS: 's'>,
|
||||||
|
}),
|
||||||
|
'context': <ANY>,
|
||||||
|
'entity_id': 'sensor.henk_moderate_activity_today',
|
||||||
|
'last_changed': <ANY>,
|
||||||
|
'last_updated': <ANY>,
|
||||||
|
'state': '1487',
|
||||||
|
})
|
||||||
|
# ---
|
||||||
# name: test_all_entities[sensor.henk_muscle_mass]
|
# name: test_all_entities[sensor.henk_muscle_mass]
|
||||||
StateSnapshot({
|
StateSnapshot({
|
||||||
'attributes': ReadOnlyDict({
|
'attributes': ReadOnlyDict({
|
||||||
@ -414,6 +510,22 @@
|
|||||||
'state': '87',
|
'state': '87',
|
||||||
})
|
})
|
||||||
# ---
|
# ---
|
||||||
|
# name: test_all_entities[sensor.henk_soft_activity_today]
|
||||||
|
StateSnapshot({
|
||||||
|
'attributes': ReadOnlyDict({
|
||||||
|
'device_class': 'duration',
|
||||||
|
'friendly_name': 'henk Soft activity today',
|
||||||
|
'last_reset': '2023-10-20T00:00:00-07:00',
|
||||||
|
'state_class': <SensorStateClass.TOTAL: 'total'>,
|
||||||
|
'unit_of_measurement': <UnitOfTime.SECONDS: 's'>,
|
||||||
|
}),
|
||||||
|
'context': <ANY>,
|
||||||
|
'entity_id': 'sensor.henk_soft_activity_today',
|
||||||
|
'last_changed': <ANY>,
|
||||||
|
'last_updated': <ANY>,
|
||||||
|
'state': '1516',
|
||||||
|
})
|
||||||
|
# ---
|
||||||
# name: test_all_entities[sensor.henk_spo2]
|
# name: test_all_entities[sensor.henk_spo2]
|
||||||
StateSnapshot({
|
StateSnapshot({
|
||||||
'attributes': ReadOnlyDict({
|
'attributes': ReadOnlyDict({
|
||||||
@ -443,6 +555,22 @@
|
|||||||
'state': '10000',
|
'state': '10000',
|
||||||
})
|
})
|
||||||
# ---
|
# ---
|
||||||
|
# name: test_all_entities[sensor.henk_steps_today]
|
||||||
|
StateSnapshot({
|
||||||
|
'attributes': ReadOnlyDict({
|
||||||
|
'friendly_name': 'henk Steps today',
|
||||||
|
'icon': 'mdi:shoe-print',
|
||||||
|
'last_reset': '2023-10-20T00:00:00-07:00',
|
||||||
|
'state_class': <SensorStateClass.TOTAL: 'total'>,
|
||||||
|
'unit_of_measurement': 'Steps',
|
||||||
|
}),
|
||||||
|
'context': <ANY>,
|
||||||
|
'entity_id': 'sensor.henk_steps_today',
|
||||||
|
'last_changed': <ANY>,
|
||||||
|
'last_updated': <ANY>,
|
||||||
|
'state': '1155',
|
||||||
|
})
|
||||||
|
# ---
|
||||||
# name: test_all_entities[sensor.henk_systolic_blood_pressure]
|
# name: test_all_entities[sensor.henk_systolic_blood_pressure]
|
||||||
StateSnapshot({
|
StateSnapshot({
|
||||||
'attributes': ReadOnlyDict({
|
'attributes': ReadOnlyDict({
|
||||||
@ -504,6 +632,21 @@
|
|||||||
'state': '996',
|
'state': '996',
|
||||||
})
|
})
|
||||||
# ---
|
# ---
|
||||||
|
# name: test_all_entities[sensor.henk_total_calories_burnt_today]
|
||||||
|
StateSnapshot({
|
||||||
|
'attributes': ReadOnlyDict({
|
||||||
|
'friendly_name': 'henk Total calories burnt today',
|
||||||
|
'last_reset': '2023-10-20T00:00:00-07:00',
|
||||||
|
'state_class': <SensorStateClass.TOTAL: 'total'>,
|
||||||
|
'unit_of_measurement': 'Calories',
|
||||||
|
}),
|
||||||
|
'context': <ANY>,
|
||||||
|
'entity_id': 'sensor.henk_total_calories_burnt_today',
|
||||||
|
'last_changed': <ANY>,
|
||||||
|
'last_updated': <ANY>,
|
||||||
|
'state': '2444.149',
|
||||||
|
})
|
||||||
|
# ---
|
||||||
# name: test_all_entities[sensor.henk_vascular_age]
|
# name: test_all_entities[sensor.henk_vascular_age]
|
||||||
StateSnapshot({
|
StateSnapshot({
|
||||||
'attributes': ReadOnlyDict({
|
'attributes': ReadOnlyDict({
|
||||||
|
@ -6,15 +6,21 @@ from freezegun.api import FrozenDateTimeFactory
|
|||||||
import pytest
|
import pytest
|
||||||
from syrupy import SnapshotAssertion
|
from syrupy import SnapshotAssertion
|
||||||
|
|
||||||
from homeassistant.const import STATE_UNAVAILABLE, Platform
|
from homeassistant.const import STATE_UNAVAILABLE, STATE_UNKNOWN, Platform
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.helpers import entity_registry as er
|
from homeassistant.helpers import entity_registry as er
|
||||||
|
|
||||||
from . import load_goals_fixture, load_measurements_fixture, setup_integration
|
from . import (
|
||||||
|
load_activity_fixture,
|
||||||
|
load_goals_fixture,
|
||||||
|
load_measurements_fixture,
|
||||||
|
setup_integration,
|
||||||
|
)
|
||||||
|
|
||||||
from tests.common import MockConfigEntry, async_fire_time_changed
|
from tests.common import MockConfigEntry, async_fire_time_changed
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.freeze_time("2023-10-21")
|
||||||
@pytest.mark.usefixtures("entity_registry_enabled_by_default")
|
@pytest.mark.usefixtures("entity_registry_enabled_by_default")
|
||||||
async def test_all_entities(
|
async def test_all_entities(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
@ -115,9 +121,7 @@ async def test_update_new_measurement_creates_new_sensor(
|
|||||||
|
|
||||||
assert hass.states.get("sensor.henk_fat_mass") is None
|
assert hass.states.get("sensor.henk_fat_mass") is None
|
||||||
|
|
||||||
withings.get_measurement_in_period.return_value = load_measurements_fixture(
|
withings.get_measurement_in_period.return_value = load_measurements_fixture()
|
||||||
"withings/measurements.json"
|
|
||||||
)
|
|
||||||
|
|
||||||
freezer.tick(timedelta(minutes=10))
|
freezer.tick(timedelta(minutes=10))
|
||||||
async_fire_time_changed(hass)
|
async_fire_time_changed(hass)
|
||||||
@ -141,10 +145,101 @@ async def test_update_new_goals_creates_new_sensor(
|
|||||||
assert hass.states.get("sensor.henk_step_goal") is None
|
assert hass.states.get("sensor.henk_step_goal") is None
|
||||||
assert hass.states.get("sensor.henk_weight_goal") is not None
|
assert hass.states.get("sensor.henk_weight_goal") is not None
|
||||||
|
|
||||||
withings.get_goals.return_value = load_goals_fixture("withings/goals.json")
|
withings.get_goals.return_value = load_goals_fixture()
|
||||||
|
|
||||||
freezer.tick(timedelta(hours=1))
|
freezer.tick(timedelta(hours=1))
|
||||||
async_fire_time_changed(hass)
|
async_fire_time_changed(hass)
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
assert hass.states.get("sensor.henk_step_goal") is not None
|
assert hass.states.get("sensor.henk_step_goal") is not None
|
||||||
|
|
||||||
|
|
||||||
|
async def test_activity_sensors_unknown_next_day(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
withings: AsyncMock,
|
||||||
|
polling_config_entry: MockConfigEntry,
|
||||||
|
freezer: FrozenDateTimeFactory,
|
||||||
|
) -> None:
|
||||||
|
"""Test activity sensors will return unknown the next day."""
|
||||||
|
freezer.move_to("2023-10-21")
|
||||||
|
await setup_integration(hass, polling_config_entry, False)
|
||||||
|
|
||||||
|
assert hass.states.get("sensor.henk_steps_today") is not None
|
||||||
|
|
||||||
|
withings.get_activities_since.return_value = []
|
||||||
|
|
||||||
|
freezer.tick(timedelta(days=1))
|
||||||
|
async_fire_time_changed(hass)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
assert hass.states.get("sensor.henk_steps_today").state == STATE_UNKNOWN
|
||||||
|
|
||||||
|
|
||||||
|
async def test_activity_sensors_same_result_same_day(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
withings: AsyncMock,
|
||||||
|
polling_config_entry: MockConfigEntry,
|
||||||
|
freezer: FrozenDateTimeFactory,
|
||||||
|
) -> None:
|
||||||
|
"""Test activity sensors will return the same result if old data is updated."""
|
||||||
|
freezer.move_to("2023-10-21")
|
||||||
|
await setup_integration(hass, polling_config_entry, False)
|
||||||
|
|
||||||
|
assert hass.states.get("sensor.henk_steps_today").state == "1155"
|
||||||
|
|
||||||
|
withings.get_activities_since.return_value = []
|
||||||
|
|
||||||
|
freezer.tick(timedelta(hours=2))
|
||||||
|
async_fire_time_changed(hass)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
assert hass.states.get("sensor.henk_steps_today").state == "1155"
|
||||||
|
|
||||||
|
|
||||||
|
async def test_activity_sensors_created_when_existed(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
withings: AsyncMock,
|
||||||
|
polling_config_entry: MockConfigEntry,
|
||||||
|
freezer: FrozenDateTimeFactory,
|
||||||
|
) -> None:
|
||||||
|
"""Test activity sensors will be added if they existed before."""
|
||||||
|
freezer.move_to("2023-10-21")
|
||||||
|
await setup_integration(hass, polling_config_entry, False)
|
||||||
|
|
||||||
|
assert hass.states.get("sensor.henk_steps_today") is not None
|
||||||
|
assert hass.states.get("sensor.henk_steps_today").state != STATE_UNKNOWN
|
||||||
|
|
||||||
|
withings.get_activities_in_period.return_value = []
|
||||||
|
|
||||||
|
await hass.config_entries.async_reload(polling_config_entry.entry_id)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
assert hass.states.get("sensor.henk_steps_today").state == STATE_UNKNOWN
|
||||||
|
|
||||||
|
|
||||||
|
async def test_activity_sensors_created_when_receive_activity_data(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
withings: AsyncMock,
|
||||||
|
polling_config_entry: MockConfigEntry,
|
||||||
|
freezer: FrozenDateTimeFactory,
|
||||||
|
) -> None:
|
||||||
|
"""Test activity sensors will be added if we receive activity data."""
|
||||||
|
freezer.move_to("2023-10-21")
|
||||||
|
withings.get_activities_in_period.return_value = []
|
||||||
|
await setup_integration(hass, polling_config_entry, False)
|
||||||
|
|
||||||
|
assert hass.states.get("sensor.henk_steps_today") is None
|
||||||
|
|
||||||
|
freezer.tick(timedelta(minutes=10))
|
||||||
|
async_fire_time_changed(hass)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
assert hass.states.get("sensor.henk_steps_today") is None
|
||||||
|
|
||||||
|
withings.get_activities_in_period.return_value = load_activity_fixture()
|
||||||
|
|
||||||
|
freezer.tick(timedelta(minutes=10))
|
||||||
|
async_fire_time_changed(hass)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
assert hass.states.get("sensor.henk_steps_today") is not None
|
||||||
|
Loading…
x
Reference in New Issue
Block a user