yeg
5 天以前 c4bb5bc9314ae5731ecae347c5aa11116ff1445e
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
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
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);
 
    /**
     * 返回该通道的工步统计。来源有工步层时读设备工步层并原样交出;来源没有工步层时由适配器
     * 从采样段推导(ADR-0067),此时记录里的方向是推导出的方向名称(见 {@link StepRecord})。
     * 未结束的段不交出,也不推导来源根本没有的工步层。
     */
    List<StepRecord> stepRecords(ChannelIdentity channel);
 
    /**
     * 返回该来源的循环统计。设备直接提供循环汇总行时原样返回;设备只有工步层时由适配器按循环聚合,
     * 并用 {@link CycleRecord#sourceKind()} 标注为聚合来源,使调用方与界面能区分推导值与设备事实。
     * 不补齐来源未提供的循环,也不跨循环借值。
     */
    List<CycleRecord> cycleRecords(ChannelIdentity channel);
 
    /** 返回适配器定义的实时持久化槽位间隔。 */
    long persistenceIntervalMillis(ChannelIdentity channel);
 
    /**
     * 返回工步与循环这些汇总记录的读取间隔。默认与实时持久化间隔一致;来源的汇总产出节奏与
     * 数据行节奏不同的实现覆盖它——推式来源的落地表在本地(实时行按展示节奏取),
     * 而汇总最快也要等来源写下下一行才可能变化,按展示节奏重算只是空跑。
     */
    default long aggregateIntervalMillis(ChannelIdentity channel) { return persistenceIntervalMillis(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>来源**没有**工步类型列时(推式来源的厂商库里既没有工步层也没有循环汇总表,ADR-0067),
     * 适配器交出的是按已核对列语义判定出的方向名称({@code charge} / {@code discharge} / {@code rest}),
     * 判定不出来时留空;此时该工步的其余一切仍是设备采样行上的原文事实。
     *
     * <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;
        }
    }
}