475 lines
13 KiB
PHP
Executable File
475 lines
13 KiB
PHP
Executable File
<?php
|
|
declare(strict_types=1);
|
|
|
|
function app_default_config(): array
|
|
{
|
|
return [
|
|
'app' => [
|
|
'title' => 'Striker 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;
|
|
}
|