homebridge-ac-freedom 2.3.0 → 2.3.1
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 +16 -0
- package/homebridge-ui/public/index.html +302 -257
- package/homebridge-ui/server.js +18 -5
- package/package.json +4 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,21 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 2.3.1
|
|
4
|
+
|
|
5
|
+
- **Config UI overhaul** — complete redesign of the custom Homebridge UI
|
|
6
|
+
- Cloud credentials (Email, Password, Region, Fetch) all on one row
|
|
7
|
+
- **Cloud Devices** section appears only after a successful Fetch
|
|
8
|
+
- **Local Devices** section with full-width Add Device button
|
|
9
|
+
- Device cards: Name + Connection, IP + MAC, Temperature Step + Poll Interval, and Features all wrapped in consistent fieldset style
|
|
10
|
+
- Connection toggle: **Cloud** / **Hybrid** (Hybrid reveals IP + MAC fields)
|
|
11
|
+
- Features row: Fan · Sleep · Display · Health · Clean · Eco · Comf. Wind (all 7 in one line, no icons)
|
|
12
|
+
- Poll Interval changed from free input to fixed dropdown: 5 / 10 / 20 / 30 / 60 s
|
|
13
|
+
- New device defaults: only **Fan** and **Sleep** enabled out of the box
|
|
14
|
+
- Section headers centered, no underline borders
|
|
15
|
+
- Badge (Cloud / Hybrid / Local) moved to the right of the card header next to Remove
|
|
16
|
+
|
|
17
|
+
> **Upgrading from v2.2?** HomeKit accessories and automations are preserved (UUID is name-based). However, the new UI uses platform-level cloud credentials — open the plugin settings, enter your email/password and press **Fetch** to rediscover your devices.
|
|
18
|
+
|
|
3
19
|
## 2.3.0
|
|
4
20
|
|
|
5
21
|
- **New architecture: Hybrid + Local Only connection modes**
|
|
@@ -16,7 +16,6 @@
|
|
|
16
16
|
--input-bg: #ffffff;
|
|
17
17
|
--bg: #ffffff;
|
|
18
18
|
--danger: #dc2626;
|
|
19
|
-
--danger-hover: #b91c1c;
|
|
20
19
|
}
|
|
21
20
|
@media (prefers-color-scheme: dark) {
|
|
22
21
|
:root {
|
|
@@ -36,7 +35,6 @@
|
|
|
36
35
|
--input-bg: rgba(255,255,255,0.08);
|
|
37
36
|
--bg: transparent;
|
|
38
37
|
--danger: #f87171;
|
|
39
|
-
--danger-hover: #ef4444;
|
|
40
38
|
}
|
|
41
39
|
}
|
|
42
40
|
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
|
@@ -46,9 +44,8 @@
|
|
|
46
44
|
}
|
|
47
45
|
|
|
48
46
|
/* ── Form elements ── */
|
|
49
|
-
.fg { margin-bottom:
|
|
47
|
+
.fg { margin-bottom: 12px; }
|
|
50
48
|
label { display: block; font-weight: 600; font-size: 13px; margin-bottom: 4px; color: var(--text); }
|
|
51
|
-
.hint { font-size: 11px; color: var(--text-muted); margin-bottom: 5px; line-height: 1.4; }
|
|
52
49
|
input[type="text"], input[type="number"], input[type="email"], input[type="password"], select {
|
|
53
50
|
width: 100%; height: 36px; padding: 0 10px; border: 1px solid var(--border);
|
|
54
51
|
border-radius: 6px; font-size: 13px; background: var(--input-bg);
|
|
@@ -63,13 +60,13 @@
|
|
|
63
60
|
font-size: 12px; font-weight: 600; transition: opacity .15s, background .15s;
|
|
64
61
|
}
|
|
65
62
|
.btn:hover { opacity: .85; }
|
|
63
|
+
.btn:disabled { opacity: .5; cursor: not-allowed; }
|
|
66
64
|
.btn-primary { background: var(--accent); color: #fff; }
|
|
67
|
-
.btn-
|
|
68
|
-
.btn-
|
|
69
|
-
.btn-
|
|
70
|
-
.btn-
|
|
65
|
+
.btn-danger { background: transparent; color: var(--danger); border: 1px solid var(--danger); }
|
|
66
|
+
.btn-danger:hover { background: var(--danger); color: #fff; opacity: 1; }
|
|
67
|
+
.btn-local { background: transparent; color: var(--local); border: 1px solid var(--local); }
|
|
68
|
+
.btn-local:hover { background: var(--local); color: #fff; opacity: 1; }
|
|
71
69
|
.btn-sm { padding: 4px 10px; font-size: 11px; }
|
|
72
|
-
.btn:disabled { opacity: .5; cursor: not-allowed; }
|
|
73
70
|
|
|
74
71
|
/* ── Cloud credentials card ── */
|
|
75
72
|
.creds-card {
|
|
@@ -81,141 +78,142 @@
|
|
|
81
78
|
font-size: 13px; font-weight: 700; color: var(--accent);
|
|
82
79
|
border-bottom: 1px solid var(--accent-border);
|
|
83
80
|
}
|
|
84
|
-
.creds-body { padding:
|
|
81
|
+
.creds-body { padding: 14px 18px 16px; }
|
|
82
|
+
.fetch-row { display: flex; align-items: center; gap: 10px; margin-top: 12px; flex-wrap: wrap; }
|
|
83
|
+
.fetch-status { font-size: 12px; font-weight: 500; color: var(--text-muted); }
|
|
84
|
+
.fetch-status.ok { color: var(--local); }
|
|
85
|
+
.fetch-status.err { color: var(--danger); }
|
|
85
86
|
|
|
86
|
-
/* ──
|
|
87
|
-
.
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
font-size: 12px; font-weight: 500; color: var(--text-muted);
|
|
87
|
+
/* ── Section header ── */
|
|
88
|
+
.section-hd {
|
|
89
|
+
font-size: 12px; font-weight: 700; text-transform: uppercase;
|
|
90
|
+
letter-spacing: .8px; color: var(--text-muted);
|
|
91
|
+
margin: 16px 0 10px; padding-bottom: 6px;
|
|
92
|
+
border-bottom: 1px solid var(--border);
|
|
93
93
|
}
|
|
94
|
-
.
|
|
95
|
-
|
|
94
|
+
.section-hd.local-hd { color: var(--local); border-bottom-color: var(--local-border); }
|
|
95
|
+
|
|
96
|
+
/* ── Grid layouts ── */
|
|
97
|
+
.row-2 { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; }
|
|
98
|
+
.row-3 { display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 12px; }
|
|
96
99
|
|
|
97
100
|
/* ── Device card ── */
|
|
98
101
|
.device-card {
|
|
99
102
|
border: 1.5px solid var(--accent-border); border-radius: 12px;
|
|
100
|
-
margin-bottom:
|
|
103
|
+
margin-bottom: 12px; background: var(--accent-light); overflow: hidden;
|
|
101
104
|
}
|
|
102
105
|
.device-card.hybrid-card { border-color: var(--hybrid-border); background: var(--hybrid-light); }
|
|
103
|
-
.device-card.local-card { border-color: var(--local-border); background: var(--local-light);
|
|
106
|
+
.device-card.local-card { border-color: var(--local-border); background: var(--local-light); }
|
|
104
107
|
|
|
105
108
|
.device-header {
|
|
106
109
|
display: flex; justify-content: space-between; align-items: center;
|
|
107
|
-
padding:
|
|
110
|
+
padding: 11px 16px; cursor: pointer; user-select: none;
|
|
108
111
|
}
|
|
109
112
|
.device-header:hover { opacity: .88; }
|
|
110
|
-
.device-header-left { display: flex; align-items: center; gap:
|
|
113
|
+
.device-header-left { display: flex; align-items: center; gap: 8px; flex: 1; min-width: 0; }
|
|
111
114
|
.chevron { font-size: 11px; color: var(--text-muted); transition: transform .2s; flex-shrink: 0; }
|
|
112
115
|
.chevron.open { transform: rotate(90deg); }
|
|
113
|
-
.dev-name { font-size:
|
|
116
|
+
.dev-name { font-size: 14px; font-weight: 700; color: var(--accent);
|
|
117
|
+
white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
114
118
|
.device-card.hybrid-card .dev-name { color: var(--hybrid); }
|
|
115
119
|
.device-card.local-card .dev-name { color: var(--local); }
|
|
116
120
|
|
|
117
121
|
.conn-badge {
|
|
118
122
|
font-size: 10px; font-weight: 700; letter-spacing: .5px;
|
|
119
|
-
padding: 2px
|
|
123
|
+
padding: 2px 7px; border-radius: 20px; flex-shrink: 0; text-transform: uppercase;
|
|
120
124
|
}
|
|
125
|
+
.conn-badge.cloud { background: var(--accent-light); color: var(--accent); border: 1px solid var(--accent-border); }
|
|
121
126
|
.conn-badge.hybrid { background: var(--hybrid-light); color: var(--hybrid); border: 1px solid var(--hybrid-border); }
|
|
122
|
-
.conn-badge.local { background: var(--local-light); color: var(--local); border: 1px solid var(--local-border);
|
|
127
|
+
.conn-badge.local { background: var(--local-light); color: var(--local); border: 1px solid var(--local-border); }
|
|
123
128
|
.dev-meta { font-size: 11px; color: var(--text-muted); white-space: nowrap; }
|
|
124
129
|
|
|
125
|
-
.device-body {
|
|
130
|
+
.device-body {
|
|
131
|
+
padding: 14px 16px 18px;
|
|
132
|
+
border-top: 1px solid var(--accent-border);
|
|
133
|
+
}
|
|
126
134
|
.device-card.hybrid-card .device-body { border-color: var(--hybrid-border); }
|
|
127
|
-
.device-card.local-card .device-body { border-color: var(--local-border);
|
|
135
|
+
.device-card.local-card .device-body { border-color: var(--local-border); }
|
|
128
136
|
.device-body.collapsed { display: none; }
|
|
129
137
|
|
|
130
|
-
/* ── Grid layouts ── */
|
|
131
|
-
.row-2 { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; }
|
|
132
|
-
.row-3 { display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 12px; }
|
|
133
|
-
|
|
134
|
-
/* ── Section divider ── */
|
|
135
|
-
.section-hd {
|
|
136
|
-
font-size: 12px; font-weight: 700; text-transform: uppercase;
|
|
137
|
-
letter-spacing: .8px; color: var(--text-muted);
|
|
138
|
-
margin: 18px 0 10px; padding-bottom: 6px;
|
|
139
|
-
border-bottom: 1px solid var(--border);
|
|
140
|
-
}
|
|
141
|
-
|
|
142
138
|
/* ── Connection toggle (segmented) ── */
|
|
143
139
|
.conn-toggle {
|
|
144
140
|
display: flex; border: 1px solid var(--border); border-radius: 8px;
|
|
145
141
|
overflow: hidden; height: 36px;
|
|
146
142
|
}
|
|
147
143
|
.conn-btn {
|
|
148
|
-
flex: 1; padding: 0 12px; border: none;
|
|
149
|
-
background: transparent; cursor: pointer;
|
|
144
|
+
flex: 1; padding: 0 12px; border: none; background: transparent; cursor: pointer;
|
|
150
145
|
font-size: 13px; font-weight: 600; color: var(--text-muted);
|
|
151
|
-
display: flex; align-items: center; justify-content: center; gap:
|
|
146
|
+
display: flex; align-items: center; justify-content: center; gap: 5px;
|
|
152
147
|
transition: background .15s, color .15s;
|
|
153
148
|
}
|
|
154
149
|
.conn-btn + .conn-btn { border-left: 1px solid var(--border); }
|
|
155
|
-
.conn-btn
|
|
150
|
+
.conn-btn.active-cloud { background: var(--accent-light); color: var(--accent); }
|
|
156
151
|
.conn-btn.active-hybrid { background: var(--hybrid-light); color: var(--hybrid); }
|
|
157
|
-
.conn-btn.active-local { background: var(--local-light); color: var(--local); }
|
|
158
|
-
|
|
159
|
-
/* ── Fieldset ── */
|
|
160
|
-
.fieldset {
|
|
161
|
-
border: 1px solid var(--border); border-radius: 8px;
|
|
162
|
-
padding: 12px 14px; margin-top: 4px;
|
|
163
|
-
}
|
|
164
152
|
|
|
165
|
-
/* ──
|
|
166
|
-
.toggle-
|
|
167
|
-
.toggle-row { display: flex; gap: 8px; }
|
|
153
|
+
/* ── Features toggle row ── */
|
|
154
|
+
.toggle-row { display: flex; gap: 6px; }
|
|
168
155
|
.toggle-item {
|
|
169
|
-
flex: 1;
|
|
170
|
-
display: flex; align-items: center; justify-content: center; gap:
|
|
171
|
-
padding:
|
|
156
|
+
flex: 1; min-width: 0;
|
|
157
|
+
display: flex; align-items: center; justify-content: center; gap: 5px;
|
|
158
|
+
padding: 7px 4px; border-radius: 7px;
|
|
172
159
|
border: 1px solid var(--border); background: var(--card-bg);
|
|
173
|
-
cursor: pointer; font-size:
|
|
174
|
-
transition: border-color .15s, background .15s;
|
|
160
|
+
cursor: pointer; font-size: 11px; font-weight: 500; color: var(--text);
|
|
161
|
+
white-space: nowrap; transition: border-color .15s, background .15s;
|
|
175
162
|
}
|
|
176
163
|
.toggle-item:hover { border-color: var(--accent); }
|
|
177
164
|
.toggle-item input[type="checkbox"] {
|
|
178
|
-
width:
|
|
165
|
+
width: 13px; height: 13px; accent-color: var(--accent); flex-shrink: 0; cursor: pointer;
|
|
179
166
|
}
|
|
180
167
|
.toggle-item.checked { border-color: var(--accent-border); background: var(--accent-light); }
|
|
181
168
|
|
|
182
|
-
/* ──
|
|
183
|
-
.
|
|
169
|
+
/* ── Fieldset ── */
|
|
170
|
+
.fieldset {
|
|
171
|
+
border: 1px solid var(--border); border-radius: 8px;
|
|
172
|
+
padding: 12px 12px 10px;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/* ── Actions ── */
|
|
176
|
+
.actions { margin-top: 14px; }
|
|
184
177
|
</style>
|
|
185
178
|
|
|
186
179
|
<div id="app"></div>
|
|
187
180
|
|
|
188
181
|
<script>
|
|
189
182
|
(async () => {
|
|
190
|
-
/* ──
|
|
183
|
+
/* ── Error safety net ─────────────────────────────────────── */
|
|
184
|
+
window.addEventListener('error', ev => {
|
|
185
|
+
const app = document.getElementById('app');
|
|
186
|
+
if (app && !app.innerHTML.trim()) {
|
|
187
|
+
app.innerHTML = `<div style="color:#dc2626;padding:16px;font-size:12px">
|
|
188
|
+
UI error: ${ev.message}<br>Check browser console.
|
|
189
|
+
</div>`;
|
|
190
|
+
}
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
/* ── i18n ─────────────────────────────────────────────────── */
|
|
191
194
|
let t = {};
|
|
192
195
|
async function loadTranslations() {
|
|
193
196
|
let lang = 'en';
|
|
194
197
|
try { lang = await homebridge.i18nCurrentLang(); } catch {}
|
|
195
|
-
const
|
|
196
|
-
for (const l of tryLangs) {
|
|
198
|
+
for (const l of [lang, lang.split('-')[0], 'en']) {
|
|
197
199
|
try {
|
|
198
|
-
const
|
|
199
|
-
if (
|
|
200
|
+
const r = await fetch(`./i18n/${l}.json`);
|
|
201
|
+
if (r.ok) { t = await r.json(); return; }
|
|
200
202
|
} catch {}
|
|
201
203
|
}
|
|
202
|
-
try { const res = await fetch('./i18n/en.json'); t = await res.json(); } catch {}
|
|
203
204
|
}
|
|
204
205
|
await loadTranslations();
|
|
205
206
|
|
|
206
|
-
/* ── Config
|
|
207
|
+
/* ── Config ───────────────────────────────────────────────── */
|
|
207
208
|
let config = {};
|
|
208
|
-
try {
|
|
209
|
-
const cfgs = await homebridge.getPluginConfig();
|
|
210
|
-
config = cfgs[0] || {};
|
|
211
|
-
} catch {}
|
|
209
|
+
try { const c = await homebridge.getPluginConfig(); config = c[0] || {}; } catch {}
|
|
212
210
|
if (!config.devices) config.devices = [];
|
|
213
211
|
if (!config.cloud) config.cloud = {};
|
|
214
212
|
|
|
215
213
|
const expanded = new Set();
|
|
216
214
|
const app = document.getElementById('app');
|
|
217
215
|
|
|
218
|
-
/* ── Helpers
|
|
216
|
+
/* ── Helpers ──────────────────────────────────────────────── */
|
|
219
217
|
function esc(s) {
|
|
220
218
|
const d = document.createElement('div');
|
|
221
219
|
d.textContent = String(s ?? '');
|
|
@@ -224,76 +222,69 @@
|
|
|
224
222
|
function bool(v, def = true) { return v === undefined ? def : !!v; }
|
|
225
223
|
async function save() { await homebridge.updatePluginConfig([config]); }
|
|
226
224
|
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
225
|
+
function featToggles(d, i) {
|
|
226
|
+
const presets = d.presets || {};
|
|
227
|
+
const feats = [
|
|
228
|
+
{ key: 'showFan', label: t.fan||'Fan', val: bool(d.showFan) },
|
|
229
|
+
{ key: 'sleep', label: t.sleep||'Sleep', val: bool(presets.sleep), preset: true },
|
|
230
|
+
{ key: 'showDisplay', label: t.display||'Display', val: bool(d.showDisplay) },
|
|
231
|
+
{ key: 'health', label: t.health||'Health', val: bool(presets.health), preset: true },
|
|
232
|
+
{ key: 'clean', label: t.clean||'Clean', val: bool(presets.clean), preset: true },
|
|
233
|
+
{ key: 'eco', label: t.eco||'Eco', val: bool(presets.eco), preset: true },
|
|
234
|
+
{ key: 'showComfWind', label: t.comfWind||'Comf. Wind', val: bool(d.showComfWind) },
|
|
235
|
+
];
|
|
236
|
+
return `
|
|
237
|
+
<div class="toggle-row">
|
|
238
|
+
${feats.map(f => `
|
|
239
|
+
<label class="toggle-item ${f.val ? 'checked' : ''}">
|
|
240
|
+
<input type="checkbox" class="feat-cb"
|
|
241
|
+
data-i="${i}" data-k="${f.key}" data-preset="${f.preset||false}"
|
|
242
|
+
${f.val ? 'checked' : ''}>
|
|
243
|
+
${f.label}
|
|
244
|
+
</label>
|
|
245
|
+
`).join('')}
|
|
246
|
+
</div>
|
|
247
|
+
`;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function settingsRow(d, i) {
|
|
251
|
+
const step = d.tempStep !== undefined ? d.tempStep : 0.5;
|
|
252
|
+
const poll = d.pollInterval || 30;
|
|
253
|
+
return `
|
|
254
|
+
<div class="fieldset" style="margin-bottom:12px">
|
|
255
|
+
<div class="row-2">
|
|
256
|
+
<div class="fg" style="margin-bottom:0">
|
|
257
|
+
<label>${t.tempStep||'Temperature Step'}</label>
|
|
258
|
+
<select class="d-field" data-i="${i}" data-f="tempStep">
|
|
259
|
+
<option value="0.5" ${step==0.5?'selected':''}>0.5 °C</option>
|
|
260
|
+
<option value="1" ${step==1 ?'selected':''}>1.0 °C</option>
|
|
261
|
+
</select>
|
|
254
262
|
</div>
|
|
255
|
-
<div class="
|
|
256
|
-
<
|
|
257
|
-
<
|
|
263
|
+
<div class="fg" style="margin-bottom:0">
|
|
264
|
+
<label>${t.pollInterval||'Poll Interval'}</label>
|
|
265
|
+
<select class="d-field" data-i="${i}" data-f="pollInterval">
|
|
266
|
+
<option value="5" ${poll==5 ?'selected':''}>5 s</option>
|
|
267
|
+
<option value="10" ${poll==10?'selected':''}>10 s</option>
|
|
268
|
+
<option value="20" ${poll==20?'selected':''}>20 s</option>
|
|
269
|
+
<option value="30" ${poll==30?'selected':''}>30 s</option>
|
|
270
|
+
<option value="60" ${poll==60?'selected':''}>60 s</option>
|
|
271
|
+
</select>
|
|
258
272
|
</div>
|
|
259
273
|
</div>
|
|
260
|
-
</div
|
|
261
|
-
|
|
262
|
-
<!-- Devices -->
|
|
263
|
-
<div class="section-hd" style="margin-top:4px">${t.devices||'Devices'}</div>
|
|
264
|
-
<div id="device-list">
|
|
265
|
-
${config.devices.map((d, i) => renderDevice(d, i)).join('')}
|
|
266
|
-
</div>
|
|
267
|
-
<div class="actions">
|
|
268
|
-
<button class="btn btn-outline" id="btn-add">+ ${t.addDevice||'Add Device'}</button>
|
|
269
|
-
</div>
|
|
270
|
-
`;
|
|
271
|
-
bindEvents();
|
|
274
|
+
</div>`;
|
|
272
275
|
}
|
|
273
276
|
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
const
|
|
277
|
-
const
|
|
278
|
-
const
|
|
279
|
-
const
|
|
280
|
-
const
|
|
281
|
-
const
|
|
282
|
-
const
|
|
283
|
-
const cardClass= isHybrid ? 'hybrid-card' : '
|
|
284
|
-
const
|
|
285
|
-
|
|
286
|
-
const featRow1 = [
|
|
287
|
-
{ key: 'showFan', label: `🌀 ${t.fan||'Fan'}`, val: bool(d.showFan) },
|
|
288
|
-
{ key: 'sleep', label: `😴 ${t.sleep||'Sleep'}`, val: bool(presets.sleep), preset: true },
|
|
289
|
-
{ key: 'showDisplay', label: `💡 ${t.display||'Display'}`, val: bool(d.showDisplay) },
|
|
290
|
-
];
|
|
291
|
-
const featRow2 = [
|
|
292
|
-
{ key: 'health', label: `🌿 ${t.health||'Health'}`, val: bool(presets.health), preset: true },
|
|
293
|
-
{ key: 'clean', label: `✨ ${t.clean||'Clean'}`, val: bool(presets.clean), preset: true },
|
|
294
|
-
{ key: 'eco', label: `🍃 ${t.eco||'Eco'}`, val: bool(presets.eco), preset: true },
|
|
295
|
-
{ key: 'showComfWind',label: `💨 ${t.comfWind||'Comf. Wind'}`, val: bool(d.showComfWind) },
|
|
296
|
-
];
|
|
277
|
+
/* ── Render cloud device card ─────────────────────────────── */
|
|
278
|
+
function renderCloudDevice(d, i) {
|
|
279
|
+
const isOpen = expanded.has(i);
|
|
280
|
+
const name = d.name || `Device #${i + 1}`;
|
|
281
|
+
const conn = (d.connection === 'local' || !d.connection) ? 'cloud' : d.connection;
|
|
282
|
+
const isHybrid = conn === 'hybrid';
|
|
283
|
+
const local = d.local || {};
|
|
284
|
+
const step = d.tempStep !== undefined ? d.tempStep : 0.5;
|
|
285
|
+
const poll = d.pollInterval || 30;
|
|
286
|
+
const cardClass = isHybrid ? 'hybrid-card' : '';
|
|
287
|
+
const badge = isHybrid ? (t.hybrid||'Hybrid') : (t.cloud||'Cloud');
|
|
297
288
|
|
|
298
289
|
return `
|
|
299
290
|
<div class="device-card ${cardClass}" id="dev-card-${i}">
|
|
@@ -301,126 +292,184 @@
|
|
|
301
292
|
<div class="device-header-left">
|
|
302
293
|
<span class="chevron ${isOpen ? 'open' : ''}">▶</span>
|
|
303
294
|
<span class="dev-name">${esc(name)}</span>
|
|
304
|
-
<span class="conn-badge ${conn}">${badgeLabel}</span>
|
|
305
|
-
${!isOpen ? `<span class="dev-meta">${poll}s · ${step}°C</span>` : ''}
|
|
306
295
|
</div>
|
|
307
|
-
<div style="flex-shrink:0;
|
|
296
|
+
<div style="display:flex;align-items:center;gap:8px;flex-shrink:0;margin-left:8px">
|
|
297
|
+
<span class="conn-badge ${conn}">${badge}</span>
|
|
308
298
|
<button class="btn btn-danger btn-sm btn-remove" data-i="${i}">${t.remove||'Remove'}</button>
|
|
309
299
|
</div>
|
|
310
300
|
</div>
|
|
311
301
|
|
|
312
302
|
<div class="device-body ${isOpen ? '' : 'collapsed'}">
|
|
313
|
-
|
|
314
303
|
<!-- Name + Connection -->
|
|
315
|
-
<div class="
|
|
316
|
-
<div class="
|
|
317
|
-
<
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
<div class="
|
|
323
|
-
<
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
304
|
+
<div class="fieldset" style="margin-bottom:12px">
|
|
305
|
+
<div class="row-2">
|
|
306
|
+
<div class="fg" style="margin-bottom:0">
|
|
307
|
+
<label>${t.deviceName||'Device Name'}</label>
|
|
308
|
+
<input type="text" class="d-field" data-i="${i}" data-f="name"
|
|
309
|
+
value="${esc(d.name||'')}">
|
|
310
|
+
</div>
|
|
311
|
+
<div class="fg" style="margin-bottom:0">
|
|
312
|
+
<label>${t.connection||'Connection'}</label>
|
|
313
|
+
<div class="conn-toggle">
|
|
314
|
+
<button class="conn-btn ${!isHybrid ? 'active-cloud' : ''} conn-pick"
|
|
315
|
+
data-i="${i}" data-v="cloud">
|
|
316
|
+
☁️ ${t.cloud||'Cloud'}
|
|
317
|
+
</button>
|
|
318
|
+
<button class="conn-btn ${isHybrid ? 'active-hybrid' : ''} conn-pick"
|
|
319
|
+
data-i="${i}" data-v="hybrid">
|
|
320
|
+
🔀 ${t.hybrid||'Hybrid'}
|
|
321
|
+
</button>
|
|
322
|
+
</div>
|
|
329
323
|
</div>
|
|
330
324
|
</div>
|
|
331
325
|
</div>
|
|
332
326
|
|
|
333
|
-
<!--
|
|
327
|
+
<!-- IP + MAC (Hybrid only) -->
|
|
334
328
|
<div id="hybrid-sec-${i}" style="${isHybrid ? '' : 'display:none'}">
|
|
335
|
-
<div class="
|
|
336
|
-
<div class="fieldset">
|
|
337
|
-
<div class="fg">
|
|
338
|
-
<label>${t.endpointId||'Device ID'} <span style="font-weight:400;color:var(--text-muted)">(${t.deviceIdOptional||'optional'})</span></label>
|
|
339
|
-
<div class="hint">${t.endpointIdHint||'Auto-filled via Fetch Devices. Leave empty to use the first discovered device.'}</div>
|
|
340
|
-
<input type="text" class="h-field" data-i="${i}" data-f="endpointId" value="${esc(d.endpointId||'')}" placeholder="auto-detect">
|
|
341
|
-
</div>
|
|
342
|
-
<div class="section-hd" style="margin-top:4px">📡 ${t.localOverride||'Local Override (optional)'}</div>
|
|
343
|
-
<div class="hint" style="margin-bottom:10px">${t.localOverrideHint||'Add IP + MAC for direct local connection. Local is preferred over cloud when available.'}</div>
|
|
329
|
+
<div class="fieldset" style="margin-bottom:12px">
|
|
344
330
|
<div class="row-2">
|
|
345
331
|
<div class="fg" style="margin-bottom:0">
|
|
346
332
|
<label>${t.ip||'IP Address'}</label>
|
|
347
|
-
<
|
|
348
|
-
|
|
333
|
+
<input type="text" class="l-field" data-i="${i}" data-f="ip"
|
|
334
|
+
value="${esc(local.ip||'')}" placeholder="192.168.1.100">
|
|
349
335
|
</div>
|
|
350
336
|
<div class="fg" style="margin-bottom:0">
|
|
351
337
|
<label>${t.mac||'MAC Address'}</label>
|
|
352
|
-
<
|
|
353
|
-
|
|
338
|
+
<input type="text" class="l-field" data-i="${i}" data-f="mac"
|
|
339
|
+
value="${esc(local.mac||'')}" placeholder="AA:BB:CC:DD:EE:FF">
|
|
354
340
|
</div>
|
|
355
341
|
</div>
|
|
356
342
|
</div>
|
|
357
343
|
</div>
|
|
358
344
|
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
345
|
+
${settingsRow(d, i)}
|
|
346
|
+
${featToggles(d, i)}
|
|
347
|
+
</div>
|
|
348
|
+
</div>`;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
/* ── Render local-only device card ───────────────────────── */
|
|
352
|
+
function renderLocalDevice(d, i) {
|
|
353
|
+
const isOpen = expanded.has(i);
|
|
354
|
+
const name = d.name || `Local #${i + 1}`;
|
|
355
|
+
const local = d.local || {};
|
|
356
|
+
const step = d.tempStep !== undefined ? d.tempStep : 0.5;
|
|
357
|
+
const poll = d.pollInterval || 30;
|
|
358
|
+
|
|
359
|
+
return `
|
|
360
|
+
<div class="device-card" id="dev-card-${i}">
|
|
361
|
+
<div class="device-header device-toggle" data-i="${i}">
|
|
362
|
+
<div class="device-header-left">
|
|
363
|
+
<span class="chevron ${isOpen ? 'open' : ''}">▶</span>
|
|
364
|
+
<span class="dev-name">${esc(name)}</span>
|
|
365
|
+
</div>
|
|
366
|
+
<div style="display:flex;align-items:center;gap:8px;flex-shrink:0;margin-left:8px">
|
|
367
|
+
<span class="conn-badge cloud">${t.local||'Local'}</span>
|
|
368
|
+
<button class="btn btn-danger btn-sm btn-remove" data-i="${i}">${t.remove||'Remove'}</button>
|
|
376
369
|
</div>
|
|
370
|
+
</div>
|
|
377
371
|
|
|
378
|
-
|
|
379
|
-
<div class="
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
<label class="toggle-item ${f.val ? 'checked' : ''}">
|
|
385
|
-
<input type="checkbox" class="feat-cb" data-i="${i}" data-k="${f.key}" data-preset="${f.preset||false}" ${f.val ? 'checked' : ''}>
|
|
386
|
-
${f.label}
|
|
387
|
-
</label>
|
|
388
|
-
`).join('')}
|
|
372
|
+
<div class="device-body ${isOpen ? '' : 'collapsed'}">
|
|
373
|
+
<div class="fieldset" style="margin-bottom:12px">
|
|
374
|
+
<div class="fg" style="margin-bottom:0">
|
|
375
|
+
<label>${t.deviceName||'Device Name'}</label>
|
|
376
|
+
<input type="text" class="d-field" data-i="${i}" data-f="name"
|
|
377
|
+
value="${esc(d.name||'')}">
|
|
389
378
|
</div>
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
379
|
+
</div>
|
|
380
|
+
<div class="fieldset" style="margin-bottom:12px">
|
|
381
|
+
<div class="row-2">
|
|
382
|
+
<div class="fg" style="margin-bottom:0">
|
|
383
|
+
<label>${t.ip||'IP Address'}</label>
|
|
384
|
+
<input type="text" class="l-field" data-i="${i}" data-f="ip"
|
|
385
|
+
value="${esc(local.ip||'')}" placeholder="192.168.1.100">
|
|
386
|
+
</div>
|
|
387
|
+
<div class="fg" style="margin-bottom:0">
|
|
388
|
+
<label>${t.mac||'MAC Address'}</label>
|
|
389
|
+
<input type="text" class="l-field" data-i="${i}" data-f="mac"
|
|
390
|
+
value="${esc(local.mac||'')}" placeholder="AA:BB:CC:DD:EE:FF">
|
|
391
|
+
</div>
|
|
397
392
|
</div>
|
|
398
393
|
</div>
|
|
399
394
|
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
395
|
+
${settingsRow(d, i)}
|
|
396
|
+
${featToggles(d, i)}
|
|
397
|
+
</div>
|
|
398
|
+
</div>`;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
/* ── Main render ──────────────────────────────────────────── */
|
|
402
|
+
function render() {
|
|
403
|
+
const cloudDevs = config.devices
|
|
404
|
+
.map((d, i) => ({ d, i }))
|
|
405
|
+
.filter(({ d }) => d.connection !== 'local');
|
|
406
|
+
const localDevs = config.devices
|
|
407
|
+
.map((d, i) => ({ d, i }))
|
|
408
|
+
.filter(({ d }) => d.connection === 'local');
|
|
409
|
+
|
|
410
|
+
app.innerHTML = `
|
|
411
|
+
<!-- Cloud Credentials -->
|
|
412
|
+
<div class="creds-card">
|
|
413
|
+
<div class="creds-header">☁️ ${t.cloudCredentials||'Cloud Credentials'}</div>
|
|
414
|
+
<div class="creds-body">
|
|
415
|
+
<div style="display:grid;grid-template-columns:1fr 1fr 140px 100px;gap:8px;align-items:flex-end">
|
|
403
416
|
<div class="fg" style="margin-bottom:0">
|
|
404
|
-
<label>${t.
|
|
405
|
-
<
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
417
|
+
<label>${t.email||'Email'}</label>
|
|
418
|
+
<input type="email" id="cloud-email"
|
|
419
|
+
value="${esc(config.cloud.email||'')}" placeholder="user@example.com">
|
|
420
|
+
</div>
|
|
421
|
+
<div class="fg" style="margin-bottom:0">
|
|
422
|
+
<label>${t.password||'Password'}</label>
|
|
423
|
+
<input type="password" id="cloud-password"
|
|
424
|
+
value="${esc(config.cloud.password||'')}">
|
|
425
|
+
</div>
|
|
426
|
+
<div class="fg" style="margin-bottom:0">
|
|
427
|
+
<label>${t.region||'Region'}</label>
|
|
428
|
+
<select id="cloud-region">
|
|
429
|
+
${['eu','usa','cn','rus'].map(r =>
|
|
430
|
+
`<option value="${r}" ${(config.cloud.region||'eu')===r?'selected':''}>
|
|
431
|
+
${{eu:'Europe',usa:'USA',cn:'China',rus:'Russia'}[r]}
|
|
432
|
+
</option>`
|
|
433
|
+
).join('')}
|
|
409
434
|
</select>
|
|
410
435
|
</div>
|
|
411
436
|
<div class="fg" style="margin-bottom:0">
|
|
412
|
-
<
|
|
413
|
-
|
|
414
|
-
|
|
437
|
+
<button class="btn btn-primary" id="btn-fetch" style="height:36px;width:100%;white-space:nowrap">
|
|
438
|
+
🔍 ${t.fetch||'Fetch'}
|
|
439
|
+
</button>
|
|
415
440
|
</div>
|
|
416
441
|
</div>
|
|
417
|
-
|
|
442
|
+
<span class="fetch-status" id="fetch-status" style="display:block;margin-top:6px"></span>
|
|
418
443
|
</div>
|
|
419
444
|
</div>
|
|
445
|
+
|
|
446
|
+
<!-- Cloud Devices -->
|
|
447
|
+
${cloudDevs.length ? `
|
|
448
|
+
<div class="section-hd" style="margin-top:8px;text-align:center;border-bottom:none;padding-bottom:0">
|
|
449
|
+
${t.cloudDevices||'Cloud Devices'}
|
|
450
|
+
</div>` : ''}
|
|
451
|
+
<div id="cloud-device-list">
|
|
452
|
+
${cloudDevs.map(({ d, i }) => renderCloudDevice(d, i)).join('')}
|
|
453
|
+
</div>
|
|
454
|
+
|
|
455
|
+
<!-- Local Devices -->
|
|
456
|
+
<div class="section-hd" style="margin-top:20px;text-align:center;border-bottom:none;padding-bottom:0">
|
|
457
|
+
${t.localDevices||'Local Devices'}
|
|
458
|
+
</div>
|
|
459
|
+
<div id="local-device-list">
|
|
460
|
+
${localDevs.map(({ d, i }) => renderLocalDevice(d, i)).join('')}
|
|
461
|
+
</div>
|
|
462
|
+
<div class="fieldset" style="margin-top:10px">
|
|
463
|
+
<button class="btn btn-primary" id="btn-add-local"
|
|
464
|
+
style="width:100%;display:flex;align-items:center;justify-content:center;gap:6px;height:36px">
|
|
465
|
+
+ ${t.addDevice||'Add Device'}
|
|
466
|
+
</button>
|
|
467
|
+
</div>
|
|
420
468
|
`;
|
|
469
|
+
bindEvents();
|
|
421
470
|
}
|
|
422
471
|
|
|
423
|
-
/* ── Event binding
|
|
472
|
+
/* ── Event binding ────────────────────────────────────────── */
|
|
424
473
|
function bindEvents() {
|
|
425
474
|
/* Collapse toggle */
|
|
426
475
|
document.querySelectorAll('.device-toggle').forEach(el => {
|
|
@@ -432,7 +481,7 @@
|
|
|
432
481
|
});
|
|
433
482
|
});
|
|
434
483
|
|
|
435
|
-
/* Cloud
|
|
484
|
+
/* Cloud credentials */
|
|
436
485
|
document.getElementById('cloud-email').addEventListener('change', e => {
|
|
437
486
|
config.cloud.email = e.target.value; save();
|
|
438
487
|
});
|
|
@@ -443,7 +492,7 @@
|
|
|
443
492
|
config.cloud.region = e.target.value; save();
|
|
444
493
|
});
|
|
445
494
|
|
|
446
|
-
/* Fetch Devices
|
|
495
|
+
/* Fetch Devices */
|
|
447
496
|
document.getElementById('btn-fetch').addEventListener('click', async () => {
|
|
448
497
|
const btn = document.getElementById('btn-fetch');
|
|
449
498
|
const status = document.getElementById('fetch-status');
|
|
@@ -453,7 +502,7 @@
|
|
|
453
502
|
|
|
454
503
|
if (!email || !password) {
|
|
455
504
|
status.className = 'fetch-status err';
|
|
456
|
-
status.textContent = '⚠️
|
|
505
|
+
status.textContent = '⚠️ Enter email and password first';
|
|
457
506
|
return;
|
|
458
507
|
}
|
|
459
508
|
|
|
@@ -463,28 +512,27 @@
|
|
|
463
512
|
status.textContent = '';
|
|
464
513
|
|
|
465
514
|
try {
|
|
466
|
-
const res
|
|
515
|
+
const res = await homebridge.request('/fetchDevices', { email, password, region });
|
|
467
516
|
const fetched = res.devices || [];
|
|
468
517
|
|
|
469
|
-
// Save credentials
|
|
470
518
|
config.cloud.email = email;
|
|
471
519
|
config.cloud.password = password;
|
|
472
520
|
config.cloud.region = region;
|
|
473
521
|
|
|
474
|
-
//
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
522
|
+
// Add new devices (matched by endpointId); preserve existing
|
|
523
|
+
const existingIds = new Set(
|
|
524
|
+
config.devices.map(d => d.endpointId).filter(Boolean)
|
|
525
|
+
);
|
|
478
526
|
let added = 0;
|
|
479
527
|
for (const fd of fetched) {
|
|
480
528
|
if (!existingIds.has(fd.endpointId)) {
|
|
481
529
|
config.devices.push({
|
|
482
|
-
name:
|
|
483
|
-
connection: '
|
|
530
|
+
name: fd.name,
|
|
531
|
+
connection: 'cloud',
|
|
484
532
|
endpointId: fd.endpointId,
|
|
485
|
-
local:
|
|
486
|
-
presets:
|
|
487
|
-
showFan: true, showComfWind:
|
|
533
|
+
local: { ip: '', mac: '' },
|
|
534
|
+
presets: { sleep: true, health: false, eco: false, clean: false },
|
|
535
|
+
showFan: true, showComfWind: false, showDisplay: false,
|
|
488
536
|
tempStep: 0.5, pollInterval: 30,
|
|
489
537
|
});
|
|
490
538
|
expanded.add(config.devices.length - 1);
|
|
@@ -493,8 +541,9 @@
|
|
|
493
541
|
}
|
|
494
542
|
|
|
495
543
|
status.className = 'fetch-status ok';
|
|
496
|
-
status.textContent =
|
|
497
|
-
|
|
544
|
+
status.textContent =
|
|
545
|
+
`✓ ${fetched.length} ${t.devicesFound||'devices found'}` +
|
|
546
|
+
(added ? ` (+${added})` : '');
|
|
498
547
|
save();
|
|
499
548
|
render();
|
|
500
549
|
} catch (err) {
|
|
@@ -502,19 +551,18 @@
|
|
|
502
551
|
status.textContent = `✗ ${t.fetchFailed||'Fetch failed'}: ${err.message || err}`;
|
|
503
552
|
} finally {
|
|
504
553
|
const b = document.getElementById('btn-fetch');
|
|
505
|
-
if (b) { b.disabled = false; b.textContent = `🔍 ${t.
|
|
554
|
+
if (b) { b.disabled = false; b.textContent = `🔍 ${t.fetch||'Fetch'}`; }
|
|
506
555
|
}
|
|
507
556
|
});
|
|
508
557
|
|
|
509
|
-
/* Add
|
|
510
|
-
document.getElementById('btn-add').addEventListener('click', () => {
|
|
558
|
+
/* Add Local Device */
|
|
559
|
+
document.getElementById('btn-add-local').addEventListener('click', () => {
|
|
511
560
|
const i = config.devices.length;
|
|
512
561
|
config.devices.push({
|
|
513
|
-
name: '', connection: '
|
|
514
|
-
endpointId: '',
|
|
562
|
+
name: '', connection: 'local',
|
|
515
563
|
local: { ip: '', mac: '' },
|
|
516
|
-
presets: { sleep: true, health:
|
|
517
|
-
showFan: true, showComfWind:
|
|
564
|
+
presets: { sleep: true, health: false, eco: false, clean: false },
|
|
565
|
+
showFan: true, showComfWind: false, showDisplay: false,
|
|
518
566
|
tempStep: 0.5, pollInterval: 30,
|
|
519
567
|
});
|
|
520
568
|
expanded.add(i);
|
|
@@ -528,13 +576,16 @@
|
|
|
528
576
|
const i = +btn.dataset.i;
|
|
529
577
|
config.devices.splice(i, 1);
|
|
530
578
|
const newExp = new Set();
|
|
531
|
-
expanded.forEach(v => {
|
|
579
|
+
expanded.forEach(v => {
|
|
580
|
+
if (v < i) newExp.add(v);
|
|
581
|
+
else if (v > i) newExp.add(v - 1);
|
|
582
|
+
});
|
|
532
583
|
expanded.clear(); newExp.forEach(v => expanded.add(v));
|
|
533
584
|
render(); save();
|
|
534
585
|
});
|
|
535
586
|
});
|
|
536
587
|
|
|
537
|
-
/* Connection toggle */
|
|
588
|
+
/* Connection toggle (cloud devices only: cloud | hybrid) */
|
|
538
589
|
document.querySelectorAll('.conn-pick').forEach(btn => {
|
|
539
590
|
btn.addEventListener('click', e => {
|
|
540
591
|
e.stopPropagation();
|
|
@@ -544,11 +595,13 @@
|
|
|
544
595
|
});
|
|
545
596
|
});
|
|
546
597
|
|
|
547
|
-
/* Device-level
|
|
598
|
+
/* Device-level fields (name, tempStep, pollInterval) */
|
|
548
599
|
document.querySelectorAll('.d-field').forEach(el => {
|
|
549
600
|
el.addEventListener('change', () => {
|
|
550
601
|
const i = +el.dataset.i, f = el.dataset.f;
|
|
551
|
-
config.devices[i][f] =
|
|
602
|
+
config.devices[i][f] = (f === 'tempStep') ? (parseFloat(el.value) || 0.5)
|
|
603
|
+
: (f === 'pollInterval') ? (parseInt(el.value) || 30)
|
|
604
|
+
: el.value;
|
|
552
605
|
save();
|
|
553
606
|
});
|
|
554
607
|
el.addEventListener('input', () => {
|
|
@@ -559,16 +612,7 @@
|
|
|
559
612
|
});
|
|
560
613
|
});
|
|
561
614
|
|
|
562
|
-
/*
|
|
563
|
-
document.querySelectorAll('.h-field').forEach(el => {
|
|
564
|
-
el.addEventListener('change', () => {
|
|
565
|
-
const i = +el.dataset.i, f = el.dataset.f;
|
|
566
|
-
config.devices[i][f] = el.value;
|
|
567
|
-
save();
|
|
568
|
-
});
|
|
569
|
-
});
|
|
570
|
-
|
|
571
|
-
/* Local fields */
|
|
615
|
+
/* Local IP / MAC fields */
|
|
572
616
|
document.querySelectorAll('.l-field').forEach(el => {
|
|
573
617
|
el.addEventListener('change', () => {
|
|
574
618
|
const i = +el.dataset.i, f = el.dataset.f;
|
|
@@ -582,7 +626,8 @@
|
|
|
582
626
|
document.querySelectorAll('.feat-cb').forEach(cb => {
|
|
583
627
|
cb.closest('.toggle-item').classList.toggle('checked', cb.checked);
|
|
584
628
|
cb.addEventListener('change', () => {
|
|
585
|
-
const i = +cb.dataset.i, k = cb.dataset.k
|
|
629
|
+
const i = +cb.dataset.i, k = cb.dataset.k;
|
|
630
|
+
const isPreset = cb.dataset.preset === 'true';
|
|
586
631
|
if (isPreset) {
|
|
587
632
|
if (!config.devices[i].presets) config.devices[i].presets = {};
|
|
588
633
|
config.devices[i].presets[k] = cb.checked;
|
package/homebridge-ui/server.js
CHANGED
|
@@ -4,14 +4,25 @@
|
|
|
4
4
|
* Provides a /fetchDevices endpoint so the config UI can log in to the
|
|
5
5
|
* AUX Cloud API and return the list of discovered devices without
|
|
6
6
|
* restarting Homebridge.
|
|
7
|
+
*
|
|
8
|
+
* Uses @homebridge/plugin-ui-utils (required by homebridge-config-ui-x v5+).
|
|
7
9
|
*/
|
|
8
10
|
|
|
9
11
|
'use strict';
|
|
10
12
|
|
|
11
|
-
const {
|
|
13
|
+
const { HomebridgePluginUiServer } = require('@homebridge/plugin-ui-utils');
|
|
14
|
+
|
|
15
|
+
class AcFreedomUiServer extends HomebridgePluginUiServer {
|
|
16
|
+
constructor() {
|
|
17
|
+
super();
|
|
18
|
+
this.onRequest('/fetchDevices', this.fetchDevices.bind(this));
|
|
19
|
+
this.ready();
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
async fetchDevices(payload) {
|
|
23
|
+
// Lazy-require so a load-time error never blocks the UI
|
|
24
|
+
const { AuxCloudAPI } = require('../src/cloud-api');
|
|
12
25
|
|
|
13
|
-
module.exports = (homebridge) => {
|
|
14
|
-
homebridge.onRequest('/fetchDevices', async (payload) => {
|
|
15
26
|
const { email, password, region } = payload || {};
|
|
16
27
|
|
|
17
28
|
if (!email || !password) {
|
|
@@ -35,5 +46,7 @@ module.exports = (homebridge) => {
|
|
|
35
46
|
name: d.friendlyName || 'AUX AC',
|
|
36
47
|
})),
|
|
37
48
|
};
|
|
38
|
-
}
|
|
39
|
-
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
(() => { return new AcFreedomUiServer(); })();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "homebridge-ac-freedom",
|
|
3
|
-
"version": "2.3.
|
|
3
|
+
"version": "2.3.1",
|
|
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",
|
|
@@ -33,6 +33,9 @@
|
|
|
33
33
|
"type": "git",
|
|
34
34
|
"url": "https://github.com/azadaydinli/homebridge-ac-freedom.git"
|
|
35
35
|
},
|
|
36
|
+
"dependencies": {
|
|
37
|
+
"@homebridge/plugin-ui-utils": "^2.0.0"
|
|
38
|
+
},
|
|
36
39
|
"engines": {
|
|
37
40
|
"homebridge": ">=1.6.0 || >=2.0.0",
|
|
38
41
|
"node": ">=18.0.0"
|