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
|
@@ -4,6 +4,10 @@ on:
|
|
|
4
4
|
push:
|
|
5
5
|
pull_request:
|
|
6
6
|
|
|
7
|
+
permissions:
|
|
8
|
+
id-token: write
|
|
9
|
+
contents: read
|
|
10
|
+
|
|
7
11
|
jobs:
|
|
8
12
|
build:
|
|
9
13
|
runs-on: ubuntu-latest
|
|
@@ -12,12 +16,10 @@ jobs:
|
|
|
12
16
|
- uses: actions/checkout@v6
|
|
13
17
|
- uses: actions/setup-node@v6
|
|
14
18
|
with:
|
|
15
|
-
node-version: "
|
|
19
|
+
node-version: "24.x"
|
|
16
20
|
registry-url: "https://registry.npmjs.org"
|
|
17
21
|
- run: yarn install
|
|
18
22
|
- run: yarn build
|
|
19
23
|
- run: npm publish --dry-run
|
|
20
24
|
- run: npm publish
|
|
21
25
|
if: startsWith(github.ref, 'refs/tags/')
|
|
22
|
-
env:
|
|
23
|
-
NODE_AUTH_TOKEN: ${{ secrets.NODE_AUTH_TOKEN }}
|
package/README.md
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
# Edilkamin.js
|
|
2
2
|
|
|
3
|
-
[](https://github.com/AndreMiras/edilkamin.js/actions/workflows/tests.yml)
|
|
4
4
|
[](https://github.com/AndreMiras/edilkamin.js/actions/workflows/cli-tests.yml)
|
|
5
5
|
[](https://app.codecov.io/gh/AndreMiras/edilkamin.js/tree/main)
|
|
6
|
-
[](https://github.com/AndreMiras/edilkamin.js/actions/workflows/documentation.yml)
|
|
7
7
|
[](https://badge.fury.io/js/edilkamin)
|
|
8
8
|
|
|
9
9
|
This is a library for the [Reverse Engineered](docs/ReverseEngineering.md) "The Mind" Edilkamin API.
|
|
@@ -31,11 +31,26 @@ Basic usage:
|
|
|
31
31
|
import { signIn, deviceInfo, setPowerOff } from "edilkamin";
|
|
32
32
|
|
|
33
33
|
const macAddress = "aabbccddeeff";
|
|
34
|
-
const token = signIn(username, password);
|
|
35
|
-
deviceInfo(token, macAddress)
|
|
36
|
-
setPowerOff(token, macAddress)
|
|
34
|
+
const token = await signIn(username, password);
|
|
35
|
+
console.log(await deviceInfo(token, macAddress));
|
|
36
|
+
console.log(await setPowerOff(token, macAddress));
|
|
37
37
|
```
|
|
38
38
|
|
|
39
|
+
For long-running applications, use `getSession()` to automatically refresh tokens:
|
|
40
|
+
|
|
41
|
+
```js
|
|
42
|
+
import { signIn, getSession, deviceInfo } from "edilkamin";
|
|
43
|
+
|
|
44
|
+
// Authenticate once
|
|
45
|
+
await signIn(username, password);
|
|
46
|
+
|
|
47
|
+
// Get current session (auto-refreshes if expired)
|
|
48
|
+
const token = await getSession();
|
|
49
|
+
console.log(await deviceInfo(token, macAddress));
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Sessions persist for ~30 days without re-authentication. Call `getSession()` to retrieve fresh tokens as needed.
|
|
53
|
+
|
|
39
54
|
It's also possible to change the default backend URL:
|
|
40
55
|
|
|
41
56
|
```js
|
|
@@ -43,8 +58,8 @@ import { signIn, configure } from "edilkamin";
|
|
|
43
58
|
|
|
44
59
|
const baseUrl = "https://my-proxy.com/";
|
|
45
60
|
const { deviceInfo, setPower } = configure(baseUrl);
|
|
46
|
-
deviceInfo(token, macAddress)
|
|
47
|
-
setPower(token, macAddress, 0)
|
|
61
|
+
console.log(await deviceInfo(token, macAddress));
|
|
62
|
+
console.log(await setPower(token, macAddress, 0));
|
|
48
63
|
```
|
|
49
64
|
|
|
50
65
|
## CLI
|
|
@@ -52,7 +67,14 @@ setPower(token, macAddress, 0).then(console.log);
|
|
|
52
67
|
The library includes a CLI tool that is useful for debugging.
|
|
53
68
|
|
|
54
69
|
```sh
|
|
70
|
+
# First time: provide credentials to authenticate
|
|
55
71
|
yarn cli deviceInfo --mac $MAC --username $USERNAME --password $PASSWORD
|
|
72
|
+
|
|
73
|
+
# Subsequent calls: session persists, credentials optional
|
|
74
|
+
yarn cli deviceInfo --mac $MAC
|
|
75
|
+
|
|
76
|
+
# Clear stored session
|
|
77
|
+
yarn cli logout
|
|
56
78
|
```
|
|
57
79
|
|
|
58
80
|
Or with `npx` once the library is installed:
|
|
@@ -61,6 +83,38 @@ Or with `npx` once the library is installed:
|
|
|
61
83
|
npx edilkamin deviceInfo --mac $MAC --username $USERNAME --password $PASSWORD
|
|
62
84
|
```
|
|
63
85
|
|
|
86
|
+
The CLI stores session data in `~/.edilkamin/session.json` and automatically refreshes tokens when needed. Sessions remain valid for ~30 days.
|
|
87
|
+
|
|
88
|
+
## API Versions
|
|
89
|
+
|
|
90
|
+
This library supports both the new and legacy Edilkamin API endpoints.
|
|
91
|
+
|
|
92
|
+
### CLI Usage
|
|
93
|
+
|
|
94
|
+
```sh
|
|
95
|
+
# New API (default)
|
|
96
|
+
yarn cli deviceInfo --mac $MAC --username $USERNAME --password $PASSWORD
|
|
97
|
+
|
|
98
|
+
# Legacy API
|
|
99
|
+
yarn cli deviceInfo --mac $MAC --username $USERNAME --password $PASSWORD --legacy
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
### Library Usage
|
|
103
|
+
|
|
104
|
+
```js
|
|
105
|
+
import { configure, signIn, OLD_API_URL, NEW_API_URL } from "edilkamin";
|
|
106
|
+
|
|
107
|
+
// New API (default)
|
|
108
|
+
const token = await signIn(username, password);
|
|
109
|
+
const api = configure();
|
|
110
|
+
|
|
111
|
+
// Legacy API
|
|
112
|
+
const legacyToken = await signIn(username, password, true);
|
|
113
|
+
const legacyApi = configure(OLD_API_URL);
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
> **Note**: The legacy API uses AWS API Gateway and may be deprecated in the future.
|
|
117
|
+
|
|
64
118
|
## Motivations
|
|
65
119
|
|
|
66
120
|
- providing an open source web alternative
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { BufferEncodedType } from "./types";
|
|
2
|
+
/**
|
|
3
|
+
* Type guard to check if a value is a serialized Node.js Buffer.
|
|
4
|
+
* Node.js Buffers serialize to JSON as: {type: "Buffer", data: [...]}
|
|
5
|
+
*
|
|
6
|
+
* @param value - The value to check
|
|
7
|
+
* @returns True if the value is a Buffer-encoded object
|
|
8
|
+
*/
|
|
9
|
+
declare const isBuffer: (value: unknown) => value is BufferEncodedType;
|
|
10
|
+
/**
|
|
11
|
+
* Decompresses a Buffer-encoded gzip object and parses the resulting JSON.
|
|
12
|
+
*
|
|
13
|
+
* @param bufferObj - A serialized Buffer object containing gzip data
|
|
14
|
+
* @returns The decompressed and parsed JSON data, or the original object on failure
|
|
15
|
+
*/
|
|
16
|
+
declare const decompressBuffer: (bufferObj: BufferEncodedType) => unknown;
|
|
17
|
+
/**
|
|
18
|
+
* Recursively processes an API response to decompress any Buffer-encoded fields.
|
|
19
|
+
* Handles nested objects and arrays, preserving structure while decompressing.
|
|
20
|
+
*
|
|
21
|
+
* @param data - The API response data to process
|
|
22
|
+
* @returns The processed data with all Buffer fields decompressed
|
|
23
|
+
*/
|
|
24
|
+
declare const processResponse: <T>(data: T) => T;
|
|
25
|
+
export { decompressBuffer, isBuffer, processResponse };
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import pako from "pako";
|
|
2
|
+
/**
|
|
3
|
+
* Type guard to check if a value is a serialized Node.js Buffer.
|
|
4
|
+
* Node.js Buffers serialize to JSON as: {type: "Buffer", data: [...]}
|
|
5
|
+
*
|
|
6
|
+
* @param value - The value to check
|
|
7
|
+
* @returns True if the value is a Buffer-encoded object
|
|
8
|
+
*/
|
|
9
|
+
const isBuffer = (value) => {
|
|
10
|
+
return (typeof value === "object" &&
|
|
11
|
+
value !== null &&
|
|
12
|
+
"type" in value &&
|
|
13
|
+
value.type === "Buffer" &&
|
|
14
|
+
"data" in value &&
|
|
15
|
+
Array.isArray(value.data));
|
|
16
|
+
};
|
|
17
|
+
/**
|
|
18
|
+
* Decompresses a Buffer-encoded gzip object and parses the resulting JSON.
|
|
19
|
+
*
|
|
20
|
+
* @param bufferObj - A serialized Buffer object containing gzip data
|
|
21
|
+
* @returns The decompressed and parsed JSON data, or the original object on failure
|
|
22
|
+
*/
|
|
23
|
+
const decompressBuffer = (bufferObj) => {
|
|
24
|
+
try {
|
|
25
|
+
// Convert data array to Uint8Array for pako
|
|
26
|
+
const compressed = new Uint8Array(bufferObj.data);
|
|
27
|
+
// Decompress with gzip
|
|
28
|
+
const decompressed = pako.ungzip(compressed, { to: "string" });
|
|
29
|
+
// Parse JSON
|
|
30
|
+
return JSON.parse(decompressed);
|
|
31
|
+
}
|
|
32
|
+
catch (error) {
|
|
33
|
+
// Log warning but return original to maintain backward compatibility
|
|
34
|
+
console.warn("Failed to decompress buffer:", error);
|
|
35
|
+
return bufferObj;
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
/**
|
|
39
|
+
* Recursively processes an API response to decompress any Buffer-encoded fields.
|
|
40
|
+
* Handles nested objects and arrays, preserving structure while decompressing.
|
|
41
|
+
*
|
|
42
|
+
* @param data - The API response data to process
|
|
43
|
+
* @returns The processed data with all Buffer fields decompressed
|
|
44
|
+
*/
|
|
45
|
+
const processResponse = (data) => {
|
|
46
|
+
if (data === null || data === undefined) {
|
|
47
|
+
return data;
|
|
48
|
+
}
|
|
49
|
+
// Check if this is a Buffer object
|
|
50
|
+
if (isBuffer(data)) {
|
|
51
|
+
const decompressed = decompressBuffer(data);
|
|
52
|
+
// Recursively process the decompressed result (may contain nested buffers)
|
|
53
|
+
return processResponse(decompressed);
|
|
54
|
+
}
|
|
55
|
+
// Recursively process arrays
|
|
56
|
+
if (Array.isArray(data)) {
|
|
57
|
+
return data.map((item) => processResponse(item));
|
|
58
|
+
}
|
|
59
|
+
// Recursively process objects
|
|
60
|
+
if (typeof data === "object") {
|
|
61
|
+
const processed = {};
|
|
62
|
+
for (const [key, value] of Object.entries(data)) {
|
|
63
|
+
processed[key] = processResponse(value);
|
|
64
|
+
}
|
|
65
|
+
return processed;
|
|
66
|
+
}
|
|
67
|
+
// Primitive value, return as-is
|
|
68
|
+
return data;
|
|
69
|
+
};
|
|
70
|
+
export { decompressBuffer, isBuffer, processResponse };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
import { strict as assert } from "assert";
|
|
2
|
+
import pako from "pako";
|
|
3
|
+
import sinon from "sinon";
|
|
4
|
+
import { decompressBuffer, isBuffer, processResponse } from "./buffer-utils";
|
|
5
|
+
/**
|
|
6
|
+
* Helper to create a gzip-compressed Buffer object for testing.
|
|
7
|
+
*/
|
|
8
|
+
const createGzippedBuffer = (data) => {
|
|
9
|
+
const json = JSON.stringify(data);
|
|
10
|
+
const compressed = pako.gzip(json);
|
|
11
|
+
return {
|
|
12
|
+
type: "Buffer",
|
|
13
|
+
data: Array.from(compressed),
|
|
14
|
+
};
|
|
15
|
+
};
|
|
16
|
+
describe("buffer-utils", () => {
|
|
17
|
+
afterEach(() => {
|
|
18
|
+
sinon.restore();
|
|
19
|
+
});
|
|
20
|
+
describe("isBuffer", () => {
|
|
21
|
+
it("should detect valid Buffer objects", () => {
|
|
22
|
+
const buffer = { type: "Buffer", data: [31, 139, 8, 0] };
|
|
23
|
+
assert.ok(isBuffer(buffer));
|
|
24
|
+
});
|
|
25
|
+
it("should detect empty Buffer objects", () => {
|
|
26
|
+
const buffer = { type: "Buffer", data: [] };
|
|
27
|
+
assert.ok(isBuffer(buffer));
|
|
28
|
+
});
|
|
29
|
+
it("should reject non-Buffer objects with wrong type", () => {
|
|
30
|
+
assert.ok(!isBuffer({ type: "NotBuffer", data: [] }));
|
|
31
|
+
});
|
|
32
|
+
it("should reject objects without type field", () => {
|
|
33
|
+
assert.ok(!isBuffer({ data: [1, 2, 3] }));
|
|
34
|
+
});
|
|
35
|
+
it("should reject objects without data field", () => {
|
|
36
|
+
assert.ok(!isBuffer({ type: "Buffer" }));
|
|
37
|
+
});
|
|
38
|
+
it("should reject objects with non-array data", () => {
|
|
39
|
+
assert.ok(!isBuffer({ type: "Buffer", data: "not an array" }));
|
|
40
|
+
});
|
|
41
|
+
it("should reject null", () => {
|
|
42
|
+
assert.ok(!isBuffer(null));
|
|
43
|
+
});
|
|
44
|
+
it("should reject undefined", () => {
|
|
45
|
+
assert.ok(!isBuffer(undefined));
|
|
46
|
+
});
|
|
47
|
+
it("should reject primitives", () => {
|
|
48
|
+
assert.ok(!isBuffer("string"));
|
|
49
|
+
assert.ok(!isBuffer(123));
|
|
50
|
+
assert.ok(!isBuffer(true));
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
describe("decompressBuffer", () => {
|
|
54
|
+
it("should decompress gzipped JSON buffer", () => {
|
|
55
|
+
const originalData = { test: "value", nested: { key: 123 } };
|
|
56
|
+
const bufferObj = createGzippedBuffer(originalData);
|
|
57
|
+
const result = decompressBuffer(bufferObj);
|
|
58
|
+
assert.deepEqual(result, originalData);
|
|
59
|
+
});
|
|
60
|
+
it("should handle gzipped arrays", () => {
|
|
61
|
+
const originalData = [1, 2, 3, "test"];
|
|
62
|
+
const bufferObj = createGzippedBuffer(originalData);
|
|
63
|
+
const result = decompressBuffer(bufferObj);
|
|
64
|
+
assert.deepEqual(result, originalData);
|
|
65
|
+
});
|
|
66
|
+
it("should handle gzipped strings", () => {
|
|
67
|
+
const originalData = "test string";
|
|
68
|
+
const bufferObj = createGzippedBuffer(originalData);
|
|
69
|
+
const result = decompressBuffer(bufferObj);
|
|
70
|
+
assert.equal(result, originalData);
|
|
71
|
+
});
|
|
72
|
+
it("should return original value if decompression fails", () => {
|
|
73
|
+
const consoleWarnStub = sinon.stub(console, "warn");
|
|
74
|
+
const invalidBuffer = { type: "Buffer", data: [1, 2, 3] };
|
|
75
|
+
const result = decompressBuffer(invalidBuffer);
|
|
76
|
+
assert.deepEqual(result, invalidBuffer);
|
|
77
|
+
assert.ok(consoleWarnStub.calledOnce);
|
|
78
|
+
});
|
|
79
|
+
it("should return original value if JSON parsing fails", () => {
|
|
80
|
+
const consoleWarnStub = sinon.stub(console, "warn");
|
|
81
|
+
// Create valid gzip but invalid JSON
|
|
82
|
+
const invalidJson = "not valid json {";
|
|
83
|
+
const compressed = pako.gzip(invalidJson);
|
|
84
|
+
const bufferObj = {
|
|
85
|
+
type: "Buffer",
|
|
86
|
+
data: Array.from(compressed),
|
|
87
|
+
};
|
|
88
|
+
const result = decompressBuffer(bufferObj);
|
|
89
|
+
assert.deepEqual(result, bufferObj);
|
|
90
|
+
assert.ok(consoleWarnStub.calledOnce);
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
describe("processResponse", () => {
|
|
94
|
+
it("should pass through null", () => {
|
|
95
|
+
assert.equal(processResponse(null), null);
|
|
96
|
+
});
|
|
97
|
+
it("should pass through undefined", () => {
|
|
98
|
+
assert.equal(processResponse(undefined), undefined);
|
|
99
|
+
});
|
|
100
|
+
it("should pass through primitives", () => {
|
|
101
|
+
assert.equal(processResponse("string"), "string");
|
|
102
|
+
assert.equal(processResponse(123), 123);
|
|
103
|
+
assert.equal(processResponse(true), true);
|
|
104
|
+
});
|
|
105
|
+
it("should pass through plain objects", () => {
|
|
106
|
+
const obj = { key: "value", nested: { num: 42 } };
|
|
107
|
+
assert.deepEqual(processResponse(obj), obj);
|
|
108
|
+
});
|
|
109
|
+
it("should pass through plain arrays", () => {
|
|
110
|
+
const arr = [1, "two", { three: 3 }];
|
|
111
|
+
assert.deepEqual(processResponse(arr), arr);
|
|
112
|
+
});
|
|
113
|
+
it("should decompress Buffer at root level", () => {
|
|
114
|
+
const originalData = { decompressed: true };
|
|
115
|
+
const buffer = createGzippedBuffer(originalData);
|
|
116
|
+
const result = processResponse(buffer);
|
|
117
|
+
assert.deepEqual(result, originalData);
|
|
118
|
+
});
|
|
119
|
+
it("should decompress nested Buffer fields", () => {
|
|
120
|
+
const statusData = { commands: { power: true } };
|
|
121
|
+
const response = {
|
|
122
|
+
plain: "data",
|
|
123
|
+
status: createGzippedBuffer(statusData),
|
|
124
|
+
};
|
|
125
|
+
const result = processResponse(response);
|
|
126
|
+
assert.equal(result.plain, "data");
|
|
127
|
+
assert.deepEqual(result.status, statusData);
|
|
128
|
+
});
|
|
129
|
+
it("should recursively decompress deeply nested Buffers", () => {
|
|
130
|
+
const innerData = { value: 42 };
|
|
131
|
+
const middleData = { inner: createGzippedBuffer(innerData) };
|
|
132
|
+
const response = {
|
|
133
|
+
outer: createGzippedBuffer(middleData),
|
|
134
|
+
};
|
|
135
|
+
const result = processResponse(response);
|
|
136
|
+
assert.deepEqual(result, { outer: { inner: { value: 42 } } });
|
|
137
|
+
});
|
|
138
|
+
it("should handle arrays containing Buffers", () => {
|
|
139
|
+
const itemData = { id: 1 };
|
|
140
|
+
const response = {
|
|
141
|
+
items: [createGzippedBuffer(itemData), { id: 2 }],
|
|
142
|
+
};
|
|
143
|
+
const result = processResponse(response);
|
|
144
|
+
assert.deepEqual(result.items, [{ id: 1 }, { id: 2 }]);
|
|
145
|
+
});
|
|
146
|
+
it("should handle mixed compressed and uncompressed fields", () => {
|
|
147
|
+
const compressedStatus = { commands: { power: true } };
|
|
148
|
+
const response = {
|
|
149
|
+
status: createGzippedBuffer(compressedStatus),
|
|
150
|
+
nvm: { user_parameters: { temperature: 22 } },
|
|
151
|
+
plain_field: "unchanged",
|
|
152
|
+
};
|
|
153
|
+
const result = processResponse(response);
|
|
154
|
+
assert.deepEqual(result.status, compressedStatus);
|
|
155
|
+
assert.deepEqual(result.nvm, { user_parameters: { temperature: 22 } });
|
|
156
|
+
assert.equal(result.plain_field, "unchanged");
|
|
157
|
+
});
|
|
158
|
+
it("should handle real-world DeviceInfo structure with compressed status", () => {
|
|
159
|
+
const statusData = {
|
|
160
|
+
commands: { power: true },
|
|
161
|
+
temperatures: { board: 25, enviroment: 20 },
|
|
162
|
+
};
|
|
163
|
+
const nvmData = {
|
|
164
|
+
user_parameters: {
|
|
165
|
+
enviroment_1_temperature: 22,
|
|
166
|
+
enviroment_2_temperature: 0,
|
|
167
|
+
enviroment_3_temperature: 0,
|
|
168
|
+
is_auto: false,
|
|
169
|
+
is_sound_active: true,
|
|
170
|
+
},
|
|
171
|
+
};
|
|
172
|
+
const response = {
|
|
173
|
+
status: createGzippedBuffer(statusData),
|
|
174
|
+
nvm: createGzippedBuffer(nvmData),
|
|
175
|
+
};
|
|
176
|
+
const result = processResponse(response);
|
|
177
|
+
assert.deepEqual(result.status, statusData);
|
|
178
|
+
assert.deepEqual(result.nvm, nvmData);
|
|
179
|
+
});
|
|
180
|
+
});
|
|
181
|
+
});
|
package/dist/esm/cli.js
CHANGED
|
@@ -11,7 +11,9 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
|
|
|
11
11
|
import { Command } from "commander";
|
|
12
12
|
import readline from "readline";
|
|
13
13
|
import { version } from "../package.json";
|
|
14
|
-
import {
|
|
14
|
+
import { NEW_API_URL, OLD_API_URL } from "./constants";
|
|
15
|
+
import { configure, configureAmplify, getSession, signIn } from "./library";
|
|
16
|
+
import { clearSession, createFileStorage } from "./token-storage";
|
|
15
17
|
const promptPassword = () => {
|
|
16
18
|
const rl = readline.createInterface({
|
|
17
19
|
input: process.stdin,
|
|
@@ -36,11 +38,12 @@ const promptPassword = () => {
|
|
|
36
38
|
};
|
|
37
39
|
/**
|
|
38
40
|
* Adds common options (username and password) to a command.
|
|
41
|
+
* Username is optional if a session already exists.
|
|
39
42
|
* @param command The command to which options should be added.
|
|
40
43
|
* @returns The command with options added.
|
|
41
44
|
*/
|
|
42
45
|
const addAuthOptions = (command) => command
|
|
43
|
-
.
|
|
46
|
+
.option("-u, --username <username>", "Username (optional if session exists)")
|
|
44
47
|
.option("-p, --password <password>", "Password");
|
|
45
48
|
/**
|
|
46
49
|
* Adds MAC address option to a command.
|
|
@@ -48,17 +51,39 @@ const addAuthOptions = (command) => command
|
|
|
48
51
|
* @returns The command with the MAC address option added.
|
|
49
52
|
*/
|
|
50
53
|
const addMacOption = (command) => command.requiredOption("-m, --mac <macAddress>", "MAC address of the device");
|
|
54
|
+
/**
|
|
55
|
+
* Adds legacy API option to a command.
|
|
56
|
+
* @param command The command to which the legacy option should be added.
|
|
57
|
+
* @returns The command with the legacy option added.
|
|
58
|
+
*/
|
|
59
|
+
const addLegacyOption = (command) => command.option("--legacy", "Use legacy API endpoint (old AWS Gateway)");
|
|
51
60
|
/**
|
|
52
61
|
* Handles common authentication and API initialization logic.
|
|
62
|
+
* Tries to use existing session first, falls back to sign-in if needed.
|
|
53
63
|
* @param options The options passed from the CLI command.
|
|
54
64
|
* @returns An object containing the normalized MAC, JWT token, and configured API instance.
|
|
55
65
|
*/
|
|
56
66
|
const initializeCommand = (options) => __awaiter(void 0, void 0, void 0, function* () {
|
|
57
|
-
const { username, password, mac } = options;
|
|
67
|
+
const { username, password, mac, legacy = false } = options;
|
|
58
68
|
const normalizedMac = mac.replace(/:/g, "");
|
|
59
|
-
|
|
60
|
-
const
|
|
61
|
-
|
|
69
|
+
// Initialize file storage for session persistence
|
|
70
|
+
const storage = createFileStorage();
|
|
71
|
+
configureAmplify(storage);
|
|
72
|
+
let jwtToken;
|
|
73
|
+
try {
|
|
74
|
+
// Try to get existing session first
|
|
75
|
+
jwtToken = yield getSession(false, legacy);
|
|
76
|
+
}
|
|
77
|
+
catch (_a) {
|
|
78
|
+
// No session, need to sign in
|
|
79
|
+
if (!username) {
|
|
80
|
+
throw new Error("No session found. Please provide --username to sign in.");
|
|
81
|
+
}
|
|
82
|
+
const pwd = password || (yield promptPassword());
|
|
83
|
+
jwtToken = yield signIn(username, pwd, legacy);
|
|
84
|
+
}
|
|
85
|
+
const apiUrl = legacy ? OLD_API_URL : NEW_API_URL;
|
|
86
|
+
const api = configure(apiUrl);
|
|
62
87
|
return { normalizedMac, jwtToken, api };
|
|
63
88
|
});
|
|
64
89
|
/**
|
|
@@ -88,12 +113,28 @@ const createProgram = () => {
|
|
|
88
113
|
.description("CLI tool for interacting with the Edilkamin API")
|
|
89
114
|
.version(version);
|
|
90
115
|
// Command: signIn
|
|
91
|
-
|
|
116
|
+
program
|
|
117
|
+
.command("signIn")
|
|
118
|
+
.description("Sign in and retrieve a JWT token")
|
|
119
|
+
.requiredOption("-u, --username <username>", "Username")
|
|
120
|
+
.option("-p, --password <password>", "Password")
|
|
121
|
+
.action((options) => __awaiter(void 0, void 0, void 0, function* () {
|
|
92
122
|
const { username, password } = options;
|
|
123
|
+
// Initialize file storage for session persistence
|
|
124
|
+
const storage = createFileStorage();
|
|
125
|
+
configureAmplify(storage);
|
|
93
126
|
const pwd = password || (yield promptPassword());
|
|
94
127
|
const jwtToken = yield signIn(username, pwd);
|
|
95
128
|
console.log("JWT Token:", jwtToken);
|
|
96
129
|
}));
|
|
130
|
+
// Command: logout
|
|
131
|
+
program
|
|
132
|
+
.command("logout")
|
|
133
|
+
.description("Clear stored session")
|
|
134
|
+
.action(() => __awaiter(void 0, void 0, void 0, function* () {
|
|
135
|
+
yield clearSession();
|
|
136
|
+
console.log("Session cleared successfully");
|
|
137
|
+
}));
|
|
97
138
|
// Generic getter commands
|
|
98
139
|
[
|
|
99
140
|
{
|
|
@@ -117,7 +158,7 @@ const createProgram = () => {
|
|
|
117
158
|
getter: (api, jwtToken, mac) => api.getTargetTemperature(jwtToken, mac),
|
|
118
159
|
},
|
|
119
160
|
].forEach(({ commandName, description, getter }) => {
|
|
120
|
-
addMacOption(addAuthOptions(program.command(commandName).description(description))).action((options) => executeGetter(options, getter));
|
|
161
|
+
addLegacyOption(addMacOption(addAuthOptions(program.command(commandName).description(description)))).action((options) => executeGetter(options, getter));
|
|
121
162
|
});
|
|
122
163
|
// Generic setter commands
|
|
123
164
|
[
|
|
@@ -132,8 +173,68 @@ const createProgram = () => {
|
|
|
132
173
|
setter: (api, jwtToken, mac, value) => api.setTargetTemperature(jwtToken, mac, value),
|
|
133
174
|
},
|
|
134
175
|
].forEach(({ commandName, description, setter }) => {
|
|
135
|
-
addMacOption(addAuthOptions(program.command(commandName).description(description)).requiredOption("-v, --value <number>", "Value to set", parseFloat)).action((options) => executeSetter(options, setter));
|
|
176
|
+
addLegacyOption(addMacOption(addAuthOptions(program.command(commandName).description(description)).requiredOption("-v, --value <number>", "Value to set", parseFloat))).action((options) => executeSetter(options, setter));
|
|
136
177
|
});
|
|
178
|
+
// Command: register
|
|
179
|
+
addLegacyOption(addAuthOptions(program
|
|
180
|
+
.command("register")
|
|
181
|
+
.description("Register a device with your account")))
|
|
182
|
+
.requiredOption("-m, --mac <macAddress>", "MAC address of the device")
|
|
183
|
+
.requiredOption("-s, --serial <serialNumber>", "Device serial number")
|
|
184
|
+
.requiredOption("-n, --name <deviceName>", "Device name")
|
|
185
|
+
.requiredOption("-r, --room <deviceRoom>", "Room name")
|
|
186
|
+
.action((options) => __awaiter(void 0, void 0, void 0, function* () {
|
|
187
|
+
const { username, password, mac, serial, name, room, legacy = false, } = options;
|
|
188
|
+
const normalizedMac = mac.replace(/:/g, "");
|
|
189
|
+
// Initialize file storage for session persistence
|
|
190
|
+
const storage = createFileStorage();
|
|
191
|
+
configureAmplify(storage);
|
|
192
|
+
let jwtToken;
|
|
193
|
+
try {
|
|
194
|
+
jwtToken = yield getSession(false, legacy);
|
|
195
|
+
}
|
|
196
|
+
catch (_a) {
|
|
197
|
+
if (!username) {
|
|
198
|
+
throw new Error("No session found. Please provide --username to sign in.");
|
|
199
|
+
}
|
|
200
|
+
const pwd = password || (yield promptPassword());
|
|
201
|
+
jwtToken = yield signIn(username, pwd, legacy);
|
|
202
|
+
}
|
|
203
|
+
const apiUrl = legacy ? OLD_API_URL : NEW_API_URL;
|
|
204
|
+
const api = configure(apiUrl);
|
|
205
|
+
const result = yield api.registerDevice(jwtToken, normalizedMac, serial, name, room);
|
|
206
|
+
console.log("Device registered successfully:");
|
|
207
|
+
console.log(JSON.stringify(result, null, 2));
|
|
208
|
+
}));
|
|
209
|
+
// Command: editDevice
|
|
210
|
+
addLegacyOption(addMacOption(addAuthOptions(program
|
|
211
|
+
.command("editDevice")
|
|
212
|
+
.description("Update device name and room"))))
|
|
213
|
+
.requiredOption("-n, --name <deviceName>", "Device name")
|
|
214
|
+
.requiredOption("-r, --room <deviceRoom>", "Room name")
|
|
215
|
+
.action((options) => __awaiter(void 0, void 0, void 0, function* () {
|
|
216
|
+
const { username, password, mac, name, room, legacy = false } = options;
|
|
217
|
+
const normalizedMac = mac.replace(/:/g, "");
|
|
218
|
+
// Initialize file storage for session persistence
|
|
219
|
+
const storage = createFileStorage();
|
|
220
|
+
configureAmplify(storage);
|
|
221
|
+
let jwtToken;
|
|
222
|
+
try {
|
|
223
|
+
jwtToken = yield getSession(false, legacy);
|
|
224
|
+
}
|
|
225
|
+
catch (_a) {
|
|
226
|
+
if (!username) {
|
|
227
|
+
throw new Error("No session found. Please provide --username to sign in.");
|
|
228
|
+
}
|
|
229
|
+
const pwd = password || (yield promptPassword());
|
|
230
|
+
jwtToken = yield signIn(username, pwd, legacy);
|
|
231
|
+
}
|
|
232
|
+
const apiUrl = legacy ? OLD_API_URL : NEW_API_URL;
|
|
233
|
+
const api = configure(apiUrl);
|
|
234
|
+
const result = yield api.editDevice(jwtToken, normalizedMac, name, room);
|
|
235
|
+
console.log("Device updated successfully:");
|
|
236
|
+
console.log(JSON.stringify(result, null, 2));
|
|
237
|
+
}));
|
|
137
238
|
return program;
|
|
138
239
|
};
|
|
139
240
|
const main = () => {
|
package/dist/esm/constants.d.ts
CHANGED
|
@@ -1,2 +1,4 @@
|
|
|
1
|
+
declare const OLD_API_URL = "https://fxtj7xkgc6.execute-api.eu-central-1.amazonaws.com/prod/";
|
|
2
|
+
declare const NEW_API_URL = "https://the-mind-api.edilkamin.com/";
|
|
1
3
|
declare const API_URL = "https://the-mind-api.edilkamin.com/";
|
|
2
|
-
export { API_URL };
|
|
4
|
+
export { API_URL, NEW_API_URL, OLD_API_URL };
|
package/dist/esm/constants.js
CHANGED
|
@@ -1,2 +1,4 @@
|
|
|
1
|
-
const
|
|
2
|
-
|
|
1
|
+
const OLD_API_URL = "https://fxtj7xkgc6.execute-api.eu-central-1.amazonaws.com/prod/";
|
|
2
|
+
const NEW_API_URL = "https://the-mind-api.edilkamin.com/";
|
|
3
|
+
const API_URL = NEW_API_URL;
|
|
4
|
+
export { API_URL, NEW_API_URL, OLD_API_URL };
|
package/dist/esm/index.d.ts
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
|
-
export {
|
|
2
|
-
export {
|
|
3
|
-
export {
|
|
4
|
-
export
|
|
1
|
+
export { decompressBuffer, isBuffer, processResponse } from "./buffer-utils";
|
|
2
|
+
export { API_URL, NEW_API_URL, OLD_API_URL } from "./constants";
|
|
3
|
+
export { configure, getSession, signIn } from "./library";
|
|
4
|
+
export { serialNumberDisplay, serialNumberFromHex, serialNumberToHex, } from "./serial-utils";
|
|
5
|
+
export { clearSession } from "./token-storage";
|
|
6
|
+
export { BufferEncodedType, CommandsType, DeviceAssociationBody, DeviceAssociationResponse, DeviceInfoRawType, DeviceInfoType, EditDeviceAssociationBody, StatusType, TemperaturesType, UserParametersType, } from "./types";
|
|
7
|
+
export declare const deviceInfo: (jwtToken: string, macAddress: string) => Promise<import("./types").DeviceInfoType>, registerDevice: (jwtToken: string, macAddress: string, serialNumber: string, deviceName?: string, deviceRoom?: string) => Promise<import("./types").DeviceAssociationResponse>, editDevice: (jwtToken: string, macAddress: string, deviceName?: string, deviceRoom?: string) => Promise<import("./types").DeviceAssociationResponse>, setPower: (jwtToken: string, macAddress: string, value: number) => Promise<import("axios").AxiosResponse<any, any, {}>>, setPowerOff: (jwtToken: string, macAddress: string) => Promise<import("axios").AxiosResponse<any, any, {}>>, setPowerOn: (jwtToken: string, macAddress: string) => Promise<import("axios").AxiosResponse<any, any, {}>>, getPower: (jwtToken: string, macAddress: string) => Promise<boolean>, getEnvironmentTemperature: (jwtToken: string, macAddress: string) => Promise<number>, getTargetTemperature: (jwtToken: string, macAddress: string) => Promise<number>, setTargetTemperature: (jwtToken: string, macAddress: string, temperature: number) => Promise<import("axios").AxiosResponse<any, any, {}>>;
|
package/dist/esm/index.js
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
import { configure } from "./library";
|
|
2
|
-
export {
|
|
3
|
-
export {
|
|
4
|
-
export
|
|
2
|
+
export { decompressBuffer, isBuffer, processResponse } from "./buffer-utils";
|
|
3
|
+
export { API_URL, NEW_API_URL, OLD_API_URL } from "./constants";
|
|
4
|
+
export { configure, getSession, signIn } from "./library";
|
|
5
|
+
export { serialNumberDisplay, serialNumberFromHex, serialNumberToHex, } from "./serial-utils";
|
|
6
|
+
export { clearSession } from "./token-storage";
|
|
7
|
+
export const { deviceInfo, registerDevice, editDevice, setPower, setPowerOff, setPowerOn, getPower, getEnvironmentTemperature, getTargetTemperature, setTargetTemperature, } = configure();
|