Add Picnic shopping cart as Todo list (#102855)

Co-authored-by: Robert Resch <robert@resch.dev>
Co-authored-by: Allen Porter <allen.porter@gmail.com>
This commit is contained in:
Duco Sebel 2023-11-22 08:40:19 +01:00 committed by GitHub
parent 3929b0163c
commit af15aab35e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 569 additions and 1 deletions

View file

@ -10,7 +10,7 @@ from .const import CONF_API, CONF_COORDINATOR, CONF_COUNTRY_CODE, DOMAIN
from .coordinator import PicnicUpdateCoordinator from .coordinator import PicnicUpdateCoordinator
from .services import async_register_services from .services import async_register_services
PLATFORMS = [Platform.SENSOR] PLATFORMS = [Platform.SENSOR, Platform.TODO]
def create_picnic_client(entry: ConfigEntry): def create_picnic_client(entry: ConfigEntry):

View file

@ -21,6 +21,11 @@
} }
}, },
"entity": { "entity": {
"todo": {
"shopping_cart": {
"name": "Shopping cart"
}
},
"sensor": { "sensor": {
"cart_items_count": { "cart_items_count": {
"name": "Cart items count" "name": "Cart items count"

View file

@ -0,0 +1,75 @@
"""Definition of Picnic shopping cart."""
from __future__ import annotations
import logging
from typing import Any, cast
from homeassistant.components.todo import TodoItem, TodoItemStatus, TodoListEntity
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.update_coordinator import (
CoordinatorEntity,
DataUpdateCoordinator,
)
from .const import CONF_COORDINATOR, DOMAIN
_LOGGER = logging.getLogger(__name__)
async def async_setup_entry(
hass: HomeAssistant,
config_entry: ConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up the Picnic shopping cart todo platform config entry."""
picnic_coordinator = hass.data[DOMAIN][config_entry.entry_id][CONF_COORDINATOR]
# Add an entity shopping card
async_add_entities([PicnicCart(picnic_coordinator, config_entry)])
class PicnicCart(TodoListEntity, CoordinatorEntity):
"""A Picnic Shopping Cart TodoListEntity."""
_attr_has_entity_name = True
_attr_translation_key = "shopping_cart"
_attr_icon = "mdi:cart"
def __init__(
self,
coordinator: DataUpdateCoordinator[Any],
config_entry: ConfigEntry,
) -> None:
"""Initialize PicnicCart."""
super().__init__(coordinator)
self._attr_device_info = DeviceInfo(
entry_type=DeviceEntryType.SERVICE,
identifiers={(DOMAIN, cast(str, config_entry.unique_id))},
manufacturer="Picnic",
model=config_entry.unique_id,
)
self._attr_unique_id = f"{config_entry.unique_id}-cart"
@property
def todo_items(self) -> list[TodoItem] | None:
"""Get the current set of items in cart items."""
if self.coordinator.data is None:
return None
_LOGGER.debug(self.coordinator.data["cart_data"]["items"])
items = []
for item in self.coordinator.data["cart_data"]["items"]:
for article in item["items"]:
items.append(
TodoItem(
summary=f"{article['name']} ({article['unit_quantity']})",
uid=f"{item['id']}-{article['id']}",
status=TodoItemStatus.NEEDS_ACTION, # We set 'NEEDS_ACTION' so they count as state
)
)
return items

View file

@ -0,0 +1,52 @@
"""Conftest for Picnic tests."""
import json
from unittest.mock import MagicMock, patch
import pytest
from homeassistant.components.picnic import CONF_COUNTRY_CODE, DOMAIN
from homeassistant.const import CONF_ACCESS_TOKEN
from homeassistant.core import HomeAssistant
from tests.common import MockConfigEntry, load_fixture
@pytest.fixture
def mock_config_entry() -> MockConfigEntry:
"""Return the default mocked config entry."""
return MockConfigEntry(
domain=DOMAIN,
data={
CONF_ACCESS_TOKEN: "x-original-picnic-auth-token",
CONF_COUNTRY_CODE: "NL",
},
unique_id="295-6y3-1nf4",
)
@pytest.fixture
def mock_picnic_api():
"""Return a mocked PicnicAPI client."""
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_deliveries.return_value = json.loads(
load_fixture("picnic/delivery.json")
)
client.get_delivery_position.return_value = {}
yield client
@pytest.fixture
async def init_integration(
hass: HomeAssistant, mock_config_entry: MockConfigEntry, mock_picnic_api: MagicMock
) -> MockConfigEntry:
"""Set up the Picnic integration for testing."""
mock_config_entry.add_to_hass(hass)
await hass.config_entries.async_setup(mock_config_entry.entry_id)
await hass.async_block_till_done()
return mock_config_entry

View file

@ -0,0 +1,337 @@
{
"items": [
{
"type": "ORDER_LINE",
"id": "763",
"items": [
{
"type": "ORDER_ARTICLE",
"id": "s1001194",
"name": "Knoflook",
"image_ids": [
"4054013cb82da80abbdcd7c8eec54f486bfa180b9cf499e94cc4013470d0dfd7"
],
"unit_quantity": "2 stuks",
"unit_quantity_sub": "€9.08/kg",
"price": 109,
"max_count": 50,
"perishable": false,
"tags": [],
"decorators": [
{
"type": "QUANTITY",
"quantity": 1
},
{
"type": "UNIT_QUANTITY",
"unit_quantity_text": "2 stuks"
}
]
}
],
"display_price": 109,
"price": 109
},
{
"type": "ORDER_LINE",
"id": "765_766",
"items": [
{
"type": "ORDER_ARTICLE",
"id": "s1046297",
"name": "Picnic magere melk",
"image_ids": [
"c2a96757634ada380726d3307e564f244cfa86e89d94c2c0e382306dbad599a3"
],
"unit_quantity": "2 x 1 liter",
"unit_quantity_sub": "€1.02/l",
"price": 204,
"max_count": 18,
"perishable": true,
"tags": [],
"decorators": [
{
"type": "QUANTITY",
"quantity": 2
},
{
"type": "UNIT_QUANTITY",
"unit_quantity_text": "2 x 1 liter"
}
]
}
],
"display_price": 408,
"price": 408
},
{
"type": "ORDER_LINE",
"id": "767",
"items": [
{
"type": "ORDER_ARTICLE",
"id": "s1010532",
"name": "Picnic magere melk",
"image_ids": [
"aa8880361f045ffcfb9f787e9b7fc2b49907be46921bf42985506dc03baa6c2c"
],
"unit_quantity": "1 liter",
"unit_quantity_sub": "€1.05/l",
"price": 105,
"max_count": 18,
"perishable": true,
"tags": [],
"decorators": [
{
"type": "QUANTITY",
"quantity": 1
},
{
"type": "UNIT_QUANTITY",
"unit_quantity_text": "1 liter"
}
]
}
],
"display_price": 105,
"price": 105
},
{
"type": "ORDER_LINE",
"id": "774_775",
"items": [
{
"type": "ORDER_ARTICLE",
"id": "s1018253",
"name": "Robijn wascapsules wit",
"image_ids": [
"c78b809ccbcd65760f8ce897e083587ee7b3f2b9719affd80983fad722b5c2d9"
],
"unit_quantity": "40 wasbeurten",
"price": 2899,
"max_count": 50,
"perishable": false,
"tags": [],
"decorators": [
{
"type": "QUANTITY",
"quantity": 1
},
{
"type": "IMMUTABLE"
},
{
"type": "UNIT_QUANTITY",
"unit_quantity_text": "40 wasbeurten"
}
]
},
{
"type": "ORDER_ARTICLE",
"id": "s1007025",
"name": "Robijn wascapsules kleur",
"image_ids": [
"ef9c8a371a639906ef20dfdcdc99296fce4102c47f0018e6329a2e4ae9f846b7"
],
"unit_quantity": "15 wasbeurten",
"price": 879,
"max_count": 50,
"perishable": false,
"tags": [],
"decorators": [
{
"type": "QUANTITY",
"quantity": 1
},
{
"type": "IMMUTABLE"
},
{
"type": "UNIT_QUANTITY",
"unit_quantity_text": "15 wasbeurten"
}
]
}
],
"display_price": 3778,
"price": 3778,
"decorators": [
{
"type": "PROMO",
"text": "1+1 gratis"
},
{
"type": "PRICE",
"display_price": 1889
}
]
},
{
"type": "ORDER_LINE",
"id": "776_777_778_779_780",
"items": [
{
"type": "ORDER_ARTICLE",
"id": "s1012699",
"name": "Chinese wokgroenten",
"image_ids": [
"b0b547a03d1d6021565618a5d32bd35df34c57b348d73252defb776ab8f8ab76"
],
"unit_quantity": "600 gram",
"unit_quantity_sub": "€4.92/kg",
"price": 295,
"max_count": 50,
"perishable": true,
"tags": [],
"decorators": [
{
"type": "QUANTITY",
"quantity": 1
},
{
"type": "IMMUTABLE"
},
{
"type": "UNIT_QUANTITY",
"unit_quantity_text": "600 gram"
}
]
},
{
"type": "ORDER_ARTICLE",
"id": "s1003425",
"name": "Picnic boerderij-eitjes",
"image_ids": [
"8be72b8144bfb7ff637d4703cfcb11e1bee789de79c069d00e879650dbf19840"
],
"unit_quantity": "6 stuks M/L",
"price": 305,
"max_count": 50,
"perishable": true,
"tags": [],
"decorators": [
{
"type": "QUANTITY",
"quantity": 1
},
{
"type": "IMMUTABLE"
},
{
"type": "UNIT_QUANTITY",
"unit_quantity_text": "6 stuks M/L"
}
]
},
{
"type": "ORDER_ARTICLE",
"id": "s1016692",
"name": "Picnic witte snelkookrijst",
"image_ids": [
"9c76c0a0143bfef650ab85fff4f0918e0b4e2927d79caa2a2bf394f292a86213"
],
"unit_quantity": "400 gram",
"unit_quantity_sub": "€3.23/kg",
"price": 129,
"max_count": 99,
"perishable": false,
"tags": [],
"decorators": [
{
"type": "QUANTITY",
"quantity": 1
},
{
"type": "IMMUTABLE"
},
{
"type": "UNIT_QUANTITY",
"unit_quantity_text": "400 gram"
}
]
},
{
"type": "ORDER_ARTICLE",
"id": "s1012503",
"name": "Conimex kruidenmix nasi",
"image_ids": [
"2eb78de465aa327a9739d9b204affce17fdf6bf7675c4fe9fa2d4ec102791c69"
],
"unit_quantity": "20 gram",
"unit_quantity_sub": "€42.50/kg",
"price": 85,
"max_count": 50,
"perishable": false,
"tags": [],
"decorators": [
{
"type": "QUANTITY",
"quantity": 1
},
{
"type": "IMMUTABLE"
},
{
"type": "UNIT_QUANTITY",
"unit_quantity_text": "20 gram"
}
]
},
{
"type": "ORDER_ARTICLE",
"id": "s1005028",
"name": "Conimex satésaus mild kant & klaar",
"image_ids": [
"0273de24577ba25526cdf31c53ef2017c62611b2bb4d82475abb2dcd9b2f5b83"
],
"unit_quantity": "400 gram",
"unit_quantity_sub": "€5.98/kg",
"price": 239,
"max_count": 50,
"perishable": false,
"tags": [],
"decorators": [
{
"type": "QUANTITY",
"quantity": 1
},
{
"type": "IMMUTABLE"
},
{
"type": "UNIT_QUANTITY",
"unit_quantity_text": "400 gram"
}
]
}
],
"display_price": 1053,
"price": 1053,
"decorators": [
{
"type": "PROMO",
"text": "Receptkorting"
},
{
"type": "PRICE",
"display_price": 880
}
]
}
],
"delivery_slots": [
{
"slot_id": "611a3b074872b23576bef456a",
"window_start": "2021-03-03T14:45:00.000+01:00",
"window_end": "2021-03-03T15:45:00.000+01:00",
"cut_off_time": "2021-03-02T22:00:00.000+01:00",
"minimum_order_value": 3500
}
],
"selected_slot": {
"slot_id": "611a3b074872b23576bef456a",
"state": "EXPLICIT"
},
"total_count": 10,
"total_price": 2535
}

View file

@ -0,0 +1,31 @@
{
"delivery_id": "z28fjso23e",
"creation_time": "2021-02-24T21:48:46.395+01:00",
"slot": {
"slot_id": "602473859a40dc24c6b65879",
"hub_id": "AMS",
"window_start": "2021-02-26T20:15:00.000+01:00",
"window_end": "2021-02-26T21:15:00.000+01:00",
"cut_off_time": "2021-02-25T22:00:00.000+01:00",
"minimum_order_value": 3500
},
"eta2": {
"start": "2021-02-26T20:54:00.000+01:00",
"end": "2021-02-26T21:14:00.000+01:00"
},
"status": "COMPLETED",
"delivery_time": {
"start": "2021-02-26T20:54:05.221+01:00",
"end": "2021-02-26T20:58:31.802+01:00"
},
"orders": [
{
"creation_time": "2021-02-24T21:48:46.418+01:00",
"total_price": 3597
},
{
"creation_time": "2021-02-25T17:10:26.816+01:00",
"total_price": 536
}
]
}

View file

@ -0,0 +1,14 @@
{
"user_id": "295-6y3-1nf4",
"firstname": "User",
"lastname": "Name",
"address": {
"house_number": 123,
"house_number_ext": "a",
"postcode": "4321 AB",
"street": "Commonstreet",
"city": "Somewhere"
},
"total_deliveries": 123,
"completed_deliveries": 112
}

View file

@ -0,0 +1,54 @@
"""Tests for Picnic Tasks todo platform."""
from unittest.mock import MagicMock
from homeassistant.core import HomeAssistant
from tests.common import MockConfigEntry
async def test_cart_list_with_items(hass: HomeAssistant, init_integration) -> None:
"""Test loading of shopping cart."""
state = hass.states.get("todo.mock_title_shopping_cart")
assert state
assert state.state == "10"
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_config_entry.add_to_hass(hass)
await hass.config_entries.async_setup(mock_config_entry.entry_id)
await hass.async_block_till_done()
state = hass.states.get("todo.mock_title_shopping_cart")
assert state
assert state.state == "0"
async def test_cart_list_unexpected_response(
hass: HomeAssistant, mock_picnic_api: MagicMock, mock_config_entry: MockConfigEntry
) -> None:
"""Test loading of shopping cart without expected response."""
mock_picnic_api.get_cart.return_value = {}
mock_config_entry.add_to_hass(hass)
await hass.config_entries.async_setup(mock_config_entry.entry_id)
await hass.async_block_till_done()
state = hass.states.get("todo.mock_title_shopping_cart")
assert state is None
async def test_cart_list_null_response(
hass: HomeAssistant, mock_picnic_api: MagicMock, mock_config_entry: MockConfigEntry
) -> None:
"""Test loading of shopping cart without response."""
mock_picnic_api.get_cart.return_value = None
mock_config_entry.add_to_hass(hass)
await hass.config_entries.async_setup(mock_config_entry.entry_id)
await hass.async_block_till_done()
state = hass.states.get("todo.mock_title_shopping_cart")
assert state is None