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 PLC-Based Energy Management?
Energy typically represents 15–30% of manufacturing costs. Most plants pay not just for energy consumed (kWh) but also for peak demand (kW) — the highest 15-minute average power draw in the billing period. A single demand spike can increase your electricity bill by thousands of dollars per month for an entire year.
PLCs are perfectly positioned for energy management because they already control the equipment that consumes power. By adding power monitoring and intelligent load coordination, you can cut energy costs 10–25% without reducing production.
📊 Power Monitoring Data Structures
Modern power meters (Schneider PM5xxx, Siemens PAC, ABB M4M) communicate via Modbus TCP or PROFINET. Structure the data cleanly:
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
FUNCTION_BLOCK FB_PowerMeterReader
VAR_INPUT
RawRegisters : ARRAY[0..39] OF INT; // Modbus holding registers
CommStatus : BOOL;
END_VAR
VAR_OUTPUT
Data : PowerMeterData;
END_VARData.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 you're 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_VARIntervalSecs := 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;
🔌 Intelligent Load Shedding
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_TYPEFUNCTION_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;
Load Shedding Priority Example
| 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
Track these metrics to identify savings opportunities:
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_TYPEFUNCTION_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
Poor power factor (< 0.9) means you're drawing more current than necessary, causing higher losses and 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;
Summary
| 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 |
Energy management isn't a separate system — it's a layer on top of your existing PLC control. Every motor start, compressor cycle, and heater activation is an opportunity to optimize.