@openfn/language-varo 1.1.4 → 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +88 -4
- package/ast.json +118 -0
- package/dist/index.cjs +166 -86
- package/dist/index.js +164 -86
- package/package.json +2 -1
- package/types/Adaptor.d.ts +47 -1
- package/types/Utils.d.ts +1 -0
- package/types/VaroEmsUtils.d.ts +8 -1
package/README.md
CHANGED
|
@@ -239,9 +239,9 @@ openfn workflow.json -m --only convertToEms
|
|
|
239
239
|
|
|
240
240
|
Some workflows required authorization to access the resources.
|
|
241
241
|
|
|
242
|
-
## Gmail token
|
|
242
|
+
## Gmail access token
|
|
243
243
|
|
|
244
|
-
|
|
244
|
+
An access token is required to access the Gmail API. This short-lived token will last 60 minutes and will have to be manually refreshed. See the documentation in the [Gmail adaptor readme](https://docs.openfn.org/adaptors/packages/gmail-readme#retrieve-an-access-token) for a guide on how to use Google Developers OAuth 2.0 Playground to retrieve and refresh the access token.
|
|
245
245
|
|
|
246
246
|
## OpenFn collections token
|
|
247
247
|
|
|
@@ -322,10 +322,94 @@ pnpm build
|
|
|
322
322
|
pnpm run setup
|
|
323
323
|
```
|
|
324
324
|
|
|
325
|
-
### Switch to
|
|
325
|
+
### Switch to a working branch of the varo adaptor
|
|
326
326
|
|
|
327
327
|
```
|
|
328
328
|
cd openfn/adaptors/adaptors
|
|
329
|
-
git checkout
|
|
329
|
+
git checkout -b varo-enhancements
|
|
330
330
|
```
|
|
331
331
|
|
|
332
|
+
# FridgeTag `records` vs. `zReports`
|
|
333
|
+
|
|
334
|
+
## Purpose
|
|
335
|
+
|
|
336
|
+
The FridgeTag parser (`parseFridgeTagToReport`) reads structured device output and emits two parallel data structures: `records` and `zReports`. This dual design allows strict compliance with EMS standards, while also preserving valuable out-of-spec information from the original FridgeTag device logs.
|
|
337
|
+
|
|
338
|
+
## `records`: EMS-compliant daily extremes
|
|
339
|
+
|
|
340
|
+
FridgeTag source data provides daily minimum and maximum temperatures, including the exact timestamp each was observed. These data points are directly compatible with EMS requirements, which demand time-stamped records representing sensor readings.
|
|
341
|
+
|
|
342
|
+
For each day in the log, the parser generates:
|
|
343
|
+
|
|
344
|
+
- One EMS record for the minimum temperature, and
|
|
345
|
+
- One EMS record for the maximum temperature.
|
|
346
|
+
|
|
347
|
+
Each record includes:
|
|
348
|
+
- `ABST`: Absolute ISO-8601 timestamp (e.g., `"2024-10-08T08:15:00.000Z"`).
|
|
349
|
+
- `TVC`: Temperature value in °C (e.g., `18.5`).
|
|
350
|
+
- `ALRM`: Optional alarm flag (e.g., `"HEAT"` or `"FRZE"`), derived from alarm conditions.
|
|
351
|
+
- `zdescription`: A label for context (e.g., `"2024-10-08 Min T"`).
|
|
352
|
+
|
|
353
|
+
Example:
|
|
354
|
+
```json
|
|
355
|
+
{
|
|
356
|
+
"ABST": "2024-10-08T08:15:00.000Z",
|
|
357
|
+
"TVC": 18.5,
|
|
358
|
+
"ALRM": null,
|
|
359
|
+
"zdescription": "2024-10-08 Min T"
|
|
360
|
+
}
|
|
361
|
+
```
|
|
362
|
+
|
|
363
|
+
These entries fully conform to EMS specifications and can be directly integrated into compliant pipelines or summaries.
|
|
364
|
+
|
|
365
|
+
## `zReports`: Out-of-spec aggregates & alarm summaries
|
|
366
|
+
|
|
367
|
+
In addition to min/max values, FridgeTag records include:
|
|
368
|
+
|
|
369
|
+
- A daily average temperature,
|
|
370
|
+
- Detailed alarm metadata: including duration of condition (`t Acc`), first alarm timestamp (`TS A`), and alarm count (`C A`).
|
|
371
|
+
|
|
372
|
+
This information is not compatible with EMS formats, which don't align with aggregated statistics and rich alarm metadata. However, this data remains operationally meaningful, especially for:
|
|
373
|
+
|
|
374
|
+
- 60-day summary reports,
|
|
375
|
+
- Country-level immunization program dashboards,
|
|
376
|
+
- Quick on-site reviews by technicians.
|
|
377
|
+
|
|
378
|
+
To retain this data without violating EMS constraints, the parser generates a `zReports` array. Each entry summarizes one day:
|
|
379
|
+
|
|
380
|
+
```json
|
|
381
|
+
{
|
|
382
|
+
"date": "2024-10-08T00:00:00.000Z",
|
|
383
|
+
"duration": "1D",
|
|
384
|
+
"alarms": [
|
|
385
|
+
{
|
|
386
|
+
"condition": "HEAT",
|
|
387
|
+
"conditionMinutes": 840,
|
|
388
|
+
"alarmTime": "2024-10-08T00:00:00.000Z"
|
|
389
|
+
}
|
|
390
|
+
],
|
|
391
|
+
"aggregates": [
|
|
392
|
+
{
|
|
393
|
+
"id": "TVC",
|
|
394
|
+
"min": 18.5,
|
|
395
|
+
"max": 21.2,
|
|
396
|
+
"average": 19.2
|
|
397
|
+
}
|
|
398
|
+
]
|
|
399
|
+
}
|
|
400
|
+
```
|
|
401
|
+
|
|
402
|
+
This structure is deliberately out-of-spec (hence the `z` prefix) and is ignored by EMS consumers. It is retained only for dashboards, exports, and enriched user-facing analytics.
|
|
403
|
+
|
|
404
|
+
## Design philosophy
|
|
405
|
+
|
|
406
|
+
This split structure reflects a disciplined compromise between compliance and pragmatism:
|
|
407
|
+
|
|
408
|
+
- `records`: strictly EMS-compliant atomic data points.
|
|
409
|
+
- `zReports`: high-value extras that don't align with EMS, but still provide value.
|
|
410
|
+
|
|
411
|
+
This ensures that:
|
|
412
|
+
|
|
413
|
+
- Nothing is lost from the original FridgeTag data.
|
|
414
|
+
- Downstream EMS systems remain unaffected.
|
|
415
|
+
- Local insights and user-friendly summaries remain rich and actionable.
|
package/ast.json
CHANGED
|
@@ -166,6 +166,124 @@
|
|
|
166
166
|
]
|
|
167
167
|
},
|
|
168
168
|
"valid": true
|
|
169
|
+
},
|
|
170
|
+
{
|
|
171
|
+
"name": "parseUtcForDataRange",
|
|
172
|
+
"params": [
|
|
173
|
+
"timeZone",
|
|
174
|
+
"startIso",
|
|
175
|
+
"endIso"
|
|
176
|
+
],
|
|
177
|
+
"docs": {
|
|
178
|
+
"description": "Computes the UTC datetime range that corresponds to a given IANA timezone.",
|
|
179
|
+
"tags": [
|
|
180
|
+
{
|
|
181
|
+
"title": "public",
|
|
182
|
+
"description": null,
|
|
183
|
+
"type": null
|
|
184
|
+
},
|
|
185
|
+
{
|
|
186
|
+
"title": "function",
|
|
187
|
+
"description": null,
|
|
188
|
+
"name": null
|
|
189
|
+
},
|
|
190
|
+
{
|
|
191
|
+
"title": "param",
|
|
192
|
+
"description": "An IANA time zone identifier (e.g. \"America/Los_Angeles\").",
|
|
193
|
+
"type": {
|
|
194
|
+
"type": "NameExpression",
|
|
195
|
+
"name": "string"
|
|
196
|
+
},
|
|
197
|
+
"name": "timeZone"
|
|
198
|
+
},
|
|
199
|
+
{
|
|
200
|
+
"title": "param",
|
|
201
|
+
"description": "Starting date in ISO format.",
|
|
202
|
+
"type": {
|
|
203
|
+
"type": "NameExpression",
|
|
204
|
+
"name": "string"
|
|
205
|
+
},
|
|
206
|
+
"name": "startIso"
|
|
207
|
+
},
|
|
208
|
+
{
|
|
209
|
+
"title": "param",
|
|
210
|
+
"description": "Ending date range in ISO format.",
|
|
211
|
+
"type": {
|
|
212
|
+
"type": "NameExpression",
|
|
213
|
+
"name": "string"
|
|
214
|
+
},
|
|
215
|
+
"name": "endIso"
|
|
216
|
+
},
|
|
217
|
+
{
|
|
218
|
+
"title": "returns",
|
|
219
|
+
"description": null,
|
|
220
|
+
"type": {
|
|
221
|
+
"type": "NameExpression",
|
|
222
|
+
"name": "UtcRange"
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
]
|
|
226
|
+
},
|
|
227
|
+
"valid": true
|
|
228
|
+
},
|
|
229
|
+
{
|
|
230
|
+
"name": "isKeyInRange",
|
|
231
|
+
"params": [
|
|
232
|
+
"key",
|
|
233
|
+
"start",
|
|
234
|
+
"end"
|
|
235
|
+
],
|
|
236
|
+
"docs": {
|
|
237
|
+
"description": "Checks whether the timestamp embedded in a key falls within a UTC datetime range.",
|
|
238
|
+
"tags": [
|
|
239
|
+
{
|
|
240
|
+
"title": "public",
|
|
241
|
+
"description": null,
|
|
242
|
+
"type": null
|
|
243
|
+
},
|
|
244
|
+
{
|
|
245
|
+
"title": "function",
|
|
246
|
+
"description": null,
|
|
247
|
+
"name": null
|
|
248
|
+
},
|
|
249
|
+
{
|
|
250
|
+
"title": "param",
|
|
251
|
+
"description": "A string key containing a UTC timestamp in the format `YYYYMMDDTHHMMSS`, following a colon (e.g. \"prefix:20250624T101530\").",
|
|
252
|
+
"type": {
|
|
253
|
+
"type": "NameExpression",
|
|
254
|
+
"name": "string"
|
|
255
|
+
},
|
|
256
|
+
"name": "key"
|
|
257
|
+
},
|
|
258
|
+
{
|
|
259
|
+
"title": "param",
|
|
260
|
+
"description": "The inclusive lower bound of the UTC datetime range.",
|
|
261
|
+
"type": {
|
|
262
|
+
"type": "NameExpression",
|
|
263
|
+
"name": "Date"
|
|
264
|
+
},
|
|
265
|
+
"name": "start"
|
|
266
|
+
},
|
|
267
|
+
{
|
|
268
|
+
"title": "param",
|
|
269
|
+
"description": "The exclusive upper bound of the UTC datetime range.",
|
|
270
|
+
"type": {
|
|
271
|
+
"type": "NameExpression",
|
|
272
|
+
"name": "Date"
|
|
273
|
+
},
|
|
274
|
+
"name": "end"
|
|
275
|
+
},
|
|
276
|
+
{
|
|
277
|
+
"title": "returns",
|
|
278
|
+
"description": "True if the parsed UTC timestamp is within the range, false otherwise.",
|
|
279
|
+
"type": {
|
|
280
|
+
"type": "NameExpression",
|
|
281
|
+
"name": "boolean"
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
]
|
|
285
|
+
},
|
|
286
|
+
"valid": true
|
|
169
287
|
}
|
|
170
288
|
],
|
|
171
289
|
"exports": [],
|
package/dist/index.cjs
CHANGED
|
@@ -33,9 +33,10 @@ __export(src_exports, {
|
|
|
33
33
|
fields: () => import_language_common2.fields,
|
|
34
34
|
fn: () => import_language_common2.fn,
|
|
35
35
|
fnIf: () => import_language_common2.fnIf,
|
|
36
|
-
|
|
36
|
+
isKeyInRange: () => isKeyInRange,
|
|
37
37
|
lastReferenceValue: () => import_language_common2.lastReferenceValue,
|
|
38
38
|
merge: () => import_language_common2.merge,
|
|
39
|
+
parseUtcForDataRange: () => parseUtcForDataRange,
|
|
39
40
|
sourceValue: () => import_language_common2.sourceValue
|
|
40
41
|
});
|
|
41
42
|
module.exports = __toCommonJS(src_exports);
|
|
@@ -56,13 +57,15 @@ __export(Adaptor_exports, {
|
|
|
56
57
|
fields: () => import_language_common2.fields,
|
|
57
58
|
fn: () => import_language_common2.fn,
|
|
58
59
|
fnIf: () => import_language_common2.fnIf,
|
|
59
|
-
|
|
60
|
+
isKeyInRange: () => isKeyInRange,
|
|
60
61
|
lastReferenceValue: () => import_language_common2.lastReferenceValue,
|
|
61
62
|
merge: () => import_language_common2.merge,
|
|
63
|
+
parseUtcForDataRange: () => parseUtcForDataRange,
|
|
62
64
|
sourceValue: () => import_language_common2.sourceValue
|
|
63
65
|
});
|
|
64
66
|
var import_language_common = require("@openfn/language-common");
|
|
65
67
|
var import_util = require("@openfn/language-common/util");
|
|
68
|
+
var import_luxon = require("luxon");
|
|
66
69
|
|
|
67
70
|
// src/Utils.js
|
|
68
71
|
function parseMetadata(message) {
|
|
@@ -81,7 +84,7 @@ function parseMetadata(message) {
|
|
|
81
84
|
}
|
|
82
85
|
function removeNullProps(obj) {
|
|
83
86
|
for (const key in obj) {
|
|
84
|
-
if (obj[key]
|
|
87
|
+
if (obj[key] === null) {
|
|
85
88
|
delete obj[key];
|
|
86
89
|
}
|
|
87
90
|
}
|
|
@@ -128,7 +131,18 @@ function formatDeviceInfo(data) {
|
|
|
128
131
|
output += formatType("Appliance", data.AMFR, data.AMOD, data.ASER);
|
|
129
132
|
output += formatType("Logger", data.LMFR, data.LMOD, data.LSER);
|
|
130
133
|
output += formatType("EMD", data.EMFR, data.EMOD, data.ESER);
|
|
131
|
-
return output || "
|
|
134
|
+
return output || "Cannot determine device info; no valid data found.";
|
|
135
|
+
}
|
|
136
|
+
function abbreviatedIsoToDate(iso) {
|
|
137
|
+
const [year, month, day, hour, minute, second] = [
|
|
138
|
+
iso.slice(0, 4),
|
|
139
|
+
iso.slice(4, 6),
|
|
140
|
+
iso.slice(6, 8),
|
|
141
|
+
iso.slice(9, 11),
|
|
142
|
+
iso.slice(11, 13),
|
|
143
|
+
iso.slice(13, 15)
|
|
144
|
+
];
|
|
145
|
+
return new Date(`${year}-${month}-${day}T${hour}:${minute}:${second}Z`);
|
|
132
146
|
}
|
|
133
147
|
|
|
134
148
|
// src/StreamingUtils.js
|
|
@@ -204,7 +218,6 @@ function mergeRecords(records, groupKey) {
|
|
|
204
218
|
}
|
|
205
219
|
if (tambAlrm != null) {
|
|
206
220
|
mergedRecord["zTambAlrm"] = tambAlrm;
|
|
207
|
-
mergedRecord["ALRM"] = tambAlrm;
|
|
208
221
|
}
|
|
209
222
|
if (tvcAlrm != null) {
|
|
210
223
|
mergedRecord["zTvcAlrm"] = tvcAlrm;
|
|
@@ -317,7 +330,7 @@ function promoteDeviceProperties(source, destination) {
|
|
|
317
330
|
}
|
|
318
331
|
|
|
319
332
|
// src/VaroEmsUtils.js
|
|
320
|
-
function parseVaroEmsToReport(metadata, data,
|
|
333
|
+
function parseVaroEmsToReport(metadata, data, rtcwMaps) {
|
|
321
334
|
const report = {
|
|
322
335
|
CID: null,
|
|
323
336
|
LAT: metadata.location.used.latitude,
|
|
@@ -341,10 +354,14 @@ function parseVaroEmsToReport(metadata, data, dataPath2) {
|
|
|
341
354
|
LSV: data.LSV,
|
|
342
355
|
records: []
|
|
343
356
|
};
|
|
344
|
-
|
|
357
|
+
removeNullProps(report);
|
|
345
358
|
for (const item of data.records) {
|
|
346
|
-
const
|
|
347
|
-
|
|
359
|
+
const deviceDate = rtcwMaps.get(item.RTCW);
|
|
360
|
+
if (!deviceDate) {
|
|
361
|
+
report.zHasUnreconciledRtcw = true;
|
|
362
|
+
continue;
|
|
363
|
+
}
|
|
364
|
+
const absoluteDate = applyDurationToDate(deviceDate, item.RELT);
|
|
348
365
|
const abst = parseIsoToAbbreviatedIso(absoluteDate);
|
|
349
366
|
const record = {
|
|
350
367
|
ABST: abst,
|
|
@@ -361,67 +378,82 @@ function parseVaroEmsToReport(metadata, data, dataPath2) {
|
|
|
361
378
|
TAMB: item.TAMB,
|
|
362
379
|
TVC: item.TVC
|
|
363
380
|
};
|
|
381
|
+
removeNullProps(record);
|
|
364
382
|
report.records.push(record);
|
|
365
383
|
}
|
|
366
384
|
return report;
|
|
367
385
|
}
|
|
368
|
-
function
|
|
369
|
-
const
|
|
370
|
-
const
|
|
371
|
-
if (!
|
|
372
|
-
|
|
386
|
+
function applyDurationToDate(incomingDate, duration, subtract = false) {
|
|
387
|
+
const date = normalizeIncomingDate(incomingDate);
|
|
388
|
+
const parsedDuration = parseDuration(duration, subtract);
|
|
389
|
+
if (!parsedDuration)
|
|
390
|
+
return date;
|
|
391
|
+
date.setUTCDate(date.getUTCDate() + parsedDuration.days);
|
|
392
|
+
date.setUTCHours(date.getUTCHours() + parsedDuration.hours);
|
|
393
|
+
date.setUTCMinutes(date.getUTCMinutes() + parsedDuration.minutes);
|
|
394
|
+
date.setUTCSeconds(date.getUTCSeconds() + parsedDuration.seconds);
|
|
395
|
+
return date;
|
|
396
|
+
}
|
|
397
|
+
function parseDuration(duration, subtract) {
|
|
398
|
+
const regex = /^P(?:(?<days>\d+)D)?(?:T(?:(?<hours>\d+)H)?(?:(?<minutes>\d+)M)?(?:(?<seconds>\d+)S)?)?$/;
|
|
399
|
+
const match = duration.match(regex);
|
|
400
|
+
if (!match)
|
|
401
|
+
throw new Error(`Invalid duration format: ${duration}`);
|
|
402
|
+
const m = subtract ? -1 : 1;
|
|
403
|
+
const days = +(match.groups.days || 0) * m;
|
|
404
|
+
const hours = +(match.groups.hours || 0) * m;
|
|
405
|
+
const minutes = +(match.groups.minutes || 0) * m;
|
|
406
|
+
const seconds = +(match.groups.seconds || 0) * m;
|
|
407
|
+
return days || hours || minutes || seconds ? { days, hours, minutes, seconds } : null;
|
|
408
|
+
}
|
|
409
|
+
function normalizeIncomingDate(incomingDate) {
|
|
410
|
+
let date = incomingDate;
|
|
411
|
+
if (typeof date === "string" && /^\d{8}T\d{6}Z$/.test(date)) {
|
|
412
|
+
date = parseAbbreviatedIsoToIso(date);
|
|
373
413
|
}
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
};
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
}
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
414
|
+
return new Date(date);
|
|
415
|
+
}
|
|
416
|
+
function parseAbbreviatedIsoToIso(abbrIso) {
|
|
417
|
+
const m = abbrIso.match(/^(\d{4})(\d{2})(\d{2})T(\d{2})(\d{2})(\d{2})Z$/);
|
|
418
|
+
if (!m)
|
|
419
|
+
throw new Error(`Invalid abbreviated ISO date format: ${abbrIso}`);
|
|
420
|
+
return `${m[1]}-${m[2]}-${m[3]}T${m[4]}:${m[5]}:${m[6]}Z`;
|
|
421
|
+
}
|
|
422
|
+
function parseIsoToAbbreviatedIso(iso) {
|
|
423
|
+
return iso.toISOString().replace(/[\-\:]/g, "").replace(".000Z", "Z");
|
|
424
|
+
}
|
|
425
|
+
function buildDeviceRtcwDateMaps(contents) {
|
|
426
|
+
const dataContents = contents.filter((c) => c.data);
|
|
427
|
+
const deviceRtcwDateMaps = /* @__PURE__ */ new Map();
|
|
428
|
+
for (const content of dataContents) {
|
|
429
|
+
const { deviceId, deviceDate, finalRtcw } = extractDeviceData(content);
|
|
430
|
+
let rtcwDateMap = deviceRtcwDateMaps.get(deviceId);
|
|
431
|
+
if (!rtcwDateMap) {
|
|
432
|
+
rtcwDateMap = /* @__PURE__ */ new Map();
|
|
433
|
+
deviceRtcwDateMaps.set(deviceId, rtcwDateMap);
|
|
387
434
|
}
|
|
388
|
-
const
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
const minutes = parseInt(match.groups.minutes || 0, 10) * multiplier;
|
|
392
|
-
const seconds = parseInt(match.groups.seconds || 0, 10) * multiplier;
|
|
393
|
-
if (days === 0 && hours === 0 && minutes === 0 && seconds === 0) {
|
|
394
|
-
return null;
|
|
435
|
+
const existingDate = rtcwDateMap.get(finalRtcw);
|
|
436
|
+
if (!existingDate || deviceDate < existingDate) {
|
|
437
|
+
rtcwDateMap.set(finalRtcw, deviceDate);
|
|
395
438
|
}
|
|
396
|
-
return { days, hours, minutes, seconds };
|
|
397
439
|
}
|
|
398
|
-
|
|
399
|
-
date.setDate(date.getDate() + duration.days);
|
|
400
|
-
date.setHours(date.getHours() + duration.hours);
|
|
401
|
-
date.setMinutes(date.getMinutes() + duration.minutes);
|
|
402
|
-
date.setSeconds(date.getSeconds() + duration.seconds);
|
|
403
|
-
}
|
|
404
|
-
const adjustedDate = new Date(incomingDate);
|
|
405
|
-
for (const duration of durations) {
|
|
406
|
-
if (!duration)
|
|
407
|
-
continue;
|
|
408
|
-
const parsedDuration = parseDuration(duration);
|
|
409
|
-
if (!parsedDuration)
|
|
410
|
-
continue;
|
|
411
|
-
applyDuration(adjustedDate, parsedDuration);
|
|
412
|
-
}
|
|
413
|
-
return adjustedDate;
|
|
440
|
+
return deviceRtcwDateMaps;
|
|
414
441
|
}
|
|
415
|
-
function
|
|
416
|
-
const regex = /^(
|
|
417
|
-
const
|
|
418
|
-
|
|
419
|
-
|
|
442
|
+
function extractDeviceData(content) {
|
|
443
|
+
const regex = /^([a-zA-Z0-9]+)_(?:.+)_([A-Z0-9]{4,15})_([0-9]{8}T[0-9]{6}Z)\.json$/;
|
|
444
|
+
const [, deviceId, deviceRelt, abbrUsbDate] = content.data.filename.match(regex);
|
|
445
|
+
const deviceDate = applyDurationToDate(abbrUsbDate, deviceRelt, true);
|
|
446
|
+
if (typeof content.data.content === "string") {
|
|
447
|
+
try {
|
|
448
|
+
content.data.content = JSON.parse(content.data.content);
|
|
449
|
+
} catch (e) {
|
|
450
|
+
console.error("Invalid JSON string in content.data.content:", e);
|
|
451
|
+
}
|
|
420
452
|
}
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
return
|
|
453
|
+
content.zDeviceId = deviceId;
|
|
454
|
+
const data = content.data.content;
|
|
455
|
+
const finalRtcw = data.records[data.records.length - 1].RTCW;
|
|
456
|
+
return { deviceId, deviceDate, finalRtcw };
|
|
425
457
|
}
|
|
426
458
|
|
|
427
459
|
// src/FridgeTagUtils.js
|
|
@@ -481,7 +513,7 @@ function parseFridgeTagToReport(metadata, nodes) {
|
|
|
481
513
|
ABST: dateTime,
|
|
482
514
|
TVC: temp,
|
|
483
515
|
ALRM: alarm,
|
|
484
|
-
|
|
516
|
+
zDescription: date + " " + tempField
|
|
485
517
|
});
|
|
486
518
|
function parseAlarm(alarmNode, description) {
|
|
487
519
|
if (alarmNode["t Acc"] === "0")
|
|
@@ -579,38 +611,46 @@ function parseFridgeTag(text) {
|
|
|
579
611
|
var import_language_common2 = require("@openfn/language-common");
|
|
580
612
|
function convertToEms(messageContents) {
|
|
581
613
|
return async (state) => {
|
|
582
|
-
var _a, _b;
|
|
583
614
|
const [resolvedMessageContents] = (0, import_util.expandReferences)(state, messageContents);
|
|
584
615
|
const reports = [];
|
|
585
|
-
console.
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
if (!metadata)
|
|
590
|
-
continue;
|
|
591
|
-
const fridgeTagNodes = parseFridgeTag(content.fridgeTag.content);
|
|
592
|
-
const result = parseFridgeTagToReport(metadata, fridgeTagNodes);
|
|
593
|
-
reports.push(result);
|
|
594
|
-
continue;
|
|
595
|
-
}
|
|
596
|
-
if ((_b = content.data) == null ? void 0 : _b.content) {
|
|
597
|
-
const metadata = parseMetadata(content);
|
|
598
|
-
if (!metadata)
|
|
599
|
-
continue;
|
|
600
|
-
const data = JSON.parse(content.data.content);
|
|
601
|
-
const dataPath2 = content.data.filename;
|
|
602
|
-
const result = parseVaroEmsToReport(metadata, data, dataPath2);
|
|
603
|
-
reports.push(result);
|
|
604
|
-
continue;
|
|
605
|
-
}
|
|
616
|
+
console.info("Incoming message contents", resolvedMessageContents.length);
|
|
617
|
+
processFridgeTagContents(resolvedMessageContents, reports);
|
|
618
|
+
processDataContents(resolvedMessageContents, reports);
|
|
619
|
+
for (const content of resolvedMessageContents.filter((c) => !c.zProcessed)) {
|
|
606
620
|
console.error(
|
|
607
621
|
`Insufficient content found for MessageID: ${content.messageId}`
|
|
608
622
|
);
|
|
609
623
|
}
|
|
610
|
-
console.
|
|
624
|
+
console.info("Converted message contents", reports.length);
|
|
611
625
|
return { ...(0, import_language_common.composeNextState)(state, reports) };
|
|
612
626
|
};
|
|
613
627
|
}
|
|
628
|
+
function processFridgeTagContents(contents, reports) {
|
|
629
|
+
const fridgeTagContents = contents.filter((c) => c.fridgeTag);
|
|
630
|
+
for (const content of fridgeTagContents) {
|
|
631
|
+
const metadata = parseMetadata(content);
|
|
632
|
+
if (!metadata)
|
|
633
|
+
continue;
|
|
634
|
+
const fridgeTagNodes = parseFridgeTag(content.fridgeTag.content);
|
|
635
|
+
const result = parseFridgeTagToReport(metadata, fridgeTagNodes);
|
|
636
|
+
reports.push(result);
|
|
637
|
+
content.zProcessed = true;
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
function processDataContents(contents, reports) {
|
|
641
|
+
const dataContents = contents.filter((c) => c.data);
|
|
642
|
+
const deviceRtcwDateMaps = buildDeviceRtcwDateMaps(dataContents);
|
|
643
|
+
for (const content of dataContents) {
|
|
644
|
+
const metadata = parseMetadata(content);
|
|
645
|
+
if (!metadata)
|
|
646
|
+
continue;
|
|
647
|
+
const data = content.data.content;
|
|
648
|
+
const rtcwMaps = deviceRtcwDateMaps.get(content.zDeviceId);
|
|
649
|
+
const result = parseVaroEmsToReport(metadata, data, rtcwMaps);
|
|
650
|
+
reports.push(result);
|
|
651
|
+
content.zProcessed = true;
|
|
652
|
+
}
|
|
653
|
+
}
|
|
614
654
|
function convertItemsToReports(items, reportType = "unknown") {
|
|
615
655
|
return async (state) => {
|
|
616
656
|
const [resolvedRecords, resolvedReportType] = (0, import_util.expandReferences)(
|
|
@@ -638,7 +678,7 @@ function convertReportsToMessageContents(reports, reportType = "unknown") {
|
|
|
638
678
|
reportType
|
|
639
679
|
);
|
|
640
680
|
const messageContents = [];
|
|
641
|
-
for (const report of resolvedReports) {
|
|
681
|
+
for (const report of resolvedReports ?? []) {
|
|
642
682
|
report["zReportType"] = resolvedReportType;
|
|
643
683
|
report["zGeneratedTimestamp"] = new Date().toISOString();
|
|
644
684
|
const serialNumber = report["ESER"] || report["LSER"] || report["ASER"];
|
|
@@ -655,6 +695,45 @@ function convertReportsToMessageContents(reports, reportType = "unknown") {
|
|
|
655
695
|
return { ...(0, import_language_common.composeNextState)(state, messageContents) };
|
|
656
696
|
};
|
|
657
697
|
}
|
|
698
|
+
function parseUtcForDataRange(timeZone, startIso, endIso) {
|
|
699
|
+
const localNow = import_luxon.DateTime.now().setZone(timeZone);
|
|
700
|
+
const wallClock = new Date(
|
|
701
|
+
localNow.year,
|
|
702
|
+
localNow.month - 1,
|
|
703
|
+
localNow.day,
|
|
704
|
+
localNow.hour,
|
|
705
|
+
localNow.minute,
|
|
706
|
+
localNow.second,
|
|
707
|
+
localNow.millisecond
|
|
708
|
+
);
|
|
709
|
+
const startLocal = import_luxon.DateTime.fromISO(startIso, { zone: timeZone });
|
|
710
|
+
const endLocal = import_luxon.DateTime.fromISO(endIso, { zone: timeZone });
|
|
711
|
+
const startUtc = startLocal.toUTC().toJSDate();
|
|
712
|
+
const endUtc = endLocal.toUTC().toJSDate();
|
|
713
|
+
const daysDiff = Math.floor(endLocal.diff(startLocal, "days").days);
|
|
714
|
+
const collectionKeyPattern = "yyyyLLdd";
|
|
715
|
+
const collectionKeys = [];
|
|
716
|
+
for (let i = 0; i <= daysDiff; i++) {
|
|
717
|
+
const currentDay = startLocal.plus({ days: i });
|
|
718
|
+
collectionKeys.push(`*${currentDay.toFormat(collectionKeyPattern)}*`);
|
|
719
|
+
}
|
|
720
|
+
return {
|
|
721
|
+
wallClock,
|
|
722
|
+
startUtc,
|
|
723
|
+
endUtc,
|
|
724
|
+
collectionKeys
|
|
725
|
+
};
|
|
726
|
+
}
|
|
727
|
+
function isKeyInRange(key, start, end) {
|
|
728
|
+
console.error(
|
|
729
|
+
"i changed this implementation and need to verify the scenarios."
|
|
730
|
+
);
|
|
731
|
+
const iso = key == null ? void 0 : key.split(":")[1];
|
|
732
|
+
if (!iso || iso.length < 15)
|
|
733
|
+
return false;
|
|
734
|
+
const date = abbreviatedIsoToDate(iso);
|
|
735
|
+
return date >= start && date < end;
|
|
736
|
+
}
|
|
658
737
|
|
|
659
738
|
// src/index.js
|
|
660
739
|
var src_default = Adaptor_exports;
|
|
@@ -673,8 +752,9 @@ var src_default = Adaptor_exports;
|
|
|
673
752
|
fields,
|
|
674
753
|
fn,
|
|
675
754
|
fnIf,
|
|
676
|
-
|
|
755
|
+
isKeyInRange,
|
|
677
756
|
lastReferenceValue,
|
|
678
757
|
merge,
|
|
758
|
+
parseUtcForDataRange,
|
|
679
759
|
sourceValue
|
|
680
760
|
});
|
package/dist/index.js
CHANGED
|
@@ -20,13 +20,15 @@ __export(Adaptor_exports, {
|
|
|
20
20
|
fields: () => fields,
|
|
21
21
|
fn: () => fn,
|
|
22
22
|
fnIf: () => fnIf,
|
|
23
|
-
|
|
23
|
+
isKeyInRange: () => isKeyInRange,
|
|
24
24
|
lastReferenceValue: () => lastReferenceValue,
|
|
25
25
|
merge: () => merge,
|
|
26
|
+
parseUtcForDataRange: () => parseUtcForDataRange,
|
|
26
27
|
sourceValue: () => sourceValue
|
|
27
28
|
});
|
|
28
29
|
import { composeNextState } from "@openfn/language-common";
|
|
29
30
|
import { expandReferences } from "@openfn/language-common/util";
|
|
31
|
+
import { DateTime } from "luxon";
|
|
30
32
|
|
|
31
33
|
// src/Utils.js
|
|
32
34
|
function parseMetadata(message) {
|
|
@@ -45,7 +47,7 @@ function parseMetadata(message) {
|
|
|
45
47
|
}
|
|
46
48
|
function removeNullProps(obj) {
|
|
47
49
|
for (const key in obj) {
|
|
48
|
-
if (obj[key]
|
|
50
|
+
if (obj[key] === null) {
|
|
49
51
|
delete obj[key];
|
|
50
52
|
}
|
|
51
53
|
}
|
|
@@ -92,7 +94,18 @@ function formatDeviceInfo(data) {
|
|
|
92
94
|
output += formatType("Appliance", data.AMFR, data.AMOD, data.ASER);
|
|
93
95
|
output += formatType("Logger", data.LMFR, data.LMOD, data.LSER);
|
|
94
96
|
output += formatType("EMD", data.EMFR, data.EMOD, data.ESER);
|
|
95
|
-
return output || "
|
|
97
|
+
return output || "Cannot determine device info; no valid data found.";
|
|
98
|
+
}
|
|
99
|
+
function abbreviatedIsoToDate(iso) {
|
|
100
|
+
const [year, month, day, hour, minute, second] = [
|
|
101
|
+
iso.slice(0, 4),
|
|
102
|
+
iso.slice(4, 6),
|
|
103
|
+
iso.slice(6, 8),
|
|
104
|
+
iso.slice(9, 11),
|
|
105
|
+
iso.slice(11, 13),
|
|
106
|
+
iso.slice(13, 15)
|
|
107
|
+
];
|
|
108
|
+
return new Date(`${year}-${month}-${day}T${hour}:${minute}:${second}Z`);
|
|
96
109
|
}
|
|
97
110
|
|
|
98
111
|
// src/StreamingUtils.js
|
|
@@ -168,7 +181,6 @@ function mergeRecords(records, groupKey) {
|
|
|
168
181
|
}
|
|
169
182
|
if (tambAlrm != null) {
|
|
170
183
|
mergedRecord["zTambAlrm"] = tambAlrm;
|
|
171
|
-
mergedRecord["ALRM"] = tambAlrm;
|
|
172
184
|
}
|
|
173
185
|
if (tvcAlrm != null) {
|
|
174
186
|
mergedRecord["zTvcAlrm"] = tvcAlrm;
|
|
@@ -281,7 +293,7 @@ function promoteDeviceProperties(source, destination) {
|
|
|
281
293
|
}
|
|
282
294
|
|
|
283
295
|
// src/VaroEmsUtils.js
|
|
284
|
-
function parseVaroEmsToReport(metadata, data,
|
|
296
|
+
function parseVaroEmsToReport(metadata, data, rtcwMaps) {
|
|
285
297
|
const report = {
|
|
286
298
|
CID: null,
|
|
287
299
|
LAT: metadata.location.used.latitude,
|
|
@@ -305,10 +317,14 @@ function parseVaroEmsToReport(metadata, data, dataPath2) {
|
|
|
305
317
|
LSV: data.LSV,
|
|
306
318
|
records: []
|
|
307
319
|
};
|
|
308
|
-
|
|
320
|
+
removeNullProps(report);
|
|
309
321
|
for (const item of data.records) {
|
|
310
|
-
const
|
|
311
|
-
|
|
322
|
+
const deviceDate = rtcwMaps.get(item.RTCW);
|
|
323
|
+
if (!deviceDate) {
|
|
324
|
+
report.zHasUnreconciledRtcw = true;
|
|
325
|
+
continue;
|
|
326
|
+
}
|
|
327
|
+
const absoluteDate = applyDurationToDate(deviceDate, item.RELT);
|
|
312
328
|
const abst = parseIsoToAbbreviatedIso(absoluteDate);
|
|
313
329
|
const record = {
|
|
314
330
|
ABST: abst,
|
|
@@ -325,67 +341,82 @@ function parseVaroEmsToReport(metadata, data, dataPath2) {
|
|
|
325
341
|
TAMB: item.TAMB,
|
|
326
342
|
TVC: item.TVC
|
|
327
343
|
};
|
|
344
|
+
removeNullProps(record);
|
|
328
345
|
report.records.push(record);
|
|
329
346
|
}
|
|
330
347
|
return report;
|
|
331
348
|
}
|
|
332
|
-
function
|
|
333
|
-
const
|
|
334
|
-
const
|
|
335
|
-
if (!
|
|
336
|
-
|
|
349
|
+
function applyDurationToDate(incomingDate, duration, subtract = false) {
|
|
350
|
+
const date = normalizeIncomingDate(incomingDate);
|
|
351
|
+
const parsedDuration = parseDuration(duration, subtract);
|
|
352
|
+
if (!parsedDuration)
|
|
353
|
+
return date;
|
|
354
|
+
date.setUTCDate(date.getUTCDate() + parsedDuration.days);
|
|
355
|
+
date.setUTCHours(date.getUTCHours() + parsedDuration.hours);
|
|
356
|
+
date.setUTCMinutes(date.getUTCMinutes() + parsedDuration.minutes);
|
|
357
|
+
date.setUTCSeconds(date.getUTCSeconds() + parsedDuration.seconds);
|
|
358
|
+
return date;
|
|
359
|
+
}
|
|
360
|
+
function parseDuration(duration, subtract) {
|
|
361
|
+
const regex = /^P(?:(?<days>\d+)D)?(?:T(?:(?<hours>\d+)H)?(?:(?<minutes>\d+)M)?(?:(?<seconds>\d+)S)?)?$/;
|
|
362
|
+
const match = duration.match(regex);
|
|
363
|
+
if (!match)
|
|
364
|
+
throw new Error(`Invalid duration format: ${duration}`);
|
|
365
|
+
const m = subtract ? -1 : 1;
|
|
366
|
+
const days = +(match.groups.days || 0) * m;
|
|
367
|
+
const hours = +(match.groups.hours || 0) * m;
|
|
368
|
+
const minutes = +(match.groups.minutes || 0) * m;
|
|
369
|
+
const seconds = +(match.groups.seconds || 0) * m;
|
|
370
|
+
return days || hours || minutes || seconds ? { days, hours, minutes, seconds } : null;
|
|
371
|
+
}
|
|
372
|
+
function normalizeIncomingDate(incomingDate) {
|
|
373
|
+
let date = incomingDate;
|
|
374
|
+
if (typeof date === "string" && /^\d{8}T\d{6}Z$/.test(date)) {
|
|
375
|
+
date = parseAbbreviatedIsoToIso(date);
|
|
337
376
|
}
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
};
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
}
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
377
|
+
return new Date(date);
|
|
378
|
+
}
|
|
379
|
+
function parseAbbreviatedIsoToIso(abbrIso) {
|
|
380
|
+
const m = abbrIso.match(/^(\d{4})(\d{2})(\d{2})T(\d{2})(\d{2})(\d{2})Z$/);
|
|
381
|
+
if (!m)
|
|
382
|
+
throw new Error(`Invalid abbreviated ISO date format: ${abbrIso}`);
|
|
383
|
+
return `${m[1]}-${m[2]}-${m[3]}T${m[4]}:${m[5]}:${m[6]}Z`;
|
|
384
|
+
}
|
|
385
|
+
function parseIsoToAbbreviatedIso(iso) {
|
|
386
|
+
return iso.toISOString().replace(/[\-\:]/g, "").replace(".000Z", "Z");
|
|
387
|
+
}
|
|
388
|
+
function buildDeviceRtcwDateMaps(contents) {
|
|
389
|
+
const dataContents = contents.filter((c) => c.data);
|
|
390
|
+
const deviceRtcwDateMaps = /* @__PURE__ */ new Map();
|
|
391
|
+
for (const content of dataContents) {
|
|
392
|
+
const { deviceId, deviceDate, finalRtcw } = extractDeviceData(content);
|
|
393
|
+
let rtcwDateMap = deviceRtcwDateMaps.get(deviceId);
|
|
394
|
+
if (!rtcwDateMap) {
|
|
395
|
+
rtcwDateMap = /* @__PURE__ */ new Map();
|
|
396
|
+
deviceRtcwDateMaps.set(deviceId, rtcwDateMap);
|
|
351
397
|
}
|
|
352
|
-
const
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
const minutes = parseInt(match.groups.minutes || 0, 10) * multiplier;
|
|
356
|
-
const seconds = parseInt(match.groups.seconds || 0, 10) * multiplier;
|
|
357
|
-
if (days === 0 && hours === 0 && minutes === 0 && seconds === 0) {
|
|
358
|
-
return null;
|
|
398
|
+
const existingDate = rtcwDateMap.get(finalRtcw);
|
|
399
|
+
if (!existingDate || deviceDate < existingDate) {
|
|
400
|
+
rtcwDateMap.set(finalRtcw, deviceDate);
|
|
359
401
|
}
|
|
360
|
-
return { days, hours, minutes, seconds };
|
|
361
402
|
}
|
|
362
|
-
|
|
363
|
-
date.setDate(date.getDate() + duration.days);
|
|
364
|
-
date.setHours(date.getHours() + duration.hours);
|
|
365
|
-
date.setMinutes(date.getMinutes() + duration.minutes);
|
|
366
|
-
date.setSeconds(date.getSeconds() + duration.seconds);
|
|
367
|
-
}
|
|
368
|
-
const adjustedDate = new Date(incomingDate);
|
|
369
|
-
for (const duration of durations) {
|
|
370
|
-
if (!duration)
|
|
371
|
-
continue;
|
|
372
|
-
const parsedDuration = parseDuration(duration);
|
|
373
|
-
if (!parsedDuration)
|
|
374
|
-
continue;
|
|
375
|
-
applyDuration(adjustedDate, parsedDuration);
|
|
376
|
-
}
|
|
377
|
-
return adjustedDate;
|
|
403
|
+
return deviceRtcwDateMaps;
|
|
378
404
|
}
|
|
379
|
-
function
|
|
380
|
-
const regex = /^(
|
|
381
|
-
const
|
|
382
|
-
|
|
383
|
-
|
|
405
|
+
function extractDeviceData(content) {
|
|
406
|
+
const regex = /^([a-zA-Z0-9]+)_(?:.+)_([A-Z0-9]{4,15})_([0-9]{8}T[0-9]{6}Z)\.json$/;
|
|
407
|
+
const [, deviceId, deviceRelt, abbrUsbDate] = content.data.filename.match(regex);
|
|
408
|
+
const deviceDate = applyDurationToDate(abbrUsbDate, deviceRelt, true);
|
|
409
|
+
if (typeof content.data.content === "string") {
|
|
410
|
+
try {
|
|
411
|
+
content.data.content = JSON.parse(content.data.content);
|
|
412
|
+
} catch (e) {
|
|
413
|
+
console.error("Invalid JSON string in content.data.content:", e);
|
|
414
|
+
}
|
|
384
415
|
}
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
return
|
|
416
|
+
content.zDeviceId = deviceId;
|
|
417
|
+
const data = content.data.content;
|
|
418
|
+
const finalRtcw = data.records[data.records.length - 1].RTCW;
|
|
419
|
+
return { deviceId, deviceDate, finalRtcw };
|
|
389
420
|
}
|
|
390
421
|
|
|
391
422
|
// src/FridgeTagUtils.js
|
|
@@ -445,7 +476,7 @@ function parseFridgeTagToReport(metadata, nodes) {
|
|
|
445
476
|
ABST: dateTime,
|
|
446
477
|
TVC: temp,
|
|
447
478
|
ALRM: alarm,
|
|
448
|
-
|
|
479
|
+
zDescription: date + " " + tempField
|
|
449
480
|
});
|
|
450
481
|
function parseAlarm(alarmNode, description) {
|
|
451
482
|
if (alarmNode["t Acc"] === "0")
|
|
@@ -551,45 +582,52 @@ import {
|
|
|
551
582
|
fields,
|
|
552
583
|
fn,
|
|
553
584
|
fnIf,
|
|
554
|
-
http,
|
|
555
585
|
lastReferenceValue,
|
|
556
586
|
merge,
|
|
557
587
|
sourceValue
|
|
558
588
|
} from "@openfn/language-common";
|
|
559
589
|
function convertToEms(messageContents) {
|
|
560
590
|
return async (state) => {
|
|
561
|
-
var _a, _b;
|
|
562
591
|
const [resolvedMessageContents] = expandReferences(state, messageContents);
|
|
563
592
|
const reports = [];
|
|
564
|
-
console.
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
if (!metadata)
|
|
569
|
-
continue;
|
|
570
|
-
const fridgeTagNodes = parseFridgeTag(content.fridgeTag.content);
|
|
571
|
-
const result = parseFridgeTagToReport(metadata, fridgeTagNodes);
|
|
572
|
-
reports.push(result);
|
|
573
|
-
continue;
|
|
574
|
-
}
|
|
575
|
-
if ((_b = content.data) == null ? void 0 : _b.content) {
|
|
576
|
-
const metadata = parseMetadata(content);
|
|
577
|
-
if (!metadata)
|
|
578
|
-
continue;
|
|
579
|
-
const data = JSON.parse(content.data.content);
|
|
580
|
-
const dataPath2 = content.data.filename;
|
|
581
|
-
const result = parseVaroEmsToReport(metadata, data, dataPath2);
|
|
582
|
-
reports.push(result);
|
|
583
|
-
continue;
|
|
584
|
-
}
|
|
593
|
+
console.info("Incoming message contents", resolvedMessageContents.length);
|
|
594
|
+
processFridgeTagContents(resolvedMessageContents, reports);
|
|
595
|
+
processDataContents(resolvedMessageContents, reports);
|
|
596
|
+
for (const content of resolvedMessageContents.filter((c) => !c.zProcessed)) {
|
|
585
597
|
console.error(
|
|
586
598
|
`Insufficient content found for MessageID: ${content.messageId}`
|
|
587
599
|
);
|
|
588
600
|
}
|
|
589
|
-
console.
|
|
601
|
+
console.info("Converted message contents", reports.length);
|
|
590
602
|
return { ...composeNextState(state, reports) };
|
|
591
603
|
};
|
|
592
604
|
}
|
|
605
|
+
function processFridgeTagContents(contents, reports) {
|
|
606
|
+
const fridgeTagContents = contents.filter((c) => c.fridgeTag);
|
|
607
|
+
for (const content of fridgeTagContents) {
|
|
608
|
+
const metadata = parseMetadata(content);
|
|
609
|
+
if (!metadata)
|
|
610
|
+
continue;
|
|
611
|
+
const fridgeTagNodes = parseFridgeTag(content.fridgeTag.content);
|
|
612
|
+
const result = parseFridgeTagToReport(metadata, fridgeTagNodes);
|
|
613
|
+
reports.push(result);
|
|
614
|
+
content.zProcessed = true;
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
function processDataContents(contents, reports) {
|
|
618
|
+
const dataContents = contents.filter((c) => c.data);
|
|
619
|
+
const deviceRtcwDateMaps = buildDeviceRtcwDateMaps(dataContents);
|
|
620
|
+
for (const content of dataContents) {
|
|
621
|
+
const metadata = parseMetadata(content);
|
|
622
|
+
if (!metadata)
|
|
623
|
+
continue;
|
|
624
|
+
const data = content.data.content;
|
|
625
|
+
const rtcwMaps = deviceRtcwDateMaps.get(content.zDeviceId);
|
|
626
|
+
const result = parseVaroEmsToReport(metadata, data, rtcwMaps);
|
|
627
|
+
reports.push(result);
|
|
628
|
+
content.zProcessed = true;
|
|
629
|
+
}
|
|
630
|
+
}
|
|
593
631
|
function convertItemsToReports(items, reportType = "unknown") {
|
|
594
632
|
return async (state) => {
|
|
595
633
|
const [resolvedRecords, resolvedReportType] = expandReferences(
|
|
@@ -617,7 +655,7 @@ function convertReportsToMessageContents(reports, reportType = "unknown") {
|
|
|
617
655
|
reportType
|
|
618
656
|
);
|
|
619
657
|
const messageContents = [];
|
|
620
|
-
for (const report of resolvedReports) {
|
|
658
|
+
for (const report of resolvedReports ?? []) {
|
|
621
659
|
report["zReportType"] = resolvedReportType;
|
|
622
660
|
report["zGeneratedTimestamp"] = new Date().toISOString();
|
|
623
661
|
const serialNumber = report["ESER"] || report["LSER"] || report["ASER"];
|
|
@@ -634,6 +672,45 @@ function convertReportsToMessageContents(reports, reportType = "unknown") {
|
|
|
634
672
|
return { ...composeNextState(state, messageContents) };
|
|
635
673
|
};
|
|
636
674
|
}
|
|
675
|
+
function parseUtcForDataRange(timeZone, startIso, endIso) {
|
|
676
|
+
const localNow = DateTime.now().setZone(timeZone);
|
|
677
|
+
const wallClock = new Date(
|
|
678
|
+
localNow.year,
|
|
679
|
+
localNow.month - 1,
|
|
680
|
+
localNow.day,
|
|
681
|
+
localNow.hour,
|
|
682
|
+
localNow.minute,
|
|
683
|
+
localNow.second,
|
|
684
|
+
localNow.millisecond
|
|
685
|
+
);
|
|
686
|
+
const startLocal = DateTime.fromISO(startIso, { zone: timeZone });
|
|
687
|
+
const endLocal = DateTime.fromISO(endIso, { zone: timeZone });
|
|
688
|
+
const startUtc = startLocal.toUTC().toJSDate();
|
|
689
|
+
const endUtc = endLocal.toUTC().toJSDate();
|
|
690
|
+
const daysDiff = Math.floor(endLocal.diff(startLocal, "days").days);
|
|
691
|
+
const collectionKeyPattern = "yyyyLLdd";
|
|
692
|
+
const collectionKeys = [];
|
|
693
|
+
for (let i = 0; i <= daysDiff; i++) {
|
|
694
|
+
const currentDay = startLocal.plus({ days: i });
|
|
695
|
+
collectionKeys.push(`*${currentDay.toFormat(collectionKeyPattern)}*`);
|
|
696
|
+
}
|
|
697
|
+
return {
|
|
698
|
+
wallClock,
|
|
699
|
+
startUtc,
|
|
700
|
+
endUtc,
|
|
701
|
+
collectionKeys
|
|
702
|
+
};
|
|
703
|
+
}
|
|
704
|
+
function isKeyInRange(key, start, end) {
|
|
705
|
+
console.error(
|
|
706
|
+
"i changed this implementation and need to verify the scenarios."
|
|
707
|
+
);
|
|
708
|
+
const iso = key == null ? void 0 : key.split(":")[1];
|
|
709
|
+
if (!iso || iso.length < 15)
|
|
710
|
+
return false;
|
|
711
|
+
const date = abbreviatedIsoToDate(iso);
|
|
712
|
+
return date >= start && date < end;
|
|
713
|
+
}
|
|
637
714
|
|
|
638
715
|
// src/index.js
|
|
639
716
|
var src_default = Adaptor_exports;
|
|
@@ -652,8 +729,9 @@ export {
|
|
|
652
729
|
fields,
|
|
653
730
|
fn,
|
|
654
731
|
fnIf,
|
|
655
|
-
|
|
732
|
+
isKeyInRange,
|
|
656
733
|
lastReferenceValue,
|
|
657
734
|
merge,
|
|
735
|
+
parseUtcForDataRange,
|
|
658
736
|
sourceValue
|
|
659
737
|
};
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@openfn/language-varo",
|
|
3
3
|
"label": "Varo",
|
|
4
|
-
"version": "
|
|
4
|
+
"version": "2.1.0",
|
|
5
5
|
"description": "OpenFn varo adaptor for cold chain information",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"exports": {
|
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
"configuration-schema.json"
|
|
22
22
|
],
|
|
23
23
|
"dependencies": {
|
|
24
|
+
"luxon": "^3.6.1",
|
|
24
25
|
"@openfn/language-common": "3.0.2"
|
|
25
26
|
},
|
|
26
27
|
"devDependencies": {
|
package/types/Adaptor.d.ts
CHANGED
|
@@ -57,4 +57,50 @@ export function convertItemsToReports(items: any[], reportType?: string): Operat
|
|
|
57
57
|
* convertReportsToMessageContents(emsReports, "ems");
|
|
58
58
|
*/
|
|
59
59
|
export function convertReportsToMessageContents(reports: any, reportType?: string): Function;
|
|
60
|
-
|
|
60
|
+
/**
|
|
61
|
+
* @typedef {Object} UtcRange
|
|
62
|
+
* @property {Date} wallClock - The current local datetime as it appears on the wall in the specified timezone.
|
|
63
|
+
* @property {Date} startUtc - UTC start date range (inclusive).
|
|
64
|
+
* @property {Date} endUtc - UTC end of date range (exclusive).
|
|
65
|
+
* @property {Array} collectionKeys - Array of wildcard patterns to match UTC dates which correspond with date range (e.g. "*20250624*").
|
|
66
|
+
*/
|
|
67
|
+
/**
|
|
68
|
+
* Computes the UTC datetime range that corresponds to a given IANA timezone.
|
|
69
|
+
* @public
|
|
70
|
+
* @function
|
|
71
|
+
* @param {string} timeZone - An IANA time zone identifier (e.g. "America/Los_Angeles").
|
|
72
|
+
* @param {string} startIso - Starting date in ISO format.
|
|
73
|
+
* @param {string} endIso - Ending date range in ISO format.
|
|
74
|
+
* @returns {UtcRange}
|
|
75
|
+
*/
|
|
76
|
+
export function parseUtcForDataRange(timeZone: string, startIso: string, endIso: string): UtcRange;
|
|
77
|
+
/**
|
|
78
|
+
* Checks whether the timestamp embedded in a key falls within a UTC datetime range.
|
|
79
|
+
*
|
|
80
|
+
* @public
|
|
81
|
+
* @function
|
|
82
|
+
* @param {string} key - A string key containing a UTC timestamp in the format `YYYYMMDDTHHMMSS`, following a colon (e.g. "prefix:20250624T101530").
|
|
83
|
+
* @param {Date} start - The inclusive lower bound of the UTC datetime range.
|
|
84
|
+
* @param {Date} end - The exclusive upper bound of the UTC datetime range.
|
|
85
|
+
* @returns {boolean} True if the parsed UTC timestamp is within the range, false otherwise.
|
|
86
|
+
*/
|
|
87
|
+
export function isKeyInRange(key: string, start: Date, end: Date): boolean;
|
|
88
|
+
export type UtcRange = {
|
|
89
|
+
/**
|
|
90
|
+
* - The current local datetime as it appears on the wall in the specified timezone.
|
|
91
|
+
*/
|
|
92
|
+
wallClock: Date;
|
|
93
|
+
/**
|
|
94
|
+
* - UTC start date range (inclusive).
|
|
95
|
+
*/
|
|
96
|
+
startUtc: Date;
|
|
97
|
+
/**
|
|
98
|
+
* - UTC end of date range (exclusive).
|
|
99
|
+
*/
|
|
100
|
+
endUtc: Date;
|
|
101
|
+
/**
|
|
102
|
+
* - Array of wildcard patterns to match UTC dates which correspond with date range (e.g. "*20250624*").
|
|
103
|
+
*/
|
|
104
|
+
collectionKeys: any[];
|
|
105
|
+
};
|
|
106
|
+
export { alterState, combine, cursor, dataPath, dataValue, each, field, fields, fn, fnIf, lastReferenceValue, merge, sourceValue } from "@openfn/language-common";
|
package/types/Utils.d.ts
CHANGED
package/types/VaroEmsUtils.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export function parseVaroEmsToReport(metadata: any, data: any,
|
|
1
|
+
export function parseVaroEmsToReport(metadata: any, data: any, rtcwMaps: any): {
|
|
2
2
|
CID: any;
|
|
3
3
|
LAT: any;
|
|
4
4
|
LNG: any;
|
|
@@ -21,3 +21,10 @@ export function parseVaroEmsToReport(metadata: any, data: any, dataPath: any): {
|
|
|
21
21
|
LSV: any;
|
|
22
22
|
records: any[];
|
|
23
23
|
};
|
|
24
|
+
export function applyDurationToDate(incomingDate: any, duration: any, subtract?: boolean): Date;
|
|
25
|
+
export function buildDeviceRtcwDateMaps(contents: any): Map<any, any>;
|
|
26
|
+
export function extractDeviceData(content: any): {
|
|
27
|
+
deviceId: any;
|
|
28
|
+
deviceDate: Date;
|
|
29
|
+
finalRtcw: any;
|
|
30
|
+
};
|