homebridge-ac-freedom 2.3.4 → 2.3.6
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/CHANGELOG.md +24 -0
- package/README.md +18 -19
- package/config.schema.json +4 -25
- package/homebridge-ui/public/index.html +28 -67
- package/package.json +1 -1
- package/src/ac-accessory.js +145 -61
- package/src/cloud-api.js +20 -13
- package/src/platform.js +10 -30
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,29 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 2.3.6
|
|
4
|
+
|
|
5
|
+
- **Config UI: feature toggles redesigned** — checkboxes removed; each feature is now a colour-coded pill: bright green when enabled, muted grey when disabled
|
|
6
|
+
|
|
7
|
+
## 2.3.5
|
|
8
|
+
|
|
9
|
+
- **Fix double beep on power-on** — `TargetHeaterCoolerState` command is suppressed when fired within 200 ms of a power command; mode is already included in `sendPower`
|
|
10
|
+
- **Fix device turns back on after power-off** — `sendPower(false)` no longer sends a separate sleep or fan command; UI reset is local-only, no extra cloud calls
|
|
11
|
+
- **Fix fan not resetting on device** — `sendPower(false)` now includes `fanSpeed=0` in the same API call, so the device restores AUTO fan on next power-on
|
|
12
|
+
- **Fix stale poll overriding command** — poll is suppressed for 5 s after any successful user command; a failed command no longer blocks state refresh
|
|
13
|
+
- **Fix mode command turning device on** — `sendMode` uses `state.power` instead of hardcoded `power=1`; changing mode while AC is off no longer wakes the device
|
|
14
|
+
- **Fix preset switches showing ON when AC is off** — `onGet` and `updateCharacteristics` now gate preset, fan and comfwind values on `state.power`
|
|
15
|
+
- **Fix temperature step** — temperature is snapped in `onSet` and pushed back to HomeKit immediately; corrects slider behaviour even when HomeKit has a cached `minStep`
|
|
16
|
+
- **Fix temperature out-of-range HAP error** — invalid temperature readings from cloud (e.g. 0°C or 100°C when AC is off) are ignored instead of forwarded to HomeKit
|
|
17
|
+
- **Fix unhandled errors in onSet** — all send handlers now use `_trySend()` which catches errors and returns `SERVICE_COMMUNICATION_FAILURE` to HomeKit instead of logging "Unhandled error"
|
|
18
|
+
- **Fix cloud error messages** — cloud API errors now extract `payload.message` instead of dumping the full JSON response
|
|
19
|
+
- **Fix `connection: "cloud"` trying local** — local connection is only attempted when `connection` is explicitly `"hybrid"`
|
|
20
|
+
- **Fix service tile order** — HomeKit climate card always shows tiles in the correct order (Fan → Sleep → Display → Health → Clean → Eco → Comf. Wind) regardless of when each service was first registered
|
|
21
|
+
- **Fix crash on command failure** — `_trySend` now correctly references `this.platform.api.hap` instead of the undefined `this.api`
|
|
22
|
+
- **Fix malformed device cookie** — cloud API now throws an actionable error instead of crashing with a JSON parse exception
|
|
23
|
+
- **Remove Temperature Step and Poll Interval settings** — both are hardcoded (1 °C step, 30 s poll); removed from Config UI and schema to prevent misconfiguration
|
|
24
|
+
- **Cleaner startup log** — reduced to one line per device; removed redundant "Restoring / Updating accessory" messages
|
|
25
|
+
- **Code cleanup** — `LOCAL_MODE_MAP` and `FAN_REMAP` extracted as module-level constants; cloud API default params moved to `DEVICE_PARAMS` constant
|
|
26
|
+
|
|
3
27
|
## 2.3.4
|
|
4
28
|
|
|
5
29
|
- **UI: badge colors** — Cloud (teal), Hybrid (green), Local (yellow) — clearly distinct from each other
|
package/README.md
CHANGED
|
@@ -31,13 +31,13 @@ A [Homebridge](https://homebridge.io) plugin for controlling **AUX-based air con
|
|
|
31
31
|
- Self Clean
|
|
32
32
|
- Comfortable Wind
|
|
33
33
|
- **Display** switch — LED display on/off
|
|
34
|
-
- **Auto-reset** — fan
|
|
35
|
-
- **
|
|
36
|
-
- **
|
|
34
|
+
- **Auto-reset** — fan resets to Auto and Sleep turns off when the AC is powered off
|
|
35
|
+
- **Consistent tile order** — Fan is always first, followed by Sleep, Display, Health, Clean, Eco, Comf. Wind
|
|
36
|
+
- **Hybrid** — single cloud login at platform level; optional IP + MAC per device for direct local Broadlink UDP (local preferred, cloud fallback)
|
|
37
|
+
- **Local** — direct Broadlink UDP only, no internet required
|
|
37
38
|
- **Multi-device** — configure as many ACs as needed
|
|
38
39
|
- **Custom config UI** — built-in Homebridge UI with Fetch Devices button
|
|
39
40
|
- Homebridge v1 & v2 compatible
|
|
40
|
-
- English UI
|
|
41
41
|
|
|
42
42
|
---
|
|
43
43
|
|
|
@@ -196,8 +196,6 @@ Required for Hybrid mode. Configured once, shared by all hybrid devices.
|
|
|
196
196
|
| `name` | string | Yes | — | Device name in HomeKit |
|
|
197
197
|
| `connection` | string | No | `"hybrid"` | `"hybrid"` or `"local"` |
|
|
198
198
|
| `endpointId` | string | No | — | Cloud device ID (auto-detected if empty) |
|
|
199
|
-
| `pollInterval` | integer | No | `30` | Polling interval in seconds (5–300) |
|
|
200
|
-
| `tempStep` | number | No | `0.5` | Temperature step: `0.5` or `1` |
|
|
201
199
|
|
|
202
200
|
### Local Settings (`local` object, per device)
|
|
203
201
|
|
|
@@ -218,7 +216,7 @@ Required for Hybrid mode. Configured once, shared by all hybrid devices.
|
|
|
218
216
|
| `showComfWind` | boolean | `false` | Comfortable Wind switch |
|
|
219
217
|
| `showDisplay` | boolean | `false` | Display (LED) switch |
|
|
220
218
|
|
|
221
|
-
> **Tip:** For a clean HomeKit appearance, enable **Fan** and
|
|
219
|
+
> **Tip:** For a clean HomeKit appearance, enable only the features you use. **Fan** and **Sleep** are enabled by default — most users need nothing else.
|
|
222
220
|
|
|
223
221
|
---
|
|
224
222
|
|
|
@@ -226,13 +224,11 @@ Required for Hybrid mode. Configured once, shared by all hybrid devices.
|
|
|
226
224
|
|
|
227
225
|
Once configured, the AC appears as a **HeaterCooler** tile. Tapping it reveals:
|
|
228
226
|
|
|
229
|
-
- Temperature control
|
|
227
|
+
- Temperature control (1 °C steps, 16–32 °C)
|
|
230
228
|
- Mode selector — Auto / Heat / Cool
|
|
231
229
|
- Fan speed slider — 0% Auto · 25% Low · 50% Medium · 75% High · 100% Turbo
|
|
232
230
|
- Swing toggle
|
|
233
|
-
-
|
|
234
|
-
|
|
235
|
-
All linked services appear as tiles inside the climate card.
|
|
231
|
+
- Linked service tiles in a fixed order: Fan → Sleep → Display → Health → Clean → Eco → Comf. Wind
|
|
236
232
|
|
|
237
233
|
---
|
|
238
234
|
|
|
@@ -246,7 +242,7 @@ All linked services appear as tiles inside the climate card.
|
|
|
246
242
|
| Works remotely | Yes | No |
|
|
247
243
|
| Local fallback | Yes (when IP + MAC configured) | — |
|
|
248
244
|
|
|
249
|
-
> ¹ **Hybrid + IP/MAC:** if internet is unavailable at startup, the device starts in local-only mode automatically. Full hybrid resumes on the next restart when internet is available.
|
|
245
|
+
> ¹ **Hybrid + IP/MAC:** if internet is unavailable at Homebridge startup, the device starts in local-only mode automatically. Full hybrid resumes on the next restart when internet is available.
|
|
250
246
|
|
|
251
247
|
---
|
|
252
248
|
|
|
@@ -258,19 +254,22 @@ All linked services appear as tiles inside the climate card.
|
|
|
258
254
|
- Check the `region` matches your account region
|
|
259
255
|
- Logging into the AC Freedom app may invalidate the plugin session — restart Homebridge after using the app
|
|
260
256
|
|
|
261
|
-
**Device not found**
|
|
257
|
+
**Device not found after Fetch**
|
|
262
258
|
- Leave `endpointId` empty for auto-detection (first discovered device is used)
|
|
263
259
|
- Use the **Fetch** button in the Homebridge UI to discover and auto-fill your device IDs
|
|
260
|
+
- If the device is missing from the cloud list, power cycle the AC and try again
|
|
264
261
|
|
|
265
|
-
**"
|
|
266
|
-
- These are transient and automatically suppressed
|
|
267
|
-
- If persistent, increase `pollInterval` to 60 or higher
|
|
268
|
-
|
|
269
|
-
**AC not responding**
|
|
262
|
+
**AC not responding / "Service Communication Failure" in HomeKit**
|
|
270
263
|
- Check the AC is powered on and connected to Wi-Fi
|
|
271
|
-
- Local mode: Homebridge and AC must be on the same network
|
|
264
|
+
- Local mode: Homebridge and the AC must be on the same network
|
|
272
265
|
- Try restarting Homebridge
|
|
273
266
|
|
|
267
|
+
**"Server busy" errors in logs**
|
|
268
|
+
- These are transient cloud API errors and are automatically suppressed — no action needed
|
|
269
|
+
|
|
270
|
+
**Tile order is wrong in HomeKit**
|
|
271
|
+
- Remove the accessory from the Home app (the individual tile, not the whole bridge), then restart Homebridge — it will re-appear with the correct order
|
|
272
|
+
|
|
274
273
|
---
|
|
275
274
|
|
|
276
275
|
## Support
|
package/config.schema.json
CHANGED
|
@@ -72,9 +72,9 @@
|
|
|
72
72
|
"type": "object",
|
|
73
73
|
"properties": {
|
|
74
74
|
"sleep": { "title": "Sleep Mode", "type": "boolean", "default": true },
|
|
75
|
-
"health": { "title": "Health / Ionizer", "type": "boolean", "default":
|
|
76
|
-
"eco": { "title": "Eco / Mildew Prevention", "type": "boolean", "default":
|
|
77
|
-
"clean": { "title": "Self Clean", "type": "boolean", "default":
|
|
75
|
+
"health": { "title": "Health / Ionizer", "type": "boolean", "default": false },
|
|
76
|
+
"eco": { "title": "Eco / Mildew Prevention", "type": "boolean", "default": false },
|
|
77
|
+
"clean": { "title": "Self Clean", "type": "boolean", "default": false }
|
|
78
78
|
}
|
|
79
79
|
},
|
|
80
80
|
"showFan": {
|
|
@@ -91,19 +91,6 @@
|
|
|
91
91
|
"title": "Display",
|
|
92
92
|
"type": "boolean",
|
|
93
93
|
"default": true
|
|
94
|
-
},
|
|
95
|
-
"pollInterval": {
|
|
96
|
-
"title": "Poll Interval (seconds)",
|
|
97
|
-
"type": "integer",
|
|
98
|
-
"default": 30,
|
|
99
|
-
"minimum": 5,
|
|
100
|
-
"maximum": 300
|
|
101
|
-
},
|
|
102
|
-
"tempStep": {
|
|
103
|
-
"title": "Temperature Step",
|
|
104
|
-
"type": "number",
|
|
105
|
-
"default": 0.5,
|
|
106
|
-
"enum": [0.5, 1]
|
|
107
94
|
}
|
|
108
95
|
}
|
|
109
96
|
}
|
|
@@ -172,15 +159,7 @@
|
|
|
172
159
|
{ "key": "devices[].showComfWind" },
|
|
173
160
|
{ "key": "devices[].showDisplay" }
|
|
174
161
|
]
|
|
175
|
-
}
|
|
176
|
-
{
|
|
177
|
-
"key": "devices[].tempStep",
|
|
178
|
-
"titleMap": {
|
|
179
|
-
"0.5": "0.5 °C",
|
|
180
|
-
"1": "1 °C"
|
|
181
|
-
}
|
|
182
|
-
},
|
|
183
|
-
{ "key": "devices[].pollInterval" }
|
|
162
|
+
}
|
|
184
163
|
]
|
|
185
164
|
}
|
|
186
165
|
]
|
|
@@ -154,17 +154,17 @@
|
|
|
154
154
|
.toggle-row { display: flex; gap: 6px; }
|
|
155
155
|
.toggle-item {
|
|
156
156
|
flex: 1; min-width: 0;
|
|
157
|
-
display: flex; align-items: center; justify-content: center;
|
|
157
|
+
display: flex; align-items: center; justify-content: center;
|
|
158
158
|
padding: 7px 4px; border-radius: 7px;
|
|
159
159
|
border: 1px solid var(--border); background: var(--card-bg);
|
|
160
|
-
cursor: pointer; font-size: 11px; font-weight: 500; color: var(--text);
|
|
161
|
-
white-space: nowrap; transition: border-color .15s, background .15s;
|
|
160
|
+
cursor: pointer; font-size: 11px; font-weight: 500; color: var(--text-muted);
|
|
161
|
+
white-space: nowrap; transition: border-color .15s, background .15s, color .15s;
|
|
162
|
+
user-select: none;
|
|
162
163
|
}
|
|
163
|
-
.toggle-item:hover { border-color:
|
|
164
|
-
.toggle-item
|
|
165
|
-
|
|
164
|
+
.toggle-item:not(.on):hover { border-color: #9ca3af; }
|
|
165
|
+
.toggle-item.on {
|
|
166
|
+
background: #16a34a; border-color: #16a34a; color: #fff; font-weight: 600;
|
|
166
167
|
}
|
|
167
|
-
.toggle-item.checked { border-color: var(--accent-border); background: var(--accent-light); }
|
|
168
168
|
|
|
169
169
|
/* ── Fieldset ── */
|
|
170
170
|
.fieldset {
|
|
@@ -224,7 +224,6 @@
|
|
|
224
224
|
fan: 'Fan', sleep: 'Sleep', display: 'Display',
|
|
225
225
|
health: 'Health', clean: 'Clean', eco: 'Eco',
|
|
226
226
|
comfWind: 'Comf. Wind',
|
|
227
|
-
tempStep: 'Temperature Step', pollInterval: 'Poll Interval',
|
|
228
227
|
addDevice: 'Add Device', remove: 'Remove',
|
|
229
228
|
hybrid: 'Hybrid', cloudCredentials: 'Cloud Credentials',
|
|
230
229
|
fetching: 'Fetching…', devicesFound: 'devices found',
|
|
@@ -254,54 +253,27 @@
|
|
|
254
253
|
function featToggles(d, i) {
|
|
255
254
|
const presets = d.presets || {};
|
|
256
255
|
const feats = [
|
|
257
|
-
{ key: 'showFan', label: t.fan||'Fan',
|
|
258
|
-
{ key: 'sleep', label: t.sleep||'Sleep',
|
|
259
|
-
{ key: 'showDisplay', label: t.display||'Display',
|
|
260
|
-
{ key: 'health', label: t.health||'Health',
|
|
261
|
-
{ key: 'clean', label: t.clean||'Clean',
|
|
262
|
-
{ key: 'eco', label: t.eco||'Eco',
|
|
263
|
-
{ key: 'showComfWind', label: t.comfWind||'Comf. Wind',
|
|
256
|
+
{ key: 'showFan', label: t.fan||'Fan', val: bool(d.showFan) },
|
|
257
|
+
{ key: 'sleep', label: t.sleep||'Sleep', val: bool(presets.sleep), preset: true },
|
|
258
|
+
{ key: 'showDisplay', label: t.display||'Display', val: bool(d.showDisplay) },
|
|
259
|
+
{ key: 'health', label: t.health||'Health', val: bool(presets.health), preset: true },
|
|
260
|
+
{ key: 'clean', label: t.clean||'Clean', val: bool(presets.clean), preset: true },
|
|
261
|
+
{ key: 'eco', label: t.eco||'Eco', val: bool(presets.eco), preset: true },
|
|
262
|
+
{ key: 'showComfWind', label: t.comfWind||'Comf. Wind', val: bool(d.showComfWind) },
|
|
264
263
|
];
|
|
264
|
+
|
|
265
265
|
return `
|
|
266
266
|
<div class="toggle-row">
|
|
267
267
|
${feats.map(f => `
|
|
268
|
-
<
|
|
269
|
-
|
|
270
|
-
data-i="${i}" data-k="${f.key}" data-preset="${f.preset||false}"
|
|
271
|
-
${f.val ? 'checked' : ''}>
|
|
268
|
+
<div class="toggle-item ${f.val ? 'on' : ''}"
|
|
269
|
+
data-i="${i}" data-k="${f.key}" data-preset="${f.preset||false}">
|
|
272
270
|
${f.label}
|
|
273
|
-
</
|
|
271
|
+
</div>
|
|
274
272
|
`).join('')}
|
|
275
273
|
</div>
|
|
276
274
|
`;
|
|
277
275
|
}
|
|
278
276
|
|
|
279
|
-
function settingsRow(d, i) {
|
|
280
|
-
const step = d.tempStep !== undefined ? d.tempStep : 0.5;
|
|
281
|
-
const poll = d.pollInterval || 30;
|
|
282
|
-
return `
|
|
283
|
-
<div class="fieldset" style="margin-bottom:12px">
|
|
284
|
-
<div class="row-2">
|
|
285
|
-
<div class="fg" style="margin-bottom:0">
|
|
286
|
-
<label>${t.tempStep||'Temperature Step'}</label>
|
|
287
|
-
<select class="d-field" data-i="${i}" data-f="tempStep">
|
|
288
|
-
<option value="0.5" ${step==0.5?'selected':''}>0.5 °C</option>
|
|
289
|
-
<option value="1" ${step==1 ?'selected':''}>1.0 °C</option>
|
|
290
|
-
</select>
|
|
291
|
-
</div>
|
|
292
|
-
<div class="fg" style="margin-bottom:0">
|
|
293
|
-
<label>${t.pollInterval||'Poll Interval'}</label>
|
|
294
|
-
<select class="d-field" data-i="${i}" data-f="pollInterval">
|
|
295
|
-
<option value="5" ${poll==5 ?'selected':''}>5 seconds</option>
|
|
296
|
-
<option value="10" ${poll==10?'selected':''}>10 seconds</option>
|
|
297
|
-
<option value="20" ${poll==20?'selected':''}>20 seconds</option>
|
|
298
|
-
<option value="30" ${poll==30?'selected':''}>30 seconds</option>
|
|
299
|
-
<option value="60" ${poll==60?'selected':''}>60 seconds</option>
|
|
300
|
-
</select>
|
|
301
|
-
</div>
|
|
302
|
-
</div>
|
|
303
|
-
</div>`;
|
|
304
|
-
}
|
|
305
277
|
|
|
306
278
|
/* ── Render cloud device card ─────────────────────────────── */
|
|
307
279
|
function renderCloudDevice(d, i) {
|
|
@@ -310,8 +282,6 @@
|
|
|
310
282
|
const conn = (d.connection === 'local' || !d.connection) ? 'cloud' : d.connection;
|
|
311
283
|
const isHybrid = conn === 'hybrid';
|
|
312
284
|
const local = d.local || {};
|
|
313
|
-
const step = d.tempStep !== undefined ? d.tempStep : 0.5;
|
|
314
|
-
const poll = d.pollInterval || 30;
|
|
315
285
|
const cardClass = isHybrid ? 'hybrid-card' : '';
|
|
316
286
|
const badge = isHybrid ? (t.hybrid||'Hybrid') : (t.cloud||'Cloud');
|
|
317
287
|
|
|
@@ -371,7 +341,6 @@
|
|
|
371
341
|
</div>
|
|
372
342
|
</div>
|
|
373
343
|
|
|
374
|
-
${settingsRow(d, i)}
|
|
375
344
|
${featToggles(d, i)}
|
|
376
345
|
</div>
|
|
377
346
|
</div>`;
|
|
@@ -382,8 +351,6 @@
|
|
|
382
351
|
const isOpen = expanded.has(i);
|
|
383
352
|
const name = d.name || `Local #${li + 1}`;
|
|
384
353
|
const local = d.local || {};
|
|
385
|
-
const step = d.tempStep !== undefined ? d.tempStep : 0.5;
|
|
386
|
-
const poll = d.pollInterval || 30;
|
|
387
354
|
|
|
388
355
|
return `
|
|
389
356
|
<div class="device-card" id="dev-card-${i}">
|
|
@@ -421,7 +388,6 @@
|
|
|
421
388
|
</div>
|
|
422
389
|
</div>
|
|
423
390
|
|
|
424
|
-
${settingsRow(d, i)}
|
|
425
391
|
${featToggles(d, i)}
|
|
426
392
|
</div>
|
|
427
393
|
</div>`;
|
|
@@ -572,7 +538,6 @@
|
|
|
572
538
|
local: { ip: '', mac: '' },
|
|
573
539
|
presets: { sleep: true, health: false, eco: false, clean: false },
|
|
574
540
|
showFan: true, showComfWind: false, showDisplay: false,
|
|
575
|
-
tempStep: 0.5, pollInterval: 30,
|
|
576
541
|
});
|
|
577
542
|
expanded.add(config.devices.length - 1);
|
|
578
543
|
added++;
|
|
@@ -602,7 +567,6 @@
|
|
|
602
567
|
local: { ip: '', mac: '' },
|
|
603
568
|
presets: { sleep: true, health: false, eco: false, clean: false },
|
|
604
569
|
showFan: true, showComfWind: false, showDisplay: false,
|
|
605
|
-
tempStep: 0.5, pollInterval: 30,
|
|
606
570
|
});
|
|
607
571
|
expanded.add(i);
|
|
608
572
|
render(); save();
|
|
@@ -634,13 +598,11 @@
|
|
|
634
598
|
});
|
|
635
599
|
});
|
|
636
600
|
|
|
637
|
-
/* Device-level fields (name
|
|
601
|
+
/* Device-level fields (name) */
|
|
638
602
|
document.querySelectorAll('.d-field').forEach(el => {
|
|
639
603
|
el.addEventListener('change', () => {
|
|
640
604
|
const i = +el.dataset.i, f = el.dataset.f;
|
|
641
|
-
config.devices[i][f] =
|
|
642
|
-
: (f === 'pollInterval') ? (parseInt(el.value) || 30)
|
|
643
|
-
: el.value;
|
|
605
|
+
config.devices[i][f] = el.value;
|
|
644
606
|
save();
|
|
645
607
|
});
|
|
646
608
|
el.addEventListener('input', () => {
|
|
@@ -661,19 +623,18 @@
|
|
|
661
623
|
});
|
|
662
624
|
});
|
|
663
625
|
|
|
664
|
-
/* Feature
|
|
665
|
-
document.querySelectorAll('.
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
const i = +
|
|
669
|
-
const isPreset =
|
|
626
|
+
/* Feature toggles */
|
|
627
|
+
document.querySelectorAll('.toggle-item').forEach(el => {
|
|
628
|
+
el.addEventListener('click', () => {
|
|
629
|
+
const isOn = el.classList.toggle('on');
|
|
630
|
+
const i = +el.dataset.i, k = el.dataset.k;
|
|
631
|
+
const isPreset = el.dataset.preset === 'true';
|
|
670
632
|
if (isPreset) {
|
|
671
633
|
if (!config.devices[i].presets) config.devices[i].presets = {};
|
|
672
|
-
config.devices[i].presets[k] =
|
|
634
|
+
config.devices[i].presets[k] = isOn;
|
|
673
635
|
} else {
|
|
674
|
-
config.devices[i][k] =
|
|
636
|
+
config.devices[i][k] = isOn;
|
|
675
637
|
}
|
|
676
|
-
cb.closest('.toggle-item').classList.toggle('checked', cb.checked);
|
|
677
638
|
save();
|
|
678
639
|
});
|
|
679
640
|
});
|
package/package.json
CHANGED
package/src/ac-accessory.js
CHANGED
|
@@ -39,9 +39,17 @@ const CLOUD_MODE = { AUTO: 4, COOL: 0, HEAT: 1, DRY: 2, FAN: 3 };
|
|
|
39
39
|
// Conversion tables applied in pollLocal / sendFanSpeed
|
|
40
40
|
const FAN_SPEED = { AUTO: 0, LOW: 1, MEDIUM: 2, HIGH: 3, TURBO: 4 };
|
|
41
41
|
|
|
42
|
-
// Local ↔
|
|
43
|
-
const
|
|
44
|
-
|
|
42
|
+
// Local ↔ cloud fan speed remap (mapping is self-inverse: 1↔3, others unchanged)
|
|
43
|
+
const FAN_REMAP = { 0: 0, 1: 3, 2: 2, 3: 1, 4: 4 };
|
|
44
|
+
|
|
45
|
+
// Cloud mode → local protocol mode byte (AUTO=0 avoids 3-bit overflow at value 8)
|
|
46
|
+
const LOCAL_MODE_MAP = {
|
|
47
|
+
[4]: 0, // AUTO
|
|
48
|
+
[0]: 1, // COOL
|
|
49
|
+
[1]: 4, // HEAT
|
|
50
|
+
[2]: 2, // DRY
|
|
51
|
+
[3]: 6, // FAN
|
|
52
|
+
};
|
|
45
53
|
|
|
46
54
|
class AcFreedomAccessory {
|
|
47
55
|
constructor(platform, accessory, config, deviceApi) {
|
|
@@ -84,9 +92,10 @@ class AcFreedomAccessory {
|
|
|
84
92
|
this.setupPresetSwitches();
|
|
85
93
|
this.setupComfWindSwitch();
|
|
86
94
|
this.setupDisplaySwitch();
|
|
95
|
+
this._reorderServices();
|
|
87
96
|
|
|
88
97
|
// Start polling
|
|
89
|
-
const interval =
|
|
98
|
+
const interval = 30000;
|
|
90
99
|
this.pollTimer = setInterval(() => this.pollState(), interval);
|
|
91
100
|
this.pollState();
|
|
92
101
|
}
|
|
@@ -122,9 +131,13 @@ class AcFreedomAccessory {
|
|
|
122
131
|
this.heaterCooler.getCharacteristic(C.Active)
|
|
123
132
|
.onGet(() => this.state.power ? C.Active.ACTIVE : C.Active.INACTIVE)
|
|
124
133
|
.onSet(async (value) => {
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
134
|
+
const newPower = value === C.Active.ACTIVE;
|
|
135
|
+
if (newPower === this.state.power) return;
|
|
136
|
+
this.state.power = newPower;
|
|
137
|
+
if (!newPower) this._powerOffReset();
|
|
138
|
+
this._powerJustChanged = true;
|
|
139
|
+
setTimeout(() => { this._powerJustChanged = false; }, 300);
|
|
140
|
+
await this._trySend(() => this.sendPower(newPower));
|
|
128
141
|
});
|
|
129
142
|
|
|
130
143
|
this.heaterCooler.getCharacteristic(C.CurrentHeaterCoolerState)
|
|
@@ -160,30 +173,43 @@ class AcFreedomAccessory {
|
|
|
160
173
|
default: mode = CLOUD_MODE.AUTO; break;
|
|
161
174
|
}
|
|
162
175
|
this.state.mode = mode;
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
176
|
+
// Wait for Active.onSet to fire first — HomeKit sends both in the same
|
|
177
|
+
// batch but order is not guaranteed; 200 ms covers the race window.
|
|
178
|
+
await new Promise(r => setTimeout(r, 200));
|
|
179
|
+
if (this._powerJustChanged) return;
|
|
180
|
+
await this._trySend(() => this.sendMode(mode));
|
|
166
181
|
});
|
|
167
182
|
|
|
168
183
|
this.heaterCooler.getCharacteristic(C.CurrentTemperature)
|
|
169
184
|
.setProps({ minValue: -20, maxValue: 60 })
|
|
170
185
|
.onGet(() => this.state.currentTemp);
|
|
171
186
|
|
|
172
|
-
const
|
|
187
|
+
const snapTemp = (v) => Math.round(v);
|
|
188
|
+
|
|
173
189
|
this.heaterCooler.getCharacteristic(C.CoolingThresholdTemperature)
|
|
174
|
-
.setProps({ minValue: 16, maxValue: 32, minStep:
|
|
175
|
-
.onGet(() => this.state.targetTemp)
|
|
190
|
+
.setProps({ minValue: 16, maxValue: 32, minStep: 1 })
|
|
191
|
+
.onGet(() => snapTemp(this.state.targetTemp))
|
|
176
192
|
.onSet(async (value) => {
|
|
177
|
-
|
|
178
|
-
|
|
193
|
+
const snapped = snapTemp(value);
|
|
194
|
+
this.state.targetTemp = snapped;
|
|
195
|
+
if (snapped !== value) {
|
|
196
|
+
this.heaterCooler.updateCharacteristic(C.CoolingThresholdTemperature, snapped);
|
|
197
|
+
this.heaterCooler.updateCharacteristic(C.HeatingThresholdTemperature, snapped);
|
|
198
|
+
}
|
|
199
|
+
await this._trySend(() => this.sendTemperature(snapped));
|
|
179
200
|
});
|
|
180
201
|
|
|
181
202
|
this.heaterCooler.getCharacteristic(C.HeatingThresholdTemperature)
|
|
182
|
-
.setProps({ minValue: 16, maxValue: 32, minStep:
|
|
183
|
-
.onGet(() => this.state.targetTemp)
|
|
203
|
+
.setProps({ minValue: 16, maxValue: 32, minStep: 1 })
|
|
204
|
+
.onGet(() => snapTemp(this.state.targetTemp))
|
|
184
205
|
.onSet(async (value) => {
|
|
185
|
-
|
|
186
|
-
|
|
206
|
+
const snapped = snapTemp(value);
|
|
207
|
+
this.state.targetTemp = snapped;
|
|
208
|
+
if (snapped !== value) {
|
|
209
|
+
this.heaterCooler.updateCharacteristic(C.CoolingThresholdTemperature, snapped);
|
|
210
|
+
this.heaterCooler.updateCharacteristic(C.HeatingThresholdTemperature, snapped);
|
|
211
|
+
}
|
|
212
|
+
await this._trySend(() => this.sendTemperature(snapped));
|
|
187
213
|
});
|
|
188
214
|
|
|
189
215
|
this.heaterCooler.getCharacteristic(C.SwingMode)
|
|
@@ -194,7 +220,7 @@ class AcFreedomAccessory {
|
|
|
194
220
|
const on = value === C.SwingMode.SWING_ENABLED;
|
|
195
221
|
this.state.swingV = on;
|
|
196
222
|
this.state.swingH = on;
|
|
197
|
-
await this.sendSwing(on, on);
|
|
223
|
+
await this._trySend(() => this.sendSwing(on, on));
|
|
198
224
|
});
|
|
199
225
|
}
|
|
200
226
|
|
|
@@ -227,18 +253,22 @@ class AcFreedomAccessory {
|
|
|
227
253
|
this.fanService.getCharacteristic(C.Active)
|
|
228
254
|
.onGet(() => this.state.power ? C.Active.ACTIVE : C.Active.INACTIVE)
|
|
229
255
|
.onSet(async (value) => {
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
256
|
+
const newPower = value === C.Active.ACTIVE;
|
|
257
|
+
if (newPower === this.state.power) return;
|
|
258
|
+
this.state.power = newPower;
|
|
259
|
+
if (!newPower) this._powerOffReset();
|
|
260
|
+
this._powerJustChanged = true;
|
|
261
|
+
setTimeout(() => { this._powerJustChanged = false; }, 300);
|
|
262
|
+
await this._trySend(() => this.sendPower(newPower));
|
|
233
263
|
});
|
|
234
264
|
|
|
235
265
|
// 0=Auto 25=Low 50=Medium 75=High 100=Turbo
|
|
236
266
|
this.fanService.getCharacteristic(C.RotationSpeed)
|
|
237
267
|
.setProps({ minValue: 0, maxValue: 100, minStep: 25 })
|
|
238
|
-
.onGet(() => this.fanSpeedToPercent(this.state.fanSpeed))
|
|
268
|
+
.onGet(() => this.state.power ? this.fanSpeedToPercent(this.state.fanSpeed) : 0)
|
|
239
269
|
.onSet(async (value) => {
|
|
240
270
|
this.state.fanSpeed = this.percentToFanSpeed(value);
|
|
241
|
-
await this.sendFanSpeed(this.state.fanSpeed);
|
|
271
|
+
await this._trySend(() => this.sendFanSpeed(this.state.fanSpeed));
|
|
242
272
|
});
|
|
243
273
|
|
|
244
274
|
this.heaterCooler.addLinkedService(this.fanService);
|
|
@@ -262,7 +292,7 @@ class AcFreedomAccessory {
|
|
|
262
292
|
svc.setCharacteristic(this.Characteristic.Name, cfg.label);
|
|
263
293
|
|
|
264
294
|
svc.getCharacteristic(this.Characteristic.On)
|
|
265
|
-
.onGet(() => this.state[key])
|
|
295
|
+
.onGet(() => this.state.power && this.state[key])
|
|
266
296
|
.onSet(async (value) => {
|
|
267
297
|
if (value) {
|
|
268
298
|
for (const otherKey of Object.keys(this.presetConfigs)) {
|
|
@@ -270,7 +300,7 @@ class AcFreedomAccessory {
|
|
|
270
300
|
}
|
|
271
301
|
}
|
|
272
302
|
this.state[key] = value;
|
|
273
|
-
await this.sendPreset(key, value);
|
|
303
|
+
await this._trySend(() => this.sendPreset(key, value));
|
|
274
304
|
this.refreshPresetSwitches(key);
|
|
275
305
|
});
|
|
276
306
|
|
|
@@ -301,10 +331,10 @@ class AcFreedomAccessory {
|
|
|
301
331
|
this.comfWindSwitch.setCharacteristic(this.Characteristic.Name, 'Comfortable Wind');
|
|
302
332
|
|
|
303
333
|
this.comfWindSwitch.getCharacteristic(this.Characteristic.On)
|
|
304
|
-
.onGet(() => this.state.comfwind)
|
|
334
|
+
.onGet(() => this.state.power && this.state.comfwind)
|
|
305
335
|
.onSet(async (value) => {
|
|
306
336
|
this.state.comfwind = value;
|
|
307
|
-
await this.sendComfWind(value);
|
|
337
|
+
await this._trySend(() => this.sendComfWind(value));
|
|
308
338
|
});
|
|
309
339
|
|
|
310
340
|
this.heaterCooler.addLinkedService(this.comfWindSwitch);
|
|
@@ -327,28 +357,22 @@ class AcFreedomAccessory {
|
|
|
327
357
|
.onGet(() => this.state.display)
|
|
328
358
|
.onSet(async (value) => {
|
|
329
359
|
this.state.display = value;
|
|
330
|
-
await this.sendDisplay(value);
|
|
360
|
+
await this._trySend(() => this.sendDisplay(value));
|
|
331
361
|
});
|
|
332
362
|
|
|
333
363
|
this.heaterCooler.addLinkedService(this.displaySwitch);
|
|
334
364
|
}
|
|
335
365
|
|
|
336
|
-
// ──
|
|
337
|
-
|
|
366
|
+
// ── Power-off UI reset ─────────────────────────────────────────
|
|
367
|
+
// Updates HomeKit display only. No cloud commands — avoids interfering
|
|
368
|
+
// with the power-off command and prevents spurious device wake-up.
|
|
369
|
+
_powerOffReset() {
|
|
338
370
|
this.state.fanSpeed = FAN_SPEED.AUTO;
|
|
339
|
-
|
|
371
|
+
this.state.sleep = false;
|
|
372
|
+
if (this.fanService)
|
|
340
373
|
this.fanService.updateCharacteristic(this.Characteristic.RotationSpeed, 0);
|
|
341
|
-
|
|
342
|
-
if (sendCommand) this.sendFanSpeed(FAN_SPEED.AUTO).catch(() => {});
|
|
343
|
-
}
|
|
344
|
-
|
|
345
|
-
resetSleep() {
|
|
346
|
-
if (!this.state.sleep) return;
|
|
347
|
-
this.state.sleep = false;
|
|
348
|
-
if (this.presetSwitches?.sleep) {
|
|
374
|
+
if (this.presetSwitches?.sleep)
|
|
349
375
|
this.presetSwitches.sleep.updateCharacteristic(this.Characteristic.On, false);
|
|
350
|
-
}
|
|
351
|
-
this.sendPreset('sleep', false).catch(() => {});
|
|
352
376
|
}
|
|
353
377
|
|
|
354
378
|
// Fan speed ↔ HomeKit percent (canonical cloud numbering)
|
|
@@ -368,6 +392,7 @@ class AcFreedomAccessory {
|
|
|
368
392
|
|
|
369
393
|
// ── Poll state ─────────────────────────────────────────────────
|
|
370
394
|
async pollState() {
|
|
395
|
+
if (this._pollLockUntil && Date.now() < this._pollLockUntil) return;
|
|
371
396
|
try {
|
|
372
397
|
const t = this.deviceApi.type;
|
|
373
398
|
if (t === 'local') {
|
|
@@ -403,8 +428,10 @@ class AcFreedomAccessory {
|
|
|
403
428
|
|
|
404
429
|
this.state.power = !!params[CLOUD.POWER];
|
|
405
430
|
this.state.mode = params[CLOUD.MODE] ?? CLOUD_MODE.AUTO;
|
|
406
|
-
|
|
407
|
-
|
|
431
|
+
const rawTarget = (params[CLOUD.TEMP_TARGET] ?? 240) / 10;
|
|
432
|
+
const rawAmbient = (params[CLOUD.TEMP_AMBIENT] ?? 240) / 10;
|
|
433
|
+
if (rawTarget >= 16 && rawTarget <= 32) this.state.targetTemp = rawTarget;
|
|
434
|
+
if (rawAmbient >= 0 && rawAmbient <= 60) this.state.currentTemp = rawAmbient;
|
|
408
435
|
this.state.fanSpeed = params[CLOUD.FAN_SPEED] ?? FAN_SPEED.AUTO;
|
|
409
436
|
this.state.swingV = !!params[CLOUD.SWING_V];
|
|
410
437
|
this.state.swingH = !!params[CLOUD.SWING_H];
|
|
@@ -434,7 +461,7 @@ class AcFreedomAccessory {
|
|
|
434
461
|
this.state.targetTemp = s.temperature;
|
|
435
462
|
this.state.currentTemp = s.ambientTemp;
|
|
436
463
|
// Normalise local fan speed → canonical cloud numbering
|
|
437
|
-
this.state.fanSpeed =
|
|
464
|
+
this.state.fanSpeed = FAN_REMAP[s.fanSpeed] ?? FAN_SPEED.AUTO;
|
|
438
465
|
this.state.swingV = s.verticalFixation === 7;
|
|
439
466
|
this.state.swingH = s.horizontalFixation === 7;
|
|
440
467
|
this.state.sleep = !!s.sleep;
|
|
@@ -466,9 +493,10 @@ class AcFreedomAccessory {
|
|
|
466
493
|
this.heaterCooler.updateCharacteristic(C.CurrentHeaterCoolerState, hcState);
|
|
467
494
|
}
|
|
468
495
|
|
|
496
|
+
const snapped = Math.round(this.state.targetTemp);
|
|
469
497
|
this.heaterCooler.updateCharacteristic(C.CurrentTemperature, this.state.currentTemp);
|
|
470
|
-
this.heaterCooler.updateCharacteristic(C.CoolingThresholdTemperature,
|
|
471
|
-
this.heaterCooler.updateCharacteristic(C.HeatingThresholdTemperature,
|
|
498
|
+
this.heaterCooler.updateCharacteristic(C.CoolingThresholdTemperature, snapped);
|
|
499
|
+
this.heaterCooler.updateCharacteristic(C.HeatingThresholdTemperature, snapped);
|
|
472
500
|
this.heaterCooler.updateCharacteristic(C.SwingMode,
|
|
473
501
|
(this.state.swingV || this.state.swingH) ? C.SwingMode.SWING_ENABLED : C.SwingMode.SWING_DISABLED);
|
|
474
502
|
|
|
@@ -480,11 +508,11 @@ class AcFreedomAccessory {
|
|
|
480
508
|
}
|
|
481
509
|
|
|
482
510
|
for (const [key, svc] of Object.entries(this.presetSwitches || {})) {
|
|
483
|
-
svc.updateCharacteristic(C.On, this.state[key]);
|
|
511
|
+
svc.updateCharacteristic(C.On, this.state.power && this.state[key]);
|
|
484
512
|
}
|
|
485
513
|
|
|
486
514
|
if (this.comfWindSwitch) {
|
|
487
|
-
this.comfWindSwitch.updateCharacteristic(C.On, this.state.comfwind);
|
|
515
|
+
this.comfWindSwitch.updateCharacteristic(C.On, this.state.power && this.state.comfwind);
|
|
488
516
|
}
|
|
489
517
|
if (this.displaySwitch) {
|
|
490
518
|
this.displaySwitch.updateCharacteristic(C.On, this.state.display);
|
|
@@ -512,28 +540,50 @@ class AcFreedomAccessory {
|
|
|
512
540
|
return cloudFn(); // legacy 'cloud' type
|
|
513
541
|
}
|
|
514
542
|
|
|
543
|
+
// Wraps a send call for use inside onSet handlers.
|
|
544
|
+
// Suppresses poll for 5 s after any command to avoid stale cloud state overriding UI.
|
|
545
|
+
// Logs the error and throws HapStatusError so HomeKit shows failure correctly.
|
|
546
|
+
async _trySend(fn) {
|
|
547
|
+
try {
|
|
548
|
+
await fn();
|
|
549
|
+
this._pollLockUntil = Date.now() + 5000;
|
|
550
|
+
} catch (err) {
|
|
551
|
+
this.log.warn('%s: command failed: %s', this.config.name, err.message);
|
|
552
|
+
throw new this.platform.api.hap.HapStatusError(
|
|
553
|
+
this.platform.api.hap.HAPStatus.SERVICE_COMMUNICATION_FAILURE,
|
|
554
|
+
);
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
|
|
515
558
|
// ── Send commands ──────────────────────────────────────────────
|
|
516
559
|
async sendPower(on) {
|
|
517
560
|
return this._send(
|
|
518
|
-
() => {
|
|
519
|
-
|
|
561
|
+
() => {
|
|
562
|
+
const a = this._localApi;
|
|
563
|
+
a.state.power = on ? 1 : 0;
|
|
564
|
+
if (on) a.state.mode = LOCAL_MODE_MAP[this.state.mode] ?? 0;
|
|
565
|
+
if (!on) a.state.fanSpeed = 0;
|
|
566
|
+
return a.setState();
|
|
567
|
+
},
|
|
568
|
+
() => {
|
|
569
|
+
const p = { [CLOUD.POWER]: on ? 1 : 0 };
|
|
570
|
+
if (on) p[CLOUD.MODE] = this.state.mode;
|
|
571
|
+
if (!on) p[CLOUD.FAN_SPEED] = 0;
|
|
572
|
+
return this.cloudSet(p);
|
|
573
|
+
},
|
|
520
574
|
);
|
|
521
575
|
}
|
|
522
576
|
|
|
523
577
|
async sendMode(mode) {
|
|
524
|
-
|
|
525
|
-
const localModeMap = {
|
|
526
|
-
[CLOUD_MODE.AUTO]: 0, [CLOUD_MODE.COOL]: 1,
|
|
527
|
-
[CLOUD_MODE.HEAT]: 4, [CLOUD_MODE.DRY]: 2, [CLOUD_MODE.FAN]: 6,
|
|
528
|
-
};
|
|
578
|
+
const pwr = this.state.power ? 1 : 0;
|
|
529
579
|
return this._send(
|
|
530
580
|
() => {
|
|
531
581
|
const a = this._localApi;
|
|
532
|
-
a.state.power =
|
|
533
|
-
a.state.mode =
|
|
582
|
+
a.state.power = pwr;
|
|
583
|
+
a.state.mode = LOCAL_MODE_MAP[mode] ?? 0;
|
|
534
584
|
return a.setState();
|
|
535
585
|
},
|
|
536
|
-
() => this.cloudSet({ [CLOUD.POWER]:
|
|
586
|
+
() => this.cloudSet({ [CLOUD.POWER]: pwr, [CLOUD.MODE]: mode }),
|
|
537
587
|
);
|
|
538
588
|
}
|
|
539
589
|
|
|
@@ -546,7 +596,7 @@ class AcFreedomAccessory {
|
|
|
546
596
|
|
|
547
597
|
async sendFanSpeed(speed) {
|
|
548
598
|
// speed is canonical cloud numbering; convert for local
|
|
549
|
-
const localSpeed =
|
|
599
|
+
const localSpeed = FAN_REMAP[speed] ?? 0;
|
|
550
600
|
return this._send(
|
|
551
601
|
() => {
|
|
552
602
|
const a = this._localApi;
|
|
@@ -609,6 +659,40 @@ class AcFreedomAccessory {
|
|
|
609
659
|
await this._cloudApi.setDeviceParams(this._cloudDevice, params);
|
|
610
660
|
}
|
|
611
661
|
|
|
662
|
+
// ── Service ordering ───────────────────────────────────────────
|
|
663
|
+
// Sorts accessory.services so HomeKit always shows tiles in the
|
|
664
|
+
// correct order regardless of when each service was first added.
|
|
665
|
+
_reorderServices() {
|
|
666
|
+
const rank = (svc) => {
|
|
667
|
+
const s = svc.subtype;
|
|
668
|
+
if (svc.UUID === this.Service.AccessoryInformation.UUID) return 0;
|
|
669
|
+
if (svc.UUID === this.Service.HeaterCooler.UUID) return 1;
|
|
670
|
+
if (s === 'fan') return 2;
|
|
671
|
+
if (s === 'sleep') return 3;
|
|
672
|
+
if (s === 'display') return 4;
|
|
673
|
+
if (s === 'health') return 5;
|
|
674
|
+
if (s === 'clean') return 6;
|
|
675
|
+
if (s === 'eco') return 7;
|
|
676
|
+
if (s === 'comfwind') return 8;
|
|
677
|
+
return 99;
|
|
678
|
+
};
|
|
679
|
+
this.accessory.services.sort((a, b) => rank(a) - rank(b));
|
|
680
|
+
|
|
681
|
+
// Rebuild linkedServices explicitly in the desired order so the HAP
|
|
682
|
+
// "linked" array is sent correctly regardless of IID values.
|
|
683
|
+
const linked = [
|
|
684
|
+
this.fanService,
|
|
685
|
+
this.presetSwitches?.sleep,
|
|
686
|
+
this.displaySwitch,
|
|
687
|
+
this.presetSwitches?.health,
|
|
688
|
+
this.presetSwitches?.clean,
|
|
689
|
+
this.presetSwitches?.eco,
|
|
690
|
+
this.comfWindSwitch,
|
|
691
|
+
].filter(Boolean);
|
|
692
|
+
this.heaterCooler.linkedServices.length = 0;
|
|
693
|
+
for (const svc of linked) this.heaterCooler.linkedServices.push(svc);
|
|
694
|
+
}
|
|
695
|
+
|
|
612
696
|
// ── Cleanup ────────────────────────────────────────────────────
|
|
613
697
|
destroy() {
|
|
614
698
|
if (this.pollTimer) { clearInterval(this.pollTimer); this.pollTimer = null; }
|
package/src/cloud-api.js
CHANGED
|
@@ -80,6 +80,13 @@ function httpRequest(url, options, body) {
|
|
|
80
80
|
});
|
|
81
81
|
}
|
|
82
82
|
|
|
83
|
+
// Default device parameter keys fetched on every poll
|
|
84
|
+
const DEVICE_PARAMS = [
|
|
85
|
+
'pwr', 'ac_mode', 'temp', 'envtemp', 'ac_mark',
|
|
86
|
+
'ac_vdir', 'ac_hdir', 'ac_slp', 'ac_health',
|
|
87
|
+
'mldprf', 'ac_clean', 'comfwind', 'scrdisp',
|
|
88
|
+
];
|
|
89
|
+
|
|
83
90
|
// ── AuxCloudAPI ──────────────────────────────────────────────────
|
|
84
91
|
class AuxCloudAPI {
|
|
85
92
|
constructor(region = 'eu') {
|
|
@@ -139,7 +146,7 @@ class AuxCloudAPI {
|
|
|
139
146
|
this.userid = result.userid;
|
|
140
147
|
return true;
|
|
141
148
|
}
|
|
142
|
-
throw new Error(`Login failed: ${
|
|
149
|
+
throw new Error(`Login failed: ${result.msg || result.status || 'unknown error'}`);
|
|
143
150
|
}
|
|
144
151
|
|
|
145
152
|
// ── Families ───────────────────────────────────────────────────
|
|
@@ -148,7 +155,7 @@ class AuxCloudAPI {
|
|
|
148
155
|
headers: this._headers(),
|
|
149
156
|
});
|
|
150
157
|
if (result.status === 0) return result.data.familyList || [];
|
|
151
|
-
throw new Error(`Get families failed: ${
|
|
158
|
+
throw new Error(`Get families failed: ${result.msg || result.status || 'unknown error'}`);
|
|
152
159
|
}
|
|
153
160
|
|
|
154
161
|
// ── Devices ────────────────────────────────────────────────────
|
|
@@ -159,7 +166,7 @@ class AuxCloudAPI {
|
|
|
159
166
|
});
|
|
160
167
|
|
|
161
168
|
if (result.status !== 0) {
|
|
162
|
-
throw new Error(`Get devices failed: ${
|
|
169
|
+
throw new Error(`Get devices failed: ${result.msg || result.status || 'unknown error'}`);
|
|
163
170
|
}
|
|
164
171
|
|
|
165
172
|
const devices = result.data?.endpoints || [];
|
|
@@ -179,7 +186,12 @@ class AuxCloudAPI {
|
|
|
179
186
|
|
|
180
187
|
// ── Get / Set device params ────────────────────────────────────
|
|
181
188
|
async _actDeviceParams(device, act, params = [], vals = []) {
|
|
182
|
-
|
|
189
|
+
let cookieRaw;
|
|
190
|
+
try {
|
|
191
|
+
cookieRaw = JSON.parse(Buffer.from(device.cookie, 'base64').toString());
|
|
192
|
+
} catch {
|
|
193
|
+
throw new Error('Device cookie is malformed — re-fetch devices in plugin settings');
|
|
194
|
+
}
|
|
183
195
|
const mappedCookie = Buffer.from(JSON.stringify({
|
|
184
196
|
device: {
|
|
185
197
|
id: cookieRaw.terminalid,
|
|
@@ -239,17 +251,12 @@ class AuxCloudAPI {
|
|
|
239
251
|
return out;
|
|
240
252
|
}
|
|
241
253
|
|
|
242
|
-
|
|
254
|
+
const errPayload = result?.event?.payload || {};
|
|
255
|
+
const reason = errPayload.message || errPayload.type || result?.event?.header?.name || 'unknown error';
|
|
256
|
+
throw new Error(`Device unreachable: ${reason}`);
|
|
243
257
|
}
|
|
244
258
|
|
|
245
|
-
async getDeviceParams(device, params) {
|
|
246
|
-
if (!params) {
|
|
247
|
-
params = [
|
|
248
|
-
'pwr', 'ac_mode', 'temp', 'envtemp', 'ac_mark',
|
|
249
|
-
'ac_vdir', 'ac_hdir', 'ac_slp', 'ac_health',
|
|
250
|
-
'mldprf', 'ac_clean', 'comfwind', 'scrdisp',
|
|
251
|
-
];
|
|
252
|
-
}
|
|
259
|
+
async getDeviceParams(device, params = DEVICE_PARAMS) {
|
|
253
260
|
return this._actDeviceParams(device, 'get', params);
|
|
254
261
|
}
|
|
255
262
|
|
package/src/platform.js
CHANGED
|
@@ -34,13 +34,11 @@ class AcFreedomPlatform {
|
|
|
34
34
|
if (!api || !config) return;
|
|
35
35
|
|
|
36
36
|
this.api.on('didFinishLaunching', () => {
|
|
37
|
-
this.log.info('AC Freedom platform loaded');
|
|
38
37
|
this.discoverDevices().catch(err => this.log.error('Fatal: %s', err.message));
|
|
39
38
|
});
|
|
40
39
|
}
|
|
41
40
|
|
|
42
41
|
configureAccessory(accessory) {
|
|
43
|
-
this.log.info('Restoring cached accessory: %s', accessory.displayName);
|
|
44
42
|
this.accessories.set(accessory.UUID, accessory);
|
|
45
43
|
}
|
|
46
44
|
|
|
@@ -72,7 +70,6 @@ class AcFreedomPlatform {
|
|
|
72
70
|
const api = new AuxCloudAPI(cloudCreds.region || 'eu');
|
|
73
71
|
try {
|
|
74
72
|
await api.login(cloudCreds.email, cloudCreds.password);
|
|
75
|
-
this.log.info('Cloud login successful: %s', cloudCreds.email);
|
|
76
73
|
|
|
77
74
|
const families = await api.getFamilies();
|
|
78
75
|
const allDevices = [];
|
|
@@ -80,7 +77,7 @@ class AcFreedomPlatform {
|
|
|
80
77
|
const devs = await api.getDevices(fam.familyid);
|
|
81
78
|
allDevices.push(...devs);
|
|
82
79
|
}
|
|
83
|
-
this.log.info('
|
|
80
|
+
this.log.info('Cloud: %s — %d device(s)', cloudCreds.email, allDevices.length);
|
|
84
81
|
return { api, devices: allDevices };
|
|
85
82
|
} catch (err) {
|
|
86
83
|
this.log.error('Cloud login failed: %s', err.message);
|
|
@@ -114,12 +111,10 @@ class AcFreedomPlatform {
|
|
|
114
111
|
|
|
115
112
|
const existingAccessory = this.accessories.get(uuid);
|
|
116
113
|
if (existingAccessory) {
|
|
117
|
-
this.log.info('Updating existing accessory: %s', deviceConfig.name);
|
|
118
114
|
const instance = new AcFreedomAccessory(this, existingAccessory, deviceConfig, deviceApi);
|
|
119
115
|
this.instances.set(uuid, instance);
|
|
120
116
|
this.api.updatePlatformAccessories([existingAccessory]);
|
|
121
117
|
} else {
|
|
122
|
-
this.log.info('Adding new accessory: %s', deviceConfig.name);
|
|
123
118
|
const accessory = new this.api.platformAccessory(deviceConfig.name, uuid);
|
|
124
119
|
const instance = new AcFreedomAccessory(this, accessory, deviceConfig, deviceApi);
|
|
125
120
|
this.instances.set(uuid, instance);
|
|
@@ -184,40 +179,25 @@ class AcFreedomPlatform {
|
|
|
184
179
|
}
|
|
185
180
|
}
|
|
186
181
|
|
|
187
|
-
// Try local connection
|
|
182
|
+
// Try local connection only when explicitly set to hybrid mode
|
|
188
183
|
let localApi = null;
|
|
189
184
|
const localCfg = deviceConfig.local;
|
|
190
|
-
if (localCfg?.ip && localCfg?.mac) {
|
|
185
|
+
if (deviceConfig.connection === 'hybrid' && localCfg?.ip && localCfg?.mac) {
|
|
191
186
|
const broadlink = new BroadlinkAcApi(localCfg.ip, localCfg.mac);
|
|
192
187
|
try {
|
|
193
188
|
const connected = await broadlink.connect();
|
|
194
189
|
if (connected) {
|
|
195
|
-
this.log.info(
|
|
196
|
-
'Hybrid device %s: local connected at %s (preferred)',
|
|
197
|
-
deviceConfig.name, localCfg.ip,
|
|
198
|
-
);
|
|
199
190
|
localApi = broadlink;
|
|
200
191
|
} else {
|
|
201
|
-
this.log.warn(
|
|
202
|
-
'Hybrid device %s: local connect failed at %s — using cloud fallback',
|
|
203
|
-
deviceConfig.name, localCfg.ip,
|
|
204
|
-
);
|
|
192
|
+
this.log.warn('%s: local connect failed at %s — using cloud', deviceConfig.name, localCfg.ip);
|
|
205
193
|
}
|
|
206
194
|
} catch (err) {
|
|
207
|
-
this.log.warn(
|
|
208
|
-
'Hybrid device %s: local connect error (%s) — using cloud fallback',
|
|
209
|
-
deviceConfig.name, err.message,
|
|
210
|
-
);
|
|
195
|
+
this.log.warn('%s: local connect error (%s) — using cloud', deviceConfig.name, err.message);
|
|
211
196
|
}
|
|
212
197
|
}
|
|
213
198
|
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
deviceConfig.name,
|
|
217
|
-
cloudDevice.friendlyName || 'AUX AC',
|
|
218
|
-
cloudDevice.endpointId,
|
|
219
|
-
localApi ? 'local+cloud' : 'cloud only',
|
|
220
|
-
);
|
|
199
|
+
const mode = localApi ? `local+cloud (${localCfg.ip})` : 'cloud';
|
|
200
|
+
this.log.info('%s: %s', deviceConfig.name, mode);
|
|
221
201
|
|
|
222
202
|
return {
|
|
223
203
|
type: 'hybrid',
|
|
@@ -239,13 +219,13 @@ class AcFreedomPlatform {
|
|
|
239
219
|
try {
|
|
240
220
|
const connected = await api.connect();
|
|
241
221
|
if (!connected) {
|
|
242
|
-
this.log.error('
|
|
222
|
+
this.log.error('%s: local connect failed at %s', config.name, local.ip);
|
|
243
223
|
return null;
|
|
244
224
|
}
|
|
245
|
-
this.log.info('
|
|
225
|
+
this.log.info('%s: local (%s)', config.name, local.ip);
|
|
246
226
|
return { type: 'local', api };
|
|
247
227
|
} catch (err) {
|
|
248
|
-
this.log.error('
|
|
228
|
+
this.log.error('%s: local connect error — %s', config.name, err.message);
|
|
249
229
|
return null;
|
|
250
230
|
}
|
|
251
231
|
}
|