Files
Symcon_Belevo_Energiemanage…/Abrechnung/module.php
2025-12-12 09:26:01 +01:00

866 lines
32 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
/**
* 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='text-align:right; margin:5px 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)
{
// Inline-styles + TCPDF-kompatible Struktur
$html = "
<table width='100%' cellpadding='4' cellspacing='0' style='font-size:10px; border-collapse: collapse;'>
<tr>
<th style='background-color:#444; color:#fff; padding:6px; border:1px solid #777; text-align:left;'>ID</th>
<th style='background-color:#444; color:#fff; padding:6px; border:1px solid #777; text-align:right;'>Import (kWh)</th>
<th style='background-color:#444; color:#fff; padding:6px; border:1px solid #777; text-align:right;'>Export (kWh)</th>
<th style='background-color:#444; color:#fff; padding:6px; border:1px solid #777; text-align:right;'>ZEV-Haus (kWh)</th>
<th style='background-color:#444; color:#fff; padding:6px; border:1px solid #777; text-align:right;'>Netz-Haus (kWh)</th>
<th style='background-color:#444; color:#fff; padding:6px; border:1px solid #777; text-align:right;'>Solar-Netz (kWh)</th>
<th style='background-color:#444; color:#fff; padding:6px; border:1px solid #777; text-align:right;'>Solar-ZEV (kWh)</th>
<th style='background-color:#444; color:#fff; padding:6px; border:1px solid #777; text-align:right;'>Kauf Solar (CHF)</th>
<th style='background-color:#444; color:#fff; padding:6px; border:1px solid #777; text-align:right;'>Kauf Netz (CHF)</th>
<th style='background-color:#444; color:#fff; padding:6px; border:1px solid #777; text-align:right;'>Verkauf Netz (CHF)</th>
<th style='background-color:#444; color:#fff; padding:6px; border:1px solid #777; text-align:right;'>Verkauf ZEV (CHF)</th>
<th style='background-color:#444; color:#fff; padding:6px; border:1px solid #777; text-align:right;'>Total CHF</th>
</tr>";
if (empty($this->powerCostCache) || !isset($this->powerCostCache[$userId])) {
$html .= "<tr><td colspan='12' align='center' style='padding:10px;'>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;
// Alternierende Hintergrundfarbe (einfach über inline style)
$bg = ($rowIndex % 2 === 0) ? '#f7f7f7' : '#ffffff';
$rowIndex++;
// Funktion zum formatieren mit monospace-alignment (Breite anpassen falls nötig)
$fmtKwh = function($v) { return number_format($v, 3, '.', "'"); }; // Tausendertrenner ' optional
$fmtChf = function($v) { return number_format($v, 2, '.', "'"); };
$html .= "<tr style='background-color:{$bg};'>
<td style='border:1px solid #ccc; padding:6px; vertical-align:top;'>{$a['name']}</td>
<td style='border:1px solid #ccc; padding:6px; text-align:right; vertical-align:top;'>
<span style='font-family:Courier, monospace; display:inline-block; min-width:90px;'>" . $fmtKwh($a['imp']) . "</span>
</td>
<td style='border:1px solid #ccc; padding:6px; text-align:right; vertical-align:top;'>
<span style='font-family:Courier, monospace; display:inline-block; min-width:90px;'>" . $fmtKwh($a['exp']) . "</span>
</td>
<td style='border:1px solid #ccc; padding:6px; text-align:right; vertical-align:top;'>
<span style='font-family:Courier, monospace; display:inline-block; min-width:90px;'>" . $fmtKwh($a['solar_bezug']) . "</span>
</td>
<td style='border:1px solid #ccc; padding:6px; text-align:right; vertical-align:top;'>
<span style='font-family:Courier, monospace; display:inline-block; min-width:90px;'>" . $fmtKwh($a['netz_bezug']) . "</span>
</td>
<td style='border:1px solid #ccc; padding:6px; text-align:right; vertical-align:top;'>
<span style='font-family:Courier, monospace; display:inline-block; min-width:90px;'>" . $fmtKwh($a['solareinspeisung']) . "</span>
</td>
<td style='border:1px solid #ccc; padding:6px; text-align:right; vertical-align:top;'>
<span style='font-family:Courier, monospace; display:inline-block; min-width:90px;'>" . $fmtKwh($a['solarverkauf']) . "</span>
</td>
<td style='border:1px solid #ccc; padding:6px; text-align:right; vertical-align:top;'>
<span style='font-family:Courier, monospace; display:inline-block; min-width:90px;'>" . $fmtChf($a['cost_solar']) . "</span>
</td>
<td style='border:1px solid #ccc; padding:6px; text-align:right; vertical-align:top;'>
<span style='font-family:Courier, monospace; display:inline-block; min-width:90px;'>" . $fmtChf($a['cost_grid']) . "</span>
</td>
<td style='border:1px solid #ccc; padding:6px; text-align:right; vertical-align:top;'>
<span style='font-family:Courier, monospace; display:inline-block; min-width:90px;'>" . $fmtChf($a['rev_feedin']) . "</span>
</td>
<td style='border:1px solid #ccc; padding:6px; text-align:right; vertical-align:top;'>
<span style='font-family:Courier, monospace; display:inline-block; min-width:90px;'>" . $fmtChf($a['rev_zev']) . "</span>
</td>
<td style='border:1px solid #ccc; padding:6px; text-align:right; vertical-align:top;'>
<span style='font-family:Courier, monospace; display:inline-block; min-width:90px;'>" . $fmtChf($subtotal) . "</span>
</td>
</tr>";
// Leerzeile zwischen Zählern (leichter grauer Hintergrund)
$html .= "<tr><td colspan='12' style='background-color:#e9e9e9; height:8px; padding:0; border:none;'></td></tr>";
}
// Subtotal - einzeilige obere Linie (in gleicher Spalte wie subtotal)
$html .= "
<tr>
<td colspan='11' style='text-align:right; padding:6px; border-top:1px solid #000; font-weight:bold;'>Subtotal</td>
<td style='text-align:right; padding:6px; border-top:1px solid #000; font-weight:bold;'>
<span style='font-family:Courier, monospace; display:inline-block; min-width:90px;'>" . number_format($sum, 2, '.', "'") . "</span>
</td>
</tr>";
// Doppel-Linie: wir fügen eine sehr dünne Linie und direkt darunter eine dickere Linie ein,
// da 'double' nicht immer zuverlässig in TCPDF gerendert wird.
$html .= "
<tr>
<td colspan='12' style='padding:0; border:none;'>
<div style='height:4px; border-top:1px solid #000;'></div>
<div style='height:4px; border-top:3px solid #000; margin-top:-3px;'></div>
</td>
</tr>";
// Gesamttotal-Zeile (gleich Spalte wie Subtotal)
$html .= "
<tr>
<td colspan='11' style='text-align:right; padding:6px; font-weight:bold;'>Gesamttotal</td>
<td style='text-align:right; padding:6px; font-weight:bold;'>
<span style='font-family:Courier, monospace; display:inline-block; min-width:90px;'>" . number_format($sum, 2, '.', "'") . "</span>
</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;
}
}
?>