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/main.js ADDED
@@ -0,0 +1,597 @@
1
+ 'use strict';
2
+
3
+ const utils = require('@iobroker/adapter-core');
4
+ const axios = require('axios').default;
5
+ const { wrapper } = require('axios-cookiejar-support');
6
+ const { CookieJar } = require('tough-cookie');
7
+ const Json2iob = require('json2iob');
8
+ const bydapi = require('./lib/bydapi');
9
+
10
+ class Byd extends utils.Adapter {
11
+ constructor(options) {
12
+ super({
13
+ ...options,
14
+ name: 'byd',
15
+ });
16
+
17
+ this.on('ready', this.onReady.bind(this));
18
+ this.on('stateChange', this.onStateChange.bind(this));
19
+ this.on('unload', this.onUnload.bind(this));
20
+
21
+ this.vehicleArray = [];
22
+ this.json2iob = new Json2iob(this);
23
+ this.updateInterval = null;
24
+ this.refreshTimeout = null;
25
+ this.session = null;
26
+ this.deviceConfig = bydapi.DEFAULT_DEVICE_CONFIG;
27
+
28
+ const jar = new CookieJar();
29
+ this.requestClient = wrapper(
30
+ axios.create({
31
+ withCredentials: true,
32
+ timeout: 3 * 60 * 1000,
33
+ jar,
34
+ }),
35
+ );
36
+ }
37
+
38
+ async onReady() {
39
+ this.setState('info.connection', false, true);
40
+
41
+ if (this.config.interval < 60) {
42
+ this.log.info('Set interval to minimum 60');
43
+ this.config.interval = 60;
44
+ }
45
+
46
+ if (!this.config.username || !this.config.password) {
47
+ this.log.error('Please set username and password in the instance settings');
48
+ return;
49
+ }
50
+
51
+ this.subscribeStates('*');
52
+
53
+ await this.login();
54
+
55
+ if (!this.session) {
56
+ return;
57
+ }
58
+
59
+ await this.getVehicleList();
60
+ await this.updateVehicles();
61
+
62
+ this.updateInterval = setInterval(() => {
63
+ this.updateVehicles();
64
+ }, this.config.interval * 1000);
65
+ }
66
+
67
+ async login() {
68
+ const { outer } = bydapi.buildLoginRequest(
69
+ this.config.username,
70
+ this.config.password,
71
+ this.config.countryCode,
72
+ this.config.language,
73
+ this.deviceConfig,
74
+ );
75
+
76
+ const envelope = bydapi.encodeEnvelope(outer);
77
+
78
+ await this.requestClient({
79
+ method: 'post',
80
+ url: `${bydapi.BASE_URL}/app/account/login`,
81
+ headers: {
82
+ 'User-Agent': bydapi.USER_AGENT,
83
+ 'Content-Type': 'application/json; charset=UTF-8',
84
+ },
85
+ data: { request: envelope },
86
+ })
87
+ .then(async res => {
88
+ this.log.debug(`Login response: ${JSON.stringify(res.data)}`);
89
+ const decoded = bydapi.decodeEnvelope(res.data);
90
+ this.log.debug(`Decoded login: ${JSON.stringify(decoded)}`);
91
+
92
+ if (decoded.code !== '0') {
93
+ this.log.error(`Login failed: code=${decoded.code} message=${decoded.message || ''}`);
94
+ return;
95
+ }
96
+
97
+ const loginKey = bydapi.pwdLoginKey(this.config.password);
98
+ const loginData = bydapi.decryptResponseData(decoded.respondData, loginKey);
99
+ const token = loginData.token || {};
100
+ const data = {
101
+ userId: token.userId,
102
+ signToken: token.signToken,
103
+ encryToken: token.encryToken,
104
+ };
105
+ this.session = {
106
+ userId: data.userId,
107
+ signToken: data.signToken,
108
+ encryToken: data.encryToken,
109
+ };
110
+
111
+ this.log.info('Login successful');
112
+ this.setState('info.connection', true, true);
113
+ })
114
+ .catch(error => {
115
+ this.log.error(`Login error: ${error.message}`);
116
+ error.response && this.log.error(JSON.stringify(error.response.data));
117
+ });
118
+ }
119
+
120
+ async getVehicleList() {
121
+ if (!this.session) {
122
+ return;
123
+ }
124
+
125
+ const { outer, contentKey } = bydapi.buildVehicleListRequest(
126
+ this.session,
127
+ this.config.countryCode,
128
+ this.config.language,
129
+ this.deviceConfig,
130
+ );
131
+
132
+ const envelope = bydapi.encodeEnvelope(outer);
133
+
134
+ await this.requestClient({
135
+ method: 'post',
136
+ url: `${bydapi.BASE_URL}/app/account/getAllListByUserId`,
137
+ headers: {
138
+ 'User-Agent': bydapi.USER_AGENT,
139
+ 'Content-Type': 'application/json; charset=UTF-8',
140
+ },
141
+ data: { request: envelope },
142
+ })
143
+ .then(async res => {
144
+ this.log.debug(`Vehicle list response: ${JSON.stringify(res.data)}`);
145
+ const decoded = bydapi.decodeEnvelope(res.data);
146
+
147
+ if (decoded.code !== '0') {
148
+ this.log.error(`Vehicle list failed: code=${decoded.code} message=${decoded.message || ''}`);
149
+ return;
150
+ }
151
+
152
+ const data = bydapi.decryptResponseData(decoded.respondData, contentKey);
153
+ this.log.debug(`Vehicle list: ${JSON.stringify(data)}`);
154
+
155
+ const vehicles = data.allCarList || [];
156
+ this.log.info(`Found ${vehicles.length} vehicle(s)`);
157
+
158
+ for (const vehicle of vehicles) {
159
+ const vin = vehicle.vin;
160
+ if (!vin) {
161
+ continue;
162
+ }
163
+
164
+ const id = vin.toString().replace(this.FORBIDDEN_CHARS, '_');
165
+ this.vehicleArray.push(vehicle);
166
+
167
+ await this.extendObject(id, {
168
+ type: 'device',
169
+ common: { name: vehicle.carSeriesName || vehicle.vin },
170
+ native: {},
171
+ });
172
+
173
+ await this.setObjectNotExistsAsync(`${id}.remote`, {
174
+ type: 'channel',
175
+ common: { name: 'Remote Controls' },
176
+ native: {},
177
+ });
178
+
179
+ const remoteArray = [
180
+ { command: 'refresh', name: 'True = Refresh' },
181
+ { command: 'lock', name: 'True = Lock' },
182
+ { command: 'unlock', name: 'True = Unlock' },
183
+ { command: 'flash', name: 'True = Flash Lights' },
184
+ { command: 'horn', name: 'True = Horn' },
185
+ { command: 'climate', name: 'True = Start Climate, False = Stop Climate' },
186
+ ];
187
+
188
+ for (const remote of remoteArray) {
189
+ this.extendObject(`${id}.remote.${remote.command}`, {
190
+ type: 'state',
191
+ common: {
192
+ name: remote.name || '',
193
+ type: 'boolean',
194
+ role: 'button',
195
+ def: false,
196
+ write: true,
197
+ read: true,
198
+ },
199
+ native: {},
200
+ });
201
+ }
202
+
203
+ this.json2iob.parse(id, vehicle, { forceIndex: true });
204
+ }
205
+ })
206
+ .catch(error => {
207
+ this.log.error(`Vehicle list error: ${error.message}`);
208
+ error.response && this.log.error(JSON.stringify(error.response.data));
209
+ });
210
+ }
211
+
212
+ async updateVehicles() {
213
+ for (const vehicle of this.vehicleArray) {
214
+ const vin = vehicle.vin;
215
+ const id = vin.toString().replace(this.FORBIDDEN_CHARS, '_');
216
+
217
+ await this.pollVehicleRealtime(vin, id);
218
+ await this.pollGpsInfo(vin, id);
219
+ await this.getEnergyConsumption(vin, id);
220
+ }
221
+ }
222
+
223
+ async pollVehicleRealtime(vin, id) {
224
+ if (!this.session) {
225
+ return;
226
+ }
227
+
228
+ // Trigger realtime data request
229
+ const triggerReq = bydapi.buildVehicleRealtimeRequest(
230
+ this.session,
231
+ this.config.countryCode,
232
+ this.config.language,
233
+ this.deviceConfig,
234
+ vin,
235
+ );
236
+
237
+ let requestSerial = null;
238
+
239
+ await this.requestClient({
240
+ method: 'post',
241
+ url: `${bydapi.BASE_URL}/vehicleInfo/vehicle/vehicleRealTimeRequest`,
242
+ headers: {
243
+ 'User-Agent': bydapi.USER_AGENT,
244
+ 'Content-Type': 'application/json; charset=UTF-8',
245
+ },
246
+ data: { request: bydapi.encodeEnvelope(triggerReq.outer) },
247
+ })
248
+ .then(async res => {
249
+ this.log.debug(`Realtime trigger response: ${JSON.stringify(res.data)}`);
250
+ const decoded = bydapi.decodeEnvelope(res.data);
251
+ if (decoded.code === '0' && decoded.respondData) {
252
+ const data = bydapi.decryptResponseData(decoded.respondData, triggerReq.contentKey);
253
+ requestSerial = data.requestSerial || null;
254
+ }
255
+ })
256
+ .catch(error => {
257
+ this.log.error(`Realtime trigger error: ${error.message}`);
258
+ });
259
+
260
+ // Poll for result (up to 10 attempts)
261
+ for (let attempt = 0; attempt < 10; attempt++) {
262
+ await this.sleep(1500);
263
+
264
+ const pollReq = bydapi.buildVehicleRealtimeRequest(
265
+ this.session,
266
+ this.config.countryCode,
267
+ this.config.language,
268
+ this.deviceConfig,
269
+ vin,
270
+ requestSerial,
271
+ );
272
+
273
+ let ready = false;
274
+
275
+ await this.requestClient({
276
+ method: 'post',
277
+ url: `${bydapi.BASE_URL}/vehicleInfo/vehicle/vehicleRealTimeResult`,
278
+ headers: {
279
+ 'User-Agent': bydapi.USER_AGENT,
280
+ 'Content-Type': 'application/json; charset=UTF-8',
281
+ },
282
+ data: { request: bydapi.encodeEnvelope(pollReq.outer) },
283
+ })
284
+ .then(async res => {
285
+ this.log.debug(`Realtime poll response: ${JSON.stringify(res.data)}`);
286
+ const decoded = bydapi.decodeEnvelope(res.data);
287
+ if (decoded.code === '0' && decoded.respondData) {
288
+ const data = bydapi.decryptResponseData(decoded.respondData, pollReq.contentKey);
289
+ this.log.debug(`Realtime data: ${JSON.stringify(data)}`);
290
+
291
+ if (bydapi.isRealtimeDataReady(data)) {
292
+ this.json2iob.parse(id, data, { forceIndex: true });
293
+ ready = true;
294
+ }
295
+ }
296
+ })
297
+ .catch(error => {
298
+ this.log.error(`Realtime poll error: ${error.message}`);
299
+ });
300
+
301
+ if (ready) {
302
+ break;
303
+ }
304
+ }
305
+ }
306
+
307
+ async pollGpsInfo(vin, id) {
308
+ if (!this.session) {
309
+ return;
310
+ }
311
+
312
+ // Trigger GPS request
313
+ const triggerReq = bydapi.buildGpsInfoRequest(
314
+ this.session,
315
+ this.config.countryCode,
316
+ this.config.language,
317
+ this.deviceConfig,
318
+ vin,
319
+ );
320
+
321
+ let requestSerial = null;
322
+
323
+ await this.requestClient({
324
+ method: 'post',
325
+ url: `${bydapi.BASE_URL}/control/getGpsInfo`,
326
+ headers: {
327
+ 'User-Agent': bydapi.USER_AGENT,
328
+ 'Content-Type': 'application/json; charset=UTF-8',
329
+ },
330
+ data: { request: bydapi.encodeEnvelope(triggerReq.outer) },
331
+ })
332
+ .then(async res => {
333
+ this.log.debug(`GPS trigger response: ${JSON.stringify(res.data)}`);
334
+ const decoded = bydapi.decodeEnvelope(res.data);
335
+ if (decoded.code === '0' && decoded.respondData) {
336
+ const data = bydapi.decryptResponseData(decoded.respondData, triggerReq.contentKey);
337
+ requestSerial = data.requestSerial || null;
338
+ }
339
+ })
340
+ .catch(error => {
341
+ this.log.error(`GPS trigger error: ${error.message}`);
342
+ });
343
+
344
+ // Poll for result (up to 10 attempts)
345
+ for (let attempt = 0; attempt < 10; attempt++) {
346
+ await this.sleep(1500);
347
+
348
+ const pollReq = bydapi.buildGpsInfoRequest(
349
+ this.session,
350
+ this.config.countryCode,
351
+ this.config.language,
352
+ this.deviceConfig,
353
+ vin,
354
+ requestSerial,
355
+ );
356
+
357
+ let ready = false;
358
+
359
+ await this.requestClient({
360
+ method: 'post',
361
+ url: `${bydapi.BASE_URL}/control/getGpsInfoResult`,
362
+ headers: {
363
+ 'User-Agent': bydapi.USER_AGENT,
364
+ 'Content-Type': 'application/json; charset=UTF-8',
365
+ },
366
+ data: { request: bydapi.encodeEnvelope(pollReq.outer) },
367
+ })
368
+ .then(async res => {
369
+ this.log.debug(`GPS poll response: ${JSON.stringify(res.data)}`);
370
+ const decoded = bydapi.decodeEnvelope(res.data);
371
+ if (decoded.code === '0' && decoded.respondData) {
372
+ const data = bydapi.decryptResponseData(decoded.respondData, pollReq.contentKey);
373
+ this.log.debug(`GPS data: ${JSON.stringify(data)}`);
374
+
375
+ if (bydapi.isGpsDataReady(data)) {
376
+ this.json2iob.parse(`${id}.gps`, data, { forceIndex: true });
377
+ ready = true;
378
+ }
379
+ }
380
+ })
381
+ .catch(error => {
382
+ this.log.error(`GPS poll error: ${error.message}`);
383
+ });
384
+
385
+ if (ready) {
386
+ break;
387
+ }
388
+ }
389
+ }
390
+
391
+ async getEnergyConsumption(vin, id) {
392
+ if (!this.session) {
393
+ return;
394
+ }
395
+
396
+ const req = bydapi.buildEnergyConsumptionRequest(
397
+ this.session,
398
+ this.config.countryCode,
399
+ this.config.language,
400
+ this.deviceConfig,
401
+ vin,
402
+ );
403
+
404
+ await this.requestClient({
405
+ method: 'post',
406
+ url: `${bydapi.BASE_URL}/vehicleInfo/vehicle/getEnergyConsumption`,
407
+ headers: {
408
+ 'User-Agent': bydapi.USER_AGENT,
409
+ 'Content-Type': 'application/json; charset=UTF-8',
410
+ },
411
+ data: { request: bydapi.encodeEnvelope(req.outer) },
412
+ })
413
+ .then(async res => {
414
+ this.log.debug(`Energy consumption response: ${JSON.stringify(res.data)}`);
415
+ const decoded = bydapi.decodeEnvelope(res.data);
416
+ if (decoded.code === '0' && decoded.respondData) {
417
+ const data = bydapi.decryptResponseData(decoded.respondData, req.contentKey);
418
+ this.log.debug(`Energy consumption data: ${JSON.stringify(data)}`);
419
+ this.json2iob.parse(`${id}.energy`, data, { forceIndex: true });
420
+ }
421
+ })
422
+ .catch(error => {
423
+ this.log.error(`Energy consumption error: ${error.message}`);
424
+ });
425
+ }
426
+
427
+ async sendRemoteControl(vin, instructionCode) {
428
+ if (!this.session) {
429
+ return;
430
+ }
431
+
432
+ // Trigger remote control
433
+ const triggerReq = bydapi.buildRemoteControlRequest(
434
+ this.session,
435
+ this.config.countryCode,
436
+ this.config.language,
437
+ this.deviceConfig,
438
+ vin,
439
+ instructionCode,
440
+ );
441
+
442
+ let requestSerial = null;
443
+
444
+ await this.requestClient({
445
+ method: 'post',
446
+ url: `${bydapi.BASE_URL}/control/remoteControl`,
447
+ headers: {
448
+ 'User-Agent': bydapi.USER_AGENT,
449
+ 'Content-Type': 'application/json; charset=UTF-8',
450
+ },
451
+ data: { request: bydapi.encodeEnvelope(triggerReq.outer) },
452
+ })
453
+ .then(async res => {
454
+ this.log.debug(`Remote control trigger response: ${JSON.stringify(res.data)}`);
455
+ const decoded = bydapi.decodeEnvelope(res.data);
456
+ if (decoded.code !== '0') {
457
+ this.log.error(`Remote control failed: code=${decoded.code} message=${decoded.message || ''}`);
458
+ } else if (decoded.respondData) {
459
+ const data = bydapi.decryptResponseData(decoded.respondData, triggerReq.contentKey);
460
+ requestSerial = data.requestSerial || null;
461
+ this.log.debug(`Remote control triggered, requestSerial: ${requestSerial}`);
462
+ }
463
+ })
464
+ .catch(error => {
465
+ this.log.error(`Remote control error: ${error.message}`);
466
+ error.response && this.log.error(JSON.stringify(error.response.data));
467
+ });
468
+
469
+ if (!requestSerial) {
470
+ return;
471
+ }
472
+
473
+ // Poll for result (up to 10 attempts)
474
+ for (let attempt = 0; attempt < 10; attempt++) {
475
+ await this.sleep(1500);
476
+
477
+ const pollReq = bydapi.buildRemoteControlRequest(
478
+ this.session,
479
+ this.config.countryCode,
480
+ this.config.language,
481
+ this.deviceConfig,
482
+ vin,
483
+ instructionCode,
484
+ requestSerial,
485
+ );
486
+
487
+ let ready = false;
488
+
489
+ await this.requestClient({
490
+ method: 'post',
491
+ url: `${bydapi.BASE_URL}/control/remoteControlResult`,
492
+ headers: {
493
+ 'User-Agent': bydapi.USER_AGENT,
494
+ 'Content-Type': 'application/json; charset=UTF-8',
495
+ },
496
+ data: { request: bydapi.encodeEnvelope(pollReq.outer) },
497
+ })
498
+ .then(async res => {
499
+ this.log.debug(`Remote control poll response: ${JSON.stringify(res.data)}`);
500
+ const decoded = bydapi.decodeEnvelope(res.data);
501
+ if (decoded.code === '0' && decoded.respondData) {
502
+ const data = bydapi.decryptResponseData(decoded.respondData, pollReq.contentKey);
503
+ this.log.debug(`Remote control result: ${JSON.stringify(data)}`);
504
+
505
+ if (bydapi.isRemoteControlReady(data)) {
506
+ const success = data.controlState === '1' || data.controlState === 1;
507
+ if (success) {
508
+ this.log.info(`Remote control success: ${instructionCode}`);
509
+ } else {
510
+ this.log.warn(`Remote control completed with state: ${data.controlState}`);
511
+ }
512
+ ready = true;
513
+ }
514
+ }
515
+ })
516
+ .catch(error => {
517
+ this.log.error(`Remote control poll error: ${error.message}`);
518
+ });
519
+
520
+ if (ready) {
521
+ break;
522
+ }
523
+ }
524
+ }
525
+
526
+ async onStateChange(id, state) {
527
+ if (!state) {
528
+ return;
529
+ }
530
+
531
+ const deviceId = id.split('.')[2];
532
+ const folder = id.split('.')[3];
533
+ const command = id.split('.')[4];
534
+
535
+ // Handle ack===true to update remote states
536
+ if (state.ack && folder === 'remote') {
537
+ return;
538
+ }
539
+
540
+ // Handle ack===false for commands
541
+ if (!state.ack && folder === 'remote') {
542
+ if (command === 'refresh') {
543
+ this.updateVehicles();
544
+ await this.setStateAsync(id, false, true);
545
+ return;
546
+ }
547
+
548
+ const commandMap = {
549
+ lock: '10',
550
+ unlock: '11',
551
+ flash: '20',
552
+ horn: '21',
553
+ climate: state.val ? '30' : '31',
554
+ };
555
+
556
+ const instructionCode = commandMap[command];
557
+ if (!instructionCode) {
558
+ this.log.warn(`Unknown command: ${command}`);
559
+ return;
560
+ }
561
+
562
+ this.log.info(`Sending remote command: ${command} for ${deviceId}`);
563
+
564
+ await this.sendRemoteControl(deviceId, instructionCode);
565
+
566
+ // Acknowledge the state change
567
+ await this.setStateAsync(id, state.val, true);
568
+
569
+ // Schedule refresh after command
570
+ this.refreshTimeout && clearTimeout(this.refreshTimeout);
571
+ this.refreshTimeout = setTimeout(() => {
572
+ this.updateVehicles();
573
+ }, 10 * 1000);
574
+ }
575
+ }
576
+
577
+ sleep(ms) {
578
+ return new Promise(resolve => setTimeout(resolve, ms));
579
+ }
580
+
581
+ onUnload(callback) {
582
+ try {
583
+ this.setState('info.connection', false, true);
584
+ this.updateInterval && clearInterval(this.updateInterval);
585
+ this.refreshTimeout && clearTimeout(this.refreshTimeout);
586
+ callback();
587
+ } catch {
588
+ callback();
589
+ }
590
+ }
591
+ }
592
+
593
+ if (require.main !== module) {
594
+ module.exports = options => new Byd(options);
595
+ } else {
596
+ new Byd();
597
+ }
package/package.json ADDED
@@ -0,0 +1,67 @@
1
+ {
2
+ "name": "iobroker.byd",
3
+ "version": "0.0.1",
4
+ "description": "iobroker Adapter for BYD cars",
5
+ "author": {
6
+ "name": "TA2k",
7
+ "email": "tombox2020@gmail.com"
8
+ },
9
+ "contributors": [],
10
+ "homepage": "https://github.com/TA2k/ioBroker.byd",
11
+ "license": "MIT",
12
+ "keywords": [
13
+ "byd",
14
+ "car"
15
+ ],
16
+ "repository": {
17
+ "type": "git",
18
+ "url": "https://github.com/TA2k/ioBroker.byd.git"
19
+ },
20
+ "engines": {
21
+ "node": ">= 20"
22
+ },
23
+ "dependencies": {
24
+ "@iobroker/adapter-core": "^3.3.2",
25
+ "axios": "^1.13.5",
26
+ "axios-cookiejar-support": "^5.0.5",
27
+ "json2iob": "^2.6.22",
28
+ "tough-cookie": "^5.1.2"
29
+ },
30
+ "devDependencies": {
31
+ "@alcalzone/release-script": "^5.0.0",
32
+ "@alcalzone/release-script-plugin-iobroker": "^4.0.0",
33
+ "@alcalzone/release-script-plugin-license": "^4.0.0",
34
+ "@alcalzone/release-script-plugin-manual-review": "^4.0.0",
35
+ "@iobroker/adapter-dev": "^1.5.0",
36
+ "@iobroker/eslint-config": "^2.2.0",
37
+ "@iobroker/testing": "^5.2.2",
38
+ "@tsconfig/node20": "^20.1.9",
39
+ "@types/iobroker": "npm:@iobroker/types@^7.1.0",
40
+ "@types/node": "^20.19.33",
41
+ "typescript": "~5.9.3"
42
+ },
43
+ "main": "main.js",
44
+ "files": [
45
+ "admin{,/!(src)/**}/!(tsconfig|tsconfig.*|.eslintrc).{json,json5}",
46
+ "admin{,/!(src)/**}/*.{html,css,png,svg,jpg,js}",
47
+ "lib/",
48
+ "www/",
49
+ "io-package.json",
50
+ "LICENSE",
51
+ "main.js"
52
+ ],
53
+ "scripts": {
54
+ "test:js": "mocha --config test/mocharc.custom.json \"{!(node_modules|test)/**/*.test.js,*.test.js,test/**/test!(PackageFiles|Startup).js}\"",
55
+ "test:package": "mocha test/package --exit",
56
+ "test:integration": "mocha test/integration --exit",
57
+ "test": "npm run test:js && npm run test:package",
58
+ "check": "tsc --noEmit -p tsconfig.check.json",
59
+ "lint": "eslint -c eslint.config.mjs .",
60
+ "translate": "translate-adapter",
61
+ "release": "release-script"
62
+ },
63
+ "bugs": {
64
+ "url": "https://github.com/TA2k/ioBroker.byd/issues"
65
+ },
66
+ "readmeFilename": "README.md"
67
+ }