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 (the length of the period meters are stored in). Change to T#300S (5 minutes) for more redundancy in case of connection errors. If the controller also polls GET /v1/strategy, use T#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 sends rSoC and 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, build sJson without the "soc" pair, so the server never takes an old value for a fresh one.
  • 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. Re-sending never stores a meter twice: a meter already stored in the current 15-minute period is skipped. soc is 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 sJson string building in the implementation.
  • Diagnostics: output variables bBusy, bError, iHttpStatus can be connected to a CODESYS visualization to monitor sending status. A 200 does not prove that every device was stored: a mistyped device code is reported in device_results in sResponse.

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