iobroker.anthbot 0.0.4 → 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/README.md +37 -0
- package/io-package.json +14 -1
- package/lib/anthbotApi.js +44 -91
- package/main.js +233 -303
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -14,11 +14,48 @@
|
|
|
14
14
|
|
|
15
15
|
Connect with Anthbot devices such as their robot mowers.
|
|
16
16
|
|
|
17
|
+
### Monitoring
|
|
18
|
+
|
|
19
|
+
Battery level is reported in the `elec` state.
|
|
20
|
+
|
|
21
|
+
View a device's status in the `mode` state (charging, mowing, standby, etc).
|
|
22
|
+
|
|
23
|
+
The last status message & it's severity (event, error, etc) are shown in the `last_code`, `last_code_text` and `last_code_type` states. For users looking for more history, the `code_list` state holds a JSON array with the a larger number of messages.
|
|
24
|
+
|
|
25
|
+
The rest of the states should be self explanatory.
|
|
26
|
+
|
|
27
|
+
### Commands
|
|
28
|
+
|
|
29
|
+
`stop_all_tasks` equates to hiting 'Stop' in the Anthbot app.
|
|
30
|
+
|
|
31
|
+
`charge_start` equates to the 'Recharge' icon in the Anthbot app.
|
|
32
|
+
|
|
33
|
+
`mow_start` equates to start when in 'Full maps' mode.
|
|
34
|
+
|
|
35
|
+
`custom_area_mow_start` equates to custom area (aka 'Zones') mode. For this to work a valid list of area IDs must already be set in the `area_list` state. Area IDs are not the same as names the Anthbot app shows. Valid area IDs can be found in the `map.custom_areas.raw` state (this will be improved later).
|
|
36
|
+
|
|
37
|
+
Ie. to start mowing one or more zones:
|
|
38
|
+
|
|
39
|
+
- Set the `area_list` state to an array of IDs. Eg: `[102, 117]`
|
|
40
|
+
- Trigger the `custom_area_mow_start` state.
|
|
41
|
+
|
|
42
|
+
`ridable_mow_start` equates to edge mowing mode. As with `custom_area_mow_start`, set the `area_list` with a valid list of ridable areas (aka. edge) IDs. Valid ridable area IDs can be found in the `map.ridable_areas.raw` state.
|
|
43
|
+
|
|
44
|
+
### Map & area (aka. zone) editing
|
|
45
|
+
|
|
46
|
+
With `area_set` it is possible to edit one or more areas. Take the JSON representation from a desired entry from the `map.custom_areas.raw_list`, modify it as required and save this the `area_set` state as a JSON array.
|
|
47
|
+
|
|
48
|
+
Note that when using `area_set` it is not necessary to define all parameters and only those provided will be changed. Eg: `[{"mow_head":10,"id":117}]` will change the angle of mowing in area 117 to 10 degrees and leave the other parameters as is.
|
|
49
|
+
|
|
17
50
|
## Changelog
|
|
18
51
|
<!--
|
|
19
52
|
Placeholder for the next version (at the beginning of the line):
|
|
20
53
|
### **WORK IN PROGRESS**
|
|
21
54
|
-->
|
|
55
|
+
### 0.0.5 (2026-05-20)
|
|
56
|
+
- (raintonr) Added brief usage tips in readme
|
|
57
|
+
- (raintonr) Added active_area, code_list & map states and ridable_mow_start command
|
|
58
|
+
|
|
22
59
|
### 0.0.4 (2026-05-18)
|
|
23
60
|
- (copilot) Adapter requires node.js >= 22 now
|
|
24
61
|
- (copilot) Adapter requires admin >= 7.7.22 now
|
package/io-package.json
CHANGED
|
@@ -1,8 +1,21 @@
|
|
|
1
1
|
{
|
|
2
2
|
"common": {
|
|
3
3
|
"name": "anthbot",
|
|
4
|
-
"version": "0.0.
|
|
4
|
+
"version": "0.0.5",
|
|
5
5
|
"news": {
|
|
6
|
+
"0.0.5": {
|
|
7
|
+
"en": "Added brief usage tips in readme\nAdded active_area, code_list & map states and ridable_mow_start command",
|
|
8
|
+
"de": "Kurze Nutzungstipps in readme hinzugefügt\nActive area, code list & map Zustände und ridable mow start Befehl hinzugefügt",
|
|
9
|
+
"ru": "Добавлены краткие советы по использованию в Readme\nДобавлено состояние active area, code list & map и команда ridable mow start",
|
|
10
|
+
"pt": "Adicionado breves dicas de uso no readme\nAdicionado ative area, code list & map states and ridable mow start command",
|
|
11
|
+
"nl": "Korte gebruik tips toegevoegd in readme\nActive area, code list & map states en ridable mow start commando toegevoegd",
|
|
12
|
+
"fr": "Ajout de brèves conseils d'utilisation dans readme\nAjout de la commande active area, code list & map et ridable mow start",
|
|
13
|
+
"it": "Aggiunto breve utilizzo suggerimenti in readme\nAggiunto active area, code list & mappa stati e comando ridable mow start",
|
|
14
|
+
"es": "Consejos de uso breves adicionales en el readme\nAñadido active area, code list & map states and ridable mow start command",
|
|
15
|
+
"pl": "Dodano krótkie wskazówki użycia w readme\nDodano active _ area, code _ list & map states oraz polecenie ridable _ mow _ start",
|
|
16
|
+
"uk": "Додайте поради щодо короткого використання в читме\nДодано активний area, code list & map States і ridable mow start команди",
|
|
17
|
+
"zh-cn": "在readme中添加简短的使用提示\n添加活动区域、 代码列表状态和可清除的 mow start 命令"
|
|
18
|
+
},
|
|
6
19
|
"0.0.4": {
|
|
7
20
|
"en": "Adapter requires node.js >= 22 now\nAdapter requires admin >= 7.7.22 now\nHandle temporary IoT access tokens (#9)",
|
|
8
21
|
"de": "Adapter benötigt node.js >= 22 jetzt\nAdapter benötigt admin >= 7.7.22 jetzt\nHandle temporäre IoT-Zugriffstoken (#9)",
|
package/lib/anthbotApi.js
CHANGED
|
@@ -138,7 +138,6 @@ class AnthbotCloudApiClient {
|
|
|
138
138
|
* @param {string} username - Username
|
|
139
139
|
* @param {string} password - Password
|
|
140
140
|
* @param {number} areaCode - Country/region code
|
|
141
|
-
* @returns {Promise<string>} Bearer token
|
|
142
141
|
*/
|
|
143
142
|
async asyncLogin(username, password, areaCode) {
|
|
144
143
|
const url = `https://${this.endpointHost}/api/v1/login`;
|
|
@@ -187,16 +186,48 @@ class AnthbotCloudApiClient {
|
|
|
187
186
|
const bearerToken = `Bearer ${accessToken}`;
|
|
188
187
|
this.bearerToken = bearerToken;
|
|
189
188
|
this.authHeaders['Authorization'] = bearerToken;
|
|
190
|
-
return bearerToken;
|
|
191
189
|
}
|
|
192
190
|
|
|
193
191
|
/**
|
|
194
|
-
*
|
|
192
|
+
* Fetch JSON and resolve the service-level payload data.
|
|
193
|
+
*
|
|
194
|
+
* @param {string} url URL to fetch from
|
|
195
|
+
* @param {RequestInit} [options] Request options (method, headers, body, etc.)
|
|
196
|
+
* @returns {Promise<any>} Payload data from the response
|
|
195
197
|
*/
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
198
|
+
async fetchPayloadData(url, options = {}) {
|
|
199
|
+
// GET is default
|
|
200
|
+
if (!options.method) {
|
|
201
|
+
options.method = 'GET';
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// Always add our auth headers
|
|
205
|
+
options.headers = { ...this.authHeaders, ...options.headers };
|
|
206
|
+
|
|
207
|
+
// Check headers given (or added above) have the Authorization (sic.)
|
|
208
|
+
if (!Object.prototype.hasOwnProperty.call(options.headers, 'Authorization')) {
|
|
209
|
+
throw new Error('Missing Authorization header');
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const response = await this.fetch(url, options);
|
|
213
|
+
if (response.status !== 200) {
|
|
214
|
+
const body = await response.text();
|
|
215
|
+
throw new Error(`Request to ${url} failed (${response.status}): ${body.slice(0, 300)}`);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
let payload;
|
|
219
|
+
try {
|
|
220
|
+
payload = /** @type {{code: number, data?: any}} */ (await response.json());
|
|
221
|
+
} catch {
|
|
222
|
+
throw new Error(`Invalid JSON response from ${url}`);
|
|
199
223
|
}
|
|
224
|
+
|
|
225
|
+
if (typeof payload !== 'object' || payload === null) {
|
|
226
|
+
throw new Error(`Invalid API payload from ${url}`);
|
|
227
|
+
} else if (payload.code !== 0) {
|
|
228
|
+
throw new Error(`API returned code=${payload.code} from ${url}`);
|
|
229
|
+
}
|
|
230
|
+
return payload.data;
|
|
200
231
|
}
|
|
201
232
|
|
|
202
233
|
/**
|
|
@@ -205,19 +236,9 @@ class AnthbotCloudApiClient {
|
|
|
205
236
|
* @returns {Promise<{alias: string, sn: string}[]>} - List of devices
|
|
206
237
|
*/
|
|
207
238
|
async asyncGetBoundDevices() {
|
|
208
|
-
this.checkToken();
|
|
209
|
-
|
|
210
239
|
const url = `https://${this.endpointHost}/api/v1/device/bind/list`;
|
|
211
|
-
const response = await this.fetch(url, {
|
|
212
|
-
method: 'GET',
|
|
213
|
-
headers: this.authHeaders,
|
|
214
|
-
});
|
|
215
240
|
|
|
216
|
-
|
|
217
|
-
throw new Error(`Request to ${url} failed, response ${response.status}`);
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
return /** @type {{data?: any}} */ (await response.json()).data;
|
|
241
|
+
return /** @type {{alias: string, sn: string}[]} */ (await this.fetchPayloadData(url));
|
|
221
242
|
}
|
|
222
243
|
|
|
223
244
|
/**
|
|
@@ -230,21 +251,10 @@ class AnthbotCloudApiClient {
|
|
|
230
251
|
* @returns {Promise<unknown>} Latest messages
|
|
231
252
|
*/
|
|
232
253
|
async asyncGetCodeList(serialNumber, pageNum = 1, pageSize = 1) {
|
|
233
|
-
this.checkToken();
|
|
234
|
-
|
|
235
254
|
// TODO: allow language other than English?
|
|
236
255
|
const url = `https://${this.endpointHost}/api/v1/device/v2/code/list?sn=${serialNumber}&pagenum=${pageNum}&pagesize=${pageSize}&language=English`;
|
|
237
256
|
|
|
238
|
-
|
|
239
|
-
method: 'GET',
|
|
240
|
-
headers: this.authHeaders,
|
|
241
|
-
});
|
|
242
|
-
|
|
243
|
-
if (response.status !== 200) {
|
|
244
|
-
throw new Error(`Request to ${url} failed, response ${response.status}`);
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
return /** @type {{data?: any}} */ (await response.json())?.data?.data;
|
|
257
|
+
return /** @type {unknown} */ ((await this.fetchPayloadData(url))?.data);
|
|
248
258
|
}
|
|
249
259
|
|
|
250
260
|
/**
|
|
@@ -270,8 +280,6 @@ class AnthbotCloudApiClient {
|
|
|
270
280
|
* @returns {Promise<string>} - URL for the requested file
|
|
271
281
|
*/
|
|
272
282
|
async asyncGetPresignedUrl(serialNumber, filename, category, sub_category) {
|
|
273
|
-
this.checkToken();
|
|
274
|
-
|
|
275
283
|
const params = new URLSearchParams({
|
|
276
284
|
filename,
|
|
277
285
|
sn: serialNumber,
|
|
@@ -280,16 +288,8 @@ class AnthbotCloudApiClient {
|
|
|
280
288
|
verification_token: this.buildVerificationToken(serialNumber),
|
|
281
289
|
});
|
|
282
290
|
const url = `https://${this.endpointHost}/api/v1/device/v2/presigned_url?${params}`;
|
|
283
|
-
const response = await this.fetch(url, {
|
|
284
|
-
method: 'GET',
|
|
285
|
-
headers: this.authHeaders,
|
|
286
|
-
});
|
|
287
291
|
|
|
288
|
-
|
|
289
|
-
throw new Error(`Request to ${url} failed, response ${response.status}`);
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
return /** @type {{data?: any}} */ (await response.json()).data?.presigned_url;
|
|
292
|
+
return /** @type {string} */ ((await this.fetchPayloadData(url))?.presigned_url);
|
|
293
293
|
}
|
|
294
294
|
|
|
295
295
|
/**
|
|
@@ -394,35 +394,10 @@ class AnthbotCloudApiClient {
|
|
|
394
394
|
this.verboseLogger(`Cache miss - fetching device region for ${serialNumber}`);
|
|
395
395
|
}
|
|
396
396
|
|
|
397
|
-
this.checkToken();
|
|
398
|
-
|
|
399
397
|
const url = `https://${this.endpointHost}/api/v1/device/v2/region`;
|
|
400
398
|
const params = new URLSearchParams({ sn: serialNumber });
|
|
401
|
-
const
|
|
402
|
-
method: 'GET',
|
|
403
|
-
headers: this.authHeaders,
|
|
404
|
-
});
|
|
405
|
-
|
|
406
|
-
if (response.status !== 200) {
|
|
407
|
-
const body = await response.text();
|
|
408
|
-
throw new Error(`Device region failed (${response.status}): ${body.slice(0, 300)}`);
|
|
409
|
-
}
|
|
399
|
+
const data = await this.fetchPayloadData(`${url}?${params}`);
|
|
410
400
|
|
|
411
|
-
let payload;
|
|
412
|
-
try {
|
|
413
|
-
payload = /** @type {{code: number, data?: any}} */ (await response.json());
|
|
414
|
-
} catch {
|
|
415
|
-
throw new Error('Invalid JSON response from device region');
|
|
416
|
-
}
|
|
417
|
-
|
|
418
|
-
if (typeof payload !== 'object' || payload === null) {
|
|
419
|
-
throw new Error('Invalid device region payload type');
|
|
420
|
-
}
|
|
421
|
-
if (payload.code !== 0) {
|
|
422
|
-
throw new Error(`Device region returned code=${payload.code}`);
|
|
423
|
-
}
|
|
424
|
-
|
|
425
|
-
const data = payload.data;
|
|
426
401
|
if (typeof data !== 'object' || data === null) {
|
|
427
402
|
throw new Error('Device region payload missing data object');
|
|
428
403
|
}
|
|
@@ -450,39 +425,17 @@ class AnthbotCloudApiClient {
|
|
|
450
425
|
* @returns {Promise<object>} - IoT credentials and expiration
|
|
451
426
|
*/
|
|
452
427
|
async asyncGetDeviceIotCredentials(serialNumber) {
|
|
453
|
-
this.checkToken();
|
|
454
|
-
|
|
455
428
|
const params = {
|
|
456
429
|
sn: serialNumber,
|
|
457
430
|
verification_token: this.buildVerificationToken(serialNumber),
|
|
458
431
|
};
|
|
459
432
|
|
|
460
|
-
const
|
|
433
|
+
const data = await this.fetchPayloadData(`https://${this.endpointHost}/api/v1/device/v2/iot/sts/arn`, {
|
|
461
434
|
method: 'POST',
|
|
462
|
-
headers: {
|
|
435
|
+
headers: { 'content-type': 'application/json' },
|
|
463
436
|
body: JSON.stringify(params),
|
|
464
437
|
});
|
|
465
438
|
|
|
466
|
-
if (response.status !== 200) {
|
|
467
|
-
const body = await response.text();
|
|
468
|
-
throw new Error(`IoT credentials failed (${response.status}): ${body.slice(0, 300)}`);
|
|
469
|
-
}
|
|
470
|
-
|
|
471
|
-
let payload;
|
|
472
|
-
try {
|
|
473
|
-
payload = /** @type {{code: number, data?: any}} */ (await response.json());
|
|
474
|
-
} catch {
|
|
475
|
-
throw new Error('Invalid JSON response from IoT credentials');
|
|
476
|
-
}
|
|
477
|
-
|
|
478
|
-
if (typeof payload !== 'object' || payload === null) {
|
|
479
|
-
throw new Error('Invalid IoT credentials payload type');
|
|
480
|
-
}
|
|
481
|
-
if (payload.code !== 0) {
|
|
482
|
-
throw new Error(`IoT credentials returned code=${payload.code}`);
|
|
483
|
-
}
|
|
484
|
-
|
|
485
|
-
const data = payload.data;
|
|
486
439
|
if (typeof data !== 'object' || data === null) {
|
|
487
440
|
throw new Error('IoT credentials payload missing data object');
|
|
488
441
|
}
|
|
@@ -544,7 +497,7 @@ class AnthbotCloudApiClient {
|
|
|
544
497
|
* Get shadow reported state
|
|
545
498
|
*
|
|
546
499
|
* @param {string} serialNumber - Device serial number
|
|
547
|
-
* @returns {Promise<
|
|
500
|
+
* @returns {Promise<any>} - Reported state
|
|
548
501
|
*/
|
|
549
502
|
async asyncGetShadowReportedState(serialNumber) {
|
|
550
503
|
await this.checkShadowClient(serialNumber);
|
package/main.js
CHANGED
|
@@ -103,7 +103,7 @@ class Anthbot extends utils.Adapter {
|
|
|
103
103
|
}
|
|
104
104
|
}
|
|
105
105
|
|
|
106
|
-
// Overlay elements from the state onto existing
|
|
106
|
+
// Overlay elements from the state onto existing custom areas so user only has to set
|
|
107
107
|
// the items they are changing and rest will be preserved.
|
|
108
108
|
|
|
109
109
|
// Variable named to match asyncSendServiceCommand data
|
|
@@ -112,7 +112,7 @@ class Anthbot extends utils.Adapter {
|
|
|
112
112
|
if (!custom_areas) {
|
|
113
113
|
this.log.error(`Bad area data in ${id}`);
|
|
114
114
|
} else {
|
|
115
|
-
// Write the given area
|
|
115
|
+
// Write the given custom area data
|
|
116
116
|
this.log.info(`${device.alias}: area_set ${JSON.stringify(customAreas)}`);
|
|
117
117
|
await this.client.asyncSendServiceCommand(serialNumber, 'area_set', {
|
|
118
118
|
custom_areas,
|
|
@@ -125,74 +125,62 @@ class Anthbot extends utils.Adapter {
|
|
|
125
125
|
}
|
|
126
126
|
|
|
127
127
|
case 'custom_area_mow_start': {
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
let command_zone_list;
|
|
138
|
-
if (typeof command_zone_list_state?.val !== 'string') {
|
|
139
|
-
this.log.error('Command zone list for ${serialNumber} is not a string');
|
|
140
|
-
} else {
|
|
141
|
-
try {
|
|
142
|
-
command_zone_list = JSON.parse(command_zone_list_state.val);
|
|
143
|
-
} catch (error) {
|
|
144
|
-
this.log.error(
|
|
145
|
-
`Failed to parse command zone list for ${serialNumber}: ${error.message}`,
|
|
146
|
-
);
|
|
147
|
-
}
|
|
128
|
+
const goodAreaList = await this.isGoodAreaList(device, `map.custom_areas.raw`);
|
|
129
|
+
if (goodAreaList) {
|
|
130
|
+
this.log.info(
|
|
131
|
+
`${device.alias}: custom_area_mow_start ${JSON.stringify(goodAreaList)}`,
|
|
132
|
+
);
|
|
133
|
+
await this.client.asyncSendServiceCommand(serialNumber, 'custom_area_mow_start', {
|
|
134
|
+
id: goodAreaList,
|
|
135
|
+
});
|
|
136
|
+
ackState = true;
|
|
148
137
|
}
|
|
138
|
+
break;
|
|
139
|
+
}
|
|
149
140
|
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
);
|
|
159
|
-
await this.client.asyncSendServiceCommand(
|
|
160
|
-
serialNumber,
|
|
161
|
-
'custom_area_mow_start',
|
|
162
|
-
{
|
|
163
|
-
id: command_zone_list,
|
|
164
|
-
},
|
|
165
|
-
);
|
|
166
|
-
ackState = true;
|
|
167
|
-
}
|
|
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;
|
|
168
149
|
}
|
|
169
150
|
break;
|
|
170
151
|
}
|
|
171
152
|
|
|
172
|
-
case '
|
|
173
|
-
let
|
|
153
|
+
case 'area_list': {
|
|
154
|
+
let areaList;
|
|
174
155
|
// This will affect the next start command only.
|
|
175
156
|
if (typeof state?.val === 'string' && state.val !== '') {
|
|
176
157
|
// Some kind of non-blank value given
|
|
177
158
|
try {
|
|
178
|
-
|
|
159
|
+
areaList = JSON.parse(state.val);
|
|
179
160
|
} catch (error) {
|
|
180
|
-
this.log.error(`Failed to parse
|
|
161
|
+
this.log.error(`Failed to parse area list for ${id}: ${error.message}`);
|
|
181
162
|
}
|
|
182
163
|
|
|
183
|
-
// Make sure
|
|
184
|
-
if (
|
|
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
|
+
) {
|
|
185
173
|
// Set to null so we don't ack it
|
|
186
|
-
|
|
174
|
+
areaList = null;
|
|
187
175
|
}
|
|
188
176
|
} else {
|
|
189
177
|
// No value given, so ack an empty list
|
|
190
|
-
|
|
178
|
+
areaList = [];
|
|
191
179
|
}
|
|
192
180
|
|
|
193
181
|
// Ack only if we now have a list
|
|
194
|
-
if (Array.isArray(
|
|
195
|
-
ackState = JSON.stringify(
|
|
182
|
+
if (Array.isArray(areaList)) {
|
|
183
|
+
ackState = JSON.stringify(areaList);
|
|
196
184
|
// We don't need to sync after this as no command was actually sent yet
|
|
197
185
|
doSync = false;
|
|
198
186
|
}
|
|
@@ -318,16 +306,6 @@ class Anthbot extends utils.Adapter {
|
|
|
318
306
|
// Wait a second for their backend
|
|
319
307
|
await new Promise(resolve => this.setTimeout(resolve, 1000, null));
|
|
320
308
|
|
|
321
|
-
// TODO: figure out how to tell when map changes and reload periodically?
|
|
322
|
-
const deviceMapFiles = await this.client.asyncGetDeviceMap(device.sn);
|
|
323
|
-
|
|
324
|
-
const areaSetting = deviceMapFiles['area_setting.json'];
|
|
325
|
-
this.log.debug(`area_setting.json: ${JSON.stringify(areaSetting)}`);
|
|
326
|
-
this.setZoneInfo(device, areaSetting?.content?.custom_areas);
|
|
327
|
-
|
|
328
|
-
const timeSetting = deviceMapFiles['time_setting.json'];
|
|
329
|
-
this.log.debug(`time_setting.json: ${JSON.stringify(timeSetting)}`);
|
|
330
|
-
|
|
331
309
|
await this.pollDevice(device);
|
|
332
310
|
this.pollingInterval = this.setInterval(async () => {
|
|
333
311
|
this.pollDevice(device);
|
|
@@ -357,18 +335,24 @@ class Anthbot extends utils.Adapter {
|
|
|
357
335
|
// Poll device
|
|
358
336
|
async pollDevice(device) {
|
|
359
337
|
if (this.checkClient()) {
|
|
338
|
+
// Map area ID for checking for changes
|
|
339
|
+
let area_id;
|
|
340
|
+
|
|
341
|
+
// Shadow state
|
|
360
342
|
try {
|
|
361
343
|
const shadowState = await this.client.asyncGetShadowReportedState(device.sn);
|
|
362
344
|
this.log.debug(`Device shadow reported state:\n${JSON.stringify(shadowState)}`);
|
|
363
345
|
await this.setShadowState(device, shadowState);
|
|
346
|
+
area_id = shadowState.map.area_id;
|
|
364
347
|
} catch (err) {
|
|
365
348
|
this.log.error(`Failed to fetch shadow state for device ${device.sn}: ${err.message}`);
|
|
366
349
|
// TODO: If something goes wrong here, might not be serious, maybe don't do a full reconnect?
|
|
367
350
|
this.retryConnection();
|
|
368
351
|
}
|
|
369
352
|
|
|
353
|
+
// Code list
|
|
370
354
|
try {
|
|
371
|
-
const codeList = await this.client.asyncGetCodeList(device.sn);
|
|
355
|
+
const codeList = await this.client.asyncGetCodeList(device.sn, 1, 20 /* TODO: make configurable? */);
|
|
372
356
|
this.log.debug(`Device code list:\n${JSON.stringify(codeList)}`);
|
|
373
357
|
await this.setCodeList(device, codeList);
|
|
374
358
|
} catch (err) {
|
|
@@ -376,243 +360,150 @@ class Anthbot extends utils.Adapter {
|
|
|
376
360
|
// TODO: If something goes wrong here, might not be serious, maybe don't do a full reconnect?
|
|
377
361
|
this.retryConnection();
|
|
378
362
|
}
|
|
379
|
-
}
|
|
380
|
-
}
|
|
381
363
|
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
type: 'device',
|
|
386
|
-
common: {
|
|
387
|
-
name: device.alias,
|
|
388
|
-
},
|
|
389
|
-
native: {},
|
|
390
|
-
});
|
|
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`);
|
|
391
367
|
|
|
392
|
-
|
|
393
|
-
await this.setObjectNotExistsAsync(`${device.sn}.elec`, {
|
|
394
|
-
type: 'state',
|
|
395
|
-
common: {
|
|
396
|
-
name: 'elec',
|
|
397
|
-
type: 'number',
|
|
398
|
-
unit: '%',
|
|
399
|
-
desc: 'Battery level',
|
|
400
|
-
role: 'level.battery',
|
|
401
|
-
read: true,
|
|
402
|
-
write: false,
|
|
403
|
-
},
|
|
404
|
-
native: {},
|
|
405
|
-
});
|
|
368
|
+
const deviceMapFiles = await this.client.asyncGetDeviceMap(device.sn);
|
|
406
369
|
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
common: {
|
|
410
|
-
name: 'mode',
|
|
411
|
-
type: 'string',
|
|
412
|
-
role: 'text',
|
|
413
|
-
desc: 'Current mode',
|
|
414
|
-
read: true,
|
|
415
|
-
write: false,
|
|
416
|
-
},
|
|
417
|
-
native: {},
|
|
418
|
-
});
|
|
370
|
+
const areaSetting = deviceMapFiles['area_setting.json'];
|
|
371
|
+
this.log.debug(`area_setting.json: ${JSON.stringify(areaSetting)}`);
|
|
419
372
|
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
common: {
|
|
423
|
-
name: 'mowing_area',
|
|
424
|
-
type: 'number',
|
|
425
|
-
unit: 'm²',
|
|
426
|
-
desc: 'Current mowing area',
|
|
427
|
-
role: 'value',
|
|
428
|
-
read: true,
|
|
429
|
-
write: false,
|
|
430
|
-
},
|
|
431
|
-
native: {},
|
|
432
|
-
});
|
|
373
|
+
const custom_areas = areaSetting?.content?.custom_areas;
|
|
374
|
+
this.log.debug(`custom_areas: ${JSON.stringify(custom_areas)}`);
|
|
433
375
|
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
common: {
|
|
437
|
-
name: 'mowing_time',
|
|
438
|
-
type: 'number',
|
|
439
|
-
unit: 's',
|
|
440
|
-
role: 'time.span',
|
|
441
|
-
desc: 'Current mowing time',
|
|
442
|
-
read: true,
|
|
443
|
-
write: false,
|
|
444
|
-
},
|
|
445
|
-
native: {},
|
|
446
|
-
});
|
|
376
|
+
// Stash custom_areas in the passed device
|
|
377
|
+
device.customAreas = custom_areas;
|
|
447
378
|
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
role: 'sensor.motion',
|
|
454
|
-
desc: 'RTK movement detected',
|
|
455
|
-
read: true,
|
|
456
|
-
write: false,
|
|
457
|
-
},
|
|
458
|
-
native: {},
|
|
459
|
-
});
|
|
379
|
+
// And save the state
|
|
380
|
+
this.setStateChanged(`${device.sn}.map.custom_areas.raw`, {
|
|
381
|
+
val: JSON.stringify(custom_areas),
|
|
382
|
+
ack: true,
|
|
383
|
+
});
|
|
460
384
|
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
role: 'sensor',
|
|
467
|
-
desc: 'RTK state',
|
|
468
|
-
read: true,
|
|
469
|
-
write: false,
|
|
470
|
-
},
|
|
471
|
-
native: {},
|
|
472
|
-
});
|
|
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
|
+
});
|
|
473
390
|
|
|
474
|
-
|
|
475
|
-
await this.setObjectNotExistsAsync(`${device.sn}.last_code`, {
|
|
476
|
-
type: 'state',
|
|
477
|
-
common: {
|
|
478
|
-
name: 'last_code',
|
|
479
|
-
type: 'number',
|
|
480
|
-
desc: 'Last code',
|
|
481
|
-
role: 'value',
|
|
482
|
-
read: true,
|
|
483
|
-
write: false,
|
|
484
|
-
},
|
|
485
|
-
native: {},
|
|
486
|
-
});
|
|
391
|
+
// TODO: plan is to create a channel for each area and store info for them indivicually.
|
|
487
392
|
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
common: {
|
|
491
|
-
name: 'last_code_text',
|
|
492
|
-
type: 'string',
|
|
493
|
-
role: 'text',
|
|
494
|
-
desc: 'Last code text',
|
|
495
|
-
read: true,
|
|
496
|
-
write: false,
|
|
497
|
-
},
|
|
498
|
-
native: {},
|
|
499
|
-
});
|
|
393
|
+
const timeSetting = deviceMapFiles['time_setting.json'];
|
|
394
|
+
this.log.debug(`time_setting.json: ${JSON.stringify(timeSetting)}`);
|
|
500
395
|
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
role: 'text',
|
|
507
|
-
desc: 'Last code type (e.g. event, error, etc.)',
|
|
508
|
-
read: true,
|
|
509
|
-
write: false,
|
|
510
|
-
},
|
|
511
|
-
native: {},
|
|
512
|
-
});
|
|
396
|
+
// And remember which area_id we loaded last
|
|
397
|
+
device.area_id = area_id;
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
}
|
|
513
401
|
|
|
514
|
-
|
|
515
|
-
|
|
402
|
+
// Create objects for device
|
|
403
|
+
async createDeviceObjects(device) {
|
|
404
|
+
await this.setObjectNotExistsAsync(device.sn, {
|
|
405
|
+
type: 'device',
|
|
516
406
|
common: {
|
|
517
|
-
name:
|
|
518
|
-
type: 'string',
|
|
519
|
-
role: 'json',
|
|
520
|
-
desc: 'JSON object with zone information',
|
|
521
|
-
read: true,
|
|
522
|
-
write: false,
|
|
407
|
+
name: device.alias,
|
|
523
408
|
},
|
|
524
409
|
native: {},
|
|
525
410
|
});
|
|
526
411
|
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
type: 'boolean',
|
|
545
|
-
role: 'button.start',
|
|
546
|
-
desc: 'Start global mowing',
|
|
547
|
-
read: false,
|
|
548
|
-
write: true,
|
|
549
|
-
},
|
|
550
|
-
native: {},
|
|
551
|
-
});
|
|
552
|
-
await this.setObjectNotExistsAsync(`${device.sn}.command.stop_all_tasks`, {
|
|
553
|
-
type: 'state',
|
|
554
|
-
common: {
|
|
555
|
-
name: 'stop',
|
|
556
|
-
type: 'boolean',
|
|
557
|
-
role: 'button.stop',
|
|
558
|
-
desc: 'Stop',
|
|
559
|
-
read: false,
|
|
560
|
-
write: true,
|
|
561
|
-
},
|
|
562
|
-
native: {},
|
|
563
|
-
});
|
|
564
|
-
await this.setObjectNotExistsAsync(`${device.sn}.command.mow_pause`, {
|
|
565
|
-
type: 'state',
|
|
566
|
-
common: {
|
|
567
|
-
name: 'pause',
|
|
568
|
-
type: 'boolean',
|
|
569
|
-
role: 'button.pause',
|
|
570
|
-
desc: 'Pause',
|
|
571
|
-
read: false,
|
|
572
|
-
write: true,
|
|
573
|
-
},
|
|
574
|
-
native: {},
|
|
575
|
-
});
|
|
576
|
-
await this.setObjectNotExistsAsync(`${device.sn}.command.charge_start`, {
|
|
577
|
-
type: 'state',
|
|
578
|
-
common: {
|
|
579
|
-
name: 'home',
|
|
580
|
-
type: 'boolean',
|
|
581
|
-
role: 'button',
|
|
582
|
-
desc: 'Return home/start charging',
|
|
583
|
-
read: false,
|
|
584
|
-
write: true,
|
|
585
|
-
},
|
|
586
|
-
native: {},
|
|
587
|
-
});
|
|
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
|
+
}
|
|
588
429
|
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
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
|
+
}
|
|
602
474
|
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
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],
|
|
611
497
|
read: false,
|
|
612
498
|
write: true,
|
|
613
|
-
}
|
|
614
|
-
|
|
615
|
-
|
|
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
|
+
}
|
|
616
507
|
}
|
|
617
508
|
|
|
618
509
|
// Helper function to set shadow state values
|
|
@@ -623,15 +514,24 @@ class Anthbot extends utils.Adapter {
|
|
|
623
514
|
this.setConnected(false);
|
|
624
515
|
}
|
|
625
516
|
|
|
517
|
+
this.setStateChanged(`${device.sn}.active_area`, {
|
|
518
|
+
val: JSON.stringify(shadowState.active_area.id),
|
|
519
|
+
ack: true,
|
|
520
|
+
});
|
|
626
521
|
this.setStateChanged(`${device.sn}.elec`, { val: shadowState.elec.value, ack: true });
|
|
627
522
|
this.setStateChanged(`${device.sn}.mode`, { val: shadowState.mode.value, ack: true });
|
|
628
523
|
this.setStateChanged(`${device.sn}.mowing_area`, { val: shadowState.mowing_area.value, ack: true });
|
|
629
524
|
this.setStateChanged(`${device.sn}.mowing_time`, { val: shadowState.mowing_time.value, ack: true });
|
|
630
525
|
this.setStateChanged(`${device.sn}.rtk_moved`, { val: shadowState.rtk.moved == 1, ack: true });
|
|
631
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 });
|
|
632
530
|
}
|
|
633
531
|
|
|
634
532
|
setCodeList(device, codeList) {
|
|
533
|
+
this.setStateChanged(`${device.sn}.code_list`, { val: JSON.stringify(codeList), ack: true });
|
|
534
|
+
|
|
635
535
|
const lastCode = codeList[0];
|
|
636
536
|
this.setStateChanged(`${device.sn}.last_code`, { val: lastCode.code, ack: true });
|
|
637
537
|
this.setStateChanged(`${device.sn}.last_code_text`, { val: lastCode.event_message, ack: true });
|
|
@@ -647,7 +547,7 @@ class Anthbot extends utils.Adapter {
|
|
|
647
547
|
for (const area of customAreas) {
|
|
648
548
|
let outArea;
|
|
649
549
|
|
|
650
|
-
const existingArea = device.
|
|
550
|
+
const existingArea = device.customAreas.find(customArea => customArea.id === area.id);
|
|
651
551
|
if (existingArea) {
|
|
652
552
|
this.log.debug(`Found existing area ${area.id} for merge: ${JSON.stringify(existingArea)}`);
|
|
653
553
|
outArea = { ...existingArea, ...area };
|
|
@@ -697,42 +597,72 @@ class Anthbot extends utils.Adapter {
|
|
|
697
597
|
return outputAreas;
|
|
698
598
|
}
|
|
699
599
|
|
|
700
|
-
|
|
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
|
+
|
|
701
612
|
// List to check must be an array
|
|
702
|
-
if (!Array.isArray(
|
|
703
|
-
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');
|
|
704
642
|
return false;
|
|
705
643
|
}
|
|
706
644
|
|
|
707
|
-
//
|
|
708
|
-
|
|
709
|
-
|
|
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}`);
|
|
710
650
|
return false;
|
|
711
651
|
}
|
|
712
652
|
|
|
713
|
-
// Each
|
|
714
|
-
|
|
715
|
-
for (const
|
|
716
|
-
if (
|
|
717
|
-
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;
|
|
718
658
|
}
|
|
719
659
|
}
|
|
720
|
-
// If we didn't continue, then we didn't find the
|
|
721
|
-
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}`);
|
|
722
662
|
return false;
|
|
723
663
|
}
|
|
724
664
|
|
|
725
|
-
return
|
|
726
|
-
}
|
|
727
|
-
|
|
728
|
-
setZoneInfo(device, zoneInfo) {
|
|
729
|
-
this.log.debug(`zone_info for ${device.sn}: ${JSON.stringify(zoneInfo)}`);
|
|
730
|
-
|
|
731
|
-
// Stash in the passed device
|
|
732
|
-
device.zoneList = zoneInfo;
|
|
733
|
-
|
|
734
|
-
// And save the state
|
|
735
|
-
this.setStateChanged(`${device.sn}.zone_info`, { val: JSON.stringify(zoneInfo), ack: true });
|
|
665
|
+
return listToCheck;
|
|
736
666
|
}
|
|
737
667
|
|
|
738
668
|
subscribeToDevice(device) {
|