ESP32 Native version of Blinky, featureful controller code for WS2811/WS2812/NeoPixels
Vous ne pouvez pas sélectionner plus de 25 sujets Les noms de sujets doivent commencer par une lettre ou un nombre, peuvent contenir des tirets ('-') et peuvent comporter jusqu'à 35 caractères.

210 lignes
6.4KB

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