flaks-node-hon 1.0.0
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/LICENSE +21 -0
- package/README.md +144 -0
- package/bin/node-hon.js +94 -0
- package/cli/ac_apply_preset.js +36 -0
- package/cli/ac_generate_preset.js +80 -0
- package/cli/ac_turn_off.js +20 -0
- package/cli/ac_turn_on.js +21 -0
- package/cli/config.js +84 -0
- package/cli/purge_cache.js +16 -0
- package/cli/show_my_ac_capabilities.js +36 -0
- package/cli/show_my_ac_devices.js +19 -0
- package/config_example.js +10 -0
- package/package.json +41 -0
- package/presets/preset_auto.json +7 -0
- package/presets/preset_cool.json +8 -0
- package/presets/preset_dry.json +6 -0
- package/presets/preset_fan.json +8 -0
- package/src/ac.js +330 -0
- package/src/api.js +123 -0
- package/src/appliance-identity.js +71 -0
- package/src/appliance.js +282 -0
- package/src/auth.js +424 -0
- package/src/caching/appliance-cache.js +71 -0
- package/src/caching/session-store.js +47 -0
- package/src/client.js +253 -0
- package/src/command.js +314 -0
- package/src/connection.js +73 -0
- package/src/constants.js +17 -0
- package/src/device.js +29 -0
- package/src/errors.js +38 -0
- package/src/index.js +25 -0
- package/src/lib/config.js +22 -0
- package/src/lib/cookie-jar.js +36 -0
- package/src/lib/logger.js +56 -0
- package/src/lib-cli/_format.js +33 -0
- package/src/lib-cli/_get-ac-client.js +29 -0
- package/src/lib-cli/_get-client.js +25 -0
- package/src/lib-cli/_prompt.js +61 -0
- package/src/lib-cli/_run.js +18 -0
- package/src/lib-cli/_select-ac.js +36 -0
- package/src/parameters.js +261 -0
- package/src/preset-generator.js +171 -0
- package/types/global.ts +19 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Flaks
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
# hOn Node.js AC Controls
|
|
2
|
+
|
|
3
|
+
A CommonJS Node.js module for hOn authentication and air conditioner controls.
|
|
4
|
+
|
|
5
|
+
## Advantages
|
|
6
|
+
|
|
7
|
+
- Speed: cached AC preset runs take about 1 second.
|
|
8
|
+
- CLI: easy to set up and use.
|
|
9
|
+
- Module: easy to reuse in other projects.
|
|
10
|
+
|
|
11
|
+
## About This Project
|
|
12
|
+
|
|
13
|
+
This project is a **Node.js port** of [Andre0512/pyhOn](https://github.com/Andre0512/pyhOn), which was originally developed by Andre0512 under the MIT License.
|
|
14
|
+
|
|
15
|
+
All credit for the original implementation goes to Andre0512. This version reimplements the library in Node.js and may differ in API and functionality to better suit the JavaScript ecosystem.
|
|
16
|
+
|
|
17
|
+
## Install From NPM
|
|
18
|
+
|
|
19
|
+
After this package is published, install it globally:
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
npm install -g node-hon
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Then create a working config in the installed package folder or run from a checkout that has `config.js`, presets, and cache files available.
|
|
26
|
+
|
|
27
|
+
## Global CLI Setup From This Repo
|
|
28
|
+
|
|
29
|
+
1. Clone or download this repository.
|
|
30
|
+
|
|
31
|
+
2. Install dependencies:
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
npm install
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
3. Create your local config:
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
cp config_example.js config.js
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
4. Edit `config.js` and set your hOn account values.
|
|
44
|
+
|
|
45
|
+
5. Install this checkout as a global npm command:
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
npm install -g .
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
6. Confirm the CLI is available:
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
node-hon list
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## CLI Usage
|
|
58
|
+
|
|
59
|
+
List available air conditioners:
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
node-hon list
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Apply a preset by MAC address:
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
node-hon apply xx-xx-xx-xx-xx-xx preset_fan
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
Apply a preset by AC name:
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
node-hon apply Bedroom preset_fan
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Turn an AC off:
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
node-hon apply bedroom off
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
Generate a preset interactively from live hOn capabilities:
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
node-hon generate-preset
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
Purge cached session and appliance command data:
|
|
90
|
+
|
|
91
|
+
```bash
|
|
92
|
+
node-hon purge-cache
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
The AC identifier can be a MAC address, unique ID, or nickname. Nickname lookup is case-insensitive, so `Bedroom` and `bedroom` can both match the same AC.
|
|
96
|
+
|
|
97
|
+
Generated and bundled presets live in `presets/`. Preset names are passed without `.json`, for example `preset_fan` loads `presets/preset_fan.json`.
|
|
98
|
+
|
|
99
|
+
Runtime cache files live under `cache/` by default, including session and appliance command cache data. Run `node-hon purge-cache`, or set `forceApplianceCacheRefresh: true` in `config.js`, when the appliance command model changes or a preset cannot find a parameter that exists in the app.
|
|
100
|
+
|
|
101
|
+
## Development CLI
|
|
102
|
+
|
|
103
|
+
You can also run the CLI scripts directly from the repository:
|
|
104
|
+
|
|
105
|
+
```bash
|
|
106
|
+
node cli/show_my_ac_devices.js
|
|
107
|
+
node cli/ac_generate_preset.js
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
`ac_generate_preset.js` loads the latest AC capabilities from the hOn API. You can set `AC_ID` or `PRESET_NAME` to preselect the air conditioner or output preset name.
|
|
111
|
+
|
|
112
|
+
## Tests
|
|
113
|
+
|
|
114
|
+
```bash
|
|
115
|
+
npm test
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
## Publishing
|
|
119
|
+
|
|
120
|
+
Before publishing, make sure you are logged in:
|
|
121
|
+
|
|
122
|
+
```bash
|
|
123
|
+
npm login
|
|
124
|
+
npm whoami
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
Verify the package:
|
|
128
|
+
|
|
129
|
+
```bash
|
|
130
|
+
npm test
|
|
131
|
+
npm run typecheck
|
|
132
|
+
npm pack --dry-run
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
Publish:
|
|
136
|
+
|
|
137
|
+
```bash
|
|
138
|
+
npm publish --access public
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
### License
|
|
142
|
+
|
|
143
|
+
This project is released under the MIT License.
|
|
144
|
+
The original Python library is also MIT-licensed. See the `LICENSE` file for details and attribution.
|
package/bin/node-hon.js
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
|
|
4
|
+
const path = require("node:path");
|
|
5
|
+
|
|
6
|
+
const packageRoot = path.resolve(__dirname, "..");
|
|
7
|
+
|
|
8
|
+
function usage() {
|
|
9
|
+
return [
|
|
10
|
+
"Usage:",
|
|
11
|
+
" node-hon apply <mac|name> <preset_name|off>",
|
|
12
|
+
" node-hon config",
|
|
13
|
+
" node-hon list",
|
|
14
|
+
" node-hon generate-preset",
|
|
15
|
+
" node-hon purge-cache"
|
|
16
|
+
].join("\n");
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
async function run(argv = process.argv.slice(2), options = {}) {
|
|
20
|
+
const stderr = options.stderr || process.stderr;
|
|
21
|
+
const commands = options.commands || defaultCommands();
|
|
22
|
+
const baseDir = options.baseDir || packageRoot;
|
|
23
|
+
const [command, ...args] = argv;
|
|
24
|
+
|
|
25
|
+
if (command === "apply") {
|
|
26
|
+
const [acId, presetName, ...rest] = args;
|
|
27
|
+
if (!acId || !presetName || rest.length) {
|
|
28
|
+
stderr.write(`${usage()}\n`);
|
|
29
|
+
return 1;
|
|
30
|
+
}
|
|
31
|
+
await commands.apply({ acId, presetName, baseDir });
|
|
32
|
+
return 0;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (command === "generate-preset") {
|
|
36
|
+
if (args.length) {
|
|
37
|
+
stderr.write(`${usage()}\n`);
|
|
38
|
+
return 1;
|
|
39
|
+
}
|
|
40
|
+
await commands.generatePreset({ baseDir });
|
|
41
|
+
return 0;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (command === "config") {
|
|
45
|
+
if (args.length) {
|
|
46
|
+
stderr.write(`${usage()}\n`);
|
|
47
|
+
return 1;
|
|
48
|
+
}
|
|
49
|
+
await commands.config({ baseDir });
|
|
50
|
+
return 0;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (command === "list") {
|
|
54
|
+
if (args.length) {
|
|
55
|
+
stderr.write(`${usage()}\n`);
|
|
56
|
+
return 1;
|
|
57
|
+
}
|
|
58
|
+
await commands.list({ baseDir });
|
|
59
|
+
return 0;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (command === "purge-cache") {
|
|
63
|
+
if (args.length) {
|
|
64
|
+
stderr.write(`${usage()}\n`);
|
|
65
|
+
return 1;
|
|
66
|
+
}
|
|
67
|
+
await commands.purgeCache({ baseDir });
|
|
68
|
+
return 0;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
stderr.write(`${usage()}\n`);
|
|
72
|
+
return 1;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function defaultCommands() {
|
|
76
|
+
return {
|
|
77
|
+
apply: require("../cli/ac_apply_preset").main,
|
|
78
|
+
config: require("../cli/config").main,
|
|
79
|
+
list: require("../cli/show_my_ac_devices").main,
|
|
80
|
+
generatePreset: require("../cli/ac_generate_preset").main,
|
|
81
|
+
purgeCache: require("../cli/purge_cache").main
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (require.main === module) {
|
|
86
|
+
run().then((code) => {
|
|
87
|
+
process.exitCode = code;
|
|
88
|
+
}).catch((error) => {
|
|
89
|
+
console.error(error);
|
|
90
|
+
process.exitCode = 1;
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
module.exports = { run, usage };
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
const path = require("node:path");
|
|
2
|
+
const getAcClient = require("../src/lib-cli/_get-ac-client");
|
|
3
|
+
const { formatAc } = require("../src/lib-cli/_format");
|
|
4
|
+
const { handleCliError } = require("../src/lib-cli/_run");
|
|
5
|
+
|
|
6
|
+
async function main(options = {}) {
|
|
7
|
+
const baseDir = options.baseDir || path.resolve(__dirname, "..");
|
|
8
|
+
const loadAcClient = options.getAcClient || getAcClient;
|
|
9
|
+
const { ac, client } = await loadAcClient({ ...options, baseDir });
|
|
10
|
+
|
|
11
|
+
try {
|
|
12
|
+
const presetName = options.presetName || process.env.PRESET_NAME || "preset_fan";
|
|
13
|
+
if (presetName === "off") {
|
|
14
|
+
await ac.powerOff();
|
|
15
|
+
console.log(`Powered off ${formatAc(ac)}`);
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
const presetFile = path.resolve(
|
|
19
|
+
baseDir,
|
|
20
|
+
"presets",
|
|
21
|
+
`${presetName}.json`,
|
|
22
|
+
);
|
|
23
|
+
|
|
24
|
+
const preset = require(presetFile);
|
|
25
|
+
await ac.applyPreset(preset, presetName);
|
|
26
|
+
console.log(`Applied ${presetName} to ${formatAc(ac)}`);
|
|
27
|
+
} finally {
|
|
28
|
+
await client.close();
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (require.main === module) {
|
|
33
|
+
main().catch(handleCliError);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
module.exports = { main };
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
const fs = require("node:fs/promises");
|
|
2
|
+
const path = require("node:path");
|
|
3
|
+
const getClient = require("../src/lib-cli/_get-client");
|
|
4
|
+
const { createAsk, promptChoice } = require("../src/lib-cli/_prompt");
|
|
5
|
+
const { formatAc, printSkipped } = require("../src/lib-cli/_format");
|
|
6
|
+
const { handleCliError } = require("../src/lib-cli/_run");
|
|
7
|
+
const { selectAirConditioner } = require("../src/lib-cli/_select-ac");
|
|
8
|
+
const {
|
|
9
|
+
buildPreset,
|
|
10
|
+
defaultValueForField,
|
|
11
|
+
getFieldDescriptors,
|
|
12
|
+
getModeOptions,
|
|
13
|
+
selectPresetCommand
|
|
14
|
+
} = require("../src/preset-generator");
|
|
15
|
+
|
|
16
|
+
async function main(options = {}) {
|
|
17
|
+
const baseDir = options.baseDir || path.resolve(__dirname, "..");
|
|
18
|
+
const client = await getClient({ ...options, baseDir });
|
|
19
|
+
const ask = options.ask || createAsk();
|
|
20
|
+
try {
|
|
21
|
+
const airConditioners = await client.getAirConditioners();
|
|
22
|
+
if (!airConditioners.length) {
|
|
23
|
+
console.log("No air conditioners found.");
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
const selectedAc = await selectAirConditioner(ask, airConditioners, options.acId);
|
|
27
|
+
const selectedCommand = selectPresetCommand(selectedAc.capabilities());
|
|
28
|
+
const generatorMode = await promptChoice(ask, "Generator mode", ["basic", "advanced"], "basic");
|
|
29
|
+
const modeOptions = getModeOptions(selectedCommand.command);
|
|
30
|
+
const presetMode = modeOptions.length ? await promptChoice(ask, "Preset mode", modeOptions, modeOptions[0]) : "";
|
|
31
|
+
const { fields, skipped } = getFieldDescriptors(selectedCommand.command, generatorMode);
|
|
32
|
+
const values = {};
|
|
33
|
+
for (const field of fields) {
|
|
34
|
+
values[field.name] = await promptField(ask, field);
|
|
35
|
+
}
|
|
36
|
+
const preset = buildPreset(selectedCommand.command, values, presetMode);
|
|
37
|
+
const presetName = await promptPresetName(ask, options.presetName);
|
|
38
|
+
const outputFile = path.resolve(baseDir, "presets", `${presetName}.json`);
|
|
39
|
+
await fs.mkdir(path.dirname(outputFile), { recursive: true });
|
|
40
|
+
await fs.writeFile(outputFile, `${JSON.stringify(preset, null, 2)}\n`);
|
|
41
|
+
console.log(`Generated ${path.relative(process.cwd(), outputFile)} for ${formatAc(selectedAc)}`);
|
|
42
|
+
console.log(`Selected command: ${selectedCommand.name}${presetMode ? ` (${presetMode})` : ""}`);
|
|
43
|
+
printSkipped(skipped);
|
|
44
|
+
} finally {
|
|
45
|
+
if (ask.close) {
|
|
46
|
+
ask.close();
|
|
47
|
+
}
|
|
48
|
+
await client.close();
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async function promptPresetName(ask, presetName = "") {
|
|
53
|
+
const fallback = presetName || process.env.PRESET_NAME || "preset_custom";
|
|
54
|
+
for (;;) {
|
|
55
|
+
const answer = (await ask.question(`Preset filename [${fallback}]: `)).trim() || fallback;
|
|
56
|
+
const safe = answer.replace(/\.json$/i, "");
|
|
57
|
+
if (/^[a-zA-Z0-9_.-]+$/.test(safe)) {
|
|
58
|
+
return safe;
|
|
59
|
+
}
|
|
60
|
+
console.log("Use only letters, numbers, dot, underscore, and dash.");
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async function promptField(ask, field) {
|
|
65
|
+
const fallback = defaultValueForField(field);
|
|
66
|
+
for (;;) {
|
|
67
|
+
const suffix = field.values.length ? ` (${field.values.join(", ")})` : "";
|
|
68
|
+
const answer = (await ask.question(`${field.name}${suffix} [${fallback}]: `)).trim() || fallback;
|
|
69
|
+
if (field.values.includes(String(answer))) {
|
|
70
|
+
return String(answer);
|
|
71
|
+
}
|
|
72
|
+
console.log(`Allowed values: ${field.values.join(", ")}`);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (require.main === module) {
|
|
77
|
+
main().catch(handleCliError);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
module.exports = { main };
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
const getAcClient = require("../src/lib-cli/_get-ac-client");
|
|
2
|
+
const { formatAc } = require("../src/lib-cli/_format");
|
|
3
|
+
const { handleCliError } = require("../src/lib-cli/_run");
|
|
4
|
+
|
|
5
|
+
async function main(options = {}) {
|
|
6
|
+
const { ac, client } = await getAcClient(options);
|
|
7
|
+
|
|
8
|
+
try {
|
|
9
|
+
await ac.powerOff();
|
|
10
|
+
console.log(`Turned off ${formatAc(ac)}`);
|
|
11
|
+
} finally {
|
|
12
|
+
await client.close();
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
if (require.main === module) {
|
|
17
|
+
main().catch(handleCliError);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
module.exports = { main };
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
const getAcClient = require("../src/lib-cli/_get-ac-client");
|
|
2
|
+
const { formatAc } = require("../src/lib-cli/_format");
|
|
3
|
+
const { handleCliError } = require("../src/lib-cli/_run");
|
|
4
|
+
|
|
5
|
+
async function main(options = {}) {
|
|
6
|
+
const { ac, client } = await getAcClient(options);
|
|
7
|
+
|
|
8
|
+
try {
|
|
9
|
+
await ac.powerOn();
|
|
10
|
+
|
|
11
|
+
console.log(`Turned on ${formatAc(ac)}`);
|
|
12
|
+
} finally {
|
|
13
|
+
await client.close();
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
if (require.main === module) {
|
|
18
|
+
main().catch(handleCliError);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
module.exports = { main };
|
package/cli/config.js
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
const fs = require("node:fs/promises");
|
|
2
|
+
const path = require("node:path");
|
|
3
|
+
const { stdout: output } = require("node:process");
|
|
4
|
+
const { askBoolean, askText, createAsk } = require("../src/lib-cli/_prompt");
|
|
5
|
+
const { handleCliError } = require("../src/lib-cli/_run");
|
|
6
|
+
|
|
7
|
+
async function main(options = {}) {
|
|
8
|
+
const baseDir = options.baseDir || path.resolve(__dirname, "..");
|
|
9
|
+
const configPath = options.configPath || path.resolve(baseDir, "config.js");
|
|
10
|
+
const examplePath = path.resolve(baseDir, "config_example.js");
|
|
11
|
+
const current = await loadConfigOrDefaults(configPath, examplePath);
|
|
12
|
+
const ask = options.ask || createAsk();
|
|
13
|
+
|
|
14
|
+
try {
|
|
15
|
+
const config = {
|
|
16
|
+
email: await askText(ask, "hOn email", current.email),
|
|
17
|
+
password: await askText(ask, "hOn password", "", current.password),
|
|
18
|
+
mobileId: await askText(ask, "Mobile ID", current.mobileId || "pyhOn-node"),
|
|
19
|
+
sessionFile: await askText(ask, "Session file", current.sessionFile || "./cache/.hon-session.json"),
|
|
20
|
+
applianceCacheFile: await askText(ask, "Appliance cache file", current.applianceCacheFile || "./cache/.hon-appliance-cache.json"),
|
|
21
|
+
forceApplianceCacheRefresh: await askBoolean(ask, "Force appliance cache refresh", Boolean(current.forceApplianceCacheRefresh)),
|
|
22
|
+
debug: await askBoolean(ask, "Debug logging", Boolean(current.debug))
|
|
23
|
+
};
|
|
24
|
+
await fs.writeFile(configPath, buildConfigText(config), "utf8");
|
|
25
|
+
output.write(`Saved ${configPath}\n`);
|
|
26
|
+
} finally {
|
|
27
|
+
if (ask.close) {
|
|
28
|
+
ask.close();
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async function loadConfigOrDefaults(configPath, examplePath) {
|
|
34
|
+
if (await exists(configPath)) {
|
|
35
|
+
return requireFresh(configPath);
|
|
36
|
+
}
|
|
37
|
+
if (await exists(examplePath)) {
|
|
38
|
+
return requireFresh(examplePath);
|
|
39
|
+
}
|
|
40
|
+
return {
|
|
41
|
+
email: "user@example.com",
|
|
42
|
+
password: "password",
|
|
43
|
+
mobileId: "pyhOn-node",
|
|
44
|
+
sessionFile: "./cache/.hon-session.json",
|
|
45
|
+
applianceCacheFile: "./cache/.hon-appliance-cache.json",
|
|
46
|
+
forceApplianceCacheRefresh: false,
|
|
47
|
+
debug: false
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function requireFresh(filePath) {
|
|
52
|
+
const resolved = require.resolve(path.resolve(filePath));
|
|
53
|
+
delete require.cache[resolved];
|
|
54
|
+
return require(resolved);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async function exists(filePath) {
|
|
58
|
+
try {
|
|
59
|
+
await fs.access(filePath);
|
|
60
|
+
return true;
|
|
61
|
+
} catch {
|
|
62
|
+
return false;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function buildConfigText(config) {
|
|
67
|
+
return `/** @type {import("./types/global").ProjectConfig} */
|
|
68
|
+
module.exports = {
|
|
69
|
+
email: ${JSON.stringify(config.email)},
|
|
70
|
+
password: ${JSON.stringify(config.password)},
|
|
71
|
+
mobileId: ${JSON.stringify(config.mobileId)},
|
|
72
|
+
sessionFile: ${JSON.stringify(config.sessionFile)},
|
|
73
|
+
applianceCacheFile: ${JSON.stringify(config.applianceCacheFile)},
|
|
74
|
+
forceApplianceCacheRefresh: ${Boolean(config.forceApplianceCacheRefresh)},
|
|
75
|
+
debug: ${Boolean(config.debug)}
|
|
76
|
+
};
|
|
77
|
+
`;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (require.main === module) {
|
|
81
|
+
main().catch(handleCliError);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
module.exports = { buildConfigText, main };
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
const fs = require("node:fs/promises");
|
|
2
|
+
const path = require("node:path");
|
|
3
|
+
const { handleCliError } = require("../src/lib-cli/_run");
|
|
4
|
+
|
|
5
|
+
async function main(options = {}) {
|
|
6
|
+
const baseDir = options.baseDir || path.resolve(__dirname, "..");
|
|
7
|
+
const cacheDir = options.cacheDir || path.resolve(baseDir, "cache");
|
|
8
|
+
await fs.rm(cacheDir, { recursive: true, force: true });
|
|
9
|
+
console.log(`Purged cache: ${cacheDir}`);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
if (require.main === module) {
|
|
13
|
+
main().catch(handleCliError);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
module.exports = { main };
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
const fs = require("node:fs");
|
|
2
|
+
const getClient = require("../src/lib-cli/_get-client");
|
|
3
|
+
|
|
4
|
+
async function main(options = {}) {
|
|
5
|
+
const client = await getClient(options);
|
|
6
|
+
|
|
7
|
+
try {
|
|
8
|
+
const airConditioners = await client.getAirConditioners();
|
|
9
|
+
if (!airConditioners.length) {
|
|
10
|
+
console.log("No air conditioners found.");
|
|
11
|
+
return;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const filename = "./hon-devices-capabilities.json";
|
|
15
|
+
const mapping = {};
|
|
16
|
+
for (const ac of airConditioners) {
|
|
17
|
+
const naming = `${ac.nickName}_${ac.macAddress}`;
|
|
18
|
+
console.log(naming);
|
|
19
|
+
|
|
20
|
+
mapping[naming] = ac.capabilities();
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
fs.writeFileSync(filename, JSON.stringify(mapping, null, 2));
|
|
24
|
+
} finally {
|
|
25
|
+
await client.close();
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (require.main === module) {
|
|
30
|
+
main().catch((error) => {
|
|
31
|
+
console.error(error);
|
|
32
|
+
process.exitCode = 1;
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
module.exports = { main };
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
const getClient = require("../src/lib-cli/_get-client");
|
|
2
|
+
const { printAcList } = require("../src/lib-cli/_format");
|
|
3
|
+
const { handleCliError } = require("../src/lib-cli/_run");
|
|
4
|
+
|
|
5
|
+
async function main(options = {}) {
|
|
6
|
+
const client = await getClient(options);
|
|
7
|
+
try {
|
|
8
|
+
const airConditioners = await client.getAirConditioners();
|
|
9
|
+
printAcList(airConditioners);
|
|
10
|
+
} finally {
|
|
11
|
+
await client.close();
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
if (require.main === module) {
|
|
16
|
+
main().catch(handleCliError);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
module.exports = { main };
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/** @type {import("./types/global").ProjectConfig} */
|
|
2
|
+
module.exports = {
|
|
3
|
+
email: "user@example.com",
|
|
4
|
+
password: "password",
|
|
5
|
+
mobileId: "pyhOn-node",
|
|
6
|
+
sessionFile: "./cache/.hon-session.json",
|
|
7
|
+
applianceCacheFile: "./cache/.hon-appliance-cache.json",
|
|
8
|
+
forceApplianceCacheRefresh: false,
|
|
9
|
+
debug: false
|
|
10
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "flaks-node-hon",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "CommonJS Node.js client for hOn auth and air conditioner controls",
|
|
5
|
+
"main": "src/index.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"node-hon": "bin/node-hon.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"bin/",
|
|
11
|
+
"cli/",
|
|
12
|
+
"src/",
|
|
13
|
+
"presets/",
|
|
14
|
+
"types/",
|
|
15
|
+
"config_example.js",
|
|
16
|
+
"README.md",
|
|
17
|
+
"LICENSE"
|
|
18
|
+
],
|
|
19
|
+
"type": "commonjs",
|
|
20
|
+
"author": "Flaks",
|
|
21
|
+
"engines": {
|
|
22
|
+
"node": ">=23"
|
|
23
|
+
},
|
|
24
|
+
"scripts": {
|
|
25
|
+
"test": "node --test \"test/*.test.js\"",
|
|
26
|
+
"typecheck": "tsc --noEmit --project jsconfig.json",
|
|
27
|
+
"prepublishOnly": "npm test && npm run typecheck"
|
|
28
|
+
},
|
|
29
|
+
"keywords": [
|
|
30
|
+
"hon",
|
|
31
|
+
"haier",
|
|
32
|
+
"candy",
|
|
33
|
+
"hoover",
|
|
34
|
+
"air-conditioner"
|
|
35
|
+
],
|
|
36
|
+
"license": "MIT",
|
|
37
|
+
"devDependencies": {
|
|
38
|
+
"@types/node": "^25.9.1",
|
|
39
|
+
"typescript": "^6.0.3"
|
|
40
|
+
}
|
|
41
|
+
}
|