diff --git a/CHANGELOG_LATEST.md b/CHANGELOG_LATEST.md index 22fa6b92e..aa8ca23ab 100644 --- a/CHANGELOG_LATEST.md +++ b/CHANGELOG_LATEST.md @@ -17,6 +17,7 @@ - Added support for mDNS [#161](https://github.com/emsesp/EMS-ESP32/issues/161) - Added last system ESP32 reset code to log (and `system info` output) - Firmware Checker in WebUI [#168](https://github.com/emsesp/EMS-ESP32/issues/168) +- Added new MQTT setting for 'response' topic ## Fixed @@ -29,13 +30,15 @@ ## Changed - Syslog BOM only for utf-8 messages [#91](https://github.com/emsesp/EMS-ESP32/issues/91) -- Check for KM200 by device-id 0x48, remove tx-delay[#90](https://github.com/emsesp/EMS-ESP32/issues/90) +- Check for KM200 by device-id 0x48, remove tx-delay [#90](https://github.com/emsesp/EMS-ESP32/issues/90) - rename `fastheatupfactor` to `fastheatup` and add percent [#122] - "unit" renamed to "uom" in API call to recall a Device Value - initial backend React changes to replace the class components (HOCs) with React Hooks - Use program-names instead of numbers - Boiler's maintenancemessage always published in MQTT (to prevent HA missing entity) - Unit of Measure 'times' added to MQTT Fails, Rx fails, Rx received, Tx fails, Tx reads & Tx writes +- Improved API. Restful HTTP API works in the same way as MQTT calls +- Removed settings for MQTT subscribe format [#173](https://github.com/emsesp/EMS-ESP32/issues/173) ## **BREAKING CHANGES** diff --git a/interface/src/mqtt/MqttSettingsForm.tsx b/interface/src/mqtt/MqttSettingsForm.tsx index f6502e70e..d789bae06 100644 --- a/interface/src/mqtt/MqttSettingsForm.tsx +++ b/interface/src/mqtt/MqttSettingsForm.tsx @@ -187,23 +187,16 @@ class MqttSettingsForm extends React.Component { Nested on a single topic As individual topics - - one topic per device - - topics for each device and it's values (main heating circuit only) - - - topic for each device and it's values (all heating circuits) - - + + } + label="Publish command output to a 'response' topic" + /> Command::cmdfunctions_; -// calls a command -// id may be used to represent a heating circuit for example, it's optional -// returns 0 if the command errored, 1 (TRUE) if ok, 2 if not found, 3 if error or 4 if not allowed -uint8_t Command::call(const uint8_t device_type, const char * cmd, const char * value, bool authenticated, const int8_t id) { - int8_t id_new = id; - char cmd_new[30] = {'\0'}; - strlcpy(cmd_new, cmd, sizeof(cmd_new)); +// takes a path and a json body, parses the data and calls the command +// the path is leading so if duplicate keys are in the input JSON it will be ignored +// returns a return code and json output +uint8_t Command::process(const char * path, const bool authenticated, const JsonObject & input, JsonObject & output) { + SUrlParser p; // parse URL for the path names + p.parse(path); + size_t num_paths = p.paths().size(); - // find the command - auto cf = find_command(device_type, cmd_new, id_new); - if ((cf == nullptr) || (cf->cmdfunction_json_)) { - LOG_WARNING(F("Command %s on %s not found"), cmd, EMSdevice::device_type_2_device_name(device_type).c_str()); + if (!num_paths) { + output.clear(); + output["message"] = "error: invalid path"; + return CommandRet::ERROR; + } + + // dump paths, for debugging + // for (auto & folder : p.paths()) { + // Serial.print(folder.c_str()); + // } + + // must start with either "api" or the hostname + if ((p.paths().front() != "api") && (p.paths().front() != Mqtt::base())) { + output.clear(); + output["message"] = "error: invalid path"; + return CommandRet::ERRORED; + } else { + p.paths().erase(p.paths().begin()); // remove it + num_paths--; + } + + std::string cmd_s; + int8_t id_n = -1; // default hc + + // check for a device + // if its not a known device (thermostat, boiler etc) look for any special MQTT subscriptions + const char * device_s = nullptr; + if (!p.paths().size()) { + // we must look for the device in the JSON body + if (input.containsKey("device")) { + device_s = input["device"]; + } + } else { + // extract it from the path + device_s = p.paths().front().c_str(); // get the device (boiler, thermostat, system etc) + } + + // validate the device + uint8_t device_type = EMSdevice::device_name_2_device_type(device_s); + if (device_type == EMSdevice::DeviceType::UNKNOWN) { + output.clear(); + char error[100]; + snprintf(error, sizeof(error), "error: unknown device %s", device_s); + output["message"] = error; return CommandRet::NOT_FOUND; } - // check if we're allowed to call it - if (cf->has_flags(CommandFlag::ADMIN_ONLY) && !authenticated) { - LOG_WARNING(F("Command %s on %s requires valid authorization"), cmd, EMSdevice::device_type_2_device_name(device_type).c_str()); - return CommandRet::NOT_ALLOWED; - } - - std::string dname = EMSdevice::device_type_2_device_name(device_type); - if (value == nullptr) { - LOG_INFO(F("Calling %s command '%s'"), dname.c_str(), cmd); - } else if (id == -1) { - LOG_INFO(F("Calling %s command '%s', value %s, id is default"), dname.c_str(), cmd, value); + const char * command_p = nullptr; + if (num_paths == 2) { + command_p = p.paths()[1].c_str(); + } else if (num_paths >= 3) { + // concatenate the path into one string + char command[50]; + snprintf(command, sizeof(command), "%s/%s", p.paths()[1].c_str(), p.paths()[2].c_str()); + command_p = command; } else { - LOG_INFO(F("Calling %s command '%s', value %s, id is %d"), dname.c_str(), cmd, value, id); + // take it from the JSON. Support both name and cmd to keep backwards compatibility + if (input.containsKey("name")) { + command_p = input["name"]; + } else if (input.containsKey("cmd")) { + command_p = input["cmd"]; + } } - return ((cf->cmdfunction_)(value, id_new)) ? CommandRet::OK : CommandRet::ERROR; + // some commands may be prefixed with hc. or wwc. so extract these + // exit if we don't have a command + command_p = parse_command_string(command_p, id_n); + if (command_p == nullptr) { + output.clear(); + output["message"] = "error: missing command"; + return CommandRet::NOT_FOUND; + } + + // if we don't have an id/hc/wwc try and get it from the JSON input + // it's allowed to have no id, and then keep the default to -1 + if (id_n == -1) { + if (input.containsKey("hc")) { + id_n = input["hc"]; + } else if (input.containsKey("wwc")) { + id_n = input["wwc"]; + } else if (input.containsKey("id")) { + id_n = input["id"]; + } + } + + // the value must always come from the input JSON. It's allowed to be empty. + JsonVariant data; + if (input.containsKey("data")) { + data = input["data"]; + } else if (input.containsKey("value")) { + data = input["value"]; + } + + // call the command based on the type + uint8_t cmd_return = CommandRet::ERROR; + if (data.is()) { + cmd_return = Command::call(device_type, command_p, data.as(), authenticated, id_n, output); + } else if (data.is()) { + char data_str[10]; + cmd_return = Command::call(device_type, command_p, Helpers::itoa(data_str, (int16_t)data.as()), authenticated, id_n, output); + } else if (data.is()) { + char data_str[10]; + cmd_return = Command::call(device_type, command_p, Helpers::render_value(data_str, (float)data.as(), 2), authenticated, id_n, output); + } else if (data.isNull()) { + // empty + cmd_return = Command::call(device_type, command_p, "", authenticated, id_n, output); + } else { + // can't process + LOG_ERROR(F("Cannot parse command")); + return CommandRet::ERROR; + } + + // write debug to log + if (cmd_return == CommandRet::OK) { + LOG_DEBUG(F("Command %s was executed successfully"), command_p); + } else { + if (!output.isNull()) { + LOG_ERROR(F("Command failed with %s (%d)"), (const char *)output["message"], cmd_return); + } else { + LOG_ERROR(F("Command failed with code %d"), cmd_return); + } + } + + return cmd_return; +} + +// takes a string like "hc1/seltemp" or "seltemp" or "wwc2.seltemp" and tries to get the id and cmd +// returns start position of the command string +const char * Command::parse_command_string(const char * command, int8_t & id) { + if (command == nullptr) { + return nullptr; + } + + // make a copy of the string command for parsing + char command_s[100]; + strncpy(command_s, command, sizeof(command_s)); + + char * p; + char * breakp; + // look for a delimeter and split the string + p = command_s; + breakp = strchr(p, '.'); + if (!breakp) { + p = command_s; // reset and look for / + breakp = strchr(p, '/'); + if (!breakp) { + p = command_s; // reset and look for _ + breakp = strchr(p, '_'); + if (!breakp) { + return command; + } + } + } + + uint8_t start_pos = breakp - p + 1; + + // extra the hc or wwc number + if (!strncmp(command, "hc", 2) && start_pos == 4) { + id = command[start_pos - 2] - '0'; + } else if (!strncmp(command, "wwc", 3) && start_pos == 5) { + id = command[start_pos - 2] - '0'; + } else { +#if defined(EMSESP_DEBUG) + LOG_DEBUG(F("command parse error, unknown hc/wwc in %s"), command_s); +#endif + } + + return (command + start_pos); +} + +// calls a command directly +uint8_t Command::call(const uint8_t device_type, const char * cmd, const char * value) { + // create a temporary buffer + StaticJsonDocument output_doc; + JsonObject output = output_doc.to(); + + // authenticated is always true and ID is the default value + return call(device_type, cmd, value, true, -1, output); } // calls a command. Takes a json object for output. // id may be used to represent a heating circuit for example // returns 0 if the command errored, 1 (TRUE) if ok, 2 if not found, 3 if error or 4 if not allowed -uint8_t Command::call(const uint8_t device_type, const char * cmd, const char * value, bool authenticated, const int8_t id, JsonObject & json) { - int8_t id_new = id; - char cmd_new[30] = {'\0'}; - strlcpy(cmd_new, cmd, sizeof(cmd_new)); +uint8_t Command::call(const uint8_t device_type, const char * cmd, const char * value, bool authenticated, const int8_t id, JsonObject & output) { + uint8_t return_code = CommandRet::OK; - auto cf = find_command(device_type, cmd_new, id_new); + // see if there is a command registered + auto cf = find_command(device_type, cmd); - // check if we're allowed to call it - if (cf != nullptr) { + // check if its a call to and end-point to a device, i.e. has no value + // except for system commands as this is a special device without any queryable entities (device values) + if ((device_type != EMSdevice::DeviceType::SYSTEM) && (!value || !strlen(value))) { + if (!cf || (cf && !cf->cmdfunction_json_)) { + return EMSESP::get_device_value_info(output, cmd, id, device_type) ? CommandRet::OK : CommandRet::ERROR; // entity = cmd + } + } + + if (cf) { + // we have a matching command + std::string dname = EMSdevice::device_type_2_device_name(device_type); + if ((value == nullptr) || !strlen(value)) { + LOG_INFO(F("Calling %s command '%s'"), dname.c_str(), cmd); + } else if (id == -1) { + LOG_INFO(F("Calling %s command '%s', value %s, id is default"), dname.c_str(), cmd, value); + } else { + LOG_INFO(F("Calling %s command '%s', value %s, id is %d"), dname.c_str(), cmd, value, id); + } + + // check permissions if (cf->has_flags(CommandFlag::ADMIN_ONLY) && !authenticated) { - LOG_WARNING(F("Command %s on %s requires valid authorization"), cmd, EMSdevice::device_type_2_device_name(device_type).c_str()); + output["message"] = "error: authentication failed"; return CommandRet::NOT_ALLOWED; // command not allowed } - } - std::string dname = EMSdevice::device_type_2_device_name(device_type); - if (value == nullptr) { - LOG_INFO(F("Calling %s command '%s'"), dname.c_str(), cmd); - } else if (id == -1) { - LOG_INFO(F("Calling %s command '%s', value %s, id is default"), dname.c_str(), cmd, value); - } else { - LOG_INFO(F("Calling %s command '%s', value %s, id is %d"), dname.c_str(), cmd, value, id_new); - } - - // check if json object is empty, if so quit - if (json.isNull()) { - LOG_WARNING(F("Ignore call for command %s in %s because it has no json body"), cmd, EMSdevice::device_type_2_device_name(device_type).c_str()); - return CommandRet::ERROR; - } - - // this is for endpoints that don't have commands, i.e not writable (e.g. boiler/syspress) - if (cf == nullptr) { - return EMSESP::get_device_value_info(json, cmd_new, id_new, device_type) ? CommandRet::OK : CommandRet::ERROR; - } - - if (cf->cmdfunction_json_) { - return ((cf->cmdfunction_json_)(value, id_new, json)) ? CommandRet::OK : CommandRet::ERROR; - } else { - if ((device_type != EMSdevice::DeviceType::SYSTEM) && (value == nullptr || strlen(value) == 0 || strcmp(value, "?") == 0 || strcmp(value, "*") == 0)) { - return EMSESP::get_device_value_info(json, cmd_new, id_new, device_type) ? CommandRet::OK : CommandRet::ERROR; + // call the function + if (cf->cmdfunction_json_) { + return_code = ((cf->cmdfunction_json_)(value, id, output)) ? CommandRet::OK : CommandRet::ERROR; } - return ((cf->cmdfunction_)(value, id_new)) ? CommandRet::OK : CommandRet::ERROR; - } -} - -// strip prefixes, check, and find command -Command::CmdFunction * Command::find_command(const uint8_t device_type, char * cmd, int8_t & id) { - // special cases for id=0 and id=-1 will be removed in V3 API - // no command for id0 - if (id == 0) { - return nullptr; - } - - // empty command is info with id0 - if (cmd[0] == '\0') { - strcpy(cmd, "info"); - id = 0; - } - - // convert cmd to lowercase - for (char * p = cmd; *p; p++) { - *p = tolower(*p); - } - - // hack for commands that could have hc or wwc prefixed. will be removed in new API V3 eventually - // scan for prefix hc. - for (uint8_t i = DeviceValueTAG::TAG_HC1; i <= DeviceValueTAG::TAG_HC4; i++) { - const char * tag = EMSdevice::tag_to_string(i).c_str(); - uint8_t len = strlen(tag); - if (strncmp(cmd, tag, len) == 0) { - if (cmd[len] != '\0') { - strcpy(cmd, &cmd[len + 1]); - } else { - strcpy(cmd, &cmd[len]); - } - id = 1 + i - DeviceValueTAG::TAG_HC1; - break; + if (cf->cmdfunction_) { + return_code = ((cf->cmdfunction_)(value, id)) ? CommandRet::OK : CommandRet::ERROR; } - } - // scan for prefix wwc. - for (uint8_t i = DeviceValueTAG::TAG_WWC1; i <= DeviceValueTAG::TAG_WWC4; i++) { - const char * tag = EMSdevice::tag_to_string(i).c_str(); - uint8_t len = strlen(tag); - if (strncmp(cmd, tag, len) == 0) { - if (cmd[len] != '\0') { - strcpy(cmd, &cmd[len + 1]); - } else { - strcpy(cmd, &cmd[len]); - } - id = 8 + i - DeviceValueTAG::TAG_WWC1; - break; + // report error if call failed + if (return_code != CommandRet::OK) { + output.clear(); + output["message"] = "error: function failed"; } + + return return_code; } - // empty command after processing prefix is info - if (cmd[0] == '\0') { - strcpy(cmd, "info"); - } - - return find_command(device_type, cmd); + // we didn't find the command and its not an endpoint, report error + char error[100]; + snprintf(error, sizeof(error), "error: invalid command %s", cmd); + output["message"] = error; + return CommandRet::NOT_FOUND; } // add a command to the list, which does not return json @@ -178,23 +285,17 @@ void Command::add(const uint8_t device_type, const __FlashStringHelper * cmd, co } cmdfunctions_.emplace_back(device_type, flags, cmd, cb, nullptr, description); // callback for json is nullptr - - Mqtt::sub_command(device_type, cmd, cb, flags); } // add a command to the list, which does return a json object as output // flag is fixed to MqttSubFlag::MQTT_SUB_FLAG_NOSUB so there will be no topic subscribed to this -void Command::add_json(const uint8_t device_type, - const __FlashStringHelper * cmd, - const cmd_json_function_p cb, - const __FlashStringHelper * description, - uint8_t flags) { +void Command::add(const uint8_t device_type, const __FlashStringHelper * cmd, const cmd_json_function_p cb, const __FlashStringHelper * description, uint8_t flags) { // if the command already exists for that device type don't add it if (find_command(device_type, uuid::read_flash_string(cmd).c_str()) != nullptr) { return; } - cmdfunctions_.emplace_back(device_type, CommandFlag::MQTT_SUB_FLAG_NOSUB | flags, cmd, nullptr, cb, description); // callback for json is included + cmdfunctions_.emplace_back(device_type, (CommandFlag::MQTT_SUB_FLAG_NOSUB | flags), cmd, nullptr, cb, description); // callback for json is included } // see if a command exists for that device type @@ -221,9 +322,9 @@ Command::CmdFunction * Command::find_command(const uint8_t device_type, const ch } // list all commands for a specific device, output as json -bool Command::list(const uint8_t device_type, JsonObject & json) { +bool Command::list(const uint8_t device_type, JsonObject & output) { if (cmdfunctions_.empty()) { - json["message"] = "no commands available"; + output["message"] = "no commands available"; return false; } @@ -239,7 +340,7 @@ bool Command::list(const uint8_t device_type, JsonObject & json) { for (auto & cl : sorted_cmds) { for (const auto & cf : cmdfunctions_) { if ((cf.device_type_ == device_type) && !cf.has_flags(CommandFlag::HIDDEN) && cf.description_ && (cl == uuid::read_flash_string(cf.cmd_))) { - json[cl] = cf.description_; + output[cl] = cf.description_; } } } @@ -282,11 +383,11 @@ void Command::show(uuid::console::Shell & shell, uint8_t device_type, bool verbo uint8_t i = cl.length(); shell.print(" "); if (cf.has_flags(MQTT_SUB_FLAG_HC)) { - shell.print("[hc] "); - i += 5; + shell.print("[hc.]"); + i += 8; } else if (cf.has_flags(MQTT_SUB_FLAG_WWC)) { - shell.print("[wwc] "); - i += 6; + shell.print("[wwc.]"); + i += 9; } shell.print(cl); // pad with spaces @@ -299,10 +400,10 @@ void Command::show(uuid::console::Shell & shell, uint8_t device_type, bool verbo shell.print(' '); } shell.print(uuid::read_flash_string(cf.description_)); - if (cf.has_flags(CommandFlag::ADMIN_ONLY)) { + if (!cf.has_flags(CommandFlag::ADMIN_ONLY)) { shell.print(' '); shell.print(COLOR_BRIGHT_RED); - shell.print('!'); + shell.print('*'); } shell.print(COLOR_RESET); } @@ -325,7 +426,7 @@ bool Command::device_has_commands(const uint8_t device_type) { } if (device_type == EMSdevice::DeviceType::DALLASSENSOR) { - return true; // we always have Sensor, but should check if there are actual sensors attached! + return (EMSESP::sensor_devices().size() != 0); } for (const auto & emsdevice : EMSESP::emsdevices) { @@ -363,10 +464,11 @@ void Command::show_devices(uuid::console::Shell & shell) { // output list of all commands to console // calls show with verbose mode set void Command::show_all(uuid::console::Shell & shell) { - shell.println(F("Available commands (!=requires authorization): ")); + shell.println(F("Available commands (*=do not need authorization): ")); // show system first shell.print(COLOR_BOLD_ON); + shell.print(COLOR_YELLOW); shell.printf(" %s: ", EMSdevice::device_type_2_device_name(EMSdevice::DeviceType::SYSTEM).c_str()); shell.print(COLOR_RESET); show(shell, EMSdevice::DeviceType::SYSTEM, true); @@ -374,6 +476,7 @@ void Command::show_all(uuid::console::Shell & shell) { // show sensor if (EMSESP::have_sensors()) { shell.print(COLOR_BOLD_ON); + shell.print(COLOR_YELLOW); shell.printf(" %s: ", EMSdevice::device_type_2_device_name(EMSdevice::DeviceType::DALLASSENSOR).c_str()); shell.print(COLOR_RESET); show(shell, EMSdevice::DeviceType::DALLASSENSOR, true); @@ -383,6 +486,7 @@ void Command::show_all(uuid::console::Shell & shell) { for (const auto & device_class : EMSFactory::device_handlers()) { if (Command::device_has_commands(device_class.first)) { shell.print(COLOR_BOLD_ON); + shell.print(COLOR_YELLOW); shell.printf(" %s: ", EMSdevice::device_type_2_device_name(device_class.first).c_str()); shell.print(COLOR_RESET); show(shell, device_class.first, true); @@ -390,4 +494,84 @@ void Command::show_all(uuid::console::Shell & shell) { } } +/** + * Extract only the path component from the passed URI and normalized it. + * Ex. //one/two////three/// becomes /one/two/three + */ +std::string SUrlParser::path() { + std::string s = "/"; // set up the beginning slash + for (std::string & f : m_folders) { + s += f; + s += "/"; + } + s.pop_back(); // deleting last letter, that is slash '/' + return std::string(s); +} + +SUrlParser::SUrlParser(const char * uri) { + parse(uri); +} + +bool SUrlParser::parse(const char * uri) { + m_folders.clear(); + m_keysvalues.clear(); + enum Type { begin, folder, param, value }; + std::string s; + + const char * c = uri; + enum Type t = Type::begin; + std::string last_param; + + if (c != NULL || *c != '\0') { + do { + if (*c == '/') { + if (s.length() > 0) { + m_folders.push_back(s); + s.clear(); + } + t = Type::folder; + } else if (*c == '?' && (t == Type::folder || t == Type::begin)) { + if (s.length() > 0) { + m_folders.push_back(s); + s.clear(); + } + t = Type::param; + } else if (*c == '=' && (t == Type::param || t == Type::begin)) { + m_keysvalues[s] = ""; + last_param = s; + s.clear(); + t = Type::value; + } else if (*c == '&' && (t == Type::value || t == Type::param || t == Type::begin)) { + if (t == Type::value) { + m_keysvalues[last_param] = s; + } else if ((t == Type::param || t == Type::begin) && (s.length() > 0)) { + m_keysvalues[s] = ""; + last_param = s; + } + t = Type::param; + s.clear(); + } else if (*c == '\0' && s.length() > 0) { + if (t == Type::value) { + m_keysvalues[last_param] = s; + } else if (t == Type::folder || t == Type::begin) { + m_folders.push_back(s); + } else if (t == Type::param) { + m_keysvalues[s] = ""; + last_param = s; + } + s.clear(); + } else if (*c == '\0' && s.length() == 0) { + if (t == Type::param && last_param.length() > 0) { + m_keysvalues[last_param] = ""; + } + s.clear(); + } else { + s += *c; + } + } while (*c++ != '\0'); + } + return true; +} + + } // namespace emsesp diff --git a/src/command.h b/src/command.h index f64a2a49e..3000992a7 100644 --- a/src/command.h +++ b/src/command.h @@ -25,6 +25,7 @@ #include #include #include +#include #include "console.h" @@ -57,7 +58,7 @@ enum CommandRet : uint8_t { }; using cmd_function_p = std::function; -using cmd_json_function_p = std::function; +using cmd_json_function_p = std::function; class Command { public: @@ -101,30 +102,37 @@ class Command { return cmdfunctions_; } - static uint8_t call(const uint8_t device_type, const char * cmd, const char * value, bool authenticated, const int8_t id, JsonObject & json); - static uint8_t call(const uint8_t device_type, const char * cmd, const char * value, bool authenticated, const int8_t id = -1); +#define add_ + static uint8_t call(const uint8_t device_type, const char * cmd, const char * value, bool authenticated, const int8_t id, JsonObject & output); + static uint8_t call(const uint8_t device_type, const char * cmd, const char * value); + + // with normal call back function taking a value and id static void add(const uint8_t device_type, const __FlashStringHelper * cmd, const cmd_function_p cb, const __FlashStringHelper * description, uint8_t flags = CommandFlag::MQTT_SUB_FLAG_DEFAULT); - static void add_json(const uint8_t device_type, - const __FlashStringHelper * cmd, - const cmd_json_function_p cb, - const __FlashStringHelper * description, - uint8_t flags = CommandFlag::MQTT_SUB_FLAG_DEFAULT); + // callback function taking value, id and a json object for its output + static void add(const uint8_t device_type, + const __FlashStringHelper * cmd, + const cmd_json_function_p cb, + const __FlashStringHelper * description, + uint8_t flags = CommandFlag::MQTT_SUB_FLAG_DEFAULT); static void show_all(uuid::console::Shell & shell); static Command::CmdFunction * find_command(const uint8_t device_type, const char * cmd); - static Command::CmdFunction * find_command(const uint8_t device_type, char * cmd, int8_t & id); static void show(uuid::console::Shell & shell, uint8_t device_type, bool verbose); static void show_devices(uuid::console::Shell & shell); static bool device_has_commands(const uint8_t device_type); - static bool list(const uint8_t device_type, JsonObject & json); + static bool list(const uint8_t device_type, JsonObject & output); + + static uint8_t process(const char * path, const bool authenticated, const JsonObject & input, JsonObject & output); + + static const char * parse_command_string(const char * command, int8_t & id); private: static uuid::log::Logger logger_; @@ -132,6 +140,31 @@ class Command { static std::vector cmdfunctions_; // list of commands }; +typedef std::unordered_map KeyValueMap_t; +typedef std::vector Folder_t; + +class SUrlParser { + private: + KeyValueMap_t m_keysvalues; + Folder_t m_folders; + + public: + SUrlParser(){}; + SUrlParser(const char * url); + + bool parse(const char * url); + + Folder_t & paths() { + return m_folders; + }; + + KeyValueMap_t & params() { + return m_keysvalues; + }; + + std::string path(); +}; + } // namespace emsesp #endif \ No newline at end of file diff --git a/src/dallassensor.cpp b/src/dallassensor.cpp index 1a8e96b7a..750444732 100644 --- a/src/dallassensor.cpp +++ b/src/dallassensor.cpp @@ -41,15 +41,15 @@ void DallasSensor::start() { bus_.begin(dallas_gpio_); #endif // API calls - Command::add_json( + Command::add( EMSdevice::DeviceType::DALLASSENSOR, F_(info), - [&](const char * value, const int8_t id, JsonObject & json) { return command_info(value, id, json); }, + [&](const char * value, const int8_t id, JsonObject & output) { return command_info(value, id, output); }, F_(info_cmd)); - Command::add_json( + Command::add( EMSdevice::DeviceType::DALLASSENSOR, F_(commands), - [&](const char * value, const int8_t id, JsonObject & json) { return command_commands(value, id, json); }, + [&](const char * value, const int8_t id, JsonObject & output) { return command_commands(value, id, output); }, F_(commands_cmd)); } } @@ -79,7 +79,7 @@ void DallasSensor::loop() { if (state_ == State::IDLE) { if (time_now - last_activity_ >= READ_INTERVAL_MS) { #ifdef EMSESP_DEBUG_SENSOR - LOG_DEBUG(F("Read sensor temperature")); + LOG_DEBUG(F("[DEBUG] Read sensor temperature")); #endif if (bus_.reset() || parasite_) { YIELD; @@ -446,14 +446,14 @@ bool DallasSensor::updated_values() { } // list commands -bool DallasSensor::command_commands(const char * value, const int8_t id, JsonObject & json) { - return Command::list(EMSdevice::DeviceType::DALLASSENSOR, json); +bool DallasSensor::command_commands(const char * value, const int8_t id, JsonObject & output) { + return Command::list(EMSdevice::DeviceType::DALLASSENSOR, output); } // creates JSON doc from values // returns false if empty // e.g. dallassensor_data = {"sensor1":{"id":"28-EA41-9497-0E03-5F","temp":23.30},"sensor2":{"id":"28-233D-9497-0C03-8B","temp":24.0}} -bool DallasSensor::command_info(const char * value, const int8_t id, JsonObject & json) { +bool DallasSensor::command_info(const char * value, const int8_t id, JsonObject & output) { if (sensors_.size() == 0) { return false; } @@ -463,21 +463,21 @@ bool DallasSensor::command_info(const char * value, const int8_t id, JsonObject char sensorID[10]; // sensor{1-n} snprintf(sensorID, 10, "sensor%d", i++); if (id == -1) { // show number and id - JsonObject dataSensor = json.createNestedObject(sensorID); + JsonObject dataSensor = output.createNestedObject(sensorID); dataSensor["id"] = sensor.to_string(); if (Helpers::hasValue(sensor.temperature_c)) { dataSensor["temp"] = (float)(sensor.temperature_c) / 10; } } else { // show according to format if (dallas_format_ == Dallas_Format::NUMBER && Helpers::hasValue(sensor.temperature_c)) { - json[sensorID] = (float)(sensor.temperature_c) / 10; + output[sensorID] = (float)(sensor.temperature_c) / 10; } else if (Helpers::hasValue(sensor.temperature_c)) { - json[sensor.to_string()] = (float)(sensor.temperature_c) / 10; + output[sensor.to_string()] = (float)(sensor.temperature_c) / 10; } } } - return (json.size() > 0); + return (output.size() > 0); } // send all dallas sensor values as a JSON package to MQTT diff --git a/src/dallassensor.h b/src/dallassensor.h index 7e46ab78d..e960e6076 100644 --- a/src/dallassensor.h +++ b/src/dallassensor.h @@ -130,8 +130,8 @@ class DallasSensor { int16_t get_temperature_c(const uint8_t addr[]); uint64_t get_id(const uint8_t addr[]); - bool command_info(const char * value, const int8_t id, JsonObject & json); - bool command_commands(const char * value, const int8_t id, JsonObject & json); + bool command_info(const char * value, const int8_t id, JsonObject & output); + bool command_commands(const char * value, const int8_t id, JsonObject & output); void delete_ha_config(uint8_t index, const char * name); diff --git a/src/default_settings.h b/src/default_settings.h index c8fb3a4dc..624735d79 100644 --- a/src/default_settings.h +++ b/src/default_settings.h @@ -148,8 +148,8 @@ #define EMSESP_DEFAULT_NESTED_FORMAT 1 #endif -#ifndef EMSESP_DEFAULT_SUBSCRIBE_FORMAT -#define EMSESP_DEFAULT_SUBSCRIBE_FORMAT 0 +#ifndef EMSESP_DEFAULT_SEND_RESPONSE +#define EMSESP_DEFAULT_SEND_RESPONSE false #endif #ifndef EMSESP_DEFAULT_SOLAR_MAXFLOW diff --git a/src/devices/boiler.cpp b/src/devices/boiler.cpp index 1d0fec3eb..f3a0a56a9 100644 --- a/src/devices/boiler.cpp +++ b/src/devices/boiler.cpp @@ -26,8 +26,6 @@ uuid::log::Logger Boiler::logger_{F_(boiler), uuid::log::Facility::CONSOLE}; Boiler::Boiler(uint8_t device_type, int8_t device_id, uint8_t product_id, const std::string & version, const std::string & name, uint8_t flags, uint8_t brand) : EMSdevice(device_type, device_id, product_id, version, name, flags, brand) { - LOG_DEBUG(F("Adding new Boiler with device ID 0x%02X"), device_id); - // cascaded heatingsources, only some values per individual heatsource (hs) if (device_id != EMSdevice::EMS_DEVICE_ID_BOILER) { uint8_t hs = device_id - EMSdevice::EMS_DEVICE_ID_BOILER_1; // heating source id, count from 0 diff --git a/src/devices/heatpump.cpp b/src/devices/heatpump.cpp index 3e5b4f584..d4c064c29 100644 --- a/src/devices/heatpump.cpp +++ b/src/devices/heatpump.cpp @@ -26,8 +26,6 @@ uuid::log::Logger Heatpump::logger_{F_(heatpump), uuid::log::Facility::CONSOLE}; Heatpump::Heatpump(uint8_t device_type, uint8_t device_id, uint8_t product_id, const std::string & version, const std::string & name, uint8_t flags, uint8_t brand) : EMSdevice(device_type, device_id, product_id, version, name, flags, brand) { - LOG_DEBUG(F("Adding new Heat Pump module with device ID 0x%02X"), device_id); - // telegram handlers register_telegram_type(0x042B, F("HP1"), true, MAKE_PF_CB(process_HPMonitor1)); register_telegram_type(0x047B, F("HP2"), true, MAKE_PF_CB(process_HPMonitor2)); diff --git a/src/devices/mixer.cpp b/src/devices/mixer.cpp index baaa9870f..460738c72 100644 --- a/src/devices/mixer.cpp +++ b/src/devices/mixer.cpp @@ -26,8 +26,6 @@ uuid::log::Logger Mixer::logger_{F_(mixer), uuid::log::Facility::CONSOLE}; Mixer::Mixer(uint8_t device_type, uint8_t device_id, uint8_t product_id, const std::string & version, const std::string & name, uint8_t flags, uint8_t brand) : EMSdevice(device_type, device_id, product_id, version, name, flags, brand) { - LOG_DEBUG(F("Adding new Mixer with device ID 0x%02X"), device_id); - // Pool module if (flags == EMSdevice::EMS_DEVICE_FLAG_MP) { register_telegram_type(0x5BA, F("HpPoolStatus"), true, MAKE_PF_CB(process_HpPoolStatus)); diff --git a/src/devices/solar.cpp b/src/devices/solar.cpp index f6ce14065..a6921848f 100644 --- a/src/devices/solar.cpp +++ b/src/devices/solar.cpp @@ -26,8 +26,6 @@ uuid::log::Logger Solar::logger_{F_(solar), uuid::log::Facility::CONSOLE}; Solar::Solar(uint8_t device_type, uint8_t device_id, uint8_t product_id, const std::string & version, const std::string & name, uint8_t flags, uint8_t brand) : EMSdevice(device_type, device_id, product_id, version, name, flags, brand) { - LOG_DEBUG(F("Adding new Solar module with device ID 0x%02X"), device_id); - // telegram handlers if (flags == EMSdevice::EMS_DEVICE_FLAG_SM10) { register_telegram_type(0x97, F("SM10Monitor"), false, MAKE_PF_CB(process_SM10Monitor)); diff --git a/src/devices/switch.cpp b/src/devices/switch.cpp index 645ff3b76..0514f0a26 100644 --- a/src/devices/switch.cpp +++ b/src/devices/switch.cpp @@ -28,8 +28,6 @@ uuid::log::Logger Switch::logger_ { Switch::Switch(uint8_t device_type, uint8_t device_id, uint8_t product_id, const std::string & version, const std::string & name, uint8_t flags, uint8_t brand) : EMSdevice(device_type, device_id, product_id, version, name, flags, brand) { - LOG_DEBUG(F("Adding new Switch with device ID 0x%02X"), device_id); - register_telegram_type(0x9C, F("WM10MonitorMessage"), false, MAKE_PF_CB(process_WM10MonitorMessage)); register_telegram_type(0x9D, F("WM10SetMessage"), false, MAKE_PF_CB(process_WM10SetMessage)); register_telegram_type(0x1E, F("WM10TempMessage"), false, MAKE_PF_CB(process_WM10TempMessage)); diff --git a/src/devices/thermostat.cpp b/src/devices/thermostat.cpp index fdc66e353..a74eeff33 100644 --- a/src/devices/thermostat.cpp +++ b/src/devices/thermostat.cpp @@ -115,7 +115,7 @@ Thermostat::Thermostat(uint8_t device_type, uint8_t device_id, uint8_t product_i monitor_typeids = {0x02A5, 0x02A6, 0x02A7, 0x02A8}; set_typeids = {}; for (uint8_t i = 0; i < monitor_typeids.size(); i++) { - register_telegram_type(monitor_typeids[i], F("CRFMonitor"), true, MAKE_PF_CB(process_CRFMonitor)); + register_telegram_type(monitor_typeids[i], F("CRFMonitor"), false, MAKE_PF_CB(process_CRFMonitor)); } // RC300/RC100 @@ -161,18 +161,14 @@ Thermostat::Thermostat(uint8_t device_type, uint8_t device_id, uint8_t product_i } } - // reserve some memory for the heating circuits (max 4 to start with) - heating_circuits_.reserve(4); - if (actual_master_thermostat != device_id) { - LOG_DEBUG(F("Adding new thermostat with device ID 0x%02X"), device_id); return; // don't fetch data if more than 1 thermostat } // // this next section is only for the master thermostat.... // - LOG_DEBUG(F("Adding new thermostat with device ID 0x%02X (as master)"), device_id); + LOG_DEBUG(F("Setting this thermostat (device ID 0x%02X) to be the master"), device_id); // register device values for common values (not heating circuit) register_device_values(); @@ -340,6 +336,7 @@ std::shared_ptr Thermostat::heating_circuit(std::sha if (!toggle_) { return nullptr; } + /* * at this point we have discovered a new heating circuit */ @@ -458,7 +455,7 @@ void Thermostat::publish_ha_config_hc(uint8_t hc_num) { // enable the a special "thermostat_hc" topic to take both mode strings and floats for each of the heating circuits std::string topic2(Mqtt::MQTT_TOPIC_MAX_SIZE, '\0'); snprintf(&topic2[0], topic2.capacity() + 1, "thermostat_hc%d", hc_num); - register_mqtt_topic(topic2, [=](const char * m) { return thermostat_ha_cmd(m, hc_num); }); + Mqtt::subscribe(EMSdevice::DeviceType::THERMOSTAT, topic2, [=](const char * m) { return thermostat_ha_cmd(m, hc_num); }); } // for HA specifically when receiving over MQTT in the thermostat topic diff --git a/src/emsdevice.cpp b/src/emsdevice.cpp index a282a1bb4..a092cbf57 100644 --- a/src/emsdevice.cpp +++ b/src/emsdevice.cpp @@ -222,6 +222,10 @@ const std::string EMSdevice::device_type_2_device_name(const uint8_t device_type // returns device_type from a string uint8_t EMSdevice::device_name_2_device_type(const char * topic) { + if (!topic) { + return DeviceType::UNKNOWN; // nullptr + } + // convert topic to lowercase and compare char lowtopic[20]; strlcpy(lowtopic, topic, sizeof(lowtopic)); @@ -368,7 +372,7 @@ bool EMSdevice::is_fetch(uint16_t telegram_id) { } // list of registered device entries, adding the HA entity if it exists -void EMSdevice::list_device_entries(JsonObject & json) { +void EMSdevice::list_device_entries(JsonObject & output) { for (const auto & dv : devicevalues_) { if (dv_is_visible(dv) && dv.type != DeviceValueType::CMD) { // if we have a tag prefix it @@ -379,7 +383,7 @@ void EMSdevice::list_device_entries(JsonObject & json) { snprintf(key, 50, "%s", uuid::read_flash_string(dv.short_name).c_str()); } - JsonArray details = json.createNestedArray(key); + JsonArray details = output.createNestedArray(key); // add the full name description details.add(dv.full_name); @@ -455,24 +459,23 @@ void EMSdevice::show_mqtt_handlers(uuid::console::Shell & shell) { Mqtt::show_topic_handlers(shell, device_type_); } -void EMSdevice::register_mqtt_topic(const std::string & topic, const mqtt_sub_function_p f) { - Mqtt::subscribe(device_type_, topic, f); -} - // register a callback function for a specific telegram type void EMSdevice::register_telegram_type(const uint16_t telegram_type_id, const __FlashStringHelper * telegram_type_name, bool fetch, const process_function_p f) { telegram_functions_.emplace_back(telegram_type_id, telegram_type_name, fetch, f); } -// add to device value library +// add to device value library, also know now as a "device entity" // arguments are: // tag: to be used to group mqtt together, either as separate topics as a nested object -// value: pointer to the value from the .h file +// value_p: pointer to the value from the .h file // type: one of DeviceValueType // options: options for enum or a divider for int (e.g. F("10")) // short_name: used in Mqtt as keys // full_name: used in Web and Console unless empty (nullptr) // uom: unit of measure from DeviceValueUOM +// has_cmd: true if this is an associated command +// min: min allowed value +// max: max allowed value void EMSdevice::register_device_value(uint8_t tag, void * value_p, uint8_t type, @@ -483,7 +486,7 @@ void EMSdevice::register_device_value(uint8_t tag, bool has_cmd, int32_t min, uint32_t max) { - // init the value depending on it's type + // initialize the device value depending on it's type if (type == DeviceValueType::STRING) { *(char *)(value_p) = {'\0'}; } else if (type == DeviceValueType::INT) { @@ -523,25 +526,32 @@ void EMSdevice::register_device_value(uint8_t tag, const cmd_function_p f, int32_t min, uint32_t max) { - register_device_value(tag, value_p, type, options, name[0], name[1], uom, (f != nullptr), min, max); + auto short_name = name[0]; + auto full_name = name[1]; + + register_device_value(tag, value_p, type, options, short_name, full_name, uom, (f != nullptr), min, max); // add a new command if it has a function attached if (f == nullptr) { return; } + uint8_t flags = CommandFlag::ADMIN_ONLY; // executing commands require admin privileges + if (tag >= TAG_HC1 && tag <= TAG_HC4) { - Command::add(device_type_, name[0], f, name[1], CommandFlag::MQTT_SUB_FLAG_HC | CommandFlag::ADMIN_ONLY); + flags |= CommandFlag::MQTT_SUB_FLAG_HC; } else if (tag >= TAG_WWC1 && tag <= TAG_WWC4) { - Command::add(device_type_, name[0], f, name[1], CommandFlag::MQTT_SUB_FLAG_WWC | CommandFlag::ADMIN_ONLY); + flags |= CommandFlag::MQTT_SUB_FLAG_WWC; } else if (tag == TAG_DEVICE_DATA_WW) { - Command::add(device_type_, name[0], f, name[1], CommandFlag::MQTT_SUB_FLAG_WW | CommandFlag::ADMIN_ONLY); - } else { - Command::add(device_type_, name[0], f, name[1], CommandFlag::ADMIN_ONLY); + flags |= CommandFlag::MQTT_SUB_FLAG_WW; } + + // add the command to our library + // cmd is the short_name and the description is the full_name + Command::add(device_type_, short_name, f, full_name, flags); } -// function with no min and max values +// function with no min and max values (set to 0) void EMSdevice::register_device_value(uint8_t tag, void * value_p, uint8_t type, @@ -552,7 +562,7 @@ void EMSdevice::register_device_value(uint8_t tag, register_device_value(tag, value_p, type, options, name, uom, f, 0, 0); } -// no command function +// no associated command function, or min/max values void EMSdevice::register_device_value(uint8_t tag, void * value_p, uint8_t type, @@ -592,9 +602,9 @@ const std::string EMSdevice::get_value_uom(const char * key) { // prepare array of device values used for the WebUI // v = value, u=uom, n=name, c=cmd -void EMSdevice::generate_values_json_web(JsonObject & json) { - json["name"] = to_string_short(); - JsonArray data = json.createNestedArray("data"); +void EMSdevice::generate_values_json_web(JsonObject & output) { + output["name"] = to_string_short(); + JsonArray data = output.createNestedArray("data"); for (const auto & dv : devicevalues_) { // ignore if full_name empty and also commands @@ -707,10 +717,11 @@ void EMSdevice::generate_values_json_web(JsonObject & json) { } } -// builds json with specific device value information -// e.g. http://ems-esp/api?device=thermostat&cmd=seltemp -bool EMSdevice::get_value_info(JsonObject & root, const char * cmd, const int8_t id) { - JsonObject json = root; +// builds json with specific single device value information +// cnd is the endpoint or name of the device entity +// returns false if failed, otherwise true +bool EMSdevice::get_value_info(JsonObject & output, const char * cmd, const int8_t id) { + JsonObject json = output; int8_t tag = id; // check if we have hc or wwc @@ -719,7 +730,7 @@ bool EMSdevice::get_value_info(JsonObject & root, const char * cmd, const int8_t } else if (id >= 8 && id <= 11) { tag = DeviceValueTAG::TAG_WWC1 + id - 8; } else if (id != -1) { - return false; + return false; // error } // search device value with this tag @@ -894,16 +905,18 @@ bool EMSdevice::get_value_info(JsonObject & root, const char * cmd, const int8_t } } + emsesp::EMSESP::logger().err(F("Can't get values for entity '%s'"), cmd); + return false; } // For each value in the device create the json object pair and add it to given json // return false if empty // this is used to create both the MQTT payloads, Console messages and Web API calls -bool EMSdevice::generate_values_json(JsonObject & root, const uint8_t tag_filter, const bool nested, const uint8_t output_target) { +bool EMSdevice::generate_values_json(JsonObject & output, const uint8_t tag_filter, const bool nested, const uint8_t output_target) { bool has_values = false; // to see if we've added a value. it's faster than doing a json.size() at the end uint8_t old_tag = 255; // NAN - JsonObject json = root; + JsonObject json = output; for (auto & dv : devicevalues_) { // conditions @@ -935,7 +948,7 @@ bool EMSdevice::generate_values_json(JsonObject & root, const uint8_t tag_filter if (dv.tag != old_tag) { old_tag = dv.tag; if (nested && have_tag && dv.tag >= DeviceValueTAG::TAG_HC1) { // no nests for boiler tags - json = root.createNestedObject(tag_to_string(dv.tag)); + json = output.createNestedObject(tag_to_string(dv.tag)); } } } diff --git a/src/emsdevice.h b/src/emsdevice.h index 427edae76..66a046816 100644 --- a/src/emsdevice.h +++ b/src/emsdevice.h @@ -238,7 +238,7 @@ class EMSdevice { void show_telegram_handlers(uuid::console::Shell & shell); char * show_telegram_handlers(char * result); void show_mqtt_handlers(uuid::console::Shell & shell); - void list_device_entries(JsonObject & json); + void list_device_entries(JsonObject & output); using process_function_p = std::function)>; @@ -249,9 +249,8 @@ class EMSdevice { bool get_value_info(JsonObject & root, const char * cmd, const int8_t id); enum OUTPUT_TARGET : uint8_t { API_VERBOSE, API, MQTT }; - bool generate_values_json(JsonObject & json, const uint8_t tag_filter, const bool nested, const uint8_t output_target); - - void generate_values_json_web(JsonObject & json); + bool generate_values_json(JsonObject & output, const uint8_t tag_filter, const bool nested, const uint8_t output_target); + void generate_values_json_web(JsonObject & output); void register_device_value(uint8_t tag, void * value_p, @@ -292,8 +291,6 @@ class EMSdevice { void read_command(const uint16_t type_id, uint8_t offset = 0, uint8_t length = 0); - void register_mqtt_topic(const std::string & topic, const mqtt_sub_function_p f); - void publish_mqtt_ha_sensor(); const std::string telegram_type_name(std::shared_ptr telegram); diff --git a/src/emsesp.cpp b/src/emsesp.cpp index d3bd27857..ad061dc29 100644 --- a/src/emsesp.cpp +++ b/src/emsesp.cpp @@ -541,12 +541,8 @@ void EMSESP::publish_sensor_values(const bool time, const bool force) { } } -// MQTT publish a telegram as raw data +// MQTT publish a telegram as raw data to the topic 'response' void EMSESP::publish_response(std::shared_ptr telegram) { - if (!Mqtt::connected()) { - return; - } - StaticJsonDocument doc; char buffer[100]; @@ -576,6 +572,7 @@ bool EMSESP::get_device_value_info(JsonObject & root, const char * cmd, const in } } + // specific for the dallassensor if (devicetype == DeviceType::DALLASSENSOR) { uint8_t i = 1; for (const auto & sensor : EMSESP::sensor_devices()) { @@ -781,7 +778,10 @@ bool EMSESP::process_telegram(std::shared_ptr telegram) { // if watching or reading... if ((telegram->type_id == read_id_) && (telegram->dest == txservice_.ems_bus_id())) { LOG_NOTICE(F("%s"), pretty_telegram(telegram).c_str()); - publish_response(telegram); + if (Mqtt::send_response()) { + publish_response(telegram); + } + if (!read_next_) { read_id_ = WATCH_ID_NONE; } @@ -989,40 +989,47 @@ bool EMSESP::add_device(const uint8_t device_id, const uint8_t product_id, std:: return true; } - Command::add_json( + Command::add( device_type, F_(info), - [device_type](const char * value, const int8_t id, JsonObject & json) { - return command_info(device_type, json, id, EMSdevice::OUTPUT_TARGET::API_VERBOSE); + [device_type](const char * value, const int8_t id, JsonObject & output) { + return command_info(device_type, output, id, EMSdevice::OUTPUT_TARGET::API_VERBOSE); }, F_(info_cmd)); - Command::add_json( + Command::add( device_type, - F("info_short"), - [device_type](const char * value, const int8_t id, JsonObject & json) { return command_info(device_type, json, id, EMSdevice::OUTPUT_TARGET::API); }, + F("list"), + [device_type](const char * value, const int8_t id, JsonObject & output) { return command_info(device_type, output, id, EMSdevice::OUTPUT_TARGET::API); }, nullptr, CommandFlag::HIDDEN); // this command is hidden - Command::add_json( + Command::add( device_type, F_(commands), - [device_type](const char * value, const int8_t id, JsonObject & json) { return command_commands(device_type, json, id); }, + [device_type](const char * value, const int8_t id, JsonObject & output) { return command_commands(device_type, output, id); }, F_(commands_cmd)); - Command::add_json( + Command::add( device_type, F_(entities), - [device_type](const char * value, const int8_t id, JsonObject & json) { return command_entities(device_type, json, id); }, + [device_type](const char * value, const int8_t id, JsonObject & output) { return command_entities(device_type, output, id); }, F_(entities_cmd)); + // MQTT subscribe to the device top-level, e.g. "ems-esp/boiler/#" + std::string topic = EMSdevice::device_type_2_device_name(device_type) + "/#"; + Mqtt::subscribe(device_type, topic, nullptr); // use empty function callback + + // Print to LOG showing we've added a new device + LOG_INFO(F("Recognized new %s with device ID 0x%02X"), EMSdevice::device_type_2_device_name(device_type).c_str(), device_id); + return true; } // list device entities -bool EMSESP::command_entities(uint8_t device_type, JsonObject & json, const int8_t id) { +bool EMSESP::command_entities(uint8_t device_type, JsonObject & output, const int8_t id) { JsonObject node; for (const auto & emsdevice : emsdevices) { if ((emsdevice) && (emsdevice->device_type() == device_type)) { - emsdevice->list_device_entries(json); + emsdevice->list_device_entries(output); return true; } } @@ -1031,14 +1038,14 @@ bool EMSESP::command_entities(uint8_t device_type, JsonObject & json, const int8 } // list all available commands, return as json -bool EMSESP::command_commands(uint8_t device_type, JsonObject & json, const int8_t id) { - return Command::list(device_type, json); +bool EMSESP::command_commands(uint8_t device_type, JsonObject & output, const int8_t id) { + return Command::list(device_type, output); } -// export all values to info command +// export all values // value is ignored here // info command always shows in verbose mode, so full names are displayed -bool EMSESP::command_info(uint8_t device_type, JsonObject & json, const int8_t id, const uint8_t output_target) { +bool EMSESP::command_info(uint8_t device_type, JsonObject & output, const int8_t id, const uint8_t output_target) { bool has_value = false; uint8_t tag; if (id >= 1 && id <= 4) { @@ -1051,13 +1058,10 @@ bool EMSESP::command_info(uint8_t device_type, JsonObject & json, const int8_t i return false; } - // if id=-1 it means we have no endpoint so default to API - uint8_t target = (id == -1) ? EMSdevice::OUTPUT_TARGET::API_VERBOSE : EMSdevice::OUTPUT_TARGET::API; - for (const auto & emsdevice : emsdevices) { if (emsdevice && (emsdevice->device_type() == device_type) && ((device_type != DeviceType::THERMOSTAT) || (emsdevice->device_id() == EMSESP::actual_master_thermostat()))) { - has_value |= emsdevice->generate_values_json(json, tag, (id < 1), target); // nested for id -1 and 0 + has_value |= emsdevice->generate_values_json(output, tag, (id < 1), output_target); // nested for id -1 and 0 } } diff --git a/src/emsesp.h b/src/emsesp.h index b64a6eb01..ed84e1bd3 100644 --- a/src/emsesp.h +++ b/src/emsesp.h @@ -249,9 +249,9 @@ class EMSESP { static void process_version(std::shared_ptr telegram); static void publish_response(std::shared_ptr telegram); static void publish_all_loop(); - static bool command_info(uint8_t device_type, JsonObject & json, const int8_t id, const uint8_t output_target); - static bool command_commands(uint8_t device_type, JsonObject & json, const int8_t id); - static bool command_entities(uint8_t device_type, JsonObject & json, const int8_t id); + static bool command_info(uint8_t device_type, JsonObject & output, const int8_t id, const uint8_t output_target); + static bool command_commands(uint8_t device_type, JsonObject & output, const int8_t id); + static bool command_entities(uint8_t device_type, JsonObject & output, const int8_t id); static constexpr uint32_t EMS_FETCH_FREQUENCY = 60000; // check every minute static uint32_t last_fetch_; diff --git a/src/locale_EN.h b/src/locale_EN.h index 6dce234f9..62cf72873 100644 --- a/src/locale_EN.h +++ b/src/locale_EN.h @@ -102,7 +102,7 @@ MAKE_PSTR_WORD(unknown) MAKE_PSTR_WORD(Dallassensor) // format strings -MAKE_PSTR(master_thermostat_fmt, "Master Thermostat Device ID: %s") +MAKE_PSTR(master_thermostat_fmt, "Master Thermostat device ID: %s") MAKE_PSTR(host_fmt, "Host: %s") MAKE_PSTR(port_fmt, "Port: %d") MAKE_PSTR(hostname_fmt, "Hostname: %s") @@ -510,7 +510,7 @@ MAKE_PSTR_LIST(wwTempOK, F("wwtempok"), F("temperature ok")) MAKE_PSTR_LIST(wwActive, F("wwactive"), F("active")) MAKE_PSTR_LIST(wwHeat, F("wwheat"), F("heating")) MAKE_PSTR_LIST(wwSetPumpPower, F("wwsetpumppower"), F("set pump power")) -MAKE_PSTR_LIST(wwMixerTemp, F("wwMixerTemp"), F("mixer temperature")) +MAKE_PSTR_LIST(wwMixerTemp, F("wwmixertemp"), F("mixer temperature")) MAKE_PSTR_LIST(wwTankMiddleTemp, F("wwtankmiddletemp"), F("tank middle temperature (TS3)")) MAKE_PSTR_LIST(wwStarts, F("wwstarts"), F("starts")) MAKE_PSTR_LIST(wwWorkM, F("wwworkm"), F("active time")) diff --git a/src/mqtt.cpp b/src/mqtt.cpp index a8f6620ab..22318c504 100644 --- a/src/mqtt.cpp +++ b/src/mqtt.cpp @@ -38,7 +38,7 @@ bool Mqtt::mqtt_enabled_; uint8_t Mqtt::ha_climate_format_; bool Mqtt::ha_enabled_; uint8_t Mqtt::nested_format_; -uint8_t Mqtt::subscribe_format_; +bool Mqtt::send_response_; std::deque Mqtt::mqtt_messages_; std::vector Mqtt::mqtt_subfunctions_; @@ -54,16 +54,17 @@ uuid::log::Logger Mqtt::logger_{F_(mqtt), uuid::log::Facility::DAEMON}; // subscribe to an MQTT topic, and store the associated callback function // only if it already hasn't been added +// topics exclude the base void Mqtt::subscribe(const uint8_t device_type, const std::string & topic, mqtt_sub_function_p cb) { // check if we already have the topic subscribed for this specific device type, if so don't add it again + // add the function (in case its not there) and quit because it already exists if (!mqtt_subfunctions_.empty()) { for (auto & mqtt_subfunction : mqtt_subfunctions_) { if ((mqtt_subfunction.device_type_ == device_type) && (strcmp(mqtt_subfunction.topic_.c_str(), topic.c_str()) == 0)) { - // add the function (in case its not there) and quit because it already exists if (cb) { mqtt_subfunction.mqtt_subfunction_ = cb; } - return; + return; // exit - don't add } } } @@ -80,86 +81,23 @@ void Mqtt::subscribe(const uint8_t device_type, const std::string & topic, mqtt_ queue_subscribe_message(topic); } -// subscribe to the command topic if it doesn't exist yet -void Mqtt::sub_command(const uint8_t device_type, const __FlashStringHelper * cmd, cmdfunction_p cb, uint8_t flags) { - if (!mqtt_enabled_) { - return; - } - - std::string topic = EMSdevice::device_type_2_device_name(device_type); // thermostat, boiler, etc... - - // see if we have already a handler for the device type (boiler, thermostat). If not add it - bool exists = false; - if (!mqtt_subfunctions_.empty()) { - for (const auto & mqtt_subfunction : mqtt_subfunctions_) { - if ((mqtt_subfunction.device_type_ == device_type) && (strcmp(mqtt_subfunction.topic_.c_str(), topic.c_str()) == 0)) { - exists = true; - } - } - } - - if (!exists) { - Mqtt::subscribe(device_type, topic, nullptr); // use an empty function handler to signal this is a command function only (e.g. ems-esp/boiler) - } - - // add the individual commands too (e.g. ems-esp/boiler/wwonetime) - // https://github.com/emsesp/EMS-ESP32/issues/31 - if (subscribe_format_ == Subscribe_Format::INDIVIDUAL_ALL_HC && ((flags & CommandFlag::MQTT_SUB_FLAG_HC) == CommandFlag::MQTT_SUB_FLAG_HC)) { - std::string hc_topic(MQTT_TOPIC_MAX_SIZE, '\0'); - hc_topic = topic + "/hc1/" + uuid::read_flash_string(cmd); - queue_subscribe_message(hc_topic); - hc_topic = topic + "/hc2/" + uuid::read_flash_string(cmd); - queue_subscribe_message(hc_topic); - hc_topic = topic + "/hc3/" + uuid::read_flash_string(cmd); - queue_subscribe_message(hc_topic); - hc_topic = topic + "/hc4/" + uuid::read_flash_string(cmd); - queue_subscribe_message(hc_topic); - } else if (subscribe_format_ != Subscribe_Format::GENERAL && ((flags & CommandFlag::MQTT_SUB_FLAG_NOSUB) != CommandFlag::MQTT_SUB_FLAG_NOSUB)) { - std::string hc_topic(MQTT_TOPIC_MAX_SIZE, '\0'); - hc_topic = topic + "/" + uuid::read_flash_string(cmd); - queue_subscribe_message(hc_topic); - } -} - -// subscribe to an MQTT topic, and store the associated callback function -// For generic functions not tied to a specific device -void Mqtt::subscribe(const std::string & topic, mqtt_sub_function_p cb) { - subscribe(0, topic, cb); // no device_id needed if generic to EMS-ESP -} - // resubscribe to all MQTT topics +// if it's already in the queue, ignore it void Mqtt::resubscribe() { if (mqtt_subfunctions_.empty()) { return; } for (const auto & mqtt_subfunction : mqtt_subfunctions_) { - // if it's already in the queue, ignore it bool found = false; for (const auto & message : mqtt_messages_) { found |= ((message.content_->operation == Operation::SUBSCRIBE) && (mqtt_subfunction.topic_ == message.content_->topic)); } + if (!found) { queue_subscribe_message(mqtt_subfunction.topic_); } } - - for (const auto & cf : Command::commands()) { - std::string topic(MQTT_TOPIC_MAX_SIZE, '\0'); - if (subscribe_format_ == Subscribe_Format::INDIVIDUAL_ALL_HC && cf.has_flags(CommandFlag::MQTT_SUB_FLAG_HC)) { - topic = EMSdevice::device_type_2_device_name(cf.device_type_) + "/hc1/" + uuid::read_flash_string(cf.cmd_); - queue_subscribe_message(topic); - topic = EMSdevice::device_type_2_device_name(cf.device_type_) + "/hc2/" + uuid::read_flash_string(cf.cmd_); - queue_subscribe_message(topic); - topic = EMSdevice::device_type_2_device_name(cf.device_type_) + "/hc3/" + uuid::read_flash_string(cf.cmd_); - queue_subscribe_message(topic); - topic = EMSdevice::device_type_2_device_name(cf.device_type_) + "/hc4/" + uuid::read_flash_string(cf.cmd_); - queue_subscribe_message(topic); - } else if (subscribe_format_ != Subscribe_Format::GENERAL && !cf.has_flags(CommandFlag::MQTT_SUB_FLAG_NOSUB)) { - topic = EMSdevice::device_type_2_device_name(cf.device_type_) + "/" + uuid::read_flash_string(cf.cmd_); - queue_subscribe_message(topic); - } - } } // Main MQTT loop - sends out top item on publish queue @@ -230,33 +168,6 @@ void Mqtt::show_mqtt(uuid::console::Shell & shell) { for (const auto & mqtt_subfunction : mqtt_subfunctions_) { shell.printfln(F(" %s/%s"), mqtt_base_.c_str(), mqtt_subfunction.topic_.c_str()); } - - // now show the commands... - for (const auto & cf : Command::commands()) { - if (subscribe_format_ == Subscribe_Format::INDIVIDUAL_ALL_HC && cf.has_flags(CommandFlag::MQTT_SUB_FLAG_HC)) { - shell.printfln(F(" %s/%s/hc1/%s"), - mqtt_base_.c_str(), - EMSdevice::device_type_2_device_name(cf.device_type_).c_str(), - uuid::read_flash_string(cf.cmd_).c_str()); - shell.printfln(F(" %s/%s/hc2/%s"), - mqtt_base_.c_str(), - EMSdevice::device_type_2_device_name(cf.device_type_).c_str(), - uuid::read_flash_string(cf.cmd_).c_str()); - shell.printfln(F(" %s/%s/hc3/%s"), - mqtt_base_.c_str(), - EMSdevice::device_type_2_device_name(cf.device_type_).c_str(), - uuid::read_flash_string(cf.cmd_).c_str()); - shell.printfln(F(" %s/%s/hc4/%s"), - mqtt_base_.c_str(), - EMSdevice::device_type_2_device_name(cf.device_type_).c_str(), - uuid::read_flash_string(cf.cmd_).c_str()); - } else if (subscribe_format_ != Subscribe_Format::GENERAL && !cf.has_flags(CommandFlag::MQTT_SUB_FLAG_NOSUB)) { - shell.printfln(F(" %s/%s/%s"), - mqtt_base_.c_str(), - EMSdevice::device_type_2_device_name(cf.device_type_).c_str(), - uuid::read_flash_string(cf.cmd_).c_str()); - } - } shell.println(); // show queues @@ -301,129 +212,75 @@ void Mqtt::show_mqtt(uuid::console::Shell & shell) { shell.println(); } +#if defined(EMSESP_DEBUG) // simulate receiving a MQTT message, used only for testing void Mqtt::incoming(const char * topic, const char * payload) { on_message(topic, payload, strlen(payload)); } +#endif // received an MQTT message that we subscribed too -void Mqtt::on_message(const char * fulltopic, const char * payload, size_t len) { - if (len == 0) { - LOG_DEBUG(F("Received empty message %s"), fulltopic); - return; // ignore empty payloads - } - if (strncmp(fulltopic, mqtt_base_.c_str(), strlen(mqtt_base_.c_str())) != 0) { - LOG_DEBUG(F("Received unknown message %s - %s"), fulltopic, payload); - return; // not for us - } - char topic[100]; - strlcpy(topic, &fulltopic[1 + strlen(mqtt_base_.c_str())], 100); - - // strip the topic substrings - char * topic_end = strchr(topic, '/'); - if (topic_end != nullptr) { - topic_end[0] = '\0'; - } - +// topic is the full path +// payload is json or a single string and converted to a json with key 'value' +void Mqtt::on_message(const char * topic, const char * payload, size_t len) { + // sometimes the payload is not terminated correctly, so make a copy // convert payload to a null-terminated char string char message[len + 2]; strlcpy(message, payload, len + 1); - LOG_DEBUG(F("Received %s => %s (length %d)"), topic, message, len); +#if defined(EMSESP_DEBUG) + if (len) { + LOG_DEBUG(F("Received topic `%s` => payload `%s` (length %d)"), topic, message, len); + } else { + LOG_DEBUG(F("Received topic `%s`"), topic); + } +#endif - // see if we have this topic in our subscription list, then call its callback handler + // check first againts any of our subscribed topics for (const auto & mf : mqtt_subfunctions_) { - if (strcmp(topic, mf.topic_.c_str()) == 0) { - // if we have callback function then call it - // otherwise proceed as process as a command + // add the base back + char full_topic[MQTT_TOPIC_MAX_SIZE]; + snprintf(full_topic, sizeof(full_topic), "%s/%s", mqtt_base_.c_str(), mf.topic_.c_str()); + if (!strcmp(topic, full_topic)) { if (mf.mqtt_subfunction_) { if (!(mf.mqtt_subfunction_)(message)) { - LOG_ERROR(F("MQTT error: invalid payload %s for this topic %s"), message, topic); - Mqtt::publish(F_(response), "invalid"); + LOG_ERROR(F("error: invalid payload %s for this topic %s"), message, topic); + if (send_response_) { + Mqtt::publish(F_(response), "error: invalid data"); + } } return; } - - // check if it's not json, then try and extract the command from the topic name - if (message[0] != '{') { - // get topic with substrings again - strlcpy(topic, &fulltopic[1 + strlen(mqtt_base_.c_str())], 100); - char * cmd_only = strchr(topic, '/'); - if (cmd_only == NULL) { - return; // invalid topic name - } - cmd_only++; // skip the / - // LOG_INFO(F("devicetype= %d, topic = %s, cmd = %s, message = %s), mf.device_type_, topic, cmd_only, message); - // call command, assume admin authentication is allowed - uint8_t cmd_return = Command::call(mf.device_type_, cmd_only, message, true); - if (cmd_return == CommandRet::NOT_FOUND) { - LOG_ERROR(F("No matching cmd (%s) in topic %s"), cmd_only, topic); - Mqtt::publish(F_(response), "unknown"); - } else if (cmd_return != CommandRet::OK) { - LOG_ERROR(F("Invalid data with cmd (%s) in topic %s"), cmd_only, topic); - Mqtt::publish(F_(response), "unknown"); - } - return; - } - - // It's a command then with the payload being JSON like {"cmd":"", "data":, "id":} - // Find the command from the json and call it directly - StaticJsonDocument doc; - DeserializationError error = deserializeJson(doc, message); - if (error) { - LOG_ERROR(F("MQTT error: payload %s, error %s"), message, error.c_str()); - return; - } - - const char * command = doc["cmd"]; - if (command == nullptr) { - LOG_ERROR(F("MQTT error: invalid payload cmd format. message=%s"), message); - return; - } - - // check for hc and id, and convert to int - int8_t n = -1; // no value - if (doc.containsKey("hc")) { - n = doc["hc"]; - } else if (doc.containsKey("id")) { - n = doc["id"]; - } - - uint8_t cmd_return = CommandRet::OK; - JsonVariant data = doc["data"]; - - if (data.is()) { - cmd_return = Command::call(mf.device_type_, command, data.as(), true, n); - } else if (data.is()) { - char data_str[10]; - cmd_return = Command::call(mf.device_type_, command, Helpers::itoa(data_str, (int16_t)data.as()), true, n); - } else if (data.is()) { - char data_str[10]; - cmd_return = Command::call(mf.device_type_, command, Helpers::render_value(data_str, (float)data.as(), 2), true, n); - } else if (data.isNull()) { - DynamicJsonDocument resp(EMSESP_JSON_SIZE_XLARGE_DYN); - JsonObject json = resp.to(); - cmd_return = Command::call(mf.device_type_, command, "", true, n, json); - if (json.size()) { - Mqtt::publish(F_(response), resp.as()); - return; - } - } - - if (cmd_return == CommandRet::NOT_FOUND) { - LOG_ERROR(F("No matching cmd (%s)"), command); - Mqtt::publish(F_(response), "unknown"); - } else if (cmd_return != CommandRet::OK) { - LOG_ERROR(F("Invalid data for cmd (%s)"), command); - Mqtt::publish(F_(response), "unknown"); - } - - return; } } - // if we got here we didn't find a topic match - LOG_ERROR(F("No MQTT handler found for topic %s and payload %s"), topic, message); + // convert payload into a json doc, if it's not empty + // if the payload is a single parameter (not JSON) create a JSON with the key 'value' + StaticJsonDocument input; + if (len != 0) { + DeserializationError error = deserializeJson(input, message); + if (error == DeserializationError::Code::InvalidInput) { + input.clear(); // this is important to clear first + input["value"] = (const char *)message; // always a string + } + } + + // parse and call the command + StaticJsonDocument output_doc; + JsonObject output = output_doc.to(); + uint8_t return_code = Command::process(topic, true, input.as(), output); // mqtt is always authenticated + + + // send MQTT response if enabled + if (!send_response_ || output.isNull()) { + return; + } + + if (return_code != CommandRet::OK) { + Mqtt::publish(F_(response), (const char *)output["message"]); + } else { + Mqtt::publish(F_(response), output); // output response from call + } } // print all the topics related to a specific device type @@ -438,7 +295,7 @@ void Mqtt::show_topic_handlers(uuid::console::Shell & shell, const uint8_t devic shell.print(F(" Subscribed MQTT topics: ")); for (const auto & mqtt_subfunction : mqtt_subfunctions_) { if (mqtt_subfunction.device_type_ == device_type) { - shell.printf(F("%s/%s "), mqtt_base_.c_str(), mqtt_subfunction.topic_.c_str()); + shell.printf(F("%s "), mqtt_subfunction.topic_.c_str()); } } shell.println(); @@ -499,7 +356,7 @@ void Mqtt::load_settings() { ha_enabled_ = mqttSettings.ha_enabled; ha_climate_format_ = mqttSettings.ha_climate_format; nested_format_ = mqttSettings.nested_format; - subscribe_format_ = mqttSettings.subscribe_format; + send_response_ = mqttSettings.send_response; // convert to milliseconds publish_time_boiler_ = mqttSettings.publish_time_boiler * 1000; @@ -564,17 +421,12 @@ void Mqtt::start() { mqttClient_->setWill(will_topic, 1, true, "offline"); // with qos 1, retain true mqttClient_->onMessage([this](char * topic, char * payload, AsyncMqttClientMessageProperties properties, size_t len, size_t index, size_t total) { - // receiving mqtt - on_message(topic, payload, len); + on_message(topic, payload, len); // receiving mqtt }); mqttClient_->onPublish([this](uint16_t packetId) { - // publish - on_publish(packetId); + on_publish(packetId); // publish }); - - // register command - Command::add(EMSdevice::DeviceType::SYSTEM, F_(publish), System::command_publish, F("forces a MQTT publish"), CommandFlag::ADMIN_ONLY); } void Mqtt::set_publish_time_boiler(uint16_t publish_time) { @@ -672,8 +524,9 @@ void Mqtt::on_connect() { EMSESP::shower_.send_mqtt_stat(false); // Send shower_activated as false EMSESP::system_.send_heartbeat(); // send heatbeat - // re-subscribe to all MQTT topics + // re-subscribe to all custom registered MQTT topics resubscribe(); + EMSESP::reset_mqtt_ha(); // re-create all HA devices if there are any publish_retain(F("status"), "online", true); // say we're alive to the Last Will topic, with retain on @@ -740,22 +593,27 @@ void Mqtt::ha_status() { } // add sub or pub task to the queue. -// a fully-qualified topic is created by prefixing the base, unless it's HA // returns a pointer to the message created +// the base is not included in the topic std::shared_ptr Mqtt::queue_message(const uint8_t operation, const std::string & topic, const std::string & payload, bool retain) { if (topic.empty()) { return nullptr; } + // if it's a publish and the payload is empty, stop + if ((operation == Operation::PUBLISH) && (payload.empty())) { + return nullptr; + } + // take the topic and prefix the base, unless its for HA std::shared_ptr message; message = std::make_shared(operation, topic, payload, retain); #ifdef EMSESP_DEBUG if (operation == Operation::PUBLISH) { - LOG_INFO("Adding to queue: (Publish) topic='%s' payload=%s", message->topic.c_str(), message->payload.c_str()); + LOG_INFO("[DEBUG] Adding to queue: (Publish) topic='%s' payload=%s", message->topic.c_str(), message->payload.c_str()); } else { - LOG_INFO("Adding to queue: (Subscribe) topic='%s'", message->topic.c_str()); + LOG_INFO("[DEBUG] Adding to queue: (Subscribe) topic='%s'", message->topic.c_str()); } #endif @@ -867,9 +725,9 @@ void Mqtt::process_queue() { auto mqtt_message = mqtt_messages_.front(); auto message = mqtt_message.content_; char topic[MQTT_TOPIC_MAX_SIZE]; + if (message->topic.find(uuid::read_flash_string(F_(homeassistant))) == 0) { - // leave topic as it is - strcpy(topic, message->topic.c_str()); + strcpy(topic, message->topic.c_str()); // leave topic as it is } else { snprintf(topic, MQTT_TOPIC_MAX_SIZE, "%s/%s", mqtt_base_.c_str(), message->topic.c_str()); } @@ -879,7 +737,7 @@ void Mqtt::process_queue() { LOG_DEBUG(F("Subscribing to topic '%s'"), topic); uint16_t packet_id = mqttClient_->subscribe(topic, mqtt_qos_); if (!packet_id) { - LOG_DEBUG(F("Error subscribing to topic '%s'"), topic); + LOG_ERROR(F("Error subscribing to topic '%s'"), topic); } mqtt_messages_.pop_front(); // remove the message from the queue @@ -1150,5 +1008,4 @@ const std::string Mqtt::tag_to_topic(uint8_t device_type, uint8_t tag) { } } - } // namespace emsesp diff --git a/src/mqtt.h b/src/mqtt.h index 372c07633..182a5be25 100644 --- a/src/mqtt.h +++ b/src/mqtt.h @@ -42,7 +42,7 @@ using uuid::console::Shell; #define MQTT_HA_PUBLISH_DELAY 50 // size of queue -#define MAX_MQTT_MESSAGES 200 +#define MAX_MQTT_MESSAGES 300 namespace emsesp { @@ -88,14 +88,6 @@ class Mqtt { }; - // subscribe_format - enum Subscribe_Format : uint8_t { - GENERAL = 0, // 0 - INDIVIDUAL_MAIN_HC, // 1 - INDIVIDUAL_ALL_HC // 2 - - }; - // for Home Assistant enum class State_class { NONE, MEASUREMENT, TOTAL_INCREASING }; enum class Device_class { NONE, TEMPERATURE, POWER_FACTOR, ENERGY, PRESSURE, POWER, SIGNAL_STRENGTH }; @@ -105,7 +97,6 @@ class Mqtt { static void on_connect(); static void subscribe(const uint8_t device_type, const std::string & topic, mqtt_sub_function_p cb); - static void subscribe(const std::string & topic, mqtt_sub_function_p cb); static void resubscribe(); static void publish(const std::string & topic, const std::string & payload); @@ -127,7 +118,6 @@ class Mqtt { const __FlashStringHelper * entity, const uint8_t uom, const bool has_cmd = false); - static void sub_command(const uint8_t device_type, const __FlashStringHelper * cmd, cmdfunction_p cb, uint8_t flags = 0); static void show_topic_handlers(uuid::console::Shell & shell, const uint8_t device_type); static void show_mqtt(uuid::console::Shell & shell); @@ -138,7 +128,9 @@ class Mqtt { mqttClient_->disconnect(); } - void incoming(const char * topic, const char * payload); // for testing only +#if defined(EMSESP_DEBUG) + void incoming(const char * topic, const char * payload = ""); // for testing only +#endif static bool connected() { #if defined(EMSESP_STANDALONE) @@ -182,30 +174,31 @@ class Mqtt { static uint8_t nested_format() { return nested_format_; } + static void nested_format(uint8_t nested_format) { nested_format_ = nested_format; } - // subscribe_format is 0 for General topics, 1 for individual with main heating circuit or 2 for individual topics with all heating circuits - static uint8_t subscribe_format() { - return subscribe_format_; - } - static void subscribe_format(uint8_t subscribe_format) { - subscribe_format_ = subscribe_format; - } - - static bool ha_enabled() { - return ha_enabled_; - } - static void ha_climate_format(uint8_t ha_climate_format) { ha_climate_format_ = ha_climate_format; } + static bool ha_enabled() { + return ha_enabled_; + } + static void ha_enabled(bool ha_enabled) { ha_enabled_ = ha_enabled; } + static bool send_response() { + return send_response_; + } + + static void send_response(bool send_response) { + send_response_ = send_response; + } + void set_qos(uint8_t mqtt_qos) { mqtt_qos_ = mqtt_qos; } @@ -297,7 +290,7 @@ class Mqtt { static uint8_t ha_climate_format_; static bool ha_enabled_; static uint8_t nested_format_; - static uint8_t subscribe_format_; + static bool send_response_; }; } // namespace emsesp diff --git a/src/shower.cpp b/src/shower.cpp index 5ce06d79a..494e8aecb 100644 --- a/src/shower.cpp +++ b/src/shower.cpp @@ -135,7 +135,7 @@ void Shower::send_mqtt_stat(bool state, bool force) { void Shower::shower_alert_stop() { if (doing_cold_shot_) { LOG_DEBUG(F("Shower Alert stopped")); - (void)Command::call(EMSdevice::DeviceType::BOILER, "wwtapactivated", "true", true); // no need to check authentication + (void)Command::call(EMSdevice::DeviceType::BOILER, "wwtapactivated", "true"); doing_cold_shot_ = false; } } @@ -143,7 +143,7 @@ void Shower::shower_alert_stop() { void Shower::shower_alert_start() { if (shower_alert_) { LOG_DEBUG(F("Shower Alert started")); - (void)Command::call(EMSdevice::DeviceType::BOILER, "wwtapactivated", "false", true); // no need to check authentication + (void)Command::call(EMSdevice::DeviceType::BOILER, "wwtapactivated", "false"); doing_cold_shot_ = true; alert_timer_start_ = uuid::get_uptime(); // timer starts now } diff --git a/src/system.cpp b/src/system.cpp index 28923754f..93498498a 100644 --- a/src/system.cpp +++ b/src/system.cpp @@ -496,44 +496,44 @@ void System::show_mem(const char * note) { } // create the json for heartbeat -bool System::heartbeat_json(JsonObject & doc) { +bool System::heartbeat_json(JsonObject & output) { uint8_t ems_status = EMSESP::bus_status(); if (ems_status == EMSESP::BUS_STATUS_TX_ERRORS) { - doc["status"] = FJSON("txerror"); + output["status"] = FJSON("txerror"); } else if (ems_status == EMSESP::BUS_STATUS_CONNECTED) { - doc["status"] = FJSON("connected"); + output["status"] = FJSON("connected"); } else { - doc["status"] = FJSON("disconnected"); + output["status"] = FJSON("disconnected"); } - doc["uptime"] = uuid::log::format_timestamp_ms(uuid::get_uptime_ms(), 3); + output["uptime"] = uuid::log::format_timestamp_ms(uuid::get_uptime_ms(), 3); - doc["uptime_sec"] = uuid::get_uptime_sec(); - doc["rxreceived"] = EMSESP::rxservice_.telegram_count(); - doc["rxfails"] = EMSESP::rxservice_.telegram_error_count(); - doc["txreads"] = EMSESP::txservice_.telegram_read_count(); - doc["txwrites"] = EMSESP::txservice_.telegram_write_count(); - doc["txfails"] = EMSESP::txservice_.telegram_fail_count(); + output["uptime_sec"] = uuid::get_uptime_sec(); + output["rxreceived"] = EMSESP::rxservice_.telegram_count(); + output["rxfails"] = EMSESP::rxservice_.telegram_error_count(); + output["txreads"] = EMSESP::txservice_.telegram_read_count(); + output["txwrites"] = EMSESP::txservice_.telegram_write_count(); + output["txfails"] = EMSESP::txservice_.telegram_fail_count(); if (Mqtt::enabled()) { - doc["mqttfails"] = Mqtt::publish_fails(); + output["mqttfails"] = Mqtt::publish_fails(); } if (EMSESP::dallas_enabled()) { - doc["dallasfails"] = EMSESP::sensor_fails(); + output["dallasfails"] = EMSESP::sensor_fails(); } #ifndef EMSESP_STANDALONE - doc["freemem"] = ESP.getFreeHeap() / 1000L; // kilobytes + output["freemem"] = ESP.getFreeHeap() / 1000L; // kilobytes #endif if (analog_enabled_) { - doc["adc"] = analog_; + output["adc"] = analog_; } #ifndef EMSESP_STANDALONE if (!ethernet_connected_) { - int8_t rssi = WiFi.RSSI(); - doc["rssi"] = rssi; - doc["wifistrength"] = wifi_quality(rssi); + int8_t rssi = WiFi.RSSI(); + output["rssi"] = rssi; + output["wifistrength"] = wifi_quality(rssi); } #endif @@ -684,16 +684,23 @@ void System::commands_init() { Command::add(EMSdevice::DeviceType::SYSTEM, F_(send), System::command_send, F("send a telegram"), CommandFlag::ADMIN_ONLY); Command::add(EMSdevice::DeviceType::SYSTEM, F_(fetch), System::command_fetch, F("refresh all EMS values"), CommandFlag::ADMIN_ONLY); Command::add(EMSdevice::DeviceType::SYSTEM, F_(restart), System::command_restart, F("restarts EMS-ESP"), CommandFlag::ADMIN_ONLY); - Command::add(EMSdevice::DeviceType::SYSTEM, F_(watch), System::command_watch, F("watch incoming telegrams"), CommandFlag::ADMIN_ONLY); + Command::add(EMSdevice::DeviceType::SYSTEM, F_(watch), System::command_watch, F("watch incoming telegrams")); + + if (Mqtt::enabled()) { + Command::add(EMSdevice::DeviceType::SYSTEM, F_(publish), System::command_publish, F("forces a MQTT publish")); + } // these commands will return data in JSON format - Command::add_json(EMSdevice::DeviceType::SYSTEM, F_(info), System::command_info, F("system status")); - Command::add_json(EMSdevice::DeviceType::SYSTEM, F_(settings), System::command_settings, F("list system settings")); - Command::add_json(EMSdevice::DeviceType::SYSTEM, F_(commands), System::command_commands, F("list system commands")); + Command::add(EMSdevice::DeviceType::SYSTEM, F_(info), System::command_info, F("system status")); + Command::add(EMSdevice::DeviceType::SYSTEM, F_(settings), System::command_settings, F("list system settings")); + Command::add(EMSdevice::DeviceType::SYSTEM, F_(commands), System::command_commands, F("list system commands")); #if defined(EMSESP_DEBUG) Command::add(EMSdevice::DeviceType::SYSTEM, F("test"), System::command_test, F("run tests")); #endif + + // MQTT subscribe "ems-esp/system/#" + Mqtt::subscribe(EMSdevice::DeviceType::SYSTEM, "system/#", nullptr); // use empty function callback } // flashes the LED @@ -844,22 +851,22 @@ bool System::check_upgrade() { } // list commands -bool System::command_commands(const char * value, const int8_t id, JsonObject & json) { - return Command::list(EMSdevice::DeviceType::SYSTEM, json); +bool System::command_commands(const char * value, const int8_t id, JsonObject & output) { + return Command::list(EMSdevice::DeviceType::SYSTEM, output); } // export all settings to JSON text // http://ems-esp/api/system/settings // value and id are ignored -bool System::command_settings(const char * value, const int8_t id, JsonObject & json) { +bool System::command_settings(const char * value, const int8_t id, JsonObject & output) { JsonObject node; - node = json.createNestedObject("System"); + node = output.createNestedObject("System"); node["version"] = EMSESP_APP_VERSION; // hide ssid from this list EMSESP::esp8266React.getNetworkSettingsService()->read([&](NetworkSettings & settings) { - node = json.createNestedObject("Network"); + node = output.createNestedObject("Network"); node["hostname"] = settings.hostname; node["static_ip_config"] = settings.staticIPConfig; node["enableIPv6"] = settings.enableIPv6; @@ -872,7 +879,7 @@ bool System::command_settings(const char * value, const int8_t id, JsonObject & #ifndef EMSESP_STANDALONE EMSESP::esp8266React.getAPSettingsService()->read([&](APSettings & settings) { - node = json.createNestedObject("AP"); + node = output.createNestedObject("AP"); node["provision_mode"] = settings.provisionMode; node["ssid"] = settings.ssid; node["local_ip"] = settings.localIP.toString(); @@ -882,7 +889,7 @@ bool System::command_settings(const char * value, const int8_t id, JsonObject & #endif EMSESP::esp8266React.getMqttSettingsService()->read([&](MqttSettings & settings) { - node = json.createNestedObject("MQTT"); + node = output.createNestedObject("MQTT"); node["enabled"] = settings.enabled; #ifndef EMSESP_STANDALONE node["host"] = settings.host; @@ -902,12 +909,12 @@ bool System::command_settings(const char * value, const int8_t id, JsonObject & node["ha_enabled"] = settings.ha_enabled; node["mqtt_qos"] = settings.mqtt_qos; node["mqtt_retain"] = settings.mqtt_retain; - node["subscribe_format"] = settings.subscribe_format; + node["send_response"] = settings.send_response; }); #ifndef EMSESP_STANDALONE EMSESP::esp8266React.getNTPSettingsService()->read([&](NTPSettings & settings) { - node = json.createNestedObject("NTP"); + node = output.createNestedObject("NTP"); node["enabled"] = settings.enabled; node["server"] = settings.server; node["tz_label"] = settings.tzLabel; @@ -915,14 +922,14 @@ bool System::command_settings(const char * value, const int8_t id, JsonObject & }); EMSESP::esp8266React.getOTASettingsService()->read([&](OTASettings & settings) { - node = json.createNestedObject("OTA"); + node = output.createNestedObject("OTA"); node["enabled"] = settings.enabled; node["port"] = settings.port; }); #endif EMSESP::webSettingsService.read([&](WebSettings & settings) { - node = json.createNestedObject("Settings"); + node = output.createNestedObject("Settings"); node["tx_mode"] = settings.tx_mode; node["ems_bus_id"] = settings.ems_bus_id; node["syslog_enabled"] = settings.syslog_enabled; @@ -953,10 +960,10 @@ bool System::command_settings(const char * value, const int8_t id, JsonObject & // export status information including the device information // http://ems-esp/api/system/info -bool System::command_info(const char * value, const int8_t id, JsonObject & json) { +bool System::command_info(const char * value, const int8_t id, JsonObject & output) { JsonObject node; - node = json.createNestedObject("System"); + node = output.createNestedObject("System"); node["version"] = EMSESP_APP_VERSION; node["uptime"] = uuid::log::format_timestamp_ms(uuid::get_uptime_ms(), 3); @@ -967,7 +974,7 @@ bool System::command_info(const char * value, const int8_t id, JsonObject & json #endif node["reset_reason"] = EMSESP::system_.reset_reason(0) + " / " + EMSESP::system_.reset_reason(1); - node = json.createNestedObject("Status"); + node = output.createNestedObject("Status"); switch (EMSESP::bus_status()) { case EMSESP::BUS_STATUS_OFFLINE: @@ -1009,7 +1016,7 @@ bool System::command_info(const char * value, const int8_t id, JsonObject & json } // show EMS devices - JsonArray devices = json.createNestedArray("Devices"); + JsonArray devices = output.createNestedArray("Devices"); for (const auto & device_class : EMSFactory::device_handlers()) { for (const auto & emsdevice : EMSESP::emsdevices) { if ((emsdevice) && (emsdevice->device_type() == device_class.first)) { diff --git a/src/system.h b/src/system.h index 2dfac2144..4e512daf1 100644 --- a/src/system.h +++ b/src/system.h @@ -59,9 +59,9 @@ class System { static bool command_syslog_level(const char * value, const int8_t id); static bool command_watch(const char * value, const int8_t id); - static bool command_info(const char * value, const int8_t id, JsonObject & json); - static bool command_settings(const char * value, const int8_t id, JsonObject & json); - static bool command_commands(const char * value, const int8_t id, JsonObject & json); + static bool command_info(const char * value, const int8_t id, JsonObject & output); + static bool command_settings(const char * value, const int8_t id, JsonObject & output); + static bool command_commands(const char * value, const int8_t id, JsonObject & output); const std::string reset_reason(uint8_t cpu); @@ -74,7 +74,7 @@ class System { void wifi_tweak(); void syslog_start(); bool check_upgrade(); - bool heartbeat_json(JsonObject & json); + bool heartbeat_json(JsonObject & output); void send_heartbeat(); void led_init(bool refresh); diff --git a/src/telegram.h b/src/telegram.h index d49f95a4b..9e3bb686f 100644 --- a/src/telegram.h +++ b/src/telegram.h @@ -374,6 +374,7 @@ class TxService : public EMSbus { #else static constexpr uint8_t MAXIMUM_TX_RETRIES = 3; #endif + static constexpr uint32_t POST_SEND_DELAY = 2000; private: diff --git a/src/test/test.cpp b/src/test/test.cpp index abbe90e31..20909b50d 100644 --- a/src/test/test.cpp +++ b/src/test/test.cpp @@ -436,7 +436,7 @@ void Test::run_test(uuid::console::Shell & shell, const std::string & cmd) { run_test("boiler"); // device type, command, data - Command::call(EMSdevice::DeviceType::BOILER, "wwtapactivated", "false", true); + Command::call(EMSdevice::DeviceType::BOILER, "wwtapactivated", "false"); } if (command == "fr120") { @@ -471,18 +471,177 @@ void Test::run_test(uuid::console::Shell & shell, const std::string & cmd) { shell.invoke_command("call boiler entities"); } - if (command == "mqtt_individual") { - shell.printfln(F("Testing individual MQTT")); - Mqtt::ha_enabled(false); // turn off HA Discovery to stop the chatter + if (command == "api") { + shell.printfln(F("Testing API with MQTT and REST")); + + Mqtt::ha_enabled(true); + // Mqtt::ha_enabled(false); + Mqtt::nested_format(1); - // Mqtt::subscribe_format(2); // individual topics, all HC - Mqtt::subscribe_format(1); // individual topics, only main HC + Mqtt::send_response(true); run_test("boiler"); run_test("thermostat"); - // shell.invoke_command("show mqtt"); - // EMSESP::mqtt_.incoming("ems-esp/boiler/wwcircpump", "off"); + /* + + AsyncWebServerRequest request2; + request2.method(HTTP_GET); + request2.url("/system/sensors"); // check if defaults to info + EMSESP::webAPIService.webAPIService_get(&request2); + + EMSESP::mqtt_.incoming("ems-esp/thermostat/mode"); // empty payload, sends reponse + EMSESP::mqtt_.incoming("ems-esp/boiler/syspress"); // empty payload, sends reponse + EMSESP::mqtt_.incoming("ems-esp/thermostat/mode", "auto"); // set mode + EMSESP::mqtt_.incoming("ems-esp/thermostat/mode"); // empty payload, sends reponse + EMSESP::mqtt_.incoming("ems-esp/system/send", "11 12 13"); + EMSESP::mqtt_.incoming("ems-esp/system/publish"); + EMSESP::mqtt_.incoming("ems-esp/thermostat/seltemp"); // empty payload, sends reponse + EMSESP::mqtt_.incoming("ems-esp/system/send", "11 12 13"); + + AsyncWebServerRequest request2; + request2.method(HTTP_GET); + request2.url("/api/thermostat"); // check if defaults to info + EMSESP::webAPIService.webAPIService_get(&request2); + request2.url("/api/thermostat/info"); + EMSESP::webAPIService.webAPIService_get(&request2); + request2.url("/api/thermostat/list"); + EMSESP::webAPIService.webAPIService_get(&request2); + request2.url("/api/thermostat/mode"); + EMSESP::webAPIService.webAPIService_get(&request2); + + request2.method(HTTP_POST); + DynamicJsonDocument docX(2000); + JsonVariant jsonX; + char dataX[] = "{\"value\":\"0B 88 19 19 02\"}"; + deserializeJson(docX, dataX); + jsonX = docX.as(); + request2.url("/api/system/send"); + EMSESP::webAPIService.webAPIService_post(&request2, jsonX); + return; + */ + + // test command parse + int8_t id_n; + const char * cmd; + char command_s[100]; + id_n = -1; + strcpy(command_s, "hc2/seltemp"); + cmd = Command::parse_command_string(command_s, id_n); + shell.printfln("test cmd parse cmd=%s id=%d", cmd, id_n); + id_n = -1; + strcpy(command_s, "seltemp"); + cmd = Command::parse_command_string(command_s, id_n); + shell.printfln("test cmd parse cmd=%s id=%d", cmd, id_n); + id_n = -1; + strcpy(command_s, "xyz/seltemp"); + cmd = Command::parse_command_string(command_s, id_n); + shell.printfln("test cmd parse cmd=%s id=%d", cmd, id_n); + id_n = -1; + strcpy(command_s, "wwc4/seltemp"); + cmd = Command::parse_command_string(command_s, id_n); + shell.printfln("test cmd parse cmd=%s id=%d", cmd, id_n); + id_n = -1; + strcpy(command_s, "hc3_seltemp"); + cmd = Command::parse_command_string(command_s, id_n); + shell.printfln("test cmd parse cmd=%s id=%d", cmd, id_n); + + // Console tests + shell.invoke_command("call thermostat entities"); + shell.invoke_command("call thermostat mode auto"); + + // MQTT good tests + EMSESP::mqtt_.incoming("ems-esp/thermostat/mode", "auto"); + EMSESP::mqtt_.incoming("ems-esp/thermostat/hc2/mode", "auto"); + EMSESP::mqtt_.incoming("ems-esp/thermostat/wwc3/mode", "auto"); + EMSESP::mqtt_.incoming("ems-esp/boiler/wwcircpump", "off"); + EMSESP::mqtt_.incoming("ems-esp/thermostat/seltemp"); // empty payload, sends reponse + EMSESP::mqtt_.incoming("ems-esp/thermostat_hc1", "22"); // HA only + EMSESP::mqtt_.incoming("ems-esp/thermostat_hc1", "off"); // HA only + EMSESP::mqtt_.incoming("ems-esp/system/send", "11 12 13"); + + // MQTT bad tests + EMSESP::mqtt_.incoming("ems-esp/thermostate/mode", "auto"); // unknown device + EMSESP::mqtt_.incoming("ems-esp/thermostat/modee", "auto"); // unknown command + +#if defined(EMSESP_STANDALONE) + // Web API TESTS + AsyncWebServerRequest request; + request.method(HTTP_GET); + + request.url("/api/thermostat"); // check if defaults to info + EMSESP::webAPIService.webAPIService_get(&request); + request.url("/api/thermostat/info"); + EMSESP::webAPIService.webAPIService_get(&request); + request.url("/api/thermostat/list"); + EMSESP::webAPIService.webAPIService_get(&request); + request.url("/api/thermostat/seltemp"); + EMSESP::webAPIService.webAPIService_get(&request); + request.url("/api/system/commands"); + EMSESP::webAPIService.webAPIService_get(&request); + request.url("/api/system/info"); + EMSESP::webAPIService.webAPIService_get(&request); + request.url("/api/boiler/syspress"); + EMSESP::webAPIService.webAPIService_get(&request); + request.url("/api/boiler/wwcurflow"); + EMSESP::webAPIService.webAPIService_get(&request); + + // POST tests + request.method(HTTP_POST); + DynamicJsonDocument doc(2000); + JsonVariant json; + + // 1 + char data1[] = "{\"name\":\"temp\",\"value\":11}"; + deserializeJson(doc, data1); + json = doc.as(); + request.url("/api/thermostat"); + EMSESP::webAPIService.webAPIService_post(&request, json); + + // 2 + char data2[] = "{\"value\":12}"; + deserializeJson(doc, data2); + json = doc.as(); + request.url("/api/thermostat/temp"); + EMSESP::webAPIService.webAPIService_post(&request, json); + + // 3 + char data3[] = "{\"device\":\"thermostat\", \"name\":\"temp\",\"value\":13}"; + deserializeJson(doc, data3); + json = doc.as(); + request.url("/api"); + EMSESP::webAPIService.webAPIService_post(&request, json); + + // 4 - system call + char data4[] = "{\"value\":\"0B 88 19 19 02\"}"; + deserializeJson(doc, data4); + json = doc.as(); + request.url("/api/system/send"); + EMSESP::webAPIService.webAPIService_post(&request, json); + + // 5 - test write value + // device=3 cmd=hc2/seltemp value=44 + char data5[] = "{\"device\":\"thermostat\", \"cmd\":\"hc2.seltemp\",\"value\":14}"; + deserializeJson(doc, data5); + json = doc.as(); + request.url("/api"); + EMSESP::webAPIService.webAPIService_post(&request, json); + + // write value from web - testing hc2/seltemp + char data6[] = "{\"id\":2,\"devicevalue\":{\"v\":\"44\",\"u\":1,\"n\":\"hc2 selected room temperature\",\"c\":\"hc2/seltemp\"}"; + deserializeJson(doc, data6); + json = doc.as(); + request.url("/rest/writeValue"); + EMSESP::webDataService.write_value(&request, json); + + // write value from web - testing hc9/seltemp - should fail! + char data7[] = "{\"id\":2,\"devicevalue\":{\"v\":\"55\",\"u\":1,\"n\":\"hc2 selected room temperature\",\"c\":\"hc9/seltemp\"}"; + deserializeJson(doc, data7); + json = doc.as(); + request.url("/rest/writeValue"); + EMSESP::webDataService.write_value(&request, json); + +#endif } if (command == "mqtt_nested") { @@ -966,82 +1125,6 @@ void Test::run_test(uuid::console::Shell & shell, const std::string & cmd) { shell.invoke_command("show mqtt"); } - if (command == "api") { -#if defined(EMSESP_STANDALONE) - shell.printfln(F("Testing RESTful API...")); - Mqtt::ha_enabled(true); - Mqtt::enabled(false); - run_test("general"); - AsyncWebServerRequest request; - - // GET - request.url("/api/thermostat"); - EMSESP::webAPIService.webAPIService_get(&request); - request.url("/api/thermostat/info"); - EMSESP::webAPIService.webAPIService_get(&request); - - // these next 2 should fail - request.url("/api/boiler/id"); - EMSESP::webAPIService.webAPIService_get(&request); - request.url("/api/thermostat/hamode"); - EMSESP::webAPIService.webAPIService_get(&request); - - request.method(HTTP_GET); - request.url("/api/thermostat/seltemp"); - EMSESP::webAPIService.webAPIService_get(&request); - - request.url("/api/boiler/syspress"); - EMSESP::webAPIService.webAPIService_get(&request); - - request.url("/api/system/commands"); - EMSESP::webAPIService.webAPIService_get(&request); - - request.url("/api/boiler/info"); - EMSESP::webAPIService.webAPIService_get(&request); - - request.url("/api/boiler/wwcurflow"); - EMSESP::webAPIService.webAPIService_get(&request); - - // POST - request.method(HTTP_POST); - - request.url("/api/system/commands"); - EMSESP::webAPIService.webAPIService_get(&request); - - DynamicJsonDocument doc(2000); - JsonVariant json; - - // 1 - char data1[] = "{\"name\":\"temp\",\"value\":11}"; - deserializeJson(doc, data1); - json = doc.as(); - request.url("/api/thermostat"); - EMSESP::webAPIService.webAPIService_post(&request, json); - - // 2 - char data2[] = "{\"value\":12}"; - deserializeJson(doc, data2); - json = doc.as(); - request.url("/api/thermostat/temp"); - EMSESP::webAPIService.webAPIService_post(&request, json); - - // 3 - char data3[] = "{\"device\":\"thermostat\", \"name\":\"temp\",\"value\":13}"; - deserializeJson(doc, data3); - json = doc.as(); - request.url("/api"); - EMSESP::webAPIService.webAPIService_post(&request, json); - - // 4 - system call - char data4[] = "{\"value\":\"0B 88 19 19 02\"}"; - deserializeJson(doc, data4); - json = doc.as(); - request.url("/api/system/send"); - EMSESP::webAPIService.webAPIService_post(&request, json); - -#endif - } - if (command == "crash") { shell.printfln(F("Forcing a crash...")); diff --git a/src/test/test.h b/src/test/test.h index c776c21b2..d9ca69ca7 100644 --- a/src/test/test.h +++ b/src/test/test.h @@ -39,9 +39,8 @@ namespace emsesp { // #define EMSESP_DEBUG_DEFAULT "shower_alert" // #define EMSESP_DEBUG_DEFAULT "310" // #define EMSESP_DEBUG_DEFAULT "render" -// #define EMSESP_DEBUG_DEFAULT "api" +#define EMSESP_DEBUG_DEFAULT "api" // #define EMSESP_DEBUG_DEFAULT "crash" -#define EMSESP_DEBUG_DEFAULT "mqtt_individual" class Test { public: diff --git a/src/version.h b/src/version.h index d867dc969..c5d96a7c3 100644 --- a/src/version.h +++ b/src/version.h @@ -1 +1 @@ -#define EMSESP_APP_VERSION "3.2.2b14" +#define EMSESP_APP_VERSION "3.3.0b0" diff --git a/src/web/WebAPIService.cpp b/src/web/WebAPIService.cpp index 48638b808..6aa658fca 100644 --- a/src/web/WebAPIService.cpp +++ b/src/web/WebAPIService.cpp @@ -35,19 +35,15 @@ WebAPIService::WebAPIService(AsyncWebServer * server, SecurityManager * security // GET /{device}/{name} // GET /device={device}?cmd={name}?data={value}[?id={hc}] void WebAPIService::webAPIService_get(AsyncWebServerRequest * request) { - // initialize parameters. These will be extracted from the URL - std::string device_s(""); - std::string cmd_s(""); - std::string value_s(""); - int id = -1; - - parse(request, device_s, cmd_s, id, value_s); + // has no body JSON so create dummy as empty object + StaticJsonDocument input_doc; + JsonObject input = input_doc.to(); + parse(request, input); } // For POSTS with an optional JSON body // HTTP_POST | HTTP_PUT | HTTP_PATCH // POST /{device}[/{hc|id}][/{name}] -// the body must have 'value'. Optional are device, name, hc and id void WebAPIService::webAPIService_post(AsyncWebServerRequest * request, JsonVariant & json) { // if no body then treat it as a secure GET if (not json.is()) { @@ -56,282 +52,68 @@ void WebAPIService::webAPIService_post(AsyncWebServerRequest * request, JsonVari } // extract values from the json. these will be used as default values - auto && body = json.as(); - -#if defined(EMSESP_STANDALONE) - Serial.println("webAPIService_post: "); - serializeJson(body, Serial); - Serial.println(); -#endif - - // make sure we have a value. There must always be a value - if (!body.containsKey(F_(value))) { - send_message_response(request, 400, "Problems parsing JSON, missing value"); // Bad Request - return; - } - - std::string value_s = body["value"].as(); // always convert value to string - std::string device_s = body["device"].as(); - - // get the command. It can be either 'name' or 'cmd' - std::string cmd_s(""); - if (body.containsKey("name")) { - cmd_s = body["name"].as(); - } else if (body.containsKey("cmd")) { - cmd_s = body["cmd"].as(); - } - - // for id, it can be part of the hc or id keys in the json body - int id = -1; - if (body.containsKey("id")) { - id = body["id"]; - } else if (body.containsKey("hc")) { - id = body["hc"]; - } else { - id = -1; - } - - // now parse the URL. The URL is always leading and will overwrite anything provided in the json body - parse(request, device_s, cmd_s, id, value_s); // pass it defaults + auto && input = json.as(); + parse(request, input); } // parse the URL looking for query or path parameters // reporting back any errors -void WebAPIService::parse(AsyncWebServerRequest * request, std::string & device_s, std::string & cmd_s, int id, std::string & value_s) { - // parse URL for the path names - SUrlParser p; - - p.parse(request->url().c_str()); - - // remove the /api from the path - if (p.paths().front() == "api") { - p.paths().erase(p.paths().begin()); - } else { - return; // bad URL - } - - uint8_t device_type; - int8_t id_n = -1; // default hc - - // check for query parameters first, the old style from v2 - // /device={device}?cmd={name}?data={value}[?id={hc} - if (p.paths().size() == 0) { - // get the device - if (request->hasParam(F_(device))) { - device_s = request->getParam(F_(device))->value().c_str(); - } - - // get cmd - if (request->hasParam(F_(cmd))) { - cmd_s = request->getParam(F_(cmd))->value().c_str(); - } - - // get data, which is optional. This is now replaced with the name 'value' in JSON body - if (request->hasParam(F_(data))) { - value_s = request->getParam(F_(data))->value().c_str(); - } - if (request->hasParam(F_(value))) { - value_s = request->getParam(F_(value))->value().c_str(); - } - - // get id (or hc), which is optional - if (request->hasParam(F_(id))) { - id_n = Helpers::atoint(request->getParam(F_(id))->value().c_str()); - } - if (request->hasParam("hc")) { - id_n = Helpers::atoint(request->getParam("hc")->value().c_str()); - } - } else { - // parse paths and json data from the newer OpenAPI standard - // [/{device}][/{hc}][/{name}] - // all paths are optional. If not set then take the values from the json body (if available) - - // see if we have a device in the path - size_t num_paths = p.paths().size(); - if (num_paths) { - // assume the next path is the 'device'. Note this could also have the value of system. - device_s = p.paths().front(); - - if (num_paths == 2) { - // next path is the name or cmd - cmd_s = p.paths()[1]; - } else if (num_paths > 2) { - // check in Command::find_command makes prefix to TAG - cmd_s = p.paths()[1] + "/" + p.paths()[2]; - } - } - } - - // device checks - if (device_s.empty()) { - // see if we have a device embedded in the json body, then use that - send_message_response(request, 422, "Missing device"); // Unprocessable Entity - return; - } - - device_type = EMSdevice::device_name_2_device_type(device_s.c_str()); - if (device_type == EMSdevice::DeviceType::UNKNOWN) { - send_message_response(request, 422, "Invalid call"); // Unprocessable Entity - return; - } - - // check that we have permissions first. We require authenticating on 1 or more of these conditions: - // 1. any HTTP POSTs or PUTs - // 2. an HTTP GET which has a 'data' parameter which is not empty (to keep v2 compatibility) +void WebAPIService::parse(AsyncWebServerRequest * request, JsonObject & input) { auto method = request->method(); - bool have_data = !value_s.empty(); bool authenticated = false; - EMSESP::webSettingsService.read([&](WebSettings & settings) { - Authentication authentication = _securityManager->authenticateRequest(request); - authenticated = settings.notoken_api | AuthenticationPredicates::IS_ADMIN(authentication); - }); - - if ((method != HTTP_GET) || ((method == HTTP_GET) && have_data)) { - if (!authenticated) { - send_message_response(request, 401, "Bad credentials"); // Unauthorized - return; + if (method == HTTP_GET) { + // special case if there is no command, then default to 'info' + if (!input.size()) { + input["cmd"] = "info"; } + } else { + // if its a POST then check authentication + EMSESP::webSettingsService.read([&](WebSettings & settings) { + Authentication authentication = _securityManager->authenticateRequest(request); + authenticated = settings.notoken_api | AuthenticationPredicates::IS_ADMIN(authentication); + }); } + // output json buffer PrettyAsyncJsonResponse * response = new PrettyAsyncJsonResponse(false, EMSESP_JSON_SIZE_XXLARGE_DYN); - JsonObject json = response->getRoot(); + JsonObject output = response->getRoot(); - // now we have all the parameters go and execute the command - // the function will also determine if authentication is needed to execute its command - uint8_t cmd_reply = Command::call(device_type, cmd_s.c_str(), (have_data ? value_s.c_str() : nullptr), authenticated, id_n, json); + // call command + uint8_t command_ret = Command::process(request->url().c_str(), authenticated, input, output); - // check for errors - if (cmd_reply == CommandRet::NOT_FOUND) { - delete response; - send_message_response(request, 400, "Command not found"); // Bad Request - return; - } else if (cmd_reply == CommandRet::NOT_ALLOWED) { - delete response; - send_message_response(request, 401, "Bad credentials"); // Unauthorized - return; - } else if (cmd_reply != CommandRet::OK) { - delete response; - send_message_response(request, 400, "Problems parsing elements"); // Bad Request - return; - } - - if (!json.size()) { - delete response; - send_message_response(request, 200, "OK"); // OK - return; + // handle response codes + // the output will be populated with a message key if an error occurred + int ret_code; + if (command_ret == CommandRet::NOT_ALLOWED) { + ret_code = 401; // Unauthorized + } else if (command_ret == CommandRet::NOT_FOUND) { + ret_code = 400; // Bad request + } else if (command_ret == CommandRet::OK) { + ret_code = 200; //OK + if (output.isNull()) { + output["message"] = "OK"; // only add if there is no json output already + } + } else { + ret_code = 400; // Bad request } // send the json that came back from the command call + response->setCode(ret_code); response->setLength(); + response->setContentType("application/json"); request->send(response); // send json response #if defined(EMSESP_STANDALONE) Serial.print(COLOR_YELLOW); - if (json.size() != 0) { - serializeJsonPretty(json, Serial); + Serial.print("return code: "); + Serial.println(ret_code); + if (output.size() != 0) { + serializeJsonPretty(output, Serial); } Serial.println(); Serial.print(COLOR_RESET); #endif } -// send a HTTP error back, with optional JSON body data -void WebAPIService::send_message_response(AsyncWebServerRequest * request, uint16_t error_code, const char * message) { - if (message == nullptr) { - AsyncWebServerResponse * response = request->beginResponse(error_code); // just send the code - request->send(response); - } else { - // build a return message and send it - PrettyAsyncJsonResponse * response = new PrettyAsyncJsonResponse(false, EMSESP_JSON_SIZE_SMALL); - JsonObject json = response->getRoot(); - json["message"] = message; - response->setCode(error_code); - response->setLength(); - response->setContentType("application/json"); - request->send(response); - } - - EMSESP::logger().debug(F("API return code: %d, message: %s"), error_code, message); -} - -/** - * Extract only the path component from the passed URI and normalized it. - * Ex. //one/two////three/// becomes /one/two/three - */ -std::string SUrlParser::path() { - std::string s = "/"; // set up the beginning slash - for (std::string & f : m_folders) { - s += f; - s += "/"; - } - s.pop_back(); // deleting last letter, that is slash '/' - return std::string(s); -} - -SUrlParser::SUrlParser(const char * uri) { - parse(uri); -} - -bool SUrlParser::parse(const char * uri) { - m_folders.clear(); - m_keysvalues.clear(); - enum Type { begin, folder, param, value }; - std::string s; - - const char * c = uri; - enum Type t = Type::begin; - std::string last_param; - - if (c != NULL || *c != '\0') { - do { - if (*c == '/') { - if (s.length() > 0) { - m_folders.push_back(s); - s.clear(); - } - t = Type::folder; - } else if (*c == '?' && (t == Type::folder || t == Type::begin)) { - if (s.length() > 0) { - m_folders.push_back(s); - s.clear(); - } - t = Type::param; - } else if (*c == '=' && (t == Type::param || t == Type::begin)) { - m_keysvalues[s] = ""; - last_param = s; - s.clear(); - t = Type::value; - } else if (*c == '&' && (t == Type::value || t == Type::param || t == Type::begin)) { - if (t == Type::value) { - m_keysvalues[last_param] = s; - } else if ((t == Type::param || t == Type::begin) && (s.length() > 0)) { - m_keysvalues[s] = ""; - last_param = s; - } - t = Type::param; - s.clear(); - } else if (*c == '\0' && s.length() > 0) { - if (t == Type::value) { - m_keysvalues[last_param] = s; - } else if (t == Type::folder || t == Type::begin) { - m_folders.push_back(s); - } else if (t == Type::param) { - m_keysvalues[s] = ""; - last_param = s; - } - s.clear(); - } else if (*c == '\0' && s.length() == 0) { - if (t == Type::param && last_param.length() > 0) { - m_keysvalues[last_param] = ""; - } - s.clear(); - } else { - s += *c; - } - } while (*c++ != '\0'); - } - return true; -} - } // namespace emsesp diff --git a/src/web/WebAPIService.h b/src/web/WebAPIService.h index 94cf5a32a..499061219 100644 --- a/src/web/WebAPIService.h +++ b/src/web/WebAPIService.h @@ -31,31 +31,6 @@ namespace emsesp { -typedef std::unordered_map KeyValueMap_t; -typedef std::vector Folder_t; - -class SUrlParser { - private: - KeyValueMap_t m_keysvalues; - Folder_t m_folders; - - public: - SUrlParser(){}; - SUrlParser(const char * url); - - bool parse(const char * url); - - Folder_t & paths() { - return m_folders; - }; - - KeyValueMap_t & params() { - return m_keysvalues; - }; - - std::string path(); -}; - class WebAPIService { public: WebAPIService(AsyncWebServer * server, SecurityManager * securityManager); @@ -67,8 +42,7 @@ class WebAPIService { SecurityManager * _securityManager; AsyncCallbackJsonWebHandler _apiHandler; // for POSTs - void parse(AsyncWebServerRequest * request, std::string & device, std::string & cmd, int id, std::string & value); - void send_message_response(AsyncWebServerRequest * request, uint16_t error_code, const char * message = nullptr); + void parse(AsyncWebServerRequest * request, JsonObject & input); }; } // namespace emsesp diff --git a/src/web/WebDataService.cpp b/src/web/WebDataService.cpp index 32a582258..6d5b0a569 100644 --- a/src/web/WebDataService.cpp +++ b/src/web/WebDataService.cpp @@ -101,7 +101,7 @@ void WebDataService::device_data(AsyncWebServerRequest * request, JsonVariant & if (emsdevice) { if (emsdevice->unique_id() == json["id"]) { // wait max 2.5 sec for updated data (post_send_delay is 2 sec) - for (uint16_t i = 0; i < 2500 && EMSESP::wait_validate(); i++) { + for (uint16_t i = 0; i < (emsesp::TxService::POST_SEND_DELAY + 500) && EMSESP::wait_validate(); i++) { delay(1); } EMSESP::wait_validate(0); // reset in case of timeout @@ -126,31 +126,49 @@ void WebDataService::device_data(AsyncWebServerRequest * request, JsonVariant & // assumes the service has been checked for admin authentication void WebDataService::write_value(AsyncWebServerRequest * request, JsonVariant & json) { if (json.is()) { - JsonObject dv = json["devicevalue"]; - uint8_t id = json["id"]; + JsonObject dv = json["devicevalue"]; + uint8_t unique_id = json["id"]; // using the unique ID from the web find the real device type + // id is the selected device for (const auto & emsdevice : EMSESP::emsdevices) { if (emsdevice) { - if (emsdevice->unique_id() == id) { - const char * cmd = dv["c"]; - uint8_t device_type = emsdevice->device_type(); - uint8_t cmd_return = CommandRet::OK; - char s[10]; + if (emsdevice->unique_id() == unique_id) { + // parse the command as it could have a hc or wwc prefixed, e.g. hc2/seltemp + const char * cmd = dv["c"]; // the command + int8_t id = -1; // default + cmd = Command::parse_command_string(cmd, id); // extract hc or wwc + + // create JSON for output + AsyncJsonResponse * response = new AsyncJsonResponse(false, EMSESP_JSON_SIZE_SMALL); + JsonObject output = response->getRoot(); + // the data could be in any format, but we need string - JsonVariant data = dv["v"]; + // authenticated is always true + JsonVariant data = dv["v"]; // the value in any format + uint8_t command_ret = CommandRet::OK; + uint8_t device_type = emsdevice->device_type(); if (data.is()) { - cmd_return = Command::call(device_type, cmd, data.as(), true); + command_ret = Command::call(device_type, cmd, data.as(), true, id, output); } else if (data.is()) { - cmd_return = Command::call(device_type, cmd, Helpers::render_value(s, data.as(), 0), true); + char s[10]; + command_ret = Command::call(device_type, cmd, Helpers::render_value(s, data.as(), 0), true, id, output); } else if (data.is()) { - cmd_return = Command::call(device_type, cmd, Helpers::render_value(s, (float)data.as(), 1), true); + char s[10]; + command_ret = Command::call(device_type, cmd, Helpers::render_value(s, (float)data.as(), 1), true, id, output); } else if (data.is()) { - cmd_return = Command::call(device_type, cmd, data.as() ? "true" : "false", true); + command_ret = Command::call(device_type, cmd, data.as() ? "true" : "false", true, id, output); } - // send "Write command sent to device" or "Write command failed" - AsyncWebServerResponse * response = request->beginResponse((cmd_return == CommandRet::OK) ? 200 : 204); + // write debug + if (command_ret != CommandRet::OK) { + EMSESP::logger().err(F("Write command failed %s (%d)"), (const char *)output["message"], command_ret); + } else { + EMSESP::logger().debug(F("Write command successful")); + } + + response->setCode((command_ret == CommandRet::OK) ? 200 : 204); + response->setLength(); request->send(response); return; } diff --git a/src/web/WebDataService.h b/src/web/WebDataService.h index adb114662..31b3673e7 100644 --- a/src/web/WebDataService.h +++ b/src/web/WebDataService.h @@ -36,7 +36,11 @@ class WebDataService { public: WebDataService(AsyncWebServer * server, SecurityManager * securityManager); +// make all functions public so we can test in the debug and standalone mode +#ifndef EMSESP_STANDALONE private: +#endif + // GET void all_devices(AsyncWebServerRequest * request); void scan_devices(AsyncWebServerRequest * request); diff --git a/src/web/WebSettingsService.cpp b/src/web/WebSettingsService.cpp index 5af677f52..1bd74d116 100644 --- a/src/web/WebSettingsService.cpp +++ b/src/web/WebSettingsService.cpp @@ -276,7 +276,6 @@ void WebSettingsService::board_profile(AsyncWebServerRequest * request, JsonVari root["tx_gpio"] = data[3]; root["pbutton_gpio"] = data[4]; } else { - delete response; AsyncWebServerResponse * response = request->beginResponse(200); request->send(response); return;