BEHAVIOR-001: learn and predict actuator actions
This commit is contained in:
@@ -7,9 +7,9 @@
|
||||
<style>
|
||||
:root { color-scheme: dark; font-family: system-ui, sans-serif; background: #10151c; color: #eaf1f8; }
|
||||
body { margin: 0; }
|
||||
header { padding: 20px; background: linear-gradient(135deg,#142b3a,#193f36); }
|
||||
header { padding: 22px; background: linear-gradient(135deg,#142b3a,#193f36); }
|
||||
h1,h2,h3 { margin: 0 0 12px; }
|
||||
header p { margin: 4px 0; color: #b9c9d6; }
|
||||
header p { margin: 5px 0; color: #c3d1dc; }
|
||||
main { display: grid; grid-template-columns: repeat(auto-fit,minmax(320px,1fr)); gap: 14px; padding: 14px; }
|
||||
section { background: #18212b; border: 1px solid #2d3a47; border-radius: 12px; padding: 16px; }
|
||||
.wide { grid-column: 1 / -1; }
|
||||
@@ -17,87 +17,92 @@
|
||||
.warn { color: #f3c969; }
|
||||
.bad { color: #ff8f8f; }
|
||||
label { display: block; margin: 9px 0 4px; color: #b9c9d6; }
|
||||
input,select,textarea,button { box-sizing: border-box; width: 100%; border-radius: 7px; border: 1px solid #3b4b5b; padding: 9px; background: #101820; color: #fff; }
|
||||
select,button { box-sizing: border-box; width: 100%; border-radius: 7px; border: 1px solid #3b4b5b; padding: 10px; background: #101820; color: #fff; }
|
||||
button { margin-top: 10px; background: #23715b; border: 0; font-weight: 700; cursor: pointer; }
|
||||
button.secondary { background: #37495c; }
|
||||
pre { white-space: pre-wrap; overflow: auto; background: #0d141b; padding: 10px; border-radius: 7px; }
|
||||
table { width: 100%; border-collapse: collapse; font-size: .9rem; }
|
||||
td,th { padding: 7px; border-bottom: 1px solid #2d3a47; text-align: left; vertical-align: top; }
|
||||
button.danger { background: #7b3434; }
|
||||
table { width: 100%; border-collapse: collapse; font-size: .92rem; }
|
||||
td,th { padding: 8px; border-bottom: 1px solid #2d3a47; text-align: left; vertical-align: top; }
|
||||
ul { margin: 8px 0; padding-left: 18px; }
|
||||
.notice { border-left: 4px solid #e8b34b; padding-left: 10px; }
|
||||
.grid-two { display:grid; grid-template-columns:repeat(auto-fit,minmax(220px,1fr)); gap:8px; }
|
||||
.notice { border-left: 4px solid #66dfa9; padding-left: 10px; }
|
||||
.grid-two { display:grid; grid-template-columns:repeat(auto-fit,minmax(240px,1fr)); gap:12px; }
|
||||
.chips { display:flex; flex-wrap:wrap; gap:6px; margin-top:8px; }
|
||||
.chip { padding:4px 8px; border-radius:999px; background:#22303c; border:1px solid #31404d; font-size:.85rem; }
|
||||
.muted { color:#9fb0be; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<h1>SillyHome Next</h1>
|
||||
<p>Aktuator-zentrierte Home-Assistant-Analyse mit nachvollziehbarer Sensorzuordnung und kontrolliertem Modell-Lebenszyklus.</p>
|
||||
<p class="notice">Sicherheitsmodus: SillyHome führt niemals selbst Aktor-Services aus. Automationen bleiben manuell freizugebende YAML-Entwürfe.</p>
|
||||
<p>Du wählst nur die Aktoren. SillyHome findet Kontext, lernt Gewohnheiten und trifft Vorhersagen im Shadow-Modus.</p>
|
||||
<p class="notice">Geschaltet wird erst nach deiner ausdrücklichen Freigabe pro Aktor.</p>
|
||||
</header>
|
||||
<main>
|
||||
<section>
|
||||
<h2>Systemstatus</h2>
|
||||
<div id="status">Prüfung läuft ...</div>
|
||||
<div class="chips" id="status-chips"></div>
|
||||
<button class="secondary" onclick="loadOverview()">Neu laden</button>
|
||||
<button onclick="runReconciliation()">Reconciliation ausführen</button>
|
||||
<button class="secondary" onclick="loadOverview()">Status aktualisieren</button>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>Aktuator wählen</h2>
|
||||
<h2>Aktor freigeben</h2>
|
||||
<p class="muted">Nach der Auswahl analysiert SillyHome automatisch passende Sensoren, Zustände und Historie.</p>
|
||||
<label for="actuator-select">Home-Assistant-Aktor</label>
|
||||
<select id="actuator-select"></select>
|
||||
<button onclick="configureActuator()">Aktuator übernehmen</button>
|
||||
<pre id="actuator-config-result">Noch kein Aktuator konfiguriert.</pre>
|
||||
<button onclick="configureActuator()">Auswählen und Lernen starten</button>
|
||||
<p id="actuator-config-result" class="muted">Noch kein Aktor ausgewählt.</p>
|
||||
</section>
|
||||
|
||||
<section class="wide">
|
||||
<h2>Konfigurierte Aktuatoren</h2>
|
||||
<h2>Ausgewählte Aktoren</h2>
|
||||
<div id="configured-actuators">Noch nicht geladen.</div>
|
||||
</section>
|
||||
|
||||
<section class="wide">
|
||||
<h2>Zuordnung und Modellstatus</h2>
|
||||
<div id="actuator-detail">Einen konfigurierten Aktuator auswählen.</div>
|
||||
</section>
|
||||
|
||||
<section class="wide">
|
||||
<h2>Automation-Entwurf</h2>
|
||||
<p>Der Entwurf muss explizit freigegeben werden. Auch danach wird nur YAML exportiert, nichts geschaltet.</p>
|
||||
<div class="grid-two">
|
||||
<div><label for="alias">Name</label><input id="alias" value="Licht bei Dunkelheit"></div>
|
||||
<div><label for="trigger">Trigger-Entity</label><input id="trigger" placeholder="sensor.flur_illuminance"></div>
|
||||
<div><label for="below">Unter Grenzwert</label><input id="below" type="number" value="10"></div>
|
||||
<div><label for="service">Dienst</label><select id="service"><option>light.turn_on</option><option>light.turn_off</option><option>switch.turn_on</option><option>switch.turn_off</option></select></div>
|
||||
<div><label for="target">Ziel-Entity</label><input id="target" placeholder="light.flur"></div>
|
||||
</div>
|
||||
<button onclick="createProposal()">Entwurf speichern</button>
|
||||
<button class="secondary" onclick="loadProposals()">Entwürfe aktualisieren</button>
|
||||
<div id="proposals"></div>
|
||||
<h2>Automatisch erkannter Lernkontext</h2>
|
||||
<div id="actuator-detail" class="muted">Wähle einen Aktor aus der Liste.</div>
|
||||
</section>
|
||||
</main>
|
||||
<script>
|
||||
const pretty = value => JSON.stringify(value, null, 2);
|
||||
const escapeHtml = value => String(value ?? "")
|
||||
.replaceAll("&", "&")
|
||||
.replaceAll("<", "<")
|
||||
.replaceAll(">", ">")
|
||||
.replaceAll('"', """)
|
||||
.replaceAll("'", "'");
|
||||
let currentActuatorId = null;
|
||||
|
||||
async function api(path, options = {}) {
|
||||
const response = await fetch(path, {headers: {"Content-Type": "application/json"}, ...options});
|
||||
const body = await response.json().catch(() => ({}));
|
||||
if (!response.ok) throw new Error(body.detail || `${response.status} ${response.statusText}`);
|
||||
const body = response.status === 204 ? null : await response.json().catch(() => ({}));
|
||||
if (!response.ok) throw new Error(body?.detail || `${response.status} ${response.statusText}`);
|
||||
return body;
|
||||
}
|
||||
|
||||
function lifecycleLabel(record) {
|
||||
const labels = {
|
||||
trained: "lernt",
|
||||
pending_history: "sammelt Historie",
|
||||
pending_assignment: "sucht Kontext",
|
||||
review_required: "geringe Zuordnungssicherheit",
|
||||
archived: "wartet auf Kontext",
|
||||
orphaned: "Aktor nicht gefunden",
|
||||
};
|
||||
return labels[record.lifecycle.status] || record.lifecycle.status;
|
||||
}
|
||||
|
||||
function statusClass(record) {
|
||||
if (record.assignment.review_required) return "warn";
|
||||
if (record.lifecycle.status === "trained") return "ok";
|
||||
if (record.lifecycle.status === "review_required" || record.lifecycle.status === "invalid") return "warn";
|
||||
if (["pending_history", "pending_assignment", "archived"].includes(record.lifecycle.status)) return "warn";
|
||||
return "bad";
|
||||
}
|
||||
|
||||
function renderEvidence(evidence) {
|
||||
return evidence.length ? `<ul>${evidence.map(item => `<li>${item}</li>`).join("")}</ul>` : "<span class='bad'>Keine Evidenz</span>";
|
||||
function behaviorLabel(record) {
|
||||
if (record.behavior.mode === "active") return "aktiv freigegeben";
|
||||
if (record.behavior.status === "trained") return "Shadow-Vorhersage";
|
||||
if (record.behavior.status === "blocked") return "Lernen blockiert";
|
||||
return "sammelt Handlungen";
|
||||
}
|
||||
|
||||
async function loadOverview() {
|
||||
@@ -110,57 +115,53 @@ async function loadOverview() {
|
||||
api("v1/actuators/reconciliation/state"),
|
||||
api("v1/actuators"),
|
||||
]);
|
||||
status.innerHTML = `<p class="ok">API und ML bereit</p><p>Letzte Reconciliation: ${reconciliation.last_completed_at || "noch nie"}</p><p>${reconciliation.last_summary}</p>`;
|
||||
status.innerHTML = `<p class="ok">System bereit</p><p>Letzte automatische Prüfung: ${escapeHtml(reconciliation.last_completed_at || "noch nie")}</p>`;
|
||||
chips.innerHTML = [
|
||||
`<span class="chip">Health: ${health.status}</span>`,
|
||||
`<span class="chip">ML: ${ml.status}</span>`,
|
||||
`<span class="chip">Aktuatoren: ${actuators.length}</span>`,
|
||||
`<span class="chip">Trainierte Modelle: ${reconciliation.trained_models}</span>`,
|
||||
`<span class="chip">API: ${escapeHtml(health.status)}</span>`,
|
||||
`<span class="chip">Lernsystem: ${escapeHtml(ml.status)}</span>`,
|
||||
`<span class="chip">Aktoren: ${actuators.length}</span>`,
|
||||
`<span class="chip">Aktive Modelle: ${reconciliation.trained_models}</span>`,
|
||||
].join("");
|
||||
} catch (error) {
|
||||
status.innerHTML = `<p class="bad">${error.message}</p>`;
|
||||
status.innerHTML = `<p class="bad">${escapeHtml(error.message)}</p>`;
|
||||
chips.innerHTML = "";
|
||||
}
|
||||
await Promise.all([loadActuatorDiscovery(), loadConfiguredActuators(), loadProposals()]);
|
||||
await Promise.all([loadActuatorDiscovery(), loadConfiguredActuators()]);
|
||||
}
|
||||
|
||||
async function loadActuatorDiscovery() {
|
||||
const select = document.getElementById("actuator-select");
|
||||
try {
|
||||
const actuators = await api("v1/actuators/discovery");
|
||||
select.innerHTML = actuators.length
|
||||
? actuators.map(entity => `<option value="${entity.entity_id}">${entity.friendly_name || entity.entity_id}${entity.area_name ? ` (${entity.area_name})` : ""}</option>`).join("")
|
||||
: "<option value=''>Keine Aktuatoren gefunden</option>";
|
||||
const [available, configured] = await Promise.all([
|
||||
api("v1/actuators/discovery"),
|
||||
api("v1/actuators"),
|
||||
]);
|
||||
const configuredIds = new Set(configured.map(record => record.actuator_entity_id));
|
||||
const choices = available.filter(entity => !configuredIds.has(entity.entity_id));
|
||||
select.innerHTML = choices.length
|
||||
? choices.map(entity => `<option value="${escapeHtml(entity.entity_id)}">${escapeHtml(entity.friendly_name || entity.entity_id)}${entity.area_name ? ` (${escapeHtml(entity.area_name)})` : ""}</option>`).join("")
|
||||
: "<option value=''>Alle erkannten Aktoren sind ausgewählt</option>";
|
||||
} catch (error) {
|
||||
select.innerHTML = `<option value="">${error.message}</option>`;
|
||||
select.innerHTML = `<option value="">${escapeHtml(error.message)}</option>`;
|
||||
}
|
||||
}
|
||||
|
||||
async function configureActuator() {
|
||||
const actuatorId = document.getElementById("actuator-select").value;
|
||||
const box = document.getElementById("actuator-config-result");
|
||||
const result = document.getElementById("actuator-config-result");
|
||||
if (!actuatorId) return;
|
||||
result.textContent = "Kontext wird automatisch analysiert ...";
|
||||
try {
|
||||
const record = await api("v1/actuators", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({actuator_entity_id: actuatorId}),
|
||||
});
|
||||
currentActuatorId = record.actuator_entity_id;
|
||||
box.textContent = pretty(record);
|
||||
result.textContent = `${record.actuator_entity_id}: ${lifecycleLabel(record)}.`;
|
||||
await loadOverview();
|
||||
await showActuator(record.actuator_entity_id);
|
||||
} catch (error) {
|
||||
box.textContent = error.message;
|
||||
}
|
||||
}
|
||||
|
||||
async function runReconciliation() {
|
||||
try {
|
||||
await api("v1/actuators/reconciliation/run", {method: "POST"});
|
||||
await loadOverview();
|
||||
if (currentActuatorId) await showActuator(currentActuatorId);
|
||||
} catch (error) {
|
||||
alert(error.message);
|
||||
result.textContent = error.message;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -170,18 +171,22 @@ async function loadConfiguredActuators() {
|
||||
const rows = await api("v1/actuators");
|
||||
box.innerHTML = rows.length ? `
|
||||
<table>
|
||||
<tr><th>Aktuator</th><th>Numerischer Sensor</th><th>Review</th><th>Modellstatus</th><th>Letztes Training</th><th>Aktion</th></tr>
|
||||
<tr><th>Aktor</th><th>Verhaltensmodell</th><th>Handlungen</th><th>Vorhersage</th><th></th></tr>
|
||||
${rows.map(record => `
|
||||
<tr>
|
||||
<td>${record.actuator_entity_id}</td>
|
||||
<td>${record.assignment.selected_numeric_entity_id || "-"}</td>
|
||||
<td class="${record.assignment.review_required ? "warn" : "ok"}">${record.assignment.review_required ? "erforderlich" : "nein"}</td>
|
||||
<td class="${statusClass(record)}">${record.lifecycle.status}</td>
|
||||
<td>${record.lifecycle.last_trained_at || "-"}</td>
|
||||
<td><button onclick="showActuator('${record.actuator_entity_id}')">Details</button></td>
|
||||
<td>${escapeHtml(record.actuator_entity_id)}</td>
|
||||
<td class="${record.behavior.status === "trained" ? "ok" : "warn"}">${escapeHtml(behaviorLabel(record))}</td>
|
||||
<td>${record.behavior.sample_count}</td>
|
||||
<td>${record.behavior.prediction
|
||||
? `${escapeHtml(record.behavior.prediction.target_state)} (${Math.round(record.behavior.prediction.confidence * 100)} %)`
|
||||
: "-"}</td>
|
||||
<td>
|
||||
<button onclick="showActuator('${escapeHtml(record.actuator_entity_id)}')">Details</button>
|
||||
<button class="danger" onclick="removeActuator('${escapeHtml(record.actuator_entity_id)}')">Entfernen</button>
|
||||
</td>
|
||||
</tr>
|
||||
`).join("")}
|
||||
</table>` : "<p>Keine konfigurierten Aktuatoren.</p>";
|
||||
</table>` : "<p>Noch keine Aktoren ausgewählt.</p>";
|
||||
} catch (error) {
|
||||
box.textContent = error.message;
|
||||
}
|
||||
@@ -192,163 +197,93 @@ async function showActuator(actuatorId) {
|
||||
const box = document.getElementById("actuator-detail");
|
||||
try {
|
||||
const record = await api(`v1/actuators/${encodeURIComponent(actuatorId)}`);
|
||||
const numericRows = record.numeric_candidates.map(candidate => `
|
||||
<tr>
|
||||
<td>${candidate.entity_id}</td>
|
||||
<td>${candidate.score.toFixed(3)} / ${candidate.confidence.toFixed(2)}</td>
|
||||
<td>${candidate.auto_accepted ? "<span class='ok'>auto</span>" : "<span class='warn'>review</span>"}</td>
|
||||
<td>${renderEvidence(candidate.evidence)}</td>
|
||||
</tr>
|
||||
`).join("");
|
||||
const contextRows = record.context_candidates.map(candidate => `
|
||||
<tr>
|
||||
<td>${candidate.entity_id}</td>
|
||||
<td>${candidate.score.toFixed(3)} / ${candidate.confidence.toFixed(2)}</td>
|
||||
<td>${candidate.auto_accepted ? "<span class='ok'>auto</span>" : "<span class='warn'>optional</span>"}</td>
|
||||
<td>${renderEvidence(candidate.evidence)}</td>
|
||||
</tr>
|
||||
`).join("");
|
||||
const contexts = [
|
||||
record.assignment.selected_numeric_entity_id,
|
||||
...record.assignment.selected_context_entity_ids,
|
||||
].filter(Boolean);
|
||||
const evidence = [...record.numeric_candidates, ...record.context_candidates]
|
||||
.filter(candidate => contexts.includes(candidate.entity_id))
|
||||
.map(candidate => `<li><strong>${escapeHtml(candidate.friendly_name || candidate.entity_id)}</strong>: ${candidate.evidence.map(escapeHtml).join(", ") || "statistisch relevanter Kandidat"}</li>`)
|
||||
.join("");
|
||||
const prediction = record.behavior.prediction;
|
||||
const activationButton = record.behavior.mode === "active"
|
||||
? `<button class="danger" onclick="setActivation('${escapeHtml(record.actuator_entity_id)}', false)">Autonomes Schalten stoppen</button>`
|
||||
: record.behavior.status === "trained"
|
||||
? `<button onclick="setActivation('${escapeHtml(record.actuator_entity_id)}', true)">Lernen und Schalten freigeben</button>`
|
||||
: "<p class='muted'>Freigabe wird möglich, sobald genügend Handlungen gelernt wurden.</p>";
|
||||
box.innerHTML = `
|
||||
<div class="grid-two">
|
||||
<div>
|
||||
<h3>Auswahl</h3>
|
||||
<p><strong>Aktuator:</strong> ${record.actuator_entity_id}</p>
|
||||
<p><strong>Numerischer Sensor:</strong> ${record.assignment.selected_numeric_entity_id || "-"}</p>
|
||||
<p><strong>Kontext:</strong> ${record.assignment.selected_context_entity_ids.join(", ") || "-"}</p>
|
||||
<p><strong>Quelle:</strong> ${record.assignment.source}</p>
|
||||
<p><strong>Review:</strong> <span class="${record.assignment.review_required ? "warn" : "ok"}">${record.assignment.review_required ? "erforderlich" : "nicht erforderlich"}</span></p>
|
||||
<p><strong>Begruendung:</strong> ${record.assignment.reason}</p>
|
||||
<h3>${escapeHtml(record.actuator_entity_id)}</h3>
|
||||
<p><strong>Status:</strong> <span class="${statusClass(record)}">${escapeHtml(lifecycleLabel(record))}</span></p>
|
||||
<p><strong>Zuordnung:</strong> automatisch</p>
|
||||
<p><strong>Sicherheit:</strong> ${Math.round(record.assignment.confidence * 100)} %</p>
|
||||
<p><strong>Bewertung:</strong> ${escapeHtml(record.assignment.reason)}</p>
|
||||
</div>
|
||||
<div>
|
||||
<h3>Modell-Lebenszyklus</h3>
|
||||
<p><strong>Status:</strong> <span class="${statusClass(record)}">${record.lifecycle.status}</span></p>
|
||||
<p><strong>Letztes Training:</strong> ${record.lifecycle.last_trained_at || "-"}</p>
|
||||
<p><strong>Messpunkte:</strong> ${record.lifecycle.last_history_point_count}</p>
|
||||
<p><strong>Grund:</strong> ${record.lifecycle.reason}</p>
|
||||
<p><strong>Nächste Aktion:</strong> ${record.lifecycle.next_action}</p>
|
||||
<button onclick="reconcileActuator('${record.actuator_entity_id}')">Diesen Aktuator erneut prüfen</button>
|
||||
<h3>Verhaltensmodell</h3>
|
||||
<p><strong>Modus:</strong> ${escapeHtml(behaviorLabel(record))}</p>
|
||||
<p><strong>Gelernte Handlungen:</strong> ${record.behavior.sample_count}</p>
|
||||
<p><strong>Davon eindeutig Benutzer:</strong> ${record.behavior.high_confidence_sample_count}</p>
|
||||
<p><strong>Letztes Training:</strong> ${escapeHtml(record.behavior.last_trained_at || "noch nicht")}</p>
|
||||
<p><strong>Status:</strong> ${escapeHtml(record.behavior.reason)}</p>
|
||||
${activationButton}
|
||||
<button class="secondary" onclick="evaluateActuator('${escapeHtml(record.actuator_entity_id)}')">Vorhersage jetzt prüfen</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid-two">
|
||||
<div>
|
||||
<h3>Manuelle Overrides</h3>
|
||||
<label for="override-numeric">Numerischer Sensor</label>
|
||||
<input id="override-numeric" value="${record.manual_override?.numeric_entity_id || record.assignment.selected_numeric_entity_id || ""}">
|
||||
<label for="override-context">Kontext-Entities (kommagetrennt)</label>
|
||||
<textarea id="override-context">${(record.manual_override?.context_entity_ids || record.assignment.selected_context_entity_ids || []).join(", ")}</textarea>
|
||||
<label for="override-note">Notiz</label>
|
||||
<input id="override-note" value="${record.manual_override?.note || ""}">
|
||||
<button onclick="saveOverride('${record.actuator_entity_id}')">Override speichern</button>
|
||||
<button class="secondary" onclick="clearOverride('${record.actuator_entity_id}')">Override löschen</button>
|
||||
</div>
|
||||
<div>
|
||||
<h3>Audit</h3>
|
||||
<pre>${pretty(record.lifecycle.audit)}</pre>
|
||||
</div>
|
||||
</div>
|
||||
<h3>Numerische Kandidaten</h3>
|
||||
${numericRows ? `<table><tr><th>Entity</th><th>Score / Confidence</th><th>Auto</th><th>Evidenz</th></tr>${numericRows}</table>` : "<p>Keine Kandidaten.</p>"}
|
||||
<h3>Kontext-Kandidaten</h3>
|
||||
${contextRows ? `<table><tr><th>Entity</th><th>Score / Confidence</th><th>Auto</th><th>Evidenz</th></tr>${contextRows}</table>` : "<p>Keine Kandidaten.</p>"}
|
||||
<h3>Aktuelle Vorhersage</h3>
|
||||
${prediction
|
||||
? `<p><strong>${escapeHtml(prediction.target_state)}</strong> mit ${Math.round(prediction.confidence * 100)} % Sicherheit. ${escapeHtml(prediction.reason)} ${prediction.executed ? "<span class='ok'>Ausgeführt.</span>" : "<span class='muted'>Nicht ausgeführt.</span>"}</p>`
|
||||
: "<p class='muted'>Aktuell ist kein gelerntes Handlungsmuster fällig.</p>"}
|
||||
<h3>Automatisch verwendeter Kontext</h3>
|
||||
${evidence ? `<ul>${evidence}</ul>` : "<p class='warn'>Noch kein geeigneter Kontext erkannt. SillyHome prüft bei neuen HA-Daten erneut.</p>"}
|
||||
`;
|
||||
} catch (error) {
|
||||
box.textContent = error.message;
|
||||
}
|
||||
}
|
||||
|
||||
async function reconcileActuator(actuatorId) {
|
||||
async function evaluateActuator(actuatorId) {
|
||||
try {
|
||||
await api(`v1/actuators/${encodeURIComponent(actuatorId)}/reconcile`, {method: "POST"});
|
||||
await loadOverview();
|
||||
await api(`v1/actuators/${encodeURIComponent(actuatorId)}/evaluate`, {method: "POST"});
|
||||
await loadConfiguredActuators();
|
||||
await showActuator(actuatorId);
|
||||
} catch (error) {
|
||||
alert(error.message);
|
||||
}
|
||||
}
|
||||
|
||||
async function saveOverride(actuatorId) {
|
||||
const numeric = document.getElementById("override-numeric").value.trim() || null;
|
||||
const contexts = document.getElementById("override-context").value
|
||||
.split(",")
|
||||
.map(item => item.trim())
|
||||
.filter(Boolean);
|
||||
const note = document.getElementById("override-note").value.trim() || null;
|
||||
async function setActivation(actuatorId, active) {
|
||||
const question = active
|
||||
? `${actuatorId} wirklich für autonomes Lernen und Schalten freigeben?`
|
||||
: `${actuatorId} wieder in den Shadow-Modus setzen?`;
|
||||
if (!confirm(question)) return;
|
||||
try {
|
||||
await api(`v1/actuators/${encodeURIComponent(actuatorId)}/override`, {
|
||||
await api(`v1/actuators/${encodeURIComponent(actuatorId)}/activation`, {
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
numeric_entity_id: numeric,
|
||||
context_entity_ids: contexts,
|
||||
note,
|
||||
}),
|
||||
body: JSON.stringify({active}),
|
||||
});
|
||||
await loadOverview();
|
||||
await loadConfiguredActuators();
|
||||
await showActuator(actuatorId);
|
||||
} catch (error) {
|
||||
alert(error.message);
|
||||
}
|
||||
}
|
||||
|
||||
async function clearOverride(actuatorId) {
|
||||
async function removeActuator(actuatorId) {
|
||||
if (!confirm(`${actuatorId} aus SillyHome entfernen?`)) return;
|
||||
try {
|
||||
await api(`v1/actuators/${encodeURIComponent(actuatorId)}/override`, {
|
||||
method: "POST",
|
||||
body: JSON.stringify({clear: true}),
|
||||
});
|
||||
await api(`v1/actuators/${encodeURIComponent(actuatorId)}`, {method: "DELETE"});
|
||||
if (currentActuatorId === actuatorId) {
|
||||
currentActuatorId = null;
|
||||
document.getElementById("actuator-detail").textContent = "Wähle einen Aktor aus der Liste.";
|
||||
}
|
||||
await loadOverview();
|
||||
await showActuator(actuatorId);
|
||||
} catch (error) {
|
||||
alert(error.message);
|
||||
}
|
||||
}
|
||||
|
||||
async function createProposal() {
|
||||
try {
|
||||
await api("v1/automations/proposals", {method: "POST", body: JSON.stringify({
|
||||
alias: document.getElementById("alias").value,
|
||||
description: "Manuell im SillyHome-Dashboard erstellter und nicht automatisch ausgeführter Entwurf.",
|
||||
trigger: {entity_id: document.getElementById("trigger").value, below: Number(document.getElementById("below").value)},
|
||||
action: {service: document.getElementById("service").value, entity_id: document.getElementById("target").value, data: {}}
|
||||
})});
|
||||
await loadProposals();
|
||||
} catch (error) {
|
||||
alert(error.message);
|
||||
}
|
||||
}
|
||||
|
||||
async function decide(id, revision, action) {
|
||||
try {
|
||||
await api(`v1/automations/proposals/${id}/${action}`, {method: "POST", body: JSON.stringify({expected_revision: revision})});
|
||||
await loadProposals();
|
||||
} catch (error) {
|
||||
alert(error.message);
|
||||
}
|
||||
}
|
||||
|
||||
async function loadProposals() {
|
||||
const box = document.getElementById("proposals");
|
||||
try {
|
||||
const rows = await api("v1/automations/proposals");
|
||||
box.innerHTML = rows.length ? `
|
||||
<table>
|
||||
<tr><th>Name</th><th>Status</th><th>Aktion</th></tr>
|
||||
${rows.map(item => `
|
||||
<tr>
|
||||
<td>${item.alias}</td>
|
||||
<td>${item.status}</td>
|
||||
<td>${item.status === "draft"
|
||||
? `<button onclick="decide('${item.proposal_id}',${item.revision},'approve')">Freigeben</button><button class="secondary" onclick="decide('${item.proposal_id}',${item.revision},'reject')">Ablehnen</button>`
|
||||
: item.status === "approved"
|
||||
? `<a href="v1/automations/proposals/${item.proposal_id}/yaml">YAML laden</a>`
|
||||
: "-"}</td>
|
||||
</tr>
|
||||
`).join("")}
|
||||
</table>` : "<p>Keine Entwürfe.</p>";
|
||||
} catch (error) {
|
||||
box.textContent = error.message;
|
||||
}
|
||||
}
|
||||
|
||||
loadOverview();
|
||||
</script>
|
||||
</body>
|
||||
|
||||
Reference in New Issue
Block a user