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 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
 
@@ -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": true },
76
- "eco": { "title": "Eco / Mildew Prevention", "type": "boolean", "default": true },
77
- "clean": { "title": "Self Clean", "type": "boolean", "default": true }
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: #7c3aed;
7
- --hybrid-light: rgba(124,58,237,0.08);
8
- --hybrid-border: rgba(124,58,237,0.22);
9
- --local: #16a34a;
10
- --local-light: rgba(22,163,74,0.07);
11
- --local-border: rgba(22,163,74,0.22);
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: #a78bfa;
26
- --hybrid-light: rgba(167,139,250,0.08);
27
- --hybrid-border: rgba(167,139,250,0.22);
28
- --local: #4ade80;
29
- --local-light: rgba(74,222,128,0.08);
30
- --local-border: rgba(74,222,128,0.22);
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(--accent-light); color: var(--accent); border: 1px solid var(--accent-border); }
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
- /* ── i18n ─────────────────────────────────────────────────── */
219
- let t = {};
220
- async function loadTranslations() {
221
- let lang = 'en';
222
- try { lang = await homebridge.i18nCurrentLang(); } catch {}
223
- for (const l of [lang, lang.split('-')[0], 'en']) {
224
- try {
225
- const r = await fetch(`./i18n/${l}.json`);
226
- if (r.ok) { t = await r.json(); return; }
227
- } catch {}
228
- }
229
- }
230
- await loadTranslations();
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 #${i + 1}`;
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 cloud">${t.local||'Local'}</span>
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, tempStep, pollInterval) */
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] = (f === 'tempStep') ? (parseFloat(el.value) || 0.5)
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "homebridge-ac-freedom",
3
- "version": "2.3.3",
3
+ "version": "2.3.5",
4
4
  "displayName": "Homebridge AC Freedom",
5
5
  "description": "Homebridge plugin for AUX air conditioners via Broadlink (local UDP) and AUX Cloud API",
6
6
  "main": "index.js",