homematic-manager 3.0.0-beta.21 → 3.0.0-beta.22

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.
Files changed (43) hide show
  1. package/README.md +5 -3
  2. package/dist/cli.js +7 -1
  3. package/dist/options.d.ts +5 -0
  4. package/dist/options.js +1 -0
  5. package/dist/server.d.ts +2 -0
  6. package/dist/server.js +1 -0
  7. package/node_modules/@homematic-manager/backend/dist/api/backend.d.ts +8 -0
  8. package/node_modules/@homematic-manager/backend/dist/api/backend.js +175 -17
  9. package/node_modules/@homematic-manager/backend/dist/config/defaults.js +41 -0
  10. package/node_modules/@homematic-manager/backend/dist/groups/client.d.ts +76 -0
  11. package/node_modules/@homematic-manager/backend/dist/groups/client.js +233 -0
  12. package/node_modules/@homematic-manager/backend/dist/index.d.ts +1 -0
  13. package/node_modules/@homematic-manager/backend/dist/index.js +2 -0
  14. package/node_modules/@homematic-manager/backend/dist/interfaces/manager.d.ts +111 -5
  15. package/node_modules/@homematic-manager/backend/dist/interfaces/manager.js +289 -13
  16. package/node_modules/@homematic-manager/backend/dist/meta/client.d.ts +29 -1
  17. package/node_modules/@homematic-manager/backend/dist/meta/client.js +89 -15
  18. package/node_modules/@homematic-manager/backend/dist/meta/service.d.ts +50 -4
  19. package/node_modules/@homematic-manager/backend/dist/meta/service.js +89 -17
  20. package/node_modules/@homematic-manager/backend/dist/meta/systemFetch.d.ts +45 -0
  21. package/node_modules/@homematic-manager/backend/dist/meta/systemFetch.js +256 -0
  22. package/node_modules/@homematic-manager/backend/dist/rega/client.d.ts +6 -0
  23. package/node_modules/@homematic-manager/backend/dist/rega/client.js +33 -9
  24. package/node_modules/@homematic-manager/backend/dist/rpc/server.d.ts +3 -4
  25. package/node_modules/@homematic-manager/backend/dist/rpc/server.js +9 -15
  26. package/node_modules/@homematic-manager/backend/package.json +1 -1
  27. package/node_modules/@homematic-manager/core/dist/address/names.d.ts +28 -0
  28. package/node_modules/@homematic-manager/core/dist/address/names.js +46 -0
  29. package/node_modules/@homematic-manager/core/dist/api/types.d.ts +203 -2
  30. package/node_modules/@homematic-manager/core/dist/devices/smokeGroups.d.ts +85 -0
  31. package/node_modules/@homematic-manager/core/dist/devices/smokeGroups.js +156 -0
  32. package/node_modules/@homematic-manager/core/dist/index.d.ts +2 -0
  33. package/node_modules/@homematic-manager/core/dist/index.js +2 -0
  34. package/node_modules/@homematic-manager/core/dist/interfaces/table.d.ts +12 -0
  35. package/node_modules/@homematic-manager/core/dist/interfaces/table.js +6 -0
  36. package/node_modules/@homematic-manager/core/dist/meta/types.d.ts +15 -0
  37. package/node_modules/@homematic-manager/core/package.json +1 -1
  38. package/package.json +3 -3
  39. package/ui/assets/index-CNnzEOiY.css +1 -0
  40. package/ui/assets/index-rHTK97G5.js +20 -0
  41. package/ui/index.html +2 -2
  42. package/ui/assets/index-D-NsbBVS.css +0 -1
  43. package/ui/assets/index-DNUNMBbb.js +0 -16
@@ -0,0 +1,233 @@
1
+ /**
2
+ * Task 57: the HTTP client for openccu-lite's heating groups - `/api/system/v1/groups`.
3
+ *
4
+ * The heating groups (the `VirtualDevices` group devices `INT000000N`) are the group process's,
5
+ * and that process has no RPC method to create one, change its members or delete it: the CCU's
6
+ * WebUI does all of that through the process's own HTTP pages, behind a WebUI session. On
7
+ * openccu-lite the box's `occulited` is the one client of those pages and offers them as a plain
8
+ * JSON API with the box's own login - the one this application already has for the metadata store
9
+ * (D-40). This client speaks that API and nothing else; on a CCU it is never built.
10
+ *
11
+ * Like the metadata client: `fetch` and `AbortSignal` only, a typed answer or a thrown
12
+ * {@link BackendError} whose `kind` says what the UI should make of it. The box's error body
13
+ * (`{error, message}`) is kept, because its message is the one worth showing - "there is no group
14
+ * 7", "members: hmipserver's device ids, as GET /groups/types lists them".
15
+ */
16
+ import { BackendError } from '../errors.js';
17
+ /**
18
+ * How long a call may take. A change makes the box's group process configure direct links, and the
19
+ * box itself waits up to thirty seconds for that process before it answers `502`; this has to
20
+ * outlast it, or the UI reports a timeout for a change that went through.
21
+ */
22
+ export const GROUPS_TIMEOUT_MS = 45_000;
23
+ /** The box refused or could not answer; `status` and `code` are what it said. */
24
+ export class HeatingGroupsApiError extends BackendError {
25
+ status;
26
+ /** The API's error word: `unknown-group`, `invalid`, `unsupported`, `hmipserver`, `forbidden`. */
27
+ code;
28
+ constructor(status, code, message) {
29
+ super({ message, kind: kindOf(status, code) });
30
+ this.name = 'HeatingGroupsApiError';
31
+ this.status = status;
32
+ this.code = code;
33
+ }
34
+ }
35
+ /**
36
+ * What the UI should make of a status: a credential problem is `config` (the settings dialog is
37
+ * where the token lives), a refused body or an id the box no longer has is `validation` (the
38
+ * dialog shows the message), a box or group process that does not answer is `connection`.
39
+ */
40
+ function kindOf(status, code) {
41
+ if (status === 401 || status === 403 || code === 'unsupported') {
42
+ return 'config';
43
+ }
44
+ if (status === 404 || status === 422 || status === 400) {
45
+ return 'validation';
46
+ }
47
+ if (status === 502 || status === 503 || status === 504) {
48
+ return 'connection';
49
+ }
50
+ return 'internal';
51
+ }
52
+ export class HeatingGroupsClient {
53
+ #options;
54
+ #fetch;
55
+ constructor(options) {
56
+ this.#options = options;
57
+ this.#fetch = options.fetch ?? globalThis.fetch;
58
+ }
59
+ get baseUrl() {
60
+ return this.#options.baseUrl;
61
+ }
62
+ /**
63
+ * Does this box have the groups API, and may this credential read it? One `GET /groups`; the
64
+ * answer's status is the whole result. Never throws: the state is what the UI decides the tab by.
65
+ */
66
+ async probe() {
67
+ try {
68
+ await this.#json('GET', '/groups');
69
+ return { available: true };
70
+ }
71
+ catch (error) {
72
+ if (error instanceof HeatingGroupsApiError) {
73
+ if (error.code === 'unsupported') {
74
+ return { available: false, reason: 'unsupported', message: error.message };
75
+ }
76
+ if (error.status === 401 || error.status === 403) {
77
+ return { available: false, reason: 'forbidden', message: error.message };
78
+ }
79
+ if (error.status === 404) {
80
+ // an older box: the route itself is missing, not a group
81
+ return { available: false, reason: 'not-offered', message: error.message };
82
+ }
83
+ }
84
+ return { available: false, reason: 'error', message: error instanceof Error ? error.message : String(error) };
85
+ }
86
+ }
87
+ /**
88
+ * `GET /groups`, then each group once for its members - the list itself names none, and a
89
+ * grid of groups without their members would say nothing. A group whose detail fails (deleted
90
+ * between the two calls) is listed without members rather than failing the list.
91
+ */
92
+ async list() {
93
+ const answer = (await this.#json('GET', '/groups'));
94
+ const groups = await Promise.all((answer.groups ?? []).map(async (wire) => {
95
+ let members = [];
96
+ try {
97
+ members = (await this.get(wire.id)).members;
98
+ }
99
+ catch {
100
+ // listed without members; the next refresh has them, or the group is gone
101
+ }
102
+ return { ...toGroup(wire), members };
103
+ }));
104
+ return { groups, devicesToConfigure: answer.devices_to_configure ?? [] };
105
+ }
106
+ /** `GET /groups/types` - what a new group can be, and what each type could take now. */
107
+ async types() {
108
+ const answer = (await this.#json('GET', '/groups/types'));
109
+ return (answer.types ?? []).map((type) => ({
110
+ id: type.id ?? '',
111
+ label: type.label ?? type.id ?? '',
112
+ assignable: type.assignable ?? [],
113
+ leftover: type.leftover ?? [],
114
+ }));
115
+ }
116
+ /** `GET /groups/{id}`. */
117
+ async get(id) {
118
+ return toDetail((await this.#json('GET', `/groups/${String(id)}`)));
119
+ }
120
+ /** `POST /groups` - the box's `create` then `save`, and the metadata side effects in one go. */
121
+ async create(name, type, members) {
122
+ return toChange((await this.#json('POST', '/groups', { name, type, members: [...members] })));
123
+ }
124
+ /**
125
+ * `PUT /groups/{id}` - the name and the members as a whole; a field left out keeps its value.
126
+ * Adding and removing is this call with the new list, exactly as the WebUI saved it.
127
+ */
128
+ async update(id, change) {
129
+ const body = {
130
+ ...(change.name === undefined ? {} : { name: change.name }),
131
+ ...(change.members === undefined ? {} : { members: [...change.members] }),
132
+ };
133
+ return toChange((await this.#json('PUT', `/groups/${String(id)}`, body)));
134
+ }
135
+ /** `DELETE /groups/{id}` - answers with the former members, whose group membership is gone. */
136
+ async remove(id) {
137
+ const answer = (await this.#json('DELETE', `/groups/${String(id)}`));
138
+ return answer.former_members ?? [];
139
+ }
140
+ #url(route) {
141
+ return `${this.#options.baseUrl.replace(/\/$/, '')}/api/system/v1${route}`;
142
+ }
143
+ async #json(method, route, body) {
144
+ const credential = this.#options.credential();
145
+ let response;
146
+ try {
147
+ response = await this.#fetch(this.#url(route), {
148
+ method,
149
+ signal: AbortSignal.timeout(this.#options.timeoutMs ?? GROUPS_TIMEOUT_MS),
150
+ headers: {
151
+ Accept: 'application/json',
152
+ ...(body === undefined ? {} : { 'Content-Type': 'application/json' }),
153
+ ...(credential === undefined ? {} : { Authorization: `Bearer ${credential}` }),
154
+ },
155
+ ...(body === undefined ? {} : { body: JSON.stringify(body) }),
156
+ });
157
+ }
158
+ catch (error) {
159
+ // the box is off, or the change outlasted the timeout above
160
+ throw new BackendError({
161
+ message: `the groups API at ${this.#options.baseUrl} did not answer: ${errorText(error)}`,
162
+ kind: 'connection',
163
+ }, { cause: error });
164
+ }
165
+ if (!response.ok) {
166
+ throw await refusal(response);
167
+ }
168
+ if (response.status === 204) {
169
+ return {};
170
+ }
171
+ try {
172
+ return await response.json();
173
+ }
174
+ catch (error) {
175
+ throw new BackendError({ message: `the groups API answered ${String(response.status)} without JSON`, kind: 'internal' }, { cause: error });
176
+ }
177
+ }
178
+ }
179
+ function toGroup(wire) {
180
+ return {
181
+ id: wire.id,
182
+ name: wire.name,
183
+ type: wire.type,
184
+ typeLabel: wire.type_label ?? wire.type,
185
+ device: wire.device,
186
+ ref: wire.ref,
187
+ };
188
+ }
189
+ function toDetail(wire) {
190
+ const types = wire.types ?? [];
191
+ return {
192
+ id: wire.id,
193
+ name: wire.name,
194
+ type: wire.type,
195
+ typeLabel: types.find((type) => type.id === wire.type)?.label ?? wire.type,
196
+ device: wire.device,
197
+ ref: wire.ref,
198
+ deviceName: wire.device_name ?? '',
199
+ forbidSingleOperation: wire.forbid_single_operation === true,
200
+ members: wire.members ?? [],
201
+ assignable: wire.assignable ?? [],
202
+ leftover: wire.leftover ?? [],
203
+ types,
204
+ };
205
+ }
206
+ function toChange(wire) {
207
+ return { ...toDetail(wire), devicesToConfigure: wire.devices_to_configure ?? [] };
208
+ }
209
+ /** The API's `{error, message}` body as the error, or the status when there is none. */
210
+ async function refusal(response) {
211
+ let code = response.status === 401 || response.status === 403 ? 'forbidden' : 'error';
212
+ let message = `${String(response.status)} ${response.statusText}`.trim();
213
+ try {
214
+ const body = (await response.json());
215
+ if (typeof body.error === 'string') {
216
+ code = body.error;
217
+ }
218
+ if (typeof body.message === 'string') {
219
+ message = body.message;
220
+ }
221
+ }
222
+ catch {
223
+ // not JSON: lighttpd's own error page, or a box that is not openccu-lite at all
224
+ }
225
+ if (response.status === 401 || response.status === 403) {
226
+ message = `the system refused the credential for the heating groups (${String(response.status)}): ${message}`;
227
+ }
228
+ return new HeatingGroupsApiError(response.status, code, message);
229
+ }
230
+ function errorText(error) {
231
+ return error instanceof Error ? error.message : String(error);
232
+ }
233
+ //# sourceMappingURL=client.js.map
@@ -31,6 +31,7 @@ export * from './cache/names.js';
31
31
  export * from './cache/unreach.js';
32
32
  export * from './cache/store.js';
33
33
  export * from './meta/index.js';
34
+ export * from './groups/client.js';
34
35
  export * from './write/queue.js';
35
36
  export * from './write/paramset.js';
36
37
  export * from './rega/auth.js';
@@ -36,6 +36,8 @@ export * from './cache/unreach.js';
36
36
  export * from './cache/store.js';
37
37
  // the metadata store: names, rooms and functions, local or from an openccu-lite box (task 24)
38
38
  export * from './meta/index.js';
39
+ // the heating groups of openccu-lite, through the box's system API (task 57)
40
+ export * from './groups/client.js';
39
41
  // the write path (task 6)
40
42
  export * from './write/queue.js';
41
43
  export * from './write/paramset.js';
@@ -10,6 +10,12 @@
10
10
  * `ping`, whose answer arrives as an event and resets the clock. HmIP-RF gets 600 s because
11
11
  * hmipserver answers pings but sends events rarely (eq-3/occu#42); an interface that answers no
12
12
  * ping at all (VirtualDevices) is watched by events only.
13
+ * At the start (task 56, D-52) an `init` that is refused or times out is not left to the watchdog:
14
+ * it is tried again after 1, 2, 4 and 8 s and then every 2 s, and the interface shows *waiting*.
15
+ * HmIP-RF has a liveness ping of its own besides (B-56, D-53): after 30 s of silence it is
16
+ * pinged, and a missing PONG 10 s later means hmipserver has restarted - it reads its handler
17
+ * list back but sends nothing until the client calls `init` again - so the interface shows
18
+ * *reconnecting* and is subscribed again on the quick schedule of the start.
13
19
  * 3. `init(url, '')` on shutdown, so the CCU stops calling a process that is gone - with a hard
14
20
  * timeout, because 2.x's `stop()` waited for an unreachable CCU and only a second `stop()` (or
15
21
  * the 15 s fallback timer) got the app closed.
@@ -39,6 +45,34 @@ export declare const SHUTDOWN_TIMEOUT_MS = 5000;
39
45
  * then with a doubling delay up to this ceiling, and only the first failure produces a notice.
40
46
  */
41
47
  export declare const MAX_INIT_BACKOFF_MS = 300000;
48
+ /**
49
+ * Task 56 (D-52): the waits between the `init` attempts of an interface that refuses or does not
50
+ * answer at the start, before {@link START_RETRY_INTERVAL_MS} takes over.
51
+ *
52
+ * On openccu-lite the addon may start before the interface processes (`runtime.start: "early"`):
53
+ * a Raspberry Pi 3 measured the first `init` 45 s before hmipserver answered, and on the 15 s watchdog
54
+ * ticks with the doubling back-off below HmIP-RF was asked again only 17 s after it was there.
55
+ */
56
+ export declare const START_RETRY_DELAYS_MS: readonly number[];
57
+ /**
58
+ * Task 56 (D-57): the wait between two start attempts after {@link START_RETRY_DELAYS_MS}.
59
+ *
60
+ * D-52 had 15 s here. Measured on openccu-lite with the addon started early, that made HmIP-RF on a
61
+ * Raspberry Pi 4 connect 2-3 s *later* than when the addon waits for hmipserver, because hmipserver
62
+ * came up between two attempts 15 s apart; with 2 s it connected 9.6 s earlier. A refused
63
+ * connection on the loopback every 2 s for at most the start window costs nothing.
64
+ */
65
+ export declare const START_RETRY_INTERVAL_MS = 2000;
66
+ /**
67
+ * Task 56: how long after `start()` (or a D-31 resubscribe) an interface that has not answered yet
68
+ * is *waiting* rather than missing. Two minutes cover a Raspberry Pi 3 booting with the addon
69
+ * started early, where the interface processes come up about 50 s after it. After the window the
70
+ * interface is treated as before: "not present" or "not answering", said once, and tried with the
71
+ * back-off up to {@link MAX_INIT_BACKOFF_MS} - a CCU without a wired gateway never runs `hs485d`.
72
+ */
73
+ export declare const START_WINDOW_MS = 120000;
74
+ /** Task 56: the wait before start attempt number `attempt` (1 for the first retry). */
75
+ export declare function startRetryDelay(attempt: number): number;
42
76
  /**
43
77
  * D-31: the grace period a server install waits, with no UI session connected, before it drops its
44
78
  * event subscriptions. Five minutes; `0` turns the whole thing off, which is what Electron uses.
@@ -49,25 +83,69 @@ export interface ManagedInterface {
49
83
  readonly target: InterfaceTarget;
50
84
  readonly client: RpcClient;
51
85
  state: InterfaceState;
52
- /** Milliseconds since epoch of the last event, ping answer or successful `init`. */
86
+ /**
87
+ * Milliseconds since epoch of the last event, ping answer or successful `init` - for the state
88
+ * the UI shows, never for a timeout (B-65: see {@link lastSeenMono}).
89
+ */
53
90
  lastEvent: number;
91
+ /**
92
+ * B-65: the same moment on the monotonic clock, which the watchdog measures the silence
93
+ * against. On the wall clock a box without a real-time clock, whose date NTP moves months
94
+ * forward a few seconds after the start, made every interface look silent once and re-`init`ed
95
+ * it. `-Infinity` until the first one, so an interface never heard of is due at once.
96
+ */
97
+ lastSeenMono: number;
54
98
  /** Consecutive failed `init` calls; 0 as soon as one succeeds. */
55
99
  failures: number;
56
- /** Milliseconds since epoch before which the watchdog does not try `init` again. */
100
+ /** On the monotonic clock (B-65): the moment before which the watchdog does not try `init` again. */
57
101
  retryAt: number;
102
+ /** Task 56: failed `init` attempts while waiting at the start; 0 once it answered. */
103
+ waitingAttempts: number;
104
+ /** Task 56: the timer of the next start attempt, which the watchdog then leaves alone. */
105
+ retryTimer: ReturnType<typeof setTimeout> | undefined;
106
+ /** Task 56: `init` succeeded since the last start - its failures are an outage, not a start. */
107
+ answered: boolean;
108
+ /**
109
+ * Task 56, B-56: when the current window of quick retries began, on the monotonic clock - the
110
+ * start (or a D-31 resubscribe) for every interface, or the moment a liveness ping went
111
+ * unanswered for this one.
112
+ */
113
+ windowStart: number;
114
+ /** B-56: the last event on the monotonic clock, for the liveness ping. */
115
+ lastEventMono: number;
116
+ /**
117
+ * B-56: an event arrived since the last successful `init`. Only then does a missing PONG mean
118
+ * that the subscription was lost: an interface that never reached us - a callback address the
119
+ * CCU cannot reach - would otherwise be re-`init`ed every 40 s, and hmipserver re-sends every
120
+ * device at each `init` (occu#45).
121
+ */
122
+ heardSinceInit: boolean;
123
+ /** B-56: the timer of the next liveness check, and the one waiting for its PONG. */
124
+ livenessTimer: ReturnType<typeof setTimeout> | undefined;
125
+ /** B-56: lost its subscription (no PONG) and is being subscribed again. */
126
+ reconnecting: boolean;
127
+ /** B-56: a liveness ping is out and its PONG is being waited for. */
128
+ awaitingPong: boolean;
58
129
  }
59
130
  export interface InterfaceManagerOptions {
60
131
  readonly connection: ConnectionConfig;
61
132
  /** What the callback servers do with an incoming call. */
62
133
  readonly handler: CallbackHandler;
63
134
  readonly onStateChanged: (states: InterfaceState[]) => void;
64
- readonly onNotice: (level: 'info' | 'warn' | 'error', message: string, interfaceName?: string) => void;
135
+ readonly onNotice: (level: 'debug' | 'info' | 'warn' | 'error', message: string, interfaceName?: string) => void;
65
136
  /** Called after a successful `init`; the backend fills its caches there. */
66
137
  readonly onConnected?: (interfaceName: string) => void | Promise<void>;
67
138
  readonly onCall?: (record: RpcCallRecord) => void;
68
139
  /** Task 48: the origin of a call that names none, handed to every client (the backend's request context). */
69
140
  readonly originOf?: RpcClientOptions['originOf'];
70
141
  readonly now?: () => number;
142
+ /**
143
+ * Task 56: a clock that only moves forward, for the start window. The wall clock is no use
144
+ * there: a box without a real-time clock boots with a date months old and NTP moves it forward
145
+ * a few seconds after the start (seen on a Raspberry Pi 4, where the jump ended the window at
146
+ * once). Defaults to `performance.now()`.
147
+ */
148
+ readonly monotonicNow?: () => number;
71
149
  readonly rpcTimeoutMs?: number;
72
150
  readonly watchdogIntervalMs?: number;
73
151
  /**
@@ -76,6 +154,18 @@ export interface InterfaceManagerOptions {
76
154
  * interface that fails once and then works.
77
155
  */
78
156
  readonly initBackoffMs?: number;
157
+ /**
158
+ * Task 56: how long after the start a refused or unanswered `init` means *waiting*; defaults to
159
+ * {@link START_WINDOW_MS}. `0` turns the quick start retries off.
160
+ */
161
+ readonly startWindowMs?: number;
162
+ /**
163
+ * B-56: overrides the liveness ping interval of every interface that has one (HmIP-RF, 30 s in
164
+ * the table); `0` turns the liveness ping off. The PONG timeout is {@link pongTimeoutMs}.
165
+ */
166
+ readonly pingIntervalMs?: number;
167
+ /** B-56: how long the PONG may take; defaults to {@link PONG_TIMEOUT_SECONDS}. */
168
+ readonly pongTimeoutMs?: number;
79
169
  /** Address to bind the callback servers to; left out, {@link callbackBindHost} decides. */
80
170
  readonly callbackHost?: string;
81
171
  /**
@@ -91,6 +181,14 @@ export interface InterfaceManagerOptions {
91
181
  * line of a fixed port that cannot be opened.
92
182
  */
93
183
  readonly callbackPins?: CallbackPins;
184
+ /**
185
+ * B-69: whether the interface lists any device now - the backend's device cache. An interface
186
+ * that answers no ping (VirtualDevices) is watched by its events alone, and with no device it
187
+ * has nothing to send: on a system without virtual groups the group process was subscribed
188
+ * again every time its event timeout ran out, about every 75 s. Left out, every such interface
189
+ * is judged by its events, as before.
190
+ */
191
+ readonly listsDevices?: (interfaceName: string) => boolean;
94
192
  /** Injected by the tests. */
95
193
  readonly createClient?: (options: RpcClientOptions) => RpcClient;
96
194
  readonly createCallbackServers?: (handler: CallbackHandler) => CallbackServerSet;
@@ -190,8 +288,16 @@ export declare class InterfaceManager {
190
288
  subscribe(): Promise<void>;
191
289
  /** One watchdog round; public so a test can drive it without waiting 15 s. */
192
290
  tick(): Promise<void>;
193
- /** A callback arrived; the interface is alive. */
194
- noteEvent(interfaceName: string): void;
291
+ /**
292
+ * A callback arrived; the interface is alive.
293
+ *
294
+ * B-56: only an `event` proves that the subscription delivers. A device callback (`newDevices`,
295
+ * `deleteDevices`, …) does not: a restarted hmipserver announces its devices to the handler it
296
+ * read back, and then sends it no event at all (measured on a Raspberry Pi 4, 16 s after the
297
+ * restart). Such a callback that nobody asked for is the moment to check, so the liveness ping
298
+ * goes out at once.
299
+ */
300
+ noteEvent(interfaceName: string, kind?: 'event' | 'device'): void;
195
301
  /** `interfaces.reconnect`: subscribes again, to one interface or to all of them. */
196
302
  reconnect(interfaceName?: string): Promise<void>;
197
303
  /**