This commit is contained in:
Striker72rus 2026-03-25 15:57:53 +03:00
parent eb1ae161ff
commit f664f325af
7 changed files with 177 additions and 91 deletions

View File

@ -7,7 +7,7 @@ from homeassistant.core import HomeAssistant
from .const import CONF_CONFIG, DOMAIN from .const import CONF_CONFIG, DOMAIN
from .frontend import async_setup_frontend from .frontend import async_setup_frontend
from .helpers import current_entry_config from .helpers import current_entry_config
from .views import WallPanelConfigView, WallPanelPanelView from .views import WallPanelConfigView, WallPanelPanelView, WallPanelProxyView
async def async_setup_entry(hass: HomeAssistant, entry) -> bool: async def async_setup_entry(hass: HomeAssistant, entry) -> bool:
@ -30,6 +30,10 @@ async def async_setup_entry(hass: HomeAssistant, entry) -> bool:
hass.http.register_view(WallPanelPanelView) hass.http.register_view(WallPanelPanelView)
state["_panel_view_registered"] = True state["_panel_view_registered"] = True
if not state.get("_proxy_view_registered"):
hass.http.register_view(WallPanelProxyView)
state["_proxy_view_registered"] = True
entry.async_on_unload(entry.add_update_listener(_async_options_updated)) entry.async_on_unload(entry.add_update_listener(_async_options_updated))
return True return True

View File

@ -74,41 +74,20 @@ class WallPanelPanel extends HTMLElement {
return `/api/wall_panel/config/${encodeURIComponent(entryId)}`; return `/api/wall_panel/config/${encodeURIComponent(entryId)}`;
} }
async _fetchPanelUrl() { _proxyUrl() {
const payload = this._panelConfig(); const payload = this._panelConfig();
const syncToken = String(payload.sync_token || '').trim(); const entryId = String(payload.entry_id || '').trim();
if (!syncToken) { if (!entryId) {
return ''; return '';
} }
const configUrls = [this._configUrl(), this._legacyConfigUrl()].filter(Boolean); return `/api/wall_panel/proxy/${encodeURIComponent(entryId)}/`;
for (const configUrl of configUrls) { }
try {
const response = await fetch(configUrl, {
method: 'GET',
headers: {
'X-Wall-Panel-Token': syncToken,
Accept: 'application/json',
},
credentials: 'same-origin',
cache: 'no-store',
});
if (!response.ok) { _hasPanelUrl() {
continue; const payload = this._panelConfig();
} const panelUrl = String(payload.panel_url || payload.ingress_url || '').trim();
return panelUrl !== '';
const data = await response.json();
const panelUrl = String(data?.panel?.panel_url || data?.panel_url || '').trim();
if (panelUrl) {
return panelUrl;
}
} catch (error) {
continue;
}
}
return '';
} }
_renderMessage(title, body, extra = '') { _renderMessage(title, body, extra = '') {
@ -197,33 +176,23 @@ class WallPanelPanel extends HTMLElement {
async _tryAttachPanel() { async _tryAttachPanel() {
const payload = this._panelConfig(); const payload = this._panelConfig();
const initialUrl = this._resolveUrl( const proxyUrl = this._proxyUrl();
payload.panel_url || payload.ingress_url || '' if (proxyUrl && proxyUrl === this._activePanelUrl) {
);
if (initialUrl && initialUrl === this._activePanelUrl) {
return; return;
} }
if (initialUrl && !(window.location.protocol === 'https:' && initialUrl.startsWith('http://'))) { if (proxyUrl && this._hasPanelUrl()) {
this._renderIframe(initialUrl); this._renderIframe(proxyUrl);
return;
}
const panelUrl = this._resolveUrl(await this._fetchPanelUrl());
if (panelUrl && panelUrl === this._activePanelUrl) {
return;
}
if (panelUrl && !(window.location.protocol === 'https:' && panelUrl.startsWith('http://'))) {
this._renderIframe(panelUrl);
return; return;
} }
const panelUrl = this._resolveUrl(payload.panel_url || payload.ingress_url || '');
const configUrl = this._configUrl(); const configUrl = this._configUrl();
this._renderMessage( this._renderMessage(
'Waiting for Wall Panel', 'Waiting for Wall Panel',
'The add-on will publish a secure HTTPS ingress URL here.', panelUrl
? 'Open this panel through Home Assistant after the add-on URL is configured.'
: 'Set the PHP panel URL in the integration options.',
configUrl ? `<code>${configUrl}</code>` : '' configUrl ? `<code>${configUrl}</code>` : ''
); );
} }

View File

@ -3,11 +3,14 @@
from __future__ import annotations from __future__ import annotations
import secrets import secrets
from urllib.parse import urljoin
from typing import Any from typing import Any
from aiohttp import web from aiohttp import web
from homeassistant.components.http import HomeAssistantView from homeassistant.components.http import HomeAssistantView
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from yarl import URL
from .const import CONF_CONFIG, CONF_PANEL_URL, CONF_SYNC_TOKEN, DOMAIN from .const import CONF_CONFIG, CONF_PANEL_URL, CONF_SYNC_TOKEN, DOMAIN
from .helpers import ( from .helpers import (
@ -88,6 +91,34 @@ def _save_entry_panel_url(hass: HomeAssistant, entry, panel_url: str) -> None:
hass.data.setdefault(DOMAIN, {}).setdefault(entry.entry_id, {})["panel_url"] = panel_url hass.data.setdefault(DOMAIN, {}).setdefault(entry.entry_id, {})["panel_url"] = panel_url
def _proxy_root(entry_id: str) -> str:
return f"/api/wall_panel/proxy/{entry_id}/"
def _forward_headers(request: web.Request) -> dict[str, str]:
headers: dict[str, str] = {}
for key in ("Accept", "Content-Type", "Range", "If-Modified-Since", "If-None-Match", "Origin", "Referer", "User-Agent"):
value = request.headers.get(key)
if value:
headers[key] = value
return headers
def _rewrite_location(location: str, base_url: str, proxy_root: str) -> str:
location = location.strip()
if not location:
return location
if location.startswith(base_url):
suffix = location[len(base_url):].lstrip("/")
return proxy_root + suffix
if location.startswith("/"):
return proxy_root + location.lstrip("/")
return location
class WallPanelConfigView(HomeAssistantView): class WallPanelConfigView(HomeAssistantView):
"""Serve and update the canonical Wall Panel config.""" """Serve and update the canonical Wall Panel config."""
@ -248,3 +279,81 @@ class WallPanelPanelView(HomeAssistantView):
"ok": True, "ok": True,
"panel": current_entry_panel(entry), "panel": current_entry_panel(entry),
}) })
class WallPanelProxyView(HomeAssistantView):
"""Reverse proxy the PHP panel through Home Assistant."""
url = "/api/wall_panel/proxy/{entry_id}/{path:.*}"
name = "api:wall_panel:proxy"
requires_auth = True
async def _proxy(self, request: web.Request, entry_id: str, path: str, method: str) -> web.StreamResponse:
hass = request.app["hass"]
entry = _entry_from_hass(hass, entry_id)
if entry is None:
return _response({"ok": False, "error": "Unknown entry"}, 404)
base_url = str(entry.options.get(CONF_PANEL_URL, "") or "").strip()
if not base_url:
return _response({"ok": False, "error": "PHP panel URL is not configured"}, 400)
if not base_url.endswith("/"):
base_url += "/"
upstream_url = urljoin(base_url, path or "")
if request.query_string:
upstream_url = upstream_url + "?" + request.query_string
body = None
if method not in {"GET", "HEAD"}:
body = await request.read()
session = async_get_clientsession(hass)
async with session.request(
method,
upstream_url,
headers=_forward_headers(request),
data=body,
allow_redirects=False,
) as upstream:
response_headers: dict[str, str] = {}
for key, value in upstream.headers.items():
lower = key.lower()
if lower in {
"connection",
"keep-alive",
"proxy-authenticate",
"proxy-authorization",
"te",
"trailers",
"transfer-encoding",
"upgrade",
"content-encoding",
}:
continue
if lower == "location":
response_headers[key] = _rewrite_location(value, base_url, _proxy_root(entry_id))
continue
response_headers[key] = value
response_body = await upstream.read()
return web.Response(
body=response_body,
status=upstream.status,
headers=response_headers,
)
async def async_get(self, request: web.Request, entry_id: str, path: str = "") -> web.StreamResponse:
return await self._proxy(request, entry_id, path, "GET")
async def async_post(self, request: web.Request, entry_id: str, path: str = "") -> web.StreamResponse:
return await self._proxy(request, entry_id, path, "POST")
async def async_put(self, request: web.Request, entry_id: str, path: str = "") -> web.StreamResponse:
return await self._proxy(request, entry_id, path, "PUT")
async def async_patch(self, request: web.Request, entry_id: str, path: str = "") -> web.StreamResponse:
return await self._proxy(request, entry_id, path, "PATCH")
async def async_delete(self, request: web.Request, entry_id: str, path: str = "") -> web.StreamResponse:
return await self._proxy(request, entry_id, path, "DELETE")

42
run.sh
View File

@ -26,26 +26,28 @@ log "config path: ${CONFIG_PATH}"
log "storage dir: ${STORAGE_DIR}" log "storage dir: ${STORAGE_DIR}"
if [ "$RUNTIME_MODE" = "addon" ]; then if [ "$RUNTIME_MODE" = "addon" ]; then
( if [ "${WALL_PANEL_ENABLE_INGRESS_REGISTRATION:-false}" = "true" ]; then
i=0 (
while [ "$i" -lt 120 ]; do i=0
log "registering ingress attempt $((i + 1))/120" while [ "$i" -lt 120 ]; do
output="$(php -r 'require "/app/lib/bootstrap.php"; $config = app_load_config(); exit(app_register_ingress_url($config) ? 0 : 1);' 2>&1)" log "registering ingress attempt $((i + 1))/120"
status=$? output="$(php -r 'require "/app/lib/bootstrap.php"; $config = app_load_config(); exit(app_register_ingress_url($config) ? 0 : 1);' 2>&1)"
if [ "$status" -eq 0 ]; then status=$?
log "ingress registered" if [ "$status" -eq 0 ]; then
exit 0 log "ingress registered"
fi exit 0
if [ -n "$output" ]; then fi
printf '%s\n' "$output" | while IFS= read -r line; do if [ -n "$output" ]; then
[ -n "$line" ] && log "$line" printf '%s\n' "$output" | while IFS= read -r line; do
done [ -n "$line" ] && log "$line"
fi done
i=$((i + 1)) fi
sleep 2 i=$((i + 1))
done sleep 2
log "ingress registration failed after retries" done
) || true & log "ingress registration failed after retries"
) || true &
fi
fi fi
exec php -S "0.0.0.0:${PORT}" -t "$DOCROOT" exec php -S "0.0.0.0:${PORT}" -t "$DOCROOT"

View File

@ -1,5 +1,5 @@
{ {
"active": true, "active": false,
"sensor_entity_id": "binary_sensor.doorbell_all_occupancy", "sensor_entity_id": "binary_sensor.doorbell_all_occupancy",
"opened_at": 1774443242, "opened_at": 1774443242,
"expires_at": null "expires_at": null

View File

@ -1,6 +1,6 @@
name: Wall Panel name: Wall Panel
description: Wall Panel PHP interface as a Home Assistant add-on description: Wall Panel PHP interface as a Home Assistant add-on
version: "1.0.20" version: "1.0.22"
slug: wall_panel slug: wall_panel
url: https://git.striker72rus.ru/PHP/wallpanell.git url: https://git.striker72rus.ru/PHP/wallpanell.git
init: false init: false

View File

@ -24,25 +24,27 @@ log "starting add-on on port ${PORT}"
log "config path: ${CONFIG_PATH}" log "config path: ${CONFIG_PATH}"
log "storage dir: ${STORAGE_DIR}" log "storage dir: ${STORAGE_DIR}"
( if [ "${WALL_PANEL_ENABLE_INGRESS_REGISTRATION:-false}" = "true" ]; then
i=0 (
while [ "$i" -lt 120 ]; do i=0
log "registering ingress attempt $((i + 1))/120" while [ "$i" -lt 120 ]; do
output="$(php -r 'require "/app/lib/bootstrap.php"; $config = app_load_config(); exit(app_register_ingress_url($config) ? 0 : 1);' 2>&1)" log "registering ingress attempt $((i + 1))/120"
status=$? output="$(php -r 'require "/app/lib/bootstrap.php"; $config = app_load_config(); exit(app_register_ingress_url($config) ? 0 : 1);' 2>&1)"
if [ "$status" -eq 0 ]; then status=$?
log "ingress registered" if [ "$status" -eq 0 ]; then
exit 0 log "ingress registered"
fi exit 0
if [ -n "$output" ]; then fi
printf '%s\n' "$output" | while IFS= read -r line; do if [ -n "$output" ]; then
[ -n "$line" ] && log "$line" printf '%s\n' "$output" | while IFS= read -r line; do
done [ -n "$line" ] && log "$line"
fi done
i=$((i + 1)) fi
sleep 2 i=$((i + 1))
done sleep 2
log "ingress registration failed after retries" done
) || true & log "ingress registration failed after retries"
) || true &
fi
exec php -S "0.0.0.0:${PORT}" -t "$DOCROOT" exec php -S "0.0.0.0:${PORT}" -t "$DOCROOT"