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;
}
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;

View File

@@ -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));

View File

@@ -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;

View File

@@ -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));

View File

@@ -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."));
}
}

View File

@@ -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

View File

@@ -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."));
}
}

View File

@@ -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

View File

@@ -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"

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, 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)
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);
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("</table>");
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);
if(!opener && _nuki != nullptr)
{
_nuki->disableHASS();
_nuki->unpair();
}
if(opener && _nukiOpener != nullptr)
{
_nukiOpener->disableHASS();
_nukiOpener->unpair();
}
waitAndProcess(false, 1000);