Move the authenticated drift check into the repository as
tools/compat_auth.py, run daily by .github/workflows/compat-auth.yml on the
maintainer's forge only. Credentials come from FH_EMAIL and FH_PASSWORD, the
output is pass/fail labels only because the run log is public, and a failed
run pushes the report to ntfy. Exit 5 means all hold, 10 drift, anything
else that it could not run.
Point compat.yml, tools/compat.py and the docs at it, add a README section,
and cut 0.5.1.
Ship icon, wordmark logo, and a dark-theme logo variant in the
integration's brand/ folder, served locally by Home Assistant 2026.3+
(home-assistant/brands no longer accepts custom integrations). Add the
wordmark to the README and a trademark note to the disclaimer.
async_added_to_hass listed the switchable boxes inline, one fetch per box
popup, so on a slower connection the platform setup ran past
SLOW_SETUP_MAX_WAIT and the whole config entry was cancelled into
setup_error — every entity unavailable despite valid credentials. Move the
listing to a background task so setup never blocks on it; the options fill in
once it returns.
Also bound every portal request to a 30s timeout so one hung request can no
longer drag a refresh, or a first setup, past Home Assistant's own limits and
fail it outright.
Regression covered in tests/test_setup_hygiene.py.
Rebuilds the README entity docs around the distinction that kept causing
confusion — the produce box is chosen, its contents are read-only, and only
add-ons are a list you edit — and re-exports the dashboard view now running,
including the date labels that stop the to-do list and the totals reading as a
contradiction.
The boxes are not in the Algolia catalogue and their category pages look empty
because the grid calls every option 'Georgia Box' — each option's real name is
only in its own select-basket popup, so listing reads them there.
POST /s/submit/select-basket carries a scope the add-on endpoints do not:
popup-toggle is 'do' for the next delivery or 'so' for the standing order. The
select uses 'do', because a mistaken permanent change is the worse one to undo.
Also makes the popup regexes whitespace-tolerant. The site writes both
openPopup("x","y") and openPopup("x", "y"), and the strict form silently
matched nothing on the basket pages.
Restore is POST /s/submit/restore-delivery, wired to switch.turn_off. Its
popup only exists once an order is actually skipped, which is why it could not
be found earlier; verified end to end by skipping Aug 18 and restoring it.
Fixes two bugs found while testing that. async_fetch treated popup and AJAX
fragments as full pages, so the signed-in heuristic read them as logged out and
skip could never run. And the to-do list mixed box produce with add-ons, which
would have invited deletes with no endpoint behind them: only add-ons are
add/removable, so the entity is now scoped to those.
The to-do list exists so Home Assistant's own conversation agent can manage the
order through HassListAddItem rather than through anything bespoke. Skip is a
switch because skipped/not-skipped is state worth reading back; donate is a
button because it cannot be undone. Un-skip raises rather than guessing at an
endpoint that has never been observed.
Fixes subscription parsing, which matched the heading row and so reported zero
on an account that has one.
actions.py covers skip, donate, cart add/remove, subscriptions and vacation
holds. Every mutating endpoint on the site is guarded by rotating per-render
tokens, so each action re-derives them from a live page rather than storing
anything; skip additionally compares the date the server states in its
confirmation against the date it was asked to skip, and refuses on a mismatch.
All actions default to dry_run.
tools/compat.py records what the integration assumes about a site that offers
no API and no stability contract, and CI asserts it daily. Only the
unauthenticated surface is covered: checking the rest would mean putting a
personal account password in public repo secrets.
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.
Add-on rows already carried quantity, unit and extended price; only the name
was exposed. The attribute now renders the full line and a new monetary
sensor reports the add-ons subtotal separately from the box.
The fixture totals were copied from a seven-add-on cart while the fixture
itself had one, so they are now self-consistent and a test asserts that
add-ons plus box price equals the portal's subtotal.
Reads delivery day, next arrival date, both upcoming carts, box contents,
add-ons and order totals from a single /p/dashboard/details fetch. Adds five
sensors and parser tests over a synthetic fixture mirroring the live markup.
Records two portal quirks: every /p/ path returns 200 so signed-in state is
detected by a Sign Out control, and cart-contents-skipped marks the locked
cart rather than a skipped order.
Implements the freshharvest.com two-step login handshake, config flow,
6-hour polling coordinator, and four delivery sensors. Portal page parsing
is not implemented; async_get_next_delivery raises until the account HTML
is mapped against a signed-in session.