搁置步的容量能量不再当该步自身值交出(与厂商库逐项核对后修)

核对方式是拿同一个测试把接口工步层对到厂商自己的库表上(C 那台两条路都能读):
21 段工步序列(含两处"回到上一步"的中断分段)、逐项结束时间、结束电流、
容量能量、循环号全部一致,逐点行数在三处(接口 inquiredf / test.rec_num / cap_step 末行 seq_id)
都是 3494。界面显示的就是库里的数,所以这比截图核更硬。

核对抓到一个真缺陷:接口的 cap/eng 在搁置步上不是该步自身累计,而是跨步过渡值
(实测 +0.290039804444444,恰等于上一步充电容量;另一处 −0.178807573333333 取负)。
原实现按"方向已确认就给"的口径,搁置方向也把它当该步容量,会写出"搁置步容量 0.29 Ah"
这种不成立的事实。现在只有充放电方向交出单项容量能量,搁置步留空;原文仍留在明细里供核对。
该步自身累计为 0 这件事设备没给,所以留空而不是补零。

battery-device-bts 71 项通过;后端全量通过、BUILD SUCCESS。
2个文件已修改
52 ■■■■■ 已修改文件
battery-device-bts/src/main/java/com/battery/device/bts/BtsXmlCodec.java 30 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
battery-device-bts/src/test/java/com/battery/device/bts/NewareBtsXmlCodecTest.java 22 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
battery-device-bts/src/main/java/com/battery/device/bts/BtsXmlCodec.java
@@ -231,7 +231,8 @@
     * </ul>
     *
     * <p>方向的判定沿用既有解析器;{@code cap}/{@code eng} 在接口上是**单一值**(不分充放),
     * 只在方向已确认时给出,方向未知时留空——不按数值大小猜它属于哪一侧(ADR-0069 §3)。
     * 只在**充放电方向**给出。搁置步留空不是保守,是实测结论:接口对搁置步给的是跨步过渡值
     * (实测等于上一步充电容量、甚至取负),照抄会写出不成立的事实(ADR-0069 §3)。
     *
     * @param xml        应答原文
     * @param receivedAt 服务器接收时间
@@ -261,20 +262,31 @@
            int cycleNo = requiredInteger(data, "cycleid") - 1;
            BigDecimal capacity = optionalDecimal(data, "cap");
            BigDecimal energy = optionalDecimal(data, "eng");
            boolean directionKnown = direction != StepDirectionResolver.Direction.UNKNOWN;
            // 单项容量能量只在**充放电方向**给出:接口的 cap/eng 是单一值,实测它对搁置步给的是
            // 跨步过渡值(C 那台实测搁置步给 +0.290039804444444,等于上一步充电的容量;还有一次取负
            // −0.178807573333333),并不是该步自身累计。照抄会写出"搁置步容量 0.29 Ah"这种不成立的事实;
            // 而该步自身累计为 0 这件事设备没给,所以留空而不是补零(ADR-0069 §3)。
            boolean singleValued = direction == StepDirectionResolver.Direction.CHARGE
                    || direction == StepDirectionResolver.Direction.DISCHARGE;
            steps.add(new StepRecord(testId.isBlank() ? null : "neware:" + testId + ":step:" + stepIndex, cycleNo,
                    requiredInteger(data, "stepid"), direction.displayName(), stepType,
                    durationMillis == null ? endedAt : endedAt.minusMillis(durationMillis),
                    endedAt, doubleValue(data, "endvolt"), doubleValue(data, "endcurr"),
                    directionKnown && capacity != null ? capacity.doubleValue() : null,
                    directionKnown && energy != null ? energy.doubleValue() : null,
                    rawTime, zoneId, receivedAt, stepMeasurements(data, durationMillis)));
                    singleValued && capacity != null ? capacity.doubleValue() : null,
                    singleValued && energy != null ? energy.doubleValue() : null,
                    rawTime, zoneId, receivedAt, stepMeasurements(data, durationMillis, capacity, energy)));
        }
        return List.copyOf(steps);
    }
    /** 工步的设备事实明细;映射表按已有键名口径命名,值全部来自应答原文。 */
    private static Map<String, BigDecimal> stepMeasurements(Element data, Long durationMillis)
    /**
     * 工步的设备事实明细;映射表按已有键名口径命名,值全部来自应答原文。
     *
     * <p>应答给的 {@code cap}/{@code eng} 在搁置步是跨步过渡值(见上),所以明细里也按方向归属:
     * 充放步留 {@code cap}/{@code eng} 原文,其余方向不留——明细与单项列不得互相矛盾。
     */
    private static Map<String, BigDecimal> stepMeasurements(Element data, Long durationMillis,
            BigDecimal capacity, BigDecimal energy)
    {
        Map<String, BigDecimal> measurements = new LinkedHashMap<>();
        if (durationMillis != null) measurements.put("stepDuration", BigDecimal.valueOf(durationMillis).movePointLeft(3));
@@ -283,6 +295,10 @@
        put(measurements, data, "startVoltage", "startvolt");
        put(measurements, data, "startCurrent", "startcurr");
        put(measurements, data, "dcir", "dcir");
        put(measurements, data, "endVoltage", "endvolt");
        put(measurements, data, "endCurrent", "endcurr");
        if (capacity != null) measurements.put("capacity", capacity);
        if (energy != null) measurements.put("energy", energy);
        return Map.copyOf(measurements);
    }
battery-device-bts/src/test/java/com/battery/device/bts/NewareBtsXmlCodecTest.java
@@ -238,6 +238,28 @@
    }
    @Test
    void leavesTheRestStepCarryOverValueOutOfTheSingleValuedColumns()
    {
        // 现场实测(C 那台):接口对搁置步给的是**跨步过渡值**,不是该步自身累计——
        // 这一步给 +0.290039804444444(等于上一步充电的容量),另一处还给过 −0.178807573333333。
        // 写成"搁置步容量 0.29 Ah"就是不成立的事实,所以搁置步的单项容量能量留空。
        String xml = "<bts><cmd>downloadStepLayer_resp</cmd>"
                + "<downloadStepLayer devtype=\"24\" devid=\"98\" subdevid=\"1\" chlid=\"1\" testid=\"2818576812\" />"
                + "<list count=\"1\"><data startseqid=\"1624\" endseqid=\"1624\" stepindex=\"9\" stepid=\"7\" cycleid=\"1\""
                + " steptype=\"rest\" steptime=\"59007400\" endatime=\"2026-08-29 08:43:06\" startvolt=\"1.2977\""
                + " endvolt=\"1.2977\" startcurr=\"0\" endcurr=\"0\" cap=\"0.290039804444444\""
                + " eng=\"0.468939271111111\" dcir=\"0\" /></list></bts>";
        StepRecord step = codec.parseStepLayer(xml, Instant.EPOCH).get(0);
        assertEquals("搁置", step.stepName());
        assertNull(step.capacityAh());
        assertNull(step.energyWh());
        // 原文仍留在明细里供核对,只是不当"该步自身累计"用。
        assertEquals(0.290039804444444, step.measurements().get("capacity").doubleValue(), 1e-15);
    }
    @Test
    void buildsTheStepLayerRequestWithoutTheOptionalElements()
    {
        String request = codec.downloadStepLayerRequest(CHANNEL);