@matter-server/dashboard 0.7.0-alpha.0-20260512-b404bea → 0.7.0

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 (171) hide show
  1. package/dist/esm/client/client-context.d.ts +3 -0
  2. package/dist/esm/client/client-context.d.ts.map +1 -1
  3. package/dist/esm/client/client-context.js +3 -1
  4. package/dist/esm/client/client-context.js.map +1 -1
  5. package/dist/esm/client/models/descriptions.js +45 -45
  6. package/dist/esm/components/avsum-ptz-strip.d.ts +41 -0
  7. package/dist/esm/components/avsum-ptz-strip.d.ts.map +1 -0
  8. package/dist/esm/components/avsum-ptz-strip.js +379 -0
  9. package/dist/esm/components/avsum-ptz-strip.js.map +6 -0
  10. package/dist/esm/components/dialogs/binding/node-binding-dialog.js +1 -1
  11. package/dist/esm/components/dialogs/binding/show-node-binding-dialog.d.ts +2 -2
  12. package/dist/esm/components/dialogs/binding/show-node-binding-dialog.d.ts.map +1 -1
  13. package/dist/esm/components/dialogs/binding/show-node-binding-dialog.js +1 -2
  14. package/dist/esm/components/dialogs/binding/show-node-binding-dialog.js.map +1 -1
  15. package/dist/esm/components/dialogs/commission-node-dialog/commission-node-dialog.d.ts +1 -0
  16. package/dist/esm/components/dialogs/commission-node-dialog/commission-node-dialog.d.ts.map +1 -1
  17. package/dist/esm/components/dialogs/commission-node-dialog/commission-node-dialog.js +12 -3
  18. package/dist/esm/components/dialogs/commission-node-dialog/commission-node-dialog.js.map +1 -1
  19. package/dist/esm/components/dialogs/commission-node-dialog/show-commission-node-dialog.d.ts +1 -2
  20. package/dist/esm/components/dialogs/commission-node-dialog/show-commission-node-dialog.d.ts.map +1 -1
  21. package/dist/esm/components/dialogs/commission-node-dialog/show-commission-node-dialog.js +1 -2
  22. package/dist/esm/components/dialogs/commission-node-dialog/show-commission-node-dialog.js.map +1 -1
  23. package/dist/esm/components/dialogs/settings/log-level-dialog.d.ts +0 -2
  24. package/dist/esm/components/dialogs/settings/log-level-dialog.d.ts.map +1 -1
  25. package/dist/esm/components/dialogs/settings/log-level-dialog.js +2 -8
  26. package/dist/esm/components/dialogs/settings/log-level-dialog.js.map +1 -1
  27. package/dist/esm/components/dialogs/settings/log-level-section.d.ts +2 -1
  28. package/dist/esm/components/dialogs/settings/log-level-section.d.ts.map +1 -1
  29. package/dist/esm/components/dialogs/settings/log-level-section.js +10 -2
  30. package/dist/esm/components/dialogs/settings/log-level-section.js.map +1 -1
  31. package/dist/esm/components/dialogs/settings/settings-dialog.d.ts +1 -0
  32. package/dist/esm/components/dialogs/settings/settings-dialog.d.ts.map +1 -1
  33. package/dist/esm/components/dialogs/settings/settings-dialog.js +8 -2
  34. package/dist/esm/components/dialogs/settings/settings-dialog.js.map +1 -1
  35. package/dist/esm/components/dialogs/settings/show-log-level-dialog.d.ts +1 -2
  36. package/dist/esm/components/dialogs/settings/show-log-level-dialog.d.ts.map +1 -1
  37. package/dist/esm/components/dialogs/settings/show-log-level-dialog.js +1 -2
  38. package/dist/esm/components/dialogs/settings/show-log-level-dialog.js.map +1 -1
  39. package/dist/esm/components/dialogs/settings/show-settings-dialog.d.ts +1 -2
  40. package/dist/esm/components/dialogs/settings/show-settings-dialog.d.ts.map +1 -1
  41. package/dist/esm/components/dialogs/settings/show-settings-dialog.js +1 -2
  42. package/dist/esm/components/dialogs/settings/show-settings-dialog.js.map +1 -1
  43. package/dist/esm/components/webrtc-stream-view.d.ts +97 -0
  44. package/dist/esm/components/webrtc-stream-view.d.ts.map +1 -0
  45. package/dist/esm/components/webrtc-stream-view.js +878 -0
  46. package/dist/esm/components/webrtc-stream-view.js.map +6 -0
  47. package/dist/esm/pages/camera-overlay.d.ts +63 -0
  48. package/dist/esm/pages/camera-overlay.d.ts.map +1 -0
  49. package/dist/esm/pages/camera-overlay.js +546 -0
  50. package/dist/esm/pages/camera-overlay.js.map +6 -0
  51. package/dist/esm/pages/cluster-commands/base-cluster-commands.d.ts +1 -0
  52. package/dist/esm/pages/cluster-commands/base-cluster-commands.d.ts.map +1 -1
  53. package/dist/esm/pages/cluster-commands/base-cluster-commands.js +11 -1
  54. package/dist/esm/pages/cluster-commands/base-cluster-commands.js.map +1 -1
  55. package/dist/esm/pages/cluster-commands/clusters/avsum-commands.d.ts +38 -0
  56. package/dist/esm/pages/cluster-commands/clusters/avsum-commands.d.ts.map +1 -0
  57. package/dist/esm/pages/cluster-commands/clusters/avsum-commands.js +534 -0
  58. package/dist/esm/pages/cluster-commands/clusters/avsum-commands.js.map +6 -0
  59. package/dist/esm/pages/cluster-commands/clusters/chime-commands.d.ts +32 -0
  60. package/dist/esm/pages/cluster-commands/clusters/chime-commands.d.ts.map +1 -0
  61. package/dist/esm/pages/cluster-commands/clusters/chime-commands.js +261 -0
  62. package/dist/esm/pages/cluster-commands/clusters/chime-commands.js.map +6 -0
  63. package/dist/esm/pages/cluster-commands/index.d.ts +2 -0
  64. package/dist/esm/pages/cluster-commands/index.d.ts.map +1 -1
  65. package/dist/esm/pages/cluster-commands/index.js +2 -0
  66. package/dist/esm/pages/cluster-commands/index.js.map +1 -1
  67. package/dist/esm/pages/components/header.d.ts +1 -0
  68. package/dist/esm/pages/components/header.d.ts.map +1 -1
  69. package/dist/esm/pages/components/header.js +10 -3
  70. package/dist/esm/pages/components/header.js.map +1 -1
  71. package/dist/esm/pages/components/node-details.d.ts +3 -0
  72. package/dist/esm/pages/components/node-details.d.ts.map +1 -1
  73. package/dist/esm/pages/components/node-details.js +34 -4
  74. package/dist/esm/pages/components/node-details.js.map +1 -1
  75. package/dist/esm/pages/components/server-details.d.ts +1 -0
  76. package/dist/esm/pages/components/server-details.d.ts.map +1 -1
  77. package/dist/esm/pages/components/server-details.js +11 -2
  78. package/dist/esm/pages/components/server-details.js.map +1 -1
  79. package/dist/esm/pages/matter-cluster-view.d.ts +1 -0
  80. package/dist/esm/pages/matter-cluster-view.d.ts.map +1 -1
  81. package/dist/esm/pages/matter-cluster-view.js +15 -8
  82. package/dist/esm/pages/matter-cluster-view.js.map +1 -1
  83. package/dist/esm/pages/matter-dashboard-app.d.ts +3 -1
  84. package/dist/esm/pages/matter-dashboard-app.d.ts.map +1 -1
  85. package/dist/esm/pages/matter-dashboard-app.js +12 -16
  86. package/dist/esm/pages/matter-dashboard-app.js.map +1 -1
  87. package/dist/esm/pages/matter-endpoint-view.d.ts +2 -1
  88. package/dist/esm/pages/matter-endpoint-view.d.ts.map +1 -1
  89. package/dist/esm/pages/matter-endpoint-view.js +17 -3
  90. package/dist/esm/pages/matter-endpoint-view.js.map +1 -1
  91. package/dist/esm/pages/matter-network-view.d.ts.map +1 -1
  92. package/dist/esm/pages/matter-network-view.js +5 -1
  93. package/dist/esm/pages/matter-network-view.js.map +1 -1
  94. package/dist/esm/pages/matter-node-view.d.ts +1 -0
  95. package/dist/esm/pages/matter-node-view.d.ts.map +1 -1
  96. package/dist/esm/pages/matter-node-view.js +15 -7
  97. package/dist/esm/pages/matter-node-view.js.map +1 -1
  98. package/dist/esm/pages/matter-server-view.d.ts +1 -0
  99. package/dist/esm/pages/matter-server-view.d.ts.map +1 -1
  100. package/dist/esm/pages/matter-server-view.js +13 -2
  101. package/dist/esm/pages/matter-server-view.js.map +1 -1
  102. package/dist/esm/pages/network/network-details.d.ts.map +1 -1
  103. package/dist/esm/pages/network/network-details.js +0 -3
  104. package/dist/esm/pages/network/network-details.js.map +1 -1
  105. package/dist/esm/pages/network/update-connections-dialog.d.ts +1 -0
  106. package/dist/esm/pages/network/update-connections-dialog.d.ts.map +1 -1
  107. package/dist/esm/pages/network/update-connections-dialog.js +7 -1
  108. package/dist/esm/pages/network/update-connections-dialog.js.map +1 -1
  109. package/dist/esm/util/attribute-shapes.d.ts +11 -0
  110. package/dist/esm/util/attribute-shapes.d.ts.map +1 -0
  111. package/dist/esm/util/attribute-shapes.js +37 -0
  112. package/dist/esm/util/attribute-shapes.js.map +6 -0
  113. package/dist/esm/util/avsum.d.ts +69 -0
  114. package/dist/esm/util/avsum.d.ts.map +1 -0
  115. package/dist/esm/util/avsum.js +150 -0
  116. package/dist/esm/util/avsum.js.map +6 -0
  117. package/dist/esm/util/chime.d.ts +20 -0
  118. package/dist/esm/util/chime.d.ts.map +1 -0
  119. package/dist/esm/util/chime.js +60 -0
  120. package/dist/esm/util/chime.js.map +6 -0
  121. package/dist/web/js/{attribute-write-dialog-DzMWN_T3.js → attribute-write-dialog-DLRP2hlx.js} +1 -1
  122. package/dist/web/js/{command-invoke-dialog-BhAOXzjX.js → command-invoke-dialog-BcCfT1sn.js} +1 -1
  123. package/dist/web/js/{commission-node-dialog-DF87YIqR.js → commission-node-dialog-BfldkUxD.js} +16 -8
  124. package/dist/web/js/{commission-node-existing-DLcWvJTL.js → commission-node-existing-omW-An0J.js} +2 -2
  125. package/dist/web/js/{commission-node-thread-m2fqED-2.js → commission-node-thread-BZpJIxlI.js} +2 -2
  126. package/dist/web/js/{commission-node-wifi-PCsot-CX.js → commission-node-wifi-HA2pYTW2.js} +2 -2
  127. package/dist/web/js/{dialog-box-DiqYXM8c.js → dialog-box-BjDID9j4.js} +1 -1
  128. package/dist/web/js/{fire_event-BnpND_gK.js → fire_event-CChLgSiV.js} +1 -1
  129. package/dist/web/js/main.js +39 -1
  130. package/dist/web/js/{matter-dashboard-app-BZOhBELR.js → matter-dashboard-app-XUWBcruM.js} +8265 -2035
  131. package/dist/web/js/{node-binding-dialog-B3lVN-FU.js → node-binding-dialog-_yHtf7WP.js} +2 -2
  132. package/dist/web/js/settings-dialog-BFPfOSOI.js +607 -0
  133. package/package.json +4 -4
  134. package/src/client/client-context.ts +1 -0
  135. package/src/client/models/descriptions.ts +45 -45
  136. package/src/components/avsum-ptz-strip.ts +388 -0
  137. package/src/components/dialogs/binding/node-binding-dialog.ts +1 -1
  138. package/src/components/dialogs/binding/show-node-binding-dialog.ts +2 -3
  139. package/src/components/dialogs/commission-node-dialog/commission-node-dialog.ts +10 -4
  140. package/src/components/dialogs/commission-node-dialog/show-commission-node-dialog.ts +1 -4
  141. package/src/components/dialogs/settings/log-level-dialog.ts +2 -9
  142. package/src/components/dialogs/settings/log-level-section.ts +11 -4
  143. package/src/components/dialogs/settings/settings-dialog.ts +8 -3
  144. package/src/components/dialogs/settings/show-log-level-dialog.ts +1 -4
  145. package/src/components/dialogs/settings/show-settings-dialog.ts +1 -4
  146. package/src/components/webrtc-stream-view.ts +999 -0
  147. package/src/pages/camera-overlay.ts +562 -0
  148. package/src/pages/cluster-commands/base-cluster-commands.ts +7 -1
  149. package/src/pages/cluster-commands/clusters/avsum-commands.ts +562 -0
  150. package/src/pages/cluster-commands/clusters/chime-commands.ts +272 -0
  151. package/src/pages/cluster-commands/index.ts +2 -0
  152. package/src/pages/components/header.ts +7 -3
  153. package/src/pages/components/node-details.ts +33 -3
  154. package/src/pages/components/server-details.ts +8 -2
  155. package/src/pages/matter-cluster-view.ts +13 -9
  156. package/src/pages/matter-dashboard-app.ts +11 -16
  157. package/src/pages/matter-endpoint-view.ts +13 -5
  158. package/src/pages/matter-network-view.ts +3 -1
  159. package/src/pages/matter-node-view.ts +9 -7
  160. package/src/pages/matter-server-view.ts +7 -2
  161. package/src/pages/network/network-details.ts +0 -3
  162. package/src/pages/network/update-connections-dialog.ts +6 -1
  163. package/src/util/attribute-shapes.ts +34 -0
  164. package/src/util/avsum.ts +212 -0
  165. package/src/util/chime.ts +82 -0
  166. package/dist/esm/util/clone_class.d.ts +0 -7
  167. package/dist/esm/util/clone_class.d.ts.map +0 -1
  168. package/dist/esm/util/clone_class.js +0 -10
  169. package/dist/esm/util/clone_class.js.map +0 -6
  170. package/dist/web/js/settings-dialog-D75IAPKe.js +0 -4039
  171. package/src/util/clone_class.ts +0 -7
@@ -0,0 +1,272 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2025-2026 Open Home Foundation
4
+ * SPDX-License-Identifier: Apache-2.0
5
+ */
6
+
7
+ import "@material/web/button/filled-button";
8
+ import "@material/web/iconbutton/icon-button";
9
+ import "@material/web/select/outlined-select";
10
+ import "@material/web/select/select-option";
11
+ import "@material/web/switch/switch";
12
+ import { mdiPlay } from "@mdi/js";
13
+ import { css, html, nothing } from "lit";
14
+ import { customElement, state } from "lit/decorators.js";
15
+ import "../../../components/ha-svg-icon.js";
16
+ import { handleAsync, handleAsyncEvent } from "../../../util/async-handler.js";
17
+ import {
18
+ CHIME_CLUSTER_ID,
19
+ play as chimePlay,
20
+ readEnabled,
21
+ readRevision,
22
+ readSelected,
23
+ readSounds,
24
+ setEnabled,
25
+ setSelected,
26
+ } from "../../../util/chime.js";
27
+ import { BaseClusterCommands } from "../base-cluster-commands.js";
28
+ import { registerClusterCommands } from "../registry.js";
29
+
30
+ @customElement("chime-cluster-commands")
31
+ class ChimeClusterCommands extends BaseClusterCommands {
32
+ @state() private _lastPlayed: { chimeId: number; at: number } | null = null;
33
+ @state() private _playDisabledUntil = 0;
34
+ private _unsubscribeNodes?: () => void;
35
+ private _unsubscribeEvents?: () => void;
36
+
37
+ override updated(changedProperties: Map<string, unknown>) {
38
+ super.updated(changedProperties);
39
+ if (changedProperties.has("client") && this.client && !this._unsubscribeNodes) {
40
+ this._unsubscribeNodes = this.client.addEventListener("nodes_changed", () => {
41
+ this.requestUpdate();
42
+ });
43
+ this._unsubscribeEvents = this.client.addNodeEventListener(ev => {
44
+ if (
45
+ ev.cluster_id !== CHIME_CLUSTER_ID ||
46
+ ev.endpoint_id !== this.endpoint ||
47
+ String(ev.node_id) !== String(this.node.node_id) ||
48
+ ev.event_id !== 0
49
+ ) {
50
+ return;
51
+ }
52
+ const raw = ev.data;
53
+ if (raw === null || typeof raw !== "object") return;
54
+ const d = raw as Record<string, unknown>;
55
+ const named = d["chimeID"];
56
+ const tagged = d["0"];
57
+ const chimeId = typeof named === "number" ? named : typeof tagged === "number" ? tagged : null;
58
+ if (chimeId !== null) this.onChimeStartedPlaying(chimeId);
59
+ });
60
+ }
61
+ }
62
+
63
+ override disconnectedCallback() {
64
+ super.disconnectedCallback();
65
+ this._unsubscribeNodes?.();
66
+ this._unsubscribeEvents?.();
67
+ this._lastPlayed = null;
68
+ }
69
+
70
+ override render() {
71
+ if (!this.node || this.cluster !== CHIME_CLUSTER_ID) return nothing;
72
+ const sounds = readSounds(this.node, this.endpoint);
73
+ const selected = readSelected(this.node, this.endpoint);
74
+ const enabled = readEnabled(this.node, this.endpoint);
75
+ const revision = readRevision(this.node, this.endpoint);
76
+ const showPerRowPlay = revision >= 2;
77
+ const showLastPlayed = revision >= 2 && this._lastPlayed !== null;
78
+ const playLocked = Date.now() < this._playDisabledUntil;
79
+ const lastSoundName = showLastPlayed
80
+ ? (sounds.find(s => s.chimeId === this._lastPlayed!.chimeId)?.name ?? `#${this._lastPlayed!.chimeId}`)
81
+ : null;
82
+
83
+ return html`
84
+ <details class="command-panel" open>
85
+ <summary>Chime</summary>
86
+ <div class="command-content">
87
+ <div class="top-row">
88
+ <label class="enabled">
89
+ <md-switch
90
+ ?selected=${enabled}
91
+ @change=${handleAsyncEvent((e: Event) =>
92
+ setEnabled(
93
+ this.client,
94
+ this.node.node_id,
95
+ this.endpoint,
96
+ // md-switch exposes `.selected` but has no typed CustomEvent subclass
97
+ (e.target as HTMLElement & { selected: boolean }).selected,
98
+ ),
99
+ )}
100
+ ></md-switch>
101
+ <span>Enabled</span>
102
+ </label>
103
+ <div class="selected">
104
+ <span class="muted">Selected:</span>
105
+ <md-outlined-select
106
+ .value=${selected !== null ? String(selected) : ""}
107
+ ?disabled=${sounds.length === 0}
108
+ @change=${handleAsyncEvent((e: Event) => {
109
+ const v = Number((e.target as HTMLSelectElement).value);
110
+ return setSelected(this.client, this.node.node_id, this.endpoint, v);
111
+ })}
112
+ >
113
+ ${sounds.map(
114
+ s => html`
115
+ <md-select-option value=${String(s.chimeId)}>
116
+ <div slot="headline">${s.name}</div>
117
+ </md-select-option>
118
+ `,
119
+ )}
120
+ </md-outlined-select>
121
+ </div>
122
+ <md-filled-button
123
+ ?disabled=${!enabled || selected === null || playLocked}
124
+ @click=${handleAsync(() => this._handlePlay())}
125
+ >
126
+ <ha-svg-icon slot="icon" .path=${mdiPlay}></ha-svg-icon>
127
+ Play
128
+ </md-filled-button>
129
+ </div>
130
+
131
+ <div class="sounds">
132
+ <div class="sounds-header">Installed sounds (${sounds.length})</div>
133
+ ${sounds.length === 0
134
+ ? html`<div class="muted empty">No sounds installed.</div>`
135
+ : sounds.map(
136
+ s => html`
137
+ <div
138
+ class="sound-row ${s.chimeId === selected ? "selected" : ""}"
139
+ @click=${handleAsync(() =>
140
+ setSelected(this.client, this.node.node_id, this.endpoint, s.chimeId),
141
+ )}
142
+ >
143
+ <span class="pid">#${s.chimeId}</span>
144
+ <span class="pname">${s.name}</span>
145
+ ${s.chimeId === selected
146
+ ? html`<span class="muted">✓ selected</span>`
147
+ : nothing}
148
+ <span class="grow"></span>
149
+ ${showPerRowPlay
150
+ ? html`<md-icon-button
151
+ ?disabled=${!enabled}
152
+ @click=${handleAsyncEvent((e: Event) => {
153
+ e.stopPropagation();
154
+ return chimePlay(
155
+ this.client,
156
+ this.node.node_id,
157
+ this.endpoint,
158
+ s.chimeId,
159
+ );
160
+ })}
161
+ >
162
+ <ha-svg-icon .path=${mdiPlay}></ha-svg-icon>
163
+ </md-icon-button>`
164
+ : nothing}
165
+ </div>
166
+ `,
167
+ )}
168
+ </div>
169
+
170
+ ${showLastPlayed
171
+ ? html`<div class="last-played">
172
+ Last played: ${lastSoundName} · ${new Date(this._lastPlayed!.at).toLocaleTimeString()}
173
+ </div>`
174
+ : nothing}
175
+ </div>
176
+ </details>
177
+ `;
178
+ }
179
+
180
+ public onChimeStartedPlaying(chimeId: number): void {
181
+ this._lastPlayed = { chimeId, at: Date.now() };
182
+ }
183
+
184
+ private async _handlePlay() {
185
+ this._playDisabledUntil = Date.now() + 1000;
186
+ this.requestUpdate();
187
+ try {
188
+ await chimePlay(this.client, this.node.node_id, this.endpoint);
189
+ } finally {
190
+ setTimeout(() => this.requestUpdate(), 1000);
191
+ }
192
+ }
193
+
194
+ static override styles = [
195
+ ...(Array.isArray(BaseClusterCommands.styles) ? BaseClusterCommands.styles : [BaseClusterCommands.styles]),
196
+ css`
197
+ .top-row {
198
+ display: flex;
199
+ align-items: center;
200
+ gap: 16px;
201
+ flex-wrap: wrap;
202
+ padding-bottom: 12px;
203
+ }
204
+ .enabled {
205
+ display: inline-flex;
206
+ align-items: center;
207
+ gap: 8px;
208
+ }
209
+ .selected {
210
+ display: inline-flex;
211
+ align-items: center;
212
+ gap: 8px;
213
+ flex: 1;
214
+ min-width: 200px;
215
+ }
216
+ .muted {
217
+ color: var(--md-sys-color-on-surface-variant);
218
+ }
219
+ .sounds {
220
+ border-top: 1px solid var(--md-sys-color-outline-variant);
221
+ padding-top: 8px;
222
+ }
223
+ .sounds-header {
224
+ font-weight: 500;
225
+ margin-bottom: 6px;
226
+ }
227
+ .sound-row {
228
+ display: flex;
229
+ align-items: center;
230
+ gap: 12px;
231
+ padding: 6px 8px;
232
+ border-radius: 4px;
233
+ cursor: pointer;
234
+ }
235
+ .sound-row:hover {
236
+ background: var(--md-sys-color-surface-container-high);
237
+ }
238
+ .sound-row.selected {
239
+ background: var(--md-sys-color-secondary-container);
240
+ }
241
+ .pid {
242
+ font-family: var(--monospace-font, monospace);
243
+ color: var(--md-sys-color-on-surface-variant);
244
+ font-size: 0.85rem;
245
+ }
246
+ .pname {
247
+ font-weight: 500;
248
+ }
249
+ .grow {
250
+ flex: 1;
251
+ }
252
+ .empty {
253
+ font-style: italic;
254
+ }
255
+ .last-played {
256
+ margin-top: 12px;
257
+ padding-top: 8px;
258
+ border-top: 1px solid var(--md-sys-color-outline-variant);
259
+ font-size: 0.85rem;
260
+ color: var(--md-sys-color-on-surface-variant);
261
+ }
262
+ `,
263
+ ];
264
+ }
265
+
266
+ registerClusterCommands(CHIME_CLUSTER_ID, "chime-cluster-commands");
267
+
268
+ declare global {
269
+ interface HTMLElementTagNameMap {
270
+ "chime-cluster-commands": ChimeClusterCommands;
271
+ }
272
+ }
@@ -16,6 +16,8 @@ export { getClusterCommandsTag, hasClusterCommands, registerClusterCommands } fr
16
16
  export { BaseClusterCommands } from "./base-cluster-commands.js";
17
17
 
18
18
  // Cluster command components (auto-register on import)
19
+ import "./clusters/avsum-commands.js";
19
20
  import "./clusters/basic-information-commands.js";
21
+ import "./clusters/chime-commands.js";
20
22
  import "./clusters/level-control-commands.js";
21
23
  import "./clusters/on-off-commands.js";
@@ -9,10 +9,12 @@ import "@material/web/divider/divider";
9
9
  import "@material/web/iconbutton/icon-button";
10
10
  import "@material/web/list/list";
11
11
  import "@material/web/list/list-item";
12
+ import { consume } from "@lit/context";
12
13
  import { MatterClient } from "@matter-server/ws-client";
13
14
  import { mdiArrowLeft, mdiBrightnessAuto, mdiCog, mdiLogout, mdiWeatherNight, mdiWeatherSunny } from "@mdi/js";
14
15
  import { LitElement, css, html, nothing } from "lit";
15
16
  import { customElement, property, state } from "lit/decorators.js";
17
+ import { clientContext, tickContext } from "../../client/client-context.js";
16
18
  import { showSettingsDialog } from "../../components/dialogs/settings/show-settings-dialog.js";
17
19
  import "../../components/ha-svg-icon";
18
20
  import { DevModeService } from "../../util/dev-mode-service.js";
@@ -35,8 +37,12 @@ export class DashboardHeader extends LitElement {
35
37
  @property({ type: Boolean }) public hasThreadDevices?: boolean;
36
38
  @property({ type: Boolean }) public hasWifiDevices?: boolean;
37
39
 
40
+ @consume({ context: clientContext })
38
41
  public client?: MatterClient;
39
42
 
43
+ @consume({ context: tickContext, subscribe: true })
44
+ protected _tick = 0;
45
+
40
46
  @state() private _themePreference: ThemePreference = ThemeService.preference;
41
47
  @state() private _effectiveTheme: EffectiveTheme = ThemeService.effectiveTheme;
42
48
  @state() private _devMode = DevModeService.active;
@@ -66,9 +72,7 @@ export class DashboardHeader extends LitElement {
66
72
  }
67
73
 
68
74
  private _openSettings() {
69
- if (this.client) {
70
- showSettingsDialog(this.client);
71
- }
75
+ showSettingsDialog();
72
76
  }
73
77
 
74
78
  private _getThemeIcon(): string {
@@ -13,14 +13,16 @@ import "@material/web/list/list";
13
13
  import "@material/web/list/list-item";
14
14
  import { consume } from "@lit/context";
15
15
  import { MatterClient, MatterNode, UpdateSource } from "@matter-server/ws-client";
16
- import { mdiChatProcessing, mdiLink, mdiShareVariant, mdiTrashCan, mdiUpdate } from "@mdi/js";
16
+ import { mdiChatProcessing, mdiLink, mdiShareVariant, mdiTrashCan, mdiUpdate, mdiVideo } from "@mdi/js";
17
17
  import { LitElement, css, html, nothing } from "lit";
18
18
  import { customElement, property, state } from "lit/decorators.js";
19
+ import { clientContext, tickContext } from "../../client/client-context.js";
19
20
  import { DeviceType } from "../../client/models/descriptions.js";
20
21
  import { showAlertDialog, showPromptDialog } from "../../components/dialog-box/show-dialog-box.js";
21
22
  import { showNodeBindingDialog } from "../../components/dialogs/binding/show-node-binding-dialog.js";
22
23
  import { handleAsync } from "../../util/async-handler.js";
23
24
  import "../../components/ha-svg-icon";
25
+ import "../camera-overlay.js";
24
26
  import { getDeviceIcon } from "../../util/device-icons.js";
25
27
  import { getEndpointDeviceTypes } from "../matter-endpoint-view.js";
26
28
  import { bindingContext } from "./context.js";
@@ -59,21 +61,26 @@ function getNodeDeviceTypes(node: MatterNode): DeviceType[] {
59
61
 
60
62
  @customElement("node-details")
61
63
  export class NodeDetails extends LitElement {
64
+ @consume({ context: clientContext })
62
65
  public client!: MatterClient;
63
66
 
67
+ @consume({ context: tickContext, subscribe: true })
68
+ protected _tick = 0;
69
+
64
70
  @property() public node?: MatterNode;
65
71
 
66
72
  @state()
67
73
  private _updateInitiated: boolean = false;
68
74
 
69
75
  @consume({ context: bindingContext })
70
- @property({ attribute: false })
71
76
  endpoint!: number;
72
77
 
73
78
  protected override render() {
74
79
  if (!this.node) return html``;
75
80
 
76
81
  const bindings = this.node.attributes[this.endpoint + "/30/0"];
82
+ const deviceTypeIds = getEndpointDeviceTypes(this.node, this.endpoint).map(d => d.id);
83
+ const isCamera = deviceTypeIds.includes(0x0142) || deviceTypeIds.includes(0x0143);
77
84
 
78
85
  return html`
79
86
  <md-list>
@@ -129,6 +136,17 @@ export class NodeDetails extends LitElement {
129
136
  : html`<md-outlined-button @click=${handleAsync(() => this._searchUpdate())}
130
137
  >Update<ha-svg-icon slot="icon" .path=${mdiUpdate}></ha-svg-icon
131
138
  ></md-outlined-button>`}
139
+ ${isCamera
140
+ ? html`
141
+ <md-outlined-button
142
+ @click=${() => this._openCameraOverlay()}
143
+ ?disabled=${!this.node.available}
144
+ >
145
+ Live View
146
+ <ha-svg-icon slot="icon" .path=${mdiVideo}></ha-svg-icon>
147
+ </md-outlined-button>
148
+ `
149
+ : nothing}
132
150
  ${bindings
133
151
  ? html`
134
152
  <md-outlined-button @click=${handleAsync(() => this._binding())}>
@@ -199,12 +217,24 @@ export class NodeDetails extends LitElement {
199
217
 
200
218
  private async _binding() {
201
219
  try {
202
- showNodeBindingDialog(this.client, this.node!, this.endpoint);
220
+ showNodeBindingDialog(this.node!, this.endpoint);
203
221
  } catch (err: unknown) {
204
222
  console.error("Binding error:", err);
205
223
  }
206
224
  }
207
225
 
226
+ private _openCameraOverlay(): void {
227
+ const overlay = document.createElement("camera-overlay");
228
+ overlay.nodeId = this.node!.node_id;
229
+ overlay.endpointId = this.endpoint;
230
+ const root = document.querySelector("matter-dashboard-app");
231
+ if (root) {
232
+ root.renderRoot.appendChild(overlay);
233
+ } else {
234
+ document.body.appendChild(overlay);
235
+ }
236
+ }
237
+
208
238
  private async _searchUpdate() {
209
239
  const nodeUpdate = await this.client.checkNodeUpdate(this.node!.node_id);
210
240
  if (!nodeUpdate) {
@@ -11,10 +11,12 @@ import "@material/web/divider/divider";
11
11
  import "@material/web/iconbutton/icon-button";
12
12
  import "@material/web/list/list";
13
13
  import "@material/web/list/list-item";
14
+ import { consume } from "@lit/context";
14
15
  import { MatterClient } from "@matter-server/ws-client";
15
16
  import { mdiFile, mdiPlus } from "@mdi/js";
16
17
  import { LitElement, css, html, nothing } from "lit";
17
18
  import { customElement } from "lit/decorators.js";
19
+ import { clientContext, tickContext } from "../../client/client-context.js";
18
20
  import { showAlertDialog, showPromptDialog } from "../../components/dialog-box/show-dialog-box.js";
19
21
  import { showCommissionNodeDialog } from "../../components/dialogs/commission-node-dialog/show-commission-node-dialog.js";
20
22
  import "../../components/ha-svg-icon";
@@ -22,8 +24,12 @@ import { handleAsync } from "../../util/async-handler.js";
22
24
 
23
25
  @customElement("server-details")
24
26
  export class ServerDetails extends LitElement {
27
+ @consume({ context: clientContext })
25
28
  public client?: MatterClient;
26
29
 
30
+ @consume({ context: tickContext, subscribe: true })
31
+ protected _tick = 0;
32
+
27
33
  protected override render() {
28
34
  if (!this.client) return html``;
29
35
 
@@ -71,7 +77,7 @@ export class ServerDetails extends LitElement {
71
77
  }
72
78
 
73
79
  private _commissionNode() {
74
- showCommissionNodeDialog(this.client!);
80
+ showCommissionNodeDialog();
75
81
  }
76
82
 
77
83
  private async _uploadDiagnosticsDumpFile() {
@@ -96,7 +102,7 @@ export class ServerDetails extends LitElement {
96
102
  reader.readAsText(selectedFile, "UTF-8");
97
103
  reader.onload = async () => {
98
104
  try {
99
- await this.client!.importTestNode(reader.result?.toString() ?? "");
105
+ await this.client?.importTestNode(reader.result?.toString() ?? "");
100
106
  } catch (err: any) {
101
107
  showAlertDialog({
102
108
  title: "Failed to import test node",
@@ -4,7 +4,7 @@
4
4
  * SPDX-License-Identifier: Apache-2.0
5
5
  */
6
6
 
7
- import { provide } from "@lit/context";
7
+ import { consume, provide } from "@lit/context";
8
8
  import "@material/web/button/outlined-button";
9
9
  import "@material/web/divider/divider";
10
10
  import "@material/web/iconbutton/icon-button";
@@ -16,6 +16,7 @@ import { mdiAlertCircleOutline, mdiPencil, mdiPlay, mdiRefresh } from "@mdi/js";
16
16
  import { css, html, LitElement, nothing, type TemplateResult } from "lit";
17
17
  import { customElement, property, state } from "lit/decorators.js";
18
18
  import { unsafeHTML } from "lit/directives/unsafe-html.js";
19
+ import { clientContext, tickContext } from "../client/client-context.js";
19
20
  import { clusters } from "../client/models/descriptions.js";
20
21
  import { showAlertDialog } from "../components/dialog-box/show-dialog-box.js";
21
22
  import { showAttributeWriteDialog } from "../components/dialogs/dev/show-attribute-write-dialog.js";
@@ -26,7 +27,7 @@ import "../pages/components/node-details";
26
27
  import { DevModeService } from "../util/dev-mode-service.js";
27
28
  import { formatHex, formatNodeAddress, getEffectiveFabricIndex } from "../util/format_hex.js";
28
29
  import { notFoundStyles } from "../util/shared-styles.js";
29
- import { getClusterCommandsTag } from "./cluster-commands/index.js";
30
+ import { BaseClusterCommands, getClusterCommandsTag } from "./cluster-commands/index.js";
30
31
  import { bindingContext } from "./components/context.js";
31
32
 
32
33
  declare global {
@@ -74,8 +75,12 @@ function clusterAttributes(attributes: { [key: string]: any }, endpoint: number,
74
75
 
75
76
  @customElement("matter-cluster-view")
76
77
  class MatterClusterView extends LitElement {
78
+ @consume({ context: clientContext })
77
79
  public client!: MatterClient;
78
80
 
81
+ @consume({ context: tickContext, subscribe: true })
82
+ protected _tick = 0;
83
+
79
84
  @property()
80
85
  public node?: MatterNode;
81
86
 
@@ -108,7 +113,7 @@ class MatterClusterView extends LitElement {
108
113
  override render() {
109
114
  if (!this.node || this.endpoint == undefined || this.cluster == undefined) {
110
115
  return html`
111
- <dashboard-header title="Not found" .client=${this.client} backButton="#"></dashboard-header>
116
+ <dashboard-header title="Not found" backButton="#"></dashboard-header>
112
117
  <div class="not-found">
113
118
  <ha-svg-icon .path=${mdiAlertCircleOutline}></ha-svg-icon>
114
119
  <p>Node, endpoint, or cluster not found</p>
@@ -130,12 +135,11 @@ class MatterClusterView extends LitElement {
130
135
  <dashboard-header
131
136
  .title=${`Node ${this.node.node_id} ${nodeHex} | Endpoint ${this.endpoint} | Cluster ${this.cluster} (${clusterName})`}
132
137
  .backButton=${`#node/${this.node.node_id}/${this.endpoint}`}
133
- .client=${this.client}
134
138
  ></dashboard-header>
135
139
 
136
140
  <!-- node details section -->
137
141
  <div class="container">
138
- <node-details .node=${this.node} .client=${this.client}></node-details>
142
+ <node-details .node=${this.node}></node-details>
139
143
  </div>
140
144
 
141
145
  <!-- Cluster commands section (if available for this cluster) -->
@@ -373,10 +377,9 @@ class MatterClusterView extends LitElement {
373
377
 
374
378
  // After render, find and configure the cluster commands component
375
379
  const container = this.shadowRoot?.getElementById("cluster-commands-container");
376
- if (container) {
377
- const commandsElement = container.firstElementChild as any;
378
- if (commandsElement && this.node && this.client) {
379
- commandsElement.client = this.client;
380
+ if (container && this.node && this.endpoint !== undefined && this.cluster !== undefined) {
381
+ const commandsElement = container.firstElementChild;
382
+ if (commandsElement instanceof BaseClusterCommands) {
380
383
  commandsElement.node = this.node;
381
384
  commandsElement.endpoint = this.endpoint;
382
385
  commandsElement.cluster = this.cluster;
@@ -394,6 +397,7 @@ class MatterClusterView extends LitElement {
394
397
  :host {
395
398
  display: block;
396
399
  background-color: var(--md-sys-color-background);
400
+ color: var(--md-sys-color-on-background);
397
401
  }
398
402
 
399
403
  .header {
@@ -9,9 +9,8 @@ import { MatterClient, MatterError } from "@matter-server/ws-client";
9
9
  import { mdiRefresh } from "@mdi/js";
10
10
  import { LitElement, PropertyValueMap, css, html } from "lit";
11
11
  import { customElement, state } from "lit/decorators.js";
12
- import { clientContext } from "../client/client-context.js";
12
+ import { clientContext, tickContext } from "../client/client-context.js";
13
13
  import "../components/ha-svg-icon";
14
- import { clone } from "../util/clone_class.js";
15
14
  import type { Route } from "../util/routing.js";
16
15
  import "./components/header";
17
16
  import type { ActiveView } from "./components/header.js";
@@ -45,10 +44,13 @@ class MatterDashboardApp extends LitElement {
45
44
  @state()
46
45
  private _state: "connecting" | "connected" | "error" | "disconnected" = "connecting";
47
46
 
47
+ @state() private _tick = 0;
48
+
48
49
  /** Track whether nodes have been loaded at least once (to avoid redirecting before data arrives) */
49
50
  private _nodesLoaded = false;
50
51
 
51
- private provider = new ContextProvider(this, { context: clientContext, initialValue: this.client });
52
+ private clientProvider = new ContextProvider(this, { context: clientContext });
53
+ private tickProvider = new ContextProvider(this, { context: tickContext, initialValue: 0 });
52
54
 
53
55
  /** Reference to updateRoute function so it can be called from event listeners */
54
56
  private _updateRoute?: () => void;
@@ -107,6 +109,9 @@ class MatterDashboardApp extends LitElement {
107
109
  private _connect() {
108
110
  this.client.startListening().then(
109
111
  () => {
112
+ // Publish context before flipping state so the connected render sees a defined client.
113
+ this.clientProvider.setValue(this.client);
114
+ this.tickProvider.setValue(++this._tick);
110
115
  this._state = "connected";
111
116
  this._setupEventListeners();
112
117
  },
@@ -126,10 +131,10 @@ class MatterDashboardApp extends LitElement {
126
131
  this._updateRoute();
127
132
  }
128
133
  this.requestUpdate();
129
- this.provider.setValue(clone(this.client));
134
+ this.tickProvider.setValue(++this._tick);
130
135
  });
131
136
  this.client.addEventListener("server_info_updated", () => {
132
- this.provider.setValue(clone(this.client));
137
+ this.tickProvider.setValue(++this._tick);
133
138
  });
134
139
  this.client.addEventListener("connection_lost", () => {
135
140
  this._state = "disconnected";
@@ -198,7 +203,6 @@ class MatterDashboardApp extends LitElement {
198
203
  // cluster level
199
204
  return html`
200
205
  <matter-cluster-view
201
- .client=${this.client}
202
206
  .node=${this.client.nodes[this._route.path[0]]}
203
207
  .endpoint=${parseInt(this._route.path[1], 10)}
204
208
  .cluster=${parseInt(this._route.path[2], 10)}
@@ -209,7 +213,6 @@ class MatterDashboardApp extends LitElement {
209
213
  // endpoint level
210
214
  return html`
211
215
  <matter-endpoint-view
212
- .client=${this.client}
213
216
  .node=${this.client.nodes[this._route.path[0]]}
214
217
  .endpoint=${parseInt(this._route.path[1], 10)}
215
218
  ></matter-endpoint-view>
@@ -217,12 +220,7 @@ class MatterDashboardApp extends LitElement {
217
220
  }
218
221
  if (this._route.prefix === "node") {
219
222
  // node level
220
- return html`
221
- <matter-node-view
222
- .client=${this.client}
223
- .node=${this.client.nodes[this._route.path[0]]}
224
- ></matter-node-view>
225
- `;
223
+ return html` <matter-node-view .node=${this.client.nodes[this._route.path[0]]}></matter-node-view> `;
226
224
  }
227
225
  // Get device counts for conditional navigation
228
226
  const { hasThreadDevices, hasWifiDevices } = this._getDeviceCounts();
@@ -230,7 +228,6 @@ class MatterDashboardApp extends LitElement {
230
228
  // Check for Thread view (#thread or #thread/123)
231
229
  if (this._route.prefix === "thread" || this._route.path[0] === "thread") {
232
230
  return html`<matter-network-view
233
- .client=${this.client}
234
231
  .nodes=${this.client.nodes}
235
232
  .activeView=${this._activeView}
236
233
  .initialSelectedNodeId=${this._initialSelectedNodeId}
@@ -242,7 +239,6 @@ class MatterDashboardApp extends LitElement {
242
239
  // Check for WiFi view (#wifi or #wifi/123)
243
240
  if (this._route.prefix === "wifi" || this._route.path[0] === "wifi") {
244
241
  return html`<matter-network-view
245
- .client=${this.client}
246
242
  .nodes=${this.client.nodes}
247
243
  .activeView=${this._activeView}
248
244
  .initialSelectedNodeId=${this._initialSelectedNodeId}
@@ -253,7 +249,6 @@ class MatterDashboardApp extends LitElement {
253
249
  }
254
250
  // root level: server overview (nodes view)
255
251
  return html`<matter-server-view
256
- .client=${this.client}
257
252
  .nodes=${this.client.nodes}
258
253
  .route=${this._route}
259
254
  .activeView=${this._activeView}