From e7799d3a383bb1719906adf535174e3002c84775 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 18 Jul 2026 14:06:42 +0000 Subject: [PATCH 1/2] Add update_delivery_times action for frequent delivery-ETA polling (#77) The delivery ETA in the announcement shifts often, so the fixed 10-minute refresh can be stale right before the courier arrives. This adds a lightweight rohlikcz.update_delivery_times action that refreshes only the delivery announcements (one request) and notifies entities, so automations can poll it every minute shortly before delivery. Includes an example automation, service description, icon, readme entry and a test. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01LFCKozF619nbadAAgAGmJY --- automations/update_delivery_times.yaml | 23 ++++++++++++ custom_components/rohlikcz/const.py | 1 + custom_components/rohlikcz/hub.py | 16 ++++++++ custom_components/rohlikcz/icons.json | 3 +- custom_components/rohlikcz/manifest.json | 2 +- custom_components/rohlikcz/services.py | 22 ++++++++++- custom_components/rohlikcz/services.yaml | 12 ++++++ readme.md | 6 ++- tests/test_init.py | 48 +++++++++++++++++++++++- 9 files changed, 128 insertions(+), 5 deletions(-) create mode 100644 automations/update_delivery_times.yaml diff --git a/automations/update_delivery_times.yaml b/automations/update_delivery_times.yaml new file mode 100644 index 0000000..15a446b --- /dev/null +++ b/automations/update_delivery_times.yaml @@ -0,0 +1,23 @@ +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). + - 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 diff --git a/custom_components/rohlikcz/const.py b/custom_components/rohlikcz/const.py index 09ed0da..7c2d956 100644 --- a/custom_components/rohlikcz/const.py +++ b/custom_components/rohlikcz/const.py @@ -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" diff --git a/custom_components/rohlikcz/hub.py b/custom_components/rohlikcz/hub.py index b08e94d..e77e285 100644 --- a/custom_components/rohlikcz/hub.py +++ b/custom_components/rohlikcz/hub.py @@ -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: diff --git a/custom_components/rohlikcz/icons.json b/custom_components/rohlikcz/icons.json index a2ac3d8..9771f10 100644 --- a/custom_components/rohlikcz/icons.json +++ b/custom_components/rohlikcz/icons.json @@ -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"} } } \ No newline at end of file diff --git a/custom_components/rohlikcz/manifest.json b/custom_components/rohlikcz/manifest.json index 329dfa1..79221bc 100644 --- a/custom_components/rohlikcz/manifest.json +++ b/custom_components/rohlikcz/manifest.json @@ -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" } diff --git a/custom_components/rohlikcz/services.py b/custom_components/rohlikcz/services.py index a6f322d..8f5be51 100644 --- a/custom_components/rohlikcz/services.py +++ b/custom_components/rohlikcz/services.py @@ -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__) @@ -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] @@ -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, diff --git a/custom_components/rohlikcz/services.yaml b/custom_components/rohlikcz/services.yaml index 8cd77ab..66c4fa8 100644 --- a/custom_components/rohlikcz/services.yaml +++ b/custom_components/rohlikcz/services.yaml @@ -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 diff --git a/readme.md b/readme.md index 833fd0b..edfdd3f 100644 --- a/readme.md +++ b/readme.md @@ -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`**. --- diff --git a/tests/test_init.py b/tests/test_init.py index c0a0a7c..8cb303c 100644 --- a/tests/test_init.py +++ b/tests/test_init.py @@ -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 @@ -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 26.4.' + ' v 08:00' + ), + } + ] + } + } + 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() From c7ec5129a02e10bf7e38904706573199c992d479 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 18 Jul 2026 16:08:49 +0000 Subject: [PATCH 2/2] Address review: newline at EOF in icons.json, clarify example entity ID Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01LFCKozF619nbadAAgAGmJY --- automations/update_delivery_times.yaml | 2 ++ custom_components/rohlikcz/icons.json | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/automations/update_delivery_times.yaml b/automations/update_delivery_times.yaml index 15a446b..6d19edd 100644 --- a/automations/update_delivery_times.yaml +++ b/automations/update_delivery_times.yaml @@ -12,6 +12,8 @@ triggers: 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') %} diff --git a/custom_components/rohlikcz/icons.json b/custom_components/rohlikcz/icons.json index 9771f10..82df06f 100644 --- a/custom_components/rohlikcz/icons.json +++ b/custom_components/rohlikcz/icons.json @@ -7,4 +7,4 @@ "search_and_add_to_cart": {"service": "mdi:cart-arrow-right"}, "update_delivery_times": {"service": "mdi:truck-fast-outline"} } -} \ No newline at end of file +}