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
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
package com.battery.device.bts;
 
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import java.time.Instant;
import java.time.ZoneId;
import java.util.List;
import java.util.Map;
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;
 
/**
 * 新威接口的编解码器按**现场实测应答原文**验证。
 *
 * <p>这些夹具不是照协议文档写的,是从真实上位机(8.0.0.530)的管道应答里抄下来的原文,
 * 因此它们同时是"实现必须吸收的实测差异"的回归防线:
 * 小数秒的 {@code totaltime}、厂商拼错的 {@code donwload_resp}、五段式 {@code dev} 身份。
 */
class NewareBtsXmlCodecTest
{
    private static final String ADAPTER = "neware-f";
    private static final ChannelIdentity CHANNEL = new ChannelIdentity(ADAPTER, 25, 81, 1, 2);
    private final BtsXmlCodec codec = new BtsXmlCodec(ADAPTER, ZoneId.of("Asia/Hong_Kong"));
 
    @Test
    void parsesRealRealtimeSnapshotWithFractionalSecondDurations()
    {
        String xml = "<?xml version=\"1.0\" encoding=\"UTF-8\"?><bts version=\"1.0\"><cmd>inquire_resp</cmd>"
                + "<list count=\"1\"><inquire dev=\"25-81-1-2-0\" cycle_id=\"1\" step_id=\"6\" step_type=\"cc\""
                + " workstatus=\"working\" current=\"0.6853\" voltage=\"1.7337\" capacity=\"5.3894\""
                + " energy=\"9.3408\" totaltime=\"37737.6\" relativetime=\"28311.8\" open_or_close=\"0\""
                + " log_code=\"0\" /></list></bts>";
 
        RealtimeSnapshot snapshot = codec.parseSnapshots(xml, Instant.parse("2026-10-10T15:50:52Z"))
                .get(CHANNEL);
 
        assertEquals(DeviceWorkStatus.WORKING, snapshot.workStatus());
        assertEquals(Integer.valueOf(1), snapshot.cycleNo());
        assertEquals(Integer.valueOf(6), snapshot.stepNo());
        assertEquals("cc", snapshot.stepType());
        // 实测给的是小数秒;早期按整数解析会在这里判成非法取值,整次观测失败。
        assertEquals(Long.valueOf(37738L), snapshot.totalTimeSeconds());
        assertEquals(Long.valueOf(28312L), snapshot.relativeTimeSeconds());
        assertEquals("0", snapshot.logCode());
        // 五段式 dev(末段是辅助序号):身份取前四段,后段忽略。
        assertEquals(CHANNEL, snapshot.channel());
    }
 
    @Test
    void parsesDataFileStateFromRealInquiredfResponse()
    {
        String xml = "<?xml version=\"1.0\" encoding=\"UTF-8\"?><bts version=\"1.0\"><cmd>inquiredf_resp</cmd>"
                + "<list count=\"1\"><chl devtype=\"25\" devid=\"81\" subdevid=\"1\" chlid=\"2\" testid=\"316\""
                + " count=\"637\">false</chl></list></bts>";
 
        BtsXmlCodec.DataFileState state = codec.parseDataFileState(xml);
 
        assertEquals(316L, state.testId());
        assertEquals(637L, state.count());
        // 厂商自报"未传完":这段数据不得当完整事实(ADR-0068 未决 5)。
        assertFalse(state.complete());
    }
 
    @Test
    void parsesRealDownloadedPointsWithAbsoluteTimeAndFullPrecision()
    {
        String xml = "<?xml version=\"1.0\" encoding=\"UTF-8\"?><bts version=\"1.0\"><cmd>donwload_resp</cmd>"
                + "<download devtype=\"25\" devid=\"81\" subdevid=\"1\" chlid=\"2\" testid=\"316\" startpos=\"527\""
                + " count=\"3\" auxid=\"0\" /><list count=\"3\">"
                + "<data seqid=\"527\" stepid=\"6\" cycleid=\"0\" steptype=\"cc\" testtime=\"21660000\""
                + " atime=\"2026-10-10 23:49:28\" volt=\"1.750143\" curr=\"0.6852889\""
                + " cap=\"4.12315481465658\" eng=\"7.12849144299129\" />"
                + "<data seqid=\"528\" stepid=\"6\" cycleid=\"0\" steptype=\"cc\" testtime=\"21720000\""
                + " atime=\"2026-10-10 23:50:28\" volt=\"1.751076\" curr=\"0.6852991\""
                + " cap=\"4.13457583333333\" eng=\"7.14849111111111\" />"
                + "<data seqid=\"529\" stepid=\"6\" cycleid=\"0\" steptype=\"cc\" testtime=\"21780000\""
                + " atime=\"2026-10-10 23:51:28\" volt=\"1.750529\" curr=\"0.6852911\""
                + " cap=\"4.14599735229492\" eng=\"7.16848481020665\" /></list></bts>";
 
        List<CabinetRealtime> rows = codec.parseDownload(xml, Instant.parse("2026-10-10T15:51:30Z"));
 
        assertEquals(3, rows.size());
        CabinetRealtime first = rows.get(0);
        assertEquals(Long.valueOf(527L), first.sequenceId());
        // atime 是设备本地时间,按适配器时区解释成 UTC(香港 = UTC+8)。
        assertEquals("2026-10-10T15:49:28Z", first.deviceTime().toString());
        assertEquals("2026-10-10 23:49:28", first.deviceTimeRaw());
        assertEquals(Integer.valueOf(6), first.stepNo());
        // download 的 cycleid 是 0 起(与库里的 cycle 一致),而 inquire 的 cycle_id 是 1 起。
        assertEquals(Integer.valueOf(0), first.cycleNo());
        assertEquals("cc", first.stepType());
        assertEquals("充电", first.stepName());
        assertEquals(Long.valueOf(21660L), first.relativeTimeSeconds());
        assertEquals(0.6852889, first.currentA(), 1e-12);
        assertEquals(1.750143, first.voltageV(), 1e-12);
        assertEquals(4.12315481465658, first.capacityAh(), 1e-12);
        assertEquals(7.12849144299129, first.energyWh(), 1e-12);
        // 行身份带测试号:seqid 在每个测试内从 1 重编,只按它会跨测试撞键。
        assertEquals("316:527", first.sourceRecordId());
        assertEquals("Asia/Hong_Kong", first.timezoneId());
    }
 
    @Test
    void acceptsTheVendorsMisspelledDownloadCommandAndEmptyPages()
    {
        List<CabinetRealtime> rows = codec.parseDownload(
                "<bts><cmd>donwload_resp</cmd><list count=\"0\" /></bts>", Instant.EPOCH);
 
        assertTrue(rows.isEmpty());
    }
 
    @Test
    void stillAcceptsTheCommandNameSpelledAsInTheProtocolDocument()
    {
        List<CabinetRealtime> rows = codec.parseDownload("<bts><cmd>download_resp</cmd><list count=\"0\" /></bts>",
                Instant.EPOCH);
 
        assertTrue(rows.isEmpty());
    }
 
    @Test
    void rejectsADownloadResponseToAnotherCommand()
    {
        BtsProtocolException exception = assertThrows(BtsProtocolException.class,
                () -> codec.parseDownload("<bts><cmd>inquire_resp</cmd><list count=\"0\" /></bts>", Instant.EPOCH));
 
        assertEquals(BtsProtocolException.Reason.UNEXPECTED_COMMAND, exception.reason());
    }
 
    @Test
    void reportsMissingDataFileRecordInsteadOfInventingAWatermark()
    {
        // 客户端不支持 inquiredf 时(实测 7.6.0.1475)应答里没有 chl 记录:这是"读不到",不是"零行"。
        BtsProtocolException exception = assertThrows(BtsProtocolException.class,
                () -> codec.parseDataFileState("<bts><cmd>inquiredf_resp</cmd><list count=\"0\" /></bts>"));
 
        assertEquals(BtsProtocolException.Reason.MISSING_FIELD, exception.reason());
    }
 
    @Test
    void buildsRequestsWithTheVendorIdentityAndPaging()
    {
        String inquire = codec.inquireRequest(Set.of(CHANNEL), false);
        assertTrue(inquire.contains("<cmd>inquire</cmd>"));
        assertTrue(inquire.contains("devtype=\"25\" devid=\"81\" subdevid=\"1\" chlid=\"2\""));
 
        String inquiredf = codec.inquiredfRequest(CHANNEL);
        assertTrue(inquiredf.contains("<cmd>inquiredf</cmd>"));
        assertTrue(inquiredf.contains("testid=\"0\""));
 
        String download = codec.downloadRequest(CHANNEL, 0L, 528L, 2);
        assertTrue(download.contains("<cmd>download</cmd>"));
        assertTrue(download.contains("startpos=\"528\""));
        assertTrue(download.contains("count=\"2\""));
        assertTrue(download.contains("auxid=\"0\""));
    }
 
    @Test
    void refusesToAddressAnotherAdaptersChannel()
    {
        ChannelIdentity foreign = new ChannelIdentity("neware-e", 25, 81, 1, 2);
 
        assertThrows(IllegalArgumentException.class, () -> codec.inquiredfRequest(foreign));
        assertThrows(IllegalArgumentException.class, () -> codec.downloadRequest(foreign, 0L, 1L, 1));
    }
 
    @Test
    void refusesPagingBeyondTheProtocolLimits()
    {
        assertThrows(IllegalArgumentException.class, () -> codec.downloadRequest(CHANNEL, 0L, 0L, 1));
        assertThrows(IllegalArgumentException.class,
                () -> codec.downloadRequest(CHANNEL, 0L, 1L, BtsXmlCodec.MAX_DOWNLOAD_PAGE + 1));
    }
 
    @Test
    void reportsTheConfiguredSiteTimezone()
    {
        assertEquals("Asia/Hong_Kong", codec.timezoneId());
    }
 
    @Test
    void keepsUnknownStepTypesWithoutInventingADirectionName()
    {
        String xml = "<bts><cmd>donwload_resp</cmd><download testid=\"9\" startpos=\"1\" count=\"1\" />"
                + "<list count=\"1\"><data seqid=\"1\" stepid=\"1\" cycleid=\"0\" steptype=\"--\" testtime=\"0\""
                + " atime=\"2026-10-10 15:08:57\" volt=\"0\" curr=\"0\" cap=\"0\" eng=\"0\" /></list></bts>";
 
        CabinetRealtime row = codec.parseDownload(xml, Instant.EPOCH).get(0);
 
        assertNull(row.stepName());
    }
 
    @Test
    void parsesRealDeviceInventoryIncludingTheCapitalisedChannelIdAttribute()
    {
        String xml = "<?xml version=\"1.0\" encoding=\"UTF-8\"?><bts version=\"1.0\"><cmd>getdevinfo_resp</cmd>"
                + "<serverip count=\"1\"><server ip=\"127.0.0.1\" port=\"3306\" /></serverip>"
                + "<middle count=\"2\">"
                + "<channel ip=\"127.0.0.1\" devtype=\"25\" devid=\"81\" subdevid=\"1\" Channelid=\"1\">true</channel>"
                + "<channel ip=\"127.0.0.1\" devtype=\"25\" devid=\"81\" subdevid=\"1\" Channelid=\"2\">false</channel>"
                + "</middle></bts>";
 
        var channels = codec.parseDeviceInfo(xml);
 
        assertEquals(2, channels.size());
        assertEquals(new ChannelIdentity(ADAPTER, 25, 81, 1, 1), channels.get(0).identity());
        assertTrue(channels.get(0).available());
        assertFalse(channels.get(1).available());
    }
 
    @Test
    void keepsTheRequestedIdentityWhenTheServerRewritesTheDeviceType()
    {
        // 实测第四台:getdevinfo 报 devtype=27,而 inquire 的应答把 devtype 改写成 25。
        // 按应答归集会与调用方请求的身份对不上(表现为"通道查不到"),所以归集要按请求的身份。
        ChannelIdentity requested = new ChannelIdentity(ADAPTER, 27, 76, 1, 1);
        String xml = "<bts><cmd>inquire_resp</cmd><list count=\"1\"><inquire dev=\"25-76-1-1-0\""
                + " cycle_id=\"1\" step_type=\"--\" workstatus=\"finish\" current=\"0\" voltage=\"0\""
                + " totaltime=\"0\" relativetime=\"0\" /></list></bts>";
 
        Map<ChannelIdentity, RealtimeSnapshot> snapshots = codec.parseSnapshots(xml, Set.of(requested), Instant.EPOCH);
 
        assertEquals(1, snapshots.size());
        RealtimeSnapshot snapshot = snapshots.get(requested);
        assertEquals(requested, snapshot.channel());
        assertEquals(DeviceWorkStatus.FINISHED, snapshot.workStatus());
        // 不带请求集合时退回按应答自己的身份归集(设备类型取应答里的 25)。
        assertEquals(new ChannelIdentity(ADAPTER, 25, 76, 1, 1),
                codec.parseSnapshots(xml, null, Instant.EPOCH).keySet().iterator().next());
    }
 
    @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);
 
        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 没有,实测三种构建互不相同)。
        // 已知字段各走自己的字段(barcode 是值、auxtemp 按已知字段表排除),未在字段表里的按动态测量原样保留。
        String xml = "<bts><cmd>inquire_resp</cmd><list count=\"1\"><inquire dev=\"25-81-1-2-0\""
                + " workstatus=\"working\" barcode=\"BC1\" auxtemp=\"25.5\" T1=\"23.4\"/></list></bts>";
 
        RealtimeSnapshot snapshot = codec.parseSnapshots(xml, Instant.EPOCH).get(CHANNEL);
 
        assertEquals("BC1", snapshot.barcode());
        assertEquals(23.4, snapshot.measurements().get("T1").value().doubleValue(), 1e-9);
        assertFalse(snapshot.measurements().containsKey("barcode"));
        assertFalse(snapshot.measurements().containsKey("auxtemp"));
    }
}