案例 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 |
规则方法拆分
Section titled “规则方法拆分”| 规则方法 | 口径 | 分值/输出 |
|---|---|---|
powerDeviationPercent |
(本班用电 - 基准用电)× 100 / 基准,整数除法承载百分比 | 同比偏离率(%,整数) |
efficiencyLevel |
标准总耗 = 产量 × 单位标准电耗;实际 ×100 大于标准 ×120 判 WASTE,大于标准 ×105 判 ATTENTION,否则 NORMAL | WASTE/ATTENTION/NORMAL |
demandExcessPercent |
(最大需量 - 合同需量)× 100 / 合同需量 | 超合同率(%,整数,可为负) |
waterDeviationPercent |
(本班用水 - 用水基准)× 100 / 基准 | 同比偏离率(%,整数) |
temperatureAbnormal |
仅恒温室车间判定:温度低于 18℃ 或高于 26℃ 返回 true;非恒温室车间恒为 false | boolean |
代码实现(完整可运行)
Section titled “代码实现(完整可运行)”将两个模型类放入 src/test/java/com/github/paohaijiao/demo/model/,规则接口放入 demo/,Service 放入 demo/service/,XML 脚本放入 src/test/resources/demo/,测试类放入 demo/,包名保持源码原样。工程根目录执行 mvn test -Dtest=WorkshopEnergyAlertServiceTest 即可复现全部 6 个用例。
WorkshopEnergyRecord.java
Section titled “WorkshopEnergyRecord.java”package com.github.paohaijiao.demo.model;
import lombok.AllArgsConstructor;import lombok.Data;import lombok.NoArgsConstructor;
/** * 车间班次能耗与工况要素 */@Data@NoArgsConstructor@AllArgsConstructorpublic 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;}WorkshopEnergyAlert.java
Section titled “WorkshopEnergyAlert.java”package com.github.paohaijiao.demo.model;
import lombok.Data;
import java.util.ArrayList;import java.util.List;
/** * 车间能耗预警决策 */@Datapublic 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); }}WorkshopEnergyRules.java
Section titled “WorkshopEnergyRules.java”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);}workshop-energy.xml
Section titled “workshop-energy.xml”<?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>WorkshopEnergyAlertService.java
Section titled “WorkshopEnergyAlertService.java”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"; }}WorkshopEnergyAlertServiceTest.java
Section titled “WorkshopEnergyAlertServiceTest.java”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; }}在工程根目录执行:
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 的厂区能管系统与信创环境,无需独立规则服务进程。