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.
Files changed (43) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +144 -0
  3. package/bin/node-hon.js +94 -0
  4. package/cli/ac_apply_preset.js +36 -0
  5. package/cli/ac_generate_preset.js +80 -0
  6. package/cli/ac_turn_off.js +20 -0
  7. package/cli/ac_turn_on.js +21 -0
  8. package/cli/config.js +84 -0
  9. package/cli/purge_cache.js +16 -0
  10. package/cli/show_my_ac_capabilities.js +36 -0
  11. package/cli/show_my_ac_devices.js +19 -0
  12. package/config_example.js +10 -0
  13. package/package.json +41 -0
  14. package/presets/preset_auto.json +7 -0
  15. package/presets/preset_cool.json +8 -0
  16. package/presets/preset_dry.json +6 -0
  17. package/presets/preset_fan.json +8 -0
  18. package/src/ac.js +330 -0
  19. package/src/api.js +123 -0
  20. package/src/appliance-identity.js +71 -0
  21. package/src/appliance.js +282 -0
  22. package/src/auth.js +424 -0
  23. package/src/caching/appliance-cache.js +71 -0
  24. package/src/caching/session-store.js +47 -0
  25. package/src/client.js +253 -0
  26. package/src/command.js +314 -0
  27. package/src/connection.js +73 -0
  28. package/src/constants.js +17 -0
  29. package/src/device.js +29 -0
  30. package/src/errors.js +38 -0
  31. package/src/index.js +25 -0
  32. package/src/lib/config.js +22 -0
  33. package/src/lib/cookie-jar.js +36 -0
  34. package/src/lib/logger.js +56 -0
  35. package/src/lib-cli/_format.js +33 -0
  36. package/src/lib-cli/_get-ac-client.js +29 -0
  37. package/src/lib-cli/_get-client.js +25 -0
  38. package/src/lib-cli/_prompt.js +61 -0
  39. package/src/lib-cli/_run.js +18 -0
  40. package/src/lib-cli/_select-ac.js +36 -0
  41. package/src/parameters.js +261 -0
  42. package/src/preset-generator.js +171 -0
  43. package/types/global.ts +19 -0
@@ -0,0 +1,71 @@
1
+ const fs = require("node:fs/promises");
2
+ const path = require("node:path");
3
+ const { findApplianceIdentifierMatches } = require("../appliance-identity");
4
+
5
+ const CACHE_VERSION = 1;
6
+
7
+ class ApplianceCache {
8
+ constructor(filePath) {
9
+ this.filePath = path.resolve(filePath || "./.hon-appliance-cache.json");
10
+ }
11
+
12
+ async read() {
13
+ try {
14
+ const text = await fs.readFile(this.filePath, "utf8");
15
+ const data = JSON.parse(text);
16
+ if (data.version !== CACHE_VERSION || !Array.isArray(data.appliances)) {
17
+ return emptyCache();
18
+ }
19
+ return data;
20
+ } catch (error) {
21
+ const fileError = /** @type {NodeJS.ErrnoException} */ (error);
22
+ if (fileError && fileError.code === "ENOENT") {
23
+ return emptyCache();
24
+ }
25
+ throw error;
26
+ }
27
+ }
28
+
29
+ async find(id) {
30
+ const matches = await this.findAll(id);
31
+ return matches[0] || null;
32
+ }
33
+
34
+ async findAll(id) {
35
+ const cache = await this.read();
36
+ return findApplianceIdentifierMatches(cache.appliances, id).map((match) => match.item);
37
+ }
38
+
39
+ async upsert(record) {
40
+ let cache;
41
+ try {
42
+ cache = await this.read();
43
+ } catch {
44
+ cache = emptyCache();
45
+ }
46
+ const next = cache.appliances.filter((item) => !recordMatches(item, record.uniqueId) && !recordMatches(item, record.macAddress));
47
+ next.push(record);
48
+ await this.write({ version: CACHE_VERSION, appliances: next });
49
+ }
50
+
51
+ async write(data) {
52
+ await fs.mkdir(path.dirname(this.filePath), { recursive: true });
53
+ const tmp = `${this.filePath}.${process.pid}.tmp`;
54
+ await fs.writeFile(tmp, `${JSON.stringify(data, null, 2)}\n`, "utf8");
55
+ await fs.rename(tmp, this.filePath);
56
+ }
57
+ }
58
+
59
+ function emptyCache() {
60
+ return { version: CACHE_VERSION, appliances: [] };
61
+ }
62
+
63
+ /**
64
+ * @param {{ macAddress?: string, uniqueId?: string, nickName?: string } | null | undefined} record
65
+ * @param {string} id
66
+ */
67
+ function recordMatches(record, id) {
68
+ return findApplianceIdentifierMatches(record ? [record] : [], id).length > 0;
69
+ }
70
+
71
+ module.exports = { ApplianceCache, CACHE_VERSION, recordMatches };
@@ -0,0 +1,47 @@
1
+ const fs = require("node:fs/promises");
2
+ const path = require("node:path");
3
+
4
+ class SessionStore {
5
+ constructor(filePath) {
6
+ this.filePath = filePath ? path.resolve(filePath) : "";
7
+ }
8
+
9
+ async read() {
10
+ if (!this.filePath) {
11
+ return null;
12
+ }
13
+ try {
14
+ const text = await fs.readFile(this.filePath, "utf8");
15
+ return JSON.parse(text);
16
+ } catch (error) {
17
+ const fileError = /** @type {NodeJS.ErrnoException} */ (error);
18
+ if (fileError && fileError.code === "ENOENT") {
19
+ return null;
20
+ }
21
+ throw error;
22
+ }
23
+ }
24
+
25
+ async write(data) {
26
+ if (!this.filePath) {
27
+ return;
28
+ }
29
+ await fs.mkdir(path.dirname(this.filePath), { recursive: true });
30
+ const tmp = `${this.filePath}.${process.pid}.tmp`;
31
+ await fs.writeFile(tmp, `${JSON.stringify(data, null, 2)}\n`, "utf8");
32
+ await fs.rename(tmp, this.filePath);
33
+ }
34
+ }
35
+
36
+ function isExpired(expiresAt, warningMs = 0) {
37
+ if (!expiresAt) {
38
+ return true;
39
+ }
40
+ const timestamp = Date.parse(expiresAt);
41
+ if (!Number.isFinite(timestamp)) {
42
+ return true;
43
+ }
44
+ return Date.now() + warningMs >= timestamp;
45
+ }
46
+
47
+ module.exports = { SessionStore, isExpired };
package/src/client.js ADDED
@@ -0,0 +1,253 @@
1
+ const path = require("node:path");
2
+ const { HonDevice } = require("./device");
3
+ const { SessionStore } = require("./caching/session-store");
4
+ const { HonAuth } = require("./auth");
5
+ const { HonAPI } = require("./api");
6
+ const { HonAppliance, isAirConditioner } = require("./appliance");
7
+ const { HonAirConditioner } = require("./ac");
8
+ const { ApplianceNotFoundError } = require("./errors");
9
+ const { DebugLogger } = require("./lib/logger");
10
+ const { ApplianceCache } = require("./caching/appliance-cache");
11
+ const { findApplianceIdentifierMatches } = require("./appliance-identity");
12
+
13
+ class HonClient {
14
+ /**
15
+ * @param {Partial<import("../types/global").ProjectConfig> & { fetch?: typeof fetch, logger?: any }} [config]
16
+ */
17
+ constructor(config = {}) {
18
+ this.config = config;
19
+ this.fetch = config.fetch || globalThis.fetch;
20
+ this.logger =
21
+ config.logger || new DebugLogger({ enabled: Boolean(config.debug) });
22
+ this.device = new HonDevice(config.mobileId);
23
+ const sessionFile = config.sessionFile
24
+ ? path.resolve(config.sessionFile)
25
+ : "";
26
+
27
+ /** @type {SessionStore | null} */
28
+ this.sessionStore = sessionFile ? new SessionStore(sessionFile) : null;
29
+ this.applianceCache = new ApplianceCache(config.applianceCacheFile || "./cache/.hon-appliance-cache.json");
30
+ this.forceApplianceCacheRefresh = Boolean(config.forceApplianceCacheRefresh);
31
+ this.auth = new HonAuth({
32
+ email: config.email,
33
+ password: config.password,
34
+ device: this.device,
35
+ fetchImpl: this.fetch,
36
+ sessionStore: this.sessionStore,
37
+ debug: Boolean(config.debug),
38
+ logger: this.logger,
39
+ });
40
+ this.api = new HonAPI(this.auth, this.device, this.fetch, this.logger);
41
+ this.appliances = [];
42
+ }
43
+
44
+ async create() {
45
+ const operation = this.logger.start("Creating hOn client...");
46
+ try {
47
+ await this.login();
48
+ // await this.setup();
49
+ operation.success("hOn client ready");
50
+ } catch (error) {
51
+ operation.failure("hOn client setup failed");
52
+ throw error;
53
+ }
54
+ return this;
55
+ }
56
+
57
+ async login() {
58
+ const operation = this.logger.start("Trying to login...");
59
+ try {
60
+ await this.auth.initialize();
61
+ operation.success("Login success");
62
+ } catch (error) {
63
+ operation.failure("Login failed");
64
+ throw error;
65
+ }
66
+ return this;
67
+ }
68
+
69
+ async refresh() {
70
+ const operation = this.logger.start("Refreshing login...");
71
+ try {
72
+ const result = await this.auth.refresh();
73
+ operation.success(result ? "Refresh success" : "Refresh skipped");
74
+ return result;
75
+ } catch (error) {
76
+ operation.failure("Refresh failed");
77
+ throw error;
78
+ }
79
+ }
80
+
81
+ async setupOne(id) {
82
+ const appliances = await this.api.loadAppliances();
83
+ const airConditioners = appliances
84
+ .map((applianceData) => new HonAppliance(this.api, applianceData))
85
+ .filter(isAirConditioner);
86
+ const matches = findApplianceIdentifierMatches(airConditioners, id);
87
+ if (matches.length > 1) {
88
+ throw new ApplianceNotFoundError(`AC_ID matches multiple air conditioners: ${id}`, {
89
+ id,
90
+ matches: matches.map(({ item }) => item.toCacheRecord())
91
+ });
92
+ }
93
+ if (matches.length === 1) {
94
+ return this.#createApplianceSetup(matches[0].item);
95
+ }
96
+ }
97
+
98
+ async getAirConditionerByIdCached(id) {
99
+ if (!id) {
100
+ throw new ApplianceNotFoundError("AC_ID is required", { available: [] });
101
+ }
102
+ if (!this.forceApplianceCacheRefresh) {
103
+ const operation = this.logger.start(`Loading AC from cache: "${id}"...`);
104
+ try {
105
+ const records = await this.applianceCache.findAll(id);
106
+ if (records.length > 1) {
107
+ throw new ApplianceNotFoundError(`AC_ID matches multiple cached air conditioners: ${id}`, {
108
+ id,
109
+ matches: records.map((record) => ({
110
+ macAddress: record.macAddress,
111
+ uniqueId: record.uniqueId,
112
+ nickName: record.nickName
113
+ }))
114
+ });
115
+ }
116
+ if (records.length === 1) {
117
+ const record = records[0];
118
+ const appliance = HonAppliance.fromCacheRecord(this.api, record);
119
+ operation.success(`Loaded AC from cache: "${id}"`);
120
+ return new HonAirConditioner(appliance, this.logger);
121
+ }
122
+ operation.failure(`AC cache miss: "${id}"`);
123
+ } catch (error) {
124
+ operation.failure(`AC cache failed: "${id}"`);
125
+ if (error instanceof ApplianceNotFoundError) {
126
+ throw error;
127
+ }
128
+ }
129
+ }
130
+
131
+ const refresh = this.logger.start(`Refreshing AC cache: "${id}"...`);
132
+ const appliance = await this.setupOne(id);
133
+ if (!appliance) {
134
+ refresh.failure(`AC cache refresh failed: "${id}"`);
135
+ throw new ApplianceNotFoundError(`No air conditioner found for AC_ID: ${id}`, { id, available: [] });
136
+ }
137
+ await this.applianceCache.upsert(appliance.toCacheRecord());
138
+ refresh.success(`AC cache refreshed: "${id}"`);
139
+ return new HonAirConditioner(appliance, this.logger);
140
+ }
141
+
142
+ async setup() {
143
+ const operation = this.logger.start("Loading appliances...");
144
+
145
+ try {
146
+ const appliances = await this.api.loadAppliances();
147
+ for (const applianceData of appliances) {
148
+ const zones = Number(applianceData.zone || 0);
149
+ if (zones > 1) {
150
+ for (let zone = 1; zone <= zones; zone += 1) {
151
+ await this.createAppliance({ ...applianceData }, zone);
152
+ }
153
+ }
154
+ await this.createAppliance(applianceData);
155
+ }
156
+ operation.success(`Loaded ${this.appliances.length} appliance(s)`);
157
+ } catch (error) {
158
+ operation.failure("Loading appliances failed");
159
+ throw error;
160
+ }
161
+ }
162
+
163
+ /**
164
+ * @param {HonAppliance} appliance
165
+ */
166
+ async #createApplianceSetup(appliance) {
167
+ const label = appliance.nickName || appliance.macAddress;
168
+ if (!this.appliances) this.appliances = [];
169
+ const operation = this.logger.start(`Loading appliance: "${label}"...`);
170
+
171
+ try {
172
+ const promises = [
173
+ appliance.loadCommands(),
174
+ appliance.loadAttributes(),
175
+ appliance.loadStatistics(),
176
+ ];
177
+ await Promise.all(promises);
178
+
179
+ this.appliances.push(appliance);
180
+ operation.success(`Loaded appliance: "${label}"`);
181
+
182
+ return appliance;
183
+ } catch (error) {
184
+ operation.failure(`Loading appliance failed: "${label}"`);
185
+ throw error;
186
+ }
187
+ }
188
+
189
+ async createAppliance(applianceData, zone = 0) {
190
+ const appliance = new HonAppliance(this.api, applianceData, zone);
191
+ if (!appliance.macAddress) {
192
+ return;
193
+ }
194
+
195
+ return this.#createApplianceSetup(appliance);
196
+ }
197
+
198
+ async getAppliances() {
199
+ if (!this.appliances.length) {
200
+ await this.setup();
201
+ }
202
+ return this.appliances;
203
+ }
204
+
205
+ async getAirConditioners() {
206
+ const appliances = await this.getAppliances();
207
+ return appliances
208
+ .filter(isAirConditioner)
209
+ .map((appliance) => new HonAirConditioner(appliance, this.logger));
210
+ }
211
+
212
+ async getAirConditionerByIdFast(id) {
213
+ const appliance = await this.setupOne(id);
214
+ if (!appliance) return;
215
+
216
+ return new HonAirConditioner(appliance, this.logger);
217
+ }
218
+
219
+ async getAirConditionerById(id) {
220
+ const airConditioners = await this.getAirConditioners();
221
+ if (!id) {
222
+ throw new ApplianceNotFoundError("AC_ID is required", {
223
+ available: airConditioners.map((ac) => ac.identifiers),
224
+ });
225
+ }
226
+ const matches = findApplianceIdentifierMatches(airConditioners, id);
227
+ if (matches.length === 1) {
228
+ return matches[0].item;
229
+ }
230
+ if (matches.length > 1) {
231
+ throw new ApplianceNotFoundError(
232
+ `AC_ID matches multiple air conditioners`,
233
+ {
234
+ id,
235
+ matches: matches.map(({ item }) => item.identifiers),
236
+ },
237
+ );
238
+ }
239
+ throw new ApplianceNotFoundError(
240
+ `No air conditioner found for AC_ID: ${id}`,
241
+ {
242
+ id,
243
+ available: airConditioners.map((ac) => ac.identifiers),
244
+ },
245
+ );
246
+ }
247
+
248
+ async close() {
249
+ return undefined;
250
+ }
251
+ }
252
+
253
+ module.exports = { HonClient };
package/src/command.js ADDED
@@ -0,0 +1,314 @@
1
+ const { HonApiError } = require("./errors");
2
+ const { HonParameterFixed, HonParameterProgram, createParameter } = require("./parameters");
3
+
4
+ class HonCommand {
5
+ /**
6
+ * @param {string} name
7
+ * @param {Record<string, any>} attributes
8
+ * @param {any} appliance
9
+ * @param {any} [categories]
10
+ * @param {string} [categoryName]
11
+ */
12
+ constructor(name, attributes, appliance, categories = null, categoryName = "") {
13
+ this.name = name;
14
+ this.appliance = appliance;
15
+ this.categories = categories || null;
16
+ this.categoryName = categoryName;
17
+ this.parameters = {};
18
+ this.data = {};
19
+ const copy = { ...attributes };
20
+ delete copy.description;
21
+ delete copy.protocolType;
22
+ this.loadParameters(copy);
23
+ }
24
+
25
+ loadParameters(attributes) {
26
+ for (const [group, items] of Object.entries(attributes)) {
27
+ if (!items || typeof items !== "object" || Array.isArray(items)) {
28
+ this.data[group] = items;
29
+ continue;
30
+ }
31
+ for (const [name, data] of Object.entries(items)) {
32
+ this.createParameter(data, name, group);
33
+ }
34
+ }
35
+ }
36
+
37
+ createParameter(data, name, group) {
38
+ if (name === "zoneMap" && this.appliance.zone) {
39
+ data.default = this.appliance.zone;
40
+ }
41
+ if (data.category === "rule") {
42
+ return;
43
+ }
44
+ const parameter = createParameter(name, data, group);
45
+ if (!parameter) {
46
+ this.data[name] = data;
47
+ return;
48
+ }
49
+ this.parameters[name] = parameter;
50
+ if (this.categoryName) {
51
+ const key = this.categoryName.includes("PROGRAM") ? "program" : "category";
52
+ this.parameters[key] = new HonParameterProgram(key, this, "custom");
53
+ }
54
+ }
55
+
56
+ get settings() {
57
+ return this.parameters;
58
+ }
59
+
60
+ get parameterGroups() {
61
+ const result = {};
62
+ for (const [name, parameter] of Object.entries(this.parameters)) {
63
+ if (!result[parameter.group]) {
64
+ result[parameter.group] = {};
65
+ }
66
+ result[parameter.group][name] = parameter.internValue;
67
+ }
68
+ return result;
69
+ }
70
+
71
+ get mandatoryParameterGroups() {
72
+ const result = {};
73
+ for (const [name, parameter] of Object.entries(this.parameters)) {
74
+ if (!parameter.mandatory) {
75
+ continue;
76
+ }
77
+ if (!result[parameter.group]) {
78
+ result[parameter.group] = {};
79
+ }
80
+ result[parameter.group][name] = parameter.internValue;
81
+ }
82
+ return result;
83
+ }
84
+
85
+ get parameterValue() {
86
+ return Object.fromEntries(Object.entries(this.parameters).map(([name, parameter]) => [name, parameter.value]));
87
+ }
88
+
89
+ get category() {
90
+ return this.categoryName;
91
+ }
92
+
93
+ set category(category) {
94
+ if (this.categories && this.categories[category]) {
95
+ this.appliance.commands[this.name] = this.categories[category];
96
+ }
97
+ }
98
+
99
+ get settingKeys() {
100
+ const result = new Set();
101
+ for (const command of Object.values(this.categories || { _: this })) {
102
+ for (const key of Object.keys(command.parameters)) {
103
+ result.add(key);
104
+ }
105
+ }
106
+ return [...result];
107
+ }
108
+
109
+ get availableSettings() {
110
+ const result = {};
111
+ for (const command of Object.values(this.categories || { _: this })) {
112
+ for (const [name, parameter] of Object.entries(command.parameters)) {
113
+ if (!result[name] || parameter.values.length > result[name].values.length) {
114
+ result[name] = parameter;
115
+ }
116
+ }
117
+ }
118
+ return result;
119
+ }
120
+
121
+ async send(onlyMandatory = false) {
122
+ const groupedParams = onlyMandatory ? this.mandatoryParameterGroups : this.parameterGroups;
123
+ return this.sendParameters(groupedParams.parameters || {});
124
+ }
125
+
126
+ async sendSpecific(paramNames) {
127
+ const params = {};
128
+ for (const [key, parameter] of Object.entries(this.parameters)) {
129
+ if (paramNames.includes(key) || parameter.mandatory) {
130
+ params[key] = parameter.value;
131
+ }
132
+ }
133
+ return this.sendParameters(params);
134
+ }
135
+
136
+ async sendParameters(params) {
137
+ const ancillaryParams = { ...(this.parameterGroups.ancillaryParameters || {}) };
138
+ delete ancillaryParams.programRules;
139
+ if (params.prStr) {
140
+ params.prStr = this.categoryName.toUpperCase();
141
+ }
142
+ this.appliance.syncCommandToParams(this.name);
143
+ const result = await this.appliance.api.sendCommand(this.appliance, this.name, params, ancillaryParams, this.categoryName);
144
+ if (!result) {
145
+ throw new HonApiError("Can't send command", { command: this.name, params });
146
+ }
147
+ return result;
148
+ }
149
+
150
+ reset() {
151
+ for (const parameter of Object.values(this.parameters)) {
152
+ parameter.reset();
153
+ }
154
+ }
155
+ }
156
+
157
+ function isCommand(data) {
158
+ return Boolean(data && typeof data === "object" && data.description !== undefined && data.protocolType !== undefined);
159
+ }
160
+
161
+ function cleanName(category) {
162
+ if (category.includes("PROGRAM")) {
163
+ return category.split(".").pop().toLowerCase();
164
+ }
165
+ return category;
166
+ }
167
+
168
+ class HonCommandLoader {
169
+ constructor(api, appliance) {
170
+ this.api = api;
171
+ this.appliance = appliance;
172
+ this.apiCommands = {};
173
+ this.favourites = [];
174
+ this.commandHistory = [];
175
+ this.commands = {};
176
+ this.applianceData = {};
177
+ this.additionalData = {};
178
+ this.rawCommands = {};
179
+ }
180
+
181
+ async loadCommands() {
182
+ const [commands, favourites, history] = await Promise.all([
183
+ this.api.loadCommands(this.appliance),
184
+ this.api.loadFavourites(this.appliance),
185
+ this.api.loadCommandHistory(this.appliance)
186
+ ]);
187
+ this.apiCommands = { ...(commands || {}) };
188
+ this.rawCommands = { ...(commands || {}) };
189
+ this.favourites = favourites || [];
190
+ this.commandHistory = history || [];
191
+ this.applianceData = this.apiCommands.applianceModel || {};
192
+ delete this.apiCommands.applianceModel;
193
+ this.getCommands();
194
+ this.addFavourites();
195
+ this.recoverLastCommandStates();
196
+ }
197
+
198
+ loadFromCache(cacheData) {
199
+ this.apiCommands = { ...(cacheData.commands || {}) };
200
+ this.rawCommands = { ...(cacheData.commands || {}) };
201
+ this.favourites = [];
202
+ this.commandHistory = [];
203
+ this.applianceData = cacheData.applianceModel || {};
204
+ this.additionalData = cacheData.additionalData || {};
205
+ this.getCommands();
206
+ }
207
+
208
+ getCommands() {
209
+ const commands = [];
210
+ for (const [name, data] of Object.entries(this.apiCommands)) {
211
+ const command = this.parseCommand(data, name);
212
+ if (command) {
213
+ commands.push(command);
214
+ }
215
+ }
216
+ this.commands = Object.fromEntries(commands.map((command) => [command.name, command]));
217
+ }
218
+
219
+ /**
220
+ * @param {any} data
221
+ * @param {string} commandName
222
+ * @param {any} [categories]
223
+ * @param {string} [categoryName]
224
+ */
225
+ parseCommand(data, commandName, categories = null, categoryName = "") {
226
+ if (!data || typeof data !== "object" || Array.isArray(data)) {
227
+ this.additionalData[commandName] = data;
228
+ return null;
229
+ }
230
+ if (isCommand(data)) {
231
+ return new HonCommand(commandName, data, this.appliance, categories, categoryName);
232
+ }
233
+ return this.parseCategories(data, commandName);
234
+ }
235
+
236
+ parseCategories(data, commandName) {
237
+ const categories = {};
238
+ for (const [category, value] of Object.entries(data)) {
239
+ const command = this.parseCommand(value, commandName, categories, category);
240
+ if (command) {
241
+ categories[cleanName(category)] = command;
242
+ }
243
+ }
244
+ if (!Object.keys(categories).length) {
245
+ return null;
246
+ }
247
+ return categories.setParameters || Object.values(categories)[0];
248
+ }
249
+
250
+ recoverLastCommandStates() {
251
+ for (const [name, baseCommand] of Object.entries(this.commands)) {
252
+ const last = this.commandHistory.find((item) => item?.command?.commandName === name);
253
+ if (!last) {
254
+ continue;
255
+ }
256
+ const parameters = { ...(last.command.parameters || {}) };
257
+ let command = baseCommand;
258
+ const program = parameters.program;
259
+ const category = parameters.category;
260
+ delete parameters.program;
261
+ delete parameters.category;
262
+ if (program && command.categories) {
263
+ command.category = cleanName(program);
264
+ command = this.commands[name];
265
+ } else if (category && command.categories) {
266
+ command.category = category;
267
+ command = this.commands[name];
268
+ }
269
+ for (const [key, value] of Object.entries(parameters)) {
270
+ if (command.settings[key]) {
271
+ try {
272
+ command.settings[key].value = value;
273
+ } catch {
274
+ // keep discovered default
275
+ }
276
+ }
277
+ }
278
+ }
279
+ }
280
+
281
+ addFavourites() {
282
+ for (const favourite of this.favourites) {
283
+ const commandName = favourite?.command?.commandName || "";
284
+ const programName = cleanName(favourite?.command?.programName || "");
285
+ const base = this.commands[commandName]?.categories?.[programName];
286
+ if (!base) {
287
+ continue;
288
+ }
289
+ const clone = Object.create(Object.getPrototypeOf(base));
290
+ Object.assign(clone, base, { parameters: { ...base.parameters } });
291
+ for (const value of Object.values(favourite.command || {})) {
292
+ if (!value || typeof value === "string") {
293
+ continue;
294
+ }
295
+ for (const [key, paramValue] of Object.entries(value)) {
296
+ if (clone.parameters[key]) {
297
+ try {
298
+ clone.parameters[key].value = paramValue;
299
+ } catch {
300
+ // keep default
301
+ }
302
+ }
303
+ }
304
+ }
305
+ clone.parameters.favourite = new HonParameterFixed("favourite", { fixedValue: "1" }, "custom");
306
+ if (clone.parameters.program && typeof clone.parameters.program.setValue === "function") {
307
+ clone.parameters.program.setValue(favourite.favouriteName);
308
+ }
309
+ this.commands[commandName].categories[favourite.favouriteName] = clone;
310
+ }
311
+ }
312
+ }
313
+
314
+ module.exports = { HonCommand, HonCommandLoader, isCommand, cleanName };