[ 'title' => 'Wall Panel', 'poll_interval_ms' => 5000, 'main_room_name' => 'Главная', 'main_room_icon' => 'mdi:home', 'edit_mode' => false, 'battery_history_hours' => 4320, ], 'home_assistant' => [ 'base_url' => '', 'token' => '', 'verify_ssl' => true, 'sync_url' => '', 'sync_token' => '', 'sync_timeout' => 10, 'sync_verify_ssl' => true, 'sync_cache_seconds' => 30, 'weather_entity_id' => '', 'auto_label' => 'auto', 'auto_entity_ids' => [], ], 'camera' => [ 'rtsp_url' => 'rtsp://10.0.6.110:45321/feff99fa45f317e7', 'stream_url' => '', 'stream_mode' => 'hls', 'poster_url' => 'http://10.0.6.110:5000/api/doorbell/latest.jpg', 'popup_timeout_minutes' => 3, 'trigger_entities' => [ 'binary_sensor.door_all_occupancy', 'binary_sensor.barn_all_occupancy', 'binary_sensor.doorbell_all_occupancy', ], ], 'rooms' => [], ]; } function app_config_path(): string { $override = trim((string)getenv('WALL_PANEL_CONFIG_PATH')); if ($override !== '') { return $override; } return APP_ROOT . '/config/config.json'; } function app_runtime_mode(): string { return strtolower(trim((string)getenv('WALL_PANEL_RUNTIME_MODE'))); } function app_addon_options_path(): string { $override = trim((string)getenv('WALL_PANEL_OPTIONS_PATH')); if ($override !== '') { return $override; } return '/data/options.json'; } function app_storage_path(string $file): string { $override = trim((string)getenv('WALL_PANEL_STORAGE_DIR')); $base = $override !== '' ? rtrim($override, '/') : APP_ROOT . '/storage'; return $base . '/' . ltrim($file, '/'); } function app_log(string $message): void { error_log('[wall_panel] ' . $message); } function app_ensure_directory(string $path): void { if (!is_dir($path)) { mkdir($path, 0775, true); } } function app_load_config(): array { $path = app_config_path(); app_ensure_directory(dirname($path)); $config = app_default_config(); $pathExists = file_exists($path); if ($pathExists) { $raw = file_get_contents($path); if ($raw === false || trim($raw) !== '') { $decoded = json_decode((string)$raw, true); if (!is_array($decoded)) { throw new RuntimeException('Invalid JSON in config/config.json'); } $config = array_replace_recursive($config, $decoded); } } $optionsPath = function_exists('app_addon_options_path') ? app_addon_options_path() : '/data/options.json'; if (app_runtime_mode() === 'addon' && is_file($optionsPath)) { $options = app_load_json_file($optionsPath, []); if (is_array($options) && $options !== []) { $config = array_replace_recursive($config, $options); } } if (!$pathExists || app_runtime_mode() === 'addon') { $json = json_encode($config, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); if ($json === false) { throw new RuntimeException('Failed to encode default config'); } file_put_contents($path, $json . PHP_EOL, LOCK_EX); } return $config; } function app_save_config(array $config): void { $path = app_config_path(); app_ensure_directory(dirname($path)); $json = json_encode($config, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); if ($json === false) { throw new RuntimeException('Failed to encode config'); } file_put_contents($path, $json . PHP_EOL, LOCK_EX); } function app_load_json_file(string $path, array $fallback = []): array { if (!file_exists($path)) { return $fallback; } $raw = file_get_contents($path); if ($raw === false || trim($raw) === '') { return $fallback; } $decoded = json_decode($raw, true); return is_array($decoded) ? $decoded : $fallback; } function app_save_json_file(string $path, array $data): void { app_ensure_directory(dirname($path)); $json = json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); if ($json === false) { throw new RuntimeException('Failed to encode JSON'); } file_put_contents($path, $json . PHP_EOL, LOCK_EX); } function app_remote_sync_enabled(array $config): bool { $syncUrl = trim((string)($config['home_assistant']['sync_url'] ?? '')); $syncToken = trim((string)($config['home_assistant']['sync_token'] ?? '')); 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 { return app_storage_path('ha_sync_cache.json'); } function app_remote_sync_cache_ttl(array $config): int { return max(5, (int)($config['home_assistant']['sync_cache_seconds'] ?? 30)); } function app_http_json_request( string $method, string $url, array $headers = [], ?array $body = null, int $timeout = 10, bool $verifySsl = true, bool $throwOnFailure = false ): array|null { $ch = curl_init($url); if ($ch === false) { if ($throwOnFailure) { throw new RuntimeException('Failed to initialize HTTP client'); } return null; } $requestHeaders = array_merge([ 'Accept: application/json', ], $headers); if ($body !== null) { $requestHeaders[] = 'Content-Type: application/json'; } curl_setopt_array($ch, [ CURLOPT_RETURNTRANSFER => true, CURLOPT_CUSTOMREQUEST => strtoupper($method), CURLOPT_HTTPHEADER => $requestHeaders, CURLOPT_TIMEOUT => max(1, $timeout), CURLOPT_CONNECTTIMEOUT => max(1, min(7, $timeout)), CURLOPT_SSL_VERIFYPEER => $verifySsl, CURLOPT_SSL_VERIFYHOST => $verifySsl ? 2 : 0, ]); if ($body !== null) { curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($body, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES)); } $raw = curl_exec($ch); $errno = curl_errno($ch); $error = curl_error($ch); $status = (int)curl_getinfo($ch, CURLINFO_RESPONSE_CODE); curl_close($ch); if ($errno !== 0) { if ($throwOnFailure) { throw new RuntimeException('HTTP request failed: ' . $error); } return null; } if ($status >= 400) { if ($throwOnFailure) { throw new RuntimeException('HTTP request returned status ' . $status); } return null; } $decoded = json_decode((string)$raw, true); return is_array($decoded) ? $decoded : []; } function app_merge_synced_config(array $baseConfig, array $syncedConfig): array { $syncFields = [ 'sync_url' => (string)($baseConfig['home_assistant']['sync_url'] ?? ''), 'sync_token' => (string)($baseConfig['home_assistant']['sync_token'] ?? ''), 'sync_timeout' => (int)($baseConfig['home_assistant']['sync_timeout'] ?? 10), 'sync_verify_ssl' => (bool)($baseConfig['home_assistant']['sync_verify_ssl'] ?? true), 'sync_cache_seconds' => (int)($baseConfig['home_assistant']['sync_cache_seconds'] ?? 30), ]; $merged = array_replace_recursive($baseConfig, $syncedConfig); foreach ($syncFields as $key => $value) { $merged['home_assistant'][$key] = $value; } return $merged; } function app_supervisor_addon_slug(): string { $override = trim((string)getenv('WALL_PANEL_ADDON_SLUG')); if ($override !== '') { return $override; } return 'self'; } function app_addon_register_panel_url(): string { $override = trim((string)getenv('WALL_PANEL_REGISTER_URL')); if ($override !== '') { return $override; } return 'http://supervisor/core/api/wall_panel/register-panel'; } function app_supervisor_ingress_url(): string { $token = trim((string)getenv('SUPERVISOR_TOKEN')); if ($token === '') { app_log('SUPERVISOR_TOKEN is missing'); 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)) { app_log('failed to fetch addon info from Supervisor'); return ''; } $ingressUrl = trim((string)($response['ingress_url'] ?? '')); if ($ingressUrl === '') { $ingressEntry = trim((string)($response['ingress_entry'] ?? '')); if ($ingressEntry !== '') { $ingressUrl = $ingressEntry; app_log('Supervisor returned empty ingress_url, using ingress_entry'); } } if ($ingressUrl === '') { $webui = trim((string)($response['webui'] ?? '')); if ($webui !== '') { $ingressUrl = $webui; app_log('Supervisor returned empty ingress_url, falling back to webui'); } else { app_log('Supervisor returned empty ingress_url'); } } return $ingressUrl; } function app_register_ingress_url(array $config): bool { if (app_runtime_mode() !== 'addon') { return false; } $ingressUrl = app_supervisor_ingress_url(); if ($ingressUrl === '') { return false; } $registerUrl = app_addon_register_panel_url(); if ($registerUrl === '') { return false; } $cacheKey = hash('sha256', $ingressUrl . '|' . $registerUrl); $cachePath = app_panel_registration_cache_path(); $cache = app_load_json_file($cachePath, []); if (trim((string)($cache['cache_key'] ?? '')) === $cacheKey) { return true; } $headers = []; $supervisorToken = trim((string)getenv('SUPERVISOR_TOKEN')); if ($supervisorToken !== '') { $headers[] = 'Authorization: Bearer ' . $supervisorToken; } $response = app_http_json_request( 'POST', $registerUrl, $headers, [ 'panel_url' => $ingressUrl, ], max(1, (int)($config['home_assistant']['sync_timeout'] ?? 10)), false, false ); if (is_array($response) && (bool)($response['ok'] ?? false)) { app_log('ingress registered: ' . $ingressUrl); app_save_json_file($cachePath, [ 'cache_key' => $cacheKey, 'ingress_url' => $ingressUrl, 'registered_at' => time(), ]); return true; } app_log('failed to register ingress at HA endpoint'); return false; } function app_load_remote_config(array $config, bool $refresh = false): array|null { if (!app_remote_sync_enabled($config)) { return null; } $cachePath = app_remote_sync_cache_path(); $cache = app_load_json_file($cachePath, []); $cachedConfig = is_array($cache['config'] ?? null) ? $cache['config'] : null; $cachedAt = (int)($cache['fetched_at'] ?? 0); $ttl = app_remote_sync_cache_ttl($config); if (!$refresh && $cachedConfig !== null && $cachedAt > 0 && (time() - $cachedAt) < $ttl) { return $cachedConfig; } $syncUrl = trim((string)($config['home_assistant']['sync_url'] ?? '')); $syncToken = trim((string)($config['home_assistant']['sync_token'] ?? '')); $timeout = max(1, (int)($config['home_assistant']['sync_timeout'] ?? 10)); $verifySsl = (bool)($config['home_assistant']['sync_verify_ssl'] ?? true); $response = app_http_json_request('GET', $syncUrl, [ 'X-Wall-Panel-Token: ' . $syncToken, ], null, $timeout, $verifySsl, false); $remoteConfig = null; if (is_array($response)) { if (is_array($response['config'] ?? null)) { $remoteConfig = $response['config']; } elseif (!isset($response['ok']) || (bool)$response['ok'] !== false) { $remoteConfig = $response; } } if (is_array($remoteConfig)) { app_save_json_file($cachePath, [ 'fetched_at' => time(), 'config' => $remoteConfig, ]); return $remoteConfig; } if ($cachedConfig !== null) { return $cachedConfig; } return null; } function app_clear_remote_config_cache(): void { $path = app_remote_sync_cache_path(); if (file_exists($path)) { @unlink($path); } } function app_sync_remote_action(array $config, string $action, array $payload): array|null { if (!app_remote_sync_enabled($config)) { return null; } $syncUrl = trim((string)($config['home_assistant']['sync_url'] ?? '')); $syncToken = trim((string)($config['home_assistant']['sync_token'] ?? '')); $timeout = max(1, (int)($config['home_assistant']['sync_timeout'] ?? 10)); $verifySsl = (bool)($config['home_assistant']['sync_verify_ssl'] ?? true); $response = app_http_json_request('POST', $syncUrl, [ 'X-Wall-Panel-Token: ' . $syncToken, ], [ 'action' => $action, 'payload' => $payload, ], $timeout, $verifySsl, false); if (!is_array($response)) { return null; } if (is_array($response['config'] ?? null)) { app_save_json_file(app_remote_sync_cache_path(), [ 'fetched_at' => time(), 'config' => $response['config'], ]); } return $response; }