diff --git a/Network.cpp b/Network.cpp index 91fb69b..a51776f 100644 --- a/Network.cpp +++ b/Network.cpp @@ -343,6 +343,97 @@ void Network::publishPresenceDetection(char *csv) _presenceCsv = csv; } +void Network::publishHASSConfig(char* deviceType, const char* baseTopic, char* name, char* uidString, char* lockAction, char* unlockAction, char* openAction, char* lockedState, char* unlockedState) +{ + String discoveryTopic = _preferences->getString(preference_mqtt_hass_discovery); + + if(discoveryTopic != "") + { + String configJSON = "{\"dev\":{\"ids\":[\"nuki_"; + configJSON.concat(uidString); + configJSON.concat("\"],\"mf\":\"Nuki\",\"mdl\":\""); + configJSON.concat(deviceType); + configJSON.concat("\",\"name\":\""); + configJSON.concat(name); + configJSON.concat("\"},\"~\":\""); + configJSON.concat(baseTopic); + configJSON.concat("\",\"name\":\""); + configJSON.concat(name); + configJSON.concat("\",\"unique_id\":\""); + configJSON.concat(uidString); + configJSON.concat("_lock\",\"cmd_t\":\"~"); + configJSON.concat(mqtt_topic_lock_action); + configJSON.concat("\",\"pl_lock\":\""); + configJSON.concat(lockAction); + configJSON.concat("\",\"pl_unlk\":\""); + configJSON.concat(unlockAction); + configJSON.concat("\",\"pl_open\":\""); + configJSON.concat(openAction); + configJSON.concat("\",\"stat_t\":\"~"); + configJSON.concat(mqtt_topic_lock_state); + configJSON.concat("\",\"stat_locked\":\""); + configJSON.concat(lockedState); + configJSON.concat("\",\"stat_unlocked\":\""); + configJSON.concat(unlockedState); + configJSON.concat("\",\"opt\":\"false\"}"); + + String path = discoveryTopic; + path.concat("/lock/"); + path.concat(uidString); + path.concat("/smartlock/config"); + + Serial.println("HASS Config:"); + Serial.println(configJSON); + + _device->mqttClient()->publish(path.c_str(), configJSON.c_str(), true); + + configJSON = "{\"dev\":{\"ids\":[\"nuki_"; + configJSON.concat(uidString); + configJSON.concat("\"],\"mf\":\"Nuki\",\"mdl\":\""); + configJSON.concat(deviceType); + configJSON.concat("\",\"name\":\""); + configJSON.concat(name); + configJSON.concat("\"},\"~\":\""); + configJSON.concat(baseTopic); + configJSON.concat("\",\"name\":\""); + configJSON.concat(name); + configJSON.concat(" battery low\",\"unique_id\":\""); + configJSON.concat(uidString); + configJSON.concat("_battery_low\",\"dev_cla\":\"battery\",\"ent_cat\":\"diagnostic\",\"pl_off\":\"0\",\"pl_on\":\"1\",\"stat_t\":\"~"); + configJSON.concat(mqtt_topic_battery_critical); + configJSON.concat("\"}"); + + path = discoveryTopic; + path.concat("/binary_sensor/"); + path.concat(uidString); + path.concat("/battery_low/config"); + + _device->mqttClient()->publish(path.c_str(), configJSON.c_str(), true); + } +} + +void Network::removeHASSConfig(char* uidString) +{ + String discoveryTopic = _preferences->getString(preference_mqtt_hass_discovery); + + if(discoveryTopic != "") + { + String path = discoveryTopic; + path.concat("/lock/"); + path.concat(uidString); + path.concat("/smartlock/config"); + + _device->mqttClient()->publish(path.c_str(), NULL, 0U, true); + + path = discoveryTopic; + path.concat("/binary_sensor/"); + path.concat(uidString); + path.concat("/battery_low/config"); + + _device->mqttClient()->publish(path.c_str(), NULL, 0U, true); + } +} + void Network::setLockActionReceivedCallback(bool (*lockActionReceivedCallback)(const char *)) { _lockActionReceivedCallback = lockActionReceivedCallback; diff --git a/Network.h b/Network.h index ff5890f..3703e93 100644 --- a/Network.h +++ b/Network.h @@ -36,6 +36,8 @@ public: void publishConfig(const NukiLock::Config& config); void publishAdvancedConfig(const NukiLock::AdvancedConfig& config); void publishPresenceDetection(char* csv); + void publishHASSConfig(char* deviceType, const char* baseTopic, char* name, char* uidString, char* lockAction, char* unlockAction, char* openAction, char* lockedState, char* unlockedState); + void removeHASSConfig(char* uidString); void setLockActionReceivedCallback(bool (*lockActionReceivedCallback)(const char* value)); void setConfigUpdateReceivedCallback(void (*configUpdateReceivedCallback)(const char* path, const char* value)); diff --git a/NetworkOpener.cpp b/NetworkOpener.cpp index d2bf43e..6de71c1 100644 --- a/NetworkOpener.cpp +++ b/NetworkOpener.cpp @@ -166,6 +166,16 @@ void NetworkOpener::publishAdvancedConfig(const NukiOpener::AdvancedConfig &conf // publishBool(mqtt_topic_config_auto_lock, config.autoLockEnabled == 1); } +void NetworkOpener::publishHASSConfig(char* deviceType, const char* baseTopic, char* name, char* uidString, char* lockAction, char* unlockAction, char* openAction, char* lockedState, char* unlockedState) +{ + _network->publishHASSConfig(deviceType, baseTopic, name, uidString, lockAction, unlockAction, openAction, lockedState, unlockedState); +} + +void NetworkOpener::removeHASSConfig(char* uidString) +{ + _network->removeHASSConfig(uidString); +} + void NetworkOpener::setLockActionReceivedCallback(bool (*lockActionReceivedCallback)(const char *)) { _lockActionReceivedCallback = lockActionReceivedCallback; diff --git a/NetworkOpener.h b/NetworkOpener.h index 5371ab7..ee15822 100644 --- a/NetworkOpener.h +++ b/NetworkOpener.h @@ -26,6 +26,8 @@ public: void publishBatteryReport(const NukiOpener::BatteryReport& batteryReport); void publishConfig(const NukiOpener::Config& config); void publishAdvancedConfig(const NukiOpener::AdvancedConfig& config); + void publishHASSConfig(char* deviceType, const char* baseTopic, char* name, char* uidString, char* lockAction, char* unlockAction, char* openAction, char* lockedState, char* unlockedState); + void removeHASSConfig(char* uidString); void setLockActionReceivedCallback(bool (*lockActionReceivedCallback)(const char* value)); void setConfigUpdateReceivedCallback(void (*configUpdateReceivedCallback)(const char* path, const char* value)); diff --git a/NukiOpenerWrapper.cpp b/NukiOpenerWrapper.cpp index bf517c2..dc3eb49 100644 --- a/NukiOpenerWrapper.cpp +++ b/NukiOpenerWrapper.cpp @@ -75,6 +75,7 @@ void NukiOpenerWrapper::update() if (_nukiOpener.pairNuki() == NukiOpener::PairingResult::Success) { Serial.println(F("Nuki opener paired")); _paired = true; + setupHASS(); } else { @@ -299,3 +300,43 @@ void NukiOpenerWrapper::readAdvancedConfig() _nukiAdvancedConfigValid = result == Nuki::CmdResult::Success; Serial.println(result); } + +void NukiOpenerWrapper::setupHASS() +{ + if(!_nukiConfigValid) // only ask for config once to save battery life + { + Nuki::CmdResult result = _nukiOpener.requestConfig(&_nukiConfig); + _nukiConfigValid = result == Nuki::CmdResult::Success; + } + if (_nukiConfigValid) + { + String baseTopic = _preferences->getString(preference_mqtt_opener_path); + char uidString[20]; + itoa(_nukiConfig.nukiId, uidString, 16); + _network->publishHASSConfig("Opener",baseTopic.c_str(),(char*)_nukiConfig.name,uidString,"deactivateRTO","activateRTO","electricStrikeActuation","locked","RTOactive"); + } + else + { + Serial.println(F("Unable to setup HASS. Invalid config received.")); + } +} + +void NukiOpenerWrapper::disableHASS() +{ + if(!_nukiConfigValid) // only ask for config once to save battery life + { + Nuki::CmdResult result = _nukiOpener.requestConfig(&_nukiConfig); + _nukiConfigValid = result == Nuki::CmdResult::Success; + } + if (_nukiConfigValid) + { + String baseTopic = _preferences->getString(preference_mqtt_opener_path); + char uidString[20]; + itoa(_nukiConfig.nukiId, uidString, 16); + _network->removeHASSConfig(uidString); + } + else + { + Serial.println(F("Unable to disable HASS. Invalid config received.")); + } +} diff --git a/NukiOpenerWrapper.h b/NukiOpenerWrapper.h index b76af46..43b8c29 100644 --- a/NukiOpenerWrapper.h +++ b/NukiOpenerWrapper.h @@ -18,6 +18,8 @@ public: void setPin(const uint16_t pin); void unpair(); + + void disableHASS(); const NukiOpener::OpenerState& keyTurnerState(); const bool isPaired(); @@ -38,6 +40,8 @@ private: void readConfig(); void readAdvancedConfig(); + + void setupHASS(); NukiOpener::LockAction lockActionToEnum(const char* str); // char array at least 14 characters diff --git a/NukiWrapper.cpp b/NukiWrapper.cpp index b1e8493..32fec91 100644 --- a/NukiWrapper.cpp +++ b/NukiWrapper.cpp @@ -76,6 +76,7 @@ void NukiWrapper::update() if (_nukiLock.pairNuki() == Nuki::PairingResult::Success) { Serial.println(F("Nuki paired")); _paired = true; + setupHASS(); } else { @@ -321,3 +322,43 @@ void NukiWrapper::readAdvancedConfig() _nukiAdvancedConfigValid = result == Nuki::CmdResult::Success; Serial.println(result); } + +void NukiWrapper::setupHASS() +{ + if(!_nukiConfigValid) // only ask for config once to save battery life + { + Nuki::CmdResult result = _nukiLock.requestConfig(&_nukiConfig); + _nukiConfigValid = result == Nuki::CmdResult::Success; + } + if (_nukiConfigValid) + { + String baseTopic = _preferences->getString(preference_mqtt_lock_path); + char uidString[20]; + itoa(_nukiConfig.nukiId, uidString, 16); + _network->publishHASSConfig("SmartLock",baseTopic.c_str(),(char*)_nukiConfig.name,uidString,"lock","unlock","unlatch","locked","unlocked"); + } + else + { + Serial.println(F("Unable to setup HASS. Invalid config received.")); + } +} + +void NukiWrapper::disableHASS() +{ + if(!_nukiConfigValid) // only ask for config once to save battery life + { + Nuki::CmdResult result = _nukiLock.requestConfig(&_nukiConfig); + _nukiConfigValid = result == Nuki::CmdResult::Success; + } + if (_nukiConfigValid) + { + String baseTopic = _preferences->getString(preference_mqtt_lock_path); + char uidString[20]; + itoa(_nukiConfig.nukiId, uidString, 16); + _network->removeHASSConfig(uidString); + } + else + { + Serial.println(F("Unable to disable HASS. Invalid config received.")); + } +} diff --git a/NukiWrapper.h b/NukiWrapper.h index 48110cd..5133db2 100644 --- a/NukiWrapper.h +++ b/NukiWrapper.h @@ -18,6 +18,8 @@ public: void setPin(const uint16_t pin); void unpair(); + + void disableHASS(); const NukiLock::KeyTurnerState& keyTurnerState(); const bool isPaired(); @@ -36,6 +38,8 @@ private: void readConfig(); void readAdvancedConfig(); + + void setupHASS(); NukiLock::LockAction lockActionToEnum(const char* str); // char array at least 14 characters diff --git a/PreferencesKeys.h b/PreferencesKeys.h index 56466db..a471121 100644 --- a/PreferencesKeys.h +++ b/PreferencesKeys.h @@ -13,6 +13,7 @@ #define preference_mqtt_ca "mqttca" #define preference_mqtt_crt "mqttcrt" #define preference_mqtt_key "mqttkey" +#define preference_mqtt_hass_discovery "hassdiscovery" #define preference_hostname "hostname" #define preference_network_timeout "nettmout" #define preference_query_interval_lockstate "lockStInterval" diff --git a/README.md b/README.md index 06eff23..05fead5 100644 --- a/README.md +++ b/README.md @@ -73,6 +73,21 @@ CA, CERT and KEY are empty -> No encryption
CA is filled but CERT and KEY are empty -> Encrypted MQTT
CA, CERT and KEY are filled -> Encrypted MQTT with client vaildation
+## Home Assistant Discovery + +Home Assistant can be setup manually using the [MQTT Lock integration](https://www.home-assistant.io/integrations/lock.mqtt/). + +For a simpler integration, this software supports [MQTT Discovery](https://www.home-assistant.io/docs/mqtt/discovery/). To enable autodiscovery, supply the discovery topic that is configured in your Home Assistant instance (typically "homeassistant") in the MQTT Configuration page. Once enabled, Smartlock and/or Opener should automatically appear on Home Assistant. + +The following mapping between Home Assistant services and Nuki commands is setup when enabling autodiscovery: +| | Smartlock | Opener | +|-------------|-----------|---------------------------| +| lock.lock | Lock | Disable Ring To Open | +| lock.unlock | Unlock | Enable Ring To Open | +| lock.open | Unlatch | Electric Strike Actuation | + +NOTE: MQTT Discovery uses retained MQTT messages to store devices configurations. In order to avoid orphan configurations on your broker please disable autodiscovery first if you no longer want to use this SW. Retained messages are automatically cleared when unpairing and when changing/disabling autodiscovery topic in MQTT Configuration page. + ## Connecting via LAN (Optional) If you prefer to connect to the MQTT Broker via LAN instead of WiFi, you can use a Wiznet W5x00 Module (W5100, W5200, W5500 are supported). diff --git a/WebCfgServer.cpp b/WebCfgServer.cpp index 6c91c0c..3cf6224 100644 --- a/WebCfgServer.cpp +++ b/WebCfgServer.cpp @@ -225,6 +225,20 @@ bool WebCfgServer::processArgs(String& message) _preferences->putString(preference_mqtt_key, value); configChanged = true; } + else if(key == "HASSDISCOVERY") + { + // Previous HASS config has to be disabled first (remove retained MQTT messages) + if ( _nuki != nullptr ) + { + _nuki->disableHASS(); + } + if ( _nukiOpener != nullptr ) + { + _nukiOpener->disableHASS(); + } + _preferences->putString(preference_mqtt_hass_discovery, value); + configChanged = true; + } else if(key == "HOSTNAME") { _preferences->putString(preference_hostname, value); @@ -493,6 +507,7 @@ void WebCfgServer::buildMqttConfigHtml(String &response) printTextarea(response, "MQTTCA", "MQTT SSL CA Certificate (*, optional)", _preferences->getString(preference_mqtt_ca).c_str(), TLS_CA_MAX_SIZE); printTextarea(response, "MQTTCRT", "MQTT SSL Client Certificate (*, optional)", _preferences->getString(preference_mqtt_crt).c_str(), TLS_CERT_MAX_SIZE); printTextarea(response, "MQTTKEY", "MQTT SSL Client Key (*, optional)", _preferences->getString(preference_mqtt_key).c_str(), TLS_KEY_MAX_SIZE); + printInputField(response, "HASSDISCOVERY", "Home Assistant discovery topic (empty to disable)", _preferences->getString(preference_mqtt_hass_discovery).c_str(), 30); printInputField(response, "NETTIMEOUT", "Network Timeout until restart (seconds; -1 to disable)", _preferences->getInt(preference_network_timeout), 5); response.concat(""); response.concat("* If no encryption is configured for the MQTT broker, leave empty.
"); @@ -584,10 +599,12 @@ void WebCfgServer::processUnpair(bool opener) _server.send(200, "text/html", response); if(!opener && _nuki != nullptr) { + _nuki->disableHASS(); _nuki->unpair(); } if(opener && _nukiOpener != nullptr) { + _nukiOpener->disableHASS(); _nukiOpener->unpair(); } waitAndProcess(false, 1000);