Manche Geraete schicken eine Zahl und meinen einen Zustand - der
go-eCharger etwa 2 fuer "Charging". Welche Zahl welchen Namen hat, steht im
value_template der Home-Assistant-Discovery als Werttabelle:
{{ ['Unknown','Idle','Charging','WaitCar','Complete','Error'][value_json|int] }}
Das ist kein Pfad in die Nutzlast, sondern eine Uebersetzung, und sie wurde
bisher verworfen. Jetzt landet sie in possible_values - in derselben
Schreibweise, die WLED fuer seine Effektliste schon benutzt, naemlich einer
Liste aus {Wert: Bezeichnung}. Damit versteht der Editor sie ohne
Zusatzarbeit und macht eine Auswahlliste daraus. Ein Versatz im Ausdruck
wandert in die Schluessel: aus [value_json|int-3] wird {"3":"Default"}.
Der Runner uebersetzt beim Lesen, eine Bedingung vergleicht also den
Klartext. Steht die Zahl nicht in der Tabelle, bleibt sie stehen - ein
erfundener Name waere schlimmer als ein roher Wert.
Dabei ist ein Folgefehler aufgefallen: Auswahlwerte in dieser Schreibweise
kamen im Editor als "[object Object]" an. Die alte addOptions kannte die
Form, der neue Modell-Aufbau nicht - aufgefallen ist es nie, weil WLED beim
Umbau abgeschaltet war. Der Katalog liefert Auswahlwerte jetzt einheitlich
als {value, label}, und zwar seitenrichtig:
Messwert angezeigt Charging, gespeichert Charging
(der Runner hat schon uebersetzt)
Parameter angezeigt Blink, gespeichert 1
(das Geraet will die Zahl)
Die Uebersicht loest die Bezeichnung ebenfalls auf: "Effekt (Blink)" statt
"Effekt (1)".
Nachgemessen: alle fuenf Werttabellen auf dem Broker richtig erkannt,
einschliesslich des Versatzes, kein Fehltreffer unter den 35 uebrigen
Vorlagen. Live liefert der go-eCharger jetzt Idle, Neutral, Auto, Eco und
None statt 1, 0, 0, 4, 0. Ein weiterer Discovery-Lauf aendert keine Zeile.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
584 lines
21 KiB
JavaScript
584 lines
21 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 = [];
|
||
let autoLockouts = [];
|
||
|
||
// --- 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") {
|
||
// Die Auswahl kommt fertig aus dem Katalog: bei Messwerten steht der
|
||
// Klartext auch als Wert, bei Geraeteparametern die Zahl dahinter.
|
||
feld = document.createElement("select");
|
||
feld.className = "form-select";
|
||
optionListe(feld, spec.options.map(o => ({ value: o.value, text: o.label })), 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].value : "";
|
||
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() {
|
||
const sperre = document.getElementById("lockoutSecs");
|
||
optionListe(sperre, autoLockouts.map(o => ({ value: o.value, text: o.label })),
|
||
autoModel.lockout_secs);
|
||
document.getElementById("autoName").value = autoModel.name;
|
||
// Kennt die Liste die Etage nicht, bleibt eine Auswahlliste sonst leer
|
||
// stehen und schickt beim Speichern "" mit.
|
||
const etage = document.getElementById("autoFloor");
|
||
etage.value = autoModel.floor;
|
||
if (etage.selectedIndex < 0) {
|
||
etage.selectedIndex = 0;
|
||
autoModel.floor = etage.value;
|
||
}
|
||
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;
|
||
autoModel.lockout_secs = Number(document.getElementById("lockoutSecs").value);
|
||
}
|
||
|
||
// --- Modal ---------------------------------------------------------------
|
||
|
||
function loadAutomatic() {
|
||
const daten = JSON.parse(document.getElementById("autoActionData").textContent);
|
||
autoDevices = daten.devices;
|
||
autoLockouts = daten.lockouts;
|
||
|
||
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,
|
||
lockout_secs: a.lockout_secs,
|
||
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,
|
||
lockout_secs: autoModel.lockout_secs,
|
||
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));
|