// Package status evaluates laundry state from the latest three power samples. // Power below 3 W is idle; missing samples take precedence over active readings. package status const windowSize = 3 // Sample represents one sampling interval. Invalid samples have no power reading. type Sample struct { Power float64 `json:"power"` Valid bool `json:"valid"` } // Result is the status API response, with readings ordered oldest first. type Result struct { State string `json:"state"` Message string `json:"message"` Readings []Sample `json:"readings"` } // Window holds the last three samples. Callers synchronize concurrent access. // The zero value is an empty window. type Window struct { samples [windowSize]Sample next int count int } // Add records an interval, including intervals without a valid reading. func (w *Window) Add(sample Sample) { w.samples[w.next] = sample w.next = (w.next + 1) % windowSize if w.count < windowSize { w.count++ } } // Readings returns an independent snapshot, oldest first, including missing data. func (w *Window) Readings() []Sample { readings := make([]Sample, w.count) for i := range w.count { readings[i] = w.samples[(w.next-w.count+i+windowSize)%windowSize] } return readings } // Evaluate applies waiting, incomplete, running, then done precedence to a // window's readings. An empty window serializes as [] rather than null. func Evaluate(readings []Sample) Result { result := Result{State: "waiting", Message: "Waiting for data...", Readings: append([]Sample{}, readings...)} if len(readings) < windowSize { return result } for _, sample := range readings { if !sample.Valid { result.State, result.Message = "incomplete", "Incomplete data" return result } } for _, sample := range readings { if sample.Power >= 3 { result.State, result.Message = "running", "Laundry not yet done" return result } } result.State, result.Message = "done", "Laundry done" return result }