From 9c93934b70ccfca081102944a47f3baafad3c218 Mon Sep 17 00:00:00 2001
From: yeg <1347747024@qq.com>
Date: 星期一, 28 九月 2026 16:43:57 +0800
Subject: [PATCH] 实现新威 7.6 的工步与循环数据取数
---
battery-device-neware/src/test/java/com/battery/device/neware/NewareMysqlAdapterContractTest.java | 194 ++++++++++++++++++++++++++++++++++++++++++++++++
1 files changed, 194 insertions(+), 0 deletions(-)
diff --git a/battery-device-neware/src/test/java/com/battery/device/neware/NewareMysqlAdapterContractTest.java b/battery-device-neware/src/test/java/com/battery/device/neware/NewareMysqlAdapterContractTest.java
index 14673a6..529c1ae 100644
--- a/battery-device-neware/src/test/java/com/battery/device/neware/NewareMysqlAdapterContractTest.java
+++ b/battery-device-neware/src/test/java/com/battery/device/neware/NewareMysqlAdapterContractTest.java
@@ -13,7 +13,9 @@
import java.util.Set;
import org.junit.jupiter.api.Test;
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;
@@ -205,6 +207,198 @@
.allMatch(item -> ADAPTER_CODE.equals(item.identity().adapterCode())));
}
+ // ---------------------------------------------------------------- 工步层与循环聚合
+
+ /**
+ * 工步记录来自设备工步层:每工步一条,取该工步的结束行;四组容量能量与中值电压放在扩展测量里,
+ * 中值电压由设备字段承载,不用峰值或谷值替代。
+ */
+ @Test
+ void stepRecordsComeFromTheDeviceStepLayerAndCarryAllFourCapacityGroups()
+ {
+ List<StepRecord> steps = adapter(stepLayerDatabase()).stepRecords(CHANNEL);
+ // 只有进入行的工步仍在进行中,不出现:设备还没写后续工步的行。
+ assertEquals(9, steps.size());
+ assertTrue(steps.stream().noneMatch(step -> step.cycleNo() == 2));
+
+ StepRecord rest = step(steps, 0, 1);
+ assertEquals("4", rest.stepType());
+ assertNull(rest.stepName());
+ assertEquals(Instant.parse("2026-09-28T01:00:00Z"), rest.startedAt());
+ assertEquals(Instant.parse("2026-09-28T01:00:38Z"), rest.endedAt());
+ assertEquals(1.3588, rest.endVoltageV(), 1e-9);
+ // 工步记录同时带结束时的电流读数。
+ assertEquals(0d, rest.endCurrentA(), 1e-9);
+ assertEquals(-1.1889, step(steps, 1, 11).endCurrentA(), 1e-9);
+ // 工步时间是设备给的 test_time(毫秒 → 秒),不是按采样时间推算的。
+ assertEquals(0, new java.math.BigDecimal("38.800").compareTo(rest.measurements().get("stepDuration")));
+ assertEquals(0, new java.math.BigDecimal("1.358800000000").compareTo(rest.measurements().get("startVoltage")));
+
+ StepRecord charge = step(steps, 0, 4);
+ assertEquals("1", charge.stepType());
+ // 单项容量只在方向已确认时给出:充电工步取充电侧。
+ assertEquals(119277040d / 36000000d, charge.capacityAh(), 1e-9);
+ assertEquals(195220576d / 36000000d, charge.energyWh(), 1e-9);
+ // 充电中值电压直接取设备工步层的 middle_vol,设备给零就保留零。
+ assertEquals(0, java.math.BigDecimal.ZERO.compareTo(charge.measurements().get("chargeMedianVoltage")));
+ assertNull(charge.measurements().get("dischargeMedianVoltage"));
+
+ StepRecord discharge = step(steps, 0, 6);
+ assertEquals("2", discharge.stepType());
+ assertEquals(68462032d / 36000000d, discharge.capacityAh(), 1e-9);
+ assertTrue(discharge.measurements().containsKey("dischargeMedianVoltage"));
+ // 四组容量能量都是设备事实:每一组都保留,不只保留方向匹配的那一组。
+ for (String field : List.of("chargeCapacity", "dischargeCapacity", "chargeEnergy", "dischargeEnergy"))
+ assertTrue(charge.measurements().containsKey(field) && discharge.measurements().containsKey(field));
+ assertEquals("neware:440:step:0:4", charge.sourceRecordId());
+ assertEquals("Asia/Hong_Kong", charge.timezoneId());
+ assertEquals(charge.endedAt(), charge.serverReceivedAt());
+ }
+
+ /** 未知工步类型保留明细并保持未知:不按电流符号,也不按容量出现在哪一组去猜方向。 */
+ @Test
+ void unknownStepTypeKeepsItsDetailAndStaysOutOfTheCycleAggregate()
+ {
+ List<StepRecord> steps = adapter(stepLayerDatabase()).stepRecords(CHANNEL);
+ StepRecord unknown = step(steps, 1, 3);
+ assertEquals("7", unknown.stepType());
+ // 单项容量留空:方向未确认时不给出可归属的单值,但四组明细照原样保留供核对。
+ assertNull(unknown.capacityAh());
+ assertNull(unknown.energyWh());
+ assertEquals(500000000d / 36000000d, unknown.measurements().get("chargeCapacity").doubleValue(), 1e-9);
+
+ CycleRecord cycle = cycle(adapter(stepLayerDatabase()).cycleRecords(CHANNEL), 1);
+ // 未知方向的那一步容量没有进任何一侧:本循环的充放电仍只由已知方向的工步构成。
+ assertEquals((119267664d + 85510512d) / 36000000d, cycle.chargeCapacityAh(), 1e-9);
+ assertEquals((67587520d + 47559464d) / 36000000d, cycle.dischargeCapacityAh(), 1e-9);
+ }
+
+ /**
+ * 循环统计由工步层按循环聚合并标注为聚合来源:同一循环内同方向的工步容量求和
+ * (实测该化成协议 cycle 1 有两个充电工步与两个放电工步),效率是放电容量除以充电容量。
+ */
+ @Test
+ void cycleRecordsAreAggregatedFromTheStepLayerAndMarkedAsAggregated()
+ {
+ List<CycleRecord> cycles = adapter(stepLayerDatabase()).cycleRecords(CHANNEL);
+ // 仍在进行的循环整体不出现:工步行还没写完,聚合出来的是半个循环。
+ assertEquals(2, cycles.size());
+
+ CycleRecord first = cycle(cycles, 0);
+ assertEquals(119277040d / 36000000d, first.chargeCapacityAh(), 1e-9);
+ // 两个放电工步求和:16219380 + 68462032。
+ assertEquals(84681412d / 36000000d, first.dischargeCapacityAh(), 1e-9);
+ assertEquals(195220576d / 36000000d, first.chargeEnergyWh(), 1e-9);
+ assertEquals(40000000d / 36000000d, first.dischargeEnergyWh(), 1e-9);
+ assertEquals(70.9956, first.efficiency(), 1e-3);
+ // 来源类别是聚合:界面与导出据此标注"这个数是推导出来的"。
+ assertEquals(CycleRecord.SOURCE_AGGREGATED, first.sourceKind());
+ assertEquals("neware:440:cycle:0", first.sourceRecordId());
+ // 循环的起止取该循环首末工步的设备时间,服务器接收时间同样取设备时刻,暂停与继续据此留白。
+ assertEquals(Instant.parse("2026-09-28T01:00:00Z"), first.startedAt());
+ assertEquals(Instant.parse("2026-09-28T14:04:47Z"), first.endedAt());
+ assertEquals(first.endedAt(), first.serverReceivedAt());
+
+ CycleRecord second = cycle(cycles, 1);
+ assertEquals((119267664d + 85510512d) / 36000000d, second.chargeCapacityAh(), 1e-9);
+ assertEquals((67587520d + 47559464d) / 36000000d, second.dischargeCapacityAh(), 1e-9);
+ assertEquals(56.2301, second.efficiency(), 1e-3);
+ }
+
+ /** 循环没有可用充电容量时效率留空:未知比率不写成零。 */
+ @Test
+ void cycleEfficiencyStaysEmptyWhenThereIsNoChargeCapacity()
+ {
+ FakeVendorDatabase database = new FakeVendorDatabase()
+ .registeredChannel(DEV_UID, 1, 5)
+ .test(new FakeVendorDatabase.TestRow(DEV_UID, 1, 5, 440L, "2026-09-28 09:00:00", ACTIVE_TABLE, null))
+ .capStep(new FakeVendorDatabase.CapStepRow(440L, DEV_UID, 1, 5, 1L, 9, 1, "2", 3600000L,
+ 13000, -5000, 0, 0, 50000000, 9000000, 0, "2026-09-28 12:00:00"))
+ .capStep(new FakeVendorDatabase.CapStepRow(440L, DEV_UID, 1, 5, 2L, 10, 1, "4", 60000L,
+ 13000, 0, 0, 0, 0, 0, 0, "2026-09-28 12:01:00"));
+ List<CycleRecord> cycles = adapter(database)
+ .cycleRecords(new ChannelIdentity(ADAPTER_CODE, 21, 36, 1, 5));
+ assertEquals(1, cycles.size());
+ assertEquals(0d, cycles.get(0).chargeCapacityAh(), 1e-9);
+ assertNull(cycles.get(0).efficiency());
+ }
+
+ /** 没有测试记录、没有工步行或通道属于别实例时,工步与循环都保持为空而不报错。 */
+ @Test
+ void stepAndCycleRecordsStayEmptyWithoutAStepLayerOrTest()
+ {
+ NewareMysqlAdapter withoutTest = adapter(new FakeVendorDatabase().registeredChannel(DEV_UID, 1, 2));
+ assertTrue(withoutTest.stepRecords(CHANNEL).isEmpty());
+ assertTrue(withoutTest.cycleRecords(CHANNEL).isEmpty());
+ assertTrue(adapter(activeDeviceDatabase()).stepRecords(CHANNEL).isEmpty());
+ org.junit.jupiter.api.Assertions.assertThrows(RuntimeException.class,
+ () -> adapter(activeDeviceDatabase()).cycleRecords(new ChannelIdentity("other-source", 21, 36, 1, 2)));
+ }
+
+ private static StepRecord step(List<StepRecord> steps, int cycleNo, int stepNo)
+ {
+ return steps.stream().filter(item -> item.cycleNo() == cycleNo && item.stepNo() == stepNo)
+ .findFirst().orElseThrow();
+ }
+
+ private static CycleRecord cycle(List<CycleRecord> cycles, int cycleNo)
+ {
+ return cycles.stream().filter(item -> item.cycleNo() == cycleNo).findFirst().orElseThrow();
+ }
+
+ /**
+ * 工步层假厂商库:cycle 0 走完搁置、放电、充电、放电,cycle 1 有两个充电工步、两个放电工步
+ * 与一个未知类型工步;末组(cycle 2 的 step 9)只有进入行——它仍在进行中。
+ */
+ private FakeVendorDatabase stepLayerDatabase()
+ {
+ return new FakeVendorDatabase()
+ .registeredChannel(DEV_UID, 1, 2)
+ .test(new FakeVendorDatabase.TestRow(DEV_UID, 1, 2, 440L, "2026-09-28 09:00:00",
+ ACTIVE_TABLE, null))
+ // cycle 0:搁置、放电、充电、放电;结束行承载该工步累计容量与能量。
+ .capStep(step(1L, 0, 1, "4", 0L, 13588, 0, 0, 0, 0, 0, "2026-09-28 09:00:00"))
+ .capStep(step(2L, 0, 1, "4", 38800L, 13588, 0, 0, 0, 0, 0, "2026-09-28 09:00:38"))
+ .capStep(step(3L, 0, 2, "2", 0L, 13222, -4735, 0, 0, 0, 0, "2026-09-28 09:00:38"))
+ .capStep(step(4L, 0, 2, "2", 3425000L, 9998, -4736, 0, 0, 16219380, 10000000,
+ "2026-09-28 10:04:37"))
+ .capStep(step(5L, 0, 4, "1", 0L, 13507, 4732, 0, 0, 0, 0, "2026-09-28 11:03:51"))
+ .capStep(step(6L, 0, 4, "1", 25200000L, 17085, 4733, 119277040, 195220576, 0, 0,
+ "2026-09-28 18:03:51"))
+ .capStep(step(7L, 0, 6, "2", 0L, 14574, -4735, 0, 0, 0, 0, "2026-09-28 18:03:51"))
+ .capStep(step(8L, 0, 6, "2", 14455700L, 9998, -4736, 0, 0, 68462032, 30000000,
+ "2026-09-28 22:04:47"))
+ // cycle 1:两个充电工步 + 两个放电工步 + 一个未知类型工步。
+ .capStep(step(9L, 1, 1, "1", 0L, 13507, 4732, 0, 0, 0, 0, "2026-09-28 22:04:47"))
+ .capStep(step(10L, 1, 1, "1", 25200000L, 17085, 4733, 119267664, 196020320, 0, 0,
+ "2026-09-29 05:04:47"))
+ .capStep(step(11L, 1, 2, "2", 0L, 14574, -4735, 0, 0, 0, 0, "2026-09-29 05:04:47"))
+ .capStep(step(12L, 1, 2, "2", 14271500L, 9998, -4736, 0, 0, 67587520, 86258072,
+ "2026-09-29 09:02:38"))
+ // 未知类型 7:充电组有值,但方向未确认,所以不进聚合、单项容量留空。
+ .capStep(step(13L, 1, 3, "7", 0L, 13000, 500, 500000000, 900000000, 0, 0, "2026-09-29 09:02:39"))
+ .capStep(step(14L, 1, 3, "7", 600000L, 13200, 600, 500000000, 900000000, 0, 0,
+ "2026-09-29 09:12:39"))
+ .capStep(step(15L, 1, 9, "1", 0L, 13092, 11860, 0, 0, 0, 0, "2026-09-29 09:12:40"))
+ .capStep(step(16L, 1, 9, "1", 7200000L, 18176, 11875, 85510512, 145614320, 0, 0,
+ "2026-09-29 11:12:40"))
+ .capStep(step(17L, 1, 11, "2", 0L, 14313, -11878, 0, 0, 0, 0, "2026-09-29 11:12:41"))
+ .capStep(step(18L, 1, 11, "2", 4001000L, 9998, -11889, 0, 0, 47559464, 58816460,
+ "2026-09-29 12:19:21"))
+ // 仍在进行的工步:只有进入行。
+ .capStep(step(19L, 2, 9, "1", 0L, 13774, 11871, 0, 0, 0, 0, "2026-09-29 12:19:22"));
+ }
+
+ /** 工步层一行的简写:本用例只用本通道的数据,中值电压按现场实测给出零。 */
+ private static FakeVendorDatabase.CapStepRow step(long seqId, int cycle, int stepId, String stepType,
+ long testTimeMillis, int voltage, int current, int capacityCharge, int energyCharge,
+ int capacityDischarge, int energyDischarge, String testAtime)
+ {
+ return new FakeVendorDatabase.CapStepRow(440L, DEV_UID, 1, 2, seqId, cycle, stepId, stepType,
+ testTimeMillis, voltage, current, capacityCharge, energyCharge, capacityDischarge, energyDischarge,
+ 0, testAtime);
+ }
+
/** 凭据只用现场取得的服务端只读账号;已知的全权限账号不进入配置。 */
@Test
void credentialsFallBackToTheReadOnlyAccountAndRefuseFullPrivilegeAccounts()
--
Gitblit v1.7.1