diff --git a/Abrechnung/module.php b/Abrechnung/module.php index 15d9a11..3b8264b 100644 --- a/Abrechnung/module.php +++ b/Abrechnung/module.php @@ -33,7 +33,7 @@ class Abrechnung extends IPSModule { $mid = @IPS_GetObjectIDByIdent($Ident, $this->InstanceID); if ($mid === false) { - $mid = IPS_CreateMedia(5); // 5 = Document + $mid = IPS_CreateMedia(5); IPS_SetParent($mid, $this->InstanceID); IPS_SetIdent($mid, $Ident); IPS_SetName($mid, $Name); @@ -82,11 +82,9 @@ class Abrechnung extends IPSModule $tariffs = json_decode($this->ReadPropertyString('Tariffs'), true); 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); @@ -102,7 +100,6 @@ class Abrechnung extends IPSModule return $pdf->Output('Abrechnung.pdf', 'S'); } - private function BuildUserInvoice($pdf, $user, $power, $water, $tariffs, $from, $to) { $pdf->AddPage(); @@ -112,17 +109,12 @@ class Abrechnung extends IPSModule

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

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


"; - // âš¡ Stromkosten (aus globaler Berechnung) - $html .= "

âš¡ Stromkosten

"; $powerResult = $this->GetCalculatedPowerCosts($user['id']); - $html .= $powerResult['html']; + $html .= "

âš¡ Stromkosten

" . $powerResult['html']; $totalPower = $powerResult['sum']; - - // 💧 Nebenkosten - $html .= "

💧 Nebenkosten (Wasser/Wärme)

"; $additionalResult = $this->CalculateAdditionalCosts($water, $tariffs, $user['id'], $from, $to); - $html .= $additionalResult['html']; + $html .= "

💧 Nebenkosten (Wasser/Wärme)

" . $additionalResult['html']; $totalAdditional = $additionalResult['sum']; $grandTotal = $totalPower + $totalAdditional; @@ -131,271 +123,178 @@ class Abrechnung extends IPSModule $pdf->writeHTML($html, true, false, true, false, ''); } -private function CalculateAllPowerCosts($powerMeters, $tariffs, $from, $to) -{ - IPS_LogMessage('Abrechnung', "CalculateAllPowerCosts: from=" . date('d.m.Y H:i', $from) . " to=" . date('d.m.Y H:i', $to)); + private function CalculateAllPowerCosts($powerMeters, $tariffs, $from, $to) + { + $this->powerCostCache = []; - $this->powerCostCache = []; // Cache leeren + $metersByUser = []; + foreach ($powerMeters as $m) { + if (!isset($m['user_id'])) continue; - // 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']); + $userId = (int)$m['user_id']; + $name = $m['name'] ?? ('Meter_' . $m['user_id']); - if (!isset($metersByUser[$userId])) { - $metersByUser[$userId] = []; - } - $metersByUser[$userId][$name] = [ - 'importVar' => $m['var_consumption'] ?? null, - 'exportVar' => $m['var_feed'] ?? 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 - IPS_LogMessage("ABR", "---- DELTA-Berechnung für Intervall " . date("H:i", $ts) . " - " . date("H:i", $slotEnd) . " ----"); - -// 2a. Deltas je Zähler des Users -IPS_LogMessage("ABR", "---- BEGIN USER-METER DELTA BLOCK ----"); -IPS_LogMessage("ABR", "Intervall: " . date("d.m.Y H:i", $ts) . " - " . date("d.m.Y H:i", $slotEnd)); - -foreach ($meters as $name => $mm) { - - IPS_LogMessage("ABR", "Meter: $name"); - IPS_LogMessage("ABR", " RAW mm JSON: " . json_encode($mm)); - - $impDelta = 0.0; - $expDelta = 0.0; - - // -------------------- - // IMPORT - // -------------------- - if (isset($mm['importVar'])) { - IPS_LogMessage("ABR", " importVar found: " . $mm['importVar']); - } else { - IPS_LogMessage("ABR", " ⚠ importVar NOT FOUND IN ARRAY!"); - } - - if (!empty($mm['importVar'])) { - - $varId = (int)$mm['importVar']; - IPS_LogMessage("ABR", " Checking IMPORT variable ID = $varId"); - - if (IPS_VariableExists($varId)) { - IPS_LogMessage("ABR", " → IMPORT variable EXISTS"); - - $impDelta = $this->getDeltaFromArchive($varId, $ts, $slotEnd); - - IPS_LogMessage("ABR", " → IMPORT DELTA = $impDelta"); - } else { - IPS_LogMessage("ABR", " ⚠ IMPORT variable DOES NOT EXIST in IPS!"); - } - } else { - IPS_LogMessage("ABR", " ⚠ importVar IS EMPTY OR NULL"); - } - - // -------------------- - // EXPORT - // -------------------- - if (isset($mm['exportVar'])) { - IPS_LogMessage("ABR", " exportVar found: " . $mm['exportVar']); - } else { - IPS_LogMessage("ABR", " ⚠ exportVar NOT FOUND IN ARRAY!"); - } - - if (!empty($mm['exportVar'])) { - - $varId = (int)$mm['exportVar']; - IPS_LogMessage("ABR", " Checking EXPORT variable ID = $varId"); - - if (IPS_VariableExists($varId)) { - IPS_LogMessage("ABR", " → EXPORT variable EXISTS"); - - $expDelta = $this->getDeltaFromArchive($varId, $ts, $slotEnd); - - IPS_LogMessage("ABR", " → EXPORT DELTA = $expDelta"); - } else { - IPS_LogMessage("ABR", " ⚠ EXPORT variable DOES NOT EXIST in IPS!"); - } - } else { - IPS_LogMessage("ABR", " ⚠ exportVar IS EMPTY OR NULL"); - } - - // -------------------- - // DELTAS WURDEN GEFUNDEN? - // -------------------- - if ($impDelta > 0.0 || $expDelta > 0.0) { - - IPS_LogMessage("ABR", " ✔ VALID DELTAS → imp=$impDelta exp=$expDelta"); - - $slot[$name] = ['imp' => $impDelta, 'exp' => $expDelta]; - - $impTotal += $impDelta; - $expTotal += $expDelta; - - } else { - - IPS_LogMessage("ABR", " ⚠ NO DELTA FOUND FOR THIS METER (imp=0 exp=0)"); - } - - IPS_LogMessage("ABR", "-------------------------------------------"); -} - -IPS_LogMessage("ABR", "---- END USER-METER DELTA BLOCK ----"); -IPS_LogMessage("ABR", "Intervall SUMME: impTotal=$impTotal | expTotal=$expTotal"); - - - if ($impTotal == 0.0 && $expTotal == 0.0) { - continue; // für diesen User in diesem Intervall nichts passiert + if (!isset($metersByUser[$userId])) { + $metersByUser[$userId] = []; } + $metersByUser[$userId][$name] = [ + 'importVar' => $m['var_consumption'] ?? null, + 'exportVar' => $m['var_feed'] ?? null + ]; - // 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; + 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 + ]; + } - // 2c. Werte pro Zähler verteilen - foreach ($slot as $name => $v) { - $imp = $v['imp']; - $exp = $v['exp']; + if (empty($metersByUser)) return; - $this->powerCostCache[$userId][$name]['imp'] += $imp; - $this->powerCostCache[$userId][$name]['exp'] += $exp; + for ($ts = $from; $ts < $to; $ts += 900) { + $slotEnd = min($to, $ts + 900); - 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; + $pGrid = $this->getTariffPriceAt($tariffs, ['Netztarif'], $ts); + $pSolar = $this->getTariffPriceAt($tariffs, ['Solartarif'], $ts); + $pFeed = $this->getTariffPriceAt($tariffs, ['Einspeisetarif'], $ts); - if ($pSolar !== null) { - $this->powerCostCache[$userId][$name]['cost_solar'] += ($imp * $pSolar) / 100.0; + foreach ($metersByUser as $userId => $meters) { + + $impTotal = 0.0; + $expTotal = 0.0; + $slot = []; + + 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 ($pFeed !== null) { - $this->powerCostCache[$userId][$name]['rev_feedin'] += ((1 - $ratio) * $exp * $pFeed) / 100.0; + 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; + + if ($impTotal <= $expTotal && $expTotal > 0.0) { + $ratio = $impTotal / $expTotal; + $pvCoversAll = true; + } elseif ($impTotal > 0.0) { + $ratio = $expTotal / $impTotal; + $pvCoversAll = false; } 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; + $ratio = 0.0; + $pvCoversAll = true; + } - 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; + 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) { + $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 { + $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 = " - - - - - - - - - - - - - "; - - if (empty($this->powerCostCache) || !isset($this->powerCostCache[$userId])) { - $html .= "
ZählerImport (kWh)Export (kWh)Solarbezug (kWh)Netzbezug (kWh)Solareinspeisung (kWh)Solarverkauf (kWh)Kosten Solar (CHF)Kosten Netz (CHF)Ertrag Einspeisung (CHF)Summe (CHF)
Keine Stromzähler für diesen Benutzer

"; - return ['html' => $html, 'sum' => 0.0]; - } - - $sum = 0.0; - - foreach ($this->powerCostCache[$userId] as $name => $a) { - $subtotal = $a['cost_grid'] + $a['cost_solar'] - $a['rev_feedin']; - $sum += $subtotal; - - $html .= " - {$a['name']} - " . number_format($a['imp'], 3) . " - " . number_format($a['exp'], 3) . " - " . number_format($a['solar_bezug'], 3) . " - " . number_format($a['netz_bezug'], 3) . " - " . number_format($a['solareinspeisung'], 3) . " - " . number_format($a['solarverkauf'], 3) . " - " . number_format($a['cost_solar'], 2) . " - " . number_format($a['cost_grid'], 2) . " - " . number_format($a['rev_feedin'], 2) . " - " . number_format($subtotal, 2) . " + private function GetCalculatedPowerCosts($userId) + { + $html = " + + + + + + + + + + + + "; + + if (empty($this->powerCostCache) || !isset($this->powerCostCache[$userId])) { + $html .= "
ZählerImport (kWh)Export (kWh)Solarbezug (kWh)Netzbezug (kWh)Solareinspeisung (kWh)Solarverkauf (kWh)Kosten Solar (CHF)Kosten Netz (CHF)Ertrag Einspeisung (CHF)Summe (CHF)
Keine Stromzähler für diesen Benutzer

"; + return ['html' => $html, 'sum' => 0.0]; + } + + $sum = 0.0; + + foreach ($this->powerCostCache[$userId] as $name => $a) { + $subtotal = $a['cost_grid'] + $a['cost_solar'] - $a['rev_feedin']; + $sum += $subtotal; + + $html .= " + {$a['name']} + " . number_format($a['imp'], 3) . " + " . number_format($a['exp'], 3) . " + " . number_format($a['solar_bezug'], 3) . " + " . number_format($a['netz_bezug'], 3) . " + " . number_format($a['solareinspeisung'], 3) . " + " . number_format($a['solarverkauf'], 3) . " + " . number_format($a['cost_solar'], 2) . " + " . number_format($a['cost_grid'], 2) . " + " . number_format($a['rev_feedin'], 2) . " + " . number_format($subtotal, 2) . " + "; + } + + $html .= " + Total Stromkosten: + " . number_format($sum, 2) . " +
"; + + return ['html' => $html, 'sum' => $sum]; } - $html .= " - Total Stromkosten: - " . number_format($sum, 2) . " -
"; - - return ['html' => $html, 'sum' => $sum]; -} - - private function CalculateAdditionalCosts($waterMeters, $tariffs, $userId, $from, $to) { $html = " @@ -433,8 +332,6 @@ private function GetCalculatedPowerCosts($userId) return ['html' => $html, 'sum' => $total]; } - // ====================== Kernlogik für Wasser/Wärme ====================== - private function AddMeterToPDFRow($meter, $tariffs, $from, $to, $type) { $rows = ''; @@ -512,8 +409,6 @@ private function GetCalculatedPowerCosts($userId) return ['row' => $rows, 'value' => $totalCost, 'tariffs' => array_values($usedTariffs)]; } - // ====================== Hilfsfunktionen ====================== - private function GetValueAt($varId, $timestamp, $nearestAfter = true) { $archiveID = @IPS_GetInstanceListByModuleID('{43192F0B-135B-4CE7-A0A7-1475603F3060}')[0]; @@ -537,17 +432,15 @@ private function GetCalculatedPowerCosts($userId) 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 + $startValue = $this->GetValueAt($varId, $tStart, false); + $endValue = $this->GetValueAt($varId, $tEnd, true); 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;