homematic-manager 3.0.0-beta.30 → 3.0.0-beta.32
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/node_modules/@homematic-manager/backend/dist/api/backend.js +5 -23
- package/node_modules/@homematic-manager/backend/dist/groups/client.d.ts +9 -10
- package/node_modules/@homematic-manager/backend/dist/groups/client.js +41 -68
- package/node_modules/@homematic-manager/backend/dist/meta/index.d.ts +3 -4
- package/node_modules/@homematic-manager/backend/dist/meta/index.js +3 -4
- package/node_modules/@homematic-manager/backend/dist/meta/occuliteProvider.d.ts +9 -2
- package/node_modules/@homematic-manager/backend/dist/meta/occuliteProvider.js +28 -17
- package/node_modules/@homematic-manager/backend/dist/meta/service.d.ts +25 -6
- package/node_modules/@homematic-manager/backend/dist/meta/service.js +55 -18
- package/node_modules/@homematic-manager/backend/dist/meta/system.d.ts +150 -0
- package/node_modules/@homematic-manager/backend/dist/meta/system.js +455 -0
- package/node_modules/@homematic-manager/backend/dist/transport/wsServer.d.ts +10 -1
- package/node_modules/@homematic-manager/backend/dist/transport/wsServer.js +48 -6
- package/node_modules/@homematic-manager/backend/package.json +2 -2
- package/node_modules/@homematic-manager/core/dist/easymodes/engine.d.ts +16 -3
- package/node_modules/@homematic-manager/core/dist/easymodes/engine.js +79 -27
- package/node_modules/@homematic-manager/core/package.json +1 -1
- package/package.json +5 -4
- package/ui/assets/{index-B7H_fshR.js → index-Co1znn_z.js} +13 -13
- package/ui/index.html +1 -1
- package/node_modules/@homematic-manager/backend/dist/meta/client.d.ts +0 -148
- package/node_modules/@homematic-manager/backend/dist/meta/client.js +0 -381
- package/node_modules/@homematic-manager/backend/dist/meta/credentials.d.ts +0 -38
- package/node_modules/@homematic-manager/backend/dist/meta/credentials.js +0 -53
- package/node_modules/@homematic-manager/backend/dist/meta/systemFetch.d.ts +0 -45
- package/node_modules/@homematic-manager/backend/dist/meta/systemFetch.js +0 -256
|
@@ -25,9 +25,8 @@ import { discoverCcus } from '../discovery/discover.js';
|
|
|
25
25
|
import { BackendError, configError, connectionError, errorMessage, internalError, isMethodUnsupported, validationError, } from '../errors.js';
|
|
26
26
|
import { DEFAULT_IDLE_UNSUBSCRIBE_MS, InterfaceManager, LOOPBACK_IP, callbackBindHost, firstBidcosInterfaceAddress, } from '../interfaces/manager.js';
|
|
27
27
|
import { HeatingGroupsClient } from '../groups/client.js';
|
|
28
|
-
import { DETECT_TIMEOUT_MS, MetaApiClient } from '../meta/client.js';
|
|
29
28
|
import { MetaService, hostBaseUrl } from '../meta/service.js';
|
|
30
|
-
import {
|
|
29
|
+
import { DETECT_TIMEOUT_MS, detectSystem, transportFor } from '../meta/system.js';
|
|
31
30
|
import { RegaService } from '../rega/client.js';
|
|
32
31
|
import { listDevicesAnswer } from '../rpc/server.js';
|
|
33
32
|
import { ApiEventEmitter } from '../util/emitter.js';
|
|
@@ -709,22 +708,14 @@ export class Backend {
|
|
|
709
708
|
if (baseUrl === '') {
|
|
710
709
|
return { answer: undefined };
|
|
711
710
|
}
|
|
712
|
-
const client = new MetaApiClient({ baseUrl, fetch: this.#systemFetch(connection) });
|
|
713
711
|
// B-67: the redirect to https:// is followed, and a certificate nothing trusts is said
|
|
714
|
-
const found = await
|
|
712
|
+
const found = await detectSystem(transportFor({ fetch: this.#options.metaOptions?.fetch, trust: connection.systemTrust }), baseUrl, this.#options.metaOptions?.detectTimeoutMs ?? DETECT_TIMEOUT_MS);
|
|
715
713
|
return {
|
|
716
714
|
answer: found.version,
|
|
717
715
|
baseUrl: found.baseUrl,
|
|
718
716
|
...(found.certificate === undefined ? {} : { certificate: found.certificate }),
|
|
719
717
|
};
|
|
720
718
|
}
|
|
721
|
-
/**
|
|
722
|
-
* B-67: the `fetch` for the system's own APIs - with the certificates the profile trusts. The
|
|
723
|
-
* tests inject theirs through `metaOptions.fetch`, which wins.
|
|
724
|
-
*/
|
|
725
|
-
#systemFetch(connection) {
|
|
726
|
-
return this.#options.metaOptions?.fetch ?? createSystemFetch(connection.systemTrust);
|
|
727
|
-
}
|
|
728
719
|
async #disconnect() {
|
|
729
720
|
this.#clearTimers();
|
|
730
721
|
this.#queue.cancel();
|
|
@@ -775,7 +766,6 @@ export class Backend {
|
|
|
775
766
|
onNotice: (level, message) => {
|
|
776
767
|
this.#notice(level, message);
|
|
777
768
|
},
|
|
778
|
-
fetch: this.#systemFetch(connection),
|
|
779
769
|
...this.#options.metaOptions,
|
|
780
770
|
});
|
|
781
771
|
if (this.#stopped) {
|
|
@@ -1342,17 +1332,9 @@ export class Backend {
|
|
|
1342
1332
|
*/
|
|
1343
1333
|
async #groupsClient() {
|
|
1344
1334
|
await this.#metaReady?.catch(() => undefined);
|
|
1345
|
-
|
|
1346
|
-
const
|
|
1347
|
-
|
|
1348
|
-
return undefined;
|
|
1349
|
-
}
|
|
1350
|
-
return new HeatingGroupsClient({
|
|
1351
|
-
baseUrl,
|
|
1352
|
-
credential: () => meta.boxCredential(),
|
|
1353
|
-
// B-67: the same trust as the store's, on the https:// URL the detection found
|
|
1354
|
-
fetch: this.#systemFetch(this.#config.connection),
|
|
1355
|
-
});
|
|
1335
|
+
// B-67: the same transport and trust as the store's, on the https:// URL the detection found
|
|
1336
|
+
const system = this.#meta?.boxLink();
|
|
1337
|
+
return system === undefined ? undefined : new HeatingGroupsClient({ system });
|
|
1356
1338
|
}
|
|
1357
1339
|
/** `groups.state`: no box is an answer, not an error - it is what every CCU says. */
|
|
1358
1340
|
async #groupsState() {
|
|
@@ -8,13 +8,15 @@
|
|
|
8
8
|
* JSON API with the box's own login - the one this application already has for the metadata store
|
|
9
9
|
* (D-40). This client speaks that API and nothing else; on a CCU it is never built.
|
|
10
10
|
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
11
|
+
* The requests are occulite-client's `system.groups` (task 72), on the same transport and with the
|
|
12
|
+
* same certificate trust as the metadata store; what stays here is the contract's shape and a
|
|
13
|
+
* thrown {@link BackendError} whose `kind` says what the UI should make of it. The box's error body
|
|
13
14
|
* (`{error, message}`) is kept, because its message is the one worth showing - "there is no group
|
|
14
15
|
* 7", "members: hmipserver's device ids, as GET /groups/types lists them".
|
|
15
16
|
*/
|
|
16
17
|
import type { HeatingGroupChange, HeatingGroupDetail, HeatingGroupList, HeatingGroupMember, HeatingGroupType, HeatingGroupsState } from '@homematic-manager/core';
|
|
17
18
|
import { BackendError } from '../errors.js';
|
|
19
|
+
import type { SystemLink } from '../meta/system.js';
|
|
18
20
|
/**
|
|
19
21
|
* How long a call may take. A change makes the box's group process configure direct links, and the
|
|
20
22
|
* box itself waits up to thirty seconds for that process before it answers `502`; this has to
|
|
@@ -22,17 +24,14 @@ import { BackendError } from '../errors.js';
|
|
|
22
24
|
*/
|
|
23
25
|
export declare const GROUPS_TIMEOUT_MS = 45000;
|
|
24
26
|
export interface HeatingGroupsClientOptions {
|
|
25
|
-
/** `http://ccu` or `http://127.0.0.1` - scheme and authority, no path. */
|
|
26
|
-
readonly baseUrl: string;
|
|
27
27
|
/**
|
|
28
|
-
* The credential every call goes out with. Reads need the
|
|
29
|
-
* `system:write`: the person's session on the box, or the API token
|
|
30
|
-
* local token by choice - it reads metadata and nothing else, and the
|
|
28
|
+
* The box, with the credential every call goes out with (its write credential). Reads need the
|
|
29
|
+
* box's `system:read`, changes `system:write`: the person's session on the box, or the API token
|
|
30
|
+
* off it. Never the addon's local token by choice - it reads metadata and nothing else, and the
|
|
31
|
+
* box answers 403.
|
|
31
32
|
*/
|
|
32
|
-
readonly
|
|
33
|
+
readonly system: SystemLink;
|
|
33
34
|
readonly timeoutMs?: number;
|
|
34
|
-
/** Injected by the tests. */
|
|
35
|
-
readonly fetch?: typeof globalThis.fetch;
|
|
36
35
|
}
|
|
37
36
|
/** The box refused or could not answer; `status` and `code` are what it said. */
|
|
38
37
|
export declare class HeatingGroupsApiError extends BackendError {
|
|
@@ -8,11 +8,13 @@
|
|
|
8
8
|
* JSON API with the box's own login - the one this application already has for the metadata store
|
|
9
9
|
* (D-40). This client speaks that API and nothing else; on a CCU it is never built.
|
|
10
10
|
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
11
|
+
* The requests are occulite-client's `system.groups` (task 72), on the same transport and with the
|
|
12
|
+
* same certificate trust as the metadata store; what stays here is the contract's shape and a
|
|
13
|
+
* thrown {@link BackendError} whose `kind` says what the UI should make of it. The box's error body
|
|
13
14
|
* (`{error, message}`) is kept, because its message is the one worth showing - "there is no group
|
|
14
15
|
* 7", "members: hmipserver's device ids, as GET /groups/types lists them".
|
|
15
16
|
*/
|
|
17
|
+
import { OccuLiteError } from 'occulite-client';
|
|
16
18
|
import { BackendError } from '../errors.js';
|
|
17
19
|
/**
|
|
18
20
|
* How long a call may take. A change makes the box's group process configure direct links, and the
|
|
@@ -51,13 +53,11 @@ function kindOf(status, code) {
|
|
|
51
53
|
}
|
|
52
54
|
export class HeatingGroupsClient {
|
|
53
55
|
#options;
|
|
54
|
-
#fetch;
|
|
55
56
|
constructor(options) {
|
|
56
57
|
this.#options = options;
|
|
57
|
-
this.#fetch = options.fetch ?? globalThis.fetch;
|
|
58
58
|
}
|
|
59
59
|
get baseUrl() {
|
|
60
|
-
return this.#options.baseUrl;
|
|
60
|
+
return this.#options.system.baseUrl;
|
|
61
61
|
}
|
|
62
62
|
/**
|
|
63
63
|
* Does this box have the groups API, and may this credential read it? One `GET /groups`; the
|
|
@@ -65,7 +65,7 @@ export class HeatingGroupsClient {
|
|
|
65
65
|
*/
|
|
66
66
|
async probe() {
|
|
67
67
|
try {
|
|
68
|
-
await this.#
|
|
68
|
+
await this.#call((groups, options) => groups.list(options));
|
|
69
69
|
return { available: true };
|
|
70
70
|
}
|
|
71
71
|
catch (error) {
|
|
@@ -90,8 +90,8 @@ export class HeatingGroupsClient {
|
|
|
90
90
|
* between the two calls) is listed without members rather than failing the list.
|
|
91
91
|
*/
|
|
92
92
|
async list() {
|
|
93
|
-
const answer = (await this.#
|
|
94
|
-
const groups = await Promise.all((answer
|
|
93
|
+
const answer = (await this.#call((groups, options) => groups.list(options)));
|
|
94
|
+
const groups = await Promise.all((answer?.groups ?? []).map(async (wire) => {
|
|
95
95
|
let members = [];
|
|
96
96
|
try {
|
|
97
97
|
members = (await this.get(wire.id)).members;
|
|
@@ -101,12 +101,12 @@ export class HeatingGroupsClient {
|
|
|
101
101
|
}
|
|
102
102
|
return { ...toGroup(wire), members };
|
|
103
103
|
}));
|
|
104
|
-
return { groups, devicesToConfigure: answer
|
|
104
|
+
return { groups, devicesToConfigure: answer?.devices_to_configure ?? [] };
|
|
105
105
|
}
|
|
106
106
|
/** `GET /groups/types` - what a new group can be, and what each type could take now. */
|
|
107
107
|
async types() {
|
|
108
|
-
const
|
|
109
|
-
return (
|
|
108
|
+
const types = (await this.#call((groups, options) => groups.types(options)));
|
|
109
|
+
return (types ?? []).map((type) => ({
|
|
110
110
|
id: type.id ?? '',
|
|
111
111
|
label: type.label ?? type.id ?? '',
|
|
112
112
|
assignable: type.assignable ?? [],
|
|
@@ -115,11 +115,11 @@ export class HeatingGroupsClient {
|
|
|
115
115
|
}
|
|
116
116
|
/** `GET /groups/{id}`. */
|
|
117
117
|
async get(id) {
|
|
118
|
-
return toDetail(
|
|
118
|
+
return toDetail(await this.#call((groups, options) => groups.get(id, options)));
|
|
119
119
|
}
|
|
120
120
|
/** `POST /groups` - the box's `create` then `save`, and the metadata side effects in one go. */
|
|
121
121
|
async create(name, type, members) {
|
|
122
|
-
return toChange(
|
|
122
|
+
return toChange(await this.#call((groups, options) => groups.create({ name, type, members: [...members] }, options)));
|
|
123
123
|
}
|
|
124
124
|
/**
|
|
125
125
|
* `PUT /groups/{id}` - the name and the members as a whole; a field left out keeps its value.
|
|
@@ -130,49 +130,22 @@ export class HeatingGroupsClient {
|
|
|
130
130
|
...(change.name === undefined ? {} : { name: change.name }),
|
|
131
131
|
...(change.members === undefined ? {} : { members: [...change.members] }),
|
|
132
132
|
};
|
|
133
|
-
return toChange(
|
|
133
|
+
return toChange(await this.#call((groups, options) => groups.update(id, body, options)));
|
|
134
134
|
}
|
|
135
135
|
/** `DELETE /groups/{id}` - answers with the former members, whose group membership is gone. */
|
|
136
136
|
async remove(id) {
|
|
137
|
-
const answer = (await this.#
|
|
138
|
-
return answer
|
|
137
|
+
const answer = (await this.#call((groups, options) => groups.remove(id, options)));
|
|
138
|
+
return answer?.former_members ?? [];
|
|
139
139
|
}
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
}
|
|
143
|
-
async #json(method, route, body) {
|
|
144
|
-
const credential = this.#options.credential();
|
|
145
|
-
let response;
|
|
140
|
+
/** One call of the package's `system.groups`, its errors turned into the contract's. */
|
|
141
|
+
async #call(run) {
|
|
146
142
|
try {
|
|
147
|
-
|
|
148
|
-
|
|
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) }),
|
|
143
|
+
return await run(this.#options.system.client('write').system.groups, {
|
|
144
|
+
timeout: this.#options.timeoutMs ?? GROUPS_TIMEOUT_MS,
|
|
156
145
|
});
|
|
157
146
|
}
|
|
158
147
|
catch (error) {
|
|
159
|
-
|
|
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 });
|
|
148
|
+
throw refusal(error, this.baseUrl);
|
|
176
149
|
}
|
|
177
150
|
}
|
|
178
151
|
}
|
|
@@ -206,26 +179,26 @@ function toDetail(wire) {
|
|
|
206
179
|
function toChange(wire) {
|
|
207
180
|
return { ...toDetail(wire), devicesToConfigure: wire.devices_to_configure ?? [] };
|
|
208
181
|
}
|
|
209
|
-
/**
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
if (
|
|
226
|
-
message = `the system refused the credential for the heating groups (${String(
|
|
227
|
-
}
|
|
228
|
-
return new HeatingGroupsApiError(
|
|
182
|
+
/**
|
|
183
|
+
* The API's `{error, message}` answer as the error; a box that did not answer (off, the change
|
|
184
|
+
* outlasted the timeout above, a certificate nobody trusts) as a connection error.
|
|
185
|
+
*/
|
|
186
|
+
function refusal(error, baseUrl) {
|
|
187
|
+
if (!(error instanceof OccuLiteError) || error.status === undefined) {
|
|
188
|
+
return new BackendError({ message: `the groups API at ${baseUrl} did not answer: ${errorText(error)}`, kind: 'connection' }, { cause: error });
|
|
189
|
+
}
|
|
190
|
+
const status = error.status;
|
|
191
|
+
const detail = error.detail;
|
|
192
|
+
let message = typeof detail?.message === 'string' ? detail.message : error.message;
|
|
193
|
+
const code = error.code.startsWith('http-')
|
|
194
|
+
? status === 401 || status === 403
|
|
195
|
+
? 'forbidden'
|
|
196
|
+
: 'error'
|
|
197
|
+
: error.code;
|
|
198
|
+
if (status === 401 || status === 403) {
|
|
199
|
+
message = `the system refused the credential for the heating groups (${String(status)}): ${message}`;
|
|
200
|
+
}
|
|
201
|
+
return new HeatingGroupsApiError(status, code, message);
|
|
229
202
|
}
|
|
230
203
|
function errorText(error) {
|
|
231
204
|
return error instanceof Error ? error.message : String(error);
|
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* D-40: the metadata store - names, rooms, functions and any taxonomy a user adds - with the
|
|
3
|
-
* providers behind it
|
|
2
|
+
* D-40: the metadata store - names, rooms, functions and any taxonomy a user adds - with the
|
|
3
|
+
* providers behind it and the connection to openccu-lite's API (occulite-client, task 72).
|
|
4
4
|
*/
|
|
5
|
-
export * from './client.js';
|
|
6
|
-
export * from './credentials.js';
|
|
7
5
|
export * from './provider.js';
|
|
8
6
|
export * from './localProvider.js';
|
|
9
7
|
export * from './occuliteProvider.js';
|
|
10
8
|
export * from './regaProvider.js';
|
|
11
9
|
export * from './service.js';
|
|
10
|
+
export * from './system.js';
|
|
12
11
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* D-40: the metadata store - names, rooms, functions and any taxonomy a user adds - with the
|
|
3
|
-
* providers behind it
|
|
2
|
+
* D-40: the metadata store - names, rooms, functions and any taxonomy a user adds - with the
|
|
3
|
+
* providers behind it and the connection to openccu-lite's API (occulite-client, task 72).
|
|
4
4
|
*/
|
|
5
|
-
export * from './client.js';
|
|
6
|
-
export * from './credentials.js';
|
|
7
5
|
export * from './provider.js';
|
|
8
6
|
export * from './localProvider.js';
|
|
9
7
|
export * from './occuliteProvider.js';
|
|
10
8
|
export * from './regaProvider.js';
|
|
11
9
|
export * from './service.js';
|
|
10
|
+
export * from './system.js';
|
|
12
11
|
//# sourceMappingURL=index.js.map
|
|
@@ -19,16 +19,23 @@
|
|
|
19
19
|
*
|
|
20
20
|
* The snapshot is persisted where the ReGa provider persists its names, for the same reason: a
|
|
21
21
|
* restart while the box is unreachable must not leave the grid nameless.
|
|
22
|
+
*
|
|
23
|
+
* The wire is occulite-client's (task 72): the requests, the writes on the API's routes, the
|
|
24
|
+
* certificate trust and the Server-Sent Events parser are the package's, reached through
|
|
25
|
+
* {@link SystemLink}. What stays here is the policy - when to re-read, how long to wait, what the
|
|
26
|
+
* state says - and the document itself, which is core's and follows the stream with core's
|
|
27
|
+
* `applyEvent`, the code the conformance corpus runs against.
|
|
22
28
|
*/
|
|
23
29
|
import { summariseAll, type MetaDocument, type MetaImportMode, type MetaNodePatch, type MetaState } from '@homematic-manager/core';
|
|
24
|
-
import { MetaApiClient } from './client.js';
|
|
25
30
|
import type { MetadataProvider, MetaMembershipEntry, MetaNameEntry, MetaProviderEvents } from './provider.js';
|
|
31
|
+
import type { SystemLink } from './system.js';
|
|
26
32
|
/** First wait before the event stream is opened again, in milliseconds. */
|
|
27
33
|
export declare const RECONNECT_MIN_MS = 2000;
|
|
28
34
|
/** Longest wait between two attempts. A box that is being updated is back well inside a minute. */
|
|
29
35
|
export declare const RECONNECT_MAX_MS = 60000;
|
|
30
36
|
export interface OcculiteProviderOptions extends MetaProviderEvents {
|
|
31
|
-
|
|
37
|
+
/** The box, with the read and the write credential (occulite-client underneath). */
|
|
38
|
+
readonly system: SystemLink;
|
|
32
39
|
/** `<cacheDir>/occulite-meta.json`: the last snapshot, so a restart without the box has names. */
|
|
33
40
|
readonly cacheFile?: string;
|
|
34
41
|
/** `occulited 0.1.0`, from the detection call. */
|
|
@@ -19,11 +19,16 @@
|
|
|
19
19
|
*
|
|
20
20
|
* The snapshot is persisted where the ReGa provider persists its names, for the same reason: a
|
|
21
21
|
* restart while the box is unreachable must not leave the grid nameless.
|
|
22
|
+
*
|
|
23
|
+
* The wire is occulite-client's (task 72): the requests, the writes on the API's routes, the
|
|
24
|
+
* certificate trust and the Server-Sent Events parser are the package's, reached through
|
|
25
|
+
* {@link SystemLink}. What stays here is the policy - when to re-read, how long to wait, what the
|
|
26
|
+
* state says - and the document itself, which is core's and follows the stream with core's
|
|
27
|
+
* `applyEvent`, the code the conformance corpus runs against.
|
|
22
28
|
*/
|
|
23
29
|
import { applyEvent, emptyDocument, isMetaError, parseDocument, summariseAll, } from '@homematic-manager/core';
|
|
24
30
|
import { errorMessage } from '../errors.js';
|
|
25
31
|
import { readJsonFile, writeJsonFile } from '../util/jsonFile.js';
|
|
26
|
-
import { MetaApiClient } from './client.js';
|
|
27
32
|
/** First wait before the event stream is opened again, in milliseconds. */
|
|
28
33
|
export const RECONNECT_MIN_MS = 2000;
|
|
29
34
|
/** Longest wait between two attempts. A box that is being updated is back well inside a minute. */
|
|
@@ -31,7 +36,7 @@ export const RECONNECT_MAX_MS = 60_000;
|
|
|
31
36
|
export class OcculiteProvider {
|
|
32
37
|
kind = 'occulite';
|
|
33
38
|
#options;
|
|
34
|
-
#
|
|
39
|
+
#system;
|
|
35
40
|
#document = emptyDocument();
|
|
36
41
|
#reachable = false;
|
|
37
42
|
#writable = true;
|
|
@@ -44,7 +49,7 @@ export class OcculiteProvider {
|
|
|
44
49
|
#wake;
|
|
45
50
|
constructor(options) {
|
|
46
51
|
this.#options = options;
|
|
47
|
-
this.#
|
|
52
|
+
this.#system = options.system;
|
|
48
53
|
}
|
|
49
54
|
state() {
|
|
50
55
|
return {
|
|
@@ -53,7 +58,7 @@ export class OcculiteProvider {
|
|
|
53
58
|
writable: this.#writable,
|
|
54
59
|
revision: this.#document.revision,
|
|
55
60
|
objects: Object.keys(this.#document.objects).length,
|
|
56
|
-
url: this.#
|
|
61
|
+
url: this.#system.baseUrl,
|
|
57
62
|
...(this.#options.implementation === undefined ? {} : { implementation: this.#options.implementation }),
|
|
58
63
|
...(this.#error === undefined ? {} : { error: this.#error }),
|
|
59
64
|
};
|
|
@@ -105,7 +110,7 @@ export class OcculiteProvider {
|
|
|
105
110
|
for (const entry of entries) {
|
|
106
111
|
sets[entry.ref] = { name: entry.name };
|
|
107
112
|
}
|
|
108
|
-
await this.#write(() =>
|
|
113
|
+
await this.#write((meta) => meta.objects.bulk({ set: sets }));
|
|
109
114
|
}
|
|
110
115
|
async setMembership(entries) {
|
|
111
116
|
if (entries.length === 0) {
|
|
@@ -113,21 +118,21 @@ export class OcculiteProvider {
|
|
|
113
118
|
}
|
|
114
119
|
const sets = {};
|
|
115
120
|
for (const entry of entries) {
|
|
116
|
-
sets[entry.ref] = { enums: entry.paths };
|
|
121
|
+
sets[entry.ref] = { enums: [...entry.paths] };
|
|
117
122
|
}
|
|
118
|
-
await this.#write(() =>
|
|
123
|
+
await this.#write((meta) => meta.objects.bulk({ set: sets }));
|
|
119
124
|
}
|
|
120
125
|
async createEnum(id, name) {
|
|
121
|
-
await this.#write(() =>
|
|
126
|
+
await this.#write((meta) => meta.enums.create(id, { ...name }));
|
|
122
127
|
}
|
|
123
128
|
async updateEnum(id, name) {
|
|
124
|
-
await this.#write(() =>
|
|
129
|
+
await this.#write((meta) => meta.enums.update(id, { name: { ...name } }));
|
|
125
130
|
}
|
|
126
131
|
async deleteEnum(id, detach) {
|
|
127
|
-
await this.#write(() =>
|
|
132
|
+
await this.#write((meta) => meta.enums.delete(id, { detach }));
|
|
128
133
|
}
|
|
129
134
|
async createNode(enumId, parent, id, name, options) {
|
|
130
|
-
await this.#write(() =>
|
|
135
|
+
await this.#write((meta) => meta.tree.create(enumId, {
|
|
131
136
|
parent,
|
|
132
137
|
id,
|
|
133
138
|
name,
|
|
@@ -137,13 +142,19 @@ export class OcculiteProvider {
|
|
|
137
142
|
return `${parent ?? enumId}/${id}`;
|
|
138
143
|
}
|
|
139
144
|
async updateNode(path, patch) {
|
|
140
|
-
await this.#write(() =>
|
|
145
|
+
await this.#write((meta) => meta.tree.update(path, {
|
|
146
|
+
...(patch.name === undefined ? {} : { name: patch.name }),
|
|
147
|
+
...(patch.icon === undefined ? {} : { icon: patch.icon }),
|
|
148
|
+
...(patch.parent === undefined ? {} : { parent: patch.parent }),
|
|
149
|
+
...(patch.position === undefined ? {} : { position: patch.position }),
|
|
150
|
+
}));
|
|
141
151
|
}
|
|
142
152
|
async deleteNode(path, detach) {
|
|
143
|
-
await this.#write(() =>
|
|
153
|
+
await this.#write((meta) => meta.tree.delete(path, { detach }));
|
|
144
154
|
}
|
|
145
155
|
async import(document, mode) {
|
|
146
|
-
|
|
156
|
+
// a document object as JSON; a string goes as it is, which the API reads as YAML
|
|
157
|
+
await this.#write((meta) => meta.import(document, { mode }));
|
|
147
158
|
}
|
|
148
159
|
/**
|
|
149
160
|
* One write, then a fresh snapshot.
|
|
@@ -155,7 +166,7 @@ export class OcculiteProvider {
|
|
|
155
166
|
*/
|
|
156
167
|
async #write(call) {
|
|
157
168
|
try {
|
|
158
|
-
const answer = await call
|
|
169
|
+
const answer = await this.#system.write(call);
|
|
159
170
|
if (!this.#writable) {
|
|
160
171
|
this.#writable = true;
|
|
161
172
|
this.#options.onStateChanged(this.state());
|
|
@@ -175,7 +186,7 @@ export class OcculiteProvider {
|
|
|
175
186
|
/** `GET /snapshot`, with every failure turned into a state instead of an exception. */
|
|
176
187
|
async #refresh() {
|
|
177
188
|
try {
|
|
178
|
-
const document = await this.#
|
|
189
|
+
const document = await this.#system.snapshot();
|
|
179
190
|
this.#document = document;
|
|
180
191
|
this.#reachable = true;
|
|
181
192
|
this.#error = undefined;
|
|
@@ -205,7 +216,7 @@ export class OcculiteProvider {
|
|
|
205
216
|
let wait = this.#options.reconnectMinMs ?? RECONNECT_MIN_MS;
|
|
206
217
|
while (!aborted()) {
|
|
207
218
|
try {
|
|
208
|
-
for await (const event of this.#
|
|
219
|
+
for await (const event of this.#system.events(this.#document.revision, signal)) {
|
|
209
220
|
wait = this.#options.reconnectMinMs ?? RECONNECT_MIN_MS;
|
|
210
221
|
if (!this.#reachable) {
|
|
211
222
|
this.#reachable = true;
|
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
import { type ConnectionConfig, type MetaDocument, type MetaEnum, type MetaHmipPairing, type MetaImportMode, type MetaNodePatch, type MetaObjectView, type MetaState, type MetaVersion, type SystemCertificateProblem } from '@homematic-manager/core';
|
|
20
20
|
import type { NameStore } from '../cache/names.js';
|
|
21
21
|
import { type RegaScriptRunner } from './regaProvider.js';
|
|
22
|
+
import { SystemLink } from './system.js';
|
|
22
23
|
/** What the service needs to know about ReGa to offer it as a provider (task 27). */
|
|
23
24
|
export interface MetaRegaLink {
|
|
24
25
|
/** ReGa is on and has answered at least once - the `auto` choice takes it only then. */
|
|
@@ -56,11 +57,28 @@ export interface MetaServiceOptions {
|
|
|
56
57
|
* common case probes once; a store at another `metaUrl` is still probed on its own.
|
|
57
58
|
*/
|
|
58
59
|
readonly hostProbe?: HostProbe | undefined;
|
|
59
|
-
/**
|
|
60
|
+
/**
|
|
61
|
+
* Injected by the tests: the local token file (occulite-client's `LOCAL_TOKEN_FILE` otherwise),
|
|
62
|
+
* and a `fetch` in place of the transport that carries the profile's certificate trust.
|
|
63
|
+
*/
|
|
60
64
|
readonly localTokenFile?: string | undefined;
|
|
61
65
|
readonly fetch?: typeof globalThis.fetch;
|
|
62
66
|
readonly detectTimeoutMs?: number;
|
|
63
67
|
}
|
|
68
|
+
/**
|
|
69
|
+
* A session id as the box hands it over: `@xxxxxxxxxx@`, the CCU convention, or bare.
|
|
70
|
+
*
|
|
71
|
+
* The API accepts both spellings; this normalises to the bare one so that one shape goes on the
|
|
72
|
+
* wire and a log line stays readable.
|
|
73
|
+
*/
|
|
74
|
+
export declare function normaliseSid(value: string | undefined): string | undefined;
|
|
75
|
+
/**
|
|
76
|
+
* The box's local token, or `undefined` when there is no file - which is every installation that
|
|
77
|
+
* is not the addon on an openccu-lite box. On the box it is the token for programs running there:
|
|
78
|
+
* it reads everything and writes nothing, on purpose (the addon reads as itself and writes as the
|
|
79
|
+
* person looking at the page). Read by occulite-client's credential resolution; never throws.
|
|
80
|
+
*/
|
|
81
|
+
export declare function readLocalToken(file?: string): Promise<string | undefined>;
|
|
64
82
|
/**
|
|
65
83
|
* B-62, B-67: what the connect found at the host - the version answer (undefined for a CCU), the
|
|
66
84
|
* base URL after the system's redirect to `https://`, and the certificate problem there.
|
|
@@ -173,11 +191,12 @@ export declare class MetaService {
|
|
|
173
191
|
*/
|
|
174
192
|
get boxUrl(): string | undefined;
|
|
175
193
|
/**
|
|
176
|
-
* Task 57: the
|
|
177
|
-
* write to the store: the person's session
|
|
178
|
-
* the
|
|
179
|
-
* metadata and nothing else, and a group is
|
|
194
|
+
* Task 57: the box's other APIs, while the store is the box's - `undefined` otherwise. A call
|
|
195
|
+
* to its system API goes out with the credential of a write to the store: the person's session
|
|
196
|
+
* first, then the token the profile configured, then the local token - which the box refuses
|
|
197
|
+
* there, as it should: an addon's local token reads metadata and nothing else, and a group is
|
|
198
|
+
* changed by a person. The same transport, so the same certificate trust (B-67).
|
|
180
199
|
*/
|
|
181
|
-
|
|
200
|
+
boxLink(): SystemLink | undefined;
|
|
182
201
|
}
|
|
183
202
|
//# sourceMappingURL=service.d.ts.map
|