homebridge-tuya-without-developer-account 1.0.9 → 1.0.12

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,24 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.0.12
4
+
5
+ - Stopped exposing `switch_inching` as a HomeKit switch because it is an internal Tuya inching/timer configuration DP, not a user-facing relay.
6
+ - Automatically removes cached `switch_inching` Switch/Outlet services from multi-gang switch accessories when the accessory is reconfigured.
7
+ - Filters hidden/internal switch configuration DPs from switch auto-discovery so they cannot create invalid HomeKit names.
8
+ - Prevents future HAP-NodeJS invalid-name warnings caused by the raw `switch_inching` service after the affected accessory cache is refreshed.
9
+
10
+ ## 1.0.11
11
+
12
+ - Fixed custom Homebridge UI initialization so the Adaptive Lighting checkbox stays checked after saving and reopening plugin settings.
13
+ - Fixed config reload handling so saved `options.enableAdaptiveLighting` is copied into the checkbox before the UI normalizes/stages plugin config.
14
+ - Preserved the `options.userCode` fix from v1.0.10 while preventing checkbox defaults from overwriting saved values.
15
+
16
+ ## 1.0.10
17
+
18
+ - Fixed the custom Homebridge settings UI so saving Adaptive Lighting or other configuration-only changes preserves `options.userCode`.
19
+ - 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.
20
+ - Prevented empty User Code values from overwriting an existing QR-auth configuration during `updatePluginConfig()`.
21
+
3
22
  ## 1.0.9
4
23
 
5
24
  - Fixed the custom Homebridge settings UI so toggling Adaptive Lighting marks the config as changed and enables **Save Configuration**.
package/README.md CHANGED
@@ -251,6 +251,10 @@ HomeKit stores temperature characteristic metadata in Celsius. Do not enter Fahr
251
251
  ## Adaptive Lighting
252
252
 
253
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
+
254
258
  ### v1.0.9 UI save-state fix
255
259
 
256
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.
@@ -20,6 +20,9 @@ const SCHEMA_CODE = {
20
20
  CURRENT_HUMIDITY: ['va_humidity', 'humidity_value'],
21
21
  INCHING: ['switch_inching'],
22
22
  };
23
+ const INTERNAL_SWITCH_SCHEMA_CODES = new Set([
24
+ 'switch_inching',
25
+ ]);
23
26
  class SwitchAccessory extends BaseAccessory_1.default {
24
27
  requiredSchema() {
25
28
  return [SCHEMA_CODE.ON];
@@ -30,7 +33,9 @@ class SwitchAccessory extends BaseAccessory_1.default {
30
33
  this.platform.log.warn('Remove old service:', oldService.UUID);
31
34
  this.accessory.removeService(oldService);
32
35
  }
33
- const schemata = this.device.schema.filter((schema) => schema.code.startsWith('switch') && schema.type === TuyaDevice_1.TuyaDeviceSchemaType.Boolean);
36
+ const schemata = this.device.schema.filter((schema) => schema.code.startsWith('switch')
37
+ && schema.type === TuyaDevice_1.TuyaDeviceSchemaType.Boolean
38
+ && !INTERNAL_SWITCH_SCHEMA_CODES.has(schema.code));
34
39
  this.log.info(`[SwitchAccessory] Found ${schemata.length} switch schemas: ${schemata.map(s => s.code).join(', ')}`);
35
40
  // Track which switch services should exist
36
41
  const validSubtypes = new Set(schemata.map(s => s.code));
@@ -38,20 +43,27 @@ class SwitchAccessory extends BaseAccessory_1.default {
38
43
  // Match both Switch and Outlet UUIDs since OutletAccessory uses Service.Outlet.
39
44
  const switchOrOutletUUIDs = new Set([this.Service.Switch.UUID, this.Service.Outlet.UUID]);
40
45
  const allSwitchServices = this.accessory.services.filter(s => switchOrOutletUUIDs.has(s.UUID) && s.subtype);
46
+ for (const oldService of [...allSwitchServices]) {
47
+ if (oldService.subtype && INTERNAL_SWITCH_SCHEMA_CODES.has(oldService.subtype)) {
48
+ this.log.warn(`Removing internal Tuya switch config service from cache: ${oldService.displayName} (subtype: ${oldService.subtype})`);
49
+ this.accessory.removeService(oldService);
50
+ }
51
+ }
52
+ const activeSwitchServices = this.accessory.services.filter(s => switchOrOutletUUIDs.has(s.UUID) && s.subtype);
41
53
  // Check early if we'll be keeping services due to auto-detect or config unchanged
42
54
  const configChanged = this.device?.configChanged ?? true;
43
55
  const isAutoDetecting = this.device?.isAutoDetecting ?? false;
44
56
  const shouldRemoveExtras = configChanged && !isAutoDetecting;
45
- if (allSwitchServices.length > schemata.length) {
57
+ if (activeSwitchServices.length > schemata.length) {
46
58
  if (shouldRemoveExtras) {
47
- this.log.warn(`[SwitchAccessory] Found ${allSwitchServices.length} cached switch services but only ${schemata.length} in schema. Removing extras...`);
59
+ this.log.warn(`[SwitchAccessory] Found ${activeSwitchServices.length} cached switch services but only ${schemata.length} in schema. Removing extras...`);
48
60
  }
49
61
  else {
50
- this.log.info(`[SwitchAccessory] Found ${allSwitchServices.length} cached switch services but only ${schemata.length} in schema. ${isAutoDetecting ? 'Auto-detect in progress' : 'Config unchanged'} – keeping for now...`);
62
+ this.log.info(`[SwitchAccessory] Found ${activeSwitchServices.length} cached switch services but only ${schemata.length} in schema. ${isAutoDetecting ? 'Auto-detect in progress' : 'Config unchanged'} – keeping for now...`);
51
63
  }
52
64
  }
53
65
  const keptCachedServices = new Map();
54
- for (const oldService of allSwitchServices) {
66
+ for (const oldService of activeSwitchServices) {
55
67
  if (!validSubtypes.has(oldService.subtype)) {
56
68
  if (shouldRemoveExtras) {
57
69
  // Config changed and not in auto-detect, so enforce the new schema
@@ -95,7 +107,7 @@ class SwitchAccessory extends BaseAccessory_1.default {
95
107
  // Other
96
108
  (0, CurrentTemperature_1.configureCurrentTemperature)(this, undefined, this.getSchema(...SCHEMA_CODE.CURRENT_TEMP));
97
109
  (0, CurrentRelativeHumidity_1.configureCurrentRelativeHumidity)(this, undefined, this.getSchema(...SCHEMA_CODE.CURRENT_HUMIDITY));
98
- this.configureInching();
110
+ this.removeInternalSwitchServices();
99
111
  }
100
112
  async onDeviceInfoUpdate(info) {
101
113
  // Re-run service configuration so newly auto-detected switches get their handlers registered.
@@ -115,33 +127,16 @@ class SwitchAccessory extends BaseAccessory_1.default {
115
127
  (0, EnergyUsage_1.configureEnergyUsage)(this.platform.api, this, service, this.getSchema(...SCHEMA_CODE.CURRENT), this.getSchema(...SCHEMA_CODE.POWER), this.getSchema(...SCHEMA_CODE.VOLTAGE), this.getSchema(...SCHEMA_CODE.TOTAL_POWER));
116
128
  }
117
129
  }
118
- configureInching() {
119
- const schema = this.getSchema(...SCHEMA_CODE.INCHING);
120
- if (!schema || schema.type !== TuyaDevice_1.TuyaDeviceSchemaType.String) {
121
- return;
122
- }
123
- const service = this.accessory.getService(schema.code)
124
- || this.accessory.addService(this.Service.Switch, schema.code, schema.code);
125
- (0, Name_1.configureName)(this, service, schema.code);
126
- service.getCharacteristic(this.Characteristic.On)
127
- .onGet(() => {
128
- this.checkOnlineStatus();
129
- const status = this.getStatus(schema.code);
130
- const buffer = Buffer.from(status.value, 'base64');
131
- return (buffer.length === 3) && (buffer[0] === 1);
132
- })
133
- .onSet(async (value) => {
134
- const status = this.getStatus(schema.code);
135
- let buffer = Buffer.from(status.value, 'base64');
136
- if (buffer.length !== 3) {
137
- buffer = Buffer.alloc(3);
130
+ removeInternalSwitchServices() {
131
+ const switchOrOutletUUIDs = new Set([this.Service.Switch.UUID, this.Service.Outlet.UUID]);
132
+ for (const service of [...this.accessory.services]) {
133
+ if (switchOrOutletUUIDs.has(service.UUID)
134
+ && service.subtype
135
+ && INTERNAL_SWITCH_SCHEMA_CODES.has(service.subtype)) {
136
+ this.log.warn(`Removing internal Tuya switch config service from cache: ${service.displayName} (subtype: ${service.subtype})`);
137
+ this.accessory.removeService(service);
138
138
  }
139
- buffer[0] = value ? 1 : 0;
140
- await this.sendCommands([{
141
- code: schema.code,
142
- value: buffer.toString('base64'),
143
- }], true);
144
- });
139
+ }
145
140
  }
146
141
  }
147
142
  exports.default = SwitchAccessory;
@@ -193,6 +193,7 @@
193
193
  let isAuthenticated = false;
194
194
  let isDirty = false;
195
195
  let detectedDevices = [];
196
+ let discoveredUserCode = '';
196
197
 
197
198
  const $ = (id) => document.getElementById(id);
198
199
 
@@ -208,8 +209,39 @@
208
209
  el.textContent = message;
209
210
  }
210
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
+
211
225
  function getUserCode() {
212
- 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;
213
245
  }
214
246
 
215
247
  function getPlatformName() {
@@ -225,7 +257,10 @@
225
257
  cfg.platform = PLATFORM;
226
258
  cfg.name = getPlatformName();
227
259
  cfg.options = cfg.options && typeof cfg.options === 'object' ? cfg.options : {};
228
- cfg.options.userCode = getUserCode();
260
+ const effectiveUserCode = getUserCode();
261
+ if (effectiveUserCode) {
262
+ cfg.options.userCode = effectiveUserCode;
263
+ }
229
264
  cfg.options.projectType = '3';
230
265
  cfg.options.enableAdaptiveLighting = $('tuyaNodevAdaptiveLighting')?.checked === true;
231
266
  if (!Array.isArray(cfg.options.deviceOverrides)) {
@@ -591,6 +626,34 @@
591
626
  }
592
627
  }
593
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
+
594
657
  async function verifySavedConfig(userCode) {
595
658
  const blocks = await withTimeout(
596
659
  homebridge.getPluginConfig(),
@@ -602,12 +665,16 @@
602
665
  }
603
666
 
604
667
  async function saveConfig() {
668
+ if (!getUserCode()) {
669
+ await discoverExistingAuth(false);
670
+ }
605
671
  const userCode = getUserCode();
606
672
  if (!userCode) {
607
673
  setStatus('User Code is required before saving.', 'warning');
608
674
  homebridge.toast.error('User Code is required before saving.', 'Tuya');
609
675
  return;
610
676
  }
677
+ setUserCode(userCode);
611
678
 
612
679
  try {
613
680
  if (!isAuthenticated) {
@@ -686,10 +753,14 @@
686
753
  mode: 'cloud',
687
754
  options: { projectType: '3', deviceOverrides: [] },
688
755
  };
689
- 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.
690
760
  $('tuyaNodevName').value = currentConfig.name || 'Tuya without developer account';
691
- $('tuyaNodevUserCode').value = currentConfig.options?.userCode || '';
761
+ if (currentConfig.options?.userCode) setUserCode(currentConfig.options.userCode);
692
762
  $('tuyaNodevAdaptiveLighting').checked = currentConfig.options?.enableAdaptiveLighting === true;
763
+ ensureConfig();
693
764
 
694
765
  if (homebridge.showSchemaForm) homebridge.showSchemaForm();
695
766
  window.homebridge.addEventListener('configChanged', (event) => {
@@ -697,10 +768,14 @@
697
768
  const block = Array.isArray(data) ? data[0] : data;
698
769
  if (block && typeof block === 'object') {
699
770
  currentConfig = block;
700
- 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.
701
774
  if (block.name) $('tuyaNodevName').value = block.name;
702
- if (block.options?.userCode) $('tuyaNodevUserCode').value = block.options.userCode;
775
+ if (block.options?.userCode) setUserCode(block.options.userCode);
776
+ else if (getUserCode()) setUserCode(getUserCode());
703
777
  $('tuyaNodevAdaptiveLighting').checked = block.options?.enableAdaptiveLighting === true;
778
+ ensureConfig();
704
779
  renderAcOverrides();
705
780
  }
706
781
  });
@@ -727,6 +802,10 @@
727
802
  $('tuyaNodevApplyAc').addEventListener('click', addOrUpdateAcOverride);
728
803
  $('tuyaNodevRemoveAc').addEventListener('click', removeSelectedAcOverride);
729
804
 
805
+ if (!getUserCode()) {
806
+ await discoverExistingAuth(false);
807
+ }
808
+
730
809
  await syncConfigToUi();
731
810
  await loadDetectedDevices(false);
732
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.9",
4
+ "version": "1.0.12",
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",