|
|
|
@@ -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( |
|
|
|
|