| | |
| | | import org.junit.jupiter.api.Test; |
| | | import com.battery.device.api.DeviceAdapterException; |
| | | 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; |
| | |
| | | "Cycle_Index":1,"Voltage":1.2701,"eCurrent":0.5,"ChargeCapacity":0.121,"ChargeEnergy":0.31}, |
| | | {"record_time":"134349698000000000","Data_Point":422,"Step_Time":600.0,"Step_Index":5, |
| | | "Cycle_Index":1,"Voltage":1.2702,"eCurrent":-0.4,"DischargeCapacity":0.05,"DischargeEnergy":0.1}]} |
| | | """; |
| | | /** |
| | | * 两个循环的采样行:循环 1 是充电、搁置、放电三段(都结束了),循环 2 的第一段还在进行。 |
| | | * 容量列是工步累计:充电段从 0.10 涨到 0.12,放电段从 0.05 涨到 0.09。 |
| | | */ |
| | | private static final String TWO_CYCLES = """ |
| | | {"kind":"SAMPLE","database":"DB_20260927_155209","table":"Ws26_IV_0","channel":26,"rows":[ |
| | | {"record_time":"134349692000000000","Data_Point":420,"Step_Time":540.0,"Step_Index":1,"Cycle_Index":1, |
| | | "Voltage":1.20,"eCurrent":1.0,"ChargeCapacity":0.10,"ChargeEnergy":0.30}, |
| | | {"record_time":"134349698000000000","Data_Point":421,"Step_Time":600.0,"Step_Index":1,"Cycle_Index":1, |
| | | "Voltage":1.25,"eCurrent":1.0,"ChargeCapacity":0.12,"ChargeEnergy":0.36}, |
| | | {"record_time":"134349704000000000","Data_Point":422,"Step_Time":1200.0,"Step_Index":2,"Cycle_Index":1, |
| | | "Voltage":1.25,"eCurrent":0.0,"ChargeCapacity":0.12,"DischargeCapacity":0.0}, |
| | | {"record_time":"134349764000000000","Data_Point":423,"Step_Time":1800.0,"Step_Index":3,"Cycle_Index":1, |
| | | "Voltage":1.24,"eCurrent":-1.0,"ChargeCapacity":0.12,"DischargeCapacity":0.05,"DischargeEnergy":0.15}, |
| | | {"record_time":"134349824000000000","Data_Point":424,"Step_Time":3600.0,"Step_Index":3,"Cycle_Index":1, |
| | | "Voltage":1.20,"eCurrent":-1.0,"ChargeCapacity":0.12,"DischargeCapacity":0.09,"DischargeEnergy":0.27}, |
| | | {"record_time":"134349830000000000","Data_Point":425,"Step_Time":60.0,"Step_Index":1,"Cycle_Index":2, |
| | | "Voltage":1.18,"eCurrent":2.0,"ChargeCapacity":0.01,"ChargeEnergy":0.02}]} |
| | | """; |
| | | /** 第一段的电流正负混用:方向判不出来,明细照留但不进循环的任何一侧。 */ |
| | | private static final String MIXED_SIGN = """ |
| | | {"kind":"SAMPLE","database":"DB_20260927_155209","table":"Ws26_IV_0","channel":26,"rows":[ |
| | | {"record_time":"134349692000000000","Data_Point":420,"Step_Index":1,"Cycle_Index":1, |
| | | "Voltage":1.20,"eCurrent":1.0,"ChargeCapacity":0.10}, |
| | | {"record_time":"134349698000000000","Data_Point":421,"Step_Index":1,"Cycle_Index":1, |
| | | "Voltage":1.20,"eCurrent":-1.0,"ChargeCapacity":0.10,"DischargeCapacity":0.02}, |
| | | {"record_time":"134349704000000000","Data_Point":422,"Step_Index":2,"Cycle_Index":1, |
| | | "Voltage":1.25,"eCurrent":1.0,"ChargeCapacity":0.12,"ChargeEnergy":0.36}, |
| | | {"record_time":"134349830000000000","Data_Point":425,"Step_Index":1,"Cycle_Index":2, |
| | | "Voltage":1.18,"eCurrent":2.0,"ChargeCapacity":0.01}]} |
| | | """; |
| | | |
| | | private final FakeIngestLanding landing = new FakeIngestLanding(); |
| | |
| | | |
| | | /** 未声明辅助测量列就不交出测量值:确认之前不按列名推断语义,也不写零占位。 */ |
| | | @Test |
| | | void collectorAndStepCycleObservationsStayEmptyWithoutDeclaredAuxiliaryMetrics() |
| | | void collectorObservationsStayEmptyWithoutDeclaredAuxiliaryMetrics() |
| | | { |
| | | ingest("{\"kind\":\"TOPOLOGY\",\"rows\":[{\"channel\":26}]}"); |
| | | ingest(BATCH); |
| | | PushIngestAdapter adapter = adapter(NOW); |
| | | |
| | | assertNull(adapter.latestCollector(ADAPTER, "collector-1")); |
| | | assertTrue(adapter.stepRecords(channel()).isEmpty()); |
| | | assertTrue(adapter.cycleRecords(channel()).isEmpty()); |
| | | // 落地表在本地,取数不产生厂商负载,所以按展示节奏取数;厂商写库节奏仍是 60 秒。 |
| | | assertEquals(5000L, adapter.persistenceIntervalMillis(channel())); |
| | | // 工步与循环的重算跟着来源的写库节奏走,不跟实时行的展示节奏:源头最快 60 秒才产出一行。 |
| | | assertEquals(60000L, adapter.aggregateIntervalMillis(channel())); |
| | | AdapterCapabilities capabilities = adapter.capabilities(); |
| | | assertTrue(capabilities.discovery() && capabilities.realtimeObservation() && !capabilities.control()); |
| | | } |
| | | |
| | | /** |
| | | * 来源没有工步层,工步与循环由采样行分段推导(ADR-0067):已结束的段各交出一条工步记录, |
| | | * 取值来自该段末行,方向由该段自己的电流符号判定;最新一段仍在进行,不交出去。 |
| | | */ |
| | | @Test |
| | | void closedSampleSegmentsBecomeStepRecordsWhileTheNewestSegmentStaysUnpublished() |
| | | { |
| | | ingest(BATCH); |
| | | PushIngestAdapter adapter = adapter(NOW); |
| | | |
| | | List<StepRecord> steps = adapter.stepRecords(channel()); |
| | | |
| | | // 三行落地行分成两段:(循环 1, 工步 4) 已结束,(循环 1, 工步 5) 还在进行。 |
| | | assertEquals(1, steps.size()); |
| | | StepRecord step = steps.get(0); |
| | | assertEquals(1, step.cycleNo()); |
| | | assertEquals(4, step.stepNo()); |
| | | // 方向取自该段电流符号:段内两行恒正即充电。 |
| | | assertEquals("charge", step.stepType()); |
| | | // 容量与能量是工步累计,取该段末行的值;工步时间是工步内相对时间(秒)。 |
| | | assertEquals(0.121, step.capacityAh(), 1e-9); |
| | | assertEquals(0.31, step.energyWh(), 1e-9); |
| | | assertEquals(600.0, step.measurements().get("stepDuration").doubleValue(), 1e-9); |
| | | assertEquals(1.26778, step.measurements().get("startVoltage").doubleValue(), 1e-9); |
| | | // 设备没给的列不写零占位:该段只有充电侧。 |
| | | assertFalse(step.measurements().containsKey("dischargeCapacity")); |
| | | assertEquals(1.2701, step.endVoltageV(), 1e-9); |
| | | assertEquals(0.5, step.endCurrentA(), 1e-9); |
| | | assertEquals(Instant.parse("2026-09-27T07:53:20Z"), step.startedAt()); |
| | | assertEquals(Instant.parse("2026-09-27T08:03:20Z"), step.endedAt()); |
| | | // 接收时间取我们收到该段末行的时刻(与推式实时路径同一口径),设备原始时间照留。 |
| | | assertEquals(NOW, step.serverReceivedAt()); |
| | | assertEquals("134349698000000000", step.deviceTimeRaw()); |
| | | // 来源行身份含来源库与段标识:同一次测试重算多少次都得到同一个身份。 |
| | | assertEquals("push:DB_20260927_155209/Ws26_IV_0:step:1:4", step.sourceRecordId()); |
| | | // 进行中的那一段所属的循环整体不交:半个循环不是事实。 |
| | | assertTrue(adapter.cycleRecords(channel()).isEmpty()); |
| | | } |
| | | |
| | | /** |
| | | * 循环由已结束的工步按方向求和(搁置段两向都不增长,贡献 0),效率是放电容量与充电容量之比; |
| | | * 推导值带来源类别,界面与导出据此标注它是聚合出来的。 |
| | | */ |
| | | @Test |
| | | void cyclesAggregateClosedStepsByDirectionAndSkipTheRunningCycle() |
| | | { |
| | | ingest(TWO_CYCLES); |
| | | PushIngestAdapter adapter = adapter(NOW); |
| | | |
| | | List<StepRecord> steps = adapter.stepRecords(channel()); |
| | | List<CycleRecord> cycles = adapter.cycleRecords(channel()); |
| | | |
| | | // 循环 1 的三段都已结束(后面已经出现循环 2 的第一段);循环 2 的第一段仍在进行。 |
| | | assertEquals(List.of("1:1:charge", "1:2:rest", "1:3:discharge"), steps.stream() |
| | | .map(step -> step.cycleNo() + ":" + step.stepNo() + ":" + step.stepType()).toList()); |
| | | // 搁置段自身的累计是 0(该段什么都没充放),不是缺失。 |
| | | assertEquals(0.0, steps.get(1).capacityAh(), 1e-9); |
| | | assertEquals(0.0, steps.get(1).energyWh(), 1e-9); |
| | | assertEquals(List.of(1), cycles.stream().map(CycleRecord::cycleNo).toList()); |
| | | |
| | | CycleRecord cycle = cycles.get(0); |
| | | assertEquals(0.12, cycle.chargeCapacityAh(), 1e-9); |
| | | assertEquals(0.09, cycle.dischargeCapacityAh(), 1e-9); |
| | | assertEquals(0.36, cycle.chargeEnergyWh(), 1e-9); |
| | | assertEquals(0.27, cycle.dischargeEnergyWh(), 1e-9); |
| | | // 效率是百分数:0.09 / 0.12。 |
| | | assertEquals(75.0, cycle.efficiency(), 1e-9); |
| | | assertEquals(CycleRecord.SOURCE_AGGREGATED, cycle.sourceKind()); |
| | | assertEquals("push:DB_20260927_155209/Ws26_IV_0:cycle:1", cycle.sourceRecordId()); |
| | | } |
| | | |
| | | /** 方向判不出来时(段内电流正负混用)明细照留、单项容量留空,也不进循环的任何一侧。 */ |
| | | @Test |
| | | void segmentsWithMixedCurrentSignsStayOutOfTheCycleAggregate() |
| | | { |
| | | ingest(MIXED_SIGN); |
| | | PushIngestAdapter adapter = adapter(NOW); |
| | | |
| | | List<StepRecord> steps = adapter.stepRecords(channel()); |
| | | List<CycleRecord> cycles = adapter.cycleRecords(channel()); |
| | | |
| | | StepRecord undecided = steps.get(0); |
| | | assertEquals(1, undecided.cycleNo()); |
| | | assertEquals(1, undecided.stepNo()); |
| | | // 方向不唯一时不猜:类型留空、单项容量与能量留空。 |
| | | assertNull(undecided.stepType()); |
| | | assertNull(undecided.capacityAh()); |
| | | assertNull(undecided.energyWh()); |
| | | // 四组容量能量作为设备事实照原样保留,供核对。 |
| | | assertEquals(0.02, undecided.measurements().get("dischargeCapacity").doubleValue(), 1e-9); |
| | | |
| | | // 该段不进任何一侧:循环 1 的放电侧算出来是 0,而不是把这一段里的 0.02 算进去。 |
| | | assertEquals(1, cycles.size()); |
| | | assertEquals(0.12, cycles.get(0).chargeCapacityAh(), 1e-9); |
| | | assertEquals(0.0, cycles.get(0).dischargeCapacityAh(), 1e-9); |
| | | } |
| | | |
| | | /** 环境量与量纲确认后落到配置:列名、指标编码、单位、采集器通道都由声明给出,代码里没有厂商列名。 */ |
| | | @Test |
| | | void declaredAuxiliaryMetricIsProjectedWithItsOwnSourceIdentityValueUnitAndDeviceTime() |