Add workout calendar to Withings (#102589)
This commit is contained in:
parent
80b3fec675
commit
9600c7fac1
5 changed files with 364 additions and 1 deletions
|
@ -62,7 +62,7 @@ from .coordinator import (
|
|||
WithingsWorkoutDataUpdateCoordinator,
|
||||
)
|
||||
|
||||
PLATFORMS = [Platform.BINARY_SENSOR, Platform.SENSOR]
|
||||
PLATFORMS = [Platform.BINARY_SENSOR, Platform.CALENDAR, Platform.SENSOR]
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema(
|
||||
{
|
||||
|
@ -129,6 +129,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
|||
class WithingsData:
|
||||
"""Dataclass to hold withings domain data."""
|
||||
|
||||
client: WithingsClient
|
||||
measurement_coordinator: WithingsMeasurementDataUpdateCoordinator
|
||||
sleep_coordinator: WithingsSleepDataUpdateCoordinator
|
||||
bed_presence_coordinator: WithingsBedPresenceDataUpdateCoordinator
|
||||
|
@ -174,6 +175,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
|||
|
||||
client.refresh_token_function = _refresh_token
|
||||
withings_data = WithingsData(
|
||||
client=client,
|
||||
measurement_coordinator=WithingsMeasurementDataUpdateCoordinator(hass, client),
|
||||
sleep_coordinator=WithingsSleepDataUpdateCoordinator(hass, client),
|
||||
bed_presence_coordinator=WithingsBedPresenceDataUpdateCoordinator(hass, client),
|
||||
|
|
104
homeassistant/components/withings/calendar.py
Normal file
104
homeassistant/components/withings/calendar.py
Normal file
|
@ -0,0 +1,104 @@
|
|||
"""Calendar platform for Withings."""
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable
|
||||
from datetime import datetime
|
||||
|
||||
from aiowithings import WithingsClient, WorkoutCategory
|
||||
|
||||
from homeassistant.components.calendar import CalendarEntity, CalendarEvent
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
import homeassistant.helpers.entity_registry as er
|
||||
|
||||
from . import DOMAIN, WithingsData
|
||||
from .coordinator import WithingsWorkoutDataUpdateCoordinator
|
||||
from .entity import WithingsEntity
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: ConfigEntry,
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the calendar platform for entity."""
|
||||
ent_reg = er.async_get(hass)
|
||||
withings_data: WithingsData = hass.data[DOMAIN][entry.entry_id]
|
||||
|
||||
workout_coordinator = withings_data.workout_coordinator
|
||||
|
||||
calendar_setup_before = ent_reg.async_get_entity_id(
|
||||
Platform.CALENDAR,
|
||||
DOMAIN,
|
||||
f"withings_{entry.unique_id}_workout",
|
||||
)
|
||||
|
||||
if workout_coordinator.data is not None or calendar_setup_before:
|
||||
async_add_entities(
|
||||
[WithingsWorkoutCalendarEntity(withings_data.client, workout_coordinator)],
|
||||
)
|
||||
else:
|
||||
remove_calendar_listener: Callable[[], None]
|
||||
|
||||
def _async_add_calendar_entity() -> None:
|
||||
"""Add calendar entity."""
|
||||
if workout_coordinator.data is not None:
|
||||
async_add_entities(
|
||||
[
|
||||
WithingsWorkoutCalendarEntity(
|
||||
withings_data.client, workout_coordinator
|
||||
)
|
||||
],
|
||||
)
|
||||
remove_calendar_listener()
|
||||
|
||||
remove_calendar_listener = workout_coordinator.async_add_listener(
|
||||
_async_add_calendar_entity
|
||||
)
|
||||
|
||||
|
||||
def get_event_name(category: WorkoutCategory) -> str:
|
||||
"""Return human-readable category."""
|
||||
name = category.name.lower().capitalize()
|
||||
return name.replace("_", " ")
|
||||
|
||||
|
||||
class WithingsWorkoutCalendarEntity(CalendarEntity, WithingsEntity):
|
||||
"""A calendar entity."""
|
||||
|
||||
_attr_translation_key = "workout"
|
||||
|
||||
coordinator: WithingsWorkoutDataUpdateCoordinator
|
||||
|
||||
def __init__(
|
||||
self, client: WithingsClient, coordinator: WithingsWorkoutDataUpdateCoordinator
|
||||
) -> None:
|
||||
"""Create the Calendar entity."""
|
||||
super().__init__(coordinator, "workout")
|
||||
self.client = client
|
||||
|
||||
@property
|
||||
def event(self) -> CalendarEvent | None:
|
||||
"""Return the next upcoming event."""
|
||||
return None
|
||||
|
||||
async def async_get_events(
|
||||
self, hass: HomeAssistant, start_date: datetime, end_date: datetime
|
||||
) -> list[CalendarEvent]:
|
||||
"""Get all events in a specific time frame."""
|
||||
workouts = await self.client.get_workouts_in_period(
|
||||
start_date.date(), end_date.date()
|
||||
)
|
||||
event_list = []
|
||||
for workout in workouts:
|
||||
event = CalendarEvent(
|
||||
start=workout.start_date,
|
||||
end=workout.end_date,
|
||||
summary=get_event_name(workout.category),
|
||||
)
|
||||
|
||||
event_list.append(event)
|
||||
|
||||
return event_list
|
|
@ -29,6 +29,11 @@
|
|||
"name": "In bed"
|
||||
}
|
||||
},
|
||||
"calendar": {
|
||||
"workout": {
|
||||
"name": "Workouts"
|
||||
}
|
||||
},
|
||||
"sensor": {
|
||||
"fat_mass": {
|
||||
"name": "Fat mass"
|
||||
|
|
167
tests/components/withings/snapshots/test_calendar.ambr
Normal file
167
tests/components/withings/snapshots/test_calendar.ambr
Normal file
|
@ -0,0 +1,167 @@
|
|||
# serializer version: 1
|
||||
# name: test_api_calendar
|
||||
list([
|
||||
dict({
|
||||
'entity_id': 'calendar.henk_workouts',
|
||||
'name': 'henk Workouts',
|
||||
}),
|
||||
])
|
||||
# ---
|
||||
# name: test_api_events
|
||||
list([
|
||||
dict({
|
||||
'description': None,
|
||||
'end': dict({
|
||||
'dateTime': '2023-08-29T12:15:13-07:00',
|
||||
}),
|
||||
'location': None,
|
||||
'recurrence_id': None,
|
||||
'rrule': None,
|
||||
'start': dict({
|
||||
'dateTime': '2023-08-29T12:06:51-07:00',
|
||||
}),
|
||||
'summary': 'Walk',
|
||||
'uid': None,
|
||||
}),
|
||||
dict({
|
||||
'description': None,
|
||||
'end': dict({
|
||||
'dateTime': '2023-08-31T01:18:44-07:00',
|
||||
}),
|
||||
'location': None,
|
||||
'recurrence_id': None,
|
||||
'rrule': None,
|
||||
'start': dict({
|
||||
'dateTime': '2023-08-31T01:08:27-07:00',
|
||||
}),
|
||||
'summary': 'Walk',
|
||||
'uid': None,
|
||||
}),
|
||||
dict({
|
||||
'description': None,
|
||||
'end': dict({
|
||||
'dateTime': '2023-08-04T09:15:19-07:00',
|
||||
}),
|
||||
'location': None,
|
||||
'recurrence_id': None,
|
||||
'rrule': None,
|
||||
'start': dict({
|
||||
'dateTime': '2023-08-04T09:00:39-07:00',
|
||||
}),
|
||||
'summary': 'Walk',
|
||||
'uid': None,
|
||||
}),
|
||||
dict({
|
||||
'description': None,
|
||||
'end': dict({
|
||||
'dateTime': '2023-09-22T16:51:01-07:00',
|
||||
}),
|
||||
'location': None,
|
||||
'recurrence_id': None,
|
||||
'rrule': None,
|
||||
'start': dict({
|
||||
'dateTime': '2023-09-22T16:33:55-07:00',
|
||||
}),
|
||||
'summary': 'Walk',
|
||||
'uid': None,
|
||||
}),
|
||||
dict({
|
||||
'description': None,
|
||||
'end': dict({
|
||||
'dateTime': '2023-09-14T11:31:46-07:00',
|
||||
}),
|
||||
'location': None,
|
||||
'recurrence_id': None,
|
||||
'rrule': None,
|
||||
'start': dict({
|
||||
'dateTime': '2023-09-14T11:20:49-07:00',
|
||||
}),
|
||||
'summary': 'Walk',
|
||||
'uid': None,
|
||||
}),
|
||||
dict({
|
||||
'description': None,
|
||||
'end': dict({
|
||||
'dateTime': '2023-09-22T16:58:13-07:00',
|
||||
}),
|
||||
'location': None,
|
||||
'recurrence_id': None,
|
||||
'rrule': None,
|
||||
'start': dict({
|
||||
'dateTime': '2023-09-22T16:55:53-07:00',
|
||||
}),
|
||||
'summary': 'Walk',
|
||||
'uid': None,
|
||||
}),
|
||||
dict({
|
||||
'description': None,
|
||||
'end': dict({
|
||||
'dateTime': '2023-09-14T11:15:27-07:00',
|
||||
}),
|
||||
'location': None,
|
||||
'recurrence_id': None,
|
||||
'rrule': None,
|
||||
'start': dict({
|
||||
'dateTime': '2023-09-14T10:42:31-07:00',
|
||||
}),
|
||||
'summary': 'Walk',
|
||||
'uid': None,
|
||||
}),
|
||||
dict({
|
||||
'description': None,
|
||||
'end': dict({
|
||||
'dateTime': '2023-10-09T00:16:07-07:00',
|
||||
}),
|
||||
'location': None,
|
||||
'recurrence_id': None,
|
||||
'rrule': None,
|
||||
'start': dict({
|
||||
'dateTime': '2023-10-09T00:12:49-07:00',
|
||||
}),
|
||||
'summary': 'Walk',
|
||||
'uid': None,
|
||||
}),
|
||||
dict({
|
||||
'description': None,
|
||||
'end': dict({
|
||||
'dateTime': '2023-10-09T02:43:58-07:00',
|
||||
}),
|
||||
'location': None,
|
||||
'recurrence_id': None,
|
||||
'rrule': None,
|
||||
'start': dict({
|
||||
'dateTime': '2023-10-09T02:39:43-07:00',
|
||||
}),
|
||||
'summary': 'Walk',
|
||||
'uid': None,
|
||||
}),
|
||||
dict({
|
||||
'description': None,
|
||||
'end': dict({
|
||||
'dateTime': '2023-10-09T02:17:12-07:00',
|
||||
}),
|
||||
'location': None,
|
||||
'recurrence_id': None,
|
||||
'rrule': None,
|
||||
'start': dict({
|
||||
'dateTime': '2023-10-09T02:13:23-07:00',
|
||||
}),
|
||||
'summary': 'Walk',
|
||||
'uid': None,
|
||||
}),
|
||||
dict({
|
||||
'description': None,
|
||||
'end': dict({
|
||||
'dateTime': '2023-10-09T02:17:12-07:00',
|
||||
}),
|
||||
'location': None,
|
||||
'recurrence_id': None,
|
||||
'rrule': None,
|
||||
'start': dict({
|
||||
'dateTime': '2023-10-09T02:13:23-07:00',
|
||||
}),
|
||||
'summary': 'Walk',
|
||||
'uid': None,
|
||||
}),
|
||||
])
|
||||
# ---
|
85
tests/components/withings/test_calendar.py
Normal file
85
tests/components/withings/test_calendar.py
Normal file
|
@ -0,0 +1,85 @@
|
|||
"""Tests for the Withings calendar."""
|
||||
from datetime import date, timedelta
|
||||
from http import HTTPStatus
|
||||
from unittest.mock import AsyncMock
|
||||
|
||||
from freezegun.api import FrozenDateTimeFactory
|
||||
from syrupy.assertion import SnapshotAssertion
|
||||
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from . import load_workout_fixture
|
||||
|
||||
from tests.common import MockConfigEntry, async_fire_time_changed
|
||||
from tests.components.withings import setup_integration
|
||||
from tests.typing import ClientSessionGenerator
|
||||
|
||||
|
||||
async def test_api_calendar(
|
||||
hass: HomeAssistant,
|
||||
snapshot: SnapshotAssertion,
|
||||
withings: AsyncMock,
|
||||
polling_config_entry: MockConfigEntry,
|
||||
hass_client: ClientSessionGenerator,
|
||||
) -> None:
|
||||
"""Test the API returns the calendar."""
|
||||
await setup_integration(hass, polling_config_entry, False)
|
||||
|
||||
client = await hass_client()
|
||||
response = await client.get("/api/calendars")
|
||||
assert response.status == HTTPStatus.OK
|
||||
data = await response.json()
|
||||
assert data == snapshot
|
||||
|
||||
|
||||
async def test_api_events(
|
||||
hass: HomeAssistant,
|
||||
snapshot: SnapshotAssertion,
|
||||
withings: AsyncMock,
|
||||
polling_config_entry: MockConfigEntry,
|
||||
hass_client: ClientSessionGenerator,
|
||||
) -> None:
|
||||
"""Test the Withings calendar view."""
|
||||
await setup_integration(hass, polling_config_entry, False)
|
||||
|
||||
client = await hass_client()
|
||||
response = await client.get(
|
||||
"/api/calendars/calendar.henk_workouts?start=2023-08-01&end=2023-11-01"
|
||||
)
|
||||
assert withings.get_workouts_in_period.called == 1
|
||||
assert withings.get_workouts_in_period.call_args_list[1].args == (
|
||||
date(2023, 8, 1),
|
||||
date(2023, 11, 1),
|
||||
)
|
||||
assert response.status == HTTPStatus.OK
|
||||
events = await response.json()
|
||||
assert events == snapshot
|
||||
|
||||
|
||||
async def test_calendar_created_when_workouts_available(
|
||||
hass: HomeAssistant,
|
||||
snapshot: SnapshotAssertion,
|
||||
withings: AsyncMock,
|
||||
polling_config_entry: MockConfigEntry,
|
||||
hass_client: ClientSessionGenerator,
|
||||
freezer: FrozenDateTimeFactory,
|
||||
) -> None:
|
||||
"""Test the calendar is only created when workouts are available."""
|
||||
withings.get_workouts_in_period.return_value = []
|
||||
await setup_integration(hass, polling_config_entry, False)
|
||||
|
||||
assert hass.states.get("calendar.henk_workouts") is None
|
||||
|
||||
freezer.tick(timedelta(minutes=10))
|
||||
async_fire_time_changed(hass)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert hass.states.get("calendar.henk_workouts") 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("calendar.henk_workouts")
|
Loading…
Add table
Reference in a new issue