Skip to content

Commit

Permalink
Merge pull request #122 from 3timeslazy/nsv3-refactor
Browse files Browse the repository at this point in the history
Moved nightscout code into separated folder
  • Loading branch information
timoschlueter authored Mar 26, 2024
2 parents b998d2e + 93d0abe commit b8be1d5
Show file tree
Hide file tree
Showing 11 changed files with 491 additions and 312 deletions.
505 changes: 329 additions & 176 deletions package-lock.json

Large diffs are not rendered by default.

22 changes: 22 additions & 0 deletions src/config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
function readConfig() {
const requiredEnvs = ['NIGHTSCOUT_API_TOKEN', 'NIGHTSCOUT_URL'];
for (let envName of requiredEnvs) {
if (!process.env[envName]) {
throw Error(`Required environment variable ${envName} is not set`);
}
}

const protocol =
process.env.NIGHTSCOUT_DISABLE_HTTPS === 'true' ? 'http://' : 'https://';
const url = new URL(protocol + process.env.NIGHTSCOUT_URL);

return {
nightscoutApiToken: process.env.NIGHTSCOUT_API_TOKEN as string,
nightscoutBaseUrl: url.toString(),

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

export default readConfig;
15 changes: 0 additions & 15 deletions src/constants/nightscout-trend-arrows.ts

This file was deleted.

34 changes: 16 additions & 18 deletions src/helpers/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,25 +3,23 @@
*
* SPDX-License-Identifier: MIT
*/
import {NIGHTSCOUT_TREND_ARROWS} from "../constants/nightscout-trend-arrows";
import { Direction } from "../nightscout/interface";

export function mapTrendArrow(libreTrendArrowRaw: number): string
{
switch (libreTrendArrowRaw)
{
case 1:
return NIGHTSCOUT_TREND_ARROWS.singleDown
case 2:
return NIGHTSCOUT_TREND_ARROWS.fortyFiveDown
case 3:
return NIGHTSCOUT_TREND_ARROWS.flat
case 4:
return NIGHTSCOUT_TREND_ARROWS.fortyFiveUp
case 5:
return NIGHTSCOUT_TREND_ARROWS.singleUp
default:
return NIGHTSCOUT_TREND_ARROWS.notComputable
}
export function mapTrendArrow(libreTrendArrowRaw: number): Direction {
switch (libreTrendArrowRaw) {
case 1:
return Direction.SingleDown;
case 2:
return Direction.FortyFiveDown;
case 3:
return Direction.Flat;
case 4:
return Direction.FortyFiveUp;
case 5:
return Direction.SingleUp;
default:
return Direction.NotComputable;
}
}

export function getUtcDateFromString(timeStamp: string): Date
Expand Down
101 changes: 24 additions & 77 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,13 @@ import {ConnectionsResponse} from "./interfaces/librelink/connections-response";
import {GraphData, GraphResponse} from "./interfaces/librelink/graph-response";
import {AuthTicket, Connection, GlucoseItem} from "./interfaces/librelink/common";
import {getUtcDateFromString, mapTrendArrow} from "./helpers/helpers";
import {LibreLinkUpHttpHeaders, NightScoutHttpHeaders} from "./interfaces/http-headers";
import {Entry} from "./interfaces/nightscout/entry";
import {LibreLinkUpHttpHeaders} from "./interfaces/http-headers";
import {Client as ClientV1} from "./nightscout/apiv1";
import {Client as ClientV3} from "./nightscout/apiv3";
import {Entry} from "./nightscout/interface";
import readConfig from "./config";

const config = readConfig();

const {combine, timestamp, printf} = format;

Expand Down Expand Up @@ -74,23 +79,6 @@ function getLibreLinkUpUrl(region: string): string
return LLU_API_ENDPOINTS.EU;
}

/**
* NightScout API
*/
const NIGHTSCOUT_URL = process.env.NIGHTSCOUT_URL;
const NIGHTSCOUT_API_TOKEN = process.env.NIGHTSCOUT_API_TOKEN;
const NIGHTSCOUT_DISABLE_HTTPS = process.env.NIGHTSCOUT_DISABLE_HTTPS || false;
const NIGHTSCOUT_DEVICE_NAME = process.env.DEVICE_NAME || "nightscout-librelink-up";

function getNightscoutUrl(): string
{
if (NIGHTSCOUT_DISABLE_HTTPS === "true")
{
return "http://" + NIGHTSCOUT_URL;
}
return "https://" + NIGHTSCOUT_URL;
}

/**
* last known authTicket
*/
Expand All @@ -108,12 +96,6 @@ const libreLinkUpHttpHeaders: LibreLinkUpHttpHeaders = {
"Authorization": undefined
}

const nightScoutHttpHeaders: NightScoutHttpHeaders = {
"api-secret": NIGHTSCOUT_API_TOKEN,
"User-Agent": USER_AGENT,
"Content-Type": "application/json",
}

if (process.env.SINGLE_SHOT === "true")
{
main().then();
Expand Down Expand Up @@ -274,53 +256,31 @@ export async function getLibreLinkUpConnection(): Promise<string | null>
}
}

async function lastEntryDate(): Promise<Date | null>
{
const url = getNightscoutUrl() + "/api/v1/entries?count=1"
const response = await axios.get(
url,
{
headers: nightScoutHttpHeaders
});

if (!response.data || response.data.length === 0)
{
return null;
}
return new Date(response.data.pop().dateString);
}
const nightscoutClient = config.nightscoutApiV3
? new ClientV3(config)
: new ClientV1(config);

export async function createFormattedMeasurements(measurementData: GraphData): Promise<Entry[]>
{
export async function createFormattedMeasurements(measurementData: GraphData): Promise<Entry[]> {
const formattedMeasurements: Entry[] = [];
const glucoseMeasurement = measurementData.connection.glucoseMeasurement;
const measurementDate = getUtcDateFromString(glucoseMeasurement.FactoryTimestamp);
const lastEntry = await lastEntryDate();
const lastEntry = await nightscoutClient.lastEntry();

// Add the most recent measurement first
if (lastEntry === null || measurementDate > lastEntry)
{
if (lastEntry === null || measurementDate > lastEntry.date) {
formattedMeasurements.push({
"type": "sgv",
"device": NIGHTSCOUT_DEVICE_NAME,
"dateString": measurementDate.toISOString(),
"date": measurementDate.getTime(),
"direction": mapTrendArrow(glucoseMeasurement.TrendArrow),
"sgv": glucoseMeasurement.ValueInMgPerDl
date: measurementDate,
direction: mapTrendArrow(glucoseMeasurement.TrendArrow),
sgv: glucoseMeasurement.ValueInMgPerDl
});
}

measurementData.graphData.forEach((glucoseMeasurementHistoryEntry: GlucoseItem) =>
{
measurementData.graphData.forEach((glucoseMeasurementHistoryEntry: GlucoseItem) => {
const entryDate = getUtcDateFromString(glucoseMeasurementHistoryEntry.FactoryTimestamp);
if (lastEntry === null ||entryDate > lastEntry)
{
if (lastEntry === null || entryDate > lastEntry.date) {
formattedMeasurements.push({
"type": "sgv",
"device": NIGHTSCOUT_DEVICE_NAME,
"dateString": entryDate.toISOString(),
"date": entryDate.getTime(),
"sgv": glucoseMeasurementHistoryEntry.ValueInMgPerDl
date: entryDate,
sgv: glucoseMeasurementHistoryEntry.ValueInMgPerDl,
});
}
});
Expand All @@ -334,24 +294,11 @@ async function uploadToNightScout(measurementData: GraphData): Promise<void>
if (formattedMeasurements.length > 0)
{
logger.info("Trying to upload " + formattedMeasurements.length + " glucose measurement items to Nightscout");
try
try
{
const url = getNightscoutUrl() + "/api/v1/entries"
const response = await axios.post(
url,
formattedMeasurements,
{
headers: nightScoutHttpHeaders
});
if (response.status !== 200)
{
logger.error("Upload to NightScout failed ", response.statusText);
}
else
{
logger.info("Upload of " + formattedMeasurements.length + " measurements to Nightscout succeeded");
}
} catch (error)
await nightscoutClient.uploadEntries(formattedMeasurements);
logger.info("Upload of " + formattedMeasurements.length + " measurements to Nightscout succeeded");
} catch (error)
{
logger.error("Upload to NightScout failed ", error);
}
Expand Down
3 changes: 0 additions & 3 deletions src/interfaces/http-headers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,3 @@ export interface LibreLinkUpHttpHeaders extends OutgoingHttpHeaders {
"product": string,
}

export interface NightScoutHttpHeaders extends OutgoingHttpHeaders {
"api-secret": string | undefined,
}
14 changes: 0 additions & 14 deletions src/interfaces/nightscout/entry.ts

This file was deleted.

53 changes: 53 additions & 0 deletions src/nightscout/apiv1.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { Entry, NightscoutAPI, NightscoutConfig } from './interface';
import { OutgoingHttpHeaders } from 'http';
import axios from 'axios';

interface NightscoutHttpHeaders extends OutgoingHttpHeaders {
'api-secret': string | undefined;
}

export class Client implements NightscoutAPI {
readonly baseUrl: string;
readonly headers: NightscoutHttpHeaders;
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;
}

async lastEntry(): Promise<Entry | null> {
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}`);
}
if (!resp.data || resp.data.length === 0) {
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 resp = await axios.post(url, entriesV1, { headers: this.headers });
if (resp.status !== 200) {
throw Error(`failed to post new entries: ${resp.statusText}`);
}

return;
}
}
15 changes: 15 additions & 0 deletions src/nightscout/apiv3.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { Entry, NightscoutAPI, NightscoutConfig } from './interface';

export class Client implements NightscoutAPI {
constructor(config: NightscoutConfig) {
throw new Error('Not implemented');
}

async lastEntry(): Promise<Entry | null> {
throw new Error('Not implemented');
}

async uploadEntries(entries: Entry[]): Promise<void> {
throw new Error('Not implemented');
}
}
31 changes: 31 additions & 0 deletions src/nightscout/interface.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
/**
* Interfaces related to the Nightscout API
*
* SPDX-License-Identifier: MIT
*/

export interface NightscoutAPI {
lastEntry(): Promise<Entry | null>;
uploadEntries(entries: Entry[]): Promise<void>;
}

export interface NightscoutConfig {
nightscoutApiToken: string;
nightscoutBaseUrl: string;
nightscoutDevice: string;
}

export interface Entry {
date: Date;
sgv: number;
direction?: Direction;
}

export enum Direction {
SingleDown = 'SingleDown',
FortyFiveDown = 'FortyFiveDown',
Flat = 'Flat',
FortyFiveUp = 'FortyFiveUp',
SingleUp = 'SingleUp',
NotComputable = 'NOT COMPUTABLE',
}
10 changes: 1 addition & 9 deletions tests/unit-tests/librelink/librelink.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import { default as entriesResponse } from "../../data/entries.json";
import { default as graphResponse } from "../../data/graph.json";
import {AuthTicket} from "../../../src/interfaces/librelink/common";
import {GraphData} from "../../../src/interfaces/librelink/graph-response";
import {Entry} from "../../../src/interfaces/nightscout/entry";
import {Entry} from "../../../src/nightscout/interface";

mock.onPost("https://api-eu.libreview.io/llu/auth/login").reply(200, loginSuccessResponse);
mock.onGet("https://api-eu.libreview.io/llu/connections").reply(200, connectionsResponse);
Expand Down Expand Up @@ -89,15 +89,11 @@ describe("LibreLink Up", () => {
const formattedMeasurements: Entry[] = await createFormattedMeasurements(glucoseMeasurements);
expect(formattedMeasurements.length).toBe(142);

expect(formattedMeasurements[0].type).toBe("sgv");
expect(formattedMeasurements[0].date).toBe(1672418860000);
expect(formattedMeasurements[0].dateString).toBe("2022-12-30T16:47:40.000Z");
expect(formattedMeasurements[0].direction).toBe("Flat");
expect(formattedMeasurements[0].sgv).toBe(115);

expect(formattedMeasurements[1].type).toBe("sgv");
expect(formattedMeasurements[1].date).toBe(1672375840000);
expect(formattedMeasurements[1].dateString).toBe("2022-12-30T04:50:40.000Z");
expect(formattedMeasurements[1]).not.toHaveProperty("direction");
expect(formattedMeasurements[1].sgv).toBe(173);
});
Expand All @@ -114,15 +110,11 @@ describe("LibreLink Up", () => {
const formattedMeasurements: Entry[] = await createFormattedMeasurements(glucoseMeasurements);
expect(formattedMeasurements.length).toBe(112);

expect(formattedMeasurements[0].type).toBe("sgv");
expect(formattedMeasurements[0].date).toBe(1672418860000);
expect(formattedMeasurements[0].dateString).toBe("2022-12-30T16:47:40.000Z");
expect(formattedMeasurements[0].direction).toBe("Flat");
expect(formattedMeasurements[0].sgv).toBe(115);

expect(formattedMeasurements[1].type).toBe("sgv");
expect(formattedMeasurements[1].date).toBe(1672384839000);
expect(formattedMeasurements[1].dateString).toBe("2022-12-30T07:20:39.000Z");
expect(formattedMeasurements[1]).not.toHaveProperty("direction");
expect(formattedMeasurements[1].sgv).toBe(177);
});
Expand Down

0 comments on commit b8be1d5

Please sign in to comment.