@mp-consulting/homebridge-daikin-cloud 1.6.3 → 1.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +34 -0
- package/homebridge-ui/assistant.js +62 -0
- package/homebridge-ui/public/index.html +32 -0
- package/homebridge-ui/public/lib/ai.css +738 -0
- package/homebridge-ui/public/lib/kit.css +703 -1
- package/homebridge-ui/public/lib/kit.js +618 -1
- package/homebridge-ui/public/script.js +286 -6
- package/homebridge-ui/server.js +6 -1
- package/package.json +8 -3
|
@@ -114,6 +114,199 @@ const UI = {
|
|
|
114
114
|
},
|
|
115
115
|
};
|
|
116
116
|
|
|
117
|
+
// ============================================================================
|
|
118
|
+
// Assistant Module (Homebridge AI Kit)
|
|
119
|
+
// ============================================================================
|
|
120
|
+
// Shown only when the shared HomebridgeAiKit platform is set up and enabled.
|
|
121
|
+
// Nothing sent to it may contain credentials, tokens or IP addresses.
|
|
122
|
+
|
|
123
|
+
const Assistant = {
|
|
124
|
+
enabled: false,
|
|
125
|
+
available: false,
|
|
126
|
+
|
|
127
|
+
// Keys kept out of every request: credentials and network addresses
|
|
128
|
+
PRIVATE_KEYS: ['clientId', 'clientSecret', 'daikinEmail', 'daikinPassword', 'callbackServerExternalAddress', 'oidcCallbackServerBindAddr'],
|
|
129
|
+
|
|
130
|
+
async init() {
|
|
131
|
+
try {
|
|
132
|
+
if (window.MpKit && MpKit.ai) {
|
|
133
|
+
const status = await MpKit.ai.status();
|
|
134
|
+
this.available = true;
|
|
135
|
+
this.enabled = !!(status && status.enabled);
|
|
136
|
+
}
|
|
137
|
+
} catch {
|
|
138
|
+
// Routes missing (AI Kit not installed) or older Homebridge UI: no Assistant
|
|
139
|
+
}
|
|
140
|
+
DOM.toggle($id('assistant-hint'), this.available && !this.enabled);
|
|
141
|
+
if (this.enabled) {
|
|
142
|
+
this.setupConfigCard();
|
|
143
|
+
}
|
|
144
|
+
},
|
|
145
|
+
|
|
146
|
+
// Error text without e-mail addresses or LAN/public IPs (loopback and 0.0.0.0 stay: they explain bind problems)
|
|
147
|
+
scrub(text) {
|
|
148
|
+
return String(text ?? '')
|
|
149
|
+
.replace(/[^\s@]+@[^\s@]+\.[^\s@]+/g, '<email>')
|
|
150
|
+
.replace(/\b(?!127\.)(?!0\.0\.0\.0\b)(?:\d{1,3}\.){3}\d{1,3}\b/g, '<address>');
|
|
151
|
+
},
|
|
152
|
+
|
|
153
|
+
// Device facts the Assistant may see (whitelist)
|
|
154
|
+
device(d) {
|
|
155
|
+
return {
|
|
156
|
+
name: d.name,
|
|
157
|
+
id: d.id,
|
|
158
|
+
model: d.model,
|
|
159
|
+
type: d.type,
|
|
160
|
+
online: !!d.online,
|
|
161
|
+
features: Array.isArray(d.features) ? d.features : [],
|
|
162
|
+
};
|
|
163
|
+
},
|
|
164
|
+
|
|
165
|
+
// Plugin settings the Assistant may see, as one sentence (no credentials or addresses)
|
|
166
|
+
context(extra) {
|
|
167
|
+
const interval = $id('updateIntervalInMinutes')?.value;
|
|
168
|
+
return [
|
|
169
|
+
extra,
|
|
170
|
+
`Authentication method: ${AuthMode.current}.`,
|
|
171
|
+
interval ? `Update interval: ${interval} minutes.` : '',
|
|
172
|
+
AuthMode.current === 'mobile_app' ? `WebSocket: ${$id('enableWebSocket')?.checked ? 'on' : 'off'}.` : '',
|
|
173
|
+
`HTTP transport: ${$id('httpTransport')?.value || 'node'}.`,
|
|
174
|
+
].filter(Boolean).join(' ');
|
|
175
|
+
},
|
|
176
|
+
|
|
177
|
+
// Streams an explanation of `error` into `answerEl`
|
|
178
|
+
async explain(button, answerEl, { error, context, device, title }) {
|
|
179
|
+
button.disabled = true;
|
|
180
|
+
button.setAttribute('aria-busy', 'true');
|
|
181
|
+
DOM.show(answerEl);
|
|
182
|
+
const answer = MpKit.ai.renderAnswer(answerEl, { title });
|
|
183
|
+
try {
|
|
184
|
+
const res = await MpKit.ai.explain({ error: this.scrub(error), context: this.scrub(context), device }, { onChunk: answer.append });
|
|
185
|
+
answer.done(res);
|
|
186
|
+
} catch (e) {
|
|
187
|
+
answer.error(e);
|
|
188
|
+
} finally {
|
|
189
|
+
button.disabled = false;
|
|
190
|
+
button.removeAttribute('aria-busy');
|
|
191
|
+
}
|
|
192
|
+
},
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Adds an "Explain" button (and its answer panel) to the slot `slotId`, next to an
|
|
196
|
+
* error the page already shows. With `alert: true` the error itself is shown too
|
|
197
|
+
* (for errors that otherwise only appear in the auto-hiding global banner).
|
|
198
|
+
*/
|
|
199
|
+
offer(slotId, { message, context, title, alert = false }) {
|
|
200
|
+
const slot = $id(slotId);
|
|
201
|
+
if (!slot || !this.enabled) {
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
slot.innerHTML = `
|
|
205
|
+
<div class="d-flex justify-content-between align-items-start gap-2 ${alert ? 'alert alert-danger mb-0' : ''}">
|
|
206
|
+
<div>${alert ? Utils.escapeHtml(message) : ''}</div>
|
|
207
|
+
${MpKit.ai.renderButton({ label: 'Explain', size: 'sm', className: 'flex-shrink-0 js-explain', title: 'Explain this problem' })}
|
|
208
|
+
</div>
|
|
209
|
+
<div class="assistant-answer mt-2 d-none"></div>`;
|
|
210
|
+
DOM.show(slot);
|
|
211
|
+
const button = slot.querySelector('.js-explain');
|
|
212
|
+
button.addEventListener('click', () => this.explain(button, slot.querySelector('.assistant-answer'), {
|
|
213
|
+
error: message,
|
|
214
|
+
context: this.context(context),
|
|
215
|
+
title,
|
|
216
|
+
}));
|
|
217
|
+
},
|
|
218
|
+
|
|
219
|
+
clear(slotId) {
|
|
220
|
+
const slot = $id(slotId);
|
|
221
|
+
if (slot) {
|
|
222
|
+
DOM.hide(slot);
|
|
223
|
+
slot.innerHTML = '';
|
|
224
|
+
}
|
|
225
|
+
},
|
|
226
|
+
|
|
227
|
+
// ── Describe Your Setup ────────────────────────────────────────────────────
|
|
228
|
+
|
|
229
|
+
setupConfigCard() {
|
|
230
|
+
DOM.show($id('assistant-config-card'));
|
|
231
|
+
$id('assistant-config-badge').innerHTML = MpKit.ai.renderBadge();
|
|
232
|
+
$id('assistant-config-action').innerHTML = MpKit.ai.renderButton({ label: 'Suggest changes', id: 'btn-assistant-config' });
|
|
233
|
+
$id('btn-assistant-config').addEventListener('click', () => this.suggestConfig());
|
|
234
|
+
},
|
|
235
|
+
|
|
236
|
+
async suggestConfig() {
|
|
237
|
+
const request = $id('assistant-config-request').value.trim();
|
|
238
|
+
if (!request) {
|
|
239
|
+
UI.showError('Describe what you want to change first');
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
const button = $id('btn-assistant-config');
|
|
243
|
+
const result = $id('assistant-config-result');
|
|
244
|
+
button.disabled = true;
|
|
245
|
+
button.setAttribute('aria-busy', 'true');
|
|
246
|
+
result.innerHTML = MpKit.ai.renderThinking('Preparing a suggestion…');
|
|
247
|
+
try {
|
|
248
|
+
const config = (await homebridge.getPluginConfig())[0] || { platform: 'DaikinCloud', name: 'Daikin Cloud' };
|
|
249
|
+
// Credentials and addresses stay in the browser: strip them before and merge them back on apply
|
|
250
|
+
const shareable = { ...config };
|
|
251
|
+
this.PRIVATE_KEYS.forEach(k => delete shareable[k]);
|
|
252
|
+
const schema = await homebridge.getPluginConfigSchema();
|
|
253
|
+
const res = await MpKit.ai.config({ schema, request, current: shareable });
|
|
254
|
+
|
|
255
|
+
// Keep keys the schema does not describe (platform, _bridge, ...) and the current key order
|
|
256
|
+
const known = new Set(Object.keys((schema.schema || schema).properties || {}));
|
|
257
|
+
const proposed = res.config || {};
|
|
258
|
+
const suggested = {};
|
|
259
|
+
Object.keys(shareable).forEach(k => {
|
|
260
|
+
if (!known.has(k)) {
|
|
261
|
+
suggested[k] = shareable[k];
|
|
262
|
+
} else if (k in proposed) {
|
|
263
|
+
suggested[k] = proposed[k];
|
|
264
|
+
}
|
|
265
|
+
});
|
|
266
|
+
Object.keys(proposed).forEach(k => {
|
|
267
|
+
if (known.has(k) && !(k in suggested)) {
|
|
268
|
+
suggested[k] = proposed[k];
|
|
269
|
+
}
|
|
270
|
+
});
|
|
271
|
+
this.PRIVATE_KEYS.forEach(k => delete suggested[k]);
|
|
272
|
+
|
|
273
|
+
result.innerHTML = '<div class="assistant-explanation mb-2"></div><div class="assistant-diff"></div>';
|
|
274
|
+
MpKit.ai.renderAnswer(result.querySelector('.assistant-explanation'), { text: res.explanation, streaming: false, title: 'Suggested change' });
|
|
275
|
+
MpKit.ai.renderDiff(result.querySelector('.assistant-diff'), {
|
|
276
|
+
before: shareable,
|
|
277
|
+
after: suggested,
|
|
278
|
+
applyLabel: 'Apply',
|
|
279
|
+
onApply: () => this.applyConfig(config, suggested),
|
|
280
|
+
});
|
|
281
|
+
} catch (e) {
|
|
282
|
+
result.innerHTML = '';
|
|
283
|
+
MpKit.ai.renderAnswer(result, { streaming: false }).error(e);
|
|
284
|
+
} finally {
|
|
285
|
+
button.disabled = false;
|
|
286
|
+
button.removeAttribute('aria-busy');
|
|
287
|
+
}
|
|
288
|
+
},
|
|
289
|
+
|
|
290
|
+
async applyConfig(config, suggested) {
|
|
291
|
+
const newConfig = { ...suggested, platform: 'DaikinCloud', name: suggested.name || config.name || 'Daikin Cloud' };
|
|
292
|
+
this.PRIVATE_KEYS.forEach(k => {
|
|
293
|
+
if (config[k] !== undefined) {
|
|
294
|
+
newConfig[k] = config[k];
|
|
295
|
+
}
|
|
296
|
+
});
|
|
297
|
+
// The settings tab saves every change straight away; do the same here
|
|
298
|
+
await homebridge.updatePluginConfig([newConfig]);
|
|
299
|
+
await homebridge.savePluginConfig();
|
|
300
|
+
|
|
301
|
+
Settings.populateForm(newConfig);
|
|
302
|
+
Settings.excludedIds = newConfig.excludedDevicesByDeviceId || [];
|
|
303
|
+
Settings.loadDeviceToggles();
|
|
304
|
+
AuthMode.current = AuthMode.previous = newConfig.authMode || 'developer_portal';
|
|
305
|
+
AuthMode.updateUI();
|
|
306
|
+
homebridge.toast.success('Change saved. Restart Homebridge to apply it.');
|
|
307
|
+
},
|
|
308
|
+
};
|
|
309
|
+
|
|
117
310
|
// ============================================================================
|
|
118
311
|
// Tab Navigation
|
|
119
312
|
// ============================================================================
|
|
@@ -307,20 +500,33 @@ const Auth = {
|
|
|
307
500
|
async testConnection() {
|
|
308
501
|
UI.setButtonLoading(El.btnTest, true, 'Testing...');
|
|
309
502
|
DOM.hide(El.testResult);
|
|
503
|
+
Assistant.clear('test-problem');
|
|
310
504
|
|
|
311
505
|
try {
|
|
312
506
|
const result = await homebridge.request('/auth/test');
|
|
313
507
|
El.testResult.className = `alert ${result.success ? 'alert-success' : 'alert-danger'}`;
|
|
314
508
|
El.testResult.textContent = result.message;
|
|
315
509
|
DOM.show(El.testResult);
|
|
510
|
+
if (!result.success) {
|
|
511
|
+
this.offerExplain(result.message);
|
|
512
|
+
}
|
|
316
513
|
} catch (error) {
|
|
317
514
|
El.testResult.className = 'alert alert-danger';
|
|
318
515
|
El.testResult.textContent = 'Test failed: ' + error.message;
|
|
319
516
|
DOM.show(El.testResult);
|
|
517
|
+
this.offerExplain(El.testResult.textContent);
|
|
320
518
|
}
|
|
321
519
|
|
|
322
520
|
UI.setButtonLoading(El.btnTest, false, null, 'Test Connection');
|
|
323
521
|
},
|
|
522
|
+
|
|
523
|
+
offerExplain(message) {
|
|
524
|
+
Assistant.offer('test-problem', {
|
|
525
|
+
message,
|
|
526
|
+
context: 'Test Connection in the plugin settings (reads the saved token and lists the gateway devices from the Daikin Onecta API) failed.',
|
|
527
|
+
title: 'Why did the connection test fail?',
|
|
528
|
+
});
|
|
529
|
+
},
|
|
324
530
|
};
|
|
325
531
|
|
|
326
532
|
// ============================================================================
|
|
@@ -387,6 +593,7 @@ const Wizard = {
|
|
|
387
593
|
}
|
|
388
594
|
|
|
389
595
|
DOM.hide(El.validationErrors);
|
|
596
|
+
Assistant.clear('wizard-start-problem');
|
|
390
597
|
const authResult = await Auth.startAuth(config);
|
|
391
598
|
|
|
392
599
|
El.authUrlDisplay.textContent = authResult.authUrl;
|
|
@@ -403,6 +610,11 @@ const Wizard = {
|
|
|
403
610
|
} catch (error) {
|
|
404
611
|
El.validationErrors.innerHTML = `<div>${Utils.escapeHtml(error.message)}</div>`;
|
|
405
612
|
DOM.show(El.validationErrors);
|
|
613
|
+
Assistant.offer('wizard-start-problem', {
|
|
614
|
+
message: error.message,
|
|
615
|
+
context: 'Starting the Developer Portal OAuth login (temporary HTTPS callback server) from the plugin settings failed.',
|
|
616
|
+
title: 'Why can the login not start?',
|
|
617
|
+
});
|
|
406
618
|
}
|
|
407
619
|
|
|
408
620
|
UI.hideLoading();
|
|
@@ -428,6 +640,7 @@ const Wizard = {
|
|
|
428
640
|
}
|
|
429
641
|
|
|
430
642
|
UI.showLoading();
|
|
643
|
+
Assistant.clear('wizard-auth-problem');
|
|
431
644
|
try {
|
|
432
645
|
const result = await homebridge.request('/auth/', { callbackUrl: url });
|
|
433
646
|
if (result.success) {
|
|
@@ -435,14 +648,24 @@ const Wizard = {
|
|
|
435
648
|
this.goToStep(3);
|
|
436
649
|
await Config.save();
|
|
437
650
|
} else {
|
|
438
|
-
|
|
651
|
+
this.authFailed('Authentication failed: ' + (result.message || 'Unknown error'));
|
|
439
652
|
}
|
|
440
653
|
} catch (error) {
|
|
441
|
-
|
|
654
|
+
this.authFailed('Authentication failed: ' + error.message);
|
|
442
655
|
}
|
|
443
656
|
UI.hideLoading();
|
|
444
657
|
},
|
|
445
658
|
|
|
659
|
+
authFailed(message) {
|
|
660
|
+
UI.showError(message);
|
|
661
|
+
Assistant.offer('wizard-auth-problem', {
|
|
662
|
+
message,
|
|
663
|
+
alert: true,
|
|
664
|
+
context: 'Completing the Developer Portal OAuth login (exchanging the authorization code from the Daikin redirect for tokens) failed.',
|
|
665
|
+
title: 'Why did the login fail?',
|
|
666
|
+
});
|
|
667
|
+
},
|
|
668
|
+
|
|
446
669
|
finish() {
|
|
447
670
|
Polling.stop();
|
|
448
671
|
this.hide();
|
|
@@ -493,7 +716,7 @@ const Polling = {
|
|
|
493
716
|
await Config.save();
|
|
494
717
|
Devices.load();
|
|
495
718
|
} else {
|
|
496
|
-
|
|
719
|
+
Wizard.authFailed('Authentication failed: ' + (result.error || 'Unknown error'));
|
|
497
720
|
}
|
|
498
721
|
return;
|
|
499
722
|
}
|
|
@@ -853,11 +1076,15 @@ const Settings = {
|
|
|
853
1076
|
// ============================================================================
|
|
854
1077
|
|
|
855
1078
|
const Devices = {
|
|
1079
|
+
items: [],
|
|
1080
|
+
|
|
856
1081
|
async load({ refresh = false } = {}) {
|
|
857
1082
|
DOM.show(El.devicesLoading);
|
|
858
1083
|
El.devicesList.innerHTML = '';
|
|
859
1084
|
DOM.hide(El.devicesEmpty);
|
|
860
1085
|
DOM.hide(El.devicesError);
|
|
1086
|
+
Assistant.clear('devices-problem');
|
|
1087
|
+
this.items = [];
|
|
861
1088
|
|
|
862
1089
|
try {
|
|
863
1090
|
const result = await DeviceList.fetch({ refresh });
|
|
@@ -873,12 +1100,45 @@ const Devices = {
|
|
|
873
1100
|
return;
|
|
874
1101
|
}
|
|
875
1102
|
|
|
876
|
-
|
|
1103
|
+
this.items = result.devices;
|
|
1104
|
+
El.devicesList.innerHTML = result.devices.map((d, i) => this.render(d, i)).join('');
|
|
877
1105
|
} catch (error) {
|
|
878
1106
|
DOM.hide(El.devicesLoading);
|
|
879
1107
|
El.devicesError.textContent = 'Failed to load: ' + error.message;
|
|
880
1108
|
DOM.show(El.devicesError);
|
|
1109
|
+
this.offerExplain(El.devicesError.textContent);
|
|
1110
|
+
}
|
|
1111
|
+
},
|
|
1112
|
+
|
|
1113
|
+
offerExplain(message) {
|
|
1114
|
+
Assistant.offer('devices-problem', {
|
|
1115
|
+
message,
|
|
1116
|
+
context: 'Loading the device list (GET /v1/gateway-devices on the Daikin Onecta API) in the plugin settings failed.',
|
|
1117
|
+
title: 'Why did loading devices fail?',
|
|
1118
|
+
});
|
|
1119
|
+
},
|
|
1120
|
+
|
|
1121
|
+
// Why a device needs attention, or null when it looks fine
|
|
1122
|
+
problem(device) {
|
|
1123
|
+
if (!device.online) {
|
|
1124
|
+
return 'The Daikin Onecta cloud reports this unit as offline: its Wi-Fi adapter has no cloud connection (isCloudConnectionUp is false), so HomeKit cannot control it.';
|
|
1125
|
+
}
|
|
1126
|
+
return null;
|
|
1127
|
+
},
|
|
1128
|
+
|
|
1129
|
+
onListClick(e) {
|
|
1130
|
+
const button = e.target.closest('.js-explain-device');
|
|
1131
|
+
const row = button && button.closest('[data-device-index]');
|
|
1132
|
+
const device = row && this.items[Number(row.dataset.deviceIndex)];
|
|
1133
|
+
if (!device) {
|
|
1134
|
+
return;
|
|
881
1135
|
}
|
|
1136
|
+
Assistant.explain(button, row.querySelector('.assistant-answer'), {
|
|
1137
|
+
error: this.problem(device) || 'The unit does not respond as expected.',
|
|
1138
|
+
context: Assistant.context('The user is looking at the device list of the Daikin Cloud plugin settings.'),
|
|
1139
|
+
device: Assistant.device(device),
|
|
1140
|
+
title: `Why does ${device.name || 'this unit'} need attention?`,
|
|
1141
|
+
});
|
|
882
1142
|
},
|
|
883
1143
|
|
|
884
1144
|
handleError(result) {
|
|
@@ -891,10 +1151,11 @@ const Devices = {
|
|
|
891
1151
|
} else {
|
|
892
1152
|
El.devicesError.textContent = result.message;
|
|
893
1153
|
DOM.show(El.devicesError);
|
|
1154
|
+
this.offerExplain(result.message);
|
|
894
1155
|
}
|
|
895
1156
|
},
|
|
896
1157
|
|
|
897
|
-
render(device) {
|
|
1158
|
+
render(device, index) {
|
|
898
1159
|
const online = device.online;
|
|
899
1160
|
const powerOn = device.powerState === 'on';
|
|
900
1161
|
const mode = device.operationMode ? Utils.capitalize(device.operationMode) : '-';
|
|
@@ -909,15 +1170,21 @@ const Devices = {
|
|
|
909
1170
|
`<span class="device-meta-item">Model: <span class="text-body-secondary">${Utils.escapeHtml(device.model)}</span></span>`,
|
|
910
1171
|
].filter(Boolean).join('<span class="device-meta-sep">·</span>');
|
|
911
1172
|
|
|
1173
|
+
const explainButton = Assistant.enabled && this.problem(device)
|
|
1174
|
+
? MpKit.ai.renderButton({ label: 'Explain', size: 'sm', className: 'js-explain-device', title: 'Explain this device problem' })
|
|
1175
|
+
: '';
|
|
1176
|
+
|
|
912
1177
|
return `
|
|
913
|
-
<div class="list-group-item">
|
|
1178
|
+
<div class="list-group-item" data-device-index="${index}">
|
|
914
1179
|
<div class="device-header">
|
|
915
1180
|
<span class="device-power ${powerOn ? 'power-on' : 'power-off'}">${powerOn ? 'ON' : 'OFF'}</span>
|
|
916
1181
|
<span class="device-name fw-semibold">${Utils.escapeHtml(device.name)}</span>
|
|
917
1182
|
${features ? `<div class="device-features">${features}</div>` : ''}
|
|
1183
|
+
${explainButton}
|
|
918
1184
|
<span class="device-status ${online ? 'online' : 'offline'}">${online ? 'Online' : 'Offline'}</span>
|
|
919
1185
|
</div>
|
|
920
1186
|
<div class="device-meta mt-1">${meta}</div>
|
|
1187
|
+
<div class="assistant-answer mt-2 d-none"></div>
|
|
921
1188
|
</div>`;
|
|
922
1189
|
},
|
|
923
1190
|
|
|
@@ -1023,6 +1290,7 @@ const MobileAuth = {
|
|
|
1023
1290
|
|
|
1024
1291
|
DOM.hide(errors);
|
|
1025
1292
|
DOM.hide(success);
|
|
1293
|
+
Assistant.clear('mobile-auth-problem');
|
|
1026
1294
|
|
|
1027
1295
|
if (!email || !password) {
|
|
1028
1296
|
errors.textContent = 'Please enter email and password';
|
|
@@ -1050,15 +1318,25 @@ const MobileAuth = {
|
|
|
1050
1318
|
} else {
|
|
1051
1319
|
errors.textContent = result.message || 'Authentication failed';
|
|
1052
1320
|
DOM.show(errors);
|
|
1321
|
+
this.offerExplain(errors.textContent);
|
|
1053
1322
|
}
|
|
1054
1323
|
} catch (error) {
|
|
1055
1324
|
errors.textContent = 'Failed: ' + error.message;
|
|
1056
1325
|
DOM.show(errors);
|
|
1326
|
+
this.offerExplain(errors.textContent);
|
|
1057
1327
|
}
|
|
1058
1328
|
|
|
1059
1329
|
UI.setButtonLoading(btn, false, null, 'Test & Save Credentials');
|
|
1060
1330
|
},
|
|
1061
1331
|
|
|
1332
|
+
offerExplain(message) {
|
|
1333
|
+
Assistant.offer('mobile-auth-problem', {
|
|
1334
|
+
message,
|
|
1335
|
+
context: 'Mobile App login (Onecta account, Gigya login at id.daikin.eu, then OIDC token exchange) from the plugin settings failed.',
|
|
1336
|
+
title: 'Why did the Mobile App login fail?',
|
|
1337
|
+
});
|
|
1338
|
+
},
|
|
1339
|
+
|
|
1062
1340
|
async saveCredentials(email, password) {
|
|
1063
1341
|
const config = await homebridge.getPluginConfig();
|
|
1064
1342
|
const platformConfig = config[0] || { platform: 'DaikinCloud', name: 'Daikin Cloud' };
|
|
@@ -1237,6 +1515,8 @@ document.addEventListener('DOMContentLoaded', async () => {
|
|
|
1237
1515
|
// getUserSettings not available in older versions — keep the early-detected theme
|
|
1238
1516
|
}
|
|
1239
1517
|
cacheElements();
|
|
1518
|
+
await Assistant.init();
|
|
1519
|
+
El.devicesList.addEventListener('click', (e) => Devices.onListClick(e));
|
|
1240
1520
|
Auth.loadStatus();
|
|
1241
1521
|
Config.load();
|
|
1242
1522
|
await AuthMode.init();
|
package/homebridge-ui/server.js
CHANGED
|
@@ -22,6 +22,7 @@ const {
|
|
|
22
22
|
RATE_LIMIT_STATUS_FILE,
|
|
23
23
|
} = require(join(__dirname, '..', 'dist', 'src', 'constants'));
|
|
24
24
|
const { validateConfig } = require(join(__dirname, '..', 'dist', 'src', 'config', 'config-manager'));
|
|
25
|
+
const { registerAssistant } = require('./assistant');
|
|
25
26
|
|
|
26
27
|
// =============================================================================
|
|
27
28
|
// Configuration
|
|
@@ -520,7 +521,11 @@ function createDaikinCloudUiServer(HomebridgePluginUiServer) {
|
|
|
520
521
|
|
|
521
522
|
this.applyTransportFromConfig();
|
|
522
523
|
this.registerHandlers();
|
|
523
|
-
|
|
524
|
+
|
|
525
|
+
// Assistant: /ai/status, /ai/explain, /ai/ask, /ai/config (configured in Homebridge AI Kit).
|
|
526
|
+
// ai-core is ESM and loaded with import(); report ready once its routes are in place
|
|
527
|
+
// (registerAssistant never rejects) so the UI's first /ai/status call finds them.
|
|
528
|
+
this.assistantReady = registerAssistant(this).then(() => this.ready());
|
|
524
529
|
}
|
|
525
530
|
|
|
526
531
|
getStoragePath() {
|
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"displayName": "Daikin Cloud",
|
|
3
3
|
"platformname": "daikincloud",
|
|
4
4
|
"name": "@mp-consulting/homebridge-daikin-cloud",
|
|
5
|
-
"version": "1.
|
|
5
|
+
"version": "1.7.0",
|
|
6
6
|
"description": "Integrate with the Daikin Cloud to control your Daikin air conditioning via the cloud",
|
|
7
7
|
"license": "(Apache-2.0 AND MIT)",
|
|
8
8
|
"repository": {
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
"smoke:ui": "node scripts/smoke-ui-server.js",
|
|
29
29
|
"schema:check": "tsx scripts/check-schema-drift.ts",
|
|
30
30
|
"watch": "npm run build && npm link && nodemon",
|
|
31
|
-
"copy:ui-kit": "
|
|
31
|
+
"copy:ui-kit": "mp-ui-kit-copy --vendor",
|
|
32
32
|
"build": "npm run copy:ui-kit && rimraf ./dist && tsc",
|
|
33
33
|
"prepublishOnly": "npm run lint && npm run build",
|
|
34
34
|
"release": "npm publish",
|
|
@@ -39,12 +39,13 @@
|
|
|
39
39
|
],
|
|
40
40
|
"dependencies": {
|
|
41
41
|
"@homebridge/plugin-ui-utils": "^2.2.6",
|
|
42
|
+
"@mp-consulting/homebridge-ai-core": "^2.0.0",
|
|
42
43
|
"ws": "^8.21.3",
|
|
43
44
|
"zod": "^4.5.4"
|
|
44
45
|
},
|
|
45
46
|
"devDependencies": {
|
|
46
47
|
"@eslint/js": "^10.0.1",
|
|
47
|
-
"@mp-consulting/homebridge-ui-kit": "^1.
|
|
48
|
+
"@mp-consulting/homebridge-ui-kit": "^1.2.0",
|
|
48
49
|
"@types/node": "^26.5.0",
|
|
49
50
|
"@types/ws": "^8.18.1",
|
|
50
51
|
"@vitest/coverage-v8": "^5.0.0",
|
|
@@ -59,6 +60,10 @@
|
|
|
59
60
|
"typescript-eslint": "^8.70.0",
|
|
60
61
|
"vitest": "^5.0.0"
|
|
61
62
|
},
|
|
63
|
+
"overrides": {
|
|
64
|
+
"@nestjs/platform-fastify": "^12.1.2",
|
|
65
|
+
"fastify": "^5.12.5"
|
|
66
|
+
},
|
|
62
67
|
"funding": {
|
|
63
68
|
"type": "paypal",
|
|
64
69
|
"url": "https://paypal.me/mickaelpalma"
|