homematic-manager 3.0.0-beta.4 → 3.0.0-beta.6

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 (31) hide show
  1. package/dist/cli.js +10 -0
  2. package/dist/server.d.ts +2 -0
  3. package/dist/server.js +1 -0
  4. package/node_modules/@homematic-manager/backend/dist/api/backend.js +40 -2
  5. package/node_modules/@homematic-manager/backend/dist/meta/index.d.ts +1 -0
  6. package/node_modules/@homematic-manager/backend/dist/meta/index.js +1 -0
  7. package/node_modules/@homematic-manager/backend/dist/meta/provider.d.ts +6 -5
  8. package/node_modules/@homematic-manager/backend/dist/meta/provider.js +5 -4
  9. package/node_modules/@homematic-manager/backend/dist/meta/regaProvider.d.ts +84 -0
  10. package/node_modules/@homematic-manager/backend/dist/meta/regaProvider.js +403 -0
  11. package/node_modules/@homematic-manager/backend/dist/meta/service.d.ts +18 -1
  12. package/node_modules/@homematic-manager/backend/dist/meta/service.js +53 -0
  13. package/node_modules/@homematic-manager/backend/dist/rega/client.d.ts +11 -0
  14. package/node_modules/@homematic-manager/backend/dist/rega/client.js +13 -0
  15. package/node_modules/@homematic-manager/backend/dist/rega/scripts.d.ts +51 -0
  16. package/node_modules/@homematic-manager/backend/dist/rega/scripts.js +171 -0
  17. package/node_modules/@homematic-manager/backend/dist/util/net.d.ts +7 -1
  18. package/node_modules/@homematic-manager/backend/dist/util/net.js +8 -1
  19. package/node_modules/@homematic-manager/backend/package.json +1 -1
  20. package/node_modules/@homematic-manager/core/dist/api/types.d.ts +27 -8
  21. package/node_modules/@homematic-manager/core/dist/api/types.js +2 -2
  22. package/node_modules/@homematic-manager/core/dist/meta/paths.js +3 -2
  23. package/node_modules/@homematic-manager/core/dist/rpc/methods.d.ts +10 -0
  24. package/node_modules/@homematic-manager/core/dist/rpc/methods.js +51 -1
  25. package/node_modules/@homematic-manager/core/package.json +1 -1
  26. package/package.json +3 -3
  27. package/ui/assets/index-C99NI3-M.css +1 -0
  28. package/ui/assets/index-Mfj6d7fY.js +14 -0
  29. package/ui/index.html +2 -2
  30. package/ui/assets/index-0KwZUy-l.js +0 -13
  31. package/ui/assets/index-qbhgN2pA.css +0 -1
package/dist/cli.js CHANGED
@@ -10,6 +10,7 @@
10
10
  */
11
11
  import { realpathSync } from 'node:fs';
12
12
  import { pathToFileURL } from 'node:url';
13
+ import { isLoopbackHost } from './auth.js';
13
14
  import { createWebHost } from './server.js';
14
15
  import { installService, uninstallService } from './install.js';
15
16
  import { createLogger } from './log.js';
@@ -93,6 +94,15 @@ export async function runCli(options = {}) {
93
94
  log.debug(`token ${host.token}`);
94
95
  }
95
96
  log.debug(`a client without a cookie can use ${host.url}?token=${host.token}`);
97
+ if (host.token !== undefined && host.issueCookie && !isLoopbackHost(parsed.host)) {
98
+ // D-41: the Docker image binds 0.0.0.0 and issues the cookie so that the UI works out of
99
+ // the box - which means whoever reaches the port is in. Said once, at start, with the ways
100
+ // out; the host cannot see a proxy or TLS in front of it, so it says this whenever it is
101
+ // in that position
102
+ log.warn(`the token cookie is handed to every browser that loads the page on ${parsed.host}: whoever reaches this port is in;` +
103
+ ' put a proxy with authentication and TLS in front, publish the port on the loopback only,' +
104
+ ' or set HMM_ISSUE_COOKIE=false and open the UI once with ?token=');
105
+ }
96
106
  if (parsed.authMode === 'rega') {
97
107
  // D-32: the token still works and `settings.cgi` still hands it out - the login is a second
98
108
  // door, not a replacement, and saying so keeps the addon's two paths straight in the log
package/dist/server.d.ts CHANGED
@@ -142,6 +142,8 @@ export interface WebHost {
142
142
  readonly base: string;
143
143
  /** `undefined` when auth is off. */
144
144
  readonly token: string | undefined;
145
+ /** Whether the page load hands the token to the browser as a cookie (D-41 says when to warn). */
146
+ readonly issueCookie: boolean;
145
147
  /** D-32: how a browser is let in. */
146
148
  readonly authMode: AuthMode;
147
149
  /** D-32/D-40: the login sessions; `undefined` in `token` mode, where there are none. */
package/dist/server.js CHANGED
@@ -465,6 +465,7 @@ export async function createWebHost(options = {}) {
465
465
  port,
466
466
  base,
467
467
  token,
468
+ issueCookie,
468
469
  authMode,
469
470
  sessions,
470
471
  close,
@@ -342,6 +342,11 @@ export class Backend {
342
342
  case 'meta.node.delete':
343
343
  await (await this.#requireMeta()).deleteNode(p[0], p[1] === true);
344
344
  return null;
345
+ case 'meta.refresh': {
346
+ const meta = await this.#requireMeta();
347
+ await meta.refresh();
348
+ return meta.state();
349
+ }
345
350
  case 'meta.export':
346
351
  return (await this.#requireMeta()).document();
347
352
  case 'meta.import':
@@ -540,6 +545,14 @@ export class Backend {
540
545
  cacheDir: this.#config.cacheDir,
541
546
  names: this.#caches.names,
542
547
  interfaceOf: (address) => this.#interfaceOf(address),
548
+ // task 27: ReGa as the store of rooms and functions on a CCU. Read through the
549
+ // service that is current at call time - a reconnect replaces it.
550
+ rega: this.#rega === undefined || !connection.rega
551
+ ? undefined
552
+ : {
553
+ available: this.#rega.available,
554
+ exec: (script) => this.#requireRega().exec(script),
555
+ },
543
556
  onChanged: () => {
544
557
  this.#onMetaChanged();
545
558
  },
@@ -567,6 +580,12 @@ export class Backend {
567
580
  this.#notice('warn', `the metadata store could not be opened: ${errorMessage(error)}`);
568
581
  }
569
582
  }
583
+ #requireRega() {
584
+ if (!this.#rega) {
585
+ throw configError('ReGa is not connected');
586
+ }
587
+ return this.#rega;
588
+ }
570
589
  /** The store changed - locally, or on the box because somebody else edited it. */
571
590
  #onMetaChanged() {
572
591
  const meta = this.#meta;
@@ -869,7 +888,22 @@ export class Backend {
869
888
  async #setNames(entries) {
870
889
  const written = this.#caches.names.set(entries);
871
890
  this.#caches.saveNames();
872
- await this.#rega?.rename(written);
891
+ // task 27: with ReGa as the metadata store the provider writes the rename itself, and a
892
+ // second `Name()` through the name service would be the same script twice - but only for
893
+ // the objects the provider holds, and only while ReGa answers it; anything else still goes
894
+ // the way it always did (found by the e2e suite: a rename lost for good is worse than one
895
+ // script twice)
896
+ const meta = this.#meta;
897
+ const held = meta?.kind === 'rega' && meta.state().reachable ? meta.document().objects : undefined;
898
+ const throughNames = held === undefined
899
+ ? written
900
+ : written.filter((entry) => {
901
+ const ref = meta?.refFor(entry.address);
902
+ return ref === undefined || !(ref in held);
903
+ });
904
+ if (throughNames.length > 0) {
905
+ await this.#rega?.rename(throughNames);
906
+ }
873
907
  // D-40: and into the metadata store, which on an openccu-lite box is the box's own. The
874
908
  // local cache is written first either way, so a store that refuses the write still leaves
875
909
  // the name where the user typed it - and the refusal is reported rather than swallowed.
@@ -884,7 +918,10 @@ export class Backend {
884
918
  /** The state, even before a connection exists - the settings dialog asks for it either way. */
885
919
  #metaState() {
886
920
  return (this.#meta?.state() ?? {
887
- provider: this.#config.connection.metaProvider === 'occulite' ? 'occulite' : 'local',
921
+ provider: this.#config.connection.metaProvider === 'occulite' ||
922
+ this.#config.connection.metaProvider === 'rega'
923
+ ? this.#config.connection.metaProvider
924
+ : 'local',
888
925
  reachable: false,
889
926
  writable: false,
890
927
  revision: 0,
@@ -1325,6 +1362,7 @@ export const API_METHOD_NAMES = [
1325
1362
  'meta.node.create',
1326
1363
  'meta.node.update',
1327
1364
  'meta.node.delete',
1365
+ 'meta.refresh',
1328
1366
  'meta.export',
1329
1367
  'meta.import',
1330
1368
  'paramset.get',
@@ -7,5 +7,6 @@ export * from './credentials.js';
7
7
  export * from './provider.js';
8
8
  export * from './localProvider.js';
9
9
  export * from './occuliteProvider.js';
10
+ export * from './regaProvider.js';
10
11
  export * from './service.js';
11
12
  //# sourceMappingURL=index.d.ts.map
@@ -7,5 +7,6 @@ export * from './credentials.js';
7
7
  export * from './provider.js';
8
8
  export * from './localProvider.js';
9
9
  export * from './occuliteProvider.js';
10
+ export * from './regaProvider.js';
10
11
  export * from './service.js';
11
12
  //# sourceMappingURL=index.js.map
@@ -6,10 +6,11 @@
6
6
  * openccu-lite box, follows its change stream and **writes back**, because on that box this
7
7
  * application is the editor of the store: there is no ReGaHSS and no WebUI to do it instead.
8
8
  *
9
- * The ReGa path is untouched by all of this (their invariant 1, our D-2): ReGa still supplies names
10
- * on a CCU, still writes a rename back through its script, and a box that has ReGa never has a
11
- * metadata API. What the provider adds is the taxonomy ReGa's rooms and functions were, for the
12
- * systems that have no ReGa at all.
9
+ * `rega` (task 27) is the third: on a CCU with ReGaHSS the rooms and functions are ReGa's own enum
10
+ * objects, read and written through the script transport the names already use, so that one
11
+ * editing UI serves a CCU and a box. A box that has ReGa never has a metadata API, so the three
12
+ * never compete: the box's API wins when it answers, ReGa when it is on and answered, and `local`
13
+ * is what every other system - Homegear, a bare rfd, the desktop without a CCU - gets.
13
14
  */
14
15
  import type { MetaDocument, MetaEnum, MetaImportMode, MetaNodePatch, MetaState } from '@homematic-manager/core';
15
16
  /** One name to write: the store's identity is the ref, not the bare address. */
@@ -34,7 +35,7 @@ export interface MetaProviderEvents {
34
35
  * box on the network; every read is synchronous because both keep the document in memory.
35
36
  */
36
37
  export interface MetadataProvider {
37
- readonly kind: 'local' | 'occulite';
38
+ readonly kind: 'local' | 'occulite' | 'rega';
38
39
  state(): MetaState;
39
40
  /** Loads the document (a file, or the box's snapshot) and starts following changes. */
40
41
  start(): Promise<void>;
@@ -6,10 +6,11 @@
6
6
  * openccu-lite box, follows its change stream and **writes back**, because on that box this
7
7
  * application is the editor of the store: there is no ReGaHSS and no WebUI to do it instead.
8
8
  *
9
- * The ReGa path is untouched by all of this (their invariant 1, our D-2): ReGa still supplies names
10
- * on a CCU, still writes a rename back through its script, and a box that has ReGa never has a
11
- * metadata API. What the provider adds is the taxonomy ReGa's rooms and functions were, for the
12
- * systems that have no ReGa at all.
9
+ * `rega` (task 27) is the third: on a CCU with ReGaHSS the rooms and functions are ReGa's own enum
10
+ * objects, read and written through the script transport the names already use, so that one
11
+ * editing UI serves a CCU and a box. A box that has ReGa never has a metadata API, so the three
12
+ * never compete: the box's API wins when it answers, ReGa when it is on and answered, and `local`
13
+ * is what every other system - Homegear, a bare rfd, the desktop without a CCU - gets.
13
14
  */
14
15
  /** The enums of a document, for a caller that only wants the trees. */
15
16
  export function enumsOf(document) {
@@ -0,0 +1,84 @@
1
+ /**
2
+ * The `rega` metadata provider (task 27): the CCU's own rooms and functions, through ReGaHSS.
3
+ *
4
+ * On a CCU the rooms and functions the user sees in the WebUI live in ReGa's DOM - enum objects
5
+ * under `ID_ROOMS` and `ID_FUNCTIONS`, each holding channel ids - and a taxonomy kept in this
6
+ * profile beside them would be a second truth. So on a CCU with ReGa this provider *is* the store:
7
+ * the same {@link MetadataProvider} surface the box's provider has, over the same script transport
8
+ * the names already use, so that the editing UI of task 25 does not know which one it talks to.
9
+ *
10
+ * What ReGa's model is, and therefore what this provider is:
11
+ *
12
+ * - **Flat.** A room is a room; there is no room below another and no floor. `state().flat` says
13
+ * so, `createNode` with a parent and a move under a parent are refused.
14
+ * - **Two taxonomies, fixed.** Rooms and functions exist; nothing else can be created or deleted.
15
+ * - **Channels are members, devices are not.** The WebUI assigns channels. A device ref in a
16
+ * membership write is applied to every channel of the device except the `:0` maintenance
17
+ * channel, which is what a user who "puts the device in the kitchen" means.
18
+ * - **Identity is the ReGa id.** A node's id is `r<id>`, stable across renames, so a path never
19
+ * has to be rewritten when a room is renamed.
20
+ * - **No change stream.** ReGa tells nobody when the WebUI edits a room. This provider reads
21
+ * everything on `start()`, after every write of its own, and on `refresh()`, which the UI asks
22
+ * for through `meta.refresh`.
23
+ *
24
+ * Failure is a state, never an exception (D-2): a script that does not run leaves the last
25
+ * document in place, sets `reachable: false` and says so once.
26
+ */
27
+ import { type Language, type MetaDocument, type MetaNodePatch, type MetaState } from '@homematic-manager/core';
28
+ import type { MetadataProvider, MetaMembershipEntry, MetaNameEntry, MetaProviderEvents } from './provider.js';
29
+ /** The part of the ReGa transport this provider uses: run a script, read what it wrote. */
30
+ export type RegaScriptRunner = (script: string) => Promise<{
31
+ output: string;
32
+ }>;
33
+ export interface RegaMetaProviderOptions extends MetaProviderEvents {
34
+ readonly exec: RegaScriptRunner;
35
+ /**
36
+ * The interface an address belongs to, from the device caches - the fallback for a device
37
+ * whose interface ReGa does not name (an older firmware, a CUxD device).
38
+ */
39
+ readonly interfaceOf: (address: string) => string | undefined;
40
+ /**
41
+ * The language the stock rooms and functions are shown in. ReGa stores them as translation keys
42
+ * (`roomKitchen`) and the WebUI translates them on display; the CCU's own default is German.
43
+ */
44
+ readonly language?: Language | undefined;
45
+ }
46
+ /**
47
+ * The rooms and functions a CCU comes with, as the WebUI translates them (`translate.lang.js` of
48
+ * the German and the English WebUI, firmware 3.89.8). ReGa's objects carry the *key* as their
49
+ * `Name()` - `roomKitchen`, not `Küche` - and every WebUI page translates it on display, so a list
50
+ * that shows the keys is not what the user sees on the CCU. Found in the first lab pass of task 27.
51
+ * A name that is not one of these keys is shown as it is; a rename writes the literal name.
52
+ */
53
+ export declare const REGA_STOCK_NAMES: Readonly<Record<string, Readonly<Record<'de' | 'en', string>>>>;
54
+ /** `roomKitchen` -> `Küche`; anything that is not a stock key is answered as it is. */
55
+ export declare function regaStockName(name: string, language: Language | undefined): string;
56
+ /** `r4711` - the node id of a ReGa enum object; stable across renames. */
57
+ export declare function regaNodeId(id: number): string;
58
+ /** The ReGa id behind a node id, or `undefined` for anything that is not one. */
59
+ export declare function parseRegaNodeId(id: string): number | undefined;
60
+ export declare class RegaMetaProvider implements MetadataProvider {
61
+ #private;
62
+ readonly kind: "rega";
63
+ constructor(options: RegaMetaProviderOptions);
64
+ state(): MetaState;
65
+ start(): Promise<void>;
66
+ stop(): Promise<void>;
67
+ /** Reads everything again. Never throws: a ReGa that does not answer is a state. */
68
+ refresh(): Promise<void>;
69
+ document(): MetaDocument;
70
+ setNames(entries: readonly MetaNameEntry[]): Promise<void>;
71
+ setMembership(entries: readonly MetaMembershipEntry[]): Promise<void>;
72
+ createEnum(): Promise<void>;
73
+ updateEnum(): Promise<void>;
74
+ deleteEnum(): Promise<void>;
75
+ /**
76
+ * A new room or function. The id the caller derived from the name is not used: ReGa hands out
77
+ * the id, and `r<id>` is what keeps the path stable when the room is renamed later.
78
+ */
79
+ createNode(enumId: string, parent: string | null, _id: string, name: string): Promise<string>;
80
+ updateNode(path: string, patch: MetaNodePatch): Promise<void>;
81
+ deleteNode(path: string, detach: boolean): Promise<void>;
82
+ import(): Promise<void>;
83
+ }
84
+ //# sourceMappingURL=regaProvider.d.ts.map