Simplify scenes to capture only target state, add target selector
- Remove DeviceBrightnessSnapshot and AutomationSnapshot from scene data model - Simplify capture_current_snapshot and apply_scene_state to targets only - Remove device/automation dependencies from scene preset API routes - Add target selector (combobox + add/remove) to scene capture modal - Fix stale profiles reference bug in scene_preset_store recapture - Update automation engine call sites for simplified scene functions - Sync scene presets cache between automations and scene-presets modules Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -10,7 +10,7 @@ import { Modal } from '../core/modal.js';
|
||||
import { CardSection } from '../core/card-sections.js';
|
||||
import { updateTabBadge } from './tabs.js';
|
||||
import { ICON_SETTINGS, ICON_START, ICON_PAUSE, ICON_CLOCK, ICON_AUTOMATION, ICON_HELP, ICON_OK, ICON_TIMER, ICON_MONITOR, ICON_RADIO, ICON_SCENE } from '../core/icons.js';
|
||||
import { csScenes, createSceneCard } from './scene-presets.js';
|
||||
import { csScenes, createSceneCard, updatePresetsCache } from './scene-presets.js';
|
||||
|
||||
// ===== Scene presets cache (shared by both selectors) =====
|
||||
let _scenesCache = [];
|
||||
@@ -62,6 +62,7 @@ export async function loadAutomations() {
|
||||
const data = await automationsResp.json();
|
||||
const scenesData = scenesResp.ok ? await scenesResp.json() : { presets: [] };
|
||||
_scenesCache = scenesData.presets || [];
|
||||
updatePresetsCache(_scenesCache);
|
||||
|
||||
// Build scene name map for card rendering
|
||||
const sceneMap = new Map(_scenesCache.map(s => [s.id, s]));
|
||||
@@ -208,6 +209,7 @@ export async function openAutomationEditor(automationId) {
|
||||
if (resp.ok) {
|
||||
const data = await resp.json();
|
||||
_scenesCache = data.presets || [];
|
||||
updatePresetsCache(_scenesCache);
|
||||
}
|
||||
} catch { /* use cached */ }
|
||||
|
||||
|
||||
@@ -9,19 +9,26 @@ import { showToast, showConfirm } from '../core/ui.js';
|
||||
import { Modal } from '../core/modal.js';
|
||||
import { CardSection } from '../core/card-sections.js';
|
||||
import {
|
||||
ICON_SCENE, ICON_CAPTURE, ICON_START, ICON_EDIT, ICON_REFRESH, ICON_TARGET, ICON_SETTINGS,
|
||||
ICON_SCENE, ICON_CAPTURE, ICON_START, ICON_EDIT, ICON_REFRESH, ICON_TARGET,
|
||||
} from '../core/icons.js';
|
||||
|
||||
let _presetsCache = [];
|
||||
let _editingId = null;
|
||||
let _allTargets = []; // fetched on capture open
|
||||
|
||||
/** Update the internal presets cache (called from automations tab after fetching). */
|
||||
export function updatePresetsCache(presets) { _presetsCache = presets; }
|
||||
|
||||
class ScenePresetEditorModal extends Modal {
|
||||
constructor() { super('scene-preset-editor-modal'); }
|
||||
snapshotValues() {
|
||||
const items = [...document.querySelectorAll('#scene-target-list .scene-target-item')]
|
||||
.map(el => el.dataset.targetId).sort().join(',');
|
||||
return {
|
||||
name: document.getElementById('scene-preset-editor-name').value,
|
||||
description: document.getElementById('scene-preset-editor-description').value,
|
||||
color: document.getElementById('scene-preset-editor-color').value,
|
||||
targets: items,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -36,14 +43,10 @@ export const csScenes = new CardSection('scenes', {
|
||||
|
||||
export function createSceneCard(preset) {
|
||||
const targetCount = (preset.targets || []).length;
|
||||
const deviceCount = (preset.devices || []).length;
|
||||
const automationCount = (preset.automations || []).length;
|
||||
const colorStyle = `border-left: 3px solid ${escapeHtml(preset.color || '#4fc3f7')}`;
|
||||
|
||||
const meta = [
|
||||
targetCount > 0 ? `${ICON_TARGET} ${targetCount} ${t('scenes.targets_count')}` : null,
|
||||
deviceCount > 0 ? `${ICON_SETTINGS} ${deviceCount} ${t('scenes.devices_count')}` : null,
|
||||
automationCount > 0 ? `${automationCount} ${t('scenes.automations_count')}` : null,
|
||||
].filter(Boolean);
|
||||
|
||||
const updated = preset.updated_at ? new Date(preset.updated_at).toLocaleString() : '';
|
||||
@@ -94,13 +97,9 @@ export function renderScenePresetsSection(presets) {
|
||||
function _renderDashboardPresetCard(preset) {
|
||||
const borderStyle = `border-left: 3px solid ${escapeHtml(preset.color)}`;
|
||||
const targetCount = (preset.targets || []).length;
|
||||
const deviceCount = (preset.devices || []).length;
|
||||
const automationCount = (preset.automations || []).length;
|
||||
|
||||
const subtitle = [
|
||||
targetCount > 0 ? `${targetCount} ${t('scenes.targets_count')}` : null,
|
||||
deviceCount > 0 ? `${deviceCount} ${t('scenes.devices_count')}` : null,
|
||||
automationCount > 0 ? `${automationCount} ${t('scenes.automations_count')}` : null,
|
||||
].filter(Boolean).join(' \u00b7 ');
|
||||
|
||||
return `<div class="dashboard-target dashboard-scene-preset dashboard-card-link" data-scene-id="${preset.id}" style="${borderStyle}" onclick="if(!event.target.closest('button')){navigateToCard('automations',null,'scenes','data-scene-id','${preset.id}')}">
|
||||
@@ -120,7 +119,7 @@ function _renderDashboardPresetCard(preset) {
|
||||
|
||||
// ===== Capture (create) =====
|
||||
|
||||
export function openScenePresetCapture() {
|
||||
export async function openScenePresetCapture() {
|
||||
_editingId = null;
|
||||
document.getElementById('scene-preset-editor-id').value = '';
|
||||
document.getElementById('scene-preset-editor-name').value = '';
|
||||
@@ -131,6 +130,22 @@ export function openScenePresetCapture() {
|
||||
const titleEl = document.querySelector('#scene-preset-editor-title span[data-i18n]');
|
||||
if (titleEl) { titleEl.setAttribute('data-i18n', 'scenes.add'); titleEl.textContent = t('scenes.add'); }
|
||||
|
||||
// Fetch targets and populate selector
|
||||
const selectorGroup = document.getElementById('scene-target-selector-group');
|
||||
const targetList = document.getElementById('scene-target-list');
|
||||
if (selectorGroup && targetList) {
|
||||
selectorGroup.style.display = '';
|
||||
targetList.innerHTML = '';
|
||||
try {
|
||||
const resp = await fetchWithAuth('/picture-targets');
|
||||
if (resp.ok) {
|
||||
const data = await resp.json();
|
||||
_allTargets = data.targets || [];
|
||||
_refreshTargetSelect();
|
||||
}
|
||||
} catch { /* ignore */ }
|
||||
}
|
||||
|
||||
scenePresetModal.open();
|
||||
scenePresetModal.snapshot();
|
||||
}
|
||||
@@ -148,6 +163,10 @@ export async function editScenePreset(presetId) {
|
||||
document.getElementById('scene-preset-editor-color').value = preset.color || '#4fc3f7';
|
||||
document.getElementById('scene-preset-editor-error').style.display = 'none';
|
||||
|
||||
// Hide target selector in edit mode (metadata only)
|
||||
const selectorGroup = document.getElementById('scene-target-selector-group');
|
||||
if (selectorGroup) selectorGroup.style.display = 'none';
|
||||
|
||||
const titleEl = document.querySelector('#scene-preset-editor-title span[data-i18n]');
|
||||
if (titleEl) { titleEl.setAttribute('data-i18n', 'scenes.edit'); titleEl.textContent = t('scenes.edit'); }
|
||||
|
||||
@@ -177,9 +196,11 @@ export async function saveScenePreset() {
|
||||
body: JSON.stringify({ name, description, color }),
|
||||
});
|
||||
} else {
|
||||
const target_ids = [...document.querySelectorAll('#scene-target-list .scene-target-item')]
|
||||
.map(el => el.dataset.targetId);
|
||||
resp = await fetchWithAuth('/scene-presets', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ name, description, color }),
|
||||
body: JSON.stringify({ name, description, color, target_ids }),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -204,6 +225,49 @@ export async function closeScenePresetEditor() {
|
||||
await scenePresetModal.close();
|
||||
}
|
||||
|
||||
// ===== Target selector helpers =====
|
||||
|
||||
function _refreshTargetSelect() {
|
||||
const select = document.getElementById('scene-target-select');
|
||||
if (!select) return;
|
||||
const added = new Set(
|
||||
[...document.querySelectorAll('#scene-target-list .scene-target-item')]
|
||||
.map(el => el.dataset.targetId)
|
||||
);
|
||||
select.innerHTML = '';
|
||||
for (const tgt of _allTargets) {
|
||||
if (added.has(tgt.id)) continue;
|
||||
const opt = document.createElement('option');
|
||||
opt.value = tgt.id;
|
||||
opt.textContent = tgt.name;
|
||||
select.appendChild(opt);
|
||||
}
|
||||
// Disable add button when no targets available
|
||||
const addBtn = select.parentElement?.querySelector('button');
|
||||
if (addBtn) addBtn.disabled = select.options.length === 0;
|
||||
}
|
||||
|
||||
export function addSceneTarget() {
|
||||
const select = document.getElementById('scene-target-select');
|
||||
const list = document.getElementById('scene-target-list');
|
||||
if (!select || !list || !select.value) return;
|
||||
|
||||
const targetId = select.value;
|
||||
const targetName = select.options[select.selectedIndex].text;
|
||||
|
||||
const item = document.createElement('div');
|
||||
item.className = 'scene-target-item';
|
||||
item.dataset.targetId = targetId;
|
||||
item.innerHTML = `<span>${escapeHtml(targetName)}</span><button type="button" class="btn-remove-condition" onclick="removeSceneTarget(this)" title="Remove">✕</button>`;
|
||||
list.appendChild(item);
|
||||
_refreshTargetSelect();
|
||||
}
|
||||
|
||||
export function removeSceneTarget(btn) {
|
||||
btn.closest('.scene-target-item').remove();
|
||||
_refreshTargetSelect();
|
||||
}
|
||||
|
||||
// ===== Activate =====
|
||||
|
||||
export async function activateScenePreset(presetId) {
|
||||
|
||||
Reference in New Issue
Block a user