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
25 changes: 25 additions & 0 deletions automations/update_delivery_times.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
alias: Track Rohlik delivery ETA closely before delivery
description: >-
The delivery ETA of an upcoming order shifts a lot, so the regular 10-minute
refresh can be off right before the courier arrives. This automation polls
the lightweight update_delivery_times action every minute during the last
30 minutes before the expected delivery, keeping the delivery time sensor
close to the real ETA.
mode: single
triggers:
- trigger: time_pattern
minutes: "/1"
conditions:
# Only poll when a delivery is expected within the next 30 minutes (also
# keeps polling up to 10 minutes past the ETA in case the courier is late).
# Replace sensor.rohlik_cz_delivery_time with your own Delivery Time sensor
# if it is named differently (e.g. with multiple accounts).
- condition: template
value_template: >-
{% set eta = states('sensor.rohlik_cz_delivery_time') %}
{{ eta not in ['unknown', 'unavailable']
and -600 <= (as_datetime(eta) - now()).total_seconds() <= 1800 }}
actions:
- action: rohlikcz.update_delivery_times
data:
config_entry_id: XXXXXXXXXXXXXXXX # Replace with your Rohlik.cz config entry ID
1 change: 1 addition & 0 deletions custom_components/rohlikcz/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@
SERVICE_FETCH_ORDER_HISTORY = "fetch_order_history"
SERVICE_ENRICH_ORDERS = "enrich_orders"
SERVICE_REFRESH_SLOTS = "refresh_slots"
SERVICE_UPDATE_DELIVERY_TIMES = "update_delivery_times"

""" Analytics options """
CONF_ANALYTICS = "analytics"
Expand Down
16 changes: 16 additions & 0 deletions custom_components/rohlikcz/hub.py
Original file line number Diff line number Diff line change
Expand Up @@ -751,6 +751,22 @@ async def refresh_slots(self) -> None:
self.data["next_delivery_slot"] = result
self.async_update_listeners()

async def refresh_delivery_times(self) -> None:
"""Cheaply refresh only the delivery announcements (delivery-time ETA).

The announcement carries the shifting delivery ETA for an upcoming
order, so this is light enough to poll every minute shortly before a
delivery. Updates self.data["delivery_announcements"] in place and
notifies entities, without disturbing the rest of the data or the
regular refresh cycle.
"""
if not self.data:
return
result = await self._client.delivery.get_announcements()
if result is not None:
self.data["delivery_announcements"] = result
self.async_update_listeners()

async def async_close(self) -> None:
"""Release resources held by the API client (called on unload)."""
try:
Expand Down
5 changes: 3 additions & 2 deletions custom_components/rohlikcz/icons.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
"search_product": {"service": "mdi:magnify"},
"get_shopping_list": {"service": "mdi:clipboard-list"},
"get_cart_content": {"service": "mdi:cart"},
"search_and_add_to_cart": {"service": "mdi:cart-arrow-right"}
"search_and_add_to_cart": {"service": "mdi:cart-arrow-right"},
"update_delivery_times": {"service": "mdi:truck-fast-outline"}
}
}
}
2 changes: 1 addition & 1 deletion custom_components/rohlikcz/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,5 @@
"iot_class": "cloud_polling",
"issue_tracker": "https://github.com/dvejsada/HA-RohlikCZ/issues",
"requirements": ["rohlik-api==0.2.0"],
"version": "0.6.0"
"version": "0.6.1"
}
22 changes: 21 additions & 1 deletion custom_components/rohlikcz/services.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
from .const import DOMAIN, ATTR_CONFIG_ENTRY_ID, ATTR_PRODUCT_ID, ATTR_QUANTITY, ATTR_PRODUCT_NAME, \
ATTR_SHOPPING_LIST_ID, ATTR_LIMIT, ATTR_FAVOURITE_ONLY, SERVICE_ADD_TO_CART, SERVICE_SEARCH_PRODUCT, SERVICE_GET_SHOPPING_LIST, \
SERVICE_GET_CART_CONTENT, SERVICE_SEARCH_AND_ADD_PRODUCT, SERVICE_UPDATE_DATA, SERVICE_FETCH_ORDER_HISTORY, SERVICE_ENRICH_ORDERS, \
SERVICE_REFRESH_SLOTS
SERVICE_REFRESH_SLOTS, SERVICE_UPDATE_DELIVERY_TIMES

_LOGGER = logging.getLogger(__name__)

Expand Down Expand Up @@ -137,6 +137,16 @@ async def async_refresh_slots(call: ServiceCall) -> None:
except Exception as err:
raise HomeAssistantError(f"Failed to refresh slots: {err}")

async def async_update_delivery_times(call: ServiceCall) -> None:
"""Cheaply refresh only the delivery-time announcement (ETA polling)."""
config_entry_id = call.data[ATTR_CONFIG_ENTRY_ID]

account = _get_account(hass, config_entry_id)
try:
await account.refresh_delivery_times()
except Exception as err:
raise HomeAssistantError(f"Failed to update delivery times: {err}")

async def async_fetch_order_history(call: ServiceCall) -> None:
"""Fetch complete order history from Rohlik."""
config_entry_id = call.data[ATTR_CONFIG_ENTRY_ID]
Expand Down Expand Up @@ -241,6 +251,16 @@ async def async_enrich_orders(call: ServiceCall) -> None:
supports_response=SupportsResponse.NONE
)

hass.services.async_register(
DOMAIN,
SERVICE_UPDATE_DELIVERY_TIMES,
async_update_delivery_times,
schema=vol.Schema({
vol.Required(ATTR_CONFIG_ENTRY_ID): cv.string
}),
supports_response=SupportsResponse.NONE
)

hass.services.async_register(
DOMAIN,
SERVICE_FETCH_ORDER_HISTORY,
Expand Down
12 changes: 12 additions & 0 deletions custom_components/rohlikcz/services.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,18 @@ refresh_slots:
config_entry:
integration: rohlikcz

update_delivery_times:
name: Update delivery times
description: Cheaply refresh only the delivery-time announcement (one request). The delivery ETA of an upcoming order shifts often, so this is light enough to poll every minute shortly before a delivery to keep the delivery time sensor accurate.
fields:
config_entry_id:
name: Account
description: The Rohlik account to use
required: true
selector:
config_entry:
integration: rohlikcz

search_and_add_to_cart:
name: Search and add to cart
description: Search for a product and add to a shopping cart
Expand Down
6 changes: 5 additions & 1 deletion readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -152,19 +152,23 @@ Each sensor's attributes contain the top N items (configurable, default 10) sort
| **`rohlikcz.get_cart_content`** | Get the current contents of your shopping cart |
| **`rohlikcz.update_data`** | Force an immediate full data refresh from Rohlík.cz |
| **`rohlikcz.refresh_slots`** | Cheaply refresh only the delivery-slot data with a single request — light enough to poll every few seconds to catch express availability |
| **`rohlikcz.update_delivery_times`** | Cheaply refresh only the delivery-time announcement with a single request — light enough to poll every minute to track the shifting delivery ETA |
| **`rohlikcz.fetch_order_history`** | Download your complete order history and store it locally (backfill) |
| **`rohlikcz.enrich_orders`** | Enrich stored orders with item details and product categories to populate the spending sensors |

> [!TIP]
> Want a notification the moment express delivery opens up? See [`automations/refresh_slots.yaml`](automations/refresh_slots.yaml) for an example that polls `refresh_slots` every 15 seconds while armed and notifies you when the **Express Available** sensor turns on.

> [!TIP]
> Delivery ETA shifting around before the courier arrives? See [`automations/update_delivery_times.yaml`](automations/update_delivery_times.yaml) for an example that polls `update_delivery_times` every minute during the last 30 minutes before delivery so the **Delivery Time** sensor stays accurate.

---

## 🔄 Data Updates

Data is refreshed from Rohlík.cz **every 10 minutes** automatically. The update covers account details, premium status, delivery slots, shopping cart, and order history.

You can trigger an immediate refresh at any time using the **`rohlikcz.update_data`** action, or refresh just the delivery slots more frequently with **`rohlikcz.refresh_slots`**.
You can trigger an immediate refresh at any time using the **`rohlikcz.update_data`** action, refresh just the delivery slots more frequently with **`rohlikcz.refresh_slots`**, or refresh just the delivery-time announcement with **`rohlikcz.update_delivery_times`**.

---

Expand Down
48 changes: 47 additions & 1 deletion tests/test_init.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,9 @@

import json
import os
from datetime import timedelta
from datetime import datetime, timedelta
from unittest.mock import AsyncMock, patch
from zoneinfo import ZoneInfo

from homeassistant.config_entries import ConfigEntryState
from homeassistant.const import CONF_EMAIL, CONF_PASSWORD
Expand Down Expand Up @@ -209,6 +210,51 @@ async def test_refresh_slots_updates_express_sensor(hass: HomeAssistant) -> None
assert account.data["login"]["data"]["user"]["id"] == 123456


async def test_update_delivery_times_updates_delivery_time_sensor(hass: HomeAssistant) -> None:
"""refresh_delivery_times merges fresh announcements and the delivery time sensor reflects it."""
data = sample_api_data()

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)
delivery_time_id = ent_reg.async_get_entity_id("sensor", DOMAIN, "123456_delivery_time")
assert hass.states.get(delivery_time_id).state == "unknown"

account = entry.runtime_data
fresh_announcements = {
"data": {
"announcements": [
{
"id": 9002,
"title": "Delivery",
"updatedAt": "2026-04-26T07:30:00+02:00",
"content": (
'Doručíme <span style="color:#009B37">26.4.</span>'
' v <span style="color:#009B37">08:00</span>'
),
}
]
}
}
account._client.delivery.get_announcements = AsyncMock(return_value=fresh_announcements)

await account.refresh_delivery_times()
await hass.async_block_till_done()

# Only the announcement data changed; the delivery time sensor picks up the ETA.
assert account.data["delivery_announcements"] == fresh_announcements
state = hass.states.get(delivery_time_id).state
assert dt_util.parse_datetime(state) == datetime(
datetime.now().year, 4, 26, 8, 0, tzinfo=ZoneInfo("Europe/Prague")
)
# Other data is untouched.
assert account.data["login"]["data"]["user"]["id"] == 123456


async def test_slot_sensors_registered(hass: HomeAssistant) -> None:
"""The three preselected-slot sensors register and parse their data."""
data = sample_api_data()
Expand Down
Loading