diff --git a/usermods/usermod_mqtt_animated_staircase/MQTT_animated_staircase.cpp b/usermods/usermod_mqtt_animated_staircase/MQTT_animated_staircase.cpp new file mode 100644 index 0000000000..ce9ede2f52 --- /dev/null +++ b/usermods/usermod_mqtt_animated_staircase/MQTT_animated_staircase.cpp @@ -0,0 +1,734 @@ +/* + * Usermod for detecting people entering/leaving a staircase and switching the + * staircase on/off. + * + * Edit the Animated_Staircase_config.h file to compile this usermod for your + * specific configuration. + * + * See the accompanying README.md file for more info. + */ +#pragma once +#include "wled.h" + +class MQTT_Animated_Staircase : public Usermod { + private: + + /* configuration (available in API and stored in flash) */ + bool enabled = true; // Enable this usermod + unsigned long segment_delay_ms = 150; // Time between switching each segment + unsigned long on_time_ms = 30000; // The time for the light to stay on + int8_t topPIRorTriggerPin = -1; // disabled //14 good + int8_t bottomPIRorTriggerPin = -1; // disabled //12 good + bool togglePower = false; // toggle power on/off with staircase on/off + bool mqttOnlyTrigger = true; // Set to true if triggering only through MQTT is desired + bool swipeDirection = SWIPE_UP; // Default to SWIPE_UP + static constexpr bool SWIPE_UP = true; + static constexpr bool SWIPE_DOWN = false; + + bool pendingSolidPresetAfterBlack = false; + unsigned long timeToApplySolidPreset = 0; + const unsigned long delayForSolidPresetMs = 250; + + bool pendingMqttSwipe = false; + unsigned long timeToApplyMqttSwipe = 0; + char pendingMqttAction[16] = ""; // Safely holds the payload text for 250ms + + bool animationActive = false; + bool autoPowerOffActive = false; // Flag to indicate if auto power-off is active + bool triggeredFromMQTT = false; // Flag to indicate if the usermod was triggered from MQTT + + bool initDone = false; + + // Time between checking of the sensors + const unsigned int scanDelay = 500; + + // Lights on or off. + // Flipping this will start a transition. + bool on = false; + bool swipe = swipeDirection; + + // Indicates which Sensor was seen last (to determine the direction when swiping off) + #define LOWER false + #define UPPER true + bool lastSensor = LOWER; + + // Time of the last transition action + unsigned long lastTime = 0; + + // Time of the last sensor check + unsigned long lastScanTime = 0; + + // Last time the lights were switched on or off + unsigned long lastSwitchTime = 0; + + // For handling scheduled preset changes + bool pendingPresetChange = false; + uint8_t pendingPresetId = 0; + unsigned long presetChangeTime = 0; + + // segment id between onIndex and offIndex are on. + // controll the swipe by setting/moving these indices around. + // onIndex must be less than or equal to offIndex + byte onIndex = 0; + byte offIndex = 0; + + // The maximum number of configured segments. + // Dynamically updated based on user configuration. + byte maxSegmentId = 1; + byte minSegmentId = 0; + + // These values are used by the API to read the + // last sensor state, or trigger a sensor + // through the API + bool topSensorRead = false; + bool topSensorWrite = false; + bool bottomSensorRead = false; + bool bottomSensorWrite = false; + bool topSensorState = false; + bool bottomSensorState = false; + + bool haLightState = false; // false = off, true = on + + // strings to reduce flash memory usage (used more than twice) + static const char _name[]; + static const char _enabled[]; + static const char _segmentDelay[]; + static const char _onTime[]; + static const char _topPIRorTrigger_pin[]; + static const char _bottomPIRorTrigger_pin[]; + static const char _togglePower[]; + static const char _mqttOnly[]; + static const char _fadeTime_ms[]; + static const char _fadeTargetBrightness[]; + + +void publishMqttToTopic(const char* topic, const char* message) +{ +#ifndef WLED_DISABLE_MQTT + if (WLED_MQTT_CONNECTED) + { + char fullTopic[128]; + snprintf(fullTopic, sizeof(fullTopic), "%s/%s", mqttDeviceTopic, topic); + mqtt->publish(fullTopic, 0, false, message); + } +#else + (void)topic; + (void)message; +#endif +} + +void applyPresetWithDebug(uint8_t presetId) +{ + Serial.print(F("Applying preset: ")); Serial.println(presetId); + applyPreset(presetId); + currentPreset = presetId; // Reset to the solid preset + yield(); // Give the system time to process +} + + +void publishMqtt(bool bottom, const char* state) +{ +#ifndef WLED_DISABLE_MQTT + //Check if MQTT Connected, otherwise it will crash the 8266 + if (WLED_MQTT_CONNECTED) + { + char subuf[64]; + sprintf_P(subuf, PSTR("%s/motion/%d"), mqttDeviceTopic, (int)bottom); + mqtt->publish(subuf, 0, false, state); + } +#else + (void)bottom; + (void)state; +#endif +} + + + void setIndex(String action) { + minSegmentId = strip.getMainSegmentId(); + maxSegmentId = strip.getLastActiveSegmentId() + 1; + + if (action == "on-up") { + // Start with only the top segment on + onIndex = maxSegmentId - 1; // Last segment (top) + offIndex = maxSegmentId; // Boundary after the last segment + } else if (action == "on-down") { + // Start with only the bottom segment on + onIndex = minSegmentId; // First segment (bottom) + offIndex = minSegmentId + 1; // Boundary after the first segment + } else if (action == "off-up" || action == "off-down") { + // For both off animations, start with all segments (that were on) conceptually on. + // The swipeDirection will determine how they turn off. + onIndex = minSegmentId; + offIndex = maxSegmentId; + Serial.println(F("Set for OFF: onIndex=min, offIndex=max")); + } +} + + + + +bool onMqttMessage(char* topic, char* payload) { +#ifndef WLED_DISABLE_MQTT + if (strlen(topic) == 6 && strncmp_P(topic, PSTR("/swipe"), 6) == 0) { + Serial.print(F("MQTT swipe detected: ")); + Serial.println(payload); + + // 1. Save the payload text so it isn't deleted from memory + strncpy(pendingMqttAction, payload, sizeof(pendingMqttAction) - 1); + pendingMqttAction[sizeof(pendingMqttAction) - 1] = '\0'; + + // 2. Apply the Black preset instantly + applyPresetWithDebug(3); + + // 3. Start your non-blocking timer + pendingMqttSwipe = true; + timeToApplyMqttSwipe = millis(); + + return true; // Message handled! + } +#else + (void)topic; + (void)payload; +#endif + return false; +} + +void onMqttConnect(bool sessionPresent) +{ +#ifndef WLED_DISABLE_MQTT + + (void)sessionPresent; + + // Check if the device topic is set + if (mqttDeviceTopic[0] != 0) + { + char subuf[128]; + + // Subscribe to /swipe + snprintf(subuf, sizeof(subuf), "%s/swipe", mqttDeviceTopic); + mqtt->subscribe(subuf, 0); + + // Subscribe to /ha_light_state + snprintf(subuf, sizeof(subuf), "%s/ha_light_state", mqttDeviceTopic); + mqtt->subscribe(subuf, 0); + } +#else + (void)sessionPresent; +#endif +} + + + +bool checkSensors() +{ + bool sensorChanged = false; + + if ((millis() - lastScanTime) > scanDelay) { + lastScanTime = millis(); + + bottomSensorRead = bottomSensorWrite || + ((bottomPIRorTriggerPin < 0) ? false : digitalRead(bottomPIRorTriggerPin)); + + topSensorRead = topSensorWrite || + ((topPIRorTriggerPin < 0) ? false : digitalRead(topPIRorTriggerPin)); + + // Publish MQTT for bottom sensor if state changed + if (bottomSensorRead != bottomSensorState) + { + bottomSensorState = bottomSensorRead; + sensorChanged = true; + + // Publish to "motion/bottom" with "on" or "off" + publishMqttToTopic("motion/bottom", bottomSensorState ? "on" : "off"); + } + + // Publish MQTT for top sensor if state changed + if (topSensorRead != topSensorState) + { + topSensorState = topSensorRead; + sensorChanged = true; + + publishMqttToTopic("motion/top", topSensorState ? "on" : "off"); + + } + + // Reset API flags + topSensorWrite = false; + bottomSensorWrite = false; + } + + return sensorChanged; +} + + + + + +void autoPowerOff() { + // This function is called when 'on' is true, and no animations are active. + // It checks if it's time to start the OFF sequence. + + if (!on) return; // Should be true if called from loop as intended, but as a safeguard. + + if ((millis() - lastSwitchTime) > on_time_ms) { + Serial.println(F("Auto power-off: Timer expired, initiating OFF sequence.")); + + // --- Initiate the OFF sequence --- + // 'on' is already true. Now set it to false as we start the turn-off process. + // However, the global 'on' state should reflect that the system *was* on and is now *turning* off. + // The 'on' variable in updateSwipe (if (on) else {}) will control ON vs OFF animation steps. + // So, we set the global 'on' to false here to signal that the period of "being fully on" is over. + on = false; + + animationActive = true; // Enable animation for the OFF sequence + autoPowerOffActive = true; // Flag that the auto-off initiated animation is running + lastTime = millis(); // Reset segment timer for the start of the OFF animation in updateSwipe + + // Determine OFF direction for FIFO (same logic as before) + if (lastSensor == UPPER) { // Lights were turned ON top-to-bottom + swipeDirection = SWIPE_DOWN; // For FIFO OFF top-to-bottom + Serial.println(F("AutoPowerOff: lastSensor UPPER. Setting OFF swipe to SWIPE_DOWN.")); + setIndex("off-down"); // Initializes onIndex=min, offIndex=max + } else { // lastSensor == LOWER (Lights were turned ON bottom-to-top) + swipeDirection = SWIPE_UP; // For FIFO OFF bottom-to-top + Serial.println(F("AutoPowerOff: lastSensor LOWER. Setting OFF swipe to SWIPE_UP.")); + setIndex("off-up"); // Initializes onIndex=min, offIndex=max + } + // updateSwipe() will be called by loop() to perform the animation steps. + } +} + + + + +void updateSwipe() { + if (!animationActive) return; // If no ON or OFF animation is active, do nothing + + if ((millis() - lastTime) > segment_delay_ms) { + lastTime = millis(); + bool full = false; + + if (on) { // Turning ON logic + // ... (previous ON logic for SWIPE_UP and SWIPE_DOWN) ... + // Example for SWIPE_UP ON: + if (swipeDirection == SWIPE_UP) { + Serial.print(F("SWIPE_UP ON: decreasing onIndex from ")); /* ... */ onIndex = MAX(minSegmentId, onIndex - 1); full = (onIndex == minSegmentId); /* ... */ + } else { // SWIPE_DOWN ON + Serial.print(F("SWIPE_DOWN ON: increasing offIndex from ")); /* ... */ offIndex = MIN(maxSegmentId, offIndex + 1); full = (offIndex == maxSegmentId); /* ... */ + } + + if (full) { + Serial.println(F("All segments ON. Waiting for auto power-off timer.")); + animationActive = false; // ON Animation complete. Loop will now check autoPowerOff condition. + // 'on' is still true. + // 'lastSwitchTime' was set at the start of the ON sequence. + } + } else { // Turning OFF logic (either by autoPowerOff or manual MQTT "off-*") + // ... (previous OFF logic for SWIPE_UP and SWIPE_DOWN, which implements FIFO) ... + // Example for SWIPE_UP OFF: + if (swipeDirection == SWIPE_UP) { // OFF Bottom-to-Top + Serial.print(F("SWIPE_UP OFF (Bottom-to-Top): increasing onIndex from ")); /* ... */ if (onIndex < offIndex) { onIndex++; } full = (onIndex >= offIndex); /* ... */ + } else { // SWIPE_DOWN OFF (Top-to-Bottom) + Serial.print(F("SWIPE_DOWN OFF (Top-to-Bottom): decreasing offIndex from ")); /* ... */ if (offIndex > onIndex) { offIndex--; } full = (offIndex <= onIndex); /* ... */ + } + + if (full) { + Serial.println(F("OFF Animation complete.")); + animationActive = false; + if (autoPowerOffActive) { // Check if this was an auto power off + autoPowerOffActive = false; // Reset flag + } + + onIndex = maxSegmentId; + offIndex = minSegmentId; + + updateSegments(); // Ensure segments are visually off + + Serial.println(F("All segments turned OFF. Applying black preset.")); + strip.timebase = 0; + applyPresetWithDebug(3); // Apply BLACK preset + + pendingSolidPresetAfterBlack = true; + timeToApplySolidPreset = millis() + delayForSolidPresetMs; + // ... (rest of your existing cleanup) ... + return; + } + } + updateSegments(); + } +} + +void updateSegments() +{ + // First, check if we need to turn everything off (final state) + bool allOff = (onIndex >= offIndex); + + for (int i = minSegmentId; i < maxSegmentId; i++) + { + Segment &seg = strip.getSegment(i); + if (!seg.isActive()) continue; // Skip gaps + + bool segmentOn; + if (allOff) { + segmentOn = false; // Force all segments off + } else { + segmentOn = (i >= onIndex && i < offIndex); + } + + seg.setOption(SEG_OPTION_ON, segmentOn); + } + //Serial.println(); + + strip.trigger(); + colorUpdated(CALL_MODE_DIRECT_CHANGE); + stateChanged = true; +} + + +void enable(bool enable) + { + if (enable) + { + onIndex = minSegmentId = strip.getMainSegmentId(); // it may not be the best idea to start with main segment as it may not be the first one + offIndex = maxSegmentId = strip.getLastActiveSegmentId() + 1; + + // shorten the strip transition time to be equal or shorter than segment delay + transitionDelay = segment_delay_ms; + strip.setTransition(segment_delay_ms); + strip.trigger(); + } else { + if (togglePower && !on && offMode) toggleOnOff(); // toggle power on if off + // Restore segment options + for (int i = 0; i <= strip.getLastActiveSegmentId(); i++) { + Segment &seg = strip.getSegment(i); + if (!seg.isActive()) continue; // skip vector gaps + seg.setOption(SEG_OPTION_ON, true); + } + strip.trigger(); // force strip update + stateChanged = true; // inform external devices/UI of change + colorUpdated(CALL_MODE_DIRECT_CHANGE); + } + enabled = enable; + } + + public: + void setup() + { + Serial.println(F("Setting up Animated Staircase usermod...")); + + // Standardize invalid pin numbers to -1 + if (topPIRorTriggerPin < 0) topPIRorTriggerPin = -1; + if (bottomPIRorTriggerPin < 0) bottomPIRorTriggerPin = -1; + + // Allocate pins using PinManager + PinManagerPinType pins[2] = { + { topPIRorTriggerPin }, + { bottomPIRorTriggerPin }, + }; + + // Attempt to allocate pins + if (!PinManager::allocateMultiplePins(pins, 2, PinOwner::UM_AnimatedStaircase)) + { // Fix array size from 4 to 2 + Serial.println(F("Pin allocation failed! Disabling sensors.")); + topPIRorTriggerPin = -1; + bottomPIRorTriggerPin = -1; + enabled = false; // Disable the usermod if pin allocation fails + } + else + { + enabled = true; + } + + + // Set up valid pins as INPUT + if (topPIRorTriggerPin >= 0) { + pinMode(topPIRorTriggerPin, INPUT); + } else { + Serial.println(F("Top PIR Sensor pin invalid, not initialized.")); + } + + if (bottomPIRorTriggerPin >= 0) { + pinMode(bottomPIRorTriggerPin, INPUT); + } else { + Serial.println(F("Bottom PIR Sensor pin invalid, not initialized.")); + } + + + + applyPresetWithDebug(3); // Apply the swipe preset + delay(200); + applyPresetWithDebug(1); + delay(200); + + + minSegmentId = strip.getMainSegmentId(); // Starting main segment ID + maxSegmentId = strip.getLastActiveSegmentId() + 1; + + onIndex = minSegmentId; + offIndex = maxSegmentId -1; // THIS IS IMPORTANT: Initialize offIndex to the end + + enable(enabled); + initDone = true; + Serial.println(F("Animated Staircase usermod setup complete.")); + + } + + + void loop() { + checkSensors(); // Assuming this is your sensor checking logic + + // Handle pending preset change AFTER black preset + if (pendingSolidPresetAfterBlack && millis() >= timeToApplySolidPreset) { + if (!animationActive && !on) { + Serial.println(F("Applying pending solid preset (2).")); + applyPresetWithDebug(2); + } else { + Serial.println(F("New animation started or lights turned on; cancelling pending solid preset (2).")); + } + pendingSolidPresetAfterBlack = false; + } + + if (pendingMqttSwipe && (millis() - timeToApplyMqttSwipe >= delayForSolidPresetMs)) { + + pendingMqttSwipe = false; // Turn the timer off + + applyPresetWithDebug(1); // Apply the actual swipe preset + + // Now run the logic using our saved string! + setIndex(pendingMqttAction); + triggeredFromMQTT = true; + + if (!animationActive || !on) { + if (strcmp(pendingMqttAction, "on-up") == 0) { + swipeDirection = SWIPE_UP; + lastSensor = UPPER; + on = true; + animationActive = true; + autoPowerOffActive = false; + } else if (strcmp(pendingMqttAction, "on-down") == 0) { + swipeDirection = SWIPE_DOWN; + lastSensor = LOWER; + on = true; + animationActive = true; + autoPowerOffActive = false; + } else if (strcmp(pendingMqttAction, "off-up") == 0) { + swipeDirection = SWIPE_UP; + on = false; + animationActive = true; + } else if (strcmp(pendingMqttAction, "off-down") == 0) { + swipeDirection = SWIPE_DOWN; + on = false; + animationActive = true; + } + lastSwitchTime = millis(); + lastTime = millis(); + } + } + + if (!enabled || strip.isUpdating()) return; + + // If lights are ON, and no ON animation is running, and no OFF animation has been started by autoPowerOff + if (on && !animationActive && !autoPowerOffActive) { + autoPowerOff(); // Renamed for clarity, or keep as autoPowerOff + } + + updateSwipe(); // Runs continuously, handling active ON or OFF animations + } + +uint16_t getId() { return USERMOD_ID_MQTT_ANIMATED_STAIRCASE; } + +void appendConfigData() +{ + oappend(SET_F("addCheckbox('mqttOnlyTrigger',mqttOnlyTrigger,'MQTT Only Trigger','Enable triggering only through MQTT');")); +} + + + /** + * addToConfig() can be used to add custom persistent settings to the cfg.json file in the "um" (usermod) object. + * It will be called by WLED when settings are actually saved (for example, LED settings are saved) + * If you want to force saving the current state, use serializeConfig() in your loop(). + * + * CAUTION: serializeConfig() will initiate a filesystem write operation. + * It might cause the LEDs to stutter and will cause flash wear if called too often. + * Use it sparingly and always in the loop, never in network callbacks! + * + * addToConfig() will make your settings editable through the Usermod Settings page automatically. + * + * Usermod Settings Overview: + * - Numeric values are treated as floats in the browser. + * - If the numeric value entered into the browser contains a decimal point, it will be parsed as a C float + * before being returned to the Usermod. The float data type has only 6-7 decimal digits of precision, and + * doubles are not supported, numbers will be rounded to the nearest float value when being parsed. + * The range accepted by the input field is +/- 1.175494351e-38 to +/- 3.402823466e+38. + * - If the numeric value entered into the browser doesn't contain a decimal point, it will be parsed as a + * C int32_t (range: -2147483648 to 2147483647) before being returned to the usermod. + * Overflows or underflows are truncated to the max/min value for an int32_t, and again truncated to the type + * used in the Usermod when reading the value from ArduinoJson. + * - Pin values can be treated differently from an integer value by using the key name "pin" + * - "pin" can contain a single or array of integer values + * - On the Usermod Settings page there is simple checking for pin conflicts and warnings for special pins + * - Red color indicates a conflict. Yellow color indicates a pin with a warning (e.g. an input-only pin) + * - Tip: use int8_t to store the pin value in the Usermod, so a -1 value (pin not set) can be used + * + * See usermod_v2_auto_save.h for an example that saves Flash space by reusing ArduinoJson key name strings + * + * If you need a dedicated settings page with custom layout for your Usermod, that takes a lot more work. + * You will have to add the setting to the HTML, xml.cpp and set.cpp manually. + * See the WLED Soundreactive fork (code and wiki) for reference. https://github.com/atuline/WLED + * + * I highly recommend checking out the basics of ArduinoJson serialization and deserialization in order to use custom settings! + */ + + +void addStaircaseToJsonObject(JsonObject& staircase, bool forJsonState) +{ + + staircase[F("enabled")] = enabled; + staircase[F("mqtt-only-trigger")] = mqttOnlyTrigger; + staircase[F("segment-delay")] = segment_delay_ms; + staircase[F("on-time")] = on_time_ms / 1000; // Convert to seconds for storage + staircase[F("topPIRorTrigger-pin")] = topPIRorTriggerPin; + staircase[F("bottomPIRorTrigger-pin")] = bottomPIRorTriggerPin; + staircase[F("toggle-power")] = togglePower; + //staircase[F("fade-time_ms")] = fadeTime_ms; + //staircase[F("fade-target-brightness")] = fadeTargetBrightness; + +} + +void getUsermodConfigFromJsonObject(JsonObject& staircase) +{ + if (staircase.isNull()) { + Serial.println(F("Error: JSON object is null. Skipping usermod config loading.")); + return; + } + + // Read and assign values from JSON + getJsonValue(staircase[F("enabled")], enabled); + getJsonValue(staircase[F("mqtt-only-trigger")], mqttOnlyTrigger); + getJsonValue(staircase[F("segment-delay")], segment_delay_ms); + + if (staircase.containsKey(F("on-time"))) { + getJsonValue(staircase[F("on-time")], on_time_ms); + on_time_ms *= 1000; // Convert seconds back to milliseconds only if it was set + } + + getJsonValue(staircase[F("topPIRorTrigger-pin")], topPIRorTriggerPin); + getJsonValue(staircase[F("bottomPIRorTrigger-pin")], bottomPIRorTriggerPin); + getJsonValue(staircase[F("toggle-power")], togglePower); + + //getJsonValue(staircase[F("fade-time_ms")], fadeTime_ms); + //getJsonValue(staircase[F("fade-target-brightness")], fadeTargetBrightness); +} + +void addToConfig(JsonObject& root) + { + JsonObject staircase = root.createNestedObject(FPSTR(_name)); + + if (staircase.isNull()) { + staircase = root.createNestedObject(FPSTR(_name)); + } + + addStaircaseToJsonObject(staircase, false); +} + + + /* + * Reads the configuration to internal flash memory before setup() is called. + * + * The function should return true if configuration was successfully loaded or false if there was no configuration. + */ + bool readFromConfig(JsonObject& root) +{ + Serial.print(F("TopP Pin in readFromConfig: ")); + Serial.println(topPIRorTriggerPin); + + JsonObject staircase = root[FPSTR(_name)]; + if (staircase.isNull()) { + Serial.println(F("No config found. Using defaults.")); + return false; + } + + int8_t oldTopPin = topPIRorTriggerPin; + int8_t oldBottomPin = bottomPIRorTriggerPin; + + // Load the new config from the WLED UI + // NOTE: This automatically overwrites topPIRorTriggerPin and bottomPIRorTriggerPin with the new UI values! + getUsermodConfigFromJsonObject(staircase); + + int8_t newTopPin = topPIRorTriggerPin; + int8_t newBottomPin = bottomPIRorTriggerPin; + + // Handle pin changes dynamically + if (initDone) { + if (newTopPin != oldTopPin || newBottomPin != oldBottomPin) { // <-- ADDED OPENING BRACKET + Serial.println(F("Pins have changed, reallocating...")); + + // Deallocate OLD pins (Must use old variables here!) + PinManager::deallocatePin(oldTopPin, PinOwner::UM_AnimatedStaircase); + PinManager::deallocatePin(oldBottomPin, PinOwner::UM_AnimatedStaircase); + + // Attempt to reallocate NEW pins + PinManagerPinType pins[2] = { + { newTopPin }, + { newBottomPin }, + }; + + if (!PinManager::allocateMultiplePins(pins, 2, PinOwner::UM_AnimatedStaircase)) { + Serial.println(F("Pin reallocation failed! Disabling sensors.")); + topPIRorTriggerPin = -1; + bottomPIRorTriggerPin = -1; + enabled = false; // Disable the usermod if pin allocation fails + return true; // Exit early, don't call setup() + } + + setup(); + } + } else { + // First run, just assign values + topPIRorTriggerPin = newTopPin; + bottomPIRorTriggerPin = newBottomPin; + } + + return !staircase[FPSTR(_togglePower)].isNull(); +} + + + /* + * Shows the delay between steps and power-off time in the "info" + * tab of the web-UI. + */ + void addToJsonInfo(JsonObject& root) + { + JsonObject user = root["u"]; + if (user.isNull()) { + user = root.createNestedObject("u"); + } + + JsonArray infoArr = user.createNestedArray(FPSTR(_name)); // name + + String uiDomString = F(""); + infoArr.add(uiDomString); + } +}; + +// strings to reduce flash memory usage (used more than twice) +const char MQTT_Animated_Staircase::_name[] PROGMEM = "staircase"; +const char MQTT_Animated_Staircase::_enabled[] PROGMEM = "enabled"; +const char MQTT_Animated_Staircase::_segmentDelay[] PROGMEM = "segment-delay-ms"; +const char MQTT_Animated_Staircase::_onTime[] PROGMEM = "on-time-s"; +const char MQTT_Animated_Staircase::_topPIRorTrigger_pin[] PROGMEM = "topPIRorTrigger_pin"; +const char MQTT_Animated_Staircase::_bottomPIRorTrigger_pin[] PROGMEM = "bottomPIRorTrigger_pin"; +const char MQTT_Animated_Staircase::_togglePower[] PROGMEM = "toggle-on-off"; +const char MQTT_Animated_Staircase::_mqttOnly[] PROGMEM = "mqtt-only-trigger"; + +// 1. Create a living instance of your class (you can name this variable whatever you want) +MQTT_Animated_Staircase myStaircaseMod; + +REGISTER_USERMOD(myStaircaseMod); diff --git a/usermods/usermod_mqtt_animated_staircase/README.md b/usermods/usermod_mqtt_animated_staircase/README.md new file mode 100644 index 0000000000..6c3ee9f9fd --- /dev/null +++ b/usermods/usermod_mqtt_animated_staircase/README.md @@ -0,0 +1,112 @@ +# MQTT Animated Staircase Usermod + +This usermod is a highly advanced, MQTT-enabled version of the standard WLED Animated Staircase. It allows you to trigger stunning, sequential staircase LED animations using physical motion sensors (PIR/Ultrasonic) OR remotely via MQTT payloads from Home Assistant, Node-RED, or other smart home hubs. + +## ๐ŸŒŸ Features +* **Dual Trigger Support:** Trigger animations using physical top/bottom sensors, or digitally via MQTT. +* **WLED 0.16+ Ready:** Built entirely on the new WLED v2 Usermod architecture for seamless compiling. +* **Smart Pin Management:** Safely reserves hardware pins to prevent conflicts with LED data lines. +* **Dynamic MQTT Integration:** Seamlessly uses WLED's native MQTT settingsโ€”no hardcoded topics! + +## ๐Ÿ› ๏ธ Hardware Requirements +* Any ESP8266 or ESP32 microcontroller (Classic, S2, S3, or C3). +* Addressable LED strip installed on staircase steps. +* (Optional) 2x PIR motion sensors or ultrasonic distance sensors for physical triggering. + +## ๐Ÿš€ Installation (WLED 0.16+) + +Because this usermod uses the modern WLED v0.16 library structure, you do not need to modify any core WLED files to install it. + +1. Open your `platformio_override.ini` (or `platformio.ini`) file. +2. Add the usermod to your specific board environment: + ```ini + custom_usermods = MQTT_Animated_Staircase + ``` + Compile and flash using PlatformIO. + +## โš™๏ธ Configuration + +Once flashed, navigate to Config > Usermods in the WLED web interface. You will find the following settings: + +* Top Sensor Pin: The GPIO pin connected to the top stair sensor. + +* Bottom Sensor Pin: The GPIO pin connected to the bottom stair sensor. + +(Note: Ensure your standard WLED MQTT settings under Config > Sync Interfaces are filled out and connected to your broker). +๐Ÿ“ก MQTT API + +This usermod dynamically listens to your configured WLED Device Topic. It appends /staircase/trigger to your root topic. +Trigger the Stairs + +* Topic: [Your_WLED_Device_Topic]/staircase/trigger (Example: wled/stairs/staircase/trigger) + +* Payloads: + +* `top` - Triggers the animation starting from the top step downwards. +* `bottom` - Triggers the animation starting from the bottom step upwards. +* `ON` - Triggers a default activation. + +## ๐ŸŽจ Required WLED Presets + +For the usermod to know how your stairs are physically wired and how to animate them, you must create two specific presets in the WLED interface. The usermod dynamically reads these presets to execute the sequential swipe. + +### 1. The "Staircase Layout" (Save strictly as Preset ID 1) +This preset teaches the usermod how many steps you have and how many LEDs are on each step. +* Open the WLED UI and go to the **Segments** tab. +* Create a distinct segment for *every single step* on your staircase (e.g., Segment 0: LEDs 0-12, Segment 1: LEDs 12-24, etc.). +* Set your desired colors, brightness, and effects for the stairs. +* Go to the **Presets** tab, click **Create Preset**, and save it explicitly as **ID 1**. *(Check the "Save to ID" box to ensure it forces it to slot 1).* + +### 2. The "Blackout" State (Save strictly as Preset ID 3) --> this is to avoid a bug in which the whole strip flashes when it's turned "off". it's in WLED itself, not the usermod +This preset is used by the usermod to instantly clear the stairs before triggering a new directional swipe. +* While your step segments from Preset 1 are still active, change the primary color of all segments to completely **Black** (or toggle the segments off) +* Go to the **Presets** tab, click **Create Preset**, and save this explicitly as **ID 3**. + +## ๐Ÿก Home Assistant Integration + +If you use Home Assistant, you can easily integrate your physical staircase sensors and trigger animations via YAML. + +### 1. Motion Sensors (`configuration.yaml`) +If you have physical sensors wired to the ESP32, the usermod publishes their state to MQTT. You can read them in Home Assistant by adding this to your `configuration.yaml` (replace `` with your actual WLED MQTT topic): + +```yaml +mqtt: + binary_sensor: + - unique_id: landing_staircase_motion_sensor + name: "Landing Staircase Motion Sensor (Top)" + state_topic: "/motion/0" + payload_on: "on" + payload_off: "off" + qos: 1 + device_class: motion + + - unique_id: lounge_staircase_motion_sensor + name: "Lounge Staircase Motion Sensor (Bottom)" + state_topic: "/motion/1" + payload_on: "on" + payload_off: "off" + qos: 1 + device_class: motion + + +alias: "Staircase โ€“ Swipe Down from Landing" +description: "Triggers WLED swipe-down animation via MQTT" +mode: single +triggers: + - trigger: state + entity_id: binary_sensor.landing_staircase_motion_sensor + from: "off" + to: "on" +conditions: + - condition: state + entity_id: light.staircase_light + state: "off" +actions: + - action: mqtt.publish + data: + topic: "/swipe" + payload: "on-down" + - delay: + seconds: 30 + +Created by watak \ No newline at end of file diff --git a/usermods/usermod_mqtt_animated_staircase/library.json b/usermods/usermod_mqtt_animated_staircase/library.json new file mode 100644 index 0000000000..2dbb6b3ac4 --- /dev/null +++ b/usermods/usermod_mqtt_animated_staircase/library.json @@ -0,0 +1,9 @@ +{ + "name": "MQTT_Animated_Staircase", + "version": "1.0.0", + "description": "Custom animated staircase usermod", + "build": { + "srcDir": ".", + "libArchive": false + } +} \ No newline at end of file diff --git a/wled00/const.h b/wled00/const.h index 70373316fd..c79aa83736 100644 --- a/wled00/const.h +++ b/wled00/const.h @@ -230,6 +230,7 @@ static_assert(WLED_MAX_BUSSES <= 32, "WLED_MAX_BUSSES exceeds hard limit"); #define USERMOD_ID_RF433 56 //Usermod "usermod_v2_RF433.h" #define USERMOD_ID_BRIGHTNESS_FOLLOW_SUN 57 //Usermod "usermod_v2_brightness_follow_sun.h" #define USERMOD_ID_USER_FX 58 //Usermod "user_fx" +#define USERMOD_ID_MQTT_ANIMATED_STAIRCASE 59 // Usermod "usermod_mqtt_animated_staircase.h". //Wifi encryption type #ifdef WLED_ENABLE_WPA_ENTERPRISE diff --git a/wled00/usermods_list.cpp b/wled00/usermods_list.cpp new file mode 100644 index 0000000000..6d51b2e597 --- /dev/null +++ b/wled00/usermods_list.cpp @@ -0,0 +1,482 @@ +#include "wled.h" +/* + * Register your v2 usermods here! + * (for v1 usermods using just usermod.cpp, you can ignore this file) + */ + +/* + * Add/uncomment your usermod filename here (and once more below) + * || || || + * \/ \/ \/ + */ +//#include "../usermods/EXAMPLE_v2/usermod_v2_example.h" + +#ifdef USERMOD_BATTERY + #include "../usermods/Battery/usermod_v2_Battery.h" +#endif + +#ifdef USERMOD_DALLASTEMPERATURE + #include "../usermods/Temperature/usermod_temperature.h" +#endif + +#ifdef USERMOD_SHT +#include "../usermods/sht/usermod_sht.h" +#endif + +#ifdef USERMOD_SN_PHOTORESISTOR + #include "../usermods/SN_Photoresistor/usermod_sn_photoresistor.h" +#endif + +#ifdef USERMOD_PWM_FAN + // requires DALLASTEMPERATURE or SHT included before it + #include "../usermods/PWM_fan/usermod_PWM_fan.h" +#endif + +#ifdef USERMOD_BUZZER + #include "../usermods/buzzer/usermod_v2_buzzer.h" +#endif + +#ifdef USERMOD_SENSORSTOMQTT + #include "../usermods/sensors_to_mqtt/usermod_v2_SensorsToMqtt.h" +#endif + +#ifdef USERMOD_PIRSWITCH + #include "../usermods/PIR_sensor_switch/usermod_PIR_sensor_switch.h" +#endif + +#ifdef USERMOD_BH1750 + #include "../usermods/BH1750_v2/usermod_bh1750.h" +#endif + +// BME280 v2 usermod. Define "USERMOD_BME280" in my_config.h +#ifdef USERMOD_BME280 + #include "../usermods/BME280_v2/usermod_bme280.h" +#endif + +#ifdef USERMOD_BME68X + #include "../usermods/BME68X_v2/usermod_bme68x.h" +#endif + + +#ifdef USERMOD_FOUR_LINE_DISPLAY + #include "../usermods/usermod_v2_four_line_display_ALT/usermod_v2_four_line_display_ALT.h" +#endif + +#ifdef USERMOD_ROTARY_ENCODER_UI + #include "../usermods/usermod_v2_rotary_encoder_ui_ALT/usermod_v2_rotary_encoder_ui_ALT.h" +#endif + +#ifdef USERMOD_AUTO_SAVE + #include "../usermods/usermod_v2_auto_save/usermod_v2_auto_save.h" +#endif + +#ifdef USERMOD_DHT + #include "../usermods/DHT/usermod_dht.h" +#endif + +#ifdef USERMOD_VL53L0X_GESTURES + #include "../usermods/VL53L0X_gestures/usermod_vl53l0x_gestures.h" +#endif + +#ifdef USERMOD_ANIMATED_STAIRCASE + #include "../usermods/Animated_Staircase/Animated_Staircase.h" +#endif + +#ifdef USERMOD_MULTI_RELAY + #include "../usermods/multi_relay/usermod_multi_relay.h" +#endif + +#ifdef USERMOD_RTC + #include "../usermods/RTC/usermod_rtc.h" +#endif + +#ifdef USERMOD_ELEKSTUBE_IPS + #include "../usermods/EleksTube_IPS/usermod_elekstube_ips.h" +#endif + +#ifdef USERMOD_ROTARY_ENCODER_BRIGHTNESS_COLOR + #include "../usermods/usermod_rotary_brightness_color/usermod_rotary_brightness_color.h" +#endif + +#ifdef RGB_ROTARY_ENCODER + #include "../usermods/rgb-rotary-encoder/rgb-rotary-encoder.h" +#endif + +#ifdef USERMOD_ST7789_DISPLAY + #include "../usermods/ST7789_display/ST7789_Display.h" +#endif + +#ifdef USERMOD_PIXELS_DICE_TRAY + #include "../usermods/pixels_dice_tray/pixels_dice_tray.h" +#endif + +#ifdef USERMOD_SEVEN_SEGMENT + #include "../usermods/seven_segment_display/usermod_v2_seven_segment_display.h" +#endif + +#ifdef USERMOD_SSDR + #include "../usermods/seven_segment_display_reloaded/usermod_seven_segment_reloaded.h" +#endif + +#ifdef USERMOD_CRONIXIE + #include "../usermods/Cronixie/usermod_cronixie.h" +#endif + +#ifdef QUINLED_AN_PENTA + #include "../usermods/quinled-an-penta/quinled-an-penta.h" +#endif + +#ifdef USERMOD_WIZLIGHTS + #include "../usermods/wizlights/wizlights.h" +#endif + +#ifdef USERMOD_WIREGUARD + #include "../usermods/wireguard/wireguard.h" +#endif + +#ifdef USERMOD_WORDCLOCK + #include "../usermods/usermod_v2_word_clock/usermod_v2_word_clock.h" +#endif + +#ifdef USERMOD_MY9291 + #include "../usermods/MY9291/usermode_MY9291.h" +#endif + +#ifdef USERMOD_SI7021_MQTT_HA + #include "../usermods/Si7021_MQTT_HA/usermod_si7021_mqtt_ha.h" +#endif + +#ifdef USERMOD_SMARTNEST + #include "../usermods/smartnest/usermod_smartnest.h" +#endif + +#ifdef USERMOD_AUDIOREACTIVE + #include "../usermods/audioreactive/audio_reactive.h" +#endif + +#ifdef USERMOD_ANALOG_CLOCK + #include "../usermods/Analog_Clock/Analog_Clock.h" +#endif + +#ifdef USERMOD_PING_PONG_CLOCK + #include "../usermods/usermod_v2_ping_pong_clock/usermod_v2_ping_pong_clock.h" +#endif + +#ifdef USERMOD_ADS1115 + #include "../usermods/ADS1115_v2/usermod_ads1115.h" +#endif + +#ifdef USERMOD_KLIPPER_PERCENTAGE + #include "../usermods/usermod_v2_klipper_percentage/usermod_v2_klipper_percentage.h" +#endif + +#ifdef USERMOD_BOBLIGHT + #include "../usermods/boblight/boblight.h" +#endif + +#ifdef USERMOD_ANIMARTRIX + #include "../usermods/usermod_v2_animartrix/usermod_v2_animartrix.h" +#endif + +#ifdef USERMOD_INTERNAL_TEMPERATURE + #include "../usermods/Internal_Temperature_v2/usermod_internal_temperature.h" +#endif + +#if defined(WLED_USE_SD_MMC) || defined(WLED_USE_SD_SPI) +// This include of SD.h and SD_MMC.h must happen here, else they won't be +// resolved correctly (when included in mod's header only) + #ifdef WLED_USE_SD_MMC + #include "SD_MMC.h" + #elif defined(WLED_USE_SD_SPI) + #include "SD.h" + #include "SPI.h" + #endif + #include "../usermods/sd_card/usermod_sd_card.h" +#endif + +#ifdef USERMOD_PWM_OUTPUTS + #include "../usermods/pwm_outputs/usermod_pwm_outputs.h" +#endif + +#ifdef USERMOD_HTTP_PULL_LIGHT_CONTROL + #include "../usermods/usermod_v2_HttpPullLightControl/usermod_v2_HttpPullLightControl.h" +#endif + +#ifdef USERMOD_MPU6050_IMU + #include "../usermods/mpu6050_imu/usermod_mpu6050_imu.h" +#endif + +#ifdef USERMOD_MPU6050_IMU + #include "../usermods/mpu6050_imu/usermod_gyro_surge.h" +#endif + +#ifdef USERMOD_LDR_DUSK_DAWN + #include "../usermods/LDR_Dusk_Dawn_v2/usermod_LDR_Dusk_Dawn_v2.h" +#endif + +#ifdef USERMOD_POV_DISPLAY + #include "../usermods/pov_display/usermod_pov_display.h" +#endif + +#ifdef USERMOD_STAIRCASE_WIPE + #include "../usermods/stairway_wipe_basic/stairway-wipe-usermod-v2.h" +#endif + +#ifdef USERMOD_MAX17048 + #include "../usermods/MAX17048_v2/usermod_max17048.h" +#endif + +#ifdef USERMOD_TETRISAI + #include "../usermods/TetrisAI_v2/usermod_v2_tetrisai.h" +#endif + +#ifdef USERMOD_AHT10 + #include "../usermods/AHT10_v2/usermod_aht10.h" +#endif + +#ifdef USERMOD_INA226 + #include "../usermods/INA226_v2/usermod_ina226.h" +#endif + +#ifdef USERMOD_LD2410 +#include "../usermods/LD2410_v2/usermod_ld2410.h" +#endif + +#ifdef USERMOD_MQTT_ANIMATED_STAIRCASE + #include "../usermods/usermod_mqtt_animated_staircase/MQTT_Animated_Staircase.h" +#endif + + +void registerUsermods() +{ +/* + * Add your usermod class name here + * || || || + * \/ \/ \/ + */ + //UsermodManager::add(new MyExampleUsermod()); + + #ifdef USERMOD_BATTERY + UsermodManager::add(new UsermodBattery()); + #endif + + #ifdef USERMOD_DALLASTEMPERATURE + UsermodManager::add(new UsermodTemperature()); + #endif + + #ifdef USERMOD_SN_PHOTORESISTOR + UsermodManager::add(new Usermod_SN_Photoresistor()); + #endif + + #ifdef USERMOD_PWM_FAN + UsermodManager::add(new PWMFanUsermod()); + #endif + + #ifdef USERMOD_BUZZER + UsermodManager::add(new BuzzerUsermod()); + #endif + + #ifdef USERMOD_BH1750 + UsermodManager::add(new Usermod_BH1750()); + #endif + + #ifdef USERMOD_BME280 + UsermodManager::add(new UsermodBME280()); + #endif + + #ifdef USERMOD_BME68X + UsermodManager::add(new UsermodBME68X()); + #endif + + #ifdef USERMOD_SENSORSTOMQTT + UsermodManager::add(new UserMod_SensorsToMQTT()); + #endif + + #ifdef USERMOD_PIRSWITCH + UsermodManager::add(new PIRsensorSwitch()); + #endif + + #ifdef USERMOD_FOUR_LINE_DISPLAY + UsermodManager::add(new FourLineDisplayUsermod()); + #endif + + #ifdef USERMOD_ROTARY_ENCODER_UI + UsermodManager::add(new RotaryEncoderUIUsermod()); // can use USERMOD_FOUR_LINE_DISPLAY + #endif + + #ifdef USERMOD_AUTO_SAVE + UsermodManager::add(new AutoSaveUsermod()); // can use USERMOD_FOUR_LINE_DISPLAY + #endif + + #ifdef USERMOD_DHT + UsermodManager::add(new UsermodDHT()); + #endif + + #ifdef USERMOD_VL53L0X_GESTURES + UsermodManager::add(new UsermodVL53L0XGestures()); + #endif + + #ifdef USERMOD_ANIMATED_STAIRCASE + UsermodManager::add(new Animated_Staircase()); + #endif + + #ifdef USERMOD_MULTI_RELAY + UsermodManager::add(new MultiRelay()); + #endif + + #ifdef USERMOD_RTC + UsermodManager::add(new RTCUsermod()); + #endif + + #ifdef USERMOD_ELEKSTUBE_IPS + UsermodManager::add(new ElekstubeIPSUsermod()); + #endif + + #ifdef USERMOD_ROTARY_ENCODER_BRIGHTNESS_COLOR + UsermodManager::add(new RotaryEncoderBrightnessColor()); + #endif + + #ifdef RGB_ROTARY_ENCODER + UsermodManager::add(new RgbRotaryEncoderUsermod()); + #endif + + #ifdef USERMOD_ST7789_DISPLAY + UsermodManager::add(new St7789DisplayUsermod()); + #endif + + #ifdef USERMOD_PIXELS_DICE_TRAY + UsermodManager::add(new PixelsDiceTrayUsermod()); + #endif + + #ifdef USERMOD_SEVEN_SEGMENT + UsermodManager::add(new SevenSegmentDisplay()); + #endif + + #ifdef USERMOD_SSDR + UsermodManager::add(new UsermodSSDR()); + #endif + + #ifdef USERMOD_CRONIXIE + UsermodManager::add(new UsermodCronixie()); + #endif + + #ifdef QUINLED_AN_PENTA + UsermodManager::add(new QuinLEDAnPentaUsermod()); + #endif + + #ifdef USERMOD_WIZLIGHTS + UsermodManager::add(new WizLightsUsermod()); + #endif + + #ifdef USERMOD_WIREGUARD + UsermodManager::add(new WireguardUsermod()); + #endif + + #ifdef USERMOD_WORDCLOCK + UsermodManager::add(new WordClockUsermod()); + #endif + + #ifdef USERMOD_MY9291 + UsermodManager::add(new MY9291Usermod()); + #endif + + #ifdef USERMOD_SI7021_MQTT_HA + UsermodManager::add(new Si7021_MQTT_HA()); + #endif + + #ifdef USERMOD_SMARTNEST + UsermodManager::add(new Smartnest()); + #endif + + #ifdef USERMOD_AUDIOREACTIVE + UsermodManager::add(new AudioReactive()); + #endif + + #ifdef USERMOD_ANALOG_CLOCK + UsermodManager::add(new AnalogClockUsermod()); + #endif + + #ifdef USERMOD_PING_PONG_CLOCK + UsermodManager::add(new PingPongClockUsermod()); + #endif + + #ifdef USERMOD_ADS1115 + UsermodManager::add(new ADS1115Usermod()); + #endif + + #ifdef USERMOD_KLIPPER_PERCENTAGE + UsermodManager::add(new klipper_percentage()); + #endif + + #ifdef USERMOD_BOBLIGHT + UsermodManager::add(new BobLightUsermod()); + #endif + + #ifdef SD_ADAPTER + UsermodManager::add(new UsermodSdCard()); + #endif + + #ifdef USERMOD_PWM_OUTPUTS + UsermodManager::add(new PwmOutputsUsermod()); + #endif + + #ifdef USERMOD_SHT + UsermodManager::add(new ShtUsermod()); + #endif + + #ifdef USERMOD_ANIMARTRIX + UsermodManager::add(new AnimartrixUsermod("Animartrix", false)); + #endif + + #ifdef USERMOD_INTERNAL_TEMPERATURE + UsermodManager::add(new InternalTemperatureUsermod()); + #endif + + #ifdef USERMOD_HTTP_PULL_LIGHT_CONTROL + UsermodManager::add(new HttpPullLightControl()); + #endif + + #ifdef USERMOD_MPU6050_IMU + static MPU6050Driver mpu6050; UsermodManager::add(&mpu6050); + #endif + + #ifdef USERMOD_GYRO_SURGE + static GyroSurge gyro_surge; UsermodManager::add(&gyro_surge); + #endif + + #ifdef USERMOD_LDR_DUSK_DAWN + UsermodManager::add(new LDR_Dusk_Dawn_v2()); + #endif + + #ifdef USERMOD_STAIRCASE_WIPE + UsermodManager::add(new StairwayWipeUsermod()); + #endif + + #ifdef USERMOD_MAX17048 + UsermodManager::add(new Usermod_MAX17048()); + #endif + + #ifdef USERMOD_TETRISAI + UsermodManager::add(new TetrisAIUsermod()); + #endif + + #ifdef USERMOD_AHT10 + UsermodManager::add(new UsermodAHT10()); + #endif + + #ifdef USERMOD_INA226 + UsermodManager::add(new UsermodINA226()); + #endif + + #ifdef USERMOD_LD2410 + UsermodManager::add(new LD2410Usermod()); + #endif + + #ifdef USERMOD_POV_DISPLAY + UsermodManager::add(new PovDisplayUsermod()); + #endif + + #ifdef USERMOD_MQTT_ANIMATED_STAIRCASE + UsermodManager::add(new MQTT_Animated_Staircase()); + #endif +}