diff --git a/code/espurna/config/defaults.h b/code/espurna/config/defaults.h index acb4c8ff..eb7dc30f 100644 --- a/code/espurna/config/defaults.h +++ b/code/espurna/config/defaults.h @@ -473,6 +473,10 @@ #define RELAY8_DELAY_OFF 0 #endif +#ifndef RELAY_DELAY_INTERLOCK +#define RELAY_DELAY_INTERLOCK 0 +#endif + // ----------------------------------------------------------------------------- // LEDs // ----------------------------------------------------------------------------- diff --git a/code/espurna/config/prototypes.h b/code/espurna/config/prototypes.h index 48953e94..8da526b4 100644 --- a/code/espurna/config/prototypes.h +++ b/code/espurna/config/prototypes.h @@ -1,6 +1,7 @@ #include #include #include +#include #include #include diff --git a/code/espurna/relay.ino b/code/espurna/relay.ino index c8e2d585..f2c1aae2 100644 --- a/code/espurna/relay.ino +++ b/code/espurna/relay.ino @@ -31,7 +31,8 @@ typedef struct { unsigned char lock; // Holds the value of target status, that cannot be changed afterwards. (0 for false, 1 for true, 2 to disable) unsigned long fw_start; // Flood window start time unsigned char fw_count; // Number of changes within the current flood window - unsigned long change_time; // Scheduled time to change + unsigned long change_start; // Time when relay was scheduled to change + unsigned long change_delay; // Delay until the next change bool report; // Whether to report to own topic bool group_report; // Whether to report to group topic @@ -42,7 +43,22 @@ typedef struct { } relay_t; std::vector _relays; bool _relayRecursive = false; -Ticker _relaySaveTicker; + +unsigned long _relay_flood_window = (1000 * RELAY_FLOOD_WINDOW); +unsigned long _relay_flood_changes = RELAY_FLOOD_CHANGES; + +unsigned long _relay_delay_interlock; +unsigned char _relay_sync_mode = RELAY_SYNC_ANY; +bool _relay_sync_locked = false; + +Ticker _relay_save_timer; +Ticker _relay_sync_timer; + +#if WEB_SUPPORT + +bool _relay_report_ws = false; + +#endif // WEB_SUPPORT #if MQTT_SUPPORT @@ -82,6 +98,69 @@ RelayStatus _relayStatusTyped(unsigned char id) { return (status) ? RelayStatus::ON : RelayStatus::OFF; } +void _relayLockAll() { + for (auto& relay : _relays) { + relay.lock = relay.target_status; + } + _relay_sync_locked = true; +} + +void _relayUnlockAll() { + for (auto& relay : _relays) { + relay.lock = RELAY_LOCK_DISABLED; + } + _relay_sync_locked = false; +} + +bool _relayStatusLock(unsigned char id, bool status) { + if (_relays[id].lock != RELAY_LOCK_DISABLED) { + bool lock = _relays[id].lock == RELAY_LOCK_ON; + if ((lock != status) || (lock != _relays[id].target_status)) { + _relays[id].target_status = lock; + _relays[id].change_delay = 0; + return false; + } + } + + return true; +} + +// https://github.com/xoseperez/espurna/issues/1510#issuecomment-461894516 +// completely reset timing on the other relay to sync with this one +// to ensure that they change state sequentially +void _relaySyncRelaysDelay(unsigned char first, unsigned char second) { + _relays[second].fw_start = _relays[first].change_start; + _relays[second].fw_count = 1; + _relays[second].change_delay = std::max({ + _relay_delay_interlock, + _relays[first].change_delay, + _relays[second].change_delay + }); +} + +void _relaySyncUnlock() { + bool unlock = true; + bool all_off = true; + for (const auto& relay : _relays) { + unlock = unlock && (relay.current_status == relay.target_status); + if (!unlock) break; + all_off = all_off && !relay.current_status; + } + + if (!unlock) return; + + auto action = []() { + _relayUnlockAll(); + _relay_report_ws = true; + }; + + if (all_off) { + _relay_sync_timer.once_ms(_relay_delay_interlock, action); + } else { + action(); + } +} + // ----------------------------------------------------------------------------- // RELAY PROVIDERS // ----------------------------------------------------------------------------- @@ -201,7 +280,7 @@ void _relayProviderStatus(unsigned char id, bool status) { */ void _relayProcess(bool mode) { - unsigned long current_time = millis(); + bool changed = false; for (unsigned char id = 0; id < _relays.size(); id++) { @@ -213,25 +292,12 @@ void _relayProcess(bool mode) { // Only process the relays we have to change to the requested mode if (target != mode) continue; - // Only process the relays that can be changed - switch (_relays[id].lock) { - case RELAY_LOCK_ON: - case RELAY_LOCK_OFF: - { - bool lock = _relays[id].lock == 1; - if (lock != _relays[id].target_status) { - _relays[id].target_status = lock; - continue; - } - break; - } - case RELAY_LOCK_DISABLED: - default: - break; - } + // Only process if the change delay has expired + if (millis() - _relays[id].change_start < _relays[id].change_delay) continue; - // Only process if the change_time has arrived - if (current_time < _relays[id].change_time) continue; + // Purge existing delay in case of cancelation + _relays[id].change_delay = 0; + changed = true; DEBUG_MSG_P(PSTR("[RELAY] #%d set to %s\n"), id, target ? "ON" : "OFF"); @@ -248,6 +314,10 @@ void _relayProcess(bool mode) { relayMQTT(id); #endif + #if WEB_SUPPORT + _relay_report_ws = true; + #endif + if (!_relayRecursive) { relayPulse(id); @@ -256,11 +326,7 @@ void _relayProcess(bool mode) { // we care about current relay status on boot unsigned char boot_mode = getSetting("relayBoot", id, RELAY_BOOT_MODE).toInt(); bool save_eeprom = ((RELAY_BOOT_SAME == boot_mode) || (RELAY_BOOT_TOGGLE == boot_mode)); - _relaySaveTicker.once_ms(RELAY_SAVE_DELAY, relaySave, save_eeprom); - - #if WEB_SUPPORT - wsPost(_relayWebSocketUpdate); - #endif + _relay_save_timer.once_ms(RELAY_SAVE_DELAY, relaySave, save_eeprom); } @@ -269,6 +335,12 @@ void _relayProcess(bool mode) { } + // Whenever we are using sync modes and any relay had changed the state, check if we can unlock + const bool needs_unlock = ((_relay_sync_mode == RELAY_SYNC_NONE_OR_ONE) || (_relay_sync_mode == RELAY_SYNC_ONE)); + if (_relay_sync_locked && needs_unlock && changed) { + _relaySyncUnlock(); + } + } #if defined(ITEAD_SONOFF_IFAN02) @@ -337,6 +409,13 @@ bool relayStatus(unsigned char id, bool status, bool report, bool group_report) if (id >= _relays.size()) return false; + if (!_relayStatusLock(id, status)) { + DEBUG_MSG_P(PSTR("[RELAY] #%d is locked to %s\n"), id, _relays[id].current_status ? "ON" : "OFF"); + _relays[id].report = true; + _relays[id].group_report = true; + return false; + } + bool changed = false; if (_relays[id].current_status == status) { @@ -346,6 +425,7 @@ bool relayStatus(unsigned char id, bool status, bool report, bool group_report) _relays[id].target_status = status; _relays[id].report = false; _relays[id].group_report = false; + _relays[id].change_delay = 0; changed = true; } @@ -360,27 +440,29 @@ bool relayStatus(unsigned char id, bool status, bool report, bool group_report) } else { unsigned long current_time = millis(); - unsigned long fw_end = _relays[id].fw_start + 1000 * RELAY_FLOOD_WINDOW; - unsigned long delay = status ? _relays[id].delay_on : _relays[id].delay_off; + unsigned long change_delay = status ? _relays[id].delay_on : _relays[id].delay_off; _relays[id].fw_count++; - _relays[id].change_time = current_time + delay; + _relays[id].change_start = current_time; + _relays[id].change_delay = std::max(_relays[id].change_delay, change_delay); // If current_time is off-limits the floodWindow... - if (current_time < _relays[id].fw_start || fw_end <= current_time) { + const auto fw_diff = current_time - _relays[id].fw_start; + if (fw_diff > _relay_flood_window) { // We reset the floodWindow _relays[id].fw_start = current_time; _relays[id].fw_count = 1; // If current_time is in the floodWindow and there have been too many requests... - } else if (_relays[id].fw_count >= RELAY_FLOOD_CHANGES) { + } else if (_relays[id].fw_count >= _relay_flood_changes) { // We schedule the changes to the end of the floodWindow // unless it's already delayed beyond that point - if (fw_end - delay > current_time) { - _relays[id].change_time = fw_end; - } + _relays[id].change_delay = std::max(change_delay, _relay_flood_window - fw_diff); + + // Another option is to always move it forward, starting from current time + //_relays[id].fw_start = current_time; } @@ -391,8 +473,8 @@ bool relayStatus(unsigned char id, bool status, bool report, bool group_report) relaySync(id); DEBUG_MSG_P(PSTR("[RELAY] #%d scheduled %s in %u ms\n"), - id, status ? "ON" : "OFF", - (_relays[id].change_time - current_time)); + id, status ? "ON" : "OFF", _relays[id].change_delay + ); changed = true; @@ -427,37 +509,44 @@ void relaySync(unsigned char id) { // Flag sync mode _relayRecursive = true; - byte relaySync = getSetting("relaySync", RELAY_SYNC).toInt(); bool status = _relays[id].target_status; // If RELAY_SYNC_SAME all relays should have the same state - if (relaySync == RELAY_SYNC_SAME) { + if (_relay_sync_mode == RELAY_SYNC_SAME) { for (unsigned short i=0; i<_relays.size(); i++) { if (i != id) relayStatus(i, status); } // If RELAY_SYNC_FIRST all relays should have the same state as first if first changes - } else if (relaySync == RELAY_SYNC_FIRST) { + } else if (_relay_sync_mode == RELAY_SYNC_FIRST) { if (id == 0) { for (unsigned short i=1; i<_relays.size(); i++) { relayStatus(i, status); } } - // If NONE_OR_ONE or ONE and setting ON we should set OFF all the others - } else if (status) { - if (relaySync != RELAY_SYNC_ANY) { - for (unsigned short i=0; i<_relays.size(); i++) { - if (i != id) relayStatus(i, false); + } else if ((_relay_sync_mode == RELAY_SYNC_NONE_OR_ONE) || (_relay_sync_mode == RELAY_SYNC_ONE)) { + // If NONE_OR_ONE or ONE and setting ON we should set OFF all the others + if (status) { + if (_relay_sync_mode != RELAY_SYNC_ANY) { + for (unsigned short other_id=0; other_id<_relays.size(); other_id++) { + if (other_id != id) { + relayStatus(other_id, false); + if (relayStatus(other_id)) { + _relaySyncRelaysDelay(other_id, id); + } + } + } + } + // If ONLY_ONE and setting OFF we should set ON the other one + } else { + if (_relay_sync_mode == RELAY_SYNC_ONE) { + unsigned char other_id = (id + 1) % _relays.size(); + _relaySyncRelaysDelay(id, other_id); + relayStatus(other_id, true); } } - - // If ONLY_ONE and setting OFF we should set ON the other one - } else { - if (relaySync == RELAY_SYNC_ONE) { - unsigned char i = (id + 1) % _relays.size(); - relayStatus(i, true); - } + _relayLockAll(); } // Unflag sync mode @@ -619,10 +708,12 @@ void _relayBoot() { _relays[i].current_status = !status; _relays[i].target_status = status; + _relays[i].change_start = millis(); + #if RELAY_PROVIDER == RELAY_PROVIDER_STM - _relays[i].change_time = millis() + 3000 + 1000 * i; - #else - _relays[i].change_time = millis(); + // XXX hack for correctly restoring relay state on boot + // because of broken stm relay firmware + _relays[i].change_delay = 3000 + 1000 * i; #endif _relays[i].lock = lock; @@ -641,11 +732,40 @@ void _relayBoot() { } +constexpr const unsigned long _relayDelayOn(unsigned char index) { + return ( + (index == 0) ? RELAY1_DELAY_ON : + (index == 1) ? RELAY2_DELAY_ON : + (index == 2) ? RELAY3_DELAY_ON : + (index == 3) ? RELAY4_DELAY_ON : + (index == 4) ? RELAY5_DELAY_ON : + (index == 5) ? RELAY6_DELAY_ON : + (index == 6) ? RELAY7_DELAY_ON : + (index == 7) ? RELAY8_DELAY_ON : 0 + ); +} + +constexpr const unsigned long _relayDelayOff(unsigned char index) { + return ( + (index == 0) ? RELAY1_DELAY_OFF : + (index == 1) ? RELAY2_DELAY_OFF : + (index == 2) ? RELAY3_DELAY_OFF : + (index == 3) ? RELAY4_DELAY_OFF : + (index == 4) ? RELAY5_DELAY_OFF : + (index == 5) ? RELAY6_DELAY_OFF : + (index == 6) ? RELAY7_DELAY_OFF : + (index == 7) ? RELAY8_DELAY_OFF : 0 + ); +} + void _relayConfigure() { for (unsigned int i=0; i<_relays.size(); i++) { _relays[i].pulse = getSetting("relayPulse", i, RELAY_PULSE_MODE).toInt(); _relays[i].pulse_ms = 1000 * getSetting("relayTime", i, RELAY_PULSE_MODE).toFloat(); + _relays[i].delay_on = getSetting("relayDelayOn", i, _relayDelayOn(i)).toInt(); + _relays[i].delay_off = getSetting("relayDelayOff", i, _relayDelayOff(i)).toInt(); + if (GPIO_NONE == _relays[i].pin) continue; pinMode(_relays[i].pin, OUTPUT); @@ -658,6 +778,12 @@ void _relayConfigure() { } } + _relay_flood_window = (1000 * getSetting("relayFloodTime", RELAY_FLOOD_WINDOW).toInt()); + _relay_flood_changes = getSetting("relayFloodChanges", RELAY_FLOOD_CHANGES).toInt(); + + _relay_delay_interlock = getSetting("relayDelayInterlock", RELAY_DELAY_INTERLOCK).toInt(); + _relay_sync_mode = getSetting("relaySync", RELAY_SYNC).toInt(); + #if MQTT_SUPPORT settingsProcessConfig({ {_relay_mqtt_payload_on, "relayPayloadOn", RELAY_MQTT_ON}, @@ -1141,6 +1267,25 @@ void _relayInitCommands() { terminalOK(); }); + #if 0 + terminalRegisterCommand(F("RELAY.INFO"), [](Embedis* e) { + DEBUG_MSG_P(PSTR(" cur tgt pin type reset lock delay_on delay_off pulse pulse_ms\n")); + DEBUG_MSG_P(PSTR(" --- --- --- ---- ----- ---- ---------- ----------- ----- ----------\n")); + for (unsigned char index = 0; index < _relays.size(); ++index) { + const auto& relay = _relays.at(index); + DEBUG_MSG_P(PSTR("%3u %3s %3s %3u %4u %5u %4u %10u %11u %5u %10u\n"), + index, + relay.current_status ? "ON" : "OFF", + relay.target_status ? "ON" : "OFF", + relay.pin, relay.type, relay.reset_pin, + relay.lock, + relay.delay_on, relay.delay_off, + relay.pulse, relay.pulse_ms + ); + } + }); + #endif + } #endif // TERMINAL_SUPPORT @@ -1152,41 +1297,47 @@ void _relayInitCommands() { void _relayLoop() { _relayProcess(false); _relayProcess(true); + #if WEB_SUPPORT + if (_relay_report_ws) { + wsPost(_relayWebSocketUpdate); + _relay_report_ws = false; + } + #endif } void relaySetup() { // Ad-hoc relays #if RELAY1_PIN != GPIO_NONE - _relays.push_back((relay_t) { RELAY1_PIN, RELAY1_TYPE, RELAY1_RESET_PIN, RELAY1_DELAY_ON, RELAY1_DELAY_OFF }); + _relays.push_back((relay_t) { RELAY1_PIN, RELAY1_TYPE, RELAY1_RESET_PIN }); #endif #if RELAY2_PIN != GPIO_NONE - _relays.push_back((relay_t) { RELAY2_PIN, RELAY2_TYPE, RELAY2_RESET_PIN, RELAY2_DELAY_ON, RELAY2_DELAY_OFF }); + _relays.push_back((relay_t) { RELAY2_PIN, RELAY2_TYPE, RELAY2_RESET_PIN }); #endif #if RELAY3_PIN != GPIO_NONE - _relays.push_back((relay_t) { RELAY3_PIN, RELAY3_TYPE, RELAY3_RESET_PIN, RELAY3_DELAY_ON, RELAY3_DELAY_OFF }); + _relays.push_back((relay_t) { RELAY3_PIN, RELAY3_TYPE, RELAY3_RESET_PIN }); #endif #if RELAY4_PIN != GPIO_NONE - _relays.push_back((relay_t) { RELAY4_PIN, RELAY4_TYPE, RELAY4_RESET_PIN, RELAY4_DELAY_ON, RELAY4_DELAY_OFF }); + _relays.push_back((relay_t) { RELAY4_PIN, RELAY4_TYPE, RELAY4_RESET_PIN }); #endif #if RELAY5_PIN != GPIO_NONE - _relays.push_back((relay_t) { RELAY5_PIN, RELAY5_TYPE, RELAY5_RESET_PIN, RELAY5_DELAY_ON, RELAY5_DELAY_OFF }); + _relays.push_back((relay_t) { RELAY5_PIN, RELAY5_TYPE, RELAY5_RESET_PIN }); #endif #if RELAY6_PIN != GPIO_NONE - _relays.push_back((relay_t) { RELAY6_PIN, RELAY6_TYPE, RELAY6_RESET_PIN, RELAY6_DELAY_ON, RELAY6_DELAY_OFF }); + _relays.push_back((relay_t) { RELAY6_PIN, RELAY6_TYPE, RELAY6_RESET_PIN }); #endif #if RELAY7_PIN != GPIO_NONE - _relays.push_back((relay_t) { RELAY7_PIN, RELAY7_TYPE, RELAY7_RESET_PIN, RELAY7_DELAY_ON, RELAY7_DELAY_OFF }); + _relays.push_back((relay_t) { RELAY7_PIN, RELAY7_TYPE, RELAY7_RESET_PIN }); #endif #if RELAY8_PIN != GPIO_NONE - _relays.push_back((relay_t) { RELAY8_PIN, RELAY8_TYPE, RELAY8_RESET_PIN, RELAY8_DELAY_ON, RELAY8_DELAY_OFF }); + _relays.push_back((relay_t) { RELAY8_PIN, RELAY8_TYPE, RELAY8_RESET_PIN }); #endif // Dummy relays for AI Light, Magic Home LED Controller, H801, Sonoff Dual and Sonoff RF Bridge // No delay_on or off for these devices to easily allow having more than // 8 channels. This behaviour will be recovered with v2. for (unsigned char i=0; i < DUMMY_RELAY_COUNT; i++) { - _relays.push_back((relay_t) {GPIO_NONE, RELAY_TYPE_NORMAL, 0, 0, 0}); + _relays.push_back((relay_t) { GPIO_NONE, RELAY_TYPE_NORMAL, GPIO_NONE }); } _relayBackwards(); diff --git a/code/espurna/scheduler.ino b/code/espurna/scheduler.ino index cf95310c..93f198d2 100644 --- a/code/espurna/scheduler.ino +++ b/code/espurna/scheduler.ino @@ -256,7 +256,7 @@ void _schLoop() { if (!ntpSynced()) return; if (_sch_restore == 0) { - for (int i = 0; i < _relays.size(); i++){ + for (unsigned char i = 0; i < relayCount(); i++){ if (getSetting("relayLastSch", i, SCHEDULER_RESTORE_LAST_SCHEDULE).toInt() == 1) _schCheck(i, 0); }