edilkamin 1.6.1 → 1.6.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.
@@ -9,9 +9,21 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
9
9
  };
10
10
  import { strict as assert } from "assert";
11
11
  import axios from "axios";
12
+ import pako from "pako";
12
13
  import sinon from "sinon";
13
14
  import { configure, createAuthService } from "../src/library";
14
15
  import { API_URL } from "./constants";
16
+ /**
17
+ * Helper to create a gzip-compressed Buffer object for testing.
18
+ */
19
+ const createGzippedBuffer = (data) => {
20
+ const json = JSON.stringify(data);
21
+ const compressed = pako.gzip(json);
22
+ return {
23
+ type: "Buffer",
24
+ data: Array.from(compressed),
25
+ };
26
+ };
15
27
  describe("library", () => {
16
28
  let axiosStub;
17
29
  const expectedToken = "mockJwtToken";
@@ -26,7 +38,7 @@ describe("library", () => {
26
38
  sinon.restore();
27
39
  });
28
40
  describe("signIn", () => {
29
- it("should sign in and return the JWT token", () => __awaiter(void 0, void 0, void 0, function* () {
41
+ it("should sign in and return the ID token by default", () => __awaiter(void 0, void 0, void 0, function* () {
30
42
  const expectedUsername = "testuser";
31
43
  const expectedPassword = "testpassword";
32
44
  const signIn = sinon.stub().resolves({ isSignedIn: true });
@@ -34,6 +46,7 @@ describe("library", () => {
34
46
  const fetchAuthSession = sinon.stub().resolves({
35
47
  tokens: {
36
48
  idToken: { toString: () => expectedToken },
49
+ accessToken: { toString: () => "accessToken" },
37
50
  },
38
51
  });
39
52
  const authStub = {
@@ -50,6 +63,28 @@ describe("library", () => {
50
63
  ]);
51
64
  assert.equal(token, expectedToken);
52
65
  }));
66
+ it("should sign in and return the access token in legacy mode", () => __awaiter(void 0, void 0, void 0, function* () {
67
+ const expectedUsername = "testuser";
68
+ const expectedPassword = "testpassword";
69
+ const signIn = sinon.stub().resolves({ isSignedIn: true });
70
+ const signOut = sinon.stub();
71
+ const fetchAuthSession = sinon.stub().resolves({
72
+ tokens: {
73
+ accessToken: { toString: () => expectedToken },
74
+ idToken: { toString: () => "idToken" },
75
+ },
76
+ });
77
+ const authStub = {
78
+ signIn,
79
+ signOut,
80
+ fetchAuthSession,
81
+ };
82
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
83
+ const authService = createAuthService(authStub);
84
+ const token = yield authService.signIn(expectedUsername, expectedPassword, true // legacy mode
85
+ );
86
+ assert.equal(token, expectedToken);
87
+ }));
53
88
  it("should throw an error if sign-in fails", () => __awaiter(void 0, void 0, void 0, function* () {
54
89
  const expectedUsername = "testuser";
55
90
  const expectedPassword = "testpassword";
@@ -76,6 +111,8 @@ describe("library", () => {
76
111
  describe("configure", () => {
77
112
  const expectedApi = [
78
113
  "deviceInfo",
114
+ "registerDevice",
115
+ "editDevice",
79
116
  "setPower",
80
117
  "setPowerOff",
81
118
  "setPowerOn",
@@ -242,4 +279,226 @@ describe("library", () => {
242
279
  }));
243
280
  });
244
281
  });
282
+ describe("registerDevice", () => {
283
+ it("should call POST /device with correct payload", () => __awaiter(void 0, void 0, void 0, function* () {
284
+ const mockResponse = {
285
+ macAddress: "AABBCCDDEEFF",
286
+ deviceName: "Test Stove",
287
+ deviceRoom: "Living Room",
288
+ serialNumber: "EDK123",
289
+ };
290
+ const mockAxios = {
291
+ post: sinon.stub().resolves({ data: mockResponse }),
292
+ get: sinon.stub(),
293
+ put: sinon.stub(),
294
+ };
295
+ axiosStub.returns(mockAxios);
296
+ const api = configure("https://example.com/api");
297
+ const result = yield api.registerDevice(expectedToken, "AA:BB:CC:DD:EE:FF", "EDK123", "Test Stove", "Living Room");
298
+ assert.deepEqual(mockAxios.post.args, [
299
+ [
300
+ "device",
301
+ {
302
+ macAddress: "AABBCCDDEEFF",
303
+ deviceName: "Test Stove",
304
+ deviceRoom: "Living Room",
305
+ serialNumber: "EDK123",
306
+ },
307
+ { headers: { Authorization: `Bearer ${expectedToken}` } },
308
+ ],
309
+ ]);
310
+ assert.deepEqual(result, mockResponse);
311
+ }));
312
+ it("should normalize MAC address by removing colons", () => __awaiter(void 0, void 0, void 0, function* () {
313
+ const mockAxios = {
314
+ post: sinon.stub().resolves({ data: {} }),
315
+ get: sinon.stub(),
316
+ put: sinon.stub(),
317
+ };
318
+ axiosStub.returns(mockAxios);
319
+ const api = configure("https://example.com/api");
320
+ yield api.registerDevice(expectedToken, "AA:BB:CC:DD:EE:FF", "EDK123");
321
+ assert.equal(mockAxios.post.args[0][1].macAddress, "AABBCCDDEEFF");
322
+ }));
323
+ it("should use empty strings as defaults for name and room", () => __awaiter(void 0, void 0, void 0, function* () {
324
+ const mockAxios = {
325
+ post: sinon.stub().resolves({ data: {} }),
326
+ get: sinon.stub(),
327
+ put: sinon.stub(),
328
+ };
329
+ axiosStub.returns(mockAxios);
330
+ const api = configure("https://example.com/api");
331
+ yield api.registerDevice(expectedToken, "AABBCCDDEEFF", "EDK123");
332
+ assert.equal(mockAxios.post.args[0][1].deviceName, "");
333
+ assert.equal(mockAxios.post.args[0][1].deviceRoom, "");
334
+ }));
335
+ });
336
+ describe("editDevice", () => {
337
+ it("should call PUT /device/{mac} with correct payload", () => __awaiter(void 0, void 0, void 0, function* () {
338
+ const mockResponse = {
339
+ macAddress: "AABBCCDDEEFF",
340
+ deviceName: "Updated Name",
341
+ deviceRoom: "Basement",
342
+ serialNumber: "EDK123",
343
+ };
344
+ const mockAxios = {
345
+ put: sinon.stub().resolves({ data: mockResponse }),
346
+ get: sinon.stub(),
347
+ post: sinon.stub(),
348
+ };
349
+ axiosStub.returns(mockAxios);
350
+ const api = configure("https://example.com/api");
351
+ const result = yield api.editDevice(expectedToken, "AA:BB:CC:DD:EE:FF", "Updated Name", "Basement");
352
+ assert.deepEqual(mockAxios.put.args, [
353
+ [
354
+ "device/AABBCCDDEEFF",
355
+ {
356
+ deviceName: "Updated Name",
357
+ deviceRoom: "Basement",
358
+ },
359
+ { headers: { Authorization: `Bearer ${expectedToken}` } },
360
+ ],
361
+ ]);
362
+ assert.deepEqual(result, mockResponse);
363
+ }));
364
+ it("should use empty strings as defaults for name and room", () => __awaiter(void 0, void 0, void 0, function* () {
365
+ const mockAxios = {
366
+ put: sinon.stub().resolves({ data: {} }),
367
+ get: sinon.stub(),
368
+ post: sinon.stub(),
369
+ };
370
+ axiosStub.returns(mockAxios);
371
+ const api = configure("https://example.com/api");
372
+ yield api.editDevice(expectedToken, "AABBCCDDEEFF");
373
+ assert.equal(mockAxios.put.args[0][1].deviceName, "");
374
+ assert.equal(mockAxios.put.args[0][1].deviceRoom, "");
375
+ }));
376
+ });
377
+ describe("deviceInfo with compressed responses", () => {
378
+ it("should decompress Buffer-encoded status field", () => __awaiter(void 0, void 0, void 0, function* () {
379
+ const statusData = {
380
+ commands: { power: true },
381
+ temperatures: { enviroment: 19, board: 25 },
382
+ };
383
+ const mockResponse = {
384
+ status: createGzippedBuffer(statusData),
385
+ nvm: {
386
+ user_parameters: {
387
+ enviroment_1_temperature: 22,
388
+ },
389
+ },
390
+ };
391
+ const mockAxios = {
392
+ get: sinon.stub().resolves({ data: mockResponse }),
393
+ };
394
+ axiosStub.returns(mockAxios);
395
+ const api = configure("https://example.com/api");
396
+ const result = yield api.deviceInfo(expectedToken, "mockMacAddress");
397
+ assert.deepEqual(result.status, statusData);
398
+ }));
399
+ it("should decompress Buffer-encoded nvm field", () => __awaiter(void 0, void 0, void 0, function* () {
400
+ const nvmData = {
401
+ user_parameters: {
402
+ enviroment_1_temperature: 22,
403
+ enviroment_2_temperature: 0,
404
+ enviroment_3_temperature: 0,
405
+ is_auto: false,
406
+ is_sound_active: true,
407
+ },
408
+ };
409
+ const mockResponse = {
410
+ status: {
411
+ commands: { power: true },
412
+ temperatures: { enviroment: 19 },
413
+ },
414
+ nvm: createGzippedBuffer(nvmData),
415
+ };
416
+ const mockAxios = {
417
+ get: sinon.stub().resolves({ data: mockResponse }),
418
+ };
419
+ axiosStub.returns(mockAxios);
420
+ const api = configure("https://example.com/api");
421
+ const result = yield api.deviceInfo(expectedToken, "mockMacAddress");
422
+ assert.deepEqual(result.nvm, nvmData);
423
+ }));
424
+ it("should handle fully compressed response (status and nvm)", () => __awaiter(void 0, void 0, void 0, function* () {
425
+ const statusData = {
426
+ commands: { power: false },
427
+ temperatures: { enviroment: 21, board: 30 },
428
+ };
429
+ const nvmData = {
430
+ user_parameters: {
431
+ enviroment_1_temperature: 20,
432
+ enviroment_2_temperature: 0,
433
+ enviroment_3_temperature: 0,
434
+ is_auto: true,
435
+ is_sound_active: false,
436
+ },
437
+ };
438
+ const mockResponse = {
439
+ status: createGzippedBuffer(statusData),
440
+ nvm: createGzippedBuffer(nvmData),
441
+ };
442
+ const mockAxios = {
443
+ get: sinon.stub().resolves({ data: mockResponse }),
444
+ };
445
+ axiosStub.returns(mockAxios);
446
+ const api = configure("https://example.com/api");
447
+ const result = yield api.deviceInfo(expectedToken, "mockMacAddress");
448
+ assert.deepEqual(result.status, statusData);
449
+ assert.deepEqual(result.nvm, nvmData);
450
+ }));
451
+ it("should work with getPower on compressed response", () => __awaiter(void 0, void 0, void 0, function* () {
452
+ const statusData = {
453
+ commands: { power: true },
454
+ temperatures: { enviroment: 19 },
455
+ };
456
+ const mockResponse = {
457
+ status: createGzippedBuffer(statusData),
458
+ nvm: { user_parameters: { enviroment_1_temperature: 22 } },
459
+ };
460
+ const mockAxios = {
461
+ get: sinon.stub().resolves({ data: mockResponse }),
462
+ };
463
+ axiosStub.returns(mockAxios);
464
+ const api = configure("https://example.com/api");
465
+ const result = yield api.getPower(expectedToken, "mockMacAddress");
466
+ assert.equal(result, true);
467
+ }));
468
+ it("should work with getEnvironmentTemperature on compressed response", () => __awaiter(void 0, void 0, void 0, function* () {
469
+ const statusData = {
470
+ commands: { power: true },
471
+ temperatures: { enviroment: 19, board: 25 },
472
+ };
473
+ const mockResponse = {
474
+ status: createGzippedBuffer(statusData),
475
+ nvm: { user_parameters: { enviroment_1_temperature: 22 } },
476
+ };
477
+ const mockAxios = {
478
+ get: sinon.stub().resolves({ data: mockResponse }),
479
+ };
480
+ axiosStub.returns(mockAxios);
481
+ const api = configure("https://example.com/api");
482
+ const result = yield api.getEnvironmentTemperature(expectedToken, "mockMacAddress");
483
+ assert.equal(result, 19);
484
+ }));
485
+ it("should work with getTargetTemperature on compressed response", () => __awaiter(void 0, void 0, void 0, function* () {
486
+ const nvmData = {
487
+ user_parameters: {
488
+ enviroment_1_temperature: 22,
489
+ },
490
+ };
491
+ const mockResponse = {
492
+ status: { commands: { power: true }, temperatures: { enviroment: 19 } },
493
+ nvm: createGzippedBuffer(nvmData),
494
+ };
495
+ const mockAxios = {
496
+ get: sinon.stub().resolves({ data: mockResponse }),
497
+ };
498
+ axiosStub.returns(mockAxios);
499
+ const api = configure("https://example.com/api");
500
+ const result = yield api.getTargetTemperature(expectedToken, "mockMacAddress");
501
+ assert.equal(result, 22);
502
+ }));
503
+ });
245
504
  });
@@ -0,0 +1,33 @@
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
+ declare const serialNumberToHex: (serial: string) => string;
12
+ /**
13
+ * Converts a hex-encoded serial number back to raw string format.
14
+ *
15
+ * @param hex - The hex-encoded serial number
16
+ * @returns Raw serial number string
17
+ *
18
+ * @example
19
+ * serialNumberFromHex("45444b313233") // returns "EDK123"
20
+ */
21
+ declare const serialNumberFromHex: (hex: string) => string;
22
+ /**
23
+ * Produces a display-friendly version of a serial number by removing
24
+ * non-printable characters and collapsing whitespace.
25
+ *
26
+ * @param serial - The raw serial number string
27
+ * @returns Display-friendly serial number
28
+ *
29
+ * @example
30
+ * serialNumberDisplay("EDK\x00123\x1F") // returns "EDK123"
31
+ */
32
+ declare const serialNumberDisplay: (serial: string) => string;
33
+ export { serialNumberDisplay, serialNumberFromHex, serialNumberToHex };
@@ -0,0 +1,45 @@
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) => {
12
+ return Buffer.from(serial, "utf-8").toString("hex");
13
+ };
14
+ /**
15
+ * Converts a hex-encoded serial number back to raw string format.
16
+ *
17
+ * @param hex - The hex-encoded serial number
18
+ * @returns Raw serial number string
19
+ *
20
+ * @example
21
+ * serialNumberFromHex("45444b313233") // returns "EDK123"
22
+ */
23
+ const serialNumberFromHex = (hex) => {
24
+ return Buffer.from(hex, "hex").toString("utf-8");
25
+ };
26
+ /**
27
+ * Produces a display-friendly version of a serial number by removing
28
+ * non-printable characters and collapsing whitespace.
29
+ *
30
+ * @param serial - The raw serial number string
31
+ * @returns Display-friendly serial number
32
+ *
33
+ * @example
34
+ * serialNumberDisplay("EDK\x00123\x1F") // returns "EDK123"
35
+ */
36
+ const serialNumberDisplay = (serial) => {
37
+ // Remove non-printable characters (ASCII 0-31, 127)
38
+ // Keep printable ASCII (32-126) and extended characters
39
+ return (serial
40
+ // eslint-disable-next-line no-control-regex
41
+ .replace(/[\x00-\x1F\x7F]/g, "")
42
+ .replace(/\s+/g, " ")
43
+ .trim());
44
+ };
45
+ export { serialNumberDisplay, serialNumberFromHex, serialNumberToHex };
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,48 @@
1
+ import { strict as assert } from "assert";
2
+ import { serialNumberDisplay, serialNumberFromHex, serialNumberToHex, } from "./serial-utils";
3
+ describe("serial-utils", () => {
4
+ describe("serialNumberToHex", () => {
5
+ it("should convert ASCII string to hex", () => {
6
+ assert.equal(serialNumberToHex("EDK123"), "45444b313233");
7
+ });
8
+ it("should handle empty string", () => {
9
+ assert.equal(serialNumberToHex(""), "");
10
+ });
11
+ it("should convert string with non-printable chars", () => {
12
+ const input = "EDK\x00123";
13
+ const hex = serialNumberToHex(input);
14
+ assert.equal(hex, "45444b00313233");
15
+ });
16
+ });
17
+ describe("serialNumberFromHex", () => {
18
+ it("should convert hex back to ASCII string", () => {
19
+ assert.equal(serialNumberFromHex("45444b313233"), "EDK123");
20
+ });
21
+ it("should handle empty string", () => {
22
+ assert.equal(serialNumberFromHex(""), "");
23
+ });
24
+ it("should round-trip with toHex", () => {
25
+ const original = "EDK\x00123\x1F";
26
+ const hex = serialNumberToHex(original);
27
+ const restored = serialNumberFromHex(hex);
28
+ assert.equal(restored, original);
29
+ });
30
+ });
31
+ describe("serialNumberDisplay", () => {
32
+ it("should remove non-printable characters", () => {
33
+ assert.equal(serialNumberDisplay("EDK\x00123\x1F"), "EDK123");
34
+ });
35
+ it("should collapse whitespace", () => {
36
+ assert.equal(serialNumberDisplay("EDK 123"), "EDK 123");
37
+ });
38
+ it("should trim leading and trailing whitespace", () => {
39
+ assert.equal(serialNumberDisplay(" EDK123 "), "EDK123");
40
+ });
41
+ it("should handle empty string", () => {
42
+ assert.equal(serialNumberDisplay(""), "");
43
+ });
44
+ it("should preserve normal serial numbers", () => {
45
+ assert.equal(serialNumberDisplay("EDK12345678"), "EDK12345678");
46
+ });
47
+ });
48
+ });
@@ -1,3 +1,11 @@
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
+ }
1
9
  interface CommandsType {
2
10
  power: boolean;
3
11
  }
@@ -22,4 +30,44 @@ interface DeviceInfoType {
22
30
  user_parameters: UserParametersType;
23
31
  };
24
32
  }
25
- export type { CommandsType, DeviceInfoType, StatusType, TemperaturesType, UserParametersType, };
33
+ /**
34
+ * Raw device info response that may contain Buffer-encoded compressed fields.
35
+ * Used internally before processing; external callers receive DeviceInfoType.
36
+ */
37
+ interface DeviceInfoRawType {
38
+ status: StatusType | BufferEncodedType;
39
+ nvm: {
40
+ user_parameters: UserParametersType;
41
+ } | BufferEncodedType;
42
+ component_info?: BufferEncodedType | Record<string, unknown>;
43
+ }
44
+ /**
45
+ * Request body for registering a device with a user account.
46
+ * All fields are required by the API.
47
+ */
48
+ interface DeviceAssociationBody {
49
+ macAddress: string;
50
+ deviceName: string;
51
+ deviceRoom: string;
52
+ serialNumber: string;
53
+ }
54
+ /**
55
+ * Request body for editing a device's name and room.
56
+ * MAC address is specified in the URL path, not the body.
57
+ * Serial number cannot be changed after registration.
58
+ */
59
+ interface EditDeviceAssociationBody {
60
+ deviceName: string;
61
+ deviceRoom: string;
62
+ }
63
+ /**
64
+ * Response from device registration endpoint.
65
+ * Structure based on Android app behavior - may need adjustment after testing.
66
+ */
67
+ interface DeviceAssociationResponse {
68
+ macAddress: string;
69
+ deviceName: string;
70
+ deviceRoom: string;
71
+ serialNumber: string;
72
+ }
73
+ export type { BufferEncodedType, CommandsType, DeviceAssociationBody, DeviceAssociationResponse, DeviceInfoRawType, DeviceInfoType, EditDeviceAssociationBody, StatusType, TemperaturesType, UserParametersType, };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "edilkamin",
3
- "version": "1.6.1",
3
+ "version": "1.6.2",
4
4
  "description": "",
5
5
  "main": "dist/cjs/index.js",
6
6
  "module": "dist/esm/index.js",
@@ -42,13 +42,15 @@
42
42
  },
43
43
  "dependencies": {
44
44
  "aws-amplify": "^6.10.0",
45
- "axios": "^1.13.2"
45
+ "axios": "^1.13.2",
46
+ "pako": "^2.1.0"
46
47
  },
47
48
  "devDependencies": {
48
49
  "@aws-amplify/cli": "^7.6.21",
49
50
  "@eslint/eslintrc": "^3.2.0",
50
51
  "@eslint/js": "^9.16.0",
51
52
  "@types/mocha": "^10.0.10",
53
+ "@types/pako": "^2.0.4",
52
54
  "@types/sinon": "^17.0.3",
53
55
  "@typescript-eslint/eslint-plugin": "^8.17.0",
54
56
  "@typescript-eslint/parser": "^8.17.0",