From 3abb6a5052caeb30dce794e018cc8e1b87c49857 Mon Sep 17 00:00:00 2001 From: Markus Kaindl Date: Fri, 6 Nov 2026 01:46:18 +0100 Subject: [PATCH 1/4] Add secondary display URL override Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../public/default-dynamic-security.json | 18 ++++++++++++++++++ packages/control/optional_data.py | 1 + packages/helpermodules/setdata.py | 5 ++++- packages/helpermodules/update_config.py | 2 ++ web/display/processAllMqttMsg.js | 10 ++++++++-- web/display/setupMqttServices.js | 1 + 6 files changed, 34 insertions(+), 3 deletions(-) diff --git a/data/config/mosquitto/public/default-dynamic-security.json b/data/config/mosquitto/public/default-dynamic-security.json index b6d2bac6ec..36c798d93e 100644 --- a/data/config/mosquitto/public/default-dynamic-security.json +++ b/data/config/mosquitto/public/default-dynamic-security.json @@ -398,6 +398,12 @@ "priority": 0, "allow": true }, + { + "acltype": "publishClientReceive", + "topic": "openWB/optional/int_display/display_url", + "priority": 0, + "allow": true + }, { "acltype": "publishClientReceive", "topic": "openWB/optional/int_display/theme", @@ -1252,6 +1258,12 @@ "priority": 0, "allow": true }, + { + "acltype": "publishClientSend", + "topic": "openWB/set/optional/int_display/display_url", + "priority": 0, + "allow": true + }, { "acltype": "publishClientSend", "topic": "openWB/set/optional/int_display/rotation", @@ -1294,6 +1306,12 @@ "priority": 0, "allow": true }, + { + "acltype": "publishClientReceive", + "topic": "openWB/optional/int_display/display_url", + "priority": 0, + "allow": true + }, { "acltype": "publishClientReceive", "topic": "openWB/optional/int_display/rotation", diff --git a/packages/control/optional_data.py b/packages/control/optional_data.py index 66260f6804..5bc061d4e5 100644 --- a/packages/control/optional_data.py +++ b/packages/control/optional_data.py @@ -137,6 +137,7 @@ class InternalDisplay: detected: bool = field(default=False, metadata={"topic": "int_display/detected"}) on_if_plugged_in: bool = field(default=True, metadata={"topic": "int_display/on_if_plugged_in"}) only_local_charge_points: bool = field(default=False, metadata={"topic": "int_display/only_local_charge_points"}) + display_url: str = field(default="", metadata={"topic": "int_display/display_url"}) pin_active: bool = field(default=False, metadata={"topic": "int_display/pin_active"}) pin_code: str = field(default="0000", metadata={"topic": "int_display/pin_code"}) rotation: int = field(default=0, metadata={"topic": "int_display/rotation"}) diff --git a/packages/helpermodules/setdata.py b/packages/helpermodules/setdata.py index 256e34d37c..325c94d4c7 100644 --- a/packages/helpermodules/setdata.py +++ b/packages/helpermodules/setdata.py @@ -63,7 +63,8 @@ def on_message(self, client: mqtt.Client, userdata, msg: mqtt.MQTTMessage): enthält Topic und Payload """ self.heartbeat = True - if decode_payload(msg.payload) != "": + if (decode_payload(msg.payload) != "" or + msg.topic == "openWB/set/optional/int_display/display_url"): mqtt_log.debug(f"Topic: {msg.topic}, Payload: {decode_payload(msg.payload)}") if "openWB/set/vehicle/" in msg.topic: if "openWB/set/vehicle/template/ev_template/" in msg.topic: @@ -956,6 +957,8 @@ def process_optional_topic(self, msg: mqtt.MQTTMessage): self._validate_value(msg, bool) elif "openWB/set/optional/int_display/pin_code" in msg.topic: self._validate_value(msg, str) + elif "openWB/set/optional/int_display/display_url" in msg.topic: + self._validate_value(msg, str) elif "openWB/set/optional/int_display/standby" in msg.topic: self._validate_value(msg, int, [(0, 600)]) elif "openWB/set/optional/int_display/theme" in msg.topic: diff --git a/packages/helpermodules/update_config.py b/packages/helpermodules/update_config.py index aa2e69aa40..c7c06c67d2 100644 --- a/packages/helpermodules/update_config.py +++ b/packages/helpermodules/update_config.py @@ -446,6 +446,7 @@ class UpdateConfig: "^openWB/optional/int_display/rotation$", "^openWB/optional/int_display/theme$", "^openWB/optional/int_display/only_local_charge_points$", + "^openWB/optional/int_display/display_url$", "^openWB/optional/monitoring/config$", "^openWB/optional/rfid/active$", "^openWB/optional/ocpp/config$", @@ -752,6 +753,7 @@ class UpdateConfig: ("openWB/optional/int_display/rotation", 0), ("openWB/optional/int_display/theme", asdict(CardsDisplayTheme())), ("openWB/optional/int_display/only_local_charge_points", False), + ("openWB/optional/int_display/display_url", ""), ("openWB/optional/monitoring/config", NO_MODULE), ("openWB/optional/ocpp/config", asdict(OcppConfig())), ("openWB/optional/rfid/active", False), diff --git a/web/display/processAllMqttMsg.js b/web/display/processAllMqttMsg.js index 0a4fca7395..18f64a3070 100644 --- a/web/display/processAllMqttMsg.js +++ b/web/display/processAllMqttMsg.js @@ -47,8 +47,14 @@ function setIframeSource() { break; } // load display from primary or local - destination = `${location.protocol}//${host}/openWB/web/display/?${query.toString()}`; - addLog(`all done, loading theme from primary`); + const configuredDisplayUrl = data["openWB/optional/int_display/display_url"]; + if (configuredDisplayUrl && configuredDisplayUrl.trim()) { + destination = configuredDisplayUrl.trim(); + addLog(`all done, loading manually configured display URL`); + } else { + destination = `${location.protocol}//${host}/openWB/web/display/?${query.toString()}`; + addLog(`all done, loading theme from primary`); + } // no iframe here as this would result in another nesting with the wrapper on primary setTimeout(() => { location.href = destination; diff --git a/web/display/setupMqttServices.js b/web/display/setupMqttServices.js index 8021930cd7..fb254e8bc0 100644 --- a/web/display/setupMqttServices.js +++ b/web/display/setupMqttServices.js @@ -20,6 +20,7 @@ var secondaryTopicsToSubscribe = { "openWB/system/current_branch": false, "openWB/system/current_commit": false, "openWB/general/extern_display_mode": false, + "openWB/optional/int_display/display_url": false, "openWB/internal_chargepoint/global_data": false, "openWB/internal_chargepoint/0/data/parent_cp": false, "openWB/internal_chargepoint/1/data/parent_cp": false, From fd028112f2d70c6d250bb4dff27d40485246f1a4 Mon Sep 17 00:00:00 2001 From: Markus Kaindl Date: Sat, 7 Nov 2026 14:34:17 +0100 Subject: [PATCH 2/4] Fix saving secondary display URL Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- packages/control/optional_data.py | 2 +- packages/helpermodules/setdata.py | 3 +-- packages/helpermodules/update_config.py | 2 +- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/packages/control/optional_data.py b/packages/control/optional_data.py index 5bc061d4e5..caa67b2a3b 100644 --- a/packages/control/optional_data.py +++ b/packages/control/optional_data.py @@ -137,7 +137,7 @@ class InternalDisplay: detected: bool = field(default=False, metadata={"topic": "int_display/detected"}) on_if_plugged_in: bool = field(default=True, metadata={"topic": "int_display/on_if_plugged_in"}) only_local_charge_points: bool = field(default=False, metadata={"topic": "int_display/only_local_charge_points"}) - display_url: str = field(default="", metadata={"topic": "int_display/display_url"}) + display_url: Optional[str] = field(default=None, metadata={"topic": "int_display/display_url"}) pin_active: bool = field(default=False, metadata={"topic": "int_display/pin_active"}) pin_code: str = field(default="0000", metadata={"topic": "int_display/pin_code"}) rotation: int = field(default=0, metadata={"topic": "int_display/rotation"}) diff --git a/packages/helpermodules/setdata.py b/packages/helpermodules/setdata.py index 325c94d4c7..e6815f170e 100644 --- a/packages/helpermodules/setdata.py +++ b/packages/helpermodules/setdata.py @@ -63,8 +63,7 @@ def on_message(self, client: mqtt.Client, userdata, msg: mqtt.MQTTMessage): enthält Topic und Payload """ self.heartbeat = True - if (decode_payload(msg.payload) != "" or - msg.topic == "openWB/set/optional/int_display/display_url"): + if decode_payload(msg.payload) != "": mqtt_log.debug(f"Topic: {msg.topic}, Payload: {decode_payload(msg.payload)}") if "openWB/set/vehicle/" in msg.topic: if "openWB/set/vehicle/template/ev_template/" in msg.topic: diff --git a/packages/helpermodules/update_config.py b/packages/helpermodules/update_config.py index c7c06c67d2..3e320bb638 100644 --- a/packages/helpermodules/update_config.py +++ b/packages/helpermodules/update_config.py @@ -753,7 +753,7 @@ class UpdateConfig: ("openWB/optional/int_display/rotation", 0), ("openWB/optional/int_display/theme", asdict(CardsDisplayTheme())), ("openWB/optional/int_display/only_local_charge_points", False), - ("openWB/optional/int_display/display_url", ""), + ("openWB/optional/int_display/display_url", None), ("openWB/optional/monitoring/config", NO_MODULE), ("openWB/optional/ocpp/config", asdict(OcppConfig())), ("openWB/optional/rfid/active", False), From bdc33ed97d2dbb6f5c6f95e281b87e4304e78b69 Mon Sep 17 00:00:00 2001 From: Markus Kaindl Date: Sat, 7 Nov 2026 17:43:39 +0100 Subject: [PATCH 3/4] Cancel stale secondary display navigation Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- web/display/processAllMqttMsg.js | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/web/display/processAllMqttMsg.js b/web/display/processAllMqttMsg.js index 18f64a3070..2ce77ede2e 100644 --- a/web/display/processAllMqttMsg.js +++ b/web/display/processAllMqttMsg.js @@ -1,4 +1,21 @@ var credentialsFetched = false; +var pendingDisplayNavigationTimeout = null; +var pendingDisplayNavigationDestination = null; + +function scheduleDisplayNavigation(destination) { + if (destination === pendingDisplayNavigationDestination) { + return; + } + if (pendingDisplayNavigationTimeout !== null) { + clearTimeout(pendingDisplayNavigationTimeout); + } + pendingDisplayNavigationDestination = destination; + pendingDisplayNavigationTimeout = setTimeout(() => { + pendingDisplayNavigationTimeout = null; + pendingDisplayNavigationDestination = null; + location.href = destination; + }, 2000); +} function setIframeSource() { if (allTopicsReceived()) { @@ -56,9 +73,7 @@ function setIframeSource() { addLog(`all done, loading theme from primary`); } // no iframe here as this would result in another nesting with the wrapper on primary - setTimeout(() => { - location.href = destination; - }, 2000); + scheduleDisplayNavigation(destination); } else { // load primary display (from primary or secondary openWB) host = location.host; From 0c6d96bdb58ff5513e9afb437a037bdccb4b03c2 Mon Sep 17 00:00:00 2001 From: Markus Kaindl Date: Sat, 7 Nov 2026 17:52:07 +0100 Subject: [PATCH 4/4] Harden display URL handling Render display logs as text and reject display URLs that do not use HTTP or HTTPS.\n\nCo-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- packages/helpermodules/setdata.py | 18 +++++++++++++++++- web/display/processAllMqttMsg.js | 4 ++-- 2 files changed, 19 insertions(+), 3 deletions(-) diff --git a/packages/helpermodules/setdata.py b/packages/helpermodules/setdata.py index e6815f170e..a094e328ad 100644 --- a/packages/helpermodules/setdata.py +++ b/packages/helpermodules/setdata.py @@ -6,6 +6,7 @@ from threading import Event from typing import List, Optional, Tuple import re +from urllib.parse import urlsplit import paho.mqtt.client as mqtt import logging @@ -23,6 +24,16 @@ TIMESTAMP_2100 = 4102441200 # 01.01.2100 00:00:00 +def _is_valid_display_url(value: str) -> bool: + if not value: + return True + try: + parsed_url = urlsplit(value) + except ValueError: + return False + return parsed_url.scheme.lower() in ("http", "https") and bool(parsed_url.hostname) + + class SetData: def __init__(self, event_ev_template: Event, @@ -957,7 +968,12 @@ def process_optional_topic(self, msg: mqtt.MQTTMessage): elif "openWB/set/optional/int_display/pin_code" in msg.topic: self._validate_value(msg, str) elif "openWB/set/optional/int_display/display_url" in msg.topic: - self._validate_value(msg, str) + display_url = decode_payload(msg.payload) + if isinstance(display_url, str) and not _is_valid_display_url(display_url): + log.error("Payload ungültig: Topic %s, display URL must use HTTP or HTTPS.", msg.topic) + Pub().pub(msg.topic, "") + else: + self._validate_value(msg, str) elif "openWB/set/optional/int_display/standby" in msg.topic: self._validate_value(msg, int, [(0, 600)]) elif "openWB/set/optional/int_display/theme" in msg.topic: diff --git a/web/display/processAllMqttMsg.js b/web/display/processAllMqttMsg.js index 2ce77ede2e..e59f5820bf 100644 --- a/web/display/processAllMqttMsg.js +++ b/web/display/processAllMqttMsg.js @@ -124,12 +124,12 @@ function setIframeSource() { function addLog(message, forceDisplay = false) { const logElement = document.getElementById('log'); - let displayedMessages = logElement.innerHTML.split("\n"); + let displayedMessages = logElement.textContent.split("\n"); if (displayedMessages.length > 25) { displayedMessages.shift(); } displayedMessages.push(message); - logElement.innerHTML = displayedMessages.join("\n"); + logElement.textContent = displayedMessages.join("\n"); if (forceDisplay) { logElement.classList.remove("hide"); }