Files
Symcon_Belevo_Energiemanage…/Abrechnung/module.php
T
2026-06-28 11:58:15 +02:00

1334 lines
52 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 $printFooterText = true; // Neu: Steuert ob die Fusszeile gedruckt wird
public function Header()
{
if ($this->logoFile !== null && file_exists($this->logoFile)) {
$this->Image($this->logoFile, 15, 10, 40);
$this->SetY(28);
} else {
$this->SetY(15);
}
}
public function Footer()
{
if (!$this->printFooterText) {
return; // Auf QR-Seite keine Rechnungs-Fusszeile drucken!
}
$this->SetY(-18);
$this->SetFont('dejavusans', '', 8);
$this->MultiCell(0, 8, $this->footerText, 0, 'C', false, 1);
}
}
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');
// NEU: MWST Variablen registrieren (Standard 8.1%)
$this->RegisterPropertyBoolean('EnableVAT', false);
$this->RegisterPropertyFloat('VATPercentage', 8.1);
// Neue Felder für den QR-Schein
$this->RegisterPropertyString('CreditorName', 'Belevo AG');
$this->RegisterPropertyString('CreditorAddress', 'Musterstrasse 1');
$this->RegisterPropertyString('CreditorCity', '6122 Menznau');
$this->RegisterPropertyString('BankIBAN', '');
$this->RegisterTariffVariableProfiles();
$this->RegisterVariableInteger('FromDate', 'Startdatum', '~UnixTimestamp', 1);
$this->RegisterVariableInteger('ToDate', 'Enddatum', '~UnixTimestamp', 2);
$this->RegisterVariableString('LastResult', 'Letzte Abrechnung', '', 3);
$this->RegisterVariableString('VisuTariffOverview', 'Tarifübersicht', '~HTMLBox', 10);
$this->RegisterVariableInteger('VisuTariffType', 'Tarifart', 'BELEVO.TariffType', 11);
$this->RegisterVariableInteger('VisuTariffStart', 'Tarif Startdatum', '~UnixTimestamp', 12);
$this->RegisterVariableInteger('VisuTariffEnd', 'Tarif Enddatum', '~UnixTimestamp', 13);
$this->RegisterVariableFloat('VisuTariffPrice', 'Tarif Rp/Einheit', 'BELEVO.TariffPrice', 14);
$this->RegisterVariableString('VisuTariffStatus', 'Tarif Status', '', 15);
$this->EnableAction('FromDate');
$this->EnableAction('ToDate');
$this->EnableAction('VisuTariffType');
$this->EnableAction('VisuTariffStart');
$this->EnableAction('VisuTariffEnd');
$this->EnableAction('VisuTariffPrice');
$this->RegisterScript(
'StartBilling',
'Abrechnung starten',
"<?php IPS_RequestAction(" . $this->InstanceID . ", 'StartBilling', ''); ?>"
);
$this->RegisterScript(
'SaveVisuTariff',
'Tarif speichern',
"<?php IPS_RequestAction(" . $this->InstanceID . ", 'SaveVisuTariff', ''); ?>"
);
$this->RegisterScript(
'DeleteVisuTariff',
'Tarif löschen',
"<?php IPS_RequestAction(" . $this->InstanceID . ", 'DeleteVisuTariff', ''); ?>"
);
$this->RegisterMediaDocument('InvoicePDF', 'Letzte Rechnung', 'pdf');
}
public function ApplyChanges()
{
parent::ApplyChanges();
$this->RegisterTariffVariableProfiles();
$this->EnsureVisuTariffObjects();
$this->InitializeVisuTariffDefaults();
$this->UpdateTariffOverview();
}
private function EnsureVisuTariffObjects()
{
$this->RegisterVariableString('VisuTariffOverview', 'Tarifübersicht', '~HTMLBox', 10);
$this->RegisterVariableInteger('VisuTariffType', 'Tarifart', 'BELEVO.TariffType', 11);
$this->RegisterVariableInteger('VisuTariffStart', 'Tarif Startdatum', '~UnixTimestamp', 12);
$this->RegisterVariableInteger('VisuTariffEnd', 'Tarif Enddatum', '~UnixTimestamp', 13);
$this->RegisterVariableFloat('VisuTariffPrice', 'Tarif Rp/Einheit', 'BELEVO.TariffPrice', 14);
$this->RegisterVariableString('VisuTariffStatus', 'Tarif Status', '', 15);
$this->EnableAction('VisuTariffType');
$this->EnableAction('VisuTariffStart');
$this->EnableAction('VisuTariffEnd');
$this->EnableAction('VisuTariffPrice');
$this->RegisterScript(
'SaveVisuTariff',
'Tarif speichern',
"<?php IPS_RequestAction(" . $this->InstanceID . ", 'SaveVisuTariff', ''); ?>"
);
$this->RegisterScript(
'DeleteVisuTariff',
'Tarif löschen',
"<?php IPS_RequestAction(" . $this->InstanceID . ", 'DeleteVisuTariff', ''); ?>"
);
}
private function RegisterTariffVariableProfiles()
{
$typeProfile = 'BELEVO.TariffType';
$types = $this->GetTariffTypes();
if (!IPS_VariableProfileExists($typeProfile)) {
IPS_CreateVariableProfile($typeProfile, 1);
}
IPS_SetVariableProfileValues($typeProfile, 0, count($types) - 1, 1);
foreach ($types as $index => $caption) {
IPS_SetVariableProfileAssociation($typeProfile, $index, $caption, '', -1);
}
$priceProfile = 'BELEVO.TariffPrice';
if (!IPS_VariableProfileExists($priceProfile)) {
IPS_CreateVariableProfile($priceProfile, 2);
}
IPS_SetVariableProfileDigits($priceProfile, 3);
IPS_SetVariableProfileText($priceProfile, '', ' Rp/Einheit');
IPS_SetVariableProfileValues($priceProfile, 0, 0, 0);
}
private function GetTariffTypes()
{
return [
'Netztarif',
'Einspeisetarif',
'Solartarif',
'Warmwasser',
'Kaltwasser',
'Wärme'
];
}
private function ReadTariffs()
{
$tariffs = json_decode($this->ReadPropertyString('Tariffs'), true);
return is_array($tariffs) ? array_values($tariffs) : [];
}
private function WriteTariffs(array $tariffs)
{
usort($tariffs, function ($a, $b) {
$typeCmp = strcmp((string)($a['unit_type'] ?? ''), (string)($b['unit_type'] ?? ''));
if ($typeCmp !== 0) {
return $typeCmp;
}
return ($this->toUnixTs($a['start'] ?? 0, false) ?? 0) <=> ($this->toUnixTs($b['start'] ?? 0, false) ?? 0);
});
IPS_SetProperty($this->InstanceID, 'Tariffs', json_encode(array_values($tariffs), JSON_UNESCAPED_UNICODE));
IPS_ApplyChanges($this->InstanceID);
$this->UpdateTariffOverview();
}
private function InitializeVisuTariffDefaults()
{
$startID = @IPS_GetObjectIDByIdent('VisuTariffStart', $this->InstanceID);
$endID = @IPS_GetObjectIDByIdent('VisuTariffEnd', $this->InstanceID);
$typeID = @IPS_GetObjectIDByIdent('VisuTariffType', $this->InstanceID);
$priceID = @IPS_GetObjectIDByIdent('VisuTariffPrice', $this->InstanceID);
if ($startID === false || $endID === false || $typeID === false || $priceID === false) {
return;
}
if ((int)GetValue($startID) > 0 && (int)GetValue($endID) > 0) {
return;
}
$tariffs = $this->ReadTariffs();
$first = $tariffs[0] ?? null;
if (is_array($first)) {
$start = $this->toUnixTs($first['start'] ?? 0, false) ?? strtotime(date('Y-01-01 00:00:00'));
$end = $this->toUnixTs($first['end'] ?? 0, false) ?? strtotime(date('Y-12-31 00:00:00'));
SetValue($typeID, $this->GetTariffTypeIndex((string)($first['unit_type'] ?? 'Netztarif')));
SetValue($priceID, (float)($first['price'] ?? 0));
} else {
$start = strtotime(date('Y-01-01 00:00:00'));
$end = strtotime(date('Y-12-31 00:00:00'));
SetValue($typeID, 0);
SetValue($priceID, 0.0);
}
SetValue($startID, $start);
SetValue($endID, $end);
}
private function SaveVisuTariffFromVariables()
{
$tariff = $this->BuildTariffFromVisuVariables();
$tariffs = $this->ReadTariffs();
$replaced = false;
foreach ($tariffs as $index => $existing) {
if (!$this->IsSameTariffSlot($existing, $tariff)) {
continue;
}
$tariffs[$index] = $tariff;
$replaced = true;
break;
}
if (!$replaced) {
$tariffs[] = $tariff;
}
$this->WriteTariffs($tariffs);
return ($replaced ? 'Tarif aktualisiert: ' : 'Tarif gespeichert: ')
. $tariff['unit_type'] . ' '
. date('d.m.Y', $tariff['start']) . ' - ' . date('d.m.Y', $tariff['end'])
. ', ' . number_format($tariff['price'], 3, '.', "'") . ' Rp/Einheit';
}
private function DeleteVisuTariffFromVariables()
{
$tariff = $this->BuildTariffFromVisuVariables();
$tariffs = $this->ReadTariffs();
$filtered = [];
$deleted = false;
foreach ($tariffs as $existing) {
if ($this->IsSameTariffSlot($existing, $tariff)) {
$deleted = true;
continue;
}
$filtered[] = $existing;
}
if (!$deleted) {
return 'Kein passender Tarif gefunden.';
}
$this->WriteTariffs($filtered);
return 'Tarif gelöscht: '
. $tariff['unit_type'] . ' '
. date('d.m.Y', $tariff['start']) . ' - ' . date('d.m.Y', $tariff['end']);
}
private function BuildTariffFromVisuVariables()
{
$types = $this->GetTariffTypes();
$typeIndex = (int)GetValue($this->GetIDForIdent('VisuTariffType'));
$type = $types[$typeIndex] ?? $types[0];
$start = (int)GetValue($this->GetIDForIdent('VisuTariffStart'));
$end = (int)GetValue($this->GetIDForIdent('VisuTariffEnd'));
$price = (float)GetValue($this->GetIDForIdent('VisuTariffPrice'));
if ($start > 0) {
$start = strtotime(date('Y-m-d 00:00:00', $start));
}
if ($end > 0) {
$end = strtotime(date('Y-m-d 00:00:00', $end));
}
if ($start <= 0 || $end <= 0) {
throw new Exception('Start- und Enddatum müssen gesetzt sein.');
}
if ($end < $start) {
throw new Exception('Enddatum darf nicht vor dem Startdatum liegen.');
}
if ($price < 0) {
throw new Exception('Tarifpreis darf nicht negativ sein.');
}
return [
'start' => $start,
'end' => $end,
'price' => round($price, 3),
'unit_type' => $type
];
}
private function IsSameTariffSlot($a, $b)
{
return strtolower(trim((string)($a['unit_type'] ?? ''))) === strtolower(trim((string)($b['unit_type'] ?? '')))
&& ($this->toUnixTs($a['start'] ?? 0, false) ?? 0) === ($this->toUnixTs($b['start'] ?? 0, false) ?? 0)
&& ($this->toUnixTs($a['end'] ?? 0, false) ?? 0) === ($this->toUnixTs($b['end'] ?? 0, false) ?? 0);
}
private function GetTariffTypeIndex($type)
{
foreach ($this->GetTariffTypes() as $index => $tariffType) {
if (strtolower($tariffType) === strtolower(trim($type))) {
return $index;
}
}
return 0;
}
private function UpdateTariffOverview()
{
$overviewID = @IPS_GetObjectIDByIdent('VisuTariffOverview', $this->InstanceID);
if ($overviewID === false) {
return;
}
$tariffs = $this->ReadTariffs();
$html = "<table width='100%' cellspacing='0' cellpadding='5' style='border-collapse:collapse;font-size:12px;'>
<tr style='background-color:#f0f0f0;font-weight:bold;'>
<td width='28%' style='border:1px solid #ccc;'>Tarifart</td>
<td width='22%' style='border:1px solid #ccc;'>Gültig von</td>
<td width='22%' style='border:1px solid #ccc;'>Gültig bis</td>
<td width='28%' style='border:1px solid #ccc;text-align:right;'>Preis</td>
</tr>";
if (empty($tariffs)) {
$html .= "<tr><td colspan='4' style='border:1px solid #ccc;'>Keine Tarife erfasst.</td></tr>";
} else {
foreach ($tariffs as $tariff) {
$start = $this->toUnixTs($tariff['start'] ?? 0, false);
$end = $this->toUnixTs($tariff['end'] ?? 0, false);
$html .= "<tr>
<td style='border:1px solid #ddd;'>" . $this->h($tariff['unit_type'] ?? '') . "</td>
<td style='border:1px solid #ddd;'>" . ($start ? date('d.m.Y', $start) : '-') . "</td>
<td style='border:1px solid #ddd;'>" . ($end ? date('d.m.Y', $end) : '-') . "</td>
<td align='right' style='border:1px solid #ddd;'>" . number_format((float)($tariff['price'] ?? 0), 3, '.', "'") . " Rp/Einheit</td>
</tr>";
}
}
$html .= '</table>';
SetValue($overviewID, $html);
}
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':
case 'VisuTariffType':
case 'VisuTariffStart':
case 'VisuTariffEnd':
case 'VisuTariffPrice':
SetValue($this->GetIDForIdent($Ident), $Value);
break;
case 'SaveVisuTariff':
try {
$message = $this->SaveVisuTariffFromVariables();
SetValue($this->GetIDForIdent('VisuTariffStatus'), $message);
echo $message;
} catch (Throwable $e) {
$message = 'Fehler beim Speichern des Tarifs: ' . $e->getMessage();
SetValue($this->GetIDForIdent('VisuTariffStatus'), $message);
echo $message;
}
break;
case 'DeleteVisuTariff':
try {
$message = $this->DeleteVisuTariffFromVariables();
SetValue($this->GetIDForIdent('VisuTariffStatus'), $message);
echo $message;
} catch (Throwable $e) {
$message = 'Fehler beim Löschen des Tarifs: ' . $e->getMessage();
SetValue($this->GetIDForIdent('VisuTariffStatus'), $message);
echo $message;
}
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);
$users = is_array($users) ? $users : [];
$power = is_array($power) ? $power : [];
$water = is_array($water) ? $water : [];
$tariffs = is_array($tariffs) ? $tariffs : [];
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();
$pdf->SetAutoPageBreak(true, 20);
$pdf->printFooterText = true; // Fusszeile für diese Rechnungsseite EINSCHALTEN
$grandTotal = $this->BuildUserInvoice($pdf, $user, $power, $water, $tariffs, $from, $to);
$this->BuildQRBill($pdf, $user, $grandTotal);
}
return $pdf->Output('Abrechnung.pdf', 'S');
}
private function BuildUserInvoice($pdf, $user, $power, $water, $tariffs, $from, $to)
{
$meterNames = [];
foreach ($power as $m) {
if (($m['user_id'] ?? null) == ($user['id'] ?? null)) {
$meterNames[] = $this->h($m['name'] ?? '');
}
}
foreach ($water as $m) {
if (($m['user_id'] ?? null) == ($user['id'] ?? null)) {
$meterNames[] = $this->h($m['name'] ?? '');
}
}
$meterText = empty($meterNames) ? '-' : implode('<br>', $meterNames);
$periodText = date('d.m.Y', $from) . ' - ' . date('d.m.Y', $to);
$html = "
<h1 style='text-align:center;font-size:18px;margin-bottom:4px;'>Elektro- und Nebenkostenabrechnung</h1>
<p style='text-align:center;font-size:10px;margin-top:0;'><strong>Liegenschaft:</strong> " . $this->h($this->ReadPropertyString('PropertyText')) . "</p>
<table width='100%' cellpadding='5' cellspacing='0' style='font-size:9px;border-collapse:collapse;'>
<tr>
<td width='56%' style='border:1px solid #d8d8d8;background-color:#fafafa;'>
<strong>Abrechnungszeitraum</strong><br>" . $periodText . "<br><br>
<strong>Zählpunkte</strong><br>" . $meterText . "
</td>
<td width='44%' style='border:1px solid #d8d8d8;'>
<strong>Rechnungsadresse</strong><br>
" . $this->h($user['name'] ?? '') . "<br>
" . $this->h($user['address'] ?? '') . "<br>
" . $this->h($user['city'] ?? '') . "
</td>
</tr>
</table>
<br>";
// ========================= Elektrizität =========================
$html .= "<h2 style='font-size:12px;margin-bottom:5px;'>Elektrizität</h2>";
$powerResult = $this->GetCalculatedPowerCosts($user['id']);
$html .= $powerResult['html'];
$totalPower = $powerResult['sum'];
$appliedTariffs = $this->CollectTariffsForUser($tariffs, ['Netztarif', 'Solartarif', 'Einspeisetarif']);
if (!empty($appliedTariffs)) {
$html .= "<p style='font-size:8px;margin-top:6px;'><strong>Angewendete Elektrizitätstarife</strong></p><ul style='font-size:8px;'>";
foreach ($appliedTariffs as $t) {
if ($t['start'] === null || $t['end'] === null) {
continue;
}
$html .= "<li>"
. $this->h($t['unit_type'] ?? '') . ': '
. date('d.m.Y', $t['start']) . ' - ' . date('d.m.Y', $t['end'])
. ' - <strong>' . number_format($t['price'], 3, '.', "'") . ' Rp/kWh</strong>'
. "</li>";
}
$html .= "</ul><br>";
}
// ========================= Nebenkosten =========================
$html .= "<h2 style='font-size:12px;margin-bottom:5px;'>Nebenkosten</h2>";
$additionalResult = $this->CalculateAdditionalCosts($water, $tariffs, $user['id'], $from, $to);
$html .= $additionalResult['html'];
$totalAdditional = $additionalResult['sum'];
// ========================= Gesamttotal & MWST =========================
$subTotal = $totalPower + $totalAdditional;
$enableVAT = $this->ReadPropertyBoolean('EnableVAT');
$html .= "<br><table width='100%' style='font-size:10px;border-collapse:collapse;' cellpadding='5' cellspacing='0'>
<tr style='background-color:#f0f0f0;'>
<td colspan='2' style='border:1px solid #ccc;'><strong>Zusammenfassung</strong></td>
</tr>
<tr>
<td width='72%' style='border:1px solid #ddd;'>Zwischentotal Elektrizität</td>
<td width='28%' align='right' style='border:1px solid #ddd;'>" . $this->FormatCurrency($totalPower) . "</td>
</tr>
<tr>
<td width='72%' style='border:1px solid #ddd;'>Zwischentotal Nebenkosten</td>
<td width='28%' align='right' style='border:1px solid #ddd;'>" . $this->FormatCurrency($totalAdditional) . "</td>
</tr>";
if ($enableVAT) {
$vatPercent = $this->ReadPropertyFloat('VATPercentage');
$vatAmount = $subTotal * ($vatPercent / 100);
$grandTotal = $subTotal + $vatAmount;
$html .= "
<tr>
<td width='72%' style='border:1px solid #ddd;'>Total exkl. MWST</td>
<td width='28%' align='right' style='border:1px solid #ddd;'>" . $this->FormatCurrency($subTotal) . "</td>
</tr>
<tr>
<td width='72%' style='border:1px solid #ddd;'>MWST " . number_format($vatPercent, 1, '.', "'") . " %</td>
<td width='28%' align='right' style='border:1px solid #ddd;'>" . $this->FormatCurrency($vatAmount) . "</td>
</tr>
<tr style='background-color:#e9e9e9;'>
<td width='72%' style='border:1px solid #bbb;font-size:12px;'><strong>Gesamttotal inkl. MWST</strong></td>
<td width='28%' align='right' style='border:1px solid #bbb;font-size:12px;'><strong>" . $this->FormatCurrency($grandTotal) . "</strong></td>
</tr>
</table>";
} else {
$grandTotal = $subTotal;
$html .= "
<tr style='background-color:#e9e9e9;'>
<td width='72%' style='border:1px solid #bbb;font-size:12px;'><strong>Gesamttotal</strong></td>
<td width='28%' align='right' style='border:1px solid #bbb;font-size:12px;'><strong>" . $this->FormatCurrency($grandTotal) . "</strong></td>
</tr>
<tr>
<td colspan='2' align='right' style='font-size:8px;color:#555;border:1px solid #ddd;'>Nicht mehrwertsteuerpflichtig.</td>
</tr>
</table>";
}
$pdf->writeHTML($html, true, false, true, false, '');
return $grandTotal;
}
// ====================== QR-Einzahlungsschein (Schweiz) ======================
private function BuildQRBill($pdf, $user, $amount)
{
$iban = str_replace(' ', '', $this->ReadPropertyString('BankIBAN'));
if (empty($iban) || $amount <= 0) {
return;
}
$pdf->AddPage();
$pdf->printFooterText = false;
$pdf->SetAutoPageBreak(false);
$font = 'helvetica';
$yStart = 192;
$amountText = number_format($amount, 2, '.', ' ');
$pdf->SetLineStyle(['dash' => '2,2', 'color' => [0, 0, 0], 'width' => 0.1]);
$pdf->Line(0, $yStart, 210, $yStart);
$pdf->Line(62, $yStart, 62, 297);
$pdf->SetLineStyle(['dash' => 0, 'color' => [0, 0, 0]]);
$creditorName = $this->ReadPropertyString('CreditorName');
$creditorAddress = $this->ReadPropertyString('CreditorAddress');
$creditorCity = $this->ReadPropertyString('CreditorCity');
$bankIBANFormatted = wordwrap($iban, 4, ' ', true);
$debtorLines = [
$user['name'] ?? '',
$user['address'] ?? '',
$user['city'] ?? ''
];
// Linker Teil: Empfangsschein
$pdf->SetFont($font, 'B', 11);
$pdf->SetXY(5, $yStart + 5);
$pdf->Cell(52, 5, 'Empfangsschein', 0, 1);
$this->WriteQrTextBlock($pdf, 5, $yStart + 12, 52, 'Konto / Zahlbar an', [
$bankIBANFormatted,
$creditorName,
$creditorAddress,
$creditorCity
], 6, 7, 3.2);
$this->WriteQrTextBlock($pdf, 5, $yStart + 39, 52, 'Zahlbar durch', $debtorLines, 6, 7, 3.2);
$pdf->SetFont($font, 'B', 6);
$pdf->SetXY(5, $yStart + 68);
$pdf->Cell(15, 3, 'Währung', 0, 0);
$pdf->Cell(30, 3, 'Betrag', 0, 1);
$pdf->SetFont($font, '', 8);
$pdf->SetX(5);
$pdf->Cell(15, 4, 'CHF', 0, 0);
$pdf->Cell(30, 4, $amountText, 0, 1);
$pdf->SetFont($font, 'B', 6);
$pdf->SetXY(38, $yStart + 90);
$pdf->Cell(19, 3, 'Annahmestelle', 0, 1, 'R');
// Rechter Teil: Zahlteil
$pdf->SetFont($font, 'B', 11);
$pdf->SetXY(67, $yStart + 5);
$pdf->Cell(50, 5, 'Zahlteil', 0, 1);
$qrString = $this->GenerateQRString($iban, $amount, $user);
$style = ['border' => false, 'padding' => 0, 'fgcolor' => [0, 0, 0], 'bgcolor' => false];
$pdf->write2DBarcode($qrString, 'QRCODE,M', 67, $yStart + 17, 46, 46, $style, 'N');
$cX = 67 + 23;
$cY = $yStart + 17 + 23;
$pdf->SetFillColor(255, 255, 255);
$pdf->Rect($cX - 3.5, $cY - 3.5, 7, 7, 'F');
$pdf->SetFillColor(0, 0, 0);
$pdf->Rect($cX - 3, $cY - 3, 6, 6, 'F');
$pdf->SetFillColor(255, 255, 255);
$pdf->Rect($cX - 0.6, $cY - 2, 1.2, 4, 'F');
$pdf->Rect($cX - 2, $cY - 0.6, 4, 1.2, 'F');
$pdf->SetFont($font, 'B', 8);
$pdf->SetXY(67, $yStart + 68);
$pdf->Cell(15, 4, 'Währung', 0, 0);
$pdf->Cell(30, 4, 'Betrag', 0, 1);
$pdf->SetFont($font, '', 10);
$pdf->SetX(67);
$pdf->Cell(15, 5, 'CHF', 0, 0);
$pdf->Cell(30, 5, $amountText, 0, 1);
$this->WriteQrTextBlock($pdf, 118, $yStart + 5, 84, 'Konto / Zahlbar an', [
$bankIBANFormatted,
$creditorName,
$creditorAddress,
$creditorCity
], 8, 9, 4);
$this->WriteQrTextBlock($pdf, 118, $yStart + 34, 84, 'Zusätzliche Informationen', [
'Abrechnung Liegenschaft'
], 8, 9, 4);
$this->WriteQrTextBlock($pdf, 118, $yStart + 50, 84, 'Zahlbar durch', $debtorLines, 8, 9, 4);
$pdf->SetAutoPageBreak(true, 20);
}
private function WriteQrTextBlock($pdf, $x, $y, $width, $label, array $lines, $labelSize = 6, $textSize = 8, $lineHeight = 3.5)
{
$pdf->SetFont('helvetica', 'B', $labelSize);
$pdf->SetXY($x, $y);
$pdf->MultiCell($width, 3, $label, 0, 'L', false, 1);
$pdf->SetFont('helvetica', '', $textSize);
foreach ($lines as $line) {
$line = trim((string)$line);
if ($line === '') {
continue;
}
$pdf->SetX($x);
$pdf->MultiCell($width, $lineHeight, $line, 0, 'L', false, 1);
}
}
private function GenerateQRString($iban, $amount, $user)
{
// Daten nach SIX Swiss Payment Standard beschneiden (max 70 Zeichen pro Zeile)
$creditorName = mb_substr($this->ReadPropertyString('CreditorName'), 0, 70);
$creditorStr = mb_substr($this->ReadPropertyString('CreditorAddress'), 0, 70);
$creditorCity = mb_substr($this->ReadPropertyString('CreditorCity'), 0, 70);
// Fallback falls ein Mieter keinen Namen oder Ort hinterlegt hat (Banken verlangen das zwingend)
$debtorName = mb_substr(empty($user['name']) ? 'Unbekannt' : $user['name'], 0, 70);
$debtorStr = mb_substr($user['address'] ?? '', 0, 70);
$debtorCity = mb_substr(empty($user['city']) ? 'Unbekannt' : $user['city'], 0, 70);
$amountStr = number_format($amount, 2, '.', '');
// EPC / Swiss QR Code String Format (Exakte Reihenfolge ist kritisch!)
$lines = [
'SPC', '0200', '1', $iban, 'K',
$creditorName, $creditorStr, $creditorCity, '', '', 'CH',
'', '', '', '', '', '', '', // Ultimate Creditor (leer)
$amountStr, 'CHF', 'K',
$debtorName, $debtorStr, $debtorCity, '', '', 'CH',
'NON', '', // Reference Type (NON = Ohne Referenz)
'Abrechnung Liegenschaft', // 30. Unstrukturierte Mitteilung
'EPD' // 31. Trailer (Zwingend exakt 'EPD' am Schluss!)
];
return implode("\r\n", $lines);
}
// ====================== 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)
{
if (empty($this->powerCostCache) || !isset($this->powerCostCache[$userId])) {
return [
'html' => "<table border='1' cellspacing='0' cellpadding='4' width='100%' style='font-size:8px;'><tr><td align='center'>Keine Stromzähler für diesen Benutzer</td></tr></table><br>",
'sum' => 0.0
];
}
$energyRows = '';
$amountRows = '';
$sum = 0.0;
$totals = [
'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
];
$rowIndex = 0;
foreach ($this->powerCostCache[$userId] as $name => $a) {
$subtotal = $a['cost_grid'] + $a['cost_solar'] - ($a['rev_feedin'] + $a['rev_zev']);
$sum += $subtotal;
foreach ($totals as $key => $_) {
$totals[$key] += (float)($a[$key] ?? 0.0);
}
$rowStyle = ($rowIndex % 2 === 0) ? " style='background-color:#fbfbfb;'" : '';
$rowIndex++;
$label = $this->h($a['name'] ?? $name);
$energyRows .= "<tr" . $rowStyle . ">
<td width='20%'>" . $label . "</td>
<td width='13%' align='right'>" . $this->FormatNumber($a['imp'], 3) . "</td>
<td width='13%' align='right'>" . $this->FormatNumber($a['exp'], 3) . "</td>
<td width='14%' align='right'>" . $this->FormatNumber($a['solar_bezug'], 3) . "</td>
<td width='14%' align='right'>" . $this->FormatNumber($a['netz_bezug'], 3) . "</td>
<td width='13%' align='right'>" . $this->FormatNumber($a['solareinspeisung'], 3) . "</td>
<td width='13%' align='right'>" . $this->FormatNumber($a['solarverkauf'], 3) . "</td>
</tr>";
$amountRows .= "<tr" . $rowStyle . ">
<td width='24%'>" . $label . "</td>
<td width='16%' align='right'>" . $this->FormatCurrency($a['cost_solar'], false) . "</td>
<td width='16%' align='right'>" . $this->FormatCurrency($a['cost_grid'], false) . "</td>
<td width='16%' align='right'>" . $this->FormatCurrency($a['rev_feedin'], false) . "</td>
<td width='16%' align='right'>" . $this->FormatCurrency($a['rev_zev'], false) . "</td>
<td width='12%' align='right'><strong>" . $this->FormatCurrency($subtotal, false) . "</strong></td>
</tr>";
}
$html = "
<table border='1' cellspacing='0' cellpadding='3' width='100%' style='font-size:7.5px;border-collapse:collapse;'>
<tr style='background-color:#eeeeee;font-weight:bold;'>
<th width='20%'>Zählpunkt</th>
<th width='13%'>Import kWh</th>
<th width='13%'>Export kWh</th>
<th width='14%'>Solarbezug kWh</th>
<th width='14%'>Netzbezug kWh</th>
<th width='13%'>Einspeisung kWh</th>
<th width='13%'>ZEV-Verkauf kWh</th>
</tr>" . $energyRows . "
<tr style='background-color:#f4f4f4;font-weight:bold;'>
<td width='20%'>Total</td>
<td width='13%' align='right'>" . $this->FormatNumber($totals['imp'], 3) . "</td>
<td width='13%' align='right'>" . $this->FormatNumber($totals['exp'], 3) . "</td>
<td width='14%' align='right'>" . $this->FormatNumber($totals['solar_bezug'], 3) . "</td>
<td width='14%' align='right'>" . $this->FormatNumber($totals['netz_bezug'], 3) . "</td>
<td width='13%' align='right'>" . $this->FormatNumber($totals['solareinspeisung'], 3) . "</td>
<td width='13%' align='right'>" . $this->FormatNumber($totals['solarverkauf'], 3) . "</td>
</tr>
</table>
<br>
<table border='1' cellspacing='0' cellpadding='3' width='100%' style='font-size:8px;border-collapse:collapse;'>
<tr style='background-color:#eeeeee;font-weight:bold;'>
<th width='24%'>Zählpunkt</th>
<th width='16%'>Kauf Solar CHF</th>
<th width='16%'>Kauf Netz CHF</th>
<th width='16%'>Gutschrift Netz CHF</th>
<th width='16%'>Gutschrift ZEV CHF</th>
<th width='12%'>Total CHF</th>
</tr>" . $amountRows . "
<tr style='background-color:#e9e9e9;font-weight:bold;'>
<td colspan='5' align='right'>Zwischentotal Elektrizität</td>
<td align='right'>" . $this->FormatCurrency($sum, false) . "</td>
</tr>
</table><br>";
return ['html' => $html, 'sum' => $sum];
}
// ====================== Nebenkosten Wasser/Wärme ======================
private function CalculateAdditionalCosts($waterMeters, $tariffs, $userId, $from, $to)
{
$html = "
<table border='1' cellspacing='0' cellpadding='3' width='100%' style='font-size:8px;border-collapse:collapse;'>
<tr style='background-color:#eeeeee;font-weight:bold;'>
<th width='16%'>Zähler</th>
<th width='11%'>Typ</th>
<th width='11%'>Von</th>
<th width='11%'>Bis</th>
<th width='10%'>Start</th>
<th width='10%'>Ende</th>
<th width='10%'>Verbrauch</th>
<th width='10%'>Tarif</th>
<th width='11%'>Kosten CHF</th>
</tr>";
$total = 0.0;
$usedTariffs = [];
$hasRows = false;
foreach ($waterMeters as $m) {
if (($m['user_id'] ?? null) != $userId) {
continue;
}
$type = $m['meter_type'] ?? 'Warmwasser';
$cost = $this->AddMeterToPDFRow($m, $tariffs, $from, $to, $type);
if ($cost['row'] !== '') {
$hasRows = true;
}
$html .= $cost['row'];
$total += $cost['value'];
$usedTariffs = array_merge($usedTariffs, $cost['tariffs']);
}
if (!$hasRows) {
$html .= "<tr><td colspan='9' align='center'>Keine Nebenkostenzähler für diesen Benutzer</td></tr>";
}
$html .= "
<tr style='background-color:#e9e9e9;font-weight:bold;'>
<td colspan='8' align='right'>Zwischentotal Nebenkosten</td>
<td align='right'>" . $this->FormatCurrency($total) . "</td>
</tr>
</table>";
if (!empty($usedTariffs)) {
$html .= "<p style='font-size:8px;margin-top:6px;'><strong>Angewendete Nebenkostentarife</strong></p><ul style='font-size:8px;'>";
foreach ($usedTariffs as $t) {
if ($t['start'] === null || $t['end'] === null) {
continue;
}
$html .= "<li>" . $this->h($t['unit_type'] ?? '') . ': '
. date('d.m.Y', $t['start']) . ' - ' . date('d.m.Y', $t['end'])
. ' - <strong>' . number_format($t['price'], 3, '.', "'") . " Rp/Einheit</strong></li>";
}
$html .= "</ul><br>";
}
return ['html' => $html, 'sum' => $total];
}
private function AddMeterToPDFRow($meter, $tariffs, $from, $to, $type)
{
$varId = (int)($meter['var_consumption'] ?? 0);
if ($varId <= 0 || !IPS_VariableExists($varId)) {
return ['row' => '', 'value' => 0, 'tariffs' => []];
}
$filteredTariffs = array_filter($tariffs, function ($t) use ($type) {
return strtolower(trim($t['unit_type'] ?? '')) === strtolower(trim($type));
});
$usedTariffs = [];
$activeTariff = null;
foreach ($filteredTariffs as $t) {
$startTs = $this->toUnixTs($t['start'] ?? 0, false);
$endTs = $this->toUnixTs($t['end'] ?? 0, true);
if ($startTs === null || $endTs === null) {
continue;
}
if ($endTs < $from || $startTs > $to) {
continue;
}
$activeTariff = [
'start' => $startTs,
'end' => $endTs,
'price' => (float)($t['price'] ?? 0),
'unit_type' => $t['unit_type'] ?? $type
];
$usedTariffs[] = $activeTariff;
break;
}
$tariffPrice = $activeTariff ? $activeTariff['price'] : 0.0;
$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);
$row = "<tr>
<td width='16%'>" . $this->h($meter['name'] ?? '') . "</td>
<td width='11%'>" . $this->h($type) . "</td>
<td width='11%' align='center'>" . date('d.m.Y', $from) . "</td>
<td width='11%' align='center'>" . date('d.m.Y', $to) . "</td>
<td width='10%' align='right'>" . $this->FormatNumber($startValue, 2) . "</td>
<td width='10%' align='right'>" . $this->FormatNumber($endValue, 2) . "</td>
<td width='10%' align='right'>" . $this->FormatNumber($verbrauch, 2) . "</td>
<td width='10%' align='right'>" . number_format($tariffPrice, 3, '.', "'") . "</td>
<td width='11%' align='right'>" . $this->FormatCurrency($kosten, false) . "</td>
</tr>";
return ['row' => $row, 'value' => $kosten, 'tariffs' => array_values($usedTariffs)];
}
// ====================== Hilfsfunktionen ======================
private function h($value)
{
return htmlspecialchars((string)$value, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
}
private function FormatNumber($value, $digits = 2)
{
return number_format((float)$value, $digits, '.', "'");
}
private function FormatCurrency($value, $withCurrency = true)
{
$amount = number_format((float)$value, 2, '.', "'");
return $withCurrency ? 'CHF ' . $amount : $amount;
}
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 $endOfDay ? strtotime(date('Y-m-d 23:59:59', $val)) : $val;
}
if (is_numeric($val)) {
$ts = (int)$val;
return $endOfDay ? strtotime(date('Y-m-d 23:59:59', $ts)) : $ts;
}
if (is_array($val) && isset($val['year'], $val['month'], $val['day'])) {
$time = $endOfDay ? '23:59:59' : '00:00:00';
return strtotime(sprintf(
'%04d-%02d-%02d %s',
$val['year'],
$val['month'],
$val['day'],
$time
));
}
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 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'],
'unit_type' => $t['unit_type'] ?? ''
];
}
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;
}
}
?>