From 691f882740b46c5354fb33404fef6efb370e05dc Mon Sep 17 00:00:00 2001
From: yeg <1347747024@qq.com>
Date: 星期五, 09 十月 2026 15:52:05 +0800
Subject: [PATCH] 实时行也带上判定出的方向,实时页的工步名称才有值
---
battery-business/src/test/java/com/battery/business/service/impl/PushIngestAdapterContractTest.java | 147 ++++++++++++++++++++++++++++++++++++++++++++++++-
1 files changed, 144 insertions(+), 3 deletions(-)
diff --git a/battery-business/src/test/java/com/battery/business/service/impl/PushIngestAdapterContractTest.java b/battery-business/src/test/java/com/battery/business/service/impl/PushIngestAdapterContractTest.java
index ec6c1be..3dd5d21 100644
--- a/battery-business/src/test/java/com/battery/business/service/impl/PushIngestAdapterContractTest.java
+++ b/battery-business/src/test/java/com/battery/business/service/impl/PushIngestAdapterContractTest.java
@@ -16,7 +16,9 @@
import org.junit.jupiter.api.Test;
import com.battery.device.api.DeviceAdapterException;
import com.battery.device.api.TestDataObservationAdapter.CabinetRealtime;
+import com.battery.device.api.TestDataObservationAdapter.CycleRecord;
import com.battery.device.api.TestDataObservationAdapter.SampleWatermark;
+import com.battery.device.api.TestDataObservationAdapter.StepRecord;
import com.battery.device.api.model.AdapterCapabilities;
import com.battery.device.api.model.ChannelIdentity;
import com.battery.device.api.model.DetectedChannel;
@@ -45,6 +47,37 @@
"Cycle_Index":1,"Voltage":1.2701,"eCurrent":0.5,"ChargeCapacity":0.121,"ChargeEnergy":0.31},
{"record_time":"134349698000000000","Data_Point":422,"Step_Time":600.0,"Step_Index":5,
"Cycle_Index":1,"Voltage":1.2702,"eCurrent":-0.4,"DischargeCapacity":0.05,"DischargeEnergy":0.1}]}
+ """;
+ /**
+ * 两个循环的采样行:循环 1 是充电、搁置、放电三段(都结束了),循环 2 的第一段还在进行。
+ * 容量列是工步累计:充电段从 0.10 涨到 0.12,放电段从 0.05 涨到 0.09。
+ */
+ private static final String TWO_CYCLES = """
+ {"kind":"SAMPLE","database":"DB_20260927_155209","table":"Ws26_IV_0","channel":26,"rows":[
+ {"record_time":"134349692000000000","Data_Point":420,"Step_Time":540.0,"Step_Index":1,"Cycle_Index":1,
+ "Voltage":1.20,"eCurrent":1.0,"ChargeCapacity":0.10,"ChargeEnergy":0.30},
+ {"record_time":"134349698000000000","Data_Point":421,"Step_Time":600.0,"Step_Index":1,"Cycle_Index":1,
+ "Voltage":1.25,"eCurrent":1.0,"ChargeCapacity":0.12,"ChargeEnergy":0.36},
+ {"record_time":"134349704000000000","Data_Point":422,"Step_Time":1200.0,"Step_Index":2,"Cycle_Index":1,
+ "Voltage":1.25,"eCurrent":0.0,"ChargeCapacity":0.12,"DischargeCapacity":0.0},
+ {"record_time":"134349764000000000","Data_Point":423,"Step_Time":1800.0,"Step_Index":3,"Cycle_Index":1,
+ "Voltage":1.24,"eCurrent":-1.0,"ChargeCapacity":0.12,"DischargeCapacity":0.05,"DischargeEnergy":0.15},
+ {"record_time":"134349824000000000","Data_Point":424,"Step_Time":3600.0,"Step_Index":3,"Cycle_Index":1,
+ "Voltage":1.20,"eCurrent":-1.0,"ChargeCapacity":0.12,"DischargeCapacity":0.09,"DischargeEnergy":0.27},
+ {"record_time":"134349830000000000","Data_Point":425,"Step_Time":60.0,"Step_Index":1,"Cycle_Index":2,
+ "Voltage":1.18,"eCurrent":2.0,"ChargeCapacity":0.01,"ChargeEnergy":0.02}]}
+ """;
+ /** 第一段的电流正负混用:方向判不出来,明细照留但不进循环的任何一侧。 */
+ private static final String MIXED_SIGN = """
+ {"kind":"SAMPLE","database":"DB_20260927_155209","table":"Ws26_IV_0","channel":26,"rows":[
+ {"record_time":"134349692000000000","Data_Point":420,"Step_Index":1,"Cycle_Index":1,
+ "Voltage":1.20,"eCurrent":1.0,"ChargeCapacity":0.10},
+ {"record_time":"134349698000000000","Data_Point":421,"Step_Index":1,"Cycle_Index":1,
+ "Voltage":1.20,"eCurrent":-1.0,"ChargeCapacity":0.10,"DischargeCapacity":0.02},
+ {"record_time":"134349704000000000","Data_Point":422,"Step_Index":2,"Cycle_Index":1,
+ "Voltage":1.25,"eCurrent":1.0,"ChargeCapacity":0.12,"ChargeEnergy":0.36},
+ {"record_time":"134349830000000000","Data_Point":425,"Step_Index":1,"Cycle_Index":2,
+ "Voltage":1.18,"eCurrent":2.0,"ChargeCapacity":0.01}]}
""";
private final FakeIngestLanding landing = new FakeIngestLanding();
@@ -124,6 +157,10 @@
assertEquals(5, latest.stepNo());
// 放电容量的取法按电流方向,与直连来源同一口径;来源行身份供幂等与冲突判定。
assertEquals(0.05, latest.capacityAh(), 1e-9);
+ // 实时行也带方向:类型与名称都按电流符号判定(该行电流为负 → 放电),
+ // 实时页的"工步名称"列用的就是它(用户对照甲方那一列「状态」)。
+ assertEquals("discharge", latest.stepType());
+ assertEquals("放电", latest.stepName());
assertEquals("push:DB_20260927_155209/Ws26_IV_0:422", latest.sourceRecordId());
// 实时快照:通道工作状态在读落地表路线下不可观测(ADR-0063)。
@@ -172,21 +209,125 @@
/** 未声明辅助测量列就不交出测量值:确认之前不按列名推断语义,也不写零占位。 */
@Test
- void collectorAndStepCycleObservationsStayEmptyWithoutDeclaredAuxiliaryMetrics()
+ void collectorObservationsStayEmptyWithoutDeclaredAuxiliaryMetrics()
{
ingest("{\"kind\":\"TOPOLOGY\",\"rows\":[{\"channel\":26}]}");
ingest(BATCH);
PushIngestAdapter adapter = adapter(NOW);
assertNull(adapter.latestCollector(ADAPTER, "collector-1"));
- assertTrue(adapter.stepRecords(channel()).isEmpty());
- assertTrue(adapter.cycleRecords(channel()).isEmpty());
// 落地表在本地,取数不产生厂商负载,所以按展示节奏取数;厂商写库节奏仍是 60 秒。
assertEquals(5000L, adapter.persistenceIntervalMillis(channel()));
+ // 工步与循环的重算跟着来源的写库节奏走,不跟实时行的展示节奏:源头最快 60 秒才产出一行。
+ assertEquals(60000L, adapter.aggregateIntervalMillis(channel()));
AdapterCapabilities capabilities = adapter.capabilities();
assertTrue(capabilities.discovery() && capabilities.realtimeObservation() && !capabilities.control());
}
+ /**
+ * 来源没有工步层,工步与循环由采样行分段推导(ADR-0067):已结束的段各交出一条工步记录,
+ * 取值来自该段末行,方向由该段自己的电流符号判定;最新一段仍在进行,不交出去。
+ */
+ @Test
+ void closedSampleSegmentsBecomeStepRecordsWhileTheNewestSegmentStaysUnpublished()
+ {
+ ingest(BATCH);
+ PushIngestAdapter adapter = adapter(NOW);
+
+ List<StepRecord> steps = adapter.stepRecords(channel());
+
+ // 三行落地行分成两段:(循环 1, 工步 4) 已结束,(循环 1, 工步 5) 还在进行。
+ assertEquals(1, steps.size());
+ StepRecord step = steps.get(0);
+ assertEquals(1, step.cycleNo());
+ assertEquals(4, step.stepNo());
+ // 方向取自该段电流符号:段内两行恒正即充电。
+ assertEquals("charge", step.stepType());
+ // 工步名称用同一套词(甲方软件那一列「状态」显示的就是充电/放电/搁置):该来源的方案里
+ // 只有「步_N」标号,这个名称是判定出的方向,不是设备给的名称。
+ assertEquals("充电", step.stepName());
+ // 容量与能量是工步累计,取该段末行的值;工步时间是工步内相对时间(秒)。
+ assertEquals(0.121, step.capacityAh(), 1e-9);
+ assertEquals(0.31, step.energyWh(), 1e-9);
+ assertEquals(600.0, step.measurements().get("stepDuration").doubleValue(), 1e-9);
+ assertEquals(1.26778, step.measurements().get("startVoltage").doubleValue(), 1e-9);
+ // 设备没给的列不写零占位:该段只有充电侧。
+ assertFalse(step.measurements().containsKey("dischargeCapacity"));
+ assertEquals(1.2701, step.endVoltageV(), 1e-9);
+ assertEquals(0.5, step.endCurrentA(), 1e-9);
+ assertEquals(Instant.parse("2026-09-27T07:53:20Z"), step.startedAt());
+ assertEquals(Instant.parse("2026-09-27T08:03:20Z"), step.endedAt());
+ // 接收时间取我们收到该段末行的时刻(与推式实时路径同一口径),设备原始时间照留。
+ assertEquals(NOW, step.serverReceivedAt());
+ assertEquals("134349698000000000", step.deviceTimeRaw());
+ // 来源行身份含来源库与段标识:同一次测试重算多少次都得到同一个身份。
+ assertEquals("push:DB_20260927_155209/Ws26_IV_0:step:1:4", step.sourceRecordId());
+ // 进行中的那一段所属的循环整体不交:半个循环不是事实。
+ assertTrue(adapter.cycleRecords(channel()).isEmpty());
+ }
+
+ /**
+ * 循环由已结束的工步按方向求和(搁置段两向都不增长,贡献 0),效率是放电容量与充电容量之比;
+ * 推导值带来源类别,界面与导出据此标注它是聚合出来的。
+ */
+ @Test
+ void cyclesAggregateClosedStepsByDirectionAndSkipTheRunningCycle()
+ {
+ ingest(TWO_CYCLES);
+ PushIngestAdapter adapter = adapter(NOW);
+
+ List<StepRecord> steps = adapter.stepRecords(channel());
+ List<CycleRecord> cycles = adapter.cycleRecords(channel());
+
+ // 循环 1 的三段都已结束(后面已经出现循环 2 的第一段);循环 2 的第一段仍在进行。
+ assertEquals(List.of("1:1:charge", "1:2:rest", "1:3:discharge"), steps.stream()
+ .map(step -> step.cycleNo() + ":" + step.stepNo() + ":" + step.stepType()).toList());
+ // 搁置段自身的累计是 0(该段什么都没充放),不是缺失。
+ assertEquals(0.0, steps.get(1).capacityAh(), 1e-9);
+ // 搁置段的名称同样是判定出来的。
+ assertEquals("搁置", steps.get(1).stepName());
+ assertEquals(0.0, steps.get(1).energyWh(), 1e-9);
+ assertEquals(List.of(1), cycles.stream().map(CycleRecord::cycleNo).toList());
+
+ CycleRecord cycle = cycles.get(0);
+ assertEquals(0.12, cycle.chargeCapacityAh(), 1e-9);
+ assertEquals(0.09, cycle.dischargeCapacityAh(), 1e-9);
+ assertEquals(0.36, cycle.chargeEnergyWh(), 1e-9);
+ assertEquals(0.27, cycle.dischargeEnergyWh(), 1e-9);
+ // 效率是百分数:0.09 / 0.12。
+ assertEquals(75.0, cycle.efficiency(), 1e-9);
+ assertEquals(CycleRecord.SOURCE_AGGREGATED, cycle.sourceKind());
+ assertEquals("push:DB_20260927_155209/Ws26_IV_0:cycle:1", cycle.sourceRecordId());
+ }
+
+ /** 方向判不出来时(段内电流正负混用)明细照留、单项容量留空,也不进循环的任何一侧。 */
+ @Test
+ void segmentsWithMixedCurrentSignsStayOutOfTheCycleAggregate()
+ {
+ ingest(MIXED_SIGN);
+ PushIngestAdapter adapter = adapter(NOW);
+
+ List<StepRecord> steps = adapter.stepRecords(channel());
+ List<CycleRecord> cycles = adapter.cycleRecords(channel());
+
+ StepRecord undecided = steps.get(0);
+ assertEquals(1, undecided.cycleNo());
+ assertEquals(1, undecided.stepNo());
+ // 方向不唯一时不猜:类型留空、单项容量与能量留空。
+ assertNull(undecided.stepType());
+ // 方向判不出来时名称也留空:不写一个没根据的名字。
+ assertNull(undecided.stepName());
+ assertNull(undecided.capacityAh());
+ assertNull(undecided.energyWh());
+ // 四组容量能量作为设备事实照原样保留,供核对。
+ assertEquals(0.02, undecided.measurements().get("dischargeCapacity").doubleValue(), 1e-9);
+
+ // 该段不进任何一侧:循环 1 的放电侧算出来是 0,而不是把这一段里的 0.02 算进去。
+ assertEquals(1, cycles.size());
+ assertEquals(0.12, cycles.get(0).chargeCapacityAh(), 1e-9);
+ assertEquals(0.0, cycles.get(0).dischargeCapacityAh(), 1e-9);
+ }
+
/** 环境量与量纲确认后落到配置:列名、指标编码、单位、采集器通道都由声明给出,代码里没有厂商列名。 */
@Test
void declaredAuxiliaryMetricIsProjectedWithItsOwnSourceIdentityValueUnitAndDeviceTime()
--
Gitblit v1.7.1