diff --git a/README.md b/README.md index 670ab50..2cc4a38 100644 --- a/README.md +++ b/README.md @@ -111,7 +111,7 @@ If a valve stops responding, NeverDry keeps trying — six attempts, with a grow | `sensor._last_duration` | s | Duration of the last session | | `sensor._session_water` | L | Water delivered in the current session | | `sensor._yearly_water` | L | Cumulative water delivered this year | -| `sensor._rain` | mm | Cumulative rain accounted for the zone | +| `sensor._rain_yearly` | L | Rain this zone received this calendar year: the yearly rain in mm times the zone's area | | `sensor._kc` | — | Current crop coefficient (seasonal) | | `sensor._flow_rate` | L/h | Design flow rate: what the zone was built to deliver | | `sensor._measured_flow_rate` | L/h | What the last supervised valve test measured, kept as a series so a slow decline reads as clogging emitters | @@ -129,7 +129,7 @@ If a valve stops responding, NeverDry keeps trying — six attempts, with a grow | `button._stop` | — | Stop the zone's irrigation immediately | | `button._reset_valve` | — | Unlock a valve that NeverDry blocked after repeated failures | -All sensors that carry a physical unit declare the proper HA `device_class` (e.g. `precipitation`, `volume_storage`, `volume_flow_rate`). Home Assistant automatically converts the displayed unit to your system preference: go to **Settings → System → General → Unit system** to switch between metric and imperial. Deficit values appear in mm or inches; volumes in litres or gallons; flow rate in L/h or gal/h; ET rate in mm/h or in/h. +All sensors that carry a physical unit declare the proper HA `device_class` (e.g. `precipitation`, `volume_storage`, `volume_flow_rate`). Home Assistant automatically converts the displayed unit to your system preference: go to **Settings → System → General → Unit system** to switch between metric and imperial. Deficit values appear in mm or inches; volumes in litres or gallons; ET rate in mm/h or in/h. Flow rates appear in L/h or gal/h: Home Assistant does not convert flow rates between unit systems, so NeverDry picks the unit from the same setting. ## Dashboard — Zone Card @@ -258,7 +258,7 @@ NeverDry is configured entirely through the UI — no YAML required. |-------|----------|---------|-------------| | Zone name | Yes | — | Display name | | Valve | No | — | Entity controlling the valve — `switch.*` or `valve.*` (omit for monitoring-only mode) | -| Area | Yes | — | Irrigated area [m²] | +| Area | Yes | - | Irrigated area [m², ft² in imperial] | | System type | Yes | — | Drip / micro-sprinkler / sprinkler / manual | | Efficiency | No | (from type) | Override distribution efficiency [0.1–1.0] | | Plant family | No | — | Sets seasonal Kc profile | @@ -266,7 +266,7 @@ NeverDry is configured entirely through the UI — no YAML required. | Site exposure | No | Full sun, open | Sun/wind exposure preset — multiplies the Kc [0.60–1.20] | | Custom microclimate factor | Advanced only | — | Explicit exposure factor [0.1–1.5], used when exposure is *Advanced* | | Guard flow rate | For timer mode | - | Valve flow rate [L/h, gal/h in imperial]. Required for timer-based zones; recommended for flow-meter and volume-dosing zones too, where it drives the expected duration and the safety-timeout scaling | -| Threshold | No | 20.0 | Mode A trigger [mm] | +| Threshold | No | 20.0 | Mode A trigger [mm, in in imperial] | | Battery sensor | No | — | Valve battery sensor — mirrored in the zone device card | | Flow meter sensor | No | — | Either a cumulative volume counter or an instantaneous flow rate — both work, and NeverDry tells them apart by the unit. See [valves tested](docs/valve-compatibility.md) for what your model exposes. Mirrored in the zone device card | diff --git a/docs/VISION.md b/docs/VISION.md index 20f4c81..423273a 100644 --- a/docs/VISION.md +++ b/docs/VISION.md @@ -41,7 +41,7 @@ A timer knows one thing: the clock. NeverDry works across several domains at onc |---|---|---|---| | **Agronomy** | Evapotranspiration, crop coefficient, the soil's water reserve, the deficit, useful rain | Which plants, and where they stand | Manual crop coefficient, custom soil, soil moisture probe, evapotranspiration method | | **Hydraulics** | Volume from flow and time, serial or parallel runs, the master valve or pump and its linger, shared water lines | The valve, and its flow rate (or how long you usually water) | A water network the zones draw from, a flow or line limit, a master, a water meter | -| **Electrical** (a little) | How many solenoids a board can hold open, confirmed commands, a valve stuck open | Nothing | A named board and its limit | +| **Electrical** (a little) | How many solenoids a board (transformer) can hold open, confirmed commands, a valve stuck open | Nothing | A valve coordinator the zones are counted on, and its limit | | **Climate and place** | Outdoor, greenhouse or indoor; frost; sun exposure | Where the garden is | A second environment (a greenhouse with its own sensors), exposure, additional sensors | | **Reliability** | Watchdog, learned latency, reachability, valve self-test | Nothing | Thresholds and timings | diff --git a/docs/assets/domain_model_uml.svg b/docs/assets/domain_model_uml.svg index b82e8b0..5942839 100644 --- a/docs/assets/domain_model_uml.svg +++ b/docs/assets/domain_model_uml.svg @@ -1 +1 @@ -

feeds ET+rain to

gates by capability match

advances deficit via

returns

holds, settles

sits at

requests liters (VALVE)

returns

requests (BY_HAND)

returns (declared)

satisfies structurally

settles deficit with

decides when

returns

ON while any is active

draws from

asks for a line, through the zone

head of the line

asks to start zones, told how many started

opens up to its limit

1

1

1

*

1

1

1

*

1

0..1

1

0..1

*

*

Environment

+temperature_sensor

+rain_sensor

+humidity_sensor

+wind_speed_sensor

+net_radiation_sensor

+temp_max_sensor

+temp_min_sensor

+soil_moisture_sensor

+rain_probability_sensor

+latitude

+rain_delay : RainDelayPolicy

+yearly_rain_mm : one sky

+declared_sensors() : SensorKind set

+satisfies(required) : bool

+missing_for(required) : SensorKind set

Zone

+name

+area_m2

+efficiency

+plant_family / manual_kc

+exposure / microclimate_factor

+placement : Placement

+actuation : VALVE or BY_HAND, default VALVE

+d_max : own reservoir

+threshold_mm

+cycle_soak : CycleSoakRule

+network : WaterNetwork, default the entry's implicit one

+deficit : Deficit, starts at 0

+counters : WaterCounters

+effective_kc(base_kc)

+accumulate(dt_h, et_h, base_kc, rain_mm) : Deficit

+water_demand_l() : liters

+needs_water() : bool

+begin_cycle()

+credit_delivery(d : Delivery) : Deficit

+settle(d : Delivery, source, at) : Deficit

+mark_irrigated(source, at) : Deficit

«enumeration»

Placement

OUTDOOR / PATIO / GREENHOUSE / INDOOR

+receives_rain : only OUTDOOR

+driven_by_outdoor_et : OUTDOOR, PATIO

«protocol»

Delivery

+liters_delivered

+elapsed_s

Scheduler

+concurrency : ConcurrencyPolicy

+min_service_interval_s

deferred: time_windows, queue, interleave_during_soak

proposed: freeze gate, by-hand requests

+allows_overlap() : bool

+evaluate_scheduled(zone, is_running) : Decision

+evaluate_reactive(zone, is_running, is_throttled) : Decision

+next_eligible(zones, is_running) : Zone

Decision

+should_irrigate

+trigger : Trigger

+reason : SkipReason

«abstract»

Driver

+entity_adapter : valve.* or switch.

+adaptive_timeout

+safety_layers : watchdog, close on error/stop/restart

+ping_interval_min

+turn_on() : confirmed

+turn_off() : confirmed

+ping() : alive or unreachable

ZoneDriver

+delivery_mode : native_volume or time_x_flow

+flow_rate_lpm

+flow_telemetry : optional

+zero_flow_guard

+deliver(liters) : DeliveryResult

MasterDriver

+start_delay_s : 1 s after the first zone opens, optional

+off_delay_s : 5 s after the last confirmed close, optional

+follow(any_zone_driver_active)

«proposed»

WaterNetwork

+name : its own entry, on the instance

+hydraulic_limit : optional, max flow or max lines

+master : MasterDriver, optional

default: implicit, one per Environment entry

+admit(zone) : bool

+next_turn(waiting entries) : entry

«proposed»

ValveCoordinator

+name : optional, none = virtual

+max_active_zones : declared net of the master

+zone_drivers : ZoneDriver set

default: one virtual coordinator, limit 1

+start(zones) : started count

+on_outside_open(driver) : warn, timer runs suspect

ManualActuator

+role : manual

+request_irrigation(liters) : DeliveryResult

+mark_irrigated(liters?) : DeliveryResult

DeliveryResult

+liters_delivered

+quality : measured, estimated, partial, delayed, low_confidence, declared

+elapsed_s

+revise(measured_liters)

«abstract»

WaterBalanceModel

+reference_frame : ReferenceFrame

+is_stateful : bool

+deficit : Deficit

+step(inputs) : Deficit

+apply_irrigation(mm) : Deficit

+reset() : Deficit

«abstract»

ETBalanceModel

+kc

+et_rate(inputs) : mm/h

+step(inputs) : Deficit

ETModel

+alpha, t_base

+et_hourly(temp_c) : mm/h

HargreavesModel

+latitude_deg

+et0_daily(Tmax, Tmin, doy) : mm/day

PenmanMonteithModel

+pressure_kpa

+et0_daily(T, rh, wind, Rn) : mm/day

VWCSystemModel

+field_capacity, root_depth

+step(VWCReading) : Deficit

VWCPerZoneModel

+source : probe/zone id

Deficit

+value_mm

+frame : ReferenceFrame

+source : per-zone identity

+is_comparable_to(other) : bool

+as_liters(area_m2) : liters

\ No newline at end of file +

feeds ET+rain to

gates by capability match

advances deficit via

returns

holds, settles

sits at

requests liters (VALVE)

returns

requests (BY_HAND)

returns (declared)

satisfies structurally

settles deficit with

decides when

returns

ON while any is active

draws from

asks for a line, through the zone

head of the line

asks to start zones, told how many started

opens up to its limit

counted on

1

1

1

*

1

1

1

*

*

1

0..1

1

0..1

*

*

1

Environment

+temperature_sensor

+rain_sensor

+humidity_sensor

+wind_speed_sensor

+net_radiation_sensor

+temp_max_sensor

+temp_min_sensor

+soil_moisture_sensor

+rain_probability_sensor

+latitude

+rain_delay : RainDelayPolicy

+yearly_rain_mm : one sky

+declared_sensors() : SensorKind set

+satisfies(required) : bool

+missing_for(required) : SensorKind set

Zone

+name

+area_m2

+efficiency

+plant_family / manual_kc

+exposure / microclimate_factor

+placement : Placement

+actuation : VALVE or BY_HAND, default VALVE

+d_max : own reservoir

+threshold_mm

+cycle_soak : CycleSoakRule

+network : WaterNetwork, default the entry's implicit one

+coordinator : ValveCoordinator, default the entry's virtual one

+deficit : Deficit, starts at 0

+counters : WaterCounters

+effective_kc(base_kc)

+accumulate(dt_h, et_h, base_kc, rain_mm) : Deficit

+water_demand_l() : liters

+needs_water() : bool

+begin_cycle()

+credit_delivery(d : Delivery) : Deficit

+settle(d : Delivery, source, at) : Deficit

+mark_irrigated(source, at) : Deficit

«enumeration»

Placement

OUTDOOR / PATIO / GREENHOUSE / INDOOR

+receives_rain : only OUTDOOR

+driven_by_outdoor_et : OUTDOOR, PATIO

«protocol»

Delivery

+liters_delivered

+elapsed_s

Scheduler

+concurrency : ConcurrencyPolicy

+min_service_interval_s

deferred: time_windows, queue, interleave_during_soak

proposed: freeze gate, by-hand requests

+allows_overlap() : bool

+evaluate_scheduled(zone, is_running) : Decision

+evaluate_reactive(zone, is_running, is_throttled) : Decision

+next_eligible(zones, is_running) : Zone

Decision

+should_irrigate

+trigger : Trigger

+reason : SkipReason

«abstract»

Driver

+entity_adapter : valve.* or switch.

+adaptive_timeout

+safety_layers : watchdog, close on error/stop/restart

+ping_interval_min

+turn_on() : confirmed

+turn_off() : confirmed

+ping() : alive or unreachable

ZoneDriver

+delivery_mode : native_volume or time_x_flow

+flow_rate_lpm

+flow_telemetry : optional

+zero_flow_guard

+deliver(liters) : DeliveryResult

MasterDriver

+start_delay_s : 1 s after the first zone opens, optional

+off_delay_s : 5 s after the last confirmed close, optional

+follow(any_zone_driver_active)

«proposed»

WaterNetwork

+name : its own entry, on the instance

+hydraulic_limit : optional, max flow or max lines

+master : MasterDriver, optional

default: implicit, one per Environment entry

+admit(zone) : bool

+next_turn(waiting entries) : entry

«proposed»

ValveCoordinator

+name : its own entry, on the instance

+max_active_zones : declared net of the master

+zone_drivers : ZoneDriver set

default: implicit, one per Environment entry, limit 1

+start(zones) : started count

+on_outside_open(driver) : warn, timer runs suspect

ManualActuator

+role : manual

+request_irrigation(liters) : DeliveryResult

+mark_irrigated(liters?) : DeliveryResult

DeliveryResult

+liters_delivered

+quality : measured, estimated, partial, delayed, low_confidence, declared

+elapsed_s

+revise(measured_liters)

«abstract»

WaterBalanceModel

+reference_frame : ReferenceFrame

+is_stateful : bool

+deficit : Deficit

+step(inputs) : Deficit

+apply_irrigation(mm) : Deficit

+reset() : Deficit

«abstract»

ETBalanceModel

+kc

+et_rate(inputs) : mm/h

+step(inputs) : Deficit

ETModel

+alpha, t_base

+et_hourly(temp_c) : mm/h

HargreavesModel

+latitude_deg

+et0_daily(Tmax, Tmin, doy) : mm/day

PenmanMonteithModel

+pressure_kpa

+et0_daily(T, rh, wind, Rn) : mm/day

VWCSystemModel

+field_capacity, root_depth

+step(VWCReading) : Deficit

VWCPerZoneModel

+source : probe/zone id

Deficit

+value_mm

+frame : ReferenceFrame

+source : per-zone identity

+is_comparable_to(other) : bool

+as_liters(area_m2) : liters

\ No newline at end of file diff --git a/docs/design/scheduler.md b/docs/design/scheduler.md index b6074da..4e8f8a1 100644 --- a/docs/design/scheduler.md +++ b/docs/design/scheduler.md @@ -26,15 +26,16 @@ environments, outdoor and greenhouse, and the shared pump declined) and §8.4.7 (the automation a user built instead, read as a specification). **Added 2026-10-10**, and not yet reflected in section 14: section 8.5 (the -electrical limit of a controller board, GH #320, held by a valve coordinator +electrical limit of a controller board (transformer), GH #320, held by a valve coordinator rather than by the scheduler), section 8.6 (the master stays on for the whole sequence, GH #95), section 8.7 (an optional hydraulic limit, declared from design figures, which revises the refusal in section 8 without undoing -the removal) and section 8.8 (two entry types, Environment and water network, -the network named by each zone, which closes Q9 and revises section 8.4.6; the +the removal) and section 8.8 (three entry types, Environment, water network +and valve coordinator, the network and the coordinator named by each zone, which closes Q9 and revises section 8.4.6; the network was first a name on the entry, and moved to the zone the same day; on 2026-10-11 the entry types by place were withdrawn, place and hand-watering -being facts of the zone). +being facts of the zone, and the valve coordinator became a third entry type, the +network's sibling). Nothing is binding while the note is `Draft`: a working answer is still a proposal. @@ -308,7 +309,7 @@ main. Admitting a zone to run concurrently requires two conditions, evaluated more line (sections 8.7 and 8.8). Both are hydraulic. Passing them makes a run *admissible*, not started: the -electrical limit of the board is a separate yes, and it is not the scheduler's +electrical limit of the board (transformer) is a separate yes, and it is not the scheduler's to give (section 8.5). **There was a third, and removing it is the substantive change here.** It read: @@ -993,9 +994,13 @@ So concurrency has two owners: optional limit (sections 8.7 and 8.8), both declared by the person who sees the plumbing. - **A valve coordinator** holds the electrical half: the zones wired to one - board and `max_active_zones`, how many of them that board may hold open. - A coordinator is optional and named on the instance, so one board can drive - zones of several entries (section 8.8). + board (transformer) and `max_active_zones`, how many of them that board may + hold open. + It is an **entry of its own**, the water network's + sibling, and each zone names the coordinator its valve is counted on, so one + coordinator can hold zones of several entries (section 8.8). It holds one + number, whatever imposes it (a transformer, a controller that runs one + station at a time, a cloud service), and models no particular controller. The scheduler asks the coordinator to start the zones it has admitted; the coordinator starts as many as its limit allows and **answers how many it @@ -1009,9 +1014,12 @@ Three choices keep each side from having to know too much: - **The master is not counted.** It stays hydraulic and keeps following zone activity. The user declares the coordinator's limit net of it: a board rated for one station plus master has a limit of 1. -- **The default coordinator is virtual.** An installation that configures none - gets one implicit coordinator holding every valve with a limit of 1: today's - behaviour, stated instead of emerging from the handlers. +- **The default coordinator is virtual.** With no valve coordinator declared, each + Environment entry has one implicit virtual coordinator holding every one of its + valves with a limit of 1: today's behaviour, stated instead of emerging from + the handlers. A zone naming no coordinator is counted there; a removed coordinator's + zones return there, named by the form first; a zone whose coordinator is not + loaded does not start until it answers. - **An outside open is observed, never refused.** NeverDry does not own the relays. A valve opened by hand or by another automation while another one on the same board is open warns, naming both, and in timer mode marks both runs as @@ -1084,37 +1092,47 @@ pair and every form that touches either side calls it: | What changes | What is checked again | |---|---| -| A zone, added or edited | its design flow rate against the maximum flow of its network, if one is declared; the network and the coordinator it names exist | +| A zone, added or edited | its design flow rate against the maximum flow of its network, if one is declared; the network and the valve coordinator it names exist | | A network's maximum flow, set or lowered | every zone on that network, from every entry; any whose design flow rate exceeds it is named | -| A coordinator, added, edited or removed | the zones assigned to it; a removed coordinator's zones return to the virtual default, and the form says so | +| A valve coordinator entry, added, edited or removed | the zones that name it, from every entry; a removed coordinator's zones return to their entry's virtual coordinator, and the form names them before the removal is confirmed | | A network entry removed | the zones that name it, from every entry; they return to their entry's implicit network, and the form names them before the removal is confirmed | A zone above the maximum flow earns a warning with a confirmation, not a refusal: it is most often a unit error (GH #318), and occasionally a zone that will only ever run alone. A static test checks that every step editing a zone, the limit or -a coordinator calls the shared function, since two forms with two copies of one +a valve coordinator calls the shared function, since two forms with two copies of one rule is how a field went missing from the zone edit form (GH #234). -### 8.8 Environments and water networks: the two entry types +### 8.8 Environments, water networks and valve coordinators: the three entry types *Decided 2026-10-10, from discussion #239 and GH #320; revised the same day: the network became an entry of its own, named by each zone; revised 2026-10-11: -entries are not typed by place.* "One valve at a time" has turned out to be +entries are not typed by place, and the valve coordinator became an entry of its own, +the network's sibling.* "One valve at a time" has turned out to be three questions, and each has its own object: | Object | Question | Level | |---|---|---| | **Scheduler** | *When*, and in what order, for the zones of one Environment | One per Environment entry | | **Water network** | *Is there water* for one more line, and whose turn is it? | One per network entry, on the instance; one implicit per Environment entry when none is named | -| **Valve coordinator** | *Is there power* for one more solenoid? (section 8.5) | One per board, on the instance | +| **Valve coordinator** | *Is there power* for one more solenoid? (section 8.5) | One per valve coordinator entry, on the instance; one implicit virtual coordinator per Environment entry when none is named | A zone starts only when its network and its coordinator both say yes. The schedulers never talk to each other. -**Two entry types: Environment and water network.** An Environment entry is a -climate: its sensors, its latitude, its scheduler and its zones. A garden is -one; a greenhouse with its own sensors is a second. The zone is always the -sub-element of an Environment, never an entry type. +**Three entry types: Environment, water network and valve coordinator.** An +Environment entry is a climate: its sensors, its latitude, its scheduler and its +zones. A garden is one; a greenhouse with its own sensors is a second. A water +network holds the water side (sections 8.6 and 8.7), a valve coordinator the electrical +side (section 8.5). The zone is always the sub-element of an Environment, never +an entry type; it names the network it draws from and the coordinator its valve is +counted on, as a rule one per transformer. + +*Revised 2026-10-11.* The coordinator was first a board name typed or created +in the zone form: the shape the network had just left, and for the same reason +it moved. A coordinator holds zones of several Environments, so its limit cannot +live in any one of them, and a name typed in two forms can split one in two. As an entry +it is declared once, with its limit, and the zones pick it from a list. **The scheduler has no type of its own**; its behaviour follows facts it already has. The freeze interlock (section 9.2) reads its Environment's temperature, so @@ -1168,7 +1186,7 @@ Coordinating a shared pump across two environments was refused because it meant part of NeverDry checking what another had already taken, with no object to hold that state. The water network is that object. -**Simple by default: every new field is optional.** Network, board, hydraulic +**Simple by default: every new field is optional.** Network, valve coordinator, hydraulic limit, master and actuation all default to today's behaviour. Two installations are the test, and neither may meet a question about any of them: @@ -1179,26 +1197,27 @@ How that holds in the forms: - The **first entry** on an instance asks nothing new: it is an Environment, as every installation today is. -- **Adding an entry** from the second on opens a menu of two: Environment and - water network. +- **Adding an entry** from the second on opens a menu of three: Environment, + water network and valve coordinator. - **The zone form** gains three optional fields in a collapsed section: the **network**, listing the network entries on the instance (empty: the entry's - implicit network, exactly as today), the **board** (empty: the virtual - coordinator), and **watered by hand** (off: a valve, as today). With watered by - hand on, the valve, network and board fields do not apply. + implicit network, exactly as today), the **valve coordinator**, listing the coordinator + entries on the instance (empty: the entry's virtual coordinator, limit 1, exactly as + today), and **watered by hand** (off: a valve, as today). With watered by + hand on, the valve, network and coordinator fields do not apply. - **Existing entries are Environments**, and nothing changes in them. Zones whose placement is greenhouse or indoor keep working as now; moving them to an Environment of their own is a choice, not a migration. -| Installation | Entries | Network | Boards | +| Installation | Entries | Network | Coordinator | |---|---|---|---| -| A terrace with seven pots | 1 Environment | none: implicit | none (virtual) | -| Four zones on one line | 1 Environment | none: implicit | none (virtual) | -| Four zones on one line, with a pump | 1 Environment + 1 network | the network, named by the four zones | none (virtual) | -| Two Netro controllers on one main | 1 Environment | none: implicit | 2 named, limit 1 each | -| Garden and greenhouse with its own sensors, on one main | 2 Environments + 1 network | the network, named by the zones of both | as wired | -| Garden on the mains, greenhouse with its own sensors on a tank | 2 Environments | none, or a network for the tank | as wired | -| Drip from the mains beside a lawn on a tank pump | 1 Environment + 1 network | the lawn names the pump's network; the drip names none | none (virtual) | +| A terrace with seven pots | 1 Environment | none: implicit | none: virtual | +| Four zones on one line | 1 Environment | none: implicit | none: virtual | +| Four zones on one line, with a pump | 1 Environment + 1 network | the network, named by the four zones | none: virtual | +| Two Netro controllers on one main | 1 Environment + 2 coordinators | none: implicit | each zone names its controller, limit 1 each | +| Garden and greenhouse with its own sensors, one controller on one main | 2 Environments + 1 network + 1 coordinator | the network, named by the zones of both | the coordinator, named by the zones of both | +| Garden on the mains, greenhouse with its own sensors on a tank | 2 Environments | none, or a network for the tank | none: virtual, one per Environment | +| Drip from the mains beside a lawn on a tank pump | 1 Environment + 1 network | the lawn names the pump's network; the drip names none | none: virtual | | House plants beside the garden | the garden's Environment | none: watered by hand | none | ## 9. The irrigability envelope @@ -1835,16 +1854,15 @@ anything wider waits for the installation that cannot be expressed that way. **2026-10-10: the electrical limit narrows it further** (section 8.5, GH #320). "One valve at a time" turns out to be two rules. The hydraulic one is the subject of this question. The electrical one has its own owner, a valve -coordinator per board, with a virtual default of one valve at a time, so it no +coordinator per transformer, with a virtual default of one valve at a time, so it no longer needs a boundary to be chosen here. What is left for Q9 is only where *hydraulic* permission to overlap is declared. **2026-10-10: closed by section 8.8.** The boundary is neither the entry nor the zone but the **water network**, an entry of its own that each zone names, shared by every zone that draws from the same line, whichever entry it belongs to. An -Environment entry is a climate, with its own scheduler; a board is a named -coordinator. The -failure the entry boundary allowed, two entries on one main, is now expressed by +Environment entry is a climate, with its own scheduler; a valve coordinator is an +entry of its own, the network's sibling. The failure the entry boundary allowed, two entries on one main, is now expressed by their zones naming the same network. ## 15. Consequences for the domain model diff --git a/docs/design_domain_object_model.md b/docs/design_domain_object_model.md index fdcf936..890cf01 100644 --- a/docs/design_domain_object_model.md +++ b/docs/design_domain_object_model.md @@ -1,7 +1,7 @@ # Design — Domain Object Model **Status:** Accepted (ADR), 2026-08-16 -**Date:** 2026-07-05 (updated 2026-07-06 with review feedback from GH #74; 2026-07-23 aligned with the water-balance reference model; 2026-07-26 the water-balance model made a first-class object; 2026-08-01 RFC to dissolve `System` into `Environment` + capability-matched models; 2026-10-10 a valve coordinator proposed for the electrical limit of a controller board, GH #320; 2026-10-10 the master's start order, linger and per-zone opt-out, GH #95; 2026-10-10 entries by place and a named water network shared across entries, discussion #239; 2026-10-10 the water network becomes an entry of its own, each zone names the network it draws from, and the per-zone master opt-out is retired; 2026-10-11 entry types are Environment and water network, place and hand-watering move to the zone) +**Date:** 2026-07-05 (updated 2026-07-06 with review feedback from GH #74; 2026-07-23 aligned with the water-balance reference model; 2026-07-26 the water-balance model made a first-class object; 2026-08-01 RFC to dissolve `System` into `Environment` + capability-matched models; 2026-10-10 a valve coordinator proposed for the electrical limit of a controller board, GH #320; 2026-10-10 the master's start order, linger and per-zone opt-out, GH #95; 2026-10-10 entries by place and a named water network shared across entries, discussion #239; 2026-10-10 the water network becomes an entry of its own, each zone names the network it draws from, and the per-zone master opt-out is retired; 2026-10-11 entry types are Environment and water network, place and hand-watering move to the zone; 2026-10-11 the valve coordinator becomes a third entry type, sibling of the water network) **Related:** GH #74 (actuator abstraction discussion), GH #94 (`valve.*` support), GH #95 (master valve/pump), GH #320 (station limit of a 24 VAC controller), [Water-Balance Reference Model](design_water_balance_reference_model.md) (where the deficit lives), [Domain Model Anomalies](design_domain_model_anomalies.md) (code-verified audit against this model) ## Purpose @@ -32,12 +32,14 @@ Two more objects are **proposed, not written**, and land together with the sched the instance, that each zone names; it holds the hydraulic limit, the master and the turn-taking between entries. Without one, each Environment entry has an implicit network of its own, one line with no master and no limit ([its section](#waternetwork-proposed)); -- the **ValveCoordinator**, which holds the electrical limit of a controller board, how many - solenoids its transformer can hold open at once ([its section](#valvecoordinator-proposed)). +- the **ValveCoordinator**, which holds the electrical limit of a controller board (transformer), how many + solenoids its transformer can hold open at once: the water network's sibling, an entry of its + own (a *valve coordinator*) that each zone names. Without one, each Environment entry has an implicit + virtual coordinator, limit 1 ([its section](#valvecoordinator-proposed)). Why they are separate objects, and why neither is the scheduler, is in the design decisions [Electrical parallel is not hydraulic parallel](#electrical-parallel-is-not-hydraulic-parallel-a-coordinator-not-the-scheduler) -and [Environments and water networks: the two entry types](#environments-and-water-networks-the-two-entry-types). +and [Environments, water networks and valve coordinators: the three entry types](#environments-water-networks-and-valve-coordinators-the-three-entry-types). ## The water-balance model (the *how much*) @@ -151,6 +153,7 @@ classDiagram +threshold_mm +cycle_soak : CycleSoakRule +network : WaterNetwork, default the entry's implicit one + +coordinator : ValveCoordinator, default the entry's virtual one +deficit : Deficit, starts at 0 +counters : WaterCounters +effective_kc(base_kc) @@ -230,12 +233,12 @@ classDiagram class ValveCoordinator { <> - +name : optional, none = virtual + +name : its own entry, on the instance +max_active_zones : declared net of the master +zone_drivers : ZoneDriver set +start(zones) started count +on_outside_open(driver) warn, timer runs suspect - default: one virtual coordinator, limit 1 + default: implicit, one per Environment entry, limit 1 } class ManualActuator { @@ -328,6 +331,7 @@ classDiagram WaterNetwork "1" o-- "0..1" MasterDriver : head of the line Scheduler "1" ..> "*" ValveCoordinator : asks to start zones, told how many started ValveCoordinator "1" o-- "*" ZoneDriver : opens up to its limit + Zone "*" --> "1" ValveCoordinator : counted on ``` `Zone` depends on `Delivery`, the structural protocol, rather than on `DeliveryResult` itself. @@ -397,6 +401,7 @@ reported truth. | `threshold_mm` | attr | Deficit that triggers irrigation | | `cycle_soak: CycleSoakRule` | attr | Dose/pause — a Zone rule, not a Scheduler policy | | `network: WaterNetwork` | attr | **Proposed.** The line this zone draws water from, picked from the networks declared on the instance. Empty: the implicit network of its entry, one line with no master and no limit, which is today's behaviour. The zone form shows the network's master read-only, so the zone says what it depends on without holding a copy of it | +| `coordinator: ValveCoordinator` | attr | **Proposed.** The valve coordinator this zone's valve is wired to, picked from the coordinator entries declared on the instance. Empty: the virtual coordinator of its entry, limit 1, which is today's behaviour. A zone watered by hand names none | | `deficit: Deficit` | attr | Its own deficit, carrying its reference frame. A new zone starts at 0 (D4) | | `counters: WaterCounters` | attr | last / session / total / yearly delivered litres | | `effective_kc(base_kc)` | method | Applies site exposure. The seasonal curve is deliberately *not* recomputed here — copying its plant table in would create the second source of truth anomaly E1 is about | @@ -421,7 +426,7 @@ them, which is what makes the rules testable without a controller. | Member | Kind | Meaning | |---|---|---| | `concurrency` | attr | `SERIAL` (today's behaviour) or `PARALLEL`. Naming it turns an emergent property — both handlers happen to bail when something is running — into a stated policy | -| freeze gate, by-hand requests | behaviour | **Proposed.** One scheduler per Environment entry, with no type of its own: its behaviour follows the facts it already has. The freeze interlock reads **its Environment's** temperature, so a greenhouse with its own sensors gates on the greenhouse air; it applies to `VALVE` zones whose placement is not `INDOOR`. A due `BY_HAND` zone becomes a request to a person rather than a command to a valve. See [Environments and water networks: the two entry types](#environments-and-water-networks-the-two-entry-types) | +| freeze gate, by-hand requests | behaviour | **Proposed.** One scheduler per Environment entry, with no type of its own: its behaviour follows the facts it already has. The freeze interlock reads **its Environment's** temperature, so a greenhouse with its own sensors gates on the greenhouse air; it applies to `VALVE` zones whose placement is not `INDOOR`. A due `BY_HAND` zone becomes a request to a person rather than a command to a valve. See [Environments, water networks and valve coordinators: the three entry types](#environments-water-networks-and-valve-coordinators-the-three-entry-types) | | `min_service_interval_s` | attr | Rate limit between service calls with the same key | | `evaluate_scheduled(zone, is_running) → Decision` | method | The daily top-up. Deliberately does **not** consult the threshold: gating a schedule on the reactive threshold turns every scheduled run into a reactive one (AI-183). Only a zone already full is skipped | | `evaluate_reactive(zone, is_running, is_throttled) → Decision` | method | Mode A: water once the deficit crosses the zone's threshold | @@ -511,21 +516,43 @@ for one more line, and whose turn it is. **Proposed, not written. It lands with the scheduler work, together with its wiring.** The electrical side of "how many at once": the only object that knows how many zone solenoids -its controller board can hold open. A 24 VAC controller has one transformer, one common wire +its controller board (transformer) can hold open. A 24 VAC controller has one transformer, one common wire and a set of station terminals, and the transformer is sized for a fixed number of solenoids (on most residential controllers, one station plus the master). Raised in GH #320. | Member | Kind | Meaning | |---|---|---| -| `max_active_zones` | attr | Zone valves the board may hold open at once, **declared by the user net of the master**: on a board rated for one station plus master, the limit is 1. The coordinator never counts the master, so neither it nor the scheduler needs to know how the board is wired | -| `name` | attr | Optional. Without one, the zone uses the entry's virtual coordinator. A name is chosen from the boards already declared on the instance, or created, so one board can drive zones of several entries (a garden and a greenhouse on one controller) | -| `zone_drivers` | attr | The `ZoneDriver`s wired to this board, from whichever entry they belong to | +| `max_active_zones` | attr | Zone valves the board (transformer) may hold open at once, **declared by the user net of the master**: on a board rated for one station plus master, the limit is 1. The coordinator never counts the master, so neither it nor the scheduler needs to know how the board is wired | +| `name` | attr | The valve coordinator entry's title. Zones pick it from a list of the coordinators declared on the instance, so one coordinator can hold zones of several Environments (a garden and a greenhouse on one controller) and a typing slip cannot split one coordinator in two | +| `zone_drivers` | attr | The `ZoneDriver`s of the zones that name this coordinator, from whichever Environment they belong to | | `start(zones) → started` | method | Opens as many of the requested zones as the limit allows and reports **how many it started**. It does not queue: the scheduler knows the answer and keeps the rest waiting | | `on_outside_open(driver)` | method | A valve opened by hand or by another automation is only observed, never refused or closed: NeverDry does not own the relays. An overlap on one board warns, naming both valves, and in timer mode marks both runs as suspect, since neither delivery can be trusted | -**The default is virtual.** With no coordinator configured, one implicit coordinator holds every -zone valve with a limit of 1: the behaviour of today, one valve at a time, stated instead of -emerging from the handlers. +**A valve coordinator is an entry of its own, the water network's sibling.** It is one of the types +offered when an entry is added, declared once with its own device and options form, and **each +zone names the coordinator its valve is counted on**, as it names the network it draws from. The two are +independent: a network is the water, a coordinator is how many valves may be open at once, and +one controller can drive valves on two networks just as one network can be fed through two +controllers. + +**General, not a model of any controller.** The coordinator holds one number, how many valves +may be open at once, whatever imposes it: a 24 VAC transformer sized for one solenoid, a +controller that runs one station at a time by design, a relay module with an interlock, a cloud +service that accepts one run per device. NeverDry does not model the device behind it, its +wiring, its protocol or its timing; whoever configures it reads the limit off the device and +declares it. A property that belongs to one brand of controller is not added here: it stays in +that controller's integration, or it is expressed through the entities Home Assistant already +exposes. + +**The defaults** mirror the network's, each the safe one: + +| Case | What happens | +|---|---| +| No valve coordinator entry declared | Each Environment entry has an **implicit virtual coordinator** holding every one of its zone valves, limit 1: today's behaviour, one valve at a time, stated instead of emerging from the handlers | +| A zone that names no coordinator | Its valve is counted on its entry's virtual coordinator | +| A coordinator entry removed while zones name it | Those zones return to their entry's virtual coordinator, and the form says so, naming them, before the removal is confirmed | +| A coordinator named by a zone is not loaded, or failed to set up | The zone **does not start** until the coordinator answers. Opening a valve without knowing how many the transformer already holds is worse than waiting; the zone reports why it is waiting | +| A zone watered by hand | Names no coordinator: it has no solenoid | **Composition, not a third `Driver`.** `Driver` means one actuator and one entity: adapter, confirmed commands, adaptive timeout, watchdog and liveness probe all assume a real switch. @@ -609,7 +636,7 @@ Optional, for whoever wants to look under the bonnet: a signed start offset and master belongs to the **water network** it heads, not to an entry and not to a zone: it is declared once on the network, two entries whose zones draw from one network share one master, and a second pump is a second network. It is **not** -counted in a controller board's electrical limit: it stays hydraulic, and that limit is declared +counted in a controller board's (transformer's) electrical limit: it stays hydraulic, and that limit is declared net of it (the `ValveCoordinator`). ### Cycle & soak: a Zone rule @@ -703,7 +730,7 @@ up (as agreed in GH #74), but the model reserves the concept now so cycle & soak rule) and concurrency (a Scheduler policy) don't get entangled when either lands. This policy is the **hydraulic** half of concurrency only. The electrical half, how many -solenoids a board can hold open, is not the scheduler's: see the next decision. +solenoids a board (transformer) can hold open, is not the scheduler's: see the next decision. ### Electrical parallel is not hydraulic parallel: a coordinator, not the scheduler @@ -713,7 +740,7 @@ must pass two independent limits, and they belong to different objects: | Limit | Question | Who knows it | Owner | |---|---|---|---| | **Hydraulic** | Does the water allow two zones at once? | The person who sees the plumbing, and the pump's plate when there is one | `WaterNetwork`: an optional limit, either a usable maximum flow or a maximum number of lines; the `SERIAL`/`PARALLEL` setting stays the scheduler's | -| **Electrical** | Can the board hold two solenoids open at once? | Printed on the transformer and on the solenoid datasheet | `ValveCoordinator`: `max_active_zones`, declared net of the master | +| **Electrical** | Can the board (transformer) hold two solenoids open at once? | Printed on the transformer and on the solenoid datasheet | `ValveCoordinator`: `max_active_zones`, declared net of the master | The two are never merged into one number, because they fail differently. A hydraulic overlap gives two weak runs and two wrong flow rates; an electrical one sags the voltage, and valves @@ -730,32 +757,42 @@ today and is not counted by the coordinator; the user declares the coordinator's it. Tying the master to the electrical count would make both the coordinator and the scheduler know how each board is wired. -With no coordinator configured, the default virtual coordinator (limit 1) keeps today's +With no valve coordinator declared, each Environment's virtual coordinator (limit 1) keeps today's behaviour, so an installation that declares nothing changes nothing. **Not written yet.** The coordinator, the water network and the wiring between them land together with the scheduler work. Until then the only action is documentation: a relay board on one transformer should have its interlock set in firmware. -### Environments and water networks: the two entry types +### Environments, water networks and valve coordinators: the three entry types *Decided 2026-10-10, from discussion #239 and GH #320; revised the same day: the network became -an entry of its own, named by each zone; revised 2026-10-11: entries are not typed by place.* -"One valve at a time" turned out to be three questions, and each has its own object: +an entry of its own, named by each zone; revised 2026-10-11: entries are not typed by place, and +the valve coordinator became an entry of its own, the network's sibling.* "One valve at a time" turned +out to be three questions, and each has its own object: | Object | Question | Level | |---|---|---| | `Scheduler` | **When**, and in what order, for the zones of one Environment | One per Environment entry | | `WaterNetwork` | **Is there water** for one more line, and whose turn is it? | One per network entry, on the instance; one implicit per Environment entry when none is named | -| `ValveCoordinator` | **Is there power** for one more solenoid? | One per board, on the instance | +| `ValveCoordinator` | **Is there power** for one more solenoid? | One per valve coordinator entry, on the instance; one implicit virtual coordinator per Environment entry when none is named | A zone starts only when its network and its coordinator both say yes. -**There are two entry types: Environment and water network.** An Environment entry is a climate: -its sensors, its latitude, its scheduler and the zones that sky falls on. A garden is one; a -greenhouse with its own sensors is a second. A water network entry holds a name, the master if -there is one, and the hydraulic limit if one is declared. The zone is always the sub-element of an -Environment, never an entry type. +**There are three entry types: Environment, water network and valve coordinator.** An Environment entry +is a climate: its sensors, its latitude, its scheduler and the zones that sky falls on. A garden +is one; a greenhouse with its own sensors is a second. A water network entry holds a name, the +master if there is one, and the hydraulic limit if one is declared. A valve coordinator entry holds a +name and how many zone valves may be open at once. The zone is always the +sub-element of an Environment, never an entry type; it names the network it draws from and the +coordinator its valve is counted on. As a rule of thumb, one coordinator per transformer: whatever +supplies the solenoids is what limits how many can be open. + +**Why the coordinator is a sibling and not a field.** The first form kept it as a board name typed +or created in the zone form. That is the shape the network had just left, for the same reason: a +coordinator holds zones of several Environments, so its limit cannot live in any one of them, and +a name typed in two forms can split one coordinator in two. As an entry it is declared once, with its +limit, and the zones pick it from a list. **Place and hand-watering are facts of the zone, not types of the entry.** The first form of this decision typed entries as outdoor, greenhouse or indoor and hung three behaviours on the type. Each @@ -790,7 +827,7 @@ explicit, and it lets two entries on one line name two different masters for one it twice, and each ask the other whether it is still in use before switching it off. The network entry holds it once; each zone shows it read-only, which is the explicitness without the copy. -**Simple by default: every new field is optional.** Water network, board, hydraulic limit, +**Simple by default: every new field is optional.** Water network, valve coordinator, hydraulic limit, master and actuation all have a default that is today's behaviour, and a simple garden is configured without seeing any of them. Two installations are the test this design must pass: @@ -798,37 +835,39 @@ configured without seeing any of them. Two installations are the test this desig - **Four independent zones on one hydraulic line**, one Environment entry: nothing else asked either. -If either of them meets a question about networks, boards or types, the form is wrong, not the +If either of them meets a question about networks, coordinators or types, the form is wrong, not the user. - The **first entry** on an instance asks nothing new: it is an Environment, as every installation today is. -- **Adding an entry** from the second on opens a menu of two: **Environment** and **water - network**. Whoever adds a second entry is already doing something beyond the simple case. +- **Adding an entry** from the second on opens a menu of three: **Environment**, **water + network** and **valve coordinator**. Whoever adds a second entry is already doing something beyond the + simple case. - **The zone form** gains three optional fields in a collapsed section: the **network**, listing the network entries on the instance (empty: the entry's implicit network, exactly as today), the - **board** (empty: the virtual coordinator), and **watered by hand** (off: a valve, as today). - With watered by hand on, the valve, network and board fields do not apply. + **valve coordinator**, listing the coordinator entries on the instance (empty: the entry's virtual coordinator, + limit 1, exactly as today), and **watered by hand** (off: a valve, as today). + With watered by hand on, the valve, network and coordinator fields do not apply. **Existing entries are Environments**, and nothing changes in them. Zones whose placement is greenhouse or indoor keep working as now; moving them to an Environment of their own is a choice, useful only where the climate genuinely differs. -| Installation | Entries | Network | Boards | +| Installation | Entries | Network | Coordinator | |---|---|---|---| -| A terrace with seven pots | 1 Environment | none: implicit | none (virtual) | -| Four zones on one line | 1 Environment | none: implicit | none (virtual) | -| Four zones on one line, with a pump | 1 Environment + 1 network | the network, named by the four zones | none (virtual) | -| Two Netro controllers on one main | 1 Environment | none: implicit | 2 named, limit 1 each | -| Garden and greenhouse with its own sensors, on one main | 2 Environments + 1 network | the network, named by the zones of both | as wired | -| Garden on the mains, greenhouse with its own sensors on a tank | 2 Environments | none, or a network for the tank | as wired | -| Drip from the mains beside a lawn on a tank pump | 1 Environment + 1 network | the lawn names the pump's network; the drip names none | none (virtual) | +| A terrace with seven pots | 1 Environment | none: implicit | none: virtual | +| Four zones on one line | 1 Environment | none: implicit | none: virtual | +| Four zones on one line, with a pump | 1 Environment + 1 network | the network, named by the four zones | none: virtual | +| Two Netro controllers on one main | 1 Environment + 2 coordinators | none: implicit | each zone names its controller, limit 1 each | +| Garden and greenhouse with its own sensors, one controller on one main | 2 Environments + 1 network + 1 coordinator | the network, named by the zones of both | the coordinator, named by the zones of both | +| Garden on the mains, greenhouse with its own sensors on a tank | 2 Environments | none, or a network for the tank | none: virtual, one per Environment | +| Drip from the mains beside a lawn on a tank pump | 1 Environment + 1 network | the lawn names the pump's network; the drip names none | none: virtual | | House plants beside the garden | the garden's Environment | none: watered by hand | none | -The defaults (no network declared, a zone naming none, a network removed, a network not loaded) -are listed with the [WaterNetwork](#waternetwork-proposed). The validation of the hydraulic limit +The defaults (none declared, a zone naming none, one removed, one not loaded) are listed with +the [WaterNetwork](#waternetwork-proposed) and the [ValveCoordinator](#valvecoordinator-proposed). The validation of the hydraulic limit runs from both sides: a zone is checked against the network and the coordinator it names, and -changing a network's limit or a coordinator checks again every zone that uses it, so the order of +changing a network's limit or a coordinator's checks again every zone that uses it, so the order of configuration does not matter. The full reasoning is in the scheduler note (`design/scheduler.md`, sections 8.7 and 8.8).