Срабатывание триггера whenchanged при сбросе счетчика нажатий при сбросе питания

Здравствуйте. Столкнулся с проблемой, что в доме произвольно включался свет, закрывались шторы, закрывались краны. Из общего, что смог заметить все они были привязаны к счетчикам нажатий через whenchanged. Проверил системный журнал, 1:43 - лег сервис:
2026-08-14T22:43:42.114Z [init] wb-mqtt-serial.service: Consumed 2d 2h 54min 11.005s CPU time.
2026-08-14T22:43:42.110Z [init] Stopped MQTT Driver for serial devices.
2026-08-14T22:43:42.106Z [init] wb-mqtt-serial.service: Scheduled restart job, restart counter is at 1.
2026-08-14T22:43:40.862Z [init] wb-mqtt-serial.service: Consumed 2d 2h 54min 11.005s CPU time.
2026-08-14T22:43:40.856Z [init] wb-mqtt-serial.service: Failed with result ‘signal’.
2026-08-14T22:43:40.855Z [init] wb-mqtt-serial.service: Main process exited, code=killed, status=11/SEGV
2026-08-14T22:43:40.854Z [mosquitto] 1786747420: Client wb-modbus closed its connection.

Собственно после этого перезапустился и я так понимаю обновились значения в счетчиках нажатий, из-за чего сработали сценарии. Так же важный нюанс, на порту mod out 3 через расширение сидят два частотных преобразователя типа T13-750W-12-H и уже было замечено, что они наводят помехи на шины, несмотря на наличие терминаторов и прочего. Выражается это в периодическом “покраснении” некоторых топиков, ошибок в статусе.
После этого пошел проверять как работает whenchanged с счетчиками нажатий в mr6c. Как и описано в документации, счетчик обнуляется после сброса питания и как выяснилось при отключении питания через перезагрузку контроллера whenchanged реагирует на обнуление счетчика и выполняется сценарий.
Исходя из этого, у меня два вопроса:

  1. Что вероятнее всего стало причиной ошибки в работе сервиса mqtt, стоит ли в срочном порядке убирать частотные преобразователи с шины или это связано с чем-то другим.
  2. Как избегать ложного триггера на счетчики нажатий, ведь всегда существует вероятность того, что после потери питания контроллер перезагрузится и тогда сработает сценарий. А использование счетчиков нажатий в движке правил является необходимостью.
    Прикладываю системный журнал и диагностический архив.
    log_20260815T014511.log (334,8 КБ)
    приложен диагностический архив, доступен только сотрудникам поддержки
    (327,9 КБ)

Добрый день!

Разберу по порядку:

Что случилось с сервисом. Процесс wb-mqtt-serial обратился к недопустимой памяти, и за это его завершило ядро. Именно это означает строка status=11/SEGV в вашем логе. Дальше systemd автоматически поднял сервис заново, это его штатное поведение при падении.

Почему код в этот момент пошёл не туда — точно сказать нельзя. Самая вероятная версия — обработка битых ответов с шины: у вас на шинах постоянно идут таймауты и повреждённые кадры, значит эти ветки кода работают непрерывно. Но это предположение, не факт.

Почему сработали сценарии. При остановке драйвер помечает свои контролы как недоступные — в топике счётчика оказывается пустое значение. При старте он опрашивает устройства и публикует значения заново. wb-rules видит цепочку «значение → пусто → значение» и отрабатывает whenChanged, передавая в правило то же самое число, что было до падения. Счётчик при этом не изменился ни на единицу, и модули питание не теряли — они и не обнулялись.

Ваш эксперимент с обесточиванием — это второй, отдельный случай: там счётчик в модуле действительно сбрасывается в 0, и правило видит изменение.

Что делать, по порядку:

1. Исправить правила. Проверки newValue > 0 недостаточно — она ловит только случай с обесточенным модулем. Нужно ещё сравнивать с предыдущим значением, тогда отсекается и повтор того же числа после рестарта драйвера:

function buttonSinglePress(input, action) {
var topic = input + " Single Press Counter";
var counter = dev[topic]; // при старте может быть null
defineRule({
whenChanged: topic,
then: function (newValue) {
if (newValue > 0 && counter !== null && newValue != counter) {
action(newValue); // настоящее нажатие
}
counter = newValue;
}
});
}

Этот вариант из темы, где коллеги разбирали такую же ситуацию: Отработка правил завязанных на счетчики при перезагрузке. Там же объяснено, почему более простые проверки не закрывают оба случая.

2. Обновить ПО контроллера до актуального стабильного релиза. У вас wb-mqtt-serial 2.180.2 — версия годичной давности, с тех пор вышло много исправлений.

3. Разобраться с шиной. Частотные преобразователи снимать срочно не нужно — они на отдельном порту MOD3, прямой связи с падением в логах не вижу. Но по архиву видно две проблемы:

  • GRD9L-S с адресом 3 не отвечает практически постоянно — почти 5000 таймаутов за два часа;

  • один из преобразователей (адрес 1) регулярно отваливается и подключается заново, второй при этом работает нормально.

Проверьте у них питание, проводку и адресацию. Плюс на шине с модулями MR6C раз в 5–10 минут проскакивают битые ответы от разных модулей — это то самое «покраснение» топиков. Стоит пройтись по монтажу: RS-485: Физическое подключение.

Можете ,пожалуйста, показать, как применять на практике. Логика скрипта не важна, а именно продемонстрировать как можно использовать, когда вызывать. Нужно ли это использовать если это топик позиции штор, а не счетчик, к примеру.

defineVirtualDevice("curtain_control_1stfloor_left", {
    title: "Curtain Control 1stfloor_left",
    cells: {
        open: {
            type: "switch",
            value: false
        },
        close: {
            type: "switch",
            value: false
        },
        press_counter: {
            type: "text",
            value: "0"
        },
        last_direction: {
            type: "text",
            value: "none" // "opening", "closing", "none"
        }
    }
});


// Правило для обработки двойного нажатия кнопки
defineRule("curtainsmovinglogic_1stfloor_left", { 
    whenChanged: "wb-mr6cv3_82/Input 6 Double Press Counter",
    then: function(newValue) {
        var position = dev["aok-am68_0x03B004/Position"];
        var currentCounter = parseInt(dev["curtain_control_1stfloor_left/press_counter"]) || 0;
        var lastDirection = dev["curtain_control_1stfloor_left/last_direction"];
        var command;
        
        // Определяем команду на основе текущей позиции и состояния
        if (position === 1 || position === 0) {
            // Шторы полностью открыты - закрыть
            command = "close";
            dev["curtain_control_1stfloor_left/press_counter"] = "0"; // после закрытия след. - открыть
            dev["curtain_control_1stfloor_left/last_direction"] = "closing";
        } else if (position === 99 || position === 100) {
            // Шторы полностью закрыты - открыть
            command = "open";
            dev["curtain_control_1stfloor_left/press_counter"] = "1"; // после открытия след. - закрыть
            dev["curtain_control_1stfloor_left/last_direction"] = "opening";
        } else {
            // Промежуточная позиция
            if (lastDirection === "opening") {
                // Если сейчас открываются - остановить и начать закрывать
                command = "close";
                dev["curtain_control_1stfloor_left/press_counter"] = "0"; // после закрытия след. - открыть
                dev["curtain_control_1stfloor_left/last_direction"] = "closing";
            } else if (lastDirection === "closing") {
                // Если сейчас закрываются - остановить и начать открывать
                command = "open";
                dev["curtain_control_1stfloor_left/press_counter"] = "1"; // после открытия след. - закрыть
                dev["curtain_control_1stfloor_left/last_direction"] = "opening";
            } else {
                // Не двигается - чередуем команды
                if (currentCounter === 0) {
                    command = "open";
                    dev["curtain_control_1stfloor_left/press_counter"] = "1";
                    dev["curtain_control_1stfloor_left/last_direction"] = "opening";
                } else {
                    command = "close";
                    dev["curtain_control_1stfloor_left/press_counter"] = "0";
                    dev["curtain_control_1stfloor_left/last_direction"] = "closing";
                }
            }
        }
        
        // Отправляем выбранную команду
        if (command === "open") {
            dev["curtain_control_1stfloor_left/open"] = !dev["curtain_control_1stfloor_left/open"];
        } else {
            dev["curtain_control_1stfloor_left/close"] = !dev["curtain_control_1stfloor_left/close"];
        }
        
        log("Position: " + position + ", Command: " + command + ", Counter: " + 
            dev["curtain_control_1stfloor_left/press_counter"] + ", LastDir: " + lastDirection);
    }
});

// Отслеживаем изменение позиции для обновления состояния
defineRule("updateStateFromPosition_1stfloor_left", {
    whenChanged: "aok-am68_0x03B004/Position",
    then: function(newValue) {
        var lastDirection = dev["curtain_control_1stfloor_left/last_direction"];
        
        if (newValue === 1 || newValue === 0) {
            // Достигли полностью открытого состояния
            dev["curtain_control_1stfloor_left/press_counter"] = "1"; // следующее - закрыть
            dev["curtain_control_1stfloor_left/last_direction"] = "none";
            log("Fully opened. Next: close");
        } else if (newValue === 99 || newValue === 100) {
            // Достигли полностью закрытого состояния
            dev["curtain_control_1stfloor_left/press_counter"] = "0"; // следующее - открыть
            dev["curtain_control_1stfloor_left/last_direction"] = "none";
            log("Fully closed. Next: open");
        } else if (lastDirection !== "none") {
            // В движении - обновляем направление на основе скорости изменения
            var oldValue = dev["curtain_control_1stfloor_left/last_position"] || newValue;
            
            if (newValue > oldValue) {
                // Позиция увеличивается - закрываются
                dev["curtain_control_1stfloor_left/last_direction"] = "closing";
            } else if (newValue < oldValue) {
                // Позиция уменьшается - открываются
                dev["curtain_control_1stfloor_left/last_direction"] = "opening";
            }
        }
        
        // Сохраняем текущую позицию для следующего сравнения
        dev["curtain_control_1stfloor_left/last_position"] = newValue;
    }
});

// Правило для отслеживания отправки команды открытия
defineRule("trackOpening_1stfloor_left", {
    whenChanged: "curtain_control_1stfloor_left/open",
    then: function(newValue) {
        if (newValue) {
            // Отправлена команда открытия
            dev["curtain_control_1stfloor_left/last_direction"] = "opening";
            log("Opening command sent");
        }
    }
});

// Правило для отслеживания отправки команды закрытия
defineRule("trackClosing_1stfloor_left", {
    whenChanged: "curtain_control_1stfloor_left/close",
    then: function(newValue) {
        if (newValue) {
            // Отправлена команда закрытия
            dev["curtain_control_1stfloor_left/last_direction"] = "closing";
            log("Closing command sent");
        }
    }
});

defineRule("curtainopen_1stfloor_left", { 
    whenChanged: "curtain_control_1stfloor_left/open",
    then: function(newValue, devName, cellName) {
        dev["aok-am68_0x03B004"]["Open"] = newValue;
        dev["aok-am68_0x025003"]["Open"] = newValue;
        log("curtainopen signal sent: " + newValue);
    }
});

defineRule("curtainclose_1stfloor_left", { 
    whenChanged: "curtain_control_1stfloor_left/close",
    then: function(newValue, devName, cellName) {
        dev["aok-am68_0x03B004"]["Close"] = newValue;
        dev["aok-am68_0x025003"]["Close"] = newValue;
        log("curtainclose signal sent: " + newValue);
    }
});

Править нужно одно правило — curtainsmovinglogic_1stfloor_left. Остальные запускаются от ячеек виртуального устройства, а не от топиков драйвера, и сами по себе не сработают.

Было:

defineRule("curtainsmovinglogic_1stfloor_left", { 
    whenChanged: "wb-mr6cv3_82/Input 6 Double Press Counter",
    then: function(newValue) {
        var position = dev["aok-am68_0x03B004/Position"];

Стало:

var lastCounter_1stfloor_left = dev["wb-mr6cv3_82/Input 6 Double Press Counter"];

defineRule("curtainsmovinglogic_1stfloor_left", { 
    whenChanged: "wb-mr6cv3_82/Input 6 Double Press Counter",
    then: function(newValue) {
        var prev = lastCounter_1stfloor_left;
        lastCounter_1stfloor_left = newValue;
        if (!(newValue > 0 && prev != null && newValue != prev)) {
            return;
        }

        var position = dev["aok-am68_0x03B004/Position"];

Дальше ваш код без изменений. Для остальных штор — то же самое, только своя переменная под каждую кнопку.

Логика такая: переменная lastCounter_1stfloor_left объявлена вне правила, поэтому создаётся один раз при загрузке скрипта и хранит последнее известное значение счётчика между срабатываниями. При каждом срабатывании старое значение сохраняется в prev, а в память сразу записывается новое — обязательно до проверки, чтобы после сброса счётчика в 0 следующее реальное нажатие отработало нормально. Дальше проверяются три условия: значение больше нуля (отсекает сброс счётчика при потере питания модуля), в памяти есть предыдущее значение (отсекает самый первый запуск, когда сравнивать не с чем) и новое значение отличается от предыдущего (отсекает повтор того же числа после рестарта драйвера, как в вашем случае). Если хотя бы одно условие не выполнено, срабатывает return и правило завершается, не дойдя до команд шторам. При настоящем нажатии все три условия выполняются и ваш код отрабатывает.

Добрый день!

Подскажите, пожалуйста, у вас остались еще вопросы по теме?

Здравствуйте. Пока нет. Ситуация с частотником еще будет позже решаться.
Также доделал под себя скрипт, на который вы ссылались, чтоб можно было быстро мастер-кнопки делать, в том числе на несколько входов. Сброс счетчика при потере питания проверил, при перезапуске шины не смог проверить, обычный перезапуск такую реакцию не вызывает:

// Одиночное нажатие
function buttonSinglePress(inputs, action) {
    if (typeof inputs === "string") inputs = [inputs];
    
    inputs.forEach(function(input) {
        var topic = input + " Single Press Counter";
        var counter = dev[topic];
        
        defineRule({
            whenChanged: topic,
            then: function (newValue) {
                if (newValue > 0 && counter !== null && newValue != counter) {
                    action(newValue);
                }
                counter = newValue;
            }
        });
    });
}

// Двойное нажатие
function buttonDoublePress(inputs, action) {
    if (typeof inputs === "string") inputs = [inputs];
    
    inputs.forEach(function(input) {
        var topic = input + " Double Press Counter";
        var counter = dev[topic];
        
        defineRule({
            whenChanged: topic,
            then: function (newValue) {
                if (newValue > 0 && counter !== null && newValue != counter) {
                    action(newValue);
                }
                counter = newValue;
            }
        });
    });
}

// Долгое нажатие
function buttonLongPress(inputs, action) {
    if (typeof inputs === "string") inputs = [inputs];
    
    inputs.forEach(function(input) {
        var topic = input + " Long Press Counter";
        var counter = dev[topic];
        
        defineRule({
            whenChanged: topic,
            then: function (newValue) {
                if (newValue > 0 && counter !== null && newValue != counter) {
                    action(newValue);
                }
                counter = newValue;
            }
        });
    });
}


// === Одиночное нажатие ===
buttonSinglePress("wb-led_46/Input 1", function() {
    dev["wb-led_46/Channel 1"] = !dev["wb-led_46/Channel 1"];
    dev["wb-led_46/Channel 2"] = !dev["wb-led_46/Channel 2"];
});

buttonSinglePress(["wb-led_46/Input 2", "wb-led_46/Input 3"], function() {
    dev["wb-led_46/Channel 1"] = true;
    dev["wb-led_46/Channel 2"] = true;
});

// === Двойное нажатие ===
buttonDoublePress("wb-led_46/Input 1", function() {
    dev["wb-led_46/Channel 1"] = true;
    dev["wb-led_46/Channel 2"] = true;
});

buttonDoublePress(["wb-led_46/Input 2", "wb-led_46/Input 3"], function() {
    dev["wb-led_46/Channel 1"] = false;
    dev["wb-led_46/Channel 2"] = false;
});

// === Долгое нажатие ===
buttonLongPress("wb-led_46/Input 1", function() {
    dev["wb-led_46/Channel 1"] = false;
    dev["wb-led_46/Channel 2"] = false;
});

buttonLongPress(["wb-led_46/Input 2", "wb-led_46/Input 3"], function() {
    dev["wb-led_46/Channel 1"] = !dev["wb-led_46/Channel 1"];
    dev["wb-led_46/Channel 2"] = !dev["wb-led_46/Channel 2"];
});

Спасибо что поделились!