Files
Symcon_Belevo_Energiemanage…/Abrechnung/module.php
2025-11-05 08:43:06 +01:00

416 lines
15 KiB
PHP
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
declare(strict_types=1);
include_once __DIR__ . '/libs/vendor/autoload.php'; // TCPDF via Composer
class Abrechnung extends IPSModule
{
public function Create()
{
parent::Create();
// Eigenschaften
$this->RegisterPropertyString('Users', '[]');
$this->RegisterPropertyString('PowerMeters', '[]');
$this->RegisterPropertyString('WaterMeters', '[]');
$this->RegisterPropertyString('Tariffs', '[]');
// Variablen
$this->RegisterVariableInteger('FromDate', 'Startdatum', '~UnixTimestamp', 1);
$this->RegisterVariableInteger('ToDate', 'Enddatum', '~UnixTimestamp', 2);
$this->RegisterVariableString('LastResult', 'Letzte Abrechnung', '', 3);
$this->EnableAction('FromDate');
$this->EnableAction('ToDate');
// Abrechnungs-Button
$this->RegisterScript('StartBilling', 'Abrechnung starten', "<?php IPS_RequestAction(" . $this->InstanceID . ", 'StartBilling', ''); ?>");
// 🧾 Media-Objekt für PDF-Ergebnis
$this->RegisterMediaDocument('InvoicePDF', 'Letzte Rechnung', 'pdf');
}
public function ApplyChanges()
{
parent::ApplyChanges();
IPS_LogMessage('Abrechnung', 'Modul geladen');
}
private function RegisterMediaDocument($Ident, $Name, $Extension, $Position = 0)
{
$mid = @IPS_GetObjectIDByIdent($Ident, $this->InstanceID);
if ($mid === false) {
$mid = IPS_CreateMedia(5); // 5 = Document
IPS_SetParent($mid, $this->InstanceID);
IPS_SetIdent($mid, $Ident);
IPS_SetName($mid, $Name);
IPS_SetPosition($mid, $Position);
IPS_SetMediaFile($mid, 'media/' . $mid . '.' . $Extension, false);
IPS_LogMessage('Abrechnung', 'Media-Datei erstellt: media/' . $mid . '.' . $Extension);
}
}
public function RequestAction($Ident, $Value)
{
IPS_LogMessage('Abrechnung', "RequestAction: $Ident");
switch ($Ident) {
case 'FromDate':
case 'ToDate':
SetValue($this->GetIDForIdent($Ident), $Value);
break;
case 'StartBilling':
IPS_LogMessage('Abrechnung', 'Starte Abrechnung...');
try {
$pdfContent = $this->GenerateInvoices();
if ($pdfContent === false) {
IPS_LogMessage('Abrechnung', '❌ GenerateInvoices() lieferte false zurück');
echo "❌ Fehler bei der PDF-Erstellung (GenerateInvoices)";
return;
}
if ($pdfContent === null) {
IPS_LogMessage('Abrechnung', '❌ GenerateInvoices() gab null zurück');
echo "❌ Fehler bei der PDF-Erstellung (leer)";
return;
}
if (strlen($pdfContent) < 100) {
IPS_LogMessage('Abrechnung', '❌ PDF-Inhalt zu kurz: ' . strlen($pdfContent));
echo "❌ Fehler bei der PDF-Erstellung (leeres PDF)";
return;
}
$mediaID = $this->GetIDForIdent('InvoicePDF');
if (!$mediaID) {
IPS_LogMessage('Abrechnung', '❌ Media-ID nicht gefunden');
echo "❌ Kein Media-Objekt vorhanden";
return;
}
IPS_SetMediaContent($mediaID, base64_encode($pdfContent));
SetValue($this->GetIDForIdent('LastResult'), 'Abrechnung vom ' . date('d.m.Y H:i'));
IPS_LogMessage('Abrechnung', '✅ Abrechnung erfolgreich gespeichert (' . strlen($pdfContent) . ' Bytes)');
echo "✅ PDF erfolgreich erstellt.";
} catch (Throwable $e) {
IPS_LogMessage('Abrechnung', '💥 Exception in StartBilling: ' . $e->getMessage());
echo "❌ Ausnahmefehler: " . $e->getMessage();
}
break;
}
}
// ====================== PDF-Logik ======================
public function GenerateInvoices()
{
$from = GetValue($this->GetIDForIdent('FromDate'));
$to = GetValue($this->GetIDForIdent('ToDate'));
IPS_LogMessage('Abrechnung', '🕒 Starte GenerateInvoices()');
IPS_LogMessage('Abrechnung', 'Zeitraum von ' . date('d.m.Y H:i', $from) . ' bis ' . date('d.m.Y H:i', $to));
$users = json_decode($this->ReadPropertyString('Users'), true);
$power = json_decode($this->ReadPropertyString('PowerMeters'), true);
$water = json_decode($this->ReadPropertyString('WaterMeters'), true);
$tariffs = json_decode($this->ReadPropertyString('Tariffs'), true);
if (!class_exists('TCPDF')) {
IPS_LogMessage('Abrechnung', '❌ TCPDF fehlt prüfe libs/vendor/autoload.php');
return false;
}
try {
$pdf = new TCPDF('P', 'mm', 'A4', true, 'UTF-8', false);
$pdf->SetCreator('IPSymcon Abrechnung');
$pdf->SetMargins(15, 15, 15);
$pdf->SetAutoPageBreak(true, 20);
$pdf->SetFont('dejavusans', '', 10);
foreach ($users as $user) {
IPS_LogMessage('Abrechnung', '→ Erstelle Seite für Benutzer: ' . $user['name']);
$pdf->AddPage();
// 🔽 Hier fügst du den neuen HTML-Block ein:
$html = "
<h2>Rechnung für {$user['name']}</h2>
<p>{$user['address']}<br>{$user['city']}</p>
<table border='1' cellpadding='3' style='font-size:8px;'>
<tr style='background-color:#e0e0e0;'>
<th>Zähler</th>
<th>Typ</th>
<th>Startzeit</th>
<th>Endzeit</th>
<th>Zählerstand Start</th>
<th>Zählerstand Ende</th>
<th>Verbrauch</th>
<th>Tarif (Rp)</th>
<th>Kosten (CHF)</th>
</tr>
";
$total = 0.0;
foreach ($power as $m) {
if ($m['user_id'] != $user['id']) continue;
$cost = $this->AddMeterToPDFRow($m, $tariffs, $from, $to, 'Strombezug');
$html .= $cost['row'];
$total += $cost['value'];
}
foreach ($water as $m) {
if ($m['user_id'] != $user['id']) continue;
$type = $m['meter_type'] ?? 'Warmwasser';
$cost = $this->AddMeterToPDFRow($m, $tariffs, $from, $to, $type);
$html .= $cost['row'];
$total += $cost['value'];
}
$html .= "</table><h3>Gesamtsumme: " . number_format($total, 2) . " CHF</h3>";
$pdf->writeHTML($html);
}
IPS_LogMessage('Abrechnung', '✅ PDF-Daten fertiggestellt');
return $pdf->Output('Abrechnung.pdf', 'S');
} catch (Throwable $e) {
IPS_LogMessage('Abrechnung', '💥 Exception in TCPDF: ' . $e->getMessage());
return false;
}
}
private function AddMeterToPDF($pdf, $meter, $tariffs, $from, $to, $type)
{
$start = $this->GetValueAt($meter['var_consumption'], $from);
$end = $this->GetValueAt($meter['var_consumption'], $to);
if ($start === null || $end === null) return 0;
$diff = max(0, $end - $start);
$tariffRp = $this->GetTariff($tariffs, $type, $from, $to);
$cost = ($tariffRp / 100) * $diff;
$pdf->writeHTML("
<tr>
<td>{$meter['name']}</td>
<td>$type</td>
<td>$start</td>
<td>$end</td>
<td>$diff</td>
<td>$tariffRp</td>
<td>" . number_format($cost, 2) . "</td>
</tr>", false, false, false, false, '');
return $cost;
}
private function GetTariff(array $tariffs, string $type, int $from, int $to): float
{
foreach ($tariffs as $t) {
$start = strtotime($t['start']);
$end = strtotime($t['end']);
if ($from >= $start && $to <= $end && strtolower($t['unit_type']) == strtolower($type)) {
return floatval($t['price']);
}
}
// Kein passender Tarif gefunden
return 0.0;
}
private function GetValueAt(int $varId, int $timestamp, bool $nearestAfter = true)
{
$archiveID = @IPS_GetInstanceListByModuleID('{43192F0B-135B-4CE7-A0A7-1475603F3060}')[0];
if (!$archiveID || !IPS_VariableExists($varId)) {
IPS_LogMessage('Abrechnung', "❌ Variable $varId oder Archiv $archiveID nicht gefunden");
return null;
}
$maxDays = 365; // maximal 1 Jahr suchen
$stepDays = 30; // in 30-Tage-Blöcken durchsuchen
$valueFound = null;
if ($nearestAfter) {
// 🔹 Suche den ersten Wert NACH dem gewünschten Zeitpunkt
for ($offset = 0; $offset < $maxDays; $offset += $stepDays) {
$from = $timestamp + $offset * 86400;
$to = $from + $stepDays * 86400;
$values = @AC_GetLoggedValues($archiveID, $varId, $from, $to, 0);
if (!empty($values)) {
// ersten Wert nach Timestamp nehmen
foreach ($values as $v) {
if ($v['TimeStamp'] >= $timestamp) {
$valueFound = floatval($v['Value']);
break 2;
}
}
}
}
} else {
// 🔹 Suche den letzten Wert VOR dem gewünschten Zeitpunkt
for ($offset = 0; $offset < $maxDays; $offset += $stepDays) {
$from = $timestamp - ($offset + $stepDays) * 86400;
$to = $timestamp - $offset * 86400;
$values = @AC_GetLoggedValues($archiveID, $varId, $from, $to, 0);
if (!empty($values)) {
// letzten Wert vor Timestamp nehmen
$last = null;
foreach ($values as $v) {
if ($v['TimeStamp'] <= $timestamp) {
$last = $v;
} else {
break;
}
}
if ($last !== null) {
$valueFound = floatval($last['Value']);
break;
}
}
}
}
// 🔹 Falls nichts im Archiv: aktuellen Variablenwert nehmen
if ($valueFound === null) {
$fallback = floatval(GetValue($varId));
IPS_LogMessage('Abrechnung', "⚠ Kein Archivwert für $varId gefunden nutze aktuellen Wert ($fallback)");
return $fallback;
}
return $valueFound;
}
private function AddMeterToPDFRow($meter, $tariffs, $from, $to, $type)
{
$rows = '';
$totalCost = 0.0;
$varId = $meter['var_consumption'];
IPS_LogMessage('Abrechnung', "🔧 [AddMeterToPDFRow] Starte für '{$meter['name']}' (Typ: {$type}) von " . date('d.m.Y H:i', $from) . " bis " . date('d.m.Y H:i', $to));
if (!IPS_VariableExists($varId)) {
IPS_LogMessage('Abrechnung', "❌ Variable {$varId} für {$meter['name']} nicht gefunden");
return ['row' => '', 'value' => 0];
}
// 1⃣ Relevante Tarife für diese Verbrauchsart finden
$filteredTariffs = array_filter($tariffs, function ($t) use ($type) {
return strtolower(trim($t['unit_type'] ?? '')) === strtolower(trim($type));
});
if (empty($filteredTariffs)) {
IPS_LogMessage('Abrechnung', "⚠ Keine passenden Tarife für {$type} gefunden");
return ['row' => '', 'value' => 0];
}
// 2⃣ Zeitlich sortieren
foreach ($filteredTariffs as &$t) {
$t['start_ts'] = is_numeric($t['start']) ? intval($t['start']) : strtotime($t['start']);
$t['end_ts'] = is_numeric($t['end']) ? intval($t['end']) : strtotime($t['end']);
}
unset($t);
usort($filteredTariffs, fn($a, $b) => $a['start_ts'] <=> $b['start_ts']);
// 3⃣ Startpunkt setzen
$currentStart = $from;
$segmentIndex = 1;
// Schleife bis Abrechnungsende
while ($currentStart < $to) {
IPS_LogMessage('Abrechnung', "➡️ Segment $segmentIndex: Beginne bei " . date('d.m.Y H:i', $currentStart));
// 🔹 Nächsten geloggten Wert nach Startzeit holen
$startValue = $this->GetValueAt($varId, $currentStart, true);
if ($startValue === null) {
IPS_LogMessage('Abrechnung', "⚠ Kein Startwert gefunden bei " . date('d.m.Y H:i', $currentStart));
break;
}
// 🔹 Aktiven Tarif bestimmen
$activeTariff = null;
foreach ($filteredTariffs as $t) {
if ($t['start_ts'] <= $currentStart && $currentStart < $t['end_ts']) {
$activeTariff = $t;
break;
}
}
// 🔹 Falls kein Tarif aktiv → 0 Rp
if (!$activeTariff) {
IPS_LogMessage('Abrechnung', "⚠ Kein Tarif aktiv bei " . date('d.m.Y H:i', $currentStart) . " → 0 Rp");
$activeTariff = [
'start_ts' => $currentStart,
'end_ts' => $to,
'price' => 0.0,
'unit_type'=> $type
];
}
$tariffEnd = $activeTariff['end_ts'];
$tariffPrice = floatval($activeTariff['price']);
$tariffLabel = number_format($tariffPrice, 2, ',', '');
IPS_LogMessage('Abrechnung', sprintf(
" 🧾 Aktiver Tarif: %.3f Rp (%s - %s)",
$tariffPrice,
date('d.m.Y H:i', $activeTariff['start_ts']),
date('d.m.Y H:i', $activeTariff['end_ts'])
));
// 🔹 Prüfen, ob Tarif vor Abrechnungsende endet
if ($tariffEnd < $to) {
// Tarif endet vor Rechnungsende → Segment bis Tarifende abrechnen
$segmentEnd = $tariffEnd;
} else {
// Tarif reicht bis oder über Rechnungsende → letztes Segment
$segmentEnd = $to;
}
// 🔹 Nächsten Archivwert nach Segmentende lesen
$endValue = $this->GetValueAt($varId, $segmentEnd, true);
if ($endValue === null) {
IPS_LogMessage('Abrechnung', "⚠ Kein Endwert gefunden bei " . date('d.m.Y H:i', $segmentEnd));
break;
}
// 🔹 Verbrauch und Kosten
$verbrauch = max(0, $endValue - $startValue);
$kosten = round(($tariffPrice / 100) * $verbrauch, 2);
$totalCost += $kosten;
IPS_LogMessage('Abrechnung', sprintf(
" 📊 Segment %d: Start=%.3f, Ende=%.3f, Verbrauch=%.3f, Tarif=%.3f Rp → %.2f CHF",
$segmentIndex, $startValue, $endValue, $verbrauch, $tariffPrice, $kosten
));
// 🔹 Zeile erzeugen
$rows .= "
<tr>
<td>{$meter['name']}</td>
<td>{$type}</td>
<td>" . date('d.m.Y H:i', $currentStart) . "</td>
<td>" . date('d.m.Y H:i', $segmentEnd) . "</td>
<td>" . number_format($startValue, 2) . "</td>
<td>" . number_format($endValue, 2) . "</td>
<td>" . number_format($verbrauch, 2) . "</td>
<td>{$tariffLabel}</td>
<td>" . number_format($kosten, 2) . "</td>
</tr>";
// 🔹 Falls Tarif endet → nächster Zyklus startet am Ende des Tarifs
if ($tariffEnd < $to) {
$currentStart = $tariffEnd;
} else {
break; // Letztes Segment erreicht
}
$segmentIndex++;
}
IPS_LogMessage('Abrechnung', sprintf("✅ [AddMeterToPDFRow] Fertig: %.2f CHF total, %d Segmente", $totalCost, $segmentIndex - 1));
return ['row' => $rows, 'value' => $totalCost];
}
}