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/LICENSE +21 -0
- package/README.md +46 -0
- package/admin/anthbot.png +0 -0
- package/admin/i18n/de.json +5 -0
- package/admin/i18n/en.json +5 -0
- package/admin/i18n/es.json +5 -0
- package/admin/i18n/fr.json +5 -0
- package/admin/i18n/it.json +5 -0
- package/admin/i18n/nl.json +5 -0
- package/admin/i18n/pl.json +5 -0
- package/admin/i18n/pt.json +5 -0
- package/admin/i18n/ru.json +5 -0
- package/admin/i18n/uk.json +5 -0
- package/admin/i18n/zh-cn.json +5 -0
- package/admin/jsonConfig.json +74 -0
- package/io-package.json +107 -0
- package/lib/adapter-config.d.ts +19 -0
- package/lib/anthbotApi.js +885 -0
- package/main.js +765 -0
- package/package.json +70 -0
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
|
+
}
|