diff --git a/app/actions/async.js b/app/actions/async.js index 87c65a481e..2c3592ef9d 100644 --- a/app/actions/async.js +++ b/app/actions/async.js @@ -115,7 +115,8 @@ export function doAppInit(opts, servicesToInit) { log('Initializing device'); device.init({ api, - version: opts.namedVersion + version: opts.namedVersion, + uploaderDestination: opts.uploaderDestination, }, function(deviceError, deviceResult){ if (deviceError) { return dispatch(sync.initializeAppFailure(deviceError)); @@ -429,10 +430,6 @@ export function doDeviceUpload(driverId, opts = {}, utc) { errorMessage = 'E_BLUETOOTH_PAIR'; } - if (targetDevice.powerOnlyWarning) { - errorMessage = 'E_USB_CABLE'; - } - device.upload( driverId, opts, diff --git a/app/constants/errorMessages.js b/app/constants/errorMessages.js index ad195c914f..f649ad1ccf 100644 --- a/app/constants/errorMessages.js +++ b/app/constants/errorMessages.js @@ -19,11 +19,7 @@ // and these error constants are a dependency through lib/core/api.js module.exports = { - E_CARELINK_CREDS: 'Check your CareLink username and password', - E_CARELINK_UNSUPPORTED: 'Tidepool does not support Minimed pumps 522, 722 or older, or the newer 6-series pumps. Sorry... If you are no longer using an unsupported pump and still get this message, create a new CareLink account and try uploading again.', - E_CARELINK_UPLOAD: 'Error processing & uploading CareLink data', E_DEVICE_UPLOAD: 'Something went wrong during device upload', - E_FETCH_CARELINK: 'Something went wrong trying to fetch CareLink data', E_FILE_EXT: 'Please choose a file ending in ', E_HID_CONNECTION: 'Hmm, your device doesn\'t appear to be connected', E_INIT: 'Error during app initialization', diff --git a/app/containers/App.js b/app/containers/App.js index a0124f09cc..2b398ec1b6 100644 --- a/app/containers/App.js +++ b/app/containers/App.js @@ -130,9 +130,15 @@ export class App extends Component { ? JSON.parse(localStore.getItem('selectedEnv')) : null; - this.props.async.fetchInfo(() => { + this.props.async.fetchInfo((err, info) => { + if (err) { + this.log('fetchInfo error:', err); + } this.props.async.doAppInit( - _.assign({ environment: this.state.server }, config, selectedEnv), + _.assign({ + environment: this.state.server, + uploaderDestination: info.versions?.uploaderDestination, + }, config, selectedEnv), { api: api, device, diff --git a/app/package.json b/app/package.json index 3a55b09b03..1ae63715a4 100644 --- a/app/package.json +++ b/app/package.json @@ -1,7 +1,7 @@ { "name": "tidepool-uploader", "productName": "tidepool-uploader", - "version": "2.61.0-upload-1440-dep-updates.3", + "version": "2.61.0-remove-jellyfish.8", "description": "Tidepool Project Universal Uploader", "main": "./main.prod.js", "author": { diff --git a/lib/commonFunctions.js b/lib/commonFunctions.js index f95175fb74..d3d73a1fab 100644 --- a/lib/commonFunctions.js +++ b/lib/commonFunctions.js @@ -22,9 +22,10 @@ var annotate = require('./eventAnnotations'); var api = require('./core/api.js'); var rollbar =require('../app/utils/rollbar'); var isBrowser = typeof window !== 'undefined'; -var moment = require('moment'); +var _ = require('lodash'); var GLUCOSE_MM = 18.01559; +var SEVEN_DAYS = 604800000; /** * Computes the number of milliseconds after midnight on the date specified. @@ -177,3 +178,126 @@ exports.addDurationToDeviceTime = function (event, duration) { }; exports.fixFloatingPoint = (n) => Math.floor(n * 100 + 0.5) / 100; + +Number.prototype.toFixedNumber = function(significant){ + var pow = Math.pow(10,significant); + return +( Math.round(this*pow) / pow ); +}; + +exports.updateDuration = function(event, lastEvent) { + + const withAnnotation = function(event) { + if(_.find(event.annotations || [], function(ann) { + return ann.code === 'final-basal/fabricated-from-schedule' || + ann.code === 'basal/unknown-duration' || + ann.code === 'status/incomplete-tuple' || + ann.code === 'bolus/extended-in-progress' || + ann.code === 'tandem/pumpSettingsOverride/estimated-duration'; + })) { + return true; + } + return false; + }; + + let updatedDuration = Date.parse(event.time) - Date.parse(lastEvent.time); + + if (event.extended && event.duration) { + // for extended boluses we don't have to calculate the duration + updatedDuration = event.duration; + } + + if (updatedDuration >= 0) { + debug('Updating duration of last event from previous upload'); + if((lastEvent.subType !== 'pumpSettingsOverride') && lastEvent.duration > 0) { + lastEvent.expectedDuration = lastEvent.duration; + } + lastEvent.duration = updatedDuration; + + if (withAnnotation(lastEvent)) { + // if the event was previously annotated because of the missing event, + // we can now remove the annotations + lastEvent = _.omit(lastEvent, 'annotations'); + } + + return lastEvent; + } else { + return null; + } +}; + +exports.stripUnwantedFields = function(record) { + const stripped = _.omit(record, + 'createdTime', 'id', 'guid', 'modifiedTime', 'revision', 'uploadId', + '_active', '_deduplicator', '_id', '_schemaVersion', '_userId', + ); + + debug(`Last ${stripped.type} (${stripped.subType || stripped.deliveryType}) from previous upload was: ${JSON.stringify(stripped, null, 4)}`); + return stripped; +}; + +exports.updatePreviousDurations = async function(data, cfg, cb) { + + + try { + // update last basal + const lastBasal = this.stripUnwantedFields(await api.getLatestRecord(cfg.groupId, cfg.deviceInfo.deviceId, 'basal')); + let datum = _.find(data.post_records, {type: 'basal'}); + if (datum != null) { + const updatedBasal = this.updateDuration(datum, lastBasal); + if (updatedBasal) { + data.post_records.push(updatedBasal); + } + } + + // update last suspend event + if (lastBasal.deliveryType === 'suspend') { + const lastDeviceEvent = this.stripUnwantedFields(await api.getLatestRecord(cfg.groupId, cfg.deviceInfo.deviceId, 'deviceEvent', 'status')); + if (lastDeviceEvent.subType === 'status') { + const index = _.findIndex(data.post_records, {type: 'deviceEvent', subType: 'status'}); + datum = data.post_records[index]; + + if (index > -1) { + const updatedStatus = this.updateDuration(datum, lastDeviceEvent); + + if (updatedStatus) { + updatedStatus.reason.resumed = datum.reason.resumed; + + if (lastDeviceEvent.payload && lastDeviceEvent.payload.reason != null) { + updatedStatus.payload.suspended = lastDeviceEvent.payload.reason; + delete updatedStatus.payload.reason; + } + if (datum.payload && datum.payload.reason != null) { + updatedStatus.payload.resumed = datum.payload.reason; + } + + data.post_records.push(updatedStatus); + data.post_records.splice(index, 1); // remove resume event that is now combined + } + } + } + } + + // update last pump settings override + const lastPumpSettingsOverride = this.stripUnwantedFields(await api.getLatestRecord(cfg.groupId, cfg.deviceInfo.deviceId, 'deviceEvent', 'pumpSettingsOverride')); + + const index = _.findIndex(data.post_records, {type: 'deviceEvent', subType: 'pumpSettingsOverride'}); + datum = data.post_records[index]; + if (datum != null) { + if (annotate.isAnnotated(lastPumpSettingsOverride, 'tandem/pumpSettingsOverride/estimated-duration')) { + const updatedOverride = this.updateDuration(datum, lastPumpSettingsOverride); + if (updatedOverride) { + data.post_records.push(updatedOverride); + } else { + debug('Not updating pump settings override as duration could not be calculated'); + } + } + if (datum.overrideType === 'normal') { + // remove override active at beginning that was stopped + data.post_records.splice(index, 1); + } + } + return cb (null, data); + } catch (error) { + return cb (error); + } +}; diff --git a/lib/core/api.js b/lib/core/api.js index 951bd73f33..7174b26a3a 100644 --- a/lib/core/api.js +++ b/lib/core/api.js @@ -473,6 +473,21 @@ api.upload.getInfo = (cb) => { }); }; +api.upload.getInfoForUser = (userId, cb) => { + api.log(`GET /info/${userId}`); + tidepool.getInfoForUser(userId, (err, resp) => { + if (err) { + if (!navigator.onLine) { + const error = new Error(ErrorMessages.E_OFFLINE); + error.originalError = err; + return cb(error); + } + return cb(err); + } + return cb(null, resp); + }); +}; + api.upload.getVersions = (cb) => { api.log('GET /info'); tidepool.checkUploadVersions((err, resp) => { @@ -524,7 +539,6 @@ function buildError(error, datasetId) { } api.log(JSON.stringify(_.omit(error, 'sessionToken'), null, '\t')); - return err; } @@ -635,200 +649,219 @@ function addDataToDataset(data, datasetId, blockIndex, uploadType, callback) { api.upload.toPlatform = (data, sessionInfo, progress, groupId, cb, uploadType = 'jellyfish', devices) => { // uploadType can either be 'jellyfish' or 'dataservices' - api.log(`attempting to upload ${data.length} device data records to ${uploadType} api`); - const grouped = _.groupBy(data, 'type'); - // eslint-disable-next-line no-restricted-syntax - for (const type in grouped) { - if ({}.hasOwnProperty.call(grouped, type)) { - api.log(grouped[type].length, 'records of type', type); + // eslint-disable-next-line consistent-return + api.upload.getInfo((err, info) => { + if (err) { + return cb(err); } - } - const blocks = []; - const BLOCKSIZE = 1000; - let nblocks = 0; - let datasetId; - /* eslint-disable camelcase */ - /* eslint-disable no-shadow */ - const post_and_progress = (data, callback) => { - if (devices) { - // multiple devices being uploaded - const percentage = (((((nblocks += 1) / blocks.length) + devices.index - 1) / devices.total) * 100.0); - api.log(`Progress: ${devices.index} / ${devices.total} - ${percentage}%`); - progress(percentage); - } else { - progress(((nblocks += 1) / blocks.length) * 100.0); - } + if (info.versions.uploaderDestination === 'platform') { + // now everything is going through Platform + if (uploadType === 'jellyfish' && ( + sessionInfo.deviceManufacturers.includes('Tandem') || + sessionInfo.deviceManufacturers.includes('Insulet'))) { + // we still have the old drivers loaded, should restart + return cb(new Error('Need to load new version of driver, please restart Uploader.')); + } - // off to the platfrom we go - if (uploadType === 'jellyfish') { - return addDataToDataset(data, groupId, nblocks, uploadType, callback); + uploadType = 'dataservices'; } - return addDataToDataset(data, datasetId, nblocks, uploadType, callback); - }; - const post_dataset_create = (uploadMeta, callback) => createDatasetForUser( - groupId, uploadMeta, callback, - ); + api.log(`attempting to upload ${data.length} device data records to ${uploadType} api`); + const grouped = _.groupBy(data, 'type'); + // eslint-disable-next-line no-restricted-syntax + for (const type in grouped) { + if ({}.hasOwnProperty.call(grouped, type)) { + api.log(grouped[type].length, 'records of type', type); + } + } - const post_dataset_finalize = (callback) => finalizeDataset(datasetId, callback); + const blocks = []; + const BLOCKSIZE = 1000; + let nblocks = 0; + let datasetId; + /* eslint-disable camelcase */ + /* eslint-disable no-shadow */ + const post_and_progress = (data, callback) => { + if (devices) { + // multiple devices being uploaded + const percentage = (((((nblocks += 1) / blocks.length) + devices.index - 1) / devices.total) * 100.0); + api.log(`Progress: ${devices.index} / ${devices.total} - ${percentage}%`); + progress(percentage); + } else { + progress(((nblocks += 1) / blocks.length) * 100.0); + } - const decorate = (data, uploadItem) => { - if (uploadType === 'jellyfish') { - const deviceRecords = _.map(data, (item) => _.extend({}, item, { - uploadId: uploadItem.uploadId, - guid: uuidv4(), - })); - return deviceRecords; - } - return data; - }; + // off to the platfrom we go + if (uploadType === 'jellyfish') { + return addDataToDataset(data, groupId, nblocks, uploadType, callback); + } + return addDataToDataset(data, datasetId, nblocks, uploadType, callback); + }; - async.waterfall([ - (callback) => { - // generate digest - const encoder = new TextEncoder(); - const data = encoder.encode(`${sessionInfo.deviceId}_${sessionInfo.start}`); - crypto.subtle.digest('SHA-256', data).then((digest) => { - const hex = Array.from(new Uint8Array(digest)).map(b => b.toString(16).padStart(2, '0')).join(''); - return hex; - }).then((hexDigest) => { - callback(null, hexDigest); - }); - }, - (digest, callback) => { - // generate and post the upload metadata - const now = new Date(); + const post_dataset_create = (uploadMeta, callback) => createDatasetForUser( + groupId, uploadMeta, callback, + ); - if (rollbar) { - const nowHammerTime = now.getTime(); - const events = _.filter(data, (event) => { - const year = parseInt(event.time.substring(0, 4), 10); - const timestamp = sundial.parseFromFormat(event.time).getTime(); + const post_dataset_finalize = (callback) => finalizeDataset(datasetId, callback); - return year < 2006 || timestamp > (nowHammerTime + (24 * 60 * sundial.MIN_TO_MSEC)); + const decorate = (data, uploadItem) => { + if (uploadType === 'jellyfish') { + const deviceRecords = _.map(data, (item) => _.extend({}, item, { + uploadId: uploadItem.uploadId, + guid: uuidv4(), + })); + return deviceRecords; + } + return data; + }; + + async.waterfall([ + (callback) => { + // generate digest + const encoder = new TextEncoder(); + const data = encoder.encode(`${sessionInfo.deviceId}_${sessionInfo.start}`); + crypto.subtle.digest('SHA-256', data).then((digest) => { + const hex = Array.from(new Uint8Array(digest)).map(b => b.toString(16).padStart(2, '0')).join(''); + return hex; + }).then((hexDigest) => { + callback(null, hexDigest); }); + }, + (digest, callback) => { + // generate and post the upload metadata + const now = new Date(); - if (events.length > 0) { - rollbar.info('Upload contains event(s) prior to 2006 or more than a day in the future', events); - } - } + if (rollbar) { + const nowHammerTime = now.getTime(); + const events = _.filter(data, (event) => { + const year = parseInt(event.time.substring(0, 4), 10); + const timestamp = sundial.parseFromFormat(event.time).getTime(); - let uploadItem = builder().makeUpload() - // yes, I'm intentionally breaking up the new Date() I made and parsing - // it again with another new Date()...it's a moment limitation... - .with_computerTime(sundial.formatDeviceTime(new Date(Date.UTC( - now.getFullYear(), now.getMonth(), now.getDate(), - now.getHours(), now.getMinutes(), now.getSeconds(), - )))) - .with_time(sessionInfo.start) - .with_timezone(sessionInfo.tzName) - .with_timezoneOffset(-new Date().getTimezoneOffset()) - .with_conversionOffset(0) - .with_timeProcessing(sessionInfo.timeProcessing) - .with_version(sessionInfo.version) - .with_deviceTags(sessionInfo.deviceTags) - .with_deviceTime(sessionInfo.deviceTime) - .with_deviceManufacturers(sessionInfo.deviceManufacturers) - .with_deviceModel(sessionInfo.deviceModel) - .with_deviceSerialNumber(sessionInfo.deviceSerialNumber) - .with_deviceId(sessionInfo.deviceId) - .with_payload(sessionInfo.payload) - .with_client({ - name: 'org.tidepool.uploader', - version: sessionInfo.version, - }); + return year < 2006 || timestamp > (nowHammerTime + (24 * 60 * sundial.MIN_TO_MSEC)); + }); - if (sessionInfo.delta != null) { - _.set(uploadItem, 'client.private.delta', sessionInfo.delta); - } + if (events.length > 0) { + rollbar.info('Upload contains event(s) prior to 2006 or more than a day in the future', events); + } + } - if (sessionInfo.blobId != null) { - _.set(uploadItem, 'client.private.blobId', sessionInfo.blobId); - } + let uploadItem = builder().makeUpload() + // yes, I'm intentionally breaking up the new Date() I made and parsing + // it again with another new Date()...it's a moment limitation... + .with_computerTime(sundial.formatDeviceTime(new Date(Date.UTC( + now.getFullYear(), now.getMonth(), now.getDate(), + now.getHours(), now.getMinutes(), now.getSeconds(), + )))) + .with_time(sessionInfo.start) + .with_timezone(sessionInfo.tzName) + .with_timezoneOffset(-new Date().getTimezoneOffset()) + .with_conversionOffset(0) + .with_timeProcessing(sessionInfo.timeProcessing) + .with_version(sessionInfo.version) + .with_deviceTags(sessionInfo.deviceTags) + .with_deviceTime(sessionInfo.deviceTime) + .with_deviceManufacturers(sessionInfo.deviceManufacturers) + .with_deviceModel(sessionInfo.deviceModel) + .with_deviceSerialNumber(sessionInfo.deviceSerialNumber) + .with_deviceId(sessionInfo.deviceId) + .with_payload(sessionInfo.payload) + .with_client({ + name: 'org.tidepool.uploader', + version: sessionInfo.version, + }); + + if (sessionInfo.delta != null) { + _.set(uploadItem, 'client.private.delta', sessionInfo.delta); + } - if (sessionInfo.annotations != null) { - _.set(uploadItem, 'annotations', sessionInfo.annotations); - } + if (sessionInfo.blobId != null) { + _.set(uploadItem, 'client.private.blobId', sessionInfo.blobId); + } - if (sessionInfo.source != null) { - _.set(uploadItem, 'client.private.source', sessionInfo.source); - } + if (sessionInfo.annotations != null) { + _.set(uploadItem, 'annotations', sessionInfo.annotations); + } - _.set(uploadItem, 'client.private.os', `${os.platform()}-${os.arch()}-${os.release()}`); + if (sessionInfo.source != null) { + _.set(uploadItem, 'client.private.source', sessionInfo.source); + } - if (uploadType === 'jellyfish') { - uploadItem.with_uploadId(`upid_${digest.slice(0, 12)}`); - uploadItem.with_guid(uuidv4()); - uploadItem.with_byUser(tidepool.getUserId()); - } - uploadItem = uploadItem.done(); - api.log('create dataset'); - - if (uploadType === 'dataservices') { - post_dataset_create(uploadItem, (err, dataset) => { - if (_.isEmpty(err)) { - api.log('created dataset'); - datasetId = _.get(dataset, 'data.uploadId'); - if (_.isEmpty(datasetId)) { - api.log('created dataset does not include uploadId'); - return callback(new Error(format('Dataset response does not contain uploadId.'))); - } - return callback(null, uploadItem); - } - api.log('error creating dataset ', err); - return callback(err); - }); - } else { - // upload metadata uploaded as usual through jellyfish - addDataToDataset(uploadItem, groupId, 0, uploadType, (err) => { - if (_.isEmpty(err)) { - api.log('saved upload metadata'); - return callback(null, uploadItem); - } - api.log('error saving upload metadata ', err); - return callback(err); - }); - } - }, - (uploadItem, callback) => { - // decorate our data with the successfully posted upload metadata - // as well as a GUID and then save to the platform - // eslint-disable-next-line no-param-reassign - data = decorate(data, uploadItem); + _.set(uploadItem, 'client.private.os', `${os.platform()}-${os.arch()}-${os.release()}`); - for (let i = 0; i < data.length; i += BLOCKSIZE) { - blocks.push(data.slice(i, i + BLOCKSIZE)); - } - api.log('start uploading the rest of the data'); - // process then finalise, or if you want you can finalize :) - async.mapSeries(blocks, post_and_progress, callback); - }, - (result, callback) => { - if (uploadType === 'jellyfish') { - callback(null, result); - } else { - api.log('finalize dataset'); - post_dataset_finalize((err) => { - if (_.isEmpty(err)) { - api.log('finalized dataset'); - return callback(null, result); - } - api.log('error finalizing dataset', err); - return callback(err); - }); - } - }, - ], (err, result) => { - if (err == null) { - api.log('upload.toPlatform: all good'); - if (env.browser && rollbar) { - rollbar.info('Successful Uploader-in-Web upload', sessionInfo); + if (uploadType === 'jellyfish') { + uploadItem.with_uploadId(`upid_${digest.slice(0, 12)}`); + uploadItem.with_guid(uuidv4()); + uploadItem.with_byUser(tidepool.getUserId()); + } + uploadItem = uploadItem.done(); + api.log('create dataset'); + + if (uploadType === 'dataservices') { + post_dataset_create(uploadItem, (err, dataset) => { + if (_.isEmpty(err)) { + api.log('created dataset'); + datasetId = _.get(dataset, 'data.uploadId'); + if (_.isEmpty(datasetId)) { + api.log('created dataset does not include uploadId'); + return callback(new Error(format('Dataset response does not contain uploadId.'))); + } + return callback(null, uploadItem); + } + api.log('error creating dataset ', err); + return callback(err); + }); + } else { + // upload metadata uploaded as usual through jellyfish + addDataToDataset(uploadItem, groupId, 0, uploadType, (err) => { + if (_.isEmpty(err)) { + api.log('saved upload metadata'); + return callback(null, uploadItem); + } + api.log('error saving upload metadata ', err); + return callback(err); + }); + } + }, + (uploadItem, callback) => { + // decorate our data with the successfully posted upload metadata + // as well as a GUID and then save to the platform + // eslint-disable-next-line no-param-reassign + data = decorate(data, uploadItem); + + for (let i = 0; i < data.length; i += BLOCKSIZE) { + blocks.push(data.slice(i, i + BLOCKSIZE)); + } + api.log('start uploading the rest of the data'); + // process then finalise, or if you want you can finalize :) + async.mapSeries(blocks, post_and_progress, callback); + }, + (result, callback) => { + if (uploadType === 'jellyfish') { + callback(null, result); + } else { + api.log('finalize dataset'); + post_dataset_finalize((err) => { + if (_.isEmpty(err)) { + api.log('finalized dataset'); + return callback(null, result); + } + api.log('error finalizing dataset', err); + return callback(err); + }); + } + }, + ], (err, result) => { + if (err == null) { + api.log('upload.toPlatform: all good'); + if (env.browser && rollbar) { + rollbar.info('Successful Uploader-in-Web upload', sessionInfo); + } + return cb(null, result); } - return cb(null, result); - } - api.log('upload.toPlatform: failed ', err); - return cb(err); + api.log('upload.toPlatform: failed ', err); + return cb(err); + }); }); }; @@ -853,6 +886,36 @@ api.getMostRecentUploadRecord = (userId, deviceId, cb) => { }); }; +api.getLatestRecord = (userId, deviceId, type, subType) => { + const options = { + type, + latest: 1, + deviceId, + subType, + }; + + api.log(`GET /data/${userId}&options=${JSON.stringify(options)}`); + return new Promise((resolve, reject) => { + tidepool.getDeviceDataForUser(userId, options, (err, resp) => { + if (err) { + if (!navigator.onLine) { + const error = new Error(ErrorMessages.E_OFFLINE); + error.originalError = err; + reject(error); + } + reject(err); + } + api.log('Latest record response:', resp); + if (resp && resp.length > 0) { + resolve(resp[0]); + } else { + // could not retrieve an upload record, so return null + resolve(null); + } + }); + }); +}; + api.upload.blob = async (blob, contentType, cb) => { api.log('POST /blobs'); const blobObject = new Blob([blob], { type: contentType }); diff --git a/lib/core/device.js b/lib/core/device.js index 403576f56a..b39ff8d520 100644 --- a/lib/core/device.js +++ b/lib/core/device.js @@ -34,7 +34,6 @@ import env from '../../app/utils/env'; import dexcomDriver from '../drivers/dexcom/dexcomDriver'; import oneTouchUltraMini from '../drivers/onetouch/oneTouchUltraMini'; import abbottPrecisionXtra from '../drivers/abbott/abbottPrecisionXtra'; -import insuletOmniPod from '../drivers/insulet/insuletDriver'; import oneTouchUltra2 from '../drivers/onetouch/oneTouchUltra2'; import oneTouchVerio from '../drivers/onetouch/oneTouchVerio'; import oneTouchVerioIQ from '../drivers/onetouch/oneTouchVerioIQ'; @@ -60,14 +59,6 @@ const device = { log: bows('Device'), }; -let tandemDriver; -try { - // eslint-disable-next-line global-require, import/no-unresolved, import/extensions - tandemDriver = require('../drivers/tandem/tandemDriver'); -} catch (e) { - device.log('Tandem driver is only available to Tidepool developers.'); -} - const hostMap = { darwin: 'mac', win32: 'win', @@ -78,8 +69,8 @@ device.deviceDrivers = { Dexcom: dexcomDriver, OneTouchUltraMini: oneTouchUltraMini, AbbottPrecisionXtra: abbottPrecisionXtra, - InsuletOmniPod: insuletOmniPod, - Tandem: tandemDriver, + InsuletOmniPod: null, // gets defined later + Tandem: null, // gets defined later OneTouchUltra2: oneTouchUltra2, OneTouchVerio: oneTouchVerio, OneTouchVerioIQ: oneTouchVerioIQ, @@ -163,6 +154,17 @@ _.forEach(_.keys(device.deviceComms), (driverId) => { device.deviceInfoCache = {}; device.init = (options, cb) => { + try { + /* eslint-disable global-require, import/no-unresolved, import/extensions */ + device.deviceDrivers.Tandem = require('../drivers/tandem/tandemDriver'); + device.deviceDrivers.TandemJellyfish = require('../drivers/tandem/jellyfish/tandemDriver'); + } catch (err) { + device.log('Tandem driver is only available to Tidepool developers.'); + } + + device.deviceDrivers.InsuletOmniPod = require('../drivers/insulet/insuletDriver'); + device.deviceDrivers.InsuletOmniPodJellyfish = require('../drivers/insulet/jellyfish/insuletDriver'); + device.defaultTimezone = options.defaultTimezone; device.api = options.api; device.version = options.version; @@ -184,17 +186,45 @@ device.getDriverManifest = (driverId) => { }; device.detectHelper = (driverId, options, cb) => { - const dm = device.createDriverManager(driverId, options); - if (dm == null) { - cb(new Error('Driver not available.')); - } else { - dm.detect(driverId, cb); - } + // eslint-disable-next-line consistent-return + device.api.upload.getInfoForUser(options.targetId, (err, info) => { + if (err) { + return cb(err); + } + device.uploaderDestination = info.uploaderDestination; + device.log('Uploader destination:', device.uploaderDestination); + + const dm = device.createDriverManager(driverId, options); + if (dm == null) { + cb(new Error('Driver not available:', driverId)); + } else { + dm.detect(driverId, cb); + } + }); }; device.createDriverManager = (driverId, options) => { const drivers = {}; + + if (device.uploaderDestination == null) { + throw new Error('Could not determine upload destination'); + } + drivers[driverId] = device.deviceDrivers[driverId]; + + if (device.uploaderDestination === 'jellyfish') { + if (driverId === 'Tandem') { + // eslint-disable-next-line no-param-reassign + drivers[driverId] = device.deviceDrivers.TandemJellyfish; + device.log('Using Jellyfish version of Tandem driver'); + } + if (driverId === 'InsuletOmniPod') { + // eslint-disable-next-line no-param-reassign + drivers[driverId] = device.deviceDrivers.InsuletOmniPodJellyfish; + device.log('Using Jellyfish version of Omnipod driver'); + } + } + const configs = {}; configs[driverId] = device.createDriverConfig(driverId, options); configs.debug = debugMode.isDebug; diff --git a/lib/drivers/insulet/cli/ibf_loader.js b/lib/drivers/insulet/cli/ibf_loader.js index badc2dc0bd..2179715973 100755 --- a/lib/drivers/insulet/cli/ibf_loader.js +++ b/lib/drivers/insulet/cli/ibf_loader.js @@ -16,22 +16,45 @@ const intro = 'Insulet CLI:'; /* * Process our raw insulet data */ -function processInsulet(driverMgr) { +function processInsulet(driverMgr, saveFile) { driverMgr.process('Insulet', (err, result) => { - if (err) { - console.log(intro, 'Error processing Insulet data:', err); - console.log(err.stack); + if (saveFile) { + const buf = new Uint8Array( + Buffer.from(JSON.stringify(result.post_records, null, 4)), + ); + console.log('Saving POST records to disk'); + fs.writeFile(saveFile, buf, error => { + if (err) { + console.log(intro, 'Error processing Insulet data:', err); + console.log(err.stack); + process.exit(); + } + + if (error) { + console.log('An error has occurred ', error); + } else { + console.log('Done!'); + } + + process.exit(); + }); + } else { + if (err) { + console.log(intro, 'Error processing Insulet data:', err); + console.log(err.stack); + } else { + console.log('Done!'); + } + process.exit(); } - console.log(`${intro} All good! loaded ${result.post_records.length} events - check in blip :)`); - process.exit(); }); } /* * Load the given insulet file and then parse and send the data to the tp-platform */ -function loadFile(filePath, tz, userid) { +function loadFile(filePath, tz, userid, saveFile) { // http://stackoverflow.com/questions/8609289/convert-a-binary-nodejs-buffer-to-javascript-arraybuffer function toArrayBuffer(buffer) { const ab = new ArrayBuffer(buffer.length); @@ -60,12 +83,12 @@ function loadFile(filePath, tz, userid) { filedata: toArrayBuffer(data), filename: filePath, timezone: tz, - version: `${pkg.name} ${pkg.version}`, + version: `${pkg.version}`, groupId: userid, }, }; - processInsulet(driverManager(drivers, cfg)); + processInsulet(driverManager(drivers, cfg), saveFile); }); } @@ -93,6 +116,7 @@ program .option('-u, --username [user]', 'username') .option('-p, --password [pw]', 'password') .option('-t, --timezone [tz]', 'named timezone', config.DEFAULT_TIMEZONE) + .option('-s, --save [filename]', 'save POST records to disk') .parse(process.argv); console.log(intro, 'Loading Insulet file...'); @@ -108,7 +132,7 @@ if (program.file && program.username && program.password) { console.log(intro, 'Loading using the timezone', program.timezone); console.log(intro, 'Loading for user ', data.userid); - loadFile(program.file, program.timezone, data.userid); + loadFile(program.file, program.timezone, data.userid, program.save); }); } else { console.log(`${intro} Insulet file at ${program.file} not found`); diff --git a/lib/drivers/insulet/insuletDriver.js b/lib/drivers/insulet/insuletDriver.js index eaf0e507bd..67e67254c9 100644 --- a/lib/drivers/insulet/insuletDriver.js +++ b/lib/drivers/insulet/insuletDriver.js @@ -291,10 +291,18 @@ module.exports = (config) => { 'immediate_duration_seconds', ], postprocess: (rec) => { - rec.volume_units = toUnits(rec.volume); - // this occurs if an extended bolus is programmed but the bolus is interrupted - // before any of the extended portion is delivered! - if (rec.extended_duration_minutes === 65535) { + if (rec.volume > 0xFFFF0000) { + // this is not documented, but apparently there can be special values for the volume, + // similar to special values for the extended duration below + rec.volume_units = 0; + } else { + rec.volume_units = toUnits(rec.volume); + } + if (rec.extended_duration_minutes > 65530) { + // this occurs if an extended bolus is programmed but the bolus is interrupted + // before any of the extended portion is delivered! + // We initially thought it would only be 65535 (0xFFFF), but sometimes + // 65533 is used as well, so we use above 65530 rec.extended_duration_msec = 0; } else if (rec.extended_duration_minutes === 0) { // a zero *here* means that this isn't an extended bolus @@ -366,8 +374,10 @@ module.exports = (config) => { rec.current_bg = null; } - if (rec.carb_grams === 65535) { + if (rec.carb_grams > 65520) { // if carb count was not given to the wizard, don't report it. + // We initially thought it would only be 65535 (0xFFFF), but sometimes + // 65526 is used as well, so we use above 65520 rec.carb_grams = null; } @@ -595,10 +605,6 @@ module.exports = (config) => { }; const LOG_FLAGS = { - // TODO: look for this flag and use it to identify - // extended boluses split over midnight instead of uploading - // them separately and annotating the extended with the - // 'insulet/bolus/split-extended' code as we're doing now CARRY_OVER_FLAG: { value: 0x01, name: 'CARRY_OVER_FLAG' }, NEW_DAY_FLAG: { value: 0x02, name: 'NEW_DAY_FLAG' }, // TODO: we should probably look for this flag on all records @@ -991,12 +997,12 @@ module.exports = (config) => { // otherwise we can't be certain of our conversion to UTC // and thus really don't want to be mucking with the basal events stream if (anAlarm.detail && anAlarm.log_index) { - const suspend = cfg.builder.makeDeviceEventSuspend() + const suspend = cfg.builder.makeDeviceEventSuspendResume() .with_deviceTime(anAlarm.deviceTime) .with_reason({ suspended: 'automatic' }); suspend.set('index', anAlarm.log_index); cfg.tzoUtil.fillInUTCInfo(suspend, anAlarm.jsDate); - return suspend.done(); + return suspend; } return null; } @@ -1254,6 +1260,7 @@ module.exports = (config) => { // when there isn't a wizard record to tie split records together, we don't // know if this is a component of a split, so we annotate if (millisInDay <= 5000 && !data.log_records[wiz_idx]) { + postbolus.aftercarryover = true; // this part happened after midnight annotate.annotateEvent(postbolus, 'insulet/bolus/split-extended'); } postbolus = postbolus.done(); @@ -1284,6 +1291,7 @@ module.exports = (config) => { } if (postbolus) { + let postwiz = null; if (wizRecords[wiz_idx]) { const wiz = data.log_records[wiz_idx] || {}; // wiz will be empty if the bolus was a quick bolus @@ -1299,7 +1307,7 @@ module.exports = (config) => { const carb = wiz.detail.carb_grams; postbolus.carbInput = carb; /* we need this to delete zero boluses * without wizard carbs in the simulator */ - let postwiz = cfg.builder.makeWizard() + postwiz = cfg.builder.makeWizard() .with_recommended({ carb: wiz.detail.carb_bolus_units_suggested, correction: wiz.detail.corr_units_suggested, @@ -1333,11 +1341,20 @@ module.exports = (config) => { // itself as the timestamp cfg.tzoUtil.fillInUTCInfo(postwiz, postbolus.jsDate); postwiz = postwiz.done(); + postrecords.push(postwiz); } } delete postbolus.jsDate; - postrecords.push(postbolus); + if (postwiz == null) { + if ( + hasFlag(LOG_FLAGS.CARRY_OVER_FLAG, bolus.flags) + && !hasFlag(LOG_FLAGS.NEW_DAY_FLAG, bolus.flags) // Eros also has carry over flag on second part + ) { + postbolus.carryover = true; + } + postrecords.push(postbolus); + } } } return records.concat(postrecords); @@ -1394,16 +1411,19 @@ module.exports = (config) => { .set('index', basal.log_index); cfg.tzoUtil.fillInUTCInfo(postbasal, basal.jsDate); if (basal.detail.temp_basal_percent != null) { - const suppressed = cfg.builder.makeScheduledBasal() - .with_rate(common.fixFloatingPoint( + const suppressed = { + type: 'basal', + deliveryType: 'scheduled', + rate: common.fixFloatingPoint( basal.detail.basal_rate_units_per_hour / basal.detail.temp_basal_percent, 2, - )) - .with_deviceTime(basal.deviceTime) - .with_time(postbasal.time) - .with_timezoneOffset(postbasal.timezoneOffset) - .with_conversionOffset(postbasal.conversionOffset) - .with_duration(basal.detail.duration_msec); + ), + }; + + if (basal.detail.temp_basal_percent === 0) { + suppressed.rate = 0; + } + postbasal.with_percent(basal.detail.temp_basal_percent) .set('suppressed', suppressed); } @@ -1446,7 +1466,7 @@ module.exports = (config) => { let postreschange = null; for (let s = 0; s < suspendrecs.length; ++s) { const suspend = data.log_records[suspendrecs[s]]; - postsuspend = cfg.builder.makeDeviceEventSuspend() + postsuspend = cfg.builder.makeDeviceEventSuspendResume() .with_deviceTime(suspend.deviceTime) // the spec doesn't really specify circumstances under which suspends happen // i.e., 'manual' reason code is an assumption here @@ -1458,7 +1478,6 @@ module.exports = (config) => { .with_reason({ suspended: 'manual' }) .set('index', suspend.log_index); cfg.tzoUtil.fillInUTCInfo(postsuspend, suspend.jsDate); - postsuspend = postsuspend.done(); if (suspend.rectype_name === 'Deactivate') { postreschange = cfg.builder.makeDeviceEventReservoirChange() .with_deviceTime(suspend.deviceTime) @@ -1468,10 +1487,11 @@ module.exports = (config) => { cfg.tzoUtil.fillInUTCInfo(postreschange, suspend.jsDate); postreschange = postreschange.done(); postrecords.push(postreschange); + } else { + postrecords.push(postsuspend); } - postrecords.push(postsuspend); - // we don't call .done() on the basal b/c it still needs duration added - // which happens in the simulator + // we don't call .done() on the suspend or basal b/c it still need + // duration added which happens in the simulator postbasal = cfg.builder.makeSuspendBasal() .with_deviceTime(suspend.deviceTime) .set('index', suspend.log_index); @@ -1603,6 +1623,11 @@ module.exports = (config) => { .set('index', bg.log_index); cfg.tzoUtil.fillInUTCInfo(postbg, bg.jsDate); let value = bg.detail.bg_reading; + + if (value > 65530) { + debug('Ignoring BG record with special value', value); + continue; + } if (hasFlag(BG_FLAGS.RANGE_ERROR_LOW_FLAG, bg.detail.flags)) { value = 19; annotate.annotateEvent(postbg, { code: 'bg/out-of-range', value: 'low', threshold: 20 }); @@ -1678,7 +1703,7 @@ module.exports = (config) => { } const bgTargetSettings = []; - const starts = _.sortBy(Object.keys(bgSettingsByStart), (start) => start); + const starts = _.sortBy(Object.keys(bgSettingsByStart), (start) => parseInt(start, 10)); for (let l = 0; l < starts.length; ++l) { const currentStart = starts[l]; @@ -2122,6 +2147,7 @@ module.exports = (config) => { progress(100); return cb(null, data); }, + 'dataservices', ); }, diff --git a/lib/drivers/insulet/insuletSimulator.js b/lib/drivers/insulet/insuletSimulator.js index f9b6c4138a..3df09a23ee 100644 --- a/lib/drivers/insulet/insuletSimulator.js +++ b/lib/drivers/insulet/insuletSimulator.js @@ -53,6 +53,7 @@ exports.make = (config = {}) => { let currBolus = null; let currStatus = null; let currTimestamp = null; + let suspendingEvent = null; function setActivationStatus(status) { activationStatus = status; @@ -70,6 +71,10 @@ exports.make = (config = {}) => { currStatus = status; } + function setSuspendingEvent(event) { + suspendingEvent = event; + } + function ensureTimestamp(e) { if (currTimestamp > e.time) { throw new Error( @@ -85,64 +90,49 @@ exports.make = (config = {}) => { events.push(e); } - function fillInSuppressed(e) { - let schedName = null; - let rate = null; - const suppressed = _.clone(e.suppressed); - if (e.previous != null) { - if (e.previous.deliveryType === 'scheduled') { - schedName = e.previous.scheduleName; - rate = e.previous.rate; - } else if (e.previous.deliveryType === 'temp') { - if (e.previous.suppressed != null) { - if (e.previous.suppressed.deliveryType === 'scheduled') { - schedName = e.previous.suppressed.scheduleName; - rate = e.previous.suppressed.rate; - } - } - } - } - - if (schedName != null) { - e.suppressed = suppressed.with_scheduleName(schedName); - - if (suppressed.rate == null || Number.isNaN(suppressed.rate)) { - // only use previous rate if rate not available - e.suppressed.with_rate(rate); - } - - e.suppressed = e.suppressed.done(); - } else { - delete e.suppressed; - } - } - return { alarm: (event) => { - if (event.payload != null && event.payload.stopsDelivery === true) { - if (event.status == null && event.index != null) { + if (event.payload != null && event.payload.stopsDelivery === true && event.index != null) { + if (event.status == null) { throw new Error('An Insulet alarm with a log index that has `stopsDelivery` in the payload must have a `status`.'); } + setSuspendingEvent(event); + } else { + simpleSimulate(event); } - simpleSimulate(event); }, basal: (event) => { ensureTimestamp(event); if (currBasal != null) { + if (currBasal.scheduleName && event.deliveryType === 'temp' && event.isAssigned('suppressed')) { + event.suppressed.scheduleName = currBasal.scheduleName; + } else if (currBasal.deliveryType === 'temp' && event.isAssigned('suppressed') && currBasal.isAssigned('suppressed')) { + event.suppressed.scheduleName = currBasal.suppressed.scheduleName; + } // sometimes there can be duplicate suspend basals, so we return early // if we come across a suspend basal when we're already in one if (currBasal.deliveryType === 'suspend' && event.deliveryType === 'suspend') { return; } + // if a device was suspended by a pod deactivation or an alarm, and we're + // resuming now, let's finish up that event and its status + if (suspendingEvent != null && event.deliveryType !== 'suspend') { + suspendingEvent.status = suspendingEvent.status.with_duration(Date.parse(event.time) - Date.parse(suspendingEvent.time)).done(); + suspendingEvent.status.reason.resumed = 'manual'; + events.push(suspendingEvent); + setSuspendingEvent(null); + } + // completing a resume event from a new pod activation // see podActivation() below for more details if (currStatus != null && currStatus.status === 'suspended') { if (activationStatus != null && activationStatus.status === 'resumed') { - const resume = activationStatus.with_previous(_.omit(currStatus, 'previous')) - .with_reason({ resumed: 'manual' }) + const resume = activationStatus.with_reason({ resumed: 'manual' }) .done(); + currStatus.reason.resumed = resume.reason.resumed; + currStatus.with_duration(Date.parse(resume.time) - Date.parse(currStatus.time)); + events.push(currStatus.done()); setCurrStatus(resume); - events.push(resume); setActivationStatus(null); } else if (event.deliveryType !== 'suspend') { // if we're in a suspend basal, we need to leave the currStatus because @@ -152,32 +142,33 @@ exports.make = (config = {}) => { setCurrStatus(null); } } - if (!currBasal.isAssigned('duration')) { - currBasal.with_duration(Date.parse(event.time) - Date.parse(currBasal.time)); - } - if (currBasal.isAssigned('suppressed')) { - fillInSuppressed(currBasal); - } + + // even though temp basals have a duration, it is the expected duration, not the + // actual duration, so we have to calculate the duration for all of them + currBasal.with_duration(Date.parse(event.time) - Date.parse(currBasal.time)); + simulations.truncateDuration(currBasal, 'insulet'); + currBasal = currBasal.done(); - event.previous = _.omit(currBasal, 'previous'); events.push(currBasal); } else if (activationStatus != null && activationStatus.status === 'resumed') { // at the very beginning of a file (which === when currBasal is null) it is common // to see a pod activation and then a basal; the former will end up as a `deviceEvent` - // resume without a `previous` but it's technically accurate, so we upload it anyway + // resume without a suspend but it's technically accurate, so we upload it anyway let initialResume; // we don't really expect this to happen, but just in case... if (currStatus != null && currStatus.status === 'suspended') { - initialResume = activationStatus.with_previous(_.omit(currStatus, 'previous')) - .with_reason({ resumed: 'manual' }) + initialResume = activationStatus.with_reason({ resumed: 'manual' }) .done(); + currStatus.reason.resumed = initialResume.reason.resumed; + currStatus.with_duration(Date.parse(initialResume.time) - Date.parse(currStatus.time)); + events.push(currStatus.done()); setCurrStatus(initialResume); - events.push(initialResume); } else { // this is the more common case, in which case we finish building a resume // that won't be connected with a suspend, kinda pointless, but accurate initialResume = activationStatus.with_reason({ resumed: 'manual' }) .done(); + annotate.annotateEvent(initialResume, 'status/incomplete-tuple'); setCurrStatus(initialResume); events.push(initialResume); } @@ -186,8 +177,34 @@ exports.make = (config = {}) => { setCurrBasal(event); }, bolus: (event) => { - simpleSimulate(event); - setCurrBolus(event); + if ((currBolus && currBolus.carryover) || event.aftercarryover) { + if ((currBolus.subType === 'dual/square' || currBolus.subType === 'square') && event.subType === 'square') { + debug('Combining', currBolus, 'with', event, 'after midnight'); + + // Combine extended bolus event that got split by Omnipod because it + // happened over midnight + if (event.normal) { + currBolus.normal += event.normal; + } + + if (event.extended) { + currBolus.extended += event.extended; + } + + if (event.duration) { + currBolus.duration += event.duration; + } + + currBolus.payload.logIndices.push(...event.payload.logIndices); + delete currBolus.carryover; + } else { + simpleSimulate(event); + setCurrBolus(event); + } + } else { + simpleSimulate(event); + setCurrBolus(event); + } }, /* * When an OmniPod users cancels a bolus partway through delivery, the data includes @@ -208,6 +225,12 @@ exports.make = (config = {}) => { } else if (event.durationLeft > 0) { currBolus.expectedExtended = common.fixFloatingPoint(currBolus.extended + event.missedInsulin, 2); currBolus.expectedDuration = currBolus.duration + event.durationLeft; + } else if (currBolus.normal > 0 && currBolus.duration === 0) { + // cancelled during up-front delivery + currBolus.expectedNormal = common.fixFloatingPoint(currBolus.normal + event.missedInsulin, 2); + } else if (currBolus.extended > 0) { + // it's possible for the duration left to be zero and still be cancelled + currBolus.expectedExtended = common.fixFloatingPoint(currBolus.extended + event.missedInsulin, 2); } else { currBolus.expectedNormal = common.fixFloatingPoint(currBolus.normal + event.missedInsulin, 2); } @@ -222,7 +245,7 @@ exports.make = (config = {}) => { if (event.status == null) { throw new Error('An Insulet `reservoirChange` event must have a `status`.'); } - simpleSimulate(event); + setSuspendingEvent(event); }, /* * We simulate the final basal in an Insulet .ibf file as a special case, to keep the logic @@ -234,9 +257,6 @@ exports.make = (config = {}) => { if (currBasal != null) { if (currBasal.deliveryType !== 'scheduled') { if (currBasal.deliveryType === 'temp') { - if (currBasal.isAssigned('suppressed')) { - fillInSuppressed(currBasal); - } currBasal = currBasal.done(); } else if (!currBasal.isAssigned('duration')) { currBasal.duration = 0; @@ -254,6 +274,14 @@ exports.make = (config = {}) => { } events.push(currBasal); } + + if (suspendingEvent != null) { + // suspending event has not been finalised yet + suspendingEvent.status.with_duration(0).done(); + annotate.annotateEvent(suspendingEvent.status, 'status/incomplete-tuple'); + events.push(suspendingEvent); + setSuspendingEvent(null); + } }, /* * Pod activations are not *quite* resume events (because further user intervention is @@ -268,11 +296,15 @@ exports.make = (config = {}) => { resume: (event) => { ensureTimestamp(event); if (currStatus != null && currStatus.status === 'suspended') { - event = event.with_previous(_.omit(currStatus, 'previous')); + currStatus.reason.resumed = event.reason.resumed; + currStatus.with_duration(Date.parse(event.time) - Date.parse(currStatus.time)); + events.push(currStatus.done()); + } else { + const resumeBasal = _.clone(event); + annotate.annotateEvent(resumeBasal, 'status/incomplete-tuple'); + events.push(resumeBasal.done()); } - event = event.done(); setCurrStatus(event); - events.push(event); }, pumpSettings: (event) => { simpleSimulate(event); @@ -297,10 +329,10 @@ exports.make = (config = {}) => { setActivationStatus(null); } setCurrStatus(event); - events.push(event); }, wizard: (event) => { simpleSimulate(event); + setCurrBolus(event.bolus); }, getEvents: () => { function filterOutZeroBoluses() { @@ -310,6 +342,14 @@ exports.make = (config = {}) => { // part of our data model, so we delete before uploading delete event.index; if (event.type === 'bolus') { + // there are instances where we can't combine a bolus that was split over + // midnight, for example where an extended bolus ran over midnight and just + // before midnight a normal bolus was also delivered, so make sure all + // carryover tags are removed before upload. Split boluses will still be + // annotated correctly with insulet/bolus/split-extended + delete event.carryover; + delete event.aftercarryover; + if (event.normal === 0 && !event.expectedNormal && !event.carbInput) { return false; } @@ -335,6 +375,36 @@ exports.make = (config = {}) => { // end result: we have to sort events again before we try to upload them const orderedEvents = _.sortBy(filterOutZeroBoluses(), (e) => e.time); + orderedEvents.forEach((item) => { + // there are edge cases where the PDM reports a basal or a bolus with + // an unusually long duration, so we set the duration to zero and + // annotate as unknown duration + if (item.type === 'basal' && item.duration > 604800000) { + item.duration = 0; + annotate.annotateEvent(item, 'basal/unknown-duration'); + } + + if (item.type === 'bolus' && item.duration > 86400000) { + item.duration = 0; + annotate.annotateEvent(item, 'bolus/unknown-duration'); + + if (item.expectedDuration > 86400000) { + _.set(item, 'payload.expectedDuration', item.expectedDuration); + delete item.expectedDuration; + } + } + + if (item.type === 'wizard' && item.bolus.duration > 86400000) { + item.bolus.duration = 0; + annotate.annotateEvent(item, 'bolus/unknown-duration'); + + if (item.bolus.expectedDuration > 86400000) { + _.set(item, 'payload.bolus.expectedDuration', item.bolus.expectedDuration); + delete item.bolus.expectedDuration; + } + } + }); + return orderedEvents; }, }; diff --git a/lib/drivers/insulet/jellyfish/insuletDriver.js b/lib/drivers/insulet/jellyfish/insuletDriver.js new file mode 100644 index 0000000000..e3e3471f4f --- /dev/null +++ b/lib/drivers/insulet/jellyfish/insuletDriver.js @@ -0,0 +1,2143 @@ +/* + * == BSD2 LICENSE == + * Copyright (c) 2014, Tidepool Project + * + * This program is free software; you can redistribute it and/or modify it under + * the terms of the associated License, which is identical to the BSD 2-Clause + * License as published by the Open Source Initiative at opensource.org. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the License for more details. + * + * You should have received a copy of the License along with this program; if + * not, you can obtain one from Tidepool Project at tidepool.org. + * == BSD2 LICENSE == + */ + +/* eslint-disable import/no-extraneous-dependencies, no-param-reassign, camelcase, + no-bitwise, no-continue, global-require, import/no-unresolved, object-curly-newline */ +import _ from 'lodash'; +import { promises as fs } from 'fs'; +import path from 'path'; +import sundial from 'sundial'; +import os from 'os'; + +import structJs from '../../../struct'; +import annotate from '../../../eventAnnotations'; +import common from '../common'; +import commonFunctions from '../../../commonFunctions'; +import logic from '../objectBuildingLogic'; +import insuletSimulatorMaker from './insuletSimulator'; +import TZOUtil from '../../../TimezoneOffsetUtil'; +import is from '../../../../app/utils/env'; + +const debug = !is.node ? require('bows')('InsuletDriver') : console.log; + +module.exports = (config) => { + const cfg = _.clone(config); + let buf; + let bytes; + const struct = structJs(); + + const BG_UNITS = 'mg/dL'; + + let isDash = false; // Dash PDM has different output values + + _.merge(cfg, { + deviceInfo: { + tags: ['insulin-pump'], + manufacturers: ['Insulet'], + }, + }); + + // all insulin unit readings are in .01 unit increments, so we divide by 100.0 to get units + // (multiplying by 0.01 tends to cause floating point issues) + const toUnits = (x) => x / 100.0; + + // Insulet reports percentage temp basals as +/- integers [-100, 100+] + // i.e., -50 for 50% of the scheduled basal rate + const convertTempPercentageToNetBasal = (x) => { + // 0 means there *wasn't* a temp basal in this record + if (x === 0) { + return null; + } + return 1.0 + (x / 100.0); + }; + + // This is a particularly weak checksum algorithm but that's what Insulet uses... + const weakChecksum = (offset, count) => { + let total = 0; + for (let i = 0; i < count; ++i) { + total += bytes[offset + i]; + } + return total & 0xFFFF; + }; + + const getRecord = (offset) => { + const recsize = struct.extractBEShort(bytes, offset); + let retval = {}; + + if (recsize) { + if ((recsize + offset) > bytes.length) { + return null; + } + retval = { + recsize, + packetlen: recsize + 2, + rawdata: new Uint8Array(buf, offset + 2, recsize - 2), + chksum: struct.extractBEShort(bytes, offset + recsize), + calcsum: weakChecksum(offset + 2, recsize - 2), + }; + retval.valid = (retval.calcsum === retval.chksum); + } else { + retval.valid = false; + } + return retval; + }; + + const addTimestamp = (o) => { + const dt = sundial.buildTimestamp(o); + if (dt) { + o.jsDate = dt; + o.deviceTime = sundial.formatDeviceTime(dt); + } + }; + + const decodeSerial = (encoded) => { + const phoneFamily = (encoded & 0x7E000000) >> 25; + const year = (encoded & 0x01E00000) >> 21; + const reset = (encoded & 0x001E0000) >> 17; + const sequence = (encoded & 0x1FFFC) >> 2; + + // eslint-disable-next-line prefer-template + const serialNumber = _.padStart(phoneFamily.toString(), 2, '0') + + _.padStart(year.toString(), 2, '0') + + _.padStart(reset.toString(), 2, '0') + + '-' + + _.padStart(sequence.toString(), 5, '0'); + return serialNumber; + }; + + // postprocess is a function that accepts a record, optionally modifies it, + // and returns null if the record is good, and a message if processing should halt. + const fixedRecords = { + ibf_version: { + format: '6S8z8z', + fields: [ + 'ibf_maj', 'ibf_min', 'ibf_patch', + 'eng_maj', 'eng_min', 'eng_patch', + 'vendorid', 'productid', + ], + postprocess: (rec) => { + if ((rec.ibf_maj === 0) && (rec.ibf_min >= 1) + && (rec.eng_maj === 0) && (rec.ibf_min >= 1) + && (rec.vendorid === 'Insulet') && (rec.productid === 'OmniPod')) { + return null; + } + return 'ibf_version record is incompatible with this driver.'; + }, + }, + pdm_version: { + format: '3S', + fields: [ + 'pdm_maj', 'pdm_min', 'pdm_patch', + ], + postprocess: (rec) => { + if (((rec.pdm_maj === 2) && (rec.pdm_min >= 3)) // Eros PDM + || ((rec.pdm_maj === 3) && (rec.pdm_min === 0))) { // Dash PDM + return null; + } + return 'pdm_version record is incompatible with this driver.'; + }, + }, + // this format gets rewritten before being used + mfg_data: { format: '??z', fields: ['data'] }, + basal_programs_hdr: { + format: '3S', + fields: ['num_progs', 'enabled_idx', 'max_name_size'], + }, + // this format gets rewritten before being used + basal_programs_name: { format: 'S??z', fields: ['index', 'name'] }, + eeprom_settings: { + format: '13.4i2b4.b5.b.bb8.i19.7b3sb19.bi', + fields: [ + 'BOLUS_INCR', + 'BOLUS_MAX', + 'BASAL_MAX', + 'LOW_VOL', + 'AUTO_OFF', + 'LANGUAGE', + 'EXPIRE_ALERT', + 'BG_REMINDER', + 'CONF_ALERT', + 'REMDR_ALERT', + 'REMOTE_ID', + 'TEMP_BAS_TYPE', + 'EXT_BOL_TYPE', + 'BOL_REMINDER', + 'BOL_CALCS', + 'BOL_CALCS_REVERSE', + 'BG_DISPLAY', + 'BG_SOUND', + 'BG_MIN', + 'BG_GOAL_LOW', + 'BG_GOAL_UP', + 'INSULIN_DURATION', + 'ALARM_REPAIR_COUNT', + 'PDM_CONFIG', + ], + postprocess: (rec) => { + rec.bolus_incr_units = toUnits(rec.BOLUS_INCR); + rec.bolus_max_units = toUnits(rec.BOLUS_MAX); + rec.basal_max_units = toUnits(rec.BASAL_MAX); + rec.low_vol_units = toUnits(rec.LOW_VOL); + if (isDash) { + rec.serial_number = decodeSerial(rec.REMOTE_ID); + } else { + rec.serial_number = rec.REMOTE_ID.toString(); + } + + if (rec.BG_DISPLAY === 0) { + rec.units = 'mg/dL'; + } else { + rec.units = 'mmol/L'; + } + rec.insulin_duration_msec = rec.INSULIN_DURATION * sundial.MIN30_TO_MSEC; + return null; + }, + }, + profile_hdr: { + format: 'b6.Si', + fields: [ + 'profile_idx', + 'error_code', + 'operation_time', + ], + }, + log_hdr: { + format: '7bS3b.S', + fields: [ + 'logs_info_revision', + 'insulin_history_revision', + 'alarm_history_revision', + 'blood_glucose_revision', + 'insulet_stats_revision', + 'day', + 'month', + 'year', + 'seconds', + 'minutes', + 'hours', + 'num_log_descriptions', + ], + }, + log_description: { + format: '5S2N', + fields: [ + 'log_index', 'backup', 'location', 'has_variable', 'record_size', + 'first_index', 'last_index', + ], + }, + log_record: { + format: 'bNSSbbsbbb.i', + fields: [ + 'log_id', 'log_index', 'record_size', 'error_code', + 'day', 'month', 'year', 'seconds', 'minutes', 'hours', + 'secs_since_powerup', + ], + }, + history_record: { + format: 'bNSSbbsbbb.ins..', + fields: [ + 'log_id', 'log_index', 'record_size', 'error_code', + 'day', 'month', 'year', 'seconds', 'minutes', 'hours', + 'secs_since_powerup', 'rectype', 'flags', + ], + }, + }; + + const logRecords = { + 0x0000: { + value: 0x0000, + name: 'End_Marker', + format: '', + fields: [], + }, + 0x0001: { + value: 0x0001, + name: 'Deactivate', + format: '', + fields: [], + }, + 0x0002: { + value: 0x0002, + name: 'Time_Change', + format: '3b.', + fields: [ + 'seconds', + 'minutes', + 'hours', + ], + }, + 0x0004: { + value: 0x0004, + name: 'Bolus', + format: 'ishs', + fields: [ + 'volume', + 'extended_duration_minutes', + 'calculation_record_offset', + 'immediate_duration_seconds', + ], + postprocess: (rec) => { + rec.volume_units = toUnits(rec.volume); + // this occurs if an extended bolus is programmed but the bolus is interrupted + // before any of the extended portion is delivered! + if (rec.extended_duration_minutes === 65535) { + rec.extended_duration_msec = 0; + } else if (rec.extended_duration_minutes === 0) { + // a zero *here* means that this isn't an extended bolus + // and we need to distinguish between two types of zeros - + // dual-wave boluses interrupted before any of the extended was + // delivered (i.e., the case above ^) or non-extended boluses (here) + // so here we use null instead of 0 + rec.extended_duration_msec = null; + } else { + rec.extended_duration_msec = rec.extended_duration_minutes * sundial.MIN_TO_MSEC; + } + rec.immediate_duration_msec = rec.immediate_duration_seconds * sundial.SEC_TO_MSEC; + }, + }, + 0x0008: { + value: 0x0008, + name: 'Basal_Rate', + format: 'ish', + fields: [ + 'basal_rate', 'duration', 'percent', + ], + postprocess: (rec) => { + rec.basal_rate_units_per_hour = toUnits(rec.basal_rate); + rec.duration_msec = rec.duration * sundial.MIN_TO_MSEC; + rec.temp_basal_percent = convertTempPercentageToNetBasal(rec.percent); + }, + }, + 0x0010: { + value: 0x0010, + name: 'Suspend', + format: '', + fields: [], + }, + 0x0020: { + value: 0x0020, + name: 'Date_Change', + format: 'bbs', + fields: [ + 'day', + 'month', + 'year', + ], + }, + 0x0040: { + value: 0x0040, + name: 'Suggested_Calc', + format: '4in3i6s', + fields: [ + 'correction_delivered', 'carb_bolus_delivered', + 'correction_programmed', 'carb_bolus_programmed', + 'correction_suggested', 'carb_bolus_suggested', + 'correction_iob', 'meal_iob', + 'correction_factor_used', 'current_bg', + 'target_bg', 'bg_correction_threshold', + 'carb_grams', 'ic_ratio_used', + ], + postprocess: (rec) => { + rec.corr_units_delivered = toUnits(rec.correction_delivered); + rec.carb_bolus_units_delivered = toUnits(rec.carb_bolus_delivered); + rec.corr_units_programmed = toUnits(rec.correction_programmed); + rec.carb_bolus_units_programmed = toUnits(rec.carb_bolus_programmed); + rec.corr_units_suggested = toUnits(rec.correction_suggested); + rec.carb_bolus_units_suggested = toUnits(rec.carb_bolus_suggested); + rec.corr_units_iob = toUnits(rec.correction_iob); + rec.meal_units_iob = toUnits(rec.meal_iob); + + if (rec.current_bg === 65535) { + // if bg was not given to the wizard, don't report it. + rec.current_bg = null; + } + + if (rec.carb_grams === 65535) { + // if carb count was not given to the wizard, don't report it. + rec.carb_grams = null; + } + + if (isDash) { + rec.correction_factor_used /= 100.0; + rec.ic_ratio_used /= 10.0; + rec.carb_grams /= 10.0; + } + }, + }, + 0x0080: { + value: 0x0080, + name: 'Remote_Hazard_Alarm', + format: '2bs3b.4s', + fields: [ + 'day', 'month', 'year', 'seconds', 'minutes', 'hours', + 'alarm_type', 'file_number', 'line_number', 'error_code', + ], + }, + 0x0400: { + value: 0x0400, + name: 'Alarm', + format: '2bs3b.4s', + fields: [ + 'day', 'month', 'year', 'seconds', 'minutes', 'hours', + 'alarm_type', 'file_number', 'line_number', 'error_code', + ], + }, + 0x0800: { + value: 0x0800, + name: 'Blood_Glucose', + format: 'is24z24zb.', + fields: [ + 'error_code', 'bg_reading', + 'user_tag_1', 'user_tag_2', + 'flags', + ], + }, + 0x1000: { + value: 0x1000, + name: 'Carb', + format: 'sbb', + fields: [ + 'carbs', + 'was_preset', + 'preset_type', + ], + }, + 0x2000: { + value: 0x2000, + name: 'Terminate_Bolus', + format: 'is', + fields: [ + 'insulin_left', + 'time_left_minutes', + ], + postprocess: (rec) => { + rec.insulin_units_left = toUnits(rec.insulin_left); + rec.time_left_msec = rec.time_left_minutes * sundial.MIN_TO_MSEC; + }, + }, + 0x4000: { + value: 0x4000, + name: 'Terminate_Basal', + format: 's', + fields: [ + 'time_left_minutes', + ], + postprocess: (rec) => { + rec.time_left_msec = rec.time_left_minutes * sundial.MIN_TO_MSEC; + }, + }, + 0x8000: { + value: 0x8000, + name: 'Activate', + format: '2S6b', + fields: [ + 'lot_number', 'serial_number', + 'pod_maj', 'pod_min', 'pod_patch', + 'interlock_maj', 'interlock_min', 'interlock_patch', + ], + }, + 0x10000: { + value: 0x10000, + name: 'Resume', + format: '', + fields: [], + }, + 0x20000: { + value: 0x20000, + name: 'Download', + format: '', + fields: [], + }, + 0x40000: { + value: 0x40000, + name: 'Occlusion', + format: '', + fields: [], + }, + }; + + const PROFILES = { + carbRatio: { + value: 11, + name: 'carbRatio', + mfrname: 'IC Ratio', + isBasal: false, + keyname: 'amount', + valuename: 'value', + }, + insulinSensitivity: { + value: 12, + name: 'insulinSensitivity', + mfrname: 'Correction', + isBasal: false, + keyname: 'amount', + valuename: 'value', + }, + bgTarget: { + value: 13, + name: 'bgTarget', + mfrname: 'Target BG', + isBasal: false, + keyname: 'low', + valuename: 'value', + }, + bgThreshold: { + value: 14, + name: 'bgThreshold', + mfrname: 'BG Threshold', + isBasal: false, + keyname: 'amount', + valuename: 'value', + }, + basalprofile0: { + value: 15, + name: 'basalprofile0', + mfrname: 'Basal Profile 0', + isBasal: true, + keyname: 'rate', + valuename: 'units', + }, + basalprofile1: { + value: 16, + name: 'basalprofile1', + mfrname: 'Basal Profile 1', + isBasal: true, + keyname: 'rate', + valuename: 'units', + }, + basalprofile2: { + value: 17, + name: 'basalprofile2', + mfrname: 'Basal Profile 2', + isBasal: true, + keyname: 'rate', + valuename: 'units', + }, + basalprofile3: { + value: 18, + name: 'basalprofile3', + mfrname: 'Basal Profile 3', + isBasal: true, + keyname: 'rate', + valuename: 'units', + }, + basalprofile4: { + value: 19, + name: 'basalprofile4', + mfrname: 'Basal Profile 4', + isBasal: true, + keyname: 'rate', + valuename: 'units', + }, + basalprofile5: { + value: 20, + name: 'basalprofile5', + mfrname: 'Basal Profile 5', + isBasal: true, + keyname: 'rate', + valuename: 'units', + }, + basalprofile6: { + value: 21, + name: 'basalprofile6', + mfrname: 'Basal Profile 6', + isBasal: true, + keyname: 'rate', + valuename: 'units', + }, + }; + + const pump_alarm_record = { + format: '2bs3b.b.b.2i6b', + fields: [ + 'day', 'month', 'year', 'seconds', 'minutes', 'hours', + 'alarm', 'error_code', 'lot_number', 'seq_number', + 'processor_maj', 'processor_min', 'processor_patch', + 'interlock_maj', 'interlock_min', 'interlock_patch', + ], + }; + + const BG_FLAGS = { + MANUAL_FLAG: { value: 0x01, name: 'MANUAL_FLAG' }, + TEMPERATURE_FLAG: { value: 0x02, name: 'TEMPERATURE_FLAG' }, + BELOW_TARGET_FLAG: { value: 0x04, name: 'BELOW_TARGET_FLAG' }, + ABOVE_TARGET_FLAG: { value: 0x08, name: 'ABOVE_TARGET_FLAG' }, + RANGE_ERROR_LOW_FLAG: { value: 0x10, name: 'RANGE_ERROR_LOW_FLAG' }, + RANGE_ERROR_HIGH_FLAG: { value: 0x20, name: 'RANGE_ERROR_HIGH_FLAG' }, + OTHER_ERROR_FLAG: { value: 0x40, name: 'OTHER_ERROR_FLAG' }, + }; + + const PDM_CONFIG_FLAGS = { + SUGGESTED_BOLUS_STYLE: { value: 0x01, name: 'SUGGESTED_BOLUS_STYLE' }, + PRODUCT_ID: { mask: 0x1E, shift: 1, name: 'PRODUCT_ID' }, + LOT_TID_SUPPORT: { value: 0x20, name: 'LOT_TID_SUPPORT' }, + BG_BOARD_TYPE: { mask: 0x3C0, shift: 6, name: 'BG_BOARD_TYPE' }, + }; + + const BG_BOARD_TYPES = { + 0: { name: 'Abbott FreeStyle', highest: 500 }, + 2: { name: 'LifeScan Verio', highest: 600 }, + 3: { name: 'None', highest: 600 }, + }; + + const LOG_FLAGS = { + // TODO: look for this flag and use it to identify + // extended boluses split over midnight instead of uploading + // them separately and annotating the extended with the + // 'insulet/bolus/split-extended' code as we're doing now + CARRY_OVER_FLAG: { value: 0x01, name: 'CARRY_OVER_FLAG' }, + NEW_DAY_FLAG: { value: 0x02, name: 'NEW_DAY_FLAG' }, + // TODO: we should probably look for this flag on all records + // and maybe annotate when we find it + IN_PROGRESS_FLAG: { value: 0x04, name: 'IN_PROGRESS_FLAG' }, + END_DAY_FLAG: { value: 0x08, name: 'END_DAY_FLAG' }, + // TODO: we should probably look for this flag on all records + // and either annotate or maybe even discard the record when we find it + UNCOMFIRMED_FLAG: { value: 0x10, name: 'UNCOMFIRMED_FLAG' }, + REVERSE_CORR_FLAG: { value: 0x0100, name: 'REVERSE_CORR_FLAG' }, + MAX_BOLUS_FLAG: { value: 0x0200, name: 'MAX_BOLUS_FLAG' }, + // filter out records marked with ERROR flag as + // the spec says these are "deleted" and should be ignored + ERROR: { value: 0x80000000, name: 'ERROR' }, + }; + + const LOG_TYPES = { + HISTORY: { value: 0x03, name: 'HISTORY' }, + PUMP_ALARM: { value: 0x05, name: 'PUMP_ALARM' }, + DELETED: { mask: 0x80000000, name: 'DELETED' }, + // this is an impossible value for a 1-byte LOG_TYPE + IGNORE: { value: 0x100, name: 'IGNORED by driver' }, + }; + + const ALARM_TYPES = { + AlrmPDM_ERROR0: { value: 0, name: 'AlrmPDM_ERROR0', explanation: 'PDM error', stopsDelivery: 'unknown' }, + AlrmPDM_ERROR1: { value: 1, name: 'AlrmPDM_ERROR1', explanation: 'PDM error', stopsDelivery: 'unknown' }, + AlrmPDM_ERROR2: { value: 2, name: 'AlrmPDM_ERROR2', explanation: 'PDM error', stopsDelivery: 'unknown' }, + AlrmPDM_ERROR3: { value: 3, name: 'AlrmPDM_ERROR3', explanation: 'PDM error', stopsDelivery: 'unknown' }, + AlrmPDM_ERROR4: { value: 4, name: 'AlrmPDM_ERROR4', explanation: 'PDM error', stopsDelivery: 'unknown' }, + AlrmPDM_ERROR5: { value: 5, name: 'AlrmPDM_ERROR5', explanation: 'PDM error', stopsDelivery: 'unknown' }, + AlrmPDM_ERROR6: { value: 6, name: 'AlrmPDM_ERROR6', explanation: 'PDM error', stopsDelivery: 'unknown' }, + AlrmPDM_ERROR7: { value: 7, name: 'AlrmPDM_ERROR7', explanation: 'PDM error', stopsDelivery: 'unknown' }, + AlrmPDM_ERROR8: { value: 8, name: 'AlrmPDM_ERROR8', explanation: 'PDM error', stopsDelivery: 'unknown' }, + AlrmPDM_ERROR9: { value: 9, name: 'AlrmPDM_ERROR9', explanation: 'PDM error', stopsDelivery: 'unknown' }, + AlrmSYSTEM_ERROR10: { value: 10, name: 'AlrmSYSTEM_ERROR10', explanation: 'system error', stopsDelivery: false }, + AlrmSYSTEM_ERROR12: { value: 12, name: 'AlrmSYSTEM_ERROR12', explanation: 'system error', stopsDelivery: 'unknown' }, + AlrmHAZ_REMOTE: { value: 13, name: 'AlrmHAZ_REMOTE', explanation: 'clock reset alarm', stopsDelivery: false }, + AlrmHAZ_PUMP_VOL: { value: 14, name: 'AlrmHAZ_PUMP_VOL', explanation: 'empty reservoir', stopsDelivery: true }, + AlrmHAZ_PUMP_AUTO_OFF: { value: 15, name: 'AlrmHAZ_PUMP_AUTO_OFF', explanation: 'auto-off', stopsDelivery: true }, + AlrmHAZ_PUMP_EXPIRED: { value: 16, name: 'AlrmHAZ_PUMP_EXPIRED', explanation: 'pod expired', stopsDelivery: true }, + AlrmHAZ_PUMP_OCCL: { value: 17, name: 'AlrmHAZ_PUMP_OCCL', explanation: 'pump site occluded', stopsDelivery: true }, + AlrmHAZ_PUMP_ACTIVATE: { value: 18, name: 'AlrmHAZ_PUMP_ACTIVATE', explanation: 'pod is a lump of coal', stopsDelivery: false }, + AlrmADV_KEY: { value: 21, name: 'AlrmADV_KEY', explanation: 'PDM stuck key detected', stopsDelivery: false }, + AlrmADV_PUMP_VOL: { value: 23, name: 'AlrmADV_PUMP_VOL', explanation: 'low reservoir', stopsDelivery: false }, + AlrmADV_PUMP_AUTO_OFF: { value: 24, name: 'AlrmADV_PUMP_AUTO_OFF', explanation: '15 minutes to auto-off warning', stopsDelivery: false }, + AlrmADV_PUMP_SUSPEND: { value: 25, name: 'AlrmADV_PUMP_SUSPEND', explanation: 'suspend done', stopsDelivery: false }, + AlrmADV_PUMP_EXP1: { value: 26, name: 'AlrmADV_PUMP_EXP1', explanation: 'pod expiration advisory', stopsDelivery: false }, + AlrmADV_PUMP_EXP2: { value: 27, name: 'AlrmADV_PUMP_EXP2', explanation: 'pod expiration alert', stopsDelivery: false }, + AlrmSYSTEM_ERROR28: { value: 28, name: 'AlrmSYSTEM_ERROR28', explanation: 'system error', stopsDelivery: 'unknown' }, + AlrmEXP_WARNING: { value: 37, name: 'AlrmEXP_WARNING', explanation: 'pod expiration advisory', stopsDelivery: false }, + AlrmHAZ_PDM_AUTO_OFF: { value: 39, name: 'AlrmHAZ_PDM_AUTO_OFF', explanation: 'auto-off', stopsDelivery: true }, + }; + + const LOG_ERRORS = { + eLogNoErr: { value: 0, name: 'eLogNoErr' }, + eLogGetEEPROMErr: { value: 3, name: 'eLogGetEEPROMErr' }, + eLogCRCErr: { value: 4, name: 'eLogCRCErr' }, + eLogLogIndexErr: { value: 6, name: 'eLogLogIndexErr' }, + eLogRecSizeErr: { value: 8, name: 'eLogRecSizeErr' }, + }; + + const TEMP_BASAL_TYPES = { + 0: 'off', + 1: 'percent', + 2: 'Units/hour', + }; + + const getItemWithValue = (list, itemname, valuename, value) => { + // eslint-disable-next-line no-restricted-syntax + for (const i in list) { + if (list[i][valuename] === value) { + return list[i][itemname]; + } + } + return null; + }; + + const getNameForValue = (list, v) => getItemWithValue(list, 'name', 'value', v); + + const getValueForName = (list, n) => getItemWithValue(list, 'value', 'name', n); + + const getFlagNames = (list, v) => { + const flags = []; + // eslint-disable-next-line no-restricted-syntax + for (const i in list) { + if (list[i].value & v) { + flags.push(list[i].name); + } + } + return flags.join('|'); + }; + + const hasFlag = (flag, v) => { + if (flag.value & v) { + return true; + } + return false; + }; + + const getFixedRecord = (recname, offset) => { + const rec = getRecord(offset); + const decoded = struct.unpack(rec.rawdata, 0, + fixedRecords[recname].format, fixedRecords[recname].fields); + _.assign(rec, decoded); + return rec; + }; + + const getManufacturingData = (offset) => { + const rec = getRecord(offset); + const fmt = `${rec.recsize}z`; + const decoded = struct.unpack(rec.rawdata, 0, fmt, fixedRecords.mfg_data.fields); + _.assign(rec, decoded); + return rec; + }; + + const getBasalProgramNames = (offset) => { + const basalProgramsHeader = getFixedRecord('basal_programs_hdr', offset); + const basalProgramNames = []; + for (let i = 0; i < 7; ++i) { + const prgOffset = 6 + i * (2 + basalProgramsHeader.max_name_size); + // the format for the name data is dependent on the name size + fixedRecords.basal_programs_name.format = + `S${basalProgramsHeader.max_name_size}z`; + const prog = struct.unpack(basalProgramsHeader.rawdata, prgOffset, + fixedRecords.basal_programs_name.format, + fixedRecords.basal_programs_name.fields); + prog.name = prog.name.replace(/\./g, '-'); + prog.name = prog.name.replace(/\$/g, ''); + basalProgramNames.push(prog); + } + basalProgramsHeader.names = basalProgramNames; + return basalProgramsHeader; + }; + + const getProfiles = (offset, basalProgramNames) => { + const profiles = []; + let totalLen = 0; + // there are 11 profiles in a row + for (let i = 0; i < 11; ++i) { + // profiles consist of a header plus 48 integers + const profile = getFixedRecord('profile_hdr', offset); + profile.standard_name = getItemWithValue(PROFILES, 'mfrname', 'value', profile.profile_idx); + if (getItemWithValue(PROFILES, 'isBasal', 'value', profile.profile_idx)) { + // user-assigned name is at an index in the program names + // relative to the profile index, which starts at 15 in + // insulet data + profile.name = basalProgramNames[profile.profile_idx - 15].name; + } + profile.steps = []; + const step_offset = struct.structlen(fixedRecords.profile_hdr.format); + for (let j = 0; j < 48; ++j) { + let val = struct.extractInt(profile.rawdata, step_offset + j * 4); + if (isDash) { + if (profile.standard_name === PROFILES.carbRatio.mfrname) { + val /= 10.0; + } + } + + profile.steps.push({ + // profile steps are every 30 minutes starting at midnight + // convert them to milliseconds + starttime: j * sundial.MIN30_TO_MSEC, + value: val, + }); + // if it's a basal profile, add the units conversion + if (getItemWithValue(PROFILES, 'isBasal', 'value', profile.profile_idx)) { + profile.steps[j].units = toUnits(profile.steps[j].value); + } + } + profiles.push(profile); + offset += profile.packetlen; + totalLen += profile.packetlen; + } + profiles.packetlen = totalLen; + return profiles; + }; + + const getLogDescriptions = (offset) => { + const logDescriptions = getFixedRecord('log_hdr', offset); + logDescriptions.descs = []; + addTimestamp(logDescriptions); + for (let i = 0; i < logDescriptions.num_log_descriptions; ++i) { + const descOffset = 15 + i * 18; + const desc = struct.unpack(logDescriptions.rawdata, descOffset, + fixedRecords.log_description.format, + fixedRecords.log_description.fields); + logDescriptions.descs.push(desc); + } + return logDescriptions; + }; + + const getLogRecord = (offset) => { + const rec = getRecord(offset); + if (rec === null) { + // packet is incomplete + return null; + } + let logheader = struct.unpack(rec.rawdata, 0, + fixedRecords.log_record.format, + fixedRecords.log_record.fields); + if (logheader.log_id === LOG_TYPES.HISTORY.value) { // history + logheader = struct.unpack(rec.rawdata, 0, + fixedRecords.history_record.format, + fixedRecords.history_record.fields); + if (logheader.error_code) { + logheader.error_text = getNameForValue(LOG_ERRORS, logheader.error_code); + } + if (logheader.flags !== 0) { + logheader.flag_text = getFlagNames(LOG_FLAGS, logheader.flags); + } + } else { + // There are other record types but we don't have documentation on them, + // so we're going to ignore them. + } + _.assign(rec, logheader); + if (rec.rectype & LOG_TYPES.DELETED.mask) { + // this is a deleted record so we're going to only return + // a deleted flag and a size + return { rectype: LOG_TYPES.IGNORE.value, packetlen: rec.packetlen }; + } + // now process further data, if there is any + if (rec.log_id === LOG_TYPES.HISTORY.value) { + if (logRecords[rec.rectype]) { + if (rec.rectype !== 0) { + addTimestamp(rec); + } + rec.rectype_name = logRecords[rec.rectype].name; + const detail = struct.unpack(rec.rawdata, + struct.structlen(fixedRecords.history_record.format), + logRecords[rec.rectype].format, + logRecords[rec.rectype].fields); + if (logRecords[rec.rectype].postprocess) { + logRecords[rec.rectype].postprocess(detail); + } + rec.detail = detail; + } else { + debug('Unknown history record type %d', rec.rectype); + } + } else if (rec.log_id === LOG_TYPES.PUMP_ALARM.value) { + rec.alarm = struct.unpack(rec.rawdata, + struct.structlen(fixedRecords.log_record.format), + pump_alarm_record.format, + pump_alarm_record.fields); + addTimestamp(rec.alarm); + rec.alarm.alarm_text = getNameForValue(ALARM_TYPES, rec.alarm.alarm); + } else { + // all other log types are meaningless to us, we're told + return { rectype: LOG_TYPES.IGNORE.value, packetlen: rec.packetlen }; + } + return rec; + }; + + const getLogRecords = (recordset) => { + // this is where we get the position-independent information + let offset = recordset.independent_offset; + let done = false; + const log_records = []; + let index = 0; + while (!done) { + const rec = getLogRecord(offset); + if (rec === null) { + // final packet is incomplete + done = true; + } else { + offset += rec.packetlen; + if (offset >= bytes.length) { + done = true; + } + if (rec.rectype === LOG_TYPES.IGNORE.value) { + continue; + } + if (rec.error_code) { + // according to the spec, a record with an error code should not be parsed + debug(`logRecord error (${rec.error_text}) at ${rec.deviceTime}, dropping.`); + } else { + index += 1; + rec.index = index; + log_records.push(rec); + } + } + } + + return log_records; + }; + + // returns indices of the matching records + const findSpecificRecords = (recordlist, rectypelist) => { + const result = []; + for (let i = 0; i < recordlist.length; ++i) { + for (let j = 0; j < rectypelist.length; ++j) { + if (recordlist[i].rectype === rectypelist[j]) { + result.push(i); + } + } + } + return result.sort((a, b) => recordlist[a].log_index - recordlist[b].log_index); + }; + + // these aren't history records, so we have to find them separately + const findPumpAlarmRecords = (recordlist) => { + const result = []; + for (let i = 0; i < recordlist.length; ++i) { + // log_index of -1 doesn't have a timestamp, not a valid record + if (recordlist[i].alarm != null && recordlist[i].log_index >= 0) { + result.push(i); + } + } + return result.sort((a, b) => recordlist[a].log_index - recordlist[b].log_index); + }; + + const linkWizardRecords = (data, bolusrecs) => { + // we need to see if two (or more!) boluses come from the same calculation (wizard) record + // if so, they're actually a dual bolus, and need to be consolidated + // so we create a table of backlinks from the calc records to the bolus records that + // refer to them + const wizRecords = {}; + for (let b = 0; b < bolusrecs.length; ++b) { + const bolus = data.log_records[bolusrecs[b]]; + let wiz_idx; + // these are boluses not linked with wizard records (i.e., quick boluses) + // but they could be dual-wave boluses with a normal and square component + // so we use the UTC timestamp to index them + if (bolus.detail.calculation_record_offset === 0) { + bolus.index = bolus.log_index; + cfg.tzoUtil.fillInUTCInfo(bolus, bolus.jsDate); + wiz_idx = bolus.time; + } else { + wiz_idx = bolusrecs[b] + bolus.detail.calculation_record_offset; + } + + let newOffset = null; + if (isDash && hasFlag(LOG_FLAGS.CARRY_OVER_FLAG, bolus.flags)) { + const sugg_calc = data.log_records[wiz_idx]; + if (sugg_calc && sugg_calc.hours > 0 && bolus.detail.extended_duration_msec !== null) { + // wizard record for extended bolus should be at midnight, but isn't, + // so we have to assume that the offset was not calculated correctly by the PDM + + newOffset = bolusrecs[b]; + // search for correct offset + while (data.log_records[newOffset].hours !== 0 || + data.log_records[newOffset].rectype_name !== 'Suggested_Calc') { + newOffset -= 1; + } + wiz_idx = newOffset; + } + } + const r = wizRecords[wiz_idx] || {}; + if (newOffset && bolus.detail.immediate_duration_msec > 0) { + // also correct immediate portion and remove incorrect offset from PDM + r.immediate = bolusrecs[b] + 1; + const oldRecord = _.find(wizRecords, (element) => element.immediate === r.immediate); + if (oldRecord) { + delete oldRecord.immediate; + } + } + if (bolus.detail.extended_duration_msec !== null || + (r.immediate && bolus.detail.extended_duration_msec === null && bolus.detail.immediate_duration_msec === 0)) { + // the extended portion of a dual-wave bolus is split into two records + // if it crosses local (deviceTime) midnight, and for cancelled extended + // boluses and extended portions after midnight the duration can be zero + if (r.extended != null) { + r.extended2 = bolusrecs[b]; + } else { + r.extended = bolusrecs[b]; + } + } else { + r.immediate = bolusrecs[b]; + } + if (r.immediate && r.extended) { + r.isDual = true; + } + wizRecords[wiz_idx] = r; + } + return wizRecords; + }; + + const buildAlarmRecords = (data, records) => { + let alarmrecs = findSpecificRecords(data.log_records, [ + getValueForName(logRecords, 'Remote_Hazard_Alarm'), + getValueForName(logRecords, 'Alarm'), + ]); + const pumpAlarms = findPumpAlarmRecords(data.log_records); + alarmrecs = alarmrecs.concat(pumpAlarms); + const postrecords = []; + + function makeSuspended(anAlarm) { + // only build a suspended in conjunction with a stopsDelivery alarm + // if the alarm is a history record with a log index + // otherwise we can't be certain of our conversion to UTC + // and thus really don't want to be mucking with the basal events stream + if (anAlarm.detail && anAlarm.log_index) { + const suspend = cfg.builder.makeDeviceEventSuspend() + .with_deviceTime(anAlarm.deviceTime) + .with_reason({ suspended: 'automatic' }); + suspend.set('index', anAlarm.log_index); + cfg.tzoUtil.fillInUTCInfo(suspend, anAlarm.jsDate); + return suspend.done(); + } + return null; + } + + function makeSuspendBasal(anAlarm) { + // we don't call .done() on the basal b/c it still needs duration added + // which happens in the simulator + // only build a suspended in conjunction with a stopsDelivery alarm + // if the alarm is a history record with a log index + // otherwise we can't be certain of our conversion to UTC + // and thus really don't want to be mucking with the basal events stream + if (anAlarm.detail && anAlarm.log_index) { + const basal = cfg.builder.makeSuspendBasal() + .with_deviceTime(anAlarm.deviceTime || anAlarm.alarm.deviceTime); + basal.set('index', anAlarm.log_index); + cfg.tzoUtil.fillInUTCInfo(basal, anAlarm.jsDate || anAlarm.alarm.jsDate); + return basal; + } + return null; + } + + for (let a = 0; a < alarmrecs.length; ++a) { + const alarm = data.log_records[alarmrecs[a]]; + let postalarm = null; + let postsuspend = null; + let postbasal = null; + let alarmValue = null; + + postalarm = cfg.builder.makeDeviceEventAlarm() + .with_deviceTime(alarm.deviceTime || alarm.alarm.deviceTime); + cfg.tzoUtil.fillInUTCInfo(postalarm, alarm.jsDate || alarm.alarm.jsDate); + // handle history-style alarms + if (alarm.detail) { + alarmValue = alarm.detail.alarm_type; + postalarm.set('index', alarm.log_index); + // handle non-history alarms + } else if (alarm.alarm.alarm_text != null) { + // alarm.alarm.alarm is not a typo! + alarmValue = alarm.alarm.alarm; + } else { + postalarm = null; + } + if (postalarm && postalarm.time === '**REQUIRED**') { + // will occur for alarms that aren't bootstrappable + // since we're no longer erroring on failure to look up UTC info + postalarm = null; + } + const alarmText = getNameForValue(ALARM_TYPES, alarmValue); + if (postalarm != null) { + switch (alarmValue) { + // alarmType `other` + // History - ALARM + case ALARM_TYPES.AlrmADV_KEY.value: + case ALARM_TYPES.AlrmEXP_WARNING.value: + case ALARM_TYPES.AlrmSYSTEM_ERROR10.value: + case ALARM_TYPES.AlrmSYSTEM_ERROR12.value: + case ALARM_TYPES.AlrmSYSTEM_ERROR28.value: + case ALARM_TYPES.AlrmPDM_ERROR0.value: + case ALARM_TYPES.AlrmPDM_ERROR1.value: + case ALARM_TYPES.AlrmPDM_ERROR2.value: + case ALARM_TYPES.AlrmPDM_ERROR3.value: + case ALARM_TYPES.AlrmPDM_ERROR4.value: + case ALARM_TYPES.AlrmPDM_ERROR5.value: + case ALARM_TYPES.AlrmPDM_ERROR6.value: + case ALARM_TYPES.AlrmPDM_ERROR7.value: + case ALARM_TYPES.AlrmPDM_ERROR8.value: + case ALARM_TYPES.AlrmPDM_ERROR9.value: // History - REMOTE HAZ + case ALARM_TYPES.AlrmHAZ_REMOTE.value: + case ALARM_TYPES.AlrmHAZ_PUMP_ACTIVATE.value: + case ALARM_TYPES.AlrmADV_PUMP_AUTO_OFF.value: + case ALARM_TYPES.AlrmADV_PUMP_SUSPEND.value: + case ALARM_TYPES.AlrmADV_PUMP_EXP1.value: + case ALARM_TYPES.AlrmADV_PUMP_EXP2.value: + postalarm = postalarm.with_alarmType('other') + .with_payload({ + alarmText, + explanation: ALARM_TYPES[alarmText].explanation, + stopsDelivery: ALARM_TYPES[alarmText].stopsDelivery, + }) + .done(); + break; + // Pump advisory and hazard alarm (non-History) + // alarmType `low_insulin` + case ALARM_TYPES.AlrmADV_PUMP_VOL.value: + postalarm = postalarm.with_alarmType('low_insulin') + .with_payload({ + alarmText, + explanation: ALARM_TYPES[alarmText].explanation, + stopsDelivery: ALARM_TYPES[alarmText].stopsDelivery, + }) + .done(); + break; + // alarmType `no_insulin` + case ALARM_TYPES.AlrmHAZ_PUMP_VOL.value: + postsuspend = makeSuspended(alarm); + postbasal = makeSuspendBasal(alarm); + postalarm = postalarm.with_alarmType('no_insulin') + .with_payload({ + alarmText: getNameForValue(ALARM_TYPES, alarmValue), + explanation: ALARM_TYPES[alarmText].explanation, + stopsDelivery: ALARM_TYPES[alarmText].stopsDelivery, + }) + .with_status(postsuspend) + .done(); + break; + // alarmType `occlusion` + case ALARM_TYPES.AlrmHAZ_PUMP_OCCL.value: + postsuspend = makeSuspended(alarm); + postbasal = makeSuspendBasal(alarm); + postalarm = postalarm.with_alarmType('occlusion') + .with_payload({ + alarmText: getNameForValue(ALARM_TYPES, alarmValue), + explanation: ALARM_TYPES[alarmText].explanation, + stopsDelivery: ALARM_TYPES[alarmText].stopsDelivery, + }) + .with_status(postsuspend) + .done(); + break; + // alarmType `no_delivery` + case ALARM_TYPES.AlrmHAZ_PUMP_EXPIRED.value: + postsuspend = makeSuspended(alarm); + postbasal = makeSuspendBasal(alarm); + postalarm = postalarm.with_alarmType('no_delivery') + .with_payload({ + alarmText: getNameForValue(ALARM_TYPES, alarmValue), + explanation: ALARM_TYPES[alarmText].explanation, + stopsDelivery: ALARM_TYPES[alarmText].stopsDelivery, + }) + .with_status(postsuspend) + .done(); + break; + // alarmType `auto_off` + case ALARM_TYPES.AlrmHAZ_PDM_AUTO_OFF.value: + case ALARM_TYPES.AlrmHAZ_PUMP_AUTO_OFF.value: + // TODO: clarify with Insulet or get data to figure out whether this (below) is + // a warning or the actual auto-off; the spec is confused + postsuspend = makeSuspended(alarm); + postbasal = makeSuspendBasal(alarm); + postalarm = postalarm.with_alarmType('auto_off') + .with_payload({ + alarmText: getNameForValue(ALARM_TYPES, alarmValue), + explanation: ALARM_TYPES[alarmText].explanation, + stopsDelivery: ALARM_TYPES[alarmText].stopsDelivery, + }) + .with_status(postsuspend) + .done(); + break; + // for alarm codes not documented in the spec + default: + if (postalarm) { + postalarm = postalarm.with_alarmType('other') + .done(); + } + break; + } + postrecords.push(postalarm); + } + if (postsuspend != null) { + postrecords.push(postsuspend); + } + if (postbasal != null) { + postrecords.push(postbasal); + } + } + return records.concat(postrecords); + }; + + const buildBolusRecords = (data, records) => { + const bolusrecs = findSpecificRecords(data.log_records, [ + getValueForName(logRecords, 'Bolus'), + ]); + const wizRecords = linkWizardRecords(data, bolusrecs); + const postrecords = []; + + for (let b = 0; b < bolusrecs.length; ++b) { + const bolus = data.log_records[bolusrecs[b]]; + let wiz_idx; + // quick boluses are indexed by UTC timestamp + if (bolus.detail.calculation_record_offset === 0) { + wiz_idx = bolus.time; + } else { + wiz_idx = bolusrecs[b] + bolus.detail.calculation_record_offset; + } + + let postbolus = null; + if (wizRecords[wiz_idx]) { + if (wizRecords[wiz_idx].immediate !== bolusrecs[b] && // check if bolus is linked to wizard record + wizRecords[wiz_idx].extended !== bolusrecs[b] && + wizRecords[wiz_idx].extended2 !== bolusrecs[b] && + data.log_records[wizRecords[wiz_idx].immediate].detail.volume_units !== 0) { // and that it hasn't been interrupted + debug(`Wizard record offset provided by PDM is incorrect: Bolus ${bolusrecs[b]} at ${bolus.deviceTime} with flag text ${bolus.flag_text} not found in wizard record ${wiz_idx} linked to ${JSON.stringify(wizRecords[wiz_idx])}`); + continue; + } + + // if we already did this dual or square bolus, skip it on the second round + if (wizRecords[wiz_idx].handled) { + continue; + } + + if (wizRecords[wiz_idx].isDual) { + const ext = wizRecords[wiz_idx].extended; + const ext2 = wizRecords[wiz_idx].extended2 || null; + const imm = wizRecords[wiz_idx].immediate; + postbolus = cfg.builder.makeDualBolus() + .with_normal(data.log_records[imm].detail.volume_units) + .with_deviceTime(data.log_records[imm].deviceTime) + .set('jsDate', data.log_records[imm].jsDate) + .set('index', data.log_records[imm].log_index); + cfg.tzoUtil.fillInUTCInfo(postbolus, data.log_records[imm].jsDate); + if (ext2 != null) { + postbolus = postbolus.with_extended(common.fixFloatingPoint( + data.log_records[ext].detail.volume_units + data.log_records[ext2].detail.volume_units, + 2, + )) + .with_duration( + data.log_records[ext].detail.extended_duration_msec + data.log_records[ext2].detail.extended_duration_msec, + ) + .done(); + } else { + if (data.log_records[ext].detail.extended_duration_msec === null) { + // extended bolus was cancelled with zero duration, with either zero or a + // very small extended portion delivered + postbolus = postbolus.with_extended(data.log_records[ext].detail.volume_units) + .with_duration(0); + } else { + postbolus = postbolus.with_extended(data.log_records[ext].detail.volume_units) + .with_duration(data.log_records[ext].detail.extended_duration_msec); + } + postbolus = postbolus.done(); + } + wizRecords[wiz_idx].handled = true; + } else if (bolus.detail.extended_duration_msec !== null) { + const square = wizRecords[wiz_idx].extended; + const square2 = wizRecords[wiz_idx].extended2 || null; + postbolus = cfg.builder.makeSquareBolus() + .with_deviceTime(data.log_records[square].deviceTime) + .set('jsDate', data.log_records[square].jsDate) + .set('index', data.log_records[square].log_index); + cfg.tzoUtil.fillInUTCInfo(postbolus, data.log_records[square].jsDate); + if (square2 != null) { + postbolus = postbolus.with_extended(common.fixFloatingPoint( + data.log_records[square].detail.volume_units + data.log_records[square2].detail.volume_units, + 2, + )) + .with_duration( + data.log_records[square].detail.extended_duration_msec + data.log_records[square2].detail.extended_duration_msec, + ).done(); + } else { + postbolus.with_extended(data.log_records[square].detail.volume_units) + .with_duration(data.log_records[square].detail.extended_duration_msec); + const millisInDay = sundial.getMsFromMidnight(postbolus.time, postbolus.timezoneOffset); + // extended boluses with timestamps at "precisely" (within 5 sec.) of midnight + // might actually be the second half of a previous dual- or square-wave bolus + // since Insulet always splits these records when they cross midnight + // when there isn't a wizard record to tie split records together, we don't + // know if this is a component of a split, so we annotate + if (millisInDay <= 5000 && !data.log_records[wiz_idx]) { + annotate.annotateEvent(postbolus, 'insulet/bolus/split-extended'); + } + postbolus = postbolus.done(); + } + wizRecords[wiz_idx].handled = true; + } else if (bolus.detail.immediate_duration_msec !== 0) { + postbolus = cfg.builder.makeNormalBolus() + .with_normal(bolus.detail.volume_units) + .with_deviceTime(bolus.deviceTime) + .set('jsDate', bolus.jsDate) + .set('index', bolus.log_index); + cfg.tzoUtil.fillInUTCInfo(postbolus, bolus.jsDate); + postbolus = postbolus.done(); + } else if (bolus.detail.volume_units !== 0) { + debug('Unexpected bolus of nonzero volume %d but zero duration!', bolus.detail.volume_units); + } else { + // we thought we could ignore zero-volume boluses, but it turns out they could + // be the result of an interrupted non-zero-volume bolus (when followed by a + // bolus termination) + postbolus = cfg.builder.makeNormalBolus() + .with_normal(bolus.detail.volume_units) + .with_deviceTime(bolus.deviceTime) + .set('jsDate', bolus.jsDate) + .set('index', bolus.log_index); + cfg.tzoUtil.fillInUTCInfo(postbolus, bolus.jsDate); + postbolus = postbolus.done(); + } + } + + if (postbolus) { + if (wizRecords[wiz_idx]) { + const wiz = data.log_records[wiz_idx] || {}; + // wiz will be empty if the bolus was a quick bolus + // and wiz.detail will be empty in various circumstances that share + // the common feature that the bolus attempting to link to the + // wizard record is the first data point (chronologically) in the + // log records + // presumably these wizard records are missing because this is + // the PDM's memory cut-off + if (!_.isEmpty(wiz) && !_.isEmpty(wiz.detail)) { + const payload = _.assign({}, wiz.detail); + const bg = wiz.detail.current_bg; + const carb = wiz.detail.carb_grams; + postbolus.carbInput = carb; /* we need this to delete zero boluses + * without wizard carbs in the simulator */ + let postwiz = cfg.builder.makeWizard() + .with_recommended({ + carb: wiz.detail.carb_bolus_units_suggested, + correction: wiz.detail.corr_units_suggested, + net: logic.calculateNetRecommendation(wiz.detail), + }) + .with_bgInput(bg) + .with_carbInput(carb) + .with_insulinCarbRatio(wiz.detail.ic_ratio_used) + .with_insulinSensitivity(wiz.detail.correction_factor_used) + .with_bolus(postbolus) + .with_payload(payload) + .with_deviceTime(postbolus.deviceTime) // use bolus time as wizard record may be 00h00 + .with_units(BG_UNITS) + .set('index', postbolus.index); + + if (wiz.detail.target_bg && wiz.detail.bg_correction_threshold) { + postwiz = postwiz.with_bgTarget({ + target: wiz.detail.target_bg, + high: wiz.detail.bg_correction_threshold, + }); + } + + if (wiz.detail.corr_units_iob || wiz.detail.meal_units_iob) { + postwiz = postwiz.with_insulinOnBoard( + common.fixFloatingPoint(wiz.detail.corr_units_iob + wiz.detail.meal_units_iob, 2), + ); + } + + // if an extended bolus continues past midnight, the wizard entry + // may have a timestamp of 00h00, so we use the time of the bolus + // itself as the timestamp + cfg.tzoUtil.fillInUTCInfo(postwiz, postbolus.jsDate); + postwiz = postwiz.done(); + postrecords.push(postwiz); + } + } + delete postbolus.jsDate; + postrecords.push(postbolus); + } + } + return records.concat(postrecords); + }; + + const buildBolusTerminations = (data, records) => { + const termrecs = findSpecificRecords(data.log_records, [ + getValueForName(logRecords, 'Terminate_Bolus'), + ]); + const postrecords = []; + let postterm = null; + for (let t = 0; t < termrecs.length; ++t) { + const term = data.log_records[termrecs[t]]; + // these get fed to the simulator but not uploaded (just used to modify other events) + // hence not using the object builder + postterm = { + type: 'termination', + subType: 'bolus', + deviceTime: term.deviceTime, + missedInsulin: term.detail.insulin_units_left, + durationLeft: term.detail.time_left_msec, + index: term.log_index, + }; + cfg.tzoUtil.fillInUTCInfo(postterm, term.jsDate); + postrecords.push(postterm); + } + return records.concat(postrecords); + }; + + const buildBasalRecords = (data, records) => { + const basalrecs = findSpecificRecords(data.log_records, [ + getValueForName(logRecords, 'Basal_Rate'), + ]); + const postrecords = []; + let postbasal = null; + for (let b = 0; b < basalrecs.length; ++b) { + const basal = data.log_records[basalrecs[b]]; + // for Tidepool's purposes, the 'duration' field of a scheduled basal is + // how long it's supposed to last. In this case, the answer is "until the next rate + // change" -- but it's NOT the duration field of the basal record; that's only for + // temp basals + if (basal.detail.duration === 0) { + postbasal = cfg.builder.makeScheduledBasal() + .with_scheduleName(data.basalPrograms.names[data.basalPrograms.enabled_idx].name) + .with_rate(basal.detail.basal_rate_units_per_hour) + .with_deviceTime(basal.deviceTime) + .set('index', basal.log_index); + cfg.tzoUtil.fillInUTCInfo(postbasal, basal.jsDate); + } else { + postbasal = cfg.builder.makeTempBasal() + .with_rate(basal.detail.basal_rate_units_per_hour) + .with_deviceTime(basal.deviceTime) + .with_duration(basal.detail.duration_msec) + .set('index', basal.log_index); + cfg.tzoUtil.fillInUTCInfo(postbasal, basal.jsDate); + if (basal.detail.temp_basal_percent != null) { + const suppressed = cfg.builder.makeScheduledBasal() + .with_rate(common.fixFloatingPoint( + basal.detail.basal_rate_units_per_hour / basal.detail.temp_basal_percent, + 2, + )) + .with_deviceTime(basal.deviceTime) + .with_time(postbasal.time) + .with_timezoneOffset(postbasal.timezoneOffset) + .with_conversionOffset(postbasal.conversionOffset) + .with_duration(basal.detail.duration_msec); + postbasal.with_percent(basal.detail.temp_basal_percent) + .set('suppressed', suppressed); + } + } + postrecords.push(postbasal); + } + return records.concat(postrecords); + }; + + // it turns out these records may not actually be implemented + // occlusions are only represented through pump advisory and hazard alarms + // TODO: maybe we should just delete this code? + const buildOcclusionRecords = (data, records) => { + const occlusionrecs = findSpecificRecords(data.log_records, [ + getValueForName(logRecords, 'Occlusion'), + ]); + const postrecords = []; + let postocc = null; + for (let o = 0; o < occlusionrecs.length; ++o) { + const occlusion = data.log_records[occlusionrecs[o]]; + postocc = cfg.builder.makeDeviceEventAlarm() + .with_deviceTime(occlusion.deviceTime) + .with_alarmType('occlusion') + .set('index', occlusion.log_index); + cfg.tzoUtil.fillInUTCInfo(postocc, occlusion.jsDate); + postocc = postocc.done(); + postrecords.push(postocc); + } + return records.concat(postrecords); + }; + + const buildSuspendRecords = (data, records) => { + const suspendrecs = findSpecificRecords(data.log_records, [ + getValueForName(logRecords, 'Suspend'), + getValueForName(logRecords, 'Deactivate'), + ]); + const postrecords = []; + let postsuspend = null; + let postbasal = null; + let postreschange = null; + for (let s = 0; s < suspendrecs.length; ++s) { + const suspend = data.log_records[suspendrecs[s]]; + postsuspend = cfg.builder.makeDeviceEventSuspend() + .with_deviceTime(suspend.deviceTime) + // the spec doesn't really specify circumstances under which suspends happen + // i.e., 'manual' reason code is an assumption here + // 'Deactivate' is probably most commonly *not* manual (b/c it's pod expiration) + // but it *can* be and the event itself doesn't identify + // in the future we could consider keeping track of the pump warnings that precede + // (e.g., in the simulator) and attempt to infer the proper reason code that way + // TODO: consider an annotation here re: unknown reason code + .with_reason({ suspended: 'manual' }) + .set('index', suspend.log_index); + cfg.tzoUtil.fillInUTCInfo(postsuspend, suspend.jsDate); + postsuspend = postsuspend.done(); + if (suspend.rectype_name === 'Deactivate') { + postreschange = cfg.builder.makeDeviceEventReservoirChange() + .with_deviceTime(suspend.deviceTime) + .with_payload({ event: 'pod_deactivation' }) + .with_status(postsuspend) + .set('index', suspend.log_index); + cfg.tzoUtil.fillInUTCInfo(postreschange, suspend.jsDate); + postreschange = postreschange.done(); + postrecords.push(postreschange); + } + postrecords.push(postsuspend); + // we don't call .done() on the basal b/c it still needs duration added + // which happens in the simulator + postbasal = cfg.builder.makeSuspendBasal() + .with_deviceTime(suspend.deviceTime) + .set('index', suspend.log_index); + cfg.tzoUtil.fillInUTCInfo(postbasal, suspend.jsDate); + postrecords.push(postbasal); + } + return records.concat(postrecords); + }; + + const buildResumeRecords = (data, records) => { + const resumerecs = findSpecificRecords(data.log_records, [ + getValueForName(logRecords, 'Resume'), + ]); + const postrecords = []; + let postresume = null; + for (let r = 0; r < resumerecs.length; ++r) { + const resume = data.log_records[resumerecs[r]]; + // we don't call .done() on a resume b/c we still need to pair it with + // its previous suspend in the simulator + postresume = cfg.builder.makeDeviceEventResume() + .with_deviceTime(resume.deviceTime) + // the spec doesn't really specify circumstances under which resumes happen + // i.e., 'manual' reason code is an assumption here + .with_reason({ resumed: 'manual' }) + .set('index', resume.log_index); + cfg.tzoUtil.fillInUTCInfo(postresume, resume.jsDate); + postrecords.push(postresume); + } + return records.concat(postrecords); + }; + + const buildActivationRecords = (data, records) => { + const activerecs = findSpecificRecords(data.log_records, [ + getValueForName(logRecords, 'Activate'), + ]); + const postrecords = []; + let postactivate = null; + for (let a = 0; a < activerecs.length; ++a) { + const activate = data.log_records[activerecs[a]]; + postactivate = cfg.builder.makeDeviceEventResume() + .with_deviceTime(activate.deviceTime) + .with_reason('new_pod') + .set('index', activate.log_index); + cfg.tzoUtil.fillInUTCInfo(postactivate, activate.jsDate); + postrecords.push(postactivate); + } + return records.concat(postrecords); + }; + + const buildTimeChangeRecords = (data, records, settings) => { + const seenChanges = {}; + function findNearbyChanges(index) { + let donechange = false; + const changes = [data.log_records[index]]; + while (!donechange) { + index -= 1; + const currentrec = data.log_records[index]; + if (currentrec.rectype_name === 'Date_Change' || currentrec.rectype_name === 'Time_Change') { + seenChanges[index] = true; + changes.push(currentrec); + } else { + donechange = true; + } + } + return changes; + } + + const changerecs = findSpecificRecords(data.log_records, [ + getValueForName(logRecords, 'Date_Change'), + getValueForName(logRecords, 'Time_Change'), + ]); + const postrecords = []; + let postchange = null; + for (let c = 0; c < changerecs.length; ++c) { + if (!seenChanges[changerecs[c]]) { + const changegroup = findNearbyChanges(changerecs[c]); + const grouped = _.groupBy(changegroup, 'rectype_name'); + const first = changegroup[0]; + const ts = sundial.buildTimestamp(_.assign( + {}, + grouped.Date_Change ? grouped.Date_Change[grouped.Date_Change.length - 1].detail : + { year: first.year, month: first.month, day: first.day }, + grouped.Time_Change ? grouped.Time_Change[grouped.Time_Change.length - 1].detail : + { hours: first.hours, minutes: first.minutes, seconds: first.seconds }, + )); + postchange = cfg.builder.makeDeviceEventTimeChange() + .with_deviceTime(first.deviceTime) + .set('jsDate', ts) + .with_change({ + from: first.deviceTime, + to: sundial.formatDeviceTime(ts), + agent: 'manual', + }) + .set('index', first.log_index); + postrecords.push(postchange); + } + } + const tzoUtil = new TZOUtil( + cfg.timezone, + settings.time, + // certain errors cause the OmniPod to reset the clock to 2007-01-01 + // (or to 2015-12-31 on the Dash) + // these are not legitimate time change records for UTC "bootstrapping" + _.filter(postrecords, (rec) => rec.change.from.slice(0, 4) > '2015'), + ); + cfg.tzoUtil = tzoUtil; + + return records.concat(tzoUtil.records); + }; + + const buildBGRecords = (data, records) => { + const bgrecs = findSpecificRecords(data.log_records, [ + getValueForName(logRecords, 'Blood_Glucose'), + ]); + const postrecords = []; + for (let i = 0; i < bgrecs.length; ++i) { + const bg = data.log_records[bgrecs[i]]; + // skip errored records: meter error code, temperature error flag, other error flag + if (bg.detail.error_code !== 0 + || hasFlag(BG_FLAGS.TEMPERATURE_FLAG, bg.detail.flags) + || hasFlag(BG_FLAGS.OTHER_ERROR_FLAG, bg.detail.flags)) { + continue; + } + const bgMeter = BG_BOARD_TYPES[PDM_CONFIG_FLAGS.BG_BOARD_TYPE.mask & data.eeprom_settings.PDM_CONFIG_FLAGS]; + let postbg = cfg.builder.makeSMBG() + .with_deviceTime(bg.deviceTime) + .with_units(BG_UNITS) + .with_subType(hasFlag(BG_FLAGS.MANUAL_FLAG, bg.detail.flags) ? 'manual' : 'linked') + .set('index', bg.log_index); + cfg.tzoUtil.fillInUTCInfo(postbg, bg.jsDate); + let value = bg.detail.bg_reading; + if (hasFlag(BG_FLAGS.RANGE_ERROR_LOW_FLAG, bg.detail.flags)) { + value = 19; + annotate.annotateEvent(postbg, { code: 'bg/out-of-range', value: 'low', threshold: 20 }); + } + if (hasFlag(BG_FLAGS.RANGE_ERROR_HIGH_FLAG, bg.detail.flags)) { + value = bgMeter.highest + 1; + annotate.annotateEvent(postbg, { code: 'bg/out-of-range', value: 'high', threshold: bgMeter.highest }); + } + postbg = postbg.with_value(value) + .done(); + postrecords.push(postbg); + if (bg.user_tag_1 || bg.user_tag_2) { + const s = `${bg.user_tag_1} ${bg.user_tag_2}`; + let note = cfg.builder.makeNote() + .with_value(s) + .with_deviceTime(bg.deviceTime); + cfg.tzoUtil.fillInUTCInfo(note, bg.jsDate); + note = note.done(); + debug('Not storing note:', s); + } + } + return records.concat(postrecords); + }; + + const buildSettingsRecord = (data, records) => { + const downloads = findSpecificRecords(data.log_records, [ + getValueForName(logRecords, 'Download'), + ]); + // there can in very rare circumstances be more than one download + // so we always take the last (most current) one + const download = data.log_records[downloads[downloads.length - 1]]; + + const schedules = {}; + const generateSchedule = (prof_idx) => { + const keyname = getItemWithValue(PROFILES, 'keyname', 'value', data.profiles[prof_idx].profile_idx); + const valname = getItemWithValue(PROFILES, 'valuename', 'value', data.profiles[prof_idx].profile_idx); + const sked = []; + const addStep = (idx) => { + const o = { start: data.profiles[prof_idx].steps[idx].starttime }; + o[keyname] = data.profiles[prof_idx].steps[idx][valname]; + sked.push(o); + }; + addStep(0); + for (let j = 1; j < data.profiles[prof_idx].steps.length; ++j) { + if (data.profiles[prof_idx].steps[j][valname] !== sked[sked.length - 1][keyname]) { + addStep(j); + } + } + return sked; + }; + const settings = {}; + for (let i = 0; i < data.profiles.length; ++i) { + if (getItemWithValue(PROFILES, 'isBasal', 'value', data.profiles[i].profile_idx)) { + if (data.profiles[i].name) { + schedules[data.profiles[i].name] = generateSchedule(i); + } + } else { + settings[getNameForValue(PROFILES, data.profiles[i].profile_idx)] = generateSchedule(i); + } + } + const bgSettingsByStart = {}; + for (let j = 0; j < settings.bgTarget.length; ++j) { + const target = settings.bgTarget[j]; + bgSettingsByStart[target.start] = { target }; + } + for (let k = 0; k < settings.bgThreshold.length; ++k) { + const threshold = settings.bgThreshold[k]; + if (bgSettingsByStart[threshold.start]) { + bgSettingsByStart[threshold.start].threshold = threshold; + } else { + bgSettingsByStart[threshold.start] = { threshold }; + } + } + + const bgTargetSettings = []; + const starts = _.sortBy(Object.keys(bgSettingsByStart), (start) => start); + + for (let l = 0; l < starts.length; ++l) { + const currentStart = starts[l]; + let start; + // find the current, or most recent previous target or threshold + // if there isn't a new one for this start time + for (let m = l; m >= 0; --m) { + if (bgSettingsByStart[starts[m]].target) { + start = starts[m]; + break; + } + } + const thisTarget = bgSettingsByStart[start].target; + + for (let n = l; n >= 0; --n) { + if (bgSettingsByStart[starts[n]].threshold) { + start = starts[n]; + break; + } + } + const thisThreshold = bgSettingsByStart[start].threshold; + + bgTargetSettings.push({ + start: parseInt(currentStart, 10), + target: thisTarget.low, + high: thisThreshold.amount, + }); + } + + const settingsUTCTime = sundial.applyTimezone(download.deviceTime, cfg.timezone).toISOString(); + + const postsettings = cfg.builder.makePumpSettings() + .with_activeSchedule(data.basalPrograms.names[data.basalPrograms.enabled_idx].name) + .with_units({ carb: 'grams', bg: BG_UNITS }) // values are sent in mg/dL even on mmol/L PDMs + .with_basalSchedules(schedules) + .with_carbRatio(settings.carbRatio) + .with_insulinSensitivity(settings.insulinSensitivity) + .with_bgTarget(bgTargetSettings) + .with_bolus({ + calculator: { + enabled: data.eeprom_settings.BOL_CALCS > 0, + insulin: { + duration: data.eeprom_settings.INSULIN_DURATION * 30, + units: 'minutes', + }, + }, + extended: { + // normal boluses = 0, percent = 1, units = 2 + enabled: data.eeprom_settings.EXT_BOL_TYPE > 0, + }, + amountMaximum: { + value: data.eeprom_settings.BOLUS_MAX / 100.0, + units: 'Units', + }, + }) + .with_basal({ + rateMaximum: { + value: data.eeprom_settings.BASAL_MAX / 100.0, + units: 'Units/hour', + }, + temporary: { + type: TEMP_BASAL_TYPES[data.eeprom_settings.TEMP_BAS_TYPE], + }, + }) + .with_display({ + bloodGlucose: { + units: data.eeprom_settings.BG_DISPLAY ? 'mmol/L' : 'mg/dL', + }, + }) + .with_manufacturers(cfg.deviceInfo.manufacturers) + .with_model(cfg.deviceInfo.model) + .with_serialNumber(cfg.deviceInfo.serialNumber) + .with_time(settingsUTCTime) + .with_deviceTime(download.deviceTime) + .with_timezoneOffset(sundial.getOffsetFromZone(settingsUTCTime, cfg.timezone)) + .with_conversionOffset(0) + .done(); + + records.push(postsettings); + return records; + }; + + return { + detect(deviceInfo, cb) { + debug('no detect function needed'); + cb(null, deviceInfo); + }, + + // eslint-disable-next-line consistent-return + setup(deviceInfo, progress, cb) { + debug('Insulet Setup (Jellyfish)!'); + progress(0); + const data = { stage: 'setup' }; + + if (cfg.filename && cfg.filedata) { + // the user has already selected a file + buf = cfg.filedata; + bytes = new Uint8Array(buf); + data.filedata = cfg.filedata; // to store as blob + progress(100); + return cb(null, data); + } + + // check if pdm is mounted as a drive + + // eslint-disable-next-line consistent-return + (async () => { + try { + if (is.electron) { + const drivelist = require('drivelist'); + const drives = await drivelist.list(); + + let filePath = null; + debug('Drives:', drives); + // eslint-disable-next-line no-restricted-syntax + for (const drive of drives) { + debug(drive.description); + if (drive.description && drive.description.toLowerCase().includes('omnipod') && !drive.system) { + if (drive.mountpoints.length > 0) { + const devicePath = drive.mountpoints[0].path; + // eslint-disable-next-line no-await-in-loop + const contents = await fs.readdir(devicePath); + const files = _.filter(contents, (file) => { + const regex = /(.+\.ibf)/g; + return file.match(regex); + }); + // On Eros PDM there should only be one .ibf file + filePath = path.join(devicePath, files[0]); + } else { + return cb(new Error('File not found. Please unplug PDM and try again.'), null); + } + } + } + + if (filePath) { + debug('filePath:', filePath); + + const res = await fs.readFile(filePath); + buf = res.buffer; + bytes = new Uint8Array(buf); + progress(100); + return cb(null, data); + } + } + + // no mounted drive with .ibf file, let's try connecting over MTP (Dash) + const Mtp = (await import('webmtp')).default; // dynamic import required for CLI tools (e.g. fslibre) + const vendorId = _.get(deviceInfo, 'usbDevice.vendorId'); + const productId = _.get(deviceInfo, 'usbDevice.productId'); + debug('USB PID/VID:', vendorId, productId); + const mtp = new Mtp(vendorId, productId); + + mtp.addEventListener('error', () => cb(new Error('WebMTP error or no device connected'), null)); + mtp.addEventListener('ready', async () => { + await mtp.openSession(); + const handles = await mtp.getObjectHandles(); + + handles.sort((a, b) => b - a); + debug('Handles:', handles); + + let fileName; + let objectHandle; + for (let i = 0; i < handles.length; i++) { + objectHandle = handles[i]; + // eslint-disable-next-line no-await-in-loop + fileName = await mtp.getFileName(objectHandle); + if (_.endsWith(fileName, '.ibf')) { + break; + } + } + + bytes = await mtp.getFile(objectHandle, fileName); + buf = bytes.buffer; + await mtp.close(); + + progress(100); + return cb(null, data); + }); + } catch (err) { + if (err.message.includes('EPERM: operation not permitted, scandir') && os.platform() === 'darwin') { + err.code = 'E_OMNIPOD_WRITE'; + } + return cb(err); + } + })(); + }, + + connect(progress, data, cb) { + debug('Insulet Connect!'); + // let's do a validation pass + data.stage = 'connect'; + progress(0); + let done = false; + let offset = 0; + data.npackets = 0; + while (!done) { + const rec = getRecord(offset); + if (rec === null) { + // final packet is incomplete + done = true; + } else { + if (!rec.valid) { + return cb('Checksum error', rec); + } + data.npackets += 1; + offset += rec.packetlen; + if (offset >= bytes.length) { + done = true; + } + } + } + + // we made it through + progress(100); + return cb(null, data); + }, + + // eslint-disable-next-line consistent-return + getConfigInfo(progress, data, cb) { + debug('Insulet GetConfigInfo!'); + data.stage = 'getConfigInfo'; + progress(0); + let offset = 0; + data.ibf_version = getFixedRecord('ibf_version', offset); + offset += data.ibf_version.packetlen; + + progress(10); + data.pdm_version = getFixedRecord('pdm_version', offset); + if (data.pdm_version.pdm_maj >= 3) { // Dash PDM + isDash = true; + cfg.deviceInfo.model = 'Dash'; + } else { + cfg.deviceInfo.model = 'Eros'; + } + data.deviceModel = cfg.deviceInfo.model; // for metrics + + if (!isDash) { + // non-Dash PDM has a built-in BGM + cfg.deviceInfo.manufacturers.push('Abbott'); + cfg.deviceInfo.tags.push('bgm'); + } + + offset += data.pdm_version.packetlen; + + progress(20); + data.mfg_data = getManufacturingData(offset); + offset += data.mfg_data.packetlen; + + progress(30); + data.basalPrograms = getBasalProgramNames(offset); + offset += data.basalPrograms.packetlen; + + progress(50); + data.eeprom_settings = getFixedRecord('eeprom_settings', offset); + offset += data.eeprom_settings.packetlen; + + progress(70); + data.profiles = getProfiles(offset, data.basalPrograms.names); + offset += data.profiles.packetlen; + + progress(80); + data.logDescriptions = getLogDescriptions(offset); + offset += data.logDescriptions.packetlen; + data.independent_offset = offset; + + const recnames = ['ibf_version', 'pdm_version', 'mfg_data', 'eeprom_settings']; + for (let i = 0; i < recnames.length; ++i) { + if (fixedRecords[recnames[i]].postprocess) { + const err = fixedRecords[recnames[i]].postprocess(data[recnames[i]]); + if (err) { + return cb(err, null); + } + } + } + + _.merge(cfg, { + deviceInfo: { + deviceTime: data.logDescriptions.deviceTime, + serialNumber: data.eeprom_settings.serial_number, + deviceId: `${data.ibf_version.vendorid.slice(0, 3)}${data.ibf_version.productid.slice(0, 3)}-${data.eeprom_settings.serial_number}`, + }, + }); + + if (is.electron) { + commonFunctions.checkDeviceTime(cfg, (err) => { + progress(100); + return cb(err, data); + }); + } else { + return cb(null, data); + } + }, + + fetchData(progress, data, cb) { + debug('Insulet FetchData!'); + data.stage = 'fetchData'; + progress(0); + const log_records = getLogRecords(data); + + /* + Because pump shut-down interferes with BtUTC, anywhere + where a pump shut-down appears in records to be processed + we only attempt to process and upload the data following + the most recent device shut-down. + */ + data.log_records = _.takeWhile(log_records, (rec) => { + if (rec.rectype_name + && rec.rectype_name === 'Remote_Hazard_Alarm' + && rec.year <= 2016) { // Eros resets clock to 2007-01-01, DASH to 2015-12-31 or 2016-01-01 + debug('Most recent pump shut down:', rec); + return false; + } + return true; + }); + debug('Will process', data.log_records.length, 'log records.'); + if (buf) { + data.filedata = buf; // to store as blob + } + progress(100); + return cb(null, data); + }, + + processData(progress, data, cb) { + debug('Insulet ProcessData!'); + data.stage = 'processData'; + // data are processed while being loaded + progress(100); + return cb(null, data); + }, + + uploadData(progress, data, cb) { + debug('Insulet UploadData!'); + data.stage = 'uploadData'; + + cfg.builder.setDefaults({ deviceId: cfg.deviceInfo.deviceId }); + + let postrecords = []; + let settings = null; + postrecords = buildSettingsRecord(data, postrecords); + if (!_.isEmpty(postrecords)) { + [settings] = postrecords; + } + // order of these matters (we use it to ensure the secondary sort order) + postrecords = buildTimeChangeRecords(data, postrecords, settings); + postrecords = buildActivationRecords(data, postrecords); + postrecords = buildAlarmRecords(data, postrecords); + postrecords = buildOcclusionRecords(data, postrecords); + postrecords = buildSuspendRecords(data, postrecords); + postrecords = buildResumeRecords(data, postrecords); + postrecords = buildBasalRecords(data, postrecords); + postrecords = buildBolusRecords(data, postrecords); + postrecords = buildBolusTerminations(data, postrecords); + postrecords = buildBGRecords(data, postrecords); + // first sort by log index + postrecords = _.sortBy(postrecords, (d) => d.index); + // finally sort by time, including indexed (history) and non-indexed records + postrecords = _.sortBy(postrecords, (d) => d.time); + const simulator = insuletSimulatorMaker.make({ settings }); + for (let j = 0; j < postrecords.length; ++j) { + const datum = postrecords[j]; + switch (datum.type) { + case 'basal': + simulator.basal(datum); + break; + case 'bolus': + simulator.bolus(datum); + break; + case 'termination': + if (datum.subType === 'bolus') { + simulator.bolusTermination(datum); + } + break; + case 'deviceEvent': + if (datum.subType === 'status') { + if (datum.status === 'suspended') { + simulator.suspend(datum); + } else if (datum.status === 'resumed') { + if (datum.reason === 'new_pod') { + simulator.podActivation(datum); + } else { + simulator.resume(datum); + } + } else { + debug('Unknown deviceEvent status!', datum.status); + } + } else if (datum.subType === 'alarm') { + simulator.alarm(datum); + } else if (datum.subType === 'reservoirChange') { + simulator.changeReservoir(datum); + } else if (datum.subType === 'timeChange') { + simulator.changeDeviceTime(datum); + } else { + debug('deviceEvent of subType %s not passed to simulator!', datum.subType); + } + break; + case 'pumpSettings': + simulator.pumpSettings(datum); + break; + case 'smbg': + simulator.smbg(datum); + break; + case 'wizard': + simulator.wizard(datum); + break; + default: + debug('[Hand-off to simulator] Unhandled type!', datum.type); + } + } + simulator.finalBasal(); + + const sessionInfo = { + deviceTags: cfg.deviceInfo.tags, + deviceTime: cfg.deviceInfo.deviceTime, + deviceManufacturers: cfg.deviceInfo.manufacturers, + deviceModel: cfg.deviceInfo.model, + deviceSerialNumber: cfg.deviceInfo.serialNumber, + deviceId: cfg.deviceInfo.deviceId, + start: sundial.utcDateString(), + timeProcessing: cfg.tzoUtil.type, + tzName: cfg.timezone, + version: cfg.version, + blobId: data.blobId, + }; + + data.post_records = simulator.getEvents(); + + cfg.api.upload.toPlatform( + data.post_records, + sessionInfo, + progress, + cfg.groupId, + (err) => { + if (err) { + debug(err); + progress(100); + return cb(err, data); + } + + progress(100); + return cb(null, data); + }, + ); + }, + + disconnect(progress, data, cb) { + debug('Insulet Disconnect!'); + data.stage = 'disconnect'; + progress(100); + return cb(null, data); + }, + + cleanup(progress, data, cb) { + debug('Insulet Cleanup!'); + data.stage = 'cleanup'; + progress(100); + delete data.stage; + return cb(null, data); + }, + + _decodeSerial: decodeSerial, + }; +}; diff --git a/lib/drivers/insulet/jellyfish/insuletSimulator.js b/lib/drivers/insulet/jellyfish/insuletSimulator.js new file mode 100644 index 0000000000..a935f95baa --- /dev/null +++ b/lib/drivers/insulet/jellyfish/insuletSimulator.js @@ -0,0 +1,341 @@ +/* + * == BSD2 LICENSE == + * Copyright (c) 2014, Tidepool Project + * + * This program is free software; you can redistribute it and/or modify it under + * the terms of the associated License, which is identical to the BSD 2-Clause + * License as published by the Open Source Initiative at opensource.org. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the License for more details. + * + * You should have received a copy of the License along with this program; if + * not, you can obtain one from Tidepool Project at tidepool.org. + * == BSD2 LICENSE == + */ + +/* eslint-disable no-param-reassign */ + +import _ from 'lodash'; +import util from 'util'; + +import annotate from '../../../eventAnnotations'; +import common from '../common'; +import simulations from '../../../commonFunctions'; + +const isBrowser = typeof window !== 'undefined'; +const debug = isBrowser ? require('bows')('InsuletDriver') : console.log; + +/** + * Creates a new "simulator" for Insulet OmniPod data. The simulator has methods for events like + * + * cbg(), smbg(), basal(), bolus(), settingsChange(), etc. + * + * This simulator exists as an abstraction over the Tidepool APIs. It was written to simplify the conversion + * of static, "retrospective" audit logs from devices into events understood by the Tidepool platform. + * + * On the input side, you have events extracted from an Insulet .ibf file. They should be delivered to the simulator + * in time order. + * + * Once all input activities are collected, the simulator will have accumulated events that the Tidepool Platform + * will understand into a local "events" array. You can retrieve the events by calling `getEvents()` + * + * @param config + * @returns {*} + */ +exports.make = (config = {}) => { + const settings = config.settings || null; + const events = []; + + let activationStatus = null; + let currBasal = null; + let currBolus = null; + let currStatus = null; + let currTimestamp = null; + + function setActivationStatus(status) { + activationStatus = status; + } + + function setCurrBasal(basal) { + currBasal = basal; + } + + function setCurrBolus(bolus) { + currBolus = bolus; + } + + function setCurrStatus(status) { + currStatus = status; + } + + function ensureTimestamp(e) { + if (currTimestamp > e.time) { + throw new Error( + util.format(`Timestamps must be in order. Current timestamp was ${currTimestamp}, but got ${e}`), + ); + } + currTimestamp = e.time; + return e; + } + + function simpleSimulate(e) { + ensureTimestamp(e); + events.push(e); + } + + function fillInSuppressed(e) { + let schedName = null; + let rate = null; + const suppressed = _.clone(e.suppressed); + if (e.previous != null) { + if (e.previous.deliveryType === 'scheduled') { + schedName = e.previous.scheduleName; + rate = e.previous.rate; + } else if (e.previous.deliveryType === 'temp') { + if (e.previous.suppressed != null) { + if (e.previous.suppressed.deliveryType === 'scheduled') { + schedName = e.previous.suppressed.scheduleName; + rate = e.previous.suppressed.rate; + } + } + } + } + + if (schedName != null) { + e.suppressed = suppressed.with_scheduleName(schedName); + + if (suppressed.rate == null || Number.isNaN(suppressed.rate)) { + // only use previous rate if rate not available + e.suppressed.with_rate(rate); + } + + e.suppressed = e.suppressed.done(); + } else { + delete e.suppressed; + } + } + + return { + alarm: (event) => { + if (event.payload != null && event.payload.stopsDelivery === true) { + if (event.status == null && event.index != null) { + throw new Error('An Insulet alarm with a log index that has `stopsDelivery` in the payload must have a `status`.'); + } + } + simpleSimulate(event); + }, + basal: (event) => { + ensureTimestamp(event); + if (currBasal != null) { + // sometimes there can be duplicate suspend basals, so we return early + // if we come across a suspend basal when we're already in one + if (currBasal.deliveryType === 'suspend' && event.deliveryType === 'suspend') { + return; + } + // completing a resume event from a new pod activation + // see podActivation() below for more details + if (currStatus != null && currStatus.status === 'suspended') { + if (activationStatus != null && activationStatus.status === 'resumed') { + const resume = activationStatus.with_previous(_.omit(currStatus, 'previous')) + .with_reason({ resumed: 'manual' }) + .done(); + setCurrStatus(resume); + events.push(resume); + setActivationStatus(null); + } else if (event.deliveryType !== 'suspend') { + // if we're in a suspend basal, we need to leave the currStatus because + // a resume is probably still impending + // but if we've already moved on to a basal that's not deliveryType: suspend + // then we need to wipe the slate clean + setCurrStatus(null); + } + } + if (!currBasal.isAssigned('duration')) { + currBasal.with_duration(Date.parse(event.time) - Date.parse(currBasal.time)); + } + if (currBasal.isAssigned('suppressed')) { + fillInSuppressed(currBasal); + } + currBasal = currBasal.done(); + event.previous = _.omit(currBasal, 'previous'); + events.push(currBasal); + } else if (activationStatus != null && activationStatus.status === 'resumed') { + // at the very beginning of a file (which === when currBasal is null) it is common + // to see a pod activation and then a basal; the former will end up as a `deviceEvent` + // resume without a `previous` but it's technically accurate, so we upload it anyway + let initialResume; + // we don't really expect this to happen, but just in case... + if (currStatus != null && currStatus.status === 'suspended') { + initialResume = activationStatus.with_previous(_.omit(currStatus, 'previous')) + .with_reason({ resumed: 'manual' }) + .done(); + setCurrStatus(initialResume); + events.push(initialResume); + } else { + // this is the more common case, in which case we finish building a resume + // that won't be connected with a suspend, kinda pointless, but accurate + initialResume = activationStatus.with_reason({ resumed: 'manual' }) + .done(); + setCurrStatus(initialResume); + events.push(initialResume); + } + setActivationStatus(null); + } + setCurrBasal(event); + }, + bolus: (event) => { + simpleSimulate(event); + setCurrBolus(event); + }, + /* + * When an OmniPod users cancels a bolus partway through delivery, the data includes + * a bolus termination event, and this is the only way for us to access the information + * that the bolus volume intially programmed and the bolus volume actually delivered + * were not the same. + * + * The logic probably looks a little funny here: we add the `missedInsulin` from the bolus + * termination to the bolus volume reported on the bolus event to obtain the bolus volume + * that the user initially programmed. This is because an Insulet bolus record always + * reports the volume of bolus that was actually delivered, not the bolus that was programmed. + * (Same for duration on extended bolus (components).) + */ + bolusTermination: (event) => { + if (currBolus != null) { + if (currBolus.subType === 'normal') { + currBolus.expectedNormal = common.fixFloatingPoint(currBolus.normal + event.missedInsulin, 2); + } else if (event.durationLeft > 0) { + currBolus.expectedExtended = common.fixFloatingPoint(currBolus.extended + event.missedInsulin, 2); + currBolus.expectedDuration = currBolus.duration + event.durationLeft; + } else { + currBolus.expectedNormal = common.fixFloatingPoint(currBolus.normal + event.missedInsulin, 2); + } + } else { + debug('Cannot find bolus to modify given bolus termination: [%j]. PDM was likely reset.', event); + } + }, + changeDeviceTime: (event) => { + simpleSimulate(event); + }, + changeReservoir: (event) => { + if (event.status == null) { + throw new Error('An Insulet `reservoirChange` event must have a `status`.'); + } + simpleSimulate(event); + }, + /* + * We simulate the final basal in an Insulet .ibf file as a special case, to keep the logic + * of basal() above cleaner. This basal is special because we don't have a following basal + * to use to determine the duration. Instead we look up the basal against the settings at + * the time of upload and try to determine a duration for the basal based on this information. + */ + finalBasal: () => { + if (currBasal != null) { + if (currBasal.deliveryType !== 'scheduled') { + if (currBasal.deliveryType === 'temp') { + if (currBasal.isAssigned('suppressed')) { + fillInSuppressed(currBasal); + } + currBasal = currBasal.done(); + } else if (!currBasal.isAssigned('duration')) { + currBasal.duration = 0; + annotate.annotateEvent(currBasal, 'basal/unknown-duration'); + currBasal = currBasal.done(); + } else { + currBasal = currBasal.done(); + } + } else if (settings != null) { + currBasal = simulations.finalScheduledBasal(currBasal, settings, 'insulet'); + } else { + currBasal.with_duration(0); + annotate.annotateEvent(currBasal, 'basal/unknown-duration'); + currBasal = currBasal.done(); + } + events.push(currBasal); + } + }, + /* + * Pod activations are not *quite* resume events (because further user intervention is + * required before insulin delivery resumes - namely, confirming that cannula insertion + * was successful). So we build activations as resumes, save them in `activationStatus` + * and then complete them as resumes upon receipt of the next `basal` event. + */ + podActivation: (event) => { + ensureTimestamp(event); + setActivationStatus(event); + }, + resume: (event) => { + ensureTimestamp(event); + if (currStatus != null && currStatus.status === 'suspended') { + event = event.with_previous(_.omit(currStatus, 'previous')); + } + event = event.done(); + setCurrStatus(event); + events.push(event); + }, + pumpSettings: (event) => { + simpleSimulate(event); + }, + smbg: (event) => { + simpleSimulate(event); + }, + suspend: (event) => { + // suspends in a series are pretty common - e.g., when an alarm that implies + // a stoppage of delivery produces a suspend and then we also get something + // like a pod deactivation immediately following + // if we're already in a suspended state, we just return early to maintain that state + if (currStatus != null && currStatus.status === 'suspended') { + return; + } + ensureTimestamp(event); + // there can be stray activation events that hang around + // i.e., when there was a PDM error and then a date & time change + // they are definitely no longer relevant if we come across an actual suspend + // so we reset back to null + if (activationStatus != null && activationStatus.status === 'resumed') { + setActivationStatus(null); + } + setCurrStatus(event); + events.push(event); + }, + wizard: (event) => { + simpleSimulate(event); + }, + getEvents: () => { + function filterOutZeroBoluses() { + return _.filter(events, (event) => { + // we include the index on all objects to be able to sort accurately in + // pump-event order despite date & time settings changes, but it's not + // part of our data model, so we delete before uploading + delete event.index; + if (event.type === 'bolus') { + if (event.normal === 0 && !event.expectedNormal && !event.carbInput) { + return false; + } + delete event.carbInput; + return true; + } + + if (event.type === 'wizard') { + const bolus = event.bolus || null; + if (bolus != null) { + if (bolus.normal === 0 && !bolus.expectedNormal && !event.carbInput) { + return false; + } + return true; + } + } + return true; + }); + } + // because we have to wait for the *next* basal to determine the duration of a current + // basal, basal events get added to `events` out of order wrt other events + // (although within their own type all the basals are always in order) + // end result: we have to sort events again before we try to upload them + const orderedEvents = _.sortBy(filterOutZeroBoluses(), (e) => e.time); + + return orderedEvents; + }, + }; +}; diff --git a/lib/drivers/medtronic/processData.js b/lib/drivers/medtronic/processData.js index c73164c388..fead26fd48 100644 --- a/lib/drivers/medtronic/processData.js +++ b/lib/drivers/medtronic/processData.js @@ -196,11 +196,6 @@ var init = function(config, settingsData) { settings = _.cloneDeep(settingsData); }; -Number.prototype.toFixedNumber = function(significant){ - var pow = Math.pow(10,significant); - return +( Math.round(this*pow) / pow ); -}; - var decodeDate = function (payload) { var encoded, second, minute, hour, day, month, year; diff --git a/lib/drivers/tandem b/lib/drivers/tandem index 690f448fbc..ff50d521b5 160000 --- a/lib/drivers/tandem +++ b/lib/drivers/tandem @@ -1 +1 @@ -Subproject commit 690f448fbcf0555b1b32fc50d884d095c43941f1 +Subproject commit ff50d521b51819d57f99f960557c69ed7200de2a diff --git a/lib/objectBuilder.js b/lib/objectBuilder.js index db76a0b493..aec5110d7d 100644 --- a/lib/objectBuilder.js +++ b/lib/objectBuilder.js @@ -139,7 +139,6 @@ module.exports = function () { scheduleName: OPTIONAL, rate: REQUIRED, duration: REQUIRED, - previous: OPTIONAL, payload: OPTIONAL, expectedDuration: OPTIONAL }); @@ -204,8 +203,15 @@ module.exports = function () { type: 'deviceEvent', subType: 'pumpSettingsOverride', overrideType: REQUIRED, + overridePreset: OPTIONAL, duration: REQUIRED, method: OPTIONAL, + expectedDuration: OPTIONAL, + bgTarget: OPTIONAL, + units: OPTIONAL, + basalRateScaleFactor: OPTIONAL, + carbRatioScaleFactor: OPTIONAL, + insulinSensitivityScaleFactor: OPTIONAL, payload: OPTIONAL }); rec._bindProps(); @@ -453,7 +459,8 @@ module.exports = function () { bgTargets: {}, sleepSchedules: OPTIONAL, insulin: OPTIONAL, - bolus: OPTIONAL, + bolus: OPTIONAL, // this can be removed after Jellyfish is deprecated + boluses: {}, manufacturers: OPTIONAL, model: OPTIONAL, serialNumber: OPTIONAL, @@ -473,7 +480,6 @@ module.exports = function () { rate: REQUIRED, duration: REQUIRED, payload: OPTIONAL, - previous: OPTIONAL, expectedDuration: OPTIONAL }); rec._bindProps(); @@ -514,7 +520,6 @@ module.exports = function () { duration: OPTIONAL, suppressed: OPTIONAL, payload: OPTIONAL, - previous: OPTIONAL, expectedDuration: OPTIONAL }); rec._bindProps(); @@ -530,7 +535,6 @@ module.exports = function () { duration: REQUIRED, suppressed: OPTIONAL, payload: OPTIONAL, - previous: OPTIONAL, expectedDuration: OPTIONAL }); rec._bindProps(); diff --git a/locales/en/translation.json b/locales/en/translation.json index 1557b470ab..4f709f8070 100644 --- a/locales/en/translation.json +++ b/locales/en/translation.json @@ -183,7 +183,6 @@ "Day": "Day", "Year": "Year", "Logout": "Logout", - "Tidepool does not support Minimed pumps 522, 722 or older, or the newer 6-series pumps. Sorry... If you are no longer using an unsupported pump and still get this message, create a new CareLink account and try uploading again.": "Tidepool does not support Minimed pumps 522, 722 or older, or the newer 6-series pumps. Sorry... If you are no longer using an unsupported pump and still get this message, create a new CareLink account and try uploading again.", "Something went wrong during device upload": "Something went wrong during device upload", "Please choose a file ending in ": "Please choose a file ending in ", "Hmm, your device doesn't appear to be connected": "Hmm, your device doesn\"t appear to be connected", diff --git a/locales/es/translation.json b/locales/es/translation.json index 1161b1de0f..0d2d547f08 100644 --- a/locales/es/translation.json +++ b/locales/es/translation.json @@ -183,12 +183,10 @@ "Day": "Día", "Year": "Año", "Logout": "Salir", - "Tidepool does not support Minimed pumps 522, 722 or older, or the newer 6-series pumps. Sorry... If you are no longer using an unsupported pump and still get this message, create a new CareLink account and try uploading again.": "Tidepool no es compatible con las bombas Minimed 522, 722 o anteriores, ni con las nuevas bombas de la serie 6. Lo sentimos ... Si ya no usa una bomba no compatible y aún recibe este mensaje, cree una nueva cuenta CareLink e intente cargar nuevamente.", "Something went wrong during device upload": "Algo salió mal durante la carga del dispositivo", "Please choose a file ending in ": "Por favor, elija un archivo que termine en", "Hmm, your device doesn't appear to be connected": "Hmm, tu dispositivo no parece estar conectado", "Error during app initialization": "Error durante la inicialización de la aplicación", - "Tidepool does not support Minimed pumps 522, 722 or older, or the newer 6-series pumps. Sorry...": "Tidepool no es compatible con las bombas Minimed 522, 722 o anteriores, ni con las nuevas bombas de la serie 6.", "Make sure no other software (e.g. CareLink, OpenAPS, Loop) is talking to your pump. Otherwise, please see the error details below or contact Tidepool Support.": "Asegúrese de que ningún otro software (por ejemplo, CareLink, OpenAPS, Loop) esté hablando con su bomba. De lo contrario, consulte los detalles del error a continuación o comuníquese con el Soporte de Tidepool.", "Not connected to the Internet!": "No hay conexón de red!", "Error reading file ": "Error leyendo el archivo ", diff --git a/package.json b/package.json index 73bfea38f8..03420595e4 100644 --- a/package.json +++ b/package.json @@ -1,10 +1,10 @@ { "name": "tidepool-uploader", + "version": "2.61.0-remove-jellyfish.8", "engines": { "node": "20.14.0" }, "packageManager": "yarn@3.6.4", - "version": "2.61.0-upload-1440-dep-updates.3", "description": "Tidepool Project Universal Uploader", "private": true, "main": "main.prod.js", @@ -54,6 +54,7 @@ "ble-glucose": "0.7.0", "body-parser": "1.20.3", "bows": "1.7.2", + "caniuse-lite": "1.0.30001651", "chrome-launcher": "0.15.2", "classnames": "2.5.1", "commander": "4.1.1", @@ -107,7 +108,7 @@ "stack-trace": "0.0.10", "sudo-prompt": "9.2.1", "sundial": "1.7.5", - "tidepool-platform-client": "0.61.0", + "tidepool-platform-client": "0.62.0-getinfouser.2", "uuid": "9.0.1", "webmtp": "0.3.3" }, diff --git a/test/app/actions/async.test.js b/test/app/actions/async.test.js index b6975bebe2..5069417af4 100644 --- a/test/app/actions/async.test.js +++ b/test/app/actions/async.test.js @@ -2883,7 +2883,7 @@ describe('Asynchronous Actions', () => { def456: {}, }, devices: { - carelink: {}, + medtronic: {}, dexcom: {}, omnipod: {} }, @@ -3011,7 +3011,7 @@ describe('Asynchronous Actions', () => { def456: {}, }, devices: { - carelink: {}, + medtronic: {}, dexcom: {}, omnipod: {} }, @@ -3092,7 +3092,7 @@ describe('Asynchronous Actions', () => { def456: {}, }, devices: { - carelink: {}, + medtronic: {}, dexcom: {}, omnipod: {} }, @@ -4038,14 +4038,14 @@ describe('Asynchronous Actions', () => { describe('user is clinician account', () => { test('should dispatch SELECT_CLINIC, then SET_PAGE (redirect to clinic user select page)', () => { const targets = { - abc123: [{key: 'carelink'}], + abc123: [{key: 'medtronic'}], def456: [ {key: 'dexcom', timezone: 'US/Mountain'}, {key: 'omnipod', timezone: 'US/Mountain'} ] }; const devicesByUser = { - abc123: ['carelink'], + abc123: ['medtronic'], def456: ['dexcom', 'omnipod'] }; const expectedActions = [ @@ -4093,7 +4093,7 @@ describe('Asynchronous Actions', () => { def456: {}, }, devices: { - carelink: {}, + medtronic: {}, dexcom: {}, omnipod: {} }, @@ -4101,7 +4101,7 @@ describe('Asynchronous Actions', () => { targetsForUpload: ['abc123', 'def456'], uploadTargetUser: 'abc123', targetDevices: { - abc123: ['carelink'] + abc123: ['medtronic'] }, targetTimezones: { abc123: 'US/Mountain' @@ -4115,14 +4115,14 @@ describe('Asynchronous Actions', () => { describe('user is not clinician account', () => { test('should dispatch SELECT_CLINIC, then SET_PAGE (redirect to main page)', () => { const targets = { - abc123: [{key: 'carelink', timezone: 'US/Eastern'}], + abc123: [{key: 'medtronic', timezone: 'US/Eastern'}], def456: [ {key: 'dexcom', timezone: 'US/Mountain'}, {key: 'omnipod', timezone: 'US/Mountain'} ] }; const devicesByUser = { - abc123: ['carelink'], + abc123: ['medtronic'], def456: ['dexcom', 'omnipod'] }; const expectedActions = [ diff --git a/test/lib/insulet/testInsuletSimulator.js b/test/lib/insulet/testInsuletSimulator.js index 9af753e7f6..c079f6b3fb 100644 --- a/test/lib/insulet/testInsuletSimulator.js +++ b/test/lib/insulet/testInsuletSimulator.js @@ -166,6 +166,33 @@ describe('insuletSimulator.js', () => { simulator.bolusTermination(term2); expect(simulator.getEvents()).deep.equals([_.assign({}, val, {expectedNormal: 4.0, expectedExtended: 2.8, expectedDuration: 3600000})]); }); + + test('is amended when duration left is zero but still cancelled ', () => { + var val = { + time: '2014-09-25T01:00:00.000Z', + deviceTime: '2014-09-25T01:00:00', + timezoneOffset: 0, + conversionOffset: 0, + deviceId: 'InsOmn1234', + normal: 1.3, + extended: 1.4, + duration: 360000, + type: 'bolus', + subType: 'dual/square' + }; + + var term = { + time: '2014-09-25T01:00:05.000Z', + type: 'termination', + subType: 'bolus', + missedInsulin: 2.3, + durationLeft: 0 + }; + + simulator.bolus(val); + simulator.bolusTermination(term); + expect(simulator.getEvents()).deep.equals([_.assign({}, val, {expectedExtended: 3.7})]); + }); }); }); @@ -310,20 +337,16 @@ describe('insuletSimulator.js', () => { }; test('passes through with a status', () => { - var suspend = { - time: '2014-09-25T01:00:00.000Z', - deviceTime: '2014-09-25T01:00:00', - timezoneOffset: 0, - conversionOffset: 0, - deviceId: 'InsOmn1234', - type: 'deviceEvent', - subType: 'status', - status: 'suspended', - reason: {suspended: 'manual'} - }; + var suspend = builder.makeDeviceEventSuspendResume() + .with_time('2014-09-25T01:00:00.000Z') + .with_deviceTime('2014-09-25T01:00:00') + .with_timezoneOffset(0) + .with_conversionOffset(0) + .with_reason({suspended: 'manual'}); var withStatus = _.assign({}, val, {status: suspend}); simulator.changeReservoir(withStatus); + simulator.finalBasal(); expect(simulator.getEvents()).deep.equals([withStatus]); }); @@ -334,17 +357,12 @@ describe('insuletSimulator.js', () => { }); describe('status', () => { - var suspend = { - time: '2014-09-25T01:00:00.000Z', - deviceTime: '2014-09-25T01:00:00', - timezoneOffset: 0, - conversionOffset: 0, - deviceId: 'InsOmn1234', - type: 'deviceEvent', - subType: 'status', - status: 'suspended', - reason: {suspended: 'automatic'} - }; + var suspend = builder.makeDeviceEventSuspendResume() + .with_time('2014-09-25T01:00:00.000Z') + .with_deviceTime('2014-09-25T01:00:00') + .with_timezoneOffset(0) + .with_conversionOffset(0) + .with_reason({suspended: 'automatic'}); var resume = builder.makeDeviceEventResume() .with_time('2014-09-25T02:00:00.000Z') .with_deviceTime('2014-09-25T02:00:00') @@ -352,25 +370,18 @@ describe('insuletSimulator.js', () => { .with_conversionOffset(0) .with_status('resumed') .with_reason({resumed: 'manual'}); - var expectedResume = _.assign({}, resume); - expectedResume = expectedResume.set('previous', suspend).done(); - test('a suspend passes through', () => { + test('a suspend does not pass through', () => { simulator.suspend(suspend); - expect(simulator.getEvents()).deep.equals([suspend]); + expect(simulator.getEvents()).deep.equals([]); }); test('a resume passes through', () => { simulator.resume(resume); + resume.annotations = [{code: 'status/incomplete-tuple'}]; expect(simulator.getEvents()).deep.equals([resume.done()]); }); - test('a resume includes a previous when preceded by a suspend', () => { - simulator.suspend(suspend); - simulator.resume(resume); - expect(simulator.getEvents()).deep.equals([suspend, expectedResume]); - }); - test('uses the timestamp of the first suspend if multiple suspends appear before a single resume', () => { var suspend2 = { time: '2014-09-25T01:05:00.000Z', @@ -386,7 +397,9 @@ describe('insuletSimulator.js', () => { simulator.suspend(suspend); simulator.suspend(suspend2); simulator.resume(resume); - expect(simulator.getEvents()).deep.equals([suspend, expectedResume]); + var expectedSuspendResume = _.clone(suspend); + expectedSuspendResume = expectedSuspendResume.with_duration(3600000); + expect(simulator.getEvents()).deep.equals([expectedSuspendResume.done()]); }); }); @@ -470,29 +483,6 @@ describe('insuletSimulator.js', () => { expect(simulator.getEvents()).deep.equals([expectedFirstBasal]); }); - test('sets previous on basals other than the first', () => { - var expectedFirstBasal = _.cloneDeep(basal1); - expectedFirstBasal = expectedFirstBasal.set('duration', 3600000).done(); - var expectedSecondBasal = _.cloneDeep(basal2); - expectedSecondBasal = expectedSecondBasal.set('duration', 1800000) - .set('previous', expectedFirstBasal) - .done(); - var expectedThirdBasal = _.cloneDeep(basal3); - expectedThirdBasal = expectedThirdBasal.set('duration', 0) - .set('previous', _.omit(expectedSecondBasal, 'previous')) - .done(); - expectedThirdBasal.annotations = [{code: 'basal/unknown-duration'}]; - simulator.basal(basal1); - simulator.basal(basal2); - simulator.basal(basal3); - simulator.finalBasal(); - expect(simulator.getEvents()).deep.equals([ - expectedFirstBasal, - expectedSecondBasal, - expectedThirdBasal - ]); - }); - test('fills in the suppressed.scheduleName for a temp basal by percentage', () => { var settings = { time: '2014-09-25T01:00:00.000Z', @@ -528,14 +518,12 @@ describe('insuletSimulator.js', () => { .with_rate(0.65) .with_percent(0.5) .with_duration(1800000); - var suppressed = builder.makeScheduledBasal() - .with_time('2014-09-25T18:10:00.000Z') - .with_deviceTime('2014-09-25T18:10:00') - .with_timezoneOffset(0) - .with_conversionOffset(0) - .with_rate(1.3) - .with_duration(1800000); - tempBasal.with_suppressed(suppressed); + var suppressed = { + type: 'basal', + deliveryType: 'scheduled', + rate: 1.3 + }; + tempBasal.set('suppressed', suppressed); var regBasal2 = builder.makeScheduledBasal() .with_time('2014-09-25T18:40:00.000Z') .with_deviceTime('2014-09-25T18:40:00') @@ -546,15 +534,10 @@ describe('insuletSimulator.js', () => { var thisSim = pwdSimulator.make({settings: settings}); var expectedFirstBasal = _.cloneDeep(regBasal1); expectedFirstBasal = expectedFirstBasal.set('duration', 300000).done(); - var expectedSecondBasal = _.cloneDeep(tempBasal); - expectedSecondBasal.set('previous', expectedFirstBasal); - expectedSecondBasal.suppressed = expectedSecondBasal.suppressed - .set('scheduleName', 'billy').done(); - expectedSecondBasal = expectedSecondBasal.done(); + var expectedSecondBasal = _.cloneDeep(tempBasal).done(); + expectedSecondBasal.suppressed.scheduleName = 'billy'; var expectedThirdBasal = _.cloneDeep(regBasal2); - expectedThirdBasal = expectedThirdBasal.set('duration', 19200000) - .set('previous', _.omit(expectedSecondBasal, 'previous')) - .done(); + expectedThirdBasal = expectedThirdBasal.set('duration', 19200000).done(); expectedThirdBasal.annotations = [{code: 'final-basal/fabricated-from-schedule'}]; thisSim.basal(regBasal1); thisSim.basal(tempBasal); @@ -686,14 +669,13 @@ describe('insuletSimulator.js', () => { }); describe('event interplay', () => { - var suspend = builder.makeDeviceEventSuspend() + var suspend = builder.makeDeviceEventSuspendResume() .with_time('2014-09-25T01:50:00.000Z') .with_deviceTime('2014-09-25T01:50:00') .with_timezoneOffset(0) .with_conversionOffset(0) .with_status('suspended') - .with_reason({suspended: 'manual'}) - .done(); + .with_reason({suspended: 'manual'}); var resume = builder.makeDeviceEventResume() .with_time('2014-09-25T02:00:00.000Z') .with_deviceTime('2014-09-25T02:00:00') @@ -723,11 +705,11 @@ describe('insuletSimulator.js', () => { simulator.finalBasal(); var expectedResume = _.cloneDeep(resume); expectedResume = expectedResume.done(); + expectedResume.annotations = [{code: 'status/incomplete-tuple'}]; var expectedFirstBasal = _.cloneDeep(basal1); expectedFirstBasal = expectedFirstBasal.set('duration', 3600000).done(); var expectedSecondBasal = _.cloneDeep(basal2); - expectedSecondBasal = expectedSecondBasal.set('previous', expectedFirstBasal) - .set('duration', 0).done(); + expectedSecondBasal = expectedSecondBasal.set('duration', 0).done(); expectedSecondBasal.annotations = [{code: 'basal/unknown-duration'}]; expect(simulator.getEvents()).deep.equals([ expectedResume, @@ -736,23 +718,21 @@ describe('insuletSimulator.js', () => { ]); }); - test('if a new pod is activated and the pump is suspended, a resume is fabricated with the suspend as its previous before basal resumes', () => { + test('if a new pod is activated and the pump is suspended, the status event is finalized before basal resumes', () => { simulator.suspend(suspend); simulator.podActivation(resume); simulator.basal(basal1); simulator.basal(basal2); simulator.finalBasal(); - var expectedResume = _.cloneDeep(resume); - expectedResume = expectedResume.set('previous', suspend).done(); + var expectedSuspendResume = _.cloneDeep(suspend).done(); + expectedSuspendResume.duration = 600000; var expectedFirstBasal = _.cloneDeep(basal1); expectedFirstBasal = expectedFirstBasal.set('duration', 3600000).done(); var expectedSecondBasal = _.cloneDeep(basal2); - expectedSecondBasal = expectedSecondBasal.set('previous', expectedFirstBasal) - .set('duration', 0).done(); + expectedSecondBasal = expectedSecondBasal.set('duration', 0).done(); expectedSecondBasal.annotations = [{code: 'basal/unknown-duration'}]; expect(simulator.getEvents()).deep.equals([ - suspend, - expectedResume, + expectedSuspendResume, expectedFirstBasal, expectedSecondBasal ]); diff --git a/test/lib/tandem/testTandemSimulator.js b/test/lib/tandem/testTandemSimulator.js index 4dac701f68..9fbf9e7541 100644 --- a/test/lib/tandem/testTandemSimulator.js +++ b/test/lib/tandem/testTandemSimulator.js @@ -201,41 +201,53 @@ describe('tandemSimulator.js', () => { }); describe('status', () => { - var suspend = { - time: '2014-09-25T01:00:00.000Z', - deviceTime: '2014-09-25T01:00:00', - timezoneOffset: 0, - conversionOffset: 0, - deviceId: 'tandem12345', - type: 'deviceEvent', - subType: 'status', - status: 'suspended', - reason: {suspended: 'automatic'} - }; + var suspend = builder.makeDeviceEventSuspendResume() + .with_time('2014-09-25T01:00:00.000Z') + .with_deviceTime('2014-09-25T01:00:00') + .with_timezoneOffset(0) + .with_conversionOffset(0) + .with_reason({suspended: 'automatic'}); var resume = builder.makeDeviceEventResume() .with_time('2014-09-25T02:00:00.000Z') .with_deviceTime('2014-09-25T02:00:00') .with_timezoneOffset(0) .with_conversionOffset(0) - .with_status('resumed') .with_reason({resumed: 'manual'}); - var expectedResume = _.assign({}, resume); - expectedResume = expectedResume.set('previous', suspend).done(); + var expectedSuspendResume = _.clone(suspend); + expectedSuspendResume = expectedSuspendResume.with_duration(60000); + + it('a suspend without resume gets annotated', function() { + + var basal = builder.makeSuspendBasal() + .with_time('2014-09-25T01:00:00.000Z') + .with_deviceTime('2014-09-25T01:00:00') + .with_timezoneOffset(0) + .with_conversionOffset(0); - test('a suspend passes through', () => { simulator.suspend(suspend); - expect(simulator.getEvents()).deep.equals([suspend]); + simulator.basal(basal); + simulator.finalize(); + + basal.annotations = [{code: 'basal/unknown-duration'}]; + + var expectedSuspend = _.clone(suspend); + expectedSuspend.annotations = [{code: 'status/incomplete-tuple'}]; + expect(simulator.getEvents()).deep.equals([expectedSuspend.done(), basal.done()]); }); - test('a resume passes through', () => { + it('a resume without suspend gets annotated', function() { + var expectedResume = _.clone(resume); + expectedResume.annotations = [{code: 'status/incomplete-tuple'}]; + simulator.resume(resume); - expect(simulator.getEvents()).deep.equals([resume.done()]); + + expect(simulator.getEvents()).deep.equals([expectedResume.done()]); }); - test('a resume includes a previous when preceded by a suspend', () => { + test('a suspend and resume is combined into a single event', () => { simulator.suspend(suspend); simulator.resume(resume); - expect(simulator.getEvents()).deep.equals([suspend, expectedResume]); + expect(simulator.getEvents()).deep.equals([expectedSuspendResume.done()]); }); test('uses the timestamp of the first suspend if multiple suspends appear before a single resume', () => { @@ -253,7 +265,7 @@ describe('tandemSimulator.js', () => { simulator.suspend(suspend); simulator.suspend(suspend2); simulator.resume(resume); - expect(simulator.getEvents()).deep.equals([suspend, expectedResume]); + expect(simulator.getEvents()).deep.equals([expectedSuspendResume.done()]); }); }); @@ -364,29 +376,6 @@ describe('tandemSimulator.js', () => { expect(simulator.getEvents()).deep.equals([expectedFirstBasal]); }); - test('sets previous on basals other than the first', () => { - var expectedFirstBasal = _.cloneDeep(basal1); - expectedFirstBasal = expectedFirstBasal.set('duration', 3600000).done(); - var expectedSecondBasal = _.cloneDeep(basal2); - expectedSecondBasal = expectedSecondBasal.set('duration', 1800000) - .set('previous', expectedFirstBasal) - .done(); - var expectedThirdBasal = _.cloneDeep(basal3); - expectedThirdBasal = expectedThirdBasal.set('duration', 0) - .set('previous', _.omit(expectedSecondBasal, 'previous')) - .done(); - expectedThirdBasal.annotations = [{code: 'basal/unknown-duration'}]; - simulator.basal(basal1); - simulator.basal(basal2); - simulator.basal(basal3); - simulator.finalize(); - expect(simulator.getEvents()).deep.equals([ - expectedFirstBasal, - expectedSecondBasal, - expectedThirdBasal - ]); - }); - test('temp basal has percentage and payload', () => { var suppressed = builder.makeScheduledBasal() .with_time('2014-09-25T18:05:00.000Z') @@ -400,8 +389,7 @@ describe('tandemSimulator.js', () => { .with_deviceTime('2014-09-25T18:10:00') .with_timezoneOffset(0) .with_conversionOffset(0) - .with_duration(1800000) - .with_previous(suppressed.done()); + .with_duration(1800000); var tempBasalStart = { type: 'temp-basal', subType: 'start', @@ -468,13 +456,13 @@ describe('tandemSimulator.js', () => { .with_timezoneOffset(0) .with_conversionOffset(0) .with_duration(6600000) - .with_previous(suppressed.done()) .set('suppressed',{ type: 'basal', deliveryType: 'scheduled', rate: 1.3 }) .set('percent', 0.65) + .set('rate', 0.845) .set('deviceId', 'tandem12345') .with_payload({'logIndices':1, duration: 1500000}) .done(); @@ -533,12 +521,10 @@ describe('tandemSimulator.js', () => { var expectedTempBasal = tempBasal.with_payload({duration:1800000}) .set('percent',0.65) .with_duration(900000) - .with_previous(suppressed.done()) .done(); var expectedTempBasal2 = tempBasal2.with_payload({duration:1800000}) .set('percent',0.65) .with_duration(900000) - .with_previous(_.omit(expectedTempBasal, 'previous')) .done(); simulator.basal(suppressed); @@ -588,7 +574,6 @@ describe('tandemSimulator.js', () => { simulator.basal(basal2); var expectedSuspend = suspend.set('duration', 1800000) - .set('previous', basal.done()) .done(); expect(simulator.getEvents()).deep.equals([basal.done(),expectedSuspend]); @@ -665,7 +650,6 @@ describe('tandemSimulator.js', () => { var expectedNewDay = _.cloneDeep(temp); expectedNewDay.percent = 0.5; expectedNewDay.payload = {duration:1800000}; - expectedNewDay.previous = expectedTempBasal; expectedNewDay.time = '2014-09-26T00:00:00.000Z'; expectedNewDay.deviceTime = '2014-09-26T00:00:00'; expectedNewDay.annotations = [{code: 'tandem/basal/fabricated-from-new-day'}]; @@ -834,7 +818,7 @@ describe('tandemSimulator.js', () => { simulator.basal(suspend); simulator.newDay(newDay); - expect(simulator.getEvents()).deep.equals([basal.done(),suspendEvent]); + expect(simulator.getEvents()).deep.equals([basal.done()]); }); test('a new-day event during a cancelled temp basal', () => { @@ -879,7 +863,6 @@ describe('tandemSimulator.js', () => { var expectedNewDay = _.cloneDeep(temp); expectedNewDay.percent = 0.5; expectedNewDay.payload = {duration:1800000}; - expectedNewDay.previous = expectedTempBasal; expectedNewDay.time = '2014-09-26T00:00:00.000Z'; expectedNewDay.deviceTime = '2014-09-26T00:00:00'; expectedNewDay.annotations = [{code: 'tandem/basal/fabricated-from-new-day'}]; diff --git a/test/lib/testCommonFunctions.js b/test/lib/testCommonFunctions.js index 251d27a44e..dd45ac08e4 100644 --- a/test/lib/testCommonFunctions.js +++ b/test/lib/testCommonFunctions.js @@ -22,6 +22,7 @@ var builder = require('../../lib/objectBuilder')(); var TZOUtil = require('../../lib/TimezoneOffsetUtil'); var common = require('../../lib/commonFunctions'); +var annotate = require('../../lib/eventAnnotations'); describe('commonFunctions.js', () => { @@ -69,7 +70,7 @@ describe('commonFunctions.js', () => { ] } }; - var finalBasal = common.finalScheduledBasal(basal,settings,'test'); + var finalBasal = common.finalScheduledBasal(basal, settings, 'test'); expect(finalBasal.annotations[0].code).to.equal('final-basal/fabricated-from-schedule'); expect(finalBasal.duration).to.equal(23001000); // 864e5 - millisInDay }); @@ -85,7 +86,7 @@ describe('commonFunctions.js', () => { ] } }; - var finalBasal = common.finalScheduledBasal(basal,settings,'test'); + var finalBasal = common.finalScheduledBasal(basal, settings, 'test'); // TODO: to make the following test more robust, consider using chai-things (new dependency) // that supports assertions on array elements, e.g.: // finalBasal.annotations.should.include.something.that.deep.equals({code : 'basal/unknown-duration'}); @@ -105,7 +106,7 @@ describe('commonFunctions.js', () => { ] } }; - var finalBasal = common.finalScheduledBasal(basal,settings,'test'); + var finalBasal = common.finalScheduledBasal(basal, settings, 'test'); expect(finalBasal.annotations[0].code).to.equal('basal/unknown-duration'); expect(finalBasal.duration).to.equal(0); }); @@ -130,9 +131,96 @@ describe('commonFunctions.js', () => { test('rounds to nearest 15 minutes for clock skew', () => { basal.with_time('2015-11-05T17:05:00.000Z') - .with_conversionOffset(420000); + .with_conversionOffset(420000); expect(common.computeMillisInCurrentDay(basal)).to.equal(61200000); }); }); + describe('stripUnwantedFields', () => { + const record = { + _deduplicator: { + hash: 'ABCD' + }, + annotations: [ + { + code: 'basal/unknown-duration' + } + ], + clockDriftOffset: -257000, + conversionOffset: 0, + deliveryType: 'suspend', + deviceId: 'tandemCIQ1234', + deviceTime: '2024-10-17T16:53:54', + guid: '1234', + id: '5678', + payload: { + logIndices: [ + 282622 + ], + }, + time: '2024-10-17T15:53:54Z', + timezoneOffset: 60, + type: 'basal', + uploadId: 'upid_1234' + }; + + test('removes unwanted fields', () => { + expect(common.stripUnwantedFields(record)).to.deep.equal({ + annotations: [ + { + code: 'basal/unknown-duration' + } + ], + clockDriftOffset: -257000, + conversionOffset: 0, + deliveryType: 'suspend', + deviceId: 'tandemCIQ1234', + deviceTime: '2024-10-17T16:53:54', + payload: { + logIndices: [ + 282622 + ], + }, + time: '2024-10-17T15:53:54Z', + timezoneOffset: 60, + type: 'basal' + }); + }); + }); + + describe('toFixedNumber', () => { + test('to 5 digits', () => { + const value = (1.12345678).toFixedNumber(5); + expect(value).to.equal(1.12346); + }); + }); + + describe('updateDuration', () => { + const lastBasal = builder.makeScheduledBasal() + .with_deviceTime('2015-11-05T17:00:00') + .with_time('2015-11-05T17:00:00.000Z') + .with_rate(0.3) + .with_duration(0) + .with_scheduleName('Test') + .with_conversionOffset(0) + .with_timezoneOffset(0) + .done(); + annotate.annotateEvent(lastBasal, 'basal/unknown-duration'); + + const basal = builder.makeScheduledBasal() + .with_deviceTime('2015-11-05T17:30:00') + .with_time('2015-11-05T17:30:00.000Z') + .with_rate(0.6) + .with_duration(15000) + .with_scheduleName('Test2') + .with_conversionOffset(0) + .with_timezoneOffset(0) + .done(); + + test('updates previous duration and removes annotations', () => { + const updatedBasal = common.updateDuration(basal, lastBasal); + expect(updatedBasal.duration).to.equal(1800000); + expect(updatedBasal.annotations).to.be.undefined; + }); + }); }); diff --git a/yarn.lock b/yarn.lock index 97e9eba55c..563eb6dbf4 100644 --- a/yarn.lock +++ b/yarn.lock @@ -22,7 +22,7 @@ __metadata: languageName: node linkType: hard -"@babel/code-frame@npm:^7.0.0, @babel/code-frame@npm:^7.12.13, @babel/code-frame@npm:^7.25.9, @babel/code-frame@npm:^7.26.0, @babel/code-frame@npm:^7.26.2": +"@babel/code-frame@npm:^7.0.0, @babel/code-frame@npm:^7.12.13, @babel/code-frame@npm:^7.26.0, @babel/code-frame@npm:^7.26.2": version: 7.26.2 resolution: "@babel/code-frame@npm:7.26.2" dependencies: @@ -33,21 +33,14 @@ __metadata: languageName: node linkType: hard -"@babel/compat-data@npm:^7.22.6, @babel/compat-data@npm:^7.25.9, @babel/compat-data@npm:^7.26.0": - version: 7.26.3 - resolution: "@babel/compat-data@npm:7.26.3" - checksum: 85c5a9fb365231688c7faeb977f1d659da1c039e17b416f8ef11733f7aebe11fe330dce20c1844cacf243766c1d643d011df1d13cac9eda36c46c6c475693d21 +"@babel/compat-data@npm:^7.22.6, @babel/compat-data@npm:^7.26.0, @babel/compat-data@npm:^7.26.5": + version: 7.26.8 + resolution: "@babel/compat-data@npm:7.26.8" + checksum: 1bb04c6860c8c9555b933cb9c3caf5ef1dac331a37a351efb67956fc679f695d487aea76e792dd43823702c1300f7906f2a298e50b4a8d7ec199ada9c340c365 languageName: node linkType: hard -"@babel/compat-data@npm:^7.26.5": - version: 7.26.5 - resolution: "@babel/compat-data@npm:7.26.5" - checksum: 7aaac0e79cf6f38478b877b1185413390bfe8ce9f2a19f906cfdf898df82f5a932579bee49c5d0d0a6fd838c715ff59d4958bfd161ef0e857e5eb083efb707b4 - languageName: node - linkType: hard - -"@babel/core@npm:7.26.0, @babel/core@npm:^7.12.3": +"@babel/core@npm:7.26.0": version: 7.26.0 resolution: "@babel/core@npm:7.26.0" dependencies: @@ -70,26 +63,26 @@ __metadata: languageName: node linkType: hard -"@babel/core@npm:^7.11.6, @babel/core@npm:^7.23.9": - version: 7.26.7 - resolution: "@babel/core@npm:7.26.7" +"@babel/core@npm:^7.11.6, @babel/core@npm:^7.12.3, @babel/core@npm:^7.23.9": + version: 7.26.9 + resolution: "@babel/core@npm:7.26.9" dependencies: "@ampproject/remapping": ^2.2.0 "@babel/code-frame": ^7.26.2 - "@babel/generator": ^7.26.5 + "@babel/generator": ^7.26.9 "@babel/helper-compilation-targets": ^7.26.5 "@babel/helper-module-transforms": ^7.26.0 - "@babel/helpers": ^7.26.7 - "@babel/parser": ^7.26.7 - "@babel/template": ^7.25.9 - "@babel/traverse": ^7.26.7 - "@babel/types": ^7.26.7 + "@babel/helpers": ^7.26.9 + "@babel/parser": ^7.26.9 + "@babel/template": ^7.26.9 + "@babel/traverse": ^7.26.9 + "@babel/types": ^7.26.9 convert-source-map: ^2.0.0 debug: ^4.1.0 gensync: ^1.0.0-beta.2 json5: ^2.2.3 semver: ^6.3.1 - checksum: 017ad8db9939ba4cdf003540a5cb33242f139efe337f66041ed7f4876dccf30b297f11bd1c9d666a355bdba517d6a195c08774e7057816bb69361dc621193e42 + checksum: b6e33bdcbb8a5c929760548be400d18cbde1f07922a784586752fd544fbf13c71331406ffdb4fcfe53f79c69ceae602efdca654ad4e9ac0c2af47efe87e7fccd languageName: node linkType: hard @@ -119,29 +112,16 @@ __metadata: languageName: node linkType: hard -"@babel/generator@npm:^7.26.0, @babel/generator@npm:^7.26.3": - version: 7.26.3 - resolution: "@babel/generator@npm:7.26.3" +"@babel/generator@npm:^7.26.0, @babel/generator@npm:^7.26.9, @babel/generator@npm:^7.7.2": + version: 7.26.9 + resolution: "@babel/generator@npm:7.26.9" dependencies: - "@babel/parser": ^7.26.3 - "@babel/types": ^7.26.3 + "@babel/parser": ^7.26.9 + "@babel/types": ^7.26.9 "@jridgewell/gen-mapping": ^0.3.5 "@jridgewell/trace-mapping": ^0.3.25 jsesc: ^3.0.2 - checksum: fb09fa55c66f272badf71c20a3a2cee0fa1a447fed32d1b84f16a668a42aff3e5f5ddc6ed5d832dda1e952187c002ca1a5cdd827022efe591b6ac44cada884ea - languageName: node - linkType: hard - -"@babel/generator@npm:^7.26.5, @babel/generator@npm:^7.7.2": - version: 7.26.5 - resolution: "@babel/generator@npm:7.26.5" - dependencies: - "@babel/parser": ^7.26.5 - "@babel/types": ^7.26.5 - "@jridgewell/gen-mapping": ^0.3.5 - "@jridgewell/trace-mapping": ^0.3.25 - jsesc: ^3.0.2 - checksum: baa42a98cd01efa3ae3634a6caa81d0738e5e0bdba4efbf1ac735216c8d7cf6bdffeab69c468e6ab2063b07db402346113def4962719746756518432f83c53ba + checksum: 57d034fb6c77dfd5e0c8ef368ff544e19cb6a27cb70d6ed5ff0552c618153dc6692d31e7d0f3a408e0fec3a519514b846c909316c3078290f3a3c1e463372eae languageName: node linkType: hard @@ -164,20 +144,7 @@ __metadata: languageName: node linkType: hard -"@babel/helper-compilation-targets@npm:^7.22.6, @babel/helper-compilation-targets@npm:^7.25.9": - version: 7.25.9 - resolution: "@babel/helper-compilation-targets@npm:7.25.9" - dependencies: - "@babel/compat-data": ^7.25.9 - "@babel/helper-validator-option": ^7.25.9 - browserslist: ^4.24.0 - lru-cache: ^5.1.1 - semver: ^6.3.1 - checksum: 3af536e2db358b38f968abdf7d512d425d1018fef2f485d6f131a57a7bcaed32c606b4e148bb230e1508fa42b5b2ac281855a68eb78270f54698c48a83201b9b - languageName: node - linkType: hard - -"@babel/helper-compilation-targets@npm:^7.26.5": +"@babel/helper-compilation-targets@npm:^7.22.6, @babel/helper-compilation-targets@npm:^7.25.9, @babel/helper-compilation-targets@npm:^7.26.5": version: 7.26.5 resolution: "@babel/helper-compilation-targets@npm:7.26.5" dependencies: @@ -191,19 +158,19 @@ __metadata: linkType: hard "@babel/helper-create-class-features-plugin@npm:^7.18.6, @babel/helper-create-class-features-plugin@npm:^7.25.9": - version: 7.25.9 - resolution: "@babel/helper-create-class-features-plugin@npm:7.25.9" + version: 7.26.9 + resolution: "@babel/helper-create-class-features-plugin@npm:7.26.9" dependencies: "@babel/helper-annotate-as-pure": ^7.25.9 "@babel/helper-member-expression-to-functions": ^7.25.9 "@babel/helper-optimise-call-expression": ^7.25.9 - "@babel/helper-replace-supers": ^7.25.9 + "@babel/helper-replace-supers": ^7.26.5 "@babel/helper-skip-transparent-expression-wrappers": ^7.25.9 - "@babel/traverse": ^7.25.9 + "@babel/traverse": ^7.26.9 semver: ^6.3.1 peerDependencies: "@babel/core": ^7.0.0 - checksum: 91dd5f203ed04568c70b052e2f26dfaac7c146447196c00b8ecbb6d3d2f3b517abadb985d3321a19d143adaed6fe17f7f79f8f50e0c20e9d8ad83e1027b42424 + checksum: d445a660d2cdd92e83c04a60f52a304e54e5cc338796b6add9dec00048f1ad12125f78145ab688d029569a9559ef64f8e0de86f456b9e2630ea46f664ffb8e45 languageName: node linkType: hard @@ -277,10 +244,10 @@ __metadata: languageName: node linkType: hard -"@babel/helper-plugin-utils@npm:^7.0.0, @babel/helper-plugin-utils@npm:^7.10.4, @babel/helper-plugin-utils@npm:^7.12.13, @babel/helper-plugin-utils@npm:^7.14.5, @babel/helper-plugin-utils@npm:^7.18.6, @babel/helper-plugin-utils@npm:^7.18.9, @babel/helper-plugin-utils@npm:^7.20.2, @babel/helper-plugin-utils@npm:^7.22.5, @babel/helper-plugin-utils@npm:^7.25.9, @babel/helper-plugin-utils@npm:^7.8.0, @babel/helper-plugin-utils@npm:^7.8.3": - version: 7.25.9 - resolution: "@babel/helper-plugin-utils@npm:7.25.9" - checksum: e19ec8acf0b696756e6d84531f532c5fe508dce57aa68c75572a77798bd04587a844a9a6c8ea7d62d673e21fdc174d091c9097fb29aea1c1b49f9c6eaa80f022 +"@babel/helper-plugin-utils@npm:^7.0.0, @babel/helper-plugin-utils@npm:^7.10.4, @babel/helper-plugin-utils@npm:^7.12.13, @babel/helper-plugin-utils@npm:^7.14.5, @babel/helper-plugin-utils@npm:^7.18.6, @babel/helper-plugin-utils@npm:^7.18.9, @babel/helper-plugin-utils@npm:^7.20.2, @babel/helper-plugin-utils@npm:^7.22.5, @babel/helper-plugin-utils@npm:^7.25.9, @babel/helper-plugin-utils@npm:^7.26.5, @babel/helper-plugin-utils@npm:^7.8.0, @babel/helper-plugin-utils@npm:^7.8.3": + version: 7.26.5 + resolution: "@babel/helper-plugin-utils@npm:7.26.5" + checksum: 4771fbb1711c624c62d12deabc2ed7435a6e6994b6ce09d5ede1bc1bf19be59c3775461a1e693bdd596af865685e87bb2abc778f62ceadc1b2095a8e2aa74180 languageName: node linkType: hard @@ -297,16 +264,16 @@ __metadata: languageName: node linkType: hard -"@babel/helper-replace-supers@npm:^7.25.9": - version: 7.25.9 - resolution: "@babel/helper-replace-supers@npm:7.25.9" +"@babel/helper-replace-supers@npm:^7.25.9, @babel/helper-replace-supers@npm:^7.26.5": + version: 7.26.5 + resolution: "@babel/helper-replace-supers@npm:7.26.5" dependencies: "@babel/helper-member-expression-to-functions": ^7.25.9 "@babel/helper-optimise-call-expression": ^7.25.9 - "@babel/traverse": ^7.25.9 + "@babel/traverse": ^7.26.5 peerDependencies: "@babel/core": ^7.0.0 - checksum: 84f40e12520b7023e52d289bf9d569a06284879fe23bbbacad86bec5d978b2669769f11b073fcfeb1567d8c547168323005fda88607a4681ecaeb4a5cdd48bb9 + checksum: c5ab31b29c7cc09e30278f8860ecdb873ce6c84b5c08bc5239c369c7c4fe9f0a63cda61b55b7bbd20edb4e5dc32e73087cc3c57d85264834bd191551d1499185 languageName: node linkType: hard @@ -352,45 +319,24 @@ __metadata: languageName: node linkType: hard -"@babel/helpers@npm:^7.26.0": - version: 7.26.0 - resolution: "@babel/helpers@npm:7.26.0" - dependencies: - "@babel/template": ^7.25.9 - "@babel/types": ^7.26.0 - checksum: d77fe8d45033d6007eadfa440355c1355eed57902d5a302f450827ad3d530343430a21210584d32eef2f216ae463d4591184c6fc60cf205bbf3a884561469200 - languageName: node - linkType: hard - -"@babel/helpers@npm:^7.26.7": - version: 7.26.7 - resolution: "@babel/helpers@npm:7.26.7" +"@babel/helpers@npm:^7.26.0, @babel/helpers@npm:^7.26.9": + version: 7.26.9 + resolution: "@babel/helpers@npm:7.26.9" dependencies: - "@babel/template": ^7.25.9 - "@babel/types": ^7.26.7 - checksum: 1c93604c7fd6dbd7aa6f3eb2f9fa56369f9ad02bac8b3afb902de6cd4264beb443cc8589bede3790ca28d7477d4c07801fe6f4943f9833ac5956b72708bbd7ac + "@babel/template": ^7.26.9 + "@babel/types": ^7.26.9 + checksum: 06363f8288a24c1cfda03eccd775ac22f79cba319b533cb0e5d0f2a04a33512881cc3f227a4c46324935504fb92999cc4758b69b5e7b3846107eadcb5ee0abca languageName: node linkType: hard -"@babel/parser@npm:^7.1.0, @babel/parser@npm:^7.14.7, @babel/parser@npm:^7.20.7, @babel/parser@npm:^7.25.9, @babel/parser@npm:^7.26.0, @babel/parser@npm:^7.26.3": - version: 7.26.3 - resolution: "@babel/parser@npm:7.26.3" +"@babel/parser@npm:^7.1.0, @babel/parser@npm:^7.14.7, @babel/parser@npm:^7.20.7, @babel/parser@npm:^7.23.9, @babel/parser@npm:^7.26.0, @babel/parser@npm:^7.26.9": + version: 7.26.9 + resolution: "@babel/parser@npm:7.26.9" dependencies: - "@babel/types": ^7.26.3 + "@babel/types": ^7.26.9 bin: parser: ./bin/babel-parser.js - checksum: e2bff2e9fa6540ee18fecc058bc74837eda2ddcecbe13454667314a93fc0ba26c1fb862c812d84f6d5f225c3bd8d191c3a42d4296e287a882c4e1f82ff2815ff - languageName: node - linkType: hard - -"@babel/parser@npm:^7.23.9, @babel/parser@npm:^7.26.5, @babel/parser@npm:^7.26.7": - version: 7.26.7 - resolution: "@babel/parser@npm:7.26.7" - dependencies: - "@babel/types": ^7.26.7 - bin: - parser: ./bin/babel-parser.js - checksum: 22aafd7a6fb9ae577cf192141e5b879477fc087872b8953df0eed60ab7e2397d01aa8d92690eb7ba406f408035dd27c86fbf9967c9a37abd9bf4b1d7cf46a823 + checksum: 2df965dbf3c67d19dc437412ceef23033b4d39b0dbd7cb498d8ab9ad9e1738338656ee72676199773b37d658edf9f4161cf255515234fed30695d74e73be5514 languageName: node linkType: hard @@ -827,13 +773,13 @@ __metadata: linkType: hard "@babel/plugin-syntax-pipeline-operator@npm:^7.25.9": - version: 7.25.9 - resolution: "@babel/plugin-syntax-pipeline-operator@npm:7.25.9" + version: 7.26.7 + resolution: "@babel/plugin-syntax-pipeline-operator@npm:7.26.7" dependencies: - "@babel/helper-plugin-utils": ^7.25.9 + "@babel/helper-plugin-utils": ^7.26.5 peerDependencies: "@babel/core": ^7.0.0-0 - checksum: c320c002dbedf4c9b84a40faffb6931e10233053e131110b27e547608e20dfdcf4a9502c44bfbbbd0e34c86f0eef6922ef7665665ffcb9055280627951cc8187 + checksum: 5f1ffe6d00f050c1dd665854f41784d8619fccf5d3e4b48a30cc24a3a65bd3d68e704e69bc9fc6260fb027035a2cc9827f45ba938a740432220ced7b60230df2 languageName: node linkType: hard @@ -894,15 +840,15 @@ __metadata: linkType: hard "@babel/plugin-transform-async-generator-functions@npm:^7.25.9": - version: 7.25.9 - resolution: "@babel/plugin-transform-async-generator-functions@npm:7.25.9" + version: 7.26.8 + resolution: "@babel/plugin-transform-async-generator-functions@npm:7.26.8" dependencies: - "@babel/helper-plugin-utils": ^7.25.9 + "@babel/helper-plugin-utils": ^7.26.5 "@babel/helper-remap-async-to-generator": ^7.25.9 - "@babel/traverse": ^7.25.9 + "@babel/traverse": ^7.26.8 peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 41e02c18c2a57de9f274fa2c5a1bf81a20ab5f321db29cc3051512b9c5bdf3f1a8c42f1fc282cb62343c6d50849f992eede954d5f7fb5e7df48ae0c59ea7e054 + checksum: 10424a1bbfbc7ffdb13cef1e832f76bb2d393a9fbfaa1eaa3091a8f6ec3e2ac0b66cf04fca9cb3fb4dbf3d1bd404d72dfce4a3742b4ef21f6271aca7076a65ef languageName: node linkType: hard @@ -920,13 +866,13 @@ __metadata: linkType: hard "@babel/plugin-transform-block-scoped-functions@npm:^7.25.9": - version: 7.25.9 - resolution: "@babel/plugin-transform-block-scoped-functions@npm:7.25.9" + version: 7.26.5 + resolution: "@babel/plugin-transform-block-scoped-functions@npm:7.26.5" dependencies: - "@babel/helper-plugin-utils": ^7.25.9 + "@babel/helper-plugin-utils": ^7.26.5 peerDependencies: "@babel/core": ^7.0.0-0 - checksum: bf31896556b33a80f017af3d445ceb532ec0f5ca9d69bc211a963ac92514d172d5c24c5ac319f384d9dfa7f1a4d8dc23032c2fe3e74f98a59467ecd86f7033ae + checksum: f2046c09bf8e588bfb1a6342d0eee733189102cf663ade27adb0130f3865123af5816b40a55ec8d8fa09271b54dfdaf977cd2f8e0b3dc97f18e690188d5a2174 languageName: node linkType: hard @@ -1073,14 +1019,14 @@ __metadata: linkType: hard "@babel/plugin-transform-for-of@npm:^7.25.9": - version: 7.25.9 - resolution: "@babel/plugin-transform-for-of@npm:7.25.9" + version: 7.26.9 + resolution: "@babel/plugin-transform-for-of@npm:7.26.9" dependencies: - "@babel/helper-plugin-utils": ^7.25.9 + "@babel/helper-plugin-utils": ^7.26.5 "@babel/helper-skip-transparent-expression-wrappers": ^7.25.9 peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 41b56e70256a29fc26ed7fb95ece062d7ec2f3b6ea8f0686349ffd004cd4816132085ee21165b89c502ee7161cb7cfb12510961638851357945dc7bc546475b7 + checksum: 361323cfc1d9e9dc0bf0d68326b5e7f4da5b8a8be8931f6cacda749d39b88ee1b0f9b4d8b771a5a4d52bb881a90da97950c8a9e6fb47f2c9db11d91f6351768e languageName: node linkType: hard @@ -1215,13 +1161,13 @@ __metadata: linkType: hard "@babel/plugin-transform-nullish-coalescing-operator@npm:^7.25.9": - version: 7.25.9 - resolution: "@babel/plugin-transform-nullish-coalescing-operator@npm:7.25.9" + version: 7.26.6 + resolution: "@babel/plugin-transform-nullish-coalescing-operator@npm:7.26.6" dependencies: - "@babel/helper-plugin-utils": ^7.25.9 + "@babel/helper-plugin-utils": ^7.26.5 peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 26e03b1c2c0408cc300e46d8f8cb639653ff3a7b03456d0d8afbb53c44f33a89323f51d99991dade3a5676921119bbdf869728bb7911799b5ef99ffafa2cdd24 + checksum: 752837d532b85c41f6bb868e83809605f513bc9a3b8e88ac3d43757c9bf839af4f246874c1c6d6902bb2844d355efccae602c3856098911f8abdd603672f8379 languageName: node linkType: hard @@ -1473,24 +1419,24 @@ __metadata: linkType: hard "@babel/plugin-transform-template-literals@npm:^7.25.9": - version: 7.25.9 - resolution: "@babel/plugin-transform-template-literals@npm:7.25.9" + version: 7.26.8 + resolution: "@babel/plugin-transform-template-literals@npm:7.26.8" dependencies: - "@babel/helper-plugin-utils": ^7.25.9 + "@babel/helper-plugin-utils": ^7.26.5 peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 92eb1d6e2d95bd24abbb74fa7640d02b66ff6214e0bb616d7fda298a7821ce15132a4265d576a3502a347a3c9e94b6c69ed265bb0784664592fa076785a3d16a + checksum: 65874c8844ce906507cd5b9c78950d6173f8339b6416a2a9e763021db5a7045315a6f0e58976ec4af5e960c003ef322576c105130a644addb8f94d1a0821a972 languageName: node linkType: hard "@babel/plugin-transform-typeof-symbol@npm:^7.25.9": - version: 7.25.9 - resolution: "@babel/plugin-transform-typeof-symbol@npm:7.25.9" + version: 7.26.7 + resolution: "@babel/plugin-transform-typeof-symbol@npm:7.26.7" dependencies: - "@babel/helper-plugin-utils": ^7.25.9 + "@babel/helper-plugin-utils": ^7.26.5 peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 3f9458840d96f61502f0e9dfaae3efe8325fa0b2151e24ea0d41307f28cdd166905419f5a43447ce0f1ae4bfd001f3906b658839a60269c254168164090b4c73 + checksum: 1fcc48bde1426527d9905d561884e1ecaf3c03eb5abb507d33f71591f8da0c384e92097feaf91cc30692e04fb7f5e6ff1cb172acc5de7675d93fdb42db850d6a languageName: node linkType: hard @@ -1684,82 +1630,48 @@ __metadata: languageName: node linkType: hard -"@babel/runtime@npm:^7.1.2, @babel/runtime@npm:^7.10.5, @babel/runtime@npm:^7.12.0, @babel/runtime@npm:^7.12.1, @babel/runtime@npm:^7.12.5, @babel/runtime@npm:^7.15.4, @babel/runtime@npm:^7.18.3, @babel/runtime@npm:^7.23.9, @babel/runtime@npm:^7.5.5, @babel/runtime@npm:^7.8.4, @babel/runtime@npm:^7.8.7, @babel/runtime@npm:^7.9.0, @babel/runtime@npm:^7.9.2": - version: 7.26.0 - resolution: "@babel/runtime@npm:7.26.0" +"@babel/runtime@npm:^7.1.2, @babel/runtime@npm:^7.10.5, @babel/runtime@npm:^7.12.0, @babel/runtime@npm:^7.12.1, @babel/runtime@npm:^7.12.5, @babel/runtime@npm:^7.15.4, @babel/runtime@npm:^7.18.3, @babel/runtime@npm:^7.22.5, @babel/runtime@npm:^7.23.9, @babel/runtime@npm:^7.5.5, @babel/runtime@npm:^7.8.4, @babel/runtime@npm:^7.8.7, @babel/runtime@npm:^7.9.0, @babel/runtime@npm:^7.9.2": + version: 7.26.9 + resolution: "@babel/runtime@npm:7.26.9" dependencies: regenerator-runtime: ^0.14.0 - checksum: c8e2c0504ab271b3467a261a8f119bf2603eb857a0d71e37791f4e3fae00f681365073cc79f141ddaa90c6077c60ba56448004ad5429d07ac73532be9f7cf28a + checksum: 838492d8a925092f9ccfbd82ec183a54f430af3a4ce88fb1337a4570629202d5123bad3097a5b8df53822504d12ccb29f45c0f6842e86094f0164f17a51eec92 languageName: node linkType: hard -"@babel/runtime@npm:^7.22.5": - version: 7.26.7 - resolution: "@babel/runtime@npm:7.26.7" - dependencies: - regenerator-runtime: ^0.14.0 - checksum: a1664a08f3f4854b895b540cca2f5f5c6c1993b5fb788c9615d70fc201e16bb254df8e0550c83eaf2749a14d87775e11a7c9ded6161203e9da7a4a323d546925 - languageName: node - linkType: hard - -"@babel/template@npm:^7.25.9, @babel/template@npm:^7.3.3": - version: 7.25.9 - resolution: "@babel/template@npm:7.25.9" - dependencies: - "@babel/code-frame": ^7.25.9 - "@babel/parser": ^7.25.9 - "@babel/types": ^7.25.9 - checksum: 103641fea19c7f4e82dc913aa6b6ac157112a96d7c724d513288f538b84bae04fb87b1f1e495ac1736367b1bc30e10f058b30208fb25f66038e1f1eb4e426472 - languageName: node - linkType: hard - -"@babel/traverse@npm:^7.25.9": - version: 7.26.4 - resolution: "@babel/traverse@npm:7.26.4" +"@babel/template@npm:^7.25.9, @babel/template@npm:^7.26.9, @babel/template@npm:^7.3.3": + version: 7.26.9 + resolution: "@babel/template@npm:7.26.9" dependencies: "@babel/code-frame": ^7.26.2 - "@babel/generator": ^7.26.3 - "@babel/parser": ^7.26.3 - "@babel/template": ^7.25.9 - "@babel/types": ^7.26.3 - debug: ^4.3.1 - globals: ^11.1.0 - checksum: dcdf51b27ab640291f968e4477933465c2910bfdcbcff8f5315d1f29b8ff861864f363e84a71fb489f5e9708e8b36b7540608ce019aa5e57ef7a4ba537e62700 + "@babel/parser": ^7.26.9 + "@babel/types": ^7.26.9 + checksum: 32259298c775e543ab994daff0c758b3d6a184349b146d6497aa46cec5907bc47a6bc09e7295a81a5eccfbd023d4811a9777cb5d698d582d09a87cabf5b576e7 languageName: node linkType: hard -"@babel/traverse@npm:^7.26.7": - version: 7.26.7 - resolution: "@babel/traverse@npm:7.26.7" +"@babel/traverse@npm:^7.25.9, @babel/traverse@npm:^7.26.5, @babel/traverse@npm:^7.26.8, @babel/traverse@npm:^7.26.9": + version: 7.26.9 + resolution: "@babel/traverse@npm:7.26.9" dependencies: "@babel/code-frame": ^7.26.2 - "@babel/generator": ^7.26.5 - "@babel/parser": ^7.26.7 - "@babel/template": ^7.25.9 - "@babel/types": ^7.26.7 + "@babel/generator": ^7.26.9 + "@babel/parser": ^7.26.9 + "@babel/template": ^7.26.9 + "@babel/types": ^7.26.9 debug: ^4.3.1 globals: ^11.1.0 - checksum: 22ea8aed130e51db320ff7b3b1555f7dfb82fa860669e593e62990bff004cf09d3dca6fecb8afbac5e51973b703d0cdebf1dc0f6c0021901506f90443c40271b - languageName: node - linkType: hard - -"@babel/types@npm:^7.0.0, @babel/types@npm:^7.20.7, @babel/types@npm:^7.25.9, @babel/types@npm:^7.26.0, @babel/types@npm:^7.26.3, @babel/types@npm:^7.3.3, @babel/types@npm:^7.4.4": - version: 7.26.3 - resolution: "@babel/types@npm:7.26.3" - dependencies: - "@babel/helper-string-parser": ^7.25.9 - "@babel/helper-validator-identifier": ^7.25.9 - checksum: 195f428080dcaadbcecc9445df7f91063beeaa91b49ccd78f38a5af6b75a6a58391d0c6614edb1ea322e57889a1684a0aab8e667951f820196901dd341f931e9 + checksum: d42d3a5e61422d96467f517447b5e254edbd64e4dbf3e13b630704d1f49beaa5209246dc6f45ba53522293bd4760ff720496d2c1ef189ecce52e9e63d9a59aa8 languageName: node linkType: hard -"@babel/types@npm:^7.26.5, @babel/types@npm:^7.26.7": - version: 7.26.7 - resolution: "@babel/types@npm:7.26.7" +"@babel/types@npm:^7.0.0, @babel/types@npm:^7.20.7, @babel/types@npm:^7.25.9, @babel/types@npm:^7.26.0, @babel/types@npm:^7.26.9, @babel/types@npm:^7.3.3, @babel/types@npm:^7.4.4": + version: 7.26.9 + resolution: "@babel/types@npm:7.26.9" dependencies: "@babel/helper-string-parser": ^7.25.9 "@babel/helper-validator-identifier": ^7.25.9 - checksum: cfb12e8794ebda6c95c92f3b90f14a9ec87ab532a247d887233068f72f8c287c0fa2e8d3d6ed5a4e512729844f7f73a613cb87d86077ae60a63a2e870e697307 + checksum: cc124c149615deb30343a4c81ac5b0e3a68bdb4b1bd61a91a2859ee8e5e5f400f6ff65be4740f407c17bfc09baa9c777e7f8f765dccf3284963956b67ac95a38 languageName: node linkType: hard @@ -1795,15 +1707,15 @@ __metadata: linkType: hard "@electron/asar@npm:^3.2.7": - version: 3.2.18 - resolution: "@electron/asar@npm:3.2.18" + version: 3.3.1 + resolution: "@electron/asar@npm:3.3.1" dependencies: commander: ^5.0.0 glob: ^7.1.6 minimatch: ^3.0.4 bin: asar: bin/asar.js - checksum: bbb983d9395d92a6cb7405435d734e465f13346b78b398ba787ed04b8d626b634eecaadfa04f43a73ba5883613acb96807869b31953304038352c8b9d4197532 + checksum: ea4c6bc26c24e3549fc1684672ac8ccd15df4358b3959a67b59b3eea72aaa784edfb1ea5b0646c576885c08ac69af6946f2f09b984c78854517633142eda2cd8 languageName: node linkType: hard @@ -2663,14 +2575,14 @@ __metadata: linkType: hard "@mui/types@npm:^7.2.15": - version: 7.2.21 - resolution: "@mui/types@npm:7.2.21" + version: 7.2.22 + resolution: "@mui/types@npm:7.2.22" peerDependencies: "@types/react": ^17.0.0 || ^18.0.0 || ^19.0.0 peerDependenciesMeta: "@types/react": optional: true - checksum: d3c005358777204debb75c684a25ec324df8a0e108440a4b8bd7c658716d6712d993253294fc790c4c9f431156502f1cafd63819643c7a7b9651ac3216ebf9f2 + checksum: 9e1fa710a7d25d71b5f8bec8ccfd04a51c09eac8ca379af1a72faf9dab7b5ee8f4827d66efbaa365b605affad9a5d616ab2b85b91ba3747c45f6a49299e1515e languageName: node linkType: hard @@ -3052,14 +2964,14 @@ __metadata: linkType: hard "@types/express-serve-static-core@npm:*, @types/express-serve-static-core@npm:^5.0.0": - version: 5.0.4 - resolution: "@types/express-serve-static-core@npm:5.0.4" + version: 5.0.6 + resolution: "@types/express-serve-static-core@npm:5.0.6" dependencies: "@types/node": "*" "@types/qs": "*" "@types/range-parser": "*" "@types/send": "*" - checksum: fb094756f4b0c88ae367c385e1857081d39f33cc38ba90d198a758528ffa0889485397871550eb9af986e4ccb0256c7304a771757c8775907506bd9f667d1d66 + checksum: bc3ea44923da7d1ffaa29eff7cc41a2b05f7340e8879fe9ee40717859937d73bcd635fcc0f8232f66af942624cc48bff42971e9e2c4075db6afe478534245855 languageName: node linkType: hard @@ -3149,11 +3061,11 @@ __metadata: linkType: hard "@types/http-proxy@npm:^1.17.8": - version: 1.17.15 - resolution: "@types/http-proxy@npm:1.17.15" + version: 1.17.16 + resolution: "@types/http-proxy@npm:1.17.16" dependencies: "@types/node": "*" - checksum: d96eaf4e22232b587b46256b89c20525c453216684481015cf50fb385b0b319b883749ccb77dee9af57d107e8440cdacd56f4234f65176d317e9777077ff5bf3 + checksum: f5ab4afe7e3feba9d87bdddbf44e03d9a836bd2cdab679a794badbff7c4bfb6bebf46bfe22d9964eb1820e1349f2ff7807cccb20fd27cb17f03db849289e5892 languageName: node linkType: hard @@ -3206,9 +3118,9 @@ __metadata: linkType: hard "@types/lodash@npm:^4.14.165": - version: 4.17.14 - resolution: "@types/lodash@npm:4.17.14" - checksum: 2dbeaff92b31cb523f6bc4bb99a3d8c88fbb001d54f2367a888add85784fb213744a9b1600e1e98b6790ab191fdb6ec839eb0e3d63fcf6fb6cf1ebe4c3d21149 + version: 4.17.15 + resolution: "@types/lodash@npm:4.17.15" + checksum: 5037564154411f4ac0b187b914031c150520a7ca037dea007f774b7123c97c0da55b2e41ae81dc1bc80ff09e8d6a236455ea676d5062af12c592b8e81baec7bf languageName: node linkType: hard @@ -3220,9 +3132,9 @@ __metadata: linkType: hard "@types/ms@npm:*": - version: 0.7.34 - resolution: "@types/ms@npm:0.7.34" - checksum: f38d36e7b6edecd9badc9cf50474159e9da5fa6965a75186cceaf883278611b9df6669dc3a3cc122b7938d317b68a9e3d573d316fcb35d1be47ec9e468c6bd8a + version: 2.1.0 + resolution: "@types/ms@npm:2.1.0" + checksum: 532d2ebb91937ccc4a89389715e5b47d4c66e708d15942fe6cc25add6dc37b2be058230a327dd50f43f89b8b6d5d52b74685a9e8f70516edfc9bdd6be910eff4 languageName: node linkType: hard @@ -3236,20 +3148,20 @@ __metadata: linkType: hard "@types/node@npm:*": - version: 22.10.5 - resolution: "@types/node@npm:22.10.5" + version: 22.13.5 + resolution: "@types/node@npm:22.13.5" dependencies: undici-types: ~6.20.0 - checksum: 3b0e966df4e130edac3ad034f1cddbe134e70f11556062468c9fbd749a3b07a44445a3a75a7eec68a104930bf05d4899f1a418c4ae48493d2c8c1544d8594bcc + checksum: 8789d9bc3efd212819fd03f7bbd429901b076703e9852ccf4950c8c7cd300d5d5a05f273d0936cbaf28194485d2bd0c265a1a25390720e353a53359526c28fb3 languageName: node linkType: hard "@types/node@npm:^20.9.0": - version: 20.17.12 - resolution: "@types/node@npm:20.17.12" + version: 20.17.19 + resolution: "@types/node@npm:20.17.19" dependencies: undici-types: ~6.19.2 - checksum: 0c0dbeb4e1480a23071ec38e97bb3d776e0ae9828174bf45f9edcf277caa955945cb10d31d84f7a73c66aaf92ae2e022be6331bd00a4bed1f2ad9639a411d17e + checksum: 4eb29628e293d47494c1fe8eaacbaf9f1317cc2ada176a74753d0ea6e3ff6cb5f3e97ba3baa1435d9934950f0fec5693272f2805387f2e70c119038ee7608c20 languageName: node linkType: hard @@ -3278,9 +3190,9 @@ __metadata: linkType: hard "@types/qs@npm:*": - version: 6.9.17 - resolution: "@types/qs@npm:6.9.17" - checksum: fc3beda0be70e820ddabaa361e8dfec5e09b482b8f6cf1515615479a027dd06cd5ba0ffbd612b654c2605523f45f484c8905a475623d6cd0c4cadcf5d0c517f5 + version: 6.9.18 + resolution: "@types/qs@npm:6.9.18" + checksum: 152fab96efd819cc82ae67c39f089df415da6deddb48f1680edaaaa4e86a2a597de7b2ff0ad391df66d11a07006a08d52c9405e86b8cb8f3d5ba15881fe56cc7 languageName: node linkType: hard @@ -3313,11 +3225,11 @@ __metadata: linkType: hard "@types/react@npm:*": - version: 19.0.4 - resolution: "@types/react@npm:19.0.4" + version: 19.0.10 + resolution: "@types/react@npm:19.0.10" dependencies: csstype: ^3.0.2 - checksum: 96efaf1f4a682f606281aec06fe34f6ebf0741815b0be3dd897bc3b28eb60e4abccb0e184f202e1fa38d1ed7c050a68bfba5c1e3b51c0f25cb554da263e7c6c8 + checksum: e257e87bc3464825014523aecc700540a9da41c3c23136c03da9b2b7999251ac70ef9e594febdefeea6abe51da2475b42e5d96af6559d76f8d54bffc0b0ddacd languageName: node linkType: hard @@ -3384,9 +3296,9 @@ __metadata: linkType: hard "@types/verror@npm:^1.10.3": - version: 1.10.10 - resolution: "@types/verror@npm:1.10.10" - checksum: 2865053bded09809edb8bcb899bf8fb82701000434d979d7aa72f9163c1c5b88d1e3bca47e4a4f5eb81d7ec168842c7fffe93dc56c4d4b7afc9d38d92408212d + version: 1.10.11 + resolution: "@types/verror@npm:1.10.11" + checksum: 647a8c43f1510a7ed113426bc428e4d6914da5912946d77b1f6e37937493bc288f49656e1114794f0e5841c14cc1582887cf605952e4e4e0e77e3cd825790fad languageName: node linkType: hard @@ -3438,70 +3350,70 @@ __metadata: languageName: node linkType: hard -"@typescript-eslint/scope-manager@npm:8.19.1": - version: 8.19.1 - resolution: "@typescript-eslint/scope-manager@npm:8.19.1" +"@typescript-eslint/scope-manager@npm:8.25.0": + version: 8.25.0 + resolution: "@typescript-eslint/scope-manager@npm:8.25.0" dependencies: - "@typescript-eslint/types": 8.19.1 - "@typescript-eslint/visitor-keys": 8.19.1 - checksum: 972ee3bc3339f549e206f01b3db30b71d99090b4d581ff1b73ce833d95e4e2f6520f7f227174c53393a2646980068463daaaeb945e417458cf6f37d60e31c173 + "@typescript-eslint/types": 8.25.0 + "@typescript-eslint/visitor-keys": 8.25.0 + checksum: 07782325450b5ab23a9ca3ccc3f681f7db738d9282ede17255e8d10217fe1375f2ee6c4c320d9a03a5477ef1fc0431358e69347bc7133e68f4f14a909ffb4328 languageName: node linkType: hard -"@typescript-eslint/types@npm:8.19.1": - version: 8.19.1 - resolution: "@typescript-eslint/types@npm:8.19.1" - checksum: 76756b9c5496e1463255aa3c881eaec51a6fe718894b91f49929e9e7e258111d86a9c38a9c76b5ada29293a4cb60b96cffac82a203ec47053aa138f298ffab67 +"@typescript-eslint/types@npm:8.25.0": + version: 8.25.0 + resolution: "@typescript-eslint/types@npm:8.25.0" + checksum: 958395fb209609beda4a57b9d52138a6f5a1941f2d39aed616e9aadad2fd453fafd5b117fe0ebf1db37aded8e21be5469634452ae7b70212f978db1799d907bf languageName: node linkType: hard -"@typescript-eslint/typescript-estree@npm:8.19.1": - version: 8.19.1 - resolution: "@typescript-eslint/typescript-estree@npm:8.19.1" +"@typescript-eslint/typescript-estree@npm:8.25.0": + version: 8.25.0 + resolution: "@typescript-eslint/typescript-estree@npm:8.25.0" dependencies: - "@typescript-eslint/types": 8.19.1 - "@typescript-eslint/visitor-keys": 8.19.1 + "@typescript-eslint/types": 8.25.0 + "@typescript-eslint/visitor-keys": 8.25.0 debug: ^4.3.4 fast-glob: ^3.3.2 is-glob: ^4.0.3 minimatch: ^9.0.4 semver: ^7.6.0 - ts-api-utils: ^2.0.0 + ts-api-utils: ^2.0.1 peerDependencies: typescript: ">=4.8.4 <5.8.0" - checksum: 982ac1735d076c595c3b6bfb4c2d02a41bb3cc27d8d05bdac9a08e9f007be3f151ded0f7e691de00b2aa86458e1cd5ef49cf1c19dd38d24269b1f107db2a700b + checksum: b103847df242dc9de3b046dd4aa33840732e17964388969110e13627f7e20fdc10801eb4718a4efd0ead470c411fdf96df791e43d2d28cf617ae416905897129 languageName: node linkType: hard "@typescript-eslint/utils@npm:^6.0.0 || ^7.0.0 || ^8.0.0": - version: 8.19.1 - resolution: "@typescript-eslint/utils@npm:8.19.1" + version: 8.25.0 + resolution: "@typescript-eslint/utils@npm:8.25.0" dependencies: "@eslint-community/eslint-utils": ^4.4.0 - "@typescript-eslint/scope-manager": 8.19.1 - "@typescript-eslint/types": 8.19.1 - "@typescript-eslint/typescript-estree": 8.19.1 + "@typescript-eslint/scope-manager": 8.25.0 + "@typescript-eslint/types": 8.25.0 + "@typescript-eslint/typescript-estree": 8.25.0 peerDependencies: eslint: ^8.57.0 || ^9.0.0 typescript: ">=4.8.4 <5.8.0" - checksum: 745c24b9538c2c4e41cda0cfe73b78d1a8aaec3958ece128cf086f1e8d09f3f53b3299610570ae5a921300c05e43d181eda099acfb3218fadf3b310bf49b290e + checksum: 60572c88805c0b3eb0d41ee9fe736931554db22e1a4ad4d274bb515894f622605109cbf0b8742fbf895eb956366df981e1700776e6c56381b4528f71643a6460 languageName: node linkType: hard -"@typescript-eslint/visitor-keys@npm:8.19.1": - version: 8.19.1 - resolution: "@typescript-eslint/visitor-keys@npm:8.19.1" +"@typescript-eslint/visitor-keys@npm:8.25.0": + version: 8.25.0 + resolution: "@typescript-eslint/visitor-keys@npm:8.25.0" dependencies: - "@typescript-eslint/types": 8.19.1 + "@typescript-eslint/types": 8.25.0 eslint-visitor-keys: ^4.2.0 - checksum: dc68a7e46cd73579c81ec5594315a8564fcc74984b3d399331e66abd48db956acfa24d445660f133609d7a969a88819ca73ded493f109f42ba659958df52be7e + checksum: e9570dd2ff84d10994af8906720e4e19e6a5c180b88b933c1b21b7ce1752a083dd5e513f9aa0d0dc6a17160eced893e55e7d3b2a3a2ae47d930e3f012fa23ef9 languageName: node linkType: hard "@ungap/structured-clone@npm:^1.2.0": - version: 1.2.1 - resolution: "@ungap/structured-clone@npm:1.2.1" - checksum: 1e3b9fef293118861f0b2159b3695fc7f3793c0707095888ebb3ac7183f78c390e68f04cd4b4cf9ac979ae0da454505e08b3aae887cdd639609a3fe529e19e59 + version: 1.3.0 + resolution: "@ungap/structured-clone@npm:1.3.0" + checksum: 64ed518f49c2b31f5b50f8570a1e37bde3b62f2460042c50f132430b2d869c4a6586f13aa33a58a4722715b8158c68cae2827389d6752ac54da2893c83e480fc languageName: node linkType: hard @@ -4200,6 +4112,13 @@ __metadata: languageName: node linkType: hard +"async-function@npm:^1.0.0": + version: 1.0.0 + resolution: "async-function@npm:1.0.0" + checksum: 9102e246d1ed9b37ac36f57f0a6ca55226876553251a31fc80677e71471f463a54c872dc78d5d7f80740c8ba624395cccbe8b60f7b690c4418f487d8e9fd1106 + languageName: node + linkType: hard + "async@npm:0.9.0": version: 0.9.0 resolution: "async@npm:0.9.0" @@ -4918,13 +4837,13 @@ __metadata: languageName: node linkType: hard -"call-bind-apply-helpers@npm:^1.0.0, call-bind-apply-helpers@npm:^1.0.1": - version: 1.0.1 - resolution: "call-bind-apply-helpers@npm:1.0.1" +"call-bind-apply-helpers@npm:^1.0.0, call-bind-apply-helpers@npm:^1.0.1, call-bind-apply-helpers@npm:^1.0.2": + version: 1.0.2 + resolution: "call-bind-apply-helpers@npm:1.0.2" dependencies: es-errors: ^1.3.0 function-bind: ^1.1.2 - checksum: 3c55343261bb387c58a4762d15ad9d42053659a62681ec5eb50690c6b52a4a666302a01d557133ce6533e8bd04530ee3b209f23dd06c9577a1925556f8fcccdf + checksum: b2863d74fcf2a6948221f65d95b91b4b2d90cfe8927650b506141e669f7d5de65cea191bf788838bc40d13846b7886c5bc5c84ab96c3adbcf88ad69a72fcdc6b languageName: node linkType: hard @@ -4993,10 +4912,17 @@ __metadata: languageName: node linkType: hard +"caniuse-lite@npm:1.0.30001651": + version: 1.0.30001651 + resolution: "caniuse-lite@npm:1.0.30001651" + checksum: c31a5a01288e70cdbbfb5cd94af3df02f295791673173b8ce6d6a16db4394a6999197d44190be5a6ff06b8c2c7d2047e94dfd5e5eb4c103ab000fca2d370afc7 + languageName: node + linkType: hard + "caniuse-lite@npm:^1.0.0, caniuse-lite@npm:^1.0.30001688": - version: 1.0.30001692 - resolution: "caniuse-lite@npm:1.0.30001692" - checksum: 484113e3fabbe223fff0380c25c861da265a34c3f75bb5af1f254423b43e713a3c7f0c313167df52fb203f42ea68bd0df8a9e73642becfe1e9fa5734b5fc55a5 + version: 1.0.30001701 + resolution: "caniuse-lite@npm:1.0.30001701" + checksum: 1abcb27ea3296dacdaa669cfe8e094432165f0541bf49ef7a88c2758d0cec5f9a839ea948f7f48c0c63c633d98f6f6c55ccf97ffebea2caaf465389500df5422 languageName: node linkType: hard @@ -5418,8 +5344,8 @@ __metadata: linkType: hard "compression@npm:^1.7.4": - version: 1.7.5 - resolution: "compression@npm:1.7.5" + version: 1.8.0 + resolution: "compression@npm:1.8.0" dependencies: bytes: 3.1.2 compressible: ~2.0.18 @@ -5428,7 +5354,7 @@ __metadata: on-headers: ~1.0.2 safe-buffer: 5.2.1 vary: ~1.1.2 - checksum: d624b5562492518eee82c4f1381ea36f69f1f10b4283bfc2dcafd7d4d7eeed17c3f0e8f2951798594b7064db7ac5a6198df34816bde2d56bb7c75ce1570880e9 + checksum: 12ca3e326b4ccb6b6e51e1d14d96fafd058ddb3be08fe888487d367d42fb4f81f25d4bf77acc517ba724370e7d74469280688baf2da8cad61062bdf62eb9fd45 languageName: node linkType: hard @@ -5775,7 +5701,7 @@ __metadata: languageName: node linkType: hard -"cross-spawn@npm:^7.0.0, cross-spawn@npm:^7.0.1, cross-spawn@npm:^7.0.2, cross-spawn@npm:^7.0.3": +"cross-spawn@npm:^7.0.0, cross-spawn@npm:^7.0.1, cross-spawn@npm:^7.0.2, cross-spawn@npm:^7.0.3, cross-spawn@npm:^7.0.6": version: 7.0.6 resolution: "cross-spawn@npm:7.0.6" dependencies: @@ -5786,7 +5712,7 @@ __metadata: languageName: node linkType: hard -"crypto-browserify@npm:^3.11.0": +"crypto-browserify@npm:^3.12.1": version: 3.12.1 resolution: "crypto-browserify@npm:3.12.1" dependencies: @@ -6619,9 +6545,9 @@ __metadata: linkType: hard "electron-to-chromium@npm:^1.5.73": - version: 1.5.79 - resolution: "electron-to-chromium@npm:1.5.79" - checksum: b51178250d8a3f5e8af74b6268c607d8572d62fe0771ef94054c27b504cdb0ef1e33b757c8cc0d771436176bb102c7bc02586a4b01daa5fe629edc655367e5e4 + version: 1.5.107 + resolution: "electron-to-chromium@npm:1.5.107" + checksum: a76ab714041e180dd2a90cbd2fbbe13ae73d0a54e5ba15a9b308f847192aae797d0ff58aedc8c256e302084d7a6d13820aeb96a0db480c16a44eecf50eb26ccf languageName: node linkType: hard @@ -6655,15 +6581,15 @@ __metadata: linkType: hard "electron@npm:^33.0.1": - version: 33.3.2 - resolution: "electron@npm:33.3.2" + version: 33.4.2 + resolution: "electron@npm:33.4.2" dependencies: "@electron/get": ^2.0.0 "@types/node": ^20.9.0 extract-zip: ^2.0.1 bin: electron: cli.js - checksum: ba3b8cfa0d4dd39e176bf89ec5466045b2f6c59f011de9aae19d3ab17a5d91f2701b5b9244b5877ef49aa41542cc9e72e117c8ea28079485e9be41eb4fcecddf + checksum: 4db8b8dc53c8e537186b1162a25e39555e5e60f28a823a95d2b2eadcbc052ae10de904c6e60e62a500acce85553c4cf639a798902c1f69834068e0b8758e7d94 languageName: node linkType: hard @@ -6764,12 +6690,12 @@ __metadata: linkType: hard "enhanced-resolve@npm:^5.17.1": - version: 5.18.0 - resolution: "enhanced-resolve@npm:5.18.0" + version: 5.18.1 + resolution: "enhanced-resolve@npm:5.18.1" dependencies: graceful-fs: ^4.2.4 tapable: ^2.2.0 - checksum: 77c6b11f0d19f21f52214e5a2c0dfb7070decb4045572f44be4cacf92b4be5e2c1d9a4c044a226d1003ca9daf9b71d498d256e7520ff5060f23d0284f814d392 + checksum: de5bea7debe3576e78173bcc409c4aee7fcb56580c602d5c47c533b92952e55d7da3d9f53b864846ba62c8bd3efb0f9ecfe5f865e57de2f3e9b6e5cda03b4e7e languageName: node linkType: hard @@ -6990,12 +6916,12 @@ __metadata: languageName: node linkType: hard -"es-object-atoms@npm:^1.0.0": - version: 1.0.0 - resolution: "es-object-atoms@npm:1.0.0" +"es-object-atoms@npm:^1.0.0, es-object-atoms@npm:^1.1.1": + version: 1.1.1 + resolution: "es-object-atoms@npm:1.1.1" dependencies: es-errors: ^1.3.0 - checksum: 26f0ff78ab93b63394e8403c353842b2272836968de4eafe97656adfb8a7c84b9099bf0fe96ed58f4a4cddc860f6e34c77f91649a58a5daa4a9c40b902744e3c + checksum: 214d3767287b12f36d3d7267ef342bbbe1e89f899cfd67040309fc65032372a8e60201410a99a1645f2f90c1912c8c49c8668066f6bdd954bcd614dda2e3da97 languageName: node linkType: hard @@ -7012,11 +6938,11 @@ __metadata: linkType: hard "es-shim-unscopables@npm:^1.0.2": - version: 1.0.2 - resolution: "es-shim-unscopables@npm:1.0.2" + version: 1.1.0 + resolution: "es-shim-unscopables@npm:1.1.0" dependencies: - hasown: ^2.0.0 - checksum: 432bd527c62065da09ed1d37a3f8e623c423683285e6188108286f4a1e8e164a5bcbfbc0051557c7d14633cd2a41ce24c7048e6bbb66a985413fd32f1be72626 + hasown: ^2.0.2 + checksum: 33cfb1ebcb2f869f0bf528be1a8660b4fe8b6cec8fc641f330e508db2284b58ee2980fad6d0828882d22858c759c0806076427a3673b6daa60f753e3b558ee15 languageName: node linkType: hard @@ -7622,7 +7548,7 @@ __metadata: languageName: node linkType: hard -"fast-glob@npm:^3.3.2": +"fast-glob@npm:^3.3.2, fast-glob@npm:^3.3.3": version: 3.3.3 resolution: "fast-glob@npm:3.3.3" dependencies: @@ -7657,9 +7583,9 @@ __metadata: linkType: hard "fast-uri@npm:^3.0.1": - version: 3.0.5 - resolution: "fast-uri@npm:3.0.5" - checksum: b56cda8e7355bad9adcc3c2eacd94cb592eaa9536497a4779a9527784f4f95a3755f30525c63583bd85807c493b396ac89926c970f19a60905ed875121ca78fd + version: 3.0.6 + resolution: "fast-uri@npm:3.0.6" + checksum: 7161ba2a7944778d679ba8e5f00d6a2bb479a2142df0982f541d67be6c979b17808f7edbb0ce78161c85035974bde3fa52b5137df31da46c0828cb629ba67c4e languageName: node linkType: hard @@ -7671,11 +7597,11 @@ __metadata: linkType: hard "fastq@npm:^1.6.0": - version: 1.18.0 - resolution: "fastq@npm:1.18.0" + version: 1.19.1 + resolution: "fastq@npm:1.19.1" dependencies: reusify: ^1.0.4 - checksum: fb8d94318c2e5545a1913c1647b35e8b7825caaba888a98ef9887085e57f5a82104aefbb05f26c81d4e220f02b2ea6f2c999132186d8c77e6c681d91870191ba + checksum: 7691d1794fb84ad0ec2a185f10e00f0e1713b894e2c9c4d42f0bc0ba5f8c00e6e655a202074ca0b91b9c3d977aab7c30c41a8dc069fb5368576ac0054870a0e6 languageName: node linkType: hard @@ -7864,9 +7790,9 @@ __metadata: linkType: hard "flatted@npm:^3.2.9": - version: 3.3.2 - resolution: "flatted@npm:3.3.2" - checksum: ac3c159742e01d0e860a861164bcfd35bb567ccbebb8a0dd041e61cf3c64a435b917dd1e7ed1c380c2ebca85735fb16644485ec33665bc6aafc3b316aa1eed44 + version: 3.3.3 + resolution: "flatted@npm:3.3.3" + checksum: 8c96c02fbeadcf4e8ffd0fa24983241e27698b0781295622591fc13585e2f226609d95e422bcf2ef044146ffacb6b68b1f20871454eddf75ab3caa6ee5f4a1fe languageName: node linkType: hard @@ -7891,43 +7817,45 @@ __metadata: linkType: hard "for-each@npm:^0.3.3": - version: 0.3.3 - resolution: "for-each@npm:0.3.3" + version: 0.3.5 + resolution: "for-each@npm:0.3.5" dependencies: - is-callable: ^1.1.3 - checksum: 6c48ff2bc63362319c65e2edca4a8e1e3483a2fabc72fbe7feaf8c73db94fc7861bd53bc02c8a66a0c1dd709da6b04eec42e0abdd6b40ce47305ae92a25e5d28 + is-callable: ^1.2.7 + checksum: 3c986d7e11f4381237cc98baa0a2f87eabe74719eee65ed7bed275163082b940ede19268c61d04c6260e0215983b12f8d885e3c8f9aa8c2113bf07c37051745c languageName: node linkType: hard "foreground-child@npm:^3.1.0": - version: 3.3.0 - resolution: "foreground-child@npm:3.3.0" + version: 3.3.1 + resolution: "foreground-child@npm:3.3.1" dependencies: - cross-spawn: ^7.0.0 + cross-spawn: ^7.0.6 signal-exit: ^4.0.1 - checksum: 1989698488f725b05b26bc9afc8a08f08ec41807cd7b92ad85d96004ddf8243fd3e79486b8348c64a3011ae5cc2c9f0936af989e1f28339805d8bc178a75b451 + checksum: b2c1a6fc0bf0233d645d9fefdfa999abf37db1b33e5dab172b3cbfb0662b88bfbd2c9e7ab853533d199050ec6b65c03fcf078fc212d26e4990220e98c6930eef languageName: node linkType: hard "form-data@npm:^3.0.0": - version: 3.0.2 - resolution: "form-data@npm:3.0.2" + version: 3.0.3 + resolution: "form-data@npm:3.0.3" dependencies: asynckit: ^0.4.0 combined-stream: ^1.0.8 - mime-types: ^2.1.12 - checksum: 25ffdeed693c8fc59b56082d15ad63f11688fabac2d14918fb339170020f66295e520a6659f3a698217f15c7924fbc593117ecd61d8391a146ea06d686793622 + es-set-tostringtag: ^2.1.0 + mime-types: ^2.1.35 + checksum: e79641abb58b3d7230816ed00645c2732cb64aa44172221644619238106556584aafd908bcc0d728fb06ef6a0d88261e72f4e01111bae3da6d2d7a429e4e1fd2 languageName: node linkType: hard "form-data@npm:^4.0.0": - version: 4.0.1 - resolution: "form-data@npm:4.0.1" + version: 4.0.2 + resolution: "form-data@npm:4.0.2" dependencies: asynckit: ^0.4.0 combined-stream: ^1.0.8 + es-set-tostringtag: ^2.1.0 mime-types: ^2.1.12 - checksum: ccee458cd5baf234d6b57f349fe9cc5f9a2ea8fd1af5ecda501a18fd1572a6dd3bf08a49f00568afd995b6a65af34cb8dec083cf9d582c4e621836499498dd84 + checksum: e887298b22c13c7c9c5a8ba3716f295a479a13ca78bfd855ef11cbce1bcf22bc0ae2062e94808e21d46e5c667664a1a1a8a7f57d7040193c1fefbfb11af58aab languageName: node linkType: hard @@ -7980,13 +7908,13 @@ __metadata: linkType: hard "fs-extra@npm:^11.1.1": - version: 11.2.0 - resolution: "fs-extra@npm:11.2.0" + version: 11.3.0 + resolution: "fs-extra@npm:11.3.0" dependencies: graceful-fs: ^4.2.0 jsonfile: ^6.0.1 universalify: ^2.0.0 - checksum: b12e42fa40ba47104202f57b8480dd098aa931c2724565e5e70779ab87605665594e76ee5fb00545f772ab9ace167fe06d2ab009c416dc8c842c5ae6df7aa7e8 + checksum: f983c706e0c22b0c0747a8e9c76aed6f391ba2d76734cf2757cd84da13417b402ed68fe25bace65228856c61d36d3b41da198f1ffbf33d0b34283a2f7a62c6e9 languageName: node linkType: hard @@ -8134,20 +8062,20 @@ __metadata: linkType: hard "get-intrinsic@npm:^1.2.4, get-intrinsic@npm:^1.2.5, get-intrinsic@npm:^1.2.6, get-intrinsic@npm:^1.2.7": - version: 1.2.7 - resolution: "get-intrinsic@npm:1.2.7" + version: 1.3.0 + resolution: "get-intrinsic@npm:1.3.0" dependencies: - call-bind-apply-helpers: ^1.0.1 + call-bind-apply-helpers: ^1.0.2 es-define-property: ^1.0.1 es-errors: ^1.3.0 - es-object-atoms: ^1.0.0 + es-object-atoms: ^1.1.1 function-bind: ^1.1.2 - get-proto: ^1.0.0 + get-proto: ^1.0.1 gopd: ^1.2.0 has-symbols: ^1.1.0 hasown: ^2.0.2 math-intrinsics: ^1.1.0 - checksum: a1597b3b432074f805b6a0ba1182130dd6517c0ea0c4eecc4b8834c803913e1ea62dfc412865be795b3dacb1555a21775b70cf9af7a18b1454ff3414e5442d4a + checksum: 301008e4482bb9a9cb49e132b88fee093bff373b4e6def8ba219b1e96b60158a6084f273ef5cafe832e42cd93462f4accb46a618d35fe59a2b507f2388c5b79d languageName: node linkType: hard @@ -8320,16 +8248,16 @@ __metadata: linkType: hard "globby@npm:^14.0.0": - version: 14.0.2 - resolution: "globby@npm:14.0.2" + version: 14.1.0 + resolution: "globby@npm:14.1.0" dependencies: "@sindresorhus/merge-streams": ^2.1.0 - fast-glob: ^3.3.2 - ignore: ^5.2.4 - path-type: ^5.0.0 + fast-glob: ^3.3.3 + ignore: ^7.0.3 + path-type: ^6.0.0 slash: ^5.1.0 - unicorn-magic: ^0.1.0 - checksum: 2cee79efefca4383a825fc2fcbdb37e5706728f2d39d4b63851927c128fff62e6334ef7d4d467949d411409ad62767dc2d214e0f837a0f6d4b7290b6711d485c + unicorn-magic: ^0.3.0 + checksum: b1f27dccc999c010ee7e0ce7c6581fd2326ac86cf0508474d526d699a029b66b35d6fa4361c8b4ad8e80809582af71d5e2080e671cf03c26e98ca67aba8834bd languageName: node linkType: hard @@ -8697,9 +8625,9 @@ __metadata: linkType: hard "http-parser-js@npm:>=0.5.1": - version: 0.5.8 - resolution: "http-parser-js@npm:0.5.8" - checksum: 6bbdf2429858e8cf13c62375b0bfb6dc3955ca0f32e58237488bc86cd2378f31d31785fd3ac4ce93f1c74e0189cf8823c91f5cb061696214fd368d2452dc871d + version: 0.5.9 + resolution: "http-parser-js@npm:0.5.9" + checksum: 5696ba51e87f423333454cdf316484b64a89d6fc4916821e8a773f9a80c050627dad240f7fde3a4da8b634b465c7d844de2b1602e808b68b392a3dd324886745 languageName: node linkType: hard @@ -8912,13 +8840,20 @@ __metadata: languageName: node linkType: hard -"ignore@npm:^5.2.0, ignore@npm:^5.2.4": +"ignore@npm:^5.2.0": version: 5.3.2 resolution: "ignore@npm:5.3.2" checksum: 2acfd32a573260ea522ea0bfeff880af426d68f6831f973129e2ba7363f422923cf53aab62f8369cbf4667c7b25b6f8a3761b34ecdb284ea18e87a5262a865be languageName: node linkType: hard +"ignore@npm:^7.0.3": + version: 7.0.3 + resolution: "ignore@npm:7.0.3" + checksum: a0826b70b217d560e3703e3d64483283dc85f4c4ebca1f5bffeeecec9a7453dd542f33a02daeefa8d6f3c5f7ef387ec014ce2733014333357dd620002fa1fd4a + languageName: node + linkType: hard + "image-size@npm:~0.5.0": version: 0.5.5 resolution: "image-size@npm:0.5.5" @@ -8950,12 +8885,12 @@ __metadata: linkType: hard "import-fresh@npm:^3.2.1": - version: 3.3.0 - resolution: "import-fresh@npm:3.3.0" + version: 3.3.1 + resolution: "import-fresh@npm:3.3.1" dependencies: parent-module: ^1.0.0 resolve-from: ^4.0.0 - checksum: 2cacfad06e652b1edc50be650f7ec3be08c5e5a6f6d12d035c440a42a8cc028e60a5b99ca08a77ab4d6b1346da7d971915828f33cdab730d3d42f08242d09baa + checksum: a06b19461b4879cc654d46f8a6244eb55eb053437afd4cbb6613cad6be203811849ed3e4ea038783092879487299fda24af932b86bdfff67c9055ba3612b8c87 languageName: node linkType: hard @@ -9117,14 +9052,15 @@ __metadata: linkType: hard "is-async-function@npm:^2.0.0": - version: 2.1.0 - resolution: "is-async-function@npm:2.1.0" + version: 2.1.1 + resolution: "is-async-function@npm:2.1.1" dependencies: + async-function: ^1.0.0 call-bound: ^1.0.3 get-proto: ^1.0.1 has-tostringtag: ^1.0.2 safe-regex-test: ^1.1.0 - checksum: e8dfa81561eb7cd845d626bf49675c735a177013943eb6919185e1f358fe8b16fd11fa477397df8ddddd31ade47092de8243997530931a4ec17cb2b9d15479c9 + checksum: 9bece45133da26636488ca127d7686b85ad3ca18927e2850cff1937a650059e90be1c71a48623f8791646bb7a241b0cabf602a0b9252dcfa5ab273f2399000e6 languageName: node linkType: hard @@ -9147,16 +9083,16 @@ __metadata: linkType: hard "is-boolean-object@npm:^1.0.1, is-boolean-object@npm:^1.2.1": - version: 1.2.1 - resolution: "is-boolean-object@npm:1.2.1" + version: 1.2.2 + resolution: "is-boolean-object@npm:1.2.2" dependencies: - call-bound: ^1.0.2 + call-bound: ^1.0.3 has-tostringtag: ^1.0.2 - checksum: 2672609f0f2536172873810a38ec006a415e43ddc6a240f7638a1659cb20dfa91cc75c8a1bed36247bb046aa8f0eab945f20d1203bc69606418bd129c745f861 + checksum: 0415b181e8f1bfd5d3f8a20f8108e64d372a72131674eea9c2923f39d065b6ad08d654765553bdbffbd92c3746f1007986c34087db1bd89a31f71be8359ccdaa languageName: node linkType: hard -"is-callable@npm:^1.1.3, is-callable@npm:^1.1.5, is-callable@npm:^1.2.7": +"is-callable@npm:^1.1.5, is-callable@npm:^1.2.7": version: 1.2.7 resolution: "is-callable@npm:1.2.7" checksum: 61fd57d03b0d984e2ed3720fb1c7a897827ea174bd44402878e059542ea8c4aeedee0ea0985998aa5cc2736b2fa6e271c08587addb5b3959ac52cf665173d1ac @@ -9463,11 +9399,11 @@ __metadata: linkType: hard "is-weakref@npm:^1.0.2, is-weakref@npm:^1.1.0": - version: 1.1.0 - resolution: "is-weakref@npm:1.1.0" + version: 1.1.1 + resolution: "is-weakref@npm:1.1.1" dependencies: - call-bound: ^1.0.2 - checksum: 2a2f3a1746ee1baecf9ac6483d903cd3f8ef3cca88e2baa42f2e85ea064bd246d218eed5f6d479fc1c76dae2231e71133b6b86160e821d176932be9fae3da4da + call-bound: ^1.0.3 + checksum: 1769b9aed5d435a3a989ffc18fc4ad1947d2acdaf530eb2bd6af844861b545047ea51102f75901f89043bed0267ed61d914ee21e6e8b9aa734ec201cdfc0726f languageName: node linkType: hard @@ -10420,12 +10356,12 @@ __metadata: linkType: hard "launch-editor@npm:^2.6.1": - version: 2.9.1 - resolution: "launch-editor@npm:2.9.1" + version: 2.10.0 + resolution: "launch-editor@npm:2.10.0" dependencies: picocolors: ^1.0.0 shell-quote: ^1.8.1 - checksum: bed887085a9729cc2ad050329d92a99f4c69bacccf96d1ed8c84670608a3a128a828ba8e9a8a41101c5aea5aea6f79984658e2fd11f6ba85e32e6e1ed16dbb1c + checksum: 0cd219f98a8be1cedc73119c1a18ff232eb1386dcc0f4e710b21234e62bf55513342a3e0939cd67c3d920fc7d714457876bc782a5b17e03f59acbbafd23c5f50 languageName: node linkType: hard @@ -10979,7 +10915,7 @@ __metadata: languageName: node linkType: hard -"mime-types@npm:^2.1.12, mime-types@npm:^2.1.27, mime-types@npm:^2.1.31, mime-types@npm:~2.1.17, mime-types@npm:~2.1.24, mime-types@npm:~2.1.34": +"mime-types@npm:^2.1.12, mime-types@npm:^2.1.27, mime-types@npm:^2.1.31, mime-types@npm:^2.1.35, mime-types@npm:~2.1.17, mime-types@npm:~2.1.24, mime-types@npm:~2.1.34": version: 2.1.35 resolution: "mime-types@npm:2.1.35" dependencies: @@ -11269,9 +11205,9 @@ __metadata: linkType: hard "mrmime@npm:^2.0.0": - version: 2.0.0 - resolution: "mrmime@npm:2.0.0" - checksum: f6fe11ec667c3d96f1ce5fd41184ed491d5f0a5f4045e82446a471ccda5f84c7f7610dff61d378b73d964f73a320bd7f89788f9e6b9403e32cc4be28ba99f569 + version: 2.0.1 + resolution: "mrmime@npm:2.0.1" + checksum: 455a555009edb2ed6e587e0fcb5e41fcbf8f1dcca28242a57d054f02204ab198bed93ba9de75db06bd3447e8603bc74e10a22440ba99431fc4a751435fba35bf languageName: node linkType: hard @@ -11308,7 +11244,7 @@ __metadata: languageName: node linkType: hard -"nanoid@npm:^3.3.7, nanoid@npm:^3.3.8": +"nanoid@npm:^3.3.8": version: 3.3.8 resolution: "nanoid@npm:3.3.8" bin: @@ -11317,10 +11253,10 @@ __metadata: languageName: node linkType: hard -"napi-build-utils@npm:^1.0.1": - version: 1.0.2 - resolution: "napi-build-utils@npm:1.0.2" - checksum: 06c14271ee966e108d55ae109f340976a9556c8603e888037145d6522726aebe89dd0c861b4b83947feaf6d39e79e08817559e8693deedc2c94e82c5cbd090c7 +"napi-build-utils@npm:^2.0.0": + version: 2.0.0 + resolution: "napi-build-utils@npm:2.0.0" + checksum: 532121efd2dd2272595580bca48859e404bdd4ed455a72a28432ba44868c38d0e64fac3026a8f82bf8563d2a18b32eb9a1d59e601a9da4e84ba4d45b922297f5 languageName: node linkType: hard @@ -11405,11 +11341,11 @@ __metadata: linkType: hard "node-abi@npm:^3.3.0, node-abi@npm:^3.45.0": - version: 3.71.0 - resolution: "node-abi@npm:3.71.0" + version: 3.74.0 + resolution: "node-abi@npm:3.74.0" dependencies: semver: ^7.3.5 - checksum: d7f34c294c0351b636688a792e41493840cc195f64a76ecdc35eb0c1682d86e633a932b03e924395b0d2f52ca1db5046898839d57bcfb5819226e64e922b0617 + checksum: b33617fe1867a261379c5b4340a7f2018547ffa652b469d9459a0038d97c227d6d57f56b921007e6614552c323fdf67feff2eeb3baa85d6f45957983d61eccc7 languageName: node linkType: hard @@ -11441,11 +11377,11 @@ __metadata: linkType: hard "node-addon-api@npm:^8.0.0": - version: 8.3.0 - resolution: "node-addon-api@npm:8.3.0" + version: 8.3.1 + resolution: "node-addon-api@npm:8.3.1" dependencies: node-gyp: latest - checksum: 87fd087f0887e91c5608ac3c99e3f374403074c9cff2c335b061b0c68a183e4cd561fb9a8b0a583f0145c9b7753180b2be0c232ef01bb97796ccf4486c87958a + checksum: 8d8566cdead5ffb866000fa1d2472833dfdaa2e5abc282c1aa29e0e6a4570e3978c6f00846bd62c96fde4b7e25972636472dc1a4dba2d5b07178c018929b9799 languageName: node linkType: hard @@ -11560,8 +11496,8 @@ __metadata: linkType: hard "node-stdlib-browser@npm:^1.3.0": - version: 1.3.0 - resolution: "node-stdlib-browser@npm:1.3.0" + version: 1.3.1 + resolution: "node-stdlib-browser@npm:1.3.1" dependencies: assert: ^2.0.0 browser-resolve: ^2.0.0 @@ -11570,7 +11506,7 @@ __metadata: console-browserify: ^1.1.0 constants-browserify: ^1.0.0 create-require: ^1.1.1 - crypto-browserify: ^3.11.0 + crypto-browserify: ^3.12.1 domain-browser: 4.22.0 events: ^3.0.0 https-browserify: ^1.0.0 @@ -11590,7 +11526,7 @@ __metadata: url: ^0.11.4 util: ^0.12.4 vm-browserify: ^1.0.1 - checksum: b39afa2e1c58b09d7495c00407c92fff6dcd2ea5a4f7ef1868a887d636e409c6b458e7d2c494685972ebcf1b6bf1a428c0a60b039223605c048f93a88484f3f8 + checksum: 2012dbd84de654c60414c7d88817c1c8214324fa4e77f09395aa2a9d3722f49fafc0abf1643dc5998a96ebcee2409ee0d957ec4c4fd50d3ff5b40e031d1feb90 languageName: node linkType: hard @@ -11667,9 +11603,9 @@ __metadata: linkType: hard "object-inspect@npm:^1.13.3, object-inspect@npm:^1.7.0": - version: 1.13.3 - resolution: "object-inspect@npm:1.13.3" - checksum: 8c962102117241e18ea403b84d2521f78291b774b03a29ee80a9863621d88265ffd11d0d7e435c4c2cea0dc2a2fbf8bbc92255737a05536590f2df2e8756f297 + version: 1.13.4 + resolution: "object-inspect@npm:1.13.4" + checksum: 582810c6a8d2ef988ea0a39e69e115a138dad8f42dd445383b394877e5816eb4268489f316a6f74ee9c4e0a984b3eab1028e3e79d62b1ed67c726661d55c7a8b languageName: node linkType: hard @@ -12186,10 +12122,10 @@ __metadata: languageName: node linkType: hard -"path-type@npm:^5.0.0": - version: 5.0.0 - resolution: "path-type@npm:5.0.0" - checksum: 15ec24050e8932c2c98d085b72cfa0d6b4eeb4cbde151a0a05726d8afae85784fc5544f733d8dfc68536587d5143d29c0bd793623fad03d7e61cc00067291cd5 +"path-type@npm:^6.0.0": + version: 6.0.0 + resolution: "path-type@npm:6.0.0" + checksum: b9f6eaf7795c48d5c9bc4c6bc3ac61315b8d36975a73497ab2e02b764c0836b71fb267ea541863153f633a069a1c2ed3c247cb781633842fc571c655ac57c00e languageName: node linkType: hard @@ -12328,9 +12264,9 @@ __metadata: linkType: hard "possible-typed-array-names@npm:^1.0.0": - version: 1.0.0 - resolution: "possible-typed-array-names@npm:1.0.0" - checksum: b32d403ece71e042385cc7856385cecf1cd8e144fa74d2f1de40d1e16035dba097bc189715925e79b67bdd1472796ff168d3a90d296356c9c94d272d5b95f3ae + version: 1.1.0 + resolution: "possible-typed-array-names@npm:1.1.0" + checksum: cfcd4f05264eee8fd184cd4897a17890561d1d473434b43ab66ad3673d9c9128981ec01e0cb1d65a52cd6b1eebfb2eae1e53e39b2e0eca86afc823ede7a4f41b languageName: node linkType: hard @@ -12671,12 +12607,12 @@ __metadata: linkType: hard "postcss-selector-parser@npm:^7.0.0": - version: 7.0.0 - resolution: "postcss-selector-parser@npm:7.0.0" + version: 7.1.0 + resolution: "postcss-selector-parser@npm:7.1.0" dependencies: cssesc: ^3.0.0 util-deprecate: ^1.0.2 - checksum: f906b7449fcbe9fa6ae739b6fc324ee3c6201aaf5224f26da27de64ccba68d878d734dd182a467881e463f7ede08972d0129b0cc4d6b671d78c6492cddcef154 + checksum: 1300e7871dd60a5132ee5462cc6e94edd4f3df28462b2495ca9ff025bd83768a908e892a18fde62cae63ff63524641baa6d58c64120f04fe6884b916663ce737 languageName: node linkType: hard @@ -12710,38 +12646,27 @@ __metadata: languageName: node linkType: hard -"postcss@npm:^8.2.1": - version: 8.4.49 - resolution: "postcss@npm:8.4.49" - dependencies: - nanoid: ^3.3.7 - picocolors: ^1.1.1 - source-map-js: ^1.2.1 - checksum: eb5d6cbdca24f50399aafa5d2bea489e4caee4c563ea1edd5a2485bc5f84e9ceef3febf170272bc83a99c31d23a316ad179213e853f34c2a7a8ffa534559d63a - languageName: node - linkType: hard - -"postcss@npm:^8.4.33": - version: 8.5.1 - resolution: "postcss@npm:8.5.1" +"postcss@npm:^8.2.1, postcss@npm:^8.4.33": + version: 8.5.3 + resolution: "postcss@npm:8.5.3" dependencies: nanoid: ^3.3.8 picocolors: ^1.1.1 source-map-js: ^1.2.1 - checksum: cfdcfcd019fca78160341080ba8986cf80cd6e9ca327ba61b86c03e95043e9bce56ad2e018851858039fd7264781797360bfba718dd216b17b3cd803a5134f2f + checksum: da574620eb84ff60e65e1d8fc6bd5ad87a19101a23d0aba113c653434161543918229a0f673d89efb3b6d4906287eb04b957310dbcf4cbebacad9d1312711461 languageName: node linkType: hard "prebuild-install@npm:^7.1.1": - version: 7.1.2 - resolution: "prebuild-install@npm:7.1.2" + version: 7.1.3 + resolution: "prebuild-install@npm:7.1.3" dependencies: detect-libc: ^2.0.0 expand-template: ^2.0.3 github-from-package: 0.0.0 minimist: ^1.2.3 mkdirp-classic: ^0.5.3 - napi-build-utils: ^1.0.1 + napi-build-utils: ^2.0.0 node-abi: ^3.3.0 pump: ^3.0.0 rc: ^1.2.7 @@ -12750,7 +12675,7 @@ __metadata: tunnel-agent: ^0.6.0 bin: prebuild-install: bin.js - checksum: 543dadf8c60e004ae9529e6013ca0cbeac8ef38b5f5ba5518cb0b622fe7f8758b34e4b5cb1a791db3cdc9d2281766302df6088bd1a225f206925d6fee17d6c5c + checksum: 300740ca415e9ddbf2bd363f1a6d2673cc11dd0665c5ec431bbb5bf024c2f13c56791fb939ce2b2a2c12f2d2a09c91316169e8063a80eb4482a44b8fe5b265e1 languageName: node linkType: hard @@ -12926,7 +12851,7 @@ __metadata: languageName: node linkType: hard -"qs@npm:^6.12.3": +"qs@npm:^6.12.3, qs@npm:^6.9.1": version: 6.14.0 resolution: "qs@npm:6.14.0" dependencies: @@ -12935,15 +12860,6 @@ __metadata: languageName: node linkType: hard -"qs@npm:^6.9.1": - version: 6.13.1 - resolution: "qs@npm:6.13.1" - dependencies: - side-channel: ^1.0.6 - checksum: 86c5059146955fab76624e95771031541328c171b1d63d48a7ac3b1fdffe262faf8bc5fcadc1684e6f3da3ec87a8dedc8c0009792aceb20c5e94dc34cf468bb9 - languageName: node - linkType: hard - "querystring-es3@npm:^0.2.1": version: 0.2.1 resolution: "querystring-es3@npm:0.2.1" @@ -13719,9 +13635,9 @@ __metadata: linkType: hard "reusify@npm:^1.0.4": - version: 1.0.4 - resolution: "reusify@npm:1.0.4" - checksum: c3076ebcc22a6bc252cb0b9c77561795256c22b757f40c0d8110b1300723f15ec0fc8685e8d4ea6d7666f36c79ccc793b1939c748bf36f18f542744a4e379fcc + version: 1.1.0 + resolution: "reusify@npm:1.1.0" + checksum: 64cb3142ac5e9ad689aca289585cb41d22521f4571f73e9488af39f6b1bd62f0cbb3d65e2ecc768ec6494052523f473f1eb4b55c3e9014b3590c17fc6a03e22a languageName: node linkType: hard @@ -13821,11 +13737,11 @@ __metadata: linkType: hard "rxjs@npm:^7.8.1": - version: 7.8.1 - resolution: "rxjs@npm:7.8.1" + version: 7.8.2 + resolution: "rxjs@npm:7.8.2" dependencies: tslib: ^2.1.0 - checksum: de4b53db1063e618ec2eca0f7965d9137cabe98cf6be9272efe6c86b47c17b987383df8574861bcced18ebd590764125a901d5506082be84a8b8e364bf05f119 + checksum: 2f233d7c832a6c255dabe0759014d7d9b1c9f1cb2f2f0d59690fd11c883c9826ea35a51740c06ab45b6ade0d9087bde9192f165cba20b6730d344b831ef80744 languageName: node linkType: hard @@ -14000,7 +13916,7 @@ __metadata: languageName: node linkType: hard -"semver@npm:7.6.3, semver@npm:^7.2.1, semver@npm:^7.3.2, semver@npm:^7.3.5, semver@npm:^7.3.8, semver@npm:^7.5.3, semver@npm:^7.6.0, semver@npm:^7.6.3": +"semver@npm:7.6.3": version: 7.6.3 resolution: "semver@npm:7.6.3" bin: @@ -14027,12 +13943,12 @@ __metadata: languageName: node linkType: hard -"semver@npm:^7.5.4": - version: 7.7.0 - resolution: "semver@npm:7.7.0" +"semver@npm:^7.2.1, semver@npm:^7.3.2, semver@npm:^7.3.5, semver@npm:^7.3.8, semver@npm:^7.5.3, semver@npm:^7.5.4, semver@npm:^7.6.0, semver@npm:^7.6.3": + version: 7.7.1 + resolution: "semver@npm:7.7.1" bin: semver: bin/semver.js - checksum: a4eefdada9c40df120935b73b0b86080d22f375ed9b950403a4b6a90cc036e552d903ff3c7c3e865823c434ee6c6473908b13d64c84aa307423d3a998e654652 + checksum: 586b825d36874007c9382d9e1ad8f93888d8670040add24a28e06a910aeebd673a2eb9e3bf169c6679d9245e66efb9057e0852e70d9daa6c27372aab1dda7104 languageName: node linkType: hard @@ -14425,12 +14341,12 @@ __metadata: linkType: hard "socks@npm:^2.6.2": - version: 2.8.3 - resolution: "socks@npm:2.8.3" + version: 2.8.4 + resolution: "socks@npm:2.8.4" dependencies: ip-address: ^9.0.5 smart-buffer: ^4.2.0 - checksum: 7a6b7f6eedf7482b9e4597d9a20e09505824208006ea8f2c49b71657427f3c137ca2ae662089baa73e1971c62322d535d9d0cf1c9235cf6f55e315c18203eadd + checksum: cd1edc924475d5dfde534adf66038df7e62c7343e6b8c0113e52dc9bb6a0a10e25b2f136197f379d695f18e8f0f2b7f6e42977bf720ddbee912a851201c396ad languageName: node linkType: hard @@ -14971,14 +14887,14 @@ __metadata: linkType: hard "tar-fs@npm:^2.0.0": - version: 2.1.1 - resolution: "tar-fs@npm:2.1.1" + version: 2.1.2 + resolution: "tar-fs@npm:2.1.2" dependencies: chownr: ^1.1.1 mkdirp-classic: ^0.5.2 pump: ^3.0.0 tar-stream: ^2.1.4 - checksum: f5b9a70059f5b2969e65f037b4e4da2daf0fa762d3d232ffd96e819e3f94665dbbbe62f76f084f1acb4dbdcce16c6e4dac08d12ffc6d24b8d76720f4d9cf032d + checksum: 6b4fcd38a644b5cd3325f687b9f1f48cd19809b63cbc8376fe794f68361849a17120d036833b3a97de6acb1df588844476309b8c2d0bcaf53f19da2d56ac07de languageName: node linkType: hard @@ -15019,7 +14935,7 @@ __metadata: languageName: node linkType: hard -"terser-webpack-plugin@npm:5.3.11, terser-webpack-plugin@npm:^5.3.10": +"terser-webpack-plugin@npm:5.3.11": version: 5.3.11 resolution: "terser-webpack-plugin@npm:5.3.11" dependencies: @@ -15041,9 +14957,31 @@ __metadata: languageName: node linkType: hard +"terser-webpack-plugin@npm:^5.3.10": + version: 5.3.12 + resolution: "terser-webpack-plugin@npm:5.3.12" + dependencies: + "@jridgewell/trace-mapping": ^0.3.25 + jest-worker: ^27.4.5 + schema-utils: ^4.3.0 + serialize-javascript: ^6.0.2 + terser: ^5.31.1 + peerDependencies: + webpack: ^5.1.0 + peerDependenciesMeta: + "@swc/core": + optional: true + esbuild: + optional: true + uglify-js: + optional: true + checksum: 7e33658f5f096547bd4bcdfa2d1d5ab4e87fe4b9aa65b4086f30049b69e7a4edb267635960788f694f37bf99228b9e270b255b4831213342b177df8a49b6f8c1 + languageName: node + linkType: hard + "terser@npm:^5.10.0, terser@npm:^5.31.1": - version: 5.37.0 - resolution: "terser@npm:5.37.0" + version: 5.39.0 + resolution: "terser@npm:5.39.0" dependencies: "@jridgewell/source-map": ^0.3.3 acorn: ^8.8.2 @@ -15051,7 +14989,7 @@ __metadata: source-map-support: ~0.5.20 bin: terser: bin/terser - checksum: 70c06a8ce1288ff4370a7e481beb6fc8b22fc4995371479f49df1552aa9cf8e794ace66e1da6e87057eda1745644311213f5043bda9a06cf55421eff68b3ac06 + checksum: e39c302aed7a70273c8b03032c37c68c8d9d3b432a7b6abe89caf9d087f7dd94d743c01ee5ba1431a095ad347c4a680b60d258f298a097cf512346d6041eb661 languageName: node linkType: hard @@ -15106,15 +15044,15 @@ __metadata: languageName: node linkType: hard -"tidepool-platform-client@npm:0.61.0": - version: 0.61.0 - resolution: "tidepool-platform-client@npm:0.61.0" +"tidepool-platform-client@npm:0.62.0-getinfouser.2": + version: 0.62.0-getinfouser.2 + resolution: "tidepool-platform-client@npm:0.62.0-getinfouser.2" dependencies: async: 0.9.0 lodash: 4.17.21 superagent: 5.2.2 uuid: 3.1.0 - checksum: 96e221a65b7a003523e03d9ba357b0c1aa371b779ffd363c325ba015b893462955188662587b502193aaf269d6784577c8d24b8c764ba4ad759d4b0ba511c8ae + checksum: 6e18df10d5ca3194d5b4490a8c326ba06f87a1420a574a9e83ed48e80399203e12d01691a3089bb8752ee8bd5b39050b6316ddf6a0dd413c867dd09cb51f25af languageName: node linkType: hard @@ -15173,6 +15111,7 @@ __metadata: ble-glucose: 0.7.0 body-parser: 1.20.3 bows: 1.7.2 + caniuse-lite: 1.0.30001651 chai: 4.4.1 chrome-launcher: 0.15.2 classnames: 2.5.1 @@ -15266,7 +15205,7 @@ __metadata: sudo-prompt: 9.2.1 sundial: 1.7.5 terser-webpack-plugin: 5.3.11 - tidepool-platform-client: 0.61.0 + tidepool-platform-client: 0.62.0-getinfouser.2 url-loader: 4.1.1 uuid: 9.0.1 webmtp: 0.3.3 @@ -15388,13 +15327,13 @@ __metadata: linkType: hard "traverse@npm:0.6.x": - version: 0.6.10 - resolution: "traverse@npm:0.6.10" + version: 0.6.11 + resolution: "traverse@npm:0.6.11" dependencies: - gopd: ^1.0.1 - typedarray.prototype.slice: ^1.0.3 - which-typed-array: ^1.1.15 - checksum: ff25d30726db4867c01ff1f1bd8a5e3356b920c4d674ddf6c3764179bb54766cf1ad0158bbd65667e1f5fbde2d4efbd814d7b24d44149cc31255f0cfe2ab2095 + gopd: ^1.2.0 + typedarray.prototype.slice: ^1.0.5 + which-typed-array: ^1.1.18 + checksum: 7cb89a604b0f09096a553497f9f0b80288ce41f869cafe2183c36a942b3b45133382e50d6d0f9f064fb2713b21813ace02a467e4b432e28623d5273236bfb2bf languageName: node linkType: hard @@ -15425,12 +15364,12 @@ __metadata: languageName: node linkType: hard -"ts-api-utils@npm:^2.0.0": - version: 2.0.0 - resolution: "ts-api-utils@npm:2.0.0" +"ts-api-utils@npm:^2.0.1": + version: 2.0.1 + resolution: "ts-api-utils@npm:2.0.1" peerDependencies: typescript: ">=4.8.4" - checksum: f16f3e4e3308e7ad7ccf0bec3e0cb2e06b46c2d6919c40b6439e37912409c72f14340d231343b2b1b8cc17c2b8b01c5f2418690ea788312db6ca4e72cf2df6d8 + checksum: ca31f4dc3c0d69691599de2955b41879c27cb91257f2a468bbb444d3f09982a5f717a941fcebd3aaa092b778710647a0be1c2b1dd75cf6c82ceffc3bf4c7d27d languageName: node linkType: hard @@ -15521,9 +15460,9 @@ __metadata: linkType: hard "type-fest@npm:^4.27.0": - version: 4.33.0 - resolution: "type-fest@npm:4.33.0" - checksum: 42c9a4e305ef86826f6c3e9fb0d230765523eba248b7927580e76fa0384a4a12dfcde3ba04ac94b3cfab664b16608f1f9b8fb6116a48c728b87350e8252fd32c + version: 4.35.0 + resolution: "type-fest@npm:4.35.0" + checksum: 14581e7b02bb43caa4893eec321a993378218c2672715d28c6f85a97756cf59864f4d90f8a794cf1929e861470debc8e682bf5c9575522872b520611b4bb14a0 languageName: node linkType: hard @@ -15590,7 +15529,7 @@ __metadata: languageName: node linkType: hard -"typedarray.prototype.slice@npm:^1.0.3": +"typedarray.prototype.slice@npm:^1.0.5": version: 1.0.5 resolution: "typedarray.prototype.slice@npm:1.0.5" dependencies: @@ -15690,10 +15629,10 @@ __metadata: languageName: node linkType: hard -"unicorn-magic@npm:^0.1.0": - version: 0.1.0 - resolution: "unicorn-magic@npm:0.1.0" - checksum: 48c5882ca3378f380318c0b4eb1d73b7e3c5b728859b060276e0a490051d4180966beeb48962d850fd0c6816543bcdfc28629dcd030bb62a286a2ae2acb5acb6 +"unicorn-magic@npm:^0.3.0": + version: 0.3.0 + resolution: "unicorn-magic@npm:0.3.0" + checksum: bdd7d7c522f9456f32a0b77af23f8854f9a7db846088c3868ec213f9550683ab6a2bdf3803577eacbafddb4e06900974385841ccb75338d17346ccef45f9cb01 languageName: node linkType: hard @@ -15755,8 +15694,8 @@ __metadata: linkType: hard "update-browserslist-db@npm:^1.1.1": - version: 1.1.2 - resolution: "update-browserslist-db@npm:1.1.2" + version: 1.1.3 + resolution: "update-browserslist-db@npm:1.1.3" dependencies: escalade: ^3.2.0 picocolors: ^1.1.1 @@ -15764,7 +15703,7 @@ __metadata: browserslist: ">= 4.21.0" bin: update-browserslist-db: cli.js - checksum: 088d2bad8ddeaeccd82d87d3f6d736d5256d697b725ffaa2b601dfd0ec16ba5fad20db8dcdccf55396e1a36194236feb69e3f5cce772e5be15a5e4261ff2815d + checksum: 7b6d8d08c34af25ee435bccac542bedcb9e57c710f3c42421615631a80aa6dd28b0a81c9d2afbef53799d482fb41453f714b8a7a0a8003e3b4ec8fb1abb819af languageName: node linkType: hard @@ -16309,7 +16248,7 @@ __metadata: languageName: node linkType: hard -"which-typed-array@npm:^1.1.15, which-typed-array@npm:^1.1.16, which-typed-array@npm:^1.1.18, which-typed-array@npm:^1.1.2": +"which-typed-array@npm:^1.1.16, which-typed-array@npm:^1.1.18, which-typed-array@npm:^1.1.2": version: 1.1.18 resolution: "which-typed-array@npm:1.1.18" dependencies: @@ -16421,8 +16360,8 @@ __metadata: linkType: hard "ws@npm:^8.18.0": - version: 8.18.0 - resolution: "ws@npm:8.18.0" + version: 8.18.1 + resolution: "ws@npm:8.18.1" peerDependencies: bufferutil: ^4.0.1 utf-8-validate: ">=5.0.2" @@ -16431,7 +16370,7 @@ __metadata: optional: true utf-8-validate: optional: true - checksum: 91d4d35bc99ff6df483bdf029b9ea4bfd7af1f16fc91231a96777a63d263e1eabf486e13a2353970efc534f9faa43bdbf9ee76525af22f4752cbc5ebda333975 + checksum: 4658357185d891bc45cc2d42a84f9e192d047e8476fb5cba25b604f7d75ca87ca0dd54cd0b2cc49aeee57c79045a741cb7d0b14501953ac60c790cd105c42f23 languageName: node linkType: hard