24 Commits
Author SHA1 Message Date
flan 22e6499a8e Check the signed-in markup from a scheduled workflow
Validate / hassfest (push) Skipped
Validate / HACS (push) Skipped
Upstream compatibility / compat (push) Successful in 8s
Validate / pytest (push) Successful in 11s
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.
2026-09-21 19:01:13 +00:00
flan bf01fd6eb0 Cut 0.5.0: bundle the Fresh Harvest brand images
Validate / hassfest (push) Skipped
Validate / HACS (push) Skipped
Validate / pytest (push) Successful in 12s
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.
2026-08-19 23:44:52 +00:00
flan b4e7ef4b70 Merge fix/select-setup-nonblocking: keep config entry setup off the portal round-trip
Validate / HACS (push) Skipped
Validate / pytest (push) Successful in 12s
Validate / hassfest (push) Skipped
2026-08-04 16:04:27 +00:00
flan 0a5441779a Fix produce-box select cancelling config entry setup
Validate / hassfest (push) Skipped
Validate / pytest (push) Successful in 11s
Validate / HACS (push) Skipped
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.
2026-08-04 16:04:27 +00:00
flan e31f84e40b Run hassfest and HACS only where they can work
Validate / hassfest (push) Skipped
Validate / HACS (push) Skipped
Validate / pytest (push) Successful in 10s
Validate / hassfest (pull_request) Skipped
Validate / HACS (pull_request) Skipped
Validate / pytest (pull_request) Successful in 9s
Both jobs fail on the Gitea mirror for reasons no secret or input changes.

hassfest is a Docker-container action that bind-mounts $GITHUB_WORKSPACE. The
Gitea runner runs the job inside a container against a separate docker-in-docker
daemon, so the path resolves on the daemon's filesystem, not the job's; docker
creates an empty directory and mounts that, and hassfest then accurately reports
that it found no integrations in the empty tree it was given. It takes no token,
so credentials were never the issue.

HACS asks the github.com API about github.repository, which on Gitea is
flan/ha-freshharvest -- a slug that exists only on Gitea. That is the 401. The
action has no input to point the lookup at the sudolulo mirror.

The same commit that fails here passes on github.com/sudolulo/ha-freshharvest,
which mirrors every push, so the validation still happens -- it just happens
where it is capable of running. The guard is "not Gitea" rather than "is
GitHub" so an unexpected server_url runs the checks instead of dropping them.

pytest is untouched and still gates every push on both forges.
2026-08-04 14:22:06 +00:00
flan 1001d019a0 Add Validate status badge to README
Validate / hassfest (push) Failing after 7s
Validate / pytest (push) Successful in 10s
Validate / HACS (push) Failing after 22s
2026-08-04 02:45:27 +00:00
flan f04dadf21a Remove README badge wall; plain-text support section
Validate / hassfest (push) Failing after 5s
Validate / HACS (push) Failing after 16s
Validate / pytest (push) Successful in 7s
2026-08-03 23:43:00 +00:00
flan 70c67901b8 Make the README user-facing, and stop the compat job writing to a mirror
Upstream compatibility / compat (push) Successful in 7s
Validate / pytest (push) Successful in 7s
Validate / HACS (push) Failing after 16s
Validate / hassfest (push) Failing after 6s
The compat badge was red on bookkeeping, not on compatibility: the check passed
16/16 and the run then failed trying to commit the refreshed matrix and open a
PR. That step should never have existed here — GitHub is a read-only mirror of
Gitea, so anything a bot pushes is clobbered by the next sync. It now publishes
the matrix to the run summary and the workflow only needs contents:read, so the
badge means what it says.

The README had grown into an implementation document. Endpoints, markup traps,
the login handshake and the drift-detection design move to docs/internals.md;
what is left is installation, entities, events, the dashboard, requirements and
troubleshooting.
2026-08-03 21:10:01 +00:00
flan 4a8c4bebd7 Correct the README and add badges
Validate / hassfest (push) Failing after 5s
Validate / HACS (push) Failing after 17s
Validate / pytest (push) Successful in 10s
It still claimed a refresh was a single GET; it has been three since
subscriptions and vacation holds were added, and write actions cost more again
because every mutating endpoint needs its tokens fetched first.

Promotes three more markup traps out of commit messages and into the docs:
popups are fragments with no Sign Out control, openPobyup is written with and
without a space after the comma, and a select's id is not its POST field. Each
cost real debugging time and none is guessable from outside.

Notes that every break so far has been silent, which is the argument for the
compatibility checks existing at all.
2026-08-03 21:04:47 +00:00
flan 957505209a Cut 0.4.0: document the control entities and ship the dashboard
Validate / hassfest (push) Failing after 6s
Validate / pytest (push) Successful in 11s
Validate / HACS (push) Failing after 15s
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.
2026-08-03 20:59:04 +00:00
flan fa09ac6d5d Make CI green: install yarl, and give the repo topics
Validate / hassfest (push) Failing after 5s
Validate / pytest (push) Successful in 9s
Validate / HACS (push) Failing after 17s
pytest could not even collect: actions.py imports yarl for URL joining, which
Home Assistant ships but a bare CI python does not. The suite passed locally
only because the venv had picked it up as a transitive dependency.

The HACS check wants repository topics, which are GitHub-side metadata and so
cannot come from the canonical Gitea repo; set on the mirror directly.
2026-08-03 20:41:25 +00:00
flan 73a3409353 Refactor: real mixin, shared event helper, structural tests
Validate / hassfest (push) Failing after 6s
Validate / HACS (push) Failing after 17s
Validate / pytest (push) Failing after 8s
FreshHarvestActions inherited from itself to graft on basket switching, which
is legal Python and a trap. BasketMixin now sits above it and is inherited
normally.

The four control platforms each carried an identical private _fire; it moves to
the base entity as fire_action. Rewriting those call sites mechanically broke
two of them - select fired 'donate' instead of 'change_basket', and button lost
an argument entirely, a TypeError reachable only by pressing it. Both fixed,
and two tests now assert call arity and that no class inherits from itself,
because neither fault is reachable from a unit test.
2026-08-03 20:40:17 +00:00
flan 6ec06ee31a Verify basket, subscribe and vacation-hold round trips against a live account
Validate / hassfest (push) Failing after 5s
Validate / pytest (push) Failing after 7s
Validate / HACS (push) Failing after 16s
Three fixes the round trips found:

The produce-box select reported the SUBSCRIPTION rather than the delivery. A
one-off switch changes the delivery while the standing order keeps naming the
old box, so during the exact week someone had changed it the entity showed the
wrong box. It now reads the order.

parse_vacation_holds looked for ISO dates. The page writes 'Tuesday, Dec 1 -
Monday, Dec 7', so it reported no holds on an account that had one — which is
indistinguishable from having none. Its test asserted the same wrong format,
so the test passed while the parser was blind.

Adds hold removal (POST /s/submit/pause-range-remove), whose popup only exists
while a hold does.
2026-08-03 20:35:49 +00:00
flan e7427082e3 Wire produce-box switching as a select entity
Validate / hassfest (push) Failing after 6s
Validate / pytest (push) Failing after 10s
Validate / HACS (push) Failing after 17s
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.
2026-08-03 20:31:13 +00:00
flan 0a218acd45 Report the item a cart action actually acted on
Validate / hassfest (push) Failing after 6s
Validate / HACS (push) Failing after 16s
Validate / pytest (push) Failing after 7s
_cart_action scraped the first .item-name off the item page, which belongs to
whatever is in the mini-cart rather than the item being added — so an add
announced someone else's groceries in its event payload. Callers pass the name
they resolved instead; verified by an add/remove round trip against a live
order.
2026-08-03 20:23:49 +00:00
flan 5e9d721a0f Add restore, and separate the produce box from add-ons
Validate / hassfest (push) Failing after 8s
Validate / HACS (push) Failing after 17s
Validate / pytest (push) Failing after 8s
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.
2026-08-03 20:20:15 +00:00
flan 551d3240a2 Add control entities: box to-do list, skip switch, donate button, subscriptions
Validate / hassfest (push) Failing after 6s
Validate / pytest (push) Failing after 8s
Validate / HACS (push) Failing after 1m8s
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.
2026-08-03 20:13:32 +00:00
flan 213c16d98e Add the write-action layer and a daily upstream compatibility check
Upstream compatibility / compat (push) Failing after 7s
Validate / hassfest (push) Failing after 25s
Validate / HACS (push) Failing after 17s
Validate / pytest (push) Successful in 9s
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.
2026-08-03 20:02:58 +00:00
flan 3e880ff2c1 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.
2026-08-03 19:32:50 +00:00
flan 527f662189 Surface add-on prices and add an add-ons total sensor
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.
2026-08-03 19:15:19 +00:00
flan a97f0aec6a Add the Fresh Harvest dashboard view as an example
Ships the tab that is running on ha-box: a delivery countdown, order tiles,
box contents rendered from the item attributes, and the still-open order.
2026-08-03 19:02:11 +00:00
flan cd20802127 Add English translations for entity and config-flow strings
Custom integrations read translations/en.json rather than strings.json, so
without this the entity names fall back to their translation keys.
2026-08-03 18:48:30 +00:00
flan e1d8ba720c Implement dashboard parsing and delivery sensors
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.
2026-08-03 17:23:35 +00:00
flan eb50a9ecc1 Add Fresh Harvest integration scaffold
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.
2026-08-03 17:05:17 +00:00