static const char *TAG = "device"; #include #include "device.hpp" #include "presets.hpp" #include "utils.hpp" Device::Device(std::string _id) : id(_id), topic_prefix("light/" + id), ready(false), mutex(xSemaphoreCreateMutex()), sem(xSemaphoreCreateBinary()), power_on(false), strip_length(0), brightness(255), client(start_mqtt_client(on_mqtt_connect, on_mqtt_message, this)) {} void Device::set_json_config(const cJSON *json) { xSemaphoreTake(mutex, portMAX_DELAY); const cJSON *state = cJSON_GetObjectItem(json, "state"); if (state && cJSON_IsString(state)) { power_on = strcmp(state->valuestring, "ON") == 0; } const cJSON *fx = cJSON_GetObjectItem(json, "effect"); if (fx && cJSON_IsString(fx)) { effect = fx->valuestring; } const cJSON *length = cJSON_GetObjectItem(json, "length"); if (length && cJSON_IsNumber(length)) { strip_length = length->valueint; } const cJSON *jbrightness = cJSON_GetObjectItem(json, "brightness"); if (jbrightness && cJSON_IsNumber(jbrightness)) { brightness = jbrightness->valueint; } publish_state_locked(); xSemaphoreGive(mutex); } // 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())); cJSON_AddBoolToObject(json, "brightness", true); cJSON_AddStringToObject(json, "brightness_command_topic", (topic_prefix + "/cmd").c_str()); cJSON_AddStringToObject(json, "brightness_state_topic", (topic_prefix + "/state").c_str()); cJSON_AddStringToObject(json, "brightness_template", "{{ value_json.brightness }}"); cJSON_AddStringToObject(json, "brightness_value_template", "{\"brightness\": {{ value }} }"); 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()); } if (esp_mqtt_client_subscribe(client, "homeassistant/status", 0) < 0) { ESP_LOGE(TAG, "Failed to subscribe to %s", "homeassistant/status"); } // Advertise on initial connection advertise_locked(); // Let everyone know our state publish_state_locked(); xSemaphoreGive(mutex); } 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; } // 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_event_handle_t event) { std::string topic(event->topic, event->topic_len); std::string command; ESP_LOGI(TAG, "Received command via MQTT: %s", topic.c_str()); if (parse_topic(topic, "homeassistant", command)) { if (command == "status") { std::string status(event->data, event->data_len); if (status == "online") { advertise(); } 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; } } ESP_LOGE(TAG, "Unhandled command: %s (%d bytes)", command.c_str(), event->data_len); } cJSON* Device::make_json_config_locked() const { cJSON *json = cJSON_CreateObject(); cJSON_AddStringToObject(json, "state", power_on ? "ON" : "OFF"); cJSON_AddStringToObject(json, "effect", effect.c_str()); cJSON_AddNumberToObject(json, "length", strip_length); cJSON_AddNumberToObject(json, "brightness", brightness); return json; } void Device::publish_state_locked() { if (ready) { cJSON *json = make_json_config_locked(); char *config = cJSON_PrintUnformatted(json); ESP_ERROR_CHECK(esp_mqtt_client_publish( client, (topic_prefix + "/state").c_str(), config, 0, 0, 0 )); cJSON_free(config); cJSON_Delete(json); } }