Add last workout sensors to Withings (#102541)
Co-authored-by: J. Nick Koston <nick@koston.org> Co-authored-by: Robert Resch <robert@resch.dev>
This commit is contained in:
parent
57a10a2e0d
commit
b42c47e800
9 changed files with 793 additions and 16 deletions
|
@ -59,6 +59,7 @@ from .coordinator import (
|
|||
WithingsGoalsDataUpdateCoordinator,
|
||||
WithingsMeasurementDataUpdateCoordinator,
|
||||
WithingsSleepDataUpdateCoordinator,
|
||||
WithingsWorkoutDataUpdateCoordinator,
|
||||
)
|
||||
|
||||
PLATFORMS = [Platform.BINARY_SENSOR, Platform.SENSOR]
|
||||
|
@ -133,6 +134,7 @@ class WithingsData:
|
|||
bed_presence_coordinator: WithingsBedPresenceDataUpdateCoordinator
|
||||
goals_coordinator: WithingsGoalsDataUpdateCoordinator
|
||||
activity_coordinator: WithingsActivityDataUpdateCoordinator
|
||||
workout_coordinator: WithingsWorkoutDataUpdateCoordinator
|
||||
coordinators: set[WithingsDataUpdateCoordinator] = field(default_factory=set)
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
|
@ -143,6 +145,7 @@ class WithingsData:
|
|||
self.bed_presence_coordinator,
|
||||
self.goals_coordinator,
|
||||
self.activity_coordinator,
|
||||
self.workout_coordinator,
|
||||
}
|
||||
|
||||
|
||||
|
@ -176,6 +179,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
|||
bed_presence_coordinator=WithingsBedPresenceDataUpdateCoordinator(hass, client),
|
||||
goals_coordinator=WithingsGoalsDataUpdateCoordinator(hass, client),
|
||||
activity_coordinator=WithingsActivityDataUpdateCoordinator(hass, client),
|
||||
workout_coordinator=WithingsWorkoutDataUpdateCoordinator(hass, client),
|
||||
)
|
||||
|
||||
for coordinator in withings_data.coordinators:
|
||||
|
|
|
@ -13,6 +13,7 @@ from aiowithings import (
|
|||
WithingsAuthenticationFailedError,
|
||||
WithingsClient,
|
||||
WithingsUnauthorizedError,
|
||||
Workout,
|
||||
aggregate_measurements,
|
||||
)
|
||||
|
||||
|
@ -224,3 +225,39 @@ class WithingsActivityDataUpdateCoordinator(
|
|||
if self._previous_data and self._previous_data.date == today:
|
||||
return self._previous_data
|
||||
return None
|
||||
|
||||
|
||||
class WithingsWorkoutDataUpdateCoordinator(
|
||||
WithingsDataUpdateCoordinator[Workout | None]
|
||||
):
|
||||
"""Withings workout coordinator."""
|
||||
|
||||
_previous_data: Workout | 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) -> Workout | None:
|
||||
"""Retrieve latest workout."""
|
||||
if self._last_valid_update is None:
|
||||
now = dt_util.utcnow()
|
||||
startdate = now - timedelta(days=14)
|
||||
workouts = await self._client.get_workouts_in_period(
|
||||
startdate.date(), now.date()
|
||||
)
|
||||
else:
|
||||
workouts = await self._client.get_workouts_since(self._last_valid_update)
|
||||
if not workouts:
|
||||
return self._previous_data
|
||||
latest_workout = max(workouts, key=lambda workout: workout.end_date)
|
||||
if (
|
||||
self._previous_data is None
|
||||
or self._previous_data.end_date >= latest_workout.end_date
|
||||
):
|
||||
self._previous_data = latest_workout
|
||||
self._last_valid_update = latest_workout.end_date
|
||||
return self._previous_data
|
||||
|
|
|
@ -5,7 +5,14 @@ from collections.abc import Callable
|
|||
from dataclasses import dataclass
|
||||
from datetime import datetime
|
||||
|
||||
from aiowithings import Activity, Goals, MeasurementType, SleepSummary
|
||||
from aiowithings import (
|
||||
Activity,
|
||||
Goals,
|
||||
MeasurementType,
|
||||
SleepSummary,
|
||||
Workout,
|
||||
WorkoutCategory,
|
||||
)
|
||||
|
||||
from homeassistant.components.sensor import (
|
||||
SensorDeviceClass,
|
||||
|
@ -44,6 +51,7 @@ from .coordinator import (
|
|||
WithingsGoalsDataUpdateCoordinator,
|
||||
WithingsMeasurementDataUpdateCoordinator,
|
||||
WithingsSleepDataUpdateCoordinator,
|
||||
WithingsWorkoutDataUpdateCoordinator,
|
||||
)
|
||||
from .entity import WithingsEntity
|
||||
|
||||
|
@ -420,7 +428,7 @@ ACTIVITY_SENSORS = [
|
|||
value_fn=lambda activity: activity.steps,
|
||||
translation_key="activity_steps_today",
|
||||
icon="mdi:shoe-print",
|
||||
native_unit_of_measurement="Steps",
|
||||
native_unit_of_measurement="steps",
|
||||
state_class=SensorStateClass.TOTAL,
|
||||
),
|
||||
WithingsActivitySensorEntityDescription(
|
||||
|
@ -438,7 +446,7 @@ ACTIVITY_SENSORS = [
|
|||
value_fn=lambda activity: activity.floors_climbed,
|
||||
translation_key="activity_floors_climbed_today",
|
||||
icon="mdi:stairs-up",
|
||||
native_unit_of_measurement="Floors",
|
||||
native_unit_of_measurement="floors",
|
||||
state_class=SensorStateClass.TOTAL,
|
||||
),
|
||||
WithingsActivitySensorEntityDescription(
|
||||
|
@ -485,7 +493,7 @@ ACTIVITY_SENSORS = [
|
|||
value_fn=lambda activity: activity.active_calories_burnt,
|
||||
suggested_display_precision=1,
|
||||
translation_key="activity_active_calories_burnt_today",
|
||||
native_unit_of_measurement="Calories",
|
||||
native_unit_of_measurement="calories",
|
||||
state_class=SensorStateClass.TOTAL,
|
||||
),
|
||||
WithingsActivitySensorEntityDescription(
|
||||
|
@ -493,7 +501,7 @@ ACTIVITY_SENSORS = [
|
|||
value_fn=lambda activity: activity.total_calories_burnt,
|
||||
suggested_display_precision=1,
|
||||
translation_key="activity_total_calories_burnt_today",
|
||||
native_unit_of_measurement="Calories",
|
||||
native_unit_of_measurement="calories",
|
||||
state_class=SensorStateClass.TOTAL,
|
||||
),
|
||||
]
|
||||
|
@ -524,7 +532,7 @@ GOALS_SENSORS: dict[str, WithingsGoalsSensorEntityDescription] = {
|
|||
value_fn=lambda goals: goals.steps,
|
||||
icon="mdi:shoe-print",
|
||||
translation_key="step_goal",
|
||||
native_unit_of_measurement="Steps",
|
||||
native_unit_of_measurement="steps",
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
SLEEP_GOAL: WithingsGoalsSensorEntityDescription(
|
||||
|
@ -548,6 +556,84 @@ GOALS_SENSORS: dict[str, WithingsGoalsSensorEntityDescription] = {
|
|||
}
|
||||
|
||||
|
||||
@dataclass
|
||||
class WithingsWorkoutSensorEntityDescriptionMixin:
|
||||
"""Mixin for describing withings data."""
|
||||
|
||||
value_fn: Callable[[Workout], StateType]
|
||||
|
||||
|
||||
@dataclass
|
||||
class WithingsWorkoutSensorEntityDescription(
|
||||
SensorEntityDescription, WithingsWorkoutSensorEntityDescriptionMixin
|
||||
):
|
||||
"""Immutable class for describing withings data."""
|
||||
|
||||
|
||||
_WORKOUT_CATEGORY = [
|
||||
workout_category.name.lower() for workout_category in WorkoutCategory
|
||||
]
|
||||
|
||||
|
||||
WORKOUT_SENSORS = [
|
||||
WithingsWorkoutSensorEntityDescription(
|
||||
key="workout_type",
|
||||
value_fn=lambda workout: workout.category.name.lower(),
|
||||
device_class=SensorDeviceClass.ENUM,
|
||||
translation_key="workout_type",
|
||||
options=_WORKOUT_CATEGORY,
|
||||
),
|
||||
WithingsWorkoutSensorEntityDescription(
|
||||
key="workout_active_calories_burnt",
|
||||
value_fn=lambda workout: workout.active_calories_burnt,
|
||||
translation_key="workout_active_calories_burnt",
|
||||
suggested_display_precision=1,
|
||||
native_unit_of_measurement="calories",
|
||||
),
|
||||
WithingsWorkoutSensorEntityDescription(
|
||||
key="workout_distance",
|
||||
value_fn=lambda workout: workout.distance,
|
||||
translation_key="workout_distance",
|
||||
device_class=SensorDeviceClass.DISTANCE,
|
||||
native_unit_of_measurement=UnitOfLength.METERS,
|
||||
suggested_display_precision=0,
|
||||
icon="mdi:map-marker-distance",
|
||||
),
|
||||
WithingsWorkoutSensorEntityDescription(
|
||||
key="workout_floors_climbed",
|
||||
value_fn=lambda workout: workout.floors_climbed,
|
||||
translation_key="workout_floors_climbed",
|
||||
icon="mdi:stairs-up",
|
||||
native_unit_of_measurement="floors",
|
||||
),
|
||||
WithingsWorkoutSensorEntityDescription(
|
||||
key="workout_intensity",
|
||||
value_fn=lambda workout: workout.intensity,
|
||||
translation_key="workout_intensity",
|
||||
),
|
||||
WithingsWorkoutSensorEntityDescription(
|
||||
key="workout_pause_duration",
|
||||
value_fn=lambda workout: workout.pause_duration or 0,
|
||||
translation_key="workout_pause_duration",
|
||||
icon="mdi:timer-pause",
|
||||
device_class=SensorDeviceClass.DURATION,
|
||||
native_unit_of_measurement=UnitOfTime.SECONDS,
|
||||
suggested_unit_of_measurement=UnitOfTime.MINUTES,
|
||||
),
|
||||
WithingsWorkoutSensorEntityDescription(
|
||||
key="workout_duration",
|
||||
value_fn=lambda workout: (
|
||||
workout.end_date - workout.start_date
|
||||
).total_seconds(),
|
||||
translation_key="workout_duration",
|
||||
icon="mdi:timer",
|
||||
device_class=SensorDeviceClass.DURATION,
|
||||
native_unit_of_measurement=UnitOfTime.SECONDS,
|
||||
suggested_unit_of_measurement=UnitOfTime.MINUTES,
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
def get_current_goals(goals: Goals) -> set[str]:
|
||||
"""Return a list of present goals."""
|
||||
result = set()
|
||||
|
@ -656,7 +742,7 @@ async def async_setup_entry(
|
|||
for attribute in SLEEP_SENSORS
|
||||
)
|
||||
else:
|
||||
remove_listener: Callable[[], None]
|
||||
remove_sleep_listener: Callable[[], None]
|
||||
|
||||
def _async_add_sleep_entities() -> None:
|
||||
"""Add sleep entities."""
|
||||
|
@ -665,12 +751,39 @@ async def async_setup_entry(
|
|||
WithingsSleepSensor(sleep_coordinator, attribute)
|
||||
for attribute in SLEEP_SENSORS
|
||||
)
|
||||
remove_listener()
|
||||
remove_sleep_listener()
|
||||
|
||||
remove_listener = sleep_coordinator.async_add_listener(
|
||||
remove_sleep_listener = sleep_coordinator.async_add_listener(
|
||||
_async_add_sleep_entities
|
||||
)
|
||||
|
||||
workout_coordinator = withings_data.workout_coordinator
|
||||
|
||||
workout_entities_setup_before = ent_reg.async_get_entity_id(
|
||||
Platform.SENSOR, DOMAIN, f"withings_{entry.unique_id}_workout_type"
|
||||
)
|
||||
|
||||
if workout_coordinator.data is not None or workout_entities_setup_before:
|
||||
entities.extend(
|
||||
WithingsWorkoutSensor(workout_coordinator, attribute)
|
||||
for attribute in WORKOUT_SENSORS
|
||||
)
|
||||
else:
|
||||
remove_workout_listener: Callable[[], None]
|
||||
|
||||
def _async_add_workout_entities() -> None:
|
||||
"""Add workout entities."""
|
||||
if workout_coordinator.data is not None:
|
||||
async_add_entities(
|
||||
WithingsWorkoutSensor(workout_coordinator, attribute)
|
||||
for attribute in WORKOUT_SENSORS
|
||||
)
|
||||
remove_workout_listener()
|
||||
|
||||
remove_workout_listener = workout_coordinator.async_add_listener(
|
||||
_async_add_workout_entities
|
||||
)
|
||||
|
||||
async_add_entities(entities)
|
||||
|
||||
|
||||
|
@ -755,3 +868,18 @@ class WithingsActivitySensor(WithingsSensor):
|
|||
def last_reset(self) -> datetime:
|
||||
"""These values reset every day."""
|
||||
return dt_util.start_of_local_day()
|
||||
|
||||
|
||||
class WithingsWorkoutSensor(WithingsSensor):
|
||||
"""Implementation of a Withings workout sensor."""
|
||||
|
||||
coordinator: WithingsWorkoutDataUpdateCoordinator
|
||||
|
||||
entity_description: WithingsWorkoutSensorEntityDescription
|
||||
|
||||
@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)
|
||||
|
|
|
@ -170,6 +170,78 @@
|
|||
},
|
||||
"activity_total_calories_burnt_today": {
|
||||
"name": "Total calories burnt today"
|
||||
},
|
||||
"workout_type": {
|
||||
"name": "Last workout type",
|
||||
"state": {
|
||||
"walk": "Walking",
|
||||
"run": "Running",
|
||||
"hiking": "Hiking",
|
||||
"skating": "Skating",
|
||||
"bmx": "BMX",
|
||||
"bicycling": "Bicycling",
|
||||
"swimming": "Swimming",
|
||||
"surfing": "Surfing",
|
||||
"kitesurfing": "Kitesurfing",
|
||||
"windsurfing": "Windsurfing",
|
||||
"bodyboard": "Bodyboard",
|
||||
"tennis": "Tennis",
|
||||
"table_tennis": "Table tennis",
|
||||
"squash": "Squash",
|
||||
"badminton": "Badminton",
|
||||
"lift_weights": "Lift weights",
|
||||
"calisthenics": "Calisthenics",
|
||||
"elliptical": "Elliptical",
|
||||
"pilates": "Pilates",
|
||||
"basket_ball": "Basket ball",
|
||||
"soccer": "Soccer",
|
||||
"football": "Football",
|
||||
"rugby": "Rugby",
|
||||
"volley_ball": "Volley ball",
|
||||
"waterpolo": "Waterpolo",
|
||||
"horse_riding": "Horse riding",
|
||||
"golf": "Golf",
|
||||
"yoga": "Yoga",
|
||||
"dancing": "Dancing",
|
||||
"boxing": "Boxing",
|
||||
"fencing": "Fencing",
|
||||
"wrestling": "Wrestling",
|
||||
"martial_arts": "Martial arts",
|
||||
"skiing": "Skiing",
|
||||
"snowboarding": "Snowboarding",
|
||||
"other": "Other",
|
||||
"no_activity": "No activity",
|
||||
"rowing": "Rowing",
|
||||
"zumba": "Zumba",
|
||||
"baseball": "Baseball",
|
||||
"handball": "Handball",
|
||||
"hockey": "Hockey",
|
||||
"ice_hockey": "Ice hockey",
|
||||
"climbing": "Climbing",
|
||||
"ice_skating": "Ice skating",
|
||||
"multi_sport": "Multi sport",
|
||||
"indoor_walk": "Indoor walking",
|
||||
"indoor_running": "Indoor running",
|
||||
"indoor_cycling": "Indoor cycling"
|
||||
}
|
||||
},
|
||||
"workout_active_calories_burnt": {
|
||||
"name": "Calories burnt last workout"
|
||||
},
|
||||
"workout_distance": {
|
||||
"name": "Distance travelled last workout"
|
||||
},
|
||||
"workout_floors_climbed": {
|
||||
"name": "Floors climbed last workout"
|
||||
},
|
||||
"workout_intensity": {
|
||||
"name": "Last workout intensity"
|
||||
},
|
||||
"workout_pause_duration": {
|
||||
"name": "Pause during last workout"
|
||||
},
|
||||
"workout_duration": {
|
||||
"name": "Last workout duration"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,7 +5,7 @@ from typing import Any
|
|||
from urllib.parse import urlparse
|
||||
|
||||
from aiohttp.test_utils import TestClient
|
||||
from aiowithings import Activity, Goals, MeasurementGroup, SleepSummary
|
||||
from aiowithings import Activity, Goals, MeasurementGroup, SleepSummary, Workout
|
||||
from freezegun.api import FrozenDateTimeFactory
|
||||
|
||||
from homeassistant.components.webhook import async_generate_url
|
||||
|
@ -89,11 +89,19 @@ def load_measurements_fixture(
|
|||
def load_activity_fixture(
|
||||
fixture: str = "withings/activity.json",
|
||||
) -> list[Activity]:
|
||||
"""Return measurement from fixture."""
|
||||
"""Return activities from fixture."""
|
||||
activity_json = load_json_array_fixture(fixture)
|
||||
return [Activity.from_api(activity) for activity in activity_json]
|
||||
|
||||
|
||||
def load_workout_fixture(
|
||||
fixture: str = "withings/workouts.json",
|
||||
) -> list[Workout]:
|
||||
"""Return workouts from fixture."""
|
||||
workouts_json = load_json_array_fixture(fixture)
|
||||
return [Workout.from_api(workout) for workout in workouts_json]
|
||||
|
||||
|
||||
def load_sleep_fixture(
|
||||
fixture: str = "withings/sleep_summaries.json",
|
||||
) -> list[SleepSummary]:
|
||||
|
|
|
@ -21,6 +21,7 @@ from tests.components.withings import (
|
|||
load_goals_fixture,
|
||||
load_measurements_fixture,
|
||||
load_sleep_fixture,
|
||||
load_workout_fixture,
|
||||
)
|
||||
|
||||
CLIENT_ID = "1234"
|
||||
|
@ -144,6 +145,8 @@ def mock_withings():
|
|||
NotificationConfiguration.from_api(not_conf) for not_conf in notification_json
|
||||
]
|
||||
|
||||
workouts = load_workout_fixture()
|
||||
|
||||
activities = load_activity_fixture()
|
||||
|
||||
mock = AsyncMock(spec=WithingsClient)
|
||||
|
@ -155,6 +158,8 @@ def mock_withings():
|
|||
mock.get_activities_since.return_value = activities
|
||||
mock.get_activities_in_period.return_value = activities
|
||||
mock.list_notification_configurations.return_value = notifications
|
||||
mock.get_workouts_since.return_value = workouts
|
||||
mock.get_workouts_in_period.return_value = workouts
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.withings.WithingsClient",
|
||||
|
|
327
tests/components/withings/fixtures/workouts.json
Normal file
327
tests/components/withings/fixtures/workouts.json
Normal file
|
@ -0,0 +1,327 @@
|
|||
[
|
||||
{
|
||||
"id": 3661300277,
|
||||
"category": 1,
|
||||
"timezone": "Europe/Amsterdam",
|
||||
"model": 1055,
|
||||
"attrib": 0,
|
||||
"startdate": 1693336011,
|
||||
"enddate": 1693336513,
|
||||
"date": "2023-08-29",
|
||||
"deviceid": null,
|
||||
"data": {
|
||||
"calories": 47,
|
||||
"intensity": 30,
|
||||
"manual_distance": 60,
|
||||
"manual_calories": 70,
|
||||
"hr_average": 80,
|
||||
"hr_min": 70,
|
||||
"hr_max": 80,
|
||||
"hr_zone_0": 100,
|
||||
"hr_zone_1": 200,
|
||||
"hr_zone_2": 300,
|
||||
"hr_zone_3": 400,
|
||||
"pause_duration": 80,
|
||||
"steps": 779,
|
||||
"distance": 680,
|
||||
"elevation": 10,
|
||||
"algo_pause_duration": null,
|
||||
"spo2_average": 15
|
||||
},
|
||||
"modified": 1693481873
|
||||
},
|
||||
{
|
||||
"id": 3661300290,
|
||||
"category": 1,
|
||||
"timezone": "Europe/Amsterdam",
|
||||
"model": 1055,
|
||||
"attrib": 0,
|
||||
"startdate": 1693469307,
|
||||
"enddate": 1693469924,
|
||||
"date": "2023-08-31",
|
||||
"deviceid": null,
|
||||
"data": {
|
||||
"algo_pause_duration": null
|
||||
},
|
||||
"modified": 1693481873
|
||||
},
|
||||
{
|
||||
"id": 3661300269,
|
||||
"category": 1,
|
||||
"timezone": "Europe/Amsterdam",
|
||||
"model": 1055,
|
||||
"attrib": 0,
|
||||
"startdate": 1691164839,
|
||||
"enddate": 1691165719,
|
||||
"date": "2023-08-04",
|
||||
"deviceid": null,
|
||||
"data": {
|
||||
"calories": 82,
|
||||
"intensity": 30,
|
||||
"manual_distance": 0,
|
||||
"manual_calories": 0,
|
||||
"hr_average": 0,
|
||||
"hr_min": 0,
|
||||
"hr_max": 0,
|
||||
"hr_zone_0": 0,
|
||||
"hr_zone_1": 0,
|
||||
"hr_zone_2": 0,
|
||||
"hr_zone_3": 0,
|
||||
"pause_duration": 0,
|
||||
"steps": 1450,
|
||||
"distance": 1294,
|
||||
"elevation": 18,
|
||||
"algo_pause_duration": null,
|
||||
"spo2_average": null
|
||||
},
|
||||
"modified": 1693481873
|
||||
},
|
||||
{
|
||||
"id": 3743596080,
|
||||
"category": 1,
|
||||
"timezone": "Europe/Amsterdam",
|
||||
"model": 1055,
|
||||
"attrib": 0,
|
||||
"startdate": 1695425635,
|
||||
"enddate": 1695426661,
|
||||
"date": "2023-09-23",
|
||||
"deviceid": null,
|
||||
"data": {
|
||||
"calories": 97,
|
||||
"intensity": 30,
|
||||
"manual_distance": 0,
|
||||
"manual_calories": 0,
|
||||
"hr_average": 0,
|
||||
"hr_min": 0,
|
||||
"hr_max": 0,
|
||||
"hr_zone_0": 0,
|
||||
"hr_zone_1": 0,
|
||||
"hr_zone_2": 0,
|
||||
"hr_zone_3": 0,
|
||||
"pause_duration": 0,
|
||||
"steps": 1650,
|
||||
"distance": 1405,
|
||||
"elevation": 19,
|
||||
"algo_pause_duration": null,
|
||||
"spo2_average": null
|
||||
},
|
||||
"modified": 1696672530
|
||||
},
|
||||
{
|
||||
"id": 3743596073,
|
||||
"category": 1,
|
||||
"timezone": "Europe/Amsterdam",
|
||||
"model": 1055,
|
||||
"attrib": 0,
|
||||
"startdate": 1694715649,
|
||||
"enddate": 1694716306,
|
||||
"date": "2023-09-14",
|
||||
"deviceid": null,
|
||||
"data": {
|
||||
"calories": 62,
|
||||
"intensity": 30,
|
||||
"manual_distance": 0,
|
||||
"manual_calories": 0,
|
||||
"hr_average": 0,
|
||||
"hr_min": 0,
|
||||
"hr_max": 0,
|
||||
"hr_zone_0": 0,
|
||||
"hr_zone_1": 0,
|
||||
"hr_zone_2": 0,
|
||||
"hr_zone_3": 0,
|
||||
"pause_duration": 0,
|
||||
"steps": 1076,
|
||||
"distance": 917,
|
||||
"elevation": 15,
|
||||
"algo_pause_duration": null,
|
||||
"spo2_average": null
|
||||
},
|
||||
"modified": 1696672530
|
||||
},
|
||||
{
|
||||
"id": 3743596085,
|
||||
"category": 1,
|
||||
"timezone": "Europe/Amsterdam",
|
||||
"model": 1055,
|
||||
"attrib": 0,
|
||||
"startdate": 1695426953,
|
||||
"enddate": 1695427093,
|
||||
"date": "2023-09-23",
|
||||
"deviceid": null,
|
||||
"data": {
|
||||
"calories": 13,
|
||||
"intensity": 30,
|
||||
"manual_distance": 0,
|
||||
"manual_calories": 0,
|
||||
"hr_average": 0,
|
||||
"hr_min": 0,
|
||||
"hr_max": 0,
|
||||
"hr_zone_0": 0,
|
||||
"hr_zone_1": 0,
|
||||
"hr_zone_2": 0,
|
||||
"hr_zone_3": 0,
|
||||
"pause_duration": 0,
|
||||
"steps": 216,
|
||||
"distance": 185,
|
||||
"elevation": 4,
|
||||
"algo_pause_duration": null,
|
||||
"spo2_average": null
|
||||
},
|
||||
"modified": 1696672530
|
||||
},
|
||||
{
|
||||
"id": 3743596072,
|
||||
"category": 1,
|
||||
"timezone": "Europe/Amsterdam",
|
||||
"model": 1055,
|
||||
"attrib": 0,
|
||||
"startdate": 1694713351,
|
||||
"enddate": 1694715327,
|
||||
"date": "2023-09-14",
|
||||
"deviceid": null,
|
||||
"data": {
|
||||
"calories": 187,
|
||||
"intensity": 30,
|
||||
"manual_distance": 0,
|
||||
"manual_calories": 0,
|
||||
"hr_average": 0,
|
||||
"hr_min": 0,
|
||||
"hr_max": 0,
|
||||
"hr_zone_0": 0,
|
||||
"hr_zone_1": 0,
|
||||
"hr_zone_2": 0,
|
||||
"hr_zone_3": 0,
|
||||
"pause_duration": 0,
|
||||
"steps": 3339,
|
||||
"distance": 2908,
|
||||
"elevation": 49,
|
||||
"algo_pause_duration": null,
|
||||
"spo2_average": null
|
||||
},
|
||||
"modified": 1696672530
|
||||
},
|
||||
{
|
||||
"id": 3752609171,
|
||||
"category": 1,
|
||||
"timezone": "Europe/Amsterdam",
|
||||
"model": 1055,
|
||||
"attrib": 0,
|
||||
"startdate": 1696835569,
|
||||
"enddate": 1696835767,
|
||||
"date": "2023-10-09",
|
||||
"deviceid": null,
|
||||
"data": {
|
||||
"calories": 18,
|
||||
"intensity": 30,
|
||||
"manual_distance": 0,
|
||||
"manual_calories": 0,
|
||||
"hr_average": 0,
|
||||
"hr_min": 0,
|
||||
"hr_max": 0,
|
||||
"hr_zone_0": 0,
|
||||
"hr_zone_1": 0,
|
||||
"hr_zone_2": 0,
|
||||
"hr_zone_3": 0,
|
||||
"pause_duration": 0,
|
||||
"steps": 291,
|
||||
"distance": 261,
|
||||
"elevation": 4,
|
||||
"algo_pause_duration": null,
|
||||
"spo2_average": null
|
||||
},
|
||||
"modified": 1697038119
|
||||
},
|
||||
{
|
||||
"id": 3752609178,
|
||||
"category": 1,
|
||||
"timezone": "Europe/Amsterdam",
|
||||
"model": 1055,
|
||||
"attrib": 0,
|
||||
"startdate": 1696844383,
|
||||
"enddate": 1696844638,
|
||||
"date": "2023-10-09",
|
||||
"deviceid": null,
|
||||
"data": {
|
||||
"calories": 24,
|
||||
"intensity": 30,
|
||||
"manual_distance": 0,
|
||||
"manual_calories": 0,
|
||||
"hr_average": 0,
|
||||
"hr_min": 0,
|
||||
"hr_max": 0,
|
||||
"hr_zone_0": 0,
|
||||
"hr_zone_1": 0,
|
||||
"hr_zone_2": 0,
|
||||
"hr_zone_3": 0,
|
||||
"pause_duration": 0,
|
||||
"steps": 267,
|
||||
"distance": 232,
|
||||
"elevation": 4,
|
||||
"algo_pause_duration": null,
|
||||
"spo2_average": null
|
||||
},
|
||||
"modified": 1697038119
|
||||
},
|
||||
{
|
||||
"id": 3752609174,
|
||||
"category": 1,
|
||||
"timezone": "Europe/Amsterdam",
|
||||
"model": 1055,
|
||||
"attrib": 0,
|
||||
"startdate": 1696842803,
|
||||
"enddate": 1696843032,
|
||||
"date": "2023-10-09",
|
||||
"deviceid": null,
|
||||
"data": {
|
||||
"calories": 21,
|
||||
"intensity": 30,
|
||||
"manual_distance": 0,
|
||||
"manual_calories": 0,
|
||||
"hr_average": 0,
|
||||
"hr_min": 0,
|
||||
"hr_max": 0,
|
||||
"hr_zone_0": 0,
|
||||
"hr_zone_1": 0,
|
||||
"hr_zone_2": 0,
|
||||
"hr_zone_3": 0,
|
||||
"pause_duration": 0,
|
||||
"steps": 403,
|
||||
"distance": 359,
|
||||
"elevation": 4,
|
||||
"algo_pause_duration": null,
|
||||
"spo2_average": null
|
||||
},
|
||||
"modified": 1697038119
|
||||
},
|
||||
{
|
||||
"id": 3752609174,
|
||||
"category": 1,
|
||||
"timezone": "Europe/Amsterdam",
|
||||
"model": 1055,
|
||||
"attrib": 0,
|
||||
"startdate": 1696842803,
|
||||
"enddate": 1696843032,
|
||||
"date": "2023-10-09",
|
||||
"deviceid": null,
|
||||
"data": {
|
||||
"calories": 21,
|
||||
"intensity": 30,
|
||||
"manual_distance": 0,
|
||||
"manual_calories": 0,
|
||||
"hr_average": 0,
|
||||
"hr_min": 0,
|
||||
"hr_max": 0,
|
||||
"hr_zone_0": 0,
|
||||
"hr_zone_1": 0,
|
||||
"hr_zone_2": 0,
|
||||
"hr_zone_3": 0,
|
||||
"pause_duration": 0,
|
||||
"steps": 403,
|
||||
"distance": 359,
|
||||
"elevation": 4,
|
||||
"algo_pause_duration": null,
|
||||
"spo2_average": null
|
||||
},
|
||||
"modified": 1697038119
|
||||
}
|
||||
]
|
|
@ -5,7 +5,7 @@
|
|||
'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',
|
||||
'unit_of_measurement': 'calories',
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.henk_active_calories_burnt_today',
|
||||
|
@ -103,6 +103,19 @@
|
|||
'state': '9',
|
||||
})
|
||||
# ---
|
||||
# name: test_all_entities[sensor.henk_calories_burnt_last_workout]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'friendly_name': 'henk Calories burnt last workout',
|
||||
'unit_of_measurement': 'calories',
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.henk_calories_burnt_last_workout',
|
||||
'last_changed': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': '24',
|
||||
})
|
||||
# ---
|
||||
# name: test_all_entities[sensor.henk_deep_sleep]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
|
@ -133,6 +146,21 @@
|
|||
'state': '70',
|
||||
})
|
||||
# ---
|
||||
# name: test_all_entities[sensor.henk_distance_travelled_last_workout]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'device_class': 'distance',
|
||||
'friendly_name': 'henk Distance travelled last workout',
|
||||
'icon': 'mdi:map-marker-distance',
|
||||
'unit_of_measurement': <UnitOfLength.METERS: 'm'>,
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.henk_distance_travelled_last_workout',
|
||||
'last_changed': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': '232',
|
||||
})
|
||||
# ---
|
||||
# name: test_all_entities[sensor.henk_distance_travelled_today]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
|
@ -209,6 +237,20 @@
|
|||
'state': '0.07',
|
||||
})
|
||||
# ---
|
||||
# name: test_all_entities[sensor.henk_floors_climbed_last_workout]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'friendly_name': 'henk Floors climbed last workout',
|
||||
'icon': 'mdi:stairs-up',
|
||||
'unit_of_measurement': 'floors',
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.henk_floors_climbed_last_workout',
|
||||
'last_changed': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': '4',
|
||||
})
|
||||
# ---
|
||||
# name: test_all_entities[sensor.henk_floors_climbed_today]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
|
@ -216,7 +258,7 @@
|
|||
'icon': 'mdi:stairs-up',
|
||||
'last_reset': '2023-10-20T00:00:00-07:00',
|
||||
'state_class': <SensorStateClass.TOTAL: 'total'>,
|
||||
'unit_of_measurement': 'Floors',
|
||||
'unit_of_measurement': 'floors',
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.henk_floors_climbed_today',
|
||||
|
@ -302,6 +344,97 @@
|
|||
'state': '100',
|
||||
})
|
||||
# ---
|
||||
# name: test_all_entities[sensor.henk_last_workout_duration]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'device_class': 'duration',
|
||||
'friendly_name': 'henk Last workout duration',
|
||||
'icon': 'mdi:timer',
|
||||
'unit_of_measurement': <UnitOfTime.SECONDS: 's'>,
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.henk_last_workout_duration',
|
||||
'last_changed': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': '255.0',
|
||||
})
|
||||
# ---
|
||||
# name: test_all_entities[sensor.henk_last_workout_intensity]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'friendly_name': 'henk Last workout intensity',
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.henk_last_workout_intensity',
|
||||
'last_changed': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': '30',
|
||||
})
|
||||
# ---
|
||||
# name: test_all_entities[sensor.henk_last_workout_type]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'device_class': 'enum',
|
||||
'friendly_name': 'henk Last workout type',
|
||||
'options': list([
|
||||
'walk',
|
||||
'run',
|
||||
'hiking',
|
||||
'skating',
|
||||
'bmx',
|
||||
'bicycling',
|
||||
'swimming',
|
||||
'surfing',
|
||||
'kitesurfing',
|
||||
'windsurfing',
|
||||
'bodyboard',
|
||||
'tennis',
|
||||
'table_tennis',
|
||||
'squash',
|
||||
'badminton',
|
||||
'lift_weights',
|
||||
'calisthenics',
|
||||
'elliptical',
|
||||
'pilates',
|
||||
'basket_ball',
|
||||
'soccer',
|
||||
'football',
|
||||
'rugby',
|
||||
'volley_ball',
|
||||
'waterpolo',
|
||||
'horse_riding',
|
||||
'golf',
|
||||
'yoga',
|
||||
'dancing',
|
||||
'boxing',
|
||||
'fencing',
|
||||
'wrestling',
|
||||
'martial_arts',
|
||||
'skiing',
|
||||
'snowboarding',
|
||||
'other',
|
||||
'no_activity',
|
||||
'rowing',
|
||||
'zumba',
|
||||
'baseball',
|
||||
'handball',
|
||||
'hockey',
|
||||
'ice_hockey',
|
||||
'climbing',
|
||||
'ice_skating',
|
||||
'multi_sport',
|
||||
'indoor_walk',
|
||||
'indoor_running',
|
||||
'indoor_cycling',
|
||||
]),
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.henk_last_workout_type',
|
||||
'last_changed': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': 'walk',
|
||||
})
|
||||
# ---
|
||||
# name: test_all_entities[sensor.henk_light_sleep]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
|
@ -407,6 +540,21 @@
|
|||
'state': '50',
|
||||
})
|
||||
# ---
|
||||
# name: test_all_entities[sensor.henk_pause_during_last_workout]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'device_class': 'duration',
|
||||
'friendly_name': 'henk Pause during last workout',
|
||||
'icon': 'mdi:timer-pause',
|
||||
'unit_of_measurement': <UnitOfTime.SECONDS: 's'>,
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.henk_pause_during_last_workout',
|
||||
'last_changed': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': '0',
|
||||
})
|
||||
# ---
|
||||
# name: test_all_entities[sensor.henk_pulse_wave_velocity]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
|
@ -546,7 +694,7 @@
|
|||
'friendly_name': 'henk Step goal',
|
||||
'icon': 'mdi:shoe-print',
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
'unit_of_measurement': 'Steps',
|
||||
'unit_of_measurement': 'steps',
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.henk_step_goal',
|
||||
|
@ -562,7 +710,7 @@
|
|||
'icon': 'mdi:shoe-print',
|
||||
'last_reset': '2023-10-20T00:00:00-07:00',
|
||||
'state_class': <SensorStateClass.TOTAL: 'total'>,
|
||||
'unit_of_measurement': 'Steps',
|
||||
'unit_of_measurement': 'steps',
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.henk_steps_today',
|
||||
|
@ -638,7 +786,7 @@
|
|||
'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',
|
||||
'unit_of_measurement': 'calories',
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.henk_total_calories_burnt_today',
|
||||
|
|
|
@ -15,6 +15,7 @@ from . import (
|
|||
load_goals_fixture,
|
||||
load_measurements_fixture,
|
||||
load_sleep_fixture,
|
||||
load_workout_fixture,
|
||||
setup_integration,
|
||||
)
|
||||
|
||||
|
@ -293,3 +294,50 @@ async def test_sleep_sensors_created_when_receive_sleep_data(
|
|||
await hass.async_block_till_done()
|
||||
|
||||
assert hass.states.get("sensor.henk_deep_sleep")
|
||||
|
||||
|
||||
async def test_workout_sensors_created_when_existed(
|
||||
hass: HomeAssistant,
|
||||
withings: AsyncMock,
|
||||
polling_config_entry: MockConfigEntry,
|
||||
freezer: FrozenDateTimeFactory,
|
||||
) -> None:
|
||||
"""Test workout sensors will be added if they existed before."""
|
||||
await setup_integration(hass, polling_config_entry, False)
|
||||
|
||||
assert hass.states.get("sensor.henk_last_workout_type")
|
||||
assert hass.states.get("sensor.henk_last_workout_type").state != STATE_UNKNOWN
|
||||
|
||||
withings.get_workouts_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_last_workout_type").state == STATE_UNKNOWN
|
||||
|
||||
|
||||
async def test_workout_sensors_created_when_receive_workout_data(
|
||||
hass: HomeAssistant,
|
||||
withings: AsyncMock,
|
||||
polling_config_entry: MockConfigEntry,
|
||||
freezer: FrozenDateTimeFactory,
|
||||
) -> None:
|
||||
"""Test workout sensors will be added if we receive workout data."""
|
||||
withings.get_workouts_in_period.return_value = []
|
||||
await setup_integration(hass, polling_config_entry, False)
|
||||
|
||||
assert hass.states.get("sensor.henk_last_workout_type") is None
|
||||
|
||||
freezer.tick(timedelta(minutes=10))
|
||||
async_fire_time_changed(hass)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert hass.states.get("sensor.henk_last_workout_type") is None
|
||||
|
||||
withings.get_workouts_in_period.return_value = load_workout_fixture()
|
||||
|
||||
freezer.tick(timedelta(minutes=10))
|
||||
async_fire_time_changed(hass)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert hass.states.get("sensor.henk_last_workout_type")
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue