diff --git a/CMakeLists.txt b/CMakeLists.txt index c63ab3a..26b3c07 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -29,6 +29,7 @@ file(GLOB SRCFILES networkDevices/WifiDevice.cpp networkDevices/W5500Device.cpp NukiWrapper.cpp + NukiOpenerWrapper.cpp MqttTopics.h WebCfgServer.cpp PresenceDetection.cpp diff --git a/Network.cpp b/Network.cpp index 6bef9ef..4b061b5 100644 --- a/Network.cpp +++ b/Network.cpp @@ -74,7 +74,7 @@ void Network::initialize() _preferences->putInt(preference_mqtt_broker_port, port); } - String mqttPath = _preferences->getString(preference_mqtt_path); + String mqttPath = _preferences->getString(preference_mqtt_lock_path); if(mqttPath.length() > 0) { size_t len = mqttPath.length(); @@ -86,7 +86,22 @@ void Network::initialize() else { strcpy(_mqttPath, "nuki"); - _preferences->putString(preference_mqtt_path, _mqttPath); + _preferences->putString(preference_mqtt_lock_path, _mqttPath); + } + + String mqttOpenerPath = _preferences->getString(preference_mqtt_opener_path); + if(mqttOpenerPath.length() > 0) + { + size_t len = mqttOpenerPath.length(); + for(int i=0; i < len; i++) + { + _mqttOpenerPath[i] = mqttOpenerPath.charAt(i); + } + } + else + { + strcpy(_mqttOpenerPath, "nukiopener"); + _preferences->putString(preference_mqtt_opener_path, _mqttOpenerPath); } String mqttUser = _preferences->getString(preference_mqtt_user); diff --git a/Network.h b/Network.h index bbc0284..6a88088 100644 --- a/Network.h +++ b/Network.h @@ -67,6 +67,7 @@ private: unsigned long _nextReconnect = 0; char _mqttBrokerAddr[101] = {0}; char _mqttPath[181] = {0}; + char _mqttOpenerPath[181] = {0}; char _mqttUser[31] = {0}; char _mqttPass[31] = {0}; int _networkTimeout = 0; diff --git a/NukiOpenerWrapper.cpp b/NukiOpenerWrapper.cpp new file mode 100644 index 0000000..9b28a05 --- /dev/null +++ b/NukiOpenerWrapper.cpp @@ -0,0 +1,333 @@ +#include "NukiOpenerWrapper.h" +#include +#include "PreferencesKeys.h" +#include "MqttTopics.h" +#include + +NukiOpenerWrapper* nukiOpenerInst; + +NukiOpenerWrapper::NukiOpenerWrapper(const std::string& deviceName, uint32_t id, Network* network, Preferences* preferences) + : _deviceName(deviceName), + _nukiBle(deviceName, id), + _network(network), + _preferences(preferences) +{ + nukiOpenerInst = this; + + memset(&_lastKeyTurnerState, sizeof(NukiLock::KeyTurnerState), 0); + memset(&_lastBatteryReport, sizeof(NukiLock::BatteryReport), 0); + memset(&_batteryReport, sizeof(NukiLock::BatteryReport), 0); + memset(&_keyTurnerState, sizeof(NukiLock::KeyTurnerState), 0); + _keyTurnerState.lockState = NukiOpener::LockState::Undefined; + + network->setLockActionReceivedCallback(nukiOpenerInst->onLockActionReceivedCallback); + network->setConfigUpdateReceivedCallback(nukiOpenerInst->onConfigUpdateReceivedCallback); +} + + +NukiOpenerWrapper::~NukiOpenerWrapper() +{ + delete _bleScanner; + _bleScanner = nullptr; +} + + +void NukiOpenerWrapper::initialize() +{ + _bleScanner = new BleScanner::Scanner(); + _bleScanner->initialize(_deviceName); + _bleScanner->setScanDuration(10); + _nukiBle.initialize(); + _nukiBle.registerBleScanner(_bleScanner); + + _intervalLockstate = _preferences->getInt(preference_query_interval_lockstate); + _intervalBattery = _preferences->getInt(preference_query_interval_battery); + _publishAuthData = _preferences->getBool(preference_publish_authdata); + + if(_intervalLockstate == 0) + { + _intervalLockstate = 60 * 5; + _preferences->putInt(preference_query_interval_lockstate, _intervalLockstate); + } + if(_intervalBattery == 0) + { + _intervalBattery = 60 * 30; + _preferences->putInt(preference_query_interval_battery, _intervalBattery); + } + + _nukiBle.setEventHandler(this); + + Serial.print(F("Lock state interval: ")); + Serial.print(_intervalLockstate); + Serial.print(F(" | Battery interval: ")); + Serial.print(_intervalBattery); + Serial.print(F(" | Publish auth data: ")); + Serial.println(_publishAuthData ? "yes" : "no"); + + if(!_publishAuthData) + { + _clearAuthData = true; + } +} + +void NukiOpenerWrapper::update() +{ + if (!_paired) { + Serial.println(F("Nuki start pairing")); + + _bleScanner->update(); + vTaskDelay( 5000 / portTICK_PERIOD_MS); + if (_nukiBle.pairNuki() == NukiOpener::PairingResult::Success) { + Serial.println(F("Nuki paired")); + _paired = true; + } + else + { + vTaskDelay( 200 / portTICK_PERIOD_MS); + return; + } + } + + vTaskDelay( 20 / portTICK_PERIOD_MS); + _bleScanner->update(); + _nukiBle.updateConnectionState(); + + unsigned long ts = millis(); + + if(_statusUpdated || _nextLockStateUpdateTs == 0 || ts >= _nextLockStateUpdateTs) + { + _statusUpdated = false; + _nextLockStateUpdateTs = ts + _intervalLockstate * 1000; + updateKeyTurnerState(); + } + if(_nextBatteryReportTs == 0 || ts > _nextBatteryReportTs) + { + _nextBatteryReportTs = ts + _intervalBattery * 1000; + updateBatteryState(); + } + if(_nextConfigUpdateTs == 0 || ts > _nextConfigUpdateTs) + { + _nextConfigUpdateTs = ts + _intervalConfig * 1000; + updateConfig(); + } + + if(_nextLockAction != (NukiOpener::LockAction)0xff) + { + NukiOpener::CmdResult cmdResult = _nukiBle.lockAction(_nextLockAction, 0, 0); + + char resultStr[15] = {0}; + NukiOpener::cmdResultToString(cmdResult, resultStr); + + _network->publishCommandResult(resultStr); + + Serial.print(F("Lock action result: ")); + Serial.println(resultStr); + + _nextLockAction = (NukiOpener::LockAction)0xff; + if(_intervalLockstate > 10) + { + _nextLockStateUpdateTs = ts + 10 * 1000; + } + } + + if(_clearAuthData) + { + _network->publishAuthorizationInfo(0, ""); + _clearAuthData = false; + } + + memcpy(&_lastKeyTurnerState, &_keyTurnerState, sizeof(NukiLock::KeyTurnerState)); +} + +void NukiOpenerWrapper::setPin(const uint16_t pin) +{ + _nukiBle.saveSecurityPincode(pin); +} + +void NukiOpenerWrapper::unpair() +{ + _nukiBle.unPairNuki(); + _paired = false; +} + +void NukiOpenerWrapper::updateKeyTurnerState() +{ + _nukiBle.requestKeyTurnerState(&_keyTurnerState); +// _network->publishKeyTurnerState(_keyTurnerState, _lastKeyTurnerState); + + if(_keyTurnerState.lockState != _lastKeyTurnerState.lockState) + { + char lockStateStr[20]; + lockstateToString(_keyTurnerState.lockState, lockStateStr); + Serial.print(F("Nuki lock state: ")); + Serial.println(lockStateStr); + } + + if(_publishAuthData) + { + updateAuthData(); + } +} + +void NukiOpenerWrapper::updateBatteryState() +{ + _nukiBle.requestBatteryReport(&_batteryReport); +// _network->publishBatteryReport(_batteryReport); +} + +void NukiOpenerWrapper::updateConfig() +{ + readConfig(); + readAdvancedConfig(); +// _network->publishConfig(_nukiConfig); +// _network->publishAdvancedConfig(_nukiAdvancedConfig); +} + +void NukiOpenerWrapper::updateAuthData() +{ + Nuki::CmdResult result = _nukiBle.retrieveLogEntries(0, 0, 0, true); + if(result != Nuki::CmdResult::Success) + { + _network->publishAuthorizationInfo(0, ""); + return; + } + vTaskDelay( 100 / portTICK_PERIOD_MS); + + result = _nukiBle.retrieveLogEntries(_nukiBle.getLogEntryCount() - 2, 1, 0, false); + if(result != Nuki::CmdResult::Success) + { + _network->publishAuthorizationInfo(0, ""); + return; + } + vTaskDelay( 200 / portTICK_PERIOD_MS); + + std::list log; + _nukiBle.getLogEntries(&log); + + if(log.size() > 0) + { + const NukiOpener::LogEntry& entry = log.front(); +// log_d("Log: %d-%d-%d %d:%d:%d %s", entry.timeStampYear, entry.timeStampMonth, entry.timeStampDay, +// entry.timeStampHour, entry.timeStampMinute, entry.timeStampSecond, entry.name); + if(entry.authId != _lastAuthId) + { + _network->publishAuthorizationInfo(entry.authId, (char *) entry.name); + _lastAuthId = entry.authId; + } + } + else + { + _network->publishAuthorizationInfo(0, ""); + } +} + +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; + return (NukiOpener::LockAction)0xff; +} + +bool NukiOpenerWrapper::onLockActionReceivedCallback(const char *value) +{ + NukiOpener::LockAction action = nukiOpenerInst->lockActionToEnum(value); + nukiOpenerInst->_nextLockAction = action; + return (int)action != 0xff; +} + +void NukiOpenerWrapper::onConfigUpdateReceivedCallback(const char *topic, const char *value) +{ + nukiOpenerInst->onConfigUpdateReceived(topic, value); +} + + +void NukiOpenerWrapper::onConfigUpdateReceived(const char *topic, const char *value) +{ + if(strcmp(topic, mqtt_topic_config_button_enabled) == 0) + { + bool newValue = atoi(value) > 0; + if(!_nukiConfigValid || _nukiConfig.buttonEnabled == newValue) return; + _nukiBle.enableButton(newValue); + _nextConfigUpdateTs = millis() + 300; + } + if(strcmp(topic, mqtt_topic_config_led_enabled) == 0) + { + bool newValue = atoi(value) > 0; + if(!_nukiConfigValid || _nukiConfig.ledEnabled == newValue) return; + _nukiBle.enableLedFlash(newValue); + _nextConfigUpdateTs = millis() + 300; + } + else if(strcmp(topic, mqtt_topic_config_led_brightness) == 0) + { + int newValue = atoi(value); + if(!_nukiConfigValid || _nukiConfig.ledBrightness == newValue) return; + _nukiBle.setLedBrightness(newValue); + _nextConfigUpdateTs = millis() + 300; + } + else if(strcmp(topic, mqtt_topic_config_auto_unlock) == 0) + { + bool newValue = !(atoi(value) > 0); + if(!_nukiAdvancedConfigValid || _nukiAdvancedConfig.autoUnLockDisabled == newValue) return; + _nukiBle.disableAutoUnlock(newValue); + _nextConfigUpdateTs = millis() + 300; + } + else if(strcmp(topic, mqtt_topic_config_auto_lock) == 0) + { + bool newValue = atoi(value) > 0; + if(!_nukiAdvancedConfigValid || _nukiAdvancedConfig.autoLockEnabled == newValue) return; + _nukiBle.enableAutoLock(newValue); + _nextConfigUpdateTs = millis() + 300; + } + else if(strcmp(topic, mqtt_topic_config_auto_lock) == 0) + { + bool newValue = atoi(value) > 0; + if(!_nukiAdvancedConfigValid || _nukiAdvancedConfig.autoLockEnabled == newValue) return; + _nukiBle.enableAutoLock(newValue); + _nextConfigUpdateTs = millis() + 300; + } +} + +const NukiOpener::KeyTurnerState &NukiOpenerWrapper::keyTurnerState() +{ + return _keyTurnerState; +} + +const bool NukiOpenerWrapper::isPaired() +{ + return _paired; +} + +BleScanner::Scanner *NukiOpenerWrapper::bleScanner() +{ + return _bleScanner; +} + +void NukiOpenerWrapper::notify(Nuki::EventType eventType) +{ + if(eventType == Nuki::EventType::KeyTurnerStatusUpdated) + { + _statusUpdated = true; + } +} + +void NukiOpenerWrapper::readConfig() +{ + Serial.print(F("Reading config. Result: ")); + Nuki::CmdResult result = _nukiBle.requestConfig(&_nukiConfig); + _nukiConfigValid = result == Nuki::CmdResult::Success; + Serial.println(result); +} + +void NukiOpenerWrapper::readAdvancedConfig() +{ + Serial.print(F("Reading advanced config. Result: ")); + Nuki::CmdResult result = _nukiBle.requestAdvancedConfig(&_nukiAdvancedConfig); + _nukiAdvancedConfigValid = result == Nuki::CmdResult::Success; + Serial.println(result); +} diff --git a/NukiOpenerWrapper.h b/NukiOpenerWrapper.h new file mode 100644 index 0000000..de33917 --- /dev/null +++ b/NukiOpenerWrapper.h @@ -0,0 +1,75 @@ +#pragma once + +#include "NukiOpener.h" +#include "Network.h" +#include "NukiOpenerConstants.h" +#include "NukiDataTypes.h" +#include "BleScanner.h" + +class NukiOpenerWrapper : public NukiOpener::SmartlockEventHandler +{ +public: + NukiOpenerWrapper(const std::string& deviceName, uint32_t id, Network* network, Preferences* preferences); + virtual ~NukiOpenerWrapper(); + + void initialize(); + void update(); + + void setPin(const uint16_t pin); + + void unpair(); + + const NukiOpener::KeyTurnerState& keyTurnerState(); + const bool isPaired(); + + BleScanner::Scanner* bleScanner(); + + void notify(NukiOpener::EventType eventType) override; + +private: + static bool onLockActionReceivedCallback(const char* value); + static void onConfigUpdateReceivedCallback(const char* topic, const char* value); + void onConfigUpdateReceived(const char* topic, const char* value); + + void updateKeyTurnerState(); + void updateBatteryState(); + void updateConfig(); + void updateAuthData(); + + void readConfig(); + void readAdvancedConfig(); + + NukiOpener::LockAction lockActionToEnum(const char* str); // char array at least 14 characters + + std::string _deviceName; + NukiOpener::NukiOpener _nukiBle; + BleScanner::Scanner* _bleScanner; + Network* _network; + Preferences* _preferences; + int _intervalLockstate = 0; // seconds + int _intervalBattery = 0; // seconds + int _intervalConfig = 60 * 60; // seconds + bool _publishAuthData = false; + bool _clearAuthData = false; + + NukiOpener::KeyTurnerState _lastKeyTurnerState; + NukiOpener::KeyTurnerState _keyTurnerState; + + uint32_t _lastAuthId = 0xffff; + + NukiOpener::BatteryReport _batteryReport; + NukiOpener::BatteryReport _lastBatteryReport; + + NukiOpener::Config _nukiConfig = {0}; + NukiOpener::AdvancedConfig _nukiAdvancedConfig = {0}; + bool _nukiConfigValid = false; + bool _nukiAdvancedConfigValid = false; + + bool _paired = false; + bool _statusUpdated = false; + unsigned long _nextLockStateUpdateTs = 0; + unsigned long _nextBatteryReportTs = 0; + unsigned long _nextConfigUpdateTs = 0; + unsigned long _nextPairTs = 0; + NukiOpener::LockAction _nextLockAction = (NukiOpener::LockAction)0xff; +}; diff --git a/PreferencesKeys.h b/PreferencesKeys.h index 6938637..f7253fe 100644 --- a/PreferencesKeys.h +++ b/PreferencesKeys.h @@ -5,7 +5,8 @@ #define preference_mqtt_broker_port "mqttport" #define preference_mqtt_user "mqttuser" #define preference_mqtt_password "mqttpass" -#define preference_mqtt_path "mqttpath" +#define preference_mqtt_lock_path "mqttpath" +#define preference_mqtt_opener_path "mqttoppath" #define preference_hostname "hostname" #define preference_network_timeout "nettmout" #define preference_query_interval_lockstate "lockStInterval" diff --git a/WebCfgServer.cpp b/WebCfgServer.cpp index 6cf4d31..a6e7283 100644 --- a/WebCfgServer.cpp +++ b/WebCfgServer.cpp @@ -147,7 +147,12 @@ bool WebCfgServer::processArgs(String& message) } else if(key == "MQTTPATH") { - _preferences->putString(preference_mqtt_path, value); + _preferences->putString(preference_mqtt_lock_path, value); + configChanged = true; + } + else if(key == "MQTTOPPATH") + { + _preferences->putString(preference_mqtt_opener_path, value); configChanged = true; } else if(key == "HOSTNAME") @@ -277,7 +282,8 @@ void WebCfgServer::buildHtml(String& response) printInputField(response, "MQTTPORT", "MQTT Broker port", _preferences->getInt(preference_mqtt_broker_port), 5); printInputField(response, "MQTTUSER", "MQTT User (# to clear)", _preferences->getString(preference_mqtt_user).c_str(), 30); printInputField(response, "MQTTPASS", "MQTT Password", "*", 30, true); - printInputField(response, "MQTTPATH", "MQTT Path", _preferences->getString(preference_mqtt_path).c_str(), 180); + printInputField(response, "MQTTPATH", "MQTT Lock Path", _preferences->getString(preference_mqtt_lock_path).c_str(), 180); + printInputField(response, "MQTTOPPATH", "MQTT Opener Path", _preferences->getString(preference_mqtt_opener_path).c_str(), 180); printInputField(response, "HOSTNAME", "Host name", _preferences->getString(preference_hostname).c_str(), 100); printInputField(response, "NETTIMEOUT", "Network Timeout until restart (seconds; -1 to disable)", _preferences->getInt(preference_network_timeout), 5); printInputField(response, "LSTINT", "Query interval lock state (seconds)", _preferences->getInt(preference_query_interval_lockstate), 10); diff --git a/main.cpp b/main.cpp index 2a728b3..fa0da7a 100644 --- a/main.cpp +++ b/main.cpp @@ -8,10 +8,12 @@ #include "PresenceDetection.h" #include "hardware/W5500EthServer.h" #include "hardware/WifiEthServer.h" +#include "NukiOpenerWrapper.h" Network* network = nullptr; WebCfgServer* webCfgServer = nullptr; NukiWrapper* nuki = nullptr; +NukiOpenerWrapper* nukiOpener = nullptr; PresenceDetection* presenceDetection = nullptr; Preferences* preferences = nullptr; EthServer* ethServer = nullptr; @@ -120,6 +122,7 @@ void setup() initEthServer(networkDevice); nuki = new NukiWrapper("NukiHub", deviceId, network, preferences); +// nukiOpener = new NukiOpenerWrapper("NukiHub", deviceId, network, preferences); webCfgServer = new WebCfgServer(nuki, network, ethServer, preferences, networkDevice == NetworkDeviceType::WiFi); webCfgServer->initialize(); nuki->initialize();