[ '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_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_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(); $storedConfig = $config; if (!file_exists($path)) { $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); } else { $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); $storedConfig = $config; } } $optionsPath = function_exists('app_addon_options_path') ? app_addon_options_path() : '/data/options.json'; if (is_file($optionsPath)) { $options = app_load_json_file($optionsPath, []); if (is_array($options) && $options !== []) { $config = array_replace_recursive($config, $options); if (app_runtime_mode() === 'addon' && $config !== $storedConfig) { app_save_config($config); } } } 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_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_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; }