edilkamin 1.3.0 → 1.3.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 +17 -0
- package/.github/workflows/documentation.yml +34 -0
- package/.github/workflows/publish.yml +21 -0
- package/.github/workflows/tests.yml +18 -0
- package/dist/{src/library.js → library.js} +3 -0
- package/docs/Release.md +41 -0
- package/docs/ReverseEngineering.md +143 -0
- package/package.json +1 -4
- package/src/cli.ts +80 -0
- package/src/constants.ts +4 -0
- package/src/index.ts +15 -0
- package/src/library.test.ts +77 -0
- package/src/library.ts +77 -0
- package/src/types.ts +36 -0
- package/tsconfig.json +16 -0
- package/dist/package.json +0 -50
- /package/dist/{src/cli.d.ts → cli.d.ts} +0 -0
- /package/dist/{src/cli.js → cli.js} +0 -0
- /package/dist/{src/constants.d.ts → constants.d.ts} +0 -0
- /package/dist/{src/constants.js → constants.js} +0 -0
- /package/dist/{src/index.d.ts → index.d.ts} +0 -0
- /package/dist/{src/index.js → index.js} +0 -0
- /package/dist/{src/library.d.ts → library.d.ts} +0 -0
- /package/dist/{src/library.test.d.ts → library.test.d.ts} +0 -0
- /package/dist/{src/library.test.js → library.test.js} +0 -0
- /package/dist/{src/types.d.ts → types.d.ts} +0 -0
- /package/dist/{src/types.js → types.js} +0 -0
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
name: CLI Tests
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
pull_request:
|
|
6
|
+
|
|
7
|
+
jobs:
|
|
8
|
+
tests:
|
|
9
|
+
runs-on: ubuntu-latest
|
|
10
|
+
timeout-minutes: 5
|
|
11
|
+
steps:
|
|
12
|
+
- uses: actions/checkout@v4
|
|
13
|
+
- uses: actions/setup-node@v4
|
|
14
|
+
with:
|
|
15
|
+
node-version: "20.x"
|
|
16
|
+
- run: yarn install --no-ignore-optional
|
|
17
|
+
- run: yarn cli --help
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
name: Documentation
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches:
|
|
6
|
+
- main
|
|
7
|
+
|
|
8
|
+
jobs:
|
|
9
|
+
deploy:
|
|
10
|
+
runs-on: ubuntu-latest
|
|
11
|
+
steps:
|
|
12
|
+
- uses: actions/checkout@v4
|
|
13
|
+
- uses: actions/setup-node@v4
|
|
14
|
+
with:
|
|
15
|
+
node-version: "20.x"
|
|
16
|
+
- name: git config
|
|
17
|
+
run: |
|
|
18
|
+
git config user.name documentation-deploy-action
|
|
19
|
+
git config user.email documentation-deploy-action@@users.noreply.github.com
|
|
20
|
+
git remote set-url origin https://${{github.actor}}:${{github.token}}@github.com/${{github.repository}}.git
|
|
21
|
+
- run: yarn install
|
|
22
|
+
- run: yarn typedoc src/index.ts --out /tmp/docs
|
|
23
|
+
- name: deploy documentation
|
|
24
|
+
run: |
|
|
25
|
+
git ls-remote --exit-code . origin/gh-pages \
|
|
26
|
+
&& git checkout -b gh-pages \
|
|
27
|
+
|| git checkout --orphan gh-pages
|
|
28
|
+
git reset --hard
|
|
29
|
+
git pull --set-upstream origin gh-pages || echo probably first commit
|
|
30
|
+
cp --recursive /tmp/docs/. .
|
|
31
|
+
echo /node_modules > .gitignore
|
|
32
|
+
git add --all
|
|
33
|
+
git commit --all --message ":memo: docs: Update generated documentation"
|
|
34
|
+
git push origin gh-pages
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
name: Publish
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
tags:
|
|
6
|
+
- "*"
|
|
7
|
+
|
|
8
|
+
jobs:
|
|
9
|
+
build:
|
|
10
|
+
runs-on: ubuntu-latest
|
|
11
|
+
steps:
|
|
12
|
+
- uses: actions/checkout@v4
|
|
13
|
+
- uses: actions/setup-node@v4
|
|
14
|
+
with:
|
|
15
|
+
node-version: "20.x"
|
|
16
|
+
registry-url: "https://registry.npmjs.org"
|
|
17
|
+
- run: yarn install
|
|
18
|
+
- run: yarn build
|
|
19
|
+
- run: npm publish
|
|
20
|
+
env:
|
|
21
|
+
NODE_AUTH_TOKEN: ${{ secrets.NODE_AUTH_TOKEN }}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
name: Tests
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
pull_request:
|
|
6
|
+
|
|
7
|
+
jobs:
|
|
8
|
+
build:
|
|
9
|
+
runs-on: ubuntu-latest
|
|
10
|
+
steps:
|
|
11
|
+
- uses: actions/checkout@v4
|
|
12
|
+
- uses: actions/setup-node@v4
|
|
13
|
+
with:
|
|
14
|
+
node-version: "20.x"
|
|
15
|
+
- run: yarn install
|
|
16
|
+
- run: yarn lint
|
|
17
|
+
- run: yarn build
|
|
18
|
+
- run: npm publish --dry-run
|
|
@@ -62,6 +62,9 @@ const headers = (jwtToken) => ({ Authorization: `Bearer ${jwtToken}` });
|
|
|
62
62
|
* Sign in to return the JWT token.
|
|
63
63
|
*/
|
|
64
64
|
const signIn = (username, password) => __awaiter(void 0, void 0, void 0, function* () {
|
|
65
|
+
// in case the user is already signed in, refs:
|
|
66
|
+
// https://github.com/aws-amplify/amplify-js/issues/13813
|
|
67
|
+
yield amplifyAuth.signOut();
|
|
65
68
|
const { isSignedIn, nextStep } = yield amplifyAuth.signIn({
|
|
66
69
|
username,
|
|
67
70
|
password,
|
package/docs/Release.md
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# How to release
|
|
2
|
+
|
|
3
|
+
This is documenting the release process.
|
|
4
|
+
|
|
5
|
+
We're also using [semantic versioning](https://semver.org/) where `major.minor.patch` should be set accordingly.
|
|
6
|
+
|
|
7
|
+
```sh
|
|
8
|
+
VERSION=major.minor.patch
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Update package.json and tag
|
|
12
|
+
|
|
13
|
+
Update the [package.json](../package.json) `version` to match the new release version.
|
|
14
|
+
|
|
15
|
+
```sh
|
|
16
|
+
sed --regexp-extended 's/"version": "(.+)"/"version": "'$VERSION'"/' --in-place package.json
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Then commit and tag:
|
|
20
|
+
|
|
21
|
+
```sh
|
|
22
|
+
git commit -a -m ":bookmark: $VERSION"
|
|
23
|
+
git tag -a $VERSION -m ":bookmark: $VERSION"
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Push everything including tags:
|
|
27
|
+
|
|
28
|
+
```sh
|
|
29
|
+
git push
|
|
30
|
+
git push --tags
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Publish to npm
|
|
34
|
+
|
|
35
|
+
Publication to npm happens automatically from GitHub Actions on tag push.
|
|
36
|
+
Alternatively it can be done manually via:
|
|
37
|
+
|
|
38
|
+
```sh
|
|
39
|
+
yarn build
|
|
40
|
+
npm publish
|
|
41
|
+
```
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
# Edilkamin Stove Reverse Engineering
|
|
2
|
+
|
|
3
|
+
Edilkamin pellet stoves remote control reverse engineering.
|
|
4
|
+
This is documenting my journey to reverse engineering [The Mind Edilkamin app](https://play.google.com/store/apps/details?id=com.edilkamin.stufe).
|
|
5
|
+
The goal was to be able to control the stove wirelessly without having to use the proprietary app.
|
|
6
|
+
|
|
7
|
+
## APK download & decompiling
|
|
8
|
+
|
|
9
|
+
The steps to decompile an APK are mainly described in this article:
|
|
10
|
+
[Reverse Engineering Sodexo's API](https://medium.com/@andre.miras/reverse-engineering-sodexos-api-d13710b7bf0d)
|
|
11
|
+
|
|
12
|
+
Here we summarize some of them.
|
|
13
|
+
|
|
14
|
+
- APK used: https://play.google.com/store/apps/details?id=com.edilkamin.stufe
|
|
15
|
+
- version: 1.2.3 (19 November 2021)
|
|
16
|
+
|
|
17
|
+
Assuming the APK is already loaded on the Android device, proceed as below to download on the computer.
|
|
18
|
+
Get the APK on device path:
|
|
19
|
+
|
|
20
|
+
```sh
|
|
21
|
+
adb shell pm list packages -f | grep edilkamin
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Output:
|
|
25
|
+
|
|
26
|
+
```
|
|
27
|
+
package:/data/app/com.edilkamin.stufe-Di57pxUTs3wzjQF0dIxeEQ==/base.apk=com.edilkamin.stufe
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Copy to the computer:
|
|
31
|
+
|
|
32
|
+
```sh
|
|
33
|
+
adb shell cp /data/app/com.edilkamin.stufe-Di57pxUTs3wzjQF0dIxeEQ==/base.apk /sdcard/
|
|
34
|
+
adb pull /sdcard/base.apk .
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Decompile (jadx v1.3.3):
|
|
38
|
+
|
|
39
|
+
```sh
|
|
40
|
+
jadx --output-dir base base.apk
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## Looking into `strings.xml`
|
|
44
|
+
|
|
45
|
+
Often an interesting starting point is the `base/resources/res/values/strings.xml` file.
|
|
46
|
+
We find the usual `google_api_key` and `google_app_id`, but no endpoint prefix or anything that
|
|
47
|
+
will be used in the short terms.
|
|
48
|
+
|
|
49
|
+
## Let's `grep` through the source
|
|
50
|
+
|
|
51
|
+
The APK source contains a lot of thirdparty code, but the actual application code is located in:
|
|
52
|
+
`base/sources/com/edilkamin/stufe/`
|
|
53
|
+
|
|
54
|
+
Let's `grep` into it looking for some endpoints:
|
|
55
|
+
|
|
56
|
+
```sh
|
|
57
|
+
cd base/sources/com/edilkamin/stufe/ && grep -irE 'http(s)://' .
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Output extract:
|
|
61
|
+
|
|
62
|
+
```
|
|
63
|
+
./network/EdilkaminApiServiceKt.java: public static final String BASE_URL = "https://s5zsjtooy4.execute-api.eu-central-1.amazonaws.com/test/";
|
|
64
|
+
./network/EdilkaminApiServiceKt.java: public static final String PROD_URL = "https://fxtj7xkgc6.execute-api.eu-central-1.amazonaws.com/prod/";
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Other interesting files found and their extract:
|
|
68
|
+
|
|
69
|
+
- `base/sources/com/edilkamin/stufe/network/ApiService.java`:
|
|
70
|
+
|
|
71
|
+
```java
|
|
72
|
+
@PUT("device/{mac_address}")
|
|
73
|
+
Object editAssociation(@Header("Authorization") String str, @Path("mac_address") String str2, @Body EditDeviceAssociationBody editDeviceAssociationBody, Continuation<Object> continuation);
|
|
74
|
+
|
|
75
|
+
@GET("device/{macAddress}/info")
|
|
76
|
+
Object getFireplaceInfo(@Path("macAddress") String str, Continuation<? super GeneralResponse> continuation);
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
- `base/resources/res/raw/amplifyconfiguration.json`:
|
|
80
|
+
|
|
81
|
+
```json
|
|
82
|
+
"Default": {
|
|
83
|
+
"PoolId": "eu-central-1_BYmQ2VBlo",
|
|
84
|
+
"AppClientId": "7sc1qltkqobo3ddqsk4542dg2h",
|
|
85
|
+
"Region": "eu-central-1"
|
|
86
|
+
}
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
## Poking the /prod/ endpoint around
|
|
90
|
+
|
|
91
|
+
```sh
|
|
92
|
+
curl https://fxtj7xkgc6.execute-api.eu-central-1.amazonaws.com/prod/
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
Output (status code 403):
|
|
96
|
+
|
|
97
|
+
```json
|
|
98
|
+
{ "message": "Missing Authentication Token" }
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
Let's try an unauthenticated endpoint then, remember that file `network/ApiService.java`.
|
|
102
|
+
|
|
103
|
+
```sh
|
|
104
|
+
curl --verbose https://fxtj7xkgc6.execute-api.eu-central-1.amazonaws.com/prod/device/AA:BB:CC:DD:EE:FF/info
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
Output (status code 404):
|
|
108
|
+
|
|
109
|
+
```json
|
|
110
|
+
{}
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
That looks already promising.
|
|
114
|
+
After poking that endpoint around and using the real device MAC address we get a valid response.
|
|
115
|
+
Note how the MAC address is all lower case and no column:
|
|
116
|
+
|
|
117
|
+
```sh
|
|
118
|
+
curl --verbose https://fxtj7xkgc6.execute-api.eu-central-1.amazonaws.com/prod/device/aabbccddeeff/info
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
Output (status code 200):
|
|
122
|
+
|
|
123
|
+
```json
|
|
124
|
+
{"mac_address":"aabbccddeeff","pk":1,"component_info":{"temp_umidity_voc_probe_3":...}}
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
Bingo!
|
|
128
|
+
|
|
129
|
+
## Turn the stove on
|
|
130
|
+
|
|
131
|
+
```sh
|
|
132
|
+
curl --verbose --request PUT --header "Content-Type: application/json" \
|
|
133
|
+
--data '{"mac_address":"aabbccddeeff", "name": "power", "value": 1}' \
|
|
134
|
+
https://fxtj7xkgc6.execute-api.eu-central-1.amazonaws.com/prod/mqtt/command
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
## Note on Security
|
|
138
|
+
|
|
139
|
+
It seems like most endpoints let you read info or control the stove without any authentication.
|
|
140
|
+
All we need is a valid device MAC address.
|
|
141
|
+
Don't leak your MAC address or people can potentially control your stove.
|
|
142
|
+
|
|
143
|
+
October 2022 update: the endpoints got updated to require a JWT token and the stoves are linked to the account.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "edilkamin",
|
|
3
|
-
"version": "1.3.
|
|
3
|
+
"version": "1.3.2",
|
|
4
4
|
"description": "",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -13,9 +13,6 @@
|
|
|
13
13
|
"format": "prettier --write src docs .github *.md",
|
|
14
14
|
"build": "tsc"
|
|
15
15
|
},
|
|
16
|
-
"files": [
|
|
17
|
-
"/dist"
|
|
18
|
-
],
|
|
19
16
|
"repository": {
|
|
20
17
|
"type": "git",
|
|
21
18
|
"url": "git+https://github.com/AndreMiras/edilkamin.js.git"
|
package/src/cli.ts
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { signIn, configure } from "./library";
|
|
3
|
+
import { Command } from "commander";
|
|
4
|
+
import readline from "readline";
|
|
5
|
+
import { version } from "../package.json";
|
|
6
|
+
|
|
7
|
+
const promptPassword = (): Promise<string> => {
|
|
8
|
+
const rl = readline.createInterface({
|
|
9
|
+
input: process.stdin,
|
|
10
|
+
output: process.stdout,
|
|
11
|
+
terminal: true,
|
|
12
|
+
});
|
|
13
|
+
return new Promise((resolve) => {
|
|
14
|
+
rl.question("Enter password: ", (password) => {
|
|
15
|
+
// Hide the password input
|
|
16
|
+
readline.moveCursor(process.stdout, 0, -1);
|
|
17
|
+
readline.clearLine(process.stdout, 0);
|
|
18
|
+
rl.close();
|
|
19
|
+
resolve(password);
|
|
20
|
+
});
|
|
21
|
+
// Disable input echoing for password
|
|
22
|
+
process.stdin.on("data", (char) => {
|
|
23
|
+
if (char.toString("hex") === "0d0a") return; // Enter key
|
|
24
|
+
process.stdout.write("*");
|
|
25
|
+
});
|
|
26
|
+
});
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Adds common options (username and password) to a command.
|
|
31
|
+
* @param command The command to which options should be added.
|
|
32
|
+
* @returns The command with options added.
|
|
33
|
+
*/
|
|
34
|
+
const addCommonOptions = (command: Command): Command =>
|
|
35
|
+
command
|
|
36
|
+
.requiredOption("-u, --username <username>", "Username")
|
|
37
|
+
.option("-p, --password <password>", "Password");
|
|
38
|
+
|
|
39
|
+
const createProgram = (): Command => {
|
|
40
|
+
const program = new Command();
|
|
41
|
+
program
|
|
42
|
+
.name("edilkamin-cli")
|
|
43
|
+
.description("CLI tool for interacting with the Edilkamin API")
|
|
44
|
+
.version(version);
|
|
45
|
+
// Command: signIn
|
|
46
|
+
addCommonOptions(
|
|
47
|
+
program.command("signIn").description("Sign in and retrieve a JWT token")
|
|
48
|
+
).action(async (options) => {
|
|
49
|
+
const { username, password } = options;
|
|
50
|
+
const pwd = password || (await promptPassword());
|
|
51
|
+
const jwtToken = await signIn(username, pwd);
|
|
52
|
+
console.log("JWT Token:", jwtToken);
|
|
53
|
+
});
|
|
54
|
+
// Command: deviceInfo
|
|
55
|
+
addCommonOptions(
|
|
56
|
+
program
|
|
57
|
+
.command("deviceInfo")
|
|
58
|
+
.description("Retrieve device info for a specific MAC address")
|
|
59
|
+
.requiredOption("-m, --mac <macAddress>", "MAC address of the device")
|
|
60
|
+
).action(async (options) => {
|
|
61
|
+
const { username, password, mac } = options;
|
|
62
|
+
const pwd = password || (await promptPassword());
|
|
63
|
+
const jwtToken = await signIn(username, pwd);
|
|
64
|
+
const api = configure(); // Use the default API configuration
|
|
65
|
+
const deviceInfo = await api.deviceInfo(jwtToken, mac);
|
|
66
|
+
console.log("Device Info:", deviceInfo.data);
|
|
67
|
+
});
|
|
68
|
+
return program;
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
const main = (): void => {
|
|
72
|
+
const program = createProgram();
|
|
73
|
+
program.parse(process.argv);
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
if (require.main === module) {
|
|
77
|
+
main();
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export { main };
|
package/src/constants.ts
ADDED
package/src/index.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { configure } from "./library";
|
|
2
|
+
|
|
3
|
+
export { API_URL } from "./constants";
|
|
4
|
+
|
|
5
|
+
export {
|
|
6
|
+
CommandsType,
|
|
7
|
+
DeviceInfoType,
|
|
8
|
+
StatusType,
|
|
9
|
+
TemperaturesType,
|
|
10
|
+
UserParametersType,
|
|
11
|
+
} from "./types";
|
|
12
|
+
|
|
13
|
+
export { signIn, configure } from "./library";
|
|
14
|
+
|
|
15
|
+
export const { deviceInfo, setPower, setPowerOff, setPowerOn } = configure();
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { strict as assert } from "assert";
|
|
2
|
+
import sinon from "sinon";
|
|
3
|
+
import { Amplify } from "aws-amplify";
|
|
4
|
+
import axios from "axios";
|
|
5
|
+
import { signIn, configure } from "../src/library";
|
|
6
|
+
|
|
7
|
+
describe("library", () => {
|
|
8
|
+
let axiosStub: sinon.SinonStub;
|
|
9
|
+
|
|
10
|
+
beforeEach(() => {
|
|
11
|
+
axiosStub = sinon.stub(axios, "create").returns({
|
|
12
|
+
get: sinon.stub(),
|
|
13
|
+
put: sinon.stub(),
|
|
14
|
+
} as any);
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
afterEach(() => {
|
|
18
|
+
sinon.restore();
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
describe("configure", () => {
|
|
22
|
+
it("should create API methods with the correct baseURL", () => {
|
|
23
|
+
const baseURL = "https://example.com/api";
|
|
24
|
+
const api = configure(baseURL);
|
|
25
|
+
assert.ok(axiosStub.calledOnce);
|
|
26
|
+
assert.deepEqual(axiosStub.firstCall.args[0], { baseURL });
|
|
27
|
+
assert.deepEqual(Object.keys(api), [
|
|
28
|
+
"deviceInfo",
|
|
29
|
+
"setPower",
|
|
30
|
+
"setPowerOff",
|
|
31
|
+
"setPowerOn",
|
|
32
|
+
]);
|
|
33
|
+
});
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
describe("API Methods", () => {
|
|
37
|
+
it("should call axios for deviceInfo", async () => {
|
|
38
|
+
const mockAxios = {
|
|
39
|
+
get: sinon
|
|
40
|
+
.stub()
|
|
41
|
+
.resolves({ data: { id: "123", name: "Mock Device" } }),
|
|
42
|
+
};
|
|
43
|
+
axiosStub.returns(mockAxios as any);
|
|
44
|
+
const api = configure("https://example.com/api");
|
|
45
|
+
const result = await api.deviceInfo("mockToken", "mockMacAddress");
|
|
46
|
+
assert.ok(mockAxios.get.calledOnce);
|
|
47
|
+
assert.equal(
|
|
48
|
+
mockAxios.get.firstCall.args[0],
|
|
49
|
+
"device/mockMacAddress/info"
|
|
50
|
+
);
|
|
51
|
+
assert.deepEqual(mockAxios.get.firstCall.args[1], {
|
|
52
|
+
headers: { Authorization: "Bearer mockToken" },
|
|
53
|
+
});
|
|
54
|
+
assert.deepEqual(result.data, { id: "123", name: "Mock Device" });
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it("should call axios for setPowerOn", async () => {
|
|
58
|
+
const mockAxios = {
|
|
59
|
+
put: sinon.stub().resolves({ status: 200 }),
|
|
60
|
+
};
|
|
61
|
+
axiosStub.returns(mockAxios as any);
|
|
62
|
+
const api = configure("https://example.com/api");
|
|
63
|
+
const result = await api.setPowerOn("mockToken", "mockMacAddress");
|
|
64
|
+
assert.ok(mockAxios.put.calledOnce);
|
|
65
|
+
assert.equal(mockAxios.put.firstCall.args[0], "mqtt/command");
|
|
66
|
+
assert.deepEqual(mockAxios.put.firstCall.args[1], {
|
|
67
|
+
mac_address: "mockMacAddress",
|
|
68
|
+
name: "power",
|
|
69
|
+
value: 1,
|
|
70
|
+
});
|
|
71
|
+
assert.deepEqual(mockAxios.put.firstCall.args[2], {
|
|
72
|
+
headers: { Authorization: "Bearer mockToken" },
|
|
73
|
+
});
|
|
74
|
+
assert.equal(result.status, 200);
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
});
|
package/src/library.ts
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { strict as assert } from "assert";
|
|
2
|
+
import { Amplify } from "aws-amplify";
|
|
3
|
+
import * as amplifyAuth from "aws-amplify/auth";
|
|
4
|
+
import axios, { AxiosInstance } from "axios";
|
|
5
|
+
import { DeviceInfoType } from "./types";
|
|
6
|
+
import { API_URL } from "./constants";
|
|
7
|
+
|
|
8
|
+
const amplifyconfiguration = {
|
|
9
|
+
aws_project_region: "eu-central-1",
|
|
10
|
+
aws_user_pools_id: "eu-central-1_BYmQ2VBlo",
|
|
11
|
+
aws_user_pools_web_client_id: "7sc1qltkqobo3ddqsk4542dg2h",
|
|
12
|
+
};
|
|
13
|
+
Amplify.configure(amplifyconfiguration);
|
|
14
|
+
|
|
15
|
+
const headers = (jwtToken: string) => ({ Authorization: `Bearer ${jwtToken}` });
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Sign in to return the JWT token.
|
|
19
|
+
*/
|
|
20
|
+
const signIn = async (username: string, password: string): Promise<string> => {
|
|
21
|
+
// in case the user is already signed in, refs:
|
|
22
|
+
// https://github.com/aws-amplify/amplify-js/issues/13813
|
|
23
|
+
await amplifyAuth.signOut();
|
|
24
|
+
const { isSignedIn, nextStep } = await amplifyAuth.signIn({
|
|
25
|
+
username,
|
|
26
|
+
password,
|
|
27
|
+
});
|
|
28
|
+
assert.ok(isSignedIn);
|
|
29
|
+
const { tokens } = await amplifyAuth.fetchAuthSession();
|
|
30
|
+
assert.ok(tokens);
|
|
31
|
+
return tokens.accessToken.toString();
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
const deviceInfo =
|
|
35
|
+
(axiosInstance: AxiosInstance) => (jwtToken: string, macAddress: string) =>
|
|
36
|
+
axiosInstance.get<DeviceInfoType>(`device/${macAddress}/info`, {
|
|
37
|
+
headers: headers(jwtToken),
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
const mqttCommand =
|
|
41
|
+
(axiosInstance: AxiosInstance) =>
|
|
42
|
+
(jwtToken: string, macAddress: string, payload: any) =>
|
|
43
|
+
axiosInstance.put(
|
|
44
|
+
"mqtt/command",
|
|
45
|
+
{ mac_address: macAddress, ...payload },
|
|
46
|
+
{ headers: headers(jwtToken) }
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
const setPower =
|
|
50
|
+
(axiosInstance: AxiosInstance) =>
|
|
51
|
+
(jwtToken: string, macAddress: string, value: number) =>
|
|
52
|
+
mqttCommand(axiosInstance)(jwtToken, macAddress, { name: "power", value });
|
|
53
|
+
|
|
54
|
+
const setPowerOn =
|
|
55
|
+
(axiosInstance: AxiosInstance) => (jwtToken: string, macAddress: string) =>
|
|
56
|
+
setPower(axiosInstance)(jwtToken, macAddress, 1);
|
|
57
|
+
const setPowerOff =
|
|
58
|
+
(axiosInstance: AxiosInstance) => (jwtToken: string, macAddress: string) =>
|
|
59
|
+
setPower(axiosInstance)(jwtToken, macAddress, 0);
|
|
60
|
+
|
|
61
|
+
const configure = (baseURL: string = API_URL) => {
|
|
62
|
+
const axiosInstance = axios.create({ baseURL });
|
|
63
|
+
const deviceInfoInstance = deviceInfo(axiosInstance);
|
|
64
|
+
const setPowerInstance = setPower(axiosInstance);
|
|
65
|
+
const setPowerOffInstance = setPowerOff(axiosInstance);
|
|
66
|
+
const setPowerOnInstance = setPowerOn(axiosInstance);
|
|
67
|
+
return {
|
|
68
|
+
deviceInfo: deviceInfoInstance,
|
|
69
|
+
setPower: setPowerInstance,
|
|
70
|
+
setPowerOff: setPowerOffInstance,
|
|
71
|
+
setPowerOn: setPowerOnInstance,
|
|
72
|
+
};
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
const defaultApi = configure();
|
|
76
|
+
|
|
77
|
+
export { signIn, configure };
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
interface CommandsType {
|
|
2
|
+
power: boolean;
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
interface TemperaturesType {
|
|
6
|
+
board: number;
|
|
7
|
+
enviroment: number;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
interface StatusType {
|
|
11
|
+
commands: CommandsType;
|
|
12
|
+
temperatures: TemperaturesType;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
interface UserParametersType {
|
|
16
|
+
enviroment_1_temperature: number;
|
|
17
|
+
enviroment_2_temperature: number;
|
|
18
|
+
enviroment_3_temperature: number;
|
|
19
|
+
is_auto: boolean;
|
|
20
|
+
is_sound_active: boolean;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
interface DeviceInfoType {
|
|
24
|
+
status: StatusType;
|
|
25
|
+
nvm: {
|
|
26
|
+
user_parameters: UserParametersType;
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export type {
|
|
31
|
+
CommandsType,
|
|
32
|
+
DeviceInfoType,
|
|
33
|
+
StatusType,
|
|
34
|
+
TemperaturesType,
|
|
35
|
+
UserParametersType,
|
|
36
|
+
};
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"module": "commonjs",
|
|
4
|
+
"target": "es2015",
|
|
5
|
+
"declaration": true,
|
|
6
|
+
"outDir": "./dist",
|
|
7
|
+
"rootDir": "src",
|
|
8
|
+
"strict": true,
|
|
9
|
+
"esModuleInterop": true,
|
|
10
|
+
"resolveJsonModule": true,
|
|
11
|
+
"preserveConstEnums": true
|
|
12
|
+
},
|
|
13
|
+
"include": [
|
|
14
|
+
"src/**/*.ts"
|
|
15
|
+
]
|
|
16
|
+
}
|
package/dist/package.json
DELETED
|
@@ -1,50 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "edilkamin",
|
|
3
|
-
"version": "1.3.0",
|
|
4
|
-
"description": "",
|
|
5
|
-
"main": "dist/index.js",
|
|
6
|
-
"types": "dist/index.d.ts",
|
|
7
|
-
"scripts": {
|
|
8
|
-
"cli": "ts-node src/cli.ts",
|
|
9
|
-
"cli:debug": "node --inspect --require ts-node/register/transpile-only src/cli.ts",
|
|
10
|
-
"test": "mocha --require ts-node/register src/*.test.ts",
|
|
11
|
-
"test:debug": "mocha --require ts-node/register/transpile-only --inspect src/*.test.ts",
|
|
12
|
-
"lint": "prettier --check src docs .github *.md",
|
|
13
|
-
"format": "prettier --write src docs .github *.md",
|
|
14
|
-
"build": "tsc"
|
|
15
|
-
},
|
|
16
|
-
"files": [
|
|
17
|
-
"/dist"
|
|
18
|
-
],
|
|
19
|
-
"repository": {
|
|
20
|
-
"type": "git",
|
|
21
|
-
"url": "git+https://github.com/AndreMiras/edilkamin.js.git"
|
|
22
|
-
},
|
|
23
|
-
"author": "Andre Miras",
|
|
24
|
-
"license": "MIT",
|
|
25
|
-
"bugs": {
|
|
26
|
-
"url": "https://github.com/AndreMiras/edilkamin.js/issues"
|
|
27
|
-
},
|
|
28
|
-
"homepage": "https://github.com/AndreMiras/edilkamin.js#readme",
|
|
29
|
-
"bin": {
|
|
30
|
-
"edilkamin": "dist/cli.js"
|
|
31
|
-
},
|
|
32
|
-
"dependencies": {
|
|
33
|
-
"aws-amplify": "^6.10.0",
|
|
34
|
-
"axios": "^0.26.0"
|
|
35
|
-
},
|
|
36
|
-
"devDependencies": {
|
|
37
|
-
"@aws-amplify/cli": "^7.6.21",
|
|
38
|
-
"@types/mocha": "^10.0.10",
|
|
39
|
-
"@types/sinon": "^17.0.3",
|
|
40
|
-
"mocha": "^10.8.2",
|
|
41
|
-
"prettier": "^2.5.1",
|
|
42
|
-
"sinon": "^19.0.2",
|
|
43
|
-
"ts-node": "^10.9.1",
|
|
44
|
-
"typedoc": "^0.27.2",
|
|
45
|
-
"typescript": "^5.7.2"
|
|
46
|
-
},
|
|
47
|
-
"optionalDependencies": {
|
|
48
|
-
"commander": "^12.1.0"
|
|
49
|
-
}
|
|
50
|
-
}
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|