diff --git a/homeassistant/components/withings/coordinator.py b/homeassistant/components/withings/coordinator.py index b87cb550a13..c5192ba3466 100644 --- a/homeassistant/components/withings/coordinator.py +++ b/homeassistant/components/withings/coordinator.py @@ -1,6 +1,6 @@ """Withings coordinator.""" from abc import abstractmethod -from datetime import timedelta +from datetime import datetime, timedelta from typing import TypeVar from aiowithings import ( @@ -33,6 +33,7 @@ class WithingsDataUpdateCoordinator(DataUpdateCoordinator[_T]): config_entry: ConfigEntry _default_update_interval: timedelta | None = UPDATE_INTERVAL + _last_valid_update: datetime | None = None def __init__(self, hass: HomeAssistant, client: WithingsClient) -> None: """Initialize the Withings data coordinator.""" @@ -80,15 +81,24 @@ class WithingsMeasurementDataUpdateCoordinator( NotificationCategory.ACTIVITY, NotificationCategory.PRESSURE, } + self._previous_data: dict[MeasurementType, float] = {} async def _internal_update_data(self) -> dict[MeasurementType, float]: """Retrieve measurement data.""" - now = dt_util.utcnow() - startdate = now - timedelta(days=7) + if self._last_valid_update is None: + 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) - - return aggregate_measurements(response) + if measurements: + self._last_valid_update = measurements[0].taken_at + aggregated_measurements = aggregate_measurements(measurements) + self._previous_data.update(aggregated_measurements) + return self._previous_data class WithingsSleepDataUpdateCoordinator( diff --git a/tests/components/withings/conftest.py b/tests/components/withings/conftest.py index 5c4a1db1182..3f3a82a03f3 100644 --- a/tests/components/withings/conftest.py +++ b/tests/components/withings/conftest.py @@ -151,6 +151,7 @@ def mock_withings(): mock = AsyncMock(spec=WithingsClient) mock.get_devices.return_value = devices 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.list_notification_configurations.return_value = notifications diff --git a/tests/components/withings/fixtures/get_meas_1.json b/tests/components/withings/fixtures/get_meas_1.json new file mode 100644 index 00000000000..a1415695746 --- /dev/null +++ b/tests/components/withings/fixtures/get_meas_1.json @@ -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 + } +] diff --git a/tests/components/withings/test_init.py b/tests/components/withings/test_init.py index 7576802999e..72b9b495344 100644 --- a/tests/components/withings/test_init.py +++ b/tests/components/withings/test_init.py @@ -212,6 +212,7 @@ async def test_webhooks_request_data( client = await hass_client_no_auth() + assert withings.get_measurement_since.call_count == 0 assert withings.get_measurement_in_period.call_count == 1 await call_webhook( @@ -220,7 +221,8 @@ async def test_webhooks_request_data( {"userid": USER_ID, "appli": NotificationCategory.WEIGHT}, 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( @@ -240,7 +242,7 @@ async def test_triggering_reauth( """Test triggering reauth.""" 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)) async_fire_time_changed(hass) await hass.async_block_till_done() diff --git a/tests/components/withings/test_sensor.py b/tests/components/withings/test_sensor.py index dd3dee1bb4d..3a937a5f686 100644 --- a/tests/components/withings/test_sensor.py +++ b/tests/components/withings/test_sensor.py @@ -2,6 +2,7 @@ from datetime import timedelta from unittest.mock import AsyncMock +from aiowithings import MeasurementGroup from freezegun.api import FrozenDateTimeFactory import pytest from syrupy import SnapshotAssertion @@ -16,7 +17,11 @@ from homeassistant.helpers import entity_registry as er from . import setup_integration 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( @@ -57,7 +62,7 @@ async def test_update_failed( """Test all entities.""" 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)) async_fire_time_changed(hass) await hass.async_block_till_done() @@ -65,3 +70,49 @@ async def test_update_failed( state = hass.states.get("sensor.henk_weight") assert state is not None 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