From b298d410eb73bcfb227aad2a5430f27f601de4fc Mon Sep 17 00:00:00 2001 From: iranl Date: Sat, 17 Aug 2024 05:21:22 +0200 Subject: [PATCH] Add Authorization entries (#456) * Add and remove libs and components for Arduino Core 3 * Arduino Core 3 * Add back Solo1 * Change ESP32-S3 to 4MB build * Add Authorization info and control * Use esp_crt_bundle for HTTPS requests * Remove Solo1 support * Improve Nuki device config read functions * Webserial * OTA Improvements * Authorization Entries * Authorization entries * Authorization --- README.md | 46 ++- pio_package_post.py | 1 + platformio.ini | 26 +- resources/github_root_ca.pem | 41 +++ sdkconfig.defaults | 4 +- src/Config.h | 1 + src/MqttTopics.h | 6 + src/NukiNetwork.cpp | 47 +-- src/NukiNetwork.h | 5 +- src/NukiNetworkLock.cpp | 177 ++++++++++- src/NukiNetworkLock.h | 4 + src/NukiNetworkOpener.cpp | 195 +++++++++++- src/NukiNetworkOpener.h | 4 + src/NukiOpenerWrapper.cpp | 552 +++++++++++++++++++++++++++++++++- src/NukiOpenerWrapper.h | 6 + src/NukiWrapper.cpp | 555 ++++++++++++++++++++++++++++++++++- src/NukiWrapper.h | 6 + src/PreferencesKeys.h | 12 +- src/WebCfgServer.cpp | 181 +++++++----- src/main.cpp | 14 +- 20 files changed, 1728 insertions(+), 155 deletions(-) create mode 100644 resources/github_root_ca.pem diff --git a/README.md b/README.md index 4c7f7fa..b77bb00 100644 --- a/README.md +++ b/README.md @@ -171,8 +171,10 @@ In a browser navigate to the IP address assigned to the ESP32. - Publish keypad entries information (Only available when a Keypad is detected): Enable to publish information about keypad codes through MQTT, see the "[Keypad control](#keypad-control-optional)" section of this README - Also publish keypad codes (Only available when a Keypad is detected): Enable to publish the actual keypad codes through MQTT, note that is could be considered a security risk - Add, modify and delete keypad codes (Only available when a Keypad is detected): Enable to allow configuration of keypad codes through MQTT, see the "[Keypad control](#keypad-control-optional)" section of this README -- Publish time control information: Enable to publish information about time control entries through MQTT, see the "[Time Control](#time-control)" section of this README -- Add, modify and delete time control entries: Enable to allow configuration of time control entries through MQTT, see the "[Time Control](#time-control)" section of this README +- Publish timecontrol information: Enable to publish information about timecontrol entries through MQTT, see the "[Timecontrol](#timecontrol)" section of this README +- Add, modify and delete timecontrol entries: Enable to allow configuration of timecontrol entries through MQTT, see the "[Timecontrol](#timecontrol)" section of this README +- Publish authorization information: Enable to publish information about authorization entries through MQTT, see the "[Authorization](#authorization)" section of this README +- Modify and delete authorization entries: Enable to allow configuration of authorization entries through MQTT, see the "[Authorization](#authorization)" section of this README - Publish auth data: Enable to publish authorization data to the MQTT topic lock/log. Requires the Nuki security code / PIN to be set, see "[Nuki Lock PIN / Nuki Opener PIN](#nuki-lock-pin--nuki-opener-pin)" below. #### Nuki Lock/Opener Access Control @@ -556,15 +558,15 @@ For example, to add a code: - write 1 to enabled - write "add" to action -## Time control using JSON (optional) +## Timecontrol using JSON (optional) -Time control entries can be added, updated and removed. This has to enabled first in the configuration portal. Check "Add, modify and delete time control entries" under "Access Level Configuration" and save the configuration. +Timecontrol entries can be added, updated and removed. This has to enabled first in the configuration portal. Check "Add, modify and delete timecontrol entries" under "Access Level Configuration" and save the configuration. -Information about current time control entries is published as JSON data to the "timecontrol/json" MQTT topic.
-This needs to be enabled separately by checking "Publish time control entries information" under "Access Level Configuration" and saving the configuration. +Information about current timecontrol entries is published as JSON data to the "timecontrol/json" MQTT topic.
+This needs to be enabled separately by checking "Publish timecontrol entries information" under "Access Level Configuration" and saving the configuration. By default a maximum of 10 entries are published. -To change Nuki Lock/Opener time control settings set the `timecontrol/actionJson` topic to a JSON formatted value containing the following nodes. +To change Nuki Lock/Opener timecontrol settings set the `timecontrol/actionJson` topic to a JSON formatted value containing the following nodes. | Node | Delete | Add | Update | Usage | Possible values | |------------------|----------|----------|----------|------------------------------------------------------------------------------------------|----------------------------------------------------------------| @@ -580,6 +582,36 @@ Examples: - Add: `{ "action": "add", "weekdays": [ "wed", "thu", "fri" ], "time": "08:00", "lockAction": "Unlock" }` - Update: `{ "action": "update", "entryId": "1234", "enabled": "1", "weekdays": [ "mon", "tue", "sat", "sun" ], "time": "08:00", "lockAction": "Lock" }` +## Authorization entries control using JSON (optional) + +Authorization entries can be updated and removed. This has to enabled first in the configuration portal. Check "Modify and delete authorization entries" under "Access Level Configuration" and save the configuration. +It is currently not (yet) possible to add authorization entries this way. + +Information about current authorization entries is published as JSON data to the "authorization/json" MQTT topic.
+This needs to be enabled separately by checking "Publish authorization entries information" under "Access Level Configuration" and saving the configuration. +By default a maximum of 10 entries are published. + +To change Nuki Lock/Opener authorization settings set the `authorization/actionJson` topic to a JSON formatted value containing the following nodes. + + +| Node | Delete | Add | Update | Usage | Possible values | +|------------------|----------|----------|----------|------------------------------------------------------------------------------------------------------------------|----------------------------------------| +| action | Required | Required | Required | The action to execute | "delete", "add", "update" | +| authId | Required | Not used | Required | The auth ID of the existing entry to delete or update | Integer | +| enabled | Not used | Not used | Optional | Enable or disable the authorization, always enabled on add | 1 = enabled, 0 = disabled | +| name | Not used | Required | Optional | The name of the authorization to create or update | String, max 20 chars | +| remoteAllowed | Not used | Optional | Optional | If this authorization is allowed remote access, requires enabled = 1 | 1 = enabled, 0 = disabled | +| timeLimited | Not used | Optional | Optional | If this authorization is restricted to access only at certain times, requires enabled = 1 | 1 = enabled, 0 = disabled | +| allowedFrom | Not used | Optional | Optional | The start timestamp from which access should be allowed (requires enabled = 1 and timeLimited = 1) | "YYYY-MM-DD HH:MM:SS" | +| allowedUntil | Not used | Optional | Optional | The end timestamp until access should be allowed (requires enabled = 1 and timeLimited = 1) | "YYYY-MM-DD HH:MM:SS" | +| allowedWeekdays | Not used | Optional | Optional | Weekdays on which access should be allowed (requires enabled = 1 and timeLimited = 1) | Array of days: "mon", "tue", "wed", "thu" , "fri" "sat", "sun"| +| allowedFromTime | Not used | Optional | Optional | The start time per day from which access should be allowed (requires enabled = 1 and timeLimited = 1) | "HH:MM" | +| allowedUntilTime | Not used | Optional | Optional | The end time per day until access should be allowed (requires enabled = 1 and timeLimited = 1) | "HH:MM" | + +Examples: +- Delete: `{ "action": "delete", "authId": "1234" }` +- Update: `{ "action": "update", "authId": "1234", "enabled": "1", "name": "Test", "timeLimited": "1", "allowedFrom": "2024-04-12 10:00:00", "allowedUntil": "2034-04-12 10:00:00", "allowedWeekdays": [ "mon", "tue", "sat", "sun" ], "allowedFromTime": "08:00", "allowedUntilTime": "16:00" }` + ## GPIO lock control (optional) The lock can be controlled via GPIO.
diff --git a/pio_package_post.py b/pio_package_post.py index b7b5d1d..2e20791 100644 --- a/pio_package_post.py +++ b/pio_package_post.py @@ -6,6 +6,7 @@ from pathlib import Path def get_board_name(env): board = env.get('BOARD_MCU') + if env.get('BOARD') == 'esp32-solo1': board = env.get('BOARD').replace('-', '') return board diff --git a/platformio.ini b/platformio.ini index 22a50fd..21d694f 100644 --- a/platformio.ini +++ b/platformio.ini @@ -80,7 +80,7 @@ extra_scripts = build_flags = ${env.build_flags} -DCONFIG_ASYNC_TCP_QUEUE_SIZE=128 - -DCONFIG_ASYNC_TCP_STACK_SIZE=24576 + -DCONFIG_ASYNC_TCP_STACK_SIZE=8192 -DWS_MAX_QUEUED_MESSAGES=128 -DCORE_DEBUG_LEVEL=ARDUHAL_LOG_LEVEL_NONE -DCONFIG_NIMBLE_CPP_LOG_LEVEL=0 @@ -137,8 +137,8 @@ build_flags = -DDEBUG_NUKI_COMMUNICATION ;-DDEBUG_NUKI_HEX_DATA -DDEBUG_NUKI_READABLE_DATA - -DCONFIG_ASYNC_TCP_QUEUE_SIZE=1024 - -DCONFIG_ASYNC_TCP_STACK_SIZE=24576 + -DCONFIG_ASYNC_TCP_QUEUE_SIZE=128 + -DCONFIG_ASYNC_TCP_STACK_SIZE=8192 -DWS_MAX_QUEUED_MESSAGES=512 [env:esp32-c3_dbg] @@ -158,8 +158,8 @@ build_flags = -DDEBUG_NUKI_COMMUNICATION ;-DDEBUG_NUKI_HEX_DATA -DDEBUG_NUKI_READABLE_DATA - -DCONFIG_ASYNC_TCP_QUEUE_SIZE=1024 - -DCONFIG_ASYNC_TCP_STACK_SIZE=24576 + -DCONFIG_ASYNC_TCP_QUEUE_SIZE=128 + -DCONFIG_ASYNC_TCP_STACK_SIZE=8192 -DWS_MAX_QUEUED_MESSAGES=512 [env:esp32-c6_dbg] @@ -179,8 +179,8 @@ build_flags = -DDEBUG_NUKI_COMMUNICATION ;-DDEBUG_NUKI_HEX_DATA -DDEBUG_NUKI_READABLE_DATA - -DCONFIG_ASYNC_TCP_QUEUE_SIZE=1024 - -DCONFIG_ASYNC_TCP_STACK_SIZE=24576 + -DCONFIG_ASYNC_TCP_QUEUE_SIZE=128 + -DCONFIG_ASYNC_TCP_STACK_SIZE=8192 -DWS_MAX_QUEUED_MESSAGES=512 [env:esp32-h2_dbg] @@ -201,8 +201,8 @@ build_flags = -DDEBUG_NUKI_COMMUNICATION ;-DDEBUG_NUKI_HEX_DATA -DDEBUG_NUKI_READABLE_DATA - -DCONFIG_ASYNC_TCP_QUEUE_SIZE=1024 - -DCONFIG_ASYNC_TCP_STACK_SIZE=24576 + -DCONFIG_ASYNC_TCP_QUEUE_SIZE=128 + -DCONFIG_ASYNC_TCP_STACK_SIZE=8192 -DWS_MAX_QUEUED_MESSAGES=512 [env:esp32-s3_dbg] @@ -222,8 +222,8 @@ build_flags = -DDEBUG_NUKI_COMMUNICATION ;-DDEBUG_NUKI_HEX_DATA -DDEBUG_NUKI_READABLE_DATA - -DCONFIG_ASYNC_TCP_QUEUE_SIZE=1024 - -DCONFIG_ASYNC_TCP_STACK_SIZE=24576 + -DCONFIG_ASYNC_TCP_QUEUE_SIZE=128 + -DCONFIG_ASYNC_TCP_STACK_SIZE=8192 -DWS_MAX_QUEUED_MESSAGES=512 [env:esp32-solo1_dbg] @@ -243,6 +243,6 @@ build_flags = -DDEBUG_NUKI_COMMUNICATION ;-DDEBUG_NUKI_HEX_DATA -DDEBUG_NUKI_READABLE_DATA - -DCONFIG_ASYNC_TCP_QUEUE_SIZE=1024 - -DCONFIG_ASYNC_TCP_STACK_SIZE=24576 + -DCONFIG_ASYNC_TCP_QUEUE_SIZE=128 + -DCONFIG_ASYNC_TCP_STACK_SIZE=8192 -DWS_MAX_QUEUED_MESSAGES=512 \ No newline at end of file diff --git a/resources/github_root_ca.pem b/resources/github_root_ca.pem new file mode 100644 index 0000000..50d97b5 --- /dev/null +++ b/resources/github_root_ca.pem @@ -0,0 +1,41 @@ +USERTrust ECC Certification Authority +===================================== +-----BEGIN CERTIFICATE----- +MIICjzCCAhWgAwIBAgIQXIuZxVqUxdJxVt7NiYDMJjAKBggqhkjOPQQDAzCBiDELMAkGA1UEBhMC +VVMxEzARBgNVBAgTCk5ldyBKZXJzZXkxFDASBgNVBAcTC0plcnNleSBDaXR5MR4wHAYDVQQKExVU +aGUgVVNFUlRSVVNUIE5ldHdvcmsxLjAsBgNVBAMTJVVTRVJUcnVzdCBFQ0MgQ2VydGlmaWNhdGlv +biBBdXRob3JpdHkwHhcNMTAwMjAxMDAwMDAwWhcNMzgwMTE4MjM1OTU5WjCBiDELMAkGA1UEBhMC +VVMxEzARBgNVBAgTCk5ldyBKZXJzZXkxFDASBgNVBAcTC0plcnNleSBDaXR5MR4wHAYDVQQKExVU +aGUgVVNFUlRSVVNUIE5ldHdvcmsxLjAsBgNVBAMTJVVTRVJUcnVzdCBFQ0MgQ2VydGlmaWNhdGlv +biBBdXRob3JpdHkwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAAQarFRaqfloI+d61SRvU8Za2EurxtW2 +0eZzca7dnNYMYf3boIkDuAUU7FfO7l0/4iGzzvfUinngo4N+LZfQYcTxmdwlkWOrfzCjtHDix6Ez +nPO/LlxTsV+zfTJ/ijTjeXmjQjBAMB0GA1UdDgQWBBQ64QmG1M8ZwpZ2dEl23OA1xmNjmjAOBgNV +HQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAKBggqhkjOPQQDAwNoADBlAjA2Z6EWCNzklwBB +HU6+4WMBzzuqQhFkoJ2UOQIReVx7Hfpkue4WQrO/isIJxOzksU0CMQDpKmFHjFJKS04YcPbWRNZu +9YO6bVi9JNlWSOrvxKJGgYhqOkbRqZtNyWHa0V1Xahg= +-----END CERTIFICATE----- + +DigiCert Global Root G2 +===================================== +-----BEGIN CERTIFICATE----- +MIIDjjCCAnagAwIBAgIQAzrx5qcRqaC7KGSxHQn65TANBgkqhkiG9w0BAQsFADBh +MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3 +d3cuZGlnaWNlcnQuY29tMSAwHgYDVQQDExdEaWdpQ2VydCBHbG9iYWwgUm9vdCBH +MjAeFw0xMzA4MDExMjAwMDBaFw0zODAxMTUxMjAwMDBaMGExCzAJBgNVBAYTAlVT +MRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5j +b20xIDAeBgNVBAMTF0RpZ2lDZXJ0IEdsb2JhbCBSb290IEcyMIIBIjANBgkqhkiG +9w0BAQEFAAOCAQ8AMIIBCgKCAQEAuzfNNNx7a8myaJCtSnX/RrohCgiN9RlUyfuI +2/Ou8jqJkTx65qsGGmvPrC3oXgkkRLpimn7Wo6h+4FR1IAWsULecYxpsMNzaHxmx +1x7e/dfgy5SDN67sH0NO3Xss0r0upS/kqbitOtSZpLYl6ZtrAGCSYP9PIUkY92eQ +q2EGnI/yuum06ZIya7XzV+hdG82MHauVBJVJ8zUtluNJbd134/tJS7SsVQepj5Wz +tCO7TG1F8PapspUwtP1MVYwnSlcUfIKdzXOS0xZKBgyMUNGPHgm+F6HmIcr9g+UQ +vIOlCsRnKPZzFBQ9RnbDhxSJITRNrw9FDKZJobq7nMWxM4MphQIDAQABo0IwQDAP +BgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBhjAdBgNVHQ4EFgQUTiJUIBiV +5uNu5g/6+rkS7QYXjzkwDQYJKoZIhvcNAQELBQADggEBAGBnKJRvDkhj6zHd6mcY +1Yl9PMWLSn/pvtsrF9+wX3N3KjITOYFnQoQj8kVnNeyIv/iPsGEMNKSuIEyExtv4 +NeF22d+mQrvHRAiGfzZ0JFrabA0UWTW98kndth/Jsw1HKj2ZL7tcu7XUIOGZX1NG +Fdtom/DzMNU+MeKNhJ7jitralj41E6Vf8PlwUHBHQRFXGU7Aj64GxJUTFy8bJZ91 +8rGOmaFvE7FBcf6IKshPECBV1/MUReXgRPTqh5Uykw7+U0b6LJ3/iyK5S9kJRaTe +pLiaWN0bfVKfjllDiIGknibVb63dDcY3fe0Dkhvld1927jyNxF1WW6LZZm6zNTfl +MrY= +-----END CERTIFICATE----- diff --git a/sdkconfig.defaults b/sdkconfig.defaults index 5020dd1..780d167 100644 --- a/sdkconfig.defaults +++ b/sdkconfig.defaults @@ -80,4 +80,6 @@ CONFIG_ETH_SPI_ETHERNET_W5500=y CONFIG_ETH_SPI_ETHERNET_DM9051=y CONFIG_ETH_SPI_ETHERNET_KSZ8851SNL=y CONFIG_MBEDTLS_CERTIFICATE_BUNDLE=y -CONFIG_MBEDTLS_CERTIFICATE_BUNDLE_DEFAULT_CMN=y \ No newline at end of file +CONFIG_MBEDTLS_CERTIFICATE_BUNDLE_DEFAULT_NONE=y +CONFIG_MBEDTLS_CUSTOM_CERTIFICATE_BUNDLE=y +CONFIG_MBEDTLS_CUSTOM_CERTIFICATE_BUNDLE_PATH="resources/github_root_ca.pem" \ No newline at end of file diff --git a/src/Config.h b/src/Config.h index e8586ad..a389afd 100644 --- a/src/Config.h +++ b/src/Config.h @@ -102,6 +102,7 @@ #define MAX_AUTHLOG 5 #define MAX_KEYPAD 10 #define MAX_TIMECONTROL 10 +#define MAX_AUTH 10 #endif #define NETWORK_TASK_SIZE 12288 diff --git a/src/MqttTopics.h b/src/MqttTopics.h index 52d2f4d..3ac1515 100644 --- a/src/MqttTopics.h +++ b/src/MqttTopics.h @@ -85,6 +85,12 @@ #define mqtt_topic_timecontrol_action "/timecontrol/action" #define mqtt_topic_timecontrol_command_result "/timecontrol/commandResult" +#define mqtt_topic_auth "/authorization" +#define mqtt_topic_auth_entries "/authorization/entries" +#define mqtt_topic_auth_json "/authorization/json" +#define mqtt_topic_auth_action "/authorization/action" +#define mqtt_topic_auth_command_result "/authorization/commandResult" + #define mqtt_topic_info_hardware_version "/info/hardwareVersion" #define mqtt_topic_info_firmware_version "/info/firmwareVersion" #define mqtt_topic_info_nuki_hub_version "/info/nukiHubVersion" diff --git a/src/NukiNetwork.cpp b/src/NukiNetwork.cpp index d64b73f..c4d18cb 100644 --- a/src/NukiNetwork.cpp +++ b/src/NukiNetwork.cpp @@ -107,7 +107,7 @@ void NukiNetwork::setupDevice() _networkDeviceType = NetworkDeviceType::WiFi; #else int custEth = _preferences->getInt(preference_network_custom_phy, 0); - + if(custEth<3) custEth++; else custEth = 0; _preferences->putInt(preference_network_custom_phy, custEth); @@ -377,7 +377,7 @@ void NukiNetwork::setupDevice() _preferences->getInt(preference_network_custom_mosi, -1), ETH_PHY_SPI_FREQ_MHZ, ETH_PHY_W5500); - break; + break; #endif } @@ -570,7 +570,6 @@ void NukiNetwork::initialize() bool NukiNetwork::update() { int64_t ts = (esp_timer_get_time() / 1000); - _device->update(); if(!_mqttEnabled) @@ -630,9 +629,9 @@ bool NukiNetwork::update() return false; } _mqttConnectCounter = 0; - if(forceEnableWebServer && !_webEnabled) + if(forceEnableWebServer && !_webEnabled) { - forceEnableWebServer = false; + forceEnableWebServer = false; delay(200); restartEsp(RestartReason::ReconfigureWebServer); } @@ -654,12 +653,13 @@ bool NukiNetwork::update() } _lastConnectedTs = ts; - + #if PRESENCE_DETECTION_ENABLED if(_presenceDetection != nullptr && (_lastPresenceTs == 0 || (ts - _lastPresenceTs) > 3000)) { char* presenceCsv = _presenceDetection->generateCsv(); bool success = publishString(_mqttPresencePrefix, mqtt_topic_presence, presenceCsv, true); + if(!success) { Log->println(F("Failed to publish presence CSV data.")); @@ -706,11 +706,12 @@ bool NukiNetwork::update() if(_lastUpdateCheckTs == 0 || (ts - _lastUpdateCheckTs) > 86400000) { _lastUpdateCheckTs = ts; + bool otaManifestSuccess = false; + JsonDocument doc; NetworkClientSecure *client = new NetworkClientSecure; if (client) { - //client->setDefaultCACertBundle(); - client->setCACertBundle(x509_crt_imported_bundle_bin_start, x509_crt_imported_bundle_bin_end - x509_crt_imported_bundle_bin_start); + client->setCACertBundle(x509_crt_imported_bundle_bin_start, x509_crt_imported_bundle_bin_end - x509_crt_imported_bundle_bin_start); { HTTPClient https; https.setFollowRedirects(HTTPC_STRICT_FOLLOW_REDIRECTS); @@ -721,28 +722,29 @@ bool NukiNetwork::update() if (httpResponseCode == HTTP_CODE_OK || httpResponseCode == HTTP_CODE_MOVED_PERMANENTLY) { - JsonDocument doc; DeserializationError jsonError = deserializeJson(doc, https.getStream()); - if (!jsonError) - { - String currentVersion = NUKI_HUB_VERSION; - - if(atof(doc["release"]["version"]) >= atof(currentVersion.c_str())) _latestVersion = doc["release"]["fullversion"]; - else if(currentVersion.indexOf("beta") > 0) _latestVersion = doc["beta"]["fullversion"]; - else if(currentVersion.indexOf("master") > 0) _latestVersion = doc["master"]["fullversion"]; - else _latestVersion = doc["release"]["fullversion"]; - - publishString(_maintenancePathPrefix, mqtt_topic_info_nuki_hub_latest, _latestVersion, true); - - if(strcmp(_latestVersion, _preferences->getString(preference_latest_version).c_str()) != 0) _preferences->putString(preference_latest_version, _latestVersion); - } + if (!jsonError) { otaManifestSuccess = true; } } } https.end(); } delete client; } + + if (otaManifestSuccess) + { + String currentVersion = NUKI_HUB_VERSION; + + if(atof(doc["release"]["version"]) >= atof(currentVersion.c_str())) _latestVersion = doc["release"]["fullversion"]; + else if(currentVersion.indexOf("beta") > 0) _latestVersion = doc["beta"]["fullversion"]; + else if(currentVersion.indexOf("master") > 0) _latestVersion = doc["master"]["fullversion"]; + else _latestVersion = doc["release"]["fullversion"]; + + publishString(_maintenancePathPrefix, mqtt_topic_info_nuki_hub_latest, _latestVersion, true); + + if(strcmp(_latestVersion, _preferences->getString(preference_latest_version).c_str()) != 0) _preferences->putString(preference_latest_version, _latestVersion); + } } } @@ -861,7 +863,6 @@ bool NukiNetwork::reconnect() _mqttConnectedTs = millis(); _mqttConnectionState = 1; delay(100); - _device->mqttOnMessage(NukiNetwork::onMqttDataReceivedCallback); for(const String& topic : _subscribedTopics) { diff --git a/src/NukiNetwork.h b/src/NukiNetwork.h index 9823df8..2c5daad 100644 --- a/src/NukiNetwork.h +++ b/src/NukiNetwork.h @@ -53,9 +53,9 @@ public: explicit NukiNetwork(Preferences* preferences, PresenceDetection* presenceDetection, Gpio* gpio, const String& maintenancePathPrefix, char* buffer, size_t bufferSize); void registerMqttReceiver(MqttReceiver* receiver); -#if PRESENCE_DETECTION_ENABLED + #if PRESENCE_DETECTION_ENABLED void setMqttPresencePath(char* path); -#endif + #endif void disableAutoRestarts(); // disable on OTA start void disableMqtt(); String localIP(); @@ -105,7 +105,6 @@ public: bool encryptionSupported(); bool mqttRecentlyConnected(); bool pathEquals(const char* prefix, const char* path, const char* referencePath); - uint16_t subscribe(const char* topic, uint8_t qos); void addReconnectedCallback(std::function reconnectedCallback); diff --git a/src/NukiNetworkLock.cpp b/src/NukiNetworkLock.cpp index 6b73614..30eb9ef 100644 --- a/src/NukiNetworkLock.cpp +++ b/src/NukiNetworkLock.cpp @@ -68,7 +68,6 @@ void NukiNetworkLock::initialize() _network->subscribe(_mqttPath, mqtt_topic_lock_action); _network->initTopic(_mqttPath, mqtt_topic_config_action, "--"); _network->subscribe(_mqttPath, mqtt_topic_config_action); - _network->subscribe(_mqttPath, mqtt_topic_reset); _network->initTopic(_mqttPath, mqtt_topic_reset, "0"); @@ -154,6 +153,12 @@ void NukiNetworkLock::initialize() _network->initTopic(_mqttPath, mqtt_topic_timecontrol_action, "--"); } + if(_preferences->getBool(preference_auth_control_enabled)) + { + _network->subscribe(_mqttPath, mqtt_topic_auth_action); + _network->initTopic(_mqttPath, mqtt_topic_auth_action, "--"); + } + if(_offEnabled) { char uidString[20]; @@ -200,7 +205,7 @@ void NukiNetworkLock::onMqttDataReceived(const char* topic, byte* payload, const { Log->println(F("Update requested via MQTT.")); String currentVersion = NUKI_HUB_VERSION; - + if(atof(_preferences->getString(preference_latest_version).c_str()) >= atof(currentVersion.c_str())) { _preferences->putString(preference_ota_updater_url, GITHUB_LATEST_UPDATER_BINARY_URL); @@ -403,6 +408,18 @@ void NukiNetworkLock::onMqttDataReceived(const char* topic, byte* payload, const publishString(mqtt_topic_timecontrol_action, "--", true); } + + if(comparePrefixedPath(topic, mqtt_topic_auth_action)) + { + if(strcmp(value, "") == 0 || strcmp(value, "--") == 0) return; + + if(_authCommandReceivedReceivedCallback != NULL) + { + _authCommandReceivedReceivedCallback(value); + } + + publishString(mqtt_topic_auth_action, "--", true); + } } void NukiNetworkLock::publishKeyTurnerState(const NukiLock::KeyTurnerState& keyTurnerState, const NukiLock::KeyTurnerState& lastKeyTurnerState) @@ -1262,6 +1279,152 @@ void NukiNetworkLock::publishTimeControl(const std::list& authEntries, uint maxAuthEntryCount) +{ + uint index = 0; + char str[50]; + char uidString[20]; + itoa(_preferences->getUInt(preference_nuki_id_lock, 0), uidString, 16); + String baseTopic = _preferences->getString(preference_mqtt_lock_path); + JsonDocument json; + + for(const auto& entry : authEntries) + { + auto jsonEntry = json.add(); + + jsonEntry["authId"] = entry.authId; + jsonEntry["idType"] = entry.idType; //CONSIDER INT TO STRING + jsonEntry["enabled"] = entry.enabled; + jsonEntry["name"] = entry.name; + jsonEntry["remoteAllowed"] = entry.remoteAllowed; + char createdDT[20]; + sprintf(createdDT, "%04d-%02d-%02d %02d:%02d:%02d", entry.createdYear, entry.createdMonth, entry.createdDay, entry.createdHour, entry.createdMinute, entry.createdSecond); + jsonEntry["dateCreated"] = createdDT; + jsonEntry["lockCount"] = entry.lockCount; + char lastActiveDT[20]; + sprintf(lastActiveDT, "%04d-%02d-%02d %02d:%02d:%02d", entry.lastActYear, entry.lastActMonth, entry.lastActDay, entry.lastActHour, entry.lastActMinute, entry.lastActSecond); + jsonEntry["dateLastActive"] = lastActiveDT; + jsonEntry["timeLimited"] = entry.timeLimited; + char allowedFromDT[20]; + sprintf(allowedFromDT, "%04d-%02d-%02d %02d:%02d:%02d", entry.allowedFromYear, entry.allowedFromMonth, entry.allowedFromDay, entry.allowedFromHour, entry.allowedFromMinute, entry.allowedFromSecond); + jsonEntry["allowedFrom"] = allowedFromDT; + char allowedUntilDT[20]; + sprintf(allowedUntilDT, "%04d-%02d-%02d %02d:%02d:%02d", entry.allowedUntilYear, entry.allowedUntilMonth, entry.allowedUntilDay, entry.allowedUntilHour, entry.allowedUntilMinute, entry.allowedUntilSecond); + jsonEntry["allowedUntil"] = allowedUntilDT; + + uint8_t allowedWeekdaysInt = entry.allowedWeekdays; + JsonArray weekdays = jsonEntry["allowedWeekdays"].to(); + + while(allowedWeekdaysInt > 0) { + if(allowedWeekdaysInt >= 64) + { + weekdays.add("mon"); + allowedWeekdaysInt -= 64; + continue; + } + if(allowedWeekdaysInt >= 32) + { + weekdays.add("tue"); + allowedWeekdaysInt -= 32; + continue; + } + if(allowedWeekdaysInt >= 16) + { + weekdays.add("wed"); + allowedWeekdaysInt -= 16; + continue; + } + if(allowedWeekdaysInt >= 8) + { + weekdays.add("thu"); + allowedWeekdaysInt -= 8; + continue; + } + if(allowedWeekdaysInt >= 4) + { + weekdays.add("fri"); + allowedWeekdaysInt -= 4; + continue; + } + if(allowedWeekdaysInt >= 2) + { + weekdays.add("sat"); + allowedWeekdaysInt -= 2; + continue; + } + if(allowedWeekdaysInt >= 1) + { + weekdays.add("sun"); + allowedWeekdaysInt -= 1; + continue; + } + } + + char allowedFromTimeT[5]; + sprintf(allowedFromTimeT, "%02d:%02d", entry.allowedFromTimeHour, entry.allowedFromTimeMin); + jsonEntry["allowedFromTime"] = allowedFromTimeT; + char allowedUntilTimeT[5]; + sprintf(allowedUntilTimeT, "%02d:%02d", entry.allowedUntilTimeHour, entry.allowedUntilTimeMin); + jsonEntry["allowedUntilTime"] = allowedUntilTimeT; + + if(_preferences->getBool(preference_auth_topic_per_entry, false)) + { + String basePath = mqtt_topic_auth; + basePath.concat("/entries/"); + basePath.concat(std::to_string(index).c_str()); + jsonEntry["index"] = index; + serializeJson(jsonEntry, _buffer, _bufferSize); + publishString(basePath.c_str(), _buffer, true); + + String basePathPrefix = "~"; + basePathPrefix.concat(basePath); + const char *basePathPrefixChr = basePathPrefix.c_str(); + + std::string baseCommand = std::string("{ \"action\": \"update\", \"authId\": \"") + std::to_string(entry.authId); + std::string enaCommand = baseCommand + (char*)"\", \"enabled\": \"1\" }"; + std::string disCommand = baseCommand + (char*)"\", \"enabled\": \"0\" }"; + std::string mqttDeviceName = std::string("auth_") + std::to_string(index); + std::string uidStringPostfix = std::string("_") + mqttDeviceName; + std::string displayName = std::string("Authorization - ") + std::to_string(entry.authId); + + _network->publishHassTopic("switch", + mqttDeviceName.c_str(), + uidString, + uidStringPostfix.c_str(), + displayName.c_str(), + _nukiName, + baseTopic.c_str(), + String("~") + basePath.c_str(), + (char*)"SmartLock", + "", + "", + "diagnostic", + String("~") + mqtt_topic_auth_action, + { { (char*)"json_attr_t", (char*)basePathPrefixChr }, + { (char*)"pl_on", (char*)enaCommand.c_str() }, + { (char*)"pl_off", (char*)disCommand.c_str() }, + { (char*)"val_tpl", (char*)"{{value_json.enabled}}" }, + { (char*)"stat_on", (char*)"1" }, + { (char*)"stat_off", (char*)"0" }}); + } + + ++index; + } + + serializeJson(json, _buffer, _bufferSize); + publishString(mqtt_topic_auth_json, _buffer, true); + + for(int j=authEntries.size(); jremoveTopic(entriesTopic, (char*)std::to_string(j).c_str()); + std::string mqttDeviceName = std::string("auth_") + std::to_string(j); + _network->removeHassTopic((char*)"switch", (char*)mqttDeviceName.c_str(), uidString); + } +} + void NukiNetworkLock::publishConfigCommandResult(const char* result) { publishString(mqtt_topic_config_action_command_result, result, true); @@ -1283,6 +1446,11 @@ void NukiNetworkLock::publishTimeControlCommandResult(const char* result) publishString(mqtt_topic_timecontrol_command_result, result, true); } +void NukiNetworkLock::publishAuthCommandResult(const char* result) +{ + publishString(mqtt_topic_auth_command_result, result, true); +} + void NukiNetworkLock::publishStatusUpdated(const bool statusUpdated) { publishBool(mqtt_topic_lock_status_updated, statusUpdated, true); @@ -1319,6 +1487,11 @@ void NukiNetworkLock::setTimeControlCommandReceivedCallback(void (*timeControlCo _timeControlCommandReceivedReceivedCallback = timeControlCommandReceivedReceivedCallback; } +void NukiNetworkLock::setAuthCommandReceivedCallback(void (*authCommandReceivedReceivedCallback)(const char *)) +{ + _authCommandReceivedReceivedCallback = authCommandReceivedReceivedCallback; +} + void NukiNetworkLock::buildMqttPath(const char* path, char* outPath, bool offPath) { int offset = 0; diff --git a/src/NukiNetworkLock.h b/src/NukiNetworkLock.h index d5da822..f15f2dd 100644 --- a/src/NukiNetworkLock.h +++ b/src/NukiNetworkLock.h @@ -39,11 +39,13 @@ public: void removeHASSConfig(char* uidString); void publishKeypad(const std::list& entries, uint maxKeypadCodeCount); void publishTimeControl(const std::list& timeControlEntries, uint maxTimeControlEntryCount); + void publishAuth(const std::list& authEntries, uint maxAuthEntryCount); void publishStatusUpdated(const bool statusUpdated); void publishConfigCommandResult(const char* result); void publishKeypadCommandResult(const char* result); void publishKeypadJsonCommandResult(const char* result); void publishTimeControlCommandResult(const char* result); + void publishAuthCommandResult(const char* result); void publishOffAction(const int value); void setLockActionReceivedCallback(LockActionResult (*lockActionReceivedCallback)(const char* value)); @@ -52,6 +54,7 @@ public: void setKeypadCommandReceivedCallback(void (*keypadCommandReceivedReceivedCallback)(const char* command, const uint& id, const String& name, const String& code, const int& enabled)); void setKeypadJsonCommandReceivedCallback(void (*keypadJsonCommandReceivedReceivedCallback)(const char* value)); void setTimeControlCommandReceivedCallback(void (*timeControlCommandReceivedReceivedCallback)(const char* value)); + void setAuthCommandReceivedCallback(void (*authCommandReceivedReceivedCallback)(const char* value)); void onMqttDataReceived(const char* topic, byte* payload, const unsigned int length) override; void publishFloat(const char* topic, const float value, bool retain, const uint8_t precision = 2); @@ -132,4 +135,5 @@ private: void (*_keypadCommandReceivedReceivedCallback)(const char* command, const uint& id, const String& name, const String& code, const int& enabled) = nullptr; void (*_keypadJsonCommandReceivedReceivedCallback)(const char* value) = nullptr; void (*_timeControlCommandReceivedReceivedCallback)(const char* value) = nullptr; + void (*_authCommandReceivedReceivedCallback)(const char* value) = nullptr; }; \ No newline at end of file diff --git a/src/NukiNetworkOpener.cpp b/src/NukiNetworkOpener.cpp index b2173a8..9408019 100644 --- a/src/NukiNetworkOpener.cpp +++ b/src/NukiNetworkOpener.cpp @@ -69,7 +69,7 @@ void NukiNetworkOpener::initialize() _network->removeTopic(_mqttPath, mqtt_topic_battery_keypad_critical); //_network->removeTopic(_mqttPath, mqtt_topic_presence); } - + if(!_preferences->getBool(preference_conf_info_enabled, true)) { _network->removeTopic(_mqttPath, mqtt_topic_config_basic_json); @@ -110,6 +110,12 @@ void NukiNetworkOpener::initialize() _network->initTopic(_mqttPath, mqtt_topic_timecontrol_action, "--"); } + if(_preferences->getBool(preference_auth_control_enabled)) + { + _network->subscribe(_mqttPath, mqtt_topic_auth_action); + _network->initTopic(_mqttPath, mqtt_topic_auth_action, "--"); + } + if(_preferences->getBool(preference_publish_authdata, false)) { _network->subscribe(_mqttPath, mqtt_topic_lock_log_rolling_last); @@ -281,6 +287,18 @@ void NukiNetworkOpener::onMqttDataReceived(const char* topic, byte* payload, con publishString(mqtt_topic_timecontrol_action, "--", true); } + + if(comparePrefixedPath(topic, mqtt_topic_auth_action)) + { + if(strcmp(value, "") == 0 || strcmp(value, "--") == 0) return; + + if(_authCommandReceivedReceivedCallback != NULL) + { + _authCommandReceivedReceivedCallback(value); + } + + publishString(mqtt_topic_auth_action, "--", true); + } } void NukiNetworkOpener::publishKeyTurnerState(const NukiOpener::OpenerState& keyTurnerState, const NukiOpener::OpenerState& lastKeyTurnerState) @@ -501,7 +519,7 @@ void NukiNetworkOpener::publishAuthorizationInfo(const std::list _lastRollingLog) { _lastRollingLog = log.index; @@ -654,7 +672,7 @@ void NukiNetworkOpener::publishConfig(const NukiOpener::Config &config) itoa(config.nukiId, uidString, 16); JsonDocument json; - + memset(_nukiName, 0, sizeof(_nukiName)); memcpy(_nukiName, config.name, sizeof(config.name)); @@ -1000,7 +1018,7 @@ void NukiNetworkOpener::publishKeypad(const std::list& en _network->removeTopic(codeTopic, "createdSec"); _network->removeTopic(codeTopic, "lockCount"); } - + for(int j=entries.size(); jpublishHassTopic("switch", mqttDeviceName.c_str(), uidString, @@ -1124,13 +1141,13 @@ void NukiNetworkOpener::publishTimeControl(const std::list& authEntries, uint maxAuthEntryCount) +{ + uint index = 0; + char str[50]; + char uidString[20]; + itoa(_preferences->getUInt(preference_nuki_id_opener, 0), uidString, 16); + String baseTopic = _preferences->getString(preference_mqtt_opener_path); + JsonDocument json; + + for(const auto& entry : authEntries) + { + auto jsonEntry = json.add(); + + jsonEntry["authId"] = entry.authId; + jsonEntry["idType"] = entry.idType; //CONSIDER INT TO STRING + jsonEntry["enabled"] = entry.enabled; + jsonEntry["name"] = entry.name; + jsonEntry["remoteAllowed"] = entry.remoteAllowed; + char createdDT[20]; + sprintf(createdDT, "%04d-%02d-%02d %02d:%02d:%02d", entry.createdYear, entry.createdMonth, entry.createdDay, entry.createdHour, entry.createdMinute, entry.createdSecond); + jsonEntry["dateCreated"] = createdDT; + jsonEntry["lockCount"] = entry.lockCount; + char lastActiveDT[20]; + sprintf(lastActiveDT, "%04d-%02d-%02d %02d:%02d:%02d", entry.lastActYear, entry.lastActMonth, entry.lastActDay, entry.lastActHour, entry.lastActMinute, entry.lastActSecond); + jsonEntry["dateLastActive"] = lastActiveDT; + jsonEntry["timeLimited"] = entry.timeLimited; + char allowedFromDT[20]; + sprintf(allowedFromDT, "%04d-%02d-%02d %02d:%02d:%02d", entry.allowedFromYear, entry.allowedFromMonth, entry.allowedFromDay, entry.allowedFromHour, entry.allowedFromMinute, entry.allowedFromSecond); + jsonEntry["allowedFrom"] = allowedFromDT; + char allowedUntilDT[20]; + sprintf(allowedUntilDT, "%04d-%02d-%02d %02d:%02d:%02d", entry.allowedUntilYear, entry.allowedUntilMonth, entry.allowedUntilDay, entry.allowedUntilHour, entry.allowedUntilMinute, entry.allowedUntilSecond); + jsonEntry["allowedUntil"] = allowedUntilDT; + + uint8_t allowedWeekdaysInt = entry.allowedWeekdays; + JsonArray weekdays = jsonEntry["allowedWeekdays"].to(); + + while(allowedWeekdaysInt > 0) { + if(allowedWeekdaysInt >= 64) + { + weekdays.add("mon"); + allowedWeekdaysInt -= 64; + continue; + } + if(allowedWeekdaysInt >= 32) + { + weekdays.add("tue"); + allowedWeekdaysInt -= 32; + continue; + } + if(allowedWeekdaysInt >= 16) + { + weekdays.add("wed"); + allowedWeekdaysInt -= 16; + continue; + } + if(allowedWeekdaysInt >= 8) + { + weekdays.add("thu"); + allowedWeekdaysInt -= 8; + continue; + } + if(allowedWeekdaysInt >= 4) + { + weekdays.add("fri"); + allowedWeekdaysInt -= 4; + continue; + } + if(allowedWeekdaysInt >= 2) + { + weekdays.add("sat"); + allowedWeekdaysInt -= 2; + continue; + } + if(allowedWeekdaysInt >= 1) + { + weekdays.add("sun"); + allowedWeekdaysInt -= 1; + continue; + } + } + + char allowedFromTimeT[5]; + sprintf(allowedFromTimeT, "%02d:%02d", entry.allowedFromTimeHour, entry.allowedFromTimeMin); + jsonEntry["allowedFromTime"] = allowedFromTimeT; + char allowedUntilTimeT[5]; + sprintf(allowedUntilTimeT, "%02d:%02d", entry.allowedUntilTimeHour, entry.allowedUntilTimeMin); + jsonEntry["allowedUntilTime"] = allowedUntilTimeT; + + if(_preferences->getBool(preference_auth_topic_per_entry, false)) + { + String basePath = mqtt_topic_auth; + basePath.concat("/entries/"); + basePath.concat(std::to_string(index).c_str()); + jsonEntry["index"] = index; + serializeJson(jsonEntry, _buffer, _bufferSize); + publishString(basePath.c_str(), _buffer, true); + + String basePathPrefix = "~"; + basePathPrefix.concat(basePath); + const char *basePathPrefixChr = basePathPrefix.c_str(); + + std::string baseCommand = std::string("{ \"action\": \"update\", \"authId\": \"") + std::to_string(entry.authId); + std::string enaCommand = baseCommand + (char*)"\", \"enabled\": \"1\" }"; + std::string disCommand = baseCommand + (char*)"\", \"enabled\": \"0\" }"; + std::string mqttDeviceName = std::string("auth_") + std::to_string(index); + std::string uidStringPostfix = std::string("_") + mqttDeviceName; + std::string displayName = std::string("Authorization - ") + std::to_string(entry.authId); + + _network->publishHassTopic("switch", + mqttDeviceName.c_str(), + uidString, + uidStringPostfix.c_str(), + displayName.c_str(), + _nukiName, + baseTopic.c_str(), + String("~") + basePath.c_str(), + (char*)"Opener", + "", + "", + "diagnostic", + String("~") + mqtt_topic_auth_action, + { { (char*)"json_attr_t", (char*)basePathPrefixChr }, + { (char*)"pl_on", (char*)enaCommand.c_str() }, + { (char*)"pl_off", (char*)disCommand.c_str() }, + { (char*)"val_tpl", (char*)"{{value_json.enabled}}" }, + { (char*)"stat_on", (char*)"1" }, + { (char*)"stat_off", (char*)"0" }}); + } + + ++index; + } + + serializeJson(json, _buffer, _bufferSize); + publishString(mqtt_topic_auth_json, _buffer, true); + + for(int j=authEntries.size(); jremoveTopic(entriesTopic, (char*)std::to_string(j).c_str()); + std::string mqttDeviceName = std::string("auth_") + std::to_string(j); + _network->removeHassTopic((char*)"switch", (char*)mqttDeviceName.c_str(), uidString); + } +} + void NukiNetworkOpener::publishConfigCommandResult(const char* result) { publishString(mqtt_topic_config_action_command_result, result, true); @@ -1163,6 +1326,11 @@ void NukiNetworkOpener::publishTimeControlCommandResult(const char* result) publishString(mqtt_topic_timecontrol_command_result, result, true); } +void NukiNetworkOpener::publishAuthCommandResult(const char* result) +{ + publishString(mqtt_topic_auth_command_result, result, true); +} + void NukiNetworkOpener::publishStatusUpdated(const bool statusUpdated) { publishBool(mqtt_topic_lock_status_updated, statusUpdated, true); @@ -1194,6 +1362,11 @@ void NukiNetworkOpener::setTimeControlCommandReceivedCallback(void (*timeControl _timeControlCommandReceivedReceivedCallback = timeControlCommandReceivedReceivedCallback; } +void NukiNetworkOpener::setAuthCommandReceivedCallback(void (*authCommandReceivedReceivedCallback)(const char *)) +{ + _authCommandReceivedReceivedCallback = authCommandReceivedReceivedCallback; +} + void NukiNetworkOpener::publishFloat(const char *topic, const float value, bool retain, const uint8_t precision) { _network->publishFloat(_mqttPath, topic, value, retain, precision); diff --git a/src/NukiNetworkOpener.h b/src/NukiNetworkOpener.h index be1cb31..218ba03 100644 --- a/src/NukiNetworkOpener.h +++ b/src/NukiNetworkOpener.h @@ -33,17 +33,20 @@ public: void removeHASSConfig(char* uidString); void publishKeypad(const std::list& entries, uint maxKeypadCodeCount); void publishTimeControl(const std::list& timeControlEntries, uint maxTimeControlEntryCount); + void publishAuth(const std::list& authEntries, uint maxAuthEntryCount); void publishStatusUpdated(const bool statusUpdated); void publishConfigCommandResult(const char* result); void publishKeypadCommandResult(const char* result); void publishKeypadJsonCommandResult(const char* result); void publishTimeControlCommandResult(const char* result); + void publishAuthCommandResult(const char* result); void setLockActionReceivedCallback(LockActionResult (*lockActionReceivedCallback)(const char* value)); void setConfigUpdateReceivedCallback(void (*configUpdateReceivedCallback)(const char* value)); void setKeypadCommandReceivedCallback(void (*keypadCommandReceivedReceivedCallback)(const char* command, const uint& id, const String& name, const String& code, const int& enabled)); void setKeypadJsonCommandReceivedCallback(void (*keypadJsonCommandReceivedReceivedCallback)(const char* value)); void setTimeControlCommandReceivedCallback(void (*timeControlCommandReceivedReceivedCallback)(const char* value)); + void setAuthCommandReceivedCallback(void (*authCommandReceivedReceivedCallback)(const char* value)); void onMqttDataReceived(const char* topic, byte* payload, const unsigned int length) override; bool reconnected(); @@ -105,4 +108,5 @@ private: void (*_keypadCommandReceivedReceivedCallback)(const char* command, const uint& id, const String& name, const String& code, const int& enabled) = nullptr; void (*_keypadJsonCommandReceivedReceivedCallback)(const char* value) = nullptr; void (*_timeControlCommandReceivedReceivedCallback)(const char* value) = nullptr; + void (*_authCommandReceivedReceivedCallback)(const char* value) = nullptr; }; diff --git a/src/NukiOpenerWrapper.cpp b/src/NukiOpenerWrapper.cpp index 65d457e..34448e9 100644 --- a/src/NukiOpenerWrapper.cpp +++ b/src/NukiOpenerWrapper.cpp @@ -33,6 +33,8 @@ NukiOpenerWrapper::NukiOpenerWrapper(const std::string& deviceName, NukiDeviceId network->setConfigUpdateReceivedCallback(nukiOpenerInst->onConfigUpdateReceivedCallback); if(_preferences->getBool(preference_disable_non_json, false)) network->setKeypadCommandReceivedCallback(nukiOpenerInst->onKeypadCommandReceivedCallback); network->setKeypadJsonCommandReceivedCallback(nukiOpenerInst->onKeypadJsonCommandReceivedCallback); + network->setTimeControlCommandReceivedCallback(nukiOpenerInst->onTimeControlCommandReceivedCallback); + network->setAuthCommandReceivedCallback(nukiOpenerInst->onAuthCommandReceivedCallback); _gpio->addCallback(NukiOpenerWrapper::gpioActionCallback); } @@ -72,6 +74,7 @@ void NukiOpenerWrapper::initialize() _publishAuthData = _preferences->getBool(preference_publish_authdata); _maxKeypadCodeCount = _preferences->getUInt(preference_opener_max_keypad_code_count); _maxTimeControlEntryCount = _preferences->getUInt(preference_opener_max_timecontrol_entry_count); + _maxAuthEntryCount = _preferences->getUInt(preference_opener_max_auth_entry_count); _restartBeaconTimeout = _preferences->getInt(preference_restart_ble_beacon_lost); _hassEnabled = _preferences->getString(preference_mqtt_hass_discovery) != ""; _nrOfRetries = _preferences->getInt(preference_command_nr_of_retries, 200); @@ -219,6 +222,11 @@ void NukiOpenerWrapper::update() _waitTimeControlUpdateTs = 0; updateTimeControl(true); } + if(_waitAuthUpdateTs != 0 && ts > _waitAuthUpdateTs) + { + _waitAuthUpdateTs = 0; + updateAuth(true); + } if(_hassEnabled && _nukiConfigValid && _nukiAdvancedConfigValid && _network->reconnected()) { setupHASS(); @@ -492,6 +500,7 @@ void NukiOpenerWrapper::updateConfig() if(_preferences->getBool(preference_conf_info_enabled, true)) _network->publishConfig(_nukiConfig); _retryConfigCount = 0; if(_preferences->getBool(preference_timecontrol_info_enabled, false)) updateTimeControl(false); + if(_preferences->getBool(preference_auth_info_enabled)) updateAuth(false); const int pinStatus = _preferences->getInt(preference_opener_pin_status, 4); @@ -729,7 +738,7 @@ void NukiOpenerWrapper::updateTimeControl(bool retrieved) while(_retryCount < _nrOfRetries + 1) { - Log->print(F("Querying opener time control: ")); + Log->print(F("Querying opener timecontrol: ")); result = _nukiOpener.retrieveTimeControlEntries(); if(result != Nuki::CmdResult::Success) { @@ -749,7 +758,7 @@ void NukiOpenerWrapper::updateTimeControl(bool retrieved) std::list timeControlEntries; _nukiOpener.getTimeControlEntries(&timeControlEntries); - Log->print(F("Opener time control entries: ")); + Log->print(F("Opener timecontrol entries: ")); Log->println(timeControlEntries.size()); timeControlEntries.sort([](const NukiOpener::TimeControlEntry& a, const NukiOpener::TimeControlEntry& b) { return a.entryId < b.entryId; }); @@ -779,6 +788,67 @@ void NukiOpenerWrapper::updateTimeControl(bool retrieved) postponeBleWatchdog(); } +void NukiOpenerWrapper::updateAuth(bool retrieved) +{ + if(!_preferences->getBool(preference_auth_info_enabled)) return; + + if(!retrieved) + { + Nuki::CmdResult result = (Nuki::CmdResult)-1; + _retryCount = 0; + + while(_retryCount < _nrOfRetries) + { + Log->print(F("Querying opener authorization: ")); + result = _nukiOpener.retrieveAuthorizationEntries(0, _preferences->getInt(preference_auth_max_entries, MAX_AUTH)); + delay(250); + if(result != Nuki::CmdResult::Success) { + ++_retryCount; + } + else break; + } + + printCommandResult(result); + if(result == Nuki::CmdResult::Success) + { + _waitAuthUpdateTs = millis() + 5000; + } + } + else + { + std::list authEntries; + _nukiOpener.getAuthorizationEntries(&authEntries); + + Log->print(F("Opener authorization entries: ")); + Log->println(authEntries.size()); + + authEntries.sort([](const NukiOpener::AuthorizationEntry& a, const NukiOpener::AuthorizationEntry& b) { return a.authId < b.authId; }); + + if(authEntries.size() > _preferences->getInt(preference_auth_max_entries, MAX_AUTH)) + { + authEntries.resize(_preferences->getInt(preference_auth_max_entries, MAX_AUTH)); + } + + uint authCount = authEntries.size(); + if(authCount > _maxAuthEntryCount) + { + _maxAuthEntryCount = authCount; + _preferences->putUInt(preference_opener_max_auth_entry_count, _maxAuthEntryCount); + } + + _network->publishAuth(authEntries, _maxAuthEntryCount); + + _authIds.clear(); + _authIds.reserve(authEntries.size()); + for(const auto& entry : authEntries) + { + _authIds.push_back(entry.authId); + } + } + + postponeBleWatchdog(); +} + void NukiOpenerWrapper::postponeBleWatchdog() { _disableBleWatchdogTs = (esp_timer_get_time() / 1000) + 15000; @@ -1477,6 +1547,16 @@ void NukiOpenerWrapper::onKeypadJsonCommandReceivedCallback(const char *value) nukiOpenerInst->onKeypadJsonCommandReceived(value); } +void NukiOpenerWrapper::onTimeControlCommandReceivedCallback(const char *value) +{ + nukiOpenerInst->onTimeControlCommandReceived(value); +} + +void NukiOpenerWrapper::onAuthCommandReceivedCallback(const char *value) +{ + nukiOpenerInst->onAuthCommandReceived(value); +} + void NukiOpenerWrapper::gpioActionCallback(const GpioAction &action, const int& pin) { switch(action) @@ -2161,7 +2241,7 @@ void NukiOpenerWrapper::onTimeControlCommandReceived(const char *value) if(idExists) { result = _nukiOpener.removeTimeControlEntry(entryId); - Log->print(F("Delete time control: ")); + Log->print(F("Delete timecontrol: ")); Log->println((int)result); } else @@ -2219,7 +2299,7 @@ void NukiOpenerWrapper::onTimeControlCommandReceived(const char *value) entry.lockAction = timeControlLockAction; result = _nukiOpener.addTimeControlEntry(entry); - Log->print(F("Add time control: ")); + Log->print(F("Add timecontrol: ")); Log->println((int)result); } else if (strcmp(action, "update") == 0) @@ -2257,13 +2337,13 @@ void NukiOpenerWrapper::onTimeControlCommandReceived(const char *value) if(!foundExisting) { - _network->publishTimeControlCommandResult("failedToRetrieveExistingKeypadEntry"); + _network->publishTimeControlCommandResult("failedToRetrieveExistingTimeControlEntry"); return; } } else { - _network->publishTimeControlCommandResult("failedToRetrieveExistingKeypadEntry"); + _network->publishTimeControlCommandResult("failedToRetrieveExistingTimeControlEntry"); return; } @@ -2281,7 +2361,7 @@ void NukiOpenerWrapper::onTimeControlCommandReceived(const char *value) entry.lockAction = timeControlLockAction; result = _nukiOpener.updateTimeControlEntry(entry); - Log->print(F("Update time control: ")); + Log->print(F("Update timecontrol: ")); Log->println((int)result); } } @@ -2314,6 +2394,464 @@ void NukiOpenerWrapper::onTimeControlCommandReceived(const char *value) } } +void NukiOpenerWrapper::onAuthCommandReceived(const char *value) +{ + if(!_nukiConfigValid) + { + _network->publishAuthCommandResult("configNotReady"); + return; + } + + if(!isPinValid()) + { + _network->publishAuthCommandResult("noValidPinSet"); + return; + } + + if(!_preferences->getBool(preference_auth_control_enabled)) + { + _network->publishAuthCommandResult("keypadControlDisabled"); + return; + } + + JsonDocument json; + DeserializationError jsonError = deserializeJson(json, value); + + if(jsonError) + { + _network->publishAuthCommandResult("invalidJson"); + return; + } + + char oldName[33]; + const char *action = json["action"].as(); + uint16_t authId = json["authId"].as(); + //uint8_t idType = json["idType"].as(); + //unsigned char secretKeyK[32] = {0x00}; + uint8_t remoteAllowed; + uint8_t enabled; + uint8_t timeLimited; + String name; + //String sharedKey; + String allowedFrom; + String allowedUntil; + String allowedWeekdays; + String allowedFromTime; + String allowedUntilTime; + + if(json.containsKey("remoteAllowed")) remoteAllowed = json["remoteAllowed"].as(); + else remoteAllowed = 2; + + if(json.containsKey("enabled")) enabled = json["enabled"].as(); + else enabled = 2; + + if(json.containsKey("timeLimited")) timeLimited = json["timeLimited"].as(); + else timeLimited = 2; + + if(json.containsKey("name")) name = json["name"].as(); + //if(json.containsKey("sharedKey")) sharedKey = json["sharedKey"].as(); + if(json.containsKey("allowedFrom")) allowedFrom = json["allowedFrom"].as(); + if(json.containsKey("allowedUntil")) allowedUntil = json["allowedUntil"].as(); + if(json.containsKey("allowedWeekdays")) allowedWeekdays = json["allowedWeekdays"].as(); + if(json.containsKey("allowedFromTime")) allowedFromTime = json["allowedFromTime"].as(); + if(json.containsKey("allowedUntilTime")) allowedUntilTime = json["allowedUntilTime"].as(); + + if(action) + { + bool idExists = false; + + if(authId) + { + idExists = std::find(_authIds.begin(), _authIds.end(), authId) != _authIds.end(); + } + + Nuki::CmdResult result = (Nuki::CmdResult)-1; + _retryCount = 0; + + while(_retryCount < _nrOfRetries) + { + if(strcmp(action, "delete") == 0) { + if(idExists) + { + result = _nukiOpener.deleteAuthorizationEntry(authId); + Log->print(F("Delete authorization: ")); + Log->println((int)result); + } + else + { + _network->publishAuthCommandResult("noExistingAuthIdSet"); + return; + } + } + else if(strcmp(action, "add") == 0 || strcmp(action, "update") == 0) + { + if(name.length() < 1) + { + if (strcmp(action, "update") != 0) + { + _network->publishAuthCommandResult("noNameSet"); + return; + } + } + + /* + if(sharedKey.length() != 64) + { + if (strcmp(action, "update") != 0) + { + _network->publishAuthCommandResult("noSharedKeySet"); + return; + } + } + else + { + for(int i=0; i 0) + { + if(allowedFrom.length() == 19) + { + allowedFromAr[0] = (uint16_t)allowedFrom.substring(0, 4).toInt(); + allowedFromAr[1] = (uint8_t)allowedFrom.substring(5, 7).toInt(); + allowedFromAr[2] = (uint8_t)allowedFrom.substring(8, 10).toInt(); + allowedFromAr[3] = (uint8_t)allowedFrom.substring(11, 13).toInt(); + allowedFromAr[4] = (uint8_t)allowedFrom.substring(14, 16).toInt(); + allowedFromAr[5] = (uint8_t)allowedFrom.substring(17, 19).toInt(); + + if(allowedFromAr[0] < 2000 || allowedFromAr[0] > 3000 || allowedFromAr[1] < 1 || allowedFromAr[1] > 12 || allowedFromAr[2] < 1 || allowedFromAr[2] > 31 || allowedFromAr[3] < 0 || allowedFromAr[3] > 23 || allowedFromAr[4] < 0 || allowedFromAr[4] > 59 || allowedFromAr[5] < 0 || allowedFromAr[5] > 59) + { + _network->publishAuthCommandResult("invalidAllowedFrom"); + return; + } + } + else + { + _network->publishAuthCommandResult("invalidAllowedFrom"); + return; + } + } + + if(allowedUntil.length() > 0) + { + if(allowedUntil.length() > 0 == 19) + { + allowedUntilAr[0] = (uint16_t)allowedUntil.substring(0, 4).toInt(); + allowedUntilAr[1] = (uint8_t)allowedUntil.substring(5, 7).toInt(); + allowedUntilAr[2] = (uint8_t)allowedUntil.substring(8, 10).toInt(); + allowedUntilAr[3] = (uint8_t)allowedUntil.substring(11, 13).toInt(); + allowedUntilAr[4] = (uint8_t)allowedUntil.substring(14, 16).toInt(); + allowedUntilAr[5] = (uint8_t)allowedUntil.substring(17, 19).toInt(); + + if(allowedUntilAr[0] < 2000 || allowedUntilAr[0] > 3000 || allowedUntilAr[1] < 1 || allowedUntilAr[1] > 12 || allowedUntilAr[2] < 1 || allowedUntilAr[2] > 31 || allowedUntilAr[3] < 0 || allowedUntilAr[3] > 23 || allowedUntilAr[4] < 0 || allowedUntilAr[4] > 59 || allowedUntilAr[5] < 0 || allowedUntilAr[5] > 59) + { + _network->publishAuthCommandResult("invalidAllowedUntil"); + return; + } + } + else + { + _network->publishAuthCommandResult("invalidAllowedUntil"); + return; + } + } + + if(allowedFromTime.length() > 0) + { + if(allowedFromTime.length() == 5) + { + allowedFromTimeAr[0] = (uint8_t)allowedFromTime.substring(0, 2).toInt(); + allowedFromTimeAr[1] = (uint8_t)allowedFromTime.substring(3, 5).toInt(); + + if(allowedFromTimeAr[0] < 0 || allowedFromTimeAr[0] > 23 || allowedFromTimeAr[1] < 0 || allowedFromTimeAr[1] > 59) + { + _network->publishAuthCommandResult("invalidAllowedFromTime"); + return; + } + } + else + { + _network->publishAuthCommandResult("invalidAllowedFromTime"); + return; + } + } + + if(allowedUntilTime.length() > 0) + { + if(allowedUntilTime.length() == 5) + { + allowedUntilTimeAr[0] = (uint8_t)allowedUntilTime.substring(0, 2).toInt(); + allowedUntilTimeAr[1] = (uint8_t)allowedUntilTime.substring(3, 5).toInt(); + + if(allowedUntilTimeAr[0] < 0 || allowedUntilTimeAr[0] > 23 || allowedUntilTimeAr[1] < 0 || allowedUntilTimeAr[1] > 59) + { + _network->publishAuthCommandResult("invalidAllowedUntilTime"); + return; + } + } + else + { + _network->publishAuthCommandResult("invalidAllowedUntilTime"); + return; + } + } + + if(allowedWeekdays.indexOf("mon") >= 0) allowedWeekdaysInt += 64; + if(allowedWeekdays.indexOf("tue") >= 0) allowedWeekdaysInt += 32; + if(allowedWeekdays.indexOf("wed") >= 0) allowedWeekdaysInt += 16; + if(allowedWeekdays.indexOf("thu") >= 0) allowedWeekdaysInt += 8; + if(allowedWeekdays.indexOf("fri") >= 0) allowedWeekdaysInt += 4; + if(allowedWeekdays.indexOf("sat") >= 0) allowedWeekdaysInt += 2; + if(allowedWeekdays.indexOf("sun") >= 0) allowedWeekdaysInt += 1; + } + + if(strcmp(action, "add") == 0) + { + _network->publishAuthCommandResult("addActionNotSupported"); + return; + + NukiOpener::NewAuthorizationEntry entry; + memset(&entry, 0, sizeof(entry)); + size_t nameLen = name.length(); + memcpy(&entry.name, name.c_str(), nameLen > 32 ? 32 : nameLen); + /* + memcpy(&entry.sharedKey, secretKeyK, 32); + + if(idType != 1) + { + _network->publishAuthCommandResult("invalidIdType"); + return; + } + + entry.idType = idType; + */ + entry.remoteAllowed = remoteAllowed == 1 ? 1 : 0; + entry.timeLimited = timeLimited == 1 ? 1 : 0; + + if(allowedFrom.length() > 0) + { + entry.allowedFromYear = allowedFromAr[0]; + entry.allowedFromMonth = allowedFromAr[1]; + entry.allowedFromDay = allowedFromAr[2]; + entry.allowedFromHour = allowedFromAr[3]; + entry.allowedFromMinute = allowedFromAr[4]; + entry.allowedFromSecond = allowedFromAr[5]; + } + + if(allowedUntil.length() > 0) + { + entry.allowedUntilYear = allowedUntilAr[0]; + entry.allowedUntilMonth = allowedUntilAr[1]; + entry.allowedUntilDay = allowedUntilAr[2]; + entry.allowedUntilHour = allowedUntilAr[3]; + entry.allowedUntilMinute = allowedUntilAr[4]; + entry.allowedUntilSecond = allowedUntilAr[5]; + } + + entry.allowedWeekdays = allowedWeekdaysInt; + + if(allowedFromTime.length() > 0) + { + entry.allowedFromTimeHour = allowedFromTimeAr[0]; + entry.allowedFromTimeMin = allowedFromTimeAr[1]; + } + + if(allowedUntilTime.length() > 0) + { + entry.allowedUntilTimeHour = allowedUntilTimeAr[0]; + entry.allowedUntilTimeMin = allowedUntilTimeAr[1]; + } + + result = _nukiOpener.addAuthorizationEntry(entry); + Log->print(F("Add authorization: ")); + Log->println((int)result); + } + else if (strcmp(action, "update") == 0) + { + if(!authId) + { + _network->publishAuthCommandResult("noAuthIdSet"); + return; + } + + if(!idExists) + { + _network->publishAuthCommandResult("noExistingAuthIdSet"); + return; + } + + Nuki::CmdResult resultAuth = _nukiOpener.retrieveAuthorizationEntries(0, _preferences->getInt(preference_auth_max_entries, MAX_AUTH)); + bool foundExisting = false; + + if(resultAuth == Nuki::CmdResult::Success) + { + delay(250); + std::list entries; + _nukiOpener.getAuthorizationEntries(&entries); + + for(const auto& entry : entries) + { + if (authId != entry.authId) continue; + else foundExisting = true; + + if(name.length() < 1) + { + memset(oldName, 0, sizeof(oldName)); + memcpy(oldName, entry.name, sizeof(entry.name)); + } + if(remoteAllowed == 2) remoteAllowed = entry.remoteAllowed; + if(enabled == 2) enabled = entry.enabled; + if(timeLimited == 2) timeLimited = entry.timeLimited; + if(allowedFrom.length() < 1) + { + allowedFrom = "old"; + allowedFromAr[0] = entry.allowedFromYear; + allowedFromAr[1] = entry.allowedFromMonth; + allowedFromAr[2] = entry.allowedFromDay; + allowedFromAr[3] = entry.allowedFromHour; + allowedFromAr[4] = entry.allowedFromMinute; + allowedFromAr[5] = entry.allowedFromSecond; + } + if(allowedUntil.length() < 1) + { + allowedUntil = "old"; + allowedUntilAr[0] = entry.allowedUntilYear; + allowedUntilAr[1] = entry.allowedUntilMonth; + allowedUntilAr[2] = entry.allowedUntilDay; + allowedUntilAr[3] = entry.allowedUntilHour; + allowedUntilAr[4] = entry.allowedUntilMinute; + allowedUntilAr[5] = entry.allowedUntilSecond; + } + if(allowedWeekdays.length() < 1) allowedWeekdaysInt = entry.allowedWeekdays; + if(allowedFromTime.length() < 1) + { + allowedFromTime = "old"; + allowedFromTimeAr[0] = entry.allowedFromTimeHour; + allowedFromTimeAr[1] = entry.allowedFromTimeMin; + } + + if(allowedUntilTime.length() < 1) + { + allowedUntilTime = "old"; + allowedUntilTimeAr[0] = entry.allowedUntilTimeHour; + allowedUntilTimeAr[1] = entry.allowedUntilTimeMin; + } + } + + if(!foundExisting) + { + _network->publishAuthCommandResult("failedToRetrieveExistingAuthorizationEntry"); + return; + } + } + else + { + _network->publishAuthCommandResult("failedToRetrieveExistingAuthorizationEntry"); + return; + } + + NukiOpener::UpdatedAuthorizationEntry entry; + + memset(&entry, 0, sizeof(entry)); + entry.authId = authId; + + if(name.length() < 1) + { + size_t nameLen = strlen(oldName); + memcpy(&entry.name, oldName, nameLen > 20 ? 20 : nameLen); + } + else + { + size_t nameLen = name.length(); + memcpy(&entry.name, name.c_str(), nameLen > 20 ? 20 : nameLen); + } + entry.remoteAllowed = remoteAllowed; + entry.enabled = enabled; + entry.timeLimited = timeLimited; + + if(enabled == 1) + { + if(timeLimited == 1) + { + if(allowedFrom.length() > 0) + { + entry.allowedFromYear = allowedFromAr[0]; + entry.allowedFromMonth = allowedFromAr[1]; + entry.allowedFromDay = allowedFromAr[2]; + entry.allowedFromHour = allowedFromAr[3]; + entry.allowedFromMinute = allowedFromAr[4]; + entry.allowedFromSecond = allowedFromAr[5]; + } + + if(allowedUntil.length() > 0) + { + entry.allowedUntilYear = allowedUntilAr[0]; + entry.allowedUntilMonth = allowedUntilAr[1]; + entry.allowedUntilDay = allowedUntilAr[2]; + entry.allowedUntilHour = allowedUntilAr[3]; + entry.allowedUntilMinute = allowedUntilAr[4]; + entry.allowedUntilSecond = allowedUntilAr[5]; + } + + entry.allowedWeekdays = allowedWeekdaysInt; + + if(allowedFromTime.length() > 0) + { + entry.allowedFromTimeHour = allowedFromTimeAr[0]; + entry.allowedFromTimeMin = allowedFromTimeAr[1]; + } + + if(allowedUntilTime.length() > 0) + { + entry.allowedUntilTimeHour = allowedUntilTimeAr[0]; + entry.allowedUntilTimeMin = allowedUntilTimeAr[1]; + } + } + } + + result = _nukiOpener.updateAuthorizationEntry(entry); + Log->print(F("Update authorization: ")); + Log->println((int)result); + } + } + else + { + _network->publishAuthCommandResult("invalidAction"); + return; + } + + if(result != Nuki::CmdResult::Success) { + ++_retryCount; + } + else break; + } + + updateAuth(false); + + if((int)result != -1) + { + char resultStr[15]; + memset(&resultStr, 0, sizeof(resultStr)); + NukiOpener::cmdResultToString(result, resultStr); + _network->publishAuthCommandResult(resultStr); + } + } + else + { + _network->publishAuthCommandResult("noActionSet"); + return; + } +} + const NukiOpener::OpenerState &NukiOpenerWrapper::keyTurnerState() { return _keyTurnerState; diff --git a/src/NukiOpenerWrapper.h b/src/NukiOpenerWrapper.h index 085da94..0c3952f 100644 --- a/src/NukiOpenerWrapper.h +++ b/src/NukiOpenerWrapper.h @@ -53,12 +53,14 @@ private: static void onKeypadCommandReceivedCallback(const char* command, const uint& id, const String& name, const String& code, const int& enabled); static void onKeypadJsonCommandReceivedCallback(const char* value); static void onTimeControlCommandReceivedCallback(const char* value); + static void onAuthCommandReceivedCallback(const char* value); static void gpioActionCallback(const GpioAction& action, const int& pin); void onKeypadCommandReceived(const char* command, const uint& id, const String& name, const String& code, const int& enabled); void onConfigUpdateReceived(const char* value); void onKeypadJsonCommandReceived(const char* value); void onTimeControlCommandReceived(const char* value); + void onAuthCommandReceived(const char* value); void updateKeyTurnerState(); void updateBatteryState(); @@ -66,6 +68,7 @@ private: void updateAuthData(bool retrieved); void updateKeypad(bool retrieved); void updateTimeControl(bool retrieved); + void updateAuth(bool retrieved); void postponeBleWatchdog(); void updateGpioOutputs(); @@ -109,6 +112,7 @@ private: int64_t _nextRetryTs = 0; std::vector _keypadCodeIds; std::vector _timeControlIds; + std::vector _authIds; NukiOpener::OpenerState _lastKeyTurnerState; NukiOpener::OpenerState _keyTurnerState; @@ -129,6 +133,7 @@ private: bool _keypadEnabled = false; uint _maxKeypadCodeCount = 0; uint _maxTimeControlEntryCount = 0; + uint _maxAuthEntryCount = 0; int _rssiPublishInterval = 0; int64_t _nextLockStateUpdateTs = 0; int64_t _nextBatteryReportTs = 0; @@ -136,6 +141,7 @@ private: int64_t _waitAuthLogUpdateTs = 0; int64_t _waitKeypadUpdateTs = 0; int64_t _waitTimeControlUpdateTs = 0; + int64_t _waitAuthUpdateTs = 0; int64_t _nextKeypadUpdateTs = 0; int64_t _nextPairTs = 0; int64_t _nextRssiTs = 0; diff --git a/src/NukiWrapper.cpp b/src/NukiWrapper.cpp index 4a1d036..02aaec0 100644 --- a/src/NukiWrapper.cpp +++ b/src/NukiWrapper.cpp @@ -37,6 +37,7 @@ NukiWrapper::NukiWrapper(const std::string& deviceName, NukiDeviceId* deviceId, if(_disableNonJSON) network->setKeypadCommandReceivedCallback(nukiInst->onKeypadCommandReceivedCallback); network->setKeypadJsonCommandReceivedCallback(nukiInst->onKeypadJsonCommandReceivedCallback); network->setTimeControlCommandReceivedCallback(nukiInst->onTimeControlCommandReceivedCallback); + network->setAuthCommandReceivedCallback(nukiInst->onAuthCommandReceivedCallback); _gpio->addCallback(NukiWrapper::gpioActionCallback); } @@ -76,6 +77,7 @@ void NukiWrapper::initialize(const bool& firstStart) _publishAuthData = _preferences->getBool(preference_publish_authdata); _maxKeypadCodeCount = _preferences->getUInt(preference_lock_max_keypad_code_count); _maxTimeControlEntryCount = _preferences->getUInt(preference_lock_max_timecontrol_entry_count); + _maxAuthEntryCount = _preferences->getUInt(preference_lock_max_auth_entry_count); _restartBeaconTimeout = _preferences->getInt(preference_restart_ble_beacon_lost); _hassEnabled = _preferences->getString(preference_mqtt_hass_discovery) != ""; _nrOfRetries = _preferences->getInt(preference_command_nr_of_retries, 200); @@ -262,7 +264,6 @@ void NukiWrapper::update() cmdResult = _nukiLock.lockAction(_nextLockAction, 0, 0); char resultStr[15] = {0}; NukiLock::cmdResultToString(cmdResult, resultStr); - _network->publishCommandResult(resultStr); Log->print(F("Lock action result: ")); @@ -344,6 +345,11 @@ void NukiWrapper::update() _waitTimeControlUpdateTs = 0; updateTimeControl(true); } + if(_waitAuthUpdateTs != 0 && ts > _waitAuthUpdateTs) + { + _waitAuthUpdateTs = 0; + updateAuth(true); + } if(_hassEnabled && _nukiConfigValid && _nukiAdvancedConfigValid && _network->reconnected()) { setupHASS(); @@ -446,7 +452,6 @@ void NukiWrapper::updateKeyTurnerState() Log->print(_retryCount + 1); Log->print("): "); result =_nukiLock.requestKeyTurnerState(&_keyTurnerState); - if(result != Nuki::CmdResult::Success) { ++_retryCount; } @@ -566,6 +571,7 @@ void NukiWrapper::updateConfig() _hardwareVersion = std::to_string(_nukiConfig.hardwareRevision[0]) + "." + std::to_string(_nukiConfig.hardwareRevision[1]); if(_preferences->getBool(preference_conf_info_enabled, true)) _network->publishConfig(_nukiConfig); if(_preferences->getBool(preference_timecontrol_info_enabled)) updateTimeControl(false); + if(_preferences->getBool(preference_auth_info_enabled)) updateAuth(false); const int pinStatus = _preferences->getInt(preference_lock_pin_status, 4); @@ -577,7 +583,6 @@ void NukiWrapper::updateConfig() while(_retryCount < _nrOfRetries + 1) { result = _nukiLock.verifySecurityPin(); - if(result != Nuki::CmdResult::Success) { ++_retryCount; } @@ -665,7 +670,6 @@ void NukiWrapper::updateAuthData(bool retrieved) { Log->print(F("Retrieve log entries: ")); result = _nukiLock.retrieveLogEntries(0, _preferences->getInt(preference_authlog_max_entries, MAX_AUTHLOG), 1, false); - if(result != Nuki::CmdResult::Success) { ++_retryCount; } @@ -737,7 +741,6 @@ void NukiWrapper::updateKeypad(bool retrieved) { Log->print(F("Querying lock keypad: ")); result = _nukiLock.retrieveKeypadEntries(0, _preferences->getInt(preference_keypad_max_entries, MAX_KEYPAD)); - if(result != Nuki::CmdResult::Success) { ++_retryCount; } @@ -802,9 +805,8 @@ void NukiWrapper::updateTimeControl(bool retrieved) while(_retryCount < _nrOfRetries + 1) { - Log->print(F("Querying lock time control: ")); + Log->print(F("Querying lock timecontrol: ")); result = _nukiLock.retrieveTimeControlEntries(); - if(result != Nuki::CmdResult::Success) { ++_retryCount; } @@ -822,7 +824,7 @@ void NukiWrapper::updateTimeControl(bool retrieved) std::list timeControlEntries; _nukiLock.getTimeControlEntries(&timeControlEntries); - Log->print(F("Lock time control entries: ")); + Log->print(F("Lock timecontrol entries: ")); Log->println(timeControlEntries.size()); timeControlEntries.sort([](const NukiLock::TimeControlEntry& a, const NukiLock::TimeControlEntry& b) { return a.entryId < b.entryId; }); @@ -852,6 +854,67 @@ void NukiWrapper::updateTimeControl(bool retrieved) postponeBleWatchdog(); } +void NukiWrapper::updateAuth(bool retrieved) +{ + if(!_preferences->getBool(preference_auth_info_enabled)) return; + + if(!retrieved) + { + Nuki::CmdResult result = (Nuki::CmdResult)-1; + _retryCount = 0; + + while(_retryCount < _nrOfRetries) + { + Log->print(F("Querying lock authorization: ")); + result = _nukiLock.retrieveAuthorizationEntries(0, _preferences->getInt(preference_auth_max_entries, MAX_AUTH)); + delay(250); + if(result != Nuki::CmdResult::Success) { + ++_retryCount; + } + else break; + } + + printCommandResult(result); + if(result == Nuki::CmdResult::Success) + { + _waitAuthUpdateTs = millis() + 5000; + } + } + else + { + std::list authEntries; + _nukiLock.getAuthorizationEntries(&authEntries); + + Log->print(F("Lock authorization entries: ")); + Log->println(authEntries.size()); + + authEntries.sort([](const NukiLock::AuthorizationEntry& a, const NukiLock::AuthorizationEntry& b) { return a.authId < b.authId; }); + + if(authEntries.size() > _preferences->getInt(preference_auth_max_entries, MAX_AUTH)) + { + authEntries.resize(_preferences->getInt(preference_auth_max_entries, MAX_AUTH)); + } + + uint authCount = authEntries.size(); + if(authCount > _maxAuthEntryCount) + { + _maxAuthEntryCount = authCount; + _preferences->putUInt(preference_lock_max_auth_entry_count, _maxAuthEntryCount); + } + + _network->publishAuth(authEntries, _maxAuthEntryCount); + + _authIds.clear(); + _authIds.reserve(authEntries.size()); + for(const auto& entry : authEntries) + { + _authIds.push_back(entry.authId); + } + } + + postponeBleWatchdog(); +} + void NukiWrapper::postponeBleWatchdog() { _disableBleWatchdogTs = (esp_timer_get_time() / 1000) + 15000; @@ -1725,6 +1788,11 @@ void NukiWrapper::onTimeControlCommandReceivedCallback(const char *value) nukiInst->onTimeControlCommandReceived(value); } +void NukiWrapper::onAuthCommandReceivedCallback(const char *value) +{ + nukiInst->onAuthCommandReceived(value); +} + void NukiWrapper::gpioActionCallback(const GpioAction &action, const int& pin) { switch(action) @@ -2436,7 +2504,7 @@ void NukiWrapper::onTimeControlCommandReceived(const char *value) if(idExists) { result = _nukiLock.removeTimeControlEntry(entryId); - Log->print(F("Delete time control: ")); + Log->print(F("Delete timecontrol: ")); Log->println((int)result); } else @@ -2495,7 +2563,7 @@ void NukiWrapper::onTimeControlCommandReceived(const char *value) entry.lockAction = timeControlLockAction; result = _nukiLock.addTimeControlEntry(entry); - Log->print(F("Add time control: ")); + Log->print(F("Add timecontrol: ")); Log->println((int)result); } else if (strcmp(action, "update") == 0) @@ -2533,13 +2601,13 @@ void NukiWrapper::onTimeControlCommandReceived(const char *value) if(!foundExisting) { - _network->publishTimeControlCommandResult("failedToRetrieveExistingKeypadEntry"); + _network->publishTimeControlCommandResult("failedToRetrieveExistingTimeControlEntry"); return; } } else { - _network->publishTimeControlCommandResult("failedToRetrieveExistingKeypadEntry"); + _network->publishTimeControlCommandResult("failedToRetrieveExistingTimeControlEntry"); return; } @@ -2558,7 +2626,7 @@ void NukiWrapper::onTimeControlCommandReceived(const char *value) entry.lockAction = timeControlLockAction; result = _nukiLock.updateTimeControlEntry(entry); - Log->print(F("Update time control: ")); + Log->print(F("Update timecontrol: ")); Log->println((int)result); } } @@ -2591,6 +2659,467 @@ void NukiWrapper::onTimeControlCommandReceived(const char *value) } } +void NukiWrapper::onAuthCommandReceived(const char *value) +{ + if(!_nukiConfigValid) + { + _network->publishAuthCommandResult("configNotReady"); + return; + } + + if(!isPinValid()) + { + _network->publishAuthCommandResult("noValidPinSet"); + return; + } + + if(!_preferences->getBool(preference_auth_control_enabled)) + { + _network->publishAuthCommandResult("keypadControlDisabled"); + return; + } + + JsonDocument json; + DeserializationError jsonError = deserializeJson(json, value); + + if(jsonError) + { + _network->publishAuthCommandResult("invalidJson"); + return; + } + + char oldName[33]; + const char *action = json["action"].as(); + uint16_t authId = json["authId"].as(); + //uint8_t idType = json["idType"].as(); + //unsigned char secretKeyK[32] = {0x00}; + uint8_t remoteAllowed; + uint8_t enabled; + uint8_t timeLimited; + String name; + //String sharedKey; + String allowedFrom; + String allowedUntil; + String allowedWeekdays; + String allowedFromTime; + String allowedUntilTime; + + if(json.containsKey("remoteAllowed")) remoteAllowed = json["remoteAllowed"].as(); + else remoteAllowed = 2; + + if(json.containsKey("enabled")) enabled = json["enabled"].as(); + else enabled = 2; + + if(json.containsKey("timeLimited")) timeLimited = json["timeLimited"].as(); + else timeLimited = 2; + + if(json.containsKey("name")) name = json["name"].as(); + //if(json.containsKey("sharedKey")) sharedKey = json["sharedKey"].as(); + if(json.containsKey("allowedFrom")) allowedFrom = json["allowedFrom"].as(); + if(json.containsKey("allowedUntil")) allowedUntil = json["allowedUntil"].as(); + if(json.containsKey("allowedWeekdays")) allowedWeekdays = json["allowedWeekdays"].as(); + if(json.containsKey("allowedFromTime")) allowedFromTime = json["allowedFromTime"].as(); + if(json.containsKey("allowedUntilTime")) allowedUntilTime = json["allowedUntilTime"].as(); + + if(action) + { + bool idExists = false; + + if(authId) + { + idExists = std::find(_authIds.begin(), _authIds.end(), authId) != _authIds.end(); + } + + Nuki::CmdResult result = (Nuki::CmdResult)-1; + _retryCount = 0; + + while(_retryCount < _nrOfRetries) + { + if(strcmp(action, "delete") == 0) { + if(idExists) + { + result = _nukiLock.deleteAuthorizationEntry(authId); + delay(250); + Log->print(F("Delete authorization: ")); + Log->println((int)result); + } + else + { + _network->publishAuthCommandResult("noExistingAuthIdSet"); + return; + } + } + else if(strcmp(action, "add") == 0 || strcmp(action, "update") == 0) + { + if(name.length() < 1) + { + if (strcmp(action, "update") != 0) + { + _network->publishAuthCommandResult("noNameSet"); + return; + } + } + + /* + if(sharedKey.length() != 64) + { + if (strcmp(action, "update") != 0) + { + _network->publishAuthCommandResult("noSharedKeySet"); + return; + } + } + else + { + for(int i=0; i 0) + { + if(allowedFrom.length() == 19) + { + allowedFromAr[0] = (uint16_t)allowedFrom.substring(0, 4).toInt(); + allowedFromAr[1] = (uint8_t)allowedFrom.substring(5, 7).toInt(); + allowedFromAr[2] = (uint8_t)allowedFrom.substring(8, 10).toInt(); + allowedFromAr[3] = (uint8_t)allowedFrom.substring(11, 13).toInt(); + allowedFromAr[4] = (uint8_t)allowedFrom.substring(14, 16).toInt(); + allowedFromAr[5] = (uint8_t)allowedFrom.substring(17, 19).toInt(); + + if(allowedFromAr[0] < 2000 || allowedFromAr[0] > 3000 || allowedFromAr[1] < 1 || allowedFromAr[1] > 12 || allowedFromAr[2] < 1 || allowedFromAr[2] > 31 || allowedFromAr[3] < 0 || allowedFromAr[3] > 23 || allowedFromAr[4] < 0 || allowedFromAr[4] > 59 || allowedFromAr[5] < 0 || allowedFromAr[5] > 59) + { + _network->publishAuthCommandResult("invalidAllowedFrom"); + return; + } + } + else + { + _network->publishAuthCommandResult("invalidAllowedFrom"); + return; + } + } + + if(allowedUntil.length() > 0) + { + if(allowedUntil.length() > 0 == 19) + { + allowedUntilAr[0] = (uint16_t)allowedUntil.substring(0, 4).toInt(); + allowedUntilAr[1] = (uint8_t)allowedUntil.substring(5, 7).toInt(); + allowedUntilAr[2] = (uint8_t)allowedUntil.substring(8, 10).toInt(); + allowedUntilAr[3] = (uint8_t)allowedUntil.substring(11, 13).toInt(); + allowedUntilAr[4] = (uint8_t)allowedUntil.substring(14, 16).toInt(); + allowedUntilAr[5] = (uint8_t)allowedUntil.substring(17, 19).toInt(); + + if(allowedUntilAr[0] < 2000 || allowedUntilAr[0] > 3000 || allowedUntilAr[1] < 1 || allowedUntilAr[1] > 12 || allowedUntilAr[2] < 1 || allowedUntilAr[2] > 31 || allowedUntilAr[3] < 0 || allowedUntilAr[3] > 23 || allowedUntilAr[4] < 0 || allowedUntilAr[4] > 59 || allowedUntilAr[5] < 0 || allowedUntilAr[5] > 59) + { + _network->publishAuthCommandResult("invalidAllowedUntil"); + return; + } + } + else + { + _network->publishAuthCommandResult("invalidAllowedUntil"); + return; + } + } + + if(allowedFromTime.length() > 0) + { + if(allowedFromTime.length() == 5) + { + allowedFromTimeAr[0] = (uint8_t)allowedFromTime.substring(0, 2).toInt(); + allowedFromTimeAr[1] = (uint8_t)allowedFromTime.substring(3, 5).toInt(); + + if(allowedFromTimeAr[0] < 0 || allowedFromTimeAr[0] > 23 || allowedFromTimeAr[1] < 0 || allowedFromTimeAr[1] > 59) + { + _network->publishAuthCommandResult("invalidAllowedFromTime"); + return; + } + } + else + { + _network->publishAuthCommandResult("invalidAllowedFromTime"); + return; + } + } + + if(allowedUntilTime.length() > 0) + { + if(allowedUntilTime.length() == 5) + { + allowedUntilTimeAr[0] = (uint8_t)allowedUntilTime.substring(0, 2).toInt(); + allowedUntilTimeAr[1] = (uint8_t)allowedUntilTime.substring(3, 5).toInt(); + + if(allowedUntilTimeAr[0] < 0 || allowedUntilTimeAr[0] > 23 || allowedUntilTimeAr[1] < 0 || allowedUntilTimeAr[1] > 59) + { + _network->publishAuthCommandResult("invalidAllowedUntilTime"); + return; + } + } + else + { + _network->publishAuthCommandResult("invalidAllowedUntilTime"); + return; + } + } + + if(allowedWeekdays.indexOf("mon") >= 0) allowedWeekdaysInt += 64; + if(allowedWeekdays.indexOf("tue") >= 0) allowedWeekdaysInt += 32; + if(allowedWeekdays.indexOf("wed") >= 0) allowedWeekdaysInt += 16; + if(allowedWeekdays.indexOf("thu") >= 0) allowedWeekdaysInt += 8; + if(allowedWeekdays.indexOf("fri") >= 0) allowedWeekdaysInt += 4; + if(allowedWeekdays.indexOf("sat") >= 0) allowedWeekdaysInt += 2; + if(allowedWeekdays.indexOf("sun") >= 0) allowedWeekdaysInt += 1; + } + + if(strcmp(action, "add") == 0) + { + _network->publishAuthCommandResult("addActionNotSupported"); + return; + + NukiLock::NewAuthorizationEntry entry; + memset(&entry, 0, sizeof(entry)); + size_t nameLen = name.length(); + memcpy(&entry.name, name.c_str(), nameLen > 32 ? 32 : nameLen); + /* + memcpy(&entry.sharedKey, secretKeyK, 32); + + if(idType != 1) + { + _network->publishAuthCommandResult("invalidIdType"); + return; + } + + entry.idType = idType; + */ + entry.remoteAllowed = remoteAllowed == 1 ? 1 : 0; + entry.timeLimited = timeLimited == 1 ? 1 : 0; + + if(allowedFrom.length() > 0) + { + entry.allowedFromYear = allowedFromAr[0]; + entry.allowedFromMonth = allowedFromAr[1]; + entry.allowedFromDay = allowedFromAr[2]; + entry.allowedFromHour = allowedFromAr[3]; + entry.allowedFromMinute = allowedFromAr[4]; + entry.allowedFromSecond = allowedFromAr[5]; + } + + if(allowedUntil.length() > 0) + { + entry.allowedUntilYear = allowedUntilAr[0]; + entry.allowedUntilMonth = allowedUntilAr[1]; + entry.allowedUntilDay = allowedUntilAr[2]; + entry.allowedUntilHour = allowedUntilAr[3]; + entry.allowedUntilMinute = allowedUntilAr[4]; + entry.allowedUntilSecond = allowedUntilAr[5]; + } + + entry.allowedWeekdays = allowedWeekdaysInt; + + if(allowedFromTime.length() > 0) + { + entry.allowedFromTimeHour = allowedFromTimeAr[0]; + entry.allowedFromTimeMin = allowedFromTimeAr[1]; + } + + if(allowedUntilTime.length() > 0) + { + entry.allowedUntilTimeHour = allowedUntilTimeAr[0]; + entry.allowedUntilTimeMin = allowedUntilTimeAr[1]; + } + + result = _nukiLock.addAuthorizationEntry(entry); + delay(250); + Log->print(F("Add authorization: ")); + Log->println((int)result); + } + else if (strcmp(action, "update") == 0) + { + if(!authId) + { + _network->publishAuthCommandResult("noAuthIdSet"); + return; + } + + if(!idExists) + { + _network->publishAuthCommandResult("noExistingAuthIdSet"); + return; + } + + Nuki::CmdResult resultAuth = _nukiLock.retrieveAuthorizationEntries(0, _preferences->getInt(preference_auth_max_entries, MAX_AUTH)); + delay(250); + bool foundExisting = false; + + if(resultAuth == Nuki::CmdResult::Success) + { + std::list entries; + _nukiLock.getAuthorizationEntries(&entries); + + for(const auto& entry : entries) + { + if (authId != entry.authId) continue; + else foundExisting = true; + + if(name.length() < 1) + { + memset(oldName, 0, sizeof(oldName)); + memcpy(oldName, entry.name, sizeof(entry.name)); + } + if(remoteAllowed == 2) remoteAllowed = entry.remoteAllowed; + if(enabled == 2) enabled = entry.enabled; + if(timeLimited == 2) timeLimited = entry.timeLimited; + if(allowedFrom.length() < 1) + { + allowedFrom = "old"; + allowedFromAr[0] = entry.allowedFromYear; + allowedFromAr[1] = entry.allowedFromMonth; + allowedFromAr[2] = entry.allowedFromDay; + allowedFromAr[3] = entry.allowedFromHour; + allowedFromAr[4] = entry.allowedFromMinute; + allowedFromAr[5] = entry.allowedFromSecond; + } + if(allowedUntil.length() < 1) + { + allowedUntil = "old"; + allowedUntilAr[0] = entry.allowedUntilYear; + allowedUntilAr[1] = entry.allowedUntilMonth; + allowedUntilAr[2] = entry.allowedUntilDay; + allowedUntilAr[3] = entry.allowedUntilHour; + allowedUntilAr[4] = entry.allowedUntilMinute; + allowedUntilAr[5] = entry.allowedUntilSecond; + } + if(allowedWeekdays.length() < 1) allowedWeekdaysInt = entry.allowedWeekdays; + if(allowedFromTime.length() < 1) + { + allowedFromTime = "old"; + allowedFromTimeAr[0] = entry.allowedFromTimeHour; + allowedFromTimeAr[1] = entry.allowedFromTimeMin; + } + + if(allowedUntilTime.length() < 1) + { + allowedUntilTime = "old"; + allowedUntilTimeAr[0] = entry.allowedUntilTimeHour; + allowedUntilTimeAr[1] = entry.allowedUntilTimeMin; + } + } + + if(!foundExisting) + { + _network->publishAuthCommandResult("failedToRetrieveExistingAuthorizationEntry"); + return; + } + } + else + { + _network->publishAuthCommandResult("failedToRetrieveExistingAuthorizationEntry"); + return; + } + + NukiLock::UpdatedAuthorizationEntry entry; + + memset(&entry, 0, sizeof(entry)); + entry.authId = authId; + + if(name.length() < 1) + { + size_t nameLen = strlen(oldName); + memcpy(&entry.name, oldName, nameLen > 20 ? 20 : nameLen); + } + else + { + size_t nameLen = name.length(); + memcpy(&entry.name, name.c_str(), nameLen > 20 ? 20 : nameLen); + } + entry.remoteAllowed = remoteAllowed; + entry.enabled = enabled; + entry.timeLimited = timeLimited; + + if(enabled == 1) + { + if(timeLimited == 1) + { + if(allowedFrom.length() > 0) + { + entry.allowedFromYear = allowedFromAr[0]; + entry.allowedFromMonth = allowedFromAr[1]; + entry.allowedFromDay = allowedFromAr[2]; + entry.allowedFromHour = allowedFromAr[3]; + entry.allowedFromMinute = allowedFromAr[4]; + entry.allowedFromSecond = allowedFromAr[5]; + } + + if(allowedUntil.length() > 0) + { + entry.allowedUntilYear = allowedUntilAr[0]; + entry.allowedUntilMonth = allowedUntilAr[1]; + entry.allowedUntilDay = allowedUntilAr[2]; + entry.allowedUntilHour = allowedUntilAr[3]; + entry.allowedUntilMinute = allowedUntilAr[4]; + entry.allowedUntilSecond = allowedUntilAr[5]; + } + + entry.allowedWeekdays = allowedWeekdaysInt; + + if(allowedFromTime.length() > 0) + { + entry.allowedFromTimeHour = allowedFromTimeAr[0]; + entry.allowedFromTimeMin = allowedFromTimeAr[1]; + } + + if(allowedUntilTime.length() > 0) + { + entry.allowedUntilTimeHour = allowedUntilTimeAr[0]; + entry.allowedUntilTimeMin = allowedUntilTimeAr[1]; + } + } + } + + result = _nukiLock.updateAuthorizationEntry(entry); + delay(250); + Log->print(F("Update authorization: ")); + Log->println((int)result); + } + } + else + { + _network->publishAuthCommandResult("invalidAction"); + return; + } + + if(result != Nuki::CmdResult::Success) { + ++_retryCount; + } + else break; + } + + updateAuth(false); + + if((int)result != -1) + { + char resultStr[15]; + memset(&resultStr, 0, sizeof(resultStr)); + NukiLock::cmdResultToString(result, resultStr); + _network->publishAuthCommandResult(resultStr); + } + } + else + { + _network->publishAuthCommandResult("noActionSet"); + return; + } +} + const NukiLock::KeyTurnerState &NukiWrapper::keyTurnerState() { return _keyTurnerState; diff --git a/src/NukiWrapper.h b/src/NukiWrapper.h index bc2b41f..90e1f4d 100644 --- a/src/NukiWrapper.h +++ b/src/NukiWrapper.h @@ -53,12 +53,14 @@ private: static void onKeypadCommandReceivedCallback(const char* command, const uint& id, const String& name, const String& code, const int& enabled); static void onKeypadJsonCommandReceivedCallback(const char* value); static void onTimeControlCommandReceivedCallback(const char* value); + static void onAuthCommandReceivedCallback(const char* value); static void gpioActionCallback(const GpioAction& action, const int& pin); void onKeypadCommandReceived(const char* command, const uint& id, const String& name, const String& code, const int& enabled); void onOfficialUpdateReceived(const char* topic, const char* value); void onConfigUpdateReceived(const char* value); void onKeypadJsonCommandReceived(const char* value); void onTimeControlCommandReceived(const char* value); + void onAuthCommandReceived(const char* value); void updateKeyTurnerState(); void updateBatteryState(); @@ -66,6 +68,7 @@ private: void updateAuthData(bool retrieved); void updateKeypad(bool retrieved); void updateTimeControl(bool retrieved); + void updateAuth(bool retrieved); void postponeBleWatchdog(); void updateGpioOutputs(); @@ -101,6 +104,7 @@ private: bool _clearAuthData = false; std::vector _keypadCodeIds; std::vector _timeControlIds; + std::vector _authIds; NukiLock::KeyTurnerState _lastKeyTurnerState; NukiLock::KeyTurnerState _keyTurnerState; @@ -122,6 +126,7 @@ private: bool _keypadEnabled = false; uint _maxKeypadCodeCount = 0; uint _maxTimeControlEntryCount = 0; + uint _maxAuthEntryCount = 0; int _nrOfRetries = 0; int _retryDelay = 0; int _retryCount = 0; @@ -137,6 +142,7 @@ private: int64_t _waitAuthLogUpdateTs = 0; int64_t _waitKeypadUpdateTs = 0; int64_t _waitTimeControlUpdateTs = 0; + int64_t _waitAuthUpdateTs = 0; int64_t _nextKeypadUpdateTs = 0; int64_t _nextRssiTs = 0; int64_t _lastRssi = 0; diff --git a/src/PreferencesKeys.h b/src/PreferencesKeys.h index f8f4452..edb8ac7 100644 --- a/src/PreferencesKeys.h +++ b/src/PreferencesKeys.h @@ -96,6 +96,12 @@ #define preference_show_secrets (char*)"showSecr" #define preference_ble_tx_power (char*)"bleTxPwr" #define preference_recon_netw_on_mqtt_discon (char*)"recNtwMqttDis" +#define preference_lock_max_auth_entry_count (char*)"maxauth" +#define preference_opener_max_auth_entry_count (char*)"opmaxauth" +#define preference_auth_control_enabled (char*)"authCtrlEna" +#define preference_auth_topic_per_entry (char*)"authPerEntry" +#define preference_auth_info_enabled (char*)"authInfoEna" +#define preference_auth_max_entries (char*)"authmaxentry" #define preference_network_custom_phy (char*)"ntwPHY" #define preference_network_custom_addr (char*)"ntwADDR" #define preference_network_custom_irq (char*)"ntwIRQ" @@ -273,7 +279,8 @@ private: preference_update_from_mqtt, preference_show_secrets, preference_ble_tx_power, preference_recon_netw_on_mqtt_discon, preference_webserial_enabled, preference_network_custom_mdc, preference_network_custom_clk, preference_network_custom_phy, preference_network_custom_addr, preference_network_custom_irq, preference_network_custom_rst, preference_network_custom_cs, preference_network_custom_sck, preference_network_custom_miso, preference_network_custom_mosi, - preference_network_custom_pwr, preference_network_custom_mdio, preference_ntw_reconfigure + preference_network_custom_pwr, preference_network_custom_mdio, preference_ntw_reconfigure, preference_lock_max_auth_entry_count, preference_opener_max_auth_entry_count, + preference_auth_control_enabled, preference_auth_topic_per_entry, preference_auth_info_enabled, preference_auth_max_entries, }; std::vector _redact = { @@ -288,7 +295,8 @@ private: preference_timecontrol_control_enabled, preference_timecontrol_info_enabled, preference_register_as_app, preference_register_opener_as_app, preference_ip_dhcp_enabled, preference_publish_authdata, preference_publish_debug_info, preference_network_wifi_fallback_disabled, preference_official_hybrid, preference_official_hybrid_actions, preference_official_hybrid_retry, preference_conf_info_enabled, preference_disable_non_json, preference_update_from_mqtt, - preference_recon_netw_on_mqtt_discon, preference_webserial_enabled, preference_ntw_reconfigure + preference_auth_control_enabled, preference_auth_topic_per_entry, preference_auth_info_enabled, preference_recon_netw_on_mqtt_discon, preference_webserial_enabled, + preference_ntw_reconfigure }; std::vector _bytePrefs = { diff --git a/src/WebCfgServer.cpp b/src/WebCfgServer.cpp index dad7f36..a6e4bd5 100644 --- a/src/WebCfgServer.cpp +++ b/src/WebCfgServer.cpp @@ -283,10 +283,9 @@ void WebCfgServer::buildOtaHtml(AsyncWebServerRequest *request, bool debug) #else String build_type = "debug"; #endif - - response->print("

"); - response->print("

"); - response->print("

"); + response->print("

"); + response->print("

"); + response->print("

"); response->print("

"); response->print("Current version: "); @@ -299,10 +298,10 @@ void WebCfgServer::buildOtaHtml(AsyncWebServerRequest *request, bool debug) #ifndef NUKI_HUB_UPDATER bool manifestSuccess = false; + JsonDocument doc; NetworkClientSecure *client = new NetworkClientSecure; if (client) { - //client->setDefaultCACertBundle(); client->setCACertBundle(x509_crt_imported_bundle_bin_start, x509_crt_imported_bundle_bin_end - x509_crt_imported_bundle_bin_start); { HTTPClient https; @@ -315,42 +314,8 @@ void WebCfgServer::buildOtaHtml(AsyncWebServerRequest *request, bool debug) if (http_responseCode == HTTP_CODE_OK || http_responseCode == HTTP_CODE_MOVED_PERMANENTLY) { - JsonDocument doc; DeserializationError jsonError = deserializeJson(doc, https.getStream()); - - if (!jsonError) - { - manifestSuccess = true; - response->print("Latest release version: "); - response->print(doc["release"]["fullversion"].as()); - response->print(" ("); - response->print(doc["release"]["build"].as()); - response->print("), "); - response->print(doc["release"]["time"].as()); - response->print("
"); - response->print("Latest beta version: "); - if(doc["beta"]["fullversion"] != "No beta available") - { - response->print(doc["beta"]["fullversion"].as()); - response->print(" ("); - response->print(doc["beta"]["build"].as()); - response->print("), "); - response->print(doc["beta"]["time"].as()); - } - else - { - response->print(doc["beta"]["fullversion"].as()); - response->print("
"); - } - response->print("
"); - response->print("Latest development version: "); - response->print(doc["master"]["fullversion"].as()); - response->print(" ("); - response->print(doc["master"]["build"].as()); - response->print("), "); - response->print(doc["master"]["time"].as()); - response->print("
"); - } + if (!jsonError) { manifestSuccess = true; } } https.end(); } @@ -362,6 +327,38 @@ void WebCfgServer::buildOtaHtml(AsyncWebServerRequest *request, bool debug) { response->print("currentverlatestverdevverbetaver"); } + else + { + response->print("Latest release version: "); + response->print(doc["release"]["fullversion"].as()); + response->print(" ("); + response->print(doc["release"]["build"].as()); + response->print("), "); + response->print(doc["release"]["time"].as()); + response->print("
"); + response->print("Latest beta version: "); + if(doc["beta"]["fullversion"] != "No beta available") + { + response->print(doc["beta"]["fullversion"].as()); + response->print(" ("); + response->print(doc["beta"]["build"].as()); + response->print("), "); + response->print(doc["beta"]["time"].as()); + } + else + { + response->print(doc["beta"]["fullversion"].as()); + response->print(""); + } + response->print("
"); + response->print("Latest development version: "); + response->print(doc["master"]["fullversion"].as()); + response->print(" ("); + response->print(doc["master"]["build"].as()); + response->print("), "); + response->print(doc["master"]["time"].as()); + response->print("
"); + } #endif response->print("
"); @@ -1458,6 +1455,19 @@ bool WebCfgServer::processArgs(AsyncWebServerRequest *request, String& message) } } } + else if(key == "AUTHMAX") + { + if(value.toInt() > 0 && value.toInt() < 51) + { + if(_preferences->getInt(preference_auth_max_entries, MAX_AUTH) != value.toInt()) + { + _preferences->putInt(preference_auth_max_entries, value.toInt()); + Log->print(F("Setting changed: ")); + Log->println(key); + configChanged = true; + } + } + } else if(key == "BUFFSIZE") { if(value.toInt() > 4095 && value.toInt() < 32769) @@ -1565,6 +1575,16 @@ bool WebCfgServer::processArgs(AsyncWebServerRequest *request, String& message) configChanged = true; } } + else if(key == "AUTHPUB") + { + if(_preferences->getBool(preference_auth_info_enabled, false) != (value == "1")) + { + _preferences->putBool(preference_auth_info_enabled, (value == "1")); + Log->print(F("Setting changed: ")); + Log->println(key); + configChanged = true; + } + } else if(key == "KPPER") { if(_preferences->getBool(preference_keypad_topic_per_entry, false) != (value == "1")) @@ -1595,6 +1615,26 @@ bool WebCfgServer::processArgs(AsyncWebServerRequest *request, String& message) configChanged = true; } } + else if(key == "AUTHPER") + { + if(_preferences->getBool(preference_auth_topic_per_entry, false) != (value == "1")) + { + _preferences->putBool(preference_auth_topic_per_entry, (value == "1")); + Log->print(F("Setting changed: ")); + Log->println(key); + configChanged = true; + } + } + else if(key == "AUTHENA") + { + if(_preferences->getBool(preference_auth_control_enabled, false) != (value == "1")) + { + _preferences->putBool(preference_auth_control_enabled, (value == "1")); + Log->print(F("Setting changed: ")); + Log->println(key); + configChanged = true; + } + } else if(key == "PUBAUTH") { if(_preferences->getBool(preference_publish_authdata, false) != (value == "1")) @@ -2220,7 +2260,7 @@ bool WebCfgServer::processArgs(AsyncWebServerRequest *request, String& message) { if(curAdvancedLockConfigAclPrefs[i] != advancedLockConfigAclPrefs[i]) { - _preferences->putBytes(preference_conf_opener_basic_acl, (byte*)(&advancedLockConfigAclPrefs), sizeof(advancedLockConfigAclPrefs)); + _preferences->putBytes(preference_conf_lock_advanced_acl, (byte*)(&advancedLockConfigAclPrefs), sizeof(advancedLockConfigAclPrefs)); Log->print(F("Setting changed: ")); Log->println("ACLCONFADVANCEDLOCK"); configChanged = true; @@ -2232,7 +2272,7 @@ bool WebCfgServer::processArgs(AsyncWebServerRequest *request, String& message) { if(curBasicOpenerConfigAclPrefs[i] != basicOpenerConfigAclPrefs[i]) { - _preferences->putBytes(preference_conf_lock_advanced_acl, (byte*)(&basicOpenerConfigAclPrefs), sizeof(basicOpenerConfigAclPrefs)); + _preferences->putBytes(preference_conf_opener_basic_acl, (byte*)(&basicOpenerConfigAclPrefs), sizeof(basicOpenerConfigAclPrefs)); Log->print(F("Setting changed: ")); Log->println("ACLCONFBASICOPENER"); configChanged = true; @@ -2450,25 +2490,13 @@ void WebCfgServer::buildImportExportHtml(AsyncWebServerRequest *request) response->print(""); response->print("

"); response->print("

"); - response->print("
Initiating config update. Please be patient.
You will be forwarded automatically when the import is complete.
"); - response->print(""); - response->print(""); + response->print(""); request->send(response); } void WebCfgServer::buildCustomNetworkConfigHtml(AsyncWebServerRequest *request) { - String header = ""; + String header = ""; AsyncResponseStream *response = request->beginResponseStream("text/html"); buildHtmlHeader(response, header); response->print("
"); @@ -2478,16 +2506,16 @@ void WebCfgServer::buildCustomNetworkConfigHtml(AsyncWebServerRequest *request) printInputField(response, "NWCUSTADDR", "ADDR", _preferences->getInt(preference_network_custom_addr, 1), 6, ""); #if defined(CONFIG_IDF_TARGET_ESP32) printDropDown(response, "NWCUSTCLK", "CLK", String(_preferences->getInt(preference_network_custom_clk, 0)), getNetworkCustomCLKOptions(), "internalopt"); - printInputField(response, "NWCUSTPWR", "PWR", _preferences->getInt(preference_network_custom_pwr, 12), 6, "internalopt"); - printInputField(response, "NWCUSTMDIO", "MDIO", _preferences->getInt(preference_network_custom_mdio), 6, "internalopt"); - printInputField(response, "NWCUSTMDC", "MDC", _preferences->getInt(preference_network_custom_mdc), 6, "internalopt"); + printInputField(response, "NWCUSTPWR", "PWR", _preferences->getInt(preference_network_custom_pwr, 12), 6, "class=\"internalopt\""); + printInputField(response, "NWCUSTMDIO", "MDIO", _preferences->getInt(preference_network_custom_mdio), 6, "class=\"internalopt\""); + printInputField(response, "NWCUSTMDC", "MDC", _preferences->getInt(preference_network_custom_mdc), 6, "class=\"internalopt\""); #endif - printInputField(response, "NWCUSTIRQ", "IRQ", _preferences->getInt(preference_network_custom_irq, -1), 6, "externalopt"); - printInputField(response, "NWCUSTRST", "RST", _preferences->getInt(preference_network_custom_rst, -1), 6, "externalopt"); - printInputField(response, "NWCUSTCS", "CS", _preferences->getInt(preference_network_custom_cs, -1), 6, "externalopt"); - printInputField(response, "NWCUSTSCK", "SCK", _preferences->getInt(preference_network_custom_sck, -1), 6, "externalopt"); - printInputField(response, "NWCUSTMISO", "MISO", _preferences->getInt(preference_network_custom_miso, -1), 6, "externalopt"); - printInputField(response, "NWCUSTMOSI", "MOSI", _preferences->getInt(preference_network_custom_mosi, -1), 6, "externalopt"); + printInputField(response, "NWCUSTIRQ", "IRQ", _preferences->getInt(preference_network_custom_irq, -1), 6, "class=\"externalopt\""); + printInputField(response, "NWCUSTRST", "RST", _preferences->getInt(preference_network_custom_rst, -1), 6, "class=\"externalopt\""); + printInputField(response, "NWCUSTCS", "CS", _preferences->getInt(preference_network_custom_cs, -1), 6, "class=\"externalopt\""); + printInputField(response, "NWCUSTSCK", "SCK", _preferences->getInt(preference_network_custom_sck, -1), 6, "class=\"externalopt\""); + printInputField(response, "NWCUSTMISO", "MISO", _preferences->getInt(preference_network_custom_miso, -1), 6, "class=\"externalopt\""); + printInputField(response, "NWCUSTMOSI", "MOSI", _preferences->getInt(preference_network_custom_mosi, -1), 6, "class=\"externalopt\""); response->print(""); @@ -2695,7 +2723,6 @@ void WebCfgServer::buildMqttConfigHtml(AsyncWebServerRequest *request) printInputField(response, "IPGTW", "Default gateway", _preferences->getString(preference_ip_gateway).c_str(), 15, ""); printInputField(response, "DNSSRV", "DNS Server", _preferences->getString(preference_ip_dns_server).c_str(), 15, ""); response->print(""); - response->print("
"); response->print("
"); response->print(""); @@ -2872,6 +2899,9 @@ void WebCfgServer::partAccLvlHtml(String &partString, int aclPart) printCheckBox(partString, "TCPUB", "Publish time control entries information", _preferences->getBool(preference_timecontrol_info_enabled), ""); printCheckBox(partString, "TCPER", "Publish a topic per time control entry and create HA sensor", _preferences->getBool(preference_timecontrol_topic_per_entry), ""); printCheckBox(partString, "TCENA", "Add, modify and delete time control entries", _preferences->getBool(preference_timecontrol_control_enabled), ""); + printCheckBox(partString, "AUTHPUB", "Publish authorization entries information", _preferences->getBool(preference_auth_info_enabled), ""); + printCheckBox(partString, "AUTHPER", "Publish a topic per authorization entry and create HA sensor", _preferences->getBool(preference_auth_topic_per_entry), ""); + printCheckBox(partString, "AUTHENA", "Modify and delete authorization entries", _preferences->getBool(preference_auth_control_enabled), ""); printCheckBox(partString, "PUBAUTH", "Publish authorization log", _preferences->getBool(preference_publish_authdata), ""); partString.concat("
"); partString.concat("
"); @@ -3514,6 +3544,9 @@ void WebCfgServer::buildInfoHtml(AsyncWebServerRequest *request) sprintf(tmp, "%02x", authorizationId[i]); response->print(tmp); } + uint32_t authorizationIdInt = authorizationId[0] + 256U*authorizationId[1] + 65536U*authorizationId[2] + 16777216U*authorizationId[3]; + response->print("\nAuthorizationId (UINT32_T): "); + response->print(authorizationIdInt); } } @@ -3865,9 +3898,17 @@ void WebCfgServer::printInputField(AsyncResponseStream *response, response->print(isPassword ? "\"password\"" : "\"text\""); if(strcmp(id, "") != 0) { - response->print(" id=\""); - response->print(id); - response->print("\""); + if(strncmp(id, "class=", 6) != 0) + { + response->print(" "); + response->print(id); + } + else + { + response->print(" id=\""); + response->print(id); + response->print("\""); + } } if(strcmp(value, "") != 0) { @@ -4129,4 +4170,4 @@ String WebCfgServer::getPreselectionForGpio(const uint8_t &pin) return String((int8_t)PinRole::Disabled); } -#endif +#endif \ No newline at end of file diff --git a/src/main.cpp b/src/main.cpp index fba9904..46fa427 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -154,7 +154,6 @@ void networkTask(void *pvParameters) } esp_task_wdt_reset(); - delay(100); } } @@ -243,6 +242,7 @@ void bootloopDetection() preferences->putInt(preference_authlog_max_entries, MAX_AUTHLOG); preferences->putInt(preference_keypad_max_entries, MAX_KEYPAD); preferences->putInt(preference_timecontrol_max_entries, MAX_TIMECONTROL); + preferences->putInt(preference_auth_max_entries, MAX_AUTH); bootloopCounter = 0; } } @@ -344,10 +344,10 @@ void otaTask(void *pvParameter) vTaskDelay(1000 / portTICK_PERIOD_MS); } } - + Log->println("Firmware upgrade failed, restarting"); esp_ota_set_boot_partition(esp_ota_get_next_update_partition(NULL)); - restartEsp(RestartReason::OTAAborted); + restartEsp(RestartReason::OTAAborted); } void setupTasks(bool ota) @@ -382,6 +382,14 @@ void setup() Serial.begin(115200); Log = &Serial; + #ifndef NUKI_HUB_UPDATER + stdout = funopen(NULL, NULL, &write_fn, NULL, NULL); + static char linebuf[1024]; + setvbuf(stdout, linebuf, _IOLBF, sizeof(linebuf)); + esp_rom_install_channel_putc(1, &ets_putc_handler); + //ets_install_putc1(&ets_putc_handler); + #endif + preferences = new Preferences(); preferences->begin("nukihub", false); bool firstStart = initPreferences(preferences);