@matter-server/dashboard 0.2.5 → 0.2.7-alpha.0-20260118-993a1c7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +16 -0
- package/dist/esm/client/models/descriptions.js +1754 -1754
- package/dist/esm/components/dialogs/binding/node-binding-dialog.d.ts.map +1 -1
- package/dist/esm/components/dialogs/binding/node-binding-dialog.js +8 -4
- package/dist/esm/components/dialogs/binding/node-binding-dialog.js.map +1 -1
- package/dist/esm/entrypoint/main.d.ts +1 -1
- package/dist/esm/entrypoint/main.d.ts.map +1 -1
- package/dist/esm/entrypoint/main.js +1 -0
- package/dist/esm/entrypoint/main.js.map +1 -1
- package/dist/esm/pages/components/header.d.ts +8 -0
- package/dist/esm/pages/components/header.d.ts.map +1 -1
- package/dist/esm/pages/components/header.js +56 -6
- package/dist/esm/pages/components/header.js.map +1 -1
- package/dist/esm/pages/components/node-details.d.ts.map +1 -1
- package/dist/esm/pages/components/node-details.js +27 -8
- package/dist/esm/pages/components/node-details.js.map +2 -2
- package/dist/esm/pages/matter-cluster-view.d.ts.map +1 -1
- package/dist/esm/pages/matter-cluster-view.js +3 -2
- package/dist/esm/pages/matter-cluster-view.js.map +1 -1
- package/dist/esm/pages/matter-dashboard-app.d.ts +6 -1
- package/dist/esm/pages/matter-dashboard-app.d.ts.map +1 -1
- package/dist/esm/pages/matter-dashboard-app.js +119 -24
- package/dist/esm/pages/matter-dashboard-app.js.map +1 -1
- package/dist/esm/pages/matter-endpoint-view.d.ts.map +1 -1
- package/dist/esm/pages/matter-endpoint-view.js +2 -1
- package/dist/esm/pages/matter-endpoint-view.js.map +1 -1
- package/dist/esm/util/format_hex.d.ts +17 -0
- package/dist/esm/util/format_hex.d.ts.map +1 -0
- package/dist/esm/util/format_hex.js +18 -0
- package/dist/esm/util/format_hex.js.map +6 -0
- package/dist/esm/util/theme-service.d.ts +27 -0
- package/dist/esm/util/theme-service.d.ts.map +1 -0
- package/dist/esm/util/theme-service.js +71 -0
- package/dist/esm/util/theme-service.js.map +6 -0
- package/dist/web/index.html +35 -0
- package/dist/web/js/{commission-node-dialog-30lGnJcb.js → commission-node-dialog-DGw5qDgH.js} +5 -5
- package/dist/web/js/{commission-node-existing-RgaziosB.js → commission-node-existing-CHyyeC8y.js} +4 -4
- package/dist/web/js/{commission-node-thread-CJn6OYQX.js → commission-node-thread-iRDSlidy.js} +4 -4
- package/dist/web/js/{commission-node-wifi-CCBBvBEh.js → commission-node-wifi-C4YNR3bG.js} +4 -4
- package/dist/web/js/{dialog-box-CFO9GMyG.js → dialog-box-ag-xOaYh.js} +2 -2
- package/dist/web/js/{fire_event-CdvT7FSP.js → fire_event-BeiEbHcE.js} +1 -1
- package/dist/web/js/main.js +140 -8
- package/dist/web/js/{matter-dashboard-app-DwI2RvT1.js → matter-dashboard-app-BxQ4W_uT.js} +3652 -3483
- package/dist/web/js/{node-binding-dialog-CmTgtqz1.js → node-binding-dialog-ClziphM0.js} +11 -7
- package/dist/web/js/{outlined-text-field-DeeCilzP.js → outlined-text-field-B-CiqgEJ.js} +2 -2
- package/dist/web/js/{prevent_default--haJaAsZ.js → prevent_default-Bs2sUnny.js} +1 -1
- package/package.json +3 -3
- package/src/client/models/descriptions.ts +1754 -1754
- package/src/components/dialogs/binding/node-binding-dialog.ts +8 -4
- package/src/entrypoint/main.ts +1 -0
- package/src/pages/components/header.ts +57 -8
- package/src/pages/components/node-details.ts +33 -8
- package/src/pages/matter-cluster-view.ts +3 -2
- package/src/pages/matter-dashboard-app.ts +123 -26
- package/src/pages/matter-endpoint-view.ts +2 -1
- package/src/util/format_hex.ts +30 -0
- package/src/util/theme-service.ts +98 -0
|
@@ -306,7 +306,7 @@ export class NodeBindingDialog extends LitElement {
|
|
|
306
306
|
<md-list style="padding-bottom:18px;">
|
|
307
307
|
${Object.values(bindings).map(
|
|
308
308
|
(entry, index) => html`
|
|
309
|
-
<md-list-item
|
|
309
|
+
<md-list-item class="binding-item">
|
|
310
310
|
<div style="display:flex;gap:10px;">
|
|
311
311
|
<div>node:${entry["node"]}</div>
|
|
312
312
|
<div>endpoint:${entry["endpoint"]}</div>
|
|
@@ -376,9 +376,13 @@ export class NodeBindingDialog extends LitElement {
|
|
|
376
376
|
}
|
|
377
377
|
|
|
378
378
|
static override styles = css`
|
|
379
|
+
.binding-item {
|
|
380
|
+
background: var(--md-sys-color-surface-container-high);
|
|
381
|
+
}
|
|
382
|
+
|
|
379
383
|
.inline-group {
|
|
380
384
|
display: flex;
|
|
381
|
-
border: 2px solid
|
|
385
|
+
border: 2px solid var(--md-sys-color-primary);
|
|
382
386
|
padding: 1px;
|
|
383
387
|
border-radius: 8px;
|
|
384
388
|
position: relative;
|
|
@@ -404,8 +408,8 @@ export class NodeBindingDialog extends LitElement {
|
|
|
404
408
|
position: absolute;
|
|
405
409
|
left: 15px;
|
|
406
410
|
top: -12px;
|
|
407
|
-
background:
|
|
408
|
-
color:
|
|
411
|
+
background: var(--md-sys-color-primary);
|
|
412
|
+
color: var(--md-sys-color-on-primary);
|
|
409
413
|
padding: 3px 15px;
|
|
410
414
|
border-radius: 4px;
|
|
411
415
|
}
|
package/src/entrypoint/main.ts
CHANGED
|
@@ -10,10 +10,11 @@ import "@material/web/iconbutton/icon-button";
|
|
|
10
10
|
import "@material/web/list/list";
|
|
11
11
|
import "@material/web/list/list-item";
|
|
12
12
|
import { MatterClient } from "@matter-server/ws-client";
|
|
13
|
-
import { mdiArrowLeft, mdiLogout } from "@mdi/js";
|
|
13
|
+
import { mdiArrowLeft, mdiBrightnessAuto, mdiLogout, mdiWeatherNight, mdiWeatherSunny } from "@mdi/js";
|
|
14
14
|
import { LitElement, css, html, nothing } from "lit";
|
|
15
|
-
import { customElement, property } from "lit/decorators.js";
|
|
15
|
+
import { customElement, property, state } from "lit/decorators.js";
|
|
16
16
|
import "../../components/ha-svg-icon";
|
|
17
|
+
import { EffectiveTheme, ThemePreference, ThemeService } from "../../util/theme-service.js";
|
|
17
18
|
|
|
18
19
|
interface HeaderAction {
|
|
19
20
|
label: string;
|
|
@@ -28,6 +29,50 @@ export class DashboardHeader extends LitElement {
|
|
|
28
29
|
|
|
29
30
|
public client?: MatterClient;
|
|
30
31
|
|
|
32
|
+
@state() private _themePreference: ThemePreference = ThemeService.preference;
|
|
33
|
+
@state() private _effectiveTheme: EffectiveTheme = ThemeService.effectiveTheme;
|
|
34
|
+
|
|
35
|
+
private _unsubscribeTheme?: () => void;
|
|
36
|
+
|
|
37
|
+
override connectedCallback() {
|
|
38
|
+
super.connectedCallback();
|
|
39
|
+
this._unsubscribeTheme = ThemeService.subscribe(theme => {
|
|
40
|
+
this._effectiveTheme = theme;
|
|
41
|
+
this._themePreference = ThemeService.preference;
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
override disconnectedCallback() {
|
|
46
|
+
super.disconnectedCallback();
|
|
47
|
+
this._unsubscribeTheme?.();
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
private _cycleTheme() {
|
|
51
|
+
ThemeService.cycleTheme();
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
private _getThemeIcon(): string {
|
|
55
|
+
switch (this._themePreference) {
|
|
56
|
+
case "light":
|
|
57
|
+
return mdiWeatherSunny;
|
|
58
|
+
case "dark":
|
|
59
|
+
return mdiWeatherNight;
|
|
60
|
+
case "system":
|
|
61
|
+
return mdiBrightnessAuto;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
private _getThemeTooltip(): string {
|
|
66
|
+
switch (this._themePreference) {
|
|
67
|
+
case "light":
|
|
68
|
+
return "Theme: Light";
|
|
69
|
+
case "dark":
|
|
70
|
+
return "Theme: Dark";
|
|
71
|
+
case "system":
|
|
72
|
+
return `Theme: System (${this._effectiveTheme})`;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
31
76
|
protected override render() {
|
|
32
77
|
return html`
|
|
33
78
|
<div class="header">
|
|
@@ -50,14 +95,18 @@ export class DashboardHeader extends LitElement {
|
|
|
50
95
|
</md-icon-button>
|
|
51
96
|
`;
|
|
52
97
|
})}
|
|
53
|
-
<!--
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
98
|
+
<!-- theme toggle button -->
|
|
99
|
+
<md-icon-button @click=${this._cycleTheme} .title=${this._getThemeTooltip()}>
|
|
100
|
+
<ha-svg-icon .path=${this._getThemeIcon()}></ha-svg-icon>
|
|
101
|
+
</md-icon-button>
|
|
102
|
+
<!-- optional logout button (only when client exists and not in production) -->
|
|
103
|
+
${this.client && !this.client.isProduction
|
|
104
|
+
? html`
|
|
105
|
+
<md-icon-button @click=${this.client.disconnect}>
|
|
58
106
|
<ha-svg-icon .path=${mdiLogout}></ha-svg-icon>
|
|
59
107
|
</md-icon-button>
|
|
60
|
-
`
|
|
108
|
+
`
|
|
109
|
+
: nothing}
|
|
61
110
|
</div>
|
|
62
111
|
</div>
|
|
63
112
|
`;
|
|
@@ -24,6 +24,27 @@ import "../../components/ha-svg-icon";
|
|
|
24
24
|
import { getEndpointDeviceTypes } from "../matter-endpoint-view.js";
|
|
25
25
|
import { bindingContext } from "./context.js";
|
|
26
26
|
|
|
27
|
+
/** Map updateState values to user-friendly labels */
|
|
28
|
+
const UPDATE_STATE_LABELS: Record<number, string> = {
|
|
29
|
+
1: "Idle",
|
|
30
|
+
2: "Querying",
|
|
31
|
+
3: "Waiting (Querying)",
|
|
32
|
+
4: "Downloading",
|
|
33
|
+
5: "Applying",
|
|
34
|
+
6: "Waiting (Applying)",
|
|
35
|
+
7: "Rolling back",
|
|
36
|
+
8: "Waiting for consent",
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
function getUpdateStateLabel(state: number, progress?: number): string {
|
|
40
|
+
const label = UPDATE_STATE_LABELS[state] || `Unknown (${state})`;
|
|
41
|
+
// Show progress only for downloading state
|
|
42
|
+
if (state === 4 && progress !== undefined) {
|
|
43
|
+
return `${label} (${progress}%)`;
|
|
44
|
+
}
|
|
45
|
+
return label;
|
|
46
|
+
}
|
|
47
|
+
|
|
27
48
|
function getNodeDeviceTypes(node: MatterNode): DeviceType[] {
|
|
28
49
|
const uniqueEndpoints = new Set(Object.keys(node.attributes).map(key => Number(key.split("/")[0])));
|
|
29
50
|
const allDeviceTypes: Set<DeviceType> = new Set();
|
|
@@ -86,16 +107,20 @@ export class NodeDetails extends LitElement {
|
|
|
86
107
|
<md-outlined-button @click=${this._reinterview}
|
|
87
108
|
>Interview<ha-svg-icon slot="icon" .path=${mdiChatProcessing}></ha-svg-icon
|
|
88
109
|
></md-outlined-button>
|
|
89
|
-
${this._updateInitiated
|
|
110
|
+
${this._updateInitiated
|
|
90
111
|
? html` <md-outlined-button disabled
|
|
91
|
-
>
|
|
92
|
-
slot="icon"
|
|
93
|
-
.path=${mdiUpdate}
|
|
94
|
-
></ha-svg-icon
|
|
112
|
+
>Checking for updates<ha-svg-icon slot="icon" .path=${mdiUpdate}></ha-svg-icon
|
|
95
113
|
></md-outlined-button>`
|
|
96
|
-
:
|
|
97
|
-
|
|
98
|
-
|
|
114
|
+
: (this.node.updateState || 0) > 1
|
|
115
|
+
? html` <md-outlined-button disabled
|
|
116
|
+
>${getUpdateStateLabel(
|
|
117
|
+
this.node.updateState!,
|
|
118
|
+
this.node.updateStateProgress,
|
|
119
|
+
)}<ha-svg-icon slot="icon" .path=${mdiUpdate}></ha-svg-icon
|
|
120
|
+
></md-outlined-button>`
|
|
121
|
+
: html`<md-outlined-button @click=${this._searchUpdate}
|
|
122
|
+
>Update<ha-svg-icon slot="icon" .path=${mdiUpdate}></ha-svg-icon
|
|
123
|
+
></md-outlined-button>`}
|
|
99
124
|
${bindings
|
|
100
125
|
? html`
|
|
101
126
|
<md-outlined-button @click=${this._binding}>
|
|
@@ -19,6 +19,7 @@ import "../components/ha-svg-icon";
|
|
|
19
19
|
import "../pages/components/node-details";
|
|
20
20
|
import { bindingContext } from "./components/context.js";
|
|
21
21
|
// Cluster command components (auto-register on import)
|
|
22
|
+
import { formatHex } from "../util/format_hex.js";
|
|
22
23
|
import { getClusterCommandsTag } from "./cluster-commands/index.js";
|
|
23
24
|
|
|
24
25
|
declare global {
|
|
@@ -103,7 +104,7 @@ class MatterClusterView extends LitElement {
|
|
|
103
104
|
Endpoint ${this.endpoint}</b
|
|
104
105
|
>
|
|
105
106
|
</div>
|
|
106
|
-
<div slot="supporting-text">ClusterId ${this.cluster} (
|
|
107
|
+
<div slot="supporting-text">ClusterId ${this.cluster} (${formatHex(this.cluster)})</div>
|
|
107
108
|
</md-list-item>
|
|
108
109
|
<md-divider></md-divider>
|
|
109
110
|
${clusterAttributes(this.node.attributes, this.endpoint, this.cluster).map(
|
|
@@ -114,7 +115,7 @@ class MatterClusterView extends LitElement {
|
|
|
114
115
|
"Custom/Unknown Attribute"}
|
|
115
116
|
</div>
|
|
116
117
|
<div slot="supporting-text">
|
|
117
|
-
AttributeId: ${attribute.key} (
|
|
118
|
+
AttributeId: ${attribute.key} (${formatHex(attribute.key)}) - Value type:
|
|
118
119
|
${clusters[this.cluster!]?.attributes[attribute.key]?.type || "unknown"}
|
|
119
120
|
</div>
|
|
120
121
|
<div slot="end">
|
|
@@ -6,11 +6,14 @@
|
|
|
6
6
|
|
|
7
7
|
import { ContextProvider } from "@lit/context";
|
|
8
8
|
import { MatterClient, MatterError } from "@matter-server/ws-client";
|
|
9
|
-
import {
|
|
9
|
+
import { mdiRefresh } from "@mdi/js";
|
|
10
|
+
import { LitElement, PropertyValueMap, css, html } from "lit";
|
|
10
11
|
import { customElement, state } from "lit/decorators.js";
|
|
11
12
|
import { clientContext } from "../client/client-context.js";
|
|
13
|
+
import "../components/ha-svg-icon";
|
|
12
14
|
import { clone } from "../util/clone_class.js";
|
|
13
15
|
import type { Route } from "../util/routing.js";
|
|
16
|
+
import "./components/header";
|
|
14
17
|
import "./matter-cluster-view";
|
|
15
18
|
import "./matter-endpoint-view";
|
|
16
19
|
import "./matter-node-view";
|
|
@@ -34,31 +37,11 @@ class MatterDashboardApp extends LitElement {
|
|
|
34
37
|
@state()
|
|
35
38
|
private _state: "connecting" | "connected" | "error" | "disconnected" = "connecting";
|
|
36
39
|
|
|
37
|
-
private _error: string | undefined;
|
|
38
|
-
|
|
39
40
|
private provider = new ContextProvider(this, { context: clientContext, initialValue: this.client });
|
|
40
41
|
|
|
41
42
|
protected override firstUpdated(_changedProperties: PropertyValueMap<any> | Map<PropertyKey, unknown>): void {
|
|
42
43
|
super.firstUpdated(_changedProperties);
|
|
43
|
-
this.
|
|
44
|
-
() => {
|
|
45
|
-
this._state = "connected";
|
|
46
|
-
this.client.addEventListener("nodes_changed", () => {
|
|
47
|
-
this.requestUpdate();
|
|
48
|
-
this.provider.setValue(clone(this.client));
|
|
49
|
-
});
|
|
50
|
-
this.client.addEventListener("server_info_updated", () => {
|
|
51
|
-
this.provider.setValue(clone(this.client));
|
|
52
|
-
});
|
|
53
|
-
this.client.addEventListener("connection_lost", () => {
|
|
54
|
-
this._state = "disconnected";
|
|
55
|
-
});
|
|
56
|
-
},
|
|
57
|
-
(err: MatterError) => {
|
|
58
|
-
this._state = "error";
|
|
59
|
-
this._error = err.message;
|
|
60
|
-
},
|
|
61
|
-
);
|
|
44
|
+
this._connect();
|
|
62
45
|
|
|
63
46
|
// Handle history changes
|
|
64
47
|
const updateRoute = () => {
|
|
@@ -72,17 +55,73 @@ class MatterDashboardApp extends LitElement {
|
|
|
72
55
|
updateRoute();
|
|
73
56
|
}
|
|
74
57
|
|
|
58
|
+
private _connect() {
|
|
59
|
+
this.client.startListening().then(
|
|
60
|
+
() => {
|
|
61
|
+
this._state = "connected";
|
|
62
|
+
this._setupEventListeners();
|
|
63
|
+
},
|
|
64
|
+
(_err: MatterError) => {
|
|
65
|
+
this._state = "error";
|
|
66
|
+
},
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
private _setupEventListeners() {
|
|
71
|
+
this.client.addEventListener("nodes_changed", () => {
|
|
72
|
+
this.requestUpdate();
|
|
73
|
+
this.provider.setValue(clone(this.client));
|
|
74
|
+
});
|
|
75
|
+
this.client.addEventListener("server_info_updated", () => {
|
|
76
|
+
this.provider.setValue(clone(this.client));
|
|
77
|
+
});
|
|
78
|
+
this.client.addEventListener("connection_lost", () => {
|
|
79
|
+
this._state = "disconnected";
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
private _reconnect = () => {
|
|
84
|
+
this._state = "connecting";
|
|
85
|
+
this._connect();
|
|
86
|
+
};
|
|
87
|
+
|
|
75
88
|
override render() {
|
|
76
89
|
if (this._state === "connecting") {
|
|
77
|
-
return html
|
|
90
|
+
return html`
|
|
91
|
+
<dashboard-header title="Matter Server"></dashboard-header>
|
|
92
|
+
<div class="status-page">
|
|
93
|
+
<p class="status-message">Connecting...</p>
|
|
94
|
+
</div>
|
|
95
|
+
`;
|
|
78
96
|
}
|
|
79
97
|
if (this._state === "disconnected") {
|
|
80
|
-
return html
|
|
98
|
+
return html`
|
|
99
|
+
<dashboard-header title="Matter Server"></dashboard-header>
|
|
100
|
+
<div class="status-page">
|
|
101
|
+
<p class="status-message error">Connection lost</p>
|
|
102
|
+
<p class="status-hint">
|
|
103
|
+
The connection to the Matter Server was lost. Please check if the server is running.
|
|
104
|
+
</p>
|
|
105
|
+
<button class="retry-button" @click=${this._reconnect}>
|
|
106
|
+
<ha-svg-icon .path=${mdiRefresh}></ha-svg-icon>
|
|
107
|
+
Reconnect
|
|
108
|
+
</button>
|
|
109
|
+
</div>
|
|
110
|
+
`;
|
|
81
111
|
}
|
|
82
112
|
if (this._state === "error") {
|
|
83
113
|
return html`
|
|
84
|
-
<
|
|
85
|
-
<
|
|
114
|
+
<dashboard-header title="Matter Server"></dashboard-header>
|
|
115
|
+
<div class="status-page">
|
|
116
|
+
<p class="status-message error">No connection</p>
|
|
117
|
+
<p class="status-hint">
|
|
118
|
+
Unable to connect to the Matter Server. Please check if the server is running.
|
|
119
|
+
</p>
|
|
120
|
+
<button class="retry-button" @click=${this._reconnect}>
|
|
121
|
+
<ha-svg-icon .path=${mdiRefresh}></ha-svg-icon>
|
|
122
|
+
Reconnect
|
|
123
|
+
</button>
|
|
124
|
+
</div>
|
|
86
125
|
`;
|
|
87
126
|
}
|
|
88
127
|
if (this._route.prefix === "node" && this._route.path.length == 3) {
|
|
@@ -122,4 +161,62 @@ class MatterDashboardApp extends LitElement {
|
|
|
122
161
|
.route=${this._route}
|
|
123
162
|
></matter-server-view>`;
|
|
124
163
|
}
|
|
164
|
+
|
|
165
|
+
static override styles = css`
|
|
166
|
+
:host {
|
|
167
|
+
display: block;
|
|
168
|
+
min-height: 100vh;
|
|
169
|
+
background-color: var(--md-sys-color-background);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
.status-page {
|
|
173
|
+
display: flex;
|
|
174
|
+
flex-direction: column;
|
|
175
|
+
align-items: center;
|
|
176
|
+
justify-content: center;
|
|
177
|
+
padding: 48px 24px;
|
|
178
|
+
text-align: center;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
.status-message {
|
|
182
|
+
font-size: 1.5rem;
|
|
183
|
+
color: var(--md-sys-color-on-background);
|
|
184
|
+
margin: 0 0 16px 0;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
.status-message.error {
|
|
188
|
+
color: var(--danger-color);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
.status-hint {
|
|
192
|
+
font-size: 1rem;
|
|
193
|
+
color: var(--md-sys-color-on-surface-variant);
|
|
194
|
+
margin: 0;
|
|
195
|
+
max-width: 400px;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
.retry-button {
|
|
199
|
+
display: inline-flex;
|
|
200
|
+
align-items: center;
|
|
201
|
+
gap: 8px;
|
|
202
|
+
margin-top: 24px;
|
|
203
|
+
padding: 12px 24px;
|
|
204
|
+
font-size: 1rem;
|
|
205
|
+
background-color: var(--md-sys-color-primary);
|
|
206
|
+
color: var(--md-sys-color-on-primary);
|
|
207
|
+
--icon-primary-color: var(--md-sys-color-on-primary);
|
|
208
|
+
border: none;
|
|
209
|
+
border-radius: 4px;
|
|
210
|
+
cursor: pointer;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
.retry-button:hover {
|
|
214
|
+
opacity: 0.9;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
.retry-button ha-svg-icon {
|
|
218
|
+
width: 20px;
|
|
219
|
+
height: 20px;
|
|
220
|
+
}
|
|
221
|
+
`;
|
|
125
222
|
}
|
|
@@ -15,6 +15,7 @@ import { customElement, property } from "lit/decorators.js";
|
|
|
15
15
|
import { guard } from "lit/directives/guard.js";
|
|
16
16
|
import { DeviceType, clusters, device_types } from "../client/models/descriptions.js";
|
|
17
17
|
import "../components/ha-svg-icon";
|
|
18
|
+
import { formatHex } from "../util/format_hex.js";
|
|
18
19
|
|
|
19
20
|
declare global {
|
|
20
21
|
interface HTMLElementTagNameMap {
|
|
@@ -97,7 +98,7 @@ class MatterEndpointView extends LitElement {
|
|
|
97
98
|
href=${`#node/${this.node!.node_id}/${this.endpoint}/${cluster}`}
|
|
98
99
|
>
|
|
99
100
|
<div slot="headline">${clusters[cluster]?.label || "Custom/Unknown Cluster"}</div>
|
|
100
|
-
<div slot="supporting-text">ClusterId ${cluster} (
|
|
101
|
+
<div slot="supporting-text">ClusterId ${cluster} (${formatHex(cluster)})</div>
|
|
101
102
|
<ha-svg-icon slot="end" .path=${mdiChevronRight}></ha-svg-icon>
|
|
102
103
|
</md-list-item>
|
|
103
104
|
`;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2025-2026 Open Home Foundation
|
|
4
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Format a number as a hex string with 0x prefix.
|
|
9
|
+
* Ensures even number of digits with a minimum of 4 digits.
|
|
10
|
+
*
|
|
11
|
+
* Examples:
|
|
12
|
+
* 0 -> "0x0000"
|
|
13
|
+
* 255 -> "0x00FF"
|
|
14
|
+
* 65535 -> "0xFFFF"
|
|
15
|
+
* 65536 -> "0x010000"
|
|
16
|
+
*/
|
|
17
|
+
export function formatHex(value: number): string {
|
|
18
|
+
let hex = value.toString(16).toUpperCase();
|
|
19
|
+
|
|
20
|
+
// Ensure minimum 4 digits
|
|
21
|
+
if (hex.length < 4) {
|
|
22
|
+
hex = hex.padStart(4, "0");
|
|
23
|
+
}
|
|
24
|
+
// Ensure even number of digits
|
|
25
|
+
else if (hex.length % 2 !== 0) {
|
|
26
|
+
hex = "0" + hex;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return `0x${hex}`;
|
|
30
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2025-2026 Open Home Foundation
|
|
4
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Theme service for managing dark/light mode preferences.
|
|
9
|
+
* Supports three modes: light, dark, and system (auto-detect from OS).
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
export type ThemePreference = "light" | "dark" | "system";
|
|
13
|
+
export type EffectiveTheme = "light" | "dark";
|
|
14
|
+
|
|
15
|
+
const STORAGE_KEY = "matterTheme";
|
|
16
|
+
|
|
17
|
+
class ThemeServiceImpl {
|
|
18
|
+
private _preference: ThemePreference = "system";
|
|
19
|
+
private _mediaQuery: MediaQueryList;
|
|
20
|
+
private _listeners: Set<(theme: EffectiveTheme) => void> = new Set();
|
|
21
|
+
|
|
22
|
+
constructor() {
|
|
23
|
+
this._mediaQuery = window.matchMedia("(prefers-color-scheme: dark)");
|
|
24
|
+
this._mediaQuery.addEventListener("change", () => this._applyTheme());
|
|
25
|
+
this._loadPreference();
|
|
26
|
+
this._applyTheme();
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
get preference(): ThemePreference {
|
|
30
|
+
return this._preference;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
get effectiveTheme(): EffectiveTheme {
|
|
34
|
+
if (this._preference === "system") {
|
|
35
|
+
return this._mediaQuery.matches ? "dark" : "light";
|
|
36
|
+
}
|
|
37
|
+
return this._preference;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
setPreference(pref: ThemePreference): void {
|
|
41
|
+
this._preference = pref;
|
|
42
|
+
localStorage.setItem(STORAGE_KEY, pref);
|
|
43
|
+
this._applyTheme();
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
cycleTheme(): ThemePreference {
|
|
47
|
+
const cycle: ThemePreference[] = ["light", "dark", "system"];
|
|
48
|
+
const currentIndex = cycle.indexOf(this._preference);
|
|
49
|
+
const nextIndex = (currentIndex + 1) % cycle.length;
|
|
50
|
+
this.setPreference(cycle[nextIndex]);
|
|
51
|
+
return this._preference;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
subscribe(callback: (theme: EffectiveTheme) => void): () => void {
|
|
55
|
+
this._listeners.add(callback);
|
|
56
|
+
return () => this._listeners.delete(callback);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
private _loadPreference(): void {
|
|
60
|
+
// Check for query parameter override (e.g., ?theme=dark)
|
|
61
|
+
const urlParams = new URLSearchParams(window.location.search);
|
|
62
|
+
const themeParam = urlParams.get("theme") as ThemePreference | null;
|
|
63
|
+
if (themeParam && ["light", "dark", "system"].includes(themeParam)) {
|
|
64
|
+
// Use query parameter value and save to localStorage
|
|
65
|
+
this._preference = themeParam;
|
|
66
|
+
localStorage.setItem(STORAGE_KEY, themeParam);
|
|
67
|
+
// Remove the query parameter from URL without reload
|
|
68
|
+
urlParams.delete("theme");
|
|
69
|
+
const newUrl = urlParams.toString()
|
|
70
|
+
? `${window.location.pathname}?${urlParams.toString()}${window.location.hash}`
|
|
71
|
+
: `${window.location.pathname}${window.location.hash}`;
|
|
72
|
+
history.replaceState({}, "", newUrl);
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Fall back to localStorage
|
|
77
|
+
const stored = localStorage.getItem(STORAGE_KEY) as ThemePreference | null;
|
|
78
|
+
if (stored && ["light", "dark", "system"].includes(stored)) {
|
|
79
|
+
this._preference = stored;
|
|
80
|
+
}
|
|
81
|
+
// Default is "system" if nothing stored
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
private _applyTheme(): void {
|
|
85
|
+
const effective = this.effectiveTheme;
|
|
86
|
+
document.documentElement.classList.toggle("dark-theme", effective === "dark");
|
|
87
|
+
|
|
88
|
+
// Update meta theme-color for mobile browsers
|
|
89
|
+
const metaThemeColor = document.querySelector('meta[name="theme-color"]');
|
|
90
|
+
if (metaThemeColor) {
|
|
91
|
+
metaThemeColor.setAttribute("content", effective === "dark" ? "#1e1e1e" : "#03a9f4");
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
this._listeners.forEach(cb => cb(effective));
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export const ThemeService = new ThemeServiceImpl();
|