Home Assistant Unofficial Reference 2024.12.1
coordinator.py
Go to the documentation of this file.
1 """Coordinator to fetch data from the Picnic API."""
2 
3 import asyncio
4 from contextlib import suppress
5 import copy
6 from datetime import timedelta
7 import logging
8 
9 from python_picnic_api import PicnicAPI
10 from python_picnic_api.session import PicnicAuthError
11 
12 from homeassistant.config_entries import ConfigEntry
13 from homeassistant.const import CONF_ACCESS_TOKEN
14 from homeassistant.core import HomeAssistant, callback
15 from homeassistant.exceptions import ConfigEntryAuthFailed
16 from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
17 
18 from .const import ADDRESS, CART_DATA, LAST_ORDER_DATA, NEXT_DELIVERY_DATA, SLOT_DATA
19 
20 
22  """The coordinator to fetch data from the Picnic API at a set interval."""
23 
24  def __init__(
25  self,
26  hass: HomeAssistant,
27  picnic_api_client: PicnicAPI,
28  config_entry: ConfigEntry,
29  ) -> None:
30  """Initialize the coordinator with the given Picnic API client."""
31  self.picnic_api_clientpicnic_api_client = picnic_api_client
32  self.config_entryconfig_entryconfig_entry = config_entry
33  self._user_address_user_address = None
34 
35  logger = logging.getLogger(__name__)
36  super().__init__(
37  hass,
38  logger,
39  name="Picnic coordinator",
40  update_interval=timedelta(minutes=30),
41  )
42 
43  async def _async_update_data(self) -> dict:
44  """Fetch data from API endpoint."""
45  try:
46  # Note: TimeoutError and aiohttp.ClientError are already
47  # handled by the data update coordinator.
48  async with asyncio.timeout(10):
49  data = await self.hasshass.async_add_executor_job(self.fetch_datafetch_data)
50 
51  # Update the auth token in the config entry if applicable
52  self._update_auth_token_update_auth_token()
53  except ValueError as error:
54  raise UpdateFailed(f"API response was malformed: {error}") from error
55  except PicnicAuthError as error:
56  raise ConfigEntryAuthFailed from error
57 
58  # Return the fetched data
59  return data
60 
61  def fetch_data(self):
62  """Fetch the data from the Picnic API and return a flat dict with only needed sensor data."""
63  # Fetch from the API and pre-process the data
64  if not (cart := self.picnic_api_clientpicnic_api_client.get_cart()):
65  raise UpdateFailed("API response doesn't contain expected data.")
66 
67  next_delivery, last_order = self._get_order_data_get_order_data()
68  slot_data = self._get_slot_data_get_slot_data(cart)
69 
70  return {
71  ADDRESS: self._get_address_get_address(),
72  CART_DATA: cart,
73  SLOT_DATA: slot_data,
74  NEXT_DELIVERY_DATA: next_delivery,
75  LAST_ORDER_DATA: last_order,
76  }
77 
78  def _get_address(self):
79  """Get the address that identifies the Picnic service."""
80  if self._user_address_user_address is None:
81  address = self.picnic_api_clientpicnic_api_client.get_user()["address"]
82  self._user_address_user_address = f'{address["street"]} {address["house_number"]}{address["house_number_ext"]}'
83 
84  return self._user_address_user_address
85 
86  @staticmethod
87  def _get_slot_data(cart: dict) -> dict:
88  """Get the selected slot, if it's explicitly selected."""
89  selected_slot = cart.get("selected_slot", {})
90  available_slots = cart.get("delivery_slots", [])
91 
92  if selected_slot.get("state") == "EXPLICIT":
93  slot_data = filter(
94  lambda slot: slot.get("slot_id") == selected_slot.get("slot_id"),
95  available_slots,
96  )
97  if slot_data:
98  return next(slot_data)
99 
100  return {}
101 
102  def _get_order_data(self) -> tuple[dict, dict]:
103  """Get data of the last order from the list of deliveries."""
104  # Get the deliveries
105  deliveries = self.picnic_api_clientpicnic_api_client.get_deliveries(summary=True)
106 
107  # Determine the last order and return an empty dict if there is none
108  try:
109  # Filter on status CURRENT and select the last on the list which is the first one to be delivered
110  # Make a deepcopy because some references are local
111  next_deliveries = list(
112  filter(lambda d: d["status"] == "CURRENT", deliveries)
113  )
114  next_delivery = (
115  copy.deepcopy(next_deliveries[-1]) if next_deliveries else {}
116  )
117  last_order = copy.deepcopy(deliveries[0]) if deliveries else {}
118  except (KeyError, TypeError):
119  # A KeyError or TypeError indicate that the response contains unexpected data
120  return {}, {}
121 
122  # Get the next order's position details if there is an undelivered order
123  delivery_position = {}
124  if next_delivery and not next_delivery.get("delivery_time"):
125  # ValueError: If no information yet can mean an empty response
126  with suppress(ValueError):
127  delivery_position = self.picnic_api_clientpicnic_api_client.get_delivery_position(
128  next_delivery["delivery_id"]
129  )
130 
131  # Determine the ETA, if available, the one from the delivery position API is more precise
132  # but, it's only available shortly before the actual delivery.
133  next_delivery["eta"] = delivery_position.get(
134  "eta_window", next_delivery.get("eta2", {})
135  )
136  if "eta2" in next_delivery:
137  del next_delivery["eta2"]
138 
139  # Determine the total price by adding up the total price of all sub-orders
140  total_price = 0
141  for order in last_order.get("orders", []):
142  total_price += order.get("total_price", 0)
143  last_order["total_price"] = total_price
144 
145  # Make sure delivery_time is a dict
146  last_order.setdefault("delivery_time", {})
147 
148  return next_delivery, last_order
149 
150  @callback
152  """Set the updated authentication token."""
153  updated_token = self.picnic_api_clientpicnic_api_client.session.auth_token
154  if self.config_entryconfig_entryconfig_entry.data.get(CONF_ACCESS_TOKEN) != updated_token:
155  # Create an updated data dict
156  data = {**self.config_entryconfig_entryconfig_entry.data, CONF_ACCESS_TOKEN: updated_token}
157 
158  # Update the config entry
159  self.hasshass.config_entries.async_update_entry(self.config_entryconfig_entryconfig_entry, data=data)
None __init__(self, HomeAssistant hass, PicnicAPI picnic_api_client, ConfigEntry config_entry)
Definition: coordinator.py:29