From 84e1ebf1550bbfbaecca0e0de5ec854e8f19288c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 9 Apr 2026 06:58:29 +0000 Subject: [PATCH 1/3] Initial plan From ed23216b773844f139cc9b9638d19e7e1c28f6ad Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 9 Apr 2026 07:07:06 +0000 Subject: [PATCH 2/3] Add Alexa per-segment device emulation Extend the Alexa/Hue emulation to optionally expose each active segment as a separate Alexa device. When enabled via the new "Also emulate a device for each active segment" checkbox in Sync settings, users can independently control brightness, color, and on/off state per segment through Alexa voice commands. Device index layout: 0=main, 1..N=presets, (N+1)..=segments. Backwards compatible: disabled by default, existing preset behavior is unchanged. ESPALEXA_MAXDEVICES raised from 10 to 20 to accommodate segment devices alongside presets. Agent-Logs-Url: https://github.com/wled/WLED/sessions/2bb549aa-be26-40ef-870e-20a7379d69e0 Co-authored-by: netmindz <442066+netmindz@users.noreply.github.com> --- wled00/alexa.cpp | 106 ++++++++++++++++++++++++++++++++-- wled00/cfg.cpp | 2 + wled00/data/settings_sync.htm | 3 +- wled00/set.cpp | 1 + wled00/wled.h | 3 +- wled00/xml.cpp | 1 + 6 files changed, 109 insertions(+), 7 deletions(-) diff --git a/wled00/alexa.cpp b/wled00/alexa.cpp index 81b9ec3469..06d6fd7be1 100644 --- a/wled00/alexa.cpp +++ b/wled00/alexa.cpp @@ -12,11 +12,16 @@ #ifndef WLED_DISABLE_ALEXA void onAlexaChange(EspalexaDevice* dev); +// index of the first segment device in the Espalexa device list (0 = no segment devices) +static unsigned alexaSegmentDeviceStart = 0; + void alexaInit() { if (!alexaEnabled || !WLED_CONNECTED) return; espalexa.removeAllDevices(); + alexaSegmentDeviceStart = 0; + // the original configured device for on/off or macros (added first, i.e. index 0) espalexaDevice = new EspalexaDevice(alexaInvocationName, onAlexaChange, EspalexaDeviceType::extendedcolor); espalexa.addDevice(espalexaDevice); @@ -32,6 +37,20 @@ void alexaInit() espalexa.addDevice(dev); } } + + // segment devices are added after the main device and preset devices + // device IDs: 0 = main, 1..N = presets, (N+1).. = segments + if (alexaExposeSegments) { + alexaSegmentDeviceStart = espalexa.getDeviceCount(); // first segment device index + for (unsigned i = 0; i < strip.getSegmentsNum(); i++) { + Segment& seg = strip.getSegment(i); + if (!seg.isActive()) continue; + String segName = seg.name ? String(seg.name) : (String(F("Segment ")) + String(i)); + EspalexaDevice* dev = new EspalexaDevice(segName.c_str(), onAlexaChange, EspalexaDeviceType::extendedcolor); + espalexa.addDevice(dev); + } + } + espalexa.begin(&server); } @@ -44,10 +63,84 @@ void handleAlexa() void onAlexaChange(EspalexaDevice* dev) { EspalexaDeviceProperty m = dev->getLastChangedProperty(); + unsigned devId = dev->getId(); + + // determine if this is a segment device + bool isSegmentDevice = alexaExposeSegments && alexaSegmentDeviceStart > 0 && devId >= alexaSegmentDeviceStart; + + if (isSegmentDevice) + { + // map device index to segment index (skipping inactive segments, same order as in alexaInit) + unsigned segDevIdx = devId - alexaSegmentDeviceStart; + unsigned segIdx = 0; + unsigned activeCount = 0; + for (unsigned i = 0; i < strip.getSegmentsNum(); i++) { + if (!strip.getSegment(i).isActive()) continue; + if (activeCount == segDevIdx) { segIdx = i; break; } + activeCount++; + } + Segment& seg = strip.getSegment(segIdx); + + if (m == EspalexaDeviceProperty::on) + { + seg.setOption(SEG_OPTION_ON, true); + seg.setOpacity(seg.opacity ? seg.opacity : 255); + stateUpdated(CALL_MODE_ALEXA); + } + else if (m == EspalexaDeviceProperty::off) + { + seg.setOption(SEG_OPTION_ON, false); + dev->setValue(0); + stateUpdated(CALL_MODE_ALEXA); + } + else if (m == EspalexaDeviceProperty::bri) + { + seg.setOption(SEG_OPTION_ON, true); + seg.setOpacity(dev->getValue()); + stateUpdated(CALL_MODE_ALEXA); + } + else // color + { + if (dev->getColorMode() == EspalexaColorMode::ct) + { + byte rgbw[4]; + uint16_t ct = dev->getCt(); + if (!ct) return; + uint16_t k = 1000000 / ct; + if (seg.isCCT()) { + seg.setCCT(k); + if (seg.hasWhite()) { + rgbw[0] = 0; rgbw[1] = 0; rgbw[2] = 0; rgbw[3] = 255; + } else { + rgbw[0] = 255; rgbw[1] = 255; rgbw[2] = 255; rgbw[3] = 0; + dev->setValue(255); + } + } else if (seg.hasWhite()) { + switch (ct) { + case 199: rgbw[0]=255; rgbw[1]=255; rgbw[2]=255; rgbw[3]=255; break; + case 234: rgbw[0]=127; rgbw[1]=127; rgbw[2]=127; rgbw[3]=255; break; + case 284: rgbw[0]= 0; rgbw[1]= 0; rgbw[2]= 0; rgbw[3]=255; break; + case 350: rgbw[0]=130; rgbw[1]= 90; rgbw[2]= 0; rgbw[3]=255; break; + case 383: rgbw[0]=255; rgbw[1]=153; rgbw[2]= 0; rgbw[3]=255; break; + default : colorKtoRGB(k, rgbw); + } + } else { + colorKtoRGB(k, rgbw); + } + seg.setColor(0, RGBW32(rgbw[0], rgbw[1], rgbw[2], rgbw[3])); + } else { + uint32_t color = dev->getRGB(); + seg.setColor(0, color); + } + stateUpdated(CALL_MODE_ALEXA); + } + return; + } + // original behavior: main device and preset devices if (m == EspalexaDeviceProperty::on) { - if (dev->getId() == 0) // Device 0 is for on/off or macros + if (devId == 0) // Device 0 is for on/off or macros { if (!macroAlexaOn) { @@ -64,13 +157,14 @@ void onAlexaChange(EspalexaDevice* dev) } else // switch-on behavior for preset devices { // turn off other preset devices - for (unsigned i = 1; i < espalexa.getDeviceCount(); i++) + unsigned presetEnd = alexaSegmentDeviceStart ? alexaSegmentDeviceStart : espalexa.getDeviceCount(); + for (unsigned i = 1; i < presetEnd; i++) { - if (i == dev->getId()) continue; + if (i == devId) continue; espalexa.getDevice(i)->setValue(0); // turn off other presets } - applyPreset(dev->getId(), CALL_MODE_ALEXA); // in alexaInit() preset 1 device was added second (index 1), preset 2 third (index 2) etc. + applyPreset(devId, CALL_MODE_ALEXA); // in alexaInit() preset 1 device was added second (index 1), preset 2 third (index 2) etc. } } else if (m == EspalexaDeviceProperty::off) { @@ -87,7 +181,9 @@ void onAlexaChange(EspalexaDevice* dev) applyPreset(macroAlexaOff, CALL_MODE_ALEXA); // below for loop stops Alexa from complaining if macroAlexaOff does not actually turn off } - for (unsigned i = 0; i < espalexa.getDeviceCount(); i++) + // set main and preset devices to off, but not segment devices + unsigned presetEnd = alexaSegmentDeviceStart ? alexaSegmentDeviceStart : espalexa.getDeviceCount(); + for (unsigned i = 0; i < presetEnd; i++) { espalexa.getDevice(i)->setValue(0); } diff --git a/wled00/cfg.cpp b/wled00/cfg.cpp index d01f83ad54..917db1d439 100644 --- a/wled00/cfg.cpp +++ b/wled00/cfg.cpp @@ -623,6 +623,7 @@ bool deserializeConfig(JsonObject doc, bool fromFS) { CJSON(macroAlexaOn, interfaces["va"]["macros"][0]); CJSON(macroAlexaOff, interfaces["va"]["macros"][1]); CJSON(alexaNumPresets, interfaces["va"]["p"]); + CJSON(alexaExposeSegments, interfaces["va"]["seg"]); #endif #ifndef WLED_DISABLE_MQTT @@ -1145,6 +1146,7 @@ void serializeConfig(JsonObject root) { if_va_macros.add(macroAlexaOff); if_va["p"] = alexaNumPresets; + if_va["seg"] = alexaExposeSegments; #endif #ifndef WLED_DISABLE_MQTT diff --git a/wled00/data/settings_sync.htm b/wled00/data/settings_sync.htm index 43baaf6718..ce9a272c91 100644 --- a/wled00/data/settings_sync.htm +++ b/wled00/data/settings_sync.htm @@ -195,7 +195,8 @@

Alexa Voice Assistant

Emulate Alexa device:
Alexa invocation name:
-Also emulate devices to call the first presets

+Also emulate devices to call the first presets
+Also emulate a device for each active segment:

diff --git a/wled00/set.cpp b/wled00/set.cpp index 925f273aee..48afe275b5 100644 --- a/wled00/set.cpp +++ b/wled00/set.cpp @@ -487,6 +487,7 @@ void handleSettingsSet(AsyncWebServerRequest *request, byte subPage) strlcpy(alexaInvocationName, request->arg(F("AI")).c_str(), 33); t = request->arg(F("AP")).toInt(); if (t >= 0 && t <= 9) alexaNumPresets = t; + alexaExposeSegments = request->hasArg(F("AS")); #endif #ifndef WLED_DISABLE_MQTT diff --git a/wled00/wled.h b/wled00/wled.h index adc5c3d4e7..dcfc439f8f 100644 --- a/wled00/wled.h +++ b/wled00/wled.h @@ -135,7 +135,7 @@ #ifndef WLED_DISABLE_ALEXA #define ESPALEXA_ASYNC #define ESPALEXA_NO_SUBPAGE - #define ESPALEXA_MAXDEVICES 10 + #define ESPALEXA_MAXDEVICES 20 // #define ESPALEXA_DEBUG #include "src/dependencies/espalexa/Espalexa.h" #include "src/dependencies/espalexa/EspalexaDevice.h" @@ -446,6 +446,7 @@ WLED_GLOBAL bool irApplyToAllSelected _INIT(true); //apply IR or ESP-NOW to all WLED_GLOBAL bool alexaEnabled _INIT(false); // enable device discovery by Amazon Echo WLED_GLOBAL char alexaInvocationName[33] _INIT("Light"); // speech control name of device. Choose something voice-to-text can understand WLED_GLOBAL byte alexaNumPresets _INIT(0); // number of presets to expose to Alexa, starting from preset 1, up to 9 +WLED_GLOBAL bool alexaExposeSegments _INIT(false); // expose each segment as a separate Alexa device #endif WLED_GLOBAL uint16_t realtimeTimeoutMs _INIT(2500); // ms timeout of realtime mode before returning to normal mode diff --git a/wled00/xml.cpp b/wled00/xml.cpp index 0d3468f9ea..c44c746900 100644 --- a/wled00/xml.cpp +++ b/wled00/xml.cpp @@ -517,6 +517,7 @@ void getSettingsJS(byte subPage, Print& settingsScript) printSetFormValue(settingsScript,PSTR("AI"),alexaInvocationName); printSetFormCheckbox(settingsScript,PSTR("SA"),notifyAlexa); printSetFormValue(settingsScript,PSTR("AP"),alexaNumPresets); + printSetFormCheckbox(settingsScript,PSTR("AS"),alexaExposeSegments); #else settingsScript.print(F("toggle('Alexa');")); // hide Alexa settings #endif From 902aa8f3b33b6d1bd215d64c575441fa3f6b96d1 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 9 Apr 2026 07:12:50 +0000 Subject: [PATCH 3/3] Address code review feedback: extract helper, add bounds check, improve clarity - Extract mapSegDevToSegIndex() helper function to eliminate duplicated segment-to-device mapping logic - Add bounds validation (return -1) when segment device offset exceeds active segment count (handles segments deactivated after discovery) - Simplify opacity logic: only set to 255 if currently 0 - Extract presetEnd variable to avoid repeated ternary expression - Reduce String heap fragmentation in segment name construction - Add comment documenting ESPALEXA_MAXDEVICES rationale Agent-Logs-Url: https://github.com/wled/WLED/sessions/2bb549aa-be26-40ef-870e-20a7379d69e0 Co-authored-by: netmindz <442066+netmindz@users.noreply.github.com> --- wled00/alexa.cpp | 34 +++++++++++++++++++++------------- wled00/wled.h | 2 +- 2 files changed, 22 insertions(+), 14 deletions(-) diff --git a/wled00/alexa.cpp b/wled00/alexa.cpp index 06d6fd7be1..f49a224f60 100644 --- a/wled00/alexa.cpp +++ b/wled00/alexa.cpp @@ -15,6 +15,19 @@ void onAlexaChange(EspalexaDevice* dev); // index of the first segment device in the Espalexa device list (0 = no segment devices) static unsigned alexaSegmentDeviceStart = 0; +// map a segment device offset to an actual segment index, skipping inactive segments +// returns -1 if the offset is out of range +static int mapSegDevToSegIndex(unsigned segDevIdx) +{ + unsigned activeCount = 0; + for (unsigned i = 0; i < strip.getSegmentsNum(); i++) { + if (!strip.getSegment(i).isActive()) continue; + if (activeCount == segDevIdx) return i; + activeCount++; + } + return -1; +} + void alexaInit() { if (!alexaEnabled || !WLED_CONNECTED) return; @@ -45,7 +58,8 @@ void alexaInit() for (unsigned i = 0; i < strip.getSegmentsNum(); i++) { Segment& seg = strip.getSegment(i); if (!seg.isActive()) continue; - String segName = seg.name ? String(seg.name) : (String(F("Segment ")) + String(i)); + String segName(seg.name ? seg.name : ""); + if (!segName.length()) segName = String(F("Segment ")) + String(i); EspalexaDevice* dev = new EspalexaDevice(segName.c_str(), onAlexaChange, EspalexaDeviceType::extendedcolor); espalexa.addDevice(dev); } @@ -70,21 +84,14 @@ void onAlexaChange(EspalexaDevice* dev) if (isSegmentDevice) { - // map device index to segment index (skipping inactive segments, same order as in alexaInit) - unsigned segDevIdx = devId - alexaSegmentDeviceStart; - unsigned segIdx = 0; - unsigned activeCount = 0; - for (unsigned i = 0; i < strip.getSegmentsNum(); i++) { - if (!strip.getSegment(i).isActive()) continue; - if (activeCount == segDevIdx) { segIdx = i; break; } - activeCount++; - } + int segIdx = mapSegDevToSegIndex(devId - alexaSegmentDeviceStart); + if (segIdx < 0) return; // segment was deactivated after discovery Segment& seg = strip.getSegment(segIdx); if (m == EspalexaDeviceProperty::on) { seg.setOption(SEG_OPTION_ON, true); - seg.setOpacity(seg.opacity ? seg.opacity : 255); + if (!seg.opacity) seg.setOpacity(255); // ensure segment is visible when turned on stateUpdated(CALL_MODE_ALEXA); } else if (m == EspalexaDeviceProperty::off) @@ -137,6 +144,9 @@ void onAlexaChange(EspalexaDevice* dev) return; } + // boundary for preset device range (excludes segment devices) + unsigned presetEnd = alexaSegmentDeviceStart ? alexaSegmentDeviceStart : espalexa.getDeviceCount(); + // original behavior: main device and preset devices if (m == EspalexaDeviceProperty::on) { @@ -157,7 +167,6 @@ void onAlexaChange(EspalexaDevice* dev) } else // switch-on behavior for preset devices { // turn off other preset devices - unsigned presetEnd = alexaSegmentDeviceStart ? alexaSegmentDeviceStart : espalexa.getDeviceCount(); for (unsigned i = 1; i < presetEnd; i++) { if (i == devId) continue; @@ -182,7 +191,6 @@ void onAlexaChange(EspalexaDevice* dev) // below for loop stops Alexa from complaining if macroAlexaOff does not actually turn off } // set main and preset devices to off, but not segment devices - unsigned presetEnd = alexaSegmentDeviceStart ? alexaSegmentDeviceStart : espalexa.getDeviceCount(); for (unsigned i = 0; i < presetEnd; i++) { espalexa.getDevice(i)->setValue(0); diff --git a/wled00/wled.h b/wled00/wled.h index dcfc439f8f..45d93aa330 100644 --- a/wled00/wled.h +++ b/wled00/wled.h @@ -135,7 +135,7 @@ #ifndef WLED_DISABLE_ALEXA #define ESPALEXA_ASYNC #define ESPALEXA_NO_SUBPAGE - #define ESPALEXA_MAXDEVICES 20 + #define ESPALEXA_MAXDEVICES 20 // 1 main + up to 9 presets + up to 10 segment devices // #define ESPALEXA_DEBUG #include "src/dependencies/espalexa/Espalexa.h" #include "src/dependencies/espalexa/EspalexaDevice.h"