homebridge-ac-freedom 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.
@@ -0,0 +1,131 @@
1
+ {
2
+ "pluginAlias": "AcFreedom",
3
+ "pluginType": "platform",
4
+ "singular": false,
5
+ "headerDisplay": "AUX Air Conditioner control via Broadlink (local) or AUX Cloud API.",
6
+ "schema": {
7
+ "type": "object",
8
+ "properties": {
9
+ "name": {
10
+ "title": "Platform Name",
11
+ "type": "string",
12
+ "default": "AC Freedom",
13
+ "required": true
14
+ },
15
+ "devices": {
16
+ "title": "Devices",
17
+ "type": "array",
18
+ "items": {
19
+ "type": "object",
20
+ "properties": {
21
+ "name": {
22
+ "title": "Device Name",
23
+ "type": "string",
24
+ "default": "AC Freedom",
25
+ "required": true
26
+ },
27
+ "connection": {
28
+ "title": "Connection Mode",
29
+ "type": "string",
30
+ "default": "cloud",
31
+ "oneOf": [
32
+ { "title": "Cloud (AUX Cloud API)", "enum": ["cloud"] },
33
+ { "title": "Local (Broadlink UDP)", "enum": ["local"] }
34
+ ],
35
+ "required": true
36
+ },
37
+ "cloud": {
38
+ "title": "Cloud Settings",
39
+ "type": "object",
40
+ "condition": { "functionBody": "return model.devices[arrayIndices[0]].connection === 'cloud';" },
41
+ "properties": {
42
+ "email": {
43
+ "title": "Email",
44
+ "type": "string",
45
+ "required": true
46
+ },
47
+ "password": {
48
+ "title": "Password",
49
+ "type": "string",
50
+ "required": true
51
+ },
52
+ "region": {
53
+ "title": "Region",
54
+ "type": "string",
55
+ "default": "eu",
56
+ "oneOf": [
57
+ { "title": "Europe", "enum": ["eu"] },
58
+ { "title": "USA", "enum": ["usa"] },
59
+ { "title": "China", "enum": ["cn"] },
60
+ { "title": "Russia", "enum": ["rus"] }
61
+ ]
62
+ },
63
+ "deviceId": {
64
+ "title": "Device ID (endpointId, leave empty for auto-detect)",
65
+ "type": "string"
66
+ }
67
+ }
68
+ },
69
+ "local": {
70
+ "title": "Local Settings",
71
+ "type": "object",
72
+ "condition": { "functionBody": "return model.devices[arrayIndices[0]].connection === 'local';" },
73
+ "properties": {
74
+ "ip": {
75
+ "title": "IP Address",
76
+ "type": "string",
77
+ "required": true
78
+ },
79
+ "mac": {
80
+ "title": "MAC Address (AA:BB:CC:DD:EE:FF)",
81
+ "type": "string",
82
+ "required": true
83
+ }
84
+ }
85
+ },
86
+ "hvacModes": {
87
+ "title": "Enabled HVAC Modes",
88
+ "type": "object",
89
+ "properties": {
90
+ "auto": { "title": "Auto", "type": "boolean", "default": true },
91
+ "cool": { "title": "Cool", "type": "boolean", "default": true },
92
+ "heat": { "title": "Heat", "type": "boolean", "default": true }
93
+ }
94
+ },
95
+ "presets": {
96
+ "title": "Enabled Preset Modes (shown as switches in climate card)",
97
+ "type": "object",
98
+ "properties": {
99
+ "sleep": { "title": "Sleep Mode", "type": "boolean", "default": true },
100
+ "health": { "title": "Health / Ionizer", "type": "boolean", "default": true },
101
+ "eco": { "title": "Eco / Mildew Prevention", "type": "boolean", "default": true },
102
+ "clean": { "title": "Self Clean", "type": "boolean", "default": true }
103
+ }
104
+ },
105
+ "showDisplay": {
106
+ "title": "Show Display Switch",
107
+ "type": "boolean",
108
+ "default": true
109
+ },
110
+ "pollInterval": {
111
+ "title": "Poll Interval (seconds)",
112
+ "type": "integer",
113
+ "default": 30,
114
+ "minimum": 5,
115
+ "maximum": 300
116
+ },
117
+ "tempStep": {
118
+ "title": "Temperature Step",
119
+ "type": "number",
120
+ "default": 0.5,
121
+ "oneOf": [
122
+ { "title": "0.5 °C", "enum": [0.5] },
123
+ { "title": "1 °C", "enum": [1] }
124
+ ]
125
+ }
126
+ }
127
+ }
128
+ }
129
+ }
130
+ }
131
+ }
package/index.js ADDED
@@ -0,0 +1,15 @@
1
+ /**
2
+ * homebridge-ac-freedom
3
+ *
4
+ * Homebridge plugin for AUX air conditioners via Broadlink (local) and AUX Cloud API.
5
+ * Exposes HeaterCooler with linked Switch services for presets inside the climate card.
6
+ */
7
+
8
+ const { AcFreedomPlatform } = require('./src/platform');
9
+
10
+ const PLUGIN_NAME = 'homebridge-ac-freedom';
11
+ const PLATFORM_NAME = 'AcFreedom';
12
+
13
+ module.exports = (api) => {
14
+ api.registerPlatform(PLUGIN_NAME, PLATFORM_NAME, AcFreedomPlatform);
15
+ };
package/package.json ADDED
@@ -0,0 +1,31 @@
1
+ {
2
+ "name": "homebridge-ac-freedom",
3
+ "version": "1.0.0",
4
+ "description": "Homebridge plugin for AUX air conditioners via Broadlink (local UDP) and AUX Cloud API",
5
+ "main": "index.js",
6
+ "scripts": {
7
+ "lint": "eslint src/"
8
+ },
9
+ "keywords": [
10
+ "homebridge-plugin",
11
+ "homebridge",
12
+ "aux",
13
+ "broadlink",
14
+ "air-conditioner",
15
+ "hvac",
16
+ "climate"
17
+ ],
18
+ "author": "azadaydinli",
19
+ "license": "MIT",
20
+ "repository": {
21
+ "type": "git",
22
+ "url": "https://github.com/azadaydinli/homebridge-ac-freedom.git"
23
+ },
24
+ "engines": {
25
+ "homebridge": ">=1.6.0",
26
+ "node": ">=18.0.0"
27
+ },
28
+ "dependencies": {},
29
+ "devDependencies": {},
30
+ "homepage": "https://github.com/azadaydinli/homebridge-ac-freedom"
31
+ }
@@ -0,0 +1,481 @@
1
+ /**
2
+ * AcFreedomAccessory
3
+ *
4
+ * Exposes a HeaterCooler service with linked Switch services for preset modes.
5
+ * Presets (sleep, health, eco, clean) and display appear as toggles inside
6
+ * the climate card in HomeKit.
7
+ */
8
+
9
+ 'use strict';
10
+
11
+ // Cloud API param keys
12
+ const CLOUD = {
13
+ POWER: 'pwr',
14
+ MODE: 'ac_mode',
15
+ TEMP_TARGET: 'temp',
16
+ TEMP_AMBIENT: 'envtemp',
17
+ FAN_SPEED: 'ac_mark',
18
+ SWING_V: 'ac_vdir',
19
+ SWING_H: 'ac_hdir',
20
+ SLEEP: 'ac_slp',
21
+ HEALTH: 'ac_health',
22
+ ECO: 'mldprf',
23
+ CLEAN: 'ac_clean',
24
+ DISPLAY: 'scrdisp',
25
+ };
26
+
27
+ // Cloud mode values: 0=COOL, 1=HEAT, 2=DRY, 3=FAN, 4=AUTO
28
+ const CLOUD_MODE = { AUTO: 4, COOL: 0, HEAT: 1, DRY: 2, FAN: 3 };
29
+
30
+ // Fan speed values
31
+ const FAN_SPEED = { AUTO: 0, LOW: 1, MEDIUM: 2, HIGH: 3, TURBO: 4, MUTE: 5 };
32
+
33
+ class AcFreedomAccessory {
34
+ constructor(platform, accessory, config, deviceApi) {
35
+ this.platform = platform;
36
+ this.accessory = accessory;
37
+ this.config = config;
38
+ this.deviceApi = deviceApi;
39
+ this.log = platform.log;
40
+
41
+ this.Service = platform.api.hap.Service;
42
+ this.Characteristic = platform.api.hap.Characteristic;
43
+
44
+ // Current state cache
45
+ this.state = {
46
+ power: false,
47
+ mode: CLOUD_MODE.AUTO,
48
+ targetTemp: 24,
49
+ currentTemp: 24,
50
+ fanSpeed: FAN_SPEED.AUTO,
51
+ swingV: false,
52
+ swingH: false,
53
+ sleep: false,
54
+ health: false,
55
+ eco: false,
56
+ clean: false,
57
+ display: true,
58
+ };
59
+
60
+ this.presetConfigs = {
61
+ sleep: { label: 'Sleep Mode', cloudKey: CLOUD.SLEEP, localAttr: 'sleep' },
62
+ health: { label: 'Health', cloudKey: CLOUD.HEALTH, localAttr: 'health' },
63
+ eco: { label: 'Eco', cloudKey: CLOUD.ECO, localAttr: 'mildew' },
64
+ clean: { label: 'Clean', cloudKey: CLOUD.CLEAN, localAttr: 'clean' },
65
+ };
66
+
67
+ this.setupAccessoryInfo();
68
+ this.setupHeaterCooler();
69
+ this.setupPresetSwitches();
70
+ this.setupDisplaySwitch();
71
+
72
+ // Start polling
73
+ const interval = (config.pollInterval || 30) * 1000;
74
+ this.pollTimer = setInterval(() => this.pollState(), interval);
75
+ this.pollState();
76
+ }
77
+
78
+ // ── Accessory Information ──────────────────────────────────────
79
+ setupAccessoryInfo() {
80
+ const infoService = this.accessory.getService(this.Service.AccessoryInformation)
81
+ || this.accessory.addService(this.Service.AccessoryInformation);
82
+
83
+ infoService
84
+ .setCharacteristic(this.Characteristic.Manufacturer, 'AUX')
85
+ .setCharacteristic(this.Characteristic.Model, 'AC Freedom')
86
+ .setCharacteristic(this.Characteristic.SerialNumber, this.config.name || 'AC-001');
87
+ }
88
+
89
+ // ── HeaterCooler Service ───────────────────────────────────────
90
+ setupHeaterCooler() {
91
+ this.heaterCooler = this.accessory.getService(this.Service.HeaterCooler)
92
+ || this.accessory.addService(this.Service.HeaterCooler, this.config.name);
93
+
94
+ const C = this.Characteristic;
95
+ const hvac = this.config.hvacModes || { auto: true, cool: true, heat: true };
96
+
97
+ // Active (on/off)
98
+ this.heaterCooler.getCharacteristic(C.Active)
99
+ .onGet(() => this.state.power ? C.Active.ACTIVE : C.Active.INACTIVE)
100
+ .onSet(async (value) => {
101
+ this.state.power = value === C.Active.ACTIVE;
102
+ await this.sendPower(this.state.power);
103
+ });
104
+
105
+ // Current state
106
+ this.heaterCooler.getCharacteristic(C.CurrentHeaterCoolerState)
107
+ .onGet(() => {
108
+ if (!this.state.power) return C.CurrentHeaterCoolerState.INACTIVE;
109
+ switch (this.state.mode) {
110
+ case CLOUD_MODE.HEAT: return C.CurrentHeaterCoolerState.HEATING;
111
+ case CLOUD_MODE.COOL: return C.CurrentHeaterCoolerState.COOLING;
112
+ default: return C.CurrentHeaterCoolerState.IDLE;
113
+ }
114
+ });
115
+
116
+ // Target state (auto/heat/cool)
117
+ const validStates = [];
118
+ if (hvac.auto) validStates.push(C.TargetHeaterCoolerState.AUTO);
119
+ if (hvac.heat) validStates.push(C.TargetHeaterCoolerState.HEAT);
120
+ if (hvac.cool) validStates.push(C.TargetHeaterCoolerState.COOL);
121
+ if (validStates.length === 0) validStates.push(C.TargetHeaterCoolerState.AUTO);
122
+
123
+ this.heaterCooler.getCharacteristic(C.TargetHeaterCoolerState)
124
+ .setProps({ validValues: validStates })
125
+ .onGet(() => {
126
+ switch (this.state.mode) {
127
+ case CLOUD_MODE.HEAT: return C.TargetHeaterCoolerState.HEAT;
128
+ case CLOUD_MODE.COOL: return C.TargetHeaterCoolerState.COOL;
129
+ default: return C.TargetHeaterCoolerState.AUTO;
130
+ }
131
+ })
132
+ .onSet(async (value) => {
133
+ let mode;
134
+ switch (value) {
135
+ case C.TargetHeaterCoolerState.HEAT: mode = CLOUD_MODE.HEAT; break;
136
+ case C.TargetHeaterCoolerState.COOL: mode = CLOUD_MODE.COOL; break;
137
+ default: mode = CLOUD_MODE.AUTO; break;
138
+ }
139
+ this.state.mode = mode;
140
+ await this.sendMode(mode);
141
+ });
142
+
143
+ // Current temperature
144
+ this.heaterCooler.getCharacteristic(C.CurrentTemperature)
145
+ .setProps({ minValue: -20, maxValue: 60 })
146
+ .onGet(() => this.state.currentTemp);
147
+
148
+ // Cooling threshold temperature
149
+ const step = this.config.tempStep || 0.5;
150
+ this.heaterCooler.getCharacteristic(C.CoolingThresholdTemperature)
151
+ .setProps({ minValue: 16, maxValue: 32, minStep: step })
152
+ .onGet(() => this.state.targetTemp)
153
+ .onSet(async (value) => {
154
+ this.state.targetTemp = value;
155
+ await this.sendTemperature(value);
156
+ });
157
+
158
+ // Heating threshold temperature
159
+ this.heaterCooler.getCharacteristic(C.HeatingThresholdTemperature)
160
+ .setProps({ minValue: 16, maxValue: 32, minStep: step })
161
+ .onGet(() => this.state.targetTemp)
162
+ .onSet(async (value) => {
163
+ this.state.targetTemp = value;
164
+ await this.sendTemperature(value);
165
+ });
166
+
167
+ // Rotation speed (fan: 0=auto, 20=mute, 40=low, 60=med, 80=high, 100=turbo)
168
+ this.heaterCooler.getCharacteristic(C.RotationSpeed)
169
+ .setProps({ minValue: 0, maxValue: 100, minStep: 20 })
170
+ .onGet(() => this.fanSpeedToPercent(this.state.fanSpeed))
171
+ .onSet(async (value) => {
172
+ this.state.fanSpeed = this.percentToFanSpeed(value);
173
+ await this.sendFanSpeed(this.state.fanSpeed);
174
+ });
175
+
176
+ // Swing mode
177
+ this.heaterCooler.getCharacteristic(C.SwingMode)
178
+ .onGet(() => (this.state.swingV || this.state.swingH)
179
+ ? C.SwingMode.SWING_ENABLED
180
+ : C.SwingMode.SWING_DISABLED)
181
+ .onSet(async (value) => {
182
+ const on = value === C.SwingMode.SWING_ENABLED;
183
+ this.state.swingV = on;
184
+ this.state.swingH = on;
185
+ await this.sendSwing(on, on);
186
+ });
187
+ }
188
+
189
+ // ── Preset Switches (linked to HeaterCooler) ──────────────────
190
+ setupPresetSwitches() {
191
+ const presets = this.config.presets || { sleep: true, health: true, eco: true, clean: true };
192
+
193
+ this.presetSwitches = {};
194
+
195
+ for (const [key, cfg] of Object.entries(this.presetConfigs)) {
196
+ if (!presets[key]) {
197
+ // Remove if previously existed
198
+ const existing = this.accessory.getServiceById(this.Service.Switch, key);
199
+ if (existing) this.accessory.removeService(existing);
200
+ continue;
201
+ }
202
+
203
+ const switchService = this.accessory.getServiceById(this.Service.Switch, key)
204
+ || this.accessory.addService(this.Service.Switch, cfg.label, key);
205
+
206
+ switchService.setCharacteristic(this.Characteristic.Name, cfg.label);
207
+
208
+ switchService.getCharacteristic(this.Characteristic.On)
209
+ .onGet(() => this.state[key])
210
+ .onSet(async (value) => {
211
+ // Turn off other presets when activating one
212
+ if (value) {
213
+ for (const otherKey of Object.keys(this.presetConfigs)) {
214
+ if (otherKey !== key) this.state[otherKey] = false;
215
+ }
216
+ }
217
+ this.state[key] = value;
218
+ await this.sendPreset(key, value);
219
+ // Update other switches UI
220
+ this.refreshPresetSwitches(key);
221
+ });
222
+
223
+ // Link to HeaterCooler so it appears inside the climate card
224
+ this.heaterCooler.addLinkedService(switchService);
225
+ this.presetSwitches[key] = switchService;
226
+ }
227
+ }
228
+
229
+ refreshPresetSwitches(exceptKey) {
230
+ for (const [key, svc] of Object.entries(this.presetSwitches)) {
231
+ if (key !== exceptKey) {
232
+ svc.updateCharacteristic(this.Characteristic.On, this.state[key]);
233
+ }
234
+ }
235
+ }
236
+
237
+ // ── Display Switch (linked to HeaterCooler) ────────────────────
238
+ setupDisplaySwitch() {
239
+ if (this.config.showDisplay === false) {
240
+ const existing = this.accessory.getServiceById(this.Service.Switch, 'display');
241
+ if (existing) this.accessory.removeService(existing);
242
+ return;
243
+ }
244
+
245
+ this.displaySwitch = this.accessory.getServiceById(this.Service.Switch, 'display')
246
+ || this.accessory.addService(this.Service.Switch, 'Display', 'display');
247
+
248
+ this.displaySwitch.setCharacteristic(this.Characteristic.Name, 'Display');
249
+
250
+ this.displaySwitch.getCharacteristic(this.Characteristic.On)
251
+ .onGet(() => this.state.display)
252
+ .onSet(async (value) => {
253
+ this.state.display = value;
254
+ await this.sendDisplay(value);
255
+ });
256
+
257
+ this.heaterCooler.addLinkedService(this.displaySwitch);
258
+ }
259
+
260
+ // ── Fan speed mapping ──────────────────────────────────────────
261
+ fanSpeedToPercent(speed) {
262
+ const map = { 0: 0, 5: 20, 1: 40, 2: 60, 3: 80, 4: 100 };
263
+ return map[speed] ?? 0;
264
+ }
265
+
266
+ percentToFanSpeed(pct) {
267
+ if (pct <= 0) return FAN_SPEED.AUTO;
268
+ if (pct <= 20) return FAN_SPEED.MUTE;
269
+ if (pct <= 40) return FAN_SPEED.LOW;
270
+ if (pct <= 60) return FAN_SPEED.MEDIUM;
271
+ if (pct <= 80) return FAN_SPEED.HIGH;
272
+ return FAN_SPEED.TURBO;
273
+ }
274
+
275
+ // ── Poll state ─────────────────────────────────────────────────
276
+ async pollState() {
277
+ try {
278
+ if (this.deviceApi.type === 'cloud') {
279
+ await this.pollCloud();
280
+ } else {
281
+ await this.pollLocal();
282
+ }
283
+ this.updateCharacteristics();
284
+ } catch (err) {
285
+ this.log.warn('Poll failed: %s', err.message);
286
+ }
287
+ }
288
+
289
+ async pollCloud() {
290
+ const { api, device } = this.deviceApi;
291
+ try {
292
+ const params = await api.getDeviceParams(device);
293
+ if (!params) return;
294
+
295
+ this.state.power = !!params[CLOUD.POWER];
296
+ this.state.mode = params[CLOUD.MODE] ?? CLOUD_MODE.AUTO;
297
+ this.state.targetTemp = (params[CLOUD.TEMP_TARGET] ?? 240) / 10;
298
+ this.state.currentTemp = (params[CLOUD.TEMP_AMBIENT] ?? 240) / 10;
299
+ this.state.fanSpeed = params[CLOUD.FAN_SPEED] ?? FAN_SPEED.AUTO;
300
+ this.state.swingV = !!params[CLOUD.SWING_V];
301
+ this.state.swingH = !!params[CLOUD.SWING_H];
302
+ this.state.sleep = !!params[CLOUD.SLEEP];
303
+ this.state.health = !!params[CLOUD.HEALTH];
304
+ this.state.eco = !!params[CLOUD.ECO];
305
+ this.state.clean = !!params[CLOUD.CLEAN];
306
+ this.state.display = !!params[CLOUD.DISPLAY];
307
+ } catch (err) {
308
+ // Re-login on token expiry
309
+ if (err.message && err.message.includes('token')) {
310
+ const cloud = this.config.cloud;
311
+ await api.login(cloud.email, cloud.password);
312
+ }
313
+ throw err;
314
+ }
315
+ }
316
+
317
+ async pollLocal() {
318
+ const { api } = this.deviceApi;
319
+ const ok = await api.update();
320
+ if (!ok) return;
321
+
322
+ const s = api.state;
323
+ this.state.power = !!s.power;
324
+ this.state.targetTemp = s.temperature;
325
+ this.state.currentTemp = s.ambientTemp;
326
+ this.state.fanSpeed = s.fanSpeed;
327
+ this.state.swingV = s.verticalFixation === 7;
328
+ this.state.swingH = s.horizontalFixation === 7;
329
+ this.state.sleep = !!s.sleep;
330
+ this.state.health = !!s.health;
331
+ this.state.eco = !!s.mildew;
332
+ this.state.clean = !!s.clean;
333
+ this.state.display = !!s.display;
334
+
335
+ // Map local mode to cloud mode values
336
+ const modeMap = { 1: CLOUD_MODE.COOL, 2: CLOUD_MODE.DRY, 4: CLOUD_MODE.HEAT, 6: CLOUD_MODE.FAN, 8: CLOUD_MODE.AUTO };
337
+ this.state.mode = modeMap[s.mode] ?? CLOUD_MODE.AUTO;
338
+ }
339
+
340
+ updateCharacteristics() {
341
+ const C = this.Characteristic;
342
+
343
+ this.heaterCooler.updateCharacteristic(C.Active,
344
+ this.state.power ? C.Active.ACTIVE : C.Active.INACTIVE);
345
+
346
+ if (!this.state.power) {
347
+ this.heaterCooler.updateCharacteristic(C.CurrentHeaterCoolerState,
348
+ C.CurrentHeaterCoolerState.INACTIVE);
349
+ } else {
350
+ switch (this.state.mode) {
351
+ case CLOUD_MODE.HEAT:
352
+ this.heaterCooler.updateCharacteristic(C.CurrentHeaterCoolerState,
353
+ C.CurrentHeaterCoolerState.HEATING);
354
+ break;
355
+ case CLOUD_MODE.COOL:
356
+ this.heaterCooler.updateCharacteristic(C.CurrentHeaterCoolerState,
357
+ C.CurrentHeaterCoolerState.COOLING);
358
+ break;
359
+ default:
360
+ this.heaterCooler.updateCharacteristic(C.CurrentHeaterCoolerState,
361
+ C.CurrentHeaterCoolerState.IDLE);
362
+ }
363
+ }
364
+
365
+ this.heaterCooler.updateCharacteristic(C.CurrentTemperature, this.state.currentTemp);
366
+ this.heaterCooler.updateCharacteristic(C.CoolingThresholdTemperature, this.state.targetTemp);
367
+ this.heaterCooler.updateCharacteristic(C.HeatingThresholdTemperature, this.state.targetTemp);
368
+ this.heaterCooler.updateCharacteristic(C.RotationSpeed, this.fanSpeedToPercent(this.state.fanSpeed));
369
+ this.heaterCooler.updateCharacteristic(C.SwingMode,
370
+ (this.state.swingV || this.state.swingH) ? C.SwingMode.SWING_ENABLED : C.SwingMode.SWING_DISABLED);
371
+
372
+ // Update preset switches
373
+ for (const [key, svc] of Object.entries(this.presetSwitches || {})) {
374
+ svc.updateCharacteristic(C.On, this.state[key]);
375
+ }
376
+
377
+ // Update display switch
378
+ if (this.displaySwitch) {
379
+ this.displaySwitch.updateCharacteristic(C.On, this.state.display);
380
+ }
381
+ }
382
+
383
+ // ── Send commands ──────────────────────────────────────────────
384
+ async sendPower(on) {
385
+ if (this.deviceApi.type === 'cloud') {
386
+ await this.cloudSet({ [CLOUD.POWER]: on ? 1 : 0 });
387
+ } else {
388
+ this.deviceApi.api.state.power = on ? 1 : 0;
389
+ await this.deviceApi.api.setState();
390
+ }
391
+ }
392
+
393
+ async sendMode(mode) {
394
+ if (this.deviceApi.type === 'cloud') {
395
+ await this.cloudSet({ [CLOUD.POWER]: 1, [CLOUD.MODE]: mode });
396
+ } else {
397
+ // Map cloud mode to local mode
398
+ const localModeMap = {
399
+ [CLOUD_MODE.AUTO]: 8, [CLOUD_MODE.COOL]: 1,
400
+ [CLOUD_MODE.HEAT]: 4, [CLOUD_MODE.DRY]: 2, [CLOUD_MODE.FAN]: 6,
401
+ };
402
+ const api = this.deviceApi.api;
403
+ api.state.power = 1;
404
+ api.state.mode = localModeMap[mode] ?? 8;
405
+ await api.setState();
406
+ }
407
+ }
408
+
409
+ async sendTemperature(temp) {
410
+ if (this.deviceApi.type === 'cloud') {
411
+ await this.cloudSet({ [CLOUD.TEMP_TARGET]: Math.round(temp * 10) });
412
+ } else {
413
+ this.deviceApi.api.state.temperature = temp;
414
+ await this.deviceApi.api.setState();
415
+ }
416
+ }
417
+
418
+ async sendFanSpeed(speed) {
419
+ if (this.deviceApi.type === 'cloud') {
420
+ await this.cloudSet({ [CLOUD.FAN_SPEED]: speed });
421
+ } else {
422
+ const api = this.deviceApi.api;
423
+ api.state.fanSpeed = speed;
424
+ api.state.turbo = speed === FAN_SPEED.TURBO ? 1 : 0;
425
+ api.state.mute = speed === FAN_SPEED.MUTE ? 1 : 0;
426
+ await api.setState();
427
+ }
428
+ }
429
+
430
+ async sendSwing(v, h) {
431
+ if (this.deviceApi.type === 'cloud') {
432
+ await this.cloudSet({ [CLOUD.SWING_V]: v ? 1 : 0, [CLOUD.SWING_H]: h ? 1 : 0 });
433
+ } else {
434
+ const api = this.deviceApi.api;
435
+ api.state.verticalFixation = v ? 7 : 0;
436
+ api.state.horizontalFixation = h ? 7 : 0;
437
+ await api.setState();
438
+ }
439
+ }
440
+
441
+ async sendPreset(key, on) {
442
+ const cfg = this.presetConfigs[key];
443
+ if (!cfg) return;
444
+
445
+ if (this.deviceApi.type === 'cloud') {
446
+ // Turn off all presets, then set the target one
447
+ const update = {
448
+ [CLOUD.SLEEP]: 0,
449
+ [CLOUD.HEALTH]: 0,
450
+ [CLOUD.ECO]: 0,
451
+ [CLOUD.CLEAN]: 0,
452
+ };
453
+ if (on) update[cfg.cloudKey] = 1;
454
+ await this.cloudSet(update);
455
+ } else {
456
+ const api = this.deviceApi.api;
457
+ api.state.sleep = 0;
458
+ api.state.health = 0;
459
+ api.state.mildew = 0;
460
+ api.state.clean = 0;
461
+ if (on) api.state[cfg.localAttr] = 1;
462
+ await api.setState();
463
+ }
464
+ }
465
+
466
+ async sendDisplay(on) {
467
+ if (this.deviceApi.type === 'cloud') {
468
+ await this.cloudSet({ [CLOUD.DISPLAY]: on ? 1 : 0 });
469
+ } else {
470
+ this.deviceApi.api.state.display = on ? 1 : 0;
471
+ await this.deviceApi.api.setState();
472
+ }
473
+ }
474
+
475
+ async cloudSet(params) {
476
+ const { api, device } = this.deviceApi;
477
+ await api.setDeviceParams(device, params);
478
+ }
479
+ }
480
+
481
+ module.exports = { AcFreedomAccessory };