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
+17
View File
@@ -10,6 +10,7 @@ Status dieser Version: **M1 Scaffold**. Das Modul ist installierbar, haelt den b
- Keine Hersteller-HTTP-APIs, keine Cloud-Token, keine direkte Kopie von `Ladestation_v2`.
- Fachliche Paritaet zu `Ladestation_v2` als Zielbild.
- Stabile Anbindung an den bestehenden `Manager` ueber `PowerSteps`, `GetCurrentData`, `SetAktuelle_Leistung` und `Do_UserCalc`.
- Geraeteprofil `fronius_wattpilot_gen1` fuer erste Fronius-Wattpilot-Gen1-Tests ueber OCPP 1.6J.
## Manager-Anbindung
@@ -51,6 +52,22 @@ Die OCPP-Klassen liegen in `Ladestation_OCPP/libs/`:
- Versionsadapter: `OCPP16Adapter`, `OCPP201Adapter`, `OCPP21Adapter`
- Fachlogik: `PowerStepCalculator`, `ChargingProfileBuilder`, `MeterValueNormalizer`, `TransactionStore`
- Sicherheit/Diagnose: `FailSafeManager`, `Diagnostics`, `CapabilityModel`, `DataTransferRegistry`, `PhaseManager`
- Geraeteprofil: `WattpilotGen1Profile`
## Fronius Wattpilot Gen1
Das Wattpilot-Profil bleibt strikt OCPP-only:
- keine lokale go-e HTTP API
- keine Fronius Solar API
- keine Fronius Cloud
- einfache `TxProfile`-Limits in Ampere bei aktiver Transaktion, sonst `TxDefaultProfile`
- genau eine Schedule-Periode
- keine Phasen-Erzwingung per OCPP
- `RemoteStartTransaction`, `RemoteStopTransaction` und `ChangeAvailability`
- `SetChargingProfile` mit begrenzten Retries und Capability-Abschaltung bei Ablehnung
Details stehen in `docs/OCPP/WATTPILOT_GEN1.md`.
## Transport
+56
View File
@@ -15,11 +15,25 @@
{ "caption": "OCPP 2.1 vorbereitet", "value": "2.1" }
]
},
{
"type": "Select",
"name": "DeviceProfile",
"caption": "Geraeteprofil",
"options": [
{ "caption": "Generisches OCPP", "value": "generic_ocpp" },
{ "caption": "Fronius Wattpilot Gen1 OCPP 1.6J", "value": "fronius_wattpilot_gen1" }
]
},
{
"type": "ValidationTextBox",
"name": "ChargePointId",
"caption": "ChargePointId"
},
{
"type": "ValidationTextBox",
"name": "AuthorizeIdTag",
"caption": "Default ID-Tag"
},
{
"type": "NumberSpinner",
"name": "EVSEId",
@@ -92,18 +106,35 @@
"caption": "EMS Watchdog",
"suffix": "Sekunden"
},
{
"type": "NumberSpinner",
"name": "OCPPHeartbeatIntervalSeconds",
"caption": "OCPP Heartbeat Intervall",
"suffix": "Sekunden"
},
{
"type": "NumberSpinner",
"name": "OCPPHeartbeatTimeoutSeconds",
"caption": "OCPP Heartbeat Timeout",
"suffix": "Sekunden"
},
{
"type": "NumberSpinner",
"name": "MeterValuesTimeoutSeconds",
"caption": "MeterValues Timeout",
"suffix": "Sekunden"
},
{
"type": "NumberSpinner",
"name": "CommandAckTimeoutSeconds",
"caption": "Command ACK Timeout",
"suffix": "Sekunden"
},
{
"type": "NumberSpinner",
"name": "SmartChargingRetryLimit",
"caption": "SmartCharging Retries"
},
{
"type": "NumberSpinner",
"name": "DebugLevel",
@@ -120,6 +151,31 @@
"type": "Button",
"caption": "Clear ChargingProfile",
"onClick": "IPS_RequestAction($id, \"ClearChargingProfile\", \"\");"
},
{
"type": "Button",
"caption": "SmartCharging testen",
"onClick": "IPS_RequestAction($id, \"ProbeSmartCharging\", \"\");"
},
{
"type": "Button",
"caption": "Remote Start",
"onClick": "IPS_RequestAction($id, \"RemoteStartTransaction\", \"\");"
},
{
"type": "Button",
"caption": "Remote Stop",
"onClick": "IPS_RequestAction($id, \"RemoteStopTransaction\", \"\");"
},
{
"type": "Button",
"caption": "Availability Operative",
"onClick": "IPS_RequestAction($id, \"ChangeAvailabilityOperative\", \"\");"
},
{
"type": "Button",
"caption": "Availability Inoperative",
"onClick": "IPS_RequestAction($id, \"ChangeAvailabilityInoperative\", \"\");"
}
]
}
@@ -18,6 +18,9 @@ class CapabilityModel
'phaseToUse' => false,
'phaseSwitching' => false,
'dataTransfer' => false,
'remoteStartStop' => false,
'availability' => false,
'simpleAmpereProfiles' => false,
'getVariables' => false,
'setVariables' => false,
'transactionEvent' => false,
@@ -104,7 +104,11 @@ class MeterValueNormalizer
private function normalizeValue(array $sample): float
{
$value = (float)($sample['value'] ?? 0);
$rawValue = $sample['value'] ?? 0;
if (is_array($rawValue)) {
$rawValue = $rawValue['value'] ?? 0;
}
$value = (float)$rawValue;
$unit = $sample['unit'] ?? ($sample['unitOfMeasure']['unit'] ?? '');
$unit = strtolower((string)$unit);
@@ -0,0 +1,89 @@
<?php
class WattpilotGen1Profile
{
public const DEVICE_PROFILE = 'fronius_wattpilot_gen1';
public static function capabilities(): array
{
return [
'smartCharging' => true,
'meterValues' => true,
'phaseMetering' => false,
'currentImportPerPhase' => false,
'voltagePerPhase' => false,
'powerImport' => true,
'powerExport' => false,
'energyImport' => true,
'energyExport' => false,
'numberPhases' => false,
'phaseToUse' => false,
'phaseSwitching' => false,
'dataTransfer' => false,
'remoteStartStop' => true,
'availability' => true,
'simpleAmpereProfiles' => true,
'getVariables' => false,
'setVariables' => false,
'transactionEvent' => false,
'soc' => false,
'temperature' => false,
'signedMeterValue' => false,
'bidirectional' => false
];
}
public static function isWattpilotBoot(array $payload): bool
{
$vendor = strtolower((string)($payload['chargePointVendor'] ?? ''));
$model = strtolower((string)($payload['chargePointModel'] ?? ''));
return strpos($vendor, 'fronius') !== false || strpos($model, 'wattpilot') !== false;
}
public static function buildChargingProfile(array $setpoint, int $profileId): array
{
$currentA = max(0.0, (float)($setpoint['effectiveCurrentA'] ?? 0.0));
$validTo = gmdate('Y-m-d\TH:i:s\Z', time() + 120);
$transactionId = (string)($setpoint['transactionId'] ?? '');
$purpose = $transactionId !== '' ? 'TxProfile' : 'TxDefaultProfile';
$profile = [
'connectorId' => (int)($setpoint['connectorId'] ?? 1),
'csChargingProfiles' => [
'chargingProfileId' => $profileId,
'stackLevel' => 0,
'chargingProfilePurpose' => $purpose,
'chargingProfileKind' => 'Absolute',
'validTo' => $validTo,
'chargingSchedule' => [
'duration' => 120,
'chargingRateUnit' => 'A',
'chargingSchedulePeriod' => [
[
'startPeriod' => 0,
'limit' => round($currentA, 1)
]
]
]
]
];
if ($transactionId !== '') {
$profile['csChargingProfiles']['transactionId'] = is_numeric($transactionId) ? (int)$transactionId : $transactionId;
}
return $profile;
}
public static function supportedMeasurands(): array
{
return [
'Power.Active.Import',
'Energy.Active.Import.Register',
'Voltage',
'Current.Import'
];
}
}
?>
+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);
}
}
}
?>
+4 -1
View File
@@ -7,11 +7,14 @@
- Vorbereitung der CSMS-Transportrolle innerhalb von IP-Symcon.
- Routing von eingehenden OCPP-Frames nach `ChargePointId`, `EVSEId` und `ConnectorId`.
- Entgegennahme von ausgehenden Frames aus `Ladestation_OCPP`.
- Pufferung ausgehender Frames je `ChargePointId` fuer den WebHook/WebSocket-Spike.
- Dokumentation des WebHook/WebSocket-Spikes.
## Status
Dieses Modul ist bewusst ein Scaffold. Symcon bietet einen WebSocket Client sowie WebHook Control mit WebSocket-Support und `ProcessHookData()` fuer PHP-Module. Ob diese Mechanik fuer dauerhafte OCPP-CSMS-Verbindungen mit realen Ladestationen robust genug ist, muss mit einer OCPP-Referenzstation oder einem Simulator getestet werden.
Dieses Modul ist bewusst ein Scaffold. Symcon bietet einen WebSocket Client sowie WebHook Control mit WebSocket-Support und `ProcessHookData()` fuer PHP-Module. Ob diese Mechanik fuer dauerhafte OCPP-CSMS-Verbindungen mit realen Ladestationen robust genug ist, muss mit einer OCPP-Referenzstation, einem Simulator oder dem Fronius Wattpilot getestet werden.
Eingehende Calls werden an `Ladestation_OCPP` geroutet. Die dort erzeugten OCPP-CallResults werden in `OCPP_Server` gepuffert und fuer denselben `ChargePointId` wieder ausgegeben, sofern der Symcon-Hook diesen Rueckkanal bereitstellt.
## Konfiguration
+5
View File
@@ -79,6 +79,11 @@
"type": "Button",
"caption": "Puffer loeschen",
"onClick": "IPS_RequestAction($id, \"ClearBuffers\", \"\");"
},
{
"type": "Button",
"caption": "Naechsten Outbound Frame holen",
"onClick": "IPS_RequestAction($id, \"DequeueOutboundFrame\", \"\");"
}
]
}
+133 -10
View File
@@ -7,6 +7,7 @@ require_once __DIR__ . '/libs/OCPPFrameRouter.php';
class OCPP_Server extends IPSModule
{
private const ATTR_CONNECTIONS = 'Connections';
private const ATTR_OUTBOUND_QUEUES = 'OutboundQueues';
public function Create()
{
@@ -20,6 +21,7 @@ class OCPP_Server extends IPSModule
$this->RegisterPropertyInteger('DebugLevel', 0);
$this->RegisterAttributeString(self::ATTR_CONNECTIONS, ConnectionRegistry::toJson(ConnectionRegistry::empty()));
$this->RegisterAttributeString(self::ATTR_OUTBOUND_QUEUES, json_encode([]));
$this->RegisterVariableString('TransportStatus', 'TransportStatus', '', 10);
$this->RegisterVariableString('LastInboundFrame', 'LastInboundFrame', '', 20);
@@ -27,6 +29,7 @@ class OCPP_Server extends IPSModule
$this->RegisterVariableString('LastRouteResult', 'LastRouteResult', '', 22);
$this->RegisterVariableInteger('ConnectionCount', 'ConnectionCount', '', 30);
$this->RegisterVariableInteger('LastMessageTime', 'LastMessageTime', '', 31);
$this->RegisterVariableInteger('OutboundQueueCount', 'OutboundQueueCount', '', 32);
$this->RegisterVariableString('WebSocketSupportStatus', 'WebSocketSupportStatus', '', 40);
$this->RegisterVariableString('LetzteMeldung', 'LetzteMeldung', '', 41);
$this->RegisterVariableInteger('LetzteMeldungZeit', 'LetzteMeldungZeit', '', 42);
@@ -51,6 +54,11 @@ class OCPP_Server extends IPSModule
$this->tryRegisterHook();
}
$defaultTarget = $this->ReadPropertyInteger('DefaultTargetInstance');
if ($defaultTarget > 0) {
$this->RegisterReference($defaultTarget);
}
$this->SetStatus(102);
}
@@ -69,6 +77,10 @@ class OCPP_Server extends IPSModule
$this->RouteInboundFrame((string)$Value);
break;
case 'DequeueOutboundFrame':
$this->DequeueOutboundFrame((string)$Value);
break;
case 'TransportWatchdog':
$this->TransportWatchdog();
break;
@@ -77,6 +89,8 @@ class OCPP_Server extends IPSModule
$this->SetValue('LastInboundFrame', '');
$this->SetValue('LastOutboundFrame', '');
$this->SetValue('LastRouteResult', '');
$this->WriteAttributeString(self::ATTR_OUTBOUND_QUEUES, json_encode([]));
$this->SetValue('OutboundQueueCount', 0);
$this->setMessage('Puffer geloescht');
break;
@@ -95,27 +109,42 @@ class OCPP_Server extends IPSModule
$path = $_SERVER['REQUEST_URI'] ?? $this->ReadPropertyString('HookPath');
$chargePointId = (new OCPPFrameRouter())->extractChargePointId((string)$path);
$this->RouteInboundFrame(json_encode([
$responseFrame = $this->RouteInboundFrame(json_encode([
'ChargePointId' => $chargePointId,
'Frame' => $raw,
'Remote' => ($_SERVER['REMOTE_ADDR'] ?? '') . ':' . ($_SERVER['REMOTE_PORT'] ?? '')
]));
header('Content-Type: application/json');
echo json_encode([
'status' => 'accepted',
'note' => 'OCPP transport scaffold. Produktiver WebSocket-Dauerbetrieb muss mit Station verifiziert werden.'
]);
if ($responseFrame !== '') {
echo $responseFrame;
return;
}
echo json_encode(['status' => 'accepted']);
}
public function QueueOutboundFrame(string $json): void
{
$this->SetValue('LastOutboundFrame', $json);
$this->SetValue('LastMessageTime', time());
$this->setMessage('Outbound Frame vorgemerkt. Aktiver WebSocket-Sendekanal ist noch Scaffold.');
$envelope = json_decode($json, true);
if (!is_array($envelope)) {
$envelope = [
'ChargePointId' => '',
'Frame' => $json,
'Timestamp' => time()
];
}
public function RouteInboundFrame(string $json): void
$frame = (string)($envelope['Frame'] ?? $json);
$chargePointId = (string)($envelope['ChargePointId'] ?? '');
$this->SetValue('LastOutboundFrame', $frame);
$this->SetValue('LastMessageTime', time());
$this->enqueueOutboundFrame($chargePointId, $frame);
$this->setMessage('Outbound Frame fuer ' . ($chargePointId === '' ? 'unbekannt' : $chargePointId) . ' gepuffert.');
}
public function RouteInboundFrame(string $json): string
{
$data = json_decode($json, true);
if (!is_array($data)) {
@@ -163,10 +192,58 @@ class OCPP_Server extends IPSModule
if ($target > 0 && IPS_InstanceExists($target)) {
IPS_RequestAction($target, 'HandleInboundFrame', $frame);
$this->setMessage('Inbound Frame an Zielinstanz ' . $target . ' geroutet.');
return;
return $this->DequeueOutboundFrame($chargePointId, $this->extractUniqueId($frame));
}
$this->setMessage('Inbound Frame empfangen, aber keine Zielinstanz gefunden.');
return '';
}
public function DequeueOutboundFrame(string $chargePointId = '', string $preferredUniqueId = ''): string
{
$queues = $this->readOutboundQueues();
if ($preferredUniqueId !== '') {
foreach ($queues as $candidateKey => $queue) {
if (!is_array($queue)) {
continue;
}
foreach ($queue as $candidateIndex => $candidateFrame) {
if ($this->isCallResultForUniqueId((string)$candidateFrame, $preferredUniqueId)) {
$frame = (string)$candidateFrame;
array_splice($queues[$candidateKey], (int)$candidateIndex, 1);
if (empty($queues[$candidateKey])) {
unset($queues[$candidateKey]);
}
$this->WriteAttributeString(self::ATTR_OUTBOUND_QUEUES, json_encode($queues));
$this->SetValue('OutboundQueueCount', $this->countOutboundFrames($queues));
return $frame;
}
}
}
}
$key = $chargePointId;
if ($key === '' || !isset($queues[$key]) || empty($queues[$key])) {
foreach ($queues as $candidate => $queue) {
if (!empty($queue)) {
$key = (string)$candidate;
break;
}
}
}
if ($key === '' || !isset($queues[$key]) || empty($queues[$key])) {
$this->SetValue('OutboundQueueCount', $this->countOutboundFrames($queues));
return '';
}
$frame = (string)array_shift($queues[$key]);
if (empty($queues[$key])) {
unset($queues[$key]);
}
$this->WriteAttributeString(self::ATTR_OUTBOUND_QUEUES, json_encode($queues));
$this->SetValue('OutboundQueueCount', $this->countOutboundFrames($queues));
return $frame;
}
public function TransportWatchdog(): void
@@ -215,6 +292,52 @@ class OCPP_Server extends IPSModule
$this->SendDebug('OCPP_Server', $message, 0);
}
}
private function enqueueOutboundFrame(string $chargePointId, string $frame): void
{
$key = $chargePointId === '' ? '_default' : $chargePointId;
$queues = $this->readOutboundQueues();
if (!isset($queues[$key]) || !is_array($queues[$key])) {
$queues[$key] = [];
}
$queues[$key][] = $frame;
$this->WriteAttributeString(self::ATTR_OUTBOUND_QUEUES, json_encode($queues));
$this->SetValue('OutboundQueueCount', $this->countOutboundFrames($queues));
}
private function readOutboundQueues(): array
{
$queues = json_decode($this->ReadAttributeString(self::ATTR_OUTBOUND_QUEUES), true);
return is_array($queues) ? $queues : [];
}
private function countOutboundFrames(array $queues): int
{
$count = 0;
foreach ($queues as $queue) {
if (is_array($queue)) {
$count += count($queue);
}
}
return $count;
}
private function extractUniqueId(string $frame): string
{
$data = json_decode($frame, true);
if (!is_array($data) || count($data) < 2) {
return '';
}
return (string)$data[1];
}
private function isCallResultForUniqueId(string $frame, string $uniqueId): bool
{
$data = json_decode($frame, true);
return is_array($data)
&& (int)($data[0] ?? 0) === 3
&& (string)($data[1] ?? '') === $uniqueId;
}
}
?>
+13
View File
@@ -56,6 +56,7 @@ Phase 1 ist ein produktionsnaher Scaffold, aber noch kein fertig abgenommener OC
Sichtbar und vorbereitet:
- OCPP 1.6 Grundgeruest.
- Fronius Wattpilot Gen1 Profil fuer OCPP 1.6J mit einfachen Ampere-Profilen.
- OCPP 2.0.1/2.1 Adapterklassen als Struktur.
- Manager-Anbindung mit `PowerSteps`.
- Managementmodi: Nie laden, Immer laden, Konstanter Strom, Nur Solar.
@@ -127,6 +128,18 @@ Quellen:
- https://www.symcon.de/de/service/dokumentation/modulreferenz/webhook-control/
- https://www.symcon.de/de/service/dokumentation/entwicklerbereich/sdk-tools/sdk-php/module/processhookdata/
## Fronius Wattpilot Gen1
Der Wattpilot-Gen1-Pfad ist in `docs/OCPP/WATTPILOT_GEN1.md` beschrieben. Die Umsetzung bleibt strikt OCPP-only und verwendet keine lokale go-e API, keine Fronius Solar API und keine Cloud.
Wichtige technische Regeln:
- OCPP 1.6J.
- einfache `SetChargingProfile`-Limits in Ampere.
- keine erzwungenen `numberPhases` oder `phaseToUse`.
- `RemoteStartTransaction`, `RemoteStopTransaction` und `ChangeAvailability` sind vorbereitet.
- Smart-Charging wird bei Ablehnung nach begrenzten Retries capability-basiert deaktiviert.
## Variablenkatalog
Bedienung:
+125
View File
@@ -0,0 +1,125 @@
# Fronius Wattpilot Gen1 ueber OCPP 1.6J
Stand: 2026-05-10
Diese Anleitung beschreibt den Testpfad fuer Fronius Wattpilot Home/Go Gen1 mit `Ladestation_OCPP` und `OCPP_Server`.
## Grundsatz
Die Integration ist OCPP-only:
- keine lokale go-e HTTP API
- keine Fronius Solar API
- keine Fronius Cloud
- kein MQTT
- keine proprietaeren Fallbacks
Der Wattpilot wird als unterstuetztes Geraet behandelt, aber nicht als Referenz fuer die gesamte OCPP-Architektur. Alle Funktionen bleiben capability-basiert.
## Symcon-Konfiguration
1. `OCPP_Server` Instanz anlegen.
2. `HookPath` auf `/hook/ocpp` lassen oder passend setzen.
3. `Ladestation_OCPP` Instanz anlegen.
4. `DeviceProfile` auf `Fronius Wattpilot Gen1 OCPP 1.6J` setzen.
5. `OCPPVersionMode` auf `OCPP 1.6` oder `Automatisch` setzen.
6. `ChargePointId` exakt so setzen, wie sie im Wattpilot konfiguriert wird.
7. `OCPPServerInstance` auf die `OCPP_Server` Instanz setzen.
8. Im `OCPP_Server` entweder `DefaultTargetInstance` auf die Ladestationsinstanz setzen oder unter `Ladepunkte` eine Route fuer `ChargePointId`, `EVSEId = 1`, `ConnectorId = 1` anlegen.
9. Die `Ladestation_OCPP` Instanz im bestehenden `Manager` als Verbraucher eintragen.
## Wattpilot-Konfiguration
Im Wattpilot OCPP aktivieren und die lokale Symcon-CSMS-Adresse eintragen.
Das genaue URL-Schema haengt vom aktiven Symcon-Webserver und der WebHook/WebSocket-Unterstuetzung ab. Fuer den Spike ist die Zielstruktur:
```text
ws://<symcon-ip>:<symcon-web-port>/hook/ocpp/<ChargePointId>
```
Bei TLS entsprechend:
```text
wss://<symcon-host>/hook/ocpp/<ChargePointId>
```
Wenn der Wattpilot im Status nicht bis "connected and accepted" kommt, ist zuerst der Symcon-WebHook/WebSocket-Rueckkanal zu pruefen. Es wird kein externer Transport-Adapter automatisch eingebaut.
## Implementierter OCPP-1.6J-Pfad
Eingehend:
- `BootNotification` -> `Accepted`, Heartbeat-Intervall
- `Heartbeat` -> `currentTime`
- `StatusNotification` -> Fahrzeugstatus, `Car_detected`, `Car_is_full`
- `MeterValues` -> Leistung, Energie, Strom, Spannung
- `Authorize` -> `Accepted`
- `StartTransaction` -> TransactionId und `Accepted`
- `StopTransaction` -> `Accepted`
- `DataTransfer` -> nur bei erlaubter Registry
Ausgehend:
- `SetChargingProfile`
- `ClearChargingProfile`
- `RemoteStartTransaction`
- `RemoteStopTransaction`
- `ChangeAvailability`
## Smart Charging fuer Wattpilot Gen1
Das Profil erzeugt bewusst nur einfache Ampere-Limits:
- `chargingRateUnit = A`
- `chargingProfileKind = Absolute`
- `chargingProfilePurpose = TxProfile` bei aktiver Transaktion, sonst `TxDefaultProfile`
- genau eine `chargingSchedulePeriod`
- kurze Gueltigkeit von 120 Sekunden
Nicht verwendet:
- mehrere Perioden
- komplexe Schedules
- `numberPhases`
- `phaseToUse`
- asymmetrische Phasenprofile
Wenn `SetChargingProfile` abgelehnt wird, versucht das Modul maximal `SmartChargingRetryLimit` Wiederholungen. Danach wird `Capability_SmartCharging = false` gesetzt und weitere Profile werden blockiert, bis "SmartCharging testen" erneut ausgefuehrt wird.
## Testablauf
1. `OCPP_Server` und `Ladestation_OCPP` anlegen und konfigurieren.
2. Wattpilot mit Symcon-CSMS-Adresse verbinden.
3. `BootNotification` pruefen:
- `OCPP_Connected = true`
- `OCPP_LastSeen` aktualisiert
- `Capability_MeterValues = true`
4. Fahrzeug einstecken:
- `StatusNotification` setzt `Car_detected`
- `Fahrzeugstatus` wechselt
5. Ladevorgang starten:
- `Remote Start` testen oder lokal starten
- `StartTransaction` wird gespeichert
6. Messwerte pruefen:
- `Ladeleistung_Effektiv`
- `Bezogene_Energie`
- optional `Strom_L1/L2/L3`, `Spannung_L1/L2/L3`
7. Manager testen:
- `PowerSteps` enthalten `0` und plausible Wattwerte
- `SetAktuelle_Leistung` schreibt nur `Power`
- `Do_UserCalc` sendet `SetChargingProfile`
8. Fehlerfaelle testen:
- WLAN trennen
- MeterValues stoppen
- falsche ChargePointId
- abgelehntes ChargingProfile
## Bekannte Grenze
`OCPP_Server` bleibt ein Symcon-Transport-Spike. Er puffert ausgehende Frames pro ChargePoint und gibt direkte CallResults zurueck. Ob die WebHook/WebSocket-Mechanik in der konkreten Symcon-Windows-Installation fuer dauerhafte OCPP-Verbindungen ausreicht, muss mit dem Wattpilot oder einem OCPP-1.6J-Simulator bestaetigt werden.
## Quellen
- Fronius Wattpilot Support: https://www.fronius.com/en/help-center/solar-energy/e-mobility/support-wattpilot
- Fronius Wattpilot OCPP 1.6 J: https://www.fronius.com/en/help-center/solar-energy/products/monitoring-control/solutions/open-interfaces/wattpilot-ocpp-1-6-j