-
Notifications
You must be signed in to change notification settings - Fork 121
Add URL Display Theme for Raspberry Pi #3205
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
MartinRinas
wants to merge
55
commits into
openWB:master
Choose a base branch
from
MartinRinas:copilot/implement-display-theme-modbus
base: master
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
55 commits
Select commit
Hold shift + click to select a range
19a9835
Update publish_docs_to_wiki.yml
MartinRinas 07abf0b
Merge branch 'openWB:master' into master
MartinRinas 1ae671a
Merge branch 'openWB:master' into master
MartinRinas 2646d1a
Merge branch 'openWB:master' into master
MartinRinas f8944c5
Merge branch 'openWB:master' into master
MartinRinas e83795f
Merge branch 'openWB:master' into master
MartinRinas c4a0d32
Merge branch 'openWB:master' into master
MartinRinas 14694d4
Merge branch 'openWB:master' into master
MartinRinas 8afa5c2
Merge branch 'openWB:master' into master
MartinRinas 6be1634
Merge branch 'openWB:master' into master
MartinRinas b6074a1
Merge branch 'openWB:master' into master
MartinRinas 6f581a8
fems: support multiple and single segment regex rqeuests (#1397)
LKuemmel 719a191
one connect to server for all components
LKuemmel 0387eed
0.1s sleep between readings
LKuemmel e033d36
fix self.data --> self._data in fems/bat.py
dj3mu 5773c81
Duo+RFID: fix matching if other cp is plugged
LKuemmel fd411b4
Revert "fix rct crashes"
LKuemmel 9577b56
RCT: 0.5s sleep between server connections
LKuemmel e360cb8
build UI
LKuemmel 59e83fa
Calculate_soc_tronity (#1398)
MartinRinas c32d6d8
Merge branch 'master' of https://github.com/openWB/core
MartinRinas 909fb3f
Merge branch 'master' of https://github.com/openWB/core
MartinRinas 3aa4283
Merge branch 'master' of https://github.com/openWB/core
MartinRinas 9d9c7b3
Merge branch 'master' of https://github.com/openWB/core
MartinRinas 17dcaea
Merge branch 'openWB:master' into master
MartinRinas 65e9b0e
Merge branch 'openWB:master' into master
MartinRinas 9d78bd5
Merge branch 'openWB:master' into master
MartinRinas d936545
Merge branch 'openWB:master' into master
MartinRinas 13cc4de
Merge branch 'openWB:master' into master
MartinRinas 068bf56
Merge branch 'openWB:master' into master
MartinRinas 27cde5d
Merge branch 'openWB:master' into master
MartinRinas 68261e6
Merge branch 'master' of https://github.com/MartinRinas/core
MartinRinas baccc10
Merge branch 'openWB:master' into master
MartinRinas 7bf97bb
Merge branch 'openWB:master' into master
MartinRinas 0596bab
Merge branch 'openWB:master' into master
MartinRinas 07389a4
Merge branch 'openWB:master' into master
MartinRinas c1c6610
Merge branch 'openWB:master' into master
MartinRinas bd49e28
fix publish soc_timestamp
MartinRinas b41f1d9
Initial plan
Copilot 34cdd45
Add URL Display Theme for Raspberry Pi
Copilot 661d0f2
Fix insecure randomness: replace Math.random() with crypto.randomUUID…
Copilot 61b54a0
Revert "Fix insecure randomness: replace Math.random() with crypto.ra…
MartinRinas 52c8ac0
symlink
MartinRinas 77da3a6
symlink
MartinRinas 1bb067e
Merge branch 'master' of https://github.com/openWB/core into copilot/…
MartinRinas 5bd0906
review comments
MartinRinas 4467eec
typos
MartinRinas abcb264
move to vite8, remove unused dependencies
MartinRinas 46a7c8a
build workflow
MartinRinas fcb945b
Merge branch 'openWB:master' into copilot/implement-display-theme-modbus
MartinRinas d0ce99d
Implement display theme deserialization and validation for URL display
MartinRinas 19eb1ae
enhance local theme loading logic
MartinRinas 785780b
fix flake
MartinRinas 180e977
Refactor theme loading logic to streamline iframe source setting and …
MartinRinas 17f9811
Enhance theme checking and logging in MQTT message processing; add te…
MartinRinas File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,34 @@ | ||
| name: Check Display Theme URL Display | ||
|
|
||
| on: | ||
| pull_request: | ||
| paths: | ||
| - packages/modules/display_themes/url_display/source/** | ||
| branches: | ||
| - master | ||
|
|
||
| jobs: | ||
| build: | ||
| runs-on: ubuntu-latest | ||
|
|
||
| strategy: | ||
| matrix: | ||
| node: [ 24 ] | ||
| # See supported Node.js release schedule at https://nodejs.org/en/about/releases/ | ||
|
|
||
| steps: | ||
| - name: Checkout Repository | ||
| uses: actions/checkout@v4 | ||
|
|
||
| - name: Setup Node.js ${{ matrix.node }} | ||
| uses: actions/setup-node@v4 | ||
| with: | ||
| node-version: ${{ matrix.node }} | ||
| cache: 'npm' | ||
| cache-dependency-path: packages/modules/display_themes/url_display/source/package-lock.json | ||
|
|
||
| - name: Install Dependencies and Build | ||
| run: | | ||
| cd packages/modules/display_themes/url_display/source | ||
| npm install | ||
| npm run build --if-present |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,29 @@ | ||
| import importlib | ||
| from typing import Any | ||
|
|
||
| from dataclass_utils import dataclass_from_dict | ||
|
|
||
|
|
||
| def deserialize_display_theme(config: dict[str, Any]) -> Any: | ||
| if not isinstance(config, dict): | ||
| raise ValueError("Die Display-Theme-Konfiguration muss ein JSON-Objekt sein.") | ||
|
|
||
| theme_type = config.get("type") | ||
| if not isinstance(theme_type, str) or not theme_type.isidentifier(): | ||
| raise ValueError("Der Typ des Display-Themes fehlt.") | ||
| if "configuration" in config and not isinstance(config["configuration"], dict): | ||
| raise ValueError("Die Konfiguration des Display-Themes muss ein JSON-Objekt sein.") | ||
|
|
||
| module_name = f"modules.display_themes.{theme_type}.config" | ||
| try: | ||
| module = importlib.import_module(f".{theme_type}.config", "modules.display_themes") | ||
| except ModuleNotFoundError as exc: | ||
| if exc.name != module_name: | ||
| raise | ||
| raise ValueError(f"Unbekanntes Display-Theme: {theme_type}") from exc | ||
|
|
||
| theme = dataclass_from_dict(module.theme_descriptor.configuration_factory, config) | ||
| default_theme = module.theme_descriptor.configuration_factory() | ||
| if not isinstance(theme.configuration, type(default_theme.configuration)): | ||
| raise ValueError("Die Konfiguration des Display-Themes hat einen ungültigen Typ.") | ||
| return theme |
Empty file.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,119 @@ | ||
| import ipaddress | ||
| import json | ||
| import socket | ||
| import subprocess | ||
| from typing import Optional | ||
| from urllib.parse import urlsplit, urlunsplit | ||
|
|
||
| from helpermodules.auto_str import auto_str | ||
| from helpermodules.utils.run_command import run_command | ||
|
|
||
| from modules.common.abstract_device import DeviceDescriptor | ||
|
|
||
|
|
||
| RFC1918_NETWORKS = ( | ||
| ipaddress.ip_network("10.0.0.0/8"), | ||
| ipaddress.ip_network("172.16.0.0/12"), | ||
| ipaddress.ip_network("192.168.0.0/16"), | ||
| ) | ||
|
|
||
|
|
||
| def _get_local_ipv4_networks() -> tuple[ipaddress.IPv4Network, ...]: | ||
| try: | ||
| output = run_command(["ip", "-j", "-4", "address", "show", "up"]) | ||
| if output is None: | ||
| raise ValueError("Der Aufruf von 'ip' hat keine Ausgabe geliefert.") | ||
| interfaces = json.loads(output) | ||
| return tuple( | ||
| ipaddress.IPv4Network(f"{address['local']}/{address['prefixlen']}", strict=False) | ||
| for interface in interfaces | ||
| for address in interface.get("addr_info", []) | ||
| if address.get("family") == "inet" | ||
| ) | ||
| except (KeyError, TypeError, ValueError, subprocess.CalledProcessError) as exc: | ||
| raise ValueError("Die lokalen IPv4-Netzwerke konnten nicht ermittelt werden.") from exc | ||
|
|
||
|
|
||
| def validate_url(url: str) -> str: | ||
| if not isinstance(url, str) or not url.strip(): | ||
| raise ValueError("Bitte eine URL angeben.") | ||
|
|
||
| normalized_url = url.strip() | ||
| if "://" not in normalized_url: | ||
| normalized_url = f"http://{normalized_url}" | ||
|
|
||
| parsed_url = urlsplit(normalized_url) | ||
| if parsed_url.scheme.lower() not in ("http", "https"): | ||
| raise ValueError("Die URL muss das Protokoll HTTP oder HTTPS verwenden.") | ||
| if parsed_url.hostname is None: | ||
| raise ValueError("Die URL enthält keinen gültigen Hostnamen.") | ||
| if parsed_url.username is not None or parsed_url.password is not None: | ||
| raise ValueError("Die URL darf keine Zugangsdaten enthalten.") | ||
| try: | ||
| parsed_url.port | ||
| except ValueError as exc: | ||
| raise ValueError("Die URL enthält keinen gültigen Port.") from exc | ||
|
|
||
| try: | ||
| resolved_addresses = { | ||
| ipaddress.IPv4Address(address[4][0]) | ||
| for address in socket.getaddrinfo( | ||
| parsed_url.hostname, | ||
| parsed_url.port, | ||
| family=socket.AF_INET, | ||
| type=socket.SOCK_STREAM, | ||
| ) | ||
| } | ||
| except socket.gaierror as exc: | ||
| raise ValueError(f"Der Hostname '{parsed_url.hostname}' konnte nicht aufgelöst werden.") from exc | ||
|
|
||
| if not resolved_addresses: | ||
| raise ValueError(f"Für den Hostnamen '{parsed_url.hostname}' wurde keine IPv4-Adresse gefunden.") | ||
|
|
||
| addresses_outside_rfc1918 = [ | ||
| address | ||
| for address in resolved_addresses | ||
| if not any(address in network for network in RFC1918_NETWORKS) | ||
| ] | ||
| if not addresses_outside_rfc1918: | ||
| return urlunsplit(parsed_url._replace(scheme=parsed_url.scheme.lower())) | ||
|
|
||
| local_networks = _get_local_ipv4_networks() | ||
| invalid_addresses = [ | ||
| str(address) | ||
| for address in addresses_outside_rfc1918 | ||
| if not any(address in network for network in local_networks) | ||
| ] | ||
| if invalid_addresses: | ||
| raise ValueError( | ||
| "Die URL muss auf eine IPv4-Adresse im lokalen Netzwerk zeigen. " | ||
| f"Nicht zulässig: {', '.join(sorted(invalid_addresses))}" | ||
| ) | ||
|
|
||
| return urlunsplit(parsed_url._replace(scheme=parsed_url.scheme.lower())) | ||
|
|
||
|
|
||
| @auto_str | ||
| class UrlDisplayThemeConfiguration: | ||
| def __init__(self, | ||
| url: str = "" | ||
| ) -> None: | ||
| self.url = validate_url(url) if url else "" | ||
|
|
||
|
|
||
| @auto_str | ||
| class UrlDisplayTheme: | ||
| def __init__(self, | ||
| name: str = "URL Display", | ||
| type: str = "url_display", | ||
| official: bool = False, | ||
| userManagementSupported: bool = False, | ||
| configuration: Optional[UrlDisplayThemeConfiguration] = None) -> None: | ||
| self.name = name | ||
| self.type = type | ||
| self.official = official | ||
| self.userManagementSupported = userManagementSupported | ||
| self.configuration = configuration or UrlDisplayThemeConfiguration() | ||
|
|
||
|
|
||
| theme_descriptor = DeviceDescriptor(configuration_factory=UrlDisplayTheme) | ||
135 changes: 135 additions & 0 deletions
135
packages/modules/display_themes/url_display/config_test.py
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,135 @@ | ||
| import socket | ||
|
|
||
| import pytest | ||
|
|
||
| from modules.display_themes import deserialize_display_theme | ||
| from modules.display_themes.url_display import config | ||
|
|
||
|
|
||
| @pytest.fixture(autouse=True) | ||
| def local_networks(monkeypatch): | ||
| def run_command(command): | ||
| assert command == ["ip", "-j", "-4", "address", "show", "up"] | ||
| return '[{"addr_info": [{"family": "inet", "local": "100.64.1.10", "prefixlen": 24}]}]' | ||
|
|
||
| monkeypatch.setattr( | ||
| config, | ||
| "run_command", | ||
| run_command, | ||
| ) | ||
|
|
||
|
|
||
| def mock_dns(monkeypatch, *addresses): | ||
| def getaddrinfo(host, port, *, family, type): | ||
| assert host | ||
| assert port is None or isinstance(port, int) | ||
| assert family == socket.AF_INET | ||
| assert type == socket.SOCK_STREAM | ||
| return [ | ||
| (socket.AF_INET, socket.SOCK_STREAM, 6, "", (address, 0)) | ||
| for address in addresses | ||
| ] | ||
|
|
||
| monkeypatch.setattr( | ||
| config.socket, | ||
| "getaddrinfo", | ||
| getaddrinfo, | ||
| ) | ||
|
|
||
|
|
||
| @pytest.mark.parametrize("address", [ | ||
| "10.0.0.1", | ||
| "172.16.0.1", | ||
| "172.31.255.254", | ||
| "192.168.1.1", | ||
| ]) | ||
| def test_rfc1918_address_is_allowed(monkeypatch, address): | ||
| mock_dns(monkeypatch, address) | ||
|
|
||
| assert config.validate_url("https://example.local:8443/path?value=1") == ( | ||
| "https://example.local:8443/path?value=1" | ||
| ) | ||
|
|
||
|
|
||
| def test_rfc1918_validation_does_not_require_interface_lookup(monkeypatch): | ||
| mock_dns(monkeypatch, "192.168.1.10") | ||
| monkeypatch.setattr( | ||
| config, | ||
| "run_command", | ||
| lambda command: pytest.fail( | ||
| f"Interface lookup should not be called for RFC1918 addresses: {command}" | ||
| ), | ||
| ) | ||
|
|
||
| assert config.validate_url("openwb.local") == "http://openwb.local" | ||
|
|
||
|
|
||
| def test_address_in_local_subnet_is_allowed(monkeypatch): | ||
| mock_dns(monkeypatch, "100.64.1.25") | ||
|
|
||
| assert config.validate_url("evcc.local/status") == "http://evcc.local/status" | ||
|
|
||
|
|
||
| @pytest.mark.parametrize("address", [ | ||
| "8.8.8.8", | ||
| "100.64.2.25", | ||
| "172.15.255.255", | ||
| "172.32.0.1", | ||
| ]) | ||
| def test_non_local_address_is_rejected(monkeypatch, address): | ||
| mock_dns(monkeypatch, address) | ||
|
|
||
| with pytest.raises(ValueError, match="lokalen Netzwerk"): | ||
| config.validate_url("https://example.com") | ||
|
|
||
|
|
||
| def test_all_resolved_addresses_must_be_local(monkeypatch): | ||
| mock_dns(monkeypatch, "192.168.1.10", "8.8.8.8") | ||
|
|
||
| with pytest.raises(ValueError, match="8.8.8.8"): | ||
| config.validate_url("https://example.local") | ||
|
|
||
|
|
||
| @pytest.mark.parametrize("url", [ | ||
| "", | ||
| "ftp://192.168.1.10", | ||
| "http://user:password@192.168.1.10", | ||
| "http://192.168.1.10:invalid", | ||
| ]) | ||
| def test_invalid_url_is_rejected(monkeypatch, url): | ||
| mock_dns(monkeypatch, "192.168.1.10") | ||
|
|
||
| with pytest.raises(ValueError): | ||
| config.validate_url(url) | ||
|
|
||
|
|
||
| def test_unresolvable_hostname_is_rejected(monkeypatch): | ||
| def raise_gaierror(*args, **kwargs): | ||
| assert args or kwargs | ||
| raise socket.gaierror | ||
|
|
||
| monkeypatch.setattr(config.socket, "getaddrinfo", raise_gaierror) | ||
|
|
||
| with pytest.raises(ValueError, match="konnte nicht aufgelöst werden"): | ||
| config.validate_url("https://missing.local") | ||
|
|
||
|
|
||
| @pytest.mark.parametrize("configuration", [None, "https://192.168.1.10", 1, []]) | ||
| def test_non_object_theme_configuration_is_rejected(configuration): | ||
| with pytest.raises(ValueError, match="JSON-Objekt"): | ||
| deserialize_display_theme({ | ||
| "name": "URL Display", | ||
| "type": "url_display", | ||
| "configuration": configuration, | ||
| }) | ||
|
|
||
|
|
||
| @pytest.mark.parametrize("theme_type", ["cards", "colors", "url_display"]) | ||
| def test_display_theme_configuration_is_deserialized(theme_type): | ||
| theme = deserialize_display_theme({ | ||
| "type": theme_type, | ||
| "configuration": {}, | ||
| }) | ||
|
|
||
| assert theme.type == theme_type | ||
| assert not isinstance(theme.configuration, dict) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| node_modules/ |
14 changes: 14 additions & 0 deletions
14
packages/modules/display_themes/url_display/source/index.html
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,14 @@ | ||
| <!doctype html> | ||
| <html lang="de"> | ||
| <head> | ||
| <meta charset="UTF-8" /> | ||
| <meta name="viewport" content="width=device-width, initial-scale=1.0" /> | ||
| <title>openWB Display - URL</title> | ||
| </head> | ||
| <body> | ||
| <div id="status"> | ||
| <p id="message">Verbinde mit MQTT...</p> | ||
| </div> | ||
| <script type="module" src="/src/main.js"></script> | ||
| </body> | ||
| </html> |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.