homebridge-tuya-without-developer-account 1.0.8 → 1.0.11
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 +18 -0
- package/README.md +9 -0
- package/homebridge-ui/public/index.html +112 -19
- package/homebridge-ui/server.js +57 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,23 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.0.11
|
|
4
|
+
|
|
5
|
+
- Fixed custom Homebridge UI initialization so the Adaptive Lighting checkbox stays checked after saving and reopening plugin settings.
|
|
6
|
+
- Fixed config reload handling so saved `options.enableAdaptiveLighting` is copied into the checkbox before the UI normalizes/stages plugin config.
|
|
7
|
+
- Preserved the `options.userCode` fix from v1.0.10 while preventing checkbox defaults from overwriting saved values.
|
|
8
|
+
|
|
9
|
+
## 1.0.10
|
|
10
|
+
|
|
11
|
+
- Fixed the custom Homebridge settings UI so saving Adaptive Lighting or other configuration-only changes preserves `options.userCode`.
|
|
12
|
+
- Added automatic discovery of existing `tuya-ha-qr-auth.<USER_CODE>.json` files from Homebridge storage. If the config is missing `userCode` but an auth file exists, the UI restores the User Code field and asks the user to save.
|
|
13
|
+
- Prevented empty User Code values from overwriting an existing QR-auth configuration during `updatePluginConfig()`.
|
|
14
|
+
|
|
15
|
+
## 1.0.9
|
|
16
|
+
|
|
17
|
+
- Fixed the custom Homebridge settings UI so toggling Adaptive Lighting marks the config as changed and enables **Save Configuration**.
|
|
18
|
+
- Save now performs a final existing-auth check before blocking, so normal configuration-only changes are not prevented when a QR auth token is already saved.
|
|
19
|
+
- Name and AC override UI changes also mark the custom config as dirty more reliably.
|
|
20
|
+
|
|
3
21
|
## 1.0.8
|
|
4
22
|
|
|
5
23
|
- Added optional HomeKit Adaptive Lighting support for eligible Tuya lights.
|
package/README.md
CHANGED
|
@@ -250,6 +250,15 @@ HomeKit stores temperature characteristic metadata in Celsius. Do not enter Fahr
|
|
|
250
250
|
|
|
251
251
|
## Adaptive Lighting
|
|
252
252
|
|
|
253
|
+
|
|
254
|
+
### v1.0.11 User Code preservation fix
|
|
255
|
+
|
|
256
|
+
Version 1.0.11 fixes a custom settings UI regression where saving Adaptive Lighting or other configuration-only changes could preserve the auth file but remove `options.userCode` from `config.json`. The UI now preserves the existing User Code and can recover it automatically from saved `tuya-ha-qr-auth.<USER_CODE>.json` files.
|
|
257
|
+
|
|
258
|
+
### v1.0.9 UI save-state fix
|
|
259
|
+
|
|
260
|
+
Version 1.0.9 fixes the custom settings UI so changing the Adaptive Lighting checkbox immediately enables **Save Configuration**. If QR authentication is already saved, the UI performs a final auth check during save and no longer blocks normal configuration-only changes.
|
|
261
|
+
|
|
253
262
|
Version 1.0.8 adds optional HomeKit Adaptive Lighting support. Enable it in the Homebridge plugin settings with **Enable Adaptive Lighting for eligible CCT/RGBCW lights**.
|
|
254
263
|
|
|
255
264
|
Adaptive Lighting is applied only to Tuya light accessories that expose both:
|
|
@@ -191,7 +191,9 @@
|
|
|
191
191
|
let currentConfig = null;
|
|
192
192
|
let pollTimer = null;
|
|
193
193
|
let isAuthenticated = false;
|
|
194
|
+
let isDirty = false;
|
|
194
195
|
let detectedDevices = [];
|
|
196
|
+
let discoveredUserCode = '';
|
|
195
197
|
|
|
196
198
|
const $ = (id) => document.getElementById(id);
|
|
197
199
|
|
|
@@ -207,8 +209,39 @@
|
|
|
207
209
|
el.textContent = message;
|
|
208
210
|
}
|
|
209
211
|
|
|
212
|
+
function firstNonEmpty(...values) {
|
|
213
|
+
for (const value of values) {
|
|
214
|
+
if (value === undefined || value === null) {
|
|
215
|
+
continue;
|
|
216
|
+
}
|
|
217
|
+
const text = String(value).trim();
|
|
218
|
+
if (text) {
|
|
219
|
+
return text;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
return '';
|
|
223
|
+
}
|
|
224
|
+
|
|
210
225
|
function getUserCode() {
|
|
211
|
-
return
|
|
226
|
+
return firstNonEmpty(
|
|
227
|
+
$('tuyaNodevUserCode')?.value,
|
|
228
|
+
currentConfig?.options?.userCode,
|
|
229
|
+
discoveredUserCode
|
|
230
|
+
);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function setUserCode(userCode) {
|
|
234
|
+
const value = String(userCode || '').trim();
|
|
235
|
+
if (!value) {
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
discoveredUserCode = value;
|
|
239
|
+
if ($('tuyaNodevUserCode')) {
|
|
240
|
+
$('tuyaNodevUserCode').value = value;
|
|
241
|
+
}
|
|
242
|
+
currentConfig = currentConfig && typeof currentConfig === 'object' ? currentConfig : {};
|
|
243
|
+
currentConfig.options = currentConfig.options && typeof currentConfig.options === 'object' ? currentConfig.options : {};
|
|
244
|
+
currentConfig.options.userCode = value;
|
|
212
245
|
}
|
|
213
246
|
|
|
214
247
|
function getPlatformName() {
|
|
@@ -224,7 +257,10 @@
|
|
|
224
257
|
cfg.platform = PLATFORM;
|
|
225
258
|
cfg.name = getPlatformName();
|
|
226
259
|
cfg.options = cfg.options && typeof cfg.options === 'object' ? cfg.options : {};
|
|
227
|
-
|
|
260
|
+
const effectiveUserCode = getUserCode();
|
|
261
|
+
if (effectiveUserCode) {
|
|
262
|
+
cfg.options.userCode = effectiveUserCode;
|
|
263
|
+
}
|
|
228
264
|
cfg.options.projectType = '3';
|
|
229
265
|
cfg.options.enableAdaptiveLighting = $('tuyaNodevAdaptiveLighting')?.checked === true;
|
|
230
266
|
if (!Array.isArray(cfg.options.deviceOverrides)) {
|
|
@@ -269,6 +305,11 @@
|
|
|
269
305
|
if (homebridge.enableSaveButton) homebridge.enableSaveButton();
|
|
270
306
|
}
|
|
271
307
|
|
|
308
|
+
function markDirty() {
|
|
309
|
+
isDirty = true;
|
|
310
|
+
enableSaving();
|
|
311
|
+
}
|
|
312
|
+
|
|
272
313
|
function getDeviceName(id) {
|
|
273
314
|
const device = detectedDevices.find((item) => item.id === id);
|
|
274
315
|
return device ? device.name : '';
|
|
@@ -417,9 +458,7 @@
|
|
|
417
458
|
const name = getDeviceName(id) || id;
|
|
418
459
|
setAcStatus(`AC override saved in plugin config for ${name}: ${minTemperature}–${maxTemperature} °C, step ${temperatureStep} °C. Click Save Configuration when ready.`, 'success');
|
|
419
460
|
homebridge.toast.success('AC temperature override added to config.', 'Tuya');
|
|
420
|
-
|
|
421
|
-
enableSaving();
|
|
422
|
-
}
|
|
461
|
+
markDirty();
|
|
423
462
|
} catch (e) {
|
|
424
463
|
setAcStatus(e.message || 'Failed to add AC override.', 'danger');
|
|
425
464
|
}
|
|
@@ -453,9 +492,7 @@
|
|
|
453
492
|
setAcStatus('Selected AC override was removed from the plugin config. Click Save Configuration when ready.', 'success');
|
|
454
493
|
homebridge.toast.success('AC temperature override removed from config.', 'Tuya');
|
|
455
494
|
}
|
|
456
|
-
|
|
457
|
-
enableSaving();
|
|
458
|
-
}
|
|
495
|
+
markDirty();
|
|
459
496
|
}
|
|
460
497
|
|
|
461
498
|
async function checkAuth(showSuccessToast = false) {
|
|
@@ -589,6 +626,34 @@
|
|
|
589
626
|
}
|
|
590
627
|
}
|
|
591
628
|
|
|
629
|
+
|
|
630
|
+
|
|
631
|
+
async function discoverExistingAuth(showMessage = false) {
|
|
632
|
+
try {
|
|
633
|
+
const res = await homebridge.request('/auth/discover', {});
|
|
634
|
+
if (res && res.found && res.userCode) {
|
|
635
|
+
const hadUserCode = !!getUserCode();
|
|
636
|
+
setUserCode(res.userCode);
|
|
637
|
+
if (!hadUserCode) {
|
|
638
|
+
isAuthenticated = true;
|
|
639
|
+
isDirty = true;
|
|
640
|
+
enableSaving();
|
|
641
|
+
const who = res.username || res.uid || res.endpoint || res.userCode;
|
|
642
|
+
setStatus(`Existing Tuya QR auth was found for ${who}. Click Save Configuration to restore the User Code in config.json.`, 'warning');
|
|
643
|
+
} else if (showMessage) {
|
|
644
|
+
const who = res.username || res.uid || res.endpoint || res.userCode;
|
|
645
|
+
setStatus(`Existing Tuya QR auth found for ${who}.`, 'success');
|
|
646
|
+
}
|
|
647
|
+
return res;
|
|
648
|
+
}
|
|
649
|
+
} catch (e) {
|
|
650
|
+
if (showMessage) {
|
|
651
|
+
setStatus(e.message || 'Could not discover existing Tuya QR authentication.', 'warning');
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
return null;
|
|
655
|
+
}
|
|
656
|
+
|
|
592
657
|
async function verifySavedConfig(userCode) {
|
|
593
658
|
const blocks = await withTimeout(
|
|
594
659
|
homebridge.getPluginConfig(),
|
|
@@ -600,18 +665,28 @@
|
|
|
600
665
|
}
|
|
601
666
|
|
|
602
667
|
async function saveConfig() {
|
|
603
|
-
if (!
|
|
604
|
-
|
|
605
|
-
return;
|
|
668
|
+
if (!getUserCode()) {
|
|
669
|
+
await discoverExistingAuth(false);
|
|
606
670
|
}
|
|
607
|
-
|
|
608
671
|
const userCode = getUserCode();
|
|
609
672
|
if (!userCode) {
|
|
610
673
|
setStatus('User Code is required before saving.', 'warning');
|
|
674
|
+
homebridge.toast.error('User Code is required before saving.', 'Tuya');
|
|
611
675
|
return;
|
|
612
676
|
}
|
|
677
|
+
setUserCode(userCode);
|
|
613
678
|
|
|
614
679
|
try {
|
|
680
|
+
if (!isAuthenticated) {
|
|
681
|
+
// Do a last auth check here. This prevents normal configuration-only changes
|
|
682
|
+
// such as Adaptive Lighting from being blocked when the token already exists
|
|
683
|
+
// but this UI session has not marked itself authenticated yet.
|
|
684
|
+
const auth = await homebridge.request('/auth/status', { userCode });
|
|
685
|
+
isAuthenticated = !!auth.authenticated;
|
|
686
|
+
if (!isAuthenticated) {
|
|
687
|
+
throw new Error('Scan and approve the QR code, or click Check Existing Auth, before saving.');
|
|
688
|
+
}
|
|
689
|
+
}
|
|
615
690
|
homebridge.showSpinner();
|
|
616
691
|
$('tuyaNodevSave').disabled = true;
|
|
617
692
|
|
|
@@ -649,11 +724,13 @@
|
|
|
649
724
|
|
|
650
725
|
if (saveTimedOut && verified) {
|
|
651
726
|
const message = 'Configuration appears to be saved, but Homebridge UI did not return a save confirmation. Close this settings window and restart Homebridge.';
|
|
727
|
+
isDirty = false;
|
|
652
728
|
homebridge.toast.success(message, 'Tuya');
|
|
653
729
|
setStatus(message, 'success');
|
|
654
730
|
return;
|
|
655
731
|
}
|
|
656
732
|
|
|
733
|
+
isDirty = false;
|
|
657
734
|
homebridge.toast.success('Configuration saved. Restart Homebridge to load devices.', 'Tuya');
|
|
658
735
|
setStatus('Configuration saved. Restart Homebridge to load devices.', 'success');
|
|
659
736
|
} catch (e) {
|
|
@@ -661,7 +738,7 @@
|
|
|
661
738
|
homebridge.toast.error(e.message || 'Failed to save configuration.', 'Tuya');
|
|
662
739
|
} finally {
|
|
663
740
|
homebridge.hideSpinner();
|
|
664
|
-
if (isAuthenticated) {
|
|
741
|
+
if (isAuthenticated || isDirty) {
|
|
665
742
|
$('tuyaNodevSave').disabled = false;
|
|
666
743
|
}
|
|
667
744
|
}
|
|
@@ -676,10 +753,14 @@
|
|
|
676
753
|
mode: 'cloud',
|
|
677
754
|
options: { projectType: '3', deviceOverrides: [] },
|
|
678
755
|
};
|
|
679
|
-
|
|
756
|
+
currentConfig.options = currentConfig.options && typeof currentConfig.options === 'object' ? currentConfig.options : {};
|
|
757
|
+
|
|
758
|
+
// Important: restore saved values into the UI before normalising/staging config.
|
|
759
|
+
// Otherwise the default unchecked checkbox can overwrite a saved true value.
|
|
680
760
|
$('tuyaNodevName').value = currentConfig.name || 'Tuya without developer account';
|
|
681
|
-
|
|
761
|
+
if (currentConfig.options?.userCode) setUserCode(currentConfig.options.userCode);
|
|
682
762
|
$('tuyaNodevAdaptiveLighting').checked = currentConfig.options?.enableAdaptiveLighting === true;
|
|
763
|
+
ensureConfig();
|
|
683
764
|
|
|
684
765
|
if (homebridge.showSchemaForm) homebridge.showSchemaForm();
|
|
685
766
|
window.homebridge.addEventListener('configChanged', (event) => {
|
|
@@ -687,10 +768,14 @@
|
|
|
687
768
|
const block = Array.isArray(data) ? data[0] : data;
|
|
688
769
|
if (block && typeof block === 'object') {
|
|
689
770
|
currentConfig = block;
|
|
690
|
-
|
|
771
|
+
currentConfig.options = currentConfig.options && typeof currentConfig.options === 'object' ? currentConfig.options : {};
|
|
772
|
+
|
|
773
|
+
// Same ordering as initial load: update form controls first, then normalise.
|
|
691
774
|
if (block.name) $('tuyaNodevName').value = block.name;
|
|
692
|
-
if (block.options?.userCode)
|
|
775
|
+
if (block.options?.userCode) setUserCode(block.options.userCode);
|
|
776
|
+
else if (getUserCode()) setUserCode(getUserCode());
|
|
693
777
|
$('tuyaNodevAdaptiveLighting').checked = block.options?.enableAdaptiveLighting === true;
|
|
778
|
+
ensureConfig();
|
|
694
779
|
renderAcOverrides();
|
|
695
780
|
}
|
|
696
781
|
});
|
|
@@ -699,10 +784,14 @@
|
|
|
699
784
|
$('tuyaNodevCheck').addEventListener('click', () => checkAuth(true));
|
|
700
785
|
$('tuyaNodevClear').addEventListener('click', clearAuth);
|
|
701
786
|
$('tuyaNodevSave').addEventListener('click', saveConfig);
|
|
702
|
-
$('tuyaNodevName').addEventListener('input',
|
|
787
|
+
$('tuyaNodevName').addEventListener('input', async () => {
|
|
788
|
+
await syncConfigToUi();
|
|
789
|
+
markDirty();
|
|
790
|
+
});
|
|
703
791
|
$('tuyaNodevAdaptiveLighting').addEventListener('change', async () => {
|
|
704
792
|
await syncConfigToUi();
|
|
705
|
-
|
|
793
|
+
markDirty();
|
|
794
|
+
setStatus('Adaptive Lighting setting changed. Click Save Configuration when ready.', 'info');
|
|
706
795
|
});
|
|
707
796
|
$('tuyaNodevUserCode').addEventListener('input', () => {
|
|
708
797
|
isAuthenticated = false;
|
|
@@ -713,6 +802,10 @@
|
|
|
713
802
|
$('tuyaNodevApplyAc').addEventListener('click', addOrUpdateAcOverride);
|
|
714
803
|
$('tuyaNodevRemoveAc').addEventListener('click', removeSelectedAcOverride);
|
|
715
804
|
|
|
805
|
+
if (!getUserCode()) {
|
|
806
|
+
await discoverExistingAuth(false);
|
|
807
|
+
}
|
|
808
|
+
|
|
716
809
|
await syncConfigToUi();
|
|
717
810
|
await loadDetectedDevices(false);
|
|
718
811
|
if (getUserCode()) {
|
package/homebridge-ui/server.js
CHANGED
|
@@ -179,6 +179,7 @@ function collectDevicesFromObject(root) {
|
|
|
179
179
|
this.onRequest('/qr/status', this.qrStatus.bind(this));
|
|
180
180
|
this.onRequest('/auth/status', this.authStatus.bind(this));
|
|
181
181
|
this.onRequest('/auth/clear', this.clearAuth.bind(this));
|
|
182
|
+
this.onRequest('/auth/discover', this.discoverAuth.bind(this));
|
|
182
183
|
this.onRequest('/devices/list', this.listDevices.bind(this));
|
|
183
184
|
this.ready();
|
|
184
185
|
}
|
|
@@ -271,6 +272,62 @@ function collectDevicesFromObject(root) {
|
|
|
271
272
|
};
|
|
272
273
|
}
|
|
273
274
|
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
async discoverAuth() {
|
|
278
|
+
let entries;
|
|
279
|
+
try {
|
|
280
|
+
entries = await fs.promises.readdir(this.homebridgeStoragePath, { withFileTypes: true });
|
|
281
|
+
} catch (err) {
|
|
282
|
+
if (err && err.code === 'ENOENT') {
|
|
283
|
+
return { found: false, auths: [] };
|
|
284
|
+
}
|
|
285
|
+
throw err;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
const auths = [];
|
|
289
|
+
for (const entry of entries) {
|
|
290
|
+
if (!entry.isFile()) {
|
|
291
|
+
continue;
|
|
292
|
+
}
|
|
293
|
+
const match = entry.name.match(/^tuya-ha-qr-auth\.(.+)\.json$/i);
|
|
294
|
+
if (!match) {
|
|
295
|
+
continue;
|
|
296
|
+
}
|
|
297
|
+
const userCode = normaliseUserCode(match[1]);
|
|
298
|
+
if (!userCode) {
|
|
299
|
+
continue;
|
|
300
|
+
}
|
|
301
|
+
const file = path.join(this.homebridgeStoragePath, entry.name);
|
|
302
|
+
try {
|
|
303
|
+
const stat = await fs.promises.stat(file);
|
|
304
|
+
const data = await this.readAuthFile(userCode);
|
|
305
|
+
if (!data) {
|
|
306
|
+
continue;
|
|
307
|
+
}
|
|
308
|
+
auths.push({
|
|
309
|
+
userCode,
|
|
310
|
+
file,
|
|
311
|
+
username: data.username || null,
|
|
312
|
+
uid: data.tokenInfo?.uid || null,
|
|
313
|
+
endpoint: data.endpoint || null,
|
|
314
|
+
savedAt: data.savedAt || null,
|
|
315
|
+
mtimeMs: stat.mtimeMs,
|
|
316
|
+
});
|
|
317
|
+
} catch {
|
|
318
|
+
// Ignore unreadable or incomplete auth files.
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
auths.sort((a, b) => (b.savedAt || b.mtimeMs || 0) - (a.savedAt || a.mtimeMs || 0));
|
|
323
|
+
const latest = auths[0] || null;
|
|
324
|
+
return {
|
|
325
|
+
found: !!latest,
|
|
326
|
+
...(latest || {}),
|
|
327
|
+
auths,
|
|
328
|
+
};
|
|
329
|
+
}
|
|
330
|
+
|
|
274
331
|
async authStatus(payload = {}) {
|
|
275
332
|
const userCode = normaliseUserCode(payload.userCode);
|
|
276
333
|
if (!userCode) {
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "homebridge-tuya-without-developer-account",
|
|
3
3
|
"displayName": "Tuya without developer account for Homebridge",
|
|
4
|
-
"version": "1.0.
|
|
4
|
+
"version": "1.0.11",
|
|
5
5
|
"description": "Homebridge plugin for Tuya and Smart Life devices using QR cloud authentication without a Tuya IoT developer account.",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"author": "Kosztyk",
|