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
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
function strToFloat(value) {
|
|
2
|
+
if (typeof value === "number") {
|
|
3
|
+
return value;
|
|
4
|
+
}
|
|
5
|
+
const text = String(value).replace(",", ".");
|
|
6
|
+
const number = Number(text);
|
|
7
|
+
if (!Number.isFinite(number)) {
|
|
8
|
+
throw new ValueError(`Invalid number: ${value}`);
|
|
9
|
+
}
|
|
10
|
+
return Number.isInteger(number) ? Number.parseInt(text, 10) : number;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function cleanValue(value) {
|
|
14
|
+
return String(value).trim().replace(/^\[/, "").replace(/\]$/, "").replaceAll("|", "_").toLowerCase();
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
class ValueError extends Error {
|
|
18
|
+
constructor(message) {
|
|
19
|
+
super(message);
|
|
20
|
+
this.name = "ValueError";
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
class HonParameter {
|
|
25
|
+
/**
|
|
26
|
+
* @param {string} key
|
|
27
|
+
* @param {Record<string, any>} [attributes]
|
|
28
|
+
* @param {string} [group]
|
|
29
|
+
*/
|
|
30
|
+
constructor(key, attributes = {}, group = "") {
|
|
31
|
+
this.key = key;
|
|
32
|
+
this.attributes = attributes;
|
|
33
|
+
this.group = group;
|
|
34
|
+
this.triggers = new Map();
|
|
35
|
+
this._values = [];
|
|
36
|
+
this.min = 0;
|
|
37
|
+
this.max = 0;
|
|
38
|
+
this.step = 1;
|
|
39
|
+
this.reset();
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
reset() {
|
|
43
|
+
this.category = this.attributes.category || "";
|
|
44
|
+
this.typology = this.attributes.typology || "";
|
|
45
|
+
this.mandatory = this.attributes.mandatory || 0;
|
|
46
|
+
this._value = "";
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
get value() {
|
|
50
|
+
return this._value == null ? "0" : this._value;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
set value(value) {
|
|
54
|
+
this._value = value;
|
|
55
|
+
this.checkTrigger(value);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
get internValue() {
|
|
59
|
+
return String(this.value);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
get values() {
|
|
63
|
+
return [String(this.value)];
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
set values(values) {
|
|
67
|
+
this._values = values;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
addTrigger(value, func, data) {
|
|
71
|
+
const key = String(value).toLowerCase();
|
|
72
|
+
if (String(this._value).toLowerCase() === key) {
|
|
73
|
+
func(data);
|
|
74
|
+
}
|
|
75
|
+
if (!this.triggers.has(key)) {
|
|
76
|
+
this.triggers.set(key, []);
|
|
77
|
+
}
|
|
78
|
+
const triggers = this.triggers.get(key);
|
|
79
|
+
if (triggers) {
|
|
80
|
+
triggers.push([func, data]);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
checkTrigger(value) {
|
|
85
|
+
const rules = this.triggers.get(String(value).toLowerCase()) || [];
|
|
86
|
+
for (const [func, data] of rules) {
|
|
87
|
+
func(data);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
class HonParameterEnum extends HonParameter {
|
|
93
|
+
reset() {
|
|
94
|
+
super.reset();
|
|
95
|
+
this.defaultValue = this.attributes.defaultValue || "";
|
|
96
|
+
this._value = this.defaultValue || "0";
|
|
97
|
+
this._values = this.attributes.enumValues || [];
|
|
98
|
+
if (this.defaultValue && !this.values.includes(cleanValue(String(this.defaultValue).replace(/^\[/, "").replace(/\]$/, "")))) {
|
|
99
|
+
this._values.push(this.defaultValue);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
get values() {
|
|
104
|
+
return this._values.map(cleanValue);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
set values(values) {
|
|
108
|
+
this._values = values;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
get internValue() {
|
|
112
|
+
const fallback = this.values[0] ?? "0";
|
|
113
|
+
return this._value == null ? String(fallback) : String(this._value);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
get value() {
|
|
117
|
+
const fallback = this.values[0] ?? "0";
|
|
118
|
+
return this._value == null ? fallback : cleanValue(this._value);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
set value(value) {
|
|
122
|
+
const cleaned = cleanValue(value);
|
|
123
|
+
if (this.values.includes(cleaned)) {
|
|
124
|
+
this._value = value;
|
|
125
|
+
this.checkTrigger(value);
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
throw new ValueError(`Allowed values: ${this.values.join(", ")}. But was: ${value}`);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
class HonParameterRange extends HonParameter {
|
|
133
|
+
reset() {
|
|
134
|
+
super.reset();
|
|
135
|
+
this.min = strToFloat(this.attributes.minimumValue || 0);
|
|
136
|
+
this.max = strToFloat(this.attributes.maximumValue || 0);
|
|
137
|
+
this.step = strToFloat(this.attributes.incrementValue || 0) || 1;
|
|
138
|
+
this.defaultValue = strToFloat(this.attributes.defaultValue ?? this.min);
|
|
139
|
+
this._value = this.defaultValue;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
get value() {
|
|
143
|
+
return this._value == null ? this.min : this._value;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
set value(value) {
|
|
147
|
+
const number = strToFloat(value);
|
|
148
|
+
const scaled = (number - this.min) * 100;
|
|
149
|
+
const step = this.step * 100;
|
|
150
|
+
if (this.min <= number && number <= this.max && Math.abs(scaled % step) < 1e-9) {
|
|
151
|
+
this._value = number;
|
|
152
|
+
this.checkTrigger(number);
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
throw new ValueError(`Allowed: min ${this.min} max ${this.max} step ${this.step}. But was: ${value}`);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
get values() {
|
|
159
|
+
const result = [];
|
|
160
|
+
for (let value = this.min; value <= this.max; value += this.step) {
|
|
161
|
+
result.push(String(value));
|
|
162
|
+
}
|
|
163
|
+
return result;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
class HonParameterFixed extends HonParameter {
|
|
168
|
+
reset() {
|
|
169
|
+
super.reset();
|
|
170
|
+
this._value = this.attributes.fixedValue || "";
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
get value() {
|
|
174
|
+
return this._value !== "" ? this._value : "0";
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
set value(value) {
|
|
178
|
+
this._value = value;
|
|
179
|
+
this.checkTrigger(value);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
class HonParameterProgram extends HonParameterEnum {
|
|
184
|
+
/**
|
|
185
|
+
* @param {string} key
|
|
186
|
+
* @param {any} command
|
|
187
|
+
* @param {string} group
|
|
188
|
+
*/
|
|
189
|
+
constructor(key, command, group) {
|
|
190
|
+
super(key, {}, group);
|
|
191
|
+
this.command = command;
|
|
192
|
+
this._value = command.categoryName.includes("PROGRAM") ? command.categoryName.split(".").pop().toLowerCase() : command.categoryName;
|
|
193
|
+
this.programs = command.categories;
|
|
194
|
+
this.typology = "enum";
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
get value() {
|
|
198
|
+
return this._value;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
set value(value) {
|
|
202
|
+
if (!this.values.includes(value)) {
|
|
203
|
+
throw new ValueError(`Allowed values: ${this.values.join(", ")}. But was: ${value}`);
|
|
204
|
+
}
|
|
205
|
+
this.command.category = value;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
get values() {
|
|
209
|
+
return Object.keys(this.programs || {})
|
|
210
|
+
.filter((value) => !value.includes("iot_recipe") && !value.includes("iot_guided"))
|
|
211
|
+
.sort();
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
set values(_values) {
|
|
215
|
+
throw new ValueError("Cannot set program values");
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
get ids() {
|
|
219
|
+
const values = {};
|
|
220
|
+
for (const [name, command] of Object.entries(this.programs || {})) {
|
|
221
|
+
const commandData = /** @type {any} */ (command);
|
|
222
|
+
if (name.includes("iot_") || !commandData.parameters.prCode) {
|
|
223
|
+
continue;
|
|
224
|
+
}
|
|
225
|
+
if (commandData.parameters.favourite && commandData.parameters.favourite.value === "1") {
|
|
226
|
+
continue;
|
|
227
|
+
}
|
|
228
|
+
values[Number(commandData.parameters.prCode.value)] = name;
|
|
229
|
+
}
|
|
230
|
+
return values;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
setValue(value) {
|
|
234
|
+
this._value = value;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function createParameter(name, data, group) {
|
|
239
|
+
switch (data.typology) {
|
|
240
|
+
case "range":
|
|
241
|
+
return new HonParameterRange(name, data, group);
|
|
242
|
+
case "enum":
|
|
243
|
+
return new HonParameterEnum(name, data, group);
|
|
244
|
+
case "fixed":
|
|
245
|
+
return new HonParameterFixed(name, data, group);
|
|
246
|
+
default:
|
|
247
|
+
return null;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
module.exports = {
|
|
252
|
+
HonParameter,
|
|
253
|
+
HonParameterEnum,
|
|
254
|
+
HonParameterRange,
|
|
255
|
+
HonParameterFixed,
|
|
256
|
+
HonParameterProgram,
|
|
257
|
+
ValueError,
|
|
258
|
+
cleanValue,
|
|
259
|
+
strToFloat,
|
|
260
|
+
createParameter
|
|
261
|
+
};
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const fs = require("node:fs");
|
|
4
|
+
|
|
5
|
+
const BASIC_FIELDS = ["tempSel", "windSpeed", "windDirectionVertical", "windDirectionHorizontal"];
|
|
6
|
+
const PRESET_COMMAND_ORDER = ["startProgram", "settings"];
|
|
7
|
+
|
|
8
|
+
class PresetGeneratorError extends Error {
|
|
9
|
+
constructor(message, details = {}) {
|
|
10
|
+
super(message);
|
|
11
|
+
this.name = "PresetGeneratorError";
|
|
12
|
+
this.details = details;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function loadCapabilitiesFile(filePath) {
|
|
17
|
+
const text = fs.readFileSync(filePath, "utf8");
|
|
18
|
+
return validateCapabilities(JSON.parse(text));
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function validateCapabilities(data) {
|
|
22
|
+
if (!data || typeof data !== "object" || Array.isArray(data)) {
|
|
23
|
+
throw new PresetGeneratorError("Capabilities file must contain a JSON object");
|
|
24
|
+
}
|
|
25
|
+
for (const [deviceName, commands] of Object.entries(data)) {
|
|
26
|
+
if (!commands || typeof commands !== "object" || Array.isArray(commands)) {
|
|
27
|
+
throw new PresetGeneratorError(`Capabilities for ${deviceName} must be an object`);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return data;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function listAirConditioners(capabilities) {
|
|
34
|
+
return Object.keys(validateCapabilities(capabilities)).sort();
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function selectAirConditioner(capabilities, acId = "") {
|
|
38
|
+
const names = listAirConditioners(capabilities);
|
|
39
|
+
if (!names.length) {
|
|
40
|
+
throw new PresetGeneratorError("No air conditioners found in capabilities file");
|
|
41
|
+
}
|
|
42
|
+
if (!acId) {
|
|
43
|
+
return { name: names[0], capabilities: capabilities[names[0]] };
|
|
44
|
+
}
|
|
45
|
+
const matches = names.filter((name) => name === acId || name.endsWith(`_${acId}`) || name.includes(acId));
|
|
46
|
+
if (matches.length === 1) {
|
|
47
|
+
return { name: matches[0], capabilities: capabilities[matches[0]] };
|
|
48
|
+
}
|
|
49
|
+
if (matches.length > 1) {
|
|
50
|
+
throw new PresetGeneratorError(`AC_ID matched multiple capability entries: ${acId}`, { matches });
|
|
51
|
+
}
|
|
52
|
+
throw new PresetGeneratorError(`AC_ID did not match any capability entry: ${acId}`, { available: names });
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function selectPresetCommand(deviceCapabilities) {
|
|
56
|
+
const orderedNames = [
|
|
57
|
+
...PRESET_COMMAND_ORDER,
|
|
58
|
+
...Object.keys(deviceCapabilities).filter((name) => !PRESET_COMMAND_ORDER.includes(name))
|
|
59
|
+
];
|
|
60
|
+
for (const name of orderedNames) {
|
|
61
|
+
const command = deviceCapabilities[name];
|
|
62
|
+
if (isCommandCapability(command)) {
|
|
63
|
+
return { name, command };
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
throw new PresetGeneratorError("No usable command capabilities found for selected AC");
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function getModeOptions(command) {
|
|
70
|
+
const categories = Array.isArray(command.categories) ? command.categories.filter(Boolean) : [];
|
|
71
|
+
if (categories.length) {
|
|
72
|
+
return [...new Set(categories)].sort();
|
|
73
|
+
}
|
|
74
|
+
const program = command.parameters?.program;
|
|
75
|
+
if (Array.isArray(program?.values)) {
|
|
76
|
+
return [...new Set(program.values.map(String))].sort();
|
|
77
|
+
}
|
|
78
|
+
return [];
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function getFieldDescriptors(command, mode = "basic") {
|
|
82
|
+
const parameters = command.parameters || {};
|
|
83
|
+
const names = mode === "advanced" ? Object.keys(parameters).sort() : BASIC_FIELDS;
|
|
84
|
+
const fields = [];
|
|
85
|
+
const skipped = [];
|
|
86
|
+
for (const name of names) {
|
|
87
|
+
const parameter = parameters[name];
|
|
88
|
+
if (!parameter) {
|
|
89
|
+
skipped.push({ name, reason: "unsupported" });
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
if (!isSettableParameter(parameter)) {
|
|
93
|
+
skipped.push({ name, reason: parameter.typology === "fixed" ? "fixed" : "internal" });
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
fields.push(describeField(name, parameter));
|
|
97
|
+
}
|
|
98
|
+
return { fields, skipped };
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function buildPreset(command, selectedValues, mode = "") {
|
|
102
|
+
const preset = {};
|
|
103
|
+
const modeOptions = getModeOptions(command);
|
|
104
|
+
if (mode) {
|
|
105
|
+
if (modeOptions.length && !modeOptions.includes(mode)) {
|
|
106
|
+
throw new PresetGeneratorError(`Unsupported mode value: ${mode}`, { values: modeOptions });
|
|
107
|
+
}
|
|
108
|
+
preset.mode = mode;
|
|
109
|
+
}
|
|
110
|
+
for (const [name, value] of Object.entries(selectedValues)) {
|
|
111
|
+
const parameter = command.parameters?.[name];
|
|
112
|
+
if (!parameter) {
|
|
113
|
+
throw new PresetGeneratorError(`Unsupported preset field: ${name}`);
|
|
114
|
+
}
|
|
115
|
+
if (!isSettableParameter(parameter)) {
|
|
116
|
+
throw new PresetGeneratorError(`Preset field is not settable: ${name}`);
|
|
117
|
+
}
|
|
118
|
+
if (!isAllowedValue(parameter, value)) {
|
|
119
|
+
throw new PresetGeneratorError(`Unsupported value for ${name}: ${value}`, { values: parameter.values || [] });
|
|
120
|
+
}
|
|
121
|
+
preset[name] = value;
|
|
122
|
+
}
|
|
123
|
+
return preset;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function defaultValueForField(field) {
|
|
127
|
+
return field.value == null ? field.values[0] || "" : String(field.value);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function isCommandCapability(command) {
|
|
131
|
+
return Boolean(command && typeof command === "object" && !Array.isArray(command) && command.parameters);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function isSettableParameter(parameter) {
|
|
135
|
+
if (!parameter || parameter.typology === "fixed") {
|
|
136
|
+
return false;
|
|
137
|
+
}
|
|
138
|
+
if (parameter.group && parameter.group !== "parameters") {
|
|
139
|
+
return false;
|
|
140
|
+
}
|
|
141
|
+
return Array.isArray(parameter.values) && parameter.values.length > 0;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function describeField(name, parameter) {
|
|
145
|
+
return {
|
|
146
|
+
name,
|
|
147
|
+
group: parameter.group || "",
|
|
148
|
+
typology: parameter.typology || "",
|
|
149
|
+
value: parameter.value,
|
|
150
|
+
values: Array.isArray(parameter.values) ? parameter.values.map(String) : []
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function isAllowedValue(parameter, value) {
|
|
155
|
+
const values = Array.isArray(parameter.values) ? parameter.values.map(String) : [];
|
|
156
|
+
return values.includes(String(value));
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
module.exports = {
|
|
160
|
+
BASIC_FIELDS,
|
|
161
|
+
PresetGeneratorError,
|
|
162
|
+
buildPreset,
|
|
163
|
+
defaultValueForField,
|
|
164
|
+
getFieldDescriptors,
|
|
165
|
+
getModeOptions,
|
|
166
|
+
listAirConditioners,
|
|
167
|
+
loadCapabilitiesFile,
|
|
168
|
+
selectAirConditioner,
|
|
169
|
+
selectPresetCommand,
|
|
170
|
+
validateCapabilities
|
|
171
|
+
};
|
package/types/global.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export interface ProjectConfig {
|
|
2
|
+
email: string;
|
|
3
|
+
password: string;
|
|
4
|
+
mobileId?: string;
|
|
5
|
+
sessionFile: string;
|
|
6
|
+
applianceCacheFile?: string;
|
|
7
|
+
forceApplianceCacheRefresh?: boolean;
|
|
8
|
+
debug?: boolean;
|
|
9
|
+
requestLogging?: boolean;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface HonSessionData {
|
|
13
|
+
refreshToken: string;
|
|
14
|
+
sessionToken: string;
|
|
15
|
+
idToken?: string;
|
|
16
|
+
accessToken?: string;
|
|
17
|
+
expiresAt?: string;
|
|
18
|
+
updatedAt?: string;
|
|
19
|
+
}
|