Refactor MQTT subscriptions #173

This commit is contained in:
proddy
2021-11-01 23:31:30 +01:00
parent 40a7026d4c
commit 01bace4048
38 changed files with 873 additions and 942 deletions

View File

@@ -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
@@ -36,6 +37,8 @@
- 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**

View File

@@ -187,23 +187,16 @@ class MqttSettingsForm extends React.Component<MqttSettingsFormProps> {
<MenuItem value={1}>Nested on a single topic</MenuItem>
<MenuItem value={2}>As individual topics</MenuItem>
</SelectValidator>
<SelectValidator
name="subscribe_format"
label="Subscribe Topics"
value={data.subscribe_format}
fullWidth
variant="outlined"
onChange={handleValueChange('subscribe_format')}
margin="normal"
>
<MenuItem value={0}>one topic per device</MenuItem>
<MenuItem value={1}>
topics for each device and it's values (main heating circuit only)
</MenuItem>
<MenuItem value={2}>
topic for each device and it's values (all heating circuits)
</MenuItem>
</SelectValidator>
<BlockFormControlLabel
control={
<Checkbox
checked={data.send_response}
onChange={handleValueChange('send_response')}
value="send_response"
/>
}
label="Publish command output to a 'response' topic"
/>
<BlockFormControlLabel
control={
<Checkbox

View File

@@ -39,5 +39,5 @@ export interface MqttSettings {
ha_enabled: boolean;
ha_climate_format: number;
nested_format: number;
subscribe_format: number;
send_response: boolean;
}

View File

@@ -64,11 +64,11 @@ export interface EMSESPData {
}
export interface DeviceValue {
v: any;
u: number;
n: string;
c: string;
l: string[];
v: any; // value, in any format
u: number; // uom
n: string; // name
c: string; // command
l: string[]; // list
}
export interface EMSESPDeviceData {

View File

@@ -175,7 +175,7 @@ void MqttSettings::read(MqttSettings & settings, JsonObject & root) {
root["ha_climate_format"] = settings.ha_climate_format;
root["ha_enabled"] = settings.ha_enabled;
root["nested_format"] = settings.nested_format;
root["subscribe_format"] = settings.subscribe_format;
root["send_response"] = settings.send_response;
}
StateUpdateResult MqttSettings::update(JsonObject & root, MqttSettings & settings) {
@@ -205,7 +205,7 @@ StateUpdateResult MqttSettings::update(JsonObject & root, MqttSettings & setting
newSettings.ha_climate_format = root["ha_climate_format"] | EMSESP_DEFAULT_HA_CLIMATE_FORMAT;
newSettings.ha_enabled = root["ha_enabled"] | EMSESP_DEFAULT_HA_ENABLED;
newSettings.nested_format = root["nested_format"] | EMSESP_DEFAULT_NESTED_FORMAT;
newSettings.subscribe_format = root["subscribe_format"] | EMSESP_DEFAULT_SUBSCRIBE_FORMAT;
newSettings.send_response = root["send_response"] | EMSESP_DEFAULT_SEND_RESPONSE;
if (newSettings.enabled != settings.enabled) {
changed = true;
@@ -220,7 +220,7 @@ StateUpdateResult MqttSettings::update(JsonObject & root, MqttSettings & setting
changed = true;
}
if (newSettings.subscribe_format != settings.subscribe_format) {
if (newSettings.send_response != settings.send_response) {
changed = true;
}

View File

@@ -89,7 +89,7 @@ class MqttSettings {
uint8_t ha_climate_format;
bool ha_enabled;
uint8_t nested_format;
uint8_t subscribe_format;
bool send_response;
static void read(MqttSettings & settings, JsonObject & root);
static StateUpdateResult update(JsonObject & root, MqttSettings & settings);

View File

@@ -39,7 +39,7 @@ class DummySettings {
uint8_t ha_climate_format = 1;
bool ha_enabled = true;
String base = "ems-esp";
uint8_t subscribe_format = 0;
bool send_response = true;
String hostname = "ems-esp";
String jwtSecret = "ems-esp";

View File

@@ -225,7 +225,7 @@ const mqtt_settings = {
ha_climate_format: 1,
ha_enabled: true,
nested_format: 1,
subscribe_format: 0,
send_response: true,
}
const mqtt_status = {
enabled: true,

View File

@@ -26,29 +26,215 @@ uuid::log::Logger Command::logger_{F_(command), uuid::log::Facility::DAEMON};
std::vector<Command::CmdFunction> 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;
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 {
// 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"];
}
}
// 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<const char *>()) {
cmd_return = Command::call(device_type, command_p, data.as<const char *>(), authenticated, id_n, output);
} else if (data.is<int>()) {
char data_str[10];
cmd_return = Command::call(device_type, command_p, Helpers::itoa(data_str, (int16_t)data.as<int>()), authenticated, id_n, output);
} else if (data.is<float>()) {
char data_str[10];
cmd_return = Command::call(device_type, command_p, Helpers::render_value(data_str, (float)data.as<float>(), 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<EMSESP_JSON_SIZE_SMALL> output_doc;
JsonObject output = output_doc.to<JsonObject>();
// 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 & output) {
uint8_t return_code = CommandRet::OK;
// see if there is a command registered
auto cf = find_command(device_type, cmd);
// 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) {
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);
@@ -56,113 +242,34 @@ uint8_t Command::call(const uint8_t device_type, const char * cmd, const char *
LOG_INFO(F("Calling %s command '%s', value %s, id is %d"), dname.c_str(), cmd, value, id);
}
return ((cf->cmdfunction_)(value, id_new)) ? CommandRet::OK : CommandRet::ERROR;
}
// 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));
auto cf = find_command(device_type, cmd_new, id_new);
// check if we're allowed to call it
if (cf != nullptr) {
// 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;
}
// call the function
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;
}
return ((cf->cmdfunction_)(value, id_new)) ? CommandRet::OK : CommandRet::ERROR;
return_code = ((cf->cmdfunction_json_)(value, id, output)) ? CommandRet::OK : CommandRet::ERROR;
}
if (cf->cmdfunction_) {
return_code = ((cf->cmdfunction_)(value, id)) ? 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;
// report error if call failed
if (return_code != CommandRet::OK) {
output.clear();
output["message"] = "error: function failed";
}
// empty command is info with id0
if (cmd[0] == '\0') {
strcpy(cmd, "info");
id = 0;
return return_code;
}
// 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;
}
}
// 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;
}
}
// 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<n>.]");
i += 8;
} else if (cf.has_flags(MQTT_SUB_FLAG_WWC)) {
shell.print("[wwc] ");
i += 6;
shell.print("[wwc<n>.]");
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

View File

@@ -25,6 +25,7 @@
#include <string>
#include <vector>
#include <functional>
#include <unordered_map>
#include "console.h"
@@ -57,7 +58,7 @@ enum CommandRet : uint8_t {
};
using cmd_function_p = std::function<bool(const char * data, const int8_t id)>;
using cmd_json_function_p = std::function<bool(const char * data, const int8_t id, JsonObject & json)>;
using cmd_json_function_p = std::function<bool(const char * data, const int8_t id, JsonObject & output)>;
class Command {
public:
@@ -101,16 +102,20 @@ 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,
// 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,
@@ -118,13 +123,16 @@ class Command {
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<CmdFunction> cmdfunctions_; // list of commands
};
typedef std::unordered_map<std::string, std::string> KeyValueMap_t;
typedef std::vector<std::string> 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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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::HeatingCircuit> 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<n>" 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

View File

@@ -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;
}
// function with no min and max values
// 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 (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));
}
}
}

View File

@@ -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<void(std::shared_ptr<const Telegram>)>;
@@ -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<const Telegram> telegram);

View File

@@ -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<const Telegram> telegram) {
if (!Mqtt::connected()) {
return;
}
StaticJsonDocument<EMSESP_JSON_SIZE_SMALL> 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<const Telegram> 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());
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
}
}

View File

@@ -249,9 +249,9 @@ class EMSESP {
static void process_version(std::shared_ptr<const Telegram> telegram);
static void publish_response(std::shared_ptr<const Telegram> 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_;

View File

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

View File

@@ -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::QueuedMqttMessage> Mqtt::mqtt_messages_;
std::vector<Mqtt::MQTTSubFunction> 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
// 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<EMSESP_JSON_SIZE_SMALL> 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
}
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");
}
// parse and call the command
StaticJsonDocument<EMSESP_JSON_SIZE_LARGE_DYN> output_doc;
JsonObject output = output_doc.to<JsonObject>();
uint8_t return_code = Command::process(topic, true, input.as<JsonObject>(), output); // mqtt is always authenticated
// send MQTT response if enabled
if (!send_response_ || output.isNull()) {
return;
}
// It's a command then with the payload being JSON like {"cmd":"<cmd>", "data":<data>, "id":<n>}
// Find the command from the json and call it directly
StaticJsonDocument<EMSESP_JSON_SIZE_SMALL> doc;
DeserializationError error = deserializeJson(doc, message);
if (error) {
LOG_ERROR(F("MQTT error: payload %s, error %s"), message, error.c_str());
return;
if (return_code != CommandRet::OK) {
Mqtt::publish(F_(response), (const char *)output["message"]);
} else {
Mqtt::publish(F_(response), output); // output response from call
}
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<const char *>()) {
cmd_return = Command::call(mf.device_type_, command, data.as<const char *>(), true, n);
} else if (data.is<int>()) {
char data_str[10];
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_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_return = Command::call(mf.device_type_, command, "", true, n, json);
if (json.size()) {
Mqtt::publish(F_(response), resp.as<JsonObject>());
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);
}
// 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<const MqttMessage> 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<MqttMessage> message;
message = std::make_shared<MqttMessage>(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

View File

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

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"));
(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
}

View File

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

View File

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

View File

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

View File

@@ -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<JsonVariant>();
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<JsonVariant>();
request.url("/api/thermostat");
EMSESP::webAPIService.webAPIService_post(&request, json);
// 2
char data2[] = "{\"value\":12}";
deserializeJson(doc, data2);
json = doc.as<JsonVariant>();
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<JsonVariant>();
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<JsonVariant>();
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<JsonVariant>();
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<JsonVariant>();
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<JsonVariant>();
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<JsonVariant>();
request.url("/api/thermostat");
EMSESP::webAPIService.webAPIService_post(&request, json);
// 2
char data2[] = "{\"value\":12}";
deserializeJson(doc, data2);
json = doc.as<JsonVariant>();
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<JsonVariant>();
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<JsonVariant>();
request.url("/api/system/send");
EMSESP::webAPIService.webAPIService_post(&request, json);
#endif
}
if (command == "crash") {
shell.printfln(F("Forcing a crash..."));

View File

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

View File

@@ -1 +1 @@
#define EMSESP_APP_VERSION "3.2.2b14"
#define EMSESP_APP_VERSION "3.3.0b0"

View File

@@ -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<EMSESP_JSON_SIZE_SMALL> input_doc;
JsonObject input = input_doc.to<JsonObject>();
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<JsonObject>()) {
@@ -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<JsonObject>();
#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<std::string>(); // always convert value to string
std::string device_s = body["device"].as<std::string>();
// get the command. It can be either 'name' or 'cmd'
std::string cmd_s("");
if (body.containsKey("name")) {
cmd_s = body["name"].as<std::string>();
} else if (body.containsKey("cmd")) {
cmd_s = body["cmd"].as<std::string>();
}
// 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<JsonObject>();
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;
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);
});
if ((method != HTTP_GET) || ((method == HTTP_GET) && have_data)) {
if (!authenticated) {
send_message_response(request, 401, "Bad credentials"); // Unauthorized
return;
}
}
// 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;
// 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
}
if (!json.size()) {
delete response;
send_message_response(request, 200, "OK"); // OK
return;
} 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

View File

@@ -31,31 +31,6 @@
namespace emsesp {
typedef std::unordered_map<std::string, std::string> KeyValueMap_t;
typedef std::vector<std::string> 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

View File

@@ -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
@@ -127,30 +127,48 @@ void WebDataService::device_data(AsyncWebServerRequest * request, JsonVariant &
void WebDataService::write_value(AsyncWebServerRequest * request, JsonVariant & json) {
if (json.is<JsonObject>()) {
JsonObject dv = json["devicevalue"];
uint8_t id = json["id"];
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<const char *>()) {
cmd_return = Command::call(device_type, cmd, data.as<const char *>(), true);
command_ret = Command::call(device_type, cmd, data.as<const char *>(), true, id, output);
} else if (data.is<int>()) {
cmd_return = Command::call(device_type, cmd, Helpers::render_value(s, data.as<int16_t>(), 0), true);
char s[10];
command_ret = Command::call(device_type, cmd, Helpers::render_value(s, data.as<int16_t>(), 0), true, id, output);
} else if (data.is<float>()) {
cmd_return = Command::call(device_type, cmd, Helpers::render_value(s, (float)data.as<float>(), 1), true);
char s[10];
command_ret = Command::call(device_type, cmd, Helpers::render_value(s, (float)data.as<float>(), 1), true, id, output);
} else if (data.is<bool>()) {
cmd_return = Command::call(device_type, cmd, data.as<bool>() ? "true" : "false", true);
command_ret = Command::call(device_type, cmd, data.as<bool>() ? "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;
}

View File

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

View File

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