flan 4a8c4bebd7
Validate / hassfest (push) Failing after 5s
Validate / HACS (push) Failing after 17s
Validate / pytest (push) Successful in 10s
Correct the README and add badges
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
2026-08-03 17:05:17 +00:00
2026-08-03 21:04:47 +00:00

ha-freshharvest

Validate Upstream compatibility HACS: custom Home Assistant 2025.2+ License: MIT GitHub Sponsors Ko-fi

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:

  1. HACS → ⋮ → Custom repositories
  2. Repository https://github.com/sudolulo/ha-freshharvest, category Integration
  3. Install Fresh Harvest, then restart Home Assistant
  4. 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_subtotal
  • open_order_free_delivery_remaining reaching 0.00 always coincides with a 0.00 delivery 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 half runs on a host that already holds the credential.

Worth knowing if you fork this: every markup break so far has been silent. Subscription rows moved and the integration reported 0 subscriptions; hold dates were not ISO and it reported 0 holds. A sensor reading zero is indistinguishable from an account with nothing in it, which is exactly why these are asserted rather than left to be noticed.

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:

  1. GET /s/popup/login returns the form plus two hidden anti-replay fields, LoginSecurity and SubmitToken, minted per session.
  2. POST /s/submit/login with LoginEmail, LoginPassword, both tokens, and an empty Redirect, yielding an fh_session_authenticated cookie.

The tokens are bound to the cookie issued by step 1, so both requests must share a cookie jar.

A refresh is three GETs: /p/dashboard/details for the delivery day, next arrival, both upcoming carts and their totals; /p/dashboard/manage-subscriptions for standing orders; and /p/dashboard/pause-deliveries for vacation holds. Three requests every six hours.

Write actions cost more, because nothing can be constructed offline — every mutating endpoint is guarded by rotating per-render tokens, so each action fetches the page that offers it, reads fresh tokens, checks they describe the intended target, and only then submits.

Markup notes

Six 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-skipped does 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.
  • Popups and AJAX replies are fragments, not pages. They carry no navigation, so a "am I still signed in?" check based on a Sign Out control reads every one of them as logged out. Skip could not run at all until these were fetched with that check disabled.
  • openPopup is written both ("x","y") and ("x", "y"). A regex requiring no space silently matches nothing on the pages that use the other form — which is every basket page.
  • A <select>'s id is not its POST field. The subscribe form's frequency control is id='FrequencyID' but name='popup-toggle'. Posting FrequencyID is accepted and does nothing.

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.

S
Description
Unofficial Home Assistant integration for Fresh Harvest (Georgia produce delivery)
Readme MIT
476 KiB
Languages
Python 98.1%
Shell 1.9%