iobroker.anthbot 0.0.2

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,765 @@
1
+ 'use strict';
2
+
3
+ /*
4
+ * Created with @iobroker/create-adapter v3.1.2
5
+ */
6
+
7
+ // The adapter-core module gives you access to the core ioBroker functions
8
+ // you need to create an adapter
9
+ const utils = require('@iobroker/adapter-core');
10
+
11
+ // Load your modules here, e.g.:
12
+ const { AnthbotCloudApiClient } = require('./lib/anthbotApi');
13
+ const POLLING_INTERVAL = 60 * 1000; // Poll every 60 seconds
14
+ const CONNECTION_RETRY_INTERVAL = 30 * 1000; // Starting retry interval
15
+ const CONNECTION_RETRY_BACKOFF = 2; // Exponential backoff factor for connection retries
16
+ const CONNECTION_RETRY_MAX_INTERVAL = 30 * 60 * 1000; // Maximum retry interval
17
+
18
+ class Anthbot extends utils.Adapter {
19
+ /**
20
+ * @param {Partial<utils.AdapterOptions>} [options] - Adapter options
21
+ */
22
+ constructor(options) {
23
+ super({
24
+ ...options,
25
+ name: 'anthbot',
26
+ });
27
+ this.on('ready', this.onReady.bind(this));
28
+ this.on('stateChange', this.onStateChange.bind(this));
29
+ this.on('unload', this.onUnload.bind(this));
30
+
31
+ this.devices = [];
32
+ this.client = null;
33
+ this.pollingInterval = null;
34
+ this.retryTimer = null;
35
+ this.currentRetryInterval = CONNECTION_RETRY_INTERVAL;
36
+ }
37
+
38
+ // Set/reset connection
39
+ async setConnected(connected) {
40
+ await this.setState('info.connection', connected, true);
41
+ }
42
+
43
+ /**
44
+ * Is called when databases are connected and adapter received configuration.
45
+ */
46
+ async onReady() {
47
+ // Initialize your adapter here
48
+ await this.setConnected(false);
49
+
50
+ // Verify we have credentials
51
+ if (this.config.username == '' || this.config.password == '' || !this.config.regionCode) {
52
+ this.log.error('Incomplete adapter configuration! Please check settings.');
53
+ this.terminate();
54
+ } else {
55
+ this.loginAndStart();
56
+ }
57
+ }
58
+
59
+ /**
60
+ * Is called if a subscribed state changes
61
+ *
62
+ * @param {string} id - State ID
63
+ * @param {ioBroker.State | null | undefined} state - State object
64
+ */
65
+ async onStateChange(id, state) {
66
+ if (state) {
67
+ if (this.checkClient() && state.ack === false) {
68
+ // This is a command from the user (e.g., from the UI or other adapter)
69
+ // and should be processed by the adapter
70
+ this.log.debug(`Command received for ${id}: ${JSON.stringify(state)}`);
71
+
72
+ // By default, set ackState null so we won't ack this
73
+ let ackState = null;
74
+ // By default sync afer valid command
75
+ let doSync = true;
76
+
77
+ const idParts = id.split('.');
78
+
79
+ const command = idParts.pop();
80
+
81
+ // Remove 'command' string literal
82
+ idParts.pop();
83
+
84
+ const serialNumber = idParts.pop();
85
+ const device = this.devices.find(checkDevice => checkDevice.sn === serialNumber);
86
+
87
+ if (!device) {
88
+ this.log.error(`Could not find device for command with serial number: ${serialNumber}`);
89
+ } else {
90
+ switch (command) {
91
+ case 'area_set': {
92
+ let customAreas;
93
+ if (typeof state?.val !== 'string') {
94
+ this.log.error('Command custom_areas for ${serialNumber} is not a string');
95
+ } else {
96
+ try {
97
+ customAreas = JSON.parse(state.val);
98
+ } catch (error) {
99
+ this.log.error(`Failed to parse for ${id}: ${error.message}`);
100
+ }
101
+ }
102
+
103
+ // Overlay elements from the state onto existing zones so user only has to set
104
+ // the items they are changing and rest will be preserved.
105
+
106
+ // Variable named to match asyncSendServiceCommand data
107
+ const custom_areas = this.validateCustomAreas(device, customAreas);
108
+
109
+ if (!custom_areas) {
110
+ this.log.error(`Bad area data in ${id}`);
111
+ } else {
112
+ // Write the given area (zone) data
113
+ this.log.info(`${device.alias}: area_set ${JSON.stringify(customAreas)}`);
114
+ await this.client.asyncSendServiceCommand(serialNumber, 'area_set', {
115
+ custom_areas,
116
+ });
117
+
118
+ ackState = JSON.stringify(customAreas);
119
+ }
120
+
121
+ break;
122
+ }
123
+
124
+ case 'custom_area_mow_start': {
125
+ // Get/check command zone_list
126
+ // This could be done in one shot, but get the state first for debug logging
127
+ const command_zone_list_state = await this.getStateAsync(`${device.sn}.command.zone_list`);
128
+ this.log.debug(
129
+ `Current command.zone_list state: ${JSON.stringify(command_zone_list_state)}`,
130
+ );
131
+
132
+ let command_zone_list;
133
+ if (typeof command_zone_list_state?.val !== 'string') {
134
+ this.log.error('Command zone list for ${serialNumber} is not a string');
135
+ } else {
136
+ try {
137
+ command_zone_list = JSON.parse(command_zone_list_state.val);
138
+ } catch (error) {
139
+ this.log.error(
140
+ `Failed to parse command zone list for ${serialNumber}: ${error.message}`,
141
+ );
142
+ }
143
+ }
144
+
145
+ if (Array.isArray(command_zone_list) && command_zone_list.length > 0) {
146
+ if (!this.isGoodZoneList(device, command_zone_list)) {
147
+ this.log.error(
148
+ 'Cannot start custom_area_mow_start due to invalid command.zone_list',
149
+ );
150
+ } else {
151
+ this.log.info(
152
+ `${device.alias}: custom_area_mow_start ${JSON.stringify(command_zone_list)}`,
153
+ );
154
+ await this.client.asyncSendServiceCommand(serialNumber, 'custom_area_mow_start', {
155
+ id: command_zone_list,
156
+ });
157
+ ackState = true;
158
+ }
159
+ }
160
+ break;
161
+ }
162
+
163
+ case 'zone_list': {
164
+ let zoneList;
165
+ // This will affect the next start command only.
166
+ if (typeof state?.val === 'string' && state.val !== '') {
167
+ // Some kind of non-blank value given
168
+ try {
169
+ zoneList = JSON.parse(state.val);
170
+ } catch (error) {
171
+ this.log.error(`Failed to parse zone list for ${id}: ${error.message}`);
172
+ }
173
+
174
+ // Make sure all IDs in list are valid
175
+ if (!this.isGoodZoneList(device, zoneList)) {
176
+ // Set to null so we don't ack it
177
+ zoneList = null;
178
+ }
179
+ } else {
180
+ // No value given, so ack an empty list
181
+ zoneList = [];
182
+ }
183
+
184
+ // Ack only if we now have a list
185
+ if (Array.isArray(zoneList)) {
186
+ ackState = JSON.stringify(zoneList);
187
+ // We don't need to sync after this as no command was actually sent yet
188
+ doSync = false;
189
+ }
190
+
191
+ break;
192
+ }
193
+
194
+ case 'mow_start':
195
+ // To start mowing have to put app_state first.
196
+ await this.client.asyncSendServiceCommand(serialNumber, 'app_state', 1);
197
+ // Purposfully fall through to send the actual command!
198
+
199
+ // Generic one-shot commands
200
+ /* falls through */
201
+ case 'charge_start':
202
+ case 'mow_pause':
203
+ case 'stop_all_tasks': {
204
+ this.log.info(`${device.alias}: ${command}`);
205
+ await this.client.asyncSendServiceCommand(serialNumber, command, 1);
206
+ ackState = true;
207
+ break;
208
+ }
209
+
210
+ default:
211
+ this.log.warn(`Unknown command: ${command}`);
212
+ }
213
+ }
214
+
215
+ // Ack command if verified valid above
216
+ if (ackState) {
217
+ await this.setState(id, ackState, true);
218
+
219
+ // Sync device if no explicitally set not to
220
+ if (doSync) {
221
+ this.syncDevice(device);
222
+ }
223
+ }
224
+ }
225
+ } else {
226
+ // The object was deleted or the state value has expired
227
+ this.log.warn(`state ${id} deleted`);
228
+ }
229
+ }
230
+
231
+ // Retry connection with backoff
232
+ async retryConnection() {
233
+ if (this.retryTimer) {
234
+ this.clearTimeout(this.retryTimer);
235
+ this.retryTimer = null;
236
+ }
237
+ this.clearPolling();
238
+ await this.setConnected(false);
239
+ this.client = null;
240
+
241
+ this.log.info(`Setting retry timer for ${this.currentRetryInterval / 1000}s`);
242
+ this.retryTimer = this.setTimeout(() => {
243
+ this.log.debug('Retry timer complete');
244
+ this.retryTimer = null;
245
+ this.loginAndStart();
246
+ }, this.currentRetryInterval);
247
+
248
+ // Backoff for next retry...
249
+ this.currentRetryInterval *= CONNECTION_RETRY_BACKOFF;
250
+
251
+ // ... but never exceed max retry interval
252
+ if (this.currentRetryInterval > CONNECTION_RETRY_MAX_INTERVAL) {
253
+ this.currentRetryInterval = CONNECTION_RETRY_MAX_INTERVAL;
254
+ }
255
+ }
256
+
257
+ // Login & start processing
258
+ async loginAndStart() {
259
+ // Login
260
+ this.client = new AnthbotCloudApiClient({ verboseLogger: this.log.debug });
261
+
262
+ this.log.info('Connecting to Anthbot cloud...');
263
+ try {
264
+ await this.client.asyncLogin({
265
+ username: this.config.username,
266
+ password: this.config.password,
267
+ areaCode: this.config.regionCode,
268
+ });
269
+ } catch (error) {
270
+ this.log.error(`Failed to login to Anthbot cloud: ${error.message}`);
271
+ await this.retryConnection();
272
+ return;
273
+ }
274
+
275
+ this.log.debug('Login successful');
276
+
277
+ this.log.debug('Searching for bound devices...');
278
+ this.devices = [];
279
+ try {
280
+ this.devices = await this.client.asyncGetBoundDevices();
281
+ } catch (error) {
282
+ this.log.error(`Failed to fetch bound devices: ${error.message}`);
283
+ await this.retryConnection();
284
+ return;
285
+ }
286
+ this.log.debug(`Found devices: ${JSON.stringify(this.devices)}`);
287
+
288
+ if (this.devices.length === 0) {
289
+ this.log.error('No bound devices found! Please check your Anthbot cloud account.');
290
+ await this.retryConnection();
291
+ return;
292
+ }
293
+
294
+ // Things look pretty good here, so reset the retry interval.
295
+ this.currentRetryInterval = CONNECTION_RETRY_INTERVAL;
296
+
297
+ // TODO: handle multiple devices (currently we just connect to the first one)
298
+ const device = this.devices[0];
299
+ this.log.info(`Connecting to ${device.alias} (${device.sn})`);
300
+ await this.createDeviceObjects(device);
301
+ this.subscribeToDevice(device);
302
+
303
+ this.syncDevice(device);
304
+ }
305
+
306
+ async syncDevice(device) {
307
+ if (this.checkClient()) {
308
+ // Reset polling interval on sync
309
+ this.clearPolling();
310
+
311
+ await this.client.asyncSendServiceCommand(device.sn, 'get_all_props', 1);
312
+ // Wait a second for their backend
313
+ await new Promise(resolve => this.setTimeout(resolve, 1000, null));
314
+
315
+ // TODO: figure out how to tell when map changes and reload periodically?
316
+ const deviceMapFiles = await this.client.asyncGetDeviceMap(device.sn);
317
+
318
+ const areaSetting = deviceMapFiles['area_setting.json'];
319
+ this.log.debug(`area_setting.json: ${JSON.stringify(areaSetting)}`);
320
+ this.setZoneInfo(device, areaSetting?.content?.custom_areas);
321
+
322
+ const timeSetting = deviceMapFiles['time_setting.json'];
323
+ this.log.debug(`time_setting.json: ${JSON.stringify(timeSetting)}`);
324
+
325
+ await this.pollDevice(device);
326
+ this.pollingInterval = this.setInterval(async () => {
327
+ this.pollDevice(device);
328
+ }, POLLING_INTERVAL);
329
+ }
330
+ }
331
+
332
+ clearPolling() {
333
+ if (this.pollingInterval) {
334
+ this.clearInterval(this.pollingInterval);
335
+ this.pollingInterval = null;
336
+ }
337
+ }
338
+
339
+ /**
340
+ * @returns {this is { client: { Object } }} this.client is an object
341
+ */
342
+ checkClient() {
343
+ if (!this.client || typeof this.client !== 'object') {
344
+ this.log.warn('No API client available!');
345
+ this.retryConnection();
346
+ return false;
347
+ }
348
+ return true;
349
+ }
350
+
351
+ // Poll device
352
+ async pollDevice(device) {
353
+ if (this.checkClient()) {
354
+ try {
355
+ const shadowState = await this.client.asyncGetShadowReportedState(device.sn);
356
+ this.log.debug(`Device shadow reported state:\n${JSON.stringify(shadowState)}`);
357
+ await this.setShadowState(device, shadowState);
358
+ } catch (err) {
359
+ this.log.error(`Failed to fetch shadow state for device ${device.sn}: ${err.message}`);
360
+ // TODO: If something goes wrong here, might not be serious, maybe don't do a full reconnect?
361
+ this.retryConnection();
362
+ }
363
+
364
+ try {
365
+ const codeList = await this.client.asyncGetCodeList(device.sn);
366
+ this.log.debug(`Device code list:\n${JSON.stringify(codeList)}`);
367
+ await this.setCodeList(device, codeList);
368
+ } catch (err) {
369
+ this.log.error(`Failed to fetch code list for device ${device.sn}: ${err.message}`);
370
+ // TODO: If something goes wrong here, might not be serious, maybe don't do a full reconnect?
371
+ this.retryConnection();
372
+ }
373
+ }
374
+ }
375
+
376
+ // Create objects for device
377
+ async createDeviceObjects(device) {
378
+ await this.setObjectNotExistsAsync(device.sn, {
379
+ type: 'device',
380
+ common: {
381
+ name: device.alias,
382
+ },
383
+ native: {},
384
+ });
385
+
386
+ // Shadow properties...
387
+ await this.setObjectNotExistsAsync(`${device.sn}.elec`, {
388
+ type: 'state',
389
+ common: {
390
+ name: 'elec',
391
+ type: 'number',
392
+ unit: '%',
393
+ desc: 'Battery level',
394
+ role: 'level.battery',
395
+ read: true,
396
+ write: false,
397
+ },
398
+ native: {},
399
+ });
400
+
401
+ await this.setObjectNotExistsAsync(`${device.sn}.mode`, {
402
+ type: 'state',
403
+ common: {
404
+ name: 'mode',
405
+ type: 'string',
406
+ role: 'text',
407
+ desc: 'Current mode',
408
+ read: true,
409
+ write: false,
410
+ },
411
+ native: {},
412
+ });
413
+
414
+ await this.setObjectNotExistsAsync(`${device.sn}.mowing_area`, {
415
+ type: 'state',
416
+ common: {
417
+ name: 'mowing_area',
418
+ type: 'number',
419
+ unit: 'm²',
420
+ desc: 'Current mowing area',
421
+ role: 'value',
422
+ read: true,
423
+ write: false,
424
+ },
425
+ native: {},
426
+ });
427
+
428
+ await this.setObjectNotExistsAsync(`${device.sn}.mowing_time`, {
429
+ type: 'state',
430
+ common: {
431
+ name: 'mowing_time',
432
+ type: 'number',
433
+ unit: 's',
434
+ role: 'time.span',
435
+ desc: 'Current mowing time',
436
+ read: true,
437
+ write: false,
438
+ },
439
+ native: {},
440
+ });
441
+
442
+ await this.setObjectNotExistsAsync(`${device.sn}.rtk_moved`, {
443
+ type: 'state',
444
+ common: {
445
+ name: 'rtk_moved',
446
+ type: 'boolean',
447
+ role: 'sensor.motion',
448
+ desc: 'RTK movement detected',
449
+ read: true,
450
+ write: false,
451
+ },
452
+ native: {},
453
+ });
454
+
455
+ await this.setObjectNotExistsAsync(`${device.sn}.rtk_state`, {
456
+ type: 'state',
457
+ common: {
458
+ name: 'rtk_state',
459
+ type: 'boolean',
460
+ role: 'sensor',
461
+ desc: 'RTK state',
462
+ read: true,
463
+ write: false,
464
+ },
465
+ native: {},
466
+ });
467
+
468
+ // Code list (aka. messages)
469
+ await this.setObjectNotExistsAsync(`${device.sn}.last_code`, {
470
+ type: 'state',
471
+ common: {
472
+ name: 'last_code',
473
+ type: 'number',
474
+ desc: 'Last code',
475
+ role: 'value',
476
+ read: true,
477
+ write: false,
478
+ },
479
+ native: {},
480
+ });
481
+
482
+ await this.setObjectNotExistsAsync(`${device.sn}.last_code_text`, {
483
+ type: 'state',
484
+ common: {
485
+ name: 'last_code_text',
486
+ type: 'string',
487
+ role: 'text',
488
+ desc: 'Last code text',
489
+ read: true,
490
+ write: false,
491
+ },
492
+ native: {},
493
+ });
494
+
495
+ await this.setObjectNotExistsAsync(`${device.sn}.last_code_type`, {
496
+ type: 'state',
497
+ common: {
498
+ name: 'last_code_type',
499
+ type: 'string',
500
+ role: 'text',
501
+ desc: 'Last code type (e.g. event, error, etc.)',
502
+ read: true,
503
+ write: false,
504
+ },
505
+ native: {},
506
+ });
507
+
508
+ await this.setObjectNotExistsAsync(`${device.sn}.zone_info`, {
509
+ type: 'state',
510
+ common: {
511
+ name: 'zone_info',
512
+ type: 'string',
513
+ role: 'json',
514
+ desc: 'JSON object with zone information',
515
+ read: true,
516
+ write: false,
517
+ },
518
+ native: {},
519
+ });
520
+
521
+ // Command buttons
522
+ await this.setObjectNotExistsAsync(`${device.sn}.command.custom_area_mow_start`, {
523
+ type: 'state',
524
+ common: {
525
+ name: 'start',
526
+ type: 'boolean',
527
+ role: 'button.start',
528
+ desc: 'Start zone mowing',
529
+ read: false,
530
+ write: true,
531
+ },
532
+ native: {},
533
+ });
534
+ await this.setObjectNotExistsAsync(`${device.sn}.command.mow_start`, {
535
+ type: 'state',
536
+ common: {
537
+ name: 'start',
538
+ type: 'boolean',
539
+ role: 'button.start',
540
+ desc: 'Start global mowing',
541
+ read: false,
542
+ write: true,
543
+ },
544
+ native: {},
545
+ });
546
+ await this.setObjectNotExistsAsync(`${device.sn}.command.stop_all_tasks`, {
547
+ type: 'state',
548
+ common: {
549
+ name: 'stop',
550
+ type: 'boolean',
551
+ role: 'button.stop',
552
+ desc: 'Stop',
553
+ read: false,
554
+ write: true,
555
+ },
556
+ native: {},
557
+ });
558
+ await this.setObjectNotExistsAsync(`${device.sn}.command.mow_pause`, {
559
+ type: 'state',
560
+ common: {
561
+ name: 'pause',
562
+ type: 'boolean',
563
+ role: 'button.pause',
564
+ desc: 'Pause',
565
+ read: false,
566
+ write: true,
567
+ },
568
+ native: {},
569
+ });
570
+ await this.setObjectNotExistsAsync(`${device.sn}.command.charge_start`, {
571
+ type: 'state',
572
+ common: {
573
+ name: 'home',
574
+ type: 'boolean',
575
+ role: 'button',
576
+ desc: 'Return home/start charging',
577
+ read: false,
578
+ write: true,
579
+ },
580
+ native: {},
581
+ });
582
+
583
+ // Zone list for relevant commands
584
+ await this.setObjectNotExistsAsync(`${device.sn}.command.zone_list`, {
585
+ type: 'state',
586
+ common: {
587
+ name: 'zone_list',
588
+ type: 'array',
589
+ role: 'info.ids',
590
+ desc: `Zone list for next command (array of zone IDs, e.g. '[101,120,132]')`,
591
+ read: false,
592
+ write: true,
593
+ },
594
+ native: {},
595
+ });
596
+
597
+ // For 'area_set'
598
+ await this.setObjectNotExistsAsync(`${device.sn}.command.area_set`, {
599
+ type: 'state',
600
+ common: {
601
+ name: 'area_set',
602
+ type: 'string',
603
+ role: 'json',
604
+ desc: 'JSON object with zone information to write',
605
+ read: false,
606
+ write: true,
607
+ },
608
+ native: {},
609
+ });
610
+ }
611
+
612
+ // Helper function to set shadow state values
613
+ setShadowState(device, shadowState) {
614
+ if (shadowState.online.value) {
615
+ this.setConnected(true);
616
+ } else {
617
+ this.setConnected(false);
618
+ }
619
+
620
+ this.setStateChanged(`${device.sn}.elec`, { val: shadowState.elec.value, ack: true });
621
+ this.setStateChanged(`${device.sn}.mode`, { val: shadowState.mode.value, ack: true });
622
+ this.setStateChanged(`${device.sn}.mowing_area`, { val: shadowState.mowing_area.value, ack: true });
623
+ this.setStateChanged(`${device.sn}.mowing_time`, { val: shadowState.mowing_time.value, ack: true });
624
+ this.setStateChanged(`${device.sn}.rtk_moved`, { val: shadowState.rtk.moved == 1, ack: true });
625
+ this.setStateChanged(`${device.sn}.rtk_state`, { val: shadowState.rtk.state == 1, ack: true });
626
+ }
627
+
628
+ setCodeList(device, codeList) {
629
+ const lastCode = codeList[0];
630
+ this.setStateChanged(`${device.sn}.last_code`, { val: lastCode.code, ack: true });
631
+ this.setStateChanged(`${device.sn}.last_code_text`, { val: lastCode.event_message, ack: true });
632
+ this.setStateChanged(`${device.sn}.last_code_type`, { val: lastCode.code_type, ack: true });
633
+ }
634
+
635
+ validateCustomAreas(device, customAreas) {
636
+ const outputAreas = [];
637
+
638
+ if (!Array.isArray(customAreas)) {
639
+ this.log.error(`Invalid customAreas: not an array`);
640
+ } else {
641
+ for (const area of customAreas) {
642
+ let outArea;
643
+
644
+ const existingArea = device.zoneList.find(zone => zone.id === area.id);
645
+ if (existingArea) {
646
+ this.log.debug(`Found existing area ${area.id} for merge: ${JSON.stringify(existingArea)}`);
647
+ outArea = { ...existingArea, ...area };
648
+ this.log.debug(`After merge ${area.id} is: ${JSON.stringify(outArea)}`);
649
+ } else {
650
+ outArea = area;
651
+ }
652
+
653
+ // Assume area is good
654
+ let isGood = true;
655
+ if (typeof outArea.id !== 'number' || typeof outArea.name !== 'string') {
656
+ // Must have ID & name (I'm guessing)
657
+ this.log.error('Invalid custom area: id or name are bad/missing');
658
+ isGood = false;
659
+ } else if (!Array.isArray(outArea.vertexs) || outArea.vertexs.length != 4) {
660
+ // vertexs must be an array of 4 co-ordinates or the Anthbot app will crash!
661
+ this.log.error('Invalid custom area: vertexs is not an array of 4 items');
662
+ isGood = false;
663
+ } else {
664
+ let goodVertexs = 0;
665
+ for (const vertex of outArea.vertexs) {
666
+ if (
667
+ !Array.isArray(vertex) ||
668
+ vertex.length != 2 ||
669
+ typeof vertex[0] !== 'number' ||
670
+ typeof vertex[1] !== 'number'
671
+ ) {
672
+ this.log.error('Invalid custom area: vertex is not a co-ordinate');
673
+ break;
674
+ } else {
675
+ goodVertexs++;
676
+ }
677
+ }
678
+ if (goodVertexs != 4) {
679
+ isGood = false;
680
+ }
681
+ }
682
+
683
+ if (!isGood) {
684
+ // Something wrong with this area so return nothing
685
+ return;
686
+ }
687
+ outputAreas.push(outArea);
688
+ }
689
+ }
690
+
691
+ return outputAreas;
692
+ }
693
+
694
+ isGoodZoneList(device, zoneList) {
695
+ // List to check must be an array
696
+ if (!Array.isArray(zoneList)) {
697
+ this.log.error(`Invalid zone list: not an array`);
698
+ return false;
699
+ }
700
+
701
+ // If we don't even have zones for our device any list is bad
702
+ if (!device.zoneList) {
703
+ this.log.error('Invalid zone list: device has no zone info');
704
+ return false;
705
+ }
706
+
707
+ // Each zone in array must be a known zone ID
708
+ checkZone: for (const zoneId of zoneList) {
709
+ for (const zone of device.zoneList) {
710
+ if (zone.id === zoneId) {
711
+ continue checkZone;
712
+ }
713
+ }
714
+ // If we didn't continue, then we didn't find the zoneId in our info list, so it's not good
715
+ this.log.error(`Invalid zone list: ${zoneId} not found in device info`);
716
+ return false;
717
+ }
718
+
719
+ return true;
720
+ }
721
+
722
+ setZoneInfo(device, zoneInfo) {
723
+ this.log.debug(`zone_info for ${device.sn}: ${JSON.stringify(zoneInfo)}`);
724
+
725
+ // Stash in the passed device
726
+ device.zoneList = zoneInfo;
727
+
728
+ // And save the state
729
+ this.setStateChanged(`${device.sn}.zone_info`, { val: JSON.stringify(zoneInfo), ack: true });
730
+ }
731
+
732
+ subscribeToDevice(device) {
733
+ this.log.debug(`Subscribing to command states for ${device.sn}`);
734
+ this.subscribeStates(`${device.sn}.command.*`);
735
+ }
736
+
737
+ /**
738
+ * Is called when adapter shuts down - callback has to be called under any circumstances!
739
+ *
740
+ * @param {() => void} callback - Callback function
741
+ */
742
+ onUnload(callback) {
743
+ try {
744
+ this.unsubscribeStates('*');
745
+ this.clearPolling();
746
+ this.setConnected(false).then(() => {
747
+ callback();
748
+ });
749
+ } catch (error) {
750
+ this.log.error(`Error during unloading: ${error.message}`);
751
+ callback();
752
+ }
753
+ }
754
+ }
755
+
756
+ if (require.main !== module) {
757
+ // Export the constructor in compact mode
758
+ /**
759
+ * @param {Partial<utils.AdapterOptions>} [options] - Adapter options
760
+ */
761
+ module.exports = options => new Anthbot(options);
762
+ } else {
763
+ // otherwise start the instance directly
764
+ new Anthbot();
765
+ }