"Nicht angemeldet."]); exit; } /* ------------------------------------------------------------- Kennzahlen -- * * Die Anzeige-Beschreibung. Reihenfolge ist Anzeigereihenfolge. * * einheit wie sie an der Zeile steht * dez Nachkommastellen. Jahressummen brauchen keine Cent. * invers weniger ist besser (Verbrauch, Kosten) * punkte Prozentwert; Veraenderungen sind Prozentpunkte, nicht Prozent * summe Zeile, unter der sich Teilbetraege aufreihen * stufe/teil eingerueckt: "davon" teilt die Summe darueber auf * teilmenge "darin": steckt bereits in der genannten Zeile und darf nicht * dazuaddiert werden. Der Wert ist deren id, damit die Anzeige * sagen kann, WORIN es steckt - die blosse Reihenfolge liesse * offen, ob die vorangehende oder die uebergeordnete Zeile * gemeint ist. * klapp Zeile mit Teilzeilen, zunaechst zusammengeklappt * unter gehoert in die Klappgruppe dieser Zeile. Das ist etwas * anderes als teilmenge: unter sagt, WOMIT eine Zeile * auf- und zugeht, teilmenge, WORIN ihr Wert steckt. * braucht Spalte in stats_daily, ohne die es die Kennzahl nicht gibt */ const KENNZAHLEN = [ ["gruppe" => "Strom vom Netz"], ["id" => "bezug", "name" => "Netzbezug", "einheit" => "kWh", "dez" => 0, "invers" => true], ["id" => "kosten", "name" => "Netzkosten (mit Grundpreis)", "einheit" => "€", "dez" => 0, "invers" => true], ["id" => "preis", "name" => "Effektiver Strompreis", "einheit" => "ct/kWh", "dez" => 1, "invers" => true], ["gruppe" => "Was die Anlage einbringt"], ["id" => "ertrag", "name" => "Ertrag der Anlage", "einheit" => "€", "dez" => 0, "summe" => true, "klapp" => true], ["id" => "eigen", "name" => "Eigenverbrauch", "einheit" => "€", "dez" => 0, "teil" => true, "unter" => "ertrag"], ["id" => "batterie", "name" => "Beitrag der Batterie", "einheit" => "€", "dez" => 0, "teilmenge" => "eigen", "unter" => "ertrag"], ["id" => "solarauto", "name" => "Solarladung des Autos", "einheit" => "€", "dez" => 0, "teilmenge" => "eigen", "unter" => "ertrag"], ["id" => "einsp", "name" => "Einspeisung", "einheit" => "€", "dez" => 0, "teil" => true, "unter" => "ertrag"], ["id" => "heizung", "name" => "Heizung über den Heizstab", "einheit" => "€", "dez" => 0, "teil" => true, "unter" => "ertrag"], ["id" => "autarkie", "name" => "Autarkie", "einheit" => "%", "dez" => 1, "punkte" => true], ["id" => "quote", "name" => "Eigenverbrauchsquote", "einheit" => "%", "dez" => 1, "punkte" => true, "braucht" => "pv_kwh"], ["gruppe" => "Auto"], ["id" => "geladen", "name" => "Geladen", "einheit" => "kWh", "dez" => 0, "summe" => true, "klapp" => true], ["id" => "ladenEG", "name" => "Wallbox EG", "einheit" => "kWh", "dez" => 0, "teil" => true, "unter" => "geladen"], ["id" => "ladenOG", "name" => "Wallbox OG", "einheit" => "kWh", "dez" => 0, "teil" => true, "unter" => "geladen"], ["id" => "solaranteil","name" => "Solaranteil der Ladung", "einheit" => "%", "dez" => 1, "punkte" => true], ["id" => "benzin", "name" => "Benzin gespart", "einheit" => "L", "dez" => 0], ["id" => "benzingeld", "name" => "Ersparnis gegenüber Benzin", "einheit" => "€", "dez" => 0], ]; /* Die drei Kennzahlen, die oben gross stehen. */ const LEITZAHLEN = ["ertrag", "kosten", "autarkie"]; /* ------------------------------------------------------------------ Summen -- * * Alle Grundgroessen in einem Ausdruck. Was hier steht, ist addierbar - genau * das ist die Bedingung dafuer, dass Monat, Jahr, Zeitraum und Gesamtsumme * aus derselben Abfrage entstehen koennen. */ function summenSQL($hatPv) { return " SUM(d.netz_bezug_kwh) AS bezug_kwh, SUM(d.netz_bezug_kwh * preis.cost) AS bezug_eur, SUM(d.netz_einsp_kwh) AS einsp_kwh, SUM(d.netz_einsp_kwh * preis.gain) AS einsp_eur, SUM((d.eigenverbrauch_kwh + IFNULL(d.pv_fehlbetrag_kwh,0)) * preis.cost) AS eigen_eur, " . ($hatPv ? "SUM(d.pv_kwh + IFNULL(d.pv_fehlbetrag_kwh,0))" : "NULL") . " AS pv_kwh, SUM(d.heizstab_solar_kwh * gas.cost / NULLIF(gas.kwhPerLitre,0)) AS heiz_eur, SUM(d.batt_entladen_kwh * preis.cost - d.batt_laden_kwh * preis.gain) AS batt_eur, SUM(d.autarkie_avg * d.stunden_erfasst) AS autarkie_gew, SUM(d.stunden_erfasst) AS stunden, SUM(d.auto_eg_kwh) AS eg_kwh, SUM(d.auto_og_kwh) AS og_kwh, SUM(d.auto_eg_solar_kwh) AS eg_solar_kwh, SUM(d.auto_og_solar_kwh) AS og_solar_kwh, SUM(d.auto_eg_solar_kwh * preis.cost) AS eg_solar_eur, SUM(d.auto_og_solar_kwh * preis.cost) AS og_solar_eur, SUM((d.auto_eg_kwh + d.auto_og_kwh) * sprit.lPer100km / NULLIF(sprit.kwhPer100km,0)) AS benzin_l, SUM((d.auto_eg_kwh + d.auto_og_kwh) * sprit.lPer100km / NULLIF(sprit.kwhPer100km,0) * sprit.cost) AS benzin_eur"; } /** Der gemeinsame Rumpf: Tageswerte, an jedem Tag der damals gueltige Tarif. */ function vonTarifen() { return " FROM stats_daily d JOIN preis ON d.tag >= preis.active_date AND (preis.folgt IS NULL OR d.tag < preis.folgt) LEFT JOIN gas ON d.tag >= gas.active_date AND (gas.folgt IS NULL OR d.tag < gas.folgt) LEFT JOIN sprit ON d.tag >= sprit.active_date AND (sprit.folgt IS NULL OR d.tag < sprit.folgt)"; } function mitTarifen() { return "WITH preis AS (SELECT active_date, cost, gain, LEAD(active_date) OVER (ORDER BY active_date) AS folgt FROM gridCosts), gas AS (SELECT active_date, cost, kwhPerLitre, LEAD(active_date) OVER (ORDER BY active_date) AS folgt FROM gasCosts), sprit AS (SELECT active_date, cost, lPer100km, kwhPer100km, LEAD(active_date) OVER (ORDER BY active_date) AS folgt FROM fuelCosts) SELECT "; } /* --------------------------------------------------------------- Rechnung -- * * Aus Summen werden Kennzahlen. Diese Funktion ist die einzige Stelle, an der * eine Kennzahl definiert ist - sie laeuft fuer jeden Monat, jedes Jahr, jeden * Zeitraum und die Gesamtsumme mit denselben Regeln. * * null heisst "nicht bestimmbar" und nicht "null": ohne Bezug gibt es keinen * effektiven Preis, ohne Ladung keinen Solaranteil. Die Anzeige zeigt darauf * einen Strich statt einer 0. */ function kennzahlenAus($s, $grundpreis) { $teile = function ($zaehler, $nenner, $faktor = 1) { return ($nenner === null || $nenner <= 0 || $zaehler === null) ? null : $zaehler / $nenner * $faktor; }; $bezug = floatval($s["bezug_kwh"]); $kosten = floatval($s["bezug_eur"]) + $grundpreis; $geladen = floatval($s["eg_kwh"]) + floatval($s["og_kwh"]); $solar = floatval($s["eg_solar_kwh"]) + floatval($s["og_solar_kwh"]); return [ "bezug" => $bezug, "kosten" => $kosten, "preis" => $teile($kosten, $bezug, 100), "ertrag" => floatval($s["eigen_eur"]) + floatval($s["einsp_eur"]) + floatval($s["heiz_eur"]), "eigen" => floatval($s["eigen_eur"]), "einsp" => floatval($s["einsp_eur"]), "heizung" => floatval($s["heiz_eur"]), "batterie" => floatval($s["batt_eur"]), "solarauto" => floatval($s["eg_solar_eur"]) + floatval($s["og_solar_eur"]), "autarkie" => $teile($s["autarkie_gew"], $s["stunden"]), // Eigenverbrauchsquote: der Anteil der Erzeugung, der im Haus blieb. "quote" => $s["pv_kwh"] === null ? null : $teile(floatval($s["pv_kwh"]) - floatval($s["einsp_kwh"]), floatval($s["pv_kwh"]), 100), "geladen" => $geladen, "ladenEG" => floatval($s["eg_kwh"]), "ladenOG" => floatval($s["og_kwh"]), "solaranteil" => $teile($solar, $geladen, 100), "benzin" => floatval($s["benzin_l"]), "benzingeld" => floatval($s["benzin_eur"]), ]; } /** Zwei Summensaetze addieren - fuer Jahre aus Monaten und die Gesamtsumme. */ function summenPlus($a, $b) { if ($a === null) return $b; foreach ($b as $k => $v) { if ($v === null) continue; $a[$k] = ($a[$k] === null ? 0 : $a[$k]) + $v; } return $a; } /* ------------------------------------------------------------------ Abruf --*/ $db = solarDb(); // Gibt es die Erzeugung als Tageswert? Ohne sie entfaellt die // Eigenverbrauchsquote - lieber eine Kennzahl weniger als eine Abfrage, die // an einer fehlenden Spalte scheitert. $hatPv = false; if ($res = $db->query("SHOW COLUMNS FROM stats_daily LIKE 'pv_kwh'")) { $hatPv = $res->num_rows > 0; } $heute = new DateTimeImmutable("today"); /* * Monatswerte. Daraus entstehen die Verlaufsbalken, durch Addition die ganzen * Jahre und durch nochmalige Addition die Gesamtsumme - eine Abfrage fuer * drei Ebenen. */ $monate = []; $sql = mitTarifen() . "YEAR(d.tag) AS jahr, MONTH(d.tag) AS monat, " . summenSQL($hatPv) . vonTarifen() . " GROUP BY 1, 2 ORDER BY 1, 2"; $res = $db->query($sql); if (!$res) { http_response_code(500); echo json_encode(["error" => "Statistik nicht abrufbar: " . $db->error]); exit; } while ($z = $res->fetch_assoc()) { $jahr = intval($z["jahr"]); $monat = intval($z["monat"]); unset($z["jahr"], $z["monat"]); // ab hier sind nur noch Summen drin $monate[$jahr][$monat] = $z; } /* * Gleicher Zeitraum: jedes Jahr vom 1. Januar bis zum heutigen Kalendertag. * Verglichen wird nach Monat und Tag statt nach DAYOFYEAR - der 7. September * ist in einem Schaltjahr der 251., sonst der 250. Tag, und ein Vergleich * ueber die Tagesnummer waere in Schaltjahren um einen Tag verschoben. */ $gleichSummen = []; $sql = mitTarifen() . "YEAR(d.tag) AS jahr, " . summenSQL($hatPv) . vonTarifen() . " WHERE (MONTH(d.tag) < MONTH(CURDATE()) OR (MONTH(d.tag) = MONTH(CURDATE()) AND DAY(d.tag) <= DAY(CURDATE()))) GROUP BY 1 ORDER BY 1"; $res = $db->query($sql); while ($res && $z = $res->fetch_assoc()) { $jahr = intval($z["jahr"]); unset($z["jahr"]); $gleichSummen[$jahr] = $z; } $jahre = array_keys($monate); sort($jahre); /* ------------------------------------------------------- Antwort aufbauen --*/ $antwort = [ "stichtag" => $heute->format("Y-m-d"), "jahre" => array_values($jahre), "kennzahlen" => array_values(array_filter(KENNZAHLEN, function ($k) use ($hatPv) { return !isset($k["braucht"]) || ($k["braucht"] === "pv_kwh" && $hatPv); })), "leitzahlen" => LEITZAHLEN, "voll" => [], "gleich" => [], "monate" => [], ]; $gesamtSummen = null; $gesamtGrund = 0.0; foreach ($jahre as $jahr) { $jahresSummen = null; $grundJahr = grundpreisImJahr($jahr); // Monatswerte. Der Grundpreis des Jahres verteilt sich nach Tagen auf die // Monate - fuer den Verlauf genau genug, und eine Abfrage je Monat waere // hundert Abfragen fuer eine Balkenhoehe. $tageImJahr = intval((new DateTimeImmutable($jahr . "-12-31"))->format("z")) + 1; for ($m = 1; $m <= 12; $m++) { if (!isset($monate[$jahr][$m])) continue; $tageImMonat = intval((new DateTimeImmutable(sprintf("%d-%02d-01", $jahr, $m)))->format("t")); $werte = kennzahlenAus($monate[$jahr][$m], $grundJahr * $tageImMonat / $tageImJahr); foreach ($werte as $id => $wert) { $antwort["monate"][$jahr][$id][$m - 1] = $wert; } $jahresSummen = summenPlus($jahresSummen, $monate[$jahr][$m]); } // Monate ohne Daten bleiben als Luecke stehen, damit der Verlauf zwoelf // Plaetze hat und der Januar links liegt. foreach (array_keys($antwort["monate"][$jahr] ?? []) as $id) { for ($m = 0; $m < 12; $m++) { if (!array_key_exists($m, $antwort["monate"][$jahr][$id])) { $antwort["monate"][$jahr][$id][$m] = null; } } ksort($antwort["monate"][$jahr][$id]); $antwort["monate"][$jahr][$id] = array_values($antwort["monate"][$jahr][$id]); } if ($jahresSummen !== null) { $antwort["voll"][$jahr] = kennzahlenAus($jahresSummen, $grundJahr); $gesamtSummen = summenPlus($gesamtSummen, $jahresSummen); $gesamtGrund += $grundJahr; } if (isset($gleichSummen[$jahr])) { /* * Der Stichtag im jeweiligen Jahr. Der Tag wird auf die Laenge des * Monats begrenzt: am 29. Februar gibt es in den anderen Jahren * keinen 29. Februar, und ein ungueltiges Datum rutschte sonst * stillschweigend in den Maerz. */ $tag = min(intval($heute->format("d")), intval((new DateTimeImmutable($jahr . "-" . $heute->format("m") . "-01"))->format("t"))); $bis = sprintf("%d-%s-%02d", $jahr, $heute->format("m"), $tag); $antwort["gleich"][$jahr] = kennzahlenAus( $gleichSummen[$jahr], grundpreisImZeitraum($jahr . "-01-01", $bis)); } } // Die Summe ueber alles - dieselbe Rechnung, nur ueber alle Jahre. $antwort["gesamt"] = $gesamtSummen === null ? null : kennzahlenAus($gesamtSummen, $gesamtGrund); echo json_encode($antwort, JSON_UNESCAPED_UNICODE);