Skip to content

Commit 44679f2

Browse files
Include (base) dashboard in the builtin-webserver (#593)
Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
1 parent 7eeb288 commit 44679f2

File tree

6 files changed

+73
-25
lines changed

6 files changed

+73
-25
lines changed

.github/workflows/release.yml

+21-10
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,9 @@ name: Publish releases
33
on:
44
release:
55
types: [published]
6+
env:
7+
PYTHON_VERSION: "3.10"
8+
NODE_VERSION: "18.x"
69

710
jobs:
811
build-and-publish-pypi:
@@ -28,10 +31,14 @@ jobs:
2831
exit 1
2932
fi
3033
fi
31-
- name: Set up Python 3.10
34+
- name: Set up Python ${{ env.PYTHON_VERSION }}
3235
uses: actions/setup-python@v5.0.0
3336
with:
34-
python-version: "3.10"
37+
python-version: ${{ env.PYTHON_VERSION }}
38+
- name: Set up Node ${{ env.NODE_VERSION }}
39+
uses: actions/setup-node@v4
40+
with:
41+
node-version: ${{ env.NODE_VERSION }}
3542
- name: Install build
3643
run: >-
3744
pip install build tomli tomli-w
@@ -48,7 +55,13 @@ jobs:
4855
4956
with open("pyproject.toml", "wb") as f:
5057
tomli_w.dump(pyproject, f)
51-
- name: Build
58+
- name: Build dashboard
59+
run: |
60+
cd dashboard
61+
script/setup
62+
script/build
63+
cd ..
64+
- name: Build python package
5265
run: >-
5366
python3 -m build
5467
- name: Publish release to PyPI
@@ -69,9 +82,9 @@ jobs:
6982
- name: Log in to the GitHub container registry
7083
uses: docker/login-action@v3.0.0
7184
with:
72-
registry: ghcr.io
73-
username: ${{ github.repository_owner }}
74-
password: ${{ secrets.GITHUB_TOKEN }}
85+
registry: ghcr.io
86+
username: ${{ github.repository_owner }}
87+
password: ${{ secrets.GITHUB_TOKEN }}
7588
- name: Set up Docker Buildx
7689
uses: docker/setup-buildx-action@v3.1.0
7790
- name: Version number for tags
@@ -95,8 +108,7 @@ jobs:
95108
ghcr.io/${{ github.repository_owner }}/python-matter-server:${{ steps.tags.outputs.major }},
96109
ghcr.io/${{ github.repository_owner }}/python-matter-server:stable
97110
push: true
98-
build-args:
99-
"PYTHON_MATTER_SERVER=${{ needs.build-and-publish-pypi.outputs.version }}"
111+
build-args: "PYTHON_MATTER_SERVER=${{ needs.build-and-publish-pypi.outputs.version }}"
100112
- name: Build and Push pre-release
101113
uses: docker/build-push-action@v5.1.0
102114
if: github.event.release.prerelease == true
@@ -108,5 +120,4 @@ jobs:
108120
ghcr.io/${{ github.repository_owner }}/python-matter-server:${{ steps.tags.outputs.patch }},
109121
ghcr.io/${{ github.repository_owner }}/python-matter-server:beta
110122
push: true
111-
build-args:
112-
"PYTHON_MATTER_SERVER=${{ needs.build-and-publish-pypi.outputs.version }}"
123+
build-args: "PYTHON_MATTER_SERVER=${{ needs.build-and-publish-pypi.outputs.version }}"

dashboard/src/entrypoint/main.ts

+10-7
Original file line numberDiff line numberDiff line change
@@ -3,16 +3,19 @@ import { MatterClient } from "../client/client";
33
async function main() {
44
import("../pages/matter-dashboard-app");
55

6-
// Turn httpX url into wsX url and append "/ws"
7-
let url = "ws" + new URL("./ws", location.href).toString().substring(4);
8-
9-
// Inside Home Assistant ingress, we will not prompt for the URL
10-
if (!location.pathname.endsWith("/ingress")) {
6+
let url = "";
7+
// Detect if we're running in the (production) webserver included in the matter server or not.
8+
if (location.href.includes(":5580")) {
9+
// production server running inside the matter server
10+
// Turn httpX url into wsX url and append "/ws"
11+
url = "ws" + new URL("./ws", location.href).toString().substring(4);
12+
} else {
13+
// development server, ask for url to matter server
1114
let storageUrl = localStorage.getItem("matterURL");
1215
if (!storageUrl) {
1316
storageUrl = prompt(
14-
"Enter Matter URL",
15-
"ws://homeassistant.local:5580/ws"
17+
"Enter Websocket URL to a running Matter Server",
18+
"ws://localhost:5580/ws"
1619
);
1720
if (!storageUrl) {
1821
alert("Unable to connect without URL");

dashboard/src/pages/matter-dashboard.ts

+9-4
Original file line numberDiff line numberDiff line change
@@ -28,14 +28,19 @@ class MatterDashboard extends LitElement {
2828

2929
render() {
3030
const nodes = this.nodeEntries(this.nodes);
31+
const isProductionServer = location.href.includes(":5580")
3132

3233
return html`
3334
<div class="header">
3435
<div>Python Matter Server</div>
3536
<div class="actions">
37+
${isProductionServer
38+
? ""
39+
: html`
3640
<md-icon-button @click=${this._disconnect}>
3741
<ha-svg-icon .path=${mdiLogout}></ha-svg-icon>
3842
</md-icon-button>
43+
`}
3944
</div>
4045
</div>
4146
<div class="container">
@@ -50,20 +55,20 @@ class MatterDashboard extends LitElement {
5055
</md-list-item>
5156
<md-divider></md-divider>
5257
${nodes.map(([id, node]) => {
53-
return html`
58+
return html`
5459
<md-list-item type="link" href=${`#node/${node.node_id}`}>
5560
<span slot="start">${node.node_id}</span>
5661
<div slot="headline">
5762
${node.vendorName} ${node.productName}
5863
${node.available
59-
? ""
60-
: html`<span class="status">OFFLINE</span>`}
64+
? ""
65+
: html`<span class="status">OFFLINE</span>`}
6166
</div>
6267
<div slot="supporting-text">${node.serialNumber}</div>
6368
<ha-svg-icon slot="end" .path=${mdiChevronRight}></ha-svg-icon>
6469
</md-list-item>
6570
`;
66-
})}
71+
})}
6772
</md-list>
6873
</div>
6974
<div class="footer">

matter_server/server/server.py

+30-1
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,11 @@
33
from __future__ import annotations
44

55
import asyncio
6+
from functools import partial
67
import ipaddress
78
import logging
9+
import os
10+
from pathlib import Path
811
from typing import Any, Callable, Set, cast
912
import weakref
1013

@@ -31,6 +34,9 @@
3134
from .storage import StorageController
3235
from .vendor_info import VendorInfo
3336

37+
DASHBOARD_DIR = Path(__file__).parent.joinpath("../../dashboard/dist/web/").resolve()
38+
DASHBOARD_DIR_EXISTS = DASHBOARD_DIR.exists()
39+
3440

3541
def mount_websocket(server: MatterServer, path: str) -> None:
3642
"""Mount the websocket endpoint."""
@@ -106,7 +112,23 @@ async def start(self) -> None:
106112
await self.device_controller.start()
107113
await self.vendor_info.start()
108114
mount_websocket(self, "/ws")
109-
self.app.router.add_route("GET", "/", self._handle_info)
115+
self.app.router.add_route("GET", "/info", self._handle_info)
116+
117+
# Host dashboard if the prebuilt files are detected
118+
if DASHBOARD_DIR_EXISTS:
119+
dashboard_dir = str(DASHBOARD_DIR)
120+
self.logger.debug("Detected dashboard files on %s", dashboard_dir)
121+
for abs_dir, _, files in os.walk(dashboard_dir):
122+
rel_dir = abs_dir.replace(dashboard_dir, "")
123+
for filename in files:
124+
filepath = os.path.join(abs_dir, filename)
125+
handler = partial(self._serve_static, filepath)
126+
if rel_dir == "" and filename == "index.html":
127+
route_path = "/"
128+
else:
129+
route_path = f"{rel_dir}/{filename}"
130+
self.app.router.add_route("GET", route_path, handler)
131+
110132
self._runner = web.AppRunner(self.app, access_log=None)
111133
await self._runner.setup()
112134
self._http = MultiHostTCPSite(
@@ -233,3 +255,10 @@ async def _handle_info(self, request: web.Request) -> web.Response:
233255
"""Handle info endpoint to serve basic server (version) info."""
234256
# pylint: disable=unused-argument
235257
return web.json_response(self.get_info(), dumps=json_dumps)
258+
259+
async def _serve_static(
260+
self, file_path: str, _request: web.Request
261+
) -> web.FileResponse:
262+
"""Serve file response."""
263+
headers = {"Cache-Control": "no-cache"}
264+
return web.FileResponse(file_path, headers=headers)

pyproject.toml

+1-1
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ zip-safe = false
7070
matter_server = ["py.typed"]
7171

7272
[tool.setuptools.packages.find]
73-
include = ["matter_server*"]
73+
include = ["matter_server*", "dashboard/dist/web*"]
7474

7575
[tool.mypy]
7676
check_untyped_defs = true

tests/server/test_server.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -119,11 +119,11 @@ async def test_server_start(
119119
assert application.call_count == 1
120120
application_instance = application.return_value
121121
add_route = application_instance.router.add_route
122-
assert add_route.call_count == 2
122+
assert add_route.call_count >= 2
123123
assert add_route.call_args_list[0][0][0] == "GET"
124124
assert add_route.call_args_list[0][0][1] == "/ws"
125125
assert add_route.call_args_list[1][0][0] == "GET"
126-
assert add_route.call_args_list[1][0][1] == "/"
126+
assert add_route.call_args_list[1][0][1] == "/info"
127127
assert app_runner.call_count == 1
128128
assert app_runner.return_value.setup.call_count == 1
129129
assert multi_host_tcp_site.call_count == 1

0 commit comments

Comments
 (0)