-
This commit is contained in:
parent
eb1ae161ff
commit
f664f325af
@ -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
|
||||||
|
|
||||||
|
|||||||
@ -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>` : ''
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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
42
run.sh
@ -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"
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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"
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user