-
This commit is contained in:
parent
f664f325af
commit
cedb9ed432
@ -2,12 +2,33 @@
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import logging
|
||||||
|
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
|
|
||||||
from .const import CONF_CONFIG, DOMAIN
|
from .const import 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, WallPanelProxyView
|
from .views import handle_config_request, handle_panel_request, handle_proxy_request
|
||||||
|
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
async def async_setup(hass: HomeAssistant, config: dict) -> bool:
|
||||||
|
"""Set up the integration-level HTTP views."""
|
||||||
|
|
||||||
|
state = hass.data.setdefault(DOMAIN, {})
|
||||||
|
if not state.get("_direct_routes_registered"):
|
||||||
|
app = hass.http.app
|
||||||
|
app.router.add_route("*", "/api/wall_panel/config/{entry_id}", handle_config_request)
|
||||||
|
app.router.add_route("*", "/api/wall_panel/panel", handle_panel_request)
|
||||||
|
app.router.add_route("*", "/api/wall_panel/proxy/{entry_id}/{path:.*}", handle_proxy_request)
|
||||||
|
state["_direct_routes_registered"] = True
|
||||||
|
_LOGGER.warning("Wall Panel direct routes registered")
|
||||||
|
|
||||||
|
_LOGGER.warning("Wall Panel integration views registered")
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
async def async_setup_entry(hass: HomeAssistant, entry) -> bool:
|
async def async_setup_entry(hass: HomeAssistant, entry) -> bool:
|
||||||
@ -22,18 +43,6 @@ async def async_setup_entry(hass: HomeAssistant, entry) -> bool:
|
|||||||
panel_url_path = await async_setup_frontend(hass, entry)
|
panel_url_path = await async_setup_frontend(hass, entry)
|
||||||
state[entry.entry_id]["panel_url_path"] = panel_url_path
|
state[entry.entry_id]["panel_url_path"] = panel_url_path
|
||||||
|
|
||||||
if not state.get("_config_view_registered"):
|
|
||||||
hass.http.register_view(WallPanelConfigView)
|
|
||||||
state["_config_view_registered"] = True
|
|
||||||
|
|
||||||
if not state.get("_panel_view_registered"):
|
|
||||||
hass.http.register_view(WallPanelPanelView)
|
|
||||||
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
|
||||||
|
|
||||||
|
|||||||
@ -170,8 +170,10 @@ class WallPanelPanel extends HTMLElement {
|
|||||||
iframe.style.border = '0';
|
iframe.style.border = '0';
|
||||||
iframe.style.display = 'block';
|
iframe.style.display = 'block';
|
||||||
iframe.style.background = 'transparent';
|
iframe.style.background = 'transparent';
|
||||||
wrap.replaceChildren(iframe);
|
iframe.addEventListener('load', () => {
|
||||||
this._activePanelUrl = panelUrl;
|
this._activePanelUrl = panelUrl;
|
||||||
|
});
|
||||||
|
wrap.replaceChildren(iframe);
|
||||||
}
|
}
|
||||||
|
|
||||||
async _tryAttachPanel() {
|
async _tryAttachPanel() {
|
||||||
|
|||||||
@ -3,6 +3,7 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import secrets
|
import secrets
|
||||||
|
import logging
|
||||||
from urllib.parse import urljoin
|
from urllib.parse import urljoin
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
@ -29,6 +30,9 @@ from .helpers import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
def _entry_from_hass(hass: HomeAssistant, entry_id: str):
|
def _entry_from_hass(hass: HomeAssistant, entry_id: str):
|
||||||
return hass.data.get(DOMAIN, {}).get(entry_id, {}).get("entry")
|
return hass.data.get(DOMAIN, {}).get(entry_id, {}).get("entry")
|
||||||
|
|
||||||
@ -77,56 +81,9 @@ def _response(data: Any, status: int = 200) -> web.Response:
|
|||||||
return web.json_response(data, status=status)
|
return web.json_response(data, status=status)
|
||||||
|
|
||||||
|
|
||||||
def _save_entry_config(hass: HomeAssistant, entry, config: dict[str, Any]) -> None:
|
async def _handle_config_request(request: web.Request, entry_id: str) -> web.Response:
|
||||||
options = dict(entry.options)
|
"""Handle canonical config requests for a specific config entry."""
|
||||||
options[CONF_CONFIG] = config
|
|
||||||
hass.config_entries.async_update_entry(entry, options=options)
|
|
||||||
hass.data.setdefault(DOMAIN, {}).setdefault(entry.entry_id, {})["config"] = config
|
|
||||||
|
|
||||||
|
|
||||||
def _save_entry_panel_url(hass: HomeAssistant, entry, panel_url: str) -> None:
|
|
||||||
options = dict(entry.options)
|
|
||||||
options[CONF_PANEL_URL] = panel_url
|
|
||||||
hass.config_entries.async_update_entry(entry, options=options)
|
|
||||||
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):
|
|
||||||
"""Serve and update the canonical Wall Panel config."""
|
|
||||||
|
|
||||||
url = "/api/wall_panel/config/{entry_id}"
|
|
||||||
name = "api:wall_panel:config"
|
|
||||||
requires_auth = False
|
|
||||||
|
|
||||||
async def async_get(self, request: web.Request, entry_id: str) -> web.Response:
|
|
||||||
hass = request.app["hass"]
|
hass = request.app["hass"]
|
||||||
entry = _entry_from_hass(hass, entry_id)
|
entry = _entry_from_hass(hass, entry_id)
|
||||||
if entry is None:
|
if entry is None:
|
||||||
@ -134,6 +91,7 @@ class WallPanelConfigView(HomeAssistantView):
|
|||||||
if not _authorized(entry, request):
|
if not _authorized(entry, request):
|
||||||
return _response({"ok": False, "error": "Unauthorized"}, 401)
|
return _response({"ok": False, "error": "Unauthorized"}, 401)
|
||||||
|
|
||||||
|
if request.method == "GET":
|
||||||
config = current_entry_config(entry)
|
config = current_entry_config(entry)
|
||||||
return _response({
|
return _response({
|
||||||
"ok": True,
|
"ok": True,
|
||||||
@ -141,14 +99,6 @@ class WallPanelConfigView(HomeAssistantView):
|
|||||||
"panel": current_entry_panel(entry),
|
"panel": current_entry_panel(entry),
|
||||||
})
|
})
|
||||||
|
|
||||||
async def async_post(self, request: web.Request, entry_id: str) -> web.Response:
|
|
||||||
hass = request.app["hass"]
|
|
||||||
entry = _entry_from_hass(hass, entry_id)
|
|
||||||
if entry is None:
|
|
||||||
return _response({"ok": False, "error": "Unknown entry"}, 404)
|
|
||||||
if not _authorized(entry, request):
|
|
||||||
return _response({"ok": False, "error": "Unauthorized"}, 401)
|
|
||||||
|
|
||||||
payload = await request.json()
|
payload = await request.json()
|
||||||
if not isinstance(payload, dict):
|
if not isinstance(payload, dict):
|
||||||
return _response({"ok": False, "error": "Invalid payload"}, 400)
|
return _response({"ok": False, "error": "Invalid payload"}, 400)
|
||||||
@ -230,14 +180,16 @@ class WallPanelConfigView(HomeAssistantView):
|
|||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
class WallPanelPanelView(HomeAssistantView):
|
async def handle_config_request(request: web.Request) -> web.Response:
|
||||||
"""Serve the active Wall Panel config without an entry id."""
|
"""Handle canonical config requests for a specific config entry."""
|
||||||
|
|
||||||
url = "/api/wall_panel/panel"
|
entry_id = str(request.match_info.get("entry_id", "") or "").strip()
|
||||||
name = "api:wall_panel:panel"
|
return await _handle_config_request(request, entry_id)
|
||||||
requires_auth = False
|
|
||||||
|
|
||||||
|
async def handle_panel_request(request: web.Request) -> web.Response:
|
||||||
|
"""Handle the panel config endpoint without requiring an entry id."""
|
||||||
|
|
||||||
async def async_get(self, request: web.Request) -> web.Response:
|
|
||||||
hass = request.app["hass"]
|
hass = request.app["hass"]
|
||||||
entry = _default_entry_from_hass(hass)
|
entry = _default_entry_from_hass(hass)
|
||||||
if entry is None:
|
if entry is None:
|
||||||
@ -245,6 +197,7 @@ class WallPanelPanelView(HomeAssistantView):
|
|||||||
if not _authorized(entry, request):
|
if not _authorized(entry, request):
|
||||||
return _response({"ok": False, "error": "Unauthorized"}, 401)
|
return _response({"ok": False, "error": "Unauthorized"}, 401)
|
||||||
|
|
||||||
|
if request.method == "GET":
|
||||||
config = current_entry_config(entry)
|
config = current_entry_config(entry)
|
||||||
return _response({
|
return _response({
|
||||||
"ok": True,
|
"ok": True,
|
||||||
@ -252,12 +205,6 @@ class WallPanelPanelView(HomeAssistantView):
|
|||||||
"panel": current_entry_panel(entry),
|
"panel": current_entry_panel(entry),
|
||||||
})
|
})
|
||||||
|
|
||||||
async def async_post(self, request: web.Request) -> web.Response:
|
|
||||||
hass = request.app["hass"]
|
|
||||||
entry = _default_entry_from_hass(hass)
|
|
||||||
if entry is None:
|
|
||||||
return _response({"ok": False, "error": "Unknown entry"}, 404)
|
|
||||||
|
|
||||||
payload = await request.json()
|
payload = await request.json()
|
||||||
if not isinstance(payload, dict):
|
if not isinstance(payload, dict):
|
||||||
return _response({"ok": False, "error": "Invalid payload"}, 400)
|
return _response({"ok": False, "error": "Invalid payload"}, 400)
|
||||||
@ -281,14 +228,9 @@ class WallPanelPanelView(HomeAssistantView):
|
|||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
class WallPanelProxyView(HomeAssistantView):
|
async def _handle_proxy_request(request: web.Request, entry_id: str, path: str, method: str) -> web.StreamResponse:
|
||||||
"""Reverse proxy the PHP panel through Home Assistant."""
|
"""Handle reverse-proxy requests to the PHP panel."""
|
||||||
|
|
||||||
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"]
|
hass = request.app["hass"]
|
||||||
entry = _entry_from_hass(hass, entry_id)
|
entry = _entry_from_hass(hass, entry_id)
|
||||||
if entry is None:
|
if entry is None:
|
||||||
@ -296,6 +238,7 @@ class WallPanelProxyView(HomeAssistantView):
|
|||||||
|
|
||||||
base_url = str(entry.options.get(CONF_PANEL_URL, "") or "").strip()
|
base_url = str(entry.options.get(CONF_PANEL_URL, "") or "").strip()
|
||||||
if not base_url:
|
if not base_url:
|
||||||
|
_LOGGER.warning("Wall Panel proxy denied for %s: panel_url is empty", entry_id)
|
||||||
return _response({"ok": False, "error": "PHP panel URL is not configured"}, 400)
|
return _response({"ok": False, "error": "PHP panel URL is not configured"}, 400)
|
||||||
if not base_url.endswith("/"):
|
if not base_url.endswith("/"):
|
||||||
base_url += "/"
|
base_url += "/"
|
||||||
@ -304,6 +247,8 @@ class WallPanelProxyView(HomeAssistantView):
|
|||||||
if request.query_string:
|
if request.query_string:
|
||||||
upstream_url = upstream_url + "?" + request.query_string
|
upstream_url = upstream_url + "?" + request.query_string
|
||||||
|
|
||||||
|
_LOGGER.warning("Wall Panel proxy %s %s -> %s", method, request.path_qs, upstream_url)
|
||||||
|
|
||||||
body = None
|
body = None
|
||||||
if method not in {"GET", "HEAD"}:
|
if method not in {"GET", "HEAD"}:
|
||||||
body = await request.read()
|
body = await request.read()
|
||||||
@ -337,23 +282,112 @@ class WallPanelProxyView(HomeAssistantView):
|
|||||||
response_headers[key] = value
|
response_headers[key] = value
|
||||||
|
|
||||||
response_body = await upstream.read()
|
response_body = await upstream.read()
|
||||||
|
_LOGGER.warning("Wall Panel upstream %s %s", upstream.status, upstream_url)
|
||||||
return web.Response(
|
return web.Response(
|
||||||
body=response_body,
|
body=response_body,
|
||||||
status=upstream.status,
|
status=upstream.status,
|
||||||
headers=response_headers,
|
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:
|
async def handle_proxy_request(request: web.Request) -> web.StreamResponse:
|
||||||
return await self._proxy(request, entry_id, path, "POST")
|
"""Handle reverse-proxy requests to the PHP panel."""
|
||||||
|
|
||||||
async def async_put(self, request: web.Request, entry_id: str, path: str = "") -> web.StreamResponse:
|
entry_id = str(request.match_info.get("entry_id", "") or "").strip()
|
||||||
return await self._proxy(request, entry_id, path, "PUT")
|
path = str(request.match_info.get("path", "") or "").strip()
|
||||||
|
method = request.method.upper()
|
||||||
|
return await _handle_proxy_request(request, entry_id, path, method)
|
||||||
|
|
||||||
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:
|
def _save_entry_config(hass: HomeAssistant, entry, config: dict[str, Any]) -> None:
|
||||||
return await self._proxy(request, entry_id, path, "DELETE")
|
options = dict(entry.options)
|
||||||
|
options[CONF_CONFIG] = config
|
||||||
|
hass.config_entries.async_update_entry(entry, options=options)
|
||||||
|
hass.data.setdefault(DOMAIN, {}).setdefault(entry.entry_id, {})["config"] = config
|
||||||
|
|
||||||
|
|
||||||
|
def _save_entry_panel_url(hass: HomeAssistant, entry, panel_url: str) -> None:
|
||||||
|
options = dict(entry.options)
|
||||||
|
options[CONF_PANEL_URL] = panel_url
|
||||||
|
hass.config_entries.async_update_entry(entry, options=options)
|
||||||
|
hass.data.setdefault(DOMAIN, {}).setdefault(entry.entry_id, {})["panel_url"] = panel_url
|
||||||
|
_LOGGER.warning("Wall Panel panel_url updated for %s: %s", entry.entry_id, 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):
|
||||||
|
"""Serve and update the canonical Wall Panel config."""
|
||||||
|
|
||||||
|
url = "/api/wall_panel/config/{entry_id}"
|
||||||
|
name = "api:wall_panel:config"
|
||||||
|
requires_auth = False
|
||||||
|
|
||||||
|
async def get(self, request: web.Request, entry_id: str) -> web.Response:
|
||||||
|
return await _handle_config_request(request, entry_id)
|
||||||
|
|
||||||
|
async def post(self, request: web.Request, entry_id: str) -> web.Response:
|
||||||
|
return await _handle_config_request(request, entry_id)
|
||||||
|
|
||||||
|
|
||||||
|
class WallPanelPanelView(HomeAssistantView):
|
||||||
|
"""Serve the active Wall Panel config without an entry id."""
|
||||||
|
|
||||||
|
url = "/api/wall_panel/panel"
|
||||||
|
name = "api:wall_panel:panel"
|
||||||
|
requires_auth = False
|
||||||
|
|
||||||
|
async def get(self, request: web.Request) -> web.Response:
|
||||||
|
return await handle_panel_request(request)
|
||||||
|
|
||||||
|
async def post(self, request: web.Request) -> web.Response:
|
||||||
|
return await handle_panel_request(request)
|
||||||
|
|
||||||
|
|
||||||
|
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 get(self, request: web.Request, entry_id: str, path: str = "") -> web.StreamResponse:
|
||||||
|
return await _handle_proxy_request(request, entry_id, path, "GET")
|
||||||
|
|
||||||
|
async def post(self, request: web.Request, entry_id: str, path: str = "") -> web.StreamResponse:
|
||||||
|
return await _handle_proxy_request(request, entry_id, path, "POST")
|
||||||
|
|
||||||
|
async def put(self, request: web.Request, entry_id: str, path: str = "") -> web.StreamResponse:
|
||||||
|
return await _handle_proxy_request(request, entry_id, path, "PUT")
|
||||||
|
|
||||||
|
async def patch(self, request: web.Request, entry_id: str, path: str = "") -> web.StreamResponse:
|
||||||
|
return await _handle_proxy_request(request, entry_id, path, "PATCH")
|
||||||
|
|
||||||
|
async def delete(self, request: web.Request, entry_id: str, path: str = "") -> web.StreamResponse:
|
||||||
|
return await _handle_proxy_request(request, entry_id, path, "DELETE")
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"active": false,
|
"active": true,
|
||||||
"sensor_entity_id": "binary_sensor.doorbell_all_occupancy",
|
"sensor_entity_id": "binary_sensor.barn_all_occupancy",
|
||||||
"opened_at": 1774443242,
|
"opened_at": 1774444953,
|
||||||
"expires_at": null
|
"expires_at": 1774444995
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user