Files
Smart-Dashboard/ajax/energyHistory.php
T
adminandClaude Opus 5 d285ec27a0 Einstellungsseite, Skoda-Seite und Karte
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>
2026-09-02 21:21:27 +02:00

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);