Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Moved nightscout code into separated folder #122

Merged
merged 1 commit into from
Mar 26, 2024
Merged
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
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
Loading