Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 4 additions & 19 deletions custom_components/rohlikcz/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,35 +4,20 @@
from __future__ import annotations

from aiohttp import ClientTimeout
from dataclasses import dataclass
from typing import Final

from rohlik_api import SITES, Site


HTTP_TIMEOUT: Final = ClientTimeout(total=10)
DOMAIN = "rohlikcz"

""" Sites """


@dataclass(frozen=True)
class Site:
"""A Rohlík Group shop. All of them run the same backend API."""

base_url: str
name: str
currency: str
timezone: str

# The shops (base URL, currency, timezone) come from rohlik-api's SITES, keyed
# by the same codes stored in config entries: cz, de, at, hu, ro.

CONF_SITE = "site"
DEFAULT_SITE = "cz"
SITES: Final[dict[str, Site]] = {
"cz": Site("https://www.rohlik.cz", "Rohlík.cz", "CZK", "Europe/Prague"),
"de": Site("https://www.knuspr.de", "Knuspr.de", "EUR", "Europe/Berlin"),
"at": Site("https://www.gurkerl.at", "Gurkerl.at", "EUR", "Europe/Vienna"),
"hu": Site("https://www.kifli.hu", "Kifli.hu", "HUF", "Europe/Budapest"),
"ro": Site("https://www.sezamo.ro", "Sezamo.ro", "RON", "Europe/Bucharest"),
}


def get_site(key: str | None) -> Site:
Expand Down
4 changes: 2 additions & 2 deletions custom_components/rohlikcz/hub.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,9 @@
from homeassistant.helpers.storage import Store
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from homeassistant.util import dt as dt_util
from rohlik_api import InvalidCredentialsError, RohlikAPI, RohlikAPIError
from rohlik_api import InvalidCredentialsError, RohlikAPI, RohlikAPIError, Site

from .const import DOMAIN, Site, get_site
from .const import DOMAIN, get_site

#: How often the integration refreshes data from the Rohlik API.
UPDATE_INTERVAL = timedelta(seconds=600)
Expand Down
4 changes: 2 additions & 2 deletions custom_components/rohlikcz/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,6 @@
"documentation": "https://github.com/dvejsada/HA-RohlikCZ",
"iot_class": "cloud_polling",
"issue_tracker": "https://github.com/dvejsada/HA-RohlikCZ/issues",
"requirements": ["rohlik-api==0.2.0"],
"version": "1.0.0-beta3"
"requirements": ["rohlik-api==0.3.0"],
"version": "1.0.0-beta4"
}
7 changes: 6 additions & 1 deletion custom_components/rohlikcz/sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -1007,10 +1007,15 @@ def extra_state_attributes(self) -> Mapping[str, Any] | None:
"""Returns cart details."""
cart = self._rohlik_account.data.get('cart')
if cart:
return {
attrs: dict[str, Any] = {
"Total items": cart.total_items,
"Can Order": cart.can_make_order,
}
# The shop's minimum order value. "Can Order" is not a minimum
# check: it also needs checkout details such as a delivery slot.
if cart.minimum_order_price is not None:
attrs["Minimum Order Price"] = cart.minimum_order_price
return attrs
return None

@property
Expand Down
2 changes: 1 addition & 1 deletion custom_components/rohlikcz/todo.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ def todo_items(self) -> list[TodoItem] | None:
items = []
for product in cart.products:
# Format the summary to include relevant information
summary = f"{product.name} ({product.quantity}) - {product.price} {self._rohlik_hub.currency}"
summary = f"{product.name} ({product.quantity}) - {product.price} {product.currency or self._rohlik_hub.currency}"

# Use cart_item_id as the unique identifier for cart items
items.append(
Expand Down
6 changes: 3 additions & 3 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ Bring your **[Rohlík.cz](https://www.rohlik.cz)** grocery deliveries into Home
> This integration uses a reverse-engineered API from the Rohlík.cz website. It is **not** officially supported by Rohlík.cz. Use it at your own risk.

> [!NOTE]
> The current release is **1.0.0-beta3**, a pre-release of the upcoming 1.0.0. It is feature-complete but still being tested — see [Installing the beta](#installing-the-beta) below, and please [report anything you run into](https://github.com/dvejsada/HA-RohlikCZ/issues).
> The current release is **1.0.0-beta4**, a pre-release of the upcoming 1.0.0. It is feature-complete but still being tested — see [Installing the beta](#installing-the-beta) below, and please [report anything you run into](https://github.com/dvejsada/HA-RohlikCZ/issues).

---

Expand Down Expand Up @@ -46,7 +46,7 @@ HACS hides pre-releases by default, so the 1.0.0 beta will not show up until you
1. Open **Rohlík.cz** in HACS.
2. Click **⋮ → Redownload**.
3. Enable **Show beta versions**.
4. Pick `v1.0.0-beta3` and download it.
4. Pick `v1.0.0-beta4` and download it.
5. Restart Home Assistant.

### Option 2 — Manual Installation
Expand Down Expand Up @@ -117,7 +117,7 @@ Enabling analytics triggers a one-time download of your full order history (this
| **Credit Balance** | Your current account credit (in the shop's currency) |
| **Reusable Bags** | Number of reusable bags on your account |
| **Premium Days Remaining** | Days left in your premium subscription *(premium users only)* |
| **Cart Total** | Current total value of your shopping cart |
| **Cart Total** | Current total value of your shopping cart; the `Minimum Order Price` attribute holds the shop's minimum order value (compare it with the total — `Can Order` also needs checkout details such as a delivery slot) |
| **Last Updated** | Timestamp of the last successful data refresh |
| **Slot Express Time** | Timestamp of the next available express delivery slot |
| **Slot Standard Time** | Timestamp of the nearest standard delivery slot |
Expand Down
2 changes: 1 addition & 1 deletion requirements_test.txt
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
pytest-homeassistant-custom-component
aioresponses
rohlik-api==0.2.0
rohlik-api==0.3.0
12 changes: 11 additions & 1 deletion tests/test_config_flow.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
"""Tests for the Rohlik.cz config and reauth flows."""
from __future__ import annotations

import json
from pathlib import Path
from unittest.mock import AsyncMock, patch

from homeassistant.config_entries import SOURCE_USER
Expand All @@ -9,7 +11,7 @@
from homeassistant.data_entry_flow import FlowResultType
from pytest_homeassistant_custom_component.common import MockConfigEntry

from rohlik_api import InvalidCredentialsError, RohlikAPIError
from rohlik_api import SITES, InvalidCredentialsError, RohlikAPIError

from custom_components.rohlikcz.config_flow import validate_input
from custom_components.rohlikcz.const import CONF_SITE, DOMAIN
Expand Down Expand Up @@ -317,3 +319,11 @@ async def test_reauth_wrong_account(hass: HomeAssistant) -> None:
)
assert result["type"] == FlowResultType.ABORT
assert result["reason"] == "wrong_account"


def test_every_site_has_a_translated_option() -> None:
"""The shop list comes from rohlik-api; each shop needs a selector label."""
translations = Path(__file__).parent.parent / "custom_components" / "rohlikcz" / "translations"
for path in translations.glob("*.json"):
options = json.loads(path.read_text(encoding="utf-8"))["selector"]["site"]["options"]
assert set(options) == set(SITES), path.name
70 changes: 69 additions & 1 deletion tests/test_init.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
from homeassistant.util import dt as dt_util
from pytest_homeassistant_custom_component.common import MockConfigEntry

from rohlik_api import APIRequestFailedError, InvalidCredentialsError
from rohlik_api import APIRequestFailedError, Cart, CartItem, InvalidCredentialsError

from custom_components.rohlikcz.const import CONF_ANALYTICS, CONF_SITE, DOMAIN
from custom_components.rohlikcz.hub import OrderStore, RohlikAccount
Expand Down Expand Up @@ -107,6 +107,74 @@ async def test_setup_without_site_is_rohlik_cz(hass: HomeAssistant) -> None:
assert hass.states.get(entity_id).attributes["unit_of_measurement"] == "CZK"


async def test_cart_minimum_order_price_attribute(hass: HomeAssistant) -> None:
"""The cart sensor exposes the shop's minimum order value when reported."""
data = sample_api_data()
data["cart"] = Cart(
total_price=250.0,
total_items=1,
can_make_order=False,
products=[],
minimum_order_price=299.0,
currency="CZK",
)
entry = _entry()
entry.add_to_hass(hass)

with _patch_get_data(return_value=data):
assert await hass.config_entries.async_setup(entry.entry_id)
await hass.async_block_till_done()

ent_reg = er.async_get(hass)
entity_id = ent_reg.async_get_entity_id("sensor", DOMAIN, "123456_cart_price")
attrs = hass.states.get(entity_id).attributes
assert attrs["Minimum Order Price"] == 299.0
assert attrs["Can Order"] is False


async def test_cart_without_minimum_order_price(hass: HomeAssistant) -> None:
"""No attribute when the API does not report a minimum."""
entry = _entry()
entry.add_to_hass(hass)

with _patch_get_data(return_value=sample_api_data()):
assert await hass.config_entries.async_setup(entry.entry_id)
await hass.async_block_till_done()

ent_reg = er.async_get(hass)
entity_id = ent_reg.async_get_entity_id("sensor", DOMAIN, "123456_cart_price")
assert "Minimum Order Price" not in hass.states.get(entity_id).attributes


async def test_cart_todo_uses_item_currency(hass: HomeAssistant) -> None:
"""Cart todo items show each line's own currency, else the shop's."""
data = sample_api_data()
data["cart"] = Cart(
total_price=3.5,
total_items=2,
can_make_order=False,
products=[
CartItem(id="1", cart_item_id="10", name="Milk", quantity=1, price=1.5, currency="EUR"),
CartItem(id="2", cart_item_id="20", name="Bread", quantity=1, price=2.0),
],
currency="EUR",
)
entry = _entry()
entry.add_to_hass(hass)

with _patch_get_data(return_value=data):
assert await hass.config_entries.async_setup(entry.entry_id)
await hass.async_block_till_done()

ent_reg = er.async_get(hass)
entity_id = ent_reg.async_get_entity_id("todo", DOMAIN, "123456-cart")
response = await hass.services.async_call(
"todo", "get_items", {"entity_id": entity_id}, blocking=True, return_response=True
)
summaries = [item["summary"] for item in response[entity_id]["items"]]
assert summaries == ["Milk (1) - 1.5 EUR", "Bread (1) - 2.0 CZK"]


async def test_czech_language_keeps_kc_unit(hass: HomeAssistant) -> None:
"""Czech installs keep the "Kč" unit their statistics were recorded in."""
hass.config.language = "cs"
Expand Down
Loading