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', 'Tarife bearbeiten', '~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', "InstanceID . ", 'StartBilling', ''); ?>" ); $this->EnsureVisuTariffObjects(); $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', 'Tarife bearbeiten', '~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'); foreach (['VisuTariffType', 'VisuTariffStart', 'VisuTariffEnd', 'VisuTariffPrice', 'VisuTariffStatus', 'SaveVisuTariff', 'DeleteVisuTariff'] as $ident) { $this->HideObjectByIdent($ident); } } private function HideObjectByIdent($Ident) { $id = @IPS_GetObjectIDByIdent($Ident, $this->InstanceID); if ($id !== false) { IPS_SetHidden($id, true); } } 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; } $types = $this->GetTariffTypes(); $tariffs = $this->ReadTariffs(); $rows = ''; foreach ($tariffs as $tariff) { $rows .= $this->BuildTariffEditorRow( (string)($tariff['unit_type'] ?? 'Netztarif'), $this->FormatDateInput($this->toUnixTs($tariff['start'] ?? 0, false)), $this->FormatDateInput($this->toUnixTs($tariff['end'] ?? 0, false)), number_format((float)($tariff['price'] ?? 0), 3, '.', '') ); } if ($rows === '') { $rows = $this->BuildTariffEditorRow('Netztarif', date('Y-01-01'), date('Y-12-31'), '0.000'); } $typeOptions = ''; foreach ($types as $type) { $typeOptions .= ''; } $instanceID = (int)$this->InstanceID; $status = $this->h(@GetValue($this->GetIDForIdent('VisuTariffStatus'))); $html = "
" . $rows . "
Tarifart Startdatum Enddatum Rp/Einheit Aktion
" . $status . "
"; SetValue($overviewID, $html); } private function BuildTariffEditorRow($type, $start, $end, $price) { $options = ''; foreach ($this->GetTariffTypes() as $tariffType) { $selected = strtolower($tariffType) === strtolower((string)$type) ? ' selected' : ''; $options .= ''; } return '' . '' . '' . '' . '' . '' . ''; } private function SaveTariffTableFromPayload($payload) { $rows = json_decode((string)$payload, true); if (!is_array($rows)) { throw new Exception('Ungültige Tarifdaten.'); } $allowedTypes = $this->GetTariffTypes(); $tariffs = []; foreach ($rows as $row) { if (!is_array($row)) { continue; } $type = trim((string)($row['unit_type'] ?? '')); $startText = trim((string)($row['start'] ?? '')); $endText = trim((string)($row['end'] ?? '')); $priceText = str_replace(',', '.', trim((string)($row['price'] ?? ''))); if ($type === '' && $startText === '' && $endText === '' && $priceText === '') { continue; } if (!in_array($type, $allowedTypes, true)) { throw new Exception('Ungültige Tarifart: ' . $type); } if ($startText === '' || $endText === '') { throw new Exception('Start- und Enddatum müssen gesetzt sein.'); } if (!is_numeric($priceText) || (float)$priceText < 0) { throw new Exception('Tarifpreis ungültig.'); } $start = strtotime($startText . ' 00:00:00'); $end = strtotime($endText . ' 00:00:00'); if ($start === false || $end === false || $end < $start) { throw new Exception('Datumsbereich ungültig.'); } $tariffs[] = [ 'start' => $start, 'end' => $end, 'price' => round((float)$priceText, 3), 'unit_type' => $type ]; } $this->WriteTariffs($tariffs); return count($tariffs) . ' Tarif(e) gespeichert.'; } private function FormatDateInput($timestamp) { return $timestamp ? date('Y-m-d', (int)$timestamp) : ''; } 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 'SaveTariffTable': try { $message = $this->SaveTariffTableFromPayload($Value); SetValue($this->GetIDForIdent('VisuTariffStatus'), $message); echo $message; } catch (Throwable $e) { $message = 'Fehler beim Speichern der Tarifliste: ' . $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) { $pdf->SetTextColor(20, 20, 20); $pdf->SetDrawColor(190, 190, 190); $pdf->SetLineWidth(0.15); $pdf->SetFont('dejavusans', 'B', 16); $pdf->SetY(max($pdf->GetY(), 38)); $pdf->Cell(0, 8, 'Elektro- und Nebenkostenabrechnung', 0, 1, 'L'); $pdf->SetFont('dejavusans', '', 8.5); $pdf->Cell(0, 5, 'Liegenschaft: ' . $this->ReadPropertyString('PropertyText'), 0, 1, 'L'); $pdf->Ln(3); $meterNames = []; foreach ($power as $m) { if (($m['user_id'] ?? null) == ($user['id'] ?? null)) { $meterNames[] = (string)($m['name'] ?? ''); } } foreach ($water as $m) { if (($m['user_id'] ?? null) == ($user['id'] ?? null)) { $meterNames[] = (string)($m['name'] ?? ''); } } $periodText = date('d.m.Y', $from) . ' - ' . date('d.m.Y', $to); $meterText = empty($meterNames) ? '-' : implode("\n", $meterNames); $addressText = trim(($user['name'] ?? '') . "\n" . ($user['address'] ?? '') . "\n" . ($user['city'] ?? '')); $y = $pdf->GetY(); $pdf->SetFillColor(248, 249, 250); $pdf->Rect(15, $y, 88, 32, 'DF'); $pdf->Rect(107, $y, 88, 32, 'D'); $pdf->SetFont('dejavusans', 'B', 8); $pdf->SetXY(18, $y + 3); $pdf->MultiCell(82, 4, "Abrechnungszeitraum", 0, 'L'); $pdf->SetFont('dejavusans', '', 8); $pdf->SetX(18); $pdf->MultiCell(82, 4, $periodText, 0, 'L'); $pdf->SetFont('dejavusans', 'B', 8); $pdf->SetX(18); $pdf->MultiCell(82, 4, "Zählpunkte", 0, 'L'); $pdf->SetFont('dejavusans', '', 8); $pdf->SetX(18); $pdf->MultiCell(82, 4, $meterText, 0, 'L'); $pdf->SetFont('dejavusans', 'B', 8); $pdf->SetXY(110, $y + 3); $pdf->MultiCell(82, 4, "Rechnungsadresse", 0, 'L'); $pdf->SetFont('dejavusans', '', 8); $pdf->SetX(110); $pdf->MultiCell(82, 4, $addressText, 0, 'L'); $pdf->SetY($y + 38); $this->PdfSectionTitle($pdf, 'Elektrizität'); $powerResult = $this->GetCalculatedPowerCosts($user['id']); $this->WritePowerPdfTables($pdf, $powerResult); $totalPower = $powerResult['sum']; $appliedTariffs = $this->CollectTariffsForUser($tariffs, ['Netztarif', 'Solartarif', 'Einspeisetarif']); $this->WriteTariffPdfTable($pdf, 'Angewendete Elektrizitätstarife', $appliedTariffs, 'Rp/kWh'); $this->PdfSectionTitle($pdf, 'Nebenkosten'); $additionalResult = $this->CalculateAdditionalCosts($water, $tariffs, $user['id'], $from, $to); $this->WriteAdditionalPdfTable($pdf, $additionalResult); $totalAdditional = $additionalResult['sum']; $this->WriteTariffPdfTable($pdf, 'Angewendete Nebenkostentarife', $additionalResult['tariffs'], 'Rp/Einheit'); $subTotal = $totalPower + $totalAdditional; $enableVAT = $this->ReadPropertyBoolean('EnableVAT'); $summaryRows = [ ['Zwischentotal Elektrizität', $this->FormatCurrency($totalPower)], ['Zwischentotal Nebenkosten', $this->FormatCurrency($totalAdditional)] ]; if ($enableVAT) { $vatPercent = $this->ReadPropertyFloat('VATPercentage'); $vatAmount = $subTotal * ($vatPercent / 100); $grandTotal = $subTotal + $vatAmount; $summaryRows[] = ['Total exkl. MWST', $this->FormatCurrency($subTotal)]; $summaryRows[] = ['MWST ' . number_format($vatPercent, 1, '.', "'") . ' %', $this->FormatCurrency($vatAmount)]; $summaryRows[] = ['Gesamttotal inkl. MWST', $this->FormatCurrency($grandTotal)]; } else { $grandTotal = $subTotal; $summaryRows[] = ['Gesamttotal', $this->FormatCurrency($grandTotal)]; } $this->WriteSummaryPdfTable($pdf, $summaryRows); if (!$enableVAT) { $pdf->SetFont('dejavusans', '', 7.5); $pdf->SetTextColor(80, 80, 80); $pdf->Cell(0, 5, 'Nicht mehrwertsteuerpflichtig.', 0, 1, 'L'); $pdf->SetTextColor(20, 20, 20); } return $grandTotal; } private function PdfSectionTitle($pdf, $title) { $this->PdfEnsureSpace($pdf, 12); $pdf->Ln(2); $pdf->SetFont('dejavusans', 'B', 11); $pdf->SetFillColor(235, 238, 242); $pdf->Cell(0, 7, $title, 0, 1, 'L', true); $pdf->Ln(1); } private function PdfEnsureSpace($pdf, $height) { if ($pdf->GetY() + $height > 276) { $pdf->AddPage(); $pdf->SetY(38); } } private function PdfTableHeader($pdf, array $columns) { $this->PdfEnsureSpace($pdf, 8); $pdf->SetFont('dejavusans', 'B', 7.2); $pdf->SetFillColor(231, 234, 238); $values = array_map(function ($column) { return $column['label']; }, $columns); $this->PdfTableRow($pdf, $columns, $values, true, true); } private function PdfTableRow($pdf, array $columns, array $values, $bold = false, $fill = false) { $pdf->SetFont('dejavusans', $bold ? 'B' : '', 7.2); $lineHeights = []; foreach ($columns as $index => $column) { $lineHeights[] = $pdf->getStringHeight($column['width'] - 2, (string)($values[$index] ?? '')) + 2; } $height = max(5.5, max($lineHeights)); $this->PdfEnsureSpace($pdf, $height + 1); $x = 15; $y = $pdf->GetY(); $pdf->SetFillColor($fill ? 245 : 255, $fill ? 246 : 255, $fill ? 248 : 255); foreach ($columns as $index => $column) { $align = $column['align'] ?? 'L'; $pdf->MultiCell( $column['width'], $height, (string)($values[$index] ?? ''), 1, $align, $fill, 0, $x, $y, true, 0, false, true, $height, 'M' ); $x += $column['width']; } $pdf->SetY($y + $height); } private function WritePowerPdfTables($pdf, array $result) { $rows = $result['rows']; if (empty($rows)) { $this->PdfTableRow($pdf, [['width' => 180, 'align' => 'C']], ['Keine Stromzähler für diesen Benutzer.']); return; } $energyColumns = [ ['width' => 48, 'label' => 'Zählpunkt'], ['width' => 30, 'label' => 'Import kWh', 'align' => 'R'], ['width' => 30, 'label' => 'Export kWh', 'align' => 'R'], ['width' => 36, 'label' => 'Solarbezug kWh', 'align' => 'R'], ['width' => 36, 'label' => 'Netzbezug kWh', 'align' => 'R'] ]; $this->PdfTableHeader($pdf, $energyColumns); foreach ($rows as $row) { $this->PdfTableRow($pdf, $energyColumns, [ $row['name'], $this->FormatNumber($row['imp'], 3), $this->FormatNumber($row['exp'], 3), $this->FormatNumber($row['solar_bezug'], 3), $this->FormatNumber($row['netz_bezug'], 3) ]); } $this->PdfTableRow($pdf, $energyColumns, [ 'Total', $this->FormatNumber($result['totals']['imp'], 3), $this->FormatNumber($result['totals']['exp'], 3), $this->FormatNumber($result['totals']['solar_bezug'], 3), $this->FormatNumber($result['totals']['netz_bezug'], 3) ], true, true); $pdf->Ln(2); $exportColumns = [ ['width' => 60, 'label' => 'Zählpunkt'], ['width' => 60, 'label' => 'Einspeisung Netz kWh', 'align' => 'R'], ['width' => 60, 'label' => 'ZEV-Verkauf kWh', 'align' => 'R'] ]; $this->PdfTableHeader($pdf, $exportColumns); foreach ($rows as $row) { $this->PdfTableRow($pdf, $exportColumns, [ $row['name'], $this->FormatNumber($row['solareinspeisung'], 3), $this->FormatNumber($row['solarverkauf'], 3) ]); } $this->PdfTableRow($pdf, $exportColumns, [ 'Total', $this->FormatNumber($result['totals']['solareinspeisung'], 3), $this->FormatNumber($result['totals']['solarverkauf'], 3) ], true, true); $pdf->Ln(2); $amountColumns = [ ['width' => 42, 'label' => 'Zählpunkt'], ['width' => 25, 'label' => 'Solar CHF', 'align' => 'R'], ['width' => 25, 'label' => 'Netz CHF', 'align' => 'R'], ['width' => 31, 'label' => 'Gutschrift Netz', 'align' => 'R'], ['width' => 31, 'label' => 'Gutschrift ZEV', 'align' => 'R'], ['width' => 26, 'label' => 'Total CHF', 'align' => 'R'] ]; $this->PdfTableHeader($pdf, $amountColumns); foreach ($rows as $row) { $this->PdfTableRow($pdf, $amountColumns, [ $row['name'], $this->FormatCurrency($row['cost_solar'], false), $this->FormatCurrency($row['cost_grid'], false), $this->FormatCurrency($row['rev_feedin'], false), $this->FormatCurrency($row['rev_zev'], false), $this->FormatCurrency($row['subtotal'], false) ]); } $this->PdfTableRow($pdf, $amountColumns, [ 'Zwischentotal Elektrizität', '', '', '', '', $this->FormatCurrency($result['sum'], false) ], true, true); } private function WriteAdditionalPdfTable($pdf, array $result) { $columns = [ ['width' => 30, 'label' => 'Zähler'], ['width' => 20, 'label' => 'Typ'], ['width' => 30, 'label' => 'Zeitraum'], ['width' => 20, 'label' => 'Start', 'align' => 'R'], ['width' => 20, 'label' => 'Ende', 'align' => 'R'], ['width' => 20, 'label' => 'Verbrauch', 'align' => 'R'], ['width' => 18, 'label' => 'Tarif', 'align' => 'R'], ['width' => 22, 'label' => 'Kosten CHF', 'align' => 'R'] ]; $this->PdfTableHeader($pdf, $columns); if (empty($result['rows'])) { $this->PdfTableRow($pdf, [['width' => 180, 'align' => 'C']], ['Keine Nebenkostenzähler für diesen Benutzer.']); } else { foreach ($result['rows'] as $row) { $this->PdfTableRow($pdf, $columns, [ $row['name'], $row['type'], date('d.m.Y', $row['from']) . "\n" . date('d.m.Y', $row['to']), $this->FormatNumber($row['start_value'], 2), $this->FormatNumber($row['end_value'], 2), $this->FormatNumber($row['consumption'], 2), number_format($row['tariff_price'], 3, '.', "'"), $this->FormatCurrency($row['cost'], false) ]); } } $this->PdfTableRow($pdf, $columns, [ 'Zwischentotal Nebenkosten', '', '', '', '', '', '', $this->FormatCurrency($result['sum'], false) ], true, true); } private function WriteTariffPdfTable($pdf, $title, array $tariffs, $unitLabel) { if (empty($tariffs)) { return; } $pdf->Ln(2); $pdf->SetFont('dejavusans', 'B', 8); $pdf->Cell(0, 5, $title, 0, 1, 'L'); $columns = [ ['width' => 48, 'label' => 'Tarifart'], ['width' => 38, 'label' => 'Von'], ['width' => 38, 'label' => 'Bis'], ['width' => 32, 'label' => 'Preis', 'align' => 'R'], ['width' => 24, 'label' => 'Einheit'] ]; $this->PdfTableHeader($pdf, $columns); foreach ($tariffs as $tariff) { if (($tariff['start'] ?? null) === null || ($tariff['end'] ?? null) === null) { continue; } $this->PdfTableRow($pdf, $columns, [ $tariff['unit_type'] ?? '', date('d.m.Y', $tariff['start']), date('d.m.Y', $tariff['end']), number_format((float)($tariff['price'] ?? 0), 3, '.', "'"), $unitLabel ]); } } private function WriteSummaryPdfTable($pdf, array $rows) { $this->PdfSectionTitle($pdf, 'Zusammenfassung'); $columns = [ ['width' => 130, 'label' => 'Position'], ['width' => 50, 'label' => 'Betrag', 'align' => 'R'] ]; foreach ($rows as $index => $row) { $isTotal = $index === count($rows) - 1; $this->PdfTableRow($pdf, $columns, [$row[0], $row[1]], $isTotal, $isTotal); } } // ====================== 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) { $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 ]; $rows = []; $sum = 0.0; if (empty($this->powerCostCache) || !isset($this->powerCostCache[$userId])) { return ['rows' => [], 'totals' => $totals, 'sum' => 0.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); } $rows[] = [ 'name' => (string)($a['name'] ?? $name), 'imp' => (float)$a['imp'], 'exp' => (float)$a['exp'], 'solar_bezug' => (float)$a['solar_bezug'], 'netz_bezug' => (float)$a['netz_bezug'], 'solareinspeisung' => (float)$a['solareinspeisung'], 'solarverkauf' => (float)$a['solarverkauf'], 'cost_solar' => (float)$a['cost_solar'], 'cost_grid' => (float)$a['cost_grid'], 'rev_feedin' => (float)$a['rev_feedin'], 'rev_zev' => (float)$a['rev_zev'], 'subtotal' => (float)$subtotal ]; } return ['rows' => $rows, 'totals' => $totals, 'sum' => $sum]; } // ====================== Nebenkosten Wasser/Wärme ====================== private function CalculateAdditionalCosts($waterMeters, $tariffs, $userId, $from, $to) { $rows = []; $total = 0.0; $usedTariffs = []; 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 (!empty($cost['row'])) { $rows[] = $cost['row']; } $total += $cost['value']; $usedTariffs = array_merge($usedTariffs, $cost['tariffs']); } return ['rows' => $rows, 'sum' => $total, 'tariffs' => $usedTariffs]; } private function AddMeterToPDFRow($meter, $tariffs, $from, $to, $type) { $varId = (int)($meter['var_consumption'] ?? 0); if ($varId <= 0 || !IPS_VariableExists($varId)) { return ['row' => null, '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' => null, 'value' => 0, 'tariffs' => []]; } $verbrauch = max(0, $endValue - $startValue); $kosten = round(($tariffPrice / 100) * $verbrauch, 2); return [ 'row' => [ 'name' => (string)($meter['name'] ?? ''), 'type' => (string)$type, 'from' => (int)$from, 'to' => (int)$to, 'start_value' => (float)$startValue, 'end_value' => (float)$endValue, 'consumption' => (float)$verbrauch, 'tariff_price' => (float)$tariffPrice, 'cost' => (float)$kosten ], '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; } } ?>