From cbefff8e4b4c1e3abaf9848050f7a9542fa5f506 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20H=C3=A4fliger?= Date: Thu, 6 Nov 2025 10:40:51 +0100 Subject: [PATCH] no message --- Abrechnung/module.php | 206 +++++++++++++++++++++++++++++++++++++----- 1 file changed, 185 insertions(+), 21 deletions(-) diff --git a/Abrechnung/module.php b/Abrechnung/module.php index 3a85ea9..0e9556c 100644 --- a/Abrechnung/module.php +++ b/Abrechnung/module.php @@ -141,42 +141,172 @@ class Abrechnung extends IPSModule private function CalculatePowerCosts($powerMeters, $tariffs, $userId, $from, $to) { + // Tabelle Kopf $html = " - - + + + + + + + + + + + + "; - $total = 0.0; - $usedTariffs = []; + // Sammler je Zähler + $acc = []; // name => arrays + // Index Zähler für User + $meters = []; foreach ($powerMeters as $m) { if ($m['user_id'] != $userId) continue; - $cost = $this->AddMeterToPDFRow($m, $tariffs, $from, $to, 'Strombezug'); - $html .= $cost['row']; - $total += $cost['value']; - $usedTariffs = array_merge($usedTariffs, $cost['tariffs']); + $name = $m['name']; + $meters[$name] = [ + 'name' => $name, + 'imp' => $m['var_import'] ?? null, + 'exp' => $m['var_export'] ?? null + ]; + $acc[$name] = [ + 'imp'=>0.0,'exp'=>0.0, + 'solar_bezug'=>0.0,'netz_bezug'=>0.0, + 'pv_einspeisung'=>0.0,'pv_verkauf'=>0.0, + 'cost_solar'=>0.0,'cost_grid'=>0.0, + 'rev_feedin'=>0.0,'rev_solar_sale'=>0.0 + ]; } + if (empty($meters)) { + $html .= "
ZählerTypStartEndeZähler StartZähler EndeVerbrauchTarif (Rp)Kosten (CHF)ZählerImport (kWh)Export (kWh)Solarbezug (kWh)Netzbezug (kWh)PV-Einspeisung (kWh)PV-Verkauf (kWh)Kosten Solar (CHF)Kosten Netz (CHF)Ertrag Einspeise (CHF)Ertrag Verkauf (CHF)Summe (CHF)
Keine Stromzähler für diesen Benutzer

"; + return ['html'=>$html, 'sum'=>0.0]; + } + + // 15-Minuten-Schritte + for ($ts = $from; $ts < $to; $ts += 900) { + $slotEnd = min($to, $ts + 900); + + // Deltas pro Zähler & Summen + $impTotal = 0.0; + $expTotal = 0.0; + $slot = []; // name => ['imp'=>x,'exp'=>y] + + foreach ($meters as $name => $mm) { + $impDelta = 0.0; + $expDelta = 0.0; + + if (!empty($mm['imp']) && IPS_VariableExists((int)$mm['imp'])) { + $impDelta = $this->readDelta((int)$mm['imp'], $ts, $slotEnd); + } + if (!empty($mm['exp']) && IPS_VariableExists((int)$mm['exp'])) { + $expDelta = $this->readDelta((int)$mm['exp'], $ts, $slotEnd); + } + + // kWh annehmen (oder deine Einheit – hier keine Umrechnung) + $impDelta = max(0.0, $impDelta); + $expDelta = max(0.0, $expDelta); + + $slot[$name] = ['imp'=>$impDelta, 'exp'=>$expDelta]; + $impTotal += $impDelta; + $expTotal += $expDelta; + } + + // Tarife zum Slot-Start + $pGrid = $this->getTariffPriceAt($tariffs, ['netztarif','netzstrom','strombezug','netz'], $ts); // Rp/kWh + $pSolar = $this->getTariffPriceAt($tariffs, ['solar','solarstom','pv','eigenverbrauch'], $ts); // Rp/kWh + $pFeed = $this->getTariffPriceAt($tariffs, ['einspeisung','feedin','einspeisetarif'], $ts); // Rp/kWh + $pSolarSell = $this->getTariffPriceAt($tariffs, ['solarverkauf','pv-verkauf'], $ts); // Rp/kWh + if ($pSolarSell === null) $pSolarSell = $pSolar; // falls kein eigener Solarverkaufstarif + + // Falls alles 0 – Slot überspringen + if ($impTotal <= 0.0 && $expTotal <= 0.0) continue; + + // Case A: impTotal <= expTotal + if ($impTotal <= $expTotal) { + $ratio = ($expTotal > 0.0) ? ($impTotal / $expTotal) : 0.0; + + foreach ($meters as $name => $_) { + $imp = $slot[$name]['imp']; + $exp = $slot[$name]['exp']; + + // Energiemengen + $acc[$name]['imp'] += $imp; + $acc[$name]['exp'] += $exp; + + $acc[$name]['solar_bezug'] += $imp; // kompletter Import deckt sich aus PV (bis exp reicht) + $acc[$name]['netz_bezug'] += 0.0; + $acc[$name]['pv_verkauf'] += $ratio * $exp; // Anteil Export, der im Haus bleibt (verkauft als Solar) + $acc[$name]['pv_einspeisung'] += max(0.0, (1.0 - $ratio) * $exp); // Überschuss ins Netz + + // Kosten / Erträge (Rp → CHF) + $acc[$name]['cost_grid'] += 0.0; + if ($pSolarSell !== null) $acc[$name]['cost_solar'] += ($imp * $pSolarSell) / 100.0; + if ($pSolar !== null) $acc[$name]['rev_feedin'] += ($ratio * $exp * $pSolar) / 100.0; + if ($pFeed !== null) $acc[$name]['rev_solar_sale'] += (max(0.0, (1.0 - $ratio) * $exp) * $pFeed) / 100.0; + } + } + // Case B: impTotal > expTotal + else { + $ratio = ($impTotal > 0.0) ? ($expTotal / $impTotal) : 0.0; + + foreach ($meters as $name => $_) { + $imp = $slot[$name]['imp']; + $exp = $slot[$name]['exp']; + + // Energiemengen + $acc[$name]['imp'] += $imp; + $acc[$name]['exp'] += $exp; + + $acc[$name]['solar_bezug'] += $ratio * $imp; // Teil des Imports aus PV + $acc[$name]['netz_bezug'] += max(0.0, (1.0 - $ratio) * $imp); + $acc[$name]['pv_verkauf'] += $exp; // gesamter Export wird intern verkauft + // keine Einspeisung in diesem Zweig lt. Vorgabe + + // Kosten / Erträge + if ($pGrid !== null) $acc[$name]['cost_grid'] += (max(0.0, (1.0 - $ratio) * $imp) * $pGrid) / 100.0; + if ($pSolar !== null) $acc[$name]['cost_solar'] += (($ratio * $imp) * $pSolar) / 100.0; + if ($pSolarSell !== null) $acc[$name]['rev_feedin'] += ($exp * $pSolarSell) / 100.0; // „Solareinspeiseertrag“ laut Vorgabe + // Solarverkauf-Ertrag bleibt hier 0 (lt. Spezifikation) + } + } + } + + // Ausgabe-Zeilen je Zähler + $grand = 0.0; + foreach ($acc as $name => $v) { + $sum = $v['cost_solar'] + $v['cost_grid'] - $v['rev_feedin'] - $v['rev_solar_sale']; + $grand += $sum; + + $html .= " + {$name} + " . number_format($v['imp'], 3) . " + " . number_format($v['exp'], 3) . " + " . number_format($v['solar_bezug'], 3) . " + " . number_format($v['netz_bezug'], 3) . " + " . number_format($v['pv_einspeisung'], 3) . " + " . number_format($v['pv_verkauf'], 3) . " + " . number_format($v['cost_solar'], 2) . " + " . number_format($v['cost_grid'], 2) . " + " . number_format($v['rev_feedin'], 2) . " + " . number_format($v['rev_solar_sale'], 2) . " + " . number_format($sum, 2) . " + "; + } + + // Total $html .= " - Total Stromkosten: - " . number_format($total, 2) . " + Total Stromkosten: + " . number_format($grand, 2) . "
"; - // 👇 Tarifliste anhängen - if (!empty($usedTariffs)) { - $html .= "

Angewendete Stromtarife:


"; - } - - return ['html' => $html, 'sum' => $total]; + return ['html'=>$html, 'sum'=>$grand]; } + // ====================== Nebenkosten ====================== private function CalculateAdditionalCosts($waterMeters, $tariffs, $userId, $from, $to) @@ -339,4 +469,38 @@ private function AddMeterToPDFRow($meter, $tariffs, $from, $to, $type) } return null; } + + private function readDelta($varId, $tStart, $tEnd) +{ + // Start: erster Wert NACH/AB tStart + $vStart = $this->GetValueAt($varId, $tStart, true); + // Ende: letzter Wert VOR/ BIS tEnd + $vEnd = $this->GetValueAt($varId, $tEnd, false); + + if ($vStart === null || $vEnd === null) return 0.0; + return max(0.0, floatval($vEnd) - floatval($vStart)); +} + +private function getTariffPriceAt($tariffs, $typeSynonyms, $ts) +{ + // passender unit_type + $wanted = array_map('strtolower', $typeSynonyms); + $cands = []; + foreach ($tariffs as $t) { + $u = strtolower(trim($t['unit_type'] ?? '')); + if (!in_array($u, $wanted)) continue; + + $s = $this->toUnixTs($t['start'], false); + $e = $this->toUnixTs($t['end'], true); + if (!$s || !$e) continue; + if ($s <= $ts && $ts <= $e) { + $cands[] = floatval($t['price']); + } + } + if (empty($cands)) return null; + // bei Überschneidungen: ersten / günstigsten / letzten – hier: letzten Eintrag priorisieren + return end($cands); +} + + }