Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 49 additions & 0 deletions docs/display-recovery.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
# Display recovery timing and validation

The package checks the live BUSY pin before normal sleep. If BUSY returns HIGH
after a successful refresh or recovery, it clears the cached safety flag and
starts recovery again. One independent `display_recovery_budget` timer covers
all recovery attempts and the final one-second sleep delays. A successful
sleep request cancels that timer. A new refresh cancels the old recovery,
timer, pending sleep, reset and fault notification before display SPI starts.
Cancellation releases RST if the old reset pulse was still LOW.

With the default 30-second budget, terminal handling reaches
`deep_sleep.enter` within 31.510 seconds for MQTT or 31.010 seconds for API,
plus event-loop scheduling latency, measured from the first recovery start.
This includes the 10 ms reset pulse, 500 ms reset guard and transport fault
notification delay. A failed recovery guard can select terminal handling
earlier. Low-battery sleep has priority over Storage Mode; both retain their
configured durations.

This bound ends at the automation action. It does not prove physical sleep or
zero SPI after a fault. On pinned ESPHome 2026.9.0:

- `deep_sleep/deep_sleep_component.cpp` calls safe-shutdown hooks, then component
teardown with a 5000 ms budget. The hooks execute before that budget starts.
- `waveshare_epaper/waveshare_epaper.cpp` implements `on_safe_shutdown()` by
calling the display driver's `deep_sleep()`.
- `WaveshareEPaperTypeA::deep_sleep()` sends command `0x10` and data `0x01` for
`2.90inv2`, then waits for idle. That model uses the 1000 ms idle timeout.
The check uses `elapsed > timeout`, so include polling/scheduling overhead.

The YAML terminal path starts no display refresh or SPI operation itself. The
stock driver still sends its sleep command during orderly shutdown. Neither a
strict no-SPI claim nor an exact physical-sleep deadline follows from these
automation tests. The normal shutdown path remains enabled for OTA image
confirmation and orderly network teardown.

Run the isolated regression suite with:

```bash
python3 -m unittest discover -s tests -p 'test_display_recovery*.py' -v
```

The executor reads package YAML and compiles its actual C++ lambdas. It models
only the required automation primitives from ESPHome 2026.9.0. GPIO, time and
transport outputs are local substitutes. Unsupported actions fail the test.
Tests execute the actual refresh-boundary action order from both normal and
maintenance callers. They cover permanent HIGH, guard oscillation, late LOW,
BUSY reassertion during the final sleep delay, cancellation during reset and
notification, fresh budgets, and battery/storage duration priority. They do not
execute the display driver, ESP-IDF, network shutdown or physical hardware.
49 changes: 49 additions & 0 deletions docs/soil-sampling-qualification.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
# Soil sampling interval qualification

## Decision

Keep the existing 3-second interval until a separately authorized bench comparison measures the analog signal. The current evidence does not establish that this interval is required. It also does not establish that 100-200 ms gives equivalent noise rejection.

This is an offline qualification. It changes no firmware, calibration, MQTT value or production device. The source review request is [upstream PR #29](https://github.com/JGAguado/Smart_Plant/pull/29#pullrequestreview-5584638551).

## Evidence and limits

The current `acquire_normal` script in `examples/multi-device/packages/smart_plant_core.yaml` performs five soil ADC updates. Each update is followed by a 3-second delay, including the last update. The programmed dwell is therefore 15 seconds. The first-to-fifth sample span is 12 seconds. Neither number is the complete Wi-Fi-on or boot-to-sleep duration.

The earlier cadence change separated AHT20 and VEML7700 acquisition while preserving soil spacing. It did not test 100-200 ms spacing. A publication span is only a lower-bound proxy for the complete awake time.

The exact locally installed ESPHome 2026.9.0 implementation was inspected:

- `components/adc/adc_sensor_esp32.cpp`: `ADCSensor::sample_fixed_attenuation_` reads the configured samples through the synchronous ADC oneshot driver. It does not impose a 3-second interval between component updates.
- `components/sensor/filter.cpp`: `SlidingWindowFilter::new_value` advances on input count. `MedianFilter::compute_result` computes the median of the accepted window. Neither method uses elapsed time to select its output.
- The configured median uses `window_size: 5`, `send_every: 5` and `send_first_at: 5`. It emits after the fifth input. The `soil_voltage` ADC sensor publishes the filtered voltage. The `soil` copy sensor derives its percentage from that publication. No first-sample value is intentionally published.

These software facts explain the sample-count contract. They cannot prove probe settling, noise correlation, switched-supply behavior or unchanged accuracy at a shorter interval. `tests/test_soil_voltage.py` executes the pinned median, rejection and calibration code with the actual package filter settings. It checks the software pipeline only. A later implementation must recheck the selected ESPHome build source.

## Candidate timing

For the same five-update loop, including its trailing delay:

| Delay | First-to-fifth span | Programmed dwell |
| --- | ---: | ---: |
| 3 s, current | 12 s | 15 s |
| 200 ms, candidate | 800 ms | 1 s |
| 100 ms, candidate | 400 ms | 500 ms |

Removing only the trailing delay would save 3 seconds without changing sample timestamps. That remains a separate source change: the existing delay also allows asynchronous transport and other components to run. It must be checked against the complete lifecycle before publication.

## Smallest remaining evidence gap

A firmware timing recommendation requires representative analog measurements. Use one spare board and one probe on an isolated bench. Do not use a production device or shared MQTT namespace. This document is a proposal, not authorization to run the bench.

1. Record the board/probe revision, supply voltage, firmware/source version, soil load and power-on age. Include a representative stable load and the post-power-on settling period.
2. Compare paired windows at 3 s, 200 ms and 100 ms. Retain every raw voltage with a relative sample timestamp, the five-sample median and the final calibrated percentage. Repeat the comparison at different battery voltages and representative dry/wet loads. Randomize the order to separate drift from timing. After each cadence change, acquire and discard a full five-sample warm-up window before recording any comparison window. Discard its median output too. The next recorded median must contain only samples taken at the candidate cadence; do not reuse a median that still contains samples from the previous trial.
3. Measure boot-to-deep-sleep duration directly from serial events or supply current. Report it separately from publication span. Compute the possible energy reduction from measured current and duration, not from the programmed delay alone.
4. Check invalid/out-of-range inputs separately with synthetic fixtures. The five-sample publication count and current calibration must remain unchanged. Reject a candidate if it adds a first-sample publication or changes invalid-value handling.
5. Before the bench, agree an acceptable voltage/percentage deviation and repeatability limit from the calibration use case. Use the calibration use case to choose these limits. A noise observation alone is not an acceptance threshold.

Select the longest candidate interval that meets the agreed analog and publication limits. If no candidate meets them, retain 3 seconds and record the observed reason. Any later fleet use needs its own canary/build/OTA authorization and rollback to the last validated firmware.

## Acceptance and rollback

Offline qualification is complete when the source mechanism, timing arithmetic, historical evidence and remaining analog gap are explicit. It does not claim a shorter interval is safe. This document has no runtime effect. Revert its documentation commit to roll it back.
40 changes: 40 additions & 0 deletions examples/multi-device/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -154,3 +154,43 @@ Run the offline package tests with `python3 -B -m unittest discover -s tests -v`
They execute the package actions and compile the policy lambdas as C++ with
simulated sensors, time and GPIO. Firmware compilation checks ESPHome schemas
and generated code. Neither check proves physical panel or network behavior.

## API connection window

The API profile samples the battery before a best-effort state-subscription
window. The default `api_connection_timeout` is 10 seconds. A logs-only client
does not release it. Critical battery protection bypasses this optional wait.
The device still completes its normal cycle if no subscriber connects.
Read [the API wake contract](../../docs/api-wake-window.md) for timing and limits.

## Measurement diagnostics

`Soil Voltage` exposes the median-filtered, plausibility-checked ADC reading.
`Soil Moisture` derives its percentage from the same publication. The package
keeps its existing wet/dry calibration and takes five samples spaced 3 seconds
apart. Each window publishes once, after its fifth sample. Read [the soil
qualification](../../docs/soil-sampling-qualification.md) before changing timing.
It defines the remaining isolated bench work. It does not approve a shorter
interval or a production experiment.

`ESPHome Version` exposes the running version and its native configuration
hash. The hash identifies the effective build, not a Git commit. A local build
or dashboard record alone does not prove which image a device is running.

Numeric gauge labels retain the measured value. Only needle coordinates are
clamped to the display range. Missing and nonfinite readings show `--`.

## Display recovery limits

The existing independent recovery timer covers all BUSY retries and final
sleep delays. A new refresh cancels stale recovery and releases an interrupted
reset pulse. With the default 30-second recovery budget, the sleep action is
reached within 31.510 seconds for MQTT or 31.010 seconds for API, plus scheduler
latency. These bounds start at the first recovery attempt.

Read [the recovery contract](../../docs/display-recovery.md) for the action
ordering, tests and limits. The bounds end at `deep_sleep.enter`, not physical
sleep. Native shutdown hooks remain active, including the stock driver's final
SPI sleep command. Both normal and emergency paths retain explicit OTA image
confirmation for compatibility. No new application display refresh follows a
terminal fault.
Loading