feat: add manual context assignment and fix actuator discovery
This commit is contained in:
@@ -26,6 +26,7 @@
|
||||
.bad { color: #ff8f8f; }
|
||||
label { display: block; margin: 9px 0 4px; color: #b9c9d6; }
|
||||
select,input,button { box-sizing: border-box; width: 100%; border-radius: 10px; border: 1px solid #3b4b5b; padding: 12px; background: #101820; color: #fff; font:inherit; }
|
||||
select[multiple] { min-height:190px; }
|
||||
button { min-height:44px; margin-top: 10px; background: #23715b; border: 0; font-weight: 700; cursor: pointer; }
|
||||
button.secondary { background: #37495c; }
|
||||
button.danger { background: #7b3434; }
|
||||
@@ -49,6 +50,7 @@
|
||||
.actions { display:flex; flex-wrap:wrap; gap:8px; margin-top:10px; }
|
||||
.actions button { flex:1 1 180px; margin-top:0; }
|
||||
.detail-header { display:flex; justify-content:space-between; gap:12px; align-items:flex-start; flex-wrap:wrap; }
|
||||
.manual-context { margin-top:14px; background:#111a23; border:1px solid #31404d; border-radius:14px; padding:14px; }
|
||||
code { color:#cfe0ec; overflow-wrap:anywhere; }
|
||||
@media (max-width: 760px) {
|
||||
header { padding:18px 14px; }
|
||||
@@ -121,6 +123,10 @@
|
||||
<label for="actuator-input">Entitätsname oder Gerät aus Home Assistant</label>
|
||||
<input id="actuator-input" list="actuator-options" placeholder="z. B. light.licht_abstellraum" autocomplete="off">
|
||||
<datalist id="actuator-options"></datalist>
|
||||
<label for="actuator-select">Oder aus Liste wählen</label>
|
||||
<select id="actuator-select" onchange="selectActuatorFromList()">
|
||||
<option value="">Geräteliste wird geladen ...</option>
|
||||
</select>
|
||||
<button onclick="configureActuator()">Gerät hinzufügen und Beobachtung starten</button>
|
||||
<p id="actuator-config-result" class="muted">Noch kein Aktor ausgewählt.</p>
|
||||
</section>
|
||||
@@ -145,6 +151,8 @@ const escapeHtml = value => String(value ?? "")
|
||||
.replaceAll('"', """)
|
||||
.replaceAll("'", "'");
|
||||
let currentActuatorId = null;
|
||||
let actuatorChoices = [];
|
||||
let contextOptions = [];
|
||||
|
||||
async function api(path, options = {}) {
|
||||
const response = await fetch(path, {headers: {"Content-Type": "application/json"}, ...options});
|
||||
@@ -209,28 +217,52 @@ async function loadOverview() {
|
||||
status.innerHTML = `<p class="bad">${escapeHtml(error.message)}</p>`;
|
||||
chips.innerHTML = "";
|
||||
}
|
||||
await Promise.all([loadActuatorDiscovery(), loadConfiguredActuators()]);
|
||||
await Promise.all([loadActuatorDiscovery(), loadContextOptions(), loadConfiguredActuators()]);
|
||||
}
|
||||
|
||||
async function loadActuatorDiscovery() {
|
||||
const options = document.getElementById("actuator-options");
|
||||
const select = document.getElementById("actuator-select");
|
||||
try {
|
||||
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));
|
||||
options.innerHTML = choices.map(entity =>
|
||||
actuatorChoices = available.filter(entity => !configuredIds.has(entity.entity_id));
|
||||
options.innerHTML = actuatorChoices.map(entity =>
|
||||
`<option value="${escapeHtml(entity.entity_id)}">${escapeHtml(entity.friendly_name || entity.entity_id)}${entity.area_name ? ` (${escapeHtml(entity.area_name)})` : ""}</option>`
|
||||
).join("");
|
||||
select.innerHTML = [
|
||||
`<option value="">Gerät auswählen ...</option>`,
|
||||
...actuatorChoices.map(entity =>
|
||||
`<option value="${escapeHtml(entity.entity_id)}">${escapeHtml(entity.area_name || "Ohne Bereich")} - ${escapeHtml(entity.friendly_name || entity.entity_id)} (${escapeHtml(entity.entity_id)})</option>`
|
||||
),
|
||||
].join("");
|
||||
} catch (error) {
|
||||
options.innerHTML = "";
|
||||
select.innerHTML = `<option value="">Geräteliste konnte nicht geladen werden</option>`;
|
||||
}
|
||||
}
|
||||
|
||||
async function loadContextOptions() {
|
||||
try {
|
||||
contextOptions = await api("v1/actuators/context-options");
|
||||
} catch (error) {
|
||||
contextOptions = [];
|
||||
}
|
||||
}
|
||||
|
||||
function selectActuatorFromList() {
|
||||
const value = document.getElementById("actuator-select").value;
|
||||
if (value) document.getElementById("actuator-input").value = value;
|
||||
}
|
||||
|
||||
async function configureActuator() {
|
||||
const actuatorId = document.getElementById("actuator-input").value.trim();
|
||||
const actuatorId = (
|
||||
document.getElementById("actuator-input").value.trim()
|
||||
|| document.getElementById("actuator-select").value.trim()
|
||||
);
|
||||
const result = document.getElementById("actuator-config-result");
|
||||
if (!actuatorId) return;
|
||||
result.textContent = "Kontext wird automatisch analysiert ...";
|
||||
@@ -309,6 +341,37 @@ async function showActuator(actuatorId, evaluationMessage = "") {
|
||||
pattern => pattern.source === "automation",
|
||||
).length;
|
||||
const relatedAutomations = record.behavior.related_automations || [];
|
||||
const manualContextIds = new Set(record.assignment.selected_context_entity_ids || []);
|
||||
const numericOptions = contextOptions.filter(entity => entity.domain === "sensor");
|
||||
const manualAssignment = `
|
||||
<div class="manual-context">
|
||||
<h3>Kontext selbst festlegen</h3>
|
||||
<p class="muted">Hier kannst du Sensoren und Zustände ergänzen, die deiner Meinung nach wichtig für den Schaltvorgang sind. Beispiele: PIR, Helligkeit außen, Luftfeuchtigkeit innen/außen oder ein Lichtzustand.</p>
|
||||
<label for="manual-numeric-select">Optionaler Haupt-Messsensor</label>
|
||||
<select id="manual-numeric-select">
|
||||
<option value="">Keinen numerischen Hauptsensor verwenden</option>
|
||||
${numericOptions.map(entity => `
|
||||
<option value="${escapeHtml(entity.entity_id)}" ${record.assignment.selected_numeric_entity_id === entity.entity_id ? "selected" : ""}>
|
||||
${escapeHtml(entity.area_name || "Ohne Bereich")} - ${escapeHtml(entity.friendly_name || entity.entity_id)} (${escapeHtml(entity.entity_id)})
|
||||
</option>
|
||||
`).join("")}
|
||||
</select>
|
||||
<label for="manual-context-select">Zusätzliche Kontext-Entities</label>
|
||||
<select id="manual-context-select" multiple>
|
||||
${contextOptions
|
||||
.filter(entity => entity.entity_id !== record.actuator_entity_id)
|
||||
.map(entity => `
|
||||
<option value="${escapeHtml(entity.entity_id)}" ${manualContextIds.has(entity.entity_id) ? "selected" : ""}>
|
||||
${escapeHtml(entity.area_name || "Ohne Bereich")} - ${escapeHtml(entity.friendly_name || entity.entity_id)} (${escapeHtml(entity.entity_id)})
|
||||
</option>
|
||||
`).join("")}
|
||||
</select>
|
||||
<div class="actions">
|
||||
<button onclick="saveManualAssignment('${escapeHtml(record.actuator_entity_id)}')">Diese Kontext-Auswahl speichern</button>
|
||||
<button class="secondary" onclick="loadContextOptions().then(() => showActuator('${escapeHtml(record.actuator_entity_id)}'))">Listen neu laden</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
const activationButton = record.behavior.mode === "active"
|
||||
? `<button class="danger" onclick="setActivation('${escapeHtml(record.actuator_entity_id)}', false, false, true)">SillyHome stoppen und pausierte HA-Automationen fortsetzen</button>
|
||||
<button class="secondary" onclick="setActivation('${escapeHtml(record.actuator_entity_id)}', false, false, false)">SillyHome stoppen; HA-Automationen pausiert lassen</button>`
|
||||
@@ -366,6 +429,7 @@ async function showActuator(actuatorId, evaluationMessage = "") {
|
||||
${automationControls}
|
||||
<h3>Welche Zusammenhänge automatisch verwendet werden</h3>
|
||||
${evidence ? `<ul>${evidence}</ul>` : "<p class='warn'>Noch kein geeigneter Kontext erkannt. SillyHome prüft bei neuen HA-Daten erneut.</p>"}
|
||||
${manualAssignment}
|
||||
`;
|
||||
document.getElementById("detail").scrollIntoView({behavior: "smooth", block: "start"});
|
||||
} catch (error) {
|
||||
@@ -373,6 +437,27 @@ async function showActuator(actuatorId, evaluationMessage = "") {
|
||||
}
|
||||
}
|
||||
|
||||
async function saveManualAssignment(actuatorId) {
|
||||
const numericEntityId = document.getElementById("manual-numeric-select").value || null;
|
||||
const contextEntityIds = Array.from(
|
||||
document.getElementById("manual-context-select").selectedOptions,
|
||||
).map(option => option.value);
|
||||
try {
|
||||
await api(`v1/actuators/${encodeURIComponent(actuatorId)}/assignment`, {
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
numeric_entity_id: numericEntityId,
|
||||
context_entity_ids: contextEntityIds,
|
||||
note: "Manuell im Dashboard gesetzt",
|
||||
}),
|
||||
});
|
||||
await loadConfiguredActuators();
|
||||
await showActuator(actuatorId, "Manuelle Kontext-Auswahl gespeichert.");
|
||||
} catch (error) {
|
||||
alert(error.message);
|
||||
}
|
||||
}
|
||||
|
||||
async function evaluateActuator(actuatorId) {
|
||||
try {
|
||||
const record = await api(
|
||||
|
||||
Reference in New Issue
Block a user