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 (the length of the period meters are stored in). Change toT#300S(5 minutes) for more redundancy in case of connection errors. If the controller also polls GET /v1/strategy, useT#60S: the command depends on the latest SoC reading of each battery, and when the newest reading of any battery is older than 30 minutes the server does not choose a command from the plan, so on a 15-minute cycle one failed send can switch the controller to its fallback. The sample always sendsrSoCand has no input that says the battery read failed. If your read can fail, add such an input and, in a cycle whose read failed, buildsJsonwithout the"soc"pair, so the server never takes an old value for a fresh one. - 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. Re-sending never stores a meter twice: a meter already stored in the current 15-minute period is skipped.socis not skipped that way; see POST /v1/ingest for how SoC readings are stored. - Device codes: must exactly match the configuration in EnergyPal. Verify with
GET /v1/devices. - Other device sets: 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. A200does not prove that every device was stored: a mistyped device code is reported indevice_resultsinsResponse.
Receiving the Control Command
To control the battery and the PV inverter, the controller polls GET /v1/strategy once a minute. That page covers the response fields, the strategy_valid gate the controller must check before applying a command, and what it does when no fresh answer arrives.
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