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/LICENSE +21 -0
- package/README.md +51 -0
- package/admin/byd.png +0 -0
- package/admin/jsonConfig.json +99 -0
- package/io-package.json +111 -0
- package/lib/adapter-config.d.ts +19 -0
- package/lib/bangcle.js +379 -0
- package/lib/bangcle_auth_tables.js +30 -0
- package/lib/bydapi.js +416 -0
- package/main.js +597 -0
- package/package.json +67 -0
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
|
+
};
|