ESP32 Native version of Blinky, featureful controller code for WS2811/WS2812/NeoPixels
Du kannst nicht mehr als 25 Themen auswählen Themen müssen entweder mit einem Buchstaben oder einer Ziffer beginnen. Sie können Bindestriche („-“) enthalten und bis zu 35 Zeichen lang sein.

220 Zeilen
7.1KB

  1. static const char *TAG = "device";
  2. #include <esp_log.h>
  3. #include "device.hpp"
  4. #include "presets.hpp"
  5. #include "utils.hpp"
  6. Device::Device(std::string _id) :
  7. id(_id),
  8. topic_prefix("light/" + id),
  9. ready(false),
  10. mutex(xSemaphoreCreateMutex()),
  11. sem(xSemaphoreCreateBinary()),
  12. power_on(false), strip_length(0), brightness(255),
  13. client(start_mqtt_client(on_mqtt_connect, on_mqtt_message, this))
  14. {}
  15. void Device::set_json_config(const cJSON *json) {
  16. xSemaphoreTake(mutex, portMAX_DELAY);
  17. const cJSON *state = cJSON_GetObjectItem(json, "state");
  18. if (state && cJSON_IsString(state)) {
  19. power_on = strcmp(state->valuestring, "ON") == 0;
  20. }
  21. const cJSON *fx = cJSON_GetObjectItem(json, "effect");
  22. if (fx && cJSON_IsString(fx)) {
  23. effect = fx->valuestring;
  24. }
  25. const cJSON *length = cJSON_GetObjectItem(json, "length");
  26. if (length && cJSON_IsNumber(length)) {
  27. strip_length = length->valueint;
  28. }
  29. const cJSON *jbrightness = cJSON_GetObjectItem(json, "brightness");
  30. if (jbrightness && cJSON_IsNumber(jbrightness)) {
  31. brightness = jbrightness->valueint;
  32. }
  33. publish_state_locked();
  34. xSemaphoreGive(mutex);
  35. }
  36. // TODO: Re-announce when presets change
  37. void Device::advertise_locked() {
  38. if (ready) {
  39. ESP_LOGI(TAG, "Publishing HA discovery message");
  40. cJSON *json = cJSON_CreateObject();
  41. cJSON_AddStringToObject(json, "unique_id", id.c_str());
  42. cJSON *device = cJSON_AddObjectToObject(json, "device");
  43. cJSON_AddStringToObject(device, "name", id.c_str());
  44. cJSON *identifiers = cJSON_AddArrayToObject(device, "identifiers");
  45. cJSON_AddItemToArray(identifiers, cJSON_CreateString(id.c_str()));
  46. cJSON_AddStringToObject(json, "state_topic", (topic_prefix + "/state").c_str());
  47. cJSON_AddStringToObject(json, "command_topic", (topic_prefix + "/cmd").c_str());
  48. cJSON_AddStringToObject(json, "schema", "json");
  49. cJSON_AddBoolToObject(json, "effect", true);
  50. cJSON *effects = cJSON_AddArrayToObject(json, "effect_list");
  51. for (auto iter = Presets::map_begin(); iter != Presets::map_end(); ++iter)
  52. cJSON_AddItemToArray(effects, cJSON_CreateString(iter->first.c_str()));
  53. cJSON_AddBoolToObject(json, "brightness", true);
  54. cJSON_AddStringToObject(json, "brightness_command_topic", (topic_prefix + "/cmd").c_str());
  55. cJSON_AddStringToObject(json, "brightness_state_topic", (topic_prefix + "/state").c_str());
  56. cJSON_AddStringToObject(json, "brightness_template", "{{ value_json.brightness }}");
  57. cJSON_AddStringToObject(json, "brightness_value_template", "{\"brightness\": {{ value }} }");
  58. char *config = cJSON_PrintUnformatted(json);
  59. if (0 > esp_mqtt_client_publish(
  60. client,
  61. ("homeassistant/light/" + id + "/config").c_str(),
  62. config, 0,
  63. 0, 0
  64. )) {
  65. ESP_LOGE(TAG, "Failed to publish discovery message");
  66. }
  67. cJSON_free(config);
  68. cJSON_Delete(json);
  69. }
  70. }
  71. void Device::advertise() {
  72. xSemaphoreTake(mutex, portMAX_DELAY);
  73. advertise_locked();
  74. xSemaphoreGive(mutex);
  75. }
  76. void Device::on_mqtt_connect() {
  77. xSemaphoreTake(mutex, portMAX_DELAY);
  78. ESP_LOGI(TAG, "Connected to MQTT");
  79. ready = true;
  80. // Subscribe to relevant topics
  81. std::string topic = topic_prefix + "/#";
  82. if (esp_mqtt_client_subscribe(client, topic.c_str(), 0) < 0) {
  83. ESP_LOGE(TAG, "Failed to subscribe to %s", topic.c_str());
  84. }
  85. if (esp_mqtt_client_subscribe(client, "homeassistant/status", 0) < 0) {
  86. ESP_LOGE(TAG, "Failed to subscribe to %s", "homeassistant/status");
  87. }
  88. // Advertise on initial connection
  89. advertise_locked();
  90. // Let everyone know our state
  91. publish_state_locked();
  92. xSemaphoreGive(mutex);
  93. }
  94. static bool parse_topic(
  95. const std::string &topic,
  96. const std::string &prefix,
  97. std::string &subtopic
  98. ) {
  99. // Fail if this doesn't start with the prefix
  100. if (topic.rfind(prefix, 0) != 0) return false;
  101. // Blank subtopic if the topic is the prefix
  102. if (topic.length() == prefix.length()) {
  103. subtopic = "";
  104. return true;
  105. }
  106. // Fail if there's no separation as expected
  107. if (topic[prefix.length()] != '/') return false;
  108. // Find the following subtopic, slash or no
  109. const auto prefix_len = prefix.length() + 1;
  110. const auto slash_pos = topic.find('/', prefix_len);
  111. size_t len = (
  112. slash_pos == std::string::npos ?
  113. std::string::npos :
  114. slash_pos - prefix_len
  115. );
  116. subtopic = topic.substr(prefix_len, len);
  117. return true;
  118. }
  119. void Device::on_mqtt_message(esp_mqtt_event_handle_t event) {
  120. std::string topic(event->topic, event->topic_len);
  121. std::string command;
  122. ESP_LOGI(TAG, "Received command via MQTT: %s", topic.c_str());
  123. if (parse_topic(topic, "homeassistant", command)) {
  124. if (command == "status") {
  125. std::string status(event->data, event->data_len);
  126. if (status == "online") {
  127. advertise();
  128. }
  129. return;
  130. }
  131. } else if (parse_topic(topic, topic_prefix, command)) {
  132. if (command == "state") {
  133. // This is from us, just ignore it
  134. return;
  135. } else if (command == "cmd") {
  136. cJSON *json = cJSON_ParseWithLength(event->data, event->data_len);
  137. if (json) {
  138. set_json_config(json);
  139. cJSON_Delete(json);
  140. xSemaphoreGive(sem);
  141. } else {
  142. ESP_LOGE(TAG, "Invalid JSON data");
  143. }
  144. return;
  145. } else if (command == "reboot") {
  146. ESP_LOGI(TAG, "Rebooting immediately");
  147. esp_restart();
  148. return;
  149. } else if (command == "data") {
  150. size_t slash = topic.rfind('/');
  151. if (slash != std::string::npos) {
  152. write_file(
  153. ("/spiffs/" + topic.substr(slash + 1) + ".json").c_str(),
  154. event->data, event->data_len
  155. );
  156. }
  157. xSemaphoreGive(sem);
  158. return;
  159. }
  160. }
  161. ESP_LOGE(TAG, "Unhandled command: %s (%d bytes)", command.c_str(), event->data_len);
  162. }
  163. cJSON* Device::make_json_config_locked() const {
  164. cJSON *json = cJSON_CreateObject();
  165. cJSON_AddStringToObject(json, "state", power_on ? "ON" : "OFF");
  166. cJSON_AddStringToObject(json, "effect", effect.c_str());
  167. cJSON_AddNumberToObject(json, "length", strip_length);
  168. cJSON_AddNumberToObject(json, "brightness", brightness);
  169. return json;
  170. }
  171. void Device::publish_state_locked() {
  172. if (ready) {
  173. cJSON *json = make_json_config_locked();
  174. char *config = cJSON_PrintUnformatted(json);
  175. ESP_ERROR_CHECK(esp_mqtt_client_publish(
  176. client, (topic_prefix + "/state").c_str(), config, 0, 0, 0
  177. ));
  178. cJSON_free(config);
  179. cJSON_Delete(json);
  180. }
  181. }