PLC Integration: WAGO PFC200 + CODESYS 3.5
Complete guide for configuring a WAGO PFC200 PLC to send PV and battery storage metrics to EnergyPal via HTTP POST.
Prerequisites
- Controller: WAGO PFC200 (750-8212) with e!RUNTIME
- CODESYS libraries:
WagoAppHTTP,WagoTypes— install via Library Manager in CODESYS IDE - Network: controller must have internet access (port 443/HTTPS). WAGO PFC200 includes built-in CA certificates.
- API key: your
energy_connection_keyfrom EnergyPal administrator - Configured devices: device codes (e.g.,
PV-1,BESS-1) must be registered in EnergyPal. Verify with GET /v1/devices.
Step 1: Declare the Function Block FB_EnergyPalSender
Create a new POU of type FUNCTION_BLOCK in your CODESYS project. Paste the following variable declaration:
FUNCTION_BLOCK FB_EnergyPalSender
VAR_INPUT
rSoC : REAL; (* Battery state of charge [%] *)
rEnergyExported : REAL; (* BESS discharge meter [kWh] *)
rEnergyImported : REAL; (* BESS charge meter [kWh] *)
rPvGenerated1 : REAL; (* PV-1 generation meter [kWh] *)
rPvGenerated2 : REAL; (* PV-2 generation meter [kWh] *)
rEnergyConsumed : REAL; (* LOAD-0 consumption meter [kWh] *)
END_VAR
VAR_OUTPUT
bBusy : BOOL; (* Request in progress *)
bError : BOOL; (* Last request failed *)
iHttpStatus : INT; (* HTTP response code *)
END_VAR
VAR
fbHttpClient : WagoAppHTTP.WagoHTTPClient;
fbTimer : TON;
bSend : BOOL;
bRequestDone : BOOL;
sJson : STRING(2048);
sUrl : STRING(255) := 'https://api.energypal.ai/v1/ingest';
sApiKey : STRING(255) := 'your-api-key'; (* <- insert your key *)
sContentType : STRING(64) := 'application/json';
sResponse : STRING(1024);
END_VAR
Set sApiKey to your actual API key. Adjust device codes if different from PV-1, PV-2, BESS-1, LOAD-0.
Step 2: Implementation
In the implementation tab of FB_EnergyPalSender, paste the following code. The block builds a JSON payload with current metric values and sends it via HTTP POST every 15 minutes:
(* === Cyclic timer - send every 15 minutes === *)
fbTimer(IN := NOT fbTimer.Q, PT := T#900S);
IF fbTimer.Q AND NOT bBusy THEN
bSend := TRUE;
END_IF
(* === Build JSON === *)
IF bSend THEN
bSend := FALSE;
bBusy := TRUE;
bError := FALSE;
sJson := CONCAT('{"metrics":{',
'"PV-1":{"energy_generated_meter":', REAL_TO_STRING(rPvGenerated1), '},',
'"PV-2":{"energy_generated_meter":', REAL_TO_STRING(rPvGenerated2), '},',
'"BESS-1":{"soc":', REAL_TO_STRING(rSoC),
',"energy_exported_meter":', REAL_TO_STRING(rEnergyExported),
',"energy_imported_meter":', REAL_TO_STRING(rEnergyImported),
'},',
'"LOAD-0":{"energy_consumed_meter":', REAL_TO_STRING(rEnergyConsumed),
'}}}');
(* === Configure and send HTTP POST === *)
fbHttpClient.sUrl := sUrl;
fbHttpClient.sContentType := sContentType;
fbHttpClient.sRequestData := sJson;
fbHttpClient.bExecute := TRUE;
(* Add X-API-Key header *)
fbHttpClient.AddHeader('X-API-Key', sApiKey);
END_IF
(* === Handle response === *)
fbHttpClient();
IF fbHttpClient.bDone OR fbHttpClient.bError THEN
bBusy := FALSE;
iHttpStatus := fbHttpClient.iStatusCode;
bError := fbHttpClient.bError OR (iHttpStatus <> 200);
sResponse := fbHttpClient.sResponseData;
fbHttpClient.bExecute := FALSE;
END_IF
Step 3: Call from PLC_PRG
In your main program PLC_PRG, create an instance of the block and connect your process variables:
PROGRAM PLC_PRG
VAR
fbSender : FB_EnergyPalSender;
(* --- Insert your process variables here --- *)
rSoC_BMS : REAL; (* e.g. Modbus read / analog input *)
rExportMeter_BMS : REAL;
rImportMeter_BMS : REAL;
rPvMeter_Inverter1 : REAL;
rPvMeter_Inverter2 : REAL;
rConsumptionMeter : REAL; (* total energy consumed [kWh] *)
END_VAR
(* Call the sender block *)
fbSender(
rSoC := rSoC_BMS,
rEnergyExported := rExportMeter_BMS,
rEnergyImported := rImportMeter_BMS,
rPvGenerated1 := rPvMeter_Inverter1,
rPvGenerated2 := rPvMeter_Inverter2,
rEnergyConsumed := rConsumptionMeter
);
(* Optional: diagnostics in visualization *)
(* fbSender.bBusy - is sending in progress *)
(* fbSender.bError - did the last send fail *)
(* fbSender.iHttpStatus - HTTP response code (200 = OK) *)
Configuration Notes
- Send interval: timer
T#900Smeans sending every 15 minutes (matching system aggregation interval). Change toT#300S(5 minutes) for more redundancy in case of connection errors. - Cumulative meters:
energy_*_metervalues must be always increasing. Never reset — the system calculates energy gain automatically. - Error handling: if
iHttpStatusis not200, the block automatically retries on the next timer cycle. The system is idempotent — duplicates within a 15-minute window are skipped. - Device codes: must exactly match the configuration in EnergyPal. Verify with
GET /v1/devices. - Custom devices: if you have a different number of inverters or batteries, adjust the
sJsonstring building in the implementation. - Diagnostics: output variables
bBusy,bError,iHttpStatuscan be connected to a CODESYS visualization to monitor sending status.
Reading Energy Prices
To fetch energy price forecasts on the WAGO PLC (for battery optimization), see the dedicated guide: Fetching Energy Prices (WAGO + CODESYS).
Testing with curl
Before configuring the PLC, test the API connection:
# Send test data
curl -X POST https://api.energypal.ai/v1/ingest \
-H "Content-Type: application/json" \
-H "X-API-Key: your-api-key" \
-d '{
"metrics": {
"PV-1": {"energy_generated_meter": 12345.0},
"BESS-1": {"soc": 85.5, "energy_exported_meter": 500.0, "energy_imported_meter": 450.0},
"LOAD-0": {"energy_consumed_meter": 98765.0}
}
}'
# Verify your device codes
curl -H "X-API-Key: your-api-key" https://api.energypal.ai/v1/devices