@camstack/addon-provider-bosch 0.1.20 → 0.1.21
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/dist/{account-integration-offline-7DUtQrPy.js → account-integration-offline-CEg0JOfN.js} +55 -1
- package/dist/{account-integration-offline-DvuqXgc8.mjs → account-integration-offline-Ciq9CZa-.mjs} +44 -2
- package/dist/{dist-CYBUFKeW.js → dist-CFPbdUpa.js} +143 -88
- package/dist/{dist-B75tWQLF.mjs → dist-D9i3id5N.mjs} +132 -89
- package/dist/home-connect/addon.js +137 -106
- package/dist/home-connect/addon.mjs +137 -106
- package/dist/homecom/addon.js +557 -433
- package/dist/homecom/addon.mjs +557 -433
- package/dist/shc/addon.js +331 -38
- package/dist/shc/addon.mjs +331 -38
- package/i18n/en.json +23 -19
- package/i18n/it.json +23 -19
- package/package.json +4 -1
package/dist/shc/addon.js
CHANGED
|
@@ -2,27 +2,41 @@ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
|
2
2
|
//#region \0rolldown/runtime.js
|
|
3
3
|
var __commonJSMin = (cb, mod) => () => (mod || (cb((mod = { exports: {} }).exports, mod), cb = null), mod.exports);
|
|
4
4
|
//#endregion
|
|
5
|
-
const require_dist = require("../dist-
|
|
5
|
+
const require_dist = require("../dist-CFPbdUpa.js");
|
|
6
6
|
let node_crypto = require("node:crypto");
|
|
7
7
|
let node_https = require("node:https");
|
|
8
8
|
let node_child_process = require("node:child_process");
|
|
9
9
|
let node_fs = require("node:fs");
|
|
10
10
|
let node_os = require("node:os");
|
|
11
11
|
let node_path = require("node:path");
|
|
12
|
+
let node_dgram = require("node:dgram");
|
|
12
13
|
let node_net = require("node:net");
|
|
13
14
|
//#region src/shc/custom-actions.ts
|
|
14
|
-
var boschShcActions = require_dist.defineCustomActions({
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
15
|
+
var boschShcActions = require_dist.defineCustomActions({
|
|
16
|
+
discoverController: require_dist.customAction(require_dist.object({}), require_dist.object({
|
|
17
|
+
hosts: require_dist.array(require_dist.object({
|
|
18
|
+
host: require_dist.string(),
|
|
19
|
+
name: require_dist.string()
|
|
20
|
+
})),
|
|
21
|
+
summary: require_dist.string()
|
|
22
|
+
}), {
|
|
23
|
+
kind: "mutation",
|
|
24
|
+
auth: "admin"
|
|
25
|
+
}),
|
|
26
|
+
registerClient: require_dist.customAction(require_dist.object({
|
|
27
|
+
host: require_dist.string().optional(),
|
|
28
|
+
hostManual: require_dist.string().optional(),
|
|
29
|
+
systemPassword: require_dist.string().min(1)
|
|
30
|
+
}), require_dist.object({
|
|
31
|
+
sslCertificate: require_dist.string().min(1).meta({ secret: true }),
|
|
32
|
+
sslKey: require_dist.string().min(1).meta({ secret: true }),
|
|
33
|
+
token: require_dist.string().min(1).meta({ secret: true }),
|
|
34
|
+
summary: require_dist.string()
|
|
35
|
+
}), {
|
|
36
|
+
kind: "mutation",
|
|
37
|
+
auth: "admin"
|
|
38
|
+
})
|
|
39
|
+
});
|
|
26
40
|
//#endregion
|
|
27
41
|
//#region src/shc/config.ts
|
|
28
42
|
/** Operator settings for one SHC integration (local mTLS + optional token). */
|
|
@@ -46,30 +60,48 @@ function buildConnectionFormSchema() {
|
|
|
46
60
|
return { sections: [{
|
|
47
61
|
id: "connection",
|
|
48
62
|
title: "Smart Home Controller",
|
|
49
|
-
titleKey: "bosch
|
|
63
|
+
titleKey: "provider-bosch:shc.s_connection.title",
|
|
50
64
|
description: "Local REST access to your Bosch Smart Home Controller.",
|
|
51
|
-
descriptionKey: "bosch
|
|
65
|
+
descriptionKey: "provider-bosch:shc.s_connection.description",
|
|
52
66
|
columns: 1,
|
|
53
67
|
fields: [
|
|
68
|
+
{
|
|
69
|
+
type: "addon-action-select",
|
|
70
|
+
addonId: "bosch-shc",
|
|
71
|
+
key: "__shc-discovered",
|
|
72
|
+
writesKey: "host",
|
|
73
|
+
omitFromSettings: true,
|
|
74
|
+
label: "Controller",
|
|
75
|
+
labelKey: "provider-bosch:shc.s_connection.__shc-discovered.label",
|
|
76
|
+
action: "discoverController",
|
|
77
|
+
mapOption: {
|
|
78
|
+
value: "host",
|
|
79
|
+
label: "name"
|
|
80
|
+
},
|
|
81
|
+
hideWhenFewerThan: 2,
|
|
82
|
+
emptyResultsMessage: "No controller found on the network."
|
|
83
|
+
},
|
|
54
84
|
{
|
|
55
85
|
type: "text",
|
|
56
86
|
key: "host",
|
|
57
87
|
label: "Host",
|
|
58
|
-
labelKey: "bosch
|
|
59
|
-
|
|
88
|
+
labelKey: "provider-bosch:shc.s_connection.host.label",
|
|
89
|
+
description: "Filled when one controller is found. Type the address if it is not.",
|
|
90
|
+
descriptionKey: "provider-bosch:shc.s_connection.host.description",
|
|
91
|
+
required: false,
|
|
60
92
|
placeholder: "192.168.1.50"
|
|
61
93
|
},
|
|
62
94
|
{
|
|
63
95
|
type: "info",
|
|
64
96
|
key: "__shc-pairing-hint",
|
|
65
97
|
label: "Pair with the controller",
|
|
66
|
-
content: "Enter the password you created in the Bosch Smart Home app
|
|
98
|
+
content: "Press the button on the controller until the LED blinks. Enter the password you created in the Bosch Smart Home app, not the code printed on the controller, then register."
|
|
67
99
|
},
|
|
68
100
|
{
|
|
69
101
|
type: "password",
|
|
70
102
|
key: "systemPassword",
|
|
71
103
|
label: "System password",
|
|
72
|
-
labelKey: "bosch
|
|
104
|
+
labelKey: "provider-bosch:shc.s_connection.systemPassword.label",
|
|
73
105
|
required: true,
|
|
74
106
|
showToggle: true
|
|
75
107
|
},
|
|
@@ -92,13 +124,46 @@ function buildConnectionFormSchema() {
|
|
|
92
124
|
},
|
|
93
125
|
successMessage: "{summary}"
|
|
94
126
|
},
|
|
127
|
+
{
|
|
128
|
+
type: "group",
|
|
129
|
+
key: "__shc-advanced",
|
|
130
|
+
label: "Advanced",
|
|
131
|
+
labelKey: "provider-bosch:shc.s_connection.__shc-advanced.label",
|
|
132
|
+
style: "accordion",
|
|
133
|
+
defaultCollapsed: true,
|
|
134
|
+
fields: [{
|
|
135
|
+
type: "secret-presence",
|
|
136
|
+
key: "__shc-registered",
|
|
137
|
+
label: "Client registration",
|
|
138
|
+
sourceKey: "sslCertificate",
|
|
139
|
+
yesLabel: "Client registered: yes",
|
|
140
|
+
noLabel: "Client registered: no"
|
|
141
|
+
}, {
|
|
142
|
+
type: "addon-action-button",
|
|
143
|
+
addonId: "bosch-shc",
|
|
144
|
+
key: "__shc-reregister",
|
|
145
|
+
label: "Register again",
|
|
146
|
+
buttonLabel: "Register again",
|
|
147
|
+
action: "registerClient",
|
|
148
|
+
paramsFromForm: {
|
|
149
|
+
host: "host",
|
|
150
|
+
systemPassword: "systemPassword"
|
|
151
|
+
},
|
|
152
|
+
assignResult: {
|
|
153
|
+
sslCertificate: "sslCertificate",
|
|
154
|
+
sslKey: "sslKey",
|
|
155
|
+
token: "token"
|
|
156
|
+
},
|
|
157
|
+
successMessage: "{summary}"
|
|
158
|
+
}]
|
|
159
|
+
},
|
|
95
160
|
{
|
|
96
161
|
type: "password",
|
|
97
162
|
key: "token",
|
|
98
163
|
label: "Controller token",
|
|
99
|
-
labelKey: "bosch
|
|
100
|
-
required:
|
|
101
|
-
showToggle:
|
|
164
|
+
labelKey: "provider-bosch:shc.s_connection.token.label",
|
|
165
|
+
required: false,
|
|
166
|
+
showToggle: false,
|
|
102
167
|
showWhen: {
|
|
103
168
|
field: "host",
|
|
104
169
|
equals: SHC_PAIRING_ONLY_SHOW_WHEN
|
|
@@ -108,8 +173,8 @@ function buildConnectionFormSchema() {
|
|
|
108
173
|
type: "password",
|
|
109
174
|
key: "sslCertificate",
|
|
110
175
|
label: "Client certificate (PEM)",
|
|
111
|
-
labelKey: "bosch
|
|
112
|
-
required:
|
|
176
|
+
labelKey: "provider-bosch:shc.s_connection.sslCertificate.label",
|
|
177
|
+
required: false,
|
|
113
178
|
showToggle: true,
|
|
114
179
|
showWhen: {
|
|
115
180
|
field: "host",
|
|
@@ -120,8 +185,8 @@ function buildConnectionFormSchema() {
|
|
|
120
185
|
type: "password",
|
|
121
186
|
key: "sslKey",
|
|
122
187
|
label: "Client private key (PEM)",
|
|
123
|
-
labelKey: "bosch
|
|
124
|
-
required:
|
|
188
|
+
labelKey: "provider-bosch:shc.s_connection.sslKey.label",
|
|
189
|
+
required: false,
|
|
125
190
|
showToggle: true,
|
|
126
191
|
showWhen: {
|
|
127
192
|
field: "host",
|
|
@@ -265,6 +330,211 @@ async function registerShcClient(deps) {
|
|
|
265
330
|
};
|
|
266
331
|
}
|
|
267
332
|
//#endregion
|
|
333
|
+
//#region src/shc/shc-controller-discovery.ts
|
|
334
|
+
var SHC_MDNS_SERVICE = "_http._tcp.local";
|
|
335
|
+
var SHC_CONTROLLER_NAME_PREFIX = "Bosch SHC";
|
|
336
|
+
var SHC_DISCOVERY_TIMEOUT_MS = 1500;
|
|
337
|
+
var MDNS_PORT = 5353;
|
|
338
|
+
var MDNS_ADDRESS = "224.0.0.251";
|
|
339
|
+
var PTR = 12;
|
|
340
|
+
var A = 1;
|
|
341
|
+
var SRV = 33;
|
|
342
|
+
function isShcControllerName(name) {
|
|
343
|
+
return (name.split("._")[0] ?? name).toLowerCase().startsWith(SHC_CONTROLLER_NAME_PREFIX.toLowerCase());
|
|
344
|
+
}
|
|
345
|
+
function readName(buf, start) {
|
|
346
|
+
const labels = [];
|
|
347
|
+
let pos = start;
|
|
348
|
+
let end = start;
|
|
349
|
+
let jumped = false;
|
|
350
|
+
for (let hops = 0; hops < 16; hops++) {
|
|
351
|
+
if (pos < 0 || pos >= buf.length) return null;
|
|
352
|
+
const len = buf[pos] ?? 0;
|
|
353
|
+
if (len === 0) {
|
|
354
|
+
if (!jumped) end = pos + 1;
|
|
355
|
+
return {
|
|
356
|
+
name: labels.join("."),
|
|
357
|
+
end
|
|
358
|
+
};
|
|
359
|
+
}
|
|
360
|
+
if ((len & 192) === 192) {
|
|
361
|
+
if (pos + 1 >= buf.length) return null;
|
|
362
|
+
if (!jumped) end = pos + 2;
|
|
363
|
+
jumped = true;
|
|
364
|
+
pos = (len & 63) << 8 | (buf[pos + 1] ?? 0);
|
|
365
|
+
continue;
|
|
366
|
+
}
|
|
367
|
+
if ((len & 192) !== 0 || pos + 1 + len > buf.length) return null;
|
|
368
|
+
labels.push(buf.subarray(pos + 1, pos + 1 + len).toString("utf8"));
|
|
369
|
+
pos += 1 + len;
|
|
370
|
+
if (!jumped) end = pos;
|
|
371
|
+
}
|
|
372
|
+
return null;
|
|
373
|
+
}
|
|
374
|
+
function readRecords(buf, offset, count) {
|
|
375
|
+
const records = [];
|
|
376
|
+
let pos = offset;
|
|
377
|
+
for (let i = 0; i < count; i++) {
|
|
378
|
+
const name = readName(buf, pos);
|
|
379
|
+
if (name === null || name.end + 10 > buf.length) return null;
|
|
380
|
+
const type = buf.readUInt16BE(name.end);
|
|
381
|
+
const dataLength = buf.readUInt16BE(name.end + 8);
|
|
382
|
+
const dataOffset = name.end + 10;
|
|
383
|
+
if (dataOffset + dataLength > buf.length) return null;
|
|
384
|
+
records.push({
|
|
385
|
+
name: name.name,
|
|
386
|
+
type,
|
|
387
|
+
message: buf,
|
|
388
|
+
dataOffset,
|
|
389
|
+
dataLength
|
|
390
|
+
});
|
|
391
|
+
pos = dataOffset + dataLength;
|
|
392
|
+
}
|
|
393
|
+
return {
|
|
394
|
+
records,
|
|
395
|
+
next: pos
|
|
396
|
+
};
|
|
397
|
+
}
|
|
398
|
+
function ipv4(buf, offset, length) {
|
|
399
|
+
if (length !== 4 || offset + 4 > buf.length) return null;
|
|
400
|
+
return `${buf[offset] ?? 0}.${buf[offset + 1] ?? 0}.${buf[offset + 2] ?? 0}.${buf[offset + 3] ?? 0}`;
|
|
401
|
+
}
|
|
402
|
+
function hostsFromRecords(records) {
|
|
403
|
+
const instances = [];
|
|
404
|
+
const srv = /* @__PURE__ */ new Map();
|
|
405
|
+
const addresses = /* @__PURE__ */ new Map();
|
|
406
|
+
for (const rec of records) {
|
|
407
|
+
const key = rec.name.toLowerCase();
|
|
408
|
+
if (rec.type === PTR) {
|
|
409
|
+
const target = readName(rec.message, rec.dataOffset);
|
|
410
|
+
if (target !== null && isShcControllerName(target.name)) instances.push(target.name);
|
|
411
|
+
} else if (rec.type === SRV && rec.dataLength >= 6) {
|
|
412
|
+
const target = readName(rec.message, rec.dataOffset + 6);
|
|
413
|
+
if (target !== null) srv.set(key, target.name);
|
|
414
|
+
} else if (rec.type === A) {
|
|
415
|
+
const ip = ipv4(rec.message, rec.dataOffset, rec.dataLength);
|
|
416
|
+
if (ip !== null) addresses.set(key, ip);
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
const out = [];
|
|
420
|
+
const seen = /* @__PURE__ */ new Set();
|
|
421
|
+
for (const instance of instances) {
|
|
422
|
+
const viaSrv = srv.get(instance.toLowerCase());
|
|
423
|
+
const host = (viaSrv !== void 0 ? addresses.get(viaSrv.toLowerCase()) : void 0) ?? addresses.get(instance.toLowerCase());
|
|
424
|
+
if (host === void 0 || seen.has(host)) continue;
|
|
425
|
+
seen.add(host);
|
|
426
|
+
const label = instance.split("._")[0] ?? instance;
|
|
427
|
+
out.push({
|
|
428
|
+
host,
|
|
429
|
+
name: label
|
|
430
|
+
});
|
|
431
|
+
}
|
|
432
|
+
return out;
|
|
433
|
+
}
|
|
434
|
+
/** Hosts advertised as a Bosch Smart Home Controller on one mDNS message. */
|
|
435
|
+
function shcHostsFromDnsMessage(buf) {
|
|
436
|
+
if (buf.length < 12) return [];
|
|
437
|
+
const questions = buf.readUInt16BE(4);
|
|
438
|
+
const answers = buf.readUInt16BE(6);
|
|
439
|
+
const authorities = buf.readUInt16BE(8);
|
|
440
|
+
const extra = buf.readUInt16BE(10);
|
|
441
|
+
let pos = 12;
|
|
442
|
+
for (let i = 0; i < questions; i++) {
|
|
443
|
+
const name = readName(buf, pos);
|
|
444
|
+
if (name === null) return [];
|
|
445
|
+
pos = name.end + 4;
|
|
446
|
+
if (pos > buf.length) return [];
|
|
447
|
+
}
|
|
448
|
+
const answerRecords = readRecords(buf, pos, answers);
|
|
449
|
+
if (answerRecords === null) return [];
|
|
450
|
+
const authorityRecords = readRecords(buf, answerRecords.next, authorities);
|
|
451
|
+
if (authorityRecords === null) return [];
|
|
452
|
+
const extraRecords = readRecords(buf, authorityRecords.next, extra);
|
|
453
|
+
if (extraRecords === null) return [];
|
|
454
|
+
return hostsFromRecords([
|
|
455
|
+
...answerRecords.records,
|
|
456
|
+
...authorityRecords.records,
|
|
457
|
+
...extraRecords.records
|
|
458
|
+
]);
|
|
459
|
+
}
|
|
460
|
+
function mdnsPtrQuery(service) {
|
|
461
|
+
const encoded = service.split(".").filter((part) => part.length > 0).map((label) => Buffer.concat([Buffer.from([label.length]), Buffer.from(label)]));
|
|
462
|
+
const header = Buffer.alloc(12);
|
|
463
|
+
header.writeUInt16BE(1, 4);
|
|
464
|
+
const tail = Buffer.alloc(4);
|
|
465
|
+
tail.writeUInt16BE(PTR, 0);
|
|
466
|
+
tail.writeUInt16BE(1, 2);
|
|
467
|
+
return Buffer.concat([
|
|
468
|
+
header,
|
|
469
|
+
...encoded,
|
|
470
|
+
Buffer.from([0]),
|
|
471
|
+
tail
|
|
472
|
+
]);
|
|
473
|
+
}
|
|
474
|
+
function closeQuietly(socket) {
|
|
475
|
+
try {
|
|
476
|
+
socket.close();
|
|
477
|
+
} catch {}
|
|
478
|
+
}
|
|
479
|
+
function exchangeMdns(query, timeoutMs) {
|
|
480
|
+
return new Promise((resolve) => {
|
|
481
|
+
let socket;
|
|
482
|
+
try {
|
|
483
|
+
socket = (0, node_dgram.createSocket)({
|
|
484
|
+
type: "udp4",
|
|
485
|
+
reuseAddr: true
|
|
486
|
+
});
|
|
487
|
+
} catch {
|
|
488
|
+
resolve([]);
|
|
489
|
+
return;
|
|
490
|
+
}
|
|
491
|
+
const packets = [];
|
|
492
|
+
let settled = false;
|
|
493
|
+
const finish = () => {
|
|
494
|
+
if (settled) return;
|
|
495
|
+
settled = true;
|
|
496
|
+
closeQuietly(socket);
|
|
497
|
+
resolve(packets);
|
|
498
|
+
};
|
|
499
|
+
socket.on("error", () => finish());
|
|
500
|
+
socket.on("message", (msg) => {
|
|
501
|
+
packets.push(Buffer.from(msg));
|
|
502
|
+
});
|
|
503
|
+
socket.bind(0, () => {
|
|
504
|
+
try {
|
|
505
|
+
socket.send(query, MDNS_PORT, MDNS_ADDRESS);
|
|
506
|
+
} catch {
|
|
507
|
+
finish();
|
|
508
|
+
}
|
|
509
|
+
});
|
|
510
|
+
setTimeout(finish, timeoutMs);
|
|
511
|
+
});
|
|
512
|
+
}
|
|
513
|
+
async function browseShcControllers(exchange = exchangeMdns) {
|
|
514
|
+
let packets;
|
|
515
|
+
try {
|
|
516
|
+
packets = await exchange(mdnsPtrQuery(SHC_MDNS_SERVICE), SHC_DISCOVERY_TIMEOUT_MS);
|
|
517
|
+
} catch {
|
|
518
|
+
return [];
|
|
519
|
+
}
|
|
520
|
+
const hosts = [];
|
|
521
|
+
const seen = /* @__PURE__ */ new Set();
|
|
522
|
+
for (const packet of packets) for (const found of shcHostsFromDnsMessage(packet)) {
|
|
523
|
+
if (seen.has(found.host)) continue;
|
|
524
|
+
seen.add(found.host);
|
|
525
|
+
hosts.push(found);
|
|
526
|
+
}
|
|
527
|
+
return hosts;
|
|
528
|
+
}
|
|
529
|
+
/** Manual host wins. Otherwise the address discovery filled in. */
|
|
530
|
+
function resolveShcPairingHost(input) {
|
|
531
|
+
const manual = input.hostManual?.trim() ?? "";
|
|
532
|
+
if (manual.length > 0) return manual;
|
|
533
|
+
const host = input.host?.trim() ?? "";
|
|
534
|
+
if (host.length > 0) return host;
|
|
535
|
+
throw new Error("Enter the controller host or IP address.");
|
|
536
|
+
}
|
|
537
|
+
//#endregion
|
|
268
538
|
//#region src/shc/shc-domain-mapping.ts
|
|
269
539
|
var ROOM_CLIMATE_MODEL = "ROOM_CLIMATE_CONTROL";
|
|
270
540
|
/** Classify a normalised device summary into an adoption / driver kind. */
|
|
@@ -23546,18 +23816,41 @@ var BoschShcProviderAddon = class extends require_dist.BaseDeviceProvider {
|
|
|
23546
23816
|
}
|
|
23547
23817
|
],
|
|
23548
23818
|
customActions: boschShcActions,
|
|
23549
|
-
actionHandlers: {
|
|
23550
|
-
|
|
23551
|
-
|
|
23552
|
-
|
|
23553
|
-
|
|
23554
|
-
|
|
23555
|
-
|
|
23556
|
-
|
|
23557
|
-
|
|
23558
|
-
|
|
23559
|
-
|
|
23560
|
-
|
|
23819
|
+
actionHandlers: {
|
|
23820
|
+
discoverController: async () => {
|
|
23821
|
+
try {
|
|
23822
|
+
const hosts = await browseShcControllers();
|
|
23823
|
+
return {
|
|
23824
|
+
hosts: hosts.map((found) => ({
|
|
23825
|
+
host: found.host,
|
|
23826
|
+
name: found.name
|
|
23827
|
+
})),
|
|
23828
|
+
summary: hosts.length === 0 ? "No controller found on the network." : `Found ${hosts.length} controller${hosts.length === 1 ? "" : "s"}.`
|
|
23829
|
+
};
|
|
23830
|
+
} catch (err) {
|
|
23831
|
+
this.ctx.logger.warn("SHC controller discovery failed", { meta: { error: require_dist.errMsg(err) } });
|
|
23832
|
+
return {
|
|
23833
|
+
hosts: [],
|
|
23834
|
+
summary: "No controller found on the network."
|
|
23835
|
+
};
|
|
23836
|
+
}
|
|
23837
|
+
},
|
|
23838
|
+
registerClient: async (input) => {
|
|
23839
|
+
const { sslCertificate, sslKey, token } = await registerShcClient({
|
|
23840
|
+
host: resolveShcPairingHost({
|
|
23841
|
+
host: input.host,
|
|
23842
|
+
hostManual: input.hostManual
|
|
23843
|
+
}),
|
|
23844
|
+
systemPassword: input.systemPassword
|
|
23845
|
+
});
|
|
23846
|
+
return {
|
|
23847
|
+
sslCertificate,
|
|
23848
|
+
sslKey,
|
|
23849
|
+
token,
|
|
23850
|
+
summary: "CamStack is registered with your Smart Home Controller."
|
|
23851
|
+
};
|
|
23852
|
+
}
|
|
23853
|
+
}
|
|
23561
23854
|
};
|
|
23562
23855
|
}
|
|
23563
23856
|
async onShutdown() {
|