Browse Source

Help Home Assistant discover devices

S3
jrhoffa 3 years ago
parent
commit
ca41ed5fce
2 changed files with 132 additions and 81 deletions
  1. +125
    -74
      main/device.cpp
  2. +7
    -7
      main/device.hpp

+ 125
- 74
main/device.cpp View File

@@ -9,7 +9,7 @@ static const char *TAG = "device";
Device::Device(std::string _id) :
id(_id),
topic_prefix("light/" + id),
ready(false), should_publish(false),
ready(false),
mutex(xSemaphoreCreateMutex()),
sem(xSemaphoreCreateBinary()),
power_on(false), strip_length(0),
@@ -37,104 +37,155 @@ void Device::set_json_config(const cJSON *json) {
xSemaphoreGive(mutex);
}

void Device::on_mqtt_connect(esp_mqtt_client_handle_t client) {
// TODO: Re-announce when presets change
void Device::advertise_locked() {
if (ready) {
ESP_LOGI(TAG, "Publishing HA discovery message");

cJSON *json = cJSON_CreateObject();
cJSON_AddStringToObject(json, "unique_id", id.c_str());
cJSON *device = cJSON_AddObjectToObject(json, "device");
cJSON_AddStringToObject(device, "name", id.c_str());
cJSON *identifiers = cJSON_AddArrayToObject(device, "identifiers");
cJSON_AddItemToArray(identifiers, cJSON_CreateString(id.c_str()));
cJSON_AddStringToObject(json, "state_topic", (topic_prefix + "/state").c_str());
cJSON_AddStringToObject(json, "command_topic", (topic_prefix + "/cmd").c_str());
cJSON_AddStringToObject(json, "schema", "json");
cJSON_AddBoolToObject(json, "effect", true);
cJSON *effects = cJSON_AddArrayToObject(json, "effect_list");
for (auto iter = Presets::map_begin(); iter != Presets::map_end(); ++iter)
cJSON_AddItemToArray(effects, cJSON_CreateString(iter->first.c_str()));

char *config = cJSON_PrintUnformatted(json);

if (0 > esp_mqtt_client_publish(
client,
("homeassistant/light/" + id + "/config").c_str(),
config, 0,
0, 0
)) {
ESP_LOGE(TAG, "Failed to publish discovery message");
}

cJSON_free(config);
cJSON_Delete(json);
}
}

void Device::advertise() {
xSemaphoreTake(mutex, portMAX_DELAY);
advertise_locked();
xSemaphoreGive(mutex);
}

void Device::on_mqtt_connect() {
xSemaphoreTake(mutex, portMAX_DELAY);

ESP_LOGI(TAG, "Connected to MQTT");

ready = true;

// Subscribe to relevant topics
std::string topic = topic_prefix + "/#";
if (esp_mqtt_client_subscribe(client, topic.c_str(), 0) < 0) {
ESP_LOGE(TAG, "Failed to subscribe to %s", topic.c_str());
}

// TODO: Re-announce when presets change
cJSON *json = cJSON_CreateObject();
cJSON_AddStringToObject(json, "unique_id", id.c_str());
cJSON *device = cJSON_AddObjectToObject(json, "device");
cJSON_AddStringToObject(device, "name", id.c_str());
cJSON *identifiers = cJSON_AddArrayToObject(device, "identifiers");
cJSON_AddItemToArray(identifiers, cJSON_CreateString(id.c_str()));
cJSON_AddStringToObject(json, "state_topic", (topic_prefix + "/state").c_str());
cJSON_AddStringToObject(json, "command_topic", (topic_prefix + "/cmd").c_str());
cJSON_AddStringToObject(json, "schema", "json");
cJSON_AddBoolToObject(json, "effect", true);
cJSON *effects = cJSON_AddArrayToObject(json, "effect_list");
for (auto iter = Presets::map_begin(); iter != Presets::map_end(); ++iter)
cJSON_AddItemToArray(effects, cJSON_CreateString(iter->first.c_str()));

char *config = cJSON_PrintUnformatted(json);

if (0 > esp_mqtt_client_publish(
client,
("homeassistant/light/" + id + "/config").c_str(),
config, 0,
0, 0
)) {
ESP_LOGE(TAG, "Failed to publish discovery message");
if (esp_mqtt_client_subscribe(client, "homeassistant/status", 0) < 0) {
ESP_LOGE(TAG, "Failed to subscribe to %s", "homeassistant/status");
}

cJSON_free(config);
cJSON_Delete(json);
// Advertise on initial connection
advertise_locked();

ready = true;
if (should_publish) publish_state_locked();
// Let everyone know our state
publish_state_locked();

xSemaphoreGive(mutex);
}

std::string Device::subtopic(const std::string &topic) {
auto slash_pos = topic.find('/', topic_prefix.length());
if (slash_pos == std::string::npos) return "";
auto another_slash_pos = topic.find('/', slash_pos + 1);
if (slash_pos == std::string::npos) {
// No further levels
return topic.substr(slash_pos + 1);

static bool parse_topic(
const std::string &topic,
const std::string &prefix,
std::string &subtopic
) {
// Fail if this doesn't start with the prefix
if (topic.rfind(prefix, 0) != 0) return false;

// Blank subtopic if the topic is the prefix
if (topic.length() == prefix.length()) {
subtopic = "";
return true;
}
// First subtopic
return topic.substr(slash_pos + 1, another_slash_pos - (slash_pos + 1));

// Fail if there's no separation as expected
if (topic[prefix.length()] != '/') return false;

// Find the following subtopic, slash or no
const auto prefix_len = prefix.length() + 1;
const auto slash_pos = topic.find('/', prefix_len);
size_t len = (
slash_pos == std::string::npos ?
std::string::npos :
slash_pos - prefix_len
);

subtopic = topic.substr(prefix_len, len);
return true;
}

void Device::on_mqtt_message(esp_mqtt_client_handle_t, esp_mqtt_event_handle_t event) {
std::string topic(event->topic, event->topic_len);
std::string command = subtopic(topic);

ESP_LOGI(TAG, "Received command via MQTT: %s", command.c_str());
void Device::on_mqtt_message(esp_mqtt_event_handle_t event) {
std::string topic(event->topic, event->topic_len);
std::string command;

if (command == "state") {
// This is from us, just ignore it
return;
ESP_LOGI(TAG, "Received command via MQTT: %s", topic.c_str());

} else if (command == "cmd") {
cJSON *json = cJSON_ParseWithLength(event->data, event->data_len);
if (json) {
set_json_config(json);
cJSON_Delete(json);
} else {
ESP_LOGE(TAG, "Invalid JSON data");
}
// Fall through & kick semaphore

} else if (command == "reboot") {
ESP_LOGI(TAG, "Rebooting immediately");
esp_restart();
return;

} else if (command == "data") {
size_t slash = topic.rfind('/');
if (slash != std::string::npos) {
write_file(
("/spiffs/" + topic.substr(slash + 1) + ".json").c_str(),
event->data, event->data_len
);
if (parse_topic(topic, "homeassistant", command)) {
if (command == "status") {
std::string status(event->data, event->data_len);
if (status == "online") {
advertise();
}
return;
}
// Fall through & kick semaphore

} else {
ESP_LOGE(TAG, "Unhandled command: %s (%d bytes)", command.c_str(), event->data_len);
return;
} else if (parse_topic(topic, topic_prefix, command)) {
if (command == "state") {
// This is from us, just ignore it
return;

} else if (command == "cmd") {
cJSON *json = cJSON_ParseWithLength(event->data, event->data_len);
if (json) {
set_json_config(json);
cJSON_Delete(json);
xSemaphoreGive(sem);
} else {
ESP_LOGE(TAG, "Invalid JSON data");
}
return;

} else if (command == "reboot") {
ESP_LOGI(TAG, "Rebooting immediately");
esp_restart();
return;

} else if (command == "data") {
size_t slash = topic.rfind('/');
if (slash != std::string::npos) {
write_file(
("/spiffs/" + topic.substr(slash + 1) + ".json").c_str(),
event->data, event->data_len
);
}
xSemaphoreGive(sem);
return;
}
}

xSemaphoreGive(sem);
ESP_LOGE(TAG, "Unhandled command: %s (%d bytes)", command.c_str(), event->data_len);
}

cJSON* Device::make_json_config_locked() const {
@@ -146,7 +197,7 @@ cJSON* Device::make_json_config_locked() const {
}

void Device::publish_state_locked() {
if (!(should_publish = !ready)) {
if (ready) {
cJSON *json = make_json_config_locked();
char *config = cJSON_PrintUnformatted(json);
ESP_ERROR_CHECK(esp_mqtt_client_publish(


+ 7
- 7
main/device.hpp View File

@@ -35,7 +35,7 @@ private:

std::string topic_prefix;

bool ready, should_publish;
bool ready;

SemaphoreHandle_t mutex;
SemaphoreHandle_t sem;
@@ -47,16 +47,16 @@ private:
esp_mqtt_client_handle_t client;

static void on_mqtt_connect(esp_mqtt_client_handle_t client, void* data) {
((Device*)data)->on_mqtt_connect(client);
((Device*)data)->on_mqtt_connect();
}
static void on_mqtt_message(esp_mqtt_client_handle_t client, esp_mqtt_event_handle_t event, void* data) {
((Device*)data)->on_mqtt_message(client, event);
((Device*)data)->on_mqtt_message(event);
}

void on_mqtt_connect(esp_mqtt_client_handle_t);
void on_mqtt_message(esp_mqtt_client_handle_t, esp_mqtt_event_handle_t);
void on_mqtt_connect();
void on_mqtt_message(esp_mqtt_event_handle_t);

void publish_state_locked();
std::string subtopic(const std::string &topic);
void advertise_locked();
void advertise();
};

Loading…
Cancel
Save