跳转到内容

案例 32:车间能耗异常判定与超标预警

本案例对应 JQuick-Java USE-CASES 场景 32(诉求 37:车间能耗异常判定与超标预警),属于制造能源管理领域,决策内容是按班次聚合电耗、水耗、最大需量、单耗、温度等多路信号,输出 CRITICAL/WARNING/INFO/NORMAL 四级预警与处置提示。

制造企业的能源管理系统按车间、班次采集能耗与工况数据。入参模型 WorkshopEnergyRecord 承载车间编号与名称、班次(DAY 白班 / MIDDLE 中班 / NIGHT 夜班)、本班总用电与历史同期基准用电、本班产量与单位产品标准电耗、用水量与用水基准、最大需量与合同约定需量、是否恒温车间及实测温度,以及燃气压力是否正常、能耗计量表是否在线、产线运行/停线状态。

规则侧只负责客观口径:用电、用水、需量三个同比/超合同比率以整数百分比承载,单耗评级把实际总用电与“产量 × 标准单耗”交叉相乘比较,超标准 20% 判 WASTE、超 5% 判 ATTENTION、否则 NORMAL;恒温车间温度超出 18-26℃ 工艺区间判异常。编排侧按巡检逻辑聚合多信号:计量表离线直接 CRITICAL 短路;停线却仍有超过基准 10% 的耗电判设备空转或偷漏电;用电偏离 30%/15%、需量超合同 10%、用水偏离 100%/50% 分别升级紧急或预警;运行产线出现 WASTE 且产量为 0 时按“能源完全空耗”紧急告警;燃气压力与恒温温度异常各叠加一条预警。

决策结果由 WorkshopEnergyAlert 承载:四级告警级别、用电同比偏离率、需量超合同率、用水同比偏离率、单耗评级,以及 signals 异常信号清单——每条信号都是可直接派单的处置文案(排查高耗能设备、错峰、巡线查漏、检查空调系统等)。多路信号只升不降,最终级别取所有命中信号的最高级。

  • 预警阈值随设备、合同、季节变化:用电同比紧急线、需量合同值、恒温工艺区间因车间和供能合同而异,硬编码进采集程序后每次调整都要协调 IT 发版。
  • 多路能媒信号互相独立又要统一分级:电、水、需量、温度、燃气分属不同计量回路,阈值散落在各子系统,缺少一个能把多信号聚合成单一告警级别的决策层。
  • 特殊工况短路逻辑复杂:计量表离线、产线停线空转、有能耗无产量等场景必须一票升级,普通评分分支很难保证这类信号不被低分稀释。
  • 预警结果难审计、难派单:只有级别没有原因,运维班组拿到告警还要回查曲线;需要级别与命中原因、处置建议同时输出。
  • 老旧能源管理系统停留在 JDK 8:厂区服务器与信创配套环境难以部署重量级 IoT 规则平台,需要可嵌入、无额外进程的轻量方案。

本案例采用 JQuick-Java 标准六件套:入参模型、决策结果模型、Rules 规则接口、XML 规则脚本、Service 编排服务、JUnit 测试,全部代码在 Oracle JDK 1.8 下实测通过。

层次 职责 对应文件
领域模型 承载班次能耗工况要素与多信号预警决策 WorkshopEnergyRecord.java、WorkshopEnergyAlert.java
规则层 电/水/需量偏离率、单耗交叉相乘评级、恒温区间判定,阈值外置 WorkshopEnergyRules.java、workshop-energy.xml
编排层 计量离线短路、多信号级别只升不降、信号文案组装 WorkshopEnergyAlertService.java
测试 6 组班次能耗真实形态 WorkshopEnergyAlertServiceTest.java
规则方法 口径 分值/输出
powerDeviationPercent (本班用电 - 基准用电)× 100 / 基准,整数除法承载百分比 同比偏离率(%,整数)
efficiencyLevel 标准总耗 = 产量 × 单位标准电耗;实际 ×100 大于标准 ×120 判 WASTE,大于标准 ×105 判 ATTENTION,否则 NORMAL WASTE/ATTENTION/NORMAL
demandExcessPercent (最大需量 - 合同需量)× 100 / 合同需量 超合同率(%,整数,可为负)
waterDeviationPercent (本班用水 - 用水基准)× 100 / 基准 同比偏离率(%,整数)
temperatureAbnormal 仅恒温室车间判定:温度低于 18℃ 或高于 26℃ 返回 true;非恒温室车间恒为 false boolean

将两个模型类放入 src/test/java/com/github/paohaijiao/demo/model/,规则接口放入 demo/,Service 放入 demo/service/,XML 脚本放入 src/test/resources/demo/,测试类放入 demo/,包名保持源码原样。工程根目录执行 mvn test -Dtest=WorkshopEnergyAlertServiceTest 即可复现全部 6 个用例。

package com.github.paohaijiao.demo.model;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* 车间班次能耗与工况要素
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class WorkshopEnergyRecord {
/**
* 车间编号
*/
private String workshopId;
/**
* 车间名称
*/
private String workshopName;
/**
* 班次:DAY 白班 / MIDDLE 中班 / NIGHT 夜班
*/
private String shift;
/**
* 本班总用电(kWh)
*/
private int totalKwh;
/**
* 历史同期基准用电(kWh)
*/
private int baselineKwh;
/**
* 本班产量
*/
private int outputQty;
/**
* 单位产品标准电耗(kWh/件)
*/
private int standardKwhPerUnit;
/**
* 本班用水量(吨)
*/
private int waterUsageTon;
/**
* 用水基准(吨)
*/
private int waterBaselineTon;
/**
* 本班最大需量(kW)
*/
private int maxDemandKw;
/**
* 合同约定需量(kW)
*/
private int demandContractKw;
/**
* 是否恒温车间
*/
private boolean tempControlled;
/**
* 车间温度(℃)
*/
private int temperature;
/**
* 燃气压力是否正常
*/
private boolean gasPressureOk;
/**
* 能耗计量表是否在线
*/
private boolean meterOnline;
/**
* 产线状态:RUNNING 运行 / STOPPED 停线
*/
private String productionLineStatus;
}
package com.github.paohaijiao.demo.model;
import lombok.Data;
import java.util.ArrayList;
import java.util.List;
/**
* 车间能耗预警决策
*/
@Data
public class WorkshopEnergyAlert {
/**
* 车间编号
*/
private String workshopId;
/**
* 告警级别:CRITICAL 紧急 / WARNING 预警 / INFO 提示 / NORMAL 正常
*/
private String alertLevel;
/**
* 用电同比偏离率(整数百分比)
*/
private int powerDeviationPercent;
/**
* 最大需量超合同率(整数百分比)
*/
private int demandExcessPercent;
/**
* 用水同比偏离率(整数百分比)
*/
private int waterDeviationPercent;
/**
* 单耗评级:WASTE 浪费 / ATTENTION 关注 / NORMAL 正常
*/
private String efficiencyLevel;
/**
* 异常信号与处置提示
*/
private List<String> signals = new ArrayList<String>();
public void addSignal(String signal) {
this.signals.add(signal);
}
}
package com.github.paohaijiao.demo;
import com.github.paohaijiao.xml.param.Param;
/**
* 场景 32:车间能耗偏离率、单耗评级与温度异常规则
*/
public interface WorkshopEnergyRules {
/**
* 用电同比偏离率(%)
*/
int powerDeviationPercent(@Param("total") int total,
@Param("baseline") int baseline);
/**
* 单位产品电耗评级(交叉相乘:total*100 对 标准耗电*阈值)
*/
String efficiencyLevel(@Param("total") int total,
@Param("output") int output,
@Param("standardPerUnit") int standardPerUnit);
/**
* 需量超合同率(%)
*/
int demandExcessPercent(@Param("maxDemand") int maxDemand,
@Param("contract") int contract);
/**
* 用水同比偏离率(%)
*/
int waterDeviationPercent(@Param("usage") int usage,
@Param("baseline") int baseline);
/**
* 恒温车间温度是否超出 18-26℃ 工艺区间
*/
boolean temperatureAbnormal(@Param("controlled") boolean controlled,
@Param("temperature") int temperature);
}
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE javas PUBLIC "-//PAOHAIJIAO//DTD API JAVA 1.0//EN"
"classpath:paohaijiao/dtd/Jquick-java.dtd">
<javas namespace="com.github.paohaijiao.demo.WorkshopEnergyRules">
<java name="powerDeviationPercent" returnClass="int">
<![CDATA[
int def powerDeviationPercent(int:total, int:baseline) {
return (total - baseline) * 100 / baseline;
}
]]>
</java>
<java name="efficiencyLevel" returnClass="java.lang.String">
<![CDATA[
str def efficiencyLevel(int:total, int:output, int:standardPerUnit) {
int standardTotal = output * standardPerUnit;
if (total * 100 > standardTotal * 120) {
return "WASTE";
} else if (total * 100 > standardTotal * 105) {
return "ATTENTION";
} else {
return "NORMAL";
}
}
]]>
</java>
<java name="demandExcessPercent" returnClass="int">
<![CDATA[
int def demandExcessPercent(int:maxDemand, int:contract) {
return (maxDemand - contract) * 100 / contract;
}
]]>
</java>
<java name="waterDeviationPercent" returnClass="int">
<![CDATA[
int def waterDeviationPercent(int:usage, int:baseline) {
return (usage - baseline) * 100 / baseline;
}
]]>
</java>
<java name="temperatureAbnormal" returnClass="boolean">
<![CDATA[
boolean def temperatureAbnormal(boolean:controlled, int:temperature) {
if (controlled) {
if (temperature >= 18) {
if (temperature <= 26) {
return false;
} else {
return true;
}
} else {
return true;
}
} else {
return false;
}
}
]]>
</java>
</javas>
package com.github.paohaijiao.demo.service;
import com.github.paohaijiao.demo.WorkshopEnergyRules;
import com.github.paohaijiao.demo.model.WorkshopEnergyAlert;
import com.github.paohaijiao.demo.model.WorkshopEnergyRecord;
/**
* 场景 32:车间能耗多信号聚合预警
*/
public class WorkshopEnergyAlertService {
private static final int LEVEL_NORMAL = 0;
private static final int LEVEL_INFO = 1;
private static final int LEVEL_WARNING = 2;
private static final int LEVEL_CRITICAL = 3;
private final WorkshopEnergyRules rules;
public WorkshopEnergyAlertService(WorkshopEnergyRules rules) {
this.rules = rules;
}
public WorkshopEnergyAlert evaluate(WorkshopEnergyRecord r) {
WorkshopEnergyAlert alert = new WorkshopEnergyAlert();
alert.setWorkshopId(r.getWorkshopId());
alert.setEfficiencyLevel("NORMAL");
if (!r.isMeterOnline()) {
alert.setAlertLevel("CRITICAL");
alert.addSignal("能耗计量表离线,数据采集中断,须立即检修计量回路");
return alert;
}
int level = LEVEL_NORMAL;
if ("STOPPED".equals(r.getProductionLineStatus())
&& r.getTotalKwh() > r.getBaselineKwh() * 0.1) {
level = upgrade(level, LEVEL_CRITICAL);
alert.addSignal("产线已停线但仍有 " + r.getTotalKwh()
+ "kWh 用电(超基准 10%),疑似设备空转或偷漏电");
}
int powerDev = rules.powerDeviationPercent(r.getTotalKwh(), r.getBaselineKwh());
alert.setPowerDeviationPercent(powerDev);
if (powerDev >= 30) {
level = upgrade(level, LEVEL_CRITICAL);
alert.addSignal("用电同比偏高 " + powerDev + "%,超过 30% 紧急线,须立即排查高耗能设备");
} else if (powerDev >= 15) {
level = upgrade(level, LEVEL_WARNING);
alert.addSignal("用电同比偏高 " + powerDev + "%,关注排产与设备能效");
}
if ("RUNNING".equals(r.getProductionLineStatus())) {
String efficiency = rules.efficiencyLevel(r.getTotalKwh(),
r.getOutputQty(), r.getStandardKwhPerUnit());
alert.setEfficiencyLevel(efficiency);
if ("WASTE".equals(efficiency)) {
if (r.getOutputQty() == 0) {
level = upgrade(level, LEVEL_CRITICAL);
alert.addSignal("本班有能耗但产量为 0,能源完全空耗");
} else {
level = upgrade(level, LEVEL_WARNING);
alert.addSignal("单位产品电耗超标准 20%,能效评级 WASTE");
}
} else if ("ATTENTION".equals(efficiency)) {
level = upgrade(level, LEVEL_INFO);
alert.addSignal("单位产品电耗超标准 5%,能效评级 ATTENTION");
}
}
int demandExcess = rules.demandExcessPercent(r.getMaxDemandKw(), r.getDemandContractKw());
alert.setDemandExcessPercent(demandExcess);
if (demandExcess >= 10) {
level = upgrade(level, LEVEL_CRITICAL);
alert.addSignal("最大需量超合同 " + demandExcess + "%,面临需量电费加价,立即错峰");
} else if (demandExcess > 0) {
level = upgrade(level, LEVEL_WARNING);
alert.addSignal("最大需量超合同 " + demandExcess + "%,接近加价线");
}
int waterDev = rules.waterDeviationPercent(r.getWaterUsageTon(), r.getWaterBaselineTon());
alert.setWaterDeviationPercent(waterDev);
if (waterDev >= 100) {
level = upgrade(level, LEVEL_CRITICAL);
alert.addSignal("用水同比偏高 " + waterDev + "%,疑似供水管网泄漏,立即巡线");
} else if (waterDev >= 50) {
level = upgrade(level, LEVEL_WARNING);
alert.addSignal("用水同比偏高 " + waterDev + "%,核查冷却水与清洗用水");
}
if (!r.isGasPressureOk()) {
level = upgrade(level, LEVEL_WARNING);
alert.addSignal("燃气压力异常,可能影响加热炉工艺稳定性");
}
if (rules.temperatureAbnormal(r.isTempControlled(), r.getTemperature())) {
level = upgrade(level, LEVEL_WARNING);
alert.addSignal("恒温车间实测 " + r.getTemperature()
+ "℃,超出 18-26℃ 工艺区间,检查空调系统");
}
alert.setAlertLevel(levelName(level));
return alert;
}
private int upgrade(int current, int candidate) {
return candidate > current ? candidate : current;
}
private String levelName(int level) {
if (level >= LEVEL_CRITICAL) {
return "CRITICAL";
}
if (level >= LEVEL_WARNING) {
return "WARNING";
}
if (level >= LEVEL_INFO) {
return "INFO";
}
return "NORMAL";
}
}
package com.github.paohaijiao.demo;
import com.github.paohaijiao.demo.model.WorkshopEnergyAlert;
import com.github.paohaijiao.demo.model.WorkshopEnergyRecord;
import com.github.paohaijiao.demo.service.WorkshopEnergyAlertService;
import com.github.paohaijiao.xml.JQuickJava;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
/**
* 场景 32:车间能耗异常判定与超标预警
* <p>
* 6 组班次形态:全要素正常、用电 18% 预警叠加温度燃气异常、用电 37%+单耗浪费+需量紧急、
* 停线空转紧急、用水泄漏紧急、计量离线紧急与需量 8% 预警。
*/
public class WorkshopEnergyAlertServiceTest {
private WorkshopEnergyAlertService service;
@Before
public void setUp() {
WorkshopEnergyRules rules = JQuickJava.create()
.createApi(WorkshopEnergyRules.class, "demo/workshop-energy.xml");
this.service = new WorkshopEnergyAlertService(rules);
}
/**
* 用电、单耗、需量、用水全部贴合基准 → NORMAL
*/
@Test
public void shouldStayNormalWhenAllMetricsOnBaseline() {
WorkshopEnergyRecord r = base("WS01");
r.setTotalKwh(800);
r.setBaselineKwh(800);
r.setOutputQty(100);
r.setWaterUsageTon(30);
r.setWaterBaselineTon(30);
WorkshopEnergyAlert a = service.evaluate(r);
Assert.assertEquals("NORMAL", a.getAlertLevel());
Assert.assertEquals(0, a.getPowerDeviationPercent());
Assert.assertEquals("NORMAL", a.getEfficiencyLevel());
Assert.assertEquals(0, a.getSignals().size());
}
/**
* 用电 +18%、恒温 28℃、燃气压力异常 → WARNING,三信号齐备
*/
@Test
public void shouldWarnOnPowerDeviationAndUtilityIssues() {
WorkshopEnergyRecord r = base("WS02");
r.setTotalKwh(950);
r.setBaselineKwh(800);
r.setOutputQty(120);
r.setTemperature(28);
r.setGasPressureOk(false);
WorkshopEnergyAlert a = service.evaluate(r);
Assert.assertEquals("WARNING", a.getAlertLevel());
Assert.assertEquals(18, a.getPowerDeviationPercent());
Assert.assertTrue(a.getSignals().contains("燃气压力异常,可能影响加热炉工艺稳定性"));
Assert.assertTrue(a.getSignals().get(0).contains("用电同比偏高 18%"));
Assert.assertTrue(a.getSignals().contains(
"恒温车间实测 28℃,超出 18-26℃ 工艺区间,检查空调系统"));
}
/**
* 用电 +37%、单耗浪费、需量超 20% → CRITICAL
*/
@Test
public void shouldRaiseCriticalForWasteAndDemandOverrun() {
WorkshopEnergyRecord r = base("WS03");
r.setTotalKwh(1100);
r.setBaselineKwh(800);
r.setOutputQty(100);
r.setMaxDemandKw(600);
r.setDemandContractKw(500);
WorkshopEnergyAlert a = service.evaluate(r);
Assert.assertEquals("CRITICAL", a.getAlertLevel());
Assert.assertEquals(37, a.getPowerDeviationPercent());
Assert.assertEquals(20, a.getDemandExcessPercent());
Assert.assertEquals("WASTE", a.getEfficiencyLevel());
Assert.assertTrue(a.getSignals().contains("单位产品电耗超标准 20%,能效评级 WASTE"));
Assert.assertTrue(a.getSignals().get(2).contains("最大需量超合同 20%"));
}
/**
* 停线仍耗电 200kWh(超基准 10%)→ 空转/偷漏电路径紧急告警
*/
@Test
public void shouldRaiseCriticalForStoppedLineEnergyUse() {
WorkshopEnergyRecord r = base("WS04");
r.setTotalKwh(200);
r.setBaselineKwh(800);
r.setProductionLineStatus("STOPPED");
r.setOutputQty(0);
WorkshopEnergyAlert a = service.evaluate(r);
Assert.assertEquals("CRITICAL", a.getAlertLevel());
Assert.assertEquals("NORMAL", a.getEfficiencyLevel());
Assert.assertTrue(a.getSignals().get(0).contains("产线已停线但仍有 200kWh 用电"));
}
/**
* 用水从 30 吨飙到 80 吨(+166%)→ 管网泄漏紧急告警
*/
@Test
public void shouldRaiseCriticalForWaterLeak() {
WorkshopEnergyRecord r = base("WS05");
r.setTotalKwh(800);
r.setBaselineKwh(800);
r.setOutputQty(100);
r.setWaterUsageTon(80);
r.setWaterBaselineTon(30);
WorkshopEnergyAlert a = service.evaluate(r);
Assert.assertEquals("CRITICAL", a.getAlertLevel());
Assert.assertEquals(166, a.getWaterDeviationPercent());
Assert.assertTrue(a.getSignals().get(0).contains("用水同比偏高 166%,疑似供水管网泄漏"));
}
/**
* 计量表离线直接 CRITICAL;最大需量超合同 8% 但未到 10% 紧急线 → WARNING
*/
@Test
public void shouldHandleMeterOfflineAndNearDemandLimit() {
WorkshopEnergyRecord offline = base("WS06");
offline.setMeterOnline(false);
WorkshopEnergyAlert a1 = service.evaluate(offline);
Assert.assertEquals("CRITICAL", a1.getAlertLevel());
Assert.assertTrue(a1.getSignals().get(0).contains("能耗计量表离线,数据采集中断"));
WorkshopEnergyRecord nearLimit = base("WS07");
nearLimit.setTotalKwh(800);
nearLimit.setBaselineKwh(800);
nearLimit.setOutputQty(100);
nearLimit.setMaxDemandKw(540);
nearLimit.setDemandContractKw(500);
WorkshopEnergyAlert a2 = service.evaluate(nearLimit);
Assert.assertEquals("WARNING", a2.getAlertLevel());
Assert.assertEquals(8, a2.getDemandExcessPercent());
Assert.assertTrue(a2.getSignals().contains("最大需量超合同 8%,接近加价线"));
}
private WorkshopEnergyRecord base(String id) {
WorkshopEnergyRecord r = new WorkshopEnergyRecord();
r.setWorkshopId(id);
r.setWorkshopName("一号车间");
r.setShift("DAY");
r.setTotalKwh(800);
r.setBaselineKwh(800);
r.setOutputQty(100);
r.setStandardKwhPerUnit(8);
r.setWaterUsageTon(30);
r.setWaterBaselineTon(30);
r.setMaxDemandKw(400);
r.setDemandContractKw(500);
r.setTempControlled(true);
r.setTemperature(22);
r.setGasPressureOk(true);
r.setMeterOnline(true);
r.setProductionLineStatus("RUNNING");
return r;
}
}

在工程根目录执行:

Terminal window
mvn test -Dtest=WorkshopEnergyAlertServiceTest

测试覆盖形态(6 个 @Test 方法,覆盖正常、多信号预警、紧急叠加、四类一票升级场景):

测试方法 业务形态 关键断言/期望决策
shouldStayNormalWhenAllMetricsOnBaseline WS01 白班:用电 800/基准 800、产 100 件、用水 30/30、需量 400 低于合同 500、22℃ NORMAL、用电偏离率 0、单耗 NORMAL、signals 0 条
shouldWarnOnPowerDeviationAndUtilityIssues WS02:用电 950(+18%)、产量 120 单耗正常、恒温 28℃、燃气压力异常 WARNING、偏离率 18;信号含“用电同比偏高 18%”(首条)、“燃气压力异常”、“恒温车间实测 28℃,超出 18-26℃ 工艺区间”
shouldRaiseCriticalForWasteAndDemandOverrun WS03:用电 1100(+37%)、产 100 件单耗 WASTE、需量 600/合同 500(+20%) CRITICAL、用电偏离 37、需量超 20、单耗 WASTE;含“单位产品电耗超标准 20%,能效评级 WASTE”,第 3 条信号含“最大需量超合同 20%”
shouldRaiseCriticalForStoppedLineEnergyUse WS04:产线 STOPPED 仍耗 200kWh(基准 800 的 25%,超 10% 线),产量 0 CRITICAL、单耗评级保持 NORMAL(停线不评单耗),首条信号含“产线已停线但仍有 200kWh 用电”
shouldRaiseCriticalForWaterLeak WS05:用水 30 吨飙到 80 吨,其余贴基准 CRITICAL、用水偏离率 166,首条信号含“用水同比偏高 166%,疑似供水管网泄漏”
shouldHandleMeterOfflineAndNearDemandLimit WS06 计量表离线;WS07 需量 540/合同 500(+8%) 离线批次 CRITICAL,首条含“能耗计量表离线,数据采集中断”;近限批次 WARNING、超合同率 8、含“最大需量超合同 8%,接近加价线”
  • 阈值 XML 外置、按车间热更新:30%/15% 用电线、10% 需量加价线、100%/50% 用水线、18-26℃ 恒温区间全部写在 workshop-energy.xml,更换车间或供能合同后换 XML/重建代理即可,不动能源采集主程序。
  • 一方法一口径,比率与评级可独立审计:五个规则方法分别只算电、单耗、需量、水、温度,决策结果同时回填三个整数比率与单耗评级,加上 signals 文案,告警成因完整可追溯。
  • Service 侧短路与只升不降聚合:计量离线一票 CRITICAL 直接返回;其余信号经 upgrade 取最高级,任一紧急信号都不会被其他正常指标稀释。
  • 交叉相乘 + 千分比整数承载比率:单耗评级用 total * 100 > standardTotal * 120 规避浮点比较;同比率用整数乘除直接产出 18、37、166 这样的整数百分比,JDK 8 下无浮点误差。
  • 产线状态上下文前置:停线时空耗判定与单耗评级都以 productionLineStatus 为闸,停线不再误评单耗、运行时才判 WASTE,避免工况错配产生误报。
  • ASM 代理零重型依赖:@Param 按名绑定、代理单例复用,可直接嵌入 JDK 8 的厂区能管系统与信创环境,无需独立规则服务进程。