diff --git a/Abrechnung/module.php b/Abrechnung/module.php index 8b8d989..7dedaeb 100644 --- a/Abrechnung/module.php +++ b/Abrechnung/module.php @@ -81,7 +81,13 @@ class Abrechnung extends IPSModule $water = json_decode($this->ReadPropertyString('WaterMeters'), true); $tariffs = json_decode($this->ReadPropertyString('Tariffs'), true); - if (!class_exists('TCPDF')) return false; + if (!class_exists('TCPDF')) { + IPS_LogMessage('Abrechnung', 'TCPDF nicht gefunden!'); + return false; + } + + // 🔥 NEU: Stromkosten EINMAL für alle User berechnen + $this->CalculateAllPowerCosts($power, $tariffs, $from, $to); $pdf = new TCPDF('P', 'mm', 'A4', true, 'UTF-8', false); $pdf->SetCreator('IPSymcon Abrechnung'); @@ -96,6 +102,7 @@ class Abrechnung extends IPSModule return $pdf->Output('Abrechnung.pdf', 'S'); } + private function BuildUserInvoice($pdf, $user, $power, $water, $tariffs, $from, $to) { $pdf->AddPage(); @@ -105,12 +112,13 @@ class Abrechnung extends IPSModule

{$user['address']}
{$user['city']}

Zeitraum: " . date('d.m.Y', $from) . " – " . date('d.m.Y', $to) . "


"; - // ⚡ Stromkosten + // ⚡ Stromkosten (aus globaler Berechnung) $html .= "

⚡ Stromkosten

"; - $powerResult = $this->CalculatePowerCosts($power, $tariffs, $user['id'], $from, $to); + $powerResult = $this->GetCalculatedPowerCosts($user['id']); $html .= $powerResult['html']; $totalPower = $powerResult['sum']; + // đź’§ Nebenkosten $html .= "

💧 Nebenkosten (Wasser/Wärme)

"; $additionalResult = $this->CalculateAdditionalCosts($water, $tariffs, $user['id'], $from, $to); @@ -123,13 +131,147 @@ class Abrechnung extends IPSModule $pdf->writeHTML($html, true, false, true, false, ''); } - // ====================== Nebenkosten ====================== -private function CalculatePowerCosts($powerMeters, $tariffs, $userId, $from, $to) +private function CalculateAllPowerCosts($powerMeters, $tariffs, $from, $to) { - IPS_LogMessage("ABR", "====================== START CalculatePowerCosts ======================"); - IPS_LogMessage("ABR", "UserId: $userId | From: $from (" . date("d.m.Y H:i", $from) . ") | To: $to (" . date("d.m.Y H:i", $to) . ")"); + IPS_LogMessage('Abrechnung', "CalculateAllPowerCosts: from=" . date('d.m.Y H:i', $from) . " to=" . date('d.m.Y H:i', $to)); - // Tabellenkopf + $this->powerCostCache = []; // Cache leeren + + // 1. Zähler je User aufbauen + $metersByUser = []; + foreach ($powerMeters as $m) { + if (!isset($m['user_id'])) { + continue; + } + $userId = (int)$m['user_id']; + $name = $m['name'] ?? ('Meter_' . $m['user_id']); + + if (!isset($metersByUser[$userId])) { + $metersByUser[$userId] = []; + } + $metersByUser[$userId][$name] = [ + 'importVar' => $m['var_import'] ?? null, + 'exportVar' => $m['var_export'] ?? null + ]; + + // Akkus initialisieren + if (!isset($this->powerCostCache[$userId])) { + $this->powerCostCache[$userId] = []; + } + $this->powerCostCache[$userId][$name] = [ + 'name' => $name, + 'imp' => 0.0, + 'exp' => 0.0, + 'solar_bezug' => 0.0, + 'netz_bezug' => 0.0, + 'solareinspeisung' => 0.0, + 'solarverkauf' => 0.0, + 'cost_solar' => 0.0, + 'cost_grid' => 0.0, + 'rev_feedin' => 0.0 + ]; + } + + if (empty($metersByUser)) { + IPS_LogMessage('Abrechnung', 'CalculateAllPowerCosts: keine Stromzähler definiert.'); + return; + } + + // 2. 15-Minuten-Schritte + for ($ts = $from; $ts < $to; $ts += 900) { + $slotEnd = min($to, $ts + 900); + + // Tarife (gleich für alle User) + $pGrid = $this->getTariffPriceAt($tariffs, ['Netztarif'], $ts); + $pSolar = $this->getTariffPriceAt($tariffs, ['Solartarif'], $ts); + $pFeed = $this->getTariffPriceAt($tariffs, ['Einspeisetarif'], $ts); + + foreach ($metersByUser as $userId => $meters) { + + $impTotal = 0.0; + $expTotal = 0.0; + $slot = []; + + // 2a. Deltas je Zähler des Users + foreach ($meters as $name => $mm) { + $impDelta = 0.0; + $expDelta = 0.0; + + if (!empty($mm['importVar']) && IPS_VariableExists((int)$mm['importVar'])) { + $impDelta = $this->getDeltaFromArchive((int)$mm['importVar'], $ts, $slotEnd); + } + if (!empty($mm['exportVar']) && IPS_VariableExists((int)$mm['exportVar'])) { + $expDelta = $this->getDeltaFromArchive((int)$mm['exportVar'], $ts, $slotEnd); + } + + if ($impDelta > 0.0 || $expDelta > 0.0) { + $slot[$name] = ['imp' => $impDelta, 'exp' => $expDelta]; + $impTotal += $impDelta; + $expTotal += $expDelta; + } + } + + if ($impTotal == 0.0 && $expTotal == 0.0) { + continue; // für diesen User in diesem Intervall nichts passiert + } + + // 2b. Verhältnis PV / Netz pro User + if ($impTotal <= $expTotal && $expTotal > 0.0) { + $ratio = $impTotal / $expTotal; // PV deckt alles + $pvCoversAll = true; + } elseif ($impTotal > 0.0) { + $ratio = $expTotal / $impTotal; // Netzzusatz nötig + $pvCoversAll = false; + } else { + $ratio = 0.0; + $pvCoversAll = true; + } + + // 2c. Werte pro Zähler verteilen + foreach ($slot as $name => $v) { + $imp = $v['imp']; + $exp = $v['exp']; + + $this->powerCostCache[$userId][$name]['imp'] += $imp; + $this->powerCostCache[$userId][$name]['exp'] += $exp; + + if ($pvCoversAll) { + // PV deckt gesamten Verbrauch + $this->powerCostCache[$userId][$name]['solar_bezug'] += $imp; + $this->powerCostCache[$userId][$name]['solareinspeisung'] += (1 - $ratio) * $exp; + $this->powerCostCache[$userId][$name]['solarverkauf'] += $ratio * $exp; + + if ($pSolar !== null) { + $this->powerCostCache[$userId][$name]['cost_solar'] += ($imp * $pSolar) / 100.0; + } + if ($pFeed !== null) { + $this->powerCostCache[$userId][$name]['rev_feedin'] += ((1 - $ratio) * $exp * $pFeed) / 100.0; + } + } else { + // Teil Netzbezug + $this->powerCostCache[$userId][$name]['solar_bezug'] += $ratio * $imp; + $this->powerCostCache[$userId][$name]['netz_bezug'] += (1 - $ratio) * $imp; + $this->powerCostCache[$userId][$name]['solarverkauf'] += $exp; + + if ($pGrid !== null) { + $this->powerCostCache[$userId][$name]['cost_grid'] += ((1 - $ratio) * $imp * $pGrid) / 100.0; + } + if ($pSolar !== null) { + $this->powerCostCache[$userId][$name]['cost_solar'] += ($ratio * $imp * $pSolar) / 100.0; + } + if ($pFeed !== null) { + $this->powerCostCache[$userId][$name]['rev_feedin'] += ($exp * $pFeed) / 100.0; + } + } + } + } + } + + IPS_LogMessage('Abrechnung', 'CalculateAllPowerCosts: Fertig.'); +} + +private function GetCalculatedPowerCosts($userId) +{ $html = " @@ -145,124 +287,19 @@ private function CalculatePowerCosts($powerMeters, $tariffs, $userId, $from, $to "; - $meters = []; - $acc = []; - - // === Stromzähler laden === - foreach ($powerMeters as $m) { - if ($m['user_id'] != $userId) continue; - - $name = $m['name']; - $meters[$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, - 'solareinspeisung' => 0.0, - 'solarverkauf' => 0.0, - 'cost_solar' => 0.0, - 'cost_grid' => 0.0, - 'rev_feedin' => 0.0 - ]; + if (empty($this->powerCostCache) || !isset($this->powerCostCache[$userId])) { + $html .= "
ZählerSumme (CHF)
Keine Stromzähler für diesen Benutzer

"; + return ['html' => $html, 'sum' => 0.0]; } - if (empty($meters)) { - $html .= "Keine Stromzähler für diesen Benutzer
"; - return ['html' => $html, 'sum' => 0]; - } - - // === 15-Minuten-Raster === - for ($ts = $from; $ts < $to; $ts += 900) { - - $slotEnd = min($to, $ts + 900); - - // Tarife bestimmen - $pGrid = $this->getTariffPriceAt($tariffs, ['Netztarif'], $ts); - $pSolar = $this->getTariffPriceAt($tariffs, ['Solartarif'], $ts); - $pFeed = $this->getTariffPriceAt($tariffs, ['Einspeisetarif'], $ts); - - $impTotal = 0.0; - $expTotal = 0.0; - $slot = []; - - // === Für jeden Zähler Import/Export lesen === - foreach ($meters as $name => $mm) { - - // Start-/Endwert Import - $impStart = $this->GetValueAt((int)$mm['imp'], $ts, false); - $impEnd = $this->GetValueAt((int)$mm['imp'], $slotEnd, true); - - // Start-/Endwert Export - $expStart = $this->GetValueAt((int)$mm['exp'], $ts, false); - $expEnd = $this->GetValueAt((int)$mm['exp'], $slotEnd, true); - - // Delta berechnen - $impDelta = ($impStart !== null && $impEnd !== null) ? max(0, $impEnd - $impStart) : 0; - $expDelta = ($expStart !== null && $expEnd !== null) ? max(0, $expEnd - $expStart) : 0; - - if ($impDelta > 0 || $expDelta > 0) { - $slot[$name] = ['imp' => $impDelta, 'exp' => $expDelta]; - $impTotal += $impDelta; - $expTotal += $expDelta; - } - } - - if ($impTotal == 0 && $expTotal == 0) { - continue; - } - - // === Verhältnis PV/Netz bestimmen === - if ($impTotal <= $expTotal && $expTotal > 0) { - $ratio = $impTotal / $expTotal; // PV deckt alles - } else { - $ratio = ($impTotal > 0) ? ($expTotal / $impTotal) : 0; - } - - // === Zuweisung auf einzelne Zähler === - foreach ($slot as $name => $v) { - - $imp = $v['imp']; - $exp = $v['exp']; - - // Summen Import/Export - $acc[$name]['imp'] += $imp; - $acc[$name]['exp'] += $exp; - - if ($impTotal <= $expTotal) { - // PV deckt Verbrauch - $acc[$name]['solar_bezug'] += $imp; - $acc[$name]['solareinspeisung'] += (1 - $ratio) * $exp; - $acc[$name]['solarverkauf'] += $ratio * $exp; - - if ($pSolar !== null) $acc[$name]['cost_solar'] += ($imp * $pSolar) / 100; - if ($pFeed !== null) $acc[$name]['rev_feedin'] += ((1 - $ratio) * $exp * $pFeed) / 100; - - } else { - // Teil Netzbezug - $acc[$name]['solar_bezug'] += $ratio * $imp; - $acc[$name]['netz_bezug'] += (1 - $ratio) * $imp; - $acc[$name]['solarverkauf'] += $exp; - - if ($pGrid !== null) $acc[$name]['cost_grid'] += ((1 - $ratio) * $imp * $pGrid) / 100; - if ($pSolar !== null) $acc[$name]['cost_solar'] += ($ratio * $imp * $pSolar) / 100; - if ($pFeed !== null) $acc[$name]['rev_feedin'] += ($exp * $pFeed) / 100; - } - } - } - - // === Tabelle schließen === $sum = 0.0; - foreach ($acc as $name => $a) { + + foreach ($this->powerCostCache[$userId] as $name => $a) { $subtotal = $a['cost_grid'] + $a['cost_solar'] - $a['rev_feedin']; $sum += $subtotal; $html .= " - {$name} + {$a['name']} " . number_format($a['imp'], 3) . " " . number_format($a['exp'], 3) . " " . number_format($a['solar_bezug'], 3) . " @@ -276,13 +313,15 @@ private function CalculatePowerCosts($powerMeters, $tariffs, $userId, $from, $to "; } - $html .= "
"; + $html .= " + Total Stromkosten: + " . number_format($sum, 2) . " +
"; return ['html' => $html, 'sum' => $sum]; } - private function CalculateAdditionalCosts($waterMeters, $tariffs, $userId, $from, $to) { $html = " @@ -421,6 +460,23 @@ private function CalculatePowerCosts($powerMeters, $tariffs, $userId, $from, $to return $closest ?? floatval(GetValue($varId)); } + private function getDeltaFromArchive(int $varId, int $tStart, int $tEnd): float + { + $startValue = $this->GetValueAt($varId, $tStart, false); // Wert davor/zu Beginn + $endValue = $this->GetValueAt($varId, $tEnd, true); // Wert danach/zum Ende + + if ($startValue === null || $endValue === null) { + IPS_LogMessage('Abrechnung', "getDeltaFromArchive: Keine Werte für Var $varId zwischen " . date('d.m.Y H:i', $tStart) . " und " . date('d.m.Y H:i', $tEnd)); + return 0.0; + } + + $diff = $endValue - $startValue; + if ($diff < 0) { + // Sicherheitsnetz bei Zähler-Reset + $diff = 0.0; + } + return (float)$diff; + } private function toUnixTs($val, $endOfDay = false) {