mirror of
https://github.com/home-assistant/core.git
synced 2026-08-24 10:13:52 -05:00
chore: update python-picnic-api2 to v2.0.1 (#178395)
This commit is contained in:
@@ -192,7 +192,7 @@ class PicnicConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
CONF_ACCESS_TOKEN: auth_token,
|
||||
CONF_COUNTRY_CODE: user_input[CONF_COUNTRY_CODE],
|
||||
}
|
||||
existing_entry = await self.async_set_unique_id(user_data["user_id"])
|
||||
existing_entry = await self.async_set_unique_id(user_data.user_id)
|
||||
|
||||
# Abort if we're adding a new config and the unique id
|
||||
# is already in use, else create the entry
|
||||
|
||||
@@ -2,12 +2,13 @@
|
||||
|
||||
import asyncio
|
||||
from contextlib import suppress
|
||||
import copy
|
||||
from dataclasses import dataclass
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
from typing import override
|
||||
|
||||
from python_picnic_api2 import PicnicAPI
|
||||
from python_picnic_api2.models import Cart, DeliverySummary, Slot
|
||||
from python_picnic_api2.session import PicnicAuthError
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
@@ -32,6 +33,25 @@ from .const import (
|
||||
type PicnicConfigEntry = ConfigEntry[PicnicUpdateCoordinator]
|
||||
|
||||
|
||||
@dataclass
|
||||
class NextDeliveryData:
|
||||
"""The next (current, undelivered) delivery, with its live ETA."""
|
||||
|
||||
delivery: DeliverySummary | None = None
|
||||
eta_start: str | None = None
|
||||
eta_end: str | None = None
|
||||
estimated_arrival: int | None = None
|
||||
|
||||
|
||||
@dataclass
|
||||
class LastOrderData:
|
||||
"""The most recent delivery, with its total price."""
|
||||
|
||||
delivery: DeliverySummary | None = None
|
||||
total_price: int = 0
|
||||
delivery_time_start: str | None = None
|
||||
|
||||
|
||||
class PicnicUpdateCoordinator(DataUpdateCoordinator):
|
||||
"""The coordinator to fetch data from the Picnic API at a set interval."""
|
||||
|
||||
@@ -86,21 +106,20 @@ class PicnicUpdateCoordinator(DataUpdateCoordinator):
|
||||
return data
|
||||
|
||||
@staticmethod
|
||||
def _get_update_interval(next_delivery: dict | None) -> timedelta:
|
||||
def _get_update_interval(next_delivery: NextDeliveryData | None) -> timedelta:
|
||||
"""Poll faster around the delivery so the live ETA is picked up in time."""
|
||||
if not next_delivery:
|
||||
if next_delivery is None or next_delivery.delivery is None:
|
||||
return DEFAULT_UPDATE_INTERVAL
|
||||
|
||||
eta = next_delivery.get("eta")
|
||||
slot = next_delivery.get("slot")
|
||||
slot = next_delivery.delivery.slot
|
||||
|
||||
start = end = None
|
||||
if eta:
|
||||
start = dt_util.parse_datetime(str(eta.get("start")))
|
||||
end = dt_util.parse_datetime(str(eta.get("end")))
|
||||
if next_delivery.eta_start and next_delivery.eta_end:
|
||||
start = dt_util.parse_datetime(next_delivery.eta_start)
|
||||
end = dt_util.parse_datetime(next_delivery.eta_end)
|
||||
if (start is None or end is None) and slot:
|
||||
start = dt_util.parse_datetime(str(slot.get("window_start")))
|
||||
end = dt_util.parse_datetime(str(slot.get("window_end")))
|
||||
start = dt_util.parse_datetime(str(slot.window_start))
|
||||
end = dt_util.parse_datetime(str(slot.window_end))
|
||||
|
||||
if start is None or end is None:
|
||||
return DEFAULT_UPDATE_INTERVAL
|
||||
@@ -129,12 +148,11 @@ class PicnicUpdateCoordinator(DataUpdateCoordinator):
|
||||
raise UpdateFailed("API response doesn't contain expected data.")
|
||||
|
||||
next_delivery, last_order = self._get_order_data()
|
||||
slot_data = self._get_slot_data(cart)
|
||||
|
||||
return {
|
||||
ADDRESS: self._get_address(),
|
||||
CART_DATA: cart,
|
||||
SLOT_DATA: slot_data,
|
||||
SLOT_DATA: self._get_slot_data(cart),
|
||||
NEXT_DELIVERY_DATA: next_delivery,
|
||||
LAST_ORDER_DATA: last_order,
|
||||
}
|
||||
@@ -142,85 +160,85 @@ class PicnicUpdateCoordinator(DataUpdateCoordinator):
|
||||
def _get_address(self):
|
||||
"""Get the address that identifies the Picnic service."""
|
||||
if self._user_address is None:
|
||||
address = self.picnic_api_client.get_user()["address"]
|
||||
address = self.picnic_api_client.get_user().address
|
||||
self._user_address = (
|
||||
f"{address['street']} "
|
||||
f"{address['house_number']}{address['house_number_ext']}"
|
||||
f"{address.street} "
|
||||
f"{address.house_number}{address.house_number_ext or ''}"
|
||||
)
|
||||
|
||||
return self._user_address
|
||||
|
||||
@staticmethod
|
||||
def _get_slot_data(cart: dict) -> dict:
|
||||
def _get_slot_data(cart: Cart) -> Slot | None:
|
||||
"""Get the selected slot, if it's explicitly selected."""
|
||||
selected_slot = cart.get("selected_slot", {})
|
||||
available_slots = cart.get("delivery_slots", [])
|
||||
selected_slot = cart.selected_slot
|
||||
|
||||
if selected_slot.get("state") == "EXPLICIT":
|
||||
slot_data = filter(
|
||||
lambda slot: slot.get("slot_id") == selected_slot.get("slot_id"),
|
||||
available_slots,
|
||||
)
|
||||
if slot_data:
|
||||
return next(slot_data)
|
||||
if selected_slot and selected_slot.state == "EXPLICIT":
|
||||
for slot in cart.delivery_slots:
|
||||
if slot.slot_id == selected_slot.slot_id:
|
||||
return slot
|
||||
|
||||
return {}
|
||||
return None
|
||||
|
||||
def _get_order_data(self) -> tuple[dict, dict]:
|
||||
@staticmethod
|
||||
def _delivery_time(delivery: DeliverySummary) -> dict | None:
|
||||
"""Return the raw delivery-time window; not a field the library models."""
|
||||
return delivery.raw.get("delivery_time") if delivery.raw else None
|
||||
|
||||
def _get_order_data(self) -> tuple[NextDeliveryData, LastOrderData]:
|
||||
"""Get data of the last order from the list of deliveries."""
|
||||
# Get the deliveries
|
||||
deliveries = self.picnic_api_client.get_deliveries(summary=True)
|
||||
|
||||
# Determine the last order and return an empty dict if there is none
|
||||
# Determine the last order and return empty data if there is none
|
||||
try:
|
||||
# Filter on status CURRENT and select the last
|
||||
# on the list which is the first one to be delivered
|
||||
# Make a deepcopy because some references are local
|
||||
next_deliveries = list(
|
||||
filter(lambda d: d["status"] == "CURRENT", deliveries)
|
||||
)
|
||||
next_delivery = (
|
||||
copy.deepcopy(next_deliveries[-1]) if next_deliveries else {}
|
||||
)
|
||||
last_order = copy.deepcopy(deliveries[0]) if deliveries else {}
|
||||
except KeyError, TypeError:
|
||||
# A KeyError or TypeError indicate that the
|
||||
next_deliveries = [d for d in deliveries if d.status == "CURRENT"]
|
||||
next_delivery = next_deliveries[-1] if next_deliveries else None
|
||||
last_order = deliveries[0] if deliveries else None
|
||||
except AttributeError, TypeError:
|
||||
# An AttributeError or TypeError indicate that the
|
||||
# response contains unexpected data
|
||||
return {}, {}
|
||||
return NextDeliveryData(), LastOrderData()
|
||||
|
||||
if last_order is None:
|
||||
return NextDeliveryData(), LastOrderData()
|
||||
|
||||
# Get the next order's position details if there is an undelivered order
|
||||
delivery_position = {}
|
||||
if next_delivery and not next_delivery.get("delivery_time"):
|
||||
if next_delivery and not self._delivery_time(next_delivery):
|
||||
# ValueError: If no information yet can mean an empty response
|
||||
with suppress(ValueError):
|
||||
delivery_position = self.picnic_api_client.get_delivery_position(
|
||||
next_delivery["delivery_id"]
|
||||
next_delivery.delivery_id
|
||||
)
|
||||
|
||||
# Determine the ETA, if available, the one from the
|
||||
# delivery position API is more precise
|
||||
# but, it's only available shortly before the actual delivery.
|
||||
next_delivery["eta"] = delivery_position.get(
|
||||
"eta_window", next_delivery.get("eta2", {})
|
||||
eta_window = delivery_position.get("eta_window") or {}
|
||||
eta2 = next_delivery.eta2 if next_delivery else None
|
||||
next_delivery_data = NextDeliveryData(
|
||||
delivery=next_delivery,
|
||||
eta_start=eta_window.get("start") or (eta2.start if eta2 else None),
|
||||
eta_end=eta_window.get("end") or (eta2.end if eta2 else None),
|
||||
# The position response's eta (unix timestamp in milliseconds) feeds
|
||||
# the estimated arrival sensor; the API only serves it shortly before
|
||||
# the delivery, so that sensor is unknown outside that window
|
||||
estimated_arrival=delivery_position.get("eta"),
|
||||
)
|
||||
if "eta2" in next_delivery:
|
||||
del next_delivery["eta2"]
|
||||
|
||||
# The position response's eta (unix timestamp in milliseconds) feeds
|
||||
# the estimated arrival sensor; the API only serves it shortly before
|
||||
# the delivery, so that sensor is unknown outside that window
|
||||
next_delivery["estimated_arrival"] = delivery_position.get("eta")
|
||||
|
||||
# Determine the total price by adding up the total price of all sub-orders
|
||||
total_price = 0
|
||||
for order in last_order.get("orders", []):
|
||||
total_price += order.get("total_price", 0)
|
||||
last_order["total_price"] = total_price
|
||||
total_price = sum(order.total_price or 0 for order in last_order.orders)
|
||||
delivery_time = self._delivery_time(last_order)
|
||||
last_order_data = LastOrderData(
|
||||
delivery=last_order,
|
||||
total_price=total_price,
|
||||
delivery_time_start=delivery_time.get("start") if delivery_time else None,
|
||||
)
|
||||
|
||||
# Make sure delivery_time is a dict
|
||||
last_order.setdefault("delivery_time", {})
|
||||
|
||||
return next_delivery, last_order
|
||||
return next_delivery_data, last_order_data
|
||||
|
||||
@callback
|
||||
def _update_auth_token(self):
|
||||
|
||||
@@ -7,5 +7,5 @@
|
||||
"integration_type": "service",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["python_picnic_api2"],
|
||||
"requirements": ["python-picnic-api2==1.3.4"]
|
||||
"requirements": ["python-picnic-api2==2.0.1"]
|
||||
}
|
||||
|
||||
@@ -39,7 +39,17 @@ from .const import (
|
||||
SENSOR_SELECTED_SLOT_MIN_ORDER_VALUE,
|
||||
SENSOR_SELECTED_SLOT_START,
|
||||
)
|
||||
from .coordinator import PicnicConfigEntry, PicnicUpdateCoordinator
|
||||
from .coordinator import (
|
||||
LastOrderData,
|
||||
NextDeliveryData,
|
||||
PicnicConfigEntry,
|
||||
PicnicUpdateCoordinator,
|
||||
)
|
||||
|
||||
_EMPTY_DATA_FACTORIES: dict[str, Callable[[], Any]] = {
|
||||
"next_delivery_data": NextDeliveryData,
|
||||
"last_order_data": LastOrderData,
|
||||
}
|
||||
|
||||
|
||||
@dataclass(frozen=True, kw_only=True)
|
||||
@@ -59,35 +69,41 @@ SENSOR_TYPES: tuple[PicnicSensorEntityDescription, ...] = (
|
||||
key=SENSOR_CART_ITEMS_COUNT,
|
||||
translation_key=SENSOR_CART_ITEMS_COUNT,
|
||||
data_type="cart_data",
|
||||
value_fn=lambda cart: cart.get("total_count", 0),
|
||||
value_fn=lambda cart: (cart.total_count or 0) if cart else 0,
|
||||
),
|
||||
PicnicSensorEntityDescription(
|
||||
key=SENSOR_CART_TOTAL_PRICE,
|
||||
translation_key=SENSOR_CART_TOTAL_PRICE,
|
||||
native_unit_of_measurement=CURRENCY_EURO,
|
||||
data_type="cart_data",
|
||||
value_fn=lambda cart: cart.get("total_price", 0) / 100,
|
||||
value_fn=lambda cart: ((cart.total_price or 0) if cart else 0) / 100,
|
||||
),
|
||||
PicnicSensorEntityDescription(
|
||||
key=SENSOR_SELECTED_SLOT_START,
|
||||
translation_key=SENSOR_SELECTED_SLOT_START,
|
||||
device_class=SensorDeviceClass.TIMESTAMP,
|
||||
data_type="slot_data",
|
||||
value_fn=lambda slot: dt_util.parse_datetime(str(slot.get("window_start"))),
|
||||
value_fn=lambda slot: (
|
||||
dt_util.parse_datetime(str(slot.window_start)) if slot else None
|
||||
),
|
||||
),
|
||||
PicnicSensorEntityDescription(
|
||||
key=SENSOR_SELECTED_SLOT_END,
|
||||
translation_key=SENSOR_SELECTED_SLOT_END,
|
||||
device_class=SensorDeviceClass.TIMESTAMP,
|
||||
data_type="slot_data",
|
||||
value_fn=lambda slot: dt_util.parse_datetime(str(slot.get("window_end"))),
|
||||
value_fn=lambda slot: (
|
||||
dt_util.parse_datetime(str(slot.window_end)) if slot else None
|
||||
),
|
||||
),
|
||||
PicnicSensorEntityDescription(
|
||||
key=SENSOR_SELECTED_SLOT_MAX_ORDER_TIME,
|
||||
translation_key=SENSOR_SELECTED_SLOT_MAX_ORDER_TIME,
|
||||
device_class=SensorDeviceClass.TIMESTAMP,
|
||||
data_type="slot_data",
|
||||
value_fn=lambda slot: dt_util.parse_datetime(str(slot.get("cut_off_time"))),
|
||||
value_fn=lambda slot: (
|
||||
dt_util.parse_datetime(str(slot.cut_off_time)) if slot else None
|
||||
),
|
||||
),
|
||||
PicnicSensorEntityDescription(
|
||||
key=SENSOR_SELECTED_SLOT_MIN_ORDER_VALUE,
|
||||
@@ -95,8 +111,8 @@ SENSOR_TYPES: tuple[PicnicSensorEntityDescription, ...] = (
|
||||
native_unit_of_measurement=CURRENCY_EURO,
|
||||
data_type="slot_data",
|
||||
value_fn=lambda slot: (
|
||||
slot["minimum_order_value"] / 100
|
||||
if slot.get("minimum_order_value")
|
||||
slot.minimum_order_value / 100
|
||||
if slot and slot.minimum_order_value
|
||||
else None
|
||||
),
|
||||
),
|
||||
@@ -105,8 +121,10 @@ SENSOR_TYPES: tuple[PicnicSensorEntityDescription, ...] = (
|
||||
translation_key=SENSOR_LAST_ORDER_SLOT_START,
|
||||
device_class=SensorDeviceClass.TIMESTAMP,
|
||||
data_type="last_order_data",
|
||||
value_fn=lambda last_order: dt_util.parse_datetime(
|
||||
str(last_order.get("slot", {}).get("window_start"))
|
||||
value_fn=lambda last_order: (
|
||||
dt_util.parse_datetime(str(last_order.delivery.slot.window_start))
|
||||
if last_order.delivery and last_order.delivery.slot
|
||||
else None
|
||||
),
|
||||
),
|
||||
PicnicSensorEntityDescription(
|
||||
@@ -114,23 +132,29 @@ SENSOR_TYPES: tuple[PicnicSensorEntityDescription, ...] = (
|
||||
translation_key=SENSOR_LAST_ORDER_SLOT_END,
|
||||
device_class=SensorDeviceClass.TIMESTAMP,
|
||||
data_type="last_order_data",
|
||||
value_fn=lambda last_order: dt_util.parse_datetime(
|
||||
str(last_order.get("slot", {}).get("window_end"))
|
||||
value_fn=lambda last_order: (
|
||||
dt_util.parse_datetime(str(last_order.delivery.slot.window_end))
|
||||
if last_order.delivery and last_order.delivery.slot
|
||||
else None
|
||||
),
|
||||
),
|
||||
PicnicSensorEntityDescription(
|
||||
key=SENSOR_LAST_ORDER_STATUS,
|
||||
translation_key=SENSOR_LAST_ORDER_STATUS,
|
||||
data_type="last_order_data",
|
||||
value_fn=lambda last_order: last_order.get("status"),
|
||||
value_fn=lambda last_order: (
|
||||
last_order.delivery.status if last_order.delivery else None
|
||||
),
|
||||
),
|
||||
PicnicSensorEntityDescription(
|
||||
key=SENSOR_LAST_ORDER_MAX_ORDER_TIME,
|
||||
translation_key=SENSOR_LAST_ORDER_MAX_ORDER_TIME,
|
||||
device_class=SensorDeviceClass.TIMESTAMP,
|
||||
data_type="last_order_data",
|
||||
value_fn=lambda last_order: dt_util.parse_datetime(
|
||||
str(last_order.get("slot", {}).get("cut_off_time"))
|
||||
value_fn=lambda last_order: (
|
||||
dt_util.parse_datetime(str(last_order.delivery.slot.cut_off_time))
|
||||
if last_order.delivery and last_order.delivery.slot
|
||||
else None
|
||||
),
|
||||
),
|
||||
PicnicSensorEntityDescription(
|
||||
@@ -139,7 +163,7 @@ SENSOR_TYPES: tuple[PicnicSensorEntityDescription, ...] = (
|
||||
device_class=SensorDeviceClass.TIMESTAMP,
|
||||
data_type="last_order_data",
|
||||
value_fn=lambda last_order: dt_util.parse_datetime(
|
||||
str(last_order.get("delivery_time", {}).get("start"))
|
||||
str(last_order.delivery_time_start)
|
||||
),
|
||||
),
|
||||
PicnicSensorEntityDescription(
|
||||
@@ -147,7 +171,7 @@ SENSOR_TYPES: tuple[PicnicSensorEntityDescription, ...] = (
|
||||
translation_key=SENSOR_LAST_ORDER_TOTAL_PRICE,
|
||||
native_unit_of_measurement=CURRENCY_EURO,
|
||||
data_type="last_order_data",
|
||||
value_fn=lambda last_order: last_order.get("total_price", 0) / 100,
|
||||
value_fn=lambda last_order: last_order.total_price / 100,
|
||||
),
|
||||
PicnicSensorEntityDescription(
|
||||
key=SENSOR_NEXT_DELIVERY_ETA_START,
|
||||
@@ -155,7 +179,7 @@ SENSOR_TYPES: tuple[PicnicSensorEntityDescription, ...] = (
|
||||
device_class=SensorDeviceClass.TIMESTAMP,
|
||||
data_type="next_delivery_data",
|
||||
value_fn=lambda next_delivery: dt_util.parse_datetime(
|
||||
str(next_delivery.get("eta", {}).get("start"))
|
||||
str(next_delivery.eta_start)
|
||||
),
|
||||
),
|
||||
PicnicSensorEntityDescription(
|
||||
@@ -164,7 +188,7 @@ SENSOR_TYPES: tuple[PicnicSensorEntityDescription, ...] = (
|
||||
device_class=SensorDeviceClass.TIMESTAMP,
|
||||
data_type="next_delivery_data",
|
||||
value_fn=lambda next_delivery: dt_util.parse_datetime(
|
||||
str(next_delivery.get("eta", {}).get("end"))
|
||||
str(next_delivery.eta_end)
|
||||
),
|
||||
),
|
||||
PicnicSensorEntityDescription(
|
||||
@@ -173,8 +197,8 @@ SENSOR_TYPES: tuple[PicnicSensorEntityDescription, ...] = (
|
||||
device_class=SensorDeviceClass.TIMESTAMP,
|
||||
data_type="next_delivery_data",
|
||||
value_fn=lambda next_delivery: (
|
||||
dt_util.utc_from_timestamp(next_delivery["estimated_arrival"] / 1000)
|
||||
if next_delivery.get("estimated_arrival")
|
||||
dt_util.utc_from_timestamp(next_delivery.estimated_arrival / 1000)
|
||||
if next_delivery.estimated_arrival
|
||||
else None
|
||||
),
|
||||
),
|
||||
@@ -183,8 +207,10 @@ SENSOR_TYPES: tuple[PicnicSensorEntityDescription, ...] = (
|
||||
translation_key=SENSOR_NEXT_DELIVERY_SLOT_START,
|
||||
device_class=SensorDeviceClass.TIMESTAMP,
|
||||
data_type="next_delivery_data",
|
||||
value_fn=lambda next_delivery: dt_util.parse_datetime(
|
||||
str(next_delivery.get("slot", {}).get("window_start"))
|
||||
value_fn=lambda next_delivery: (
|
||||
dt_util.parse_datetime(str(next_delivery.delivery.slot.window_start))
|
||||
if next_delivery.delivery and next_delivery.delivery.slot
|
||||
else None
|
||||
),
|
||||
),
|
||||
PicnicSensorEntityDescription(
|
||||
@@ -192,8 +218,10 @@ SENSOR_TYPES: tuple[PicnicSensorEntityDescription, ...] = (
|
||||
translation_key=SENSOR_NEXT_DELIVERY_SLOT_END,
|
||||
device_class=SensorDeviceClass.TIMESTAMP,
|
||||
data_type="next_delivery_data",
|
||||
value_fn=lambda next_delivery: dt_util.parse_datetime(
|
||||
str(next_delivery.get("slot", {}).get("window_end"))
|
||||
value_fn=lambda next_delivery: (
|
||||
dt_util.parse_datetime(str(next_delivery.delivery.slot.window_end))
|
||||
if next_delivery.delivery and next_delivery.delivery.slot
|
||||
else None
|
||||
),
|
||||
),
|
||||
)
|
||||
@@ -243,9 +271,11 @@ class PicnicSensor(SensorEntity, CoordinatorEntity[PicnicUpdateCoordinator]):
|
||||
@override
|
||||
def native_value(self) -> StateType | datetime:
|
||||
"""Return the value reported by the sensor."""
|
||||
data_set = (
|
||||
self.coordinator.data.get(self.entity_description.data_type, {})
|
||||
if self.coordinator.data is not None
|
||||
else {}
|
||||
)
|
||||
data = self.coordinator.data or {}
|
||||
data_type = self.entity_description.data_type
|
||||
if data_type in data:
|
||||
data_set = data[data_type]
|
||||
else:
|
||||
factory = _EMPTY_DATA_FACTORIES.get(data_type)
|
||||
data_set = factory() if factory else None
|
||||
return self.entity_description.value_fn(data_set)
|
||||
|
||||
@@ -80,12 +80,12 @@ def product_search(api_client: PicnicAPI, product_name: str | None) -> str | Non
|
||||
|
||||
search_result = api_client.search(product_name)
|
||||
|
||||
if not search_result or "items" not in search_result[0]:
|
||||
if not search_result or not search_result.items:
|
||||
return None
|
||||
|
||||
# Return the first valid result
|
||||
for item in search_result[0]["items"]:
|
||||
if "name" in item:
|
||||
return str(item["id"])
|
||||
for item in search_result.items:
|
||||
if item.name:
|
||||
return str(item.id)
|
||||
|
||||
return None
|
||||
|
||||
@@ -15,7 +15,7 @@ from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
|
||||
from .const import DOMAIN
|
||||
from .const import CART_DATA, DOMAIN
|
||||
from .coordinator import PicnicConfigEntry, PicnicUpdateCoordinator
|
||||
from .services import product_search
|
||||
|
||||
@@ -62,17 +62,18 @@ class PicnicCart(TodoListEntity, CoordinatorEntity[PicnicUpdateCoordinator]):
|
||||
if self.coordinator.data is None:
|
||||
return None
|
||||
|
||||
_LOGGER.debug(self.coordinator.data["cart_data"]["items"])
|
||||
cart = self.coordinator.data[CART_DATA]
|
||||
_LOGGER.debug(cart.items)
|
||||
|
||||
return [
|
||||
TodoItem(
|
||||
summary=f"{article['name']} ({article['unit_quantity']})",
|
||||
uid=f"{item['id']}-{article['id']}",
|
||||
summary=f"{article.name} ({article.unit_quantity})",
|
||||
uid=f"{line.id}-{article.id}",
|
||||
# We set 'NEEDS_ACTION' so they count as state
|
||||
status=TodoItemStatus.NEEDS_ACTION,
|
||||
)
|
||||
for item in self.coordinator.data["cart_data"]["items"]
|
||||
for article in item["items"]
|
||||
for line in cart.items
|
||||
for article in line.items
|
||||
]
|
||||
|
||||
@override
|
||||
|
||||
Generated
+1
-1
@@ -2741,7 +2741,7 @@ python-otbr-api==2.10.0
|
||||
python-overseerr==0.9.0
|
||||
|
||||
# homeassistant.components.picnic
|
||||
python-picnic-api2==1.3.4
|
||||
python-picnic-api2==2.0.1
|
||||
|
||||
# homeassistant.components.pooldose
|
||||
python-pooldose==0.9.6
|
||||
|
||||
@@ -6,6 +6,7 @@ import json
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import pytest
|
||||
from python_picnic_api2.models import Cart, DeliverySummary, Eta, User
|
||||
|
||||
from homeassistant.components.picnic import CONF_COUNTRY_CODE, DOMAIN
|
||||
from homeassistant.const import CONF_ACCESS_TOKEN
|
||||
@@ -19,7 +20,7 @@ ENTITY_ID = "todo.mock_title_shopping_cart"
|
||||
|
||||
SetupDeliveryFixture = Callable[
|
||||
[str, tuple[timedelta, timedelta] | None, tuple[timedelta, timedelta]],
|
||||
Awaitable[dict],
|
||||
Awaitable[DeliverySummary],
|
||||
]
|
||||
|
||||
|
||||
@@ -42,10 +43,14 @@ def mock_picnic_api():
|
||||
with patch("homeassistant.components.picnic.PicnicAPI") as mock:
|
||||
client = mock.return_value
|
||||
client.session.auth_token = "3q29fpwhulzes"
|
||||
client.get_cart.return_value = json.loads(load_fixture("picnic/cart.json"))
|
||||
client.get_user.return_value = json.loads(load_fixture("picnic/user.json"))
|
||||
client.get_cart.return_value = Cart.from_api(
|
||||
json.loads(load_fixture("picnic/cart.json"))
|
||||
)
|
||||
client.get_user.return_value = User.from_api(
|
||||
json.loads(load_fixture("picnic/user.json"))
|
||||
)
|
||||
client.get_deliveries.return_value = [
|
||||
json.loads(load_fixture("picnic/delivery.json"))
|
||||
DeliverySummary.from_api(json.loads(load_fixture("picnic/delivery.json")))
|
||||
]
|
||||
client.get_delivery_position.return_value = {}
|
||||
yield client
|
||||
@@ -63,19 +68,18 @@ def setup_delivery(
|
||||
status: str,
|
||||
eta2: tuple[timedelta, timedelta] | None,
|
||||
slot_window: tuple[timedelta, timedelta],
|
||||
) -> dict:
|
||||
) -> DeliverySummary:
|
||||
delivery = mock_picnic_api.get_deliveries.return_value[0]
|
||||
delivery["status"] = status
|
||||
delivery["delivery_time"] = None
|
||||
delivery.status = status
|
||||
# delivery_time isn't a modelled field; it lives on the raw payload
|
||||
delivery.raw["delivery_time"] = None
|
||||
# eta2 is the API's field name for the route-planning ETA
|
||||
delivery["eta2"] = eta2 and {
|
||||
"start": (dt_util.utcnow() + eta2[0]).isoformat(),
|
||||
"end": (dt_util.utcnow() + eta2[1]).isoformat(),
|
||||
}
|
||||
delivery["slot"]["window_start"] = (
|
||||
dt_util.utcnow() + slot_window[0]
|
||||
).isoformat()
|
||||
delivery["slot"]["window_end"] = (dt_util.utcnow() + slot_window[1]).isoformat()
|
||||
delivery.eta2 = eta2 and Eta(
|
||||
start=(dt_util.utcnow() + eta2[0]).isoformat(),
|
||||
end=(dt_util.utcnow() + eta2[1]).isoformat(),
|
||||
)
|
||||
delivery.slot.window_start = (dt_util.utcnow() + slot_window[0]).isoformat()
|
||||
delivery.slot.window_end = (dt_util.utcnow() + slot_window[1]).isoformat()
|
||||
|
||||
mock_config_entry.add_to_hass(hass)
|
||||
await hass.config_entries.async_setup(mock_config_entry.entry_id)
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
from python_picnic_api2.models import User
|
||||
from python_picnic_api2.session import (
|
||||
Picnic2FAError,
|
||||
Picnic2FARequired,
|
||||
@@ -36,7 +37,7 @@ def picnic_api():
|
||||
) as picnic_mock:
|
||||
instance = picnic_mock.return_value
|
||||
instance.session.auth_token = auth_token
|
||||
instance.get_user.return_value = auth_data
|
||||
instance.get_user.return_value = User.from_api(auth_data)
|
||||
instance.login.return_value = None # no 2FA by default
|
||||
instance.generate_2fa_code.return_value = None
|
||||
instance.verify_2fa_code.return_value = None
|
||||
@@ -389,7 +390,7 @@ async def test_form_already_configured(hass: HomeAssistant, picnic_api) -> None:
|
||||
# user_id as set for the picnic_api mock response.
|
||||
MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
unique_id=picnic_api().get_user()["user_id"],
|
||||
unique_id=picnic_api().get_user().user_id,
|
||||
data={CONF_ACCESS_TOKEN: "a3p98fsen.a39p3fap", CONF_COUNTRY_CODE: "NL"},
|
||||
).add_to_hass(hass)
|
||||
|
||||
@@ -418,7 +419,7 @@ async def test_step_reauth(hass: HomeAssistant, picnic_api) -> None:
|
||||
|
||||
entry = MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
unique_id=picnic_api().get_user()["user_id"],
|
||||
unique_id=picnic_api().get_user().user_id,
|
||||
data=conf,
|
||||
)
|
||||
entry.add_to_hass(hass)
|
||||
|
||||
@@ -5,6 +5,7 @@ from unittest.mock import MagicMock
|
||||
|
||||
from freezegun.api import FrozenDateTimeFactory
|
||||
import pytest
|
||||
from python_picnic_api2.models import Eta
|
||||
|
||||
from homeassistant.components.picnic.const import (
|
||||
DEFAULT_UPDATE_INTERVAL,
|
||||
@@ -112,15 +113,11 @@ async def test_update_interval_with_malformed_eta(
|
||||
) -> None:
|
||||
"""Test that a malformed ETA falls back to the slot window."""
|
||||
delivery = mock_picnic_api.get_deliveries.return_value[0]
|
||||
delivery["status"] = "CURRENT"
|
||||
delivery["delivery_time"] = None
|
||||
delivery["eta2"] = {"start": "malformed", "end": "malformed"}
|
||||
delivery["slot"]["window_start"] = (
|
||||
dt_util.utcnow() + timedelta(minutes=10)
|
||||
).isoformat()
|
||||
delivery["slot"]["window_end"] = (
|
||||
dt_util.utcnow() + timedelta(minutes=70)
|
||||
).isoformat()
|
||||
delivery.status = "CURRENT"
|
||||
delivery.raw["delivery_time"] = None
|
||||
delivery.eta2 = Eta(start="malformed", end="malformed")
|
||||
delivery.slot.window_start = (dt_util.utcnow() + timedelta(minutes=10)).isoformat()
|
||||
delivery.slot.window_end = (dt_util.utcnow() + timedelta(minutes=70)).isoformat()
|
||||
|
||||
mock_config_entry.add_to_hass(hass)
|
||||
await hass.config_entries.async_setup(mock_config_entry.entry_id)
|
||||
@@ -146,7 +143,7 @@ async def test_update_interval_relaxes_after_delivery(
|
||||
coordinator = mock_config_entry.runtime_data
|
||||
assert coordinator.update_interval == DELIVERY_UPDATE_INTERVAL
|
||||
|
||||
delivery["status"] = "COMPLETED"
|
||||
delivery.status = "COMPLETED"
|
||||
freezer.tick(DELIVERY_UPDATE_INTERVAL + timedelta(seconds=30))
|
||||
async_fire_time_changed(hass)
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
|
||||
@@ -6,6 +6,7 @@ import unittest
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
from python_picnic_api2.models import Cart, DeliverySummary, User
|
||||
import requests
|
||||
|
||||
from homeassistant import config_entries
|
||||
@@ -150,14 +151,14 @@ class TestPicnicSensor(unittest.IsolatedAsyncioTestCase):
|
||||
):
|
||||
"""Set up the Picnic sensor platform."""
|
||||
if use_default_responses:
|
||||
self.picnic_mock().get_user.return_value = copy.deepcopy(
|
||||
DEFAULT_USER_RESPONSE
|
||||
self.picnic_mock().get_user.return_value = User.from_api(
|
||||
copy.deepcopy(DEFAULT_USER_RESPONSE)
|
||||
)
|
||||
self.picnic_mock().get_cart.return_value = copy.deepcopy(
|
||||
DEFAULT_CART_RESPONSE
|
||||
self.picnic_mock().get_cart.return_value = Cart.from_api(
|
||||
copy.deepcopy(DEFAULT_CART_RESPONSE)
|
||||
)
|
||||
self.picnic_mock().get_deliveries.return_value = [
|
||||
copy.deepcopy(DEFAULT_DELIVERY_RESPONSE)
|
||||
DeliverySummary.from_api(copy.deepcopy(DEFAULT_DELIVERY_RESPONSE))
|
||||
]
|
||||
self.picnic_mock().get_delivery_position.return_value = {}
|
||||
|
||||
@@ -315,10 +316,12 @@ class TestPicnicSensor(unittest.IsolatedAsyncioTestCase):
|
||||
cart_response["selected_slot"]["state"] = "IMPLICIT"
|
||||
|
||||
# Set mock responses
|
||||
self.picnic_mock().get_user.return_value = copy.deepcopy(DEFAULT_USER_RESPONSE)
|
||||
self.picnic_mock().get_cart.return_value = cart_response
|
||||
self.picnic_mock().get_user.return_value = User.from_api(
|
||||
copy.deepcopy(DEFAULT_USER_RESPONSE)
|
||||
)
|
||||
self.picnic_mock().get_cart.return_value = Cart.from_api(cart_response)
|
||||
self.picnic_mock().get_deliveries.return_value = [
|
||||
copy.deepcopy(DEFAULT_DELIVERY_RESPONSE)
|
||||
DeliverySummary.from_api(copy.deepcopy(DEFAULT_DELIVERY_RESPONSE))
|
||||
]
|
||||
self.picnic_mock().get_delivery_position.return_value = {}
|
||||
await self._setup_platform()
|
||||
@@ -343,9 +346,15 @@ class TestPicnicSensor(unittest.IsolatedAsyncioTestCase):
|
||||
delivery_response["status"] = "CURRENT"
|
||||
|
||||
# Set mock responses
|
||||
self.picnic_mock().get_user.return_value = copy.deepcopy(DEFAULT_USER_RESPONSE)
|
||||
self.picnic_mock().get_cart.return_value = copy.deepcopy(DEFAULT_CART_RESPONSE)
|
||||
self.picnic_mock().get_deliveries.return_value = [delivery_response]
|
||||
self.picnic_mock().get_user.return_value = User.from_api(
|
||||
copy.deepcopy(DEFAULT_USER_RESPONSE)
|
||||
)
|
||||
self.picnic_mock().get_cart.return_value = Cart.from_api(
|
||||
copy.deepcopy(DEFAULT_CART_RESPONSE)
|
||||
)
|
||||
self.picnic_mock().get_deliveries.return_value = [
|
||||
DeliverySummary.from_api(delivery_response)
|
||||
]
|
||||
self.picnic_mock().get_delivery_position.return_value = {}
|
||||
await self._setup_platform()
|
||||
|
||||
@@ -381,7 +390,9 @@ class TestPicnicSensor(unittest.IsolatedAsyncioTestCase):
|
||||
delivery_response = copy.deepcopy(DEFAULT_DELIVERY_RESPONSE)
|
||||
delivery_response["eta2"] = eta_dates
|
||||
delivery_response["status"] = "CURRENT"
|
||||
self.picnic_mock().get_deliveries.return_value = [delivery_response]
|
||||
self.picnic_mock().get_deliveries.return_value = [
|
||||
DeliverySummary.from_api(delivery_response)
|
||||
]
|
||||
await self._coordinator.async_refresh()
|
||||
|
||||
# Assert eta times are not available due to malformed date strings
|
||||
@@ -404,7 +415,9 @@ class TestPicnicSensor(unittest.IsolatedAsyncioTestCase):
|
||||
delivery_response = copy.deepcopy(DEFAULT_DELIVERY_RESPONSE)
|
||||
del delivery_response["delivery_time"]
|
||||
delivery_response["status"] = "CURRENT"
|
||||
self.picnic_mock().get_deliveries.return_value = [delivery_response]
|
||||
self.picnic_mock().get_deliveries.return_value = [
|
||||
DeliverySummary.from_api(delivery_response)
|
||||
]
|
||||
self.picnic_mock().get_delivery_position.return_value = {
|
||||
"eta_window": {
|
||||
"start": "2021-03-05T10:19:20.452+00:00",
|
||||
@@ -543,14 +556,18 @@ class TestPicnicSensor(unittest.IsolatedAsyncioTestCase):
|
||||
undelivered_order_2["eta2"]["end"] = "2022-03-08T13:45:00.000+01:00"
|
||||
|
||||
deliveries_response = [
|
||||
undelivered_order_2,
|
||||
undelivered_order,
|
||||
copy.deepcopy(DEFAULT_DELIVERY_RESPONSE),
|
||||
DeliverySummary.from_api(undelivered_order_2),
|
||||
DeliverySummary.from_api(undelivered_order),
|
||||
DeliverySummary.from_api(copy.deepcopy(DEFAULT_DELIVERY_RESPONSE)),
|
||||
]
|
||||
|
||||
# Set mock responses
|
||||
self.picnic_mock().get_user.return_value = copy.deepcopy(DEFAULT_USER_RESPONSE)
|
||||
self.picnic_mock().get_cart.return_value = copy.deepcopy(DEFAULT_CART_RESPONSE)
|
||||
self.picnic_mock().get_user.return_value = User.from_api(
|
||||
copy.deepcopy(DEFAULT_USER_RESPONSE)
|
||||
)
|
||||
self.picnic_mock().get_cart.return_value = Cart.from_api(
|
||||
copy.deepcopy(DEFAULT_CART_RESPONSE)
|
||||
)
|
||||
self.picnic_mock().get_deliveries.return_value = deliveries_response
|
||||
self.picnic_mock().get_delivery_position.return_value = {}
|
||||
await self._setup_platform()
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import pytest
|
||||
from python_picnic_api2.models import SearchResult, SearchResultItem, User
|
||||
|
||||
from homeassistant.components.picnic import CONF_COUNTRY_CODE, DOMAIN
|
||||
from homeassistant.components.picnic.const import SERVICE_ADD_PRODUCT_TO_CART
|
||||
@@ -29,7 +30,7 @@ def create_picnic_api_client(unique_id):
|
||||
}
|
||||
picnic_mock = MagicMock()
|
||||
picnic_mock.session.auth_token = auth_token
|
||||
picnic_mock.get_user.return_value = auth_data
|
||||
picnic_mock.get_user.return_value = User.from_api(auth_data)
|
||||
|
||||
return picnic_mock
|
||||
|
||||
@@ -98,24 +99,22 @@ async def test_add_product_using_name(
|
||||
"""Test adding a product by name."""
|
||||
|
||||
# Set the return value of the search api endpoint
|
||||
picnic_api_client.search.return_value = [
|
||||
{
|
||||
"items": [
|
||||
{
|
||||
"id": "2525404",
|
||||
"name": "Best tea",
|
||||
"display_price": 321,
|
||||
"unit_quantity": "big bags",
|
||||
},
|
||||
{
|
||||
"id": "2525500",
|
||||
"name": "Cheap tea",
|
||||
"display_price": 100,
|
||||
"unit_quantity": "small bags",
|
||||
},
|
||||
]
|
||||
}
|
||||
]
|
||||
picnic_api_client.search.return_value = SearchResult(
|
||||
items=[
|
||||
SearchResultItem(
|
||||
id="2525404",
|
||||
name="Best tea",
|
||||
display_price=321,
|
||||
unit_quantity="big bags",
|
||||
),
|
||||
SearchResultItem(
|
||||
id="2525500",
|
||||
name="Cheap tea",
|
||||
display_price=100,
|
||||
unit_quantity="small bags",
|
||||
),
|
||||
]
|
||||
)
|
||||
|
||||
await hass.services.async_call(
|
||||
DOMAIN,
|
||||
@@ -137,7 +136,7 @@ async def test_add_product_using_name_no_results(
|
||||
|
||||
# Set the search return value and check that the right exception
|
||||
# is raised during the service call
|
||||
picnic_api_client.search.return_value = []
|
||||
picnic_api_client.search.return_value = SearchResult(items=[])
|
||||
with pytest.raises(PicnicServiceException):
|
||||
await hass.services.async_call(
|
||||
DOMAIN,
|
||||
@@ -159,7 +158,9 @@ async def test_add_product_using_name_no_named_results(
|
||||
|
||||
# Set the search return value and check that the right exception
|
||||
# is raised during the service call
|
||||
picnic_api_client.search.return_value = [{"items": [{"attr": "test"}]}]
|
||||
picnic_api_client.search.return_value = SearchResult(
|
||||
items=[SearchResultItem(id="999")]
|
||||
)
|
||||
with pytest.raises(PicnicServiceException):
|
||||
await hass.services.async_call(
|
||||
DOMAIN,
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
from unittest.mock import MagicMock, Mock
|
||||
|
||||
import pytest
|
||||
from python_picnic_api2.models import Cart, SearchResult, SearchResultItem
|
||||
from syrupy.assertion import SnapshotAssertion
|
||||
|
||||
from homeassistant.components.todo import ATTR_ITEM, DOMAIN as TODO_DOMAIN, TodoServices
|
||||
@@ -33,7 +34,7 @@ async def test_cart_list_empty_items(
|
||||
hass: HomeAssistant, mock_picnic_api: MagicMock, mock_config_entry: MockConfigEntry
|
||||
) -> None:
|
||||
"""Test loading of shopping cart without items."""
|
||||
mock_picnic_api.get_cart.return_value = {"items": []}
|
||||
mock_picnic_api.get_cart.return_value = Cart(items=[])
|
||||
mock_config_entry.add_to_hass(hass)
|
||||
await hass.config_entries.async_setup(mock_config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
@@ -76,17 +77,9 @@ async def test_create_todo_list_item(
|
||||
assert len(mock_picnic_api.get_cart.mock_calls) == 1
|
||||
|
||||
mock_picnic_api.search = Mock()
|
||||
mock_picnic_api.search.return_value = [
|
||||
{
|
||||
"items": [
|
||||
{
|
||||
"id": 321,
|
||||
"name": "Picnic Melk",
|
||||
"unit_quantity": "2 liter",
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
mock_picnic_api.search.return_value = SearchResult(
|
||||
items=[SearchResultItem(id="321", name="Picnic Melk", unit_quantity="2 liter")]
|
||||
)
|
||||
|
||||
mock_picnic_api.add_product = Mock()
|
||||
|
||||
@@ -115,7 +108,7 @@ async def test_create_todo_list_item_not_found(
|
||||
) -> None:
|
||||
"""Test for creating a picnic cart item when ID is not found."""
|
||||
mock_picnic_api.search = Mock()
|
||||
mock_picnic_api.search.return_value = [{"items": []}]
|
||||
mock_picnic_api.search.return_value = SearchResult(items=[])
|
||||
|
||||
with pytest.raises(ServiceValidationError):
|
||||
await hass.services.async_call(
|
||||
|
||||
Reference in New Issue
Block a user