# Changelog

Each release leads with what changed in plain English; the technical notes are one click below. The integration and the controller are released as a matching pair. This page is the project's two changelog files from release v3.18.1, shown as written, with room names and one sentence redacted (DECISIONS D8).

[Integration](#integration) [Controller](#controller)

## Integration

All notable changes to Crop Steering will be documented in this file.

**Two views per release.** Each version leads with **🌱 In plain English** — what changed and why it matters, written so anyone can follow it without knowing the internals — followed by **🔧 Technical notes**, the entity- and code-level detail for developers and AI agents working on the repo.

The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [[3.18.1] - 2026-10-28](#3-18-1)

Pair: controller 3.18.1

3.18.1 — simpler README and first-run guide, one docs index, internal notes archived, bug report form; dashboard: readable Today strip on phones (#236), decluttered daily plan card (#239).

Pair: **controller 3.18.1**. **C1: documentation and two dashboard display fixes.** The controller, engine, integration runtime, entities, add-on options and persisted state are unchanged; the dashboard bundle changes only how two screens are drawn. **Approved rehearsal, no staging soak:** the owner approved releasing and promoting this candidate on 2026-10-27, and on 2026-10-28 approved including the two dashboard display fixes (docs/RELEASING.md step 5, `approved-rehearsal`). No staging photoperiod and no visual check on a staging room is claimed.

### 🌱 In plain English

- **Readable Today strip on a phone.** Each watering (the dashboard calls it a shot) is a thin tick on a phone instead of a pile of pills, and the hour labels no longer overlap the "now" marker.
- **A tidier daily plan card.** The chart key keeps Recorded, Target and Draft; the rest is explained in the workings, and the phase table folds in with them.
- **Simpler documentation.** A shorter README and first-run guide, one documentation index, internal notes moved to an archive, and a bug report form on GitHub.
- **Nothing else changes on your box.** Updating changes no setting and no watering behaviour.

**🔧 Technical notes**

- #236 `fix(dashboard)`: below 720 px a shot on the Today strip is a 3 px tick (was an 8 px minimum block); the axis drops an hour label within 18% of the span of the "now" chip (`frontend/src/components/shot-timeline.css`, `frontend/src/pages/today.tsx`).
- #239 `fix(dashboard)`: the plan card's previous grow-day peak and low move to the Earlier days toggle's tooltip; the chart key keeps Recorded, Target and Draft; the on-chart "Re-water" label is removed; the phase table folds into "Phase by phase, and how this is worked out"; gauge footers wrap (`frontend/src/components/planning-curve.tsx`, `frontend/src/styles.css`, `frontend/src/components/hmi.css`, `frontend/scripts/verify-steering-visuals.mjs`). The three committed `dashboard.html` bundles are rebuilt from `frontend/src`.
- Docs (#234, #237, #238, #240, #241, #242, #243): short README, `docs/FIRST_RUN.md`, `docs/README.md` as the one index, internal notes under `docs/archive/` and `docs/dev/` (moved pages keep a "This page moved" stub), 2.x changelog sections in `docs/archive/CHANGELOG-2.x.md`, `.github/ISSUE_TEMPLATE/bug_report.yml`, `ROADMAP.md` and `docs/SYSTEM_MAP.md`. #240 changed only path comments in `addons/f2_control/f2_control/guards.py`, `zone_history.py` and `frontend/src/lib/parity-types.ts`.
- CI (#233, #235, #244): GitHub Actions bumped, Dependabot targets `testing`, the Academy browser test waits for the new video group before seeking.
- No change to controller, engine or integration code, entity ids, add-on options or `/data/state.json`.

## [[3.18.0] - 2026-10-24](#3-18-0)

Pair: controller 3.18.0

**⚠️ This is a very large upgrade.** Almost every page of the dashboard is new or rebuilt: timer programs, presets, alert rules, a new History section, feed and pump calibration, pump supply sharing, quiet hours and an Upgrade report.

**If you are running this on a live grow, do NOT just click update** unless you have time for a big configuration session straight after. Watering will not stop, but there is a lot to set up and check.

**Get your AI to configure it from the new docs.** Point your AI assistant (Claude, ChatGPT, or any assistant connected through the Crop Steering MCP server) at the new documentation and have it work through the Upgrade report with you, one line at a time, asking you before each save.

**Your settings come across as far as they can.** The update keeps every zone watering and dosing exactly as before, and carries your current settings into the new features wherever it can do that without guessing. Nothing new that waters or doses is switched on for you.

- **Carried across:** every zone keeps watering by steering; each recipe stage you had edited is copied as a preset; a "before 3.18 update" preset of your current settings is saved as a rollback point; temperatures in °F if Home Assistant uses °F; every notification recipient; your pH up/down pumps keep their role; your earlier setting changes, stock moves, calibrations and dosing records are imported into History; a room with no feed EC probe keeps the old 3.0 mS/cm assumption so it flushes as before.
- **Needs you:** the irrigation supply (each pump's real flow, how many zones it feeds at once, its flow meter: nothing is saved until you save it); the 2-point feed pH and EC probe calibration; the solution and a pump calibration for each nutrient pump; ppm instead of mS/cm if that is how you work; automations that still use a retired entity; grow-plan recipes saved only in your browser (export them). Optional: alert rules, quiet hours, camera, CO2 and light panels, new dashboards.
- Each room's **Upgrade report** lists exactly what was carried and what needs you: Crop Steering → Settings → System status, and a card in Home Assistant's Settings → Repairs.

**What it looks like.** Screenshots of the 3.18 dashboard running on its built-in demo data (sample rooms, not a real facility); the phone view is in the dark theme.

- **Status: all rooms.** Every room and every zone on one page: phase, moisture and water today.
  ![Status, all rooms](https://cropsteering.growlabs.nz/upstream-img/status-all-rooms-desktop-light.png)
- **Zone page.** One zone in full: moisture, EC, what it is waiting for and every shot today.
  ![Zone page](https://cropsteering.growlabs.nz/upstream-img/zone-desktop-light.png)
- **Timeline.** The day's phases and shots for a room on one line, so you can see what happened when.
  ![Irrigation timeline](https://cropsteering.growlabs.nz/upstream-img/irrigation-timeline-desktop-light.png)
- **Timer programs.** Each zone can water by steering (as before), by timer programs, or be off.
  ![Timer programs](https://cropsteering.growlabs.nz/upstream-img/timer-programs-desktop-light.png)
- **Grow plan & presets.** Save a room's or a zone's targets, phase timing and limits, and copy them to other rooms or zones.
  ![Grow plan and presets](https://cropsteering.growlabs.nz/upstream-img/grow-plan-presets-desktop-light.png)
- **History: explorer.** Any reading, any room, any period, on one chart.
  ![History explorer](https://cropsteering.growlabs.nz/upstream-img/history-explorer-desktop-light.png)
- **History: changes.** Every setting change, who made it and when, including what the upgrade carried across.
  ![History of changes](https://cropsteering.growlabs.nz/upstream-img/history-changes-desktop-light.png)
- **Feed & dosing.** Each dosing pump with its solution, its stock tank and its doses.
  ![Feed and dosing](https://cropsteering.growlabs.nz/upstream-img/feed-dosing-desktop-light.png)
- **Feed pH/EC calibration.** A guided 2-point calibration at the tank; nothing changes until you commit it.
  ![Feed probe calibration](https://cropsteering.growlabs.nz/upstream-img/feed-calibration-desktop-light.png)
- **Setup: pumps & valves.** Your pumps, main line and valves, and the new pump supply: real flow and zones at once.
  ![Setup, pumps and valves](https://cropsteering.growlabs.nz/upstream-img/setup-pumps-valves-desktop-light.png)
- **Alert rules.** Your own notifications, with a preview of how often each would have fired.
  ![Alert rules](https://cropsteering.growlabs.nz/upstream-img/alert-rules-desktop-light.png)
- **System status.** The health of every part, and each room's Upgrade report.
  ![System status](https://cropsteering.growlabs.nz/upstream-img/system-status-desktop-light.png)
- **AI assistant.** The exact instructions it works from, your room notes and what it costs.
  ![AI assistant settings](https://cropsteering.growlabs.nz/upstream-img/ai-assistant-desktop-light.png)
- **On your phone.** Every page works on a phone in the grow room.
  ![Phone view](https://cropsteering.growlabs.nz/upstream-img/status-all-rooms-phone-dark.png)

**Update the integration BEFORE the add-on.** Update Crop Steering in HACS first and restart Home Assistant, then update the Crop Steering controller app. The integration runs the upgrade and writes the Upgrade report; the 3.18 controller reads what it writes.

**Upgrade guide and AI setup.**

- [3.18 upgrade report, line by line](https://cropsteering.growlabs.nz/docs/upgrade-report): one section per Upgrade report line, written so an AI can follow it step by step.
- [Upgrade to 3.18: the plan](https://cropsteering.growlabs.nz/docs/upgrade-guide): what stays the same, a first-hour checklist and how to go back.
- [Set up an install with an AI assistant](https://cropsteering.growlabs.nz/docs/ai-setup): start read-only and approve each change, and [Configure Crop Steering through MCP](https://cropsteering.growlabs.nz/docs/mcp): connect an assistant to your Home Assistant.
- [User guide](https://cropsteering.growlabs.nz/docs/user-guide), [AI assistant](https://cropsteering.growlabs.nz/docs/ai-assistant) and the [Crop Steering Academy](https://jaketherabbit.github.io/HA-Irrigation-Strategy/learn/).

Pair: **controller 3.18.0**. **C3: the parity release.** The controller, the engine, the integration, entities, add-on behaviour, persisted state and the dashboard all change. **Approved rehearsal, no staging soak:** the owner approved releasing and promoting this candidate without the staging soak on 2026-10-23 (docs/RELEASING.md step 5, `approved-rehearsal`). It passed CI (including the real Home Assistant tier on the newest and oldest supported versions, the add-on image builds and the controller container smoke), a 216-visit route sweep of the dashboard and a final code review. **Nothing in this release has been run on real plumbing**: no staging photoperiod, no catch test and no fault drills.

### 🌱 In plain English

**New in 3.18 (the parity release)**

- **Timer programs and watering modes.** Each zone waters by steering (as before), by timer programs, or is off.
- **Pump sharing and flow checks.** Say how much each pump really delivers and how many zones it can feed at once; shots queue instead of starving each other. A flow meter checks each shot's water against what was asked for, and the controller looks for leaks.
- **Calibration.** A guided 2-point feed pH and EC probe calibration, and a prime-and-calibrate wizard for each dosing pump. Nothing changes until you commit it.
- **Presets and copy.** Save a room's or a zone's targets, phase timing and limits as a preset and copy them to other rooms or zones. Grow-plan recipes saved in your browser import as presets in one click.
- **History.** An explorer for any reading over any period, a change journal of every setting change (who, when, and what the upgrade carried across) with put back and undo, and a compare view.
- **Alerts.** Your own alert rules, tested against yesterday before you save them, an alerts inbox and quiet hours per phone.
- **Display and dashboards.** Display units (ppm or mS/cm, °F or °C), and dashboards made from templates.
- **System status.** The health of every part and each room's Upgrade report in one place.
- **Feed & dosing.** Every dosing pump with its solution, stock tank and doses; dosing refusals are recorded with their reason.
- **The upgrade itself.** Your settings carried across where that was safe, with a report of the rest.

**Fixed**

- A shot that fails after its valve opened now counts the water it delivered.
- The controller reads the pump, the main-line valve and the zone valve back ON at the start of every shot. A dead relay or an offline plug now cancels the shot with an alert instead of counting water that never flowed, and a zone that keeps failing backs off.
- P0 now waits for the plants to start drinking after lights-on. Only a zone at its rescue floor skips the wait (#195). Rooms that skipped P0 every morning now take their first shot later, up to the zone's P0 maximum wait.
- A zone with a dead moisture probe gets its minimum daily water again, only with the lights on and outside P0. Its fallback timer shot no longer fires at night or outside P1/P2.
- With no feed EC probe and no recipe feed EC, the controller no longer invents a feed EC of 3.0 on a new room; such a room gets one card saying EC flushing is off. Rooms that had it before the update keep the 3.0 assumption (see the Upgrade report).
- A failed read of the shot-length cap no longer lifts it to 900 s, an unreadable feed EC/pH band holds the shots, and a failed Home Assistant read or write is never taken as a real value.
- An error in one room's pass no longer stops the other rooms, and a shot-start error can no longer wedge the controller.
- Lights-on and lights-off set to the same hour are not used.
- A setpoint the controller failed to write is no longer credited to it in History.
- The phase sensor never makes up a phase, and the controller says so when it has no next-shot estimate.
- One source of truth for the derived watering values (shot length, adjusted re-water point, EC ratio, room averages, shots today). Settings that only fed retired sensors are hidden, and Repairs names any automation that still uses one.
- The dashboard shows the controller's own dryback, peak and next shot, its own limits, EC band and tank marks, reads water volumes in the unit Home Assistant reports, shows every time on Home Assistant's clock, and its links land where they say.
- All rooms is a visual page with slabs and tanks, without duplicate pickers.
- The dosing form uses the room's own dose limits.
- `apply_recipe` and `save_recipe` act on one room, and the Recipe stage select shows a stage only once it is applied.
- Long-term history reads the sensors from this room's archived runs.
- The MCP server accepts what the integration accepts (pumps, valves, probe units).

**Deprecated.** The named-stage recipe system (the Recipe stage and Irrigation phase selects, and the `apply_recipe` and `save_recipe` services) is marked as going away in **a future major release**. It still works; the first use after a restart raises a Repairs notice saying what to use instead.

**Documentation.** A shorter README, a [documentation index](https://github.com/JakeTheRabbit/HA-Irrigation-Strategy/blob/v3.18.1/docs/README.md), one page per new feature in [docs/features](https://github.com/JakeTheRabbit/HA-Irrigation-Strategy/blob/v3.18.1/docs/README.md#use-it-every-day), the system map and ranked system audit, and help text that follows the 3.18 page names.

**🔧 Technical notes**

- **Upgrade migration:** `custom_components/crop_steering/migration.py`, a run-once, versioned ledger (`crop_steering.migration` store) per room and site. Every seeded value is journaled as `config.changed` with actor `migration`; old stores are read only. The report is served by `crop_steering.migration_report` and raised as the Repairs issue `upgrade_report_3_18` (CS-616) while a room has a "Needs you" line. M15 keeps controller 3.17.0's 3.0 mS/cm feed EC for a room with no feed EC probe and no recipe feed EC. Row by row: [3.18 upgrade report, line by line](https://cropsteering.growlabs.nz/docs/upgrade-report).
- **Parity backend (#230):** one ordered GuardChain for every shot check, with dosing refusals through it and journalled; pump capacity and an exclusive-valve queue; timer programs and per-zone watering modes (an unparseable `timers_config` falls back to the engine under each zone's mode); timed pump prime and calibration runs; metered-vs-commanded water reconcile per shot (`flow.py`, baseline from the pass's own reading, never a read before the pump opens) and CS-319 leak detection; calibrated feed probes for the source-water gate, mix check and pH correct; journal outbox and backfill; integration APIs for site, timers, presets and copy, alert rules and inbox, changes, timeline, system status, dashboards, probe offsets and calibration. New error codes CS-612, CS-615, CS-616.
- **Parity dashboard (#230):** Feed & dosing, Pumps & valves, Prime & calibrate a pump, Feed calibration, Stock, Water & flow, Timer programs, History explorer/changes/compare, Alerts, Notifications, System status and AI pages. Bundles rebuilt deterministically (three copies byte-identical). Route sweep: every page × desktop/phone × light/dark × 3.18/3.17 demo backends, 216 of 216 visits clean.
- **Fix pull requests merged through #229:** #196 All rooms visual slabs and tanks; #197 shot error after the valve opened counts its water; #198 docs follow 3.17 section names; #199 water volumes in Home Assistant's unit; #200 dosing form uses room dose limits; #201 failed shot-cap read no longer lifts the cap (CS-203); #202 unreadable feed EC/pH band holds shots; #203 blind fallback shot only with lights on, in P1/P2; #204 one room's error no longer stops the others (CS-212); #205 `apply_recipe`/`save_recipe` scoped to one room; #206 recipe stage select shows a stage once applied; #207 `current_phase` never invents a phase; #208 pump, main line and valve read back ON at shot start (CS-302/303/304, backoff after 3 cancelled shots); #209 equal lights-on/off hours unused; #210 shot-start error cannot wedge the controller (CTL-032); #211 controller's own dryback, peak and next shot on the dashboard; #212 system map and ranked audit; #213 Academy local URL; #214 MCP accepts pumps, valves, probe units; #215 failed Home Assistant read/write never a real value; #216 blind-zone minimum daily water and no invented feed EC (CTL-031, CTL-008, CS-907); #217 every dashboard and Academy browser check on pull requests; #218 no next-shot estimate said so; #219 dashboard links land where they say; #220 `history_long` reads this room's archived runs; #221 recipe system deprecated (CS-613), stage applied once, dashboard follows the controller's steering mode; #222 one source of truth for derived watering values (`shot_length.py`, CS-614); #223 failed setpoint write takes back its claim (CTL-035); #224 actions pinned to commits, honest `run_ci.sh`, faster container smoke; #225 dashboard one source for limits, bands, time and readability; #226 README and feature matrix; #227 P0 bypass only at the rescue floor (#195); #228 help text catch-up.
- **Deprecation (#221):** new `deprecations.py` (`note_deprecated`): a log warning on every use and one Repairs issue (`deprecated_<key>`, CS-613) per Home Assistant run. `apply_recipe` no longer calls `select.select_option` (INT-003); `buildRoom` picks the EC target family as the controller's `_veg()` does (INT-014).
- **Documentation:** new `docs/README.md`, `docs/UPGRADING-TO-PARITY.md`, `docs/AI-SETUP.md`, `docs/features/*.md`; `docs/ENTITIES.md` gains the 3.18 entities. The screenshots in `docs/screens/` are captured from the compiled demo with the clock pinned (`node frontend/scripts/capture-screens.mjs`, then `python scripts/optimise_screens.py`).
- Rolling back to 3.17.0, and what 3.17.0 does not read after the update, is covered in [Upgrading to 3.18](https://cropsteering.growlabs.nz/docs/upgrade-guide).

## [[3.17.0] - 2026-10-21](#3-17-0)

Pair: controller 3.17.0

Pair: **controller 3.17.0**. **C1: dashboard navigation.** Controller, engine, integration runtime, entities, options and persisted state are unchanged. The dashboard bundle was hot-deployed to the owner's Work HA add-on before release, at the owner's request; no photoperiod soak is claimed.

### 🌱 In plain English

- **Get to any zone in one click.** The menu lists every room with its state, and every zone under it with its phase and moisture. Choosing a room keeps you in the section you are in; choosing a zone opens its page, in any room.
- **Built for a phone in the grow room.** The five sections sit in a bar along the bottom of the screen, where a thumb reaches them, and a tap anywhere on a zone card opens the zone.
- **Links remember the room.** The address now holds the room you have open, so a bookmark, a shared link or a reload brings you back to the same room.
- **Clearer names.** Overview is now **Status** (with **All rooms** and **Room**), and Settings & help is now **Settings**. Old links still work.
- **Water at room level.** Each room on All rooms shows its water today: total litres and litres per plant.

**🔧 Technical notes**

- New `frontend/src/components/room-tree.tsx` (+ css): rooms from `facilityRooms`, zones with phase, VWC and stale/paused flags; desktop sidebar and the phone sheet. `App.tsx` gains `openRoom` (keeps the section) and a fixed `.bottom-nav` at ≤720 px; the selected room is written to `?room=` as its short alias (`resolveRequestedRoom`), else its id.
- `page-tabs.tsx` labels: Status / All rooms / Room / Settings. Routes, hashes and legacy redirects are unchanged. `all-zones.tsx`: room water total and per-plant average over zones reporting both; whole-card zone tap via a stretched title button.
- Docs, `docs/error-codes.json` labels (regenerated `ERROR_CODES.md`), screenshots (27, plus `img/mobile-all-rooms.png`) and browser checks follow the new names; `verify-dashboard.mjs`'s static server reads a file before writing headers.
- No change to `addons/f2_control/f2_control/`, `crop-steering-engine/`, integration Python, entity ids, add-on options or `/data/state.json`.

**Earlier 3.x releases (24, 3.16.1 back to 3.0.0)**

## [[3.16.1] - 2026-10-20](#3-16-1)

Pair: controller 3.16.1

Pair: **controller 3.16.1**. **C0: teaching documentation and website.** Controller, engine, integration runtime, dashboard bundle, entities, options and persisted state are unchanged. Published at the owner's request; no soak is required for C0 and none is claimed.

### 🌱 In plain English

- **Learn crop steering from zero.** A free beginner course, the Crop Steering Academy, now ships with the project and on the project website: 24 short captioned lesson videos, five longer chaptered parts, exercises with feedback and full transcripts.
- **Water, roots and air first.** The lessons build from physical basics to VWC, capacity, dryback, EC, reading the graphs, equipment, setup and day-to-day operation.
- **Nothing changes on your box.** Updating changes no setting, no irrigation behaviour and no screen in the dashboard.

**🔧 Technical notes**

- New `www/learn/` academy (catalogue, player, navigation, quiz progress export/import) and `docs/training/` course sources; GitHub Pages builds and installs checksum-pinned narration and video from the `course-media-2026.10.19-r3` pre-release.
- Course production scripts (`scripts/render_*`, `scripts/package_course_videos.py`, `scripts/pilot_timing.py`) and their Python/Node/browser tests. Release packaging workflow and Pages workflow updated for the academy; no runtime workflow gate changed.
- No change to `addons/f2_control/`, `custom_components/crop_steering/` code, `crop-steering-engine/`, entity ids, add-on options or `/data/state.json`. Version fields advance together only to keep `main` on a tagged pair.

## [[3.16.0] - 2026-10-18](#3-16-0)

Pair: controller 3.16.0

Pair: **controller 3.16.0**. **C1: dashboard and operator guidance.** Controller and integration runtime behavior are unchanged. Published at the owner's request after the live UI rehearsal; no full photoperiod or physical water-delivery soak is claimed.

### 🌱 In plain English

- **See every room together.** All zones is the landing page, with moisture, pore EC, last irrigation, water totals and each room's feed supply. Choose another starting page in Settings > Display.
- **Plan and adjust irrigation in one workspace.** Current day and Calendar share the combined moisture/EC canvas. On a desktop, the graph stays beside the target controls.
- **Find each setting in one place.** Equipment owns room and zone setup, History owns recorded evidence and grow records, and Capabilities points to each setting's editor.
- **Keep your work while you navigate.** Draft warnings protect unsaved target and setup edits; refused setup saves explain what must be resolved.
- **Use the updated guides and demo.** The README, public demo, tutorials and screenshots follow the new navigation. Water delivery remains labelled by source and average per plant is not measured uptake.

**🔧 Technical notes**

- Browser-local landing preference; explicit bookmarks and legacy links retain precedence. All-room landing reads the existing controller snapshot and never issues actuator commands.
- Canonical configuration ownership, read-only capability directory, hydraulic conflict checks and stock level preservation; existing backend validation, revision and actuation guards retained.
- Irrigation retains current-day and schedule ownership, one visible planning canvas, pending drafts and shared zone focus. A collapsed stage guide preserves the full-grow reference.
- Existing settings, entity IDs, room mappings, controller algorithms and persisted state format are unchanged. Both package versions advance together; rebuild the controller to retain its packaged dashboard update.
- Setup validation test uses a loopback-only resolver to avoid a process-long pycares thread in the oldest supported Home Assistant test harness. No production resolver or controller changes.

## [[3.15.0] - 2026-10-17](#3-15-0)

Pair: controller 3.15.0

Pair: **controller 3.15.0**. Restored controls, operating history and editable AI context. Promoted under the owner's approved rehearsal instruction; no real-plumbing soak is claimed.

### 🌱 In plain English

**Read this first. Unreadable waste-rule settings now hold irrigation instead of silently turning the rule off. A pH dosing fault stays latched through an unavailable enable switch; only a confirmed switch-off clears it.**

- **Choose the controls you need in Features.** Phase timing, safety, sensing, dosing and display settings have one searchable catalogue. New optional modes keep the existing behavior until selected.
- **See more of the room.** Choose cards or a table, camera and light readings, probe panels, plant-stress indicators and history overlays. Extra readings require mapped sensors.
- **Keep a record of each grow.** Save cultivar labels, grow-day summaries, harvest weight and the operating history. An optional Influx connection extends sensor history; Recorder remains the default.
- **Read the instructions the AI actually receives.** Review its shipped instructions, add room context, see the saved revision, and choose instruction cache durations. Estimated cache savings are labelled as estimates.
- **Use optional runoff and dosing checks.** Shared-sump runoff is a water-balance estimate, not a measured figure for each zone. Corrected AI tool schemas avoid one cause of rejected requests; all answers remain checked before use.

**🔧 Technical notes**

- Restored phase/safety packs #170/#164, sensing/runoff #167/#177, dosing #168; neutral defaults, additive state migration and fail-closed readback retained. Last known waste policy persists across restart; only a truly absent legacy setting defaults Off. pH enable unknown/unavailable no longer clears the fault latch (#179).
- Features registry and grow-day summary #165/#166/#172, Display settings and zone/history panels #169/#173/#174, feature metadata and optional DLI mapping #181. Sensor mappings are display-only unless explicitly described by a control feature.
- Per-zone cultivar labels #175, grow/harvest records #176, optional admin-only InfluxQL/Flux connection and bounded long-history reads #178 with Recorder fallback. No live Influx credentials or physical runoff calibration are included.
- Prompt contract corrections #171 and capability-conditioned restored-mode brief #182. AI Instructions #180 publishes the exact live block hashes and revisioned room context, with reviewed save/rollback/readback and room cache TTL controls. Substrate and stage remain existing room settings; shipped doctrine replacement is not included.
- Provider schema mitigation #183 stops strict grammar compilation for answer_choice alone. Four other tools, including set_values, remain strict; local vote and setpoint validation is unchanged.

## [[3.14.0] - 2026-10-16](#3-14-0)

Pair: controller 3.14.0

Pair: **controller 3.14.0**. #154, #160 and #162. Released by the owner's decision without a staging soak (approved rehearsal).

### 🌱 In plain English

**Read this first. It changes what the AI assistant is asked on rooms that update:**

- **The AI assistant is now Claude only, and it is asked every question by default, including the P1 ramp.** A judge list copied from the old assistant is read as "all", and the log says once which judges that adds. The first AI route (Cloudflare / TypeSafe) is gone: a room that used it moves to Claude when it has an Anthropic key, and runs on its rules when it has none. The powers checklist is still where you turn each power on or off.

What else you get:

- **History explorer on the zone page.** Pick a date range (today, the grow-day, 3, 7 or 14 days, this run, or your own), tick any sensor in the room, see the phases and shots, and lay the previous run over it for the same days of the run.
- **Clearer cards.** The daily plan, zone cards and Today show one status line, four gauges and one advisory, with the previous grow-day, a short key and the reference dryback band. The longer explanations sit under "How this is worked out". A reading of 1% or less is shown as a probe gap, not as a dry zone.
- "Jev" now reads "AI assistant" everywhere you see it. Entity ids do not change.

**🔧 Technical notes**

- #162: `jev/` -> `brain/`, `jev_bridge` -> `brain_bridge`; new option `ai_judges` (default `all`), `jev_judges` read as a fallback while `ai_judges` is the default, and a list that names only judges from the move is read as all; `jev_*` tuning options renamed `ai_*` with the old value used while the new one is at its default; Cloudflare/TypeSafe transports removed (old keys accepted, unused); CS-712 "First AI route removed" card once per release; docs/AI_ASSISTANT.md (docs/JEV.md is a stub). Old installs 2.25.1 to 3.11.1 proven in tests_ha.
- #160: zone page History explorer (range in the URL, per-unit axes, day-by-day history downsampled to about 720 points per sensor, previous run aligned by grow day and lights-on); the room descriptor publishes mapped room temperature, humidity and VPD sensors when mapped.
- #154: HMI cards (status line, gauges, one advisory chip with Apply to draft, projection table); VWC <= 1% is a probe gap, left out of lines, peak, dryback and statistics.

## [[3.13.0] - 2026-10-16](#3-13-0)

Pair: controller 3.13.0

Pair: **controller 3.13.0**. Everything in 3.12.2 (which was tagged but never promoted) plus #156, #157, #158 and #159. Released by the owner's decision without a staging soak (approved rehearsal).

### 🌱 In plain English

**Read these first. They change when water flows on rooms that update:**

- **P1 now ends when the ramp stalls under its target.** If two ramp shots in a row do not raise the moisture, the zone moves to P2, and for the rest of that grow-day its re-water point sits just under what the zone reached, so P2 does not keep chasing a level the slab cannot hold. A probe in doubt (water bypass, a table that does not drain, a reading that does not move) keeps the old behaviour. With the AI assistant on, it is asked first and may hand over one shot sooner.
- **The AI assistant has powers you can tick, per room.** The six it already used stay on. Two new ones are off until you turn them on: "Set targets from what's achievable" (it sets field capacity, peak, re-water, rescue floor and dryback target itself, limited only by each setting's own range) and "Tune shot sizes". Every value it writes is read back and logged with its reason and the result it expects.

What else you get:

- **The AI assistant knows the goal and the rules.** Each night it gets a scorecard per zone for the last five grow-days and its own earlier changes with what followed, and its cached prompt carries the rules and a description of how the controller works, built from the code.
- **A run progress strip at the top of Today:** day X of N, days done and left, harvest date, the grow plan and stage, and the feed recipe with this week's EC, on a bar split into the plan's phases.
- **Compare runs has charts** for each grow week (from 3.12.2), a missing hold switch is named (CS-210), and the AI's "why" names the kill switch and holds.

**🔧 Technical notes**

- #158: engine P1 below-target stall exit (`p1_stall_shots`, default 2; rise judged by `p1_rise` on settled readings); controller state gains `p1_flat` and `p1_stall` (old state loads as 0/None); day-scoped re-water via `auto_setpoints.p2_band`; envelope allows a stall hand-over below the ceiling; ramp judge asked at the first non-rising shot.
- #156: per-room `switch.crop_steering_<prefix>ai_power_<key>` (missing switch = its default); `set_targets_from_achievable` and `tune_shot_sizes` on the Claude route only, values clamped only to each entity's range and logged; goal scorecard, rules POW-01..10 and `jev.controller_brief` in the cached system block; cache reads logged.
- #157: dashboard run progress strip from `runs_get`, `strategy_get`, the AI stage and the dosing feed attribute; no integration change.
- #159: a push to testing no longer cancels the previous Validate run.
- 3.12.2 content: #149, #150, #151, #152, #153.

## [[3.12.2] - 2026-10-16](#3-12-2)

Pair: controller 3.12.2

Pair: **controller 3.12.2**. Five changes, #149 to #153. Released by the owner's decision without a staging soak (approved rehearsal).

### 🌱 In plain English

- **Compare runs now has charts.** For each grow week the two runs sit on top of each other: moisture peak and trough, dryback, pore EC, water per plant and shots a day.
- **A hold switch that does not exist now says so by name.** The room still stops watering (fail-closed), and the controller names every missing hold entity at start and on a repairs card (CS-210), so you can fix the list.
- **The AI assistant's "why" answers now name the kill switch and the holds** when they are the reason a zone did not water.
- **If you copied an old judges list, the controller tells you once** which newer judges it leaves out, and suggests "all".
- The docs describe the current AI assistant and the move to 3.12.0 or later.

**🔧 Technical notes**

- #152: Compare runs charts per grow week (VWC peak/trough, dryback from daily peak and trough, pore EC high/low, water per plant per day, shots per day from the shots-today counter), all from Home Assistant history; Dryback and Shots a day also become table columns. Stacks at 390 px; axe clean in light and dark.
- #149: a hold entity missing from Home Assistant is named in the shot block message, a start-up warning and a repairs card (CS-210), proven in tests_ha. Behaviour stays fail-closed.
- #151: the why-evidence includes the kill switch and every hold entity with its state.
- #150: one log line when `jev_judges` omits newer judges; "all" already selects every judge.
- #153: docs/JEV.md describes the current assistant (TypeSafe and Cloudflare marked deprecated); MOVING_FROM_JEV says 3.12.0.
- No state-file or option change; an old install loads unchanged.

## [[3.12.1] - 2026-10-16](#3-12-1)

Pair: controller 3.12.1

Pair: **controller 3.12.1**. One change, #147. Released by the owner's decision without a staging soak (approved rehearsal).

### 🌱 In plain English

- **The AI assistant can now use an Anthropic-compatible router.** Set the new `anthropic_base_url` option to the router's address, and the controller sends the AI questions there with your router key. Leave it blank to use Anthropic directly, as before. If the router does not answer, the controller uses its rules for that decision.

**🔧 Technical notes**

- New add-on option `anthropic_base_url` (blank by default; an old options file loads unchanged). With it set, a requests-based client reads the first JSON object of each reply (a trailing `data: [DONE]` line is ignored) and maps a suffixed tool name back only to a tool the request offered. `anthropic_model` then also accepts `<prefix>/<model>`, billed at that model's price row. Failure classes, the budget and key handling are unchanged.

## [[3.12.0] - 2026-10-16](#3-12-0)

Pair: controller 3.12.0

Pair: **controller 3.12.0**. This release brings the Jev edition (3.11.1) into this repository and adds 13 groups of changes on top of it. It was released by the owner's decision without a staging soak (approved rehearsal).

### 🌱 In plain English

**Read these first. They change when water flows on rooms that update:**

- **P3 rescue shots now wait 10 minutes between shots in every zone.** Set "P3 time between shots" to 0 to go back to no gap.
- **If a room's settings cannot be read since start, the zones that need them for shot size wait (CS-402) instead of watering on built-in values.**
- **If a hold switch you set up becomes unavailable, its room now stops watering until it is back. Before, an unavailable hold switch was ignored.**
- **On the first start after this update, each room holds watering for about 3 minutes while the controller checks that no second controller drives it.**
- **The EC PID option is gone. Rooms that had it on now use the normal step rule for EC.**
- **Auto setpoints no longer moves the rescue floor, keeps the P0 dryback, and moves the field-capacity ladder at most 1 point a grow-day.**
- **On a room that runs a grow plan, a dead or flat EC probe no longer keeps every zone on yesterday's targets. At lights-on every zone with a working moisture probe moves to the new day.**
- **When a zone's moisture probe comes back during the day, the zone carries on from where its day is. It does not start the morning ramp again.**

What else you get:

- **It works on any Home Assistant.** No built-in room names, entity ids or site numbers. One table of defaults and units. A valve, pump or main line can be a switch, an input_boolean or a valve.
- **Run the controller as an app or as a plain Docker container** (Home Assistant Container and Core).
- **An optional AI assistant (Claude Haiku).** It decides only inside the safety limits, and the controller goes back to its rules when the AI is off, out of budget or not available. Put the Anthropic key and a daily budget in the app options.
- **The controller checks that each room runs its grow plan** and tells you in one line, with a CS-713 card when it does not.
- **Safety:** a max-run latch, fail-closed holds, a settled P1 rise, guarded probe combining, a test shot button, and a stopped heartbeat.
- **Dosing and the reservoir:** dosing runs on the main loop, tank level and reservoir minimum, feed schedule by week, and two-point probe calibration.
- **The setup wizard checks itself**, and the dashboard has a first-run tour, a stale-data banner and a targets editor.
- **All text is in Simplified Technical English**, and every setting has an "About" tip that tells you what it does, what it changes and what to check.
- **Moving from the Jev edition:** follow docs/MOVING_FROM_JEV.md once.

**🔧 Technical notes**

- Intake: Jev 3.11.1 fast-forwarded (#132), with a guard so a 2.25.1 install that only set Cloudflare keys does not start the Jev brain.
- Families: groundwork (#133), any Home Assistant (#134), doctrine pack and substrate per room (#135), standalone Docker (#136), safety ports (#137), engine ports and substrate profiles (#138), change log, read-back, overlays, re-notify and shot-landed check (#139), notifications (#140), dosing and reservoir (#141), setup wizard and dashboard (#142), Claude Haiku brain (#143), brain judges and plan adherence (#144), ASD-STE100 text and tooltips (#145).
- New codes: CS-312 (max-run latch), CS-313 (undrivable actuator), CS-503 (probe unit), CS-609 (second controller), CS-610, CS-611, CS-708 to CS-713.
- Image: requests and the Anthropic SDK closure installed from hashed requirement files; armv7 skips the SDK and runs rules.
- Every family passed the full CI (lint, integration, controller, real Home Assistant on the newest and oldest supported versions, container smoke, dashboard browser workflows).

## [[3.11.1] - 2026-10-06](#3-11-1)

Pair: controller 3.11.1

Pair: **controller 3.11.1**. Class **C3**. One pull request, #42, the fix to 3.11.0's first live afternoon on Room A (6 October 2026), which left zone 2 with its re-water point under its rescue floor. The controller changes; the integration carries the version and What's new.

### 🌱 In plain English

- **The four moisture setpoints stay in line.** When field capacity falls, the re-water point and the rescue floor fall with it. A value under the lowest a setting allows is now held at that lowest value, and the four are written in an order that keeps them in line after every write. On 6 October zone 2's new rescue floor, 17.9, was refused under its minimum of 20 after its re-water point had already moved to 25.4.
- **Nothing moves until a lights-on has kept the day before's highest reading.** Updated after lights-on, 3.11.0 used the day in progress instead, and zone 2's field capacity followed a ramp that had stopped at its shot limit down from 67.5 to 51.4.

**🔧 Technical notes**

- Controller (#42): `_follow_fc` clamps each value to `setpoint_supervisor.BOUNDS` and to the per-zone number's own `min` and `max`; it writes top-down when field capacity rises and bottom-up when it falls, and the first refused write stops the rest. `_auto_write` returns whether it wrote and records its de-duplication only on success. `_vwc_maxima` has no stand-in: `(None, None)` until `peak_prev` exists.
- No state-file, option or entity change.

## [[3.11.0] - 2026-10-06](#3-11-0)

Pair: controller 3.11.0

Pair: **controller 3.11.0**. Class **C3**. One pull request, #40, the owner's rules for a zone's P1 and its setpoints (6 October 2026): the P1 target is the highest reading of the grow-day before, field capacity rises with the sensor and what sits under it moves with it, the 2-points-a-day limit is gone, and P1 ends when a shot at the target no longer raises the zone's highest reading. The engine and the controller change; the integration carries the version and What's new.

### 🌱 In plain English

- **Tomorrow's P1 target is today's highest reading.** At lights-on the controller keeps the grow-day's highest VWC reading, and that is the new day's P1 target, however far it is from the old one. The limit of 2 points a day is gone.
- **Field capacity rises with the sensor.** It is the higher of today's and yesterday's highest readings, so it goes up the same day the probe reads higher. When it moves by a point or more, the P2 re-water point and the rescue floor move by the same points.
- **P1 ends when the slab stops taking water.** The ramp runs to the P1 target, then gives one more shot. If that shot did not raise the zone's highest reading by half a point, the slab is full and the zone goes to P2. If it did, another shot follows, until one does not. The maximum P1 shots still cap it.
- **Pore EC no longer holds P1 open.** P2's dilute shots correct a high pore EC.

**🔧 Technical notes**

- Engine (#40, both copies): `ZoneSnapshot.p1_reached`, `p1_peak_before_shot`, `p1_extra_shots`; `P1_RISE_PTS` 0.5. P1 fires `p1_ramp` below the ceiling and `p1_extra` (cap-exempt) at or over it, and hands over at the maximum shots, or once an extra shot is in, a full P1 interval has passed and the peak rose by less than 0.5 ("P1 full: ..."). The EC exits, the P1 flush and the budget-spent exit are gone.
- Controller (#40): zone key `peak_prev` (kept at lights-on); `_vwc_maxima` / `_follow_fc` write field capacity = max(today's peak, `peak_prev`) and the P1 target = `peak_prev`, and move the re-water threshold and rescue floor with field capacity. The learner writes neither, its 2-point limit (`DAY_RISE_PTS`, `day_ceiling`, `p1_entry`, `stale_target`) and 3.10.0's settled-reading tracker are removed, and so is `p1_ec_gate`. The P1 rule's state (`p1_reached`, `p1_peak_before`, `p1_extra`) is latched, counted and persisted. A zone on the blind path keeps its peak at lights-on too. Until the first lights-on after the update, the grow-day in progress stands in for the day before only once its P1 is over.
- State file: four new zone keys; a 3.10.0 file loads with them empty and its retired learner keys are dropped; 3.10.0 ignores the new keys after a rollback.

## [[3.10.0] - 2026-10-06](#3-10-0)

Pair: controller 3.10.0

Pair: **controller 3.10.0**. Class **C3**. Two pull requests, both asked for by the owner for Room A zone 2 on 5 and 6 October 2026: #37, a zone with high pore EC is flushed whenever its table drains, whatever its moisture reading; and #38, field capacity follows the sensor. Both change the controller only; the integration carries the version, the error-code text and What's new.

### 🌱 In plain English

- **High EC gets flushed.** On 5 October Room A zone 2's pore EC sat at 7 against a target of 4.5 and its plants were yellowing, yet it got no flush all day. Every flush needed the moisture reading under field capacity minus 2, and zone 2's probe reads far above its field capacity. Now a high-EC zone is flushed whenever the feed is weaker than its slab and its table is draining. The daily water limit and the 45 minutes each flush gets to drain still apply. A table that is not draining (the "moisture rising with no water" alert) now holds flushes as well as the daily minimum.
- **Field capacity follows the probe.** It is now the zone's highest reading 20 minutes or more after a shot, today or yesterday, and never lower than the P1 target + 2. When the probe reads higher, field capacity goes up the same day. Until now it was only ever the P1 target + 2, which may rise 2 points a day, so zone 2 sat at 63 to 68 after every shot against a field capacity of 54. It also follows on a zone watering without its probe.
- **Everything under field capacity moves with it.** When the probe moves field capacity by a point or more, the P1 target, the P2 re-water point and the rescue (P3 emergency) floor move by the same points, so the dryback under full stays the size it was set. P0 and the overnight dryback are percentages of the day's peak and follow by themselves. Zone 2: field capacity 54 to 68, P1 target 52 to 65.5, re-water 27.5 to 41.5, rescue 20 to 34.

**🔧 Technical notes**

- Engine (#37, both copies): `ZoneSnapshot.backed_up`. The capped EC corrections (`p0_ec_flush`, `p1_flush`, `p2_dilute`) need dilutive feed and `not backed_up`; the cap-exempt `flush_high_ec` and `p2_rescue` keep `vwc < field_capacity - 2`. A dilutive zone that reads full falls through to the capped rules instead of the early BLOCK return. In P2, a rescue that cannot fire no longer skips the dilute.
- Controller (#37): the CS-310 hold also passes `backed_up=True`. Catalog text for CS-310 and CS-311, ERROR_CODES.md and the dashboard bundles follow.
- Controller (#38): `auto_setpoints.track_full` (learner keys `full_date`, `full_day`, `full_prev`) is fed every loop from the probe, before Jev's probe judge can set it aside. `field_capacity(target, seen)` is `max(target + 2, seen)` within 40 to 90, written by the learner pass and, for blind zones, by `_follow_fc`. Until anything is tracked, the last `plateau_hist` value seeds it. When `seen` (not the target + 2 floor) moves field capacity by `FC_SHIFT_MIN` (1 point) or more, the P1 target, `p2_vwc_threshold` and `p3_emergency_vwc_threshold` move by the same points (never above the learner's peak, nor within 2 of field capacity; nothing but field capacity when the ladder is or would be inverted), and the learner's `day_ceiling` and `p1_entry` move with the target so its brake does not pull it back. Jev's bridge re-centres the Setpoints judge's range on the moved threshold.
- State file: three new learner keys. A 3.9.0 file loads with them empty; 3.9.0 ignores them after a rollback.

## [[3.9.0] - 2026-10-05](#3-9-0)

Pair: controller 3.9.0

Pair: **controller 3.9.0**. Class **C3**. Eight pull requests, merged into `testing` together at the owner's request ("complete and merge all the pull requests"): three controller changes (#31 each zone's plateau and nights, #32 Jev's availability, #34 one writer per setpoint with a ratchet guard), one integration change (#35 stock tanks drawn from what their pumps run), three dashboard changes (#28, #29, #33) and the dryback planner's design (#30). That is more than the one C2 or C3 change a candidate normally carries (docs/RELEASING.md, step 3); the owner chose to release them together, and the release audit records it.

### 🌱 In plain English

- **Zones get their P1 targets kept up to date again.** Since 26 September, in a room where Jev manages setpoints, nothing moved a zone's P1 target or field capacity. Room A zone 2's probe read 15 to 20 points higher after the 29 September sump flood, so every morning started above its ramp target and P1 never ramped. Now Jev keeps the P2 shot size and re-water point, and the Auto setpoints learner keeps the P1 target and field capacity, with two brakes so it cannot ratchet a zone up again: a target rises at most 2 points a day, and only after that day's ramp reached it; a zone that starts its ramp above its target goes to 2 points over where the ramp began, never straight to an old learned peak.
- **Jev is there more often.** When TypeSafe fails, Jev retries, then asks through Cloudflare (once its account and token are set in the app). An unsure answer gets a second look with more evidence. A judge whose calls keep not working out for a zone acts there only when it is sure twice in a row. The daily budget defaults to 5,000 calls, with a warning at 80 %; a box keeps the budget it already has until its owner changes it.
- **Each zone remembers its plateau and its nights.** Every morning the app's log and the activity feed say how each zone landed overnight: where P1 ended, how fast it dried at night and how far it dropped in P0. No watering decision changes; the dryback planner will use this.
- **Stock tanks go down when their pump runs.** A stock tank linked to a dosing pump now loses what that pump actually pumped, whoever started it. What was pumped before the update was never taken off, so after updating, read each linked tank's level off its side once and save it with **Set level**.
- **Charts.** Equipment › Tank & pump has a chart of the tank's level over 24 hours, 3 days or 7 days with every shot, pump run and fill on it. On the zone chart each key entry switches its layer on and off, a cursor bar shows where a reading is taken, the reading sits under the chart, and yesterday's line is drawn even on a day the room was switched off for a while.

**🔧 Technical notes**

- Controller (#34): `auto_setpoints` ratchet guard (learner keys `day_ceiling`, `p1_entry`, `stale_target`; a stale target goes to `min(p1_entry + 2, learned peak + 1)`, otherwise up at most 2 over the grow-day's ceiling after a `reached` or believed `plateau` outcome) and the lever split (`jev_bridge.judge_levers`: with the Setpoints judge running, the learner writes the P1 target, field capacity and rescue floor, never `p2_shot_size` or `p2_vwc_threshold`). `zone_N_auto_setpoints.managed_by` is now a map of entity to writer.
- Controller (#31): per-zone plateau and night history in the state file, logged each morning as "Zn night: landed ...". `decide()` and its inputs are unchanged. A history that loads with wrong types is dropped, and the hooks never block a decision, a shot count or a phase change.
- Controller (#32): Jev routes (TypeSafe direct, then Cloudflare `/ai/run`), 3 tries within 60 s per route on a 429, a 5xx or a timeout; questions older than 5 minutes dropped unasked; second looks under 0.7 or on split phrasings (up to 2); the stricter gate after 3 calls in a row that did not work out (two agreeing calls at 0.8), read from the ledger; CS-706 (budget at 80 %) and CS-707 (a judge on the stricter gate). New options `jev_budget_warn_pct`, `jev_retries`, `jev_reask_max`, `jev_unsure_below`, `jev_strict_after`, `jev_strict_prob`; `jev_daily_calls` defaults to 5000.
- Integration (#35): `StockStore` follows each linked pump's power and dosing entities and draws seconds run x calibrated flow once per run (key `pump:<id>:<start>`); `stock_draw` leaves pump-linked tanks alone (new `linked` answer field); a fill that draws no tank is no longer logged.
- Dashboard (#28, #29, #33): `dayTrace` keeps a day the room was switched off during (marked `roomOff`); chart layer toggles, cursor bar, readout line and stored chart preferences; the Tank & pump level chart (`tank-level.tsx`), with `controller.timeline` allowing the selected room's tank and pump entities. Bundles rebuilt.
- Docs (#30): the dryback planner and Jev line-steering design, its roadmap, and the plans for steps 1 and 2.
- State file: three history keys (#31) and three learner keys (#34). A 3.8.0 file loads with all of them empty; 3.8.0 ignores them after a rollback (the history is lost, nothing else).

## [[3.8.0] - 2026-09-30](#3-8-0)

Pair: controller 3.8.0

Pair: **controller 3.8.0**. Class **C3**. One pull request: #26, critical alerts for tables backing up: moisture rising with no water (CS-310) and a sump pump standing still (CS-311). Backtested on Room A's readings from 22 to 30 September; checked by the controller, lean, engine, real Home Assistant, dashboard and browser suites, with the new rules mutation-checked.

### 🌱 In plain English

- **A table that isn't draining is now an emergency.** When a zone's moisture keeps rising while no water goes in (its valve shut, by the controller and by hand), the slabs are sitting in runoff: the table, its drain or the sump is backed up. Crop Steering says so straight away, as a critical alert, and stops that zone's daily minimum from adding more water until it drains.
- **A sump pump that stops is an emergency too.** Name the sump pump's power sensor in the controller app (`sump_power_sensor`) and Crop Steering warns when it hasn't run for three hours while the room is on. On 28 September Room A's sump pump stopped for 41 hours; Zone 2 filled from below the next day.

**🔧 Technical notes**

- Controller (#26): `_watch_backup` (CS-310: a 3-point rise within 60 minutes, the valve shut for that time and 45 minutes before, waits 45 minutes after a start; holds `min_daily_volume` while flagged, clears an hour after the rise stops) and `_watch_sump` (CS-311: no reading over 20 W in `ha_history` for `sump_silent_hours`, read every 5 minutes, nothing on no evidence); new options `sump_power_sensor`, `sump_silent_hours`.
- docs/error-codes.json: CS-310 and CS-311 (critical); both in the notification catalog's emergency and hardware kinds; ERROR_CODES.md regenerated; dashboard bundles rebuilt.

## [[3.7.0] - 2026-09-30](#3-7-0)

Pair: controller 3.7.0

Pair: **controller 3.7.0**. Class **C3**. One pull request: #24, the minimum water per plant per day spread evenly over the day instead of all at lights-on. Checked by the engine, controller, lean and real Home Assistant suites, with the pacing mutation-checked.

### 🌱 In plain English

- **Every plant gets its minimum, whatever its probe says.** A zone whose probe sits in a wet spot reads wetter than its slab and was barely watered (one zone got 0.41 L a plant in a day). The daily minimum per plant (the zone's "min daily mL per plant" helper) now waters it through the day.
- **Spread out, not dumped.** It waits for the morning dryback, catches up during the ramp if the zone is behind, and spaces the rest through the day so the whole minimum is in three hours before lights-off, before the evening dryback. Every shot the zone gets counts toward it.
- Nothing else changes: the drown ceiling and the 10-minute spacing still hold it back, and the daily limit can't block it.

**🔧 Technical notes**

- Engine (#24, both copies): `floor_share` (a straight line from lights-on to `min_floor_finish_h`, 3 h, before lights-off); the floor never fires in P0 and is due only while `daily_vol` is under `min_daily_volume * floor_share`; `ZoneSnapshot.hours_since_lights_on` (None = front-stacked as before); `ZoneParams.min_floor_finish_h` (bounded 0 to 12).
- Controller (#24): passes `hours_since_lights_on` in the snapshot.
- Tests: `crop-steering-engine/tests/test_min_daily_pacing.py` (the line, behind/ahead, never in P0, catch-up after the finish, a simulated day with a probe stuck wet: 5 evenly spaced shots, all in by 19:00).

## [[3.6.0] - 2026-09-30](#3-6-0)

Pair: controller 3.6.0

Pair: **controller 3.6.0**. Class **C3**. One pull request: #22, who gets which phone push (a checkbox per phone for each kind of alert, the rooms it covers, the pushes routed by the integration), with three new pushes: no watering for a while, a zone changing phase, Jev moving a setting. An adversarial review of the alert path found two ways a critical push could be lost and a loop that could stall; all fixed with tests (11 of 11 mutation-checked). Checked by the lean, controller, real Home Assistant, dashboard and browser suites.

### 🌱 In plain English

- **Choose who gets what.** Settings & help › Notifications lists the site's phones with a checkbox for each kind of alert: emergencies, hardware lockouts, sensors and drift, watering stopped, phase changes, stock tanks, dosing, Jev, setup. Each phone can cover every room or only some, and has a **Send a test** button. Staff can change their own phone's boxes; an administrator can change everyone's.
- **New things to be told about.** A room that is on, with its lights on and a zone in P1 or P2, that has watered nothing for 3 hours (you pick 1 to 12); a zone moving from one phase to the next; Jev moving a zone's shot size or re-water point.
- **Nothing is lost.** Until a phone is set up, every push goes where it always did. If the routing can't be reached, a push goes there too. An emergency that no phone's row covers for its room goes to every phone that gets emergencies, or to the phone set in the controller app when none does. Tapping any push opens Crop Steering.

**🔧 Technical notes**

- Integration (#22): `notify_catalog.py` (the nine kinds; code → kinds, unknown codes by severity) and `notify_api.py` (one site-wide store `crop_steering.notify`; services `notify_get`, `notify_save`, `notify_test`, `notify`; `sensor.crop_steering_notify_config`; phones listed from mobile_app registrations with their owner; each phone sent to at once with an 8 s limit; `data.tag`, the panel link and, for emergencies, high-priority keys on mobile_app services only; an emergency no row covers goes to every phone that takes emergencies; Repairs cards CS-601 to CS-608 pushed when raised; a store that can't be read is logged, and a kind a newer version saved is dropped rather than the whole setup).
- Controller (#22): `_alert` routes through `crop_steering.notify` when a phone is set up and falls back to `notify_service` when the call fails or an emergency reaches nobody (never both); the setup sensor is read by the loop, never waited on by an alert; phase changes and Jev's setpoint moves queued, one push per room and kind, sent after the loop's shots within 10 s; CS-209 idle watchdog (clock rebuilt after a restart); a pump whose turn_on errored (CS-302) and a batch's mix pump that did not start (CS-805) are switched off before their alert.
- Dashboard (#22): Settings & help › Notifications (grid on a computer, a card per phone on a phone, what each kind covers, add a phone, review before save, rooms nobody gets emergencies for).
- docs/NOTIFICATIONS.md is the contract.

## [[3.5.0] - 2026-09-28](#3-5-0)

Pair: controller 3.5.0

Pair: **controller 3.5.0**. Class **C3**. One pull request: #19, batch-tank dosing (a page per room's tank, single doses and whole batches through the controller, stock tanks drawn by what was dosed). Not run on hardware; checked by the lean, controller, real Home Assistant, dashboard and browser suites.

### 🌱 In plain English

- **Dosing, from the Crop Steering page.** Each room has Equipment › Dosing for its own batch tank: dose any nutrient pump by the mL, or make a whole batch (fill until the float says full, mix, dose each nutrient in the recipe's order, mix again, record the fill). Watering is held while a batch runs, and Stop works at any point.
- **The pumps' own boards still time every dose.** Crop Steering checks each dose started and finished, cuts a pump's power if it runs past its time, refuses an uncalibrated pump or a dose longer than 20 minutes, and after a restart switches all dosing hardware off and tells you, never carrying on by itself.
- **Stock tanks that follow the doses.** Link each pump to a stock tank and every dose takes what was actually dosed off its level; each pump shows a small bottle with the litres and batches left.
- Nothing happens until a room's dosing is set up, and your existing Home Assistant dosing scripts are untouched.

**🔧 Technical notes**

- Integration (#19): `dosing.py`/`dosing_api.py` (store `crop_steering.dosing.<entry_id>`; services `dosing_get`, `dosing_save`, `dosing_request`; `sensor.crop_steering_<prefix>dosing_config`); `stock_draw` (idempotent), linked tanks skipped by the fill rule and by a hand-recorded batch; the stock sensor's tanks gain `id` and `pump`.
- Controller (#19): `dosing_runner.py` (one thread; write-ahead `/data/dosing_state.json`; stuck hardware held and re-sent until it reads off; restart recovery; SIGTERM switches irrigation off first; alerts CS-801 to CS-807; `sensor.crop_steering_<prefix>dosing`); `_blocked` holds a batch's rooms; app `timeout: 30`.
- Dashboard (#19): Equipment › Dosing (pump drawings, dose form, live telemetry, batch steps and recipe, stock strip and bottles, history, setup); one stock colour rule on both pages.
- docs/DOSING.md is the contract; an adversarial safety review's 24 findings are fixed with tests.

## [[3.4.2] - 2026-09-28](#3-4-2)

Pair: controller 3.4.2

Pair: **controller 3.4.2**. Class **C3**. One pull request: #18, a pump plug that reports OFF late is sent OFF again, not held. Not run on hardware; checked by the controller and lean suites.

### 🌱 In plain English

- **No more false "hardware fault" from a slow pump plug.** On 28 September Room A stopped watering at 11:18 because the pump's Zigbee plug took about 10 seconds to report it had switched off, and the controller only waited 6. Now anything that has not reported OFF is told again and given 10 more seconds before the room is held. A valve that is really stuck open is still caught within 16 seconds.

**🔧 Technical notes**

- Controller (#18): `_confirm_switches` re-sends `switch.turn_off` to whatever does not read `off` after the first 6 s window and reads back for `CONFIRM_RETRY_S` (10 s) more; every close path (normal close, a shot cut short, error cleanup, an interrupted shot's reconcile) uses it. Two new regression tests reproduce the live fault.

## [[3.4.1] - 2026-09-28](#3-4-1)

Pair: controller 3.4.1

Pair: **controller 3.4.1**. Class **C1**. Two pull requests: #15 (C1), the demo shows Jev's re-water range as the controller bands it; #16 (C0), the README explains the Jev edition from scratch, with a screenshot of every page where Jev shows. Nothing the controller reads changes. Not run on hardware; checked by the dashboard and browser suites.

### 🌱 In plain English

- **A front page for newcomers.** The README now starts from zero: what crop steering is and the words it uses, how the controller and Jev work together, the eleven questions Jev answers, what it can never do, every dashboard page that shows Jev with a current screenshot, letting Jev move setpoints and every way to take them back, and what it costs. Its install buttons, and the install guide's, add this repository instead of the original.
- **The demo's Jev range is the real one.** The demo showed Jev allowed 1.5 points either side of the re-water point. The controller allows 2 under and 1 over, and the demo now shows that.

**🔧 Technical notes**

- Dashboard (#15): the demo's `setpoints.range.p2_vwc_threshold` is `[59, 62]`, what `setpoints.band()` gives for its numbers; the browser checks follow; bundles rebuilt.
- Docs (#16): README rewritten with nine new `img/jev-*.png`, and the water use, stock tanks and setup screenshots retaken; the `docs/INSTALL.md` buttons point at this repository; the app's `DOCS.md` describes Jev and its options; `docs/JEV.md` names the 3.4.0 pages.

## [[3.4.0] - 2026-09-28](#3-4-0)

Pair: controller 3.4.0

Pair: **controller 3.4.0**. Class **C3**. Two pull requests: #12 (C3), Jev's data for the simplified dashboard and no Pore EC question while EC stacking is off; #13 (C1), the dashboard rebuilt around what each page is for, from 11 pages to 5. Not run on hardware; checked by the lean, controller, dashboard and browser suites.

### 🌱 In plain English

- **A dashboard built around what you want to know.** The menu is now Today, Plan, History, Equipment and Settings & help, plus a page per zone. Today fits one screen: the room in one line, then a card per zone that is flagged only when it needs you, with the reason, and what Jev changed. Each zone has its own page with the day chart, its probes, its targets and what Jev thinks of it. Plan is a compact targets table (about a third of its old length) with the flower laid out by stage; History is one timeline of shots, phases, setting changes, alerts and Jev's decisions; Equipment holds the probes, stock tanks, tank and pump, and setup. Every feature is still there.
- **Less noise from Jev.** It no longer asks about pore EC while the room's EC stacking is off, where its answers changed nothing; each check on an earlier decision shows as a ✓ or ✗ on that decision; and the day's call count no longer resets when the controller restarts.

**🔧 Technical notes**

- Controller (#12): the salt judge needs `stacking_on`; journal outcome entries carry `of` (the decision's time) and the action in words; `/data/jev_usage.json` keeps the day's calls, tokens and errors; each zone's Jev sensor publishes `setpoints` (the operator's values, Jev's range, current values, last move, pause, managed).
- Dashboard (#13): five-entry menu with legacy redirects, room status and safety switches in the header, `today` and `zone` pages, History (timeline merging the activity log and Jev's journal), Plan (targets table with Jev's ranges, schedule by stage), Equipment (probe health, stock headline, tank and pump, setup), Settings & help; attention rules, timeline merge, redirects and chart scaling tested; bundles rebuilt.

## [[3.3.0] - 2026-09-28](#3-3-0)

Pair: controller 3.3.0

Pair: **controller 3.3.0**. Class **C3**. Two pull requests: #7 (C3), a published grow guide as a second cited source in Jev's doctrine; #10 (C1), the dashboard's grow day redrawn in the grow guide's chart language and Jev's decisions shown live. Not run on hardware; checked by the lean, controller, dashboard and browser suites and against the real Jev (17 of 17).

### 🌱 In plain English

- **A grow day you can read at a glance.** "Today's grow day" follows the grow guide's charts: the stage and its targets on one line at the top, then a card per zone with its numbers (moisture, pore EC against the stage's range, water today, shots, what comes next, last night's dryback against the target) and one clean chart: the light cycle, the phases P0-P3, field capacity, the runoff zone for the zone's steering, moisture with every shot on the line, pore EC with the stage's range, and Jev's decisions marked where they happened.
- **Jev's decisions, live.** The latest five sit under the charts and every decision is on the Activity page: what Jev answered and how sure it was, what it asked for, and what the controller did (acted, refused and why, advice, nothing), with how earlier actions turned out.
- **Jev knows a published grow guide too.** Alongside your own GrowLabs pages, every question now carries a published grow guide: when the first shot should come after lights-on, why the ramp uses small shots, how bigger or smaller maintenance shots move root-zone EC, the grow guide's drybacks and targets for each stage, runoff targets, where the probe goes and how to check it against caught runoff. Your pages still come first, and where the two differ your stage arc wins.

**🔧 Technical notes**

- `jev/doctrine.py` (#7): a published grow guide (metric, Precision Irrigation Strategy, pp. 33-41) as a second cited source, 24 rules after the owner's in ramp, dryback, maintenance, EC, stage, probe, closed loop and slab (123 in all); the last dryback rule says the owner's arc leads where the two differ.
- Dashboard (#10): a grow-guide-style SVG chart per zone (light band, phase columns and badges, FC line, runoff zone by steering, maintenance band over P1/P2, rescue floor over P3, VWC with shot dots and projection, pore EC on a right axis with the stage band, Jev markers), a numbers row per zone, a stage line from the room's Jev sensor, a y-axis fitted to the zone, 7-8 legend items; `lib/jev-log.ts` and `components/jev-log.tsx` (compact list on the Overview, full panel with filters on Activity), `jev`/`jev_log` among the watched sensors so a first post shows live. Bundles rebuilt (three identical copies).

## [[3.2.0] - 2026-09-28](#3-2-0)

Pair: controller 3.2.0

Pair: **controller 3.2.0**. Class **C3**. One pull request (#6): the Setpoints judge. Once a night Jev may move one of a zone's own maintenance settings one notch, inside a range around the operator's value, while the room's Auto setpoints switch is on. The integration's code is unchanged; its version moves with the pair. Not run on hardware; checked by the lean and controller suites and against the real Jev (17 of 17).

### 🌱 In plain English

- **Jev adjusts tomorrow's maintenance watering.** Once a night, after lights-off, it looks at how much each zone drank against the rest of the room, how its shots held, and where its root-zone EC sits against today's stage, and picks one small step (slightly smaller or bigger shots, or re-watering slightly later or sooner) or leaves it alone. Only while the room's Auto setpoints switch is on.
- **Your values stay in charge.** It only moves the zone's own P2 shot size and re-water threshold, 0.5 at a time, never more than a small range from what you set (and never a shot under 3 %). Change a value yourself and yours becomes the new centre. If a zone needs a rescue shot after a change, the change is undone and Jev leaves that zone alone for two days.
- **The old Auto Setpoints learner stands aside** while Jev runs, so a zone never has two hands on its settings.

**🔧 Technical notes**

- `jev/judges/setpoints.py` (P3, the first 3 h after lights-off, once a grow-day): smaller_shots / bigger_shots / later_rewater / sooner_rewater / keep. Evidence includes water per plant against the room's median zone and pore EC against the stage's range, with code stating which way each calls for (the grow guide: EC rises with smaller shots); it acts when both phrasings pick the same lever and that lever, or its direction, holds 60 %.
- Envelope `setpoint`: the room switch on and no grow plan, not paused, one change a grow-day, one 0.5 notch, inside `band()` (shot ±1 % of the operator's value, never under 3 %; threshold 2 under to 1 over, at least 4 above the rescue floor and 2 under the ramp ceiling), and the value unchanged since Jev was asked.
- `jev/setpoint_memory.py` (`/data/jev_setpoints.json`): the operator's values, Jev's writes, a pending write, the last move, a pause. `jev_bridge.py`: `setpoint_view` (read only in P3), `apply_setpoint`, `guard_setpoints` (a P3 rescue shot within 30 h: put back, pause 48 h, CS-404). `controller._auto_tick`: the base learner never writes nor asks its old per-hour judge while the judge runs.

## [[3.1.0] - 2026-09-28](#3-1-0)

Pair: controller 3.1.0

Pair: **controller 3.1.0**. Class **C3**. One pull request (#5): every decision Jev makes is journalled with what the controller did with it, and published for the dashboard's live Jev log. The integration's code is unchanged; its version moves with the pair. Not run on hardware; checked by the lean and controller suites.

### 🌱 In plain English

- **Follow Jev live.** Every answer Jev gives is recorded with what the controller did about it: acted, refused (and why), nothing to do, or waiting, plus how earlier actions turned out and whether an alert went to the phone or stayed a card. The newest 30 per room are published for the dashboard's Jev log, and the room's Jev sensor carries today's stage of flower as numbers for the day chart.

**🔧 Technical notes**

- `jev/journal.py`: `/data/jev_journal.jsonl` (the last 400 decisions, kept across restarts). `jev/brain.py` journals each new answer once with its result on that pass, a later admission of a standing answer, and each outcome; journalling never raises into the pass.
- `jev_bridge.py`: `sensor.crop_steering_<prefix>jev_log` (state: the newest decision in one line; `entries`: the last 30, newest first, free text capped under the recorder's 16 KB), `stage` on `sensor.crop_steering_<prefix>jev` (day of flower, stage, steering, pore EC range, dryback points, runoff %), and an alert's push-or-card call journalled whenever it changes.

## [[3.0.1] - 2026-09-27](#3-0-1)

Pair: controller 3.0.1

Pair: **controller 3.0.1**. Class **C3**. One pull request (#3): Jev's zones judge compares each zone's water per plant with the room's median zone, itself included, so only the odd zone out gets the CS-702 alert. Found on the work HA on 3.0.0's first live evening. An advice-only judge: no water decision changes. The integration's code is unchanged; its version moves with the pair. Not run on hardware; checked by the lean and controller suites (four new tests that fail on 3.0.0) and against the real Jev (14 of 14).

### 🌱 In plain English

- **Only the odd zone out is flagged.** The check of each zone's water per plant compared every zone with its thirstiest neighbour, so when one zone had far more water than the rest, the others were flagged too and told of a valve or dripper fault they did not have (Room A, 27 Sep: zone 1 had 2477 mL a plant, zones 2 and 3 about 1000, and all three were flagged). Each zone is now compared with the room's middle zone.

**🔧 Technical notes**

- `jev/judges/zones.py` `_ratio`: the comparison is `statistics.median` of every zone in the room, this zone included (it was `others[len(others) // 2]`, the larger of two sorted siblings). A room of two zones compares each zone with the other, as before. The evidence (`compared_with`, `share_of_that`) and the CS-702 message say what the zone was compared with.

## [[3.0.0] - 2026-09-27](#3-0-0)

**The Jev edition.** Pair: **controller 3.0.0**. Class **C3**. This repository starts from Crop Steering 2.25.1 and adds TypeSafe Jev, a typed-decision model, as the judge of every decision the engine made with a fixed rule, inside a deterministic envelope. With no Jev key, or Jev off, slow or down, the engine runs exactly as 2.25.1. Not run on hardware; checked by the lean, controller, engine, real-Home-Assistant and browser suites, and against the real Jev (14 of 14 situations answered as an experienced grower would). What it is and what "truly insane and amazing" means here: docs/JEV.md.

### 🌱 In plain English

- **Jev judges ten decisions** the engine used to make by fixed rules: when the morning ramp starts and when it is done, why pore EC moved and how to steer it, when the day's watering stops, whether a probe tells the truth, whether each shot landed, whether an overnight low is a probe fault, why one zone drinks differently, whether the zone is on the stage's arc for today's day of flower, and which alerts buzz a phone.
- **The owner's own crop-steering doctrine** (his GrowLabs wiki: TEROS-12, closed loop, flowering stages, rockwool, one steering law, slab strategy, ripening) is written into every question, with the stage arc for each day of flower.
- **Code keeps everything physical.** Jev never opens a valve, sizes a shot, crosses the daily floor or cap, or stops a rescue. It can bring a phase forward, set a lying probe aside (on the dead-probe path), choose the EC steer's mode and raise advice; each of those passes a code check first. A zone steered vegetative is never stopped early.
- **A council, not a coin toss:** every question is asked in two phrasings, and Jev acts only when both lead to the same action.
- **Everything is shown:** a Jev sensor per zone and per room, and a log of what acted and how it turned out.
- **Two ways to reach Jev:** a TypeSafe API key, or a Cloudflare Workers AI account and token.

**🔧 Technical notes**

- **Runtime (`addons/f2_control/f2_control/jev/`):** `client` (a background asker with a daily budget; TypeSafe direct or Cloudflare), `council` (two phrasings, `firm_in` action groups), `context`, `envelope`, `ledger` (`/data/jev_ledger.jsonl`), `brain` (decides once per answer, re-admits the standing directive each pass), `triage`, `doctrine` (99 cited rules, the slab-guide stage arc). Judges: dawn, ramp, salt, dusk, probe, shot, night, zones, stage. `jev_bridge.py` feeds the controller's pass; the controller calls it in `_loop_room`, `_advance_shot_counters` and `_alert`, each inside a try that leaves the base engine's decision.
- **New alerts:** CS-701 water not reaching a zone, CS-702 a zone's water per plant out of line, CS-703 an overnight low that looks like a probe fault, CS-704 a probe Jev set aside, CS-705 a zone off its stage's arc.
- **Options:** `typesafe_api_key`, `jev_enabled`, `jev_judges`, `jev_daily_calls`, `jev_flower_start`, `jev_flower_days` (with the existing `cf_*`). Sensors: `sensor.crop_steering_<prefix>zone_N_jev`, `sensor.crop_steering_<prefix>jev`.
- **Upgrade from the original:** a different repository, so the controller app moves once, carrying `/data/state.json` and its options (docs/INSTALL.md); the integration keeps its domain and entity ids. No state-file change.

## [Older releases](#older-releases)

Releases 2.13.0 to 2.25.1 are in [docs/archive/CHANGELOG-2.x.md](https://github.com/JakeTheRabbit/HA-Irrigation-Strategy/blob/v3.18.1/docs/archive/CHANGELOG-2.x.md). Releases before 2.13.0 (up to July 2026) are in the git history of this file.

## Controller

The companion controller app has its own changelog file, kept with [the controller's source](https://github.com/JakeTheRabbit/HA-Irrigation-Strategy/tree/main/addons/f2_control) in the main repository.

### 3.18.1

Pair with integration 3.18.1. **C1.** Documentation, plus two dashboard display fixes in the bundled dashboard (a readable Today strip on phones and a decluttered daily plan card). Controller code, engine, state, options and physical safeguards are unchanged; only path comments in `guards.py` and `zone_history.py` moved with the docs.

### 3.18.0

**⚠️ This is a very large upgrade.** Almost every page of the dashboard is new or rebuilt: timer programs, presets, alert rules, a new History section, feed and pump calibration, pump supply sharing, quiet hours and an Upgrade report.

**If you are running this on a live grow, do NOT just click update** unless you have time for a big configuration session straight after. Watering will not stop, but there is a lot to set up and check.

**Get your AI to configure it from the new docs.** Point your AI assistant (Claude, ChatGPT, or any assistant connected through the Crop Steering MCP server) at the new documentation and have it work through the Upgrade report with you, one line at a time, asking you before each save.

**Your settings come across as far as they can.** The update keeps every zone watering and dosing exactly as before, and carries your current settings into the new features wherever it can do that without guessing. Nothing new that waters or doses is switched on for you.

- **Carried across:** every zone keeps watering by steering; each recipe stage you had edited is copied as a preset; a "before 3.18 update" preset of your current settings is saved as a rollback point; temperatures in °F if Home Assistant uses °F; every notification recipient; your pH up/down pumps keep their role; your earlier setting changes, stock moves, calibrations and dosing records are imported into History; a room with no feed EC probe keeps the old 3.0 mS/cm assumption so it flushes as before.
- **Needs you:** the irrigation supply (each pump's real flow, how many zones it feeds at once, its flow meter: nothing is saved until you save it); the 2-point feed pH and EC probe calibration; the solution and a pump calibration for each nutrient pump; ppm instead of mS/cm if that is how you work; automations that still use a retired entity; grow-plan recipes saved only in your browser (export them). Optional: alert rules, quiet hours, camera, CO2 and light panels, new dashboards.
- Each room's **Upgrade report** lists exactly what was carried and what needs you: Crop Steering → Settings → System status, and a card in Home Assistant's Settings → Repairs.

**What it looks like.** Screenshots of the 3.18 dashboard running on its built-in demo data (sample rooms, not a real facility); the phone view is in the dark theme.

- **Status: all rooms.** Every room and every zone on one page: phase, moisture and water today.
  ![Status, all rooms](https://cropsteering.growlabs.nz/upstream-img/status-all-rooms-desktop-light.png)
- **Zone page.** One zone in full: moisture, EC, what it is waiting for and every shot today.
  ![Zone page](https://cropsteering.growlabs.nz/upstream-img/zone-desktop-light.png)
- **Timeline.** The day's phases and shots for a room on one line, so you can see what happened when.
  ![Irrigation timeline](https://cropsteering.growlabs.nz/upstream-img/irrigation-timeline-desktop-light.png)
- **Timer programs.** Each zone can water by steering (as before), by timer programs, or be off.
  ![Timer programs](https://cropsteering.growlabs.nz/upstream-img/timer-programs-desktop-light.png)
- **Grow plan & presets.** Save a room's or a zone's targets, phase timing and limits, and copy them to other rooms or zones.
  ![Grow plan and presets](https://cropsteering.growlabs.nz/upstream-img/grow-plan-presets-desktop-light.png)
- **History: explorer.** Any reading, any room, any period, on one chart.
  ![History explorer](https://cropsteering.growlabs.nz/upstream-img/history-explorer-desktop-light.png)
- **History: changes.** Every setting change, who made it and when, including what the upgrade carried across.
  ![History of changes](https://cropsteering.growlabs.nz/upstream-img/history-changes-desktop-light.png)
- **Feed & dosing.** Each dosing pump with its solution, its stock tank and its doses.
  ![Feed and dosing](https://cropsteering.growlabs.nz/upstream-img/feed-dosing-desktop-light.png)
- **Feed pH/EC calibration.** A guided 2-point calibration at the tank; nothing changes until you commit it.
  ![Feed probe calibration](https://cropsteering.growlabs.nz/upstream-img/feed-calibration-desktop-light.png)
- **Setup: pumps & valves.** Your pumps, main line and valves, and the new pump supply: real flow and zones at once.
  ![Setup, pumps and valves](https://cropsteering.growlabs.nz/upstream-img/setup-pumps-valves-desktop-light.png)
- **Alert rules.** Your own notifications, with a preview of how often each would have fired.
  ![Alert rules](https://cropsteering.growlabs.nz/upstream-img/alert-rules-desktop-light.png)
- **System status.** The health of every part, and each room's Upgrade report.
  ![System status](https://cropsteering.growlabs.nz/upstream-img/system-status-desktop-light.png)
- **AI assistant.** The exact instructions it works from, your room notes and what it costs.
  ![AI assistant settings](https://cropsteering.growlabs.nz/upstream-img/ai-assistant-desktop-light.png)
- **On your phone.** Every page works on a phone in the grow room.
  ![Phone view](https://cropsteering.growlabs.nz/upstream-img/status-all-rooms-phone-dark.png)

**Update the integration BEFORE the add-on.** Update Crop Steering in HACS first and restart Home Assistant, then update the Crop Steering controller app. The integration runs the upgrade and writes the Upgrade report; the 3.18 controller reads what it writes.

**Upgrade guide and AI setup.**

- [3.18 upgrade report, line by line](https://github.com/JakeTheRabbit/HA-Irrigation-Strategy/blob/v3.18.0/docs/UPGRADING.md): one section per Upgrade report line, written so an AI can follow it step by step.
- [Upgrade to 3.18: the plan](https://github.com/JakeTheRabbit/HA-Irrigation-Strategy/blob/v3.18.0/docs/UPGRADING-TO-PARITY.md): what stays the same, a first-hour checklist and how to go back.
- [Set up an install with an AI assistant](https://github.com/JakeTheRabbit/HA-Irrigation-Strategy/blob/v3.18.0/docs/AI-SETUP.md): start read-only and approve each change, and [Configure Crop Steering through MCP](https://github.com/JakeTheRabbit/HA-Irrigation-Strategy/blob/v3.18.0/docs/MCP.md): connect an assistant to your Home Assistant.
- [User guide](https://github.com/JakeTheRabbit/HA-Irrigation-Strategy/blob/v3.18.0/docs/USER_GUIDE.md), [AI assistant](https://github.com/JakeTheRabbit/HA-Irrigation-Strategy/blob/v3.18.0/docs/AI_ASSISTANT.md) and the [Crop Steering Academy](https://jaketherabbit.github.io/HA-Irrigation-Strategy/learn/).

Pair: **controller 3.18.0**. **C3: the parity release.** The controller, the engine, the integration, entities, add-on behaviour, persisted state and the dashboard all change. **Approved rehearsal, no staging soak:** the owner approved releasing and promoting this candidate without the staging soak on 2026-10-23 (docs/RELEASING.md step 5, `approved-rehearsal`). It passed CI (including the real Home Assistant tier on the newest and oldest supported versions, the add-on image builds and the controller container smoke), a 216-visit route sweep of the dashboard and a final code review. **Nothing in this release has been run on real plumbing**: no staging photoperiod, no catch test and no fault drills.

#### In plain English

**New in 3.18 (the parity release)**

- **Timer programs and watering modes.** Each zone waters by steering (as before), by timer programs, or is off.
- **Pump sharing and flow checks.** Say how much each pump really delivers and how many zones it can feed at once; shots queue instead of starving each other. A flow meter checks each shot's water against what was asked for, and the controller looks for leaks.
- **Calibration.** A guided 2-point feed pH and EC probe calibration, and a prime-and-calibrate wizard for each dosing pump. Nothing changes until you commit it.
- **Presets and copy.** Save a room's or a zone's targets, phase timing and limits as a preset and copy them to other rooms or zones. Grow-plan recipes saved in your browser import as presets in one click.
- **History.** An explorer for any reading over any period, a change journal of every setting change (who, when, and what the upgrade carried across) with put back and undo, and a compare view.
- **Alerts.** Your own alert rules, tested against yesterday before you save them, an alerts inbox and quiet hours per phone.
- **Display and dashboards.** Display units (ppm or mS/cm, °F or °C), and dashboards made from templates.
- **System status.** The health of every part and each room's Upgrade report in one place.
- **Feed & dosing.** Every dosing pump with its solution, stock tank and doses; dosing refusals are recorded with their reason.
- **The upgrade itself.** Your settings carried across where that was safe, with a report of the rest.

**Fixed**

- A shot that fails after its valve opened now counts the water it delivered.
- The controller reads the pump, the main-line valve and the zone valve back ON at the start of every shot. A dead relay or an offline plug now cancels the shot with an alert instead of counting water that never flowed, and a zone that keeps failing backs off.
- P0 now waits for the plants to start drinking after lights-on. Only a zone at its rescue floor skips the wait (#195). Rooms that skipped P0 every morning now take their first shot later, up to the zone's P0 maximum wait.
- A zone with a dead moisture probe gets its minimum daily water again, only with the lights on and outside P0. Its fallback timer shot no longer fires at night or outside P1/P2.
- With no feed EC probe and no recipe feed EC, the controller no longer invents a feed EC of 3.0 on a new room; such a room gets one card saying EC flushing is off. Rooms that had it before the update keep the 3.0 assumption (see the Upgrade report).
- A failed read of the shot-length cap no longer lifts it to 900 s, an unreadable feed EC/pH band holds the shots, and a failed Home Assistant read or write is never taken as a real value.
- An error in one room's pass no longer stops the other rooms, and a shot-start error can no longer wedge the controller.
- Lights-on and lights-off set to the same hour are not used.
- A setpoint the controller failed to write is no longer credited to it in History.
- The phase sensor never makes up a phase, and the controller says so when it has no next-shot estimate.
- One source of truth for the derived watering values (shot length, adjusted re-water point, EC ratio, room averages, shots today). Settings that only fed retired sensors are hidden, and Repairs names any automation that still uses one.
- The dashboard shows the controller's own dryback, peak and next shot, its own limits, EC band and tank marks, reads water volumes in the unit Home Assistant reports, shows every time on Home Assistant's clock, and its links land where they say.
- All rooms is a visual page with slabs and tanks, without duplicate pickers.
- The dosing form uses the room's own dose limits.
- `apply_recipe` and `save_recipe` act on one room, and the Recipe stage select shows a stage only once it is applied.
- Long-term history reads the sensors from this room's archived runs.
- The MCP server accepts what the integration accepts (pumps, valves, probe units).

**Deprecated.** The named-stage recipe system (the Recipe stage and Irrigation phase selects, and the `apply_recipe` and `save_recipe` services) is marked as going away in **a future major release**. It still works; the first use after a restart raises a Repairs notice saying what to use instead.

**Documentation.** A shorter README, a [documentation index](https://github.com/JakeTheRabbit/HA-Irrigation-Strategy/blob/v3.18.0/docs/README.md), one page per new feature in [docs/features](https://github.com/JakeTheRabbit/HA-Irrigation-Strategy/blob/v3.18.0/docs/README.md#using-it), the system map and ranked system audit, and help text that follows the 3.18 page names.

#### Technical notes

- **Upgrade migration:** `custom_components/crop_steering/migration.py`, a run-once, versioned ledger (`crop_steering.migration` store) per room and site. Every seeded value is journaled as `config.changed` with actor `migration`; old stores are read only. The report is served by `crop_steering.migration_report` and raised as the Repairs issue `upgrade_report_3_18` (CS-616) while a room has a "Needs you" line. M15 keeps controller 3.17.0's 3.0 mS/cm feed EC for a room with no feed EC probe and no recipe feed EC. Row by row: [docs/UPGRADING.md](https://github.com/JakeTheRabbit/HA-Irrigation-Strategy/blob/v3.18.0/docs/UPGRADING.md).
- **Parity backend (#230):** one ordered GuardChain for every shot check, with dosing refusals through it and journalled; pump capacity and an exclusive-valve queue; timer programs and per-zone watering modes (an unparseable `timers_config` falls back to the engine under each zone's mode); timed pump prime and calibration runs; metered-vs-commanded water reconcile per shot (`flow.py`, baseline from the pass's own reading, never a read before the pump opens) and CS-319 leak detection; calibrated feed probes for the source-water gate, mix check and pH correct; journal outbox and backfill; integration APIs for site, timers, presets and copy, alert rules and inbox, changes, timeline, system status, dashboards, probe offsets and calibration. New error codes CS-612, CS-615, CS-616.
- **Parity dashboard (#230):** Feed & dosing, Pumps & valves, Prime & calibrate a pump, Feed calibration, Stock, Water & flow, Timer programs, History explorer/changes/compare, Alerts, Notifications, System status and AI pages. Bundles rebuilt deterministically (three copies byte-identical). Route sweep: every page × desktop/phone × light/dark × 3.18/3.17 demo backends, 216 of 216 visits clean.
- **Fix pull requests merged through #229:** #196 All rooms visual slabs and tanks; #197 shot error after the valve opened counts its water; #198 docs follow 3.17 section names; #199 water volumes in Home Assistant's unit; #200 dosing form uses room dose limits; #201 failed shot-cap read no longer lifts the cap (CS-203); #202 unreadable feed EC/pH band holds shots; #203 blind fallback shot only with lights on, in P1/P2; #204 one room's error no longer stops the others (CS-212); #205 `apply_recipe`/`save_recipe` scoped to one room; #206 recipe stage select shows a stage once applied; #207 `current_phase` never invents a phase; #208 pump, main line and valve read back ON at shot start (CS-302/303/304, backoff after 3 cancelled shots); #209 equal lights-on/off hours unused; #210 shot-start error cannot wedge the controller (CTL-032); #211 controller's own dryback, peak and next shot on the dashboard; #212 system map and ranked audit; #213 Academy local URL; #214 MCP accepts pumps, valves, probe units; #215 failed Home Assistant read/write never a real value; #216 blind-zone minimum daily water and no invented feed EC (CTL-031, CTL-008, CS-907); #217 every dashboard and Academy browser check on pull requests; #218 no next-shot estimate said so; #219 dashboard links land where they say; #220 `history_long` reads this room's archived runs; #221 recipe system deprecated (CS-613), stage applied once, dashboard follows the controller's steering mode; #222 one source of truth for derived watering values (`shot_length.py`, CS-614); #223 failed setpoint write takes back its claim (CTL-035); #224 actions pinned to commits, honest `run_ci.sh`, faster container smoke; #225 dashboard one source for limits, bands, time and readability; #226 README and feature matrix; #227 P0 bypass only at the rescue floor (#195); #228 help text catch-up.
- **Deprecation (#221):** new `deprecations.py` (`note_deprecated`): a log warning on every use and one Repairs issue (`deprecated_<key>`, CS-613) per Home Assistant run. `apply_recipe` no longer calls `select.select_option` (INT-003); `buildRoom` picks the EC target family as the controller's `_veg()` does (INT-014).
- **Documentation:** new `docs/README.md`, `docs/UPGRADING-TO-PARITY.md`, `docs/AI-SETUP.md`, `docs/features/*.md`; `docs/ENTITIES.md` gains the 3.18 entities. The screenshots in `docs/screens/` are captured from the compiled demo with the clock pinned (`node frontend/scripts/capture-screens.mjs`, then `python scripts/optimise_screens.py`).
- Rolling back to 3.17.0, and what 3.17.0 does not read after the update, is covered in [Upgrading to 3.18](https://github.com/JakeTheRabbit/HA-Irrigation-Strategy/blob/v3.18.0/docs/UPGRADING-TO-PARITY.md).

### 3.17.0

Pair with integration 3.17.0. **C1.** Updated dashboard package only: room and zone tree in the menu, phone bottom section bar, the selected room in the page address, Overview renamed Status and Settings & help renamed Settings, room water totals on All rooms. Controller, engine, state, options and physical safeguards are unchanged; the controller code is byte-identical to 3.16.1.

### 3.16.1

Pair with integration 3.16.1. **C0.** Adds the Crop Steering Academy teaching course and website publication. Controller, engine, state, options and physical safeguards are unchanged; the controller code is byte-identical to 3.16.0.

### 3.16.0

Pair with integration 3.16.0. **C1.** Updated dashboard package: all-room All zones landing, browser starting-page preference, consolidated Irrigation workspace and canonical settings ownership. Controller, engine, state and physical safeguards are unchanged. Published after an owner-requested UI rehearsal; no full photoperiod or physical plumbing soak is claimed. Rebuild/update both halves for matching packaged assets.

### 3.15.0

Pair with integration 3.15.0. **C3.** Restored opt-in phase, safety, sensing, runoff estimate and dosing controls; additive state migration; persisted waste policy fails closed when unreadable; pH dosing faults survive unknown enable readings. Includes revised generated AI instructions, revisioned room context and cache controls, and the answer-choice schema mitigation while set_values stays strict. New features retain neutral defaults. Approved rehearsal release; no plumbing soak claimed.

### 3.14.0

Pair with integration 3.14.0. **C2.** #162: the AI assistant is Claude only and asks every judge by default (ramp included); `ai_judges` replaces `jev_judges` (read as a fallback), `jev_*` tuning options become `ai_*`, the Cloudflare/TypeSafe route is removed with old keys still accepted; package `jev/` is now `brain/`. No state-file change.

### 3.13.0

Pair with integration 3.13.0. **C2.** #158: P1 ends when the ramp stalls under its target (two non-rising shots), with a day-scoped re-water under the stalled peak; state gains `p1_flat`/`p1_stall` (optional keys, an old file loads unchanged). #156: AI assistant powers per room, with the AI setting targets and shot sizes itself when ticked; goal scorecard, rules and controller brief in the cached prompt. Includes 3.12.2.

### 3.12.2

Pair with integration 3.12.2. **C3.** #149: a missing hold entity is named at start and in the block message (CS-210), still fail-closed. #151: the why-evidence names the kill switch and holds. #150: one log line when a copied `jev_judges` leaves out newer judges. No state-file change.

### 3.12.1

Pair with integration 3.12.1. **C3.** #147: optional `anthropic_base_url`, so the Claude brain can call an Anthropic-compatible router (tolerant reply parsing, router-prefixed model ids). Blank keeps the direct Anthropic path. No state-file change.

### 3.12.0

Pair with integration 3.12.0. **C3.** The Jev edition 3.11.1 brought into this repository, plus the controller side of the 13 change groups in #133 to #145: container mode (one image, app or plain Docker), a bounded safe-off on stop, the duplicate-controller hold, safety ports (settings wait, fail-closed holds, max-run latch, settled P1 rise, guarded probe use, test shot, stopped heartbeat, P3 time between shots), engine ports (P0 extra dryback, P2 spacing, P3 hold, EC interval, zone-sized caps), change log and read-back, decaying re-notify, shot-landed check, dosing on the main loop with tank level and reservoir minimum, the optional Claude Haiku brain with its judges and plan adherence (CS-713), and STE text. The state file gains optional keys only; an old file loads unchanged.

### 3.11.1

Pair with integration 3.11.1. **C3.** The ladder fix (#42). The four setpoints that follow the zone's highest readings stay inside each number's own min and max as well as the engine's bounds, are written top-down when field capacity rises and bottom-up when it falls, and a refused write stops the rest, so the ladder never inverts (6 Oct: Room A zone 2's floor of 17.9 was refused under 20 after its threshold had moved). Nothing moves until a lights-on has kept a day before (`peak_prev`): the day in progress is no stand-in. No state-file change.

### 3.11.0

Pair with integration 3.11.0. **C3.** The owner's P1 rules (#40). The P1 target is the zone's highest reading of the grow-day before (zone key `peak_prev`, kept at lights-on), with no 2-points-a-day limit; field capacity is the higher of today's and the day before's highest readings, and the re-water threshold and rescue floor move with it. P1 ends when a shot at the target no longer raises the zone's highest reading by 0.5 (the engine's `p1_extra` shots), or at the maximum shots; pore EC no longer holds P1 open. A zone on the blind path keeps its peak at lights-on too; until the first lights-on after the update, the grow-day in progress stands in for the day before only once its P1 is over. The learner writes neither the P1 target nor field capacity. New zone keys `peak_prev`, `p1_reached`, `p1_peak_before`, `p1_extra`; a 3.10.0 state file loads with them empty, and 3.10.0 ignores them after a rollback.

### 3.10.0

Pair with integration 3.10.0. **C3.** EC flushes (#37): the capped corrections (P0 flush, P1 flush, P2 dilute) need dilutive feed and a table that drains (`ZoneSnapshot.backed_up`, set from the CS-310 hold), not a moisture reading under field capacity - 2; the cap-exempt anti-lockout and P2 rescue keep that test, and a dilutive zone that reads full falls through to the capped rules instead of being blocked from all water. Field capacity follows the sensor (#38): the zone's highest reading 20 minutes or more after a shot, today or yesterday, never under the P1 target + 2, tracked every loop from the probe (learner keys `full_date`, `full_day`, `full_prev`) and written for blind zones too; when the probe moves it by a point or more, the P1 target, P2 re-water threshold and rescue floor move by the same points. A 3.9.0 state file loads with the new keys empty; 3.9.0 ignores them after a rollback.

### 3.9.0

Pair with integration 3.9.0. **C3.** One writer per setpoint under Jev's Setpoints judge (#34): the Auto setpoints learner writes the P1 target, field capacity and rescue floor, the judge the P2 shot size and re-water threshold; a learned P1 target rises at most 2 points a day and only after that day's ramp reached it, and a stale target goes to 2 points over the reading P1 began at, capped at the learned peak plus 1 (learner keys `day_ceiling`, `p1_entry`, `stale_target`). Each zone's plateau and nights are kept in the state file and logged every morning (#31); `decide()` is unchanged. Jev's availability (#32): TypeSafe then Cloudflare with retries, stale questions dropped, second looks, the stricter gate, CS-706 and CS-707; new options `jev_budget_warn_pct`, `jev_retries`, `jev_reask_max`, `jev_unsure_below`, `jev_strict_after` and `jev_strict_prob`, and `jev_daily_calls` defaults to 5000. A 3.8.0 state file loads with the new keys empty; 3.8.0 ignores them after a rollback.

### 3.8.0

Pair with integration 3.8.0. **C3.** Tables backing up (#26): CS-310 (critical) when a zone's moisture rises by 3 points within an hour while its valve (the controller's or a person's) stayed shut for that time and the 45 minutes before; the zone's daily minimum is held until the reading has not risen for an hour. CS-311 (critical) when the sump pump named in the new option `sump_power_sensor` (one sensor or `room=sensor` pairs) shows no run in Home Assistant's history for `sump_silent_hours` (default 3) while the room is on. New options only; no change to the state file.

### 3.7.0

Pair with integration 3.7.0. **C3.** The minimum-daily floor (`input_number.crop_steering_<prefix>zone_N_min_daily_ml_per_plant` x plant count) is paced (#24): never in P0, then due only while the zone is behind a straight line to the whole minimum 3 hours before lights-off, so a zone behind at the end of P0 catches up in P1 and the rest is spaced through P2; past that point it fires until met. It still waters whatever the probe reads (only the drown ceiling and the 10-minute spacing hold it). The snapshot now carries `hours_since_lights_on`. No change to options or the state file.

### 3.6.0

Pair with integration 3.6.0. **C3.** Who gets which phone push (#22): once any phone is set up on Settings & help › Notifications, each alert's push goes through the integration's `crop_steering.notify` to the phones whose row ticks its kind; with none, or when that call fails, it goes to `notify_service` exactly as before, and so does an emergency that reached no phone. Phase changes and Jev's moves are sent after the loop's shots, one push per room, within 10 s; a pump whose turn_on errored (CS-302) and a mix pump that did not start (CS-805) are switched off before their alert. New: pushes for a zone changing phase and for Jev moving a setting (through the routing only), and CS-209 when a room with its lights on has watered nothing for the set hours. No change to options or the state file.

### 3.5.0

Pair with integration 3.5.0. **C3.** Batch-tank dosing: the controller runs single doses and batches requested from the new Equipment › Dosing page, holds watering during a batch, draws each dose from its linked stock tank, and recovers after a restart (#19). New file `/data/dosing_state.json`; new app option `timeout: 30` (Supervisor stop timeout); no change to existing options or the state file. Not run on hardware.

### 3.4.2

Pair with integration 3.4.2. **C3.** A switch that has not reported OFF 6 s after a close is sent OFF again and read back for 10 s more before a hardware hold (#18). No change to add-on options or the state file. Not run on hardware.

### 3.4.1

Pair with integration 3.4.1. **C1.** The app's documentation page describes Jev and its options, and the install guide's buttons add this repository (#16); the dashboard demo shows Jev's re-water range as the controller bands it (#15). No change to the controller, its options or the state file. Not run on hardware.

### 3.4.0

Pair with integration 3.4.0. **C3.** Jev's Pore EC judge is asked only while EC stacking is on; journal outcomes name their decision (`of`) in words; the day's Jev usage survives restarts (`/data/jev_usage.json`); each zone's Jev sensor publishes its `setpoints` range (#12). The dashboard is rebuilt to five pages (#13). No change to add-on options or the state file. Not run on hardware.

### 3.3.0

Pair with integration 3.3.0. **C3.** Jev's doctrine gains a published grow guide as a second cited source (24 rules after the owner's, 123 in all; #7). The dashboard's grow day and live Jev log (#10) ship with the integration and this app's page. No change to add-on options or the state file. Not run on hardware.

### 3.2.0

Pair with integration 3.2.0. **C3.** The Setpoints judge: one notch a night on a zone's own P2 shot size or re-water threshold, inside a range around the operator's value, while the room's Auto setpoints switch is on; a rescue shot after a change puts it back and pauses the zone 48 h; the base Auto Setpoints learner never writes while it runs (#6). New file `/data/jev_setpoints.json`; no change to add-on options or the state file. Not run on hardware.

### 3.1.0

Pair with integration 3.1.0. **C3.** Jev's decisions are journalled (`/data/jev_journal.jsonl`) and published as `sensor.crop_steering_<prefix>jev_log` for the dashboard's live log; the room's Jev sensor gains `stage` (#5). No change to add-on options or the state file. Not run on hardware.

### 3.0.1

Pair with integration 3.0.1. **C3.** Jev's zones judge compares each zone's water per plant with the room's median zone, itself included, instead of its thirstiest sibling, so only the odd zone out gets the CS-702 alert (#3). Advice only: no water decision changes. No change to add-on options or the state file. Not run on hardware.

### 3.0.0

The Jev edition. Pair with integration 3.0.0. **C3.** Jev judges the engine's decisions inside a code envelope (docs/JEV.md); with no Jev key it runs exactly as 2.25.1. New options: `typesafe_api_key`, `jev_enabled`, `jev_judges`, `jev_daily_calls`, `jev_flower_start`, `jev_flower_days`. No state-file change. Not run on hardware.

### 2.25.1

Pair with integration 2.25.1. **No controller change:** the version moves with the pair (the integration's zone sensors now combine a zone's probes, #130). No change to add-on options or the state file.

### 2.25.0

Pair with integration 2.25.0. **C3.** Like 2.24.0, a bundled release of several changes, for the owner to approve; not run on hardware before release. No change to add-on options or the state file.

- **Retired switches.** `_carry_retired_switches` switches the room's engine switch off while System Enabled or Auto Irrigation Enabled reads off (CS-208), and `_blocked` no longer gates on them; a missing or unreadable one changes nothing. While an interrupted shot is still being settled it leaves the engine switch alone, so the cleanup that closes that shot's valve is not taken for a person taking over.
- **A zone switched off stops its running shot.** `_wait_shot` reads the zone's `switch.crop_steering_<prefix>zone_N_enabled` every round with the engine switch, Room Active and manual override; CS-305 names the switch that stopped the shot.
- **An unreadable zone switch holds the zone.** `_blocked` returns `<entity> unreadable (reads neither on nor off)` when `switch.crop_steering_<prefix>zone_N_enabled` is missing, `unknown` or `unavailable`, so a room whose integration is not running is not watered on the backup timer. A running shot still ends only on a definite OFF.
- **Publishes what each zone waits for.** `sensor.crop_steering_<prefix>zone_N_waiting_for_app` every pass, from the engine's `waiting_for`: for the phase the zone is in after this pass, and an empty list for a zone with no usable probe or a room that is off.
- **The vitals follow the room.** A room with no feed EC probe leaves feed EC out of its line, and one whose probe reads nothing usable says "unreadable". When the room's `select.crop_steering_<prefix>water_today_view` says `PER_PLANT`, each zone's water today is divided by its plant count: "344 mL/plant day".
- **The dashboard the app serves** is the 2.25.0 build: each zone's "Next:", the switch over every zone, Water today per plant and What's new.

### 2.24.0

Pair with integration 2.24.0. **C3.** Owner-approved rehearsal release without a staging soak (26 Sep 2026); not run on hardware before release. No change to add-on options. One new key in the state file, `_room_off_since` in a room's block, which an old state file does not have and does not need.

- **No shot while a switch is offline.** `_blocked` returns `<switches> offline (reads neither on nor off)` when the zone's pump, main line or valve reads anything but `on`/`off`, checked last so every other reason keeps priority. Nothing is opened and no hardware hold latches; the zone waters once the switch reads again. A switch that goes offline during a shot still latches CS-301.
- **A dead probe waits 15 minutes.** `BLIND_GRACE_MIN` = 15: a zone whose probe has been unreadable for less than that in this run gets no timer shot, sibling copy or CS-102 alert (`blind_wait`); the time rules still apply.
- **A room switched back on within a day carries on.** The switch-off time is saved as `_room_off_since`; switched on less than `ROOM_RESUME_H` = 24 h later, the room keeps its phases, today's counters and learned state. Longer, or unknown, starts the fresh run as before.
- **A zone can be moved to a phase by hand** with the integration's new `select.crop_steering_zone_N_set_phase`. `_apply_phase_request` resets the select to Keep and only then moves the zone, once; it actuates nothing and does not need the kill switch.
- **Keeps reporting during a shot.** `_wait_shot` repeats the room's heartbeat and zone labels once they are a minute old (at most two short writes per round, so the kill switch is still read about every 2 s), and a room is reported before its first shot after a restart or a switch-on.
- **The dashboard the app serves** is the 2.24.0 build: the setting names and explainers, the Watering switch in the status line and Settings, and the zone's Phase section.

### 2.23.0

Pair with integration 2.23.0. **C1** for the controller app: no behaviour change; its code changes in comments only (the syntax tree is unchanged). Owner-approved rehearsal release without a staging soak (25 Sep 2026). No change to add-on options, the state file or irrigation.

- **The dashboard the app serves** is the 2.23.0 build: each zone's lane on the Overview's grow-day chart shows its target, yesterday and the projected rest of the day.
- **Configuration tab:** `instance_name`, `hold_entities` and `rediscover_seconds` now have a name and a description, and the `notify_service` example is neutral.

### 2.22.0

Pair with integration 2.22.0. **C1** for the controller app: no controller code change. Owner-approved rehearsal release without a staging soak (25 Sep 2026). No change to add-on options, the state file or irrigation.

- **The dashboard the app serves** (`www/public/dashboard.html`) is the 2.22.0 build: the Stock tanks page, mini visuals on every page, water use per zone, typing the balance into the whole-grow table, the tank's EC and pH history, and "Watering" instead of "Controller not running" while a shot holds the controller.

### 2.21.0

Pair with integration 2.21.0, one number for both halves from this release on. **C3.** Owner-approved rehearsal release without a staging soak (25 Sep 2026); not run on hardware before release. No change to add-on options or the state file.

- **C3. A shot interrupted by a Home Assistant restart is closed, not left running.** After a restart (or a switch reconnecting) Home Assistant reports every switch as changed at that moment, so `_inflight_plan` took the interrupted shot's own open valve for a person's and left it on without an alert. When `last_changed` falls outside `INFLIGHT_OPEN_WINDOW_S`, the new `ha_history()` + `_on_since_shot()` read the recorder: ON since the shot opened it, with only `unavailable`/`unknown` between, is the shot's and is closed; an OFF since, or ON before the shot, is a person's; no history is `unsure` (CS-309, retried every loop). Not run on hardware. No change to options or the state file.
- **C1, dashboard only.** The dashboard the app serves (`www/public/dashboard.html`) is rebuilt with the Overview's grow-day timeline and the dashboard changes in the integration's changelog (the Overview's layout and a calmer look on every page). No change to options, the state file or irrigation.
- **C3. A shot that something else cuts short ends there, and only its water is counted.** 23 Sep, 11:25: the batch tank ran empty 4 s into a 170 s shot, the dosing hold came on and a guard closed the feed path, and the controller waited out and counted all 170 s. `_wait_shot` now also ends a shot when a `hold_entities` entity reads ON or the shot's own valve reads OFF after being seen ON, with the same bounded reads and ≤2 s cadence; the kill switch, room switch and override are read first and still win. `_close_cut_short` switches off the shot's valve and main line unless they read OFF, and its pump unless it reads OFF or a hold is ON. It reads back only what it switched off, clears `shot_inflight` as a normal close does, and never latches a hardware hold for a feed path somebody else closed. Counted time: the valve's `last_changed` when it falls inside the shot, else when the interruption was seen; the shot counts as a kill-switch abort does. One debounced alert names the entity and the seconds delivered against planned. Not run on hardware. No change to options or the state file.

### 0.16.5

Pair with integration 2.19.5. **C3.** Owner-approved rehearsal release without a staging soak (23 Sep 2026); not run on hardware before release. No change to add-on options or the state file.

- **A held grow plan never stops an emergency, watchdog or minimum-daily shot.** Each zone the plan holds is decided with the engine's new `ZoneSnapshot.steering_held`, so `decide()` returns the rescue behind a routine shot; `_blocked` and the shot preflight let `PLAN_HOLD_EXEMPT` kinds (`p3_emergency`, `watchdog`, `min_daily`, and for a blind zone `blind_fallback` and `blind_copy_rescue`) through the plan hold, and every other gate still applies. A held zone that is not firing shows the hold as its block. No change to options or the state file. Pairs with the integration's plan fixes in the same release (a plan no longer holds a room all day over a missed lights-on).
- **Zone status has one writer.** The controller publishes each zone's label, with its reason, on `sensor.crop_steering_<prefix>zone_N_status_app` (`Room off` included) and no longer writes `zone_N_status`, which the integration now mirrors from it. With an older integration, `zone_N_status` shows that integration's fixed-threshold label until it is updated. No change to options, the state file or irrigation.

### 0.16.4

Pair with integration 2.19.4. **C1.** No controller code change: the dashboard served by the app is the 2.19.4 build (controller-health status line, live updates instead of polling). No change to options, the state file or irrigation.

### 0.16.3

Pair with integration 2.19.3. **C3.** Owner-approved rehearsal release without a staging soak (23 Sep 2026); not run on hardware before release. No change to add-on options.

- **A room deleted and set up again is adopted afresh** (#50): the descriptor's `entry_id` changing re-opens adoption through the usual gate (kill switch and hardware OFF); first sight changes nothing, so a running room resumes without a disarm cycle.
- Installed only from `JakeTheRabbit/HA-Irrigation-Strategy`; the `f2-control` mirror is retired. `url` in `config.yaml` now points here. No change to options, the state file or irrigation.
- **The daily limit is a budget with typed exemptions.** The watchdog, P3 emergency and high-EC flushes (anti-lockout, P2 rescue) pass it, and so does the P1 ramp, which always runs in full. Top-ups, P1/P2/P0 EC-correction shots and the min-daily floor stop at it. A shot that would cross it is cut to what is left (under 5 s: held, `BLOCK daily-cap (x L left)`). The "flush" in a reason's text no longer makes a shot exempt.
- **A zone over budget and starving gets the watchdog shot** instead of nothing (22 Sep: Z1 dry 14:06-22:00). No watchdog in P0: the night no longer counts as "no water" at lights-on.
- **P1 at its ceiling, held open only by pore EC, completes once the budget is spent.**
- **Pore EC is settled EC.** Every EC rule uses the last reading taken 45 minutes after a shot, held in between; EC corrections wait for the next settled reading. Only settled readings feed the EC offset step / PID. Published as `ec_settled` on each zone's safety status.
- **A new grow-day resets a zone found in P1/P2** (the controller was not running across lights-off), and yesterday's EC offset is cleared before the first tick of the day.
- **Interrupted shots.** Each shot writes down what it will open before opening it; the next loop closes exactly that, only while the room's kill switch is ON and only switches ON continuously since the shot opened them. Anything a person has switched since (hand-watering, tank circulation), everything upstream of it, the main line and pump while another valve on the line is open, and the pump while a hold is on, are left alone. Nothing is switched off on a timer.
- **Stopping the app closes only what is in flight.** SIGTERM (stop, update, restart) used to switch off every mapped pump and valve; it now closes only the shot running, by the same rules, and counts its water. With no shot running it switches nothing off, so tank circulation and hand-watering carry on through an update. The error-cleanup read-back is as patient as the normal one (no false hold from a late Zigbee OFF report); alerts are only silenced once Home Assistant has them, and a hardware hold latched while it was unreachable is announced once it is back.
- State file: additive `_shot_inflight` (room block), `ec_settled` / `ec_settled_at` (zone). An old file loads; the 0.16.2 controller loads the new one.

### 0.16.2

Pair with integration 2.19.2. **C3.** Found on the first real install (a one-zone tent); the fixes themselves were not run on hardware before release.

- **Zones are never invented.** Started before the integration was set up, the controller fell back to the shipped `num_zones: 3` and reported zones 2 and 3 of a one-zone tent as "no hardware mapped". It now has no zones until a room exists, checks every loop, and picks the room up by itself: no restart needed. The log says so: *"the Crop Steering integration has not published a room yet..."*. `num_zones` is still the fallback when Home Assistant cannot be reached at start, and a hand-mapped `hardware` option still keeps its zone count. A state file that already holds the phantom zones loads as before.
- **Switch-on is no longer an irrigation event.** New switch-on timestamps are explicitly marked by `last_shot_is_anchor` and published as `unknown` until an irrigation is recorded, also for a room that is off. An old file without the flag keeps its timestamp: zero daily counters or missing history cannot establish whether an old timestamp was switch-on or irrigation. Electrical operation alone does not prove water delivery.
- Final review fixes keep the descriptor's complete zone list when HA sensors appear gradually, preserve old irrigation timestamps after daily rollover, and keep legacy numeric-string/malformed excluded-volume state loadable. Regression tests cover each case.
- The dashboard served by the app is the 2.19.2 build (the side menu scrolls on small screens).
- No change to add-on options. No change to what a working install waters, or when.

### 0.16.1

Pair with integration 2.19.1. Not run on hardware before release.

- The controller reports its own version: `controller_version` in every room's `ai_heartbeat`, and in the first log line (`f2-control 0.16.x starting | rooms ...`). The dashboard's sidebar shows it next to the integration's. It is read from the `config.yaml` the image was built from (copied in as `/app/addon.yaml`), so there is no second number to keep in step.
- No change to irrigation behaviour, options or the state file.

### 0.16.0

Pair with integration 2.19.0. **C3.** Released without a staging soak by decision of its two operators; not run on hardware before release.

- A room can DECLARE its plumbing (`plumbing` in the engine descriptor, set in the integration's setup). Declared: the mapped pump and main-line have to match it, or the room is held with a reason, nothing opens and nothing is counted. This closes the 2.18.0 case where a pumped room with no pump mapped was watered with the valve open and no pump, and the shot counted as delivered.
- **Never declared (every existing install): no change**, and no disarm cycle after the update. The layout joins the saved setup fingerprint only when it is present, so the fingerprint 0.15.x saved still matches.
- A layout this controller does not know (a newer integration) is held, not guessed.
- No change to add-on options or to the state file.

### 0.15.2

Pair with integration 2.18.1. No change to irrigation behaviour, options or saved state.

- Test seam: the state file location can be overridden with the `F2_STATE_PATH` environment variable. **Unset, as on every install, it is `/data/state.json` exactly as before.** The constructor reads that file, and on adopting a setup writes it, before a test can redirect it; GitHub runners have no `/data`, so CI never noticed, but on any machine where `/data` exists and is writable (a devcontainer, this add-on's own container) the test suite wrote a real file there and leaked it into the next test.
- Bundles nothing new; the dashboard is the 2.18.0 build.

### 0.15.1

- A room switched off stays off while Home Assistant restarts. The room's on/off switch reads unavailable for a moment during a core restart, and unavailable used to mean on: an empty room began a fresh run and raised its probe alerts again. The controller now keeps the last value it read (also across its own restart) and only treats a switch it has never seen as on, which is what keeps integrations older than the switch watering.

### 0.15.0

Pair with integration 2.18.0. Existing options, engine flags, room IDs, counters and learned state are kept. Updating from 0.14.0 resumes by itself; no kill-switch cycle is needed.

- A zone needs only its valve. Pump and main-line valve are optional, so a room with one smart plug or solenoid per zone irrigates. Lead times are skipped with the hardware they belong to; the close read-back and the hardware-fault latch cover whatever the room has. Three-switch rooms run exactly as before.
- The source-water EC probe may report µS/cm; it is converted to mS/cm before the sanity range and the gate. Previously such a probe read as out of range and the gate blocked every shot without saying why.
- The Cloudflare judge manages P2. With Auto Setpoints on and `cf_account_id` + `cf_api_token` set, `typesafe/jev` is asked once per clock hour during P2 (lights on, no dated plan). It may move the zone's `p2_shot_size` within 1-4 % and hold the working peak within 2 points of the learned one: one bounded step per lever per grow-day. A tripped guard, a low-confidence answer, or no answer in 5 s changes nothing; irrigation never waits on it. Status attributes `jev_last`, `jev_changed_today`, `working_peak_adjust`.
- Bundles the 2.18.0 dashboard (setup helpers in Rooms & setup).

### 0.14.0

Pair with integration 2.17.0. Existing options, engine flags, room IDs, counters and learned state are kept. **After this update, turn the engine kill switch off and on once**: the previous build saved no record of the setup it had accepted, so this first start has nothing to resume from. Later restarts and reboots carry on by themselves.

- Room on/off: `switch.crop_steering_<prefix>room_active` off means no irrigation of any kind and no alerts for that room.
- P1 no longer ends on a clock. It runs until the target is recovered after `p1_minimum_shots`, or `p1_maximum_shots` is reached.
- Auto Setpoints (off by default): learns each zone's ceiling, gain and dry-down; on a P1 plateau hands over to P2 and carries the achieved peak forward. Optional Cloudflare `typesafe/jev` check via the new `cf_*` options.
- The accepted setup revision is saved with a fingerprint, so an unchanged setup resumes after a restart with the kill switch left on (hardware must read off). A pending setup raises a notification naming what must read off.
- Switch read-back after a shot re-reads for up to 6 s instead of once at 1 s: a late Zigbee report no longer latches a false hardware hold.
- Bundles the 2.17.0 dashboard (recorded sensor data and the projected P0-P3 day on the Today graph).

### 0.13.3

- Combine daily targets and dated plans under Irrigation plan, with Today and Schedule views.
- Join schematic VWC and EC references through the night and into the next day.
- Show effective scheduled targets instead of disabled manual fallback fields when a schedule owns the room.
- Dashboard-only correction; controller decisions and stored settings are unchanged.

### 0.13.2

- Bundle graphical tank/pump telemetry and current zone state with last irrigation timestamps.
- Publish irrigation event timestamps with an explicit timezone offset.
- Include synthetic demo recipes/runs and the Home Assistant sidebar recovery button.

### Changelog — Crop Steering add-on

The full project changelog (integration + add-on) lives at [https://github.com/JakeTheRabbit/HA-Irrigation-Strategy/blob/main/CHANGELOG.md](https://github.com/JakeTheRabbit/HA-Irrigation-Strategy/blob/main/CHANGELOG.md).

#### 0.13.1

- Read the same-room legacy maximum-shot-duration entity when the canonical entity is absent, matching the dashboard calculator and reviewed edits. Reject an invalid existing duration cap before actuation.
- Bundle the user-authored recipe library and pair with integration 2.15.0. Existing room identities, options and persistent controller data remain compatible.

#### 0.13.0

- Pair with integration 2.14.0 for reactive manual setpoint previews, run comparisons and clear zone/per-plant water calculations.
- Calculate new delivery counters from configured zone flow and elapsed valve runtime, including capped/truncated/minimum shots and partial aborts. Snapshot sizing before each shot so a mid-shot configuration edit cannot rewrite its volume.
- Keep existing options, engine flags, room IDs, learned state and old daily/weekly totals. Delivery remains a flow-based estimate, not a meter reading.

#### 0.12.1

- Correct room names in the bundled workspace when HA descriptor sensors share a generic friendly name.
- Pair with integration 2.13.2. Irrigation decisions, options and persistent state format are unchanged from 0.12.0.

#### 0.12.0

Update the Crop Steering integration to 2.13.0 before updating this controller. Existing options, enable flags, room IDs and persistent runtime data are retained.

- Serve the native dashboard with per-zone day/week grow plans and a combined VWC/EC planning graph.
- Read versioned setup and strategy snapshots, with explicit configuration acknowledgement and safe holds for invalid required plans.
- Correct low-flow sizing, elapsed duration accounting, volume caps, stale sequential decisions and shared-hardware recovery.
- Remove historical package examples from the installation bundle; sensor and equipment mapping belongs to the integration.
- Missing pore EC keeps base VWC shot sizing and pauses EC offset/PID adaptation, with a visible degraded status.
- Feed EC/pH and pore readings require finite values and valid, fresh, timezone-aware timestamps (60 seconds future-skew tolerance).
- Weekly delivery estimates persist seven grow-day buckets, include interrupted delivery and expose incomplete legacy history.

#### 0.11.0

**⚠️ Update the Crop Steering integration BEFORE rebuilding this add-on.** The engine now reads the default room's pump/mainline/valves from the integration's `sensor.crop_steering_engine_config`. On an old integration that doesn't publish it, the default room **holds safe (no watering)** until the integration is updated (the engine re-checks every 5 minutes and resumes on its own).

- **Portability:** default-room hardware comes from the integration's engine_config descriptor — the hardcoded Room A pump/valve fallback is gone. An unmapped room holds safe with a clear "no hardware mapped" reason instead of actuating another install's entities.
- **Kill switch works mid-shot:** shots are delivered in ≤2 s slices that re-check the kill switch and the zone's manual override; an abort closes the valve immediately and counts only the water actually delivered.
- **`notify_service` default is now empty** — unset means persistent notifications only (no more default pointing at the developer's phone). If you had it set explicitly, nothing changes.
- **Dosing/fill/flush holds are now the `hold_entities` option (empty default).** The previously hardcoded Room A hold entities (`input_boolean.nutrient_dosing_active`, `input_boolean.f2_fill_mode`, `input_boolean.f2_flush_mode`, `switch.tank_filling`) are no longer checked automatically — add yours to `hold_entities` in the Configuration tab or that gate stays off.
- **Live room discovery:** rooms added in the integration UI join within `rediscover_seconds` (default 300 s) without an add-on restart, fail-safe OFF.
- **Missing-setpoint alerts:** if a `number.crop_steering_*` setpoint entity disappears (per-zone AND global) for 3+ loops, you get one rate-limited alert + a vitals line instead of the engine silently running built-in defaults.
- **Timezone hardening:** tzdata baked into the image; startup logs the effective local time and alerts if the container clock disagrees with Home Assistant's configured zone.
- Heartbeat sensors publish the room's `enable_flag` (used by the integration's Repairs health checks); vitals title uses the new `instance_name` option.

#### 0.10.5

- **Fix: zones could freeze in P2 if the fused-sensor entity_id didn't match.** The engine read each zone's probe at `sensor.crop_steering_<room>vwc_zone_N` / `ec_zone_N`, but on a box first set up under an older integration the HA registry keeps the legacy id `..._zone_N_vwc` / `_zone_N_ec` forever. The mismatch made every zone read as "blind" → `decide()` was skipped → the P0-P3 phase machine stopped (it never even forced P3 at lights-off), and the zones sat on a blind timer. The engine now resolves each fused sensor under **both** naming conventions (and `_detect_zones` counts either), so it finds the probe regardless of which id the registry assigned.
- **Hardening — a dead probe can no longer strand the daily cycle.** A blind zone still honours the time-based phase forces (lights-off → P3, P3 → P0 at the new photoperiod) even while VWC-driven steering is paused, so it can't freeze overnight. The "probe dead" alert now **repeats** (every ~30 min while blind) with the exact entity_id the engine is looking for, instead of firing once and going silent.

#### 0.10.4

- **Engine Log panel now works.** It used to tail `/local/f2_engine.log`, which the add-on can't write (no `/config` map) — so it was always empty. It now reads the engine's published decision feed (`sensor.crop_steering_activity_log`), room-scoped, and only re-renders when the feed changes (also removes the 4s render churn).
- **"Next" on the irrigation-frequency card.** The engine now publishes a per-zone next-shot estimate `sensor.crop_steering_<room>_prediction_zone_N_next_irrigation_hours` (P2: time for VWC to dry to the re-water threshold at the current dryback rate; "—" when not computable).
- **Safety (HA-down observability):** the shot CLOSE sequence now checks every `turn_off` and reads the valve back HA-aware — a failed/unconfirmed close (e.g. HA unreachable mid-shot) raises a CRITICAL alert instead of silently reading as "closed". The shot is still counted (water was delivered) so the daily cap stays honest. NOTE: software cannot close a valve when HA is down — the hardware fail-safe (NC valves, pump-relay default-off, an independent watchdog) is the real guarantee.

#### 0.10.3

- **Diagnostic:** f2.html logs one `[f2-perf]` console line per 30s tick (JS heap, DOM-node count + delta, Chart-instance count) to pinpoint the dashboard lag. Open the console, filter `[f2-perf]`, leave it until it lags, and watch which number climbs. Harmless; removed once the leak is found.

#### 0.10.2

- **Fix: per-zone "Volume fed vs daily cap" + "Irrigation frequency" tiles were blank (—).** The engine now publishes `sensor.crop_steering_<room>_zone_N_daily_water_app` (litres fed today) and `..._irrigation_count_app` (shots today) — the data was already tracked in zone state, just not republished. Resets at the lights-on (P3→P0) rollover like the other daily counters. (Per room.)

#### 0.10.1

- **Per-room dashboards (`?room=<slug>`).** The operator console (`f2.html`) and the mobile one-pager (`overview.html`) now scope to an additional room with `?room=f1` etc. — every `crop_steering_*` read/write is routed to that room's entities (two chokepoints, no per-id edits), the kill-switch button controls the **room's** kill switch, and a badge shows which room you're viewing. No `?room=` (default room) is unchanged.

#### 0.10.0

- **Multi-room engine.** One add-on now drives **every** configured room, not just the first. Refactored around a `Room` abstraction; each room is a fully self-contained control loop with its own pump/mainline/valves, reservoir pH/EC feed gate, photoperiod, kill switch and durable state, namespaced `crop_steering_<slug>_*`. Additional rooms are discovered from the integration's published `sensor.crop_steering_<prefix>engine_config` descriptors and come up **fail-safe OFF** (`switch.crop_steering_<slug>_engine_enabled`, default off). The default room is byte-identical to before. `/data/state.json` is nested by room; an old flat single-room file still loads transparently.

#### 0.9.1

- New logo (cannabis leaf + green growth chart + rising arrow, with the Home Assistant and Python marks). Updated the add-on icon and logo.

#### 0.9.0

- **Vmax advisory.** The engine now watches each zone's morning P1 wet-up and publishes the detected field-capacity ceiling as `sensor.crop_steering_zone_N_vmax_detected` (with a confidence attribute). Advisory only — it does **not** change any irrigation decision. Pairs with the integration's named-stage recipes (2.10.0).
- Re-synced the vendored `crop_steering_engine` with the canonical source.

#### 0.8.3

- Use the original Open Crop Steering logo artwork (leaf + water-drop in a green ring) for the add-on icon and logo, instead of the redrawn version.

#### 0.8.2

- New logo (cannabis leaf + growth chart), matching the project mark.

#### 0.8.1

- Fix: the dripper flow rate + drippers/plant now drive shot length even if a per-zone plant count is unset (it used to fall back to a generic flow value). Plant count cancels out of the duration maths, so it no longer gates the dripper settings.

#### 0.8.0

- Generic out of the box: the source-water pH/EC feed gate is now **optional**. Set `feed_ec_sensor` / `feed_ph_sensor` to your reservoir probes to enable it; leave them blank to disable it (dosing / tank-fill holds still apply). Removed the hardcoded Room A probe defaults; the substrate/flow fallbacks are neutral placeholders.

#### 0.7.0

- Renamed **the earlier name → Crop Steering** with a new logo. Proper description + feature list (Documentation tab) and this changelog. The dashboards are served as a sidebar panel (toggle **Show in sidebar** on the Info tab).

#### 0.6.0

- **Configure once**: lights hours and zone count are now read from the integration, so the add-on options for them are just fallbacks. Ends the "lights out of sync" class of bug.

#### 0.5.0

- The engine reads each zone's **fused** sensor (`sensor.crop_steering_vwc_zone_N` / `ec_zone_N`), so you can add **any number of probes per zone** in the integration UI and the engine uses all of them (averaged, outliers rejected). Removed the hardcoded zone sensors.

#### 0.4.0

- Bundled the operator **dashboards into the add-on**, served over Home Assistant **ingress** (sidebar panel) with a Live/Demo chooser — no more copying `f2.html` to `/config/www`.

#### 0.3.0

- Friendlier title + **custom icon**; help **tooltips** on every Configuration option.

#### 0.2.0

- First standalone add-on release: pure crop-steering engine + REST IO + 30-min vitals, gated by the kill switch. Replaces the retired legacy engine.

Source files: [CHANGELOG.md](https://github.com/JakeTheRabbit/HA-Irrigation-Strategy/blob/v3.18.1/CHANGELOG.md) and [addons/f2_control/CHANGELOG.md](https://github.com/JakeTheRabbit/HA-Irrigation-Strategy/blob/v3.18.1/addons/f2_control/CHANGELOG.md).

## Releases

- [3.18.1 2026-10-28](#3-18-1)
- [3.18.0 2026-10-24](#3-18-0)
- [3.17.0 2026-10-21](#3-17-0)
- [Earlier 3.x releases](#earlier-releases)
- [Older releases](#earlier-history)
- [Controller](#controller)

Source: https://cropsteering.growlabs.nz/changelog
