/* * control — setpoint, hysteresis, room selection, and the stale-data ladder. * * Runs on the ESP32, not in Home Assistant. The house keeps heating when the * broker, zigbee2mqtt or HA is down; it degrades through the documented * fallback ladder rather than simply stopping. * * All temperatures in this module are degrees Celsius. Fahrenheit exists only * in the UI layer. */ #pragma once #include #include #include "esp_err.h" /* -------------------------------------------------------------- tuning -- */ /* Evaluate on a fixed cadence, not on every incoming reading (PRD 4.3). */ #define CONTROL_INTERVAL_S 10 /* Hysteresis. Call for heat below setpoint-deadband, stop at setpoint+deadband. */ #define CONTROL_DEADBAND_C 0.5f #define CONTROL_SETPOINT_MIN_C 5.0f #define CONTROL_SETPOINT_MAX_C 30.0f #define CONTROL_SETPOINT_DEFAULT_C 20.0f /* exactly 68F */ /* A reading older than this no longer governs anything (PRD 5.5, N=20). */ #define CONTROL_STALE_AFTER_S (20 * 60) /* * Conservative setpoint used once we are running on stale or absent data. * Low enough that a wrong reading cannot cook the house, high enough to keep * pipes safe. */ #define CONTROL_FALLBACK_SETPOINT_C 16.0f /* * Last-resort freeze protection, used only when there is no reading at all — * not stale, not old, none since boot. * * With no temperature there is no closed loop to run, so this is an open-loop * duty cycle: heat for ON, then wait at least OFF. PRD 5.5 requires that * stale data must never hold the relay closed and must never silently mean no * heat either; with zero data those are the only two failure modes available, * and a low duty cycle sits between them. Every relay interlock still applies * on top, so this cannot short-cycle the furnace. */ #define CONTROL_FREEZE_DUTY_ON_S (6 * 60) #define CONTROL_FREEZE_DUTY_OFF_S (24 * 60) /* --------------------------------------------------------------- modes -- */ typedef enum { ROOM_MODE_MANUAL = 0, /* encoder picks the governing room */ ROOM_MODE_SCHEDULE, /* time-of-day rules */ ROOM_MODE_FORMULA, /* min across rooms — coldest room wins */ ROOM_MODE_COUNT, } room_mode_t; /* * How the governing temperature is currently being derived. This is the * fallback ladder of PRD 5.5, in order of preference, and it is what the UI * renders as a fault. */ typedef enum { CONTROL_SRC_SELECTED = 0, /* the chosen room, fresh */ CONTROL_SRC_OTHER_ROOM, /* chosen room stale, another is fresh */ CONTROL_SRC_STALE, /* nothing fresh; running aged data, clamped */ CONTROL_SRC_NONE, /* no reading at all; freeze-protection duty */ } control_source_t; typedef struct { float setpoint_c; /* what the user asked for */ float effective_setpoint_c; /* after any fallback clamp */ float governing_temp_c; /* NaN when source is CONTROL_SRC_NONE */ int governing_room; /* index into the room table, -1 if none */ room_mode_t mode; control_source_t source; bool calling_for_heat; /* what control asked the relay for */ bool schedule_clock_valid; } control_state_t; /* Start the control task. Requires relay_init() and zigbee_client_init(). */ esp_err_t control_init(void); void control_get_state(control_state_t *out); /* Setpoint, clamped to bounds and persisted to NVS. */ void control_set_setpoint(float celsius); void control_adjust_setpoint(float delta_c); float control_get_setpoint(void); void control_set_mode(room_mode_t mode); room_mode_t control_get_mode(void); /* * Manual room selection. Also switches to ROOM_MODE_MANUAL — PRD 4.2 requires * manual override to be reachable regardless of the active mode. */ void control_select_room(int room_index); int control_get_selected_room(void); const char *control_source_str(control_source_t src); const char *control_mode_str(room_mode_t mode);