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

354 lines
13 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 + 864000, 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)
{
$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];
}
// Filtere Tarife für diesen Typ (z. B. "Strombezug" oder "Warmwasser")
$tariffs = array_filter($tariffs, function ($t) use ($type) {
return strtolower($t['unit_type']) == strtolower($type);
});
if (empty($tariffs)) {
IPS_LogMessage('Abrechnung', "⚠ Keine passenden Tarife für $type gefunden");
return ['row' => '', 'value' => 0];
}
// Sortiere Tarife chronologisch
usort($tariffs, function ($a, $b) {
return strtotime($a['start']) <=> strtotime($b['start']);
});
$current = $from;
while ($current < $to) {
// passenden Tarif für aktuellen Zeitpunkt finden
$activeTariff = null;
foreach ($tariffs as $t) {
$tariffStart = strtotime($t['start']);
$tariffEnd = strtotime($t['end']);
if ($current >= $tariffStart && $current < $tariffEnd) {
$activeTariff = $t;
break;
}
}
// wenn kein aktiver Tarif gefunden → Nulltarif bis nächster Start oder Rechnungsende
if ($activeTariff === null) {
$nextTariffStart = $to;
foreach ($tariffs as $t) {
$ts = strtotime($t['start']);
if ($ts > $current && $ts < $nextTariffStart) {
$nextTariffStart = $ts;
}
}
$segmentStart = $current;
$segmentEnd = min($nextTariffStart, $to);
$segmentTariff = 0.0;
} else {
$segmentStart = $current;
$segmentEnd = min(strtotime($activeTariff['end']), $to);
$segmentTariff = floatval($activeTariff['price']);
}
// Zählerstände direkt aus Archiv
$startVal = $this->GetValueAt($varId, $segmentStart, false);
$endVal = $this->GetValueAt($varId, $segmentEnd, true);
if ($startVal === null || $endVal === null) {
IPS_LogMessage('Abrechnung', "⚠ Keine Werte im Archiv gefunden für {$meter['name']} ($type)");
$current = $segmentEnd;
continue;
}
$diff = max(0, $endVal - $startVal);
$costCHF = ($segmentTariff / 100) * $diff;
$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($segmentTariff, 2) . "</td>
<td>" . number_format($costCHF, 2) . "</td>
</tr>";
$totalCost += $costCHF;
$current = $segmentEnd;
}
if ($rows == '') {
// Falls keine Tarifzeile ermittelt wurde
$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];
}
}