Import/Export config over MQTT

This commit is contained in:
iranl
2025-01-31 20:21:26 +01:00
parent 57456f42f9
commit 24bbe22e87
25 changed files with 1792 additions and 1206 deletions

View File

@@ -14,6 +14,7 @@
NukiNetwork* NukiNetwork::_inst = nullptr;
extern bool timeSynced;
extern bool wifiFallback;
extern bool disableNetwork;
extern bool forceEnableWebServer;
@@ -21,11 +22,12 @@ extern const uint8_t x509_crt_imported_bundle_bin_start[] asm("_binary_x509_crt_
extern const uint8_t x509_crt_imported_bundle_bin_end[] asm("_binary_x509_crt_bundle_end");
#ifndef NUKI_HUB_UPDATER
NukiNetwork::NukiNetwork(Preferences *preferences, Gpio* gpio, const String& maintenancePathPrefix, char* buffer, size_t bufferSize)
NukiNetwork::NukiNetwork(Preferences *preferences, Gpio* gpio, const String& maintenancePathPrefix, char* buffer, size_t bufferSize, ImportExport* importExport)
: _preferences(preferences),
_gpio(gpio),
_buffer(buffer),
_bufferSize(bufferSize)
_bufferSize(bufferSize),
_importExport(importExport)
#else
NukiNetwork::NukiNetwork(Preferences *preferences)
: _preferences(preferences)
@@ -66,7 +68,7 @@ void NukiNetwork::setupDevice()
{
_ipConfiguration = new IPConfiguration(_preferences);
int hardwareDetect = _preferences->getInt(preference_network_hardware, 0);
Log->print(("Hardware detect: "));
Log->print("Hardware detect: ");
Log->println(hardwareDetect);
_firstBootAfterDeviceChange = _preferences->getBool(preference_ntw_reconfigure, false);
@@ -94,13 +96,13 @@ void NukiNetwork::setupDevice()
#ifndef CONFIG_IDF_TARGET_ESP32H2
if(!_firstBootAfterDeviceChange)
{
Log->println(("Failed to connect to network. Wi-Fi fallback is disabled, rebooting."));
Log->println("Failed to connect to network. Wi-Fi fallback is disabled, rebooting.");
wifiFallback = false;
sleep(5);
restartEsp(RestartReason::NetworkDeviceCriticalFailureNoWifiFallback);
}
Log->println(("Switching to Wi-Fi device as fallback."));
Log->println("Switching to Wi-Fi device as fallback.");
_networkDeviceType = NetworkDeviceType::WiFi;
#else
int custEth = _preferences->getInt(preference_network_custom_phy, 0);
@@ -124,7 +126,7 @@ void NukiNetwork::setupDevice()
_device = NetworkDeviceInstantiator::Create(_networkDeviceType, _hostname, _preferences, _ipConfiguration);
Log->print(("Network device: "));
Log->print("Network device: ");
Log->println(_device->deviceName());
#ifndef NUKI_HUB_UPDATER
@@ -223,7 +225,7 @@ void NukiNetwork::initialize()
strcpy(_hostnameArr, _hostname.c_str());
_device->initialize();
Log->print(("Host name: "));
Log->print("Host name: ");
Log->println(_hostname);
}
@@ -273,7 +275,7 @@ void NukiNetwork::initialize()
strcpy(_hostnameArr, _hostname.c_str());
_device->initialize();
Log->print(("Host name: "));
Log->print("Host name: ");
Log->println(_hostname);
String brokerAddr = _preferences->getString(preference_mqtt_broker);
@@ -300,9 +302,9 @@ void NukiNetwork::initialize()
}
}
Log->print(("MQTT Broker: "));
Log->print("MQTT Broker: ");
Log->print(_mqttBrokerAddr);
Log->print((":"));
Log->print(":");
Log->println(_mqttPort);
_device->mqttSetClientId(_hostnameArr);
@@ -314,7 +316,7 @@ void NukiNetwork::initialize()
if(rebGpio)
{
Log->println(("Rebuild MQTT GPIO structure"));
Log->println("Rebuild MQTT GPIO structure");
}
for (const auto &pinEntry: _gpio->pinConfiguration())
{
@@ -412,7 +414,7 @@ bool NukiNetwork::update()
if(_logIp && _device->isConnected() && !_device->localIP().equals("0.0.0.0"))
{
_logIp = false;
Log->print(("IP: "));
Log->print("IP: ");
Log->println(_device->localIP());
_firstDisconnected = true;
}
@@ -470,6 +472,12 @@ bool NukiNetwork::update()
_lastRssi = rssi;
}
}
if(_overwriteNukiHubConfigTS > 0 && espMillis() > _overwriteNukiHubConfigTS)
{
publishString(_maintenancePathPrefix, mqtt_topic_nuki_hub_config_json, "--", true);
_overwriteNukiHubConfigTS = -1;
}
if(_lastMaintenanceTs == 0 || (ts - _lastMaintenanceTs) > 30000)
{
@@ -575,9 +583,9 @@ bool NukiNetwork::update()
buildMqttPath(gpioPath, {mqtt_topic_gpio_prefix, (mqtt_topic_gpio_pin + std::to_string(pin)).c_str(), mqtt_topic_gpio_state});
publishInt(_lockPath.c_str(), gpioPath, pinState, _retainGpio);
Log->print(("GPIO "));
Log->print("GPIO ");
Log->print(pin);
Log->print((" (Input) --> "));
Log->print(" (Input) --> ");
Log->println(pinState);
}
}
@@ -597,31 +605,31 @@ void NukiNetwork::onMqttDisconnect(const espMqttClientTypes::DisconnectReason &r
switch(reason)
{
case espMqttClientTypes::DisconnectReason::USER_OK:
Log->println(("USER_OK"));
Log->println("USER_OK");
break;
case espMqttClientTypes::DisconnectReason::MQTT_UNACCEPTABLE_PROTOCOL_VERSION:
Log->println(("MQTT_UNACCEPTABLE_PROTOCOL_VERSION"));
Log->println("MQTT_UNACCEPTABLE_PROTOCOL_VERSION");
break;
case espMqttClientTypes::DisconnectReason::MQTT_IDENTIFIER_REJECTED:
Log->println(("MQTT_IDENTIFIER_REJECTED"));
Log->println("MQTT_IDENTIFIER_REJECTED");
break;
case espMqttClientTypes::DisconnectReason::MQTT_SERVER_UNAVAILABLE:
Log->println(("MQTT_SERVER_UNAVAILABLE"));
Log->println("MQTT_SERVER_UNAVAILABLE");
break;
case espMqttClientTypes::DisconnectReason::MQTT_MALFORMED_CREDENTIALS:
Log->println(("MQTT_MALFORMED_CREDENTIALS"));
Log->println("MQTT_MALFORMED_CREDENTIALS");
break;
case espMqttClientTypes::DisconnectReason::MQTT_NOT_AUTHORIZED:
Log->println(("MQTT_NOT_AUTHORIZED"));
Log->println("MQTT_NOT_AUTHORIZED");
break;
case espMqttClientTypes::DisconnectReason::TLS_BAD_FINGERPRINT:
Log->println(("TLS_BAD_FINGERPRINT"));
Log->println("TLS_BAD_FINGERPRINT");
break;
case espMqttClientTypes::DisconnectReason::TCP_DISCONNECTED:
Log->println(("TCP_DISCONNECTED"));
Log->println("TCP_DISCONNECTED");
break;
default:
Log->println(("Unknown"));
Log->println("Unknown");
break;
}
}
@@ -634,7 +642,7 @@ bool NukiNetwork::reconnect()
{
if(strcmp(_mqttBrokerAddr, "") == 0)
{
Log->println(("MQTT Broker not configured, aborting connection attempt."));
Log->println("MQTT Broker not configured, aborting connection attempt.");
_nextReconnect = espMillis() + 5000;
if(_device->isConnected())
@@ -644,17 +652,17 @@ bool NukiNetwork::reconnect()
return false;
}
Log->println(("Attempting MQTT connection"));
Log->println("Attempting MQTT connection");
_connectReplyReceived = false;
if(strlen(_mqttUser) == 0)
{
Log->println(("MQTT: Connecting without credentials"));
Log->println("MQTT: Connecting without credentials");
}
else
{
Log->print(("MQTT: Connecting with user: "));
Log->print("MQTT: Connecting with user: ");
Log->println(_mqttUser);
_device->mqttSetCredentials(_mqttUser, _mqttPass);
}
@@ -677,7 +685,7 @@ bool NukiNetwork::reconnect()
if (_device->mqttConnected())
{
Log->println(("MQTT connected"));
Log->println("MQTT connected");
_mqttConnectedTs = millis();
_mqttConnectionState = 1;
delay(100);
@@ -766,6 +774,18 @@ bool NukiNetwork::reconnect()
subscribe(_maintenancePathPrefix, mqtt_topic_update);
}
if(_preferences->getBool(preference_publish_config, false))
{
initTopic(_maintenancePathPrefix, mqtt_topic_nuki_hub_config_json, "--");
}
if(_preferences->getBool(preference_config_from_mqtt, false) || _preferences->getBool(preference_publish_config, false))
{
initTopic(_maintenancePathPrefix, mqtt_topic_nuki_hub_config_action, "--");
subscribe(_maintenancePathPrefix, mqtt_topic_nuki_hub_config_action);
initTopic(_maintenancePathPrefix, mqtt_topic_nuki_hub_config_action_command_result, "--");
}
initTopic(_maintenancePathPrefix, mqtt_topic_webserver_action, "--");
subscribe(_maintenancePathPrefix, mqtt_topic_webserver_action);
initTopic(_maintenancePathPrefix, mqtt_topic_webserver_state, (_preferences->getBool(preference_webserver_enabled, true) || forceEnableWebServer ? "1" : "0"));
@@ -792,7 +812,7 @@ bool NukiNetwork::reconnect()
}
else
{
Log->print(("MQTT connect failed"));
Log->print("MQTT connect failed");
_mqttConnectionState = 0;
_nextReconnect = espMillis() + 5000;
//_device->mqttDisconnect(true);
@@ -911,14 +931,14 @@ void NukiNetwork::onMqttDataReceived(const char* topic, byte* payload, const uns
if(comparePrefixedPath(topic, mqtt_topic_reset) && strcmp(data, "1") == 0 && !mqttRecentlyConnected())
{
Log->println(("Restart requested via MQTT."));
Log->println("Restart requested via MQTT.");
clearWifiFallback();
delay(200);
restartEsp(RestartReason::RequestedViaMqtt);
}
else if(comparePrefixedPath(topic, mqtt_topic_update) && strcmp(data, "1") == 0 && _preferences->getBool(preference_update_from_mqtt, false) && !mqttRecentlyConnected())
{
Log->println(("Update requested via MQTT."));
Log->println("Update requested via MQTT.");
bool otaManifestSuccess = false;
JsonDocument doc;
@@ -959,13 +979,13 @@ void NukiNetwork::onMqttDataReceived(const char* topic, byte* payload, const uns
{
if(strcmp(NUKI_HUB_VERSION, doc["release"]["fullversion"].as<const char*>()) == 0 && strcmp(NUKI_HUB_BUILD, doc["release"]["build"].as<const char*>()) == 0 && strcmp(NUKI_HUB_DATE, doc["release"]["time"].as<const char*>()) == 0)
{
Log->println(("Nuki Hub is already on the latest release version, OTA update aborted."));
Log->println("Nuki Hub is already on the latest release version, OTA update aborted.");
}
else
{
_preferences->putString(preference_ota_updater_url, GITHUB_LATEST_UPDATER_BINARY_URL);
_preferences->putString(preference_ota_main_url, GITHUB_LATEST_RELEASE_BINARY_URL);
Log->println(("Updating to latest release version."));
Log->println("Updating to latest release version.");
delay(200);
restartEsp(RestartReason::OTAReboot);
}
@@ -974,13 +994,13 @@ void NukiNetwork::onMqttDataReceived(const char* topic, byte* payload, const uns
{
if(strcmp(NUKI_HUB_VERSION, doc["beta"]["fullversion"].as<const char*>()) == 0 && strcmp(NUKI_HUB_BUILD, doc["beta"]["build"].as<const char*>()) == 0 && strcmp(NUKI_HUB_DATE, doc["beta"]["time"].as<const char*>()) == 0)
{
Log->println(("Nuki Hub is already on the latest beta version, OTA update aborted."));
Log->println("Nuki Hub is already on the latest beta version, OTA update aborted.");
}
else
{
_preferences->putString(preference_ota_updater_url, GITHUB_BETA_UPDATER_BINARY_URL);
_preferences->putString(preference_ota_main_url, GITHUB_BETA_RELEASE_BINARY_URL);
Log->println(("Updating to latest beta version."));
Log->println("Updating to latest beta version.");
delay(200);
restartEsp(RestartReason::OTAReboot);
}
@@ -989,13 +1009,13 @@ void NukiNetwork::onMqttDataReceived(const char* topic, byte* payload, const uns
{
if(strcmp(NUKI_HUB_VERSION, doc["master"]["fullversion"].as<const char*>()) == 0 && strcmp(NUKI_HUB_BUILD, doc["master"]["build"].as<const char*>()) == 0 && strcmp(NUKI_HUB_DATE, doc["master"]["time"].as<const char*>()) == 0)
{
Log->println(("Nuki Hub is already on the latest development version, OTA update aborted."));
Log->println("Nuki Hub is already on the latest development version, OTA update aborted.");
}
else
{
_preferences->putString(preference_ota_updater_url, GITHUB_MASTER_UPDATER_BINARY_URL);
_preferences->putString(preference_ota_main_url, GITHUB_MASTER_RELEASE_BINARY_URL);
Log->println(("Updating to latest developmemt version."));
Log->println("Updating to latest developmemt version.");
delay(200);
restartEsp(RestartReason::OTAReboot);
}
@@ -1004,13 +1024,13 @@ void NukiNetwork::onMqttDataReceived(const char* topic, byte* payload, const uns
{
if(strcmp(NUKI_HUB_VERSION, doc["release"]["fullversion"].as<const char*>()) == 0 && strcmp(NUKI_HUB_BUILD, doc["release"]["build"].as<const char*>()) == 0 && strcmp(NUKI_HUB_DATE, doc["release"]["time"].as<const char*>()) == 0)
{
Log->println(("Nuki Hub is already on the latest release version, OTA update aborted."));
Log->println("Nuki Hub is already on the latest release version, OTA update aborted.");
}
else
{
_preferences->putString(preference_ota_updater_url, GITHUB_LATEST_UPDATER_BINARY_URL);
_preferences->putString(preference_ota_main_url, GITHUB_LATEST_RELEASE_BINARY_URL);
Log->println(("Updating to latest release version."));
Log->println("Updating to latest release version.");
delay(200);
restartEsp(RestartReason::OTAReboot);
}
@@ -1018,7 +1038,7 @@ void NukiNetwork::onMqttDataReceived(const char* topic, byte* payload, const uns
}
else
{
Log->println(("Failed to retrieve OTA manifest, OTA update aborted."));
Log->println("Failed to retrieve OTA manifest, OTA update aborted.");
}
}
else if(comparePrefixedPath(topic, mqtt_topic_webserver_action) && !mqttRecentlyConnected())
@@ -1035,7 +1055,7 @@ void NukiNetwork::onMqttDataReceived(const char* topic, byte* payload, const uns
{
return;
}
Log->println(("Webserver enabled, restarting."));
Log->println("Webserver enabled, restarting.");
_preferences->putBool(preference_webserver_enabled, true);
}
else if (strcmp(data, "0") == 0)
@@ -1044,20 +1064,184 @@ void NukiNetwork::onMqttDataReceived(const char* topic, byte* payload, const uns
{
return;
}
Log->println(("Webserver disabled, restarting."));
Log->println("Webserver disabled, restarting.");
_preferences->putBool(preference_webserver_enabled, false);
}
clearWifiFallback();
delay(200);
restartEsp(RestartReason::ReconfigureWebServer);
}
else if(comparePrefixedPath(topic, mqtt_topic_nuki_hub_config_action) && !mqttRecentlyConnected())
{
if(strcmp(data, "") == 0 || strcmp(data, "--") == 0)
{
return;
}
else
{
Log->println("JSON config update received");
JsonDocument doc;
DeserializationError error = deserializeJson(doc, data);
if (error)
{
Log->println("Invalid JSON for import/export");
publishString(_maintenancePathPrefix, mqtt_topic_nuki_hub_config_action_command_result, "{\"error\": \"jsonInvalid\"}", false);
publishString(_maintenancePathPrefix, mqtt_topic_nuki_hub_config_action, "--", true);
}
else
{
if(_preferences->getBool(preference_cred_duo_approval, false))
{
if (!timeSynced)
{
publishString(_maintenancePathPrefix, mqtt_topic_nuki_hub_config_action_command_result, "{\"error\": \"duoTimeNotSynced\"}", false);
publishString(_maintenancePathPrefix, mqtt_topic_nuki_hub_config_action, "--", true);
return;
}
else if (_importExport->startDuoAuth((char*)"Approve Nuki Hub setting change"))
{
int duoResult = 2;
while (duoResult == 2)
{
duoResult = _importExport->checkDuoApprove();
delay(2000);
esp_task_wdt_reset();
}
if (duoResult != 1)
{
publishString(_maintenancePathPrefix, mqtt_topic_nuki_hub_config_action_command_result, "{\"error\": \"duoApprovalFailed\"}", false);
publishString(_maintenancePathPrefix, mqtt_topic_nuki_hub_config_action, "--", true);
return;
}
}
}
if(!doc["exportHTTPS"].isNull() && _device->isEncrypted())
{
if(_preferences->getBool(preference_publish_config, false))
{
if(_device->isEncrypted())
{
JsonDocument json;
_importExport->exportHttpsJson(json);
serializeJson(json, _buffer, _bufferSize);
publishString(_maintenancePathPrefix, mqtt_topic_nuki_hub_config_json, _buffer, false);
if (doc["exportHTTPS"].as<int>() > 0)
{
_overwriteNukiHubConfigTS = espMillis() + (doc["exportHTTPS"].as<int>() * 1000);
}
}
else
{
publishString(_maintenancePathPrefix, mqtt_topic_nuki_hub_config_action_command_result, "{\"error\": \"mqttExportNotEncrypted\"}", false);
}
}
else
{
publishString(_maintenancePathPrefix, mqtt_topic_nuki_hub_config_action_command_result, "{\"error\": \"mqttExportNotEnabled\"}", false);
}
}
else if(!doc["exportMQTTS"].isNull())
{
if(_preferences->getBool(preference_publish_config, false))
{
if(_device->isEncrypted())
{
JsonDocument json;
_importExport->exportMqttsJson(json);
serializeJson(json, _buffer, _bufferSize);
publishString(_maintenancePathPrefix, mqtt_topic_nuki_hub_config_json, _buffer, false);
if (doc["exportMQTTS"].as<int>() > 0)
{
_overwriteNukiHubConfigTS = espMillis() + (doc["exportMQTTS"].as<int>() * 1000);
}
}
else
{
publishString(_maintenancePathPrefix, mqtt_topic_nuki_hub_config_action_command_result, "{\"error\": \"mqttExportNotEncrypted\"}", false);
}
}
else
{
publishString(_maintenancePathPrefix, mqtt_topic_nuki_hub_config_action_command_result, "{\"error\": \"mqttExportNotEnabled\"}", false);
}
}
else if(!doc["exportNH"].isNull())
{
if(_preferences->getBool(preference_publish_config, false))
{
bool redacted = false;
if(!doc["redacted"].isNull())
{
if(_device->isEncrypted())
{
redacted = true;
}
else
{
publishString(_maintenancePathPrefix, mqtt_topic_nuki_hub_config_action_command_result, "{\"error\": \"mqttExportNotEncrypted\"}", false);
}
}
bool pairing = false;
if(!doc["pairing"].isNull())
{
if(_device->isEncrypted())
{
pairing = true;
}
else
{
publishString(_maintenancePathPrefix, mqtt_topic_nuki_hub_config_action_command_result, "{\"error\": \"mqttExportNotEncrypted\"}", false);
}
}
JsonDocument json;
_importExport->exportNukiHubJson(json, redacted, pairing, _preferences->getBool(preference_lock_enabled, true), _preferences->getBool(preference_opener_enabled, false));
serializeJson(json, _buffer, _bufferSize);
publishString(_maintenancePathPrefix, mqtt_topic_nuki_hub_config_json, _buffer, false);
if (doc["exportNH"].as<int>() > 0)
{
_overwriteNukiHubConfigTS = espMillis() + (doc["exportNH"].as<int>() * 1000);
}
}
else
{
publishString(_maintenancePathPrefix, mqtt_topic_nuki_hub_config_action_command_result, "{\"error\": \"mqttExportNotEnabled\"}", false);
}
}
else
{
if(_preferences->getBool(preference_config_from_mqtt, false))
{
JsonDocument json;
json = _importExport->importJson(doc);
serializeJson(json, _buffer, _bufferSize);
publishString(_maintenancePathPrefix, mqtt_topic_nuki_hub_config_json, _buffer, false);
publishString(_maintenancePathPrefix, mqtt_topic_nuki_hub_config_action, "--", true);
delay(200);
restartEsp(RestartReason::ConfigurationUpdated);
}
else
{
publishString(_maintenancePathPrefix, mqtt_topic_nuki_hub_config_action_command_result, "{\"error\": \"mqttImportNotEnabled\"}", false);
}
}
publishString(_maintenancePathPrefix, mqtt_topic_nuki_hub_config_action, "--", true);
}
}
}
}
void NukiNetwork::parseGpioTopics(const espMqttClientTypes::MessageProperties &properties, const char *topic, const uint8_t *payload, size_t& len, size_t& index, size_t& total)
{
char gpioPath[250];
buildMqttPath(gpioPath, {_lockPath.c_str(), mqtt_topic_gpio_prefix, mqtt_topic_gpio_pin});
// /nuki_t/gpio/pin_17/state
size_t gpioLen = strlen(gpioPath);
if(strncmp(gpioPath, topic, gpioLen) == 0)
{
@@ -1073,9 +1257,9 @@ void NukiNetwork::parseGpioTopics(const espMqttClientTypes::MessageProperties &p
if(_gpio->getPinRole(pin) == PinRole::GeneralOutput)
{
const uint8_t pinState = strcmp((const char*)payload, "1") == 0 ? HIGH : LOW;
Log->print(("GPIO "));
Log->print("GPIO ");
Log->print(pin);
Log->print((" (Output) --> "));
Log->print(" (Output) --> ");
Log->println(pinState);
digitalWrite(pin, pinState);
}
@@ -1177,7 +1361,7 @@ void NukiNetwork::removeTopic(const String& mqttPath, const String& mqttTopic)
publish(path.c_str(), "", true);
#ifdef DEBUG_NUKIHUB
Log->print(("Removing MQTT topic: "));
Log->print("Removing MQTT topic: ");
Log->println(path.c_str());
#endif
}