homebridge-tuya-without-developer-account 1.0.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.
Files changed (97) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/LICENSE +20 -0
  3. package/PUBLISHING.md +80 -0
  4. package/README.md +233 -0
  5. package/SUPPORTED_DEVICES.md +206 -0
  6. package/config.schema.json +131 -0
  7. package/dist/cloud/api/TuyaHACloudAPI.js +286 -0
  8. package/dist/cloud/api/TuyaHASharingMQ.js +114 -0
  9. package/dist/cloud/device/TuyaDevice.js +50 -0
  10. package/dist/cloud/device/TuyaHADeviceManager.js +355 -0
  11. package/dist/index.js +7 -0
  12. package/dist/platform.js +397 -0
  13. package/dist/settings.js +18 -0
  14. package/dist/shared/AccessoryFactory.js +276 -0
  15. package/dist/shared/accessories/AccessoryFactory.js +305 -0
  16. package/dist/shared/accessories/AirConditionerAccessory.js +307 -0
  17. package/dist/shared/accessories/AirPurifierAccessory.js +90 -0
  18. package/dist/shared/accessories/AirQualitySensorAccessory.js +30 -0
  19. package/dist/shared/accessories/BaseAccessory.js +406 -0
  20. package/dist/shared/accessories/BlindsAccessory.js +199 -0
  21. package/dist/shared/accessories/CameraAccessory.js +121 -0
  22. package/dist/shared/accessories/CarbonDioxideSensorAccessory.js +52 -0
  23. package/dist/shared/accessories/CarbonMonoxideSensorAccessory.js +52 -0
  24. package/dist/shared/accessories/ContactSensorAccessory.js +30 -0
  25. package/dist/shared/accessories/DehumidifierAccessory.js +68 -0
  26. package/dist/shared/accessories/DiffuserAccessory.js +55 -0
  27. package/dist/shared/accessories/DimmerAccessory.js +94 -0
  28. package/dist/shared/accessories/DoorbellAccessory.js +91 -0
  29. package/dist/shared/accessories/ExtractionHoodAccessory.js +120 -0
  30. package/dist/shared/accessories/FanAccessory.js +129 -0
  31. package/dist/shared/accessories/GarageDoorAccessory.js +69 -0
  32. package/dist/shared/accessories/HeaterAccessory.js +102 -0
  33. package/dist/shared/accessories/HeaterAccessory_old.js +96 -0
  34. package/dist/shared/accessories/HumanPresenceSensorAccessory.js +20 -0
  35. package/dist/shared/accessories/HumidifierAccessory.js +137 -0
  36. package/dist/shared/accessories/IRAirConditionerAccessory.js +278 -0
  37. package/dist/shared/accessories/IRControlHubAccessory.js +49 -0
  38. package/dist/shared/accessories/IRControlHubSubAccessory.js +52 -0
  39. package/dist/shared/accessories/IRGenericAccessory.js +49 -0
  40. package/dist/shared/accessories/LeakSensorAccessory.js +36 -0
  41. package/dist/shared/accessories/LightAccessory.js +36 -0
  42. package/dist/shared/accessories/LightSensorAccessory.js +32 -0
  43. package/dist/shared/accessories/LocationWeatherAccessory.js +72 -0
  44. package/dist/shared/accessories/LockAccessory.js +56 -0
  45. package/dist/shared/accessories/MotionSensorAccessory.js +20 -0
  46. package/dist/shared/accessories/OutletAccessory.js +23 -0
  47. package/dist/shared/accessories/PetFeederAccessory.js +139 -0
  48. package/dist/shared/accessories/SceneAccessory.js +32 -0
  49. package/dist/shared/accessories/SceneSwitchAccessory.js +44 -0
  50. package/dist/shared/accessories/SecuritySystemAccessory.js +30 -0
  51. package/dist/shared/accessories/SmokeSensorAccessory.js +35 -0
  52. package/dist/shared/accessories/SwitchAccessory.js +148 -0
  53. package/dist/shared/accessories/TemperatureHumiditySensorAccessory.js +24 -0
  54. package/dist/shared/accessories/ThermostatAccessory.js +192 -0
  55. package/dist/shared/accessories/TowerRackAccessory.js +157 -0
  56. package/dist/shared/accessories/ValveAccessory.js +45 -0
  57. package/dist/shared/accessories/VibrationSensorAccessory.js +46 -0
  58. package/dist/shared/accessories/WeatherStationAccessory.js +58 -0
  59. package/dist/shared/accessories/WetBulbGlobeTemperatureAccessory.js +23 -0
  60. package/dist/shared/accessories/WhiteNoiseLightAccessory.js +59 -0
  61. package/dist/shared/accessories/WindowAccessory.js +14 -0
  62. package/dist/shared/accessories/WindowCoveringAccessory.js +156 -0
  63. package/dist/shared/accessories/WirelessSwitchAccessory.js +42 -0
  64. package/dist/shared/accessories/characteristic/Active.js +22 -0
  65. package/dist/shared/accessories/characteristic/AirQuality.js +74 -0
  66. package/dist/shared/accessories/characteristic/CurrentRelativeHumidity.js +23 -0
  67. package/dist/shared/accessories/characteristic/CurrentTemperature.js +23 -0
  68. package/dist/shared/accessories/characteristic/CurrentWeather.js +49 -0
  69. package/dist/shared/accessories/characteristic/CurrentWeatherByOpenMeteo.js +49 -0
  70. package/dist/shared/accessories/characteristic/CurrentWetBulbGlobeTemperature.js +48 -0
  71. package/dist/shared/accessories/characteristic/EnergyUsage.js +98 -0
  72. package/dist/shared/accessories/characteristic/Light.js +268 -0
  73. package/dist/shared/accessories/characteristic/LightSensor.js +23 -0
  74. package/dist/shared/accessories/characteristic/LockPhysicalControls.js +21 -0
  75. package/dist/shared/accessories/characteristic/MotionDetected.js +22 -0
  76. package/dist/shared/accessories/characteristic/Name.js +15 -0
  77. package/dist/shared/accessories/characteristic/OccupancyDetected.js +19 -0
  78. package/dist/shared/accessories/characteristic/On.js +25 -0
  79. package/dist/shared/accessories/characteristic/OutletInUse.js +14 -0
  80. package/dist/shared/accessories/characteristic/ProgrammableSwitchEvent.js +89 -0
  81. package/dist/shared/accessories/characteristic/RelativeHumidityDehumidifierThreshold.js +28 -0
  82. package/dist/shared/accessories/characteristic/RotationSpeed.js +78 -0
  83. package/dist/shared/accessories/characteristic/SecuritySystemState.js +74 -0
  84. package/dist/shared/accessories/characteristic/SwingMode.js +21 -0
  85. package/dist/shared/accessories/characteristic/TargetTemperature.js +29 -0
  86. package/dist/shared/accessories/characteristic/TemperatureDisplayUnits.js +25 -0
  87. package/dist/shared/util/ConfigHash.js +79 -0
  88. package/dist/shared/util/FfmpegStreamingProcess.js +126 -0
  89. package/dist/shared/util/InfraredTool.js +392 -0
  90. package/dist/shared/util/Logger.js +42 -0
  91. package/dist/shared/util/TuyaRecordingDelegate.js +22 -0
  92. package/dist/shared/util/TuyaStreamDelegate.js +329 -0
  93. package/dist/shared/util/color.js +23 -0
  94. package/dist/shared/util/util.js +135 -0
  95. package/homebridge-ui/public/index.html +329 -0
  96. package/homebridge-ui/server.js +224 -0
  97. package/package.json +61 -0
@@ -0,0 +1,329 @@
1
+ <style>
2
+ .tuya-nodev-card {
3
+ border: 1px solid rgba(127, 127, 127, 0.25);
4
+ border-radius: 12px;
5
+ padding: 16px;
6
+ margin-bottom: 16px;
7
+ }
8
+ .tuya-nodev-title {
9
+ margin-bottom: 6px;
10
+ font-weight: 700;
11
+ font-size: 1.05rem;
12
+ }
13
+ .tuya-nodev-actions {
14
+ display: flex;
15
+ flex-wrap: wrap;
16
+ gap: 8px;
17
+ margin-top: 12px;
18
+ }
19
+ .tuya-nodev-qr-wrap {
20
+ display: none;
21
+ text-align: center;
22
+ margin: 16px 0;
23
+ }
24
+ .tuya-nodev-qr-wrap img {
25
+ max-width: 300px;
26
+ width: 100%;
27
+ border-radius: 12px;
28
+ background: white;
29
+ padding: 10px;
30
+ }
31
+ .tuya-nodev-small {
32
+ font-size: 0.875rem;
33
+ opacity: 0.85;
34
+ }
35
+ .tuya-nodev-status {
36
+ margin-top: 12px;
37
+ }
38
+ .tuya-nodev-raw {
39
+ display: none;
40
+ word-break: break-all;
41
+ font-family: monospace;
42
+ font-size: 0.8rem;
43
+ margin-top: 8px;
44
+ }
45
+ </style>
46
+
47
+ <div class="tuya-nodev-card">
48
+ <div class="tuya-nodev-title">Tuya QR Cloud Authentication</div>
49
+ <p class="tuya-nodev-small mb-3">
50
+ This plugin connects Tuya / Smart Life devices to Homebridge using the same QR authorization style used by the official Home Assistant Tuya integration. It does not need a Tuya IoT developer account, Access ID, Access Secret, cloud project, username, or password.
51
+ </p>
52
+
53
+ <div class="form-group">
54
+ <label for="tuyaNodevName">Platform name</label>
55
+ <input id="tuyaNodevName" class="form-control" type="text" value="Tuya without developer account">
56
+ </div>
57
+
58
+ <div class="form-group">
59
+ <label for="tuyaNodevUserCode">Tuya User Code</label>
60
+ <input id="tuyaNodevUserCode" class="form-control" type="text" placeholder="Paste the Tuya User Code here" autocomplete="off">
61
+ <small class="form-text text-muted">In the Tuya Smart or Smart Life app, find the User Code under account/security settings, then paste it here.</small>
62
+ </div>
63
+
64
+ <div class="tuya-nodev-actions">
65
+ <button id="tuyaNodevGenerate" class="btn btn-primary" type="button">Generate QR Code</button>
66
+ <button id="tuyaNodevCheck" class="btn btn-outline-secondary" type="button">Check Existing Auth</button>
67
+ <button id="tuyaNodevClear" class="btn btn-outline-danger" type="button">Clear Saved Auth</button>
68
+ <button id="tuyaNodevSave" class="btn btn-success" type="button" disabled>Save Configuration</button>
69
+ </div>
70
+
71
+ <div id="tuyaNodevQrWrap" class="tuya-nodev-qr-wrap">
72
+ <img id="tuyaNodevQr" alt="Tuya QR Code">
73
+ <div class="tuya-nodev-small mt-2">Scan this QR code with the Tuya Smart or Smart Life mobile app.</div>
74
+ <div id="tuyaNodevRaw" class="tuya-nodev-raw"></div>
75
+ </div>
76
+
77
+ <div id="tuyaNodevStatus" class="tuya-nodev-status alert alert-info">
78
+ Enter your User Code, click Generate QR Code, then scan with the Tuya Smart or Smart Life app.
79
+ </div>
80
+ </div>
81
+
82
+ <script>
83
+ (() => {
84
+ const PLATFORM = 'TuyaNoDeveloperAccount';
85
+ let currentConfig = null;
86
+ let pollTimer = null;
87
+ let isAuthenticated = false;
88
+
89
+ const $ = (id) => document.getElementById(id);
90
+
91
+ function setStatus(message, type = 'info') {
92
+ const el = $('tuyaNodevStatus');
93
+ el.className = `tuya-nodev-status alert alert-${type}`;
94
+ el.textContent = message;
95
+ }
96
+
97
+ function getUserCode() {
98
+ return $('tuyaNodevUserCode').value.trim();
99
+ }
100
+
101
+ function getPlatformName() {
102
+ return $('tuyaNodevName').value.trim() || 'Tuya without developer account';
103
+ }
104
+
105
+ function normaliseConfig(base) {
106
+ const cfg = base && typeof base === 'object' ? JSON.parse(JSON.stringify(base)) : {};
107
+ cfg.platform = PLATFORM;
108
+ cfg.name = getPlatformName();
109
+ cfg.options = cfg.options && typeof cfg.options === 'object' ? cfg.options : {};
110
+ cfg.options.userCode = getUserCode();
111
+ cfg.options.projectType = '3';
112
+ delete cfg.options.accessId;
113
+ delete cfg.options.accessKey;
114
+ delete cfg.options.username;
115
+ delete cfg.options.password;
116
+ delete cfg.options.countryCode;
117
+ delete cfg.options.endpoint;
118
+ delete cfg.local;
119
+ cfg.mode = 'cloud';
120
+ return cfg;
121
+ }
122
+
123
+ async function syncConfigToUi() {
124
+ currentConfig = normaliseConfig(currentConfig);
125
+ await homebridge.updatePluginConfig([currentConfig]);
126
+ }
127
+
128
+ function stopPolling() {
129
+ if (pollTimer) {
130
+ clearInterval(pollTimer);
131
+ pollTimer = null;
132
+ }
133
+ }
134
+
135
+ function disableSaving() {
136
+ $('tuyaNodevSave').disabled = true;
137
+ if (homebridge.disableSaveButton) homebridge.disableSaveButton();
138
+ }
139
+
140
+ function enableSaving() {
141
+ $('tuyaNodevSave').disabled = false;
142
+ if (homebridge.enableSaveButton) homebridge.enableSaveButton();
143
+ }
144
+
145
+ async function checkAuth(showSuccessToast = false) {
146
+ const userCode = getUserCode();
147
+ if (!userCode) {
148
+ setStatus('User Code is required.', 'warning');
149
+ return false;
150
+ }
151
+ try {
152
+ const res = await homebridge.request('/auth/status', { userCode });
153
+ isAuthenticated = !!res.authenticated;
154
+ if (isAuthenticated) {
155
+ enableSaving();
156
+ const who = res.username || res.uid || 'Tuya user';
157
+ setStatus(`Authenticated as ${who}. You can save the configuration.`, 'success');
158
+ if (showSuccessToast) homebridge.toast.success('Tuya QR authentication is already saved.', 'Tuya');
159
+ return true;
160
+ }
161
+ disableSaving();
162
+ setStatus('No saved Tuya QR authentication found for this User Code. Generate and scan a QR code before saving.', 'warning');
163
+ return false;
164
+ } catch (e) {
165
+ isAuthenticated = false;
166
+ disableSaving();
167
+ setStatus(e.message || 'Could not check authentication.', 'danger');
168
+ return false;
169
+ }
170
+ }
171
+
172
+ async function pollStatus() {
173
+ const userCode = getUserCode();
174
+ try {
175
+ const res = await homebridge.request('/qr/status', { userCode });
176
+ if (res.authenticated) {
177
+ stopPolling();
178
+ isAuthenticated = true;
179
+ enableSaving();
180
+ const who = res.username || res.uid || 'Tuya user';
181
+ setStatus(`QR approved. Auth token was saved for ${who}. Now click Save Configuration.`, 'success');
182
+ homebridge.toast.success('QR scan approved. Click Save Configuration.', 'Tuya');
183
+ await syncConfigToUi();
184
+ return;
185
+ }
186
+ if (res.expired) {
187
+ stopPolling();
188
+ isAuthenticated = false;
189
+ disableSaving();
190
+ setStatus('QR code expired. Generate a new QR code.', 'warning');
191
+ return;
192
+ }
193
+ setStatus(res.message || 'Waiting for Tuya app scan / approval...', 'info');
194
+ } catch (e) {
195
+ setStatus(e.message || 'Waiting for QR approval...', 'warning');
196
+ }
197
+ }
198
+
199
+ async function generateQr() {
200
+ const userCode = getUserCode();
201
+ if (!userCode) {
202
+ setStatus('User Code is required.', 'warning');
203
+ return;
204
+ }
205
+ try {
206
+ stopPolling();
207
+ homebridge.showSpinner();
208
+ isAuthenticated = false;
209
+ disableSaving();
210
+ await syncConfigToUi();
211
+ const res = await homebridge.request('/qr/start', {
212
+ userCode,
213
+ force: true,
214
+ });
215
+ if (res.alreadyAuthenticated || res.authenticated) {
216
+ isAuthenticated = true;
217
+ enableSaving();
218
+ setStatus('Existing Tuya QR authentication found. You can save the configuration.', 'success');
219
+ return;
220
+ }
221
+ $('tuyaNodevQr').src = res.qrDataUrl;
222
+ $('tuyaNodevQrWrap').style.display = 'block';
223
+ $('tuyaNodevRaw').style.display = 'block';
224
+ $('tuyaNodevRaw').textContent = res.qrPayload;
225
+ setStatus('QR code generated. Scan it with the Tuya app. Waiting for approval...', 'info');
226
+ pollTimer = setInterval(pollStatus, 3000);
227
+ setTimeout(pollStatus, 1000);
228
+ } catch (e) {
229
+ stopPolling();
230
+ setStatus(e.message || 'Failed to generate QR code.', 'danger');
231
+ homebridge.toast.error(e.message || 'Failed to generate QR code.', 'Tuya');
232
+ } finally {
233
+ homebridge.hideSpinner();
234
+ }
235
+ }
236
+
237
+ async function clearAuth() {
238
+ const userCode = getUserCode();
239
+ if (!userCode) {
240
+ setStatus('User Code is required.', 'warning');
241
+ return;
242
+ }
243
+ try {
244
+ stopPolling();
245
+ await homebridge.request('/auth/clear', { userCode });
246
+ isAuthenticated = false;
247
+ disableSaving();
248
+ setStatus('Saved Tuya QR auth was cleared. Generate and scan a new QR code.', 'warning');
249
+ homebridge.toast.success('Saved auth cleared.', 'Tuya');
250
+ } catch (e) {
251
+ setStatus(e.message || 'Failed to clear auth.', 'danger');
252
+ }
253
+ }
254
+
255
+ async function saveConfig() {
256
+ if (!isAuthenticated) {
257
+ setStatus('Scan and approve the QR code before saving.', 'warning');
258
+ return;
259
+ }
260
+ try {
261
+ homebridge.showSpinner();
262
+ await syncConfigToUi();
263
+ await homebridge.savePluginConfig();
264
+ homebridge.toast.success('Configuration saved. Restart Homebridge to load devices.', 'Tuya');
265
+ setStatus('Configuration saved. Restart Homebridge to load devices.', 'success');
266
+ } catch (e) {
267
+ setStatus(e.message || 'Failed to save configuration.', 'danger');
268
+ homebridge.toast.error(e.message || 'Failed to save configuration.', 'Tuya');
269
+ } finally {
270
+ homebridge.hideSpinner();
271
+ }
272
+ }
273
+
274
+ async function init() {
275
+ try {
276
+ const blocks = await homebridge.getPluginConfig();
277
+ currentConfig = Array.isArray(blocks) && blocks.length > 0 ? blocks[0] : {
278
+ platform: PLATFORM,
279
+ name: 'Tuya without developer account',
280
+ mode: 'cloud',
281
+ options: { projectType: '3' },
282
+ };
283
+ currentConfig.platform = PLATFORM;
284
+ currentConfig.mode = 'cloud';
285
+ currentConfig.options = currentConfig.options || {};
286
+ currentConfig.options.projectType = '3';
287
+ $('tuyaNodevName').value = currentConfig.name || 'Tuya without developer account';
288
+ $('tuyaNodevUserCode').value = currentConfig.options?.userCode || '';
289
+
290
+ if (homebridge.showSchemaForm) homebridge.showSchemaForm();
291
+ window.homebridge.addEventListener('configChanged', (event) => {
292
+ const data = event.data;
293
+ const block = Array.isArray(data) ? data[0] : data;
294
+ if (block && typeof block === 'object') {
295
+ currentConfig = block;
296
+ currentConfig.platform = PLATFORM;
297
+ currentConfig.mode = 'cloud';
298
+ currentConfig.options = currentConfig.options || {};
299
+ currentConfig.options.projectType = '3';
300
+ if (block.name) $('tuyaNodevName').value = block.name;
301
+ if (block.options?.userCode) $('tuyaNodevUserCode').value = block.options.userCode;
302
+ }
303
+ });
304
+
305
+ $('tuyaNodevGenerate').addEventListener('click', generateQr);
306
+ $('tuyaNodevCheck').addEventListener('click', () => checkAuth(true));
307
+ $('tuyaNodevClear').addEventListener('click', clearAuth);
308
+ $('tuyaNodevSave').addEventListener('click', saveConfig);
309
+ $('tuyaNodevName').addEventListener('input', syncConfigToUi);
310
+ $('tuyaNodevUserCode').addEventListener('input', () => {
311
+ isAuthenticated = false;
312
+ disableSaving();
313
+ syncConfigToUi();
314
+ });
315
+
316
+ await syncConfigToUi();
317
+ if (getUserCode()) {
318
+ await checkAuth(false);
319
+ } else {
320
+ disableSaving();
321
+ }
322
+ } catch (e) {
323
+ setStatus(e.message || 'Failed to initialise Tuya QR UI.', 'danger');
324
+ }
325
+ }
326
+
327
+ window.homebridge.addEventListener('ready', init);
328
+ })();
329
+ </script>
@@ -0,0 +1,224 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const qrcode = require('qrcode');
6
+ const { default: TuyaHACloudAPI } = require('../dist/cloud/api/TuyaHACloudAPI');
7
+
8
+ function safeUserCode(userCode) {
9
+ return String(userCode || '').replace(/[^a-zA-Z0-9_.-]/g, '_');
10
+ }
11
+
12
+ function normaliseUserCode(userCode) {
13
+ return String(userCode || '').trim();
14
+ }
15
+
16
+ (async () => {
17
+ const { HomebridgePluginUiServer, RequestError } = await import('@homebridge/plugin-ui-utils');
18
+
19
+ class TuyaNoDeveloperAccountUiServer extends HomebridgePluginUiServer {
20
+ constructor() {
21
+ super();
22
+ this.sessions = new Map();
23
+ this.onRequest('/qr/start', this.startQr.bind(this));
24
+ this.onRequest('/qr/status', this.qrStatus.bind(this));
25
+ this.onRequest('/auth/status', this.authStatus.bind(this));
26
+ this.onRequest('/auth/clear', this.clearAuth.bind(this));
27
+ this.ready();
28
+ }
29
+
30
+ getAuthFile(userCode) {
31
+ return path.join(this.homebridgeStoragePath, `tuya-ha-qr-auth.${safeUserCode(userCode)}.json`);
32
+ }
33
+
34
+ async readAuthFile(userCode) {
35
+ try {
36
+ const raw = await fs.promises.readFile(this.getAuthFile(userCode), 'utf8');
37
+ const data = JSON.parse(raw);
38
+ if (!data.userCode || !data.endpoint || !data.terminalId || !data.tokenInfo?.access_token || !data.tokenInfo?.refresh_token) {
39
+ return null;
40
+ }
41
+ return data;
42
+ } catch {
43
+ return null;
44
+ }
45
+ }
46
+
47
+ async writeAuthFile(userCode, data) {
48
+ const file = this.getAuthFile(userCode);
49
+ await fs.promises.mkdir(path.dirname(file), { recursive: true });
50
+ await fs.promises.writeFile(file, JSON.stringify(data, null, 2), { mode: 0o600 });
51
+ return file;
52
+ }
53
+
54
+ async authStatus(payload = {}) {
55
+ const userCode = normaliseUserCode(payload.userCode);
56
+ if (!userCode) {
57
+ throw new RequestError('User Code is required.', { status: 400 });
58
+ }
59
+ const data = await this.readAuthFile(userCode);
60
+ return {
61
+ authenticated: !!data,
62
+ file: this.getAuthFile(userCode),
63
+ username: data?.username || null,
64
+ uid: data?.tokenInfo?.uid || null,
65
+ endpoint: data?.endpoint || null,
66
+ savedAt: data?.savedAt || null,
67
+ };
68
+ }
69
+
70
+ async clearAuth(payload = {}) {
71
+ const userCode = normaliseUserCode(payload.userCode);
72
+ if (!userCode) {
73
+ throw new RequestError('User Code is required.', { status: 400 });
74
+ }
75
+ const file = this.getAuthFile(userCode);
76
+ try {
77
+ await fs.promises.unlink(file);
78
+ } catch (err) {
79
+ if (!err || err.code !== 'ENOENT') {
80
+ throw err;
81
+ }
82
+ }
83
+ this.sessions.delete(userCode);
84
+ return { cleared: true, file };
85
+ }
86
+
87
+ async startQr(payload = {}) {
88
+ const userCode = normaliseUserCode(payload.userCode);
89
+ if (!userCode) {
90
+ throw new RequestError('User Code is required.', { status: 400 });
91
+ }
92
+
93
+ const existing = await this.readAuthFile(userCode);
94
+ if (existing && !payload.force) {
95
+ return {
96
+ alreadyAuthenticated: true,
97
+ authenticated: true,
98
+ file: this.getAuthFile(userCode),
99
+ username: existing.username || null,
100
+ uid: existing.tokenInfo?.uid || null,
101
+ endpoint: existing.endpoint || null,
102
+ };
103
+ }
104
+
105
+ const api = new TuyaHACloudAPI(userCode, '', 'https://apigw.iotbing.com', undefined, console, !!payload.debug);
106
+ const response = await api.getQRCodeToken();
107
+ if (!response || !response.success) {
108
+ throw new RequestError(`Failed to create Tuya QR token: ${response?.code || ''} ${response?.msg || 'Unknown error'}`.trim(), {
109
+ status: 502,
110
+ response,
111
+ });
112
+ }
113
+
114
+ const token = response.result?.qrcode;
115
+ if (!token) {
116
+ throw new RequestError('Tuya QR token response did not include result.qrcode.', { status: 502, response });
117
+ }
118
+
119
+ const qrPayload = `tuyaSmart--qrLogin?token=${token}`;
120
+ const qrDataUrl = await qrcode.toDataURL(qrPayload, {
121
+ errorCorrectionLevel: 'M',
122
+ margin: 2,
123
+ width: 300,
124
+ });
125
+
126
+ this.sessions.set(userCode, {
127
+ userCode,
128
+ token,
129
+ qrPayload,
130
+ createdAt: Date.now(),
131
+ });
132
+
133
+ return {
134
+ authenticated: false,
135
+ alreadyAuthenticated: false,
136
+ token,
137
+ qrPayload,
138
+ qrDataUrl,
139
+ expiresInSeconds: 180,
140
+ };
141
+ }
142
+
143
+ async qrStatus(payload = {}) {
144
+ const userCode = normaliseUserCode(payload.userCode);
145
+ if (!userCode) {
146
+ throw new RequestError('User Code is required.', { status: 400 });
147
+ }
148
+ const session = this.sessions.get(userCode);
149
+ if (!session) {
150
+ const existing = await this.readAuthFile(userCode);
151
+ if (existing) {
152
+ return {
153
+ authenticated: true,
154
+ pending: false,
155
+ file: this.getAuthFile(userCode),
156
+ username: existing.username || null,
157
+ uid: existing.tokenInfo?.uid || null,
158
+ endpoint: existing.endpoint || null,
159
+ };
160
+ }
161
+ throw new RequestError('No active QR session. Generate a QR code first.', { status: 404 });
162
+ }
163
+
164
+ if (Date.now() - session.createdAt > 3 * 60 * 1000) {
165
+ this.sessions.delete(userCode);
166
+ return {
167
+ authenticated: false,
168
+ pending: false,
169
+ expired: true,
170
+ message: 'QR code expired. Generate a new QR code.',
171
+ };
172
+ }
173
+
174
+ const api = new TuyaHACloudAPI(userCode, '', 'https://apigw.iotbing.com', undefined, console, !!payload.debug);
175
+ const loginResponse = await api.getQRCodeLoginResult(session.token);
176
+
177
+ if (loginResponse && loginResponse.success) {
178
+ const info = loginResponse.result || {};
179
+ const authData = {
180
+ userCode,
181
+ terminalId: info.terminal_id,
182
+ endpoint: info.endpoint,
183
+ tokenInfo: {
184
+ t: loginResponse.t || info.t || Date.now(),
185
+ uid: info.uid,
186
+ expire_time: info.expire_time,
187
+ access_token: info.access_token,
188
+ refresh_token: info.refresh_token,
189
+ },
190
+ username: info.username,
191
+ savedAt: Date.now(),
192
+ };
193
+
194
+ if (!authData.terminalId || !authData.endpoint || !authData.tokenInfo.access_token || !authData.tokenInfo.refresh_token) {
195
+ throw new RequestError('Tuya login succeeded but the response was incomplete.', {
196
+ status: 502,
197
+ response: loginResponse,
198
+ });
199
+ }
200
+
201
+ const file = await this.writeAuthFile(userCode, authData);
202
+ this.sessions.delete(userCode);
203
+ return {
204
+ authenticated: true,
205
+ pending: false,
206
+ file,
207
+ username: authData.username || null,
208
+ uid: authData.tokenInfo.uid || null,
209
+ endpoint: authData.endpoint || null,
210
+ };
211
+ }
212
+
213
+ const message = `${loginResponse?.code || ''} ${loginResponse?.msg || 'Waiting for scan / approval'}`.trim();
214
+ return {
215
+ authenticated: false,
216
+ pending: true,
217
+ code: loginResponse?.code || null,
218
+ message,
219
+ };
220
+ }
221
+ }
222
+
223
+ return new TuyaNoDeveloperAccountUiServer();
224
+ })();
package/package.json ADDED
@@ -0,0 +1,61 @@
1
+ {
2
+ "name": "homebridge-tuya-without-developer-account",
3
+ "displayName": "Tuya without developer account for Homebridge",
4
+ "version": "1.0.0",
5
+ "description": "Homebridge plugin for Tuya and Smart Life devices using Home Assistant-style QR cloud authentication, without a Tuya IoT developer account.",
6
+ "license": "MIT",
7
+ "author": "Kosztyk",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "https://github.com/kosztyk/homebridge-tuya-without-developer-account.git"
11
+ },
12
+ "homepage": "https://github.com/kosztyk/homebridge-tuya-without-developer-account#readme",
13
+ "bugs": {
14
+ "url": "https://github.com/kosztyk/homebridge-tuya-without-developer-account/issues"
15
+ },
16
+ "engines": {
17
+ "homebridge": "^1.8.0 || ^2.0.0-beta.0",
18
+ "node": "^20 || ^22 || ^24 || ^25"
19
+ },
20
+ "peerDependencies": {
21
+ "homebridge": "^1.8.0 || ^2.0.0-beta.0"
22
+ },
23
+ "main": "dist/index.js",
24
+ "scripts": {
25
+ "pack:check": "npm pack --dry-run",
26
+ "publish:public": "npm publish --access public"
27
+ },
28
+ "keywords": [
29
+ "homebridge-plugin",
30
+ "homebridge",
31
+ "homekit",
32
+ "tuya",
33
+ "smartlife",
34
+ "smart-life",
35
+ "qr-auth",
36
+ "no-developer-account",
37
+ "home-assistant-style-auth"
38
+ ],
39
+ "dependencies": {
40
+ "@homebridge/camera-utils": "^3.0.0",
41
+ "@homebridge/plugin-ui-utils": "^2.2.3",
42
+ "color-convert": "^3.1.3",
43
+ "kelvin-to-rgb": "^1.0.2",
44
+ "mqtt": "^5.15.1",
45
+ "qrcode": "^1.5.4"
46
+ },
47
+ "files": [
48
+ "dist/**/*",
49
+ "homebridge-ui/**/*",
50
+ "config.schema.json",
51
+ "README.md",
52
+ "SUPPORTED_DEVICES.md",
53
+ "CHANGELOG.md",
54
+ "PUBLISHING.md",
55
+ "LICENSE"
56
+ ],
57
+ "homebridge": {
58
+ "plugin_type": "platform",
59
+ "platform": "TuyaNoDeveloperAccount"
60
+ }
61
+ }