Add profile conditions, scene presets, MQTT integration, and Scenes tab
Feature 1 — Profile Conditions: time-of-day, system idle (Win32 GetLastInputInfo), and display state (GUID_CONSOLE_DISPLAY_STATE) condition types for automatic profile activation. Feature 2 — Scene Presets: snapshot/restore system that captures target running states, device brightness, and profile enables. Server-side capture with 5-step activation order. Dedicated Scenes tab with CardSection-based card grid, command palette integration, and dashboard quick-activate section. Feature 3 — MQTT Integration: MQTTService singleton with aiomqtt, MQTTLEDClient device provider for pixel output, MQTT profile condition type with topic/payload matching, and frontend support for MQTT device type and condition editor. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -9,7 +9,7 @@ import { showToast, showConfirm, setTabRefreshing } from '../core/ui.js';
|
||||
import { Modal } from '../core/modal.js';
|
||||
import { CardSection } from '../core/card-sections.js';
|
||||
import { updateTabBadge } from './tabs.js';
|
||||
import { ICON_SETTINGS, ICON_STOP_PLAIN, ICON_START, ICON_PAUSE, ICON_CLOCK, ICON_TARGET, ICON_PROFILE, ICON_HELP, ICON_OK } from '../core/icons.js';
|
||||
import { ICON_SETTINGS, ICON_STOP_PLAIN, ICON_START, ICON_PAUSE, ICON_CLOCK, ICON_TARGET, ICON_PROFILE, ICON_HELP, ICON_OK, ICON_TIMER, ICON_MONITOR, ICON_RADIO } from '../core/icons.js';
|
||||
|
||||
class ProfileEditorModal extends Modal {
|
||||
constructor() { super('profile-editor-modal'); }
|
||||
@@ -116,6 +116,20 @@ function createProfileCard(profile, runningTargetIds = new Set()) {
|
||||
const matchLabel = t('profiles.condition.application.match_type.' + (c.match_type || 'running'));
|
||||
return `<span class="stream-card-prop stream-card-prop-full">${t('profiles.condition.application')}: ${apps} (${matchLabel})</span>`;
|
||||
}
|
||||
if (c.condition_type === 'time_of_day') {
|
||||
return `<span class="stream-card-prop">${ICON_CLOCK} ${c.start_time || '00:00'} – ${c.end_time || '23:59'}</span>`;
|
||||
}
|
||||
if (c.condition_type === 'system_idle') {
|
||||
const mode = c.when_idle !== false ? t('profiles.condition.system_idle.when_idle') : t('profiles.condition.system_idle.when_active');
|
||||
return `<span class="stream-card-prop">${ICON_TIMER} ${c.idle_minutes || 5}m (${mode})</span>`;
|
||||
}
|
||||
if (c.condition_type === 'display_state') {
|
||||
const stateLabel = t('profiles.condition.display_state.' + (c.state || 'on'));
|
||||
return `<span class="stream-card-prop">${ICON_MONITOR} ${t('profiles.condition.display_state')}: ${stateLabel}</span>`;
|
||||
}
|
||||
if (c.condition_type === 'mqtt') {
|
||||
return `<span class="stream-card-prop stream-card-prop-full">${ICON_RADIO} ${t('profiles.condition.mqtt')}: ${escapeHtml(c.topic || '')} = ${escapeHtml(c.payload || '*')}</span>`;
|
||||
}
|
||||
return `<span class="stream-card-prop">${c.condition_type}</span>`;
|
||||
});
|
||||
const logicLabel = profile.condition_logic === 'and' ? t('profiles.logic.and') : t('profiles.logic.or');
|
||||
@@ -259,6 +273,10 @@ function addProfileConditionRow(condition) {
|
||||
<select class="condition-type-select">
|
||||
<option value="always" ${condType === 'always' ? 'selected' : ''}>${t('profiles.condition.always')}</option>
|
||||
<option value="application" ${condType === 'application' ? 'selected' : ''}>${t('profiles.condition.application')}</option>
|
||||
<option value="time_of_day" ${condType === 'time_of_day' ? 'selected' : ''}>${t('profiles.condition.time_of_day')}</option>
|
||||
<option value="system_idle" ${condType === 'system_idle' ? 'selected' : ''}>${t('profiles.condition.system_idle')}</option>
|
||||
<option value="display_state" ${condType === 'display_state' ? 'selected' : ''}>${t('profiles.condition.display_state')}</option>
|
||||
<option value="mqtt" ${condType === 'mqtt' ? 'selected' : ''}>${t('profiles.condition.mqtt')}</option>
|
||||
</select>
|
||||
<button type="button" class="btn-remove-condition" onclick="this.closest('.profile-condition-row').remove()" title="Remove">✕</button>
|
||||
</div>
|
||||
@@ -273,6 +291,81 @@ function addProfileConditionRow(condition) {
|
||||
container.innerHTML = `<small class="condition-always-desc">${t('profiles.condition.always.hint')}</small>`;
|
||||
return;
|
||||
}
|
||||
if (type === 'time_of_day') {
|
||||
const startTime = data.start_time || '00:00';
|
||||
const endTime = data.end_time || '23:59';
|
||||
container.innerHTML = `
|
||||
<div class="condition-fields">
|
||||
<div class="condition-field">
|
||||
<label>${t('profiles.condition.time_of_day.start_time')}</label>
|
||||
<input type="time" class="condition-start-time" value="${startTime}">
|
||||
</div>
|
||||
<div class="condition-field">
|
||||
<label>${t('profiles.condition.time_of_day.end_time')}</label>
|
||||
<input type="time" class="condition-end-time" value="${endTime}">
|
||||
</div>
|
||||
<small class="condition-always-desc">${t('profiles.condition.time_of_day.overnight_hint')}</small>
|
||||
</div>`;
|
||||
return;
|
||||
}
|
||||
if (type === 'system_idle') {
|
||||
const idleMinutes = data.idle_minutes ?? 5;
|
||||
const whenIdle = data.when_idle ?? true;
|
||||
container.innerHTML = `
|
||||
<div class="condition-fields">
|
||||
<div class="condition-field">
|
||||
<label>${t('profiles.condition.system_idle.idle_minutes')}</label>
|
||||
<input type="number" class="condition-idle-minutes" min="1" max="999" value="${idleMinutes}">
|
||||
</div>
|
||||
<div class="condition-field">
|
||||
<label>${t('profiles.condition.system_idle.mode')}</label>
|
||||
<select class="condition-when-idle">
|
||||
<option value="true" ${whenIdle ? 'selected' : ''}>${t('profiles.condition.system_idle.when_idle')}</option>
|
||||
<option value="false" ${!whenIdle ? 'selected' : ''}>${t('profiles.condition.system_idle.when_active')}</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>`;
|
||||
return;
|
||||
}
|
||||
if (type === 'display_state') {
|
||||
const dState = data.state || 'on';
|
||||
container.innerHTML = `
|
||||
<div class="condition-fields">
|
||||
<div class="condition-field">
|
||||
<label>${t('profiles.condition.display_state.state')}</label>
|
||||
<select class="condition-display-state">
|
||||
<option value="on" ${dState === 'on' ? 'selected' : ''}>${t('profiles.condition.display_state.on')}</option>
|
||||
<option value="off" ${dState === 'off' ? 'selected' : ''}>${t('profiles.condition.display_state.off')}</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>`;
|
||||
return;
|
||||
}
|
||||
if (type === 'mqtt') {
|
||||
const topic = data.topic || '';
|
||||
const payload = data.payload || '';
|
||||
const matchMode = data.match_mode || 'exact';
|
||||
container.innerHTML = `
|
||||
<div class="condition-fields">
|
||||
<div class="condition-field">
|
||||
<label>${t('profiles.condition.mqtt.topic')}</label>
|
||||
<input type="text" class="condition-mqtt-topic" value="${escapeHtml(topic)}" placeholder="home/status/power">
|
||||
</div>
|
||||
<div class="condition-field">
|
||||
<label>${t('profiles.condition.mqtt.payload')}</label>
|
||||
<input type="text" class="condition-mqtt-payload" value="${escapeHtml(payload)}" placeholder="ON">
|
||||
</div>
|
||||
<div class="condition-field">
|
||||
<label>${t('profiles.condition.mqtt.match_mode')}</label>
|
||||
<select class="condition-mqtt-match-mode">
|
||||
<option value="exact" ${matchMode === 'exact' ? 'selected' : ''}>${t('profiles.condition.mqtt.match_mode.exact')}</option>
|
||||
<option value="contains" ${matchMode === 'contains' ? 'selected' : ''}>${t('profiles.condition.mqtt.match_mode.contains')}</option>
|
||||
<option value="regex" ${matchMode === 'regex' ? 'selected' : ''}>${t('profiles.condition.mqtt.match_mode.regex')}</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>`;
|
||||
return;
|
||||
}
|
||||
const appsValue = (data.apps || []).join('\n');
|
||||
const matchType = data.match_type || 'running';
|
||||
container.innerHTML = `
|
||||
@@ -308,7 +401,7 @@ function addProfileConditionRow(condition) {
|
||||
|
||||
renderFields(condType, condition);
|
||||
typeSelect.addEventListener('change', () => {
|
||||
renderFields(typeSelect.value, { apps: [], match_type: 'running' });
|
||||
renderFields(typeSelect.value, {});
|
||||
});
|
||||
|
||||
list.appendChild(row);
|
||||
@@ -382,6 +475,30 @@ function getProfileEditorConditions() {
|
||||
const condType = typeSelect ? typeSelect.value : 'application';
|
||||
if (condType === 'always') {
|
||||
conditions.push({ condition_type: 'always' });
|
||||
} else if (condType === 'time_of_day') {
|
||||
conditions.push({
|
||||
condition_type: 'time_of_day',
|
||||
start_time: row.querySelector('.condition-start-time').value || '00:00',
|
||||
end_time: row.querySelector('.condition-end-time').value || '23:59',
|
||||
});
|
||||
} else if (condType === 'system_idle') {
|
||||
conditions.push({
|
||||
condition_type: 'system_idle',
|
||||
idle_minutes: parseInt(row.querySelector('.condition-idle-minutes').value, 10) || 5,
|
||||
when_idle: row.querySelector('.condition-when-idle').value === 'true',
|
||||
});
|
||||
} else if (condType === 'display_state') {
|
||||
conditions.push({
|
||||
condition_type: 'display_state',
|
||||
state: row.querySelector('.condition-display-state').value || 'on',
|
||||
});
|
||||
} else if (condType === 'mqtt') {
|
||||
conditions.push({
|
||||
condition_type: 'mqtt',
|
||||
topic: row.querySelector('.condition-mqtt-topic').value.trim(),
|
||||
payload: row.querySelector('.condition-mqtt-payload').value,
|
||||
match_mode: row.querySelector('.condition-mqtt-match-mode').value || 'exact',
|
||||
});
|
||||
} else {
|
||||
const matchType = row.querySelector('.condition-match-type').value;
|
||||
const appsText = row.querySelector('.condition-apps').value.trim();
|
||||
|
||||
Reference in New Issue
Block a user