homebridge-ac-freedom 2.3.3 → 2.3.5
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 +28 -0
- package/README.md +1 -0
- package/config.schema.json +4 -25
- package/homebridge-ui/public/index.html +42 -75
- 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/homebridge-ui/public/i18n/ar.json +0 -30
- package/homebridge-ui/public/i18n/de.json +0 -30
- package/homebridge-ui/public/i18n/en.json +0 -30
- package/homebridge-ui/public/i18n/es.json +0 -30
- package/homebridge-ui/public/i18n/fr.json +0 -30
- package/homebridge-ui/public/i18n/it.json +0 -30
- package/homebridge-ui/public/i18n/ja.json +0 -30
- package/homebridge-ui/public/i18n/ko.json +0 -30
- package/homebridge-ui/public/i18n/nl.json +0 -30
- package/homebridge-ui/public/i18n/pl.json +0 -30
- package/homebridge-ui/public/i18n/pt.json +0 -30
- package/homebridge-ui/public/i18n/ru.json +0 -30
- package/homebridge-ui/public/i18n/tr.json +0 -30
- package/homebridge-ui/public/i18n/zh-CN.json +0 -30
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,33 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 2.3.5
|
|
4
|
+
|
|
5
|
+
- **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`
|
|
6
|
+
- **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
|
|
7
|
+
- **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
|
|
8
|
+
- **Fix stale poll overriding command** — poll is suppressed for 5 s after any successful user command; a failed command no longer blocks state refresh
|
|
9
|
+
- **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
|
|
10
|
+
- **Fix preset switches showing ON when AC is off** — `onGet` and `updateCharacteristics` now gate preset, fan and comfwind values on `state.power`
|
|
11
|
+
- **Fix temperature step** — temperature is snapped in `onSet` and pushed back to HomeKit immediately; corrects slider behaviour even when HomeKit has a cached `minStep`
|
|
12
|
+
- **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
|
|
13
|
+
- **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"
|
|
14
|
+
- **Fix cloud error messages** — cloud API errors now extract `payload.message` instead of dumping the full JSON response
|
|
15
|
+
- **Fix `connection: "cloud"` trying local** — local connection is only attempted when `connection` is explicitly `"hybrid"`
|
|
16
|
+
- **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
|
|
17
|
+
- **Fix crash on command failure** — `_trySend` now correctly references `this.platform.api.hap` instead of the undefined `this.api`
|
|
18
|
+
- **Fix malformed device cookie** — cloud API now throws an actionable error instead of crashing with a JSON parse exception
|
|
19
|
+
- **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
|
|
20
|
+
- **Cleaner startup log** — reduced to one line per device; removed redundant "Restoring / Updating accessory" messages
|
|
21
|
+
- **Code cleanup** — `LOCAL_MODE_MAP` and `FAN_REMAP` extracted as module-level constants; cloud API default params moved to `DEVICE_PARAMS` constant
|
|
22
|
+
|
|
23
|
+
## 2.3.4
|
|
24
|
+
|
|
25
|
+
- **UI: badge colors** — Cloud (teal), Hybrid (green), Local (yellow) — clearly distinct from each other
|
|
26
|
+
- **UI: card colors** — all device cards now use the same neutral color regardless of connection mode
|
|
27
|
+
- **UI: Local device numbering** — Local devices now always start from Local #1, independent of cloud device count
|
|
28
|
+
- **UI: Poll Interval label** — removed "(seconds)" from the label; dropdown options now show "5 seconds", "10 seconds", etc.
|
|
29
|
+
- **UI: English only** — removed all 14 language files; strings are now embedded directly in the UI (no i18n fetch on load)
|
|
30
|
+
|
|
3
31
|
## 2.3.3
|
|
4
32
|
|
|
5
33
|
- **Hybrid offline startup** — if cloud is unavailable at Homebridge startup and the device has a local IP + MAC configured, the device now starts in local-only mode instead of failing. Full hybrid mode resumes automatically on the next restart when internet is available.
|
package/README.md
CHANGED
|
@@ -37,6 +37,7 @@ A [Homebridge](https://homebridge.io) plugin for controlling **AUX-based air con
|
|
|
37
37
|
- **Multi-device** — configure as many ACs as needed
|
|
38
38
|
- **Custom config UI** — built-in Homebridge UI with Fetch Devices button
|
|
39
39
|
- Homebridge v1 & v2 compatible
|
|
40
|
+
- English UI
|
|
40
41
|
|
|
41
42
|
---
|
|
42
43
|
|
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
|
]
|
|
@@ -3,12 +3,15 @@
|
|
|
3
3
|
--accent: #0095c8;
|
|
4
4
|
--accent-light: rgba(0,149,200,0.10);
|
|
5
5
|
--accent-border: rgba(0,149,200,0.28);
|
|
6
|
-
--hybrid: #
|
|
7
|
-
--hybrid-light: rgba(
|
|
8
|
-
--hybrid-border: rgba(
|
|
9
|
-
--
|
|
10
|
-
--
|
|
11
|
-
--
|
|
6
|
+
--hybrid: #16a34a;
|
|
7
|
+
--hybrid-light: rgba(22,163,74,0.08);
|
|
8
|
+
--hybrid-border: rgba(22,163,74,0.22);
|
|
9
|
+
--cloud-badge: #0095c8;
|
|
10
|
+
--cloud-badge-light: rgba(0,149,200,0.08);
|
|
11
|
+
--cloud-badge-border: rgba(0,149,200,0.28);
|
|
12
|
+
--local: #a16207;
|
|
13
|
+
--local-light: rgba(161,98,7,0.08);
|
|
14
|
+
--local-border: rgba(161,98,7,0.25);
|
|
12
15
|
--text: #1a1a2e;
|
|
13
16
|
--text-muted: #6b7280;
|
|
14
17
|
--border: #e5e7eb;
|
|
@@ -22,12 +25,15 @@
|
|
|
22
25
|
--accent: #00b4d8;
|
|
23
26
|
--accent-light: rgba(0,180,216,0.08);
|
|
24
27
|
--accent-border: rgba(0,180,216,0.26);
|
|
25
|
-
--hybrid: #
|
|
26
|
-
--hybrid-light: rgba(
|
|
27
|
-
--hybrid-border: rgba(
|
|
28
|
-
--
|
|
29
|
-
--
|
|
30
|
-
--
|
|
28
|
+
--hybrid: #4ade80;
|
|
29
|
+
--hybrid-light: rgba(74,222,128,0.08);
|
|
30
|
+
--hybrid-border: rgba(74,222,128,0.22);
|
|
31
|
+
--cloud-badge: #00b4d8;
|
|
32
|
+
--cloud-badge-light: rgba(0,180,216,0.08);
|
|
33
|
+
--cloud-badge-border: rgba(0,180,216,0.26);
|
|
34
|
+
--local: #facc15;
|
|
35
|
+
--local-light: rgba(250,204,21,0.08);
|
|
36
|
+
--local-border: rgba(250,204,21,0.22);
|
|
31
37
|
--text: #e2e8f0;
|
|
32
38
|
--text-muted: #94a3b8;
|
|
33
39
|
--border: #334155;
|
|
@@ -102,8 +108,6 @@
|
|
|
102
108
|
border: 1.5px solid var(--accent-border); border-radius: 12px;
|
|
103
109
|
margin-bottom: 12px; background: var(--accent-light); overflow: hidden;
|
|
104
110
|
}
|
|
105
|
-
.device-card.hybrid-card { border-color: var(--hybrid-border); background: var(--hybrid-light); }
|
|
106
|
-
.device-card.local-card { border-color: var(--local-border); background: var(--local-light); }
|
|
107
111
|
|
|
108
112
|
.device-header {
|
|
109
113
|
display: flex; justify-content: space-between; align-items: center;
|
|
@@ -115,14 +119,12 @@
|
|
|
115
119
|
.chevron.open { transform: rotate(90deg); }
|
|
116
120
|
.dev-name { font-size: 14px; font-weight: 700; color: var(--accent);
|
|
117
121
|
white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
118
|
-
.device-card.hybrid-card .dev-name { color: var(--hybrid); }
|
|
119
|
-
.device-card.local-card .dev-name { color: var(--local); }
|
|
120
122
|
|
|
121
123
|
.conn-badge {
|
|
122
124
|
font-size: 10px; font-weight: 700; letter-spacing: .5px;
|
|
123
125
|
padding: 2px 7px; border-radius: 20px; flex-shrink: 0; text-transform: uppercase;
|
|
124
126
|
}
|
|
125
|
-
.conn-badge.cloud { background: var(--
|
|
127
|
+
.conn-badge.cloud { background: var(--cloud-badge-light); color: var(--cloud-badge); border: 1px solid var(--cloud-badge-border); }
|
|
126
128
|
.conn-badge.hybrid { background: var(--hybrid-light); color: var(--hybrid); border: 1px solid var(--hybrid-border); }
|
|
127
129
|
.conn-badge.local { background: var(--local-light); color: var(--local); border: 1px solid var(--local-border); }
|
|
128
130
|
.dev-meta { font-size: 11px; color: var(--text-muted); white-space: nowrap; }
|
|
@@ -131,8 +133,6 @@
|
|
|
131
133
|
padding: 14px 16px 18px;
|
|
132
134
|
border-top: 1px solid var(--accent-border);
|
|
133
135
|
}
|
|
134
|
-
.device-card.hybrid-card .device-body { border-color: var(--hybrid-border); }
|
|
135
|
-
.device-card.local-card .device-body { border-color: var(--local-border); }
|
|
136
136
|
.device-body.collapsed { display: none; }
|
|
137
137
|
|
|
138
138
|
/* ── Connection toggle (segmented) ── */
|
|
@@ -215,19 +215,21 @@
|
|
|
215
215
|
}
|
|
216
216
|
});
|
|
217
217
|
|
|
218
|
-
/* ──
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
218
|
+
/* ── Strings ──────────────────────────────────────────────── */
|
|
219
|
+
const t = {
|
|
220
|
+
deviceName: 'Device Name', connection: 'Connection Mode',
|
|
221
|
+
cloud: 'Cloud', local: 'Local',
|
|
222
|
+
email: 'Email', password: 'Password', region: 'Region',
|
|
223
|
+
ip: 'IP Address', mac: 'MAC Address',
|
|
224
|
+
fan: 'Fan', sleep: 'Sleep', display: 'Display',
|
|
225
|
+
health: 'Health', clean: 'Clean', eco: 'Eco',
|
|
226
|
+
comfWind: 'Comf. Wind',
|
|
227
|
+
addDevice: 'Add Device', remove: 'Remove',
|
|
228
|
+
hybrid: 'Hybrid', cloudCredentials: 'Cloud Credentials',
|
|
229
|
+
fetching: 'Fetching…', devicesFound: 'devices found',
|
|
230
|
+
fetchFailed: 'Fetch failed', fetch: 'Fetch',
|
|
231
|
+
cloudDevices: 'Cloud Devices', localDevices: 'Local Devices',
|
|
232
|
+
};
|
|
231
233
|
|
|
232
234
|
/* ── Config ───────────────────────────────────────────────── */
|
|
233
235
|
let config = {};
|
|
@@ -273,32 +275,6 @@
|
|
|
273
275
|
`;
|
|
274
276
|
}
|
|
275
277
|
|
|
276
|
-
function settingsRow(d, i) {
|
|
277
|
-
const step = d.tempStep !== undefined ? d.tempStep : 0.5;
|
|
278
|
-
const poll = d.pollInterval || 30;
|
|
279
|
-
return `
|
|
280
|
-
<div class="fieldset" style="margin-bottom:12px">
|
|
281
|
-
<div class="row-2">
|
|
282
|
-
<div class="fg" style="margin-bottom:0">
|
|
283
|
-
<label>${t.tempStep||'Temperature Step'}</label>
|
|
284
|
-
<select class="d-field" data-i="${i}" data-f="tempStep">
|
|
285
|
-
<option value="0.5" ${step==0.5?'selected':''}>0.5 °C</option>
|
|
286
|
-
<option value="1" ${step==1 ?'selected':''}>1.0 °C</option>
|
|
287
|
-
</select>
|
|
288
|
-
</div>
|
|
289
|
-
<div class="fg" style="margin-bottom:0">
|
|
290
|
-
<label>${t.pollInterval||'Poll Interval'}</label>
|
|
291
|
-
<select class="d-field" data-i="${i}" data-f="pollInterval">
|
|
292
|
-
<option value="5" ${poll==5 ?'selected':''}>5 s</option>
|
|
293
|
-
<option value="10" ${poll==10?'selected':''}>10 s</option>
|
|
294
|
-
<option value="20" ${poll==20?'selected':''}>20 s</option>
|
|
295
|
-
<option value="30" ${poll==30?'selected':''}>30 s</option>
|
|
296
|
-
<option value="60" ${poll==60?'selected':''}>60 s</option>
|
|
297
|
-
</select>
|
|
298
|
-
</div>
|
|
299
|
-
</div>
|
|
300
|
-
</div>`;
|
|
301
|
-
}
|
|
302
278
|
|
|
303
279
|
/* ── Render cloud device card ─────────────────────────────── */
|
|
304
280
|
function renderCloudDevice(d, i) {
|
|
@@ -307,8 +283,6 @@
|
|
|
307
283
|
const conn = (d.connection === 'local' || !d.connection) ? 'cloud' : d.connection;
|
|
308
284
|
const isHybrid = conn === 'hybrid';
|
|
309
285
|
const local = d.local || {};
|
|
310
|
-
const step = d.tempStep !== undefined ? d.tempStep : 0.5;
|
|
311
|
-
const poll = d.pollInterval || 30;
|
|
312
286
|
const cardClass = isHybrid ? 'hybrid-card' : '';
|
|
313
287
|
const badge = isHybrid ? (t.hybrid||'Hybrid') : (t.cloud||'Cloud');
|
|
314
288
|
|
|
@@ -368,19 +342,16 @@
|
|
|
368
342
|
</div>
|
|
369
343
|
</div>
|
|
370
344
|
|
|
371
|
-
${settingsRow(d, i)}
|
|
372
345
|
${featToggles(d, i)}
|
|
373
346
|
</div>
|
|
374
347
|
</div>`;
|
|
375
348
|
}
|
|
376
349
|
|
|
377
350
|
/* ── Render local-only device card ───────────────────────── */
|
|
378
|
-
function renderLocalDevice(d, i) {
|
|
351
|
+
function renderLocalDevice(d, i, li) {
|
|
379
352
|
const isOpen = expanded.has(i);
|
|
380
|
-
const name = d.name || `Local #${
|
|
353
|
+
const name = d.name || `Local #${li + 1}`;
|
|
381
354
|
const local = d.local || {};
|
|
382
|
-
const step = d.tempStep !== undefined ? d.tempStep : 0.5;
|
|
383
|
-
const poll = d.pollInterval || 30;
|
|
384
355
|
|
|
385
356
|
return `
|
|
386
357
|
<div class="device-card" id="dev-card-${i}">
|
|
@@ -390,7 +361,7 @@
|
|
|
390
361
|
<span class="dev-name">${esc(name)}</span>
|
|
391
362
|
</div>
|
|
392
363
|
<div style="display:flex;align-items:center;gap:8px;flex-shrink:0;margin-left:8px">
|
|
393
|
-
<span class="conn-badge
|
|
364
|
+
<span class="conn-badge local">${t.local||'Local'}</span>
|
|
394
365
|
<button class="btn btn-danger btn-sm btn-remove" data-i="${i}">${t.remove||'Remove'}</button>
|
|
395
366
|
</div>
|
|
396
367
|
</div>
|
|
@@ -418,7 +389,6 @@
|
|
|
418
389
|
</div>
|
|
419
390
|
</div>
|
|
420
391
|
|
|
421
|
-
${settingsRow(d, i)}
|
|
422
392
|
${featToggles(d, i)}
|
|
423
393
|
</div>
|
|
424
394
|
</div>`;
|
|
@@ -431,7 +401,8 @@
|
|
|
431
401
|
.filter(({ d }) => d.connection !== 'local');
|
|
432
402
|
const localDevs = config.devices
|
|
433
403
|
.map((d, i) => ({ d, i }))
|
|
434
|
-
.filter(({ d }) => d.connection === 'local')
|
|
404
|
+
.filter(({ d }) => d.connection === 'local')
|
|
405
|
+
.map((item, li) => ({ ...item, li }));
|
|
435
406
|
|
|
436
407
|
app.innerHTML = `
|
|
437
408
|
<!-- Cloud Credentials -->
|
|
@@ -486,7 +457,7 @@
|
|
|
486
457
|
${t.localDevices||'Local Devices'}
|
|
487
458
|
</div>
|
|
488
459
|
<div id="local-device-list">
|
|
489
|
-
${localDevs.map(({ d, i }) => renderLocalDevice(d, i)).join('')}
|
|
460
|
+
${localDevs.map(({ d, i, li }) => renderLocalDevice(d, i, li)).join('')}
|
|
490
461
|
</div>
|
|
491
462
|
<div class="fieldset" style="margin-top:10px">
|
|
492
463
|
<button class="btn btn-primary" id="btn-add-local"
|
|
@@ -568,7 +539,6 @@
|
|
|
568
539
|
local: { ip: '', mac: '' },
|
|
569
540
|
presets: { sleep: true, health: false, eco: false, clean: false },
|
|
570
541
|
showFan: true, showComfWind: false, showDisplay: false,
|
|
571
|
-
tempStep: 0.5, pollInterval: 30,
|
|
572
542
|
});
|
|
573
543
|
expanded.add(config.devices.length - 1);
|
|
574
544
|
added++;
|
|
@@ -598,7 +568,6 @@
|
|
|
598
568
|
local: { ip: '', mac: '' },
|
|
599
569
|
presets: { sleep: true, health: false, eco: false, clean: false },
|
|
600
570
|
showFan: true, showComfWind: false, showDisplay: false,
|
|
601
|
-
tempStep: 0.5, pollInterval: 30,
|
|
602
571
|
});
|
|
603
572
|
expanded.add(i);
|
|
604
573
|
render(); save();
|
|
@@ -630,13 +599,11 @@
|
|
|
630
599
|
});
|
|
631
600
|
});
|
|
632
601
|
|
|
633
|
-
/* Device-level fields (name
|
|
602
|
+
/* Device-level fields (name) */
|
|
634
603
|
document.querySelectorAll('.d-field').forEach(el => {
|
|
635
604
|
el.addEventListener('change', () => {
|
|
636
605
|
const i = +el.dataset.i, f = el.dataset.f;
|
|
637
|
-
config.devices[i][f] =
|
|
638
|
-
: (f === 'pollInterval') ? (parseInt(el.value) || 30)
|
|
639
|
-
: el.value;
|
|
606
|
+
config.devices[i][f] = el.value;
|
|
640
607
|
save();
|
|
641
608
|
});
|
|
642
609
|
el.addEventListener('input', () => {
|
package/package.json
CHANGED