From b0e93d613c0c2951d62c9a9c7f31d89d4018f485 Mon Sep 17 00:00:00 2001 From: Peter Siegmund Date: Thu, 1 Jan 2026 17:31:41 +0100 Subject: [PATCH] add segment settings Signed-off-by: Peter Siegmund --- firmware/README-API.md | 84 ++++++++++++++ firmware/storage/www/css/index.css | 132 ++++++++++++++++++++++ firmware/storage/www/index.html | 37 ++++++- firmware/storage/www/js/i18n.js | 40 ++++++- firmware/storage/www/js/wled.js | 172 +++++++++++++++++++++++++++++ 5 files changed, 461 insertions(+), 4 deletions(-) create mode 100644 firmware/storage/www/js/wled.js diff --git a/firmware/README-API.md b/firmware/README-API.md index 359ca4b..c77cdd1 100644 --- a/firmware/README-API.md +++ b/firmware/README-API.md @@ -7,6 +7,7 @@ This document describes all REST API endpoints and WebSocket messages required f - [REST API Endpoints](#rest-api-endpoints) - [WiFi](#wifi) - [Light Control](#light-control) + - [WLED Configuration](#wled-configuration) - [Schema](#schema) - [Devices](#devices) - [Scenes](#scenes) @@ -201,6 +202,89 @@ Returns current light status (alternative to WebSocket). --- +### WLED Configuration + +#### Get WLED Configuration + +Returns the current WLED configuration including host and all segments. + +- **URL:** `/api/wled/config` +- **Method:** `GET` +- **Response:** + +```json +{ + "host": "192.168.1.100", + "segments": [ + { + "name": "Main Light", + "start": 0, + "leds": 60 + }, + { + "name": "Accent Light", + "start": 60, + "leds": 30 + } + ] +} +``` + +| Field | Type | Description | +|--------------------|--------|------------------------------------------| +| host | string | WLED host address (IP or hostname) | +| segments | array | List of LED segments | +| segments[].name | string | Optional segment name | +| segments[].start | number | Start LED index (0-based) | +| segments[].leds | number | Number of LEDs in this segment | + +--- + +#### Save WLED Configuration + +Saves the WLED configuration. + +- **URL:** `/api/wled/config` +- **Method:** `POST` +- **Content-Type:** `application/json` +- **Request Body:** + +```json +{ + "host": "192.168.1.100", + "segments": [ + { + "name": "Main Light", + "start": 0, + "leds": 60 + }, + { + "name": "Accent Light", + "start": 60, + "leds": 30 + } + ] +} +``` + +| Field | Type | Required | Description | +|--------------------|--------|----------|------------------------------------------| +| host | string | Yes | WLED host address (IP or hostname) | +| segments | array | Yes | List of LED segments (can be empty) | +| segments[].name | string | No | Optional segment name | +| segments[].start | number | Yes | Start LED index (0-based) | +| segments[].leds | number | Yes | Number of LEDs in this segment | + +- **Response:** `200 OK` on success, `400 Bad Request` on validation error + +**Notes:** +- The firmware uses this configuration to communicate with a WLED controller +- Segments are mapped to the WLED JSON API segment control +- Changes are persisted to NVS (non-volatile storage) +- The host can be an IP address (e.g., `192.168.1.100`) or hostname (e.g., `wled.local`) + +--- + ### Schema #### Load Schema diff --git a/firmware/storage/www/css/index.css b/firmware/storage/www/css/index.css index 7b38b94..2cc17cd 100644 --- a/firmware/storage/www/css/index.css +++ b/firmware/storage/www/css/index.css @@ -1198,4 +1198,136 @@ body { .scene-action-row select { width: 100%; } +} + +/* WLED Configuration */ +.segment-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 12px; +} + +.segment-header h3 { + margin: 0; + font-size: 1rem; + color: var(--text); +} + +.btn-small { + padding: 6px 12px; + font-size: 0.85rem; +} + +.wled-segments-list { + display: flex; + flex-direction: column; + gap: 12px; + margin-bottom: 20px; +} + +.wled-segment-item { + display: grid; + grid-template-columns: 1fr auto auto auto; + gap: 12px; + align-items: flex-end; + padding: 16px; + background: var(--bg-color); + border-radius: 10px; + border: 1px solid var(--border); +} + +.segment-name-field { + display: flex; + flex-direction: column; + gap: 4px; +} + +.segment-number { + font-weight: 600; + font-size: 0.75rem; + color: var(--primary); +} + +.segment-name-input { + padding: 8px 12px; + border: 1px solid var(--border); + border-radius: 6px; + background: var(--card-bg); + color: var(--text); + font-size: 0.9rem; + height: 38px; + box-sizing: border-box; +} + +.segment-field { + display: flex; + flex-direction: column; + gap: 4px; +} + +.segment-field label { + font-size: 0.75rem; + color: var(--text-secondary); + white-space: nowrap; +} + +.segment-field input { + padding: 8px 10px; + border: 1px solid var(--border); + border-radius: 6px; + background: var(--card-bg); + color: var(--text); + font-size: 0.9rem; + text-align: center; + width: 70px; + height: 38px; + box-sizing: border-box; +} + +.segment-remove-btn { + padding: 8px; + background: transparent; + border: 1px solid var(--border); + border-radius: 6px; + cursor: pointer; + color: var(--text-secondary); + transition: all 0.2s; + height: 38px; + width: 38px; + box-sizing: border-box; + align-self: flex-end; +} + +.segment-remove-btn:hover { + background: #ff4444; + border-color: #ff4444; + color: white; +} + +/* Responsive for WLED */ +@media (max-width: 600px) { + .segment-header { + flex-direction: column; + align-items: flex-start; + gap: 10px; + } + + .wled-segment-item { + grid-template-columns: 1fr 1fr; + grid-template-rows: auto auto auto; + } + + .segment-number { + grid-column: 1 / -1; + } + + .segment-name-input { + grid-column: 1 / -1; + } + + .segment-remove-btn { + grid-column: 1 / -1; + justify-self: end; + } } \ No newline at end of file diff --git a/firmware/storage/www/index.html b/firmware/storage/www/index.html index 53f2a96..0383a43 100644 --- a/firmware/storage/www/index.html +++ b/firmware/storage/www/index.html @@ -135,7 +135,8 @@
- +
@@ -185,8 +186,39 @@
- +
+ +
+

WLED Konfiguration

+

Konfiguriere die WLED-Segmente und LEDs pro + Segment

+ +
+

Segmente

+ +
+ +
+ +
+ 💡 +

Keine Segmente konfiguriert

+

Klicke auf "Segment hinzufügen" + um ein Segment zu erstellen

+
+
+ +
+ +
+ +
+
+ +

Licht-Schema Editor

@@ -423,6 +455,7 @@ + diff --git a/firmware/storage/www/js/i18n.js b/firmware/storage/www/js/i18n.js index d163f28..0fdc3de 100644 --- a/firmware/storage/www/js/i18n.js +++ b/firmware/storage/www/js/i18n.js @@ -12,10 +12,28 @@ const translations = { // Sub Tabs 'subtab.wifi': '📶 WLAN', - 'subtab.schema': '💡 Schema', + 'subtab.light': '💡 Lichtsteuerung', 'subtab.devices': '🔗 Geräte', 'subtab.scenes': '🎬 Szenen', + // WLED Configuration + 'wled.config.title': 'WLED Konfiguration', + 'wled.config.desc': 'Konfiguriere die WLED-Segmente und LEDs pro Segment', + 'wled.host': 'WLED Host', + 'wled.host.placeholder': 'z.B. 192.168.1.100 oder wled.local', + 'wled.segments.title': 'Segmente', + 'wled.segments.empty': 'Keine Segmente konfiguriert', + 'wled.segments.empty.hint': 'Klicke auf "Segment hinzufügen" um ein Segment zu erstellen', + 'wled.segment.add': '➕ Segment hinzufügen', + 'wled.segment.name': 'Segment {num}', + 'wled.segment.leds': 'Anzahl LEDs', + 'wled.segment.start': 'Start-LED', + 'wled.segment.remove': 'Entfernen', + 'wled.saved': 'WLED-Konfiguration gespeichert!', + 'wled.error.host': 'Bitte WLED Host eingeben', + 'wled.error.save': 'Fehler beim Speichern der WLED-Konfiguration', + 'wled.loaded': 'WLED-Konfiguration geladen', + // Light Control 'control.light.title': 'Lichtsteuerung', 'control.light.onoff': 'Ein/Aus', @@ -168,10 +186,28 @@ const translations = { // Sub Tabs 'subtab.wifi': '📶 WiFi', - 'subtab.schema': '💡 Schema', + 'subtab.light': '💡 Light Control', 'subtab.devices': '🔗 Devices', 'subtab.scenes': '🎬 Scenes', + // WLED Configuration + 'wled.config.title': 'WLED Configuration', + 'wled.config.desc': 'Configure WLED segments and LEDs per segment', + 'wled.host': 'WLED Host', + 'wled.host.placeholder': 'e.g. 192.168.1.100 or wled.local', + 'wled.segments.title': 'Segments', + 'wled.segments.empty': 'No segments configured', + 'wled.segments.empty.hint': 'Click "Add Segment" to create a segment', + 'wled.segment.add': '➕ Add Segment', + 'wled.segment.name': 'Segment {num}', + 'wled.segment.leds': 'Number of LEDs', + 'wled.segment.start': 'Start LED', + 'wled.segment.remove': 'Remove', + 'wled.saved': 'WLED configuration saved!', + 'wled.error.host': 'Please enter WLED host', + 'wled.error.save': 'Error saving WLED configuration', + 'wled.loaded': 'WLED configuration loaded', + // Light Control 'control.light.title': 'Light Control', 'control.light.onoff': 'On/Off', diff --git a/firmware/storage/www/js/wled.js b/firmware/storage/www/js/wled.js new file mode 100644 index 0000000..737c98f --- /dev/null +++ b/firmware/storage/www/js/wled.js @@ -0,0 +1,172 @@ +// WLED Configuration Module +// Manages WLED segments and LED configuration + +let wledConfig = { + segments: [] +}; + +/** + * Initialize WLED module + */ +function initWled() { + loadWledConfig(); +} + +/** + * Load WLED configuration from server + */ +async function loadWledConfig() { + try { + const response = await fetch('/api/wled/config'); + if (response.ok) { + wledConfig = await response.json(); + renderWledSegments(); + showStatus('wled-status', t('wled.loaded'), 'success'); + } + } catch (error) { + console.log('Using default WLED config'); + wledConfig = { segments: [] }; + renderWledSegments(); + } +} + +/** + * Render WLED segments list + */ +function renderWledSegments() { + const list = document.getElementById('wled-segments-list'); + const emptyState = document.getElementById('no-wled-segments'); + + if (!list) return; + + // Clear existing segments (keep empty state) + const existingItems = list.querySelectorAll('.wled-segment-item'); + existingItems.forEach(item => item.remove()); + + if (wledConfig.segments.length === 0) { + if (emptyState) emptyState.style.display = 'block'; + return; + } + + if (emptyState) emptyState.style.display = 'none'; + + wledConfig.segments.forEach((segment, index) => { + const item = createSegmentElement(segment, index); + list.insertBefore(item, emptyState); + }); +} + +/** + * Create segment DOM element + * @param {object} segment - Segment data + * @param {number} index - Segment index + * @returns {HTMLElement} Segment element + */ +function createSegmentElement(segment, index) { + const item = document.createElement('div'); + item.className = 'wled-segment-item'; + item.dataset.index = index; + + item.innerHTML = ` +
+ + +
+
+ + +
+
+ + +
+ + `; + + return item; +} + +/** + * Add a new WLED segment + */ +function addWledSegment() { + // Calculate next start position + let nextStart = 0; + if (wledConfig.segments.length > 0) { + const lastSegment = wledConfig.segments[wledConfig.segments.length - 1]; + nextStart = (lastSegment.start || 0) + (lastSegment.leds || 0); + } + + wledConfig.segments.push({ + name: '', + start: nextStart, + leds: 10 + }); + + renderWledSegments(); +} + +/** + * Update a segment property + * @param {number} index - Segment index + * @param {string} property - Property name + * @param {*} value - New value + */ +function updateSegment(index, property, value) { + if (index >= 0 && index < wledConfig.segments.length) { + wledConfig.segments[index][property] = value; + } +} + +/** + * Remove a WLED segment + * @param {number} index - Segment index to remove + */ +function removeWledSegment(index) { + if (index >= 0 && index < wledConfig.segments.length) { + wledConfig.segments.splice(index, 1); + renderWledSegments(); + } +} + +/** + * Save WLED configuration to server + */ +async function saveWledConfig() { + try { + const response = await fetch('/api/wled/config', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(wledConfig) + }); + + if (response.ok) { + showStatus('wled-status', t('wled.saved'), 'success'); + } else { + throw new Error('Save failed'); + } + } catch (error) { + console.error('Error saving WLED config:', error); + showStatus('wled-status', t('wled.error.save'), 'error'); + } +} + +/** + * Helper function to escape HTML + * @param {string} text - Text to escape + * @returns {string} Escaped text + */ +function escapeHtml(text) { + const div = document.createElement('div'); + div.textContent = text; + return div.innerHTML; +} + +// Initialize when DOM is ready +document.addEventListener('DOMContentLoaded', initWled);