Compare commits
4 Commits
52f6c2acab
...
29785a96bc
| Author | SHA1 | Date | |
|---|---|---|---|
|
29785a96bc
|
|||
|
ee587f1381
|
|||
|
a66c48e713
|
|||
|
b0e93d613c
|
@@ -5,8 +5,10 @@ This document describes all REST API endpoints and WebSocket messages required f
|
||||
## Table of Contents
|
||||
|
||||
- [REST API Endpoints](#rest-api-endpoints)
|
||||
- [Capabilities](#capabilities)
|
||||
- [WiFi](#wifi)
|
||||
- [Light Control](#light-control)
|
||||
- [LED Configuration](#led-configuration)
|
||||
- [Schema](#schema)
|
||||
- [Devices](#devices)
|
||||
- [Scenes](#scenes)
|
||||
@@ -19,6 +21,33 @@ This document describes all REST API endpoints and WebSocket messages required f
|
||||
|
||||
## REST API Endpoints
|
||||
|
||||
### Capabilities
|
||||
|
||||
#### Get Device Capabilities
|
||||
|
||||
Returns the device capabilities. Used to determine which features are available.
|
||||
|
||||
- **URL:** `/api/capabilities`
|
||||
- **Method:** `GET`
|
||||
- **Response:**
|
||||
|
||||
```json
|
||||
{
|
||||
"thread": true
|
||||
}
|
||||
```
|
||||
|
||||
| Field | Type | Description |
|
||||
|--------|---------|-------------------------------------------------------|
|
||||
| thread | boolean | Whether Thread/Matter features are enabled |
|
||||
|
||||
**Notes:**
|
||||
- If `thread` is `true`, the UI shows Matter device management and Scenes
|
||||
- If `thread` is `false` or the endpoint is unavailable, these features are hidden
|
||||
- The client can also force-enable features via URL parameter `?thread=true`
|
||||
|
||||
---
|
||||
|
||||
### WiFi
|
||||
|
||||
#### Scan Networks
|
||||
@@ -125,6 +154,34 @@ Turns the main light on or off.
|
||||
|
||||
---
|
||||
|
||||
#### Set Thunder Effect
|
||||
|
||||
Turns the thunder/lightning effect on or off.
|
||||
|
||||
- **URL:** `/api/light/thunder`
|
||||
- **Method:** `POST`
|
||||
- **Content-Type:** `application/json`
|
||||
- **Request Body:**
|
||||
|
||||
```json
|
||||
{
|
||||
"on": true
|
||||
}
|
||||
```
|
||||
|
||||
| Field | Type | Required | Description |
|
||||
|-------|---------|----------|--------------------------------|
|
||||
| on | boolean | Yes | `true` = on, `false` = off |
|
||||
|
||||
- **Response:** `200 OK` on success
|
||||
|
||||
**Notes:**
|
||||
- When enabled, random lightning flashes are triggered
|
||||
- Can be combined with any light mode
|
||||
- Thunder effect stops automatically when light is turned off
|
||||
|
||||
---
|
||||
|
||||
#### Set Light Mode
|
||||
|
||||
Sets the lighting mode.
|
||||
@@ -182,6 +239,7 @@ Returns current light status (alternative to WebSocket).
|
||||
```json
|
||||
{
|
||||
"on": true,
|
||||
"thunder": false,
|
||||
"mode": "simulation",
|
||||
"schema": "schema_01.csv",
|
||||
"color": {
|
||||
@@ -192,12 +250,91 @@ Returns current light status (alternative to WebSocket).
|
||||
}
|
||||
```
|
||||
|
||||
| Field | Type | Description |
|
||||
|--------|---------|--------------------------------------|
|
||||
| on | boolean | Current power state |
|
||||
| mode | string | Current mode (day/night/simulation) |
|
||||
| schema | string | Active schema filename |
|
||||
| color | object | Current RGB color being displayed |
|
||||
| Field | Type | Description |
|
||||
|---------|---------|--------------------------------------|
|
||||
| on | boolean | Current power state |
|
||||
| thunder | boolean | Current thunder effect state |
|
||||
| mode | string | Current mode (day/night/simulation) |
|
||||
| schema | string | Active schema filename |
|
||||
| color | object | Current RGB color being displayed |
|
||||
|
||||
---
|
||||
|
||||
### LED Configuration
|
||||
|
||||
#### Get LED Configuration
|
||||
|
||||
Returns the current LED segment configuration.
|
||||
|
||||
- **URL:** `/api/wled/config`
|
||||
- **Method:** `GET`
|
||||
- **Response:**
|
||||
|
||||
```json
|
||||
{
|
||||
"segments": [
|
||||
{
|
||||
"name": "Main Light",
|
||||
"start": 0,
|
||||
"leds": 60
|
||||
},
|
||||
{
|
||||
"name": "Accent Light",
|
||||
"start": 60,
|
||||
"leds": 30
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
| Field | Type | Description |
|
||||
|--------------------|--------|------------------------------------------|
|
||||
| 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 LED Configuration
|
||||
|
||||
Saves the LED segment configuration.
|
||||
|
||||
- **URL:** `/api/wled/config`
|
||||
- **Method:** `POST`
|
||||
- **Content-Type:** `application/json`
|
||||
- **Request Body:**
|
||||
|
||||
```json
|
||||
{
|
||||
"segments": [
|
||||
{
|
||||
"name": "Main Light",
|
||||
"start": 0,
|
||||
"leds": 60
|
||||
},
|
||||
{
|
||||
"name": "Accent Light",
|
||||
"start": 60,
|
||||
"leds": 30
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
| Field | Type | Required | Description |
|
||||
|--------------------|--------|----------|------------------------------------------|
|
||||
| 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:**
|
||||
- Segments define how the LED strip is divided into logical groups
|
||||
- Changes are persisted to NVS (non-volatile storage)
|
||||
- Each segment can be controlled independently in the light schema
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -1199,3 +1199,135 @@ body {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
/* LED 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 LED */
|
||||
@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;
|
||||
}
|
||||
}
|
||||
@@ -40,7 +40,6 @@
|
||||
<h2 data-i18n="control.light.title">Lichtsteuerung</h2>
|
||||
|
||||
<div class="control-section">
|
||||
<h3 data-i18n="control.light.onoff">Ein/Aus</h3>
|
||||
<div class="control-group">
|
||||
<div class="toggle-row">
|
||||
<span class="toggle-label" data-i18n="control.light.light">Licht</span>
|
||||
@@ -49,6 +48,13 @@
|
||||
<span class="toggle-icon" id="light-icon">💡</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="toggle-row">
|
||||
<span class="toggle-label" data-i18n="control.light.thunder">Gewitter</span>
|
||||
<button class="toggle-switch" id="thunder-toggle" onclick="toggleThunder()">
|
||||
<span class="toggle-state" id="thunder-state" data-i18n="common.off">AUS</span>
|
||||
<span class="toggle-icon" id="thunder-icon">⚡</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="light-status" class="status"></div>
|
||||
</div>
|
||||
@@ -135,9 +141,12 @@
|
||||
<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('devices')" data-i18n="subtab.devices">🔗 Geräte</button>
|
||||
<button class="sub-tab" onclick="switchSubTab('scenes')" data-i18n="subtab.scenes">🎬 Szenen</button>
|
||||
<button class="sub-tab" onclick="switchSubTab('schema')" data-i18n="subtab.light">💡
|
||||
Lichtsteuerung</button>
|
||||
<button class="sub-tab" id="subtab-btn-devices" onclick="switchSubTab('devices')"
|
||||
data-i18n="subtab.devices">🔗 Geräte</button>
|
||||
<button class="sub-tab" id="subtab-btn-scenes" onclick="switchSubTab('scenes')"
|
||||
data-i18n="subtab.scenes">🎬 Szenen</button>
|
||||
</div>
|
||||
|
||||
<!-- WLAN Sub-Tab -->
|
||||
@@ -185,8 +194,40 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Schema Sub-Tab -->
|
||||
<!-- Schema Sub-Tab (Lichtsteuerung) -->
|
||||
<div id="subtab-schema" class="sub-tab-content">
|
||||
<!-- LED Konfiguration -->
|
||||
<div class="card">
|
||||
<h2 data-i18n="wled.config.title">LED Konfiguration</h2>
|
||||
<p class="card-description" data-i18n="wled.config.desc">Konfiguriere die LED-Segmente und Anzahl
|
||||
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>
|
||||
|
||||
@@ -416,6 +457,7 @@
|
||||
|
||||
<!-- JavaScript Modules -->
|
||||
<script src="js/i18n.js"></script>
|
||||
<script src="js/capabilities.js"></script>
|
||||
<script src="js/wifi-shared.js"></script>
|
||||
<script src="js/ui.js"></script>
|
||||
<script src="js/websocket.js"></script>
|
||||
@@ -423,6 +465,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>
|
||||
|
||||
|
||||
@@ -41,13 +41,19 @@ document.addEventListener('touchend', (e) => {
|
||||
}, false);
|
||||
|
||||
// Initialization
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
document.addEventListener('DOMContentLoaded', async () => {
|
||||
initI18n();
|
||||
initTheme();
|
||||
await initCapabilities();
|
||||
initWebSocket();
|
||||
updateConnectionStatus();
|
||||
loadScenes();
|
||||
loadPairedDevices();
|
||||
loadLightStatus();
|
||||
|
||||
// Only load scenes and devices if thread is enabled
|
||||
if (isThreadEnabled()) {
|
||||
loadScenes();
|
||||
loadPairedDevices();
|
||||
}
|
||||
// WiFi status polling (less frequent)
|
||||
setInterval(updateConnectionStatus, 30000);
|
||||
});
|
||||
|
||||
94
firmware/storage/www/js/capabilities.js
Normal file
94
firmware/storage/www/js/capabilities.js
Normal file
@@ -0,0 +1,94 @@
|
||||
// Capabilities Module
|
||||
// Checks device capabilities and controls feature visibility
|
||||
|
||||
let capabilities = {
|
||||
thread: false
|
||||
};
|
||||
|
||||
/**
|
||||
* Initialize capabilities module
|
||||
* Fetches from server, falls back to URL parameter for offline testing
|
||||
*/
|
||||
async function initCapabilities() {
|
||||
// Try to fetch from server first
|
||||
const success = await fetchCapabilities();
|
||||
|
||||
// If server not available, check URL parameter (for offline testing)
|
||||
if (!success) {
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
if (urlParams.get('thread') === 'true') {
|
||||
capabilities.thread = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Apply visibility based on capabilities
|
||||
applyCapabilities();
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch capabilities from server
|
||||
* @returns {boolean} true if successful
|
||||
*/
|
||||
async function fetchCapabilities() {
|
||||
try {
|
||||
const response = await fetch('/api/capabilities');
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
capabilities = { ...capabilities, ...data };
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
} catch (error) {
|
||||
console.log('Capabilities not available, using defaults');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if thread/Matter is enabled
|
||||
* @returns {boolean}
|
||||
*/
|
||||
function isThreadEnabled() {
|
||||
return capabilities.thread === true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply capabilities to UI - show/hide elements
|
||||
*/
|
||||
function applyCapabilities() {
|
||||
const threadEnabled = isThreadEnabled();
|
||||
|
||||
// Elements to show/hide based on thread capability
|
||||
const threadElements = [
|
||||
// Control tab elements
|
||||
'scenes-control-card',
|
||||
'devices-control-card',
|
||||
// Config sub-tabs
|
||||
'subtab-btn-devices',
|
||||
'subtab-btn-scenes',
|
||||
// Config sub-tab contents
|
||||
'subtab-devices',
|
||||
'subtab-scenes'
|
||||
];
|
||||
|
||||
threadElements.forEach(id => {
|
||||
const element = document.getElementById(id);
|
||||
if (element) {
|
||||
element.style.display = threadEnabled ? '' : 'none';
|
||||
}
|
||||
});
|
||||
|
||||
// Also hide scene devices section in scene modal if thread disabled
|
||||
const sceneDevicesSection = document.querySelector('#scene-modal .form-group:has(#scene-devices-list)');
|
||||
if (sceneDevicesSection) {
|
||||
sceneDevicesSection.style.display = threadEnabled ? '' : 'none';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all capabilities
|
||||
* @returns {object}
|
||||
*/
|
||||
function getCapabilities() {
|
||||
return { ...capabilities };
|
||||
}
|
||||
@@ -12,14 +12,29 @@ const translations = {
|
||||
|
||||
// Sub Tabs
|
||||
'subtab.wifi': '📶 WLAN',
|
||||
'subtab.schema': '💡 Schema',
|
||||
'subtab.light': '💡 Lichtsteuerung',
|
||||
'subtab.devices': '🔗 Geräte',
|
||||
'subtab.scenes': '🎬 Szenen',
|
||||
|
||||
// LED Configuration
|
||||
'wled.config.title': 'LED Konfiguration',
|
||||
'wled.config.desc': 'Konfiguriere die LED-Segmente und Anzahl LEDs pro Segment',
|
||||
'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': 'LED-Konfiguration gespeichert!',
|
||||
'wled.error.save': 'Fehler beim Speichern der LED-Konfiguration',
|
||||
'wled.loaded': 'LED-Konfiguration geladen',
|
||||
|
||||
// Light Control
|
||||
'control.light.title': 'Lichtsteuerung',
|
||||
'control.light.onoff': 'Ein/Aus',
|
||||
'control.light.light': 'Licht',
|
||||
'control.light.thunder': 'Gewitter',
|
||||
'control.mode.title': 'Betriebsmodus',
|
||||
'control.schema.active': 'Aktives Schema',
|
||||
'control.status.title': 'Aktueller Status',
|
||||
@@ -168,14 +183,29 @@ const translations = {
|
||||
|
||||
// Sub Tabs
|
||||
'subtab.wifi': '📶 WiFi',
|
||||
'subtab.schema': '💡 Schema',
|
||||
'subtab.light': '💡 Light Control',
|
||||
'subtab.devices': '🔗 Devices',
|
||||
'subtab.scenes': '🎬 Scenes',
|
||||
|
||||
// LED Configuration
|
||||
'wled.config.title': 'LED Configuration',
|
||||
'wled.config.desc': 'Configure LED segments and number of LEDs per segment',
|
||||
'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': 'LED configuration saved!',
|
||||
'wled.error.save': 'Error saving LED configuration',
|
||||
'wled.loaded': 'LED configuration loaded',
|
||||
|
||||
// Light Control
|
||||
'control.light.title': 'Light Control',
|
||||
'control.light.onoff': 'On/Off',
|
||||
'control.light.light': 'Light',
|
||||
'control.light.thunder': 'Thunder',
|
||||
'control.mode.title': 'Operating Mode',
|
||||
'control.schema.active': 'Active Schema',
|
||||
'control.status.title': 'Current Status',
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
// Light control
|
||||
let thunderOn = false;
|
||||
|
||||
async function toggleLight() {
|
||||
lightOn = !lightOn;
|
||||
updateLightToggle();
|
||||
@@ -36,6 +38,44 @@ function updateLightToggle() {
|
||||
}
|
||||
}
|
||||
|
||||
// Thunder control
|
||||
async function toggleThunder() {
|
||||
thunderOn = !thunderOn;
|
||||
updateThunderToggle();
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/light/thunder', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ on: thunderOn })
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
showStatus('light-status', `${t('control.light.thunder')} ${thunderOn ? t('common.on') : t('common.off')}`, 'success');
|
||||
} else {
|
||||
throw new Error(t('error'));
|
||||
}
|
||||
} catch (error) {
|
||||
showStatus('light-status', `Demo: ${t('control.light.thunder')} ${thunderOn ? t('common.on') : t('common.off')}`, 'success');
|
||||
}
|
||||
}
|
||||
|
||||
function updateThunderToggle() {
|
||||
const toggle = document.getElementById('thunder-toggle');
|
||||
const state = document.getElementById('thunder-state');
|
||||
const icon = document.getElementById('thunder-icon');
|
||||
|
||||
if (thunderOn) {
|
||||
toggle.classList.add('active');
|
||||
state.textContent = t('common.on');
|
||||
icon.textContent = '⛈️';
|
||||
} else {
|
||||
toggle.classList.remove('active');
|
||||
state.textContent = t('common.off');
|
||||
icon.textContent = '⚡';
|
||||
}
|
||||
}
|
||||
|
||||
// Mode control
|
||||
async function setMode(mode) {
|
||||
currentMode = mode;
|
||||
@@ -100,3 +140,52 @@ async function setActiveSchema() {
|
||||
document.getElementById('current-schema').textContent = schemaName;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Load light status from server
|
||||
*/
|
||||
async function loadLightStatus() {
|
||||
try {
|
||||
const response = await fetch('/api/light/status');
|
||||
if (response.ok) {
|
||||
const status = await response.json();
|
||||
|
||||
// Update light state
|
||||
if (typeof status.on === 'boolean') {
|
||||
lightOn = status.on;
|
||||
updateLightToggle();
|
||||
}
|
||||
|
||||
// Update thunder state
|
||||
if (typeof status.thunder === 'boolean') {
|
||||
thunderOn = status.thunder;
|
||||
updateThunderToggle();
|
||||
}
|
||||
|
||||
// Update mode
|
||||
if (status.mode) {
|
||||
currentMode = status.mode;
|
||||
updateModeButtons();
|
||||
updateSimulationOptions();
|
||||
document.getElementById('current-mode').textContent = t(`mode.${status.mode}`);
|
||||
}
|
||||
|
||||
// Update schema
|
||||
if (status.schema) {
|
||||
document.getElementById('active-schema').value = status.schema;
|
||||
const schemaNum = status.schema.replace('schema_0', '').replace('.csv', '');
|
||||
document.getElementById('current-schema').textContent = t(`schema.name.${schemaNum}`);
|
||||
}
|
||||
|
||||
// Update current color
|
||||
if (status.color) {
|
||||
const colorPreview = document.getElementById('current-color');
|
||||
if (colorPreview) {
|
||||
colorPreview.style.backgroundColor = `rgb(${status.color.r}, ${status.color.g}, ${status.color.b})`;
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.log('Light status not available');
|
||||
}
|
||||
}
|
||||
|
||||
172
firmware/storage/www/js/wled.js
Normal file
172
firmware/storage/www/js/wled.js
Normal file
@@ -0,0 +1,172 @@
|
||||
// LED Configuration Module
|
||||
// Manages LED segments and 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 LED 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