Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
500 changes: 252 additions & 248 deletions package-lock.json

Large diffs are not rendered by default.

18 changes: 9 additions & 9 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "nightscout-librelink-up",
"version": "3.0.0",
"version": "3.1.0",
"description": "Script written in TypeScript that uploads CGM readings from LibreLink Up to Nightscout",
"main": "dist/index.js",
"scripts": {
Expand Down Expand Up @@ -35,24 +35,24 @@
},
"homepage": "https://github.com/timoschlueter/nightscout-librelink-up#readme",
"dependencies": {
"axios": "~1.12.2",
"axios": "~1.13.2",
"axios-mock-adapter": "~2.1.0",
"http-cookie-agent": "~7.0.2",
"node-cron": "~4.2.1",
"tough-cookie": "~5.0.0",
"tough-cookie": "~5.1.2",
"winston": "~3.18.3"
},
"devDependencies": {
"@tsconfig/node20": "~20.1.6",
"@tsconfig/node20": "~20.1.7",
"@types/jest": "~30.0.0",
"@types/node": "~24.6.2",
"@types/node": "~24.10.1",
"@types/node-cron": "~3.0.11",
"@types/tough-cookie": "~4.0.5",
"@typescript-eslint/eslint-plugin": "~8.45.0",
"@typescript-eslint/parser": "~8.45.0",
"eslint": "~9.36.0",
"@typescript-eslint/eslint-plugin": "~8.46.4",
"@typescript-eslint/parser": "~8.46.4",
"eslint": "~9.39.1",
"jest": "~30.2.0",
"ts-jest": "~29.4.4",
"ts-jest": "~29.4.5",
"ts-node": "~10.9.2",
"typescript": "~5.9.3"
}
Expand Down
9 changes: 5 additions & 4 deletions src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,15 +56,16 @@ function readConfig()
linkUpUsername: process.env.LINK_UP_USERNAME as string,
linkUpPassword: process.env.LINK_UP_PASSWORD as string,
linkUpVersion: process.env.LINK_UP_VERSION || "4.16.0",

logLevel: process.env.LOG_LEVEL || 'info',
singleShot: process.env.SINGLE_SHOT === 'true',
allData: process.env.ALL_DATA === 'true',

nightscoutApiV3: process.env.NIGHTSCOUT_API_V3 === 'true',
nightscoutDisableHttps: process.env.NIGHTSCOUT_DISABLE_HTTPS === 'true',
nightscoutDevice: process.env.DEVICE_NAME || 'nightscout-librelink-up',

nightscoutDevice:
process.env.NIGHTSCOUT_API_V3 === "true"
? process.env.NIGHTSCOUT_DEVICE_NAME || "freestyle-libre"
: process.env.NIGHTSCOUT_DEVICE_NAME || "nightscout-librelink-up",
nightscoutApp: process.env.APP || 'librelinkup-to-nightscout-script',
linkUpRegion: process.env.LINK_UP_REGION || 'EU',
linkUpTimeInterval: Number(process.env.LINK_UP_TIME_INTERVAL) || 5,
linkUpConnection: process.env.LINK_UP_CONNECTION as string,
Expand Down
2 changes: 1 addition & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ const logFormat = printf(({level, message}) =>
return `[${level}]: ${message}`;
});

const logger = createLogger({
export const logger = createLogger({
format: combine(
timestamp(),
logFormat
Expand Down
71 changes: 35 additions & 36 deletions src/nightscout/apiv1.ts
Original file line number Diff line number Diff line change
@@ -1,64 +1,63 @@
import {Entry, NightscoutAPI, NightscoutConfig} from "./interface";
import axios, {RawAxiosRequestHeaders} from "axios";
import axios, { RawAxiosRequestHeaders } from "axios";
import { Entry, NightscoutAPI, NightscoutConfig } from "./interface";
import { logger } from "..";

interface NightscoutHttpHeaders extends RawAxiosRequestHeaders
{
"api-secret": string | undefined;
interface NightscoutApiV1HttpHeaders extends RawAxiosRequestHeaders {
"api-secret": string | undefined;
}

export class Client implements NightscoutAPI
{
readonly baseUrl: string;
readonly headers: NightscoutHttpHeaders;
readonly device: string;
export class Client implements NightscoutAPI {
readonly baseUrl: string;
readonly headers: NightscoutApiV1HttpHeaders;
readonly device: string;

constructor(config: NightscoutConfig)
{
this.baseUrl = config.nightscoutBaseUrl;
this.headers = {
"api-secret": config.nightscoutApiToken,
"User-Agent": "FreeStyle LibreLink Up NightScout Uploader",
"Content-Type": "application/json",
};
this.device = config.nightscoutDevice;
}
this.baseUrl = config.nightscoutBaseUrl;
this.headers = {
"api-secret": config.nightscoutApiToken,
"User-Agent": "FreeStyle LibreLink Up NightScout Uploader",
"Content-Type": "application/json",
};
this.device = config.nightscoutDevice;
}

async lastEntry(): Promise<Entry | null>
{
const url = new URL("/api/v1/entries?count=1", this.baseUrl).toString();
const url = new URL("/api/v1/entries?count=1", this.baseUrl).toString();
const resp = await axios.get(url, {headers: this.headers});

if (resp.status !== 200)
{
throw Error(`failed to get last entry: ${resp.statusText}`);
}
throw Error(`failed to get last entry: ${resp.statusText}`);
}

if (!resp.data || resp.data.length === 0)
{
return null;
}
return resp.data.pop();
return null;
}
return resp.data.pop();
}

async uploadEntries(entries: Entry[]): Promise<void>
{
const url = new URL("/api/v1/entries", this.baseUrl).toString();
const entriesV1 = entries.map((e) => ({
type: "sgv",
sgv: e.sgv,
direction: e.direction?.toString(),
device: this.device,
date: e.date.getTime(),
dateString: e.date.toISOString(),
}));
const url = new URL("/api/v1/entries", this.baseUrl).toString();
const entriesV1 = entries.map((e) => ({
type: "sgv",
sgv: e.sgv,
direction: e.direction?.toString(),
device: this.device,
date: e.date.getTime(),
dateString: e.date.toISOString(),
}));

const resp = await axios.post(url, entriesV1, {headers: this.headers});

if (resp.status !== 200)
{
throw Error(`failed to post new entries: ${resp.statusText} ${resp.status}`);
}

return;
}

return;
}
}
124 changes: 114 additions & 10 deletions src/nightscout/apiv3.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,119 @@
import { Entry, NightscoutAPI, NightscoutConfig } from './interface';
import axios, { RawAxiosRequestHeaders } from "axios";
import { logger } from "..";
import { Entry, NightscoutAPI, NightscoutConfig } from "./interface";

interface NightscoutApiV3HttpHeaders extends RawAxiosRequestHeaders {
Authorization: string;
}

export class Client implements NightscoutAPI {
constructor(config: NightscoutConfig) {
throw new Error('Not implemented');
}
readonly baseUrl: string;
readonly headers: NightscoutApiV3HttpHeaders;
readonly device: string;
readonly accessToken: string;
readonly app: string;

constructor(config: NightscoutConfig) {
this.baseUrl = config.nightscoutBaseUrl;
this.headers = {
"User-Agent": "FreeStyle LibreLink Up NightScout Uploader",
"Content-Type": "application/json",
Authorization: "",
};
this.device = config.nightscoutDevice;
this.accessToken = config.nightscoutApiToken;
this.app = config.nightscoutApp;
}

private addBearerJwtToken(jwtToken: string): string {
if (!jwtToken) {
throw Error("No jwtToken found");
}
return `Bearer ${jwtToken}`;
}

async getJwtToken(): Promise<string> {
let newToken = "";
try {
const url = new URL(
`/api/v2/authorization/request/${this.accessToken}`,
this.baseUrl
).toString();
const resp = await axios.get(url, {
headers: {
...this.headers,
Accept: "application/json",
},
});
if (resp.status !== 200 || !resp.data.token) {
throw Error(`Error getting JWT token: ${resp.statusText} `);
}
newToken = await resp.data.token;
} catch (error) {
logger.error("Error getting JWT token:", error);
}
return newToken;
}

async lastEntry(): Promise<Entry | null> {
const jwtToken = await this.getJwtToken();
const url = new URL(
"/api/v3/entries?limit=1&sort$desc=date",
this.baseUrl
).toString();
const resp = await axios.get(url, {
headers: {
...this.headers,
Authorization: this.addBearerJwtToken(jwtToken),
} as NightscoutApiV3HttpHeaders,
});
if (resp.status !== 200) {
throw Error(`Failed to get last entry: ${resp.statusText}`);
}
if (!resp.data.result || resp.data.result.length === 0) {
throw Error(
`Last entry not found in response data: ${JSON.stringify(resp.data)}`
);
}
return resp.data.result.pop();
}

async uploadEntries(entries: Entry[]): Promise<void> {
const jwtToken = await this.getJwtToken();
const url = new URL("/api/v3/entries", this.baseUrl).toString();

if (!entries.length) {
throw Error(`No entries to upload`);
}

const entryPayloads = entries.map((entry) => ({
type: "sgv",
sgv: entry.sgv,
direction: entry.direction?.toString(),
device: this.device,
date: entry.date.getTime(),
app: this.app,
}));

async lastEntry(): Promise<Entry | null> {
throw new Error('Not implemented');
}
// APIv3 accepts only single entries
const responses = await Promise.all(
entryPayloads.map((entryV3) =>
axios.post(url, entryV3, {
headers: {
...this.headers,
Authorization: this.addBearerJwtToken(jwtToken),
} as NightscoutApiV3HttpHeaders,
})
)
);

async uploadEntries(entries: Entry[]): Promise<void> {
throw new Error('Not implemented');
}
responses.forEach((resp) => {
if (resp.status !== 201) {
throw Error(
`failed to post new entries: ${resp.statusText} ${resp.status}`
);
}
});
return;
}
}
1 change: 1 addition & 0 deletions src/nightscout/interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ export interface NightscoutConfig {
nightscoutApiToken: string;
nightscoutBaseUrl: string;
nightscoutDevice: string;
nightscoutApp: string;
}

export interface Entry {
Expand Down
4 changes: 3 additions & 1 deletion tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@
"compilerOptions": {
"outDir": "dist",
"strict": true,
"resolveJsonModule": true
"resolveJsonModule": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true
},
"include": [
"src"
Expand Down