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/src/ac.js
ADDED
|
@@ -0,0 +1,330 @@
|
|
|
1
|
+
const fs = require("node:fs/promises");
|
|
2
|
+
const { UnsupportedControlError } = require("./errors");
|
|
3
|
+
const { cleanValue } = require("./parameters");
|
|
4
|
+
|
|
5
|
+
const POWER_ON_COMMANDS = ["startProgram", "start", "resumeProgram", "powerOn", "turnOn"];
|
|
6
|
+
const POWER_OFF_COMMANDS = ["stopProgram", "stop", "pauseProgram", "powerOff", "turnOff"];
|
|
7
|
+
const PRESET_COMMAND_ORDER = ["startProgram", "settings"];
|
|
8
|
+
|
|
9
|
+
class HonAirConditioner {
|
|
10
|
+
/**
|
|
11
|
+
* @param {any} appliance
|
|
12
|
+
* @param {any} [logger]
|
|
13
|
+
*/
|
|
14
|
+
constructor(appliance, logger = null) {
|
|
15
|
+
this.appliance = appliance;
|
|
16
|
+
this.logger = logger;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
get macAddress() {
|
|
20
|
+
return this.appliance.macAddress;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
get uniqueId() {
|
|
24
|
+
return this.appliance.uniqueId;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
get nickName() {
|
|
28
|
+
return this.appliance.nickName;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
get identifiers() {
|
|
32
|
+
return {
|
|
33
|
+
macAddress: this.macAddress,
|
|
34
|
+
uniqueId: this.uniqueId,
|
|
35
|
+
nickName: this.nickName
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async powerOn() {
|
|
40
|
+
const command = this.findCommand(POWER_ON_COMMANDS);
|
|
41
|
+
if (!command) {
|
|
42
|
+
return this.setPowerValue(true);
|
|
43
|
+
}
|
|
44
|
+
return command.send();
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async powerOff() {
|
|
48
|
+
const command = this.findCommand(POWER_OFF_COMMANDS);
|
|
49
|
+
if (!command) {
|
|
50
|
+
return this.setPowerValue(false);
|
|
51
|
+
}
|
|
52
|
+
return command.send();
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async setMode(value) {
|
|
56
|
+
return this.setControl(["machMode", "mode", "program", "prStr"], value);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async setTargetTemperature(value) {
|
|
60
|
+
return this.setControl(["tempSel", "targetTemperature", "temperature", "temp", "setTemperature"], value);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async setFanSpeed(value) {
|
|
64
|
+
return this.setControl(["windSpeed", "fanSpeed", "fan", "airFlow"], value);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
async setSwing(value) {
|
|
68
|
+
return this.setControl(["windDirectionVertical", "windDirection", "swing", "swingMode", "airDirection"], value);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
async sendCommand(name, params = {}) {
|
|
72
|
+
const command = this.appliance.commands[name];
|
|
73
|
+
if (!command) {
|
|
74
|
+
throw this.unsupported(`command:${name}`);
|
|
75
|
+
}
|
|
76
|
+
for (const [key, value] of Object.entries(params)) {
|
|
77
|
+
if (!command.parameters[key]) {
|
|
78
|
+
throw this.unsupported(`${name}.${key}`);
|
|
79
|
+
}
|
|
80
|
+
command.parameters[key].value = value;
|
|
81
|
+
}
|
|
82
|
+
return command.sendSpecific(Object.keys(params));
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
async applyPresetFile(filePath) {
|
|
86
|
+
const text = await fs.readFile(filePath, "utf8");
|
|
87
|
+
return this.applyPreset(JSON.parse(text));
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
async applyPreset(preset, name = "") {
|
|
91
|
+
if (!preset || typeof preset !== "object" || Array.isArray(preset)) {
|
|
92
|
+
throw this.unsupported("preset", { reason: "Preset must be a JSON object" });
|
|
93
|
+
}
|
|
94
|
+
const label = name || preset.mode || "custom";
|
|
95
|
+
const operation = this.logger?.start(`Turning on preset: "${label}"...`);
|
|
96
|
+
try {
|
|
97
|
+
const resolved = this.resolvePreset(preset);
|
|
98
|
+
this.logger?.log(`Selected preset command: "${resolved.command.name}" (${resolved.command.categoryName || "default"})`);
|
|
99
|
+
for (const { parameter, value } of resolved.params) {
|
|
100
|
+
parameter.value = value;
|
|
101
|
+
}
|
|
102
|
+
const result = await resolved.command.sendSpecific(resolved.params.map(({ key }) => key));
|
|
103
|
+
operation?.success(`Turned on preset: "${label}"`);
|
|
104
|
+
return result;
|
|
105
|
+
} catch (error) {
|
|
106
|
+
operation?.failure(`Preset failed: "${label}"`);
|
|
107
|
+
throw error;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
resolvePreset(preset) {
|
|
112
|
+
const errors = [];
|
|
113
|
+
for (const command of this.presetCommandCandidates(preset.mode)) {
|
|
114
|
+
const resolved = this.tryResolvePresetForCommand(command, preset);
|
|
115
|
+
if (resolved.ok) {
|
|
116
|
+
return resolved;
|
|
117
|
+
}
|
|
118
|
+
errors.push(resolved.error);
|
|
119
|
+
}
|
|
120
|
+
throw this.unsupported("preset", {
|
|
121
|
+
preset,
|
|
122
|
+
failures: errors,
|
|
123
|
+
available: this.capabilities()
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
presetCommandCandidates(mode) {
|
|
128
|
+
const ordered = [];
|
|
129
|
+
for (const name of PRESET_COMMAND_ORDER) {
|
|
130
|
+
if (this.appliance.commands[name]) {
|
|
131
|
+
ordered.push(this.appliance.commands[name]);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
for (const [name, command] of Object.entries(this.appliance.commands)) {
|
|
135
|
+
if (!PRESET_COMMAND_ORDER.includes(name)) {
|
|
136
|
+
ordered.push(command);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
return ordered.map((command) => this.commandForMode(command, mode));
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
commandForMode(command, mode) {
|
|
143
|
+
if (!mode || !command.categories) {
|
|
144
|
+
return command;
|
|
145
|
+
}
|
|
146
|
+
const wanted = cleanValue(mode);
|
|
147
|
+
if (command.categories[wanted]) {
|
|
148
|
+
return command.categories[wanted];
|
|
149
|
+
}
|
|
150
|
+
return (
|
|
151
|
+
Object.entries(command.categories).find(([category]) => cleanValue(category) === wanted)?.[1] ||
|
|
152
|
+
command
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
tryResolvePresetForCommand(command, preset) {
|
|
157
|
+
const params = [];
|
|
158
|
+
const seen = new Set();
|
|
159
|
+
for (const [field, value] of Object.entries(preset)) {
|
|
160
|
+
if (field === "mode") {
|
|
161
|
+
if (this.modeMatchesCommand(command, value)) {
|
|
162
|
+
continue;
|
|
163
|
+
}
|
|
164
|
+
const modeParam = this.resolveExactParameter(command, field, value);
|
|
165
|
+
if (modeParam) {
|
|
166
|
+
params.push(modeParam);
|
|
167
|
+
seen.add(modeParam.key);
|
|
168
|
+
continue;
|
|
169
|
+
}
|
|
170
|
+
return this.unresolved(command, field, value);
|
|
171
|
+
}
|
|
172
|
+
const resolved = this.resolveExactParameter(command, field, value);
|
|
173
|
+
if (!resolved) {
|
|
174
|
+
return this.unresolved(command, field, value);
|
|
175
|
+
}
|
|
176
|
+
if (!seen.has(resolved.key)) {
|
|
177
|
+
params.push(resolved);
|
|
178
|
+
seen.add(resolved.key);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
return { ok: true, command, params };
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
modeMatchesCommand(command, mode) {
|
|
185
|
+
return Boolean(command.categoryName && cleanValue(command.categoryName.split(".").pop()) === cleanValue(mode));
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
resolveExactParameter(command, key, value) {
|
|
189
|
+
const parameter = command.parameters[key];
|
|
190
|
+
if (!parameter || !this.canSetValue(parameter, value)) {
|
|
191
|
+
return null;
|
|
192
|
+
}
|
|
193
|
+
return { key, parameter, value };
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
canSetValue(parameter, value) {
|
|
197
|
+
try {
|
|
198
|
+
const original = parameter.value;
|
|
199
|
+
parameter.value = value;
|
|
200
|
+
parameter.value = original;
|
|
201
|
+
return true;
|
|
202
|
+
} catch {
|
|
203
|
+
return false;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
unresolved(command, field, value) {
|
|
208
|
+
return {
|
|
209
|
+
ok: false,
|
|
210
|
+
error: {
|
|
211
|
+
command: command.name,
|
|
212
|
+
category: command.categoryName,
|
|
213
|
+
presetField: field,
|
|
214
|
+
requestedValue: value,
|
|
215
|
+
parameters: Object.keys(command.parameters),
|
|
216
|
+
matches: this.describeParameter(command, field)
|
|
217
|
+
}
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
describeParameter(command, key) {
|
|
222
|
+
const keys = [key];
|
|
223
|
+
return Object.fromEntries(
|
|
224
|
+
keys
|
|
225
|
+
.filter((name) => command.parameters[name])
|
|
226
|
+
.map((name) => [
|
|
227
|
+
name,
|
|
228
|
+
{
|
|
229
|
+
typology: command.parameters[name].typology,
|
|
230
|
+
value: command.parameters[name].value,
|
|
231
|
+
values: command.parameters[name].values
|
|
232
|
+
}
|
|
233
|
+
])
|
|
234
|
+
);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
async setControl(keys, value) {
|
|
238
|
+
const keyList = Array.isArray(keys) ? keys : [keys];
|
|
239
|
+
for (const command of Object.values(this.appliance.commands)) {
|
|
240
|
+
for (const key of keyList) {
|
|
241
|
+
const parameter = command.parameters[key];
|
|
242
|
+
if (!parameter) {
|
|
243
|
+
continue;
|
|
244
|
+
}
|
|
245
|
+
parameter.value = value;
|
|
246
|
+
return command.sendSpecific([key]);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
throw this.unsupported(keyList[0]);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
async setPowerValue(on) {
|
|
253
|
+
const candidates = ["onOffStatus", "onOff", "power", "airConditionerStatus"];
|
|
254
|
+
for (const command of Object.values(this.appliance.commands)) {
|
|
255
|
+
for (const key of candidates) {
|
|
256
|
+
const parameter = command.parameters[key];
|
|
257
|
+
if (!parameter) {
|
|
258
|
+
continue;
|
|
259
|
+
}
|
|
260
|
+
const value = choosePowerValue(parameter, on);
|
|
261
|
+
if (value == null) {
|
|
262
|
+
continue;
|
|
263
|
+
}
|
|
264
|
+
parameter.value = value;
|
|
265
|
+
return command.sendSpecific([key]);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
throw this.unsupported(on ? "powerOn" : "powerOff");
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
findCommand(names) {
|
|
272
|
+
for (const name of names) {
|
|
273
|
+
if (this.appliance.commands[name]) {
|
|
274
|
+
return this.appliance.commands[name];
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
return null;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
unsupported(control, details = {}) {
|
|
281
|
+
return new UnsupportedControlError(`AC control is not available: ${control}`, {
|
|
282
|
+
control,
|
|
283
|
+
appliance: this.identifiers,
|
|
284
|
+
commands: Object.keys(this.appliance.commands),
|
|
285
|
+
...details
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
capabilities() {
|
|
290
|
+
return Object.fromEntries(
|
|
291
|
+
Object.entries(this.appliance.commands).map(([name, command]) => [
|
|
292
|
+
name,
|
|
293
|
+
describeCommand(command)
|
|
294
|
+
])
|
|
295
|
+
);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
function choosePowerValue(parameter, on) {
|
|
300
|
+
const values = parameter.values || [];
|
|
301
|
+
const preferred = on ? ["1", "on", "true", "start", "run"] : ["0", "off", "false", "stop"];
|
|
302
|
+
for (const value of preferred) {
|
|
303
|
+
if (values.includes(value)) {
|
|
304
|
+
return value;
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
if (values.length === 2) {
|
|
308
|
+
return on ? values[1] : values[0];
|
|
309
|
+
}
|
|
310
|
+
return null;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
function describeCommand(command) {
|
|
314
|
+
const result = {
|
|
315
|
+
category: command.categoryName || "",
|
|
316
|
+
categories: command.categories ? Object.keys(command.categories) : [],
|
|
317
|
+
parameters: {}
|
|
318
|
+
};
|
|
319
|
+
for (const [key, parameter] of Object.entries(command.parameters)) {
|
|
320
|
+
result.parameters[key] = {
|
|
321
|
+
group: parameter.group,
|
|
322
|
+
typology: parameter.typology,
|
|
323
|
+
value: parameter.value,
|
|
324
|
+
values: parameter.values
|
|
325
|
+
};
|
|
326
|
+
}
|
|
327
|
+
return result;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
module.exports = { HonAirConditioner };
|
package/src/api.js
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
const constants = require("./constants");
|
|
2
|
+
const { HonConnection, HonAnonymousConnection } = require("./connection");
|
|
3
|
+
const { HonApiError } = require("./errors");
|
|
4
|
+
|
|
5
|
+
class HonAPI {
|
|
6
|
+
constructor(auth, device, fetchImpl = globalThis.fetch, logger = null) {
|
|
7
|
+
this.auth = auth;
|
|
8
|
+
this.device = device;
|
|
9
|
+
this.logger = logger;
|
|
10
|
+
this.hon = new HonConnection(auth, fetchImpl);
|
|
11
|
+
this.anonymous = new HonAnonymousConnection(fetchImpl);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
async loadAppliances() {
|
|
15
|
+
const response = await this.hon.get(`${constants.API_URL}/commands/v1/appliance`);
|
|
16
|
+
const result = await response.json();
|
|
17
|
+
return result?.payload?.appliances || [];
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
async loadCommands(appliance) {
|
|
21
|
+
const params = new URLSearchParams({
|
|
22
|
+
applianceType: appliance.applianceType,
|
|
23
|
+
applianceModelId: appliance.applianceModelId,
|
|
24
|
+
macAddress: appliance.macAddress,
|
|
25
|
+
os: constants.OS,
|
|
26
|
+
appVersion: constants.APP_VERSION,
|
|
27
|
+
code: appliance.code
|
|
28
|
+
});
|
|
29
|
+
if (appliance.info.eepromId) {
|
|
30
|
+
params.set("firmwareId", appliance.info.eepromId);
|
|
31
|
+
}
|
|
32
|
+
if (appliance.info.fwVersion) {
|
|
33
|
+
params.set("fwVersion", appliance.info.fwVersion);
|
|
34
|
+
}
|
|
35
|
+
if (appliance.info.series) {
|
|
36
|
+
params.set("series", appliance.info.series);
|
|
37
|
+
}
|
|
38
|
+
const response = await this.hon.get(`${constants.API_URL}/commands/v1/retrieve?${params}`);
|
|
39
|
+
const payload = (await response.json())?.payload || {};
|
|
40
|
+
if (!payload || payload.resultCode !== "0") {
|
|
41
|
+
return {};
|
|
42
|
+
}
|
|
43
|
+
delete payload.resultCode;
|
|
44
|
+
return payload;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async loadCommandHistory(appliance) {
|
|
48
|
+
const response = await this.hon.get(`${constants.API_URL}/commands/v1/appliance/${appliance.macAddress}/history`);
|
|
49
|
+
return (await response.json())?.payload?.history || [];
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async loadFavourites(appliance) {
|
|
53
|
+
const response = await this.hon.get(`${constants.API_URL}/commands/v1/appliance/${appliance.macAddress}/favourite`);
|
|
54
|
+
return (await response.json())?.payload?.favourites || [];
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async loadAttributes(appliance) {
|
|
58
|
+
const params = new URLSearchParams({
|
|
59
|
+
macAddress: appliance.macAddress,
|
|
60
|
+
applianceType: appliance.applianceType,
|
|
61
|
+
category: "CYCLE"
|
|
62
|
+
});
|
|
63
|
+
const response = await this.hon.get(`${constants.API_URL}/commands/v1/context?${params}`);
|
|
64
|
+
return (await response.json())?.payload || {};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
async loadStatistics(appliance) {
|
|
68
|
+
const params = new URLSearchParams({
|
|
69
|
+
macAddress: appliance.macAddress,
|
|
70
|
+
applianceType: appliance.applianceType
|
|
71
|
+
});
|
|
72
|
+
const response = await this.hon.get(`${constants.API_URL}/commands/v1/statistics?${params}`);
|
|
73
|
+
return (await response.json())?.payload || {};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
async loadMaintenance(appliance) {
|
|
77
|
+
const params = new URLSearchParams({ macAddress: appliance.macAddress });
|
|
78
|
+
const response = await this.hon.get(`${constants.API_URL}/commands/v1/maintenance-cycle?${params}`);
|
|
79
|
+
return (await response.json())?.payload || {};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
async sendCommand(appliance, command, parameters, ancillaryParameters = {}, programName = "") {
|
|
83
|
+
const label = appliance.nickName || appliance.macAddress;
|
|
84
|
+
const operation = this.logger?.start(`Sending command: "${command}" to "${label}"...`);
|
|
85
|
+
const now = new Date();
|
|
86
|
+
const timestamp = now.toISOString().replace(/\.\d{3}Z$/, `.${String(now.getMilliseconds()).padStart(3, "0")}Z`);
|
|
87
|
+
const data = {
|
|
88
|
+
macAddress: appliance.macAddress,
|
|
89
|
+
timestamp,
|
|
90
|
+
commandName: command,
|
|
91
|
+
transactionId: `${appliance.macAddress}_${timestamp}`,
|
|
92
|
+
applianceOptions: appliance.options,
|
|
93
|
+
device: this.device.get(true),
|
|
94
|
+
attributes: {
|
|
95
|
+
channel: "mobileApp",
|
|
96
|
+
origin: "standardProgram",
|
|
97
|
+
energyLabel: "0"
|
|
98
|
+
},
|
|
99
|
+
ancillaryParameters,
|
|
100
|
+
parameters,
|
|
101
|
+
applianceType: appliance.applianceType
|
|
102
|
+
};
|
|
103
|
+
if (command === "startProgram" && programName) {
|
|
104
|
+
data.programName = programName.toUpperCase();
|
|
105
|
+
}
|
|
106
|
+
try {
|
|
107
|
+
const response = await this.hon.post(`${constants.API_URL}/commands/v1/send`, {
|
|
108
|
+
body: JSON.stringify(data)
|
|
109
|
+
});
|
|
110
|
+
const result = await response.json();
|
|
111
|
+
if (result?.payload?.resultCode === "0") {
|
|
112
|
+
operation?.success(`Command sent: "${command}" to "${label}"`);
|
|
113
|
+
return true;
|
|
114
|
+
}
|
|
115
|
+
throw new HonApiError("Can't send command", { payload: result, request: data });
|
|
116
|
+
} catch (error) {
|
|
117
|
+
operation?.failure(`Command failed: "${command}" to "${label}"`);
|
|
118
|
+
throw error;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
module.exports = { HonAPI };
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const IDENTIFIER_FIELDS = ["macAddress", "uniqueId", "nickName"];
|
|
4
|
+
const NAME_FIELDS = ["nickName", "applianceName", "applianceNickName", "applianceNickname", "name"];
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* @param {any} item
|
|
8
|
+
*/
|
|
9
|
+
function applianceIdentifiers(item) {
|
|
10
|
+
const info = item?.info && typeof item.info === "object" ? item.info : {};
|
|
11
|
+
const nickNames = uniqueStrings([
|
|
12
|
+
item?.nickName,
|
|
13
|
+
...NAME_FIELDS.map((field) => item?.[field]),
|
|
14
|
+
...NAME_FIELDS.map((field) => info[field])
|
|
15
|
+
]);
|
|
16
|
+
return {
|
|
17
|
+
macAddress: String(item?.macAddress || ""),
|
|
18
|
+
uniqueId: String(item?.uniqueId || ""),
|
|
19
|
+
nickName: nickNames[0] || "",
|
|
20
|
+
nickNames
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* @param {any} item
|
|
26
|
+
* @param {string} id
|
|
27
|
+
*/
|
|
28
|
+
function applianceIdentifierMatch(item, id) {
|
|
29
|
+
const identifiers = applianceIdentifiers(item);
|
|
30
|
+
for (const field of IDENTIFIER_FIELDS) {
|
|
31
|
+
if (identifiers[field] && identifiers[field] === id) {
|
|
32
|
+
return { matched: true, field };
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
if (identifiers.nickNames.some((name) => name === id)) {
|
|
36
|
+
return { matched: true, field: "nickName" };
|
|
37
|
+
}
|
|
38
|
+
if (identifiers.nickNames.some((name) => name.toLowerCase() === String(id).toLowerCase())) {
|
|
39
|
+
return { matched: true, field: "nickName", caseInsensitive: true };
|
|
40
|
+
}
|
|
41
|
+
return { matched: false, field: "" };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function uniqueStrings(values) {
|
|
45
|
+
return [...new Set(values.filter((value) => value != null).map((value) => String(value)).filter(Boolean))];
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* @template T
|
|
50
|
+
* @param {T[]} items
|
|
51
|
+
* @param {string} id
|
|
52
|
+
* @returns {{ item: T, field: string, caseInsensitive: boolean }[]}
|
|
53
|
+
*/
|
|
54
|
+
function findApplianceIdentifierMatches(items, id) {
|
|
55
|
+
if (!id) {
|
|
56
|
+
return [];
|
|
57
|
+
}
|
|
58
|
+
return items
|
|
59
|
+
.map((item) => {
|
|
60
|
+
const match = applianceIdentifierMatch(/** @type {any} */ (item), id);
|
|
61
|
+
return { item, field: match.field, caseInsensitive: Boolean(match.caseInsensitive), matched: match.matched };
|
|
62
|
+
})
|
|
63
|
+
.filter((match) => match.matched)
|
|
64
|
+
.map(({ item, field, caseInsensitive }) => ({ item, field, caseInsensitive }));
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
module.exports = {
|
|
68
|
+
applianceIdentifierMatch,
|
|
69
|
+
applianceIdentifiers,
|
|
70
|
+
findApplianceIdentifierMatches
|
|
71
|
+
};
|