iobroker.byd 0.0.1

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/lib/bydapi.js ADDED
@@ -0,0 +1,416 @@
1
+ 'use strict';
2
+
3
+ const crypto = require('crypto');
4
+ const bangcle = require('./bangcle');
5
+
6
+ const BASE_URL = 'https://dilinkappoversea-eu.byd.auto';
7
+ const USER_AGENT = 'okhttp/4.12.0';
8
+
9
+ const DEFAULT_DEVICE_CONFIG = Object.freeze({
10
+ imeiMd5: '00000000000000000000000000000000',
11
+ networkType: 'wifi',
12
+ appInnerVersion: '322',
13
+ appVersion: '3.2.2',
14
+ osType: '15',
15
+ osVersion: '35',
16
+ timeZone: 'Europe/Amsterdam',
17
+ deviceType: '0',
18
+ mobileBrand: 'XIAOMI',
19
+ mobileModel: 'POCO F1',
20
+ softType: '0',
21
+ tboxVersion: '3',
22
+ isAuto: '1',
23
+ ostype: 'and',
24
+ imei: 'BANGCLE01234',
25
+ mac: '00:00:00:00:00:00',
26
+ model: 'POCO F1',
27
+ sdk: '35',
28
+ mod: 'Xiaomi',
29
+ });
30
+
31
+ // Crypto helpers
32
+ function md5Hex(value) {
33
+ return crypto.createHash('md5').update(value, 'utf8').digest('hex').toUpperCase();
34
+ }
35
+
36
+ function pwdLoginKey(password) {
37
+ return md5Hex(md5Hex(password));
38
+ }
39
+
40
+ function sha1Mixed(value) {
41
+ const digest = crypto.createHash('sha1').update(value, 'utf8').digest();
42
+ const mixed = Array.from(digest)
43
+ .map((byte, index) => {
44
+ const hex = byte.toString(16).padStart(2, '0');
45
+ return index % 2 === 0 ? hex.toUpperCase() : hex.toLowerCase();
46
+ })
47
+ .join('');
48
+
49
+ let filtered = '';
50
+ for (let i = 0; i < mixed.length; i += 1) {
51
+ const ch = mixed[i];
52
+ if (ch === '0' && i % 2 === 0) {
53
+ continue;
54
+ }
55
+ filtered += ch;
56
+ }
57
+ return filtered;
58
+ }
59
+
60
+ function buildSignString(fields, password) {
61
+ const keys = Object.keys(fields).sort();
62
+ const joined = keys.map(key => `${key}=${String(fields[key])}`).join('&');
63
+ return `${joined}&password=${password}`;
64
+ }
65
+
66
+ function computeCheckcode(payload) {
67
+ const json = JSON.stringify(payload);
68
+ const md5 = crypto.createHash('md5').update(json, 'utf8').digest('hex');
69
+ return `${md5.slice(24, 32)}${md5.slice(8, 16)}${md5.slice(16, 24)}${md5.slice(0, 8)}`;
70
+ }
71
+
72
+ function aesEncryptHex(plaintextUtf8, keyHex) {
73
+ const key = Buffer.from(keyHex, 'hex');
74
+ const iv = Buffer.alloc(16, 0);
75
+ const cipher = crypto.createCipheriv('aes-128-cbc', key, iv);
76
+ return Buffer.concat([cipher.update(plaintextUtf8, 'utf8'), cipher.final()])
77
+ .toString('hex')
78
+ .toUpperCase();
79
+ }
80
+
81
+ function aesDecryptUtf8(cipherHex, keyHex) {
82
+ const key = Buffer.from(keyHex, 'hex');
83
+ const iv = Buffer.alloc(16, 0);
84
+ const ciphertext = Buffer.from(cipherHex, 'hex');
85
+ const decipher = crypto.createDecipheriv('aes-128-cbc', key, iv);
86
+ return Buffer.concat([decipher.update(ciphertext), decipher.final()]).toString('utf8');
87
+ }
88
+
89
+ function randomHex16() {
90
+ return crypto.randomBytes(16).toString('hex').toUpperCase();
91
+ }
92
+
93
+ // Bangcle envelope
94
+ /**
95
+ *
96
+ */
97
+ function encodeEnvelope(payload) {
98
+ return bangcle.encodeEnvelope(JSON.stringify(payload));
99
+ }
100
+
101
+ /**
102
+ *
103
+ */
104
+ function decodeEnvelope(rawPayload) {
105
+ // Handle response object with 'response' field
106
+ const payload = typeof rawPayload === 'object' && rawPayload.response ? rawPayload.response : rawPayload;
107
+
108
+ if (typeof payload !== 'string' || !payload.trim()) {
109
+ throw new Error('Empty response payload');
110
+ }
111
+ const decodedText = bangcle.decodeEnvelope(payload).toString('utf8').trim();
112
+ const normalised =
113
+ decodedText.startsWith('F{') || decodedText.startsWith('F[') ? decodedText.slice(1) : decodedText;
114
+ return JSON.parse(normalised);
115
+ }
116
+
117
+ /**
118
+ *
119
+ */
120
+ function decryptResponseData(respondDataHex, keyHex) {
121
+ const plain = aesDecryptUtf8(respondDataHex, keyHex);
122
+ return JSON.parse(plain);
123
+ }
124
+
125
+ // Common fields for all requests
126
+ function commonOuterFields(deviceConfig) {
127
+ return {
128
+ ostype: deviceConfig.ostype,
129
+ imei: deviceConfig.imei,
130
+ mac: deviceConfig.mac,
131
+ model: deviceConfig.model,
132
+ sdk: deviceConfig.sdk,
133
+ mod: deviceConfig.mod,
134
+ };
135
+ }
136
+
137
+ // Build login request
138
+ /**
139
+ *
140
+ */
141
+ function buildLoginRequest(username, password, countryCode, language, deviceConfig) {
142
+ const nowMs = Date.now();
143
+ const random = randomHex16();
144
+ const reqTimestamp = String(nowMs);
145
+ const serviceTime = String(Date.now());
146
+
147
+ const inner = {
148
+ appInnerVersion: deviceConfig.appInnerVersion,
149
+ appVersion: deviceConfig.appVersion,
150
+ deviceName: `${deviceConfig.mobileBrand}${deviceConfig.mobileModel}`,
151
+ deviceType: deviceConfig.deviceType,
152
+ imeiMD5: deviceConfig.imeiMd5,
153
+ isAuto: deviceConfig.isAuto,
154
+ mobileBrand: deviceConfig.mobileBrand,
155
+ mobileModel: deviceConfig.mobileModel,
156
+ networkType: deviceConfig.networkType,
157
+ osType: deviceConfig.osType,
158
+ osVersion: deviceConfig.osVersion,
159
+ random,
160
+ softType: deviceConfig.softType,
161
+ timeStamp: reqTimestamp,
162
+ timeZone: deviceConfig.timeZone,
163
+ };
164
+
165
+ const encryData = aesEncryptHex(JSON.stringify(inner), pwdLoginKey(password));
166
+
167
+ const signFields = {
168
+ ...inner,
169
+ countryCode,
170
+ functionType: 'pwdLogin',
171
+ identifier: username,
172
+ identifierType: '0',
173
+ language,
174
+ reqTimestamp,
175
+ };
176
+
177
+ const sign = sha1Mixed(buildSignString(signFields, md5Hex(password)));
178
+
179
+ const outer = {
180
+ countryCode,
181
+ encryData,
182
+ functionType: 'pwdLogin',
183
+ identifier: username,
184
+ identifierType: '0',
185
+ imeiMD5: deviceConfig.imeiMd5,
186
+ isAuto: deviceConfig.isAuto,
187
+ language,
188
+ reqTimestamp,
189
+ sign,
190
+ signKey: password,
191
+ ...commonOuterFields(deviceConfig),
192
+ serviceTime,
193
+ };
194
+ outer.checkcode = computeCheckcode(outer);
195
+
196
+ return { outer };
197
+ }
198
+
199
+ // Build token-based request envelope
200
+ function buildTokenEnvelope(session, countryCode, language, deviceConfig, inner) {
201
+ const nowMs = Date.now();
202
+ const reqTimestamp = String(nowMs);
203
+ const contentKey = md5Hex(session.encryToken);
204
+ const signKey = md5Hex(session.signToken);
205
+ const encryData = aesEncryptHex(JSON.stringify(inner), contentKey);
206
+
207
+ const signFields = {
208
+ ...inner,
209
+ countryCode,
210
+ identifier: session.userId,
211
+ imeiMD5: deviceConfig.imeiMd5,
212
+ language,
213
+ reqTimestamp,
214
+ };
215
+ const sign = sha1Mixed(buildSignString(signFields, signKey));
216
+
217
+ const outer = {
218
+ countryCode,
219
+ encryData,
220
+ identifier: session.userId,
221
+ imeiMD5: deviceConfig.imeiMd5,
222
+ language,
223
+ reqTimestamp,
224
+ sign,
225
+ ...commonOuterFields(deviceConfig),
226
+ serviceTime: String(Date.now()),
227
+ };
228
+ outer.checkcode = computeCheckcode(outer);
229
+
230
+ return { outer, contentKey };
231
+ }
232
+
233
+ // Build vehicle list request
234
+ /**
235
+ *
236
+ */
237
+ function buildVehicleListRequest(session, countryCode, language, deviceConfig) {
238
+ const inner = {
239
+ deviceType: deviceConfig.deviceType,
240
+ imeiMD5: deviceConfig.imeiMd5,
241
+ networkType: deviceConfig.networkType,
242
+ random: randomHex16(),
243
+ timeStamp: String(Date.now()),
244
+ version: deviceConfig.appInnerVersion,
245
+ };
246
+ return buildTokenEnvelope(session, countryCode, language, deviceConfig, inner);
247
+ }
248
+
249
+ // Build vehicle realtime request
250
+ /**
251
+ *
252
+ */
253
+ function buildVehicleRealtimeRequest(session, countryCode, language, deviceConfig, vin, requestSerial = null) {
254
+ const inner = {
255
+ deviceType: deviceConfig.deviceType,
256
+ energyType: '0',
257
+ imeiMD5: deviceConfig.imeiMd5,
258
+ networkType: deviceConfig.networkType,
259
+ random: randomHex16(),
260
+ tboxVersion: deviceConfig.tboxVersion,
261
+ timeStamp: String(Date.now()),
262
+ version: deviceConfig.appInnerVersion,
263
+ vin,
264
+ };
265
+ if (requestSerial) {
266
+ inner.requestSerial = requestSerial;
267
+ }
268
+ return buildTokenEnvelope(session, countryCode, language, deviceConfig, inner);
269
+ }
270
+
271
+ // Build GPS info request
272
+ /**
273
+ *
274
+ */
275
+ function buildGpsInfoRequest(session, countryCode, language, deviceConfig, vin, requestSerial = null) {
276
+ const inner = {
277
+ deviceType: deviceConfig.deviceType,
278
+ imeiMD5: deviceConfig.imeiMd5,
279
+ networkType: deviceConfig.networkType,
280
+ random: randomHex16(),
281
+ timeStamp: String(Date.now()),
282
+ version: deviceConfig.appInnerVersion,
283
+ vin,
284
+ };
285
+ if (requestSerial) {
286
+ inner.requestSerial = requestSerial;
287
+ }
288
+ return buildTokenEnvelope(session, countryCode, language, deviceConfig, inner);
289
+ }
290
+
291
+ // Build remote control request
292
+ /**
293
+ *
294
+ */
295
+ function buildRemoteControlRequest(
296
+ session,
297
+ countryCode,
298
+ language,
299
+ deviceConfig,
300
+ vin,
301
+ instructionCode,
302
+ requestSerial = null,
303
+ ) {
304
+ const inner = {
305
+ deviceType: deviceConfig.deviceType,
306
+ imeiMD5: deviceConfig.imeiMd5,
307
+ instructionCode,
308
+ networkType: deviceConfig.networkType,
309
+ random: randomHex16(),
310
+ timeStamp: String(Date.now()),
311
+ version: deviceConfig.appInnerVersion,
312
+ vin,
313
+ };
314
+ if (requestSerial) {
315
+ inner.requestSerial = requestSerial;
316
+ }
317
+ return buildTokenEnvelope(session, countryCode, language, deviceConfig, inner);
318
+ }
319
+
320
+ // Build energy consumption request
321
+ /**
322
+ *
323
+ */
324
+ function buildEnergyConsumptionRequest(session, countryCode, language, deviceConfig, vin) {
325
+ const inner = {
326
+ deviceType: deviceConfig.deviceType,
327
+ imeiMD5: deviceConfig.imeiMd5,
328
+ networkType: deviceConfig.networkType,
329
+ random: randomHex16(),
330
+ timeStamp: String(Date.now()),
331
+ version: deviceConfig.appInnerVersion,
332
+ vin,
333
+ };
334
+ return buildTokenEnvelope(session, countryCode, language, deviceConfig, inner);
335
+ }
336
+
337
+ // Data readiness checks
338
+ /**
339
+ *
340
+ */
341
+ function isRealtimeDataReady(vehicleInfo) {
342
+ if (!vehicleInfo || typeof vehicleInfo !== 'object') {
343
+ return false;
344
+ }
345
+ if (Number(vehicleInfo.onlineState) === 2) {
346
+ return false;
347
+ }
348
+ const tireFields = [
349
+ 'leftFrontTirepressure',
350
+ 'rightFrontTirepressure',
351
+ 'leftRearTirepressure',
352
+ 'rightRearTirepressure',
353
+ ];
354
+ if (tireFields.some(field => Number(vehicleInfo[field]) > 0)) {
355
+ return true;
356
+ }
357
+ if (Number(vehicleInfo.time) > 0) {
358
+ return true;
359
+ }
360
+ if (Number(vehicleInfo.enduranceMileage) > 0) {
361
+ return true;
362
+ }
363
+ return false;
364
+ }
365
+
366
+ /**
367
+ *
368
+ */
369
+ function isGpsDataReady(gpsInfo) {
370
+ if (!gpsInfo || typeof gpsInfo !== 'object') {
371
+ return false;
372
+ }
373
+ const keys = Object.keys(gpsInfo);
374
+ if (!keys.length || (keys.length === 1 && keys[0] === 'requestSerial')) {
375
+ return false;
376
+ }
377
+ return true;
378
+ }
379
+
380
+ /**
381
+ *
382
+ */
383
+ function isRemoteControlReady(data) {
384
+ if (!data || typeof data !== 'object') {
385
+ return false;
386
+ }
387
+ // Check for controlState field indicating completion
388
+ // 0 = pending, 1 = success, 2 = failed
389
+ if (data.controlState !== undefined && data.controlState !== '0' && data.controlState !== 0) {
390
+ return true;
391
+ }
392
+ // Check for result field
393
+ if (data.result !== undefined) {
394
+ return true;
395
+ }
396
+ return false;
397
+ }
398
+
399
+ module.exports = {
400
+ BASE_URL,
401
+ USER_AGENT,
402
+ DEFAULT_DEVICE_CONFIG,
403
+ pwdLoginKey,
404
+ encodeEnvelope,
405
+ decodeEnvelope,
406
+ decryptResponseData,
407
+ buildLoginRequest,
408
+ buildVehicleListRequest,
409
+ buildVehicleRealtimeRequest,
410
+ buildGpsInfoRequest,
411
+ buildRemoteControlRequest,
412
+ buildEnergyConsumptionRequest,
413
+ isRealtimeDataReady,
414
+ isGpsDataReady,
415
+ isRemoteControlReady,
416
+ };