Files
Symcon_Belevo_Energiemanage…/Abrechnung/module.php
2025-12-12 11:17:51 +01:00

855 lines
29 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='margin:0 0 0 0; font-size:11px;'>
Gesamttotal: <strong>CHF " . number_format($grandTotal, 2) . ".-</strong>
</h2>
";
$pdf->writeHTML($html, true, false, true, false, '');
}
// ====================== Stromkosten (15-Minuten, alle User) ======================
private function CalculateAllPowerCosts($powerMeters, $tariffs, $from, $to)
{
$this->powerCostCache = [];
// Zähler je User aufbauen
$metersByUser = [];
foreach ($powerMeters as $m) {
if (!isset($m['user_id'])) {
continue;
}
$userId = (int)$m['user_id'];
$name = $m['name'] ?? ('Meter_' . $m['user_id']);
if (!isset($metersByUser[$userId])) {
$metersByUser[$userId] = [];
}
$metersByUser[$userId][$name] = [
'importVar' => $m['var_consumption'] ?? null,
'exportVar' => $m['var_feed'] ?? null
];
if (!isset($this->powerCostCache[$userId])) {
$this->powerCostCache[$userId] = [];
}
$this->powerCostCache[$userId][$name] = [
'name' => $name,
'imp' => 0.0,
'exp' => 0.0,
'solar_bezug' => 0.0,
'netz_bezug' => 0.0,
'solareinspeisung' => 0.0,
'solarverkauf' => 0.0,
'cost_solar' => 0.0,
'cost_grid' => 0.0,
'rev_feedin' => 0.0,
'rev_zev' => 0.0
];
}
if (empty($metersByUser)) {
return;
}
// 15-Minuten-Schritte
for ($ts = $from; $ts < $to; $ts += 900) {
$slotEnd = min($to, $ts + 900);
// Tarife (gleich für alle User)
$pGrid = $this->getTariffPriceAt($tariffs, ['Netztarif'], $ts);
$pSolar = $this->getTariffPriceAt($tariffs, ['Solartarif'], $ts);
$pFeed = $this->getTariffPriceAt($tariffs, ['Einspeisetarif'], $ts);
// ==========================================================
// 1) GLOBAL DELTAS EINMAL BERECHNEN
// ==========================================================
$globalImpTotal = 0.0;
$globalExpTotal = 0.0;
$globalSlot = [];
foreach ($powerMeters as $m) {
$name = $m['name'];
$impDelta = 0.0;
$expDelta = 0.0;
// IMPORT
if (!empty($m['var_consumption']) && IPS_VariableExists((int)$m['var_consumption'])) {
$impDelta = $this->getDeltaFromArchive((int)$m['var_consumption'], $ts, $slotEnd);
}
// EXPORT
if (!empty($m['var_feed']) && IPS_VariableExists((int)$m['var_feed'])) {
$expDelta = $this->getDeltaFromArchive((int)$m['var_feed'], $ts, $slotEnd);
}
if ($impDelta > 0.0 || $expDelta > 0.0) {
$globalSlot[$name] = [
'imp' => $impDelta,
'exp' => $expDelta,
'user_id' => (int)$m['user_id']
];
$globalImpTotal += $impDelta;
$globalExpTotal += $expDelta;
}
}
// Wenn keine Imp/Exp vorhanden → nächster Slot
if ($globalImpTotal == 0.0 && $globalExpTotal == 0.0) {
continue;
}
// ==========================================================
// 2) Verhältnis PV / Netz
// ==========================================================
if ($globalImpTotal == 0.0 && $globalExpTotal > 0.0) {
// nur Export → PV deckt alles
$ratio = 0.0;
$pvCoversAll = true;
} elseif ($globalExpTotal == 0.0 && $globalImpTotal > 0.0) {
// nur Import → Netz deckt alles
$ratio = 0.0;
$pvCoversAll = false;
} elseif ($globalImpTotal <= $globalExpTotal) {
// PV produziert genug
$ratio = $globalExpTotal ? ($globalImpTotal / $globalExpTotal) : 0.0;
$pvCoversAll = true;
} else {
// Netzbezug notwendig
$ratio = $globalImpTotal ? ($globalExpTotal / $globalImpTotal) : 0.0;
$pvCoversAll = false;
}
// ==========================================================
// 3) PRO USER VERTEILEN
// ==========================================================
foreach ($globalSlot as $name => $v) {
$userId = $v['user_id'];
// Falls User nicht im Cache ist → ignorieren
if (!isset($this->powerCostCache[$userId][$name])) {
continue;
}
$imp = $v['imp'];
$exp = $v['exp'];
// Totale speichern
$this->powerCostCache[$userId][$name]['imp'] += $imp;
$this->powerCostCache[$userId][$name]['exp'] += $exp;
// PV deckt alles
if ($pvCoversAll) {
$this->powerCostCache[$userId][$name]['solar_bezug'] += $imp;
$this->powerCostCache[$userId][$name]['solareinspeisung'] += (1 - $ratio) * $exp;
$this->powerCostCache[$userId][$name]['solarverkauf'] += $ratio * $exp;
if ($pSolar !== null) {
$this->powerCostCache[$userId][$name]['cost_solar'] += ($imp * $pSolar) / 100.0;
}
if ($pFeed !== null) {
$this->powerCostCache[$userId][$name]['rev_feedin'] += ((1 - $ratio) * $exp * $pFeed) / 100.0;
$this->powerCostCache[$userId][$name]['rev_zev'] += ($ratio * $exp * $pSolar) / 100.0;
}
// Netz deckt einen Teil
} else {
$this->powerCostCache[$userId][$name]['solar_bezug'] += $ratio * $imp;
$this->powerCostCache[$userId][$name]['netz_bezug'] += (1 - $ratio) * $imp;
$this->powerCostCache[$userId][$name]['solarverkauf'] += $exp;
if ($pGrid !== null) {
$this->powerCostCache[$userId][$name]['cost_grid'] += ((1 - $ratio) * $imp * $pGrid) / 100.0;
}
if ($pSolar !== null) {
$this->powerCostCache[$userId][$name]['cost_solar'] += ($ratio * $imp * $pSolar) / 100.0;
}
if ($pFeed !== null) {
$this->powerCostCache[$userId][$name]['rev_zev'] += ($exp * $pSolar) / 100.0;
}
}
}
}
}
private function GetCalculatedPowerCosts($userId)
{
$html = "
<style>
table.powercost {
width: 100%;
border-collapse: collapse;
font-size: 10px;
}
table.powercost th {
background-color: #444;
color: white;
padding: 4px;
text-align: center;
font-weight: bold;
border: 1px solid #777;
}
table.powercost td {
padding: 3px 4px;
border: 1px solid #ccc;
}
.row-even { background-color: #f7f7f7; }
.row-odd { background-color: #ffffff; }
.row-empty { background-color: #e0e0e0; height: 5px; }
/* Neue Formatierungen */
.num {
text-align: right;
font-family: monospace;
width: 90px;
}
.subtotal-row {
border-top: 1px solid black;
font-weight: bold;
}
.total-row {
border-top: 3px double black;
font-weight: bold;
background-color: #ddd;
}
</style>
<table class='powercost'>
<tr>
<th>ID</th>
<th>Import (kWh)</th>
<th>Export (kWh)</th>
<th>ZEV-Haus (kWh)</th>
<th>Netz-Haus (kWh)</th>
<th>Solar-Netz (kWh)</th>
<th>Solar-ZEV (kWh)</th>
<th>Kauf Solar (CHF)</th>
<th>Kauf Netz (CHF)</th>
<th>Verkauf Netz(CHF)</th>
<th>Verkauf ZEV(CHF)</th>
<th>Total CHF</th>
</tr>";
if (empty($this->powerCostCache) || !isset($this->powerCostCache[$userId])) {
$html .= "<tr><td colspan='12' align='center'>Keine Stromzähler für diesen Benutzer</td></tr></table>";
return ['html' => $html, 'sum' => 0.0];
}
$sum = 0.0;
$rowIndex = 0;
foreach ($this->powerCostCache[$userId] as $name => $a) {
$subtotal = $a['cost_grid'] + $a['cost_solar'] - ($a['rev_feedin'] + $a['rev_zev']);
$sum += $subtotal;
$rowClass = ($rowIndex % 2 === 0) ? "row-even" : "row-odd";
$rowIndex++;
// Datenzeile
$html .= "<tr class='{$rowClass}'>
<td>{$a['name']}</td>
<td class='num' style='background-color:#f0f0f0; text-align: right;'>" . number_format($a['imp'], 3, '.', "'") . "</td>
<td class='num' text-align='right'>" . number_format($a['exp'], 3, '.', "'") . "</td>
<td class='num' text-align='right'>" . number_format($a['solar_bezug'], 3, '.', "'") . "</td>
<td class='num' text-align='right'>" . number_format($a['netz_bezug'], 3, '.', "'") . "</td>
<td class='num' text-align='right'>" . number_format($a['solareinspeisung'], 3, '.', "'") . "</td>
<td class='num' text-align='right'>" . number_format($a['solarverkauf'], 3, '.', "'") . "</td>
<td class='num' text-align='right'>" . number_format($a['cost_solar'], 2, '.', "'") . "</td>
<td class='num' text-align='right'>" . number_format($a['cost_grid'], 2, '.', "'") . "</td>
<td class='num' text-align='right'>" . number_format($a['rev_feedin'], 2, '.', "'") . "</td>
<td class='num' text-align='right'>" . number_format($a['rev_zev'], 2, '.', "'") . "</td>
<td class='num underline' text-align='right'><strong>" . number_format($subtotal, 2, '.', "'") . "</strong></td>
</tr>";
// Leerzeile
$html .= "<tr class='row-empty'><td colspan='12'></td></tr>";
}
// Gesamttotal
$html .= "
<tr class='total-row'>
<td colspan='11' text-align='right'><b>Total</b></td>
<td align='right'></td>
<td align='right'></td>
<td align='right'></td>
<td align='right'></td>
<td align='right'></td>
<td align='right'></td>
<td align='right'></td>
<td align='right'></td>
<td align='right'></td>
<td align='right'></td>
<td class='num double-underline' text-align='right'><strong>" . number_format($sum, 2, '.', "'") . "</strong></td>
</tr>
</table>";
return ['html' => $html, 'sum' => $sum];
}
// ====================== Nebenkosten Wasser/Wärme ======================
private function CalculateAdditionalCosts($waterMeters, $tariffs, $userId, $from, $to)
{
$html = "
<table border='1' cellspacing='0' cellpadding='2' width='100%' style='font-size:4px;'>
<tr style='background-color:#f0f0f0;'>
<th>ID</th>
<th>Typ</th>
<th>Datum von</th>
<th>Datum bis</th>
<th>Zähler Start</th>
<th>Zähler Ende</th>
<th>Verbrauch</th>
<th>Tarif</th>
<th>Kosten CHF</th>
</tr>";
$total = 0.0;
$usedTariffs = [];
foreach ($waterMeters as $m) {
if ($m['user_id'] != $userId) {
continue;
}
$type = $m['meter_type'] ?? 'Warmwasser';
$cost = $this->AddMeterToPDFRow($m, $tariffs, $from, $to, $type);
$html .= $cost['row'];
$total += $cost['value'];
$usedTariffs = array_merge($usedTariffs, $cost['tariffs']);
}
$html .= "<tr style='background-color:#f9f9f9; font-weight:bold;'>
<td colspan='25' align='right'><b>Total</b></td>
<td align='right'></td>
<td align='right'></td>
<td align='right'></td>
<td align='right'></td>
<td align='right'></td>
<td align='right'></td>
<td align='right'></td>
<td align='right'><b>CHF " . number_format($total, 2) . ".-</b></td>
</tr>";
if (!empty($usedTariffs)) {
$html .= "<p><strong>Angewendete Nebenkostentarife</strong></p><ul>";
foreach ($usedTariffs as $t) {
$html .= "<li>" . date('d.m.Y', $t['start']) . " " . date('d.m.Y', $t['end']) . " — <strong> " .
number_format($t['price'], 2) . " Rp/m3</strong></li>";
}
$html .= "</ul><br><hr>";
}
return ['html' => $html, 'sum' => $total];
}
private function AddMeterToPDFRow($meter, $tariffs, $from, $to, $type)
{
$rows = '';
$totalCost = 0.0;
$usedTariffs = [];
$varId = $meter['var_consumption'];
if (!IPS_VariableExists($varId)) {
return ['row' => '', 'value' => 0, 'tariffs' => []];
}
// Relevante Tarife nach Typ filtern
$filteredTariffs = array_filter($tariffs, fn($t) =>
strtolower(trim($t['unit_type'] ?? '')) === strtolower(trim($type))
);
$activeTariff = null;
foreach ($filteredTariffs as $t) {
$startTs = $this->toUnixTs($t['start'], false);
$endTs = $this->toUnixTs($t['end'], true);
if ($startTs === null || $endTs === null) {
continue;
}
// Tarif überlappt den Abrechnungszeitraum
if ($endTs < $from || $startTs > $to) {
continue;
}
$activeTariff = [
'start' => $startTs,
'end' => $endTs,
'price' => (float)$t['price']
];
$usedTariffs[] = $activeTariff;
break; // erster passender Tarif reicht
}
$tariffPrice = $activeTariff ? $activeTariff['price'] : 0.0;
// Verbrauch = letzter Wert vor/gleich "to" minus letzter Wert vor/gleich "from"
$startValue = $this->GetValueAt($varId, $from, false);
$endValue = $this->GetValueAt($varId, $to, false);
if ($startValue === null || $endValue === null) {
return ['row' => '', 'value' => 0, 'tariffs' => []];
}
$verbrauch = max(0, $endValue - $startValue);
$kosten = round(($tariffPrice / 100) * $verbrauch, 2);
$totalCost = $kosten;
$rows .= "<tr>
<td>{$meter['name']}</td>
<td>{$type}</td>
<td align='center'>" . date('d.m.Y', $from) . "</td>
<td align='center'>" . date('d.m.Y', $to) . "</td>
<td align='right'>" . number_format($startValue, 2) . "</td>
<td align='right'>" . number_format($endValue, 2) . "</td>
<td align='right'>" . number_format($verbrauch, 2) . "</td>
<td align='right'>" . number_format($tariffPrice, 2) . "</td>
<td align='right'>" . number_format($kosten, 2) . "</td>
</tr>";
if ($rows === '') {
return ['row' => '', 'value' => 0, 'tariffs' => []];
}
return ['row' => $rows, 'value' => $totalCost, 'tariffs' => array_values($usedTariffs)];
}
// ====================== Hilfsfunktionen ======================
private function GetValueAt($varId, $timestamp, $nearestAfter = true)
{
$archiveID = @IPS_GetInstanceListByModuleID('{43192F0B-135B-4CE7-A0A7-1475603F3060}')[0];
if (!$archiveID || !IPS_VariableExists($varId)) {
return null;
}
if ($nearestAfter) {
// Erster Wert NACH oder GENAU ab Timestamp
$values = @AC_GetLoggedValues(
$archiveID,
$varId,
$timestamp, // start
time(), // end
1 // LIMIT
);
} else {
// Letzter Wert DAVOR oder GENAU bis Timestamp
$values = @AC_GetLoggedValues(
$archiveID,
$varId,
0, // start
$timestamp, // end
1 // LIMIT
);
}
if (!empty($values)) {
return (float)$values[0]['Value'];
}
// Fallback → Live-Wert
return (float)GetValue($varId);
}
private function getDeltaFromArchive(int $varId, int $tStart, int $tEnd): float
{
// Werte holen
$startValue = $this->GetValueAt($varId, $tStart, false);
$endValue = $this->GetValueAt($varId, $tEnd, false);
if ($startValue === null || $endValue === null) {
return 0.0;
}
// Delta berechnen
$diff = $endValue - $startValue;
if ($diff < 0) {
$diff = 0.0;
}
return (float)$diff;
}
private function toUnixTs($val, $endOfDay = false)
{
if (is_int($val)) {
return $val;
}
if (is_numeric($val)) {
return (int)$val;
}
if (is_string($val)) {
$s = trim($val);
if ($s !== '' && $s[0] === '{') {
$obj = json_decode($s, true);
if (isset($obj['year'], $obj['month'], $obj['day'])) {
$time = $endOfDay ? '23:59:59' : '00:00:00';
return strtotime(sprintf(
'%04d-%02d-%02d %s',
$obj['year'],
$obj['month'],
$obj['day'],
$time
));
}
}
$ts = strtotime($s);
return $ts === false ? null : $ts;
}
return null;
}
private function readDelta($varId, $tStart, $tEnd)
{
if (!is_int($tStart)) {
$tStart = strtotime($tStart);
}
if (!is_int($tEnd)) {
$tEnd = strtotime($tEnd);
}
$archiveID = @IPS_GetInstanceListByModuleID('{43192F0B-135B-4CE7-A0A7-1475603F3060}')[0];
if (!$archiveID || !IPS_VariableExists($varId)) {
return 0.0;
}
$values = @AC_GetLoggedValues($archiveID, $varId, $tStart - 86400, $tEnd + 86400, 0);
if (empty($values)) {
return 0.0;
}
usort($values, fn($a, $b) => (int)$a['TimeStamp'] <=> (int)$b['TimeStamp']);
$vStart = null;
$vEnd = null;
foreach ($values as $v) {
if ($v['TimeStamp'] <= $tStart) {
$vStart = $v['Value'];
}
if ($v['TimeStamp'] <= $tEnd) {
$vEnd = $v['Value'];
}
if ($v['TimeStamp'] > $tEnd) {
break;
}
}
if ($vStart === null) {
$vStart = (float)GetValue($varId);
}
if ($vEnd === null) {
$vEnd = (float)GetValue($varId);
}
$diff = $vEnd - $vStart;
return ($diff < 0) ? 0.0 : $diff;
}
private function getTariffPriceAt($tariffs, $typeSynonyms, $ts)
{
$wanted = array_map('strtolower', $typeSynonyms);
$cands = [];
foreach ($tariffs as $t) {
$u = strtolower(trim($t['unit_type'] ?? ''));
if (!in_array($u, $wanted, true)) {
continue;
}
$s = $this->toUnixTs($t['start'], false);
$e = $this->toUnixTs($t['end'], true);
if (!$s || !$e) {
continue;
}
if ($s <= $ts && $ts <= $e) {
$cands[] = (float)$t['price'];
}
}
if (empty($cands)) {
return null;
}
return end($cands);
}
private function CollectTariffsForUser($tariffs, $types)
{
$result = [];
$wanted = array_map('strtolower', $types);
foreach ($tariffs as $t) {
$type = strtolower(trim($t['unit_type'] ?? ''));
if (!in_array($type, $wanted, true)) {
continue;
}
$start = $this->toUnixTs($t['start'], false);
$end = $this->toUnixTs($t['end'], true);
$result[] = [
'start' => $start,
'end' => $end,
'price' => (float)$t['price']
];
}
return $result;
}
private function GetLogoFile()
{
$mediaID = (int)$this->ReadPropertyInteger('LogoMediaID');
if ($mediaID <= 0 || !IPS_MediaExists($mediaID)) {
return null;
}
$media = IPS_GetMedia($mediaID);
$ext = 'png';
if (!empty($media['MediaFile'])) {
$extFromFile = pathinfo($media['MediaFile'], PATHINFO_EXTENSION);
if ($extFromFile !== '') {
$ext = $extFromFile;
}
}
$path = IPS_GetKernelDir() . 'media/logo_' . $mediaID . '.' . $ext;
// Bild aus der Symcon-Media-Datenbank extrahieren
$raw = IPS_GetMediaContent($mediaID); // base64
$bin = base64_decode($raw);
if ($bin === false) {
return null;
}
file_put_contents($path, $bin);
return $path;
}
}
?>