Skip to content

Commit ea4b22e

Browse files
Merge pull request #206 from KelvinKramp/apiv3
Apiv3
2 parents 74334eb + 73b78f9 commit ea4b22e

File tree

5 files changed

+156
-51
lines changed

5 files changed

+156
-51
lines changed

src/config.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -56,15 +56,16 @@ function readConfig()
5656
linkUpUsername: process.env.LINK_UP_USERNAME as string,
5757
linkUpPassword: process.env.LINK_UP_PASSWORD as string,
5858
linkUpVersion: process.env.LINK_UP_VERSION || "4.16.0",
59-
6059
logLevel: process.env.LOG_LEVEL || 'info',
6160
singleShot: process.env.SINGLE_SHOT === 'true',
6261
allData: process.env.ALL_DATA === 'true',
63-
6462
nightscoutApiV3: process.env.NIGHTSCOUT_API_V3 === 'true',
6563
nightscoutDisableHttps: process.env.NIGHTSCOUT_DISABLE_HTTPS === 'true',
66-
nightscoutDevice: process.env.DEVICE_NAME || 'nightscout-librelink-up',
67-
64+
nightscoutDevice:
65+
process.env.NIGHTSCOUT_API_V3 === "true"
66+
? process.env.NIGHTSCOUT_DEVICE_NAME || "freestyle-libre"
67+
: process.env.NIGHTSCOUT_DEVICE_NAME || "nightscout-librelink-up",
68+
nightscoutApp: process.env.APP || 'librelinkup-to-nightscout-script',
6869
linkUpRegion: process.env.LINK_UP_REGION || 'EU',
6970
linkUpTimeInterval: Number(process.env.LINK_UP_TIME_INTERVAL) || 5,
7071
linkUpConnection: process.env.LINK_UP_CONNECTION as string,

src/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ const logFormat = printf(({level, message}) =>
5151
return `[${level}]: ${message}`;
5252
});
5353

54-
const logger = createLogger({
54+
export const logger = createLogger({
5555
format: combine(
5656
timestamp(),
5757
logFormat

src/nightscout/apiv1.ts

Lines changed: 35 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,64 +1,63 @@
1-
import {Entry, NightscoutAPI, NightscoutConfig} from "./interface";
2-
import axios, {RawAxiosRequestHeaders} from "axios";
1+
import axios, { RawAxiosRequestHeaders } from "axios";
2+
import { Entry, NightscoutAPI, NightscoutConfig } from "./interface";
3+
import { logger } from "..";
34

4-
interface NightscoutHttpHeaders extends RawAxiosRequestHeaders
5-
{
6-
"api-secret": string | undefined;
5+
interface NightscoutApiV1HttpHeaders extends RawAxiosRequestHeaders {
6+
"api-secret": string | undefined;
77
}
88

9-
export class Client implements NightscoutAPI
10-
{
11-
readonly baseUrl: string;
12-
readonly headers: NightscoutHttpHeaders;
13-
readonly device: string;
9+
export class Client implements NightscoutAPI {
10+
readonly baseUrl: string;
11+
readonly headers: NightscoutApiV1HttpHeaders;
12+
readonly device: string;
1413

1514
constructor(config: NightscoutConfig)
1615
{
17-
this.baseUrl = config.nightscoutBaseUrl;
18-
this.headers = {
19-
"api-secret": config.nightscoutApiToken,
20-
"User-Agent": "FreeStyle LibreLink Up NightScout Uploader",
21-
"Content-Type": "application/json",
22-
};
23-
this.device = config.nightscoutDevice;
24-
}
16+
this.baseUrl = config.nightscoutBaseUrl;
17+
this.headers = {
18+
"api-secret": config.nightscoutApiToken,
19+
"User-Agent": "FreeStyle LibreLink Up NightScout Uploader",
20+
"Content-Type": "application/json",
21+
};
22+
this.device = config.nightscoutDevice;
23+
}
2524

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

3130
if (resp.status !== 200)
3231
{
33-
throw Error(`failed to get last entry: ${resp.statusText}`);
34-
}
32+
throw Error(`failed to get last entry: ${resp.statusText}`);
33+
}
3534

3635
if (!resp.data || resp.data.length === 0)
3736
{
38-
return null;
39-
}
40-
return resp.data.pop();
37+
return null;
4138
}
39+
return resp.data.pop();
40+
}
4241

4342
async uploadEntries(entries: Entry[]): Promise<void>
4443
{
45-
const url = new URL("/api/v1/entries", this.baseUrl).toString();
46-
const entriesV1 = entries.map((e) => ({
47-
type: "sgv",
48-
sgv: e.sgv,
49-
direction: e.direction?.toString(),
50-
device: this.device,
51-
date: e.date.getTime(),
52-
dateString: e.date.toISOString(),
53-
}));
44+
const url = new URL("/api/v1/entries", this.baseUrl).toString();
45+
const entriesV1 = entries.map((e) => ({
46+
type: "sgv",
47+
sgv: e.sgv,
48+
direction: e.direction?.toString(),
49+
device: this.device,
50+
date: e.date.getTime(),
51+
dateString: e.date.toISOString(),
52+
}));
5453

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

5756
if (resp.status !== 200)
5857
{
5958
throw Error(`failed to post new entries: ${resp.statusText} ${resp.status}`);
60-
}
61-
62-
return;
6359
}
60+
61+
return;
62+
}
6463
}

src/nightscout/apiv3.ts

Lines changed: 114 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,119 @@
1-
import { Entry, NightscoutAPI, NightscoutConfig } from './interface';
1+
import axios, { RawAxiosRequestHeaders } from "axios";
2+
import { logger } from "..";
3+
import { Entry, NightscoutAPI, NightscoutConfig } from "./interface";
4+
5+
interface NightscoutApiV3HttpHeaders extends RawAxiosRequestHeaders {
6+
Authorization: string;
7+
}
28

39
export class Client implements NightscoutAPI {
4-
constructor(config: NightscoutConfig) {
5-
throw new Error('Not implemented');
6-
}
10+
readonly baseUrl: string;
11+
readonly headers: NightscoutApiV3HttpHeaders;
12+
readonly device: string;
13+
readonly accessToken: string;
14+
readonly app: string;
15+
16+
constructor(config: NightscoutConfig) {
17+
this.baseUrl = config.nightscoutBaseUrl;
18+
this.headers = {
19+
"User-Agent": "FreeStyle LibreLink Up NightScout Uploader",
20+
"Content-Type": "application/json",
21+
Authorization: "",
22+
};
23+
this.device = config.nightscoutDevice;
24+
this.accessToken = config.nightscoutApiToken;
25+
this.app = config.nightscoutApp;
26+
}
27+
28+
private addBearerJwtToken(jwtToken: string): string {
29+
if (!jwtToken) {
30+
throw Error("No jwtToken found");
31+
}
32+
return `Bearer ${jwtToken}`;
33+
}
34+
35+
async getJwtToken(): Promise<string> {
36+
let newToken = "";
37+
try {
38+
const url = new URL(
39+
`/api/v2/authorization/request/${this.accessToken}`,
40+
this.baseUrl
41+
).toString();
42+
const resp = await axios.get(url, {
43+
headers: {
44+
...this.headers,
45+
Accept: "application/json",
46+
},
47+
});
48+
if (resp.status !== 200 || !resp.data.token) {
49+
throw Error(`Error getting JWT token: ${resp.statusText} `);
50+
}
51+
newToken = await resp.data.token;
52+
} catch (error) {
53+
logger.error("Error getting JWT token:", error);
54+
}
55+
return newToken;
56+
}
57+
58+
async lastEntry(): Promise<Entry | null> {
59+
const jwtToken = await this.getJwtToken();
60+
const url = new URL(
61+
"/api/v3/entries?limit=1&sort$desc=date",
62+
this.baseUrl
63+
).toString();
64+
const resp = await axios.get(url, {
65+
headers: {
66+
...this.headers,
67+
Authorization: this.addBearerJwtToken(jwtToken),
68+
} as NightscoutApiV3HttpHeaders,
69+
});
70+
if (resp.status !== 200) {
71+
throw Error(`Failed to get last entry: ${resp.statusText}`);
72+
}
73+
if (!resp.data.result || resp.data.result.length === 0) {
74+
throw Error(
75+
`Last entry not found in response data: ${JSON.stringify(resp.data)}`
76+
);
77+
}
78+
return resp.data.result.pop();
79+
}
80+
81+
async uploadEntries(entries: Entry[]): Promise<void> {
82+
const jwtToken = await this.getJwtToken();
83+
const url = new URL("/api/v3/entries", this.baseUrl).toString();
84+
85+
if (!entries.length) {
86+
throw Error(`No entries to upload`);
87+
}
88+
89+
const entryPayloads = entries.map((entry) => ({
90+
type: "sgv",
91+
sgv: entry.sgv,
92+
direction: entry.direction?.toString(),
93+
device: this.device,
94+
date: entry.date.getTime(),
95+
app: this.app,
96+
}));
797

8-
async lastEntry(): Promise<Entry | null> {
9-
throw new Error('Not implemented');
10-
}
98+
// APIv3 accepts only single entries
99+
const responses = await Promise.all(
100+
entryPayloads.map((entryV3) =>
101+
axios.post(url, entryV3, {
102+
headers: {
103+
...this.headers,
104+
Authorization: this.addBearerJwtToken(jwtToken),
105+
} as NightscoutApiV3HttpHeaders,
106+
})
107+
)
108+
);
11109

12-
async uploadEntries(entries: Entry[]): Promise<void> {
13-
throw new Error('Not implemented');
14-
}
110+
responses.forEach((resp) => {
111+
if (resp.status !== 201) {
112+
throw Error(
113+
`failed to post new entries: ${resp.statusText} ${resp.status}`
114+
);
115+
}
116+
});
117+
return;
118+
}
15119
}

src/nightscout/interface.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ export interface NightscoutConfig {
1313
nightscoutApiToken: string;
1414
nightscoutBaseUrl: string;
1515
nightscoutDevice: string;
16+
nightscoutApp: string;
1617
}
1718

1819
export interface Entry {

0 commit comments

Comments
 (0)