From ab8841c81f7b58ec2db35b20b9ef53dea6ce689a Mon Sep 17 00:00:00 2001 From: iranl Date: Sun, 21 Apr 2024 14:01:58 +0200 Subject: [PATCH] Time Control --- NetworkLock.cpp | 4 +- NetworkLock.h | 2 +- NetworkOpener.cpp | 16 +++--- NetworkOpener.h | 2 +- NukiOpenerWrapper.cpp | 113 ++++++++++++++++++++++++++++-------------- NukiOpenerWrapper.h | 3 +- NukiWrapper.cpp | 106 ++++++++++++++++++++++++++------------- NukiWrapper.h | 3 +- README.md | 6 +-- 9 files changed, 167 insertions(+), 88 deletions(-) diff --git a/NetworkLock.cpp b/NetworkLock.cpp index 42beb14..21ed905 100644 --- a/NetworkLock.cpp +++ b/NetworkLock.cpp @@ -629,12 +629,12 @@ void NetworkLock::publishKeypad(const std::list& entries, } } -void NetworkLock::publishTimeControl(const std::list& entries) +void NetworkLock::publishTimeControl(const std::list& timeControlEntries) { char str[50]; JsonDocument json; - for(const auto& entry : entries) + for(const auto& entry : timeControlEntries) { auto jsonEntry = json.add(); diff --git a/NetworkLock.h b/NetworkLock.h index 1100fed..f259f38 100644 --- a/NetworkLock.h +++ b/NetworkLock.h @@ -37,7 +37,7 @@ public: void publishHASSConfig(char* deviceType, const char* baseTopic, char* name, char* uidString, const bool& hasDoorSensor, const bool& hasKeypad, const bool& publishAuthData, char* lockAction, char* unlockAction, char* openAction); void removeHASSConfig(char* uidString); void publishKeypad(const std::list& entries, uint maxKeypadCodeCount); - void publishTimeControl(const std::list& entries); + void publishTimeControl(const std::list& timeControlEntries); void publishKeypadCommandResult(const char* result); void publishKeypadJsonCommandResult(const char* result); void publishTimeControlCommandResult(const char* result); diff --git a/NetworkOpener.cpp b/NetworkOpener.cpp index 2d11225..1daa850 100644 --- a/NetworkOpener.cpp +++ b/NetworkOpener.cpp @@ -398,24 +398,24 @@ void NetworkOpener::publishAuthorizationInfo(const std::list& entrie } } -void NetworkOpener::publishTimeControl(const std::list& entries) +void NetworkOpener::publishTimeControl(const std::list& timeControlEntries) { char str[50]; JsonDocument json; - for(const auto& entry : entries) + for(const auto& entry : timeControlEntries) { auto jsonEntry = json.add(); @@ -749,7 +749,7 @@ void NetworkOpener::publishTimeControl(const std::list& entries, uint maxKeypadCodeCount); - void publishTimeControl(const std::list& entries); + void publishTimeControl(const std::list& timeControlEntries); void publishKeypadCommandResult(const char* result); void publishKeypadJsonCommandResult(const char* result); void publishTimeControlCommandResult(const char* result); diff --git a/NukiOpenerWrapper.cpp b/NukiOpenerWrapper.cpp index 84eebd3..f934bf5 100644 --- a/NukiOpenerWrapper.cpp +++ b/NukiOpenerWrapper.cpp @@ -172,6 +172,11 @@ void NukiOpenerWrapper::update() setupHASS(); } } + if(_nextTimeControlUpdateTs != 0 && ts > _nextTimeControlUpdateTs) + { + _nextTimeControlUpdateTs = 0; + updateTimeControl(true); + } if(_hassEnabled && _configRead && _network->reconnected()) { setupHASS(); @@ -407,8 +412,8 @@ void NukiOpenerWrapper::updateConfig() _network->publishConfig(_nukiConfig); _retryConfigCount = 0; - if(_preferences->getBool(preference_timecontrol_info_enabled)) updateTimeControl(); - + if(_preferences->getBool(preference_timecontrol_info_enabled)) updateTimeControl(false); + const int pinStatus = _preferences->getInt(preference_opener_pin_status, 4); if(isPinSet()) { @@ -503,6 +508,9 @@ void NukiOpenerWrapper::updateKeypad() { std::list entries; _nukiOpener.getKeypadEntries(&entries); + + Log->print(F("Opener keypad codes: ")); + Log->println(entries.size()); entries.sort([](const NukiLock::KeypadEntry& a, const NukiLock::KeypadEntry& b) { return a.codeId < b.codeId; }); @@ -526,23 +534,35 @@ void NukiOpenerWrapper::updateKeypad() postponeBleWatchdog(); } -void NukiOpenerWrapper::updateTimeControl() +void NukiOpenerWrapper::updateTimeControl(bool retrieved) { if(!_preferences->getBool(preference_timecontrol_info_enabled)) return; - Log->print(F("Querying lock time control: ")); - Nuki::CmdResult result = _nukiOpener.retrieveTimeControlEntries(); - printCommandResult(result); - if(result == Nuki::CmdResult::Success) + if(!retrieved) { - std::list entries; - _nukiOpener.getTimeControlEntries(&entries); + Log->print(F("Querying opener time control: ")); + Nuki::CmdResult result = _nukiOpener.retrieveTimeControlEntries(); + printCommandResult(result); + if(result == Nuki::CmdResult::Success) + { + _nextTimeControlUpdateTs = millis() + 5000; + } + } + else + { + std::list timeControlEntries; + _nukiOpener.getTimeControlEntries(&timeControlEntries); + + Log->print(F("Opener time control entries: ")); + Log->println(timeControlEntries.size()); - entries.sort([](const NukiLock::TimeControlEntry& a, const NukiLock::TimeControlEntry& b) { return a.entryId < b.entryId; }); + timeControlEntries.sort([](const NukiOpener::TimeControlEntry& a, const NukiOpener::TimeControlEntry& b) { return a.entryId < b.entryId; }); + + _network->publishTimeControl(timeControlEntries); _timeControlIds.clear(); - _timeControlIds.reserve(entries.size()); - for(const auto& entry : entries) + _timeControlIds.reserve(timeControlEntries.size()); + for(const auto& entry : timeControlEntries) { _timeControlIds.push_back(entry.entryId); } @@ -558,24 +578,31 @@ void NukiOpenerWrapper::postponeBleWatchdog() NukiOpener::LockAction NukiOpenerWrapper::lockActionToEnum(const char *str) { - if(strcmp(str, "activateRTO") == 0) return NukiOpener::LockAction::ActivateRTO; - else if(strcmp(str, "deactivateRTO") == 0) return NukiOpener::LockAction::DeactivateRTO; - else if(strcmp(str, "electricStrikeActuation") == 0) return NukiOpener::LockAction::ElectricStrikeActuation; - else if(strcmp(str, "activateCM") == 0) return NukiOpener::LockAction::ActivateCM; - else if(strcmp(str, "deactivateCM") == 0) return NukiOpener::LockAction::DeactivateCM; - else if(strcmp(str, "fobAction2") == 0) return NukiOpener::LockAction::FobAction2; - else if(strcmp(str, "fobAction1") == 0) return NukiOpener::LockAction::FobAction1; - else if(strcmp(str, "fobAction3") == 0) return NukiOpener::LockAction::FobAction3; + if(strcmp(str, "activateRTO") == 0 || strcmp(str, "ActivateRTO") == 0) return NukiOpener::LockAction::ActivateRTO; + else if(strcmp(str, "deactivateRTO") == 0 || strcmp(str, "DeactivateRTO") == 0) return NukiOpener::LockAction::DeactivateRTO; + else if(strcmp(str, "electricStrikeActuation") == 0 || strcmp(str, "ElectricStrikeActuation") == 0) return NukiOpener::LockAction::ElectricStrikeActuation; + else if(strcmp(str, "activateCM") == 0 || strcmp(str, "ActivateCM") == 0) return NukiOpener::LockAction::ActivateCM; + else if(strcmp(str, "deactivateCM") == 0 || strcmp(str, "DeactivateCM") == 0) return NukiOpener::LockAction::DeactivateCM; + else if(strcmp(str, "fobAction2") == 0 || strcmp(str, "FobAction2") == 0) return NukiOpener::LockAction::FobAction2; + else if(strcmp(str, "fobAction1") == 0 || strcmp(str, "FobAction1") == 0) return NukiOpener::LockAction::FobAction1; + else if(strcmp(str, "fobAction3") == 0 || strcmp(str, "FobAction3") == 0) return NukiOpener::LockAction::FobAction3; return (NukiOpener::LockAction)0xff; } LockActionResult NukiOpenerWrapper::onLockActionReceivedCallback(const char *value) { - NukiOpener::LockAction action = nukiOpenerInst->lockActionToEnum(value); - if((int)action == 0xff) + NukiOpener::LockAction action; + + if(strlen(value) > 0) { - return LockActionResult::UnknownAction; + action = nukiOpenerInst->lockActionToEnum(value); + + if((int)action == 0xff) + { + return LockActionResult::UnknownAction; + } } + else return LockActionResult::UnknownAction; nukiOpenerPreferences = new Preferences(); nukiOpenerPreferences->begin("nukihub", true); @@ -822,7 +849,7 @@ void NukiOpenerWrapper::onKeypadJsonCommandReceived(const char *value) return; } - DynamicJsonDocument json(2048); + JsonDocument json; DeserializationError jsonError = deserializeJson(json, value); if(jsonError) @@ -991,7 +1018,7 @@ void NukiOpenerWrapper::onKeypadJsonCommandReceived(const char *value) return; } } - + if(allowedWeekdays.indexOf("mon") >= 0) allowedWeekdaysInt += 64; if(allowedWeekdays.indexOf("tue") >= 0) allowedWeekdaysInt += 32; if(allowedWeekdays.indexOf("wed") >= 0) allowedWeekdaysInt += 16; @@ -1000,7 +1027,7 @@ void NukiOpenerWrapper::onKeypadJsonCommandReceived(const char *value) if(allowedWeekdays.indexOf("sat") >= 0) allowedWeekdaysInt += 2; if(allowedWeekdays.indexOf("sun") >= 0) allowedWeekdaysInt += 1; } - + if(strcmp(action, "add") == 0) { NukiOpener::NewKeypadEntry entry; @@ -1123,7 +1150,7 @@ void NukiOpenerWrapper::onKeypadJsonCommandReceived(const char *value) void NukiOpenerWrapper::onTimeControlCommandReceived(const char *value) { - if(_nukiLock.getSecurityPincode() == 0) + if(_nukiOpener.getSecurityPincode() == 0) { _network->publishTimeControlCommandResult("noPinSet"); return; @@ -1151,9 +1178,21 @@ void NukiOpenerWrapper::onTimeControlCommandReceived(const char *value) uint8_t enabled = json["enabled"].as(); String weekdays = json["weekdays"].as(); const char *time = json["time"].as(); - NukiOpener::LockAction timeControlLockAction = nukiOpenerInst->lockActionToEnum(json["lockAction"].as()); + const char *lockAct = json["lockAction"].as(); + NukiOpener::LockAction timeControlLockAction; - if((int)timeControlLockAction == 0xff) + if(strlen(lockAct) > 0) + { + + timeControlLockAction = nukiOpenerInst->lockActionToEnum(lockAct); + + if((int)timeControlLockAction == 0xff) + { + _network->publishTimeControlCommandResult("invalidLockAction"); + return; + } + } + else { _network->publishTimeControlCommandResult("invalidLockAction"); return; @@ -1198,19 +1237,19 @@ void NukiOpenerWrapper::onTimeControlCommandReceived(const char *value) if(timeAr[0] < 0 || timeAr[0] > 23 || timeAr[1] < 0 || timeAr[1] > 59) { - _network->publishKeypadJsonCommandResult("invalidTime"); + _network->publishTimeControlCommandResult("invalidTime"); return; } } else { - _network->publishKeypadJsonCommandResult("invalidTime"); + _network->publishTimeControlCommandResult("invalidTime"); return; } } else { - _network->publishKeypadJsonCommandResult("invalidTime"); + _network->publishTimeControlCommandResult("invalidTime"); return; } @@ -1224,7 +1263,7 @@ void NukiOpenerWrapper::onTimeControlCommandReceived(const char *value) if(strcmp(action, "add") == 0) { - NukiLock::NewTimeControlEntry entry; + NukiOpener::NewTimeControlEntry entry; memset(&entry, 0, sizeof(entry)); entry.weekdays = weekdaysInt; @@ -1236,13 +1275,13 @@ void NukiOpenerWrapper::onTimeControlCommandReceived(const char *value) entry.lockAction = timeControlLockAction; - result = _nukiOpener.addKeypadEntry(entry); + result = _nukiOpener.addTimeControlEntry(entry); Log->print("Add time control: "); Log->println((int)result); } else if (strcmp(action, "update") == 0) { - NukiLock::TimeControlEntry entry; + NukiOpener::TimeControlEntry entry; memset(&entry, 0, sizeof(entry)); entry.entryId = entryId; entry.enabled = enabled == 0 ? 0 : 1; @@ -1271,10 +1310,10 @@ void NukiOpenerWrapper::onTimeControlCommandReceived(const char *value) { char resultStr[15]; memset(&resultStr, 0, sizeof(resultStr)); - NukiLock::cmdResultToString(result, resultStr); + NukiOpener::cmdResultToString(result, resultStr); _network->publishTimeControlCommandResult(resultStr); } - + _nextConfigUpdateTs = millis() + 300; } else diff --git a/NukiOpenerWrapper.h b/NukiOpenerWrapper.h index d0a21d1..b37f8f5 100644 --- a/NukiOpenerWrapper.h +++ b/NukiOpenerWrapper.h @@ -62,7 +62,7 @@ private: void updateConfig(); void updateAuthData(); void updateKeypad(); - void updateTimeControl(); + void updateTimeControl(bool retrieved); void postponeBleWatchdog(); void updateGpioOutputs(); @@ -122,6 +122,7 @@ private: unsigned long _nextLockStateUpdateTs = 0; unsigned long _nextBatteryReportTs = 0; unsigned long _nextConfigUpdateTs = 0; + unsigned long _nextTimeControlUpdateTs = 0; unsigned long _nextKeypadUpdateTs = 0; unsigned long _nextPairTs = 0; long _nextRssiTs = 0; diff --git a/NukiWrapper.cpp b/NukiWrapper.cpp index 4b5a6c6..81fbe90 100644 --- a/NukiWrapper.cpp +++ b/NukiWrapper.cpp @@ -184,6 +184,11 @@ void NukiWrapper::update() setupHASS(); } } + if(_nextTimeControlUpdateTs != 0 && ts > _nextTimeControlUpdateTs) + { + _nextTimeControlUpdateTs = 0; + updateTimeControl(true); + } if(_hassEnabled && _configRead && _network->reconnected()) { setupHASS(); @@ -381,8 +386,8 @@ void NukiWrapper::updateConfig() _network->publishConfig(_nukiConfig); _retryConfigCount = 0; - if(_preferences->getBool(preference_timecontrol_info_enabled)) updateTimeControl(); - + if(_preferences->getBool(preference_timecontrol_info_enabled)) updateTimeControl(false); + const int pinStatus = _preferences->getInt(preference_lock_pin_status, 4); if(isPinSet()) { @@ -477,6 +482,9 @@ void NukiWrapper::updateKeypad() { std::list entries; _nukiLock.getKeypadEntries(&entries); + + Log->print(F("Lock keypad codes: ")); + Log->println(entries.size()); entries.sort([](const NukiLock::KeypadEntry& a, const NukiLock::KeypadEntry& b) { return a.codeId < b.codeId; }); @@ -500,28 +508,40 @@ void NukiWrapper::updateKeypad() postponeBleWatchdog(); } -void NukiWrapper::updateTimeControl() +void NukiWrapper::updateTimeControl(bool retrieved) { if(!_preferences->getBool(preference_timecontrol_info_enabled)) return; - Log->print(F("Querying lock time control: ")); - Nuki::CmdResult result = _nukiLock.retrieveTimeControlEntries(); - printCommandResult(result); - if(result == Nuki::CmdResult::Success) + if(!retrieved) { - std::list entries; - _nukiLock.getTimeControlEntries(&entries); + Log->print(F("Querying lock time control: ")); + Nuki::CmdResult result = _nukiLock.retrieveTimeControlEntries(); + printCommandResult(result); + if(result == Nuki::CmdResult::Success) + { + _nextTimeControlUpdateTs = millis() + 5000; + } + } + else + { + std::list timeControlEntries; + _nukiLock.getTimeControlEntries(&timeControlEntries); + + Log->print(F("Lock time control entries: ")); + Log->println(timeControlEntries.size()); - entries.sort([](const NukiLock::TimeControlEntry& a, const NukiLock::TimeControlEntry& b) { return a.entryId < b.entryId; }); + timeControlEntries.sort([](const NukiLock::TimeControlEntry& a, const NukiLock::TimeControlEntry& b) { return a.entryId < b.entryId; }); + + _network->publishTimeControl(timeControlEntries); _timeControlIds.clear(); - _timeControlIds.reserve(entries.size()); - for(const auto& entry : entries) + _timeControlIds.reserve(timeControlEntries.size()); + for(const auto& entry : timeControlEntries) { _timeControlIds.push_back(entry.entryId); } } - + postponeBleWatchdog(); } @@ -532,26 +552,32 @@ void NukiWrapper::postponeBleWatchdog() NukiLock::LockAction NukiWrapper::lockActionToEnum(const char *str) { - if(strcmp(str, "unlock") == 0) return NukiLock::LockAction::Unlock; - else if(strcmp(str, "lock") == 0) return NukiLock::LockAction::Lock; - else if(strcmp(str, "unlatch") == 0) return NukiLock::LockAction::Unlatch; - else if(strcmp(str, "lockNgo") == 0) return NukiLock::LockAction::LockNgo; - else if(strcmp(str, "lockNgoUnlatch") == 0) return NukiLock::LockAction::LockNgoUnlatch; - else if(strcmp(str, "fullLock") == 0) return NukiLock::LockAction::FullLock; - else if(strcmp(str, "fobAction2") == 0) return NukiLock::LockAction::FobAction2; - else if(strcmp(str, "fobAction1") == 0) return NukiLock::LockAction::FobAction1; - else if(strcmp(str, "fobAction3") == 0) return NukiLock::LockAction::FobAction3; + if(strcmp(str, "unlock") == 0 || strcmp(str, "Unlock") == 0) return NukiLock::LockAction::Unlock; + else if(strcmp(str, "lock") == 0 || strcmp(str, "Lock") == 0) return NukiLock::LockAction::Lock; + else if(strcmp(str, "unlatch") == 0 || strcmp(str, "Unlatch") == 0) return NukiLock::LockAction::Unlatch; + else if(strcmp(str, "lockNgo") == 0 || strcmp(str, "LockNgo") == 0) return NukiLock::LockAction::LockNgo; + else if(strcmp(str, "lockNgoUnlatch") == 0 || strcmp(str, "LockNgoUnlatch") == 0) return NukiLock::LockAction::LockNgoUnlatch; + else if(strcmp(str, "fullLock") == 0 || strcmp(str, "FullLock") == 0) return NukiLock::LockAction::FullLock; + else if(strcmp(str, "fobAction2") == 0 || strcmp(str, "FobAction2") == 0) return NukiLock::LockAction::FobAction2; + else if(strcmp(str, "fobAction1") == 0 || strcmp(str, "FobAction1") == 0) return NukiLock::LockAction::FobAction1; + else if(strcmp(str, "fobAction3") == 0 || strcmp(str, "FobAction3") == 0) return NukiLock::LockAction::FobAction3; return (NukiLock::LockAction)0xff; } LockActionResult NukiWrapper::onLockActionReceivedCallback(const char *value) { - NukiLock::LockAction action = nukiInst->lockActionToEnum(value); - - if((int)action == 0xff) + NukiLock::LockAction action; + + if(strlen(value) > 0) { - return LockActionResult::UnknownAction; + action = nukiInst->lockActionToEnum(value); + + if((int)action == 0xff) + { + return LockActionResult::UnknownAction; + } } + else return LockActionResult::UnknownAction; nukiLockPreferences = new Preferences(); nukiLockPreferences->begin("nukihub", true); @@ -821,7 +847,7 @@ void NukiWrapper::onKeypadJsonCommandReceived(const char *value) return; } - DynamicJsonDocument json(2048); + JsonDocument json; DeserializationError jsonError = deserializeJson(json, value); if(jsonError) @@ -1150,9 +1176,21 @@ void NukiWrapper::onTimeControlCommandReceived(const char *value) uint8_t enabled = json["enabled"].as(); String weekdays = json["weekdays"].as(); const char *time = json["time"].as(); - NukiLock::LockAction timeControlLockAction = nukiInst->lockActionToEnum(json["lockAction"].as()); + const char *lockAct = json["lockAction"].as(); + NukiLock::LockAction timeControlLockAction; - if((int)timeControlLockAction == 0xff) + if(strlen(lockAct) > 0) + { + + timeControlLockAction = nukiInst->lockActionToEnum(lockAct); + + if((int)timeControlLockAction == 0xff) + { + _network->publishTimeControlCommandResult("invalidLockAction"); + return; + } + } + else { _network->publishTimeControlCommandResult("invalidLockAction"); return; @@ -1197,19 +1235,19 @@ void NukiWrapper::onTimeControlCommandReceived(const char *value) if(timeAr[0] < 0 || timeAr[0] > 23 || timeAr[1] < 0 || timeAr[1] > 59) { - _network->publishKeypadJsonCommandResult("invalidTime"); + _network->publishTimeControlCommandResult("invalidTime"); return; } } else { - _network->publishKeypadJsonCommandResult("invalidTime"); + _network->publishTimeControlCommandResult("invalidTime"); return; } } else { - _network->publishKeypadJsonCommandResult("invalidTime"); + _network->publishTimeControlCommandResult("invalidTime"); return; } @@ -1235,7 +1273,7 @@ void NukiWrapper::onTimeControlCommandReceived(const char *value) entry.lockAction = timeControlLockAction; - result = _nukiLock.addKeypadEntry(entry); + result = _nukiLock.addTimeControlEntry(entry); Log->print("Add time control: "); Log->println((int)result); } @@ -1273,7 +1311,7 @@ void NukiWrapper::onTimeControlCommandReceived(const char *value) NukiLock::cmdResultToString(result, resultStr); _network->publishTimeControlCommandResult(resultStr); } - + _nextConfigUpdateTs = millis() + 300; } else diff --git a/NukiWrapper.h b/NukiWrapper.h index 6ab984b..c122cbf 100644 --- a/NukiWrapper.h +++ b/NukiWrapper.h @@ -61,7 +61,7 @@ private: void updateConfig(); void updateAuthData(); void updateKeypad(); - void updateTimeControl(); + void updateTimeControl(bool retrieved); void postponeBleWatchdog(); void updateGpioOutputs(); @@ -121,6 +121,7 @@ private: unsigned long _nextLockStateUpdateTs = 0; unsigned long _nextBatteryReportTs = 0; unsigned long _nextConfigUpdateTs = 0; + unsigned long _nextTimeControlUpdateTs = 0; unsigned long _nextKeypadUpdateTs = 0; unsigned long _nextRssiTs = 0; unsigned long _lastRssi = 0; diff --git a/README.md b/README.md index 0b3d377..bef2487 100644 --- a/README.md +++ b/README.md @@ -399,13 +399,13 @@ To change Nuki Lock/Opener time control settings set the `timecontrol/actionJson | enabled | Not used | Not used | Optional | Enable or disable the entry, enabled if not set | 1 = enabled, 0 = disabled | | weekdays | Not used | Optional | Optional | Weekdays on which the chosen lock action should be exectued | Array of days: "mon", "tue", "wed", "thu" , "fri" "sat", "sun" | | time | Not used | Required | Required | The time on which the chosen lock action should be executed | "HH:MM" | -| lockaction | Not used | Required | Required | The lock action that should be executed on the chosen weekdays at the chosen time | For the Nuki lock: "Unlock", "Lock", "Unlatch", "LockNgo", "LockNgoUnlatch", "FullLock", "FobAction1", "FobAction2", "FobAction3. For the Nuki Opener: "ActivateRTO", "DeactivateRTO", "ElectricStrikeActuation", "ActivateCM", "DeactivateCM", "FobAction1", "FobAction2", "FobAction3" | +| lockAction | Not used | Required | Required | The lock action that should be executed on the chosen weekdays at the chosen time | For the Nuki lock: "Unlock", "Lock", "Unlatch", "LockNgo", "LockNgoUnlatch", "FullLock". For the Nuki Opener: "ActivateRTO", "DeactivateRTO", "ElectricStrikeActuation", "ActivateCM", "DeactivateCM" | Example usage:
Examples: - Delete: `{ "action": "delete", "entryId": "1234" }` -- Add: `{ "action": "add", "weekdays": [ "wed", "thu", "fri" ], "time": "08:00", "lockaction": "unlock" }` -- Update: `{ "action": "update", "entryId": "1234", "enabled": "1", "weekdays": [ "mon", "tue", "sat", "sun" ], "time": "08:00", "lockaction": "lock" }` +- Add: `{ "action": "add", "weekdays": [ "wed", "thu", "fri" ], "time": "08:00", "lockAction": "Unlock" }` +- Update: `{ "action": "update", "entryId": "1234", "enabled": "1", "weekdays": [ "mon", "tue", "sat", "sun" ], "time": "08:00", "lockAction": "Lock" }` ## GPIO lock control (optional)