Skip to content
Crop Steering

Changelog

The integration and the controller are released as a matching pair. This page is the project's two changelog files from release v2.19.2, shown as written.

Integration

All notable changes to the Advanced Automated Crop Steering System 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, and this project adheres to Semantic Versioning.

[2.19.2] - 2026-09-21

Pair: controller 0.16.2. Class C3. Everything here comes from two reviews by use: the first real install on a one-zone tent, set up from a phone, and an independent review of 2.19.1 upstream. Nine small changes, each its own pull request with its own tests (#15 to #23). This candidate carries more than one behaviour change (two C3, four C2), which docs/RELEASING.md says a candidate should not; they were bundled by decision of the person running the only staging room, and a failed soak would have to be bisected across them. The defects were seen on real hardware; the fixes have not run on hardware before release. Update with the engine off, read the controller log, then watch the first shot.

๐ŸŒฑ In plain English#

  • Set things up in either order. If the controller app was started before the integration was set up (the order the app store invites), a one-zone tent was shown Zones 2 and 3 that do not exist, each complaining "no hardware mapped", and a minute after setup Settings > Repairs told you to create a kill-switch helper by hand. Both are gone. The controller now waits for a room, invents nothing, and picks the room up by itself within a minute, with no restart. If you saw that Repairs card: do not create the helper. A room made by the wizard has its own kill switch, Engine Enabled; the helper would be a second one that does nothing.
  • A zone is called what you called it. Name a zone "GT1" and its device in Home Assistant was still "Zone 1" or "Crop Steering Zone 1", depending on which part of the integration got there first. It is now the name you typed, and renaming the zone in Configure renames the device. A device you renamed yourself in Home Assistant keeps your name. No entity id changes.
  • Switching a room on no longer creates an irrigation event. New switch-on timestamps are marked explicitly and displayed as unknown until an irrigation is recorded. Older unmarked timestamps are preserved: zero daily counters or missing history cannot establish whether an old timestamp represented irrigation or switch-on. Electrical operation does not prove water delivery.
  • The menu scrolls on a phone. On a small screen the lower half of the side menu (including Help & tools and the version numbers) could not be reached.
  • Configure > Edit parameters no longer locks out a room that steers dry. The number entities accept a P1 target and a P2 threshold as low as 5 %, the form insisted on 30 % and 25 %, and since 2.18.1 it opens on your current values. A room at P1 20 % could not submit the form even unchanged. The form now has exactly the limits of the entities it edits.
  • Assistants using the MCP tools can change how a room is plumbed. A room that had declared its plumbing could never gain or lose its pump through them: the tools did not know the question existed. The plumbing and the pump or main-line mapping now travel together in one reviewed proposal, and a proposal that contradicts the declared plumbing is refused at preview.
  • Two holes in the release checks are closed. A release pull request could also change a default, a dependency or an add-on permission inside the three files that hold the version numbers; and a controller-only release was accepted that could then never be promoted. Every release now raises the integration's number, a controller-only fix included.

๐Ÿ”ง Technical notes#

  • Zones are never invented (#17, C3). The shipped add-on options carry num_zones: 3, documented as "only used if Home Assistant isn't reachable at startup" but also used when Home Assistant answered and the integration simply was not set up yet. New _default_zone_ids(options, descriptor) -> (ids, provisional): the descriptor's authoritative active_zone_ids / num_zones first, then fused sensors (provisional without a descriptor), else a hand-mapped hardware option keeps num_zones, else no zones when Home Assistant answers, else the documented fallback. While provisional the controller rediscovers every loop and re-resolves zones, enable flag and feed sensors. A state file that already holds phantom zones still loads. It also stops the controller's pre-setup zone_N_status states pushing the integration's own status sensor onto sensor.crop_steering_zone_1_status_2 on a first install.
  • Last irrigation is an event (#18, C3). New per-zone state field last_shot_is_anchor (additive: _fresh_zone default False; an old file without it retains its existing timestamp, because daily counters reset and history expires). _room_switched_on still stamps last_shot so the blind-probe schedule counts from switch-on; zone_N_last_irrigation_app publishes unknown while the flag is set, including for a room that is off. No option or entity change. Final review added regression coverage for partial sensor startup, legacy numeric strings and malformed excluded-volume values, and genuine irrigation timestamps after daily rollover.
  • The kill-switch repair judges the right switch (#19, C2). health._kill_switch() trusted the heartbeat's enable_flag over the room's own descriptor. While the heartbeat reports an older setup_revision than the descriptor (both plain ints), the engine holds every zone anyway and adoption moves it to the descriptor's flag, so that flag is the one judged. An up-to-date engine is believed as before (custom add-on enable_flag), a deleted legacy helper is still reported, and a controller too old to report a revision is believed as before.
  • Zone device name (#16, C2). room.zone_device_name(entry, zone_num), used by the button, number and select platforms, which used to register the same device under two different names. The room device is left alone.
  • Edit parameters limits (#20, C2). The four fields read native_min_value / native_max_value from NUMBER_DESCRIPTIONS. Only the lower limits of p1_target_vwc (30 -> 5) and p2_vwc_threshold (25 -> 5) actually move.
  • MCP plumbing (#21, C2, mcp-server/ only). plumbing in the strict input schema; the room carries plumbing ("" = never declared) and plumbing_inferred; config() includes the layout only when declared, which puts it in the payload, the comparison snapshot and the readback digest, and never declares one on the operator's behalf. New tests_ha/test_mcp_setup_contract.py sends that payload to the real setup_save.
  • Sidebar scroll (#15, C1). .desktop-sidebar / .mobile-sidebar get overflow-y: auto and overscroll-behavior: contain, their children flex-shrink: 0; checked in verify-live.mjs at 390x640 and 1280x480. The committed bundle is rebuilt from that source.
  • Release guards (#22, #23, C0). without_version() compares each version file with only its version field blanked, and check_pull_request takes the files' text as a required argument; a release that does not change the integration version is refused, and a release branch is named for the integration version. Both take effect once promoted, because GitHub reads the workflow from main.
  • Upgrade in place. No add-on option and no entity id changes. One additive state-file field (last_shot_is_anchor). A room that never declared its plumbing still publishes byte-for-byte the descriptor it did, and the controller's saved setup fingerprint still matches, so an update resumes without a disarm cycle. Every change above is tested both on a fresh install and from the seeded snapshots of old installs in tests_ha/fixtures/.
  • Documented, not changed. A probe whose reading has not changed for 20 minutes is treated as dead and the zone goes onto the blind schedule. That is deliberate (a probe pulled out of its cube leaves a plant that will need saving), and it also fires on a bench test with no plant in the cube. docs/troubleshooting.md now says so, along with the three first-install symptoms fixed above.

[2.19.1] - 2026-09-21

Pair: controller 0.16.1. Class C3 by the table, because the controller is touched, though what changes there is one new attribute on a sensor it already publishes: no change to irrigation behaviour, options, the state file or any entity id. Not run on hardware before release: update with the engine off, then check the sidebar reads 2.19.1 and 0.16.1.

๐ŸŒฑ In plain English#

  • Crop Steering has its icon. Adding the integration, the integrations page and HACS all showed a grey "icon not available" box. The integration now carries its own icon and logo, with versions that stay readable on a dark theme. Needs Home Assistant 2026.3 or newer; older versions carry on showing the placeholder, and nothing else changes for them.
  • You can see what you are running without leaving the dashboard. The sidebar, under Help & tools, now shows the integration version and the controller version. Both come from the parts that are actually running, not from the page, so it cannot show a version you have not got, and a half that is too old to say (or a controller that is not running) reads "not reported". After an update it is the quickest check that both halves really moved.

๐Ÿ”ง Technical notes#

  • New custom_components/crop_steering/brand/ (icon, logo, @2x, and dark_ variants), which Home Assistant 2026.3+ serves for a custom integration with no manifest change. Built from img/crop-steering-logo.png by scripts/make_brand_images.py: the icon is the emblem alone (it is shown at about 40 px), the logo keeps the wordmark, dark variants sit on a white rounded tile, all reduced to 256 colours with alpha (10-53 KB each). tests/test_brand_images.py pins names and sizes from the PNG headers. Class C1: nothing the controller reads.
  • Versions in the sidebar. The integration publishes integration_version in each room's engine_config descriptor (from SOFTWARE_VERSION, already tied to manifest.json by the version tests). The controller publishes controller_version in each room's ai_heartbeat and logs it on start; it reads the number from the config.yaml it was built from, which the Dockerfile now copies into the image as /app/addon.yaml. No number is duplicated anywhere, so a release has nothing extra to bump; the tests fail if the report and config.yaml disagree or the Dockerfile stops shipping the file. The descriptor attribute is proven not to enter the controller's setup fingerprint, so an integration update still resumes without a disarm cycle. Class C3 by the table (the controller and its Dockerfile are touched), though the change is one new attribute on a sensor the controller already publishes: no state-file, option or entity-id change.

[2.19.0] - 2026-09-21

Pair: controller 0.16.0. It also carries 2.18.1 / controller 0.15.2, which was never published by itself. Class C3. Released without a staging soak by decision of the two people who run it; see the record. Not run on hardware before release: treat the first update of each box as the first run. Engine off, update, check the log, watch the first shot.

๐ŸŒฑ In plain English#

  • Setup now asks how your room is plumbed, and holds you to the answer. 2.18.0 worked it out from what you left empty: no pump chosen meant "this room has no pump". That is right for a tent on one smart plug. It is silently wrong for a room that does have a pump and whose pump was never chosen, or was cleared by mistake: the controller opened the valve, ran no pump, and counted the water as delivered while the plants got none. The wizard and Rooms & setup now ask the question outright (zone valves only; a pump, then zone valves; a main-line valve, then zone valves; or all three), and the switches you choose have to match your answer.
  • If they ever stop matching, the room is held, with a reason. A room that says it has a pump and has none mapped is not watered, nothing is counted as delivered, and you get a notification saying what to fix. It used to look healthy until the plants wilted.
  • Nothing changes for a room that is already set up. It carries on exactly as it does today, and an update does not need the kill switch cycled. Open Rooms & setup (or Configure) when it suits you: it shows what your current switches imply and asks you to confirm. From then on the protection above applies to that room too.

๐Ÿ”ง Technical notes#

  • New optional plumbing in a room's setup and, only when declared, in the engine descriptor: valves_only, pump_valves, mainline_valves, pump_mainline_valves (custom_components/crop_steering/plumbing.py; the controller carries the same table and a test reads both). prepare_setup refuses a save whose mapped switches contradict a declared layout, both ways, for an active room. Once declared it can be changed, not withdrawn; a client that does not send it keeps the stored value.
  • Controller: plumbing_hold() gates _blocked and, as a last line of defence, _execute_shot. A contradiction or a layout the controller does not know holds the room, alerts (f2_plumbing_<room>, at most every 30 minutes), opens nothing and counts nothing. A mapped switch the declaration disowns is still safed on exit and still has to read OFF for adoption.
  • Upgrade in place: an undeclared room publishes byte-for-byte the descriptor it did (key set pinned in tests/test_plumbing.py), and plumbing joins the setup fingerprint only when present, so the fingerprint saved by controller 0.15.x still matches and the room resumes without a disarm cycle. Proven in tests_ha/ from seeded snapshots: a one-switch tent recorded by running upstream 2.18.0's own wizard (fixtures/entry_2_18_one_switch_tent.json, fingerprint computed by controller 0.15.1's code) and the 2.17 pumped room. Declaring later is an ordinary setup change (new revision, adopted with the kill switch and hardware OFF).
  • Wizard and Configure: a required list question at the top of the hardware step; no prefill for a new room; Configure prefills the declared layout or what the saved switches imply. Clearing the pump of a room declared with one is refused in the form. Rooms & setup asks only when setup_read reports the plumbing capability, so a newer dashboard served by the add-on beside an older integration neither asks nor sends it.
  • Not changed: the env-file path (rooms configured that way stay undeclared until saved through a form), add-on options, the state file format, every entity id.
  • Not run on hardware. Everything above is proven against a real Home Assistant and the real controller code with a fake switch layer. No physical pump or valve has been driven by this build.

[2.18.1] - 2026-09-21

Pair with controller 0.15.2 (0.

Pair with controller 0.15.2 (0.15.1 plus a test-only seam; irrigation behaviour is unchanged). Bug fixes only. Nothing an operator has set moves: every fix below was checked against seeded snapshots of older installs. Each was reproduced on 2.18.0 inside a real Home Assistant before it was fixed; the write-up is docs/audits/2026-09-21-first-run-review.md.

๐ŸŒฑ In plain English

  • The integration starts on older Home Assistant. On anything before Home Assistant 2026.5 the setup wizard finished and the integration then showed "Failed to set up": no entities, no dashboard. It lists 2024.3 as supported, and now it is.
  • Your lights times are used. The wizard asks when your lights turn on and off, stored the answer, and then always ran on 12 and 0. The grow-day, the morning dry-back and the overnight phase now follow the hours you typed. If you already set them on a dashboard, those are kept.
  • "Edit parameters" does something. Changing a value under Configure said "saved" and quietly put the old value back. It now changes what the controller reads, and the form opens on the current value rather than the one from the day you installed.
  • A sensor or pump can be removed, not just swapped. Under Configure you could replace a mapping but never clear it: it came straight back. That matters more now that a pump is optional.
  • Boxes that did nothing say so. The waste-valve box promised the valve is "forced closed during a shot". The controller has never operated it, so if your plumbing relies on that, arrange it yourself. The grow-light, notification, humidity and VPD boxes are likewise marked as recorded only.
  • The pump box says what leaving it empty means. Since 2.18.0 an empty pump is accepted and the controller then never runs one: it opens the zone switch and counts the shot as delivered. That is right for a one-switch tent and wrong for a room that has a pump, so the box now says so where you choose it.
  • Every box is explained. The feed EC and feed pH pickers showed their raw names (feed_ec_sensor) with no label at all, and each zone's name and "in use" box had no help text. Two Configure messages showed as raw keys.
  • Small pots can be adjusted. A 0.65 L rockwool cube was accepted by setup and then could not be edited, because the setting's minimum was 1 L.

๐Ÿ”ง Technical notes

  • setup_panel: frontend.async_panel_exists was added in Home Assistant 2026.5.0 (absent from core tags 2024.3.0, 2025.1.0, 2026.2.3, 2026.3.0, 2026.4.0) and was called unconditionally inside async_setup_entry: AttributeError, entry state SETUP_ERROR. _panel_exists() uses the helper when it exists and otherwise PANEL in hass.data[frontend.DATA_PANELS], which is what the helper does. Neither test tier could see it: tests/test_setup_panel.py assigns the function onto its own stub, and tests_ha/conftest.py replaced the whole panel registration with a no-op. tests_ha now stands in for the web server only and runs the registration against the real frontend module; with the fix reverted, that tier fails.
  • number.PARAM_TO_ENTITY_KEY gains lights_on_hour / lights_off_hour. A seed only applies to an entity being created for the first time (RestoreEntity wins afterwards); pinned by a seeded upgrade where setup recorded 10-22, the operator set 8-20, and 8-20 survives.
  • OptionsFlowHandler.async_step_edit_parameters calls number.set_value on the live entities before _update, so the reload restores the value just written; defaults come from the live entities. The OFF check and its abort are unchanged, and nothing is written when it refuses.
  • _hardware_schema._ent prefills with description={"suggested_value": ...} instead of default=. The frontend omits an emptied field and voluptuous re-applied the default. test_native_hardware_schema_retains_explicit_tank_telemetry now asserts the same intent (the form opens showing the mapping) and additionally that the field can be cleared.
  • Translations: options.abort.not_env_config / reload_failed; labels and tooltips for feed_ec_sensor / feed_ph_sensor on both mapping forms; tooltips for all 24 zone_N_name / zone_N_active; truthful text for waste_switch, light_entity, notification_service, humidity_sensor, vpd_sensor (no runtime consumer in the integration, the add-on or the engine) and for pump_switch.
  • Global substrate_volume minimum 1.0 -> 0.1 and drippers_per_plant maximum 6 -> 20, matching setup_api.SIZING and the per-zone entities.
  • Tests, lean: tests/test_translations.py (every abort reason, error key and menu entry has a message in the flow that raises it; every mapping-form field has a label and a tooltip; hassfest's own key, quoted-placeholder and orphan-tooltip patterns; a field with no runtime consumer may not promise behaviour, and fails the day one gains a consumer). An honest pre-2026.5 case in tests/test_setup_panel.py.
  • Tests, real Home Assistant (tests_ha/, 4 -> 30): test_setup_entry.py, test_configure.py, test_upgrade_in_place.py driven by seeded snapshots in tests_ha/fixtures/ (a 2.17 wizard room and an env-file era room: tuned values, an operator-renamed entity, a unit-less probe, and controller 0.14's saved setup fingerprint resuming with the kill switch ON), and test_install_to_controller.py, which hands a freshly installed room to the real add-on controller and requires it to find, adopt and water it.
  • Controller test seam: F2_STATE_PATH (see the add-on changelog). Docs: docs/TESTING.md describes the real-Home-Assistant tier, the fixtures and running hassfest without Docker.
  • Not changed, raised for a decision: an unmapped pump is read as "this room has no pump" (2.18.0). See the audit for a demonstration and three options.

[2.18.0] - 2026-09-21

Pair with controller 0.15.1 (0.

Pair with controller 0.15.1 (0.15.0 plus one fix: a room switched off stays off while Home Assistant restarts).

๐ŸŒฑ In plain English

  • The setup wizard no longer throws your work away. If something was wrong at the end (a valve that was on, a probe in the wrong unit, a mistyped entity), the wizard closed and every zone, sensor and size you had entered was gone. It now shows the same step again with everything still filled in and says what to fix. Problems are reported on the step where you entered them, not three screens later, and the message says whether the entity is on, unreachable or does not exist instead of always "must read OFF".
  • A brand-new install is found by the controller. On a fresh install Home Assistant named most of this integration's entities from their labels (number.p1_target_vwc, sensor.engine_config) instead of the crop_steering_ ids the controller and dashboard read, so a new room was never discovered and never watered. New installs and new rooms now register under the documented ids. Existing rooms are untouched: Home Assistant keeps the ids it already holds. If you first installed on 2.17 or earlier and your room was never found, update, then remove the room and add it again.
  • A tent with one switch works. A room whose only hardware is one smart plug or solenoid per zone saved fine and then never watered, because the controller insisted on a separate pump and main-line valve. A zone now needs only its valve; pump and main-line are used when you have them. Every safety check still applies to whatever hardware the room has.
  • Probes in other units are converted, not rejected. Pore EC in ยตS/cm and moisture reported as a 0-1 volume fraction are accepted and converted to mS/cm and percent, including mixed probes in one zone and the source-water EC probe. ppm is still refused, with the reason: the 500 or 700 scale is not something a sensor reports.
  • The Cloudflare judge now looks after P2. With Auto Setpoints on and a Cloudflare token in the controller's options, the typesafe/jev model is asked once an hour during P2 whether pore EC should be flushed or stacked and whether the peak still fits. It may nudge that zone's P2 shot size (1-4 % of substrate) and hold the working peak up to 2 points above or below the learned one: one small step per lever per grow-day, never while a dated plan owns the room. It cannot fire, size or delay a shot. If a guard trips (probe not believable, water not landing) or Cloudflare does not answer within 5 seconds, nothing changes. The zone's Auto Setpoints status shows what it said and what it changed today.
  • Less typing, fewer wrong numbers in Rooms & setup. Choose litres or US gallons and L/h or GPH (always saved as metric); pick a common block or pot with its litres shown; work out real dripper flow from a catch test; and see the zone's learned peak as a suggestion beside field capacity. Suggestions are never applied for you.

๐Ÿ”ง Technical notes

  • config_flow: _retry_form re-shows a step through add_suggested_values_to_schema with errors.base = setup_invalid; the zones step validates with prepare_setup and safety_blockers before moving on; the reconfigure zone map does the same. safety_blockers messages keep "must read OFF" and append the cause.
  • Controller: pump and mainline are optional in discovery, late mapping, setup adoption, the per-zone gate and _execute_shot; lead times are skipped with the hardware they belong to, the close read-back and the hardware-fault latch cover the actuators that exist. The three-switch sequence and its timing are unchanged (tested).
  • Controller _auto_tick: hourly P2 consult (auto_setpoints.jev_due / jev_verdict), p2_shot_size joins managed while the judge is configured, working_peak = learned peak + peak_adj drives the P1 target and the P2 threshold. New status attributes jev_last, jev_changed_today, working_peak_adjust. Learned state restores across restarts.
  • New units.py: exact conversions only (ยตS/cm, Greek-mu ฮผS/cm, uS/cm -> mS/cm; mยณ/mยณ -> %). Unknown or missing units pass through unchanged so older installs keep their readings. The controller converts the source-water EC probe the same way before its 0-20 sanity range.
  • number, select, sensor and button set self.entity_id from the object id they already computed. Home Assistant ignores _attr_object_id (the 2.17.1 switch fix, now everywhere): in a real Home Assistant 75 of a one-zone room's 134 entities registered under label-derived ids. Found by the new real-HA test job; a permanent test asserts every entity lands on the id its code asks for.
  • CI: a real-home-assistant job runs tests_ha/ in a real Home Assistant (pytest-homeassistant-custom-component, Python 3.13): the wizard end to end for a one-switch room, and the registered id of every entity. The sidebar panel is stubbed there (it needs the frontend wheel and a web server). The stub suite could not see the 2.17.0 entity-id bug; this can.

[2.17.2] - 2026-09-20

Pair with controller 0.14.0 (unchanged).

Documentation only; no code change. Pair with controller 0.14.0 (unchanged).

  • Feature matrix: rows for the 2.17 features with what is tested and what was exercised live on 20 September (room off, restart-safe setup, patient read-back), what was not (the full P1 ramp through a live lights-on, Auto Setpoints writing live), and an updated live deployment row.
  • Planning guide: "Read the combined graph" now describes the projected P0-P3 day, the overnight lines and the P2 threshold note, replacing text from before 2.16.1.
  • Entity reference: room_active, auto_setpoints and the per-zone auto_setpoints sensor.
  • User guide, sidebar guide and the controller's documentation tab no longer describe the Today / Schedule navigation as awaiting verification or use the old page names.

[2.17.1] - 2026-09-20

Pair with controller 0.14.0 (unchanged).

Pair with controller 0.14.0 (unchanged).

  • Fix: the new Room Active and Auto Setpoints switches registered under ids made from their labels (switch.crop_steering_room_active_off_empty_room_no_irrigation_no_alerts), which the controller and dashboard never look for, so the room on/off control did nothing on a fresh 2.17.0 install. Switches now suggest switch.crop_steering_<prefix><key> when first registered. Found on the first live install, where the four entities were renamed in the entity registry.
  • If you installed 2.17.0: rename the two switches per room to switch.crop_steering_<prefix>room_active and switch.crop_steering_<prefix>auto_setpoints in Settings > Entities (an entity already registered keeps its id), or remove them and restart on 2.17.1.

[2.17.0] - 2026-09-20

Pair with controller 0.14.0.

Pair with controller 0.14.0.

๐ŸŒฑ In plain English

  • Room on/off. Each room has a Room Active switch. Turn it off when nothing is growing: no irrigation (scheduled, emergency or blind-probe fallback), no alerts, no repair issues, and the room's open notifications are dismissed. Turn it back on and the room starts a clean cycle from the overnight phase; water history is kept.
  • P1 always runs in full. The ramp no longer ends on a clock. However late the first shot lands, P1 fires its shots in order until the target is recovered (after at least the new minimum shot count) or the maximum shot count is reached. Only then does P2 start.
  • See the sensor where you set the target. The Today graph you drag targets on now draws that zone's recorded VWC and pore EC underneath them (this grow-day and the previous one), with now, peak and trough above it, on an axis scaled to the readings instead of 0-100 %. A deeper 24 h / 72 h / 7 d history panel sits below, and setpoint fields warn when a target sits outside what the probe reads.
  • The whole day is drawn the way it runs. P0 keeps drying after lights-on, P1 climbs one step per shot (all of them), P2 fires a shot each time VWC falls to its threshold, and P3 dries down overnight to the next lights-on. Timing uses the zone's own measured dry-down rate, so it is a projection, not a schedule. Hover any riser for its time and size.
  • A restart no longer strands irrigation. The controller used to forget which setup it had accepted whenever it restarted, then refuse to water until the kill switch was turned off and on again, without saying so. On 2026-09-20 a host reboot cost F2 two hours of its morning ramp that way. It now remembers the setup it accepted and carries on after a restart if nothing changed (pump, mainline and valves must still read off). A genuinely changed setup still needs the off-and-on, and now says so with a notification naming exactly what has to read off. The first start after this upgrade still needs one off-and-on, because the old build saved nothing to remember.
  • A slow pump report no longer stops the room. After a shot the controller checks that pump, mainline and valve all read off. It used to look once, a second later, and a Zigbee plug that answered in 1.6 seconds latched a false hardware hold that stopped F2 for 16 hours. It now looks at 1 second as before and then keeps re-reading for up to 6, so a late report passes and a genuinely stuck valve still latches within the same minute.
  • When P2 shows no sawtooth, the graph says why. The engine fires a P2 shot only once VWC has dried down to the P2 threshold. A threshold far under the P1 target spends the whole window drying, so the graph now draws the threshold across P2, says how many points and hours away it is, and names the threshold that gives shots from the start of P2.
  • Auto Setpoints (off by default). The controller learns each zone's real ceiling, what a shot lifts it, and how fast it dries. When a P1 ramp stops rising for two shots it hands over to P2 and carries the achieved peak forward as the P1 target, then probes 1 point higher after 3 days. It only ever rewrites per-zone target numbers; the engine still decides every shot.

๐Ÿ”ง Technical notes

  • switch.crop_steering_<prefix>room_active (default on) and switch.crop_steering_<prefix>auto_setpoints (default off). health.py clears the room's repair issues while the room is off; the controller publishes app_status: room_off and a room_active heartbeat attribute.
  • Engine core: P1 time exit removed; ZoneParams.p1_min_shots (from number.โ€ฆp1_minimum_shots, clamped to p1_max_shots). Both vendored copies stay byte-identical.
  • New pure modules in the controller: auto_setpoints (learner), setpoint_supervisor (bounded, stepped, ladder-safe writes), curve_tracker (day planner), engine_twin (test twin around the real decide()), jev_policy (optional Cloudflare typesafe/jev judge, consulted only on a plateau; any failure returns no verdict and never blocks irrigation).
  • Gain is learned only from ramp shots fired at least 2 points under the ceiling; dryback rates fold in once per grow-day as that day's mean. Near-ceiling top-ups no longer shrink the P2 band.
  • Publishes sensor.crop_steering_<prefix>zone_<N>_auto_setpoints (off / learning / tracking / frozen) with learned_peak, gain, day_rate, night_rate, p1_outcome, hold_days, frozen_reason, last_change, jev, managed.
  • Add-on options cf_account_id, cf_api_token, cf_gateway_id (all optional). Auto Setpoints never writes while a dated plan owns the room.
  • Dashboard: foldRecorded, smoothRecorded, dryRates, projectDay and planningAxis in frontend/src/lib/planning-curve.ts; the dryback target is measured from the projected peak (as the engine measures it from the recorded one) rather than from field capacity. Lines carry a scale-free data-planning-values signature because the axis now follows the data.
  • Fix: the live history request had no end_time, so Home Assistant returned only the first 24 h of a 72 h or 7 d window.
  • Controller: the adopted setup_revision is saved per room with a fingerprint of what was adopted (_setup in /data/state.json: pump, mainline, valves, enable flag, active zones, feed sensors). Same revision and fingerprint after a restart is resumed without the engine flag reading off; hardware must still read off. Malformed or missing records keep the full fail-safe. A pending setup raises f2_setup_<room> (debounced, dismissed on adoption, silent for archived rooms), and the per-zone hold line carries [blocked: ...] in every phase.
  • Controller _confirm_switches: first read at 1 s, then every 0.5 s to a 6 s deadline (CONFIRM_FIRST_READ_S, CONFIRM_POLL_S, CONFIRM_TIMEOUT_S). Regression tests cover a 1.6 s report (no latch) and a pump that never reports off (latches, bounded).
  • Dashboard: p2Advice explains a missing or late P2 sawtooth; nominal dry-down is now 2 / 1 points per hour (lights on / off) until a zone has history, replacing 0.7 / 0.35 taken from one low-light week.

[2.16.1] - 2026-09-08

  • Replace competing Manual setpoints and Grow plan navigation with Irrigation plan: Today and Schedule.
  • Show effective active schedule targets in Today; hide misleading fallback controls while a schedule owns the room.
  • Connect VWC and dashed EC planning references across lights-off and overnight to the next lights-on. Missing EC anchors remain gaps; overnight EC is an interpolation, not a prediction.
  • Keep emergency-floor edits and saved-reference overlays independent. Preserve legacy routes and unsaved-draft navigation guards.
  • Bundle the same dashboard in controller 0.13.3; no controller decision changes.

[2.16.0] - 2026-09-08

Pair with controller 0.13.2 for the bundled dashboard and timezone-aware irrigation event publication.

  • Add a local MCP connector for LLM-assisted configuration with reviewed, room-scoped proposals and opt-in writes.

  • Seed the isolated demo with synthetic named recipes and current/previous runs; live libraries remain unseeded.

  • Temporarily collapse the Home Assistant sidebar while embedded, with persistent desktop/mobile menu access and restore on leaving.

  • Add graphical room tank level, pump and fill status, tank EC/pH/temperature, and recorded fill completion time to Overview.

  • Show controller state, mapped valve status and last irrigation time in zone tables, mobile cards and details.

  • Add optional room-specific tank telemetry mappings, separate from feed-water safety gates. Unknown data stays unknown; level changes are never presented as fill events.

  • Pair with controller 0.13.2 for the bundled dashboard and timezone-aware irrigation event publication.

[2.15.0] - 2026-09-08

Pair with controller 0.13.1.

  • Add an empty, room-scoped library for saving and reusing user-authored plans as local drafts. Browser storage is separate for live and demo; loading preserves the current zone start dates and uses existing plan validation/review.
  • Support the legacy same-room maximum_shot_duration entity alongside the canonical name in the controller and runtime calculator. Canonical entities take precedence; invalid configured values do not silently acquire another room's cap.
  • Pair with controller 0.13.1. No crop-guide numerical presets or publisher endorsement are included.

[2.14.0] - 2026-09-08

๐ŸŒฑ In plain English. See the whole day while editing setpoints: the draft VWC/EC curves and P3 emergency floor move immediately beside the saved reference. Compare retained readings over a day, week, month or run-to-date with another run at the same grow age. Water cards distinguish total zone delivery, average per plant and pot capacity, with a local runtime calculator.

๐Ÿ”ง Technical notes โ€” integration 2.14.0, controller 0.13.0.

  • Add phase-focused manual editing, bounds-aware graph handles, saved/draft overlays and read-only active-plan previews.
  • Add room-scoped, revisioned run metadata with captured target references and Recorder comparisons. Recorded history remains subject to retention; a reference captured today is not a historical target audit.
  • Add explicit all-plant daily zone litres, per-plant averages, nominal phase-shot volumes and capped runtime estimates. P1 series budgets are conditional; daily adaptive shot counts are not predicted.
  • Count new delivered litres from configured flow and elapsed runtime, including duration caps, fractional-second truncation, minimum runtimes and partial aborts. Freeze sizing per shot to prevent in-flight configuration edits changing its recorded volume. Existing totals are preserved.
  • Keep all edits local until reviewed; comparison registration and runtime calculators do not activate irrigation.

[2.13.2] - 2026-09-08

  • Show each room's configured name in the workspace instead of a generic sensor label.
  • Package the corrected workspace in controller 0.12.1; irrigation logic is unchanged from 0.12.0.
  • Document the verified in-place upgrade, preserved settings and consolidated branches.

[2.13.1] - 2026-09-08

๐ŸŒฑ In plain English. Fix a startup failure when multiple rooms load at once. Every room can now share the native sidebar reliably. Controller 0.12.0 remains the matching version.

๐Ÿ”ง Technical notes. Serialize sidebar/static-path registration across concurrent config-entry setup. Live installation exposed the duplicate-panel exception; deterministic concurrent-startup and retry tests cover the correction.

[2.13.0] - 2026-09-08

๐ŸŒฑ In plain English. One Home Assistant native workspace brings room setup, current readings and whole-grow planning together. The combined VWC/EC planning graph follows each zone's selected day, week and steering profile. Existing installations keep their room identities, setpoints and hydraulic settings.

๐Ÿ”ง Technical notes โ€” integration 2.13.0, controller 0.12.0.

  • Replace the dashboard family with one React/shadcn operator workspace using inherited Home Assistant themes, bundled fonts and responsive layouts.
  • Add reactive combined VWC/EC planning curves, recorded dual-axis history and per-zone day/week recipes with continuous steering between explicit endpoint profiles.
  • Add durable plan storage, revisioned preview/save/arm/disarm services and atomic expiring controller snapshots applied at local lights-on boundaries.
  • Add reviewed room/zone lifecycle and searchable sensor mapping with stable IDs, archived restoration, per-zone sizing and controller adoption status.
  • Add local catch-test calculations, sensor diagnostics and equipment maps; remove unsupported yield/potency claims from the active UI.
  • Correct duration accounting, volume-cap bypasses, shared-hardware fault recovery, relative-dryback timing conversion, low-flow sizing and stale sequential-plan decisions.
  • Bundle the dashboard in the integration with automatic sidebar registration, publish app-repository metadata, consolidate install/operation instructions, and archive superseded assets with provenance.
  • Resolve setup hydraulics from the current room/zone number entities so a rename or mapping edit preserves live plant counts, pot size and dripper settings.
  • Archive unused facility examples, environment templates and disabled workflows; add current README screenshots and a public interactive demo link.
  • Preserve base VWC shot sizes while EC is unknown, suspend EC adaptation, and expose degraded EC status (#37).
  • Reject nonfinite or invalid/stale/future-dated feed readings; describe arithmetic sensor averaging and relative dryback accurately (#38, #39).
  • Persist timed manual override deadlines across restart/reload and cancel obsolete callbacks on retrigger/manual changes (#40).
  • Track rolling seven grow-day delivery estimates with explicit partial-history coverage; missing weekly sources remain unknown (#41).
  • Audit all branch tips and retain recoverable archives; use tracked-only controller release packaging.
  • See docs/FEATURE_MATRIX.md for validation evidence and live commissioning limits.

[2.12.0] - 2026-07-03

๐ŸŒฑ In plain English. This release makes the system genuinely portable to any Home Assistant โ€” not just the facility it was built on โ€” and closes the production-audit findings (#14โ€“#31). The biggest changes you'll feel: the engine now learns your pump and valves from the integration's setup wizard (no more hidden facility defaults), the kill switch stops water mid-shot within a couple of seconds instead of after the full shot, services can target a specific room on multi-room installs, and a generated dashboard covers however many zones you actually have. Fixes a crash that broke every service call on current Home Assistant versions.

๐Ÿ”ง Technical notes โ€” integration 2.12.0.

  • Fix (HA compat): replaced the removed hass.helpers.template.now() with homeassistant.util.dt.now() in all six service handlers โ€” transition_phase, execute_irrigation_shot, custom_shot, set_manual_override and friends crashed with AttributeError on current HA cores (#15).
  • Multi-room services: every service accepts an optional room slug (documented in services.yaml); entity targets and computed sensors resolve the room's prefix instead of hardcoding the default room's ids. An unknown room slug raises instead of silently steering the default room (#16). Event payloads gain a room key (additive).
  • Recipes: apply_recipe / save_recipe raise HomeAssistantError when the recipe store is unavailable instead of silently doing nothing (#27).
  • Health / Repairs: the kill-switch and heartbeat checks resolve per room โ€” preferring the enable_flag the running engine publishes on its heartbeat, then the room's engine_config descriptor โ€” so custom kill switches and additional rooms are all monitored, with no false "kill switch missing" cards (#22).
  • Config flow: .env / config.yaml reads moved off the event loop via async_add_executor_job (#25).
  • Metadata / hygiene: dead SERVICE_* constants replaced with the real service names; iot_class corrected to calculated (#31). .gitignore no longer ignores the shipped custom_components/ and www/ directories (new files were silently invisible to git); five dashboard files that ship via the publish script are now actually tracked (#29).
  • Dashboards: scripts/build_lovelace.py is zone-count-driven (1โ€“24, no hardcoded [1,2,3]), per-room via CROP_STEERING_PREFIX, and free of facility entity ids; the phantom input_select.growth_phase / nutrient_phase "selectors disagree" false alarm is gone from the generator and the committed sample. The static dashboards/ and packages/ files are marked as F2 facility examples (#23, #24).
  • CI: the add-on image is built on every PR; the vendored engine copy is diffed against the source package; repo-hygiene gate; the "Full Installation Test" now runs the real test suites instead of echoing success (#17, #18, #19). New dependency-free fake-HA harness drives the real service handlers, health checks, and add-on controller (tests/, addons/f2_control/tests/).

Crop Steering add-on 0.11.0 (paired with this release)#

๐ŸŒฑ In plain English. The engine no longer carries the original facility inside it. It learns your pump and valves from the Crop Steering integration, holds safe (with a clear message) if nothing is mapped yet, reacts to the kill switch during a shot, notices new rooms without a restart, and tells you loudly if a setting entity has gone missing instead of quietly running on built-in defaults.

โš ๏ธ Upgrade notes (read before Rebuild).

  • Update the integration first. The engine reads the default room's pump/mainline/valves from sensor.crop_steering_engine_config. Rebuilding the add-on while running an old integration that doesn't publish that sensor leaves the default room held safe (no watering) until the integration is updated โ€” the engine re-checks every 5 minutes and resumes on its own.
  • notify_service now defaults to empty. Installs that explicitly set it (F2 does) are unchanged; unset = persistent notifications only, no mobile push.
  • Dosing/fill/flush holds moved to the hold_entities option (empty default). The four previously hardcoded F2 entities (input_boolean.nutrient_dosing_active, input_boolean.f2_fill_mode, input_boolean.f2_flush_mode, switch.tank_filling) are no longer checked automatically โ€” F2 must add them to hold_entities in the add-on Configuration or its tank-dosing holds go dark after the rebuild (same pattern as the v0.8.0 feed-sensor change).

๐Ÿ”ง Technical notes.

  • Default-room hardware from the integration descriptor; hardcoded F2 pump/valve fallback removed; unmapped room โ†’ _blocked() returns "no hardware mapped" and _safe_off() tolerates it (#14).
  • _wait_shot(): shots delivered in โ‰ค2 s slices re-checking the kill switch and the zone's manual override; aborts run the normal close sequence and count delivered volume proportionally (#20).
  • Periodic _rediscover() (default 300 s, rediscover_seconds option): late/UI-added rooms join fail-safe OFF; an unmapped default room picks up its hardware once the integration is up (#26).
  • Missing-setpoint surfacing: a setpoint entity absent (per-zone AND global) for โ‰ฅ3 consecutive loops raises one rate-limited alert + a vitals line; engine-only knobs (min_floor_drown_ceiling) are exempt by design (#27).
  • Timezone hardening: tzdata in the image; startup logs the effective local offset and alerts if it disagrees with HA's configured zone (#28).
  • Heartbeat sensors now publish the room's enable_flag (consumed by the integration's health checks). Vitals branding via instance_name option; sensor.f2_control_vitals id unchanged.
  • Packaging: tracked placeholder www/public/index.html so docker build works from a clean checkout; the publish script still overlays the full dashboard set (#17). Version 0.10.5 โ†’ 0.11.0.
Earlier history (20 entries, Unreleased back to 1.x)

[Unreleased]

Crop Steering add-on 0.10.5#

๐ŸŒฑ In plain English. Fixes a serious silent failure: on some installs the engine couldn't find the zone moisture probes at all, so the zones froze in whatever phase they were in (they never even went to overnight dryback at lights-off) and watering fell back to a dumb timer. This happened when a box was first set up under an older version โ€” Home Assistant keeps the original sensor names forever, and the engine was only looking for the new ones. The engine now finds your probes under either naming, and even if a probe truly dies, the daily light-cycle phases keep moving and you get a repeating phone alert (instead of one easy-to-miss ping) telling you exactly which sensor it wants.

๐Ÿ”ง Technical notes.

  • New Controller._fused_id(): resolves each zone's fused sensor by probing HA for the current id (sensor.crop_steering_<room>vwc_zone_N) then the registry-sticky legacy id (..._zone_N_vwc); caches the hit; re-resolves while neither exists yet (HA still booting); an explicit zones-option override wins. _detect_zones counts zones under either convention.
  • New Controller._blind_time_transition(): a blind zone (no readable VWC) still honours the time-based phase forces โ€” lights-off โ†’ P3, and P3 โ†’ P0 at the new photoperiod including the daily counter/EC-state reset โ€” so a dead probe can never strand the daily cycle. VWC-driven transitions stay paused while blind.
  • The blind alert re-fires on the _alert 30-min debounce (was once-per-blindness) and names the exact entity_id being looked for.
  • Regression-locked by tests/test_blind_and_fused.py (11 cases: both naming conventions, override, cache/re-resolve semantics, blind P3/P0 forces + counter reset, mid-day hold).

Crop Steering add-on 0.10.4#

๐ŸŒฑ In plain English. Dashboard fixes the operator asked for: the Engine Log panel now shows the engine's live decision feed (it was always empty before), the "next" value on the irrigation- frequency card is populated, and (with 0.10.2) the per-zone Volume fed + shots tiles work. Plus a safety improvement: if a valve can't be confirmed closed (e.g. HA drops mid-shot) the engine now raises a CRITICAL alert instead of silently assuming it closed.

๐Ÿ”ง Technical notes.

  • Engine Log โ†’ reads sensor.crop_steering_activity_log (feed attr) instead of the unwritable /local/f2_engine.log; change-gated (also removes the 4s re-render churn โ€” a lag suspect).
  • New per-zone prediction_zone_N_next_irrigation_hours published from decide() inputs (P2 dry-to- threshold รท dryback rate).
  • _execute_shot close sequence: check every turn_off + HA-aware read-back; alert on failed/unconfirmed close (don't let a dead-HA read mask a stuck-open valve). Shot still counted so the daily cap stays honest. Software can't close a valve when HA is down โ€” hardware fail-safe is the guarantee (NC valves, pump default-off, independent watchdog).
  • core.py: comment documenting that the MIN-DAILY floor and the daily-volume cap are mutually exclusive by construction (the audit-flagged "floor under-water" case is unreachable โ€” min_daily_volume <= max_daily_volume clamp + the floor only fires below min). No behaviour change.

Crop Steering add-on 0.10.2#

๐ŸŒฑ In plain English. Fixes the per-zone "Volume fed vs daily cap" and "Irrigation frequency" tiles showing blank (โ€”). The engine now publishes each zone's litres-fed-today + shots-today.

๐Ÿ”ง Technical notes. _publish_status now emits sensor.crop_steering_<room>_zone_N_daily_water_app (L, from state.daily_vol) and ..._irrigation_count_app (from state.shots) per zone โ€” the dashboard already read them; they just weren't being republished. Reset at the P3โ†’P0 lights-on rollover.

Crop Steering add-on 0.10.1#

๐ŸŒฑ In plain English. Per-room dashboards. View (and drive) any additional room by adding ?room=<slug> to the dashboard URL โ€” e.g. f2.html?room=f1 or overview.html?room=f1. A green badge shows which room you're looking at, and the kill-switch button controls that room's kill switch.

๐Ÿ”ง Technical notes. Two chokepoints in www/f2.html + www/overview.html โ€” fetchStates aliases every crop_steering_<slug>_* entity onto the un-prefixed id the UI reads, and svc/callService rewrites entity_id on writes โ€” so all ~150 entity references scope to the room without per-id edits. The kill switch is resolved per room explicitly (switch.crop_steering_<slug>_engine_enabled vs the default input_boolean.f2_control_enabled). No ?room= โ‡’ identity no-op (default room unchanged). Facility tiles (camera/tank/climate, not crop_steering_-namespaced) stay on the default room.

[2.11.0] - 2026-06-29

๐ŸŒฑ In plain English. Multiple rooms are now autonomously steered. Until now you could configure a second grow room (add the integration again), but the engine only ever watered the first one. Now the one add-on drives every room โ€” each with its own pump/mainline/valves, reservoir pH/EC probes, photoperiod, kill switch and durable state, fully isolated. A new room comes up fail-safe OFF (its own kill switch starts off) so nothing waters until you arm it. Your existing room is completely unchanged.

๐Ÿ”ง Technical notes.

  • Integration: each room now publishes a sensor.crop_steering_<prefix>engine_config whose attributes carry the room's hardware map (pump / mainline / per-zone valves), kill switch, feed probes and zone count โ€” the add-on can't read the config entry directly, so this is how it discovers a room. Additional rooms also get a per-room kill switch switch.crop_steering_<slug>_engine_enabled (default OFF); the default room keeps input_boolean.f2_control_enabled unchanged. The setup wizard's hardware step now also collects optional per-room source-water EC/pH probes.
  • Add-on (engine 0.10.0): refactored the controller around a Room abstraction; loop_once now iterates rooms (_loop_room). Every per-zone entity read/write is namespaced by the room prefix (sensors, numbers, selects, switches, the gates, the published status). Lights, feed gate, kill switch, Vmax tracking and blind-zone handling are all per-room. The default room is built from the add-on options exactly as before (prefix "" โ‡’ byte-identical entity ids), and additional rooms are discovered from the published descriptors. Shots still serialise across rooms (one _busy lock).
  • State: /data/state.json is now nested by room slug; an old flat single-room file loads transparently as the default room (and named rooms seed fresh) โ€” proven by tests/test_state_migration.py. New build_engine_config (pure) + tests/test_room.py cases.
  • manifest 2.10.0 โ†’ 2.11.0. (Per-room dashboard scoping โ€” ?room= โ€” is still the remaining roadmap item; the engine and entities are fully multi-room.)

Crop Steering add-on 0.9.1#

๐ŸŒฑ In plain English. New logo โ€” cannabis leaf + a green growth chart and rising arrow, with the Home Assistant and Python marks.

๐Ÿ”ง Technical notes. Rebuilt img/crop-steering-logo.png (README hero) + add-on icon.png (emblem, squared) / logo.png from the new artwork; add-on 0.9.0 โ†’ 0.9.1. Integration unchanged.

[2.10.0] - 2026-06-29

๐ŸŒฑ In plain English. Two of the genuinely useful ideas from the retired engine are back โ€” rebuilt lean and safe. Named-stage recipes: pick Veg / Transition / Bulk / Ripen from a dropdown and it writes that stage's setpoints (VWC targets, dryback, EC targets, EC ceiling, shot size) into your zones in one move โ€” no more hand-entering every number per stage. Vmax advisory: the engine now watches each zone's morning wet-up and publishes the field-capacity ceiling it actually reaches (sensor.crop_steering_zone_N_vmax_detected, with a confidence) so you can steer off the substrate's real ceiling โ€” advisory only, it changes nothing on its own.

๐Ÿ”ง Technical notes.

  • Recipes (integration): new select.crop_steering_recipe_stage (Veg/Transition/Bulk/Ripen/Custom) โ€” selecting a stage applies its row. The recipe table is stored server-side per room via the HA Store helper (.storage/crop_steering_recipes_<entry>), not entities (no per-stage sprawl) and not browser localStorage. Read it from the select's attributes; author it with the new crop_steering.save_recipe service; apply with crop_steering.apply_recipe. Applying writes the existing number.crop_steering_[zone_N_]<param> entities โ€” engine unchanged, works with the add-on off, every value clamped by the number's min/max and still gated by the kill switch. Curated param set (8): p1_target_vwc, p2_vwc_threshold, generative_dryback_target, p0_dryback_drop_percent, ec_target_gen_p1/p2, maximum_ec, p2_shot_size. New recipe.py (+ build_targets pure helper), tests/test_recipe.py.
  • Vmax advisory (engine/add-on 0.9.0): new pure detect_vmax() in crop-steering-engine (P1 wet-up plateau detection โ€” marginal-uptake collapse โ†’ ceiling + confidence; unit-tested, tests/test_vmax.py). The add-on tracks the per-zone P1 wet-up (non-persisted) and publishes sensor.crop_steering_zone_N_vmax_detected. Does not touch any irrigation decision.
  • Re-synced the add-on's vendored engine copy with the canonical crop-steering-engine/src (had drifted by a docstring). manifest 2.9.4 โ†’ 2.10.0.

[2.9.4] - 2026-06-29

๐ŸŒฑ In plain English. Honesty + cleanup pass. The retired AppDaemon engine (and its experimental adaptive / Vmax layer) is removed from the repo โ€” the f2-control add-on has been the only engine for a while, and keeping the dead code around made the docs claim things the live system doesn't do. The README is corrected to match the actual code: the self-tuning "Vmax / adaptive steering" section is gone (it was never in the live engine), the dashboard tab names and the safety gate chain are fixed, and overstated claims (a >4 h self-healing watchdog, daily shot-count caps, ?room= dashboard scoping) are reworded to what's real. New SYSTEM_GUIDE.html (feature list + flowcharts) and docs/DASHBOARDS.md (what's wired, what's hardcoded to F2, and how to populate the dashboards for your own facility).

๐Ÿ”ง Technical notes.

  • Removed appdaemon/ (retired engine + libs + the lean harness), appdaemon.yaml, and packages/f2_adaptive_steering.yaml (the retired adaptive layer's HA package).
  • Test coverage preserved: the 28 useful cases from the deleted test_lean_decide.py were ported to crop-steering-engine/tests/test_decide_edge_cases.py (imports the active crop_steering_engine); engine suite now 39 cases. CI drops the standalone lean-harness step (the engine pytest step already covers it); state-persistence stays covered by tests/test_state_migration.py.
  • README corrected against the code: deleted the Adaptive/Vmax section; fixed the features table (sensor fusion credited to the integration, watering watchdog vs the fictional self-healing one, daily volume cap only, multi-room note); fixed the safety-gate mermaid (field capacity is the P1 ceiling, not a pre-shot gate); fixed the dashboard tab names (Overview ยท Substrate ยท Zones ยท Steering ยท Analyze ยท Climate ยท Floorplan); removed the non-functional ?room= deep-link; clarified the hardware map is set in the integration wizard, not the add-on Configuration. Zones badge 1โ€“24+ โ†’ 1โ€“24.
  • Scrubbed every remaining AppDaemon reference from docs, CI workflows, CLAUDE.md, crop-steering-engine, and integration code comments.
  • SYSTEM_GUIDE.html rebuilt (self-contained, render-verified) and mirrored to www/; SYSTEM_GUIDE.md rewritten to match. manifest 2.9.3 โ†’ 2.9.4.

Crop Steering add-on 0.8.3#

๐ŸŒฑ In plain English. Uses the original Open Crop Steering logo artwork (leaf + water-drop in a green ring) for the add-on icon and logo, instead of a redrawn version.

๐Ÿ”ง Technical notes. icon.png (cropped circular emblem) + logo.png rebuilt from the source artwork; version 0.8.2 โ†’ 0.8.3. Integration unchanged (the README hero uses the same artwork).

[2.9.3] - 2026-06-28

๐ŸŒฑ In plain English. Branding + docs refresh. New Crop Steering logo (leaf + water-drop in a green ring). The README now explains, clearly: the two parts of the install (the integration vs the add-on, and which does what), how the dashboard works and how to jump between its parts (and run it per room), and how to embed it as a card. The feature list is brought current.

๐Ÿ”ง Technical notes.

  • New img/crop-steering-logo.png (README hero) + add-on icon.png / logo.png.
  • README: a two-part install table; the wizard step rewritten (entity-picker dropdowns, 1โ€“24+ zones, Configureโ†’Edit zones, multi-room, Settingsโ†’Repairs); the dashboard section rewritten (served as a sidebar ingress panel, the tab bar, ?room= deep-links, an iframe/Webpage card, and the Lovelace alternative); new feature rows (multi-room, sidebar dashboards, health checks, configure-once, multi-probe per zone). manifest 2.9.2 โ†’ 2.9.3.

Crop Steering add-on 0.8.2#

๐ŸŒฑ In plain English. New logo, matching the project mark.

๐Ÿ”ง Technical notes. New icon.png + logo.png; version 0.8.1 โ†’ 0.8.2.

[2.9.2] - 2026-06-28

๐ŸŒฑ In plain English. Fixes the log warnings about EC sensors using "mS/cm which is not a valid unit for device class voltage". The EC sensors were mistakenly tagged as voltage โ€” they measure conductivity, so the wrong tag is removed. The value and unit (mS/cm) are unchanged.

๐Ÿ”ง Technical notes.

  • sensor.py: removed device_class=SensorDeviceClass.VOLTAGE from the EC sensors (configured_avg_ec, ec_zone_N). EC now reports mS/cm with no (incorrect) device class โ†’ no more unit-validation warnings. manifest 2.9.1 โ†’ 2.9.2.

Not a code bug: "the engine ignored my dripper flow rate / drippers per plant" is the old add-on (0.2.0), which reads its own options, not the per-zone integration numbers. Add-on 0.5.0+ reads number.crop_steering_dripper_flow_rate + per-zone drippers_per_plant live โ€” update the Crop Steering add-on to 0.8.0.

[2.9.1] - 2026-06-28

๐ŸŒฑ In plain English. Bug fixes. The integration no longer spams the log trying to create test helpers (a dead feature), and the Configure (gear) button works again. Also โ€” there's now a proper GitHub release, so HACS will actually offer the update; earlier versions (2.7โ€“2.9) were pushed but never released, which is why the new setup wizard hadn't shown up yet.

๐Ÿ”ง Technical notes.

  • Removed _create_test_helpers โ€” it called the non-existent input_boolean.create / input_number.create services (the "Action input_boolean.create not found" spam, 56ร—). The simulation helper entities are gone.
  • OptionsFlow no longer assigns self.config_entry (read-only in modern HA โ†’ 500 on the Configure dialog); uses an internal self._entry.
  • Dropped a stale "v2.3.1" startup log line; removed the now-unused import. manifest 2.9.0 โ†’ 2.9.1, and tagged a release so HACS surfaces the UI setup wizard, reconfigure flow, multi-room and Repairs.

[2.9.0] - 2026-06-28

๐ŸŒฑ In plain English. Multi-room โ€” Stage 1. You can now add more than one grow room: go to Devices & Services โ†’ Add Integration โ†’ Crop Steering again, give the room a name, and map its own zones, sensors, pump and setpoints. Each room is completely isolated from the others. Your existing setup is the "default" room and is untouched โ€” its entities keep exactly the same names. (The engine driving the new rooms comes in the next add-on release; this stage lets you create and wire them.)

๐Ÿ”ง Technical notes.

  • Removed the single-instance lock; each config entry is now a room. New room.py: the default room uses no entity prefix (back-compat), named rooms namespace as crop_steering_<slug>_*. Entity object_id in every platform (sensor/number/switch/select/ button) now prepends room_prefix(entry) โ€” "" for the default room, so existing entity ids are byte-identical.
  • config_flow: async_step_room names an additional room (slug โ†’ prefix) and routes it through the UI mapping flow; .env/manual default-room paths set room_prefix="". Test-helper entities are created only for the default room (no collisions).
  • Health/Repairs are per-room: issue ids suffixed by room slug, fused-sensor checks use the room prefix, kill-switch/engine checks run for the default room only (until the engine is multi-room).
  • tests/test_room.py locks "default room stays un-prefixed". manifest 2.8.0 โ†’ 2.9.0.

Crop Steering add-on 0.8.1#

๐ŸŒฑ In plain English. Fixes "it ignored my dripper flow rate and drippers per plant." The engine only used those numbers when a per-zone plant count was also set; if plant count was blank it quietly fell back to a generic flow value. Now your dripper flow rate + drippers/plant always drive how long each shot runs (plant count cancels out of that maths anyway).

๐Ÿ”ง Technical notes.

  • controller.py _zone_flow_lps: dropped the plant_count > 0 gate (plant_count cancels against the zone-total substrate in the duration), defaulting it to 1; falls back to the flow_lps option only if drippers_per_plant or dripper_flow_rate is unset. 0.8.0 โ†’ 0.8.1.
  • tests/test_state_migration.py: regression test that dripper settings drive flow at plant_count=0.

Crop Steering add-on 0.8.0#

๐ŸŒฑ In plain English. Works out of the box on any system โ€” no more F2-specific defaults. The source-water pH/EC feed gate is now optional: leave the two sensor fields blank and that gate is simply off (your dosing / tank-fill holds still apply); set them to your reservoir probes to turn it on. The fallback substrate/flow numbers are generic placeholders now, not F2's.

๐Ÿ”ง Technical notes.

  • config.yaml: new optional feed_ec_sensor / feed_ph_sensor (str?, default ""); removed the hardcoded F2 atlas/aquaponics probe defaults. substrate_l 6 โ†’ 5, flow_lps 0.067 โ†’ 0.02 as neutral last-resort fallbacks (real shot sizing still reads the per-zone integration numbers). Version 0.7.0 โ†’ 0.8.0.
  • controller.py: feed sensors read from options; an empty value disables that half of the source-water gate (logged) so a fresh install isn't held by a dead default probe. Tooltips added.
  • Closes the "genericise F2-specific defaults" debt โ€” the add-on now works for anyone, not just F2.

Crop Steering add-on 0.7.0#

๐ŸŒฑ In plain English. The add-on is renamed F2 Control โ†’ Crop Steering with a new logo, a proper description + feature list, and a changelog you can open from the Info page. The operator dashboards show as a sidebar panel (toggle Show in sidebar on the add-on's Info tab). ("F2" was just one grow room โ€” this drives any number of zones.)

๐Ÿ”ง Technical notes. config.yaml name/panel_title โ†’ "Crop Steering"; new icon.png + logo.png (Open Crop Steering mark); DOCS.md (Documentation tab) + add-on CHANGELOG.md (Info-page Changelog link); version 0.6.0 โ†’ 0.7.0. Slug stays f2_control (kill switch input_boolean.f2_control_enabled unchanged) so existing installs aren't orphaned.

[2.8.0] - 2026-06-28

๐ŸŒฑ In plain English. The integration now watches your setup and flags problems in Settings โ†’ Repairs with a plain-language fix โ€” e.g. the kill-switch helper is missing, the engine add-on isn't running, a zone has no moisture sensor, or a zone's sensor reads unavailable. No more silent misconfiguration; each card clears itself once you fix it.

๐Ÿ”ง Technical notes.

  • New health.py + hooks in __init__.py: a read-only check runs 60 s after setup and every 5 min, creating/clearing Home Assistant issue_registry issues โ€” kill_switch_missing (error), engine_offline, zone_no_sensor, fused_sensor_unavailable (warnings) โ€” each with a learn_more_url to the Troubleshooting wiki. Timers are cancelled and issues cleared on unload. Never reads/writes hardware.
  • strings.json + translations/en.json: an issues block (title + description) for each.
  • manifest 2.7.0 โ†’ 2.8.0 (README badge + this changelog in sync).

[2.7.0] - 2026-06-27

๐ŸŒฑ In plain English. You can now set the whole thing up โ€” and fix it later โ€” from the Home Assistant UI with dropdowns, no text file. Pick "Manual UI configuration" at setup and it walks you through choosing each zone's valve and moisture/EC probes (as many as you have per zone), then your pump, main line, lights and substrate facts. Already installed and forgot a sensor? Go to the integration's Configure โ†’ Edit zones & hardware and add or swap sensors, or add a whole zone โ€” no reinstall. Every field has a help tooltip explaining what it is. This fixes the "I didn't set it up right at the start and there's no way to change it" problem.

๐Ÿ”ง Technical notes.

  • config_flow.py: the manual path (async_step_zones โ†’ new async_step_hardware) now uses HA EntitySelectors โ€” per zone: valve switch, VWC sensor (multiple), EC sensor (multiple), plant count; then shared pump/mainline/waste/light + lights hours + substrate volume/dripper/field-capacity/max-EC + optional room sensors + notify. Writes the same config-entry shape as the .env path (zone_switch + vwc_sensors/ec_sensors lists + vwc_front/back compat).
  • OptionsFlow: async_step_edit_zones (count) โ†’ new async_step_edit_zones_map (entity remap, prefilled) โ†’ updates the entry and reloads the integration. Replaces the old dead zone submenu.
  • Multiple probes per zone are now selectable and flow through to the integration's _average_sensor_values fusion (median/average + the engine's outlier rejection).
  • strings.json + new translations/en.json: data_description tooltips on every field of every config and options step (zones 1โ€“24 + all hardware), plus menu labels.
  • Bumped integration manifest.json 2.6.0 โ†’ 2.7.0 (README badge + this changelog in sync).

f2-control add-on 0.6.0#

๐ŸŒฑ In plain English. Configure once. The engine now reads your lights hours and zone count straight from the integration, so you set them in the Crop Steering UI and the add-on just follows โ€” no more keeping the add-on options and the integration in sync (the class of bug behind the "lights were 2 hours off" problem). The add-on options for these become fallbacks used only if the integration isn't reachable.

๐Ÿ”ง Technical notes.

  • controller.py: zone count is auto-detected from sensor.crop_steering_vwc_zone_N (_detect_zones, fallback to the num_zones option). Lights are read live each loop from number.crop_steering_lights_on_hour / _off_hour (_refresh_lights), fallback to the add-on option; the startup log prints the resolved hours + source, and alerts once if the integration value disagrees with the legacy option.
  • config.yaml 0.5.0 โ†’ 0.6.0; num_zones / lights_* options relabelled as fallbacks.
  • Operator note (live grow): the engine now reads number.crop_steering_lights_on_hour/ _off_hour. Set those on the dashboard to your real hours before arming โ€” the add-on log shows config: lights HH:00-HH:00 (source: โ€ฆ); confirm it's right. If the integration entity is missing it transparently falls back to the add-on option.
  • Verified: docker build + run against the HA base image โ†’ engine boots and logs config: lights 10:00-22:00 (source: add-on option โ€ฆ); no crash; state tests 5/5.

f2-control add-on 0.5.0#

๐ŸŒฑ In plain English. The engine now reads each zone's sensors from the integration's fused per-zone reading, which averages every probe you've mapped to that zone. So you can add as many moisture/EC probes per zone as you like in the Crop Steering UI and the engine uses all of them (averaged, outliers rejected) โ€” no add-on editing, and it works for any number of zones.

๐Ÿ”ง Technical notes.

  • controller.py: the per-zone sensor map now defaults to sensor.crop_steering_vwc_zone_N / sensor.crop_steering_ec_zone_N (the integration's _average_sensor_values fusion) for num_zones zones, instead of hardcoded raw F2 probes. An explicit zones option still overrides. Removes the F2-specific zone-sensor hardcoding.
  • config.yaml: new num_zones option (default 3) + schema; version 0.4.0 โ†’ 0.5.0. translations/en.yaml: num_zones help text.
  • Operator note (live grow): on Rebuild, confirm sensor.crop_steering_vwc_zone_1 reads the same as your raw probe before arming โ€” a single-source fused sensor equals the raw value. If a fused sensor is missing the zone holds (no actuation) rather than mis-steering.

f2-control add-on 0.4.0#

๐ŸŒฑ In plain English. The dashboards now come inside the add-on โ€” no more copying f2.html to /config/www over Samba or SSH. After you install the add-on, F2 Crop Steering appears in your Home Assistant sidebar; open it and you get a Live/Demo chooser. Live reads your real sensors using your existing HA login (nothing to paste, no URL to edit โ€” it works in the mobile app), and Demo shows mock data so you can look around safely. Thanks to @ChillingSilence (Chill-Division) for the idea (PR #1).

๐Ÿ”ง Technical notes.

  • config.yaml: ingress: true, ingress_port: 8420, panel_icon: mdi:sprout, panel_title: F2 Crop Steering; version 0.3.0 โ†’ 0.4.0.
  • Dockerfile: apk add nginx; serve /www/public on 8420; sed strips CRLF from run.sh (a Windows-checkout shebang issue that otherwise breaks the s6 launch).
  • run.sh: starts nginx (daemonizes) then execs the controller as the foreground process, so its SIGTERM safe-valve-off still fires on stop. No s6 service files โ€” avoids the s6-overlay v2/v3 services.d ambiguity; nginx dying never touches the engine.
  • nginx.conf: static, read-only, gz, logs to stdout; reachable only through HA ingress (no ports: mapping โ†’ not exposed to the LAN).
  • web-index.html: the in-panel Live/Demo landing. Live uses getToken() โ†’ localStorage.hassTokens (same origin under ingress) with a one-time token-paste fallback.
  • Single source of truth: scripts/publish_addon.sh assembles the add-on web root from the repo www/ at publish time (no duplicate dashboards committed in the monorepo).
  • Verified: docker build against ghcr.io/home-assistant/amd64-base-python:3.12-alpine3.20
    • run โ†’ engine boots ([f2-control] starting โ€ฆ loop 60s) and nginx serves the dashboards (landing/f2.html/overview 200, unknown path 404). Live-data-through-ingress is the operator's on-box check.

f2-control add-on 0.3.0#

๐ŸŒฑ In plain English. The add-on now has a proper name (โ€œF2 Crop Steeringโ€) and a custom icon in the Add-on Store, and every setting on its Configuration page has a help description (what notify_service, the lights hours, the kill switch, etc. actually do). Under the hood: the add-on safely remembers each zone's state across restarts and updates โ€” so upgrading never wipes your progress or makes you set things up again โ€” and there are now automated tests that guarantee that stays true.

๐Ÿ”ง Technical notes.

  • Add-on UX: config.yaml name: F2 Crop Steering, version 0.2.0 โ†’ 0.3.0; added icon.png + logo.png; added translations/en.yaml with configuration: name/description for all eight options (shown as field help in the Supervisor UI).
  • Documented the persistence + compatibility contract in CLAUDE.md (Compatibility & data): /data/state.json is non-ephemeral; _load_state tolerates missing/old/partial state; options read with defaults; stay generic (flagged the F2-specific feed-sensor / substrate_l / flow_lps defaults as known debt).
  • Tests: tests/test_state_migration.py (in-place upgrade โ€” old state.json loads: missing file, corrupt JSON, missing/unknown keys, bad timestamp, new zone seeded, saveโ†’load round-trip) and tests/test_version_consistency.py (manifest == CHANGELOG == README badge). Added TESTING.md and tests/run_ci.sh (local CI mirror). Both new suites green; full tests/ 34/34.

[2.6.0] - 2026-06-27

๐ŸŒฑ In plain English#

  • The Analyze page is rebuilt and actually useful. It now shows your room at a glance: a 24-hour timeline of when the lights, pump and each zone's valves ran; simple graphs of each zone's moisture and salt level; your climate (temperature, humidity, VPD, COโ‚‚, light); a "what's holding you back" ranking that points at the single worst factor; and your feed water's pH and EC. Every card has a plain note on what good and bad look like.
  • The dashboard no longer makes your mouse stutter. The 3D room view used to render non-stop in the background and bog down the whole computer โ€” now it only draws when you're looking at it.
  • The pump short-cycling is fixed. It was firing tiny bursts because it sized each watering for one plant's worth of substrate instead of the whole row, so the moisture never rose and it kept re-firing. Waterings are now the right length.
  • A guaranteed minimum daily water per plant (optional safety). You can set how many millilitres each plant must get per day; the system delivers it early in the day regardless of what the moisture probe says โ€” so a faulty or badly-placed sensor can't quietly starve a plant. Off until you set a number.
  • A safety limit on how long any single watering can run, so a wrong setting can't flood the room.
  • Smaller fixes to the salt-steering control and the feed-water safety checks.

To get these on your live system: the engine runs as a Home Assistant add-on that bundles its code when it's built โ€” so after updating the files you must Rebuild the add-on (Add-ons โ†’ F2 Control โ†’ โ‹ฎ โ†’ Rebuild), not just restart it. The dashboards update on a normal browser hard-refresh.

๐Ÿ”ง Technical notes#

Added#

  • Comprehensive Analyze tab (www/f2.html), all live off real F2 entities. Seven full-width cards: a 24 h equipment-state timeline (lights / pump / mainline / zone valves / recirc / CO2 / dehumidifier), per-zone VWC dryback + pore-EC-vs-target sparklines, water + substrate-temp + phase per zone, climate drivers (air temp / RH / VPD / CO2 / PPFD / DLI), a Liebig limiting-factor ranking (the bottleneck flagged first), and feed pH/EC gauges vs their bands. No Chart.js โ€” inline SVG sparklines + CSS bars, each card with what / good / bad help text.
  • Minimum daily water floor โ€” per-plant safety. input_number.crop_steering_zone_N_min_daily_ml_per_plant (mL/plant/day; ร— plant_count โ†’ a zone-litres floor). Guaranteed, front-stacked and sensor-independent: every plant gets its minimum, delivered from lights-on as fast as the anti-short-cycle spacing allows, regardless of the VWC probe (a lying/dead probe can't suppress it). The only VWC gate is a hard anti-drown ceiling (drown_ceiling, default 90); feed-water + dosing safety still apply. Clamped < max_daily_volume. 0 = off. (addons/f2_control/f2_min_daily_package.yaml)
  • Hard max shot-duration cap (flood guard). number.crop_steering_max_shot_duration (default 900 s) clamps the computed shot length and alerts if the raw value exceeds the cap (a substrate/flow misconfig) โ€” the upper bound to match the per-zone minimum-water floor's lower bound.

Changed#

  • Analyze rebuilt lightweight. Replaced the collapsing Chart.js auto-fit grid (broken widths) with full-width stacked cards; removes all Chart.js cost on that view.
  • www/overview.html โ€” per-zone daily water now reads sensor.crop_steering_zone_N_daily_water_usage (the live entity; the old name was blank), and the Pore-EC target resolves by the zone's veg/gen steering mode instead of hardcoding generative.

Fixed#

  • Dashboard mouse-glitch / desktop stutter. The 3D floorplan (separate WebGL app, www/floorplan, built from three-playground) ran a perpetual setAnimationLoop, rendering a shadow pass at the monitor's native refresh (60โ€“144 Hz) forever and saturating the GPU. Converted to render-on-demand (draw only on orbit / zoom / live-data change / door-swing; OrbitControls damping off; shadow baked once at 1024). Idle = 0 animation frames (verified).
  • f2.html 30 s churn. Tune-editor reseed gated to the active view (was rebuilding its heavy SVG from sensor noise every tick while hidden); the dashboard trend chart skips its re-bucketize when history is unchanged; chart build-animations off; the 30 s poll + chart pipeline pause while the browser tab is backgrounded; the engine-log status dot only pulses when the panel is open.
  • EC steering double-correction. Removed a second P2 EC-steer nudge in the pure decide() โ€” the IO shell already accumulates and bakes the EC offset into the P2 threshold; _params now clamps that offset-threshold to the engine's safe band.
  • PID cross-photoperiod windup. The EC-PID integral + previous-error reset on the P0 transition.
  • Shot-sizing short-cycle. Shot duration divided per-plant substrate by zone-total flow (plant_count ร— drippers ร— flow_rate), so per-plant 6 L blocks ร— 36 plants computed shots ~36ร— too short (~14 s instead of ~324 s for a 6 % shot) โ†’ VWC never rose โ†’ the pump short-cycled. _substrate_l now returns the zone total (per-plant block ร— plant_count) to match the zone-total flow; enter substrate_volume PER-PLANT.

Deploy note. The f2-control add-on bakes its code into the image at build time (Dockerfile COPY), so the engine changes in this release require an add-on Rebuild (Supervisor โ†’ F2 Control โ†’ โ‹ฎ โ†’ Rebuild), not just a restart, to take effect. (Interim: set the add-on Configuration options substrate_l = row-total litres and flow_lps = zone flow, then Save, to correct shot length on the running build without a rebuild.)

[2.5.0] - 2026-06-27

๐ŸŒฑ In plain English#

  • A phone-friendly control page (overview.html): per-zone moisture, EC and targets, room climate, and the pump / light / dosing controls all on one mobile screen.
  • Optional precision salt-steering (a PID controller) for substrate EC โ€” more accurate than the simple version, off by default.
  • Waterings are sized from your real hardware (substrate size + dripper flow) instead of a fixed guess.
  • It won't water with bad feed water โ€” now checks pH as well as EC, and fails safe if a probe dies.
  • No more pump machine-gunning on salt corrections โ€” those waterings now wait a minimum gap so they flush properly instead of stacking salt.

๐Ÿ”ง Technical notes#

Added#

  • Mobile control surface โ€” www/overview.html. A lightweight, phone-first one-pager with three tabs: Steer (per-zone VWC + target / pore-EC + target / substrate temp / phase / last shot / daily L; room temp, VPD, CO2, PPFD, DLI, total daily water), Controls (main pump, flush/fill modes, grow lights, per-zone enable), and Dosing (the veg-room peristaltic pumps โ€” Cleanse/Core/Bloom/Balance โ€” feeding F2). Vanilla JS, one 30 s poll, ?demo mode, confirmed actuation.
  • PID EC loop (engine ec_pid(), flag-gated, OFF by default). A real Kp/Ki/Kd controller with integral anti-windup and output clamped to ยฑ20 % of the P2 threshold, as an opt-in upgrade to the stepped EC-offset. Enable with input_boolean.crop_steering_ec_pid_enabled; gains tunable via input_number.crop_steering_ec_pid_{kp,ki,kd} (addons/f2_control/f2_ec_pid_package.yaml).

Changed#

  • Shot sizing reads the LIVE config. The f2-control add-on now computes shot duration from the real substrate volume + per-zone flow (plant_count ร— drippers_per_plant ร— dripper_flow_rate) instead of hardcoded options โ€” a mis-set substrate (6 L vs the real 9 L) fired shots ~โ…“ too short, so VWC never rose and the pump machine-gunned. Fixed.
  • .env is now authoritative for setpoints โ€” number entities seed their initial value from the parsed crop_steering.env params, falling back to DEFAULT_VALUES.

Fixed#

  • pH half of the source-water gate โ€” f2-control now gates on feed pH (irrigation_ph_min/max, with last-good grace + fail-closed on a dead probe), not just EC. Bad-pH feed no longer waters.
  • Fail-closed hardware writes โ€” ha_call returns False on non-2xx; a shot aborts (cuts hardware, alerts, does NOT advance shot/daily/last_shot counters) if any pump/mainline/valve command errors.
  • P2 EC-correction short-cycle โ€” flush/dilute/rescue shots respect a minimum interval (p2_min_interval_min, default 10 min); no-runoff nibbles can no longer stack EC + machine-gun the pump.
  • f2.html lag โ€” only the active tab rebuilds per 30 s tick, and backdrop-filter blur removed from the ~78 glass cards (fixes cursor/scroll jank).
  • services.execute_irrigation_shot โ€” zone ids coerced to int (a zone: 1 call no longer rejects against a "1" key after a config-entry reload).

Removed#

  • The retired AppDaemon engine is no longer bundled in the release artifact (kept in-repo as a manual rollback only).

[2.4.0] - 2026-06-26

Added โ€” f2-control standalone add-on + extracted crop-steering-engine (AppDaemon retired)#

  • addons/f2_control/ โ€” a standalone Home Assistant add-on that replaces the AppDaemon engine as the autonomous coordinator. One synchronous Python process, plain REST polling of HA (no asyncio coroutine trap), hardware shots with valve-close readback, republishes the full sensor.crop_steering_* status surface, sends 30-minute vitals to the operator's phone, and is gated by a hard kill switch input_boolean.f2_control_enabled (OFF = reads/computes but never actuates). Installs from the local add-on store; no apps.yaml, no scipy/numpy.
  • crop-steering-engine/ โ€” the pure decision core extracted as a standalone, pip-installable Python package (src-layout, offline unit tests, ruff/mypy/pytest CI). No HA, no I/O โ€” decide() over two dataclasses, so the same engine runs identically inside the add-on, AppDaemon, or a test. The add-on vendors a copy for a self-contained Docker build.
  • www/overview.html โ€” a new lightweight, mobile-first one-page vitals/overview (per-zone VWC/EC/phase/last-shot/daily water, system + feed status, kill-switch toggle). Vanilla JS, one 30-second poll, ?demo standalone mode.

Changed#

  • www/f2.html performance โ€” the 30-second refresh now only rebuilds the active tab's data (off-screen tabs no longer re-render every tick) and the camera frame only refetches while its tile is visible. Fixes the lag/freeze.
  • www/irrigation-manual.html โ€” restyled to the F2 white-paper dark theme (Home Assistant Roboto font) and updated for the f2-control add-on architecture (kill switch, per-zone EC targets, new safety features).

Fixed#

  • Engine P2 short-cycle / EC stacking โ€” P2 EC-correction shots (flush / dilute / rescue) now respect a minimum interval (p2_min_interval_min, default 10 min). Small no-runoff shots fired every tick were stacking pore EC instead of diluting it, short-cycling the pump. VWC top-ups stay ungated (self-limiting).

Security#

  • Scrubbed operator PII (real names, site label) from tracked config + docs before public release.

Added โ€” adaptive steering: self-tuning P1/P2 and predictive P3 (optional, off by default)#

  • New drop-in engine module appdaemon/apps/crop_steering/adaptive_steering.py, gated by input_boolean.f2_adaptive_steering_enabled. Wires into master_crop_steering_app.py via four small hooks (import; tick() at the end of _update_zone_vwc_capacity; per-zone rate in _get_zone_dryback_rate; buffer-safe cap in _should_zone_start_p3). Every setpoint write is clamped and confidence-gated; nothing runs unless the switch is on.
  • Per-zone P1 Vmax detection โ€” votes four independent saturation signals (marginal-uptake collapse, peak plateau, pore-EC runoff, saturating-curve fit) to detect each zone's true field-capacity ceiling during the morning ramp, with a confidence score. Published as sensor.crop_steering_zone_X_vmax_detected.
  • Dryback-derived P2 โ€” the P2 trigger is set to Vmax ร— (1 โˆ’ dryback_target%) per zone/mode, so each zone dries back the correct % from its own measured ceiling; the EC-stacker trims around it.
  • Multi-day P1 ramp โ€” the P1 target climbs toward the detected Vmax (capped below field capacity) a configurable amount per day, instead of a static hand-tuned number.
  • Predictive per-zone overnight (P3) cutoff โ€” measures each zone's own overnight dryback rate and feeds it into the engine's P3-start timing (previously a single fused room rate); caps the overnight target dryback so the predicted lights-on VWC stays above the P3 emergency floor + buffer, landing on the dryback target without firing overnight emergency shots. Forecast published as sensor.crop_steering_zone_X_p3_prediction.
  • Control surface in packages/f2_adaptive_steering.yaml (enable switch + f2_vmax_confidence_min, f2_p1_climb_rate_per_day, f2_p1_margin).

Added โ€” manual pump modes (flush / fill)#

  • packages/f2_pump_modes.yaml: input_boolean.f2_flush_mode and f2_fill_mode hand the pump to the operator (hose-flood each plant, or tank fill/dosing). While either is on the engine pauses its own shots and the hardware watchdog is exempted; a 30-minute reminder notification fires so the mode is never left on by accident.

Added โ€” unified operator console#

  • www/crop_steering.html: one dependency-free, responsive dashboard (replaces separate desktop/mobile pages). One-glance verdict (ALL GOOD / WATCH / ACT NOW) with a 7-point system-health checklist; word-threshold per-zone status (VWC DRY/LOW/GOOD/FULL/WET, EC LOW/ON-TARGET/HIGH/OVER, dryback); an issues side-drawer with persisted first-seen timestamps and durations; expandable VWC+EC and per-zone trace graphs; per-field coaching tooltips with recommended values; clickable pump/valve toggles; and a feed-lockout panel that names the exact gate blocking a low-VWC zone.

Fixed โ€” overnight P3 timing and dashboard correctness#

  • Per-zone overnight dryback rate replaces the single shared/fused rate that previously timed every zone's P3 cutoff off the room average.
  • Buffer-safe overnight cap stops a zone drying past the P3 emergency floor โ†’ no spurious overnight emergency shots.
  • Phase-aware floor (dashboard): a zone in P3 is judged against the P3 emergency floor, not the P2 maintenance floor, so normal generative night dryback no longer false-alarms as "under-watered".
  • Trust verdict keys off fusion confidence, not the flaky sensor_health metric โ€” kills a permanent false "data untrusted" state.
  • Alert debounce + real timestamps: noisy alerts must persist two cycles before showing and carry a persisted first-seen time, so the alert list stops being a graveyard of one-tick blips.

Added โ€” autonomous EC-stacking via P2 dryback (engine closed loop)#

  • New _ec_stack_dryback per-zone loop in the AppDaemon engine. In P2 it modulates each zone's number.crop_steering_zone_X_p2_vwc_threshold to drive smoothed pore-EC toward that zone's phase target (โ€ฆ_ec_target_{veg|gen}_p2): deeper dryback concentrates salts (EC stacks up), and a pore-EC drop is read as runoff and deepens the dryback further. EWMA-smoothed, 30-min cooldown, bounded by the P3 emergency floor and the min(p1_target, adaptive-cap) ceiling, gated by switch.crop_steering_ec_stacking_enabled. It is the single owner of the P2 dryback lever (the F2 heartbeat oversees it, no longer tunes it).

Fixed โ€” engine stability & timing#

  • EC sensor-fusion throttle: _on_ec_sensor_update now throttles the heavy fusion + entity writes (10 s per probe, non-blocking lock) so high-frequency EC ticks can no longer starve the AppDaemon worker thread / back up the queue (which had been false-firing the hardware watchdog mid-shot). The critical-EC safety check still runs on every tick.
  • Daily counters reset at lights-on, not on restart: _load_persistent_state is now photoperiod-aware โ€” the daily water/shot totals reset at the P3โ†’P0 lights-on boundary, so a mid-day AppDaemon restart no longer zeroes the running day.
  • Manual phase pin holds across restarts: the per-zone input_select.crop_steering_zone_X_phase_control is re-read every engine cycle and re-asserted, so a pinned phase survives a restart instead of snapping back to the P2 default.
  • Stuck-low / not-absorbing alert: a zone fed โ‰ฅ2 shots whose VWC stays flat below its P2 floor (channelling / probe out of the wet core / dried rockwool cube) is flagged instead of being chased with more water.

Added โ€” operator dashboards#

  • AROYA-style multi-zone VWC + EC overlay chart (dual-axis, scrubber, 24 h / 3 d / 1 w) on the desktop and mobile control dashboards; full per-entity Control surface and a dark operator-console re-skin.

Experimental โ€” branch feature/dial-ppfd-rehydration (NOT on main)#

  • 1โ€“10 crop-steering dial (input_number.crop_steering_steering_dial) that blends the EC-stack target from vegetativeโ†’generative. PPFDโ†’support coupling and a dried-cube rehydration mode are planned on the same branch.

Changed โ€” consolidated to a single deployed engine#

The repo now tracks exactly what runs in production: one AppDaemon engine (master_crop_steering_app.py โ€” 4-phase crop-steering irrigation) plus the HA integration. README, CLAUDE.md and docs/SYSTEM_OVERVIEW.md rewritten to match.

Removed โ€” undeployed experimental layers#

The intelligence/ subsystem (RootSense substrate AI + ClimateSense climate control) was never deployed and has been removed from main, along with its tests, the F1/Green Wave legacyag dashboards, the docs/upgrade/ planning docs, and the lean crop_steering_v2 controller. All recoverable from the archive/pre-doc-cleanup-2026-06 tag.

[Historical] - 3.0.0-dev "RootSense" (experimental, never deployed)

Added (HVAC mode-switching hardened + LLM advisor Phase L0)#

HVAC mode-switching (operator request: explicit dispatch wiring)#

  • propose_hvac_off() helper for explicit HVAC shutdown โ€” used by the watchdog when the unit is in heat mode during a high-temp emergency (it's actively making things worse) and available to a future crop_steering.hvac_force_off service.
  • Watchdog now receives hvac_mode and force-emits HVAC_MODE=off during a temp emergency only when the unit is currently heating. When cooling, it leaves the unit running โ€” we want max cooling under emergency.
  • Dispatch path refactored: pure action_to_service_call(action) helper translates every Action kind to (HA-service-path, kwargs). The AppDaemon adapter is now a one-liner around it. Tested for all 5 ActionKind variants including all 6 valid hvac_mode values (off/cool/heat/heat_cool/dry/fan_only) โ€” confirms the IR-blaster template (better_thermostat โ†’ SmartIR) gets the right service.
  • 4 new control-loop tests covering the watchdog HVAC-off rule, the propose_hvac_off helper, and the dispatch translator.

LLM advisor โ€” Phase L0 (report builder, NO LLM calls)#

  • New appdaemon/apps/crop_steering/intelligence/llm/ package.
  • report_builder.py โ€” RootSenseReportBuilder AppDaemon app builds a compact JSON snapshot every 15 min. The schema mirrors the design in LLM_HEALTHCHECK_PLAN.md:
    • top-level: ts, phase, intent, recipe_phase, recipe_day, climate, substrate (per-zone), deltas_15m_ago, active_anomalies, triage, estimated_tokens.
    • climate: temp/rh/vpd/leaf_vpd/co2 with _target + _status classification (on_target / near_target / off_target).
    • triage tag: anomaly:* > drift:* > heartbeat > ok (local rule engine, no LLM).
  • Pure helpers: classify_status(), compute_deltas(), compute_triage(), estimate_tokens() โ€” all unit-tested in isolation.
  • Published as:
    • sensor.crop_steering_rootsense_report_latest (state = triage tag, attributes = full payload).
    • sensor.crop_steering_rootsense_report_size_tokens (token estimate).
    • Bus event report.ready.
    • HA event crop_steering_rootsense_report.
  • Switch switch.crop_steering_intelligence_llm_report_enabled (default OFF) gates the whole pillar.
  • 21 new unit tests covering classify_status edge cases, delta threshold + nested-substrate walking, triage precedence (anomaly > drift > heartbeat > ok), estimate_tokens proportionality, a realistic full-payload fits-under-500-tokens guard, build_report integration against synthetic state with day/night target switching, anomaly inclusion, drift triage, delta vs prior history, and a guard test that asserts the module imports no LLM client (anthropic / openai / requests / aiohttp absent โ€” by construction the module CANNOT call an LLM).
  • Also fixed a silently-dropped field: SensorMap now has explicit leaf_temp (was being filtered out by the YAML loader because the dataclass field didn't exist).

Added (Dashboard wiring)#

  • New "LLM Advisor" view in dashboards/legacyag/30_intelligence.yaml: pillar status, latest triage tag, full report payload as syntax-highlighted markdown, 7-day token-size + triage-history graphs.

Changed (Recorder includes)#

  • packages/rootsense/00_recorder.yaml extended for sensor.climate_leaf_vpd_kpa, sensor.climate_derived_rh_target_pct, the L0 report sensors, and the LLM-report-enable switch.

Tests#

  • Total intelligence tests: 121 (was 95).
  • Full suite: 154 passing.

Changed (ClimateSense โ€” layered control refactor + leaf VPD supervisory variable)#

The monolithic control.py (~290 lines) is replaced with a layered package mirroring commercial-BMS architecture:

intelligence/climate/
  leaf_vpd.py              โ† pure math (Tetens + RH inversion)
  control/
    actions.py             โ† Action / ActionKind dataclasses
    hvac.py                โ† bang-bang + calibration + mode-switch + IR refresh
    dehumidifier.py        โ† lead-lag staging + rotation + demand persistence
    humidifier.py          โ† bang-bang with min-off
    co2.py                 โ† pulse cadence + lights-on lead time + adaptive
    exhaust.py             โ† emergency + scheduled refresh + watchdog
    coordinator.py         โ† cross-actuator conflict resolution
    watchdog.py            โ† Tier 4 safety (sensor staleness, runaway, emergency)
    app.py                 โ† thin AppDaemon adapter

Added (corrected for F1 reality)#

  • Leaf VPD as the supervisory variable. The recipe declares leaf_vpd_kpa per phase; the control layer reads current leaf temp
    • air temp + air RH and inverts the Tetens equation to derive the required RH to hit target leaf VPD. The dehu/humid loops chase the derived RH, not the recipe's day_rh_pct (kept as fallback for installs without a leaf-temp sensor).
  • 22 unit tests covering Tetens math against published tables, round-trip property tests on the RH inversion, and 6 cannabis-phase envelope checks (transplant through ripening) where solved RH must fall within published cultivation chart bands.

Added (control patterns from commercial cultivation HVAC)#

  • HVAC mode-switching with cooldown โ€” controller never flips coolโ†”heat more often than mode_change_cooldown_min (default 30 min). IR-blaster insurance: re-issue last setpoint every refresh_interval_min to recover from lost IR commands.
  • Lead-lag dehumidifier rotation. F1 has 2 dehu units (4 relays as contactor pairs). Lead engages first on demand persistence; lag only joins if RH stays high stage_persistence_min after that. Reverse order on release (lag off first). Lead/lag swap every 7 days for wear leveling.
  • Demand persistence โ€” every actuator requires the deviation to last 3-5 min before staging on; prevents reacting to door-open spikes.
  • ACโ†”dehu cooperation โ€” coordinator defers staging the LAG dehu while AC is already cooling AND temp is above target (AC cooling is already condensing water out via condensation).
  • Adaptive COโ‚‚ pulse cadence โ€” extends/shortens pulse_off based on observed ฮ”COโ‚‚ during the previous rest period; clamped to [pulse_off_min_seconds, pulse_off_max_seconds].
  • Lights-on lead time for COโ‚‚ โ€” solenoid won't inject until lights have been on for lights_on_lead_min (default 30 min) so stomata are open and COโ‚‚ uptake is real.
  • Tier 4 watchdog โ€” runs every tick. Flags stale sensors (no update in 90 s), force-closes any actuator that exceeds its actuator_max_runtime_min, and re-asserts emergency exhaust + COโ‚‚ cutoff regardless of what other layers are doing.
  • Exhaust controller โ€” emergency-only by default (room is sealed). Triggers on temp > 32 ยฐC OR COโ‚‚ > 1800 ppm; releases when both are back in safe range. Optional scheduled refresh window.
  • Severity-tiered actions โ€” every Action carries a severity (normal / safety / emergency). Coordinator drops normal-severity proposals when a critical anomaly is active; safety/emergency always emit.

Added (operator-facing docs)#

  • docs/upgrade/SEQUENCE_OF_OPERATIONS.md โ€” the operator-readable contract describing exactly what every actuator does, when, and why. Sections cover authority hierarchy, sensors, supervisory variable, per-actuator sequences, cross-actuator rules, failure modes, and loss-of-comms safe state.
  • docs/upgrade/CONTROL_THEORY.md โ€” design rationale. Why bang-bang not PID, why leaf VPD not air RH, why hardware calibration is first-class, why staged with lead-lag rotation, why coordinator over master controller, why severity-tiered actions, what this design will NOT do.

Added (corrected hardware reality for F1)#

  • hardware_f1.yaml updated to match operator description:
    • 2 dehu units (was incorrectly described as 4 separate dehus); each is a contactor pair driving run + fan relays.
    • Wet-tip contactors with built-in 2-min hardware cooldown โ€” software no longer enforces min_off (the contactors do).
    • 2ร— 9 kW IR-controlled AC presented as a single climate entity with HVAC mode-switching support.
    • Exhaust system added (emergency + scheduled refresh).
    • Leaf-temp sensor present โ€” primary supervisory variable.

Tests#

  • 32 new tests in test_control_loops.py (HVAC, dehu, humidifier, CO2, exhaust, coordinator, watchdog).
  • 22 new tests in test_leaf_vpd.py (Tetens, AVP, leaf VPD, RH solver, cannabis-phase envelopes).
  • Total intelligence tests: 95. Full suite: 123 passing.

Added (ClimateSense โ€” environmental control sibling)#

  • New appdaemon/apps/crop_steering/intelligence/climate/ package โ€” five pillars mirroring RootSense exactly (sensing, timeline, control, lights, anomaly) plus a hardware calibration module.
  • Hardware calibration is first-class. A YAML file (hardware_f1.yaml shipped) captures per-actuator quirks like "set heat pump to 27 ยฐC, room reads 29 ยฐC". Control loops command target + offset so the room actually hits target. Tests confirm the math is right and the file shape is forward-compatible with new keys.
  • Recipe-driven setpoints. A YAML at /config/recipes/<name>.yaml describes per-phase targets across the entire grow (temp / RH / COโ‚‚ / VPD / PPFD / photoperiod). The shipped athena_f1_default.yaml is a 7-phase ~84-day Athena cannabis recipe. The timeline pillar resolves "what are my targets right now" from day-in-grow + day/night state and publishes per-metric sensor.climate_target_* entities.
  • Closed-loop control: bang-bang temp with calibration offset and deadband, RH with hysteresis matching the existing 40_environment.yaml defaults (drop-in replacement once enabled), pulse-injection COโ‚‚ with hard cap and lights-off safety, optional lights manager with PPFD ramps.
  • Climate anomaly scanner: temp/RH/VPD excursions, COโ‚‚ low during photoperiod, COโ‚‚ overshoot, DLI undershoot prediction, sensor unavailability. Reuses the existing crop_steering_anomaly event format with code prefixed climate_*.
  • 6 new switches in the integration: 5 module-enable switches mirroring RootSense's pattern, plus ..._climate_drives_intent_enabled to let the recipe optionally drive the cultivator-intent slider.
  • New number entity number.crop_steering_climate_grow_day_offset โ€” operator hand-sets day-in-grow.
  • 12 new unit tests covering hardware calibration math (cool/heat offsets, deadband, clipping, real F1 file load, forward-compat key filtering) and recipe loader (phase-for-day resolution, ramp defaults, validation rejection of empty phases, intent extraction).
  • Total intelligence test count: 51 (was 39); full suite 79.

Added (Documentation)#

  • docs/SYSTEM_OVERVIEW.md โ€” single source of truth for how the whole stack fits together. Hardware โ†’ entities โ†’ control โ†’ dashboards. Includes the heat-pump calibration explanation and a full reference doc index.
  • appdaemon/apps/crop_steering/intelligence/climate/hardware_f1.yaml โ€” F1-specific calibration file with the heat pump's cool_offset_c: -2.0 documented inline.

Added (Dashboards)#

  • New Recipe view in dashboards/legacyag/40_setpoints.yaml โ€” active phase + day-in-grow + DLI today/predicted + the 5 ClimateSense module switches + measured-vs-recipe-target overlay.
  • Recorder package extended with all sensor.climate_target_* globs and the new ClimateSense switches/numbers.

Added (Phase 2 โ€” Adaptive Irrigation goes live)#

  • number.crop_steering_steering_intent โ€” single -100..+100 dial that drives every derived parameter via the IntentResolver.
  • select.crop_steering_steering_mode_derived โ€” Generative / Mixed-generative / Balanced / Mixed-vegetative / Vegetative bucketing of the intent slider.
  • crop_steering.custom_shot HA service. Pure event-router with full schema validation. Orchestrator listens for the resulting event and applies safety gates.
  • IntentResolver._publish_derived_mode() keeps the derived select in sync with the intent slider on every tick.
  • 9 unit tests for IntentResolver (lerp endpoints, profile-dict consistency guard, every profile key has an entity mapping, intent=0/ยฑ100 produce correct values, live read of dryback sliders, 11-point bucketing matrix, derived sensor attributes). 13 tests total in tests/intelligence/, all green.

Added (Test coverage + migration doc)#

  • 26 additional unit tests across tests/intelligence/:
    • test_anomaly_scanner.py (8) โ€” emitter blockage, EC drift, flat-line gating by photoperiod, peer-deviation, no-double-fire.
    • test_orchestrator.py (7) โ€” emergency rescue, flush trigger and cooldown, anomaly suppression, custom-shot event routing.
    • test_agronomic.py (11) โ€” VPD math against published Athena chart values, Penman-Monteith monotonicity, transpiration envelope sanity, VPD ceiling publishing.
  • Total intelligence test count: 39 passing (was 13).
  • Two real bugs caught and fixed in the process: missing _dryback_window init in AgronomicIntelligence reproduced via fixture, and an over-loose peer-deviation test data set that didn't actually exercise the 2ฯƒ rule.
  • tests/intelligence/_appdaemon_stub.py โ€” shared AppDaemon stub module so individual test files don't duplicate ~50 lines each.
  • MIGRATION.md โ€” operator-facing v2.3.x โ†’ v3.0 upgrade guide with step-by-step rollout sequence, rollback path, and troubleshooting.

Added (Linked F1 dashboard suite)#

  • Five linked Lovelace dashboards under dashboards/legacyag/, built on the existing custom:agency-sensor-analytics-card that the live F1 install already uses. Replaces the earlier history-graph-card prototype (rootsense_history.yaml) which has been removed.
    • 00_overview.yaml โ€” landing page, room climate + substrate
      • RootSense status at a glance.
    • 10_climate.yaml โ€” three views (Temp & RH / COโ‚‚ / VPD) with measured-vs-target overlays and full actuator control panels.
    • 20_substrate.yaml โ€” two views (per-table VWC & EC / RootSense per-zone intelligence). 7-day FC + dryback velocity + porosity
      • EC stack.
    • 30_intelligence.yaml โ€” three views (Intent / Anomalies / Custom shot console). Includes one-click test-emitter and rescue-shot buttons.
    • 40_setpoints.yaml โ€” single page collecting every operator target, plus measured-vs-target time series for climate and substrate. Anchor for the future ClimateSense recipe timeline.
  • A shared markdown nav block at the top of every dashboard links the five together for one-click switching.

Added (Plans)#

  • docs/upgrade/INFLUXDB_GRAPHS_PLAN.md โ€” how to swap the agency-sensor-analytics-card's data fetch from HA's recorder history API to InfluxDB v2 Flux queries. ~1.5 focused-days work. Default stays HA-history; per-card opt-in via data_source: influxdb.

Added (Polish)#

  • ENTITIES.md updated end-to-end with new RootSense entities and the dryback semantic clarification ("% drop from peak", not "VWC value to dry to").
  • README.md gains a RootSense v3 section with module status table and opt-in steps.
  • .gitignore excludes appdaemon/apps/crop_steering/state/ (local SQLite) and a stray nested clone directory.

Added (Phase 1 โ€” Root Zone Intelligence wiring)#

  • New shared base appdaemon/apps/crop_steering/intelligence/base.py provides module-enable gating and common helpers for all five pillars.
  • 5 module-enable switches in the integration so each pillar is independently toggleable from the HA UI:
    • switch.crop_steering_intelligence_root_zone_enabled
    • switch.crop_steering_intelligence_adaptive_enabled
    • switch.crop_steering_intelligence_agronomic_enabled
    • switch.crop_steering_intelligence_orchestrator_enabled
    • switch.crop_steering_intelligence_anomaly_enabled Default OFF โ€” existing v2.x installs are unaffected on first upgrade.
  • RootZoneIntelligence now publishes three live derived sensors per zone (previously stubs):
    • sensor.crop_steering_zone_{n}_dryback_velocity_pct_per_hr
    • sensor.crop_steering_zone_{n}_substrate_porosity_estimate_ml_per_pct
    • sensor.crop_steering_zone_{n}_ec_stack_index
  • Local dryback-episode tracker (DrybackTracker) detects peak/valley pairs from the rolling per-zone VWC buffer, persists each episode to rootsense.db, publishes dryback.complete on the bus, and fires the HA crop_steering_dryback_complete event.
  • Recorder includes package: packages/rootsense/00_recorder.yaml ensures the new sensors and module switches are kept in HA's history database.
  • Reconciliation document docs/upgrade/RECONCILIATION.md mapping the gap analysis P1/P2 items to RootSense plan phases.
  • Unit tests: tests/intelligence/test_dryback_tracker.py covers the episode tracker state machine (4 cases, all green).

Added (Phase 0 โ€” scaffolding only, no behaviour change)#

  • New appdaemon/apps/crop_steering/intelligence/ package containing the four RootSense intelligence pillars as opt-in AppDaemon apps:
    • root_zone.py โ€” automated field-capacity detection, dryback episodes, substrate analytics sensors.
    • adaptive_irrigation.py โ€” cultivator-intent slider, profile interpolation, bandit-based shot-size optimisation.
    • agronomic.py โ€” Penman-Monteith transpiration estimate, VPD ceiling per cultivar, nightly run reports.
    • orchestration.py โ€” coordinator with crop_steering.custom_shot service, emergency rescue, EC flush guardrails.
    • anomaly.py โ€” cross-cutting anomaly scanner (emitter blockage, EC drift, sensor flat-line, peer-group VWC deviation).
  • Shared infrastructure:
    • bus.py โ€” in-process pub/sub (RootSenseBus).
    • store.py โ€” local SQLite analytics store (rootsense.db).
  • docs/upgrade/ROOTSENSE_v3_PLAN.md โ€” full upgrade plan, feature mapping, testing strategy, and future roadmap.
  • docs/upgrade/apps.example.yaml โ€” example AppDaemon app declarations.

Fixed#

  • Testability hardening: moved ShotCalculator into new pure helper module custom_components/crop_steering/calculations.py so unit tests no longer require a Home Assistant runtime during import.

Documentation#

  • Added docs/upgrade/GAP_ANALYSIS_2026-05.md with a full module-by-module gap analysis, prioritized production backlog, and explicit To-Do sequence.
  • Updated README with a dedicated "Upgrade Gap Analysis & To-Do" section linking to the new tracker document.

Changed#

  • P0 dryback is now unambiguously "% drop from peak VWC", surfaced as two independent operator-facing number entities:
    • number.crop_steering_veg_p0_dryback_drop_pct (range 2โ€“40, default 12).
    • number.crop_steering_gen_p0_dryback_drop_pct (range 2โ€“50, default 22). Defaults follow Athena cannabis guidance (small drop in vegetative growth, larger drop in generative). Both values are read live by the IntentResolver every tick โ€” neither is hard-coded. The interpolated current target is pushed to the existing number.crop_steering_p0_dryback_drop_percent entity that the legacy P0-exit predicate already consumes, and exposed as sensor.crop_steering_p0_dryback_drop_pct_current for dashboards.
  • Legacy entities number.crop_steering_veg_dryback_target and number.crop_steering_gen_dryback_target are retained as aliases. Their default values are corrected from 50 / 40 (which were too aggressive under the "drop %" semantic) to the new 12 / 22. Their min ranges are widened to 2 so existing installs that already configured them lower continue to load without validation errors.
  • DEFAULT_VEG_P0_DRYBACK_DROP_PCT / DEFAULT_GEN_P0_DRYBACK_DROP_PCT added to custom_components/crop_steering/const.py as the single source of truth.

Notes#

  • Existing apps (master_crop_steering_app.py, phase_state_machine.py, detectors, profiles) are untouched in this commit.
  • Modules are not added to apps.yaml automatically; existing installs are unaffected. See docs/upgrade/apps.example.yaml to opt in.
  • Full migration to v3.0 happens incrementally across PRs #2โ€“#5 per the plan.

[2.3.1] - 2025-01-03

๐Ÿ”ง Critical Fixes & Documentation Overhaul#

  • CRITICAL FIX - Reconstructed corrupted config_flow.py that prevented integration loading
  • Documentation Accuracy - Complete rewrite of all .md files to reflect actual system capabilities
  • Code Quality - Major refactoring with 85% reduction in code duplication
  • Dependency Clarity - Fixed misleading "zero dependencies" claims (AppDaemon required for automation)

๐Ÿ“š Documentation Updates#

  • README.md - Updated to v2.3.1, corrected dependency information
  • installation_guide.md - Complete beginner-friendly rewrite with step-by-step instructions
  • dashboard_guide.md - Rewritten for actual AppDaemon YAML dashboard system
  • troubleshooting.md - Updated for rule-based system (removed AI/ML references)
  • Removed - ai_operation_guide.md (contained incorrect AI/ML information)

๐Ÿ’ก System Clarification#

  • Rule-Based Logic - System uses sophisticated rule-based irrigation logic, not AI/ML
  • Statistical Analysis - Uses scipy.stats for trend analysis and sensor validation
  • AppDaemon Requirement - AppDaemon needed for automatic phase transitions and advanced features
  • Core Integration - Works standalone for manual control, AppDaemon adds automation

๐Ÿ—๏ธ Code Quality Improvements#

  • Helper Classes - Introduced ShotCalculator to eliminate code duplication
  • Constants - Replaced magic numbers with named constants in const.py
  • Version Consistency - Standardized version strings across all files
  • Import Optimization - Fixed missing imports and standardized patterns

โš ๏ธ Truth in Documentation#

  • No More AI Claims - Removed all references to machine learning and AI capabilities
  • Accurate Feature List - Documentation now reflects actual implemented features
  • Realistic Expectations - Clear distinction between manual and automated operation modes
  • Beginner Focus - All guides rewritten for new users with no assumptions

[2.3.0] - 2024-12-26

๐Ÿš€ Major Features#

  • Full GUI Configuration - Complete zone and sensor setup through Home Assistant UI
  • Zero Dependencies - Removed ALL external Python packages (numpy, pandas, scipy, plotly)
  • Clean Architecture - Removed all redundant scripts and command line tools
  • Fixed Phase Logic - P3 now correctly persists through entire lights-off period
  • System-wide Light Controls - Removed illogical per-zone light controls

๐Ÿ—‘๏ธ Removed (Cleanup)#

  • fix_appdaemon_requirements.sh - No longer needed, zero dependencies
  • requirements.txt - System uses only standard Python libraries
  • configure_crop_steering.py - Replaced by GUI configuration
  • advanced_crop_steering_dashboard.py - Uses YAML dashboards instead
  • Command line dependency for zone configuration

โœจ Improvements#

  • GUI Config Flow - Advanced setup with sensor configuration for each zone
  • YAML Dashboards - AppDaemon native dashboards without Plotly
  • Async/Await Fixes - Proper coroutine handling in AppDaemon
  • Sensor Fusion - Fixed VWC/EC mixing issue
  • State Machine - Clean phase management implementation

๐Ÿ“ Documentation#

  • Updated README to v2.3.0 with GUI configuration
  • Removed all references to deprecated scripts
  • Added proper crop steering terminology explanations
  • Marked zone_configuration_helper.py as deprecated

โš ๏ธ Breaking Changes#

  • None - existing configurations continue to work

[2.1.1] - 2024-12-21

๐Ÿ”„ AppDaemon v15+ Compatibility Update#

  • Updated all documentation for AppDaemon v15+ directory changes
  • Fixed file paths from /config/appdaemon/ to /addon_configs/a0d7b954_appdaemon/
  • Enhanced fix script to auto-detect AppDaemon version and paths
  • Added migration guide for upgrading from AppDaemon v14 to v15+
  • Updated installation instructions with correct Samba share paths

๐Ÿ“ Documentation Updates#

  • docs/installation_guide.md - Updated for AppDaemon v15+ paths
  • docs/appdaemon_v15_migration.md - NEW comprehensive migration guide
  • docs/dynamic_zones_guide.md - Added AppDaemon v15+ compatibility notes
  • fix_appdaemon_requirements.sh - Enhanced to handle both old and new paths
  • README.md - Updated setup instructions for AppDaemon v15+

๐Ÿ”ง Technical Changes#

  • Auto-detection of AppDaemon directory structure in scripts
  • Backward compatibility maintained for AppDaemon v14 and earlier
  • Improved error messages for missing AppDaemon directories
  • Updated Samba share paths in all documentation

โš ๏ธ Important Notes#

  • If using AppDaemon v15+: Files are now in /addon_configs/a0d7b954_appdaemon/
  • Samba access: Use \\YOUR_HA_IP\addon_configs\a0d7b954_appdaemon
  • Migration required: Run updated scripts to move files to correct locations

[2.1.0] - 2024-12-21

๐Ÿš€ Major Features Added#

  • Dynamic Zone Configuration - Support for 1-6 irrigation zones (previously hardcoded to 3)
  • Interactive Zone Setup Tool - zone_configuration_helper.py for easy zone configuration
  • AppDaemon Compatibility Fix - fix_appdaemon_requirements.sh resolves scikit-learn installation issues
  • Flexible Entity Creation - Automatic generation of sensors and switches per configured zone

โœจ New Components#

  • zone_config.py - Zone configuration parser and validator
  • zone_configuration_helper.py - Interactive setup wizard
  • fix_appdaemon_requirements.sh - AppDaemon compatibility fix script
  • docs/dynamic_zones_guide.md - Complete zone configuration documentation
  • services.yaml - Proper service documentation

๐Ÿ”ง Enhancements#

  • Config Flow Improvements - Two setup modes: automatic (from env file) or manual (UI wizard)
  • Service Updates - Dynamic zone validation in irrigation services
  • Entity Scaling - All components now create entities per configured zone
  • Error Handling - Improved validation and graceful error handling
  • Documentation - Updated all guides for v2.1.0 features

๐Ÿ› Bug Fixes#

  • Fixed hardcoded zone iteration in AppDaemon modules
  • Resolved entity creation for zones 2+ (previously only zone 1 worked)
  • Fixed service validation to accept configured zones instead of hardcoded 1-3
  • Removed obsolete package references in config flow
  • Fixed sensor averaging to use actual configured sensors

๐Ÿ—๏ธ Architecture Changes#

  • Zone Detection - Dynamic zone discovery from configuration
  • Entity Generation - Runtime entity creation based on zone count
  • AppDaemon Integration - Removed hardcoded zone loops
  • Configuration Management - Centralized zone config through new parser

๐Ÿ“Š Per-Zone Entities Created#

For each configured zone:

  • switch.crop_steering_zone_X_enabled - Zone enable/disable
  • switch.crop_steering_zone_X_manual_override - Manual control
  • sensor.crop_steering_vwc_zone_X - Average VWC for zone
  • sensor.crop_steering_ec_zone_X - Average EC for zone
  • sensor.crop_steering_zone_X_status - Zone operational status
  • sensor.crop_steering_zone_X_last_irrigation - Last irrigation timestamp

๐Ÿ”„ Migration Notes#

  • Existing 3-zone setups continue working without changes
  • New installations can configure any number of zones 1-6
  • Zone configuration now centralized in crop_steering.env
  • Helper scripts automate setup process

โš ๏ธ Breaking Changes#

  • None - fully backward compatible with existing installations

๐Ÿ”ฎ Future Enhancements#

  • Zone grouping for simultaneous irrigation
  • Zone-specific crop profiles
  • Individual zone scheduling
  • Water usage tracking per zone

[2.0.0] - 2024-12-15

๐Ÿš€ Major Release - Complete System Overhaul#

  • New Architecture - Integration + AppDaemon AI modules (replaced package-based approach)
  • Advanced AI Features - Machine learning, sensor fusion, dryback detection
  • Professional Dashboard - Real-time Plotly visualizations
  • Quality Assurance - Complete code validation and optimization
  • Production Ready - Comprehensive testing and error handling

โœจ AI Features#

  • Machine Learning Engine - Predictive irrigation with ensemble models
  • Intelligent Sensor Fusion - Multi-sensor validation with outlier detection
  • Advanced Dryback Detection - Peak detection algorithms
  • Intelligent Crop Profiles - Strain-specific optimization

๐Ÿ—๏ธ Architecture#

  • Home Assistant Integration - Native HA integration with config flow
  • AppDaemon AI Modules - Advanced machine learning and automation
  • Configuration Management - File-based hardware configuration
  • Real-time Dashboard - Professional monitoring interface

๐Ÿ“š Documentation#

  • Complete installation guide for beginners
  • AI operation guide
  • Dashboard usage guide
  • Troubleshooting documentation

[1.x] - Legacy Versions

Previous package-based implementations. See git history for details.

Controller

The companion controller app has its own changelog file. It is mirrored to the controller repository for existing installations.

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.

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 F2 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 F2 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 F2 probe defaults; the substrate/flow fallbacks are neutral placeholders.

0.7.0#

  • Renamed F2 Control โ†’ 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 and addons/f2_control/CHANGELOG.md.