$entityId, 'state_entity_id' => trim((string)($item['state_entity_id'] ?? $entityId)) ?: $entityId, 'command' => trim((string)($item['command'] ?? 'set_temperature')) ?: 'set_temperature', 'value' => array_key_exists('value', $item) ? $item['value'] : null, 'active_value' => array_key_exists('active_value', $item) ? $item['active_value'] : (array_key_exists('value', $item) ? $item['value'] : null), 'label_active' => (string)($item['label_active'] ?? $item['active_label'] ?? $item['label'] ?? ''), 'label_inactive' => (string)($item['label_inactive'] ?? $item['inactive_label'] ?? $item['label'] ?? ''), 'icon' => (string)($item['icon'] ?? 'mdi:thermometer'), 'active_color' => (string)($item['active_color'] ?? '#4caf50'), 'inactive_color' => (string)($item['inactive_color'] ?? '#c8e6c9'), 'active_text_color' => (string)($item['active_text_color'] ?? 'white'), 'inactive_text_color' => (string)($item['inactive_text_color'] ?? 'black'), 'active_icon_color' => (string)($item['active_icon_color'] ?? 'white'), 'inactive_icon_color' => (string)($item['inactive_icon_color'] ?? 'gray'), ]; } return array_values($actions); } function app_main_boiler(array $config): array { $boiler = $config['app']['main_boiler'] ?? []; if (!is_array($boiler)) { $boiler = []; } return [ 'title' => (string)($boiler['title'] ?? 'Бойлер'), 'sensor_entity_id' => trim((string)($boiler['sensor_entity_id'] ?? '')), 'history_hours' => max(1, (int)($boiler['history_hours'] ?? 24)), ]; } function app_main_print(array $config): array { $print = $config['app']['main_print'] ?? []; if (!is_array($print)) { $print = []; } return [ 'title' => (string)($print['title'] ?? 'Печать'), 'current_stage_entity_id' => trim((string)($print['current_stage_entity_id'] ?? '')), 'print_progress_entity_id' => trim((string)($print['print_progress_entity_id'] ?? '')), 'start_time_entity_id' => trim((string)($print['start_time_entity_id'] ?? '')), 'end_time_entity_id' => trim((string)($print['end_time_entity_id'] ?? '')), ]; } function app_state_file(): string { return app_storage_path('popup_state.json'); } function app_load_popup_state(): array { return app_load_json_file(app_state_file(), [ 'active' => false, 'sensor_entity_id' => null, 'opened_at' => null, 'expires_at' => null, ]); } function app_save_popup_state(array $state): void { app_save_json_file(app_state_file(), $state); } function app_clear_expired_popup_state(array $config): array { $state = app_load_popup_state(); if (!($state['active'] ?? false)) { return $state; } $expiresAt = (int)($state['expires_at'] ?? 0); if ($expiresAt > 0 && time() >= $expiresAt) { $state['active'] = false; $state['expires_at'] = null; app_save_popup_state($state); } return $state; } function app_popup_close_delay_seconds(): int { return 30; } function app_default_card_type(string $entityId): string { $domain = explode('.', $entityId, 2)[0] ?? ''; return match ($domain) { 'cover' => 'cover', 'climate' => 'climate', 'light', 'switch' => 'toggle', default => 'toggle', }; } function app_entity_domain(string $entityId): string { return explode('.', $entityId, 2)[0] ?? 'unknown'; } function app_entity_name(array $entity, array $registryEntry = [], array $override = []): string { if (!empty($override['title'])) { return (string)$override['title']; } $domain = app_entity_domain((string)($entity['entity_id'] ?? '')); if ($domain === 'fan' && !empty($registryEntry['name'])) { return (string)$registryEntry['name']; } if (!empty($entity['attributes']['friendly_name'])) { return (string)$entity['attributes']['friendly_name']; } if (!empty($registryEntry['name'])) { return (string)$registryEntry['name']; } return $entity['entity_id']; } function app_entity_icon(array $entity, array $override = []): string { if (!empty($override['icon'])) { return (string)$override['icon']; } if (!empty($entity['attributes']['icon'])) { return (string)$entity['attributes']['icon']; } return match (app_entity_domain((string)$entity['entity_id'])) { 'light' => 'mdi:lightbulb', 'switch' => 'mdi:toggle-switch', 'cover' => 'mdi:curtains', 'climate' => 'mdi:air-conditioner', 'weather' => 'mdi:weather-partly-cloudy', 'binary_sensor' => 'mdi:motion-sensor', default => 'mdi:devices', }; } function app_labels_from_entity(array $entity, array $registryEntry = []): array { $labels = []; foreach ([ $entity['attributes']['labels'] ?? null, $entity['attributes']['label_ids'] ?? null, $entity['labels'] ?? null, $registryEntry['labels'] ?? null, $registryEntry['label_ids'] ?? null, ] as $source) { $labels = array_merge($labels, app_flatten_label_source($source)); } return array_values(array_unique(array_map('strval', $labels))); } function app_entity_has_auto_label(array $labels, string $autoLabel): bool { $needle = strtolower(trim($autoLabel)); if ($needle === '') { return false; } foreach ($labels as $label) { $candidate = strtolower(trim((string)$label)); if ($candidate === '') { continue; } if ($candidate === $needle || str_contains($candidate, $needle)) { return true; } } return false; } function app_flatten_label_source(mixed $source): array { if ($source === null) { return []; } if (is_string($source) || is_numeric($source)) { return [(string)$source]; } if (!is_array($source)) { return []; } $labels = []; foreach ($source as $item) { if (is_string($item) || is_numeric($item)) { $labels[] = (string)$item; continue; } if (is_array($item)) { foreach (['name', 'label', 'id', 'label_id', 'entity_id'] as $key) { if (!empty($item[$key])) { $labels[] = (string)$item[$key]; } } } } return $labels; } function app_is_active_entity(array $entity): bool { $state = strtolower((string)($entity['state'] ?? '')); $domain = app_entity_domain((string)$entity['entity_id']); $deviceClass = strtolower((string)($entity['attributes']['device_class'] ?? '')); if (in_array($state, ['unavailable', 'unknown', 'none'], true)) { return false; } return match ($domain) { 'binary_sensor' => $deviceClass === 'door' ? in_array($state, ['on', 'open'], true) : !in_array($state, ['off', 'false', '0', 'idle'], true), 'cover' => in_array($state, ['open', 'opening', 'closing'], true), 'climate' => !in_array($state, ['off', 'unavailable', 'unknown'], true), default => !in_array($state, ['off', 'false', '0', 'idle'], true), }; } function app_is_door_contact_entity(array $entity, array $registryEntry = []): bool { $entityId = (string)($entity['entity_id'] ?? ''); if ($entityId === '' || app_entity_domain($entityId) !== 'binary_sensor') { return false; } $deviceClass = strtolower((string)($entity['attributes']['device_class'] ?? '')); if (in_array($deviceClass, ['door', 'garage_door'], true)) { return true; } $name = strtolower((string)($entity['attributes']['friendly_name'] ?? $registryEntry['name'] ?? '')); if ($name !== '' && (str_contains($name, 'door') || str_contains($name, 'двер'))) { return true; } return false; } function app_select_weather_entity(array $states, array $config): ?array { $preferred = trim((string)($config['home_assistant']['weather_entity_id'] ?? '')); if ($preferred !== '') { foreach ($states as $entity) { if (($entity['entity_id'] ?? null) === $preferred) { return $entity; } } } foreach ($states as $entity) { if (($entity['entity_id'] ?? null) === 'weather.yandex_weather') { return $entity; } } foreach ($states as $entity) { if (app_entity_domain((string)($entity['entity_id'] ?? '')) === 'weather') { return $entity; } } return null; } function app_registry_map(array $entityRegistry): array { $map = []; foreach ($entityRegistry as $item) { if (!empty($item['entity_id'])) { $map[(string)$item['entity_id']] = $item; } } return $map; } function app_device_registry_map(array $deviceRegistry): array { $map = []; foreach ($deviceRegistry as $item) { if (!is_array($item)) { continue; } $deviceId = (string)($item['device_id'] ?? $item['id'] ?? $item['di'] ?? ''); if ($deviceId === '') { continue; } $map[$deviceId] = $item; } return $map; } function app_room_definitions(array $config, array $haData): array { $rooms = []; $areasById = []; $floorsById = []; $roomOverrides = []; foreach ($haData['floors'] ?? [] as $floor) { $floorId = (string)($floor['floor_id'] ?? $floor['id'] ?? ''); if ($floorId === '') { continue; } $floorsById[$floorId] = [ 'floor_id' => $floorId, 'name' => (string)($floor['name'] ?? $floorId), 'icon' => (string)($floor['icon'] ?? 'mdi:stairs'), 'level' => isset($floor['level']) ? (int)$floor['level'] : 0, ]; } foreach ($haData['areas'] ?? [] as $area) { $areaId = (string)($area['area_id'] ?? $area['id'] ?? ''); if ($areaId === '') { continue; } $area['area_id'] = $areaId; $areasById[$areaId] = $area; } $configuredRooms = $config['rooms'] ?? []; if (!is_array($configuredRooms)) { $configuredRooms = []; } foreach ($configuredRooms as $room) { if (!is_array($room) || empty($room['id'])) { continue; } $room['visible'] = $room['visible'] ?? true; $room['order'] = isset($room['order']) ? (int)$room['order'] : 9999; $room['entity_overrides'] = is_array($room['entity_overrides'] ?? null) ? $room['entity_overrides'] : []; $roomOverrides[(string)$room['id']] = $room; } foreach ($areasById as $areaId => $area) { $floorId = (string)($area['floor_id'] ?? $area['fi'] ?? $area['floor'] ?? ''); if ($floorId === '' || !isset($floorsById[$floorId])) { continue; } $room = $roomOverrides[$areaId] ?? []; $overrideName = trim((string)($room['name'] ?? '')); if ($overrideName === '' || $overrideName === $areaId) { $overrideName = ''; } $rooms[$areaId] = [ 'id' => $areaId, 'name' => $overrideName !== '' ? $overrideName : (string)($area['name'] ?? $areaId), 'icon' => (string)($room['icon'] ?? $area['icon'] ?? 'mdi:home-variant'), 'area_id' => $areaId, 'floor_id' => $floorId, 'floor_name' => (string)($room['floor_name'] ?? $floorsById[$floorId]['name'] ?? $floorId), 'floor_level' => (int)($room['floor_level'] ?? $floorsById[$floorId]['level'] ?? 0), 'visible' => array_key_exists('visible', $room) ? (bool)$room['visible'] : true, 'order' => isset($room['order']) ? (int)$room['order'] : 9999, 'entity_ids' => is_array($room['entity_ids'] ?? null) ? $room['entity_ids'] : [], 'entity_overrides' => is_array($room['entity_overrides'] ?? null) ? $room['entity_overrides'] : [], ]; } $rooms = array_values($rooms); usort($rooms, static function (array $a, array $b): int { $visibleA = (bool)($a['visible'] ?? true); $visibleB = (bool)($b['visible'] ?? true); if ($visibleA !== $visibleB) { return $visibleA ? -1 : 1; } $orderA = (int)($a['order'] ?? 9999); $orderB = (int)($b['order'] ?? 9999); if ($orderA !== $orderB) { return $orderA <=> $orderB; } $floorLevelA = (int)($a['floor_level'] ?? 0); $floorLevelB = (int)($b['floor_level'] ?? 0); if ($floorLevelA !== $floorLevelB) { return $floorLevelA <=> $floorLevelB; } return strcasecmp((string)($a['name'] ?? ''), (string)($b['name'] ?? '')); }); return $rooms; } function app_room_entities(array $room, array $haData, bool $includeHidden = false): array { $states = $haData['states'] ?? []; $registryMap = app_registry_map($haData['entity_registry'] ?? []); $deviceMap = app_device_registry_map($haData['device_registry'] ?? []); $explicitIds = $room['entity_ids'] ?? []; $areaId = $room['area_id'] ?? $room['id'] ?? null; $entityOverrides = $room['entity_overrides'] ?? []; $candidates = []; foreach ($states as $entity) { $entityId = (string)($entity['entity_id'] ?? ''); if ($entityId === '') { continue; } $registryEntry = $registryMap[$entityId] ?? []; $entityAreaId = (string)($registryEntry['area_id'] ?? ''); $matchesArea = $areaId !== null && $areaId !== '' && $entityAreaId === (string)$areaId; $matchesExplicit = is_array($explicitIds) && in_array($entityId, $explicitIds, true); $matchesDeviceArea = false; if (!$matchesArea && !$matchesExplicit && $entityAreaId === '') { $deviceId = (string)($registryEntry['device_id'] ?? $registryEntry['id'] ?? $registryEntry['di'] ?? ''); $deviceAreaId = $deviceId !== '' ? (string)($deviceMap[$deviceId]['area_id'] ?? '') : ''; $matchesDeviceArea = $areaId !== null && $areaId !== '' && $deviceAreaId === (string)$areaId; } if (!$matchesArea && !$matchesExplicit && !$matchesDeviceArea) { continue; } if (!empty($registryEntry['hidden_by']) || !empty($registryEntry['disabled_by'])) { continue; } $override = is_array($entityOverrides[$entityId] ?? null) ? $entityOverrides[$entityId] : []; $isVisible = $override['visible'] ?? true; if ($isVisible === false && !$includeHidden) { continue; } $domain = app_entity_domain($entityId); $cardType = (string)($override['card_type'] ?? app_default_card_type($entityId)); $labels = app_labels_from_entity($entity, $registryEntry); $candidates[] = [ 'entity_id' => $entityId, 'domain' => $domain, 'name' => app_entity_name($entity, $registryEntry, $override), 'icon' => app_entity_icon($entity, $override), 'state' => $entity['state'] ?? 'unknown', 'attributes' => $entity['attributes'] ?? [], 'labels' => $labels, 'area_id' => $entityAreaId ?: null, 'card_type' => $cardType, 'order' => (int)($override['order'] ?? 9999), 'subtitle' => app_entity_subtitle($entity, $cardType), 'override' => $override, 'visible' => (bool)$isVisible, ]; } usort($candidates, static function (array $a, array $b): int { if ($a['order'] === $b['order']) { return strcasecmp($a['name'], $b['name']); } return $a['order'] <=> $b['order']; }); return $candidates; } function app_entity_index(array $config, array $haData): array { $states = $haData['states'] ?? []; $registryMap = app_registry_map($haData['entity_registry'] ?? []); $autoLabel = (string)($config['home_assistant']['auto_label'] ?? 'auto'); $manualAuto = array_flip(array_map('strval', $config['home_assistant']['auto_entity_ids'] ?? [])); $items = []; foreach ($states as $entity) { $entityId = (string)($entity['entity_id'] ?? ''); if ($entityId === '') { continue; } $registryEntry = $registryMap[$entityId] ?? []; $labels = app_labels_from_entity($entity, $registryEntry); $isHidden = !empty($registryEntry['hidden_by']) || !empty($registryEntry['disabled_by']) || !empty($registryEntry['entity_registry_enabled_default']) && empty($entity['state']); $items[$entityId] = [ 'entity_id' => $entityId, 'domain' => app_entity_domain($entityId), 'name' => app_entity_name($entity, $registryEntry, []), 'icon' => app_entity_icon($entity, []), 'state' => $entity['state'] ?? 'unknown', 'attributes' => $entity['attributes'] ?? [], 'labels' => $labels, 'area_id' => (string)($registryEntry['area_id'] ?? ''), 'card_type' => app_default_card_type($entityId), 'is_door_contact' => app_is_door_contact_entity($entity, $registryEntry), 'is_auto' => app_entity_has_auto_label($labels, $autoLabel) || isset($manualAuto[$entityId]), 'is_hidden' => (bool)$isHidden, ]; } return $items; } function app_entity_subtitle(array $entity, string $cardType): string { $state = (string)($entity['state'] ?? ''); $attr = $entity['attributes'] ?? []; return match ($cardType) { 'cover' => isset($attr['current_position']) ? 'Позиция ' . $attr['current_position'] . '%' : match ($state) { 'open' => 'Открыто', 'closed' => 'Закрыто', default => ucfirst($state), }, 'climate' => app_climate_subtitle($entity), default => app_generic_subtitle($state, $attr), }; } function app_generic_subtitle(string $state, array $attr): string { if ($state === '') { return 'Нет данных'; } if (isset($attr['current_temperature']) && is_numeric($attr['current_temperature'])) { return (string)$state . ' · ' . rtrim(rtrim((string)$attr['current_temperature'], '0'), '.') . '°'; } return match ($state) { 'on' => 'Включено', 'off' => 'Выключено', 'open' => 'Открыто', 'closed' => 'Закрыто', default => ucfirst($state), }; } function app_climate_subtitle(array $entity): string { $attr = $entity['attributes'] ?? []; $pieces = []; if (isset($attr['current_temperature']) && is_numeric($attr['current_temperature'])) { $pieces[] = rtrim(rtrim((string)$attr['current_temperature'], '0'), '.') . '°'; } if (isset($attr['temperature']) && is_numeric($attr['temperature'])) { $pieces[] = 'Цель ' . rtrim(rtrim((string)$attr['temperature'], '0'), '.') . '°'; } if (!empty($attr['hvac_action'])) { $pieces[] = (string)$attr['hvac_action']; } return $pieces ? implode(' · ', $pieces) : 'Климат'; } function app_room_active_count(array $items): int { $count = 0; foreach ($items as $item) { if (is_array($item) && app_is_active_entity($item)) { $count++; } } return $count; } function app_room_temperature_badge(array $items): ?string { foreach ($items as $item) { if (!is_array($item)) { continue; } $attr = $item['attributes'] ?? []; $candidate = $attr['current_temperature'] ?? $attr['temperature'] ?? null; if ($candidate === null || !is_numeric($candidate)) { continue; } return (string)round((float)$candidate) . '°'; } return null; } function app_main_entities(array $config, array $haData): array { $states = $haData['states'] ?? []; $registryMap = app_registry_map($haData['entity_registry'] ?? []); $autoLabel = (string)($config['home_assistant']['auto_label'] ?? 'auto'); $manualAuto = array_flip(array_map('strval', $config['home_assistant']['auto_entity_ids'] ?? [])); $items = []; foreach ($states as $entity) { $entityId = (string)($entity['entity_id'] ?? ''); if ($entityId === '') { continue; } $registryEntry = $registryMap[$entityId] ?? []; $labels = app_labels_from_entity($entity, $registryEntry); $isAuto = app_entity_has_auto_label($labels, $autoLabel) || isset($manualAuto[$entityId]); $isHidden = !empty($registryEntry['hidden_by']) || !empty($registryEntry['disabled_by']); $domain = app_entity_domain($entityId); $isDoorContact = app_is_door_contact_entity($entity, $registryEntry); if (!$isAuto || !app_is_active_entity($entity) || $isHidden) { continue; } if (!in_array($domain, ['light', 'switch', 'cover', 'fan', 'binary_sensor'], true)) { continue; } if ($domain === 'binary_sensor' && !$isDoorContact) { continue; } $cardType = app_default_card_type($entityId); $items[] = [ 'entity_id' => $entityId, 'domain' => $domain, 'name' => app_entity_name($entity, $registryEntry, []), 'icon' => app_entity_icon($entity, []), 'state' => $entity['state'] ?? 'unknown', 'attributes' => $entity['attributes'] ?? [], 'labels' => $labels, 'card_type' => $cardType, 'is_door_contact' => $isDoorContact, 'subtitle' => app_entity_subtitle($entity, $cardType), 'last_changed' => (string)($entity['last_changed'] ?? $entity['last_updated'] ?? ''), ]; } usort($items, static function (array $a, array $b): int { $timeA = strtotime((string)($a['last_changed'] ?? '')) ?: 0; $timeB = strtotime((string)($b['last_changed'] ?? '')) ?: 0; if ($timeA !== $timeB) { return $timeA <=> $timeB; } return strcasecmp($a['name'], $b['name']); }); return $items; } function app_weather_summary(?array $entity): ?array { if (!$entity) { return null; } $attr = $entity['attributes'] ?? []; return [ 'entity_id' => $entity['entity_id'] ?? null, 'name' => $attr['friendly_name'] ?? 'Погода', 'state' => $entity['state'] ?? null, 'temperature' => $attr['temperature'] ?? null, 'sensor_temperature' => null, 'wind_speed' => $attr['wind_speed'] ?? null, 'condition' => $attr['condition'] ?? ($entity['state'] ?? null), ]; } function app_find_state(array $states, string $entityId): ?array { foreach ($states as $entity) { if (($entity['entity_id'] ?? null) === $entityId) { return $entity; } } return null; } function app_room_summary(array $room, array $items, array $haData): array { $activeCount = app_room_active_count($items); $temperatureBadge = app_room_temperature_badge($items); if ($temperatureBadge === null) { $allItems = app_room_entities($room, $haData, true); $explicitTemperatureIds = []; foreach (($room['entity_overrides'] ?? []) as $entityId => $override) { if (!is_array($override)) { continue; } if (str_ends_with((string)$entityId, '_temperature') && !empty($override['visible'])) { $explicitTemperatureIds[] = (string)$entityId; } if (str_ends_with((string)$entityId, '_temperature') && array_key_exists('visible', $override) && $override['visible'] === false) { $explicitTemperatureIds[] = (string)$entityId; } } foreach ($explicitTemperatureIds as $temperatureEntityId) { foreach (($haData['states'] ?? []) as $entity) { if (($entity['entity_id'] ?? null) !== $temperatureEntityId) { continue; } $candidate = $entity['attributes']['current_temperature'] ?? $entity['attributes']['temperature'] ?? $entity['state'] ?? null; if ($candidate !== null && is_numeric($candidate)) { $temperatureBadge = (string)round((float)$candidate) . '°'; break 2; } } } if ($temperatureBadge === null) { $temperatureBadge = app_room_temperature_badge($allItems); } } return [ 'id' => $room['id'], 'name' => $room['name'] ?? $room['id'], 'icon' => $room['icon'] ?? 'mdi:home-variant', 'floor_id' => $room['floor_id'] ?? null, 'floor_name' => $room['floor_name'] ?? null, 'floor_level' => isset($room['floor_level']) ? (int)$room['floor_level'] : null, 'visible' => (bool)($room['visible'] ?? true), 'order' => (int)($room['order'] ?? 9999), 'entity_count' => $activeCount, 'active_entity_count' => $activeCount, 'temperature_badge' => $temperatureBadge, ]; } function app_build_snapshot(array $config, HomeAssistantClient $client, ?string $selectedRoomId = null): array { $haData = $client->fetchSnapshotData(); $demo = $client->isDemo(); $editMode = (bool)($config['app']['edit_mode'] ?? false); $rooms = app_room_definitions($config, $haData); $mainRoom = [ 'id' => 'main', 'name' => (string)($config['app']['main_room_name'] ?? 'Главная'), 'icon' => (string)($config['app']['main_room_icon'] ?? 'mdi:home'), 'visible' => true, 'entity_count' => 0, ]; $roomSummaries = [$mainRoom]; $roomViews = []; $spaceIndex = []; $spaceEntities = []; foreach ($rooms as $room) { $entities = app_room_entities($room, $haData, $editMode); $summary = app_room_summary($room, $entities, $haData); $roomSummaries[] = $summary; $roomViews[$room['id']] = [ 'id' => $room['id'], 'name' => $room['name'] ?? $room['id'], 'icon' => $room['icon'] ?? 'mdi:home-variant', 'visible' => (bool)($room['visible'] ?? true), 'order' => (int)($room['order'] ?? 9999), 'entities' => $entities, 'entity_count' => $summary['entity_count'], 'active_entity_count' => $summary['active_entity_count'], 'temperature_badge' => $summary['temperature_badge'], ]; $spaceIndex[$room['id']] = $roomViews[$room['id']]; $spaceEntities[$room['id']] = $entities; } $states = $haData['states'] ?? []; $entityIndex = app_entity_index($config, $haData); $mainEntities = app_main_entities($config, $haData); $mainActiveCount = count($mainEntities); $mainRoom['entity_count'] = $mainActiveCount; $mainRoom['active_entity_count'] = $mainActiveCount; $mainRoom['temperature_badge'] = null; $roomSummaries[0] = $mainRoom; $weatherEntity = app_select_weather_entity($states, $config); $weather = app_weather_summary($weatherEntity); $weatherSensor = app_find_state($states, 'sensor.weather_temperature'); if ($weather !== null) { $weather['sensor_temperature'] = $weatherSensor['state'] ?? null; } $selectedRoomId = $selectedRoomId ?: 'main'; if (!isset($roomViews[$selectedRoomId]) && $selectedRoomId !== 'main') { $selectedRoomId = 'main'; } $selectedRoom = $selectedRoomId === 'main' ? [ 'id' => 'main', 'name' => $mainRoom['name'], 'icon' => $mainRoom['icon'], 'visible' => true, 'entities' => $mainEntities, 'entity_count' => count($mainEntities), 'active_entity_count' => count($mainEntities), 'temperature_badge' => null, ] : ($spaceIndex[$selectedRoomId] ?? $roomViews[$selectedRoomId]); if ($selectedRoomId === 'main') { $selectedRoom['weather'] = $weather; } $popup = app_clear_expired_popup_state($config); $camera = $config['camera'] ?? []; return [ 'ok' => true, 'demo' => $demo, 'server_time' => time(), 'settings' => [ 'title' => $config['app']['title'] ?? 'Wall Panel', 'poll_interval_ms' => (int)($config['app']['poll_interval_ms'] ?? 5000), 'edit_mode' => (bool)($config['app']['edit_mode'] ?? false), 'main_room_name' => $mainRoom['name'], 'ha_connection' => [ 'base_url' => (string)($config['home_assistant']['base_url'] ?? ''), 'token' => (string)($config['home_assistant']['token'] ?? ''), 'verify_ssl' => (bool)($config['home_assistant']['verify_ssl'] ?? true), ], 'camera' => [ 'poster_url' => (string)($camera['poster_url'] ?? ''), 'stream_url' => (string)($camera['stream_url'] ?? ''), 'stream_mode' => (string)($camera['stream_mode'] ?? 'hls'), 'popup_timeout_minutes' => (int)($camera['popup_timeout_minutes'] ?? 3), 'trigger_entities' => app_trigger_entities($config), ], 'main_weather_actions' => app_main_weather_actions($config), 'main_boiler' => app_main_boiler($config), 'main_print' => app_main_print($config), ], 'spaces' => $roomSummaries, 'selected_space' => $selectedRoom, 'space_index' => $spaceIndex, 'space_entities' => $spaceEntities, 'entity_index' => $entityIndex, 'weather' => $weather, 'main_entities' => $mainEntities, 'popup' => [ 'active' => (bool)($popup['active'] ?? false), 'sensor_entity_id' => $popup['sensor_entity_id'] ?? null, 'opened_at' => $popup['opened_at'] ?? null, 'expires_at' => $popup['expires_at'] ?? null, 'poster_url' => (string)($camera['poster_url'] ?? ''), 'stream_url' => (string)($camera['stream_url'] ?? ''), 'stream_mode' => (string)($camera['stream_mode'] ?? 'hls'), 'title' => 'Камера', ], 'rooms' => $roomSummaries, 'selected_room' => $selectedRoom, ]; } function app_update_entity_override(array $config, string $roomId, string $entityId, array $patch): array { $rooms = $config['rooms'] ?? []; if (!is_array($rooms)) { $rooms = []; } $found = false; foreach ($rooms as &$room) { if (!is_array($room) || (string)($room['id'] ?? '') !== $roomId) { continue; } $room['entity_overrides'] = is_array($room['entity_overrides'] ?? null) ? $room['entity_overrides'] : []; $current = is_array($room['entity_overrides'][$entityId] ?? null) ? $room['entity_overrides'][$entityId] : []; $room['entity_overrides'][$entityId] = array_replace_recursive($current, array_filter($patch, static fn($value) => $value !== null)); $found = true; break; } unset($room); if (!$found) { $newRoom = [ 'id' => $roomId, 'visible' => true, 'entity_ids' => [], 'entity_overrides' => [ $entityId => array_filter($patch, static fn($value) => $value !== null), ], ]; if (array_key_exists('name', $patch) && trim((string)$patch['name']) !== '') { $newRoom['name'] = (string)$patch['name']; } if (array_key_exists('icon', $patch) && trim((string)$patch['icon']) !== '') { $newRoom['icon'] = (string)$patch['icon']; } $rooms[] = [ ...$newRoom, ]; } $config['rooms'] = $rooms; app_save_config($config); return $config; } function app_update_room_override(array $config, string $roomId, array $patch): array { $rooms = $config['rooms'] ?? []; if (!is_array($rooms)) { $rooms = []; } $found = false; foreach ($rooms as &$room) { if (!is_array($room) || (string)($room['id'] ?? '') !== $roomId) { continue; } foreach (['visible', 'order', 'name', 'icon'] as $key) { if (array_key_exists($key, $patch) && $patch[$key] !== null) { $room[$key] = $key === 'visible' ? (bool)$patch[$key] : ($key === 'order' ? (int)$patch[$key] : (string)$patch[$key]); } } if (!isset($room['entity_ids']) || !is_array($room['entity_ids'])) { $room['entity_ids'] = []; } if (!isset($room['entity_overrides']) || !is_array($room['entity_overrides'])) { $room['entity_overrides'] = []; } $found = true; break; } unset($room); if (!$found) { $newRoom = [ 'id' => $roomId, 'visible' => array_key_exists('visible', $patch) ? (bool)$patch['visible'] : true, 'order' => array_key_exists('order', $patch) ? (int)$patch['order'] : 9999, 'entity_ids' => [], 'entity_overrides' => [], ]; if (array_key_exists('name', $patch) && trim((string)$patch['name']) !== '') { $newRoom['name'] = (string)$patch['name']; } if (array_key_exists('icon', $patch) && trim((string)$patch['icon']) !== '') { $newRoom['icon'] = (string)$patch['icon']; } $rooms[] = $newRoom; } $config['rooms'] = $rooms; app_save_config($config); return $config; } function app_handle_popup_event(array $config, array $payload): array { $camera = $config['camera'] ?? []; $watch = app_trigger_entities($config); $sensor = (string)($payload['sensor_entity_id'] ?? $payload['entity_id'] ?? ''); $state = strtolower((string)($payload['state'] ?? $payload['to'] ?? '')); $command = strtolower((string)($payload['command'] ?? '')); $popupState = app_load_popup_state(); $timeoutMinutes = max(1, (int)($camera['popup_timeout_minutes'] ?? 3)); $closeDelaySeconds = app_popup_close_delay_seconds(); $timeoutSeconds = max($closeDelaySeconds, $timeoutMinutes * 60); if ($command === 'open') { $popupState = [ 'active' => true, 'sensor_entity_id' => $sensor !== '' ? $sensor : 'debug', 'opened_at' => time(), 'expires_at' => time() + $timeoutSeconds, ]; app_save_popup_state($popupState); } elseif (in_array($sensor, $watch, true) && $state === 'on') { $popupState = [ 'active' => true, 'sensor_entity_id' => $sensor, 'opened_at' => time(), 'expires_at' => null, ]; app_save_popup_state($popupState); } elseif ($state === 'off' && ($popupState['sensor_entity_id'] ?? null) === $sensor && ($popupState['active'] ?? false)) { $popupState['active'] = true; $popupState['expires_at'] = time() + $closeDelaySeconds; if (empty($popupState['opened_at'])) { $popupState['opened_at'] = time(); } app_save_popup_state($popupState); } elseif (($payload['command'] ?? '') === 'close') { $popupState['active'] = false; $popupState['expires_at'] = null; app_save_popup_state($popupState); } return $popupState; } function app_service_for_entity(string $entityId, string $command): array { $domain = app_entity_domain($entityId); return match ($command) { 'toggle' => [$domain, 'toggle', ['entity_id' => $entityId]], 'turn_on' => [$domain, 'turn_on', ['entity_id' => $entityId]], 'turn_off' => [$domain, 'turn_off', ['entity_id' => $entityId]], 'open' => ['cover', 'open_cover', ['entity_id' => $entityId]], 'close' => ['cover', 'close_cover', ['entity_id' => $entityId]], 'stop' => ['cover', 'stop_cover', ['entity_id' => $entityId]], 'set_position' => ['cover', 'set_cover_position', ['entity_id' => $entityId]], 'set_temperature' => ['climate', 'set_temperature', ['entity_id' => $entityId]], 'set_hvac_mode' => ['climate', 'set_hvac_mode', ['entity_id' => $entityId]], 'set_fan_mode' => ['climate', 'set_fan_mode', ['entity_id' => $entityId]], 'set_swing_mode' => ['climate', 'set_swing_mode', ['entity_id' => $entityId]], 'set_preset_mode' => ['climate', 'set_preset_mode', ['entity_id' => $entityId]], default => [$domain, $command, ['entity_id' => $entityId]], }; }