Fix Threshold and improve program and UI features
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -3,3 +3,4 @@
|
|||||||
.vscode/c_cpp_properties.json
|
.vscode/c_cpp_properties.json
|
||||||
.vscode/launch.json
|
.vscode/launch.json
|
||||||
.vscode/ipch
|
.vscode/ipch
|
||||||
|
dist/
|
||||||
66
README.md
66
README.md
@@ -315,7 +315,9 @@ Modes:
|
|||||||
| `cool` | Runs the pump when measured temperature is above the target |
|
| `cool` | Runs the pump when measured temperature is above the target |
|
||||||
| `warm` | Runs the pump when measured temperature is below the target |
|
| `warm` | Runs the pump when measured temperature is below the target |
|
||||||
|
|
||||||
The target temperature is stored in Celsius and supports up to two decimals. The firmware compares the measured temperature and target temperature at two-decimal precision. In `cool` mode, the pump runs when the measured temperature is at least 0.25 C above the target. In `warm` mode, the pump runs when the measured temperature is at least 0.25 C below the target.
|
The target temperature and threshold are stored in Celsius and support up to two decimals. The firmware compares the measured temperature, target temperature, and threshold at two-decimal precision. The threshold acts as the start offset only. In `cool` mode, the pump starts when the measured temperature is at least the threshold above the target and then stops only when the measured temperature reaches the target again. In `warm` mode, the pump starts when the measured temperature is at least the threshold below the target and then stops only when the measured temperature reaches the target again. The default threshold is 0.25 C.
|
||||||
|
|
||||||
|
The max runtime is stored in minutes and defaults to 5 minutes. It limits one continuous automatic pump run. If the pump runs for the configured max runtime without reaching the target temperature, the firmware switches the program mode to `off`, turns the pump off, persists the new mode, and emits a `program` SSE event with only the changed fields, such as `mode`, `pumpEnabled`, and `reason`, so the Program tile can update without overwriting settings the user is editing.
|
||||||
|
|
||||||
Read the current program:
|
Read the current program:
|
||||||
|
|
||||||
@@ -331,10 +333,10 @@ POST /api/program
|
|||||||
Authorization: Bearer <token>
|
Authorization: Bearer <token>
|
||||||
Content-Type: application/json
|
Content-Type: application/json
|
||||||
|
|
||||||
{"mode":"cool","targetTemperatureC":22.75}
|
{"mode":"cool","targetTemperatureC":22.75,"toleranceC":0.25,"maxRuntimeMinutes":5}
|
||||||
```
|
```
|
||||||
|
|
||||||
The response includes `mode`, `targetTemperatureC`, `targetTemperatureF`, `toleranceC`, `pumpEnabled`, `sensorConnected`, and the latest `temperatureC`.
|
The response includes `mode`, `targetTemperatureC`, `targetTemperatureF`, `toleranceC`, `maxRuntimeMinutes`, `pumpEnabled`, `sensorConnected`, and the latest `temperatureC`.
|
||||||
|
|
||||||
### Authentication
|
### Authentication
|
||||||
|
|
||||||
@@ -476,6 +478,57 @@ Content-Type: application/json
|
|||||||
|
|
||||||
Use `"role":"PUBLIC"` to make a known API public.
|
Use `"role":"PUBLIC"` to make a known API public.
|
||||||
|
|
||||||
|
### Project scripts
|
||||||
|
|
||||||
|
The `scripts/` directory contains small build and packaging helpers. They assume they are run from the project checkout and use the `seeed_xiao_esp32c3` PlatformIO environment by default.
|
||||||
|
|
||||||
|
| Script | Purpose |
|
||||||
|
| --- | --- |
|
||||||
|
| `scripts/platformio_targets.py` | Registers the custom PlatformIO target `uploadall`, shown in the PlatformIO UI as `Upload Firmware and Filesystem`. |
|
||||||
|
| `scripts/create_update_package.py` | Combines an existing firmware image and LittleFS image into one `.tslpkg` OTA update package. |
|
||||||
|
| `scripts/build_ota_package.py` | Builds LittleFS and firmware, then creates a timestamped `.tslpkg` file in `dist/`. |
|
||||||
|
| `scripts/build_upload_device.py` | Builds LittleFS and firmware, then uploads both to a connected device. |
|
||||||
|
|
||||||
|
Create a timestamped OTA package in `dist/`:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
python scripts/build_ota_package.py
|
||||||
|
```
|
||||||
|
|
||||||
|
Useful options:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
python scripts/build_ota_package.py -e seeed_xiao_esp32c3 -o dist
|
||||||
|
python scripts/build_ota_package.py --pio C:\Users\<user>\.platformio\penv\Scripts\pio.exe
|
||||||
|
```
|
||||||
|
|
||||||
|
Upload firmware and filesystem to a connected device:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
python scripts/build_upload_device.py --upload-port COM3
|
||||||
|
```
|
||||||
|
|
||||||
|
Useful options:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
python scripts/build_upload_device.py -e seeed_xiao_esp32c3 -p COM3
|
||||||
|
python scripts/build_upload_device.py --pio C:\Users\<user>\.platformio\penv\Scripts\pio.exe
|
||||||
|
```
|
||||||
|
|
||||||
|
Create a combined package from already-built images:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
pio run
|
||||||
|
pio run -t buildfs
|
||||||
|
python scripts/create_update_package.py
|
||||||
|
```
|
||||||
|
|
||||||
|
Useful options:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
python scripts/create_update_package.py --firmware .pio/build/seeed_xiao_esp32c3/firmware.bin --filesystem .pio/build/seeed_xiao_esp32c3/littlefs.bin --output .pio/build/seeed_xiao_esp32c3/update.tslpkg
|
||||||
|
```
|
||||||
|
|
||||||
### Firmware and filesystem updates
|
### Firmware and filesystem updates
|
||||||
|
|
||||||
Because the Admin UI is stored in LittleFS, OTA releases are distributed as one combined update package. The package contains both the LittleFS filesystem image and the firmware image, so UI and firmware changes cannot drift apart during an update.
|
Because the Admin UI is stored in LittleFS, OTA releases are distributed as one combined update package. The package contains both the LittleFS filesystem image and the firmware image, so UI and firmware changes cannot drift apart during an update.
|
||||||
@@ -489,16 +542,13 @@ Because the Admin UI is stored in LittleFS, OTA releases are distributed as one
|
|||||||
Create the single OTA package:
|
Create the single OTA package:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
pio run
|
python scripts/build_ota_package.py
|
||||||
pio run -t buildfs
|
|
||||||
python scripts/create_update_package.py
|
|
||||||
```
|
```
|
||||||
|
|
||||||
For a direct USB flash during development:
|
For a direct USB flash during development:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
pio run -t upload
|
python scripts/build_upload_device.py --upload-port COM3
|
||||||
pio run -t uploadfs
|
|
||||||
```
|
```
|
||||||
|
|
||||||
For OTA updates, host `update.tslpkg` on your update server. The Admin UI has one update package URL. The default is:
|
For OTA updates, host `update.tslpkg` on your update server. The Admin UI has one update package URL. The default is:
|
||||||
|
|||||||
@@ -7,11 +7,11 @@ class ProgramCard extends HTMLElement {
|
|||||||
connectedCallback() {
|
connectedCallback() {
|
||||||
this.innerHTML = `
|
this.innerHTML = `
|
||||||
<style>
|
<style>
|
||||||
:host{display:block}section{height:100%;box-sizing:border-box}.radioRow{display:flex;gap:10px;flex-wrap:wrap;margin:8px 0}.radioRow label{display:inline-flex;gap:5px;align-items:center;margin:0}.radioRow input{width:auto}.row{display:flex;gap:8px;align-items:end}.row label{flex:1}.status{display:flex;gap:6px;flex-wrap:wrap;margin:8px 0}.badge{display:inline-flex;align-items:center;background:#eef3f6;border-radius:6px;padding:4px 7px;color:#263238;font-size:12px}
|
:host{display:block}section{height:100%;box-sizing:border-box}.radioRow{display:flex;gap:10px;flex-wrap:wrap;margin:8px 0}.radioRow label{display:inline-flex;gap:5px;align-items:center;margin:0}.radioRow input{width:auto}.row{display:flex;gap:8px;align-items:end;flex-wrap:wrap}.row label{flex:1;min-width:120px}.row button{flex:0 0 auto}.status{display:flex;gap:6px;flex-wrap:wrap;margin:8px 0}.badge{display:inline-flex;align-items:center;background:#eef3f6;border-radius:6px;padding:4px 7px;color:#263238;font-size:12px}
|
||||||
</style>
|
</style>
|
||||||
<section>
|
<section>
|
||||||
<h3>Program</h3>
|
<h3>Program</h3>
|
||||||
<div class="row"><label>Target Temperature<input id="target" type="number" min="-40" max="85" step="0.01" value="22.00"></label><button id="save">Set</button></div>
|
<div class="row"><label>Target Temperature<input id="target" type="number" min="-40" max="85" step="0.01" value="22.00"></label><label>Threshold<input id="threshold" type="number" min="0" max="20" step="0.01" value="0.25"></label><label>MaxRuntime<input id="maxRuntime" type="number" min="1" max="1440" step="1" value="5"></label><button id="save">Set</button></div>
|
||||||
<label>Mode</label>
|
<label>Mode</label>
|
||||||
<div class="radioRow">
|
<div class="radioRow">
|
||||||
<label><input type="radio" name="programMode" value="off"> Off</label>
|
<label><input type="radio" name="programMode" value="off"> Off</label>
|
||||||
@@ -46,6 +46,11 @@ class ProgramCard extends HTMLElement {
|
|||||||
this.renderPump(JSON.parse(event.data));
|
this.renderPump(JSON.parse(event.data));
|
||||||
} catch (error) {}
|
} catch (error) {}
|
||||||
});
|
});
|
||||||
|
this.source.addEventListener('program', event => {
|
||||||
|
try {
|
||||||
|
this.renderProgramChange(JSON.parse(event.data));
|
||||||
|
} catch (error) {}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
stopEvents() {
|
stopEvents() {
|
||||||
@@ -59,10 +64,13 @@ class ProgramCard extends HTMLElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
renderSettings(json) {
|
renderSettings(json) {
|
||||||
const mode = json.mode || 'off';
|
if (json.mode !== undefined) {
|
||||||
const modeInput = this.querySelector('input[name="programMode"][value="' + mode + '"]');
|
const modeInput = this.querySelector('input[name="programMode"][value="' + json.mode + '"]');
|
||||||
if (modeInput) modeInput.checked = true;
|
if (modeInput) modeInput.checked = true;
|
||||||
|
}
|
||||||
if (json.targetTemperatureC !== undefined) this.querySelector('#target').value = Number(json.targetTemperatureC).toFixed(2);
|
if (json.targetTemperatureC !== undefined) this.querySelector('#target').value = Number(json.targetTemperatureC).toFixed(2);
|
||||||
|
if (json.toleranceC !== undefined) this.querySelector('#threshold').value = Number(json.toleranceC).toFixed(2);
|
||||||
|
if (json.maxRuntimeMinutes !== undefined) this.querySelector('#maxRuntime').value = String(Math.round(Number(json.maxRuntimeMinutes)));
|
||||||
}
|
}
|
||||||
|
|
||||||
renderLiveTemperature(json) {
|
renderLiveTemperature(json) {
|
||||||
@@ -81,6 +89,11 @@ class ProgramCard extends HTMLElement {
|
|||||||
this.renderPump(json);
|
this.renderPump(json);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
renderProgramChange(json) {
|
||||||
|
this.renderSettings(json);
|
||||||
|
this.renderPump(json);
|
||||||
|
}
|
||||||
|
|
||||||
async loadProgram() {
|
async loadProgram() {
|
||||||
const text = await window.App.req('GET', '/api/program');
|
const text = await window.App.req('GET', '/api/program');
|
||||||
this.querySelector('#out').textContent = text;
|
this.querySelector('#out').textContent = text;
|
||||||
@@ -92,7 +105,9 @@ class ProgramCard extends HTMLElement {
|
|||||||
|
|
||||||
async saveProgram() {
|
async saveProgram() {
|
||||||
const target = Number(this.querySelector('#target').value);
|
const target = Number(this.querySelector('#target').value);
|
||||||
const text = await window.App.req('POST', '/api/program', {mode: this.selectedMode(), targetTemperatureC: Number(target.toFixed(2))});
|
const threshold = Number(this.querySelector('#threshold').value);
|
||||||
|
const maxRuntime = Number(this.querySelector('#maxRuntime').value);
|
||||||
|
const text = await window.App.req('POST', '/api/program', {mode: this.selectedMode(), targetTemperatureC: Number(target.toFixed(2)), toleranceC: Number(threshold.toFixed(2)), maxRuntimeMinutes: Math.round(maxRuntime)});
|
||||||
this.querySelector('#out').textContent = text;
|
this.querySelector('#out').textContent = text;
|
||||||
try {
|
try {
|
||||||
const json = JSON.parse(text);
|
const json = JSON.parse(text);
|
||||||
|
|||||||
@@ -38,6 +38,10 @@
|
|||||||
#define PROGRAM_TOLERANCE_C 0.25f
|
#define PROGRAM_TOLERANCE_C 0.25f
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
#ifndef PROGRAM_DEFAULT_MAX_RUNTIME_MINUTES
|
||||||
|
#define PROGRAM_DEFAULT_MAX_RUNTIME_MINUTES 5
|
||||||
|
#endif
|
||||||
|
|
||||||
#ifndef PROJECT_NAME
|
#ifndef PROJECT_NAME
|
||||||
#define PROJECT_NAME "TSL Bed Cooler"
|
#define PROJECT_NAME "TSL Bed Cooler"
|
||||||
#endif
|
#endif
|
||||||
@@ -113,6 +117,8 @@ extern bool ledInverted;
|
|||||||
extern bool pumpEnabled;
|
extern bool pumpEnabled;
|
||||||
extern ProgramMode programMode;
|
extern ProgramMode programMode;
|
||||||
extern float programTargetC;
|
extern float programTargetC;
|
||||||
|
extern float programToleranceC;
|
||||||
|
extern uint16_t programMaxRuntimeMinutes;
|
||||||
extern String updateUrl;
|
extern String updateUrl;
|
||||||
extern String networkHostname;
|
extern String networkHostname;
|
||||||
extern bool networkDhcp;
|
extern bool networkDhcp;
|
||||||
|
|||||||
@@ -30,6 +30,8 @@ bool ledInverted = DEFAULT_LED_INVERTED;
|
|||||||
bool pumpEnabled = false;
|
bool pumpEnabled = false;
|
||||||
ProgramMode programMode = PROGRAM_OFF;
|
ProgramMode programMode = PROGRAM_OFF;
|
||||||
float programTargetC = PROGRAM_DEFAULT_TARGET_C;
|
float programTargetC = PROGRAM_DEFAULT_TARGET_C;
|
||||||
|
float programToleranceC = PROGRAM_TOLERANCE_C;
|
||||||
|
uint16_t programMaxRuntimeMinutes = PROGRAM_DEFAULT_MAX_RUNTIME_MINUTES;
|
||||||
String updateUrl = DEFAULT_UPDATE_URL;
|
String updateUrl = DEFAULT_UPDATE_URL;
|
||||||
String networkHostname;
|
String networkHostname;
|
||||||
bool networkDhcp = true;
|
bool networkDhcp = true;
|
||||||
@@ -112,6 +114,10 @@ void loadSettings() {
|
|||||||
programMode = (ProgramMode)constrain(prefs.getUChar("progMode", PROGRAM_OFF), PROGRAM_OFF, PROGRAM_WARM);
|
programMode = (ProgramMode)constrain(prefs.getUChar("progMode", PROGRAM_OFF), PROGRAM_OFF, PROGRAM_WARM);
|
||||||
programTargetC = prefs.getFloat("progTargetC", PROGRAM_DEFAULT_TARGET_C);
|
programTargetC = prefs.getFloat("progTargetC", PROGRAM_DEFAULT_TARGET_C);
|
||||||
if (isnan(programTargetC) || programTargetC < -40.0f || programTargetC > 85.0f) programTargetC = PROGRAM_DEFAULT_TARGET_C;
|
if (isnan(programTargetC) || programTargetC < -40.0f || programTargetC > 85.0f) programTargetC = PROGRAM_DEFAULT_TARGET_C;
|
||||||
|
programToleranceC = prefs.getFloat("progTolC", PROGRAM_TOLERANCE_C);
|
||||||
|
if (isnan(programToleranceC) || programToleranceC < 0.0f || programToleranceC > 20.0f) programToleranceC = PROGRAM_TOLERANCE_C;
|
||||||
|
programToleranceC = roundf(programToleranceC * 100.0f) / 100.0f;
|
||||||
|
programMaxRuntimeMinutes = (uint16_t)constrain((int)prefs.getUShort("progMaxMin", PROGRAM_DEFAULT_MAX_RUNTIME_MINUTES), 1, 1440);
|
||||||
updateUrl = prefString("updateUrl", DEFAULT_UPDATE_URL);
|
updateUrl = prefString("updateUrl", DEFAULT_UPDATE_URL);
|
||||||
networkHostname = prefString("netHost", "");
|
networkHostname = prefString("netHost", "");
|
||||||
networkDhcp = prefs.getBool("netDhcp", true);
|
networkDhcp = prefs.getBool("netDhcp", true);
|
||||||
|
|||||||
@@ -10,6 +10,8 @@ static bool temperatureStarted = false;
|
|||||||
static bool temperaturePending = false;
|
static bool temperaturePending = false;
|
||||||
static float lastTemperatureC = NAN;
|
static float lastTemperatureC = NAN;
|
||||||
static uint32_t temperatureRequestMs = 0;
|
static uint32_t temperatureRequestMs = 0;
|
||||||
|
static uint32_t programPumpRunStartedMs = 0;
|
||||||
|
static uint32_t programModeSseUntilMs = 0;
|
||||||
|
|
||||||
static void beginTemperatureSensor() {
|
static void beginTemperatureSensor() {
|
||||||
if (temperatureStarted) return;
|
if (temperatureStarted) return;
|
||||||
@@ -98,7 +100,8 @@ static String programJsonFields() {
|
|||||||
String json = "\"mode\":\"" + programModeName() + "\"";
|
String json = "\"mode\":\"" + programModeName() + "\"";
|
||||||
json += ",\"targetTemperatureC\":" + String(programTargetC, 2);
|
json += ",\"targetTemperatureC\":" + String(programTargetC, 2);
|
||||||
json += ",\"targetTemperatureF\":" + String((programTargetC * 9.0f / 5.0f) + 32.0f, 2);
|
json += ",\"targetTemperatureF\":" + String((programTargetC * 9.0f / 5.0f) + 32.0f, 2);
|
||||||
json += ",\"toleranceC\":" + String(PROGRAM_TOLERANCE_C, 2);
|
json += ",\"toleranceC\":" + String(programToleranceC, 2);
|
||||||
|
json += ",\"maxRuntimeMinutes\":" + String(programMaxRuntimeMinutes);
|
||||||
json += ",\"pumpEnabled\":" + String(pumpEnabled ? "true" : "false");
|
json += ",\"pumpEnabled\":" + String(pumpEnabled ? "true" : "false");
|
||||||
json += ",\"sensorConnected\":";
|
json += ",\"sensorConnected\":";
|
||||||
json += isnan(temperatureC) ? "false" : "true";
|
json += isnan(temperatureC) ? "false" : "true";
|
||||||
@@ -115,22 +118,49 @@ void updateProgram() {
|
|||||||
lastProgramMs = now;
|
lastProgramMs = now;
|
||||||
|
|
||||||
if (programMode == PROGRAM_OFF) {
|
if (programMode == PROGRAM_OFF) {
|
||||||
|
programPumpRunStartedMs = 0;
|
||||||
setPumpEnabled(false, " by program");
|
setPumpEnabled(false, " by program");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
float temperatureC = currentTemperatureC();
|
float temperatureC = currentTemperatureC();
|
||||||
if (isnan(temperatureC)) {
|
if (isnan(temperatureC)) {
|
||||||
|
programPumpRunStartedMs = 0;
|
||||||
setPumpEnabled(false, " by program sensor fault");
|
setPumpEnabled(false, " by program sensor fault");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
float roundedTemperatureC = roundf(temperatureC * 100.0f) / 100.0f;
|
float roundedTemperatureC = roundf(temperatureC * 100.0f) / 100.0f;
|
||||||
|
bool shouldRun = false;
|
||||||
if (programMode == PROGRAM_COOL) {
|
if (programMode == PROGRAM_COOL) {
|
||||||
setPumpEnabled(roundedTemperatureC >= programTargetC + PROGRAM_TOLERANCE_C, " by cool program");
|
shouldRun = pumpEnabled
|
||||||
|
? roundedTemperatureC > programTargetC
|
||||||
|
: roundedTemperatureC >= programTargetC + programToleranceC;
|
||||||
} else if (programMode == PROGRAM_WARM) {
|
} else if (programMode == PROGRAM_WARM) {
|
||||||
setPumpEnabled(roundedTemperatureC <= programTargetC - PROGRAM_TOLERANCE_C, " by warm program");
|
shouldRun = pumpEnabled
|
||||||
|
? roundedTemperatureC < programTargetC
|
||||||
|
: roundedTemperatureC <= programTargetC - programToleranceC;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!shouldRun) {
|
||||||
|
programPumpRunStartedMs = 0;
|
||||||
|
setPumpEnabled(false, " by " + programModeName() + " program");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!pumpEnabled || programPumpRunStartedMs == 0) programPumpRunStartedMs = now;
|
||||||
|
uint32_t maxRuntimeMs = (uint32_t)programMaxRuntimeMinutes * 60000UL;
|
||||||
|
if (now - programPumpRunStartedMs >= maxRuntimeMs) {
|
||||||
|
programMode = PROGRAM_OFF;
|
||||||
|
prefs.putUChar("progMode", programMode);
|
||||||
|
programPumpRunStartedMs = 0;
|
||||||
|
programModeSseUntilMs = now + 15000UL;
|
||||||
|
setPumpEnabled(false, " by program max runtime");
|
||||||
|
appendLog(LOG_WARN, "Program switched off after pump ran for " + String(programMaxRuntimeMinutes) + " minutes without reaching target");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setPumpEnabled(true, " by " + programModeName() + " program");
|
||||||
}
|
}
|
||||||
|
|
||||||
void handlePing() {
|
void handlePing() {
|
||||||
@@ -170,10 +200,17 @@ void handleProgram() {
|
|||||||
programMode = parseProgramMode(jsonStringValue(body, "mode", programModeName()), programMode);
|
programMode = parseProgramMode(jsonStringValue(body, "mode", programModeName()), programMode);
|
||||||
programTargetC = constrain(jsonFloatValue(body, "targetTemperatureC", programTargetC), -40.0f, 85.0f);
|
programTargetC = constrain(jsonFloatValue(body, "targetTemperatureC", programTargetC), -40.0f, 85.0f);
|
||||||
programTargetC = roundf(programTargetC * 100.0f) / 100.0f;
|
programTargetC = roundf(programTargetC * 100.0f) / 100.0f;
|
||||||
|
programToleranceC = constrain(jsonFloatValue(body, "toleranceC", programToleranceC), 0.0f, 20.0f);
|
||||||
|
programToleranceC = roundf(programToleranceC * 100.0f) / 100.0f;
|
||||||
|
programMaxRuntimeMinutes = (uint16_t)constrain(jsonIntValue(body, "maxRuntimeMinutes", programMaxRuntimeMinutes), 1, 1440);
|
||||||
|
programPumpRunStartedMs = 0;
|
||||||
|
programModeSseUntilMs = 0;
|
||||||
prefs.putUChar("progMode", programMode);
|
prefs.putUChar("progMode", programMode);
|
||||||
prefs.putFloat("progTargetC", programTargetC);
|
prefs.putFloat("progTargetC", programTargetC);
|
||||||
|
prefs.putFloat("progTolC", programToleranceC);
|
||||||
|
prefs.putUShort("progMaxMin", programMaxRuntimeMinutes);
|
||||||
updateProgram();
|
updateProgram();
|
||||||
appendLog(LOG_INFO, "Program set to " + programModeName() + " target " + String(programTargetC, 2) + " C");
|
appendLog(LOG_INFO, "Program set to " + programModeName() + " target " + String(programTargetC, 2) + " C threshold " + String(programToleranceC, 2) + " C max runtime " + String(programMaxRuntimeMinutes) + " min");
|
||||||
}
|
}
|
||||||
sendJson(200, jsonOk(programJsonFields()));
|
sendJson(200, jsonOk(programJsonFields()));
|
||||||
}
|
}
|
||||||
@@ -190,6 +227,10 @@ void handleTemperatureEvents() {
|
|||||||
payload += "data: {" + temperatureJsonFields() + "}\n\n";
|
payload += "data: {" + temperatureJsonFields() + "}\n\n";
|
||||||
payload += "event: pump\n";
|
payload += "event: pump\n";
|
||||||
payload += "data: {" + pumpJsonFields() + "}\n\n";
|
payload += "data: {" + pumpJsonFields() + "}\n\n";
|
||||||
|
if ((int32_t)(millis() - programModeSseUntilMs) < 0) {
|
||||||
|
payload += "event: program\n";
|
||||||
|
payload += "data: {\"mode\":\"" + programModeName() + "\",\"pumpEnabled\":" + String(pumpEnabled ? "true" : "false") + ",\"reason\":\"maxRuntime\"}\n\n";
|
||||||
|
}
|
||||||
server.sendHeader("Cache-Control", "no-store");
|
server.sendHeader("Cache-Control", "no-store");
|
||||||
server.sendHeader("Connection", "close");
|
server.sendHeader("Connection", "close");
|
||||||
server.send(200, "text/event-stream", payload);
|
server.send(200, "text/event-stream", payload);
|
||||||
|
|||||||
Reference in New Issue
Block a user