Merge pull request #10 from rodriguezst/hass_discovery

Add MQTT discovery for Home Assistant
This commit is contained in:
Jan-Ole Schümann
2022-06-25 09:50:36 +02:00
committed by GitHub
11 changed files with 228 additions and 0 deletions

View File

@@ -343,6 +343,97 @@ void Network::publishPresenceDetection(char *csv)
_presenceCsv = 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 *)) void Network::setLockActionReceivedCallback(bool (*lockActionReceivedCallback)(const char *))
{ {
_lockActionReceivedCallback = lockActionReceivedCallback; _lockActionReceivedCallback = lockActionReceivedCallback;

View File

@@ -36,6 +36,8 @@ public:
void publishConfig(const NukiLock::Config& config); void publishConfig(const NukiLock::Config& config);
void publishAdvancedConfig(const NukiLock::AdvancedConfig& config); void publishAdvancedConfig(const NukiLock::AdvancedConfig& config);
void publishPresenceDetection(char* csv); 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 setLockActionReceivedCallback(bool (*lockActionReceivedCallback)(const char* value));
void setConfigUpdateReceivedCallback(void (*configUpdateReceivedCallback)(const char* path, const char* value)); void setConfigUpdateReceivedCallback(void (*configUpdateReceivedCallback)(const char* path, const char* value));

View File

@@ -166,6 +166,16 @@ void NetworkOpener::publishAdvancedConfig(const NukiOpener::AdvancedConfig &conf
// publishBool(mqtt_topic_config_auto_lock, config.autoLockEnabled == 1); // 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 *)) void NetworkOpener::setLockActionReceivedCallback(bool (*lockActionReceivedCallback)(const char *))
{ {
_lockActionReceivedCallback = lockActionReceivedCallback; _lockActionReceivedCallback = lockActionReceivedCallback;

View File

@@ -26,6 +26,8 @@ public:
void publishBatteryReport(const NukiOpener::BatteryReport& batteryReport); void publishBatteryReport(const NukiOpener::BatteryReport& batteryReport);
void publishConfig(const NukiOpener::Config& config); void publishConfig(const NukiOpener::Config& config);
void publishAdvancedConfig(const NukiOpener::AdvancedConfig& 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 setLockActionReceivedCallback(bool (*lockActionReceivedCallback)(const char* value));
void setConfigUpdateReceivedCallback(void (*configUpdateReceivedCallback)(const char* path, const char* value)); void setConfigUpdateReceivedCallback(void (*configUpdateReceivedCallback)(const char* path, const char* value));

View File

@@ -75,6 +75,7 @@ void NukiOpenerWrapper::update()
if (_nukiOpener.pairNuki() == NukiOpener::PairingResult::Success) { if (_nukiOpener.pairNuki() == NukiOpener::PairingResult::Success) {
Serial.println(F("Nuki opener paired")); Serial.println(F("Nuki opener paired"));
_paired = true; _paired = true;
setupHASS();
} }
else else
{ {
@@ -299,3 +300,43 @@ void NukiOpenerWrapper::readAdvancedConfig()
_nukiAdvancedConfigValid = result == Nuki::CmdResult::Success; _nukiAdvancedConfigValid = result == Nuki::CmdResult::Success;
Serial.println(result); 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."));
}
}

View File

@@ -19,6 +19,8 @@ public:
void unpair(); void unpair();
void disableHASS();
const NukiOpener::OpenerState& keyTurnerState(); const NukiOpener::OpenerState& keyTurnerState();
const bool isPaired(); const bool isPaired();
@@ -39,6 +41,8 @@ private:
void readConfig(); void readConfig();
void readAdvancedConfig(); void readAdvancedConfig();
void setupHASS();
NukiOpener::LockAction lockActionToEnum(const char* str); // char array at least 14 characters NukiOpener::LockAction lockActionToEnum(const char* str); // char array at least 14 characters
std::string _deviceName; std::string _deviceName;

View File

@@ -76,6 +76,7 @@ void NukiWrapper::update()
if (_nukiLock.pairNuki() == Nuki::PairingResult::Success) { if (_nukiLock.pairNuki() == Nuki::PairingResult::Success) {
Serial.println(F("Nuki paired")); Serial.println(F("Nuki paired"));
_paired = true; _paired = true;
setupHASS();
} }
else else
{ {
@@ -321,3 +322,43 @@ void NukiWrapper::readAdvancedConfig()
_nukiAdvancedConfigValid = result == Nuki::CmdResult::Success; _nukiAdvancedConfigValid = result == Nuki::CmdResult::Success;
Serial.println(result); 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."));
}
}

View File

@@ -19,6 +19,8 @@ public:
void unpair(); void unpair();
void disableHASS();
const NukiLock::KeyTurnerState& keyTurnerState(); const NukiLock::KeyTurnerState& keyTurnerState();
const bool isPaired(); const bool isPaired();
@@ -37,6 +39,8 @@ private:
void readConfig(); void readConfig();
void readAdvancedConfig(); void readAdvancedConfig();
void setupHASS();
NukiLock::LockAction lockActionToEnum(const char* str); // char array at least 14 characters NukiLock::LockAction lockActionToEnum(const char* str); // char array at least 14 characters
std::string _deviceName; std::string _deviceName;

View File

@@ -13,6 +13,7 @@
#define preference_mqtt_ca "mqttca" #define preference_mqtt_ca "mqttca"
#define preference_mqtt_crt "mqttcrt" #define preference_mqtt_crt "mqttcrt"
#define preference_mqtt_key "mqttkey" #define preference_mqtt_key "mqttkey"
#define preference_mqtt_hass_discovery "hassdiscovery"
#define preference_hostname "hostname" #define preference_hostname "hostname"
#define preference_network_timeout "nettmout" #define preference_network_timeout "nettmout"
#define preference_query_interval_lockstate "lockStInterval" #define preference_query_interval_lockstate "lockStInterval"

View File

@@ -73,6 +73,21 @@ CA, CERT and KEY are empty -> No encryption<br>
CA is filled but CERT and KEY are empty -> Encrypted MQTT<br> CA is filled but CERT and KEY are empty -> Encrypted MQTT<br>
CA, CERT and KEY are filled -> Encrypted MQTT with client vaildation<br> CA, CERT and KEY are filled -> Encrypted MQTT with client vaildation<br>
## 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) ## 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). 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).

View File

@@ -225,6 +225,20 @@ bool WebCfgServer::processArgs(String& message)
_preferences->putString(preference_mqtt_key, value); _preferences->putString(preference_mqtt_key, value);
configChanged = true; 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") else if(key == "HOSTNAME")
{ {
_preferences->putString(preference_hostname, value); _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, "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, "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); 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); printInputField(response, "NETTIMEOUT", "Network Timeout until restart (seconds; -1 to disable)", _preferences->getInt(preference_network_timeout), 5);
response.concat("</table>"); response.concat("</table>");
response.concat("* If no encryption is configured for the MQTT broker, leave empty.<br>"); response.concat("* If no encryption is configured for the MQTT broker, leave empty.<br>");
@@ -584,10 +599,12 @@ void WebCfgServer::processUnpair(bool opener)
_server.send(200, "text/html", response); _server.send(200, "text/html", response);
if(!opener && _nuki != nullptr) if(!opener && _nuki != nullptr)
{ {
_nuki->disableHASS();
_nuki->unpair(); _nuki->unpair();
} }
if(opener && _nukiOpener != nullptr) if(opener && _nukiOpener != nullptr)
{ {
_nukiOpener->disableHASS();
_nukiOpener->unpair(); _nukiOpener->unpair();
} }
waitAndProcess(false, 1000); waitAndProcess(false, 1000);