feat(workflow): publish verified forecast controls and consolidated docs
Tests / test (push) Successful in 1m1s

Approved by Daniel Haefliger for develop and beta. Author dh_Agent, authenticated account dh. Preserve published battery, charging and overall Energy Pie changes. No deployment or plant control authorization.
This commit is contained in:
dh
2026-10-08 10:02:58 +00:00
parent af69151165
commit 74a2d6c685
82 changed files with 5054 additions and 617 deletions
@@ -845,20 +845,14 @@
schedule: true
});
if (result.status === "license_required") {
byId("forecastApiStatus").textContent = "Keine aktive Prognoselizenz";
return setForecastState("Für dieses System ist eine aktive Prognoselizenz erforderlich.");
}
if (result.status === "not_connected") {
byId("forecastApiStatus").textContent = "Manager noch nicht verbunden";
return setForecastState("Verbinde den Manager zuerst mit dem Lizenzcode dieses Systems.");
}
if (result.status === "no_data") {
byId("forecastApiStatus").textContent = "Verbindung bereit, noch keine Daten";
return setForecastState("Für dieses System liegen im gewählten Zeitfenster noch keine Prognosedaten vor.");
return setForecastState("Für dieses System liegen im gewählten Zeitfenster noch keine historischen Daten vor.");
}
byId("forecastApiStatus").textContent = result.configured
? "Prognose aktiv"
: "Daten empfangen, Konfiguration offen";
renderForecastChart(result.series || []);
}
@@ -1167,13 +1161,7 @@
form.elements.tarif_peak_fest.value = forecastNumber(configuration.tarif_peak_fest, 5);
renderTariffSelector(form, "import", configuration.tarif_bezug_modus, configuration.tarif_bezug, configuration.tarif_bezug_fest, true);
renderTariffSelector(form, "export", configuration.tarif_einspeisung_modus, configuration.tarif_einspeisung, configuration.tarif_einspeisung_fest, true);
Object.entries(forecastCapabilityVariants).forEach(function (entry) {
var capability = entry[0];
var variants = entry[1];
form.elements[capability].checked = variants.some(function (variant) {
return Boolean(configuration["prog_var_" + variant]);
});
});
form.dataset.forecastVariants = JSON.stringify(forecastGeneratorFlags(configuration));
var managerOwned = result.technicalSource === "manager" && Boolean(result.topology);
renderForecastRoofs(configuration.daecher || [], managerOwned);
renderForecastTopology(result.topology || null);
@@ -1192,59 +1180,47 @@
control.disabled = true;
});
}
var licensed = result.forecastCapabilities || {
pv: true,
load: true,
schedule: true
};
[
["forecast_pv", "pv"],
["forecast_load", "load"],
["grid_schedule", "schedule"]
].forEach(function (entry) {
if (!licensed[entry[1]]) {
form.elements[entry[0]].checked = false;
form.elements[entry[0]].disabled = true;
}
});
var licensed = result.forecastCapabilities;
byId("forecastLicensedScope").textContent = licensed ? ([
licensed.pv ? "PV-Ertragsprognose" : "", licensed.load ? "Hausprognose" : "", licensed.schedule ? "Netzfahrplan" : ""
].filter(Boolean).join(" · ") || "Keine Prognosefunktion lizenziert") : "Lizenzumfang nicht bestätigt";
byId("forecastConfigStatus").textContent = managerOwned
? "Technik vom Manager"
: (result.configured ? "Gespeichert" : "Noch nicht gespeichert");
byId("forecastConfigStatus").className = "status " + (result.configured ? "available" : "review");
form.classList.remove("hidden");
byId("forecastConfigSection").classList.remove("hidden");
}
async function loadForecastConfiguration(plant, requestId) {
var form = byId("forecastConfigForm");
form.classList.add("hidden");
form.dataset.plantId = "";
try {
var result = await api("/api/plants/" + plant.id + "/prognosis/configuration");
if (requestId !== state.forecastRequest) return;
fillForecastConfiguration(plant, result);
} catch (error) {
if (requestId !== state.forecastRequest) return;
byId("forecastConfigStatus").textContent = "Nicht verfügbar";
byId("forecastConfigError").textContent = error.message;
byId("forecastConfigSection").classList.remove("hidden");
}
}
function forecastConfigurationPayload(form) {
var selected = {
forecast_pv: form.elements.forecast_pv.checked,
forecast_load: form.elements.forecast_load.checked,
grid_schedule: form.elements.grid_schedule.checked
};
if (selected.grid_schedule) {
selected.forecast_pv = true;
selected.forecast_load = true;
}
if (!Object.values(selected).some(Boolean)) {
throw new Error("Bitte mindestens eine Prognosefunktion aktivieren.");
}
function forecastGeneratorFlags(configuration) {
var variants = {};
Object.entries(forecastCapabilityVariants).forEach(function (entry) {
entry[1].forEach(function (variant) {
variants["prog_var_" + variant] = selected[entry[0]];
var key = "prog_var_" + variant;
if (Object.prototype.hasOwnProperty.call(configuration, key)) variants[key] = configuration[key];
});
});
return variants;
}
function forecastConfigurationPayload(form) {
var variants = forecastGeneratorFlags(JSON.parse(form.dataset.forecastVariants || "{}"));
var roofs = Array.from(byId("forecastRoofs").querySelectorAll(".forecast-roof-row")).map(function (row) {
return {
kwp: Number(row.querySelector("[data-roof-field='kwp']").value),
@@ -1271,13 +1247,15 @@
}
var integratedPlanner = null;
var forecastHistoryRequest = 0;
var forecastHistoryKey = null;
async function loadIntegratedPlanner(plant, requestId) {
var host = byId("forecastPlanner");
if (!host || !plant.installation_id) return;
host.textContent = "Aktueller Netzfahrplan wird geladen.";
try {
var plannerModule = await import("/netplan-v4.js?v=20261006-integrated");
var plannerModule = await import("/netplan-v4.js?v=20261008-workflow");
if (requestId !== state.forecastRequest || !byId("view-forecast").classList.contains("active")) return;
integratedPlanner = plannerModule.mountPlanner(host, { plantId: plant.id, session: state.session });
await integratedPlanner.ready;
@@ -1292,6 +1270,8 @@
if (byId("forecastPlanner")) byId("forecastPlanner").replaceChildren();
var plant = selectedPlant("forecastPlant");
var requestId = ++state.forecastRequest;
++forecastHistoryRequest;
forecastHistoryKey = null;
if (!plant || state.forecastPlantId !== plant.id) {
state.forecastPlantId = plant ? plant.id : null;
state.forecastResult = null;
@@ -1300,27 +1280,51 @@
state.forecastRangeEnd = null;
}
resetForecastChart();
if (!plant) return setForecastState("Kein System verfügbar.");
if (!plant) {
byId("forecastPlanner").textContent = "Kein System verfügbar.";
return;
}
byId("forecastVariant").textContent = managerLabel(plant.manager_variant);
byId("forecastConnection").textContent = plant.installation_id ? "Verbunden mit " + plant.installation_id : "Noch nicht verknüpft";
renderForecastLicenseWarning(plant);
if (!byId("view-forecast").classList.contains("active")) return;
byId("forecastApiStatus").textContent = plant.installation_id
? "Betriebsstatus aus der Manager-Rückmeldung"
: "Manager-Verknüpfung erforderlich";
if (plant.installation_id) void loadIntegratedPlanner(plant, requestId);
else byId("forecastPlanner").textContent = "Verbinde den Manager zuerst mit dem Lizenzcode dieses Systems.";
void loadForecastConfiguration(plant, requestId);
void loadForecastHistory();
}
void loadIntegratedPlanner(plant, requestId);
function historicalForecastResult(result, now) {
var keys = ["PV", "Hausverbrauch", "Netzleistung", "SOC", "prog_var_1", "prog_var_2", "prog_var_10", "prog_var_11", "prog_var_21", "prog_var_22"];
return Object.assign({}, result, {
scheduleSourceModel: null,
series: (result.series || []).filter(function (series) { return keys.includes(series.key); }).map(function (series) {
return Object.assign({}, series, { points: (series.points || []).filter(function (point) {
var stamp = Date.parse(point.time);
return Number.isFinite(stamp) && stamp <= now;
}) });
})
});
}
setForecastState("Prognosedaten werden geladen.");
byId("forecastApiStatus").textContent = "Datenverbindung wird geprüft";
async function loadForecastHistory() {
var plant = selectedPlant("forecastPlant"), requestId = state.forecastRequest;
if (!plant || !byId("view-forecast").classList.contains("active") || !byId("forecastHistory").open) return;
var key = plant.id + ":" + state.forecastHistoryDays;
if (forecastHistoryKey === key) return;
var historyId = ++forecastHistoryRequest;
setForecastState("Historische Messwerte und Modellvergleiche werden geladen.");
try {
var result = await api("/api/plants/" + plant.id + "/prognosis?historyDays=" + encodeURIComponent(state.forecastHistoryDays));
if (requestId !== state.forecastRequest) return;
renderForecastResult(result);
if (["available", "no_data"].includes(result.status)) {
await loadForecastConfiguration(plant, requestId);
}
if (requestId !== state.forecastRequest || historyId !== forecastHistoryRequest) return;
forecastHistoryKey = key;
renderForecastResult(historicalForecastResult(result, Date.now()));
} catch (error) {
if (requestId !== state.forecastRequest) return;
byId("forecastApiStatus").textContent = "Prognosedienst nicht erreichbar";
if (requestId !== state.forecastRequest || historyId !== forecastHistoryRequest) return;
setForecastState(error.message);
}
}
@@ -2167,12 +2171,13 @@
byId("peakUpgrade").addEventListener("change", updateOrder);
byId("orderPlant").addEventListener("change", function () { prepareOrderEditor(); });
byId("forecastPlant").addEventListener("change", updateForecast);
byId("forecastHistory").addEventListener("toggle", loadForecastHistory);
byId("forecastHistoryDays").addEventListener("change", function (event) {
state.forecastHistoryDays = Number(event.target.value) || 1;
state.forecastRangeStart = null;
state.forecastRangeEnd = null;
state.forecastVisibleSeries = null;
updateForecast();
loadForecastHistory();
});
byId("forecastSeriesControls").addEventListener("change", function (event) {
var control = event.target.closest("[data-forecast-series]");