no message

This commit is contained in:
2025-12-05 10:24:15 +01:00
parent 17aed355b3
commit 82e4d4c523

View File

@@ -20,7 +20,11 @@ class Abrechnung extends IPSModule
$this->EnableAction('FromDate'); $this->EnableAction('FromDate');
$this->EnableAction('ToDate'); $this->EnableAction('ToDate');
$this->RegisterScript('StartBilling', 'Abrechnung starten', "<?php IPS_RequestAction(" . $this->InstanceID . ", 'StartBilling', ''); ?>"); $this->RegisterScript(
'StartBilling',
'Abrechnung starten',
"<?php IPS_RequestAction(" . $this->InstanceID . ", 'StartBilling', ''); ?>"
);
$this->RegisterMediaDocument('InvoicePDF', 'Letzte Rechnung', 'pdf'); $this->RegisterMediaDocument('InvoicePDF', 'Letzte Rechnung', 'pdf');
} }
@@ -85,6 +89,7 @@ class Abrechnung extends IPSModule
return false; return false;
} }
// Stromkosten einmal für alle User berechnen
$this->CalculateAllPowerCosts($power, $tariffs, $from, $to); $this->CalculateAllPowerCosts($power, $tariffs, $from, $to);
$pdf = new TCPDF('P', 'mm', 'A4', true, 'UTF-8', false); $pdf = new TCPDF('P', 'mm', 'A4', true, 'UTF-8', false);
@@ -109,10 +114,12 @@ class Abrechnung extends IPSModule
<p>{$user['address']}<br>{$user['city']}</p> <p>{$user['address']}<br>{$user['city']}</p>
<p><strong>Zeitraum:</strong> " . date('d.m.Y', $from) . " " . date('d.m.Y', $to) . "</p><br>"; <p><strong>Zeitraum:</strong> " . date('d.m.Y', $from) . " " . date('d.m.Y', $to) . "</p><br>";
// Stromkosten (aus globaler Berechnung)
$powerResult = $this->GetCalculatedPowerCosts($user['id']); $powerResult = $this->GetCalculatedPowerCosts($user['id']);
$html .= "<h3>⚡ Stromkosten</h3>" . $powerResult['html']; $html .= "<h3>⚡ Stromkosten</h3>" . $powerResult['html'];
$totalPower = $powerResult['sum']; $totalPower = $powerResult['sum'];
// Nebenkosten (Wasser/Wärme)
$additionalResult = $this->CalculateAdditionalCosts($water, $tariffs, $user['id'], $from, $to); $additionalResult = $this->CalculateAdditionalCosts($water, $tariffs, $user['id'], $from, $to);
$html .= "<h3>💧 Nebenkosten (Wasser/Wärme)</h3>" . $additionalResult['html']; $html .= "<h3>💧 Nebenkosten (Wasser/Wärme)</h3>" . $additionalResult['html'];
$totalAdditional = $additionalResult['sum']; $totalAdditional = $additionalResult['sum'];
@@ -123,13 +130,18 @@ class Abrechnung extends IPSModule
$pdf->writeHTML($html, true, false, true, false, ''); $pdf->writeHTML($html, true, false, true, false, '');
} }
// ====================== Stromkosten (15-Minuten, alle User) ======================
private function CalculateAllPowerCosts($powerMeters, $tariffs, $from, $to) private function CalculateAllPowerCosts($powerMeters, $tariffs, $from, $to)
{ {
$this->powerCostCache = []; $this->powerCostCache = [];
// Zähler je User aufbauen
$metersByUser = []; $metersByUser = [];
foreach ($powerMeters as $m) { foreach ($powerMeters as $m) {
if (!isset($m['user_id'])) continue; if (!isset($m['user_id'])) {
continue;
}
$userId = (int)$m['user_id']; $userId = (int)$m['user_id'];
$name = $m['name'] ?? ('Meter_' . $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) { for ($ts = $from; $ts < $to; $ts += 900) {
$slotEnd = min($to, $ts + 900); $slotEnd = min($to, $ts + 900);
// Tarife (gleich für alle User)
$pGrid = $this->getTariffPriceAt($tariffs, ['Netztarif'], $ts); $pGrid = $this->getTariffPriceAt($tariffs, ['Netztarif'], $ts);
$pSolar = $this->getTariffPriceAt($tariffs, ['Solartarif'], $ts); $pSolar = $this->getTariffPriceAt($tariffs, ['Solartarif'], $ts);
$pFeed = $this->getTariffPriceAt($tariffs, ['Einspeisetarif'], $ts); $pFeed = $this->getTariffPriceAt($tariffs, ['Einspeisetarif'], $ts);
@@ -174,6 +190,7 @@ class Abrechnung extends IPSModule
$expTotal = 0.0; $expTotal = 0.0;
$slot = []; $slot = [];
// Deltas je Zähler des Users
foreach ($meters as $name => $mm) { foreach ($meters as $name => $mm) {
$impDelta = 0.0; $impDelta = 0.0;
@@ -193,8 +210,11 @@ class Abrechnung extends IPSModule
} }
} }
if ($impTotal == 0.0 && $expTotal == 0.0) continue; if ($impTotal == 0.0 && $expTotal == 0.0) {
continue;
}
// Verhältnis PV / Netz pro User
if ($impTotal <= $expTotal && $expTotal > 0.0) { if ($impTotal <= $expTotal && $expTotal > 0.0) {
$ratio = $impTotal / $expTotal; $ratio = $impTotal / $expTotal;
$pvCoversAll = true; $pvCoversAll = true;
@@ -206,6 +226,7 @@ class Abrechnung extends IPSModule
$pvCoversAll = true; $pvCoversAll = true;
} }
// Werte pro Zähler verteilen
foreach ($slot as $name => $v) { foreach ($slot as $name => $v) {
$imp = $v['imp']; $imp = $v['imp'];
$exp = $v['exp']; $exp = $v['exp'];
@@ -214,6 +235,7 @@ class Abrechnung extends IPSModule
$this->powerCostCache[$userId][$name]['exp'] += $exp; $this->powerCostCache[$userId][$name]['exp'] += $exp;
if ($pvCoversAll) { if ($pvCoversAll) {
// PV deckt gesamten Verbrauch
$this->powerCostCache[$userId][$name]['solar_bezug'] += $imp; $this->powerCostCache[$userId][$name]['solar_bezug'] += $imp;
$this->powerCostCache[$userId][$name]['solareinspeisung'] += (1 - $ratio) * $exp; $this->powerCostCache[$userId][$name]['solareinspeisung'] += (1 - $ratio) * $exp;
$this->powerCostCache[$userId][$name]['solarverkauf'] += $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; $this->powerCostCache[$userId][$name]['rev_feedin'] += ((1 - $ratio) * $exp * $pFeed) / 100.0;
} }
} else { } else {
// Teil Netzbezug
$this->powerCostCache[$userId][$name]['solar_bezug'] += $ratio * $imp; $this->powerCostCache[$userId][$name]['solar_bezug'] += $ratio * $imp;
$this->powerCostCache[$userId][$name]['netz_bezug'] += (1 - $ratio) * $imp; $this->powerCostCache[$userId][$name]['netz_bezug'] += (1 - $ratio) * $imp;
$this->powerCostCache[$userId][$name]['solarverkauf'] += $exp; $this->powerCostCache[$userId][$name]['solarverkauf'] += $exp;
@@ -295,6 +318,8 @@ class Abrechnung extends IPSModule
return ['html' => $html, 'sum' => $sum]; return ['html' => $html, 'sum' => $sum];
} }
// ====================== Nebenkosten Wasser/Wärme ======================
private function CalculateAdditionalCosts($waterMeters, $tariffs, $userId, $from, $to) private function CalculateAdditionalCosts($waterMeters, $tariffs, $userId, $from, $to)
{ {
$html = "<table border='1' cellspacing='0' cellpadding='3' width='100%' style='font-size:6px;'> $html = "<table border='1' cellspacing='0' cellpadding='3' width='100%' style='font-size:6px;'>
@@ -307,7 +332,9 @@ class Abrechnung extends IPSModule
$usedTariffs = []; $usedTariffs = [];
foreach ($waterMeters as $m) { foreach ($waterMeters as $m) {
if ($m['user_id'] != $userId) continue; if ($m['user_id'] != $userId) {
continue;
}
$type = $m['meter_type'] ?? 'Warmwasser'; $type = $m['meter_type'] ?? 'Warmwasser';
$cost = $this->AddMeterToPDFRow($m, $tariffs, $from, $to, $type); $cost = $this->AddMeterToPDFRow($m, $tariffs, $from, $to, $type);
$html .= $cost['row']; $html .= $cost['row'];
@@ -339,7 +366,9 @@ class Abrechnung extends IPSModule
$usedTariffs = []; $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) => $filteredTariffs = array_filter($tariffs, fn($t) =>
strtolower(trim($t['unit_type'] ?? '')) === strtolower(trim($type)) 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]; $activeTariff = ['start_ts' => $currentStart, 'end_ts' => $to, 'price' => 0.0];
} }
$tariffEnd = intval($activeTariff['end_ts']); $tariffEnd = (int)$activeTariff['end_ts'];
$tariffPrice = floatval($activeTariff['price']); $tariffPrice = (float)$activeTariff['price'];
$key = $activeTariff['start_ts'] . '-' . $activeTariff['end_ts'] . '-' . $tariffPrice; $key = $activeTariff['start_ts'] . '-' . $activeTariff['end_ts'] . '-' . $tariffPrice;
if (!isset($usedTariffs[$key])) { if (!isset($usedTariffs[$key])) {
@@ -382,7 +411,9 @@ class Abrechnung extends IPSModule
$segmentEnd = ($tariffEnd < $to) ? $tariffEnd : $to; $segmentEnd = ($tariffEnd < $to) ? $tariffEnd : $to;
$endValue = $this->GetValueAt($varId, $segmentEnd, true); $endValue = $this->GetValueAt($varId, $segmentEnd, true);
if ($startValue === null || $endValue === null) break; if ($startValue === null || $endValue === null) {
break;
}
$verbrauch = max(0, $endValue - $startValue); $verbrauch = max(0, $endValue - $startValue);
$kosten = round(($tariffPrice / 100) * $verbrauch, 2); $kosten = round(($tariffPrice / 100) * $verbrauch, 2);
@@ -400,40 +431,55 @@ class Abrechnung extends IPSModule
<td align='right'>" . number_format($kosten, 2) . "</td> <td align='right'>" . number_format($kosten, 2) . "</td>
</tr>"; </tr>";
if ($tariffEnd < $to) $currentStart = $tariffEnd + 1; if ($tariffEnd < $to) {
else break; $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)]; return ['row' => $rows, 'value' => $totalCost, 'tariffs' => array_values($usedTariffs)];
} }
// ====================== Hilfsfunktionen ======================
private function GetValueAt($varId, $timestamp, $nearestAfter = true) private function GetValueAt($varId, $timestamp, $nearestAfter = true)
{ {
$archiveID = @IPS_GetInstanceListByModuleID('{43192F0B-135B-4CE7-A0A7-1475603F3060}')[0]; $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); $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; $closest = null;
foreach ($values as $v) { foreach ($values as $v) {
if ($nearestAfter && $v['TimeStamp'] >= $timestamp) { if ($nearestAfter && $v['TimeStamp'] >= $timestamp) {
// erster Wert NACH oder GENAU zum Timestamp
$closest = $v['Value']; $closest = $v['Value'];
break; break;
} elseif (!$nearestAfter && $v['TimeStamp'] <= $timestamp) { } elseif (!$nearestAfter && $v['TimeStamp'] <= $timestamp) {
// letzter Wert DAVOR oder GENAU zum Timestamp
$closest = $v['Value']; $closest = $v['Value'];
} }
} }
return $closest ?? floatval(GetValue($varId)); return $closest ?? (float)GetValue($varId);
} }
private function getDeltaFromArchive(int $varId, int $tStart, int $tEnd): float 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); $startValue = $this->GetValueAt($varId, $tStart, false);
$endValue = $this->GetValueAt($varId, $tEnd, true); $endValue = $this->GetValueAt($varId, $tEnd, false);
if ($startValue === null || $endValue === null) { if ($startValue === null || $endValue === null) {
return 0.0; return 0.0;
@@ -441,6 +487,7 @@ class Abrechnung extends IPSModule
$diff = $endValue - $startValue; $diff = $endValue - $startValue;
if ($diff < 0) { if ($diff < 0) {
// Sicherheitsnetz bei Zähler-Reset
$diff = 0.0; $diff = 0.0;
} }
return (float)$diff; return (float)$diff;
@@ -448,8 +495,12 @@ class Abrechnung extends IPSModule
private function toUnixTs($val, $endOfDay = false) private function toUnixTs($val, $endOfDay = false)
{ {
if (is_int($val)) return $val; if (is_int($val)) {
if (is_numeric($val)) return intval($val); return $val;
}
if (is_numeric($val)) {
return (int)$val;
}
if (is_string($val)) { if (is_string($val)) {
$s = trim($val); $s = trim($val);
@@ -457,7 +508,13 @@ class Abrechnung extends IPSModule
$obj = json_decode($s, true); $obj = json_decode($s, true);
if (isset($obj['year'], $obj['month'], $obj['day'])) { if (isset($obj['year'], $obj['month'], $obj['day'])) {
$time = $endOfDay ? '23:59:59' : '00:00:00'; $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); $ts = strtotime($s);
@@ -468,27 +525,45 @@ class Abrechnung extends IPSModule
private function readDelta($varId, $tStart, $tEnd) private function readDelta($varId, $tStart, $tEnd)
{ {
if (!is_int($tStart)) $tStart = strtotime($tStart); if (!is_int($tStart)) {
if (!is_int($tEnd)) $tEnd = strtotime($tEnd); $tStart = strtotime($tStart);
}
if (!is_int($tEnd)) {
$tEnd = strtotime($tEnd);
}
$archiveID = @IPS_GetInstanceListByModuleID('{43192F0B-135B-4CE7-A0A7-1475603F3060}')[0]; $archiveID = @IPS_GetInstanceListByModuleID('{43192F0B-135B-4CE7-A0A7-1475603F3060}')[0];
if (!$archiveID || !IPS_VariableExists($varId)) return 0.0; if (!$archiveID || !IPS_VariableExists($varId)) {
return 0.0;
}
$values = @AC_GetLoggedValues($archiveID, $varId, $tStart - 86400, $tEnd + 86400, 0); $values = @AC_GetLoggedValues($archiveID, $varId, $tStart - 86400, $tEnd + 86400, 0);
if (empty($values)) return 0.0; if (empty($values)) {
return 0.0;
}
usort($values, fn($a, $b) => intval($a['TimeStamp']) <=> intval($b['TimeStamp'])); usort($values, fn($a, $b) => (int)$a['TimeStamp'] <=> (int)$b['TimeStamp']);
$vStart = null; $vStart = null;
$vEnd = null; $vEnd = null;
foreach ($values as $v) { foreach ($values as $v) {
if ($v['TimeStamp'] <= $tStart) $vStart = $v['Value']; if ($v['TimeStamp'] <= $tStart) {
if ($v['TimeStamp'] <= $tEnd) $vEnd = $v['Value']; $vStart = $v['Value'];
if ($v['TimeStamp'] > $tEnd) break; }
if ($v['TimeStamp'] <= $tEnd) {
$vEnd = $v['Value'];
}
if ($v['TimeStamp'] > $tEnd) {
break;
}
} }
if ($vStart === null) $vStart = floatval(GetValue($varId)); if ($vStart === null) {
if ($vEnd === null) $vEnd = floatval(GetValue($varId)); $vStart = (float)GetValue($varId);
}
if ($vEnd === null) {
$vEnd = (float)GetValue($varId);
}
$diff = $vEnd - $vStart; $diff = $vEnd - $vStart;
return ($diff < 0) ? 0.0 : $diff; return ($diff < 0) ? 0.0 : $diff;
@@ -498,16 +573,26 @@ class Abrechnung extends IPSModule
{ {
$wanted = array_map('strtolower', $typeSynonyms); $wanted = array_map('strtolower', $typeSynonyms);
$cands = []; $cands = [];
foreach ($tariffs as $t) { foreach ($tariffs as $t) {
$u = strtolower(trim($t['unit_type'] ?? '')); $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); $s = $this->toUnixTs($t['start'], false);
$e = $this->toUnixTs($t['end'], true); $e = $this->toUnixTs($t['end'], true);
if (!$s || !$e) continue; if (!$s || !$e) {
if ($s <= $ts && $ts <= $e) $cands[] = floatval($t['price']); continue;
}
if ($s <= $ts && $ts <= $e) {
$cands[] = (float)$t['price'];
}
}
if (empty($cands)) {
return null;
} }
if (empty($cands)) return null;
return end($cands); return end($cands);
} }
} }