wallpanell/lib/dashboard.php
2026-03-19 21:27:01 +03:00

1064 lines
37 KiB
PHP
Executable File

<?php
declare(strict_types=1);
function app_is_placeholder(string $value): bool
{
$value = trim($value);
return $value === '' || str_contains($value, 'PASTE_') || str_contains($value, 'YOUR_');
}
function app_trigger_entities(array $config): array
{
return array_values(array_filter(array_map('strval', $config['camera']['trigger_entities'] ?? [])));
}
function app_main_weather_actions(array $config): array
{
$items = $config['app']['main_weather_actions'] ?? [];
if (!is_array($items)) {
return [];
}
$actions = [];
foreach ($items as $item) {
if (!is_array($item)) {
continue;
}
$entityId = trim((string)($item['entity_id'] ?? ''));
if ($entityId === '') {
continue;
}
$actions[] = [
'entity_id' => $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]],
};
}