From 77f6a180751690f727683a5a4e0695bb7b90dbdf Mon Sep 17 00:00:00 2001 From: proddy Date: Tue, 20 Jul 2021 21:45:29 +0200 Subject: [PATCH] commands take a set of flags, like NEED_ADMIN or HIDDEN --- src/command.cpp | 93 ++++++++++++++++++++++++-------------- src/command.h | 57 +++++++++++++++++------ src/console.cpp | 29 ++++++------ src/emsdevice.h | 7 +-- src/mqtt.cpp | 44 ++++++++++-------- src/mqtt.h | 17 ++++++- src/shower.cpp | 4 +- src/system.cpp | 29 ++++++++---- src/system.h | 8 ++-- src/test/test.cpp | 15 +++++- src/test/test.h | 4 +- src/web/WebAPIService.cpp | 31 ++++++++----- src/web/WebDataService.cpp | 13 +++--- 13 files changed, 229 insertions(+), 122 deletions(-) diff --git a/src/command.cpp b/src/command.cpp index 0123d30d1..e324dc32e 100644 --- a/src/command.cpp +++ b/src/command.cpp @@ -28,57 +28,66 @@ std::vector Command::cmdfunctions_; // calls a command // id may be used to represent a heating circuit for example, it's optional -// returns false if error or not found -bool Command::call(const uint8_t device_type, const char * cmd, const char * value, const int8_t id) { +// 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[20] = {'\0'}; strlcpy(cmd_new, cmd, 20); + // 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()); - return false; // command not found, or requires a json + return 2; // command 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 not permitted. requires admin."), cmd, EMSdevice::device_type_2_device_name(device_type).c_str()); + return 4; // command not allowed } -#ifdef EMSESP_DEBUG std::string dname = EMSdevice::device_type_2_device_name(device_type); if (value == nullptr) { - LOG_DEBUG(F("[DEBUG] Calling %s command '%s'"), dname.c_str(), cmd); + LOG_INFO(F("Calling %s command '%s'"), dname.c_str(), cmd); } else if (id == -1) { - LOG_DEBUG(F("[DEBUG] Calling %s command '%s', value %s, id is default"), dname.c_str(), cmd, value); + LOG_INFO(F("Calling %s command '%s', value %s, id is default"), dname.c_str(), cmd, value); } else { - LOG_DEBUG(F("[DEBUG] Calling %s command '%s', value %s, id is %d"), dname.c_str(), cmd, value, id); + LOG_INFO(F("Calling %s command '%s', value %s, id is %d"), dname.c_str(), cmd, value, id); } -#endif return ((cf->cmdfunction_)(value, id_new)); } // calls a command. Takes a json object for output. // id may be used to represent a heating circuit for example -// returns false if error or not found -bool Command::call(const uint8_t device_type, const char * cmd, const char * value, const int8_t id, JsonObject & json) { +// 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[20] = {'\0'}; strlcpy(cmd_new, cmd, 20); auto cf = find_command(device_type, cmd_new, id_new); -#ifdef EMSESP_DEBUG + // check if we're allowed to call it + if (cf->has_flags(CommandFlag::ADMIN_ONLY) && !authenticated) { + LOG_WARNING(F("Command %s on %s not permitted. requires admin."), cmd, EMSdevice::device_type_2_device_name(device_type).c_str()); + return 4; // command not allowed + } + std::string dname = EMSdevice::device_type_2_device_name(device_type); if (value == nullptr) { - LOG_DEBUG(F("[DEBUG] Calling %s command '%s'"), dname.c_str(), cmd); + LOG_INFO(F("Calling %s command '%s'"), dname.c_str(), cmd); } else if (id == -1) { - LOG_DEBUG(F("[DEBUG] Calling %s command '%s', value %s, id is default"), dname.c_str(), cmd, value); + LOG_INFO(F("Calling %s command '%s', value %s, id is default"), dname.c_str(), cmd, value); } else { - LOG_DEBUG(F("[DEBUG] Calling %s command '%s', value %s, id is %d"), dname.c_str(), cmd, value, id); + LOG_INFO(F("Calling %s command '%s', value %s, id is %d"), dname.c_str(), cmd, value, id); } -#endif // check if json object is empty, if so quit if (json.isNull()) { - LOG_WARNING(F("Ignore call for command %s in %s because no json"), cmd, EMSdevice::device_type_2_device_name(device_type).c_str()); - return false; + 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 3; } // this is for endpoints that don't have commands, i.e not writable (e.g. boiler/syspress) @@ -98,20 +107,24 @@ bool Command::call(const uint8_t device_type, const char * cmd, const char * val // strip prefixes, check, and find command Command::CmdFunction * Command::find_command(const uint8_t device_type, char * cmd, int8_t & id) { + // TODO 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); } + // TODO 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(); @@ -151,33 +164,40 @@ Command::CmdFunction * Command::find_command(const uint8_t device_type, char * c } // add a command to the list, which does not return json -void Command::add(const uint8_t device_type, const __FlashStringHelper * cmd, cmdfunction_p cb, const __FlashStringHelper * description, uint8_t flag) { +// these commands are not callable directly via MQTT subscriptions either +void Command::add(const uint8_t device_type, const __FlashStringHelper * cmd, cmdfunction_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; } - // if the description is empty, it's hidden which means it will not show up in Web or Console as an available command - bool hidden = (description == nullptr); + // if the description is empty, it's hidden which means it will not show up in Web API or Console as an available command + // TODO check whether we still need this piece of code + if (description == nullptr) { + flags |= CommandFlag::HIDDEN; + } - cmdfunctions_.emplace_back(device_type, flag, cmd, cb, nullptr, description, hidden); // callback for json is nullptr + cmdfunctions_.emplace_back(device_type, flags, cmd, cb, nullptr, description); // callback for json is nullptr // see if we need to subscribe if (Mqtt::enabled()) { - Mqtt::register_command(device_type, cmd, cb, flag); + Mqtt::register_command(device_type, cmd, cb, flags); } } -// add a command to the list, which does return json object as output -// flag is fixed -// optional parameter hidden for commands that will not show up on the Console -void Command::add_with_json(const uint8_t device_type, const __FlashStringHelper * cmd, cmdfunction_json_p cb, const __FlashStringHelper * description, bool hidden) { +// add a command to the list, which does return a json object as output +// flag is fixed to MqttSubFlag::FLAG_NOSUB +void Command::add_returns_json(const uint8_t device_type, + const __FlashStringHelper * cmd, + cmdfunction_json_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, MqttSubFlag::FLAG_NOSUB, cmd, nullptr, cb, description, hidden); // 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 @@ -213,7 +233,7 @@ bool Command::list(const uint8_t device_type, JsonObject & json) { // create a list of commands, sort them std::list sorted_cmds; for (const auto & cf : cmdfunctions_) { - if ((cf.device_type_ == device_type) && !cf.hidden_) { + if ((cf.device_type_ == device_type) && !cf.has_flags(CommandFlag::HIDDEN)) { sorted_cmds.push_back(uuid::read_flash_string(cf.cmd_)); } } @@ -221,7 +241,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.hidden_ && cf.description_ && (cl == uuid::read_flash_string(cf.cmd_))) { + if ((cf.device_type_ == device_type) && !cf.has_flags(CommandFlag::HIDDEN) && cf.description_ && (cl == uuid::read_flash_string(cf.cmd_))) { json[cl] = cf.description_; } } @@ -240,7 +260,7 @@ void Command::show(uuid::console::Shell & shell, uint8_t device_type, bool verbo // create a list of commands, sort them std::list sorted_cmds; for (const auto & cf : cmdfunctions_) { - if ((cf.device_type_ == device_type) && !cf.hidden_) { + if ((cf.device_type_ == device_type) && !cf.has_flags(CommandFlag::HIDDEN)) { sorted_cmds.push_back(uuid::read_flash_string(cf.cmd_)); } } @@ -261,13 +281,13 @@ void Command::show(uuid::console::Shell & shell, uint8_t device_type, bool verbo for (auto & cl : sorted_cmds) { // find and print the description for (const auto & cf : cmdfunctions_) { - if ((cf.device_type_ == device_type) && !cf.hidden_ && cf.description_ && (cl == uuid::read_flash_string(cf.cmd_))) { + if ((cf.device_type_ == device_type) && !cf.has_flags(CommandFlag::HIDDEN) && cf.description_ && (cl == uuid::read_flash_string(cf.cmd_))) { uint8_t i = cl.length(); shell.print(" "); - if (cf.flag_ == FLAG_HC) { + if (cf.has_flags(MQTT_SUB_FLAG_HC)) { shell.print("[hc] "); i += 5; - } else if (cf.flag_ == FLAG_WWC) { + } else if (cf.has_flags(MQTT_SUB_FLAG_WWC)) { shell.print("[wwc] "); i += 6; } @@ -278,6 +298,11 @@ void Command::show(uuid::console::Shell & shell, uint8_t device_type, bool verbo } shell.print(COLOR_BRIGHT_CYAN); shell.print(uuid::read_flash_string(cf.description_)); + if (cf.has_flags(CommandFlag::ADMIN_ONLY)) { + shell.print(' '); + shell.print(COLOR_BRIGHT_RED); + shell.print('*'); + } shell.print(COLOR_RESET); } } @@ -337,7 +362,7 @@ 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 per device: ")); + shell.println(F("Available commands: ")); // show system first shell.print(COLOR_BOLD_ON); diff --git a/src/command.h b/src/command.h index 08ec8b5b3..daee30e80 100644 --- a/src/command.h +++ b/src/command.h @@ -34,6 +34,17 @@ using uuid::console::Shell; namespace emsesp { +// mqtt flags for command subscriptions +enum CommandFlag : uint8_t { + MQTT_SUB_FLAG_NORMAL = 0, // 0 + MQTT_SUB_FLAG_HC = (1 << 0), // 1 + MQTT_SUB_FLAG_WWC = (1 << 1), // 2 + MQTT_SUB_FLAG_NOSUB = (1 << 2), // 4 + HIDDEN = (1 << 3), // 8 + ADMIN_ONLY = (1 << 4) // 16 + +}; + using cmdfunction_p = std::function; using cmdfunction_json_p = std::function; @@ -41,27 +52,37 @@ class Command { public: struct CmdFunction { uint8_t device_type_; // DeviceType:: - uint8_t flag_; // mqtt flags for command subscriptions + uint8_t flags_; // mqtt flags for command subscriptions const __FlashStringHelper * cmd_; cmdfunction_p cmdfunction_; cmdfunction_json_p cmdfunction_json_; const __FlashStringHelper * description_; - bool hidden_; // if its command not to be shown on the Console CmdFunction(const uint8_t device_type, - const uint8_t flag, + const uint8_t flags, const __FlashStringHelper * cmd, cmdfunction_p cmdfunction, cmdfunction_json_p cmdfunction_json, - const __FlashStringHelper * description, - bool hidden = false) + const __FlashStringHelper * description) : device_type_(device_type) - , flag_(flag) + , flags_(flags) , cmd_(cmd) , cmdfunction_(cmdfunction) , cmdfunction_json_(cmdfunction_json) - , description_(description) - , hidden_(hidden) { + , description_(description) { + } + + inline void add_flags(uint8_t flags) { + flags_ |= flags; + } + inline bool has_flags(uint8_t flags) const { + return (flags_ & flags) == flags; + } + inline void remove_flags(uint8_t flags) { + flags_ &= ~flags; + } + inline uint8_t flags() const { + return flags_; } }; @@ -69,11 +90,21 @@ class Command { return cmdfunctions_; } - static bool call(const uint8_t device_type, const char * cmd, const char * value, const int8_t id, JsonObject & json); - static bool call(const uint8_t device_type, const char * cmd, const char * value, const int8_t id = -1); - static void add(const uint8_t device_type, const __FlashStringHelper * cmd, cmdfunction_p cb, const __FlashStringHelper * description, uint8_t flag = 0); - static void - add_with_json(const uint8_t device_type, const __FlashStringHelper * cmd, cmdfunction_json_p cb, const __FlashStringHelper * description, bool hidden = false); + 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); + + static void add(const uint8_t device_type, + const __FlashStringHelper * cmd, + cmdfunction_p cb, + const __FlashStringHelper * description, + uint8_t flags = CommandFlag::MQTT_SUB_FLAG_NORMAL); + + static void add_returns_json(const uint8_t device_type, + const __FlashStringHelper * cmd, + cmdfunction_json_p cb, + const __FlashStringHelper * description, + uint8_t flags = CommandFlag::MQTT_SUB_FLAG_NORMAL); + 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); diff --git a/src/console.cpp b/src/console.cpp index 9cf210108..cc654e63e 100644 --- a/src/console.cpp +++ b/src/console.cpp @@ -376,15 +376,8 @@ void EMSESPShell::add_console_commands() { DynamicJsonDocument doc(EMSESP_JSON_SIZE_XLARGE_DYN); JsonObject json = doc.to(); - bool ok = false; // validate that a command is present if (arguments.size() < 2) { - // // no cmd specified, default to empty command - // if (Command::call(device_type, "", "", -1, json)) { - // serializeJsonPretty(doc, shell); - // shell.println(); - // return; - // } shell.print(F("Missing command. Available commands are: ")); Command::show(shell, device_type, false); // non-verbose mode return; @@ -392,30 +385,36 @@ void EMSESPShell::add_console_commands() { const char * cmd = arguments[1].c_str(); + uint8_t cmd_return = 1; // OK + if (arguments.size() == 2) { // no value specified, just the cmd - ok = Command::call(device_type, cmd, nullptr, -1, json); + cmd_return = Command::call(device_type, cmd, nullptr, true, -1, json); } else if (arguments.size() == 3) { if (strncmp(cmd, "info", 4) == 0) { // info has a id but no value - ok = Command::call(device_type, cmd, nullptr, atoi(arguments.back().c_str()), json); + cmd_return = Command::call(device_type, cmd, nullptr, true, atoi(arguments.back().c_str()), json); } else { - // has a value but no id - ok = Command::call(device_type, cmd, arguments.back().c_str(), -1, json); + // has a value but no id so use -1 + cmd_return = Command::call(device_type, cmd, arguments.back().c_str(), true, -1, json); } } else { // use value, which could be an id or hc - ok = Command::call(device_type, cmd, arguments[2].c_str(), atoi(arguments[3].c_str()), json); + cmd_return = Command::call(device_type, cmd, arguments[2].c_str(), true, atoi(arguments[3].c_str()), json); } - if (ok && json.size()) { + if (cmd_return == 1 && json.size()) { serializeJsonPretty(doc, shell); shell.println(); return; - } else if (!ok) { - shell.println(F("Unknown command, value, or id.")); + } + + if (cmd_return == 2) { + shell.println(F("Unknown command")); shell.print(F("Available commands are: ")); Command::show(shell, device_type, false); // non-verbose mode + } else if (cmd_return == 3) { + shell.println(F("Bad syntax")); } }, [&](Shell & shell __attribute__((unused)), const std::vector & arguments) -> std::vector { diff --git a/src/emsdevice.h b/src/emsdevice.h index ef19c7494..4a771f0c6 100644 --- a/src/emsdevice.h +++ b/src/emsdevice.h @@ -120,9 +120,6 @@ enum DeviceValueTAG : uint8_t { }; -// mqtt flags for command subscriptions -enum MqttSubFlag : uint8_t { FLAG_NORMAL = 0, FLAG_HC, FLAG_WWC, FLAG_NOSUB }; - // mqtt-HA flags enum DeviceValueHA : uint8_t { HA_NONE = 0, HA_VALUE, HA_DONE }; @@ -170,6 +167,7 @@ class EMSdevice { return ((device_id & 0x7F) == (device_id_ & 0x7F)); } + // flags inline void add_flags(uint8_t flags) { flags_ |= flags; } @@ -281,15 +279,14 @@ class EMSdevice { const __FlashStringHelper * const * options, const __FlashStringHelper * const * name, uint8_t uom); - // void register_device_value(uint8_t tag, void * value_p, uint8_t type, const __FlashStringHelper * const * options, const __FlashStringHelper * const * name, uint8_t uom, int32_t min, uint32_t max); void write_command(const uint16_t type_id, const uint8_t offset, uint8_t * message_data, const uint8_t message_length, const uint16_t validate_typeid); void write_command(const uint16_t type_id, const uint8_t offset, const uint8_t value, const uint16_t validate_typeid); void write_command(const uint16_t type_id, const uint8_t offset, const uint8_t value); + void read_command(const uint16_t type_id, uint8_t offset = 0, uint8_t length = 0); void register_mqtt_topic(const std::string & topic, mqtt_subfunction_p f); - // void register_cmd(const __FlashStringHelper * cmd, cmdfunction_p f, uint8_t flag = 0); void publish_mqtt_ha_sensor(); diff --git a/src/mqtt.cpp b/src/mqtt.cpp index a5db4a438..5d0efe268 100644 --- a/src/mqtt.cpp +++ b/src/mqtt.cpp @@ -83,7 +83,7 @@ void Mqtt::subscribe(const uint8_t device_type, const std::string & topic, mqtt_ } // subscribe to the command topic if it doesn't exist yet -void Mqtt::register_command(const uint8_t device_type, const __FlashStringHelper * cmd, cmdfunction_p cb, uint8_t flag) { +void Mqtt::register_command(const uint8_t device_type, const __FlashStringHelper * cmd, cmdfunction_p cb, uint8_t flags) { std::string cmd_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 @@ -108,7 +108,7 @@ void Mqtt::register_command(const uint8_t device_type, const __FlashStringHelper // register the individual commands too (e.g. ems-esp/boiler/wwonetime) // https://github.com/emsesp/EMS-ESP32/issues/31 std::string topic(MQTT_TOPIC_MAX_SIZE, '\0'); - if (subscribe_format_ == 2 && flag == MqttSubFlag::FLAG_HC) { + if (subscribe_format_ == Subscribe_Format::INDIVIDUAL_MAIN_HC && ((flags & CommandFlag::MQTT_SUB_FLAG_HC) == CommandFlag::MQTT_SUB_FLAG_HC)) { topic = cmd_topic + "/hc1/" + uuid::read_flash_string(cmd); queue_subscribe_message(topic); topic = cmd_topic + "/hc2/" + uuid::read_flash_string(cmd); @@ -117,7 +117,7 @@ void Mqtt::register_command(const uint8_t device_type, const __FlashStringHelper queue_subscribe_message(topic); topic = cmd_topic + "/hc4/" + uuid::read_flash_string(cmd); queue_subscribe_message(topic); - } else if (subscribe_format_ && flag != MqttSubFlag::FLAG_NOSUB) { + } else if (subscribe_format_ != Subscribe_Format::GENERAL && ((flags & CommandFlag::MQTT_SUB_FLAG_NOSUB) == CommandFlag::MQTT_SUB_FLAG_NOSUB)) { topic = cmd_topic + "/" + uuid::read_flash_string(cmd); queue_subscribe_message(topic); } @@ -140,7 +140,7 @@ void Mqtt::resubscribe() { } for (const auto & cf : Command::commands()) { std::string topic(MQTT_TOPIC_MAX_SIZE, '\0'); - if (subscribe_format_ == 2 && cf.flag_ == MqttSubFlag::FLAG_HC) { + if (subscribe_format_ == Subscribe_Format::INDIVIDUAL_MAIN_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_); @@ -149,7 +149,7 @@ void Mqtt::resubscribe() { 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_ && cf.flag_ != MqttSubFlag::FLAG_NOSUB) { + } 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); } @@ -225,7 +225,7 @@ void Mqtt::show_mqtt(uuid::console::Shell & shell) { shell.printfln(F(" %s/%s"), mqtt_base_.c_str(), mqtt_subfunction.topic_.c_str()); } for (const auto & cf : Command::commands()) { - if (subscribe_format_ == 2 && cf.flag_ == MqttSubFlag::FLAG_HC) { + if (subscribe_format_ == 2 && 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(), @@ -242,7 +242,7 @@ void Mqtt::show_mqtt(uuid::console::Shell & shell) { 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_ && cf.flag_ != MqttSubFlag::FLAG_NOSUB) { + } else if (subscribe_format_ == 1 && !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(), @@ -346,8 +346,13 @@ void Mqtt::on_message(const char * fulltopic, const char * payload, size_t len) } cmd_only++; // skip the / // LOG_INFO(F("devicetype= %d, topic = %s, cmd = %s, message = %s), mf.device_type_, topic, cmd_only, message); - if (!Command::call(mf.device_type_, cmd_only, message)) { - LOG_ERROR(F("No matching cmd (%s) in topic %s, or invalid data"), cmd_only, topic); + // call command, assume admin authentication is allowed + uint8_t cmd_return = Command::call(mf.device_type_, cmd_only, message, true); + if (cmd_return == 2) { + LOG_ERROR(F("No matching cmd (%s) in topic %s"), cmd_only, topic); + Mqtt::publish(F_(response), "unknown"); + } else if (cmd_return == 3) { + LOG_ERROR(F("Invalid data with cmd (%s) in topic %s"), cmd_only, topic); Mqtt::publish(F_(response), "unknown"); } return; @@ -376,29 +381,32 @@ void Mqtt::on_message(const char * fulltopic, const char * payload, size_t len) n = doc["id"]; } - bool cmd_known = false; - JsonVariant data = doc["data"]; + uint8_t cmd_return = 1; // OK + JsonVariant data = doc["data"]; if (data.is()) { - cmd_known = Command::call(mf.device_type_, command, data.as(), n); + cmd_return = Command::call(mf.device_type_, command, data.as(), true, n); } else if (data.is()) { char data_str[10]; - cmd_known = Command::call(mf.device_type_, command, Helpers::itoa(data_str, (int16_t)data.as()), n); + 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_known = Command::call(mf.device_type_, command, Helpers::render_value(data_str, (float)data.as(), 2), n); + 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_known = Command::call(mf.device_type_, command, "", n, json); - if (cmd_known && json.size()) { + cmd_return = Command::call(mf.device_type_, command, "", true, n, json); + if (json.size()) { Mqtt::publish(F_(response), resp.as()); return; } } - if (!cmd_known) { - LOG_ERROR(F("No matching cmd (%s) or invalid data"), command); + if (cmd_return == 2) { + LOG_ERROR(F("No matching cmd (%s)"), command); + Mqtt::publish(F_(response), "unknown"); + } else if (cmd_return == 3) { + LOG_ERROR(F("Invalid data for cmd (%s)"), command); Mqtt::publish(F_(response), "unknown"); } diff --git a/src/mqtt.h b/src/mqtt.h index 9e7e5eb7e..51cc6ca8c 100644 --- a/src/mqtt.h +++ b/src/mqtt.h @@ -81,7 +81,20 @@ class Mqtt { enum Operation { PUBLISH, SUBSCRIBE }; - enum HA_Climate_Format : uint8_t { CURRENT = 1, SETPOINT, ZERO }; + enum HA_Climate_Format : uint8_t { + CURRENT = 1, // 1 + SETPOINT, // 2 + ZERO // 3 + + }; + + // subscribe_format + enum Subscribe_Format : uint8_t { + GENERAL = 0, // 0 + INDIVIDUAL_MAIN_HC, // 1 + INDIVIDUAL_ALL_HC // 2 + + }; static constexpr uint8_t MQTT_TOPIC_MAX_SIZE = 128; // note this should really match the user setting in mqttSettings.maxTopicLength @@ -109,7 +122,7 @@ class Mqtt { const uint8_t device_type, const __FlashStringHelper * entity, const uint8_t uom = 0); - static void register_command(const uint8_t device_type, const __FlashStringHelper * cmd, cmdfunction_p cb, uint8_t tag = 0); + static void register_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); diff --git a/src/shower.cpp b/src/shower.cpp index 6d3e810d0..bf1ae4d25 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")); - Command::call(EMSdevice::DeviceType::BOILER, "wwtapactivated", "true"); + (void) Command::call(EMSdevice::DeviceType::BOILER, "wwtapactivated", "true", true); // no need to check authentication 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")); - Command::call(EMSdevice::DeviceType::BOILER, "wwtapactivated", "false"); + (void) Command::call(EMSdevice::DeviceType::BOILER, "wwtapactivated", "false", true); // no need to check authentication doing_cold_shot_ = true; alert_timer_start_ = uuid::get_uptime(); // timer starts now } diff --git a/src/system.cpp b/src/system.cpp index d3d462d4e..e20124b18 100644 --- a/src/system.cpp +++ b/src/system.cpp @@ -624,13 +624,14 @@ void System::system_check() { // these commands respond to the topic "system" and take a payload like {cmd:"", data:"", id:""} // no individual subscribe for pin command because id is needed void System::commands_init() { - Command::add(EMSdevice::DeviceType::SYSTEM, F_(pin), System::command_pin, F("set GPIO"), MqttSubFlag::FLAG_NOSUB); - Command::add(EMSdevice::DeviceType::SYSTEM, F_(send), System::command_send, F("send a telegram")); - Command::add(EMSdevice::DeviceType::SYSTEM, F_(publish), System::command_publish, F("force a MQTT publish")); - Command::add(EMSdevice::DeviceType::SYSTEM, F_(fetch), System::command_fetch, F("refresh all EMS values")); - Command::add_with_json(EMSdevice::DeviceType::SYSTEM, F_(info), System::command_info, F("system status")); - Command::add_with_json(EMSdevice::DeviceType::SYSTEM, F_(settings), System::command_settings, F("list system settings")); - Command::add_with_json(EMSdevice::DeviceType::SYSTEM, F_(commands), System::command_commands, F("list system commands")); + Command::add(EMSdevice::DeviceType::SYSTEM, F_(pin), System::command_pin, F("set GPIO"), CommandFlag::MQTT_SUB_FLAG_NOSUB | CommandFlag::ADMIN_ONLY); + Command::add(EMSdevice::DeviceType::SYSTEM, F_(send), System::command_send, F("send a telegram"), CommandFlag::ADMIN_ONLY); + Command::add(EMSdevice::DeviceType::SYSTEM, F_(publish), System::command_publish, F("force a MQTT publish"), 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_returns_json(EMSdevice::DeviceType::SYSTEM, F_(info), System::command_info, F("system status")); + Command::add_returns_json(EMSdevice::DeviceType::SYSTEM, F_(settings), System::command_settings, F("list system settings")); + Command::add_returns_json(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 @@ -795,11 +796,12 @@ bool System::command_settings(const char * value, const int8_t id, JsonObject & node = json.createNestedObject("System"); node["version"] = EMSESP_APP_VERSION; + // hide ssid from this list EMSESP::esp8266React.getNetworkSettingsService()->read([&](NetworkSettings & settings) { - node = json.createNestedObject("Network"); - // node["ssid"] = settings.ssid; // commented out - people don't like others to see this + node = json.createNestedObject("Network"); node["hostname"] = settings.hostname; node["static_ip_config"] = settings.staticIPConfig; + node["enableIPv6"] = settings.enableIPv6; JsonUtils::writeIP(node, "local_ip", settings.localIP); JsonUtils::writeIP(node, "gateway_ip", settings.gatewayIP); JsonUtils::writeIP(node, "subnet_mask", settings.subnetMask); @@ -839,6 +841,7 @@ 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; }); #ifndef EMSESP_STANDALONE @@ -996,4 +999,12 @@ bool System::load_board_profile(std::vector & data, const std::string & return true; } +// restart command - perform a hard reset +bool System::command_restart(const char * value, const int8_t id) { +#ifndef EMSESP_STANDALONE + ESP.restart(); +#endif + return true; +} + } // namespace emsesp diff --git a/src/system.h b/src/system.h index d4b784932..300baec68 100644 --- a/src/system.h +++ b/src/system.h @@ -52,13 +52,15 @@ class System { static bool command_send(const char * value, const int8_t id); static bool command_publish(const char * value, const int8_t id); static bool command_fetch(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_restart(const char * value, const int8_t id); #if defined(EMSESP_DEBUG) static bool command_test(const char * value, const int8_t id); #endif + 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); + void restart(); void format(uuid::console::Shell & shell); void upload_status(bool in_progress); diff --git a/src/test/test.cpp b/src/test/test.cpp index 94e8bc111..16b73f49f 100644 --- a/src/test/test.cpp +++ b/src/test/test.cpp @@ -352,7 +352,7 @@ void Test::run_test(uuid::console::Shell & shell, const std::string & cmd) { if (emsdevice) { doc.clear(); JsonObject json = doc.to(); - Command::call(emsdevice->device_type(), "info", nullptr, -1, json); + Command::call(emsdevice->device_type(), "info", nullptr, true, -1, json); Serial.print(COLOR_YELLOW); if (json.size() != 0) { @@ -424,7 +424,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"); + Command::call(EMSdevice::DeviceType::BOILER, "wwtapactivated", "false", true); } if (command == "fr120") { @@ -935,12 +935,23 @@ void Test::run_test(uuid::console::Shell & shell, const std::string & cmd) { Mqtt::ha_enabled(false); run_test("general"); AsyncWebServerRequest request; + + // GET 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); + + // POST + request.method(HTTP_POST); + request.url("/api/system/commands"); + EMSESP::webAPIService.webAPIService_get(&request); + #endif } diff --git a/src/test/test.h b/src/test/test.h index df96d7e1f..87e0c6bf0 100644 --- a/src/test/test.h +++ b/src/test/test.h @@ -38,8 +38,8 @@ namespace emsesp { // #define EMSESP_DEBUG_DEFAULT "board_profile" // #define EMSESP_DEBUG_DEFAULT "shower_alert" // #define EMSESP_DEBUG_DEFAULT "310" -// #define EMSESP_DEBUG_DEFAULT "api" -#define EMSESP_DEBUG_DEFAULT "crash" +#define EMSESP_DEBUG_DEFAULT "api" +// #define EMSESP_DEBUG_DEFAULT "crash" class Test { public: diff --git a/src/web/WebAPIService.cpp b/src/web/WebAPIService.cpp index c1e121300..20f06110f 100644 --- a/src/web/WebAPIService.cpp +++ b/src/web/WebAPIService.cpp @@ -26,7 +26,7 @@ namespace emsesp { WebAPIService::WebAPIService(AsyncWebServer * server, SecurityManager * securityManager) : _securityManager(securityManager) - , _apiHandler("/api", std::bind(&WebAPIService::webAPIService_post, this, _1, _2), 256) { // for POSTS + , _apiHandler("/api", std::bind(&WebAPIService::webAPIService_post, this, _1, _2), 256) { // for POSTS, must use 'Content-Type: application/json' in header server->on("/api", HTTP_GET, std::bind(&WebAPIService::webAPIService_get, this, _1)); // for GETS server->addHandler(&_apiHandler); } @@ -47,7 +47,7 @@ void WebAPIService::webAPIService_get(AsyncWebServerRequest * request) { // HTTP_POST | HTTP_PUT | HTTP_PATCH // POST/PUT /{device}[/{hc}][/{name}] void WebAPIService::webAPIService_post(AsyncWebServerRequest * request, JsonVariant & json) { - // extra the params from the json body + // if no body then treat it as a secure GET if (not json.is()) { webAPIService_get(request); return; @@ -158,33 +158,42 @@ void WebAPIService::parse(AsyncWebServerRequest * request, std::string & device_ // check that we have permissions first. We require authenticating on 1 or more of these conditions: // 1. any HTTP POSTs or PUTs - // 2. a HTTP GET which has a 'data' parameter which is not empty (to keep v2 compatibility) - auto method = request->method(); - bool have_data = !value_s.empty(); - bool admin_allowed; + // 2. an HTTP GET which has a 'data' parameter which is not empty (to keep v2 compatibility) + auto method = request->method(); + bool have_data = !value_s.empty(); + bool authenticated = false; EMSESP::webSettingsService.read([&](WebSettings & settings) { Authentication authentication = _securityManager->authenticateRequest(request); - admin_allowed = settings.notoken_api | AuthenticationPredicates::IS_ADMIN(authentication); + authenticated = settings.notoken_api | AuthenticationPredicates::IS_ADMIN(authentication); }); if ((method != HTTP_GET) || ((method == HTTP_GET) && have_data)) { - if (!admin_allowed) { + if (!authenticated) { send_message_response(request, 401, "Bad credentials"); // Unauthorized return; } } - // now we have all the parameters go and execute the command PrettyAsyncJsonResponse * response = new PrettyAsyncJsonResponse(false, EMSESP_JSON_SIZE_XLARGE_DYN); JsonObject json = response->getRoot(); - bool ok = Command::call(device_type, cmd_s.c_str(), (have_data ? value_s.c_str() : nullptr), id_n, json); + // 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); // check for errors - if (!ok) { + if (cmd_reply == 2) { + delete response; + send_message_response(request, 400, "Command not found"); // Bad Request + return; + } else if (cmd_reply == 3) { delete response; send_message_response(request, 400, "Problems parsing elements"); // Bad Request return; + } else if (cmd_reply == 4) { + delete response; + send_message_response(request, 401, "Bad credentials"); // Unauthorized + return; } if (!json.size()) { diff --git a/src/web/WebDataService.cpp b/src/web/WebDataService.cpp index e3ee34a38..86c5f142f 100644 --- a/src/web/WebDataService.cpp +++ b/src/web/WebDataService.cpp @@ -118,6 +118,7 @@ void WebDataService::device_data(AsyncWebServerRequest * request, JsonVariant & } // takes a command and its data value from a specific Device, from the Web +// assumes the service has been checked for admin authentication void WebDataService::write_value(AsyncWebServerRequest * request, JsonVariant & json) { if (json.is()) { JsonObject dv = json["devicevalue"]; @@ -129,22 +130,22 @@ void WebDataService::write_value(AsyncWebServerRequest * request, JsonVariant & if (emsdevice->unique_id() == id) { const char * cmd = dv["c"]; uint8_t device_type = emsdevice->device_type(); - bool ok = false; + uint8_t cmd_return = 1; // OK char s[10]; // the data could be in any format, but we need string JsonVariant data = dv["v"]; if (data.is()) { - ok = Command::call(device_type, cmd, data.as()); + cmd_return = Command::call(device_type, cmd, data.as(), true); } else if (data.is()) { - ok = Command::call(device_type, cmd, Helpers::render_value(s, data.as(), 0)); + cmd_return = Command::call(device_type, cmd, Helpers::render_value(s, data.as(), 0), true); } else if (data.is()) { - ok = Command::call(device_type, cmd, Helpers::render_value(s, (float)data.as(), 1)); + cmd_return = Command::call(device_type, cmd, Helpers::render_value(s, (float)data.as(), 1), true); } else if (data.is()) { - ok = Command::call(device_type, cmd, data.as() ? "true" : "false"); + cmd_return = Command::call(device_type, cmd, data.as() ? "true" : "false", true); } // send "Write command sent to device" or "Write command failed" - AsyncWebServerResponse * response = request->beginResponse(ok ? 200 : 204); + AsyncWebServerResponse * response = request->beginResponse((cmd_return == 1) ? 200 : 204); request->send(response); return; }