mirror of
https://github.com/emsesp/EMS-ESP32.git
synced 2025-12-06 07:49:52 +03:00
Refactor MQTT subscriptions #173
This commit is contained in:
@@ -17,6 +17,7 @@
|
||||
- Added support for mDNS [#161](https://github.com/emsesp/EMS-ESP32/issues/161)
|
||||
- Added last system ESP32 reset code to log (and `system info` output)
|
||||
- Firmware Checker in WebUI [#168](https://github.com/emsesp/EMS-ESP32/issues/168)
|
||||
- Added new MQTT setting for 'response' topic
|
||||
|
||||
## Fixed
|
||||
|
||||
@@ -29,13 +30,15 @@
|
||||
## Changed
|
||||
|
||||
- Syslog BOM only for utf-8 messages [#91](https://github.com/emsesp/EMS-ESP32/issues/91)
|
||||
- Check for KM200 by device-id 0x48, remove tx-delay[#90](https://github.com/emsesp/EMS-ESP32/issues/90)
|
||||
- Check for KM200 by device-id 0x48, remove tx-delay [#90](https://github.com/emsesp/EMS-ESP32/issues/90)
|
||||
- rename `fastheatupfactor` to `fastheatup` and add percent [#122]
|
||||
- "unit" renamed to "uom" in API call to recall a Device Value
|
||||
- initial backend React changes to replace the class components (HOCs) with React Hooks
|
||||
- Use program-names instead of numbers
|
||||
- Boiler's maintenancemessage always published in MQTT (to prevent HA missing entity)
|
||||
- Unit of Measure 'times' added to MQTT Fails, Rx fails, Rx received, Tx fails, Tx reads & Tx writes
|
||||
- Improved API. Restful HTTP API works in the same way as MQTT calls
|
||||
- Removed settings for MQTT subscribe format [#173](https://github.com/emsesp/EMS-ESP32/issues/173)
|
||||
|
||||
## **BREAKING CHANGES**
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -39,5 +39,5 @@ export interface MqttSettings {
|
||||
ha_enabled: boolean;
|
||||
ha_climate_format: number;
|
||||
nested_format: number;
|
||||
subscribe_format: number;
|
||||
send_response: boolean;
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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,
|
||||
|
||||
448
src/command.cpp
448
src/command.cpp
@@ -26,143 +26,250 @@ 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;
|
||||
}
|
||||
|
||||
std::string dname = EMSdevice::device_type_2_device_name(device_type);
|
||||
if (value == nullptr) {
|
||||
LOG_INFO(F("Calling %s command '%s'"), dname.c_str(), cmd);
|
||||
} else if (id == -1) {
|
||||
LOG_INFO(F("Calling %s command '%s', value %s, id is default"), dname.c_str(), cmd, value);
|
||||
const char * command_p = nullptr;
|
||||
if (num_paths == 2) {
|
||||
command_p = p.paths()[1].c_str();
|
||||
} else if (num_paths >= 3) {
|
||||
// concatenate the path into one string
|
||||
char command[50];
|
||||
snprintf(command, sizeof(command), "%s/%s", p.paths()[1].c_str(), p.paths()[2].c_str());
|
||||
command_p = command;
|
||||
} else {
|
||||
LOG_INFO(F("Calling %s command '%s', value %s, id is %d"), dname.c_str(), cmd, value, id);
|
||||
// take it from the JSON. Support both name and cmd to keep backwards compatibility
|
||||
if (input.containsKey("name")) {
|
||||
command_p = input["name"];
|
||||
} else if (input.containsKey("cmd")) {
|
||||
command_p = input["cmd"];
|
||||
}
|
||||
}
|
||||
|
||||
return ((cf->cmdfunction_)(value, id_new)) ? CommandRet::OK : CommandRet::ERROR;
|
||||
// some commands may be prefixed with hc. or wwc. so extract these
|
||||
// exit if we don't have a command
|
||||
command_p = parse_command_string(command_p, id_n);
|
||||
if (command_p == nullptr) {
|
||||
output.clear();
|
||||
output["message"] = "error: missing command";
|
||||
return CommandRet::NOT_FOUND;
|
||||
}
|
||||
|
||||
// if we don't have an id/hc/wwc try and get it from the JSON input
|
||||
// it's allowed to have no id, and then keep the default to -1
|
||||
if (id_n == -1) {
|
||||
if (input.containsKey("hc")) {
|
||||
id_n = input["hc"];
|
||||
} else if (input.containsKey("wwc")) {
|
||||
id_n = input["wwc"];
|
||||
} else if (input.containsKey("id")) {
|
||||
id_n = input["id"];
|
||||
}
|
||||
}
|
||||
|
||||
// the value must always come from the input JSON. It's allowed to be empty.
|
||||
JsonVariant data;
|
||||
if (input.containsKey("data")) {
|
||||
data = input["data"];
|
||||
} else if (input.containsKey("value")) {
|
||||
data = input["value"];
|
||||
}
|
||||
|
||||
// call the command based on the type
|
||||
uint8_t cmd_return = CommandRet::ERROR;
|
||||
if (data.is<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 & json) {
|
||||
int8_t id_new = id;
|
||||
char cmd_new[30] = {'\0'};
|
||||
strlcpy(cmd_new, cmd, sizeof(cmd_new));
|
||||
uint8_t Command::call(const uint8_t device_type, const char * cmd, const char * value, bool authenticated, const int8_t id, JsonObject & output) {
|
||||
uint8_t return_code = CommandRet::OK;
|
||||
|
||||
auto cf = find_command(device_type, cmd_new, id_new);
|
||||
// see if there is a command registered
|
||||
auto cf = find_command(device_type, cmd);
|
||||
|
||||
// check if we're allowed to call it
|
||||
if (cf != nullptr) {
|
||||
// check if its a call to and end-point to a device, i.e. has no value
|
||||
// except for system commands as this is a special device without any queryable entities (device values)
|
||||
if ((device_type != EMSdevice::DeviceType::SYSTEM) && (!value || !strlen(value))) {
|
||||
if (!cf || (cf && !cf->cmdfunction_json_)) {
|
||||
return EMSESP::get_device_value_info(output, cmd, id, device_type) ? CommandRet::OK : CommandRet::ERROR; // entity = cmd
|
||||
}
|
||||
}
|
||||
|
||||
if (cf) {
|
||||
// we have a matching command
|
||||
std::string dname = EMSdevice::device_type_2_device_name(device_type);
|
||||
if ((value == nullptr) || !strlen(value)) {
|
||||
LOG_INFO(F("Calling %s command '%s'"), dname.c_str(), cmd);
|
||||
} else if (id == -1) {
|
||||
LOG_INFO(F("Calling %s command '%s', value %s, id is default"), dname.c_str(), cmd, value);
|
||||
} else {
|
||||
LOG_INFO(F("Calling %s command '%s', value %s, id is %d"), dname.c_str(), cmd, value, id);
|
||||
}
|
||||
|
||||
// check permissions
|
||||
if (cf->has_flags(CommandFlag::ADMIN_ONLY) && !authenticated) {
|
||||
LOG_WARNING(F("Command %s on %s requires valid authorization"), cmd, EMSdevice::device_type_2_device_name(device_type).c_str());
|
||||
output["message"] = "error: authentication failed";
|
||||
return CommandRet::NOT_ALLOWED; // command not allowed
|
||||
}
|
||||
}
|
||||
|
||||
std::string dname = EMSdevice::device_type_2_device_name(device_type);
|
||||
if (value == nullptr) {
|
||||
LOG_INFO(F("Calling %s command '%s'"), dname.c_str(), cmd);
|
||||
} else if (id == -1) {
|
||||
LOG_INFO(F("Calling %s command '%s', value %s, id is default"), dname.c_str(), cmd, value);
|
||||
} else {
|
||||
LOG_INFO(F("Calling %s command '%s', value %s, id is %d"), dname.c_str(), cmd, value, id_new);
|
||||
}
|
||||
|
||||
// check if json object is empty, if so quit
|
||||
if (json.isNull()) {
|
||||
LOG_WARNING(F("Ignore call for command %s in %s because it has no json body"), cmd, EMSdevice::device_type_2_device_name(device_type).c_str());
|
||||
return CommandRet::ERROR;
|
||||
}
|
||||
|
||||
// this is for endpoints that don't have commands, i.e not writable (e.g. boiler/syspress)
|
||||
if (cf == nullptr) {
|
||||
return EMSESP::get_device_value_info(json, cmd_new, id_new, device_type) ? CommandRet::OK : CommandRet::ERROR;
|
||||
}
|
||||
|
||||
if (cf->cmdfunction_json_) {
|
||||
return ((cf->cmdfunction_json_)(value, id_new, json)) ? CommandRet::OK : CommandRet::ERROR;
|
||||
} else {
|
||||
if ((device_type != EMSdevice::DeviceType::SYSTEM) && (value == nullptr || strlen(value) == 0 || strcmp(value, "?") == 0 || strcmp(value, "*") == 0)) {
|
||||
return EMSESP::get_device_value_info(json, cmd_new, id_new, device_type) ? CommandRet::OK : CommandRet::ERROR;
|
||||
// call the function
|
||||
if (cf->cmdfunction_json_) {
|
||||
return_code = ((cf->cmdfunction_json_)(value, id, output)) ? CommandRet::OK : CommandRet::ERROR;
|
||||
}
|
||||
return ((cf->cmdfunction_)(value, id_new)) ? CommandRet::OK : CommandRet::ERROR;
|
||||
}
|
||||
}
|
||||
|
||||
// strip prefixes, check, and find command
|
||||
Command::CmdFunction * Command::find_command(const uint8_t device_type, char * cmd, int8_t & id) {
|
||||
// special cases for id=0 and id=-1 will be removed in V3 API
|
||||
// no command for id0
|
||||
if (id == 0) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// empty command is info with id0
|
||||
if (cmd[0] == '\0') {
|
||||
strcpy(cmd, "info");
|
||||
id = 0;
|
||||
}
|
||||
|
||||
// convert cmd to lowercase
|
||||
for (char * p = cmd; *p; p++) {
|
||||
*p = tolower(*p);
|
||||
}
|
||||
|
||||
// hack for commands that could have hc or wwc prefixed. will be removed in new API V3 eventually
|
||||
// scan for prefix hc.
|
||||
for (uint8_t i = DeviceValueTAG::TAG_HC1; i <= DeviceValueTAG::TAG_HC4; i++) {
|
||||
const char * tag = EMSdevice::tag_to_string(i).c_str();
|
||||
uint8_t len = strlen(tag);
|
||||
if (strncmp(cmd, tag, len) == 0) {
|
||||
if (cmd[len] != '\0') {
|
||||
strcpy(cmd, &cmd[len + 1]);
|
||||
} else {
|
||||
strcpy(cmd, &cmd[len]);
|
||||
}
|
||||
id = 1 + i - DeviceValueTAG::TAG_HC1;
|
||||
break;
|
||||
if (cf->cmdfunction_) {
|
||||
return_code = ((cf->cmdfunction_)(value, id)) ? CommandRet::OK : CommandRet::ERROR;
|
||||
}
|
||||
}
|
||||
|
||||
// scan for prefix wwc.
|
||||
for (uint8_t i = DeviceValueTAG::TAG_WWC1; i <= DeviceValueTAG::TAG_WWC4; i++) {
|
||||
const char * tag = EMSdevice::tag_to_string(i).c_str();
|
||||
uint8_t len = strlen(tag);
|
||||
if (strncmp(cmd, tag, len) == 0) {
|
||||
if (cmd[len] != '\0') {
|
||||
strcpy(cmd, &cmd[len + 1]);
|
||||
} else {
|
||||
strcpy(cmd, &cmd[len]);
|
||||
}
|
||||
id = 8 + i - DeviceValueTAG::TAG_WWC1;
|
||||
break;
|
||||
// report error if call failed
|
||||
if (return_code != CommandRet::OK) {
|
||||
output.clear();
|
||||
output["message"] = "error: function failed";
|
||||
}
|
||||
|
||||
return return_code;
|
||||
}
|
||||
|
||||
// empty command after processing prefix is info
|
||||
if (cmd[0] == '\0') {
|
||||
strcpy(cmd, "info");
|
||||
}
|
||||
|
||||
return find_command(device_type, cmd);
|
||||
// we didn't find the command and its not an endpoint, report error
|
||||
char error[100];
|
||||
snprintf(error, sizeof(error), "error: invalid command %s", cmd);
|
||||
output["message"] = error;
|
||||
return CommandRet::NOT_FOUND;
|
||||
}
|
||||
|
||||
// add a command to the list, which does not return json
|
||||
@@ -178,23 +285,17 @@ void Command::add(const uint8_t device_type, const __FlashStringHelper * cmd, co
|
||||
}
|
||||
|
||||
cmdfunctions_.emplace_back(device_type, flags, cmd, cb, nullptr, description); // callback for json is nullptr
|
||||
|
||||
Mqtt::sub_command(device_type, cmd, cb, flags);
|
||||
}
|
||||
|
||||
// add a command to the list, which does return a json object as output
|
||||
// flag is fixed to MqttSubFlag::MQTT_SUB_FLAG_NOSUB so there will be no topic subscribed to this
|
||||
void Command::add_json(const uint8_t device_type,
|
||||
const __FlashStringHelper * cmd,
|
||||
const cmd_json_function_p cb,
|
||||
const __FlashStringHelper * description,
|
||||
uint8_t flags) {
|
||||
void Command::add(const uint8_t device_type, const __FlashStringHelper * cmd, const cmd_json_function_p cb, const __FlashStringHelper * description, uint8_t flags) {
|
||||
// if the command already exists for that device type don't add it
|
||||
if (find_command(device_type, uuid::read_flash_string(cmd).c_str()) != nullptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
cmdfunctions_.emplace_back(device_type, CommandFlag::MQTT_SUB_FLAG_NOSUB | flags, cmd, nullptr, cb, description); // callback for json is included
|
||||
cmdfunctions_.emplace_back(device_type, (CommandFlag::MQTT_SUB_FLAG_NOSUB | flags), cmd, nullptr, cb, description); // callback for json is included
|
||||
}
|
||||
|
||||
// see if a command exists for that device type
|
||||
@@ -221,9 +322,9 @@ Command::CmdFunction * Command::find_command(const uint8_t device_type, const ch
|
||||
}
|
||||
|
||||
// list all commands for a specific device, output as json
|
||||
bool Command::list(const uint8_t device_type, JsonObject & json) {
|
||||
bool Command::list(const uint8_t device_type, JsonObject & output) {
|
||||
if (cmdfunctions_.empty()) {
|
||||
json["message"] = "no commands available";
|
||||
output["message"] = "no commands available";
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -239,7 +340,7 @@ bool Command::list(const uint8_t device_type, JsonObject & json) {
|
||||
for (auto & cl : sorted_cmds) {
|
||||
for (const auto & cf : cmdfunctions_) {
|
||||
if ((cf.device_type_ == device_type) && !cf.has_flags(CommandFlag::HIDDEN) && cf.description_ && (cl == uuid::read_flash_string(cf.cmd_))) {
|
||||
json[cl] = cf.description_;
|
||||
output[cl] = cf.description_;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -282,11 +383,11 @@ void Command::show(uuid::console::Shell & shell, uint8_t device_type, bool verbo
|
||||
uint8_t i = cl.length();
|
||||
shell.print(" ");
|
||||
if (cf.has_flags(MQTT_SUB_FLAG_HC)) {
|
||||
shell.print("[hc] ");
|
||||
i += 5;
|
||||
shell.print("[hc<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
|
||||
|
||||
@@ -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,30 +102,37 @@ class Command {
|
||||
return cmdfunctions_;
|
||||
}
|
||||
|
||||
static uint8_t call(const uint8_t device_type, const char * cmd, const char * value, bool authenticated, const int8_t id, JsonObject & json);
|
||||
static uint8_t call(const uint8_t device_type, const char * cmd, const char * value, bool authenticated, const int8_t id = -1);
|
||||
#define add_
|
||||
|
||||
static uint8_t call(const uint8_t device_type, const char * cmd, const char * value, bool authenticated, const int8_t id, JsonObject & output);
|
||||
static uint8_t call(const uint8_t device_type, const char * cmd, const char * value);
|
||||
|
||||
// with normal call back function taking a value and id
|
||||
static void add(const uint8_t device_type,
|
||||
const __FlashStringHelper * cmd,
|
||||
const cmd_function_p cb,
|
||||
const __FlashStringHelper * description,
|
||||
uint8_t flags = CommandFlag::MQTT_SUB_FLAG_DEFAULT);
|
||||
|
||||
static void add_json(const uint8_t device_type,
|
||||
const __FlashStringHelper * cmd,
|
||||
const cmd_json_function_p cb,
|
||||
const __FlashStringHelper * description,
|
||||
uint8_t flags = CommandFlag::MQTT_SUB_FLAG_DEFAULT);
|
||||
// callback function taking value, id and a json object for its output
|
||||
static void add(const uint8_t device_type,
|
||||
const __FlashStringHelper * cmd,
|
||||
const cmd_json_function_p cb,
|
||||
const __FlashStringHelper * description,
|
||||
uint8_t flags = CommandFlag::MQTT_SUB_FLAG_DEFAULT);
|
||||
|
||||
static void show_all(uuid::console::Shell & shell);
|
||||
static Command::CmdFunction * find_command(const uint8_t device_type, const char * cmd);
|
||||
static Command::CmdFunction * find_command(const uint8_t device_type, char * cmd, int8_t & id);
|
||||
|
||||
static void show(uuid::console::Shell & shell, uint8_t device_type, bool verbose);
|
||||
static void show_devices(uuid::console::Shell & shell);
|
||||
static bool device_has_commands(const uint8_t device_type);
|
||||
|
||||
static bool list(const uint8_t device_type, JsonObject & json);
|
||||
static bool list(const uint8_t device_type, JsonObject & output);
|
||||
|
||||
static uint8_t process(const char * path, const bool authenticated, const JsonObject & input, JsonObject & output);
|
||||
|
||||
static const char * parse_command_string(const char * command, int8_t & id);
|
||||
|
||||
private:
|
||||
static uuid::log::Logger logger_;
|
||||
@@ -132,6 +140,31 @@ class Command {
|
||||
static std::vector<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
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -222,6 +222,10 @@ const std::string EMSdevice::device_type_2_device_name(const uint8_t device_type
|
||||
|
||||
// returns device_type from a string
|
||||
uint8_t EMSdevice::device_name_2_device_type(const char * topic) {
|
||||
if (!topic) {
|
||||
return DeviceType::UNKNOWN; // nullptr
|
||||
}
|
||||
|
||||
// convert topic to lowercase and compare
|
||||
char lowtopic[20];
|
||||
strlcpy(lowtopic, topic, sizeof(lowtopic));
|
||||
@@ -368,7 +372,7 @@ bool EMSdevice::is_fetch(uint16_t telegram_id) {
|
||||
}
|
||||
|
||||
// list of registered device entries, adding the HA entity if it exists
|
||||
void EMSdevice::list_device_entries(JsonObject & json) {
|
||||
void EMSdevice::list_device_entries(JsonObject & output) {
|
||||
for (const auto & dv : devicevalues_) {
|
||||
if (dv_is_visible(dv) && dv.type != DeviceValueType::CMD) {
|
||||
// if we have a tag prefix it
|
||||
@@ -379,7 +383,7 @@ void EMSdevice::list_device_entries(JsonObject & json) {
|
||||
snprintf(key, 50, "%s", uuid::read_flash_string(dv.short_name).c_str());
|
||||
}
|
||||
|
||||
JsonArray details = json.createNestedArray(key);
|
||||
JsonArray details = output.createNestedArray(key);
|
||||
|
||||
// add the full name description
|
||||
details.add(dv.full_name);
|
||||
@@ -455,24 +459,23 @@ void EMSdevice::show_mqtt_handlers(uuid::console::Shell & shell) {
|
||||
Mqtt::show_topic_handlers(shell, device_type_);
|
||||
}
|
||||
|
||||
void EMSdevice::register_mqtt_topic(const std::string & topic, const mqtt_sub_function_p f) {
|
||||
Mqtt::subscribe(device_type_, topic, f);
|
||||
}
|
||||
|
||||
// register a callback function for a specific telegram type
|
||||
void EMSdevice::register_telegram_type(const uint16_t telegram_type_id, const __FlashStringHelper * telegram_type_name, bool fetch, const process_function_p f) {
|
||||
telegram_functions_.emplace_back(telegram_type_id, telegram_type_name, fetch, f);
|
||||
}
|
||||
|
||||
// add to device value library
|
||||
// add to device value library, also know now as a "device entity"
|
||||
// arguments are:
|
||||
// tag: to be used to group mqtt together, either as separate topics as a nested object
|
||||
// value: pointer to the value from the .h file
|
||||
// value_p: pointer to the value from the .h file
|
||||
// type: one of DeviceValueType
|
||||
// options: options for enum or a divider for int (e.g. F("10"))
|
||||
// short_name: used in Mqtt as keys
|
||||
// full_name: used in Web and Console unless empty (nullptr)
|
||||
// uom: unit of measure from DeviceValueUOM
|
||||
// has_cmd: true if this is an associated command
|
||||
// min: min allowed value
|
||||
// max: max allowed value
|
||||
void EMSdevice::register_device_value(uint8_t tag,
|
||||
void * value_p,
|
||||
uint8_t type,
|
||||
@@ -483,7 +486,7 @@ void EMSdevice::register_device_value(uint8_t tag,
|
||||
bool has_cmd,
|
||||
int32_t min,
|
||||
uint32_t max) {
|
||||
// init the value depending on it's type
|
||||
// initialize the device value depending on it's type
|
||||
if (type == DeviceValueType::STRING) {
|
||||
*(char *)(value_p) = {'\0'};
|
||||
} else if (type == DeviceValueType::INT) {
|
||||
@@ -523,25 +526,32 @@ void EMSdevice::register_device_value(uint8_t tag,
|
||||
const cmd_function_p f,
|
||||
int32_t min,
|
||||
uint32_t max) {
|
||||
register_device_value(tag, value_p, type, options, name[0], name[1], uom, (f != nullptr), min, max);
|
||||
auto short_name = name[0];
|
||||
auto full_name = name[1];
|
||||
|
||||
register_device_value(tag, value_p, type, options, short_name, full_name, uom, (f != nullptr), min, max);
|
||||
|
||||
// add a new command if it has a function attached
|
||||
if (f == nullptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
uint8_t flags = CommandFlag::ADMIN_ONLY; // executing commands require admin privileges
|
||||
|
||||
if (tag >= TAG_HC1 && tag <= TAG_HC4) {
|
||||
Command::add(device_type_, name[0], f, name[1], CommandFlag::MQTT_SUB_FLAG_HC | CommandFlag::ADMIN_ONLY);
|
||||
flags |= CommandFlag::MQTT_SUB_FLAG_HC;
|
||||
} else if (tag >= TAG_WWC1 && tag <= TAG_WWC4) {
|
||||
Command::add(device_type_, name[0], f, name[1], CommandFlag::MQTT_SUB_FLAG_WWC | CommandFlag::ADMIN_ONLY);
|
||||
flags |= CommandFlag::MQTT_SUB_FLAG_WWC;
|
||||
} else if (tag == TAG_DEVICE_DATA_WW) {
|
||||
Command::add(device_type_, name[0], f, name[1], CommandFlag::MQTT_SUB_FLAG_WW | CommandFlag::ADMIN_ONLY);
|
||||
} else {
|
||||
Command::add(device_type_, name[0], f, name[1], CommandFlag::ADMIN_ONLY);
|
||||
flags |= CommandFlag::MQTT_SUB_FLAG_WW;
|
||||
}
|
||||
|
||||
// add the command to our library
|
||||
// cmd is the short_name and the description is the full_name
|
||||
Command::add(device_type_, short_name, f, full_name, flags);
|
||||
}
|
||||
|
||||
// function with no min and max values
|
||||
// function with no min and max values (set to 0)
|
||||
void EMSdevice::register_device_value(uint8_t tag,
|
||||
void * value_p,
|
||||
uint8_t type,
|
||||
@@ -552,7 +562,7 @@ void EMSdevice::register_device_value(uint8_t tag,
|
||||
register_device_value(tag, value_p, type, options, name, uom, f, 0, 0);
|
||||
}
|
||||
|
||||
// no command function
|
||||
// no associated command function, or min/max values
|
||||
void EMSdevice::register_device_value(uint8_t tag,
|
||||
void * value_p,
|
||||
uint8_t type,
|
||||
@@ -592,9 +602,9 @@ const std::string EMSdevice::get_value_uom(const char * key) {
|
||||
|
||||
// prepare array of device values used for the WebUI
|
||||
// v = value, u=uom, n=name, c=cmd
|
||||
void EMSdevice::generate_values_json_web(JsonObject & json) {
|
||||
json["name"] = to_string_short();
|
||||
JsonArray data = json.createNestedArray("data");
|
||||
void EMSdevice::generate_values_json_web(JsonObject & output) {
|
||||
output["name"] = to_string_short();
|
||||
JsonArray data = output.createNestedArray("data");
|
||||
|
||||
for (const auto & dv : devicevalues_) {
|
||||
// ignore if full_name empty and also commands
|
||||
@@ -707,10 +717,11 @@ void EMSdevice::generate_values_json_web(JsonObject & json) {
|
||||
}
|
||||
}
|
||||
|
||||
// builds json with specific device value information
|
||||
// e.g. http://ems-esp/api?device=thermostat&cmd=seltemp
|
||||
bool EMSdevice::get_value_info(JsonObject & root, const char * cmd, const int8_t id) {
|
||||
JsonObject json = root;
|
||||
// builds json with specific single device value information
|
||||
// cnd is the endpoint or name of the device entity
|
||||
// returns false if failed, otherwise true
|
||||
bool EMSdevice::get_value_info(JsonObject & output, const char * cmd, const int8_t id) {
|
||||
JsonObject json = output;
|
||||
int8_t tag = id;
|
||||
|
||||
// check if we have hc or wwc
|
||||
@@ -719,7 +730,7 @@ bool EMSdevice::get_value_info(JsonObject & root, const char * cmd, const int8_t
|
||||
} else if (id >= 8 && id <= 11) {
|
||||
tag = DeviceValueTAG::TAG_WWC1 + id - 8;
|
||||
} else if (id != -1) {
|
||||
return false;
|
||||
return false; // error
|
||||
}
|
||||
|
||||
// search device value with this tag
|
||||
@@ -894,16 +905,18 @@ bool EMSdevice::get_value_info(JsonObject & root, const char * cmd, const int8_t
|
||||
}
|
||||
}
|
||||
|
||||
emsesp::EMSESP::logger().err(F("Can't get values for entity '%s'"), cmd);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// For each value in the device create the json object pair and add it to given json
|
||||
// return false if empty
|
||||
// this is used to create both the MQTT payloads, Console messages and Web API calls
|
||||
bool EMSdevice::generate_values_json(JsonObject & root, const uint8_t tag_filter, const bool nested, const uint8_t output_target) {
|
||||
bool EMSdevice::generate_values_json(JsonObject & output, const uint8_t tag_filter, const bool nested, const uint8_t output_target) {
|
||||
bool has_values = false; // to see if we've added a value. it's faster than doing a json.size() at the end
|
||||
uint8_t old_tag = 255; // NAN
|
||||
JsonObject json = root;
|
||||
JsonObject json = output;
|
||||
|
||||
for (auto & dv : devicevalues_) {
|
||||
// conditions
|
||||
@@ -935,7 +948,7 @@ bool EMSdevice::generate_values_json(JsonObject & root, const uint8_t tag_filter
|
||||
if (dv.tag != old_tag) {
|
||||
old_tag = dv.tag;
|
||||
if (nested && have_tag && dv.tag >= DeviceValueTAG::TAG_HC1) { // no nests for boiler tags
|
||||
json = root.createNestedObject(tag_to_string(dv.tag));
|
||||
json = output.createNestedObject(tag_to_string(dv.tag));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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());
|
||||
publish_response(telegram);
|
||||
if (Mqtt::send_response()) {
|
||||
publish_response(telegram);
|
||||
}
|
||||
|
||||
if (!read_next_) {
|
||||
read_id_ = WATCH_ID_NONE;
|
||||
}
|
||||
@@ -989,40 +989,47 @@ bool EMSESP::add_device(const uint8_t device_id, const uint8_t product_id, std::
|
||||
return true;
|
||||
}
|
||||
|
||||
Command::add_json(
|
||||
Command::add(
|
||||
device_type,
|
||||
F_(info),
|
||||
[device_type](const char * value, const int8_t id, JsonObject & json) {
|
||||
return command_info(device_type, json, id, EMSdevice::OUTPUT_TARGET::API_VERBOSE);
|
||||
[device_type](const char * value, const int8_t id, JsonObject & output) {
|
||||
return command_info(device_type, output, id, EMSdevice::OUTPUT_TARGET::API_VERBOSE);
|
||||
},
|
||||
F_(info_cmd));
|
||||
Command::add_json(
|
||||
Command::add(
|
||||
device_type,
|
||||
F("info_short"),
|
||||
[device_type](const char * value, const int8_t id, JsonObject & json) { return command_info(device_type, json, id, EMSdevice::OUTPUT_TARGET::API); },
|
||||
F("list"),
|
||||
[device_type](const char * value, const int8_t id, JsonObject & output) { return command_info(device_type, output, id, EMSdevice::OUTPUT_TARGET::API); },
|
||||
nullptr,
|
||||
CommandFlag::HIDDEN); // this command is hidden
|
||||
Command::add_json(
|
||||
Command::add(
|
||||
device_type,
|
||||
F_(commands),
|
||||
[device_type](const char * value, const int8_t id, JsonObject & json) { return command_commands(device_type, json, id); },
|
||||
[device_type](const char * value, const int8_t id, JsonObject & output) { return command_commands(device_type, output, id); },
|
||||
F_(commands_cmd));
|
||||
Command::add_json(
|
||||
Command::add(
|
||||
device_type,
|
||||
F_(entities),
|
||||
[device_type](const char * value, const int8_t id, JsonObject & json) { return command_entities(device_type, json, id); },
|
||||
[device_type](const char * value, const int8_t id, JsonObject & output) { return command_entities(device_type, output, id); },
|
||||
F_(entities_cmd));
|
||||
|
||||
// MQTT subscribe to the device top-level, e.g. "ems-esp/boiler/#"
|
||||
std::string topic = EMSdevice::device_type_2_device_name(device_type) + "/#";
|
||||
Mqtt::subscribe(device_type, topic, nullptr); // use empty function callback
|
||||
|
||||
// Print to LOG showing we've added a new device
|
||||
LOG_INFO(F("Recognized new %s with device ID 0x%02X"), EMSdevice::device_type_2_device_name(device_type).c_str(), device_id);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// list device entities
|
||||
bool EMSESP::command_entities(uint8_t device_type, JsonObject & json, const int8_t id) {
|
||||
bool EMSESP::command_entities(uint8_t device_type, JsonObject & output, const int8_t id) {
|
||||
JsonObject node;
|
||||
|
||||
for (const auto & emsdevice : emsdevices) {
|
||||
if ((emsdevice) && (emsdevice->device_type() == device_type)) {
|
||||
emsdevice->list_device_entries(json);
|
||||
emsdevice->list_device_entries(output);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -1031,14 +1038,14 @@ bool EMSESP::command_entities(uint8_t device_type, JsonObject & json, const int8
|
||||
}
|
||||
|
||||
// list all available commands, return as json
|
||||
bool EMSESP::command_commands(uint8_t device_type, JsonObject & json, const int8_t id) {
|
||||
return Command::list(device_type, json);
|
||||
bool EMSESP::command_commands(uint8_t device_type, JsonObject & output, const int8_t id) {
|
||||
return Command::list(device_type, output);
|
||||
}
|
||||
|
||||
// export all values to info command
|
||||
// export all values
|
||||
// value is ignored here
|
||||
// info command always shows in verbose mode, so full names are displayed
|
||||
bool EMSESP::command_info(uint8_t device_type, JsonObject & json, const int8_t id, const uint8_t output_target) {
|
||||
bool EMSESP::command_info(uint8_t device_type, JsonObject & output, const int8_t id, const uint8_t output_target) {
|
||||
bool has_value = false;
|
||||
uint8_t tag;
|
||||
if (id >= 1 && id <= 4) {
|
||||
@@ -1051,13 +1058,10 @@ bool EMSESP::command_info(uint8_t device_type, JsonObject & json, const int8_t i
|
||||
return false;
|
||||
}
|
||||
|
||||
// if id=-1 it means we have no endpoint so default to API
|
||||
uint8_t target = (id == -1) ? EMSdevice::OUTPUT_TARGET::API_VERBOSE : EMSdevice::OUTPUT_TARGET::API;
|
||||
|
||||
for (const auto & emsdevice : emsdevices) {
|
||||
if (emsdevice && (emsdevice->device_type() == device_type)
|
||||
&& ((device_type != DeviceType::THERMOSTAT) || (emsdevice->device_id() == EMSESP::actual_master_thermostat()))) {
|
||||
has_value |= emsdevice->generate_values_json(json, tag, (id < 1), target); // nested for id -1 and 0
|
||||
has_value |= emsdevice->generate_values_json(output, tag, (id < 1), output_target); // nested for id -1 and 0
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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_;
|
||||
|
||||
@@ -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"))
|
||||
|
||||
287
src/mqtt.cpp
287
src/mqtt.cpp
@@ -38,7 +38,7 @@ bool Mqtt::mqtt_enabled_;
|
||||
uint8_t Mqtt::ha_climate_format_;
|
||||
bool Mqtt::ha_enabled_;
|
||||
uint8_t Mqtt::nested_format_;
|
||||
uint8_t Mqtt::subscribe_format_;
|
||||
bool Mqtt::send_response_;
|
||||
|
||||
std::deque<Mqtt::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
|
||||
}
|
||||
cmd_only++; // skip the /
|
||||
// LOG_INFO(F("devicetype= %d, topic = %s, cmd = %s, message = %s), mf.device_type_, topic, cmd_only, message);
|
||||
// call command, assume admin authentication is allowed
|
||||
uint8_t cmd_return = Command::call(mf.device_type_, cmd_only, message, true);
|
||||
if (cmd_return == CommandRet::NOT_FOUND) {
|
||||
LOG_ERROR(F("No matching cmd (%s) in topic %s"), cmd_only, topic);
|
||||
Mqtt::publish(F_(response), "unknown");
|
||||
} else if (cmd_return != CommandRet::OK) {
|
||||
LOG_ERROR(F("Invalid data with cmd (%s) in topic %s"), cmd_only, topic);
|
||||
Mqtt::publish(F_(response), "unknown");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// It's a command then with the payload being JSON like {"cmd":"<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;
|
||||
}
|
||||
|
||||
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);
|
||||
// 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
|
||||
}
|
||||
}
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
if (return_code != CommandRet::OK) {
|
||||
Mqtt::publish(F_(response), (const char *)output["message"]);
|
||||
} else {
|
||||
Mqtt::publish(F_(response), output); // output response from call
|
||||
}
|
||||
}
|
||||
|
||||
// print all the topics related to a specific device type
|
||||
@@ -438,7 +295,7 @@ void Mqtt::show_topic_handlers(uuid::console::Shell & shell, const uint8_t devic
|
||||
shell.print(F(" Subscribed MQTT topics: "));
|
||||
for (const auto & mqtt_subfunction : mqtt_subfunctions_) {
|
||||
if (mqtt_subfunction.device_type_ == device_type) {
|
||||
shell.printf(F("%s/%s "), mqtt_base_.c_str(), mqtt_subfunction.topic_.c_str());
|
||||
shell.printf(F("%s "), mqtt_subfunction.topic_.c_str());
|
||||
}
|
||||
}
|
||||
shell.println();
|
||||
@@ -499,7 +356,7 @@ void Mqtt::load_settings() {
|
||||
ha_enabled_ = mqttSettings.ha_enabled;
|
||||
ha_climate_format_ = mqttSettings.ha_climate_format;
|
||||
nested_format_ = mqttSettings.nested_format;
|
||||
subscribe_format_ = mqttSettings.subscribe_format;
|
||||
send_response_ = mqttSettings.send_response;
|
||||
|
||||
// convert to milliseconds
|
||||
publish_time_boiler_ = mqttSettings.publish_time_boiler * 1000;
|
||||
@@ -564,17 +421,12 @@ void Mqtt::start() {
|
||||
mqttClient_->setWill(will_topic, 1, true, "offline"); // with qos 1, retain true
|
||||
|
||||
mqttClient_->onMessage([this](char * topic, char * payload, AsyncMqttClientMessageProperties properties, size_t len, size_t index, size_t total) {
|
||||
// receiving mqtt
|
||||
on_message(topic, payload, len);
|
||||
on_message(topic, payload, len); // receiving mqtt
|
||||
});
|
||||
|
||||
mqttClient_->onPublish([this](uint16_t packetId) {
|
||||
// publish
|
||||
on_publish(packetId);
|
||||
on_publish(packetId); // publish
|
||||
});
|
||||
|
||||
// register command
|
||||
Command::add(EMSdevice::DeviceType::SYSTEM, F_(publish), System::command_publish, F("forces a MQTT publish"), CommandFlag::ADMIN_ONLY);
|
||||
}
|
||||
|
||||
void Mqtt::set_publish_time_boiler(uint16_t publish_time) {
|
||||
@@ -672,8 +524,9 @@ void Mqtt::on_connect() {
|
||||
EMSESP::shower_.send_mqtt_stat(false); // Send shower_activated as false
|
||||
EMSESP::system_.send_heartbeat(); // send heatbeat
|
||||
|
||||
// re-subscribe to all MQTT topics
|
||||
// re-subscribe to all custom registered MQTT topics
|
||||
resubscribe();
|
||||
|
||||
EMSESP::reset_mqtt_ha(); // re-create all HA devices if there are any
|
||||
|
||||
publish_retain(F("status"), "online", true); // say we're alive to the Last Will topic, with retain on
|
||||
@@ -740,22 +593,27 @@ void Mqtt::ha_status() {
|
||||
}
|
||||
|
||||
// add sub or pub task to the queue.
|
||||
// a fully-qualified topic is created by prefixing the base, unless it's HA
|
||||
// returns a pointer to the message created
|
||||
// the base is not included in the topic
|
||||
std::shared_ptr<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
|
||||
|
||||
43
src/mqtt.h
43
src/mqtt.h
@@ -42,7 +42,7 @@ using uuid::console::Shell;
|
||||
#define MQTT_HA_PUBLISH_DELAY 50
|
||||
|
||||
// size of queue
|
||||
#define MAX_MQTT_MESSAGES 200
|
||||
#define MAX_MQTT_MESSAGES 300
|
||||
|
||||
namespace emsesp {
|
||||
|
||||
@@ -88,14 +88,6 @@ class Mqtt {
|
||||
|
||||
};
|
||||
|
||||
// subscribe_format
|
||||
enum Subscribe_Format : uint8_t {
|
||||
GENERAL = 0, // 0
|
||||
INDIVIDUAL_MAIN_HC, // 1
|
||||
INDIVIDUAL_ALL_HC // 2
|
||||
|
||||
};
|
||||
|
||||
// for Home Assistant
|
||||
enum class State_class { NONE, MEASUREMENT, TOTAL_INCREASING };
|
||||
enum class Device_class { NONE, TEMPERATURE, POWER_FACTOR, ENERGY, PRESSURE, POWER, SIGNAL_STRENGTH };
|
||||
@@ -105,7 +97,6 @@ class Mqtt {
|
||||
static void on_connect();
|
||||
|
||||
static void subscribe(const uint8_t device_type, const std::string & topic, mqtt_sub_function_p cb);
|
||||
static void subscribe(const std::string & topic, mqtt_sub_function_p cb);
|
||||
static void resubscribe();
|
||||
|
||||
static void publish(const std::string & topic, const std::string & payload);
|
||||
@@ -127,7 +118,6 @@ class Mqtt {
|
||||
const __FlashStringHelper * entity,
|
||||
const uint8_t uom,
|
||||
const bool has_cmd = false);
|
||||
static void sub_command(const uint8_t device_type, const __FlashStringHelper * cmd, cmdfunction_p cb, uint8_t flags = 0);
|
||||
|
||||
static void show_topic_handlers(uuid::console::Shell & shell, const uint8_t device_type);
|
||||
static void show_mqtt(uuid::console::Shell & shell);
|
||||
@@ -138,7 +128,9 @@ class Mqtt {
|
||||
mqttClient_->disconnect();
|
||||
}
|
||||
|
||||
void incoming(const char * topic, const char * payload); // for testing only
|
||||
#if defined(EMSESP_DEBUG)
|
||||
void incoming(const char * topic, const char * payload = ""); // for testing only
|
||||
#endif
|
||||
|
||||
static bool connected() {
|
||||
#if defined(EMSESP_STANDALONE)
|
||||
@@ -182,30 +174,31 @@ class Mqtt {
|
||||
static uint8_t nested_format() {
|
||||
return nested_format_;
|
||||
}
|
||||
|
||||
static void nested_format(uint8_t nested_format) {
|
||||
nested_format_ = nested_format;
|
||||
}
|
||||
|
||||
// subscribe_format is 0 for General topics, 1 for individual with main heating circuit or 2 for individual topics with all heating circuits
|
||||
static uint8_t subscribe_format() {
|
||||
return subscribe_format_;
|
||||
}
|
||||
static void subscribe_format(uint8_t subscribe_format) {
|
||||
subscribe_format_ = subscribe_format;
|
||||
}
|
||||
|
||||
static bool ha_enabled() {
|
||||
return ha_enabled_;
|
||||
}
|
||||
|
||||
static void ha_climate_format(uint8_t ha_climate_format) {
|
||||
ha_climate_format_ = ha_climate_format;
|
||||
}
|
||||
|
||||
static bool ha_enabled() {
|
||||
return ha_enabled_;
|
||||
}
|
||||
|
||||
static void ha_enabled(bool ha_enabled) {
|
||||
ha_enabled_ = ha_enabled;
|
||||
}
|
||||
|
||||
static bool send_response() {
|
||||
return send_response_;
|
||||
}
|
||||
|
||||
static void send_response(bool send_response) {
|
||||
send_response_ = send_response;
|
||||
}
|
||||
|
||||
void set_qos(uint8_t mqtt_qos) {
|
||||
mqtt_qos_ = mqtt_qos;
|
||||
}
|
||||
@@ -297,7 +290,7 @@ class Mqtt {
|
||||
static uint8_t ha_climate_format_;
|
||||
static bool ha_enabled_;
|
||||
static uint8_t nested_format_;
|
||||
static uint8_t subscribe_format_;
|
||||
static bool send_response_;
|
||||
};
|
||||
|
||||
} // namespace emsesp
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -496,44 +496,44 @@ void System::show_mem(const char * note) {
|
||||
}
|
||||
|
||||
// create the json for heartbeat
|
||||
bool System::heartbeat_json(JsonObject & doc) {
|
||||
bool System::heartbeat_json(JsonObject & output) {
|
||||
uint8_t ems_status = EMSESP::bus_status();
|
||||
if (ems_status == EMSESP::BUS_STATUS_TX_ERRORS) {
|
||||
doc["status"] = FJSON("txerror");
|
||||
output["status"] = FJSON("txerror");
|
||||
} else if (ems_status == EMSESP::BUS_STATUS_CONNECTED) {
|
||||
doc["status"] = FJSON("connected");
|
||||
output["status"] = FJSON("connected");
|
||||
} else {
|
||||
doc["status"] = FJSON("disconnected");
|
||||
output["status"] = FJSON("disconnected");
|
||||
}
|
||||
|
||||
doc["uptime"] = uuid::log::format_timestamp_ms(uuid::get_uptime_ms(), 3);
|
||||
output["uptime"] = uuid::log::format_timestamp_ms(uuid::get_uptime_ms(), 3);
|
||||
|
||||
doc["uptime_sec"] = uuid::get_uptime_sec();
|
||||
doc["rxreceived"] = EMSESP::rxservice_.telegram_count();
|
||||
doc["rxfails"] = EMSESP::rxservice_.telegram_error_count();
|
||||
doc["txreads"] = EMSESP::txservice_.telegram_read_count();
|
||||
doc["txwrites"] = EMSESP::txservice_.telegram_write_count();
|
||||
doc["txfails"] = EMSESP::txservice_.telegram_fail_count();
|
||||
output["uptime_sec"] = uuid::get_uptime_sec();
|
||||
output["rxreceived"] = EMSESP::rxservice_.telegram_count();
|
||||
output["rxfails"] = EMSESP::rxservice_.telegram_error_count();
|
||||
output["txreads"] = EMSESP::txservice_.telegram_read_count();
|
||||
output["txwrites"] = EMSESP::txservice_.telegram_write_count();
|
||||
output["txfails"] = EMSESP::txservice_.telegram_fail_count();
|
||||
if (Mqtt::enabled()) {
|
||||
doc["mqttfails"] = Mqtt::publish_fails();
|
||||
output["mqttfails"] = Mqtt::publish_fails();
|
||||
}
|
||||
if (EMSESP::dallas_enabled()) {
|
||||
doc["dallasfails"] = EMSESP::sensor_fails();
|
||||
output["dallasfails"] = EMSESP::sensor_fails();
|
||||
}
|
||||
|
||||
#ifndef EMSESP_STANDALONE
|
||||
doc["freemem"] = ESP.getFreeHeap() / 1000L; // kilobytes
|
||||
output["freemem"] = ESP.getFreeHeap() / 1000L; // kilobytes
|
||||
#endif
|
||||
|
||||
if (analog_enabled_) {
|
||||
doc["adc"] = analog_;
|
||||
output["adc"] = analog_;
|
||||
}
|
||||
|
||||
#ifndef EMSESP_STANDALONE
|
||||
if (!ethernet_connected_) {
|
||||
int8_t rssi = WiFi.RSSI();
|
||||
doc["rssi"] = rssi;
|
||||
doc["wifistrength"] = wifi_quality(rssi);
|
||||
int8_t rssi = WiFi.RSSI();
|
||||
output["rssi"] = rssi;
|
||||
output["wifistrength"] = wifi_quality(rssi);
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -684,16 +684,23 @@ void System::commands_init() {
|
||||
Command::add(EMSdevice::DeviceType::SYSTEM, F_(send), System::command_send, F("send a telegram"), CommandFlag::ADMIN_ONLY);
|
||||
Command::add(EMSdevice::DeviceType::SYSTEM, F_(fetch), System::command_fetch, F("refresh all EMS values"), CommandFlag::ADMIN_ONLY);
|
||||
Command::add(EMSdevice::DeviceType::SYSTEM, F_(restart), System::command_restart, F("restarts EMS-ESP"), CommandFlag::ADMIN_ONLY);
|
||||
Command::add(EMSdevice::DeviceType::SYSTEM, F_(watch), System::command_watch, F("watch incoming telegrams"), CommandFlag::ADMIN_ONLY);
|
||||
Command::add(EMSdevice::DeviceType::SYSTEM, F_(watch), System::command_watch, F("watch incoming telegrams"));
|
||||
|
||||
if (Mqtt::enabled()) {
|
||||
Command::add(EMSdevice::DeviceType::SYSTEM, F_(publish), System::command_publish, F("forces a MQTT publish"));
|
||||
}
|
||||
|
||||
// these commands will return data in JSON format
|
||||
Command::add_json(EMSdevice::DeviceType::SYSTEM, F_(info), System::command_info, F("system status"));
|
||||
Command::add_json(EMSdevice::DeviceType::SYSTEM, F_(settings), System::command_settings, F("list system settings"));
|
||||
Command::add_json(EMSdevice::DeviceType::SYSTEM, F_(commands), System::command_commands, F("list system commands"));
|
||||
Command::add(EMSdevice::DeviceType::SYSTEM, F_(info), System::command_info, F("system status"));
|
||||
Command::add(EMSdevice::DeviceType::SYSTEM, F_(settings), System::command_settings, F("list system settings"));
|
||||
Command::add(EMSdevice::DeviceType::SYSTEM, F_(commands), System::command_commands, F("list system commands"));
|
||||
|
||||
#if defined(EMSESP_DEBUG)
|
||||
Command::add(EMSdevice::DeviceType::SYSTEM, F("test"), System::command_test, F("run tests"));
|
||||
#endif
|
||||
|
||||
// MQTT subscribe "ems-esp/system/#"
|
||||
Mqtt::subscribe(EMSdevice::DeviceType::SYSTEM, "system/#", nullptr); // use empty function callback
|
||||
}
|
||||
|
||||
// flashes the LED
|
||||
@@ -844,22 +851,22 @@ bool System::check_upgrade() {
|
||||
}
|
||||
|
||||
// list commands
|
||||
bool System::command_commands(const char * value, const int8_t id, JsonObject & json) {
|
||||
return Command::list(EMSdevice::DeviceType::SYSTEM, json);
|
||||
bool System::command_commands(const char * value, const int8_t id, JsonObject & output) {
|
||||
return Command::list(EMSdevice::DeviceType::SYSTEM, output);
|
||||
}
|
||||
|
||||
// export all settings to JSON text
|
||||
// http://ems-esp/api/system/settings
|
||||
// value and id are ignored
|
||||
bool System::command_settings(const char * value, const int8_t id, JsonObject & json) {
|
||||
bool System::command_settings(const char * value, const int8_t id, JsonObject & output) {
|
||||
JsonObject node;
|
||||
|
||||
node = json.createNestedObject("System");
|
||||
node = output.createNestedObject("System");
|
||||
node["version"] = EMSESP_APP_VERSION;
|
||||
|
||||
// hide ssid from this list
|
||||
EMSESP::esp8266React.getNetworkSettingsService()->read([&](NetworkSettings & settings) {
|
||||
node = json.createNestedObject("Network");
|
||||
node = output.createNestedObject("Network");
|
||||
node["hostname"] = settings.hostname;
|
||||
node["static_ip_config"] = settings.staticIPConfig;
|
||||
node["enableIPv6"] = settings.enableIPv6;
|
||||
@@ -872,7 +879,7 @@ bool System::command_settings(const char * value, const int8_t id, JsonObject &
|
||||
|
||||
#ifndef EMSESP_STANDALONE
|
||||
EMSESP::esp8266React.getAPSettingsService()->read([&](APSettings & settings) {
|
||||
node = json.createNestedObject("AP");
|
||||
node = output.createNestedObject("AP");
|
||||
node["provision_mode"] = settings.provisionMode;
|
||||
node["ssid"] = settings.ssid;
|
||||
node["local_ip"] = settings.localIP.toString();
|
||||
@@ -882,7 +889,7 @@ bool System::command_settings(const char * value, const int8_t id, JsonObject &
|
||||
#endif
|
||||
|
||||
EMSESP::esp8266React.getMqttSettingsService()->read([&](MqttSettings & settings) {
|
||||
node = json.createNestedObject("MQTT");
|
||||
node = output.createNestedObject("MQTT");
|
||||
node["enabled"] = settings.enabled;
|
||||
#ifndef EMSESP_STANDALONE
|
||||
node["host"] = settings.host;
|
||||
@@ -902,12 +909,12 @@ bool System::command_settings(const char * value, const int8_t id, JsonObject &
|
||||
node["ha_enabled"] = settings.ha_enabled;
|
||||
node["mqtt_qos"] = settings.mqtt_qos;
|
||||
node["mqtt_retain"] = settings.mqtt_retain;
|
||||
node["subscribe_format"] = settings.subscribe_format;
|
||||
node["send_response"] = settings.send_response;
|
||||
});
|
||||
|
||||
#ifndef EMSESP_STANDALONE
|
||||
EMSESP::esp8266React.getNTPSettingsService()->read([&](NTPSettings & settings) {
|
||||
node = json.createNestedObject("NTP");
|
||||
node = output.createNestedObject("NTP");
|
||||
node["enabled"] = settings.enabled;
|
||||
node["server"] = settings.server;
|
||||
node["tz_label"] = settings.tzLabel;
|
||||
@@ -915,14 +922,14 @@ bool System::command_settings(const char * value, const int8_t id, JsonObject &
|
||||
});
|
||||
|
||||
EMSESP::esp8266React.getOTASettingsService()->read([&](OTASettings & settings) {
|
||||
node = json.createNestedObject("OTA");
|
||||
node = output.createNestedObject("OTA");
|
||||
node["enabled"] = settings.enabled;
|
||||
node["port"] = settings.port;
|
||||
});
|
||||
#endif
|
||||
|
||||
EMSESP::webSettingsService.read([&](WebSettings & settings) {
|
||||
node = json.createNestedObject("Settings");
|
||||
node = output.createNestedObject("Settings");
|
||||
node["tx_mode"] = settings.tx_mode;
|
||||
node["ems_bus_id"] = settings.ems_bus_id;
|
||||
node["syslog_enabled"] = settings.syslog_enabled;
|
||||
@@ -953,10 +960,10 @@ bool System::command_settings(const char * value, const int8_t id, JsonObject &
|
||||
|
||||
// export status information including the device information
|
||||
// http://ems-esp/api/system/info
|
||||
bool System::command_info(const char * value, const int8_t id, JsonObject & json) {
|
||||
bool System::command_info(const char * value, const int8_t id, JsonObject & output) {
|
||||
JsonObject node;
|
||||
|
||||
node = json.createNestedObject("System");
|
||||
node = output.createNestedObject("System");
|
||||
|
||||
node["version"] = EMSESP_APP_VERSION;
|
||||
node["uptime"] = uuid::log::format_timestamp_ms(uuid::get_uptime_ms(), 3);
|
||||
@@ -967,7 +974,7 @@ bool System::command_info(const char * value, const int8_t id, JsonObject & json
|
||||
#endif
|
||||
node["reset_reason"] = EMSESP::system_.reset_reason(0) + " / " + EMSESP::system_.reset_reason(1);
|
||||
|
||||
node = json.createNestedObject("Status");
|
||||
node = output.createNestedObject("Status");
|
||||
|
||||
switch (EMSESP::bus_status()) {
|
||||
case EMSESP::BUS_STATUS_OFFLINE:
|
||||
@@ -1009,7 +1016,7 @@ bool System::command_info(const char * value, const int8_t id, JsonObject & json
|
||||
}
|
||||
|
||||
// show EMS devices
|
||||
JsonArray devices = json.createNestedArray("Devices");
|
||||
JsonArray devices = output.createNestedArray("Devices");
|
||||
for (const auto & device_class : EMSFactory::device_handlers()) {
|
||||
for (const auto & emsdevice : EMSESP::emsdevices) {
|
||||
if ((emsdevice) && (emsdevice->device_type() == device_class.first)) {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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..."));
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -1 +1 @@
|
||||
#define EMSESP_APP_VERSION "3.2.2b14"
|
||||
#define EMSESP_APP_VERSION "3.3.0b0"
|
||||
|
||||
@@ -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;
|
||||
|
||||
EMSESP::webSettingsService.read([&](WebSettings & settings) {
|
||||
Authentication authentication = _securityManager->authenticateRequest(request);
|
||||
authenticated = settings.notoken_api | AuthenticationPredicates::IS_ADMIN(authentication);
|
||||
});
|
||||
|
||||
if ((method != HTTP_GET) || ((method == HTTP_GET) && have_data)) {
|
||||
if (!authenticated) {
|
||||
send_message_response(request, 401, "Bad credentials"); // Unauthorized
|
||||
return;
|
||||
if (method == HTTP_GET) {
|
||||
// special case if there is no command, then default to 'info'
|
||||
if (!input.size()) {
|
||||
input["cmd"] = "info";
|
||||
}
|
||||
} else {
|
||||
// if its a POST then check authentication
|
||||
EMSESP::webSettingsService.read([&](WebSettings & settings) {
|
||||
Authentication authentication = _securityManager->authenticateRequest(request);
|
||||
authenticated = settings.notoken_api | AuthenticationPredicates::IS_ADMIN(authentication);
|
||||
});
|
||||
}
|
||||
|
||||
// output json buffer
|
||||
PrettyAsyncJsonResponse * response = new PrettyAsyncJsonResponse(false, EMSESP_JSON_SIZE_XXLARGE_DYN);
|
||||
JsonObject json = response->getRoot();
|
||||
JsonObject output = response->getRoot();
|
||||
|
||||
// now we have all the parameters go and execute the command
|
||||
// the function will also determine if authentication is needed to execute its command
|
||||
uint8_t cmd_reply = Command::call(device_type, cmd_s.c_str(), (have_data ? value_s.c_str() : nullptr), authenticated, id_n, json);
|
||||
// call command
|
||||
uint8_t command_ret = Command::process(request->url().c_str(), authenticated, input, output);
|
||||
|
||||
// check for errors
|
||||
if (cmd_reply == CommandRet::NOT_FOUND) {
|
||||
delete response;
|
||||
send_message_response(request, 400, "Command not found"); // Bad Request
|
||||
return;
|
||||
} else if (cmd_reply == CommandRet::NOT_ALLOWED) {
|
||||
delete response;
|
||||
send_message_response(request, 401, "Bad credentials"); // Unauthorized
|
||||
return;
|
||||
} else if (cmd_reply != CommandRet::OK) {
|
||||
delete response;
|
||||
send_message_response(request, 400, "Problems parsing elements"); // Bad Request
|
||||
return;
|
||||
}
|
||||
|
||||
if (!json.size()) {
|
||||
delete response;
|
||||
send_message_response(request, 200, "OK"); // OK
|
||||
return;
|
||||
// handle response codes
|
||||
// the output will be populated with a message key if an error occurred
|
||||
int ret_code;
|
||||
if (command_ret == CommandRet::NOT_ALLOWED) {
|
||||
ret_code = 401; // Unauthorized
|
||||
} else if (command_ret == CommandRet::NOT_FOUND) {
|
||||
ret_code = 400; // Bad request
|
||||
} else if (command_ret == CommandRet::OK) {
|
||||
ret_code = 200; //OK
|
||||
if (output.isNull()) {
|
||||
output["message"] = "OK"; // only add if there is no json output already
|
||||
}
|
||||
} else {
|
||||
ret_code = 400; // Bad request
|
||||
}
|
||||
|
||||
// send the json that came back from the command call
|
||||
response->setCode(ret_code);
|
||||
response->setLength();
|
||||
response->setContentType("application/json");
|
||||
request->send(response); // send json response
|
||||
|
||||
#if defined(EMSESP_STANDALONE)
|
||||
Serial.print(COLOR_YELLOW);
|
||||
if (json.size() != 0) {
|
||||
serializeJsonPretty(json, Serial);
|
||||
Serial.print("return code: ");
|
||||
Serial.println(ret_code);
|
||||
if (output.size() != 0) {
|
||||
serializeJsonPretty(output, Serial);
|
||||
}
|
||||
Serial.println();
|
||||
Serial.print(COLOR_RESET);
|
||||
#endif
|
||||
}
|
||||
|
||||
// send a HTTP error back, with optional JSON body data
|
||||
void WebAPIService::send_message_response(AsyncWebServerRequest * request, uint16_t error_code, const char * message) {
|
||||
if (message == nullptr) {
|
||||
AsyncWebServerResponse * response = request->beginResponse(error_code); // just send the code
|
||||
request->send(response);
|
||||
} else {
|
||||
// build a return message and send it
|
||||
PrettyAsyncJsonResponse * response = new PrettyAsyncJsonResponse(false, EMSESP_JSON_SIZE_SMALL);
|
||||
JsonObject json = response->getRoot();
|
||||
json["message"] = message;
|
||||
response->setCode(error_code);
|
||||
response->setLength();
|
||||
response->setContentType("application/json");
|
||||
request->send(response);
|
||||
}
|
||||
|
||||
EMSESP::logger().debug(F("API return code: %d, message: %s"), error_code, message);
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract only the path component from the passed URI and normalized it.
|
||||
* Ex. //one/two////three/// becomes /one/two/three
|
||||
*/
|
||||
std::string SUrlParser::path() {
|
||||
std::string s = "/"; // set up the beginning slash
|
||||
for (std::string & f : m_folders) {
|
||||
s += f;
|
||||
s += "/";
|
||||
}
|
||||
s.pop_back(); // deleting last letter, that is slash '/'
|
||||
return std::string(s);
|
||||
}
|
||||
|
||||
SUrlParser::SUrlParser(const char * uri) {
|
||||
parse(uri);
|
||||
}
|
||||
|
||||
bool SUrlParser::parse(const char * uri) {
|
||||
m_folders.clear();
|
||||
m_keysvalues.clear();
|
||||
enum Type { begin, folder, param, value };
|
||||
std::string s;
|
||||
|
||||
const char * c = uri;
|
||||
enum Type t = Type::begin;
|
||||
std::string last_param;
|
||||
|
||||
if (c != NULL || *c != '\0') {
|
||||
do {
|
||||
if (*c == '/') {
|
||||
if (s.length() > 0) {
|
||||
m_folders.push_back(s);
|
||||
s.clear();
|
||||
}
|
||||
t = Type::folder;
|
||||
} else if (*c == '?' && (t == Type::folder || t == Type::begin)) {
|
||||
if (s.length() > 0) {
|
||||
m_folders.push_back(s);
|
||||
s.clear();
|
||||
}
|
||||
t = Type::param;
|
||||
} else if (*c == '=' && (t == Type::param || t == Type::begin)) {
|
||||
m_keysvalues[s] = "";
|
||||
last_param = s;
|
||||
s.clear();
|
||||
t = Type::value;
|
||||
} else if (*c == '&' && (t == Type::value || t == Type::param || t == Type::begin)) {
|
||||
if (t == Type::value) {
|
||||
m_keysvalues[last_param] = s;
|
||||
} else if ((t == Type::param || t == Type::begin) && (s.length() > 0)) {
|
||||
m_keysvalues[s] = "";
|
||||
last_param = s;
|
||||
}
|
||||
t = Type::param;
|
||||
s.clear();
|
||||
} else if (*c == '\0' && s.length() > 0) {
|
||||
if (t == Type::value) {
|
||||
m_keysvalues[last_param] = s;
|
||||
} else if (t == Type::folder || t == Type::begin) {
|
||||
m_folders.push_back(s);
|
||||
} else if (t == Type::param) {
|
||||
m_keysvalues[s] = "";
|
||||
last_param = s;
|
||||
}
|
||||
s.clear();
|
||||
} else if (*c == '\0' && s.length() == 0) {
|
||||
if (t == Type::param && last_param.length() > 0) {
|
||||
m_keysvalues[last_param] = "";
|
||||
}
|
||||
s.clear();
|
||||
} else {
|
||||
s += *c;
|
||||
}
|
||||
} while (*c++ != '\0');
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace emsesp
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -101,7 +101,7 @@ void WebDataService::device_data(AsyncWebServerRequest * request, JsonVariant &
|
||||
if (emsdevice) {
|
||||
if (emsdevice->unique_id() == json["id"]) {
|
||||
// wait max 2.5 sec for updated data (post_send_delay is 2 sec)
|
||||
for (uint16_t i = 0; i < 2500 && EMSESP::wait_validate(); i++) {
|
||||
for (uint16_t i = 0; i < (emsesp::TxService::POST_SEND_DELAY + 500) && EMSESP::wait_validate(); i++) {
|
||||
delay(1);
|
||||
}
|
||||
EMSESP::wait_validate(0); // reset in case of timeout
|
||||
@@ -126,31 +126,49 @@ void WebDataService::device_data(AsyncWebServerRequest * request, JsonVariant &
|
||||
// assumes the service has been checked for admin authentication
|
||||
void WebDataService::write_value(AsyncWebServerRequest * request, JsonVariant & json) {
|
||||
if (json.is<JsonObject>()) {
|
||||
JsonObject dv = json["devicevalue"];
|
||||
uint8_t id = json["id"];
|
||||
JsonObject dv = json["devicevalue"];
|
||||
uint8_t unique_id = json["id"];
|
||||
|
||||
// using the unique ID from the web find the real device type
|
||||
// id is the selected device
|
||||
for (const auto & emsdevice : EMSESP::emsdevices) {
|
||||
if (emsdevice) {
|
||||
if (emsdevice->unique_id() == id) {
|
||||
const char * cmd = dv["c"];
|
||||
uint8_t device_type = emsdevice->device_type();
|
||||
uint8_t cmd_return = CommandRet::OK;
|
||||
char s[10];
|
||||
if (emsdevice->unique_id() == unique_id) {
|
||||
// parse the command as it could have a hc or wwc prefixed, e.g. hc2/seltemp
|
||||
const char * cmd = dv["c"]; // the command
|
||||
int8_t id = -1; // default
|
||||
cmd = Command::parse_command_string(cmd, id); // extract hc or wwc
|
||||
|
||||
// create JSON for output
|
||||
AsyncJsonResponse * response = new AsyncJsonResponse(false, EMSESP_JSON_SIZE_SMALL);
|
||||
JsonObject output = response->getRoot();
|
||||
|
||||
// the data could be in any format, but we need string
|
||||
JsonVariant data = dv["v"];
|
||||
// authenticated is always true
|
||||
JsonVariant data = dv["v"]; // the value in any format
|
||||
uint8_t command_ret = CommandRet::OK;
|
||||
uint8_t device_type = emsdevice->device_type();
|
||||
if (data.is<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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user