diff --git a/main/device.cpp b/main/device.cpp index a0ae804..f01542d 100644 --- a/main/device.cpp +++ b/main/device.cpp @@ -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( diff --git a/main/device.hpp b/main/device.hpp index ef3404c..e645b07 100644 --- a/main/device.hpp +++ b/main/device.hpp @@ -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(); };