chore: update python-picnic-api2 to v2.0.1 (#178395)

This commit is contained in:
Noah Groß
2026-08-07 13:51:21 +02:00
committed by GitHub
parent c06bab0cc1
commit ab3451bd07
13 changed files with 243 additions and 181 deletions
@@ -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
+76 -58
View File
@@ -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"]
}
+60 -30
View File
@@ -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)
+4 -4
View File
@@ -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
+7 -6
View File
@@ -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
+1 -1
View File
@@ -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
+19 -15
View File
@@ -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)
+4 -3
View File
@@ -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)
+7 -10
View File
@@ -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)
+35 -18
View File
@@ -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()
+22 -21
View File
@@ -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,
+6 -13
View File
@@ -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(