Bisher konnte man Automatiken nur anlegen - ausgefuehrt hat sie niemand. restricted/autoActions/autoaction_runner.py holt das nach. Dauerlaeufer statt Cronjob, aus zwei Gruenden: Schwellwert-Ausloeser sollen greifen, wenn die MQTT-Nachricht hereinkommt, und actor_states.current_value wird sonst von niemandem fortgeschrieben - beim Discovery einmal gesetzt und danach nie wieder. Ein zustandsloser Lauf haette gar nichts, womit er vergleichen koennte. Der Runner pflegt den Wert nebenbei mit, wovon auch der Editor profitiert: er zeigt neben jedem Messwert den aktuellen Stand. Ausgeloest wird nur auf der steigenden Flanke (automations.cond_met), sonst wuerde "Temperatur ueber 22 Grad" bei jedem Takt erneut feuern. Aus demselben Grund heissen Zeit-Ausloeser jetzt "ab 16:30" statt "gleich 16:30": ein Gleichheitsvergleich waere nur in einer einzigen Minute wahr, und ein Ausfall in genau dieser Minute kostet den ganzen Tag. Dieselbe Ueberlegung steht hinter den breiten Zeitfenstern in auto_watering.py. Der Weg zum Geraet haengt an der URL des Aktors: mqtt:// abonniert und publiziert, http:// pollt und haengt Parameter an, io:// spricht mit der Tahoma-Box, Logic rechnet Uhrzeit, Datum und Sonnenzeiten. Alle vier stehen in transports.py; eine fuenfte Geraeteart ist eine weitere Klasse mit passt(), zustaende_lesen() und senden(). fetch_calendar.py fuellt calendar_days aus openholidaysapi.org, damit "in den Ferien" und "an Feiertagen" eine Grundlage haben. Einmal jaehrlich per Cron. Die Uebersicht blendet auf schmalen Schirmen Spalten aus, statt sie wegzuschieben - auf dem Handy waren die Knoepfe zum Pausieren und Loeschen sonst nur per Seitwaertsscrollen erreichbar. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
567 lines
20 KiB
JavaScript
567 lines
20 KiB
JavaScript
/**
|
||
* Editor und Uebersicht fuer die Automatiken.
|
||
*
|
||
* Der Editor arbeitet auf einem Modell und zeichnet daraus die Oberflaeche -
|
||
* nicht umgekehrt. Frueher stand die erste Bedingungszeile als festes HTML im
|
||
* PHP und alle weiteren wurden im JavaScript zusammengesetzt; beide Fassungen
|
||
* liefen auseinander, und beim Speichern musste der Server aus durchnummerierten
|
||
* Feldnamen wieder eine Struktur raten.
|
||
*
|
||
* Modell:
|
||
* autoModel.groups = [ [ {state_id, operator, value}, ... ], ... ]
|
||
* eine innere Liste ist ein UND-Block, die aeussere
|
||
* verknuepft die Bloecke mit ODER
|
||
* autoModel.actions = [ {command_id, params: {parameterID: wert}}, ... ]
|
||
*
|
||
* Der Geraetekatalog kommt zusammen mit dem Datensatz im selben Dokument
|
||
* (siehe ajax/AutoAction.php), es gibt also keine Nachladerei je Zeile.
|
||
*/
|
||
|
||
let autoModel = null;
|
||
let autoDevices = [];
|
||
|
||
// --- Katalog-Helfer ------------------------------------------------------
|
||
|
||
function deviceById(id) {
|
||
return autoDevices.find(d => d.id === id) || null;
|
||
}
|
||
|
||
function stateById(id) {
|
||
for (const dev of autoDevices) {
|
||
const state = dev.states.find(s => s.id === id);
|
||
if (state) return { device: dev, state: state };
|
||
}
|
||
return null;
|
||
}
|
||
|
||
function commandById(id) {
|
||
for (const dev of autoDevices) {
|
||
const cmd = dev.commands.find(c => c.id === id);
|
||
if (cmd) return { device: dev, command: cmd };
|
||
}
|
||
return null;
|
||
}
|
||
|
||
function operatorLabel(state, op) {
|
||
const treffer = state.operators.find(o => o.value === op);
|
||
return treffer ? treffer.label : op;
|
||
}
|
||
|
||
/** Erster Messwert des ersten Geraets - Vorbelegung fuer eine neue Zeile. */
|
||
function ersterMesswert() {
|
||
const dev = autoDevices.find(d => d.states.length > 0);
|
||
return dev ? dev.states[0] : null;
|
||
}
|
||
|
||
function ersterBefehl() {
|
||
const dev = autoDevices.find(d => d.commands.length > 0);
|
||
return dev ? dev.commands[0] : null;
|
||
}
|
||
|
||
// --- Bausteine fuer die Oberflaeche --------------------------------------
|
||
|
||
function optionListe(select, eintraege, gewaehlt) {
|
||
select.innerHTML = "";
|
||
eintraege.forEach(e => {
|
||
const opt = document.createElement("option");
|
||
opt.value = e.value;
|
||
opt.text = e.text;
|
||
if (String(e.value) === String(gewaehlt)) opt.selected = true;
|
||
select.appendChild(opt);
|
||
});
|
||
}
|
||
|
||
/**
|
||
* Wertfeld passend zum Datentyp des Messwerts oder Parameters.
|
||
* Liefert das fertige Element; der Aufrufer haengt seinen Listener an.
|
||
*/
|
||
function wertFeld(spec, wert) {
|
||
let feld;
|
||
if (spec.input === "bool") {
|
||
feld = document.createElement("select");
|
||
feld.className = "form-select";
|
||
optionListe(feld, [{ value: "true", text: "JA" }, { value: "false", text: "NEIN" }],
|
||
wert === "" ? "true" : wert);
|
||
} else if (spec.input === "select") {
|
||
feld = document.createElement("select");
|
||
feld.className = "form-select";
|
||
optionListe(feld, spec.options.map(o => ({ value: o, text: o })), wert);
|
||
} else {
|
||
feld = document.createElement("input");
|
||
feld.className = "form-control";
|
||
feld.type = spec.input;
|
||
if (spec.min !== undefined && spec.min !== null) feld.min = spec.min;
|
||
if (spec.max !== undefined && spec.max !== null) feld.max = spec.max;
|
||
feld.value = wert !== "" ? wert
|
||
: spec.input === "time" ? "00:00"
|
||
: spec.input === "date" ? new Date().toISOString().split("T")[0]
|
||
: spec.input === "number" ? "0" : "";
|
||
}
|
||
return feld;
|
||
}
|
||
|
||
/** Standardwert, wenn eine Zeile auf einen anderen Messwert umgestellt wird. */
|
||
function standardWert(spec) {
|
||
if (spec.input === "bool") return "true";
|
||
if (spec.input === "select") return spec.options.length ? spec.options[0] : "";
|
||
if (spec.input === "time") return "00:00";
|
||
if (spec.input === "date") return new Date().toISOString().split("T")[0];
|
||
if (spec.input === "number") return "0";
|
||
return "";
|
||
}
|
||
|
||
// --- Ausloeser -----------------------------------------------------------
|
||
|
||
/**
|
||
* Zeichnet alle UND-Bloecke. Jeder Block hat sein eigenes "+ Bedingung", denn
|
||
* mit einem einzigen Knopf ganz unten kaeme man immer nur an den letzten
|
||
* Block heran und koennte eine bestehende Alternative nie mehr ergaenzen.
|
||
*/
|
||
function renderConditions() {
|
||
const ziel = document.getElementById("condBlocks");
|
||
ziel.innerHTML = "";
|
||
|
||
autoModel.groups.forEach((gruppe, gi) => {
|
||
if (gi > 0) {
|
||
const trenner = document.createElement("div");
|
||
trenner.className = "d-flex align-items-center my-2 text-body-secondary";
|
||
trenner.innerHTML = "<hr class='flex-grow-1 m-0'>"
|
||
+ "<span class='px-3 small fw-bold'>ODER</span>"
|
||
+ "<hr class='flex-grow-1 m-0'>";
|
||
ziel.appendChild(trenner);
|
||
}
|
||
|
||
const block = document.createElement("div");
|
||
block.className = "border rounded p-2";
|
||
|
||
gruppe.forEach((bed, bi) => {
|
||
if (bi > 0) {
|
||
const und = document.createElement("div");
|
||
und.className = "small fw-bold text-body-secondary ms-1 my-1";
|
||
und.textContent = "und";
|
||
block.appendChild(und);
|
||
}
|
||
block.appendChild(conditionRow(gi, bi, bed));
|
||
});
|
||
|
||
const add = document.createElement("button");
|
||
add.type = "button";
|
||
add.className = "btn btn-outline-success btn-sm mt-2";
|
||
add.innerHTML = "<i class='bi bi-plus-lg'></i> Bedingung";
|
||
add.onclick = () => {
|
||
const state = ersterMesswert();
|
||
if (!state) return;
|
||
gruppe.push({ state_id: state.id, operator: state.operators[0].value, value: standardWert(state) });
|
||
renderConditions();
|
||
};
|
||
block.appendChild(add);
|
||
|
||
ziel.appendChild(block);
|
||
});
|
||
|
||
renderSummary();
|
||
}
|
||
|
||
function conditionRow(gi, bi, bed) {
|
||
const zeile = document.createElement("div");
|
||
zeile.className = "input-group";
|
||
|
||
const treffer = stateById(bed.state_id);
|
||
const dev = treffer ? treffer.device : null;
|
||
const state = treffer ? treffer.state : null;
|
||
|
||
// Geraet
|
||
const geraet = document.createElement("select");
|
||
geraet.className = "form-select";
|
||
optionListe(geraet, autoDevices.filter(d => d.states.length)
|
||
.map(d => ({ value: d.id, text: d.name })),
|
||
dev ? dev.id : "");
|
||
geraet.onchange = () => {
|
||
const neu = deviceById(Number(geraet.value));
|
||
if (!neu || !neu.states.length) return;
|
||
bed.state_id = neu.states[0].id;
|
||
bed.operator = neu.states[0].operators[0].value;
|
||
bed.value = standardWert(neu.states[0]);
|
||
renderConditions();
|
||
};
|
||
zeile.appendChild(geraet);
|
||
|
||
// Messwert - nur zeigen, wenn das Geraet mehr als einen hat
|
||
if (dev && dev.states.length > 1) {
|
||
const messwert = document.createElement("select");
|
||
messwert.className = "form-select";
|
||
optionListe(messwert, dev.states.map(s => ({
|
||
value: s.id,
|
||
text: s.name + (s.value ? " (= " + s.value + ")" : "")
|
||
})), bed.state_id);
|
||
messwert.onchange = () => {
|
||
const neu = dev.states.find(s => s.id === Number(messwert.value));
|
||
bed.state_id = neu.id;
|
||
bed.operator = neu.operators[0].value;
|
||
bed.value = standardWert(neu);
|
||
renderConditions();
|
||
};
|
||
zeile.appendChild(messwert);
|
||
}
|
||
|
||
if (state) {
|
||
// Operator. Eine Auswahlliste statt des frueheren Umschalt-Knopfes: der
|
||
// zeigte nur seinen aktuellen Zustand, welche Vergleiche es sonst noch
|
||
// gibt, fand man erst durch Klicken heraus.
|
||
const op = document.createElement("select");
|
||
// Feste, schmale Breite: der Pfeil der Auswahlliste braucht Platz, sonst
|
||
// schiebt er das Zeichen aus dem sichtbaren Bereich.
|
||
op.className = "form-select flex-grow-0 text-center";
|
||
op.style.flex = "0 0 5.5rem";
|
||
optionListe(op, state.operators.map(o => ({ value: o.value, text: o.label })), bed.operator);
|
||
op.onchange = () => { bed.operator = op.value; renderSummary(); };
|
||
zeile.appendChild(op);
|
||
|
||
const wert = wertFeld(state, bed.value);
|
||
wert.onchange = () => { bed.value = wert.value; renderSummary(); };
|
||
zeile.appendChild(wert);
|
||
|
||
if (state.unit) {
|
||
const einheit = document.createElement("span");
|
||
einheit.className = "input-group-text";
|
||
einheit.textContent = state.unit;
|
||
zeile.appendChild(einheit);
|
||
}
|
||
}
|
||
|
||
const weg = document.createElement("button");
|
||
weg.type = "button";
|
||
weg.className = "btn btn-outline-danger";
|
||
weg.innerHTML = "<i class='bi bi-trash3'></i>";
|
||
weg.onclick = () => {
|
||
autoModel.groups[gi].splice(bi, 1);
|
||
// Ein leerer Block hat keine Bedeutung mehr und verschwindet mit.
|
||
if (!autoModel.groups[gi].length) autoModel.groups.splice(gi, 1);
|
||
renderConditions();
|
||
};
|
||
zeile.appendChild(weg);
|
||
|
||
return zeile;
|
||
}
|
||
|
||
/**
|
||
* Derselbe Satz, den auch die Uebersicht und der Runner benutzen. Er steht
|
||
* unter den Bloecken, damit man die Klammerung nicht aus der Optik erschliessen
|
||
* muss, sondern schwarz auf weiss liest.
|
||
*/
|
||
function conditionText() {
|
||
const teile = autoModel.groups.filter(g => g.length).map(gruppe => {
|
||
const text = gruppe.map(bed => {
|
||
const treffer = stateById(bed.state_id);
|
||
if (!treffer) return "?";
|
||
let s = treffer.device.name + ": " + treffer.state.name + " "
|
||
+ operatorLabel(treffer.state, bed.operator) + " " + bed.value;
|
||
if (treffer.state.unit) s += " " + treffer.state.unit;
|
||
return s;
|
||
}).join(" und ");
|
||
return (autoModel.groups.length > 1 && gruppe.length > 1) ? "(" + text + ")" : text;
|
||
});
|
||
return teile.join(" oder ");
|
||
}
|
||
|
||
function renderSummary() {
|
||
const text = conditionText();
|
||
document.getElementById("condSummary").innerHTML = text
|
||
? "Löst aus, wenn <strong>" + text.replace(/&/g, "&").replace(/</g, "<") + "</strong>."
|
||
: "Noch kein Auslöser – die Automatik würde nie starten.";
|
||
}
|
||
|
||
// --- Aktionen ------------------------------------------------------------
|
||
|
||
function renderActions() {
|
||
const ziel = document.getElementById("actionList");
|
||
ziel.innerHTML = "";
|
||
autoModel.actions.forEach((aktion, ai) => ziel.appendChild(actionRow(ai, aktion)));
|
||
}
|
||
|
||
function actionRow(ai, aktion) {
|
||
const zeile = document.createElement("div");
|
||
zeile.className = "input-group mb-2";
|
||
|
||
const treffer = commandById(aktion.command_id);
|
||
const dev = treffer ? treffer.device : null;
|
||
const cmd = treffer ? treffer.command : null;
|
||
|
||
const geraet = document.createElement("select");
|
||
geraet.className = "form-select";
|
||
optionListe(geraet, autoDevices.filter(d => d.commands.length)
|
||
.map(d => ({ value: d.id, text: d.name })),
|
||
dev ? dev.id : "");
|
||
geraet.onchange = () => {
|
||
const neu = deviceById(Number(geraet.value));
|
||
if (!neu || !neu.commands.length) return;
|
||
aktion.command_id = neu.commands[0].id;
|
||
aktion.params = standardParams(neu.commands[0]);
|
||
renderActions();
|
||
};
|
||
zeile.appendChild(geraet);
|
||
|
||
if (dev && dev.commands.length > 1) {
|
||
const befehl = document.createElement("select");
|
||
befehl.className = "form-select";
|
||
optionListe(befehl, dev.commands.map(c => ({ value: c.id, text: c.name })), aktion.command_id);
|
||
befehl.onchange = () => {
|
||
const neu = dev.commands.find(c => c.id === Number(befehl.value));
|
||
aktion.command_id = neu.id;
|
||
aktion.params = standardParams(neu);
|
||
renderActions();
|
||
};
|
||
zeile.appendChild(befehl);
|
||
}
|
||
|
||
// Je Parameter ein Feld - wie viele es sind, sagt das Geraet. Frueher gab es
|
||
// vier feste Felder "Wert 1" bis "Wert 4", die je nach Kommando versteckt
|
||
// wurden, ihren Inhalt aber trotzdem mitschickten.
|
||
if (cmd) {
|
||
cmd.params.forEach(p => {
|
||
const beschriftung = document.createElement("span");
|
||
beschriftung.className = "input-group-text";
|
||
beschriftung.textContent = p.name;
|
||
zeile.appendChild(beschriftung);
|
||
|
||
const wert = wertFeld(p, aktion.params[p.id] !== undefined ? aktion.params[p.id] : standardWert(p));
|
||
wert.onchange = () => { aktion.params[p.id] = wert.value; };
|
||
zeile.appendChild(wert);
|
||
});
|
||
}
|
||
|
||
const weg = document.createElement("button");
|
||
weg.type = "button";
|
||
weg.className = "btn btn-outline-danger";
|
||
weg.innerHTML = "<i class='bi bi-trash3'></i>";
|
||
weg.onclick = () => { autoModel.actions.splice(ai, 1); renderActions(); };
|
||
zeile.appendChild(weg);
|
||
|
||
return zeile;
|
||
}
|
||
|
||
function standardParams(cmd) {
|
||
const params = {};
|
||
cmd.params.forEach(p => { params[p.id] = standardWert(p); });
|
||
return params;
|
||
}
|
||
|
||
// --- Rahmenbedingungen ---------------------------------------------------
|
||
|
||
const WOCHENTAGE = ["Mo", "Di", "Mi", "Do", "Fr", "Sa", "So"];
|
||
|
||
function renderWeekdays() {
|
||
const ziel = document.getElementById("weekdayGroup");
|
||
ziel.innerHTML = "";
|
||
WOCHENTAGE.forEach((tag, i) => {
|
||
const feld = document.createElement("div");
|
||
feld.className = "input-group-text";
|
||
feld.innerHTML = "<input class='form-check-input mt-0 me-1' type='checkbox' id='day" + i + "'"
|
||
+ (autoModel.weekdays & (1 << i) ? " checked" : "") + ">"
|
||
+ "<label class='form-check-label' for='day" + i + "'>" + tag + "</label>";
|
||
feld.querySelector("input").onchange = e => {
|
||
if (e.target.checked) autoModel.weekdays |= (1 << i);
|
||
else autoModel.weekdays &= ~(1 << i);
|
||
};
|
||
ziel.appendChild(feld);
|
||
});
|
||
}
|
||
|
||
function fuelleFormular() {
|
||
document.getElementById("autoName").value = autoModel.name;
|
||
document.getElementById("autoFloor").value = autoModel.floor;
|
||
document.getElementById("tSpanFrom").value = autoModel.window_from;
|
||
document.getElementById("tSpanTo").value = autoModel.window_to;
|
||
document.getElementById("actFerien").checked = autoModel.on_vacation;
|
||
document.getElementById("actFeier").checked = autoModel.on_holiday;
|
||
document.getElementById("runOnce").checked = autoModel.force_once;
|
||
renderWeekdays();
|
||
renderConditions();
|
||
renderActions();
|
||
}
|
||
|
||
/** Die Felder, die direkt am Formular haengen, ins Modell zurueckholen. */
|
||
function leseFormular() {
|
||
autoModel.name = document.getElementById("autoName").value.trim();
|
||
autoModel.floor = document.getElementById("autoFloor").value;
|
||
autoModel.window_from = document.getElementById("tSpanFrom").value;
|
||
autoModel.window_to = document.getElementById("tSpanTo").value;
|
||
autoModel.on_vacation = document.getElementById("actFerien").checked;
|
||
autoModel.on_holiday = document.getElementById("actFeier").checked;
|
||
autoModel.force_once = document.getElementById("runOnce").checked;
|
||
}
|
||
|
||
// --- Modal ---------------------------------------------------------------
|
||
|
||
function loadAutomatic() {
|
||
const daten = JSON.parse(document.getElementById("autoActionData").textContent);
|
||
autoDevices = daten.devices;
|
||
|
||
const a = daten.automation;
|
||
autoModel = {
|
||
id: a.id,
|
||
name: a.name,
|
||
floor: a.floor,
|
||
enabled: a.enabled,
|
||
window_from: a.window_from,
|
||
window_to: a.window_to,
|
||
weekdays: a.weekdays,
|
||
on_vacation: a.on_vacation,
|
||
on_holiday: a.on_holiday,
|
||
force_once: a.force_once,
|
||
groups: [],
|
||
actions: a.actions.map(x => ({ command_id: x.command_id, params: Object.assign({}, x.params) }))
|
||
};
|
||
|
||
// Die flache Liste aus der Datenbank wieder in Bloecke fassen. Die
|
||
// Gruppennummern kommen aufsteigend und dicht, siehe saveAutomation().
|
||
const nachGruppe = new Map();
|
||
a.conditions.forEach(c => {
|
||
if (!nachGruppe.has(c.group)) nachGruppe.set(c.group, []);
|
||
nachGruppe.get(c.group).push({ state_id: c.state_id, operator: c.operator, value: c.value });
|
||
});
|
||
autoModel.groups = Array.from(nachGruppe.values());
|
||
|
||
// Eine neue Automatik startet mit einer leeren Bedingung und einer Aktion,
|
||
// sonst steht man vor einem leeren Formular ohne erkennbaren Anfang.
|
||
if (!autoModel.groups.length) {
|
||
const state = ersterMesswert();
|
||
if (state) {
|
||
autoModel.groups = [[{ state_id: state.id, operator: state.operators[0].value, value: standardWert(state) }]];
|
||
}
|
||
}
|
||
if (!autoModel.actions.length) {
|
||
const cmd = ersterBefehl();
|
||
if (cmd) autoModel.actions = [{ command_id: cmd.id, params: standardParams(cmd) }];
|
||
}
|
||
|
||
fuelleFormular();
|
||
}
|
||
|
||
function openAutoActionModal(params) {
|
||
const contentURL = "./ajax/AutoAction.php" + params;
|
||
document.getElementById("modal-title").innerHTML =
|
||
params.search("id=") > 0 ? "Automatik bearbeiten" : "Neue Automatik anlegen";
|
||
|
||
// Der Speichern-Knopf gehoert allen Modals gemeinsam. Ein Klon ohne
|
||
// Zuhoerer stellt sicher, dass nicht auch noch der Handler eines vorher
|
||
// geoeffneten Modals mitfeuert.
|
||
const alt = document.getElementById("modalSaveBtn");
|
||
const btn = alt.cloneNode(true);
|
||
alt.replaceWith(btn);
|
||
btn.addEventListener("click", submitAutoAction);
|
||
|
||
// Der Editor braucht mehr Breite als die Ladedialoge, gibt sie aber wieder
|
||
// her, damit die anderen Modals unveraendert aussehen.
|
||
const dialog = document.querySelector("#modalEV .modal-dialog");
|
||
dialog.classList.add("modal-xl");
|
||
document.getElementById("modalEV").addEventListener("hidden.bs.modal",
|
||
() => dialog.classList.remove("modal-xl"), { once: true });
|
||
|
||
const modalBodyElement = document.getElementById("modal-body");
|
||
modalBodyElement.innerHTML = loadingHTML("Wird geladen...");
|
||
modalEV.show();
|
||
|
||
fetch(contentURL, {
|
||
method: "GET",
|
||
headers: { "X-Requested-From-Modal": "a", "Requested-With-Ajax": "ajax" }
|
||
})
|
||
.then(response => response.text())
|
||
.then(html => {
|
||
modalBodyElement.innerHTML = html;
|
||
loadAutomatic();
|
||
document.getElementById("btnAddGroup").addEventListener("click", () => {
|
||
const state = ersterMesswert();
|
||
if (!state) return;
|
||
autoModel.groups.push([{ state_id: state.id, operator: state.operators[0].value, value: standardWert(state) }]);
|
||
renderConditions();
|
||
});
|
||
document.getElementById("btnAddActor").addEventListener("click", () => {
|
||
const cmd = ersterBefehl();
|
||
if (!cmd) return;
|
||
autoModel.actions.push({ command_id: cmd.id, params: standardParams(cmd) });
|
||
renderActions();
|
||
});
|
||
})
|
||
.catch(error => {
|
||
modalBodyElement.innerHTML = "Konnte den Editor nicht laden: " + error.message;
|
||
});
|
||
}
|
||
|
||
function submitAutoAction() {
|
||
leseFormular();
|
||
const nutzlast = {
|
||
id: autoModel.id,
|
||
name: autoModel.name,
|
||
floor: autoModel.floor,
|
||
enabled: autoModel.enabled,
|
||
window_from: autoModel.window_from,
|
||
window_to: autoModel.window_to,
|
||
weekdays: autoModel.weekdays,
|
||
on_vacation: autoModel.on_vacation,
|
||
on_holiday: autoModel.on_holiday,
|
||
force_once: autoModel.force_once,
|
||
conditions: [],
|
||
actions: autoModel.actions
|
||
};
|
||
autoModel.groups.forEach((gruppe, gi) => {
|
||
gruppe.forEach(bed => {
|
||
nutzlast.conditions.push({ group: gi, state_id: bed.state_id, operator: bed.operator, value: bed.value });
|
||
});
|
||
});
|
||
|
||
automationRequest("save", nutzlast).then(antwort => {
|
||
if (antwort.error) {
|
||
alert(antwort.error);
|
||
return;
|
||
}
|
||
modalEV.hide();
|
||
refreshAutomations(nutzlast.floor);
|
||
});
|
||
}
|
||
|
||
// --- Uebersicht ----------------------------------------------------------
|
||
|
||
function automationRequest(action, body) {
|
||
return fetch("./ajax/AutoAction.php?action=" + action, {
|
||
method: "POST",
|
||
headers: { "Content-Type": "application/json", "Requested-With-Ajax": "ajax" },
|
||
body: JSON.stringify(body)
|
||
})
|
||
.then(response => response.json())
|
||
.catch(error => ({ error: error.message }));
|
||
}
|
||
|
||
/** Die Tabelle einer Etage neu holen. Ohne Etage alle drei. */
|
||
function refreshAutomations(floor) {
|
||
const etagen = floor ? [floor] : ["UG", "EG", "OG"];
|
||
etagen.forEach(etage => {
|
||
const ziel = document.getElementById("actions-" + etage + "-list");
|
||
if (!ziel) return;
|
||
fetch("./ajax/AutoAction.php?action=list&floor=" + etage, {
|
||
method: "GET",
|
||
headers: { "Requested-With-Ajax": "ajax" }
|
||
})
|
||
.then(response => response.text())
|
||
.then(html => { ziel.innerHTML = html; })
|
||
.catch(() => { ziel.innerHTML = "<p class='text-danger'>Liste konnte nicht geladen werden.</p>"; });
|
||
});
|
||
}
|
||
|
||
function toggleAutomation(id) {
|
||
automationRequest("toggle", { id: id }).then(antwort => {
|
||
if (antwort.error) alert(antwort.error);
|
||
else refreshAutomations(null);
|
||
});
|
||
}
|
||
|
||
function deleteAutomation(id, name) {
|
||
if (!confirm("Automatik \"" + name + "\" wirklich löschen?")) return;
|
||
automationRequest("delete", { id: id }).then(antwort => {
|
||
if (antwort.error) alert(antwort.error);
|
||
else refreshAutomations(null);
|
||
});
|
||
}
|
||
|
||
document.addEventListener("DOMContentLoaded", () => refreshAutomations(null));
|