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.
This commit is contained in:
flan
2026-08-03 20:59:04 +00:00
parent fa09ac6d5d
commit 957505209a
4 changed files with 136 additions and 57 deletions
+24
View File
@@ -5,6 +5,30 @@ All notable changes to this project are documented here.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [0.4.0] - 2026-08-03
Control, not just reporting: the box can now be managed from Home Assistant.
### Added
- Skip and restore, donate, add/remove add-ons, subscribe/unsubscribe,
vacation holds, and produce-box switching — every reversible one verified by
a live round trip that returned the account to its prior state.
- Entities for each: a produce-box select, a skip switch, a donate button, an
add-ons to-do list the built-in conversation agent can drive, and sensors for
subscriptions and vacation holds.
- A `freshharvest_action` event after every write, carrying action/success/
target/detail so automations can notify on the outcome.
- A four-section dashboard view with the controls in place, exported to
`examples/dashboard-view.yaml`.
### Notes
- Donating is the one action never executed: it cannot be undone, so it will be
proven the first time there is a box actually worth giving away.
- Entities are not exposed to the conversation agent by default; that is the
operator's call.
## [Unreleased] ## [Unreleased]
### Added ### Added
+29 -8
View File
@@ -32,6 +32,26 @@ 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 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. 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 ### The order arriving next
| Entity | Example | | Entity | Example |
@@ -45,11 +65,6 @@ edit is the following week's — so both are exposed separately.
| `sensor.fresh_harvest_next_delivery_fee` | `5.99` | | `sensor.fresh_harvest_next_delivery_fee` | `5.99` |
| `sensor.fresh_harvest_next_delivery_items` | `11` | | `sensor.fresh_harvest_next_delivery_items` | `11` |
`next_delivery_items` carries the contents as attributes: `produce`, `add_ons`
(each with quantity, unit and extended price), `produce_count`, `add_ons_count`
and `box`. The totals sensor carries `driver_tip` and `bounty_savings`, which
are optional or promotional rather than charges.
### The order you can still change ### The order you can still change
| Entity | Example | | Entity | Example |
@@ -61,15 +76,21 @@ are optional or promotional rather than charges.
| `sensor.fresh_harvest_shopping_window` | `Shop tomorrow` | | `sensor.fresh_harvest_shopping_window` | `Shop tomorrow` |
`binary_sensor.fresh_harvest_order_open` is the one to automate on: it turns off `binary_sensor.fresh_harvest_order_open` is the one to automate on: it turns off
when the cutoff passes, which is the last moment to add anything to the box. when the cutoff passes, the last moment to change the box.
`shopping_window` reads `closed` when nothing is changeable — distinct from
unknown.
### The account ### The account
| Entity | Example | | Entity | Example |
| --- | --- | | --- | --- |
| `sensor.fresh_harvest_delivery_day` | `Tuesdays` | | `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 ## Consistency guarantees
+7 -3
View File
@@ -1,12 +1,16 @@
{ {
"domain": "freshharvest", "domain": "freshharvest",
"name": "Fresh Harvest", "name": "Fresh Harvest",
"codeowners": ["@sudolulo"], "codeowners": [
"@sudolulo"
],
"config_flow": true, "config_flow": true,
"documentation": "https://github.com/sudolulo/ha-freshharvest", "documentation": "https://github.com/sudolulo/ha-freshharvest",
"integration_type": "service", "integration_type": "service",
"iot_class": "cloud_polling", "iot_class": "cloud_polling",
"issue_tracker": "https://github.com/sudolulo/ha-freshharvest/issues", "issue_tracker": "https://github.com/sudolulo/ha-freshharvest/issues",
"requirements": ["beautifulsoup4>=4.12"], "requirements": [
"version": "0.3.0" "beautifulsoup4>=4.12"
],
"version": "0.4.0"
} }
+76 -46
View File
@@ -1,11 +1,16 @@
# Fresh Harvest dashboard view. # Fresh Harvest dashboard view.
# #
# Four sections: the next delivery, its cost broken down per line, what is in # Four sections: the next delivery, what is in the box, the controls, and the
# the box, and the order that can still be changed. # add-ons you can still edit.
#
# The split matters. The produce box is CHOSEN (a select), its contents are
# read-only, and only add-ons are add/removable (a to-do list). The to-do list
# edits the OPEN order while the totals describe the ARRIVING one, which is why
# each is labelled with its date -- unlabelled, they read as a contradiction.
# #
# To use it, open your dashboard, choose "Edit dashboard" -> "Raw configuration # To use it, open your dashboard, choose "Edit dashboard" -> "Raw configuration
# editor", and paste this under `views:`. It needs the entities this # editor", and paste this under `views:`. It needs only the entities this
# integration creates and nothing else — no custom cards. # integration creates -- no custom cards.
type: sections type: sections
max_columns: 4 max_columns: 4
@@ -44,37 +49,6 @@ sections:
name: Items name: Items
icon: mdi:basket-check icon: mdi:basket-check
color: light-green color: light-green
- type: grid
cards:
- type: heading
heading: Cost Breakdown
heading_style: title
icon: mdi:receipt-text-outline
- type: tile
entity: sensor.fresh_harvest_next_delivery_box_price
name: Produce box
icon: mdi:package-variant
color: green
- type: tile
entity: sensor.fresh_harvest_next_delivery_add_ons
name: Add-ons
icon: mdi:cart-plus
color: purple
- type: tile
entity: sensor.fresh_harvest_next_delivery_subtotal
name: Subtotal
icon: mdi:calculator
color: grey
- type: tile
entity: sensor.fresh_harvest_next_delivery_tax
name: Tax
icon: mdi:bank
color: grey
- type: tile
entity: sensor.fresh_harvest_next_delivery_fee
name: Delivery fee
icon: mdi:truck-outline
color: grey
- type: markdown - type: markdown
content: |- content: |-
{%- set fee = states('sensor.fresh_harvest_next_delivery_fee') | float(-1) -%} {%- set fee = states('sensor.fresh_harvest_next_delivery_fee') | float(-1) -%}
@@ -112,24 +86,31 @@ sections:
- type: grid - type: grid
cards: cards:
- type: heading - type: heading
heading: Still Open heading: Manage
heading_style: title heading_style: title
icon: mdi:cart-arrow-right icon: mdi:tune-variant
- type: tile - type: tile
entity: binary_sensor.fresh_harvest_order_open entity: select.fresh_harvest_produce_box
name: Can still change name: Produce box
icon: mdi:pencil-outline icon: mdi:package-variant-closed
color: amber color: green
features:
- type: select-options
- type: tile - type: tile
entity: sensor.fresh_harvest_open_order_delivery entity: switch.fresh_harvest_skip_next_order
name: Delivery name: Skip next order
icon: mdi:calendar-arrow-right icon: mdi:calendar-remove
color: amber color: amber
- type: tile - type: tile
entity: sensor.fresh_harvest_shopping_window entity: sensor.fresh_harvest_shopping_window
name: Shopping window name: Shopping window
icon: mdi:clock-alert-outline icon: mdi:clock-alert-outline
color: orange color: orange
- type: tile
entity: sensor.fresh_harvest_open_order_delivery
name: Open order
icon: mdi:calendar-arrow-right
color: amber
- type: tile - type: tile
entity: sensor.fresh_harvest_open_order_total entity: sensor.fresh_harvest_open_order_total
name: Running total name: Running total
@@ -145,8 +126,57 @@ sections:
{%- elif left > 0 -%} {%- elif left > 0 -%}
The **{{ when }}** order needs **${{ '%.2f' | format(left) }} more** for free delivery. The **{{ when }}** order needs **${{ '%.2f' | format(left) }} more** for free delivery.
{%- endif -%} {%- endif -%}
- type: tile
entity: button.fresh_harvest_donate_next_order
name: Donate this box
icon: mdi:hand-heart
color: pink
hide_state: true
- type: markdown - type: markdown
content: |- content: |-
Once an order passes its cutoff it locks for packing, and the following week's cart opens. Donating gives the whole box away and **cannot be undone**.
[Change this order →](https://freshharvest.com/p/dashboard/manage-orders) [Manage on freshharvest.com →](https://freshharvest.com/p/dashboard/manage-orders)
- type: grid
cards:
- type: heading
heading: Add-ons & Standing Orders
heading_style: title
icon: mdi:cart-plus
- type: markdown
content: |-
{%- set d = states('sensor.fresh_harvest_open_order_delivery') -%}
{%- if d not in ['unknown', 'unavailable', 'none'] -%}
Editing the **{{ (d | as_datetime).strftime('%b %-d') }}** order — the one still open.
{%- else -%}
No order is open for changes right now.
{%- endif -%}
- type: todo-list
entity: todo.fresh_harvest_add_ons
display_order: alpha_asc
- type: tile
entity: sensor.fresh_harvest_next_delivery_add_ons
name: Arriving box add-ons
icon: mdi:cart-plus
color: purple
- type: tile
entity: sensor.fresh_harvest_subscriptions
name: Subscriptions
icon: mdi:autorenew
color: cyan
- type: markdown
content: |-
{%- set items = state_attr('sensor.fresh_harvest_subscriptions', 'items') or [] -%}
{%- if items -%}
**Repeating every week**
{% for i in items %}
- {{ i }}
{%- endfor %}
{%- else -%}
_No standing orders beyond the box._
{%- endif -%}
- type: tile
entity: sensor.fresh_harvest_vacation_holds
name: Vacation holds
icon: mdi:airplane
color: blue