Added MQTT auto discovery #180

This commit is contained in:
Robert Strouse 2023-10-28 11:39:42 -07:00
parent f3e3553dad
commit 79c7ecccac
10 changed files with 156 additions and 9 deletions

View file

@ -573,6 +573,7 @@ bool ShadeConfigFile::readSettingsRecord() {
} }
bool ShadeConfigFile::readGroupRecord(SomfyGroup *group) { bool ShadeConfigFile::readGroupRecord(SomfyGroup *group) {
pref.begin("ShadeCodes"); pref.begin("ShadeCodes");
uint32_t startPos = this->file.position();
group->setGroupId(this->readUInt8(255)); group->setGroupId(this->readUInt8(255));
group->groupType = static_cast<group_types>(this->readUInt8(0)); group->groupType = static_cast<group_types>(this->readUInt8(0));
group->setRemoteAddress(this->readUInt32(0)); group->setRemoteAddress(this->readUInt32(0));
@ -597,10 +598,15 @@ bool ShadeConfigFile::readGroupRecord(SomfyGroup *group) {
if(group->getGroupId() == 255) group->clear(); if(group->getGroupId() == 255) group->clear();
else group->compressLinkedShadeIds(); else group->compressLinkedShadeIds();
pref.end(); pref.end();
if(this->file.position() != startPos + this->header.groupRecordSize) {
Serial.println("Reading to end of group record");
this->seekChar(CFG_REC_END);
}
return true; return true;
} }
bool ShadeConfigFile::readShadeRecord(SomfyShade *shade) { bool ShadeConfigFile::readShadeRecord(SomfyShade *shade) {
pref.begin("ShadeCodes"); pref.begin("ShadeCodes");
uint32_t startPos = this->file.position();
shade->setShadeId(this->readUInt8(255)); shade->setShadeId(this->readUInt8(255));
shade->paired = this->readBool(false); shade->paired = this->readBool(false);
shade->shadeType = static_cast<shade_types>(this->readUInt8(0)); shade->shadeType = static_cast<shade_types>(this->readUInt8(0));
@ -676,6 +682,10 @@ bool ShadeConfigFile::readShadeRecord(SomfyShade *shade) {
} }
if(shade->proto == radio_proto::GP_Remote) if(shade->proto == radio_proto::GP_Remote)
pinMode(shade->gpioMy, OUTPUT); pinMode(shade->gpioMy, OUTPUT);
if(this->file.position() != startPos + this->header.shadeRecordSize) {
Serial.println("Reading to end of shade record");
this->seekChar(CFG_REC_END);
}
return true; return true;
} }
bool ShadeConfigFile::loadFile(SomfyShadeController *s, const char *filename) { bool ShadeConfigFile::loadFile(SomfyShadeController *s, const char *filename) {

View file

@ -245,21 +245,25 @@ bool MQTTSettings::begin() {
} }
bool MQTTSettings::toJSON(JsonObject &obj) { bool MQTTSettings::toJSON(JsonObject &obj) {
obj["enabled"] = this->enabled; obj["enabled"] = this->enabled;
obj["pubDisco"] = this->pubDisco;
obj["protocol"] = this->protocol; obj["protocol"] = this->protocol;
obj["hostname"] = this->hostname; obj["hostname"] = this->hostname;
obj["port"] = this->port; obj["port"] = this->port;
obj["username"] = this->username; obj["username"] = this->username;
obj["password"] = this->password; obj["password"] = this->password;
obj["rootTopic"] = this->rootTopic; obj["rootTopic"] = this->rootTopic;
obj["discoTopic"] = this->discoTopic;
return true; return true;
} }
bool MQTTSettings::fromJSON(JsonObject &obj) { bool MQTTSettings::fromJSON(JsonObject &obj) {
if(obj.containsKey("enabled")) this->enabled = obj["enabled"]; if(obj.containsKey("enabled")) this->enabled = obj["enabled"];
if(obj.containsKey("pubDisco")) this->pubDisco = obj["pubDisco"];
this->parseValueString(obj, "protocol", this->protocol, sizeof(this->protocol)); this->parseValueString(obj, "protocol", this->protocol, sizeof(this->protocol));
this->parseValueString(obj, "hostname", this->hostname, sizeof(this->hostname)); this->parseValueString(obj, "hostname", this->hostname, sizeof(this->hostname));
this->parseValueString(obj, "username", this->username, sizeof(this->username)); this->parseValueString(obj, "username", this->username, sizeof(this->username));
this->parseValueString(obj, "password", this->password, sizeof(this->password)); this->parseValueString(obj, "password", this->password, sizeof(this->password));
this->parseValueString(obj, "rootTopic", this->rootTopic, sizeof(this->rootTopic)); this->parseValueString(obj, "rootTopic", this->rootTopic, sizeof(this->rootTopic));
this->parseValueString(obj, "discoTopic", this->discoTopic, sizeof(this->discoTopic));
if(obj.containsKey("port")) this->port = obj["port"]; if(obj.containsKey("port")) this->port = obj["port"];
return true; return true;
} }
@ -273,6 +277,8 @@ bool MQTTSettings::save() {
pref.putString("password", this->password); pref.putString("password", this->password);
pref.putString("rootTopic", this->rootTopic); pref.putString("rootTopic", this->rootTopic);
pref.putBool("enabled", this->enabled); pref.putBool("enabled", this->enabled);
pref.putBool("pubDisco", this->pubDisco);
pref.putString("discoTopic", this->discoTopic);
pref.end(); pref.end();
return true; return true;
} }
@ -285,6 +291,8 @@ bool MQTTSettings::load() {
pref.getString("password", this->password, sizeof(this->password)); pref.getString("password", this->password, sizeof(this->password));
pref.getString("rootTopic", this->rootTopic, sizeof(this->rootTopic)); pref.getString("rootTopic", this->rootTopic, sizeof(this->rootTopic));
this->enabled = pref.getBool("enabled", false); this->enabled = pref.getBool("enabled", false);
this->pubDisco = pref.getBool("pubDisco", false);
pref.getString("discoTopic", this->discoTopic, sizeof(this->discoTopic));
pref.end(); pref.end();
return true; return true;
} }

View file

@ -131,12 +131,14 @@ class SecuritySettings: BaseSettings {
class MQTTSettings: BaseSettings { class MQTTSettings: BaseSettings {
public: public:
bool enabled = false; bool enabled = false;
bool pubDisco = false;
char hostname[65] = "ESPSomfyRTS"; char hostname[65] = "ESPSomfyRTS";
char protocol[10] = "mqtt://"; char protocol[10] = "mqtt://";
uint16_t port = 1883; uint16_t port = 1883;
char username[33] = ""; char username[33] = "";
char password[33] = ""; char password[33] = "";
char rootTopic[65] = ""; char rootTopic[65] = "";
char discoTopic[65] = "homeassistant";
bool begin(); bool begin();
bool save(); bool save();
bool load(); bool load();

View file

@ -309,6 +309,27 @@ bool MQTTClass::publish(const char *topic, JsonObject &obj, bool retain) {
serializeJson(obj, g_content, sizeof(g_content)); serializeJson(obj, g_content, sizeof(g_content));
return this->publish(topic, g_content, retain); return this->publish(topic, g_content, retain);
} }
bool MQTTClass::publishBuffer(const char *topic, uint8_t *data, uint16_t len, bool retain) {
size_t res;
uint16_t offset = 0;
uint16_t to_write = len;
uint16_t buff_len;
mqttClient.beginPublish(topic, len, retain);
do {
buff_len = to_write;
if(buff_len > 128) buff_len = 128;
res = mqttClient.write(data+offset, buff_len);
offset += buff_len;
to_write -= buff_len;
} while(res == buff_len && to_write > 0);
mqttClient.endPublish();
return true;
}
bool MQTTClass::publishDisco(const char *topic, JsonObject &obj, bool retain) {
serializeJson(obj, g_content, sizeof(g_content));
this->publishBuffer(topic, (uint8_t *)g_content, strlen(g_content), retain);
return true;
}
bool MQTTClass::publish(const char *topic, int8_t val, bool retain) { bool MQTTClass::publish(const char *topic, int8_t val, bool retain) {
snprintf(g_content, sizeof(g_content), "%d", val); snprintf(g_content, sizeof(g_content), "%d", val);
return this->publish(topic, g_content, retain); return this->publish(topic, g_content, retain);

2
MQTT.h
View file

@ -24,6 +24,8 @@ class MQTTClass {
bool publish(const char *topic, uint32_t val, bool retain = false); bool publish(const char *topic, uint32_t val, bool retain = false);
bool publish(const char *topic, uint16_t val, bool retain = false); bool publish(const char *topic, uint16_t val, bool retain = false);
bool publish(const char *topic, bool val, bool retain = false); bool publish(const char *topic, bool val, bool retain = false);
bool publishBuffer(const char *topic, uint8_t *data, uint16_t len, bool retain = false);
bool publishDisco(const char *topic, JsonObject &obj, bool retain = false);
bool subscribe(const char *topic); bool subscribe(const char *topic);
bool unsubscribe(const char *topic); bool unsubscribe(const char *topic);
static void receive(const char *topic, byte *payload, uint32_t length); static void receive(const char *topic, byte *payload, uint32_t length);

View file

@ -1299,8 +1299,99 @@ void SomfyShade::publishState() {
mqtt.publish(topic, isWindy); mqtt.publish(topic, isWindy);
} }
} }
void SomfyShade::publishDisco() {
if(!mqtt.connected() || !settings.MQTT.pubDisco) return;
char topic[128] = "";
DynamicJsonDocument doc(2048);
JsonObject obj = doc.to<JsonObject>();
snprintf(topic, sizeof(topic), "%s/shades/%d", settings.MQTT.rootTopic, this->shadeId);
obj["~"] = topic;
JsonObject dobj = obj.createNestedObject("device");
dobj["hw_version"] = settings.fwVersion.name;
dobj["name"] = settings.hostname;
dobj["mf"] = "rstrouse";
JsonArray arrids = dobj.createNestedArray("identifiers");
//snprintf(topic, sizeof(topic), "mqtt_espsomfyrts_%s_shade%d", settings.serverId, this->shadeId);
snprintf(topic, sizeof(topic), "mqtt_espsomfyrts_%s", settings.serverId);
arrids.add(topic);
//snprintf(topic, sizeof(topic), "ESPSomfy-RTS_%s", settings.serverId);
dobj["via_device"] = topic;
dobj["model"] = "ESPSomfy-RTS MQTT";
snprintf(topic, sizeof(topic), "%s/status", settings.MQTT.rootTopic);
obj["availability_topic"] = topic;
obj["payload_available"] = "online";
obj["payload_not_available"] = "offline";
obj["name"] = this->name;
snprintf(topic, sizeof(topic), "mqtt_%s_shade%d", settings.serverId, this->shadeId);
obj["unique_id"] = topic;
switch(this->shadeType) {
case shade_types::blind:
obj["device_class"] = "blind";
break;
case shade_types::ldrapery:
case shade_types::rdrapery:
case shade_types::cdrapery:
obj["device_class"] = "curtain";
break;
case shade_types::garage1:
case shade_types::garage3:
obj["device_class"] = "garage";
break;
case shade_types::awning:
obj["device_class"] = "awning";
break;
case shade_types::shutter:
obj["device_class"] = "shutter";
break;
case shade_types::drycontact:
break;
default:
obj["device_class"] = "shade";
break;
}
if(this->shadeType != shade_types::drycontact) {
if(this->tiltType != tilt_types::tiltonly) {
obj["command_topic"] = "~/direction/set";
obj["position_topic"] = "~/position";
obj["set_position_topic"] = "~/target/set";
obj["state_topic"] = "~/direction";
obj["payload_close"] = "1";
obj["payload_open"] = "-1";
obj["payload_stop"] = "0";
}
else {
obj["payload_close"] = nullptr;
obj["payload_open"] = nullptr;
obj["payload_stop"] = nullptr;
}
if(this->tiltType != tilt_types::none) {
obj["tilt_command_topic"] = "~/tiltTarget/set";
obj["tilt_status_topic"] = "~/tiltPosition";
}
obj["position_open"] = 0;
obj["position_closed"] = 100;
obj["state_closing"] = "1";
obj["state_opening"] = "-1";
obj["state_stopped"] = "0";
snprintf(topic, sizeof(topic), "%s/cover/%d/config", settings.MQTT.discoTopic, this->shadeId);
}
else {
obj["payload_on"] = 100;
obj["payload_off"] = 0;
obj["state_off"] = 0;
obj["state_on"] = 100;
obj["state_topic"] = "~/position";
obj["command_topic"] = "~/target/set";
snprintf(topic, sizeof(topic), "%s/switch/%d/config", settings.MQTT.discoTopic, this->shadeId);
}
obj["enabled_by_default"] = true;
mqtt.publishDisco(topic, obj);
}
void SomfyShade::publish() { void SomfyShade::publish() {
if(mqtt.connected()) { if(mqtt.connected()) {
this->publishDisco();
char topic[32]; char topic[32];
snprintf(topic, sizeof(topic), "shades/%u/shadeId", this->shadeId); snprintf(topic, sizeof(topic), "shades/%u/shadeId", this->shadeId);
mqtt.publish(topic, this->shadeId); mqtt.publish(topic, this->shadeId);
@ -2511,6 +2602,7 @@ bool SomfyShade::save() {
pref.end(); pref.end();
} }
this->commit(); this->commit();
this->publishDisco();
return true; return true;
} }
bool SomfyGroup::save() { somfy.commit(); return true; } bool SomfyGroup::save() { somfy.commit(); return true; }

View file

@ -319,6 +319,7 @@ class SomfyShade : public SomfyRemote {
bool publish(const char *topic, uint32_t val, bool retain = false); bool publish(const char *topic, uint32_t val, bool retain = false);
bool publish(const char *topic, uint16_t val, bool retain = false); bool publish(const char *topic, uint16_t val, bool retain = false);
bool publish(const char *topic, bool val, bool retain = false); bool publish(const char *topic, bool val, bool retain = false);
void publishDisco();
}; };
class SomfyGroup : public SomfyRemote { class SomfyGroup : public SomfyRemote {
protected: protected:

Binary file not shown.

View file

@ -245,10 +245,16 @@
</div> </div>
<div id="divMQTT" class="subtab-content" style="display:none;"> <div id="divMQTT" class="subtab-content" style="display:none;">
<div class="field-group" style="vertical-align:middle;"> <div class="field-group" style="vertical-align:middle;margin-top:-20px;">
<div class="field-group" style="display:inline-block;width:auto;">
<input id="cbMqttEnabled" name="mqtt-enabled" type="checkbox" data-bind="mqtt.enabled" style="display:inline-block;" /> <input id="cbMqttEnabled" name="mqtt-enabled" type="checkbox" data-bind="mqtt.enabled" style="display:inline-block;" />
<label for="cbMqttEnabled" style="display:inline-block;cursor:pointer;">Enable MQTT client</label> <label for="cbMqttEnabled" style="display:inline-block;cursor:pointer;">Enable MQTT client</label>
</div> </div>
<div class="field-group" style="display:inline-block;width:auto;float:right;">
<input id="cbPubDisco" name="pub-disco" type="checkbox" data-bind="mqtt.pubDisco" style="display:inline-block;margin-left:7px;" onclick="document.getElementById('divDiscoveryTopic').style.display = this.checked ? '' : 'none'" />
<label for="cbPubDisco" style="display:inline-block;cursor:pointer;">Publish Discovery</label>
</div>
</div>
<div class="field-group1" style="white-space:nowrap;"> <div class="field-group1" style="white-space:nowrap;">
<select name="mqtt-protocol" data-bind="mqtt.protocol" style="width:54px;"> <select name="mqtt-protocol" data-bind="mqtt.protocol" style="width:54px;">
<option>MQTT</option> <option>MQTT</option>
@ -271,6 +277,10 @@
<input id="fldMqttTopic" name="mqtt-topic" type="text" length=64 data-bind="mqtt.rootTopic" placeholder="Root Topic"> <input id="fldMqttTopic" name="mqtt-topic" type="text" length=64 data-bind="mqtt.rootTopic" placeholder="Root Topic">
<label for="fldMqttTopic">Root Topic</label> <label for="fldMqttTopic">Root Topic</label>
</div> </div>
<div class="field-group" id="divDiscoveryTopic" style="display:none;">
<input id="fldDiscoTopic" name="disco-topic" type="text" length=64 data-bind="mqtt.discoTopic" placeholder="Discovery Topic">
<label for="fldDiscoTopic">Discovery Topic</label>
</div>
<div class="button-container"> <div class="button-container">
<button id="btnConnectMQTT" type="button" onclick="mqtt.connectMQTT();">Save MQTT Settings</button> <button id="btnConnectMQTT" type="button" onclick="mqtt.connectMQTT();">Save MQTT Settings</button>
@ -785,11 +795,11 @@
</div> </div>
<div id="divLoginPassword" style="display:none;" onkeyup="if (event.code === 'Enter') security.login();"> <div id="divLoginPassword" style="display:none;" onkeyup="if (event.code === 'Enter') security.login();">
<div class="field-group"> <div class="field-group">
<input id="fldLoginUsername" name="username" type="text" data-bind="login.username" length=32 placeholder="Username"/> <input id="fldLoginUsername" name="username" type="text" data-bind="login.username" length=32 placeholder="Username" />
<label for="fldLoginUsername">Username</label> <label for="fldLoginUsername">Username</label>
</div> </div>
<div class="field-group"> <div class="field-group">
<input id="fldLoginPassword" name="password" type="password" data-bind="login.password" length=32 placeholder="Password"/> <input id="fldLoginPassword" name="password" type="password" data-bind="login.password" length=32 placeholder="Password" />
<label for="fldLoginPassword">Password</label> <label for="fldLoginPassword">Password</label>
</div> </div>
</div> </div>

View file

@ -3947,6 +3947,7 @@ class MQTT {
else { else {
console.log(settings); console.log(settings);
ui.toElement(document.getElementById('divMQTT'), { mqtt: settings }); ui.toElement(document.getElementById('divMQTT'), { mqtt: settings });
document.getElementById('divDiscoveryTopic').style.display = settings.pubDisco ? '' : 'none';
} }
}); });
} }