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.
ha-freshharvest
Unofficial Home Assistant integration for Fresh Harvest, the Georgia local-produce delivery subscription.
Reports what is arriving, what is in the box, what it costs — broken down per line so you can automate on any single part — and how long you have left to change the next order.
Installation
HACS (custom repository)
This is not in the HACS default list. Add it yourself:
- HACS → ⋮ → Custom repositories
- Repository
https://github.com/sudolulo/ha-freshharvest, category Integration - Install Fresh Harvest, then restart Home Assistant
- Settings → Devices & Services → Add Integration → Fresh Harvest
Manual
Copy custom_components/freshharvest/ into your Home Assistant
config/custom_components/ directory and restart, then add the integration as
above.
Credentials are your normal freshharvest.com email and password.
Entities
The next delivery and the open order are usually two different deliveries. Once an order passes its cutoff it locks for packing, and the cart you can still edit is the following week's — so both are exposed separately.
Three things arrive in a delivery and only one is a list you edit:
| What it is | Entity | |
|---|---|---|
| Produce box | Chosen, not assembled. Ten options. | select.fresh_harvest_produce_box |
| Box contents | Fresh Harvest fills it; read-only. | produce attr of ..._next_delivery_items |
| Add-ons | Yours to add and remove. | todo.fresh_harvest_add_ons |
Controls
| Entity | Notes |
|---|---|
select.fresh_harvest_produce_box |
Switches the next delivery only, not the standing order |
switch.fresh_harvest_skip_next_order |
Skip, and turn back off to restore |
button.fresh_harvest_donate_next_order |
Donates the box. Not reversible |
todo.fresh_harvest_add_ons |
Add/remove items; names resolve via the site's search index |
Home Assistant's built-in conversation agent can drive the to-do list through
HassListAddItem, so no bespoke voice work is needed.
The order arriving next
| Entity | Example |
|---|---|
sensor.fresh_harvest_next_delivery |
2026-08-04 |
sensor.fresh_harvest_next_delivery_total |
66.16 |
sensor.fresh_harvest_next_delivery_subtotal |
58.42 |
sensor.fresh_harvest_next_delivery_box_price |
33.00 |
sensor.fresh_harvest_next_delivery_add_ons |
25.42 |
sensor.fresh_harvest_next_delivery_tax |
1.75 |
sensor.fresh_harvest_next_delivery_fee |
5.99 |
sensor.fresh_harvest_next_delivery_items |
11 |
The order you can still change
| Entity | Example |
|---|---|
binary_sensor.fresh_harvest_order_open |
on |
sensor.fresh_harvest_open_order_delivery |
2026-08-11 |
sensor.fresh_harvest_open_order_total |
38.99 |
sensor.fresh_harvest_open_order_free_delivery_remaining |
11.58 |
sensor.fresh_harvest_shopping_window |
Shop tomorrow |
binary_sensor.fresh_harvest_order_open is the one to automate on: it turns off
when the cutoff passes, the last moment to change the box.
The account
| Entity | Example |
|---|---|
sensor.fresh_harvest_delivery_day |
Tuesdays |
sensor.fresh_harvest_subscriptions |
1, with each standing order in attributes |
sensor.fresh_harvest_vacation_holds |
0, with ranges in attributes |
Events
Every write action fires freshharvest_action with action, success,
target and detail, so an automation can notify on an add succeeding or a
skip failing.
Consistency guarantees
Two invariants hold against the portal's own arithmetic, and tests assert both:
next_delivery_box_price+next_delivery_add_ons==next_delivery_subtotalopen_order_free_delivery_remainingreaching0.00always coincides with a0.00delivery fee
Upstream compatibility
freshharvest.com has no API and no stability contract — this integration reads HTML and posts to form endpoints, so a redesign can change what a value means without changing its shape. tools/compat.py records every assumption and CI asserts them against the live site daily, refreshing this table and opening an issue on drift.
Last checked 2026-08-03.
| Area | Assumption | Status | Detail |
|---|---|---|---|
| Login | /s/popup/login serves the form |
✅ | 2273 bytes |
| Login | hidden LoginSecurity is minted |
✅ | 154 chars |
| Login | hidden SubmitToken is minted |
✅ | 174 chars |
| Login | posts to /s/submit/login |
✅ | /s/submit/login |
| Login | field LoginEmail present |
✅ | |
| Login | field LoginPassword present |
✅ | |
| Catalogue | Algolia credentials readable from site JS | ✅ | app id + search key found |
| Catalogue | index name readable | ✅ | dev_FullTest |
| Catalogue | index returns a plausible catalogue | ✅ | 946 records |
| Catalogue | record field ID |
✅ | present |
| Catalogue | record field Name |
✅ | present |
| Catalogue | record field Price |
✅ | present |
| Catalogue | record field Measurement |
✅ | present |
| Catalogue | record field Categories |
✅ | present |
| Endpoints | cart add/remove URL shape unchanged | ✅ | /p/Ajax/order-manage/ |
| Endpoints | popup route is /x/popup/{type}/{token} |
✅ | found |
Only the unauthenticated surface is checked here. The authenticated contract — dashboard markup, cart add hashes, skip popups, subscribe forms — needs a real session, and the only way to give public CI one is to put a personal grocery account's password in repo secrets. That belongs in a job on a host that already has credential access, not here.
How it works
Fresh Harvest is not on Shopify, Farmigo, or Local Line — the page metadata
reports Vy Technology - Custom Code. It is a server-rendered jQuery site with
no JSON API and no mobile app, so this integration signs in and parses HTML.
Login is a two-step handshake:
GET /s/popup/loginreturns the form plus two hidden anti-replay fields,LoginSecurityandSubmitToken, minted per session.POST /s/submit/loginwithLoginEmail,LoginPassword, both tokens, and an emptyRedirect, yielding anfh_session_authenticatedcookie.
The tokens are bound to the cookie issued by step 1, so both requests must
share a cookie jar. Everything then comes from a single
GET /p/dashboard/details, which carries the delivery day, next arrival date,
both upcoming carts, their contents, and their totals. One request per refresh,
every six hours.
Markup notes
Three traps, none guessable from the outside:
- HTTP status means nothing. Every
/p/*path returns 200, including invented ones. Signed-in state is detected by the presence of a Sign Out control, not by a status code. cart-contents-skippeddoes not mean the order was skipped. It marks the locked cart — the one past its cutoff and arriving next. Treating it as "skipped" reports the wrong delivery as cancelled. The reliable signal for "can still be changed" is a non-empty.cart-customize-wrapper.- The free-delivery bar only renders on carts below the threshold. An order that already qualifies has no bar at all, so the threshold is read once from whichever cart shows it and applied to every order.
Dashboard
examples/dashboard-view.yaml is a ready-made tab
— a countdown heading ("Arriving tomorrow"), tiles for the cost breakdown, the
full box contents rendered from the attributes, and the still-changeable order.
Paste it under views: in the raw configuration editor.
Tests
pip install beautifulsoup4 pytest yarl
pytest tests/
The fixture is synthetic but mirrors the real markup, with placeholder cart IDs and self-consistent totals; the live page carries the account holder's name, address and phone number, so it is never committed.
Compatibility
Requires Home Assistant 2025.2 or newer. Developed and running against 2026.7.
Disclaimer
Unofficial and unaffiliated — not endorsed by or supported by Fresh Harvest. Please do not lower the six-hour poll interval: this is a small business's website, not an API.