Files
Symcon_Belevo_Energiemanage…/Abrechnung/module.php
2025-11-05 07:42:18 +01:00

406 lines
15 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
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;
}
if ($nearestAfter) {
// 🔹 Suche den ersten Wert nach dem gegebenen Zeitpunkt
$values = @AC_GetLoggedValues($archiveID, $varId, $timestamp, $timestamp + 7 * 86400, 1000);
if (!empty($values)) {
foreach ($values as $v) {
if ($v['TimeStamp'] >= $timestamp) {
return floatval($v['Value']);
}
}
}
} else {
// 🔹 Suche den letzten Wert vor dem gegebenen Zeitpunkt
$values = @AC_GetLoggedValues($archiveID, $varId, $timestamp - 7 * 86400, $timestamp, 1000);
if (!empty($values)) {
return floatval($values[count($values) - 1]['Value']);
}
}
// Fallback aktuellen Variablenwert nehmen
$fallback = floatval(GetValue($varId));
IPS_LogMessage('Abrechnung', "⚠ Kein Archivwert für $varId gefunden, nutze aktuellen Wert ($fallback)");
return $fallback;
}
private function AddMeterToPDFRow($meter, $tariffs, $from, $to, $type)
{
$rows = '';
$totalCost = 0.0;
$varId = $meter['var_consumption'];
if (!IPS_VariableExists($varId)) {
IPS_LogMessage('Abrechnung', "❌ Variable {$varId} für {$meter['name']} nicht gefunden");
return ['row' => '', 'value' => 0];
}
// 🧩 Einheitstyp tolerant abgleichen
$filteredTariffs = [];
foreach ($tariffs as $t) {
$tariffType = strtolower(trim($t['unit_type'] ?? ''));
$typeNorm = strtolower(trim($type));
// Mapping-Tabelle für Synonyme
$map = [
'strombezug' => ['strom', 'strombezug', 'stromzähler', 'stromverbrauch'],
'warmwasser' => ['warmwasser', 'wasser', 'hww'],
'kaltwasser' => ['kaltwasser', 'wasser', 'kww'],
'wärme' => ['wärme', 'heizung', 'heat']
];
if (in_array($tariffType, $map[$typeNorm] ?? []) || $tariffType == $typeNorm) {
$filteredTariffs[] = $t;
}
}
// 🪵 Logging zur Kontrolle
IPS_LogMessage('Abrechnung', "🔍 Suche Tarife für Typ '$type': gefunden " . count($filteredTariffs) . " Treffer");
foreach ($filteredTariffs as $t) {
IPS_LogMessage('Abrechnung', " Tarif '{$t['unit_type']}' von {$t['start']} bis {$t['end']} | {$t['price']} Rp");
}
if (empty($filteredTariffs)) {
IPS_LogMessage('Abrechnung', "⚠ Keine passenden Tarife für $type gefunden");
return ['row' => '', 'value' => 0];
}
// Nach Startdatum sortieren
usort($filteredTariffs, function ($a, $b) {
return strtotime($a['start']) <=> strtotime($b['start']);
});
$current = $from;
while ($current < $to) {
$activeTariff = null;
$segmentStart = $current;
$segmentEnd = $to;
// Passenden Tarif für aktuellen Zeitpunkt finden
foreach ($filteredTariffs as $t) {
$tariffStart = strtotime($t['start'] . ' 00:00:00');
$tariffEnd = strtotime($t['end'] . ' 23:59:59');
if ($tariffStart === false || $tariffEnd === false) {
IPS_LogMessage('Abrechnung', "❌ Ungültiges Tarifdatum: " . json_encode($t));
continue;
}
// Tarif gilt, wenn er sich mit dem Zeitraum überschneidet
if ($tariffEnd > $segmentStart && $tariffStart < $to) {
if ($tariffStart > $segmentStart) {
$segmentEnd = min($tariffStart, $to);
$activeTariff = null;
break;
}
$segmentEnd = min($tariffEnd, $to);
$activeTariff = $t;
break;
}
}
// Kein aktiver Tarif -> Nulltarif bis zum nächsten Start
if ($activeTariff === null) {
$nextStart = $to;
foreach ($filteredTariffs as $t) {
$ts = strtotime($t['start'] . ' 00:00:00');
if ($ts > $segmentStart && $ts < $nextStart) {
$nextStart = $ts;
}
}
$segmentEnd = min($nextStart, $to);
$tariffPrice = 0.0;
$tariffLabel = 'kein Tarif';
} else {
$tariffPrice = floatval($activeTariff['price']);
$tariffLabel = number_format($tariffPrice, 2);
}
// 🔹 Archivwerte lesen
$startVal = $this->GetValueAt($varId, $segmentStart, false);
$endVal = $this->GetValueAt($varId, $segmentEnd, true);
if ($startVal === null || $endVal === null) {
IPS_LogMessage('Abrechnung', "⚠ Keine Archivwerte für {$meter['name']} ($type) von " . date('d.m.Y H:i', $segmentStart) . " bis " . date('d.m.Y H:i', $segmentEnd));
$current = $segmentEnd + 1;
continue;
}
$diff = max(0, $endVal - $startVal);
$costCHF = ($tariffPrice / 100) * $diff;
// 🪵 Logausgabe pro Abschnitt
IPS_LogMessage('Abrechnung', "{$meter['name']} ($type): " . date('d.m.Y H:i', $segmentStart) . " bis " . date('d.m.Y H:i', $segmentEnd) . " | Tarif {$tariffLabel} Rp | Verbrauch {$diff}");
// 🔸 Zeile fürs PDF
$rows .= "
<tr>
<td>{$meter['name']}</td>
<td>$type</td>
<td>" . date('d.m.Y H:i', $segmentStart) . "</td>
<td>" . date('d.m.Y H:i', $segmentEnd) . "</td>
<td>" . number_format($startVal, 2) . "</td>
<td>" . number_format($endVal, 2) . "</td>
<td>" . number_format($diff, 2) . "</td>
<td>" . number_format($tariffPrice, 2) . "</td>
<td>" . number_format($costCHF, 2) . "</td>
</tr>";
$totalCost += $costCHF;
$current = $segmentEnd + 1;
}
// Falls keine Zeile zustande kam
if ($rows == '') {
$startVal = $this->GetValueAt($varId, $from, false);
$endVal = $this->GetValueAt($varId, $to, true);
$diff = max(0, $endVal - $startVal);
$rows .= "
<tr>
<td>{$meter['name']}</td>
<td>$type</td>
<td>" . date('d.m.Y H:i', $from) . "</td>
<td>" . date('d.m.Y H:i', $to) . "</td>
<td>" . number_format($startVal, 2) . "</td>
<td>" . number_format($endVal, 2) . "</td>
<td>" . number_format($diff, 2) . "</td>
<td>0</td>
<td>0.00</td>
</tr>";
}
return ['row' => $rows, 'value' => $totalCost];
}
}