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.
- package/README.md +5 -3
- package/dist/cli.js +7 -1
- package/dist/options.d.ts +5 -0
- package/dist/options.js +1 -0
- package/dist/server.d.ts +2 -0
- package/dist/server.js +1 -0
- package/node_modules/@homematic-manager/backend/dist/api/backend.d.ts +8 -0
- package/node_modules/@homematic-manager/backend/dist/api/backend.js +175 -17
- package/node_modules/@homematic-manager/backend/dist/config/defaults.js +41 -0
- package/node_modules/@homematic-manager/backend/dist/groups/client.d.ts +76 -0
- package/node_modules/@homematic-manager/backend/dist/groups/client.js +233 -0
- package/node_modules/@homematic-manager/backend/dist/index.d.ts +1 -0
- package/node_modules/@homematic-manager/backend/dist/index.js +2 -0
- package/node_modules/@homematic-manager/backend/dist/interfaces/manager.d.ts +111 -5
- package/node_modules/@homematic-manager/backend/dist/interfaces/manager.js +289 -13
- package/node_modules/@homematic-manager/backend/dist/meta/client.d.ts +29 -1
- package/node_modules/@homematic-manager/backend/dist/meta/client.js +89 -15
- package/node_modules/@homematic-manager/backend/dist/meta/service.d.ts +50 -4
- package/node_modules/@homematic-manager/backend/dist/meta/service.js +89 -17
- package/node_modules/@homematic-manager/backend/dist/meta/systemFetch.d.ts +45 -0
- package/node_modules/@homematic-manager/backend/dist/meta/systemFetch.js +256 -0
- package/node_modules/@homematic-manager/backend/dist/rega/client.d.ts +6 -0
- package/node_modules/@homematic-manager/backend/dist/rega/client.js +33 -9
- package/node_modules/@homematic-manager/backend/dist/rpc/server.d.ts +3 -4
- package/node_modules/@homematic-manager/backend/dist/rpc/server.js +9 -15
- package/node_modules/@homematic-manager/backend/package.json +1 -1
- package/node_modules/@homematic-manager/core/dist/address/names.d.ts +28 -0
- package/node_modules/@homematic-manager/core/dist/address/names.js +46 -0
- package/node_modules/@homematic-manager/core/dist/api/types.d.ts +203 -2
- package/node_modules/@homematic-manager/core/dist/devices/smokeGroups.d.ts +85 -0
- package/node_modules/@homematic-manager/core/dist/devices/smokeGroups.js +156 -0
- package/node_modules/@homematic-manager/core/dist/index.d.ts +2 -0
- package/node_modules/@homematic-manager/core/dist/index.js +2 -0
- package/node_modules/@homematic-manager/core/dist/interfaces/table.d.ts +12 -0
- package/node_modules/@homematic-manager/core/dist/interfaces/table.js +6 -0
- package/node_modules/@homematic-manager/core/dist/meta/types.d.ts +15 -0
- package/node_modules/@homematic-manager/core/package.json +1 -1
- package/package.json +3 -3
- package/ui/assets/index-CNnzEOiY.css +1 -0
- package/ui/assets/index-rHTK97G5.js +20 -0
- package/ui/index.html +2 -2
- package/ui/assets/index-D-NsbBVS.css +0 -1
- 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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
194
|
-
|
|
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
|
/**
|