-
This commit is contained in:
parent
03f21e2de8
commit
7e2f5b18b3
@ -49,6 +49,11 @@ body.is-embedded {
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
body.is-ha-native #camera-modal,
|
||||
body.is-ha-native .camera-modal {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
button,
|
||||
input,
|
||||
select,
|
||||
@ -261,7 +266,7 @@ textarea {
|
||||
|
||||
.room-list__group {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
@ -293,7 +298,8 @@ textarea {
|
||||
height: 42px;
|
||||
}
|
||||
|
||||
.app-shell--embed .room-item__icon i {
|
||||
.app-shell--embed .room-item__icon i,
|
||||
.app-shell--embed .room-item__icon ha-icon {
|
||||
font-size: 32px;
|
||||
}
|
||||
|
||||
@ -375,6 +381,17 @@ textarea {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.room-item__icon ha-icon,
|
||||
.grid-card__icon ha-icon,
|
||||
.mushroom-button__icon ha-icon,
|
||||
.icon-node--ha {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.icon-node__img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
@ -509,6 +526,10 @@ textarea {
|
||||
padding: 22px 16px 20px;
|
||||
}
|
||||
|
||||
.app-shell.is-ha-native .content-top {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.content-top {
|
||||
display: none;
|
||||
margin-bottom: 16px;
|
||||
|
||||
719
assets/app.js
719
assets/app.js
@ -3,7 +3,7 @@
|
||||
const MOBILE_BREAKPOINT = 920;
|
||||
const state = {
|
||||
snapshot: bootstrap,
|
||||
embedMode: Boolean(bootstrap?.ui?.embed),
|
||||
embedMode: Boolean(bootstrap?.ui?.embed || window.WALL_PANEL_HA_MODE),
|
||||
selectedRoomId: 'main',
|
||||
isMobileViewport: false,
|
||||
mobileView: 'spaces',
|
||||
@ -44,10 +44,14 @@
|
||||
snapshotPollTimer: null,
|
||||
haSnapshotListenerInstalled: false,
|
||||
debugLastRenderSignature: '',
|
||||
lastRenderSignature: '',
|
||||
lastSidebarRenderSignature: '',
|
||||
lastContentRenderSignature: '',
|
||||
};
|
||||
|
||||
const els = {};
|
||||
const client = window.StrikerPanelClient || (window.StrikerPanelClient = {});
|
||||
let renderFrame = null;
|
||||
const debugEnabled = (() => {
|
||||
try {
|
||||
if (window.StrikerPanelDebug) return true;
|
||||
@ -81,14 +85,20 @@
|
||||
}
|
||||
state.snapshot = snapshot;
|
||||
initRefs();
|
||||
state.embedMode = detectEmbeddedContext();
|
||||
state.embedMode = detectEmbeddedContext() || isHaRuntime();
|
||||
syncLayoutState();
|
||||
render();
|
||||
if (renderFrame) {
|
||||
return;
|
||||
}
|
||||
renderFrame = window.requestAnimationFrame(() => {
|
||||
renderFrame = null;
|
||||
render();
|
||||
});
|
||||
};
|
||||
|
||||
client.refresh = () => {
|
||||
initRefs();
|
||||
state.embedMode = detectEmbeddedContext();
|
||||
state.embedMode = detectEmbeddedContext() || isHaRuntime();
|
||||
syncLayoutState();
|
||||
render();
|
||||
};
|
||||
@ -118,11 +128,22 @@
|
||||
|
||||
function isHaRuntime() {
|
||||
return Boolean(
|
||||
window.WALL_PANEL_HA_MODE ||
|
||||
haBridge()
|
||||
|| bootstrap?.ui?.mode === 'ha-native'
|
||||
);
|
||||
}
|
||||
|
||||
async function waitForHaBridge(timeoutMs = 1000) {
|
||||
const startedAt = Date.now();
|
||||
let bridge = haBridge();
|
||||
while (!bridge && (Date.now() - startedAt) < timeoutMs) {
|
||||
await sleep(50);
|
||||
bridge = haBridge();
|
||||
}
|
||||
return bridge;
|
||||
}
|
||||
|
||||
function sleep(ms) {
|
||||
return new Promise((resolve) => window.setTimeout(resolve, Math.max(0, Number(ms) || 0)));
|
||||
}
|
||||
@ -160,6 +181,254 @@
|
||||
};
|
||||
}
|
||||
|
||||
function snapshotEntityToken(snapshot, entityId) {
|
||||
if (!snapshot || !entityId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const collections = [
|
||||
snapshot.main_entities,
|
||||
snapshot.selected_space?.entities,
|
||||
snapshot.selected_room?.entities,
|
||||
];
|
||||
if (snapshot.entity_index && typeof snapshot.entity_index === 'object') {
|
||||
collections.push(Object.values(snapshot.entity_index));
|
||||
}
|
||||
if (snapshot.space_index && typeof snapshot.space_index === 'object') {
|
||||
Object.values(snapshot.space_index).forEach((room) => {
|
||||
if (room?.entities) {
|
||||
collections.push(room.entities);
|
||||
}
|
||||
});
|
||||
}
|
||||
if (snapshot.space_entities && typeof snapshot.space_entities === 'object') {
|
||||
Object.values(snapshot.space_entities).forEach((entities) => collections.push(entities));
|
||||
}
|
||||
if (snapshot.battery_room?.entities) {
|
||||
collections.push(snapshot.battery_room.entities);
|
||||
}
|
||||
|
||||
for (const collection of collections) {
|
||||
if (!Array.isArray(collection)) continue;
|
||||
const found = collection.find((entity) => entity && entity.entity_id === entityId);
|
||||
if (found) {
|
||||
const attr = found.attributes || {};
|
||||
return [
|
||||
String(found.entity_id || ''),
|
||||
String(found.state ?? ''),
|
||||
String(found.last_changed || found.last_updated || ''),
|
||||
String(attr.current_temperature ?? attr.temperature ?? ''),
|
||||
String(attr.current_position ?? ''),
|
||||
String(attr.hvac_action ?? ''),
|
||||
];
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function entityRenderToken(entity) {
|
||||
const attr = entity?.attributes || {};
|
||||
return [
|
||||
String(entity?.entity_id || ''),
|
||||
String(entity?.state ?? ''),
|
||||
String(entity?.order ?? 9999),
|
||||
entity?.visible === false ? '0' : '1',
|
||||
String(entity?.domain || ''),
|
||||
String(entity?.card_type || ''),
|
||||
String(entity?.subtitle || ''),
|
||||
String(entity?.icon || ''),
|
||||
String(attr.current_temperature ?? attr.temperature ?? ''),
|
||||
String(attr.current_position ?? ''),
|
||||
String(attr.hvac_action ?? ''),
|
||||
];
|
||||
}
|
||||
|
||||
function roomRenderToken(room) {
|
||||
return [
|
||||
String(room?.id || ''),
|
||||
String(room?.name || ''),
|
||||
String(room?.icon || ''),
|
||||
room?.visible === false ? '0' : '1',
|
||||
String(room?.order ?? 9999),
|
||||
String(room?.entity_count ?? 0),
|
||||
String(room?.active_entity_count ?? 0),
|
||||
String(room?.temperature_badge || ''),
|
||||
String(room?.battery_summary_text || ''),
|
||||
room?.virtual ? '1' : '0',
|
||||
];
|
||||
}
|
||||
|
||||
function selectedRoomRenderToken(snapshot) {
|
||||
const room = snapshot?.selected_room || snapshot?.selected_space || {};
|
||||
if (!room?.id) {
|
||||
return ['unknown'];
|
||||
}
|
||||
|
||||
if (room.id === 'main') {
|
||||
const boiler = snapshot?.settings?.main_boiler || {};
|
||||
const printConfig = snapshot?.settings?.main_print || {};
|
||||
const weatherActions = Array.isArray(snapshot?.settings?.main_weather_actions)
|
||||
? snapshot.settings.main_weather_actions
|
||||
: [];
|
||||
return [
|
||||
'main',
|
||||
room.name || '',
|
||||
(Array.isArray(snapshot?.main_entities) ? snapshot.main_entities : []).map(entityRenderToken),
|
||||
snapshotEntityToken(snapshot, boiler.sensor_entity_id || ''),
|
||||
snapshotEntityToken(snapshot, printConfig.current_stage_entity_id || ''),
|
||||
snapshotEntityToken(snapshot, printConfig.print_progress_entity_id || ''),
|
||||
snapshotEntityToken(snapshot, printConfig.start_time_entity_id || ''),
|
||||
snapshotEntityToken(snapshot, printConfig.end_time_entity_id || ''),
|
||||
weatherActions.map((action) => [
|
||||
String(action?.entity_id || ''),
|
||||
String(action?.state_entity_id || ''),
|
||||
String(action?.command || ''),
|
||||
String(action?.value ?? ''),
|
||||
String(action?.active_value ?? ''),
|
||||
String(action?.label_active || ''),
|
||||
String(action?.label_inactive || ''),
|
||||
String(mainWeatherActionIsActive(snapshot, action) ? '1' : '0'),
|
||||
]),
|
||||
snapshot?.weather ? [
|
||||
String(snapshot.weather.entity_id || ''),
|
||||
String(snapshot.weather.state ?? ''),
|
||||
String(snapshot.weather.temperature ?? ''),
|
||||
String(snapshot.weather.sensor_temperature ?? ''),
|
||||
String(snapshot.weather.wind_speed ?? ''),
|
||||
String(snapshot.weather.condition ?? ''),
|
||||
] : null,
|
||||
];
|
||||
}
|
||||
|
||||
if (room.id === 'batteries') {
|
||||
return [
|
||||
'batteries',
|
||||
room.name || '',
|
||||
room.battery_summary_text || '',
|
||||
Number(room.entity_count ?? 0) || 0,
|
||||
Number(room.problem_count ?? room.active_entity_count ?? 0) || 0,
|
||||
(Array.isArray(room.entities) ? room.entities : []).map((item) => [
|
||||
String(item?.entity_id || ''),
|
||||
String(item?.battery_status || ''),
|
||||
String(item?.battery_percent_text || ''),
|
||||
String(item?.forecast_minutes_left ?? ''),
|
||||
String(item?.forecast_text || ''),
|
||||
String(item?.source_text || ''),
|
||||
]),
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
room.id,
|
||||
room.name || '',
|
||||
room.icon || '',
|
||||
room.visible === false ? '0' : '1',
|
||||
String(room.order ?? 9999),
|
||||
String(room.temperature_badge || ''),
|
||||
roomGridEntries(snapshot, room.id).map((entry) => (
|
||||
entry.kind === 'layout'
|
||||
? ['layout', String(entry.id || ''), String(entry.order ?? 9999), String(entry.payload?.type || 'ghost')]
|
||||
: ['entity', ...entityRenderToken(entry.payload)]
|
||||
)),
|
||||
state.editMode
|
||||
? roomEntitiesIncludingHidden(snapshot, room.id)
|
||||
.filter((entity) => entity.visible === false)
|
||||
.map(entityRenderToken)
|
||||
: [],
|
||||
];
|
||||
}
|
||||
|
||||
function buildVisibleSnapshotSignature(snapshot) {
|
||||
const rooms = Array.isArray(snapshot?.rooms) ? snapshot.rooms : Array.isArray(snapshot?.spaces) ? snapshot.spaces : [];
|
||||
const selectedRoom = snapshot?.selected_room || snapshot?.selected_space || {};
|
||||
const batteryRoom = snapshot?.battery_room || null;
|
||||
return JSON.stringify([
|
||||
String(snapshot?.ui?.mode || 'unknown'),
|
||||
String(selectedRoom?.id || 'main'),
|
||||
rooms.map(roomRenderToken),
|
||||
batteryRoom ? roomRenderToken(batteryRoom) : null,
|
||||
selectedRoomRenderToken(snapshot),
|
||||
]);
|
||||
}
|
||||
|
||||
function buildSidebarRenderSignature(snapshot) {
|
||||
const rooms = Array.isArray(snapshot?.rooms) ? snapshot.rooms : Array.isArray(snapshot?.spaces) ? snapshot.spaces : [];
|
||||
const batteryRoom = snapshot?.battery_room || null;
|
||||
return JSON.stringify([
|
||||
String(state.selectedRoomId || 'main'),
|
||||
String(state.editMode ? '1' : '0'),
|
||||
String(state.mobileView || 'spaces'),
|
||||
String(isMobileViewport() ? '1' : '0'),
|
||||
rooms.map(roomRenderToken),
|
||||
batteryRoom ? roomRenderToken(batteryRoom) : null,
|
||||
]);
|
||||
}
|
||||
|
||||
function buildContentRenderSignature(snapshot) {
|
||||
const room = snapshot?.selected_room || snapshot?.selected_space || {};
|
||||
return JSON.stringify([
|
||||
String(state.selectedRoomId || room.id || 'main'),
|
||||
String(room.id || 'main'),
|
||||
String(state.editMode ? '1' : '0'),
|
||||
String(state.mobileView || 'spaces'),
|
||||
String(isMobileViewport() ? '1' : '0'),
|
||||
selectedRoomRenderToken(snapshot),
|
||||
]);
|
||||
}
|
||||
|
||||
function buildRenderSignature(snapshot) {
|
||||
const history = state.mainBoilerHistory || {};
|
||||
return JSON.stringify([
|
||||
buildVisibleSnapshotSignature(snapshot),
|
||||
String(state.selectedRoomId || 'main'),
|
||||
String(state.editMode ? '1' : '0'),
|
||||
String(state.mobileView || 'spaces'),
|
||||
String(isMobileViewport() ? '1' : '0'),
|
||||
String(history.entityId || ''),
|
||||
String(history.loadedAt || 0),
|
||||
String(Array.isArray(history.points) ? history.points.length : 0),
|
||||
String(history.loading ? '1' : '0'),
|
||||
String(history.error || ''),
|
||||
String(state.entityPopup?.active ? '1' : '0'),
|
||||
String(state.entityPopup?.entityId || ''),
|
||||
String(state.temperatureSensorPopup?.active ? '1' : '0'),
|
||||
String(state.temperatureSensorPopup?.roomId || ''),
|
||||
String(state.lastPopupSignature || ''),
|
||||
String(state.lastEntityPopupSignature || ''),
|
||||
String(state.lastTemperatureSensorPopupSignature || ''),
|
||||
]);
|
||||
}
|
||||
|
||||
function renderSidebarSection(snapshot) {
|
||||
const nextSignature = buildSidebarRenderSignature(snapshot);
|
||||
if (nextSignature === state.lastSidebarRenderSignature) {
|
||||
return false;
|
||||
}
|
||||
state.lastSidebarRenderSignature = nextSignature;
|
||||
renderRoomButtons(snapshot, snapshot.spaces || snapshot.rooms, snapshot.battery_room);
|
||||
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';
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function renderContentSection(snapshot) {
|
||||
const nextSignature = buildContentRenderSignature(snapshot);
|
||||
if (nextSignature === state.lastContentRenderSignature) {
|
||||
return false;
|
||||
}
|
||||
state.lastContentRenderSignature = nextSignature;
|
||||
syncLayoutState();
|
||||
renderDashboard(snapshot);
|
||||
renderSelectedRoom(snapshot);
|
||||
return true;
|
||||
}
|
||||
|
||||
function requestSummary(action, params = {}) {
|
||||
const summary = { action };
|
||||
['space_id', 'room_id', 'entity_id', 'layout_item_id', 'command', 'value', 'hours', 'state', 'edit_mode'].forEach((key) => {
|
||||
@ -309,6 +578,9 @@
|
||||
}
|
||||
|
||||
function detectEmbeddedContext() {
|
||||
if (isHaRuntime()) {
|
||||
return true;
|
||||
}
|
||||
if (Boolean(bootstrap?.ui?.embed)) {
|
||||
return true;
|
||||
}
|
||||
@ -378,20 +650,160 @@
|
||||
return svg;
|
||||
}
|
||||
|
||||
function createCustomIconElement(source, fallback = 'mdi:help-circle-outline') {
|
||||
const iconTemplateCache = new Map();
|
||||
const iconTemplatePromiseCache = new Map();
|
||||
const customBrandIconsUrl = 'https://home.striker72rus.ru/local/community/custom-brand-icons/custom-brand-icons.js';
|
||||
let customBrandIconsPromise = null;
|
||||
|
||||
function templateFromSvgText(svgText) {
|
||||
const template = document.createElement('template');
|
||||
template.innerHTML = String(svgText || '').trim();
|
||||
return template;
|
||||
}
|
||||
|
||||
function iconUrl(source) {
|
||||
return `https://api.iconify.design/${source}.svg`;
|
||||
}
|
||||
|
||||
function ensureCustomBrandIconsLoaded() {
|
||||
if (window.customIcons || window.customIconsets) {
|
||||
return Promise.resolve(true);
|
||||
}
|
||||
if (customBrandIconsPromise) {
|
||||
return customBrandIconsPromise;
|
||||
}
|
||||
|
||||
const existing = document.querySelector('script[data-wall-panel-custom-brand-icons="1"]');
|
||||
if (existing) {
|
||||
customBrandIconsPromise = new Promise((resolve, reject) => {
|
||||
if (window.customIcons || window.customIconsets) {
|
||||
resolve(true);
|
||||
return;
|
||||
}
|
||||
existing.addEventListener('load', () => resolve(true), { once: true });
|
||||
existing.addEventListener('error', () => reject(new Error('custom-brand-icons load failed')), { once: true });
|
||||
}).finally(() => {
|
||||
customBrandIconsPromise = null;
|
||||
});
|
||||
return customBrandIconsPromise;
|
||||
}
|
||||
|
||||
const script = document.createElement('script');
|
||||
script.src = customBrandIconsUrl;
|
||||
script.defer = true;
|
||||
script.async = true;
|
||||
script.dataset.wallPanelCustomBrandIcons = '1';
|
||||
customBrandIconsPromise = new Promise((resolve, reject) => {
|
||||
script.addEventListener('load', () => resolve(true), { once: true });
|
||||
script.addEventListener('error', () => reject(new Error('custom-brand-icons load failed')), { once: true });
|
||||
}).finally(() => {
|
||||
customBrandIconsPromise = null;
|
||||
});
|
||||
(document.head || document.documentElement).appendChild(script);
|
||||
return customBrandIconsPromise;
|
||||
}
|
||||
|
||||
function applyTemplateToNode(target, template) {
|
||||
if (!target || !template || !target.isConnected) {
|
||||
return;
|
||||
}
|
||||
target.replaceChildren(template.content.cloneNode(true));
|
||||
}
|
||||
|
||||
function primeRemoteIconTemplate(source) {
|
||||
const key = `remote:${source}`;
|
||||
if (iconTemplateCache.has(key)) {
|
||||
return Promise.resolve(iconTemplateCache.get(key));
|
||||
}
|
||||
if (iconTemplatePromiseCache.has(key)) {
|
||||
return iconTemplatePromiseCache.get(key);
|
||||
}
|
||||
|
||||
const promise = fetch(iconUrl(source), {
|
||||
cache: 'force-cache',
|
||||
credentials: 'omit',
|
||||
headers: { Accept: 'image/svg+xml' },
|
||||
}).then((res) => {
|
||||
if (!res.ok) {
|
||||
throw new Error(`Icon load failed: ${res.status}`);
|
||||
}
|
||||
return res.text();
|
||||
}).then((svgText) => {
|
||||
const template = templateFromSvgText(svgText);
|
||||
iconTemplateCache.set(key, template);
|
||||
return template;
|
||||
}).finally(() => {
|
||||
iconTemplatePromiseCache.delete(key);
|
||||
});
|
||||
|
||||
iconTemplatePromiseCache.set(key, promise);
|
||||
return promise;
|
||||
}
|
||||
|
||||
function primeCustomIconTemplate(source) {
|
||||
const key = `custom:${source}`;
|
||||
if (iconTemplateCache.has(key)) {
|
||||
return Promise.resolve(iconTemplateCache.get(key));
|
||||
}
|
||||
if (iconTemplatePromiseCache.has(key)) {
|
||||
return iconTemplatePromiseCache.get(key);
|
||||
}
|
||||
|
||||
const [prefix, name] = source.split(':', 2);
|
||||
const customSet = window.customIcons?.[prefix] || window.customIconsets?.[prefix];
|
||||
const getIcon = typeof customSet === 'function' ? customSet : customSet?.getIcon;
|
||||
if (typeof getIcon !== 'function') {
|
||||
return null;
|
||||
return Promise.resolve(null);
|
||||
}
|
||||
|
||||
const promise = Promise.resolve(getIcon(name)).then((definition) => {
|
||||
if (!definition) {
|
||||
return null;
|
||||
}
|
||||
const template = document.createElement('template');
|
||||
template.content.appendChild(createSvgIcon(definition));
|
||||
iconTemplateCache.set(key, template);
|
||||
return template;
|
||||
}).finally(() => {
|
||||
iconTemplatePromiseCache.delete(key);
|
||||
});
|
||||
|
||||
iconTemplatePromiseCache.set(key, promise);
|
||||
return promise;
|
||||
}
|
||||
|
||||
function createCustomIconElement(source, fallback = 'mdi:help-circle-outline') {
|
||||
const cached = iconTemplateCache.get(`custom:${source}`);
|
||||
if (cached) {
|
||||
return cached.content.cloneNode(true);
|
||||
}
|
||||
|
||||
const [prefix] = source.split(':', 2);
|
||||
const customSet = window.customIcons?.[prefix] || window.customIconsets?.[prefix];
|
||||
const getIcon = typeof customSet === 'function' ? customSet : customSet?.getIcon;
|
||||
if (typeof getIcon !== 'function') {
|
||||
if (!isHaRuntime()) {
|
||||
return null;
|
||||
}
|
||||
const wrap = document.createElement('span');
|
||||
wrap.className = 'icon-node';
|
||||
wrap.appendChild(createIconElement(fallback));
|
||||
ensureCustomBrandIconsLoaded().then(() => primeCustomIconTemplate(source)).then((template) => {
|
||||
if (!template) return;
|
||||
applyTemplateToNode(wrap, template);
|
||||
}).catch(() => {
|
||||
if (!wrap.isConnected) return;
|
||||
wrap.replaceChildren(createIconElement(fallback));
|
||||
});
|
||||
return wrap;
|
||||
}
|
||||
|
||||
const wrap = document.createElement('span');
|
||||
wrap.className = 'icon-node';
|
||||
wrap.appendChild(createIconElement(fallback));
|
||||
Promise.resolve(getIcon(name)).then((definition) => {
|
||||
if (!definition || !wrap.isConnected) return;
|
||||
wrap.replaceChildren(createSvgIcon(definition));
|
||||
primeCustomIconTemplate(source).then((template) => {
|
||||
if (!template) return;
|
||||
applyTemplateToNode(wrap, template);
|
||||
}).catch(() => {
|
||||
if (!wrap.isConnected) return;
|
||||
wrap.replaceChildren(createIconElement(fallback));
|
||||
@ -403,77 +815,71 @@
|
||||
const source = normalizeIconSource(icon) || fallback;
|
||||
|
||||
if (source.startsWith('mdi:')) {
|
||||
if (isHaRuntime()) {
|
||||
const haIcon = document.createElement('ha-icon');
|
||||
haIcon.setAttribute('icon', source);
|
||||
haIcon.className = 'icon-node icon-node--ha';
|
||||
return haIcon;
|
||||
}
|
||||
const i = document.createElement('i');
|
||||
i.className = iconClass(source);
|
||||
i.setAttribute('aria-hidden', 'true');
|
||||
return i;
|
||||
}
|
||||
|
||||
if (source.startsWith('fas:') || source.startsWith('far:') || source.startsWith('fab:')) {
|
||||
const custom = createCustomIconElement(source, fallback);
|
||||
if (custom) {
|
||||
return custom;
|
||||
}
|
||||
const mappedSource = source.startsWith('fas:')
|
||||
? source.replace(/^fas:/, 'fa-solid:')
|
||||
: source.startsWith('far:')
|
||||
? source.replace(/^far:/, 'fa-regular:')
|
||||
: source.replace(/^fab:/, 'fa-brands:');
|
||||
const wrap = document.createElement('span');
|
||||
wrap.className = 'icon-node';
|
||||
wrap.appendChild(createIconElement(fallback));
|
||||
const img = document.createElement('img');
|
||||
img.className = 'icon-node__img';
|
||||
img.alt = '';
|
||||
img.decoding = 'async';
|
||||
img.loading = 'lazy';
|
||||
img.referrerPolicy = 'no-referrer';
|
||||
img.src = `https://api.iconify.design/${mappedSource}.svg`;
|
||||
img.addEventListener('load', () => {
|
||||
if (!img.isConnected || !wrap.isConnected) return;
|
||||
wrap.replaceChildren(img);
|
||||
});
|
||||
img.addEventListener('error', () => {
|
||||
if (img.dataset.fallbackApplied === '1') return;
|
||||
img.dataset.fallbackApplied = '1';
|
||||
wrap.replaceChildren(createIconElement(fallback));
|
||||
});
|
||||
wrap.appendChild(img);
|
||||
return wrap;
|
||||
}
|
||||
|
||||
const custom = createCustomIconElement(source, fallback);
|
||||
if (custom) {
|
||||
return custom;
|
||||
}
|
||||
|
||||
if (source.startsWith('fas:') || source.startsWith('far:') || source.startsWith('fab:')) {
|
||||
const mappedSource = source.startsWith('fas:')
|
||||
? source.replace(/^fas:/, 'fa-solid:')
|
||||
: source.startsWith('far:')
|
||||
? source.replace(/^far:/, 'fa-regular:')
|
||||
: source.replace(/^fab:/, 'fa-brands:');
|
||||
const cached = iconTemplateCache.get(`remote:${mappedSource}`);
|
||||
if (cached) {
|
||||
return cached.content.cloneNode(true);
|
||||
}
|
||||
const wrap = document.createElement('span');
|
||||
wrap.className = 'icon-node';
|
||||
wrap.appendChild(createIconElement(fallback));
|
||||
primeRemoteIconTemplate(mappedSource).then((template) => {
|
||||
applyTemplateToNode(wrap, template);
|
||||
}).catch(() => {
|
||||
if (!wrap.isConnected) return;
|
||||
wrap.replaceChildren(createIconElement(fallback));
|
||||
});
|
||||
return wrap;
|
||||
}
|
||||
|
||||
const remoteCached = iconTemplateCache.get(`remote:${source}`);
|
||||
if (remoteCached) {
|
||||
return remoteCached.content.cloneNode(true);
|
||||
}
|
||||
|
||||
const wrap = document.createElement('span');
|
||||
wrap.className = 'icon-node';
|
||||
wrap.appendChild(createIconElement(fallback));
|
||||
|
||||
if (source.includes(':')) {
|
||||
const img = document.createElement('img');
|
||||
img.className = 'icon-node__img';
|
||||
img.alt = '';
|
||||
img.decoding = 'async';
|
||||
img.loading = 'lazy';
|
||||
img.referrerPolicy = 'no-referrer';
|
||||
img.src = `https://api.iconify.design/${source}.svg`;
|
||||
img.addEventListener('load', () => {
|
||||
if (!img.isConnected || !wrap.isConnected) return;
|
||||
wrap.replaceChildren(img);
|
||||
});
|
||||
img.addEventListener('error', () => {
|
||||
if (img.dataset.fallbackApplied === '1') return;
|
||||
img.dataset.fallbackApplied = '1';
|
||||
primeRemoteIconTemplate(source).then((template) => {
|
||||
applyTemplateToNode(wrap, template);
|
||||
}).catch(() => {
|
||||
if (!wrap.isConnected) return;
|
||||
wrap.replaceChildren(createIconElement(fallback));
|
||||
});
|
||||
wrap.appendChild(img);
|
||||
return wrap;
|
||||
}
|
||||
|
||||
return createIconElement(fallback);
|
||||
}
|
||||
|
||||
if (isHaRuntime()) {
|
||||
void ensureCustomBrandIconsLoaded();
|
||||
}
|
||||
|
||||
function esc(value) {
|
||||
return String(value ?? '');
|
||||
}
|
||||
@ -550,6 +956,9 @@
|
||||
}
|
||||
|
||||
function popupTriggerEntities() {
|
||||
if (isHaRuntime()) {
|
||||
return new Set();
|
||||
}
|
||||
const fromSnapshot = state.snapshot?.settings?.camera?.trigger_entities;
|
||||
const fromBootstrap = bootstrap?.settings?.camera?.trigger_entities;
|
||||
const triggers = Array.isArray(fromSnapshot) ? fromSnapshot : Array.isArray(fromBootstrap) ? fromBootstrap : [];
|
||||
@ -669,10 +1078,18 @@
|
||||
async function apiGet(action, params = {}) {
|
||||
const bridge = haBridge();
|
||||
if (bridge?.request) {
|
||||
console.log('[Striker Panel]', 'apiGet via bridge', requestSummary(action, params));
|
||||
debugLog('apiGet via bridge', requestSummary(action, params));
|
||||
return bridge.request('GET', action, params);
|
||||
}
|
||||
console.log('[Striker Panel]', 'apiGet via http', requestSummary(action, params));
|
||||
if (isHaRuntime()) {
|
||||
const waited = await waitForHaBridge(1000);
|
||||
if (waited?.request) {
|
||||
debugLog('apiGet via delayed bridge', requestSummary(action, params));
|
||||
return waited.request('GET', action, params);
|
||||
}
|
||||
throw new Error('HA bridge is not ready');
|
||||
}
|
||||
debugLog('apiGet via http', requestSummary(action, params));
|
||||
const res = await fetch(buildUrl(action, params), {
|
||||
headers: { Accept: 'application/json' },
|
||||
cache: 'no-store',
|
||||
@ -686,10 +1103,18 @@
|
||||
async function apiPost(action, payload = {}) {
|
||||
const bridge = haBridge();
|
||||
if (bridge?.request) {
|
||||
console.log('[Striker Panel]', 'apiPost via bridge', requestSummary(action, payload));
|
||||
debugLog('apiPost via bridge', requestSummary(action, payload));
|
||||
return bridge.request('POST', action, payload);
|
||||
}
|
||||
console.log('[Striker Panel]', 'apiPost via http', requestSummary(action, payload));
|
||||
if (isHaRuntime()) {
|
||||
const waited = await waitForHaBridge(1000);
|
||||
if (waited?.request) {
|
||||
debugLog('apiPost via delayed bridge', requestSummary(action, payload));
|
||||
return waited.request('POST', action, payload);
|
||||
}
|
||||
throw new Error('HA bridge is not ready');
|
||||
}
|
||||
debugLog('apiPost via http', requestSummary(action, payload));
|
||||
const res = await fetch(buildUrl(action), {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
|
||||
@ -1183,12 +1608,16 @@
|
||||
if (!els.appShell) return;
|
||||
|
||||
const mobile = isMobileViewport();
|
||||
const embedded = Boolean(state.embedMode);
|
||||
const haNative = isHaRuntime();
|
||||
const embedded = Boolean(state.embedMode || haNative);
|
||||
state.embedMode = embedded;
|
||||
document.body.classList.toggle('is-mobile-ui', mobile);
|
||||
document.body.classList.toggle('is-embedded', embedded);
|
||||
document.body.classList.toggle('is-ha-native', haNative);
|
||||
els.appShell.classList.toggle('is-mobile', mobile);
|
||||
els.appShell.classList.toggle('is-desktop', !mobile);
|
||||
els.appShell.classList.toggle('app-shell--embed', embedded);
|
||||
els.appShell.classList.toggle('is-ha-native', haNative);
|
||||
els.appShell.classList.toggle('mobile-view-spaces', mobile && state.mobileView !== 'room');
|
||||
els.appShell.classList.toggle('mobile-view-room', mobile && state.mobileView === 'room');
|
||||
|
||||
@ -1865,6 +2294,9 @@
|
||||
}
|
||||
|
||||
function applyPopupState(active, sensorEntityId) {
|
||||
if (isHaRuntime()) {
|
||||
return;
|
||||
}
|
||||
const camera = state.snapshot?.settings?.camera || bootstrap?.settings?.camera || {};
|
||||
const popup = state.snapshot?.popup || {};
|
||||
if (active && Date.now() < Number(state.popupAutoOpenBlockedUntil || 0)) {
|
||||
@ -1888,6 +2320,9 @@
|
||||
}
|
||||
|
||||
function applyPopupSnapshot(popup = {}) {
|
||||
if (isHaRuntime()) {
|
||||
return;
|
||||
}
|
||||
const snapshot = state.snapshot || bootstrap;
|
||||
snapshot.popup = mergePopupWithCamera({
|
||||
...(snapshot.popup || {}),
|
||||
@ -1897,6 +2332,9 @@
|
||||
}
|
||||
|
||||
function syncTriggerPopup(entityId, stateValue) {
|
||||
if (isHaRuntime()) {
|
||||
return;
|
||||
}
|
||||
const value = String(stateValue || '').toLowerCase();
|
||||
if (!['on', 'off'].includes(value)) {
|
||||
return;
|
||||
@ -2870,13 +3308,13 @@
|
||||
const minus = document.createElement('button');
|
||||
minus.type = 'button';
|
||||
minus.className = 'round-button entity-modal__round-button';
|
||||
minus.innerHTML = '<i class="mdi mdi-minus"></i>';
|
||||
minus.appendChild(createIconElement('mdi:minus'));
|
||||
minus.addEventListener('click', () => handleClimateTemperature(entity, -1));
|
||||
|
||||
const plus = document.createElement('button');
|
||||
plus.type = 'button';
|
||||
plus.className = 'round-button entity-modal__round-button';
|
||||
plus.innerHTML = '<i class="mdi mdi-plus"></i>';
|
||||
plus.appendChild(createIconElement('mdi:plus'));
|
||||
plus.addEventListener('click', () => handleClimateTemperature(entity, 1));
|
||||
|
||||
controls.append(minus, plus);
|
||||
@ -3349,13 +3787,15 @@
|
||||
const upBtn = document.createElement('button');
|
||||
upBtn.type = 'button';
|
||||
upBtn.className = 'mushroom-button mushroom-button--small';
|
||||
upBtn.innerHTML = '<i class="mdi mdi-arrow-up"></i> Вверх';
|
||||
upBtn.appendChild(createIconElement('mdi:arrow-up'));
|
||||
upBtn.appendChild(document.createTextNode(' Вверх'));
|
||||
upBtn.addEventListener('click', () => reorderRoomGridEntry(currentRoom()?.id, 'entity', entity.entity_id, -1));
|
||||
|
||||
const downBtn = document.createElement('button');
|
||||
downBtn.type = 'button';
|
||||
downBtn.className = 'mushroom-button mushroom-button--small';
|
||||
downBtn.innerHTML = '<i class="mdi mdi-arrow-down"></i> Вниз';
|
||||
downBtn.appendChild(createIconElement('mdi:arrow-down'));
|
||||
downBtn.appendChild(document.createTextNode(' Вниз'));
|
||||
downBtn.addEventListener('click', () => reorderRoomGridEntry(currentRoom()?.id, 'entity', entity.entity_id, 1));
|
||||
|
||||
actions.append(upBtn, downBtn);
|
||||
@ -3371,7 +3811,7 @@
|
||||
const btn = document.createElement('button');
|
||||
btn.type = 'button';
|
||||
btn.className = 'mini-action mini-action--wide';
|
||||
btn.innerHTML = hidden ? '<i class="mdi mdi-eye"></i>' : '<i class="mdi mdi-eye-off"></i>';
|
||||
btn.appendChild(createIconElement(hidden ? 'mdi:eye' : 'mdi:eye-off'));
|
||||
btn.title = hidden ? 'Показать' : 'Скрыть';
|
||||
btn.addEventListener('click', (event) => {
|
||||
event.stopPropagation();
|
||||
@ -3484,7 +3924,8 @@
|
||||
const settingsBtn = document.createElement('button');
|
||||
settingsBtn.type = 'button';
|
||||
settingsBtn.className = 'mushroom-button mushroom-button--small mushroom-button--wide';
|
||||
settingsBtn.innerHTML = '<i class="mdi mdi-cog-outline"></i> Настройки';
|
||||
settingsBtn.appendChild(createIconElement('mdi:cog-outline'));
|
||||
settingsBtn.appendChild(document.createTextNode(' Настройки'));
|
||||
settingsBtn.addEventListener('click', (event) => {
|
||||
event.stopPropagation();
|
||||
state.layoutItemSettingsOpen = {
|
||||
@ -3497,7 +3938,8 @@
|
||||
const upBtn = document.createElement('button');
|
||||
upBtn.type = 'button';
|
||||
upBtn.className = 'mushroom-button mushroom-button--small';
|
||||
upBtn.innerHTML = '<i class="mdi mdi-arrow-up"></i> Вверх';
|
||||
upBtn.appendChild(createIconElement('mdi:arrow-up'));
|
||||
upBtn.appendChild(document.createTextNode(' Вверх'));
|
||||
upBtn.addEventListener('click', (event) => {
|
||||
event.stopPropagation();
|
||||
reorderRoomGridEntry(room.id, 'layout', item.id, -1);
|
||||
@ -3506,7 +3948,8 @@
|
||||
const downBtn = document.createElement('button');
|
||||
downBtn.type = 'button';
|
||||
downBtn.className = 'mushroom-button mushroom-button--small';
|
||||
downBtn.innerHTML = '<i class="mdi mdi-arrow-down"></i> Вниз';
|
||||
downBtn.appendChild(createIconElement('mdi:arrow-down'));
|
||||
downBtn.appendChild(document.createTextNode(' Вниз'));
|
||||
downBtn.addEventListener('click', (event) => {
|
||||
event.stopPropagation();
|
||||
reorderRoomGridEntry(room.id, 'layout', item.id, 1);
|
||||
@ -3515,7 +3958,8 @@
|
||||
const deleteBtn = document.createElement('button');
|
||||
deleteBtn.type = 'button';
|
||||
deleteBtn.className = 'mushroom-button mushroom-button--small mushroom-button--wide';
|
||||
deleteBtn.innerHTML = '<i class="mdi mdi-delete-outline"></i> Удалить';
|
||||
deleteBtn.appendChild(createIconElement('mdi:delete-outline'));
|
||||
deleteBtn.appendChild(document.createTextNode(' Удалить'));
|
||||
deleteBtn.addEventListener('click', (event) => {
|
||||
event.stopPropagation();
|
||||
deleteRoomLayoutItem(room.id, item.id);
|
||||
@ -3531,7 +3975,8 @@
|
||||
const tempBtn = document.createElement('button');
|
||||
tempBtn.type = 'button';
|
||||
tempBtn.className = 'mushroom-button mushroom-button--small mushroom-button--wide';
|
||||
tempBtn.innerHTML = '<i class="mdi mdi-thermometer"></i> Выбрать датчик температуры';
|
||||
tempBtn.appendChild(createIconElement('mdi:thermometer'));
|
||||
tempBtn.appendChild(document.createTextNode(' Выбрать датчик температуры'));
|
||||
tempBtn.addEventListener('click', (event) => {
|
||||
event.stopPropagation();
|
||||
openTemperatureSensorPopup(room.id);
|
||||
@ -3654,6 +4099,12 @@
|
||||
}
|
||||
els.roomList.innerHTML = '';
|
||||
const sortedRooms = [...(rooms || [])].sort((left, right) => {
|
||||
const leftVisible = left?.visible === false ? 1 : 0;
|
||||
const rightVisible = right?.visible === false ? 1 : 0;
|
||||
if (leftVisible !== rightVisible) {
|
||||
return leftVisible - rightVisible;
|
||||
}
|
||||
|
||||
if (left.id === 'main') return -1;
|
||||
if (right.id === 'main') return 1;
|
||||
|
||||
@ -3841,7 +4292,8 @@
|
||||
const addButton = document.createElement('button');
|
||||
addButton.type = 'button';
|
||||
addButton.className = 'mushroom-button mushroom-button--small content-header__ghost-button';
|
||||
addButton.innerHTML = '<i class="mdi mdi-plus"></i><span>Пустая карточка</span>';
|
||||
addButton.appendChild(createIconElement('mdi:plus'));
|
||||
addButton.appendChild(document.createElement('span')).textContent = 'Пустая карточка';
|
||||
addButton.addEventListener('click', () => {
|
||||
createRoomLayoutItem(room.id);
|
||||
});
|
||||
@ -3849,7 +4301,8 @@
|
||||
const temperatureButton = document.createElement('button');
|
||||
temperatureButton.type = 'button';
|
||||
temperatureButton.className = 'mushroom-button mushroom-button--small content-header__ghost-button';
|
||||
temperatureButton.innerHTML = '<i class="mdi mdi-thermometer"></i><span>Выбрать датчик температуры</span>';
|
||||
temperatureButton.appendChild(createIconElement('mdi:thermometer'));
|
||||
temperatureButton.appendChild(document.createElement('span')).textContent = 'Выбрать датчик температуры';
|
||||
temperatureButton.addEventListener('click', () => {
|
||||
openTemperatureSensorPopup(room.id);
|
||||
});
|
||||
@ -3991,6 +4444,10 @@
|
||||
}
|
||||
|
||||
function renderPopup(snapshot) {
|
||||
if (isHaRuntime()) {
|
||||
hidePopup({ preserveSnapshot: true });
|
||||
return;
|
||||
}
|
||||
if (isMobileViewport()) {
|
||||
hidePopup({ preserveSnapshot: true });
|
||||
return;
|
||||
@ -4123,6 +4580,9 @@
|
||||
}
|
||||
|
||||
async function showDebugPopup() {
|
||||
if (isHaRuntime()) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const response = await apiPost('popup', { command: 'open' });
|
||||
const snapshot = state.snapshot || bootstrap;
|
||||
@ -4266,53 +4726,25 @@
|
||||
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'),
|
||||
]);
|
||||
const renderSignature = buildRenderSignature(snapshot);
|
||||
if (renderSignature === state.lastRenderSignature) {
|
||||
return;
|
||||
}
|
||||
state.lastRenderSignature = renderSignature;
|
||||
if (renderSignature !== state.debugLastRenderSignature) {
|
||||
state.debugLastRenderSignature = renderSignature;
|
||||
debugLog('render()', snapshotSummary(snapshot));
|
||||
}
|
||||
|
||||
syncLayoutState();
|
||||
renderDashboard(snapshot);
|
||||
renderSelectedRoom(snapshot);
|
||||
renderRoomButtons(snapshot, snapshot.spaces || snapshot.rooms, snapshot.battery_room);
|
||||
renderSidebarSection(snapshot);
|
||||
renderContentSection(snapshot);
|
||||
renderPopup(snapshot);
|
||||
renderEntityPopup(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 renderDashboardOnly() {
|
||||
const snapshot = state.snapshot || bootstrap;
|
||||
if (!snapshot || !(snapshot.spaces || snapshot.rooms)) return;
|
||||
syncLayoutState();
|
||||
renderSelectedRoom(snapshot);
|
||||
renderDashboard(snapshot);
|
||||
renderPopup(snapshot);
|
||||
renderEntityPopup(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';
|
||||
}
|
||||
renderContentSection(state.snapshot || bootstrap);
|
||||
}
|
||||
|
||||
function refreshCurrentRoomLayout(entityId) {
|
||||
@ -4357,24 +4789,11 @@
|
||||
}
|
||||
|
||||
function renderSidebarOnly() {
|
||||
const snapshot = state.snapshot || bootstrap;
|
||||
if (!snapshot || !(snapshot.spaces || snapshot.rooms)) return;
|
||||
renderRoomButtons(snapshot, snapshot.spaces || snapshot.rooms, snapshot.battery_room);
|
||||
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';
|
||||
}
|
||||
renderSidebarSection(state.snapshot || bootstrap);
|
||||
}
|
||||
|
||||
function renderSelectionOnly() {
|
||||
const snapshot = state.snapshot || bootstrap;
|
||||
if (!snapshot || !(snapshot.spaces || snapshot.rooms)) return;
|
||||
syncLayoutState();
|
||||
renderSelectedRoom(snapshot);
|
||||
renderContentSection(state.snapshot || bootstrap);
|
||||
}
|
||||
|
||||
async function handleEntityAction(entity, command) {
|
||||
@ -4586,32 +5005,34 @@
|
||||
renderSelectionOnly();
|
||||
});
|
||||
|
||||
bind(els.cameraBackdrop, 'click', (event) => {
|
||||
if (event.target === els.cameraBackdrop) {
|
||||
apiPost('popup', { command: 'close' }).catch(() => {});
|
||||
hidePopup({ suppressAutoOpen: true });
|
||||
}
|
||||
});
|
||||
if (!isHaRuntime()) {
|
||||
bind(els.cameraBackdrop, 'click', (event) => {
|
||||
if (event.target === els.cameraBackdrop) {
|
||||
apiPost('popup', { command: 'close' }).catch(() => {});
|
||||
hidePopup({ suppressAutoOpen: true });
|
||||
}
|
||||
});
|
||||
|
||||
bind(els.cameraModalPanel, 'click', (event) => {
|
||||
event.stopPropagation();
|
||||
});
|
||||
|
||||
const closeCameraPopup = async (event) => {
|
||||
if (event) {
|
||||
event.preventDefault();
|
||||
bind(els.cameraModalPanel, 'click', (event) => {
|
||||
event.stopPropagation();
|
||||
}
|
||||
try {
|
||||
await apiPost('popup', { command: 'close' });
|
||||
} catch (error) {
|
||||
console.warn(error);
|
||||
}
|
||||
hidePopup({ suppressAutoOpen: true });
|
||||
};
|
||||
});
|
||||
|
||||
bind(els.cameraClose, 'pointerdown', closeCameraPopup);
|
||||
bind(els.cameraClose, 'click', closeCameraPopup);
|
||||
const closeCameraPopup = async (event) => {
|
||||
if (event) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
}
|
||||
try {
|
||||
await apiPost('popup', { command: 'close' });
|
||||
} catch (error) {
|
||||
console.warn(error);
|
||||
}
|
||||
hidePopup({ suppressAutoOpen: true });
|
||||
};
|
||||
|
||||
bind(els.cameraClose, 'pointerdown', closeCameraPopup);
|
||||
bind(els.cameraClose, 'click', closeCameraPopup);
|
||||
}
|
||||
|
||||
els.entityBackdrop?.addEventListener('click', (event) => {
|
||||
if (event.target === els.entityBackdrop) {
|
||||
@ -4975,7 +5396,7 @@
|
||||
mode: bootstrap?.ui?.mode || 'unknown',
|
||||
});
|
||||
initRefs();
|
||||
state.embedMode = detectEmbeddedContext();
|
||||
state.embedMode = detectEmbeddedContext() || isHaRuntime();
|
||||
syncLayoutState();
|
||||
syncViewportState();
|
||||
bindPressFeedback();
|
||||
|
||||
@ -6,6 +6,8 @@ import logging
|
||||
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from .const import DEFAULT_DASHBOARD_URL_PATH
|
||||
from .const import DEFAULT_FRONTEND_URL_PATH
|
||||
from .const import DOMAIN
|
||||
from .frontend import async_setup_frontend
|
||||
from .helpers import current_entry_config
|
||||
@ -53,8 +55,16 @@ async def async_unload_entry(hass: HomeAssistant, entry) -> bool:
|
||||
from homeassistant.components.frontend import async_remove_panel
|
||||
|
||||
state = hass.data.get(DOMAIN, {})
|
||||
panel_url_path = str(state.get(entry.entry_id, {}).get("panel_url_path") or entry.options.get("frontend_url_path", "wall-panel") or "wall-panel").strip()
|
||||
panel_url_path = str(state.get(entry.entry_id, {}).get("panel_url_path") or DEFAULT_FRONTEND_URL_PATH).strip()
|
||||
dashboard_url_path = str(state.get(entry.entry_id, {}).get("dashboard_url_path") or DEFAULT_DASHBOARD_URL_PATH).strip()
|
||||
async_remove_panel(hass, panel_url_path)
|
||||
async_remove_panel(hass, dashboard_url_path)
|
||||
lovelace = hass.data.get("lovelace")
|
||||
dashboards = getattr(lovelace, "dashboards", None)
|
||||
if dashboards is None and isinstance(lovelace, dict):
|
||||
dashboards = lovelace.get("dashboards")
|
||||
if dashboards is not None:
|
||||
dashboards.pop(dashboard_url_path, None)
|
||||
state.pop(entry.entry_id, None)
|
||||
return True
|
||||
|
||||
|
||||
@ -49,6 +49,11 @@ body.is-embedded {
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
body.is-ha-native #camera-modal,
|
||||
body.is-ha-native .camera-modal {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
button,
|
||||
input,
|
||||
select,
|
||||
@ -261,7 +266,7 @@ textarea {
|
||||
|
||||
.room-list__group {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
@ -293,7 +298,8 @@ textarea {
|
||||
height: 42px;
|
||||
}
|
||||
|
||||
.app-shell--embed .room-item__icon i {
|
||||
.app-shell--embed .room-item__icon i,
|
||||
.app-shell--embed .room-item__icon ha-icon {
|
||||
font-size: 32px;
|
||||
}
|
||||
|
||||
@ -375,6 +381,17 @@ textarea {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.room-item__icon ha-icon,
|
||||
.grid-card__icon ha-icon,
|
||||
.mushroom-button__icon ha-icon,
|
||||
.icon-node--ha {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.icon-node__img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
@ -509,6 +526,10 @@ textarea {
|
||||
padding: 22px 16px 20px;
|
||||
}
|
||||
|
||||
.app-shell.is-ha-native .content-top {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.content-top {
|
||||
display: none;
|
||||
margin-bottom: 16px;
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -13,12 +13,10 @@ from homeassistant.helpers.selector import TextSelector, TextSelectorConfig, Tex
|
||||
|
||||
from .const import (
|
||||
CONF_CONFIG,
|
||||
CONF_FRONTEND_URL_PATH,
|
||||
CONF_REQUIRE_ADMIN,
|
||||
CONF_SIDEBAR_ICON,
|
||||
CONF_SIDEBAR_TITLE,
|
||||
CONF_SYNC_TOKEN,
|
||||
DEFAULT_FRONTEND_URL_PATH,
|
||||
DEFAULT_SIDEBAR_ICON,
|
||||
DEFAULT_SIDEBAR_TITLE,
|
||||
DOMAIN,
|
||||
@ -31,7 +29,6 @@ def _schema(defaults: dict[str, Any]) -> vol.Schema:
|
||||
vol.Optional(CONF_NAME, default=defaults.get(CONF_NAME, "Striker Panel")): 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_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_CONFIG,
|
||||
@ -60,7 +57,6 @@ class WallPanelConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
CONF_NAME: user_input.get(CONF_NAME, "Striker Panel"),
|
||||
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_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_SYNC_TOKEN: secrets.token_urlsafe(24),
|
||||
CONF_CONFIG: config,
|
||||
@ -73,7 +69,6 @@ class WallPanelConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
CONF_NAME: "Striker Panel",
|
||||
CONF_SIDEBAR_TITLE: DEFAULT_SIDEBAR_TITLE,
|
||||
CONF_SIDEBAR_ICON: DEFAULT_SIDEBAR_ICON,
|
||||
CONF_FRONTEND_URL_PATH: DEFAULT_FRONTEND_URL_PATH,
|
||||
CONF_REQUIRE_ADMIN: False,
|
||||
CONF_CONFIG: config_to_json(normalize_config({})),
|
||||
}
|
||||
@ -102,7 +97,6 @@ class WallPanelOptionsFlow(config_entries.OptionsFlow):
|
||||
CONF_NAME: user_input.get(CONF_NAME, data.get(CONF_NAME, "Striker Panel")),
|
||||
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_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_SYNC_TOKEN: data.get(CONF_SYNC_TOKEN, secrets.token_urlsafe(24)),
|
||||
CONF_CONFIG: config,
|
||||
@ -113,7 +107,6 @@ class WallPanelOptionsFlow(config_entries.OptionsFlow):
|
||||
CONF_NAME: self.config_entry.options.get(CONF_NAME, "Striker Panel"),
|
||||
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_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_CONFIG: config_to_json(current_entry_config(self.config_entry)),
|
||||
}
|
||||
|
||||
@ -15,3 +15,4 @@ DEFAULT_PANEL_URL = ""
|
||||
DEFAULT_SIDEBAR_TITLE = "Striker Panel"
|
||||
DEFAULT_SIDEBAR_ICON = "mdi:view-dashboard"
|
||||
DEFAULT_FRONTEND_URL_PATH = "striker-panel"
|
||||
DEFAULT_DASHBOARD_URL_PATH = "wall-panel"
|
||||
|
||||
@ -5,13 +5,14 @@ from __future__ import annotations
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
from homeassistant.components.lovelace import _register_panel
|
||||
from homeassistant.components.lovelace.dashboard import LovelaceYAML
|
||||
from homeassistant.components.frontend import async_register_built_in_panel
|
||||
from homeassistant.components.http import StaticPathConfig
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from .const import (
|
||||
CONF_FRONTEND_URL_PATH,
|
||||
CONF_PANEL_URL,
|
||||
DEFAULT_DASHBOARD_URL_PATH,
|
||||
CONF_REQUIRE_ADMIN,
|
||||
CONF_SIDEBAR_ICON,
|
||||
CONF_SIDEBAR_TITLE,
|
||||
@ -25,10 +26,38 @@ 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
|
||||
# Keep the HA panel identity stable so Home Assistant can always
|
||||
# discover this panel in the default-panel picker.
|
||||
return DEFAULT_FRONTEND_URL_PATH
|
||||
|
||||
|
||||
def _dashboard_url_path(entry) -> str:
|
||||
# Keep the Lovelace dashboard identity stable and separate from the
|
||||
# built-in HA panel path.
|
||||
return DEFAULT_DASHBOARD_URL_PATH
|
||||
|
||||
|
||||
def _dashboard_config(sidebar_title: str, sidebar_icon: str, require_admin: bool) -> dict[str, object]:
|
||||
return {
|
||||
"mode": "yaml",
|
||||
"title": sidebar_title,
|
||||
"icon": sidebar_icon,
|
||||
"show_in_sidebar": False,
|
||||
"require_admin": require_admin,
|
||||
"filename": "custom_components/wall_panel/lovelace/ui-lovelace.yaml",
|
||||
}
|
||||
|
||||
|
||||
def _register_lovelace_dashboard(hass: HomeAssistant, dashboard_url_path: str, dashboard_config: dict[str, object]) -> None:
|
||||
lovelace = hass.data.setdefault("lovelace", {})
|
||||
dashboards = getattr(lovelace, "dashboards", None)
|
||||
if dashboards is None and isinstance(lovelace, dict):
|
||||
dashboards = lovelace.setdefault("dashboards", {})
|
||||
if dashboards is None:
|
||||
return
|
||||
|
||||
dashboards[dashboard_url_path] = LovelaceYAML(hass, dashboard_url_path, dashboard_config)
|
||||
_register_panel(hass, dashboard_url_path, "yaml", dashboard_config, False)
|
||||
|
||||
|
||||
async def async_setup_frontend(hass: HomeAssistant, entry) -> str:
|
||||
@ -55,12 +84,16 @@ async def async_setup_frontend(hass: HomeAssistant, entry) -> str:
|
||||
state["_static_paths_registered"] = True
|
||||
|
||||
panel_url_path = _panel_url_path(entry)
|
||||
dashboard_url_path = _dashboard_url_path(entry)
|
||||
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()
|
||||
require_admin = bool(entry.options.get(CONF_REQUIRE_ADMIN, False))
|
||||
sync_token = str(entry.options.get(CONF_SYNC_TOKEN, "") or "").strip()
|
||||
asset_version = str(int(time.time()))
|
||||
runtime_config = current_entry_config(entry)
|
||||
dashboard_config = _dashboard_config(sidebar_title, sidebar_icon, require_admin)
|
||||
|
||||
_register_lovelace_dashboard(hass, dashboard_url_path, dashboard_config)
|
||||
|
||||
async_register_built_in_panel(
|
||||
hass,
|
||||
@ -85,4 +118,11 @@ async def async_setup_frontend(hass: HomeAssistant, entry) -> str:
|
||||
update=True,
|
||||
)
|
||||
|
||||
state = hass.data.setdefault(DOMAIN, {})
|
||||
state[entry.entry_id] = {
|
||||
**state.get(entry.entry_id, {}),
|
||||
"dashboard_url_path": dashboard_url_path,
|
||||
"panel_url_path": panel_url_path,
|
||||
}
|
||||
|
||||
return panel_url_path
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -169,7 +169,8 @@ def current_entry_panel(entry) -> dict[str, Any]:
|
||||
CONF_SYNC_TOKEN: str(entry.options.get(CONF_SYNC_TOKEN, "") or ""),
|
||||
CONF_SIDEBAR_TITLE: str(entry.options.get(CONF_SIDEBAR_TITLE, DEFAULT_SIDEBAR_TITLE) or DEFAULT_SIDEBAR_TITLE),
|
||||
CONF_SIDEBAR_ICON: str(entry.options.get(CONF_SIDEBAR_ICON, DEFAULT_SIDEBAR_ICON) or DEFAULT_SIDEBAR_ICON),
|
||||
CONF_FRONTEND_URL_PATH: str(entry.options.get(CONF_FRONTEND_URL_PATH, DEFAULT_FRONTEND_URL_PATH) or DEFAULT_FRONTEND_URL_PATH),
|
||||
# The HA panel path must remain canonical and independent from the PHP proxy URL.
|
||||
CONF_FRONTEND_URL_PATH: DEFAULT_FRONTEND_URL_PATH,
|
||||
CONF_REQUIRE_ADMIN: bool(entry.options.get(CONF_REQUIRE_ADMIN, False)),
|
||||
}
|
||||
|
||||
|
||||
9
custom_components/wall_panel/lovelace/ui-lovelace.yaml
Executable file
9
custom_components/wall_panel/lovelace/ui-lovelace.yaml
Executable file
@ -0,0 +1,9 @@
|
||||
title: Home Panel
|
||||
views:
|
||||
- title: Home Panel
|
||||
path: home-panel
|
||||
panel: true
|
||||
cards:
|
||||
- type: iframe
|
||||
url: /striker-panel
|
||||
aspect_ratio: 100%
|
||||
@ -1,11 +1,11 @@
|
||||
{
|
||||
"items": {
|
||||
"sensor.garage_motion_battery": {
|
||||
"loaded_at": 1774456464,
|
||||
"loaded_at": 1774470062,
|
||||
"history_hours": 4320,
|
||||
"points": [
|
||||
{
|
||||
"timestamp": 1773851664,
|
||||
"timestamp": 1773865262,
|
||||
"value": 100
|
||||
},
|
||||
{
|
||||
@ -20,11 +20,11 @@
|
||||
"percent": 100
|
||||
},
|
||||
"sensor.garage_light_battery": {
|
||||
"loaded_at": 1774456464,
|
||||
"loaded_at": 1774470062,
|
||||
"history_hours": 4320,
|
||||
"points": [
|
||||
{
|
||||
"timestamp": 1773851664,
|
||||
"timestamp": 1773865262,
|
||||
"value": 100
|
||||
},
|
||||
{
|
||||
@ -39,11 +39,11 @@
|
||||
"percent": 100
|
||||
},
|
||||
"sensor.garage_door_motion_battery": {
|
||||
"loaded_at": 1774456464,
|
||||
"loaded_at": 1774470062,
|
||||
"history_hours": 4320,
|
||||
"points": [
|
||||
{
|
||||
"timestamp": 1773851664,
|
||||
"timestamp": 1773865262,
|
||||
"value": 100
|
||||
},
|
||||
{
|
||||
@ -58,11 +58,11 @@
|
||||
"percent": 100
|
||||
},
|
||||
"sensor.stair_up_motion_battery": {
|
||||
"loaded_at": 1774456464,
|
||||
"loaded_at": 1774470062,
|
||||
"history_hours": 4320,
|
||||
"points": [
|
||||
{
|
||||
"timestamp": 1773851664,
|
||||
"timestamp": 1773865262,
|
||||
"value": 100
|
||||
},
|
||||
{
|
||||
@ -77,11 +77,11 @@
|
||||
"percent": 100
|
||||
},
|
||||
"sensor.stair_down_motion_battery": {
|
||||
"loaded_at": 1774456464,
|
||||
"loaded_at": 1774470062,
|
||||
"history_hours": 4320,
|
||||
"points": [
|
||||
{
|
||||
"timestamp": 1773851664,
|
||||
"timestamp": 1773865262,
|
||||
"value": 100
|
||||
},
|
||||
{
|
||||
@ -96,11 +96,11 @@
|
||||
"percent": 100
|
||||
},
|
||||
"sensor.stair_light_battery": {
|
||||
"loaded_at": 1774456464,
|
||||
"loaded_at": 1774470062,
|
||||
"history_hours": 4320,
|
||||
"points": [
|
||||
{
|
||||
"timestamp": 1773851664,
|
||||
"timestamp": 1773865262,
|
||||
"value": 100
|
||||
},
|
||||
{
|
||||
@ -115,11 +115,11 @@
|
||||
"percent": 100
|
||||
},
|
||||
"sensor.door_sensor_2_battery": {
|
||||
"loaded_at": 1774456464,
|
||||
"loaded_at": 1774470062,
|
||||
"history_hours": 4320,
|
||||
"points": [
|
||||
{
|
||||
"timestamp": 1773851664,
|
||||
"timestamp": 1773865262,
|
||||
"value": 90
|
||||
},
|
||||
{
|
||||
@ -134,11 +134,11 @@
|
||||
"percent": 90
|
||||
},
|
||||
"sensor.wleak_battery": {
|
||||
"loaded_at": 1774456464,
|
||||
"loaded_at": 1774470062,
|
||||
"history_hours": 4320,
|
||||
"points": [
|
||||
{
|
||||
"timestamp": 1773851664,
|
||||
"timestamp": 1773865262,
|
||||
"value": 100
|
||||
},
|
||||
{
|
||||
@ -153,11 +153,11 @@
|
||||
"percent": 100
|
||||
},
|
||||
"sensor.0xa4c138433d675809_battery": {
|
||||
"loaded_at": 1774456464,
|
||||
"loaded_at": 1774470062,
|
||||
"history_hours": 4320,
|
||||
"points": [
|
||||
{
|
||||
"timestamp": 1773851664,
|
||||
"timestamp": 1773865262,
|
||||
"value": 1
|
||||
},
|
||||
{
|
||||
@ -172,11 +172,11 @@
|
||||
"percent": 1
|
||||
},
|
||||
"sensor.0xa4c138997cb4fdd1_battery": {
|
||||
"loaded_at": 1774456464,
|
||||
"loaded_at": 1774470062,
|
||||
"history_hours": 4320,
|
||||
"points": [
|
||||
{
|
||||
"timestamp": 1773851664,
|
||||
"timestamp": 1773865262,
|
||||
"value": 100
|
||||
},
|
||||
{
|
||||
@ -191,11 +191,11 @@
|
||||
"percent": 100
|
||||
},
|
||||
"sensor.printer_knopka_battery": {
|
||||
"loaded_at": 1774456464,
|
||||
"loaded_at": 1774470062,
|
||||
"history_hours": 4320,
|
||||
"points": [
|
||||
{
|
||||
"timestamp": 1773851664,
|
||||
"timestamp": 1773865262,
|
||||
"value": 74
|
||||
},
|
||||
{
|
||||
@ -210,11 +210,11 @@
|
||||
"percent": 74
|
||||
},
|
||||
"sensor.lestnitsa_dvizhenie_2_etazh_battery": {
|
||||
"loaded_at": 1774456464,
|
||||
"loaded_at": 1774470062,
|
||||
"history_hours": 4320,
|
||||
"points": [
|
||||
{
|
||||
"timestamp": 1773851664,
|
||||
"timestamp": 1773865262,
|
||||
"value": 100
|
||||
},
|
||||
{
|
||||
@ -229,11 +229,11 @@
|
||||
"percent": 100
|
||||
},
|
||||
"sensor.spalnia_knopka_girliand_battery": {
|
||||
"loaded_at": 1774456464,
|
||||
"loaded_at": 1774470062,
|
||||
"history_hours": 4320,
|
||||
"points": [
|
||||
{
|
||||
"timestamp": 1773851664,
|
||||
"timestamp": 1773865262,
|
||||
"value": 29
|
||||
},
|
||||
{
|
||||
@ -248,11 +248,11 @@
|
||||
"percent": 29
|
||||
},
|
||||
"sensor.ulitsa_temperatura_battery": {
|
||||
"loaded_at": 1774456464,
|
||||
"loaded_at": 1774470062,
|
||||
"history_hours": 4320,
|
||||
"points": [
|
||||
{
|
||||
"timestamp": 1773851664,
|
||||
"timestamp": 1773865262,
|
||||
"value": 63
|
||||
},
|
||||
{
|
||||
@ -267,11 +267,11 @@
|
||||
"percent": 62
|
||||
},
|
||||
"sensor.0x44e2f8fffeb65d8e_battery": {
|
||||
"loaded_at": 1774456464,
|
||||
"loaded_at": 1774470062,
|
||||
"history_hours": 4320,
|
||||
"points": [
|
||||
{
|
||||
"timestamp": 1773851664,
|
||||
"timestamp": 1773865262,
|
||||
"value": 50
|
||||
},
|
||||
{
|
||||
@ -289,40 +289,24 @@
|
||||
{
|
||||
"timestamp": 1773925068,
|
||||
"value": 55
|
||||
},
|
||||
{
|
||||
"timestamp": 1773944510,
|
||||
"value": 50
|
||||
}
|
||||
],
|
||||
"forecast_minutes_left": null,
|
||||
"forecast_text": null,
|
||||
"forecast_slope_per_hour": 0.2435,
|
||||
"forecast_slope_per_hour": 0.054,
|
||||
"forecast_reason": "Заряд не падает",
|
||||
"percent": 40
|
||||
},
|
||||
"sensor.0x54ef4410009a6a11_battery": {
|
||||
"loaded_at": 1774456464,
|
||||
"loaded_at": 1774470062,
|
||||
"history_hours": 4320,
|
||||
"points": [
|
||||
{
|
||||
"timestamp": 1773851664,
|
||||
"value": 92
|
||||
},
|
||||
{
|
||||
"timestamp": 1773852462,
|
||||
"value": 93
|
||||
},
|
||||
{
|
||||
"timestamp": 1773855575,
|
||||
"value": 91
|
||||
},
|
||||
{
|
||||
"timestamp": 1773858820,
|
||||
"value": 92
|
||||
},
|
||||
{
|
||||
"timestamp": 1773862156,
|
||||
"value": 91
|
||||
},
|
||||
{
|
||||
"timestamp": 1773865222,
|
||||
"timestamp": 1773865262,
|
||||
"value": 92
|
||||
},
|
||||
{
|
||||
@ -388,20 +372,32 @@
|
||||
{
|
||||
"timestamp": 1773936397,
|
||||
"value": 93
|
||||
},
|
||||
{
|
||||
"timestamp": 1773939621,
|
||||
"value": 92
|
||||
},
|
||||
{
|
||||
"timestamp": 1773942985,
|
||||
"value": 94
|
||||
},
|
||||
{
|
||||
"timestamp": 1773946246,
|
||||
"value": 93
|
||||
}
|
||||
],
|
||||
"forecast_minutes_left": null,
|
||||
"forecast_text": null,
|
||||
"forecast_slope_per_hour": 0.015,
|
||||
"forecast_reason": "Заряд не падает",
|
||||
"percent": 92
|
||||
"forecast_slope_per_hour": 0.0057,
|
||||
"forecast_reason": "Нет заметного разряда",
|
||||
"percent": 89
|
||||
},
|
||||
"sensor.0x00124b0035558456_battery": {
|
||||
"loaded_at": 1774456464,
|
||||
"loaded_at": 1774470062,
|
||||
"history_hours": 4320,
|
||||
"points": [
|
||||
{
|
||||
"timestamp": 1773851664,
|
||||
"timestamp": 1773865262,
|
||||
"value": 82
|
||||
},
|
||||
{
|
||||
@ -416,11 +412,11 @@
|
||||
"percent": 73
|
||||
},
|
||||
"sensor.0xa4c13874f5fdfd2a_battery": {
|
||||
"loaded_at": 1774456464,
|
||||
"loaded_at": 1774470062,
|
||||
"history_hours": 4320,
|
||||
"points": [
|
||||
{
|
||||
"timestamp": 1773851664,
|
||||
"timestamp": 1773865262,
|
||||
"value": 92
|
||||
},
|
||||
{
|
||||
@ -435,11 +431,11 @@
|
||||
"percent": 93
|
||||
},
|
||||
"sensor.0x54ef44100119db20_battery": {
|
||||
"loaded_at": 1774456464,
|
||||
"loaded_at": 1774470062,
|
||||
"history_hours": 4320,
|
||||
"points": [
|
||||
{
|
||||
"timestamp": 1773851664,
|
||||
"timestamp": 1773865262,
|
||||
"value": 100
|
||||
},
|
||||
{
|
||||
@ -464,11 +460,11 @@
|
||||
"percent": 100
|
||||
},
|
||||
"sensor.0x0ceff6fffe6cffc4_battery": {
|
||||
"loaded_at": 1774456464,
|
||||
"loaded_at": 1774470062,
|
||||
"history_hours": 4320,
|
||||
"points": [
|
||||
{
|
||||
"timestamp": 1773851664,
|
||||
"timestamp": 1773865262,
|
||||
"value": 45
|
||||
},
|
||||
{
|
||||
@ -510,20 +506,52 @@
|
||||
{
|
||||
"timestamp": 1773925068,
|
||||
"value": 50
|
||||
},
|
||||
{
|
||||
"timestamp": 1773944491,
|
||||
"value": 55
|
||||
},
|
||||
{
|
||||
"timestamp": 1773944511,
|
||||
"value": 50
|
||||
},
|
||||
{
|
||||
"timestamp": 1773946215,
|
||||
"value": 45
|
||||
},
|
||||
{
|
||||
"timestamp": 1773946247,
|
||||
"value": 50
|
||||
},
|
||||
{
|
||||
"timestamp": 1773946532,
|
||||
"value": 45
|
||||
},
|
||||
{
|
||||
"timestamp": 1773946537,
|
||||
"value": 55
|
||||
},
|
||||
{
|
||||
"timestamp": 1773946544,
|
||||
"value": 45
|
||||
},
|
||||
{
|
||||
"timestamp": 1773946566,
|
||||
"value": 50
|
||||
}
|
||||
],
|
||||
"forecast_minutes_left": null,
|
||||
"forecast_text": null,
|
||||
"forecast_slope_per_hour": 0.2618,
|
||||
"forecast_slope_per_hour": 0.1586,
|
||||
"forecast_reason": "Заряд не падает",
|
||||
"percent": 55
|
||||
"percent": 50
|
||||
},
|
||||
"sensor.0x0ceff6fffe6cdee0_battery": {
|
||||
"loaded_at": 1774456464,
|
||||
"loaded_at": 1774470062,
|
||||
"history_hours": 4320,
|
||||
"points": [
|
||||
{
|
||||
"timestamp": 1773851664,
|
||||
"timestamp": 1773865262,
|
||||
"value": 0
|
||||
},
|
||||
{
|
||||
@ -557,13 +585,21 @@
|
||||
{
|
||||
"timestamp": 1773925068,
|
||||
"value": 60
|
||||
},
|
||||
{
|
||||
"timestamp": 1773944490,
|
||||
"value": 65
|
||||
},
|
||||
{
|
||||
"timestamp": 1773944518,
|
||||
"value": 55
|
||||
}
|
||||
],
|
||||
"forecast_minutes_left": null,
|
||||
"forecast_text": null,
|
||||
"forecast_slope_per_hour": 3.1712,
|
||||
"forecast_slope_per_hour": 2.7826,
|
||||
"forecast_reason": "Заряд не падает",
|
||||
"percent": 55
|
||||
"percent": 50
|
||||
},
|
||||
"sensor.0x705464fffe43dee0_battery": {
|
||||
"loaded_at": 1774456345,
|
||||
@ -605,11 +641,11 @@
|
||||
"percent": 25
|
||||
},
|
||||
"sensor.0xa4c138259d164c22_battery": {
|
||||
"loaded_at": 1774456464,
|
||||
"loaded_at": 1774470062,
|
||||
"history_hours": 4320,
|
||||
"points": [
|
||||
{
|
||||
"timestamp": 1773851664,
|
||||
"timestamp": 1773865262,
|
||||
"value": 88.5
|
||||
},
|
||||
{
|
||||
@ -647,11 +683,11 @@
|
||||
"percent": 88
|
||||
},
|
||||
"sensor.spalnya_temp_battery": {
|
||||
"loaded_at": 1774456464,
|
||||
"loaded_at": 1774470062,
|
||||
"history_hours": 4320,
|
||||
"points": [
|
||||
{
|
||||
"timestamp": 1773851664,
|
||||
"timestamp": 1773865262,
|
||||
"value": 3
|
||||
},
|
||||
{
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"active": false,
|
||||
"sensor_entity_id": "binary_sensor.doorbell_all_occupancy",
|
||||
"opened_at": 1774456141,
|
||||
"opened_at": 1774457850,
|
||||
"expires_at": null
|
||||
}
|
||||
|
||||
@ -49,6 +49,11 @@ body.is-embedded {
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
body.is-ha-native #camera-modal,
|
||||
body.is-ha-native .camera-modal {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
button,
|
||||
input,
|
||||
select,
|
||||
@ -261,7 +266,7 @@ textarea {
|
||||
|
||||
.room-list__group {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
@ -293,7 +298,8 @@ textarea {
|
||||
height: 42px;
|
||||
}
|
||||
|
||||
.app-shell--embed .room-item__icon i {
|
||||
.app-shell--embed .room-item__icon i,
|
||||
.app-shell--embed .room-item__icon ha-icon {
|
||||
font-size: 32px;
|
||||
}
|
||||
|
||||
@ -375,6 +381,17 @@ textarea {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.room-item__icon ha-icon,
|
||||
.grid-card__icon ha-icon,
|
||||
.mushroom-button__icon ha-icon,
|
||||
.icon-node--ha {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.icon-node__img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
@ -509,6 +526,10 @@ textarea {
|
||||
padding: 22px 16px 20px;
|
||||
}
|
||||
|
||||
.app-shell.is-ha-native .content-top {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.content-top {
|
||||
display: none;
|
||||
margin-bottom: 16px;
|
||||
|
||||
@ -3,7 +3,7 @@
|
||||
const MOBILE_BREAKPOINT = 920;
|
||||
const state = {
|
||||
snapshot: bootstrap,
|
||||
embedMode: Boolean(bootstrap?.ui?.embed),
|
||||
embedMode: Boolean(bootstrap?.ui?.embed || window.WALL_PANEL_HA_MODE),
|
||||
selectedRoomId: 'main',
|
||||
isMobileViewport: false,
|
||||
mobileView: 'spaces',
|
||||
@ -44,10 +44,14 @@
|
||||
snapshotPollTimer: null,
|
||||
haSnapshotListenerInstalled: false,
|
||||
debugLastRenderSignature: '',
|
||||
lastRenderSignature: '',
|
||||
lastSidebarRenderSignature: '',
|
||||
lastContentRenderSignature: '',
|
||||
};
|
||||
|
||||
const els = {};
|
||||
const client = window.StrikerPanelClient || (window.StrikerPanelClient = {});
|
||||
let renderFrame = null;
|
||||
const debugEnabled = (() => {
|
||||
try {
|
||||
if (window.StrikerPanelDebug) return true;
|
||||
@ -81,14 +85,20 @@
|
||||
}
|
||||
state.snapshot = snapshot;
|
||||
initRefs();
|
||||
state.embedMode = detectEmbeddedContext();
|
||||
state.embedMode = detectEmbeddedContext() || isHaRuntime();
|
||||
syncLayoutState();
|
||||
render();
|
||||
if (renderFrame) {
|
||||
return;
|
||||
}
|
||||
renderFrame = window.requestAnimationFrame(() => {
|
||||
renderFrame = null;
|
||||
render();
|
||||
});
|
||||
};
|
||||
|
||||
client.refresh = () => {
|
||||
initRefs();
|
||||
state.embedMode = detectEmbeddedContext();
|
||||
state.embedMode = detectEmbeddedContext() || isHaRuntime();
|
||||
syncLayoutState();
|
||||
render();
|
||||
};
|
||||
@ -118,11 +128,22 @@
|
||||
|
||||
function isHaRuntime() {
|
||||
return Boolean(
|
||||
window.WALL_PANEL_HA_MODE ||
|
||||
haBridge()
|
||||
|| bootstrap?.ui?.mode === 'ha-native'
|
||||
);
|
||||
}
|
||||
|
||||
async function waitForHaBridge(timeoutMs = 1000) {
|
||||
const startedAt = Date.now();
|
||||
let bridge = haBridge();
|
||||
while (!bridge && (Date.now() - startedAt) < timeoutMs) {
|
||||
await sleep(50);
|
||||
bridge = haBridge();
|
||||
}
|
||||
return bridge;
|
||||
}
|
||||
|
||||
function sleep(ms) {
|
||||
return new Promise((resolve) => window.setTimeout(resolve, Math.max(0, Number(ms) || 0)));
|
||||
}
|
||||
@ -160,6 +181,254 @@
|
||||
};
|
||||
}
|
||||
|
||||
function snapshotEntityToken(snapshot, entityId) {
|
||||
if (!snapshot || !entityId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const collections = [
|
||||
snapshot.main_entities,
|
||||
snapshot.selected_space?.entities,
|
||||
snapshot.selected_room?.entities,
|
||||
];
|
||||
if (snapshot.entity_index && typeof snapshot.entity_index === 'object') {
|
||||
collections.push(Object.values(snapshot.entity_index));
|
||||
}
|
||||
if (snapshot.space_index && typeof snapshot.space_index === 'object') {
|
||||
Object.values(snapshot.space_index).forEach((room) => {
|
||||
if (room?.entities) {
|
||||
collections.push(room.entities);
|
||||
}
|
||||
});
|
||||
}
|
||||
if (snapshot.space_entities && typeof snapshot.space_entities === 'object') {
|
||||
Object.values(snapshot.space_entities).forEach((entities) => collections.push(entities));
|
||||
}
|
||||
if (snapshot.battery_room?.entities) {
|
||||
collections.push(snapshot.battery_room.entities);
|
||||
}
|
||||
|
||||
for (const collection of collections) {
|
||||
if (!Array.isArray(collection)) continue;
|
||||
const found = collection.find((entity) => entity && entity.entity_id === entityId);
|
||||
if (found) {
|
||||
const attr = found.attributes || {};
|
||||
return [
|
||||
String(found.entity_id || ''),
|
||||
String(found.state ?? ''),
|
||||
String(found.last_changed || found.last_updated || ''),
|
||||
String(attr.current_temperature ?? attr.temperature ?? ''),
|
||||
String(attr.current_position ?? ''),
|
||||
String(attr.hvac_action ?? ''),
|
||||
];
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function entityRenderToken(entity) {
|
||||
const attr = entity?.attributes || {};
|
||||
return [
|
||||
String(entity?.entity_id || ''),
|
||||
String(entity?.state ?? ''),
|
||||
String(entity?.order ?? 9999),
|
||||
entity?.visible === false ? '0' : '1',
|
||||
String(entity?.domain || ''),
|
||||
String(entity?.card_type || ''),
|
||||
String(entity?.subtitle || ''),
|
||||
String(entity?.icon || ''),
|
||||
String(attr.current_temperature ?? attr.temperature ?? ''),
|
||||
String(attr.current_position ?? ''),
|
||||
String(attr.hvac_action ?? ''),
|
||||
];
|
||||
}
|
||||
|
||||
function roomRenderToken(room) {
|
||||
return [
|
||||
String(room?.id || ''),
|
||||
String(room?.name || ''),
|
||||
String(room?.icon || ''),
|
||||
room?.visible === false ? '0' : '1',
|
||||
String(room?.order ?? 9999),
|
||||
String(room?.entity_count ?? 0),
|
||||
String(room?.active_entity_count ?? 0),
|
||||
String(room?.temperature_badge || ''),
|
||||
String(room?.battery_summary_text || ''),
|
||||
room?.virtual ? '1' : '0',
|
||||
];
|
||||
}
|
||||
|
||||
function selectedRoomRenderToken(snapshot) {
|
||||
const room = snapshot?.selected_room || snapshot?.selected_space || {};
|
||||
if (!room?.id) {
|
||||
return ['unknown'];
|
||||
}
|
||||
|
||||
if (room.id === 'main') {
|
||||
const boiler = snapshot?.settings?.main_boiler || {};
|
||||
const printConfig = snapshot?.settings?.main_print || {};
|
||||
const weatherActions = Array.isArray(snapshot?.settings?.main_weather_actions)
|
||||
? snapshot.settings.main_weather_actions
|
||||
: [];
|
||||
return [
|
||||
'main',
|
||||
room.name || '',
|
||||
(Array.isArray(snapshot?.main_entities) ? snapshot.main_entities : []).map(entityRenderToken),
|
||||
snapshotEntityToken(snapshot, boiler.sensor_entity_id || ''),
|
||||
snapshotEntityToken(snapshot, printConfig.current_stage_entity_id || ''),
|
||||
snapshotEntityToken(snapshot, printConfig.print_progress_entity_id || ''),
|
||||
snapshotEntityToken(snapshot, printConfig.start_time_entity_id || ''),
|
||||
snapshotEntityToken(snapshot, printConfig.end_time_entity_id || ''),
|
||||
weatherActions.map((action) => [
|
||||
String(action?.entity_id || ''),
|
||||
String(action?.state_entity_id || ''),
|
||||
String(action?.command || ''),
|
||||
String(action?.value ?? ''),
|
||||
String(action?.active_value ?? ''),
|
||||
String(action?.label_active || ''),
|
||||
String(action?.label_inactive || ''),
|
||||
String(mainWeatherActionIsActive(snapshot, action) ? '1' : '0'),
|
||||
]),
|
||||
snapshot?.weather ? [
|
||||
String(snapshot.weather.entity_id || ''),
|
||||
String(snapshot.weather.state ?? ''),
|
||||
String(snapshot.weather.temperature ?? ''),
|
||||
String(snapshot.weather.sensor_temperature ?? ''),
|
||||
String(snapshot.weather.wind_speed ?? ''),
|
||||
String(snapshot.weather.condition ?? ''),
|
||||
] : null,
|
||||
];
|
||||
}
|
||||
|
||||
if (room.id === 'batteries') {
|
||||
return [
|
||||
'batteries',
|
||||
room.name || '',
|
||||
room.battery_summary_text || '',
|
||||
Number(room.entity_count ?? 0) || 0,
|
||||
Number(room.problem_count ?? room.active_entity_count ?? 0) || 0,
|
||||
(Array.isArray(room.entities) ? room.entities : []).map((item) => [
|
||||
String(item?.entity_id || ''),
|
||||
String(item?.battery_status || ''),
|
||||
String(item?.battery_percent_text || ''),
|
||||
String(item?.forecast_minutes_left ?? ''),
|
||||
String(item?.forecast_text || ''),
|
||||
String(item?.source_text || ''),
|
||||
]),
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
room.id,
|
||||
room.name || '',
|
||||
room.icon || '',
|
||||
room.visible === false ? '0' : '1',
|
||||
String(room.order ?? 9999),
|
||||
String(room.temperature_badge || ''),
|
||||
roomGridEntries(snapshot, room.id).map((entry) => (
|
||||
entry.kind === 'layout'
|
||||
? ['layout', String(entry.id || ''), String(entry.order ?? 9999), String(entry.payload?.type || 'ghost')]
|
||||
: ['entity', ...entityRenderToken(entry.payload)]
|
||||
)),
|
||||
state.editMode
|
||||
? roomEntitiesIncludingHidden(snapshot, room.id)
|
||||
.filter((entity) => entity.visible === false)
|
||||
.map(entityRenderToken)
|
||||
: [],
|
||||
];
|
||||
}
|
||||
|
||||
function buildVisibleSnapshotSignature(snapshot) {
|
||||
const rooms = Array.isArray(snapshot?.rooms) ? snapshot.rooms : Array.isArray(snapshot?.spaces) ? snapshot.spaces : [];
|
||||
const selectedRoom = snapshot?.selected_room || snapshot?.selected_space || {};
|
||||
const batteryRoom = snapshot?.battery_room || null;
|
||||
return JSON.stringify([
|
||||
String(snapshot?.ui?.mode || 'unknown'),
|
||||
String(selectedRoom?.id || 'main'),
|
||||
rooms.map(roomRenderToken),
|
||||
batteryRoom ? roomRenderToken(batteryRoom) : null,
|
||||
selectedRoomRenderToken(snapshot),
|
||||
]);
|
||||
}
|
||||
|
||||
function buildSidebarRenderSignature(snapshot) {
|
||||
const rooms = Array.isArray(snapshot?.rooms) ? snapshot.rooms : Array.isArray(snapshot?.spaces) ? snapshot.spaces : [];
|
||||
const batteryRoom = snapshot?.battery_room || null;
|
||||
return JSON.stringify([
|
||||
String(state.selectedRoomId || 'main'),
|
||||
String(state.editMode ? '1' : '0'),
|
||||
String(state.mobileView || 'spaces'),
|
||||
String(isMobileViewport() ? '1' : '0'),
|
||||
rooms.map(roomRenderToken),
|
||||
batteryRoom ? roomRenderToken(batteryRoom) : null,
|
||||
]);
|
||||
}
|
||||
|
||||
function buildContentRenderSignature(snapshot) {
|
||||
const room = snapshot?.selected_room || snapshot?.selected_space || {};
|
||||
return JSON.stringify([
|
||||
String(state.selectedRoomId || room.id || 'main'),
|
||||
String(room.id || 'main'),
|
||||
String(state.editMode ? '1' : '0'),
|
||||
String(state.mobileView || 'spaces'),
|
||||
String(isMobileViewport() ? '1' : '0'),
|
||||
selectedRoomRenderToken(snapshot),
|
||||
]);
|
||||
}
|
||||
|
||||
function buildRenderSignature(snapshot) {
|
||||
const history = state.mainBoilerHistory || {};
|
||||
return JSON.stringify([
|
||||
buildVisibleSnapshotSignature(snapshot),
|
||||
String(state.selectedRoomId || 'main'),
|
||||
String(state.editMode ? '1' : '0'),
|
||||
String(state.mobileView || 'spaces'),
|
||||
String(isMobileViewport() ? '1' : '0'),
|
||||
String(history.entityId || ''),
|
||||
String(history.loadedAt || 0),
|
||||
String(Array.isArray(history.points) ? history.points.length : 0),
|
||||
String(history.loading ? '1' : '0'),
|
||||
String(history.error || ''),
|
||||
String(state.entityPopup?.active ? '1' : '0'),
|
||||
String(state.entityPopup?.entityId || ''),
|
||||
String(state.temperatureSensorPopup?.active ? '1' : '0'),
|
||||
String(state.temperatureSensorPopup?.roomId || ''),
|
||||
String(state.lastPopupSignature || ''),
|
||||
String(state.lastEntityPopupSignature || ''),
|
||||
String(state.lastTemperatureSensorPopupSignature || ''),
|
||||
]);
|
||||
}
|
||||
|
||||
function renderSidebarSection(snapshot) {
|
||||
const nextSignature = buildSidebarRenderSignature(snapshot);
|
||||
if (nextSignature === state.lastSidebarRenderSignature) {
|
||||
return false;
|
||||
}
|
||||
state.lastSidebarRenderSignature = nextSignature;
|
||||
renderRoomButtons(snapshot, snapshot.spaces || snapshot.rooms, snapshot.battery_room);
|
||||
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';
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function renderContentSection(snapshot) {
|
||||
const nextSignature = buildContentRenderSignature(snapshot);
|
||||
if (nextSignature === state.lastContentRenderSignature) {
|
||||
return false;
|
||||
}
|
||||
state.lastContentRenderSignature = nextSignature;
|
||||
syncLayoutState();
|
||||
renderDashboard(snapshot);
|
||||
renderSelectedRoom(snapshot);
|
||||
return true;
|
||||
}
|
||||
|
||||
function requestSummary(action, params = {}) {
|
||||
const summary = { action };
|
||||
['space_id', 'room_id', 'entity_id', 'layout_item_id', 'command', 'value', 'hours', 'state', 'edit_mode'].forEach((key) => {
|
||||
@ -309,6 +578,9 @@
|
||||
}
|
||||
|
||||
function detectEmbeddedContext() {
|
||||
if (isHaRuntime()) {
|
||||
return true;
|
||||
}
|
||||
if (Boolean(bootstrap?.ui?.embed)) {
|
||||
return true;
|
||||
}
|
||||
@ -378,20 +650,160 @@
|
||||
return svg;
|
||||
}
|
||||
|
||||
function createCustomIconElement(source, fallback = 'mdi:help-circle-outline') {
|
||||
const iconTemplateCache = new Map();
|
||||
const iconTemplatePromiseCache = new Map();
|
||||
const customBrandIconsUrl = 'https://home.striker72rus.ru/local/community/custom-brand-icons/custom-brand-icons.js';
|
||||
let customBrandIconsPromise = null;
|
||||
|
||||
function templateFromSvgText(svgText) {
|
||||
const template = document.createElement('template');
|
||||
template.innerHTML = String(svgText || '').trim();
|
||||
return template;
|
||||
}
|
||||
|
||||
function iconUrl(source) {
|
||||
return `https://api.iconify.design/${source}.svg`;
|
||||
}
|
||||
|
||||
function ensureCustomBrandIconsLoaded() {
|
||||
if (window.customIcons || window.customIconsets) {
|
||||
return Promise.resolve(true);
|
||||
}
|
||||
if (customBrandIconsPromise) {
|
||||
return customBrandIconsPromise;
|
||||
}
|
||||
|
||||
const existing = document.querySelector('script[data-wall-panel-custom-brand-icons="1"]');
|
||||
if (existing) {
|
||||
customBrandIconsPromise = new Promise((resolve, reject) => {
|
||||
if (window.customIcons || window.customIconsets) {
|
||||
resolve(true);
|
||||
return;
|
||||
}
|
||||
existing.addEventListener('load', () => resolve(true), { once: true });
|
||||
existing.addEventListener('error', () => reject(new Error('custom-brand-icons load failed')), { once: true });
|
||||
}).finally(() => {
|
||||
customBrandIconsPromise = null;
|
||||
});
|
||||
return customBrandIconsPromise;
|
||||
}
|
||||
|
||||
const script = document.createElement('script');
|
||||
script.src = customBrandIconsUrl;
|
||||
script.defer = true;
|
||||
script.async = true;
|
||||
script.dataset.wallPanelCustomBrandIcons = '1';
|
||||
customBrandIconsPromise = new Promise((resolve, reject) => {
|
||||
script.addEventListener('load', () => resolve(true), { once: true });
|
||||
script.addEventListener('error', () => reject(new Error('custom-brand-icons load failed')), { once: true });
|
||||
}).finally(() => {
|
||||
customBrandIconsPromise = null;
|
||||
});
|
||||
(document.head || document.documentElement).appendChild(script);
|
||||
return customBrandIconsPromise;
|
||||
}
|
||||
|
||||
function applyTemplateToNode(target, template) {
|
||||
if (!target || !template || !target.isConnected) {
|
||||
return;
|
||||
}
|
||||
target.replaceChildren(template.content.cloneNode(true));
|
||||
}
|
||||
|
||||
function primeRemoteIconTemplate(source) {
|
||||
const key = `remote:${source}`;
|
||||
if (iconTemplateCache.has(key)) {
|
||||
return Promise.resolve(iconTemplateCache.get(key));
|
||||
}
|
||||
if (iconTemplatePromiseCache.has(key)) {
|
||||
return iconTemplatePromiseCache.get(key);
|
||||
}
|
||||
|
||||
const promise = fetch(iconUrl(source), {
|
||||
cache: 'force-cache',
|
||||
credentials: 'omit',
|
||||
headers: { Accept: 'image/svg+xml' },
|
||||
}).then((res) => {
|
||||
if (!res.ok) {
|
||||
throw new Error(`Icon load failed: ${res.status}`);
|
||||
}
|
||||
return res.text();
|
||||
}).then((svgText) => {
|
||||
const template = templateFromSvgText(svgText);
|
||||
iconTemplateCache.set(key, template);
|
||||
return template;
|
||||
}).finally(() => {
|
||||
iconTemplatePromiseCache.delete(key);
|
||||
});
|
||||
|
||||
iconTemplatePromiseCache.set(key, promise);
|
||||
return promise;
|
||||
}
|
||||
|
||||
function primeCustomIconTemplate(source) {
|
||||
const key = `custom:${source}`;
|
||||
if (iconTemplateCache.has(key)) {
|
||||
return Promise.resolve(iconTemplateCache.get(key));
|
||||
}
|
||||
if (iconTemplatePromiseCache.has(key)) {
|
||||
return iconTemplatePromiseCache.get(key);
|
||||
}
|
||||
|
||||
const [prefix, name] = source.split(':', 2);
|
||||
const customSet = window.customIcons?.[prefix] || window.customIconsets?.[prefix];
|
||||
const getIcon = typeof customSet === 'function' ? customSet : customSet?.getIcon;
|
||||
if (typeof getIcon !== 'function') {
|
||||
return null;
|
||||
return Promise.resolve(null);
|
||||
}
|
||||
|
||||
const promise = Promise.resolve(getIcon(name)).then((definition) => {
|
||||
if (!definition) {
|
||||
return null;
|
||||
}
|
||||
const template = document.createElement('template');
|
||||
template.content.appendChild(createSvgIcon(definition));
|
||||
iconTemplateCache.set(key, template);
|
||||
return template;
|
||||
}).finally(() => {
|
||||
iconTemplatePromiseCache.delete(key);
|
||||
});
|
||||
|
||||
iconTemplatePromiseCache.set(key, promise);
|
||||
return promise;
|
||||
}
|
||||
|
||||
function createCustomIconElement(source, fallback = 'mdi:help-circle-outline') {
|
||||
const cached = iconTemplateCache.get(`custom:${source}`);
|
||||
if (cached) {
|
||||
return cached.content.cloneNode(true);
|
||||
}
|
||||
|
||||
const [prefix] = source.split(':', 2);
|
||||
const customSet = window.customIcons?.[prefix] || window.customIconsets?.[prefix];
|
||||
const getIcon = typeof customSet === 'function' ? customSet : customSet?.getIcon;
|
||||
if (typeof getIcon !== 'function') {
|
||||
if (!isHaRuntime()) {
|
||||
return null;
|
||||
}
|
||||
const wrap = document.createElement('span');
|
||||
wrap.className = 'icon-node';
|
||||
wrap.appendChild(createIconElement(fallback));
|
||||
ensureCustomBrandIconsLoaded().then(() => primeCustomIconTemplate(source)).then((template) => {
|
||||
if (!template) return;
|
||||
applyTemplateToNode(wrap, template);
|
||||
}).catch(() => {
|
||||
if (!wrap.isConnected) return;
|
||||
wrap.replaceChildren(createIconElement(fallback));
|
||||
});
|
||||
return wrap;
|
||||
}
|
||||
|
||||
const wrap = document.createElement('span');
|
||||
wrap.className = 'icon-node';
|
||||
wrap.appendChild(createIconElement(fallback));
|
||||
Promise.resolve(getIcon(name)).then((definition) => {
|
||||
if (!definition || !wrap.isConnected) return;
|
||||
wrap.replaceChildren(createSvgIcon(definition));
|
||||
primeCustomIconTemplate(source).then((template) => {
|
||||
if (!template) return;
|
||||
applyTemplateToNode(wrap, template);
|
||||
}).catch(() => {
|
||||
if (!wrap.isConnected) return;
|
||||
wrap.replaceChildren(createIconElement(fallback));
|
||||
@ -403,77 +815,71 @@
|
||||
const source = normalizeIconSource(icon) || fallback;
|
||||
|
||||
if (source.startsWith('mdi:')) {
|
||||
if (isHaRuntime()) {
|
||||
const haIcon = document.createElement('ha-icon');
|
||||
haIcon.setAttribute('icon', source);
|
||||
haIcon.className = 'icon-node icon-node--ha';
|
||||
return haIcon;
|
||||
}
|
||||
const i = document.createElement('i');
|
||||
i.className = iconClass(source);
|
||||
i.setAttribute('aria-hidden', 'true');
|
||||
return i;
|
||||
}
|
||||
|
||||
if (source.startsWith('fas:') || source.startsWith('far:') || source.startsWith('fab:')) {
|
||||
const custom = createCustomIconElement(source, fallback);
|
||||
if (custom) {
|
||||
return custom;
|
||||
}
|
||||
const mappedSource = source.startsWith('fas:')
|
||||
? source.replace(/^fas:/, 'fa-solid:')
|
||||
: source.startsWith('far:')
|
||||
? source.replace(/^far:/, 'fa-regular:')
|
||||
: source.replace(/^fab:/, 'fa-brands:');
|
||||
const wrap = document.createElement('span');
|
||||
wrap.className = 'icon-node';
|
||||
wrap.appendChild(createIconElement(fallback));
|
||||
const img = document.createElement('img');
|
||||
img.className = 'icon-node__img';
|
||||
img.alt = '';
|
||||
img.decoding = 'async';
|
||||
img.loading = 'lazy';
|
||||
img.referrerPolicy = 'no-referrer';
|
||||
img.src = `https://api.iconify.design/${mappedSource}.svg`;
|
||||
img.addEventListener('load', () => {
|
||||
if (!img.isConnected || !wrap.isConnected) return;
|
||||
wrap.replaceChildren(img);
|
||||
});
|
||||
img.addEventListener('error', () => {
|
||||
if (img.dataset.fallbackApplied === '1') return;
|
||||
img.dataset.fallbackApplied = '1';
|
||||
wrap.replaceChildren(createIconElement(fallback));
|
||||
});
|
||||
wrap.appendChild(img);
|
||||
return wrap;
|
||||
}
|
||||
|
||||
const custom = createCustomIconElement(source, fallback);
|
||||
if (custom) {
|
||||
return custom;
|
||||
}
|
||||
|
||||
if (source.startsWith('fas:') || source.startsWith('far:') || source.startsWith('fab:')) {
|
||||
const mappedSource = source.startsWith('fas:')
|
||||
? source.replace(/^fas:/, 'fa-solid:')
|
||||
: source.startsWith('far:')
|
||||
? source.replace(/^far:/, 'fa-regular:')
|
||||
: source.replace(/^fab:/, 'fa-brands:');
|
||||
const cached = iconTemplateCache.get(`remote:${mappedSource}`);
|
||||
if (cached) {
|
||||
return cached.content.cloneNode(true);
|
||||
}
|
||||
const wrap = document.createElement('span');
|
||||
wrap.className = 'icon-node';
|
||||
wrap.appendChild(createIconElement(fallback));
|
||||
primeRemoteIconTemplate(mappedSource).then((template) => {
|
||||
applyTemplateToNode(wrap, template);
|
||||
}).catch(() => {
|
||||
if (!wrap.isConnected) return;
|
||||
wrap.replaceChildren(createIconElement(fallback));
|
||||
});
|
||||
return wrap;
|
||||
}
|
||||
|
||||
const remoteCached = iconTemplateCache.get(`remote:${source}`);
|
||||
if (remoteCached) {
|
||||
return remoteCached.content.cloneNode(true);
|
||||
}
|
||||
|
||||
const wrap = document.createElement('span');
|
||||
wrap.className = 'icon-node';
|
||||
wrap.appendChild(createIconElement(fallback));
|
||||
|
||||
if (source.includes(':')) {
|
||||
const img = document.createElement('img');
|
||||
img.className = 'icon-node__img';
|
||||
img.alt = '';
|
||||
img.decoding = 'async';
|
||||
img.loading = 'lazy';
|
||||
img.referrerPolicy = 'no-referrer';
|
||||
img.src = `https://api.iconify.design/${source}.svg`;
|
||||
img.addEventListener('load', () => {
|
||||
if (!img.isConnected || !wrap.isConnected) return;
|
||||
wrap.replaceChildren(img);
|
||||
});
|
||||
img.addEventListener('error', () => {
|
||||
if (img.dataset.fallbackApplied === '1') return;
|
||||
img.dataset.fallbackApplied = '1';
|
||||
primeRemoteIconTemplate(source).then((template) => {
|
||||
applyTemplateToNode(wrap, template);
|
||||
}).catch(() => {
|
||||
if (!wrap.isConnected) return;
|
||||
wrap.replaceChildren(createIconElement(fallback));
|
||||
});
|
||||
wrap.appendChild(img);
|
||||
return wrap;
|
||||
}
|
||||
|
||||
return createIconElement(fallback);
|
||||
}
|
||||
|
||||
if (isHaRuntime()) {
|
||||
void ensureCustomBrandIconsLoaded();
|
||||
}
|
||||
|
||||
function esc(value) {
|
||||
return String(value ?? '');
|
||||
}
|
||||
@ -550,6 +956,9 @@
|
||||
}
|
||||
|
||||
function popupTriggerEntities() {
|
||||
if (isHaRuntime()) {
|
||||
return new Set();
|
||||
}
|
||||
const fromSnapshot = state.snapshot?.settings?.camera?.trigger_entities;
|
||||
const fromBootstrap = bootstrap?.settings?.camera?.trigger_entities;
|
||||
const triggers = Array.isArray(fromSnapshot) ? fromSnapshot : Array.isArray(fromBootstrap) ? fromBootstrap : [];
|
||||
@ -672,6 +1081,14 @@
|
||||
debugLog('apiGet via bridge', requestSummary(action, params));
|
||||
return bridge.request('GET', action, params);
|
||||
}
|
||||
if (isHaRuntime()) {
|
||||
const waited = await waitForHaBridge(1000);
|
||||
if (waited?.request) {
|
||||
debugLog('apiGet via delayed bridge', requestSummary(action, params));
|
||||
return waited.request('GET', action, params);
|
||||
}
|
||||
throw new Error('HA bridge is not ready');
|
||||
}
|
||||
debugLog('apiGet via http', requestSummary(action, params));
|
||||
const res = await fetch(buildUrl(action, params), {
|
||||
headers: { Accept: 'application/json' },
|
||||
@ -689,6 +1106,14 @@
|
||||
debugLog('apiPost via bridge', requestSummary(action, payload));
|
||||
return bridge.request('POST', action, payload);
|
||||
}
|
||||
if (isHaRuntime()) {
|
||||
const waited = await waitForHaBridge(1000);
|
||||
if (waited?.request) {
|
||||
debugLog('apiPost via delayed bridge', requestSummary(action, payload));
|
||||
return waited.request('POST', action, payload);
|
||||
}
|
||||
throw new Error('HA bridge is not ready');
|
||||
}
|
||||
debugLog('apiPost via http', requestSummary(action, payload));
|
||||
const res = await fetch(buildUrl(action), {
|
||||
method: 'POST',
|
||||
@ -1183,12 +1608,16 @@
|
||||
if (!els.appShell) return;
|
||||
|
||||
const mobile = isMobileViewport();
|
||||
const embedded = Boolean(state.embedMode);
|
||||
const haNative = isHaRuntime();
|
||||
const embedded = Boolean(state.embedMode || haNative);
|
||||
state.embedMode = embedded;
|
||||
document.body.classList.toggle('is-mobile-ui', mobile);
|
||||
document.body.classList.toggle('is-embedded', embedded);
|
||||
document.body.classList.toggle('is-ha-native', haNative);
|
||||
els.appShell.classList.toggle('is-mobile', mobile);
|
||||
els.appShell.classList.toggle('is-desktop', !mobile);
|
||||
els.appShell.classList.toggle('app-shell--embed', embedded);
|
||||
els.appShell.classList.toggle('is-ha-native', haNative);
|
||||
els.appShell.classList.toggle('mobile-view-spaces', mobile && state.mobileView !== 'room');
|
||||
els.appShell.classList.toggle('mobile-view-room', mobile && state.mobileView === 'room');
|
||||
|
||||
@ -1865,6 +2294,9 @@
|
||||
}
|
||||
|
||||
function applyPopupState(active, sensorEntityId) {
|
||||
if (isHaRuntime()) {
|
||||
return;
|
||||
}
|
||||
const camera = state.snapshot?.settings?.camera || bootstrap?.settings?.camera || {};
|
||||
const popup = state.snapshot?.popup || {};
|
||||
if (active && Date.now() < Number(state.popupAutoOpenBlockedUntil || 0)) {
|
||||
@ -1888,6 +2320,9 @@
|
||||
}
|
||||
|
||||
function applyPopupSnapshot(popup = {}) {
|
||||
if (isHaRuntime()) {
|
||||
return;
|
||||
}
|
||||
const snapshot = state.snapshot || bootstrap;
|
||||
snapshot.popup = mergePopupWithCamera({
|
||||
...(snapshot.popup || {}),
|
||||
@ -1897,6 +2332,9 @@
|
||||
}
|
||||
|
||||
function syncTriggerPopup(entityId, stateValue) {
|
||||
if (isHaRuntime()) {
|
||||
return;
|
||||
}
|
||||
const value = String(stateValue || '').toLowerCase();
|
||||
if (!['on', 'off'].includes(value)) {
|
||||
return;
|
||||
@ -2870,13 +3308,13 @@
|
||||
const minus = document.createElement('button');
|
||||
minus.type = 'button';
|
||||
minus.className = 'round-button entity-modal__round-button';
|
||||
minus.innerHTML = '<i class="mdi mdi-minus"></i>';
|
||||
minus.appendChild(createIconElement('mdi:minus'));
|
||||
minus.addEventListener('click', () => handleClimateTemperature(entity, -1));
|
||||
|
||||
const plus = document.createElement('button');
|
||||
plus.type = 'button';
|
||||
plus.className = 'round-button entity-modal__round-button';
|
||||
plus.innerHTML = '<i class="mdi mdi-plus"></i>';
|
||||
plus.appendChild(createIconElement('mdi:plus'));
|
||||
plus.addEventListener('click', () => handleClimateTemperature(entity, 1));
|
||||
|
||||
controls.append(minus, plus);
|
||||
@ -3349,13 +3787,15 @@
|
||||
const upBtn = document.createElement('button');
|
||||
upBtn.type = 'button';
|
||||
upBtn.className = 'mushroom-button mushroom-button--small';
|
||||
upBtn.innerHTML = '<i class="mdi mdi-arrow-up"></i> Вверх';
|
||||
upBtn.appendChild(createIconElement('mdi:arrow-up'));
|
||||
upBtn.appendChild(document.createTextNode(' Вверх'));
|
||||
upBtn.addEventListener('click', () => reorderRoomGridEntry(currentRoom()?.id, 'entity', entity.entity_id, -1));
|
||||
|
||||
const downBtn = document.createElement('button');
|
||||
downBtn.type = 'button';
|
||||
downBtn.className = 'mushroom-button mushroom-button--small';
|
||||
downBtn.innerHTML = '<i class="mdi mdi-arrow-down"></i> Вниз';
|
||||
downBtn.appendChild(createIconElement('mdi:arrow-down'));
|
||||
downBtn.appendChild(document.createTextNode(' Вниз'));
|
||||
downBtn.addEventListener('click', () => reorderRoomGridEntry(currentRoom()?.id, 'entity', entity.entity_id, 1));
|
||||
|
||||
actions.append(upBtn, downBtn);
|
||||
@ -3371,7 +3811,7 @@
|
||||
const btn = document.createElement('button');
|
||||
btn.type = 'button';
|
||||
btn.className = 'mini-action mini-action--wide';
|
||||
btn.innerHTML = hidden ? '<i class="mdi mdi-eye"></i>' : '<i class="mdi mdi-eye-off"></i>';
|
||||
btn.appendChild(createIconElement(hidden ? 'mdi:eye' : 'mdi:eye-off'));
|
||||
btn.title = hidden ? 'Показать' : 'Скрыть';
|
||||
btn.addEventListener('click', (event) => {
|
||||
event.stopPropagation();
|
||||
@ -3484,7 +3924,8 @@
|
||||
const settingsBtn = document.createElement('button');
|
||||
settingsBtn.type = 'button';
|
||||
settingsBtn.className = 'mushroom-button mushroom-button--small mushroom-button--wide';
|
||||
settingsBtn.innerHTML = '<i class="mdi mdi-cog-outline"></i> Настройки';
|
||||
settingsBtn.appendChild(createIconElement('mdi:cog-outline'));
|
||||
settingsBtn.appendChild(document.createTextNode(' Настройки'));
|
||||
settingsBtn.addEventListener('click', (event) => {
|
||||
event.stopPropagation();
|
||||
state.layoutItemSettingsOpen = {
|
||||
@ -3497,7 +3938,8 @@
|
||||
const upBtn = document.createElement('button');
|
||||
upBtn.type = 'button';
|
||||
upBtn.className = 'mushroom-button mushroom-button--small';
|
||||
upBtn.innerHTML = '<i class="mdi mdi-arrow-up"></i> Вверх';
|
||||
upBtn.appendChild(createIconElement('mdi:arrow-up'));
|
||||
upBtn.appendChild(document.createTextNode(' Вверх'));
|
||||
upBtn.addEventListener('click', (event) => {
|
||||
event.stopPropagation();
|
||||
reorderRoomGridEntry(room.id, 'layout', item.id, -1);
|
||||
@ -3506,7 +3948,8 @@
|
||||
const downBtn = document.createElement('button');
|
||||
downBtn.type = 'button';
|
||||
downBtn.className = 'mushroom-button mushroom-button--small';
|
||||
downBtn.innerHTML = '<i class="mdi mdi-arrow-down"></i> Вниз';
|
||||
downBtn.appendChild(createIconElement('mdi:arrow-down'));
|
||||
downBtn.appendChild(document.createTextNode(' Вниз'));
|
||||
downBtn.addEventListener('click', (event) => {
|
||||
event.stopPropagation();
|
||||
reorderRoomGridEntry(room.id, 'layout', item.id, 1);
|
||||
@ -3515,7 +3958,8 @@
|
||||
const deleteBtn = document.createElement('button');
|
||||
deleteBtn.type = 'button';
|
||||
deleteBtn.className = 'mushroom-button mushroom-button--small mushroom-button--wide';
|
||||
deleteBtn.innerHTML = '<i class="mdi mdi-delete-outline"></i> Удалить';
|
||||
deleteBtn.appendChild(createIconElement('mdi:delete-outline'));
|
||||
deleteBtn.appendChild(document.createTextNode(' Удалить'));
|
||||
deleteBtn.addEventListener('click', (event) => {
|
||||
event.stopPropagation();
|
||||
deleteRoomLayoutItem(room.id, item.id);
|
||||
@ -3531,7 +3975,8 @@
|
||||
const tempBtn = document.createElement('button');
|
||||
tempBtn.type = 'button';
|
||||
tempBtn.className = 'mushroom-button mushroom-button--small mushroom-button--wide';
|
||||
tempBtn.innerHTML = '<i class="mdi mdi-thermometer"></i> Выбрать датчик температуры';
|
||||
tempBtn.appendChild(createIconElement('mdi:thermometer'));
|
||||
tempBtn.appendChild(document.createTextNode(' Выбрать датчик температуры'));
|
||||
tempBtn.addEventListener('click', (event) => {
|
||||
event.stopPropagation();
|
||||
openTemperatureSensorPopup(room.id);
|
||||
@ -3654,6 +4099,12 @@
|
||||
}
|
||||
els.roomList.innerHTML = '';
|
||||
const sortedRooms = [...(rooms || [])].sort((left, right) => {
|
||||
const leftVisible = left?.visible === false ? 1 : 0;
|
||||
const rightVisible = right?.visible === false ? 1 : 0;
|
||||
if (leftVisible !== rightVisible) {
|
||||
return leftVisible - rightVisible;
|
||||
}
|
||||
|
||||
if (left.id === 'main') return -1;
|
||||
if (right.id === 'main') return 1;
|
||||
|
||||
@ -3841,7 +4292,8 @@
|
||||
const addButton = document.createElement('button');
|
||||
addButton.type = 'button';
|
||||
addButton.className = 'mushroom-button mushroom-button--small content-header__ghost-button';
|
||||
addButton.innerHTML = '<i class="mdi mdi-plus"></i><span>Пустая карточка</span>';
|
||||
addButton.appendChild(createIconElement('mdi:plus'));
|
||||
addButton.appendChild(document.createElement('span')).textContent = 'Пустая карточка';
|
||||
addButton.addEventListener('click', () => {
|
||||
createRoomLayoutItem(room.id);
|
||||
});
|
||||
@ -3849,7 +4301,8 @@
|
||||
const temperatureButton = document.createElement('button');
|
||||
temperatureButton.type = 'button';
|
||||
temperatureButton.className = 'mushroom-button mushroom-button--small content-header__ghost-button';
|
||||
temperatureButton.innerHTML = '<i class="mdi mdi-thermometer"></i><span>Выбрать датчик температуры</span>';
|
||||
temperatureButton.appendChild(createIconElement('mdi:thermometer'));
|
||||
temperatureButton.appendChild(document.createElement('span')).textContent = 'Выбрать датчик температуры';
|
||||
temperatureButton.addEventListener('click', () => {
|
||||
openTemperatureSensorPopup(room.id);
|
||||
});
|
||||
@ -3991,6 +4444,10 @@
|
||||
}
|
||||
|
||||
function renderPopup(snapshot) {
|
||||
if (isHaRuntime()) {
|
||||
hidePopup({ preserveSnapshot: true });
|
||||
return;
|
||||
}
|
||||
if (isMobileViewport()) {
|
||||
hidePopup({ preserveSnapshot: true });
|
||||
return;
|
||||
@ -4123,6 +4580,9 @@
|
||||
}
|
||||
|
||||
async function showDebugPopup() {
|
||||
if (isHaRuntime()) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const response = await apiPost('popup', { command: 'open' });
|
||||
const snapshot = state.snapshot || bootstrap;
|
||||
@ -4266,53 +4726,25 @@
|
||||
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'),
|
||||
]);
|
||||
const renderSignature = buildRenderSignature(snapshot);
|
||||
if (renderSignature === state.lastRenderSignature) {
|
||||
return;
|
||||
}
|
||||
state.lastRenderSignature = renderSignature;
|
||||
if (renderSignature !== state.debugLastRenderSignature) {
|
||||
state.debugLastRenderSignature = renderSignature;
|
||||
debugLog('render()', snapshotSummary(snapshot));
|
||||
}
|
||||
|
||||
syncLayoutState();
|
||||
renderDashboard(snapshot);
|
||||
renderSelectedRoom(snapshot);
|
||||
renderRoomButtons(snapshot, snapshot.spaces || snapshot.rooms, snapshot.battery_room);
|
||||
renderSidebarSection(snapshot);
|
||||
renderContentSection(snapshot);
|
||||
renderPopup(snapshot);
|
||||
renderEntityPopup(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 renderDashboardOnly() {
|
||||
const snapshot = state.snapshot || bootstrap;
|
||||
if (!snapshot || !(snapshot.spaces || snapshot.rooms)) return;
|
||||
syncLayoutState();
|
||||
renderSelectedRoom(snapshot);
|
||||
renderDashboard(snapshot);
|
||||
renderPopup(snapshot);
|
||||
renderEntityPopup(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';
|
||||
}
|
||||
renderContentSection(state.snapshot || bootstrap);
|
||||
}
|
||||
|
||||
function refreshCurrentRoomLayout(entityId) {
|
||||
@ -4357,24 +4789,11 @@
|
||||
}
|
||||
|
||||
function renderSidebarOnly() {
|
||||
const snapshot = state.snapshot || bootstrap;
|
||||
if (!snapshot || !(snapshot.spaces || snapshot.rooms)) return;
|
||||
renderRoomButtons(snapshot, snapshot.spaces || snapshot.rooms, snapshot.battery_room);
|
||||
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';
|
||||
}
|
||||
renderSidebarSection(state.snapshot || bootstrap);
|
||||
}
|
||||
|
||||
function renderSelectionOnly() {
|
||||
const snapshot = state.snapshot || bootstrap;
|
||||
if (!snapshot || !(snapshot.spaces || snapshot.rooms)) return;
|
||||
syncLayoutState();
|
||||
renderSelectedRoom(snapshot);
|
||||
renderContentSection(state.snapshot || bootstrap);
|
||||
}
|
||||
|
||||
async function handleEntityAction(entity, command) {
|
||||
@ -4586,32 +5005,34 @@
|
||||
renderSelectionOnly();
|
||||
});
|
||||
|
||||
bind(els.cameraBackdrop, 'click', (event) => {
|
||||
if (event.target === els.cameraBackdrop) {
|
||||
apiPost('popup', { command: 'close' }).catch(() => {});
|
||||
hidePopup({ suppressAutoOpen: true });
|
||||
}
|
||||
});
|
||||
if (!isHaRuntime()) {
|
||||
bind(els.cameraBackdrop, 'click', (event) => {
|
||||
if (event.target === els.cameraBackdrop) {
|
||||
apiPost('popup', { command: 'close' }).catch(() => {});
|
||||
hidePopup({ suppressAutoOpen: true });
|
||||
}
|
||||
});
|
||||
|
||||
bind(els.cameraModalPanel, 'click', (event) => {
|
||||
event.stopPropagation();
|
||||
});
|
||||
|
||||
const closeCameraPopup = async (event) => {
|
||||
if (event) {
|
||||
event.preventDefault();
|
||||
bind(els.cameraModalPanel, 'click', (event) => {
|
||||
event.stopPropagation();
|
||||
}
|
||||
try {
|
||||
await apiPost('popup', { command: 'close' });
|
||||
} catch (error) {
|
||||
console.warn(error);
|
||||
}
|
||||
hidePopup({ suppressAutoOpen: true });
|
||||
};
|
||||
});
|
||||
|
||||
bind(els.cameraClose, 'pointerdown', closeCameraPopup);
|
||||
bind(els.cameraClose, 'click', closeCameraPopup);
|
||||
const closeCameraPopup = async (event) => {
|
||||
if (event) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
}
|
||||
try {
|
||||
await apiPost('popup', { command: 'close' });
|
||||
} catch (error) {
|
||||
console.warn(error);
|
||||
}
|
||||
hidePopup({ suppressAutoOpen: true });
|
||||
};
|
||||
|
||||
bind(els.cameraClose, 'pointerdown', closeCameraPopup);
|
||||
bind(els.cameraClose, 'click', closeCameraPopup);
|
||||
}
|
||||
|
||||
els.entityBackdrop?.addEventListener('click', (event) => {
|
||||
if (event.target === els.entityBackdrop) {
|
||||
@ -4975,7 +5396,7 @@
|
||||
mode: bootstrap?.ui?.mode || 'unknown',
|
||||
});
|
||||
initRefs();
|
||||
state.embedMode = detectEmbeddedContext();
|
||||
state.embedMode = detectEmbeddedContext() || isHaRuntime();
|
||||
syncLayoutState();
|
||||
syncViewportState();
|
||||
bindPressFeedback();
|
||||
|
||||
Loading…
Reference in New Issue
Block a user