From d3d33b452f8f259d6be97c934ed0c1846c064ccb Mon Sep 17 00:00:00 2001 From: s-hadinger <49731213+s-hadinger@users.noreply.github.com> Date: Fri, 29 May 2026 13:41:31 +0200 Subject: [PATCH] Trigger events to Berry when 'USE_RULES' is not enabled (#24796) * Trigger events to Berry when 'USE_RULES' is not enabled * replace long with int32_t * Fix SystemEventsSetNewPower signature --- CHANGELOG.md | 1 + tasmota/tasmota_xdrv_driver/xdrv_10_rules.ino | 194 ++----- .../xdrv_10_system_events.ino | 474 ++++++++++++++++++ .../tasmota_xx2c_global/xdrv_interface.ino | 11 +- 4 files changed, 524 insertions(+), 156 deletions(-) create mode 100644 tasmota/tasmota_xdrv_driver/xdrv_10_system_events.ino diff --git a/CHANGELOG.md b/CHANGELOG.md index ada025c28..86a33a156 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ All notable changes to this project will be documented in this file. - ESP32 VID6608 hardware RMT support for Automotive gauge driver (#24759) - Support for uDisplay ST7305 (#24738) - Support for hosted MCU different from esp32c6 +- Trigger events to Berry when `USE_RULES` is not enabled ### Breaking Changed diff --git a/tasmota/tasmota_xdrv_driver/xdrv_10_rules.ino b/tasmota/tasmota_xdrv_driver/xdrv_10_rules.ino index 1db5ad24b..b8f6a68a9 100644 --- a/tasmota/tasmota_xdrv_driver/xdrv_10_rules.ino +++ b/tasmota/tasmota_xdrv_driver/xdrv_10_rules.ino @@ -140,6 +140,29 @@ const uint8_t kExpressionOperatorsPriorities[] PROGMEM = {1, 1, 2, 2, 3, 4}; #define BERRY_RULES 0 #endif +// Forward declarations for the shared System Events helpers defined in +// xdrv_10_system_events.ino. These symbols are always compiled (independent of +// USE_RULES / USE_SCRIPT / USE_BERRY) and are linked into every build. The +// Rules dispatcher (Xdrv10) calls Init/Every50ms/EverySecond/SaveBeforeRestart; +// the kRulesCommands table below resolves &CmndEvent to the externally-defined +// CmndEvent in xdrv_10_system_events.ino. +// +// SystemEventsSetTeleperiod / SystemEventsSetNewPower are thin setters exposed because +// struct SYSTEM_EVENTS is file-local to xdrv_10_system_events.ino and not visible from +// this translation unit. The Rules Xdrv10 dispatcher calls SystemEventsSetTeleperiod +// around FUNC_TELEPERIOD_RULES_PROCESS to mirror Rules.teleperiod onto +// SystemEvents.teleperiod; RulesSetPower (FUNC_SET_POWER) calls SystemEventsSetNewPower +// to snapshot the current power mask for the next 50ms tick. +extern void SystemEventsInit(void); +extern bool SystemEventsEvery50msPowerDimmerEvent(void); +extern void SystemEventsEvery50msRulesFlag(void); +extern void SystemEventsEvery50ms(void); +extern void SystemEventsEverySecond(void); +extern void SystemEventsSaveBeforeRestart(void); +extern void SystemEventsSetTeleperiod(bool v); +extern void SystemEventsSetNewPower(int32_t v); +extern void CmndEvent(void); + const char kRulesCommands[] PROGMEM = "|" // No prefix D_CMND_RULE "|" D_CMND_RULETIMER "|" D_CMND_EVENT "|" D_CMND_VAR "|" D_CMND_MEM "|" D_CMND_ADD "|" D_CMND_SUB "|" D_CMND_MULT "|" D_CMND_SCALE "|" D_CMND_CALC_RESOLUTION @@ -164,18 +187,11 @@ struct RULES { uint32_t triggers[MAX_RULE_SETS] = { 0 }; uint8_t trigger_count[MAX_RULE_SETS] = { 0 }; - long new_power = -1; - long old_power = -1; - long old_dimm = -1; - - uint16_t last_minute = 60; uint16_t vars_event = 0; // Bitmask supporting MAX_RULE_VARS bits uint16_t mems_event = 0; // Bitmask supporting MAX_RULE_MEMS bits bool teleperiod = false; bool busy = false; bool no_execute = false; // Don't actually execute rule commands - - char event_data[RULE_MAX_EVENTSZ]; } Rules; char rules_vars[MAX_RULE_VARS][33] = {{ 0 }}; @@ -940,63 +956,12 @@ void RulesEvery50ms(void) if ((Settings->rule_enabled || BERRY_RULES) && !Rules.busy) { // Any rule enabled char json_event[RULE_MAX_EVENTSZ +16]; // Add 16 chars for {"Event": .. } - if (-1 == Rules.new_power) { Rules.new_power = TasmotaGlobal.power; } - if (Rules.new_power != Rules.old_power) { - if (Rules.old_power != -1) { - for (uint32_t i = 0; i < TasmotaGlobal.devices_present; i++) { - uint8_t new_state = (Rules.new_power >> i) &1; - if (new_state != ((Rules.old_power >> i) &1)) { - snprintf_P(json_event, sizeof(json_event), PSTR("{\"Power%d\":{\"State\":%d}}"), i +1, new_state); - RulesProcessEvent(json_event); - } - } - } else { - // Boot time POWER OUTPUTS (Relays) Status - for (uint32_t i = 0; i < TasmotaGlobal.devices_present; i++) { - uint8_t new_state = (Rules.new_power >> i) &1; - snprintf_P(json_event, sizeof(json_event), PSTR("{\"Power%d\":{\"Boot\":%d}}"), i +1, new_state); - RulesProcessEvent(json_event); - } - // Boot time SWITCHES Status - for (uint32_t i = 0; i < MAX_SWITCHES_SET; i++) { - if (SwitchUsed(i)) { - snprintf_P(json_event, sizeof(json_event), PSTR("{\"%s\":{\"Boot\":%d}}"), GetSwitchText(i).c_str(), (SwitchState(i))); - RulesProcessEvent(json_event); - } - } - } - Rules.old_power = Rules.new_power; - } - else if (Rules.old_dimm != Settings->light_dimmer) { - if (Rules.old_dimm != -1) { - snprintf_P(json_event, sizeof(json_event), PSTR("{\"Dimmer\":{\"State\":%d}}"), Settings->light_dimmer); - } else { - // Boot time DIMMER VALUE - snprintf_P(json_event, sizeof(json_event), PSTR("{\"Dimmer\":{\"Boot\":%d}}"), Settings->light_dimmer); - } - RulesProcessEvent(json_event); - Rules.old_dimm = Settings->light_dimmer; - } - else if (Rules.event_data[0]) { - char *event; - char *parameter; - event = strtok_r(Rules.event_data, "=", ¶meter); // Rules.event_data = fanspeed=10 - if (event) { - event = Trim(event); - if (parameter) { - parameter = Trim(parameter); - } else { - parameter = event + strlen(event); // '\0' - } - bool quotes = (parameter[0] != '{'); - snprintf_P(json_event, sizeof(json_event), PSTR("{\"Event\":{\"%s\":%s%s%s}}"), event, (quotes)?"\"":"", parameter, (quotes)?"\"":""); - Rules.event_data[0] ='\0'; - RulesProcessEvent(json_event); - } else { - Rules.event_data[0] ='\0'; - } - } - else if (Rules.vars_event || Rules.mems_event){ + // Strict-priority drain to match pre-migration semantics: + // Power -> Dimmer -> Event (System Events) -> Vars/Mems (Rules) -> rules_flag (System Events) + // Each higher-priority branch yields the tick when it emits. + if (SystemEventsEvery50msPowerDimmerEvent()) { return; } + + if (Rules.vars_event || Rules.mems_event) { if (Rules.vars_event) { for (uint32_t i = 0; i < MAX_RULE_VARS; i++) { if (bitRead(Rules.vars_event, i)) { @@ -1017,70 +982,10 @@ void RulesEvery50ms(void) } } } + return; } - else if (TasmotaGlobal.rules_flag.data) { - json_event[0] = '\0'; - if (TasmotaGlobal.rules_flag.system_init) { - TasmotaGlobal.rules_flag.system_init = 0; - strncpy_P(json_event, PSTR("{\"System\":{\"Init\":1}}"), sizeof(json_event)); - } - else if (TasmotaGlobal.rules_flag.system_boot) { - TasmotaGlobal.rules_flag.system_boot = 0; - strncpy_P(json_event, PSTR("{\"System\":{\"Boot\":1}}"), sizeof(json_event)); - } - else if (TasmotaGlobal.rules_flag.time_init) { - TasmotaGlobal.rules_flag.time_init = 0; - snprintf_P(json_event, sizeof(json_event), PSTR("{\"Time\":{\"Initialized\":%d}}"), MinutesPastMidnight()); - } - else if (TasmotaGlobal.rules_flag.time_set) { - TasmotaGlobal.rules_flag.time_set = 0; - snprintf_P(json_event, sizeof(json_event), PSTR("{\"Time\":{\"Set\":%d}}"), MinutesPastMidnight()); - } - else if (TasmotaGlobal.rules_flag.mqtt_connected) { - TasmotaGlobal.rules_flag.mqtt_connected = 0; - strncpy_P(json_event, PSTR("{\"MQTT\":{\"Connected\":1}}"), sizeof(json_event)); - } - else if (TasmotaGlobal.rules_flag.mqtt_disconnected) { - TasmotaGlobal.rules_flag.mqtt_disconnected = 0; - strncpy_P(json_event, PSTR("{\"MQTT\":{\"Disconnected\":1}}"), sizeof(json_event)); - } - else if (TasmotaGlobal.rules_flag.wifi_connected) { - TasmotaGlobal.rules_flag.wifi_connected = 0; - strncpy_P(json_event, PSTR("{\"WIFI\":{\"Connected\":1}}"), sizeof(json_event)); - } - else if (TasmotaGlobal.rules_flag.wifi_disconnected) { - TasmotaGlobal.rules_flag.wifi_disconnected = 0; - strncpy_P(json_event, PSTR("{\"WIFI\":{\"Disconnected\":1}}"), sizeof(json_event)); - } -//#if defined(ESP32) && CONFIG_IDF_TARGET_ESP32 && defined(USE_ETHERNET) -#if defined(ESP32) && defined(USE_ETHERNET) - else if (TasmotaGlobal.rules_flag.eth_connected) { - TasmotaGlobal.rules_flag.eth_connected = 0; - strncpy_P(json_event, PSTR("{\"ETH\":{\"Connected\":1}}"), sizeof(json_event)); - } - else if (TasmotaGlobal.rules_flag.eth_disconnected) { - TasmotaGlobal.rules_flag.eth_disconnected = 0; - strncpy_P(json_event, PSTR("{\"ETH\":{\"Disconnected\":1}}"), sizeof(json_event)); - } -#endif // USE_ETHERNET - else if (TasmotaGlobal.rules_flag.http_init) { - TasmotaGlobal.rules_flag.http_init = 0; - strncpy_P(json_event, PSTR("{\"HTTP\":{\"Initialized\":1}}"), sizeof(json_event)); - } -#ifdef USE_SHUTTER - else if (TasmotaGlobal.rules_flag.shutter_moved) { - TasmotaGlobal.rules_flag.shutter_moved = 0; - strncpy_P(json_event, PSTR("{\"SHUTTER\":{\"Moved\":1}}"), sizeof(json_event)); - } - else if (TasmotaGlobal.rules_flag.shutter_moving) { - TasmotaGlobal.rules_flag.shutter_moving = 0; - strncpy_P(json_event, PSTR("{\"SHUTTER\":{\"Moving\":1}}"), sizeof(json_event)); - } -#endif // USE_SHUTTER - if (json_event[0]) { - RulesProcessEvent(json_event); // Only service one event within 50mS - } - } + + SystemEventsEvery50msRulesFlag(); } } @@ -1096,13 +1001,7 @@ void RulesEverySecond(void) { char json_event[120]; if ((Settings->rule_enabled || BERRY_RULES) && !Rules.busy) { // Any rule enabled - if (RtcTime.valid) { - if ((TasmotaGlobal.uptime > 60) && (RtcTime.minute != Rules.last_minute)) { // Execute from one minute after restart every minute only once - Rules.last_minute = RtcTime.minute; - snprintf_P(json_event, sizeof(json_event), PSTR("{\"Time\":{\"Minute\":%d}}"), MinutesPastMidnight()); - RulesProcessEvent(json_event); - } - } + SystemEventsEverySecond(); // Time#Minute (shared producer, gated to preserve pre-migration semantics) } for (uint32_t i = 0; i < MAX_RULE_TIMERS; i++) { if (Rules.timer[i] != 0L) { // Timer active? @@ -1120,10 +1019,7 @@ void RulesEverySecond(void) void RulesSaveBeforeRestart(void) { if ((Settings->rule_enabled || BERRY_RULES) && !Rules.busy) { // Any rule enabled - char json_event[32]; - - strncpy_P(json_event, PSTR("{\"System\":{\"Save\":1}}"), sizeof(json_event)); - RulesProcessEvent(json_event); + SystemEventsSaveBeforeRestart(); // System#Save (shared producer, gated to preserve pre-migration semantics) } } @@ -1147,7 +1043,7 @@ void RulesShow() { void RulesSetPower(void) { - Rules.new_power = XdrvMailbox.index; + SystemEventsSetNewPower(XdrvMailbox.index); } #ifdef SUPPORT_MQTT_EVENT @@ -2352,17 +2248,6 @@ void CmndRuleTimer(void) ResponseJsonEnd(); } -void CmndEvent(void) -{ - if (XdrvMailbox.data_len > 0) { - strlcpy(Rules.event_data, XdrvMailbox.data, sizeof(Rules.event_data)); -#ifdef USE_DEVICE_GROUPS - if (!XdrvMailbox.grpflg) SendDeviceGroupMessage(1, DGR_MSGTYP_UPDATE, DGR_ITEM_EVENT, XdrvMailbox.data); -#endif // USE_DEVICE_GROUPS - } - if (XdrvMailbox.command) ResponseCmndDone(); -} - void CmndVariable(void) { if ((XdrvMailbox.index > 0) && (XdrvMailbox.index <= MAX_RULE_VARS)) { @@ -2484,13 +2369,13 @@ bool Xdrv10(uint32_t function) switch (function) { case FUNC_EVERY_50_MSECOND: - RulesEvery50ms(); + RulesEvery50ms(); // drives full priority ladder: Power/Dimmer/Event -> Vars/Mems -> rules_flag break; case FUNC_EVERY_100_MSECOND: RulesEvery100ms(); break; case FUNC_EVERY_SECOND: - RulesEverySecond(); + RulesEverySecond(); // drives Time#Minute (gated) + Rules.timer[] (RuleTimer) break; case FUNC_SET_POWER: RulesSetPower(); @@ -2503,11 +2388,13 @@ bool Xdrv10(uint32_t function) break; case FUNC_TELEPERIOD_RULES_PROCESS: Rules.teleperiod = true; + SystemEventsSetTeleperiod(true); // mirror, since SystemEventsEvery50ms reads it result = RulesProcess(); Rules.teleperiod = false; + SystemEventsSetTeleperiod(false); break; case FUNC_SAVE_BEFORE_RESTART: - RulesSaveBeforeRestart(); + RulesSaveBeforeRestart(); // gated wrapper around SystemEventsSaveBeforeRestart break; #ifdef SUPPORT_MQTT_EVENT case FUNC_MQTT_DATA: @@ -2516,6 +2403,7 @@ bool Xdrv10(uint32_t function) #endif // SUPPORT_MQTT_EVENT case FUNC_PRE_INIT: RulesInit(); + SystemEventsInit(); break; #ifdef USE_WEBSERVER #ifdef USE_VIEW_RULE_MEMS_AND_VARS diff --git a/tasmota/tasmota_xdrv_driver/xdrv_10_system_events.ino b/tasmota/tasmota_xdrv_driver/xdrv_10_system_events.ino new file mode 100644 index 000000000..63bdb353b --- /dev/null +++ b/tasmota/tasmota_xdrv_driver/xdrv_10_system_events.ino @@ -0,0 +1,474 @@ +/* + xdrv_10_system_events.ino - system events producer for Tasmota + + Copyright (C) 2021 ESP Easy Group and Theo Arends + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +*/ + +/*********************************************************************************************\ + * System events producer + * + * This file is the single source of truth for emitting Tasmota's "system" rules-events: + * - Power#State / Power#Boot, #Boot + * - Dimmer#State / Dimmer#Boot + * - Time#Minute, Time#Initialized, Time#Set + * - System#Init, System#Boot, System#Save + * - MQTT#Connected, MQTT#Disconnected + * - WIFI#Connected, WIFI#Disconnected + * - ETH#Connected, ETH#Disconnected (ESP32 + USE_ETHERNET only) + * - HTTP#Initialized + * - SHUTTER#Moved, SHUTTER#Moving (USE_SHUTTER only) + * - Event# (user-driven `Event` command) + * + * The emission helpers (SystemEventsInit, SystemEventsEvery50ms, SystemEventsEverySecond, + * SystemEventsSaveBeforeRestart, CmndEvent) are always compiled, regardless of USE_RULES, + * USE_SCRIPT, or USE_BERRY. They are defined exactly once across the codebase, in this file. + * + * Slot-10 mutual exclusion + * ------------------------ + * Exactly one of the three slot-10 files registers Xdrv10 / XDRV_10 per build: + * + * USE_RULES defined -> xdrv_10_rules.ino owns Xdrv10 (calls helpers from here) + * USE_SCRIPT defined (no USE_RULES) -> xdrv_10_scripter.ino owns Xdrv10 (helpers here unused) + * neither defined -> this file owns Xdrv10 (calls helpers locally) + * + * The conditional Xdrv10 / RulesProcessEvent / RulesProcess stub for the "neither" build is + * gated by `#ifndef USE_RULES` / `#ifndef USE_SCRIPT` and added in a separate sub-task. + * + * Maintenance hazard: RulesBitfield layout + * ---------------------------------------- + * The strict-priority drainage in SystemEventsEvery50ms and the inline `rules_flag.*` polls + * in xdrv_10_scripter.ino both depend on the bit layout of RulesBitfield in + * tasmota/include/tasmota_types.h. Any change to RulesBitfield (adding, removing, or + * reordering bits) requires matching updates here AND in xdrv_10_rules.ino AND in + * xdrv_10_scripter.ino. The comment next to the RulesBitfield definition in + * tasmota_types.h is the canonical reminder for this contract. +\*********************************************************************************************/ + +#ifndef RULE_MAX_EVENTSZ +#define RULE_MAX_EVENTSZ 100 +#endif + +struct SYSTEM_EVENTS { + int32_t new_power = -1; // current TasmotaGlobal.power snapshot + int32_t old_power = -1; // last seen power mask; -1 == "never sampled" (boot path) + int32_t old_dimm = -1; // last seen Settings->light_dimmer; -1 == boot path + uint16_t last_minute = 60; // last RtcTime.minute that fired Time#Minute (60 = invalid sentinel) + bool teleperiod = false; // set by FUNC_TELEPERIOD_RULES_PROCESS in the active dispatcher + bool busy = false; // re-entry guard for the local RulesProcessEvent stub + char event_data[RULE_MAX_EVENTSZ]; // single-slot queue for the `Event` command +} SystemEvents; + +/*********************************************************************************************\ + * Producer initialization +\*********************************************************************************************/ + +void SystemEventsInit(void) { + // Start with all rules-flag bits clear so the first 50ms drainage tick has a clean slate. + // Settings->rule_enabled and Settings->rule_once are intentionally NOT touched here; they + // are part of the Rules-engine domain and remain owned by xdrv_10_rules.ino. + TasmotaGlobal.rules_flag.data = 0; + SystemEvents.teleperiod = false; +} + +/*********************************************************************************************\ + * Cross-translation-unit setters + * + * `xdrv_10_rules.ino` (in USE_RULES builds) needs to write a couple of fields in + * `struct SYSTEM_EVENTS`, but the struct definition above is file-scoped and not visible + * from `xdrv_10_rules.ino` (each .ino is a separate translation unit). Rather than promote + * the struct definition to a shared header just to expose two fields, these tiny + * inline-style setters are exposed as `extern` from the rules dispatcher. The cost is one + * extra function call per access; the benefit is keeping `struct SYSTEM_EVENTS` private to + * this producer. + * + * - `SystemEventsSetTeleperiod` mirrors `Rules.teleperiod` around + * `FUNC_TELEPERIOD_RULES_PROCESS` so `SystemEventsEvery50ms` sees the right value. + * - `SystemEventsSetNewPower` is called from `RulesSetPower` (Rules' `FUNC_SET_POWER` hook) + * to update the snapshot of the current power mask so the next 50ms tick can detect the + * transition and emit `Power#State`. +\*********************************************************************************************/ + +void SystemEventsSetTeleperiod(bool v) { + SystemEvents.teleperiod = v; +} + +void SystemEventsSetNewPower(int32_t v) { + SystemEvents.new_power = v; +} + +/*********************************************************************************************\ + * 50ms producer tick + * + * Verbatim port of the Power/Dimmer/Event/rules_flag branches of RulesEvery50ms() from + * xdrv_10_rules.ino, MINUS the Vars/Mems-events branch (which stays in Rules) and MINUS the + * `Settings->rule_enabled` outer gate (this producer always runs whenever its dispatcher + * calls it; the active RulesProcessEvent owns its own re-entry guard). + * + * The producer is split into two halves so the Rules dispatcher can interleave its + * Rules-engine-only Vars/Mems drain between them, restoring the original strict-priority + * else-if ladder: + * + * 1. SystemEventsEvery50msPowerDimmerEvent() — Power, Dimmer, queued Event (returns true + * if any branch emitted, telling the caller to yield this tick). + * 2. (Rules dispatcher) Vars/Mems drain. + * 3. SystemEventsEvery50msRulesFlag() — rules_flag bitfield drain (one bit per tick). + * + * The combined SystemEventsEvery50ms() wrapper below preserves the legacy single-call API + * and is used by the standalone dispatcher (neither USE_RULES nor USE_SCRIPT) where there + * is no Vars/Mems drain to interleave. + * + * At-most-one-event-per-tick discipline: each branch that emits MUST `return` before + * subsequent branches are evaluated. Branch 1 (boot fan-out) is the documented exception + * during the very first tick, where multiple Power#Boot and switch Boot events are emitted + * back-to-back. + * + * Strict-priority drain inside the rules_flag bitfield mirrors today's ordering exactly: + * system_init > system_boot > time_init > time_set + * > mqtt_connected > mqtt_disconnected + * > wifi_connected > wifi_disconnected + * > eth_connected > eth_disconnected (ESP32 + USE_ETHERNET only) + * > http_init + * > shutter_moved > shutter_moving (USE_SHUTTER only) +\*********************************************************************************************/ + +// Returns true if a Power/Dimmer/Event branch emitted (caller should yield this tick). +bool SystemEventsEvery50msPowerDimmerEvent(void) { + char json_event[RULE_MAX_EVENTSZ + 16]; // Add 16 chars for {"Event": .. } + + if (-1 == SystemEvents.new_power) { SystemEvents.new_power = TasmotaGlobal.power; } + + // Branch 1: Power state / boot + if (SystemEvents.new_power != SystemEvents.old_power) { + if (SystemEvents.old_power != -1) { + for (uint32_t i = 0; i < TasmotaGlobal.devices_present; i++) { + uint8_t new_state = (SystemEvents.new_power >> i) & 1; + if (new_state != ((SystemEvents.old_power >> i) & 1)) { + snprintf_P(json_event, sizeof(json_event), PSTR("{\"Power%d\":{\"State\":%d}}"), i + 1, new_state); + RulesProcessEvent(json_event); + } + } + } else { + // Boot time POWER OUTPUTS (Relays) Status + for (uint32_t i = 0; i < TasmotaGlobal.devices_present; i++) { + uint8_t new_state = (SystemEvents.new_power >> i) & 1; + snprintf_P(json_event, sizeof(json_event), PSTR("{\"Power%d\":{\"Boot\":%d}}"), i + 1, new_state); + RulesProcessEvent(json_event); + } + // Boot time SWITCHES Status + for (uint32_t i = 0; i < MAX_SWITCHES_SET; i++) { + if (SwitchUsed(i)) { + snprintf_P(json_event, sizeof(json_event), PSTR("{\"%s\":{\"Boot\":%d}}"), GetSwitchText(i).c_str(), (SwitchState(i))); + RulesProcessEvent(json_event); + } + } + } + SystemEvents.old_power = SystemEvents.new_power; + return true; + } + + // Branch 2: Dimmer state / boot + if (SystemEvents.old_dimm != Settings->light_dimmer) { + if (SystemEvents.old_dimm != -1) { + snprintf_P(json_event, sizeof(json_event), PSTR("{\"Dimmer\":{\"State\":%d}}"), Settings->light_dimmer); + } else { + // Boot time DIMMER VALUE + snprintf_P(json_event, sizeof(json_event), PSTR("{\"Dimmer\":{\"Boot\":%d}}"), Settings->light_dimmer); + } + RulesProcessEvent(json_event); + SystemEvents.old_dimm = Settings->light_dimmer; + return true; + } + + // Branch 3: queued `Event` command (single-slot queue) + if (SystemEvents.event_data[0]) { + char *event; + char *parameter; + event = strtok_r(SystemEvents.event_data, "=", ¶meter); // SystemEvents.event_data = fanspeed=10 + if (event) { + event = Trim(event); + if (parameter) { + parameter = Trim(parameter); + } else { + parameter = event + strlen(event); // '\0' + } + bool quotes = (parameter[0] != '{'); + snprintf_P(json_event, sizeof(json_event), PSTR("{\"Event\":{\"%s\":%s%s%s}}"), event, (quotes) ? "\"" : "", parameter, (quotes) ? "\"" : ""); + SystemEvents.event_data[0] = '\0'; + RulesProcessEvent(json_event); + } else { + SystemEvents.event_data[0] = '\0'; + } + return true; + } + + return false; +} + +// rules_flag bitfield drainage — strict priority, one bit per tick. +void SystemEventsEvery50msRulesFlag(void) { + if (TasmotaGlobal.rules_flag.data) { + char json_event[RULE_MAX_EVENTSZ + 16]; + json_event[0] = '\0'; + if (TasmotaGlobal.rules_flag.system_init) { + TasmotaGlobal.rules_flag.system_init = 0; + strncpy_P(json_event, PSTR("{\"System\":{\"Init\":1}}"), sizeof(json_event)); + } + else if (TasmotaGlobal.rules_flag.system_boot) { + TasmotaGlobal.rules_flag.system_boot = 0; + strncpy_P(json_event, PSTR("{\"System\":{\"Boot\":1}}"), sizeof(json_event)); + } + else if (TasmotaGlobal.rules_flag.time_init) { + TasmotaGlobal.rules_flag.time_init = 0; + snprintf_P(json_event, sizeof(json_event), PSTR("{\"Time\":{\"Initialized\":%d}}"), MinutesPastMidnight()); + } + else if (TasmotaGlobal.rules_flag.time_set) { + TasmotaGlobal.rules_flag.time_set = 0; + snprintf_P(json_event, sizeof(json_event), PSTR("{\"Time\":{\"Set\":%d}}"), MinutesPastMidnight()); + } + else if (TasmotaGlobal.rules_flag.mqtt_connected) { + TasmotaGlobal.rules_flag.mqtt_connected = 0; + strncpy_P(json_event, PSTR("{\"MQTT\":{\"Connected\":1}}"), sizeof(json_event)); + } + else if (TasmotaGlobal.rules_flag.mqtt_disconnected) { + TasmotaGlobal.rules_flag.mqtt_disconnected = 0; + strncpy_P(json_event, PSTR("{\"MQTT\":{\"Disconnected\":1}}"), sizeof(json_event)); + } + else if (TasmotaGlobal.rules_flag.wifi_connected) { + TasmotaGlobal.rules_flag.wifi_connected = 0; + strncpy_P(json_event, PSTR("{\"WIFI\":{\"Connected\":1}}"), sizeof(json_event)); + } + else if (TasmotaGlobal.rules_flag.wifi_disconnected) { + TasmotaGlobal.rules_flag.wifi_disconnected = 0; + strncpy_P(json_event, PSTR("{\"WIFI\":{\"Disconnected\":1}}"), sizeof(json_event)); + } +#if defined(ESP32) && defined(USE_ETHERNET) + else if (TasmotaGlobal.rules_flag.eth_connected) { + TasmotaGlobal.rules_flag.eth_connected = 0; + strncpy_P(json_event, PSTR("{\"ETH\":{\"Connected\":1}}"), sizeof(json_event)); + } + else if (TasmotaGlobal.rules_flag.eth_disconnected) { + TasmotaGlobal.rules_flag.eth_disconnected = 0; + strncpy_P(json_event, PSTR("{\"ETH\":{\"Disconnected\":1}}"), sizeof(json_event)); + } +#endif // ESP32 && USE_ETHERNET + else if (TasmotaGlobal.rules_flag.http_init) { + TasmotaGlobal.rules_flag.http_init = 0; + strncpy_P(json_event, PSTR("{\"HTTP\":{\"Initialized\":1}}"), sizeof(json_event)); + } +#ifdef USE_SHUTTER + else if (TasmotaGlobal.rules_flag.shutter_moved) { + TasmotaGlobal.rules_flag.shutter_moved = 0; + strncpy_P(json_event, PSTR("{\"SHUTTER\":{\"Moved\":1}}"), sizeof(json_event)); + } + else if (TasmotaGlobal.rules_flag.shutter_moving) { + TasmotaGlobal.rules_flag.shutter_moving = 0; + strncpy_P(json_event, PSTR("{\"SHUTTER\":{\"Moving\":1}}"), sizeof(json_event)); + } +#endif // USE_SHUTTER + if (json_event[0]) { + RulesProcessEvent(json_event); // Only service one event within 50ms + } + } +} + +// Legacy single-call wrapper. Used by the standalone Xdrv10 dispatcher in the +// "neither USE_RULES nor USE_SCRIPT" build, where there is no Vars/Mems drain to interleave. +void SystemEventsEvery50ms(void) { + if (SystemEventsEvery50msPowerDimmerEvent()) { return; } + SystemEventsEvery50msRulesFlag(); +} + +/*********************************************************************************************\ + * Per-second producer tick + * + * Emits Time#Minute once per RTC minute, but only after the device has been up for at least + * 60 seconds AND the RTC reports a valid time. The 60-second guard suppresses a spurious + * Time#Minute emission during boot when last_minute is still the sentinel (60). + * + * Note: this function does NOT emit Rules#Timer. The RuleTimer[] machinery and its + * {"Rules":{"Timer":}} emission remain a Rules-engine concern in xdrv_10_rules.ino; + * Rules timers are not part of the system-event surface that we expose to Scripter or to + * the "neither" build mode. +\*********************************************************************************************/ + +void SystemEventsEverySecond(void) { + if (RtcTime.valid && (TasmotaGlobal.uptime > 60) && (RtcTime.minute != SystemEvents.last_minute)) { + // Execute from one minute after restart every minute only once + SystemEvents.last_minute = RtcTime.minute; + char json_event[120]; + snprintf_P(json_event, sizeof(json_event), PSTR("{\"Time\":{\"Minute\":%d}}"), MinutesPastMidnight()); + RulesProcessEvent(json_event); + } +} + +/*********************************************************************************************\ + * Save-before-restart producer hook + * + * Emits System#Save right before the device persists its config and restarts, giving rules + * (or Berry handlers in the "neither" build) a final opportunity to react. Gating against + * Settings->rule_enabled / Rules.busy is the responsibility of the active RulesProcessEvent + * implementation, not of this producer. +\*********************************************************************************************/ + +void SystemEventsSaveBeforeRestart(void) { + RulesProcessEvent("{\"System\":{\"Save\":1}}"); +} + +/*********************************************************************************************\ + * `Event` command handler + * + * Queues a user-supplied `key=value` payload into the single-slot `SystemEvents.event_data` + * buffer. The next `SystemEventsEvery50ms` tick will tokenize and emit it as a + * `{"Event":{"":}}` JSON event (Branch 3 of the 50ms producer tick). + * + * This is the SINGLE definition of `CmndEvent` for every build mode. Rules' `kRulesCommands` + * table points at this externally-defined symbol via an `extern void CmndEvent(void);` in + * `xdrv_10_rules.ino`. Scripter does not register the `Event` command and is unaffected by + * this symbol. + * + * In `USE_SCRIPT`-only builds, this function is still linked (always-compiled) but no + * command table references it; it is dead code in that build mode. Acceptable cost for + * keeping Scripter untouched. +\*********************************************************************************************/ + +void CmndEvent(void) { + if (XdrvMailbox.data_len > 0) { + strlcpy(SystemEvents.event_data, XdrvMailbox.data, sizeof(SystemEvents.event_data)); +#ifdef USE_DEVICE_GROUPS + if (!XdrvMailbox.grpflg) SendDeviceGroupMessage(1, DGR_MSGTYP_UPDATE, DGR_ITEM_EVENT, XdrvMailbox.data); +#endif // USE_DEVICE_GROUPS + } + if (XdrvMailbox.command) ResponseCmndDone(); +} + +/*********************************************************************************************\ + * Conditional Xdrv10 / RulesProcessEvent / RulesProcess stub + * + * Compiled ONLY when neither USE_RULES nor USE_SCRIPT is defined ("neither" build mode). + * In this mode, this file owns slot 10 and provides minimal local definitions of the + * symbols that the rest of Tasmota expects: `RulesProcessEvent`, `RulesProcess`, and the + * `Xdrv10` dispatcher. In USE_RULES builds, xdrv_10_rules.ino owns these symbols; in + * USE_SCRIPT-only builds, xdrv_10_scripter.ino owns them. + * + * The local `RulesProcessEvent` stub provides: + * - Berry forwarding (under USE_BERRY) BEFORE the busy guard, matching Rules' ordering. + * - A re-entry guard via `SystemEvents.busy` (independent of `Rules.busy`). + * - The legacy `{"k":"v"}` -> `{"k":{"Data":"v"}}` rewrite on a local String copy when + * the event contains exactly one `:`. The rewritten string is not consumed by anyone + * in this build (no rule sets, no Scripter), but is computed for parity so the + * observable side-effects (String allocation, toUpperCase) match Rules' behavior. +\*********************************************************************************************/ + +#ifndef USE_RULES +#ifndef USE_SCRIPT + +bool RulesProcessEvent(const char *json_event) { +#ifdef USE_BERRY + // Events are passed to Berry before any local processing, mirroring Rules' ordering. + // Berry has its own busy/re-entry handling internally. + callBerryRule(json_event, SystemEvents.teleperiod); +#endif // USE_BERRY + + if (SystemEvents.busy) { return false; } + if (!strlen(json_event)) { return true; } + + SystemEvents.busy = true; + + // Data: rewrite — preserved bit-for-bit from xdrv_10_rules.ino's RulesProcessEvent. + // json_event = {"INA219":{"Voltage":4.494,"Current":0.020,"Power":0.089}} -> unchanged (two colons) + // json_event = {"System":{"Boot":1}} -> unchanged (two colons) + // json_event = {"SerialReceived":"on"} -> {"SerialReceived":{"Data":"on"}} + String event_saved = json_event; + char *p = strchr(json_event, ':'); + if ((p != NULL) && !(strchr(++p, ':'))) { // Find second colon + event_saved.replace(F(":"), F(":{\"Data\":")); + event_saved += F("}"); + } + event_saved.toUpperCase(); + // event_saved is intentionally not consumed further: there are no rule sets to evaluate + // in this build mode and Scripter is absent. Computing it preserves observable behavior + // (allocation/toUpperCase) and keeps a documented hook for future consumers. + + SystemEvents.busy = false; + return true; +} + +// Local `RulesProcess` stub — mirrors xdrv_10_rules.ino's `RulesProcess` shape so the +// `Xdrv10` dispatcher (added in task 2.3) can route `FUNC_RULES_PROCESS` and +// `FUNC_TELEPERIOD_RULES_PROCESS` here. The busy guard is checked against +// `SystemEvents.busy` (independent of `Rules.busy`, which does not exist in this build). +bool RulesProcess(void) { + if (!SystemEvents.busy) { + return RulesProcessEvent(XdrvMailbox.data); + } + return false; +} + +/*********************************************************************************************\ + * Slot-10 ownership: Xdrv10 dispatcher and command table + * + * In this "neither" build mode (no USE_RULES, no USE_SCRIPT), this file owns slot 10. The + * dispatcher routes the standard FUNC_* callbacks to the producer helpers defined above, + * plus the local `RulesProcess` stub for FUNC_RULES_PROCESS / FUNC_TELEPERIOD_RULES_PROCESS. + * + * `D_CMND_EVENT` is defined locally because xdrv_10_rules.ino (which normally defines it) + * is not compiled in this build mode. +\*********************************************************************************************/ + +#define XDRV_10 10 + +#define D_CMND_EVENT "Event" + +const char kSystemEventsCommands[] PROGMEM = "|" D_CMND_EVENT; + +void (* const SystemEventsCommand[])(void) PROGMEM = { &CmndEvent }; + +bool Xdrv10(uint32_t function) { + bool result = false; + + switch (function) { + case FUNC_PRE_INIT: + SystemEventsInit(); + break; + case FUNC_EVERY_50_MSECOND: + SystemEventsEvery50ms(); + break; + case FUNC_EVERY_SECOND: + SystemEventsEverySecond(); + break; + case FUNC_SAVE_BEFORE_RESTART: + SystemEventsSaveBeforeRestart(); + break; + case FUNC_RULES_PROCESS: + result = RulesProcess(); + break; + case FUNC_TELEPERIOD_RULES_PROCESS: + SystemEvents.teleperiod = true; + result = RulesProcess(); + SystemEvents.teleperiod = false; + break; + case FUNC_COMMAND: + result = DecodeCommand(kSystemEventsCommands, SystemEventsCommand); + break; + case FUNC_ACTIVE: + result = true; + break; + } + return result; +} + +#endif // !USE_SCRIPT +#endif // !USE_RULES diff --git a/tasmota/tasmota_xx2c_global/xdrv_interface.ino b/tasmota/tasmota_xx2c_global/xdrv_interface.ino index fb32682ee..f7d9ea395 100644 --- a/tasmota/tasmota_xx2c_global/xdrv_interface.ino +++ b/tasmota/tasmota_xx2c_global/xdrv_interface.ino @@ -1088,11 +1088,16 @@ bool XdrvRulesProcess(bool teleperiod, const char* event) { char* data_save = XdrvMailbox.data; XdrvMailbox.data = (char*)event; bool rule_handled = XdrvCallDriver(10, (teleperiod) ? FUNC_TELEPERIOD_RULES_PROCESS : FUNC_RULES_PROCESS); -#if defined(USE_BERRY) && !defined(USE_RULES) - // events are sent to Berry in Rules driver, or here if USE_RULES is not defined (only on a subset) +#if defined(USE_BERRY) && !defined(USE_RULES) && defined(USE_SCRIPT) + // Berry forwarding for events going through XdrvRulesProcess: + // - USE_RULES build: xdrv_10_rules.ino's RulesProcessEvent calls callBerryRule itself. + // - USE_SCRIPT only: Scripter's RulesProcessEvent does NOT call Berry, so we forward here. + // - "neither" build: xdrv_10_system_events.ino's RulesProcessEvent calls callBerryRule itself. + // Forwarding is therefore only needed when Scripter owns slot 10, otherwise Berry receives + // the event twice (once via slot 10's dispatcher and once via this fallback). bool berry_handled = XdrvCallDriver(52, FUNC_RULES_PROCESS); rule_handled |= berry_handled; -#endif // USE_BERRY and No USE_RULES +#endif // USE_BERRY && !USE_RULES && USE_SCRIPT XdrvMailbox.data = data_save; return rule_handled; }