Home Assistant Unofficial Reference 2024.12.1
coordinator.py
Go to the documentation of this file.
1 """Withings coordinator."""
2 
3 from __future__ import annotations
4 
5 from abc import abstractmethod
6 from datetime import date, datetime, timedelta
7 from typing import TYPE_CHECKING
8 
9 from aiowithings import (
10  Activity,
11  Device,
12  Goals,
13  MeasurementPosition,
14  MeasurementType,
15  NotificationCategory,
16  SleepSummary,
17  SleepSummaryDataFields,
18  WithingsAuthenticationFailedError,
19  WithingsClient,
20  WithingsUnauthorizedError,
21  Workout,
22  aggregate_measurements,
23 )
24 
25 from homeassistant.core import HomeAssistant
26 from homeassistant.exceptions import ConfigEntryAuthFailed
27 from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
28 from homeassistant.util import dt as dt_util
29 
30 from .const import LOGGER
31 
32 if TYPE_CHECKING:
33  from . import WithingsConfigEntry
34 
35 UPDATE_INTERVAL = timedelta(minutes=10)
36 
37 
38 class WithingsDataUpdateCoordinator[_DataT](DataUpdateCoordinator[_DataT]):
39  """Base coordinator."""
40 
41  config_entry: WithingsConfigEntry
42  _default_update_interval: timedelta | None = UPDATE_INTERVAL
43  _last_valid_update: datetime | None = None
44  webhooks_connected: bool = False
45  coordinator_name: str = ""
46 
47  def __init__(self, hass: HomeAssistant, client: WithingsClient) -> None:
48  """Initialize the Withings data coordinator."""
49  super().__init__(
50  hass,
51  LOGGER,
52  name="",
53  update_interval=self._default_update_interval,
54  )
55  self.namenamename = f"Withings {self.config_entry.unique_id} {self.coordinator_name}"
56  self._client_client = client
57  self.notification_categories: set[NotificationCategory] = set()
58 
59  def webhook_subscription_listener(self, connected: bool) -> None:
60  """Call when webhook status changed."""
61  self.webhooks_connectedwebhooks_connected = connected
62  if connected:
64  else:
65  self.update_intervalupdate_intervalupdate_intervalupdate_intervalupdate_interval = self._default_update_interval
66 
68  self, notification_category: NotificationCategory
69  ) -> None:
70  """Update data when webhook is called."""
71  LOGGER.debug(
72  "Withings webhook triggered for category %s for user %s",
73  notification_category,
74  self.config_entryconfig_entry.unique_id,
75  )
76  await self.async_request_refreshasync_request_refresh()
77 
78  async def _async_update_data(self) -> _DataT:
79  try:
80  return await self._internal_update_data_internal_update_data()
81  except (WithingsUnauthorizedError, WithingsAuthenticationFailedError) as exc:
82  raise ConfigEntryAuthFailed from exc
83 
84  @abstractmethod
85  async def _internal_update_data(self) -> _DataT:
86  """Update coordinator data."""
87 
88 
89 class WithingsMeasurementDataUpdateCoordinator(
90  WithingsDataUpdateCoordinator[
91  dict[tuple[MeasurementType, MeasurementPosition | None], float]
92  ]
93 ):
94  """Withings measurement coordinator."""
95 
96  coordinator_name: str = "measurements"
97 
98  def __init__(self, hass: HomeAssistant, client: WithingsClient) -> None:
99  """Initialize the Withings data coordinator."""
100  super().__init__(hass, client)
101  self.notification_categoriesnotification_categories = {
102  NotificationCategory.WEIGHT,
103  NotificationCategory.PRESSURE,
104  }
105  self._previous_data: dict[
106  tuple[MeasurementType, MeasurementPosition | None], float
107  ] = {}
108 
110  self,
111  ) -> dict[tuple[MeasurementType, MeasurementPosition | None], float]:
112  """Retrieve measurement data."""
113  if self._last_valid_update_last_valid_update is None:
114  now = dt_util.utcnow()
115  startdate = now - timedelta(days=14)
116  measurements = await self._client_client.get_measurement_in_period(startdate, now)
117  else:
118  measurements = await self._client_client.get_measurement_since(
119  self._last_valid_update_last_valid_update
120  )
121 
122  if measurements:
123  self._last_valid_update_last_valid_update = measurements[0].taken_at
124  aggregated_measurements = aggregate_measurements(measurements)
125  self._previous_data.update(aggregated_measurements)
126  return self._previous_data
127 
128 
130  WithingsDataUpdateCoordinator[SleepSummary | None]
131 ):
132  """Withings sleep coordinator."""
133 
134  coordinator_name: str = "sleep"
135 
136  def __init__(self, hass: HomeAssistant, client: WithingsClient) -> None:
137  """Initialize the Withings data coordinator."""
138  super().__init__(hass, client)
139  self.notification_categoriesnotification_categories = {
140  NotificationCategory.SLEEP,
141  }
142 
143  async def _internal_update_data(self) -> SleepSummary | None:
144  """Retrieve sleep data."""
145  now = dt_util.now()
146  yesterday = now - timedelta(days=1)
147  yesterday_noon = dt_util.start_of_local_day(yesterday) + timedelta(hours=12)
148  yesterday_noon_utc = dt_util.as_utc(yesterday_noon)
149 
150  response = await self._client_client.get_sleep_summary_since(
151  sleep_summary_since=yesterday_noon_utc,
152  sleep_summary_data_fields=[
153  SleepSummaryDataFields.BREATHING_DISTURBANCES_INTENSITY,
154  SleepSummaryDataFields.DEEP_SLEEP_DURATION,
155  SleepSummaryDataFields.SLEEP_LATENCY,
156  SleepSummaryDataFields.WAKE_UP_LATENCY,
157  SleepSummaryDataFields.AVERAGE_HEART_RATE,
158  SleepSummaryDataFields.MIN_HEART_RATE,
159  SleepSummaryDataFields.MAX_HEART_RATE,
160  SleepSummaryDataFields.LIGHT_SLEEP_DURATION,
161  SleepSummaryDataFields.REM_SLEEP_DURATION,
162  SleepSummaryDataFields.AVERAGE_RESPIRATION_RATE,
163  SleepSummaryDataFields.MIN_RESPIRATION_RATE,
164  SleepSummaryDataFields.MAX_RESPIRATION_RATE,
165  SleepSummaryDataFields.SLEEP_SCORE,
166  SleepSummaryDataFields.SNORING,
167  SleepSummaryDataFields.SNORING_COUNT,
168  SleepSummaryDataFields.WAKE_UP_COUNT,
169  SleepSummaryDataFields.TOTAL_TIME_AWAKE,
170  ],
171  )
172  if not response:
173  return None
174 
175  return sorted(
176  response, key=lambda sleep_summary: sleep_summary.end_date, reverse=True
177  )[0]
178 
179 
181  """Withings bed presence coordinator."""
182 
183  coordinator_name: str = "bed presence"
184  in_bed: bool | None = None
185  _default_update_interval = None
186 
187  def __init__(self, hass: HomeAssistant, client: WithingsClient) -> None:
188  """Initialize the Withings data coordinator."""
189  super().__init__(hass, client)
190  self.notification_categoriesnotification_categories = {
191  NotificationCategory.IN_BED,
192  NotificationCategory.OUT_BED,
193  }
194 
196  self, notification_category: NotificationCategory
197  ) -> None:
198  """Only set new in bed value instead of refresh."""
199  self.in_bedin_bed = notification_category == NotificationCategory.IN_BED
200  self.async_update_listenersasync_update_listeners()
201 
202  async def _internal_update_data(self) -> None:
203  """Update coordinator data."""
204 
205 
206 class WithingsGoalsDataUpdateCoordinator(WithingsDataUpdateCoordinator[Goals]):
207  """Withings goals coordinator."""
208 
209  coordinator_name: str = "goals"
210  _default_update_interval = timedelta(hours=1)
211 
212  def webhook_subscription_listener(self, connected: bool) -> None:
213  """Call when webhook status changed."""
214  # Webhooks aren't available for this datapoint, so we keep polling
215 
216  async def _internal_update_data(self) -> Goals:
217  """Retrieve goals data."""
218  return await self._client_client.get_goals()
219 
220 
222  WithingsDataUpdateCoordinator[Activity | None]
223 ):
224  """Withings activity coordinator."""
225 
226  coordinator_name: str = "activity"
227  _previous_data: Activity | None = None
228 
229  def __init__(self, hass: HomeAssistant, client: WithingsClient) -> None:
230  """Initialize the Withings data coordinator."""
231  super().__init__(hass, client)
232  self.notification_categoriesnotification_categories = {
233  NotificationCategory.ACTIVITY,
234  }
235 
236  async def _internal_update_data(self) -> Activity | None:
237  """Retrieve latest activity."""
238  if self._last_valid_update_last_valid_update is None:
239  now = dt_util.utcnow()
240  startdate = now - timedelta(days=14)
241  activities = await self._client_client.get_activities_in_period(
242  startdate.date(), now.date()
243  )
244  else:
245  activities = await self._client_client.get_activities_since(
246  self._last_valid_update_last_valid_update
247  )
248 
249  today = date.today()
250  for activity in activities:
251  if activity.date == today:
252  self._previous_data_previous_data = activity
253  self._last_valid_update_last_valid_update = activity.modified
254  return activity
255  if self._previous_data_previous_data and self._previous_data_previous_data.date == today:
256  return self._previous_data_previous_data
257  return None
258 
259 
261  WithingsDataUpdateCoordinator[Workout | None]
262 ):
263  """Withings workout coordinator."""
264 
265  coordinator_name: str = "workout"
266  _previous_data: Workout | None = None
267 
268  def __init__(self, hass: HomeAssistant, client: WithingsClient) -> None:
269  """Initialize the Withings data coordinator."""
270  super().__init__(hass, client)
271  self.notification_categoriesnotification_categories = {
272  NotificationCategory.ACTIVITY,
273  }
274 
275  async def _internal_update_data(self) -> Workout | None:
276  """Retrieve latest workout."""
277  if self._last_valid_update_last_valid_update is None:
278  now = dt_util.utcnow()
279  startdate = now - timedelta(days=14)
280  workouts = await self._client_client.get_workouts_in_period(
281  startdate.date(), now.date()
282  )
283  else:
284  workouts = await self._client_client.get_workouts_since(self._last_valid_update_last_valid_update)
285  if not workouts:
286  return self._previous_data_previous_data
287  latest_workout = max(workouts, key=lambda workout: workout.end_date)
288  if (
289  self._previous_data_previous_data is None
290  or self._previous_data_previous_data.end_date >= latest_workout.end_date
291  ):
292  self._previous_data_previous_data = latest_workout
293  self._last_valid_update_last_valid_update = latest_workout.end_date
294  return self._previous_data_previous_data
295 
296 
298  WithingsDataUpdateCoordinator[dict[str, Device]]
299 ):
300  """Withings device coordinator."""
301 
302  coordinator_name: str = "device"
303  _default_update_interval = timedelta(hours=1)
304 
305  async def _internal_update_data(self) -> dict[str, Device]:
306  """Update coordinator data."""
307  devices = await self._client_client.get_devices()
308  return {device.device_id: device for device in devices}
None __init__(self, HomeAssistant hass, WithingsClient client)
Definition: coordinator.py:229
None async_webhook_data_updated(self, NotificationCategory notification_category)
Definition: coordinator.py:197
None async_webhook_data_updated(self, NotificationCategory notification_category)
Definition: coordinator.py:69
None __init__(self, HomeAssistant hass, WithingsClient client)
Definition: coordinator.py:47
dict[tuple[MeasurementType, MeasurementPosition|None], float] _internal_update_data(self)
Definition: coordinator.py:111
None __init__(self, HomeAssistant hass, WithingsClient client)
Definition: coordinator.py:136
None __init__(self, HomeAssistant hass, WithingsClient client)
Definition: coordinator.py:268
IssData update(pyiss.ISS iss)
Definition: __init__.py:33