Energy Management
PLC Energy Management: Power Monitoring, Load Shedding & Demand Control in Structured Text
Electricity is your factory's second-largest cost. Build PLC-based energy management with power monitoring, intelligent load shedding, demand prediction, and ISO 50001 KPI tracking in Structured Text.
Why do energy management in the PLC
In energy-intensive plants, energy can represent 15–30% of manufacturing costs. Most plants pay for the energy they consume (kWh) and also for peak demand (kW): the highest 15-minute average power draw in the billing period. A single demand spike can add thousands of dollars per month to the electricity bill for an entire year.
The PLC already controls the equipment that consumes the power, so it is the natural place to manage it. Add power monitoring and coordinate the loads, and you can cut energy costs by 10–25% without reducing production.
A data structure for the power meter
Current power meters (Schneider PM5xxx, Siemens PAC, ABB M4M) communicate over Modbus TCP or PROFINET. Put everything one meter reports into a single structure, grouped by instantaneous, accumulated and demand values:
TYPE PowerMeterData :
STRUCT
// Instantaneous values
Voltage_L1N : REAL; // Volts
Voltage_L2N : REAL;
Voltage_L3N : REAL;
Voltage_L1L2 : REAL;
Current_L1 : REAL; // Amps
Current_L2 : REAL;
Current_L3 : REAL;
ActivePower : REAL; // kW (total 3-phase)
ReactivePower : REAL; // kVAR
ApparentPower : REAL; // kVA
PowerFactor : REAL; // 0.0 to 1.0
Frequency : REAL; // Hz
// Accumulated values
ActiveEnergy : REAL; // kWh (total since reset)
ReactiveEnergy : REAL; // kVARh
// Demand values
DemandPower : REAL; // kW (current demand interval average)
PeakDemand : REAL; // kW (highest this billing period)
// Quality
CommOK : BOOL;
DataValid : BOOL;
END_STRUCT;
END_TYPE
Scaling from Modbus registers
The reader block turns the raw holding registers into engineering units and marks the data invalid as soon as communication drops. The register numbers and word order are meter-specific, so check them against your meter's register map.
FUNCTION_BLOCK FB_PowerMeterReader
VAR_INPUT
RawRegisters : ARRAY[0..39] OF INT; // Modbus holding registers
CommStatus : BOOL;
END_VAR
VAR_OUTPUT
Data : PowerMeterData;
END_VAR
Data.CommOK := CommStatus;
IF NOT CommStatus THEN
Data.DataValid := FALSE;
RETURN;
END_IF;
// Scale registers to engineering units (meter-specific scaling)
// Example: PM5xxx uses register pairs for REAL values
Data.Voltage_L1N := FC_RegsToReal(
HighWord := INT_TO_WORD(RawRegisters[0]),
LowWord := INT_TO_WORD(RawRegisters[1]),
BigEndian := TRUE
);
Data.Current_L1 := FC_RegsToReal(
HighWord := INT_TO_WORD(RawRegisters[6]),
LowWord := INT_TO_WORD(RawRegisters[7]),
BigEndian := TRUE
);
Data.ActivePower := FC_RegsToReal(
HighWord := INT_TO_WORD(RawRegisters[12]),
LowWord := INT_TO_WORD(RawRegisters[13]),
BigEndian := TRUE
);
Data.PowerFactor := FC_RegsToReal(
HighWord := INT_TO_WORD(RawRegisters[18]),
LowWord := INT_TO_WORD(RawRegisters[19]),
BigEndian := TRUE
);
Data.DataValid := TRUE;
Demand monitoring and prediction
The utility measures demand in 15-minute intervals. If you can predict that you are about to exceed your target, you can shed loads before the interval closes:
FUNCTION_BLOCK FB_DemandPredictor
VAR_INPUT
InstantPower : REAL; // Current kW
DemandTarget : REAL; // kW target (contract limit)
IntervalLength : TIME := T#15M; // Utility demand interval
END_VAR
VAR_OUTPUT
CurrentDemand : REAL; // Rolling average this interval
PredictedDemand : REAL; // Where we'll end up
ExceedingTarget : BOOL;
ShedRequired : REAL; // kW to shed to stay under target
IntervalProgress: REAL; // 0–100%
END_VAR
VAR
EnergyAccum : REAL := 0.0; // kWh accumulated this interval
SampleCount : DINT := 0;
IntervalSecs : REAL;
ElapsedSecs : REAL;
tmrInterval : TON;
scanTimeSec : REAL := 0.01;
END_VAR
IntervalSecs := TIME_TO_REAL(IntervalLength) / 1000.0;
// Accumulate energy
EnergyAccum := EnergyAccum + (InstantPower * scanTimeSec / 3600.0);
SampleCount := SampleCount + 1;
ElapsedSecs := DINT_TO_REAL(SampleCount) * scanTimeSec;
// Current demand = energy / elapsed hours * 1 hour
IF ElapsedSecs > 0.0 THEN
CurrentDemand := EnergyAccum / (ElapsedSecs / 3600.0);
END_IF;
// Predict end-of-interval demand
IntervalProgress := (ElapsedSecs / IntervalSecs) * 100.0;
IF IntervalProgress > 5.0 THEN
// Linear projection
PredictedDemand := CurrentDemand;
// Weight recent power more heavily in early interval
IF IntervalProgress < 50.0 THEN
PredictedDemand := (CurrentDemand * 0.4) + (InstantPower * 0.6);
END_IF;
END_IF;
ExceedingTarget := PredictedDemand > (DemandTarget * 0.95);
ShedRequired := PredictedDemand - DemandTarget;
IF ShedRequired < 0.0 THEN ShedRequired := 0.0; END_IF;
// Reset at interval boundary
tmrInterval(IN := TRUE, PT := IntervalLength);
IF tmrInterval.Q THEN
tmrInterval(IN := FALSE);
EnergyAccum := 0.0;
SampleCount := 0;
END_IF;
Load shedding by priority
When demand approaches the target, shed loads in priority order, least critical first:
TYPE SheddableLoad :
STRUCT
LoadName : STRING[20];
RatedPower : REAL; // kW when running
Priority : INT; // 1=shed first, 5=shed last
MinOffTime : TIME; // Don't cycle too fast
CanShed : BOOL; // Currently available to shed
IsShed : BOOL; // Currently shed
RunCmd : BOOL; // Normal run command
END_STRUCT;
END_TYPE
FUNCTION_BLOCK FB_LoadShedder
VAR_INPUT
Enable : BOOL;
ShedRequired : REAL; // kW to shed
Loads : ARRAY[1..12] OF SheddableLoad;
NumLoads : INT;
END_VAR
VAR_OUTPUT
TotalShed : REAL; // kW currently shed
LoadsShed : INT; // Count of shed loads
ShedCommands : ARRAY[1..12] OF BOOL; // TRUE = shed this load
END_VAR
VAR
i : INT;
accumulated : REAL;
tmrMinOff : ARRAY[1..12] OF TON;
END_VAR
IF NOT Enable OR ShedRequired <= 0.0 THEN
// No shedding needed — release all loads
FOR i := 1 TO NumLoads DO
ShedCommands[i] := FALSE;
END_FOR;
TotalShed := 0.0;
LoadsShed := 0;
RETURN;
END_IF;
// Shed loads in priority order (1=first to shed)
accumulated := 0.0;
LoadsShed := 0;
FOR i := 1 TO NumLoads DO
// Simple priority scan — shed lowest priority first
IF Loads[i].CanShed AND accumulated < ShedRequired THEN
ShedCommands[i] := TRUE;
accumulated := accumulated + Loads[i].RatedPower;
LoadsShed := LoadsShed + 1;
ELSE
ShedCommands[i] := FALSE;
END_IF;
END_FOR;
TotalShed := accumulated;
Example shedding priorities
| Priority | Load | Rated kW | Notes |
|---|---|---|---|
| 1 (first) | HVAC compressor 2 | 45 | Redundant; HVAC 1 maintains temp |
| 2 | Lighting zone B | 12 | Non-production area |
| 3 | Air compressor 3 | 30 | Receiver tank provides buffer |
| 4 | Cooling tower fan 2 | 22 | Partial cooling acceptable short-term |
| 5 (last) | Packaging line 2 | 35 | Reduces throughput; shed only if critical |
Energy KPI calculations
These metrics show where the savings are:
TYPE EnergyKPIs :
STRUCT
kWh_Total : REAL; // Total energy consumed
kWh_PerUnit : REAL; // Energy per product unit (SEC)
PeakDemand_kW : REAL; // Peak demand this period
LoadFactor : REAL; // Average/Peak (0–1, higher=better)
PowerFactor_Avg : REAL; // Average PF (target >0.95)
CostPerHour : REAL; // Estimated $/hour
CO2_Tonnes : REAL; // Carbon footprint
END_STRUCT;
END_TYPE
FUNCTION_BLOCK FB_EnergyKPICalculator
VAR_INPUT
ActivePower : REAL; // Current kW
PowerFactor : REAL;
ProductCount : DINT; // Units produced this period
CostPerKWh : REAL; // $/kWh
CO2PerKWh : REAL; // kg CO2/kWh (grid emission factor)
PeriodHours : REAL;
END_VAR
VAR_OUTPUT
KPIs : EnergyKPIs;
END_VAR
VAR
EnergyAccum : REAL := 0.0;
PFAccum : REAL := 0.0;
SampleCount : DINT := 0;
scanTimeSec : REAL := 0.01;
END_VAR
// Accumulate energy
EnergyAccum := EnergyAccum + (ActivePower * scanTimeSec / 3600.0);
KPIs.kWh_Total := EnergyAccum;
// Track peak demand
IF ActivePower > KPIs.PeakDemand_kW THEN
KPIs.PeakDemand_kW := ActivePower;
END_IF;
// Average power factor
SampleCount := SampleCount + 1;
PFAccum := PFAccum + PowerFactor;
KPIs.PowerFactor_Avg := PFAccum / DINT_TO_REAL(SampleCount);
// Specific Energy Consumption (SEC) — kWh per unit produced
IF ProductCount > 0 THEN
KPIs.kWh_PerUnit := EnergyAccum / DINT_TO_REAL(ProductCount);
END_IF;
// Load factor
IF KPIs.PeakDemand_kW > 0.0 AND PeriodHours > 0.0 THEN
KPIs.LoadFactor := (EnergyAccum / PeriodHours) / KPIs.PeakDemand_kW;
END_IF;
// Cost and carbon
KPIs.CostPerHour := ActivePower * CostPerKWh;
KPIs.CO2_Tonnes := EnergyAccum * CO2PerKWh / 1000.0;
Power factor correction
A poor power factor (< 0.9) means you are drawing more current than necessary, which raises losses and brings utility penalties:
FUNCTION_BLOCK FB_PFCController
VAR_INPUT
PowerFactor : REAL; // Measured PF
TargetPF : REAL := 0.95;
ReactivePower : REAL; // kVAR measured
CapBankSteps : INT := 6; // Number of capacitor steps
StepSize : REAL := 25.0; // kVAR per step
END_VAR
VAR_OUTPUT
StepsEngaged : INT;
CapacitorKVAR : REAL;
PFCorrected : REAL;
END_VAR
VAR
RequiredKVAR : REAL;
tmrStepDelay : TON;
pendingStep : INT;
END_VAR
// Calculate required compensation
IF PowerFactor < TargetPF AND PowerFactor > 0.0 THEN
// kVAR needed = kW × (tan(arccos(current PF)) - tan(arccos(target PF)))
// Simplified: use reactive power directly
RequiredKVAR := ReactivePower * (1.0 - (TargetPF / PowerFactor));
pendingStep := REAL_TO_INT(RequiredKVAR / StepSize);
IF pendingStep < 0 THEN pendingStep := 0; END_IF;
IF pendingStep > CapBankSteps THEN pendingStep := CapBankSteps; END_IF;
ELSE
pendingStep := 0;
END_IF;
// Step change delay — prevent capacitor hunting
tmrStepDelay(IN := (pendingStep <> StepsEngaged), PT := T#30S);
IF tmrStepDelay.Q THEN
StepsEngaged := pendingStep;
tmrStepDelay(IN := FALSE);
END_IF;
CapacitorKVAR := INT_TO_REAL(StepsEngaged) * StepSize;
What each block covers
| Feature | Implementation |
|---|---|
| Power monitoring | Structured data from Modbus meters with scaling |
| Demand prediction | Linear projection with weighted recent-power bias |
| Load shedding | Priority-based automatic shedding with min-off timers |
| Energy KPIs | SEC, load factor, power factor, cost, CO2 tracking |
| Power factor correction | Automatic capacitor bank stepping with anti-hunting delay |
| ISO 50001 data | Structured logging for energy management system compliance |