Expose every order component as its own entity and prepare for release
Validate / hassfest (push) Failing after 23s
Validate / pytest (push) Successful in 9s
Validate / HACS (push) Failing after 1m21s

Each cost line is now an entity rather than an attribute: subtotal, box price,
add-ons, tax and delivery fee, plus a total and free-delivery-remaining for the
open order, a delivery-day sensor, and a binary sensor that turns off at the
cutoff. Entities share a base class and declare a scope, so a description
states only the field it reads.

Also parses the driver tip, Bounty savings and the free-delivery threshold. The
threshold is carried across orders because the progress bar only renders on
carts that have not met it.

Adds LICENSE, hacs.json, a CI workflow, a pre-publish audit script, and a test
that cross-checks every entity's translation_key against both translation
files. Manifest URLs now point at GitHub rather than a private forge.
This commit is contained in:
flan
2026-08-03 19:32:50 +00:00
parent 527f662189
commit 3e880ff2c1
18 changed files with 760 additions and 144 deletions
+29
View File
@@ -96,7 +96,12 @@ class DeliveryOrder:
subtotal: float | None = None
tax: float | None = None
delivery_fee: float | None = None
driver_tip: float | None = None
total: float | None = None
# What a Bounty membership would knock off this order. Marketing, not a charge.
bounty_savings: float | None = None
# How much more this order needs to qualify for free delivery, 0.0 once it does.
free_delivery_remaining: float | None = None
# Non-empty only while the order can still be changed, e.g. "Shop tomorrow"
# or "Shop thru Sunday 8/9". Empty once the order is locked for packing.
shop_window: str | None = None
@@ -126,6 +131,9 @@ class AccountSnapshot:
next_delivery: date | None = None
delivery_day: str | None = None
# Account-wide spend needed for free delivery. Only rendered on carts that
# have not reached it, so it is read once and applied to every order.
free_delivery_threshold: float | None = None
orders: list[DeliveryOrder] = field(default_factory=list)
@property
@@ -262,9 +270,30 @@ def parse_dashboard(html: str) -> AccountSnapshot:
order.tax = amount
elif key.startswith("delivery"):
order.delivery_fee = amount
elif key.startswith("driver tip"):
# Reads "Add Tip" until one is set, which is not an amount.
order.driver_tip = amount
elif "bounty savings" in key:
order.bounty_savings = amount
progress = soup.select_one(f"#DeliveryProgressBar-{delivery_id} progress")
if progress is not None and snapshot.free_delivery_threshold is None:
try:
snapshot.free_delivery_threshold = float(progress.get("max"))
except (TypeError, ValueError):
_LOGGER.debug("unparsable free-delivery threshold on %s", delivery_id)
snapshot.orders.append(order)
# Applied after the loop: the threshold is only rendered on carts that have
# not met it, so an order that already qualifies would otherwise miss it.
if snapshot.free_delivery_threshold is not None:
for order in snapshot.orders:
if order.subtotal is not None:
order.free_delivery_remaining = round(
max(0.0, snapshot.free_delivery_threshold - order.subtotal), 2
)
if not snapshot.orders and snapshot.next_delivery is None:
raise FreshHarvestError("dashboard markup not recognised")
return snapshot