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

PriorityLoadRated kWNotes
1 (first)HVAC compressor 245Redundant; HVAC 1 maintains temp
2Lighting zone B12Non-production area
3Air compressor 330Receiver tank provides buffer
4Cooling tower fan 222Partial cooling acceptable short-term
5 (last)Packaging line 235Reduces 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

FeatureImplementation
Power monitoringStructured data from Modbus meters with scaling
Demand predictionLinear projection with weighted recent-power bias
Load sheddingPriority-based automatic shedding with min-off timers
Energy KPIsSEC, load factor, power factor, cost, CO2 tracking
Power factor correctionAutomatic capacitor bank stepping with anti-hunting delay
ISO 50001 dataStructured logging for energy management system compliance
None of this needs a separate system. It runs as a layer on top of your existing PLC control, and every motor start, compressor cycle and heater activation it already commands is a chance to save energy.