diff --git a/Abrechnung/module.php b/Abrechnung/module.php index 7493b41..832cbe3 100644 --- a/Abrechnung/module.php +++ b/Abrechnung/module.php @@ -20,7 +20,11 @@ class Abrechnung extends IPSModule $this->EnableAction('FromDate'); $this->EnableAction('ToDate'); - $this->RegisterScript('StartBilling', 'Abrechnung starten', "InstanceID . ", 'StartBilling', ''); ?>"); + $this->RegisterScript( + 'StartBilling', + 'Abrechnung starten', + "InstanceID . ", 'StartBilling', ''); ?>" + ); $this->RegisterMediaDocument('InvoicePDF', 'Letzte Rechnung', 'pdf'); } @@ -85,6 +89,7 @@ class Abrechnung extends IPSModule return false; } + // Stromkosten einmal für alle User berechnen $this->CalculateAllPowerCosts($power, $tariffs, $from, $to); $pdf = new TCPDF('P', 'mm', 'A4', true, 'UTF-8', false); @@ -109,10 +114,12 @@ class Abrechnung extends IPSModule

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

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


"; + // Stromkosten (aus globaler Berechnung) $powerResult = $this->GetCalculatedPowerCosts($user['id']); $html .= "

⚡ Stromkosten

" . $powerResult['html']; $totalPower = $powerResult['sum']; + // Nebenkosten (Wasser/Wärme) $additionalResult = $this->CalculateAdditionalCosts($water, $tariffs, $user['id'], $from, $to); $html .= "

💧 Nebenkosten (Wasser/Wärme)

" . $additionalResult['html']; $totalAdditional = $additionalResult['sum']; @@ -123,13 +130,18 @@ class Abrechnung extends IPSModule $pdf->writeHTML($html, true, false, true, false, ''); } + // ====================== Stromkosten (15-Minuten, alle User) ====================== + private function CalculateAllPowerCosts($powerMeters, $tariffs, $from, $to) { $this->powerCostCache = []; + // Zähler je User aufbauen $metersByUser = []; foreach ($powerMeters as $m) { - if (!isset($m['user_id'])) continue; + if (!isset($m['user_id'])) { + continue; + } $userId = (int)$m['user_id']; $name = $m['name'] ?? ('Meter_' . $m['user_id']); @@ -159,11 +171,15 @@ class Abrechnung extends IPSModule ]; } - if (empty($metersByUser)) return; + if (empty($metersByUser)) { + return; + } + // 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); @@ -174,6 +190,7 @@ class Abrechnung extends IPSModule $expTotal = 0.0; $slot = []; + // Deltas je Zähler des Users foreach ($meters as $name => $mm) { $impDelta = 0.0; @@ -193,19 +210,23 @@ class Abrechnung extends IPSModule } } - 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 { - $ratio = 0.0; - $pvCoversAll = true; + if ($impTotal == 0.0 && $expTotal == 0.0) { + continue; } + // Verhältnis PV / Netz pro User + if ($impTotal <= $expTotal && $expTotal > 0.0) { + $ratio = $impTotal / $expTotal; + $pvCoversAll = true; + } elseif ($impTotal > 0.0) { + $ratio = $expTotal / $impTotal; + $pvCoversAll = false; + } else { + $ratio = 0.0; + $pvCoversAll = true; + } + + // Werte pro Zähler verteilen foreach ($slot as $name => $v) { $imp = $v['imp']; $exp = $v['exp']; @@ -214,6 +235,7 @@ class Abrechnung extends IPSModule $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; @@ -225,6 +247,7 @@ class Abrechnung extends IPSModule $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; @@ -295,6 +318,8 @@ class Abrechnung extends IPSModule return ['html' => $html, 'sum' => $sum]; } + // ====================== Nebenkosten Wasser/Wärme ====================== + private function CalculateAdditionalCosts($waterMeters, $tariffs, $userId, $from, $to) { $html = " @@ -303,11 +328,13 @@ class Abrechnung extends IPSModule "; - $total = 0.0; + $total = 0.0; $usedTariffs = []; foreach ($waterMeters as $m) { - if ($m['user_id'] != $userId) continue; + if ($m['user_id'] != $userId) { + continue; + } $type = $m['meter_type'] ?? 'Warmwasser'; $cost = $this->AddMeterToPDFRow($m, $tariffs, $from, $to, $type); $html .= $cost['row']; @@ -334,12 +361,14 @@ class Abrechnung extends IPSModule private function AddMeterToPDFRow($meter, $tariffs, $from, $to, $type) { - $rows = ''; - $totalCost = 0.0; + $rows = ''; + $totalCost = 0.0; $usedTariffs = []; - $varId = $meter['var_consumption']; + $varId = $meter['var_consumption']; - if (!IPS_VariableExists($varId)) return ['row' => '', 'value' => 0, 'tariffs' => []]; + if (!IPS_VariableExists($varId)) { + return ['row' => '', 'value' => 0, 'tariffs' => []]; + } $filteredTariffs = array_filter($tariffs, fn($t) => strtolower(trim($t['unit_type'] ?? '')) === strtolower(trim($type)) @@ -366,8 +395,8 @@ class Abrechnung extends IPSModule $activeTariff = ['start_ts' => $currentStart, 'end_ts' => $to, 'price' => 0.0]; } - $tariffEnd = intval($activeTariff['end_ts']); - $tariffPrice = floatval($activeTariff['price']); + $tariffEnd = (int)$activeTariff['end_ts']; + $tariffPrice = (float)$activeTariff['price']; $key = $activeTariff['start_ts'] . '-' . $activeTariff['end_ts'] . '-' . $tariffPrice; if (!isset($usedTariffs[$key])) { @@ -382,10 +411,12 @@ class Abrechnung extends IPSModule $segmentEnd = ($tariffEnd < $to) ? $tariffEnd : $to; $endValue = $this->GetValueAt($varId, $segmentEnd, true); - if ($startValue === null || $endValue === null) break; + if ($startValue === null || $endValue === null) { + break; + } $verbrauch = max(0, $endValue - $startValue); - $kosten = round(($tariffPrice / 100) * $verbrauch, 2); + $kosten = round(($tariffPrice / 100) * $verbrauch, 2); $totalCost += $kosten; $rows .= " @@ -400,40 +431,55 @@ class Abrechnung extends IPSModule "; - if ($tariffEnd < $to) $currentStart = $tariffEnd + 1; - else break; + if ($tariffEnd < $to) { + $currentStart = $tariffEnd + 1; + } else { + break; + } } - if ($rows === '') return ['row' => '', 'value' => 0, 'tariffs' => []]; + if ($rows === '') { + return ['row' => '', 'value' => 0, 'tariffs' => []]; + } 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]; - if (!$archiveID || !IPS_VariableExists($varId)) return null; + if (!$archiveID || !IPS_VariableExists($varId)) { + return null; + } + // 30 Tage Fenster um den Zeitpunkt $values = @AC_GetLoggedValues($archiveID, $varId, $timestamp - 30 * 86400, $timestamp + 30 * 86400, 0); - if (empty($values)) return floatval(GetValue($varId)); + if (empty($values)) { + return (float)GetValue($varId); + } $closest = null; foreach ($values as $v) { if ($nearestAfter && $v['TimeStamp'] >= $timestamp) { + // erster Wert NACH oder GENAU zum Timestamp $closest = $v['Value']; break; } elseif (!$nearestAfter && $v['TimeStamp'] <= $timestamp) { + // letzter Wert DAVOR oder GENAU zum Timestamp $closest = $v['Value']; } } - return $closest ?? floatval(GetValue($varId)); + return $closest ?? (float)GetValue($varId); } private function getDeltaFromArchive(int $varId, int $tStart, int $tEnd): float { + // Beide Werte: immer letzter geloggter Wert VOR/GENAU zum Zeitpunkt $startValue = $this->GetValueAt($varId, $tStart, false); - $endValue = $this->GetValueAt($varId, $tEnd, true); + $endValue = $this->GetValueAt($varId, $tEnd, false); if ($startValue === null || $endValue === null) { return 0.0; @@ -441,6 +487,7 @@ class Abrechnung extends IPSModule $diff = $endValue - $startValue; if ($diff < 0) { + // Sicherheitsnetz bei Zähler-Reset $diff = 0.0; } return (float)$diff; @@ -448,8 +495,12 @@ class Abrechnung extends IPSModule private function toUnixTs($val, $endOfDay = false) { - if (is_int($val)) return $val; - if (is_numeric($val)) return intval($val); + if (is_int($val)) { + return $val; + } + if (is_numeric($val)) { + return (int)$val; + } if (is_string($val)) { $s = trim($val); @@ -457,7 +508,13 @@ class Abrechnung extends IPSModule $obj = json_decode($s, true); if (isset($obj['year'], $obj['month'], $obj['day'])) { $time = $endOfDay ? '23:59:59' : '00:00:00'; - return strtotime(sprintf('%04d-%02d-%02d %s', $obj['year'], $obj['month'], $obj['day'], $time)); + return strtotime(sprintf( + '%04d-%02d-%02d %s', + $obj['year'], + $obj['month'], + $obj['day'], + $time + )); } } $ts = strtotime($s); @@ -468,27 +525,45 @@ class Abrechnung extends IPSModule private function readDelta($varId, $tStart, $tEnd) { - if (!is_int($tStart)) $tStart = strtotime($tStart); - if (!is_int($tEnd)) $tEnd = strtotime($tEnd); - - $archiveID = @IPS_GetInstanceListByModuleID('{43192F0B-135B-4CE7-A0A7-1475603F3060}')[0]; - if (!$archiveID || !IPS_VariableExists($varId)) return 0.0; - - $values = @AC_GetLoggedValues($archiveID, $varId, $tStart - 86400, $tEnd + 86400, 0); - if (empty($values)) return 0.0; - - usort($values, fn($a, $b) => intval($a['TimeStamp']) <=> intval($b['TimeStamp'])); - $vStart = null; - $vEnd = null; - - foreach ($values as $v) { - if ($v['TimeStamp'] <= $tStart) $vStart = $v['Value']; - if ($v['TimeStamp'] <= $tEnd) $vEnd = $v['Value']; - if ($v['TimeStamp'] > $tEnd) break; + if (!is_int($tStart)) { + $tStart = strtotime($tStart); + } + if (!is_int($tEnd)) { + $tEnd = strtotime($tEnd); } - if ($vStart === null) $vStart = floatval(GetValue($varId)); - if ($vEnd === null) $vEnd = floatval(GetValue($varId)); + $archiveID = @IPS_GetInstanceListByModuleID('{43192F0B-135B-4CE7-A0A7-1475603F3060}')[0]; + if (!$archiveID || !IPS_VariableExists($varId)) { + return 0.0; + } + + $values = @AC_GetLoggedValues($archiveID, $varId, $tStart - 86400, $tEnd + 86400, 0); + if (empty($values)) { + return 0.0; + } + + usort($values, fn($a, $b) => (int)$a['TimeStamp'] <=> (int)$b['TimeStamp']); + $vStart = null; + $vEnd = null; + + foreach ($values as $v) { + if ($v['TimeStamp'] <= $tStart) { + $vStart = $v['Value']; + } + if ($v['TimeStamp'] <= $tEnd) { + $vEnd = $v['Value']; + } + if ($v['TimeStamp'] > $tEnd) { + break; + } + } + + if ($vStart === null) { + $vStart = (float)GetValue($varId); + } + if ($vEnd === null) { + $vEnd = (float)GetValue($varId); + } $diff = $vEnd - $vStart; return ($diff < 0) ? 0.0 : $diff; @@ -497,17 +572,27 @@ class Abrechnung extends IPSModule private function getTariffPriceAt($tariffs, $typeSynonyms, $ts) { $wanted = array_map('strtolower', $typeSynonyms); - $cands = []; + $cands = []; + foreach ($tariffs as $t) { $u = strtolower(trim($t['unit_type'] ?? '')); - if (!in_array($u, $wanted)) continue; + if (!in_array($u, $wanted, true)) { + 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 (!$s || !$e) { + continue; + } + if ($s <= $ts && $ts <= $e) { + $cands[] = (float)$t['price']; + } + } + + if (empty($cands)) { + return null; } - if (empty($cands)) return null; return end($cands); } }
Zähler StartZähler EndeVerbrauchTarif (Rp)Kosten (CHF)
" . number_format($kosten, 2) . "