diff --git a/CMakeLists.txt b/CMakeLists.txt index aef29e4..58ebbeb 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -50,8 +50,8 @@ file(GLOB SRCFILES networkDevices/NetworkDevice.h networkDevices/WifiDevice.cpp networkDevices/W5500Device.cpp - networkDevices/ClientSyncEthernet.cpp - networkDevices/espMqttClientEthernet.cpp + networkDevices/ClientSyncW5500.cpp + networkDevices/espMqttClientW5500.cpp NukiWrapper.cpp NukiOpenerWrapper.cpp MqttTopics.h @@ -62,6 +62,7 @@ file(GLOB SRCFILES PreferencesKeys.h Gpio.cpp Logger.cpp + RestartReason.h # include/RTOS.h lib/WiFiManager/WiFiManager.cpp lib/WiFiManager/wm_consts_en.h @@ -122,15 +123,9 @@ target_link_arduino_libraries(${PROJECT_NAME} WiFi WiFiClientSecure Update -# WebServer DNSServer Preferences - SPIFFS SPI -# Ethernet -# esp32 -# Wire -# FS ) target_link_arduino_libraries(DNSServer PUBLIC WiFi) diff --git a/Config.h b/Config.h index 55d5653..60fe004 100644 --- a/Config.h +++ b/Config.h @@ -1,6 +1,6 @@ #pragma once -#define NUKI_HUB_VERSION "8.3" +#define NUKI_HUB_VERSION "8.8" #define MQTT_QOS_LEVEL 1 #define MQTT_CLEAN_SESSIONS false \ No newline at end of file diff --git a/Gpio.cpp b/Gpio.cpp index 1f34bf6..db790f4 100644 --- a/Gpio.cpp +++ b/Gpio.cpp @@ -27,7 +27,6 @@ void Gpio::isrLock() if(millis() < _lockedTs) return; _nuki->lock(); _lockedTs = millis() + _debounceTime; - Log->println(F("Lock via GPIO"));; } void Gpio::isrUnlock() @@ -35,7 +34,6 @@ void Gpio::isrUnlock() if(millis() < _lockedTs) return; _nuki->unlock(); _lockedTs = millis() + _debounceTime; - Log->println(F("Unlock via GPIO"));; } void Gpio::isrUnlatch() @@ -43,5 +41,4 @@ void Gpio::isrUnlatch() if(millis() < _lockedTs) return; _nuki->unlatch(); _lockedTs = millis() + _debounceTime; - Log->println(F("Unlatch via GPIO"));; } diff --git a/MqttTopics.h b/MqttTopics.h index 998222a..95a87c6 100644 --- a/MqttTopics.h +++ b/MqttTopics.h @@ -45,4 +45,6 @@ #define mqtt_topic_uptime "/maintenance/uptime" #define mqtt_topic_wifi_rssi "/maintenance/wifiRssi" #define mqtt_topic_log "/maintenance/log" -#define mqtt_topic_freeheap "/maintenance/freeHeap" \ No newline at end of file +#define mqtt_topic_freeheap "/maintenance/freeHeap" +#define mqtt_topic_restart_reason_fw "/maintenance/restartReasonNukiHub" +#define mqtt_topic_restart_reason_esp "/maintenance/restartReasonNukiEsp" \ No newline at end of file diff --git a/Network.cpp b/Network.cpp index a3574b1..0f82cb2 100644 --- a/Network.cpp +++ b/Network.cpp @@ -6,6 +6,7 @@ #include "Logger.h" #include "Config.h" #include +#include "RestartReason.h" Network* Network::_inst = nullptr; @@ -65,8 +66,7 @@ void Network::setupDevice() Log->println(F(" for network device selection")); pinMode(hardwareDetectGpio, INPUT_PULLUP); - _networkDeviceType = NetworkDeviceType::W5500; -// digitalRead(hardwareDetectGpio) == HIGH ? NetworkDeviceType::WiFi : NetworkDeviceType::W5500; + _networkDeviceType = digitalRead(hardwareDetectGpio) == HIGH ? NetworkDeviceType::WiFi : NetworkDeviceType::W5500; } else if(hardwareDetect == 3) { @@ -173,6 +173,8 @@ void Network::initialize() _networkTimeout = -1; _preferences->putInt(preference_network_timeout, _networkTimeout); } + + _publishDebugInfo = _preferences->getBool(preference_publish_debug_info); } bool Network::update() @@ -185,7 +187,7 @@ bool Network::update() { if(_restartOnDisconnect && millis() > 60000) { - ESP.restart(); + restartEsp(RestartReason::RestartOnDisconnectWatchdog); } Log->println(F("Network not connected. Trying reconnect.")); @@ -197,7 +199,7 @@ bool Network::update() strcpy(WiFi_fallbackDetect, "wifi_fallback"); Log->println("Network device has a critical failure, enable fallback to Wifi and reboot."); delay(200); - ESP.restart(); + restartEsp(RestartReason::NetworkDeviceCriticalFailure); break; case ReconnectStatus::Success: memset(WiFi_fallbackDetect, 0, sizeof(WiFi_fallbackDetect)); @@ -216,7 +218,7 @@ bool Network::update() { Log->println("Network timeout has been reached, restarting ..."); delay(200); - ESP.restart(); + restartEsp(RestartReason::NetworkTimeoutWatchdog); } bool success = reconnect(); @@ -254,7 +256,12 @@ bool Network::update() if(_lastMaintenanceTs == 0 || (ts - _lastMaintenanceTs) > 30000) { publishULong(_maintenancePathPrefix, mqtt_topic_uptime, ts / 1000 / 60); -// publishUInt(_maintenancePathPrefix, mqtt_topic_freeheap, esp_get_free_heap_size()); + if(_publishDebugInfo) + { + publishUInt(_maintenancePathPrefix, mqtt_topic_freeheap, esp_get_free_heap_size()); + publishString(_maintenancePathPrefix, mqtt_topic_restart_reason_fw, getRestartReason().c_str()); + publishString(_maintenancePathPrefix, mqtt_topic_restart_reason_esp, getEspRestartReason().c_str()); + } _lastMaintenanceTs = ts; } @@ -367,6 +374,10 @@ bool Network::reconnect() } delay(1000); _mqttConnectionState = 2; + for(const auto& callback : _reconnectedCallbacks) + { + callback(); + } } else { @@ -471,6 +482,11 @@ bool Network::encryptionSupported() return _device->supportsEncryption(); } +const String Network::networkDeviceName() const +{ + return _device->deviceName(); +} + void Network::publishFloat(const char* prefix, const char* topic, const float value, const uint8_t precision) { char str[30]; @@ -924,3 +940,8 @@ void Network::setKeepAliveCallback(std::function reconnectTick) { _keepAliveCallback = reconnectTick; } + +void Network::addReconnectedCallback(std::function reconnectedCallback) +{ + _reconnectedCallbacks.push_back(reconnectedCallback); +} diff --git a/Network.h b/Network.h index 82add6d..7907727 100644 --- a/Network.h +++ b/Network.h @@ -45,12 +45,14 @@ public: int mqttConnectionState(); // 0 = not connected; 1 = connected; 2 = connected and mqtt processed bool encryptionSupported(); + const String networkDeviceName() const; const NetworkDeviceType networkDeviceType(); uint16_t subscribe(const char* topic, uint8_t qos); void setKeepAliveCallback(std::function reconnectTick); + void addReconnectedCallback(std::function reconnectedCallback); private: static void onMqttDataReceivedCallback(const espMqttClientTypes::MessageProperties& properties, const char* topic, const uint8_t* payload, size_t len, size_t index, size_t total); @@ -82,6 +84,7 @@ private: char* _presenceCsv = nullptr; bool _restartOnDisconnect = false; bool _firstConnect = true; + bool _publishDebugInfo = false; std::vector _subscribedTopics; std::map _initTopics; @@ -89,7 +92,8 @@ private: unsigned long _lastMaintenanceTs = 0; unsigned long _lastRssiTs = 0; long _rssiPublishInterval = 0; - std::function _keepAliveCallback; + std::function _keepAliveCallback = nullptr; + std::vector> _reconnectedCallbacks; NetworkDeviceType _networkDeviceType = (NetworkDeviceType)-1; diff --git a/NetworkLock.cpp b/NetworkLock.cpp index 8450946..7d14f77 100644 --- a/NetworkLock.cpp +++ b/NetworkLock.cpp @@ -5,6 +5,7 @@ #include "PreferencesKeys.h" #include "Pins.h" #include "Logger.h" +#include "RestartReason.h" NetworkLock::NetworkLock(Network* network, Preferences* preferences) : _network(network), @@ -69,6 +70,11 @@ void NetworkLock::initialize() _network->initTopic(_mqttPath, mqtt_topic_keypad_command_code, "000000"); _network->initTopic(_mqttPath, mqtt_topic_keypad_command_enabled, "1"); } + + _network->addReconnectedCallback([&]() + { + _reconnected = true; + }); } void NetworkLock::onMqttDataReceived(const char* topic, byte* payload, const unsigned int length) @@ -81,7 +87,7 @@ void NetworkLock::onMqttDataReceived(const char* topic, byte* payload, const uns { Log->println(F("Restart requested via MQTT.")); delay(200); - ESP.restart(); + restartEsp(RestartReason::RequestedViaMqtt); } if(processActions && comparePrefixedPath(topic, mqtt_topic_lock_action)) @@ -545,7 +551,6 @@ void NetworkLock::publishKeypadEntry(const String topic, NukiLock::KeypadEntry e publishInt(concat(topic, "/lockCount").c_str(), entry.lockCount); } - void NetworkLock::publishULong(const char *topic, const unsigned long value) { return _network->publishULong(_mqttPath, topic, value); @@ -557,3 +562,10 @@ String NetworkLock::concat(String a, String b) c.concat(b); return c; } + +bool NetworkLock::reconnected() +{ + bool r = _reconnected; + _reconnected = false; + return r; +} diff --git a/NetworkLock.h b/NetworkLock.h index 0b00831..b113f32 100644 --- a/NetworkLock.h +++ b/NetworkLock.h @@ -40,7 +40,11 @@ public: void onMqttDataReceived(const char* topic, byte* payload, const unsigned int length) override; + bool reconnected(); + private: + bool comparePrefixedPath(const char* fullPath, const char* subPath); + void publishFloat(const char* topic, const float value, const uint8_t precision = 2); void publishInt(const char* topic, const int value); void publishUInt(const char* topic, const unsigned int value); @@ -50,7 +54,6 @@ private: bool publishString(const char* topic, const std::string& value); bool publishString(const char* topic, const char* value); void publishKeypadEntry(const String topic, NukiLock::KeypadEntry entry); - bool comparePrefixedPath(const char* fullPath, const char* subPath); String concat(String a, String b); @@ -65,6 +68,7 @@ private: bool _firstTunerStatePublish = true; unsigned long _lastMaintenanceTs = 0; bool _haEnabled= false; + bool _reconnected = false; String _keypadCommandName = ""; String _keypadCommandCode = ""; diff --git a/NetworkOpener.cpp b/NetworkOpener.cpp index 9fe4898..68c8569 100644 --- a/NetworkOpener.cpp +++ b/NetworkOpener.cpp @@ -42,6 +42,25 @@ void NetworkOpener::initialize() { _network->subscribe(_mqttPath, topic); } + + if(_preferences->getBool(preference_keypad_control_enabled)) + { + _network->subscribe(_mqttPath, mqtt_topic_keypad_command_action); + _network->subscribe(_mqttPath, mqtt_topic_keypad_command_id); + _network->subscribe(_mqttPath, mqtt_topic_keypad_command_name); + _network->subscribe(_mqttPath, mqtt_topic_keypad_command_code); + _network->subscribe(_mqttPath, mqtt_topic_keypad_command_enabled); + _network->initTopic(_mqttPath, mqtt_topic_keypad_command_action, "--"); + _network->initTopic(_mqttPath, mqtt_topic_keypad_command_id, "0"); + _network->initTopic(_mqttPath, mqtt_topic_keypad_command_name, "--"); + _network->initTopic(_mqttPath, mqtt_topic_keypad_command_code, "000000"); + _network->initTopic(_mqttPath, mqtt_topic_keypad_command_enabled, "1"); + } + + _network->addReconnectedCallback([&]() + { + _reconnected = true; + }); } void NetworkOpener::update() @@ -76,6 +95,46 @@ void NetworkOpener::onMqttDataReceived(const char* topic, byte* payload, const u publishString(mqtt_topic_lock_action, success ? "ack" : "unknown_action"); } + if(processActions && comparePrefixedPath(topic, mqtt_topic_keypad_command_action)) + { + if(_keypadCommandReceivedReceivedCallback != nullptr) + { + if(strcmp(value, "--") == 0) return; + + _keypadCommandReceivedReceivedCallback(value, _keypadCommandId, _keypadCommandName, _keypadCommandCode, _keypadCommandEnabled); + + _keypadCommandId = 0; + _keypadCommandName = "--"; + _keypadCommandCode = "000000"; + _keypadCommandEnabled = 1; + + if(strcmp(value, "--") != 0) + { + publishString(mqtt_topic_keypad_command_action, "--"); + } + publishInt(mqtt_topic_keypad_command_id, _keypadCommandId); + publishString(mqtt_topic_keypad_command_name, _keypadCommandName); + publishString(mqtt_topic_keypad_command_code, _keypadCommandCode); + publishInt(mqtt_topic_keypad_command_enabled, _keypadCommandEnabled); + } + } + else if(comparePrefixedPath(topic, mqtt_topic_keypad_command_id)) + { + _keypadCommandId = atoi(value); + } + else if(comparePrefixedPath(topic, mqtt_topic_keypad_command_name)) + { + _keypadCommandName = value; + } + else if(comparePrefixedPath(topic, mqtt_topic_keypad_command_code)) + { + _keypadCommandCode = value; + } + else if(comparePrefixedPath(topic, mqtt_topic_keypad_command_enabled)) + { + _keypadCommandEnabled = atoi(value); + } + for(auto configTopic : _configTopics) { if(comparePrefixedPath(topic, configTopic)) @@ -399,6 +458,36 @@ void NetworkOpener::removeHASSConfig(char* uidString) _network->removeHASSConfig(uidString); } +void NetworkOpener::publishKeypad(const std::list& entries, uint maxKeypadCodeCount) +{ + uint index = 0; + for(const auto& entry : entries) + { + String basePath = mqtt_topic_keypad; + basePath.concat("/code_"); + basePath.concat(std::to_string(index).c_str()); + publishKeypadEntry(basePath, entry); + + ++index; + } + while(index < maxKeypadCodeCount) + { + NukiLock::KeypadEntry entry; + memset(&entry, 0, sizeof(entry)); + String basePath = mqtt_topic_keypad; + basePath.concat("/code_"); + basePath.concat(std::to_string(index).c_str()); + publishKeypadEntry(basePath, entry); + + ++index; + } +} + +void NetworkOpener::publishKeypadCommandResult(const char* result) +{ + publishString(mqtt_topic_keypad_command_result, result); +} + void NetworkOpener::setLockActionReceivedCallback(bool (*lockActionReceivedCallback)(const char *)) { _lockActionReceivedCallback = lockActionReceivedCallback; @@ -409,6 +498,11 @@ void NetworkOpener::setConfigUpdateReceivedCallback(void (*configUpdateReceivedC _configUpdateReceivedCallback = configUpdateReceivedCallback; } +void NetworkOpener::setKeypadCommandReceivedCallback(void (*keypadCommandReceivedReceivedCallback)(const char* command, const uint& id, const String& name, const String& code, const int& enabled)) +{ + _keypadCommandReceivedReceivedCallback = keypadCommandReceivedReceivedCallback; +} + void NetworkOpener::publishFloat(const char *topic, const float value, const uint8_t precision) { _network->publishFloat(_mqttPath, topic, value, precision); @@ -450,6 +544,24 @@ void NetworkOpener::publishString(const char* topic, const char* value) _network->publishString(_mqttPath, topic, value); } +void NetworkOpener::publishKeypadEntry(const String topic, NukiLock::KeypadEntry entry) +{ + char codeName[sizeof(entry.name) + 1]; + memset(codeName, 0, sizeof(codeName)); + memcpy(codeName, entry.name, sizeof(entry.name)); + + publishInt(concat(topic, "/id").c_str(), entry.codeId); + publishBool(concat(topic, "/enabled").c_str(), entry.enabled); + publishString(concat(topic, "/name").c_str(), codeName); + publishInt(concat(topic, "/createdYear").c_str(), entry.dateCreatedYear); + publishInt(concat(topic, "/createdMonth").c_str(), entry.dateCreatedMonth); + publishInt(concat(topic, "/createdDay").c_str(), entry.dateCreatedDay); + publishInt(concat(topic, "/createdHour").c_str(), entry.dateCreatedHour); + publishInt(concat(topic, "/createdMin").c_str(), entry.dateCreatedMin); + publishInt(concat(topic, "/createdSec").c_str(), entry.dateCreatedSec); + publishInt(concat(topic, "/lockCount").c_str(), entry.lockCount); +} + void NetworkOpener::buildMqttPath(const char* path, char* outPath) { int offset = 0; @@ -486,3 +598,17 @@ bool NetworkOpener::comparePrefixedPath(const char *fullPath, const char *subPat return strcmp(fullPath, prefixedPath) == 0; } + +String NetworkOpener::concat(String a, String b) +{ + String c = a; + c.concat(b); + return c; +} + +bool NetworkOpener::reconnected() +{ + bool r = _reconnected; + _reconnected = false; + return r; +} \ No newline at end of file diff --git a/NetworkOpener.h b/NetworkOpener.h index 873a5ee..6b9ce9c 100644 --- a/NetworkOpener.h +++ b/NetworkOpener.h @@ -32,12 +32,17 @@ public: void publishBleAddress(const std::string& address); void publishHASSConfig(char* deviceType, const char* baseTopic, char* name, char* uidString, char* lockAction, char* unlockAction, char* openAction, char* lockedState, char* unlockedState); void removeHASSConfig(char* uidString); + void publishKeypad(const std::list& entries, uint maxKeypadCodeCount); + void publishKeypadCommandResult(const char* result); void setLockActionReceivedCallback(bool (*lockActionReceivedCallback)(const char* value)); void setConfigUpdateReceivedCallback(void (*configUpdateReceivedCallback)(const char* path, const char* value)); + void setKeypadCommandReceivedCallback(void (*keypadCommandReceivedReceivedCallback)(const char* command, const uint& id, const String& name, const String& code, const int& enabled)); void onMqttDataReceived(const char* topic, byte* payload, const unsigned int length) override; + bool reconnected(); + private: bool comparePrefixedPath(const char* fullPath, const char* subPath); @@ -48,11 +53,14 @@ private: void publishString(const char* topic, const String& value); void publishString(const char* topic, const std::string& value); void publishString(const char* topic, const char* value); + void publishKeypadEntry(const String topic, NukiLock::KeypadEntry entry); void buildMqttPath(const char* path, char* outPath); void subscribe(const char* path); void logactionCompletionStatusToString(uint8_t value, char* out); + String concat(String a, String b); + Preferences* _preferences; Network* _network = nullptr; @@ -64,8 +72,15 @@ private: bool _firstTunerStatePublish = true; bool _haEnabled= false; + bool _reconnected = false; + + String _keypadCommandName = ""; + String _keypadCommandCode = ""; + uint _keypadCommandId = 0; + int _keypadCommandEnabled = 1; unsigned long _resetLockStateTs = 0; bool (*_lockActionReceivedCallback)(const char* value) = nullptr; void (*_configUpdateReceivedCallback)(const char* path, const char* value) = nullptr; + void (*_keypadCommandReceivedReceivedCallback)(const char* command, const uint& id, const String& name, const String& code, const int& enabled) = nullptr; }; diff --git a/NukiOpenerWrapper.cpp b/NukiOpenerWrapper.cpp index 7ba2685..0f5eb63 100644 --- a/NukiOpenerWrapper.cpp +++ b/NukiOpenerWrapper.cpp @@ -3,6 +3,7 @@ #include "PreferencesKeys.h" #include "MqttTopics.h" #include "Logger.h" +#include "RestartReason.h" #include NukiOpenerWrapper* nukiOpenerInst; @@ -24,6 +25,7 @@ NukiOpenerWrapper::NukiOpenerWrapper(const std::string& deviceName, uint32_t id, network->setLockActionReceivedCallback(nukiOpenerInst->onLockActionReceivedCallback); network->setConfigUpdateReceivedCallback(nukiOpenerInst->onConfigUpdateReceivedCallback); + network->setKeypadCommandReceivedCallback(nukiOpenerInst->onKeypadCommandReceivedCallback); } @@ -39,24 +41,56 @@ void NukiOpenerWrapper::initialize() _nukiOpener.registerBleScanner(_bleScanner); _intervalLockstate = _preferences->getInt(preference_query_interval_lockstate); + _intervalConfig = _preferences->getInt(preference_query_interval_configuration); _intervalBattery = _preferences->getInt(preference_query_interval_battery); + _intervalKeypad = _preferences->getInt(preference_query_interval_keypad); + _keypadEnabled = _preferences->getBool(preference_keypad_control_enabled); _publishAuthData = _preferences->getBool(preference_publish_authdata); + _maxKeypadCodeCount = _preferences->getUInt(preference_opener_max_keypad_code_count); _restartBeaconTimeout = _preferences->getInt(preference_restart_ble_beacon_lost); _hassEnabled = _preferences->getString(preference_mqtt_hass_discovery) != ""; _nrOfRetries = _preferences->getInt(preference_command_nr_of_retries); _retryDelay = _preferences->getInt(preference_command_retry_delay); _rssiPublishInterval = _preferences->getInt(preference_rssi_publish_interval) * 1000; + if(_retryDelay <= 100) + { + _retryDelay = 100; + _preferences->putInt(preference_command_retry_delay, _retryDelay); + } + if(_intervalLockstate == 0) { - _intervalLockstate = 60 * 5; + _intervalLockstate = 60 * 30; _preferences->putInt(preference_query_interval_lockstate, _intervalLockstate); } + if(_intervalConfig == 0) + { + _intervalConfig = 60 * 60; + _preferences->putInt(preference_query_interval_configuration, _intervalConfig); + } if(_intervalBattery == 0) { _intervalBattery = 60 * 30; _preferences->putInt(preference_query_interval_battery, _intervalBattery); } + if(_intervalKeypad == 0) + { + _intervalKeypad = 60 * 30; + _preferences->putInt(preference_query_interval_keypad, _intervalKeypad); + } + /* + if(_intervalKeypad == 0) + { + _intervalKeypad = 60 * 30; + _preferences->putInt(preference_query_interval_keypad, _intervalKeypad); + } + */ + if(_restartBeaconTimeout < 10) + { + _restartBeaconTimeout = -1; + _preferences->putInt(preference_restart_ble_beacon_lost, _restartBeaconTimeout); + } _nukiOpener.setEventHandler(this); @@ -108,7 +142,7 @@ void NukiOpenerWrapper::update() Log->print((millis() - _nukiOpener.getLastReceivedBeaconTs()) / 1000); Log->println(" seconds, restarting device."); delay(200); - ESP.restart(); + restartEsp(RestartReason::BLEBeaconWatchdog); } _nukiOpener.updateConnectionState(); @@ -128,12 +162,15 @@ void NukiOpenerWrapper::update() { _nextConfigUpdateTs = ts + _intervalConfig * 1000; updateConfig(); - if(_hassEnabled) + if(_hassEnabled && !_hassSetupCompleted) { setupHASS(); } } - + if(_hassEnabled && _configRead && _network->reconnected()) + { + setupHASS(); + } if(_rssiPublishInterval > 0 && (_nextRssiTs == 0 || ts > _nextRssiTs)) { _nextRssiTs = ts + _rssiPublishInterval; @@ -146,6 +183,12 @@ void NukiOpenerWrapper::update() } } + if(_hasKeypad && _keypadEnabled && (_nextKeypadUpdateTs == 0 || ts > _nextKeypadUpdateTs)) + { + _nextKeypadUpdateTs = ts + _intervalKeypad * 1000; + updateKeypad(); + } + if(_nextLockAction != (NukiOpener::LockAction)0xff && ts > _nextRetryTs) { Nuki::CmdResult cmdResult = _nukiOpener.lockAction(_nextLockAction, 0, 0); @@ -194,6 +237,7 @@ void NukiOpenerWrapper::update() _nextLockAction = (NukiOpener::LockAction) 0xff; } } + postponeBleWatchdog(); } if(_clearAuthData) @@ -205,6 +249,11 @@ void NukiOpenerWrapper::update() memcpy(&_lastKeyTurnerState, &_keyTurnerState, sizeof(NukiOpener::OpenerState)); } +bool NukiOpenerWrapper::isPinSet() +{ + return _nukiOpener.getSecurityPincode() != 0; +} + void NukiOpenerWrapper::setPin(const uint16_t pin) { _nukiOpener.saveSecurityPincode(pin); @@ -218,7 +267,18 @@ void NukiOpenerWrapper::unpair() void NukiOpenerWrapper::updateKeyTurnerState() { - _nukiOpener.requestOpenerState(&_keyTurnerState); + Nuki::CmdResult result =_nukiOpener.requestOpenerState(&_keyTurnerState); + if(result != Nuki::CmdResult::Success) + { + _retryLockstateCount++; + postponeBleWatchdog(); + if(_retryLockstateCount < _nrOfRetries) + { + _nextLockStateUpdateTs = millis() + _retryDelay; + } + return; + } + _retryLockstateCount = 0; if(_statusUpdated && _keyTurnerState.lockState == NukiOpener::LockState::Locked && @@ -249,20 +309,34 @@ void NukiOpenerWrapper::updateKeyTurnerState() { updateAuthData(); } + + postponeBleWatchdog(); } void NukiOpenerWrapper::updateBatteryState() { - _nukiOpener.requestBatteryReport(&_batteryReport); - _network->publishBatteryReport(_batteryReport); + Nuki::CmdResult result = _nukiOpener.requestBatteryReport(&_batteryReport); + if(result == Nuki::CmdResult::Success) + { + _network->publishBatteryReport(_batteryReport); + } + postponeBleWatchdog(); } void NukiOpenerWrapper::updateConfig() { readConfig(); readAdvancedConfig(); - _network->publishConfig(_nukiConfig); - _network->publishAdvancedConfig(_nukiAdvancedConfig); + _configRead = true; + _hasKeypad = _nukiConfig.hasKeypad > 0; + if(_nukiConfigValid) + { + _network->publishConfig(_nukiConfig); + } + if(_nukiAdvancedConfigValid) + { + _network->publishAdvancedConfig(_nukiAdvancedConfig); + } } void NukiOpenerWrapper::updateAuthData() @@ -290,6 +364,41 @@ void NukiOpenerWrapper::updateAuthData() { _network->publishAuthorizationInfo(log); } + postponeBleWatchdog(); +} + +void NukiOpenerWrapper::updateKeypad() +{ + Nuki::CmdResult result = _nukiOpener.retrieveKeypadEntries(0, 0xffff); + if(result == 1) + { + std::list entries; + _nukiOpener.getKeypadEntries(&entries); + + entries.sort([](const NukiLock::KeypadEntry& a, const NukiLock::KeypadEntry& b) { return a.codeId < b.codeId; }); + + uint keypadCount = entries.size(); + if(keypadCount > _maxKeypadCodeCount) + { + _maxKeypadCodeCount = keypadCount; + _preferences->putUInt(preference_opener_max_keypad_code_count, _maxKeypadCodeCount); + } + + _network->publishKeypad(entries, _maxKeypadCodeCount); + + _keypadCodeIds.clear(); + _keypadCodeIds.reserve(entries.size()); + for(const auto& entry : entries) + { + _keypadCodeIds.push_back(entry.codeId); + } + } + postponeBleWatchdog(); +} + +void NukiOpenerWrapper::postponeBleWatchdog() +{ + _disableBleWatchdogTs = millis() + 15000; } NukiOpener::LockAction NukiOpenerWrapper::lockActionToEnum(const char *str) @@ -317,6 +426,10 @@ void NukiOpenerWrapper::onConfigUpdateReceivedCallback(const char *topic, const nukiOpenerInst->onConfigUpdateReceived(topic, value); } +void NukiOpenerWrapper::onKeypadCommandReceivedCallback(const char *command, const uint &id, const String &name, const String &code, const int& enabled) +{ + nukiOpenerInst->onKeypadCommandReceived(command, id, name, code, enabled); +} void NukiOpenerWrapper::onConfigUpdateReceived(const char *topic, const char *value) { @@ -343,6 +456,117 @@ void NukiOpenerWrapper::onConfigUpdateReceived(const char *topic, const char *va } } +void NukiOpenerWrapper::onKeypadCommandReceived(const char *command, const uint &id, const String &name, const String &code, const int& enabled) +{ + if(!_hasKeypad) + { + if(_configRead) + { + _network->publishKeypadCommandResult("KeypadNotAvailable"); + } + return; + } + if(!_keypadEnabled) + { + return; + } + + bool idExists = std::find(_keypadCodeIds.begin(), _keypadCodeIds.end(), id) != _keypadCodeIds.end(); + int codeInt = code.toInt(); + bool codeValid = codeInt > 100000 && codeInt < 1000000 && (code.indexOf('0') == -1); + NukiLock::CmdResult result = (NukiLock::CmdResult)-1; + + if(strcmp(command, "add") == 0) + { + if(name == "" || name == "--") + { + _network->publishKeypadCommandResult("MissingParameterName"); + return; + } + if(codeInt == 0) + { + _network->publishKeypadCommandResult("MissingParameterCode"); + return; + } + if(!codeValid) + { + _network->publishKeypadCommandResult("CodeInvalid"); + return; + } + + NukiLock::NewKeypadEntry entry; + memset(&entry, 0, sizeof(entry)); + size_t nameLen = name.length(); + memcpy(&entry.name, name.c_str(), nameLen > 20 ? 20 : nameLen); + entry.code = codeInt; + result = _nukiOpener.addKeypadEntry(entry); + Log->print("Add keypad code: "); Log->println((int)result); + updateKeypad(); + } + else if(strcmp(command, "delete") == 0) + { + if(!idExists) + { + _network->publishKeypadCommandResult("UnknownId"); + return; + } + result = _nukiOpener.deleteKeypadEntry(id); + Log->print("Delete keypad code: "); Log->println((int)result); + updateKeypad(); + } + else if(strcmp(command, "update") == 0) + { + if(name == "" || name == "--") + { + _network->publishKeypadCommandResult("MissingParameterName"); + return; + } + if(codeInt == 0) + { + _network->publishKeypadCommandResult("MissingParameterCode"); + return; + } + if(!codeValid) + { + _network->publishKeypadCommandResult("CodeInvalid"); + return; + } + if(!idExists) + { + _network->publishKeypadCommandResult("UnknownId"); + return; + } + + NukiLock::UpdatedKeypadEntry entry; + memset(&entry, 0, sizeof(entry)); + entry.codeId = id; + size_t nameLen = name.length(); + memcpy(&entry.name, name.c_str(), nameLen > 20 ? 20 : nameLen); + entry.code = codeInt; + entry.enabled = enabled == 0 ? 0 : 1; + result = _nukiOpener.updateKeypadEntry(entry); + Log->print("Update keypad code: "); Log->println((int)result); + updateKeypad(); + } + else if(command == "--") + { + return; + } + else + { + _network->publishKeypadCommandResult("UnknownCommand"); + return; + } + + if((int)result != -1) + { + char resultStr[15]; + memset(&resultStr, 0, sizeof(resultStr)); + NukiOpener::cmdResultToString(result, resultStr); + _network->publishKeypadCommandResult(resultStr); + } +} + const NukiOpener::OpenerState &NukiOpenerWrapper::keyTurnerState() { return _keyTurnerState; @@ -353,6 +577,11 @@ const bool NukiOpenerWrapper::isPaired() return _paired; } +const bool NukiOpenerWrapper::hasKeypad() +{ + return _hasKeypad; +} + const BLEAddress NukiOpenerWrapper::getBleAddress() const { return _nukiOpener.getBleAddress(); @@ -379,6 +608,7 @@ void NukiOpenerWrapper::readConfig() char resultStr[20]; NukiOpener::cmdResultToString(result, resultStr); Log->println(resultStr); + postponeBleWatchdog(); } void NukiOpenerWrapper::readAdvancedConfig() @@ -389,11 +619,12 @@ void NukiOpenerWrapper::readAdvancedConfig() char resultStr[20]; NukiOpener::cmdResultToString(result, resultStr); Log->println(resultStr); + postponeBleWatchdog(); } void NukiOpenerWrapper::setupHASS() { - if(!_nukiConfigValid || _hassSetupCompleted) return; + if(!_nukiConfigValid) return; String baseTopic = _preferences->getString(preference_mqtt_opener_path); char uidString[20]; diff --git a/NukiOpenerWrapper.h b/NukiOpenerWrapper.h index 152d9cf..cdfd2b4 100644 --- a/NukiOpenerWrapper.h +++ b/NukiOpenerWrapper.h @@ -15,6 +15,7 @@ public: void initialize(); void update(); + bool isPinSet(); void setPin(const uint16_t pin); void unpair(); @@ -23,6 +24,7 @@ public: const NukiOpener::OpenerState& keyTurnerState(); const bool isPaired(); + const bool hasKeypad(); const BLEAddress getBleAddress() const; BleScanner::Scanner* bleScanner(); @@ -32,12 +34,16 @@ public: private: static bool onLockActionReceivedCallback(const char* value); static void onConfigUpdateReceivedCallback(const char* topic, const char* value); + static void onKeypadCommandReceivedCallback(const char* command, const uint& id, const String& name, const String& code, const int& enabled); void onConfigUpdateReceived(const char* topic, const char* value); + void onKeypadCommandReceived(const char* command, const uint& id, const String& name, const String& code, const int& enabled); void updateKeyTurnerState(); void updateBatteryState(); void updateConfig(); void updateAuthData(); + void updateKeypad(); + void postponeBleWatchdog(); void readConfig(); void readAdvancedConfig(); @@ -54,13 +60,16 @@ private: int _intervalLockstate = 0; // seconds int _intervalBattery = 0; // seconds int _intervalConfig = 60 * 60; // seconds + int _intervalKeypad = 0; // seconds int _restartBeaconTimeout = 0; // seconds bool _publishAuthData = false; bool _clearAuthData = false; int _nrOfRetries = 0; int _retryDelay = 0; int _retryCount = 0; + int _retryLockstateCount = 0; unsigned long _nextRetryTs = 0; + std::vector _keypadCodeIds; NukiOpener::OpenerState _lastKeyTurnerState; NukiOpener::OpenerState _keyTurnerState; @@ -77,12 +86,18 @@ private: bool _paired = false; bool _statusUpdated = false; + bool _hasKeypad = false; + bool _keypadEnabled = false; + uint _maxKeypadCodeCount = 0; + bool _configRead = false; long _rssiPublishInterval = 0; unsigned long _nextLockStateUpdateTs = 0; unsigned long _nextBatteryReportTs = 0; unsigned long _nextConfigUpdateTs = 0; + unsigned long _nextKeypadUpdateTs = 0; unsigned long _nextPairTs = 0; long _nextRssiTs = 0; unsigned long _lastRssi = 0; + unsigned long _disableBleWatchdogTs = 0; NukiOpener::LockAction _nextLockAction = (NukiOpener::LockAction)0xff; }; diff --git a/NukiWrapper.cpp b/NukiWrapper.cpp index 7c72a76..5b04191 100644 --- a/NukiWrapper.cpp +++ b/NukiWrapper.cpp @@ -3,6 +3,7 @@ #include "PreferencesKeys.h" #include "MqttTopics.h" #include "Logger.h" +#include "RestartReason.h" #include NukiWrapper* nukiInst; @@ -41,11 +42,12 @@ void NukiWrapper::initialize(const bool& firstStart) _nukiLock.registerBleScanner(_bleScanner); _intervalLockstate = _preferences->getInt(preference_query_interval_lockstate); + _intervalConfig = _preferences->getInt(preference_query_interval_battery); _intervalBattery = _preferences->getInt(preference_query_interval_battery); _intervalKeypad = _preferences->getInt(preference_query_interval_keypad); _keypadEnabled = _preferences->getBool(preference_keypad_control_enabled); _publishAuthData = _preferences->getBool(preference_publish_authdata); - _maxKeypadCodeCount = _preferences->getUInt(preference_max_keypad_code_count); + _maxKeypadCodeCount = _preferences->getUInt(preference_lock_max_keypad_code_count); _restartBeaconTimeout = _preferences->getInt(preference_restart_ble_beacon_lost); _hassEnabled = _preferences->getString(preference_mqtt_hass_discovery) != ""; _nrOfRetries = _preferences->getInt(preference_command_nr_of_retries); @@ -57,6 +59,7 @@ void NukiWrapper::initialize(const bool& firstStart) _preferences->putInt(preference_command_nr_of_retries, 3); _preferences->putInt(preference_command_retry_delay, 1000); } + if(_retryDelay <= 100) { _retryDelay = 100; @@ -68,6 +71,11 @@ void NukiWrapper::initialize(const bool& firstStart) _intervalLockstate = 60 * 30; _preferences->putInt(preference_query_interval_lockstate, _intervalLockstate); } + if(_intervalConfig == 0) + { + _intervalConfig = 60 * 60; + _preferences->putInt(preference_query_interval_configuration, _intervalConfig); + } if(_intervalBattery == 0) { _intervalBattery = 60 * 30; @@ -128,13 +136,14 @@ void NukiWrapper::update() if(_restartBeaconTimeout > 0 && ts > 60000 && lastReceivedBeaconTs > 0 && + _disableBleWatchdogTs < ts && (ts - lastReceivedBeaconTs > _restartBeaconTimeout * 1000)) { Log->print("No BLE beacon received from the lock for "); Log->print((millis() - _nukiLock.getLastReceivedBeaconTs()) / 1000); Log->println(" seconds, restarting device."); delay(200); - ESP.restart(); + restartEsp(RestartReason::BLEBeaconWatchdog); } _nukiLock.updateConnectionState(); @@ -154,11 +163,15 @@ void NukiWrapper::update() { _nextConfigUpdateTs = ts + _intervalConfig * 1000; updateConfig(); - if(_hassEnabled) + if(_hassEnabled && !_hassSetupCompleted) { setupHASS(); } } + if(_hassEnabled && _configRead && _network->reconnected()) + { + setupHASS(); + } if(_rssiPublishInterval > 0 && (_nextRssiTs == 0 || ts > _nextRssiTs)) { _nextRssiTs = ts + _rssiPublishInterval; @@ -225,6 +238,7 @@ void NukiWrapper::update() _nextLockAction = (NukiLock::LockAction) 0xff; } } + postponeBleWatchdog(); } if(_clearAuthData) @@ -251,6 +265,11 @@ void NukiWrapper::unlatch() _nextLockAction = NukiLock::LockAction::Unlatch; } +bool NukiWrapper::isPinSet() +{ + return _nukiLock.getSecurityPincode() != 0; +} + void NukiWrapper::setPin(const uint16_t pin) { _nukiLock.saveSecurityPincode(pin); @@ -264,7 +283,19 @@ void NukiWrapper::unpair() void NukiWrapper::updateKeyTurnerState() { - _nukiLock.requestKeyTurnerState(&_keyTurnerState); + Nuki::CmdResult result =_nukiLock.requestKeyTurnerState(&_keyTurnerState); + if(result != Nuki::CmdResult::Success) + { + _retryLockstateCount++; + postponeBleWatchdog(); + if(_retryLockstateCount < _nrOfRetries) + { + _nextLockStateUpdateTs = millis() + _retryDelay; + } + return; + } + _retryLockstateCount = 0; + _network->publishKeyTurnerState(_keyTurnerState, _lastKeyTurnerState); if(_keyTurnerState.lockState != _lastKeyTurnerState.lockState) @@ -279,12 +310,18 @@ void NukiWrapper::updateKeyTurnerState() { updateAuthData(); } + + postponeBleWatchdog(); } void NukiWrapper::updateBatteryState() { - _nukiLock.requestBatteryReport(&_batteryReport); - _network->publishBatteryReport(_batteryReport); + Nuki::CmdResult result = _nukiLock.requestBatteryReport(&_batteryReport); + if(result == Nuki::CmdResult::Success) + { + _network->publishBatteryReport(_batteryReport); + } + postponeBleWatchdog(); } void NukiWrapper::updateConfig() @@ -293,8 +330,14 @@ void NukiWrapper::updateConfig() readAdvancedConfig(); _configRead = true; _hasKeypad = _nukiConfig.hasKeypad > 0; - _network->publishConfig(_nukiConfig); - _network->publishAdvancedConfig(_nukiAdvancedConfig); + if(_nukiConfigValid) + { + _network->publishConfig(_nukiConfig); + } + if(_nukiAdvancedConfigValid) + { + _network->publishAdvancedConfig(_nukiAdvancedConfig); + } } void NukiWrapper::updateAuthData() @@ -322,6 +365,7 @@ void NukiWrapper::updateAuthData() { _network->publishAuthorizationInfo(log); } + postponeBleWatchdog(); } void NukiWrapper::updateKeypad() @@ -338,7 +382,7 @@ void NukiWrapper::updateKeypad() if(keypadCount > _maxKeypadCodeCount) { _maxKeypadCodeCount = keypadCount; - _preferences->putUInt(preference_max_keypad_code_count, _maxKeypadCodeCount); + _preferences->putUInt(preference_lock_max_keypad_code_count, _maxKeypadCodeCount); } _network->publishKeypad(entries, _maxKeypadCodeCount); @@ -350,6 +394,12 @@ void NukiWrapper::updateKeypad() _keypadCodeIds.push_back(entry.codeId); } } + postponeBleWatchdog(); +} + +void NukiWrapper::postponeBleWatchdog() +{ + _disableBleWatchdogTs = millis() + 15000; } NukiLock::LockAction NukiWrapper::lockActionToEnum(const char *str) @@ -378,13 +428,11 @@ void NukiWrapper::onConfigUpdateReceivedCallback(const char *topic, const char * nukiInst->onConfigUpdateReceived(topic, value); } - void NukiWrapper::onKeypadCommandReceivedCallback(const char *command, const uint &id, const String &name, const String &code, const int& enabled) { nukiInst->onKeypadCommandReceived(command, id, name, code, enabled); } - void NukiWrapper::onConfigUpdateReceived(const char *topic, const char *value) { if(strcmp(topic, mqtt_topic_config_button_enabled) == 0) @@ -594,7 +642,7 @@ void NukiWrapper::readAdvancedConfig() void NukiWrapper::setupHASS() { - if(!_nukiConfigValid || _hassSetupCompleted) return; + if(!_nukiConfigValid) return; String baseTopic = _preferences->getString(preference_mqtt_lock_path); char uidString[20]; diff --git a/NukiWrapper.h b/NukiWrapper.h index 6a6e758..a1161d8 100644 --- a/NukiWrapper.h +++ b/NukiWrapper.h @@ -19,6 +19,7 @@ public: void unlock(); void unlatch(); + bool isPinSet(); void setPin(const uint16_t pin); void unpair(); @@ -43,12 +44,12 @@ private: void updateConfig(); void updateAuthData(); void updateKeypad(); + void postponeBleWatchdog(); void readConfig(); void readAdvancedConfig(); void setupHASS(); - bool hasDoorSensor(); NukiLock::LockAction lockActionToEnum(const char* str); // char array at least 14 characters @@ -84,11 +85,12 @@ private: bool _statusUpdated = false; bool _hasKeypad = false; bool _keypadEnabled = false; - bool _configRead = false; uint _maxKeypadCodeCount = 0; + bool _configRead = false; int _nrOfRetries = 0; int _retryDelay = 0; int _retryCount = 0; + int _retryLockstateCount = 0; long _rssiPublishInterval = 0; unsigned long _nextRetryTs = 0; unsigned long _nextLockStateUpdateTs = 0; @@ -97,5 +99,6 @@ private: unsigned long _nextKeypadUpdateTs = 0; unsigned long _nextRssiTs = 0; unsigned long _lastRssi = 0; - NukiLock::LockAction _nextLockAction = (NukiLock::LockAction)0xff; + unsigned long _disableBleWatchdogTs = 0; + volatile NukiLock::LockAction _nextLockAction = (NukiLock::LockAction)0xff; }; diff --git a/Ota.cpp b/Ota.cpp index ef4b791..b29314b 100644 --- a/Ota.cpp +++ b/Ota.cpp @@ -1,6 +1,7 @@ #include #include "Ota.h" #include "Logger.h" +#include "RestartReason.h" #define FULL_PACKET 1436 // HTTP_UPLOAD_BUFLEN in WebServer,h @@ -26,7 +27,7 @@ void Ota::updateFirmware(uint8_t* buf, size_t size) if (ESP_OK == esp_ota_set_boot_partition(esp_ota_get_next_update_partition(NULL))) { delay(2000); - esp_restart(); + restartEsp(RestartReason::OTACompleted); } else { diff --git a/PreferencesKeys.h b/PreferencesKeys.h index 806bf44..0403cb7 100644 --- a/PreferencesKeys.h +++ b/PreferencesKeys.h @@ -1,5 +1,7 @@ #pragma once +#include + #define preference_started_before "run" #define preference_deviceId "deviceId" #define preference_mqtt_broker "mqttbroker" @@ -11,7 +13,8 @@ #define preference_mqtt_lock_path "mqttpath" #define preference_opener_enabled "openerena" #define preference_mqtt_opener_path "mqttoppath" -#define preference_max_keypad_code_count "maxkpad" +#define preference_lock_max_keypad_code_count "maxkpad" +#define preference_opener_max_keypad_code_count "opmaxkpad" #define preference_mqtt_ca "mqttca" #define preference_mqtt_crt "mqttcrt" #define preference_mqtt_key "mqttkey" @@ -25,6 +28,7 @@ #define preference_restart_timer "resttmr" #define preference_restart_ble_beacon_lost "rstbcn" #define preference_query_interval_lockstate "lockStInterval" +#define preference_query_interval_configuration "configInterval" #define preference_query_interval_battery "batInterval" #define preference_query_interval_keypad "kpInterval" #define preference_keypad_control_enabled "kpEnabled" @@ -35,11 +39,183 @@ #define preference_cred_password "crdpass" #define preference_publish_authdata "pubauth" #define preference_gpio_locking_enabled "gpiolck" +#define preference_publish_debug_info "pubdbg" #define preference_presence_detection_timeout "prdtimeout" #define preference_has_mac_saved "hasmac" #define preference_has_mac_byte_0 "macb0" #define preference_has_mac_byte_1 "macb1" #define preference_has_mac_byte_2 "macb2" +class DebugPreferences +{ +private: + std::vector _keys = + { + preference_started_before, preference_deviceId, preference_mqtt_broker, preference_mqtt_broker_port, preference_mqtt_user, preference_mqtt_password, preference_mqtt_log_enabled, preference_lock_enabled, preference_mqtt_lock_path, preference_opener_enabled, preference_mqtt_opener_path, preference_lock_max_keypad_code_count, preference_opener_max_keypad_code_count, preference_mqtt_ca, preference_mqtt_crt, preference_mqtt_key, preference_mqtt_hass_discovery, preference_network_hardware, preference_network_hardware_gpio, preference_rssi_publish_interval, preference_hostname, preference_network_timeout, preference_restart_on_disconnect, preference_restart_timer, preference_restart_ble_beacon_lost, preference_query_interval_lockstate, preference_query_interval_configuration, preference_query_interval_battery, preference_query_interval_keypad, preference_keypad_control_enabled, preference_register_as_app, preference_command_nr_of_retries, preference_command_retry_delay, preference_cred_user, preference_cred_password, preference_publish_authdata, preference_gpio_locking_enabled, preference_publish_debug_info, preference_presence_detection_timeout, preference_has_mac_saved, preference_has_mac_byte_0, preference_has_mac_byte_1, preference_has_mac_byte_2, + }; + std::vector _redact = + { + preference_mqtt_user, preference_mqtt_password, + preference_mqtt_ca, preference_mqtt_crt, preference_mqtt_key, + preference_cred_user, preference_cred_password, + }; + std::vector _boolPrefs = + { + preference_started_before, preference_mqtt_log_enabled, preference_lock_enabled, preference_opener_enabled, + preference_restart_on_disconnect, preference_keypad_control_enabled, preference_register_as_app, + preference_publish_authdata, preference_gpio_locking_enabled, preference_has_mac_saved, preference_publish_debug_info + }; + const bool isRedacted(const char* key) const + { + return std::find(_redact.begin(), _redact.end(), key) != _redact.end(); + } + const String redact(const String s) const + { + return s == "" ? "" : "***"; + } + const String redact(const int i) const + { + return i == 0 ? "" : "***"; + } + const String redact(const uint i) const + { + return i == 0 ? "" : "***"; + } + const String redact(const int64_t i) const + { + return i == 0 ? "" : "***"; + } + const String redact(const uint64_t i) const + { + return i == 0 ? "" : "***"; + } + const void appendPreferenceInt8(Preferences *preferences, String& s, const char* description, const char* key) + { + s.concat(description); + s.concat(": "); + s.concat(isRedacted(key) ? redact(preferences->getChar(key)) : String(preferences->getChar(key))); + s.concat("\n"); + } + const void appendPreferenceUInt8(Preferences *preferences, String& s, const char* description, const char* key) + { + s.concat(description); + s.concat(": "); + s.concat(isRedacted(key) ? redact(preferences->getUChar(key)) : String(preferences->getUChar(key))); + s.concat("\n"); + } + const void appendPreferenceInt16(Preferences *preferences, String& s, const char* description, const char* key) + { + s.concat(description); + s.concat(": "); + s.concat(isRedacted(key) ? redact(preferences->getShort(key)) : String(preferences->getShort(key))); + s.concat("\n"); + } + const void appendPreferenceUInt16(Preferences *preferences, String& s, const char* description, const char* key) + { + s.concat(description); + s.concat(": "); + s.concat(isRedacted(key) ? redact(preferences->getUShort(key)) : String(preferences->getUShort(key))); + s.concat("\n"); + } + const void appendPreferenceInt32(Preferences *preferences, String& s, const char* description, const char* key) + { + s.concat(description); + s.concat(": "); + s.concat(isRedacted(key) ? redact(preferences->getInt(key)) : String(preferences->getInt(key))); + s.concat("\n"); + } + const void appendPreferenceUInt32(Preferences *preferences, String& s, const char* description, const char* key) + { + s.concat(description); + s.concat(": "); + s.concat(isRedacted(key) ? redact(preferences->getUInt(key)) : String(preferences->getUInt(key))); + s.concat("\n"); + } + const void appendPreferenceInt64(Preferences *preferences, String& s, const char* description, const char* key) + { + s.concat(description); + s.concat(": "); + s.concat(isRedacted(key) ? redact(preferences->getLong64(key)) : String(preferences->getLong64(key))); + s.concat("\n"); + } + const void appendPreferenceUInt64(Preferences *preferences, String& s, const char* description, const char* key) + { + s.concat(description); + s.concat(": "); + s.concat(isRedacted(key) ? redact(preferences->getULong64(key)) : String(preferences->getULong64(key))); + s.concat("\n"); + } + const void appendPreferenceBool(Preferences *preferences, String& s, const char* description, const char* key) + { + s.concat(description); + s.concat(": "); + s.concat(preferences->getBool(key) ? "true" : "false"); + s.concat("\n"); + } + const void appendPreferenceString(Preferences *preferences, String& s, const char* description, const char* key) + { + s.concat(description); + s.concat(": "); + s.concat(isRedacted(key) ? redact(preferences->getString(key)) : preferences->getString(key)); + s.concat("\n"); + } + + const void appendPreference(Preferences *preferences, String& s, const char* key) + { + if(std::find(_boolPrefs.begin(), _boolPrefs.end(), key) != _boolPrefs.end()) + { + appendPreferenceBool(preferences, s, key, key); + return; + } + + switch(preferences->getType(key)) + { + case PT_I8: + appendPreferenceInt8(preferences, s, key, key); + break; + case PT_I16: + appendPreferenceInt16(preferences, s, key, key); + break; + case PT_I32: + appendPreferenceInt32(preferences, s, key, key); + break; + case PT_I64: + appendPreferenceInt64(preferences, s, key, key); + break; + case PT_U8: + appendPreferenceUInt8(preferences, s, key, key); + break; + case PT_U16: + appendPreferenceUInt16(preferences, s, key, key); + break; + case PT_U32: + appendPreferenceUInt32(preferences, s, key, key); + break; + case PT_U64: + appendPreferenceUInt64(preferences, s, key, key); + break; + case PT_STR: + appendPreferenceString(preferences, s, key, key); + break; + default: + appendPreferenceString(preferences, s, key, key); + break; + } + } + +public: + const String preferencesToString(Preferences *preferences) + { + String s = ""; + + for(const auto& key : _keys) + { + appendPreference(preferences, s, key); + } + + return s; + } + +}; diff --git a/README.md b/README.md index 3f5c4e4..6575cbe 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ NUKI Smart Lock 2.0
NUKI Smart Lock 3.0
NUKI Smart Lock 3.0 Pro
NUKI Opener
-NUKI Keypad 1.0 +NUKI Keypad 1.0 (2.0 not supported yet due to current [NUKI limitations](https://developer.nuki.io/t/keypad-2-not-setting-has-keypad-flag-in-config-of-nuki-lock/19539)) ## Installation @@ -124,6 +124,8 @@ NOTE: MQTT Discovery uses retained MQTT messages to store devices configurations ## Keypad control (optional) +Note: Only devices listed in the [About section](#about) are currently supported. + If a keypad is connected to the lock, keypad codes can be added, updated and removed. This has to enabled first in the configuration portal. Check "Enabled keypad control via MQTT" and save the configuration. After enabling keypad control, information about codes is published under "keypad/code_x", x starting from 0 up the number of configured codes. diff --git a/RestartReason.h b/RestartReason.h new file mode 100644 index 0000000..8da243d --- /dev/null +++ b/RestartReason.h @@ -0,0 +1,109 @@ +#pragma once + +enum class RestartReason +{ + RequestedViaMqtt, + BLEBeaconWatchdog, + RestartOnDisconnectWatchdog, + RestartIntervalWatchdog, + NetworkTimeoutWatchdog, + WifiInitFailed, + ReconfigureWifi, + NetworkDeviceCriticalFailure, + ConfigurationUpdated, + RestartTimer, + OTACompleted, + OTATimeout, + OTAAborted, + OTAUnknownState, + DeviceUnpaired, +}; + +#define RESTART_REASON_VALID_DETECT 0xa00ab00bc00bd00d; + +extern int restartReason; +extern uint64_t restartReasonValid; + +inline static void restartEsp(RestartReason reason) +{ + restartReason = (int)reason; + restartReasonValid = RESTART_REASON_VALID_DETECT; + ESP.restart(); +} + +inline static String getRestartReason() +{ + uint64_t cmp = RESTART_REASON_VALID_DETECT; + if(restartReasonValid != cmp) + { + return "UnknownNoRestartRegistered"; + } + + switch((RestartReason)restartReason) + { + case RestartReason::RequestedViaMqtt: + return "RequestedViaMqtt"; + case RestartReason::BLEBeaconWatchdog: + return "BLEBeaconWatchdog"; + case RestartReason::RestartOnDisconnectWatchdog: + return "RestartOnDisconnectWatchdog"; + case RestartReason::RestartIntervalWatchdog: + return "RestartIntervalWatchdog"; + case RestartReason::NetworkTimeoutWatchdog: + return "NetworkTimeoutWatchdog"; + case RestartReason::WifiInitFailed: + return "WifiInitFailed"; + case RestartReason::ReconfigureWifi: + return "ReconfigureWifi"; + case RestartReason::NetworkDeviceCriticalFailure: + return "NetworkDeviceCriticalFailure"; + case RestartReason::ConfigurationUpdated: + return "ConfigurationUpdated"; + case RestartReason::RestartTimer: + return "RestartTimer"; + case RestartReason::OTACompleted: + return "OTACompleted"; + case RestartReason::OTATimeout: + return "OTATimeout"; + case RestartReason::OTAAborted: + return "OTAAborted"; + case RestartReason::OTAUnknownState: + return "OTAUnknownState"; + case RestartReason::DeviceUnpaired: + return "DeviceUnpaired"; + default: + return "Unknown: " + restartReason; + } +} + +inline static String getEspRestartReason() +{ + esp_reset_reason_t reason = esp_reset_reason(); + switch(reason) + { + case esp_reset_reason_t::ESP_RST_UNKNOWN: + return "ESP_RST_UNKNOWN: Reset reason can not be determined."; + case esp_reset_reason_t::ESP_RST_POWERON: + return "ESP_RST_POWERON: Reset due to power-on event."; + case esp_reset_reason_t::ESP_RST_EXT: + return "ESP_RST_EXT: Reset by external pin"; + case esp_reset_reason_t::ESP_RST_SW: + return "ESP_RST_SW: Software reset via esp_restart."; + case esp_reset_reason_t::ESP_RST_PANIC: + return "ESP_RST_PANIC: Software reset due to exception/panic."; + case esp_reset_reason_t::ESP_RST_INT_WDT: + return "ESP_RST_INT_WDT: Reset (software or hardware) due to interrupt watchdog"; + case esp_reset_reason_t::ESP_RST_TASK_WDT: + return "ESP_RST_TASK_WDT: Reset due to task watchdog."; + case esp_reset_reason_t::ESP_RST_WDT: + return "ESP_RST_WDT: Reset due to other watchdogs."; + case esp_reset_reason_t::ESP_RST_DEEPSLEEP: + return "ESP_RST_DEEPSLEEP: Reset after exiting deep sleep mode."; + case esp_reset_reason_t::ESP_RST_BROWNOUT: + return "ESP_RST_BROWNOUT: Brownout reset (software or hardware)"; + case esp_reset_reason_t::ESP_RST_SDIO: + return "ESP_RST_SDIO: Reset over SDIO."; + default: + return "Unknown: " + (int)reason; + } +} \ No newline at end of file diff --git a/WebCfgServer.cpp b/WebCfgServer.cpp index c6ba8d1..e79a46b 100644 --- a/WebCfgServer.cpp +++ b/WebCfgServer.cpp @@ -4,6 +4,7 @@ #include "hardware/WifiEthServer.h" #include "Logger.h" #include "Config.h" +#include "RestartReason.h" #include WebCfgServer::WebCfgServer(NukiWrapper* nuki, NukiOpenerWrapper* nukiOpener, Network* network, EthServer* ethServer, Preferences* preferences, bool allowRestartToPortal) @@ -119,8 +120,8 @@ void WebCfgServer::initialize() return _server.requestAuthentication(); } String message = ""; - bool restartEsp = processArgs(message); - if(restartEsp) + bool restart = processArgs(message); + if(restart) { String response = ""; buildConfirmHtml(response, message); @@ -128,7 +129,7 @@ void WebCfgServer::initialize() Log->println(F("Restarting")); waitAndProcess(true, 1000); - ESP.restart(); + restartEsp(RestartReason::ConfigurationUpdated); } else { @@ -159,13 +160,43 @@ void WebCfgServer::initialize() handleOtaUpload(); }); + _server.on("/info", [&]() { + if (_hasCredentials && !_server.authenticate(_credUser, _credPassword)) { + return _server.requestAuthentication(); + } + String response = ""; + buildInfoHtml(response); + _server.send(200, "text/html", response); + }); + _server.on("/debugon", [&]() { + _preferences->putBool(preference_publish_debug_info, true); + + String response = ""; + buildConfirmHtml(response, "OK"); + _server.send(200, "text/html", response); + Log->println(F("Restarting")); + + waitAndProcess(true, 1000); + restartEsp(RestartReason::ConfigurationUpdated); + }); + _server.on("/debugoff", [&]() { + _preferences->putBool(preference_publish_debug_info, false); + + String response = ""; + buildConfirmHtml(response, "OK"); + _server.send(200, "text/html", response); + Log->println(F("Restarting")); + + waitAndProcess(true, 1000); + restartEsp(RestartReason::ConfigurationUpdated); + }); _server.begin(); _network->setKeepAliveCallback([&]() - { - update(); - }); + { + update(); + }); } bool WebCfgServer::processArgs(String& message) @@ -301,6 +332,11 @@ bool WebCfgServer::processArgs(String& message) _preferences->putInt(preference_query_interval_lockstate, value.toInt()); configChanged = true; } + else if(key == "CFGINT") + { + _preferences->putInt(preference_query_interval_configuration, value.toInt()); + configChanged = true; + } else if(key == "BATINT") { _preferences->putInt(preference_query_interval_battery, value.toInt()); @@ -445,7 +481,7 @@ void WebCfgServer::update() { Log->println(F("OTA time out, restarting")); delay(200); - ESP.restart(); + restartEsp(RestartReason::OTATimeout); } if(!_enabled) return; @@ -479,7 +515,7 @@ void WebCfgServer::buildHtml(String& response) printParameter(response, "NUKI Opener paired", _nukiOpener->isPaired() ? ("Yes (BLE Address " + _nukiOpener->getBleAddress().toString() + ")").c_str() : "No"); printParameter(response, "NUKI Opener state", lockstateArr); } - printParameter(response, "Firmware", version.c_str()); + printParameter(response, "Firmware", version.c_str(), "/info"); response.concat("

"); response.concat("

MQTT and Network Configuration

"); @@ -511,8 +547,8 @@ void WebCfgServer::buildCredHtml(String &response) response.concat("
"); response.concat("

Credentials

"); response.concat(""); - printInputField(response, "CREDUSER", "User (# to clear)", _preferences->getString(preference_cred_user).c_str(), 30); - printInputField(response, "CREDPASS", "Password (max 30 characters)", "*", 30, true); + printInputField(response, "CREDUSER", "User (# to clear)", _preferences->getString(preference_cred_user).c_str(), 30, false, true); + printInputField(response, "CREDPASS", "Password", "*", 30, true, true); printInputField(response, "CREDPASSRE", "Retype password", "*", 30, true); response.concat("
"); response.concat("
"); @@ -602,16 +638,16 @@ void WebCfgServer::buildMqttConfigHtml(String &response) printInputField(response, "HOSTNAME", "Host name", _preferences->getString(preference_hostname).c_str(), 100); printInputField(response, "MQTTSERVER", "MQTT Broker", _preferences->getString(preference_mqtt_broker).c_str(), 100); 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, "MQTTUSER", "MQTT User (# to clear)", _preferences->getString(preference_mqtt_user).c_str(), 30, false, true); + printInputField(response, "MQTTPASS", "MQTT Password", "*", 30, true, true); response.concat("
"); response.concat("

Advanced MQTT and Network Configuration

"); response.concat(""); printInputField(response, "HASSDISCOVERY", "Home Assistant discovery topic (empty to disable; usually homeassistant)", _preferences->getString(preference_mqtt_hass_discovery).c_str(), 30); - printTextarea(response, "MQTTCA", "MQTT SSL CA Certificate (*, optional)", _preferences->getString(preference_mqtt_ca).c_str(), TLS_CA_MAX_SIZE, _network->encryptionSupported()); - printTextarea(response, "MQTTCRT", "MQTT SSL Client Certificate (*, optional)", _preferences->getString(preference_mqtt_crt).c_str(), TLS_CERT_MAX_SIZE, _network->encryptionSupported()); - printTextarea(response, "MQTTKEY", "MQTT SSL Client Key (*, optional)", _preferences->getString(preference_mqtt_key).c_str(), TLS_KEY_MAX_SIZE, _network->encryptionSupported()); + printTextarea(response, "MQTTCA", "MQTT SSL CA Certificate (*, optional)", _preferences->getString(preference_mqtt_ca).c_str(), TLS_CA_MAX_SIZE, _network->encryptionSupported(), true); + printTextarea(response, "MQTTCRT", "MQTT SSL Client Certificate (*, optional)", _preferences->getString(preference_mqtt_crt).c_str(), TLS_CERT_MAX_SIZE, _network->encryptionSupported(), true); + printTextarea(response, "MQTTKEY", "MQTT SSL Client Key (*, optional)", _preferences->getString(preference_mqtt_key).c_str(), TLS_KEY_MAX_SIZE, _network->encryptionSupported(), true); printDropDown(response, "NWHW", "Network hardware", String(_preferences->getInt(preference_network_hardware)), getNetworkDetectionOptions()); printDropDown(response, "NWHWDT", "Network hardware detection", String(_preferences->getInt(preference_network_hardware_gpio)), getNetworkGpioOptions()); printInputField(response, "RSSI", "RSSI Publish interval (seconds; -1 to disable)", _preferences->getInt(preference_rssi_publish_interval), 6); @@ -652,8 +688,9 @@ void WebCfgServer::buildNukiConfigHtml(String &response) printCheckBox(response, "REGAPP", "Register as app (on: register as app, off: register as bridge; needs re-pairing if changed)", _preferences->getBool(preference_register_as_app)); printInputField(response, "LSTINT", "Query interval lock state (seconds)", _preferences->getInt(preference_query_interval_lockstate), 10); + printInputField(response, "CFGINT", "Query interval configuration (seconds)", _preferences->getInt(preference_query_interval_configuration), 10); printInputField(response, "BATINT", "Query interval battery (seconds)", _preferences->getInt(preference_query_interval_battery), 10); - if(_nuki != nullptr && _nuki->hasKeypad()) + if((_nuki != nullptr && _nuki->hasKeypad()) || (_nukiOpener != nullptr && _nukiOpener->hasKeypad())) { printInputField(response, "KPINT", "Query interval keypad (seconds)", _preferences->getInt(preference_query_interval_keypad), 10); printCheckBox(response, "KPENA", "Enabled keypad control via MQTT", _preferences->getBool(preference_keypad_control_enabled)); @@ -698,6 +735,60 @@ void WebCfgServer::buildConfigureWifiHtml(String &response) response.concat(""); } +void WebCfgServer::buildInfoHtml(String &response) +{ + DebugPreferences debugPreferences; + + buildHtmlHeader(response); + response.concat("

System Information

");
+
+    response.concat("Firmware version: ");
+    response.concat(NUKI_HUB_VERSION);
+    response.concat("\n");
+
+    response.concat(debugPreferences.preferencesToString(_preferences));
+
+    response.concat("MQTT connected: ");
+    response.concat(_network->mqttConnectionState() > 0 ? "Yes\n" : "No\n");
+
+    if(_nuki != nullptr)
+    {
+        response.concat("Lock paired: ");
+        response.concat(_nuki->isPaired() ? "Yes\n" : "No\n");
+        response.concat("Lock PIN set: ");
+        response.concat(_nuki->isPaired() ? _nuki->isPinSet() ? "Yes\n" : "No\n" : "-\n");
+    }
+    if(_nukiOpener != nullptr)
+    {
+        response.concat("Opener paired: ");
+        response.concat(_nukiOpener->isPaired() ? "Yes\n" : "No\n");
+        response.concat("Opener PIN set: ");
+        response.concat(_nukiOpener->isPaired() ? _nukiOpener->isPinSet() ? "Yes\n" : "No\n" : "-\n");
+    }
+
+    response.concat("Network device: ");
+    response.concat(_network->networkDeviceName());
+    response.concat("\n");
+
+    response.concat("Uptime: ");
+    response.concat(millis() / 1000 / 60);
+    response.concat(" minutes\n");
+
+    response.concat("Heap: ");
+    response.concat(esp_get_free_heap_size());
+    response.concat("\n");
+
+    response.concat("Restart reason FW: ");
+    response.concat(getRestartReason());
+    response.concat("\n");
+
+    response.concat("Restart reason ESP: ");
+    response.concat(getEspRestartReason());
+    response.concat("\n");
+
+    response.concat("
"); +} + void WebCfgServer::processUnpair(bool opener) { String response = ""; @@ -733,7 +824,7 @@ void WebCfgServer::processUnpair(bool opener) _nukiOpener->unpair(); } waitAndProcess(false, 1000); - ESP.restart(); + restartEsp(RestartReason::DeviceUnpaired); } void WebCfgServer::buildHtmlHeader(String &response) @@ -751,8 +842,9 @@ void WebCfgServer::printInputField(String& response, const char *token, const char *description, const char *value, - const size_t maxLength, - const bool isPassword) + const size_t& maxLength, + const bool& isPassword, + const bool& showLengthRestriction) { char maxLengthStr[20]; @@ -760,6 +852,14 @@ void WebCfgServer::printInputField(String& response, response.concat("
"); response.concat(description); + + if(showLengthRestriction) + { + response.concat(" (Max. "); + response.concat(maxLength); + response.concat(" characters)"); + } + response.concat(""); response.concat(""); response.concat(description); + if(showLengthRestriction) + { + response.concat(" (Max. "); + response.concat(maxLength); + response.concat(" characters)"); + } response.concat(""); response.concat("