iobroker.anthbot 0.0.3 → 0.0.5

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 CHANGED
@@ -50,7 +50,7 @@ class Anthbot extends utils.Adapter {
50
50
  // Verify we have credentials
51
51
  if (this.config.username == '' || this.config.password == '' || !this.config.regionCode) {
52
52
  this.log.error('Incomplete adapter configuration! Please check settings.');
53
- this.terminate();
53
+ // Don't actually terminate - when the adapter config is updated that will trigger a restart
54
54
  } else {
55
55
  this.loginAndStart();
56
56
  }
@@ -82,143 +82,141 @@ class Anthbot extends utils.Adapter {
82
82
  idParts.pop();
83
83
 
84
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}`);
85
+ if (!serialNumber) {
86
+ this.log.error(`No serial number found in command ${id}`);
89
87
  } 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}`);
88
+ const device = this.devices.find(checkDevice => checkDevice.sn === serialNumber);
89
+
90
+ if (!serialNumber || !device) {
91
+ this.log.error(`Could not find device for command with serial number: ${serialNumber}`);
92
+ } else {
93
+ switch (command) {
94
+ case 'area_set': {
95
+ let customAreas;
96
+ if (typeof state?.val !== 'string') {
97
+ this.log.error('Command custom_areas for ${serialNumber} is not a string');
98
+ } else {
99
+ try {
100
+ customAreas = JSON.parse(state.val);
101
+ } catch (error) {
102
+ this.log.error(`Failed to parse for ${id}: ${error.message}`);
103
+ }
100
104
  }
101
- }
102
105
 
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.
106
+ // Overlay elements from the state onto existing custom areas so user only has to set
107
+ // the items they are changing and rest will be preserved.
105
108
 
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
- }
109
+ // Variable named to match asyncSendServiceCommand data
110
+ const custom_areas = this.validateCustomAreas(device, customAreas);
120
111
 
121
- break;
122
- }
112
+ if (!custom_areas) {
113
+ this.log.error(`Bad area data in ${id}`);
114
+ } else {
115
+ // Write the given custom area data
116
+ this.log.info(`${device.alias}: area_set ${JSON.stringify(customAreas)}`);
117
+ await this.client.asyncSendServiceCommand(serialNumber, 'area_set', {
118
+ custom_areas,
119
+ });
123
120
 
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
- );
121
+ ackState = JSON.stringify(customAreas);
142
122
  }
123
+
124
+ break;
143
125
  }
144
126
 
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 {
127
+ case 'custom_area_mow_start': {
128
+ const goodAreaList = await this.isGoodAreaList(device, `map.custom_areas.raw`);
129
+ if (goodAreaList) {
151
130
  this.log.info(
152
- `${device.alias}: custom_area_mow_start ${JSON.stringify(command_zone_list)}`,
131
+ `${device.alias}: custom_area_mow_start ${JSON.stringify(goodAreaList)}`,
153
132
  );
154
133
  await this.client.asyncSendServiceCommand(serialNumber, 'custom_area_mow_start', {
155
- id: command_zone_list,
134
+ id: goodAreaList,
156
135
  });
157
136
  ackState = true;
158
137
  }
138
+ break;
159
139
  }
160
- break;
161
- }
162
140
 
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}`);
141
+ case 'ridable_mow_start': {
142
+ const goodAreaList = await this.isGoodAreaList(device, `map.ridable_areas.raw`);
143
+ if (goodAreaList) {
144
+ this.log.info(`${device.alias}: ridable_mow_start ${JSON.stringify(goodAreaList)}`);
145
+ await this.client.asyncSendServiceCommand(serialNumber, 'ridable_mow_start', {
146
+ id: goodAreaList,
147
+ });
148
+ ackState = true;
172
149
  }
150
+ break;
151
+ }
173
152
 
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;
153
+ case 'area_list': {
154
+ let areaList;
155
+ // This will affect the next start command only.
156
+ if (typeof state?.val === 'string' && state.val !== '') {
157
+ // Some kind of non-blank value given
158
+ try {
159
+ areaList = JSON.parse(state.val);
160
+ } catch (error) {
161
+ this.log.error(`Failed to parse area list for ${id}: ${error.message}`);
162
+ }
163
+
164
+ // Make sure this is a list & is of valid custom or ridable area IDs
165
+ if (
166
+ !areaList ||
167
+ !Array.isArray(areaList) ||
168
+ !(
169
+ (await this.isGoodAreaList(device, 'map.custom_areas.raw', areaList)) ||
170
+ (await this.isGoodAreaList(device, 'map.ridable_areas.raw', areaList))
171
+ )
172
+ ) {
173
+ // Set to null so we don't ack it
174
+ areaList = null;
175
+ }
176
+ } else {
177
+ // No value given, so ack an empty list
178
+ areaList = [];
179
+ }
180
+
181
+ // Ack only if we now have a list
182
+ if (Array.isArray(areaList)) {
183
+ ackState = JSON.stringify(areaList);
184
+ // We don't need to sync after this as no command was actually sent yet
185
+ doSync = false;
178
186
  }
179
- } else {
180
- // No value given, so ack an empty list
181
- zoneList = [];
182
- }
183
187
 
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;
188
+ break;
189
189
  }
190
190
 
191
- break;
192
- }
191
+ case 'mow_start':
192
+ // To start mowing have to put app_state first.
193
+ await this.client.asyncSendServiceCommand(serialNumber, 'app_state', 1);
194
+ // Purposfully fall through to send the actual command!
195
+
196
+ // Generic one-shot commands
197
+ /* falls through */
198
+ case 'charge_start':
199
+ case 'mow_pause':
200
+ case 'stop_all_tasks': {
201
+ this.log.info(`${device.alias}: ${command}`);
202
+ await this.client.asyncSendServiceCommand(serialNumber, command, 1);
203
+ ackState = true;
204
+ break;
205
+ }
193
206
 
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;
207
+ default:
208
+ this.log.warn(`Unknown command: ${command}`);
208
209
  }
209
-
210
- default:
211
- this.log.warn(`Unknown command: ${command}`);
212
210
  }
213
- }
214
211
 
215
- // Ack command if verified valid above
216
- if (ackState) {
217
- await this.setState(id, ackState, true);
212
+ // Ack command if verified valid above
213
+ if (ackState) {
214
+ await this.setState(id, ackState, true);
218
215
 
219
- // Sync device if no explicitally set not to
220
- if (doSync) {
221
- this.syncDevice(device);
216
+ // Sync device if no explicitally set not to
217
+ if (doSync) {
218
+ this.syncDevice(device);
219
+ }
222
220
  }
223
221
  }
224
222
  }
@@ -261,11 +259,7 @@ class Anthbot extends utils.Adapter {
261
259
 
262
260
  this.log.info('Connecting to Anthbot cloud...');
263
261
  try {
264
- await this.client.asyncLogin({
265
- username: this.config.username,
266
- password: this.config.password,
267
- areaCode: this.config.regionCode,
268
- });
262
+ await this.client.asyncLogin(this.config.username, this.config.password, this.config.regionCode);
269
263
  } catch (error) {
270
264
  this.log.error(`Failed to login to Anthbot cloud: ${error.message}`);
271
265
  await this.retryConnection();
@@ -312,16 +306,6 @@ class Anthbot extends utils.Adapter {
312
306
  // Wait a second for their backend
313
307
  await new Promise(resolve => this.setTimeout(resolve, 1000, null));
314
308
 
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
309
  await this.pollDevice(device);
326
310
  this.pollingInterval = this.setInterval(async () => {
327
311
  this.pollDevice(device);
@@ -351,18 +335,24 @@ class Anthbot extends utils.Adapter {
351
335
  // Poll device
352
336
  async pollDevice(device) {
353
337
  if (this.checkClient()) {
338
+ // Map area ID for checking for changes
339
+ let area_id;
340
+
341
+ // Shadow state
354
342
  try {
355
343
  const shadowState = await this.client.asyncGetShadowReportedState(device.sn);
356
344
  this.log.debug(`Device shadow reported state:\n${JSON.stringify(shadowState)}`);
357
345
  await this.setShadowState(device, shadowState);
346
+ area_id = shadowState.map.area_id;
358
347
  } catch (err) {
359
348
  this.log.error(`Failed to fetch shadow state for device ${device.sn}: ${err.message}`);
360
349
  // TODO: If something goes wrong here, might not be serious, maybe don't do a full reconnect?
361
350
  this.retryConnection();
362
351
  }
363
352
 
353
+ // Code list
364
354
  try {
365
- const codeList = await this.client.asyncGetCodeList(device.sn);
355
+ const codeList = await this.client.asyncGetCodeList(device.sn, 1, 20 /* TODO: make configurable? */);
366
356
  this.log.debug(`Device code list:\n${JSON.stringify(codeList)}`);
367
357
  await this.setCodeList(device, codeList);
368
358
  } catch (err) {
@@ -370,243 +360,150 @@ class Anthbot extends utils.Adapter {
370
360
  // TODO: If something goes wrong here, might not be serious, maybe don't do a full reconnect?
371
361
  this.retryConnection();
372
362
  }
373
- }
374
- }
375
363
 
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
- });
364
+ // Fetch map if changed
365
+ if (area_id != device.area_id) {
366
+ this.log.info(`Device map area_id change detected, fetching new map info`);
385
367
 
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
- });
368
+ const deviceMapFiles = await this.client.asyncGetDeviceMap(device.sn);
400
369
 
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
- });
370
+ const areaSetting = deviceMapFiles['area_setting.json'];
371
+ this.log.debug(`area_setting.json: ${JSON.stringify(areaSetting)}`);
413
372
 
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
- });
373
+ const custom_areas = areaSetting?.content?.custom_areas;
374
+ this.log.debug(`custom_areas: ${JSON.stringify(custom_areas)}`);
427
375
 
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
- });
376
+ // Stash custom_areas in the passed device
377
+ device.customAreas = custom_areas;
441
378
 
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
- });
379
+ // And save the state
380
+ this.setStateChanged(`${device.sn}.map.custom_areas.raw`, {
381
+ val: JSON.stringify(custom_areas),
382
+ ack: true,
383
+ });
454
384
 
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
- });
385
+ // Save ridable areas state
386
+ this.setStateChanged(`${device.sn}.map.ridable_areas.raw`, {
387
+ val: JSON.stringify(areaSetting?.content?.ridable_areas),
388
+ ack: true,
389
+ });
467
390
 
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
- });
391
+ // TODO: plan is to create a channel for each area and store info for them indivicually.
481
392
 
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
- });
393
+ const timeSetting = deviceMapFiles['time_setting.json'];
394
+ this.log.debug(`time_setting.json: ${JSON.stringify(timeSetting)}`);
494
395
 
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
- });
396
+ // And remember which area_id we loaded last
397
+ device.area_id = area_id;
398
+ }
399
+ }
400
+ }
507
401
 
508
- await this.setObjectNotExistsAsync(`${device.sn}.zone_info`, {
509
- type: 'state',
402
+ // Create objects for device
403
+ async createDeviceObjects(device) {
404
+ await this.setObjectNotExistsAsync(device.sn, {
405
+ type: 'device',
510
406
  common: {
511
- name: 'zone_info',
512
- type: 'string',
513
- role: 'json',
514
- desc: 'JSON object with zone information',
515
- read: true,
516
- write: false,
407
+ name: device.alias,
517
408
  },
518
409
  native: {},
519
410
  });
520
411
 
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
- });
412
+ const channels = [
413
+ ['command', 'Commands'],
414
+ ['map', 'Map info'],
415
+ ['map.custom_areas', 'Custom areas, aka. Zones'],
416
+ ['map.ridable_areas', 'Ridable areas, aka. Edges'],
417
+ ];
418
+
419
+ for (const channel of channels) {
420
+ await this.setObjectNotExistsAsync(`${device.sn}.${channel[0]}`, {
421
+ type: 'channel',
422
+ common: {
423
+ name: channel[0],
424
+ desc: channel[1],
425
+ },
426
+ native: {},
427
+ });
428
+ }
582
429
 
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
- });
430
+ const readOnlyStates = [
431
+ // Shadow properties
432
+ ['active_area', 'array', 'info.ids', 'List of areas currently being mowed'],
433
+ ['elec', 'number', 'level.battery', 'Battery level', '%'],
434
+ ['mode', 'string', 'text', 'Current mode'],
435
+ ['mowing_area', 'number', 'value', 'Current mowing area', 'm²'],
436
+ ['mowing_time', 'number', 'time.span', 'Current mowing time', 's'],
437
+ ['rtk_moved', 'boolean', 'sensor.motion', 'RTK movement detected'],
438
+ ['rtk_state', 'boolean', 'sensor', 'RTK state'],
439
+
440
+ // Code list (aka. messages)
441
+ // Full list for 'power users'
442
+ ['code_list', 'string', 'json', 'JSON object with last page of codes'],
443
+ // Last code for simplcity
444
+ ['last_code', 'number', 'value', 'Last code'],
445
+ ['last_code_text', 'string', 'text', 'Last code text'],
446
+ ['last_code_type', 'string', 'text', 'Last code type (e.g. event, error, etc.)'],
447
+
448
+ // Maps
449
+ ['map.area_id', 'string', 'text', 'ID of current map area'],
450
+ ['map.map_area', 'number', 'value', 'Surface area of map', 'm²'],
451
+ ['map.custom_areas.raw', 'string', 'json', 'JSON object with custom area (aka. zone) information'],
452
+ ['map.ridable_areas.raw', 'string', 'json', 'JSON object with ridable area (aka. edge) information'],
453
+ ];
454
+
455
+ for (const state of readOnlyStates) {
456
+ const common = {
457
+ name: state[0],
458
+ type: state[1],
459
+ role: state[2],
460
+ desc: state[3],
461
+ read: true,
462
+ write: false,
463
+ };
464
+ if (state[4]) {
465
+ common.unit = state[4];
466
+ }
467
+ // @ts-expect-error as 'type' below as a plain string doesn't check against ioBroker.CommonType
468
+ await this.setObjectNotExistsAsync(`${device.sn}.${state[0]}`, {
469
+ type: 'state',
470
+ common,
471
+ native: {},
472
+ });
473
+ }
596
474
 
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',
475
+ const commandStates = [
476
+ // Command buttons
477
+ ['mow_start', 'boolean', 'button.start', 'Start global mowing'],
478
+ ['stop_all_tasks', 'boolean', 'button.stop', 'Stop'],
479
+ ['mow_pause', 'boolean', 'button.pause', 'Pause'],
480
+ ['charge_start', 'boolean', 'button', 'Return home/start charging'],
481
+ ['custom_area_mow_start', 'boolean', 'button.start', 'Start custom area (aka. zone) mowing'],
482
+ ['ridable_mow_start', 'boolean', 'button.start', 'Start ridable area (aka. edge) mowing'],
483
+
484
+ // Area list for relevant commands
485
+ ['area_list', 'string', 'info.ids', `Areas for next command (array of IDs, e.g. '[101,120,132]')`],
486
+
487
+ // For 'area_set'
488
+ ['area_set', 'string', 'json', 'JSON object with custom area (aka. zone) information to write'],
489
+ ];
490
+
491
+ for (const state of commandStates) {
492
+ const common = {
493
+ name: state[0],
494
+ type: state[1],
495
+ role: state[2],
496
+ desc: state[3],
605
497
  read: false,
606
498
  write: true,
607
- },
608
- native: {},
609
- });
499
+ };
500
+ // @ts-expect-error as 'type' below as a plain string doesn't check against ioBroker.CommonType
501
+ await this.setObjectNotExistsAsync(`${device.sn}.command.${state[0]}`, {
502
+ type: 'state',
503
+ common,
504
+ native: {},
505
+ });
506
+ }
610
507
  }
611
508
 
612
509
  // Helper function to set shadow state values
@@ -617,15 +514,24 @@ class Anthbot extends utils.Adapter {
617
514
  this.setConnected(false);
618
515
  }
619
516
 
517
+ this.setStateChanged(`${device.sn}.active_area`, {
518
+ val: JSON.stringify(shadowState.active_area.id),
519
+ ack: true,
520
+ });
620
521
  this.setStateChanged(`${device.sn}.elec`, { val: shadowState.elec.value, ack: true });
621
522
  this.setStateChanged(`${device.sn}.mode`, { val: shadowState.mode.value, ack: true });
622
523
  this.setStateChanged(`${device.sn}.mowing_area`, { val: shadowState.mowing_area.value, ack: true });
623
524
  this.setStateChanged(`${device.sn}.mowing_time`, { val: shadowState.mowing_time.value, ack: true });
624
525
  this.setStateChanged(`${device.sn}.rtk_moved`, { val: shadowState.rtk.moved == 1, ack: true });
625
526
  this.setStateChanged(`${device.sn}.rtk_state`, { val: shadowState.rtk.state == 1, ack: true });
527
+
528
+ this.setStateChanged(`${device.sn}.map.area_id`, { val: shadowState.map.area_id, ack: true });
529
+ this.setStateChanged(`${device.sn}.map.map_area`, { val: shadowState.map.map_area, ack: true });
626
530
  }
627
531
 
628
532
  setCodeList(device, codeList) {
533
+ this.setStateChanged(`${device.sn}.code_list`, { val: JSON.stringify(codeList), ack: true });
534
+
629
535
  const lastCode = codeList[0];
630
536
  this.setStateChanged(`${device.sn}.last_code`, { val: lastCode.code, ack: true });
631
537
  this.setStateChanged(`${device.sn}.last_code_text`, { val: lastCode.event_message, ack: true });
@@ -641,7 +547,7 @@ class Anthbot extends utils.Adapter {
641
547
  for (const area of customAreas) {
642
548
  let outArea;
643
549
 
644
- const existingArea = device.zoneList.find(zone => zone.id === area.id);
550
+ const existingArea = device.customAreas.find(customArea => customArea.id === area.id);
645
551
  if (existingArea) {
646
552
  this.log.debug(`Found existing area ${area.id} for merge: ${JSON.stringify(existingArea)}`);
647
553
  outArea = { ...existingArea, ...area };
@@ -691,42 +597,72 @@ class Anthbot extends utils.Adapter {
691
597
  return outputAreas;
692
598
  }
693
599
 
694
- isGoodZoneList(device, zoneList) {
600
+ parseJsonList(jsonString) {
601
+ let out;
602
+ if (typeof jsonString !== 'string') {
603
+ this.log.error('JSON to parse is not a string');
604
+ } else {
605
+ try {
606
+ out = JSON.parse(jsonString);
607
+ } catch (error) {
608
+ this.log.error(`Failed to parse JSON list: ${error.message}`);
609
+ }
610
+ }
611
+
695
612
  // List to check must be an array
696
- if (!Array.isArray(zoneList)) {
697
- this.log.error(`Invalid zone list: not an array`);
613
+ if (!Array.isArray(out)) {
614
+ this.log.error(`Invalid JSON list: not an array`);
615
+ out = undefined;
616
+ }
617
+
618
+ // List to check must have at least one item
619
+ if (out && out.length < 1) {
620
+ this.log.error(`Invalid JSON list: array is empty`);
621
+ out = undefined;
622
+ }
623
+
624
+ return out;
625
+ }
626
+
627
+ /**
628
+ *
629
+ * @param {object} device Device object for the check or specific list
630
+ * @param {string} checkListStateId State ID holding raw JSON list to check against
631
+ * @param {array | null} passedToCheck Optional list to check instead of fetching from command state
632
+ * @returns {Promise<array | false>} List if good, false if not
633
+ */
634
+
635
+ async isGoodAreaList(device, checkListStateId, passedToCheck = null) {
636
+ const listToCheck = passedToCheck
637
+ ? passedToCheck
638
+ : this.parseJsonList((await this.getStateAsync(`${device.sn}.command.area_list`))?.val);
639
+
640
+ if (!listToCheck || !Array.isArray(listToCheck)) {
641
+ this.log.error('Area list to check is not valid');
698
642
  return false;
699
643
  }
700
644
 
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');
645
+ // Get IDs to check against
646
+ const checkAreas = this.parseJsonList((await this.getStateAsync(`${device.sn}.${checkListStateId}`))?.val);
647
+ // If we don't even have custom areas for our device any list is bad
648
+ if (!checkAreas) {
649
+ this.log.error(`Area list invalid: no IDs to check against in ${checkListStateId}`);
704
650
  return false;
705
651
  }
706
652
 
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;
653
+ // Each area in array must be a known custom area ID
654
+ checkArea: for (const areaToCheck of listToCheck) {
655
+ for (const checkArea of checkAreas) {
656
+ if (checkArea.id === areaToCheck) {
657
+ continue checkArea;
712
658
  }
713
659
  }
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`);
660
+ // If we didn't continue, then we didn't find the area ID in our info list, so it's not good
661
+ this.log.warn(`Invalid custom area list: ${areaToCheck} not found in check list ${checkListStateId}`);
716
662
  return false;
717
663
  }
718
664
 
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 });
665
+ return listToCheck;
730
666
  }
731
667
 
732
668
  subscribeToDevice(device) {