From 6b99cc391064cb38c706979fafcecb9f50ea1587 Mon Sep 17 00:00:00 2001
From: yeg <1347747024@qq.com>
Date: 星期日, 11 十月 2026 02:07:44 +0800
Subject: [PATCH] 新威直连补上工步层与循环汇总(downloadStepLayer 实机验证后落地)

---
 battery-device-bts/src/test/java/com/battery/device/bts/NewareBtsXmlCodecTest.java |  106 +++++++++++++++++++++++++++++++++++++++++++++++++++++
 1 files changed, 106 insertions(+), 0 deletions(-)

diff --git a/battery-device-bts/src/test/java/com/battery/device/bts/NewareBtsXmlCodecTest.java b/battery-device-bts/src/test/java/com/battery/device/bts/NewareBtsXmlCodecTest.java
index 9f007be..5e30636 100644
--- a/battery-device-bts/src/test/java/com/battery/device/bts/NewareBtsXmlCodecTest.java
+++ b/battery-device-bts/src/test/java/com/battery/device/bts/NewareBtsXmlCodecTest.java
@@ -12,6 +12,7 @@
 import java.util.Set;
 import org.junit.jupiter.api.Test;
 import com.battery.device.api.TestDataObservationAdapter.CabinetRealtime;
+import com.battery.device.api.TestDataObservationAdapter.StepRecord;
 import com.battery.device.api.model.ChannelIdentity;
 import com.battery.device.api.model.DeviceWorkStatus;
 import com.battery.device.api.model.RealtimeSnapshot;
@@ -237,6 +238,111 @@
     }
 
     @Test
+    void buildsTheStepLayerRequestWithoutTheOptionalElements()
+    {
+        String request = codec.downloadStepLayerRequest(CHANNEL);
+
+        assertTrue(request.contains("<cmd>downloadStepLayer</cmd>"));
+        assertTrue(request.contains("devtype=\"25\" devid=\"81\" subdevid=\"1\" chlid=\"2\""));
+        assertTrue(request.contains("testid=\"0\""));
+        assertTrue(request.contains("dcir=\"0\""));
+    }
+
+    @Test
+    void parsesRealStepLayerWithNormalisedCycleAndDerivedStartTime()
+    {
+        // 现场实测原文(`26.117.165.166` 的 `25-81-1-1`,测试 316)。三处要点都在这个夹具里:
+        // cycleid 是 1 起要减一;只有 endatime,开始时间由 endatime − steptime 还原;
+        // stepindex 才是身份(stepid 在循环里会重复)。
+        String xml = "<?xml version=\"1.0\" encoding=\"UTF-8\"?><bts version=\"1.0\">"
+                + "<cmd>downloadStepLayer_resp</cmd>"
+                + "<downloadStepLayer devtype=\"25\" devid=\"81\" subdevid=\"1\" chlid=\"1\" testid=\"316\" />"
+                + "<list count=\"4\">"
+                + "<data startseqid=\"1\" endseqid=\"2\" stepindex=\"1\" stepid=\"1\" cycleid=\"1\" steptype=\"rest\""
+                + " steptime=\"23100\" endatime=\"2026-10-10 09:09:14\" startvolt=\"0.806611083984375\""
+                + " endvolt=\"0.80638037109375\" startcurr=\"0\" endcurr=\"0\" cap=\"0\" eng=\"0\" dcir=\"0\" />"
+                + "<data startseqid=\"3\" endseqid=\"304\" stepindex=\"2\" stepid=\"2\" cycleid=\"1\" steptype=\"cc\""
+                + " steptime=\"18000000\" endatime=\"2026-10-10 14:09:14\" startvolt=\"1.07542861328125\""
+                + " endvolt=\"1.972108203125\" startcurr=\"0.563903076171875\" endcurr=\"0.563332702636719\""
+                + " cap=\"2.81630277777778\" eng=\"5.58447055555556\" dcir=\"0\" />"
+                + "<data startseqid=\"305\" endseqid=\"307\" stepindex=\"3\" stepid=\"3\" cycleid=\"1\" steptype=\"rest\""
+                + " steptime=\"83600\" endatime=\"2026-10-10 14:10:38\" startvolt=\"1.7850955078125\""
+                + " endvolt=\"1.42794287109375\" startcurr=\"0\" endcurr=\"0\" cap=\"0\" eng=\"0\" dcir=\"0\" />"
+                + "<data startseqid=\"308\" endseqid=\"309\" stepindex=\"4\" stepid=\"4\" cycleid=\"1\" steptype=\"dc\""
+                + " steptime=\"27600\" endatime=\"2026-10-10 14:11:05\" startvolt=\"1.27452001953125\""
+                + " endvolt=\"0.49604326171875\" startcurr=\"-0.562971618652344\" endcurr=\"-0.5622275390625\""
+                + " cap=\"0.00431017035590278\" eng=\"0.00426197048611111\" dcir=\"0\" /></list></bts>";
+
+        List<StepRecord> steps = codec.parseStepLayer(xml, Instant.parse("2026-10-10T14:12:00Z"));
+
+        assertEquals(4, steps.size());
+        StepRecord first = steps.get(0);
+        assertEquals(0, first.cycleNo());
+        assertEquals(1, first.stepNo());
+        assertEquals("rest", first.stepType());
+        assertEquals("搁置", first.stepName());
+        assertEquals("2026-10-10T01:09:14Z", first.endedAt().toString());
+        // 开始时间 = 结束时间 − 23100 ms;与该测试首行 atime(09:08:51 设备本地)对得上。
+        assertEquals("2026-10-10T01:08:50.900Z", first.startedAt().toString());
+        assertEquals("neware:316:step:1", first.sourceRecordId());
+        assertEquals("Asia/Hong_Kong", first.timezoneId());
+        assertEquals("2026-10-10 09:09:14", first.deviceTimeRaw());
+
+        StepRecord charge = steps.get(1);
+        assertEquals("充电", charge.stepName());
+        assertEquals(2.81630277777778, charge.capacityAh(), 1e-12);
+        assertEquals(5.58447055555556, charge.energyWh(), 1e-12);
+        assertEquals(1.972108203125, charge.endVoltageV(), 1e-12);
+        assertEquals(0.563332702636719, charge.endCurrentA(), 1e-12);
+        assertEquals(3, charge.measurements().get("startSeqId").intValue());
+        assertEquals(304, charge.measurements().get("endSeqId").intValue());
+
+        StepRecord discharge = steps.get(3);
+        assertEquals("放电", discharge.stepName());
+        assertEquals(0.00431017035590278, discharge.capacityAh(), 1e-15);
+    }
+
+    @Test
+    void leavesStepCapacityEmptyWhenTheDirectionIsUnknown()
+    {
+        // 方向未知时不给单项容量能量:cap 是单一值,方向没确认就不知道它属于哪一侧(ADR-0069 §3)。
+        String xml = "<bts><cmd>downloadStepLayer_resp</cmd>"
+                + "<downloadStepLayer devtype=\"25\" devid=\"81\" subdevid=\"1\" chlid=\"1\" testid=\"9\" />"
+                + "<list count=\"1\"><data startseqid=\"1\" endseqid=\"9\" stepindex=\"1\" stepid=\"1\" cycleid=\"2\""
+                + " steptype=\"--\" steptime=\"60000\" endatime=\"2026-10-10 09:09:14\" startvolt=\"1\" endvolt=\"1\""
+                + " startcurr=\"0\" endcurr=\"0\" cap=\"1.5\" eng=\"2.5\" dcir=\"0\" /></list></bts>";
+
+        StepRecord step = codec.parseStepLayer(xml, Instant.EPOCH).get(0);
+
+        assertNull(step.stepName());
+        assertNull(step.capacityAh());
+        assertNull(step.energyWh());
+        assertEquals(1, step.cycleNo());
+        // 明细里保留的是设备原文事实(起始电压、首末序号),没有按方向分派的单项值。
+        assertEquals(1.0, step.measurements().get("startVoltage").doubleValue(), 1e-9);
+        assertEquals(9, step.measurements().get("endSeqId").intValue());
+    }
+
+    @Test
+    void reportsAnEmptyStepLayerInsteadOfFailing()
+    {
+        // 该通道的缓冲里没有测试时(实测 D 那台就是),应答是空列表而不是错误。
+        List<StepRecord> steps = codec.parseStepLayer(
+                "<bts><cmd>downloadStepLayer_resp</cmd><list count=\"0\" /></bts>", Instant.EPOCH);
+
+        assertTrue(steps.isEmpty());
+    }
+
+    @Test
+    void rejectsAStepLayerResponseToAnotherCommand()
+    {
+        BtsProtocolException exception = assertThrows(BtsProtocolException.class,
+                () -> codec.parseStepLayer("<bts><cmd>inquire_resp</cmd><list count=\"0\" /></bts>", Instant.EPOCH));
+
+        assertEquals(BtsProtocolException.Reason.UNEXPECTED_COMMAND, exception.reason());
+    }
+
+    @Test
     void keepsOnlyAttributesOutsideTheKnownFieldSetAsDynamicMeasurements()
     {
         // 字段集随客户端构建不同(8.0.0.484 有 barcode/auxtemp,8.0.0.530 没有,实测三种构建互不相同)。

--
Gitblit v1.7.1