diff --git a/CMakeLists.txt b/CMakeLists.txt index fd4e1c1..2b57b64 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -7,7 +7,7 @@ project(nuki_hub CXX) # ARDUHAL_LOG_LEVEL_NONE, define ARDUHAL_LOG_LEVEL_ERROR, define ARDUHAL_LOG_LEVEL_WARN, define ARDUHAL_LOG_LEVEL_INFO, # define ARDUHAL_LOG_LEVEL_DEBUG, define ARDUHAL_LOG_LEVEL_VERBOSE -set(LOG_LEVEL ARDUHAL_LOG_LEVEL_NONE) +set(LOG_LEVEL ARDUHAL_LOG_LEVEL_DEBUG) include_directories(${PROJECT_NAME} PRIVATE @@ -25,10 +25,12 @@ include_directories(${PROJECT_NAME} file(GLOB SRCFILES Pins.h Network.cpp + NetworkOpener.cpp networkDevices/NetworkDevice.h networkDevices/WifiDevice.cpp networkDevices/W5500Device.cpp NukiWrapper.cpp + NukiOpenerWrapper.cpp MqttTopics.h WebCfgServer.cpp PresenceDetection.cpp @@ -41,9 +43,16 @@ file(GLOB SRCFILES lib/WiFiManager/WiFiManager.cpp lib/Crc16/Crc16.h lib/nuki_ble/src/NukiBle.cpp + lib/nuki_ble/src/NukiBle.hpp + lib/nuki_ble/src/NukiLock.cpp + lib/nuki_ble/src/NukiOpener.cpp lib/nuki_ble/src/NukiConstants.h + lib/nuki_ble/src/NukiOpenerConstants.h + lib/nuki_ble/src/NukiLockConstants.h lib/nuki_ble/src/NukiDataTypes.h lib/nuki_ble/src/NukiUtils.cpp + lib/nuki_ble/src/NukiLockUtils.cpp + lib/nuki_ble/src/NukiOpenerUtils.cpp lib/BleScanner/src/BleInterfaces.h lib/BleScanner/src/BleScanner.cpp lib/pubsubclient/src/PubSubClient.cpp @@ -61,7 +70,7 @@ add_executable(${PROJECT_NAME} main.cpp ${SRCFILES} ${SRCFILESREC} -) + ) target_compile_definitions(${PROJECT_NAME} PRIVATE diff --git a/Network.cpp b/Network.cpp index 9a28484..31af0ee 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,7 @@ void Network::initialize() else { strcpy(_mqttPath, "nuki"); - _preferences->putString(preference_mqtt_path, _mqttPath); + _preferences->putString(preference_mqtt_lock_path, _mqttPath); } String mqttUser = _preferences->getString(preference_mqtt_user); @@ -205,7 +205,7 @@ void Network::update() if(_presenceCsv != nullptr && strlen(_presenceCsv) > 0) { - publishString(mqtt_topic_presence, _presenceCsv); + publishString_P(mqtt_topic_presence, _presenceCsv); _presenceCsv = nullptr; } @@ -251,13 +251,18 @@ void Network::onMqttDataReceived(char *&topic, byte *&payload, unsigned int &len } } } + + if(_mqttTopicReceivedForwardCallback != nullptr) + { + _mqttTopicReceivedForwardCallback(topic, payload, length); + } } -void Network::publishKeyTurnerState(const Nuki::KeyTurnerState& keyTurnerState, const Nuki::KeyTurnerState& lastKeyTurnerState) +void Network::publishKeyTurnerState(const NukiLock::KeyTurnerState& keyTurnerState, const NukiLock::KeyTurnerState& lastKeyTurnerState) { char str[50]; - if((_firstTunerStatePublish || keyTurnerState.lockState != lastKeyTurnerState.lockState) && keyTurnerState.lockState != Nuki::LockState::Undefined) + if((_firstTunerStatePublish || keyTurnerState.lockState != lastKeyTurnerState.lockState) && keyTurnerState.lockState != NukiLock::LockState::Undefined) { memset(&str, 0, sizeof(str)); lockstateToString(keyTurnerState.lockState, str); @@ -274,25 +279,21 @@ void Network::publishKeyTurnerState(const Nuki::KeyTurnerState& keyTurnerState, if(_firstTunerStatePublish || keyTurnerState.lastLockActionCompletionStatus != lastKeyTurnerState.lastLockActionCompletionStatus) { memset(&str, 0, sizeof(str)); - completionStatusToString(keyTurnerState.lastLockActionCompletionStatus, str); + NukiLock::completionStatusToString(keyTurnerState.lastLockActionCompletionStatus, str); publishString(mqtt_topic_lock_completionStatus, str); } if(_firstTunerStatePublish || keyTurnerState.doorSensorState != lastKeyTurnerState.doorSensorState) { memset(&str, 0, sizeof(str)); - doorSensorStateToString(keyTurnerState.doorSensorState, str); + NukiLock::doorSensorStateToString(keyTurnerState.doorSensorState, str); publishString(mqtt_topic_door_sensor_state, str); } if(_firstTunerStatePublish || keyTurnerState.criticalBatteryState != lastKeyTurnerState.criticalBatteryState) { - uint8_t level = (keyTurnerState.criticalBatteryState & 0b11111100) >> 1; bool critical = (keyTurnerState.criticalBatteryState & 0b00000001) > 0; - bool charging = (keyTurnerState.criticalBatteryState & 0b00000010) > 0; - publishInt(mqtt_topic_battery_level, level); // percent publishBool(mqtt_topic_battery_critical, critical); - publishBool(mqtt_topic_battery_charging, charging); } _firstTunerStatePublish = false; @@ -309,7 +310,7 @@ void Network::publishCommandResult(const char *resultStr) publishString(mqtt_topic_lock_action_command_result, resultStr); } -void Network::publishBatteryReport(const Nuki::BatteryReport& batteryReport) +void Network::publishBatteryReport(const NukiLock::BatteryReport& batteryReport) { publishFloat(mqtt_topic_battery_voltage, (float)batteryReport.batteryVoltage / 1000.0); publishInt(mqtt_topic_battery_drain, batteryReport.batteryDrain); // milliwatt seconds @@ -317,14 +318,14 @@ void Network::publishBatteryReport(const Nuki::BatteryReport& batteryReport) publishInt(mqtt_topic_battery_lock_distance, batteryReport.lockDistance); // degrees } -void Network::publishConfig(const Nuki::Config &config) +void Network::publishConfig(const NukiLock::Config &config) { publishBool(mqtt_topic_config_button_enabled, config.buttonEnabled == 1); publishBool(mqtt_topic_config_led_enabled, config.ledEnabled == 1); publishInt(mqtt_topic_config_led_brightness, config.ledBrightness); } -void Network::publishAdvancedConfig(const Nuki::AdvancedConfig &config) +void Network::publishAdvancedConfig(const NukiLock::AdvancedConfig &config) { publishBool(mqtt_topic_config_auto_unlock, config.autoUnLockDisabled == 0); publishBool(mqtt_topic_config_auto_lock, config.autoLockEnabled == 1); @@ -345,6 +346,11 @@ void Network::setConfigUpdateReceivedCallback(void (*configUpdateReceivedCallbac _configUpdateReceivedCallback = configUpdateReceivedCallback; } +void Network::setMqttDataReceivedForwardCallback(void (*callback)(char *, uint8_t *, unsigned int)) +{ + _mqttTopicReceivedForwardCallback = callback; +} + void Network::publishFloat(const char* topic, const float value, const uint8_t precision) { char str[30]; @@ -388,6 +394,12 @@ void Network::publishString(const char *topic, const char *value) _device->mqttClient()->publish(path, value); } +void Network::publishString_P(const char *topic, const char *value) +{ + char path[200] = {0}; + buildMqttPath(topic, path); + _device->mqttClient()->publish_P(path, value, true); +} bool Network::isMqttConnected() { @@ -434,3 +446,8 @@ bool Network::comparePrefixedPath(const char *fullPath, const char *subPath) buildMqttPath(subPath, prefixedPath); return strcmp(fullPath, prefixedPath) == 0; } + +NetworkDevice *Network::device() +{ + return _device; +} diff --git a/Network.h b/Network.h index 5d9e49d..05c4d8c 100644 --- a/Network.h +++ b/Network.h @@ -8,6 +8,7 @@ #include #include "NukiConstants.h" #include "SpiffsCookie.h" +#include "NukiLockConstants.h" enum class NetworkDeviceType { @@ -28,19 +29,22 @@ public: bool isMqttConnected(); - void publishKeyTurnerState(const Nuki::KeyTurnerState& keyTurnerState, const Nuki::KeyTurnerState& lastKeyTurnerState); + void publishKeyTurnerState(const NukiLock::KeyTurnerState& keyTurnerState, const NukiLock::KeyTurnerState& lastKeyTurnerState); void publishAuthorizationInfo(const uint32_t authId, const char* authName); void publishCommandResult(const char* resultStr); - void publishBatteryReport(const Nuki::BatteryReport& batteryReport); - void publishConfig(const Nuki::Config& config); - void publishAdvancedConfig(const Nuki::AdvancedConfig& config); + void publishBatteryReport(const NukiLock::BatteryReport& batteryReport); + void publishConfig(const NukiLock::Config& config); + void publishAdvancedConfig(const NukiLock::AdvancedConfig& config); void publishPresenceDetection(char* csv); void setLockActionReceivedCallback(bool (*lockActionReceivedCallback)(const char* value)); void setConfigUpdateReceivedCallback(void (*configUpdateReceivedCallback)(const char* path, const char* value)); + void setMqttDataReceivedForwardCallback(void (*callback)(char*, uint8_t*, unsigned int)); void restartAndConfigureWifi(); + NetworkDevice* device(); + private: static void onMqttDataReceivedCallback(char* topic, byte* payload, unsigned int length); void onMqttDataReceived(char*& topic, byte*& payload, unsigned int& length); @@ -51,6 +55,7 @@ private: void publishUInt(const char* topic, const unsigned int value); void publishBool(const char* topic, const bool value); void publishString(const char* topic, const char* value); + void publishString_P(const char* topic, const char* value); void buildMqttPath(const char* path, char* outPath); void subscribe(const char* path); @@ -80,4 +85,5 @@ private: bool (*_lockActionReceivedCallback)(const char* value) = nullptr; void (*_configUpdateReceivedCallback)(const char* path, const char* value) = nullptr; + void (*_mqttTopicReceivedForwardCallback)(char*, uint8_t*, unsigned int) = nullptr; }; diff --git a/NetworkOpener.cpp b/NetworkOpener.cpp new file mode 100644 index 0000000..d2bf43e --- /dev/null +++ b/NetworkOpener.cpp @@ -0,0 +1,256 @@ +#include "NetworkOpener.h" +#include // https://github.com/tzapu/WiFiManager +#include "Arduino.h" +#include "MqttTopics.h" +#include "PreferencesKeys.h" +#include "Pins.h" + +NetworkOpener* nwInstOpener; + +NetworkOpener::NetworkOpener(Network* network, Preferences* preferences) + : _preferences(preferences), + _network(network) +{ + nwInstOpener = this; + + _configTopics.reserve(5); +// _configTopics.push_back(mqtt_topic_config_button_enabled); +// _configTopics.push_back(mqtt_topic_config_led_enabled); +// _configTopics.push_back(mqtt_topic_config_led_brightness); +// _configTopics.push_back(mqtt_topic_config_auto_unlock); +// _configTopics.push_back(mqtt_topic_config_auto_lock); +} + +void NetworkOpener::initialize() +{ + String mqttPath = _preferences->getString(preference_mqtt_opener_path); + if(mqttPath.length() > 0) + { + size_t len = mqttPath.length(); + for(int i=0; i < len; i++) + { + _mqttPath[i] = mqttPath.charAt(i); + } + } + else + { + strcpy(_mqttPath, "nukiopener"); + _preferences->putString(preference_mqtt_opener_path, _mqttPath); + } + + _network->setMqttDataReceivedForwardCallback(nwInstOpener->onMqttDataReceivedCallback); +} + +void NetworkOpener::update() +{ + bool connected = _network->device()->mqttClient()->connected(); + + if(!_isConnected && connected) + { + subscribe(mqtt_topic_lock_action); + } + + _isConnected = connected; + +// long ts = millis(); + +} + +void NetworkOpener::onMqttDataReceivedCallback(char *topic, byte *payload, unsigned int length) +{ + nwInstOpener->onMqttDataReceived(topic, payload, length); +} + +void NetworkOpener::onMqttDataReceived(char *&topic, byte *&payload, unsigned int &length) +{ + char value[50] = {0}; + size_t l = min(length, sizeof(value)-1); + + for(int i=0; i 0; + publishBool(mqtt_topic_battery_critical, critical); + } + + _firstTunerStatePublish = false; +} + +void NetworkOpener::publishAuthorizationInfo(const uint32_t authId, const char *authName) +{ + publishUInt(mqtt_topic_lock_auth_id, authId); + publishString(mqtt_topic_lock_auth_name, authName); +} + +void NetworkOpener::publishCommandResult(const char *resultStr) +{ + publishString(mqtt_topic_lock_action_command_result, resultStr); +} + +void NetworkOpener::publishBatteryReport(const NukiOpener::BatteryReport& batteryReport) +{ + publishFloat(mqtt_topic_battery_voltage, (float)batteryReport.batteryVoltage / 1000.0); +} + +void NetworkOpener::publishConfig(const NukiOpener::Config &config) +{ + publishBool(mqtt_topic_config_button_enabled, config.buttonEnabled == 1); + publishBool(mqtt_topic_config_led_enabled, config.ledFlashEnabled == 1); +} + +void NetworkOpener::publishAdvancedConfig(const NukiOpener::AdvancedConfig &config) +{ +// publishBool(mqtt_topic_config_auto_unlock, config.autoUnLockDisabled == 0); +// publishBool(mqtt_topic_config_auto_lock, config.autoLockEnabled == 1); +} + +void NetworkOpener::setLockActionReceivedCallback(bool (*lockActionReceivedCallback)(const char *)) +{ + _lockActionReceivedCallback = lockActionReceivedCallback; +} + +void NetworkOpener::setConfigUpdateReceivedCallback(void (*configUpdateReceivedCallback)(const char *, const char *)) +{ + _configUpdateReceivedCallback = configUpdateReceivedCallback; +} + +void NetworkOpener::publishFloat(const char* topic, const float value, const uint8_t precision) +{ + char str[30]; + dtostrf(value, 0, precision, str); + char path[200] = {0}; + buildMqttPath(topic, path); + _network->device()->mqttClient()->publish(path, str); +} + +void NetworkOpener::publishInt(const char *topic, const int value) +{ + char str[30]; + itoa(value, str, 10); + char path[200] = {0}; + buildMqttPath(topic, path); + _network->device()->mqttClient()->publish(path, str); +} + +void NetworkOpener::publishUInt(const char *topic, const unsigned int value) +{ + char str[30]; + utoa(value, str, 10); + char path[200] = {0}; + buildMqttPath(topic, path); + _network->device()->mqttClient()->publish(path, str); +} + +void NetworkOpener::publishBool(const char *topic, const bool value) +{ + char str[2] = {0}; + str[0] = value ? '1' : '0'; + char path[200] = {0}; + buildMqttPath(topic, path); + _network->device()->mqttClient()->publish(path, str); +} + +void NetworkOpener::publishString(const char *topic, const char *value) +{ + char path[200] = {0}; + buildMqttPath(topic, path); + _network->device()->mqttClient()->publish(path, value); +} + +void NetworkOpener::buildMqttPath(const char* path, char* outPath) +{ + int offset = 0; + for(const char& c : _mqttPath) + { + if(c == 0x00) + { + break; + } + outPath[offset] = c; + ++offset; + } + int i=0; + while(outPath[i] != 0x00) + { + outPath[offset] = path[i]; + ++i; + ++offset; + } + outPath[i+1] = 0x00; +} + +void NetworkOpener::subscribe(const char *path) +{ + char prefixedPath[500]; + buildMqttPath(path, prefixedPath); + _network->device()->mqttClient()->subscribe(prefixedPath); +} + +bool NetworkOpener::comparePrefixedPath(const char *fullPath, const char *subPath) +{ + char prefixedPath[500]; + buildMqttPath(subPath, prefixedPath); + return strcmp(fullPath, prefixedPath) == 0; +} \ No newline at end of file diff --git a/NetworkOpener.h b/NetworkOpener.h new file mode 100644 index 0000000..5371ab7 --- /dev/null +++ b/NetworkOpener.h @@ -0,0 +1,60 @@ +#pragma once + +#include +#include "networkDevices/NetworkDevice.h" +#include "networkDevices/WifiDevice.h" +#include "networkDevices/W5500Device.h" +#include +#include +#include "NukiConstants.h" +#include "SpiffsCookie.h" +#include "NukiOpenerConstants.h" +#include "Network.h" + +class NetworkOpener +{ +public: + explicit NetworkOpener(Network* network, Preferences* preferences); + virtual ~NetworkOpener() = default; + + void initialize(); + void update(); + + void publishKeyTurnerState(const NukiOpener::OpenerState& keyTurnerState, const NukiOpener::OpenerState& lastKeyTurnerState); + void publishAuthorizationInfo(const uint32_t authId, const char* authName); + void publishCommandResult(const char* resultStr); + void publishBatteryReport(const NukiOpener::BatteryReport& batteryReport); + void publishConfig(const NukiOpener::Config& config); + void publishAdvancedConfig(const NukiOpener::AdvancedConfig& config); + + void setLockActionReceivedCallback(bool (*lockActionReceivedCallback)(const char* value)); + void setConfigUpdateReceivedCallback(void (*configUpdateReceivedCallback)(const char* path, const char* value)); + +private: + static void onMqttDataReceivedCallback(char* topic, byte* payload, unsigned int length); + void onMqttDataReceived(char*& topic, byte*& payload, unsigned int& length); + 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); + void publishBool(const char* topic, const bool value); + void publishString(const char* topic, const char* value); + + void buildMqttPath(const char* path, char* outPath); + void subscribe(const char* path); + + Preferences* _preferences; + + Network* _network = nullptr; + + char _mqttPath[181] = {0}; + bool _isConnected = false; + + std::vector _configTopics; + + bool _firstTunerStatePublish = true; + + bool (*_lockActionReceivedCallback)(const char* value) = nullptr; + void (*_configUpdateReceivedCallback)(const char* path, const char* value) = nullptr; +}; diff --git a/NukiOpenerWrapper.cpp b/NukiOpenerWrapper.cpp new file mode 100644 index 0000000..bf517c2 --- /dev/null +++ b/NukiOpenerWrapper.cpp @@ -0,0 +1,301 @@ +#include "NukiOpenerWrapper.h" +#include +#include "PreferencesKeys.h" +#include "MqttTopics.h" +#include + +NukiOpenerWrapper* nukiOpenerInst; + +NukiOpenerWrapper::NukiOpenerWrapper(const std::string& deviceName, uint32_t id, BleScanner::Scanner* scanner, NetworkOpener* network, Preferences* preferences) +: _deviceName(deviceName), + _nukiOpener(deviceName, id), + _bleScanner(scanner), + _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() +{ + _bleScanner = nullptr; +} + + +void NukiOpenerWrapper::initialize() +{ + _nukiOpener.initialize(); + _nukiOpener.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); + } + + _nukiOpener.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 opener start pairing")); + + if (_nukiOpener.pairNuki() == NukiOpener::PairingResult::Success) { + Serial.println(F("Nuki opener paired")); + _paired = true; + } + else + { + vTaskDelay( 200 / portTICK_PERIOD_MS); + return; + } + } + + _nukiOpener.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 = _nukiOpener.lockAction(_nextLockAction, 0, 0); + + char resultStr[15] = {0}; + NukiOpener::cmdResultToString(cmdResult, resultStr); + + _network->publishCommandResult(resultStr); + + Serial.print(F("Opener 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(NukiOpener::OpenerState)); +} + +void NukiOpenerWrapper::setPin(const uint16_t pin) +{ + _nukiOpener.saveSecurityPincode(pin); +} + +void NukiOpenerWrapper::unpair() +{ + _nukiOpener.unPairNuki(); + _paired = false; +} + +void NukiOpenerWrapper::updateKeyTurnerState() +{ + _nukiOpener.requestOpenerState(&_keyTurnerState); + _network->publishKeyTurnerState(_keyTurnerState, _lastKeyTurnerState); + + if(_keyTurnerState.lockState != _lastKeyTurnerState.lockState) + { + char lockStateStr[20]; + lockstateToString(_keyTurnerState.lockState, lockStateStr); + Serial.print(F("Nuki opener state: ")); + Serial.println((int)_keyTurnerState.lockState); +// Serial.println((int)_keyTurnerState.nukiState); +// Serial.println((int)_keyTurnerState.currentTimeYear); +// Serial.println((int)_keyTurnerState.ringToOpenTimer); + } + + if(_publishAuthData) + { + updateAuthData(); + } +} + +void NukiOpenerWrapper::updateBatteryState() +{ + _nukiOpener.requestBatteryReport(&_batteryReport); + _network->publishBatteryReport(_batteryReport); +} + +void NukiOpenerWrapper::updateConfig() +{ + readConfig(); + readAdvancedConfig(); + _network->publishConfig(_nukiConfig); + _network->publishAdvancedConfig(_nukiAdvancedConfig); +} + +void NukiOpenerWrapper::updateAuthData() +{ + Nuki::CmdResult result = _nukiOpener.retrieveLogEntries(0, 0, 0, true); + if(result != Nuki::CmdResult::Success) + { + _network->publishAuthorizationInfo(0, ""); + return; + } + vTaskDelay( 100 / portTICK_PERIOD_MS); + + result = _nukiOpener.retrieveLogEntries(_nukiOpener.getLogEntryCount() - 2, 1, 0, false); + if(result != Nuki::CmdResult::Success) + { + _network->publishAuthorizationInfo(0, ""); + return; + } + vTaskDelay( 200 / portTICK_PERIOD_MS); + + std::list log; + _nukiOpener.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; + _nukiOpener.enableButton(newValue); + _nextConfigUpdateTs = millis() + 300; + } + if(strcmp(topic, mqtt_topic_config_led_enabled) == 0) + { + bool newValue = atoi(value) > 0; + if(!_nukiConfigValid || _nukiConfig.ledFlashEnabled == newValue) return; + _nukiOpener.enableLedFlash(newValue); + _nextConfigUpdateTs = millis() + 300; + } +} + +const NukiOpener::OpenerState &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 opener config. Result: ")); + Nuki::CmdResult result = _nukiOpener.requestConfig(&_nukiConfig); + _nukiConfigValid = result == Nuki::CmdResult::Success; + Serial.println(result); +} + +void NukiOpenerWrapper::readAdvancedConfig() +{ + Serial.print(F("Reading opener advanced config. Result: ")); + Nuki::CmdResult result = _nukiOpener.requestAdvancedConfig(&_nukiAdvancedConfig); + _nukiAdvancedConfigValid = result == Nuki::CmdResult::Success; + Serial.println(result); +} diff --git a/NukiOpenerWrapper.h b/NukiOpenerWrapper.h new file mode 100644 index 0000000..b76af46 --- /dev/null +++ b/NukiOpenerWrapper.h @@ -0,0 +1,75 @@ +#pragma once + +#include "NukiOpener.h" +#include "NetworkOpener.h" +#include "NukiOpenerConstants.h" +#include "NukiDataTypes.h" +#include "BleScanner.h" + +class NukiOpenerWrapper : public NukiOpener::SmartlockEventHandler +{ +public: + NukiOpenerWrapper(const std::string& deviceName, uint32_t id, BleScanner::Scanner* scanner, NetworkOpener* network, Preferences* preferences); + virtual ~NukiOpenerWrapper(); + + void initialize(); + void update(); + + void setPin(const uint16_t pin); + + void unpair(); + + const NukiOpener::OpenerState& 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 _nukiOpener; + BleScanner::Scanner* _bleScanner; + NetworkOpener* _network; + Preferences* _preferences; + int _intervalLockstate = 0; // seconds + int _intervalBattery = 0; // seconds + int _intervalConfig = 60 * 60; // seconds + bool _publishAuthData = false; + bool _clearAuthData = false; + + NukiOpener::OpenerState _lastKeyTurnerState; + NukiOpener::OpenerState _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/NukiWrapper.cpp b/NukiWrapper.cpp index 502fea4..b1e8493 100644 --- a/NukiWrapper.cpp +++ b/NukiWrapper.cpp @@ -2,23 +2,24 @@ #include #include "PreferencesKeys.h" #include "MqttTopics.h" -#include +#include NukiWrapper* nukiInst; -NukiWrapper::NukiWrapper(const std::string& deviceName, uint32_t id, Network* network, Preferences* preferences) +NukiWrapper::NukiWrapper(const std::string& deviceName, uint32_t id, BleScanner::Scanner* scanner, Network* network, Preferences* preferences) : _deviceName(deviceName), - _nukiBle(deviceName, id), + _bleScanner(scanner), + _nukiLock(deviceName, id), _network(network), _preferences(preferences) { nukiInst = this; - memset(&_lastKeyTurnerState, sizeof(Nuki::KeyTurnerState), 0); - memset(&_lastBatteryReport, sizeof(Nuki::BatteryReport), 0); - memset(&_batteryReport, sizeof(Nuki::BatteryReport), 0); - memset(&_keyTurnerState, sizeof(Nuki::KeyTurnerState), 0); - _keyTurnerState.lockState = Nuki::LockState::Undefined; + 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 = NukiLock::LockState::Undefined; network->setLockActionReceivedCallback(nukiInst->onLockActionReceivedCallback); network->setConfigUpdateReceivedCallback(nukiInst->onConfigUpdateReceivedCallback); @@ -27,18 +28,15 @@ NukiWrapper::NukiWrapper(const std::string& deviceName, uint32_t id, Network* ne NukiWrapper::~NukiWrapper() { - delete _bleScanner; _bleScanner = nullptr; } void NukiWrapper::initialize() { - _bleScanner = new BleScanner::Scanner(); - _bleScanner->initialize(_deviceName); - _bleScanner->setScanDuration(10); - _nukiBle.initialize(); - _nukiBle.registerBleScanner(_bleScanner); + + _nukiLock.initialize(); + _nukiLock.registerBleScanner(_bleScanner); _intervalLockstate = _preferences->getInt(preference_query_interval_lockstate); _intervalBattery = _preferences->getInt(preference_query_interval_battery); @@ -55,7 +53,7 @@ void NukiWrapper::initialize() _preferences->putInt(preference_query_interval_battery, _intervalBattery); } - _nukiBle.setEventHandler(this); + _nukiLock.setEventHandler(this); Serial.print(F("Lock state interval: ")); Serial.print(_intervalLockstate); @@ -75,9 +73,7 @@ void NukiWrapper::update() if (!_paired) { Serial.println(F("Nuki start pairing")); - _bleScanner->update(); - vTaskDelay( 5000 / portTICK_PERIOD_MS); - if (_nukiBle.pairNuki() == Nuki::PairingResult::Success) { + if (_nukiLock.pairNuki() == Nuki::PairingResult::Success) { Serial.println(F("Nuki paired")); _paired = true; } @@ -88,9 +84,7 @@ void NukiWrapper::update() } } - vTaskDelay( 20 / portTICK_PERIOD_MS); - _bleScanner->update(); - _nukiBle.updateConnectionState(); + _nukiLock.updateConnectionState(); unsigned long ts = millis(); @@ -111,19 +105,19 @@ void NukiWrapper::update() updateConfig(); } - if(_nextLockAction != (Nuki::LockAction)0xff) + if(_nextLockAction != (NukiLock::LockAction)0xff) { - Nuki::CmdResult cmdResult = _nukiBle.lockAction(_nextLockAction, 0, 0); + Nuki::CmdResult cmdResult = _nukiLock.lockAction(_nextLockAction, 0, 0); char resultStr[15] = {0}; - Nuki::cmdResultToString(cmdResult, resultStr); + NukiLock::cmdResultToString(cmdResult, resultStr); _network->publishCommandResult(resultStr); Serial.print(F("Lock action result: ")); Serial.println(resultStr); - _nextLockAction = (Nuki::LockAction)0xff; + _nextLockAction = (NukiLock::LockAction)0xff; if(_intervalLockstate > 10) { _nextLockStateUpdateTs = ts + 10 * 1000; @@ -136,23 +130,23 @@ void NukiWrapper::update() _clearAuthData = false; } - memcpy(&_lastKeyTurnerState, &_keyTurnerState, sizeof(Nuki::KeyTurnerState)); + memcpy(&_lastKeyTurnerState, &_keyTurnerState, sizeof(NukiLock::KeyTurnerState)); } void NukiWrapper::setPin(const uint16_t pin) { - _nukiBle.saveSecurityPincode(pin); + _nukiLock.saveSecurityPincode(pin); } void NukiWrapper::unpair() { - _nukiBle.unPairNuki(); + _nukiLock.unPairNuki(); _paired = false; } void NukiWrapper::updateKeyTurnerState() { - _nukiBle.requestKeyTurnerState(&_keyTurnerState); + _nukiLock.requestKeyTurnerState(&_keyTurnerState); _network->publishKeyTurnerState(_keyTurnerState, _lastKeyTurnerState); if(_keyTurnerState.lockState != _lastKeyTurnerState.lockState) @@ -171,7 +165,7 @@ void NukiWrapper::updateKeyTurnerState() void NukiWrapper::updateBatteryState() { - _nukiBle.requestBatteryReport(&_batteryReport); + _nukiLock.requestBatteryReport(&_batteryReport); _network->publishBatteryReport(_batteryReport); } @@ -185,7 +179,7 @@ void NukiWrapper::updateConfig() void NukiWrapper::updateAuthData() { - Nuki::CmdResult result = _nukiBle.retrieveLogEntries(0, 0, 0, true); + Nuki::CmdResult result = _nukiLock.retrieveLogEntries(0, 0, 0, true); if(result != Nuki::CmdResult::Success) { _network->publishAuthorizationInfo(0, ""); @@ -193,7 +187,7 @@ void NukiWrapper::updateAuthData() } vTaskDelay( 100 / portTICK_PERIOD_MS); - result = _nukiBle.retrieveLogEntries(_nukiBle.getLogEntryCount() - 2, 1, 0, false); + result = _nukiLock.retrieveLogEntries(_nukiLock.getLogEntryCount() - 2, 1, 0, false); if(result != Nuki::CmdResult::Success) { _network->publishAuthorizationInfo(0, ""); @@ -202,7 +196,7 @@ void NukiWrapper::updateAuthData() vTaskDelay( 200 / portTICK_PERIOD_MS); std::list log; - _nukiBle.getLogEntries(&log); + _nukiLock.getLogEntries(&log); if(log.size() > 0) { @@ -221,23 +215,23 @@ void NukiWrapper::updateAuthData() } } -Nuki::LockAction NukiWrapper::lockActionToEnum(const char *str) +NukiLock::LockAction NukiWrapper::lockActionToEnum(const char *str) { - if(strcmp(str, "unlock") == 0) return Nuki::LockAction::Unlock; - else if(strcmp(str, "lock") == 0) return Nuki::LockAction::Lock; - else if(strcmp(str, "unlatch") == 0) return Nuki::LockAction::Unlatch; - else if(strcmp(str, "lockNgo") == 0) return Nuki::LockAction::LockNgo; - else if(strcmp(str, "lockNgoUnlatch") == 0) return Nuki::LockAction::LockNgoUnlatch; - else if(strcmp(str, "fullLock") == 0) return Nuki::LockAction::FullLock; - else if(strcmp(str, "fobAction2") == 0) return Nuki::LockAction::FobAction2; - else if(strcmp(str, "fobAction1") == 0) return Nuki::LockAction::FobAction1; - else if(strcmp(str, "fobAction3") == 0) return Nuki::LockAction::FobAction3; - return (Nuki::LockAction)0xff; + 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; + return (NukiLock::LockAction)0xff; } bool NukiWrapper::onLockActionReceivedCallback(const char *value) { - Nuki::LockAction action = nukiInst->lockActionToEnum(value); + NukiLock::LockAction action = nukiInst->lockActionToEnum(value); nukiInst->_nextLockAction = action; return (int)action != 0xff; } @@ -254,47 +248,47 @@ void NukiWrapper::onConfigUpdateReceived(const char *topic, const char *value) { bool newValue = atoi(value) > 0; if(!_nukiConfigValid || _nukiConfig.buttonEnabled == newValue) return; - _nukiBle.enableButton(newValue); + _nukiLock.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); + _nukiLock.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); + _nukiLock.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); + _nukiLock.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); + _nukiLock.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); + _nukiLock.enableAutoLock(newValue); _nextConfigUpdateTs = millis() + 300; } } -const Nuki::KeyTurnerState &NukiWrapper::keyTurnerState() +const NukiLock::KeyTurnerState &NukiWrapper::keyTurnerState() { return _keyTurnerState; } @@ -304,11 +298,6 @@ const bool NukiWrapper::isPaired() return _paired; } -BleScanner::Scanner *NukiWrapper::bleScanner() -{ - return _bleScanner; -} - void NukiWrapper::notify(Nuki::EventType eventType) { if(eventType == Nuki::EventType::KeyTurnerStatusUpdated) @@ -320,7 +309,7 @@ void NukiWrapper::notify(Nuki::EventType eventType) void NukiWrapper::readConfig() { Serial.print(F("Reading config. Result: ")); - Nuki::CmdResult result = _nukiBle.requestConfig(&_nukiConfig); + Nuki::CmdResult result = _nukiLock.requestConfig(&_nukiConfig); _nukiConfigValid = result == Nuki::CmdResult::Success; Serial.println(result); } @@ -328,7 +317,7 @@ void NukiWrapper::readConfig() void NukiWrapper::readAdvancedConfig() { Serial.print(F("Reading advanced config. Result: ")); - Nuki::CmdResult result = _nukiBle.requestAdvancedConfig(&_nukiAdvancedConfig); + Nuki::CmdResult result = _nukiLock.requestAdvancedConfig(&_nukiAdvancedConfig); _nukiAdvancedConfigValid = result == Nuki::CmdResult::Success; Serial.println(result); } diff --git a/NukiWrapper.h b/NukiWrapper.h index a62376b..48110cd 100644 --- a/NukiWrapper.h +++ b/NukiWrapper.h @@ -1,15 +1,15 @@ #pragma once -#include "NukiBle.h" #include "Network.h" #include "NukiConstants.h" #include "NukiDataTypes.h" #include "BleScanner.h" +#include "NukiLock.h" class NukiWrapper : public Nuki::SmartlockEventHandler { public: - NukiWrapper(const std::string& deviceName, uint32_t id, Network* network, Preferences* preferences); + NukiWrapper(const std::string& deviceName, uint32_t id, BleScanner::Scanner* scanner, Network* network, Preferences* preferences); virtual ~NukiWrapper(); void initialize(); @@ -19,11 +19,9 @@ public: void unpair(); - const Nuki::KeyTurnerState& keyTurnerState(); + const NukiLock::KeyTurnerState& keyTurnerState(); const bool isPaired(); - BleScanner::Scanner* bleScanner(); - void notify(Nuki::EventType eventType) override; private: @@ -39,10 +37,10 @@ private: void readConfig(); void readAdvancedConfig(); - Nuki::LockAction lockActionToEnum(const char* str); // char array at least 14 characters + NukiLock::LockAction lockActionToEnum(const char* str); // char array at least 14 characters std::string _deviceName; - Nuki::NukiBle _nukiBle; + NukiLock::NukiLock _nukiLock; BleScanner::Scanner* _bleScanner; Network* _network; Preferences* _preferences; @@ -52,16 +50,16 @@ private: bool _publishAuthData = false; bool _clearAuthData = false; - Nuki::KeyTurnerState _lastKeyTurnerState; - Nuki::KeyTurnerState _keyTurnerState; + NukiLock::KeyTurnerState _lastKeyTurnerState; + NukiLock::KeyTurnerState _keyTurnerState; uint32_t _lastAuthId = 0xffff; - Nuki::BatteryReport _batteryReport; - Nuki::BatteryReport _lastBatteryReport; + NukiLock::BatteryReport _batteryReport; + NukiLock::BatteryReport _lastBatteryReport; - Nuki::Config _nukiConfig = {0}; - Nuki::AdvancedConfig _nukiAdvancedConfig = {0}; + NukiLock::Config _nukiConfig = {0}; + NukiLock::AdvancedConfig _nukiAdvancedConfig = {0}; bool _nukiConfigValid = false; bool _nukiAdvancedConfigValid = false; @@ -71,5 +69,5 @@ private: unsigned long _nextBatteryReportTs = 0; unsigned long _nextConfigUpdateTs = 0; unsigned long _nextPairTs = 0; - Nuki::LockAction _nextLockAction = (Nuki::LockAction)0xff; + NukiLock::LockAction _nextLockAction = (NukiLock::LockAction)0xff; }; diff --git a/PreferencesKeys.h b/PreferencesKeys.h index 6938637..8e48fc1 100644 --- a/PreferencesKeys.h +++ b/PreferencesKeys.h @@ -1,11 +1,15 @@ #pragma once +#define preference_started_befores "run" #define preference_deviceId "deviceId" #define preference_mqtt_broker "mqttbroker" #define preference_mqtt_broker_port "mqttport" #define preference_mqtt_user "mqttuser" #define preference_mqtt_password "mqttpass" -#define preference_mqtt_path "mqttpath" +#define preference_lock_enabled "lockena" +#define preference_mqtt_lock_path "mqttpath" +#define preference_opener_enabled "openerena" +#define preference_mqtt_opener_path "mqttoppath" #define preference_hostname "hostname" #define preference_network_timeout "nettmout" #define preference_query_interval_lockstate "lockStInterval" diff --git a/PresenceDetection.cpp b/PresenceDetection.cpp index c384433..681bcc0 100644 --- a/PresenceDetection.cpp +++ b/PresenceDetection.cpp @@ -37,7 +37,7 @@ void PresenceDetection::initialize() void PresenceDetection::update() { - vTaskDelay( 5000 / portTICK_PERIOD_MS); + delay(3000); if(_timeout < 0) return; if(_devices.size() == 0) @@ -65,6 +65,8 @@ void PresenceDetection::update() _csv[_csvIndex-1] = 0x00; +// Serial.print("Devices found: "); +// Serial.println(_devices.size()); _network->publishPresenceDetection(_csv); } @@ -108,12 +110,13 @@ void PresenceDetection::buildCsv(const PdDevice &device) _csvIndex++; } - void PresenceDetection::onResult(NimBLEAdvertisedDevice *device) { std::string addressStr = device->getAddress().toString(); char addrArrComp[13] = {0}; +// Serial.println(addressStr.c_str()); + addrArrComp[0] = addressStr.at(0); addrArrComp[1] = addressStr.at(1); addrArrComp[2] = addressStr.at(3); @@ -132,6 +135,7 @@ void PresenceDetection::onResult(NimBLEAdvertisedDevice *device) auto it = _devices.find(addr); if(it == _devices.end()) { + PdDevice pdDevice; int i=0; @@ -142,12 +146,19 @@ void PresenceDetection::onResult(NimBLEAdvertisedDevice *device) ++i; } + if(device->haveRSSI()) + { + pdDevice.hasRssi = true; + pdDevice.rssi = device->getRSSI(); + } + + std::string nameStr = "-"; if(device->haveName()) { std::string nameStr = device->getName(); - int i=0; - size_t len = nameStr.length(); + i=0; + len = nameStr.length(); while(i < len && i < sizeof(pdDevice.name)-1) { pdDevice.name[i] = nameStr.at(i); @@ -158,12 +169,6 @@ void PresenceDetection::onResult(NimBLEAdvertisedDevice *device) _devices[addr] = pdDevice; } - - if(device->haveRSSI()) - { - pdDevice.hasRssi = true; - pdDevice.rssi = device->getRSSI(); - } } else { diff --git a/README.md b/README.md index 861f7a8..ffed63f 100644 --- a/README.md +++ b/README.md @@ -26,6 +26,8 @@ Just enable pairing mode on the NUKI lock (press button for a few seconds) and p ## MQTT Interface +### Lock + - lock/action: Allows to execute lock actions. After receiving the action, the value is set to "ack". Possible actions: unlock, lock, unlatch, lockNgo, lockNgoUnlatch, fullLock, fobAction1, fobAction2, fobAction3 - lock/state: Reports the current lock state as a string. Possible values are: uncalibrated, locked, unlocked, unlatched, unlockedLnga, unlatching, bootRun, motorBlocked - lock/trigger: The trigger of the last action: autoLock, automatic, button, manual, system @@ -35,7 +37,6 @@ Just enable pairing mode on the NUKI lock (press button for a few seconds) and p - lock/commandResult: Result of the last action as reported by NUKI library: success, failed, timeOut, working, notPaired, error, undefined - lock/doorSensorState: State of the door sensor: unavailable, deactivated, doorClosed, doorOpened, doorStateUnknown, calibrating -- battery/voltage: Reports the current battery voltage in Volts. - battery/level: Battery level in percent - battery/critical: 1 if battery level is critical, otherwise 0 - battery/charging: 1 if charging, otherwise 0 @@ -43,6 +44,21 @@ Just enable pairing mode on the NUKI lock (press button for a few seconds) and p - battery/drain: The drain of the last lock action in Milliwattseconds (mWs) - battery/maxTurnCurrent: The highest current of the turn motor during the last lock action (A) +### Opener + +- lock/action: Allows to execute lock actions. After receiving the action, the value is set to "ack". Possible actions: activateRTO, deactivateRTO, electricStrikeActuation, activateCM, deactivateCM, fobAction1, fobAction2, fobAction3 +- lock/state: Reports the current lock state as a string. Possible values are: locked, RTOactive, open, opening, uncalibrated +- lock/trigger: The trigger of the last action: autoLock, automatic, button, manual, system +- lock/completionStatus: Status of the last action as reported by NUKI lock (needs bluetooth connection): success, motorBlocked, canceled, tooRecent, busy, lowMotorVoltage, clutchFailure, motorPowerFailure, incompleteFailure, invalidCode, otherError, unknown +- lock/authorizationId: If enabled in the web interface, this node returns the authorization id of the last lock action +- lock/authorizationName: If enabled in the web interface, this node returns the authorization name of the last lock action +- lock/commandResult: Result of the last action as reported by NUKI library: success, failed, timeOut, working, notPaired, error, undefined +- lock/doorSensorState: State of the door sensor: unavailable, deactivated, doorClosed, doorOpened, doorStateUnknown, calibrating + +- battery/voltage: Reports the current battery voltage in Volts. +- battery/critical: 1 if battery level is critical, otherwise 0 + +### Misc - presence/devices: List of detected bluetooth devices as CSV. Can be used for presence detection ## Connecting via LAN (Optional) diff --git a/Version.h b/Version.h index 0d6b3c3..ec6d5f2 100644 --- a/Version.h +++ b/Version.h @@ -1,3 +1,3 @@ #pragma once -#define nuki_hub_version "2.6" \ No newline at end of file +#define nuki_hub_version "3.0" \ No newline at end of file diff --git a/WebCfgServer.cpp b/WebCfgServer.cpp index 1d65999..ac258e9 100644 --- a/WebCfgServer.cpp +++ b/WebCfgServer.cpp @@ -3,9 +3,10 @@ #include "Version.h" #include "hardware/WifiEthServer.h" -WebCfgServer::WebCfgServer(NukiWrapper* nuki, Network* network, EthServer* ethServer, Preferences* preferences, bool allowRestartToPortal) +WebCfgServer::WebCfgServer(NukiWrapper* nuki, NukiOpenerWrapper* nukiOpener, Network* network, EthServer* ethServer, Preferences* preferences, bool allowRestartToPortal) : _server(ethServer), _nuki(nuki), + _nukiOpener(nukiOpener), _network(network), _preferences(preferences), _allowRestartToPortal(allowRestartToPortal) @@ -26,7 +27,6 @@ WebCfgServer::WebCfgServer(NukiWrapper* nuki, Network* network, EthServer* ethSe } } - void WebCfgServer::initialize() { _server.on("/", [&]() { @@ -45,6 +45,14 @@ void WebCfgServer::initialize() buildCredHtml(response); _server.send(200, "text/html", response); }); + _server.on("/mqttconfig", [&]() { + if (_hasCredentials && !_server.authenticate(_credUser, _credPassword)) { + return _server.requestAuthentication(); + } + String response = ""; + buildMqttConfigHtml(response); + _server.send(200, "text/html", response); + }); _server.on("/wifi", [&]() { if (_hasCredentials && !_server.authenticate(_credUser, _credPassword)) { return _server.requestAuthentication(); @@ -53,12 +61,19 @@ void WebCfgServer::initialize() buildConfigureWifiHtml(response); _server.send(200, "text/html", response); }); - _server.on("/unpair", [&]() { + _server.on("/unpairlock", [&]() { if (_hasCredentials && !_server.authenticate(_credUser, _credPassword)) { return _server.requestAuthentication(); } - processUnpair(); + processUnpair(false); + }); + _server.on("/unpairopener", [&]() { + if (_hasCredentials && !_server.authenticate(_credUser, _credPassword)) { + return _server.requestAuthentication(); + } + + processUnpair(true); }); _server.on("/wifimanager", [&]() { if (_hasCredentials && !_server.authenticate(_credUser, _credPassword)) { @@ -107,8 +122,6 @@ bool WebCfgServer::processArgs(String& message) bool clearMqttCredentials = false; bool clearCredentials = false; - bool publishAuthData = false; - int count = _server.args(); for(int index = 0; index < count; index++) { @@ -147,7 +160,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") @@ -177,7 +195,18 @@ bool WebCfgServer::processArgs(String& message) } else if(key == "PUBAUTH") { - publishAuthData = true; + _preferences->putBool(preference_publish_authdata, (value == "1")); + configChanged = true; + } + else if(key == "LOCKENA") + { + _preferences->putBool(preference_lock_enabled, (value == "1")); + configChanged = true; + } + else if(key == "OPENA") + { + _preferences->putBool(preference_opener_enabled, (value == "1")); + configChanged = true; } else if(key == "CREDUSER") { @@ -196,25 +225,32 @@ bool WebCfgServer::processArgs(String& message) _preferences->putString(preference_cred_password, value); configChanged = true; } - else if(key == "NUKIPIN") + else if(key == "NUKIPIN" && _nuki != nullptr) { if(value == "#") { - message = "PIN cleared"; + message = "NUKI Lock PIN cleared"; _nuki->setPin(0xffff); } else { - message = "PIN saved"; + message = "NUKI Lock PIN saved"; _nuki->setPin(value.toInt()); } } - } - - if(_preferences->getBool(preference_publish_authdata) != publishAuthData) - { - _preferences->putBool(preference_publish_authdata, publishAuthData); - configChanged = true; + else if(key == "NUKIOPPIN" && _nukiOpener != nullptr) + { + if(value == "#") + { + message = "NUKI Opener PIN cleared"; + _nukiOpener->setPin(0xffff); + } + else + { + message = "NUKI Opener PIN saved"; + _nukiOpener->setPin(value.toInt()); + } + } } if(clearMqttCredentials) @@ -257,29 +293,49 @@ void WebCfgServer::buildHtml(String& response) String version = " "; version.concat(nuki_hub_version); - char lockstateArr[20]; - Nuki::lockstateToString(_nuki->keyTurnerState().lockState, lockstateArr); - String lockState = " "; - lockState.concat(lockstateArr); - response.concat(""); - printParameter(response, "Paired", _nuki->isPaired() ? " Yes" : " No"); + printParameter(response, "MQTT Connected", _network->isMqttConnected() ? " Yes" : " No"); - printParameter(response, "Lock state", lockState.c_str()); + if(_nuki != nullptr) + { + String lockState = " "; + char lockstateArr[20]; + NukiLock::lockstateToString(_nuki->keyTurnerState().lockState, lockstateArr); + lockState.concat(lockstateArr); + printParameter(response, "NUKI Lock paired", _nuki->isPaired() ? " Yes" : " No"); + printParameter(response, "NUKI Lock state", lockState.c_str()); + } + if(_nukiOpener != nullptr) + { + String lockState = " "; + char lockstateArr[20]; + NukiOpener::lockstateToString(_nukiOpener->keyTurnerState().lockState, lockstateArr); + lockState.concat(lockstateArr); + printParameter(response, "NUKI Opener paired", _nukiOpener->isPaired() ? " Yes" : " No"); + printParameter(response, "NUKI Opener state", lockState.c_str()); + } printParameter(response, "Firmware", version.c_str()); response.concat("


"); + response.concat("

MQTT and Network Configuration

"); + response.concat("
"); + response.concat(""); + response.concat("
"); + response.concat("
"); - response.concat("

Configuration

"); + response.concat("

Configuration

"); response.concat(""); - 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, "MQTTPATH", "MQTT Path", _preferences->getString(preference_mqtt_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); + printCheckBox(response, "LOCKENA", "NUKI Lock enabled", _preferences->getBool(preference_lock_enabled)); + if(_preferences->getBool(preference_lock_enabled)) + { + printInputField(response, "MQTTPATH", "MQTT Lock Path", _preferences->getString(preference_mqtt_lock_path).c_str(), 180); + } + printCheckBox(response, "OPENA", "NUKI Opener enabled", _preferences->getBool(preference_opener_enabled)); + if(_preferences->getBool(preference_opener_enabled)) + { + printInputField(response, "MQTTOPPATH", "MQTT Opener Path", _preferences->getString(preference_mqtt_opener_path).c_str(), 180); + } printInputField(response, "LSTINT", "Query interval lock state (seconds)", _preferences->getInt(preference_query_interval_lockstate), 10); printInputField(response, "BATINT", "Query interval battery (seconds)", _preferences->getInt(preference_query_interval_battery), 10); printCheckBox(response, "PUBAUTH", "Publish auth data (May reduce battery life)", _preferences->getBool(preference_publish_authdata)); @@ -288,9 +344,9 @@ void WebCfgServer::buildHtml(String& response) response.concat("
"); - response.concat("

"); + response.concat(""); - response.concat("

Credentials

"); + response.concat("

Credentials

"); response.concat(""); response.concat(""); response.concat(""); @@ -321,25 +377,71 @@ void WebCfgServer::buildCredHtml(String &response) response.concat("
"); response.concat(""); - response.concat("

"); - response.concat("

NUKI Pin Code

"); + if(_nuki != nullptr) + { + response.concat("

"); + response.concat("

NUKI Lock PIN

"); + response.concat("
"); + printInputField(response, "NUKIPIN", "PIN Code (# to clear)", "*", 20, true); + response.concat("
"); + response.concat("
"); + response.concat(""); + } + + if(_nukiOpener != nullptr) + { + response.concat("

"); + response.concat("

NUKI Opener PIN

"); + response.concat(""); + printInputField(response, "NUKIOPPIN", "PIN Code (# to clear)", "*", 20, true); + response.concat("
"); + response.concat("
"); + response.concat("
"); + } + + _confirmCode = generateConfirmCode(); + if(_nuki != nullptr) + { + response.concat("

Unpair NUKI Lock

"); + response.concat("
"); + String message = "Type "; + message.concat(_confirmCode); + message.concat(" to confirm unpair"); + printInputField(response, "CONFIRMTOKEN", message.c_str(), "", 10); + response.concat("

"); + } + + if(_nukiOpener != nullptr) + { + response.concat("

Unpair NUKI Opener

"); + response.concat("
"); + String message = "Type "; + message.concat(_confirmCode); + message.concat(" to confirm unpair"); + printInputField(response, "CONFIRMTOKEN", message.c_str(), "", 10); + response.concat("

"); + } + + + response.concat("\n"); + response.concat("\n"); +} + +void WebCfgServer::buildMqttConfigHtml(String &response) +{ + response.concat("
"); + response.concat("

MQTT COnfiguration

"); response.concat(""); - printInputField(response, "NUKIPIN", "PIN Code (# to clear)", "*", 20, true); + 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, "NETTIMEOUT", "Network Timeout until restart (seconds; -1 to disable)", _preferences->getInt(preference_network_timeout), 5); response.concat("
"); response.concat("
"); response.concat("
"); - _confirmCode = generateConfirmCode(); - response.concat("

Unpair NUKI

"); - response.concat("
"); - String message = "Type "; - message.concat(_confirmCode); - message.concat(" to confirm unpair"); - printInputField(response, "CONFIRMTOKEN", message.c_str(), "", 10); - response.concat("

"); - - response.concat("\n"); - response.concat("\n"); } void WebCfgServer::buildConfirmHtml(String &response, const String &message, uint32_t redirectDelay) @@ -375,15 +477,7 @@ void WebCfgServer::buildConfigureWifiHtml(String &response) response.concat("\n"); } -//printInputField(response, "CONFIRMTOKEN", "Type confirm", "", 10); - - -//int count = _server.args(); -//for(int index = 0; index < count; index++) -//{ -//String key = _server.argName(index); -//String value = _server.arg(index); -void WebCfgServer::processUnpair() +void WebCfgServer::processUnpair(bool opener) { String response = ""; if(_server.args() == 0) @@ -405,9 +499,16 @@ void WebCfgServer::processUnpair() } } - buildConfirmHtml(response, "Unpairing NUKI and restarting.", 3); + buildConfirmHtml(response, opener ? "Unpairing NUKI Opener and restarting." : "Unpairing NUKI Lock and restarting.", 3); _server.send(200, "text/html", response); - _nuki->unpair(); + if(!opener && _nuki != nullptr) + { + _nuki->unpair(); + } + if(opener && _nukiOpener != nullptr) + { + _nukiOpener->unpair(); + } waitAndProcess(false, 1000); ESP.restart(); } @@ -463,20 +564,20 @@ void WebCfgServer::printInputField(String& response, void WebCfgServer::printCheckBox(String &response, const char *token, const char *description, const bool value) { - response.concat(""); - response.concat(""); + response.concat(""); response.concat(description); - response.concat(""); - response.concat(""); - response.concat(" "); + + response.concat(""); + + response.concat(""); - response.concat(""); - response.concat(""); + response.concat("/>"); } void WebCfgServer::printParameter(String& response, const char *description, const char *value) diff --git a/WebCfgServer.h b/WebCfgServer.h index 7b6af4f..f8152bf 100644 --- a/WebCfgServer.h +++ b/WebCfgServer.h @@ -4,6 +4,7 @@ #include #include "NukiWrapper.h" #include "Network.h" +#include "NukiOpenerWrapper.h" enum class TokenType { @@ -20,7 +21,7 @@ enum class TokenType class WebCfgServer { public: - WebCfgServer(NukiWrapper* nuki, Network* network, EthServer* ethServer, Preferences* preferences, bool allowRestartToPortal); + WebCfgServer(NukiWrapper* nuki, NukiOpenerWrapper* nukiOpener, Network* network, EthServer* ethServer, Preferences* preferences, bool allowRestartToPortal); ~WebCfgServer() = default; void initialize(); @@ -31,9 +32,10 @@ private: bool processArgs(String& message); void buildHtml(String& response); void buildCredHtml(String& response); + void buildMqttConfigHtml(String& response); void buildConfirmHtml(String& response, const String &message, uint32_t redirectDelay = 5); void buildConfigureWifiHtml(String& response); - void processUnpair(); + void processUnpair(bool opener); void buildHtmlHeader(String& response); void printInputField(String& response, const char* token, const char* description, const char* value, const size_t maxLength, const bool isPassword = false); @@ -47,6 +49,7 @@ private: WebServer _server; NukiWrapper* _nuki; + NukiOpenerWrapper* _nukiOpener; Network* _network; Preferences* _preferences; diff --git a/lib/nuki_ble b/lib/nuki_ble index 53a4fc1..b1e1baa 160000 --- a/lib/nuki_ble +++ b/lib/nuki_ble @@ -1 +1 @@ -Subproject commit 53a4fc1b321bbc21d779477acc85adf3eafce56e +Subproject commit b1e1baa0a97a8070b2ed5682c8116b0e0999046d diff --git a/main.cpp b/main.cpp index 2a728b3..9c09801 100644 --- a/main.cpp +++ b/main.cpp @@ -8,19 +8,27 @@ #include "PresenceDetection.h" #include "hardware/W5500EthServer.h" #include "hardware/WifiEthServer.h" +#include "NukiOpenerWrapper.h" Network* network = nullptr; +NetworkOpener* networkOpener = nullptr; WebCfgServer* webCfgServer = nullptr; +BleScanner::Scanner* bleScanner = nullptr; NukiWrapper* nuki = nullptr; +NukiOpenerWrapper* nukiOpener = nullptr; PresenceDetection* presenceDetection = nullptr; Preferences* preferences = nullptr; EthServer* ethServer = nullptr; +bool lockEnabled = false; +bool openerEnabled = false; + void networkTask(void *pvParameters) { while(true) { network->update(); + networkOpener->update(); webCfgServer->update(); vTaskDelay(200 / portTICK_PERIOD_MS); } @@ -30,7 +38,24 @@ void nukiTask(void *pvParameters) { while(true) { - nuki->update(); + bleScanner->update(); + vTaskDelay( 20 / portTICK_PERIOD_MS); + + bool needsPairing = (lockEnabled && !nuki->isPaired()) || (openerEnabled && !nukiOpener->isPaired()); + + if (needsPairing) + { + vTaskDelay( 5000 / portTICK_PERIOD_MS); + } + + if(lockEnabled) + { + nuki->update(); + } + if(openerEnabled) + { + nukiOpener->update(); + } } } @@ -96,19 +121,33 @@ void initEthServer(const NetworkDeviceType device) } } +void initNuki() +{ + +} + void setup() { pinMode(NETWORK_SELECT, INPUT_PULLUP); Serial.begin(115200); + preferences = new Preferences(); + preferences->begin("nukihub", false); + + if(!preferences->getBool(preference_started_befores)) + { + preferences->putBool(preference_started_befores, true); + preferences->putBool(preference_lock_enabled, true); + } + // const NetworkDeviceType networkDevice = NetworkDeviceType::WiFi; const NetworkDeviceType networkDevice = digitalRead(NETWORK_SELECT) == HIGH ? NetworkDeviceType::WiFi : NetworkDeviceType::W5500; - preferences = new Preferences(); - preferences->begin("nukihub", false); network = new Network(networkDevice, preferences); network->initialize(); + networkOpener = new NetworkOpener(network, preferences); + networkOpener->initialize(); uint32_t deviceId = preferences->getUInt(preference_deviceId); if(deviceId == 0) @@ -119,12 +158,30 @@ void setup() initEthServer(networkDevice); - nuki = new NukiWrapper("NukiHub", deviceId, network, preferences); - webCfgServer = new WebCfgServer(nuki, network, ethServer, preferences, networkDevice == NetworkDeviceType::WiFi); - webCfgServer->initialize(); - nuki->initialize(); + bleScanner = new BleScanner::Scanner(); + bleScanner->initialize("NukiHub"); + bleScanner->setScanDuration(10); - presenceDetection = new PresenceDetection(preferences, nuki->bleScanner(), network); + lockEnabled = preferences->getBool(preference_lock_enabled); + Serial.println(lockEnabled ? F("NUKI Lock enabled") : F("NUKI Lock disabled")); + if(lockEnabled) + { + nuki = new NukiWrapper("NukiHub", deviceId, bleScanner, network, preferences); + nuki->initialize(); + } + + openerEnabled = preferences->getBool(preference_opener_enabled); + Serial.println(openerEnabled ? F("NUKI Opener enabled") : F("NUKI Opener disabled")); + if(openerEnabled) + { + nukiOpener = new NukiOpenerWrapper("NukiHub", deviceId, bleScanner, networkOpener, preferences); + nukiOpener->initialize(); + } + + webCfgServer = new WebCfgServer(nuki, nukiOpener, network, ethServer, preferences, networkDevice == NetworkDeviceType::WiFi); + webCfgServer->initialize(); + + presenceDetection = new PresenceDetection(preferences, bleScanner, network); presenceDetection->initialize(); setupTasks();