Skip to main content

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_key from 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#900S means sending every 15 minutes (matching system aggregation interval). Change to T#300S (5 minutes) for more redundancy in case of connection errors.
  • Cumulative meters: energy_*_meter values must be always increasing. Never reset — the system calculates energy gain automatically.
  • Error handling: if iHttpStatus is not 200, 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 sJson string building in the implementation.
  • Diagnostics: output variables bBusy, bError, iHttpStatus can 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