edilkamin 1.6.1 → 1.7.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/cli-tests.yml +1 -1
- package/.github/workflows/documentation.yml +1 -1
- package/.github/workflows/publish.yml +5 -3
- package/.github/workflows/tests.yml +1 -1
- package/README.md +61 -7
- package/dist/esm/buffer-utils.d.ts +25 -0
- package/dist/esm/buffer-utils.js +70 -0
- package/dist/esm/buffer-utils.test.d.ts +1 -0
- package/dist/esm/buffer-utils.test.js +181 -0
- package/dist/esm/cli.js +110 -9
- package/dist/esm/constants.d.ts +3 -1
- package/dist/esm/constants.js +4 -2
- package/dist/esm/index.d.ts +7 -4
- package/dist/esm/index.js +6 -3
- package/dist/esm/library.d.ts +18 -4
- package/dist/esm/library.js +87 -9
- package/dist/esm/library.test.js +321 -1
- package/dist/esm/serial-utils.d.ts +33 -0
- package/dist/esm/serial-utils.js +45 -0
- package/dist/esm/serial-utils.test.d.ts +1 -0
- package/dist/esm/serial-utils.test.js +48 -0
- package/dist/esm/token-storage.d.ts +14 -0
- package/dist/esm/token-storage.js +81 -0
- package/dist/esm/types.d.ts +49 -1
- package/eslint.config.mjs +12 -1
- package/package.json +5 -3
- package/src/buffer-utils.test.ts +225 -0
- package/src/buffer-utils.ts +83 -0
- package/src/cli.ts +192 -33
- package/src/constants.ts +5 -2
- package/src/index.ts +16 -2
- package/src/library.test.ts +402 -5
- package/src/library.ts +140 -14
- package/src/serial-utils.test.ts +64 -0
- package/src/serial-utils.ts +50 -0
- package/src/token-storage.ts +78 -0
- package/src/types.ts +60 -0
package/src/library.ts
CHANGED
|
@@ -1,10 +1,18 @@
|
|
|
1
1
|
import { strict as assert } from "assert";
|
|
2
2
|
import { Amplify } from "aws-amplify";
|
|
3
3
|
import * as amplifyAuth from "aws-amplify/auth";
|
|
4
|
+
import { cognitoUserPoolsTokenProvider } from "aws-amplify/auth/cognito";
|
|
4
5
|
import axios, { AxiosInstance } from "axios";
|
|
5
6
|
|
|
7
|
+
import { processResponse } from "./buffer-utils";
|
|
6
8
|
import { API_URL } from "./constants";
|
|
7
|
-
import {
|
|
9
|
+
import {
|
|
10
|
+
DeviceAssociationBody,
|
|
11
|
+
DeviceAssociationResponse,
|
|
12
|
+
DeviceInfoRawType,
|
|
13
|
+
DeviceInfoType,
|
|
14
|
+
EditDeviceAssociationBody,
|
|
15
|
+
} from "./types";
|
|
8
16
|
|
|
9
17
|
const amplifyconfiguration = {
|
|
10
18
|
aws_project_region: "eu-central-1",
|
|
@@ -19,14 +27,25 @@ const amplifyconfiguration = {
|
|
|
19
27
|
*/
|
|
20
28
|
const headers = (jwtToken: string) => ({ Authorization: `Bearer ${jwtToken}` });
|
|
21
29
|
|
|
30
|
+
let amplifyConfigured = false;
|
|
31
|
+
|
|
22
32
|
/**
|
|
23
33
|
* Configures Amplify if not already configured.
|
|
24
|
-
*
|
|
34
|
+
* Uses a local flag to avoid calling getConfig() which prints a warning.
|
|
35
|
+
* @param {object} [storage] - Optional custom storage adapter for token persistence
|
|
25
36
|
*/
|
|
26
|
-
const configureAmplify = (
|
|
27
|
-
|
|
28
|
-
|
|
37
|
+
const configureAmplify = (storage?: {
|
|
38
|
+
setItem: (key: string, value: string) => Promise<void>;
|
|
39
|
+
getItem: (key: string) => Promise<string | null>;
|
|
40
|
+
removeItem: (key: string) => Promise<void>;
|
|
41
|
+
clear: () => Promise<void>;
|
|
42
|
+
}) => {
|
|
43
|
+
if (amplifyConfigured) return;
|
|
29
44
|
Amplify.configure(amplifyconfiguration);
|
|
45
|
+
if (storage) {
|
|
46
|
+
cognitoUserPoolsTokenProvider.setKeyValueStorage(storage);
|
|
47
|
+
}
|
|
48
|
+
amplifyConfigured = true;
|
|
30
49
|
};
|
|
31
50
|
|
|
32
51
|
/**
|
|
@@ -39,12 +58,14 @@ const createAuthService = (auth: typeof amplifyAuth) => {
|
|
|
39
58
|
* Signs in a user with the provided credentials.
|
|
40
59
|
* @param {string} username - The username of the user.
|
|
41
60
|
* @param {string} password - The password of the user.
|
|
61
|
+
* @param {boolean} [legacy=false] - If true, returns accessToken for legacy API.
|
|
42
62
|
* @returns {Promise<string>} - The JWT token of the signed-in user.
|
|
43
63
|
* @throws {Error} - If sign-in fails or no tokens are retrieved.
|
|
44
64
|
*/
|
|
45
65
|
const signIn = async (
|
|
46
66
|
username: string,
|
|
47
|
-
password: string
|
|
67
|
+
password: string,
|
|
68
|
+
legacy: boolean = false,
|
|
48
69
|
): Promise<string> => {
|
|
49
70
|
configureAmplify();
|
|
50
71
|
await auth.signOut(); // Ensure the user is signed out first
|
|
@@ -52,32 +73,62 @@ const createAuthService = (auth: typeof amplifyAuth) => {
|
|
|
52
73
|
assert.ok(isSignedIn, "Sign-in failed");
|
|
53
74
|
const { tokens } = await auth.fetchAuthSession();
|
|
54
75
|
assert.ok(tokens, "No tokens found");
|
|
76
|
+
if (legacy) {
|
|
77
|
+
assert.ok(tokens.accessToken, "No access token found");
|
|
78
|
+
return tokens.accessToken.toString();
|
|
79
|
+
}
|
|
55
80
|
assert.ok(tokens.idToken, "No ID token found");
|
|
56
81
|
return tokens.idToken.toString();
|
|
57
82
|
};
|
|
58
|
-
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Retrieves the current session, refreshing tokens if necessary.
|
|
86
|
+
* Requires a prior successful signIn() call.
|
|
87
|
+
* @param {boolean} [forceRefresh=false] - Force token refresh even if valid
|
|
88
|
+
* @param {boolean} [legacy=false] - If true, returns accessToken for legacy API
|
|
89
|
+
* @returns {Promise<string>} - The JWT token (idToken or accessToken)
|
|
90
|
+
* @throws {Error} - If no session exists (user needs to sign in)
|
|
91
|
+
*/
|
|
92
|
+
const getSession = async (
|
|
93
|
+
forceRefresh: boolean = false,
|
|
94
|
+
legacy: boolean = false,
|
|
95
|
+
): Promise<string> => {
|
|
96
|
+
configureAmplify();
|
|
97
|
+
const { tokens } = await auth.fetchAuthSession({ forceRefresh });
|
|
98
|
+
assert.ok(tokens, "No session found - please sign in first");
|
|
99
|
+
if (legacy) {
|
|
100
|
+
assert.ok(tokens.accessToken, "No access token found");
|
|
101
|
+
return tokens.accessToken.toString();
|
|
102
|
+
}
|
|
103
|
+
assert.ok(tokens.idToken, "No ID token found");
|
|
104
|
+
return tokens.idToken.toString();
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
return { signIn, getSession };
|
|
59
108
|
};
|
|
60
109
|
|
|
61
110
|
// Create the default auth service using amplifyAuth
|
|
62
|
-
const { signIn } = createAuthService(amplifyAuth);
|
|
111
|
+
const { signIn, getSession } = createAuthService(amplifyAuth);
|
|
63
112
|
|
|
64
113
|
const deviceInfo =
|
|
65
114
|
(axiosInstance: AxiosInstance) =>
|
|
66
115
|
/**
|
|
67
116
|
* Retrieves information about a device by its MAC address.
|
|
117
|
+
* Automatically decompresses any gzip-compressed Buffer fields in the response.
|
|
68
118
|
*
|
|
69
119
|
* @param {string} jwtToken - The JWT token for authentication.
|
|
70
120
|
* @param {string} macAddress - The MAC address of the device.
|
|
71
121
|
* @returns {Promise<DeviceInfoType>} - A promise that resolves to the device info.
|
|
72
122
|
*/
|
|
73
|
-
async (jwtToken: string, macAddress: string) => {
|
|
74
|
-
const response = await axiosInstance.get<
|
|
123
|
+
async (jwtToken: string, macAddress: string): Promise<DeviceInfoType> => {
|
|
124
|
+
const response = await axiosInstance.get<DeviceInfoRawType>(
|
|
75
125
|
`device/${macAddress}/info`,
|
|
76
126
|
{
|
|
77
127
|
headers: headers(jwtToken),
|
|
78
|
-
}
|
|
128
|
+
},
|
|
79
129
|
);
|
|
80
|
-
|
|
130
|
+
// Process response to decompress any gzipped Buffer fields
|
|
131
|
+
return processResponse(response.data) as DeviceInfoType;
|
|
81
132
|
};
|
|
82
133
|
|
|
83
134
|
const mqttCommand =
|
|
@@ -87,7 +138,7 @@ const mqttCommand =
|
|
|
87
138
|
axiosInstance.put(
|
|
88
139
|
"mqtt/command",
|
|
89
140
|
{ mac_address: macAddress, ...payload },
|
|
90
|
-
{ headers: headers(jwtToken) }
|
|
141
|
+
{ headers: headers(jwtToken) },
|
|
91
142
|
);
|
|
92
143
|
|
|
93
144
|
const setPower =
|
|
@@ -193,6 +244,70 @@ const setTargetTemperature =
|
|
|
193
244
|
value: temperature,
|
|
194
245
|
});
|
|
195
246
|
|
|
247
|
+
const registerDevice =
|
|
248
|
+
(axiosInstance: AxiosInstance) =>
|
|
249
|
+
/**
|
|
250
|
+
* Registers a device with the user's account.
|
|
251
|
+
* This must be called before other device operations will work on the new API.
|
|
252
|
+
*
|
|
253
|
+
* @param {string} jwtToken - The JWT token for authentication.
|
|
254
|
+
* @param {string} macAddress - The MAC address of the device (colons optional).
|
|
255
|
+
* @param {string} serialNumber - The device serial number.
|
|
256
|
+
* @param {string} deviceName - User-friendly name for the device (default: empty string).
|
|
257
|
+
* @param {string} deviceRoom - Room name for the device (default: empty string).
|
|
258
|
+
* @returns {Promise<DeviceAssociationResponse>} - A promise that resolves to the registration response.
|
|
259
|
+
*/
|
|
260
|
+
async (
|
|
261
|
+
jwtToken: string,
|
|
262
|
+
macAddress: string,
|
|
263
|
+
serialNumber: string,
|
|
264
|
+
deviceName: string = "",
|
|
265
|
+
deviceRoom: string = "",
|
|
266
|
+
): Promise<DeviceAssociationResponse> => {
|
|
267
|
+
const body: DeviceAssociationBody = {
|
|
268
|
+
macAddress: macAddress.replace(/:/g, ""),
|
|
269
|
+
deviceName,
|
|
270
|
+
deviceRoom,
|
|
271
|
+
serialNumber,
|
|
272
|
+
};
|
|
273
|
+
const response = await axiosInstance.post<DeviceAssociationResponse>(
|
|
274
|
+
"device",
|
|
275
|
+
body,
|
|
276
|
+
{ headers: headers(jwtToken) },
|
|
277
|
+
);
|
|
278
|
+
return response.data;
|
|
279
|
+
};
|
|
280
|
+
|
|
281
|
+
const editDevice =
|
|
282
|
+
(axiosInstance: AxiosInstance) =>
|
|
283
|
+
/**
|
|
284
|
+
* Updates a device's name and room.
|
|
285
|
+
*
|
|
286
|
+
* @param {string} jwtToken - The JWT token for authentication.
|
|
287
|
+
* @param {string} macAddress - The MAC address of the device (colons optional).
|
|
288
|
+
* @param {string} deviceName - New name for the device (default: empty string).
|
|
289
|
+
* @param {string} deviceRoom - New room for the device (default: empty string).
|
|
290
|
+
* @returns {Promise<DeviceAssociationResponse>} - A promise that resolves to the update response.
|
|
291
|
+
*/
|
|
292
|
+
async (
|
|
293
|
+
jwtToken: string,
|
|
294
|
+
macAddress: string,
|
|
295
|
+
deviceName: string = "",
|
|
296
|
+
deviceRoom: string = "",
|
|
297
|
+
): Promise<DeviceAssociationResponse> => {
|
|
298
|
+
const normalizedMac = macAddress.replace(/:/g, "");
|
|
299
|
+
const body: EditDeviceAssociationBody = {
|
|
300
|
+
deviceName,
|
|
301
|
+
deviceRoom,
|
|
302
|
+
};
|
|
303
|
+
const response = await axiosInstance.put<DeviceAssociationResponse>(
|
|
304
|
+
`device/${normalizedMac}`,
|
|
305
|
+
body,
|
|
306
|
+
{ headers: headers(jwtToken) },
|
|
307
|
+
);
|
|
308
|
+
return response.data;
|
|
309
|
+
};
|
|
310
|
+
|
|
196
311
|
/**
|
|
197
312
|
* Configures the library for API interactions.
|
|
198
313
|
* Initializes API methods with a specified base URL.
|
|
@@ -207,6 +322,8 @@ const setTargetTemperature =
|
|
|
207
322
|
const configure = (baseURL: string = API_URL) => {
|
|
208
323
|
const axiosInstance = axios.create({ baseURL });
|
|
209
324
|
const deviceInfoInstance = deviceInfo(axiosInstance);
|
|
325
|
+
const registerDeviceInstance = registerDevice(axiosInstance);
|
|
326
|
+
const editDeviceInstance = editDevice(axiosInstance);
|
|
210
327
|
const setPowerInstance = setPower(axiosInstance);
|
|
211
328
|
const setPowerOffInstance = setPowerOff(axiosInstance);
|
|
212
329
|
const setPowerOnInstance = setPowerOn(axiosInstance);
|
|
@@ -217,6 +334,8 @@ const configure = (baseURL: string = API_URL) => {
|
|
|
217
334
|
const setTargetTemperatureInstance = setTargetTemperature(axiosInstance);
|
|
218
335
|
return {
|
|
219
336
|
deviceInfo: deviceInfoInstance,
|
|
337
|
+
registerDevice: registerDeviceInstance,
|
|
338
|
+
editDevice: editDeviceInstance,
|
|
220
339
|
setPower: setPowerInstance,
|
|
221
340
|
setPowerOff: setPowerOffInstance,
|
|
222
341
|
setPowerOn: setPowerOnInstance,
|
|
@@ -227,4 +346,11 @@ const configure = (baseURL: string = API_URL) => {
|
|
|
227
346
|
};
|
|
228
347
|
};
|
|
229
348
|
|
|
230
|
-
export {
|
|
349
|
+
export {
|
|
350
|
+
configure,
|
|
351
|
+
configureAmplify,
|
|
352
|
+
createAuthService,
|
|
353
|
+
getSession,
|
|
354
|
+
headers,
|
|
355
|
+
signIn,
|
|
356
|
+
};
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { strict as assert } from "assert";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
serialNumberDisplay,
|
|
5
|
+
serialNumberFromHex,
|
|
6
|
+
serialNumberToHex,
|
|
7
|
+
} from "./serial-utils";
|
|
8
|
+
|
|
9
|
+
describe("serial-utils", () => {
|
|
10
|
+
describe("serialNumberToHex", () => {
|
|
11
|
+
it("should convert ASCII string to hex", () => {
|
|
12
|
+
assert.equal(serialNumberToHex("EDK123"), "45444b313233");
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it("should handle empty string", () => {
|
|
16
|
+
assert.equal(serialNumberToHex(""), "");
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it("should convert string with non-printable chars", () => {
|
|
20
|
+
const input = "EDK\x00123";
|
|
21
|
+
const hex = serialNumberToHex(input);
|
|
22
|
+
assert.equal(hex, "45444b00313233");
|
|
23
|
+
});
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
describe("serialNumberFromHex", () => {
|
|
27
|
+
it("should convert hex back to ASCII string", () => {
|
|
28
|
+
assert.equal(serialNumberFromHex("45444b313233"), "EDK123");
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it("should handle empty string", () => {
|
|
32
|
+
assert.equal(serialNumberFromHex(""), "");
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it("should round-trip with toHex", () => {
|
|
36
|
+
const original = "EDK\x00123\x1F";
|
|
37
|
+
const hex = serialNumberToHex(original);
|
|
38
|
+
const restored = serialNumberFromHex(hex);
|
|
39
|
+
assert.equal(restored, original);
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
describe("serialNumberDisplay", () => {
|
|
44
|
+
it("should remove non-printable characters", () => {
|
|
45
|
+
assert.equal(serialNumberDisplay("EDK\x00123\x1F"), "EDK123");
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it("should collapse whitespace", () => {
|
|
49
|
+
assert.equal(serialNumberDisplay("EDK 123"), "EDK 123");
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it("should trim leading and trailing whitespace", () => {
|
|
53
|
+
assert.equal(serialNumberDisplay(" EDK123 "), "EDK123");
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it("should handle empty string", () => {
|
|
57
|
+
assert.equal(serialNumberDisplay(""), "");
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it("should preserve normal serial numbers", () => {
|
|
61
|
+
assert.equal(serialNumberDisplay("EDK12345678"), "EDK12345678");
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
});
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Converts a raw serial number string to hex-encoded format.
|
|
3
|
+
* This is useful when serial numbers contain non-printable characters.
|
|
4
|
+
*
|
|
5
|
+
* @param serial - The raw serial number string
|
|
6
|
+
* @returns Hex-encoded string representation
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* serialNumberToHex("EDK123") // returns "45444b313233"
|
|
10
|
+
*/
|
|
11
|
+
const serialNumberToHex = (serial: string): string => {
|
|
12
|
+
return Buffer.from(serial, "utf-8").toString("hex");
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Converts a hex-encoded serial number back to raw string format.
|
|
17
|
+
*
|
|
18
|
+
* @param hex - The hex-encoded serial number
|
|
19
|
+
* @returns Raw serial number string
|
|
20
|
+
*
|
|
21
|
+
* @example
|
|
22
|
+
* serialNumberFromHex("45444b313233") // returns "EDK123"
|
|
23
|
+
*/
|
|
24
|
+
const serialNumberFromHex = (hex: string): string => {
|
|
25
|
+
return Buffer.from(hex, "hex").toString("utf-8");
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Produces a display-friendly version of a serial number by removing
|
|
30
|
+
* non-printable characters and collapsing whitespace.
|
|
31
|
+
*
|
|
32
|
+
* @param serial - The raw serial number string
|
|
33
|
+
* @returns Display-friendly serial number
|
|
34
|
+
*
|
|
35
|
+
* @example
|
|
36
|
+
* serialNumberDisplay("EDK\x00123\x1F") // returns "EDK123"
|
|
37
|
+
*/
|
|
38
|
+
const serialNumberDisplay = (serial: string): string => {
|
|
39
|
+
// Remove non-printable characters (ASCII 0-31, 127)
|
|
40
|
+
// Keep printable ASCII (32-126) and extended characters
|
|
41
|
+
return (
|
|
42
|
+
serial
|
|
43
|
+
// eslint-disable-next-line no-control-regex
|
|
44
|
+
.replace(/[\x00-\x1F\x7F]/g, "")
|
|
45
|
+
.replace(/\s+/g, " ")
|
|
46
|
+
.trim()
|
|
47
|
+
);
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
export { serialNumberDisplay, serialNumberFromHex, serialNumberToHex };
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { promises as fs } from "fs";
|
|
2
|
+
import * as os from "os";
|
|
3
|
+
import * as path from "path";
|
|
4
|
+
|
|
5
|
+
const TOKEN_DIR = path.join(os.homedir(), ".edilkamin");
|
|
6
|
+
const TOKEN_FILE = path.join(TOKEN_DIR, "session.json");
|
|
7
|
+
|
|
8
|
+
interface StoredData {
|
|
9
|
+
[key: string]: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Custom storage adapter for AWS Amplify that persists to file system.
|
|
14
|
+
* Used for CLI to maintain sessions between invocations.
|
|
15
|
+
*/
|
|
16
|
+
export const createFileStorage = () => {
|
|
17
|
+
let cache: StoredData = {};
|
|
18
|
+
let loaded = false;
|
|
19
|
+
|
|
20
|
+
const ensureDir = async (): Promise<void> => {
|
|
21
|
+
try {
|
|
22
|
+
await fs.mkdir(TOKEN_DIR, { recursive: true, mode: 0o700 });
|
|
23
|
+
} catch {
|
|
24
|
+
// Directory may already exist
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const load = async (): Promise<void> => {
|
|
29
|
+
if (loaded) return;
|
|
30
|
+
try {
|
|
31
|
+
const data = await fs.readFile(TOKEN_FILE, "utf-8");
|
|
32
|
+
cache = JSON.parse(data);
|
|
33
|
+
} catch {
|
|
34
|
+
cache = {};
|
|
35
|
+
}
|
|
36
|
+
loaded = true;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
const save = async (): Promise<void> => {
|
|
40
|
+
await ensureDir();
|
|
41
|
+
await fs.writeFile(TOKEN_FILE, JSON.stringify(cache), {
|
|
42
|
+
encoding: "utf-8",
|
|
43
|
+
mode: 0o600,
|
|
44
|
+
});
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
return {
|
|
48
|
+
setItem: async (key: string, value: string): Promise<void> => {
|
|
49
|
+
await load();
|
|
50
|
+
cache[key] = value;
|
|
51
|
+
await save();
|
|
52
|
+
},
|
|
53
|
+
getItem: async (key: string): Promise<string | null> => {
|
|
54
|
+
await load();
|
|
55
|
+
return cache[key] ?? null;
|
|
56
|
+
},
|
|
57
|
+
removeItem: async (key: string): Promise<void> => {
|
|
58
|
+
await load();
|
|
59
|
+
delete cache[key];
|
|
60
|
+
await save();
|
|
61
|
+
},
|
|
62
|
+
clear: async (): Promise<void> => {
|
|
63
|
+
cache = {};
|
|
64
|
+
await save();
|
|
65
|
+
},
|
|
66
|
+
};
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Clears all stored session data.
|
|
71
|
+
*/
|
|
72
|
+
export const clearSession = async (): Promise<void> => {
|
|
73
|
+
try {
|
|
74
|
+
await fs.unlink(TOKEN_FILE);
|
|
75
|
+
} catch {
|
|
76
|
+
// File may not exist
|
|
77
|
+
}
|
|
78
|
+
};
|
package/src/types.ts
CHANGED
|
@@ -1,3 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Represents a Node.js Buffer object serialized to JSON.
|
|
3
|
+
* This format is used by the Edilkamin API for gzip-compressed fields.
|
|
4
|
+
*/
|
|
5
|
+
interface BufferEncodedType {
|
|
6
|
+
type: "Buffer";
|
|
7
|
+
data: number[];
|
|
8
|
+
}
|
|
9
|
+
|
|
1
10
|
interface CommandsType {
|
|
2
11
|
power: boolean;
|
|
3
12
|
}
|
|
@@ -27,9 +36,60 @@ interface DeviceInfoType {
|
|
|
27
36
|
};
|
|
28
37
|
}
|
|
29
38
|
|
|
39
|
+
/**
|
|
40
|
+
* Raw device info response that may contain Buffer-encoded compressed fields.
|
|
41
|
+
* Used internally before processing; external callers receive DeviceInfoType.
|
|
42
|
+
*/
|
|
43
|
+
interface DeviceInfoRawType {
|
|
44
|
+
status: StatusType | BufferEncodedType;
|
|
45
|
+
nvm:
|
|
46
|
+
| {
|
|
47
|
+
user_parameters: UserParametersType;
|
|
48
|
+
}
|
|
49
|
+
| BufferEncodedType;
|
|
50
|
+
component_info?: BufferEncodedType | Record<string, unknown>;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Request body for registering a device with a user account.
|
|
55
|
+
* All fields are required by the API.
|
|
56
|
+
*/
|
|
57
|
+
interface DeviceAssociationBody {
|
|
58
|
+
macAddress: string;
|
|
59
|
+
deviceName: string;
|
|
60
|
+
deviceRoom: string;
|
|
61
|
+
serialNumber: string;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Request body for editing a device's name and room.
|
|
66
|
+
* MAC address is specified in the URL path, not the body.
|
|
67
|
+
* Serial number cannot be changed after registration.
|
|
68
|
+
*/
|
|
69
|
+
interface EditDeviceAssociationBody {
|
|
70
|
+
deviceName: string;
|
|
71
|
+
deviceRoom: string;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Response from device registration endpoint.
|
|
76
|
+
* Structure based on Android app behavior - may need adjustment after testing.
|
|
77
|
+
*/
|
|
78
|
+
interface DeviceAssociationResponse {
|
|
79
|
+
macAddress: string;
|
|
80
|
+
deviceName: string;
|
|
81
|
+
deviceRoom: string;
|
|
82
|
+
serialNumber: string;
|
|
83
|
+
}
|
|
84
|
+
|
|
30
85
|
export type {
|
|
86
|
+
BufferEncodedType,
|
|
31
87
|
CommandsType,
|
|
88
|
+
DeviceAssociationBody,
|
|
89
|
+
DeviceAssociationResponse,
|
|
90
|
+
DeviceInfoRawType,
|
|
32
91
|
DeviceInfoType,
|
|
92
|
+
EditDeviceAssociationBody,
|
|
33
93
|
StatusType,
|
|
34
94
|
TemperaturesType,
|
|
35
95
|
UserParametersType,
|