Sammelstand der offenen Arbeit im Dashboard. Einstellungen: eine Eingabemaske fuer die Energiepreise unter restricted/settings.php, dazu costs.php als Modell und ajax/settings.php als Endpunkt. Die Preise stehen jetzt in gridCosts, gasCosts und fuelCosts statt fest im Quelltext von getStats.php; die Jahresstatistik rechnet ueber LEAD() den jeweils gueltigen Zeitraum aus und schlaegt den anteiligen Grundpreis auf. Eingegeben wird in gewohnten Einheiten - l/100 km, kWh/100 km, Euro je Liter -, umgerechnet wird beim Speichern. solarLog_costs.sql beschreibt die Umstellung der Tabellen. Skoda: eigene Seite mit Live-Werten ueber MQTT und Historie ueber ajax/skoda.php, Kommandos ueber ajax/skodaCmd.php. Karte: Leaflet mit einem eigenen Kachel-Zwischenspeicher (ajax/tile.php), dessen Ablage unter tiles/ nicht ins Repository gehoert. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
142 lines
6.0 KiB
PHP
142 lines
6.0 KiB
PHP
<?php
|
|
/*
|
|
* Aggregierte Energiedaten für die Historie-Seite.
|
|
* Ersetzt getConsData_{month,year,decade}.php und getProdData_{month,year,decade}.php.
|
|
*
|
|
* Aufruf: energyHistory.php?series=prod|cons&range=month|year|decade
|
|
*
|
|
* Zwei Quellen, je nach Zeitraum:
|
|
*
|
|
* month EnergyFlow, also die Rohdaten. Nach Tagen gruppiert, und der
|
|
* heutige Balken soll bis zur letzten Messung reichen. Das
|
|
* Stundenarchiv wird nur einmal nachts fortgeschrieben und wäre
|
|
* für den laufenden Tag noch leer.
|
|
*
|
|
* year EnergyFlow_hourly, das Stundenarchiv. Die Rohdaten werden nach
|
|
* decade zwölf Monaten ausgedünnt (solarlog-rollup.sh thin), diese
|
|
* Diagramme fänden dort also nichts mehr. Bei Gruppierung nach
|
|
* Monat bzw. Jahr fällt der fehlende laufende Tag nicht auf.
|
|
*
|
|
* Beide Quellen liefern Wattstunden, weil die Diagramme auf der y-Achse durch
|
|
* 1.000.000 teilen und MWh beschriften. Die Rohdaten sind Momentanleistungen
|
|
* alle fünf Minuten, daher /12; im Archiv stehen bereits kWh, daher *1000.
|
|
* Gegengerechnet über einen kompletten Monat stimmen beide auf 0,003 % überein
|
|
* - der Rest ist die /12-Näherung gegenüber der echten Zeitgewichtung.
|
|
*
|
|
* pv_fehlbetrag_kwh wird zur erfassten Erzeugung addiert. Wenn Wechselrichter
|
|
* die Verbindung zur OpenDTU verlieren, fehlt ihr Beitrag in pvP bzw. pv_kwh -
|
|
* die Einspeisung, die Batterieladung und der Heizstab sind aber vollständig
|
|
* gemessen. Ohne die Korrektur wird der Direktverbrauch als Differenz negativ
|
|
* und der Balken bleibt leer, etwa am 03.08.2026 mit -0,5 statt +15,9 kWh.
|
|
* Im Rohdaten-Zweig kommt der Betrag über eine nach Tagen vorverdichtete
|
|
* abgeleitete Tabelle dazu, sonst zählte ihn jede der zwölf Zeilen je Stunde
|
|
* erneut; für den laufenden Tag gibt es ihn noch nicht, dann bleibt er 0.
|
|
*
|
|
* Die Einspeisung kommt in beiden Fällen aus gridPfeed, dem Zählerregister,
|
|
* nicht aus dem Saldo gridP. Innerhalb eines Fensters kommen Bezug und
|
|
* Einspeisung gleichzeitig vor; der Saldo löscht kurze Bezüge weg und passt
|
|
* dann nicht mehr zu dem, was abgerechnet wird.
|
|
*/
|
|
|
|
require_once("../helper.php");
|
|
require_once("./chartData.php");
|
|
|
|
// Zeitraum -> [Format der Beschriftung, Quelle]
|
|
$ranges = [
|
|
"month" => ["'%d.%b.'", "roh"],
|
|
"year" => ["'%b. %y'", "archiv"],
|
|
"decade" => ["'%Y'", "archiv"],
|
|
];
|
|
|
|
// Filter auf die Zeitspalte. ZEIT wird durch den Spaltennamen der Quelle ersetzt.
|
|
$filters = [
|
|
"month" => "DATE_FORMAT(ZEIT,'%Y%m%d') >= DATE_FORMAT(DATE_SUB(now(),INTERVAL 1 MONTH),'%Y%m%d')",
|
|
"year" => "DATE_FORMAT(ZEIT,'%Y%m') >= DATE_FORMAT(DATE_SUB(now(),INTERVAL 13 MONTH),'%Y%m')",
|
|
"decade" => "DATE_FORMAT(ZEIT,'%Y') >= DATE_FORMAT(DATE_SUB(now(),INTERVAL 10 YEAR),'%Y')",
|
|
];
|
|
|
|
// Datenreihen je Diagramm. Beide teilen sich Solarertrag und Heizstab und
|
|
// unterscheiden sich nur darin, wohin die Energie fließt (prod) bzw. woher
|
|
// sie kommt (cons).
|
|
$quellen = [
|
|
"roh" => [
|
|
"tabelle" => "EnergyFlow",
|
|
"zeit" => "datetime",
|
|
"join" => "LEFT JOIN (SELECT DATE(stunde) AS tag, SUM(pv_fehlbetrag_kwh) AS fehlt
|
|
FROM EnergyFlow_hourly GROUP BY 1) f ON f.tag = DATE(datetime)",
|
|
"reihen" => [
|
|
"prod" => "SUM(pvP/12 - gridPfeed/12 - IF(battP < 0, -battP/12,0) - heaterPwr/12)
|
|
+ IFNULL(MAX(f.fehlt),0)*1000 AS Direktverbrauch,
|
|
SUM(heaterPwr/12) AS Heizstab,
|
|
SUM(IF(battP < 0, -battP/12,0)) AS Batterieladung,
|
|
SUM(gridPfeed/12) AS Einspeisung",
|
|
"cons" => "SUM(pvP/12 - gridPfeed/12 - IF(battP < 0, -battP/12,0) - heaterPwr/12)
|
|
+ IFNULL(MAX(f.fehlt),0)*1000 AS Solarverbrauch,
|
|
SUM(heaterPwr/12) AS Heizstab,
|
|
SUM(IF(battP > 0, battP/12,0)) AS Batteriebezug,
|
|
SUM(gridPcons/12) AS Netzbezug",
|
|
],
|
|
],
|
|
"archiv" => [
|
|
"tabelle" => "EnergyFlow_hourly",
|
|
"zeit" => "stunde",
|
|
"join" => "",
|
|
"reihen" => [
|
|
"prod" => "SUM((pv_kwh + IFNULL(pv_fehlbetrag_kwh,0) - netz_einsp_kwh - batt_laden_kwh - heizstab_kwh)*1000) AS Direktverbrauch,
|
|
SUM(heizstab_kwh*1000) AS Heizstab,
|
|
SUM(batt_laden_kwh*1000) AS Batterieladung,
|
|
SUM(netz_einsp_kwh*1000) AS Einspeisung",
|
|
"cons" => "SUM((pv_kwh + IFNULL(pv_fehlbetrag_kwh,0) - netz_einsp_kwh - batt_laden_kwh - heizstab_kwh)*1000) AS Solarverbrauch,
|
|
SUM(heizstab_kwh*1000) AS Heizstab,
|
|
SUM(batt_entladen_kwh*1000) AS Batteriebezug,
|
|
SUM(netz_bezug_kwh*1000) AS Netzbezug",
|
|
],
|
|
],
|
|
];
|
|
|
|
$linecolors["Direktverbrauch"] = "#FFFF00";
|
|
$linecolors["Solarverbrauch"] = "#FFFF00";
|
|
$linecolors["UG"] = "#FFaa00";
|
|
$linecolors["OG"] = "#FF4400";
|
|
$linecolors["Auto UG"] = "#00aaFF";
|
|
$linecolors["Auto OG"] = "#0044FF";
|
|
$linecolors["Heizstab"] = "#FF6600";
|
|
$linecolors["Batterieladung"] = "#00aa00";
|
|
$linecolors["Batteriebezug"] = "#00aa00";
|
|
$linecolors["Einspeisung"] = "#FF0000";
|
|
$linecolors["Netzbezug"] = "#FF0000";
|
|
$linecolors["Ladestand"] = "#00aa00";
|
|
$linecolors["Vorhersage"] = "#2222FF";
|
|
|
|
header('Content-Type: application/json; charset=utf-8');
|
|
|
|
$rangeKey = isset($_GET["range"]) ? $_GET["range"] : "";
|
|
$seriesKey = isset($_GET["series"]) ? $_GET["series"] : "";
|
|
if (!isset($ranges[$rangeKey]) || !in_array($seriesKey, ["prod", "cons"], true)) {
|
|
http_response_code(400);
|
|
echo json_encode(["error" => "Unbekannter Zeitraum oder Datenreihe."]);
|
|
exit;
|
|
}
|
|
list($labelFormat, $quelleKey) = $ranges[$rangeKey];
|
|
$quelle = $quellen[$quelleKey];
|
|
$zeit = $quelle["zeit"];
|
|
$filter = str_replace("ZEIT", $zeit, $filters[$rangeKey]);
|
|
|
|
$obj = (object)[];
|
|
$obj->labels = [];
|
|
$obj->datasets = [];
|
|
|
|
if (checkLogin()) {
|
|
$mysql = new mysqli($mysql_server, $mysql_solarUser, $mysql_solarPass, $mysql_solarDB);
|
|
$query = "SELECT
|
|
DATE_FORMAT($zeit, $labelFormat) AS 'time',
|
|
" . $quelle["reihen"][$seriesKey] . "
|
|
FROM " . $quelle["tabelle"] . " " . $quelle["join"] . "
|
|
WHERE $filter
|
|
GROUP BY DATE_FORMAT($zeit, $labelFormat)
|
|
ORDER BY MIN($zeit) ; ";
|
|
$obj = buildChartData($mysql, $query, $linecolors);
|
|
}
|
|
|
|
echo json_encode($obj);
|