Skip to content

Commit

Permalink
Merge pull request #482 from sidoh/feature/homeassistant_discovery
Browse files Browse the repository at this point in the history
New feature: HomeAssistant MQTT discovery
  • Loading branch information
sidoh authored Jun 23, 2019
2 parents f176ee1 + 5924422 commit 58979b5
Show file tree
Hide file tree
Showing 18 changed files with 461 additions and 37 deletions.
4 changes: 2 additions & 2 deletions dist/index.html.gz.h

Large diffs are not rendered by default.

146 changes: 146 additions & 0 deletions lib/MQTT/HomeAssistantDiscoveryClient.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
#include <HomeAssistantDiscoveryClient.h>

HomeAssistantDiscoveryClient::HomeAssistantDiscoveryClient(Settings& settings, MqttClient* mqttClient)
: settings(settings)
, mqttClient(mqttClient)
{ }

void HomeAssistantDiscoveryClient::sendDiscoverableDevices(const std::map<String, BulbId>& aliases) {
#ifdef MQTT_DEBUG
Serial.println(F("HomeAssistantDiscoveryClient: Sending discoverable devices..."));
#endif

for (auto itr = aliases.begin(); itr != aliases.end(); ++itr) {
addConfig(itr->first.c_str(), itr->second);
}
}

void HomeAssistantDiscoveryClient::removeOldDevices(const std::map<uint32_t, BulbId>& aliases) {
#ifdef MQTT_DEBUG
Serial.println(F("HomeAssistantDiscoveryClient: Removing discoverable devices..."));
#endif

for (auto itr = aliases.begin(); itr != aliases.end(); ++itr) {
removeConfig(itr->second);
}
}

void HomeAssistantDiscoveryClient::removeConfig(const BulbId& bulbId) {
// Remove by publishing an empty message
String topic = buildTopic(bulbId);
mqttClient->send(topic.c_str(), "", true);
}

void HomeAssistantDiscoveryClient::addConfig(const char* alias, const BulbId& bulbId) {
String topic = buildTopic(bulbId);
DynamicJsonDocument config(1024);

config[F("schema")] = F("json");
config[F("name")] = alias;
config[F("command_topic")] = mqttClient->bindTopicString(settings.mqttTopicPattern, bulbId);
config[F("state_topic")] = mqttClient->bindTopicString(settings.mqttStateTopicPattern, bulbId);

// HomeAssistant only supports simple client availability
if (settings.mqttClientStatusTopic.length() > 0 && settings.simpleMqttClientStatus) {
config[F("availability_topic")] = settings.mqttClientStatusTopic;
config[F("payload_available")] = F("connected");
config[F("payload_not_available")] = F("disconnected");
}

// Configure supported commands based on the bulb type

// All supported bulbs support brightness and night mode
config[F("brightness")] = true;
config[F("effect")] = true;

JsonArray effects = config.createNestedArray(F("effect_list"));
effects.add(F("night_mode"));

// These bulbs support RGB color
switch (bulbId.deviceType) {
case REMOTE_TYPE_FUT089:
case REMOTE_TYPE_RGB:
case REMOTE_TYPE_RGB_CCT:
case REMOTE_TYPE_RGBW:
config[F("rgb")] = true;
break;
default:
break; //nothing
}

// These bulbs support adjustable white values
switch (bulbId.deviceType) {
case REMOTE_TYPE_CCT:
case REMOTE_TYPE_FUT089:
case REMOTE_TYPE_FUT091:
case REMOTE_TYPE_RGB_CCT:
config[F("color_temp")] = true;
break;
default:
break; //nothing
}

// These bulbs support switching between rgb/white, and have a "white_mode" command
switch (bulbId.deviceType) {
case REMOTE_TYPE_FUT089:
case REMOTE_TYPE_RGB_CCT:
case REMOTE_TYPE_RGBW:
effects.add(F("white_mode"));
break;
default:
break; //nothing
}

String message;
serializeJson(config, message);

#ifdef MQTT_DEBUG
Serial.printf_P(PSTR("HomeAssistantDiscoveryClient: adding discoverable device: %s...\n"), alias);
Serial.printf_P(PSTR(" topic: %s\nconfig: %s\n"), topic.c_str(), message.c_str());
#endif


mqttClient->send(topic.c_str(), message.c_str(), true);
}

// Topic syntax:
// <discovery_prefix>/<component>/[<node_id>/]<object_id>/config
//
// source: https://www.home-assistant.io/docs/mqtt/discovery/
String HomeAssistantDiscoveryClient::buildTopic(const BulbId& bulbId) {
String topic = settings.homeAssistantDiscoveryPrefix;

// Don't require the user to entier a "/" (or break things if they do)
if (! topic.endsWith("/")) {
topic += "/";
}

topic += "light/";
// Use a static ID that doesn't depend on configuration.
topic += "milight_hub_" + String(ESP.getChipId());

// make the object ID based on the actual parameters rather than the alias.
topic += "/";
topic += MiLightRemoteTypeHelpers::remoteTypeToString(bulbId.deviceType);
topic += "_";
topic += bulbId.getHexDeviceId();
topic += "_";
topic += bulbId.groupId;
topic += "/config";

return topic;
}

String HomeAssistantDiscoveryClient::bindTopicVariables(const String& topic, const char* alias, const BulbId& bulbId) {
String boundTopic = topic;
String hexDeviceId = bulbId.getHexDeviceId();

boundTopic.replace(":device_alias", alias);
boundTopic.replace(":device_id", hexDeviceId);
boundTopic.replace(":hex_device_id", hexDeviceId);
boundTopic.replace(":dec_device_id", String(bulbId.deviceId));
boundTopic.replace(":device_type", MiLightRemoteTypeHelpers::remoteTypeToString(bulbId.deviceType));
boundTopic.replace(":group_id", String(bulbId.groupId));

return boundTopic;
}
23 changes: 23 additions & 0 deletions lib/MQTT/HomeAssistantDiscoveryClient.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
#pragma once

#include <BulbId.h>
#include <MqttClient.h>
#include <map>

class HomeAssistantDiscoveryClient {
public:
HomeAssistantDiscoveryClient(Settings& settings, MqttClient* mqttClient);

void addConfig(const char* alias, const BulbId& bulbId);
void removeConfig(const BulbId& bulbId);

void sendDiscoverableDevices(const std::map<String, BulbId>& aliases);
void removeOldDevices(const std::map<uint32_t, BulbId>& aliases);

private:
Settings& settings;
MqttClient* mqttClient;

String buildTopic(const BulbId& bulbId);
String bindTopicVariables(const String& topic, const char* alias, const BulbId& bulbId);
};
76 changes: 55 additions & 21 deletions lib/MQTT/MqttClient.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@ MqttClient::MqttClient(Settings& settings, MiLightClient*& milightClient)
: mqttClient(tcpClient),
milightClient(milightClient),
settings(settings),
lastConnectAttempt(0)
lastConnectAttempt(0),
connected(false)
{
String strDomain = settings.mqttServer();
this->domain = new char[strDomain.length() + 1];
Expand All @@ -30,6 +31,10 @@ MqttClient::~MqttClient() {
delete this->domain;
}

void MqttClient::onConnect(OnConnectFn fn) {
this->onConnectFn = fn;
}

void MqttClient::begin() {
#ifdef MQTT_DEBUG
printf_P(
Expand Down Expand Up @@ -117,6 +122,13 @@ void MqttClient::reconnect() {
void MqttClient::handleClient() {
reconnect();
mqttClient.loop();

if (!connected && mqttClient.connected()) {
this->connected = true;
this->onConnectFn();
} else if (!mqttClient.connected()) {
this->connected = false;
}
}

void MqttClient::sendUpdate(const MiLightRemoteConfig& remoteConfig, uint16_t deviceId, uint16_t groupId, const char* update) {
Expand Down Expand Up @@ -144,6 +156,32 @@ void MqttClient::subscribe() {
mqttClient.subscribe(topic.c_str());
}

void MqttClient::send(const char* topic, const char* message, const bool retain) {
size_t len = strlen(message);
size_t topicLen = strlen(topic);

if ((topicLen + len + 10) < MQTT_MAX_PACKET_SIZE ) {
mqttClient.publish(topic, message, retain);
} else {
const uint8_t* messageBuffer = reinterpret_cast<const uint8_t*>(message);
mqttClient.beginPublish(topic, len, retain);

#ifdef MQTT_DEBUG
Serial.printf_P(PSTR("Printing message in parts because it's too large for the packet buffer (%d bytes)"), len);
#endif

for (size_t i = 0; i < len; i += MQTT_PACKET_CHUNK_SIZE) {
size_t toWrite = std::min(static_cast<size_t>(MQTT_PACKET_CHUNK_SIZE), len - i);
mqttClient.write(messageBuffer+i, toWrite);
#ifdef MQTT_DEBUG
Serial.printf_P(PSTR(" Wrote %d bytes\n"), toWrite);
#endif
}

mqttClient.endPublish();
}
}

void MqttClient::publish(
const String& _topic,
const MiLightRemoteConfig &remoteConfig,
Expand All @@ -156,14 +194,14 @@ void MqttClient::publish(
return;
}

String topic = _topic;
MqttClient::bindTopicString(topic, remoteConfig, deviceId, groupId);
BulbId bulbId(deviceId, groupId, remoteConfig.type);
String topic = bindTopicString(_topic, bulbId);

#ifdef MQTT_DEBUG
printf("MqttClient - publishing update to %s\n", topic.c_str());
#endif

mqttClient.publish(topic.c_str(), message, retain);
send(topic.c_str(), message, retain);
}

void MqttClient::publishCallback(char* topic, byte* payload, int length) {
Expand Down Expand Up @@ -236,28 +274,24 @@ void MqttClient::publishCallback(char* topic, byte* payload, int length) {
milightClient->update(obj);
}

inline void MqttClient::bindTopicString(
String& topicPattern,
const MiLightRemoteConfig& remoteConfig,
const uint16_t deviceId,
const uint16_t groupId
) {
String deviceIdHex = String(deviceId, 16);
deviceIdHex.toUpperCase();
deviceIdHex = String("0x") + deviceIdHex;
String MqttClient::bindTopicString(const String& topicPattern, const BulbId& bulbId) {
String boundTopic = topicPattern;
String deviceIdHex = bulbId.getHexDeviceId();

topicPattern.replace(":device_id", deviceIdHex);
topicPattern.replace(":hex_device_id", deviceIdHex);
topicPattern.replace(":dec_device_id", String(deviceId));
topicPattern.replace(":group_id", String(groupId));
topicPattern.replace(":device_type", remoteConfig.name);
boundTopic.replace(":device_id", deviceIdHex);
boundTopic.replace(":hex_device_id", deviceIdHex);
boundTopic.replace(":dec_device_id", String(bulbId.deviceId));
boundTopic.replace(":group_id", String(bulbId.groupId));
boundTopic.replace(":device_type", MiLightRemoteTypeHelpers::remoteTypeToString(bulbId.deviceType));

auto it = settings.findAlias(remoteConfig.type, deviceId, groupId);
auto it = settings.findAlias(bulbId.deviceType, bulbId.deviceId, bulbId.groupId);
if (it != settings.groupIdAliases.end()) {
topicPattern.replace(":device_alias", it->first);
boundTopic.replace(":device_alias", it->first);
} else {
topicPattern.replace(":device_alias", "__unnamed_group");
boundTopic.replace(":device_alias", "__unnamed_group");
}

return boundTopic;
}

String MqttClient::generateConnectionStatusMessage(const char* connectionStatus) {
Expand Down
19 changes: 12 additions & 7 deletions lib/MQTT/MqttClient.h
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,17 @@
#define MQTT_CONNECTION_ATTEMPT_FREQUENCY 5000
#endif

#ifndef MQTT_PACKET_CHUNK_SIZE
#define MQTT_PACKET_CHUNK_SIZE 128
#endif

#ifndef _MQTT_CLIENT_H
#define _MQTT_CLIENT_H

class MqttClient {
public:
using OnConnectFn = std::function<void()>;

MqttClient(Settings& settings, MiLightClient*& milightClient);
~MqttClient();

Expand All @@ -21,6 +27,10 @@ class MqttClient {
void reconnect();
void sendUpdate(const MiLightRemoteConfig& remoteConfig, uint16_t deviceId, uint16_t groupId, const char* update);
void sendState(const MiLightRemoteConfig& remoteConfig, uint16_t deviceId, uint16_t groupId, const char* update);
void send(const char* topic, const char* message, const bool retain = false);
void onConnect(OnConnectFn fn);

String bindTopicString(const String& topicPattern, const BulbId& bulbId);

private:
WiFiClient tcpClient;
Expand All @@ -29,6 +39,8 @@ class MqttClient {
Settings& settings;
char* domain;
unsigned long lastConnectAttempt;
OnConnectFn onConnectFn;
bool connected;

void sendBirthMessage();
bool connect();
Expand All @@ -43,13 +55,6 @@ class MqttClient {
const bool retain = false
);

inline void bindTopicString(
String& topicPattern,
const MiLightRemoteConfig& remoteConfig,
const uint16_t deviceId,
const uint16_t groupId
);

String generateConnectionStatusMessage(const char* status);
};

Expand Down
2 changes: 1 addition & 1 deletion lib/MiLight/PacketSender.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ void PacketSender::updateResendCount() {

if (signedResends < static_cast<int>(settings.packetRepeatMinimum)) {
signedResends = settings.packetRepeatMinimum;
} else if (signedResends > settings.packetRepeats) {
} else if (signedResends > static_cast<int>(settings.packetRepeats)) {
signedResends = settings.packetRepeats;
}

Expand Down
2 changes: 1 addition & 1 deletion lib/MiLightState/GroupStatePersistence.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,6 @@ void GroupStatePersistence::clear(const BulbId &id) {
}

char* GroupStatePersistence::buildFilename(const BulbId &id, char *buffer) {
uint32_t compactId = (id.deviceId << 24) | (id.deviceType << 8) | id.groupId;
uint32_t compactId = id.getCompactId();
return buffer + sprintf(buffer, "%s%x", FILE_PREFIX, compactId);
}
Loading

0 comments on commit 58979b5

Please sign in to comment.