855 lines
29 KiB
PHP
855 lines
29 KiB
PHP
<?php
|
||
declare(strict_types=1);
|
||
|
||
include_once __DIR__ . '/libs/vendor/autoload.php'; // TCPDF via Composer
|
||
|
||
/**
|
||
* Eigene TCPDF-Klasse mit Logo im Header und Text in der Fusszeile
|
||
*/
|
||
class InvoicePDF extends TCPDF
|
||
{
|
||
public $logoFile = null;
|
||
public $footerText = '';
|
||
|
||
public function Header()
|
||
{
|
||
if ($this->logoFile !== null && file_exists($this->logoFile)) {
|
||
// x, y, width in mm
|
||
$this->Image($this->logoFile, 15, 10, 40);
|
||
$this->SetY(28);
|
||
} else {
|
||
$this->SetY(15);
|
||
}
|
||
}
|
||
|
||
public function Footer()
|
||
{
|
||
$this->SetY(-15);
|
||
$this->SetFont('dejavusans', '', 8);
|
||
$this->Cell(0, 10, $this->footerText, 0, 0, 'C');
|
||
}
|
||
}
|
||
|
||
class Abrechnung extends IPSModule
|
||
{
|
||
public function Create()
|
||
{
|
||
parent::Create();
|
||
|
||
$this->RegisterPropertyString('Users', '[]');
|
||
$this->RegisterPropertyString('PowerMeters', '[]');
|
||
$this->RegisterPropertyString('WaterMeters', '[]');
|
||
$this->RegisterPropertyString('Tariffs', '[]');
|
||
$this->RegisterPropertyInteger('LogoMediaID', 0);
|
||
$this->RegisterPropertyString('PropertyText', 'Liegenschaft');
|
||
$this->RegisterPropertyString('FooterText', 'Belevo AG • 6122 Menznau • www.belevo.ch');
|
||
|
||
$this->RegisterVariableInteger('FromDate', 'Startdatum', '~UnixTimestamp', 1);
|
||
$this->RegisterVariableInteger('ToDate', 'Enddatum', '~UnixTimestamp', 2);
|
||
$this->RegisterVariableString('LastResult', 'Letzte Abrechnung', '', 3);
|
||
$this->EnableAction('FromDate');
|
||
$this->EnableAction('ToDate');
|
||
|
||
$this->RegisterScript(
|
||
'StartBilling',
|
||
'Abrechnung starten',
|
||
"<?php IPS_RequestAction(" . $this->InstanceID . ", 'StartBilling', ''); ?>"
|
||
);
|
||
$this->RegisterMediaDocument('InvoicePDF', 'Letzte Rechnung', 'pdf');
|
||
}
|
||
|
||
public function ApplyChanges()
|
||
{
|
||
parent::ApplyChanges();
|
||
}
|
||
|
||
private function RegisterMediaDocument($Ident, $Name, $Extension, $Position = 0)
|
||
{
|
||
$mid = @IPS_GetObjectIDByIdent($Ident, $this->InstanceID);
|
||
if ($mid === false) {
|
||
$mid = IPS_CreateMedia(5);
|
||
IPS_SetParent($mid, $this->InstanceID);
|
||
IPS_SetIdent($mid, $Ident);
|
||
IPS_SetName($mid, $Name);
|
||
IPS_SetPosition($mid, $Position);
|
||
IPS_SetMediaFile($mid, 'media/' . $mid . '.' . $Extension, false);
|
||
}
|
||
}
|
||
|
||
public function RequestAction($Ident, $Value)
|
||
{
|
||
switch ($Ident) {
|
||
case 'FromDate':
|
||
case 'ToDate':
|
||
SetValue($this->GetIDForIdent($Ident), $Value);
|
||
break;
|
||
|
||
case 'StartBilling':
|
||
try {
|
||
$pdfContent = $this->GenerateInvoices();
|
||
if (!$pdfContent) {
|
||
echo "❌ Fehler bei der PDF-Erstellung";
|
||
return;
|
||
}
|
||
|
||
$mediaID = $this->GetIDForIdent('InvoicePDF');
|
||
IPS_SetMediaContent($mediaID, base64_encode($pdfContent));
|
||
SetValue($this->GetIDForIdent('LastResult'), 'Abrechnung vom ' . date('d.m.Y H:i'));
|
||
echo "✅ PDF erfolgreich erstellt.";
|
||
} catch (Throwable $e) {
|
||
echo "❌ Ausnahmefehler: " . $e->getMessage();
|
||
}
|
||
break;
|
||
}
|
||
}
|
||
|
||
// ====================== PDF-Erstellung ======================
|
||
|
||
public function GenerateInvoices()
|
||
{
|
||
$from = GetValue($this->GetIDForIdent('FromDate'));
|
||
$to = GetValue($this->GetIDForIdent('ToDate'));
|
||
|
||
$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')) {
|
||
return false;
|
||
}
|
||
|
||
// Stromkosten einmal für alle User berechnen (15-Minuten-Logik)
|
||
$this->CalculateAllPowerCosts($power, $tariffs, $from, $to);
|
||
|
||
// Logo & Fusszeile vorbereiten
|
||
$logoFile = $this->GetLogoFile();
|
||
$footerText = $this->ReadPropertyString('FooterText');
|
||
|
||
// PDF-Objekt
|
||
$pdf = new InvoicePDF('P', 'mm', 'A4', true, 'UTF-8', false);
|
||
$pdf->logoFile = $logoFile;
|
||
$pdf->footerText = $footerText;
|
||
|
||
$pdf->SetCreator('IPSymcon Abrechnung');
|
||
// Oben mehr Platz für Logo lassen
|
||
$pdf->SetMargins(15, 35, 15);
|
||
$pdf->SetAutoPageBreak(true, 20);
|
||
$pdf->SetFont('dejavusans', '', 8);
|
||
|
||
foreach ($users as $user) {
|
||
$pdf->AddPage();
|
||
$this->BuildUserInvoice($pdf, $user, $power, $water, $tariffs, $from, $to);
|
||
}
|
||
|
||
return $pdf->Output('Abrechnung.pdf', 'S');
|
||
}
|
||
|
||
private function BuildUserInvoice($pdf, $user, $power, $water, $tariffs, $from, $to)
|
||
{
|
||
// Kopfbereich
|
||
$html = "
|
||
<h1 style='text-align:center; margin-bottom:0;'>Elektro- und Nebenkostenabrechnung</h1>
|
||
<h4 style='text-align:center; margin-bottom:0;'>Betrifft Liegenschaft: " . $this->ReadPropertyString("PropertyText") . " </h4>
|
||
|
||
<hr>
|
||
|
||
<table width='100%' style='font-size:10px;'>
|
||
<tr>
|
||
<td width='60%'>
|
||
<strong>Zählpunkte:</strong><br>";
|
||
|
||
// Alle Zählerpunkte des Users auflisten
|
||
foreach ($power as $m) {
|
||
if ($m['user_id'] == $user['id']) {
|
||
$html .= htmlspecialchars($m['name']) . "<br>";
|
||
}
|
||
}
|
||
foreach ($water as $m) {
|
||
if ($m['user_id'] == $user['id']) {
|
||
$html .= htmlspecialchars($m['name']) . "<br>";
|
||
}
|
||
}
|
||
|
||
$html .= "
|
||
</td>
|
||
<td width='40%'>
|
||
<strong>Rechnungsadresse:</strong><br>
|
||
{$user['name']}<br>
|
||
{$user['address']}<br>
|
||
{$user['city']}<br>
|
||
</td>
|
||
</tr>
|
||
</table>
|
||
<p style='font-size:8px; margin-top:10px;'>
|
||
<strong>Abrechnungszeitraum:</strong> " . date('d.m.Y', $from) . " – " . date('d.m.Y', $to) . "
|
||
</p>
|
||
<hr><br>
|
||
";
|
||
|
||
// ========================= Elektrizität =========================
|
||
$html .= "<br><h2 style='margin-bottom:3px;margin-top:10px; font-size:10px;'>Elektrizität</h2>";
|
||
|
||
$powerResult = $this->GetCalculatedPowerCosts($user['id']);
|
||
$html .= $powerResult['html'];
|
||
$totalPower = $powerResult['sum'];
|
||
|
||
// Elektrizitätstarife anzeigen
|
||
$appliedTariffs = $this->CollectTariffsForUser($tariffs, ['Netztarif', 'Solartarif', 'Einspeisetarif']);
|
||
if (!empty($appliedTariffs)) {
|
||
$html .= "<p style='font-size:8px; margin-top:4px;'><strong>Angewendete Elektrizitätstarife:</strong></p>
|
||
<ul style='font-size:8px;'>";
|
||
foreach ($appliedTariffs as $t) {
|
||
$html .= "<li>"
|
||
. date('d.m.Y', $t['start']) . " – " . date('d.m.Y', $t['end'])
|
||
. " — <strong>" . number_format($t['price'], 2) . " Rp/kWh</strong>"
|
||
. "</li>";
|
||
}
|
||
$html .= "</ul><br><hr><br>";
|
||
}
|
||
|
||
// ========================= Nebenkosten =========================
|
||
$html .= "<br><h2 style='margin-bottom:3px; font-size:10px;'>Nebenkosten</h2>";
|
||
|
||
$additionalResult = $this->CalculateAdditionalCosts($water, $tariffs, $user['id'], $from, $to);
|
||
$html .= $additionalResult['html'];
|
||
$totalAdditional = $additionalResult['sum'];
|
||
|
||
// ========================= Gesamttotal =========================
|
||
$grandTotal = $totalPower + $totalAdditional;
|
||
$html .= "
|
||
<h2 style='margin:0 0 0 0; font-size:11px;'>
|
||
Gesamttotal: <strong>CHF " . number_format($grandTotal, 2) . ".-</strong>
|
||
</h2>
|
||
";
|
||
$pdf->writeHTML($html, true, false, true, false, '');
|
||
}
|
||
|
||
// ====================== Stromkosten (15-Minuten, alle User) ======================
|
||
|
||
private function CalculateAllPowerCosts($powerMeters, $tariffs, $from, $to)
|
||
{
|
||
$this->powerCostCache = [];
|
||
|
||
// Zähler je User aufbauen
|
||
$metersByUser = [];
|
||
foreach ($powerMeters as $m) {
|
||
if (!isset($m['user_id'])) {
|
||
continue;
|
||
}
|
||
|
||
$userId = (int)$m['user_id'];
|
||
$name = $m['name'] ?? ('Meter_' . $m['user_id']);
|
||
|
||
if (!isset($metersByUser[$userId])) {
|
||
$metersByUser[$userId] = [];
|
||
}
|
||
$metersByUser[$userId][$name] = [
|
||
'importVar' => $m['var_consumption'] ?? null,
|
||
'exportVar' => $m['var_feed'] ?? null
|
||
];
|
||
|
||
if (!isset($this->powerCostCache[$userId])) {
|
||
$this->powerCostCache[$userId] = [];
|
||
}
|
||
$this->powerCostCache[$userId][$name] = [
|
||
'name' => $name,
|
||
'imp' => 0.0,
|
||
'exp' => 0.0,
|
||
'solar_bezug' => 0.0,
|
||
'netz_bezug' => 0.0,
|
||
'solareinspeisung' => 0.0,
|
||
'solarverkauf' => 0.0,
|
||
'cost_solar' => 0.0,
|
||
'cost_grid' => 0.0,
|
||
'rev_feedin' => 0.0,
|
||
'rev_zev' => 0.0
|
||
];
|
||
}
|
||
|
||
if (empty($metersByUser)) {
|
||
return;
|
||
}
|
||
|
||
// 15-Minuten-Schritte
|
||
for ($ts = $from; $ts < $to; $ts += 900) {
|
||
$slotEnd = min($to, $ts + 900);
|
||
|
||
// Tarife (gleich für alle User)
|
||
$pGrid = $this->getTariffPriceAt($tariffs, ['Netztarif'], $ts);
|
||
$pSolar = $this->getTariffPriceAt($tariffs, ['Solartarif'], $ts);
|
||
$pFeed = $this->getTariffPriceAt($tariffs, ['Einspeisetarif'], $ts);
|
||
|
||
// ==========================================================
|
||
// 1) GLOBAL DELTAS EINMAL BERECHNEN
|
||
// ==========================================================
|
||
$globalImpTotal = 0.0;
|
||
$globalExpTotal = 0.0;
|
||
$globalSlot = [];
|
||
|
||
foreach ($powerMeters as $m) {
|
||
|
||
$name = $m['name'];
|
||
$impDelta = 0.0;
|
||
$expDelta = 0.0;
|
||
|
||
// IMPORT
|
||
if (!empty($m['var_consumption']) && IPS_VariableExists((int)$m['var_consumption'])) {
|
||
$impDelta = $this->getDeltaFromArchive((int)$m['var_consumption'], $ts, $slotEnd);
|
||
}
|
||
|
||
// EXPORT
|
||
if (!empty($m['var_feed']) && IPS_VariableExists((int)$m['var_feed'])) {
|
||
$expDelta = $this->getDeltaFromArchive((int)$m['var_feed'], $ts, $slotEnd);
|
||
}
|
||
|
||
if ($impDelta > 0.0 || $expDelta > 0.0) {
|
||
$globalSlot[$name] = [
|
||
'imp' => $impDelta,
|
||
'exp' => $expDelta,
|
||
'user_id' => (int)$m['user_id']
|
||
];
|
||
|
||
$globalImpTotal += $impDelta;
|
||
$globalExpTotal += $expDelta;
|
||
}
|
||
}
|
||
|
||
// Wenn keine Imp/Exp vorhanden → nächster Slot
|
||
if ($globalImpTotal == 0.0 && $globalExpTotal == 0.0) {
|
||
continue;
|
||
}
|
||
|
||
// ==========================================================
|
||
// 2) Verhältnis PV / Netz
|
||
// ==========================================================
|
||
if ($globalImpTotal == 0.0 && $globalExpTotal > 0.0) {
|
||
// nur Export → PV deckt alles
|
||
$ratio = 0.0;
|
||
$pvCoversAll = true;
|
||
|
||
} elseif ($globalExpTotal == 0.0 && $globalImpTotal > 0.0) {
|
||
// nur Import → Netz deckt alles
|
||
$ratio = 0.0;
|
||
$pvCoversAll = false;
|
||
|
||
} elseif ($globalImpTotal <= $globalExpTotal) {
|
||
// PV produziert genug
|
||
$ratio = $globalExpTotal ? ($globalImpTotal / $globalExpTotal) : 0.0;
|
||
$pvCoversAll = true;
|
||
|
||
} else {
|
||
// Netzbezug notwendig
|
||
$ratio = $globalImpTotal ? ($globalExpTotal / $globalImpTotal) : 0.0;
|
||
$pvCoversAll = false;
|
||
}
|
||
|
||
// ==========================================================
|
||
// 3) PRO USER VERTEILEN
|
||
// ==========================================================
|
||
foreach ($globalSlot as $name => $v) {
|
||
$userId = $v['user_id'];
|
||
|
||
// Falls User nicht im Cache ist → ignorieren
|
||
if (!isset($this->powerCostCache[$userId][$name])) {
|
||
continue;
|
||
}
|
||
|
||
$imp = $v['imp'];
|
||
$exp = $v['exp'];
|
||
|
||
// Totale speichern
|
||
$this->powerCostCache[$userId][$name]['imp'] += $imp;
|
||
$this->powerCostCache[$userId][$name]['exp'] += $exp;
|
||
|
||
// PV deckt alles
|
||
if ($pvCoversAll) {
|
||
$this->powerCostCache[$userId][$name]['solar_bezug'] += $imp;
|
||
$this->powerCostCache[$userId][$name]['solareinspeisung'] += (1 - $ratio) * $exp;
|
||
$this->powerCostCache[$userId][$name]['solarverkauf'] += $ratio * $exp;
|
||
|
||
if ($pSolar !== null) {
|
||
$this->powerCostCache[$userId][$name]['cost_solar'] += ($imp * $pSolar) / 100.0;
|
||
}
|
||
if ($pFeed !== null) {
|
||
$this->powerCostCache[$userId][$name]['rev_feedin'] += ((1 - $ratio) * $exp * $pFeed) / 100.0;
|
||
$this->powerCostCache[$userId][$name]['rev_zev'] += ($ratio * $exp * $pSolar) / 100.0;
|
||
}
|
||
|
||
// Netz deckt einen Teil
|
||
} else {
|
||
$this->powerCostCache[$userId][$name]['solar_bezug'] += $ratio * $imp;
|
||
$this->powerCostCache[$userId][$name]['netz_bezug'] += (1 - $ratio) * $imp;
|
||
$this->powerCostCache[$userId][$name]['solarverkauf'] += $exp;
|
||
|
||
if ($pGrid !== null) {
|
||
$this->powerCostCache[$userId][$name]['cost_grid'] += ((1 - $ratio) * $imp * $pGrid) / 100.0;
|
||
}
|
||
if ($pSolar !== null) {
|
||
$this->powerCostCache[$userId][$name]['cost_solar'] += ($ratio * $imp * $pSolar) / 100.0;
|
||
}
|
||
if ($pFeed !== null) {
|
||
$this->powerCostCache[$userId][$name]['rev_zev'] += ($exp * $pSolar) / 100.0;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
}
|
||
private function GetCalculatedPowerCosts($userId)
|
||
{
|
||
$html = "
|
||
<style>
|
||
table.powercost {
|
||
width: 100%;
|
||
border-collapse: collapse;
|
||
font-size: 10px;
|
||
}
|
||
table.powercost th {
|
||
background-color: #444;
|
||
color: white;
|
||
padding: 4px;
|
||
text-align: center;
|
||
font-weight: bold;
|
||
border: 1px solid #777;
|
||
}
|
||
table.powercost td {
|
||
padding: 3px 4px;
|
||
border: 1px solid #ccc;
|
||
}
|
||
.row-even { background-color: #f7f7f7; }
|
||
.row-odd { background-color: #ffffff; }
|
||
.row-empty { background-color: #e0e0e0; height: 5px; }
|
||
|
||
/* Neue Formatierungen */
|
||
.num {
|
||
text-align: right;
|
||
font-family: monospace;
|
||
width: 90px;
|
||
}
|
||
.subtotal-row {
|
||
border-top: 1px solid black;
|
||
font-weight: bold;
|
||
}
|
||
.total-row {
|
||
border-top: 3px double black;
|
||
font-weight: bold;
|
||
background-color: #ddd;
|
||
}
|
||
</style>
|
||
|
||
<table class='powercost'>
|
||
<tr>
|
||
<th>ID</th>
|
||
<th>Import (kWh)</th>
|
||
<th>Export (kWh)</th>
|
||
<th>ZEV-Haus (kWh)</th>
|
||
<th>Netz-Haus (kWh)</th>
|
||
<th>Solar-Netz (kWh)</th>
|
||
<th>Solar-ZEV (kWh)</th>
|
||
<th>Kauf Solar (CHF)</th>
|
||
<th>Kauf Netz (CHF)</th>
|
||
<th>Verkauf Netz(CHF)</th>
|
||
<th>Verkauf ZEV(CHF)</th>
|
||
<th>Total CHF</th>
|
||
</tr>";
|
||
|
||
if (empty($this->powerCostCache) || !isset($this->powerCostCache[$userId])) {
|
||
$html .= "<tr><td colspan='12' align='center'>Keine Stromzähler für diesen Benutzer</td></tr></table>";
|
||
return ['html' => $html, 'sum' => 0.0];
|
||
}
|
||
|
||
$sum = 0.0;
|
||
$rowIndex = 0;
|
||
|
||
foreach ($this->powerCostCache[$userId] as $name => $a) {
|
||
|
||
$subtotal = $a['cost_grid'] + $a['cost_solar'] - ($a['rev_feedin'] + $a['rev_zev']);
|
||
$sum += $subtotal;
|
||
|
||
$rowClass = ($rowIndex % 2 === 0) ? "row-even" : "row-odd";
|
||
$rowIndex++;
|
||
|
||
// Datenzeile
|
||
$html .= "<tr class='{$rowClass}'>
|
||
<td>{$a['name']}</td>
|
||
<td class='num' style='background-color:#f0f0f0; text-align: right;'>" . number_format($a['imp'], 3, '.', "'") . "</td>
|
||
<td class='num' text-align='right'>" . number_format($a['exp'], 3, '.', "'") . "</td>
|
||
<td class='num' text-align='right'>" . number_format($a['solar_bezug'], 3, '.', "'") . "</td>
|
||
<td class='num' text-align='right'>" . number_format($a['netz_bezug'], 3, '.', "'") . "</td>
|
||
<td class='num' text-align='right'>" . number_format($a['solareinspeisung'], 3, '.', "'") . "</td>
|
||
<td class='num' text-align='right'>" . number_format($a['solarverkauf'], 3, '.', "'") . "</td>
|
||
<td class='num' text-align='right'>" . number_format($a['cost_solar'], 2, '.', "'") . "</td>
|
||
<td class='num' text-align='right'>" . number_format($a['cost_grid'], 2, '.', "'") . "</td>
|
||
<td class='num' text-align='right'>" . number_format($a['rev_feedin'], 2, '.', "'") . "</td>
|
||
<td class='num' text-align='right'>" . number_format($a['rev_zev'], 2, '.', "'") . "</td>
|
||
<td class='num underline' text-align='right'><strong>" . number_format($subtotal, 2, '.', "'") . "</strong></td>
|
||
</tr>";
|
||
|
||
// Leerzeile
|
||
$html .= "<tr class='row-empty'><td colspan='12'></td></tr>";
|
||
}
|
||
|
||
// Gesamttotal
|
||
$html .= "
|
||
<tr class='total-row'>
|
||
<td colspan='11' text-align='right'><b>Total</b></td>
|
||
<td align='right'></td>
|
||
<td align='right'></td>
|
||
<td align='right'></td>
|
||
<td align='right'></td>
|
||
<td align='right'></td>
|
||
<td align='right'></td>
|
||
<td align='right'></td>
|
||
<td align='right'></td>
|
||
<td align='right'></td>
|
||
<td align='right'></td>
|
||
<td class='num double-underline' text-align='right'><strong>" . number_format($sum, 2, '.', "'") . "</strong></td>
|
||
</tr>
|
||
</table>";
|
||
|
||
return ['html' => $html, 'sum' => $sum];
|
||
}
|
||
|
||
// ====================== Nebenkosten Wasser/Wärme ======================
|
||
|
||
private function CalculateAdditionalCosts($waterMeters, $tariffs, $userId, $from, $to)
|
||
{
|
||
$html = "
|
||
<table border='1' cellspacing='0' cellpadding='2' width='100%' style='font-size:4px;'>
|
||
<tr style='background-color:#f0f0f0;'>
|
||
<th>ID</th>
|
||
<th>Typ</th>
|
||
<th>Datum von</th>
|
||
<th>Datum bis</th>
|
||
<th>Zähler Start</th>
|
||
<th>Zähler Ende</th>
|
||
<th>Verbrauch</th>
|
||
<th>Tarif</th>
|
||
<th>Kosten CHF</th>
|
||
</tr>";
|
||
|
||
$total = 0.0;
|
||
$usedTariffs = [];
|
||
|
||
foreach ($waterMeters as $m) {
|
||
if ($m['user_id'] != $userId) {
|
||
continue;
|
||
}
|
||
$type = $m['meter_type'] ?? 'Warmwasser';
|
||
$cost = $this->AddMeterToPDFRow($m, $tariffs, $from, $to, $type);
|
||
$html .= $cost['row'];
|
||
$total += $cost['value'];
|
||
$usedTariffs = array_merge($usedTariffs, $cost['tariffs']);
|
||
}
|
||
|
||
$html .= "<tr style='background-color:#f9f9f9; font-weight:bold;'>
|
||
<td colspan='25' align='right'><b>Total</b></td>
|
||
<td align='right'></td>
|
||
<td align='right'></td>
|
||
<td align='right'></td>
|
||
<td align='right'></td>
|
||
<td align='right'></td>
|
||
<td align='right'></td>
|
||
<td align='right'></td>
|
||
<td align='right'><b>CHF " . number_format($total, 2) . ".-</b></td>
|
||
</tr>";
|
||
|
||
if (!empty($usedTariffs)) {
|
||
$html .= "<p><strong>Angewendete Nebenkostentarife</strong></p><ul>";
|
||
foreach ($usedTariffs as $t) {
|
||
$html .= "<li>" . date('d.m.Y', $t['start']) . " – " . date('d.m.Y', $t['end']) . " — <strong> " .
|
||
number_format($t['price'], 2) . " Rp/m3</strong></li>";
|
||
}
|
||
$html .= "</ul><br><hr>";
|
||
}
|
||
|
||
return ['html' => $html, 'sum' => $total];
|
||
}
|
||
|
||
private function AddMeterToPDFRow($meter, $tariffs, $from, $to, $type)
|
||
{
|
||
$rows = '';
|
||
$totalCost = 0.0;
|
||
$usedTariffs = [];
|
||
$varId = $meter['var_consumption'];
|
||
|
||
if (!IPS_VariableExists($varId)) {
|
||
return ['row' => '', 'value' => 0, 'tariffs' => []];
|
||
}
|
||
|
||
// Relevante Tarife nach Typ filtern
|
||
$filteredTariffs = array_filter($tariffs, fn($t) =>
|
||
strtolower(trim($t['unit_type'] ?? '')) === strtolower(trim($type))
|
||
);
|
||
|
||
$activeTariff = null;
|
||
foreach ($filteredTariffs as $t) {
|
||
$startTs = $this->toUnixTs($t['start'], false);
|
||
$endTs = $this->toUnixTs($t['end'], true);
|
||
if ($startTs === null || $endTs === null) {
|
||
continue;
|
||
}
|
||
// Tarif überlappt den Abrechnungszeitraum
|
||
if ($endTs < $from || $startTs > $to) {
|
||
continue;
|
||
}
|
||
$activeTariff = [
|
||
'start' => $startTs,
|
||
'end' => $endTs,
|
||
'price' => (float)$t['price']
|
||
];
|
||
$usedTariffs[] = $activeTariff;
|
||
break; // erster passender Tarif reicht
|
||
}
|
||
|
||
$tariffPrice = $activeTariff ? $activeTariff['price'] : 0.0;
|
||
|
||
// Verbrauch = letzter Wert vor/gleich "to" minus letzter Wert vor/gleich "from"
|
||
$startValue = $this->GetValueAt($varId, $from, false);
|
||
$endValue = $this->GetValueAt($varId, $to, false);
|
||
|
||
if ($startValue === null || $endValue === null) {
|
||
return ['row' => '', 'value' => 0, 'tariffs' => []];
|
||
}
|
||
|
||
$verbrauch = max(0, $endValue - $startValue);
|
||
$kosten = round(($tariffPrice / 100) * $verbrauch, 2);
|
||
$totalCost = $kosten;
|
||
|
||
$rows .= "<tr>
|
||
<td>{$meter['name']}</td>
|
||
<td>{$type}</td>
|
||
<td align='center'>" . date('d.m.Y', $from) . "</td>
|
||
<td align='center'>" . date('d.m.Y', $to) . "</td>
|
||
<td align='right'>" . number_format($startValue, 2) . "</td>
|
||
<td align='right'>" . number_format($endValue, 2) . "</td>
|
||
<td align='right'>" . number_format($verbrauch, 2) . "</td>
|
||
<td align='right'>" . number_format($tariffPrice, 2) . "</td>
|
||
<td align='right'>" . number_format($kosten, 2) . "</td>
|
||
</tr>";
|
||
|
||
if ($rows === '') {
|
||
return ['row' => '', 'value' => 0, 'tariffs' => []];
|
||
}
|
||
|
||
return ['row' => $rows, 'value' => $totalCost, 'tariffs' => array_values($usedTariffs)];
|
||
}
|
||
|
||
// ====================== Hilfsfunktionen ======================
|
||
|
||
private function GetValueAt($varId, $timestamp, $nearestAfter = true)
|
||
{
|
||
$archiveID = @IPS_GetInstanceListByModuleID('{43192F0B-135B-4CE7-A0A7-1475603F3060}')[0];
|
||
if (!$archiveID || !IPS_VariableExists($varId)) {
|
||
return null;
|
||
}
|
||
|
||
if ($nearestAfter) {
|
||
// Erster Wert NACH oder GENAU ab Timestamp
|
||
$values = @AC_GetLoggedValues(
|
||
$archiveID,
|
||
$varId,
|
||
$timestamp, // start
|
||
time(), // end
|
||
1 // LIMIT
|
||
);
|
||
} else {
|
||
// Letzter Wert DAVOR oder GENAU bis Timestamp
|
||
$values = @AC_GetLoggedValues(
|
||
$archiveID,
|
||
$varId,
|
||
0, // start
|
||
$timestamp, // end
|
||
1 // LIMIT
|
||
);
|
||
}
|
||
|
||
if (!empty($values)) {
|
||
return (float)$values[0]['Value'];
|
||
}
|
||
|
||
// Fallback → Live-Wert
|
||
return (float)GetValue($varId);
|
||
}
|
||
|
||
|
||
private function getDeltaFromArchive(int $varId, int $tStart, int $tEnd): float
|
||
{
|
||
// Werte holen
|
||
$startValue = $this->GetValueAt($varId, $tStart, false);
|
||
$endValue = $this->GetValueAt($varId, $tEnd, false);
|
||
|
||
if ($startValue === null || $endValue === null) {
|
||
return 0.0;
|
||
}
|
||
|
||
// Delta berechnen
|
||
$diff = $endValue - $startValue;
|
||
|
||
if ($diff < 0) {
|
||
$diff = 0.0;
|
||
}
|
||
|
||
return (float)$diff;
|
||
}
|
||
|
||
private function toUnixTs($val, $endOfDay = false)
|
||
{
|
||
if (is_int($val)) {
|
||
return $val;
|
||
}
|
||
if (is_numeric($val)) {
|
||
return (int)$val;
|
||
}
|
||
|
||
if (is_string($val)) {
|
||
$s = trim($val);
|
||
if ($s !== '' && $s[0] === '{') {
|
||
$obj = json_decode($s, true);
|
||
if (isset($obj['year'], $obj['month'], $obj['day'])) {
|
||
$time = $endOfDay ? '23:59:59' : '00:00:00';
|
||
return strtotime(sprintf(
|
||
'%04d-%02d-%02d %s',
|
||
$obj['year'],
|
||
$obj['month'],
|
||
$obj['day'],
|
||
$time
|
||
));
|
||
}
|
||
}
|
||
$ts = strtotime($s);
|
||
return $ts === false ? null : $ts;
|
||
}
|
||
return null;
|
||
}
|
||
|
||
private function readDelta($varId, $tStart, $tEnd)
|
||
{
|
||
if (!is_int($tStart)) {
|
||
$tStart = strtotime($tStart);
|
||
}
|
||
if (!is_int($tEnd)) {
|
||
$tEnd = strtotime($tEnd);
|
||
}
|
||
|
||
$archiveID = @IPS_GetInstanceListByModuleID('{43192F0B-135B-4CE7-A0A7-1475603F3060}')[0];
|
||
if (!$archiveID || !IPS_VariableExists($varId)) {
|
||
return 0.0;
|
||
}
|
||
|
||
$values = @AC_GetLoggedValues($archiveID, $varId, $tStart - 86400, $tEnd + 86400, 0);
|
||
if (empty($values)) {
|
||
return 0.0;
|
||
}
|
||
|
||
usort($values, fn($a, $b) => (int)$a['TimeStamp'] <=> (int)$b['TimeStamp']);
|
||
$vStart = null;
|
||
$vEnd = null;
|
||
|
||
foreach ($values as $v) {
|
||
if ($v['TimeStamp'] <= $tStart) {
|
||
$vStart = $v['Value'];
|
||
}
|
||
if ($v['TimeStamp'] <= $tEnd) {
|
||
$vEnd = $v['Value'];
|
||
}
|
||
if ($v['TimeStamp'] > $tEnd) {
|
||
break;
|
||
}
|
||
}
|
||
|
||
if ($vStart === null) {
|
||
$vStart = (float)GetValue($varId);
|
||
}
|
||
if ($vEnd === null) {
|
||
$vEnd = (float)GetValue($varId);
|
||
}
|
||
|
||
$diff = $vEnd - $vStart;
|
||
return ($diff < 0) ? 0.0 : $diff;
|
||
}
|
||
|
||
private function getTariffPriceAt($tariffs, $typeSynonyms, $ts)
|
||
{
|
||
$wanted = array_map('strtolower', $typeSynonyms);
|
||
$cands = [];
|
||
|
||
foreach ($tariffs as $t) {
|
||
$u = strtolower(trim($t['unit_type'] ?? ''));
|
||
if (!in_array($u, $wanted, true)) {
|
||
continue;
|
||
}
|
||
|
||
$s = $this->toUnixTs($t['start'], false);
|
||
$e = $this->toUnixTs($t['end'], true);
|
||
if (!$s || !$e) {
|
||
continue;
|
||
}
|
||
if ($s <= $ts && $ts <= $e) {
|
||
$cands[] = (float)$t['price'];
|
||
}
|
||
}
|
||
|
||
if (empty($cands)) {
|
||
return null;
|
||
}
|
||
return end($cands);
|
||
}
|
||
|
||
private function CollectTariffsForUser($tariffs, $types)
|
||
{
|
||
$result = [];
|
||
$wanted = array_map('strtolower', $types);
|
||
|
||
foreach ($tariffs as $t) {
|
||
$type = strtolower(trim($t['unit_type'] ?? ''));
|
||
if (!in_array($type, $wanted, true)) {
|
||
continue;
|
||
}
|
||
|
||
$start = $this->toUnixTs($t['start'], false);
|
||
$end = $this->toUnixTs($t['end'], true);
|
||
|
||
$result[] = [
|
||
'start' => $start,
|
||
'end' => $end,
|
||
'price' => (float)$t['price']
|
||
];
|
||
}
|
||
|
||
return $result;
|
||
}
|
||
|
||
private function GetLogoFile()
|
||
{
|
||
$mediaID = (int)$this->ReadPropertyInteger('LogoMediaID');
|
||
if ($mediaID <= 0 || !IPS_MediaExists($mediaID)) {
|
||
return null;
|
||
}
|
||
|
||
$media = IPS_GetMedia($mediaID);
|
||
$ext = 'png';
|
||
if (!empty($media['MediaFile'])) {
|
||
$extFromFile = pathinfo($media['MediaFile'], PATHINFO_EXTENSION);
|
||
if ($extFromFile !== '') {
|
||
$ext = $extFromFile;
|
||
}
|
||
}
|
||
|
||
$path = IPS_GetKernelDir() . 'media/logo_' . $mediaID . '.' . $ext;
|
||
|
||
// Bild aus der Symcon-Media-Datenbank extrahieren
|
||
$raw = IPS_GetMediaContent($mediaID); // base64
|
||
$bin = base64_decode($raw);
|
||
if ($bin === false) {
|
||
return null;
|
||
}
|
||
|
||
file_put_contents($path, $bin);
|
||
|
||
return $path;
|
||
}
|
||
}
|
||
?>
|