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.
Files changed (26) hide show
  1. package/node_modules/@homematic-manager/backend/dist/api/backend.js +5 -23
  2. package/node_modules/@homematic-manager/backend/dist/groups/client.d.ts +9 -10
  3. package/node_modules/@homematic-manager/backend/dist/groups/client.js +41 -68
  4. package/node_modules/@homematic-manager/backend/dist/meta/index.d.ts +3 -4
  5. package/node_modules/@homematic-manager/backend/dist/meta/index.js +3 -4
  6. package/node_modules/@homematic-manager/backend/dist/meta/occuliteProvider.d.ts +9 -2
  7. package/node_modules/@homematic-manager/backend/dist/meta/occuliteProvider.js +28 -17
  8. package/node_modules/@homematic-manager/backend/dist/meta/service.d.ts +25 -6
  9. package/node_modules/@homematic-manager/backend/dist/meta/service.js +55 -18
  10. package/node_modules/@homematic-manager/backend/dist/meta/system.d.ts +150 -0
  11. package/node_modules/@homematic-manager/backend/dist/meta/system.js +455 -0
  12. package/node_modules/@homematic-manager/backend/dist/transport/wsServer.d.ts +10 -1
  13. package/node_modules/@homematic-manager/backend/dist/transport/wsServer.js +48 -6
  14. package/node_modules/@homematic-manager/backend/package.json +2 -2
  15. package/node_modules/@homematic-manager/core/dist/easymodes/engine.d.ts +16 -3
  16. package/node_modules/@homematic-manager/core/dist/easymodes/engine.js +79 -27
  17. package/node_modules/@homematic-manager/core/package.json +1 -1
  18. package/package.json +5 -4
  19. package/ui/assets/{index-B7H_fshR.js → index-Co1znn_z.js} +13 -13
  20. package/ui/index.html +1 -1
  21. package/node_modules/@homematic-manager/backend/dist/meta/client.d.ts +0 -148
  22. package/node_modules/@homematic-manager/backend/dist/meta/client.js +0 -381
  23. package/node_modules/@homematic-manager/backend/dist/meta/credentials.d.ts +0 -38
  24. package/node_modules/@homematic-manager/backend/dist/meta/credentials.js +0 -53
  25. package/node_modules/@homematic-manager/backend/dist/meta/systemFetch.d.ts +0 -45
  26. 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 { createSystemFetch } from '../meta/systemFetch.js';
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 client.detect(this.#options.metaOptions?.detectTimeoutMs ?? DETECT_TIMEOUT_MS);
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
- const meta = this.#meta;
1346
- const baseUrl = meta?.boxUrl;
1347
- if (meta === undefined || baseUrl === undefined) {
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
- * 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
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 box's `system:read`, changes
29
- * `system:write`: the person's session on the box, or the API token off it. Never the addon's
30
- * local token by choice - it reads metadata and nothing else, and the box answers 403.
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 credential: () => string | undefined;
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
- * 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
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.#json('GET', '/groups');
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.#json('GET', '/groups'));
94
- const groups = await Promise.all((answer.groups ?? []).map(async (wire) => {
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.devices_to_configure ?? [] };
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 answer = (await this.#json('GET', '/groups/types'));
109
- return (answer.types ?? []).map((type) => ({
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((await this.#json('GET', `/groups/${String(id)}`)));
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((await this.#json('POST', '/groups', { name, type, members: [...members] })));
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((await this.#json('PUT', `/groups/${String(id)}`, body)));
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.#json('DELETE', `/groups/${String(id)}`));
138
- return answer.former_members ?? [];
137
+ const answer = (await this.#call((groups, options) => groups.remove(id, options)));
138
+ return answer?.former_members ?? [];
139
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;
140
+ /** One call of the package's `system.groups`, its errors turned into the contract's. */
141
+ async #call(run) {
146
142
  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) }),
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
- // 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 });
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
- /** 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);
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 two
3
- * providers behind it, the HTTP client for openccu-lite's API and where the credentials come from.
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 two
3
- * providers behind it, the HTTP client for openccu-lite's API and where the credentials come from.
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
- readonly client: MetaApiClient;
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
- #client;
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.#client = options.client;
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.#client.baseUrl,
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(() => this.#client.bulk(sets));
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(() => this.#client.bulk(sets));
123
+ await this.#write((meta) => meta.objects.bulk({ set: sets }));
119
124
  }
120
125
  async createEnum(id, name) {
121
- await this.#write(() => this.#client.createEnum(id, name));
126
+ await this.#write((meta) => meta.enums.create(id, { ...name }));
122
127
  }
123
128
  async updateEnum(id, name) {
124
- await this.#write(() => this.#client.updateEnum(id, name));
129
+ await this.#write((meta) => meta.enums.update(id, { name: { ...name } }));
125
130
  }
126
131
  async deleteEnum(id, detach) {
127
- await this.#write(() => this.#client.deleteEnum(id, detach ? 'detach' : 'refuse'));
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(() => this.#client.createNode(enumId, {
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(() => this.#client.updateNode(path, patch));
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(() => this.#client.deleteNode(path, detach ? 'detach' : 'refuse'));
153
+ await this.#write((meta) => meta.tree.delete(path, { detach }));
144
154
  }
145
155
  async import(document, mode) {
146
- await this.#write(() => this.#client.import(document, mode));
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.#client.snapshot();
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.#client.events(this.#document.revision, signal)) {
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
- /** Injected by the tests. */
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 credential a call to the box's system API goes out with. The same order as a
177
- * write to the store: the person's session first, then the token the profile configured, then
178
- * the local token - which the box refuses there, as it should: an addon's local token reads
179
- * metadata and nothing else, and a group is changed by a person.
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
- boxCredential(): string | undefined;
200
+ boxLink(): SystemLink | undefined;
182
201
  }
183
202
  //# sourceMappingURL=service.d.ts.map