Add relay enable toggles to WebUI

This commit is contained in:
2026-07-02 23:50:19 -06:00
parent 2e4fb4ad6c
commit 563434a0a1
4 changed files with 57 additions and 11 deletions
+1 -1
View File
@@ -396,7 +396,7 @@ Fields:
|---|---:|---|
| `id` | Yes | `relay_1` or `relay_2` |
| `name` | No | User display name |
| `enabled` | No | Configured enabled state |
| `enabled` | No | Configured enabled state. When set to `false`, the relay output is forced OFF and future relay control requests are rejected with `relay_disabled`. |
Returns the updated config.
+2
View File
@@ -69,6 +69,8 @@ HTTP endpoint:
POST /api/v1/config/relay
Disabled relays are forced OFF immediately, hidden from normal dashboard controls, and protected from `/api/v1/relay/set` commands until re-enabled.
## Temperature Sensor Configuration
Temperature sensors use generic firmware IDs:
@@ -1249,11 +1249,20 @@ function renderConfigControls(data){
<div class="configrow">
<label>${r.id}</label>
<input id="relayName${i}" placeholder="Relay name" oninput="markConfigFieldTouched('relayName${i}')">
<label>${r.enabled?"Enabled":"Disabled"}</label>
<label class="tempCheck">
<input id="relayEnabled${i}" type="checkbox" onchange="markConfigFieldTouched('relayEnabled${i}')">
Enabled
</label>
</div>`).join("");
}
relays.forEach((r,i)=>setConfigValue("relayName"+i,r.name||""));
relays.forEach((r,i)=>{
setConfigValue("relayName"+i,r.name||"");
const enabled=$("relayEnabled"+i);
if(enabled && !configTouched["relayEnabled"+i] && document.activeElement!==enabled){
enabled.checked=!!r.enabled;
}
});
}
setConfigValue("tempEnabledCount", temps.filter(t=>t.enabled).length);
@@ -1359,8 +1368,8 @@ async function saveRelayConfig(){
const relays=data.config?.relays||[];
for(let i=0;i<relays.length;i++){
const name=$("relayName"+i)?.value.trim();
if(!name) continue;
const name=$("relayName"+i)?.value.trim() || relays[i].name || relays[i].id;
const enabled=$("relayEnabled"+i)?.checked ?? relays[i].enabled;
await fetch(api("/config/relay"),{
method:"POST",
@@ -1368,14 +1377,14 @@ async function saveRelayConfig(){
body:JSON.stringify({
id:relays[i].id,
name:name,
enabled:relays[i].enabled
enabled:enabled
})
});
}
await fetch(api("/config/save"),{method:"POST"});
await load();
alert("Relay names saved");
alert("Relay settings saved");
}
async function saveTempConfig(){
@@ -2643,9 +2652,19 @@ void sendError(Stream& output, const char* message) {
int findRelayConfigIndex(const String& id);
bool relayExistsAndAvailable(const String& relayId) {
int index = findRelayConfigIndex(relayId);
return index >= 0 && appConfig.relays[index].available;
}
bool relayIsEnabled(const String& relayId) {
int index = findRelayConfigIndex(relayId);
return index >= 0 && appConfig.relays[index].enabled;
}
bool setRelayById(const String& relayId, bool enabled) {
int index = findRelayConfigIndex(relayId);
if (index < 0 || !appConfig.relays[index].available) {
if (index < 0 || !appConfig.relays[index].available || !appConfig.relays[index].enabled) {
return false;
}
@@ -3651,7 +3670,13 @@ void handleUpdateRelayConfig() {
}
if (doc["name"].is<String>()) appConfig.relays[index].name = doc["name"].as<String>();
if (doc["enabled"].is<bool>()) appConfig.relays[index].enabled = doc["enabled"].as<bool>();
if (doc["enabled"].is<bool>()) {
appConfig.relays[index].enabled = doc["enabled"].as<bool>();
if (!appConfig.relays[index].enabled) {
relays.state[index] = false;
updateRelayOutputs();
}
}
sendOkConfig();
}
@@ -3994,10 +4019,18 @@ void handleSetRelayPost() {
bool state = doc["state"] | false;
unsigned long applyStartMs = millis();
if (!setRelayById(relayId, state)) {
if (!relayExistsAndAvailable(relayId)) {
server.send(404, "application/json", "{\"ok\":false,\"error\":\"unknown_relay\"}");
return;
}
if (!relayIsEnabled(relayId)) {
server.send(409, "application/json", "{\"ok\":false,\"error\":\"relay_disabled\"}");
return;
}
if (!setRelayById(relayId, state)) {
server.send(409, "application/json", "{\"ok\":false,\"error\":\"relay_disabled\"}");
return;
}
unsigned long appliedMs = millis() - applyStartMs;
DynamicJsonDocument response(512);
@@ -4052,10 +4085,18 @@ void handleGenericRelayRoute() {
return;
}
if (!setRelayById(relayId, enabled)) {
if (!relayExistsAndAvailable(relayId)) {
server.send(404, "application/json", "{\"ok\":false,\"error\":\"unknown_relay\"}");
return;
}
if (!relayIsEnabled(relayId)) {
server.send(409, "application/json", "{\"ok\":false,\"error\":\"relay_disabled\"}");
return;
}
if (!setRelayById(relayId, enabled)) {
server.send(409, "application/json", "{\"ok\":false,\"error\":\"relay_disabled\"}");
return;
}
DynamicJsonDocument doc(512);
doc["ok"] = true;
+3
View File
@@ -248,6 +248,8 @@ def test_relay_set_contract_fixtures_and_firmware_errors():
source = firmware_source()
assert '"invalid_json"' in source
assert '"unknown_relay"' in source
assert "relay_disabled" in source
assert "relayIsEnabled" in source
def test_relay_api_is_output_count_agnostic():
@@ -259,6 +261,7 @@ def test_relay_api_is_output_count_agnostic():
assert 'relay["hardware_channel"] = appConfig.relays[i].hardwareChannel;' in source
assert 'relay["available"] = appConfig.relays[i].available;' in source
assert 'relays.state[index] = enabled;' in source
assert '!appConfig.relays[index].enabled' in source
assert 'relays.relay1' not in source
assert 'relays.relay2' not in source