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,131 @@
1
+ {
2
+ "pluginAlias": "TuyaNoDeveloperAccount",
3
+ "pluginType": "platform",
4
+ "singular": true,
5
+ "headerDisplay": "<p align='center'><strong>Tuya without developer account for Homebridge</strong></p><p>This plugin uses Home Assistant-style Tuya QR Cloud Authentication. It does not ask for Tuya IoT Access ID, Access Secret, cloud project, username, or password. Open the plugin settings, enter your Tuya User Code, generate the QR code, scan it with the Tuya Smart or Smart Life app, then save.</p>",
6
+ "footerDisplay": "<p><strong>Note:</strong> If authentication is cleared or expires, generate a new QR code from this settings screen.</p>",
7
+ "customUi": true,
8
+ "schema": {
9
+ "type": "object",
10
+ "required": [
11
+ "options"
12
+ ],
13
+ "properties": {
14
+ "name": {
15
+ "type": "string",
16
+ "title": "Name",
17
+ "default": "Tuya without developer account"
18
+ },
19
+ "options": {
20
+ "type": "object",
21
+ "title": "QR Cloud Authentication",
22
+ "required": [
23
+ "userCode"
24
+ ],
25
+ "properties": {
26
+ "userCode": {
27
+ "type": "string",
28
+ "title": "Tuya User Code",
29
+ "description": "The User Code from the Tuya Smart or Smart Life mobile app. This is the only required cloud authentication value."
30
+ },
31
+ "homeWhitelist": {
32
+ "type": "array",
33
+ "title": "Home Whitelist",
34
+ "description": "Optional. Restrict discovery to specific Tuya home IDs after authentication.",
35
+ "items": {
36
+ "type": "string"
37
+ }
38
+ },
39
+ "deviceOverrides": {
40
+ "type": "array",
41
+ "title": "Device Overrides",
42
+ "description": "Optional advanced overrides. Use this only if a discovered device needs a custom category, schema mapping, or to be hidden/unbridged.",
43
+ "items": {
44
+ "type": "object",
45
+ "properties": {
46
+ "id": {
47
+ "type": "string",
48
+ "title": "Device ID / Product ID / global"
49
+ },
50
+ "category": {
51
+ "type": "string",
52
+ "title": "Category Override"
53
+ },
54
+ "unbridged": {
55
+ "type": "boolean",
56
+ "title": "Expose as external accessory",
57
+ "default": false
58
+ },
59
+ "adaptiveLighting": {
60
+ "type": "boolean",
61
+ "title": "Enable Adaptive Lighting",
62
+ "default": false
63
+ },
64
+ "schema": {
65
+ "type": "array",
66
+ "title": "Schema Overrides",
67
+ "items": {
68
+ "type": "object",
69
+ "properties": {
70
+ "code": {
71
+ "type": "string"
72
+ },
73
+ "newCode": {
74
+ "type": "string"
75
+ },
76
+ "type": {
77
+ "type": "string"
78
+ },
79
+ "property": {
80
+ "type": "object"
81
+ }
82
+ }
83
+ }
84
+ }
85
+ },
86
+ "required": [
87
+ "id"
88
+ ]
89
+ }
90
+ },
91
+ "debug": {
92
+ "type": "boolean",
93
+ "title": "Enable debug logging",
94
+ "default": false
95
+ },
96
+ "debugLevel": {
97
+ "type": "array",
98
+ "title": "Debug level",
99
+ "items": {
100
+ "type": "string",
101
+ "enum": [
102
+ "api",
103
+ "mqtt"
104
+ ]
105
+ }
106
+ }
107
+ }
108
+ }
109
+ }
110
+ },
111
+ "layout": [
112
+ {
113
+ "key": "name"
114
+ },
115
+ {
116
+ "key": "options.userCode"
117
+ },
118
+ {
119
+ "type": "fieldset",
120
+ "title": "Advanced",
121
+ "expandable": true,
122
+ "expanded": false,
123
+ "items": [
124
+ "options.homeWhitelist",
125
+ "options.deviceOverrides",
126
+ "options.debug",
127
+ "options.debugLevel"
128
+ ]
129
+ }
130
+ ]
131
+ }
@@ -0,0 +1,286 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.TUYA_HA_QR_ENDPOINT = exports.TUYA_HA_SCHEMA = exports.TUYA_HA_CLIENT_ID = void 0;
7
+ /* eslint-disable max-len */
8
+ const https_1 = __importDefault(require("https"));
9
+ const crypto_1 = __importDefault(require("crypto"));
10
+ const util_1 = require("../../shared/util/util");
11
+ const Logger_1 = require("../../shared/util/Logger");
12
+ exports.TUYA_HA_CLIENT_ID = 'HA_3y9q4ak7g4ephrvke';
13
+ exports.TUYA_HA_SCHEMA = 'haauthorize';
14
+ exports.TUYA_HA_QR_ENDPOINT = 'https://apigw.iotbing.com';
15
+ class TuyaHACloudAPI {
16
+ constructor(userCode, terminalId, endpoint, tokenInfo, log = console, debug = false) {
17
+ this.userCode = userCode;
18
+ this.terminalId = terminalId;
19
+ this.endpoint = endpoint;
20
+ this.log = log;
21
+ this.debug = debug;
22
+ this.tokenInfo = {
23
+ access_token: '',
24
+ refresh_token: '',
25
+ uid: '',
26
+ expire: 0,
27
+ };
28
+ this.refreshTokenInProgress = false;
29
+ this.log = new Logger_1.PrefixLogger(log, TuyaHACloudAPI.name, debug);
30
+ if (tokenInfo) {
31
+ this.setTokenInfo(tokenInfo);
32
+ }
33
+ }
34
+ setTokenInfo(tokenInfo) {
35
+ this.tokenInfo = {
36
+ access_token: tokenInfo.access_token,
37
+ refresh_token: tokenInfo.refresh_token,
38
+ uid: tokenInfo.uid,
39
+ expire: (tokenInfo.t || Date.now()) + (tokenInfo.expire_time || 0) * 1000,
40
+ };
41
+ }
42
+ exportTokenInfo() {
43
+ return {
44
+ t: Date.now(),
45
+ uid: this.tokenInfo.uid,
46
+ expire_time: Math.max(0, Math.floor((this.tokenInfo.expire - Date.now()) / 1000)),
47
+ access_token: this.tokenInfo.access_token,
48
+ refresh_token: this.tokenInfo.refresh_token,
49
+ };
50
+ }
51
+ isLogin() {
52
+ return this.tokenInfo.access_token.length > 0;
53
+ }
54
+ isTokenExpired() {
55
+ return (this.tokenInfo.expire - 60 * 1000 <= Date.now());
56
+ }
57
+ async getQRCodeToken() {
58
+ const path = `/v1.0/m/life/home-assistant/qrcode/tokens?clientid=${encodeURIComponent(exports.TUYA_HA_CLIENT_ID)}&usercode=${encodeURIComponent(this.userCode)}&schema=${encodeURIComponent(exports.TUYA_HA_SCHEMA)}`;
59
+ return this.rawRequest('POST', exports.TUYA_HA_QR_ENDPOINT, path);
60
+ }
61
+ async getQRCodeLoginResult(token) {
62
+ const path = `/v1.0/m/life/home-assistant/qrcode/tokens/${encodeURIComponent(token)}?clientid=${encodeURIComponent(exports.TUYA_HA_CLIENT_ID)}&usercode=${encodeURIComponent(this.userCode)}`;
63
+ return this.rawRequest('GET', exports.TUYA_HA_QR_ENDPOINT, path);
64
+ }
65
+ async rawRequest(method, endpoint, path, body) {
66
+ this.log.debug('HA raw request: %s %s%s', method, endpoint, path);
67
+ const res = await (0, util_1.retry)(async () => new Promise((resolve, reject) => {
68
+ const req = https_1.default.request({
69
+ host: new URL(endpoint).host,
70
+ method,
71
+ path,
72
+ headers: body ? { 'Content-Type': 'application/json' } : undefined,
73
+ }, response => {
74
+ response.setEncoding('utf8');
75
+ let rawData = '';
76
+ response.on('data', chunk => rawData += chunk);
77
+ response.on('end', () => {
78
+ try {
79
+ const parsed = rawData ? JSON.parse(rawData) : {};
80
+ resolve(parsed);
81
+ }
82
+ catch (error) {
83
+ reject(error);
84
+ }
85
+ });
86
+ });
87
+ req.on('error', reject);
88
+ if (body) {
89
+ req.write(JSON.stringify(body));
90
+ }
91
+ req.end();
92
+ }), { retriesMax: 3, interval: 500, exponential: true, factor: 2, jitter: 100 });
93
+ this.log.debug('HA raw response: %s', JSON.stringify(res));
94
+ return res;
95
+ }
96
+ async refreshAccessTokenIfNeed() {
97
+ if (!this.isLogin()) {
98
+ return;
99
+ }
100
+ if (!this.isTokenExpired()) {
101
+ return;
102
+ }
103
+ if (this.refreshTokenInProgress) {
104
+ return;
105
+ }
106
+ this.refreshTokenInProgress = true;
107
+ try {
108
+ this.log.info('Refreshing Tuya Home Assistant QR access token.');
109
+ const response = await this.get(`/v1.0/m/token/${this.tokenInfo.refresh_token}`);
110
+ if (response && response.success) {
111
+ const result = response.result || {};
112
+ const tokenInfo = {
113
+ t: response.t || Date.now(),
114
+ expire_time: result.expireTime || result.expire_time || 0,
115
+ uid: result.uid,
116
+ access_token: result.accessToken || result.access_token,
117
+ refresh_token: result.refreshToken || result.refresh_token,
118
+ };
119
+ this.setTokenInfo(tokenInfo);
120
+ }
121
+ }
122
+ catch (error) {
123
+ const msg = error instanceof Error ? error.message : String(error);
124
+ this.log.error('Failed to refresh Tuya access token: %s', msg);
125
+ }
126
+ finally {
127
+ this.refreshTokenInProgress = false;
128
+ }
129
+ }
130
+ async request(method, path, params, body) {
131
+ if (!this.refreshTokenInProgress) {
132
+ await this.refreshAccessTokenIfNeed();
133
+ }
134
+ const rid = (0, util_1.generateUUID)();
135
+ const sid = '';
136
+ const hashKey = crypto_1.default.createHash('md5').update(rid + this.tokenInfo.refresh_token).digest('hex');
137
+ const secret = this.secretGenerating(rid, sid, hashKey);
138
+ let queryEncData = '';
139
+ let finalParams;
140
+ if (params && Object.keys(params).length > 0) {
141
+ queryEncData = this.aesGcmEncrypt(this.formToJson(params), secret);
142
+ finalParams = { encdata: queryEncData };
143
+ }
144
+ let bodyEncData = '';
145
+ let finalBody;
146
+ if (body && Object.keys(body).length > 0) {
147
+ bodyEncData = this.aesGcmEncrypt(this.formToJson(body), secret);
148
+ finalBody = { encdata: bodyEncData };
149
+ }
150
+ const t = Date.now();
151
+ const headers = {
152
+ 'X-appKey': exports.TUYA_HA_CLIENT_ID,
153
+ 'X-requestId': rid,
154
+ 'X-sid': sid,
155
+ 'X-time': String(t),
156
+ };
157
+ if (this.tokenInfo.access_token) {
158
+ headers['X-token'] = this.tokenInfo.access_token;
159
+ }
160
+ headers['X-sign'] = this.restfulSign(hashKey, queryEncData, bodyEncData, headers);
161
+ let requestPath = path;
162
+ if (finalParams) {
163
+ requestPath += '?' + new URLSearchParams(finalParams).toString();
164
+ }
165
+ this.log.debug('HA encrypted request:\nmethod = %s\nendpoint = %s\npath = %s\nbody = %s', method, this.endpoint, requestPath, JSON.stringify(finalBody));
166
+ const res = await (0, util_1.retry)(async () => new Promise((resolve, reject) => {
167
+ const req = https_1.default.request({
168
+ host: new URL(this.endpoint).host,
169
+ method,
170
+ path: requestPath,
171
+ headers,
172
+ }, response => {
173
+ response.setEncoding('utf8');
174
+ let rawData = '';
175
+ response.on('data', chunk => rawData += chunk);
176
+ response.on('end', () => {
177
+ try {
178
+ const parsed = rawData ? JSON.parse(rawData) : {};
179
+ if (parsed && parsed.success && parsed.result && typeof parsed.result === 'string') {
180
+ const decrypted = this.aesGcmDecrypt(parsed.result, secret);
181
+ try {
182
+ parsed.result = JSON.parse(decrypted);
183
+ }
184
+ catch {
185
+ parsed.result = decrypted;
186
+ }
187
+ }
188
+ resolve(parsed);
189
+ }
190
+ catch (error) {
191
+ reject(error);
192
+ }
193
+ });
194
+ });
195
+ req.on('error', reject);
196
+ if (finalBody) {
197
+ req.write(JSON.stringify(finalBody));
198
+ }
199
+ req.end();
200
+ }), { retriesMax: 5, interval: 300, exponential: true, factor: 2, jitter: 100 });
201
+ this.log.debug('HA encrypted response: %s', JSON.stringify(res));
202
+ return res;
203
+ }
204
+ async get(path, params) {
205
+ return this.request('GET', path, params || null, null);
206
+ }
207
+ async post(path, params, body) {
208
+ // Keep compatibility with the old plugin style where post(path, body) is common.
209
+ if (body === undefined) {
210
+ body = params || null;
211
+ params = null;
212
+ }
213
+ return this.request('POST', path, params || null, body || null);
214
+ }
215
+ async put(path, body) {
216
+ return this.request('PUT', path, null, body || null);
217
+ }
218
+ async delete(path, params) {
219
+ return this.request('DELETE', path, params || null, null);
220
+ }
221
+ async getDeviceDetails(deviceId) {
222
+ return this.get('/v1.0/m/life/ha/devices/detail', { devIds: deviceId });
223
+ }
224
+ formToJson(content) {
225
+ return JSON.stringify(content);
226
+ }
227
+ randomNonce(length = 12) {
228
+ const chars = 'ABCDEFGHJKMNPQRSTWXYZabcdefhijkmnprstwxyz2345678';
229
+ let value = '';
230
+ for (let i = 0; i < length; i++) {
231
+ value += chars[Math.floor(Math.random() * chars.length)];
232
+ }
233
+ return value;
234
+ }
235
+ aesGcmEncrypt(rawData, secret) {
236
+ const nonce = this.randomNonce(12);
237
+ const cipher = crypto_1.default.createCipheriv('aes-128-gcm', Buffer.from(secret, 'utf8'), Buffer.from(nonce, 'utf8'));
238
+ const encrypted = Buffer.concat([cipher.update(rawData, 'utf8'), cipher.final(), cipher.getAuthTag()]);
239
+ return Buffer.from(nonce, 'utf8').toString('base64') + encrypted.toString('base64');
240
+ }
241
+ aesGcmDecrypt(cipherData, secret) {
242
+ const data = Buffer.from(cipherData, 'base64');
243
+ const nonce = data.subarray(0, 12);
244
+ const cipherTextWithTag = data.subarray(12);
245
+ const authTag = cipherTextWithTag.subarray(cipherTextWithTag.length - 16);
246
+ const cipherText = cipherTextWithTag.subarray(0, cipherTextWithTag.length - 16);
247
+ const decipher = crypto_1.default.createDecipheriv('aes-128-gcm', Buffer.from(secret, 'utf8'), nonce);
248
+ decipher.setAuthTag(authTag);
249
+ return Buffer.concat([decipher.update(cipherText), decipher.final()]).toString('utf8');
250
+ }
251
+ secretGenerating(rid, sid, hashKey) {
252
+ let message = hashKey;
253
+ const mod = 16;
254
+ if (sid !== '') {
255
+ const sidLength = sid.length;
256
+ const length = sidLength < mod ? sidLength : mod;
257
+ let ecode = '';
258
+ for (let i = 0; i < length; i++) {
259
+ const idx = sid.charCodeAt(i) % mod;
260
+ ecode += sid[idx];
261
+ }
262
+ message += '_' + ecode;
263
+ }
264
+ return crypto_1.default.createHmac('sha256', Buffer.from(rid, 'utf8')).update(Buffer.from(message, 'utf8')).digest('hex').slice(0, 16);
265
+ }
266
+ restfulSign(hashKey, queryEncData, bodyEncData, data) {
267
+ const headers = ['X-appKey', 'X-requestId', 'X-sid', 'X-time', 'X-token'];
268
+ const headerParts = [];
269
+ for (const item of headers) {
270
+ const val = data[item] || '';
271
+ if (val !== '') {
272
+ headerParts.push(`${item}=${val}`);
273
+ }
274
+ }
275
+ let signStr = headerParts.join('||');
276
+ if (queryEncData) {
277
+ signStr += queryEncData;
278
+ }
279
+ if (bodyEncData) {
280
+ signStr += bodyEncData;
281
+ }
282
+ return crypto_1.default.createHmac('sha256', Buffer.from(hashKey, 'utf8')).update(Buffer.from(signStr, 'utf8')).digest('hex');
283
+ }
284
+ }
285
+ exports.default = TuyaHACloudAPI;
286
+ //# sourceMappingURL=TuyaHACloudAPI.js.map
@@ -0,0 +1,114 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ /* eslint-disable max-len */
7
+ const mqtt_1 = __importDefault(require("mqtt"));
8
+ const url_1 = require("url");
9
+ const util_1 = require("../../shared/util/util");
10
+ const Logger_1 = require("../../shared/util/Logger");
11
+ class TuyaHASharingMQ {
12
+ constructor(api, ownerIds, devices, log = console, debug = false) {
13
+ this.api = api;
14
+ this.ownerIds = ownerIds;
15
+ this.devices = devices;
16
+ this.log = log;
17
+ this.debug = debug;
18
+ this.messageListeners = new Set();
19
+ this.log = new Logger_1.PrefixLogger(log, TuyaHASharingMQ.name, debug);
20
+ }
21
+ async start() {
22
+ await this.connect();
23
+ }
24
+ stop() {
25
+ if (this.reconnectTimer) {
26
+ clearTimeout(this.reconnectTimer);
27
+ this.reconnectTimer = undefined;
28
+ }
29
+ if (this.client) {
30
+ this.client.removeAllListeners();
31
+ this.client.end(true);
32
+ this.client = undefined;
33
+ }
34
+ }
35
+ addMessageListener(listener) {
36
+ this.messageListeners.add(listener);
37
+ }
38
+ removeMessageListener(listener) {
39
+ this.messageListeners.delete(listener);
40
+ }
41
+ subscribeDevice(deviceId, supportLocal = false) {
42
+ if (!this.client || !this.config) {
43
+ return;
44
+ }
45
+ this.client.subscribe(this.subscribeTopic(deviceId, supportLocal));
46
+ }
47
+ unSubscribeDevice(deviceId, supportLocal = false) {
48
+ if (!this.client || !this.config) {
49
+ return;
50
+ }
51
+ this.client.unsubscribe(this.subscribeTopic(deviceId, supportLocal));
52
+ }
53
+ async connect() {
54
+ this.stop();
55
+ const res = await this.api.post('/v1.0/m/life/ha/access/config', null, { linkId: `homebridge-tuya-ha.${(0, util_1.generateUUID)()}` });
56
+ if (!res || !res.success) {
57
+ this.log.warn('Get Tuya HA MQTT config failed. code = %s, msg = %s', res?.code, res?.msg);
58
+ return;
59
+ }
60
+ this.config = res.result;
61
+ const url = new url_1.URL(this.config.url);
62
+ const protocol = url.protocol === 'ssl:' ? 'mqtts' : url.protocol.replace(':', '') || 'mqtt';
63
+ const connectUrl = `${protocol}://${url.hostname}:${url.port}`;
64
+ this.log.debug('Connecting to Tuya HA MQTT: %s', connectUrl);
65
+ const client = mqtt_1.default.connect(connectUrl, {
66
+ clientId: this.config.clientId,
67
+ username: this.config.username,
68
+ password: this.config.password,
69
+ });
70
+ client.on('connect', this.onConnect.bind(this));
71
+ client.on('error', error => this.log.error('Tuya HA MQTT error: %s', error.message));
72
+ client.on('end', () => this.log.debug('Tuya HA MQTT end'));
73
+ client.on('message', this.onMessage.bind(this));
74
+ this.client = client;
75
+ const timeout = Math.max(60, (this.config.expireTime || 7200) - 60);
76
+ this.reconnectTimer = setTimeout(() => this.connect().catch(error => this.log.error('Tuya HA MQTT reconnect failed: %s', error.message)), timeout * 1000);
77
+ }
78
+ onConnect() {
79
+ this.log.debug('Tuya HA MQTT connected');
80
+ if (!this.client || !this.config) {
81
+ return;
82
+ }
83
+ for (const ownerId of this.ownerIds) {
84
+ this.client.subscribe(this.config.topic.ownerId.sub.replace('{ownerId}', ownerId));
85
+ }
86
+ const topics = this.devices.map(device => this.subscribeTopic(device.id, device.support_local || device.supportLocal || false));
87
+ if (topics.length > 0) {
88
+ this.client.subscribe(topics);
89
+ }
90
+ }
91
+ subscribeTopic(deviceId, supportLocal = false) {
92
+ if (!this.config) {
93
+ return '';
94
+ }
95
+ let topic = this.config.topic.devId.sub.replace('{devId}', deviceId);
96
+ topic += supportLocal ? '/pen' : '/sta';
97
+ return topic;
98
+ }
99
+ onMessage(_topic, payload) {
100
+ try {
101
+ const message = JSON.parse(payload.toString('utf8'));
102
+ this.log.debug('Tuya HA MQTT message: %s', JSON.stringify(message));
103
+ for (const listener of this.messageListeners) {
104
+ listener(message);
105
+ }
106
+ }
107
+ catch (error) {
108
+ const msg = error instanceof Error ? error.message : String(error);
109
+ this.log.warn('Could not parse Tuya HA MQTT message: %s', msg);
110
+ }
111
+ }
112
+ }
113
+ exports.default = TuyaHASharingMQ;
114
+ //# sourceMappingURL=TuyaHASharingMQ.js.map
@@ -0,0 +1,50 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.TuyaDeviceSchemaType = exports.TuyaDeviceSchemaMode = void 0;
4
+ var TuyaDeviceSchemaMode;
5
+ (function (TuyaDeviceSchemaMode) {
6
+ TuyaDeviceSchemaMode["UNKNOWN"] = "";
7
+ TuyaDeviceSchemaMode["READ_WRITE"] = "rw";
8
+ TuyaDeviceSchemaMode["READ_ONLY"] = "ro";
9
+ TuyaDeviceSchemaMode["WRITE_ONLY"] = "wo";
10
+ })(TuyaDeviceSchemaMode || (exports.TuyaDeviceSchemaMode = TuyaDeviceSchemaMode = {}));
11
+ var TuyaDeviceSchemaType;
12
+ (function (TuyaDeviceSchemaType) {
13
+ TuyaDeviceSchemaType["Boolean"] = "Boolean";
14
+ TuyaDeviceSchemaType["Integer"] = "Integer";
15
+ TuyaDeviceSchemaType["Enum"] = "Enum";
16
+ TuyaDeviceSchemaType["String"] = "String";
17
+ TuyaDeviceSchemaType["Json"] = "Json";
18
+ TuyaDeviceSchemaType["Raw"] = "Raw";
19
+ })(TuyaDeviceSchemaType || (exports.TuyaDeviceSchemaType = TuyaDeviceSchemaType = {}));
20
+ class TuyaDevice {
21
+ constructor(obj) {
22
+ Object.assign(this, obj);
23
+ // Deep copy status array to ensure independence between instances
24
+ if (Array.isArray(this.status)) {
25
+ this.status = this.status.map(s => ({ ...s }));
26
+ }
27
+ else {
28
+ this.status = [];
29
+ }
30
+ // Deep copy schema array to ensure independence between instances
31
+ if (Array.isArray(this.schema)) {
32
+ this.schema = this.schema.map(s => ({ ...s }));
33
+ }
34
+ else {
35
+ this.schema = [];
36
+ }
37
+ }
38
+ isVirtualDevice() {
39
+ return this.id.startsWith('vdevo');
40
+ }
41
+ isIRControlHub() {
42
+ return ['wnykq', 'hwktwkq', 'wsdykq']
43
+ .includes(this.category);
44
+ }
45
+ isIRRemoteControl() {
46
+ return this.remote_keys !== undefined || this.category.startsWith('infrared_');
47
+ }
48
+ }
49
+ exports.default = TuyaDevice;
50
+ //# sourceMappingURL=TuyaDevice.js.map