16/09/2026, 21:07:41
Voici la deuxième partie du code.
Code :
const char INDEX_HTML[] PROGMEM = R"rawliteral(
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>KNX - LED Effect</title>
<style>
:root {
--bg: #0b1120;
--card: #151e2f;
--border: #334155;
--text: #f8fafc;
--muted: #94a3b8;
--red: #ef4444;
--green: #22c55e;
--blue: #3b82f6;
--primary: #6366f1;
--primary2: #818cf8;
--rainbow: linear-gradient(135deg, #ef4444, #f59e0b, #10b981, #3b82f6, #8b5cf6);
}
* { box-sizing: border-box; }
body {
margin: 0; min-height: 100vh;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
background: radial-gradient(circle at top, #1e1b4b 0%, var(--bg) 50%);
color: var(--text);
}
.container { width: 100%; max-width: 600px; margin: auto; padding: 20px 16px 40px; }
.header { text-align: center; margin-bottom: 20px; }
.header h1 { margin: 0; font-size: 26px; }
.subtitle { color: var(--muted); font-size: 14px; margin-top: 4px; }
.card {
background: rgba(21, 30, 47, 0.9); border: 1px solid var(--border);
border-radius: 18px; padding: 20px; margin-bottom: 16px;
backdrop-filter: blur(10px); box-shadow: 0 10px 30px rgba(0,0,0,0.3);
}
.card-title { font-size: 16px; font-weight: 700; margin-bottom: 15px; color: var(--primary2); display: flex; justify-content: space-between; align-items: center;}
.preview-box {
width: 100%; height: 50px; border-radius: 12px; margin-bottom: 20px;
border: 2px solid var(--border); transition: background 0.1s;
box-shadow: inset 0 2px 10px rgba(0,0,0,0.5);
}
.topbar { display:flex; flex-wrap:wrap; gap:8px; justify-content:center; margin-bottom:16px; }
.pill { background: var(--card); border:1px solid var(--border); border-radius:20px; padding:6px 14px; font-size:12.5px; color:var(--muted); }
b.ok{color:#22c55e;} b.err{color:#ef4444;}
.led-toggle { display:flex; align-items:center; justify-content:space-between; margin-bottom:16px; }
.onoff-btn { border:none; border-radius:24px; padding:10px 22px; font-weight:700; font-size:13px; cursor:pointer; background:#1e293b; color:var(--muted); }
.onoff-btn.on { background:linear-gradient(90deg,#3b82f6,#22c55e); color:#0b1120; }
.slider-group { margin-bottom: 14px; }
.slider-header { display: flex; justify-content: space-between; font-size: 14px; font-weight: 600; margin-bottom: 6px; }
.slider {
width: 100%; height: 8px; border-radius: 10px; appearance: none; outline: none;
background: #1e293b; cursor: pointer;
}
.slider::-webkit-slider-thumb {
appearance: none; width: 22px; height: 22px; border-radius: 50%;
background: #fff; border: 3px solid var(--border); cursor: pointer;
box-shadow: 0 2px 8px rgba(0,0,0,0.4);
}
.slider-master::-webkit-slider-thumb { border-color: var(--primary); }
.slider-speed::-webkit-slider-thumb { border-color: #f59e0b; }
.slider-r::-webkit-slider-thumb { border-color: var(--red); }
.slider-g::-webkit-slider-thumb { border-color: var(--green); }
.slider-b::-webkit-slider-thumb { border-color: var(--blue); }
.effects-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 10px; margin-top: 10px; }
@media (min-width: 480px) { .effects-grid { grid-template-columns: repeat(4, 1fr); } }
.btn-fx {
background: #1e293b; border: 1px solid var(--border); color: var(--text);
padding: 12px 8px; border-radius: 10px; font-size: 12px; font-weight: 600;
cursor: pointer; text-align: center; transition: .2s;
}
.btn-fx:hover { border-color: var(--primary); transform: translateY(-1px); }
.btn-fx.active { background: var(--primary); border-color: var(--primary); box-shadow: 0 4px 15px rgba(99,102,241,0.4); }
.btn-rainbow { border: 1px solid rgba(255,255,255,0.2); }
.btn-rainbow:hover { border-color: #ec4899; }
.btn-rainbow.active { background: var(--rainbow); border-color: #ffffff; box-shadow: 0 4px 15px rgba(236,72,153,0.5); }
.btn-stop { background: #ef4444; border: 1px solid #dc2626; color: white; padding: 8px 16px; border-radius: 8px; font-weight: bold; cursor: pointer; font-size: 12px; }
.picker-row { display: flex; align-items: center; justify-content: space-between; gap: 10px; margin-bottom: 15px; background: #0f172a; padding: 10px 14px; border-radius: 10px; }
.color-picker { width: 60px; height: 35px; border: none; border-radius: 8px; cursor: pointer; background: none; }
.info-row { display: flex; justify-content: space-between; padding: 6px 0; border-bottom: 1px solid rgba(148,163,184,0.1); font-size: 13px; }
.info-row:last-child { border-bottom: none; }
.info-label { color: var(--muted); }
details { background: var(--card); border: 1px solid var(--border); border-radius: 16px; padding: 16px 20px; margin-top: 4px; }
summary { cursor: pointer; font-weight: 700; font-size: 13.5px; color: var(--muted); }
input[type=text], select, button.full { width:100%; padding:10px; margin:8px 0; box-sizing:border-box; border:1px solid var(--border); border-radius:8px; font-size:14px; background:#0f172a; color:var(--text); font-family:inherit; }
button.full { background:#2d6fb3; color:#fff; font-weight:700; cursor:pointer; border:none; }
#statusMsg { min-height: 18px; color: #22c55e; font-size: 13px; }
table { width:100%; border-collapse:collapse; font-size:12.5px; margin-top:8px; }
th, td { border-bottom:1px solid var(--border); padding:6px 4px; text-align:left; color:var(--muted); }
th { color: var(--text); }
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>KNX - RGB Effect</h1>
<div class="subtitle">ESP32 Dynamic Lighting + KNX</div>
</div>
<div class="topbar">
<div class="pill" id="dtNow">...</div>
<div class="pill">KNX: <span id="knxStatus">...</span></div>
</div>
<div class="card">
<div id="preview" class="preview-box"></div>
<div class="led-toggle">
<span>Controller Ein/Aus</span>
<button class="onoff-btn" id="onoffBtn" onclick="toggleOnOff()">...</button>
</div>
<div class="slider-group">
<div class="slider-header">
<span>Gesamthelligkeit (Master)</span>
<span id="valMaster">100%</span>
</div>
<input type="range" id="sliderMaster" class="slider slider-master" min="0" max="100" value="100">
</div>
<div class="slider-group">
<div class="slider-header" style="color: #f59e0b;">
<span>Effekt-Geschwindigkeit (Speed)</span>
<span id="valSpeed">50%</span>
</div>
<input type="range" id="sliderSpeed" class="slider slider-speed" min="1" max="100" value="50">
</div>
</div>
<div class="card">
<div class="card-title">
<span>16 Programme (Mit Wunschfarbe)</span>
<button class="btn-stop" onclick="stopEffect()">⏹ Effekte Aus</button>
</div>
<div class="picker-row">
<span style="font-size: 13px; color: #f8fafc; font-weight: 600;">Farbe für Programme wählen:</span>
<input type="color" id="fxPicker" class="color-picker" value="#ff0000">
</div>
<div class="effects-grid">
<button class="btn-fx" id="fx1" onclick="setEffect(1)">1. Fade Pulse</button>
<button class="btn-fx" id="fx2" onclick="setEffect(2)">2. Fast Pulse</button>
<button class="btn-fx" id="fx3" onclick="setEffect(3)">3. Knight Rider</button>
<button class="btn-fx" id="fx4" onclick="setEffect(4)">4. Stroboskop</button>
<button class="btn-fx" id="fx5" onclick="setEffect(5)">5. Blitz-Strobo</button>
<button class="btn-fx" id="fx6" onclick="setEffect(6)">6. Atmung</button>
<button class="btn-fx" id="fx7" onclick="setEffect(7)">7. Kerzenflackern</button>
<button class="btn-fx" id="fx8" onclick="setEffect(8)">8. Martinshorn</button>
<button class="btn-fx" id="fx9" onclick="setEffect(9)">9. Gewitter-Blitze</button>
<button class="btn-fx" id="fx10" onclick="setEffect(10)">10. Feuerflackern</button>
<button class="btn-fx" id="fx11" onclick="setEffect(11)">11. Random Takt</button>
<button class="btn-fx" id="fx12" onclick="setEffect(12)">12. Smooth Waves</button>
<button class="btn-fx" id="fx13" onclick="setEffect(13)">13. Dimmer Cycle</button>
<button class="btn-fx" id="fx14" onclick="setEffect(14)">14. Sparkle / Funken</button>
<button class="btn-fx" id="fx15" onclick="setEffect(15)">15. Dual Flash</button>
<button class="btn-fx" id="fx16" onclick="setEffect(16)">16. Herzschlag</button>
</div>
</div>
<div class="card" style="border-color: #818cf8;">
<div class="card-title" style="color: #ec4899;">
<span>? 8 Regenbogen & Multi-Color</span>
</div>
<div class="effects-grid">
<button class="btn-fx btn-rainbow" id="fx17" onclick="setEffect(17)">17. Rainbow Loop</button>
<button class="btn-fx btn-rainbow" id="fx18" onclick="setEffect(18)">18. Rainbow Chase</button>
<button class="btn-fx btn-rainbow" id="fx19" onclick="setEffect(19)">19. Party Strobo</button>
<button class="btn-fx btn-rainbow" id="fx20" onclick="setEffect(20)">20. Disko Random</button>
<button class="btn-fx btn-rainbow" id="fx21" onclick="setEffect(21)">21. Aurora Ozean</button>
<button class="btn-fx btn-rainbow" id="fx22" onclick="setEffect(22)">22. Sunset Fire</button>
<button class="btn-fx btn-rainbow" id="fx23" onclick="setEffect(23)">23. Neon Party</button>
<button class="btn-fx btn-rainbow" id="fx24" onclick="setEffect(24)">24. Polizei Blau/Rot</button>
</div>
</div>
<div class="card">
<div class="card-title">Manuelle Farbwahl</div>
<div class="slider-group">
<div class="slider-header" style="color: var(--red);"><span>Rot</span><span id="valR">255</span></div>
<input type="range" id="sliderR" class="slider slider-r" min="0" max="255" value="255">
</div>
<div class="slider-group">
<div class="slider-header" style="color: var(--green);"><span>Grün</span><span id="valG">255</span></div>
<input type="range" id="sliderG" class="slider slider-g" min="0" max="255" value="255">
</div>
<div class="slider-group">
<div class="slider-header" style="color: var(--blue);"><span>Blau</span><span id="valB">255</span></div>
<input type="range" id="sliderB" class="slider slider-b" min="0" max="255" value="255">
</div>
</div>
<div class="card">
<div class="info-row">
<span class="info-label">Datum & Uhrzeit</span>
<span id="infoDatetime" style="font-weight: 600; color: var(--primary2);">Wird geladen...</span>
</div>
<div class="info-row"><span class="info-label">Status</span><span id="infoStatus">Bereit</span></div>
</div>
<details>
<summary>Erweitert - Manueller KNX Befehl & Telegrammverlauf</summary>
<label>Gruppenadresse</label><input type="text" id="ga" value="1/0/0">
<label>Datenpunkttyp (DPT)</label><select id="dpt">
<option value="DPT1">DPT 1 (1 Bit)</option>
<option value="DPT2">DPT 2 (2 Bit, C+V, 0..3)</option>
<option value="DPT3">DPT 3 (Dimmen, -7..7)</option>
<option value="DPT5">DPT 5 (1 Byte 0..255)</option>
<option value="DPT9">DPT 9 (2 Byte Float)</option>
<option value="DPT10">DPT 10 (Zeit, 14:30:00)</option>
<option value="DPT11">DPT 11 (Datum, 24.12.26)</option>
<option value="DPT14">DPT 14 (4 Byte Float)</option>
<option value="DPT16">DPT 16 (Text)</option>
<option value="DPT232">DPT 232 (RGB, z.B. 255,128,0)</option>
</select>
<label>Wert / Payload</label><input type="text" id="val" value="1">
<button class="full" onclick="sendTelegram()">KNX Telegramm Senden</button>
<button class="full" style="background:#3a3f4d;" onclick="fetch('/reconnect').then(()=>syncStatus())">KNX Reconnect</button>
<div id="statusMsg"></div>
<table><thead><tr><th>Zeit</th><th>GA</th><th>Bytes</th><th>Hex</th><th>Wert</th></tr></thead>
<tbody id="logBody"><tr><td colspan="5">Lade...</td></tr></tbody></table>
</details>
</div>
<script>
const sM = document.getElementById("sliderMaster");
const sSp = document.getElementById("sliderSpeed");
const sR = document.getElementById("sliderR");
const sG = document.getElementById("sliderG");
const sB = document.getElementById("sliderB");
const fxPicker = document.getElementById("fxPicker");
const preview = document.getElementById("preview");
const infoDatetime = document.getElementById("infoDatetime");
const onoffBtn = document.getElementById("onoffBtn");
let sendTimer;
let currentServerTime = null;
let isUserInteracting = false;
let currentActiveFx = 0;
let ledIsOn = true;
[sM, sSp, sR, sG, sB].forEach(el => {
el.addEventListener("mousedown", () => isUserInteracting = true);
el.addEventListener("touchstart", () => isUserInteracting = true);
el.addEventListener("mouseup", () => isUserInteracting = false);
el.addEventListener("touchend", () => isUserInteracting = false);
});
sM.addEventListener("input", onSliderChange);
sR.addEventListener("input", onRGBColorSliderChange);
sG.addEventListener("input", onRGBColorSliderChange);
sB.addEventListener("input", onRGBColorSliderChange);
sSp.addEventListener("input", onSpeedSliderChange);
fxPicker.addEventListener("input", function() {
const hex = this.value;
const r = parseInt(hex.substr(1,2), 16);
const g = parseInt(hex.substr(3,2), 16);
const b = parseInt(hex.substr(5,2), 16);
sR.value = r; sG.value = g; sB.value = b;
updateUI();
fetch(`/api/fxcolor?r=${r}&g=${g}&b=${b}`).catch(e => console.log(e));
});
function onRGBColorSliderChange() {
if (currentActiveFx !== 0) {
stopEffect();
}
onSliderChange();
}
function onSpeedSliderChange() {
updateUI();
clearTimeout(sendTimer);
sendTimer = setTimeout(() => {
fetch(`/api/speed?val=${sSp.value}`).catch(e => console.log(e));
}, 40);
}
function onSliderChange() {
updateUI();
clearTimeout(sendTimer);
sendTimer = setTimeout(sendData, 40);
}
function toggleOnOff() {
ledIsOn = !ledIsOn;
applyOnOffUI();
fetch(`/api/onoff?val=${ledIsOn ? 1 : 0}`).catch(e => console.log(e));
}
function applyOnOffUI() {
onoffBtn.classList.toggle("on", ledIsOn);
onoffBtn.innerText = ledIsOn ? "AN" : "AUS";
}
function setEffect(fxId) {
currentActiveFx = fxId;
highlightFxButton(fxId);
fetch(`/api/effect?id=${fxId}&speed=${sSp.value}`).catch(e => console.log(e));
}
function stopEffect() {
currentActiveFx = 0;
highlightFxButton(0);
fetch(`/api/effect?id=0&speed=${sSp.value}`).catch(e => console.log(e));
}
function highlightFxButton(fxId) {
for (let i = 1; i <= 24; i++) {
const btn = document.getElementById(`fx${i}`);
if (btn) {
if (i === fxId) btn.classList.add("active");
else btn.classList.remove("active");
}
}
}
function updateUI() {
document.getElementById("valMaster").innerText = sM.value + "%";
document.getElementById("valSpeed").innerText = sSp.value + "%";
document.getElementById("valR").innerText = sR.value;
document.getElementById("valG").innerText = sG.value;
document.getElementById("valB").innerText = sB.value;
const factor = sM.value / 100;
const r = Math.round(sR.value * factor);
const g = Math.round(sG.value * factor);
const b = Math.round(sB.value * factor);
preview.style.background = `rgb(${r}, ${g}, ${b})`;
preview.style.boxShadow = `0 0 20px rgb(${r}, ${g}, ${b})`;
}
function sendData() {
const url = `/api/rgb?r=${sR.value}&g=${sG.value}&b=${sB.value}&m=${sM.value}&speed=${sSp.value}`;
fetch(url).catch(e => console.log(e));
}
function updateClockLocally() {
if (!currentServerTime) return;
currentServerTime.setSeconds(currentServerTime.getSeconds() + 1);
const d = currentServerTime;
const pad = n => String(n).padStart(2, '0');
infoDatetime.innerText = `${pad(d.getDate())}.${pad(d.getMonth()+1)}.${d.getFullYear()} ${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`;
}
function flash(t) {
const el = document.getElementById('statusMsg');
if (el) { el.innerText = t; clearTimeout(window._st); window._st = setTimeout(() => el.innerText = '', 2500); }
}
function sendTelegram() {
const ga = document.getElementById('ga').value;
const dpt = document.getElementById('dpt').value;
const val = document.getElementById('val').value;
fetch('/send?ga=' + encodeURIComponent(ga) + '&dpt=' + encodeURIComponent(dpt) + '&val=' + encodeURIComponent(val))
.then(r => r.text()).then(flash).catch(() => {});
}
function syncStatus() {
fetch("/api/status")
.then(res => res.json())
.then(data => {
document.getElementById('dtNow').innerText = data.datetime;
document.getElementById('knxStatus').innerHTML = data.knxConnected
? "<b class='ok'>Verbunden (Ch " + data.knxChannel + ")</b>"
: "<b class='err'>Getrennt</b>";
ledIsOn = data.on;
applyOnOffUI();
if (!isUserInteracting) {
sR.value = data.r; sG.value = data.g; sB.value = data.b;
sM.value = data.m; sSp.value = data.speed;
}
currentActiveFx = data.fx;
highlightFxButton(data.fx);
updateUI();
if (data.epoch > 0) currentServerTime = new Date(data.epoch * 1000);
var rows = '';
(data.log || []).forEach(function(e) {
rows += '<tr><td>' + e.time + '</td><td>' + e.ga + '</td><td>' + e.len + '</td><td>' + e.hex + '</td><td>' + e.value + '</td></tr>';
});
document.getElementById('logBody').innerHTML = rows || '<tr><td colspan="5">Noch keine Telegramme empfangen</td></tr>';
}).catch(e => console.log(e));
}
syncStatus();
setInterval(updateClockLocally, 1000);
setInterval(syncStatus, 1000);
</script>
</body>
</html>
)rawliteral";
String escapeJson(String s) {
String out;
for (size_t i = 0; i < s.length(); i++) {
char c = s[i];
if (c == '"' || c == '\\') out += '\\';
out += c;
}
return out;
}
void handleRoot() {
server.send_P(200, "text/html; charset=utf-8", INDEX_HTML);
}
void handleRGB() {
if (server.hasArg("r") && server.hasArg("g") && server.hasArg("b") && server.hasArg("m")) {
int r = server.arg("r").toInt();
int g = server.arg("g").toInt();
int b = server.arg("b").toInt();
int m = server.arg("m").toInt();
applyEffectSet(0);
applyMasterPercent(m);
applyColor(r, g, b);
if (server.hasArg("speed")) applySpeedPercent(server.arg("speed").toInt());
server.send(200, "application/json", "{\"success\":true}");
} else {
server.send(400, "text/plain", "Fehlende Parameter");
}
}
void handleSpeed() {
if (server.hasArg("val")) {
applySpeedPercent(server.arg("val").toInt());
server.send(200, "application/json", "{\"success\":true}");
} else {
server.send(400, "text/plain", "Parameter fehlt");
}
}
void handleEffect() {
if (server.hasArg("id")) {
applyEffectSet(server.arg("id").toInt());
if (server.hasArg("speed")) applySpeedPercent(server.arg("speed").toInt());
server.send(200, "application/json", "{\"success\":true}");
} else {
server.send(400, "text/plain", "Parameter fehlt");
}
}
void handleFxColor() {
if (server.hasArg("r") && server.hasArg("g") && server.hasArg("b")) {
applyColor(server.arg("r").toInt(), server.arg("g").toInt(), server.arg("b").toInt());
server.send(200, "application/json", "{\"success\":true}");
} else {
server.send(400, "text/plain", "Parameter fehlt");
}
}
void handleOnOff() {
if (server.hasArg("val")) {
applyOnOff(server.arg("val").toInt() > 0);
server.send(200, "application/json", "{\"success\":true}");
} else {
server.send(400, "text/plain", "Parameter fehlt");
}
}
void handleSend() {
if (server.hasArg("ga") && server.hasArg("dpt") && server.hasArg("val")) {
String ga = server.arg("ga");
String dpt = server.arg("dpt");
String val = server.arg("val");
bool ok = sendKnxValue(ga, dpt, val);
server.send(200, "text/plain", ok ? "Telegramm gesendet und per ACK bestaetigt!" : "Gesendet, aber KEIN ACK erhalten - siehe Serial-Monitor!");
} else {
server.send(400, "text/plain", "Fehlende Parameter!");
}
}
void handleReconnect() {
connectKnx();
server.send(200, "text/plain", "Reconnected!");
}
void handleStatus() {
String json = "{";
json += "\"project\":\"" + escapeJson(String(projectName)) + "\",";
json += "\"datetime\":\"" + escapeJson(getFormattedDateTime()) + "\",";
json += "\"knxConnected\":" + String(isKnxConnected ? "true" : "false") + ",";
json += "\"knxChannel\":" + String(knxChannelId) + ",";
json += "\"on\":" + String(ledOn ? "true" : "false") + ",";
json += "\"r\":" + String(targetR);
json += ",\"g\":" + String(targetG);
json += ",\"b\":" + String(targetB);
json += ",\"m\":" + String(targetMaster);
json += ",\"speed\":" + String(effectSpeed);
json += ",\"fx\":" + String(activeEffect);
json += ",\"colorTemp\":" + String(colorTempPercent);
json += ",\"hue\":" + String(hueDegrees);
json += ",\"epoch\":" + String(getEpochTime());
json += ",\"log\":[";
for (int i = 0; i < knxLogFilled; i++) {
int idx = (knxLogNext - 1 - i + KNX_LOG_SIZE) % KNX_LOG_SIZE;
KnxLogEntry &e = knxLog[idx];
String hex = "";
for (int b = 0; b < e.len; b++) {
char hb[4];
snprintf(hb, sizeof(hb), "%02X ", e.data[b]);
hex += hb;
}
hex.trim();
if (i > 0) json += ",";
json += "{";
json += "\"time\":\"" + escapeJson(String(e.timeStr)) + "\",";
json += "\"ga\":\"" + escapeJson(gaToString(e.ga)) + "\",";
json += "\"len\":" + String(e.len) + ",";
json += "\"hex\":\"" + escapeJson(hex) + "\",";
json += "\"value\":\"" + escapeJson(interpretKnxData(e.data, e.len)) + "\"";
json += "}";
}
json += "]}";
server.send(200, "application/json", json);
}
void connectWiFi() {
WiFi.mode(WIFI_STA);
WiFi.setHostname(HOSTNAME);
WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
Serial.print("Verbinde WLAN");
unsigned long start = millis();
while (WiFi.status() != WL_CONNECTED && millis() - start < 15000) {
delay(500);
Serial.print(".");
}
Serial.println();
if (WiFi.status() == WL_CONNECTED) {
Serial.print("IP - "); Serial.println(WiFi.localIP());
}
}
void setup() {
Serial.begin(115200);
delay(200);
Serial.printf("\n===> %s <===\n", projectName);
preferences.begin("rgb-dimmer", false);
loadSettings();
ledcAttach(PIN_RED, PWM_FREQUENCY, PWM_RESOLUTION);
ledcAttach(PIN_GREEN, PWM_FREQUENCY, PWM_RESOLUTION);
ledcAttach(PIN_BLUE, PWM_FREQUENCY, PWM_RESOLUTION);
writePWMHardware(ledOn ? currentR : 0, ledOn ? currentG : 0, ledOn ? currentB : 0);
gaLedOnOffParsed = parseGroupAddress(gaLedOnOff);
gaEffectStepParsed = parseGroupAddress(gaEffectStep);
gaEffectSetParsed = parseGroupAddress(gaEffectSet);
gaColorRGBParsed = parseGroupAddress(gaColorRGB);
gaBrightnessParsed = parseGroupAddress(gaBrightness);
gaSpeedParsed = parseGroupAddress(gaSpeed);
gaColorTempStepParsed = parseGroupAddress(gaColorTempStep);
gaHueStepParsed = parseGroupAddress(gaHueStep);
connectWiFi();
if (WiFi.status() == WL_CONNECTED) {
configTzTime(TZ_INFO, NTP_SERVER);
Serial.println("Warte auf Zeitsynchronisation (NTP)...");
struct tm timeinfo;
if (getLocalTime(&timeinfo, 5000)) {
Serial.println("Zeit synchronisiert: " + getFormattedDateTime());
} else {
Serial.println("Zeit konnte nicht synchronisiert werden (wird spaeter automatisch nachgeholt).");
}
if (MDNS.begin(HOSTNAME)) MDNS.addService("http", "tcp", 80);
}
udp.begin(localPort);
connectKnx(true);
server.on("/", HTTP_GET, handleRoot);
server.on("/api/rgb", HTTP_GET, handleRGB);
server.on("/api/speed", HTTP_GET, handleSpeed);
server.on("/api/effect", HTTP_GET, handleEffect);
server.on("/api/fxcolor", HTTP_GET, handleFxColor);
server.on("/api/onoff", HTTP_GET, handleOnOff);
server.on("/api/status", HTTP_GET, handleStatus);
server.on("/send", HTTP_GET, handleSend);
server.on("/reconnect", HTTP_GET, handleReconnect);
server.begin();
Serial.println("Webserver gestartet. 'help' fuer Serial-Befehle.");
}
void loop() {
server.handleClient();
handleKnxRx();
handleSerialCommands();
if (!ledOn) {
if (currentR != 0 || currentG != 0 || currentB != 0) {
currentR = currentG = currentB = 0;
isFading = false;
writePWMHardware(0, 0, 0);
}
} else {
processEffects();
updateFade();
processColorTempDim();
processHueDim();
}
if (isKnxConnected) {
if (millis() - lastHeartbeat > HEARTBEAT_INTERVAL) {
sendConnectionStateRequest();
lastHeartbeat = millis();
}
} else {
static unsigned long lastReconnectAttempt = 0;
if (millis() - lastReconnectAttempt > 3000) {
connectKnx();
lastReconnectAttempt = millis();
}
}
}
