-
This commit is contained in:
parent
ab51630786
commit
333806e9cd
@ -85,6 +85,8 @@ Add-on использует persistent config file:
|
|||||||
|
|
||||||
Внутренний runtime cache add-on хранит в `/data/wall_panel`.
|
Внутренний runtime cache add-on хранит в `/data/wall_panel`.
|
||||||
|
|
||||||
|
Если Home Assistant открыт по `https://`, custom component ждёт ingress URL add-on и встраивает его только после того, как add-on сам его зарегистрирует. Это убирает mixed content и не требует внешнего `http://`-доступа.
|
||||||
|
|
||||||
### Старый embed-режим
|
### Старый embed-режим
|
||||||
|
|
||||||
Отдельный PHP-доступ по-прежнему работает:
|
Отдельный PHP-доступ по-прежнему работает:
|
||||||
|
|||||||
@ -14,6 +14,7 @@ from .const import (
|
|||||||
CONF_REQUIRE_ADMIN,
|
CONF_REQUIRE_ADMIN,
|
||||||
CONF_SIDEBAR_ICON,
|
CONF_SIDEBAR_ICON,
|
||||||
CONF_SIDEBAR_TITLE,
|
CONF_SIDEBAR_TITLE,
|
||||||
|
CONF_SYNC_TOKEN,
|
||||||
DEFAULT_FRONTEND_URL_PATH,
|
DEFAULT_FRONTEND_URL_PATH,
|
||||||
DEFAULT_SIDEBAR_ICON,
|
DEFAULT_SIDEBAR_ICON,
|
||||||
DEFAULT_SIDEBAR_TITLE,
|
DEFAULT_SIDEBAR_TITLE,
|
||||||
@ -41,6 +42,7 @@ async def async_setup_frontend(hass: HomeAssistant, entry) -> str:
|
|||||||
sidebar_icon = str(entry.options.get(CONF_SIDEBAR_ICON, DEFAULT_SIDEBAR_ICON) or DEFAULT_SIDEBAR_ICON).strip()
|
sidebar_icon = str(entry.options.get(CONF_SIDEBAR_ICON, DEFAULT_SIDEBAR_ICON) or DEFAULT_SIDEBAR_ICON).strip()
|
||||||
require_admin = bool(entry.options.get(CONF_REQUIRE_ADMIN, False))
|
require_admin = bool(entry.options.get(CONF_REQUIRE_ADMIN, False))
|
||||||
panel_url = str(entry.options.get(CONF_PANEL_URL, "") or "").strip()
|
panel_url = str(entry.options.get(CONF_PANEL_URL, "") or "").strip()
|
||||||
|
sync_token = str(entry.options.get(CONF_SYNC_TOKEN, "") or "").strip()
|
||||||
|
|
||||||
async_register_built_in_panel(
|
async_register_built_in_panel(
|
||||||
hass,
|
hass,
|
||||||
@ -58,6 +60,7 @@ async def async_setup_frontend(hass: HomeAssistant, entry) -> str:
|
|||||||
"panel_url": panel_url,
|
"panel_url": panel_url,
|
||||||
"panel_url_path": panel_url_path,
|
"panel_url_path": panel_url_path,
|
||||||
"entry_id": entry.entry_id,
|
"entry_id": entry.entry_id,
|
||||||
|
"sync_token": sync_token,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@ -4,6 +4,8 @@ class WallPanelPanel extends HTMLElement {
|
|||||||
this._hass = null;
|
this._hass = null;
|
||||||
this._panel = null;
|
this._panel = null;
|
||||||
this._narrow = false;
|
this._narrow = false;
|
||||||
|
this._pollTimer = null;
|
||||||
|
this._activePanelUrl = '';
|
||||||
this.attachShadow({ mode: 'open' });
|
this.attachShadow({ mode: 'open' });
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -26,6 +28,13 @@ class WallPanelPanel extends HTMLElement {
|
|||||||
this._render();
|
this._render();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
disconnectedCallback() {
|
||||||
|
if (this._pollTimer) {
|
||||||
|
window.clearInterval(this._pollTimer);
|
||||||
|
this._pollTimer = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
_resolveUrl(rawUrl) {
|
_resolveUrl(rawUrl) {
|
||||||
const value = String(rawUrl || '').trim();
|
const value = String(rawUrl || '').trim();
|
||||||
if (!value) {
|
if (!value) {
|
||||||
@ -46,18 +55,57 @@ class WallPanelPanel extends HTMLElement {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_render() {
|
_panelConfig() {
|
||||||
|
const config = this._panel?.config || {};
|
||||||
|
const customConfig = config._panel_custom || {};
|
||||||
|
return customConfig.config || customConfig || config;
|
||||||
|
}
|
||||||
|
|
||||||
|
_configUrl() {
|
||||||
|
const payload = this._panelConfig();
|
||||||
|
const entryId = String(payload.entry_id || '').trim();
|
||||||
|
if (!entryId) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
return `/api/wall_panel/config/${encodeURIComponent(entryId)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
async _fetchPanelUrl() {
|
||||||
|
const payload = this._panelConfig();
|
||||||
|
const configUrl = this._configUrl();
|
||||||
|
const syncToken = String(payload.sync_token || '').trim();
|
||||||
|
if (!configUrl || !syncToken) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
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) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
const panelUrl = String(data?.panel?.panel_url || data?.panel_url || '').trim();
|
||||||
|
return panelUrl;
|
||||||
|
} catch (error) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_renderMessage(title, body, extra = '') {
|
||||||
if (!this.shadowRoot) {
|
if (!this.shadowRoot) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const config = this._panel?.config || {};
|
|
||||||
const customConfig = config._panel_custom || {};
|
|
||||||
const payload = customConfig.config || customConfig || config;
|
|
||||||
const panelUrl = this._resolveUrl(
|
|
||||||
payload.panel_url || config.panel_url || customConfig.panel_url || ''
|
|
||||||
);
|
|
||||||
|
|
||||||
this.shadowRoot.innerHTML = `
|
this.shadowRoot.innerHTML = `
|
||||||
<style>
|
<style>
|
||||||
:host {
|
:host {
|
||||||
@ -75,14 +123,7 @@ class WallPanelPanel extends HTMLElement {
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
background: var(--primary-background-color, #0d0f14);
|
background: var(--primary-background-color, #0d0f14);
|
||||||
}
|
}
|
||||||
iframe {
|
.message {
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
border: 0;
|
|
||||||
display: block;
|
|
||||||
background: transparent;
|
|
||||||
}
|
|
||||||
.empty {
|
|
||||||
display: grid;
|
display: grid;
|
||||||
place-items: center;
|
place-items: center;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
@ -92,30 +133,41 @@ class WallPanelPanel extends HTMLElement {
|
|||||||
font-family: var(--primary-font-family, sans-serif);
|
font-family: var(--primary-font-family, sans-serif);
|
||||||
color: var(--secondary-text-color, #b3b8c2);
|
color: var(--secondary-text-color, #b3b8c2);
|
||||||
}
|
}
|
||||||
.empty strong {
|
.message strong {
|
||||||
display: block;
|
display: block;
|
||||||
color: var(--primary-text-color, #fff);
|
color: var(--primary-text-color, #fff);
|
||||||
margin-bottom: 8px;
|
margin-bottom: 8px;
|
||||||
font-size: 1.1rem;
|
font-size: 1.1rem;
|
||||||
}
|
}
|
||||||
|
.message code {
|
||||||
|
display: inline-block;
|
||||||
|
margin-top: 10px;
|
||||||
|
padding: 6px 10px;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: rgba(255, 255, 255, 0.06);
|
||||||
|
color: var(--primary-text-color, #fff);
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
<div class="wrap"></div>
|
<div class="wrap">
|
||||||
|
<div class="message">
|
||||||
|
<div>
|
||||||
|
<strong>${title}</strong>
|
||||||
|
<div>${body}</div>
|
||||||
|
${extra}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
`;
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
const wrap = this.shadowRoot.querySelector('.wrap');
|
_renderIframe(panelUrl) {
|
||||||
if (!wrap) {
|
if (!this.shadowRoot) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!panelUrl) {
|
const wrap = this.shadowRoot.querySelector('.wrap');
|
||||||
wrap.innerHTML = `
|
if (!wrap) {
|
||||||
<div class="empty">
|
|
||||||
<div>
|
|
||||||
<strong>Wall Panel is not configured</strong>
|
|
||||||
<div>Set the PHP panel URL in the integration options.</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -124,7 +176,62 @@ class WallPanelPanel extends HTMLElement {
|
|||||||
iframe.loading = 'eager';
|
iframe.loading = 'eager';
|
||||||
iframe.referrerPolicy = 'no-referrer';
|
iframe.referrerPolicy = 'no-referrer';
|
||||||
iframe.allow = 'autoplay; fullscreen; picture-in-picture';
|
iframe.allow = 'autoplay; fullscreen; picture-in-picture';
|
||||||
|
iframe.style.width = '100%';
|
||||||
|
iframe.style.height = '100%';
|
||||||
|
iframe.style.border = '0';
|
||||||
|
iframe.style.display = 'block';
|
||||||
|
iframe.style.background = 'transparent';
|
||||||
wrap.replaceChildren(iframe);
|
wrap.replaceChildren(iframe);
|
||||||
|
this._activePanelUrl = panelUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
async _tryAttachPanel() {
|
||||||
|
const payload = this._panelConfig();
|
||||||
|
const initialUrl = this._resolveUrl(
|
||||||
|
payload.panel_url || payload.ingress_url || ''
|
||||||
|
);
|
||||||
|
|
||||||
|
if (initialUrl && initialUrl === this._activePanelUrl) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (initialUrl && !(window.location.protocol === 'https:' && initialUrl.startsWith('http://'))) {
|
||||||
|
this._renderIframe(initialUrl);
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
const configUrl = this._configUrl();
|
||||||
|
this._renderMessage(
|
||||||
|
'Waiting for Wall Panel',
|
||||||
|
'The add-on will publish a secure HTTPS ingress URL here.',
|
||||||
|
configUrl ? `<code>${configUrl}</code>` : ''
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
_render() {
|
||||||
|
if (!this.shadowRoot) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this._pollTimer) {
|
||||||
|
window.clearInterval(this._pollTimer);
|
||||||
|
this._pollTimer = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
this._tryAttachPanel();
|
||||||
|
this._pollTimer = window.setInterval(() => {
|
||||||
|
this._tryAttachPanel();
|
||||||
|
}, 2000);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -9,12 +9,13 @@ 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 .const import CONF_CONFIG, CONF_SYNC_TOKEN, DOMAIN
|
from .const import CONF_CONFIG, CONF_PANEL_URL, CONF_SYNC_TOKEN, DOMAIN
|
||||||
from .helpers import (
|
from .helpers import (
|
||||||
build_patch_payload,
|
build_patch_payload,
|
||||||
config_to_json,
|
config_to_json,
|
||||||
create_room_layout_item,
|
create_room_layout_item,
|
||||||
current_entry_config,
|
current_entry_config,
|
||||||
|
current_entry_panel,
|
||||||
delete_room_layout_item,
|
delete_room_layout_item,
|
||||||
normalize_config,
|
normalize_config,
|
||||||
reorder_room_grid,
|
reorder_room_grid,
|
||||||
@ -63,6 +64,13 @@ def _save_entry_config(hass: HomeAssistant, entry, config: dict[str, Any]) -> No
|
|||||||
hass.data.setdefault(DOMAIN, {}).setdefault(entry.entry_id, {})["config"] = config
|
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
|
||||||
|
|
||||||
|
|
||||||
class WallPanelConfigView(HomeAssistantView):
|
class WallPanelConfigView(HomeAssistantView):
|
||||||
"""Serve and update the canonical Wall Panel config."""
|
"""Serve and update the canonical Wall Panel config."""
|
||||||
|
|
||||||
@ -79,7 +87,11 @@ class WallPanelConfigView(HomeAssistantView):
|
|||||||
return _response({"ok": False, "error": "Unauthorized"}, 401)
|
return _response({"ok": False, "error": "Unauthorized"}, 401)
|
||||||
|
|
||||||
config = current_entry_config(entry)
|
config = current_entry_config(entry)
|
||||||
return _response(config)
|
return _response({
|
||||||
|
"ok": True,
|
||||||
|
"config": config,
|
||||||
|
"panel": current_entry_panel(entry),
|
||||||
|
})
|
||||||
|
|
||||||
async def async_post(self, request: web.Request, entry_id: str) -> web.Response:
|
async def async_post(self, request: web.Request, entry_id: str) -> web.Response:
|
||||||
hass = request.app["hass"]
|
hass = request.app["hass"]
|
||||||
@ -148,6 +160,15 @@ class WallPanelConfigView(HomeAssistantView):
|
|||||||
if not room_id or not isinstance(entries, list):
|
if not room_id or not isinstance(entries, list):
|
||||||
return _response({"ok": False, "error": "room_id and entries are required"}, 400)
|
return _response({"ok": False, "error": "room_id and entries are required"}, 400)
|
||||||
config = reorder_room_grid(config, room_id, entries)
|
config = reorder_room_grid(config, room_id, entries)
|
||||||
|
elif action == "register-panel":
|
||||||
|
panel_url = str(action_payload.get("panel_url", "") or "").strip()
|
||||||
|
if not panel_url:
|
||||||
|
return _response({"ok": False, "error": "panel_url is required"}, 400)
|
||||||
|
_save_entry_panel_url(hass, entry, panel_url)
|
||||||
|
return _response({
|
||||||
|
"ok": True,
|
||||||
|
"panel": current_entry_panel(entry),
|
||||||
|
})
|
||||||
elif isinstance(payload.get("config"), dict):
|
elif isinstance(payload.get("config"), dict):
|
||||||
config = normalize_config(payload["config"])
|
config = normalize_config(payload["config"])
|
||||||
else:
|
else:
|
||||||
@ -157,4 +178,5 @@ class WallPanelConfigView(HomeAssistantView):
|
|||||||
return _response({
|
return _response({
|
||||||
"ok": True,
|
"ok": True,
|
||||||
"config": config,
|
"config": config,
|
||||||
|
"panel": current_entry_panel(entry),
|
||||||
})
|
})
|
||||||
|
|||||||
@ -168,6 +168,11 @@ function app_remote_sync_enabled(array $config): bool
|
|||||||
return $syncUrl !== '' && $syncToken !== '';
|
return $syncUrl !== '' && $syncToken !== '';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function app_panel_registration_cache_path(): string
|
||||||
|
{
|
||||||
|
return app_storage_path('panel_registration_cache.json');
|
||||||
|
}
|
||||||
|
|
||||||
function app_remote_sync_cache_path(): string
|
function app_remote_sync_cache_path(): string
|
||||||
{
|
{
|
||||||
return app_storage_path('ha_sync_cache.json');
|
return app_storage_path('ha_sync_cache.json');
|
||||||
@ -258,6 +263,74 @@ function app_merge_synced_config(array $baseConfig, array $syncedConfig): array
|
|||||||
return $merged;
|
return $merged;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function app_supervisor_addon_slug(): string
|
||||||
|
{
|
||||||
|
$override = trim((string)getenv('WALL_PANEL_ADDON_SLUG'));
|
||||||
|
if ($override !== '') {
|
||||||
|
return $override;
|
||||||
|
}
|
||||||
|
|
||||||
|
return 'wall_panel';
|
||||||
|
}
|
||||||
|
|
||||||
|
function app_supervisor_ingress_url(): string
|
||||||
|
{
|
||||||
|
$token = trim((string)getenv('SUPERVISOR_TOKEN'));
|
||||||
|
if ($token === '') {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
$url = 'http://supervisor/addons/' . rawurlencode(app_supervisor_addon_slug()) . '/info';
|
||||||
|
$response = app_http_json_request('GET', $url, [
|
||||||
|
'Authorization: Bearer ' . $token,
|
||||||
|
], null, 10, false, false);
|
||||||
|
|
||||||
|
if (!is_array($response)) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
return trim((string)($response['ingress_url'] ?? ''));
|
||||||
|
}
|
||||||
|
|
||||||
|
function app_register_ingress_url(array $config): void
|
||||||
|
{
|
||||||
|
if (!app_remote_sync_enabled($config)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$ingressUrl = app_supervisor_ingress_url();
|
||||||
|
if ($ingressUrl === '') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$syncUrl = trim((string)($config['home_assistant']['sync_url'] ?? ''));
|
||||||
|
$syncToken = trim((string)($config['home_assistant']['sync_token'] ?? ''));
|
||||||
|
$cacheKey = hash('sha256', $ingressUrl . '|' . $syncUrl . '|' . $syncToken);
|
||||||
|
|
||||||
|
$cachePath = app_panel_registration_cache_path();
|
||||||
|
$cache = app_load_json_file($cachePath, []);
|
||||||
|
if (trim((string)($cache['cache_key'] ?? '')) === $cacheKey) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$response = app_http_json_request('POST', $syncUrl, [
|
||||||
|
'X-Wall-Panel-Token: ' . $syncToken,
|
||||||
|
], [
|
||||||
|
'action' => 'register-panel',
|
||||||
|
'payload' => [
|
||||||
|
'panel_url' => $ingressUrl,
|
||||||
|
],
|
||||||
|
], max(1, (int)($config['home_assistant']['sync_timeout'] ?? 10)), (bool)($config['home_assistant']['sync_verify_ssl'] ?? true), false);
|
||||||
|
|
||||||
|
if (is_array($response) && (bool)($response['ok'] ?? false)) {
|
||||||
|
app_save_json_file($cachePath, [
|
||||||
|
'cache_key' => $cacheKey,
|
||||||
|
'ingress_url' => $ingressUrl,
|
||||||
|
'registered_at' => time(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function app_load_remote_config(array $config, bool $refresh = false): array|null
|
function app_load_remote_config(array $config, bool $refresh = false): array|null
|
||||||
{
|
{
|
||||||
if (!app_remote_sync_enabled($config)) {
|
if (!app_remote_sync_enabled($config)) {
|
||||||
|
|||||||
2
run.sh
2
run.sh
@ -16,4 +16,6 @@ export WALL_PANEL_CONFIG_PATH="$CONFIG_PATH"
|
|||||||
export WALL_PANEL_STORAGE_DIR="$STORAGE_DIR"
|
export WALL_PANEL_STORAGE_DIR="$STORAGE_DIR"
|
||||||
export WALL_PANEL_RUNTIME_MODE="addon"
|
export WALL_PANEL_RUNTIME_MODE="addon"
|
||||||
|
|
||||||
|
php -r 'require "/app/lib/bootstrap.php"; $config = app_load_config(); app_register_ingress_url($config);' >/dev/null 2>&1 || true &
|
||||||
|
|
||||||
exec php -S "0.0.0.0:${PORT}" -t "$DOCROOT"
|
exec php -S "0.0.0.0:${PORT}" -t "$DOCROOT"
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"active": false,
|
"active": false,
|
||||||
"sensor_entity_id": "binary_sensor.barn_all_occupancy",
|
"sensor_entity_id": "binary_sensor.barn_all_occupancy",
|
||||||
"opened_at": 1774436558,
|
"opened_at": 1774439958,
|
||||||
"expires_at": null
|
"expires_at": null
|
||||||
}
|
}
|
||||||
|
|||||||
@ -84,6 +84,8 @@ Add-on использует persistent config file:
|
|||||||
|
|
||||||
Внутренний runtime cache add-on хранит в `/data/wall_panel`.
|
Внутренний runtime cache add-on хранит в `/data/wall_panel`.
|
||||||
|
|
||||||
|
Если Home Assistant открыт по `https://`, custom component ждёт ingress URL add-on и встраивает его только после того, как add-on сам его зарегистрирует. Это убирает mixed content и не требует внешнего `http://`-доступа.
|
||||||
|
|
||||||
### Старый embed-режим
|
### Старый embed-режим
|
||||||
|
|
||||||
Отдельный PHP-доступ по-прежнему работает:
|
Отдельный PHP-доступ по-прежнему работает:
|
||||||
|
|||||||
@ -168,6 +168,11 @@ function app_remote_sync_enabled(array $config): bool
|
|||||||
return $syncUrl !== '' && $syncToken !== '';
|
return $syncUrl !== '' && $syncToken !== '';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function app_panel_registration_cache_path(): string
|
||||||
|
{
|
||||||
|
return app_storage_path('panel_registration_cache.json');
|
||||||
|
}
|
||||||
|
|
||||||
function app_remote_sync_cache_path(): string
|
function app_remote_sync_cache_path(): string
|
||||||
{
|
{
|
||||||
return app_storage_path('ha_sync_cache.json');
|
return app_storage_path('ha_sync_cache.json');
|
||||||
@ -258,6 +263,74 @@ function app_merge_synced_config(array $baseConfig, array $syncedConfig): array
|
|||||||
return $merged;
|
return $merged;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function app_supervisor_addon_slug(): string
|
||||||
|
{
|
||||||
|
$override = trim((string)getenv('WALL_PANEL_ADDON_SLUG'));
|
||||||
|
if ($override !== '') {
|
||||||
|
return $override;
|
||||||
|
}
|
||||||
|
|
||||||
|
return 'wall_panel';
|
||||||
|
}
|
||||||
|
|
||||||
|
function app_supervisor_ingress_url(): string
|
||||||
|
{
|
||||||
|
$token = trim((string)getenv('SUPERVISOR_TOKEN'));
|
||||||
|
if ($token === '') {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
$url = 'http://supervisor/addons/' . rawurlencode(app_supervisor_addon_slug()) . '/info';
|
||||||
|
$response = app_http_json_request('GET', $url, [
|
||||||
|
'Authorization: Bearer ' . $token,
|
||||||
|
], null, 10, false, false);
|
||||||
|
|
||||||
|
if (!is_array($response)) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
return trim((string)($response['ingress_url'] ?? ''));
|
||||||
|
}
|
||||||
|
|
||||||
|
function app_register_ingress_url(array $config): void
|
||||||
|
{
|
||||||
|
if (!app_remote_sync_enabled($config)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$ingressUrl = app_supervisor_ingress_url();
|
||||||
|
if ($ingressUrl === '') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$syncUrl = trim((string)($config['home_assistant']['sync_url'] ?? ''));
|
||||||
|
$syncToken = trim((string)($config['home_assistant']['sync_token'] ?? ''));
|
||||||
|
$cacheKey = hash('sha256', $ingressUrl . '|' . $syncUrl . '|' . $syncToken);
|
||||||
|
|
||||||
|
$cachePath = app_panel_registration_cache_path();
|
||||||
|
$cache = app_load_json_file($cachePath, []);
|
||||||
|
if (trim((string)($cache['cache_key'] ?? '')) === $cacheKey) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$response = app_http_json_request('POST', $syncUrl, [
|
||||||
|
'X-Wall-Panel-Token: ' . $syncToken,
|
||||||
|
], [
|
||||||
|
'action' => 'register-panel',
|
||||||
|
'payload' => [
|
||||||
|
'panel_url' => $ingressUrl,
|
||||||
|
],
|
||||||
|
], max(1, (int)($config['home_assistant']['sync_timeout'] ?? 10)), (bool)($config['home_assistant']['sync_verify_ssl'] ?? true), false);
|
||||||
|
|
||||||
|
if (is_array($response) && (bool)($response['ok'] ?? false)) {
|
||||||
|
app_save_json_file($cachePath, [
|
||||||
|
'cache_key' => $cacheKey,
|
||||||
|
'ingress_url' => $ingressUrl,
|
||||||
|
'registered_at' => time(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function app_load_remote_config(array $config, bool $refresh = false): array|null
|
function app_load_remote_config(array $config, bool $refresh = false): array|null
|
||||||
{
|
{
|
||||||
if (!app_remote_sync_enabled($config)) {
|
if (!app_remote_sync_enabled($config)) {
|
||||||
|
|||||||
@ -16,4 +16,6 @@ export WALL_PANEL_CONFIG_PATH="$CONFIG_PATH"
|
|||||||
export WALL_PANEL_STORAGE_DIR="$STORAGE_DIR"
|
export WALL_PANEL_STORAGE_DIR="$STORAGE_DIR"
|
||||||
export WALL_PANEL_RUNTIME_MODE="addon"
|
export WALL_PANEL_RUNTIME_MODE="addon"
|
||||||
|
|
||||||
|
php -r 'require "/app/lib/bootstrap.php"; $config = app_load_config(); app_register_ingress_url($config);' >/dev/null 2>&1 || true &
|
||||||
|
|
||||||
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