add segment settings
Signed-off-by: Peter Siegmund <developer@mars3142.org>
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -135,7 +135,8 @@
|
||||
<div id="tab-config" class="tab-content">
|
||||
<div class="sub-tabs">
|
||||
<button class="sub-tab active" onclick="switchSubTab('wifi')" data-i18n="subtab.wifi">📶 WLAN</button>
|
||||
<button class="sub-tab" onclick="switchSubTab('schema')" data-i18n="subtab.schema">💡 Schema</button>
|
||||
<button class="sub-tab" onclick="switchSubTab('schema')" data-i18n="subtab.light">💡
|
||||
Lichtsteuerung</button>
|
||||
<button class="sub-tab" onclick="switchSubTab('devices')" data-i18n="subtab.devices">🔗 Geräte</button>
|
||||
<button class="sub-tab" onclick="switchSubTab('scenes')" data-i18n="subtab.scenes">🎬 Szenen</button>
|
||||
</div>
|
||||
@@ -185,8 +186,39 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Schema Sub-Tab -->
|
||||
<!-- Schema Sub-Tab (Lichtsteuerung) -->
|
||||
<div id="subtab-schema" class="sub-tab-content">
|
||||
<!-- WLED Konfiguration -->
|
||||
<div class="card">
|
||||
<h2 data-i18n="wled.config.title">WLED Konfiguration</h2>
|
||||
<p class="card-description" data-i18n="wled.config.desc">Konfiguriere die WLED-Segmente und LEDs pro
|
||||
Segment</p>
|
||||
|
||||
<div class="segment-header">
|
||||
<h3 data-i18n="wled.segments.title">Segmente</h3>
|
||||
<button class="btn btn-secondary btn-small" onclick="addWledSegment()"
|
||||
data-i18n="wled.segment.add">➕ Segment hinzufügen</button>
|
||||
</div>
|
||||
|
||||
<div id="wled-segments-list" class="wled-segments-list">
|
||||
<!-- Wird dynamisch gefüllt -->
|
||||
<div class="empty-state" id="no-wled-segments">
|
||||
<span class="empty-icon">💡</span>
|
||||
<p data-i18n="wled.segments.empty">Keine Segmente konfiguriert</p>
|
||||
<p class="empty-hint" data-i18n="wled.segments.empty.hint">Klicke auf "Segment hinzufügen"
|
||||
um ein Segment zu erstellen</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="btn-group">
|
||||
<button class="btn btn-primary" onclick="saveWledConfig()" data-i18n="btn.save">💾
|
||||
Speichern</button>
|
||||
</div>
|
||||
|
||||
<div id="wled-status" class="status"></div>
|
||||
</div>
|
||||
|
||||
<!-- Schema Editor -->
|
||||
<div class="card">
|
||||
<h2 data-i18n="schema.editor.title">Licht-Schema Editor</h2>
|
||||
|
||||
@@ -423,6 +455,7 @@
|
||||
<script src="js/scenes.js"></script>
|
||||
<script src="js/devices.js"></script>
|
||||
<script src="js/schema.js"></script>
|
||||
<script src="js/wled.js"></script>
|
||||
<script src="js/app.js"></script>
|
||||
</body>
|
||||
|
||||
|
||||
@@ -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',
|
||||
|
||||
172
firmware/storage/www/js/wled.js
Normal file
172
firmware/storage/www/js/wled.js
Normal file
@@ -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 = `
|
||||
<div class="segment-name-field">
|
||||
<label class="segment-number">${t('wled.segment.name', { num: index + 1 })}</label>
|
||||
<input type="text" class="segment-name-input" value="${escapeHtml(segment.name || '')}"
|
||||
placeholder="${t('wled.segment.name', { num: index + 1 })}"
|
||||
onchange="updateSegment(${index}, 'name', this.value)">
|
||||
</div>
|
||||
<div class="segment-field">
|
||||
<label data-i18n="wled.segment.start">${t('wled.segment.start')}</label>
|
||||
<input type="number" min="0" value="${segment.start || 0}"
|
||||
onchange="updateSegment(${index}, 'start', parseInt(this.value))">
|
||||
</div>
|
||||
<div class="segment-field">
|
||||
<label data-i18n="wled.segment.leds">${t('wled.segment.leds')}</label>
|
||||
<input type="number" min="1" value="${segment.leds || 1}"
|
||||
onchange="updateSegment(${index}, 'leds', parseInt(this.value))">
|
||||
</div>
|
||||
<button class="segment-remove-btn" onclick="removeWledSegment(${index})" title="${t('wled.segment.remove')}">
|
||||
🗑️
|
||||
</button>
|
||||
`;
|
||||
|
||||
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);
|
||||
Reference in New Issue
Block a user