commands take a set of flags, like NEED_ADMIN or HIDDEN

This commit is contained in:
proddy
2021-07-20 21:45:29 +02:00
parent 0762d9e124
commit 77f6a18075
13 changed files with 229 additions and 122 deletions

View File

@@ -28,57 +28,66 @@ std::vector<Command::CmdFunction> 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<std::string> 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<std::string> 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);

View File

@@ -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<bool(const char * data, const int8_t id)>;
using cmdfunction_json_p = std::function<bool(const char * data, const int8_t id, JsonObject & json)>;
@@ -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);

View File

@@ -376,15 +376,8 @@ void EMSESPShell::add_console_commands() {
DynamicJsonDocument doc(EMSESP_JSON_SIZE_XLARGE_DYN);
JsonObject json = doc.to<JsonObject>();
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<std::string> & arguments) -> std::vector<std::string> {

View File

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

View File

@@ -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<const char *>()) {
cmd_known = Command::call(mf.device_type_, command, data.as<const char *>(), n);
cmd_return = Command::call(mf.device_type_, command, data.as<const char *>(), true, n);
} else if (data.is<int>()) {
char data_str[10];
cmd_known = Command::call(mf.device_type_, command, Helpers::itoa(data_str, (int16_t)data.as<int>()), n);
cmd_return = Command::call(mf.device_type_, command, Helpers::itoa(data_str, (int16_t)data.as<int>()), true, n);
} else if (data.is<float>()) {
char data_str[10];
cmd_known = Command::call(mf.device_type_, command, Helpers::render_value(data_str, (float)data.as<float>(), 2), n);
cmd_return = Command::call(mf.device_type_, command, Helpers::render_value(data_str, (float)data.as<float>(), 2), true, n);
} else if (data.isNull()) {
DynamicJsonDocument resp(EMSESP_JSON_SIZE_XLARGE_DYN);
JsonObject json = resp.to<JsonObject>();
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<JsonObject>());
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");
}

View File

@@ -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);

View File

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

View File

@@ -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<uint8_t> & 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

View File

@@ -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);

View File

@@ -352,7 +352,7 @@ void Test::run_test(uuid::console::Shell & shell, const std::string & cmd) {
if (emsdevice) {
doc.clear();
JsonObject json = doc.to<JsonObject>();
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
}

View File

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

View File

@@ -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<JsonObject>()) {
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()) {

View File

@@ -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>()) {
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<const char *>()) {
ok = Command::call(device_type, cmd, data.as<const char *>());
cmd_return = Command::call(device_type, cmd, data.as<const char *>(), true);
} else if (data.is<int>()) {
ok = Command::call(device_type, cmd, Helpers::render_value(s, data.as<int16_t>(), 0));
cmd_return = Command::call(device_type, cmd, Helpers::render_value(s, data.as<int16_t>(), 0), true);
} else if (data.is<float>()) {
ok = Command::call(device_type, cmd, Helpers::render_value(s, (float)data.as<float>(), 1));
cmd_return = Command::call(device_type, cmd, Helpers::render_value(s, (float)data.as<float>(), 1), true);
} else if (data.is<bool>()) {
ok = Command::call(device_type, cmd, data.as<bool>() ? "true" : "false");
cmd_return = Command::call(device_type, cmd, data.as<bool>() ? "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;
}