Update Websocket und weiteres.

This commit is contained in:
2026-05-10 17:26:05 +02:00
parent e240b17d3d
commit 233aa56d40
9 changed files with 233 additions and 60 deletions
+123 -20
View File
@@ -22,7 +22,8 @@ class Ladestation_OCPP extends IPSModule
private const ATTR_CAPABILITIES = 'Capabilities';
private const ATTR_LAST_EMS_UPDATE = 'LastEmsUpdate';
private const ATTR_LAST_OCPP_HEARTBEAT = 'LastOcppHeartbeat';
private const ATTR_PENDING_COMMAND = 'PendingCommand';
private const ATTR_PENDING_COMMANDS = 'PendingCommands';
private const ATTR_LAST_ENERGY_AGGREGATION = 'LastEnergyAggregation';
public function Create()
{
@@ -48,7 +49,7 @@ class Ladestation_OCPP extends IPSModule
$this->RegisterPropertyInteger('MeterPowerIntervalSeconds', 1);
$this->RegisterPropertyInteger('MeterEnergyIntervalSeconds', 60);
$this->RegisterPropertyInteger('IdleCounterMax', 2);
$this->RegisterPropertyInteger('Interval', 1);
$this->RegisterPropertyInteger('Interval', 2);
$this->RegisterPropertyInteger('PhaseSwitchHoldSeconds', 120);
$this->RegisterPropertyInteger('PhaseSwitchPauseSeconds', 30);
$this->RegisterPropertyBoolean('AllowAutomaticPhaseSwitch', false);
@@ -65,7 +66,8 @@ class Ladestation_OCPP extends IPSModule
$this->RegisterAttributeString(self::ATTR_CAPABILITIES, json_encode(CapabilityModel::defaults()));
$this->RegisterAttributeInteger(self::ATTR_LAST_EMS_UPDATE, 0);
$this->RegisterAttributeInteger(self::ATTR_LAST_OCPP_HEARTBEAT, 0);
$this->RegisterAttributeString(self::ATTR_PENDING_COMMAND, json_encode([]));
$this->RegisterAttributeString(self::ATTR_PENDING_COMMANDS, json_encode([]));
$this->RegisterAttributeInteger(self::ATTR_LAST_ENERGY_AGGREGATION, 0);
$this->registerEmsVariables();
$this->registerControlVariables();
@@ -73,7 +75,7 @@ class Ladestation_OCPP extends IPSModule
$this->registerMeterVariables();
$this->registerDiagnosticVariables();
$this->RegisterTimer('Timer_Do_UserCalc', 1000, 'IPS_RequestAction(' . $this->InstanceID . ', "Do_UserCalc", "");');
$this->RegisterTimer('Timer_Do_UserCalc', 2000, 'IPS_RequestAction(' . $this->InstanceID . ', "Do_UserCalc", "");');
$this->RegisterTimer('Timer_StatusWatchdog', 5000, 'IPS_RequestAction(' . $this->InstanceID . ', "StatusWatchdog", "");');
$this->RegisterTimer('Timer_EMSWatchdog', 5000, 'IPS_RequestAction(' . $this->InstanceID . ', "EMSWatchdog", "");');
$this->RegisterTimer('Timer_OCPPWatchdog', 5000, 'IPS_RequestAction(' . $this->InstanceID . ', "OCPPWatchdog", "");');
@@ -86,7 +88,7 @@ class Ladestation_OCPP extends IPSModule
{
parent::ApplyChanges();
$interval = max(1, $this->ReadPropertyInteger('Interval'));
$interval = max(2, $this->ReadPropertyInteger('Interval'));
$this->SetTimerInterval('Timer_Do_UserCalc', $interval * 1000);
$this->SetTimerInterval('Timer_StatusWatchdog', 5000);
$this->SetTimerInterval('Timer_EMSWatchdog', 5000);
@@ -621,6 +623,7 @@ class Ladestation_OCPP extends IPSModule
private function queueOcppCommandInternal(string $action, array $payload, int $retryCount, bool $force): void
{
$this->processPendingCommands();
$version = $this->effectiveOcppVersion();
if ($action === 'SetChargingProfile' && !$force && !$this->canUseSmartCharging()) {
$this->SetValue('LetzterCommandStatus', 'SetChargingProfile blockiert: Capability nicht bestaetigt.');
@@ -630,13 +633,17 @@ class Ladestation_OCPP extends IPSModule
$message = OCPPMessage::call($action, $payload, $version, $this->ReadPropertyString('ChargePointId'));
$this->SetValue('LetzterCommandTimestamp', time());
$this->SetValue('LetzterCommandStatus', 'Queued OCPP: ' . $action);
$this->WriteAttributeString(self::ATTR_PENDING_COMMAND, json_encode([
$pendingCommands = $this->readPendingCommands();
$pendingCommands[$message->uniqueId] = [
'uniqueId' => $message->uniqueId,
'action' => $action,
'payload' => $payload,
'retryCount' => $retryCount,
'timestamp' => time()
]));
'timestamp' => time(),
'ackDeadline' => time() + max(5, $this->ReadPropertyInteger('CommandAckTimeoutSeconds')),
'status' => 'pending'
];
$this->writePendingCommands($pendingCommands);
$this->queueOcppMessage($message);
}
@@ -654,6 +661,8 @@ class Ladestation_OCPP extends IPSModule
private function runWatchdogs(): void
{
$this->processPendingCommands();
$manager = new FailSafeManager();
$result = $manager->evaluate([
'lastEmsUpdate' => $this->ReadAttributeInteger(self::ATTR_LAST_EMS_UPDATE),
@@ -815,9 +824,21 @@ class Ladestation_OCPP extends IPSModule
if ($this->GetValue('LetzterMeterValueZeitpunkt') > 0) {
return;
}
$now = time();
$lastAggregation = $this->ReadAttributeInteger(self::ATTR_LAST_ENERGY_AGGREGATION);
if ($lastAggregation <= 0) {
$this->WriteAttributeInteger(self::ATTR_LAST_ENERGY_AGGREGATION, $now);
return;
}
$seconds = max(0, $now - $lastAggregation);
if ($seconds <= 0) {
return;
}
$energy = (float)$this->GetValue('Bezogene_Energie');
$energy += (float)$this->GetValue('Ladeleistung_Effektiv') * (max(1, $this->ReadPropertyInteger('MeterEnergyIntervalSeconds')) / 3600);
$energy += ((float)$this->GetValue('Ladeleistung_Effektiv') * $seconds) / 3600.0;
$this->SetValue('Bezogene_Energie', $energy);
$this->WriteAttributeInteger(self::ATTR_LAST_ENERGY_AGGREGATION, $now);
}
private function extractIdToken(array $payload): string
@@ -970,16 +991,19 @@ class Ladestation_OCPP extends IPSModule
private function handleCallResult(OCPPMessage $message): void
{
$pending = $this->readPendingCommand();
if (($pending['uniqueId'] ?? '') !== $message->uniqueId) {
$pendingCommands = $this->readPendingCommands();
if (!isset($pendingCommands[$message->uniqueId])) {
$this->SetValue('LetzterCommandStatus', 'CallResult ohne passenden Pending Command: ' . $message->uniqueId);
return;
}
$pending = $pendingCommands[$message->uniqueId];
unset($pendingCommands[$message->uniqueId]);
$this->writePendingCommands($pendingCommands);
$action = (string)($pending['action'] ?? '');
$status = (string)($message->payload['status'] ?? 'Accepted');
$this->SetValue('LetzterCommandStatus', $action . ' bestaetigt: ' . $status);
$this->WriteAttributeString(self::ATTR_PENDING_COMMAND, json_encode([]));
if ($action === 'SetChargingProfile') {
if ($status === 'Accepted') {
@@ -997,7 +1021,13 @@ class Ladestation_OCPP extends IPSModule
private function handleCallError(OCPPMessage $message): void
{
$pending = $this->readPendingCommand();
$pendingCommands = $this->readPendingCommands();
$pending = $pendingCommands[$message->uniqueId] ?? [];
if (isset($pendingCommands[$message->uniqueId])) {
unset($pendingCommands[$message->uniqueId]);
$this->writePendingCommands($pendingCommands);
}
$action = (string)($pending['action'] ?? 'Unknown');
$code = (string)($message->payload['errorCode'] ?? $message->action);
$description = (string)($message->payload['errorDescription'] ?? '');
@@ -1010,7 +1040,7 @@ class Ladestation_OCPP extends IPSModule
return;
}
$this->WriteAttributeString(self::ATTR_PENDING_COMMAND, json_encode([]));
$this->writePendingCommands($pendingCommands);
}
private function retryOrBlockSmartCharging(array $pending, string $reason): void
@@ -1019,9 +1049,22 @@ class Ladestation_OCPP extends IPSModule
$limit = max(0, $this->ReadPropertyInteger('SmartChargingRetryLimit'));
if ($retryCount < $limit) {
$next = $retryCount + 1;
$delay = min(300, (int)pow(2, $next) * 5);
$retryKey = 'retry-' . str_replace('.', '', uniqid('', true));
$pendingCommands = $this->readPendingCommands();
$pendingCommands[$retryKey] = [
'uniqueId' => $retryKey,
'action' => 'SetChargingProfile',
'payload' => (array)($pending['payload'] ?? []),
'retryCount' => $next,
'timestamp' => time(),
'nextRetryAt' => time() + $delay,
'status' => 'retry_wait',
'reason' => $reason
];
$this->writePendingCommands($pendingCommands);
$this->SetValue('WattpilotProfileRetries', $next);
$this->SetValue('LetzterCommandStatus', 'SetChargingProfile Retry ' . $next . '/' . $limit . ': ' . $reason);
$this->queueOcppCommandInternal('SetChargingProfile', (array)($pending['payload'] ?? []), $next, true);
$this->SetValue('LetzterCommandStatus', 'SetChargingProfile Retry ' . $next . '/' . $limit . ' in ' . $delay . 's: ' . $reason);
return;
}
@@ -1030,7 +1073,6 @@ class Ladestation_OCPP extends IPSModule
$this->writeCapabilities($capabilities);
$this->SetValue('LetztesChargingProfileAccepted', false);
$this->SetValue('WattpilotSmartChargingBlocked', true);
$this->WriteAttributeString(self::ATTR_PENDING_COMMAND, json_encode([]));
$this->setDiagnostic('Warnung', 'Wattpilot Gen1', 'SmartCharging nach ' . $limit . ' Retries deaktiviert: ' . $reason);
}
@@ -1082,10 +1124,71 @@ class Ladestation_OCPP extends IPSModule
$this->SetValue('Capability_SimpleAmpereProfiles', (bool)($capabilities['simpleAmpereProfiles'] ?? false));
}
private function readPendingCommand(): array
private function readPendingCommands(): array
{
$pending = json_decode($this->ReadAttributeString(self::ATTR_PENDING_COMMAND), true);
return is_array($pending) ? $pending : [];
$pending = json_decode($this->ReadAttributeString(self::ATTR_PENDING_COMMANDS), true);
if (!is_array($pending)) {
return [];
}
if (isset($pending['uniqueId'])) {
return [(string)$pending['uniqueId'] => $pending];
}
return $pending;
}
private function writePendingCommands(array $pendingCommands): void
{
$this->WriteAttributeString(self::ATTR_PENDING_COMMANDS, json_encode($pendingCommands));
}
private function processPendingCommands(): void
{
$pendingCommands = $this->readPendingCommands();
if (empty($pendingCommands)) {
return;
}
$now = time();
$changed = false;
foreach ($pendingCommands as $uniqueId => $pending) {
$status = (string)($pending['status'] ?? 'pending');
if ($status === 'retry_wait') {
if ($now >= (int)($pending['nextRetryAt'] ?? 0)) {
unset($pendingCommands[$uniqueId]);
$this->writePendingCommands($pendingCommands);
$this->queueOcppCommandInternal(
(string)($pending['action'] ?? 'SetChargingProfile'),
(array)($pending['payload'] ?? []),
(int)($pending['retryCount'] ?? 0),
true
);
$pendingCommands = $this->readPendingCommands();
$changed = true;
}
continue;
}
if ($status === 'pending' && $now > (int)($pending['ackDeadline'] ?? 0)) {
unset($pendingCommands[$uniqueId]);
$changed = true;
if ((string)($pending['action'] ?? '') === 'SetChargingProfile') {
$this->writePendingCommands($pendingCommands);
$this->retryOrBlockSmartCharging($pending, 'ACK timeout');
$pendingCommands = $this->readPendingCommands();
} else {
$this->SetValue('LetzterCommandStatus', (string)($pending['action'] ?? 'Command') . ' ACK timeout');
}
}
if ($now - (int)($pending['timestamp'] ?? $now) > 3600) {
unset($pendingCommands[$uniqueId]);
$changed = true;
}
}
if ($changed) {
$this->writePendingCommands($pendingCommands);
}
}
private function updateMeterCapabilities(array $values): void