-
This commit is contained in:
parent
b5d3feb726
commit
03f21e2de8
@ -2115,6 +2115,11 @@ body.is-mobile-ui #camera-modal {
|
|||||||
max-width: none;
|
max-width: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.main-dashboard__cards .main-dashboard__room-grid {
|
||||||
|
grid-column: 1 / -1;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
.room-entities-section {
|
.room-entities-section {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 14px;
|
gap: 14px;
|
||||||
|
|||||||
485
assets/app.js
485
assets/app.js
@ -42,20 +42,210 @@
|
|||||||
haSubscribeId: 1,
|
haSubscribeId: 1,
|
||||||
roomSelectionToken: 0,
|
roomSelectionToken: 0,
|
||||||
snapshotPollTimer: null,
|
snapshotPollTimer: null,
|
||||||
|
haSnapshotListenerInstalled: false,
|
||||||
|
debugLastRenderSignature: '',
|
||||||
};
|
};
|
||||||
|
|
||||||
const els = {};
|
const els = {};
|
||||||
|
const client = window.StrikerPanelClient || (window.StrikerPanelClient = {});
|
||||||
|
const debugEnabled = (() => {
|
||||||
|
try {
|
||||||
|
if (window.StrikerPanelDebug) return true;
|
||||||
|
const search = new URLSearchParams(window.location.search);
|
||||||
|
if (['1', 'true', 'yes', 'on'].includes(String(search.get('wp_debug') || '').toLowerCase())) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
const stored = window.localStorage?.getItem('striker-panel-debug');
|
||||||
|
if (['1', 'true', 'yes', 'on'].includes(String(stored || '').toLowerCase())) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
return Boolean(window.StrikerPanelDebug);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
})();
|
||||||
|
const debugLog = (...args) => {
|
||||||
|
if (debugEnabled) {
|
||||||
|
console.log('[Striker Panel]', ...args);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const debugWarn = (...args) => {
|
||||||
|
if (debugEnabled) {
|
||||||
|
console.warn('[Striker Panel]', ...args);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
client.renderFromSnapshot = (snapshot) => {
|
||||||
|
if (!snapshot || typeof snapshot !== 'object') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
state.snapshot = snapshot;
|
||||||
|
initRefs();
|
||||||
|
state.embedMode = detectEmbeddedContext();
|
||||||
|
syncLayoutState();
|
||||||
|
render();
|
||||||
|
};
|
||||||
|
|
||||||
|
client.refresh = () => {
|
||||||
|
initRefs();
|
||||||
|
state.embedMode = detectEmbeddedContext();
|
||||||
|
syncLayoutState();
|
||||||
|
render();
|
||||||
|
};
|
||||||
|
|
||||||
function $(id) {
|
function $(id) {
|
||||||
return document.getElementById(id);
|
const root = client.mountRoot || document;
|
||||||
|
if (!root) return null;
|
||||||
|
if (typeof root.getElementById === 'function') {
|
||||||
|
return root.getElementById(id);
|
||||||
|
}
|
||||||
|
return root.querySelector?.(`#${CSS.escape(id)}`) || null;
|
||||||
}
|
}
|
||||||
|
|
||||||
function q(sel, root = document) {
|
function q(sel, root) {
|
||||||
return root.querySelector(sel);
|
const actualRoot = root || client.mountRoot || document;
|
||||||
|
return actualRoot.querySelector(sel);
|
||||||
}
|
}
|
||||||
|
|
||||||
function qa(sel, root = document) {
|
function qa(sel, root) {
|
||||||
return Array.from(root.querySelectorAll(sel));
|
const actualRoot = root || client.mountRoot || document;
|
||||||
|
return Array.from(actualRoot.querySelectorAll(sel));
|
||||||
|
}
|
||||||
|
|
||||||
|
function haBridge() {
|
||||||
|
return window.WALL_PANEL_HA_BRIDGE || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isHaRuntime() {
|
||||||
|
return Boolean(
|
||||||
|
haBridge()
|
||||||
|
|| bootstrap?.ui?.mode === 'ha-native'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function sleep(ms) {
|
||||||
|
return new Promise((resolve) => window.setTimeout(resolve, Math.max(0, Number(ms) || 0)));
|
||||||
|
}
|
||||||
|
|
||||||
|
function snapshotLooksReady(snapshot) {
|
||||||
|
if (!snapshot || typeof snapshot !== 'object') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const rooms = Array.isArray(snapshot.rooms) ? snapshot.rooms : [];
|
||||||
|
const spaces = Array.isArray(snapshot.spaces) ? snapshot.spaces : [];
|
||||||
|
if (rooms.length > 0 || spaces.length > 0) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (snapshot.selected_room?.id || snapshot.selected_space?.id) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function snapshotSummary(snapshot) {
|
||||||
|
const selectedRoom = snapshot?.selected_room || snapshot?.selected_space || null;
|
||||||
|
const rooms = Array.isArray(snapshot?.rooms) ? snapshot.rooms : Array.isArray(snapshot?.spaces) ? snapshot.spaces : [];
|
||||||
|
const mainEntities = Array.isArray(snapshot?.main_entities) ? snapshot.main_entities : [];
|
||||||
|
const selectedEntities = Array.isArray(selectedRoom?.entities) ? selectedRoom.entities : [];
|
||||||
|
const popup = snapshot?.popup || {};
|
||||||
|
return {
|
||||||
|
mode: snapshot?.ui?.mode || 'unknown',
|
||||||
|
selected_room_id: selectedRoom?.id || null,
|
||||||
|
selected_room_name: selectedRoom?.name || null,
|
||||||
|
rooms: rooms.length,
|
||||||
|
main_entities: mainEntities.length,
|
||||||
|
selected_room_entities: selectedEntities.length,
|
||||||
|
popup_active: Boolean(popup.active),
|
||||||
|
popup_sensor: popup.sensor_entity_id || null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function requestSummary(action, params = {}) {
|
||||||
|
const summary = { action };
|
||||||
|
['space_id', 'room_id', 'entity_id', 'layout_item_id', 'command', 'value', 'hours', 'state', 'edit_mode'].forEach((key) => {
|
||||||
|
if (params[key] !== undefined && params[key] !== null && params[key] !== '') {
|
||||||
|
summary[key] = params[key];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (params.payload && typeof params.payload === 'object') {
|
||||||
|
summary.payload_keys = Object.keys(params.payload);
|
||||||
|
}
|
||||||
|
return summary;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function resolveInitialSnapshot() {
|
||||||
|
const bootstrapSnapshot = window.APP_BOOTSTRAP || bootstrap || {};
|
||||||
|
debugLog('resolveInitialSnapshot()', {
|
||||||
|
ha_runtime: isHaRuntime(),
|
||||||
|
bridge_ready: Boolean(haBridge()),
|
||||||
|
bootstrap_ready: snapshotLooksReady(bootstrapSnapshot),
|
||||||
|
});
|
||||||
|
if (!isHaRuntime()) {
|
||||||
|
debugLog('resolveInitialSnapshot -> bootstrap (standalone)', snapshotSummary(bootstrapSnapshot));
|
||||||
|
return bootstrapSnapshot;
|
||||||
|
}
|
||||||
|
|
||||||
|
const waitForHaBridge = async (timeoutMs = 1000) => {
|
||||||
|
const startedAt = Date.now();
|
||||||
|
let bridge = haBridge();
|
||||||
|
while (!bridge && (Date.now() - startedAt) < timeoutMs) {
|
||||||
|
await sleep(50);
|
||||||
|
bridge = haBridge();
|
||||||
|
}
|
||||||
|
return bridge;
|
||||||
|
};
|
||||||
|
|
||||||
|
const tryBridgeSnapshot = async () => {
|
||||||
|
const bridge = await waitForHaBridge();
|
||||||
|
if (!bridge) {
|
||||||
|
debugLog('HA bridge not ready yet');
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const roomId = state.selectedRoomId || 'main';
|
||||||
|
try {
|
||||||
|
if (typeof bridge.getSnapshot === 'function') {
|
||||||
|
debugLog('request initial snapshot via bridge.getSnapshot()', { room_id: roomId });
|
||||||
|
const snapshot = await bridge.getSnapshot(roomId);
|
||||||
|
if (snapshotLooksReady(snapshot)) {
|
||||||
|
debugLog('initial snapshot received via bridge.getSnapshot()', snapshotSummary(snapshot));
|
||||||
|
return snapshot;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
debugWarn('initial snapshot bridge.getSnapshot() failed', error);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (typeof bridge.request === 'function') {
|
||||||
|
debugLog('request initial snapshot via bridge.request(GET snapshot)', { room_id: roomId });
|
||||||
|
const snapshot = await bridge.request('GET', 'snapshot', { space_id: roomId });
|
||||||
|
if (snapshotLooksReady(snapshot)) {
|
||||||
|
debugLog('initial snapshot received via bridge.request(GET snapshot)', snapshotSummary(snapshot));
|
||||||
|
return snapshot;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
debugWarn('initial snapshot bridge.request(GET snapshot) failed', error);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const firstPass = await tryBridgeSnapshot();
|
||||||
|
if (firstPass) {
|
||||||
|
return firstPass;
|
||||||
|
}
|
||||||
|
|
||||||
|
await sleep(150);
|
||||||
|
|
||||||
|
const secondPass = await tryBridgeSnapshot();
|
||||||
|
if (secondPass) {
|
||||||
|
return secondPass;
|
||||||
|
}
|
||||||
|
|
||||||
|
debugLog('resolveInitialSnapshot -> fallback bootstrap', snapshotSummary(bootstrapSnapshot));
|
||||||
|
return snapshotLooksReady(bootstrapSnapshot) ? bootstrapSnapshot : bootstrapSnapshot;
|
||||||
}
|
}
|
||||||
|
|
||||||
const PRESSABLE_SELECTOR = [
|
const PRESSABLE_SELECTOR = [
|
||||||
@ -198,6 +388,7 @@
|
|||||||
|
|
||||||
const wrap = document.createElement('span');
|
const wrap = document.createElement('span');
|
||||||
wrap.className = 'icon-node';
|
wrap.className = 'icon-node';
|
||||||
|
wrap.appendChild(createIconElement(fallback));
|
||||||
Promise.resolve(getIcon(name)).then((definition) => {
|
Promise.resolve(getIcon(name)).then((definition) => {
|
||||||
if (!definition || !wrap.isConnected) return;
|
if (!definition || !wrap.isConnected) return;
|
||||||
wrap.replaceChildren(createSvgIcon(definition));
|
wrap.replaceChildren(createSvgIcon(definition));
|
||||||
@ -229,6 +420,7 @@
|
|||||||
: source.replace(/^fab:/, 'fa-brands:');
|
: source.replace(/^fab:/, 'fa-brands:');
|
||||||
const wrap = document.createElement('span');
|
const wrap = document.createElement('span');
|
||||||
wrap.className = 'icon-node';
|
wrap.className = 'icon-node';
|
||||||
|
wrap.appendChild(createIconElement(fallback));
|
||||||
const img = document.createElement('img');
|
const img = document.createElement('img');
|
||||||
img.className = 'icon-node__img';
|
img.className = 'icon-node__img';
|
||||||
img.alt = '';
|
img.alt = '';
|
||||||
@ -236,6 +428,10 @@
|
|||||||
img.loading = 'lazy';
|
img.loading = 'lazy';
|
||||||
img.referrerPolicy = 'no-referrer';
|
img.referrerPolicy = 'no-referrer';
|
||||||
img.src = `https://api.iconify.design/${mappedSource}.svg`;
|
img.src = `https://api.iconify.design/${mappedSource}.svg`;
|
||||||
|
img.addEventListener('load', () => {
|
||||||
|
if (!img.isConnected || !wrap.isConnected) return;
|
||||||
|
wrap.replaceChildren(img);
|
||||||
|
});
|
||||||
img.addEventListener('error', () => {
|
img.addEventListener('error', () => {
|
||||||
if (img.dataset.fallbackApplied === '1') return;
|
if (img.dataset.fallbackApplied === '1') return;
|
||||||
img.dataset.fallbackApplied = '1';
|
img.dataset.fallbackApplied = '1';
|
||||||
@ -252,6 +448,7 @@
|
|||||||
|
|
||||||
const wrap = document.createElement('span');
|
const wrap = document.createElement('span');
|
||||||
wrap.className = 'icon-node';
|
wrap.className = 'icon-node';
|
||||||
|
wrap.appendChild(createIconElement(fallback));
|
||||||
|
|
||||||
if (source.includes(':')) {
|
if (source.includes(':')) {
|
||||||
const img = document.createElement('img');
|
const img = document.createElement('img');
|
||||||
@ -261,6 +458,10 @@
|
|||||||
img.loading = 'lazy';
|
img.loading = 'lazy';
|
||||||
img.referrerPolicy = 'no-referrer';
|
img.referrerPolicy = 'no-referrer';
|
||||||
img.src = `https://api.iconify.design/${source}.svg`;
|
img.src = `https://api.iconify.design/${source}.svg`;
|
||||||
|
img.addEventListener('load', () => {
|
||||||
|
if (!img.isConnected || !wrap.isConnected) return;
|
||||||
|
wrap.replaceChildren(img);
|
||||||
|
});
|
||||||
img.addEventListener('error', () => {
|
img.addEventListener('error', () => {
|
||||||
if (img.dataset.fallbackApplied === '1') return;
|
if (img.dataset.fallbackApplied === '1') return;
|
||||||
img.dataset.fallbackApplied = '1';
|
img.dataset.fallbackApplied = '1';
|
||||||
@ -448,6 +649,9 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function buildUrl(action, params = {}) {
|
function buildUrl(action, params = {}) {
|
||||||
|
if (isHaRuntime()) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
const url = new URL('api.php', window.location.href);
|
const url = new URL('api.php', window.location.href);
|
||||||
url.searchParams.set('action', action);
|
url.searchParams.set('action', action);
|
||||||
Object.entries(params).forEach(([key, value]) => {
|
Object.entries(params).forEach(([key, value]) => {
|
||||||
@ -463,6 +667,12 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function apiGet(action, params = {}) {
|
async function apiGet(action, params = {}) {
|
||||||
|
const bridge = haBridge();
|
||||||
|
if (bridge?.request) {
|
||||||
|
console.log('[Striker Panel]', 'apiGet via bridge', requestSummary(action, params));
|
||||||
|
return bridge.request('GET', action, params);
|
||||||
|
}
|
||||||
|
console.log('[Striker Panel]', 'apiGet via http', requestSummary(action, params));
|
||||||
const res = await fetch(buildUrl(action, params), {
|
const res = await fetch(buildUrl(action, params), {
|
||||||
headers: { Accept: 'application/json' },
|
headers: { Accept: 'application/json' },
|
||||||
cache: 'no-store',
|
cache: 'no-store',
|
||||||
@ -474,6 +684,12 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function apiPost(action, payload = {}) {
|
async function apiPost(action, payload = {}) {
|
||||||
|
const bridge = haBridge();
|
||||||
|
if (bridge?.request) {
|
||||||
|
console.log('[Striker Panel]', 'apiPost via bridge', requestSummary(action, payload));
|
||||||
|
return bridge.request('POST', action, payload);
|
||||||
|
}
|
||||||
|
console.log('[Striker Panel]', 'apiPost via http', requestSummary(action, payload));
|
||||||
const res = await fetch(buildUrl(action), {
|
const res = await fetch(buildUrl(action), {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
|
headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
|
||||||
@ -487,7 +703,11 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function fetchSnapshot(roomId = state.selectedRoomId || 'main') {
|
async function fetchSnapshot(roomId = state.selectedRoomId || 'main') {
|
||||||
return apiGet('snapshot', { space_id: roomId || 'main' });
|
const response = await apiGet('snapshot', { space_id: roomId || 'main' });
|
||||||
|
if (response && response.ok === true && response.selected_room) {
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
return response;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadSnapshot(roomId = state.selectedRoomId || 'main') {
|
async function loadSnapshot(roomId = state.selectedRoomId || 'main') {
|
||||||
@ -1004,6 +1224,87 @@
|
|||||||
return q('.main-dashboard__cards', els.dashboardSurface);
|
return q('.main-dashboard__cards', els.dashboardSurface);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function renderMainRoomSummaryGrid(snapshot) {
|
||||||
|
const rooms = Array.isArray(snapshot?.spaces) ? snapshot.spaces : Array.isArray(snapshot?.rooms) ? snapshot.rooms : [];
|
||||||
|
const batteryRoom = snapshot?.battery_room || null;
|
||||||
|
const cards = document.createElement('div');
|
||||||
|
cards.className = 'room-list__group main-dashboard__room-grid';
|
||||||
|
|
||||||
|
const roomCard = (room, options = {}) => {
|
||||||
|
if (!room) return null;
|
||||||
|
const card = document.createElement('div');
|
||||||
|
card.className = `room-item ${room.id === state.selectedRoomId ? 'is-selected' : ''} ${room.id === 'main' ? 'is-main' : ''} ${room.virtual ? 'is-virtual is-battery-room' : ''} ${options.hidden ? 'is-hidden-room' : ''}`.trim();
|
||||||
|
card.dataset.roomId = room.id;
|
||||||
|
card.tabIndex = 0;
|
||||||
|
card.setAttribute('role', 'button');
|
||||||
|
card.addEventListener('click', () => setSelectedRoom(room.id));
|
||||||
|
card.addEventListener('keydown', (event) => {
|
||||||
|
if (event.key === 'Enter' || event.key === ' ') {
|
||||||
|
event.preventDefault();
|
||||||
|
setSelectedRoom(room.id);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const content = document.createElement('div');
|
||||||
|
content.className = 'room-item__content';
|
||||||
|
|
||||||
|
const icon = document.createElement('div');
|
||||||
|
icon.className = 'room-item__icon';
|
||||||
|
icon.appendChild(createIconElement(room.icon || 'mdi:home-variant'));
|
||||||
|
|
||||||
|
const body = document.createElement('div');
|
||||||
|
body.className = 'room-item__body';
|
||||||
|
const activeCount = room.id === 'batteries'
|
||||||
|
? Number(room.problem_count ?? room.active_entity_count ?? room.entity_count ?? 0) || 0
|
||||||
|
: Number(room.active_entity_count ?? room.entity_count ?? 0) || 0;
|
||||||
|
const metaText = room.id === 'main'
|
||||||
|
? 'Главный экран'
|
||||||
|
: room.id === 'batteries'
|
||||||
|
? (room.battery_summary_text || `${room.entity_count || 0} батареек`)
|
||||||
|
: activeCount > 0
|
||||||
|
? `${activeCount} ${pluralizeActiveEntities(activeCount)}`
|
||||||
|
: 'Нет активных';
|
||||||
|
body.innerHTML = `
|
||||||
|
<div class="room-item__name">${esc(room.name || '')}</div>
|
||||||
|
<div class="room-item__meta">${esc(metaText)}</div>
|
||||||
|
`;
|
||||||
|
content.append(icon, body);
|
||||||
|
|
||||||
|
const tempBadge = roomTemperatureBadge(snapshot, room);
|
||||||
|
if (tempBadge) {
|
||||||
|
card.classList.add('has-temp');
|
||||||
|
const temp = document.createElement('div');
|
||||||
|
temp.className = 'room-item__temp';
|
||||||
|
temp.textContent = tempBadge;
|
||||||
|
card.appendChild(temp);
|
||||||
|
}
|
||||||
|
|
||||||
|
card.append(content);
|
||||||
|
return card;
|
||||||
|
};
|
||||||
|
|
||||||
|
const orderedRooms = [...rooms]
|
||||||
|
.filter((room) => room && room.id !== 'main' && room.visible !== false && room.id !== 'batteries')
|
||||||
|
.sort((left, right) => {
|
||||||
|
const leftOrder = Number(left.order ?? 9999);
|
||||||
|
const rightOrder = Number(right.order ?? 9999);
|
||||||
|
if (leftOrder !== rightOrder) return leftOrder - rightOrder;
|
||||||
|
return String(left.name || '').localeCompare(String(right.name || ''), 'ru');
|
||||||
|
});
|
||||||
|
|
||||||
|
orderedRooms.forEach((room) => {
|
||||||
|
const card = roomCard(room);
|
||||||
|
if (card) cards.appendChild(card);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (batteryRoom && !isMobileViewport()) {
|
||||||
|
const card = roomCard(batteryRoom);
|
||||||
|
if (card) cards.appendChild(card);
|
||||||
|
}
|
||||||
|
|
||||||
|
return cards;
|
||||||
|
}
|
||||||
|
|
||||||
function currentDashboardCardsContainer() {
|
function currentDashboardCardsContainer() {
|
||||||
const snapshot = state.snapshot || bootstrap;
|
const snapshot = state.snapshot || bootstrap;
|
||||||
const room = snapshot.selected_space || snapshot.selected_room || {};
|
const room = snapshot.selected_space || snapshot.selected_room || {};
|
||||||
@ -1613,10 +1914,16 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function haConnection() {
|
function haConnection() {
|
||||||
|
if (isHaRuntime()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
return state.snapshot?.settings?.ha_connection || bootstrap?.settings?.ha_connection || {};
|
return state.snapshot?.settings?.ha_connection || bootstrap?.settings?.ha_connection || {};
|
||||||
}
|
}
|
||||||
|
|
||||||
function haWsUrl(baseUrl) {
|
function haWsUrl(baseUrl) {
|
||||||
|
if (isHaRuntime()) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
if (!baseUrl) return '';
|
if (!baseUrl) return '';
|
||||||
try {
|
try {
|
||||||
const url = new URL(baseUrl);
|
const url = new URL(baseUrl);
|
||||||
@ -3342,6 +3649,9 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function renderRoomButtons(snapshot, rooms, batteryRoom = null) {
|
function renderRoomButtons(snapshot, rooms, batteryRoom = null) {
|
||||||
|
if (!els.roomList) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
els.roomList.innerHTML = '';
|
els.roomList.innerHTML = '';
|
||||||
const sortedRooms = [...(rooms || [])].sort((left, right) => {
|
const sortedRooms = [...(rooms || [])].sort((left, right) => {
|
||||||
if (left.id === 'main') return -1;
|
if (left.id === 'main') return -1;
|
||||||
@ -3466,6 +3776,11 @@
|
|||||||
|
|
||||||
function renderSelectedRoom(snapshot) {
|
function renderSelectedRoom(snapshot) {
|
||||||
const room = snapshot.selected_space || snapshot.selected_room || {};
|
const room = snapshot.selected_space || snapshot.selected_room || {};
|
||||||
|
const setText = (el, value) => {
|
||||||
|
if (el) {
|
||||||
|
el.textContent = value;
|
||||||
|
}
|
||||||
|
};
|
||||||
if (els.contentTop) {
|
if (els.contentTop) {
|
||||||
els.contentTop.classList.toggle('is-main', room.id === 'main');
|
els.contentTop.classList.toggle('is-main', room.id === 'main');
|
||||||
}
|
}
|
||||||
@ -3474,8 +3789,8 @@
|
|||||||
}
|
}
|
||||||
updateMainPrintStrip(snapshot);
|
updateMainPrintStrip(snapshot);
|
||||||
if (room.id === 'batteries') {
|
if (room.id === 'batteries') {
|
||||||
els.selectedRoomEyebrow.textContent = 'Псевдо-комната';
|
setText(els.selectedRoomEyebrow, 'Псевдо-комната');
|
||||||
els.selectedRoomTitle.textContent = room.name || 'Батарейки';
|
setText(els.selectedRoomTitle, room.name || 'Батарейки');
|
||||||
const total = Number(room.entity_count ?? 0) || 0;
|
const total = Number(room.entity_count ?? 0) || 0;
|
||||||
const critical = Number(room.problem_count ?? room.active_entity_count ?? 0) || 0;
|
const critical = Number(room.problem_count ?? room.active_entity_count ?? 0) || 0;
|
||||||
const unavailable = Number(room.unavailable_count ?? 0) || 0;
|
const unavailable = Number(room.unavailable_count ?? 0) || 0;
|
||||||
@ -3490,26 +3805,26 @@
|
|||||||
if (unknown > 0) {
|
if (unknown > 0) {
|
||||||
summaryParts.push(`${unknown} ${pluralizeRu(unknown, 'неизвестная', 'неизвестных', 'неизвестных')}`);
|
summaryParts.push(`${unknown} ${pluralizeRu(unknown, 'неизвестная', 'неизвестных', 'неизвестных')}`);
|
||||||
}
|
}
|
||||||
els.selectedRoomMeta.textContent = summaryParts.length
|
setText(els.selectedRoomMeta, summaryParts.length
|
||||||
? `${summaryParts.join(' · ')} · ${total} ${pluralizeRu(total, 'батарейка', 'батарейки', 'батареек')}`
|
? `${summaryParts.join(' · ')} · ${total} ${pluralizeRu(total, 'батарейка', 'батарейки', 'батареек')}`
|
||||||
: `${total} ${pluralizeRu(total, 'батарейка', 'батарейки', 'батареек')}`;
|
: `${total} ${pluralizeRu(total, 'батарейка', 'батарейки', 'батареек')}`);
|
||||||
renderSelectedRoomActions(snapshot);
|
renderSelectedRoomActions(snapshot);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (room.id !== 'main') {
|
if (room.id !== 'main') {
|
||||||
els.selectedRoomEyebrow.textContent = 'Пространство';
|
setText(els.selectedRoomEyebrow, 'Пространство');
|
||||||
els.selectedRoomTitle.textContent = room.name || 'Панель';
|
setText(els.selectedRoomTitle, room.name || 'Панель');
|
||||||
const entities = roomEntities(snapshot, room.id || 'main');
|
const entities = roomEntities(snapshot, room.id || 'main');
|
||||||
const activeCount = Number(room.active_entity_count ?? entities.length) || 0;
|
const activeCount = Number(room.active_entity_count ?? entities.length) || 0;
|
||||||
els.selectedRoomMeta.textContent = `${activeCount} ${pluralizeActiveEntities(activeCount)}`;
|
setText(els.selectedRoomMeta, `${activeCount} ${pluralizeActiveEntities(activeCount)}`);
|
||||||
renderSelectedRoomActions(snapshot);
|
renderSelectedRoomActions(snapshot);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const entities = roomEntities(snapshot, room.id || 'main');
|
const entities = roomEntities(snapshot, room.id || 'main');
|
||||||
els.selectedRoomEyebrow.textContent = '';
|
setText(els.selectedRoomEyebrow, '');
|
||||||
els.selectedRoomTitle.textContent = room.name || 'Панель';
|
setText(els.selectedRoomTitle, room.name || 'Панель');
|
||||||
els.selectedRoomMeta.textContent = `${entities.length} ${pluralizeIncludedEntities(entities.length)}`;
|
setText(els.selectedRoomMeta, `${entities.length} ${pluralizeIncludedEntities(entities.length)}`);
|
||||||
renderSelectedRoomActions(snapshot);
|
renderSelectedRoomActions(snapshot);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -3545,23 +3860,32 @@
|
|||||||
function renderDashboard(snapshot) {
|
function renderDashboard(snapshot) {
|
||||||
const room = snapshot.selected_space || snapshot.selected_room || {};
|
const room = snapshot.selected_space || snapshot.selected_room || {};
|
||||||
const grid = els.dashboardSurface;
|
const grid = els.dashboardSurface;
|
||||||
|
if (!grid) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
grid.innerHTML = '';
|
grid.innerHTML = '';
|
||||||
|
|
||||||
if (room.id === 'main') {
|
if (room.id === 'main') {
|
||||||
const layout = document.createElement('div');
|
const layout = document.createElement('div');
|
||||||
layout.className = 'main-dashboard';
|
layout.className = 'main-dashboard';
|
||||||
|
|
||||||
const hero = renderMainHero(snapshot);
|
const mainEntities = roomEntities(snapshot, 'main');
|
||||||
|
|
||||||
const cards = document.createElement('div');
|
const cards = document.createElement('div');
|
||||||
cards.className = 'grid-surface main-dashboard__cards';
|
cards.className = 'grid-surface main-dashboard__cards';
|
||||||
|
|
||||||
const mainEntities = roomEntities(snapshot, 'main');
|
if (mainEntities.length) {
|
||||||
mainEntities.forEach((entity) => {
|
mainEntities.forEach((entity) => {
|
||||||
cards.appendChild(renderEntityCard(entity, { isMain: true }));
|
cards.appendChild(renderEntityCard(entity, { isMain: true }));
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const hero = renderMainHero(snapshot);
|
||||||
|
|
||||||
|
layout.append(hero);
|
||||||
|
if (mainEntities.length) {
|
||||||
|
layout.append(cards);
|
||||||
|
}
|
||||||
|
|
||||||
layout.append(hero, cards);
|
|
||||||
grid.appendChild(layout);
|
grid.appendChild(layout);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -3683,8 +4007,10 @@
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
if (popup.active && els.cameraBackdrop?.classList.contains('is-open') && signature === state.lastPopupSignature) {
|
if (popup.active && els.cameraBackdrop?.classList.contains('is-open') && signature === state.lastPopupSignature) {
|
||||||
|
if (els.cameraBackdrop) {
|
||||||
els.cameraBackdrop.classList.add('is-open');
|
els.cameraBackdrop.classList.add('is-open');
|
||||||
els.cameraBackdrop.setAttribute('aria-hidden', 'false');
|
els.cameraBackdrop.setAttribute('aria-hidden', 'false');
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -3695,11 +4021,17 @@
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (els.cameraPoster) {
|
||||||
els.cameraPoster.src = popup.poster_url || '';
|
els.cameraPoster.src = popup.poster_url || '';
|
||||||
els.cameraPoster.alt = popup.sensor_entity_id || 'camera';
|
els.cameraPoster.alt = popup.sensor_entity_id || 'camera';
|
||||||
|
}
|
||||||
|
if (els.cameraBackdrop) {
|
||||||
els.cameraBackdrop.classList.add('is-open');
|
els.cameraBackdrop.classList.add('is-open');
|
||||||
els.cameraBackdrop.setAttribute('aria-hidden', 'false');
|
els.cameraBackdrop.setAttribute('aria-hidden', 'false');
|
||||||
|
}
|
||||||
|
if (els.cameraPlaceholder) {
|
||||||
els.cameraPlaceholder.classList.add('is-visible');
|
els.cameraPlaceholder.classList.add('is-visible');
|
||||||
|
}
|
||||||
|
|
||||||
const expiresAt = Number(popup.expires_at || 0);
|
const expiresAt = Number(popup.expires_at || 0);
|
||||||
if (expiresAt > 0) {
|
if (expiresAt > 0) {
|
||||||
@ -3709,11 +4041,15 @@
|
|||||||
const mins = Math.floor(remaining / 60);
|
const mins = Math.floor(remaining / 60);
|
||||||
const secs = remaining % 60;
|
const secs = remaining % 60;
|
||||||
if (remaining > 0) {
|
if (remaining > 0) {
|
||||||
|
if (els.cameraCountdown) {
|
||||||
els.cameraCountdown.textContent = `Закроется через ${mins}:${String(secs).padStart(2, '0')}`;
|
els.cameraCountdown.textContent = `Закроется через ${mins}:${String(secs).padStart(2, '0')}`;
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (els.cameraCountdown) {
|
||||||
els.cameraCountdown.textContent = 'Закрытие...';
|
els.cameraCountdown.textContent = 'Закрытие...';
|
||||||
|
}
|
||||||
if (closeRequested) {
|
if (closeRequested) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -3736,7 +4072,9 @@
|
|||||||
clearInterval(state.popupDismissTimer);
|
clearInterval(state.popupDismissTimer);
|
||||||
state.popupDismissTimer = setInterval(updateCountdown, 1000);
|
state.popupDismissTimer = setInterval(updateCountdown, 1000);
|
||||||
} else {
|
} else {
|
||||||
|
if (els.cameraCountdown) {
|
||||||
els.cameraCountdown.textContent = '';
|
els.cameraCountdown.textContent = '';
|
||||||
|
}
|
||||||
clearInterval(state.popupDismissTimer);
|
clearInterval(state.popupDismissTimer);
|
||||||
state.popupDismissTimer = null;
|
state.popupDismissTimer = null;
|
||||||
}
|
}
|
||||||
@ -3759,14 +4097,26 @@
|
|||||||
active: false,
|
active: false,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
if (els.cameraBackdrop) {
|
||||||
els.cameraBackdrop.classList.remove('is-open');
|
els.cameraBackdrop.classList.remove('is-open');
|
||||||
els.cameraBackdrop.setAttribute('aria-hidden', 'true');
|
els.cameraBackdrop.setAttribute('aria-hidden', 'true');
|
||||||
|
}
|
||||||
|
if (els.cameraStage) {
|
||||||
els.cameraStage.innerHTML = '';
|
els.cameraStage.innerHTML = '';
|
||||||
|
if (els.cameraPoster) {
|
||||||
els.cameraStage.appendChild(els.cameraPoster);
|
els.cameraStage.appendChild(els.cameraPoster);
|
||||||
|
}
|
||||||
|
if (els.cameraPlaceholder) {
|
||||||
els.cameraStage.appendChild(els.cameraPlaceholder);
|
els.cameraStage.appendChild(els.cameraPlaceholder);
|
||||||
els.cameraPlaceholder.classList.add('is-visible');
|
els.cameraPlaceholder.classList.add('is-visible');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (els.cameraPoster) {
|
||||||
els.cameraPoster.removeAttribute('src');
|
els.cameraPoster.removeAttribute('src');
|
||||||
|
}
|
||||||
|
if (els.cameraCountdown) {
|
||||||
els.cameraCountdown.textContent = '';
|
els.cameraCountdown.textContent = '';
|
||||||
|
}
|
||||||
clearInterval(state.popupDismissTimer);
|
clearInterval(state.popupDismissTimer);
|
||||||
state.popupDismissTimer = null;
|
state.popupDismissTimer = null;
|
||||||
destroyStream();
|
destroyStream();
|
||||||
@ -3916,19 +4266,35 @@
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const renderSignature = JSON.stringify([
|
||||||
|
snapshot?.selected_room?.id || snapshot?.selected_space?.id || 'main',
|
||||||
|
Array.isArray(snapshot?.rooms) ? snapshot.rooms.length : Array.isArray(snapshot?.spaces) ? snapshot.spaces.length : 0,
|
||||||
|
Array.isArray(snapshot?.main_entities) ? snapshot.main_entities.length : 0,
|
||||||
|
Boolean(snapshot?.popup?.active),
|
||||||
|
Boolean(snapshot?.ui?.mode === 'ha-native'),
|
||||||
|
]);
|
||||||
|
if (renderSignature !== state.debugLastRenderSignature) {
|
||||||
|
state.debugLastRenderSignature = renderSignature;
|
||||||
|
debugLog('render()', snapshotSummary(snapshot));
|
||||||
|
}
|
||||||
|
|
||||||
syncLayoutState();
|
syncLayoutState();
|
||||||
renderRoomButtons(snapshot, snapshot.spaces || snapshot.rooms, snapshot.battery_room);
|
|
||||||
renderSelectedRoom(snapshot);
|
|
||||||
renderDashboard(snapshot);
|
renderDashboard(snapshot);
|
||||||
|
renderSelectedRoom(snapshot);
|
||||||
|
renderRoomButtons(snapshot, snapshot.spaces || snapshot.rooms, snapshot.battery_room);
|
||||||
renderPopup(snapshot);
|
renderPopup(snapshot);
|
||||||
renderEntityPopup(snapshot);
|
renderEntityPopup(snapshot);
|
||||||
renderTemperatureSensorPopup(snapshot);
|
renderTemperatureSensorPopup(snapshot);
|
||||||
|
|
||||||
const roomCount = Math.max(0, (snapshot.spaces?.length || snapshot.rooms?.length || 1) - 1);
|
const roomCount = Math.max(0, (snapshot.spaces?.length || snapshot.rooms?.length || 1) - 1);
|
||||||
|
if (els.roomsCount) {
|
||||||
els.roomsCount.textContent = state.editMode ? `${roomCount} ${pluralizeRooms(roomCount)}` : '';
|
els.roomsCount.textContent = state.editMode ? `${roomCount} ${pluralizeRooms(roomCount)}` : '';
|
||||||
|
}
|
||||||
|
if (els.editModeToggle) {
|
||||||
els.editModeToggle.classList.toggle('is-active', state.editMode);
|
els.editModeToggle.classList.toggle('is-active', state.editMode);
|
||||||
els.editModeToggle.title = state.editMode ? 'Edit mode on' : 'Edit mode off';
|
els.editModeToggle.title = state.editMode ? 'Edit mode on' : 'Edit mode off';
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function renderDashboardOnly() {
|
function renderDashboardOnly() {
|
||||||
const snapshot = state.snapshot || bootstrap;
|
const snapshot = state.snapshot || bootstrap;
|
||||||
@ -3939,6 +4305,14 @@
|
|||||||
renderPopup(snapshot);
|
renderPopup(snapshot);
|
||||||
renderEntityPopup(snapshot);
|
renderEntityPopup(snapshot);
|
||||||
renderTemperatureSensorPopup(snapshot);
|
renderTemperatureSensorPopup(snapshot);
|
||||||
|
const roomCount = Math.max(0, (snapshot.spaces?.length || snapshot.rooms?.length || 1) - 1);
|
||||||
|
if (els.roomsCount) {
|
||||||
|
els.roomsCount.textContent = state.editMode ? `${roomCount} ${pluralizeRooms(roomCount)}` : '';
|
||||||
|
}
|
||||||
|
if (els.editModeToggle) {
|
||||||
|
els.editModeToggle.classList.toggle('is-active', state.editMode);
|
||||||
|
els.editModeToggle.title = state.editMode ? 'Edit mode on' : 'Edit mode off';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function refreshCurrentRoomLayout(entityId) {
|
function refreshCurrentRoomLayout(entityId) {
|
||||||
@ -3968,6 +4342,9 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
const container = els.dashboardSurface;
|
const container = els.dashboardSurface;
|
||||||
|
if (!container) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
const order = new Map(roomEntities(snapshot, room.id).map((entity) => [entity.entity_id, Number(entity.order ?? 9999)]));
|
const order = new Map(roomEntities(snapshot, room.id).map((entity) => [entity.entity_id, Number(entity.order ?? 9999)]));
|
||||||
const cards = Array.from(container.querySelectorAll('.grid-card[data-entity-id]'));
|
const cards = Array.from(container.querySelectorAll('.grid-card[data-entity-id]'));
|
||||||
cards.sort((left, right) => {
|
cards.sort((left, right) => {
|
||||||
@ -3984,10 +4361,14 @@
|
|||||||
if (!snapshot || !(snapshot.spaces || snapshot.rooms)) return;
|
if (!snapshot || !(snapshot.spaces || snapshot.rooms)) return;
|
||||||
renderRoomButtons(snapshot, snapshot.spaces || snapshot.rooms, snapshot.battery_room);
|
renderRoomButtons(snapshot, snapshot.spaces || snapshot.rooms, snapshot.battery_room);
|
||||||
const roomCount = Math.max(0, (snapshot.spaces?.length || snapshot.rooms?.length || 1) - 1);
|
const roomCount = Math.max(0, (snapshot.spaces?.length || snapshot.rooms?.length || 1) - 1);
|
||||||
|
if (els.roomsCount) {
|
||||||
els.roomsCount.textContent = state.editMode ? `${roomCount} ${pluralizeRooms(roomCount)}` : '';
|
els.roomsCount.textContent = state.editMode ? `${roomCount} ${pluralizeRooms(roomCount)}` : '';
|
||||||
|
}
|
||||||
|
if (els.editModeToggle) {
|
||||||
els.editModeToggle.classList.toggle('is-active', state.editMode);
|
els.editModeToggle.classList.toggle('is-active', state.editMode);
|
||||||
els.editModeToggle.title = state.editMode ? 'Edit mode on' : 'Edit mode off';
|
els.editModeToggle.title = state.editMode ? 'Edit mode on' : 'Edit mode off';
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function renderSelectionOnly() {
|
function renderSelectionOnly() {
|
||||||
const snapshot = state.snapshot || bootstrap;
|
const snapshot = state.snapshot || bootstrap;
|
||||||
@ -4191,6 +4572,11 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function wireEvents() {
|
function wireEvents() {
|
||||||
|
const bind = (el, type, handler, options) => {
|
||||||
|
if (!el) return;
|
||||||
|
el.addEventListener(type, handler, options);
|
||||||
|
};
|
||||||
|
|
||||||
els.selectedRoomBack?.addEventListener('click', () => {
|
els.selectedRoomBack?.addEventListener('click', () => {
|
||||||
if (!isMobileViewport()) return;
|
if (!isMobileViewport()) return;
|
||||||
closeEntityPopup();
|
closeEntityPopup();
|
||||||
@ -4200,14 +4586,14 @@
|
|||||||
renderSelectionOnly();
|
renderSelectionOnly();
|
||||||
});
|
});
|
||||||
|
|
||||||
els.cameraBackdrop.addEventListener('click', (event) => {
|
bind(els.cameraBackdrop, 'click', (event) => {
|
||||||
if (event.target === els.cameraBackdrop) {
|
if (event.target === els.cameraBackdrop) {
|
||||||
apiPost('popup', { command: 'close' }).catch(() => {});
|
apiPost('popup', { command: 'close' }).catch(() => {});
|
||||||
hidePopup({ suppressAutoOpen: true });
|
hidePopup({ suppressAutoOpen: true });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
els.cameraModalPanel.addEventListener('click', (event) => {
|
bind(els.cameraModalPanel, 'click', (event) => {
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -4224,8 +4610,8 @@
|
|||||||
hidePopup({ suppressAutoOpen: true });
|
hidePopup({ suppressAutoOpen: true });
|
||||||
};
|
};
|
||||||
|
|
||||||
els.cameraClose.addEventListener('pointerdown', closeCameraPopup);
|
bind(els.cameraClose, 'pointerdown', closeCameraPopup);
|
||||||
els.cameraClose.addEventListener('click', closeCameraPopup);
|
bind(els.cameraClose, 'click', closeCameraPopup);
|
||||||
|
|
||||||
els.entityBackdrop?.addEventListener('click', (event) => {
|
els.entityBackdrop?.addEventListener('click', (event) => {
|
||||||
if (event.target === els.entityBackdrop) {
|
if (event.target === els.entityBackdrop) {
|
||||||
@ -4233,7 +4619,7 @@
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
els.entityModalPanel?.addEventListener('click', (event) => {
|
bind(els.entityModalPanel, 'click', (event) => {
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -4247,19 +4633,19 @@
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
els.temperatureSensorModalPanel?.addEventListener('click', (event) => {
|
bind(els.temperatureSensorModalPanel, 'click', (event) => {
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
});
|
});
|
||||||
|
|
||||||
els.temperatureSensorClose?.addEventListener('click', () => {
|
bind(els.temperatureSensorClose, 'click', () => {
|
||||||
closeTemperatureSensorPopup();
|
closeTemperatureSensorPopup();
|
||||||
});
|
});
|
||||||
|
|
||||||
els.popupDebugButton?.addEventListener('click', () => {
|
bind(els.popupDebugButton, 'click', () => {
|
||||||
showDebugPopup();
|
showDebugPopup();
|
||||||
});
|
});
|
||||||
|
|
||||||
els.editModeToggle.addEventListener('click', async () => {
|
bind(els.editModeToggle, 'click', async () => {
|
||||||
state.editMode = !state.editMode;
|
state.editMode = !state.editMode;
|
||||||
try {
|
try {
|
||||||
await apiPost('save-settings', {
|
await apiPost('save-settings', {
|
||||||
@ -4380,6 +4766,9 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function startSnapshotPolling() {
|
function startSnapshotPolling() {
|
||||||
|
if (isHaRuntime()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
const interval = Math.max(1000, Number(state.snapshot?.settings?.poll_interval_ms || bootstrap?.settings?.poll_interval_ms || 5000));
|
const interval = Math.max(1000, Number(state.snapshot?.settings?.poll_interval_ms || bootstrap?.settings?.poll_interval_ms || 5000));
|
||||||
if (state.snapshotPollTimer) {
|
if (state.snapshotPollTimer) {
|
||||||
clearInterval(state.snapshotPollTimer);
|
clearInterval(state.snapshotPollTimer);
|
||||||
@ -4396,6 +4785,9 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function handleHaMessage(message) {
|
function handleHaMessage(message) {
|
||||||
|
if (isHaRuntime()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (!message || typeof message !== 'object') {
|
if (!message || typeof message !== 'object') {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -4519,6 +4911,11 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function connectRealtime() {
|
function connectRealtime() {
|
||||||
|
if (isHaRuntime()) {
|
||||||
|
setStatus('HA native mode', 'online');
|
||||||
|
stopSnapshotPolling();
|
||||||
|
return;
|
||||||
|
}
|
||||||
const connection = haConnection();
|
const connection = haConnection();
|
||||||
const baseUrl = connection.base_url || '';
|
const baseUrl = connection.base_url || '';
|
||||||
const token = connection.token || '';
|
const token = connection.token || '';
|
||||||
@ -4572,6 +4969,11 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function start() {
|
async function start() {
|
||||||
|
debugLog('start()', {
|
||||||
|
ha_runtime: isHaRuntime(),
|
||||||
|
embed_mode: Boolean(bootstrap?.ui?.embed),
|
||||||
|
mode: bootstrap?.ui?.mode || 'unknown',
|
||||||
|
});
|
||||||
initRefs();
|
initRefs();
|
||||||
state.embedMode = detectEmbeddedContext();
|
state.embedMode = detectEmbeddedContext();
|
||||||
syncLayoutState();
|
syncLayoutState();
|
||||||
@ -4582,6 +4984,19 @@
|
|||||||
state.clockTimer = setInterval(updateClock, 1000);
|
state.clockTimer = setInterval(updateClock, 1000);
|
||||||
wireEvents();
|
wireEvents();
|
||||||
|
|
||||||
|
if (!state.haSnapshotListenerInstalled) {
|
||||||
|
state.haSnapshotListenerInstalled = true;
|
||||||
|
window.addEventListener('wall-panel-snapshot-updated', (event) => {
|
||||||
|
const snapshot = event?.detail?.snapshot || event?.detail || null;
|
||||||
|
if (!snapshot || typeof snapshot !== 'object') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
debugLog('wall-panel-snapshot-updated', snapshotSummary(snapshot));
|
||||||
|
state.snapshot = snapshot;
|
||||||
|
render();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const viewportQuery = mobileViewportQuery();
|
const viewportQuery = mobileViewportQuery();
|
||||||
const handleViewportChange = () => {
|
const handleViewportChange = () => {
|
||||||
syncViewportState();
|
syncViewportState();
|
||||||
@ -4593,8 +5008,8 @@
|
|||||||
viewportQuery.addListener(handleViewportChange);
|
viewportQuery.addListener(handleViewportChange);
|
||||||
}
|
}
|
||||||
|
|
||||||
const initial = window.APP_BOOTSTRAP || {};
|
state.snapshot = await resolveInitialSnapshot();
|
||||||
state.snapshot = initial;
|
debugLog('initial snapshot applied', snapshotSummary(state.snapshot || bootstrap));
|
||||||
render();
|
render();
|
||||||
connectRealtime();
|
connectRealtime();
|
||||||
if (!state.snapshotPollTimer) {
|
if (!state.snapshotPollTimer) {
|
||||||
@ -4602,5 +5017,9 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (document.readyState === 'loading') {
|
||||||
document.addEventListener('DOMContentLoaded', start);
|
document.addEventListener('DOMContentLoaded', start);
|
||||||
|
} else {
|
||||||
|
start();
|
||||||
|
}
|
||||||
})();
|
})();
|
||||||
|
|||||||
2574
custom_components/wall_panel/assets/app.css
Executable file
2574
custom_components/wall_panel/assets/app.css
Executable file
File diff suppressed because it is too large
Load Diff
4932
custom_components/wall_panel/assets/app.js
Executable file
4932
custom_components/wall_panel/assets/app.js
Executable file
File diff suppressed because it is too large
Load Diff
@ -14,29 +14,25 @@ from homeassistant.helpers.selector import TextSelector, TextSelectorConfig, Tex
|
|||||||
from .const import (
|
from .const import (
|
||||||
CONF_CONFIG,
|
CONF_CONFIG,
|
||||||
CONF_FRONTEND_URL_PATH,
|
CONF_FRONTEND_URL_PATH,
|
||||||
CONF_PANEL_URL,
|
|
||||||
CONF_REQUIRE_ADMIN,
|
CONF_REQUIRE_ADMIN,
|
||||||
CONF_SIDEBAR_ICON,
|
CONF_SIDEBAR_ICON,
|
||||||
CONF_SIDEBAR_TITLE,
|
CONF_SIDEBAR_TITLE,
|
||||||
CONF_SYNC_TOKEN,
|
CONF_SYNC_TOKEN,
|
||||||
DEFAULT_FRONTEND_URL_PATH,
|
DEFAULT_FRONTEND_URL_PATH,
|
||||||
DEFAULT_PANEL_URL,
|
|
||||||
DEFAULT_SIDEBAR_ICON,
|
DEFAULT_SIDEBAR_ICON,
|
||||||
DEFAULT_SIDEBAR_TITLE,
|
DEFAULT_SIDEBAR_TITLE,
|
||||||
DOMAIN,
|
DOMAIN,
|
||||||
)
|
)
|
||||||
from .helpers import config_to_json, normalize_config, parse_config_json
|
from .helpers import config_to_json, current_entry_config, normalize_config, parse_config_json
|
||||||
|
|
||||||
|
|
||||||
def _schema(defaults: dict[str, Any]) -> vol.Schema:
|
def _schema(defaults: dict[str, Any]) -> vol.Schema:
|
||||||
return vol.Schema({
|
return vol.Schema({
|
||||||
vol.Optional(CONF_NAME, default=defaults.get(CONF_NAME, "Striker Panel")): str,
|
vol.Optional(CONF_NAME, default=defaults.get(CONF_NAME, "Striker Panel")): str,
|
||||||
vol.Optional(CONF_PANEL_URL, default=defaults.get(CONF_PANEL_URL, DEFAULT_PANEL_URL)): str,
|
|
||||||
vol.Optional(CONF_SIDEBAR_TITLE, default=defaults.get(CONF_SIDEBAR_TITLE, DEFAULT_SIDEBAR_TITLE)): str,
|
vol.Optional(CONF_SIDEBAR_TITLE, default=defaults.get(CONF_SIDEBAR_TITLE, DEFAULT_SIDEBAR_TITLE)): str,
|
||||||
vol.Optional(CONF_SIDEBAR_ICON, default=defaults.get(CONF_SIDEBAR_ICON, DEFAULT_SIDEBAR_ICON)): str,
|
vol.Optional(CONF_SIDEBAR_ICON, default=defaults.get(CONF_SIDEBAR_ICON, DEFAULT_SIDEBAR_ICON)): str,
|
||||||
vol.Optional(CONF_FRONTEND_URL_PATH, default=defaults.get(CONF_FRONTEND_URL_PATH, DEFAULT_FRONTEND_URL_PATH)): str,
|
vol.Optional(CONF_FRONTEND_URL_PATH, default=defaults.get(CONF_FRONTEND_URL_PATH, DEFAULT_FRONTEND_URL_PATH)): str,
|
||||||
vol.Optional(CONF_REQUIRE_ADMIN, default=bool(defaults.get(CONF_REQUIRE_ADMIN, False))): bool,
|
vol.Optional(CONF_REQUIRE_ADMIN, default=bool(defaults.get(CONF_REQUIRE_ADMIN, False))): bool,
|
||||||
vol.Optional(CONF_SYNC_TOKEN, default=defaults.get(CONF_SYNC_TOKEN, secrets.token_urlsafe(24))): str,
|
|
||||||
vol.Optional(
|
vol.Optional(
|
||||||
CONF_CONFIG,
|
CONF_CONFIG,
|
||||||
default=defaults.get(CONF_CONFIG, config_to_json(normalize_config({}))),
|
default=defaults.get(CONF_CONFIG, config_to_json(normalize_config({}))),
|
||||||
@ -62,12 +58,11 @@ class WallPanelConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
|||||||
else:
|
else:
|
||||||
data = {
|
data = {
|
||||||
CONF_NAME: user_input.get(CONF_NAME, "Striker Panel"),
|
CONF_NAME: user_input.get(CONF_NAME, "Striker Panel"),
|
||||||
CONF_PANEL_URL: str(user_input.get(CONF_PANEL_URL, "") or ""),
|
|
||||||
CONF_SIDEBAR_TITLE: str(user_input.get(CONF_SIDEBAR_TITLE, DEFAULT_SIDEBAR_TITLE) or DEFAULT_SIDEBAR_TITLE),
|
CONF_SIDEBAR_TITLE: str(user_input.get(CONF_SIDEBAR_TITLE, DEFAULT_SIDEBAR_TITLE) or DEFAULT_SIDEBAR_TITLE),
|
||||||
CONF_SIDEBAR_ICON: str(user_input.get(CONF_SIDEBAR_ICON, DEFAULT_SIDEBAR_ICON) or DEFAULT_SIDEBAR_ICON),
|
CONF_SIDEBAR_ICON: str(user_input.get(CONF_SIDEBAR_ICON, DEFAULT_SIDEBAR_ICON) or DEFAULT_SIDEBAR_ICON),
|
||||||
CONF_FRONTEND_URL_PATH: str(user_input.get(CONF_FRONTEND_URL_PATH, DEFAULT_FRONTEND_URL_PATH) or DEFAULT_FRONTEND_URL_PATH),
|
CONF_FRONTEND_URL_PATH: str(user_input.get(CONF_FRONTEND_URL_PATH, DEFAULT_FRONTEND_URL_PATH) or DEFAULT_FRONTEND_URL_PATH),
|
||||||
CONF_REQUIRE_ADMIN: bool(user_input.get(CONF_REQUIRE_ADMIN, False)),
|
CONF_REQUIRE_ADMIN: bool(user_input.get(CONF_REQUIRE_ADMIN, False)),
|
||||||
CONF_SYNC_TOKEN: str(user_input.get(CONF_SYNC_TOKEN, "") or ""),
|
CONF_SYNC_TOKEN: secrets.token_urlsafe(24),
|
||||||
CONF_CONFIG: config,
|
CONF_CONFIG: config,
|
||||||
}
|
}
|
||||||
await self.async_set_unique_id(DOMAIN)
|
await self.async_set_unique_id(DOMAIN)
|
||||||
@ -76,12 +71,10 @@ class WallPanelConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
|||||||
|
|
||||||
defaults = {
|
defaults = {
|
||||||
CONF_NAME: "Striker Panel",
|
CONF_NAME: "Striker Panel",
|
||||||
CONF_PANEL_URL: DEFAULT_PANEL_URL,
|
|
||||||
CONF_SIDEBAR_TITLE: DEFAULT_SIDEBAR_TITLE,
|
CONF_SIDEBAR_TITLE: DEFAULT_SIDEBAR_TITLE,
|
||||||
CONF_SIDEBAR_ICON: DEFAULT_SIDEBAR_ICON,
|
CONF_SIDEBAR_ICON: DEFAULT_SIDEBAR_ICON,
|
||||||
CONF_FRONTEND_URL_PATH: DEFAULT_FRONTEND_URL_PATH,
|
CONF_FRONTEND_URL_PATH: DEFAULT_FRONTEND_URL_PATH,
|
||||||
CONF_REQUIRE_ADMIN: False,
|
CONF_REQUIRE_ADMIN: False,
|
||||||
CONF_SYNC_TOKEN: secrets.token_urlsafe(24),
|
|
||||||
CONF_CONFIG: config_to_json(normalize_config({})),
|
CONF_CONFIG: config_to_json(normalize_config({})),
|
||||||
}
|
}
|
||||||
return self.async_show_form(step_id="user", data_schema=_schema(defaults), errors=errors)
|
return self.async_show_form(step_id="user", data_schema=_schema(defaults), errors=errors)
|
||||||
@ -107,25 +100,22 @@ class WallPanelOptionsFlow(config_entries.OptionsFlow):
|
|||||||
data = dict(self.config_entry.options)
|
data = dict(self.config_entry.options)
|
||||||
data.update({
|
data.update({
|
||||||
CONF_NAME: user_input.get(CONF_NAME, data.get(CONF_NAME, "Striker Panel")),
|
CONF_NAME: user_input.get(CONF_NAME, data.get(CONF_NAME, "Striker Panel")),
|
||||||
CONF_PANEL_URL: str(user_input.get(CONF_PANEL_URL, "") or ""),
|
|
||||||
CONF_SIDEBAR_TITLE: str(user_input.get(CONF_SIDEBAR_TITLE, DEFAULT_SIDEBAR_TITLE) or DEFAULT_SIDEBAR_TITLE),
|
CONF_SIDEBAR_TITLE: str(user_input.get(CONF_SIDEBAR_TITLE, DEFAULT_SIDEBAR_TITLE) or DEFAULT_SIDEBAR_TITLE),
|
||||||
CONF_SIDEBAR_ICON: str(user_input.get(CONF_SIDEBAR_ICON, DEFAULT_SIDEBAR_ICON) or DEFAULT_SIDEBAR_ICON),
|
CONF_SIDEBAR_ICON: str(user_input.get(CONF_SIDEBAR_ICON, DEFAULT_SIDEBAR_ICON) or DEFAULT_SIDEBAR_ICON),
|
||||||
CONF_FRONTEND_URL_PATH: str(user_input.get(CONF_FRONTEND_URL_PATH, DEFAULT_FRONTEND_URL_PATH) or DEFAULT_FRONTEND_URL_PATH),
|
CONF_FRONTEND_URL_PATH: str(user_input.get(CONF_FRONTEND_URL_PATH, DEFAULT_FRONTEND_URL_PATH) or DEFAULT_FRONTEND_URL_PATH),
|
||||||
CONF_REQUIRE_ADMIN: bool(user_input.get(CONF_REQUIRE_ADMIN, False)),
|
CONF_REQUIRE_ADMIN: bool(user_input.get(CONF_REQUIRE_ADMIN, False)),
|
||||||
CONF_SYNC_TOKEN: str(user_input.get(CONF_SYNC_TOKEN, "") or ""),
|
CONF_SYNC_TOKEN: data.get(CONF_SYNC_TOKEN, secrets.token_urlsafe(24)),
|
||||||
CONF_CONFIG: config,
|
CONF_CONFIG: config,
|
||||||
})
|
})
|
||||||
return self.async_create_entry(title="", data=data)
|
return self.async_create_entry(title="", data=data)
|
||||||
|
|
||||||
defaults = {
|
defaults = {
|
||||||
CONF_NAME: self.config_entry.options.get(CONF_NAME, "Striker Panel"),
|
CONF_NAME: self.config_entry.options.get(CONF_NAME, "Striker Panel"),
|
||||||
CONF_PANEL_URL: self.config_entry.options.get(CONF_PANEL_URL, DEFAULT_PANEL_URL),
|
|
||||||
CONF_SIDEBAR_TITLE: self.config_entry.options.get(CONF_SIDEBAR_TITLE, DEFAULT_SIDEBAR_TITLE),
|
CONF_SIDEBAR_TITLE: self.config_entry.options.get(CONF_SIDEBAR_TITLE, DEFAULT_SIDEBAR_TITLE),
|
||||||
CONF_SIDEBAR_ICON: self.config_entry.options.get(CONF_SIDEBAR_ICON, DEFAULT_SIDEBAR_ICON),
|
CONF_SIDEBAR_ICON: self.config_entry.options.get(CONF_SIDEBAR_ICON, DEFAULT_SIDEBAR_ICON),
|
||||||
CONF_FRONTEND_URL_PATH: self.config_entry.options.get(CONF_FRONTEND_URL_PATH, DEFAULT_FRONTEND_URL_PATH),
|
CONF_FRONTEND_URL_PATH: self.config_entry.options.get(CONF_FRONTEND_URL_PATH, DEFAULT_FRONTEND_URL_PATH),
|
||||||
CONF_REQUIRE_ADMIN: self.config_entry.options.get(CONF_REQUIRE_ADMIN, False),
|
CONF_REQUIRE_ADMIN: self.config_entry.options.get(CONF_REQUIRE_ADMIN, False),
|
||||||
CONF_SYNC_TOKEN: self.config_entry.options.get(CONF_SYNC_TOKEN, secrets.token_urlsafe(24)),
|
CONF_CONFIG: config_to_json(current_entry_config(self.config_entry)),
|
||||||
CONF_CONFIG: config_to_json(normalize_config(self.config_entry.options.get(CONF_CONFIG, {}))),
|
|
||||||
}
|
}
|
||||||
return self.async_show_form(step_id="init", data_schema=_schema(defaults), errors=errors)
|
return self.async_show_form(step_id="init", data_schema=_schema(defaults), errors=errors)
|
||||||
|
|
||||||
|
|||||||
@ -21,12 +21,23 @@ from .const import (
|
|||||||
DEFAULT_SIDEBAR_TITLE,
|
DEFAULT_SIDEBAR_TITLE,
|
||||||
DOMAIN,
|
DOMAIN,
|
||||||
)
|
)
|
||||||
|
from .helpers import current_entry_config
|
||||||
|
|
||||||
|
|
||||||
|
def _panel_url_path(entry) -> str:
|
||||||
|
raw = str(entry.options.get(CONF_FRONTEND_URL_PATH, DEFAULT_FRONTEND_URL_PATH) or DEFAULT_FRONTEND_URL_PATH).strip()
|
||||||
|
if raw in {"", "wall-panel"}:
|
||||||
|
return DEFAULT_FRONTEND_URL_PATH
|
||||||
|
return raw
|
||||||
|
|
||||||
|
|
||||||
async def async_setup_frontend(hass: HomeAssistant, entry) -> str:
|
async def async_setup_frontend(hass: HomeAssistant, entry) -> str:
|
||||||
"""Register the custom panel and static frontend assets."""
|
"""Register the custom panel and static frontend assets."""
|
||||||
|
|
||||||
frontend_dir = Path(__file__).parent / "frontend"
|
frontend_dir = Path(__file__).parent / "frontend"
|
||||||
|
# Bundle the HA-native assets inside the component so the integration
|
||||||
|
# does not depend on files living in the root of /config.
|
||||||
|
assets_dir = Path(__file__).parent / "assets"
|
||||||
state = hass.data.setdefault(DOMAIN, {})
|
state = hass.data.setdefault(DOMAIN, {})
|
||||||
if not state.get("_static_paths_registered"):
|
if not state.get("_static_paths_registered"):
|
||||||
await hass.http.async_register_static_paths([
|
await hass.http.async_register_static_paths([
|
||||||
@ -35,16 +46,21 @@ async def async_setup_frontend(hass: HomeAssistant, entry) -> str:
|
|||||||
str(frontend_dir),
|
str(frontend_dir),
|
||||||
cache_headers=False,
|
cache_headers=False,
|
||||||
),
|
),
|
||||||
|
StaticPathConfig(
|
||||||
|
f"/api/{DOMAIN}/assets",
|
||||||
|
str(assets_dir),
|
||||||
|
cache_headers=False,
|
||||||
|
),
|
||||||
])
|
])
|
||||||
state["_static_paths_registered"] = True
|
state["_static_paths_registered"] = True
|
||||||
|
|
||||||
panel_url_path = str(entry.options.get(CONF_FRONTEND_URL_PATH, DEFAULT_FRONTEND_URL_PATH) or DEFAULT_FRONTEND_URL_PATH).strip()
|
panel_url_path = _panel_url_path(entry)
|
||||||
sidebar_title = str(entry.options.get(CONF_SIDEBAR_TITLE, DEFAULT_SIDEBAR_TITLE) or DEFAULT_SIDEBAR_TITLE).strip()
|
sidebar_title = str(entry.options.get(CONF_SIDEBAR_TITLE, DEFAULT_SIDEBAR_TITLE) or DEFAULT_SIDEBAR_TITLE).strip()
|
||||||
sidebar_icon = str(entry.options.get(CONF_SIDEBAR_ICON, DEFAULT_SIDEBAR_ICON) or DEFAULT_SIDEBAR_ICON).strip()
|
sidebar_icon = str(entry.options.get(CONF_SIDEBAR_ICON, DEFAULT_SIDEBAR_ICON) or DEFAULT_SIDEBAR_ICON).strip()
|
||||||
require_admin = bool(entry.options.get(CONF_REQUIRE_ADMIN, False))
|
require_admin = bool(entry.options.get(CONF_REQUIRE_ADMIN, False))
|
||||||
panel_url = str(entry.options.get(CONF_PANEL_URL, "") or "").strip()
|
|
||||||
sync_token = str(entry.options.get(CONF_SYNC_TOKEN, "") or "").strip()
|
sync_token = str(entry.options.get(CONF_SYNC_TOKEN, "") or "").strip()
|
||||||
asset_version = str(int(time.time()))
|
asset_version = str(int(time.time()))
|
||||||
|
runtime_config = current_entry_config(entry)
|
||||||
|
|
||||||
async_register_built_in_panel(
|
async_register_built_in_panel(
|
||||||
hass,
|
hass,
|
||||||
@ -56,13 +72,12 @@ async def async_setup_frontend(hass: HomeAssistant, entry) -> str:
|
|||||||
"_panel_custom": {
|
"_panel_custom": {
|
||||||
"name": "striker-panel-panel",
|
"name": "striker-panel-panel",
|
||||||
"module_url": f"/api/{DOMAIN}/frontend/panel.js?v={asset_version}",
|
"module_url": f"/api/{DOMAIN}/frontend/panel.js?v={asset_version}",
|
||||||
"embed_iframe": False,
|
|
||||||
"trust_external": False,
|
|
||||||
"config": {
|
"config": {
|
||||||
"panel_url": panel_url,
|
"runtime_config": runtime_config,
|
||||||
"panel_url_path": panel_url_path,
|
"ui_mode": "ha-native",
|
||||||
"entry_id": entry.entry_id,
|
"entry_id": entry.entry_id,
|
||||||
"sync_token": sync_token,
|
"sync_token": sync_token,
|
||||||
|
"config_url": f"/api/{DOMAIN}/config/{entry.entry_id}",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@ -3,7 +3,9 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import json
|
import json
|
||||||
|
import os
|
||||||
from copy import deepcopy
|
from copy import deepcopy
|
||||||
|
from pathlib import Path
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from .const import (
|
from .const import (
|
||||||
@ -21,6 +23,13 @@ from .const import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def shared_config_path() -> Path:
|
||||||
|
override = os.getenv("WALL_PANEL_SHARED_CONFIG_PATH", "").strip()
|
||||||
|
if override:
|
||||||
|
return Path(override)
|
||||||
|
return Path("/config/wall_panel/wall_panel_config.json")
|
||||||
|
|
||||||
|
|
||||||
def default_config() -> dict[str, Any]:
|
def default_config() -> dict[str, Any]:
|
||||||
return {
|
return {
|
||||||
"app": {
|
"app": {
|
||||||
@ -79,7 +88,79 @@ def parse_config_json(raw: str) -> dict[str, Any]:
|
|||||||
|
|
||||||
|
|
||||||
def current_entry_config(entry) -> dict[str, Any]:
|
def current_entry_config(entry) -> dict[str, Any]:
|
||||||
return normalize_config(entry.options.get(CONF_CONFIG))
|
path = shared_config_path()
|
||||||
|
options_config = normalize_config(entry.options.get(CONF_CONFIG))
|
||||||
|
|
||||||
|
def _config_score(config: dict[str, Any]) -> int:
|
||||||
|
score = 0
|
||||||
|
rooms = config.get("rooms", [])
|
||||||
|
if isinstance(rooms, list):
|
||||||
|
score += len(rooms) * 25
|
||||||
|
for room in rooms:
|
||||||
|
if not isinstance(room, dict):
|
||||||
|
continue
|
||||||
|
score += len([value for value in (
|
||||||
|
room.get("name"),
|
||||||
|
room.get("icon"),
|
||||||
|
room.get("area_id"),
|
||||||
|
room.get("floor_id"),
|
||||||
|
room.get("temperature_sensor_entity_id"),
|
||||||
|
) if isinstance(value, str) and value.strip()])
|
||||||
|
entity_ids = room.get("entity_ids", [])
|
||||||
|
if isinstance(entity_ids, list):
|
||||||
|
score += len([item for item in entity_ids if str(item).strip()])
|
||||||
|
overrides = room.get("entity_overrides", {})
|
||||||
|
if isinstance(overrides, dict):
|
||||||
|
score += len(overrides) * 3
|
||||||
|
layout_items = room.get("layout_items", [])
|
||||||
|
if isinstance(layout_items, list):
|
||||||
|
score += len(layout_items) * 5
|
||||||
|
|
||||||
|
app = config.get("app", {})
|
||||||
|
if isinstance(app, dict):
|
||||||
|
for key in ("title", "main_room_name", "main_room_icon"):
|
||||||
|
value = app.get(key)
|
||||||
|
if isinstance(value, str) and value.strip():
|
||||||
|
score += 3
|
||||||
|
for key in ("main_boiler", "main_print"):
|
||||||
|
value = app.get(key)
|
||||||
|
if isinstance(value, dict) and value:
|
||||||
|
score += len(value) * 2
|
||||||
|
actions = app.get("main_weather_actions", [])
|
||||||
|
if isinstance(actions, list):
|
||||||
|
score += len(actions) * 2
|
||||||
|
|
||||||
|
camera = config.get("camera", {})
|
||||||
|
if isinstance(camera, dict):
|
||||||
|
for key in ("rtsp_url", "stream_url", "poster_url"):
|
||||||
|
value = camera.get(key)
|
||||||
|
if isinstance(value, str) and value.strip():
|
||||||
|
score += 5
|
||||||
|
trigger_entities = camera.get("trigger_entities", [])
|
||||||
|
if isinstance(trigger_entities, list):
|
||||||
|
score += len([item for item in trigger_entities if str(item).strip()])
|
||||||
|
|
||||||
|
return score
|
||||||
|
|
||||||
|
chosen_config = options_config
|
||||||
|
try:
|
||||||
|
if path.is_file():
|
||||||
|
raw = path.read_text(encoding="utf-8")
|
||||||
|
if raw.strip():
|
||||||
|
data = json.loads(raw)
|
||||||
|
if isinstance(data, dict):
|
||||||
|
file_config = normalize_config(data)
|
||||||
|
if _config_score(file_config) >= _config_score(options_config):
|
||||||
|
chosen_config = file_config
|
||||||
|
except (OSError, json.JSONDecodeError, ValueError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
try:
|
||||||
|
path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
path.write_text(config_to_json(chosen_config) + "\n", encoding="utf-8")
|
||||||
|
except OSError:
|
||||||
|
pass
|
||||||
|
return chosen_config
|
||||||
|
|
||||||
|
|
||||||
def current_entry_panel(entry) -> dict[str, Any]:
|
def current_entry_panel(entry) -> dict[str, Any]:
|
||||||
@ -238,6 +319,12 @@ def build_patch_payload(payload: dict[str, Any], keys: list[str]) -> dict[str, A
|
|||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def save_shared_config(config: dict[str, Any]) -> None:
|
||||||
|
path = shared_config_path()
|
||||||
|
path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
path.write_text(config_to_json(normalize_config(config)) + "\n", encoding="utf-8")
|
||||||
|
|
||||||
|
|
||||||
def _deep_merge(target: dict[str, Any], source: dict[str, Any]) -> None:
|
def _deep_merge(target: dict[str, Any], source: dict[str, Any]) -> None:
|
||||||
for key, value in source.items():
|
for key, value in source.items():
|
||||||
if isinstance(value, dict) and isinstance(target.get(key), dict):
|
if isinstance(value, dict) and isinstance(target.get(key), dict):
|
||||||
|
|||||||
@ -24,6 +24,7 @@ from .helpers import (
|
|||||||
normalize_config,
|
normalize_config,
|
||||||
reorder_room_grid,
|
reorder_room_grid,
|
||||||
save_settings,
|
save_settings,
|
||||||
|
save_shared_config,
|
||||||
update_entity_override,
|
update_entity_override,
|
||||||
update_room_layout_item,
|
update_room_layout_item,
|
||||||
update_room_override,
|
update_room_override,
|
||||||
@ -66,6 +67,11 @@ def _request_token(request: web.Request) -> str:
|
|||||||
return request.query.get("token", "").strip()
|
return request.query.get("token", "").strip()
|
||||||
|
|
||||||
|
|
||||||
|
def _proxy_cookie_name(entry_id: str) -> str:
|
||||||
|
safe_entry_id = "".join(ch for ch in entry_id if ch.isalnum() or ch in {"-", "_"})
|
||||||
|
return f"wall_panel_proxy_{safe_entry_id}"
|
||||||
|
|
||||||
|
|
||||||
def _authorized(entry, request: web.Request) -> bool:
|
def _authorized(entry, request: web.Request) -> bool:
|
||||||
expected = str(entry.options.get(CONF_SYNC_TOKEN, "") or "").strip()
|
expected = str(entry.options.get(CONF_SYNC_TOKEN, "") or "").strip()
|
||||||
if not expected:
|
if not expected:
|
||||||
@ -74,6 +80,19 @@ def _authorized(entry, request: web.Request) -> bool:
|
|||||||
return secrets.compare_digest(_request_token(request), expected)
|
return secrets.compare_digest(_request_token(request), expected)
|
||||||
|
|
||||||
|
|
||||||
|
def _proxy_authorized(entry, request: web.Request, entry_id: str) -> bool:
|
||||||
|
expected = str(entry.options.get(CONF_SYNC_TOKEN, "") or "").strip()
|
||||||
|
if not expected:
|
||||||
|
return False
|
||||||
|
|
||||||
|
token = _request_token(request)
|
||||||
|
if token and secrets.compare_digest(token, expected):
|
||||||
|
return True
|
||||||
|
|
||||||
|
cookie = request.cookies.get(_proxy_cookie_name(entry_id), "")
|
||||||
|
return bool(cookie) and secrets.compare_digest(cookie, expected)
|
||||||
|
|
||||||
|
|
||||||
def _response(data: Any, status: int = 200) -> web.Response:
|
def _response(data: Any, status: int = 200) -> web.Response:
|
||||||
if isinstance(data, str):
|
if isinstance(data, str):
|
||||||
return web.Response(text=data, status=status, content_type="text/plain; charset=utf-8")
|
return web.Response(text=data, status=status, content_type="text/plain; charset=utf-8")
|
||||||
@ -235,7 +254,7 @@ async def _handle_proxy_request(request: web.Request, entry_id: str, path: str,
|
|||||||
entry = _entry_from_hass(hass, entry_id)
|
entry = _entry_from_hass(hass, entry_id)
|
||||||
if entry is None:
|
if entry is None:
|
||||||
return _response({"ok": False, "error": "Unknown entry"}, 404)
|
return _response({"ok": False, "error": "Unknown entry"}, 404)
|
||||||
if not _authorized(entry, request):
|
if not _proxy_authorized(entry, request, entry_id):
|
||||||
_LOGGER.warning("Wall Panel proxy denied for %s: unauthorized", entry_id)
|
_LOGGER.warning("Wall Panel proxy denied for %s: unauthorized", entry_id)
|
||||||
return _response({"ok": False, "error": "Unauthorized"}, 401)
|
return _response({"ok": False, "error": "Unauthorized"}, 401)
|
||||||
|
|
||||||
@ -286,11 +305,26 @@ async def _handle_proxy_request(request: web.Request, entry_id: str, path: str,
|
|||||||
|
|
||||||
response_body = await upstream.read()
|
response_body = await upstream.read()
|
||||||
_LOGGER.warning("Wall Panel upstream %s %s", upstream.status, upstream_url)
|
_LOGGER.warning("Wall Panel upstream %s %s", upstream.status, upstream_url)
|
||||||
return web.Response(
|
response = web.Response(
|
||||||
body=response_body,
|
body=response_body,
|
||||||
status=upstream.status,
|
status=upstream.status,
|
||||||
headers=response_headers,
|
headers=response_headers,
|
||||||
)
|
)
|
||||||
|
expected = str(entry.options.get(CONF_SYNC_TOKEN, "") or "").strip()
|
||||||
|
if expected:
|
||||||
|
# The iframe's HTML may authenticate with ?token=..., but its relative
|
||||||
|
# CSS/JS/API requests will not inherit that query string. A scoped cookie
|
||||||
|
# keeps the whole proxied panel session authorized.
|
||||||
|
response.set_cookie(
|
||||||
|
_proxy_cookie_name(entry_id),
|
||||||
|
expected,
|
||||||
|
path=_proxy_root(entry_id),
|
||||||
|
secure=True,
|
||||||
|
httponly=True,
|
||||||
|
samesite="Lax",
|
||||||
|
max_age=60 * 60 * 24,
|
||||||
|
)
|
||||||
|
return response
|
||||||
|
|
||||||
|
|
||||||
async def handle_proxy_request(request: web.Request) -> web.StreamResponse:
|
async def handle_proxy_request(request: web.Request) -> web.StreamResponse:
|
||||||
@ -303,6 +337,7 @@ async def handle_proxy_request(request: web.Request) -> web.StreamResponse:
|
|||||||
|
|
||||||
|
|
||||||
def _save_entry_config(hass: HomeAssistant, entry, config: dict[str, Any]) -> None:
|
def _save_entry_config(hass: HomeAssistant, entry, config: dict[str, Any]) -> None:
|
||||||
|
save_shared_config(config)
|
||||||
options = dict(entry.options)
|
options = dict(entry.options)
|
||||||
options[CONF_CONFIG] = config
|
options[CONF_CONFIG] = config
|
||||||
hass.config_entries.async_update_entry(entry, options=options)
|
hass.config_entries.async_update_entry(entry, options=options)
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"active": false,
|
"active": false,
|
||||||
"sensor_entity_id": "binary_sensor.barn_all_occupancy",
|
"sensor_entity_id": "binary_sensor.doorbell_all_occupancy",
|
||||||
"opened_at": 1774445418,
|
"opened_at": 1774456141,
|
||||||
"expires_at": null
|
"expires_at": null
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2115,6 +2115,11 @@ body.is-mobile-ui #camera-modal {
|
|||||||
max-width: none;
|
max-width: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.main-dashboard__cards .main-dashboard__room-grid {
|
||||||
|
grid-column: 1 / -1;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
.room-entities-section {
|
.room-entities-section {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 14px;
|
gap: 14px;
|
||||||
|
|||||||
@ -42,20 +42,210 @@
|
|||||||
haSubscribeId: 1,
|
haSubscribeId: 1,
|
||||||
roomSelectionToken: 0,
|
roomSelectionToken: 0,
|
||||||
snapshotPollTimer: null,
|
snapshotPollTimer: null,
|
||||||
|
haSnapshotListenerInstalled: false,
|
||||||
|
debugLastRenderSignature: '',
|
||||||
};
|
};
|
||||||
|
|
||||||
const els = {};
|
const els = {};
|
||||||
|
const client = window.StrikerPanelClient || (window.StrikerPanelClient = {});
|
||||||
|
const debugEnabled = (() => {
|
||||||
|
try {
|
||||||
|
if (window.StrikerPanelDebug) return true;
|
||||||
|
const search = new URLSearchParams(window.location.search);
|
||||||
|
if (['1', 'true', 'yes', 'on'].includes(String(search.get('wp_debug') || '').toLowerCase())) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
const stored = window.localStorage?.getItem('striker-panel-debug');
|
||||||
|
if (['1', 'true', 'yes', 'on'].includes(String(stored || '').toLowerCase())) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
return Boolean(window.StrikerPanelDebug);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
})();
|
||||||
|
const debugLog = (...args) => {
|
||||||
|
if (debugEnabled) {
|
||||||
|
console.log('[Striker Panel]', ...args);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const debugWarn = (...args) => {
|
||||||
|
if (debugEnabled) {
|
||||||
|
console.warn('[Striker Panel]', ...args);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
client.renderFromSnapshot = (snapshot) => {
|
||||||
|
if (!snapshot || typeof snapshot !== 'object') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
state.snapshot = snapshot;
|
||||||
|
initRefs();
|
||||||
|
state.embedMode = detectEmbeddedContext();
|
||||||
|
syncLayoutState();
|
||||||
|
render();
|
||||||
|
};
|
||||||
|
|
||||||
|
client.refresh = () => {
|
||||||
|
initRefs();
|
||||||
|
state.embedMode = detectEmbeddedContext();
|
||||||
|
syncLayoutState();
|
||||||
|
render();
|
||||||
|
};
|
||||||
|
|
||||||
function $(id) {
|
function $(id) {
|
||||||
return document.getElementById(id);
|
const root = client.mountRoot || document;
|
||||||
|
if (!root) return null;
|
||||||
|
if (typeof root.getElementById === 'function') {
|
||||||
|
return root.getElementById(id);
|
||||||
|
}
|
||||||
|
return root.querySelector?.(`#${CSS.escape(id)}`) || null;
|
||||||
}
|
}
|
||||||
|
|
||||||
function q(sel, root = document) {
|
function q(sel, root) {
|
||||||
return root.querySelector(sel);
|
const actualRoot = root || client.mountRoot || document;
|
||||||
|
return actualRoot.querySelector(sel);
|
||||||
}
|
}
|
||||||
|
|
||||||
function qa(sel, root = document) {
|
function qa(sel, root) {
|
||||||
return Array.from(root.querySelectorAll(sel));
|
const actualRoot = root || client.mountRoot || document;
|
||||||
|
return Array.from(actualRoot.querySelectorAll(sel));
|
||||||
|
}
|
||||||
|
|
||||||
|
function haBridge() {
|
||||||
|
return window.WALL_PANEL_HA_BRIDGE || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isHaRuntime() {
|
||||||
|
return Boolean(
|
||||||
|
haBridge()
|
||||||
|
|| bootstrap?.ui?.mode === 'ha-native'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function sleep(ms) {
|
||||||
|
return new Promise((resolve) => window.setTimeout(resolve, Math.max(0, Number(ms) || 0)));
|
||||||
|
}
|
||||||
|
|
||||||
|
function snapshotLooksReady(snapshot) {
|
||||||
|
if (!snapshot || typeof snapshot !== 'object') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const rooms = Array.isArray(snapshot.rooms) ? snapshot.rooms : [];
|
||||||
|
const spaces = Array.isArray(snapshot.spaces) ? snapshot.spaces : [];
|
||||||
|
if (rooms.length > 0 || spaces.length > 0) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (snapshot.selected_room?.id || snapshot.selected_space?.id) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function snapshotSummary(snapshot) {
|
||||||
|
const selectedRoom = snapshot?.selected_room || snapshot?.selected_space || null;
|
||||||
|
const rooms = Array.isArray(snapshot?.rooms) ? snapshot.rooms : Array.isArray(snapshot?.spaces) ? snapshot.spaces : [];
|
||||||
|
const mainEntities = Array.isArray(snapshot?.main_entities) ? snapshot.main_entities : [];
|
||||||
|
const selectedEntities = Array.isArray(selectedRoom?.entities) ? selectedRoom.entities : [];
|
||||||
|
const popup = snapshot?.popup || {};
|
||||||
|
return {
|
||||||
|
mode: snapshot?.ui?.mode || 'unknown',
|
||||||
|
selected_room_id: selectedRoom?.id || null,
|
||||||
|
selected_room_name: selectedRoom?.name || null,
|
||||||
|
rooms: rooms.length,
|
||||||
|
main_entities: mainEntities.length,
|
||||||
|
selected_room_entities: selectedEntities.length,
|
||||||
|
popup_active: Boolean(popup.active),
|
||||||
|
popup_sensor: popup.sensor_entity_id || null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function requestSummary(action, params = {}) {
|
||||||
|
const summary = { action };
|
||||||
|
['space_id', 'room_id', 'entity_id', 'layout_item_id', 'command', 'value', 'hours', 'state', 'edit_mode'].forEach((key) => {
|
||||||
|
if (params[key] !== undefined && params[key] !== null && params[key] !== '') {
|
||||||
|
summary[key] = params[key];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (params.payload && typeof params.payload === 'object') {
|
||||||
|
summary.payload_keys = Object.keys(params.payload);
|
||||||
|
}
|
||||||
|
return summary;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function resolveInitialSnapshot() {
|
||||||
|
const bootstrapSnapshot = window.APP_BOOTSTRAP || bootstrap || {};
|
||||||
|
debugLog('resolveInitialSnapshot()', {
|
||||||
|
ha_runtime: isHaRuntime(),
|
||||||
|
bridge_ready: Boolean(haBridge()),
|
||||||
|
bootstrap_ready: snapshotLooksReady(bootstrapSnapshot),
|
||||||
|
});
|
||||||
|
if (!isHaRuntime()) {
|
||||||
|
debugLog('resolveInitialSnapshot -> bootstrap (standalone)', snapshotSummary(bootstrapSnapshot));
|
||||||
|
return bootstrapSnapshot;
|
||||||
|
}
|
||||||
|
|
||||||
|
const waitForHaBridge = async (timeoutMs = 1000) => {
|
||||||
|
const startedAt = Date.now();
|
||||||
|
let bridge = haBridge();
|
||||||
|
while (!bridge && (Date.now() - startedAt) < timeoutMs) {
|
||||||
|
await sleep(50);
|
||||||
|
bridge = haBridge();
|
||||||
|
}
|
||||||
|
return bridge;
|
||||||
|
};
|
||||||
|
|
||||||
|
const tryBridgeSnapshot = async () => {
|
||||||
|
const bridge = await waitForHaBridge();
|
||||||
|
if (!bridge) {
|
||||||
|
debugLog('HA bridge not ready yet');
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const roomId = state.selectedRoomId || 'main';
|
||||||
|
try {
|
||||||
|
if (typeof bridge.getSnapshot === 'function') {
|
||||||
|
debugLog('request initial snapshot via bridge.getSnapshot()', { room_id: roomId });
|
||||||
|
const snapshot = await bridge.getSnapshot(roomId);
|
||||||
|
if (snapshotLooksReady(snapshot)) {
|
||||||
|
debugLog('initial snapshot received via bridge.getSnapshot()', snapshotSummary(snapshot));
|
||||||
|
return snapshot;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
debugWarn('initial snapshot bridge.getSnapshot() failed', error);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (typeof bridge.request === 'function') {
|
||||||
|
debugLog('request initial snapshot via bridge.request(GET snapshot)', { room_id: roomId });
|
||||||
|
const snapshot = await bridge.request('GET', 'snapshot', { space_id: roomId });
|
||||||
|
if (snapshotLooksReady(snapshot)) {
|
||||||
|
debugLog('initial snapshot received via bridge.request(GET snapshot)', snapshotSummary(snapshot));
|
||||||
|
return snapshot;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
debugWarn('initial snapshot bridge.request(GET snapshot) failed', error);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const firstPass = await tryBridgeSnapshot();
|
||||||
|
if (firstPass) {
|
||||||
|
return firstPass;
|
||||||
|
}
|
||||||
|
|
||||||
|
await sleep(150);
|
||||||
|
|
||||||
|
const secondPass = await tryBridgeSnapshot();
|
||||||
|
if (secondPass) {
|
||||||
|
return secondPass;
|
||||||
|
}
|
||||||
|
|
||||||
|
debugLog('resolveInitialSnapshot -> fallback bootstrap', snapshotSummary(bootstrapSnapshot));
|
||||||
|
return snapshotLooksReady(bootstrapSnapshot) ? bootstrapSnapshot : bootstrapSnapshot;
|
||||||
}
|
}
|
||||||
|
|
||||||
const PRESSABLE_SELECTOR = [
|
const PRESSABLE_SELECTOR = [
|
||||||
@ -198,6 +388,7 @@
|
|||||||
|
|
||||||
const wrap = document.createElement('span');
|
const wrap = document.createElement('span');
|
||||||
wrap.className = 'icon-node';
|
wrap.className = 'icon-node';
|
||||||
|
wrap.appendChild(createIconElement(fallback));
|
||||||
Promise.resolve(getIcon(name)).then((definition) => {
|
Promise.resolve(getIcon(name)).then((definition) => {
|
||||||
if (!definition || !wrap.isConnected) return;
|
if (!definition || !wrap.isConnected) return;
|
||||||
wrap.replaceChildren(createSvgIcon(definition));
|
wrap.replaceChildren(createSvgIcon(definition));
|
||||||
@ -229,6 +420,7 @@
|
|||||||
: source.replace(/^fab:/, 'fa-brands:');
|
: source.replace(/^fab:/, 'fa-brands:');
|
||||||
const wrap = document.createElement('span');
|
const wrap = document.createElement('span');
|
||||||
wrap.className = 'icon-node';
|
wrap.className = 'icon-node';
|
||||||
|
wrap.appendChild(createIconElement(fallback));
|
||||||
const img = document.createElement('img');
|
const img = document.createElement('img');
|
||||||
img.className = 'icon-node__img';
|
img.className = 'icon-node__img';
|
||||||
img.alt = '';
|
img.alt = '';
|
||||||
@ -236,6 +428,10 @@
|
|||||||
img.loading = 'lazy';
|
img.loading = 'lazy';
|
||||||
img.referrerPolicy = 'no-referrer';
|
img.referrerPolicy = 'no-referrer';
|
||||||
img.src = `https://api.iconify.design/${mappedSource}.svg`;
|
img.src = `https://api.iconify.design/${mappedSource}.svg`;
|
||||||
|
img.addEventListener('load', () => {
|
||||||
|
if (!img.isConnected || !wrap.isConnected) return;
|
||||||
|
wrap.replaceChildren(img);
|
||||||
|
});
|
||||||
img.addEventListener('error', () => {
|
img.addEventListener('error', () => {
|
||||||
if (img.dataset.fallbackApplied === '1') return;
|
if (img.dataset.fallbackApplied === '1') return;
|
||||||
img.dataset.fallbackApplied = '1';
|
img.dataset.fallbackApplied = '1';
|
||||||
@ -252,6 +448,7 @@
|
|||||||
|
|
||||||
const wrap = document.createElement('span');
|
const wrap = document.createElement('span');
|
||||||
wrap.className = 'icon-node';
|
wrap.className = 'icon-node';
|
||||||
|
wrap.appendChild(createIconElement(fallback));
|
||||||
|
|
||||||
if (source.includes(':')) {
|
if (source.includes(':')) {
|
||||||
const img = document.createElement('img');
|
const img = document.createElement('img');
|
||||||
@ -261,6 +458,10 @@
|
|||||||
img.loading = 'lazy';
|
img.loading = 'lazy';
|
||||||
img.referrerPolicy = 'no-referrer';
|
img.referrerPolicy = 'no-referrer';
|
||||||
img.src = `https://api.iconify.design/${source}.svg`;
|
img.src = `https://api.iconify.design/${source}.svg`;
|
||||||
|
img.addEventListener('load', () => {
|
||||||
|
if (!img.isConnected || !wrap.isConnected) return;
|
||||||
|
wrap.replaceChildren(img);
|
||||||
|
});
|
||||||
img.addEventListener('error', () => {
|
img.addEventListener('error', () => {
|
||||||
if (img.dataset.fallbackApplied === '1') return;
|
if (img.dataset.fallbackApplied === '1') return;
|
||||||
img.dataset.fallbackApplied = '1';
|
img.dataset.fallbackApplied = '1';
|
||||||
@ -448,6 +649,9 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function buildUrl(action, params = {}) {
|
function buildUrl(action, params = {}) {
|
||||||
|
if (isHaRuntime()) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
const url = new URL('api.php', window.location.href);
|
const url = new URL('api.php', window.location.href);
|
||||||
url.searchParams.set('action', action);
|
url.searchParams.set('action', action);
|
||||||
Object.entries(params).forEach(([key, value]) => {
|
Object.entries(params).forEach(([key, value]) => {
|
||||||
@ -463,6 +667,12 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function apiGet(action, params = {}) {
|
async function apiGet(action, params = {}) {
|
||||||
|
const bridge = haBridge();
|
||||||
|
if (bridge?.request) {
|
||||||
|
debugLog('apiGet via bridge', requestSummary(action, params));
|
||||||
|
return bridge.request('GET', action, params);
|
||||||
|
}
|
||||||
|
debugLog('apiGet via http', requestSummary(action, params));
|
||||||
const res = await fetch(buildUrl(action, params), {
|
const res = await fetch(buildUrl(action, params), {
|
||||||
headers: { Accept: 'application/json' },
|
headers: { Accept: 'application/json' },
|
||||||
cache: 'no-store',
|
cache: 'no-store',
|
||||||
@ -474,6 +684,12 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function apiPost(action, payload = {}) {
|
async function apiPost(action, payload = {}) {
|
||||||
|
const bridge = haBridge();
|
||||||
|
if (bridge?.request) {
|
||||||
|
debugLog('apiPost via bridge', requestSummary(action, payload));
|
||||||
|
return bridge.request('POST', action, payload);
|
||||||
|
}
|
||||||
|
debugLog('apiPost via http', requestSummary(action, payload));
|
||||||
const res = await fetch(buildUrl(action), {
|
const res = await fetch(buildUrl(action), {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
|
headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
|
||||||
@ -487,7 +703,11 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function fetchSnapshot(roomId = state.selectedRoomId || 'main') {
|
async function fetchSnapshot(roomId = state.selectedRoomId || 'main') {
|
||||||
return apiGet('snapshot', { space_id: roomId || 'main' });
|
const response = await apiGet('snapshot', { space_id: roomId || 'main' });
|
||||||
|
if (response && response.ok === true && response.selected_room) {
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
return response;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadSnapshot(roomId = state.selectedRoomId || 'main') {
|
async function loadSnapshot(roomId = state.selectedRoomId || 'main') {
|
||||||
@ -1004,6 +1224,87 @@
|
|||||||
return q('.main-dashboard__cards', els.dashboardSurface);
|
return q('.main-dashboard__cards', els.dashboardSurface);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function renderMainRoomSummaryGrid(snapshot) {
|
||||||
|
const rooms = Array.isArray(snapshot?.spaces) ? snapshot.spaces : Array.isArray(snapshot?.rooms) ? snapshot.rooms : [];
|
||||||
|
const batteryRoom = snapshot?.battery_room || null;
|
||||||
|
const cards = document.createElement('div');
|
||||||
|
cards.className = 'room-list__group main-dashboard__room-grid';
|
||||||
|
|
||||||
|
const roomCard = (room, options = {}) => {
|
||||||
|
if (!room) return null;
|
||||||
|
const card = document.createElement('div');
|
||||||
|
card.className = `room-item ${room.id === state.selectedRoomId ? 'is-selected' : ''} ${room.id === 'main' ? 'is-main' : ''} ${room.virtual ? 'is-virtual is-battery-room' : ''} ${options.hidden ? 'is-hidden-room' : ''}`.trim();
|
||||||
|
card.dataset.roomId = room.id;
|
||||||
|
card.tabIndex = 0;
|
||||||
|
card.setAttribute('role', 'button');
|
||||||
|
card.addEventListener('click', () => setSelectedRoom(room.id));
|
||||||
|
card.addEventListener('keydown', (event) => {
|
||||||
|
if (event.key === 'Enter' || event.key === ' ') {
|
||||||
|
event.preventDefault();
|
||||||
|
setSelectedRoom(room.id);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const content = document.createElement('div');
|
||||||
|
content.className = 'room-item__content';
|
||||||
|
|
||||||
|
const icon = document.createElement('div');
|
||||||
|
icon.className = 'room-item__icon';
|
||||||
|
icon.appendChild(createIconElement(room.icon || 'mdi:home-variant'));
|
||||||
|
|
||||||
|
const body = document.createElement('div');
|
||||||
|
body.className = 'room-item__body';
|
||||||
|
const activeCount = room.id === 'batteries'
|
||||||
|
? Number(room.problem_count ?? room.active_entity_count ?? room.entity_count ?? 0) || 0
|
||||||
|
: Number(room.active_entity_count ?? room.entity_count ?? 0) || 0;
|
||||||
|
const metaText = room.id === 'main'
|
||||||
|
? 'Главный экран'
|
||||||
|
: room.id === 'batteries'
|
||||||
|
? (room.battery_summary_text || `${room.entity_count || 0} батареек`)
|
||||||
|
: activeCount > 0
|
||||||
|
? `${activeCount} ${pluralizeActiveEntities(activeCount)}`
|
||||||
|
: 'Нет активных';
|
||||||
|
body.innerHTML = `
|
||||||
|
<div class="room-item__name">${esc(room.name || '')}</div>
|
||||||
|
<div class="room-item__meta">${esc(metaText)}</div>
|
||||||
|
`;
|
||||||
|
content.append(icon, body);
|
||||||
|
|
||||||
|
const tempBadge = roomTemperatureBadge(snapshot, room);
|
||||||
|
if (tempBadge) {
|
||||||
|
card.classList.add('has-temp');
|
||||||
|
const temp = document.createElement('div');
|
||||||
|
temp.className = 'room-item__temp';
|
||||||
|
temp.textContent = tempBadge;
|
||||||
|
card.appendChild(temp);
|
||||||
|
}
|
||||||
|
|
||||||
|
card.append(content);
|
||||||
|
return card;
|
||||||
|
};
|
||||||
|
|
||||||
|
const orderedRooms = [...rooms]
|
||||||
|
.filter((room) => room && room.id !== 'main' && room.visible !== false && room.id !== 'batteries')
|
||||||
|
.sort((left, right) => {
|
||||||
|
const leftOrder = Number(left.order ?? 9999);
|
||||||
|
const rightOrder = Number(right.order ?? 9999);
|
||||||
|
if (leftOrder !== rightOrder) return leftOrder - rightOrder;
|
||||||
|
return String(left.name || '').localeCompare(String(right.name || ''), 'ru');
|
||||||
|
});
|
||||||
|
|
||||||
|
orderedRooms.forEach((room) => {
|
||||||
|
const card = roomCard(room);
|
||||||
|
if (card) cards.appendChild(card);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (batteryRoom && !isMobileViewport()) {
|
||||||
|
const card = roomCard(batteryRoom);
|
||||||
|
if (card) cards.appendChild(card);
|
||||||
|
}
|
||||||
|
|
||||||
|
return cards;
|
||||||
|
}
|
||||||
|
|
||||||
function currentDashboardCardsContainer() {
|
function currentDashboardCardsContainer() {
|
||||||
const snapshot = state.snapshot || bootstrap;
|
const snapshot = state.snapshot || bootstrap;
|
||||||
const room = snapshot.selected_space || snapshot.selected_room || {};
|
const room = snapshot.selected_space || snapshot.selected_room || {};
|
||||||
@ -1613,10 +1914,16 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function haConnection() {
|
function haConnection() {
|
||||||
|
if (isHaRuntime()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
return state.snapshot?.settings?.ha_connection || bootstrap?.settings?.ha_connection || {};
|
return state.snapshot?.settings?.ha_connection || bootstrap?.settings?.ha_connection || {};
|
||||||
}
|
}
|
||||||
|
|
||||||
function haWsUrl(baseUrl) {
|
function haWsUrl(baseUrl) {
|
||||||
|
if (isHaRuntime()) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
if (!baseUrl) return '';
|
if (!baseUrl) return '';
|
||||||
try {
|
try {
|
||||||
const url = new URL(baseUrl);
|
const url = new URL(baseUrl);
|
||||||
@ -3342,6 +3649,9 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function renderRoomButtons(snapshot, rooms, batteryRoom = null) {
|
function renderRoomButtons(snapshot, rooms, batteryRoom = null) {
|
||||||
|
if (!els.roomList) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
els.roomList.innerHTML = '';
|
els.roomList.innerHTML = '';
|
||||||
const sortedRooms = [...(rooms || [])].sort((left, right) => {
|
const sortedRooms = [...(rooms || [])].sort((left, right) => {
|
||||||
if (left.id === 'main') return -1;
|
if (left.id === 'main') return -1;
|
||||||
@ -3466,6 +3776,11 @@
|
|||||||
|
|
||||||
function renderSelectedRoom(snapshot) {
|
function renderSelectedRoom(snapshot) {
|
||||||
const room = snapshot.selected_space || snapshot.selected_room || {};
|
const room = snapshot.selected_space || snapshot.selected_room || {};
|
||||||
|
const setText = (el, value) => {
|
||||||
|
if (el) {
|
||||||
|
el.textContent = value;
|
||||||
|
}
|
||||||
|
};
|
||||||
if (els.contentTop) {
|
if (els.contentTop) {
|
||||||
els.contentTop.classList.toggle('is-main', room.id === 'main');
|
els.contentTop.classList.toggle('is-main', room.id === 'main');
|
||||||
}
|
}
|
||||||
@ -3474,8 +3789,8 @@
|
|||||||
}
|
}
|
||||||
updateMainPrintStrip(snapshot);
|
updateMainPrintStrip(snapshot);
|
||||||
if (room.id === 'batteries') {
|
if (room.id === 'batteries') {
|
||||||
els.selectedRoomEyebrow.textContent = 'Псевдо-комната';
|
setText(els.selectedRoomEyebrow, 'Псевдо-комната');
|
||||||
els.selectedRoomTitle.textContent = room.name || 'Батарейки';
|
setText(els.selectedRoomTitle, room.name || 'Батарейки');
|
||||||
const total = Number(room.entity_count ?? 0) || 0;
|
const total = Number(room.entity_count ?? 0) || 0;
|
||||||
const critical = Number(room.problem_count ?? room.active_entity_count ?? 0) || 0;
|
const critical = Number(room.problem_count ?? room.active_entity_count ?? 0) || 0;
|
||||||
const unavailable = Number(room.unavailable_count ?? 0) || 0;
|
const unavailable = Number(room.unavailable_count ?? 0) || 0;
|
||||||
@ -3490,26 +3805,26 @@
|
|||||||
if (unknown > 0) {
|
if (unknown > 0) {
|
||||||
summaryParts.push(`${unknown} ${pluralizeRu(unknown, 'неизвестная', 'неизвестных', 'неизвестных')}`);
|
summaryParts.push(`${unknown} ${pluralizeRu(unknown, 'неизвестная', 'неизвестных', 'неизвестных')}`);
|
||||||
}
|
}
|
||||||
els.selectedRoomMeta.textContent = summaryParts.length
|
setText(els.selectedRoomMeta, summaryParts.length
|
||||||
? `${summaryParts.join(' · ')} · ${total} ${pluralizeRu(total, 'батарейка', 'батарейки', 'батареек')}`
|
? `${summaryParts.join(' · ')} · ${total} ${pluralizeRu(total, 'батарейка', 'батарейки', 'батареек')}`
|
||||||
: `${total} ${pluralizeRu(total, 'батарейка', 'батарейки', 'батареек')}`;
|
: `${total} ${pluralizeRu(total, 'батарейка', 'батарейки', 'батареек')}`);
|
||||||
renderSelectedRoomActions(snapshot);
|
renderSelectedRoomActions(snapshot);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (room.id !== 'main') {
|
if (room.id !== 'main') {
|
||||||
els.selectedRoomEyebrow.textContent = 'Пространство';
|
setText(els.selectedRoomEyebrow, 'Пространство');
|
||||||
els.selectedRoomTitle.textContent = room.name || 'Панель';
|
setText(els.selectedRoomTitle, room.name || 'Панель');
|
||||||
const entities = roomEntities(snapshot, room.id || 'main');
|
const entities = roomEntities(snapshot, room.id || 'main');
|
||||||
const activeCount = Number(room.active_entity_count ?? entities.length) || 0;
|
const activeCount = Number(room.active_entity_count ?? entities.length) || 0;
|
||||||
els.selectedRoomMeta.textContent = `${activeCount} ${pluralizeActiveEntities(activeCount)}`;
|
setText(els.selectedRoomMeta, `${activeCount} ${pluralizeActiveEntities(activeCount)}`);
|
||||||
renderSelectedRoomActions(snapshot);
|
renderSelectedRoomActions(snapshot);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const entities = roomEntities(snapshot, room.id || 'main');
|
const entities = roomEntities(snapshot, room.id || 'main');
|
||||||
els.selectedRoomEyebrow.textContent = '';
|
setText(els.selectedRoomEyebrow, '');
|
||||||
els.selectedRoomTitle.textContent = room.name || 'Панель';
|
setText(els.selectedRoomTitle, room.name || 'Панель');
|
||||||
els.selectedRoomMeta.textContent = `${entities.length} ${pluralizeIncludedEntities(entities.length)}`;
|
setText(els.selectedRoomMeta, `${entities.length} ${pluralizeIncludedEntities(entities.length)}`);
|
||||||
renderSelectedRoomActions(snapshot);
|
renderSelectedRoomActions(snapshot);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -3545,23 +3860,32 @@
|
|||||||
function renderDashboard(snapshot) {
|
function renderDashboard(snapshot) {
|
||||||
const room = snapshot.selected_space || snapshot.selected_room || {};
|
const room = snapshot.selected_space || snapshot.selected_room || {};
|
||||||
const grid = els.dashboardSurface;
|
const grid = els.dashboardSurface;
|
||||||
|
if (!grid) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
grid.innerHTML = '';
|
grid.innerHTML = '';
|
||||||
|
|
||||||
if (room.id === 'main') {
|
if (room.id === 'main') {
|
||||||
const layout = document.createElement('div');
|
const layout = document.createElement('div');
|
||||||
layout.className = 'main-dashboard';
|
layout.className = 'main-dashboard';
|
||||||
|
|
||||||
const hero = renderMainHero(snapshot);
|
const mainEntities = roomEntities(snapshot, 'main');
|
||||||
|
|
||||||
const cards = document.createElement('div');
|
const cards = document.createElement('div');
|
||||||
cards.className = 'grid-surface main-dashboard__cards';
|
cards.className = 'grid-surface main-dashboard__cards';
|
||||||
|
|
||||||
const mainEntities = roomEntities(snapshot, 'main');
|
if (mainEntities.length) {
|
||||||
mainEntities.forEach((entity) => {
|
mainEntities.forEach((entity) => {
|
||||||
cards.appendChild(renderEntityCard(entity, { isMain: true }));
|
cards.appendChild(renderEntityCard(entity, { isMain: true }));
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const hero = renderMainHero(snapshot);
|
||||||
|
|
||||||
|
layout.append(hero);
|
||||||
|
if (mainEntities.length) {
|
||||||
|
layout.append(cards);
|
||||||
|
}
|
||||||
|
|
||||||
layout.append(hero, cards);
|
|
||||||
grid.appendChild(layout);
|
grid.appendChild(layout);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -3683,8 +4007,10 @@
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
if (popup.active && els.cameraBackdrop?.classList.contains('is-open') && signature === state.lastPopupSignature) {
|
if (popup.active && els.cameraBackdrop?.classList.contains('is-open') && signature === state.lastPopupSignature) {
|
||||||
|
if (els.cameraBackdrop) {
|
||||||
els.cameraBackdrop.classList.add('is-open');
|
els.cameraBackdrop.classList.add('is-open');
|
||||||
els.cameraBackdrop.setAttribute('aria-hidden', 'false');
|
els.cameraBackdrop.setAttribute('aria-hidden', 'false');
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -3695,11 +4021,17 @@
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (els.cameraPoster) {
|
||||||
els.cameraPoster.src = popup.poster_url || '';
|
els.cameraPoster.src = popup.poster_url || '';
|
||||||
els.cameraPoster.alt = popup.sensor_entity_id || 'camera';
|
els.cameraPoster.alt = popup.sensor_entity_id || 'camera';
|
||||||
|
}
|
||||||
|
if (els.cameraBackdrop) {
|
||||||
els.cameraBackdrop.classList.add('is-open');
|
els.cameraBackdrop.classList.add('is-open');
|
||||||
els.cameraBackdrop.setAttribute('aria-hidden', 'false');
|
els.cameraBackdrop.setAttribute('aria-hidden', 'false');
|
||||||
|
}
|
||||||
|
if (els.cameraPlaceholder) {
|
||||||
els.cameraPlaceholder.classList.add('is-visible');
|
els.cameraPlaceholder.classList.add('is-visible');
|
||||||
|
}
|
||||||
|
|
||||||
const expiresAt = Number(popup.expires_at || 0);
|
const expiresAt = Number(popup.expires_at || 0);
|
||||||
if (expiresAt > 0) {
|
if (expiresAt > 0) {
|
||||||
@ -3709,11 +4041,15 @@
|
|||||||
const mins = Math.floor(remaining / 60);
|
const mins = Math.floor(remaining / 60);
|
||||||
const secs = remaining % 60;
|
const secs = remaining % 60;
|
||||||
if (remaining > 0) {
|
if (remaining > 0) {
|
||||||
|
if (els.cameraCountdown) {
|
||||||
els.cameraCountdown.textContent = `Закроется через ${mins}:${String(secs).padStart(2, '0')}`;
|
els.cameraCountdown.textContent = `Закроется через ${mins}:${String(secs).padStart(2, '0')}`;
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (els.cameraCountdown) {
|
||||||
els.cameraCountdown.textContent = 'Закрытие...';
|
els.cameraCountdown.textContent = 'Закрытие...';
|
||||||
|
}
|
||||||
if (closeRequested) {
|
if (closeRequested) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -3736,7 +4072,9 @@
|
|||||||
clearInterval(state.popupDismissTimer);
|
clearInterval(state.popupDismissTimer);
|
||||||
state.popupDismissTimer = setInterval(updateCountdown, 1000);
|
state.popupDismissTimer = setInterval(updateCountdown, 1000);
|
||||||
} else {
|
} else {
|
||||||
|
if (els.cameraCountdown) {
|
||||||
els.cameraCountdown.textContent = '';
|
els.cameraCountdown.textContent = '';
|
||||||
|
}
|
||||||
clearInterval(state.popupDismissTimer);
|
clearInterval(state.popupDismissTimer);
|
||||||
state.popupDismissTimer = null;
|
state.popupDismissTimer = null;
|
||||||
}
|
}
|
||||||
@ -3759,14 +4097,26 @@
|
|||||||
active: false,
|
active: false,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
if (els.cameraBackdrop) {
|
||||||
els.cameraBackdrop.classList.remove('is-open');
|
els.cameraBackdrop.classList.remove('is-open');
|
||||||
els.cameraBackdrop.setAttribute('aria-hidden', 'true');
|
els.cameraBackdrop.setAttribute('aria-hidden', 'true');
|
||||||
|
}
|
||||||
|
if (els.cameraStage) {
|
||||||
els.cameraStage.innerHTML = '';
|
els.cameraStage.innerHTML = '';
|
||||||
|
if (els.cameraPoster) {
|
||||||
els.cameraStage.appendChild(els.cameraPoster);
|
els.cameraStage.appendChild(els.cameraPoster);
|
||||||
|
}
|
||||||
|
if (els.cameraPlaceholder) {
|
||||||
els.cameraStage.appendChild(els.cameraPlaceholder);
|
els.cameraStage.appendChild(els.cameraPlaceholder);
|
||||||
els.cameraPlaceholder.classList.add('is-visible');
|
els.cameraPlaceholder.classList.add('is-visible');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (els.cameraPoster) {
|
||||||
els.cameraPoster.removeAttribute('src');
|
els.cameraPoster.removeAttribute('src');
|
||||||
|
}
|
||||||
|
if (els.cameraCountdown) {
|
||||||
els.cameraCountdown.textContent = '';
|
els.cameraCountdown.textContent = '';
|
||||||
|
}
|
||||||
clearInterval(state.popupDismissTimer);
|
clearInterval(state.popupDismissTimer);
|
||||||
state.popupDismissTimer = null;
|
state.popupDismissTimer = null;
|
||||||
destroyStream();
|
destroyStream();
|
||||||
@ -3916,19 +4266,35 @@
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const renderSignature = JSON.stringify([
|
||||||
|
snapshot?.selected_room?.id || snapshot?.selected_space?.id || 'main',
|
||||||
|
Array.isArray(snapshot?.rooms) ? snapshot.rooms.length : Array.isArray(snapshot?.spaces) ? snapshot.spaces.length : 0,
|
||||||
|
Array.isArray(snapshot?.main_entities) ? snapshot.main_entities.length : 0,
|
||||||
|
Boolean(snapshot?.popup?.active),
|
||||||
|
Boolean(snapshot?.ui?.mode === 'ha-native'),
|
||||||
|
]);
|
||||||
|
if (renderSignature !== state.debugLastRenderSignature) {
|
||||||
|
state.debugLastRenderSignature = renderSignature;
|
||||||
|
debugLog('render()', snapshotSummary(snapshot));
|
||||||
|
}
|
||||||
|
|
||||||
syncLayoutState();
|
syncLayoutState();
|
||||||
renderRoomButtons(snapshot, snapshot.spaces || snapshot.rooms, snapshot.battery_room);
|
|
||||||
renderSelectedRoom(snapshot);
|
|
||||||
renderDashboard(snapshot);
|
renderDashboard(snapshot);
|
||||||
|
renderSelectedRoom(snapshot);
|
||||||
|
renderRoomButtons(snapshot, snapshot.spaces || snapshot.rooms, snapshot.battery_room);
|
||||||
renderPopup(snapshot);
|
renderPopup(snapshot);
|
||||||
renderEntityPopup(snapshot);
|
renderEntityPopup(snapshot);
|
||||||
renderTemperatureSensorPopup(snapshot);
|
renderTemperatureSensorPopup(snapshot);
|
||||||
|
|
||||||
const roomCount = Math.max(0, (snapshot.spaces?.length || snapshot.rooms?.length || 1) - 1);
|
const roomCount = Math.max(0, (snapshot.spaces?.length || snapshot.rooms?.length || 1) - 1);
|
||||||
|
if (els.roomsCount) {
|
||||||
els.roomsCount.textContent = state.editMode ? `${roomCount} ${pluralizeRooms(roomCount)}` : '';
|
els.roomsCount.textContent = state.editMode ? `${roomCount} ${pluralizeRooms(roomCount)}` : '';
|
||||||
|
}
|
||||||
|
if (els.editModeToggle) {
|
||||||
els.editModeToggle.classList.toggle('is-active', state.editMode);
|
els.editModeToggle.classList.toggle('is-active', state.editMode);
|
||||||
els.editModeToggle.title = state.editMode ? 'Edit mode on' : 'Edit mode off';
|
els.editModeToggle.title = state.editMode ? 'Edit mode on' : 'Edit mode off';
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function renderDashboardOnly() {
|
function renderDashboardOnly() {
|
||||||
const snapshot = state.snapshot || bootstrap;
|
const snapshot = state.snapshot || bootstrap;
|
||||||
@ -3939,6 +4305,14 @@
|
|||||||
renderPopup(snapshot);
|
renderPopup(snapshot);
|
||||||
renderEntityPopup(snapshot);
|
renderEntityPopup(snapshot);
|
||||||
renderTemperatureSensorPopup(snapshot);
|
renderTemperatureSensorPopup(snapshot);
|
||||||
|
const roomCount = Math.max(0, (snapshot.spaces?.length || snapshot.rooms?.length || 1) - 1);
|
||||||
|
if (els.roomsCount) {
|
||||||
|
els.roomsCount.textContent = state.editMode ? `${roomCount} ${pluralizeRooms(roomCount)}` : '';
|
||||||
|
}
|
||||||
|
if (els.editModeToggle) {
|
||||||
|
els.editModeToggle.classList.toggle('is-active', state.editMode);
|
||||||
|
els.editModeToggle.title = state.editMode ? 'Edit mode on' : 'Edit mode off';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function refreshCurrentRoomLayout(entityId) {
|
function refreshCurrentRoomLayout(entityId) {
|
||||||
@ -3968,6 +4342,9 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
const container = els.dashboardSurface;
|
const container = els.dashboardSurface;
|
||||||
|
if (!container) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
const order = new Map(roomEntities(snapshot, room.id).map((entity) => [entity.entity_id, Number(entity.order ?? 9999)]));
|
const order = new Map(roomEntities(snapshot, room.id).map((entity) => [entity.entity_id, Number(entity.order ?? 9999)]));
|
||||||
const cards = Array.from(container.querySelectorAll('.grid-card[data-entity-id]'));
|
const cards = Array.from(container.querySelectorAll('.grid-card[data-entity-id]'));
|
||||||
cards.sort((left, right) => {
|
cards.sort((left, right) => {
|
||||||
@ -3984,10 +4361,14 @@
|
|||||||
if (!snapshot || !(snapshot.spaces || snapshot.rooms)) return;
|
if (!snapshot || !(snapshot.spaces || snapshot.rooms)) return;
|
||||||
renderRoomButtons(snapshot, snapshot.spaces || snapshot.rooms, snapshot.battery_room);
|
renderRoomButtons(snapshot, snapshot.spaces || snapshot.rooms, snapshot.battery_room);
|
||||||
const roomCount = Math.max(0, (snapshot.spaces?.length || snapshot.rooms?.length || 1) - 1);
|
const roomCount = Math.max(0, (snapshot.spaces?.length || snapshot.rooms?.length || 1) - 1);
|
||||||
|
if (els.roomsCount) {
|
||||||
els.roomsCount.textContent = state.editMode ? `${roomCount} ${pluralizeRooms(roomCount)}` : '';
|
els.roomsCount.textContent = state.editMode ? `${roomCount} ${pluralizeRooms(roomCount)}` : '';
|
||||||
|
}
|
||||||
|
if (els.editModeToggle) {
|
||||||
els.editModeToggle.classList.toggle('is-active', state.editMode);
|
els.editModeToggle.classList.toggle('is-active', state.editMode);
|
||||||
els.editModeToggle.title = state.editMode ? 'Edit mode on' : 'Edit mode off';
|
els.editModeToggle.title = state.editMode ? 'Edit mode on' : 'Edit mode off';
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function renderSelectionOnly() {
|
function renderSelectionOnly() {
|
||||||
const snapshot = state.snapshot || bootstrap;
|
const snapshot = state.snapshot || bootstrap;
|
||||||
@ -4191,6 +4572,11 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function wireEvents() {
|
function wireEvents() {
|
||||||
|
const bind = (el, type, handler, options) => {
|
||||||
|
if (!el) return;
|
||||||
|
el.addEventListener(type, handler, options);
|
||||||
|
};
|
||||||
|
|
||||||
els.selectedRoomBack?.addEventListener('click', () => {
|
els.selectedRoomBack?.addEventListener('click', () => {
|
||||||
if (!isMobileViewport()) return;
|
if (!isMobileViewport()) return;
|
||||||
closeEntityPopup();
|
closeEntityPopup();
|
||||||
@ -4200,14 +4586,14 @@
|
|||||||
renderSelectionOnly();
|
renderSelectionOnly();
|
||||||
});
|
});
|
||||||
|
|
||||||
els.cameraBackdrop.addEventListener('click', (event) => {
|
bind(els.cameraBackdrop, 'click', (event) => {
|
||||||
if (event.target === els.cameraBackdrop) {
|
if (event.target === els.cameraBackdrop) {
|
||||||
apiPost('popup', { command: 'close' }).catch(() => {});
|
apiPost('popup', { command: 'close' }).catch(() => {});
|
||||||
hidePopup({ suppressAutoOpen: true });
|
hidePopup({ suppressAutoOpen: true });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
els.cameraModalPanel.addEventListener('click', (event) => {
|
bind(els.cameraModalPanel, 'click', (event) => {
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -4224,8 +4610,8 @@
|
|||||||
hidePopup({ suppressAutoOpen: true });
|
hidePopup({ suppressAutoOpen: true });
|
||||||
};
|
};
|
||||||
|
|
||||||
els.cameraClose.addEventListener('pointerdown', closeCameraPopup);
|
bind(els.cameraClose, 'pointerdown', closeCameraPopup);
|
||||||
els.cameraClose.addEventListener('click', closeCameraPopup);
|
bind(els.cameraClose, 'click', closeCameraPopup);
|
||||||
|
|
||||||
els.entityBackdrop?.addEventListener('click', (event) => {
|
els.entityBackdrop?.addEventListener('click', (event) => {
|
||||||
if (event.target === els.entityBackdrop) {
|
if (event.target === els.entityBackdrop) {
|
||||||
@ -4233,7 +4619,7 @@
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
els.entityModalPanel?.addEventListener('click', (event) => {
|
bind(els.entityModalPanel, 'click', (event) => {
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -4247,19 +4633,19 @@
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
els.temperatureSensorModalPanel?.addEventListener('click', (event) => {
|
bind(els.temperatureSensorModalPanel, 'click', (event) => {
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
});
|
});
|
||||||
|
|
||||||
els.temperatureSensorClose?.addEventListener('click', () => {
|
bind(els.temperatureSensorClose, 'click', () => {
|
||||||
closeTemperatureSensorPopup();
|
closeTemperatureSensorPopup();
|
||||||
});
|
});
|
||||||
|
|
||||||
els.popupDebugButton?.addEventListener('click', () => {
|
bind(els.popupDebugButton, 'click', () => {
|
||||||
showDebugPopup();
|
showDebugPopup();
|
||||||
});
|
});
|
||||||
|
|
||||||
els.editModeToggle.addEventListener('click', async () => {
|
bind(els.editModeToggle, 'click', async () => {
|
||||||
state.editMode = !state.editMode;
|
state.editMode = !state.editMode;
|
||||||
try {
|
try {
|
||||||
await apiPost('save-settings', {
|
await apiPost('save-settings', {
|
||||||
@ -4380,6 +4766,9 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function startSnapshotPolling() {
|
function startSnapshotPolling() {
|
||||||
|
if (isHaRuntime()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
const interval = Math.max(1000, Number(state.snapshot?.settings?.poll_interval_ms || bootstrap?.settings?.poll_interval_ms || 5000));
|
const interval = Math.max(1000, Number(state.snapshot?.settings?.poll_interval_ms || bootstrap?.settings?.poll_interval_ms || 5000));
|
||||||
if (state.snapshotPollTimer) {
|
if (state.snapshotPollTimer) {
|
||||||
clearInterval(state.snapshotPollTimer);
|
clearInterval(state.snapshotPollTimer);
|
||||||
@ -4396,6 +4785,9 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function handleHaMessage(message) {
|
function handleHaMessage(message) {
|
||||||
|
if (isHaRuntime()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (!message || typeof message !== 'object') {
|
if (!message || typeof message !== 'object') {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -4519,6 +4911,11 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function connectRealtime() {
|
function connectRealtime() {
|
||||||
|
if (isHaRuntime()) {
|
||||||
|
setStatus('HA native mode', 'online');
|
||||||
|
stopSnapshotPolling();
|
||||||
|
return;
|
||||||
|
}
|
||||||
const connection = haConnection();
|
const connection = haConnection();
|
||||||
const baseUrl = connection.base_url || '';
|
const baseUrl = connection.base_url || '';
|
||||||
const token = connection.token || '';
|
const token = connection.token || '';
|
||||||
@ -4572,6 +4969,11 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function start() {
|
async function start() {
|
||||||
|
debugLog('start()', {
|
||||||
|
ha_runtime: isHaRuntime(),
|
||||||
|
embed_mode: Boolean(bootstrap?.ui?.embed),
|
||||||
|
mode: bootstrap?.ui?.mode || 'unknown',
|
||||||
|
});
|
||||||
initRefs();
|
initRefs();
|
||||||
state.embedMode = detectEmbeddedContext();
|
state.embedMode = detectEmbeddedContext();
|
||||||
syncLayoutState();
|
syncLayoutState();
|
||||||
@ -4582,6 +4984,19 @@
|
|||||||
state.clockTimer = setInterval(updateClock, 1000);
|
state.clockTimer = setInterval(updateClock, 1000);
|
||||||
wireEvents();
|
wireEvents();
|
||||||
|
|
||||||
|
if (!state.haSnapshotListenerInstalled) {
|
||||||
|
state.haSnapshotListenerInstalled = true;
|
||||||
|
window.addEventListener('wall-panel-snapshot-updated', (event) => {
|
||||||
|
const snapshot = event?.detail?.snapshot || event?.detail || null;
|
||||||
|
if (!snapshot || typeof snapshot !== 'object') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
debugLog('wall-panel-snapshot-updated', snapshotSummary(snapshot));
|
||||||
|
state.snapshot = snapshot;
|
||||||
|
render();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const viewportQuery = mobileViewportQuery();
|
const viewportQuery = mobileViewportQuery();
|
||||||
const handleViewportChange = () => {
|
const handleViewportChange = () => {
|
||||||
syncViewportState();
|
syncViewportState();
|
||||||
@ -4593,8 +5008,8 @@
|
|||||||
viewportQuery.addListener(handleViewportChange);
|
viewportQuery.addListener(handleViewportChange);
|
||||||
}
|
}
|
||||||
|
|
||||||
const initial = window.APP_BOOTSTRAP || {};
|
state.snapshot = await resolveInitialSnapshot();
|
||||||
state.snapshot = initial;
|
debugLog('initial snapshot applied', snapshotSummary(state.snapshot || bootstrap));
|
||||||
render();
|
render();
|
||||||
connectRealtime();
|
connectRealtime();
|
||||||
if (!state.snapshotPollTimer) {
|
if (!state.snapshotPollTimer) {
|
||||||
@ -4602,5 +5017,9 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (document.readyState === 'loading') {
|
||||||
document.addEventListener('DOMContentLoaded', start);
|
document.addEventListener('DOMContentLoaded', start);
|
||||||
|
} else {
|
||||||
|
start();
|
||||||
|
}
|
||||||
})();
|
})();
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user