mirror of
https://github.com/arendst/Tasmota.git
synced 2026-07-27 20:05:46 +00:00
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
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
/*********************************************************************************************\
|
||||
* System events producer
|
||||
*
|
||||
* This file is the single source of truth for emitting Tasmota's "system" rules-events:
|
||||
* - Power<i>#State / Power<i>#Boot, <switch>#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#<key> (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<i>#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":<i>}} 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":{"<key>":<rendered_value>}}` 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
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user