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/LICENSE +1 -0
- package/README.md +44 -0
- package/io-package.json +38 -4
- package/lib/anthbotApi.js +187 -111
- package/main.js +304 -368
- package/package.json +6 -7
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
this.log.error(
|
|
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
|
-
|
|
104
|
-
|
|
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
|
-
|
|
107
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
146
|
-
|
|
147
|
-
|
|
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(
|
|
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:
|
|
134
|
+
id: goodAreaList,
|
|
156
135
|
});
|
|
157
136
|
ackState = true;
|
|
158
137
|
}
|
|
138
|
+
break;
|
|
159
139
|
}
|
|
160
|
-
break;
|
|
161
|
-
}
|
|
162
140
|
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
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
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
195
|
-
|
|
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
|
-
|
|
216
|
-
|
|
217
|
-
|
|
212
|
+
// Ack command if verified valid above
|
|
213
|
+
if (ackState) {
|
|
214
|
+
await this.setState(id, ackState, true);
|
|
218
215
|
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
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
|
-
|
|
377
|
-
|
|
378
|
-
|
|
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
|
-
|
|
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
|
-
|
|
402
|
-
|
|
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
|
-
|
|
415
|
-
|
|
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
|
-
|
|
429
|
-
|
|
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
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
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
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
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
|
-
|
|
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
|
-
|
|
483
|
-
|
|
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
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
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
|
-
|
|
509
|
-
|
|
402
|
+
// Create objects for device
|
|
403
|
+
async createDeviceObjects(device) {
|
|
404
|
+
await this.setObjectNotExistsAsync(device.sn, {
|
|
405
|
+
type: 'device',
|
|
510
406
|
common: {
|
|
511
|
-
name:
|
|
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
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
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
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
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
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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(
|
|
697
|
-
this.log.error(`Invalid
|
|
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
|
-
//
|
|
702
|
-
|
|
703
|
-
|
|
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
|
|
708
|
-
|
|
709
|
-
for (const
|
|
710
|
-
if (
|
|
711
|
-
continue
|
|
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
|
|
715
|
-
this.log.
|
|
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
|
|
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) {
|