1064 lines
37 KiB
PHP
Executable File
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]],
|
|
};
|
|
}
|