Обновление скрипта ButtonHelper (обработка нажатий)

Обновил скрипт, найденный здесь, на просторах форума.
Скрипт позволяет обрабатывать нажатия как “сухих контактов” (A1-4, WBIO-DI-WD-14, MCM8 …), так и переназначать нажатия с одних устройств на другие (например “wb-led_XXX/Input 1”)
В исходный модуль ActionButtons добавлены новые функции, расширена логика обработки нажатий и устранена потенциальная ошибка работы с таймерами.

  1. Добавлена функция switchOffAll
  • Принимает произвольное количество аргументов в формате "устройство/контрол", перечисленных в формате wirenboard через запятую. Например, чтобы выключить все устройства в комнате.
  • Автоматически определяет тип текущего значения и выключает устройство:
    • booleanfalse (реле/переключатель)
    • строка "R;G;B""0;0;0" (RGB-лента)
    • number0 (диммер/уровень)
  • Включает валидацию формата и проверку существования устройства.
  1. Реализован обработчик shortLongPress
  • Распознаёт последовательность: короткое нажатие → быстрое второе нажатие → удержание .
  • Работает параллельно с singlePress , doublePress , longPress и longRelease .
  • Настройки чувствительности задаются через timeToNextPress (окно между нажатиями) и timeOfLongPress (порог удержания).
  1. Исправлена ошибка ERROR: trying to stop unknown timer
  • Причина: В wb-rules вызов clearTimeout() на уже отработавшем таймере генерирует ошибку в системном логе. Номер таймера рос с каждой обработкой, так как ссылка на него не сбрасывалась.
    • Добавлена функция safeClear() , которая проверяет ссылку на null и оборачивает clearTimeout в try...catch .
    • Все переменные таймеров принудительно обнуляются (= null ) сразу после выполнения колбэка setTimeout .
    • Состояние кнопки инкапсулировано в объект state , что исключает конфликты замыканий.
    • Добавлены проверки типов и безопасные значения по умолчанию для параметров таймаутов.
(function () {
'use strict';

var ActionButtons = {};


/**
 * Function that identifies what kind of press was performed: single, double or long press;
 * and assigns an action for each type of press.
 *
 * @param  {string} trigger         -  Name of device and control in the following format: "<device>/<control>".
 * @param  {object} action          -  Defines actions to be taken for each type of button press.
 *                                  Key: "singlePress" or "doublePress" or "longPress" or "longRelease" or "shortLongPress".
 *                                  Value: Object of the following structure {func: <function name>, prop: <array of parameters to be passed>}
 *                                  Example:
 *                                  {
 *                                      singlePress: {func: myFunc1, prop: ["wb-mr6c_1", "K1"]},
 *                                      doublePress: {func: myFunc2, prop: ["wb-mrgbw-d_2", "RGB", "255;177;85"]},
 *                                      longPress: {func: myFunc3, prop: []},
 *                                      longRelease: {func: myFunc4, prop: []}
 *                                      shortLongPress: {func: myFunc5, prop: []}
 *                                  }
 * @param  {number} timeToNextPress -  Time (ms) after button up to wait for the next press before reseting the counter. Default is 300 ms.
 * @param  {number} timeOfLongPress -  Time (ms) after button down to be considered as as a long press. Default is 1000 ms (1 sec).
 */
ActionButtons.onButtonPress = function (trigger, action, timeToNextPress, timeOfLongPress) {

    var state = {
        counter: 0,
        timerShort: null,
        timerLong: null,
        isLong: false,
        waitingShortLong: false
    };

    timeToNextPress = (typeof timeToNextPress === 'number') ? timeToNextPress : 300;
    timeOfLongPress = (typeof timeOfLongPress === 'number') ? timeOfLongPress : 1000;

    // Безопасная очистка таймера
    function safeClear(timerRef) {
        if (timerRef !== null) {
            try { clearTimeout(timerRef); } catch(e) {}
        }
        return null; // всегда возвращаем null
    }

    defineRule("btn_" + trigger.replace(/[\/]/g, "_"), {
        whenChanged: trigger,
        then: function (newValue) {
            
            // ================= КНОПКА НАЖАТА =================
            if (newValue) {
                state.timerShort = safeClear(state.timerShort);

                // Если ждём продолжения "короткое + длинное"
                if (state.waitingShortLong) {
                    state.timerLong = setTimeout(function () {
                        if (action.shortLongPress && typeof action.shortLongPress.func === "function") {
                            action.shortLongPress.func.apply(this, action.shortLongPress.prop || []);
                            // log.info("shortLongPress triggered");
                        }
                        state.timerLong = null;
                        state.waitingShortLong = false;
                        state.counter = 0;
                        state.isLong = true;
                    }, timeOfLongPress);
                    return;
                }

                // Обычное первое нажатие (длинное)
                state.timerLong = setTimeout(function () {
                    if (action.longPress && typeof action.longPress.func === "function") {
                        action.longPress.func.apply(this, action.longPress.prop || []);
                        // log.info("longPress triggered");
                    }
                    state.timerLong = null;
                    state.isLong = true;
                    state.counter = 0;
                }, timeOfLongPress);
            } 
            // ================= КНОПКА ОТПУЩЕНА =================
            else {
                // Отпускание после длинного нажатия
                if (state.isLong) {
                    state.timerLong = safeClear(state.timerLong);
                    if (action.longRelease && typeof action.longRelease.func === "function") {
                        action.longRelease.func.apply(this, action.longRelease.prop || []);
                        // log.info("longRelease triggered");
                    }
                    state.isLong = false;
                    state.waitingShortLong = false;
                    state.counter = 0;
                    return;
                }

                // Отмена таймера длинного нажатия (было короткое)
                state.timerLong = safeClear(state.timerLong);
                state.counter += 1;

                // Таймер ожидания следующего клика
                state.timerShort = safeClear(state.timerShort);
                state.timerShort = setTimeout(function () {
                    if (state.counter === 1) {
                        if (action.singlePress && typeof action.singlePress.func === "function") {
                            action.singlePress.func.apply(this, action.singlePress.prop || []);
                            // log.info("singlePress triggered");
                        }
                    } else if (state.counter === 2) {
                        if (action.doublePress && typeof action.doublePress.func === "function") {
                            action.doublePress.func.apply(this, action.doublePress.prop || []);
                            // log.info("doublePress triggered");
                        }
                    }
                    state.timerShort = null;
                    state.waitingShortLong = false;
                    state.counter = 0;
                }, timeToNextPress);

                // Если после 1-го клика есть обработчик shortLongPress, помечаем ожидание
                if (state.counter === 1 && action.shortLongPress) {
                    state.waitingShortLong = true;
                }
            }
        }
    });
};

// export as Node module / AMD module / browser variable
if (typeof exports === 'object' && typeof module !== 'undefined') {
    module.exports = ActionButtons;
} else if (typeof define === 'function' && define.amd) {
    define(ActionButtons);
} else {
    global.ActionButtons = ActionButtons;
}
}());

ActionButtons.onButtonPress(
"wb-gpio/EXT2_IN4",          //Вход, за которым следим.
{
    singlePress: {
       //func: switchRelay, prop: ["wb-led_227", "Channel 3"]
    },
    doublePress: {
       //func: switchRelay, prop: ["wb-mio-gpio_157:2", "K2"]
       //func: switchDimmerRGB, prop: ["wb-mr6c_10", "K2", "wb-mrgbw-d_24"]
    },
    longPress: {
       //func: switchRelay, prop: ["wb-mio-gpio_157:2", "K3"]
       //func: setRandomRGB, prop: ["wb-mr6c_10", "K2", "wb-mrgbw-d_24"]
       //func: switchOffAll, prop: ["wb-led_224/Channel 3", "wb-led_206/Channel 4"]
    },
    shortLongPress: {
       //func: switchRelay, prop: ["wb-mio-gpio_157:2", "K3"]
       //func: setRandomRGB, prop: ["wb-mr6c_10", "K2", "wb-mrgbw-d_24"]
       //func: switchOffAll, prop: ["wb-led_224/Channel 3", "wb-led_206/Channel 4"]
    }
},
300, 1000
);

/**
* Helper Functions
*/
function switchRelay(device, control) { //Принимает в параметрах устройство и выход. Переключает состояние выхода на противоположное.
  log.info("LongPress switchRelay" ,device, control) //Это лог. Он попадает в /var/log/messages
  dev[device][control] = !dev[device + "/" + control];
}

function switchDimmerRGB(relayDevice, relayControl, dimmerDevice) {
   dev[relayDevice][relayControl] = true;
   if (dev[dimmerDevice + "/RGB"] !== "0;0;0") {
     dev[dimmerDevice]["RGB"] = "0;0;0";
   }
   else {
     dev[dimmerDevice]["RGB"] = dev[relayDevice + "/RGB"];
   }
}

function setRandomRGB(relayDevice, relayControl, dimmerDevice) {
  dev[relayDevice][relayControl] = true;
  dev[relayDevice + "/RGB"] = "" + Math.floor(Math.random() * 255) + ";" + Math.floor(Math.random() * 255) + ";" + Math.floor(Math.random() * 255);
  dev[dimmerDevice]["RGB"] = dev[relayDevice + "/RGB"];
}

function switchOffAll() {
    for (var i = 0; i < arguments.length; i++) {
        var arg = arguments[i];

      // Пропускаем нестроковые аргументы
        if (typeof arg !== "string") {
            log.warn("LongPress switchOffAll: argument", i, "is not a string, skipping");
            continue;
        }
        
        // Парсим формат "device/control"
        var separatorIndex = arg.indexOf("/");
        if (separatorIndex === -1) {
            log.warn("LongPress switchOffAll: invalid format for argument", i, 
                    "- expected 'device/control', got:", arg);
            continue;
        }
        
        var device = arg.substring(0, separatorIndex);
        var control = arg.substring(separatorIndex + 1);
        
        // Проверяем существование устройства
        if (!dev[device]) {
            log.warn("LongPress switchOffAll: device not found", device);
            continue;
        }
        
        var currentValue = dev[device + "/" + control];
        
        // Выключаем в зависимости от типа значения
        if (typeof currentValue === "boolean") {
            // Реле/переключатель → false
            dev[device][control] = false;
        } 
        else if (typeof currentValue === "string" && currentValue.match(/^\d+;\d+;\d+$/)) {
            // RGB-значение → "0;0;0" (чёрный)
            dev[device][control] = "0;0;0";
        } 
        else if (typeof currentValue === "number") {
            // Диммер/яркость → 0
            dev[device][control] = 0;
        } 
        else {
            // Fallback: пробуем выключить
            dev[device][control] = false;
        }
    }
}

Проверено на Wiren Board 8.4.3 release wb-2606 (as testing)
Буду рад замечаним, дополнениям, исправлениям

Добрый день.
Отлично, что делитесь!