@faymaz/jsdexcom
Version:
Node.js library for accessing Dexcom Share API with international support
293 lines (261 loc) • 9.32 kB
JavaScript
/**
* JSdexcom - A Node.js library for Dexcom Share API
* Provides access to real-time CGM data with international support
* @class JSDexcom
* @author faymaz
* @version 2.1.0
* Available regions for Dexcom servers
* @static
* @readonly
* Create a new JSdexcom instance
* @param {string} username - Dexcom Share username
* @param {string} password - Dexcom Share password
* @param {string} [region='ous'] - Region code (us, ous, jp)
* static BaseUrls = {
'us': 'https://share2.dexcom.com',
'ous': 'https://shareous1.dexcom.com',
'jp': 'https://shareous1.dexcom.com'
};
*/
class JSDexcom {
constructor(username, password, region = 'ous') {
this.username = username;
this.password = password;
this.region = region.toLowerCase();
this.baseUrl = region.toLowerCase() === 'us'
? 'https://share2.dexcom.com'
: 'https://shareous1.dexcom.com';
this.applicationId = 'd89443d2-327c-4a6f-89e5-496bbb0317db';
this.sessionId = null;
this.accountId = null; // Added accountId tracking
}
/**
* Authenticate with Dexcom Share server
* @returns {Promise<string>} Session ID for subsequent requests
* @throws {Error} If authentication fails
* @private
*/
async authenticate() {
// Step 1: Get account ID
if (!this.accountId) {
console.log('Getting account ID...');
const authUrl = `${this.baseUrl}/ShareWebServices/Services/General/AuthenticatePublisherAccount`;
const response = await fetch(authUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
'User-Agent': 'Dexcom Share/3.0.2.11'
},
body: JSON.stringify({
accountName: this.username,
password: this.password,
applicationId: this.applicationId
})
});
if (!response.ok) {
throw new Error(`Account authentication failed: ${response.status}`);
}
this.accountId = (await response.text()).replace(/"/g, '');
if (this.accountId === '00000000-0000-0000-0000-000000000000') {
throw new Error('Invalid credentials');
}
}
// Step 2: Login with account ID
console.log('Getting session ID...');
const loginUrl = `${this.baseUrl}/ShareWebServices/Services/General/LoginPublisherAccountById`;
const loginResponse = await fetch(loginUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
'User-Agent': 'Dexcom Share/3.0.2.11'
},
body: JSON.stringify({
accountId: this.accountId,
password: this.password,
applicationId: this.applicationId
})
});
if (!loginResponse.ok) {
throw new Error(`Session login failed: ${loginResponse.status}`);
}
this.sessionId = (await loginResponse.text()).replace(/"/g, '');
if (this.sessionId === '00000000-0000-0000-0000-000000000000') {
throw new Error('Login failed');
}
return this.sessionId;
}
/**
* Get latest glucose reading with delta calculation
* @returns {Promise<Object>} Object containing current and previous readings with trend analysis
* @throws {Error} If fetching readings fails
*/
async getLatestGlucoseWithDelta() {
if (!this.sessionId) {
await this.authenticate();
}
const url = `${this.baseUrl}/ShareWebServices/Services/Publisher/ReadPublisherLatestGlucoseValues`;
const params = new URLSearchParams({
sessionId: this.sessionId,
minutes: '10',
maxCount: '2'
});
try {
console.log('Fetching glucose readings...');
const response = await fetch(`${url}?${params}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
'User-Agent': 'Dexcom Share/3.0.2.11'
}
});
// If session expired
if (response.status === 500) {
const error = await response.json();
if (error.Code === 'SessionIdNotFound') {
this.sessionId = null; // Clear session ID only
await this.authenticate(); // Get new session
return this.getLatestGlucoseWithDelta();
}
throw new Error(`Server error: ${error.Message}`);
}
if (!response.ok) {
throw new Error(`Failed to get readings: ${response.status}`);
}
const readings = await response.json();
if (!Array.isArray(readings) || readings.length === 0) {
throw new Error('No readings available');
}
const current = this.formatReading(readings[0]);
const previous = readings.length > 1 ? this.formatReading(readings[1]) : null;
const delta = previous ? current._value - previous._value : null;
const deltaTime = previous ?
(current._datetime.getTime() - previous._datetime.getTime()) / (1000 * 60) :
null;
return {
current: {
...current,
_delta: delta,
_delta_time: deltaTime,
_previous_value: previous?._value,
_rate_of_change: deltaTime ? (delta / deltaTime) : null,
_trend_description: this.getTrendDescription(delta)
},
previous: previous
};
} catch (error) {
// If it's an authentication error, try one more time with new credentials
if (error.message.includes('SessionIdNotFound')) {
this.sessionId = null;
this.accountId = null; // Reset both tokens
await this.authenticate();
return this.getLatestGlucoseWithDelta();
}
throw error;
}
}
/**
* Get a single glucose reading
* @returns {Promise<Object>} Latest glucose reading
* @throws {Error} If fetching reading fails
*/
async getLatestGlucose() {
if (!this.sessionId) {
await this.authenticate();
}
const url = `${this.baseUrl}/ShareWebServices/Services/Publisher/ReadPublisherLatestGlucoseValues`;
const params = new URLSearchParams({
sessionId: this.sessionId,
minutes: '10',
maxCount: '1'
});
const response = await fetch(`${url}?${params}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
'User-Agent': 'Dexcom Share/3.0.2.11'
}
});
if (response.status === 500) {
const error = await response.json();
if (error.Code === 'SessionIdNotFound') {
this.sessionId = null;
return this.getLatestGlucose();
}
throw new Error(`Server error: ${error.Message}`);
}
if (!response.ok) {
throw new Error(`Request failed with status ${response.status}`);
}
const readings = await response.json();
if (!Array.isArray(readings) || readings.length === 0) {
throw new Error('No readings available');
}
return this.formatReading(readings[0]);
}
/**
* Format raw glucose reading data
* @param {Object} reading - Raw reading from Dexcom
* @returns {Object} Formatted reading with additional information
* @private
*/
formatReading(reading) {
const TREND_ARROWS = {
'None': '→',
'DoubleUp': '↑↑',
'SingleUp': '↑',
'FortyFiveUp': '↗',
'Flat': '→',
'FortyFiveDown': '↘',
'SingleDown': '↓',
'DoubleDown': '↓↓',
'NotComputable': '?',
'RateOutOfRange': '⚠️'
};
return {
_json: {
WT: reading.WT,
ST: reading.ST,
DT: reading.DT,
Value: reading.Value,
Trend: reading.Trend
},
_value: reading.Value,
_trend_direction: reading.Trend,
_trend_arrow: TREND_ARROWS[reading.Trend] || '?',
_datetime: new Date(parseInt(reading.WT.match(/\d+/)[0])),
_status: this.getGlucoseStatus(reading.Value)
};
}
/**
* Get glucose status based on value
* @param {number} value - Glucose value in mg/dL
* @returns {string} Status description (LOW, HIGH, or IN RANGE)
* @private
*/
getGlucoseStatus(value) {
if (value < 70) return 'LOW';
if (value > 180) return 'HIGH';
return 'IN RANGE';
}
/**
* Get human readable trend description
* @param {number} delta - Change in glucose value
* @returns {string} Trend description
* @private
*/
getTrendDescription(delta) {
if (delta === null) return 'Unknown';
if (delta > 15) return 'Rising quickly';
if (delta > 7) return 'Rising';
if (delta > 3) return 'Rising slowly';
if (delta >= -3) return 'Stable';
if (delta >= -7) return 'Dropping slowly';
if (delta >= -15) return 'Dropping';
return 'Dropping quickly';
}
}
export default JSDexcom;