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.
This commit is contained in:
flan
2026-08-03 17:23:35 +00:00
parent eb50a9ecc1
commit e1d8ba720c
10 changed files with 588 additions and 123 deletions
+43 -30
View File
@@ -3,55 +3,68 @@
Unofficial Home Assistant integration for [Fresh Harvest](https://freshharvest.com/),
the Georgia local-produce delivery subscription.
> **Status: incomplete — does not work yet.** The login handshake is implemented
> and the entity scaffolding is in place, but the portal page parsing is not
> written. See [Why it is unfinished](#why-it-is-unfinished).
## Entities
## What it is meant to expose
| Entity | Example | Notes |
| --- | --- | --- |
| `sensor.fresh_harvest_next_delivery` | `2026-08-04` | Attributes: `delivery_day`, `box` |
| `sensor.fresh_harvest_next_delivery_total` | `109.06` | Attributes: `subtotal`, `tax`, `delivery_fee` |
| `sensor.fresh_harvest_next_delivery_items` | `14` | Attributes: `box`, `produce`, `add_ons` |
| `sensor.fresh_harvest_open_order_delivery` | `2026-08-11` | The order you can still change |
| `sensor.fresh_harvest_shopping_window` | `Shop tomorrow` | `closed` when nothing is customizable |
| Entity | Value |
| --- | --- |
| `sensor.fresh_harvest_next_delivery` | Date of the next scheduled delivery |
| `sensor.fresh_harvest_order_total` | Cost of the upcoming order |
| `sensor.fresh_harvest_order_status` | Portal order status |
| `sensor.fresh_harvest_items_in_box` | Item count, with contents in `items` attribute |
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.
## How the site works
## 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 scrapes HTML.
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`.
an empty `Redirect`, yielding an `fh_session_authenticated` cookie.
The tokens are bound to the cookie issued by step 1, so the two requests must
share a cookie jar and cannot be cached or split.
The tokens are bound to the cookie issued by step 1, so both requests must
share a cookie jar. Everything the integration needs 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.
## Why it is unfinished
## Markup notes
Signed out, **every** `/p/*` path returns HTTP 200 — including invented ones.
The site has no distinguishable 404, so the account pages cannot be located by
probing, and the delivery markup cannot be guessed. Finishing this requires one
signed-in session to capture the real account, delivery, and box-contents pages.
Two traps are worth recording, since neither is guessable from the outside:
Concretely, what remains:
- Implement `FreshHarvestClient.async_get_next_delivery()` in
[api.py](custom_components/freshharvest/api.py).
- Replace the provisional `_looks_authenticated()` heuristic, which currently
guesses at a "sign out" link, with a real signed-in marker.
- **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`.
## Installation
Copy `custom_components/freshharvest/` into your Home Assistant `config/custom_components/`
directory and restart, then add the integration from **Settings → Devices & Services**.
Copy `custom_components/freshharvest/` into your Home Assistant
`config/custom_components/` directory and restart Home Assistant, then add the
integration from **Settings → Devices & Services**. Credentials are your normal
freshharvest.com email and password.
## Tests
```
pip install beautifulsoup4 pytest
pytest tests/
```
The fixture is synthetic but mirrors the real markup; the live page carries the
account holder's name, address, and phone number, so it is not committed.
## Disclaimer
Unofficial and unaffiliated. Polls every 6 hours; please do not lower that —
this is a small business's website, not an API.
Unofficial and unaffiliated. Polls once every 6 hours; please do not lower that
— this is a small business's website, not an API.