Add workout calendar to Withings (#102589)

This commit is contained in:
Joost Lekkerkerker 2023-10-24 16:38:11 +02:00 committed by GitHub
parent 80b3fec675
commit 9600c7fac1
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 364 additions and 1 deletions

View file

@ -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),

View 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

View file

@ -29,6 +29,11 @@
"name": "In bed"
}
},
"calendar": {
"workout": {
"name": "Workouts"
}
},
"sensor": {
"fat_mass": {
"name": "Fat mass"

View 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,
}),
])
# ---

View 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")