Skip to content

Instantly share code, notes, and snippets.

@bewest
Last active August 31, 2018 20:37
Show Gist options
  • Save bewest/45252f6e87abd0ee7a8f to your computer and use it in GitHub Desktop.
Save bewest/45252f6e87abd0ee7a8f to your computer and use it in GitHub Desktop.
dexcom share2 webservice to nightscout
# Logs
logs
*.log
# Runtime data
pids
*.pid
*.seed
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# node-waf configuration
.lock-wscript
# Compiled binary addons (http://nodejs.org/api/addons.html)
build/Release
# Dependency directory
# https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git
node_modules

dexcom webservice

Fetches data from Dexcom's webservice and puts it in Nightscout.

Environment variables

These are required.

  • DEXCOM_ACCOUNT_NAME - Dexcom Share user name
  • DEXCOM_PASSWORD - Dexcom Share password
  • API_SECRET - (already set in Azure) your Nightscout API Secret
  • NS - Your fully qualified https://foo.bar.example.com endpoint with no path.

Features

These environment variables are optional.

  • maxCount - default: 1, maximum number of records to process
  • minutes - default: 1440, number of minutes to include

These features can be used to emulate gap sync.

  • SHARE_INTERVAL - default: 60000 * 5, number of ms to wait between updates

Output/logs

The output looks like this when it works:

$ env $(cat shansel.env )  node index.js 
Entries [ { sgv: 70,
    date: 1426298639000,
    dateString: '2015-03-14T02:03:59.000Z',
    trend: 4,
    device: 'share2',
    type: 'sgv' } ]
Nightscout upload error null status 200 []

How it works:

Using the Share2 app allows an iphone to push data from a Dexcom receiver to Dexcom's webservices. This program fetches a user's data from Dexcom's servers, and stores it in their own Nightscout server.

By default, this program run as node index.js, will loop forever. As described by Scott Hanselman, one of my many advisors, this logs in to Dexcom Share as the data publisher. It re-uses the token every 5 minutes to fetch the maxCount latest glucose records within the last specified minutes. This information is then sent to the user's specified Nightscout install, making the data available to the beloved pebble watch and other equipment owned and operated by the receiver's owner. It will continue to re-use the same sessionID until it expires, at which point it should attempt to log in again. If it can log in again, it will continue to re-use the new token to fetch data, storing it into Nightscout.

/**
* Author: Ben West
* https://github.com/bewest
* Advisor: Scott Hanselman
* http://www.hanselman.com/blog/BridgingDexcomShareCGMReceiversAndNightscout.aspx
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* 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
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*
* @description: Allows user to store their Dexcom data in their own
* Nightscout server by facilitating the transfer of latest records
* from Dexcom's server into theirs.
*/
var request = require('request');
var qs = require('querystring');
var crypto = require('crypto');
// Defaults
var Defaults = {
"applicationId":"d89443d2-327c-4a6f-89e5-496bbb0317db"
, "agent": "Dexcom Share/3.0.2.11 CFNetwork/711.2.23 Darwin/14.0.0"
, login: 'https://share1.dexcom.com/ShareWebServices/Services/General/LoginPublisherAccountByName'
, accept: 'application/json'
, 'content-type': 'application/json'
, LatestGlucose: "https://share1.dexcom.com/ShareWebServices/Services/Publisher/ReadPublisherLatestGlucoseValues"
// ?sessionID=e59c836f-5aeb-4b95-afa2-39cf2769fede&minutes=1440&maxCount=1"
, nightscout_upload: '/api/v1/entries.json'
};
// assemble the POST body for the login endpoint
function login_payload (opts) {
var body = {
"password": opts.password
, "applicationId" : opts.applicationId || Defaults.applicationId
, "accountName": opts.accountName
};
return body;
}
// Login to Dexcom's server.
function authorize (opts, then) {
var url = Defaults.login;
var body = login_payload(opts);
var headers = { 'User-Agent': Defaults.agent
, 'Content-Type': Defaults['content-type']
, 'Accept': Defaults.accept };
var req ={ uri: url, body: body, json: true, headers: headers, method: 'POST'
, rejectUnauthorized: false };
// Asynchronously calls the `then` function when the request's I/O
// is done.
return request(req, then);
}
// Assemble query string for fetching data.
function fetch_query (opts) {
// ?sessionID=e59c836f-5aeb-4b95-afa2-39cf2769fede&minutes=1440&maxCount=1"
var q = {
sessionID: opts.sessionID
, minutes: opts.minutes || 1440
, maxCount: opts.maxCount || 1
};
var url = Defaults.LatestGlucose + '?' + qs.stringify(q);
return url;
}
// Asynchronously fetch data from Dexcom's server.
// Will fetch `minutes` and `maxCount` records.
function fetch (opts, then) {
var url = fetch_query(opts);
var body = "";
var headers = { 'User-Agent': Defaults.agent
, 'Content-Type': Defaults['content-type']
, 'Content-Length': 0
, 'Accept': Defaults.accept };
var req ={ uri: url, body: body, json: true, headers: headers, method: 'POST'
, rejectUnauthorized: false };
return request(req, then);
}
// Authenticate and fetch data from Dexcom.
function do_everything (opts, then) {
var login_opts = opts.login;
var fetch_opts = opts.fetch;
authorize(login_opts, function (err, res, body) {
fetch_opts.sessionID = body;
fetch(fetch_opts, function (err, res, glucose) {
then(err, glucose);
});
});
}
// Map Dexcom's property values to Nightscout's.
function dex_to_entry (d) {
/*
[ { DT: '/Date(1426292016000-0700)/',
ST: '/Date(1426295616000)/',
Trend: 4,
Value: 101,
WT: '/Date(1426292039000)/' } ]
*/
var regex = /\((.*)\)/;
var wall = parseInt(d.WT.match(regex)[1]);
var date = new Date(wall);
var entry = {
sgv: d.Value
, date: wall
, dateString: date.toISOString( )
, trend: d.Trend
, device: 'share2'
, type: 'sgv'
// , device: 'dexcom'
};
return entry;
}
// Record data into Nightscout.
function report_to_nightscout (opts, then) {
var shasum = crypto.createHash('sha1');
var hash = shasum.update(opts.API_SECRET);
var headers = { 'api-secret': shasum.digest('hex')
, 'Content-Type': Defaults['content-type']
, 'Accept': Defaults.accept };
var url = opts.endpoint + Defaults.nightscout_upload;
var req = { uri: url, body: opts.entries, json: true, headers: headers, method: 'POST'
, rejectUnauthorized: false };
return request(req, then);
}
function engine (opts) {
var failures = 0;
function my ( ) {
console.log('RUNNING', 'failures', failures);
if (my.sessionID) {
var fetch_opts = new Object(opts.fetch);
fetch_opts.sessionID = my.sessionID;
fetch(fetch_opts, function (err, res, glucose) {
if (res.statusCode < 400) {
to_nightscout(glucose);
} else {
my.sessionID = null;
refresh_token( );
}
});
} else {
failures++;
refresh_token( );
}
}
function refresh_token ( ) {
console.log('Fetching new token');
authorize(opts.login, function (err, res, body) {
if (!err && body) {
my.sessionID = body;
failures = 0;
my( );
} else {
failures++;
console.log("Error refreshing token", err);
}
});
}
function to_nightscout (glucose) {
var ns_config = new Object(opts.nightscout);
if (glucose) {
// Translate to Nightscout data.
var entries = glucose.map(dex_to_entry);
console.log('Entries', entries);
if (ns_config.endpoint) {
ns_config.entries = entries;
// Send data to Nightscout.
report_to_nightscout(ns_config, function (err, response, body) {
console.log("Nightscout upload", 'error', err, 'status', response.statusCode, body);
});
}
}
}
my( );
return my;
}
// If run from commandline, run the whole program.
if (!module.parent) {
var args = process.argv.slice(2);
var config = {
accountName: process.env['DEXCOM_ACCOUNT_NAME']
, password: process.env['DEXCOM_PASSWORD']
};
var ns_config = {
API_SECRET: process.env['API_SECRET']
, endpoint: process.env['NS']
};
var interval = process.env['SHARE_INTERVAL'] || 60000 * 5;
var fetch_config = { maxCount: process.env.maxCount || 1, minutes: process.env.minutes || 1440 };
var meta = {
login: config
, fetch: fetch_config
, nightscout: ns_config
};
switch (args[0]) {
case 'login':
authorize(config, console.log.bind(console, 'login'));
break;
case 'fetch':
config = { sessionID: args[1] };
fetch(config, console.log.bind(console, 'fetched'));
break;
case 'testdaemon':
setInterval(engine(meta), 2500);
break;
case 'run':
// Authorize and fetch from Dexcom.
do_everything(meta, function (err, glucose) {
console.log('From Dexcom', err, glucose);
if (glucose) {
// Translate to Nightscout data.
var entries = glucose.map(dex_to_entry);
console.log('Entries', entries);
if (ns_config.endpoint) {
ns_config.entries = entries;
// Send data to Nightscout.
report_to_nightscout(ns_config, function (err, response, body) {
console.log("Nightscout upload", 'error', err, 'status', response.statusCode, body);
});
}
}
});
break;
default:
setInterval(engine(meta), interval);
break;
break;
}
}
{
"name": "dexcom-share-web",
"version": "0.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "",
"license": "BSD-2-Clause",
"dependencies": {
"request": "~2.53.0"
}
}
@bewest
Copy link
Author

bewest commented Mar 14, 2015

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment