integration Fronius ladestation mit ocpp.

This commit is contained in:
2026-05-10 12:38:49 +02:00
parent bba6494c59
commit e240b17d3d
11 changed files with 861 additions and 21 deletions
+412 -10
View File
@@ -14,6 +14,7 @@ require_once __DIR__ . '/libs/PhaseManager.php';
require_once __DIR__ . '/libs/FailSafeManager.php';
require_once __DIR__ . '/libs/DataTransferRegistry.php';
require_once __DIR__ . '/libs/Diagnostics.php';
require_once __DIR__ . '/libs/WattpilotGen1Profile.php';
class Ladestation_OCPP extends IPSModule
{
@@ -21,23 +22,29 @@ 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';
public function Create()
{
parent::Create();
$this->RegisterPropertyString('OCPPVersionMode', 'auto');
$this->RegisterPropertyString('DeviceProfile', 'generic_ocpp');
$this->RegisterPropertyString('ChargePointId', '');
$this->RegisterPropertyInteger('EVSEId', 1);
$this->RegisterPropertyInteger('ConnectorId', 1);
$this->RegisterPropertyInteger('OCPPServerInstance', 0);
$this->RegisterPropertyString('AuthorizeIdTag', 'Symcon');
$this->RegisterPropertyFloat('MaxCurrentAbs', 32.0);
$this->RegisterPropertyFloat('MinCurrent', 6.0);
$this->RegisterPropertyFloat('SafeCurrent', 6.0);
$this->RegisterPropertyString('SafeOffStrategy', 'SafeCurrent');
$this->RegisterPropertyInteger('EMSWatchdogSeconds', 120);
$this->RegisterPropertyInteger('OCPPHeartbeatIntervalSeconds', 30);
$this->RegisterPropertyInteger('OCPPHeartbeatTimeoutSeconds', 90);
$this->RegisterPropertyInteger('MeterValuesTimeoutSeconds', 180);
$this->RegisterPropertyInteger('CommandAckTimeoutSeconds', 30);
$this->RegisterPropertyInteger('SmartChargingRetryLimit', 3);
$this->RegisterPropertyInteger('MeterPowerIntervalSeconds', 1);
$this->RegisterPropertyInteger('MeterEnergyIntervalSeconds', 60);
$this->RegisterPropertyInteger('IdleCounterMax', 2);
@@ -58,6 +65,7 @@ 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->registerEmsVariables();
$this->registerControlVariables();
@@ -98,6 +106,7 @@ class Ladestation_OCPP extends IPSModule
$this->EnableAction('VNB_Limit_W');
$this->EnableAction('Sperre_Prio');
$this->EnableAction('PV_Prio');
$this->applyDeviceProfileCapabilities();
if ($this->GetValue('Max_Current_abs') <= 0) {
$this->SetValue('Max_Current_abs', $this->ReadPropertyFloat('MaxCurrentAbs'));
@@ -113,10 +122,15 @@ class Ladestation_OCPP extends IPSModule
$this->SetValue('ChargePointId', $chargePointId);
$this->SetValue('EVSEId', $this->ReadPropertyInteger('EVSEId'));
$this->SetValue('ConnectorId', $this->ReadPropertyInteger('ConnectorId'));
$server = $this->ReadPropertyInteger('OCPPServerInstance');
if ($server > 0) {
$this->RegisterReference($server);
}
if ($chargePointId === '') {
$this->SetStatus(201);
$this->setDiagnostic('Warnung', 'Konfiguration', 'ChargePointId ist noch nicht gesetzt.');
$this->SetTimerInterval('Timer_Do_UserCalc', 0);
$this->SetSummary('OCPP Scaffold: ChargePointId fehlt');
return;
}
@@ -174,6 +188,29 @@ class Ladestation_OCPP extends IPSModule
$this->queueOcppCommand('ClearChargingProfile', $this->buildClearProfilePayload());
break;
case 'RemoteStartTransaction':
$this->queueOcppCommand('RemoteStartTransaction', $this->buildRemoteStartPayload());
break;
case 'RemoteStopTransaction':
$payload = $this->buildRemoteStopPayload();
if (!empty($payload)) {
$this->queueOcppCommand('RemoteStopTransaction', $payload);
}
break;
case 'ChangeAvailabilityOperative':
$this->queueOcppCommand('ChangeAvailability', $this->buildAvailabilityPayload('Operative'));
break;
case 'ChangeAvailabilityInoperative':
$this->queueOcppCommand('ChangeAvailability', $this->buildAvailabilityPayload('Inoperative'));
break;
case 'ProbeSmartCharging':
$this->probeSmartCharging();
break;
case 'HandleInboundFrame':
$this->HandleInboundFrame((string)$Value);
break;
@@ -233,7 +270,7 @@ class Ladestation_OCPP extends IPSModule
if ($this->shouldSendChargingProfile($oldPower, $targetPower)) {
$setpoint = $this->buildEffectiveSetpoint($targetPower);
$profile = (new ChargingProfileBuilder())->build($setpoint, $this->effectiveOcppVersion());
$profile = $this->buildChargingProfilePayload($setpoint);
$this->queueOcppCommand('SetChargingProfile', $profile);
}
@@ -252,34 +289,55 @@ class Ladestation_OCPP extends IPSModule
$this->SetValue('LetzteMeldungZeit', time());
$this->SetValue('LetzteMeldung', $message->action);
$this->SetValue('OCPP_Online', true);
$this->SetValue('OCPP_Connected', true);
$this->SetValue('OCPP_LastSeen', time());
$this->SetValue('OCPP_Error', '');
$this->WriteAttributeInteger(self::ATTR_LAST_OCPP_HEARTBEAT, time());
if ($message->messageTypeId === 3) {
$this->handleCallResult($message);
return;
}
if ($message->messageTypeId === 4) {
$this->handleCallError($message);
return;
}
switch ($message->action) {
case 'BootNotification':
$this->handleBootNotification($message);
$this->respondToCall($message, $this->buildBootResponse());
break;
case 'Heartbeat':
$this->setDiagnostic('Info', 'OCPP', 'Heartbeat empfangen.');
$this->respondToCall($message, ['currentTime' => gmdate('Y-m-d\TH:i:s\Z')]);
break;
case 'StatusNotification':
$this->handleStatusNotification($message->payload);
$this->respondToCall($message, []);
break;
case 'MeterValues':
$this->handleMeterValues($message->payload);
$this->respondToCall($message, []);
break;
case 'StartTransaction':
case 'StopTransaction':
case 'TransactionEvent':
$this->handleTransaction($message);
$this->respondToCall($message, $this->buildTransactionResponse($message));
break;
case 'Authorize':
$this->SetValue('LetztesIdToken', $this->extractIdToken($message->payload));
$this->respondToCall($message, ['idTagInfo' => ['status' => 'Accepted']]);
break;
case 'DataTransfer':
$this->handleDataTransfer($message->payload);
$this->respondToCall($message, $this->buildDataTransferResponse($message->payload));
break;
default:
$this->setDiagnostic('Info', 'OCPP', 'OCPP Aktion empfangen: ' . $message->action);
$this->respondToCall($message, []);
break;
}
}
@@ -312,6 +370,7 @@ class Ladestation_OCPP extends IPSModule
$this->RegisterVariableInteger('VNB_Mode', 'VNB_Mode', '', 39);
$this->RegisterVariableFloat('VNB_Limit_A', 'VNB_Limit_A', '', 40);
$this->RegisterVariableFloat('VNB_Limit_W', 'VNB_Limit_W', '', 41);
$this->RegisterVariableFloat('Ladestrom', 'Ladestrom', '', 42);
}
private function registerStatusVariables(): void
@@ -336,6 +395,10 @@ class Ladestation_OCPP extends IPSModule
$this->RegisterVariableString('AktiverReduktionsgrund', 'AktiverReduktionsgrund', '', 77);
$this->RegisterVariableInteger('LetzterFreigabewechsel', 'LetzterFreigabewechsel', '', 78);
$this->RegisterVariableString('LetztesIdToken', 'LetztesIdToken', '', 79);
$this->RegisterVariableString('DeviceProfile', 'DeviceProfile', '', 80);
$this->RegisterVariableBoolean('OCPP_Connected', 'OCPP_Connected', '~Switch', 81);
$this->RegisterVariableInteger('OCPP_LastSeen', 'OCPP_LastSeen', '', 82);
$this->RegisterVariableString('OCPP_Error', 'OCPP_Error', '', 83);
}
private function registerMeterVariables(): void
@@ -373,6 +436,14 @@ class Ladestation_OCPP extends IPSModule
$this->RegisterVariableInteger('LetzterCommandTimestamp', 'LetzterCommandTimestamp', '', 130);
$this->RegisterVariableString('LetzterCommandStatus', 'LetzterCommandStatus', '', 131);
$this->RegisterVariableFloat('SollIstAbweichung', 'SollIstAbweichung', '', 132);
$this->RegisterVariableBoolean('Capability_SmartCharging', 'Capability_SmartCharging', '~Switch', 133);
$this->RegisterVariableBoolean('Capability_MeterValues', 'Capability_MeterValues', '~Switch', 134);
$this->RegisterVariableBoolean('Capability_RemoteStartStop', 'Capability_RemoteStartStop', '~Switch', 135);
$this->RegisterVariableBoolean('Capability_Availability', 'Capability_Availability', '~Switch', 136);
$this->RegisterVariableBoolean('Capability_PhaseValues', 'Capability_PhaseValues', '~Switch', 137);
$this->RegisterVariableBoolean('Capability_SimpleAmpereProfiles', 'Capability_SimpleAmpereProfiles', '~Switch', 138);
$this->RegisterVariableInteger('WattpilotProfileRetries', 'WattpilotProfileRetries', '', 139);
$this->RegisterVariableBoolean('WattpilotSmartChargingBlocked', 'WattpilotSmartChargingBlocked', '~Switch', 140);
}
private function setInitialDefaults(): void
@@ -387,7 +458,11 @@ class Ladestation_OCPP extends IPSModule
'Max_Current_abs' => 32.0,
'SafeCurrent' => 6.0,
'SafeOff' => 0.0,
'DeviceProfile' => 'generic_ocpp',
'OCPP_Version' => 'auto',
'OCPP_Connected' => false,
'OCPP_LastSeen' => 0,
'OCPP_Error' => '',
'ConnectorStatus' => 'Unknown',
'ChargingState' => 'Unknown',
'NumberPhases' => 3,
@@ -400,7 +475,9 @@ class Ladestation_OCPP extends IPSModule
'LetzteMeldung' => 'Initialisiert',
'Fehlerklasse' => '',
'AktuelleStoerung' => '',
'LetzterCommandStatus' => 'Noch kein OCPP-Transport verifiziert'
'LetzterCommandStatus' => 'Noch kein OCPP-Transport verifiziert',
'WattpilotProfileRetries' => 0,
'WattpilotSmartChargingBlocked' => false
];
foreach ($defaults as $ident => $value) {
@@ -476,13 +553,16 @@ class Ladestation_OCPP extends IPSModule
private function buildEffectiveSetpoint(float $powerW): array
{
$phases = (int)$this->GetValue('NumberPhases');
$currentA = PhaseManager::currentFromPower($powerW, $phases, $this->averageVoltage());
$this->SetValue('Ladestrom', $currentA);
return [
'effectivePowerW' => $powerW,
'effectiveCurrentA' => PhaseManager::currentFromPower($powerW, $phases, $this->averageVoltage()),
'effectiveCurrentA' => $currentA,
'numberPhases' => $phases,
'phaseToUse' => (int)$this->GetValue('PhaseToUse'),
'evseId' => $this->ReadPropertyInteger('EVSEId'),
'connectorId' => $this->ReadPropertyInteger('ConnectorId'),
'transactionId' => $this->GetValue('TransactionId'),
'reason' => $this->GetValue('AktiverReduktionsgrund'),
'activeReleaseSource' => $this->GetValue('AktiveFreigabequelle'),
'activeBlockingReason' => $this->GetValue('AktiverSperrgrund'),
@@ -498,6 +578,33 @@ class Ladestation_OCPP extends IPSModule
return ['evseId' => $this->ReadPropertyInteger('EVSEId')];
}
private function buildRemoteStartPayload(): array
{
return [
'connectorId' => $this->ReadPropertyInteger('ConnectorId'),
'idTag' => $this->ReadPropertyString('AuthorizeIdTag')
];
}
private function buildRemoteStopPayload(): array
{
$state = TransactionStore::fromJson($this->ReadAttributeString(self::ATTR_TRANSACTION));
$transactionId = (string)($state['activeTransactionId'] ?? $this->GetValue('TransactionId'));
if ($transactionId === '') {
$this->setDiagnostic('Warnung', 'OCPP', 'RemoteStop ohne aktive Transaction angefordert.');
return [];
}
return ['transactionId' => is_numeric($transactionId) ? (int)$transactionId : $transactionId];
}
private function buildAvailabilityPayload(string $type): array
{
return [
'connectorId' => $this->ReadPropertyInteger('ConnectorId'),
'type' => $type
];
}
private function shouldSendChargingProfile(float $oldPower, float $newPower): bool
{
$delta = abs($oldPower - $newPower);
@@ -509,18 +616,40 @@ class Ladestation_OCPP extends IPSModule
private function queueOcppCommand(string $action, array $payload): void
{
$version = $this->effectiveOcppVersion();
$message = OCPPMessage::call($action, $payload, $version, $this->ReadPropertyString('ChargePointId'));
$server = $this->ReadPropertyInteger('OCPPServerInstance');
$this->SetValue('LetzterCommandTimestamp', time());
$this->SetValue('LetzterCommandStatus', 'Queued scaffold: ' . $action);
$this->queueOcppCommandInternal($action, $payload, 0, false);
}
private function queueOcppCommandInternal(string $action, array $payload, int $retryCount, bool $force): void
{
$version = $this->effectiveOcppVersion();
if ($action === 'SetChargingProfile' && !$force && !$this->canUseSmartCharging()) {
$this->SetValue('LetzterCommandStatus', 'SetChargingProfile blockiert: Capability nicht bestaetigt.');
return;
}
$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([
'uniqueId' => $message->uniqueId,
'action' => $action,
'payload' => $payload,
'retryCount' => $retryCount,
'timestamp' => time()
]));
$this->queueOcppMessage($message);
}
private function queueOcppMessage(OCPPMessage $message): void
{
$server = $this->ReadPropertyInteger('OCPPServerInstance');
if ($server > 0 && IPS_InstanceExists($server)) {
IPS_RequestAction($server, 'QueueOutboundFrame', json_encode(OCPPTransport::buildEnvelope($message, $this->InstanceID)));
return;
}
$this->setDiagnostic('Warnung', 'OCPP Transport', 'Kein OCPP_Server verbunden. Command nur lokal vorgemerkt: ' . $action);
$this->setDiagnostic('Warnung', 'OCPP Transport', 'Kein OCPP_Server verbunden. Frame nur lokal vorgemerkt: ' . $message->action);
}
private function runWatchdogs(): void
@@ -546,6 +675,18 @@ class Ladestation_OCPP extends IPSModule
$this->SetValue('AktiverSperrgrund', $result['blockingReason']);
$this->SetValue('StoerungAktiv', true);
}
$lastMeter = (int)$this->GetValue('LetzterMeterValueZeitpunkt');
if ($lastMeter > 0 && (time() - $lastMeter) > max(30, $this->ReadPropertyInteger('MeterValuesTimeoutSeconds'))) {
$this->SetValue('WarnungAktiv', true);
$this->SetValue('OCPP_Error', 'MeterValues Timeout');
$this->setDiagnostic('Warnung', 'OCPP MeterValues', 'MeterValues sind veraltet.');
}
$lastSeen = (int)$this->GetValue('OCPP_LastSeen');
if ($lastSeen > 0 && (time() - $lastSeen) > max(60, $this->ReadPropertyInteger('OCPPHeartbeatTimeoutSeconds'))) {
$this->SetValue('OCPP_Connected', false);
}
}
private function ProcessIdleCounter(): void
@@ -563,7 +704,12 @@ class Ladestation_OCPP extends IPSModule
{
$version = $this->effectiveOcppVersion();
$this->SetValue('OCPP_Version', $version);
$this->WriteAttributeString(self::ATTR_CAPABILITIES, json_encode(CapabilityModel::detectFromBoot($version, $message->payload)));
$capabilities = CapabilityModel::detectFromBoot($version, $message->payload);
if ($this->isWattpilotProfile() || WattpilotGen1Profile::isWattpilotBoot($message->payload)) {
$this->SetValue('DeviceProfile', WattpilotGen1Profile::DEVICE_PROFILE);
$capabilities = CapabilityModel::merge($capabilities, WattpilotGen1Profile::capabilities());
}
$this->writeCapabilities($capabilities);
$this->setDiagnostic('Info', 'OCPP', 'BootNotification verarbeitet.');
}
@@ -574,6 +720,21 @@ class Ladestation_OCPP extends IPSModule
$this->SetValue('ChargingState', (string)($payload['chargingState'] ?? $status));
$this->SetValue('Car_detected', in_array($status, ['Preparing', 'Charging', 'SuspendedEV', 'SuspendedEVSE', 'Finishing', 'Occupied'], true));
$this->SetValue('Car_is_full', in_array($status, ['Finishing', 'SuspendedEV'], true));
$statusMap = [
'Available' => 1,
'Preparing' => 2,
'Charging' => 3,
'SuspendedEV' => 4,
'SuspendedEVSE' => 5,
'Finishing' => 6,
'Reserved' => 7,
'Unavailable' => 0,
'Faulted' => -1
];
$this->SetValue('Fahrzeugstatus', $statusMap[$status] ?? 0);
if ($status === 'Faulted') {
$this->setDiagnostic('Stoerung', 'OCPP Status', 'Ladestation meldet Faulted.', (string)($payload['errorCode'] ?? ''));
}
}
private function handleMeterValues(array $payload): void
@@ -610,6 +771,8 @@ class Ladestation_OCPP extends IPSModule
}
$this->SetValue('MesswertQualitaet', $values['quality']);
$this->SetValue('LetzterMeterValueZeitpunkt', (int)$values['timestamp']);
$this->updateMeterCapabilities($values);
$this->updatePhaseStateFromMeters($values);
}
private function handleTransaction(OCPPMessage $message): void
@@ -628,6 +791,8 @@ class Ladestation_OCPP extends IPSModule
}
if ($message->action === 'StopTransaction') {
$state['sessionStopReason'] = (string)($payload['reason'] ?? 'Unknown');
$state['activeTransactionId'] = '';
$this->SetValue('TransactionId', '');
}
$this->WriteAttributeString(self::ATTR_TRANSACTION, TransactionStore::toJson($state));
$this->SetValue('LetztesIdToken', $state['lastIdToken']);
@@ -713,6 +878,9 @@ class Ladestation_OCPP extends IPSModule
{
$this->SetValue('LetzteMeldung', $text);
$this->SetValue('LetzteMeldungZeit', time());
if ($severity === 'Fehler' || $severity === 'Stoerung') {
$this->SetValue('OCPP_Error', $text);
}
$this->SetValue('Kommunikationsstatus', Diagnostics::statusFromFlags((bool)$this->GetValue('OCPP_Online'), (bool)$this->GetValue('StoerungAktiv')));
if ($severity === 'Warnung') {
$this->SetValue('WarnungAktiv', true);
@@ -730,6 +898,240 @@ class Ladestation_OCPP extends IPSModule
$this->SendDebug($source, $severity . ': ' . $text, 0);
}
}
private function buildChargingProfilePayload(array $setpoint): array
{
if ($this->isWattpilotProfile()) {
return WattpilotGen1Profile::buildChargingProfile($setpoint, (int)(time() % 100000));
}
return (new ChargingProfileBuilder())->build($setpoint, $this->effectiveOcppVersion());
}
private function canUseSmartCharging(): bool
{
$capabilities = $this->readCapabilities();
return (bool)($capabilities['smartCharging'] ?? false) && !$this->GetValue('WattpilotSmartChargingBlocked');
}
private function probeSmartCharging(): void
{
$power = PhaseManager::wattsFromCurrent((float)$this->GetValue('SafeCurrent'), (int)$this->GetValue('NumberPhases'), $this->averageVoltage());
$setpoint = $this->buildEffectiveSetpoint($power);
$this->SetValue('WattpilotSmartChargingBlocked', false);
$this->queueOcppCommandInternal('SetChargingProfile', $this->buildChargingProfilePayload($setpoint), 0, true);
}
private function respondToCall(OCPPMessage $request, array $payload): void
{
if ($request->messageTypeId !== 2) {
return;
}
$this->queueOcppMessage(OCPPMessage::callResult($request->uniqueId, $payload, $request->version, $request->chargePointId));
}
private function buildBootResponse(): array
{
return [
'status' => 'Accepted',
'currentTime' => gmdate('Y-m-d\TH:i:s\Z'),
'interval' => max(10, $this->ReadPropertyInteger('OCPPHeartbeatIntervalSeconds'))
];
}
private function buildTransactionResponse(OCPPMessage $message): array
{
if ($message->action === 'StartTransaction') {
$state = TransactionStore::fromJson($this->ReadAttributeString(self::ATTR_TRANSACTION));
$transactionId = $state['activeTransactionId'] !== '' ? $state['activeTransactionId'] : (string)time();
$state['activeTransactionId'] = $transactionId;
$state['lastTransactionState'] = 'StartTransaction';
$this->WriteAttributeString(self::ATTR_TRANSACTION, TransactionStore::toJson($state));
$this->SetValue('TransactionId', $transactionId);
return [
'transactionId' => is_numeric($transactionId) ? (int)$transactionId : $transactionId,
'idTagInfo' => ['status' => 'Accepted']
];
}
if ($message->action === 'StopTransaction') {
return ['idTagInfo' => ['status' => 'Accepted']];
}
return [];
}
private function buildDataTransferResponse(array $payload): array
{
$registry = new DataTransferRegistry($this->ReadPropertyBoolean('AllowDataTransfer'));
$vendorId = (string)($payload['vendorId'] ?? '');
$messageId = (string)($payload['messageId'] ?? '');
return [
'status' => $registry->isAllowed($vendorId, $messageId) ? 'Accepted' : 'Rejected'
];
}
private function handleCallResult(OCPPMessage $message): void
{
$pending = $this->readPendingCommand();
if (($pending['uniqueId'] ?? '') !== $message->uniqueId) {
$this->SetValue('LetzterCommandStatus', 'CallResult ohne passenden Pending Command: ' . $message->uniqueId);
return;
}
$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') {
$capabilities = $this->readCapabilities();
$capabilities['smartCharging'] = true;
$this->writeCapabilities($capabilities);
$this->SetValue('LetztesChargingProfileAccepted', true);
$this->SetValue('WattpilotProfileRetries', 0);
$this->SetValue('WattpilotSmartChargingBlocked', false);
return;
}
$this->retryOrBlockSmartCharging($pending, 'CallResult status ' . $status);
}
}
private function handleCallError(OCPPMessage $message): void
{
$pending = $this->readPendingCommand();
$action = (string)($pending['action'] ?? 'Unknown');
$code = (string)($message->payload['errorCode'] ?? $message->action);
$description = (string)($message->payload['errorDescription'] ?? '');
$this->SetValue('LetzterOCPPFehlercode', $code);
$this->SetValue('LetzterCommandStatus', $action . ' abgelehnt: ' . $code);
$this->setDiagnostic('Warnung', 'OCPP Command', $action . ' abgelehnt: ' . $description, $code);
if ($action === 'SetChargingProfile') {
$this->retryOrBlockSmartCharging($pending, $code);
return;
}
$this->WriteAttributeString(self::ATTR_PENDING_COMMAND, json_encode([]));
}
private function retryOrBlockSmartCharging(array $pending, string $reason): void
{
$retryCount = (int)($pending['retryCount'] ?? 0);
$limit = max(0, $this->ReadPropertyInteger('SmartChargingRetryLimit'));
if ($retryCount < $limit) {
$next = $retryCount + 1;
$this->SetValue('WattpilotProfileRetries', $next);
$this->SetValue('LetzterCommandStatus', 'SetChargingProfile Retry ' . $next . '/' . $limit . ': ' . $reason);
$this->queueOcppCommandInternal('SetChargingProfile', (array)($pending['payload'] ?? []), $next, true);
return;
}
$capabilities = $this->readCapabilities();
$capabilities['smartCharging'] = false;
$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);
}
private function applyDeviceProfileCapabilities(): void
{
$profile = $this->ReadPropertyString('DeviceProfile');
$this->SetValue('DeviceProfile', $profile);
if ($profile !== WattpilotGen1Profile::DEVICE_PROFILE) {
$this->setCapabilityVariables($this->readCapabilities());
return;
}
$capabilities = CapabilityModel::merge($this->readCapabilities(), WattpilotGen1Profile::capabilities());
$this->writeCapabilities($capabilities);
$this->SetValue('OCPP_Version', '1.6');
$this->SetValue('AutomatischePhasenumschaltung', false);
$this->SetValue('Capability_SimpleAmpereProfiles', true);
}
private function isWattpilotProfile(): bool
{
return $this->ReadPropertyString('DeviceProfile') === WattpilotGen1Profile::DEVICE_PROFILE
|| $this->GetValue('DeviceProfile') === WattpilotGen1Profile::DEVICE_PROFILE;
}
private function readCapabilities(): array
{
$data = json_decode($this->ReadAttributeString(self::ATTR_CAPABILITIES), true);
if (!is_array($data)) {
return CapabilityModel::defaults();
}
return CapabilityModel::merge(CapabilityModel::defaults(), $data);
}
private function writeCapabilities(array $capabilities): void
{
$capabilities = CapabilityModel::merge(CapabilityModel::defaults(), $capabilities);
$this->WriteAttributeString(self::ATTR_CAPABILITIES, json_encode($capabilities));
$this->setCapabilityVariables($capabilities);
}
private function setCapabilityVariables(array $capabilities): void
{
$this->SetValue('Capability_SmartCharging', (bool)($capabilities['smartCharging'] ?? false));
$this->SetValue('Capability_MeterValues', (bool)($capabilities['meterValues'] ?? false));
$this->SetValue('Capability_RemoteStartStop', (bool)($capabilities['remoteStartStop'] ?? false));
$this->SetValue('Capability_Availability', (bool)($capabilities['availability'] ?? false));
$this->SetValue('Capability_PhaseValues', (bool)($capabilities['phaseMetering'] ?? false));
$this->SetValue('Capability_SimpleAmpereProfiles', (bool)($capabilities['simpleAmpereProfiles'] ?? false));
}
private function readPendingCommand(): array
{
$pending = json_decode($this->ReadAttributeString(self::ATTR_PENDING_COMMAND), true);
return is_array($pending) ? $pending : [];
}
private function updateMeterCapabilities(array $values): void
{
$capabilities = $this->readCapabilities();
$capabilities['meterValues'] = true;
$capabilities['powerImport'] = $capabilities['powerImport'] || $values['powerImportW'] !== null;
$capabilities['powerExport'] = $capabilities['powerExport'] || $values['powerExportW'] !== null;
$capabilities['energyImport'] = $capabilities['energyImport'] || $values['energyImportWh'] !== null;
$capabilities['energyExport'] = $capabilities['energyExport'] || $values['energyExportWh'] !== null;
$capabilities['soc'] = $capabilities['soc'] || $values['soc'] !== null;
$capabilities['temperature'] = $capabilities['temperature'] || $values['temperature'] !== null;
foreach ([1, 2, 3] as $phase) {
if ($values['currentA'][$phase] !== null) {
$capabilities['currentImportPerPhase'] = true;
$capabilities['phaseMetering'] = true;
}
if ($values['voltageV'][$phase] !== null) {
$capabilities['voltagePerPhase'] = true;
$capabilities['phaseMetering'] = true;
}
}
$this->writeCapabilities($capabilities);
}
private function updatePhaseStateFromMeters(array $values): void
{
$activePhases = 0;
foreach ([1, 2, 3] as $phase) {
if ($values['currentA'][$phase] !== null && (float)$values['currentA'][$phase] > 0.5) {
$activePhases++;
}
}
if ($activePhases === 1) {
$this->SetValue('Is_1_ph', true);
$this->SetValue('NumberPhases', 1);
return;
}
if ($activePhases >= 2) {
$this->SetValue('Is_1_ph', false);
$this->SetValue('NumberPhases', 3);
}
}
}
?>