yeg
2026-09-29 b78295e8da96c3e5e76befeef9582ce5273dbb74
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
package com.battery.device.api;
 
import java.math.BigDecimal;
import java.time.Instant;
import java.util.List;
import com.battery.device.api.model.ChannelIdentity;
import com.battery.device.api.model.Measurement;
 
/** 供业务持久化使用的厂商无关测试数据观测扩展。 */
public interface TestDataObservationAdapter extends DeviceObservationAdapter
{
    /** 返回指定检测柜通道最近一次实时观测;没有成功观测时返回空值。 */
    CabinetRealtime latestCabinet(ChannelIdentity channel);
 
    /**
     * 返回采集器独立实时快照;不得通过历史下载补采。
     * 采集器测量按"同一来源的辅助测量"表达(ADR-0064),所以必须指名是哪个来源实例,
     * 否则多实例下无法确定该向哪台机器请求。
     */
    CollectorRealtime latestCollector(String adapterCode, String collectorCode);
 
    /** 返回设备直接提供的工步统计,不从实时采样推导。 */
    List<StepRecord> stepRecords(ChannelIdentity channel);
 
    /**
     * 返回该来源的循环统计。设备直接提供循环汇总行时原样返回;设备只有工步层时由适配器按循环聚合,
     * 并用 {@link CycleRecord#sourceKind()} 标注为聚合来源,使调用方与界面能区分推导值与设备事实。
     * 不补齐来源未提供的循环,也不跨循环借值。
     */
    List<CycleRecord> cycleRecords(ChannelIdentity channel);
 
    /** 返回适配器定义的实时持久化槽位间隔。 */
    long persistenceIntervalMillis(ChannelIdentity channel);
 
    /** 返回设备计划方案;设备未提供时保持为空,不推导计划。 */
    default TestPlan testPlan(ChannelIdentity channel) { return null; }
 
    /**
     * 读取通道当前的接收水位。水位是来源里已存在的最新一行(设备绝对时间 + 协议序号),
     * 用于把"开始接收"和"继续接收"切在这一时刻之后:水位之前的数据行即使仍在来源里也不进本测试。
     *
     * <p>返回空值表示该来源不按设备数据行接收(例如由系统轮询产生槽位的来源),此时不启用逐行接收。
     * 设备时间为空的水位表示这一刻来源里没有任何可用行,之后出现的行全部属于本次接收。
     * 来源不可达时必须抛出异常,不能返回空值——空值会被误当作"来源不按行接收"。
     */
    default SampleWatermark currentWatermark(ChannelIdentity channel) { return null; }
 
    /**
     * 返回严格晚于水位的设备数据行,按设备时间与协议序号升序。
     * 同一绝对秒内的多行各自成立,调用方不得按秒合并。
     * 水位为空表示调用方尚未取得水位,此时返回空列表:不把来源里的设备历史并入本次接收。
     */
    default List<CabinetRealtime> samplesSince(ChannelIdentity channel, SampleWatermark watermark) { return List.of(); }
 
    /** 接收水位:设备绝对时间与协议序号的二元组;序号在同一厂商测试内有效。 */
    record SampleWatermark(Instant deviceTime, long sequenceId) { }
 
    record TestPlan(int currentCycleNo, int currentStepNo, int plannedCycleCount, List<PlanRow> rows) { }
    record PlanRow(String rowType, int stepNo, Integer cyclePosition, String stepName, double durationSeconds,
            Double voltageV, String executionStatus, Integer cycleCount, Long recordCount) { }
 
    /**
     * 柜端成功观测;相对时长使用设备提供的秒数,缺失不按采样时间推算。
     * {@code sourceRecordId} 是来源行的稳定身份(如厂商测试编号加协议序号),
     * 供幂等与冲突判定使用;来源不提供时保持为空,调用方退化为按协议序号识别。
     */
    record CabinetRealtime(long sequenceId, Instant deviceTime, double currentA, double voltageV,
            double capacityAh, double energyWh, int cycleNo, int stepNo, String stepName, String stepType,
            Instant serverReceivedAt, String deviceTimeRaw, String timezoneId, Long relativeTimeSeconds,
            String sourceRecordId)
    {
        /** 兼容不携带来源行身份的既有适配器。 */
        public CabinetRealtime(long sequenceId, Instant deviceTime, double currentA, double voltageV,
                double capacityAh, double energyWh, int cycleNo, int stepNo, String stepName, String stepType,
                Instant serverReceivedAt, String deviceTimeRaw, String timezoneId, Long relativeTimeSeconds)
        {
            this(sequenceId, deviceTime, currentA, voltageV, capacityAh, energyWh, cycleNo, stepNo, stepName,
                    stepType, serverReceivedAt, deviceTimeRaw, timezoneId, relativeTimeSeconds, null);
        }
    }
 
    record CollectorRealtime(String collectorCode, boolean online, int sampleIntervalMs,
            List<CollectorMeasurement> measurements, String timezoneId)
    {
        public CollectorRealtime { measurements = measurements == null ? List.of() : List.copyOf(measurements); }
    }
 
    record CollectorMeasurement(String channelCode, long sequenceId, Instant deviceTime, String deviceTimeRaw,
            Instant serverReceivedAt, String metricCode, BigDecimal value, String unit) { }
 
    /**
     * 一条设备工步层记录。{@code stepType} 保留设备给出的原始类型(新威为数字枚举),
     * 方向由调用方按该类型解释,不由适配器按电流符号推断。
     *
     * <p>{@code endVoltageV} 与 {@code endCurrentA} 是该工步结束时的设备读数。
     * {@code capacityAh} 与 {@code energyWh} 是该工步自身累计的单项值,只在方向已确认时给出;
     * 方向未知时保持为空,四个方向的容量能量仍在 {@code measurements} 中作为设备事实保留。
     */
    record StepRecord(String sourceRecordId, int cycleNo, int stepNo, String stepName, String stepType,
            Instant startedAt, Instant endedAt, double endVoltageV, double endCurrentA, Double capacityAh,
            Double energyWh, String deviceTimeRaw, String timezoneId, Instant serverReceivedAt,
            java.util.Map<String, BigDecimal> measurements) { }
 
    /**
     * 一条循环统计。{@code sourceRecordId} 是来源行的稳定身份,供幂等与冲突判定使用。
     *
     * <p>设备没有循环汇总行时,循环统计由工步层按循环聚合得出;{@code sourceKind} 记录这个来源事实,
     * 使界面与导出能明确标注"这个数是聚合的还是设备给的",取值只能是下面三个常量之一。
     *
     * <p>{@code efficiency} 为单位为百分比的充放电效率。该循环没有可用的充电容量时保持为空:
     * 未知比率不用零代替,零值只表示设备确实给出零。
     */
    record CycleRecord(String sourceRecordId, int cycleNo, Instant startedAt, Instant endedAt,
            double chargeCapacityAh, double dischargeCapacityAh, double chargeEnergyWh,
            double dischargeEnergyWh, Double efficiency, String deviceTimeRaw, String timezoneId,
            Instant serverReceivedAt, String sourceKind)
    {
        /** 来源类别:设备直接提供。 */
        public static final String SOURCE_DEVICE = "DEVICE";
        /** 来源类别:来源没有循环汇总行,由工步层按循环聚合得出。 */
        public static final String SOURCE_AGGREGATED = "AGGREGATED";
        /** 来源类别:授权文件导入。 */
        public static final String SOURCE_IMPORTED = "IMPORTED";
 
        public CycleRecord
        {
            // 未标注一律按设备事实:引入聚合之前写进库的循环记录都来自设备或文件。
            sourceKind = sourceKind == null || sourceKind.isBlank() ? SOURCE_DEVICE : sourceKind;
        }
    }
}