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
| Zähler Start | Zähler Ende | Verbrauch | Tarif (Rp) | Kosten (CHF) |
";
- $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
| " . number_format($kosten, 2) . " |
";
- 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);
}
}