From bb822fef2366c1a4de1945862c81807a9f649050 Mon Sep 17 00:00:00 2001 From: rawframe <44139088+rawframe@users.noreply.github.com> Date: Wed, 10 Dec 2025 04:02:28 +0000 Subject: [PATCH 01/14] Add README for PIR Sensor Switch v2 --- usermods/PIR_sensor_swtich_v2/readme_v2.md | 1 + 1 file changed, 1 insertion(+) create mode 100644 usermods/PIR_sensor_swtich_v2/readme_v2.md diff --git a/usermods/PIR_sensor_swtich_v2/readme_v2.md b/usermods/PIR_sensor_swtich_v2/readme_v2.md new file mode 100644 index 0000000000..6333802ff5 --- /dev/null +++ b/usermods/PIR_sensor_swtich_v2/readme_v2.md @@ -0,0 +1 @@ +# PIR Sensor Switch v2 From 1ee89c313eb2357e1a70ba70badbf14f949c7f2d Mon Sep 17 00:00:00 2001 From: rawframe <44139088+rawframe@users.noreply.github.com> Date: Wed, 10 Dec 2025 04:04:00 +0000 Subject: [PATCH 02/14] Create readme_v2.md --- usermods/PIR_sensor_swtich_v2/PIR_sensor_switch_v2/readme_v2.md | 1 + 1 file changed, 1 insertion(+) create mode 100644 usermods/PIR_sensor_swtich_v2/PIR_sensor_switch_v2/readme_v2.md diff --git a/usermods/PIR_sensor_swtich_v2/PIR_sensor_switch_v2/readme_v2.md b/usermods/PIR_sensor_swtich_v2/PIR_sensor_switch_v2/readme_v2.md new file mode 100644 index 0000000000..8b13789179 --- /dev/null +++ b/usermods/PIR_sensor_swtich_v2/PIR_sensor_switch_v2/readme_v2.md @@ -0,0 +1 @@ + From 1b9116deeab69019202fb1674ddced5d76720ce4 Mon Sep 17 00:00:00 2001 From: rawframe <44139088+rawframe@users.noreply.github.com> Date: Wed, 10 Dec 2025 04:06:57 +0000 Subject: [PATCH 03/14] Delete usermods/PIR_sensor_swtich_v2 directory --- usermods/PIR_sensor_swtich_v2/PIR_sensor_switch_v2/readme_v2.md | 1 - usermods/PIR_sensor_swtich_v2/readme_v2.md | 1 - 2 files changed, 2 deletions(-) delete mode 100644 usermods/PIR_sensor_swtich_v2/PIR_sensor_switch_v2/readme_v2.md delete mode 100644 usermods/PIR_sensor_swtich_v2/readme_v2.md diff --git a/usermods/PIR_sensor_swtich_v2/PIR_sensor_switch_v2/readme_v2.md b/usermods/PIR_sensor_swtich_v2/PIR_sensor_switch_v2/readme_v2.md deleted file mode 100644 index 8b13789179..0000000000 --- a/usermods/PIR_sensor_swtich_v2/PIR_sensor_switch_v2/readme_v2.md +++ /dev/null @@ -1 +0,0 @@ - diff --git a/usermods/PIR_sensor_swtich_v2/readme_v2.md b/usermods/PIR_sensor_swtich_v2/readme_v2.md deleted file mode 100644 index 6333802ff5..0000000000 --- a/usermods/PIR_sensor_swtich_v2/readme_v2.md +++ /dev/null @@ -1 +0,0 @@ -# PIR Sensor Switch v2 From a99f6aeb90cfb6398bfd1d7672926024c1b12358 Mon Sep 17 00:00:00 2001 From: rawframe <44139088+rawframe@users.noreply.github.com> Date: Wed, 10 Dec 2025 04:08:14 +0000 Subject: [PATCH 04/14] Create readme_v2.md --- usermods/PIR_sensor_switch_v2/readme_v2.md | 1 + 1 file changed, 1 insertion(+) create mode 100644 usermods/PIR_sensor_switch_v2/readme_v2.md diff --git a/usermods/PIR_sensor_switch_v2/readme_v2.md b/usermods/PIR_sensor_switch_v2/readme_v2.md new file mode 100644 index 0000000000..8b13789179 --- /dev/null +++ b/usermods/PIR_sensor_switch_v2/readme_v2.md @@ -0,0 +1 @@ + From e707ae21bb7a031ac1b5e96938872fee6086d97b Mon Sep 17 00:00:00 2001 From: rawframe <44139088+rawframe@users.noreply.github.com> Date: Wed, 10 Dec 2025 04:09:11 +0000 Subject: [PATCH 05/14] Add files via upload --- .../PIR_Highlight_Standby | 347 ++++++++++ .../PIR_sensor_switch_v2.cpp | 623 ++++++++++++++++++ ...nfo Page (3x PIRs, 3x Actions example).jpg | Bin 0 -> 16918 bytes ...ngs Page (3x PIRs, 3x Actions example).jpg | Bin 0 -> 25038 bytes usermods/PIR_sensor_switch_v2/library.json | 6 + usermods/PIR_sensor_switch_v2/readme.md | 109 +++ usermods/PIR_sensor_switch_v2/readme_v2.md | 74 ++- 7 files changed, 1158 insertions(+), 1 deletion(-) create mode 100644 usermods/PIR_sensor_switch_v2/PIR_Highlight_Standby create mode 100644 usermods/PIR_sensor_switch_v2/PIR_sensor_switch_v2.cpp create mode 100644 usermods/PIR_sensor_switch_v2/Usermod Info Page (3x PIRs, 3x Actions example).jpg create mode 100644 usermods/PIR_sensor_switch_v2/Usermod Settings Page (3x PIRs, 3x Actions example).jpg create mode 100644 usermods/PIR_sensor_switch_v2/library.json create mode 100644 usermods/PIR_sensor_switch_v2/readme.md diff --git a/usermods/PIR_sensor_switch_v2/PIR_Highlight_Standby b/usermods/PIR_sensor_switch_v2/PIR_Highlight_Standby new file mode 100644 index 0000000000..46756f7d57 --- /dev/null +++ b/usermods/PIR_sensor_switch_v2/PIR_Highlight_Standby @@ -0,0 +1,347 @@ +#pragma once + +#include "wled.h" + +/* + * -------------------- + * Rawframe edit: + * - TESTED ON WLED VS.0.10.1 - WHERE ONLY PRESET 16 SAVES SEGMENTS - some macros may not be needed if this changes. + * - Code has been modified as my usage changed, as such it has poor use of functions vs if thens, but feel free to change it for me :) + * + * Edited to SWITCH between two lighting scenes/modes : STANDBY and HIGHLIGHT + * + * Usage: + * - Standby is the default mode and Highlight is activated when the PIR detects activity. + * - PIR delay now set to same value as Nightlight feature on boot but otherwise controlled as normal. + * - Standby and Highlight brightness can be set on the fly (default values set on boot via macros calling presets). + * - Macros are used to set Standby and Highlight states (macros can load saved presets etc). + * + * - Macro short button press = Highlight state default (used on boot only and sets default brightness). + * - Macro double button press = Standby state default (used on boot only and sets default brightness). + * - Macro long button press = Highlight state (after boot). + * - Macro 16 = Standby state (after boot). + * + * ! It is advised not to set 'Apply preset at boot' or a boot macro (that activates a preset) as we will call our own macros on boot. + * + * - When the strip is off before PIR activates the strip will return to off for Standby mode, and vice versa. + * - When the strip is turned off while in Highlight mode, it will return to standby mode. (This behaviour could be changed easily if for some reason you wanted the lights to go out when the pir is activated). + * - Macros can be chained so you could do almost anything, such as have standby mode also turn on the nightlight function with a new time delay. + * + * Segment Notes: + * - It's easier to save the segment selections in preset than apply via macro while we a limited to preset 16. (Ie, instead of selecting sections at the point of activating standby/highlight modes). + * - Because only preset 16 saves segments, for now we are having to use addiotional macros to control segments where they are involved. Macros can be chained so this works but it would be better if macros also accepted json-api commands. (Testing http api segement behaviour of SS with SB left me a little confused). + * + * Future: + * - Maybe a second timer/timetable that turns on/off standby mode also after set inactivity period / date & times. For now this can be achieved others ways so may not be worth eating more processing power. + * + * -------------------- + * + * This usermod handles PIR sensor states. + * The strip will be switched on and the off timer will be resetted when the sensor goes HIGH. + * When the sensor state goes LOW, the off timer is started and when it expires, the strip is switched off. + * + * + * Usermods allow you to add own functionality to WLED more easily + * See: https://github.com/wled-dev/WLED/wiki/Add-own-functionality + * + * v2 usermods are class inheritance based and can (but don't have to) implement more functions, each of them is shown in this example. + * Multiple v2 usermods can be added to one compilation easily. + * + * Creating a usermod: + * This file serves as an example. If you want to create a usermod, it is recommended to use usermod_v2_empty.h from the usermods folder as a template. + * Please remember to rename the class and file to a descriptive name. + * You may also use multiple .h and .cpp files. + * + * Using a usermod: + * 1. Copy the usermod into the sketch folder (same folder as wled00.ino) + * 2. Register the usermod by adding #include "usermod_filename.h" in the top and registerUsermod(new MyUsermodClass()) in the bottom of usermods_list.cpp + */ + +class PIRsensorSwitch : public Usermod { + private: + // PIR sensor pin + const uint8_t PIRsensorPin = 13; // D7 on D1 mini + // notification mode for stateUpdated() + const byte NotifyUpdateMode = CALL_MODE_NO_NOTIFY; // CALL_MODE_DIRECT_CHANGE + // 1 min delay before switch off after the sensor state goes LOW + uint32_t m_switchOffDelay = 60000; + // off timer start time + uint32_t m_offTimerStart = 0; + // current PIR sensor pin state + byte m_PIRsensorPinState = LOW; + // PIR sensor enabled - ISR attached + bool m_PIRenabled = true; + // temp standby brightness store. initial value set as nightlight default target brightness + byte briStandby _INIT(nightlightTargetBri); + // temp hightlight brightness store. initial value set as current brightness + byte briHighlight _INIT(bri); + // highlight active/deactive monitor + bool highlightActive = false; + // wled on/off state in standby mode + bool standbyoff = false; + + /* + * return or change if new PIR sensor state is available + */ + static volatile bool newPIRsensorState(bool changeState = false, bool newState = false) { + static volatile bool s_PIRsensorState = false; + if (changeState) { + s_PIRsensorState = newState; + } + return s_PIRsensorState; + } + + /* + * PIR sensor state has changed + */ + static void IRAM_ATTR ISR_PIRstateChange() { + newPIRsensorState(true, true); + } + + /* + * switch strip on/off + */ + // now allowing adjustable standby and highlight brightness + void switchStrip(bool switchOn) { + //if (switchOn && bri == 0) { + if (switchOn) { // **pir sensor is on and activated** + //bri = briLast; + if (bri != 0) { // is WLED currently on + if (highlightActive) { // and is Highlight already on + briHighlight = bri; // then update highlight brightness with current brightness + } + else { + briStandby = bri; // else update standby brightness with current brightness + } + } + else { // WLED is currently off + if (!highlightActive) { // and Highlight is not already on + briStandby = briLast; // then update standby brightness with last active brightness (before turned off) + standbyoff = true; + } + else { // and Highlight is already on + briHighlight = briLast; // then set hightlight brightness to last active brightness (before turned off) + } + } + applyMacro(16); // apply highlight lighting without brightness + if (bri != briHighlight) { + bri = briHighlight; // set current highlight brightness to last set highlight brightness + } + stateUpdated(NotifyUpdateMode); + highlightActive = true; // flag highlight is on + } + else { // **pir timer has elapsed** + //briLast = bri; + //bri = 0; + if (bri != 0) { // is WLED currently on + briHighlight = bri; // update highlight brightness with current brightness + if (!standbyoff) { // + bri = briStandby; // set standby brightness to last set standby brightness + } + else { // + briLast = briStandby; // set standby off brightness + bri = 0; // set power off in standby + standbyoff = false; // turn off flag + } + applyMacro(macroLongPress); // apply standby lighting without brightness + } + else { // WLED is currently off + briHighlight = briLast; // set last active brightness (before turned off) to highlight lighting brightness + if (!standbyoff) { // + bri = briStandby; // set standby brightness to last set standby brightness + } + else { // + briLast = briStandby; // set standby off brightness + bri = 0; // set power off in standby + standbyoff = false; // turn off flag + } + applyMacro(macroLongPress); // apply standby lighting without brightness + } + stateUpdated(NotifyUpdateMode); + highlightActive = false; // flag highlight is off + } + } + + /* + * Read and update PIR sensor state. + * Initilize/reset switch off timer + */ + bool updatePIRsensorState() { + if (newPIRsensorState()) { + m_PIRsensorPinState = digitalRead(PIRsensorPin); + + if (m_PIRsensorPinState == HIGH) { + m_offTimerStart = 0; + switchStrip(true); + } + else if (bri != 0) { + // start switch off timer + m_offTimerStart = millis(); + } + newPIRsensorState(true, false); + return true; + } + return false; + } + + /* + * switch off the strip if the delay has elapsed + */ + bool handleOffTimer() { + if (m_offTimerStart > 0) { + if ((millis() - m_offTimerStart > m_switchOffDelay) || bri == 0 ) { // now also checking for manual power off during highlight mode + switchStrip(false); + m_offTimerStart = 0; + return true; + } + } + return false; + } + + public: + //Functions called by WLED + + /* + * setup() is called once at boot. WiFi is not yet connected at this point. + * You can use it to initialize variables, sensors or similar. + */ + void setup() { + // PIR Sensor mode INPUT_PULLUP + pinMode(PIRsensorPin, INPUT_PULLUP); + // assign interrupt function and set CHANGE mode + attachInterrupt(digitalPinToInterrupt(PIRsensorPin), ISR_PIRstateChange, CHANGE); + // set delay to nightlight default duration on boot (after which json PIRoffSec overides if needed) + m_switchOffDelay = (nightlightDelayMins*60000); + applyMacro(macroButton); // apply default highlight lighting + briHighlight = bri; + applyMacro(macroDoublePress); // apply default standby lighting with brightness + briStandby = bri; + } + + + /* + * connected() is called every time the WiFi is (re)connected + * Use it to initialize network interfaces + */ + void connected() { + + } + + + /* + * loop() is called continuously. Here you can check for events, read sensors, etc. + */ + void loop() { + if (!updatePIRsensorState()) { + handleOffTimer(); + } + } + + /* + * addToJsonInfo() can be used to add custom entries to the /json/info part of the JSON API. + * + * Add PIR sensor state and switch off timer duration to jsoninfo + */ + void addToJsonInfo(JsonObject& root) + { + //this code adds "u":{"⏲ PIR sensor state":uiDomString} to the info object + // the value contains a button to toggle the sensor enabled/disabled + JsonObject user = root["u"]; + if (user.isNull()) user = root.createNestedObject("u"); + + JsonArray infoArr = user.createNestedArray("⏲ PIR sensor state"); //name + String uiDomString = ""; + infoArr.add(uiDomString); //value + + //this code adds "u":{"⏲ switch off timer":uiDomString} to the info object + infoArr = user.createNestedArray("⏲ switch off timer"); //name + + // off timer + if (m_offTimerStart > 0) { + uiDomString = ""; + unsigned int offSeconds = (m_switchOffDelay - (millis() - m_offTimerStart)) / 1000; + if (offSeconds >= 3600) { + uiDomString += (offSeconds / 3600); + uiDomString += " hours "; + offSeconds %= 3600; + } + if (offSeconds >= 60) { + uiDomString += (offSeconds / 60); + offSeconds %= 60; + } else if (uiDomString.length() > 0){ + uiDomString += 0; + } + if (uiDomString.length() > 0){ + uiDomString += " min "; + } + uiDomString += (offSeconds); + infoArr.add(uiDomString + " sec"); + } else { + infoArr.add("inactive"); + } + } + + + /* + * addToJsonState() can be used to add custom entries to the /json/state part of the JSON API (state object). + * Values in the state object may be modified by connected clients + * Add "PIRenabled" to json state. This can be used to disable/enable the sensor. + * Add "PIRoffSec" to json state. This can be used to adjust milliseconds . + */ + void addToJsonState(JsonObject& root) + { + root["PIRenabled"] = m_PIRenabled; + root["PIRoffSec"] = (m_switchOffDelay / 1000); + } + + + /* + * readFromJsonState() can be used to receive data clients send to the /json/state part of the JSON API (state object). + * Values in the state object may be modified by connected clients + * Read "PIRenabled" from json state and switch enable/disable the PIR sensor. + * Read "PIRoffSec" from json state and adjust milliseconds . + */ + void readFromJsonState(JsonObject& root) + { + if (root["PIRoffSec"] != nullptr) { + m_switchOffDelay = (1000 * max(60UL, min(43200UL, root["PIRoffSec"].as()))); + } + + if (root["PIRenabled"] != nullptr) { + if (root["PIRenabled"] && !m_PIRenabled) { + attachInterrupt(digitalPinToInterrupt(PIRsensorPin), ISR_PIRstateChange, CHANGE); + newPIRsensorState(true, true); + } + else if(m_PIRenabled) { + detachInterrupt(PIRsensorPin); + } + m_PIRenabled = root["PIRenabled"]; + } + } + + + /* + * getId() allows you to optionally give your V2 usermod an unique ID (please define it in const.h!). + * This could be used in the future for the system to determine whether your usermod is installed. + */ + uint16_t getId() + { + return USERMOD_ID_PIRSWITCH; + } + + //More methods can be added in the future, this example will then be extended. + //Your usermod will remain compatible as it does not need to implement all methods from the Usermod base class! +}; diff --git a/usermods/PIR_sensor_switch_v2/PIR_sensor_switch_v2.cpp b/usermods/PIR_sensor_switch_v2/PIR_sensor_switch_v2.cpp new file mode 100644 index 0000000000..83c76eae43 --- /dev/null +++ b/usermods/PIR_sensor_switch_v2/PIR_sensor_switch_v2.cpp @@ -0,0 +1,623 @@ +/* + * Usermod for PIR sensor motion detection. + * + * This usermod handles PIR sensor states and triggers actions (presets) based on motion. + * It supports multiple PIR sensors and multiple actions, with configurable linking. + * + * Features: + * - Multiple PIR sensors + * - Multiple Actions (On/Off presets, delays) + * - Flexible linking between PIRs and Actions + * - Web UI for configuration and status + * - API for external control + */ + +#include "wled.h" + +// ---------- Configuration defaults ---------- +#ifndef PIR_SENSOR_PIN + #ifdef ARDUINO_ARCH_ESP32 + #define PIR_SENSOR_PIN 23 + #else + #define PIR_SENSOR_PIN 13 + #endif +#endif + +#ifndef PIR_SENSOR_MAX + #define PIR_SENSOR_MAX 2 +#endif + +#ifndef ACTION_MAX + #define ACTION_MAX 2 +#endif + +#ifndef PRESET_FIFO_SIZE + #define PRESET_FIFO_SIZE 16 +#endif + +#ifndef PRESET_SPACING_MS + #define PRESET_SPACING_MS 5 +#endif + +static const char _name[] PROGMEM = "MotionDetection"; + +class MotionDetectionUsermod : public Usermod { +private: + bool initDone = false; + unsigned long lastLoop = 0; + + // PIR config/runtime + bool pirEnabled[PIR_SENSOR_MAX] = { true }; + int8_t pirPins[PIR_SENSOR_MAX] = { PIR_SENSOR_PIN }; + bool pirState[PIR_SENSOR_MAX] = { LOW }; + + // Per-PIR links to actions (initialized at runtime to match ACTION_MAX) + // OPTIMIZATION: Use bitmask instead of 2D bool array to save RAM + uint8_t pirActions[PIR_SENSOR_MAX]; + + // Action profile (presets/timers/contributors). Enabled flag removed from struct usage. + struct ActionProfile { + uint8_t onPreset; + uint8_t offPreset; + uint32_t offDelayMs; + bool contrib[PIR_SENSOR_MAX]; + uint8_t activeCount; + unsigned long offStartMs; + ActionProfile() { + onPreset = 0; + offPreset = 0; + offDelayMs = 600 * 1000UL; + activeCount = 0; + offStartMs = 0; + for (int i = 0; i < PIR_SENSOR_MAX; i++) contrib[i] = false; + } + } actions[ACTION_MAX]; + + // Authoritative simple boolean array for enabled state (PIR-like) + bool actionEnabled[ACTION_MAX] = { true }; + + // Preset FIFO + uint8_t presetFifo[PRESET_FIFO_SIZE]; + uint8_t fifoHead = 0; + uint8_t fifoTail = 0; + unsigned long lastPresetApplyMs = 0; + + + + inline bool fifoEmpty() { return fifoHead == fifoTail; } + inline bool fifoFull() { return ((fifoHead + 1) % PRESET_FIFO_SIZE) == fifoTail; } + + inline void enqueuePreset(uint8_t preset) { + if (preset == 0) return; + // OPTIMIZATION: Prevent flooding by ignoring duplicate sequential requests + if (!fifoEmpty() && presetFifo[(fifoHead + PRESET_FIFO_SIZE - 1) % PRESET_FIFO_SIZE] == preset) return; + + if (fifoFull()) fifoTail = (fifoTail + 1) % PRESET_FIFO_SIZE; // drop oldest + presetFifo[fifoHead] = preset; + fifoHead = (fifoHead + 1) % PRESET_FIFO_SIZE; + } + + void processPresetFifo() { + if (fifoEmpty()) return; + unsigned long now = millis(); + if (now - lastPresetApplyMs < PRESET_SPACING_MS) return; + uint8_t preset = presetFifo[fifoTail]; + fifoTail = (fifoTail + 1) % PRESET_FIFO_SIZE; + lastPresetApplyMs = now; + applyPreset(preset, CALL_MODE_BUTTON_PRESET); + } + +public: + MotionDetectionUsermod() {} + ~MotionDetectionUsermod() {} + + /* + * readFromConfig() is called prior to setup() to read configuration from cfg.json + * You can use it to initialize variables, sensors or similar. + */ + bool readFromConfig(JsonObject &root) override { + if (!root.containsKey(FPSTR(_name))) return false; + JsonObject top = root[FPSTR(_name)]; + + // PIRs + char key[16]; // OPTIMIZATION: Buffer for snprintf to avoid String allocation + for (int i = 0; i < PIR_SENSOR_MAX; i++) { + snprintf(key, sizeof(key), "PIR %d", i + 1); + if (!top.containsKey(key)) continue; + JsonObject p = top[key]; + if (!p.isNull()) { + if (p.containsKey("pin")) pirPins[i] = p["pin"] | pirPins[i]; + if (p.containsKey("enabled")) pirEnabled[i] = p["enabled"].as(); // FIX: Explicit cast + for (int a = 0; a < ACTION_MAX; a++) { + snprintf(key, sizeof(key), "Action %d", a + 1); + if (p.containsKey(key)) { + bool linked = p[key].as(); // FIX: Explicit cast for Link Action checkboxes + if (linked) pirActions[i] |= (1 << a); + else pirActions[i] &= ~(1 << a); + } + } + } + } + + // Actions: read into struct fields and read enabled into boolean array + for (int a = 0; a < ACTION_MAX; a++) { + snprintf(key, sizeof(key), "Action %d", a + 1); + if (!top.containsKey(key)) continue; + JsonObject ap = top[key]; + if (!ap.isNull()) { + if (ap.containsKey("onPreset")) actions[a].onPreset = ap["onPreset"] | actions[a].onPreset; + if (ap.containsKey("offPreset")) actions[a].offPreset = ap["offPreset"] | actions[a].offPreset; + if (ap.containsKey("offSec")) { + uint32_t s = ap["offSec"] | (actions[a].offDelayMs / 1000UL); + actions[a].offDelayMs = s * 1000UL; + } + if (ap.containsKey("enabled")) { + actionEnabled[a] = ap["enabled"].as(); // FIX: Explicit cast + } + } + } + + // If config didn't include enabled flags, ensure sensible defaults + for (int a = 0; a < ACTION_MAX; a++) { + // actionEnabled is initialized to true in class def. + // If config was missing, it stays true. + // If config had "enabled": false, it became false. + // No further action needed here. + } + + return true; + } + + /* + * setup() is called once at boot. WiFi is not yet connected at this point. + * readFromConfig() is called prior to setup() + */ + void setup() override { + // allocate pins and initialize states + for (int i = 0; i < PIR_SENSOR_MAX; i++) { + pirState[i] = LOW; + if (pirPins[i] < 0) { pirEnabled[i] = false; continue; } + if (PinManager::allocatePin(pirPins[i], false, PinOwner::UM_PIR)) { + #ifdef ESP8266 + pinMode(pirPins[i], pirPins[i] == 16 ? INPUT_PULLDOWN_16 : INPUT_PULLUP); + #else + pinMode(pirPins[i], INPUT_PULLDOWN); + #endif + pirState[i] = digitalRead(pirPins[i]); + } else { + DEBUG_PRINT(F("PIR pin allocation failed: ")); + DEBUG_PRINTLN(pirPins[i]); + pirPins[i] = -1; + pirEnabled[i] = false; + } + } + + // initialize action runtime state (pirActions loaded from config, not overwritten here!) + for (int a = 0; a < ACTION_MAX; a++) { + // actionEnabled[a] = true; // REMOVED: Do not overwrite config/state! + actions[a].activeCount = 0; + actions[a].offStartMs = 0; + for (int i = 0; i < PIR_SENSOR_MAX; i++) actions[a].contrib[i] = false; + } + + fifoHead = fifoTail = 0; + lastPresetApplyMs = 0; + + initDone = true; + } + + /* + * loop() is called continuously. Here you can check for events, read sensors, etc. + */ + void loop() override { + if (!initDone) return; + + processPresetFifo(); + + // poll PIRs ~5Hz + if (millis() - lastLoop < 200) return; + lastLoop = millis(); + + for (int i = 0; i < PIR_SENSOR_MAX; i++) { + if (!pirEnabled[i] || pirPins[i] < 0) continue; + + bool pin = digitalRead(pirPins[i]); + if (pin == pirState[i]) continue; + pirState[i] = pin; + + if (pin == HIGH) { + // rising edge: mark contributor and manage action + for (int a = 0; a < ACTION_MAX; a++) { + if (!actionEnabled[a]) continue; // use simple boolean gating (PIR-like) + if (!((pirActions[i] >> a) & 0x01)) continue; // OPTIMIZATION: Bitmask check + + if (!actions[a].contrib[i]) { + actions[a].contrib[i] = true; + actions[a].activeCount++; + } + + // if action just became active, enqueue onPreset; always clear offStartMs + if (actions[a].activeCount == 1) { + // OPTIMIZATION: Removed redundant offStartMs = 0 + if (actions[a].onPreset) enqueuePreset(actions[a].onPreset); + } else { + // OPTIMIZATION: Removed redundant offStartMs = 0 + } + } + } else { + // falling edge: remove contributor, maybe start unified off timer + for (int a = 0; a < ACTION_MAX; a++) { + if (!actionEnabled[a]) continue; + if (!((pirActions[i] >> a) & 0x01)) continue; // OPTIMIZATION: Bitmask check + + if (actions[a].contrib[i]) { + actions[a].contrib[i] = false; + if (actions[a].activeCount > 0) actions[a].activeCount--; + if (actions[a].activeCount == 0) { + actions[a].offStartMs = millis(); + } + } + } + } + } + + // check unified off timers + unsigned long now = millis(); + for (int a = 0; a < ACTION_MAX; a++) { + if (!actionEnabled[a]) continue; + if (actions[a].activeCount == 0) { + if (actions[a].offStartMs > 0 && now - actions[a].offStartMs > actions[a].offDelayMs) { + actions[a].offStartMs = 0; + if (actions[a].offPreset) enqueuePreset(actions[a].offPreset); + } + } else { + // OPTIMIZATION: Removed redundant offStartMs = 0 + } + } + + processPresetFifo(); + } + + /* + * addToJsonInfo() can be used to add custom entries to the /json/info part of the JSON API. + * Creating an "u" object allows you to add custom key/value pairs to the Info section of the WLED web UI. + */ + void addToJsonInfo(JsonObject &root) override { + JsonObject user = root["u"]; + if (user.isNull()) user = root.createNestedObject("u"); + + char buf[64]; // OPTIMIZATION: Buffer for snprintf + // PIR blocks + for (int i = 0; i < PIR_SENSOR_MAX; i++) { + snprintf(buf, sizeof(buf), "PIR %d", i+1); + JsonArray infoArr = user.createNestedArray(buf); + + if (pirPins[i] < 0) { + infoArr.add("⛔ PIR not configured"); + continue; + } + + String uiDomString; + uiDomString = F(""); + + infoArr.add(pirEnabled[i] ? (pirState[i] ? "● motion" : "○ idle") : "⛔ disabled"); + infoArr.add(uiDomString); + + String linked = ""; + bool any = false; + for (int a = 0; a < ACTION_MAX; a++) { + if ((pirActions[i] >> a) & 0x01) { // OPTIMIZATION: Bitmask check + if (any) linked += ", "; + linked += String(a+1); + any = true; + } + } + infoArr.add(any ? String("🔗 actions: ") + linked : "🔗 no actions linked"); + } + + // Action blocks: use PIR-like keys and boolean array + unsigned long now = millis(); + for (int a = 0; a < ACTION_MAX; a++) { + snprintf(buf, sizeof(buf), "Action %d", a+1); + JsonArray infoArr = user.createNestedArray(buf); + + bool en = actionEnabled[a]; + + // status first + String status; + if (!en) { + status = "⛔ disabled"; + } else { + bool active = actions[a].activeCount > 0; + uint32_t remain = 0; + if (!active && actions[a].offStartMs > 0) { + unsigned long end = actions[a].offStartMs + actions[a].offDelayMs; + if (end > now) remain = (end - now) / 1000; + } + if (active) status = "⏱ active"; + else if (remain > 0) status = String("⏱ off in ") + String(remain) + "s"; + else status = "○ inactive"; + } + infoArr.add(status); // MUST be first + + // button second (PIR-style key "actionN") + String uiDomString; + uiDomString = F(""); + infoArr.add(uiDomString); // MUST be second + + // details + String onP = actions[a].onPreset ? String(actions[a].onPreset) : String("none"); + String offP = actions[a].offPreset ? String(actions[a].offPreset) : String("none"); + infoArr.add(String("On: ") + onP + String(" Off: ") + offP); + infoArr.add(String("Delay: ") + String(actions[a].offDelayMs / 1000UL) + "s"); + } + + + } + + /* + * readFromJsonState() can be used to receive data clients send to the /json/state part of the JSON API (state object). + * Values in the state object may be modified by connected clients + */ + void readFromJsonState(JsonObject &root) override { + // accept nested under usermod name or flat keys + JsonObject um = root.containsKey(FPSTR(_name)) ? root[FPSTR(_name)] : root; + bool anyChange = false; + + // PIR keys + for (int i = 0; i < PIR_SENSOR_MAX; i++) { + String k = "pir" + String(i); + if (!um.containsKey(k)) continue; + bool en = um[k].is() ? um[k].as() : (um[k].as() != 0); + if (pirEnabled[i] != en) { + pirEnabled[i] = en; + anyChange = true; + if (!en) { + for (int a = 0; a < ACTION_MAX; a++) { + if (actions[a].contrib[i]) { + actions[a].contrib[i] = false; + if (actions[a].activeCount > 0) actions[a].activeCount--; + } + } + } + } + } + + // Action keys: accept actionN, actionN_on, actionN_off, 1-based actionN + // OPTIMIZATION: Use pointer arithmetic to parse keys without creating Strings + for (JsonPair p : um) { + const char* key = p.key().c_str(); + if (strncmp(key, "action", 6) != 0) continue; + + // extract numeric suffix + const char* ptr = key + 6; // after "action" + if (!isdigit(*ptr)) continue; + + int idx = 0; + while (isdigit(*ptr)) { + idx = idx * 10 + (*ptr - '0'); + ptr++; + } + + // tail after digits (may be "_on", "_off", or empty) + const char* tail = ptr; + if (*tail == '_') tail++; // skip underscore + + // map 1-based keys (action1 -> index 0) if needed + int mappedIdx = -1; + if (idx >= 0 && idx < ACTION_MAX && *tail == '\0') { + mappedIdx = idx; // action0 style + } else if (idx >= 1 && idx <= ACTION_MAX && *tail == '\0') { + mappedIdx = idx - 1; // action1 style -> index 0 + } else if (idx >= 0 && idx < ACTION_MAX) { + mappedIdx = idx; // action0_on style + } else { + continue; + } + + // determine desired enabled state + bool en = false; + bool found = false; + + if (*tail != '\0') { + if (strcasecmp(tail, "on") == 0 || strcasecmp(tail, "enable") == 0) { + en = true; + found = true; + } else if (strcasecmp(tail, "off") == 0 || strcasecmp(tail, "disable") == 0) { + en = false; + found = true; + } else { + // fallback: read value if present + en = um[key].is() ? um[key].as() : (um[key].as() != 0); + found = true; + } + } else { + // no tail: use the provided value (true/false or numeric) + en = um[key].is() ? um[key].as() : (um[key].as() != 0); + found = true; + } + + if (!found) continue; + if (mappedIdx < 0 || mappedIdx >= ACTION_MAX) continue; + + // set simple boolean and clear runtime contributor state when toggling + if (actionEnabled[mappedIdx] != en) { + actionEnabled[mappedIdx] = en; + anyChange = true; + // when disabling, clear contributors and active counts + if (!en) { + for (int i = 0; i < PIR_SENSOR_MAX; i++) { + if (actions[mappedIdx].contrib[i]) { + actions[mappedIdx].contrib[i] = false; + } + } + actions[mappedIdx].activeCount = 0; + actions[mappedIdx].offStartMs = 0; + } else { + // enabling: ensure timers cleared + actions[mappedIdx].offStartMs = 0; + } + } + } + + if (anyChange) stateUpdated(CALL_MODE_BUTTON); + } + + /* + * 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) + */ + void addToConfig(JsonObject &root) override { + JsonObject top = root.createNestedObject(FPSTR(_name)); + + char key[16]; // OPTIMIZATION: Buffer for snprintf + // PIRs + for (int i = 0; i < PIR_SENSOR_MAX; i++) { + snprintf(key, sizeof(key), "PIR %d", i + 1); + JsonObject p = top.createNestedObject(key); + p["pin"] = pirPins[i]; + p["enabled"] = pirEnabled[i]; + for (int a = 0; a < ACTION_MAX; a++) { + snprintf(key, sizeof(key), "Action %d", a + 1); + p[key] = (bool)((pirActions[i] >> a) & 0x01); // OPTIMIZATION: Bitmask check + } + } + + // Actions + for (int a = 0; a < ACTION_MAX; a++) { + snprintf(key, sizeof(key), "Action %d", a + 1); + JsonObject ap = top.createNestedObject(key); + ap["enabled"] = actionEnabled[a]; + ap["onPreset"] = actions[a].onPreset; + ap["offPreset"] = actions[a].offPreset; + ap["offSec"] = actions[a].offDelayMs / 1000UL; + } + } + + /* + * appendConfigData() is called when user enters usermod settings page + * it may add additional metadata for certain entry fields (adding drop down is possible) + */ + void appendConfigData() override { + // Indicator to show the mod is active (will be updated by JS) + oappend(F("addInfo('MotionDetection:PIR 1:enabled',1,'');")); + + // Inject CSS via JS + // Scoped CSS to #pir-root + // Removed border from .um-g to prevent double lines between blocks + // Added border-top and border-bottom to #pir-root for the only desired lines + // OPTIMIZATION: Combined strings + oappend(F("var s=document.createElement('style');s.innerHTML='#pir-root{border-top:1px solid #444;border-bottom:1px solid #444;padding:10px 0;margin:10px 0} #pir-root .um-g{display:flex;flex-wrap:wrap;justify-content:center;gap:10px;margin:4px 0;padding:10px;border-radius:5px;background:#222} #pir-root .um-h{width:100%;text-align:center;font-weight:bold;margin-bottom:5px;color:#fca} #pir-root .um-i{display:flex;flex-direction:column;align-items:center;background:#333;padding:5px;border-radius:4px;min-width:80px;border:1px solid #555} #pir-root .um-i label{font-size:0.8em;margin-bottom:2px;color:#aaa} #pir-root .um-i input[type=\"checkbox\"]{margin:5px} #pir-root .um-i input[type=\"number\"],#pir-root .um-i select{width:70px;text-align:center} #pir-root hr{display:none !important} #pir-root br{display:none}';document.head.appendChild(s);")); + + // Inject Layout Logic + // OPTIMIZATION: Combined strings + oappend(F("function aUM(){" + "var st=document.getElementById('um-status');" + "try {" + "var count=0;" + // Fix Main Heading + "const headers = document.querySelectorAll('h3');" + "let header = null;" + "headers.forEach(h=>{ if(h.textContent.trim()==='MotionDetection'){ header=h; h.textContent='Motion Detection'; h.style.marginTop='20px'; h.style.color=''; h.style.textDecoration='none'; } });" + + // Create Root Container + "if(header && !document.getElementById('pir-root')){" + "const root = document.createElement('div'); root.id='pir-root';" + "header.parentNode.insertBefore(root, header);" + "root.appendChild(header);" + "let next = root.nextSibling;" + "while(next && next.tagName!=='H3' && next.tagName!=='BUTTON' && !(next.tagName==='DIV' && next.className==='overlay')){" + "let curr = next; next = next.nextSibling; root.appendChild(curr);" + "}" + "}" + + // Selector 'f' + "const f=(k)=>{var e=document.getElementsByName(`MotionDetection:${k}`); if(e.length===0)return null; for(var i=0;i{let c=e; if(c.previousSibling&&c.previousSibling.type==='hidden')c=c.previousSibling; if(c.previousSibling&&c.previousSibling.nodeType===3&&c.previousSibling.textContent.trim().length>0)c=c.previousSibling; return c;};" + + // Helper w: FIX: Robustly find hidden inputs by searching backwards + "const w=(e,l)=>{if(!e)return null;const d=document.createElement('div');d.className='um-i';" + "let curr=e.previousSibling; let lblNode=null;" + "if(curr&&curr.type==='hidden') curr=curr.previousSibling;" + "while(curr&&curr.nodeType===3&&!curr.textContent.trim()) curr=curr.previousSibling;" + "if(curr&&curr.nodeType===3) lblNode=curr;" + "if(lblNode) lblNode.remove();" + "const lbl=document.createElement('label');lbl.textContent=l||(lblNode?lblNode.textContent.trim():'');if(lbl.textContent)d.appendChild(lbl);" + // FIX: Search backwards for the hidden input with the same name + "let sib = e.previousSibling;" + "while(sib) {" + "if(sib.type === 'hidden' && sib.name === e.name) {" + "d.appendChild(sib); break;" + "}" + "if(sib.tagName === 'DIV' || sib.tagName === 'H3' || sib.tagName === 'BUTTON') break;" + "sib = sib.previousSibling;" + "}" + "d.appendChild(e);return d;};" + + // Loop for Actions + "for(let i=1;i<=8;i++){" + "const en=f(`Action ${i}:enabled`); if(!en) continue;" + "const g=document.createElement('div');g.className='um-g';g.innerHTML=`
Action ${i}
`;" + "const s=f(`Action ${i}:offSec`); const on=f(`Action ${i}:onPreset`); const off=f(`Action ${i}:offPreset`);" + "const anchor = en || on;" + "if(anchor) anchor.parentNode.insertBefore(g, getStart(anchor));" + "if(en){count++; g.appendChild(w(en, 'Enabled'));}" + "if(s){count++; g.appendChild(w(s, 'Off Delay (s)'));}" + "if(on){count++; g.appendChild(w(on, 'On Preset'));}" + "if(off){count++; g.appendChild(w(off, 'Off Preset'));}" + "}" + + // Loop for PIRs + "for(let i=1;i<=8;i++){" + "const en=f(`PIR ${i}:enabled`); if(!en) continue;" + "const g=document.createElement('div');g.className='um-g';g.innerHTML=`
PIR Sensor ${i}
`;" + "const p=f(`PIR ${i}:pin`);" + "const anchor = en || p;" + "if(anchor) anchor.parentNode.insertBefore(g, getStart(anchor));" + "if(en){count++; g.appendChild(w(en, 'Enabled'));}" + "if(p){count++; g.appendChild(w(p, 'Pin'));}" + "for(let a=1;a<=8;a++){const l=f(`PIR ${i}:Action ${a}`);if(l){count++; g.appendChild(w(l, `Link Action ${a}`));}}" + "}" + + // AGGRESSIVE CLEANUP + "const root = document.getElementById('pir-root');" + "if(root){" + "const children = Array.from(root.children);" + "children.forEach(c => {" + "if(c.tagName!=='H3' && c.tagName!=='BUTTON' && !c.classList.contains('um-g')){ c.remove(); }" + "});" + "let n = root.firstChild; while(n){ let next=n.nextSibling; if(n.nodeType===3){ n.remove(); } n=next; }" + "let prev = root.previousElementSibling; if(prev && prev.tagName==='HR') prev.remove();" + "}" + + "} catch(e) {" + "if(st){st.innerHTML='Error: '+e.message; st.style.display='block'; st.style.color='red';}" + "}" + "} setTimeout(aUM,1000);")); + } + + /* + * getId() allows you to optionally give your V2 usermod an unique ID (please define it in const.h!). + */ + uint16_t getId() override { return USERMOD_ID_PIRSWITCH; } +}; + +// Register instance +static MotionDetectionUsermod motionDetectionUsermod; +REGISTER_USERMOD(motionDetectionUsermod); diff --git a/usermods/PIR_sensor_switch_v2/Usermod Info Page (3x PIRs, 3x Actions example).jpg b/usermods/PIR_sensor_switch_v2/Usermod Info Page (3x PIRs, 3x Actions example).jpg new file mode 100644 index 0000000000000000000000000000000000000000..0ddd967b0ff08926ebe5b8303906179015afd34a GIT binary patch literal 16918 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 F|384ENs<5n literal 0 HcmV?d00001 diff --git a/usermods/PIR_sensor_switch_v2/Usermod Settings Page (3x PIRs, 3x Actions example).jpg b/usermods/PIR_sensor_switch_v2/Usermod Settings Page (3x PIRs, 3x Actions example).jpg new file mode 100644 index 0000000000000000000000000000000000000000..e06f24885b03c481029a7859c334a8a50e527ada GIT binary patch literal 25038 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 literal 0 HcmV?d00001 diff --git a/usermods/PIR_sensor_switch_v2/library.json b/usermods/PIR_sensor_switch_v2/library.json new file mode 100644 index 0000000000..a848bba2f0 --- /dev/null +++ b/usermods/PIR_sensor_switch_v2/library.json @@ -0,0 +1,6 @@ +{ + "name": "PIR_sensor_switch_v2", + "build": { + "libArchive": false + } +} \ No newline at end of file diff --git a/usermods/PIR_sensor_switch_v2/readme.md b/usermods/PIR_sensor_switch_v2/readme.md new file mode 100644 index 0000000000..86497e34d0 --- /dev/null +++ b/usermods/PIR_sensor_switch_v2/readme.md @@ -0,0 +1,109 @@ +# PIR sensor switch + +This usermod-v2 modification allows the connection of a PIR sensor to switch on the LED strip when motion is detected. The switch-off occurs ten minutes after no more motion is detected. + +_Story:_ + +I use the PIR Sensor to automatically turn on the WLED analog clock in my home office room when I am there. +The LED strip is switched [using a relay](https://kno.wled.ge/features/relay-control/) to keep the power consumption low when it is switched off. + +## Web interface + +The info page in the web interface shows the remaining time of the off timer. Usermod can also be temporarily disbled/enabled from the info page by clicking PIR button. + +## Sensor connection + +My setup uses an HC-SR501 or HC-SR602 sensor, an HC-SR505 should also work. + +The usermod uses GPIO13 (D1 mini pin D7) by default for the sensor signal, but can be changed in the Usermod settings page. +[This example page](http://www.esp8266learning.com/wemos-mini-pir-sensor-example.php) describes how to connect the sensor. + +Use the potentiometers on the sensor to set the time delay to the minimum and the sensitivity to about half, or slightly above. +You can also use usermod's off timer instead of sensor's. In such case rotate the potentiometer to its shortest time possible (or use SR602 which lacks such potentiometer). + +## Usermod installation + +**NOTE:** Usermod has been included in master branch of WLED so it can be compiled in directly just by defining `-D USERMOD_PIRSWITCH` and optionally `-D PIR_SENSOR_PIN=16` to override default pin. You can also change the default off time by adding `-D PIR_SENSOR_OFF_SEC=30`. + +## API to enable/disable the PIR sensor from outside. For example from another usermod + +To query or change the PIR sensor state the methods `bool PIRsensorEnabled()` and `void EnablePIRsensor(bool enable)` are available. + +When the PIR sensor state changes an MQTT message is broadcasted with topic `wled/deviceMAC/motion` and message `on` or `off`. +Usermod can also be configured to send just the MQTT message but not change WLED state using settings page as well as responding to motion only at night +(assuming NTP and latitude/longitude are set to determine sunrise/sunset times). + +### There are two options to get access to the usermod instance + +_1._ Include `usermod_PIR_sensor_switch.h` **before** you include other usermods in `usermods_list.cpp' + +or + +_2._ Use `#include "usermod_PIR_sensor_switch.h"` at the top of the `usermod.h` where you need it. + +**Example usermod.h :** + +```cpp +#include "wled.h" + +#include "usermod_PIR_sensor_switch.h" + +class MyUsermod : public Usermod { + //... + + void togglePIRSensor() { + #ifdef USERMOD_PIR_SENSOR_SWITCH + PIRsensorSwitch *PIRsensor = (PIRsensorSwitch::*) UsermodManager::lookup(USERMOD_ID_PIRSWITCH); + if (PIRsensor != nullptr) { + PIRsensor->EnablePIRsensor(!PIRsensor->PIRsensorEnabled()); + } + #endif + } + //... +}; +``` + +### Configuration options + +Usermod can be configured via the Usermods settings page. + +* `PIRenabled` - enable/disable usermod +* `pin` - dynamically change GPIO pin where PIR sensor is attached to ESP +* `PIRoffSec` - number of seconds after PIR sensor deactivates when usermod triggers Off preset (or turns WLED off) +* `on-preset` - preset triggered when PIR activates (if this is 0 it will just turn WLED on) +* `off-preset` - preset triggered when PIR deactivates (if this is 0 it will just turn WLED off) +* `nighttime-only` - enable triggering only between sunset and sunrise (you will need to set up _NTP_, _Lat_ & _Lon_ in Time & Macro settings) +* `mqtt-only` - send only MQTT messages, do not interact with WLED +* `off-only` - only trigger presets or turn WLED on/off if WLED is not already on (displaying effect) +* `notifications` - enable or disable sending notifications to other WLED instances using Sync button +* `HA-discovery` - enable automatic discovery in Home Assistant +* `override` - override PIR input when WLED state is changed using UI +* `domoticz-idx` - Domoticz virtual switch ID (used with MQTT `domoticz/in`) + +Have fun - @gegu & @blazoncek + +## Change log + +2021-04 + +* Adaptation for runtime configuration. + +2021-11 + +* Added information about dynamic configuration options +* Added option to temporary enable/disable usermod from WLED UI (Info dialog) + +2022-11 + +* Added compile time option for off timer. +* Added Home Assistant autodiscovery MQTT broadcast. +* Updated info on compiling. + +2023-?? + +* Override option +* Domoticz virtual switch ID (used with MQTT `domoticz/in`) + +2024-02 + +* Added compile time option to expand number of PIR sensors (they are logically ORed) `-D PIR_SENSOR_MAX_SENSORS=3` diff --git a/usermods/PIR_sensor_switch_v2/readme_v2.md b/usermods/PIR_sensor_switch_v2/readme_v2.md index 8b13789179..a90c7b19c6 100644 --- a/usermods/PIR_sensor_switch_v2/readme_v2.md +++ b/usermods/PIR_sensor_switch_v2/readme_v2.md @@ -1 +1,73 @@ - +# PIR Sensor Switch v2 + +**By rawframe** + +## Background + +Back in the day, I added PIR functionality to WLED before it became a built-in feature. Since then, a PIR usermod was added (credit to @gegu & @blazoncek! for PIR_sensor_switch). + +However, I needed something more flexible for my setup - multiple PIRs triggering different actions in various combinations. The official version works great for simple cases, but when you need complex linking between sensors and actions, macro chains get messy real quick, and moreover we still cannot easily have seemless fx (i.e avoid restarting an fx animation when called again), So I built this. + +## Screenshots + +![alt text]() +![alt text]() + +## What It Does + +### Multiple PIRs +- Support for multiple PIR sensors (configurable up to 8) +- Each PIR can be independently enabled/disabled +- Configure different GPIO pins for each sensor +- Real-time status monitoring in the Web UI + +### Flexible Actions +- Define multiple "Actions" (also configurable up to 8) +- Each Action has: + - **On Preset** - triggers when motion detected + - **Off Preset** - triggers when motion stops (with delay) + - **Off Delay** - how long to wait before triggering the off preset + - **Enable/Disable toggle** - turn actions on/off without losing settings + +### Smart Linking +- Link any PIR to any Action (many-to-many relationships) +- One PIR can trigger multiple Actions +- One Action can be triggered by multiple PIRs +- Actions stay active as long as ANY linked PIR detects motion +- Off timers only start when ALL linked PIRs go idle + +### Web UI +- Toggle PIRs and Actions on/off directly from the Info page +- See motion status in real-time (● motion / ○ idle) +- View countdown timers for off delays +- Config page for full setup (pins, presets, delays, linking) + +### API Control +- Enable/disable PIRs via JSON API: `{"MotionDetection":{"pir0":true}}` +- Enable/disable Actions via JSON API: `{"MotionDetection":{"action0":true}}` +- Integrates with Home Assistant, Node-RED, etc. + +## Use Cases + +This helps if you need: +- Multiple PIRs in different rooms/zones +- Different lighting presets for different sensors +- Combined motion detection (e.g., "turn off only when ALL sensors are idle") +- Independent motion zones that don't interfere with each other +- Complex automation without macro spaghetti + +## Technical Bits + +- Uses a preset FIFO queue to prevent flooding +- Optimized RAM usage with bitmasks instead of 2D arrays, but you could change this if you need more than 8 pirs or actions. +- Contributor tracking system ensures Actions behave correctly with multiple PIRs +- Persistent configuration saved to `cfg.json` +- Fully compatible with WLED's preset system + +## Notes + +- To add more pirs or actions, just change the defines in the usermod.h + - PIR_SENSOR_MAX + - ACTION_MAX + +- Add if anyone wants to add the other options from the @gegu & @blazoncek usermod then feel free to do so. From c328296135c86b78893e947043e40126bfeafb3e Mon Sep 17 00:00:00 2001 From: rawframe <44139088+rawframe@users.noreply.github.com> Date: Wed, 10 Dec 2025 04:11:17 +0000 Subject: [PATCH 06/14] Remove old readme file for PIR sensor switch --- usermods/PIR_sensor_switch_v2/{readme.md => readme_old.md} | 1 + 1 file changed, 1 insertion(+) rename usermods/PIR_sensor_switch_v2/{readme.md => readme_old.md} (97%) diff --git a/usermods/PIR_sensor_switch_v2/readme.md b/usermods/PIR_sensor_switch_v2/readme_old.md similarity index 97% rename from usermods/PIR_sensor_switch_v2/readme.md rename to usermods/PIR_sensor_switch_v2/readme_old.md index 86497e34d0..71b05614a9 100644 --- a/usermods/PIR_sensor_switch_v2/readme.md +++ b/usermods/PIR_sensor_switch_v2/readme_old.md @@ -107,3 +107,4 @@ Have fun - @gegu & @blazoncek 2024-02 * Added compile time option to expand number of PIR sensors (they are logically ORed) `-D PIR_SENSOR_MAX_SENSORS=3` + From 7c8a5ab60641e4c615873a72802654fadffc4c20 Mon Sep 17 00:00:00 2001 From: rawframe <44139088+rawframe@users.noreply.github.com> Date: Wed, 10 Dec 2025 04:12:10 +0000 Subject: [PATCH 07/14] Update and rename readme_v2.md to readme.md --- usermods/PIR_sensor_switch_v2/{readme_v2.md => readme.md} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename usermods/PIR_sensor_switch_v2/{readme_v2.md => readme.md} (100%) diff --git a/usermods/PIR_sensor_switch_v2/readme_v2.md b/usermods/PIR_sensor_switch_v2/readme.md similarity index 100% rename from usermods/PIR_sensor_switch_v2/readme_v2.md rename to usermods/PIR_sensor_switch_v2/readme.md From 51939f023fdc49392a85e3b1dd9c7af6c092f872 Mon Sep 17 00:00:00 2001 From: rawframe <44139088+rawframe@users.noreply.github.com> Date: Wed, 10 Dec 2025 04:19:07 +0000 Subject: [PATCH 08/14] Update README with enhanced formatting and content --- usermods/PIR_sensor_switch_v2/readme.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/usermods/PIR_sensor_switch_v2/readme.md b/usermods/PIR_sensor_switch_v2/readme.md index a90c7b19c6..da6a847782 100644 --- a/usermods/PIR_sensor_switch_v2/readme.md +++ b/usermods/PIR_sensor_switch_v2/readme.md @@ -66,8 +66,9 @@ This helps if you need: ## Notes -- To add more pirs or actions, just change the defines in the usermod.h +- To add more pirs or actions, just change the defines in the the cpp file (defaults) or the platformio.ini / override - PIR_SENSOR_MAX - ACTION_MAX - Add if anyone wants to add the other options from the @gegu & @blazoncek usermod then feel free to do so. + From 09da13aeaf45586a44a05c8d91c662c51141e87a Mon Sep 17 00:00:00 2001 From: rawframe <44139088+rawframe@users.noreply.github.com> Date: Wed, 10 Dec 2025 04:25:49 +0000 Subject: [PATCH 09/14] Refactor README for PIR Sensor Switch v2 Updated README to improve formatting and clarity. --- usermods/PIR_sensor_switch_v2/readme.md | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/usermods/PIR_sensor_switch_v2/readme.md b/usermods/PIR_sensor_switch_v2/readme.md index da6a847782..47cbf9da80 100644 --- a/usermods/PIR_sensor_switch_v2/readme.md +++ b/usermods/PIR_sensor_switch_v2/readme.md @@ -1,6 +1,6 @@ # PIR Sensor Switch v2 -**By rawframe** +**Rawframe** ## Background @@ -39,13 +39,12 @@ However, I needed something more flexible for my setup - multiple PIRs triggerin ### Web UI - Toggle PIRs and Actions on/off directly from the Info page - See motion status in real-time (● motion / ○ idle) -- View countdown timers for off delays +- View per Action countdown timers for off delays - Config page for full setup (pins, presets, delays, linking) ### API Control - Enable/disable PIRs via JSON API: `{"MotionDetection":{"pir0":true}}` - Enable/disable Actions via JSON API: `{"MotionDetection":{"action0":true}}` -- Integrates with Home Assistant, Node-RED, etc. ## Use Cases @@ -54,15 +53,13 @@ This helps if you need: - Different lighting presets for different sensors - Combined motion detection (e.g., "turn off only when ALL sensors are idle") - Independent motion zones that don't interfere with each other -- Complex automation without macro spaghetti +- Complex automation without macro chains ## Technical Bits -- Uses a preset FIFO queue to prevent flooding -- Optimized RAM usage with bitmasks instead of 2D arrays, but you could change this if you need more than 8 pirs or actions. -- Contributor tracking system ensures Actions behave correctly with multiple PIRs -- Persistent configuration saved to `cfg.json` -- Fully compatible with WLED's preset system +- Uses a preset FIFO queue to prevent flooding (on testing I noticed if presets were called at the same time one would usually be skipped). +- Optimized RAM usage with bitmasks instead of 2D arrays, but you could change this if you need more than 8 PIRs or Actions. +- PIR to Acion contributor tracking ensures Actions behave correctly with multiple PIRs ## Notes @@ -70,5 +67,6 @@ This helps if you need: - PIR_SENSOR_MAX - ACTION_MAX -- Add if anyone wants to add the other options from the @gegu & @blazoncek usermod then feel free to do so. +- If anyone wants to add the other options from the @gegu & @blazoncek usermod then feel free to do so bit remember to adjust the custom webapge formatting also. +Hope its useful :) From 42cc9d67e63831a6526a18b804b6ac72e848580c Mon Sep 17 00:00:00 2001 From: rawframe <44139088+rawframe@users.noreply.github.com> Date: Thu, 11 Dec 2025 00:53:54 +0000 Subject: [PATCH 10/14] Format and update readme for PIR Sensor Switch v2 --- usermods/PIR_sensor_switch_v2/readme.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/usermods/PIR_sensor_switch_v2/readme.md b/usermods/PIR_sensor_switch_v2/readme.md index 47cbf9da80..1ef4a197b2 100644 --- a/usermods/PIR_sensor_switch_v2/readme.md +++ b/usermods/PIR_sensor_switch_v2/readme.md @@ -6,7 +6,7 @@ Back in the day, I added PIR functionality to WLED before it became a built-in feature. Since then, a PIR usermod was added (credit to @gegu & @blazoncek! for PIR_sensor_switch). -However, I needed something more flexible for my setup - multiple PIRs triggering different actions in various combinations. The official version works great for simple cases, but when you need complex linking between sensors and actions, macro chains get messy real quick, and moreover we still cannot easily have seemless fx (i.e avoid restarting an fx animation when called again), So I built this. +However, I needed something more flexible for my setup - multiple PIRs triggering different actions in various combinations. The official version works great for simple cases, but when you need complex linking between sensors and actions, macro chains get messy, and moreover we still cannot easily have seamless fx (i.e avoid restarting an fx animation when called again), So I built this. ## Screenshots @@ -59,14 +59,14 @@ This helps if you need: - Uses a preset FIFO queue to prevent flooding (on testing I noticed if presets were called at the same time one would usually be skipped). - Optimized RAM usage with bitmasks instead of 2D arrays, but you could change this if you need more than 8 PIRs or Actions. -- PIR to Acion contributor tracking ensures Actions behave correctly with multiple PIRs +- PIR to Action contributor tracking ensures Actions behave correctly with multiple PIRs ## Notes -- To add more pirs or actions, just change the defines in the the cpp file (defaults) or the platformio.ini / override +- To add more pirs or actions, just change the defines in the cpp file (defaults) or the platformio.ini / override - PIR_SENSOR_MAX - ACTION_MAX -- If anyone wants to add the other options from the @gegu & @blazoncek usermod then feel free to do so bit remember to adjust the custom webapge formatting also. +- If anyone wants to add the other options from the @gegu & @blazoncek usermod then feel free to do so but remember to adjust the custom webpage formatting also. -Hope its useful :) +Hope it's useful :) From 975d15d0a0e878d03ec86eab949efd616f635e5d Mon Sep 17 00:00:00 2001 From: rawframe <44139088+rawframe@users.noreply.github.com> Date: Thu, 11 Dec 2025 01:01:51 +0000 Subject: [PATCH 11/14] Refactor readme for PIR Sensor Switch v2 Updated the readme to improve formatting and clarity. --- usermods/PIR_sensor_switch_v2/readme.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/usermods/PIR_sensor_switch_v2/readme.md b/usermods/PIR_sensor_switch_v2/readme.md index 1ef4a197b2..f81fbe4ced 100644 --- a/usermods/PIR_sensor_switch_v2/readme.md +++ b/usermods/PIR_sensor_switch_v2/readme.md @@ -63,10 +63,11 @@ This helps if you need: ## Notes -- To add more pirs or actions, just change the defines in the cpp file (defaults) or the platformio.ini / override +- To add more PIRs or Actions, just change the defines in the cpp file (defaults) or the platformio.ini / override - PIR_SENSOR_MAX - ACTION_MAX -- If anyone wants to add the other options from the @gegu & @blazoncek usermod then feel free to do so but remember to adjust the custom webpage formatting also. +- If anyone wants to add the other options from the @gegu & @blazoncek usermod then feel free to do so but remember to adjust the custom webpage formatting accordingly. Hope it's useful :) + From fbb5790db61ea5aa5ab4a9bf31081ed0313eceb6 Mon Sep 17 00:00:00 2001 From: rawframe <44139088+rawframe@users.noreply.github.com> Date: Thu, 11 Dec 2025 01:06:59 +0000 Subject: [PATCH 12/14] Format readme.md with Markdown syntax --- usermods/PIR_sensor_switch_v2/readme.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/usermods/PIR_sensor_switch_v2/readme.md b/usermods/PIR_sensor_switch_v2/readme.md index f81fbe4ced..5bc7b4b21a 100644 --- a/usermods/PIR_sensor_switch_v2/readme.md +++ b/usermods/PIR_sensor_switch_v2/readme.md @@ -1,6 +1,6 @@ # PIR Sensor Switch v2 -**Rawframe** +## Rawframe ## Background @@ -71,3 +71,4 @@ This helps if you need: Hope it's useful :) + From 8930ad8f75bb72f1e0641a2735ea46952fc6fddd Mon Sep 17 00:00:00 2001 From: rawframe <44139088+rawframe@users.noreply.github.com> Date: Thu, 11 Dec 2025 01:19:04 +0000 Subject: [PATCH 13/14] Update - redundant code removal and tweaks --- .../PIR_sensor_switch_v2.cpp | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/usermods/PIR_sensor_switch_v2/PIR_sensor_switch_v2.cpp b/usermods/PIR_sensor_switch_v2/PIR_sensor_switch_v2.cpp index 83c76eae43..c83e637a25 100644 --- a/usermods/PIR_sensor_switch_v2/PIR_sensor_switch_v2.cpp +++ b/usermods/PIR_sensor_switch_v2/PIR_sensor_switch_v2.cpp @@ -1,4 +1,5 @@ /* + * Rawframe * Usermod for PIR sensor motion detection. * * This usermod handles PIR sensor states and triggers actions (presets) based on motion. @@ -157,14 +158,6 @@ class MotionDetectionUsermod : public Usermod { } } - // If config didn't include enabled flags, ensure sensible defaults - for (int a = 0; a < ACTION_MAX; a++) { - // actionEnabled is initialized to true in class def. - // If config was missing, it stays true. - // If config had "enabled": false, it became false. - // No further action needed here. - } - return true; } @@ -337,9 +330,12 @@ class MotionDetectionUsermod : public Usermod { } else { bool active = actions[a].activeCount > 0; uint32_t remain = 0; + // Calculate remaining time, avoiding underflow if timer already elapsed if (!active && actions[a].offStartMs > 0) { - unsigned long end = actions[a].offStartMs + actions[a].offDelayMs; - if (end > now) remain = (end - now) / 1000; + unsigned long elapsed = now - actions[a].offStartMs; + if (elapsed < actions[a].offDelayMs) { + remain = (actions[a].offDelayMs - elapsed) / 1000; + } } if (active) status = "⏱ active"; else if (remain > 0) status = String("⏱ off in ") + String(remain) + "s"; From 0826e96849a6a746d3e7d914196fd5cb721f599d Mon Sep 17 00:00:00 2001 From: rawframe <44139088+rawframe@users.noreply.github.com> Date: Thu, 11 Dec 2025 02:12:58 +0000 Subject: [PATCH 14/14] Update to timer max limit --- usermods/PIR_sensor_switch_v2/PIR_sensor_switch_v2.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/usermods/PIR_sensor_switch_v2/PIR_sensor_switch_v2.cpp b/usermods/PIR_sensor_switch_v2/PIR_sensor_switch_v2.cpp index c83e637a25..95a3228aa5 100644 --- a/usermods/PIR_sensor_switch_v2/PIR_sensor_switch_v2.cpp +++ b/usermods/PIR_sensor_switch_v2/PIR_sensor_switch_v2.cpp @@ -150,6 +150,7 @@ class MotionDetectionUsermod : public Usermod { if (ap.containsKey("offPreset")) actions[a].offPreset = ap["offPreset"] | actions[a].offPreset; if (ap.containsKey("offSec")) { uint32_t s = ap["offSec"] | (actions[a].offDelayMs / 1000UL); + if (s > 4294967) s = 4294967; // Max ~49.7 days, prevents overflow when multiplied by 1000 actions[a].offDelayMs = s * 1000UL; } if (ap.containsKey("enabled")) { @@ -617,3 +618,4 @@ class MotionDetectionUsermod : public Usermod { // Register instance static MotionDetectionUsermod motionDetectionUsermod; REGISTER_USERMOD(motionDetectionUsermod); +