@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,562 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2025-2026 Open Home Foundation
4
+ * SPDX-License-Identifier: Apache-2.0
5
+ */
6
+
7
+ import "@material/web/button/outlined-button";
8
+ import "@material/web/iconbutton/outlined-icon-button";
9
+ import {
10
+ mdiArrowDown,
11
+ mdiArrowLeft,
12
+ mdiArrowRight,
13
+ mdiArrowUp,
14
+ mdiCircleMedium,
15
+ mdiContentSaveOutline,
16
+ mdiMinus,
17
+ mdiPencil,
18
+ mdiPlus,
19
+ mdiTrashCan,
20
+ } from "@mdi/js";
21
+ import { css, html, nothing } from "lit";
22
+ import { customElement, state } from "lit/decorators.js";
23
+ import "../../../components/ha-svg-icon.js";
24
+ import { showAlertDialog, showPromptDialog } from "../../../components/dialog-box/show-dialog-box.js";
25
+ import { handleAsync, handleAsyncEvent } from "../../../util/async-handler.js";
26
+ import {
27
+ AVSUM_CLUSTER_ID,
28
+ moveToPreset,
29
+ readDptzStreams,
30
+ readFeatures,
31
+ readMovementState,
32
+ readPosition,
33
+ readPresets,
34
+ readRanges,
35
+ relativeMove,
36
+ removePreset,
37
+ savePreset,
38
+ } from "../../../util/avsum.js";
39
+ import { BaseClusterCommands } from "../base-cluster-commands.js";
40
+ import { registerClusterCommands } from "../registry.js";
41
+
42
+ @customElement("avsum-cluster-commands")
43
+ class AvsumClusterCommands extends BaseClusterCommands {
44
+ private _unsubscribeNodes?: () => void;
45
+ @state() private _toast: string | null = null;
46
+ private _toastTimer?: ReturnType<typeof setTimeout>;
47
+ @state() private _newPresetName = "";
48
+
49
+ override updated(changedProperties: Map<string, unknown>) {
50
+ super.updated(changedProperties);
51
+ if (changedProperties.has("client") && this.client && !this._unsubscribeNodes) {
52
+ this._unsubscribeNodes = this.client.addEventListener("nodes_changed", () => this.requestUpdate());
53
+ }
54
+ }
55
+
56
+ override disconnectedCallback() {
57
+ super.disconnectedCallback();
58
+ this._unsubscribeNodes?.();
59
+ if (this._toastTimer) {
60
+ clearTimeout(this._toastTimer);
61
+ this._toastTimer = undefined;
62
+ }
63
+ }
64
+
65
+ override render() {
66
+ if (!this.node || this.cluster !== AVSUM_CLUSTER_ID) return nothing;
67
+ const features = readFeatures(this.node, this.endpoint);
68
+ const pos = readPosition(this.node, this.endpoint);
69
+ const ranges = readRanges(this.node, this.endpoint);
70
+ const movement = readMovementState(this.node, this.endpoint);
71
+
72
+ return html`
73
+ <details class="command-panel" open>
74
+ <summary>Camera AV Settings — Pan / Tilt / Zoom</summary>
75
+ <div class="command-content">
76
+ <div class="readout">
77
+ ${features.mPan && pos.pan !== null
78
+ ? html`<span><b>Pan</b> ${this._fmtDeg(pos.pan)}</span>`
79
+ : nothing}
80
+ ${features.mTilt && pos.tilt !== null
81
+ ? html`<span><b>Tilt</b> ${this._fmtDeg(pos.tilt)}</span>`
82
+ : nothing}
83
+ ${features.mZoom && pos.zoom !== null ? html`<span><b>Zoom</b> ${pos.zoom}×</span>` : nothing}
84
+ ${movement !== "unknown"
85
+ ? html`<span class="badge ${movement === "moving" ? "moving" : ""}"
86
+ >${movement === "moving" ? "Moving…" : "Idle"}</span
87
+ >`
88
+ : nothing}
89
+ ${features.mPan && ranges.panMin !== null && ranges.panMax !== null
90
+ ? html`<span class="range">P [${ranges.panMin}°, ${ranges.panMax}°]</span>`
91
+ : nothing}
92
+ ${features.mTilt && ranges.tiltMin !== null && ranges.tiltMax !== null
93
+ ? html`<span class="range">T [${ranges.tiltMin}°, ${ranges.tiltMax}°]</span>`
94
+ : nothing}
95
+ ${features.mZoom && ranges.zoomMax !== null
96
+ ? html`<span class="range">Z [1×, ${ranges.zoomMax}×]</span>`
97
+ : nothing}
98
+ </div>
99
+ ${features.mPan || features.mTilt || features.mZoom
100
+ ? html`<div class="dpad-row">
101
+ <div class="dpad-grid">
102
+ <span></span>
103
+ <md-outlined-icon-button
104
+ ?disabled=${!features.mTilt || movement === "moving"}
105
+ title="Tilt up (Shift = fine)"
106
+ aria-label="Tilt up"
107
+ @click=${handleAsyncEvent((e: MouseEvent) =>
108
+ this._move({ tiltDelta: this._stepFromEvent(e, 10) }),
109
+ )}
110
+ >
111
+ <ha-svg-icon .path=${mdiArrowUp}></ha-svg-icon>
112
+ </md-outlined-icon-button>
113
+ <span></span>
114
+ <md-outlined-icon-button
115
+ ?disabled=${!features.mPan || movement === "moving"}
116
+ title="Pan left (Shift = fine)"
117
+ aria-label="Pan left"
118
+ @click=${handleAsyncEvent((e: MouseEvent) =>
119
+ this._move({ panDelta: this._stepFromEvent(e, -10) }),
120
+ )}
121
+ >
122
+ <ha-svg-icon .path=${mdiArrowLeft}></ha-svg-icon>
123
+ </md-outlined-icon-button>
124
+ <span class="dpad-center" aria-hidden="true">
125
+ <ha-svg-icon .path=${mdiCircleMedium}></ha-svg-icon>
126
+ </span>
127
+ <md-outlined-icon-button
128
+ ?disabled=${!features.mPan || movement === "moving"}
129
+ title="Pan right (Shift = fine)"
130
+ aria-label="Pan right"
131
+ @click=${handleAsyncEvent((e: MouseEvent) =>
132
+ this._move({ panDelta: this._stepFromEvent(e, 10) }),
133
+ )}
134
+ >
135
+ <ha-svg-icon .path=${mdiArrowRight}></ha-svg-icon>
136
+ </md-outlined-icon-button>
137
+ <span></span>
138
+ <md-outlined-icon-button
139
+ ?disabled=${!features.mTilt || movement === "moving"}
140
+ title="Tilt down (Shift = fine)"
141
+ aria-label="Tilt down"
142
+ @click=${handleAsyncEvent((e: MouseEvent) =>
143
+ this._move({ tiltDelta: this._stepFromEvent(e, -10) }),
144
+ )}
145
+ >
146
+ <ha-svg-icon .path=${mdiArrowDown}></ha-svg-icon>
147
+ </md-outlined-icon-button>
148
+ <span></span>
149
+ </div>
150
+ ${features.mZoom
151
+ ? html`<div class="zoom-col">
152
+ <md-outlined-icon-button
153
+ ?disabled=${movement === "moving"}
154
+ title="Zoom in (Shift = fine)"
155
+ aria-label="Zoom in"
156
+ @click=${handleAsyncEvent((e: MouseEvent) =>
157
+ this._move({ zoomDelta: this._stepFromEvent(e, 10) }),
158
+ )}
159
+ >
160
+ <ha-svg-icon .path=${mdiPlus}></ha-svg-icon>
161
+ </md-outlined-icon-button>
162
+ <span class="muted small">zoom</span>
163
+ <md-outlined-icon-button
164
+ ?disabled=${movement === "moving"}
165
+ title="Zoom out (Shift = fine)"
166
+ aria-label="Zoom out"
167
+ @click=${handleAsyncEvent((e: MouseEvent) =>
168
+ this._move({ zoomDelta: this._stepFromEvent(e, -10) }),
169
+ )}
170
+ >
171
+ <ha-svg-icon .path=${mdiMinus}></ha-svg-icon>
172
+ </md-outlined-icon-button>
173
+ </div>`
174
+ : nothing}
175
+ </div>`
176
+ : nothing}
177
+ ${this._toast ? html`<div class="toast">${this._toast}</div>` : nothing}
178
+ ${features.mPresets
179
+ ? (() => {
180
+ const { items, max } = readPresets(this.node, this.endpoint);
181
+ return html`
182
+ <div class="presets-frame">
183
+ <div class="presets-header">
184
+ <span>Presets</span>
185
+ <span class="muted small">${items.length} / ${max}</span>
186
+ </div>
187
+ <div class="chip-row">
188
+ ${items.map(
189
+ p => html`<button
190
+ class="chip"
191
+ title="p=${this._fmtDeg(p.settings.pan ?? 0)} · t=${this._fmtDeg(
192
+ p.settings.tilt ?? 0,
193
+ )} · z=${p.settings.zoom ?? 1}×"
194
+ @click=${handleAsync(() => this._goPreset(p.presetId))}
195
+ >
196
+ ${p.name}
197
+ </button>`,
198
+ )}
199
+ ${items.length === 0
200
+ ? html`<span class="muted small">No presets saved.</span>`
201
+ : nothing}
202
+ </div>
203
+ <details class="manager">
204
+ <summary>Manage presets…</summary>
205
+ ${items.map(
206
+ p => html`<div class="preset-row">
207
+ <span class="pid">#${p.presetId}</span>
208
+ <span class="pname">${p.name}</span>
209
+ <span class="pcoord">
210
+ p=${this._fmtDeg(p.settings.pan ?? 0)} ·
211
+ t=${this._fmtDeg(p.settings.tilt ?? 0)} ·
212
+ z=${p.settings.zoom ?? 1}×
213
+ </span>
214
+ <span class="grow"></span>
215
+ <md-outlined-icon-button
216
+ title="Overwrite with current MPTZ"
217
+ @click=${handleAsync(() => this._savePresetUpdate(p.presetId))}
218
+ >
219
+ <ha-svg-icon .path=${mdiContentSaveOutline}></ha-svg-icon>
220
+ </md-outlined-icon-button>
221
+ <md-outlined-icon-button
222
+ title="Rename (also overwrites position with current MPTZ)"
223
+ aria-label="Rename preset"
224
+ @click=${handleAsync(() =>
225
+ this._renamePreset(p.presetId, p.name),
226
+ )}
227
+ >
228
+ <ha-svg-icon .path=${mdiPencil}></ha-svg-icon>
229
+ </md-outlined-icon-button>
230
+ <md-outlined-icon-button
231
+ title="Remove"
232
+ @click=${handleAsync(() =>
233
+ this._removePreset(p.presetId, p.name),
234
+ )}
235
+ >
236
+ <ha-svg-icon .path=${mdiTrashCan}></ha-svg-icon>
237
+ </md-outlined-icon-button>
238
+ </div>`,
239
+ )}
240
+ <div class="add-bar">
241
+ <input
242
+ class="add-input"
243
+ placeholder="New preset name…"
244
+ .value=${this._newPresetName}
245
+ @input=${(e: Event) =>
246
+ (this._newPresetName = (e.target as HTMLInputElement).value)}
247
+ />
248
+ <md-outlined-button
249
+ ?disabled=${!this._newPresetName.trim() || items.length >= max}
250
+ @click=${handleAsync(() => this._saveNewPreset())}
251
+ >
252
+ Save current MPTZ
253
+ </md-outlined-button>
254
+ </div>
255
+ </details>
256
+ </div>
257
+ `;
258
+ })()
259
+ : nothing}
260
+ ${features.dptz
261
+ ? (() => {
262
+ const streams = readDptzStreams(this.node, this.endpoint);
263
+ return html`<div class="dptz-note">
264
+ Digital PTZ: <b>${streams.length}</b> active stream${streams.length === 1 ? "" : "s"}
265
+ <span class="muted small">(controls available during live view)</span>
266
+ </div>`;
267
+ })()
268
+ : nothing}
269
+ </div>
270
+ </details>
271
+ `;
272
+ }
273
+
274
+ private _fmtDeg(v: number): string {
275
+ const sign = v >= 0 ? "+" : "";
276
+ return `${sign}${v}°`;
277
+ }
278
+
279
+ private _showBusy() {
280
+ this._toast = "Camera busy";
281
+ this._scheduleToastClear();
282
+ }
283
+
284
+ private _isBusy(err: unknown): boolean {
285
+ const msg = err instanceof Error ? err.message.toLowerCase() : String(err).toLowerCase();
286
+ return msg.includes("busy");
287
+ }
288
+
289
+ private _stepFromEvent(e: MouseEvent, base: number): number {
290
+ return e.shiftKey ? Math.sign(base) : base;
291
+ }
292
+
293
+ private async _move(delta: { panDelta?: number; tiltDelta?: number; zoomDelta?: number }) {
294
+ try {
295
+ await relativeMove(this.client, this.node.node_id, this.endpoint, delta);
296
+ } catch (err) {
297
+ if (this._isBusy(err)) {
298
+ this._showBusy();
299
+ return;
300
+ }
301
+ showAlertDialog({ title: "Move failed", text: err instanceof Error ? err.message : String(err) });
302
+ }
303
+ }
304
+
305
+ private async _goPreset(presetId: number) {
306
+ try {
307
+ await moveToPreset(this.client, this.node.node_id, this.endpoint, presetId);
308
+ } catch (err) {
309
+ const msg = err instanceof Error ? err.message : String(err);
310
+ if (this._isBusy(err)) return this._showBusy();
311
+ if (msg.toLowerCase().includes("not_found")) {
312
+ this._toast = "Preset removed";
313
+ this._scheduleToastClear();
314
+ return;
315
+ }
316
+ showAlertDialog({ title: "Move failed", text: msg });
317
+ }
318
+ }
319
+
320
+ private _scheduleToastClear() {
321
+ if (this._toastTimer) clearTimeout(this._toastTimer);
322
+ this._toastTimer = setTimeout(() => {
323
+ this._toast = null;
324
+ }, 2000);
325
+ }
326
+
327
+ private async _savePresetUpdate(presetId: number) {
328
+ if (!this.node) return;
329
+ const preset = readPresets(this.node, this.endpoint).items.find(p => p.presetId === presetId);
330
+ if (!preset) return;
331
+ try {
332
+ await savePreset(this.client, this.node.node_id, this.endpoint, preset.name, presetId);
333
+ } catch (err) {
334
+ showAlertDialog({ title: "Update failed", text: err instanceof Error ? err.message : String(err) });
335
+ }
336
+ }
337
+
338
+ private async _renamePreset(presetId: number, currentName: string) {
339
+ const next = window.prompt("New name (max 32 chars)", currentName);
340
+ if (typeof next !== "string" || !next.trim()) return;
341
+ try {
342
+ await savePreset(this.client, this.node.node_id, this.endpoint, next.trim().slice(0, 32), presetId);
343
+ } catch (err) {
344
+ showAlertDialog({ title: "Rename failed", text: err instanceof Error ? err.message : String(err) });
345
+ }
346
+ }
347
+
348
+ private async _removePreset(presetId: number, name: string) {
349
+ const ok = await showPromptDialog({
350
+ title: "Remove preset",
351
+ text: `Remove "${name}"?`,
352
+ confirmText: "Remove",
353
+ });
354
+ if (!ok) return;
355
+ try {
356
+ await removePreset(this.client, this.node.node_id, this.endpoint, presetId);
357
+ } catch (err) {
358
+ showAlertDialog({ title: "Remove failed", text: err instanceof Error ? err.message : String(err) });
359
+ }
360
+ }
361
+
362
+ private async _saveNewPreset() {
363
+ if (!this.node) return;
364
+ const name = this._newPresetName.trim().slice(0, 32);
365
+ if (!name) return;
366
+ try {
367
+ await savePreset(this.client, this.node.node_id, this.endpoint, name);
368
+ this._newPresetName = "";
369
+ } catch (err) {
370
+ const msg = err instanceof Error ? err.message : String(err);
371
+ if (msg.toLowerCase().includes("resource_exhausted")) {
372
+ const max = readPresets(this.node, this.endpoint).max;
373
+ this._toast = `Preset list full (${max})`;
374
+ this._scheduleToastClear();
375
+ return;
376
+ }
377
+ showAlertDialog({ title: "Save failed", text: msg });
378
+ }
379
+ }
380
+
381
+ static override styles = [
382
+ ...(Array.isArray(BaseClusterCommands.styles) ? BaseClusterCommands.styles : [BaseClusterCommands.styles]),
383
+ css`
384
+ .readout {
385
+ display: flex;
386
+ align-items: center;
387
+ gap: 16px;
388
+ flex-wrap: wrap;
389
+ font-family: var(--monospace-font, monospace);
390
+ font-size: 0.85rem;
391
+ padding: 4px 0 12px;
392
+ }
393
+ .readout b {
394
+ font-weight: 500;
395
+ color: var(--md-sys-color-on-surface-variant);
396
+ margin-right: 4px;
397
+ }
398
+ .badge {
399
+ padding: 2px 8px;
400
+ border-radius: 4px;
401
+ background: var(--md-sys-color-secondary-container);
402
+ color: var(--md-sys-color-on-secondary-container);
403
+ font-size: 0.75rem;
404
+ }
405
+ .badge.moving {
406
+ background: var(--md-sys-color-primary-container);
407
+ color: var(--md-sys-color-on-primary-container);
408
+ }
409
+ .range {
410
+ color: var(--md-sys-color-on-surface-variant);
411
+ font-size: 0.75rem;
412
+ }
413
+ .dpad-row {
414
+ display: flex;
415
+ align-items: center;
416
+ justify-content: center;
417
+ gap: 24px;
418
+ padding: 12px 0;
419
+ }
420
+ md-outlined-icon-button {
421
+ --md-outlined-icon-button-icon-color: var(--md-sys-color-on-surface);
422
+ --md-outlined-icon-button-outline-color: var(--md-sys-color-outline);
423
+ }
424
+ md-outlined-icon-button[disabled] {
425
+ opacity: 0.6;
426
+ }
427
+ .dpad-grid {
428
+ display: grid;
429
+ grid-template-columns: 40px 40px 40px;
430
+ grid-template-rows: 40px 40px 40px;
431
+ gap: 4px;
432
+ }
433
+ .dpad-center {
434
+ display: grid;
435
+ place-items: center;
436
+ color: var(--md-sys-color-on-surface-variant);
437
+ opacity: 0.4;
438
+ }
439
+ .zoom-col {
440
+ display: flex;
441
+ flex-direction: column;
442
+ align-items: center;
443
+ gap: 4px;
444
+ }
445
+ .muted.small {
446
+ font-size: 0.7rem;
447
+ color: var(--md-sys-color-on-surface-variant);
448
+ }
449
+ .muted {
450
+ color: var(--md-sys-color-on-surface-variant);
451
+ }
452
+ .toast {
453
+ margin-top: 8px;
454
+ padding: 6px 12px;
455
+ background: var(--md-sys-color-inverse-surface);
456
+ color: var(--md-sys-color-inverse-on-surface);
457
+ border-radius: 4px;
458
+ font-size: 0.85rem;
459
+ text-align: center;
460
+ }
461
+ .presets-frame {
462
+ margin-top: 12px;
463
+ padding-top: 12px;
464
+ border-top: 1px solid var(--md-sys-color-outline-variant);
465
+ }
466
+ .presets-header {
467
+ display: flex;
468
+ justify-content: space-between;
469
+ font-weight: 500;
470
+ margin-bottom: 8px;
471
+ }
472
+ .chip-row {
473
+ display: flex;
474
+ gap: 6px;
475
+ flex-wrap: wrap;
476
+ margin-bottom: 8px;
477
+ }
478
+ .chip {
479
+ padding: 6px 14px;
480
+ border-radius: 16px;
481
+ background: var(--md-sys-color-primary-container);
482
+ color: var(--md-sys-color-on-primary-container);
483
+ border: none;
484
+ cursor: pointer;
485
+ font-size: 0.85rem;
486
+ font-family: inherit;
487
+ }
488
+ .chip:hover {
489
+ filter: brightness(1.05);
490
+ }
491
+ details.manager summary {
492
+ cursor: pointer;
493
+ font-size: 0.85rem;
494
+ color: var(--md-sys-color-on-surface-variant);
495
+ padding: 4px 0;
496
+ }
497
+ .preset-row {
498
+ display: flex;
499
+ align-items: center;
500
+ gap: 12px;
501
+ padding: 4px 8px;
502
+ border-radius: 4px;
503
+ }
504
+ .preset-row:hover {
505
+ background: var(--md-sys-color-surface-container-high);
506
+ }
507
+ .pid {
508
+ font-family: var(--monospace-font, monospace);
509
+ color: var(--md-sys-color-on-surface-variant);
510
+ font-size: 0.85rem;
511
+ min-width: 32px;
512
+ }
513
+ .pname {
514
+ font-weight: 500;
515
+ }
516
+ .pcoord {
517
+ font-family: var(--monospace-font, monospace);
518
+ font-size: 0.75rem;
519
+ color: var(--md-sys-color-on-surface-variant);
520
+ }
521
+ .grow {
522
+ flex: 1;
523
+ }
524
+ .add-bar {
525
+ display: flex;
526
+ gap: 8px;
527
+ align-items: center;
528
+ padding-top: 8px;
529
+ margin-top: 8px;
530
+ border-top: 1px solid var(--md-sys-color-outline-variant);
531
+ }
532
+ .add-input {
533
+ flex: 1;
534
+ padding: 6px 8px;
535
+ border: 1px solid var(--md-sys-color-outline);
536
+ border-radius: 4px;
537
+ background: var(--md-sys-color-surface);
538
+ color: var(--md-sys-color-on-surface);
539
+ font-family: inherit;
540
+ font-size: 0.85rem;
541
+ }
542
+ .dptz-note {
543
+ margin-top: 12px;
544
+ padding: 8px 12px;
545
+ background: var(--md-sys-color-surface-container);
546
+ border-radius: 4px;
547
+ font-size: 0.85rem;
548
+ }
549
+ .dptz-note b {
550
+ font-weight: 500;
551
+ }
552
+ `,
553
+ ];
554
+ }
555
+
556
+ registerClusterCommands(AVSUM_CLUSTER_ID, "avsum-cluster-commands");
557
+
558
+ declare global {
559
+ interface HTMLElementTagNameMap {
560
+ "avsum-cluster-commands": AvsumClusterCommands;
561
+ }
562
+ }