Update Withings measurements incrementally after the first update (#102002)

* Update incrementally after the first update

* Update incrementally after the first update
This commit is contained in:
Joost Lekkerkerker 2023-10-14 16:19:04 +02:00 committed by GitHub
parent 302b444269
commit 76083a0b4c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 171 additions and 10 deletions

View file

@ -1,6 +1,6 @@
"""Withings coordinator.""" """Withings coordinator."""
from abc import abstractmethod from abc import abstractmethod
from datetime import timedelta from datetime import datetime, timedelta
from typing import TypeVar from typing import TypeVar
from aiowithings import ( from aiowithings import (
@ -33,6 +33,7 @@ class WithingsDataUpdateCoordinator(DataUpdateCoordinator[_T]):
config_entry: ConfigEntry config_entry: ConfigEntry
_default_update_interval: timedelta | None = UPDATE_INTERVAL _default_update_interval: timedelta | None = UPDATE_INTERVAL
_last_valid_update: datetime | None = None
def __init__(self, hass: HomeAssistant, client: WithingsClient) -> None: def __init__(self, hass: HomeAssistant, client: WithingsClient) -> None:
"""Initialize the Withings data coordinator.""" """Initialize the Withings data coordinator."""
@ -80,15 +81,24 @@ class WithingsMeasurementDataUpdateCoordinator(
NotificationCategory.ACTIVITY, NotificationCategory.ACTIVITY,
NotificationCategory.PRESSURE, NotificationCategory.PRESSURE,
} }
self._previous_data: dict[MeasurementType, float] = {}
async def _internal_update_data(self) -> dict[MeasurementType, float]: async def _internal_update_data(self) -> dict[MeasurementType, float]:
"""Retrieve measurement data.""" """Retrieve measurement data."""
now = dt_util.utcnow() if self._last_valid_update is None:
startdate = now - timedelta(days=7) now = dt_util.utcnow()
startdate = now - timedelta(days=14)
measurements = await self._client.get_measurement_in_period(startdate, now)
else:
measurements = await self._client.get_measurement_since(
self._last_valid_update
)
response = await self._client.get_measurement_in_period(startdate, now) if measurements:
self._last_valid_update = measurements[0].taken_at
return aggregate_measurements(response) aggregated_measurements = aggregate_measurements(measurements)
self._previous_data.update(aggregated_measurements)
return self._previous_data
class WithingsSleepDataUpdateCoordinator( class WithingsSleepDataUpdateCoordinator(

View file

@ -151,6 +151,7 @@ def mock_withings():
mock = AsyncMock(spec=WithingsClient) mock = AsyncMock(spec=WithingsClient)
mock.get_devices.return_value = devices mock.get_devices.return_value = devices
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_sleep_summary_since.return_value = sleep_summaries mock.get_sleep_summary_since.return_value = sleep_summaries
mock.list_notification_configurations.return_value = notifications mock.list_notification_configurations.return_value = notifications

View file

@ -0,0 +1,97 @@
[
{
"grpid": 1,
"attrib": 0,
"date": 1618605055,
"created": 1618605055,
"modified": 1618605055,
"category": 1,
"deviceid": "91a7e556c2022ef54dca6e07a853c3193734d148",
"hash_deviceid": "91a7e556c2022ef54dca6e07a853c3193734d148",
"measures": [
{
"type": 1,
"unit": 0,
"value": 71
},
{
"type": 8,
"unit": 0,
"value": 5
},
{
"type": 5,
"unit": 0,
"value": 60
},
{
"type": 76,
"unit": 0,
"value": 50
},
{
"type": 88,
"unit": 0,
"value": 10
},
{
"type": 4,
"unit": 0,
"value": 2
},
{
"type": 12,
"unit": 0,
"value": 40
},
{
"type": 71,
"unit": 0,
"value": 40
},
{
"type": 73,
"unit": 0,
"value": 20
},
{
"type": 6,
"unit": -3,
"value": 70
},
{
"type": 9,
"unit": 0,
"value": 70
},
{
"type": 10,
"unit": 0,
"value": 100
},
{
"type": 11,
"unit": 0,
"value": 60
},
{
"type": 54,
"unit": -2,
"value": 95
},
{
"type": 77,
"unit": -2,
"value": 95
},
{
"type": 91,
"unit": 0,
"value": 100
}
],
"modelid": 45,
"model": "BPM Connect",
"comment": null
}
]

View file

@ -212,6 +212,7 @@ async def test_webhooks_request_data(
client = await hass_client_no_auth() client = await hass_client_no_auth()
assert withings.get_measurement_since.call_count == 0
assert withings.get_measurement_in_period.call_count == 1 assert withings.get_measurement_in_period.call_count == 1
await call_webhook( await call_webhook(
@ -220,7 +221,8 @@ async def test_webhooks_request_data(
{"userid": USER_ID, "appli": NotificationCategory.WEIGHT}, {"userid": USER_ID, "appli": NotificationCategory.WEIGHT},
client, client,
) )
assert withings.get_measurement_in_period.call_count == 2 assert withings.get_measurement_since.call_count == 1
assert withings.get_measurement_in_period.call_count == 1
@pytest.mark.parametrize( @pytest.mark.parametrize(
@ -240,7 +242,7 @@ async def test_triggering_reauth(
"""Test triggering reauth.""" """Test triggering reauth."""
await setup_integration(hass, polling_config_entry, False) await setup_integration(hass, polling_config_entry, False)
withings.get_measurement_in_period.side_effect = error withings.get_measurement_since.side_effect = error
freezer.tick(timedelta(minutes=10)) freezer.tick(timedelta(minutes=10))
async_fire_time_changed(hass) async_fire_time_changed(hass)
await hass.async_block_till_done() await hass.async_block_till_done()

View file

@ -2,6 +2,7 @@
from datetime import timedelta from datetime import timedelta
from unittest.mock import AsyncMock from unittest.mock import AsyncMock
from aiowithings import MeasurementGroup
from freezegun.api import FrozenDateTimeFactory from freezegun.api import FrozenDateTimeFactory
import pytest import pytest
from syrupy import SnapshotAssertion from syrupy import SnapshotAssertion
@ -16,7 +17,11 @@ from homeassistant.helpers import entity_registry as er
from . import setup_integration from . import setup_integration
from .conftest import USER_ID from .conftest import USER_ID
from tests.common import MockConfigEntry, async_fire_time_changed from tests.common import (
MockConfigEntry,
async_fire_time_changed,
load_json_array_fixture,
)
async def async_get_entity_id( async def async_get_entity_id(
@ -57,7 +62,7 @@ async def test_update_failed(
"""Test all entities.""" """Test all entities."""
await setup_integration(hass, polling_config_entry, False) await setup_integration(hass, polling_config_entry, False)
withings.get_measurement_in_period.side_effect = Exception withings.get_measurement_since.side_effect = Exception
freezer.tick(timedelta(minutes=10)) freezer.tick(timedelta(minutes=10))
async_fire_time_changed(hass) async_fire_time_changed(hass)
await hass.async_block_till_done() await hass.async_block_till_done()
@ -65,3 +70,49 @@ async def test_update_failed(
state = hass.states.get("sensor.henk_weight") state = hass.states.get("sensor.henk_weight")
assert state is not None assert state is not None
assert state.state == STATE_UNAVAILABLE assert state.state == STATE_UNAVAILABLE
async def test_update_updates_incrementally(
hass: HomeAssistant,
snapshot: SnapshotAssertion,
withings: AsyncMock,
polling_config_entry: MockConfigEntry,
freezer: FrozenDateTimeFactory,
) -> None:
"""Test fetching new data updates since the last valid update."""
await setup_integration(hass, polling_config_entry, False)
async def _skip_10_minutes() -> None:
freezer.tick(timedelta(minutes=10))
async_fire_time_changed(hass)
await hass.async_block_till_done()
meas_json = load_json_array_fixture("withings/get_meas_1.json")
measurement_groups = [
MeasurementGroup.from_api(measurement) for measurement in meas_json
]
assert withings.get_measurement_since.call_args_list == []
await _skip_10_minutes()
assert (
str(withings.get_measurement_since.call_args_list[0].args[0])
== "2019-08-01 12:00:00+00:00"
)
withings.get_measurement_since.return_value = measurement_groups
await _skip_10_minutes()
assert (
str(withings.get_measurement_since.call_args_list[1].args[0])
== "2019-08-01 12:00:00+00:00"
)
await _skip_10_minutes()
assert (
str(withings.get_measurement_since.call_args_list[2].args[0])
== "2021-04-16 20:30:55+00:00"
)
state = hass.states.get("sensor.henk_weight")
assert state is not None
assert state.state == "71"
assert len(withings.get_measurement_in_period.call_args_list) == 1