edilkamin 1.6.0 → 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.
@@ -0,0 +1,225 @@
1
+ import { strict as assert } from "assert";
2
+ import pako from "pako";
3
+ import sinon from "sinon";
4
+
5
+ import { decompressBuffer, isBuffer, processResponse } from "./buffer-utils";
6
+
7
+ /**
8
+ * Helper to create a gzip-compressed Buffer object for testing.
9
+ */
10
+ const createGzippedBuffer = (
11
+ data: unknown
12
+ ): { type: "Buffer"; data: number[] } => {
13
+ const json = JSON.stringify(data);
14
+ const compressed = pako.gzip(json);
15
+ return {
16
+ type: "Buffer",
17
+ data: Array.from(compressed),
18
+ };
19
+ };
20
+
21
+ describe("buffer-utils", () => {
22
+ afterEach(() => {
23
+ sinon.restore();
24
+ });
25
+
26
+ describe("isBuffer", () => {
27
+ it("should detect valid Buffer objects", () => {
28
+ const buffer = { type: "Buffer", data: [31, 139, 8, 0] };
29
+ assert.ok(isBuffer(buffer));
30
+ });
31
+
32
+ it("should detect empty Buffer objects", () => {
33
+ const buffer = { type: "Buffer", data: [] };
34
+ assert.ok(isBuffer(buffer));
35
+ });
36
+
37
+ it("should reject non-Buffer objects with wrong type", () => {
38
+ assert.ok(!isBuffer({ type: "NotBuffer", data: [] }));
39
+ });
40
+
41
+ it("should reject objects without type field", () => {
42
+ assert.ok(!isBuffer({ data: [1, 2, 3] }));
43
+ });
44
+
45
+ it("should reject objects without data field", () => {
46
+ assert.ok(!isBuffer({ type: "Buffer" }));
47
+ });
48
+
49
+ it("should reject objects with non-array data", () => {
50
+ assert.ok(!isBuffer({ type: "Buffer", data: "not an array" }));
51
+ });
52
+
53
+ it("should reject null", () => {
54
+ assert.ok(!isBuffer(null));
55
+ });
56
+
57
+ it("should reject undefined", () => {
58
+ assert.ok(!isBuffer(undefined));
59
+ });
60
+
61
+ it("should reject primitives", () => {
62
+ assert.ok(!isBuffer("string"));
63
+ assert.ok(!isBuffer(123));
64
+ assert.ok(!isBuffer(true));
65
+ });
66
+ });
67
+
68
+ describe("decompressBuffer", () => {
69
+ it("should decompress gzipped JSON buffer", () => {
70
+ const originalData = { test: "value", nested: { key: 123 } };
71
+ const bufferObj = createGzippedBuffer(originalData);
72
+
73
+ const result = decompressBuffer(bufferObj);
74
+ assert.deepEqual(result, originalData);
75
+ });
76
+
77
+ it("should handle gzipped arrays", () => {
78
+ const originalData = [1, 2, 3, "test"];
79
+ const bufferObj = createGzippedBuffer(originalData);
80
+
81
+ const result = decompressBuffer(bufferObj);
82
+ assert.deepEqual(result, originalData);
83
+ });
84
+
85
+ it("should handle gzipped strings", () => {
86
+ const originalData = "test string";
87
+ const bufferObj = createGzippedBuffer(originalData);
88
+
89
+ const result = decompressBuffer(bufferObj);
90
+ assert.equal(result, originalData);
91
+ });
92
+
93
+ it("should return original value if decompression fails", () => {
94
+ const consoleWarnStub = sinon.stub(console, "warn");
95
+ const invalidBuffer = { type: "Buffer" as const, data: [1, 2, 3] };
96
+
97
+ const result = decompressBuffer(invalidBuffer);
98
+
99
+ assert.deepEqual(result, invalidBuffer);
100
+ assert.ok(consoleWarnStub.calledOnce);
101
+ });
102
+
103
+ it("should return original value if JSON parsing fails", () => {
104
+ const consoleWarnStub = sinon.stub(console, "warn");
105
+ // Create valid gzip but invalid JSON
106
+ const invalidJson = "not valid json {";
107
+ const compressed = pako.gzip(invalidJson);
108
+ const bufferObj = {
109
+ type: "Buffer" as const,
110
+ data: Array.from(compressed),
111
+ };
112
+
113
+ const result = decompressBuffer(bufferObj);
114
+
115
+ assert.deepEqual(result, bufferObj);
116
+ assert.ok(consoleWarnStub.calledOnce);
117
+ });
118
+ });
119
+
120
+ describe("processResponse", () => {
121
+ it("should pass through null", () => {
122
+ assert.equal(processResponse(null), null);
123
+ });
124
+
125
+ it("should pass through undefined", () => {
126
+ assert.equal(processResponse(undefined), undefined);
127
+ });
128
+
129
+ it("should pass through primitives", () => {
130
+ assert.equal(processResponse("string"), "string");
131
+ assert.equal(processResponse(123), 123);
132
+ assert.equal(processResponse(true), true);
133
+ });
134
+
135
+ it("should pass through plain objects", () => {
136
+ const obj = { key: "value", nested: { num: 42 } };
137
+ assert.deepEqual(processResponse(obj), obj);
138
+ });
139
+
140
+ it("should pass through plain arrays", () => {
141
+ const arr = [1, "two", { three: 3 }];
142
+ assert.deepEqual(processResponse(arr), arr);
143
+ });
144
+
145
+ it("should decompress Buffer at root level", () => {
146
+ const originalData = { decompressed: true };
147
+ const buffer = createGzippedBuffer(originalData);
148
+
149
+ const result = processResponse(buffer);
150
+ assert.deepEqual(result, originalData);
151
+ });
152
+
153
+ it("should decompress nested Buffer fields", () => {
154
+ const statusData = { commands: { power: true } };
155
+ const response = {
156
+ plain: "data",
157
+ status: createGzippedBuffer(statusData),
158
+ };
159
+
160
+ const result = processResponse(response);
161
+ assert.equal(result.plain, "data");
162
+ assert.deepEqual(result.status, statusData);
163
+ });
164
+
165
+ it("should recursively decompress deeply nested Buffers", () => {
166
+ const innerData = { value: 42 };
167
+ const middleData = { inner: createGzippedBuffer(innerData) };
168
+ const response = {
169
+ outer: createGzippedBuffer(middleData),
170
+ };
171
+
172
+ const result = processResponse(response);
173
+ assert.deepEqual(result, { outer: { inner: { value: 42 } } });
174
+ });
175
+
176
+ it("should handle arrays containing Buffers", () => {
177
+ const itemData = { id: 1 };
178
+ const response = {
179
+ items: [createGzippedBuffer(itemData), { id: 2 }],
180
+ };
181
+
182
+ const result = processResponse(response);
183
+ assert.deepEqual(result.items, [{ id: 1 }, { id: 2 }]);
184
+ });
185
+
186
+ it("should handle mixed compressed and uncompressed fields", () => {
187
+ const compressedStatus = { commands: { power: true } };
188
+ const response = {
189
+ status: createGzippedBuffer(compressedStatus),
190
+ nvm: { user_parameters: { temperature: 22 } },
191
+ plain_field: "unchanged",
192
+ };
193
+
194
+ const result = processResponse(response);
195
+ assert.deepEqual(result.status, compressedStatus);
196
+ assert.deepEqual(result.nvm, { user_parameters: { temperature: 22 } });
197
+ assert.equal(result.plain_field, "unchanged");
198
+ });
199
+
200
+ it("should handle real-world DeviceInfo structure with compressed status", () => {
201
+ const statusData = {
202
+ commands: { power: true },
203
+ temperatures: { board: 25, enviroment: 20 },
204
+ };
205
+ const nvmData = {
206
+ user_parameters: {
207
+ enviroment_1_temperature: 22,
208
+ enviroment_2_temperature: 0,
209
+ enviroment_3_temperature: 0,
210
+ is_auto: false,
211
+ is_sound_active: true,
212
+ },
213
+ };
214
+
215
+ const response = {
216
+ status: createGzippedBuffer(statusData),
217
+ nvm: createGzippedBuffer(nvmData),
218
+ };
219
+
220
+ const result = processResponse(response);
221
+ assert.deepEqual(result.status, statusData);
222
+ assert.deepEqual(result.nvm, nvmData);
223
+ });
224
+ });
225
+ });
@@ -0,0 +1,83 @@
1
+ import pako from "pako";
2
+
3
+ import { BufferEncodedType } from "./types";
4
+
5
+ /**
6
+ * Type guard to check if a value is a serialized Node.js Buffer.
7
+ * Node.js Buffers serialize to JSON as: {type: "Buffer", data: [...]}
8
+ *
9
+ * @param value - The value to check
10
+ * @returns True if the value is a Buffer-encoded object
11
+ */
12
+ const isBuffer = (value: unknown): value is BufferEncodedType => {
13
+ return (
14
+ typeof value === "object" &&
15
+ value !== null &&
16
+ "type" in value &&
17
+ (value as Record<string, unknown>).type === "Buffer" &&
18
+ "data" in value &&
19
+ Array.isArray((value as Record<string, unknown>).data)
20
+ );
21
+ };
22
+
23
+ /**
24
+ * Decompresses a Buffer-encoded gzip object and parses the resulting JSON.
25
+ *
26
+ * @param bufferObj - A serialized Buffer object containing gzip data
27
+ * @returns The decompressed and parsed JSON data, or the original object on failure
28
+ */
29
+ const decompressBuffer = (bufferObj: BufferEncodedType): unknown => {
30
+ try {
31
+ // Convert data array to Uint8Array for pako
32
+ const compressed = new Uint8Array(bufferObj.data);
33
+
34
+ // Decompress with gzip
35
+ const decompressed = pako.ungzip(compressed, { to: "string" });
36
+
37
+ // Parse JSON
38
+ return JSON.parse(decompressed);
39
+ } catch (error) {
40
+ // Log warning but return original to maintain backward compatibility
41
+ console.warn("Failed to decompress buffer:", error);
42
+ return bufferObj;
43
+ }
44
+ };
45
+
46
+ /**
47
+ * Recursively processes an API response to decompress any Buffer-encoded fields.
48
+ * Handles nested objects and arrays, preserving structure while decompressing.
49
+ *
50
+ * @param data - The API response data to process
51
+ * @returns The processed data with all Buffer fields decompressed
52
+ */
53
+ const processResponse = <T>(data: T): T => {
54
+ if (data === null || data === undefined) {
55
+ return data;
56
+ }
57
+
58
+ // Check if this is a Buffer object
59
+ if (isBuffer(data)) {
60
+ const decompressed = decompressBuffer(data);
61
+ // Recursively process the decompressed result (may contain nested buffers)
62
+ return processResponse(decompressed) as T;
63
+ }
64
+
65
+ // Recursively process arrays
66
+ if (Array.isArray(data)) {
67
+ return data.map((item) => processResponse(item)) as T;
68
+ }
69
+
70
+ // Recursively process objects
71
+ if (typeof data === "object") {
72
+ const processed: Record<string, unknown> = {};
73
+ for (const [key, value] of Object.entries(data)) {
74
+ processed[key] = processResponse(value);
75
+ }
76
+ return processed as T;
77
+ }
78
+
79
+ // Primitive value, return as-is
80
+ return data;
81
+ };
82
+
83
+ export { decompressBuffer, isBuffer, processResponse };
package/src/cli.ts CHANGED
@@ -3,6 +3,7 @@ import { Command } from "commander";
3
3
  import readline from "readline";
4
4
 
5
5
  import { version } from "../package.json";
6
+ import { NEW_API_URL, OLD_API_URL } from "./constants";
6
7
  import { configure, signIn } from "./library";
7
8
 
8
9
  const promptPassword = (): Promise<string> => {
@@ -45,6 +46,14 @@ const addAuthOptions = (command: Command): Command =>
45
46
  const addMacOption = (command: Command): Command =>
46
47
  command.requiredOption("-m, --mac <macAddress>", "MAC address of the device");
47
48
 
49
+ /**
50
+ * Adds legacy API option to a command.
51
+ * @param command The command to which the legacy option should be added.
52
+ * @returns The command with the legacy option added.
53
+ */
54
+ const addLegacyOption = (command: Command): Command =>
55
+ command.option("--legacy", "Use legacy API endpoint (old AWS Gateway)");
56
+
48
57
  /**
49
58
  * Handles common authentication and API initialization logic.
50
59
  * @param options The options passed from the CLI command.
@@ -54,16 +63,18 @@ const initializeCommand = async (options: {
54
63
  username: string;
55
64
  password?: string;
56
65
  mac: string;
66
+ legacy?: boolean;
57
67
  }): Promise<{
58
68
  normalizedMac: string;
59
69
  jwtToken: string;
60
70
  api: ReturnType<typeof configure>;
61
71
  }> => {
62
- const { username, password, mac } = options;
72
+ const { username, password, mac, legacy = false } = options;
63
73
  const normalizedMac = mac.replace(/:/g, "");
64
74
  const pwd = password || (await promptPassword());
65
- const jwtToken = await signIn(username, pwd);
66
- const api = configure();
75
+ const jwtToken = await signIn(username, pwd, legacy);
76
+ const apiUrl = legacy ? OLD_API_URL : NEW_API_URL;
77
+ const api = configure(apiUrl);
67
78
  return { normalizedMac, jwtToken, api };
68
79
  };
69
80
 
@@ -73,7 +84,12 @@ const initializeCommand = async (options: {
73
84
  * @param getter A function to call on the configured API object.
74
85
  */
75
86
  const executeGetter = async (
76
- options: { username: string; password?: string; mac: string },
87
+ options: {
88
+ username: string;
89
+ password?: string;
90
+ mac: string;
91
+ legacy?: boolean;
92
+ },
77
93
  getter: (
78
94
  api: ReturnType<typeof configure>,
79
95
  jwtToken: string,
@@ -91,7 +107,13 @@ const executeGetter = async (
91
107
  * @param setter A function to call on the configured API object.
92
108
  */
93
109
  const executeSetter = async (
94
- options: { username: string; password?: string; mac: string; value: number },
110
+ options: {
111
+ username: string;
112
+ password?: string;
113
+ mac: string;
114
+ value: number;
115
+ legacy?: boolean;
116
+ },
95
117
  setter: (
96
118
  api: ReturnType<typeof configure>,
97
119
  jwtToken: string,
@@ -158,8 +180,10 @@ const createProgram = (): Command => {
158
180
  ) => api.getTargetTemperature(jwtToken, mac),
159
181
  },
160
182
  ].forEach(({ commandName, description, getter }) => {
161
- addMacOption(
162
- addAuthOptions(program.command(commandName).description(description))
183
+ addLegacyOption(
184
+ addMacOption(
185
+ addAuthOptions(program.command(commandName).description(description))
186
+ )
163
187
  ).action((options) => executeGetter(options, getter));
164
188
  });
165
189
  // Generic setter commands
@@ -185,13 +209,75 @@ const createProgram = (): Command => {
185
209
  ) => api.setTargetTemperature(jwtToken, mac, value),
186
210
  },
187
211
  ].forEach(({ commandName, description, setter }) => {
188
- addMacOption(
189
- addAuthOptions(
190
- program.command(commandName).description(description)
191
- ).requiredOption("-v, --value <number>", "Value to set", parseFloat)
212
+ addLegacyOption(
213
+ addMacOption(
214
+ addAuthOptions(
215
+ program.command(commandName).description(description)
216
+ ).requiredOption("-v, --value <number>", "Value to set", parseFloat)
217
+ )
192
218
  ).action((options) => executeSetter(options, setter));
193
219
  });
194
220
 
221
+ // Command: register
222
+ addLegacyOption(
223
+ addAuthOptions(
224
+ program
225
+ .command("register")
226
+ .description("Register a device with your account")
227
+ )
228
+ )
229
+ .requiredOption("-m, --mac <macAddress>", "MAC address of the device")
230
+ .requiredOption("-s, --serial <serialNumber>", "Device serial number")
231
+ .requiredOption("-n, --name <deviceName>", "Device name")
232
+ .requiredOption("-r, --room <deviceRoom>", "Room name")
233
+ .action(async (options) => {
234
+ const {
235
+ username,
236
+ password,
237
+ mac,
238
+ serial,
239
+ name,
240
+ room,
241
+ legacy = false,
242
+ } = options;
243
+ const normalizedMac = mac.replace(/:/g, "");
244
+ const pwd = password || (await promptPassword());
245
+ const jwtToken = await signIn(username, pwd, legacy);
246
+ const apiUrl = legacy ? OLD_API_URL : NEW_API_URL;
247
+ const api = configure(apiUrl);
248
+ const result = await api.registerDevice(
249
+ jwtToken,
250
+ normalizedMac,
251
+ serial,
252
+ name,
253
+ room
254
+ );
255
+ console.log("Device registered successfully:");
256
+ console.log(JSON.stringify(result, null, 2));
257
+ });
258
+
259
+ // Command: editDevice
260
+ addLegacyOption(
261
+ addMacOption(
262
+ addAuthOptions(
263
+ program.command("editDevice").description("Update device name and room")
264
+ )
265
+ )
266
+ )
267
+ .requiredOption("-n, --name <deviceName>", "Device name")
268
+ .requiredOption("-r, --room <deviceRoom>", "Room name")
269
+ .action(async (options) => {
270
+ const { username, password, mac, name, room, legacy = false } = options;
271
+ const normalizedMac = mac.replace(/:/g, "");
272
+ const pwd = password || (await promptPassword());
273
+ const jwtToken = await signIn(username, pwd, legacy);
274
+ const apiUrl = legacy ? OLD_API_URL : NEW_API_URL;
275
+ const api = configure(apiUrl);
276
+ const result = await api.editDevice(jwtToken, normalizedMac, name, room);
277
+ console.log("Device updated successfully:");
278
+ console.log(JSON.stringify(result, null, 2));
279
+ });
280
+
195
281
  return program;
196
282
  };
197
283
 
package/src/constants.ts CHANGED
@@ -1,3 +1,6 @@
1
- const API_URL = "https://the-mind-api.edilkamin.com/";
1
+ const OLD_API_URL =
2
+ "https://fxtj7xkgc6.execute-api.eu-central-1.amazonaws.com/prod/";
3
+ const NEW_API_URL = "https://the-mind-api.edilkamin.com/";
4
+ const API_URL = NEW_API_URL;
2
5
 
3
- export { API_URL };
6
+ export { API_URL, NEW_API_URL, OLD_API_URL };
package/src/index.ts CHANGED
@@ -1,10 +1,21 @@
1
1
  import { configure } from "./library";
2
2
 
3
- export { API_URL } from "./constants";
3
+ export { decompressBuffer, isBuffer, processResponse } from "./buffer-utils";
4
+ export { API_URL, NEW_API_URL, OLD_API_URL } from "./constants";
4
5
  export { configure, signIn } from "./library";
5
6
  export {
7
+ serialNumberDisplay,
8
+ serialNumberFromHex,
9
+ serialNumberToHex,
10
+ } from "./serial-utils";
11
+ export {
12
+ BufferEncodedType,
6
13
  CommandsType,
14
+ DeviceAssociationBody,
15
+ DeviceAssociationResponse,
16
+ DeviceInfoRawType,
7
17
  DeviceInfoType,
18
+ EditDeviceAssociationBody,
8
19
  StatusType,
9
20
  TemperaturesType,
10
21
  UserParametersType,
@@ -12,6 +23,8 @@ export {
12
23
 
13
24
  export const {
14
25
  deviceInfo,
26
+ registerDevice,
27
+ editDevice,
15
28
  setPower,
16
29
  setPowerOff,
17
30
  setPowerOn,