# Plan Status: **ready_for_implement** Goal: implement the scope defined in `ROADMAP.md` — a phone-first webpage, served by a small Go backend in Docker, that shows whether the laundry is done based on the last 3 one-minute power samples from `tele/tasmota_washingmashine_C21183/SENSOR`. ## Scope - Go module `mqtt_washingmachine` (Go 1.26), standard library plus `github.com/eclipse/paho.mqtt.golang` for MQTT. - Layout: - `main.go`: config from env, wiring, graceful shutdown. - `internal/status/`: pure logic (sample window, status evaluation). - `internal/sensor/`: Tasmota payload parsing, MQTT subscription, per-minute sampler. - `internal/web/`: HTTP handlers and embedded static page (`go:embed`). - Docker packaging and documentation. - Out of scope: persistence, TLS/auth, history beyond 3 samples, multiple devices. ## Design decisions (from ROADMAP.md) - Threshold: 3.00 W. A sample is "idle" only if `power < 3.0`; `>= 3.0` is "active". - Sampling: the MQTT handler stores only the latest `ENERGY.Power` value and its receive time. A ticker fires every `SAMPLE_INTERVAL` (default 60s). At each tick: if a message was received since the previous tick, record its power as a sample; otherwise record a missing sample. Then clear the "received" flag. - Window: ring of the last 3 samples (oldest first). Each sample is `{Power float64, Valid bool}`. - Status evaluation (first match wins): 1. fewer than 3 samples recorded since start -> `waiting`, "Waiting for data...", grey 2. any of the last 3 samples missing -> `incomplete`, "Incomplete data", grey 3. any sample `>= 3.0` -> `running`, "Laundry not yet done", red 4. otherwise (all 3 `< 3.0`) -> `done`, "Laundry done", default text color - JSON contract for `GET /api/status`: ```json {"state":"running","message":"Laundry not yet done","readings":[{"power":47.7,"valid":true},{"power":0,"valid":false}]} ``` `readings` has 0–3 entries, oldest first. The page renders the display text, color class, and `47.70W` / `--` formatting. - Config env vars: `MQTT_BROKER` (default `tcp://192.168.1.12:1883`), `MQTT_TOPIC` (default `tele/tasmota_washingmashine_C21183/SENSOR`), `MQTT_USERNAME`, `MQTT_PASSWORD` (optional), `HTTP_ADDR` (default `:8080`), `SAMPLE_INTERVAL` (Go duration, default `60s`). - MQTT client uses auto-reconnect and re-subscribes in the `OnConnect` handler. ## Acceptance Criteria - `GET /` returns a white-background page with a viewport meta tag, a very large status text, and the last 3 readings below it in normal-size black text formatted like `47.70W`; missing readings show `--`. - The page fetches `/api/status` on load and every 60 s; no horizontal scroll at 360 px width. - Status states and colors match the table in `ROADMAP.md` exactly, including `>= 3.00 W` = not done. - The backend collects samples continuously with no page open; a page load immediately reflects the current state. - A minute with no MQTT message produces a missing sample and `incomplete` state. - Malformed payloads or payloads without `ENERGY.Power` are ignored (logged), without crashing or changing the latest value. - `docker build` succeeds and the container runs with env-var configuration; README documents setup. - `go fmt ./...`, `go vet ./...`, `go test ./...` all pass. ## Tasks ### T-001 — Status logic and Tasmota payload parsing Tests first, then implementation. - `internal/status/status_test.go`, `internal/status/status.go`: window of 3, `Evaluate`, boundary tests (2.99 -> idle, 3.00 -> active, 0 W all three -> done, mixed -> running, gap -> incomplete, <3 samples -> waiting). - `internal/sensor/payload_test.go`, `internal/sensor/payload.go`: parse the sample payload from the roadmap (`Power` 139.60 etc.), missing `ENERGY`, missing `Power`, invalid JSON. - `go.mod` (module init, no external deps yet in this task). - Files to change: `go.mod`, `internal/status/*`, `internal/sensor/payload*.go`. - Documentation: package doc comments; no README change yet. ### T-002 — Sampler, MQTT subscription, HTTP server and page Tests first, then implementation. - `internal/sensor/sampler_test.go`, `internal/sensor/sampler.go`: latest-value store plus ticker-driven sampling into the window, with an injectable tick channel/clock for deterministic tests (message between ticks -> valid sample, none -> missing). - `internal/sensor/mqtt.go`: paho client, auto-reconnect, subscribe in `OnConnect`, feeds payloads to the parser then the sampler (thin glue, not unit-tested against a real broker). - `internal/web/handler_test.go`, `internal/web/handler.go`, `internal/web/static/index.html`: `/` serves the embedded page, `/api/status` returns the JSON contract (test with `httptest`, including state/message/readings per state). Page styling: white background, `font-size` about 3rem+ bold for status via `clamp()`, readings in black at normal size, red/grey classes, `meta viewport`, 60 s `setInterval` fetch, graceful display if the fetch fails. - `main.go`: env config with defaults, wiring, signal-based shutdown. - `go.mod`, `go.sum`: add `github.com/eclipse/paho.mqtt.golang`. - Documentation: code comments; README deferred to T-003. ### T-003 — Docker packaging and README - `Dockerfile` (multi-stage: golang builder with `CGO_ENABLED=0`, final `scratch` or `distroless/static` image, `EXPOSE 8080`). - `docker-compose.yml` example with the env vars from this plan. - `.dockerignore`. - `README.md`: replace the project TODOs (Overview, Getting Started) with what it does, status rules, env var table, `docker compose up` instructions, the Tasmota `TelePeriod` note (10 s, any value works since sampling is per minute), and limitations (in-memory state, LAN only, no auth). Keep the existing AI Workflow section untouched. English only. - Files to change: `Dockerfile`, `docker-compose.yml`, `.dockerignore`, `README.md`. - Validation extra: `docker build .` if Docker is available; otherwise note it in the handoff. ## Validation - `go fmt ./...` - `go vet ./...` - `go test ./...` - Manual check after T-002: run with `HTTP_ADDR=:8080`, publish test payloads with `mosquitto_pub` (if available), open `/` at a phone-width viewport. ## Commit subjects (suggested) - T-001: `feat(status): evaluate laundry state from the last three power samples` - T-002: `feat(web): show live laundry status page fed by MQTT power readings` - T-003: `feat(docker): package laundry status service with Docker and README`