486 lines
17 KiB
PHP
486 lines
17 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
require_once __DIR__ . '/libs/EnergyChartCalculator.php';
|
|
|
|
use Belevo\EnelixUtils\Energiediagramm\EnergyChartCalculator;
|
|
|
|
class Energiediagramm extends IPSModule
|
|
{
|
|
private const ARCHIVE_MODULE_ID = '{43192F0B-135B-4CE7-A0A7-1475603F3060}';
|
|
private const ATTR_RANGE = 'SelectedRange';
|
|
private const ATTR_DATE = 'SelectedDate';
|
|
private const ATTR_CUSTOM_START = 'CustomStart';
|
|
private const ATTR_CUSTOM_END = 'CustomEnd';
|
|
private const COLOR_SELF_CONSUMPTION = '#2F80ED';
|
|
private const COLOR_AUTARKY = '#22A06B';
|
|
|
|
public function Create(): void
|
|
{
|
|
parent::Create();
|
|
|
|
$this->RegisterPropertyInteger('ProduktionZaehlerID', 0);
|
|
$this->RegisterPropertyInteger('EinspeisungZaehlerID', 0);
|
|
$this->RegisterPropertyInteger('NetzbezugZaehlerID', 0);
|
|
$this->RegisterPropertyInteger('VerbrauchZaehlerID', 0);
|
|
$this->RegisterPropertyFloat('Zaehlerfaktor', 1.0);
|
|
$this->RegisterPropertyString('Standardzeitraum', 'day');
|
|
$this->RegisterPropertyInteger('Aktualisierungsintervall', 60);
|
|
$this->RegisterPropertyString('FarbeEigenverbrauch', self::COLOR_SELF_CONSUMPTION);
|
|
$this->RegisterPropertyString('FarbeAutarkie', self::COLOR_AUTARKY);
|
|
$this->RegisterPropertyBoolean('Debug', false);
|
|
|
|
$today = date('Y-m-d');
|
|
$this->RegisterAttributeString(self::ATTR_RANGE, 'day');
|
|
$this->RegisterAttributeString(self::ATTR_DATE, $today);
|
|
$this->RegisterAttributeString(self::ATTR_CUSTOM_START, $today);
|
|
$this->RegisterAttributeString(self::ATTR_CUSTOM_END, $today);
|
|
|
|
$visualizationType = version_compare(IPS_GetKernelVersion(), '9.0', '>=')
|
|
? 2
|
|
: 1;
|
|
$this->SetVisualizationType($visualizationType);
|
|
$this->RegisterTimer(
|
|
'Refresh',
|
|
0,
|
|
'IPS_RequestAction($_IPS["TARGET"], "Refresh", true);'
|
|
);
|
|
}
|
|
|
|
public function ApplyChanges(): void
|
|
{
|
|
parent::ApplyChanges();
|
|
|
|
$today = date('Y-m-d');
|
|
$defaultRange = $this->ReadPropertyString('Standardzeitraum');
|
|
if (!in_array($defaultRange, ['day', 'week', 'month', 'year', 'total'], true)) {
|
|
$defaultRange = 'day';
|
|
}
|
|
|
|
$this->WriteAttributeString(self::ATTR_RANGE, $defaultRange);
|
|
$this->WriteAttributeString(self::ATTR_DATE, $today);
|
|
if (!$this->validStoredDate($this->ReadAttributeString(self::ATTR_CUSTOM_START))) {
|
|
$this->WriteAttributeString(self::ATTR_CUSTOM_START, $today);
|
|
}
|
|
if (!$this->validStoredDate($this->ReadAttributeString(self::ATTR_CUSTOM_END))) {
|
|
$this->WriteAttributeString(self::ATTR_CUSTOM_END, $today);
|
|
}
|
|
|
|
$interval = $this->ReadPropertyInteger('Aktualisierungsintervall');
|
|
$this->SetTimerInterval('Refresh', max(10, min(3600, $interval)) * 1000);
|
|
|
|
if ($this->requiredSourceIdsAreEmpty()) {
|
|
$this->SetStatus(104);
|
|
} elseif (!$this->configurationIsValid()) {
|
|
$this->SetStatus(201);
|
|
} elseif ($this->archiveId() === 0) {
|
|
$this->SetStatus(202);
|
|
} else {
|
|
$this->SetStatus(102);
|
|
}
|
|
|
|
$this->recalculateAndPush();
|
|
}
|
|
|
|
public function GetVisualizationTile(): string
|
|
{
|
|
$path = __DIR__ . '/module.html';
|
|
$html = @file_get_contents($path);
|
|
if ($html === false) {
|
|
return '<div style="padding:12px">Darstellung nicht verfügbar.</div>';
|
|
}
|
|
|
|
try {
|
|
$initial = json_encode(
|
|
$this->buildPayload(),
|
|
JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_THROW_ON_ERROR
|
|
);
|
|
} catch (Throwable $error) {
|
|
$initial = '{"hasData":false,"noDataHint":"Darstellung konnte nicht geladen werden."}';
|
|
}
|
|
|
|
return str_replace('__ENELIX_INITIAL_STATE__', $initial, $html);
|
|
}
|
|
|
|
public function GetVisualizationPopup(): string
|
|
{
|
|
return $this->GetVisualizationTile();
|
|
}
|
|
|
|
public function RequestAction($Ident, $Value): void
|
|
{
|
|
try {
|
|
switch ((string) $Ident) {
|
|
case 'SetRange':
|
|
$this->setRange((string) $Value);
|
|
break;
|
|
case 'SetDate':
|
|
$this->setDate((string) $Value);
|
|
break;
|
|
case 'SetCustomRange':
|
|
$this->setCustomRange((string) $Value);
|
|
break;
|
|
case 'Prev':
|
|
case 'Next':
|
|
case 'Today':
|
|
$this->shiftDate((string) $Ident);
|
|
break;
|
|
case 'Refresh':
|
|
break;
|
|
default:
|
|
throw new InvalidArgumentException('Unbekannte Aktion.');
|
|
}
|
|
|
|
$this->recalculateAndPush();
|
|
} catch (Throwable $error) {
|
|
$this->debug('Aktion abgewiesen', $error->getMessage());
|
|
}
|
|
}
|
|
|
|
private function setRange(string $range): void
|
|
{
|
|
if (!in_array($range, EnergyChartCalculator::RANGES, true)) {
|
|
throw new InvalidArgumentException('Unbekannter Zeitraum.');
|
|
}
|
|
|
|
$this->WriteAttributeString(self::ATTR_RANGE, $range);
|
|
}
|
|
|
|
private function setDate(string $date): void
|
|
{
|
|
if (!$this->validStoredDate($date)) {
|
|
throw new InvalidArgumentException('Ungültiges oder zukünftiges Datum.');
|
|
}
|
|
|
|
$this->WriteAttributeString(self::ATTR_DATE, $date);
|
|
}
|
|
|
|
private function setCustomRange(string $json): void
|
|
{
|
|
try {
|
|
$value = json_decode($json, true, 512, JSON_THROW_ON_ERROR);
|
|
} catch (JsonException $error) {
|
|
throw new InvalidArgumentException('Ungültiger Datumsbereich.', 0, $error);
|
|
}
|
|
|
|
$start = is_array($value) ? (string) ($value['start'] ?? '') : '';
|
|
$end = is_array($value) ? (string) ($value['end'] ?? '') : '';
|
|
if (!$this->validStoredDate($start) || !$this->validStoredDate($end)) {
|
|
throw new InvalidArgumentException('Ungültiger oder zukünftiger Datumsbereich.');
|
|
}
|
|
if ($start > $end) {
|
|
throw new InvalidArgumentException('Das Startdatum liegt nach dem Enddatum.');
|
|
}
|
|
|
|
$this->WriteAttributeString(self::ATTR_CUSTOM_START, $start);
|
|
$this->WriteAttributeString(self::ATTR_CUSTOM_END, $end);
|
|
$this->WriteAttributeString(self::ATTR_RANGE, 'custom');
|
|
}
|
|
|
|
private function shiftDate(string $action): void
|
|
{
|
|
$range = $this->ReadAttributeString(self::ATTR_RANGE);
|
|
if ($range === 'total' || $range === 'custom') {
|
|
return;
|
|
}
|
|
|
|
if ($action === 'Today') {
|
|
$this->WriteAttributeString(self::ATTR_DATE, date('Y-m-d'));
|
|
return;
|
|
}
|
|
|
|
$date = $this->ReadAttributeString(self::ATTR_DATE);
|
|
$base = strtotime($date . ' 00:00:00');
|
|
if ($base === false) {
|
|
$base = strtotime(date('Y-m-d') . ' 00:00:00');
|
|
}
|
|
|
|
$previous = $action === 'Prev';
|
|
$modifier = [
|
|
'day' => $previous ? '-1 day' : '+1 day',
|
|
'week' => $previous ? '-1 week' : '+1 week',
|
|
'month' => $previous ? 'first day of previous month' : 'first day of next month',
|
|
'year' => $previous ? 'first day of january previous year' : 'first day of january next year',
|
|
][$range] ?? 'today';
|
|
|
|
$shifted = strtotime($modifier, $base);
|
|
if ($shifted === false || $shifted > time()) {
|
|
$shifted = time();
|
|
}
|
|
$this->WriteAttributeString(self::ATTR_DATE, date('Y-m-d', $shifted));
|
|
}
|
|
|
|
private function recalculateAndPush(): void
|
|
{
|
|
try {
|
|
$payload = $this->buildPayload();
|
|
$this->UpdateVisualizationValue(json_encode($payload, JSON_THROW_ON_ERROR));
|
|
} catch (Throwable $error) {
|
|
$this->debug('Aktualisierung fehlgeschlagen', $error->getMessage());
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @return array<string, mixed>
|
|
*/
|
|
private function buildPayload(): array
|
|
{
|
|
$range = $this->ReadAttributeString(self::ATTR_RANGE);
|
|
if (!in_array($range, EnergyChartCalculator::RANGES, true)) {
|
|
$range = $this->ReadPropertyString('Standardzeitraum');
|
|
}
|
|
if (!in_array($range, EnergyChartCalculator::RANGES, true)) {
|
|
$range = 'day';
|
|
}
|
|
|
|
$date = $this->ReadAttributeString(self::ATTR_DATE);
|
|
if (!$this->validStoredDate($date)) {
|
|
$date = date('Y-m-d');
|
|
}
|
|
$customStart = $this->ReadAttributeString(self::ATTR_CUSTOM_START);
|
|
$customEnd = $this->ReadAttributeString(self::ATTR_CUSTOM_END);
|
|
if (!$this->validStoredDate($customStart)) {
|
|
$customStart = date('Y-m-d');
|
|
}
|
|
if (!$this->validStoredDate($customEnd)) {
|
|
$customEnd = date('Y-m-d');
|
|
}
|
|
|
|
[$start, $end] = EnergyChartCalculator::rangeBounds(
|
|
$range,
|
|
$date,
|
|
$customStart,
|
|
$customEnd
|
|
);
|
|
|
|
$payload = [
|
|
'range' => $range,
|
|
'date' => $date,
|
|
'customStart' => $customStart,
|
|
'customEnd' => $customEnd,
|
|
'tStart' => $start,
|
|
'tEnd' => $end,
|
|
'hasData' => false,
|
|
'noDataHint' => 'Energiezähler konfigurieren.',
|
|
'colors' => [
|
|
'selfConsumption' => EnergyChartCalculator::normalizeColor(
|
|
$this->ReadPropertyString('FarbeEigenverbrauch'),
|
|
self::COLOR_SELF_CONSUMPTION
|
|
),
|
|
'autarky' => EnergyChartCalculator::normalizeColor(
|
|
$this->ReadPropertyString('FarbeAutarkie'),
|
|
self::COLOR_AUTARKY
|
|
),
|
|
],
|
|
'values' => EnergyChartCalculator::metrics(0.0, 0.0, 0.0),
|
|
];
|
|
|
|
if (!$this->configurationIsValid()) {
|
|
return $payload;
|
|
}
|
|
|
|
$archiveId = $this->archiveId();
|
|
if ($archiveId === 0) {
|
|
$payload['noDataHint'] = 'Das IP-Symcon-Archiv ist nicht verfügbar.';
|
|
return $payload;
|
|
}
|
|
|
|
$factor = $this->ReadPropertyFloat('Zaehlerfaktor');
|
|
$total = $range === 'total';
|
|
$production = $this->readEnergy(
|
|
$archiveId,
|
|
$this->ReadPropertyInteger('ProduktionZaehlerID'),
|
|
$start,
|
|
$end,
|
|
$total
|
|
);
|
|
$feedIn = $this->readEnergy(
|
|
$archiveId,
|
|
$this->ReadPropertyInteger('EinspeisungZaehlerID'),
|
|
$start,
|
|
$end,
|
|
$total
|
|
);
|
|
$grid = $this->readEnergy(
|
|
$archiveId,
|
|
$this->ReadPropertyInteger('NetzbezugZaehlerID'),
|
|
$start,
|
|
$end,
|
|
$total
|
|
);
|
|
|
|
$consumption = null;
|
|
$consumptionAvailable = true;
|
|
$consumptionId = $this->ReadPropertyInteger('VerbrauchZaehlerID');
|
|
if ($consumptionId > 0) {
|
|
$consumptionResult = $this->readEnergy($archiveId, $consumptionId, $start, $end, $total);
|
|
$consumptionAvailable = $consumptionResult['available'];
|
|
if ($consumptionAvailable) {
|
|
$consumption = $consumptionResult['value'] * $factor;
|
|
}
|
|
}
|
|
|
|
$payload['hasData'] = $production['available']
|
|
&& $feedIn['available']
|
|
&& $grid['available']
|
|
&& $consumptionAvailable;
|
|
$payload['values'] = EnergyChartCalculator::metrics(
|
|
$production['value'] * $factor,
|
|
$feedIn['value'] * $factor,
|
|
$grid['value'] * $factor,
|
|
$consumption
|
|
);
|
|
|
|
if (!$payload['hasData']) {
|
|
$lastLog = max(
|
|
$this->lastLogTimestamp($archiveId, $this->ReadPropertyInteger('ProduktionZaehlerID')),
|
|
$this->lastLogTimestamp($archiveId, $this->ReadPropertyInteger('EinspeisungZaehlerID')),
|
|
$this->lastLogTimestamp($archiveId, $this->ReadPropertyInteger('NetzbezugZaehlerID')),
|
|
$this->lastLogTimestamp($archiveId, $consumptionId)
|
|
);
|
|
$payload['noDataHint'] = $lastLog > 0
|
|
? 'Letzter Archivwert: ' . date('d.m.Y H:i', $lastLog)
|
|
: 'Für diesen Zeitraum sind keine Archivwerte vorhanden.';
|
|
} else {
|
|
$payload['noDataHint'] = '';
|
|
}
|
|
|
|
return $payload;
|
|
}
|
|
|
|
/**
|
|
* @return array{value: float, available: bool}
|
|
*/
|
|
private function readEnergy(
|
|
int $archiveId,
|
|
int $variableId,
|
|
int $start,
|
|
int $end,
|
|
bool $total
|
|
): array {
|
|
if (!$this->isNumericVariable($variableId)) {
|
|
return ['value' => 0.0, 'available' => false];
|
|
}
|
|
|
|
if ($total) {
|
|
$value = GetValue($variableId);
|
|
return [
|
|
'value' => is_numeric($value) ? max(0.0, (float) $value) : 0.0,
|
|
'available' => is_numeric($value),
|
|
];
|
|
}
|
|
|
|
$before = @AC_GetLoggedValues($archiveId, $variableId, 0, $start, 1);
|
|
$within = @AC_GetLoggedValues($archiveId, $variableId, $start, min($end, time()), 0);
|
|
$before = is_array($before) ? $before : [];
|
|
$within = is_array($within) ? $within : [];
|
|
usort($before, static fn (array $left, array $right): int =>
|
|
((int) $left['TimeStamp']) <=> ((int) $right['TimeStamp'])
|
|
);
|
|
usort($within, static fn (array $left, array $right): int =>
|
|
((int) $left['TimeStamp']) <=> ((int) $right['TimeStamp'])
|
|
);
|
|
|
|
if ($before === [] && $within === []) {
|
|
return ['value' => 0.0, 'available' => false];
|
|
}
|
|
|
|
$startValue = $before !== []
|
|
? (float) $before[count($before) - 1]['Value']
|
|
: (float) $within[0]['Value'];
|
|
$endValue = $within !== []
|
|
? (float) $within[count($within) - 1]['Value']
|
|
: $startValue;
|
|
|
|
return [
|
|
'value' => max(0.0, $endValue - $startValue),
|
|
'available' => true,
|
|
];
|
|
}
|
|
|
|
private function lastLogTimestamp(int $archiveId, int $variableId): int
|
|
{
|
|
if (!$this->isNumericVariable($variableId)) {
|
|
return 0;
|
|
}
|
|
|
|
$values = @AC_GetLoggedValues($archiveId, $variableId, 0, time(), 1);
|
|
return is_array($values) && isset($values[0]['TimeStamp'])
|
|
? (int) $values[0]['TimeStamp']
|
|
: 0;
|
|
}
|
|
|
|
private function configurationIsValid(): bool
|
|
{
|
|
$requiredIds = [
|
|
$this->ReadPropertyInteger('ProduktionZaehlerID'),
|
|
$this->ReadPropertyInteger('EinspeisungZaehlerID'),
|
|
$this->ReadPropertyInteger('NetzbezugZaehlerID'),
|
|
];
|
|
foreach ($requiredIds as $variableId) {
|
|
if (!$this->isNumericVariable($variableId)) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
$consumptionId = $this->ReadPropertyInteger('VerbrauchZaehlerID');
|
|
if ($consumptionId > 0 && !$this->isNumericVariable($consumptionId)) {
|
|
return false;
|
|
}
|
|
|
|
return $this->ReadPropertyFloat('Zaehlerfaktor') > 0.0
|
|
&& $this->ReadPropertyInteger('Aktualisierungsintervall') >= 10
|
|
&& $this->ReadPropertyInteger('Aktualisierungsintervall') <= 3600
|
|
&& in_array(
|
|
$this->ReadPropertyString('Standardzeitraum'),
|
|
['day', 'week', 'month', 'year', 'total'],
|
|
true
|
|
)
|
|
&& EnergyChartCalculator::normalizeColor(
|
|
$this->ReadPropertyString('FarbeEigenverbrauch'),
|
|
''
|
|
) !== ''
|
|
&& EnergyChartCalculator::normalizeColor(
|
|
$this->ReadPropertyString('FarbeAutarkie'),
|
|
''
|
|
) !== '';
|
|
}
|
|
|
|
private function requiredSourceIdsAreEmpty(): bool
|
|
{
|
|
return $this->ReadPropertyInteger('ProduktionZaehlerID') === 0
|
|
&& $this->ReadPropertyInteger('EinspeisungZaehlerID') === 0
|
|
&& $this->ReadPropertyInteger('NetzbezugZaehlerID') === 0;
|
|
}
|
|
|
|
private function archiveId(): int
|
|
{
|
|
$archives = IPS_GetInstanceListByModuleID(self::ARCHIVE_MODULE_ID);
|
|
return isset($archives[0]) ? (int) $archives[0] : 0;
|
|
}
|
|
|
|
private function isNumericVariable(int $variableId): bool
|
|
{
|
|
if ($variableId <= 0 || !IPS_VariableExists($variableId)) {
|
|
return false;
|
|
}
|
|
|
|
$variable = IPS_GetVariable($variableId);
|
|
return in_array((int) ($variable['VariableType'] ?? -1), [1, 2], true);
|
|
}
|
|
|
|
private function validStoredDate(string $date): bool
|
|
{
|
|
return EnergyChartCalculator::isValidDate($date)
|
|
&& $date <= date('Y-m-d');
|
|
}
|
|
|
|
private function debug(string $title, string $message): void
|
|
{
|
|
if ($this->ReadPropertyBoolean('Debug')) {
|
|
$this->SendDebug($title, $message, 0);
|
|
}
|
|
}
|
|
}
|