The Planning Loop

At every solar milestone and band transition, Iris, the AI planner receives a structured greenhouse context window. Routine checks and major reviews use the same audited MCP tool contract, trigger IDs, planner-instance stamps, registry validation, scorecards, and ESP32 safety boundary. Iris does not choose what the greenhouse should target. The crop target band handles plant policy, computed from the diurnal profiles of the currently active control crops with smooth hour-by-hour interpolation. The dispatcher then derives the firmware-enforced whole-house band, including a VPD band that can differ from the strict crop envelope because the ESP32 controls one air mass while zone mister targets remain local. Verdify currently has five active crop profiles, including the center-zone Vanda orchids; the center zone uses derived climate context rather than a dedicated probe. The planner chooses how aggressively the controller should chase those targets given forecast, memory, equipment state, and known physical limits. When the plan lands, Slack Operations is the human-readable layer: plan ID, prior scorecard, forecast, tactical posture, experiment, and watch items.

Each plan is a hypothesis. The system measures what happened, scores the result, and extracts lessons when predictions diverge from reality.

Control split: crop profiles define target bands, the dispatcher derives the firmware-enforced band, Iris chooses bounded tactics, MCP and the dispatcher validate writes, and ESP32 firmware decides relay state every 5 seconds.

The Closed Loop

Gather

Assemble context from sensors, forecast, crop bands, equipment, lessons, scorecards, prior plans, and static greenhouse documentation.

Validate

Score the previous plan and extract a lesson when reality diverged from the hypothesis.

Route

Deliver each trigger with the same MCP allowlist, audit banner, and trigger-scoped context.

Reason

Balance temperature, VPD, water, fog, heat, light, cost, lessons, and retrieved context against the next 72 hours.

Plan

Write tactical waypoints for target tightening, hysteresis, misting, fog, fan staging, and water budget.

Dispatch

Every 5 minutes, push changed setpoints to the ESP32; firmware enforces physical state every 5 seconds.

Brief

Post the operator-facing Slack summary: what changed, why it changed, and what a human should watch.

Learn

Logged outcomes become scorecard data and validated lessons for the next planning cycle.

Left: plan outcome scores over 30 days — each dot is a cycle's self-assessment. Right: recent plan history with hypotheses and outcomes.

Step 0: Validate the Previous Plan

Before writing anything new, the planner reviews what happened since its last plan. It reads the plan_journal entry — hypothesis, experiment, expected outcome — then checks actual data: stress hours, zone temperatures, VPD compliance, equipment runtimes, water usage. It scores the plan 1–10. If something unexpected happened, the planner extracts a lesson. Lessons accumulate in the planner_lessons table with confidence that increases each time the pattern is re-confirmed. The claim is not that the AI rewrites itself; it is that future plans can read validated outcomes from earlier failures.

Step 1: Gather Context

The planner assembles structured context from the database:

Current State

Zone temperatures, VPD, humidity, CO2, lux, outdoor weather, active ESP32 mode, relays, and tunables.

Recent Pattern

Yesterday's hourly temperature, VPD, RH, outdoor conditions, and today's heat, cold, and VPD stress hours.

Forward View

72-hour forecast, solar radiation, cloud cover, wind, actual-to-forecast firmware compliance bands, and source-trace tables for crop/dispatcher/readback provenance.

Biology

Active crops, zones, growth stages, VPD targets, health scores, disease risk, and DIF.

Memory

Validated lessons, previous waypoints, plan journal hypotheses, and scorecard outcomes.

Static Context

Website pages describing the greenhouse structure, equipment, zones, crops, known limits, and build notes.

The band trace is the key addition. The planner sees crop targets and the stitched firmware compliance band: historical pushes before now, dispatcher projection after now. The operator graph stays sparse. The source trace table carries the rest: crop target value, dispatcher value, firmware push, cfg readback, latest planner context, and the derived trigger/padding thresholds implied by active tunables. Iris can then plan tactics around those targets: pre-cool before a solar peak, increase misting before a dry afternoon, widen hysteresis when the band is narrow and physics makes it hard to hold.

The local-memory story matters. Iris is not reasoning from a blank prompt. The context bundle pulls current telemetry and prior outcomes, the static-context builder makes the website’s greenhouse documentation available to the agent, and the lessons table gives durable findings a structured place to live. Planner runs are trigger-scoped so context stays bounded while memory remains database-backed and auditable. Semantic similarity search covers lessons, historical plans, site docs, playbooks, and observations, so the planner can compare the next decision against what actually happened last time.

Step 1b: Route the Planner Instance

Not every planning event needs the same model. Verdify uses a dual-Iris policy:

Planner runTrigger-scoped planning

Routine heartbeats, transitions, milestones, and forecast/deviation checks all receive explicit context and an audit banner.

Model routePolicy controlled

The routing policy can choose a smaller or heavier model, but the tool surface and audit requirements stay the same.

Shared contractSame tools and audit trail

All routes use MCP tools, trigger IDs, planner-instance stamps, setpoint validation, scorecards, and the same ESP32 safety boundary.

Step 2: Reason Across All Systems

The planner doesn’t optimize one variable. It reasons across seven interconnected systems simultaneously:

  • Temperature — The south zone runs 9°F hotter than east at peak. temp_high applies to the average, so south is already in COOL_S2 when the average hits threshold.
  • VPD — On a 14% RH day, the system fights VPD stress for 8+ hours. Lowering vpd_high means misters engage earlier — more water, more humidity, less plant stress.
  • Misting — 60-second pulses with 45-second gaps is the tuned sweet spot. Higher VPD weight concentrates water on the driest zone.
  • Economiser — When outdoor enthalpy is lower than indoor, the vent opens for free cooling. The planner understands enthalpy dynamics when planning ventilation strategy.
  • Grow lights — Two circuits (816W + 630W) supplement natural DLI. The planner adjusts per-circuit runtime, lux threshold, hysteresis, and dwell knobs.
  • Irrigation and wetting — Climate misters, scheduled drip/fert paths, and flush paths all respect the same direct-wet activity gate, but demand still comes from the climate loop or the irrigation scheduler.
  • Cost — Electric heat costs 3.9× more per BTU than gas. Running fog during solar production can be cheap, but it still appears in the resource accounting.

Step 3: Write the Plan

The planner writes two categories of parameters: Target tightening (optional, clamped to crop band): The crop band sets the outer envelope (e.g., temp 72-78F at midday). The planner can tighten within the band on mild days. If it sets temp_high=75, the ESP32 targets 75 instead of 78. On extreme days, it leaves these alone and the band edges are used. Values are always clamped to the band. The planner cannot exceed crop science limits. Tactical parameters are the planner’s main output:

Band Chasing

vpd_hysteresis, temp_hysteresis, and optional target tightening inside crop limits.

Cooling

d_cool_stage_2, fan cycling limits, and enthalpy-based economiser enablement.

Misting

mister_engage_kpa, pulse duration, pulse gap, and daily water budget.

Fog

fog_burst_min and escalation timing when misters cannot control VPD.

Heat

Minimum heat on/off times and staged electric/gas heat behavior.

A typical plan has 8-16 waypoints at natural breakpoints: pre-dawn, dawn, morning ramp, solar noon, afternoon peak, decline, evening, night. Each new plan atomically replaces all future waypoints from previous plans.

The Slack brief is not a separate source of truth. It is the readable operator version of the same plan_journal and setpoint_plan records: plan ID, hypothesis, forecast context, expected risks, and watch items. See Slack Operations for screenshots of a successful SUNRISE plan, daily operator tasks, and a forecast-deviation adjustment.

AI-Writable Tunables

The canonical tunable reference is now generated from the registry, MCP contracts, ingestor maps, firmware source, and live database evidence:

AI Tunables Traceability

Iris can write only registry-approved tunables. set_plan is stricter than set_tunable: routine sunrise/sunset plans must include the 37 tactical Tier 1 parameters at each waypoint and may not include crop-band parameters. set_tunable can make a one-shot adjustment to any planner-pushable registry entry; band-owned values are still clamped or overwritten by the band dispatcher.

Write path: Iris calls MCP set_plan or set_tunable; MCP validates names and registry bounds, stamps trigger audit data, and writes setpoint_plan. v_active_plan resolves the active value per parameter. The dispatcher pushes changed values to ESPHome number/switch entities and logs final writes in setpoint_changes, including trigger_id and planner_instance. setpoint_snapshot stores cfg_* readbacks from firmware so pushed intent can be checked against actual configured state.

Tunable lanePlanner can doFirmware still owns
Routine plan contractWrite 37 tactical Tier 1 parameters at each waypoint.Relay timing, mode priority, hard safety rails, dwell enforcement, and physical interlocks.
One-shot knobsMake bounded tactical adjustments through set_tunable.Clamp behavior, readback truth, and physical actuator state.
Band-owned valuesRead for context; request explicit one-shot overrides only when justified.Crop-band recomputation and controller enforcement.
Non-writable safety/readback valuesRead for context only.Emergency heat/cool/VPD safety behavior and sensor freshness.

The important implementation nuance is mister_engage_kpa: it is effectful, but it is not the firmware state-machine entry trigger. Firmware enters humidification from vpd_high plus vpd_watch_dwell_s; mister_engage_kpa gates physical S1 mister pulses once humidity or zone demand exists, and zone stress can bypass the global threshold.

Activity And Direct-Wet Gates

Lighting and direct plant wetting now share one schedule concept: global biological activity. The dispatcher mirrors the main lighting circuit’s start hour and qualified-light-minutes target into activity_start_hour, activity_start_minute, and activity_duration_min. Firmware then calculates per-zone wet windows with tunable offsets and drydown holds.

The default direct-wet policy is zone-generic:

PathStart offsetDrydown holdRelays covered
Wall drip60 min120 minwall clean/fert drip and flush
South misters60 min120 minsouth clean/fert misters
West misters60 min120 minwest clean/fert misters
Center wetting120 min180 mincenter mister and center clean/fert drip

direct_wet_min_temp_f defaults to 65°F and blocks automated direct wetting when the house is too cold. sw_direct_wet_gate_enabled can disable the gate, but normal operation leaves it on.

Fertigation is scheduler-style. irrig_wall_days_mask and irrig_center_days_mask choose watering days. irrig_wall_fert_days_mask and irrig_center_fert_days_mask choose fertigation days; when a fert mask is nonzero, it supersedes the older every-N-cycle fert setting. Fert jobs open the fert master, run the zone fert path, then flush with clean water.

Step 4: Journal the Hypothesis

Every plan writes a structured journal entry: what the world looks like, what the planner thinks will happen, one specific experiment being tested, and a measurable expected outcome. The next cycle validates the hypothesis and scores the result. This isn’t logging for debugging. It’s the experimental protocol that turns each cycle into a learning opportunity.

Step 5: Dispatch to the ESP32

The dispatcher runs every 5 minutes inside the ingestor service. It reads the active plan, compares each value to what the ESP32 currently reports, and pushes only changed values via aioesphomeapi (encrypted, immediate). cfg_* readbacks in setpoint_snapshot confirm what actually landed on the controller. Between dispatches, the ESP32 runs completely autonomously. If the AI layer goes offline, the controller keeps the last valid setpoints and hard safety rails. The /setpoints endpoints remain aligned for diagnostics and recovery tooling, but the current production firmware does not run an HTTP setpoint poller.

The Learning System

Left: forecast temperature bias over 30 days. Right: setpoint write rate and oscillation count. Confidence levels:
  • Low: First observation. Might be coincidence.
  • Medium: Confirmed 2+ times under similar conditions.
  • High: Validated 5+ times. Mandatory unless conditions clearly differ. The planner must check every active lesson before finalizing a plan. See Lessons Learned for the full list.

Planning Goals (Priority Order)

  1. Minimize VPD stress hours — plant transpiration balance
  2. Minimize heat stress hours — physics-limited by cooling capacity
  3. Maximize DLI — daily light integral for growth
  4. Minimize water usage — misting water isn’t free
  5. Minimize energy cost — USD 0.111/kWh, USD 0.83/therm, USD 0.00484/gal
  6. Maintain positive DIF — 8–15°F day warmer than night
  7. Maximize compliance % — time within setpoint bands These are ordered. The planner will spend water and energy to reduce VPD stress. It won’t sacrifice plant health for a lower utility bill.

Control Loop Performance

Temperature (left) and VPD (right) vs setpoint bands. Where the line tracks the plan, the system is in control. Where it diverges — typically on hot, dry spring afternoons — the greenhouse has hit a physics limit. The planner knows this. It optimizes for minimal stress, not zero stress. ---

Full dashboards: Controller ↗ · Planning ↗