|
|
|
@@ -281,6 +281,7 @@ let discoveryLoadPromise = null;
|
|
|
|
let overviewLoadPromise = null;
|
|
|
|
let overviewLoadPromise = null;
|
|
|
|
let systemLoadPromise = null;
|
|
|
|
let systemLoadPromise = null;
|
|
|
|
let currentSensorWeightGroups = [];
|
|
|
|
let currentSensorWeightGroups = [];
|
|
|
|
|
|
|
|
let latestSimulationResults = new Map();
|
|
|
|
let visibleActuatorLimit = 24;
|
|
|
|
let visibleActuatorLimit = 24;
|
|
|
|
const ACTUATOR_RESULT_LIMIT = 50;
|
|
|
|
const ACTUATOR_RESULT_LIMIT = 50;
|
|
|
|
const STATUS_TIMEOUT_MS = 2000;
|
|
|
|
const STATUS_TIMEOUT_MS = 2000;
|
|
|
|
@@ -1255,7 +1256,7 @@ async function showActuator(actuatorId, evaluationMessage = "") {
|
|
|
|
const simulationControls = weightedCandidates.length ? `
|
|
|
|
const simulationControls = weightedCandidates.length ? `
|
|
|
|
<details class="manual-context" open>
|
|
|
|
<details class="manual-context" open>
|
|
|
|
<summary>Aktor-Simulation</summary>
|
|
|
|
<summary>Aktor-Simulation</summary>
|
|
|
|
<p class="muted">Teste Sensorzustände und Gewichtungen, ohne Home Assistant zu schalten.</p>
|
|
|
|
<p class="muted">Teste Sensorzustände und Gewichtungen, ohne Home Assistant zu schalten. Danach kannst du die beste Gewichtung übernehmen oder direkt in den Dry-run wechseln.</p>
|
|
|
|
<div class="card-list">
|
|
|
|
<div class="card-list">
|
|
|
|
${weightedCandidates.map(candidate => {
|
|
|
|
${weightedCandidates.map(candidate => {
|
|
|
|
const effective = Math.round((candidate.effective_weight ?? 1) * 100);
|
|
|
|
const effective = Math.round((candidate.effective_weight ?? 1) * 100);
|
|
|
|
@@ -1677,6 +1678,7 @@ async function simulateActuator(actuatorId) {
|
|
|
|
max_results: 6,
|
|
|
|
max_results: 6,
|
|
|
|
}),
|
|
|
|
}),
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
latestSimulationResults.set(actuatorId, results);
|
|
|
|
box.innerHTML = results.length ? results.map((result, index) => {
|
|
|
|
box.innerHTML = results.length ? results.map((result, index) => {
|
|
|
|
const prediction = result.prediction;
|
|
|
|
const prediction = result.prediction;
|
|
|
|
const factors = result.decision_factors || [];
|
|
|
|
const factors = result.decision_factors || [];
|
|
|
|
@@ -1691,6 +1693,10 @@ async function simulateActuator(actuatorId) {
|
|
|
|
<p class="muted">Gewichtung: ${Object.entries(result.sensor_weights || {}).map(([entity, weight]) => `${escapeHtml(entity)}=${Math.round(weight * 100)} %`).join(", ") || "Standard"}</p>
|
|
|
|
<p class="muted">Gewichtung: ${Object.entries(result.sensor_weights || {}).map(([entity, weight]) => `${escapeHtml(entity)}=${Math.round(weight * 100)} %`).join(", ") || "Standard"}</p>
|
|
|
|
${result.blockers?.length ? `<p class="warn">${result.blockers.map(escapeHtml).join(" ")}</p>` : "<p class='ok'>Würde nach Sicherheitsprüfung schalten.</p>"}
|
|
|
|
${result.blockers?.length ? `<p class="warn">${result.blockers.map(escapeHtml).join(" ")}</p>` : "<p class='ok'>Würde nach Sicherheitsprüfung schalten.</p>"}
|
|
|
|
${factors.length ? `<ul>${factors.slice(0, 4).map(factor => `<li>${escapeHtml(factor.label)}: ${Math.round((factor.contribution || 0) * 100)} % Beitrag</li>`).join("")}</ul>` : ""}
|
|
|
|
${factors.length ? `<ul>${factors.slice(0, 4).map(factor => `<li>${escapeHtml(factor.label)}: ${Math.round((factor.contribution || 0) * 100)} % Beitrag</li>`).join("")}</ul>` : ""}
|
|
|
|
|
|
|
|
<div class="actions">
|
|
|
|
|
|
|
|
<button class="secondary compact" onclick="applySimulationWeights('${escapeHtml(actuatorId)}', '${escapeHtml(result.scenario_id)}', false)">Gewichtung übernehmen</button>
|
|
|
|
|
|
|
|
<button class="compact" onclick="applySimulationWeights('${escapeHtml(actuatorId)}', '${escapeHtml(result.scenario_id)}', true)">Übernehmen + Dry-run starten</button>
|
|
|
|
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
`;
|
|
|
|
`;
|
|
|
|
}).join("") : "<p class='muted'>Keine Simulationsergebnisse.</p>";
|
|
|
|
}).join("") : "<p class='muted'>Keine Simulationsergebnisse.</p>";
|
|
|
|
@@ -1699,6 +1705,41 @@ async function simulateActuator(actuatorId) {
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async function applySimulationWeights(actuatorId, scenarioId, startDryRun) {
|
|
|
|
|
|
|
|
const result = (latestSimulationResults.get(actuatorId) || [])
|
|
|
|
|
|
|
|
.find(item => item.scenario_id === scenarioId);
|
|
|
|
|
|
|
|
if (!result) {
|
|
|
|
|
|
|
|
alert("Simulationsergebnis ist nicht mehr verfügbar. Bitte neu simulieren.");
|
|
|
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
|
|
await api(`v1/actuators/${encodeURIComponent(actuatorId)}/weights`, {
|
|
|
|
|
|
|
|
method: "POST",
|
|
|
|
|
|
|
|
body: JSON.stringify({
|
|
|
|
|
|
|
|
sensor_weights: result.sensor_weights || {},
|
|
|
|
|
|
|
|
sensor_weight_groups: currentSensorWeightGroups,
|
|
|
|
|
|
|
|
note: `Aus Simulation ${scenarioId} übernommen`,
|
|
|
|
|
|
|
|
}),
|
|
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
if (startDryRun) {
|
|
|
|
|
|
|
|
await api(`v1/actuators/${encodeURIComponent(actuatorId)}/dry-run`, {
|
|
|
|
|
|
|
|
method: "POST",
|
|
|
|
|
|
|
|
body: JSON.stringify({enabled: true}),
|
|
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
invalidateDashboardCache();
|
|
|
|
|
|
|
|
await loadConfiguredActuators();
|
|
|
|
|
|
|
|
await showActuator(
|
|
|
|
|
|
|
|
actuatorId,
|
|
|
|
|
|
|
|
startDryRun
|
|
|
|
|
|
|
|
? "Simulation übernommen und Dry-run gestartet."
|
|
|
|
|
|
|
|
: "Simulation übernommen.",
|
|
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
|
|
alert(error.message);
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function saveManualAssignment(actuatorId) {
|
|
|
|
async function saveManualAssignment(actuatorId) {
|
|
|
|
const numericEntityId = document.getElementById("manual-numeric-select").value || null;
|
|
|
|
const numericEntityId = document.getElementById("manual-numeric-select").value || null;
|
|
|
|
const selectedContextIds = Array.from(
|
|
|
|
const selectedContextIds = Array.from(
|
|
|
|
|