Files
Symcon_Belevo_Energiemanage…/Abrechnung/module.php
2025-11-04 17:03:21 +01:00

322 lines
12 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 = false)
{
$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) {
// Endwert: erster Wert >= Timestamp
$values = @AC_GetLoggedValues($archiveID, $varId, $timestamp, $timestamp + 86400, 1);
if ($values && count($values) > 0) return floatval($values[0]['Value']);
} else {
// Startwert: letzter Wert <= Timestamp
$values = @AC_GetLoggedValues($archiveID, $varId, 0, $timestamp, 1);
if ($values && count($values) > 0) return floatval($values[count($values)-1]['Value']);
}
// Fallback auf aktuellen Wert
$current = floatval(GetValue($varId));
IPS_LogMessage('Abrechnung', "⚠ Kein Archivwert für $varId gefunden, nehme aktuellen Wert: $current");
return $current;
}
private function AddMeterToPDFRow($meter, $tariffs, $from, $to, $type)
{
$startValTotal = $this->GetValueAt($meter['var_consumption'], $from, false);
$endValTotal = $this->GetValueAt($meter['var_consumption'], $to, true);
IPS_LogMessage('Abrechnung', "Meter {$meter['name']} ($type) von " . date('d.m.Y H:i', $from) . " bis " . date('d.m.Y H:i', $to) . ": start=$startValTotal, end=$endValTotal");
if ($startValTotal === null || $endValTotal === null) return ['row' => '', 'value' => 0];
$totalDiff = max(0, $endValTotal - $startValTotal);
if ($totalDiff <= 0) return ['row' => '', 'value' => 0];
$rows = '';
$totalCost = 0.0;
$foundTariff = false;
foreach ($tariffs as $t) {
// Einheit auflösen (ignoriere Groß/Kleinschreibung, evtl. Synonyme)
$unitType = strtolower($t['unit_type']);
$meterType = strtolower($type);
if ($meterType === 'strombezug') $meterType = 'strom';
if ($meterType === 'warmwasser') $meterType = 'wasser';
if ($unitType !== $meterType) continue;
$tariffStart = strtotime($t['start']);
$tariffEnd = strtotime($t['end']);
// Überschneidung
$sectionStart = max($from, $tariffStart);
$sectionEnd = min($to, $tariffEnd);
if ($sectionEnd <= $sectionStart) continue;
$sectionFraction = ($sectionEnd - $sectionStart) / ($to - $from);
$sectionDiff = $totalDiff * $sectionFraction;
$sectionStartVal = $startValTotal + $totalDiff * (($sectionStart - $from) / ($to - $from));
$sectionEndVal = $sectionStartVal + $sectionDiff;
$tariffRp = floatval($t['price']); // Preis in Rappen pro Einheit
$costCHF = ($tariffRp / 100) * $sectionDiff;
$rows .= "
<tr>
<td>{$meter['name']}</td>
<td>$type</td>
<td>" . date('d.m.Y H:i', $sectionStart) . "</td>
<td>" . date('d.m.Y H:i', $sectionEnd) . "</td>
<td>" . number_format($sectionStartVal, 2) . "</td>
<td>" . number_format($sectionEndVal, 2) . "</td>
<td>" . number_format($sectionDiff, 2) . "</td>
<td>" . number_format($tariffRp, 2) . "</td>
<td>" . number_format($costCHF, 2) . "</td>
</tr>";
$totalCost += $costCHF;
$foundTariff = true;
}
// Falls kein passender Tarif gefunden
if (!$foundTariff) {
$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($startValTotal, 2) . "</td>
<td>" . number_format($endValTotal, 2) . "</td>
<td>" . number_format($totalDiff, 2) . "</td>
<td>0</td>
<td>0.00</td>
</tr>";
}
return ['row' => $rows, 'value' => $totalCost];
}
}