feat(audio): move beat routing server-side and extend presets
Route beat-triggered manual selects from the controller server, add preset background and beat-counter UI support, and bump led-driver to include the matching pattern/runtime fixes. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -190,6 +190,14 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
const presetNewColorInput = document.getElementById('preset-new-color');
|
||||
const presetBrightnessInput = document.getElementById('preset-brightness-input');
|
||||
const presetDelayInput = document.getElementById('preset-delay-input');
|
||||
const presetDelayField = presetDelayInput ? presetDelayInput.closest('.preset-editor-field') : null;
|
||||
const presetBackgroundInput = document.getElementById('preset-background-input');
|
||||
const presetBackgroundButton = document.getElementById('preset-background-btn');
|
||||
const presetManualModeInput = document.getElementById('preset-manual-mode-input');
|
||||
const presetManualModeHint = document.getElementById('preset-manual-mode-hint');
|
||||
const presetManualModeLabel = document.getElementById('preset-manual-mode-label');
|
||||
const presetManualBeatNWrap = document.getElementById('preset-manual-beat-n-wrap');
|
||||
const presetManualBeatNInput = document.getElementById('preset-manual-beat-n-input');
|
||||
const presetDefaultButton = document.getElementById('preset-default-btn');
|
||||
const presetRemoveFromTabButton = document.getElementById('preset-remove-from-zone-btn');
|
||||
const presetSaveButton = document.getElementById('preset-save-btn');
|
||||
@@ -219,6 +227,100 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
return Infinity; // No limit if not specified
|
||||
};
|
||||
|
||||
const resolvePatternConfig = (patternName) => {
|
||||
const rawPatternName = String(patternName || '').trim();
|
||||
const normalizedPatternName = rawPatternName.endsWith('.py')
|
||||
? rawPatternName.slice(0, -3)
|
||||
: rawPatternName;
|
||||
let patternConfig =
|
||||
(cachedPatterns && cachedPatterns[rawPatternName]) ||
|
||||
(cachedPatterns && cachedPatterns[normalizedPatternName]) ||
|
||||
null;
|
||||
if (!patternConfig && cachedPatterns && typeof cachedPatterns === 'object') {
|
||||
const lower = normalizedPatternName.toLowerCase();
|
||||
const matchedKey = Object.keys(cachedPatterns).find(
|
||||
(k) => String(k).toLowerCase() === lower,
|
||||
);
|
||||
if (matchedKey) {
|
||||
patternConfig = cachedPatterns[matchedKey];
|
||||
}
|
||||
}
|
||||
if (patternConfig && typeof patternConfig === 'object' && patternConfig.data && typeof patternConfig.data === 'object') {
|
||||
patternConfig = patternConfig.data;
|
||||
}
|
||||
if (
|
||||
patternConfig &&
|
||||
typeof patternConfig === 'object' &&
|
||||
patternConfig.parameter_mappings &&
|
||||
typeof patternConfig.parameter_mappings === 'object'
|
||||
) {
|
||||
patternConfig = patternConfig.parameter_mappings;
|
||||
}
|
||||
return patternConfig && typeof patternConfig === 'object' ? patternConfig : null;
|
||||
};
|
||||
|
||||
/** From db/pattern.json; missing key means pattern allows manual / beat (backward compatible). */
|
||||
const patternSupportsManual = (patternName) => {
|
||||
const cfg = resolvePatternConfig(patternName);
|
||||
if (!cfg) {
|
||||
return true;
|
||||
}
|
||||
return cfg.supports_manual !== false;
|
||||
};
|
||||
|
||||
const updateManualBeatNVisibility = () => {
|
||||
if (!presetManualBeatNWrap) {
|
||||
return;
|
||||
}
|
||||
const manualOn = presetManualModeInput && presetManualModeInput.checked;
|
||||
const patternName = presetPatternInput ? presetPatternInput.value.trim() : '';
|
||||
const ok = !patternName || patternSupportsManual(patternName);
|
||||
presetManualBeatNWrap.style.display = manualOn && ok ? '' : 'none';
|
||||
};
|
||||
|
||||
const updatePresetBackgroundButton = () => {
|
||||
if (!presetBackgroundButton || !presetBackgroundInput) return;
|
||||
const color = coercePresetBackground({ background: presetBackgroundInput.value });
|
||||
presetBackgroundInput.value = color;
|
||||
presetBackgroundButton.textContent = color;
|
||||
presetBackgroundButton.style.backgroundColor = color;
|
||||
presetBackgroundButton.style.color = '#fff';
|
||||
presetBackgroundButton.style.borderColor = 'rgba(255, 255, 255, 0.6)';
|
||||
};
|
||||
|
||||
const updateDelayVisibilityForManualMode = () => {
|
||||
if (!presetDelayField) return;
|
||||
const manualOn = presetManualModeInput && presetManualModeInput.checked;
|
||||
presetDelayField.style.display = manualOn ? 'none' : '';
|
||||
};
|
||||
|
||||
const updateManualModeAvailability = () => {
|
||||
if (!presetManualModeInput) {
|
||||
return;
|
||||
}
|
||||
const patternName = presetPatternInput ? presetPatternInput.value.trim() : '';
|
||||
const ok = !patternName || patternSupportsManual(patternName);
|
||||
presetManualModeInput.disabled = !ok;
|
||||
if (presetManualModeLabel) {
|
||||
presetManualModeLabel.style.opacity = ok ? '' : '0.55';
|
||||
}
|
||||
if (presetManualModeHint) {
|
||||
if (!patternName || ok) {
|
||||
presetManualModeHint.style.display = 'none';
|
||||
presetManualModeHint.textContent = '';
|
||||
} else {
|
||||
presetManualModeHint.style.display = '';
|
||||
presetManualModeHint.textContent =
|
||||
'This pattern is a poor fit for manual mode or audio beat triggers; use auto mode for best results.';
|
||||
}
|
||||
}
|
||||
if (!ok) {
|
||||
presetManualModeInput.checked = false;
|
||||
}
|
||||
updateManualBeatNVisibility();
|
||||
updateDelayVisibilityForManualMode();
|
||||
};
|
||||
|
||||
// Function to show/hide color section based on max_colors
|
||||
const updateColorSectionVisibility = () => {
|
||||
const maxColors = getMaxColors();
|
||||
@@ -255,18 +357,6 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
return Number.isFinite(n) ? n : 0;
|
||||
};
|
||||
|
||||
const patternSupportsBackgroundColor = () => {
|
||||
if (!presetPatternInput || !presetPatternInput.value) {
|
||||
return false;
|
||||
}
|
||||
const pattern = String(presetPatternInput.value).trim();
|
||||
const meta =
|
||||
(cachedPatterns && cachedPatterns[pattern]) ||
|
||||
(cachedPatterns && cachedPatterns[pattern.toLowerCase()]) ||
|
||||
null;
|
||||
return !!(meta && typeof meta === 'object' && meta.has_background === true);
|
||||
};
|
||||
|
||||
const renderPresetColors = (colors, paletteRefs) => {
|
||||
if (!presetColorsContainer) return;
|
||||
|
||||
@@ -311,18 +401,11 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
swatchContainer.style.cssText = 'display: flex; flex-wrap: nowrap; gap: 0.5rem; align-items: flex-start; overflow-x: auto;';
|
||||
swatchContainer.classList.add('color-swatches-container');
|
||||
|
||||
const showBackgroundLabel = patternSupportsBackgroundColor() && currentPresetColors.length > 1;
|
||||
currentPresetColors.forEach((color, index) => {
|
||||
const isBackgroundColor = showBackgroundLabel && index === currentPresetColors.length - 1;
|
||||
const swatchWrapper = document.createElement('div');
|
||||
swatchWrapper.style.cssText = 'position: relative; display: inline-block;';
|
||||
if (isBackgroundColor) {
|
||||
// Keep the background color swatch at the far right.
|
||||
swatchWrapper.style.marginLeft = 'auto';
|
||||
}
|
||||
swatchWrapper.draggable = true;
|
||||
swatchWrapper.dataset.colorIndex = index;
|
||||
swatchWrapper.dataset.backgroundColor = isBackgroundColor ? '1' : '0';
|
||||
const refAtIndex = currentPresetPaletteRefs[index];
|
||||
swatchWrapper.dataset.paletteRef = Number.isInteger(refAtIndex) ? String(refAtIndex) : '';
|
||||
swatchWrapper.classList.add('draggable-color-swatch');
|
||||
@@ -443,18 +526,6 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
swatchWrapper.appendChild(swatch);
|
||||
swatchWrapper.appendChild(colorPicker);
|
||||
swatchWrapper.appendChild(removeBtn);
|
||||
if (isBackgroundColor) {
|
||||
const bgLabel = document.createElement('div');
|
||||
bgLabel.textContent = 'Background';
|
||||
bgLabel.style.cssText = `
|
||||
margin-top: 0.25rem;
|
||||
text-align: center;
|
||||
font-size: 0.72rem;
|
||||
color: #cfcfcf;
|
||||
letter-spacing: 0.02em;
|
||||
`;
|
||||
swatchWrapper.appendChild(bgLabel);
|
||||
}
|
||||
swatchContainer.appendChild(swatchWrapper);
|
||||
});
|
||||
|
||||
@@ -476,10 +547,6 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
e.preventDefault();
|
||||
const dragging = swatchContainer.querySelector('.dragging-color');
|
||||
if (!dragging) return;
|
||||
const backgroundEl = swatchContainer.querySelector('.draggable-color-swatch[data-background-color="1"]');
|
||||
if (backgroundEl) {
|
||||
swatchContainer.appendChild(backgroundEl);
|
||||
}
|
||||
|
||||
// Get new order of colors from DOM
|
||||
const colorElements = [...swatchContainer.querySelectorAll('.draggable-color-swatch')];
|
||||
@@ -503,7 +570,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
|
||||
presetColorsContainer.appendChild(swatchContainer);
|
||||
};
|
||||
|
||||
|
||||
// Function to get drag after element for colors (horizontal layout)
|
||||
const getDragAfterElementForColors = (container, x) => {
|
||||
const draggableElements = [...container.querySelectorAll('.draggable-color-swatch:not(.dragging-color)')];
|
||||
@@ -527,12 +594,27 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
presetNameInput.value = preset.name || '';
|
||||
const patternName = preset.pattern || '';
|
||||
presetPatternInput.value = patternName;
|
||||
const colors = Array.isArray(preset.colors) ? preset.colors : [];
|
||||
const paletteRefs = Array.isArray(preset.palette_refs) ? preset.palette_refs : [];
|
||||
const colors = Array.isArray(preset.colors) ? preset.colors.slice() : [];
|
||||
const paletteRefs = Array.isArray(preset.palette_refs) ? preset.palette_refs.slice() : [];
|
||||
renderPresetColors(colors, paletteRefs);
|
||||
presetBrightnessInput.value = preset.brightness || 0;
|
||||
presetDelayInput.value = preset.delay || 0;
|
||||
|
||||
if (presetBackgroundInput) {
|
||||
presetBackgroundInput.value = coercePresetBackground(preset);
|
||||
}
|
||||
updatePresetBackgroundButton();
|
||||
if (presetManualModeInput) {
|
||||
const autoVal = typeof preset.auto === 'boolean' ? preset.auto : true;
|
||||
presetManualModeInput.checked = !autoVal;
|
||||
}
|
||||
if (presetManualBeatNInput) {
|
||||
const raw = preset.manual_beat_n;
|
||||
let n = typeof raw === 'number' ? raw : parseInt(String(raw != null ? raw : '1'), 10);
|
||||
if (!Number.isFinite(n)) n = 1;
|
||||
n = Math.max(1, Math.min(64, n));
|
||||
presetManualBeatNInput.value = String(n);
|
||||
}
|
||||
|
||||
// Update color section visibility based on pattern
|
||||
updateColorSectionVisibility();
|
||||
|
||||
@@ -587,6 +669,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
|
||||
// After values: show only mapped n params with labels from pattern.json; clear hidden inputs
|
||||
updatePresetNLabels(patternName);
|
||||
updateManualModeAvailability();
|
||||
updatePresetEditorTabActionsVisibility();
|
||||
};
|
||||
|
||||
@@ -609,7 +692,21 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
n6: 0,
|
||||
n7: 0,
|
||||
n8: 0,
|
||||
background: '#000000',
|
||||
auto: true,
|
||||
manual_beat_n: 1,
|
||||
});
|
||||
if (presetManualModeInput) {
|
||||
presetManualModeInput.checked = false;
|
||||
}
|
||||
if (presetManualBeatNInput) {
|
||||
presetManualBeatNInput.value = '1';
|
||||
}
|
||||
if (presetBackgroundInput) {
|
||||
presetBackgroundInput.value = '#000000';
|
||||
}
|
||||
updatePresetBackgroundButton();
|
||||
updateManualModeAvailability();
|
||||
// Re-enable name and pattern when clearing (for new preset)
|
||||
if (presetNameInput) {
|
||||
presetNameInput.disabled = false;
|
||||
@@ -687,6 +784,14 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
// Use canonical field names expected by the device / API
|
||||
brightness: presetBrightnessInput ? parseInt(presetBrightnessInput.value, 10) || 0 : 0,
|
||||
delay: presetDelayInput ? parseInt(presetDelayInput.value, 10) || 0 : 0,
|
||||
background: presetBackgroundInput ? presetBackgroundInput.value : '#000000',
|
||||
auto: presetManualModeInput ? !presetManualModeInput.checked : true,
|
||||
manual_beat_n: (() => {
|
||||
if (!presetManualBeatNInput) return 1;
|
||||
let n = parseInt(presetManualBeatNInput.value, 10);
|
||||
if (!Number.isFinite(n)) n = 1;
|
||||
return Math.max(1, Math.min(64, n));
|
||||
})(),
|
||||
};
|
||||
|
||||
// Always store numeric parameters as n1..n8.
|
||||
@@ -847,6 +952,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
if (nGrid) {
|
||||
nGrid.style.display = visibleNKeys.size > 0 ? '' : 'none';
|
||||
}
|
||||
updateManualModeAvailability();
|
||||
};
|
||||
|
||||
const renderPresets = (presets) => {
|
||||
@@ -1220,6 +1326,21 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
updateColorSectionVisibility();
|
||||
// Re-render colors to show updated max colors limit
|
||||
renderPresetColors(currentPresetColors, currentPresetPaletteRefs);
|
||||
updateManualModeAvailability();
|
||||
});
|
||||
}
|
||||
if (presetManualModeInput) {
|
||||
presetManualModeInput.addEventListener('change', () => {
|
||||
updateManualBeatNVisibility();
|
||||
updateDelayVisibilityForManualMode();
|
||||
});
|
||||
}
|
||||
if (presetBackgroundButton && presetBackgroundInput) {
|
||||
presetBackgroundButton.addEventListener('click', () => {
|
||||
presetBackgroundInput.click();
|
||||
});
|
||||
presetBackgroundInput.addEventListener('input', () => {
|
||||
updatePresetBackgroundButton();
|
||||
});
|
||||
}
|
||||
// Color picker auto-add handler
|
||||
@@ -1452,6 +1573,65 @@ const coercePresetInt = (v, def = 0) => {
|
||||
return Number.isFinite(t) ? t : def;
|
||||
};
|
||||
|
||||
/** Device field ``a`` / API ``auto``; missing → auto-run (matches server build_preset_dict). */
|
||||
const coercePresetAuto = (preset) => {
|
||||
if (!preset || typeof preset !== 'object') {
|
||||
return true;
|
||||
}
|
||||
const v =
|
||||
preset.auto !== undefined && preset.auto !== null ? preset.auto : preset.a;
|
||||
if (typeof v === 'boolean') {
|
||||
return v;
|
||||
}
|
||||
if (v === 0 || v === '0') {
|
||||
return false;
|
||||
}
|
||||
if (v === 1 || v === '1') {
|
||||
return true;
|
||||
}
|
||||
if (typeof v === 'string') {
|
||||
const l = v.trim().toLowerCase();
|
||||
if (['false', '0', 'no', 'off'].includes(l)) {
|
||||
return false;
|
||||
}
|
||||
if (['true', '1', 'yes', 'on'].includes(l)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
/** Preset background colour; accepts #RRGGBB or [r,g,b]. */
|
||||
const coercePresetBackground = (preset) => {
|
||||
if (!preset || typeof preset !== 'object') {
|
||||
return '#000000';
|
||||
}
|
||||
const raw = preset.background !== undefined && preset.background !== null ? preset.background : preset.bg;
|
||||
if (typeof raw === 'string') {
|
||||
const s = raw.trim();
|
||||
if (/^#[0-9a-fA-F]{6}$/.test(s)) {
|
||||
return s.toUpperCase();
|
||||
}
|
||||
}
|
||||
if (Array.isArray(raw) && raw.length === 3) {
|
||||
const r = coercePresetInt(raw[0], 0);
|
||||
const g = coercePresetInt(raw[1], 0);
|
||||
const b = coercePresetInt(raw[2], 0);
|
||||
const clamp = (n) => Math.max(0, Math.min(255, n));
|
||||
return `#${clamp(r).toString(16).padStart(2, '0')}${clamp(g).toString(16).padStart(2, '0')}${clamp(b).toString(16).padStart(2, '0')}`.toUpperCase();
|
||||
}
|
||||
return '#000000';
|
||||
};
|
||||
|
||||
/** Audio beat stride for manual presets (led-controller only; firmware ignores this key). */
|
||||
const coerceManualBeatN = (preset) => {
|
||||
if (!preset || typeof preset !== 'object') return 1;
|
||||
const raw = preset.manual_beat_n;
|
||||
let n = typeof raw === 'number' ? raw : parseInt(String(raw != null ? raw : '1'), 10);
|
||||
if (!Number.isFinite(n)) n = 1;
|
||||
return Math.max(1, Math.min(64, n));
|
||||
};
|
||||
|
||||
// Build driver messages for a single preset; deliver via /presets/push (ESP-NOW + TCP).
|
||||
// Send order:
|
||||
// 1) preset payload (optionally with save)
|
||||
@@ -1473,23 +1653,28 @@ const sendPresetViaEspNow = async (
|
||||
const colors = resolveColorsWithPaletteRefs(baseColors, preset.palette_refs, paletteColors);
|
||||
|
||||
const wirePresetId = devicePresetId != null ? String(devicePresetId) : String(presetId);
|
||||
const presetAuto = coercePresetAuto(preset);
|
||||
const presetBackground = coercePresetBackground(preset);
|
||||
const presetMessage = {
|
||||
v: '1',
|
||||
presets: {
|
||||
[wirePresetId]: {
|
||||
pattern: preset.pattern || 'off',
|
||||
colors,
|
||||
bg: presetBackground,
|
||||
delay: typeof preset.delay === 'number' ? preset.delay : 100,
|
||||
brightness: typeof preset.brightness === 'number'
|
||||
? preset.brightness
|
||||
: (typeof preset.br === 'number' ? preset.br : 127),
|
||||
auto: typeof preset.auto === 'boolean' ? preset.auto : true,
|
||||
auto: presetAuto,
|
||||
a: presetAuto,
|
||||
n1: coercePresetInt(preset.n1),
|
||||
n2: coercePresetInt(preset.n2),
|
||||
n3: coercePresetInt(preset.n3),
|
||||
n4: coercePresetInt(preset.n4),
|
||||
n5: coercePresetInt(preset.n5),
|
||||
n6: coercePresetInt(preset.n6),
|
||||
manual_beat_n: coerceManualBeatN(preset),
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -1555,6 +1740,29 @@ const sendDefaultPreset = async (presetId, deviceNames) => {
|
||||
}
|
||||
};
|
||||
|
||||
const sendPresetSelectViaEspNow = async (presetId, deviceNames) => {
|
||||
if (!presetId) {
|
||||
return;
|
||||
}
|
||||
const nameTargets = Array.isArray(deviceNames)
|
||||
? deviceNames.map((n) => (n || '').trim()).filter((n) => n.length > 0)
|
||||
: [];
|
||||
if (!nameTargets.length) {
|
||||
return;
|
||||
}
|
||||
const select = {};
|
||||
nameTargets.forEach((name) => {
|
||||
select[name] = [String(presetId)];
|
||||
});
|
||||
const macTargets =
|
||||
nameTargets.length > 0 &&
|
||||
typeof window.tabsManager !== 'undefined' &&
|
||||
typeof window.tabsManager.resolveTabDeviceMacs === 'function'
|
||||
? await window.tabsManager.resolveTabDeviceMacs(nameTargets)
|
||||
: [];
|
||||
await postDriverSequence([{ v: '1', select }], macTargets);
|
||||
};
|
||||
|
||||
// Expose for other scripts (zones.js) so they can reuse the shared WebSocket.
|
||||
try {
|
||||
window.sendPresetViaEspNow = sendPresetViaEspNow;
|
||||
@@ -1569,6 +1777,8 @@ try {
|
||||
|
||||
// Store selected preset per zone
|
||||
const selectedPresets = {};
|
||||
// Store selected preset payload per zone for beat-trigger reliability.
|
||||
const selectedPresetPayloads = {};
|
||||
// Run vs Edit for zone preset strip (in-memory only — each full page load starts in run mode)
|
||||
let presetUiMode = 'run';
|
||||
|
||||
@@ -1858,6 +2068,7 @@ const createPresetButton = (presetId, preset, zoneId, isSelected = false) => {
|
||||
button.className = 'pattern-button preset-tile-main';
|
||||
if (isSelected) {
|
||||
button.classList.add('active');
|
||||
selectedPresetPayloads[zoneId] = preset;
|
||||
}
|
||||
|
||||
const colors = Array.isArray(preset.colors) ? preset.colors.filter((c) => c) : [];
|
||||
@@ -1881,6 +2092,49 @@ const createPresetButton = (presetId, preset, zoneId, isSelected = false) => {
|
||||
presetNameLabel.className = 'pattern-button-label';
|
||||
button.appendChild(presetNameLabel);
|
||||
|
||||
const bgSwatch = document.createElement('span');
|
||||
const bgColor = coercePresetBackground(preset);
|
||||
bgSwatch.title = `Background: ${bgColor}`;
|
||||
bgSwatch.style.cssText = `
|
||||
position: absolute;
|
||||
left: 4px;
|
||||
bottom: 4px;
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border-radius: 2px;
|
||||
background: ${bgColor};
|
||||
border: 1px solid rgba(255, 255, 255, 0.7);
|
||||
box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.5);
|
||||
pointer-events: none;
|
||||
z-index: 2;
|
||||
`;
|
||||
button.appendChild(bgSwatch);
|
||||
|
||||
const isManualPreset = preset && typeof preset.auto === 'boolean' ? !preset.auto : false;
|
||||
if (isManualPreset) {
|
||||
const manualBadge = document.createElement('span');
|
||||
manualBadge.textContent = '1';
|
||||
manualBadge.title = 'Manual preset';
|
||||
manualBadge.style.cssText = `
|
||||
position: absolute;
|
||||
right: 4px;
|
||||
bottom: 4px;
|
||||
min-width: 16px;
|
||||
height: 16px;
|
||||
border-radius: 8px;
|
||||
background: rgba(0, 0, 0, 0.72);
|
||||
color: #fff;
|
||||
border: 1px solid rgba(255, 255, 255, 0.5);
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
line-height: 14px;
|
||||
text-align: center;
|
||||
pointer-events: none;
|
||||
z-index: 2;
|
||||
`;
|
||||
button.appendChild(manualBadge);
|
||||
}
|
||||
|
||||
button.addEventListener('click', () => {
|
||||
if (isDraggingPreset) return;
|
||||
const presetsListEl = document.getElementById('presets-list-zone');
|
||||
@@ -1889,6 +2143,7 @@ const createPresetButton = (presetId, preset, zoneId, isSelected = false) => {
|
||||
}
|
||||
button.classList.add('active');
|
||||
selectedPresets[zoneId] = presetId;
|
||||
selectedPresetPayloads[zoneId] = preset;
|
||||
const section = row.closest('.presets-section');
|
||||
const deviceNames = tabDeviceNamesFromSection(section);
|
||||
sendPresetViaEspNow(presetId, preset, deviceNames, false, false, '2').catch((err) => {
|
||||
|
||||
Reference in New Issue
Block a user