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 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 $('tuyaNodevUserCode').value.trim();
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
- cfg.options.userCode = getUserCode();
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
- if (isAuthenticated) {
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
- if (isAuthenticated) {
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 (!isAuthenticated) {
604
- setStatus('Scan and approve the QR code before saving.', 'warning');
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
- ensureConfig();
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
- $('tuyaNodevUserCode').value = currentConfig.options?.userCode || '';
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
- ensureConfig();
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) $('tuyaNodevUserCode').value = 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', syncConfigToUi);
787
+ $('tuyaNodevName').addEventListener('input', async () => {
788
+ await syncConfigToUi();
789
+ markDirty();
790
+ });
703
791
  $('tuyaNodevAdaptiveLighting').addEventListener('change', async () => {
704
792
  await syncConfigToUi();
705
- if (isAuthenticated) enableSaving();
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()) {
@@ -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.8",
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",