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 +19 -0
- package/README.md +4 -0
- package/dist/shared/accessories/SwitchAccessory.js +27 -32
- package/homebridge-ui/public/index.html +85 -6
- package/homebridge-ui/server.js +57 -0
- package/package.json +1 -1
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')
|
|
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 (
|
|
57
|
+
if (activeSwitchServices.length > schemata.length) {
|
|
46
58
|
if (shouldRemoveExtras) {
|
|
47
|
-
this.log.warn(`[SwitchAccessory] Found ${
|
|
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 ${
|
|
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
|
|
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.
|
|
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
|
-
|
|
119
|
-
const
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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)
|
|
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()) {
|
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.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",
|