Merge remote-tracking branch 'origin/dev'

This commit is contained in:
proddy
2021-11-28 23:03:15 +01:00
145 changed files with 6122 additions and 3179 deletions

View File

@@ -26,150 +26,277 @@ 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[20] = {'\0'};
strlcpy(cmd_new, cmd, 20);
// 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
// the entry point will be either via the Web API (api/) or MQTT (<base>/)
// returns a return code and json output
uint8_t Command::process(const char * path, const bool is_admin, const JsonObject & input, JsonObject & output) {
SUrlParser p; // parse URL for the path names
p.parse(path);
// find the command
auto cf = find_command(device_type, cmd_new, id_new);
if ((cf == nullptr) || (cf->cmdfunction_json_)) {
LOG_WARNING(F("Command %s on %s not found"), cmd, EMSdevice::device_type_2_device_name(device_type).c_str());
return CommandRet::NOT_FOUND;
if (!p.paths().size()) {
return message(CommandRet::ERROR, "invalid path", output);
}
// check if we're allowed to call it
if (cf->has_flags(CommandFlag::ADMIN_ONLY) && !authenticated) {
LOG_WARNING(F("Command %s on %s not permitted. requires admin."), cmd, EMSdevice::device_type_2_device_name(device_type).c_str());
return 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);
// check first if it's from API, if so strip the "api/"
if ((p.paths().front() == "api")) {
p.paths().erase(p.paths().begin());
} else {
LOG_INFO(F("Calling %s command '%s', value %s, id is %d"), dname.c_str(), cmd, value, id);
// not /api, so must be MQTT path. Check for base and remove it.
if (!strncmp(path, Mqtt::base().c_str(), Mqtt::base().length())) {
char new_path[Mqtt::MQTT_TOPIC_MAX_SIZE];
strncpy(new_path, path, sizeof(new_path));
p.parse(new_path + Mqtt::base().length() + 1); // re-parse the stripped path
} else {
return message(CommandRet::ERROR, "unrecognized path", output); // error
}
}
return ((cf->cmdfunction_)(value, id_new)) ? CommandRet::OK : CommandRet::ERROR;
// Serial.println(p.path().c_str()); // dump paths, for debugging
// re-calculate new path
// if there is only a path (URL) and no body then error!
size_t num_paths = p.paths().size();
if (!num_paths && !input.size()) {
return message(CommandRet::ERROR, "missing command in path", output);
}
std::string cmd_s;
int8_t id_n = -1; // default hc
// check for a device as first item in the path
// if its not a known device (thermostat, boiler etc) look for any special MQTT subscriptions
const char * device_s = nullptr;
if (!num_paths) {
// 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, make sure it exists
uint8_t device_type = EMSdevice::device_name_2_device_type(device_s);
if (!device_has_commands(device_type)) {
LOG_DEBUG(F("Command failed: unknown device '%s'"), device_s);
return message(CommandRet::ERROR, "unknown device", output);
}
// the next value on the path should be the command
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 as it could be in the format 'hc/XXX'
char command[50];
snprintf(command, sizeof(command), "%s/%s", p.paths()[1].c_str(), p.paths()[2].c_str());
command_p = command;
} else {
// take it from the JSON
if (input.containsKey("entity")) {
command_p = input["entity"];
} else if (input.containsKey("cmd")) {
command_p = input["cmd"];
}
}
// some commands may be prefixed with hc. or wwc. so extract these if they exist
// parse_command_string returns the extracted command
command_p = parse_command_string(command_p, id_n);
if (command_p == nullptr) {
// handle dead endpoints like api/system or api/boiler
// default to 'info' for SYSTEM and DALLASENSOR, the other devices to 'values' for shortname version
if (num_paths < 3) {
if (device_type < EMSdevice::DeviceType::BOILER) {
command_p = "info";
} else {
command_p = "values";
}
} else {
return message(CommandRet::NOT_FOUND, "missing or bad command", output);
}
}
// 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"];
id_n += 7; // wwc1 has id 8
} 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 return_code = CommandRet::ERROR;
if (data.is<const char *>()) {
return_code = Command::call(device_type, command_p, data.as<const char *>(), is_admin, id_n, output);
} else if (data.is<int>()) {
char data_str[10];
return_code = Command::call(device_type, command_p, Helpers::itoa(data_str, (int16_t)data.as<int>()), is_admin, id_n, output);
} else if (data.is<float>()) {
char data_str[10];
return_code = Command::call(device_type, command_p, Helpers::render_value(data_str, (float)data.as<float>(), 2), is_admin, id_n, output);
} else if (data.isNull()) {
return_code = Command::call(device_type, command_p, "", is_admin, id_n, output); // empty, will do a query instead
} else {
return message(CommandRet::ERROR, "cannot parse command", output); // can't process
}
return return_code;
}
const std::string Command::return_code_string(const uint8_t return_code) {
switch (return_code) {
case CommandRet::ERROR:
return read_flash_string(F("Error"));
break;
case CommandRet::OK:
return read_flash_string(F("OK"));
break;
case CommandRet::NOT_FOUND:
return read_flash_string(F("Not Found"));
break;
case CommandRet::NOT_ALLOWED:
return read_flash_string(F("Not Authorized"));
break;
case CommandRet::FAIL:
return read_flash_string(F("Failed"));
break;
}
char s[4];
return Helpers::smallitoa(s, return_code);
}
// 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));
// look for a delimeter and split the string
char * p = command_s;
char * 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;
}
}
}
// extract the hc or wwc number
uint8_t start_pos = breakp - p + 1;
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' + 7; // wwc1 has id 8
} else {
#if defined(EMSESP_DEBUG)
LOG_DEBUG(F("[DEBUG] Command parse error, unknown hc/wwc in %s"), command_s);
#endif
return nullptr;
}
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[20] = {'\0'};
strlcpy(cmd_new, cmd, 20);
auto cf = find_command(device_type, cmd_new, id_new);
// check if we're allowed to call it
if (cf != nullptr) {
if (cf->has_flags(CommandFlag::ADMIN_ONLY) && !authenticated && value != nullptr) {
LOG_WARNING(F("Command %s on %s not permitted. requires admin."), cmd, EMSdevice::device_type_2_device_name(device_type).c_str());
return CommandRet::NOT_ALLOWED; // command not allowed
}
}
uint8_t Command::call(const uint8_t device_type, const char * cmd, const char * value, const bool is_admin, const int8_t id, JsonObject & output) {
uint8_t return_code = CommandRet::OK;
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);
}
// 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;
}
// see if there is a command registered
auto cf = find_command(device_type, cmd);
// 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;
}
// 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)
// exclude SYSTEM and DALLASSENSOR
if (cf->cmdfunction_json_) {
return ((cf->cmdfunction_json_)(value, id_new, json)) ? CommandRet::OK : CommandRet::ERROR;
} else {
if ((device_type != EMSdevice::DeviceType::SYSTEM) && (value == nullptr || strlen(value) == 0 || strcmp(value, "?") == 0 || strcmp(value, "*") == 0)) {
return EMSESP::get_device_value_info(json, cmd_new, id_new, device_type) ? CommandRet::OK : CommandRet::ERROR;
}
return ((cf->cmdfunction_)(value, id_new)) ? CommandRet::OK : CommandRet::ERROR;
}
}
// 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 ((device_type >= EMSdevice::DeviceType::BOILER) && (!value || !strlen(value))) {
if (!cf || !cf->cmdfunction_json_) {
#if defined(EMSESP_DEBUG)
LOG_INFO(F("[DEBUG] Calling %s command '%s' to retrieve values"), dname.c_str(), cmd);
#endif
return EMSESP::get_device_value_info(output, cmd, id, device_type) ? CommandRet::OK : CommandRet::ERROR; // entity = cmd
}
}
// 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;
if (cf) {
// we have a matching command
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) && !is_admin) {
output["message"] = "authentication failed";
return CommandRet::NOT_ALLOWED; // command not allowed
}
// call the function
if (cf->cmdfunction_json_) {
return_code = ((cf->cmdfunction_json_)(value, id, output)) ? CommandRet::OK : CommandRet::ERROR;
}
if (cf->cmdfunction_) {
return_code = ((cf->cmdfunction_)(value, id)) ? CommandRet::OK : CommandRet::ERROR;
}
// report error if call failed
if (return_code != CommandRet::OK) {
return message(return_code, "callback function failed", output);
}
return return_code;
}
// empty command after processing prefix is info
if (cmd[0] == '\0') {
strlcpy(cmd, "info", 20);
}
return find_command(device_type, cmd);
// we didn't find the command and its not an endpoint, report error
LOG_DEBUG(F("Command failed: invalid command '%s'"), cmd);
return message(CommandRet::NOT_FOUND, "invalid command", output);
}
// add a command to the list, which does not return json
// these commands are not callable directly via MQTT subscriptions either
void Command::add(const uint8_t device_type, const __FlashStringHelper * cmd, const cmd_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) {
if (find_command(device_type, read_flash_string(cmd).c_str()) != nullptr) {
return;
}
@@ -179,26 +306,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
// see if we need to subscribe
if (Mqtt::enabled()) {
Mqtt::register_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::FLAG_NOSUB
void Command::add_json(const uint8_t device_type,
const __FlashStringHelper * cmd,
const cmd_json_function_p cb,
const __FlashStringHelper * description,
uint8_t flags) {
// flag is fixed to MqttSubFlag::MQTT_SUB_FLAG_NOSUB so there will be no topic subscribed to this
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) {
if (find_command(device_type, 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
@@ -209,14 +327,14 @@ Command::CmdFunction * Command::find_command(const uint8_t device_type, const ch
}
// convert cmd to lowercase and compare
char lowerCmd[20];
char lowerCmd[30];
strlcpy(lowerCmd, cmd, sizeof(lowerCmd));
for (char * p = lowerCmd; *p; p++) {
*p = tolower(*p);
}
for (auto & cf : cmdfunctions_) {
if (!strcmp(lowerCmd, Helpers::toLower(uuid::read_flash_string(cf.cmd_)).c_str()) && (cf.device_type_ == device_type)) {
if (!strcmp(lowerCmd, Helpers::toLower(read_flash_string(cf.cmd_)).c_str()) && (cf.device_type_ == device_type)) {
return &cf;
}
}
@@ -225,9 +343,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;
}
@@ -235,15 +353,15 @@ bool Command::list(const uint8_t device_type, JsonObject & json) {
std::list<std::string> sorted_cmds;
for (const auto & cf : cmdfunctions_) {
if ((cf.device_type_ == device_type) && !cf.has_flags(CommandFlag::HIDDEN)) {
sorted_cmds.push_back(uuid::read_flash_string(cf.cmd_));
sorted_cmds.push_back(read_flash_string(cf.cmd_));
}
}
sorted_cmds.sort();
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_;
if ((cf.device_type_ == device_type) && !cf.has_flags(CommandFlag::HIDDEN) && cf.description_ && (cl == read_flash_string(cf.cmd_))) {
output[cl] = cf.description_;
}
}
}
@@ -262,7 +380,7 @@ void Command::show(uuid::console::Shell & shell, uint8_t device_type, bool verbo
std::list<std::string> sorted_cmds;
for (const auto & cf : cmdfunctions_) {
if ((cf.device_type_ == device_type) && !cf.has_flags(CommandFlag::HIDDEN)) {
sorted_cmds.push_back(uuid::read_flash_string(cf.cmd_));
sorted_cmds.push_back(read_flash_string(cf.cmd_));
}
}
sorted_cmds.sort();
@@ -282,15 +400,15 @@ void Command::show(uuid::console::Shell & shell, uint8_t device_type, bool verbo
for (auto & cl : sorted_cmds) {
// find and print the description
for (const auto & cf : cmdfunctions_) {
if ((cf.device_type_ == device_type) && !cf.has_flags(CommandFlag::HIDDEN) && cf.description_ && (cl == uuid::read_flash_string(cf.cmd_))) {
if ((cf.device_type_ == device_type) && !cf.has_flags(CommandFlag::HIDDEN) && cf.description_ && (cl == read_flash_string(cf.cmd_))) {
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
@@ -298,8 +416,12 @@ void Command::show(uuid::console::Shell & shell, uint8_t device_type, bool verbo
shell.print(' ');
}
shell.print(COLOR_BRIGHT_CYAN);
shell.print(uuid::read_flash_string(cf.description_));
if (cf.has_flags(CommandFlag::ADMIN_ONLY)) {
if (cf.has_flags(MQTT_SUB_FLAG_WW)) {
shell.print(EMSdevice::tag_to_string(TAG_DEVICE_DATA_WW));
shell.print(' ');
}
shell.print(read_flash_string(cf.description_));
if (!cf.has_flags(CommandFlag::ADMIN_ONLY)) {
shell.print(' ');
shell.print(COLOR_BRIGHT_RED);
shell.print('*');
@@ -325,7 +447,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 +485,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: "));
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 +497,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 +507,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 +515,81 @@ void Command::show_all(uuid::console::Shell & shell) {
}
}
// Extract only the path component from the passed URI and normalized it
// e.g. //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 != nullptr || *c != '\0') {
do {
if (*c == '/') {
if (s.length() > 0) {
m_folders.push_back(s);
s.clear();
}
t = Type::folder;
} else if (*c == '?' && (t == Type::folder || t == Type::begin)) {
if (s.length() > 0) {
m_folders.push_back(s);
s.clear();
}
t = Type::param;
} else if (*c == '=' && (t == Type::param || t == Type::begin)) {
m_keysvalues[s] = "";
last_param = s;
s.clear();
t = Type::value;
} else if (*c == '&' && (t == Type::value || t == Type::param || t == Type::begin)) {
if (t == Type::value) {
m_keysvalues[last_param] = s;
} else if ((t == Type::param || t == Type::begin) && (s.length() > 0)) {
m_keysvalues[s] = "";
last_param = s;
}
t = Type::param;
s.clear();
} else if (*c == '\0' && s.length() > 0) {
if (t == Type::value) {
m_keysvalues[last_param] = s;
} else if (t == Type::folder || t == Type::begin) {
m_folders.push_back(s);
} else if (t == Type::param) {
m_keysvalues[s] = "";
last_param = s;
}
s.clear();
} else if (*c == '\0' && s.length() == 0) {
if (t == Type::param && last_param.length() > 0) {
m_keysvalues[last_param] = "";
}
s.clear();
} else {
s += *c;
}
} while (*c++ != '\0');
}
return true;
}
} // namespace emsesp

View File

@@ -25,6 +25,7 @@
#include <string>
#include <vector>
#include <functional>
#include <unordered_map>
#include "console.h"
@@ -36,18 +37,19 @@ namespace emsesp {
// mqtt flags for command subscriptions
enum CommandFlag : uint8_t {
MQTT_SUB_FLAG_NORMAL = 0, // 0
MQTT_SUB_FLAG_HC = (1 << 0), // 1
MQTT_SUB_FLAG_WWC = (1 << 1), // 2
MQTT_SUB_FLAG_NOSUB = (1 << 2), // 4
HIDDEN = (1 << 3), // 8
ADMIN_ONLY = (1 << 4) // 16
MQTT_SUB_FLAG_DEFAULT = 0, // 0 no flags set, always subscribe to MQTT
MQTT_SUB_FLAG_HC = (1 << 0), // 1 TAG_HC1 - TAG_HC4
MQTT_SUB_FLAG_WWC = (1 << 1), // 2 TAG_WWC1 - TAG_WWC4
MQTT_SUB_FLAG_NOSUB = (1 << 2), // 4
MQTT_SUB_FLAG_WW = (1 << 3), // 8 TAG_DEVICE_DATA_WW
HIDDEN = (1 << 4), // 16 do not show in API or Web
ADMIN_ONLY = (1 << 5) // 32 requires authentication
};
// return status after calling a Command
enum CommandRet : uint8_t {
ERRORED = 0,
FAIL = 0, // 0 or FALSE
OK, // 1 or TRUE
NOT_FOUND, // 2
ERROR, // 3
@@ -56,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:
@@ -100,35 +102,75 @@ 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, const bool is_admin, 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_NORMAL);
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_NORMAL);
// 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 is_admin, const JsonObject & input, JsonObject & output);
static const char * parse_command_string(const char * command, int8_t & id);
static const std::string return_code_string(const uint8_t return_code);
private:
static uuid::log::Logger logger_;
static std::vector<CmdFunction> cmdfunctions_; // list of commands
static std::vector<CmdFunction> cmdfunctions_; // the list of commands
inline static uint8_t message(uint8_t error_code, const char * message, JsonObject & output) {
output.clear();
output["message"] = (const char *)message;
return error_code;
}
};
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

View File

@@ -77,7 +77,7 @@ void EMSESPShell::display_banner() {
if (console_hostname_.empty()) {
console_hostname_.resize(16, '\0');
snprintf_P(&console_hostname_[0], console_hostname_.capacity() + 1, PSTR("ems-esp"));
snprintf(&console_hostname_[0], console_hostname_.capacity() + 1, "ems-esp");
}
// load the list of commands
@@ -235,19 +235,25 @@ void EMSESPShell::add_console_commands() {
shell.printfln(F_(bus_id_fmt), settings.ems_bus_id);
char buffer[4];
shell.printfln(F_(master_thermostat_fmt),
settings.master_thermostat == 0 ? uuid::read_flash_string(F_(auto)).c_str()
settings.master_thermostat == 0 ? read_flash_string(F_(auto)).c_str()
: Helpers::hextoa(buffer, settings.master_thermostat));
shell.printfln(F_(board_profile_fmt), settings.board_profile.c_str());
});
});
commands->add_command(ShellContext::MAIN,
CommandFlags::ADMIN,
CommandFlags::USER,
flash_string_vector{F_(read)},
flash_string_vector{F_(deviceid_mandatory), F_(typeid_mandatory), F_(offset_optional)},
[=](Shell & shell __attribute__((unused)), const std::vector<std::string> & arguments) {
uint8_t device_id = Helpers::hextoint(arguments.front().c_str());
uint16_t type_id = Helpers::hextoint(arguments[1].c_str());
uint8_t device_id = Helpers::hextoint(arguments.front().c_str());
if (!EMSESP::valid_device(device_id)) {
shell.printfln(F("Invalid device ID"));
return;
}
uint16_t type_id = Helpers::hextoint(arguments[1].c_str());
if (arguments.size() == 4) {
uint16_t offset = Helpers::hextoint(arguments[2].c_str());
uint8_t length = Helpers::hextoint(arguments.back().c_str());
@@ -272,8 +278,7 @@ void EMSESPShell::add_console_commands() {
settings.master_thermostat = value;
EMSESP::actual_master_thermostat(value); // set the internal value too
char buffer[5];
shell.printfln(F_(master_thermostat_fmt),
!value ? uuid::read_flash_string(F_(auto)).c_str() : Helpers::hextoa(buffer, value));
shell.printfln(F_(master_thermostat_fmt), !value ? read_flash_string(F_(auto)).c_str() : Helpers::hextoa(buffer, value));
return StateUpdateResult::CHANGED;
},
"local");
@@ -373,8 +378,6 @@ void EMSESPShell::add_console_commands() {
return;
}
DynamicJsonDocument doc(EMSESP_JSON_SIZE_XLARGE_DYN);
JsonObject json = doc.to<JsonObject>();
// validate that a command is present
if (arguments.size() < 2) {
@@ -383,37 +386,45 @@ void EMSESPShell::add_console_commands() {
return;
}
const char * cmd = arguments[1].c_str();
uint8_t cmd_return = CommandRet::OK;
DynamicJsonDocument doc(EMSESP_JSON_SIZE_XXLARGE_DYN);
int8_t id = -1;
const char * cmd = Command::parse_command_string(arguments[1].c_str(), id);
uint8_t return_code = CommandRet::OK;
JsonObject json = doc.to<JsonObject>();
if (arguments.size() == 2) {
// no value specified, just the cmd
cmd_return = Command::call(device_type, cmd, nullptr, true, -1, json);
return_code = Command::call(device_type, cmd, nullptr, true, id, json);
} else if (arguments.size() == 3) {
if (strncmp(cmd, "info", 4) == 0) {
// info has a id but no value
cmd_return = Command::call(device_type, cmd, nullptr, true, atoi(arguments.back().c_str()), json);
return_code = Command::call(device_type, cmd, nullptr, true, atoi(arguments.back().c_str()), json);
} else if (arguments[2] == "?") {
return_code = Command::call(device_type, cmd, nullptr, true, id, json);
} else {
// has a value but no id so use -1
cmd_return = Command::call(device_type, cmd, arguments.back().c_str(), true, -1, json);
return_code = Command::call(device_type, cmd, arguments.back().c_str(), true, id, json);
}
} else {
// use value, which could be an id or hc
cmd_return = Command::call(device_type, cmd, arguments[2].c_str(), true, atoi(arguments[3].c_str()), json);
if (arguments[2] == "?") {
return_code = Command::call(device_type, cmd, nullptr, true, atoi(arguments[3].c_str()), json);
} else {
return_code = Command::call(device_type, cmd, arguments[2].c_str(), true, atoi(arguments[3].c_str()), json);
}
}
if (cmd_return == CommandRet::OK && json.size()) {
if (return_code == CommandRet::OK && json.size()) {
serializeJsonPretty(doc, shell);
shell.println();
return;
}
if (cmd_return == CommandRet::NOT_FOUND) {
if (return_code == CommandRet::NOT_FOUND) {
shell.println(F("Unknown command"));
shell.print(F("Available commands are: "));
Command::show(shell, device_type, false); // non-verbose mode
} else if (cmd_return != CommandRet::OK) {
} else if (return_code != CommandRet::OK) {
shell.println(F("Bad syntax"));
}
},
@@ -434,7 +445,7 @@ void EMSESPShell::add_console_commands() {
if (Command::device_has_commands(device_type)) {
for (const auto & cf : Command::commands()) {
if (cf.device_type_ == device_type) {
command_list.emplace_back(uuid::read_flash_string(cf.cmd_));
command_list.emplace_back(read_flash_string(cf.cmd_));
}
}
return command_list;
@@ -596,7 +607,7 @@ void Console::load_system_commands(unsigned int context) {
CommandFlags::ADMIN,
flash_string_vector{F_(restart)},
[](Shell & shell __attribute__((unused)), const std::vector<std::string> & arguments __attribute__((unused))) {
EMSESP::system_.restart();
EMSESP::system_.system_restart();
});
EMSESPShell::commands->add_command(context,
@@ -717,7 +728,7 @@ void Console::load_system_commands(unsigned int context) {
[](Shell & shell, const std::vector<std::string> & arguments) {
if (arguments.size() == 0) {
EMSESP::webSettingsService.read([&](WebSettings & settings) {
for (uint8_t i = 0; i < NUM_SENSOR_NAMES; i++) {
for (uint8_t i = 0; i < MAX_NUM_SENSOR_NAMES; i++) {
if (!settings.sensor[i].id.isEmpty()) {
shell.print(settings.sensor[i].id);
shell.print(" : ");
@@ -751,32 +762,33 @@ void Console::load_system_commands(unsigned int context) {
EMSESP::dallassensor_.update(arguments.front().c_str(), arguments[1].c_str(), offset);
});
EMSESPShell::commands
->add_command(context,
CommandFlags::ADMIN,
flash_string_vector{F_(set), F_(board_profile)},
flash_string_vector{F_(name_mandatory)},
[](Shell & shell, const std::vector<std::string> & arguments) {
std::vector<uint8_t> data; // led, dallas, rx, tx, button
std::string board_profile = Helpers::toUpper(arguments.front());
if (!EMSESP::system_.load_board_profile(data, board_profile)) {
shell.println(F("Invalid board profile (S32, E32, MH-ET, NODEMCU, OLIMEX, TLK110, LAN8720, CUSTOM)"));
return;
}
EMSESP::webSettingsService.update(
[&](WebSettings & settings) {
settings.board_profile = board_profile.c_str();
settings.led_gpio = data[0];
settings.dallas_gpio = data[1];
settings.rx_gpio = data[2];
settings.tx_gpio = data[3];
settings.pbutton_gpio = data[4];
return StateUpdateResult::CHANGED;
},
"local");
shell.printfln("Loaded board profile %s (%d,%d,%d,%d,%d)", board_profile.c_str(), data[0], data[1], data[2], data[3], data[4]);
EMSESP::system_.network_init(true);
});
EMSESPShell::commands->add_command(
context,
CommandFlags::ADMIN,
flash_string_vector{F_(set), F_(board_profile)},
flash_string_vector{F_(name_mandatory)},
[](Shell & shell, const std::vector<std::string> & arguments) {
std::vector<uint8_t> data; // led, dallas, rx, tx, button
std::string board_profile = Helpers::toUpper(arguments.front());
if (!EMSESP::system_.load_board_profile(data, board_profile)) {
shell.println(F("Invalid board profile (S32, E32, MH-ET, NODEMCU, OLIMEX, CUSTOM)"));
return;
}
EMSESP::webSettingsService.update(
[&](WebSettings & settings) {
settings.board_profile = board_profile.c_str();
settings.led_gpio = data[0];
settings.dallas_gpio = data[1];
settings.rx_gpio = data[2];
settings.tx_gpio = data[3];
settings.pbutton_gpio = data[4];
settings.phy_type = data[5];
return StateUpdateResult::CHANGED;
},
"local");
shell.printfln("Loaded board profile %s (%d,%d,%d,%d,%d,%d)", board_profile.c_str(), data[0], data[1], data[2], data[3], data[4], data[5]);
EMSESP::system_.network_init(true);
});
EMSESPShell::commands->add_command(context,
CommandFlags::ADMIN,
flash_string_vector{F_(show), F_(users)},
@@ -811,14 +823,14 @@ std::string EMSESPShell::prompt_suffix() {
}
void EMSESPShell::end_of_transmission() {
invoke_command(uuid::read_flash_string(F_(exit)));
invoke_command(read_flash_string(F_(exit)));
}
EMSESPStreamConsole::EMSESPStreamConsole(Stream & stream, bool local)
: uuid::console::Shell(commands, ShellContext::MAIN, local ? (CommandFlags::USER | CommandFlags::LOCAL) : CommandFlags::USER)
, uuid::console::StreamConsole(stream)
, EMSESPShell()
, name_(uuid::read_flash_string(F("Serial")))
, name_(read_flash_string(F("Serial")))
, pty_(SIZE_MAX)
, addr_()
, port_(0) {
@@ -841,7 +853,7 @@ EMSESPStreamConsole::EMSESPStreamConsole(Stream & stream, const IPAddress & addr
ptys_[pty_] = true;
}
snprintf_P(text.data(), text.size(), PSTR("pty%u"), pty_);
snprintf(text.data(), text.size(), "pty%u", pty_);
name_ = text.data();
#ifndef EMSESP_STANDALONE
logger().info(F("Allocated console %s for connection from [%s]:%u"), name_.c_str(), uuid::printable_to_string(addr_).c_str(), port_);

View File

@@ -41,15 +41,15 @@ void DallasSensor::start() {
bus_.begin(dallas_gpio_);
#endif
// API calls
Command::add_json(
Command::add(
EMSdevice::DeviceType::DALLASSENSOR,
F_(info),
[&](const char * value, const int8_t id, JsonObject & json) { return command_info(value, id, json); },
[&](const char * value, const int8_t id, JsonObject & output) { return command_info(value, id, output); },
F_(info_cmd));
Command::add_json(
Command::add(
EMSdevice::DeviceType::DALLASSENSOR,
F_(commands),
[&](const char * value, const int8_t id, JsonObject & json) { return command_commands(value, id, json); },
[&](const char * value, const int8_t id, JsonObject & output) { return command_commands(value, id, output); },
F_(commands_cmd));
}
}
@@ -79,7 +79,7 @@ void DallasSensor::loop() {
if (state_ == State::IDLE) {
if (time_now - last_activity_ >= READ_INTERVAL_MS) {
#ifdef EMSESP_DEBUG_SENSOR
LOG_DEBUG(F("Read sensor temperature"));
LOG_DEBUG(F("[DEBUG] Read sensor temperature"));
#endif
if (bus_.reset() || parasite_) {
YIELD;
@@ -210,9 +210,6 @@ bool DallasSensor::temperature_convert_complete() {
#endif
}
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wunused-parameter"
int16_t DallasSensor::get_temperature_c(const uint8_t addr[]) {
#ifndef EMSESP_STANDALONE
if (!bus_.reset()) {
@@ -276,8 +273,6 @@ int16_t DallasSensor::get_temperature_c(const uint8_t addr[]) {
#endif
}
#pragma GCC diagnostic pop
const std::vector<DallasSensor::Sensor> DallasSensor::sensors() const {
return sensors_;
}
@@ -299,13 +294,13 @@ uint64_t DallasSensor::Sensor::id() const {
std::string DallasSensor::Sensor::id_string() const {
std::string str(20, '\0');
snprintf_P(&str[0],
str.capacity() + 1,
PSTR("%02X-%04X-%04X-%04X"),
(unsigned int)(id_ >> 48) & 0xFF,
(unsigned int)(id_ >> 32) & 0xFFFF,
(unsigned int)(id_ >> 16) & 0xFFFF,
(unsigned int)(id_)&0xFFFF);
snprintf(&str[0],
str.capacity() + 1,
"%02X-%04X-%04X-%04X",
(unsigned int)(id_ >> 48) & 0xFF,
(unsigned int)(id_ >> 32) & 0xFFFF,
(unsigned int)(id_ >> 16) & 0xFFFF,
(unsigned int)(id_)&0xFFFF);
return str;
}
@@ -313,7 +308,7 @@ std::string DallasSensor::Sensor::to_string(const bool name) const {
std::string str = id_string();
EMSESP::webSettingsService.read([&](WebSettings & settings) {
if (settings.dallas_format == Dallas_Format::NAME || name) {
for (uint8_t i = 0; i < NUM_SENSOR_NAMES; i++) {
for (uint8_t i = 0; i < MAX_NUM_SENSOR_NAMES; i++) {
if (strcmp(settings.sensor[i].id.c_str(), str.c_str()) == 0) {
str = settings.sensor[i].name.c_str();
}
@@ -328,7 +323,7 @@ int16_t DallasSensor::Sensor::offset() const {
std::string str = id_string();
int16_t offset = 0; // default value
EMSESP::webSettingsService.read([&](WebSettings & settings) {
for (uint8_t i = 0; i < NUM_SENSOR_NAMES; i++) {
for (uint8_t i = 0; i < MAX_NUM_SENSOR_NAMES; i++) {
if (strcmp(settings.sensor[i].id.c_str(), str.c_str()) == 0) {
offset = settings.sensor[i].offset;
}
@@ -347,7 +342,7 @@ void DallasSensor::delete_ha_config(uint8_t index, const char * name) {
std::string topicname = name;
std::replace(topicname.begin(), topicname.end(), '-', '_');
snprintf_P(topic, sizeof(topic), PSTR("homeassistant/sensor/%s/dallassensor_%s/config"), Mqtt::base().c_str(), topicname.c_str());
snprintf(topic, sizeof(topic), "homeassistant/sensor/%s/dallassensor_%s/config", Mqtt::base().c_str(), topicname.c_str());
Mqtt::publish(topic);
registered_ha_[index] = false; // forces a recreate of the HA config topic
}
@@ -376,7 +371,7 @@ bool DallasSensor::update(const char * idstr, const char * name, int16_t offset)
EMSESP::webSettingsService.update(
[&](WebSettings & settings) {
// check for new name of stored id
for (uint8_t i = 0; i < NUM_SENSOR_NAMES; i++) {
for (uint8_t i = 0; i < MAX_NUM_SENSOR_NAMES; i++) {
if (strcmp(id, settings.sensor[i].id.c_str()) == 0) {
if (strlen(name) == 0 && offset == 0) { // delete entry if name and offset is empty
LOG_INFO(F("Deleting entry for sensor %s"), id);
@@ -397,7 +392,7 @@ bool DallasSensor::update(const char * idstr, const char * name, int16_t offset)
}
// check for free place
for (uint8_t i = 0; i < NUM_SENSOR_NAMES; i++) {
for (uint8_t i = 0; i < MAX_NUM_SENSOR_NAMES; i++) {
if (settings.sensor[i].id.isEmpty()) {
settings.sensor[i].id = id;
settings.sensor[i].name = (strlen(name) == 0) ? id : name;
@@ -410,7 +405,7 @@ bool DallasSensor::update(const char * idstr, const char * name, int16_t offset)
}
// check if there is a unused id and overwrite it
for (uint8_t i = 0; i < NUM_SENSOR_NAMES; i++) {
for (uint8_t i = 0; i < MAX_NUM_SENSOR_NAMES; i++) {
bool found = false;
for (const auto & sensor : sensors_) {
if (strcmp(sensor.id_string().c_str(), settings.sensor[i].id.c_str()) == 0) {
@@ -446,14 +441,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;
}
@@ -461,23 +456,23 @@ bool DallasSensor::command_info(const char * value, const int8_t id, JsonObject
uint8_t i = 1; // sensor count
for (const auto & sensor : sensors_) {
char sensorID[10]; // sensor{1-n}
snprintf_P(sensorID, 10, PSTR("sensor%d"), i++);
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
@@ -493,7 +488,7 @@ void DallasSensor::publish_values(const bool force) {
for (const auto & sensor : sensors_) {
char sensorID[10]; // sensor{1-n}
snprintf_P(sensorID, 10, PSTR("sensor%d"), sensor_no);
snprintf(sensorID, 10, "sensor%d", sensor_no);
if (dallas_format_ == Dallas_Format::NUMBER) {
// e.g. dallassensor_data = {"sensor1":{"id":"28-EA41-9497-0E03","temp":23.3},"sensor2":{"id":"28-233D-9497-0C03","temp":24.0}}
JsonObject dataSensor = doc.createNestedObject(sensorID);
@@ -513,28 +508,28 @@ void DallasSensor::publish_values(const bool force) {
config["dev_cla"] = FJSON("temperature");
char stat_t[50];
snprintf_P(stat_t, sizeof(stat_t), PSTR("%s/dallassensor_data"), Mqtt::base().c_str());
snprintf(stat_t, sizeof(stat_t), "%s/dallassensor_data", Mqtt::base().c_str());
config["stat_t"] = stat_t;
config["unit_of_meas"] = FJSON("°C");
char str[50];
if (dallas_format_ != Dallas_Format::NUMBER) {
snprintf_P(str, sizeof(str), PSTR("{{value_json['%s']}}"), sensor.to_string().c_str());
snprintf(str, sizeof(str), "{{value_json['%s']}}", sensor.to_string().c_str());
} else {
snprintf_P(str, sizeof(str), PSTR("{{value_json.sensor%d.temp}}"), sensor_no);
snprintf(str, sizeof(str), "{{value_json.sensor%d.temp}}", sensor_no);
}
config["val_tpl"] = str;
// name as sensor number not the long unique ID
if (dallas_format_ != Dallas_Format::NUMBER) {
snprintf_P(str, sizeof(str), PSTR("Dallas Sensor %s"), sensor.to_string().c_str());
snprintf(str, sizeof(str), "Dallas Sensor %s", sensor.to_string().c_str());
} else {
snprintf_P(str, sizeof(str), PSTR("Dallas Sensor %d"), sensor_no);
snprintf(str, sizeof(str), "Dallas Sensor %d", sensor_no);
}
config["name"] = str;
snprintf_P(str, sizeof(str), PSTR("dallasensor_%s"), sensor.to_string().c_str());
snprintf(str, sizeof(str), "dallasensor_%s", sensor.to_string().c_str());
config["uniq_id"] = str;
JsonObject dev = config.createNestedObject("dev");
@@ -546,12 +541,12 @@ void DallasSensor::publish_values(const bool force) {
char topic[Mqtt::MQTT_TOPIC_MAX_SIZE];
if (dallas_format_ == Dallas_Format::NUMBER) {
snprintf_P(topic, sizeof(topic), PSTR("sensor/%s/dallassensor_%d/config"), Mqtt::base().c_str(), sensor_no);
snprintf(topic, sizeof(topic), "sensor/%s/dallassensor_%d/config", Mqtt::base().c_str(), sensor_no);
} else {
// use '_' as HA doesn't like '-' in the topic name
std::string topicname = sensor.to_string();
std::replace(topicname.begin(), topicname.end(), '-', '_');
snprintf_P(topic, sizeof(topic), PSTR("sensor/%s/dallassensor_%s/config"), Mqtt::base().c_str(), topicname.c_str());
snprintf(topic, sizeof(topic), "sensor/%s/dallassensor_%s/config", Mqtt::base().c_str(), topicname.c_str());
}
Mqtt::publish_ha(topic, config.as<JsonObject>());

View File

@@ -130,8 +130,8 @@ class DallasSensor {
int16_t get_temperature_c(const uint8_t addr[]);
uint64_t get_id(const uint8_t addr[]);
bool command_info(const char * value, const int8_t id, JsonObject & json);
bool command_commands(const char * value, const int8_t id, JsonObject & json);
bool command_info(const char * value, const int8_t id, JsonObject & output);
bool command_commands(const char * value, const int8_t id, JsonObject & output);
void delete_ha_config(uint8_t index, const char * name);

View File

@@ -24,10 +24,6 @@
#define EMSESP_DEFAULT_TX_MODE 1 // EMS1.0
#endif
#ifndef EMSESP_DEFAULT_TX_DELAY
#define EMSESP_DEFAULT_TX_DELAY 0 // no delay
#endif
#ifndef EMSESP_DEFAULT_EMS_BUS_ID
#define EMSESP_DEFAULT_EMS_BUS_ID 0x0B // service key
#endif
@@ -96,7 +92,7 @@
#define EMSESP_DEFAULT_BOARD_PROFILE "S32" // Gateway S32
#endif
// Default GPIO PIN definitions - based on Wemos/Nodemcu
// Default GPIO PIN definitions
#ifndef EMSESP_DEFAULT_RX_GPIO
#define EMSESP_DEFAULT_RX_GPIO 23 // D7
@@ -118,6 +114,10 @@
#define EMSESP_DEFAULT_PBUTTON_GPIO 0
#endif
#ifndef EMSESP_DEFAULT_PHY_TYPE
#define EMSESP_DEFAULT_PHY_TYPE 0 // No Ethernet, just Wifi
#endif
// MQTT
#ifndef EMSESP_DEFAULT_BOOL_FORMAT
@@ -152,8 +152,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
@@ -164,4 +164,16 @@
#define EMSESP_DEFAULT_SENSOR_NAME ""
#endif
#ifndef EMSESP_DEFAULT_WEBLOG_LEVEL
#define EMSESP_DEFAULT_WEBLOG_LEVEL 6 // INFO
#endif
#ifndef EMSESP_DEFAULT_WEBLOG_BUFFER
#define EMSESP_DEFAULT_WEBLOG_BUFFER 50
#endif
#ifndef EMSESP_DEFAULT_WEBLOG_COMPACT
#define EMSESP_DEFAULT_WEBLOG_COMPACT true
#endif
#endif

View File

@@ -35,7 +35,8 @@
{133, DeviceType::BOILER, F("Logano GB125/KB195i/Logamatic MC110"), DeviceFlags::EMS_DEVICE_FLAG_NONE},
{167, DeviceType::BOILER, F("Cerapur Aero"), DeviceFlags::EMS_DEVICE_FLAG_NONE},
{170, DeviceType::BOILER, F("Logano GB212"), DeviceFlags::EMS_DEVICE_FLAG_NONE},
{172, DeviceType::BOILER, F("Enviline/Compress 6000AW/Hybrid 7000iAW/SupraEco"), DeviceFlags::EMS_DEVICE_FLAG_HEATPUMP},
{172, DeviceType::BOILER, F("Enviline/Compress 6000AW/Hybrid 7000iAW/SupraEco/Geo 5xx"), DeviceFlags::EMS_DEVICE_FLAG_HEATPUMP},
{173, DeviceType::BOILER, F("Geo 5xx"), DeviceFlags::EMS_DEVICE_FLAG_HEATPUMP},
{195, DeviceType::BOILER, F("Condens 5000i/Greenstar 8000"), DeviceFlags::EMS_DEVICE_FLAG_NONE},
{203, DeviceType::BOILER, F("Logamax U122/Cerapur"), DeviceFlags::EMS_DEVICE_FLAG_NONE},
{208, DeviceType::BOILER, F("Logamax Plus/GB192/Condens GC9000"), DeviceFlags::EMS_DEVICE_FLAG_NONE},
@@ -61,13 +62,15 @@
{218, DeviceType::CONTROLLER, F("M200/RFM200"), DeviceFlags::EMS_DEVICE_FLAG_NONE}, // 0x50
{224, DeviceType::CONTROLLER, F("9000i"), DeviceFlags::EMS_DEVICE_FLAG_NONE}, // 0x09
{230, DeviceType::CONTROLLER, F("BC Base"), DeviceFlags::EMS_DEVICE_FLAG_NONE}, // 0x09
{240, DeviceType::CONTROLLER, F("Rego 3000"), DeviceFlags::EMS_DEVICE_FLAG_NONE}, // 0x09
{241, DeviceType::CONTROLLER, F("Condens 5000i"), DeviceFlags::EMS_DEVICE_FLAG_NONE}, // 0x09
// Thermostat - not currently supporting write operations, like the Easy/100 types - 0x18
{202, DeviceType::THERMOSTAT, F("Logamatic TC100/Moduline Easy"), DeviceFlags::EMS_DEVICE_FLAG_EASY | DeviceFlags::EMS_DEVICE_FLAG_NO_WRITE}, // 0x18, cannot write
{203, DeviceType::THERMOSTAT, F("EasyControl CT200"), DeviceFlags::EMS_DEVICE_FLAG_EASY | DeviceFlags::EMS_DEVICE_FLAG_NO_WRITE}, // 0x18, cannot write
// Thermostat - Buderus/Nefit/Bosch specific - 0x17 / 0x10 / 0x18 / 0x19 / 0x38
// Thermostat - Buderus/Nefit/Bosch specific - 0x17 / 0x10 / 0x18 / 0x19-0x1B for hc2-4 / 0x38
{ 65, DeviceType::THERMOSTAT, F("RC10"), DeviceFlags::EMS_DEVICE_FLAG_RC20_N},// 0x17
{ 67, DeviceType::THERMOSTAT, F("RC30"), DeviceFlags::EMS_DEVICE_FLAG_RC30_N},// 0x10 - based on RC35
{ 77, DeviceType::THERMOSTAT, F("RC20/Moduline 300"), DeviceFlags::EMS_DEVICE_FLAG_RC20},// 0x17
{ 78, DeviceType::THERMOSTAT, F("Moduline 400"), DeviceFlags::EMS_DEVICE_FLAG_RC30}, // 0x10
@@ -77,14 +80,16 @@
{ 90, DeviceType::THERMOSTAT, F("RC10/Moduline 100"), DeviceFlags::EMS_DEVICE_FLAG_RC20_N}, // 0x17
{ 93, DeviceType::THERMOSTAT, F("RC20RF"), DeviceFlags::EMS_DEVICE_FLAG_RC20}, // 0x19
{ 94, DeviceType::THERMOSTAT, F("RFM20 Remote"), DeviceFlags::EMS_DEVICE_FLAG_NONE}, // 0x18
{151, DeviceType::THERMOSTAT, F("RC25"), DeviceFlags::EMS_DEVICE_FLAG_RC20_N}, // 0x17
{157, DeviceType::THERMOSTAT, F("RC200/CW100"), DeviceFlags::EMS_DEVICE_FLAG_RC100}, // 0x18
{158, DeviceType::THERMOSTAT, F("RC300/RC310/Moduline 3000/1010H/CW400/Sense II"), DeviceFlags::EMS_DEVICE_FLAG_RC300}, // 0x10
{165, DeviceType::THERMOSTAT, F("RC100/Moduline 1000/1010"), DeviceFlags::EMS_DEVICE_FLAG_RC100}, // 0x18, 0x38
{172, DeviceType::THERMOSTAT, F("Rego 2000/3000"), DeviceFlags::EMS_DEVICE_FLAG_RC300}, // 0x10
{216, DeviceType::THERMOSTAT, F("CRF200S"), DeviceFlags::EMS_DEVICE_FLAG_CRF | DeviceFlags::EMS_DEVICE_FLAG_NO_WRITE}, // 0x18
// Thermostat - Sieger - 0x10 / 0x17
{ 66, DeviceType::THERMOSTAT, F("ES72/RC20"), DeviceFlags::EMS_DEVICE_FLAG_RC20_N}, // 0x17 or remote
{ 76, DeviceType::THERMOSTAT, F("ES73"), DeviceFlags::EMS_DEVICE_FLAG_RC35}, // 0x10
{ 76, DeviceType::THERMOSTAT, F("ES73"), DeviceFlags::EMS_DEVICE_FLAG_RC30_N}, // 0x10
{113, DeviceType::THERMOSTAT, F("ES72/RC20"), DeviceFlags::EMS_DEVICE_FLAG_RC20_N}, // 0x17
// Thermostat - Junkers - 0x10
@@ -92,7 +97,7 @@
{106, DeviceType::THERMOSTAT, F("FW200"), DeviceFlags::EMS_DEVICE_FLAG_JUNKERS},
{107, DeviceType::THERMOSTAT, F("FR100"), DeviceFlags::EMS_DEVICE_FLAG_JUNKERS | DeviceFlags::EMS_DEVICE_FLAG_JUNKERS_OLD}, // older model
{108, DeviceType::THERMOSTAT, F("FR110"), DeviceFlags::EMS_DEVICE_FLAG_JUNKERS | DeviceFlags::EMS_DEVICE_FLAG_JUNKERS_OLD}, // older model
{111, DeviceType::THERMOSTAT, F("FR10"), DeviceFlags::EMS_DEVICE_FLAG_JUNKERS},
{111, DeviceType::THERMOSTAT, F("FR10"), DeviceFlags::EMS_DEVICE_FLAG_JUNKERS | DeviceFlags::EMS_DEVICE_FLAG_JUNKERS_OLD}, // older model
{147, DeviceType::THERMOSTAT, F("FR50"), DeviceFlags::EMS_DEVICE_FLAG_JUNKERS | DeviceFlags::EMS_DEVICE_FLAG_JUNKERS_OLD},
{191, DeviceType::THERMOSTAT, F("FR120"), DeviceFlags::EMS_DEVICE_FLAG_JUNKERS | DeviceFlags::EMS_DEVICE_FLAG_JUNKERS_OLD}, // older model
{192, DeviceType::THERMOSTAT, F("FW120"), DeviceFlags::EMS_DEVICE_FLAG_JUNKERS},
@@ -110,6 +115,7 @@
{159, DeviceType::MIXER, F("MM50"), DeviceFlags::EMS_DEVICE_FLAG_MMPLUS},
{160, DeviceType::MIXER, F("MM100"), DeviceFlags::EMS_DEVICE_FLAG_MMPLUS},
{161, DeviceType::MIXER, F("MM200"), DeviceFlags::EMS_DEVICE_FLAG_MMPLUS},
{204, DeviceType::MIXER, F("MP100"), DeviceFlags::EMS_DEVICE_FLAG_MP},
// Heat Pumps - 0x38
{200, DeviceType::HEATPUMP, F("HP Module"), DeviceFlags::EMS_DEVICE_FLAG_NONE},
@@ -120,10 +126,16 @@
{205, DeviceType::CONNECT, F("Moduline Easy Connect"), DeviceFlags::EMS_DEVICE_FLAG_NONE},
{206, DeviceType::CONNECT, F("Easy Connect"), DeviceFlags::EMS_DEVICE_FLAG_NONE},
// wireless sensor base- 0x50
{236, DeviceType::CONNECT, F("Wireless sensor base"), DeviceFlags::EMS_DEVICE_FLAG_NONE},
// Switches - 0x11
{ 71, DeviceType::SWITCH, F("WM10"), DeviceFlags::EMS_DEVICE_FLAG_NONE},
// Gateways - 0x48
{189, DeviceType::GATEWAY, F("KM200/MB LAN 2"), DeviceFlags::EMS_DEVICE_FLAG_NONE}
{189, DeviceType::GATEWAY, F("KM200/MB LAN 2"), DeviceFlags::EMS_DEVICE_FLAG_NONE},
// generic - 0x40 or other with no product-id and no version
{0, DeviceType::GENERIC, F("unknown"), DeviceFlags::EMS_DEVICE_FLAG_NONE}
// clang-format on

View File

@@ -26,8 +26,6 @@ uuid::log::Logger Boiler::logger_{F_(boiler), uuid::log::Facility::CONSOLE};
Boiler::Boiler(uint8_t device_type, int8_t device_id, uint8_t product_id, const std::string & version, const std::string & name, uint8_t flags, uint8_t brand)
: EMSdevice(device_type, device_id, product_id, version, name, flags, brand) {
LOG_DEBUG(F("Adding new Boiler with device ID 0x%02X"), device_id);
// cascaded heatingsources, only some values per individual heatsource (hs)
if (device_id != EMSdevice::EMS_DEVICE_ID_BOILER) {
uint8_t hs = device_id - EMSdevice::EMS_DEVICE_ID_BOILER_1; // heating source id, count from 0
@@ -44,6 +42,7 @@ Boiler::Boiler(uint8_t device_type, int8_t device_id, uint8_t product_id, const
register_device_value(TAG_HS1 + hs, &curBurnPow_, DeviceValueType::UINT, nullptr, FL_(curBurnPow), DeviceValueUOM::PERCENT);
return;
}
// register values for master boiler/cascade module
reserve_telgram_functions(25); // reserve some space for the telegram registries, to avoid memory fragmentation
@@ -51,12 +50,13 @@ Boiler::Boiler(uint8_t device_type, int8_t device_id, uint8_t product_id, const
// common for all boilers
register_telegram_type(0x10, F("UBAErrorMessage1"), false, MAKE_PF_CB(process_UBAErrorMessage));
register_telegram_type(0x11, F("UBAErrorMessage2"), false, MAKE_PF_CB(process_UBAErrorMessage));
register_telegram_type(0xC2, F("UBAErrorMessage3"), false, MAKE_PF_CB(process_UBAErrorMessage2));
register_telegram_type(0x14, F("UBATotalUptime"), true, MAKE_PF_CB(process_UBATotalUptime));
register_telegram_type(0x15, F("UBAMaintenanceData"), false, MAKE_PF_CB(process_UBAMaintenanceData));
register_telegram_type(0x1C, F("UBAMaintenanceStatus"), false, MAKE_PF_CB(process_UBAMaintenanceStatus));
// EMS1.0 and maybe EMS+?
register_telegram_type(0x18, F("UBAMonitorFast"), false, MAKE_PF_CB(process_UBAMonitorFast));
register_telegram_type(0x19, F("UBAMonitorSlow"), true, MAKE_PF_CB(process_UBAMonitorSlow));
register_telegram_type(0x19, F("UBAMonitorSlow"), false, MAKE_PF_CB(process_UBAMonitorSlow));
register_telegram_type(0x1A, F("UBASetPoints"), false, MAKE_PF_CB(process_UBASetPoints));
register_telegram_type(0x35, F("UBAFlags"), false, MAKE_PF_CB(process_UBAFlags));
// only EMS 1.0
@@ -68,21 +68,24 @@ Boiler::Boiler(uint8_t device_type, int8_t device_id, uint8_t product_id, const
register_telegram_type(0x26, F("UBASettingsWW"), true, MAKE_PF_CB(process_UBASettingsWW));
register_telegram_type(0x2A, F("MC110Status"), false, MAKE_PF_CB(process_MC110Status));
}
// only EMS+
if (model() != EMSdevice::EMS_DEVICE_FLAG_EMS && model() != EMSdevice::EMS_DEVICE_FLAG_HT3) {
register_telegram_type(0xD1, F("UBAOutdoorTemp"), false, MAKE_PF_CB(process_UBAOutdoorTemp));
register_telegram_type(0xE3, F("UBAMonitorSlowPlus"), false, MAKE_PF_CB(process_UBAMonitorSlowPlus2));
register_telegram_type(0xE3, F("UBAMonitorSlowPlus2"), false, MAKE_PF_CB(process_UBAMonitorSlowPlus2));
register_telegram_type(0xE4, F("UBAMonitorFastPlus"), false, MAKE_PF_CB(process_UBAMonitorFastPlus));
register_telegram_type(0xE5, F("UBAMonitorSlowPlus"), false, MAKE_PF_CB(process_UBAMonitorSlowPlus));
register_telegram_type(0xE6, F("UBAParametersPlus"), true, MAKE_PF_CB(process_UBAParametersPlus));
register_telegram_type(0xE9, F("UBAMonitorWWPlus"), false, MAKE_PF_CB(process_UBAMonitorWWPlus));
register_telegram_type(0xEA, F("UBAParameterWWPlus"), true, MAKE_PF_CB(process_UBAParameterWWPlus));
}
if (model() == EMSdevice::EMS_DEVICE_FLAG_HEATPUMP) {
register_telegram_type(0x494, F("UBAEnergySupplied"), false, MAKE_PF_CB(process_UBAEnergySupplied));
register_telegram_type(0x495, F("UBAInformation"), false, MAKE_PF_CB(process_UBAInformation));
register_telegram_type(0x48D, F("HpPower"), false, MAKE_PF_CB(process_HpPower));
register_telegram_type(0x48D, F("HpPower"), true, MAKE_PF_CB(process_HpPower));
register_telegram_type(0x48F, F("HpOutdoor"), false, MAKE_PF_CB(process_HpOutdoor));
register_telegram_type(0x48A, F("HpPool"), true, MAKE_PF_CB(process_HpPool));
}
// MQTT commands for boiler topic
@@ -96,13 +99,14 @@ Boiler::Boiler(uint8_t device_type, int8_t device_id, uint8_t product_id, const
DeviceValueType::CMD,
FL_(enum_bool),
FL_(wwtapactivated),
DeviceValueUOM::BOOLEAN,
DeviceValueUOM::NONE,
MAKE_CF_CB(set_tapwarmwater_activated));
// reset is a command, so uses a dummy variable which is unused. It will not be shown in MQTT, Web or Console
register_device_value(TAG_BOILER_DATA, &dummy8u_, DeviceValueType::CMD, FL_(enum_reset), FL_(reset), DeviceValueUOM::LIST, MAKE_CF_CB(set_reset));
register_device_value(TAG_BOILER_DATA, &heatingActive_, DeviceValueType::BOOL, nullptr, FL_(heatingActive), DeviceValueUOM::BOOLEAN);
register_device_value(TAG_BOILER_DATA, &tapwaterActive_, DeviceValueType::BOOL, nullptr, FL_(tapwaterActive), DeviceValueUOM::BOOLEAN);
// reset is a command, so uses a dummy variable which is unused. It will not be shown in MQTT, Web or Console
register_device_value(TAG_BOILER_DATA, &dummy8u_, DeviceValueType::CMD, FL_(enum_reset), FL_(reset), DeviceValueUOM::NONE, MAKE_CF_CB(set_reset));
register_device_value(TAG_BOILER_DATA, &heatingActive_, DeviceValueType::BOOL, nullptr, FL_(heatingActive), DeviceValueUOM::NONE);
register_device_value(TAG_BOILER_DATA, &tapwaterActive_, DeviceValueType::BOOL, nullptr, FL_(tapwaterActive), DeviceValueUOM::NONE);
register_device_value(TAG_BOILER_DATA, &selFlowTemp_, DeviceValueType::UINT, nullptr, FL_(selFlowTemp), DeviceValueUOM::DEGREES, MAKE_CF_CB(set_flow_temp));
register_device_value(TAG_BOILER_DATA, &selBurnPow_, DeviceValueType::UINT, nullptr, FL_(selBurnPow), DeviceValueUOM::PERCENT, MAKE_CF_CB(set_burn_power));
register_device_value(TAG_BOILER_DATA, &heatingPumpMod_, DeviceValueType::UINT, nullptr, FL_(heatingPumpMod), DeviceValueUOM::PERCENT);
@@ -114,13 +118,13 @@ Boiler::Boiler(uint8_t device_type, int8_t device_id, uint8_t product_id, const
register_device_value(TAG_BOILER_DATA, &sysPress_, DeviceValueType::UINT, FL_(div10), FL_(sysPress), DeviceValueUOM::BAR);
register_device_value(TAG_BOILER_DATA, &boilTemp_, DeviceValueType::USHORT, FL_(div10), FL_(boilTemp), DeviceValueUOM::DEGREES);
register_device_value(TAG_BOILER_DATA, &exhaustTemp_, DeviceValueType::USHORT, FL_(div10), FL_(exhaustTemp), DeviceValueUOM::DEGREES);
register_device_value(TAG_BOILER_DATA, &burnGas_, DeviceValueType::BOOL, nullptr, FL_(burnGas), DeviceValueUOM::BOOLEAN);
register_device_value(TAG_BOILER_DATA, &burnGas_, DeviceValueType::BOOL, nullptr, FL_(burnGas), DeviceValueUOM::NONE);
register_device_value(TAG_BOILER_DATA, &flameCurr_, DeviceValueType::USHORT, FL_(div10), FL_(flameCurr), DeviceValueUOM::UA);
register_device_value(TAG_BOILER_DATA, &heatingPump_, DeviceValueType::BOOL, nullptr, FL_(heatingPump), DeviceValueUOM::BOOLEAN);
register_device_value(TAG_BOILER_DATA, &fanWork_, DeviceValueType::BOOL, nullptr, FL_(fanWork), DeviceValueUOM::BOOLEAN);
register_device_value(TAG_BOILER_DATA, &ignWork_, DeviceValueType::BOOL, nullptr, FL_(ignWork), DeviceValueUOM::BOOLEAN);
register_device_value(TAG_BOILER_DATA, &heatingPump_, DeviceValueType::BOOL, nullptr, FL_(heatingPump), DeviceValueUOM::NONE);
register_device_value(TAG_BOILER_DATA, &fanWork_, DeviceValueType::BOOL, nullptr, FL_(fanWork), DeviceValueUOM::NONE);
register_device_value(TAG_BOILER_DATA, &ignWork_, DeviceValueType::BOOL, nullptr, FL_(ignWork), DeviceValueUOM::NONE);
register_device_value(
TAG_BOILER_DATA, &heatingActivated_, DeviceValueType::BOOL, nullptr, FL_(heatingActivated), DeviceValueUOM::BOOLEAN, MAKE_CF_CB(set_heating_activated));
TAG_BOILER_DATA, &heatingActivated_, DeviceValueType::BOOL, nullptr, FL_(heatingActivated), DeviceValueUOM::NONE, MAKE_CF_CB(set_heating_activated));
register_device_value(TAG_BOILER_DATA, &heatingTemp_, DeviceValueType::UINT, nullptr, FL_(heatingTemp), DeviceValueUOM::DEGREES, MAKE_CF_CB(set_heating_temp));
register_device_value(TAG_BOILER_DATA, &pumpModMax_, DeviceValueType::UINT, nullptr, FL_(pumpModMax), DeviceValueUOM::PERCENT, MAKE_CF_CB(set_max_pump));
register_device_value(TAG_BOILER_DATA, &pumpModMin_, DeviceValueType::UINT, nullptr, FL_(pumpModMin), DeviceValueUOM::PERCENT, MAKE_CF_CB(set_min_pump));
@@ -133,46 +137,67 @@ Boiler::Boiler(uint8_t device_type, int8_t device_id, uint8_t product_id, const
register_device_value(TAG_BOILER_DATA, &setFlowTemp_, DeviceValueType::UINT, nullptr, FL_(setFlowTemp), DeviceValueUOM::DEGREES);
register_device_value(TAG_BOILER_DATA, &setBurnPow_, DeviceValueType::UINT, nullptr, FL_(setBurnPow), DeviceValueUOM::PERCENT);
register_device_value(TAG_BOILER_DATA, &curBurnPow_, DeviceValueType::UINT, nullptr, FL_(curBurnPow), DeviceValueUOM::PERCENT);
register_device_value(TAG_BOILER_DATA, &burnStarts_, DeviceValueType::ULONG, nullptr, FL_(burnStarts), DeviceValueUOM::NUM);
register_device_value(TAG_BOILER_DATA, &burnStarts_, DeviceValueType::ULONG, nullptr, FL_(burnStarts), DeviceValueUOM::TIMES);
register_device_value(TAG_BOILER_DATA, &burnWorkMin_, DeviceValueType::TIME, nullptr, FL_(burnWorkMin), DeviceValueUOM::MINUTES);
register_device_value(TAG_BOILER_DATA, &heatWorkMin_, DeviceValueType::TIME, nullptr, FL_(heatWorkMin), DeviceValueUOM::MINUTES);
register_device_value(TAG_BOILER_DATA, &UBAuptime_, DeviceValueType::TIME, nullptr, FL_(UBAuptime), DeviceValueUOM::MINUTES);
register_device_value(TAG_BOILER_DATA, &lastCode_, DeviceValueType::TEXT, nullptr, FL_(lastCode), DeviceValueUOM::NONE);
register_device_value(TAG_BOILER_DATA, &serviceCode_, DeviceValueType::TEXT, nullptr, FL_(serviceCode), DeviceValueUOM::NONE);
register_device_value(TAG_BOILER_DATA, &lastCode_, DeviceValueType::STRING, nullptr, FL_(lastCode), DeviceValueUOM::NONE);
register_device_value(TAG_BOILER_DATA, &serviceCode_, DeviceValueType::STRING, nullptr, FL_(serviceCode), DeviceValueUOM::NONE);
register_device_value(TAG_BOILER_DATA, &serviceCodeNumber_, DeviceValueType::USHORT, nullptr, FL_(serviceCodeNumber), DeviceValueUOM::NONE);
register_device_value(TAG_BOILER_DATA, &maintenanceMessage_, DeviceValueType::TEXT, nullptr, FL_(maintenanceMessage), DeviceValueUOM::NONE);
register_device_value(TAG_BOILER_DATA, &maintenanceMessage_, DeviceValueType::STRING, nullptr, FL_(maintenanceMessage), DeviceValueUOM::NONE);
register_device_value(TAG_BOILER_DATA,
&maintenanceType_,
DeviceValueType::ENUM,
FL_(enum_off_time_date),
FL_(maintenanceType),
DeviceValueUOM::LIST,
DeviceValueUOM::NONE,
MAKE_CF_CB(set_maintenance));
register_device_value(
TAG_BOILER_DATA, &maintenanceTime_, DeviceValueType::USHORT, nullptr, FL_(maintenanceTime), DeviceValueUOM::HOURS, MAKE_CF_CB(set_maintenancetime));
register_device_value(
TAG_BOILER_DATA, &maintenanceDate_, DeviceValueType::TEXT, nullptr, FL_(maintenanceDate), DeviceValueUOM::NONE, MAKE_CF_CB(set_maintenancedate));
TAG_BOILER_DATA, &maintenanceDate_, DeviceValueType::STRING, nullptr, FL_(maintenanceDate), DeviceValueUOM::NONE, MAKE_CF_CB(set_maintenancedate));
// heatpump info
if (model() == EMS_DEVICE_FLAG_HEATPUMP) {
register_device_value(TAG_BOILER_DATA, &upTimeControl_, DeviceValueType::TIME, FL_(div60), FL_(upTimeControl), DeviceValueUOM::MINUTES);
register_device_value(TAG_BOILER_DATA, &upTimeCompHeating_, DeviceValueType::TIME, FL_(div60), FL_(upTimeCompHeating), DeviceValueUOM::MINUTES);
register_device_value(TAG_BOILER_DATA, &upTimeCompCooling_, DeviceValueType::TIME, FL_(div60), FL_(upTimeCompCooling), DeviceValueUOM::MINUTES);
register_device_value(TAG_BOILER_DATA, &upTimeCompWw_, DeviceValueType::TIME, FL_(div60), FL_(upTimeCompWw), DeviceValueUOM::MINUTES);
register_device_value(TAG_BOILER_DATA, &heatingStarts_, DeviceValueType::ULONG, nullptr, FL_(heatingStarts), DeviceValueUOM::NUM);
register_device_value(TAG_BOILER_DATA, &coolingStarts_, DeviceValueType::ULONG, nullptr, FL_(coolingStarts), DeviceValueUOM::NUM);
register_device_value(TAG_BOILER_DATA, &upTimeCompPool_, DeviceValueType::TIME, FL_(div60), FL_(upTimeCompPool), DeviceValueUOM::MINUTES);
register_device_value(TAG_BOILER_DATA, &totalCompStarts_, DeviceValueType::ULONG, nullptr, FL_(totalcompStarts), DeviceValueUOM::TIMES);
register_device_value(TAG_BOILER_DATA, &heatingStarts_, DeviceValueType::ULONG, nullptr, FL_(heatingStarts), DeviceValueUOM::TIMES);
register_device_value(TAG_BOILER_DATA, &coolingStarts_, DeviceValueType::ULONG, nullptr, FL_(coolingStarts), DeviceValueUOM::TIMES);
register_device_value(TAG_BOILER_DATA, &wwStarts2_, DeviceValueType::ULONG, nullptr, FL_(wwStarts2), DeviceValueUOM::TIMES);
register_device_value(TAG_BOILER_DATA, &poolStarts_, DeviceValueType::ULONG, nullptr, FL_(poolStarts), DeviceValueUOM::TIMES);
register_device_value(TAG_BOILER_DATA, &nrgConsTotal_, DeviceValueType::ULONG, nullptr, FL_(nrgConsTotal), DeviceValueUOM::KWH);
register_device_value(TAG_BOILER_DATA, &nrgConsCompTotal_, DeviceValueType::ULONG, nullptr, FL_(nrgConsCompTotal), DeviceValueUOM::KWH);
register_device_value(TAG_BOILER_DATA, &nrgConsCompHeating_, DeviceValueType::ULONG, nullptr, FL_(nrgConsCompHeating), DeviceValueUOM::KWH);
register_device_value(TAG_BOILER_DATA, &nrgConsCompWw_, DeviceValueType::ULONG, nullptr, FL_(nrgConsCompWw), DeviceValueUOM::KWH);
register_device_value(TAG_BOILER_DATA, &nrgConsCompCooling_, DeviceValueType::ULONG, nullptr, FL_(nrgConsCompCooling), DeviceValueUOM::KWH);
register_device_value(TAG_BOILER_DATA, &nrgConsCompPool_, DeviceValueType::ULONG, nullptr, FL_(nrgConsCompPool), DeviceValueUOM::KWH);
register_device_value(TAG_BOILER_DATA, &auxElecHeatNrgConsTotal_, DeviceValueType::ULONG, nullptr, FL_(auxElecHeatNrgConsTotal), DeviceValueUOM::KWH);
register_device_value(TAG_BOILER_DATA, &auxElecHeatNrgConsHeating_, DeviceValueType::ULONG, nullptr, FL_(auxElecHeatNrgConsHeating), DeviceValueUOM::KWH);
register_device_value(TAG_BOILER_DATA, &auxElecHeatNrgConsWW_, DeviceValueType::ULONG, nullptr, FL_(auxElecHeatNrgConsWW), DeviceValueUOM::KWH);
register_device_value(TAG_BOILER_DATA, &auxElecHeatNrgConsPool_, DeviceValueType::ULONG, nullptr, FL_(auxElecHeatNrgConsPool), DeviceValueUOM::KWH);
register_device_value(TAG_BOILER_DATA, &nrgSuppTotal_, DeviceValueType::ULONG, nullptr, FL_(nrgSuppTotal), DeviceValueUOM::KWH);
register_device_value(TAG_BOILER_DATA, &nrgSuppHeating_, DeviceValueType::ULONG, nullptr, FL_(nrgSuppHeating), DeviceValueUOM::KWH);
register_device_value(TAG_BOILER_DATA, &nrgSuppWw_, DeviceValueType::ULONG, nullptr, FL_(nrgSuppWw), DeviceValueUOM::KWH);
register_device_value(TAG_BOILER_DATA, &nrgSuppCooling_, DeviceValueType::ULONG, nullptr, FL_(nrgSuppCooling), DeviceValueUOM::KWH);
register_device_value(TAG_BOILER_DATA, &nrgSuppPool_, DeviceValueType::ULONG, nullptr, FL_(nrgSuppPool), DeviceValueUOM::KWH);
register_device_value(TAG_BOILER_DATA, &hpPower_, DeviceValueType::UINT, FL_(div10), FL_(hpPower), DeviceValueUOM::KW);
register_device_value(TAG_BOILER_DATA, &hpCompOn_, DeviceValueType::BOOL, nullptr, FL_(hpCompOn), DeviceValueUOM::NONE);
register_device_value(TAG_BOILER_DATA, &hpActivity_, DeviceValueType::ENUM, FL_(enum_hpactivity), FL_(hpActivity), DeviceValueUOM::NONE);
register_device_value(TAG_BOILER_DATA, &hpHeatingOn_, DeviceValueType::BOOL, nullptr, FL_(hpHeatingOn), DeviceValueUOM::NONE);
register_device_value(TAG_BOILER_DATA, &hpCoolingOn_, DeviceValueType::BOOL, nullptr, FL_(hpCoolingOn), DeviceValueUOM::NONE);
register_device_value(TAG_BOILER_DATA, &hpWwOn_, DeviceValueType::BOOL, nullptr, FL_(hpWwOn), DeviceValueUOM::NONE);
register_device_value(TAG_BOILER_DATA, &hpPoolOn_, DeviceValueType::BOOL, nullptr, FL_(hpPoolOn), DeviceValueUOM::NONE);
register_device_value(TAG_BOILER_DATA, &hpBrinePumpSpd_, DeviceValueType::UINT, nullptr, FL_(hpBrinePumpSpd), DeviceValueUOM::PERCENT);
register_device_value(TAG_BOILER_DATA, &hpSwitchValve_, DeviceValueType::BOOL, nullptr, FL_(hpSwitchValve), DeviceValueUOM::NONE);
register_device_value(TAG_BOILER_DATA, &hpCompSpd_, DeviceValueType::UINT, nullptr, FL_(hpCompSpd), DeviceValueUOM::PERCENT);
register_device_value(TAG_BOILER_DATA, &hpCircSpd_, DeviceValueType::UINT, nullptr, FL_(hpCircSpd), DeviceValueUOM::PERCENT);
register_device_value(TAG_BOILER_DATA, &hpBrineIn_, DeviceValueType::SHORT, FL_(div10), FL_(hpBrineIn), DeviceValueUOM::DEGREES);
register_device_value(TAG_BOILER_DATA, &hpBrineOut_, DeviceValueType::SHORT, FL_(div10), FL_(hpBrineOut), DeviceValueUOM::DEGREES);
register_device_value(TAG_BOILER_DATA, &hpSuctionGas_, DeviceValueType::SHORT, FL_(div10), FL_(hpSuctionGas), DeviceValueUOM::DEGREES);
register_device_value(TAG_BOILER_DATA, &hpHotGas_, DeviceValueType::SHORT, FL_(div10), FL_(hpHotGas), DeviceValueUOM::DEGREES);
register_device_value(TAG_BOILER_DATA, &hpTc0_, DeviceValueType::SHORT, FL_(div10), FL_(hpTc0), DeviceValueUOM::DEGREES);
register_device_value(TAG_BOILER_DATA, &hpTc1_, DeviceValueType::SHORT, FL_(div10), FL_(hpTc1), DeviceValueUOM::DEGREES);
register_device_value(TAG_BOILER_DATA, &hpTc3_, DeviceValueType::SHORT, FL_(div10), FL_(hpTc3), DeviceValueUOM::DEGREES);
@@ -184,78 +209,71 @@ Boiler::Boiler(uint8_t device_type, int8_t device_id, uint8_t product_id, const
register_device_value(TAG_BOILER_DATA, &hpTl2_, DeviceValueType::SHORT, FL_(div10), FL_(hpTl2), DeviceValueUOM::DEGREES);
register_device_value(TAG_BOILER_DATA, &hpPl1_, DeviceValueType::SHORT, FL_(div10), FL_(hpPl1), DeviceValueUOM::DEGREES);
register_device_value(TAG_BOILER_DATA, &hpPh1_, DeviceValueType::SHORT, FL_(div10), FL_(hpPh1), DeviceValueUOM::DEGREES);
register_device_value(TAG_BOILER_DATA, &poolSetTemp_, DeviceValueType::UINT, FL_(div2), FL_(poolSetTemp), DeviceValueUOM::DEGREES, MAKE_CF_CB(set_pool_temp));
}
// warm water - boiler_data_ww topic
register_device_value(TAG_BOILER_DATA_WW, &wwSelTemp_, DeviceValueType::UINT, nullptr, FL_(wwSelTemp), DeviceValueUOM::DEGREES, MAKE_CF_CB(set_warmwater_temp));
register_device_value(TAG_BOILER_DATA_WW, &wwSetTemp_, DeviceValueType::UINT, nullptr, FL_(wwSetTemp), DeviceValueUOM::DEGREES);
register_device_value(TAG_BOILER_DATA_WW, &wwType_, DeviceValueType::ENUM, FL_(enum_flow), FL_(wwType), DeviceValueUOM::NONE);
register_device_value(TAG_DEVICE_DATA_WW, &wwSelTemp_, DeviceValueType::UINT, nullptr, FL_(wwSelTemp), DeviceValueUOM::DEGREES, MAKE_CF_CB(set_ww_temp));
register_device_value(TAG_DEVICE_DATA_WW, &wwSetTemp_, DeviceValueType::UINT, nullptr, FL_(wwSetTemp), DeviceValueUOM::DEGREES);
register_device_value(TAG_DEVICE_DATA_WW, &wwSelTempLow_, DeviceValueType::UINT, nullptr, FL_(wwSelTempLow), DeviceValueUOM::DEGREES, MAKE_CF_CB(set_ww_temp_low));
register_device_value(TAG_DEVICE_DATA_WW, &wwSelTempOff_, DeviceValueType::UINT, nullptr, FL_(wwSelTempOff), DeviceValueUOM::DEGREES);
register_device_value(
TAG_BOILER_DATA_WW, &wwComfort_, DeviceValueType::ENUM, FL_(enum_comfort), FL_(wwComfort), DeviceValueUOM::LIST, MAKE_CF_CB(set_warmwater_mode));
TAG_DEVICE_DATA_WW, &wwSelTempSingle_, DeviceValueType::UINT, nullptr, FL_(wwSelTempSingle), DeviceValueUOM::DEGREES, MAKE_CF_CB(set_ww_temp_single));
register_device_value(TAG_DEVICE_DATA_WW, &wwType_, DeviceValueType::ENUM, FL_(enum_flow), FL_(wwType), DeviceValueUOM::NONE);
register_device_value(TAG_DEVICE_DATA_WW, &wwComfort_, DeviceValueType::ENUM, FL_(enum_comfort), FL_(wwComfort), DeviceValueUOM::NONE, MAKE_CF_CB(set_ww_mode));
register_device_value(
TAG_BOILER_DATA_WW, &wwFlowTempOffset_, DeviceValueType::UINT, nullptr, FL_(wwFlowTempOffset), DeviceValueUOM::NONE, MAKE_CF_CB(set_wWFlowTempOffset));
TAG_DEVICE_DATA_WW, &wwFlowTempOffset_, DeviceValueType::UINT, nullptr, FL_(wwFlowTempOffset), DeviceValueUOM::NONE, MAKE_CF_CB(set_ww_flowTempOffset));
register_device_value(TAG_DEVICE_DATA_WW, &wwMaxPower_, DeviceValueType::UINT, nullptr, FL_(wwMaxPower), DeviceValueUOM::PERCENT, MAKE_CF_CB(set_ww_maxpower));
register_device_value(
TAG_BOILER_DATA_WW, &wwMaxPower_, DeviceValueType::UINT, nullptr, FL_(wwMaxPower), DeviceValueUOM::PERCENT, MAKE_CF_CB(set_warmwater_maxpower));
register_device_value(
TAG_BOILER_DATA_WW, &wwCircPump_, DeviceValueType::BOOL, nullptr, FL_(wwCircPump), DeviceValueUOM::BOOLEAN, MAKE_CF_CB(set_warmwater_circulation_pump));
register_device_value(TAG_BOILER_DATA_WW, &wWChargeType_, DeviceValueType::ENUM, FL_(enum_charge), FL_(wwChargeType), DeviceValueUOM::LIST);
register_device_value(TAG_BOILER_DATA_WW, &wwHystOn_, DeviceValueType::INT, nullptr, FL_(wwHystOn), DeviceValueUOM::DEGREES, MAKE_CF_CB(set_ww_hyst_on));
register_device_value(TAG_BOILER_DATA_WW, &wwHystOff_, DeviceValueType::INT, nullptr, FL_(wwHystOff), DeviceValueUOM::DEGREES, MAKE_CF_CB(set_ww_hyst_off));
register_device_value(TAG_BOILER_DATA_WW,
TAG_DEVICE_DATA_WW, &wwCircPump_, DeviceValueType::BOOL, nullptr, FL_(wwCircPump), DeviceValueUOM::NONE, MAKE_CF_CB(set_ww_circulation_pump));
register_device_value(TAG_DEVICE_DATA_WW, &wwChargeType_, DeviceValueType::ENUM, FL_(enum_charge), FL_(wwChargeType), DeviceValueUOM::NONE);
register_device_value(TAG_DEVICE_DATA_WW, &wwHystOn_, DeviceValueType::INT, nullptr, FL_(wwHystOn), DeviceValueUOM::DEGREES, MAKE_CF_CB(set_ww_hyst_on));
register_device_value(TAG_DEVICE_DATA_WW, &wwHystOff_, DeviceValueType::INT, nullptr, FL_(wwHystOff), DeviceValueUOM::DEGREES, MAKE_CF_CB(set_ww_hyst_off));
register_device_value(TAG_DEVICE_DATA_WW,
&wwDisinfectionTemp_,
DeviceValueType::UINT,
nullptr,
FL_(wwDisinfectionTemp),
DeviceValueUOM::DEGREES,
MAKE_CF_CB(set_disinfect_temp));
register_device_value(TAG_BOILER_DATA_WW,
&wwCircMode_,
DeviceValueType::ENUM,
FL_(enum_freq),
FL_(wwCircMode),
DeviceValueUOM::LIST,
MAKE_CF_CB(set_warmwater_circulation_mode));
register_device_value(TAG_BOILER_DATA_WW, &wwCirc_, DeviceValueType::BOOL, nullptr, FL_(wwCirc), DeviceValueUOM::BOOLEAN, MAKE_CF_CB(set_warmwater_circulation));
register_device_value(TAG_BOILER_DATA_WW, &wwCurTemp_, DeviceValueType::USHORT, FL_(div10), FL_(wwCurTemp), DeviceValueUOM::DEGREES);
register_device_value(TAG_BOILER_DATA_WW, &wwCurTemp2_, DeviceValueType::USHORT, FL_(div10), FL_(wwCurTemp2), DeviceValueUOM::DEGREES);
register_device_value(TAG_BOILER_DATA_WW, &wwCurFlow_, DeviceValueType::UINT, FL_(div10), FL_(wwCurFlow), DeviceValueUOM::LMIN);
register_device_value(TAG_BOILER_DATA_WW, &wwStorageTemp1_, DeviceValueType::USHORT, FL_(div10), FL_(wwStorageTemp1), DeviceValueUOM::DEGREES);
register_device_value(TAG_BOILER_DATA_WW, &wwStorageTemp2_, DeviceValueType::USHORT, FL_(div10), FL_(wwStorageTemp2), DeviceValueUOM::DEGREES);
MAKE_CF_CB(set_ww_disinfect_temp));
register_device_value(
TAG_BOILER_DATA_WW, &wwActivated_, DeviceValueType::BOOL, nullptr, FL_(wwActivated), DeviceValueUOM::BOOLEAN, MAKE_CF_CB(set_warmwater_activated));
register_device_value(TAG_BOILER_DATA_WW, &wwOneTime_, DeviceValueType::BOOL, nullptr, FL_(wwOneTime), DeviceValueUOM::BOOLEAN, MAKE_CF_CB(set_warmwater_onetime));
register_device_value(TAG_BOILER_DATA_WW, &wwDisinfecting_, DeviceValueType::BOOL, nullptr, FL_(wwDisinfecting), DeviceValueUOM::BOOLEAN);
register_device_value(TAG_BOILER_DATA_WW, &wwCharging_, DeviceValueType::BOOL, nullptr, FL_(wwCharging), DeviceValueUOM::BOOLEAN);
register_device_value(TAG_BOILER_DATA_WW, &wwRecharging_, DeviceValueType::BOOL, nullptr, FL_(wwRecharging), DeviceValueUOM::BOOLEAN);
register_device_value(TAG_BOILER_DATA_WW, &wwTempOK_, DeviceValueType::BOOL, nullptr, FL_(wwTempOK), DeviceValueUOM::BOOLEAN);
register_device_value(TAG_BOILER_DATA_WW, &wwActive_, DeviceValueType::BOOL, nullptr, FL_(wwActive), DeviceValueUOM::BOOLEAN);
register_device_value(TAG_BOILER_DATA_WW, &wwHeat_, DeviceValueType::BOOL, nullptr, FL_(wwHeat), DeviceValueUOM::BOOLEAN);
register_device_value(TAG_BOILER_DATA_WW, &wwSetPumpPower_, DeviceValueType::UINT, nullptr, FL_(wwSetPumpPower), DeviceValueUOM::PERCENT);
register_device_value(TAG_BOILER_DATA_WW, &wwMixerTemp_, DeviceValueType::USHORT, FL_(div10), FL_(wwMixerTemp), DeviceValueUOM::DEGREES);
register_device_value(TAG_BOILER_DATA_WW, &wwTankMiddleTemp_, DeviceValueType::USHORT, FL_(div10), FL_(wwTankMiddleTemp), DeviceValueUOM::DEGREES);
register_device_value(TAG_BOILER_DATA_WW, &wWStarts_, DeviceValueType::ULONG, nullptr, FL_(wwStarts), DeviceValueUOM::NUM);
register_device_value(TAG_BOILER_DATA_WW, &wwStarts2_, DeviceValueType::ULONG, nullptr, FL_(wwStarts2), DeviceValueUOM::NUM);
register_device_value(TAG_BOILER_DATA_WW, &wwWorkM_, DeviceValueType::TIME, nullptr, FL_(wwWorkM), DeviceValueUOM::MINUTES);
TAG_DEVICE_DATA_WW, &wwCircMode_, DeviceValueType::ENUM, FL_(enum_freq), FL_(wwCircMode), DeviceValueUOM::NONE, MAKE_CF_CB(set_ww_circulation_mode));
register_device_value(TAG_DEVICE_DATA_WW, &wwCirc_, DeviceValueType::BOOL, nullptr, FL_(wwCirc), DeviceValueUOM::NONE, MAKE_CF_CB(set_ww_circulation));
register_device_value(TAG_DEVICE_DATA_WW, &wwCurTemp_, DeviceValueType::USHORT, FL_(div10), FL_(wwCurTemp), DeviceValueUOM::DEGREES);
register_device_value(TAG_DEVICE_DATA_WW, &wwCurTemp2_, DeviceValueType::USHORT, FL_(div10), FL_(wwCurTemp2), DeviceValueUOM::DEGREES);
register_device_value(TAG_DEVICE_DATA_WW, &wwCurFlow_, DeviceValueType::UINT, FL_(div10), FL_(wwCurFlow), DeviceValueUOM::LMIN);
register_device_value(TAG_DEVICE_DATA_WW, &wwStorageTemp1_, DeviceValueType::USHORT, FL_(div10), FL_(wwStorageTemp1), DeviceValueUOM::DEGREES);
register_device_value(TAG_DEVICE_DATA_WW, &wwStorageTemp2_, DeviceValueType::USHORT, FL_(div10), FL_(wwStorageTemp2), DeviceValueUOM::DEGREES);
register_device_value(TAG_DEVICE_DATA_WW, &wwActivated_, DeviceValueType::BOOL, nullptr, FL_(wwActivated), DeviceValueUOM::NONE, MAKE_CF_CB(set_ww_activated));
register_device_value(TAG_DEVICE_DATA_WW, &wwOneTime_, DeviceValueType::BOOL, nullptr, FL_(wwOneTime), DeviceValueUOM::NONE, MAKE_CF_CB(set_ww_onetime));
register_device_value(TAG_DEVICE_DATA_WW, &wwDisinfect_, DeviceValueType::BOOL, nullptr, FL_(wwDisinfect), DeviceValueUOM::NONE, MAKE_CF_CB(set_ww_disinfect));
register_device_value(TAG_DEVICE_DATA_WW, &wwCharging_, DeviceValueType::BOOL, nullptr, FL_(wwCharging), DeviceValueUOM::NONE);
register_device_value(TAG_DEVICE_DATA_WW, &wwRecharging_, DeviceValueType::BOOL, nullptr, FL_(wwRecharging), DeviceValueUOM::NONE);
register_device_value(TAG_DEVICE_DATA_WW, &wwTempOK_, DeviceValueType::BOOL, nullptr, FL_(wwTempOK), DeviceValueUOM::NONE);
register_device_value(TAG_DEVICE_DATA_WW, &wwActive_, DeviceValueType::BOOL, nullptr, FL_(wwActive), DeviceValueUOM::NONE);
register_device_value(TAG_DEVICE_DATA_WW, &wwHeat_, DeviceValueType::BOOL, nullptr, FL_(wwHeat), DeviceValueUOM::NONE);
register_device_value(TAG_DEVICE_DATA_WW, &wwSetPumpPower_, DeviceValueType::UINT, nullptr, FL_(wwSetPumpPower), DeviceValueUOM::PERCENT);
register_device_value(TAG_DEVICE_DATA_WW, &wwMixerTemp_, DeviceValueType::USHORT, FL_(div10), FL_(wwMixerTemp), DeviceValueUOM::DEGREES);
register_device_value(TAG_DEVICE_DATA_WW, &wwTankMiddleTemp_, DeviceValueType::USHORT, FL_(div10), FL_(wwTankMiddleTemp), DeviceValueUOM::DEGREES);
register_device_value(TAG_DEVICE_DATA_WW, &wwStarts_, DeviceValueType::ULONG, nullptr, FL_(wwStarts), DeviceValueUOM::TIMES);
register_device_value(TAG_DEVICE_DATA_WW, &wwWorkM_, DeviceValueType::TIME, nullptr, FL_(wwWorkM), DeviceValueUOM::MINUTES);
// fetch some initial data
EMSESP::send_read_request(0x10,
device_id); // read last errorcode on start (only published on errors)
EMSESP::send_read_request(0x11,
device_id); // read last errorcode on start (only published on errors)
EMSESP::send_read_request(0x15,
device_id); // read maintenace data on start (only published on change)
EMSESP::send_read_request(0x1C,
device_id); // read maintenace status on start (only published on change)
EMSESP::send_read_request(0x10, device_id); // read last errorcode on start (only published on errors)
EMSESP::send_read_request(0x11, device_id); // read last errorcode on start (only published on errors)
EMSESP::send_read_request(0xC2, device_id); // read last errorcode on start (only published on errors)
EMSESP::send_read_request(0x15, device_id); // read maintenace data on start (only published on change)
EMSESP::send_read_request(0x1C, device_id); // read maintenace status on start (only published on change)
}
// publish HA config
bool Boiler::publish_ha_config() {
bool Boiler::publish_ha_device_config() {
StaticJsonDocument<EMSESP_JSON_SIZE_HA_CONFIG> doc;
doc["uniq_id"] = F_(boiler);
doc["ic"] = F_(icondevice);
char stat_t[Mqtt::MQTT_TOPIC_MAX_SIZE];
snprintf_P(stat_t, sizeof(stat_t), PSTR("%s/%s"), Mqtt::base().c_str(), Mqtt::tag_to_topic(device_type(), DeviceValueTAG::TAG_NONE).c_str());
snprintf(stat_t, sizeof(stat_t), "%s/%s", Mqtt::base().c_str(), Mqtt::tag_to_topic(device_type(), DeviceValueTAG::TAG_NONE).c_str());
doc["stat_t"] = stat_t;
char name_s[40];
@@ -272,7 +290,7 @@ bool Boiler::publish_ha_config() {
ids.add("ems-esp-boiler");
char topic[Mqtt::MQTT_TOPIC_MAX_SIZE];
snprintf_P(topic, sizeof(topic), PSTR("sensor/%s/boiler/config"), Mqtt::base().c_str());
snprintf(topic, sizeof(topic), "sensor/%s/boiler/config", Mqtt::base().c_str());
Mqtt::publish_ha(topic,
doc.as<JsonObject>()); // publish the config payload with retain flag
@@ -335,7 +353,7 @@ void Boiler::process_UBAParameterWW(std::shared_ptr<const Telegram> telegram) {
has_update(telegram->read_value(wwCircPump_, 6)); // 0xFF means on
has_update(telegram->read_value(wwCircMode_, 7)); // 1=1x3min 6=6x3min 7=continuous
has_update(telegram->read_value(wwDisinfectionTemp_, 8));
has_update(telegram->read_bitvalue(wWChargeType_, 10, 0)); // 0 = charge pump, 0xff = 3-way valve
has_update(telegram->read_bitvalue(wwChargeType_, 10, 0)); // 0 = charge pump, 0xff = 3-way valve
telegram->read_value(wwComfort_, 9);
if (wwComfort_ == 0x00) {
@@ -365,10 +383,9 @@ void Boiler::process_UBAMonitorFast(std::shared_ptr<const Telegram> telegram) {
has_update(telegram->read_bitvalue(wwCirc_, 7, 7));
// warm water storage sensors (if present)
// wWStorageTemp2 is also used by some brands as the boiler temperature - see https://github.com/emsesp/EMS-ESP/issues/206
has_update(telegram->read_value(wwStorageTemp1_, 9)); // 0x8300 if not available
has_update(telegram->read_value(wwStorageTemp2_,
11)); // 0x8000 if not available - this is boiler temp
// wwStorageTemp2 is also used by some brands as the boiler temperature - see https://github.com/emsesp/EMS-ESP/issues/206
has_update(telegram->read_value(wwStorageTemp1_, 9)); // 0x8300 if not available
has_update(telegram->read_value(wwStorageTemp2_, 11)); // 0x8000 if not available - this is boiler temp
has_update(telegram->read_value(retTemp_, 13));
has_update(telegram->read_value(flameCurr_, 15));
@@ -437,10 +454,10 @@ void Boiler::process_UBAMonitorWW(std::shared_ptr<const Telegram> telegram) {
has_update(telegram->read_value(wwType_, 8));
has_update(telegram->read_value(wwCurFlow_, 9));
has_update(telegram->read_value(wwWorkM_, 10, 3)); // force to 3 bytes
has_update(telegram->read_value(wWStarts_, 13, 3)); // force to 3 bytes
has_update(telegram->read_value(wwStarts_, 13, 3)); // force to 3 bytes
has_update(telegram->read_bitvalue(wwOneTime_, 5, 1));
has_update(telegram->read_bitvalue(wwDisinfecting_, 5, 2));
has_update(telegram->read_bitvalue(wwDisinfect_, 5, 2));
has_update(telegram->read_bitvalue(wwCharging_, 5, 3));
has_update(telegram->read_bitvalue(wwRecharging_, 5, 4));
has_update(telegram->read_bitvalue(wwTempOK_, 5, 5));
@@ -464,8 +481,7 @@ void Boiler::process_UBAMonitorFastPlus(std::shared_ptr<const Telegram> telegram
has_update(telegram->read_value(selBurnPow_, 9));
has_update(telegram->read_value(curFlowTemp_, 7));
has_update(telegram->read_value(flameCurr_, 19));
has_update(telegram->read_value(retTemp_,
17)); // can be 0 if no sensor, handled in export_values
has_update(telegram->read_value(retTemp_, 17)); // can be 0 if no sensor, handled in export_values
has_update(telegram->read_value(sysPress_, 21));
// read 3 char service code / installation status as appears on the display
@@ -501,8 +517,7 @@ void Boiler::process_UBAMonitorSlow(std::shared_ptr<const Telegram> telegram) {
has_update(telegram->read_value(outdoorTemp_, 0));
has_update(telegram->read_value(boilTemp_, 2));
has_update(telegram->read_value(exhaustTemp_, 4));
has_update(telegram->read_value(switchTemp_,
25)); // only if there is a mixer module present
has_update(telegram->read_value(switchTemp_, 25)); // only if there is a mixer module present
has_update(telegram->read_value(heatingPumpMod_, 9));
has_update(telegram->read_value(burnStarts_, 10, 3)); // force to 3 bytes
has_update(telegram->read_value(burnWorkMin_, 13, 3)); // force to 3 bytes
@@ -561,12 +576,14 @@ void Boiler::process_UBAParametersPlus(std::shared_ptr<const Telegram> telegram)
void Boiler::process_UBAParameterWWPlus(std::shared_ptr<const Telegram> telegram) {
has_update(telegram->read_value(wwActivated_, 5)); // 0x01 means on
has_update(telegram->read_value(wwCircPump_, 10)); // 0x01 means yes
has_update(telegram->read_value(wwCircMode_,
11)); // 1=1x3min... 6=6x3min, 7=continuous
// has_update(telegram->read_value(wWDisinfectTemp_, 12)); // settings, status in E9
// has_update(telegram->read_value(wWSelTemp_, 6)); // settings, status in E9
has_update(telegram->read_value(wwCircMode_, 11)); // 1=1x3min... 6=6x3min, 7=continuous
has_update(telegram->read_value(wwDisinfectionTemp_, 12));
has_update(telegram->read_value(wwSelTemp_, 6));
has_update(telegram->read_value(wwHystOn_, 7));
has_update(telegram->read_value(wwHystOff_, 8));
has_update(telegram->read_value(wwSelTempOff_, 0)); // confusing description in #96, hopefully this is right
has_update(telegram->read_value(wwSelTempSingle_, 16));
has_update(telegram->read_value(wwSelTempLow_, 18));
}
// 0xE9 - WW monitor ems+
@@ -577,49 +594,54 @@ void Boiler::process_UBAMonitorWWPlus(std::shared_ptr<const Telegram> telegram)
has_update(telegram->read_value(wwCurTemp2_, 3));
has_update(telegram->read_value(wwWorkM_, 14, 3)); // force to 3 bytes
has_update(telegram->read_value(wWStarts_, 17, 3)); // force to 3 bytes
has_update(telegram->read_value(wwStarts_, 17, 3)); // force to 3 bytes
has_update(telegram->read_bitvalue(wwOneTime_, 12, 2));
has_update(telegram->read_bitvalue(wwDisinfecting_, 12, 3));
has_update(telegram->read_bitvalue(wwDisinfect_, 12, 3));
has_update(telegram->read_bitvalue(wwCharging_, 12, 4));
has_update(telegram->read_bitvalue(wwRecharging_, 13, 4));
has_update(telegram->read_bitvalue(wwTempOK_, 13, 5));
has_update(telegram->read_bitvalue(wwCirc_, 13, 2));
// has_update(telegram->read_value(wWActivated_, 20)); // Activated is in 0xEA, this is something other 0/100%
has_update(telegram->read_value(wwSelTemp_, 10));
has_update(telegram->read_value(wwDisinfectionTemp_, 9));
// has_update(telegram->read_value(wwActivated_, 20)); // Activated is in 0xEA, this is something other 0/100%
// has_update(telegram->read_value(wwSelTemp_, 10)); // see #96, this is not wwSelTemp (set in EA)
// has_update(telegram->read_value(wwDisinfectionTemp_, 9));
}
/*
* UBAInformation - type 0x495
* all values 32 bit
* 08 0B FF 00 03 95 01 01 AB 83 00 27 78 EB 00 84 FA 39 FF FF FF 00 00 53 7D 8D 00 00 0F 04 1C
* 08 00 FF 00 03 95 01 01 AB 83 00 27 78 EB 00 84 FA 39 FF FF FF 00 00 53 7D 8D 00 00 0F 04 63
* 08 00 FF 18 03 95 00 00 05 84 00 00 07 22 FF FF FF FF 00 00 02 5C 00 00 03 C0 00 00 01 98 64
* 08 00 FF 30 03 95 00 00 00 D4 FF FF FF FF 00 00 1C 70 FF FF FF FF 00 00 20 30 00 00 0E 06 FB
* 08 00 FF 48 03 95 00 00 06 C0 00 00 07 66 FF FF FF FF 2E
* 08 00 FF 00 03 95 00 0F 8E C2 00 08 39 C8 00 00 18 7A 00 07 3C 80 00 00 00 00 00 00 00 E5 F6 00
* 08 00 FF 18 03 95 00 00 00 A1 00 00 00 00 00 00 00 44 00 00 00 00 00 00 00 0A 00 00 00 0A BD 00
* 08 00 FF 30 03 95 00 00 00 00 00 00 00 00 00 00 02 10 00 00 00 00 00 00 02 1A 00 00 00 02 66 00
* 08 00 FF 48 03 95 00 00 01 15 00 00 00 00 00 00 00 F9 29 00
*
*/
void Boiler::process_UBAInformation(std::shared_ptr<const Telegram> telegram) {
has_update(telegram->read_value(upTimeControl_, 0));
has_update(telegram->read_value(upTimeCompHeating_, 8));
has_update(telegram->read_value(upTimeCompCooling_, 16));
has_update(telegram->read_value(upTimeCompWw_, 4));
has_update(telegram->read_value(upTimeCompPool_, 12));
has_update(telegram->read_value(totalCompStarts_, 20));
has_update(telegram->read_value(heatingStarts_, 28));
has_update(telegram->read_value(coolingStarts_, 36));
has_update(telegram->read_value(wwStarts2_, 24));
has_update(telegram->read_value(poolStarts_, 32));
has_update(telegram->read_value(nrgConsTotal_, 64));
has_update(telegram->read_value(auxElecHeatNrgConsTotal_, 40));
has_update(telegram->read_value(auxElecHeatNrgConsHeating_, 48));
has_update(telegram->read_value(auxElecHeatNrgConsWW_, 44));
has_update(telegram->read_value(auxElecHeatNrgConsPool_, 52));
has_update(telegram->read_value(nrgConsCompTotal_, 56));
has_update(telegram->read_value(nrgConsCompHeating_, 68));
has_update(telegram->read_value(nrgConsCompWw_, 72));
has_update(telegram->read_value(nrgConsCompCooling_, 76));
has_update(telegram->read_value(nrgConsCompPool_, 80));
}
/*
@@ -634,11 +656,48 @@ void Boiler::process_UBAEnergySupplied(std::shared_ptr<const Telegram> telegram)
has_update(telegram->read_value(nrgSuppHeating_, 12));
has_update(telegram->read_value(nrgSuppWw_, 8));
has_update(telegram->read_value(nrgSuppCooling_, 16));
has_update(telegram->read_value(nrgSuppPool_, 20));
}
// Heatpump power - type 0x48D
//08 00 FF 00 03 8D 03 00 10 30 10 60 00 04 00 00 00 17 00 00 00 3C 38 0E 64 00 00 0C 33 C7 00
//XR1A050001 A05 Pump Heat circuit (1.0 ) 1 >> 1 & 0x01 ?
//XR1A040001 A04 Pump Cold circuit (1.0 ) 1 & 0x1 ?
void Boiler::process_HpPower(std::shared_ptr<const Telegram> telegram) {
has_update(telegram->read_value(hpPower_, 11));
has_update(telegram->read_bitvalue(hpCompOn_, 3, 4));
has_update(telegram->read_value(hpBrinePumpSpd_, 5));
has_update(telegram->read_value(hpCompSpd_, 17));
has_update(telegram->read_value(hpCircSpd_, 4));
has_update(telegram->read_bitvalue(hpSwitchValve_, 0, 6));
has_update(telegram->read_value(hpActivity_, 7));
hpHeatingOn_ = 0;
hpCoolingOn_ = 0;
hpWwOn_ = 0;
hpPoolOn_ = 0;
switch (hpActivity_) {
case 1: {
hpHeatingOn_ = 0xFF;
break;
}
case 2: {
hpCoolingOn_ = 0xFF;
break;
}
case 3: {
hpWwOn_ = 0xFF;
;
break;
}
case 4: {
hpPoolOn_ = 0xFF;
;
break;
}
}
}
// Heatpump outdoor unit - type 0x48F
@@ -654,8 +713,21 @@ void Boiler::process_HpOutdoor(std::shared_ptr<const Telegram> telegram) {
has_update(telegram->read_value(hpTl2_, 12));
has_update(telegram->read_value(hpPl1_, 26));
has_update(telegram->read_value(hpPh1_, 28));
has_update(telegram->read_value(hpBrineIn_, 8));
has_update(telegram->read_value(hpBrineOut_, 10));
has_update(telegram->read_value(hpSuctionGas_, 20));
has_update(telegram->read_value(hpHotGas_, 0));
}
// Heatpump pool unit - type 0x48A
// 08 00 FF 00 03 8A 01 4C 01 0C 00 00 0A 00 1E 00 00 01 00 04 4A 00
void Boiler::process_HpPool(std::shared_ptr<const Telegram> telegram) {
has_update(telegram->read_value(poolSetTemp_, 1));
}
// 0x2A - MC110Status
// e.g. 88 00 2A 00 00 00 00 00 00 00 00 00 D2 00 00 80 00 00 01 08 80 00 02 47 00
// see https://github.com/emsesp/EMS-ESP/issues/397
@@ -673,8 +745,7 @@ void Boiler::process_UBAOutdoorTemp(std::shared_ptr<const Telegram> telegram) {
// UBASetPoint 0x1A
void Boiler::process_UBASetPoints(std::shared_ptr<const Telegram> telegram) {
has_update(telegram->read_value(setFlowTemp_,
0)); // boiler set temp from thermostat
has_update(telegram->read_value(setFlowTemp_, 0)); // boiler set temp from thermostat
has_update(telegram->read_value(setBurnPow_, 1)); // max json power in %
has_update(telegram->read_value(wwSetPumpPower_, 2)); // ww pump speed/power?
}
@@ -704,9 +775,12 @@ void Boiler::process_UBAMaintenanceStatus(std::shared_ptr<const Telegram> telegr
uint8_t message_code = maintenanceMessage_[2] - '0';
has_update(telegram->read_value(message_code, 5));
// ignore if 0, which means all is ok
if (Helpers::hasValue(message_code) && message_code > 0) {
snprintf_P(maintenanceMessage_, sizeof(maintenanceMessage_), PSTR("H%02d"), message_code);
if (message_code > 0) {
snprintf(maintenanceMessage_, sizeof(maintenanceMessage_), "H%02d", message_code);
} else {
// No message. All Ok. But set a blank message so value is still in the MQTT payload to avoid HA giving warnings
maintenanceMessage_[0] = ' ';
maintenanceMessage_[1] = '\0';
}
}
@@ -733,19 +807,63 @@ void Boiler::process_UBAErrorMessage(std::shared_ptr<const Telegram> telegram) {
uint32_t date = (year - 2000) * 535680UL + month * 44640UL + day * 1440UL + hour * 60 + min;
// store only the newest code from telegrams 10 and 11
if (date > lastCodeDate_) {
snprintf_P(lastCode_, sizeof(lastCode_), PSTR("%s(%d) %02d.%02d.%d %02d:%02d"), code, codeNo, day, month, year, hour, min);
snprintf(lastCode_, sizeof(lastCode_), "%s(%d) %02d.%02d.%d %02d:%02d", code, codeNo, day, month, year, hour, min);
lastCodeDate_ = date;
}
}
}
// 0xC2
void Boiler::process_UBAErrorMessage2(std::shared_ptr<const Telegram> telegram) {
// not sure why this test is in , so removing
// if (telegram->offset > 0 || telegram->message_length < 14) {
// return;
// }
char code[4];
uint16_t codeNo;
char start_time[17];
char end_time[17];
if (!(telegram->message_data[10] & 0x80)) { // no valid start date means no error?
return;
}
code[0] = telegram->message_data[5];
code[1] = telegram->message_data[6];
code[2] = telegram->message_data[7];
code[3] = 0;
telegram->read_value(codeNo, 8);
uint16_t start_year = (telegram->message_data[10] & 0x7F) + 2000;
uint8_t start_month = telegram->message_data[11];
uint8_t start_day = telegram->message_data[13];
uint8_t start_hour = telegram->message_data[12];
uint8_t start_min = telegram->message_data[14];
snprintf(start_time, sizeof(start_time), "%d.%02d.%02d %02d:%02d", start_year, start_month, start_day, start_hour, start_min);
uint16_t end_year = (telegram->message_data[15] & 0x7F) + 2000;
uint8_t end_month = telegram->message_data[16];
uint8_t end_day = telegram->message_data[18];
uint8_t end_hour = telegram->message_data[17];
uint8_t end_min = telegram->message_data[19];
if (telegram->message_data[15] & 0x80) { // valid end date
snprintf(end_time, sizeof(end_time), "%d.%02d.%02d %02d:%02d", end_year, end_month, end_day, end_hour, end_min);
} else { // no valid end date means error still persists
snprintf(end_time, sizeof(end_time), "%s", "none");
}
snprintf(lastCode_, sizeof(lastCode_), "%s/%d start: %s, end: %s", code, codeNo, start_time, end_time);
}
// 0x15
void Boiler::process_UBAMaintenanceData(std::shared_ptr<const Telegram> telegram) {
if (telegram->offset > 0 || telegram->message_length < 5) {
return;
}
// first byte: Maintenance messages (0 = none, 1 = by operating hours, 2 = by date)
// first byte: Maintenance messages (0 = none, 1 = by operating hours, 2 = by date)
has_update(telegram->read_value(maintenanceType_, 0));
uint8_t time = (maintenanceTime_ == EMS_VALUE_USHORT_NOTSET) ? EMS_VALUE_UINT_NOTSET : maintenanceTime_ / 100;
@@ -759,40 +877,67 @@ void Boiler::process_UBAMaintenanceData(std::shared_ptr<const Telegram> telegram
uint8_t month = telegram->message_data[3];
uint8_t year = telegram->message_data[4];
if (day > 0 && month > 0) {
snprintf_P(maintenanceDate_, sizeof(maintenanceDate_), PSTR("%02d.%02d.%04d"), day, month, year + 2000);
snprintf(maintenanceDate_, sizeof(maintenanceDate_), "%02d.%02d.%04d", day, month, year + 2000);
}
}
// Set the warm water temperature 0x33/0x35 or 0xEA
bool Boiler::set_warmwater_temp(const char * value, const int8_t id) {
bool Boiler::set_ww_temp(const char * value, const int8_t id) {
int v = 0;
if (!Helpers::value2number(value, v)) {
LOG_WARNING(F("Set boiler warm water temperature: Invalid value"));
LOG_WARNING(F("Set boiler ww temperature: Invalid value"));
return false;
}
LOG_INFO(F("Setting boiler warm water temperature to %d C"), v);
if (get_toggle_fetch(EMS_TYPE_UBAParametersPlus)) {
LOG_INFO(F("Setting boiler ww temperature to %d C"), v);
if (is_fetch(EMS_TYPE_UBAParametersPlus)) {
write_command(EMS_TYPE_UBAParameterWWPlus, 6, v, EMS_TYPE_UBAParameterWWPlus);
} else {
// some boiler have it in 0x33, some in 0x35
write_command(EMS_TYPE_UBAFlags, 3, v, 0x34); // for i9000, see #397
write_command(EMS_TYPE_UBAFlags, 3, v, EMS_TYPE_UBAParameterWW); // for i9000, see #397
write_command(EMS_TYPE_UBAParameterWW, 2, v, EMS_TYPE_UBAParameterWW); // read seltemp back
}
return true;
}
// Set the warm water disinfection temperature
bool Boiler::set_disinfect_temp(const char * value, const int8_t id) {
// Set the lower warm water temperature 0xEA
bool Boiler::set_ww_temp_low(const char * value, const int8_t id) {
int v = 0;
if (!Helpers::value2number(value, v)) {
LOG_WARNING(F("Set boiler warm water disinfect temperature: Invalid value"));
LOG_WARNING(F("Set boiler lower ww temperature: Invalid value"));
return false;
}
LOG_INFO(F("Setting boiler warm water disinfect temperature to %d C"), v);
if (get_toggle_fetch(EMS_TYPE_UBAParametersPlus)) {
LOG_INFO(F("Setting boiler lower ww temperature to %d C"), v);
write_command(EMS_TYPE_UBAParameterWWPlus, 18, v, EMS_TYPE_UBAParameterWWPlus);
return true;
}
// Set the warm water single charge temperature 0xEA
bool Boiler::set_ww_temp_single(const char * value, const int8_t id) {
int v = 0;
if (!Helpers::value2number(value, v)) {
LOG_WARNING(F("Set single charge ww temperature: Invalid value"));
return false;
}
LOG_INFO(F("Setting single charge ww temperature to %d C"), v);
write_command(EMS_TYPE_UBAParameterWWPlus, 16, v, EMS_TYPE_UBAParameterWWPlus);
return true;
}
// Set the warm water disinfection temperature
bool Boiler::set_ww_disinfect_temp(const char * value, const int8_t id) {
int v = 0;
if (!Helpers::value2number(value, v)) {
LOG_WARNING(F("Set boiler ww disinfect temperature: Invalid value"));
return false;
}
LOG_INFO(F("Setting boiler ww disinfect temperature to %d C"), v);
if (is_fetch(EMS_TYPE_UBAParametersPlus)) {
write_command(EMS_TYPE_UBAParameterWWPlus, 12, v, EMS_TYPE_UBAParameterWWPlus);
} else {
write_command(EMS_TYPE_UBAParameterWW, 8, v, EMS_TYPE_UBAParameterWW);
@@ -800,6 +945,7 @@ bool Boiler::set_disinfect_temp(const char * value, const int8_t id) {
return true;
}
// flow temp
bool Boiler::set_flow_temp(const char * value, const int8_t id) {
int v = 0;
@@ -829,14 +975,14 @@ bool Boiler::set_burn_power(const char * value, const int8_t id) {
}
// Set the warm water flow temperature offset 0x33
bool Boiler::set_wWFlowTempOffset(const char * value, const int8_t id) {
bool Boiler::set_ww_flowTempOffset(const char * value, const int8_t id) {
int v = 0;
if (!Helpers::value2number(value, v)) {
LOG_WARNING(F("Set boiler warm water flow temperature offset: Invalid value"));
LOG_WARNING(F("Set boiler ww flow temperature offset: Invalid value"));
return false;
}
LOG_INFO(F("Setting boiler warm water flow temperature offset to %d C"), v);
LOG_INFO(F("Setting boiler ww flow temperature offset to %d C"), v);
write_command(EMS_TYPE_UBAParameterWW, 5, v, EMS_TYPE_UBAParameterWW);
return true;
@@ -851,7 +997,7 @@ bool Boiler::set_heating_activated(const char * value, const int8_t id) {
}
LOG_INFO(F("Setting boiler heating %s"), v ? "on" : "off");
if (get_toggle_fetch(EMS_TYPE_UBAParametersPlus)) {
if (is_fetch(EMS_TYPE_UBAParametersPlus)) {
write_command(EMS_TYPE_UBAParametersPlus, 0, v ? 0x01 : 0, EMS_TYPE_UBAParametersPlus);
} else {
write_command(EMS_TYPE_UBAParameters, 0, v ? 0xFF : 0, EMS_TYPE_UBAParameters);
@@ -869,7 +1015,7 @@ bool Boiler::set_heating_temp(const char * value, const int8_t id) {
}
LOG_INFO(F("Setting boiler heating temperature to %d C"), v);
if (get_toggle_fetch(EMS_TYPE_UBAParametersPlus)) {
if (is_fetch(EMS_TYPE_UBAParametersPlus)) {
write_command(EMS_TYPE_UBAParametersPlus, 1, v, EMS_TYPE_UBAParametersPlus);
} else {
write_command(EMS_TYPE_UBAParameters, 1, v, EMS_TYPE_UBAParameters);
@@ -887,7 +1033,7 @@ bool Boiler::set_min_power(const char * value, const int8_t id) {
}
LOG_INFO(F("Setting boiler min power to %d %%"), v);
if (get_toggle_fetch(EMS_TYPE_UBAParametersPlus)) {
if (is_fetch(EMS_TYPE_UBAParametersPlus)) {
write_command(EMS_TYPE_UBAParametersPlus, 5, v, EMS_TYPE_UBAParametersPlus);
} else {
write_command(EMS_TYPE_UBAParameters, 3, v, EMS_TYPE_UBAParameters);
@@ -905,7 +1051,7 @@ bool Boiler::set_max_power(const char * value, const int8_t id) {
}
LOG_INFO(F("Setting boiler max power to %d %%"), v);
if (get_toggle_fetch(EMS_TYPE_UBAParametersPlus)) {
if (is_fetch(EMS_TYPE_UBAParametersPlus)) {
write_command(EMS_TYPE_UBAParametersPlus, 4, v, EMS_TYPE_UBAParametersPlus);
} else {
write_command(EMS_TYPE_UBAParameters, 2, v, EMS_TYPE_UBAParameters);
@@ -923,7 +1069,7 @@ bool Boiler::set_ww_hyst_on(const char * value, const int8_t id) {
}
LOG_INFO(F("Setting ww on hysteresis on to %d C"), v);
if (get_toggle_fetch(EMS_TYPE_UBAParameterWWPlus)) {
if (is_fetch(EMS_TYPE_UBAParameterWWPlus)) {
write_command(EMS_TYPE_UBAParameterWWPlus, 7, v, EMS_TYPE_UBAParameterWWPlus);
} else {
write_command(EMS_TYPE_UBAParameterWW, 3, v, EMS_TYPE_UBAParameterWW);
@@ -941,7 +1087,7 @@ bool Boiler::set_ww_hyst_off(const char * value, const int8_t id) {
}
LOG_INFO(F("Setting ww off hysteresis off to %d C"), v);
if (get_toggle_fetch(EMS_TYPE_UBAParameterWWPlus)) {
if (is_fetch(EMS_TYPE_UBAParameterWWPlus)) {
write_command(EMS_TYPE_UBAParameterWWPlus, 8, v, EMS_TYPE_UBAParameterWWPlus);
} else {
write_command(EMS_TYPE_UBAParameterWW, 4, v, EMS_TYPE_UBAParameterWW);
@@ -951,14 +1097,14 @@ bool Boiler::set_ww_hyst_off(const char * value, const int8_t id) {
}
// set warm water max power
bool Boiler::set_warmwater_maxpower(const char * value, const int8_t id) {
bool Boiler::set_ww_maxpower(const char * value, const int8_t id) {
int v = 0;
if (!Helpers::value2number(value, v)) {
LOG_WARNING(F("Set warm water max power: Invalid value"));
LOG_WARNING(F("Set ww max power: Invalid value"));
return false;
}
LOG_INFO(F("Setting warm water max power to %d %%"), v);
LOG_INFO(F("Setting ww max power to %d %%"), v);
write_command(EMS_TYPE_UBASettingsWW, 7, v, EMS_TYPE_UBASettingsWW);
return true;
@@ -973,7 +1119,7 @@ bool Boiler::set_min_pump(const char * value, const int8_t id) {
}
LOG_INFO(F("Setting pump min to %d %%"), v);
if (get_toggle_fetch(EMS_TYPE_UBAParametersPlus)) {
if (is_fetch(EMS_TYPE_UBAParametersPlus)) {
write_command(EMS_TYPE_UBAParametersPlus, 14, v, EMS_TYPE_UBAParametersPlus);
} else {
write_command(EMS_TYPE_UBAParameters, 10, v, EMS_TYPE_UBAParameters);
@@ -991,7 +1137,7 @@ bool Boiler::set_max_pump(const char * value, const int8_t id) {
}
LOG_INFO(F("Setting pump max to %d %%"), v);
if (get_toggle_fetch(EMS_TYPE_UBAParametersPlus)) {
if (is_fetch(EMS_TYPE_UBAParametersPlus)) {
write_command(EMS_TYPE_UBAParametersPlus, 13, v, EMS_TYPE_UBAParametersPlus);
} else {
write_command(EMS_TYPE_UBAParameters, 9, v, EMS_TYPE_UBAParameters);
@@ -1009,7 +1155,7 @@ bool Boiler::set_hyst_on(const char * value, const int8_t id) {
}
LOG_INFO(F("Setting boiler hysteresis on to %d C"), v);
if (get_toggle_fetch(EMS_TYPE_UBAParametersPlus)) {
if (is_fetch(EMS_TYPE_UBAParametersPlus)) {
write_command(EMS_TYPE_UBAParametersPlus, 9, v, EMS_TYPE_UBAParametersPlus);
} else {
write_command(EMS_TYPE_UBAParameters, 5, v, EMS_TYPE_UBAParameters);
@@ -1027,7 +1173,7 @@ bool Boiler::set_hyst_off(const char * value, const int8_t id) {
}
LOG_INFO(F("Setting boiler hysteresis off to %d C"), v);
if (get_toggle_fetch(EMS_TYPE_UBAParametersPlus)) {
if (is_fetch(EMS_TYPE_UBAParametersPlus)) {
write_command(EMS_TYPE_UBAParametersPlus, 8, v, EMS_TYPE_UBAParametersPlus);
} else {
write_command(EMS_TYPE_UBAParameters, 4, v, EMS_TYPE_UBAParameters);
@@ -1045,7 +1191,7 @@ bool Boiler::set_burn_period(const char * value, const int8_t id) {
}
LOG_INFO(F("Setting burner min period to %d min"), v);
if (get_toggle_fetch(EMS_TYPE_UBAParametersPlus)) {
if (is_fetch(EMS_TYPE_UBAParametersPlus)) {
write_command(EMS_TYPE_UBAParametersPlus, 10, v, EMS_TYPE_UBAParametersPlus);
} else {
write_command(EMS_TYPE_UBAParameters, 6, v, EMS_TYPE_UBAParameters);
@@ -1062,7 +1208,7 @@ bool Boiler::set_pump_delay(const char * value, const int8_t id) {
return false;
}
if (get_toggle_fetch(EMS_TYPE_UBAParameters)) {
if (is_fetch(EMS_TYPE_UBAParameters)) {
LOG_INFO(F("Setting boiler pump delay to %d min"), v);
write_command(EMS_TYPE_UBAParameters, 8, v, EMS_TYPE_UBAParameters);
return true;
@@ -1073,24 +1219,24 @@ bool Boiler::set_pump_delay(const char * value, const int8_t id) {
// note some boilers do not have this setting, than it's done by thermostat
// on a RC35 it's by EMSESP::send_write_request(0x37, 0x10, 2, &set, 1, 0); (set is 1,2,3) 1=hot, 2=eco, 3=intelligent
bool Boiler::set_warmwater_mode(const char * value, const int8_t id) {
bool Boiler::set_ww_mode(const char * value, const int8_t id) {
uint8_t set;
if (!Helpers::value2enum(value, set, FL_(enum_comfort))) {
LOG_WARNING(F("Set boiler warm water mode: Invalid value"));
LOG_WARNING(F("Set boiler ww mode: Invalid value"));
return false;
}
if (!get_toggle_fetch(EMS_TYPE_UBAParameterWW)) {
if (!is_fetch(EMS_TYPE_UBAParameterWW)) {
return false;
}
if (set == 0) {
LOG_INFO(F("Setting boiler warm water to Hot"));
LOG_INFO(F("Setting boiler ww to Hot"));
} else if (set == 1) {
LOG_INFO(F("Setting boiler warm water to Eco"));
LOG_INFO(F("Setting boiler ww to Eco"));
set = 0xD8;
} else if (set == 2) {
LOG_INFO(F("Setting boiler warm water to Intelligent"));
LOG_INFO(F("Setting boiler ww to Intelligent"));
set = 0xEC;
} else {
return false; // do nothing
@@ -1101,19 +1247,19 @@ bool Boiler::set_warmwater_mode(const char * value, const int8_t id) {
}
// turn on/off warm water
bool Boiler::set_warmwater_activated(const char * value, const int8_t id) {
bool Boiler::set_ww_activated(const char * value, const int8_t id) {
bool v = false;
if (!Helpers::value2bool(value, v)) {
LOG_WARNING(F("Set boiler warm water active: Invalid value"));
LOG_WARNING(F("Set boiler ww active: Invalid value"));
return false;
}
LOG_INFO(F("Setting boiler warm water active %s"), v ? "on" : "off");
LOG_INFO(F("Setting boiler ww active %s"), v ? "on" : "off");
// https://github.com/emsesp/EMS-ESP/issues/268
// 08 for HT3 seems to be wrong, see https://github.com/emsesp/EMS-ESP32/issues/89
if (get_toggle_fetch(EMS_TYPE_UBAParameterWWPlus)) {
if (is_fetch(EMS_TYPE_UBAParameterWWPlus)) {
write_command(EMS_TYPE_UBAParameterWWPlus, 1, v ? 1 : 0, EMS_TYPE_UBAParameterWWPlus);
} else {
write_command(EMS_TYPE_UBAParameterWW, 1, v ? 0xFF : 0, EMS_TYPE_UBAParameterWW);
@@ -1161,17 +1307,16 @@ bool Boiler::set_tapwarmwater_activated(const char * value, const int8_t id) {
// Activate / De-activate One Time warm water 0x35
// true = on, false = off
// See also https://github.com/emsesp/EMS-ESP/issues/341#issuecomment-596245458 for Junkers
bool Boiler::set_warmwater_onetime(const char * value, const int8_t id) {
bool Boiler::set_ww_onetime(const char * value, const int8_t id) {
bool v = false;
if (!Helpers::value2bool(value, v)) {
LOG_WARNING(F("Set warm water OneTime loading: Invalid value"));
LOG_WARNING(F("Set ww OneTime loading: Invalid value"));
return false;
}
LOG_INFO(F("Setting warm water OneTime loading %s"), v ? "on" : "off");
if (get_toggle_fetch(EMS_TYPE_UBAParameterWWPlus)) {
write_command(EMS_TYPE_UBAFlags, 0, (v ? 0x22 : 0x02),
0xE9); // not sure if this is in flags
LOG_INFO(F("Setting ww OneTime loading %s"), v ? "on" : "off");
if (is_fetch(EMS_TYPE_UBAParameterWWPlus)) {
write_command(EMS_TYPE_UBAFlags, 0, (v ? 0x22 : 0x02), 0xE9); // not sure if this is in flags
} else {
write_command(EMS_TYPE_UBAFlags, 0, (v ? 0x22 : 0x02), 0x34);
}
@@ -1181,17 +1326,16 @@ bool Boiler::set_warmwater_onetime(const char * value, const int8_t id) {
// Activate / De-activate circulation of warm water 0x35
// true = on, false = off
bool Boiler::set_warmwater_circulation(const char * value, const int8_t id) {
bool Boiler::set_ww_circulation(const char * value, const int8_t id) {
bool v = false;
if (!Helpers::value2bool(value, v)) {
LOG_WARNING(F("Set warm water circulation: Invalid value"));
LOG_WARNING(F("Set ww circulation: Invalid value"));
return false;
}
LOG_INFO(F("Setting warm water circulation %s"), v ? "on" : "off");
if (get_toggle_fetch(EMS_TYPE_UBAParameterWWPlus)) {
write_command(EMS_TYPE_UBAFlags, 1, (v ? 0x22 : 0x02),
0xE9); // not sure if this is in flags
LOG_INFO(F("Setting ww circulation %s"), v ? "on" : "off");
if (is_fetch(EMS_TYPE_UBAParameterWWPlus)) {
write_command(EMS_TYPE_UBAFlags, 1, (v ? 0x22 : 0x02), 0xE9); // not sure if this is in flags
} else {
write_command(EMS_TYPE_UBAFlags, 1, (v ? 0x22 : 0x02), 0x34);
}
@@ -1199,17 +1343,35 @@ bool Boiler::set_warmwater_circulation(const char * value, const int8_t id) {
return true;
}
// configuration of warm water circulation pump
bool Boiler::set_warmwater_circulation_pump(const char * value, const int8_t id) {
// starting warm water disinfect, set to off seems not working
bool Boiler::set_ww_disinfect(const char * value, const int8_t id) {
bool v = false;
if (!Helpers::value2bool(value, v)) {
LOG_WARNING(F("Set warm water circulation pump: Invalid value"));
LOG_WARNING(F("Set ww disinfect: Invalid value"));
return false;
}
LOG_INFO(F("Setting warm water circulation pump %s"), v ? "on" : "off");
LOG_INFO(F("Setting ww disinfect %s"), v ? "on" : "off");
if (is_fetch(EMS_TYPE_UBAParameterWWPlus)) {
write_command(EMS_TYPE_UBAFlags, 0, (v ? 0x44 : 0x04), 0xE9); // not sure if this is in flags
} else {
write_command(EMS_TYPE_UBAFlags, 0, (v ? 0x44 : 0x04), 0x34);
}
if (get_toggle_fetch(EMS_TYPE_UBAParameterWWPlus)) {
return true;
}
// configuration of warm water circulation pump
bool Boiler::set_ww_circulation_pump(const char * value, const int8_t id) {
bool v = false;
if (!Helpers::value2bool(value, v)) {
LOG_WARNING(F("Set ww circulation pump: Invalid value"));
return false;
}
LOG_INFO(F("Setting ww circulation pump %s"), v ? "on" : "off");
if (is_fetch(EMS_TYPE_UBAParameterWWPlus)) {
write_command(EMS_TYPE_UBAParameterWWPlus, 10, v ? 0x01 : 0x00, EMS_TYPE_UBAParameterWWPlus);
} else {
write_command(EMS_TYPE_UBAParameterWW, 6, v ? 0xFF : 0x00, EMS_TYPE_UBAParameterWW);
@@ -1220,23 +1382,23 @@ bool Boiler::set_warmwater_circulation_pump(const char * value, const int8_t id)
// Set the mode of circulation, 1x3min, ... 6x3min, continuous
// true = on, false = off
bool Boiler::set_warmwater_circulation_mode(const char * value, const int8_t id) {
bool Boiler::set_ww_circulation_mode(const char * value, const int8_t id) {
int v = 0;
if (!Helpers::value2number(value, v)) {
LOG_WARNING(F("Set warm water circulation mode: Invalid value"));
LOG_WARNING(F("Set ww circulation mode: Invalid value"));
return false;
}
if (v < 7) {
LOG_INFO(F("Setting warm water circulation mode %dx3min"), v);
LOG_INFO(F("Setting ww circulation mode %dx3min"), v);
} else if (v == 7) {
LOG_INFO(F("Setting warm water circulation mode continuous"));
LOG_INFO(F("Setting ww circulation mode continuous"));
} else {
LOG_WARNING(F("Set warm water circulation mode: Invalid value"));
LOG_WARNING(F("Set ww circulation mode: Invalid value"));
return false;
}
if (get_toggle_fetch(EMS_TYPE_UBAParameterWWPlus)) {
if (is_fetch(EMS_TYPE_UBAParameterWWPlus)) {
write_command(EMS_TYPE_UBAParameterWWPlus, 11, v, EMS_TYPE_UBAParameterWWPlus);
} else {
write_command(EMS_TYPE_UBAParameterWW, 7, v, EMS_TYPE_UBAParameterWW);
@@ -1271,7 +1433,7 @@ bool Boiler::set_reset(const char * value, const int8_t id) {
bool Boiler::set_maintenance(const char * value, const int8_t id) {
std::string s(12, '\0');
if (Helpers::value2string(value, s)) {
if (s == Helpers::toLower(uuid::read_flash_string(F_(reset)))) {
if (s == Helpers::toLower(read_flash_string(F_(reset)))) {
LOG_INFO(F("Reset boiler maintenance message"));
write_command(0x05, 0x08, 0xFF, 0x1C);
return true;
@@ -1349,4 +1511,20 @@ bool Boiler::set_maintenancedate(const char * value, const int8_t id) {
return false;
}
// Set the pool temperature 0x48A
bool Boiler::set_pool_temp(const char * value, const int8_t id) {
float v = 0;
uint8_t v2 = 0;
if (!Helpers::value2float(value, v)) {
LOG_WARNING(F("Set pool water temperature: Invalid value"));
return false;
}
v2 = (int((v * 2) + 0.5) & 0xFF);
LOG_INFO(F("Setting pool temperature to %d C"), v2 / 2);
write_command(0x48A, 1, v2, 0x48A);
return true;
}
} // namespace emsesp

View File

@@ -27,7 +27,7 @@ class Boiler : public EMSdevice {
public:
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);
virtual bool publish_ha_config();
virtual bool publish_ha_device_config();
private:
static uuid::log::Logger logger_;
@@ -57,10 +57,13 @@ class Boiler : public EMSdevice {
// ww
uint8_t wwSetTemp_; // Warm Water set temperature
uint8_t wwSelTemp_; // Warm Water selected temperature
uint8_t wwSelTempLow_; // Warm Water lower selected temperature
uint8_t wwSelTempOff_; // Warm Water selected temperature for off position
uint8_t wwSelTempSingle_; // Warm Water single charge temperature
uint8_t wwType_; // 0-off, 1-flow, 2-flowbuffer, 3-buffer, 4-layered buffer
uint8_t wwComfort_; // WW comfort mode
uint8_t wwCircPump_; // Warm Water circulation pump available
uint8_t wWChargeType_; // Warm Water charge type (pump or 3-way-valve)
uint8_t wwChargeType_; // Warm Water charge type (pump or 3-way-valve)
uint8_t wwDisinfectionTemp_; // Warm Water disinfection temperature to prevent infection
uint8_t wwCircMode_; // Warm Water circulation pump mode
uint8_t wwCirc_; // Circulation on/off
@@ -71,7 +74,7 @@ class Boiler : public EMSdevice {
uint16_t wwStorageTemp2_; // warm water storage temp 2
uint8_t wwActivated_; // Warm Water activated
uint8_t wwOneTime_; // Warm Water one time function on/off
uint8_t wwDisinfecting_; // Warm Water disinfection on/off
uint8_t wwDisinfect_; // Warm Water disinfection on/off
uint8_t wwCharging_; // Warm Water charging on/off
uint8_t wwRecharging_; // Warm Water recharge on/off
uint8_t wwTempOK_; // Warm Water temperature ok on/off
@@ -80,9 +83,9 @@ class Boiler : public EMSdevice {
uint8_t wwSetPumpPower_; // ww pump speed/power?
uint8_t wwFlowTempOffset_; // Boiler offset for ww heating
uint8_t wwMaxPower_; // Warm Water maximum power
uint32_t wWStarts_; // Warm Water # starts
uint32_t wwStarts_; // Warm Water starts
uint32_t wwStarts2_; // Warm water control starts
uint32_t wwWorkM_; // Warm Water # minutes
uint32_t wwWorkM_; // Warm Water minutes
int8_t wwHystOn_;
int8_t wwHystOff_;
uint8_t wwTapActivated_; // maintenance-mode to switch DHW off
@@ -124,11 +127,11 @@ class Boiler : public EMSdevice {
uint8_t setFlowTemp_; // boiler setpoint temp
uint8_t curBurnPow_; // Burner current power %
uint8_t setBurnPow_; // max output power in %
uint32_t burnStarts_; // # burner restarts
uint32_t burnStarts_; // burner restarts
uint32_t burnWorkMin_; // Total burner operating time
uint32_t heatWorkMin_; // Total heat operating time
uint32_t UBAuptime_; // Total UBA working hours
char lastCode_[30]; // last error code
char lastCode_[60]; // last error code
char serviceCode_[4]; // 3 character status/service code
uint16_t serviceCodeNumber_; // error/service code
@@ -137,38 +140,62 @@ class Boiler : public EMSdevice {
uint32_t upTimeCompHeating_; // Operating time compressor heating
uint32_t upTimeCompCooling_; // Operating time compressor cooling
uint32_t upTimeCompWw_; // Operating time compressor warm water
uint32_t upTimeCompPool_; // Operating time compressor pool
uint32_t totalCompStarts_; // Total Commpressor control starts
uint32_t heatingStarts_; // Heating control starts
uint32_t coolingStarts_; // Cooling control starts
uint32_t poolStarts_; // Warm water control starts
uint32_t nrgConsTotal_; // Energy consumption total
uint32_t nrgConsCompTotal_; // Energy consumption compressor total
uint32_t nrgConsCompHeating_; // Energy consumption compressor heating
uint32_t nrgConsCompWw_; // Energy consumption compressor warm water
uint32_t nrgConsCompCooling_; // Energy consumption compressor cooling
uint32_t nrgConsCompPool_; // Energy consumption compressor pool
uint32_t nrgSuppTotal_; // Energy supplied total
uint32_t nrgSuppHeating_; // Energy supplied heating
uint32_t nrgSuppWw_; // Energy supplied warm water
uint32_t nrgSuppCooling_; // Energy supplied cooling
uint32_t nrgSuppPool_; // Energy supplied pool
uint32_t auxElecHeatNrgConsTotal_; // Auxiliary electrical heater energy consumption total
uint32_t auxElecHeatNrgConsHeating_; // Auxiliary electrical heater energy consumption heating
uint32_t auxElecHeatNrgConsWW_; // Auxiliary electrical heater energy consumption DHW
uint32_t auxElecHeatNrgConsPool_; // Auxiliary electrical heater energy consumption Pool
char maintenanceMessage_[4];
char maintenanceDate_[12];
uint8_t maintenanceType_;
uint16_t maintenanceTime_;
// heatpump
uint8_t hpPower_;
int16_t hpTc0_;
int16_t hpTc1_;
int16_t hpTc3_;
int16_t hpTr3_;
int16_t hpTr4_;
int16_t hpTr5_;
int16_t hpTr6_;
int16_t hpTr7_;
int16_t hpTl2_;
int16_t hpPl1_;
int16_t hpPh1_;
uint8_t hpPower_;
uint8_t hpCompOn_;
uint8_t hpBrinePumpSpd_;
uint8_t hpCompSpd_;
uint8_t hpCircSpd_;
uint16_t hpBrineIn_;
uint16_t hpBrineOut_;
uint16_t hpSuctionGas_;
uint16_t hpHotGas_;
uint8_t hpSwitchValve_;
uint8_t hpActivity_;
uint8_t hpHeatingOn_;
uint8_t hpCoolingOn_;
uint8_t hpWwOn_;
uint8_t hpPoolOn_;
uint8_t hpHeatingOn;
int16_t hpTc0_;
int16_t hpTc1_;
int16_t hpTc3_;
int16_t hpTr3_;
int16_t hpTr4_;
int16_t hpTr5_;
int16_t hpTr6_;
int16_t hpTr7_;
int16_t hpTl2_;
int16_t hpPl1_;
int16_t hpPh1_;
// Pool unit
int8_t poolSetTemp_;
void process_UBAParameterWW(std::shared_ptr<const Telegram> telegram);
void process_UBAMonitorFast(std::shared_ptr<const Telegram> telegram);
@@ -188,6 +215,7 @@ class Boiler : public EMSdevice {
void process_UBAMaintenanceStatus(std::shared_ptr<const Telegram> telegram);
void process_UBAMaintenanceData(std::shared_ptr<const Telegram> telegram);
void process_UBAErrorMessage(std::shared_ptr<const Telegram> telegram);
void process_UBAErrorMessage2(std::shared_ptr<const Telegram> telegram);
void process_UBAMonitorWWPlus(std::shared_ptr<const Telegram> telegram);
void process_UBAInformation(std::shared_ptr<const Telegram> telegram);
void process_UBAEnergySupplied(std::shared_ptr<const Telegram> telegram);
@@ -195,19 +223,23 @@ class Boiler : public EMSdevice {
void process_UBASettingsWW(std::shared_ptr<const Telegram> telegram);
void process_HpPower(std::shared_ptr<const Telegram> telegram);
void process_HpOutdoor(std::shared_ptr<const Telegram> telegram);
void process_HpPool(std::shared_ptr<const Telegram> telegram);
// commands - none of these use the additional id parameter
bool set_warmwater_mode(const char * value, const int8_t id);
bool set_warmwater_activated(const char * value, const int8_t id);
bool set_ww_mode(const char * value, const int8_t id);
bool set_ww_activated(const char * value, const int8_t id);
bool set_tapwarmwater_activated(const char * value, const int8_t id);
bool set_warmwater_onetime(const char * value, const int8_t id);
bool set_warmwater_circulation(const char * value, const int8_t id);
bool set_warmwater_circulation_pump(const char * value, const int8_t id);
bool set_warmwater_circulation_mode(const char * value, const int8_t id);
bool set_warmwater_temp(const char * value, const int8_t id);
bool set_disinfect_temp(const char * value, const int8_t id);
bool set_warmwater_maxpower(const char * value, const int8_t id);
bool set_wWFlowTempOffset(const char * value, const int8_t id);
bool set_ww_onetime(const char * value, const int8_t id);
bool set_ww_disinfect(const char * value, const int8_t id);
bool set_ww_circulation(const char * value, const int8_t id);
bool set_ww_circulation_pump(const char * value, const int8_t id);
bool set_ww_circulation_mode(const char * value, const int8_t id);
bool set_ww_temp(const char * value, const int8_t id);
bool set_ww_temp_low(const char * value, const int8_t id);
bool set_ww_temp_single(const char * value, const int8_t id);
bool set_ww_disinfect_temp(const char * value, const int8_t id);
bool set_ww_maxpower(const char * value, const int8_t id);
bool set_ww_flowTempOffset(const char * value, const int8_t id);
bool set_flow_temp(const char * value, const int8_t id);
bool set_burn_power(const char * value, const int8_t id);
bool set_heating_activated(const char * value, const int8_t id);
@@ -226,6 +258,7 @@ class Boiler : public EMSdevice {
bool set_maintenancedate(const char * value, const int8_t id);
bool set_ww_hyst_on(const char * value, const int8_t id);
bool set_ww_hyst_off(const char * value, const int8_t id);
bool set_pool_temp(const char * value, const int8_t id);
};
} // namespace emsesp

View File

@@ -29,7 +29,7 @@ Connect::Connect(uint8_t device_type, uint8_t device_id, uint8_t product_id, con
}
// publish HA config
bool Connect::publish_ha_config() {
bool Connect::publish_ha_device_config() {
return true;
}

View File

@@ -27,7 +27,7 @@ class Connect : public EMSdevice {
public:
Connect(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);
virtual bool publish_ha_config();
virtual bool publish_ha_device_config();
private:
static uuid::log::Logger logger_;

View File

@@ -29,7 +29,7 @@ Controller::Controller(uint8_t device_type, uint8_t device_id, uint8_t product_i
}
// publish HA config
bool Controller::publish_ha_config() {
bool Controller::publish_ha_device_config() {
return true;
}

View File

@@ -27,7 +27,7 @@ class Controller : public EMSdevice {
public:
Controller(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);
virtual bool publish_ha_config();
virtual bool publish_ha_device_config();
private:
static uuid::log::Logger logger_;

View File

@@ -29,7 +29,7 @@ Gateway::Gateway(uint8_t device_type, uint8_t device_id, uint8_t product_id, con
}
// publish HA config
bool Gateway::publish_ha_config() {
bool Gateway::publish_ha_device_config() {
return true;
}

View File

@@ -27,7 +27,7 @@ class Gateway : public EMSdevice {
public:
Gateway(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);
virtual bool publish_ha_config();
virtual bool publish_ha_device_config();
private:
static uuid::log::Logger logger_;

View File

@@ -26,11 +26,21 @@ uuid::log::Logger Generic::logger_{F_(generic), uuid::log::Facility::CONSOLE};
Generic::Generic(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) {
// RF-Sensor 0x40 sending temperature in telegram 0x435, see https://github.com/emsesp/EMS-ESP32/issues/103
if (device_id == 0x40) {
register_telegram_type(0x435, F("RFSensorMessage"), false, MAKE_PF_CB(process_RFSensorMessage));
register_device_value(TAG_NONE, &rfTemp_, DeviceValueType::SHORT, FL_(div10), FL_(RFTemp), DeviceValueUOM::DEGREES);
}
}
// publish HA config
bool Generic::publish_ha_config() {
bool Generic::publish_ha_device_config() {
return true;
}
// type 0x435 rf remote sensor
void Generic::process_RFSensorMessage(std::shared_ptr<const Telegram> telegram) {
has_update(telegram->read_value(rfTemp_, 0)); // is * 10
}
} // namespace emsesp

View File

@@ -27,10 +27,14 @@ class Generic : public EMSdevice {
public:
Generic(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);
virtual bool publish_ha_config();
virtual bool publish_ha_device_config();
private:
static uuid::log::Logger logger_;
int16_t rfTemp_;
void process_RFSensorMessage(std::shared_ptr<const Telegram> telegram);
};
} // namespace emsesp

View File

@@ -26,8 +26,6 @@ uuid::log::Logger Heatpump::logger_{F_(heatpump), uuid::log::Facility::CONSOLE};
Heatpump::Heatpump(uint8_t device_type, uint8_t device_id, uint8_t product_id, const std::string & version, const std::string & name, uint8_t flags, uint8_t brand)
: EMSdevice(device_type, device_id, product_id, version, name, flags, brand) {
LOG_DEBUG(F("Adding new Heat Pump module with device ID 0x%02X"), device_id);
// telegram handlers
register_telegram_type(0x042B, F("HP1"), true, MAKE_PF_CB(process_HPMonitor1));
register_telegram_type(0x047B, F("HP2"), true, MAKE_PF_CB(process_HPMonitor2));
@@ -41,13 +39,13 @@ Heatpump::Heatpump(uint8_t device_type, uint8_t device_id, uint8_t product_id, c
}
// publish HA config
bool Heatpump::publish_ha_config() {
bool Heatpump::publish_ha_device_config() {
StaticJsonDocument<EMSESP_JSON_SIZE_HA_CONFIG> doc;
doc["uniq_id"] = F_(heatpump);
doc["ic"] = F_(icondevice);
char stat_t[Mqtt::MQTT_TOPIC_MAX_SIZE];
snprintf_P(stat_t, sizeof(stat_t), PSTR("%s/%s"), Mqtt::base().c_str(), Mqtt::tag_to_topic(device_type(), DeviceValueTAG::TAG_NONE).c_str());
snprintf(stat_t, sizeof(stat_t), "%s/%s", Mqtt::base().c_str(), Mqtt::tag_to_topic(device_type(), DeviceValueTAG::TAG_NONE).c_str());
doc["stat_t"] = stat_t;
char name_s[40];
@@ -64,7 +62,7 @@ bool Heatpump::publish_ha_config() {
ids.add("ems-esp-heatpump");
char topic[Mqtt::MQTT_TOPIC_MAX_SIZE];
snprintf_P(topic, sizeof(topic), PSTR("sensor/%s/heatpump/config"), Mqtt::base().c_str());
snprintf(topic, sizeof(topic), "sensor/%s/heatpump/config", Mqtt::base().c_str());
Mqtt::publish_ha(topic, doc.as<JsonObject>()); // publish the config payload with retain flag
return true;

View File

@@ -27,7 +27,7 @@ class Heatpump : public EMSdevice {
public:
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);
virtual bool publish_ha_config();
virtual bool publish_ha_device_config();
private:
static uuid::log::Logger logger_;

View File

@@ -26,58 +26,81 @@ 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));
type_ = Type::MP;
register_device_value(TAG_NONE, &id_, DeviceValueType::UINT, nullptr, FL_(ID), DeviceValueUOM::NONE);
register_device_value(TAG_NONE, &poolTemp_, DeviceValueType::SHORT, FL_(div10), FL_(poolTemp), DeviceValueUOM::DEGREES);
register_device_value(TAG_NONE, &poolShuntStatus_, DeviceValueType::ENUM, FL_(enum_shunt), FL_(poolShuntStatus), DeviceValueUOM::NONE);
register_device_value(TAG_NONE, &poolShunt_, DeviceValueType::UINT, nullptr, FL_(poolShunt), DeviceValueUOM::PERCENT);
}
// EMS+
if (flags == EMSdevice::EMS_DEVICE_FLAG_MMPLUS) {
if (device_id <= 0x27) {
// telegram handlers 0x20 - 0x27 for HC
if (device_id >= 0x20 && device_id <= 0x27) {
register_telegram_type(device_id - 0x20 + 0x02D7, F("MMPLUSStatusMessage_HC"), true, MAKE_PF_CB(process_MMPLUSStatusMessage_HC));
} else {
// telegram handlers for warm water/DHW 0x28, 0x29
// register_telegram_type(device_id - 0x20 + 0x02E1, F("MMPLUSStetMessage_HC"), true, MAKE_PF_CB(process_MMPLUSSetMessage_HC));
type_ = Type::HC;
hc_ = device_id - 0x20 + 1;
uint8_t tag = TAG_HC1 + hc_ - 1;
register_device_value(tag, &id_, DeviceValueType::UINT, nullptr, FL_(ID), DeviceValueUOM::NONE);
register_device_value(tag, &flowTempHc_, DeviceValueType::USHORT, FL_(div10), FL_(flowTempHc), DeviceValueUOM::DEGREES);
register_device_value(tag, &status_, DeviceValueType::INT, nullptr, FL_(mixerStatus), DeviceValueUOM::PERCENT);
register_device_value(tag, &flowSetTemp_, DeviceValueType::UINT, nullptr, FL_(flowSetTemp), DeviceValueUOM::DEGREES, MAKE_CF_CB(set_flowSetTemp));
register_device_value(tag, &pumpStatus_, DeviceValueType::BOOL, nullptr, FL_(pumpStatus), DeviceValueUOM::NONE, MAKE_CF_CB(set_pump));
} else if (device_id >= 0x28 && device_id <= 0x29) {
register_telegram_type(device_id - 0x28 + 0x0331, F("MMPLUSStatusMessage_WWC"), true, MAKE_PF_CB(process_MMPLUSStatusMessage_WWC));
// register_telegram_type(device_id - 0x28 + 0x033B, F("MMPLUSSetMessage_WWC"), true, MAKE_PF_CB(process_MMPLUSSetMessage_WWC));
type_ = Type::WWC;
hc_ = device_id - 0x28 + 1;
uint8_t tag = TAG_WWC1 + hc_ - 1;
register_device_value(tag, &id_, DeviceValueType::UINT, nullptr, FL_(ID), DeviceValueUOM::NONE);
register_device_value(tag, &flowTempHc_, DeviceValueType::USHORT, FL_(div10), FL_(wwTemp), DeviceValueUOM::DEGREES);
register_device_value(tag, &pumpStatus_, DeviceValueType::BOOL, nullptr, FL_(wwPumpStatus), DeviceValueUOM::NONE);
register_device_value(tag, &status_, DeviceValueType::INT, nullptr, FL_(wwTempStatus), DeviceValueUOM::NONE);
}
}
// EMS 1.0
if (flags == EMSdevice::EMS_DEVICE_FLAG_MM10) {
register_telegram_type(0x00AA, F("MMConfigMessage"), false, MAKE_PF_CB(process_MMConfigMessage));
register_telegram_type(0x00AB, F("MMStatusMessage"), true, MAKE_PF_CB(process_MMStatusMessage));
register_telegram_type(0x00AA, F("MMConfigMessage"), true, MAKE_PF_CB(process_MMConfigMessage));
register_telegram_type(0x00AB, F("MMStatusMessage"), false, MAKE_PF_CB(process_MMStatusMessage));
register_telegram_type(0x00AC, F("MMSetMessage"), false, MAKE_PF_CB(process_MMSetMessage));
// EMSESP::send_read_request(0xAA, device_id);
type_ = Type::HC;
hc_ = device_id - 0x20 + 1;
uint8_t tag = TAG_HC1 + hc_ - 1;
register_device_value(tag, &id_, DeviceValueType::UINT, nullptr, FL_(ID), DeviceValueUOM::NONE);
register_device_value(tag, &flowTempHc_, DeviceValueType::USHORT, FL_(div10), FL_(flowTempHc), DeviceValueUOM::DEGREES);
register_device_value(tag, &status_, DeviceValueType::INT, nullptr, FL_(mixerStatus), DeviceValueUOM::PERCENT);
register_device_value(tag, &flowSetTemp_, DeviceValueType::UINT, nullptr, FL_(flowSetTemp), DeviceValueUOM::DEGREES, MAKE_CF_CB(set_flowSetTemp));
register_device_value(tag, &pumpStatus_, DeviceValueType::BOOL, nullptr, FL_(pumpStatus), DeviceValueUOM::NONE, MAKE_CF_CB(set_pump));
register_device_value(tag, &activated_, DeviceValueType::BOOL, nullptr, FL_(activated), DeviceValueUOM::NONE, MAKE_CF_CB(set_activated));
register_device_value(tag, &setValveTime_, DeviceValueType::UINT, FL_(mul10), FL_(mixerSetTime), DeviceValueUOM::SECONDS, MAKE_CF_CB(set_setValveTime), 1, 12);
}
// HT3
if (flags == EMSdevice::EMS_DEVICE_FLAG_IPM) {
register_telegram_type(0x010C, F("IPMStatusMessage"), false, MAKE_PF_CB(process_IPMStatusMessage));
register_telegram_type(0x001E, F("IPMTempMessage"), false, MAKE_PF_CB(process_IPMTempMessage));
// register_telegram_type(0x0023, F("IPMSetMessage"), false, MAKE_PF_CB(process_IPMSetMessage));
}
// register the device values and set hc_ and type_
if (device_id <= 0x27) {
register_telegram_type(0x011E, F("IPMTempMessage"), false, MAKE_PF_CB(process_IPMTempMessage));
// register_telegram_type(0x0123, F("IPMSetMessage"), false, MAKE_PF_CB(process_IPMSetMessage));
type_ = Type::HC;
hc_ = device_id - 0x20 + 1;
uint8_t tag = TAG_HC1 + hc_ - 1;
register_device_value(tag, &id_, DeviceValueType::UINT, nullptr, FL_(ID), DeviceValueUOM::NONE);
register_device_value(tag, &flowSetTemp_, DeviceValueType::UINT, nullptr, FL_(flowSetTemp), DeviceValueUOM::DEGREES);
register_device_value(tag, &flowTempHc_, DeviceValueType::USHORT, FL_(div10), FL_(flowTempHc), DeviceValueUOM::DEGREES);
register_device_value(tag, &pumpStatus_, DeviceValueType::BOOL, nullptr, FL_(pumpStatus), DeviceValueUOM::BOOLEAN);
register_device_value(tag, &status_, DeviceValueType::INT, nullptr, FL_(mixerStatus), DeviceValueUOM::PERCENT);
register_device_value(tag, &flowSetTemp_, DeviceValueType::UINT, nullptr, FL_(flowSetTemp), DeviceValueUOM::DEGREES, MAKE_CF_CB(set_flowSetTemp));
register_device_value(tag, &pumpStatus_, DeviceValueType::BOOL, nullptr, FL_(pumpStatus), DeviceValueUOM::NONE, MAKE_CF_CB(set_pump));
register_device_value(tag, &flowTempVf_, DeviceValueType::USHORT, FL_(div10), FL_(flowTempVf), DeviceValueUOM::DEGREES);
} else {
type_ = Type::WWC;
hc_ = device_id - 0x28 + 1;
uint8_t tag = TAG_WWC1 + hc_ - 1;
register_device_value(tag, &id_, DeviceValueType::UINT, nullptr, FL_(ID), DeviceValueUOM::NONE);
register_device_value(tag, &flowTempHc_, DeviceValueType::USHORT, FL_(div10), FL_(wwTemp), DeviceValueUOM::DEGREES);
register_device_value(tag, &pumpStatus_, DeviceValueType::BOOL, nullptr, FL_(wwPumpStatus), DeviceValueUOM::BOOLEAN);
register_device_value(tag, &status_, DeviceValueType::INT, nullptr, FL_(wwTempStatus), DeviceValueUOM::NONE);
}
id_ = product_id;
}
// publish HA config
bool Mixer::publish_ha_config() {
bool Mixer::publish_ha_device_config() {
// if we don't have valid values for this HC don't add it ever again
if (!Helpers::hasValue(pumpStatus_)) {
return false;
@@ -86,24 +109,34 @@ bool Mixer::publish_ha_config() {
StaticJsonDocument<EMSESP_JSON_SIZE_HA_CONFIG> doc;
char uniq_id[20];
snprintf_P(uniq_id, sizeof(uniq_id), PSTR("Mixer%02X"), device_id() - 0x20 + 1);
if (type_ == Type::MP) {
snprintf(uniq_id, sizeof(uniq_id), "MixerMP");
} else {
snprintf(uniq_id, sizeof(uniq_id), "Mixer%02X", device_id() - 0x20 + 1);
}
doc["uniq_id"] = uniq_id;
doc["ic"] = F_(icondevice);
char stat_t[Mqtt::MQTT_TOPIC_MAX_SIZE];
snprintf_P(stat_t, sizeof(stat_t), PSTR("%s/%s"), Mqtt::base().c_str(), Mqtt::tag_to_topic(device_type(), DeviceValueTAG::TAG_NONE).c_str());
snprintf(stat_t, sizeof(stat_t), "%s/%s", Mqtt::base().c_str(), Mqtt::tag_to_topic(device_type(), DeviceValueTAG::TAG_NONE).c_str());
doc["stat_t"] = stat_t;
char name[20];
snprintf_P(name, sizeof(name), PSTR("Mixer %02X"), device_id() - 0x20 + 1);
if (type_ == Type::MP) {
snprintf(name, sizeof(name), "Mixer MP");
} else {
snprintf(name, sizeof(name), "Mixer %02X", device_id() - 0x20 + 1);
}
doc["name"] = name;
char tpl[30];
if (type_ == Type::HC) {
snprintf_P(tpl, sizeof(tpl), PSTR("{{value_json.hc%d.id}}"), device_id() - 0x20 + 1);
snprintf(tpl, sizeof(tpl), "{{value_json.hc%d.id}}", device_id() - 0x20 + 1);
} else if (type_ == Type::WWC) {
snprintf(tpl, sizeof(tpl), "{{value_json.wwc%d.id}}", device_id() - 0x28 + 1);
} else {
snprintf_P(tpl, sizeof(tpl), PSTR("{{value_json.wwc%d.id}}"), device_id() - 0x28 + 1);
snprintf(tpl, sizeof(tpl), "{{value_json.id}}");
}
doc["val_tpl"] = tpl;
@@ -118,9 +151,11 @@ bool Mixer::publish_ha_config() {
// determine the topic, if its HC and WWC. This is determined by the incoming telegram types.
std::string topic(Mqtt::MQTT_TOPIC_MAX_SIZE, '\0');
if (type_ == Type::HC) {
snprintf_P(&topic[0], topic.capacity() + 1, PSTR("sensor/%s/mixer_hc%d/config"), Mqtt::base().c_str(), hc_);
} else {
snprintf_P(&topic[0], topic.capacity() + 1, PSTR("sensor/%s/mixer_wwc%d/config"), Mqtt::base().c_str(), hc_); // WWC
snprintf(&topic[0], topic.capacity() + 1, "sensor/%s/mixer_hc%d/config", Mqtt::base().c_str(), hc_);
} else if (type_ == Type::WWC) {
snprintf(&topic[0], topic.capacity() + 1, "sensor/%s/mixer_wwc%d/config", Mqtt::base().c_str(), hc_); // WWC
} else if (type_ == Type::MP) {
snprintf(&topic[0], topic.capacity() + 1, "sensor/%s/mixer_mp/config", Mqtt::base().c_str());
}
Mqtt::publish_ha(topic, doc.as<JsonObject>()); // publish the config payload with retain flag
@@ -188,16 +223,24 @@ void Mixer::process_MMStatusMessage(std::shared_ptr<const Telegram> telegram) {
has_update(telegram->read_value(status_, 4)); // valve status -100 to 100
}
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wunused-parameter"
// Pool mixer MP100, - 0x5BA
void Mixer::process_HpPoolStatus(std::shared_ptr<const Telegram> telegram) {
has_update(telegram->read_value(poolTemp_, 0));
has_update(telegram->read_value(poolShuntStatus__, 2));
has_update(telegram->read_value(poolShunt_, 3)); // 0-100% how much is the shunt open?
poolShuntStatus_ = poolShunt_ == 100 ? 3 : (poolShunt_ == 0 ? 4 : poolShuntStatus__);
}
// Mixer on a MM10 - 0xAA
// e.g. Thermostat -> Mixer Module, type 0xAA, telegram: 10 21 AA 00 FF 0C 0A 11 0A 32 xx
void Mixer::process_MMConfigMessage(std::shared_ptr<const Telegram> telegram) {
// pos 0: active FF = on
// pos 1: valve runtime 0C = 120 sec in units of 10 sec
has_update(telegram->read_value(activated_, 0)); // on = 0xFF
has_update(telegram->read_value(setValveTime_, 1)); // valve runtime in 10 sec, max 120 s
}
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wunused-parameter"
// Mixer on a MM10 - 0xAC
// e.g. Thermostat -> Mixer Module, type 0xAC, telegram: 10 21 AC 00 1E 64 01 AB
void Mixer::process_MMSetMessage(std::shared_ptr<const Telegram> telegram) {
@@ -213,4 +256,75 @@ void Mixer::process_IPMSetMessage(std::shared_ptr<const Telegram> telegram) {
#pragma GCC diagnostic pop
bool Mixer::set_flowSetTemp(const char * value, const int8_t id) {
int v;
if (!Helpers::value2number(value, v)) {
return false;
}
LOG_INFO(F("Setting mixer flow set temperature to %d"), v);
if (flags() == EMSdevice::EMS_DEVICE_FLAG_MM10) {
write_command(0xAC, 0, v, 0xAB);
return true;
}
if (flags() == EMSdevice::EMS_DEVICE_FLAG_IPM) {
write_command(0x123, 0, v, 0x10C);
return true;
}
if (flags() == EMSdevice::EMS_DEVICE_FLAG_MMPLUS) {
uint8_t hc = device_id() - 0x20;
write_command(0x2E1 + hc, 1, v, 0x2D7 + hc);
return true;
}
return false;
}
bool Mixer::set_pump(const char * value, const int8_t id) {
bool b;
if (!Helpers::value2bool(value, b)) {
return false;
}
LOG_INFO(F("Setting mixer pump %s"), b ? "on" : "off");
if (flags() == EMSdevice::EMS_DEVICE_FLAG_MM10) {
write_command(0xAC, 1, b ? 0x64 : 0, 0xAB);
return true;
}
if (flags() == EMSdevice::EMS_DEVICE_FLAG_IPM) {
write_command(0x123, 1, b ? 0x64 : 0, 0x10C);
return true;
}
if (flags() == EMSdevice::EMS_DEVICE_FLAG_MMPLUS) {
uint8_t hc = device_id() - 0x20;
write_command(0x2E1 + hc, 2, b ? 0x64 : 0, 0x2D7 + hc);
return true;
}
return false;
}
bool Mixer::set_activated(const char * value, const int8_t id) {
bool b;
if (!Helpers::value2bool(value, b)) {
return false;
}
if (flags() == EMSdevice::EMS_DEVICE_FLAG_MM10) {
LOG_INFO(F("Setting mixer %s"), value);
write_command(0xAA, 0, b ? 0xFF : 0, 0xAA);
return true;
}
return false;
}
bool Mixer::set_setValveTime(const char * value, const int8_t id) {
int v;
if (!Helpers::value2number(value, v)) {
return false;
}
if (flags() == EMSdevice::EMS_DEVICE_FLAG_MM10) {
v = (v + 5) / 10;
LOG_INFO(F("Setting mixer valve time to %ds"), v * 10);
write_command(0xAA, 1, v, 0xAA);
return true;
}
return false;
}
} // namespace emsesp

View File

@@ -27,7 +27,7 @@ class Mixer : public EMSdevice {
public:
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);
virtual bool publish_ha_config();
virtual bool publish_ha_device_config();
private:
static uuid::log::Logger logger_;
@@ -40,11 +40,19 @@ class Mixer : public EMSdevice {
void process_MMStatusMessage(std::shared_ptr<const Telegram> telegram);
void process_MMConfigMessage(std::shared_ptr<const Telegram> telegram);
void process_MMSetMessage(std::shared_ptr<const Telegram> telegram);
void process_HpPoolStatus(std::shared_ptr<const Telegram> telegram);
bool set_flowSetTemp(const char * value, const int8_t id);
bool set_pump(const char * value, const int8_t id);
bool set_activated(const char * value, const int8_t id);
bool set_setValveTime(const char * value, const int8_t id);
enum class Type {
NONE,
HC, // heating circuit
WWC // warm water circuit
HC, // heating circuit
WWC, // warm water circuit
MP // pool
};
private:
@@ -53,9 +61,16 @@ class Mixer : public EMSdevice {
uint8_t pumpStatus_;
int8_t status_;
uint8_t flowSetTemp_;
uint8_t activated_;
uint8_t setValveTime_;
Type type_ = Type::NONE;
uint16_t hc_ = EMS_VALUE_USHORT_NOTSET;
int16_t poolTemp_;
int8_t poolShuntStatus_;
int8_t poolShunt_;
Type type_ = Type::NONE;
uint16_t hc_ = EMS_VALUE_USHORT_NOTSET;
int8_t poolShuntStatus__ = EMS_VALUE_INT_NOTSET; // temp value
uint8_t id_;
};

View File

@@ -26,8 +26,6 @@ uuid::log::Logger Solar::logger_{F_(solar), uuid::log::Facility::CONSOLE};
Solar::Solar(uint8_t device_type, uint8_t device_id, uint8_t product_id, const std::string & version, const std::string & name, uint8_t flags, uint8_t brand)
: EMSdevice(device_type, device_id, product_id, version, name, flags, brand) {
LOG_DEBUG(F("Adding new Solar module with device ID 0x%02X"), device_id);
// telegram handlers
if (flags == EMSdevice::EMS_DEVICE_FLAG_SM10) {
register_telegram_type(0x97, F("SM10Monitor"), false, MAKE_PF_CB(process_SM10Monitor));
@@ -61,26 +59,25 @@ Solar::Solar(uint8_t device_type, uint8_t device_id, uint8_t product_id, const s
}
// device values...
// special case for a device_id with 0x2A where it's not actual a solar module
if (device_id == 0x2A) {
register_device_value(TAG_NONE, &type_, DeviceValueType::TEXT, nullptr, FL_(type), DeviceValueUOM::NONE);
strlcpy(type_, "warm water circuit", sizeof(type_));
register_device_value(TAG_NONE, &wwTemp_1_, DeviceValueType::UINT, nullptr, FL_(wwTemp1), DeviceValueUOM::DEGREES);
register_device_value(TAG_NONE, &wwTemp_3_, DeviceValueType::UINT, nullptr, FL_(wwTemp3), DeviceValueUOM::DEGREES);
register_device_value(TAG_NONE, &wwTemp_4_, DeviceValueType::UINT, nullptr, FL_(wwTemp4), DeviceValueUOM::DEGREES);
register_device_value(TAG_NONE, &wwTemp_5_, DeviceValueType::UINT, nullptr, FL_(wwTemp5), DeviceValueUOM::DEGREES);
register_device_value(TAG_NONE, &wwTemp_7_, DeviceValueType::UINT, nullptr, FL_(wwTemp7), DeviceValueUOM::DEGREES);
register_device_value(TAG_NONE, &wwPump_, DeviceValueType::UINT, nullptr, FL_(wwPump), DeviceValueUOM::DEGREES);
return;
}
register_device_value(TAG_NONE, &id_, DeviceValueType::UINT, nullptr, FL_(ID), DeviceValueUOM::NONE);
id_ = product_id;
// special case for a device_id with 0x2A where it's not actual a solar module
if (device_id == 0x2A) {
register_device_value(TAG_NONE, &type_, DeviceValueType::STRING, nullptr, FL_(type), DeviceValueUOM::NONE);
strlcpy(type_, "warm water circuit", sizeof(type_));
register_device_value(TAG_DEVICE_DATA_WW, &wwTemp_1_, DeviceValueType::UINT, nullptr, FL_(wwTemp1), DeviceValueUOM::DEGREES);
register_device_value(TAG_DEVICE_DATA_WW, &wwTemp_3_, DeviceValueType::UINT, nullptr, FL_(wwTemp3), DeviceValueUOM::DEGREES);
register_device_value(TAG_DEVICE_DATA_WW, &wwTemp_4_, DeviceValueType::UINT, nullptr, FL_(wwTemp4), DeviceValueUOM::DEGREES);
register_device_value(TAG_DEVICE_DATA_WW, &wwTemp_5_, DeviceValueType::UINT, nullptr, FL_(wwTemp5), DeviceValueUOM::DEGREES);
register_device_value(TAG_DEVICE_DATA_WW, &wwTemp_7_, DeviceValueType::UINT, nullptr, FL_(wwTemp7), DeviceValueUOM::DEGREES);
register_device_value(TAG_DEVICE_DATA_WW, &wwPump_, DeviceValueType::UINT, nullptr, FL_(wwPump), DeviceValueUOM::DEGREES);
return;
}
register_device_value(TAG_NONE, &collectorTemp_, DeviceValueType::SHORT, FL_(div10), FL_(collectorTemp), DeviceValueUOM::DEGREES);
register_device_value(TAG_NONE, &tankBottomTemp_, DeviceValueType::SHORT, FL_(div10), FL_(tankBottomTemp), DeviceValueUOM::DEGREES);
register_device_value(TAG_NONE, &solarPump_, DeviceValueType::BOOL, nullptr, FL_(solarPump), DeviceValueUOM::BOOLEAN);
register_device_value(TAG_NONE, &solarPump_, DeviceValueType::BOOL, nullptr, FL_(solarPump), DeviceValueUOM::NONE);
register_device_value(TAG_NONE, &pumpWorkTime_, DeviceValueType::TIME, nullptr, FL_(pumpWorkTime), DeviceValueUOM::MINUTES);
register_device_value(TAG_NONE, &tankMaxTemp_, DeviceValueType::UINT, nullptr, FL_(tankMaxTemp), DeviceValueUOM::DEGREES, MAKE_CF_CB(set_TankMaxTemp));
@@ -91,32 +88,37 @@ Solar::Solar(uint8_t device_type, uint8_t device_id, uint8_t product_id, const s
TAG_NONE, &solarPumpTurnonDiff_, DeviceValueType::UINT, nullptr, FL_(solarPumpTurnonDiff), DeviceValueUOM::DEGREES, MAKE_CF_CB(set_TurnonDiff));
register_device_value(
TAG_NONE, &solarPumpTurnoffDiff_, DeviceValueType::UINT, nullptr, FL_(solarPumpTurnoffDiff), DeviceValueUOM::DEGREES, MAKE_CF_CB(set_TurnoffDiff));
register_device_value(TAG_NONE, &collectorShutdown_, DeviceValueType::BOOL, nullptr, FL_(collectorShutdown), DeviceValueUOM::BOOLEAN);
register_device_value(TAG_NONE, &tankHeated_, DeviceValueType::BOOL, nullptr, FL_(tankHeated), DeviceValueUOM::BOOLEAN);
register_device_value(TAG_NONE, &collectorShutdown_, DeviceValueType::BOOL, nullptr, FL_(collectorShutdown), DeviceValueUOM::NONE);
register_device_value(TAG_NONE, &tankHeated_, DeviceValueType::BOOL, nullptr, FL_(tankHeated), DeviceValueUOM::NONE);
register_device_value(TAG_NONE, &solarPower_, DeviceValueType::ULONG, nullptr, FL_(solarPower), DeviceValueUOM::W);
register_device_value(TAG_NONE, &energyLastHour_, DeviceValueType::ULONG, FL_(div10), FL_(energyLastHour), DeviceValueUOM::WH);
register_device_value(TAG_NONE, &maxFlow_, DeviceValueType::UINT, FL_(div10), FL_(maxFlow), DeviceValueUOM::LMIN, MAKE_CF_CB(set_SM10MaxFlow));
register_device_value(TAG_NONE, &wwMinTemp_, DeviceValueType::UINT, nullptr, FL_(wwMinTemp), DeviceValueUOM::DEGREES, MAKE_CF_CB(set_wwMinTemp));
register_device_value(TAG_NONE, &solarIsEnabled_, DeviceValueType::BOOL, nullptr, FL_(activated), DeviceValueUOM::BOOLEAN, MAKE_CF_CB(set_solarEnabled));
register_device_value(TAG_DEVICE_DATA_WW, &wwMinTemp_, DeviceValueType::UINT, nullptr, FL_(wwMinTemp), DeviceValueUOM::DEGREES, MAKE_CF_CB(set_wwMinTemp));
register_device_value(TAG_NONE, &solarIsEnabled_, DeviceValueType::BOOL, nullptr, FL_(activated), DeviceValueUOM::NONE, MAKE_CF_CB(set_solarEnabled));
}
if (flags == EMSdevice::EMS_DEVICE_FLAG_ISM) {
register_device_value(TAG_NONE, &collectorShutdown_, DeviceValueType::BOOL, nullptr, FL_(collectorShutdown), DeviceValueUOM::BOOLEAN);
register_device_value(TAG_NONE, &tankHeated_, DeviceValueType::BOOL, nullptr, FL_(tankHeated), DeviceValueUOM::BOOLEAN);
register_device_value(TAG_NONE, &collectorShutdown_, DeviceValueType::BOOL, nullptr, FL_(collectorShutdown), DeviceValueUOM::NONE);
register_device_value(TAG_NONE, &tankHeated_, DeviceValueType::BOOL, nullptr, FL_(tankHeated), DeviceValueUOM::NONE);
register_device_value(TAG_NONE, &energyLastHour_, DeviceValueType::ULONG, FL_(div10), FL_(energyLastHour), DeviceValueUOM::WH);
}
if (flags == EMSdevice::EMS_DEVICE_FLAG_SM100) {
register_device_value(TAG_NONE, &solarPumpModulation_, DeviceValueType::UINT, nullptr, FL_(solarPumpModulation), DeviceValueUOM::PERCENT);
register_device_value(TAG_NONE, &solarPumpMinMod_, DeviceValueType::UINT, nullptr, FL_(pumpMinMod), DeviceValueUOM::PERCENT, MAKE_CF_CB(set_PumpMinMod));
register_device_value(
TAG_NONE, &solarPumpTurnonDiff_, DeviceValueType::UINT, nullptr, FL_(solarPumpTurnonDiff), DeviceValueUOM::DEGREES, MAKE_CF_CB(set_TurnonDiff));
register_device_value(
TAG_NONE, &solarPumpTurnoffDiff_, DeviceValueType::UINT, nullptr, FL_(solarPumpTurnoffDiff), DeviceValueUOM::DEGREES, MAKE_CF_CB(set_TurnoffDiff));
TAG_NONE, &solarPumpTurnonDiff_, DeviceValueType::UINT, FL_(div10), FL_(solarPumpTurnonDiff), DeviceValueUOM::DEGREES, MAKE_CF_CB(set_TurnonDiff));
register_device_value(TAG_NONE,
&solarPumpTurnoffDiff_,
DeviceValueType::UINT,
FL_(div10),
FL_(solarPumpTurnoffDiff),
DeviceValueUOM::DEGREES,
MAKE_CF_CB(set_TurnoffDiff));
register_device_value(TAG_NONE, &tankBottomTemp2_, DeviceValueType::SHORT, FL_(div10), FL_(tank2BottomTemp), DeviceValueUOM::DEGREES);
register_device_value(TAG_NONE, &heatExchangerTemp_, DeviceValueType::SHORT, FL_(div10), FL_(heatExchangerTemp), DeviceValueUOM::DEGREES);
register_device_value(TAG_NONE, &cylinderPumpModulation_, DeviceValueType::UINT, nullptr, FL_(cylinderPumpModulation), DeviceValueUOM::PERCENT);
register_device_value(TAG_NONE, &valveStatus_, DeviceValueType::BOOL, nullptr, FL_(valveStatus), DeviceValueUOM::BOOLEAN);
register_device_value(TAG_NONE, &tankHeated_, DeviceValueType::BOOL, nullptr, FL_(tankHeated), DeviceValueUOM::BOOLEAN);
register_device_value(TAG_NONE, &collectorShutdown_, DeviceValueType::BOOL, nullptr, FL_(collectorShutdown), DeviceValueUOM::BOOLEAN);
register_device_value(TAG_NONE, &valveStatus_, DeviceValueType::BOOL, nullptr, FL_(valveStatus), DeviceValueUOM::NONE);
register_device_value(TAG_NONE, &tankHeated_, DeviceValueType::BOOL, nullptr, FL_(tankHeated), DeviceValueUOM::NONE);
register_device_value(TAG_NONE, &collectorShutdown_, DeviceValueType::BOOL, nullptr, FL_(collectorShutdown), DeviceValueUOM::NONE);
register_device_value(
TAG_NONE, &collectorMaxTemp_, DeviceValueType::UINT, nullptr, FL_(collectorMaxTemp), DeviceValueUOM::DEGREES, MAKE_CF_CB(set_CollectorMaxTemp));
register_device_value(
@@ -125,42 +127,37 @@ Solar::Solar(uint8_t device_type, uint8_t device_id, uint8_t product_id, const s
register_device_value(TAG_NONE, &energyToday_, DeviceValueType::ULONG, nullptr, FL_(energyToday), DeviceValueUOM::WH);
register_device_value(TAG_NONE, &energyTotal_, DeviceValueType::ULONG, FL_(div10), FL_(energyTotal), DeviceValueUOM::KWH);
register_device_value(TAG_NONE,
&heatTransferSystem_,
DeviceValueType::BOOL,
nullptr,
FL_(heatTransferSystem),
DeviceValueUOM::BOOLEAN,
MAKE_CF_CB(set_heatTransferSystem));
register_device_value(TAG_NONE, &externalTank_, DeviceValueType::BOOL, nullptr, FL_(externalTank), DeviceValueUOM::BOOLEAN, MAKE_CF_CB(set_externalTank));
register_device_value(
TAG_NONE, &thermalDisinfect_, DeviceValueType::BOOL, nullptr, FL_(thermalDisinfect), DeviceValueUOM::BOOLEAN, MAKE_CF_CB(set_thermalDisinfect));
register_device_value(TAG_NONE, &heatMetering_, DeviceValueType::BOOL, nullptr, FL_(heatMetering), DeviceValueUOM::BOOLEAN, MAKE_CF_CB(set_heatMetering));
register_device_value(TAG_NONE, &solarIsEnabled_, DeviceValueType::BOOL, nullptr, FL_(activated), DeviceValueUOM::BOOLEAN, MAKE_CF_CB(set_solarEnabled));
TAG_NONE, &heatTransferSystem_, DeviceValueType::BOOL, nullptr, FL_(heatTransferSystem), DeviceValueUOM::NONE, MAKE_CF_CB(set_heatTransferSystem));
register_device_value(TAG_NONE, &externalTank_, DeviceValueType::BOOL, nullptr, FL_(externalTank), DeviceValueUOM::NONE, MAKE_CF_CB(set_externalTank));
register_device_value(
TAG_NONE, &thermalDisinfect_, DeviceValueType::BOOL, nullptr, FL_(thermalDisinfect), DeviceValueUOM::NONE, MAKE_CF_CB(set_thermalDisinfect));
register_device_value(TAG_NONE, &heatMetering_, DeviceValueType::BOOL, nullptr, FL_(heatMetering), DeviceValueUOM::NONE, MAKE_CF_CB(set_heatMetering));
register_device_value(TAG_NONE, &solarIsEnabled_, DeviceValueType::BOOL, nullptr, FL_(activated), DeviceValueUOM::NONE, MAKE_CF_CB(set_solarEnabled));
// telegram 0x035A
register_device_value(
TAG_NONE, &solarPumpMode_, DeviceValueType::ENUM, FL_(enum_solarmode), FL_(solarPumpMode), DeviceValueUOM::LIST, MAKE_CF_CB(set_solarMode));
TAG_NONE, &solarPumpMode_, DeviceValueType::ENUM, FL_(enum_solarmode), FL_(solarPumpMode), DeviceValueUOM::NONE, MAKE_CF_CB(set_solarMode));
register_device_value(TAG_NONE,
&solarPumpKick_,
DeviceValueType::BOOL,
nullptr,
FL_(solarPumpKick),
DeviceValueUOM::BOOLEAN,
DeviceValueUOM::NONE,
MAKE_CF_CB(set_solarPumpKick)); // pump kick for vacuum collector, 00=off
register_device_value(TAG_NONE,
&plainWaterMode_,
DeviceValueType::BOOL,
nullptr,
FL_(plainWaterMode),
DeviceValueUOM::BOOLEAN,
DeviceValueUOM::NONE,
MAKE_CF_CB(set_plainWaterMode)); // system does not use antifreeze, 00=off
register_device_value(TAG_NONE,
&doubleMatchFlow_,
DeviceValueType::BOOL,
nullptr,
FL_(doubleMatchFlow),
DeviceValueUOM::BOOLEAN,
DeviceValueUOM::NONE,
MAKE_CF_CB(set_doubleMatchFlow)); // double Match Flow, 00=off
// telegram 0x380
@@ -177,19 +174,19 @@ Solar::Solar(uint8_t device_type, uint8_t device_id, uint8_t product_id, const s
DeviceValueType::ENUM,
FL_(enum_collectortype),
FL_(collector1Type),
DeviceValueUOM::LIST,
DeviceValueUOM::NONE,
MAKE_CF_CB(set_collector1Type)); // Type of collector field 1, 01=flat, 02=vacuum
}
}
// publish HA config
bool Solar::publish_ha_config() {
bool Solar::publish_ha_device_config() {
StaticJsonDocument<EMSESP_JSON_SIZE_HA_CONFIG> doc;
doc["uniq_id"] = F_(solar);
doc["ic"] = F_(icondevice);
char stat_t[Mqtt::MQTT_TOPIC_MAX_SIZE];
snprintf_P(stat_t, sizeof(stat_t), PSTR("%s/%s"), Mqtt::base().c_str(), Mqtt::tag_to_topic(device_type(), DeviceValueTAG::TAG_NONE).c_str());
snprintf(stat_t, sizeof(stat_t), "%s/%s", Mqtt::base().c_str(), Mqtt::tag_to_topic(device_type(), DeviceValueTAG::TAG_NONE).c_str());
doc["stat_t"] = stat_t;
char name_s[40];
@@ -206,7 +203,7 @@ bool Solar::publish_ha_config() {
ids.add("ems-esp-solar");
char topic[Mqtt::MQTT_TOPIC_MAX_SIZE];
snprintf_P(topic, sizeof(topic), PSTR("sensor/%s/solar/config"), Mqtt::base().c_str());
snprintf(topic, sizeof(topic), "sensor/%s/solar/config", Mqtt::base().c_str());
Mqtt::publish_ha(topic, doc.as<JsonObject>()); // publish the config payload with retain flag
return true;
@@ -530,27 +527,27 @@ bool Solar::set_wwMinTemp(const char * value, const int8_t id) {
}
bool Solar::set_TurnoffDiff(const char * value, const int8_t id) {
int temperature;
if (!Helpers::value2number(value, temperature)) {
float temperature;
if (!Helpers::value2float(value, temperature)) {
return false;
}
if (flags() == EMSdevice::EMS_DEVICE_FLAG_SM10) {
write_command(0x96, 8, (uint8_t)temperature, 0x96);
} else {
write_command(0x35A, 7, (uint8_t)temperature, 0x35A);
write_command(0x35A, 7, (uint8_t)(temperature * 10), 0x35A);
}
return true;
}
bool Solar::set_TurnonDiff(const char * value, const int8_t id) {
int temperature;
if (!Helpers::value2number(value, temperature)) {
float temperature;
if (!Helpers::value2float(value, temperature)) {
return false;
}
if (flags() == EMSdevice::EMS_DEVICE_FLAG_SM10) {
write_command(0x96, 7, (uint8_t)temperature, 0x96);
} else {
write_command(0x35A, 8, (uint8_t)temperature, 0x35A);
write_command(0x35A, 8, (uint8_t)(temperature * 10), 0x35A);
}
return true;
}

View File

@@ -27,7 +27,7 @@ class Solar : public EMSdevice {
public:
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);
virtual bool publish_ha_config();
virtual bool publish_ha_device_config();
private:
static uuid::log::Logger logger_;

View File

@@ -28,21 +28,19 @@ 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));
register_device_value(TAG_NONE, &id_, DeviceValueType::UINT, nullptr, FL_(ID), DeviceValueUOM::NONE);
register_device_value(TAG_NONE, &activated_, DeviceValueType::BOOL, nullptr, FL_(activated), DeviceValueUOM::BOOLEAN);
register_device_value(TAG_NONE, &activated_, DeviceValueType::BOOL, nullptr, FL_(activated), DeviceValueUOM::NONE);
register_device_value(TAG_NONE, &flowTempHc_, DeviceValueType::USHORT, FL_(div10), FL_(flowTempHc), DeviceValueUOM::DEGREES);
register_device_value(TAG_NONE, &status_, DeviceValueType::INT, nullptr, FL_(status), DeviceValueUOM::NONE);
id_ = product_id;
}
// publish HA config
bool Switch::publish_ha_config() {
bool Switch::publish_ha_device_config() {
// if we don't have valid values don't add it ever again
if (!Helpers::hasValue(flowTempHc_)) {
return false;
@@ -53,11 +51,11 @@ bool Switch::publish_ha_config() {
doc["ic"] = F_(icondevice);
char stat_t[Mqtt::MQTT_TOPIC_MAX_SIZE];
snprintf_P(stat_t, sizeof(stat_t), PSTR("%s/%s"), Mqtt::base().c_str(), Mqtt::tag_to_topic(device_type(), DeviceValueTAG::TAG_NONE).c_str());
snprintf(stat_t, sizeof(stat_t), "%s/%s", Mqtt::base().c_str(), Mqtt::tag_to_topic(device_type(), DeviceValueTAG::TAG_NONE).c_str());
doc["stat_t"] = stat_t;
char name_s[40];
snprintf_P(name_s, sizeof(name_s), FSTR_(productid_fmt), device_type_name().c_str());
snprintf(name_s, sizeof(name_s), FSTR_(productid_fmt), device_type_name().c_str());
doc["name"] = name_s;
doc["val_tpl"] = FJSON("{{value_json.id}}");
@@ -70,7 +68,7 @@ bool Switch::publish_ha_config() {
ids.add("ems-esp-switch");
char topic[Mqtt::MQTT_TOPIC_MAX_SIZE];
snprintf_P(topic, sizeof(topic), PSTR("sensor/%s/switch/config"), Mqtt::base().c_str());
snprintf(topic, sizeof(topic), "sensor/%s/switch/config", Mqtt::base().c_str());
Mqtt::publish_ha(topic, doc.as<JsonObject>()); // publish the config payload with retain flag
return true;

View File

@@ -27,7 +27,7 @@ class Switch : public EMSdevice {
public:
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);
virtual bool publish_ha_config();
virtual bool publish_ha_device_config();
private:
static uuid::log::Logger logger_;

File diff suppressed because it is too large Load Diff

View File

@@ -55,6 +55,7 @@ class Thermostat : public EMSdevice {
uint8_t manualtemp;
uint8_t summer_setmode;
uint8_t roominfluence;
int16_t curroominfl;
uint8_t flowtempoffset;
uint8_t minflowtemp;
uint8_t maxflowtemp;
@@ -65,6 +66,13 @@ class Thermostat : public EMSdevice {
uint8_t pause;
uint8_t party;
int8_t noreducetemp; // signed -20°C to +10°C
uint8_t wwprio;
uint8_t fastHeatup;
char holiday[22];
char vacation[22];
// RC 10
uint8_t reducehours; // night reduce duration
uint16_t reduceminutes; // remaining minutes to night->day
uint8_t hc_num() const {
return hc_num_;
@@ -119,7 +127,7 @@ class Thermostat : public EMSdevice {
static std::string mode_tostring(uint8_t mode);
virtual bool publish_ha_config();
virtual bool publish_ha_device_config();
private:
static uuid::log::Logger logger_;
@@ -137,6 +145,7 @@ class Thermostat : public EMSdevice {
std::vector<uint16_t> set_typeids;
std::vector<uint16_t> timer_typeids;
std::vector<uint16_t> summer_typeids;
std::vector<uint16_t> summer2_typeids;
std::vector<uint16_t> curve_typeids;
// standard for all thermostats
@@ -154,7 +163,10 @@ class Thermostat : public EMSdevice {
int8_t ibaCalIntTemperature_; // offset int. temperature sensor, by * 0.1 Kelvin (-5.0 to 5.0K)
int8_t ibaMinExtTemperature_; // min ext temp for heating curve, in deg., 0xF6=-10, 0x0 = 0, 0xFF=-1
uint8_t ibaBuildingType_; // building type: 0 = light, 1 = medium, 2 = heavy
uint8_t ibaClockOffset_; // offset (in sec) to clock, 0xff = -1 s, 0x02 = 2 s
int8_t ibaClockOffset_; // offset (in sec) to clock, 0xff = -1 s, 0x02 = 2 s
uint8_t ibaDamping_; // damping 0-off, 0xff-on
uint8_t backlight_;
uint8_t heatingpid_;
int8_t dampedoutdoortemp_;
uint16_t tempsensor1_;
@@ -170,6 +182,15 @@ class Thermostat : public EMSdevice {
uint8_t wwCircMode_;
uint8_t wwSetTemp_;
uint8_t wwSetTempLow_;
uint8_t wwCharge_;
uint8_t wwChargeDuration_;
uint8_t wwDisinfect_;
uint8_t wwDisinfectDay_;
uint8_t wwDisinfectHour_;
uint8_t wwMaxTemp_;
uint8_t wwOneTimeKey_;
uint8_t wwProgMode_;
uint8_t wwCircProg_;
std::vector<std::shared_ptr<HeatingCircuit>> heating_circuits_; // each thermostat can have multiple heating circuits
@@ -255,7 +276,7 @@ class Thermostat : public EMSdevice {
std::shared_ptr<Thermostat::HeatingCircuit> heating_circuit(std::shared_ptr<const Telegram> telegram);
std::shared_ptr<Thermostat::HeatingCircuit> heating_circuit(const uint8_t hc_num);
void publish_ha_config_hc(uint8_t hc_num);
void publish_ha_config_hc(std::shared_ptr<Thermostat::HeatingCircuit> hc);
void register_device_values_hc(std::shared_ptr<Thermostat::HeatingCircuit> hc);
bool thermostat_ha_cmd(const char * message, uint8_t hc_num);
@@ -278,10 +299,12 @@ class Thermostat : public EMSdevice {
void process_RC20Set_2(std::shared_ptr<const Telegram> telegram);
void process_RC10Monitor(std::shared_ptr<const Telegram> telegram);
void process_RC10Set(std::shared_ptr<const Telegram> telegram);
void process_RC10Set_2(std::shared_ptr<const Telegram> telegram);
void process_CRFMonitor(std::shared_ptr<const Telegram> telegram);
void process_RC300Monitor(std::shared_ptr<const Telegram> telegram);
void process_RC300Set(std::shared_ptr<const Telegram> telegram);
void process_RC300Summer(std::shared_ptr<const Telegram> telegram);
void process_RC300Summer2(std::shared_ptr<const Telegram> telegram);
void process_RC300WWmode(std::shared_ptr<const Telegram> telegram);
void process_RC300WWmode2(std::shared_ptr<const Telegram> telegram);
void process_RC300WWtemp(std::shared_ptr<const Telegram> telegram);
@@ -303,7 +326,10 @@ class Thermostat : public EMSdevice {
// set functions - these use the id/hc
bool set_mode(const char * value, const int8_t id);
bool set_control(const char * value, const int8_t id);
bool set_holiday(const char * value, const int8_t id);
bool set_holiday(const char * value, const int8_t id, const bool vacation = false);
bool set_vacation(const char * value, const int8_t id) {
return set_holiday(value, id, true);
}
bool set_pause(const char * value, const int8_t id);
bool set_party(const char * value, const int8_t id);
bool set_summermode(const char * value, const int8_t id);
@@ -331,20 +357,36 @@ class Thermostat : public EMSdevice {
bool set_switchtime(const char * value, const int8_t id);
bool set_program(const char * value, const int8_t id);
bool set_controlmode(const char * value, const int8_t id);
bool set_wwprio(const char * value, const int8_t id);
bool set_fastheatup(const char * value, const int8_t id);
// set functions - these don't use the id/hc, the parameters are ignored
bool set_wwmode(const char * value, const int8_t id);
bool set_wwtemp(const char * value, const int8_t id);
bool set_wwtemplow(const char * value, const int8_t id);
bool set_wwonetime(const char * value, const int8_t id);
bool set_wwcircmode(const char * value, const int8_t id);
bool set_wwcharge(const char * value, const int8_t id);
bool set_wwchargeduration(const char * value, const int8_t id);
bool set_wwDisinfect(const char * value, const int8_t id);
bool set_wwDisinfectDay(const char * value, const int8_t id);
bool set_wwDisinfectHour(const char * value, const int8_t id);
bool set_wwMaxTemp(const char * value, const int8_t id);
bool set_wwOneTimeKey(const char * value, const int8_t id);
bool set_wwProgMode(const char * value, const int8_t id);
bool set_wwCircProg(const char * value, const int8_t id);
bool set_datetime(const char * value, const int8_t id);
bool set_minexttemp(const char * value, const int8_t id);
bool set_clockoffset(const char * value, const int8_t id);
bool set_calinttemp(const char * value, const int8_t id);
bool set_display(const char * value, const int8_t id);
bool set_building(const char * value, const int8_t id);
bool set_damping(const char * value, const int8_t id);
bool set_language(const char * value, const int8_t id);
bool set_heatingtype(const char * value, const int8_t id);
bool set_reducehours(const char * value, const int8_t id);
bool set_backlight(const char * value, const int8_t id);
bool set_heatingpid(const char * value, const int8_t id);
};
} // namespace emsesp

File diff suppressed because it is too large Load Diff

View File

@@ -39,14 +39,13 @@ enum DeviceValueType : uint8_t {
ULONG,
TIME, // same as ULONG (32 bits)
ENUM,
TEXT,
STRING,
CMD // special for commands only
};
// Unit Of Measurement mapping - maps to DeviceValueUOM_s in emsdevice.cpp
// uom - also used with HA
// sequence is important!
// Unit Of Measurement mapping - maps to DeviceValueUOM_s in emsdevice.cpp. Sequence is important!!
// also used with HA as uom
enum DeviceValueUOM : uint8_t {
NONE = 0, // 0
@@ -64,9 +63,9 @@ enum DeviceValueUOM : uint8_t {
KB, // 12
SECONDS, // 13
DBM, // 14
NUM, // 15
BOOLEAN, // 16
LIST // 17
MV, // 15
TIMES, // 16
OCLOCK // 17
};
@@ -78,11 +77,11 @@ MAKE_PSTR(icontime, "mdi:clock-outline") // DeviceValueUOM::SECONDS MIN
MAKE_PSTR(iconkb, "mdi:memory") // DeviceValueUOM::KB
MAKE_PSTR(iconlmin, "mdi:water-boiler") // DeviceValueUOM::LMIN
MAKE_PSTR(iconkwh, "mdi:transmission-tower") // DeviceValueUOM::KWH & WH
MAKE_PSTR(iconua, "mdi:flash-circle") // DeviceValueUOM::UA
MAKE_PSTR(iconua, "mdi:lightning-bolt-circle") // DeviceValueUOM::UA
MAKE_PSTR(iconbar, "mdi:gauge") // DeviceValueUOM::BAR
MAKE_PSTR(iconkw, "mdi:omega") // DeviceValueUOM::KW & W
MAKE_PSTR(icondbm, "mdi:wifi-strength-2") // DeviceValueUOM::DBM
MAKE_PSTR(iconnum, "mdi:counter") // DeviceValueUOM::NUM
MAKE_PSTR(iconnum, "mdi:counter") // DeviceValueUOM::NONE
MAKE_PSTR(icondevice, "mdi:home-automation") // for devices in HA
@@ -91,7 +90,7 @@ enum DeviceValueTAG : uint8_t {
TAG_NONE = 0, // wild card
TAG_HEARTBEAT,
TAG_BOILER_DATA,
TAG_BOILER_DATA_WW,
TAG_DEVICE_DATA_WW,
TAG_THERMOSTAT_DATA,
TAG_HC1,
TAG_HC2,
@@ -120,8 +119,15 @@ enum DeviceValueTAG : uint8_t {
};
// mqtt-HA flags
enum DeviceValueHA : uint8_t { HA_NONE = 0, HA_VALUE, HA_DONE };
// states of a device value
enum DeviceValueState : uint8_t {
DV_DEFAULT = 0, // 0 - does not yet have a value
DV_ACTIVE = (1 << 0), // 1 - has a value
DV_VISIBLE = (1 << 1), // 2 - shown on web and console
DV_HA_CONFIG_CREATED = (1 << 2) // 4 - set if the HA config has been created
};
class EMSdevice {
public:
@@ -130,7 +136,7 @@ class EMSdevice {
static constexpr uint8_t EMS_DEVICES_MAX_TELEGRAMS = 20;
// virtual functions overrules by derived classes
virtual bool publish_ha_config() = 0;
virtual bool publish_ha_device_config() = 0;
// device_type defines which derived class to use, e.g. BOILER, THERMOSTAT etc..
EMSdevice(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)
@@ -143,18 +149,18 @@ class EMSdevice {
, brand_(brand) {
}
inline uint8_t device_id() const {
return device_id_;
}
std::string device_type_name() const;
static std::string device_type_2_device_name(const uint8_t device_type);
static uint8_t device_name_2_device_type(const char * topic);
const std::string device_type_name() const;
static const std::string device_type_2_device_name(const uint8_t device_type);
static uint8_t device_name_2_device_type(const char * topic);
static const std::string uom_to_string(uint8_t uom);
static const std::string tag_to_string(uint8_t tag);
static const std::string tag_to_mqtt(uint8_t tag);
inline uint8_t device_id() const {
return device_id_;
}
inline uint8_t product_id() const {
return product_id_;
}
@@ -181,9 +187,8 @@ class EMSdevice {
return flags_;
}
// see enum DeviceType below
inline uint8_t device_type() const {
return device_type_;
return device_type_; // see enum DeviceType below
}
inline void version(std::string & version) {
@@ -226,26 +231,28 @@ class EMSdevice {
has_update_ |= has_update;
}
std::string brand_to_string() const;
static uint8_t decode_brand(uint8_t value);
const std::string brand_to_string() const;
static uint8_t decode_brand(uint8_t value);
std::string to_string() const;
std::string to_string_short() const;
const std::string to_string() const;
const std::string to_string_short() const;
void show_telegram_handlers(uuid::console::Shell & shell);
void show_device_values_debug(uuid::console::Shell & shell);
char * show_telegram_handlers(char * result);
void show_mqtt_handlers(uuid::console::Shell & shell);
void list_device_entries(JsonObject & output);
using process_function_p = std::function<void(std::shared_ptr<const Telegram>)>;
void register_telegram_type(const uint16_t telegram_type_id, const __FlashStringHelper * telegram_type_name, bool fetch, const process_function_p cb);
bool handle_telegram(std::shared_ptr<const Telegram> telegram);
std::string get_value_uom(const char * key);
bool get_value_info(JsonObject & root, const char * cmd, const int8_t id);
bool generate_values_json(JsonObject & json, const uint8_t tag_filter, const bool nested, const bool console = false);
void generate_values_json_web(JsonObject & json);
const std::string get_value_uom(const char * key);
bool get_value_info(JsonObject & root, const char * cmd, const int8_t id);
enum OUTPUT_TARGET : uint8_t { API_VERBOSE, API_SHORTNAMES, MQTT };
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,
@@ -286,15 +293,13 @@ 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_entity_config();
void publish_mqtt_ha_sensor();
std::string telegram_type_name(std::shared_ptr<const Telegram> telegram);
const std::string telegram_type_name(std::shared_ptr<const Telegram> telegram);
void fetch_values();
void toggle_fetch(uint16_t telegram_id, bool toggle);
bool get_toggle_fetch(uint16_t telegram_id);
bool is_fetch(uint16_t telegram_id);
bool ha_config_done() const {
return ha_config_done_;
@@ -313,7 +318,8 @@ class EMSdevice {
BUDERUS, // 3
NEFIT, // 4
SIEGER, // 5
WORCESTER // 11
WORCESTER, // 11
IVT // 13
};
enum DeviceType : uint8_t {
@@ -359,6 +365,7 @@ class EMSdevice {
static constexpr uint8_t EMS_DEVICE_FLAG_MMPLUS = 1;
static constexpr uint8_t EMS_DEVICE_FLAG_MM10 = 2;
static constexpr uint8_t EMS_DEVICE_FLAG_IPM = 3;
static constexpr uint8_t EMS_DEVICE_FLAG_MP = 4;
// Thermostats
static constexpr uint8_t EMS_DEVICE_FLAG_NO_WRITE = (1 << 7); // last bit
@@ -410,13 +417,14 @@ class EMSdevice {
}
};
// DeviceValue holds all the attributes for a device value (also a device parameter)
struct DeviceValue {
uint8_t device_type; // EMSdevice::DeviceType
uint8_t tag; // DeviceValueTAG::*
void * value_p; // pointer to variable of any type
uint8_t type; // DeviceValueType::*
const __FlashStringHelper * const * options; // options as a flash char array
uint8_t options_size; // # options in the char array, calculated
uint8_t options_size; // number of options in the char array, calculated
const __FlashStringHelper * short_name; // used in MQTT
const __FlashStringHelper * full_name; // used in Web and Console
uint8_t uom; // DeviceValueUOM::*
@@ -424,6 +432,7 @@ class EMSdevice {
bool has_cmd; // true if there is a Console/MQTT command which matches the short_name
int32_t min;
uint32_t max;
uint8_t state; // DeviceValueState::*
DeviceValue(uint8_t device_type,
uint8_t tag,
@@ -437,7 +446,8 @@ class EMSdevice {
uint8_t ha,
bool has_cmd,
int32_t min,
uint32_t max)
uint32_t max,
uint8_t state)
: device_type(device_type)
, tag(tag)
, value_p(value_p)
@@ -450,18 +460,36 @@ class EMSdevice {
, ha(ha)
, has_cmd(has_cmd)
, min(min)
, max(max) {
, max(max)
, state(state) {
}
// state flags
inline void add_state(uint8_t s) {
state |= s;
}
inline bool has_state(uint8_t s) const {
return (state & s) == s;
}
inline void remove_state(uint8_t s) {
state &= ~s;
}
inline uint8_t get_state() const {
return state;
}
};
const std::vector<DeviceValue> devicevalues() const;
std::vector<TelegramFunction> telegram_functions_; // each EMS device has its own set of registered telegram types
std::vector<DeviceValue> devicevalues_;
const std::string device_entity_ha(DeviceValue const & dv);
bool check_dv_hasvalue(const DeviceValue & dv);
void init_devicevalues(uint8_t size) {
devicevalues_.reserve(size);
}
std::vector<TelegramFunction> telegram_functions_; // each EMS device has its own set of registered telegram types
std::vector<DeviceValue> devicevalues_;
};
} // namespace emsesp

View File

@@ -74,10 +74,10 @@ uint32_t EMSESP::last_fetch_ = 0;
uint8_t EMSESP::publish_all_idx_ = 0;
uint8_t EMSESP::unique_id_count_ = 0;
bool EMSESP::trace_raw_ = false;
uint64_t EMSESP::tx_delay_ = 0;
uint8_t EMSESP::bool_format_ = 1;
uint8_t EMSESP::enum_format_ = 1;
uint16_t EMSESP::wait_validate_ = 0;
bool EMSESP::wait_km_ = true;
// for a specific EMS device go and request data values
// or if device_id is 0 it will fetch from all our known and active devices
@@ -94,6 +94,18 @@ void EMSESP::fetch_device_values(const uint8_t device_id) {
}
}
// see if the device ID exists
bool EMSESP::valid_device(const uint8_t device_id) {
for (const auto & emsdevice : emsdevices) {
if (emsdevice) {
if (emsdevice->is_device_id(device_id)) {
return true;
}
}
}
return false; // can't find it
}
// for a specific EMS device type go and request data values
void EMSESP::fetch_device_values_type(const uint8_t device_type) {
for (const auto & emsdevice : emsdevices) {
@@ -180,10 +192,9 @@ void EMSESP::init_uart() {
uint8_t rx_gpio;
uint8_t tx_gpio;
EMSESP::webSettingsService.read([&](WebSettings & settings) {
tx_mode = settings.tx_mode;
tx_delay_ = settings.tx_delay * 1000;
rx_gpio = settings.rx_gpio;
tx_gpio = settings.tx_gpio;
tx_mode = settings.tx_mode;
rx_gpio = settings.rx_gpio;
tx_gpio = settings.tx_gpio;
});
EMSuart::stop();
@@ -301,7 +312,7 @@ void EMSESP::show_ems(uuid::console::Shell & shell) {
}
// show EMS device values to the shell console
// generate_values_json is called in verbose mode (set to true)
// generate_values_json is called in verbose mode
void EMSESP::show_device_values(uuid::console::Shell & shell) {
if (emsdevices.empty()) {
shell.printfln(F("No EMS devices detected. Try using 'scan devices' from the ems menu."));
@@ -318,7 +329,7 @@ void EMSESP::show_device_values(uuid::console::Shell & shell) {
DynamicJsonDocument doc(EMSESP_JSON_SIZE_XLARGE_DYN); // use max size
JsonObject json = doc.to<JsonObject>();
emsdevice->generate_values_json(json, DeviceValueTAG::TAG_NONE, true, true); // console mode and nested
emsdevice->generate_values_json(json, DeviceValueTAG::TAG_NONE, true, EMSdevice::OUTPUT_TARGET::API_VERBOSE); // verbose mode and nested
// print line
uint8_t id = 0;
@@ -426,7 +437,6 @@ void EMSESP::publish_all_loop() {
Mqtt::ha_status();
}
system_.send_heartbeat();
shower_.send_mqtt_stat(false, true);
break;
default:
// all finished
@@ -447,30 +457,25 @@ void EMSESP::reset_mqtt_ha() {
}
// create json doc for the devices values and add to MQTT publish queue
// generate_values_json is called without verbose mode (defaults to false)
// generate_values_json is called to build the device value (dv) object array
void EMSESP::publish_device_values(uint8_t device_type) {
DynamicJsonDocument doc(EMSESP_JSON_SIZE_XLARGE_DYN); // use max size
JsonObject json = doc.to<JsonObject>();
bool need_publish = false;
uint8_t nested = Mqtt::nested_format();
bool nested = (Mqtt::nested_format() == 1); // 1 is nested, 2 is single
// group by device type
for (const auto & emsdevice : emsdevices) {
if (emsdevice && (emsdevice->device_type() == device_type)) {
// if we're using HA, done is checked for each sensor in devices
if (Mqtt::ha_enabled()) {
emsdevice->publish_mqtt_ha_sensor(); // create the configs for each value as a sensor
}
// if its a boiler, generate json for each group and publish it directly
// if its a boiler, generate json for each group and publish it directly. not nested
if (device_type == DeviceType::BOILER) {
if (emsdevice->generate_values_json(json, DeviceValueTAG::TAG_BOILER_DATA, false)) {
if (emsdevice->generate_values_json(json, DeviceValueTAG::TAG_BOILER_DATA, false, EMSdevice::OUTPUT_TARGET::MQTT)) {
Mqtt::publish(Mqtt::tag_to_topic(device_type, DeviceValueTAG::TAG_BOILER_DATA), json);
}
doc.clear();
if (emsdevice->generate_values_json(json, DeviceValueTAG::TAG_BOILER_DATA_WW, false)) {
Mqtt::publish(Mqtt::tag_to_topic(device_type, DeviceValueTAG::TAG_BOILER_DATA_WW), json);
if (emsdevice->generate_values_json(json, DeviceValueTAG::TAG_DEVICE_DATA_WW, false, EMSdevice::OUTPUT_TARGET::MQTT)) {
Mqtt::publish(Mqtt::tag_to_topic(device_type, DeviceValueTAG::TAG_DEVICE_DATA_WW), json);
}
need_publish = false;
}
@@ -480,15 +485,15 @@ void EMSESP::publish_device_values(uint8_t device_type) {
// only publish the single master thermostat
if (emsdevice->device_id() == EMSESP::actual_master_thermostat()) {
if (nested) {
need_publish |= emsdevice->generate_values_json(json, DeviceValueTAG::TAG_NONE, true); // nested
need_publish |= emsdevice->generate_values_json(json, DeviceValueTAG::TAG_NONE, true, EMSdevice::OUTPUT_TARGET::MQTT); // nested
} else {
if (emsdevice->generate_values_json(json, DeviceValueTAG::TAG_THERMOSTAT_DATA, false)) { // not nested
if (emsdevice->generate_values_json(json, DeviceValueTAG::TAG_THERMOSTAT_DATA, false, EMSdevice::OUTPUT_TARGET::MQTT)) { // not nested
Mqtt::publish(Mqtt::tag_to_topic(device_type, DeviceValueTAG::TAG_NONE), json);
}
doc.clear();
for (uint8_t hc_tag = TAG_HC1; hc_tag <= DeviceValueTAG::TAG_HC4; hc_tag++) {
if (emsdevice->generate_values_json(json, hc_tag, false)) { // not nested
if (emsdevice->generate_values_json(json, hc_tag, false, EMSdevice::OUTPUT_TARGET::MQTT)) { // not nested
Mqtt::publish(Mqtt::tag_to_topic(device_type, hc_tag), json);
}
doc.clear();
@@ -501,10 +506,10 @@ void EMSESP::publish_device_values(uint8_t device_type) {
// Mixer
else if (device_type == DeviceType::MIXER) {
if (nested) {
need_publish |= emsdevice->generate_values_json(json, DeviceValueTAG::TAG_NONE, true); // nested
need_publish |= emsdevice->generate_values_json(json, DeviceValueTAG::TAG_NONE, true, EMSdevice::OUTPUT_TARGET::MQTT); // nested
} else {
for (uint8_t hc_tag = TAG_HC1; hc_tag <= DeviceValueTAG::TAG_WWC4; hc_tag++) {
if (emsdevice->generate_values_json(json, hc_tag, false)) { // not nested
if (emsdevice->generate_values_json(json, hc_tag, false, EMSdevice::OUTPUT_TARGET::MQTT)) { // not nested
Mqtt::publish(Mqtt::tag_to_topic(device_type, hc_tag), json);
}
doc.clear();
@@ -514,15 +519,20 @@ void EMSESP::publish_device_values(uint8_t device_type) {
} else {
// for all other devices add the values to the json
need_publish |= emsdevice->generate_values_json(json, DeviceValueTAG::TAG_NONE, true); // nested
need_publish |= emsdevice->generate_values_json(json, DeviceValueTAG::TAG_NONE, true, EMSdevice::OUTPUT_TARGET::MQTT); // nested
}
}
// if we're using HA, done is checked for each sensor in devices
if (Mqtt::ha_enabled()) {
emsdevice->publish_mqtt_ha_entity_config(); // create the configs for each value as a sensor
}
}
// publish it under a single topic, only if we have data to publish
if (need_publish) {
char topic[Mqtt::MQTT_TOPIC_MAX_SIZE];
snprintf_P(topic, sizeof(topic), PSTR("%s_data"), EMSdevice::device_type_2_device_name(device_type).c_str());
snprintf(topic, sizeof(topic), "%s_data", EMSdevice::device_type_2_device_name(device_type).c_str());
Mqtt::publish(topic, json);
}
}
@@ -543,12 +553,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];
@@ -556,7 +562,7 @@ void EMSESP::publish_response(std::shared_ptr<const Telegram> telegram) {
doc["dest"] = Helpers::hextoa(buffer, telegram->dest);
doc["type"] = Helpers::hextoa(buffer, telegram->type_id);
doc["offset"] = Helpers::hextoa(buffer, telegram->offset);
strcpy(buffer, Helpers::data_to_hex(telegram->message_data, telegram->message_length - 1).c_str()); // exclude CRC
strcpy(buffer, Helpers::data_to_hex(telegram->message_data, telegram->message_length).c_str()); // telegram is without crc
doc["data"] = buffer;
if (telegram->message_length <= 4) {
@@ -570,7 +576,7 @@ void EMSESP::publish_response(std::shared_ptr<const Telegram> telegram) {
Mqtt::publish(F_(response), doc.as<JsonObject>());
}
// builds json with the detail of each value, for all specific device type or the dallas sensor
// builds json with the detail of each value, for a specific EMS device type or the dallas sensor
bool EMSESP::get_device_value_info(JsonObject & root, const char * cmd, const int8_t id, const uint8_t devicetype) {
for (const auto & emsdevice : emsdevices) {
if (emsdevice->device_type() == devicetype) {
@@ -578,11 +584,12 @@ 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()) {
char sensorID[10];
snprintf_P(sensorID, 10, PSTR("sensor%d"), i++);
snprintf(sensorID, 10, "sensor%d", i++);
if ((strcmp(cmd, sensorID) == 0) || (strcmp(cmd, Helpers::toLower(sensor.to_string()).c_str()) == 0)) {
root["name"] = sensor.to_string();
if (Helpers::hasValue(sensor.temperature_c)) {
@@ -669,30 +676,30 @@ std::string EMSESP::pretty_telegram(std::shared_ptr<const Telegram> telegram) {
std::string str(200, '\0');
if (offset) {
snprintf_P(&str[0],
str.capacity() + 1,
PSTR("%s(0x%02X) %s %s(0x%02X), %s(0x%02X), data: %s (offset %d)"),
src_name.c_str(),
src,
direction.c_str(),
dest_name.c_str(),
dest,
type_name.c_str(),
telegram->type_id,
telegram->to_string_message().c_str(),
offset);
snprintf(&str[0],
str.capacity() + 1,
"%s(0x%02X) %s %s(0x%02X), %s(0x%02X), data: %s (offset %d)",
src_name.c_str(),
src,
direction.c_str(),
dest_name.c_str(),
dest,
type_name.c_str(),
telegram->type_id,
telegram->to_string_message().c_str(),
offset);
} else {
snprintf_P(&str[0],
str.capacity() + 1,
PSTR("%s(0x%02X) %s %s(0x%02X), %s(0x%02X), data: %s"),
src_name.c_str(),
src,
direction.c_str(),
dest_name.c_str(),
dest,
type_name.c_str(),
telegram->type_id,
telegram->to_string_message().c_str());
snprintf(&str[0],
str.capacity() + 1,
"%s(0x%02X) %s %s(0x%02X), %s(0x%02X), data: %s",
src_name.c_str(),
src,
direction.c_str(),
dest_name.c_str(),
dest,
type_name.c_str(),
telegram->type_id,
telegram->to_string_message().c_str());
}
return str;
@@ -723,9 +730,7 @@ void EMSESP::process_UBADevices(std::shared_ptr<const Telegram> telegram) {
uint8_t device_id = ((data_byte + 1) * 8) + bit;
// if we haven't already detected this device, request it's version details, unless its us (EMS-ESP)
// when the version info is received, it will automagically add the device
// always skip modem device 0x0D, it does not reply to version request
// see https://github.com/emsesp/EMS-ESP/issues/460#issuecomment-709553012
if ((device_id != EMSbus::ems_bus_id()) && !(EMSESP::device_exists(device_id)) && (device_id != 0x0D) && (device_id != 0x0C)) {
if ((device_id != EMSbus::ems_bus_id()) && !(EMSESP::device_exists(device_id))) {
LOG_DEBUG(F("New EMS device detected with ID 0x%02X. Requesting version information."), device_id);
send_read_request(EMSdevice::EMS_TYPE_VERSION, device_id);
}
@@ -741,6 +746,11 @@ void EMSESP::process_UBADevices(std::shared_ptr<const Telegram> telegram) {
void EMSESP::process_version(std::shared_ptr<const Telegram> telegram) {
// check for valid telegram, just in case
if (telegram->message_length < 3) {
// for empty telegram add device with empty product, version and brand
if (!telegram->message_length) {
std::string version = "00.00";
(void)add_device(telegram->src, 0, version, 0);
}
return;
}
@@ -761,7 +771,7 @@ void EMSESP::process_version(std::shared_ptr<const Telegram> telegram) {
// get version as XX.XX
std::string version(6, '\0');
snprintf_P(&version[0], version.capacity() + 1, PSTR("%02d.%02d"), telegram->message_data[offset + 1], telegram->message_data[offset + 2]);
snprintf(&version[0], version.capacity() + 1, "%02d.%02d", telegram->message_data[offset + 1], telegram->message_data[offset + 2]);
// some devices store the protocol type (HT3, Buderus) in the last byte
uint8_t brand;
@@ -783,7 +793,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;
}
@@ -804,16 +817,14 @@ bool EMSESP::process_telegram(std::shared_ptr<const Telegram> telegram) {
return false;
}
// remember if we first get scan results from UBADevices
static bool first_scan_done_ = false;
// check for common types, like the Version(0x02)
if (telegram->type_id == EMSdevice::EMS_TYPE_VERSION) {
process_version(telegram);
return true;
} else if (telegram->type_id == EMSdevice::EMS_TYPE_UBADevices) {
process_UBADevices(telegram);
if (telegram->dest == EMSbus::ems_bus_id()) {
first_scan_done_ = true;
// do not flood tx-queue with version requests while waiting for km200
if (!wait_km_) {
process_UBADevices(telegram);
}
return true;
}
@@ -853,8 +864,7 @@ bool EMSESP::process_telegram(std::shared_ptr<const Telegram> telegram) {
if (watch() == WATCH_UNKNOWN) {
LOG_NOTICE(F("%s"), pretty_telegram(telegram).c_str());
}
if (first_scan_done_ && !knowndevice && (telegram->src != EMSbus::ems_bus_id()) && (telegram->src != 0x0B) && (telegram->src != 0x0C)
&& (telegram->src != 0x0D)) {
if (!wait_km_ && !knowndevice && (telegram->src != EMSbus::ems_bus_id()) && (telegram->message_length > 0)) {
send_read_request(EMSdevice::EMS_TYPE_VERSION, telegram->src);
}
}
@@ -886,25 +896,30 @@ void EMSESP::show_devices(uuid::console::Shell & shell) {
shell.printfln(F("These EMS devices are currently active:"));
shell.println();
// count the number of thermostats
uint8_t num_thermostats = 0;
for (const auto & emsdevice : emsdevices) {
if ((emsdevice) && (emsdevice->device_type() == DeviceType::THERMOSTAT)) {
num_thermostats++;
}
}
// for all device objects from emsdevice.h (UNKNOWN, SYSTEM, BOILER, THERMOSTAT, MIXER, SOLAR, HEATPUMP, GATEWAY, SWITCH, CONTROLLER, CONNECT)
// so we keep a consistent order
for (const auto & device_class : EMSFactory::device_handlers()) {
// shell.printf(F("[factory ID: %d] "), device_class.first);
for (const auto & emsdevice : emsdevices) {
if ((emsdevice) && (emsdevice->device_type() == device_class.first)) {
shell.printf(F("(%d) %s: %s"), emsdevice->unique_id(), emsdevice->device_type_name().c_str(), emsdevice->to_string().c_str());
if ((emsdevice->device_type() == EMSdevice::DeviceType::THERMOSTAT) && (emsdevice->device_id() == actual_master_thermostat())) {
shell.printf(F(" ** master device **"));
if ((num_thermostats > 1) && (emsdevice->device_type() == EMSdevice::DeviceType::THERMOSTAT)
&& (emsdevice->device_id() == actual_master_thermostat())) {
shell.printf(F(" **master device**"));
}
shell.println();
emsdevice->show_telegram_handlers(shell);
#if defined(EMSESP_DEBUG)
emsdevice->show_mqtt_handlers(shell);
shell.println();
// emsdevice->show_device_values_debug(shell);
#endif
shell.println();
}
}
@@ -932,8 +947,8 @@ bool EMSESP::add_device(const uint8_t device_id, const uint8_t product_id, std::
}
// find the name and flags in our database
for (const auto & device : device_library_) {
if (device.product_id == product_id) {
emsdevice->name(std::move(uuid::read_flash_string(device.name)));
if (device.product_id == product_id && device.device_type == emsdevice->device_type()) {
emsdevice->name(std::move(read_flash_string(device.name)));
emsdevice->add_flags(device.flags);
}
}
@@ -972,49 +987,111 @@ bool EMSESP::add_device(const uint8_t device_id, const uint8_t product_id, std::
return false; // not found
}
auto name = uuid::read_flash_string(device_p->name);
auto name = read_flash_string(device_p->name);
auto device_type = device_p->device_type;
auto flags = device_p->flags;
// empty reply to version, read a generic device from database
if (product_id == 0) {
// check for known device IDs
if (device_id == 0x40) {
// see: https://github.com/emsesp/EMS-ESP32/issues/103#issuecomment-911717342
name = "rf room temperature sensor"; // generic
} else if (device_id == 0x17) {
name = "generic thermostat";
device_type = DeviceType::THERMOSTAT;
flags = DeviceFlags::EMS_DEVICE_FLAG_RC10 | DeviceFlags::EMS_DEVICE_FLAG_NO_WRITE;
} else if (device_id == 0x04) {
name = "RS232";
device_type = DeviceType::CONNECT;
} else if (device_id == 0x0A) {
name = "terminal";
device_type = DeviceType::CONNECT;
} else if (device_id == 0x0B) {
name = "service key";
device_type = DeviceType::CONNECT;
} else if (device_id == 0x0C) {
name = "cascade";
device_type = DeviceType::CONNECT;
} else if (device_id == 0x0D) {
// see https://github.com/emsesp/EMS-ESP/issues/460#issuecomment-709553012
name = "modem";
device_type = DeviceType::CONNECT;
} else if (device_id == 0x0E) {
name = "converter"; // generic
} else {
LOG_WARNING(F("Unrecognized EMS device (device ID 0x%02X, no product ID). Please report on GitHub."), device_id);
return false;
}
}
LOG_DEBUG(F("Adding new device %s (device ID 0x%02X, product ID %d, version %s)"), name.c_str(), device_id, product_id, version.c_str());
emsdevices.push_back(EMSFactory::add(device_type, device_id, product_id, version, name, flags, brand));
emsdevices.back()->unique_id(++unique_id_count_);
fetch_device_values(device_id); // go and fetch its data
// add info commands, but not for all devices
// add command commands for all devices, except for connect, controller and gateway
if ((device_type == DeviceType::CONNECT) || (device_type == DeviceType::CONTROLLER) || (device_type == DeviceType::GATEWAY)) {
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, true); },
[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, false); },
F("values"),
[device_type](const char * value, const int8_t id, JsonObject & output) {
return command_info(device_type, output, id, EMSdevice::OUTPUT_TARGET::API_SHORTNAMES); // HIDDEN command showing short names, used in e.g. /api/boiler
},
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(
device_type,
F_(entities),
[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 e.g. "ems-esp/boiler/#"
Mqtt::subscribe(device_type, EMSdevice::device_type_2_device_name(device_type) + "/#", nullptr);
// 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 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);
// list device entities
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(output);
return true;
}
}
return false;
}
// export all values to info command
// 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, bool verbose) {
// list all available commands, return as json
bool EMSESP::command_commands(uint8_t device_type, JsonObject & output, const int8_t id) {
return Command::list(device_type, output);
}
// export all values for a specific device
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) {
@@ -1030,7 +1107,7 @@ bool EMSESP::command_info(uint8_t device_type, JsonObject & json, const int8_t i
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), verbose && (id == -1)); // nested for id -1,0 & console for id -1
has_value |= emsdevice->generate_values_json(output, tag, (id < 1), output_target); // use nested for id -1 and 0
}
}
@@ -1132,17 +1209,21 @@ void EMSESP::incoming_telegram(uint8_t * data, const uint8_t length) {
// check for poll
if (length == 1) {
static uint64_t delayed_tx_start_ = 0;
if (!rxservice_.bus_connected() && (tx_delay_ > 0)) {
delayed_tx_start_ = uuid::get_uptime_ms();
LOG_DEBUG(F("Tx delay started"));
// if ht3 poll must be ems_bus_id else if Buderus poll must be (ems_bus_id | 0x80)
uint8_t poll_id = (first_value ^ 0x80 ^ rxservice_.ems_mask());
static uint32_t connect_time = 0;
if (!rxservice_.bus_connected()) {
wait_km_ = true;
connect_time = uuid::get_uptime_sec();
}
if ((first_value ^ 0x80 ^ rxservice_.ems_mask()) == txservice_.ems_bus_id()) {
if (poll_id == txservice_.ems_bus_id()) {
EMSbus::last_bus_activity(uuid::get_uptime()); // set the flag indication the EMS bus is active
}
// first send delayed after connect
if ((uuid::get_uptime_ms() - delayed_tx_start_) < tx_delay_) {
return;
if (wait_km_) {
if (poll_id != 0x48 && (uuid::get_uptime_sec() - connect_time) < EMS_WAIT_KM_TIMEOUT) {
return;
}
wait_km_ = false; // KM200 is polled, from now on it is safe to send
}
#ifdef EMSESP_UART_DEBUG
@@ -1156,12 +1237,11 @@ void EMSESP::incoming_telegram(uint8_t * data, const uint8_t length) {
}
#endif
// check for poll to us, if so send top message from Tx queue immediately and quit
// if ht3 poll must be ems_bus_id else if Buderus poll must be (ems_bus_id | 0x80)
if ((first_value ^ 0x80 ^ rxservice_.ems_mask()) == txservice_.ems_bus_id()) {
if (poll_id == txservice_.ems_bus_id()) {
txservice_.send();
}
// send remote room temperature if active
Roomctrl::send(first_value ^ 0x80 ^ rxservice_.ems_mask());
Roomctrl::send(poll_id);
return;
} else {
#ifdef EMSESP_UART_DEBUG
@@ -1213,6 +1293,7 @@ void EMSESP::start() {
emsdevices.reserve(5); // reserve space for initially 5 devices to avoid mem frag issues
LOG_INFO(F("Last system reset reason Core0: %s, Core1: %s"), system_.reset_reason(0).c_str(), system_.reset_reason(1).c_str());
LOG_INFO(F("EMS Device library loaded with %d records"), device_library_.size());
#if defined(EMSESP_STANDALONE)
@@ -1244,7 +1325,7 @@ void EMSESP::loop() {
console_.loop(); // telnet/serial console
// https://github.com/emsesp/EMS-ESP32/issues/78#issuecomment-877599145
delay(1); // helps telnet catch up. don't think its needed in ESP32 3.1.0
// delay(1); // helps telnet catch up. don't think its needed in ESP32 >3.1.0?
}
} // namespace emsesp

View File

@@ -55,14 +55,20 @@
#define WATCH_ID_NONE 0 // no watch id set
#define EMSESP_JSON_SIZE_HA_CONFIG 768 // for HA config payloads, using StaticJsonDocument
#define EMSESP_JSON_SIZE_SMALL 256 // for smaller json docs, using StaticJsonDocument
#define EMSESP_JSON_SIZE_MEDIUM 768 // for medium json docs from ems devices, using StaticJsonDocument
#define EMSESP_JSON_SIZE_LARGE 1024 // for large json docs from ems devices, like boiler or thermostat data, using StaticJsonDocument
#define EMSESP_JSON_SIZE_MEDIUM_DYN 1024 // for large json docs, using DynamicJsonDocument
#define EMSESP_JSON_SIZE_LARGE_DYN 2048 // for very large json docs, using DynamicJsonDocument
#define EMSESP_JSON_SIZE_XLARGE_DYN 4096 // for very very large json docs, using DynamicJsonDocument
#define EMSESP_JSON_SIZE_XXLARGE_DYN 8192 // for extra very very large json docs, using DynamicJsonDocument
#define EMSESP_JSON_SIZE_HA_CONFIG 768 // for HA config payloads, using StaticJsonDocument
#define EMSESP_JSON_SIZE_SMALL 256 // for smaller json docs, using StaticJsonDocument
#define EMSESP_JSON_SIZE_MEDIUM 768 // for medium json docs from ems devices, using StaticJsonDocument
#define EMSESP_JSON_SIZE_LARGE 1024 // for large json docs from ems devices, like boiler or thermostat data, using StaticJsonDocument
#define EMSESP_JSON_SIZE_MEDIUM_DYN 1024 // for large json docs, using DynamicJsonDocument
#define EMSESP_JSON_SIZE_LARGE_DYN 2048 // for very large json docs, using DynamicJsonDocument
#ifndef EMSESP_STANDALONE
#define EMSESP_JSON_SIZE_XLARGE_DYN 4096 // for very very large json docs, using DynamicJsonDocument
#else
#define EMSESP_JSON_SIZE_XLARGE_DYN 16384 // for very very large json docs, using DynamicJsonDocument
#endif
#define EMSESP_JSON_SIZE_XXLARGE_DYN 16384 // for extra very very large json docs, using DynamicJsonDocument
// helpers for callback functions
#define MAKE_PF_CB(__f) [&](std::shared_ptr<const Telegram> t) { __f(t); } // for Process Function callbacks to EMSDevice::process_function_p
@@ -206,6 +212,7 @@ class EMSESP {
static void fetch_device_values(const uint8_t device_id = 0);
static void fetch_device_values_type(const uint8_t device_type);
static bool valid_device(const uint8_t device_id);
static bool add_device(const uint8_t device_id, const uint8_t product_id, std::string & version, const uint8_t brand);
static void scan_devices();
@@ -243,8 +250,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, bool verbose = true);
static bool command_commands(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_;
@@ -267,10 +275,12 @@ class EMSESP {
static uint8_t publish_all_idx_;
static uint8_t unique_id_count_;
static bool trace_raw_;
static uint64_t tx_delay_;
static uint8_t bool_format_;
static uint8_t enum_format_;
static uint16_t wait_validate_;
static bool wait_km_;
static constexpr uint8_t EMS_WAIT_KM_TIMEOUT = 60; // wait one minute
};
} // namespace emsesp

View File

@@ -23,6 +23,7 @@
#include "dallassensor.h"
#include "version.h"
#include "default_settings.h"
#include <ESP8266React.h>
#include <uuid/log.h>
@@ -36,6 +37,7 @@ class EMSESP {
static DallasSensor dallassensor_;
static uuid::log::Logger logger();
static ESP8266React esp8266React;
};
} // namespace emsesp

View File

@@ -57,13 +57,11 @@ char * Helpers::ultostr(char * ptr, uint32_t value, const uint8_t base) {
*ptr = '\0';
unsigned long res = 0;
do {
res = value - base * (t = value / base);
unsigned long res = value - base * (t = value / base);
if (res < 10) {
*--ptr = '0' + res;
} else if ((res >= 10) && (res < 16)) {
} else if (res < 16) {
*--ptr = 'A' - 10 + res;
}
} while ((value = t) != 0);
@@ -128,9 +126,9 @@ char * Helpers::smallitoa(char * result, const uint16_t value) {
char * Helpers::render_boolean(char * result, bool value) {
uint8_t bool_format_ = EMSESP::bool_format();
if (bool_format_ == BOOL_FORMAT_ONOFF) {
strlcpy(result, value ? uuid::read_flash_string(F_(on)).c_str() : uuid::read_flash_string(F_(off)).c_str(), 5);
strlcpy(result, value ? read_flash_string(F_(on)).c_str() : read_flash_string(F_(off)).c_str(), 5);
} else if (bool_format_ == BOOL_FORMAT_ONOFF_CAP) {
strlcpy(result, value ? uuid::read_flash_string(F_(ON)).c_str() : uuid::read_flash_string(F_(OFF)).c_str(), 5);
strlcpy(result, value ? read_flash_string(F_(ON)).c_str() : read_flash_string(F_(OFF)).c_str(), 5);
} else if (bool_format_ == BOOL_FORMAT_TRUEFALSE) {
strlcpy(result, value ? "true" : "false", 7);
} else {
@@ -141,6 +139,7 @@ char * Helpers::render_boolean(char * result, bool value) {
}
// render for native char strings
// format is not used
char * Helpers::render_value(char * result, const char * value, uint8_t format __attribute__((unused))) {
strcpy(result, value);
return result;
@@ -253,6 +252,7 @@ char * Helpers::render_value(char * result, const int16_t value, const uint8_t f
}
// uint16: convert unsigned short (two bytes) to text string and prints it
// format: 0=no division, other divide by the value given and render with a decimal point
char * Helpers::render_value(char * result, const uint16_t value, const uint8_t format) {
if (!hasValue(value)) {
return nullptr;
@@ -262,6 +262,7 @@ char * Helpers::render_value(char * result, const uint16_t value, const uint8_t
}
// int8: convert signed byte to text string and prints it
// format: 0=no division, other divide by the value given and render with a decimal point
char * Helpers::render_value(char * result, const int8_t value, const uint8_t format) {
if (!hasValue(value)) {
return nullptr;
@@ -271,6 +272,7 @@ char * Helpers::render_value(char * result, const int8_t value, const uint8_t fo
}
// uint32: render long (4 byte) unsigned values
// format: 0=no division, other divide by the value given and render with a decimal point
char * Helpers::render_value(char * result, const uint32_t value, const uint8_t format) {
if (!hasValue(value)) {
return nullptr;
@@ -304,7 +306,7 @@ char * Helpers::render_value(char * result, const uint32_t value, const uint8_t
// creates string of hex values from an arrray of bytes
std::string Helpers::data_to_hex(const uint8_t * data, const uint8_t length) {
if (length == 0) {
return uuid::read_flash_string(F("<empty>"));
return read_flash_string(F("<empty>"));
}
std::string str(160, '\0');
@@ -350,7 +352,7 @@ uint32_t Helpers::hextoint(const char * hex) {
// quick char to long
uint16_t Helpers::atoint(const char * value) {
unsigned int x = 0;
while (*value != '\0') {
while (*value >= '0' && *value <= '9') {
x = (x * 10) + (*value - '0');
++value;
}
@@ -359,7 +361,7 @@ uint16_t Helpers::atoint(const char * value) {
// rounds a number to 2 decimal places
// example: round2(3.14159) -> 3.14
double Helpers::round2(double value, const uint8_t divider) {
float Helpers::round2(float value, const uint8_t divider) {
uint8_t div = (divider ? divider : 1); // prevent div-by-zero
if (value >= 0) {
@@ -462,12 +464,12 @@ bool Helpers::value2bool(const char * v, bool & value) {
std::string bool_str = toLower(v); // convert to lower case
if ((bool_str == uuid::read_flash_string(F_(on))) || (bool_str == "1") or (bool_str == "true")) {
if ((bool_str == read_flash_string(F_(on))) || (bool_str == "1") or (bool_str == "true")) {
value = true;
return true; // is a bool
}
if ((bool_str == uuid::read_flash_string(F_(off))) || (bool_str == "0") or (bool_str == "false")) {
if ((bool_str == read_flash_string(F_(off))) || (bool_str == "0") or (bool_str == "false")) {
value = false;
return true; // is a bool
}
@@ -482,8 +484,8 @@ bool Helpers::value2enum(const char * v, uint8_t & value, const __FlashStringHel
}
std::string str = toLower(v);
for (value = 0; strs[value]; value++) {
std::string str1 = toLower(uuid::read_flash_string(strs[value]));
if ((str1 == uuid::read_flash_string(F_(off)) && str == "false") || (str1 == uuid::read_flash_string(F_(on)) && str == "true") || (str == str1)
std::string str1 = toLower(read_flash_string(strs[value]));
if ((str1 == read_flash_string(F_(off)) && str == "false") || (str1 == read_flash_string(F_(on)) && str == "true") || (str == str1)
|| (v[0] == ('0' + value) && v[1] == '\0')) {
return true;
}

View File

@@ -50,7 +50,7 @@ class Helpers {
static uint16_t atoint(const char * value);
static bool check_abs(const int32_t i);
static uint32_t abs(const int32_t i);
static double round2(double value, const uint8_t divider);
static float round2(float value, const uint8_t divider);
static std::string toLower(std::string const & s);
static std::string toUpper(std::string const & s);

View File

@@ -46,6 +46,7 @@ MAKE_PSTR_WORD(restart)
MAKE_PSTR_WORD(format)
MAKE_PSTR_WORD(raw)
MAKE_PSTR_WORD(watch)
MAKE_PSTR_WORD(syslog_level)
MAKE_PSTR_WORD(send)
MAKE_PSTR_WORD(telegram)
MAKE_PSTR_WORD(bus_id)
@@ -82,6 +83,8 @@ MAKE_PSTR_WORD(commands)
MAKE_PSTR_WORD(info)
MAKE_PSTR_WORD(settings)
MAKE_PSTR_WORD(value)
MAKE_PSTR_WORD(error)
MAKE_PSTR_WORD(entities)
// devices
MAKE_PSTR_WORD(boiler)
@@ -99,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")
@@ -114,6 +117,8 @@ MAKE_PSTR(log_level_fmt, "Log level: %s")
MAKE_STR(productid_fmt, "%s EMS Product ID")
MAKE_PSTR_LIST(enum_syslog_level, F_(off), F("emerg"), F("alert"), F("crit"), F_(error), F("warn"), F("notice"), F_(info), F_(debug), F("trace"), F("all"))
MAKE_PSTR_LIST(enum_watch, F_(off), F_(on), F_(raw), F_(unknown))
// strings
MAKE_PSTR(EMSESP, "EMS-ESP")
MAKE_PSTR(cmd_optional, "[cmd]")
@@ -142,8 +147,9 @@ MAKE_PSTR(password_prompt, "Password: ")
MAKE_PSTR(unset, "<unset>")
// command descriptions
MAKE_PSTR(info_cmd, "list all values")
MAKE_PSTR(commands_cmd, "list all commands")
MAKE_PSTR(info_cmd, "lists all values")
MAKE_PSTR(commands_cmd, "lists all commands")
MAKE_PSTR(entities_cmd, "lists all entities")
MAKE_PSTR_WORD(number)
MAKE_PSTR_WORD(enum)
@@ -158,6 +164,8 @@ MAKE_PSTR_LIST(div2, F_(2))
MAKE_PSTR_LIST(div10, F_(10))
MAKE_PSTR_LIST(div100, F_(100))
MAKE_PSTR_LIST(div60, F_(60))
MAKE_PSTR_LIST(mul10, F("*10"))
MAKE_PSTR_LIST(mul15, F("*15"))
// Unit Of Measurement mapping - maps to DeviceValueUOM_s in emsdevice.cpp
// uom - also used with HA see https://github.com/home-assistant/core/blob/d7ac4bd65379e11461c7ce0893d3533d8d8b8cbf/homeassistant/const.py#L384
@@ -175,9 +183,11 @@ MAKE_PSTR(w, "W")
MAKE_PSTR(kb, "KB")
MAKE_PSTR(seconds, "seconds")
MAKE_PSTR(dbm, "dBm")
MAKE_PSTR(num, " ") // this is hack so HA renders numbers correctly
MAKE_PSTR(bool, " ") // this is hack so HA renders numbers correctly
MAKE_PSTR(blank, " ") // this is hack so HA renders numbers correctly
MAKE_PSTR(mv, "mV")
MAKE_PSTR(times, "times")
MAKE_PSTR(oclock, "o'clock")
MAKE_PSTR(days, "days")
// TAG mapping - maps to DeviceValueTAG_s in emsdevice.cpp
// use empty string if want to suppress showing tags
@@ -185,7 +195,7 @@ MAKE_PSTR(blank, " ") // this is hack so HA renders numbers correctly
MAKE_PSTR(tag_none, "")
MAKE_PSTR(tag_heartbeat, "")
MAKE_PSTR(tag_boiler_data, "")
MAKE_PSTR(tag_boiler_data_ww, "ww")
MAKE_PSTR(tag_device_data_ww, "ww")
MAKE_PSTR(tag_thermostat_data, "")
MAKE_PSTR(tag_hc1, "hc1")
MAKE_PSTR(tag_hc2, "hc2")
@@ -214,7 +224,7 @@ MAKE_PSTR(tag_hs16, "hs16")
// MQTT topic names
MAKE_PSTR(tag_boiler_data_mqtt, "")
MAKE_PSTR(tag_boiler_data_ww_mqtt, "ww")
MAKE_PSTR(tag_device_data_ww_mqtt, "ww")
// boiler
MAKE_PSTR_WORD(time)
@@ -236,7 +246,6 @@ MAKE_PSTR_WORD(buffer)
MAKE_PSTR(bufferedflow, "buffered flow")
MAKE_PSTR(layeredbuffer, "layered buffer")
MAKE_PSTR_WORD(maintenance)
MAKE_PSTR_WORD(error)
// boiler lists
MAKE_PSTR_LIST(enum_off_time_date, F_(off), F_(time), F_(date))
@@ -247,6 +256,12 @@ MAKE_PSTR_LIST(enum_flow, F_(off), F_(flow), F_(bufferedflow), F_(buffer), F_(la
MAKE_PSTR_LIST(enum_reset, F_(maintenance), F_(error))
MAKE_PSTR_LIST(enum_bool, F_(off), F_(on))
//heatpump
MAKE_PSTR_LIST(enum_hpactivity, F("none"), F("heating"), F("cooling"), F("warm water"), F("pool"))
// mixer
MAKE_PSTR_LIST(enum_shunt, F("stopped"), F("opening"), F("closing"), F("open"), F("close"))
// thermostat
MAKE_PSTR_WORD(light)
MAKE_PSTR_WORD(medium)
@@ -288,9 +303,9 @@ MAKE_PSTR_WORD(design)
MAKE_PSTR_WORD(tempauto)
MAKE_PSTR_WORD(minflow)
MAKE_PSTR_WORD(maxflow)
MAKE_PSTR_WORD(rc3x)
MAKE_PSTR_WORD(rc20)
MAKE_PSTR(internal_temperature, "internal temperature")
MAKE_PSTR(internal_setpoint, "internal setpoint")
MAKE_PSTR(external_temperature, "external temperature")
@@ -312,11 +327,12 @@ MAKE_PSTR_LIST(enum_ibaMainDisplay,
F_(smoke_temperature))
MAKE_PSTR_LIST(enum_ibaLanguage, F_(german), F_(dutch), F_(french), F_(italian))
MAKE_PSTR_LIST(enum_floordrystatus, F_(off), F_(start), F_(heat), F_(hold), F_(cool), F_(end))
MAKE_PSTR_LIST(enum_ibaBuildingType, F_(light), F_(medium), F_(heavy)) // RC300
MAKE_PSTR_LIST(enum_ibaBuildingType, F_(light), F_(medium), F_(heavy))
MAKE_PSTR_LIST(enum_PID, F("fast"), F_(medium), F("slow"))
MAKE_PSTR_LIST(enum_wwMode, F_(off), F_(low), F_(high), F_(auto), F_(own_prog))
MAKE_PSTR_LIST(enum_wwCircMode, F_(off), F_(on), F_(auto), F_(own_prog))
MAKE_PSTR_LIST(enum_wwMode2, F_(off), F_(on), F_(auto))
MAKE_PSTR_LIST(enum_wwCircMode2, F_(off), F_(on), F_(auto))
MAKE_PSTR_LIST(enum_wwMode3, F_(on), F_(off), F_(auto))
MAKE_PSTR_LIST(enum_heatingtype, F_(off), F_(radiator), F_(convector), F_(floor))
MAKE_PSTR_LIST(enum_summermode, F_(summer), F_(auto), F_(winter))
@@ -325,6 +341,9 @@ MAKE_PSTR_LIST(enum_mode2, F_(off), F_(manual), F_(auto)) // RC20
MAKE_PSTR_LIST(enum_mode3, F_(night), F_(day), F_(auto)) // RC35, RC30
MAKE_PSTR_LIST(enum_mode4, F_(nofrost), F_(eco), F_(heat), F_(auto)) // JUNKERS
MAKE_PSTR_LIST(enum_mode5, F_(auto), F_(off)) // CRF
MAKE_PSTR_LIST(enum_mode6, F_(off), F_(night), F_(day)) // RC10
MAKE_PSTR_LIST(enum_hamode, F_(off), F_(heat), F_(auto), F_(heat), F_(off), F_(heat), F_(auto), F_(auto), F_(auto), F_(auto))
MAKE_PSTR_LIST(enum_modetype, F_(eco), F_(comfort))
MAKE_PSTR_LIST(enum_modetype2, F_(day))
@@ -338,22 +357,27 @@ MAKE_PSTR_LIST(enum_controlmode, F_(off), F_(optimized), F_(simple), F_(mpc), F_
MAKE_PSTR_LIST(enum_controlmode2, F_(outdoor), F_(room))
MAKE_PSTR_LIST(enum_controlmode3, F_(off), F_(room), F_(outdoor), F("room+outdoor"))
MAKE_PSTR_LIST(enum_control, F_(off), F_(rc20), F_(rc3x))
MAKE_PSTR_LIST(enum_j_control, F_(off), F("fb10"), F("fb110"))
MAKE_PSTR_LIST(enum_hamode, F_(off), F_(heat), F_(auto), F_(heat), F_(off), F_(heat), F_(auto), F_(auto), F_(auto), F_(auto))
MAKE_PSTR_LIST(enum_wwProgMode, F("std_prog"), F_(own_prog))
MAKE_PSTR_LIST(enum_dayOfWeek, F("mo"), F("tu"), F("we"), F("th"), F("fr"), F("sa"), F("so"), F("all"))
MAKE_PSTR_LIST(enum_progMode, F("prog_1"), F("prog_2"))
MAKE_PSTR_LIST(enum_progMode2, F("own_1"), F("family"), F("morning"), F("evening"), F("am"), F("pm"), F("midday"), F("singles"), F("seniors"), F("new"), F("own_2"))
MAKE_PSTR_LIST(enum_progMode3, F("family"), F("morning"), F("evening"), F("am"), F("pm"), F("midday"), F("singles"), F("seniors"))
MAKE_PSTR_LIST(enum_progMode4, F("prog_a"), F("prog_b"), F("prog_c"), F("prog_d"), F("prog_e"), F("prog_f"))
// solar list
MAKE_PSTR_LIST(enum_solarmode, F_(constant), F("pwm"), F("analog"))
MAKE_PSTR_LIST(enum_collectortype, F("flat"), F("vacuum"))
// MQTT topics and full text for values and commands
// MQTT topic for homeassistant. Must include /
MAKE_PSTR(homeassistant, "homeassistant/")
// id used to store the device ID, goes into MQTT payload
// empty full name to prevent being shown in web or console
// id used to store the device ID. empty full name so only gets displayed in the MQTT payload
MAKE_PSTR_LIST(ID, F_(id))
// Boiler
// extra commands, no output
// extra commands, with no json output
MAKE_PSTR_LIST(wwtapactivated, F("wwtapactivated"), F("turn on/off DHW by going into maintenance mode"))
MAKE_PSTR_LIST(reset, F("reset"), F("reset messages"))
@@ -374,7 +398,7 @@ MAKE_PSTR_LIST(curFlowTemp, F("curflowtemp"), F("current flow temperature"))
MAKE_PSTR_LIST(retTemp, F("rettemp"), F("return temperature"))
MAKE_PSTR_LIST(switchTemp, F("switchtemp"), F("mixing switch temperature"))
MAKE_PSTR_LIST(sysPress, F("syspress"), F("system pressure"))
MAKE_PSTR_LIST(boilTemp, F("boiltemp"), F("boiler temperature"))
MAKE_PSTR_LIST(boilTemp, F("boiltemp"), F("actual boiler temperature"))
MAKE_PSTR_LIST(exhaustTemp, F("exhausttemp"), F("exhaust temperature"))
MAKE_PSTR_LIST(burnGas, F("burngas"), F("gas"))
MAKE_PSTR_LIST(flameCurr, F("flamecurr"), F("flame current"))
@@ -394,7 +418,7 @@ MAKE_PSTR_LIST(boilHystOff, F("boilhystoff"), F("hysteresis off temperature"))
MAKE_PSTR_LIST(setFlowTemp, F("setflowtemp"), F("set flow temperature"))
MAKE_PSTR_LIST(setBurnPow, F("setburnpow"), F("burner set power"))
MAKE_PSTR_LIST(curBurnPow, F("curburnpow"), F("burner current power"))
MAKE_PSTR_LIST(burnStarts, F("burnstarts"), F("# burner starts"))
MAKE_PSTR_LIST(burnStarts, F("burnstarts"), F("burner starts"))
MAKE_PSTR_LIST(burnWorkMin, F("burnworkmin"), F("total burner operating time"))
MAKE_PSTR_LIST(heatWorkMin, F("heatworkmin"), F("total heat operating time"))
MAKE_PSTR_LIST(UBAuptime, F("ubauptime"), F("total UBA operating time"))
@@ -409,24 +433,45 @@ MAKE_PSTR_LIST(upTimeControl, F("uptimecontrol"), F("operating time total heat")
MAKE_PSTR_LIST(upTimeCompHeating, F("uptimecompheating"), F("operating time compressor heating"))
MAKE_PSTR_LIST(upTimeCompCooling, F("uptimecompcooling"), F("operating time compressor cooling"))
MAKE_PSTR_LIST(upTimeCompWw, F("uptimecompww"), F("operating time compressor warm water"))
MAKE_PSTR_LIST(heatingStarts, F("heatingstarts"), F("# heating control starts"))
MAKE_PSTR_LIST(coolingStarts, F("coolingstarts"), F("# cooling control starts"))
MAKE_PSTR_LIST(upTimeCompPool, F("uptimecomppool"), F("operating time compressor pool"))
MAKE_PSTR_LIST(totalcompStarts, F("totalcompstarts"), F("total compressor control starts"))
MAKE_PSTR_LIST(heatingStarts, F("heatingstarts"), F("heating control starts"))
MAKE_PSTR_LIST(coolingStarts, F("coolingstarts"), F("cooling control starts"))
MAKE_PSTR_LIST(wwStarts2, F("wwstarts2"), F("warm water control starts"))
MAKE_PSTR_LIST(poolStarts, F("poolstarts"), F("pool control starts"))
MAKE_PSTR_LIST(nrgConsTotal, F("nrgconstotal"), F("total energy consumption"))
MAKE_PSTR_LIST(nrgConsCompTotal, F("nrgconscomptotal"), F("energy consumption compressor total"))
MAKE_PSTR_LIST(nrgConsCompHeating, F("nrgconscompheating"), F("energy consumption compressor heating"))
MAKE_PSTR_LIST(nrgConsCompWw, F("nrgconscompww"), F("energy consumption compressor warm water"))
MAKE_PSTR_LIST(nrgConsCompCooling, F("nrgconscompcooling"), F("energy consumption compressor cooling"))
MAKE_PSTR_LIST(nrgConsCompPool, F("nrgconscomppool"), F("energy consumption compressor pool"))
MAKE_PSTR_LIST(nrgSuppTotal, F("nrgsupptotal"), F("total energy supplied"))
MAKE_PSTR_LIST(nrgSuppHeating, F("nrgsuppheating"), F("total energy supplied heating"))
MAKE_PSTR_LIST(nrgSuppWw, F("nrgsuppww"), F("total energy warm supplied warm water"))
MAKE_PSTR_LIST(nrgSuppCooling, F("nrgsuppcooling"), F("total energy supplied cooling"))
MAKE_PSTR_LIST(nrgSuppPool, F("nrgsupppool"), F("total energy supplied pool"))
MAKE_PSTR_LIST(auxElecHeatNrgConsTotal, F("auxelecheatnrgconstotal"), F("auxiliary electrical heater energy consumption total"))
MAKE_PSTR_LIST(auxElecHeatNrgConsHeating, F("auxelecheatnrgconsheating"), F("auxiliary electrical heater energy consumption heating"))
MAKE_PSTR_LIST(auxElecHeatNrgConsWW, F("auxelecheatnrgconsww"), F("auxiliary electrical heater energy consumption"))
MAKE_PSTR_LIST(auxElecHeatNrgConsWW, F("auxelecheatnrgconsww"), F("auxiliary electrical heater energy consumption warm water"))
MAKE_PSTR_LIST(auxElecHeatNrgConsPool, F("auxelecheatnrgconspool"), F("auxiliary electrical heater energy consumption pool"))
MAKE_PSTR_LIST(hpPower, F("hppower"), F("heatpump power"))
MAKE_PSTR_LIST(hpTc0, F("hptc0"), F("water temperature condenser inlet (TC0)"))
MAKE_PSTR_LIST(hpTc1, F("hptc1"), F("water temperature condenser output (TC1)"))
MAKE_PSTR_LIST(hpPower, F("hppower"), F("Compressor power output"))
MAKE_PSTR_LIST(hpCompOn, F("hpcompon"), F("HP Compressor"))
MAKE_PSTR_LIST(hpHeatingOn, F("hpheatingon"), F("HP Heating"))
MAKE_PSTR_LIST(hpCoolingOn, F("hpcoolingon"), F("HP Cooling"))
MAKE_PSTR_LIST(hpWwOn, F("hpwwon"), F("HP Warm water"))
MAKE_PSTR_LIST(hpPoolOn, F("hppoolon"), F("HP Pool"))
MAKE_PSTR_LIST(hpBrinePumpSpd, F("hpbrinepumpspd"), F("Brine Pump Speed"))
MAKE_PSTR_LIST(hpCompSpd, F("hpcompspd"), F("Compressor Speed"))
MAKE_PSTR_LIST(hpCircSpd, F("hpcircspd"), F("Circulation pump Speed"))
MAKE_PSTR_LIST(hpBrineIn, F("hpbrinein"), F("Brine in/Evaporator"))
MAKE_PSTR_LIST(hpBrineOut, F("hpbrineout"), F("Brine out/Condenser"))
MAKE_PSTR_LIST(hpSuctionGas, F("hpsuctiongas"), F("Suction gas"))
MAKE_PSTR_LIST(hpHotGas, F("hphotgas"), F("Hot gas/Compressed"))
MAKE_PSTR_LIST(hpSwitchValve, F("hpswitchvalve"), F("Switch Valve"))
MAKE_PSTR_LIST(hpActivity, F("hpactivity"), F("Compressor Activity"))
MAKE_PSTR_LIST(hpTc0, F("hptc0"), F("Heat carrier return (TC0)"))
MAKE_PSTR_LIST(hpTc1, F("hptc1"), F("Heat carrier forward (TC1)"))
MAKE_PSTR_LIST(hpTc3, F("hptc3"), F("condenser temperature (TC3)"))
MAKE_PSTR_LIST(hpTr3, F("hptr3"), F("refrigerant temperature liquid side (condenser output) (TR3)"))
MAKE_PSTR_LIST(hpTr4, F("hptr4"), F("evaporator inlet temperature (TR4)"))
@@ -436,9 +481,16 @@ MAKE_PSTR_LIST(hpTr7, F("hptr7"), F("refrigerant temperature gas side (condenser
MAKE_PSTR_LIST(hpTl2, F("hptl2"), F("air inlet temperature (TL2)"))
MAKE_PSTR_LIST(hpPl1, F("hppl1"), F("low pressure side temperature (PL1)"))
MAKE_PSTR_LIST(hpPh1, F("hpph1"), F("high pressure side temperature (PH1)"))
MAKE_PSTR_LIST(poolSetTemp, F("poolsettemp"), F("pool set temperature"))
MAKE_PSTR_LIST(poolTemp, F("pooltemp"), F("pool temperature"))
MAKE_PSTR_LIST(poolShuntStatus, F("poolshuntstatus"), F("pool shunt status opening/closing"))
MAKE_PSTR_LIST(poolShunt, F("poolshunt"), F("pool shunt open/close (0% = pool / 100% = heat)"))
// the following are warm water for the boiler and automatically tagged with 'ww'
MAKE_PSTR_LIST(wwSelTemp, F("wwseltemp"), F("selected temperature"))
MAKE_PSTR_LIST(wwSelTempLow, F("wwseltemplow"), F("selected lower temperature"))
MAKE_PSTR_LIST(wwSelTempOff, F("wwseltempoff"), F("selected temperature for off"))
MAKE_PSTR_LIST(wwSelTempSingle, F("wwseltempsingle"), F("single charge temperature"))
MAKE_PSTR_LIST(wwSetTemp, F("wwsettemp"), F("set temperature"))
MAKE_PSTR_LIST(wwType, F("wwtype"), F("type"))
MAKE_PSTR_LIST(wwComfort, F("wwcomfort"), F("comfort"))
@@ -456,52 +508,64 @@ MAKE_PSTR_LIST(wwStorageTemp1, F("wwstoragetemp1"), F("storage intern temperatur
MAKE_PSTR_LIST(wwStorageTemp2, F("wwstoragetemp2"), F("storage extern temperature"))
MAKE_PSTR_LIST(wwActivated, F("wwactivated"), F("activated"))
MAKE_PSTR_LIST(wwOneTime, F("wwonetime"), F("one time charging"))
MAKE_PSTR_LIST(wwDisinfecting, F("wwdisinfecting"), F("disinfecting"))
MAKE_PSTR_LIST(wwDisinfect, F("wwdisinfect"), F("disinfection"))
MAKE_PSTR_LIST(wwCharging, F("wwcharging"), F("charging"))
MAKE_PSTR_LIST(wwRecharging, F("wwrecharging"), F("recharging"))
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(wwStarts2, F("wwstarts2"), F("# control starts"))
MAKE_PSTR_LIST(wwStarts, F("wwstarts"), F("starts"))
MAKE_PSTR_LIST(wwWorkM, F("wwworkm"), F("active time"))
MAKE_PSTR_LIST(wwHystOn, F("wwhyston"), F("hysteresis on temperature"))
MAKE_PSTR_LIST(wwHystOff, F("wwhystoff"), F("hysteresis off temperature"))
MAKE_PSTR_LIST(wwProgMode, F("wwprogmode"), F("program mode"))
MAKE_PSTR_LIST(wwCircProg, F("wwcircprog"), F("circulation program mode"))
// MAKE_PSTR_LIST(wwDisinfect, F("wwdisinfect"), F("disinfection")) // same as in boiler
MAKE_PSTR_LIST(wwDisinfectDay, F("wwdisinfectday"), F("disinfection day"))
MAKE_PSTR_LIST(wwDisinfectHour, F("wwdisinfecthour"), F("disinfection hour"))
MAKE_PSTR_LIST(wwDisinfectTime, F("wwdisinfecttime"), F("disinfection time"))
MAKE_PSTR_LIST(wwMaxTemp, F("wwmaxtemp"), F("maximum temperature"))
MAKE_PSTR_LIST(wwOneTimeKey, F("wwonetimekey"), F("one time key function"))
// thermostat
// extra commands
MAKE_PSTR_LIST(switchtime, F("switchtime"), F("single program switchtime"))
// extra commands, with no long name so they don't show up in WebUI
// commands, with no long name so they only appear in the MQTT payloads
MAKE_PSTR_LIST(temp, F("temp"))
MAKE_PSTR_LIST(hatemp, F("hatemp"))
MAKE_PSTR_LIST(hamode, F("hamode"))
// mqtt values / commands
MAKE_PSTR_LIST(switchtime, F("switchtime"), F("single program switchtime"))
MAKE_PSTR_LIST(dateTime, F("datetime"), F("date/time"))
MAKE_PSTR_LIST(errorCode, F("errorcode"), F("error code"))
MAKE_PSTR_LIST(ibaMainDisplay, F("display"), F("display"))
MAKE_PSTR_LIST(ibaLanguage, F("language"), F("language"))
MAKE_PSTR_LIST(ibaClockOffset, F("clockoffset"), F("clock offset"))
MAKE_PSTR_LIST(ibaBuildingType, F("building"), F("building"))
MAKE_PSTR_LIST(heatingPID, F("heatingpid"), F("heating PID"))
MAKE_PSTR_LIST(ibaCalIntTemperature, F("intoffset"), F("offset internal temperature"))
MAKE_PSTR_LIST(ibaMinExtTemperature, F("minexttemp"), F("minimal external temperature"))
MAKE_PSTR_LIST(backlight, F("backlight"), F("key backlight"))
MAKE_PSTR_LIST(damping, F("damping"), F("damping outdoor temperature"))
MAKE_PSTR_LIST(tempsensor1, F("inttemp1"), F("temperature sensor 1"))
MAKE_PSTR_LIST(tempsensor2, F("inttemp2"), F("temperature sensor 2"))
MAKE_PSTR_LIST(dampedoutdoortemp, F("dampedoutdoortemp"), F("damped outdoor temperature"))
MAKE_PSTR_LIST(floordrystatus, F("floordry"), F("floor drying"))
MAKE_PSTR_LIST(dampedoutdoortemp2, F("dampedoutdoortemp"), F("damped outdoor temperature"))
MAKE_PSTR_LIST(floordrytemp, F("floordrytemp"), F("floor drying temperature"))
MAKE_PSTR_LIST(wwMode, F("wwmode"), F("mode"))
MAKE_PSTR_LIST(wwSetTempLow, F("wwsettemplow"), F("set low temperature"))
MAKE_PSTR_LIST(wwCharge, F("wwcharge"), F("charge"))
MAKE_PSTR_LIST(wwChargeDuration, F("wwchargeduration"), F("charge duration"))
MAKE_PSTR_LIST(wwExtra1, F("wwextra1"), F("circuit 1 extra"))
MAKE_PSTR_LIST(wwExtra2, F("wwextra2"), F("circuit 2 extra"))
MAKE_PSTR_LIST(setpoint_roomTemp, F("seltemp"), F("selected room temperature"))
MAKE_PSTR_LIST(curr_roomTemp, F("currtemp"), F("current room temperature"))
MAKE_PSTR_LIST(mode, F("mode"), F("mode"))
MAKE_PSTR_LIST(modetype, F("modetype"), F("mode type"))
MAKE_PSTR_LIST(fastheatup, F("fastheatup"), F("fast heatup"))
MAKE_PSTR_LIST(daytemp, F("daytemp"), F("day temperature"))
MAKE_PSTR_LIST(heattemp, F("heattemp"), F("heat temperature"))
MAKE_PSTR_LIST(nighttemp, F("nighttemp"), F("night temperature"))
@@ -515,15 +579,19 @@ MAKE_PSTR_LIST(offsettemp, F("offsettemp"), F("offset temperature"))
MAKE_PSTR_LIST(minflowtemp, F("minflowtemp"), F("min flow temperature"))
MAKE_PSTR_LIST(maxflowtemp, F("maxflowtemp"), F("max flow temperature"))
MAKE_PSTR_LIST(roominfluence, F("roominfluence"), F("room influence"))
MAKE_PSTR_LIST(curroominfl, F("curroominfl"), F("current room influence"))
MAKE_PSTR_LIST(nofrosttemp, F("nofrosttemp"), F("nofrost temperature"))
MAKE_PSTR_LIST(targetflowtemp, F("targetflowtemp"), F("target flow temperature"))
MAKE_PSTR_LIST(heatingtype, F("heatingtype"), F("heating type"))
MAKE_PSTR_LIST(summersetmode, F("summersetmode"), F("set summer mode"))
MAKE_PSTR_LIST(controlmode, F("controlmode"), F("control mode"))
MAKE_PSTR_LIST(control, F("control"), F("control device"))
MAKE_PSTR_LIST(holidays, F("holidays"), F("holiday dates"))
MAKE_PSTR_LIST(vacations, F("vacations"), F("vacation dates"))
MAKE_PSTR_LIST(program, F("program"), F("program"))
MAKE_PSTR_LIST(pause, F("pause"), F("pause time"))
MAKE_PSTR_LIST(party, F("party"), F("party time"))
MAKE_PSTR_LIST(wwprio, F("wwprio"), F("warm water priority"))
MAKE_PSTR_LIST(holidaytemp, F("holidaytemp"), F("holiday temperature"))
MAKE_PSTR_LIST(summermode, F("summermode"), F("summer mode"))
@@ -532,6 +600,8 @@ MAKE_PSTR_LIST(flowtempoffset, F("flowtempoffset"), F("flow temperature offset")
MAKE_PSTR_LIST(reducemode, F("reducemode"), F("reduce mode"))
MAKE_PSTR_LIST(noreducetemp, F("noreducetemp"), F("no reduce below temperature"))
MAKE_PSTR_LIST(remotetemp, F("remotetemp"), F("room temperature from remote"))
MAKE_PSTR_LIST(reducehours, F("reducehours"), F("duration for nighttemp"))
MAKE_PSTR_LIST(reduceminutes, F("reduceminutes"), F("remaining time for nightmode"))
// heatpump
MAKE_PSTR_LIST(airHumidity, F("airhumidity"), F("relative air humidity"))
@@ -543,6 +613,7 @@ MAKE_PSTR_LIST(flowTempHc, F("flowtemphc"), F("flow temperature in assigned hc (
MAKE_PSTR_LIST(pumpStatus, F("pumpstatus"), F("pump status in assigned hc (PC1)"))
MAKE_PSTR_LIST(mixerStatus, F("valvestatus"), F("mixing valve actuator in assigned hc (VC1)"))
MAKE_PSTR_LIST(flowTempVf, F("flowtempvf"), F("flow temperature in header (T0/Vf)"))
MAKE_PSTR_LIST(mixerSetTime, F("valvesettime"), F("time to set valve"))
MAKE_PSTR_LIST(wwPumpStatus, F("pumpstatus"), F("pump status in assigned wwc (PC1)"))
MAKE_PSTR_LIST(wwTempStatus, F("wwtempstatus"), F("temperature switch in assigned wwc (MC1)"))
MAKE_PSTR_LIST(wwTemp, F("wwtemp"), F("current temperature"))
@@ -599,3 +670,6 @@ MAKE_PSTR_LIST(collector1Type, F("collector1type"), F("collector 1 type"))
// switch
MAKE_PSTR_LIST(activated, F("activated"), F("activated"))
MAKE_PSTR_LIST(status, F("status"), F("status"))
// RF sensor, id 0x40, telegram 0x435
MAKE_PSTR_LIST(RFTemp, F("rftemp"), F("RF room temperature sensor"));

View File

@@ -38,7 +38,7 @@ bool Mqtt::mqtt_enabled_;
uint8_t Mqtt::ha_climate_format_;
bool Mqtt::ha_enabled_;
uint8_t Mqtt::nested_format_;
uint8_t Mqtt::subscribe_format_;
bool Mqtt::send_response_;
std::deque<Mqtt::QueuedMqttMessage> Mqtt::mqtt_messages_;
std::vector<Mqtt::MQTTSubFunction> Mqtt::mqtt_subfunctions_;
@@ -54,105 +54,48 @@ 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, if so don't add it again
// 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
}
}
}
// register in our libary with the callback function.
// We store the original topic without base
// We store the original topic string without base
mqtt_subfunctions_.emplace_back(device_type, std::move(topic), std::move(cb));
if (!enabled()) {
return;
}
LOG_DEBUG(F("Subscribing MQTT topic %s for device type %s"), topic.c_str(), EMSdevice::device_type_2_device_name(device_type).c_str());
// add to MQTT queue as a subscribe operation
queue_subscribe_message(topic);
}
// subscribe to the command topic if it doesn't exist yet
void Mqtt::register_command(const uint8_t device_type, const __FlashStringHelper * cmd, cmdfunction_p cb, uint8_t flags) {
std::string cmd_topic = EMSdevice::device_type_2_device_name(device_type); // thermostat, boiler, etc...
// see if we have already a handler for the device type (boiler, thermostat). If not add it
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(), cmd_topic.c_str()) == 0)) {
exists = true;
}
}
}
if (!exists) {
Mqtt::subscribe(device_type, cmd_topic, nullptr); // use an empty function handler to signal this is a command function only (e.g. ems-esp/boiler)
LOG_DEBUG(F("Registering MQTT cmd %s with topic %s"), uuid::read_flash_string(cmd).c_str(), EMSdevice::device_type_2_device_name(device_type).c_str());
}
if (!enabled()) {
return;
}
// register 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 topic(MQTT_TOPIC_MAX_SIZE, '\0');
topic = cmd_topic + "/hc1/" + uuid::read_flash_string(cmd);
queue_subscribe_message(topic);
topic = cmd_topic + "/hc2/" + uuid::read_flash_string(cmd);
queue_subscribe_message(topic);
topic = cmd_topic + "/hc3/" + uuid::read_flash_string(cmd);
queue_subscribe_message(topic);
topic = cmd_topic + "/hc4/" + uuid::read_flash_string(cmd);
queue_subscribe_message(topic);
} else if (subscribe_format_ != Subscribe_Format::GENERAL && ((flags & CommandFlag::MQTT_SUB_FLAG_NOSUB) == CommandFlag::MQTT_SUB_FLAG_NOSUB)) {
std::string topic(MQTT_TOPIC_MAX_SIZE, '\0');
topic = cmd_topic + "/" + uuid::read_flash_string(cmd);
queue_subscribe_message(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_) {
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);
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_);
}
}
}
@@ -215,9 +158,9 @@ void Mqtt::loop() {
// print MQTT log and other stuff to console
void Mqtt::show_mqtt(uuid::console::Shell & shell) {
shell.printfln(F("MQTT is %s"), connected() ? uuid::read_flash_string(F_(connected)).c_str() : uuid::read_flash_string(F_(disconnected)).c_str());
shell.printfln(F("MQTT is %s"), connected() ? read_flash_string(F_(connected)).c_str() : read_flash_string(F_(disconnected)).c_str());
shell.printfln(F("MQTT publish fails count: %lu"), mqtt_publish_fails_);
shell.printfln(F("MQTT publish errors: %lu"), mqtt_publish_fails_);
shell.println();
// show subscriptions
@@ -225,31 +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());
}
for (const auto & cf : Command::commands()) {
if (subscribe_format_ == 2 && cf.has_flags(CommandFlag::MQTT_SUB_FLAG_HC)) {
shell.printfln(F(" %s/%s/hc1/%s"),
mqtt_base_.c_str(),
EMSdevice::device_type_2_device_name(cf.device_type_).c_str(),
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_ == 1 && !cf.has_flags(CommandFlag::MQTT_SUB_FLAG_NOSUB)) {
shell.printfln(F(" %s/%s/%s"),
mqtt_base_.c_str(),
EMSdevice::device_type_2_device_name(cf.device_type_).c_str(),
uuid::read_flash_string(cf.cmd_).c_str());
}
}
shell.println();
// show queues
@@ -265,9 +183,9 @@ void Mqtt::show_mqtt(uuid::console::Shell & shell) {
auto content = message.content_;
char topic[MQTT_TOPIC_MAX_SIZE];
if ((strncmp(content->topic.c_str(), "homeassistant/", 13) != 0)) {
snprintf_P(topic, sizeof(topic), PSTR("%s/%s"), Mqtt::base().c_str(), content->topic.c_str());
snprintf(topic, sizeof(topic), "%s/%s", Mqtt::base().c_str(), content->topic.c_str());
} else {
snprintf_P(topic, sizeof(topic), PSTR("%s"), content->topic.c_str());
snprintf(topic, sizeof(topic), "%s", content->topic.c_str());
}
if (content->operation == Operation::PUBLISH) {
@@ -294,129 +212,86 @@ 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);
char message[len + 2] = {'\0'};
if (payload != nullptr) {
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);
StaticJsonDocument<EMSESP_JSON_SIZE_SMALL> input_doc;
StaticJsonDocument<EMSESP_JSON_SIZE_LARGE_DYN> output_doc;
JsonObject input, output;
// convert payload into a json doc
// if the payload doesn't not contain the key 'value' or 'data', treat the whole payload as the 'value'
if (len != 0) {
DeserializationError error = deserializeJson(input_doc, message);
if ((!input_doc.containsKey("value") && !input_doc.containsKey("data")) || error) {
input_doc.clear();
input_doc["value"] = (const char *)message; // always a string
}
}
// parse and call the command
input = input_doc.as<JsonObject>();
output = output_doc.to<JsonObject>();
uint8_t return_code = Command::process(topic, true, input, output); // mqtt is always authenticated
if (return_code != CommandRet::OK) {
char error[100];
if (output.size()) {
snprintf(error, sizeof(error), "Call failed with error: %s (%s)", (const char *)output["message"], Command::return_code_string(return_code).c_str());
} else {
snprintf(error, sizeof(error), "Call failed with error code (%s)", Command::return_code_string(return_code).c_str());
}
LOG_ERROR(error);
if (send_response_) {
Mqtt::publish(F_(response), error);
}
} else {
// all good, send back json output from call
if (send_response_) {
Mqtt::publish(F_(response), output);
}
}
}
// print all the topics related to a specific device type
@@ -431,7 +306,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();
@@ -492,7 +367,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;
@@ -553,21 +428,16 @@ void Mqtt::start() {
// create will_topic with the base prefixed. It has to be static because asyncmqttclient destroys the reference
static char will_topic[MQTT_TOPIC_MAX_SIZE];
snprintf_P(will_topic, MQTT_TOPIC_MAX_SIZE, PSTR("%s/status"), mqtt_base_.c_str());
snprintf(will_topic, MQTT_TOPIC_MAX_SIZE, "%s/status", mqtt_base_.c_str());
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
});
// create space for command buffer, to avoid heap memory fragmentation
mqtt_subfunctions_.reserve(5);
}
void Mqtt::set_publish_time_boiler(uint16_t publish_time) {
@@ -632,7 +502,7 @@ void Mqtt::on_connect() {
load_settings(); // reload MQTT settings - in case they have changes
// send info topic appended with the version information as JSON
StaticJsonDocument<EMSESP_JSON_SIZE_SMALL> doc;
StaticJsonDocument<EMSESP_JSON_SIZE_MEDIUM> doc;
// first time to connect
if (connectcount_ == 1) {
doc["event"] = FJSON("start");
@@ -642,15 +512,28 @@ void Mqtt::on_connect() {
doc["version"] = EMSESP_APP_VERSION;
#ifndef EMSESP_STANDALONE
if (EMSESP::system_.ethernet_connected()) {
doc["ip"] = ETH.localIP().toString();
if (ETH.localIPv6().toString() != "0000:0000:0000:0000:0000:0000:0000:0000") {
doc["ipv6"] = ETH.localIPv6().toString();
}
} else {
doc["ip"] = WiFi.localIP().toString();
if (WiFi.status() == WL_CONNECTED) {
doc["connection"] = F("WiFi");
doc["hostname"] = WiFi.getHostname();
doc["SSID"] = WiFi.SSID();
doc["BSSID"] = WiFi.BSSIDstr();
doc["RSSI"] = WiFi.RSSI();
doc["MAC"] = WiFi.macAddress();
doc["IPv4 address"] = uuid::printable_to_string(WiFi.localIP()) + "/" + uuid::printable_to_string(WiFi.subnetMask());
doc["IPv4 gateway"] = uuid::printable_to_string(WiFi.gatewayIP());
doc["IPv4 nameserver"] = uuid::printable_to_string(WiFi.dnsIP());
if (WiFi.localIPv6().toString() != "0000:0000:0000:0000:0000:0000:0000:0000") {
doc["ipv6"] = WiFi.localIPv6().toString();
doc["IPv6 address"] = uuid::printable_to_string(WiFi.localIPv6());
}
} else if (EMSESP::system_.ethernet_connected()) {
doc["connection"] = F("Ethernet");
doc["hostname"] = ETH.getHostname();
doc["MAC"] = ETH.macAddress();
doc["IPv4 address"] = uuid::printable_to_string(ETH.localIP()) + "/" + uuid::printable_to_string(ETH.subnetMask());
doc["IPv4 gateway"] = uuid::printable_to_string(ETH.gatewayIP());
doc["IPv4 nameserver"] = uuid::printable_to_string(ETH.dnsIP());
if (ETH.localIPv6().toString() != "0000:0000:0000:0000:0000:0000:0000:0000") {
doc["IPv6 address"] = uuid::printable_to_string(ETH.localIPv6());
}
}
#endif
@@ -662,11 +545,12 @@ void Mqtt::on_connect() {
}
// send initial MQTT messages for some of our services
EMSESP::shower_.send_mqtt_stat(false); // Send shower_activated as false
EMSESP::system_.send_heartbeat(); // send heatbeat
EMSESP::shower_.set_shower_state(false, true); // 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
@@ -696,7 +580,7 @@ void Mqtt::ha_status() {
doc["stat_t"] = FJSON("~/heartbeat");
doc["name"] = FJSON("EMS-ESP status");
doc["ic"] = F_(icondevice);
doc["val_tpl"] = FJSON("{{value_json['status']}}");
doc["val_tpl"] = FJSON("{{value_json['bus_status']}}");
JsonObject dev = doc.createNestedObject("dev");
dev["name"] = F_(EMSESP); // "EMS-ESP"
@@ -707,54 +591,79 @@ void Mqtt::ha_status() {
ids.add("ems-esp");
char topic[MQTT_TOPIC_MAX_SIZE];
snprintf_P(topic, sizeof(topic), PSTR("sensor/%s/system/config"), mqtt_base_.c_str());
snprintf(topic, sizeof(topic), "sensor/%s/system/config", mqtt_base_.c_str());
Mqtt::publish_ha(topic, doc.as<JsonObject>()); // publish the config payload with retain flag
// create the sensors - must match the MQTT payload keys
if (!EMSESP::system_.ethernet_connected()) {
publish_mqtt_ha_sensor(DeviceValueType::INT, DeviceValueTAG::TAG_HEARTBEAT, F("WiFi RSSI"), EMSdevice::DeviceType::SYSTEM, F("rssi"), DeviceValueUOM::DBM);
publish_mqtt_ha_sensor(DeviceValueType::INT,
DeviceValueTAG::TAG_HEARTBEAT,
F("WiFi strength"),
EMSdevice::DeviceType::SYSTEM,
F("wifistrength"),
DeviceValueUOM::PERCENT);
publish_ha_sensor_config(DeviceValueType::INT, DeviceValueTAG::TAG_HEARTBEAT, F("WiFi RSSI"), EMSdevice::DeviceType::SYSTEM, F("rssi"), DeviceValueUOM::DBM);
publish_ha_sensor_config(DeviceValueType::INT,
DeviceValueTAG::TAG_HEARTBEAT,
F("WiFi strength"),
EMSdevice::DeviceType::SYSTEM,
F("wifistrength"),
DeviceValueUOM::PERCENT);
}
publish_mqtt_ha_sensor(DeviceValueType::INT, DeviceValueTAG::TAG_HEARTBEAT, F("Uptime"), EMSdevice::DeviceType::SYSTEM, F("uptime"));
publish_mqtt_ha_sensor(DeviceValueType::INT,
DeviceValueTAG::TAG_HEARTBEAT,
F("Uptime (sec)"),
EMSdevice::DeviceType::SYSTEM,
F("uptime_sec"),
DeviceValueUOM::SECONDS);
publish_mqtt_ha_sensor(DeviceValueType::INT, DeviceValueTAG::TAG_HEARTBEAT, F("Free memory"), EMSdevice::DeviceType::SYSTEM, F("freemem"), DeviceValueUOM::KB);
publish_mqtt_ha_sensor(DeviceValueType::INT, DeviceValueTAG::TAG_HEARTBEAT, F("# MQTT fails"), EMSdevice::DeviceType::SYSTEM, F("mqttfails"), DeviceValueUOM::NUM);
publish_mqtt_ha_sensor(DeviceValueType::INT,
DeviceValueTAG::TAG_HEARTBEAT,
F("# Rx received"),
EMSdevice::DeviceType::SYSTEM,
F("rxreceived"),
DeviceValueUOM::NUM);
publish_mqtt_ha_sensor(DeviceValueType::INT, DeviceValueTAG::TAG_HEARTBEAT, F("# Rx fails"), EMSdevice::DeviceType::SYSTEM, F("rxfails"), DeviceValueUOM::NUM);
publish_mqtt_ha_sensor(DeviceValueType::INT, DeviceValueTAG::TAG_HEARTBEAT, F("# Tx reads"), EMSdevice::DeviceType::SYSTEM, F("txread"), DeviceValueUOM::NUM);
publish_mqtt_ha_sensor(DeviceValueType::INT, DeviceValueTAG::TAG_HEARTBEAT, F("# Tx writes"), EMSdevice::DeviceType::SYSTEM, F("txwrite"), DeviceValueUOM::NUM);
publish_mqtt_ha_sensor(DeviceValueType::INT, DeviceValueTAG::TAG_HEARTBEAT, F("# Tx fails"), EMSdevice::DeviceType::SYSTEM, F("txfails"), DeviceValueUOM::NUM);
publish_ha_sensor_config(DeviceValueType::INT, DeviceValueTAG::TAG_HEARTBEAT, F("Uptime"), EMSdevice::DeviceType::SYSTEM, F("uptime"), DeviceValueUOM::NONE);
publish_ha_sensor_config(DeviceValueType::INT,
DeviceValueTAG::TAG_HEARTBEAT,
F("Uptime (sec)"),
EMSdevice::DeviceType::SYSTEM,
F("uptime_sec"),
DeviceValueUOM::SECONDS);
publish_ha_sensor_config(DeviceValueType::INT, DeviceValueTAG::TAG_HEARTBEAT, F("Free memory"), EMSdevice::DeviceType::SYSTEM, F("freemem"), DeviceValueUOM::KB);
publish_ha_sensor_config(DeviceValueType::INT,
DeviceValueTAG::TAG_HEARTBEAT,
F("MQTT fails"),
EMSdevice::DeviceType::SYSTEM,
F("mqttfails"),
DeviceValueUOM::TIMES);
publish_ha_sensor_config(DeviceValueType::INT,
DeviceValueTAG::TAG_HEARTBEAT,
F("Rx received"),
EMSdevice::DeviceType::SYSTEM,
F("rxreceived"),
DeviceValueUOM::TIMES);
publish_ha_sensor_config(DeviceValueType::INT, DeviceValueTAG::TAG_HEARTBEAT, F("Rx fails"), EMSdevice::DeviceType::SYSTEM, F("rxfails"), DeviceValueUOM::TIMES);
publish_ha_sensor_config(DeviceValueType::INT, DeviceValueTAG::TAG_HEARTBEAT, F("Tx reads"), EMSdevice::DeviceType::SYSTEM, F("txreads"), DeviceValueUOM::TIMES);
publish_ha_sensor_config(DeviceValueType::INT, DeviceValueTAG::TAG_HEARTBEAT, F("Tx writes"), EMSdevice::DeviceType::SYSTEM, F("txwrites"), DeviceValueUOM::TIMES);
publish_ha_sensor_config(DeviceValueType::INT, DeviceValueTAG::TAG_HEARTBEAT, F("Tx fails"), EMSdevice::DeviceType::SYSTEM, F("txfails"), DeviceValueUOM::TIMES);
}
// 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())) {
#ifdef EMSESP_DEBUG
LOG_WARNING("[DEBUG] Publish empty payload - quitting");
#endif
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);
// LOG_INFO("Added to queue: %s %s", message->topic.c_str(), message->payload.c_str()); // debugging only
#ifdef EMSESP_DEBUG
if (operation == Operation::PUBLISH) {
if (message->payload.empty()) {
LOG_INFO("[DEBUG] Adding to queue: (Publish) topic='%s' empty payload", message->topic.c_str());
} else {
LOG_INFO("[DEBUG] Adding to queue: (Publish) topic='%s' payload=%s", message->topic.c_str(), message->payload.c_str());
}
} else {
LOG_INFO("[DEBUG] Adding to queue: (Subscribe) topic='%s'", message->topic.c_str());
}
#endif
// if the queue is full, make room but removing the last one
if (mqtt_messages_.size() >= MAX_MQTT_MESSAGES) {
@@ -785,16 +694,16 @@ void Mqtt::publish(const std::string & topic, const std::string & payload) {
// MQTT Publish, using a user's retain flag - except for char * strings
void Mqtt::publish(const __FlashStringHelper * topic, const char * payload) {
queue_publish_message(uuid::read_flash_string(topic), payload, mqtt_retain_);
queue_publish_message(read_flash_string(topic), payload, mqtt_retain_);
}
// MQTT Publish, using a specific retain flag, topic is a flash string
void Mqtt::publish(const __FlashStringHelper * topic, const std::string & payload) {
queue_publish_message(uuid::read_flash_string(topic), payload, mqtt_retain_);
queue_publish_message(read_flash_string(topic), payload, mqtt_retain_);
}
void Mqtt::publish(const __FlashStringHelper * topic, const JsonObject & payload) {
publish(uuid::read_flash_string(topic), payload);
publish(read_flash_string(topic), payload);
}
// publish json doc, only if its not empty
@@ -809,7 +718,7 @@ void Mqtt::publish(const std::string & topic) {
// MQTT Publish, using a specific retain flag, topic is a flash string, forcing retain flag
void Mqtt::publish_retain(const __FlashStringHelper * topic, const std::string & payload, bool retain) {
queue_publish_message(uuid::read_flash_string(topic), payload, retain);
queue_publish_message(read_flash_string(topic), payload, retain);
}
// publish json doc, only if its not empty, using the retain flag
@@ -822,11 +731,25 @@ void Mqtt::publish_retain(const std::string & topic, const JsonObject & payload,
}
void Mqtt::publish_retain(const __FlashStringHelper * topic, const JsonObject & payload, bool retain) {
publish_retain(uuid::read_flash_string(topic), payload, retain);
publish_retain(read_flash_string(topic), payload, retain);
}
void Mqtt::publish_ha(const __FlashStringHelper * topic, const JsonObject & payload) {
publish_ha(uuid::read_flash_string(topic), payload);
publish_ha(read_flash_string(topic), payload);
}
// publish empty payload to remove the topic
void Mqtt::publish_ha(const std::string & topic) {
if (!enabled()) {
return;
}
std::string fulltopic = read_flash_string(F_(homeassistant)) + topic;
#if defined(EMSESP_DEBUG)
LOG_DEBUG(F("[DEBUG] Publishing empty HA topic=%s"), fulltopic.c_str());
#endif
publish(fulltopic); // call it immediately, don't queue it
}
// publish a Home Assistant config topic and payload, with retain flag off.
@@ -835,14 +758,11 @@ void Mqtt::publish_ha(const std::string & topic, const JsonObject & payload) {
return;
}
// empty payload will remove the previous config
// publish(topic);
std::string payload_text;
payload_text.reserve(measureJson(payload) + 1);
serializeJson(payload, payload_text); // convert json to string
std::string fulltopic = uuid::read_flash_string(F_(homeassistant)) + topic;
std::string fulltopic = read_flash_string(F_(homeassistant)) + topic;
#if defined(EMSESP_STANDALONE)
LOG_DEBUG(F("Publishing HA topic=%s, payload=%s"), fulltopic.c_str(), payload_text.c_str());
#elif defined(EMSESP_DEBUG)
@@ -864,19 +784,19 @@ 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());
if (message->topic.find(read_flash_string(F_(homeassistant))) == 0) {
strcpy(topic, message->topic.c_str()); // leave topic as it is
} else {
snprintf_P(topic, MQTT_TOPIC_MAX_SIZE, PSTR("%s/%s"), mqtt_base_.c_str(), message->topic.c_str());
snprintf(topic, MQTT_TOPIC_MAX_SIZE, "%s/%s", mqtt_base_.c_str(), message->topic.c_str());
}
// if we're subscribing...
if (message->operation == Operation::SUBSCRIBE) {
LOG_DEBUG(F("Subscribing to topic: %s"), topic);
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 %s"), topic);
LOG_ERROR(F("Error subscribing to topic '%s'"), topic);
}
mqtt_messages_.pop_front(); // remove the message from the queue
@@ -931,64 +851,85 @@ void Mqtt::process_queue() {
mqtt_messages_.pop_front(); // remove the message from the queue
}
void Mqtt::publish_ha_sensor_config(uint8_t type, // EMSdevice::DeviceValueType
uint8_t tag, // EMSdevice::DeviceValueTAG
const __FlashStringHelper * name,
const uint8_t device_type, // EMSdevice::DeviceType
const __FlashStringHelper * entity,
const uint8_t uom) { // EMSdevice::DeviceValueUOM (0=NONE)
publish_ha_sensor_config(type, tag, name, device_type, entity, uom, false, false);
}
// HA config for a sensor and binary_sensor entity
// entity must match the key/value pair in the *_data topic
// note: some string copying here into chars, it looks messy but does help with heap fragmentation issues
void Mqtt::publish_mqtt_ha_sensor(uint8_t type, // EMSdevice::DeviceValueType
uint8_t tag, // EMSdevice::DeviceValueTAG
const __FlashStringHelper * name,
const uint8_t device_type, // EMSdevice::DeviceType
const __FlashStringHelper * entity,
const uint8_t uom) { // EMSdevice::DeviceValueUOM (0=NONE)
void Mqtt::publish_ha_sensor_config(uint8_t type, // EMSdevice::DeviceValueType
uint8_t tag, // EMSdevice::DeviceValueTAG
const __FlashStringHelper * name,
const uint8_t device_type, // EMSdevice::DeviceType
const __FlashStringHelper * entity,
const uint8_t uom, // EMSdevice::DeviceValueUOM (0=NONE)
const bool remove, // true if we want to remove this topic
const bool has_cmd) {
// ignore if name (fullname) is empty
if (name == nullptr) {
return;
}
// bool have_tag = !EMSdevice::tag_to_string(tag).empty() && (device_type != EMSdevice::DeviceType::BOILER); // ignore boiler
bool have_tag = !EMSdevice::tag_to_string(tag).empty();
// nested_format is 1 if nested, otherwise 2 for single topics
bool is_nested;
if (device_type == EMSdevice::DeviceType::BOILER) {
is_nested = false; // boiler never uses nested
} else {
is_nested = (nested_format_ == 1);
}
// create the device name
char device_name[50];
strlcpy(device_name, EMSdevice::device_type_2_device_name(device_type).c_str(), sizeof(device_name));
DynamicJsonDocument doc(EMSESP_JSON_SIZE_HA_CONFIG);
doc["~"] = mqtt_base_;
// create entity by add the tag if present, seperating with a .
// create entity by add the hc/wwc tag if present, seperating with a .
char new_entity[50];
if (have_tag) {
snprintf_P(new_entity, sizeof(new_entity), PSTR("%s.%s"), EMSdevice::tag_to_string(tag).c_str(), uuid::read_flash_string(entity).c_str());
if (tag >= DeviceValueTAG::TAG_HC1) {
snprintf(new_entity, sizeof(new_entity), "%s.%s", EMSdevice::tag_to_string(tag).c_str(), read_flash_string(entity).c_str());
} else {
snprintf_P(new_entity, sizeof(new_entity), PSTR("%s"), uuid::read_flash_string(entity).c_str());
snprintf(new_entity, sizeof(new_entity), "%s", read_flash_string(entity).c_str());
}
// build unique identifier which will be used in the topic
// and replacing all . with _ as not to break HA
// build unique identifier which will be used in the topic, replacing all . with _ as not to break HA
std::string uniq(50, '\0');
snprintf_P(&uniq[0], uniq.capacity() + 1, PSTR("%s_%s"), device_name, new_entity);
snprintf(&uniq[0], uniq.capacity() + 1, "%s_%s", device_name, new_entity);
std::replace(uniq.begin(), uniq.end(), '.', '_');
// create the topic
char topic[MQTT_TOPIC_MAX_SIZE];
if (type == DeviceValueType::BOOL) {
snprintf(topic, sizeof(topic), "binary_sensor/%s/%s/config", mqtt_base_.c_str(), uniq.c_str()); // binary sensor
} else {
snprintf(topic, sizeof(topic), "sensor/%s/%s/config", mqtt_base_.c_str(), uniq.c_str()); // normal HA sensor, not a boolean one
}
// if we're asking to remove this topic, send an empty payload
// https://github.com/emsesp/EMS-ESP32/issues/196
if (remove) {
LOG_WARNING(F("Lost device value for %s. Removing HA config"), uniq.c_str());
publish_ha(topic);
return;
}
bool have_tag = !EMSdevice::tag_to_string(tag).empty();
// nested_format is 1 if nested, otherwise 2 for single topics
bool is_nested = (nested_format_ == 1);
DynamicJsonDocument doc(EMSESP_JSON_SIZE_HA_CONFIG);
doc["~"] = mqtt_base_;
doc["uniq_id"] = uniq;
// state topic
char stat_t[MQTT_TOPIC_MAX_SIZE];
snprintf_P(stat_t, sizeof(stat_t), PSTR("~/%s"), tag_to_topic(device_type, tag).c_str());
snprintf(stat_t, sizeof(stat_t), "~/%s", tag_to_topic(device_type, tag).c_str());
doc["stat_t"] = stat_t;
// name = <device> <tag> <name>
char new_name[80];
if (have_tag) {
snprintf_P(new_name, sizeof(new_name), PSTR("%s %s %s"), device_name, EMSdevice::tag_to_string(tag).c_str(), uuid::read_flash_string(name).c_str());
snprintf(new_name, sizeof(new_name), "%s %s %s", device_name, EMSdevice::tag_to_string(tag).c_str(), read_flash_string(name).c_str());
} else {
snprintf_P(new_name, sizeof(new_name), PSTR("%s %s"), device_name, uuid::read_flash_string(name).c_str());
snprintf(new_name, sizeof(new_name), "%s %s", device_name, read_flash_string(name).c_str());
}
new_name[0] = toupper(new_name[0]); // capitalize first letter
doc["name"] = new_name;
@@ -997,26 +938,22 @@ void Mqtt::publish_mqtt_ha_sensor(uint8_t type, // EMSdevice
// if its nested mqtt format then use the appended entity name, otherwise take the original
char val_tpl[50];
if (is_nested) {
snprintf_P(val_tpl, sizeof(val_tpl), PSTR("{{value_json.%s}}"), new_entity);
snprintf(val_tpl, sizeof(val_tpl), "{{value_json.%s}}", new_entity);
} else {
snprintf_P(val_tpl, sizeof(val_tpl), PSTR("{{value_json.%s}}"), uuid::read_flash_string(entity).c_str());
snprintf(val_tpl, sizeof(val_tpl), "{{value_json.%s}}", read_flash_string(entity).c_str());
}
doc["val_tpl"] = val_tpl;
char topic[MQTT_TOPIC_MAX_SIZE];
// look at the device value type
if (type == DeviceValueType::BOOL) {
// binary sensor
snprintf_P(topic, sizeof(topic), PSTR("binary_sensor/%s/%s/config"), mqtt_base_.c_str(), uniq.c_str()); // topic
// how to render boolean. HA only accepts String values
char result[10];
doc[F("payload_on")] = Helpers::render_boolean(result, true);
doc[F("payload_off")] = Helpers::render_boolean(result, false);
} else {
// normal HA sensor, not a boolean one
snprintf_P(topic, sizeof(topic), PSTR("sensor/%s/%s/config"), mqtt_base_.c_str(), uniq.c_str()); // topic
// set default state and device class for HA
auto set_state_class = State_class::NONE;
auto set_device_class = Device_class::NONE;
// unit of measure and map the HA icon
if (uom != DeviceValueUOM::NONE) {
@@ -1025,10 +962,12 @@ void Mqtt::publish_mqtt_ha_sensor(uint8_t type, // EMSdevice
switch (uom) {
case DeviceValueUOM::DEGREES:
doc["ic"] = F_(icondegrees);
doc["ic"] = F_(icondegrees);
set_device_class = Device_class::TEMPERATURE;
break;
case DeviceValueUOM::PERCENT:
doc["ic"] = F_(iconpercent);
doc["ic"] = F_(iconpercent);
set_device_class = Device_class::POWER_FACTOR;
break;
case DeviceValueUOM::SECONDS:
case DeviceValueUOM::MINUTES:
@@ -1043,28 +982,75 @@ void Mqtt::publish_mqtt_ha_sensor(uint8_t type, // EMSdevice
break;
case DeviceValueUOM::WH:
case DeviceValueUOM::KWH:
doc["ic"] = F_(iconkwh);
doc["ic"] = F_(iconkwh);
set_state_class = State_class::TOTAL_INCREASING;
set_device_class = Device_class::ENERGY;
break;
case DeviceValueUOM::UA:
doc["ic"] = F_(iconua);
break;
case DeviceValueUOM::BAR:
doc["ic"] = F_(iconbar);
doc["ic"] = F_(iconbar);
set_device_class = Device_class::PRESSURE;
break;
case DeviceValueUOM::W:
case DeviceValueUOM::KW:
doc["ic"] = F_(iconkw);
doc["ic"] = F_(iconkw);
set_state_class = State_class::MEASUREMENT;
set_device_class = Device_class::POWER;
break;
case DeviceValueUOM::DBM:
doc["ic"] = F_(icondbm);
break;
case DeviceValueUOM::NUM:
doc["ic"] = F_(iconnum);
doc["ic"] = F_(icondbm);
set_device_class = Device_class::SIGNAL_STRENGTH;
break;
case DeviceValueUOM::NONE:
if (type == DeviceValueType::INT || type == DeviceValueType::UINT || type == DeviceValueType::SHORT || type == DeviceValueType::USHORT
|| type == DeviceValueType::ULONG) {
doc["ic"] = F_(iconnum);
}
break;
case DeviceValueUOM::TIMES:
set_state_class = State_class::TOTAL_INCREASING;
doc["ic"] = F_(iconnum);
break;
default:
break;
}
// see if we need to set the state_class and device_class
// ignore any commands
if (!has_cmd) {
// state class
if (set_state_class == State_class::MEASUREMENT) {
doc["state_class"] = F("measurement");
} else if (set_state_class == State_class::TOTAL_INCREASING) {
doc["state_class"] = F("total_increasing");
}
// device class
switch (set_device_class) {
case Device_class::ENERGY:
doc["device_class"] = F("energy");
break;
case Device_class::POWER:
doc["device_class"] = F("power");
break;
case Device_class::POWER_FACTOR:
doc["device_class"] = F("power_factor");
break;
case Device_class::PRESSURE:
doc["device_class"] = F("pressure");
break;
case Device_class::SIGNAL_STRENGTH:
doc["device_class"] = F("signal_strength");
break;
case Device_class::TEMPERATURE:
doc["device_class"] = F("temperature");
break;
default:
break;
}
}
}
JsonObject dev = doc.createNestedObject("dev");
@@ -1075,7 +1061,7 @@ void Mqtt::publish_mqtt_ha_sensor(uint8_t type, // EMSdevice
ids.add("ems-esp");
} else {
char ha_device[40];
snprintf_P(ha_device, sizeof(ha_device), PSTR("ems-esp-%s"), device_name);
snprintf(ha_device, sizeof(ha_device), "ems-esp-%s", device_name);
ids.add(ha_device);
}
@@ -1099,5 +1085,4 @@ const std::string Mqtt::tag_to_topic(uint8_t device_type, uint8_t tag) {
}
}
} // namespace emsesp

View File

@@ -42,7 +42,7 @@ using uuid::console::Shell;
#define MQTT_HA_PUBLISH_DELAY 50
// size of queue
#define MAX_MQTT_MESSAGES 200
#define MAX_MQTT_MESSAGES 300
namespace emsesp {
@@ -88,20 +88,15 @@ 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 };
static constexpr uint8_t MQTT_TOPIC_MAX_SIZE = 128; // note this should really match the user setting in mqttSettings.maxTopicLength
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);
@@ -115,14 +110,23 @@ class Mqtt {
static void publish_retain(const __FlashStringHelper * topic, const JsonObject & payload, bool retain);
static void publish_ha(const std::string & topic, const JsonObject & payload);
static void publish_ha(const __FlashStringHelper * topic, const JsonObject & payload);
static void publish_ha(const std::string & topic);
static void publish_mqtt_ha_sensor(uint8_t type,
uint8_t tag,
const __FlashStringHelper * name,
const uint8_t device_type,
const __FlashStringHelper * entity,
const uint8_t uom = 0);
static void register_command(const uint8_t device_type, const __FlashStringHelper * cmd, cmdfunction_p cb, uint8_t flags = 0);
static void publish_ha_sensor_config(uint8_t type,
uint8_t tag,
const __FlashStringHelper * name,
const uint8_t device_type,
const __FlashStringHelper * entity,
const uint8_t uom,
const bool remove,
const bool has_cmd);
static void publish_ha_sensor_config(uint8_t type,
uint8_t tag,
const __FlashStringHelper * name,
const uint8_t device_type,
const __FlashStringHelper * entity,
const uint8_t uom);
static void show_topic_handlers(uuid::console::Shell & shell, const uint8_t device_type);
static void show_mqtt(uuid::console::Shell & shell);
@@ -133,7 +137,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)
@@ -159,6 +165,10 @@ class Mqtt {
return mqtt_base_;
}
static void base(const char * base) {
mqtt_base_ = base;
}
static uint16_t publish_count() {
return mqtt_message_id_;
}
@@ -173,6 +183,7 @@ class Mqtt {
return ha_climate_format_;
}
// nested_format is 1 if nested, otherwise 2 for single topics
static uint8_t nested_format() {
return nested_format_;
}
@@ -181,18 +192,26 @@ class Mqtt {
nested_format_ = nested_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;
}
@@ -284,7 +303,7 @@ class Mqtt {
static uint8_t ha_climate_format_;
static bool ha_enabled_;
static uint8_t nested_format_;
static uint8_t subscribe_format_;
static bool send_response_;
};
} // namespace emsesp

View File

@@ -28,7 +28,7 @@ void Shower::start() {
shower_alert_ = settings.shower_alert;
});
send_mqtt_stat(false);
set_shower_state(false, true); // turns shower to off and creates HA topic if not already done
}
void Shower::loop() {
@@ -49,18 +49,17 @@ void Shower::loop() {
timer_pause_ = 0; // remove any last pauses
doing_cold_shot_ = false;
duration_ = 0;
shower_on_ = false;
shower_state_ = false;
} else {
// hot water has been on for a while
// first check to see if hot water has been on long enough to be recognized as a Shower/Bath
if (!shower_on_ && (time_now - timer_start_) > SHOWER_MIN_DURATION) {
shower_on_ = true;
send_mqtt_stat(true);
publish_values();
if (!shower_state_ && (time_now - timer_start_) > SHOWER_MIN_DURATION) {
set_shower_state(true);
publish_shower_data();
LOG_DEBUG(F("[Shower] hot water still running, starting shower timer"));
}
// check if the shower has been on too long
else if ((((time_now - timer_start_) > SHOWER_MAX_DURATION) && !doing_cold_shot_) && shower_alert_) {
else if ((time_now - timer_start_) > SHOWER_MAX_DURATION) {
shower_alert_start();
}
}
@@ -77,18 +76,27 @@ void Shower::loop() {
if ((timer_pause_ - timer_start_) > SHOWER_OFFSET_TIME) {
duration_ = (timer_pause_ - timer_start_ - SHOWER_OFFSET_TIME);
if (duration_ > SHOWER_MIN_DURATION) {
send_mqtt_stat(false);
publish_shower_data();
LOG_DEBUG(F("[Shower] finished with duration %d"), duration_);
publish_values();
}
}
#if defined(EMSESP_DEBUG)
else {
if (shower_state_) {
Mqtt::publish("message", "shower state is ON");
} else {
Mqtt::publish("message", "shower state is OFF");
}
}
#endif
// reset everything
timer_start_ = 0;
timer_pause_ = 0;
shower_on_ = false;
doing_cold_shot_ = false;
alert_timer_start_ = 0;
set_shower_state(false);
}
}
return;
@@ -97,19 +105,70 @@ void Shower::loop() {
// at this point we're in the shower cold shot (doing_cold_shot_ == true)
// keep repeating until the time is up
if ((time_now - alert_timer_start_) > SHOWER_COLDSHOT_DURATION) {
shower_alert_stop();
}
}
// send status of shower to MQTT
void Shower::send_mqtt_stat(bool state, bool force) {
// turn back on the hot water for the shower
void Shower::shower_alert_stop() {
if (doing_cold_shot_) {
LOG_DEBUG(F("Shower Alert stopped"));
(void)Command::call(EMSdevice::DeviceType::BOILER, "wwtapactivated", "true");
doing_cold_shot_ = false;
}
}
// turn off hot water to send a shot of cold
void Shower::shower_alert_start() {
if (shower_alert_) {
LOG_DEBUG(F("Shower Alert started"));
(void)Command::call(EMSdevice::DeviceType::BOILER, "wwtapactivated", "false");
doing_cold_shot_ = true;
alert_timer_start_ = uuid::get_uptime(); // timer starts now
}
}
// Publish to the shower_data topic
// showing whether the shower timer and alert are enabled or disabled
// and the duration of the last shower
void Shower::publish_shower_data() {
StaticJsonDocument<EMSESP_JSON_SIZE_SMALL> doc;
char result[10];
doc["shower_timer"] = Helpers::render_boolean(result, shower_timer_);
doc["shower_alert"] = Helpers::render_boolean(result, shower_alert_);
// only publish shower duration if there is a value
if (duration_ > SHOWER_MIN_DURATION) {
char s[50];
snprintf(s, 50, "%d minutes and %d seconds", (uint8_t)(duration_ / 60000), (uint8_t)((duration_ / 1000) % 60));
doc["duration"] = s;
}
Mqtt::publish(F("shower_data"), doc.as<JsonObject>());
}
// send status of shower to MQTT topic called shower_active - which is determined by the state parameter
// and creates the HA config topic if HA enabled
// force is used by EMSESP::publish_all_loop()
void Shower::set_shower_state(bool state, bool force) {
if (!shower_timer_ && !shower_alert_) {
return;
}
char s[7];
Mqtt::publish(F("shower_active"), Helpers::render_boolean(s, state)); // https://github.com/emsesp/EMS-ESP/issues/369
// sets the state
shower_state_ = state;
// if we're in HA mode make sure we've first sent out the HA MQTT Discovery config topic
// only publish if that state has changed
static bool old_shower_state_;
if ((shower_state_ == old_shower_state_) && !force) {
return;
}
old_shower_state_ = shower_state_; // copy current state
char s[7];
Mqtt::publish(F("shower_active"), Helpers::render_boolean(s, shower_state_)); // https://github.com/emsesp/EMS-ESP/issues/369
// send out HA MQTT Discovery config topic
if ((Mqtt::ha_enabled()) && (!ha_configdone_ || force)) {
ha_configdone_ = true;
@@ -126,46 +185,9 @@ void Shower::send_mqtt_stat(bool state, bool force) {
ids.add("ems-esp");
char topic[Mqtt::MQTT_TOPIC_MAX_SIZE];
snprintf_P(topic, sizeof(topic), PSTR("binary_sensor/%s/shower_active/config"), Mqtt::base().c_str());
snprintf(topic, sizeof(topic), "binary_sensor/%s/shower_active/config", Mqtt::base().c_str());
Mqtt::publish_ha(topic, doc.as<JsonObject>()); // publish the config payload with retain flag
}
}
// turn back on the hot water for the shower
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
doing_cold_shot_ = false;
}
}
// turn off hot water to send a shot of cold
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
doing_cold_shot_ = true;
alert_timer_start_ = uuid::get_uptime(); // timer starts now
}
}
// Publish shower data
// returns true if added to MQTT queue went ok
void Shower::publish_values() {
StaticJsonDocument<EMSESP_JSON_SIZE_SMALL> doc;
char result[10];
doc["shower_timer"] = Helpers::render_boolean(result, shower_timer_);
doc["shower_alert"] = Helpers::render_boolean(result, shower_alert_);
// only publish shower duration if there is a value
if (duration_ > SHOWER_MIN_DURATION) {
char s[50];
snprintf_P(s, 50, PSTR("%d minutes and %d seconds"), (uint8_t)(duration_ / 60000), (uint8_t)((duration_ / 1000) % 60));
doc["duration"] = s;
}
Mqtt::publish(F("shower_data"), doc.as<JsonObject>());
}
} // namespace emsesp

View File

@@ -28,7 +28,7 @@ class Shower {
void start();
void loop();
void send_mqtt_stat(bool state, bool force = false);
void set_shower_state(bool state, bool force = false);
bool shower_alert() const {
return shower_alert_;
@@ -55,14 +55,14 @@ class Shower {
static constexpr uint32_t SHOWER_COLDSHOT_DURATION = 10000; // 10 seconds for cold water before turning back hot water
static constexpr uint32_t SHOWER_MAX_DURATION = 420000; // in ms. 7 minutes, before trigger a shot of cold water
void publish_values();
void publish_shower_data();
void shower_alert_start();
void shower_alert_stop();
bool shower_timer_; // true if we want to report back on shower times
bool shower_alert_; // true if we want the alert of cold water
bool ha_configdone_ = false; // for HA MQTT Discovery
bool shower_on_;
bool shower_state_;
uint32_t timer_start_; // ms
uint32_t timer_pause_; // ms
uint32_t duration_; // ms

View File

@@ -23,6 +23,22 @@
#include "test/test.h"
#endif
#ifndef EMSESP_STANDALONE
#ifdef ESP_IDF_VERSION_MAJOR // IDF 4+
#if CONFIG_IDF_TARGET_ESP32 // ESP32/PICO-D4
#include "../esp32/rom/rtc.h"
#elif CONFIG_IDF_TARGET_ESP32S2
#include "../esp32s2/rom/rtc.h"
#elif CONFIG_IDF_TARGET_ESP32C3
#include "../esp32c3/rom/rtc.h"
#else
#error Target CONFIG_IDF_TARGET is not supported
#endif
#else // ESP32 Before IDF 4.0
#include "../rom/rtc.h"
#endif
#endif
namespace emsesp {
uuid::log::Logger System::logger_{F_(system), uuid::log::Facility::KERN};
@@ -34,6 +50,7 @@ uuid::syslog::SyslogService System::syslog_;
// init statics
uint32_t System::heap_start_ = 1; // avoid using 0 to divide-by-zero later
PButton System::myPButton_;
bool System::restart_requested_ = false;
// send on/off to a gpio pin
// value: true = HIGH, false = LOW
@@ -69,16 +86,16 @@ bool System::command_fetch(const char * value, const int8_t id) {
LOG_INFO(F("Requesting data from EMS devices"));
EMSESP::fetch_device_values();
return true;
} else if (value_s == "boiler") {
} else if (value_s == read_flash_string(F_(boiler))) {
EMSESP::fetch_device_values_type(EMSdevice::DeviceType::BOILER);
return true;
} else if (value_s == "thermostat") {
} else if (value_s == read_flash_string(F_(thermostat))) {
EMSESP::fetch_device_values_type(EMSdevice::DeviceType::THERMOSTAT);
return true;
} else if (value_s == "solar") {
} else if (value_s == read_flash_string(F_(solar))) {
EMSESP::fetch_device_values_type(EMSdevice::DeviceType::SOLAR);
return true;
} else if (value_s == "mixer") {
} else if (value_s == read_flash_string(F_(mixer))) {
EMSESP::fetch_device_values_type(EMSdevice::DeviceType::MIXER);
return true;
}
@@ -96,22 +113,22 @@ bool System::command_publish(const char * value, const int8_t id) {
EMSESP::publish_all(true); // includes HA
LOG_INFO(F("Publishing all data to MQTT, including HA configs"));
return true;
} else if (value_s == "boiler") {
} else if (value_s == read_flash_string(F_(boiler))) {
EMSESP::publish_device_values(EMSdevice::DeviceType::BOILER);
return true;
} else if (value_s == "thermostat") {
} else if (value_s == read_flash_string(F_(thermostat))) {
EMSESP::publish_device_values(EMSdevice::DeviceType::THERMOSTAT);
return true;
} else if (value_s == "solar") {
} else if (value_s == read_flash_string(F_(solar))) {
EMSESP::publish_device_values(EMSdevice::DeviceType::SOLAR);
return true;
} else if (value_s == "mixer") {
} else if (value_s == read_flash_string(F_(mixer))) {
EMSESP::publish_device_values(EMSdevice::DeviceType::MIXER);
return true;
} else if (value_s == "other") {
EMSESP::publish_other_values();
return true;
} else if (value_s == "dallassensor") {
} else if (value_s == read_flash_string(F_(dallassensor))) {
EMSESP::publish_sensor_values(true);
return true;
}
@@ -122,9 +139,46 @@ bool System::command_publish(const char * value, const int8_t id) {
return true;
}
// syslog level
bool System::command_syslog_level(const char * value, const int8_t id) {
uint8_t s = 0xff;
if (Helpers::value2enum(value, s, FL_(enum_syslog_level))) {
EMSESP::webSettingsService.update(
[&](WebSettings & settings) {
settings.syslog_level = (int8_t)s - 1;
return StateUpdateResult::CHANGED;
},
"local");
EMSESP::system_.syslog_start();
return true;
}
return false;
}
// watch
bool System::command_watch(const char * value, const int8_t id) {
uint8_t w = 0xff;
if (Helpers::value2enum(value, w, FL_(enum_watch))) {
if (w == 0 || EMSESP::watch() == EMSESP::Watch::WATCH_OFF) {
EMSESP::watch_id(0);
}
EMSESP::watch(w);
return true;
}
uint16_t i = Helpers::hextoint(value);
if (i) {
EMSESP::watch_id(i);
if (EMSESP::watch() == EMSESP::Watch::WATCH_OFF) {
EMSESP::watch(EMSESP::Watch::WATCH_ON);
}
return true;
}
return false;
}
// restart EMS-ESP
void System::restart() {
LOG_INFO(F("Restarting system..."));
void System::system_restart() {
LOG_INFO(F("Restarting EMS-ESP..."));
Shell::loop_all();
delay(1000); // wait a second
#ifndef EMSESP_STANDALONE
@@ -155,7 +209,7 @@ void System::format(uuid::console::Shell & shell) {
LITTLEFS.format();
#endif
System::restart();
System::system_restart();
}
void System::syslog_start() {
@@ -178,6 +232,10 @@ void System::syslog_start() {
syslog_.mark_interval(syslog_mark_interval_);
syslog_.destination(syslog_host_.c_str(), syslog_port_);
syslog_.hostname(hostname().c_str());
// register the command
Command::add(EMSdevice::DeviceType::SYSTEM, F_(syslog_level), System::command_syslog_level, F("changes syslog level"), CommandFlag::ADMIN_ONLY);
} else if (was_enabled) {
// in case service is still running, this flushes the queue
// https://github.com/emsesp/EMS-ESP/issues/496
@@ -207,6 +265,9 @@ void System::get_settings() {
// Board profile
board_profile_ = settings.board_profile;
// Ethernet PHY
phy_type_ = settings.phy_type;
});
}
@@ -233,8 +294,10 @@ void System::wifi_tweak() {
bool s1 = WiFi.getSleep();
WiFi.setSleep(false); // turn off sleep - WIFI_PS_NONE
bool s2 = WiFi.getSleep();
#if defined(EMSESP_DEBUG)
LOG_DEBUG(F("[DEBUG] Adjusting WiFi - Tx power %d->%d, Sleep %d->%d"), p1, p2, s1, s2);
#endif
#endif
}
// check for valid ESP32 pins. This is very dependent on which ESP32 board is being used.
@@ -385,6 +448,11 @@ void System::upload_status(bool in_progress) {
// checks system health and handles LED flashing wizardry
void System::loop() {
// check if we're supposed to do a reset/restart
if (restart_requested()) {
this->system_restart();
}
#ifndef EMSESP_STANDALONE
myPButton_.check(); // check button press
@@ -404,8 +472,8 @@ void System::loop() {
last_heartbeat_ = currentMillis;
send_heartbeat();
}
#ifndef EMSESP_STANDALONE
#if defined(EMSESP_DEBUG)
/*
static uint32_t last_memcheck_ = 0;
@@ -415,6 +483,7 @@ void System::loop() {
}
*/
#endif
#endif
#endif
@@ -430,44 +499,44 @@ void System::show_mem(const char * note) {
}
// create the json for heartbeat
bool System::heartbeat_json(JsonObject & doc) {
uint8_t ems_status = EMSESP::bus_status();
if (ems_status == EMSESP::BUS_STATUS_TX_ERRORS) {
doc["status"] = FJSON("txerror");
} else if (ems_status == EMSESP::BUS_STATUS_CONNECTED) {
doc["status"] = FJSON("connected");
bool System::heartbeat_json(JsonObject & output) {
uint8_t bus_status = EMSESP::bus_status();
if (bus_status == EMSESP::BUS_STATUS_TX_ERRORS) {
output["bus_status"] = FJSON("txerror");
} else if (bus_status == EMSESP::BUS_STATUS_CONNECTED) {
output["bus_status"] = FJSON("connected");
} else {
doc["status"] = FJSON("disconnected");
output["bus_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["txread"] = EMSESP::txservice_.telegram_read_count();
doc["txwrite"] = 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
@@ -527,9 +596,7 @@ void System::network_init(bool refresh) {
last_system_check_ = 0; // force the LED to go from fast flash to pulse
send_heartbeat();
// check board profile for those which use ethernet
// ethernet uses lots of additional memory so we only start it when it's explicitly set in the config
if (!board_profile_.equals("E32") && !board_profile_.equals("TLK110") && !board_profile_.equals("LAN8720") && !board_profile_.equals("OLIMEX")) {
if (phy_type_ == PHY_type::PHY_TYPE_NONE) {
return;
}
@@ -540,24 +607,14 @@ void System::network_init(bool refresh) {
eth_phy_type_t type; // Type of the Ethernet PHY (LAN8720 or TLK110)
eth_clock_mode_t clock_mode; // ETH_CLOCK_GPIO0_IN or ETH_CLOCK_GPIO0_OUT, ETH_CLOCK_GPIO16_OUT, ETH_CLOCK_GPIO17_OUT for 50Hz inverted clock
if (board_profile_.equals("E32") || board_profile_.equals("LAN8720")) {
// BBQKees Gateway E32 (LAN8720)
if (phy_type_ == PHY_type::PHY_TYPE_LAN8720) {
phy_addr = 1;
power = 16;
mdc = 23;
mdio = 18;
type = ETH_PHY_LAN8720;
clock_mode = ETH_CLOCK_GPIO0_IN;
} else if (board_profile_.equals("OLIMEX")) {
// Olimex ESP32-EVB (LAN8720)
phy_addr = 0;
power = -1;
mdc = 23;
mdio = 18;
type = ETH_PHY_LAN8720;
clock_mode = ETH_CLOCK_GPIO0_IN;
} else if (board_profile_.equals("TLK110")) {
// Ethernet (TLK110)
} else if (phy_type_ == PHY_type::PHY_TYPE_TLK110) {
phy_addr = 31;
power = -1;
mdc = 23;
@@ -565,11 +622,20 @@ void System::network_init(bool refresh) {
type = ETH_PHY_TLK110;
clock_mode = ETH_CLOCK_GPIO0_IN;
} else {
return; // invalid combi
return; // no valid profile
}
// bool have_ethernet = ETH.begin(phy_addr, power, mdc, mdio, type, clock_mode);
(void)ETH.begin(phy_addr, power, mdc, mdio, type, clock_mode);
// special case for Olimex ESP32-EVB (LAN8720) (different power and phy_addr)
if (board_profile_.equals("OLIMEX")) {
phy_addr = 0;
power = -1;
mdc = 23;
mdio = 18;
type = ETH_PHY_LAN8720;
clock_mode = ETH_CLOCK_GPIO0_IN;
}
ETH.begin(phy_addr, power, mdc, mdio, type, clock_mode);
}
// check health of system, done every few seconds
@@ -606,20 +672,33 @@ void System::system_check() {
}
// commands - takes static function pointers
// these commands respond to the topic "system" and take a payload like {cmd:"", data:"", id:""}
// no individual subscribe for pin command because id is needed
void System::commands_init() {
Command::add(EMSdevice::DeviceType::SYSTEM, F_(pin), System::command_pin, F("set GPIO"), CommandFlag::MQTT_SUB_FLAG_NOSUB | CommandFlag::ADMIN_ONLY);
Command::add(EMSdevice::DeviceType::SYSTEM, F_(send), System::command_send, F("send a telegram"), CommandFlag::ADMIN_ONLY);
Command::add(EMSdevice::DeviceType::SYSTEM, F_(publish), System::command_publish, F("force a MQTT publish"), CommandFlag::ADMIN_ONLY);
Command::add(EMSdevice::DeviceType::SYSTEM, F_(fetch), System::command_fetch, F("refresh all EMS values"), CommandFlag::ADMIN_ONLY);
Command::add(EMSdevice::DeviceType::SYSTEM,
F_(pin),
System::command_pin,
F("sets a GPIO on/off"),
CommandFlag::MQTT_SUB_FLAG_NOSUB | CommandFlag::ADMIN_ONLY); // dont create a MQTT topic for this
Command::add(EMSdevice::DeviceType::SYSTEM, F_(send), System::command_send, F("sends a telegram"), CommandFlag::ADMIN_ONLY);
Command::add(EMSdevice::DeviceType::SYSTEM, F_(fetch), System::command_fetch, F("refreshes 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_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_(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(EMSdevice::DeviceType::SYSTEM, F_(info), System::command_info, F("show system status"));
Command::add(EMSdevice::DeviceType::SYSTEM, F_(settings), System::command_settings, F("shows system settings"));
Command::add(EMSdevice::DeviceType::SYSTEM, F_(commands), System::command_commands, F("shows system commands"));
#if defined(EMSESP_DEBUG)
Command::add(EMSdevice::DeviceType::SYSTEM, F("test"), System::command_test, F("run tests"));
Command::add(EMSdevice::DeviceType::SYSTEM, F("test"), System::command_test, F("runs a specific test"));
#endif
// MQTT subscribe "ems-esp/system/#"
Mqtt::subscribe(EMSdevice::DeviceType::SYSTEM, "system/#", nullptr); // use empty function callback
}
// flashes the LED
@@ -749,7 +828,7 @@ void System::show_system(uuid::console::Shell & shell) {
} else {
shell.printfln(F("Syslog: %s"), syslog_.started() ? "started" : "stopped");
shell.print(F(" "));
shell.printfln(F_(host_fmt), !syslog_host_.isEmpty() ? syslog_host_.c_str() : uuid::read_flash_string(F_(unset)).c_str());
shell.printfln(F_(host_fmt), !syslog_host_.isEmpty() ? syslog_host_.c_str() : read_flash_string(F_(unset)).c_str());
shell.printfln(F(" IP: %s"), uuid::printable_to_string(syslog_.ip()).c_str());
shell.print(F(" "));
shell.printfln(F_(port_fmt), syslog_port_);
@@ -770,22 +849,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;
@@ -798,7 +877,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();
@@ -808,7 +887,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;
@@ -828,12 +907,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;
@@ -841,38 +920,44 @@ 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["tx_mode"] = settings.tx_mode;
node["tx_delay"] = settings.tx_delay;
node["ems_bus_id"] = settings.ems_bus_id;
node = output.createNestedObject("Settings");
node["board_profile"] = settings.board_profile;
node["tx_mode"] = settings.tx_mode;
node["ems_bus_id"] = settings.ems_bus_id;
node["syslog_enabled"] = settings.syslog_enabled;
node["syslog_level"] = settings.syslog_level;
node["syslog_mark_interval"] = settings.syslog_mark_interval;
node["syslog_host"] = settings.syslog_host;
node["syslog_port"] = settings.syslog_port;
node["master_thermostat"] = settings.master_thermostat;
node["shower_timer"] = settings.shower_timer;
node["shower_alert"] = settings.shower_alert;
node["rx_gpio"] = settings.rx_gpio;
node["tx_gpio"] = settings.tx_gpio;
node["dallas_gpio"] = settings.dallas_gpio;
node["dallas_parasite"] = settings.dallas_parasite;
node["led_gpio"] = settings.led_gpio;
node["hide_led"] = settings.hide_led;
node["notoken_api"] = settings.notoken_api;
node["dallas_format"] = settings.dallas_format;
node["bool_format"] = settings.bool_format;
node["enum_format"] = settings.enum_format;
node["analog_enabled"] = settings.analog_enabled;
node["pbutton_gpio"] = settings.pbutton_gpio;
node["board_profile"] = settings.board_profile;
node["master_thermostat"] = settings.master_thermostat;
node["shower_timer"] = settings.shower_timer;
node["shower_alert"] = settings.shower_alert;
node["rx_gpio"] = settings.rx_gpio;
node["tx_gpio"] = settings.tx_gpio;
node["dallas_gpio"] = settings.dallas_gpio;
node["pbutton_gpio"] = settings.pbutton_gpio;
node["led_gpio"] = settings.led_gpio;
node["phy_type"] = settings.phy_type;
node["hide_led"] = settings.hide_led;
node["notoken_api"] = settings.notoken_api;
node["dallas_parasite"] = settings.dallas_parasite;
node["dallas_format"] = settings.dallas_format;
node["bool_format"] = settings.bool_format;
node["enum_format"] = settings.enum_format;
node["analog_enabled"] = settings.analog_enabled;
});
return true;
@@ -880,31 +965,67 @@ 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");
// System
node = output.createNestedObject("System");
node["version"] = EMSESP_APP_VERSION;
node["uptime"] = uuid::log::format_timestamp_ms(uuid::get_uptime_ms(), 3);
node["uptime_sec"] = uuid::get_uptime_sec();
node["version"] = EMSESP_APP_VERSION;
node["uptime"] = uuid::log::format_timestamp_ms(uuid::get_uptime_ms(), 3);
node["uptime (seconds)"] = uuid::get_uptime_sec();
#ifndef EMSESP_STANDALONE
node["freemem"] = ESP.getFreeHeap() / 1000L; // kilobytes
#endif
node["reset reason"] = EMSESP::system_.reset_reason(0) + " / " + EMSESP::system_.reset_reason(1);
node = json.createNestedObject("Status");
if (EMSESP::dallas_enabled()) {
node["Dallas sensors"] = EMSESP::sensor_devices().size();
}
#ifndef EMSESP_STANDALONE
// Network
node = output.createNestedObject("Network");
if (WiFi.status() == WL_CONNECTED) {
node["connection"] = F("WiFi");
node["hostname"] = WiFi.getHostname();
node["SSID"] = WiFi.SSID();
node["BSSID"] = WiFi.BSSIDstr();
node["RSSI"] = WiFi.RSSI();
node["MAC"] = WiFi.macAddress();
node["IPv4 address"] = uuid::printable_to_string(WiFi.localIP()) + "/" + uuid::printable_to_string(WiFi.subnetMask());
node["IPv4 gateway"] = uuid::printable_to_string(WiFi.gatewayIP());
node["IPv4 nameserver"] = uuid::printable_to_string(WiFi.dnsIP());
if (WiFi.localIPv6().toString() != "0000:0000:0000:0000:0000:0000:0000:0000") {
node["IPv6 address"] = uuid::printable_to_string(WiFi.localIPv6());
}
} else if (EMSESP::system_.ethernet_connected()) {
node["connection"] = F("Ethernet");
node["hostname"] = ETH.getHostname();
node["MAC"] = ETH.macAddress();
node["IPv4 address"] = uuid::printable_to_string(ETH.localIP()) + "/" + uuid::printable_to_string(ETH.subnetMask());
node["IPv4 gateway"] = uuid::printable_to_string(ETH.gatewayIP());
node["IPv4 nameserver"] = uuid::printable_to_string(ETH.dnsIP());
if (ETH.localIPv6().toString() != "0000:0000:0000:0000:0000:0000:0000:0000") {
node["IPv6 address"] = uuid::printable_to_string(ETH.localIPv6());
}
}
#endif
// Status
node = output.createNestedObject("Status");
switch (EMSESP::bus_status()) {
case EMSESP::BUS_STATUS_OFFLINE:
node["bus"] = (F("disconnected"));
node["bus status"] = (F("disconnected"));
break;
case EMSESP::BUS_STATUS_TX_ERRORS:
node["bus"] = (F("connected, instable tx"));
node["bus status"] = (F("connected, instable tx"));
break;
case EMSESP::BUS_STATUS_CONNECTED:
default:
node["bus"] = (F("connected"));
node["bus status"] = (F("connected"));
break;
}
@@ -918,24 +1039,24 @@ bool System::command_info(const char * value, const int8_t id, JsonObject & json
node["rx line quality"] = EMSESP::rxservice_.quality();
node["tx line quality"] = EMSESP::txservice_.quality();
if (Mqtt::enabled()) {
node["MQTT"] = Mqtt::connected() ? F_(connected) : F_(disconnected);
node["MQTT publishes"] = Mqtt::publish_count();
node["MQTT publish fails"] = Mqtt::publish_fails();
}
if (EMSESP::dallas_enabled()) {
node["dallas sensors"] = EMSESP::sensor_devices().size();
node["dallas reads"] = EMSESP::sensor_reads();
node["dallas fails"] = EMSESP::sensor_fails();
node["Dallas reads"] = EMSESP::sensor_reads();
node["Dallas fails"] = EMSESP::sensor_fails();
}
#ifndef EMSESP_STANDALONE
if (EMSESP::system_.syslog_enabled_) {
node["syslog_ip"] = syslog_.ip();
node["syslog_started"] = syslog_.started();
node["syslog IP"] = syslog_.ip();
node["syslog active"] = syslog_.started();
}
#endif
}
// show EMS devices
JsonArray devices = json.createNestedArray("Devices");
// Devices - show EMS 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)) {
@@ -943,7 +1064,10 @@ bool System::command_info(const char * value, const int8_t id, JsonObject & json
obj["type"] = emsdevice->device_type_name();
obj["name"] = emsdevice->to_string();
char result[200];
obj["handlers"] = emsdevice->show_telegram_handlers(result);
(void)emsdevice->show_telegram_handlers(result);
if (result[0] != '\0') {
obj["handlers"] = result; // don't show hanlders if there aren't any
}
}
}
}
@@ -960,39 +1084,79 @@ bool System::command_test(const char * value, const int8_t id) {
#endif
// takes a board profile and populates a data array with GPIO configurations
// data = led, dallas, rx, tx, button
// data = led, dallas, rx, tx, button, phy_type
// returns false if profile is not found
bool System::load_board_profile(std::vector<uint8_t> & data, const std::string & board_profile) {
if (board_profile == "S32") {
data = {2, 18, 23, 5, 0}; // BBQKees Gateway S32
data = {2, 18, 23, 5, 0, PHY_type::PHY_TYPE_NONE}; // BBQKees Gateway S32
} else if (board_profile == "E32") {
data = {2, 4, 5, 17, 33}; // BBQKees Gateway E32
data = {2, 4, 5, 17, 33, PHY_type::PHY_TYPE_LAN8720}; // BBQKees Gateway E32
} else if (board_profile == "MH-ET") {
data = {2, 18, 23, 5, 0}; // MH-ET Live D1 Mini
data = {2, 18, 23, 5, 0, PHY_type::PHY_TYPE_NONE}; // MH-ET Live D1 Mini
} else if (board_profile == "NODEMCU") {
data = {2, 18, 23, 5, 0}; // NodeMCU 32S
data = {2, 18, 23, 5, 0, PHY_type::PHY_TYPE_NONE}; // NodeMCU 32S
} else if (board_profile == "LOLIN") {
data = {2, 18, 17, 16, 0}; // Lolin D32
data = {2, 18, 17, 16, 0, PHY_type::PHY_TYPE_NONE}; // Lolin D32
} else if (board_profile == "OLIMEX") {
data = {0, 0, 36, 4, 34}; // Olimex ESP32-EVB (uses U1TXD/U1RXD/BUTTON, no LED or Dallas)
} else if (board_profile == "TLK110") {
data = {2, 4, 5, 17, 33}; // Generic Ethernet (TLK110)
} else if (board_profile == "LAN8720") {
data = {2, 4, 5, 17, 33}; // Generic Ethernet (LAN8720)
data = {0, 0, 36, 4, 34, PHY_type::PHY_TYPE_LAN8720}; // Olimex ESP32-EVB (uses U1TXD/U1RXD/BUTTON, no LED or Dallas)
} else {
data = {EMSESP_DEFAULT_LED_GPIO, EMSESP_DEFAULT_DALLAS_GPIO, EMSESP_DEFAULT_RX_GPIO, EMSESP_DEFAULT_TX_GPIO, EMSESP_DEFAULT_PBUTTON_GPIO};
data = {EMSESP_DEFAULT_LED_GPIO,
EMSESP_DEFAULT_DALLAS_GPIO,
EMSESP_DEFAULT_RX_GPIO,
EMSESP_DEFAULT_TX_GPIO,
EMSESP_DEFAULT_PBUTTON_GPIO,
EMSESP_DEFAULT_PHY_TYPE};
return (board_profile == "CUSTOM");
}
return true;
}
// restart command - perform a hard reset
// restart command - perform a hard reset by setting flag
bool System::command_restart(const char * value, const int8_t id) {
#ifndef EMSESP_STANDALONE
ESP.restart();
#endif
restart_requested(true);
return true;
}
const std::string System::reset_reason(uint8_t cpu) {
#ifndef EMSESP_STANDALONE
switch (rtc_get_reset_reason(cpu)) {
case 1:
return ("Power on reset");
// case 2 :reset pin not on esp32
case 3:
return ("Software reset");
case 4:
return ("Legacy watch dog reset");
case 5:
return ("Deep sleep reset");
case 6:
return ("Reset by SDIO");
case 7:
return ("Timer group0 watch dog reset");
case 8:
return ("Timer group1 watch dog reset");
case 9:
return ("RTC watch dog reset");
case 10:
return ("Intrusion reset CPU");
case 11:
return ("Timer group reset CPU");
case 12:
return ("Software reset CPU");
case 13:
return ("RTC watch dog reset: CPU");
case 14:
return ("APP CPU reseted by PRO CPU");
case 15:
return ("Brownout reset");
case 16:
return ("RTC watch dog reset: CPU+RTC");
default:
break;
}
#endif
return ("Unkonwn");
}
} // namespace emsesp

View File

@@ -42,6 +42,8 @@ using uuid::console::Shell;
namespace emsesp {
enum PHY_type : uint8_t { PHY_TYPE_NONE = 0, PHY_TYPE_LAN8720, PHY_TYPE_TLK110 };
class System {
public:
void start(uint32_t heap_start);
@@ -56,12 +58,16 @@ class System {
#if defined(EMSESP_DEBUG)
static bool command_test(const char * value, const int8_t id);
#endif
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);
void restart();
const std::string reset_reason(uint8_t cpu);
void system_restart();
void format(uuid::console::Shell & shell);
void upload_status(bool in_progress);
bool upload_status();
@@ -70,7 +76,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);
@@ -82,6 +88,14 @@ class System {
static bool is_valid_gpio(uint8_t pin);
static bool load_board_profile(std::vector<uint8_t> & data, const std::string & board_profile);
static void restart_requested(bool restart_requested) {
restart_requested_ = restart_requested;
}
static bool restart_requested() {
return restart_requested_;
}
bool analog_enabled() {
return analog_enabled_;
}
@@ -106,17 +120,14 @@ class System {
ethernet_connected_ = b;
}
void network_connected(bool b) {
network_connected_ = b;
}
bool network_connected() {
#ifndef EMSESP_STANDALONE
return network_connected_;
return (ethernet_connected() || WiFi.isConnected());
#else
return true;
#endif
}
void show_system(uuid::console::Shell & shell);
void wifi_reconnect();
void show_users(uuid::console::Shell & shell);
@@ -124,6 +135,7 @@ class System {
private:
static uuid::log::Logger logger_;
static uint32_t heap_start_;
static bool restart_requested_;
// button
static PButton myPButton_; // PButton instance
@@ -160,7 +172,6 @@ class System {
uint32_t last_system_check_ = 0;
bool upload_status_ = false; // true if we're in the middle of a OTA firmware upload
bool ethernet_connected_ = false;
bool network_connected_ = false;
uint16_t analog_;
// settings
@@ -172,6 +183,7 @@ class System {
bool low_clock_;
String board_profile_;
uint8_t pbutton_gpio_;
uint8_t phy_type_;
int8_t syslog_level_;
uint32_t syslog_mark_interval_;
String syslog_host_;

View File

@@ -84,6 +84,7 @@ std::string Telegram::to_string() const {
uint8_t data[EMS_MAX_TELEGRAM_LENGTH];
uint8_t length = 0;
data[0] = this->src ^ RxService::ems_mask();
data[3] = this->offset;
if (this->operation == Telegram::Operation::TX_READ) {
data[1] = this->dest | 0x80;
data[4] = this->message_data[0];
@@ -233,6 +234,15 @@ void RxService::add(uint8_t * data, uint8_t length) {
rx_telegrams_.emplace_back(rx_telegram_id_++, std::move(telegram)); // add to queue
}
// add empty telegram to rx-queue
void RxService::add_empty(const uint8_t src, const uint8_t dest, const uint16_t type_id) {
auto telegram = std::make_shared<Telegram>(Telegram::Operation::RX, src, dest, type_id, 0, nullptr, 0);
// only if queue is not full
if (rx_telegrams_.size() < MAX_RX_TELEGRAMS) {
rx_telegrams_.emplace_back(rx_telegram_id_++, std::move(telegram)); // add to queue
}
}
// start and initialize Tx
// send out request to EMS bus for all devices
void TxService::start() {
@@ -542,14 +552,14 @@ void TxService::send_raw(const char * telegram_data) {
// get first value, which should be the src
if ((p = strtok(telegram, " ,"))) { // delimiter
strlcpy(value, p, 10);
strlcpy(value, p, sizeof(value));
data[0] = (uint8_t)strtol(value, 0, 16);
}
// and iterate until end
while (p != 0) {
if ((p = strtok(nullptr, " ,"))) {
strlcpy(value, p, 10);
strlcpy(value, p, sizeof(value));
uint8_t val = (uint8_t)strtol(value, 0, 16);
data[++count] = val;
}
@@ -575,6 +585,9 @@ void TxService::retry_tx(const uint8_t operation, const uint8_t * data, const ui
(operation == Telegram::Operation::TX_WRITE) ? F("Write") : F("Read"),
MAXIMUM_TX_RETRIES,
telegram_last_->to_string().c_str());
if (operation == Telegram::Operation::TX_READ) {
EMSESP::rxservice_.add_empty(telegram_last_->dest, telegram_last_->src, telegram_last_->type_id);
}
return;
}

View File

@@ -210,6 +210,7 @@ class RxService : public EMSbus {
void loop();
void add(uint8_t * data, uint8_t length);
void add_empty(const uint8_t src, const uint8_t dst, const uint16_t type_id);
uint32_t telegram_count() const {
return telegram_count_;
@@ -374,6 +375,7 @@ class TxService : public EMSbus {
#else
static constexpr uint8_t MAXIMUM_TX_RETRIES = 3;
#endif
static constexpr uint32_t POST_SEND_DELAY = 2000;
private:

View File

@@ -30,7 +30,7 @@ bool Test::run_test(const char * command, int8_t id) {
}
if (strcmp(command, "general") == 0) {
EMSESP::logger().info(F("Testing general..."));
EMSESP::logger().info(F("Testing general. Adding a Boiler and Thermostat"));
add_device(0x08, 123); // Nefit Trendline
add_device(0x18, 157); // Bosch CR100
@@ -54,7 +54,7 @@ bool Test::run_test(const char * command, int8_t id) {
}
if (strcmp(command, "310") == 0) {
EMSESP::logger().info(F("Testing GB072/RC310..."));
EMSESP::logger().info(F("Adding a GB072/RC310 combo..."));
add_device(0x08, 123); // GB072
add_device(0x10, 158); // RC310
@@ -81,7 +81,7 @@ bool Test::run_test(const char * command, int8_t id) {
}
if (strcmp(command, "gateway") == 0) {
EMSESP::logger().info(F("Testing gateway..."));
EMSESP::logger().info(F("Adding a Gateway..."));
// add 0x48 KM200, via a version command
rx_telegram({0x48, 0x0B, 0x02, 0x00, 0xBD, 0x04, 0x06, 00, 00, 00, 00, 00, 00, 00});
@@ -101,7 +101,7 @@ bool Test::run_test(const char * command, int8_t id) {
}
if (strcmp(command, "mixer") == 0) {
EMSESP::logger().info(F("Testing mixer..."));
EMSESP::logger().info(F("Adding a mixer..."));
// add controller
add_device(0x09, 114);
@@ -123,7 +123,7 @@ bool Test::run_test(const char * command, int8_t id) {
}
if (strcmp(command, "boiler") == 0) {
EMSESP::logger().info(F("Testing boiler..."));
EMSESP::logger().info(F("Adding boiler..."));
add_device(0x08, 123); // Nefit Trendline
// UBAuptime
@@ -140,7 +140,7 @@ bool Test::run_test(const char * command, int8_t id) {
}
if (strcmp(command, "thermostat") == 0) {
EMSESP::logger().info(F("Testing thermostat..."));
EMSESP::logger().info(F("Adding thermostat..."));
add_device(0x10, 192); // FW120
@@ -153,7 +153,7 @@ bool Test::run_test(const char * command, int8_t id) {
}
if (strcmp(command, "solar") == 0) {
EMSESP::logger().info(F("Testing solar..."));
EMSESP::logger().info(F("Adding solar..."));
add_device(0x30, 163); // SM100
@@ -172,7 +172,7 @@ bool Test::run_test(const char * command, int8_t id) {
}
if (strcmp(command, "heatpump") == 0) {
EMSESP::logger().info(F("Testing heatpump..."));
EMSESP::logger().info(F("Adding heatpump..."));
add_device(0x38, 200); // Enviline module
add_device(0x10, 192); // FW120 thermostat
@@ -187,7 +187,8 @@ bool Test::run_test(const char * command, int8_t id) {
return false;
}
// used with the 'test' command, under su/admin
// These next tests are run from the Console
// using the test command
void Test::run_test(uuid::console::Shell & shell, const std::string & cmd) {
// switch to su
shell.add_flags(CommandFlags::ADMIN);
@@ -270,6 +271,17 @@ void Test::run_test(uuid::console::Shell & shell, const std::string & cmd) {
char result[100];
Helpers::render_value(result, test_float, 2);
shell.printfln("Float test from %f to %s", test_float, result);
uint16_t temp;
float doub;
temp = 0x0201; // decimal 513
doub = Helpers::round2(temp, 10); // divide by 10
shell.printfln("Round test from x%02X to %d to %f", temp, temp, doub);
doub = Helpers::round2(temp, 10); // divide by 10
shell.printfln("Round test div10 from x%02X to %d to %f", temp, temp, doub);
temp = 0x63;
doub = Helpers::round2(temp, 2); // divide by 2
shell.printfln("Round test div2 from x%02X to %d to %f", temp, temp, doub);
}
if (command == "devices") {
@@ -414,6 +426,7 @@ void Test::run_test(uuid::console::Shell & shell, const std::string & cmd) {
EMSESP::mqtt_.incoming("ems-esp/boiler", "{\"cmd\":\"wwonetime\",\"id\":0,\"data\":\"off\"}");
EMSESP::mqtt_.incoming("ems-esp/boiler", "{\"cmd\":\"wwonetime\",\"hc\":1,\"data\":\"on\"}");
EMSESP::mqtt_.incoming("ems-esp/boiler", "{\"cmd\":\"wwonetime\",\"data\":\"on\",\"hc\":1}");
EMSESP::mqtt_.incoming("ems-esp/boiler", "{\"cmd\":\"heatingactivated\",\"data\":1}");
shell.invoke_command("show mqtt");
}
@@ -424,7 +437,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") {
@@ -452,6 +465,327 @@ void Test::run_test(uuid::console::Shell & shell, const std::string & cmd) {
shell.invoke_command("call system publish");
shell.invoke_command("show mqtt");
shell.invoke_command("call boiler fanwork");
shell.invoke_command("call thermostat seltemp"); // sensor.thermostat_hc1_selected_room_temperature
shell.invoke_command("call thermostat entities");
shell.invoke_command("call boiler entities");
}
if (command == "dv") {
shell.printfln(F("Testing device value rendering"));
Mqtt::ha_enabled(true);
Mqtt::nested_format(1);
Mqtt::send_response(false);
run_test("boiler");
run_test("thermostat");
// change a value to null/bogus/dormant
shell.invoke_command("call boiler wwseltemp");
shell.invoke_command("call system publish");
}
if (command == "lastcode") {
shell.printfln(F("Testing lastcode"));
Mqtt::ha_enabled(false);
Mqtt::nested_format(1);
Mqtt::send_response(false);
run_test("boiler");
// run_test("thermostat");
// 0xC2
// [emsesp] Boiler(0x08) -> Me(0x0B), UBAErrorMessage3(0xC2), data: 08 AC 00 10 31 48 30 31 15 80 95 0B 0E 10 38 00 7F FF FF FF 08 AC 00 10 09 41 30
uart_telegram(
{0x08, 0x0B, 0xC2, 0, 0x08, 0xAC, 00, 0x10, 0x31, 0x48, 0x30, 0x31, 0x15, 0x80, 0x95, 0x0B, 0x0E, 0x10, 0x38, 00, 0x7F, 0xFF, 0xFF, 0xFF});
// shell.invoke_command("show");
}
if (command == "dv2") {
shell.printfln(F("Testing device value lost"));
// Boiler -> Me, UBAParameterWW(0x33)
// wwseltemp = goes from 52 degrees (0x34) to void (0xFF)
// it should delete the HA config topic homeassistant/sensor/ems-esp/boiler_wwseltemp/config
uart_telegram({0x08, 0x0B, 0x33, 0x00, 0x08, 0xFF, 0xFF, 0xFB, 0x00, 0x28, 0x00, 0x00, 0x46, 0x00, 0xFF, 0xFF, 0x00});
shell.invoke_command("call boiler wwseltemp");
shell.invoke_command("call system publish");
}
if (command == "api") {
shell.printfln(F("Testing API with MQTT and REST, standalone"));
Mqtt::ha_enabled(true);
// Mqtt::ha_enabled(false);
Mqtt::nested_format(1);
Mqtt::send_response(true);
run_test("boiler");
run_test("thermostat");
#if defined(EMSESP_STANDALONE)
AsyncWebServerRequest requestX;
DynamicJsonDocument docX(2000);
JsonVariant jsonX;
requestX.method(HTTP_GET);
/*
requestX.url("/api"); // should fail
EMSESP::webAPIService.webAPIService_get(&requestX);
return;
*/
/*
requestX.url("/api/thermostat/seltemp");
EMSESP::webAPIService.webAPIService_get(&requestX);
return;
*/
/*
requestX.url("/api/thermostat/mode/auto");
EMSESP::webAPIService.webAPIService_get(&requestX);
return;
*/
/*
requestX.url("/api/thermostat"); // check if defaults to info
EMSESP::webAPIService.webAPIService_get(&requestX);
requestX.url("/api/thermostat/info");
EMSESP::webAPIService.webAPIService_get(&requestX);
requestX.url("/api/thermostat/values");
EMSESP::webAPIService.webAPIService_get(&requestX);
return;
requestX.url("/api/thermostat/mode");
EMSESP::webAPIService.webAPIService_get(&requestX);
return;
*/
/*
requestX.url("/api/system"); // check if defaults to info
EMSESP::webAPIService.webAPIService_get(&requestX);
emsesp::EMSESP::logger().notice("*");
requestX.url("/api/system/info");
EMSESP::webAPIService.webAPIService_get(&requestX);
emsesp::EMSESP::logger().notice("*");
requestX.url("/api/thermostat"); // check if defaults to values
EMSESP::webAPIService.webAPIService_get(&requestX);
emsesp::EMSESP::logger().notice("*");
requestX.url("/api/thermostat/info");
EMSESP::webAPIService.webAPIService_get(&requestX);
emsesp::EMSESP::logger().notice("*");
requestX.url("/api/thermostat/seltemp");
EMSESP::webAPIService.webAPIService_get(&requestX);
return;
*/
/*
requestX.url("/api/system/restart");
EMSESP::webAPIService.webAPIService_get(&requestX);
return;
*/
/*
requestX.url("/api/dallassensor/xxxx");
EMSESP::webAPIService.webAPIService_get(&requestX);
emsesp::EMSESP::logger().notice("****");
requestX.url("/api/dallassensor/info");
EMSESP::webAPIService.webAPIService_get(&requestX);
return;
*/
/*
requestX.url("/api"); // should fail
EMSESP::webAPIService.webAPIService_get(&requestX);
requestX.method(HTTP_POST);
char dataX[] = "{\"device\":\"thermostat\", \"entity\":\"seltemp\",\"value\":13}";
deserializeJson(docX, dataX);
jsonX = docX.as<JsonVariant>();
requestX.url("/api");
EMSESP::webAPIService.webAPIService_post(&requestX, jsonX);
return;
*/
/*
// char dataX[] = "{\"value\":\"0B 88 19 19 02\"}";
char dataX[] = "{\"name\":\"temp\",\"value\":11}";
deserializeJson(docX, dataX);
jsonX = docX.as<JsonVariant>();
// requestX.url("/api/system/send");
requestX.url("/api/thermostat");
EMSESP::webAPIService.webAPIService_post(&requestX, jsonX);
return;
*/
/*
char dataX[] = "{}";
deserializeJson(docX, dataX);
jsonX = docX.as<JsonVariant>();
requestX.url("/api/thermostat/mode/auto"); // should fail
EMSESP::webAPIService.webAPIService_post(&requestX, 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);
#endif
// 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");
EMSESP::mqtt_.incoming("ems-esp/boiler/syspress"); // empty payload, sends reponse
EMSESP::mqtt_.incoming("ems-esp/thermostat/mode"); // empty payload, sends reponse
EMSESP::mqtt_.incoming("ems-esp/system/publish");
EMSESP::mqtt_.incoming("ems-esp/thermostat/seltemp"); // empty payload, sends reponse
EMSESP::mqtt_.incoming("ems-esp/boiler/wwseltemp", "59");
EMSESP::mqtt_.incoming("ems-esp/boiler/wwseltemp");
EMSESP::mqtt_.incoming("ems-esp/thermostat", "{\"cmd\":\"mode\",\"data\":\"heat\",\"id\":1}");
// MQTT bad tests
EMSESP::mqtt_.incoming("ems-esp/thermostate/mode", "auto"); // unknown device
EMSESP::mqtt_.incoming("ems-esp/thermostat/modee", "auto"); // unknown command
EMSESP::mqtt_.incoming("ems-esp/thermostat/mode/auto", "auto"); // invalid, not allowed
// check extended MQTT base
Mqtt::base("home/cellar/heating");
EMSESP::mqtt_.incoming("home/cellar/heating/thermostat/mode"); // empty payload, sends reponse
#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/values");
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\":\"seltemp\",\"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);
// emsesp::EMSESP::logger().notice("*");
// should fail
char data8[] = "{}";
deserializeJson(doc, data8);
json = doc.as<JsonVariant>();
request.url("/api/thermostat/mode/auto");
EMSESP::webAPIService.webAPIService_post(&request, json);
#endif
}
if (command == "mqtt_nested") {
@@ -466,8 +800,9 @@ void Test::run_test(uuid::console::Shell & shell, const std::string & cmd) {
// first with nested
Mqtt::nested_format(1);
shell.invoke_command("call system publish");
shell.invoke_command("show mqtt");
// then without nested
// then without nested - single mode
Mqtt::nested_format(2);
shell.invoke_command("call system publish");
shell.invoke_command("show mqtt");
@@ -797,8 +1132,8 @@ void Test::run_test(uuid::console::Shell & shell, const std::string & cmd) {
// fit it up, to its limit of the Json buffer (which is about 169 records)
for (uint8_t i = 0; i < 200; i++) {
snprintf_P(key, 7, PSTR("key%03d"), i);
snprintf_P(value, 7, PSTR("val%03d"), i);
snprintf(key, 7, "key%03d", i);
snprintf(value, 7, "val%03d", i);
doc[key] = value;
}
doc.shrinkToFit();
@@ -813,6 +1148,9 @@ void Test::run_test(uuid::console::Shell & shell, const std::string & cmd) {
if (command == "mqtt") {
shell.printfln(F("Testing MQTT..."));
Mqtt::ha_enabled(false);
Mqtt::enabled(true);
// add a boiler
add_device(0x08, 123); // Nefit Trendline
@@ -825,6 +1163,8 @@ void Test::run_test(uuid::console::Shell & shell, const std::string & cmd) {
uart_telegram("98 00 FF 00 01 A5 00 CF 21 2E 00 00 2E 24 03 25 03 03 01 03 25 00 C8 00 00 11 01 03"); // without CRC
uart_telegram_withCRC("98 00 FF 00 01 A5 00 CF 21 2E 00 00 2E 24 03 25 03 03 01 03 25 00 C8 00 00 11 01 03 13"); // with CRC
shell.invoke_command("call system publish");
shell.invoke_command("show mqtt");
shell.loop_all();
char boiler_topic[Mqtt::MQTT_TOPIC_MAX_SIZE];
@@ -837,11 +1177,11 @@ void Test::run_test(uuid::console::Shell & shell, const std::string & cmd) {
strcpy(thermostat_topic, "ems-esp/thermostat");
strcpy(system_topic, "ems-esp/system");
EMSESP::mqtt_.incoming(boiler_topic, ""); // test if ignore empty payloads // invalid format
EMSESP::mqtt_.incoming(boiler_topic, ""); // test if ignore empty payloads
EMSESP::mqtt_.incoming(boiler_topic, "12345"); // invalid format
EMSESP::mqtt_.incoming("bad_topic", "12345"); // no matching topic
EMSESP::mqtt_.incoming(boiler_topic, "{\"cmd\":\"garbage\",\"data\":22.52}"); // should report error
EMSESP::mqtt_.incoming(boiler_topic, "12345"); // error: invalid format
EMSESP::mqtt_.incoming("bad_topic", "12345"); // error: no matching topic
EMSESP::mqtt_.incoming(boiler_topic, "{\"cmd\":\"garbage\",\"data\":22.52}"); // error: should report error
EMSESP::mqtt_.incoming(boiler_topic, "{\"cmd\":\"comfort\",\"data\":\"eco\"}");
EMSESP::mqtt_.incoming(boiler_topic, "{\"cmd\":\"wwactivated\",\"data\":\"1\"}"); // with quotes
@@ -853,7 +1193,7 @@ void Test::run_test(uuid::console::Shell & shell, const std::string & cmd) {
EMSESP::mqtt_.incoming(thermostat_topic, "{\"cmd\":\"wwmode\",\"data\":\"auto\"}");
EMSESP::mqtt_.incoming(thermostat_topic, "{\"cmd\":\"control\",\"data\":\"1\"}"); // RC35 only, should error
EMSESP::mqtt_.incoming(thermostat_topic, "{\"cmd\":\"mode\",\"data\":\"poep\",\"id\":2}"); // invalid mode
EMSESP::mqtt_.incoming(thermostat_topic, "{\"cmd\":\"mode\",\"data\":\"typo\",\"id\":2}"); // invalid mode
EMSESP::mqtt_.incoming(thermostat_topic, "{\"cmd\":\"mode\",\"data\":\"auto\",\"id\":2}");
EMSESP::mqtt_.incoming(thermostat_topic, "{\"cmd\":\"mode\",\"data\":\"auto\",\"hc\":2}"); // hc as number
EMSESP::mqtt_.incoming(thermostat_topic, "{\"cmd\":\"temp\",\"data\":19.5,\"hc\":1}"); // data as number
@@ -929,64 +1269,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(false);
Mqtt::enabled(false);
run_test("general");
AsyncWebServerRequest request;
// GET
request.method(HTTP_GET);
request.url("/api/thermostat/seltemp");
EMSESP::webAPIService.webAPIService_get(&request);
request.url("/api/boiler/syspress");
EMSESP::webAPIService.webAPIService_get(&request);
request.url("/api/system/commands");
EMSESP::webAPIService.webAPIService_get(&request);
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\":11}";
deserializeJson(doc, data3);
json = doc.as<JsonVariant>();
request.url("/api");
EMSESP::webAPIService.webAPIService_post(&request, json);
#endif
}
if (command == "crash") {
shell.printfln(F("Forcing a crash..."));

View File

@@ -38,8 +38,10 @@ namespace emsesp {
// #define EMSESP_DEBUG_DEFAULT "board_profile"
// #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 "crash"
// #define EMSESP_DEBUG_DEFAULT "dv"
class Test {
public:

View File

@@ -1 +1 @@
#define EMSESP_APP_VERSION "3.2.1"
#define EMSESP_APP_VERSION "3.3.0"

View File

@@ -31,23 +31,19 @@ WebAPIService::WebAPIService(AsyncWebServer * server, SecurityManager * security
server->addHandler(&_apiHandler);
}
// HTTP GET
// GET /{device}
// GET /{device}/{name}
// GET /device={device}?cmd={name}?data={value}[?id={hc}]
// GET /{device}/{entity}
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 input object
StaticJsonDocument<EMSESP_JSON_SIZE_SMALL> input_doc;
JsonObject input = input_doc.to<JsonObject>();
parse(request, input);
}
// For POSTS with an optional JSON body
// For HTTP 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,61 @@ 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];
}
}
}
// now go and verify the values
// device check
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)
auto method = request->method();
bool have_data = !value_s.empty();
bool authenticated = false;
void WebAPIService::parse(AsyncWebServerRequest * request, JsonObject & input) {
// check if the user has admin privileges (token is included and authorized)
bool is_admin;
EMSESP::webSettingsService.read([&](WebSettings & settings) {
Authentication authentication = _securityManager->authenticateRequest(request);
authenticated = settings.notoken_api | AuthenticationPredicates::IS_ADMIN(authentication);
is_admin = settings.notoken_api | AuthenticationPredicates::IS_ADMIN(authentication);
});
if ((method != HTTP_GET) || ((method == HTTP_GET) && have_data)) {
if (!authenticated) {
send_message_response(request, 401, "Bad credentials"); // Unauthorized
return;
// output json buffer
PrettyAsyncJsonResponse * response = new PrettyAsyncJsonResponse(false, EMSESP_JSON_SIZE_XXLARGE_DYN);
JsonObject output = response->getRoot();
// call command
uint8_t return_code = Command::process(request->url().c_str(), is_admin, input, output);
if (return_code != CommandRet::OK) {
char error[100];
if (output.size()) {
snprintf(error, sizeof(error), "Call failed with error: %s (%s)", (const char *)output["message"], Command::return_code_string(return_code).c_str());
} else {
snprintf(error, sizeof(error), "Call failed with error code (%s)", Command::return_code_string(return_code).c_str());
}
emsesp::EMSESP::logger().err(error);
} else {
emsesp::EMSESP::logger().debug(F("API command called successfully"));
// if there was no json output from the call, default to the output message 'OK'.
if (!output.size()) {
output["message"] = "OK";
}
}
PrettyAsyncJsonResponse * response = new PrettyAsyncJsonResponse(false, EMSESP_JSON_SIZE_XLARGE_DYN);
JsonObject json = 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);
// 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;
}
// send the json that came back from the command call
// FAIL, OK, NOT_FOUND, ERROR, NOT_ALLOWED = 400 (bad request), 200 (OK), 400 (not found), 400 (bad request), 401 (unauthorized)
int ret_codes[5] = {400, 200, 400, 400, 401};
response->setCode(ret_codes[return_code]);
response->setLength();
request->send(response); // send json response
response->setContentType("application/json");
request->send(response);
#if defined(EMSESP_STANDALONE)
Serial.print(COLOR_YELLOW);
if (json.size() != 0) {
serializeJsonPretty(json, Serial);
Serial.print("web response code: ");
Serial.println(ret_codes[return_code]);
if (output.size()) {
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 * error_message) {
if (error_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"] = error_message;
response->setCode(error_code);
response->setLength();
response->setContentType("application/json");
request->send(response);
}
EMSESP::logger().debug(F("API returns, code: %d, message: %s"), error_code, error_message);
}
/**
* Extract only the path component from the passed URI and normalized it.
* Ex. //one/two////three/// becomes /one/two/three
*/
std::string SUrlParser::path() {
std::string s = "/"; // set up the beginning slash
for (std::string & f : m_folders) {
s += f;
s += "/";
}
s.pop_back(); // deleting last letter, that is slash '/'
return std::string(s);
}
SUrlParser::SUrlParser(const char * uri) {
parse(uri);
}
bool SUrlParser::parse(const char * uri) {
m_folders.clear();
m_keysvalues.clear();
enum Type { begin, folder, param, value };
std::string s;
const char * c = uri;
enum Type t = Type::begin;
std::string last_param;
if (c != NULL || *c != '\0') {
do {
if (*c == '/') {
if (s.length() > 0) {
m_folders.push_back(s);
s.clear();
}
t = Type::folder;
} else if (*c == '?' && (t == Type::folder || t == Type::begin)) {
if (s.length() > 0) {
m_folders.push_back(s);
s.clear();
}
t = Type::param;
} else if (*c == '=' && (t == Type::param || t == Type::begin)) {
m_keysvalues[s] = "";
last_param = s;
s.clear();
t = Type::value;
} else if (*c == '&' && (t == Type::value || t == Type::param || t == Type::begin)) {
if (t == Type::value) {
m_keysvalues[last_param] = s;
} else if ((t == Type::param || t == Type::begin) && (s.length() > 0)) {
m_keysvalues[s] = "";
last_param = s;
}
t = Type::param;
s.clear();
} else if (*c == '\0' && s.length() > 0) {
if (t == Type::value) {
m_keysvalues[last_param] = s;
} else if (t == Type::folder || t == Type::begin) {
m_folders.push_back(s);
} else if (t == Type::param) {
m_keysvalues[s] = "";
last_param = s;
}
s.clear();
} else if (*c == '\0' && s.length() == 0) {
if (t == Type::param && last_param.length() > 0) {
m_keysvalues[last_param] = "";
}
s.clear();
} else {
s += *c;
}
} while (*c++ != '\0');
}
return true;
}
} // namespace emsesp

View File

@@ -31,31 +31,6 @@
namespace emsesp {
typedef std::unordered_map<std::string, std::string> KeyValueMap_t;
typedef std::vector<std::string> Folder_t;
class SUrlParser {
private:
KeyValueMap_t m_keysvalues;
Folder_t m_folders;
public:
SUrlParser(){};
SUrlParser(const char * url);
bool parse(const char * url);
Folder_t & paths() {
return m_folders;
};
KeyValueMap_t & params() {
return m_keysvalues;
};
std::string path();
};
class WebAPIService {
public:
WebAPIService(AsyncWebServer * server, SecurityManager * securityManager);
@@ -67,8 +42,7 @@ class WebAPIService {
SecurityManager * _securityManager;
AsyncCallbackJsonWebHandler _apiHandler; // for POSTs
void parse(AsyncWebServerRequest * request, std::string & device, std::string & cmd, int id, std::string & value);
void send_message_response(AsyncWebServerRequest * request, uint16_t error_code, const char * error_message = nullptr);
void parse(AsyncWebServerRequest * request, JsonObject & input);
};
} // namespace emsesp

View File

@@ -55,20 +55,20 @@ void WebDataService::scan_devices(AsyncWebServerRequest * request) {
}
void WebDataService::all_devices(AsyncWebServerRequest * request) {
AsyncJsonResponse * response = new AsyncJsonResponse(false, EMSESP_JSON_SIZE_LARGE_DYN);
AsyncJsonResponse * response = new AsyncJsonResponse(false, EMSESP_JSON_SIZE_XLARGE_DYN);
JsonObject root = response->getRoot();
JsonArray devices = root.createNestedArray("devices");
for (const auto & emsdevice : EMSESP::emsdevices) {
if (emsdevice) {
JsonObject obj = devices.createNestedObject();
obj["id"] = emsdevice->unique_id();
obj["type"] = emsdevice->device_type_name();
obj["brand"] = emsdevice->brand_to_string();
obj["name"] = emsdevice->name();
obj["deviceid"] = emsdevice->device_id();
obj["productid"] = emsdevice->product_id();
obj["version"] = emsdevice->version();
JsonObject obj = devices.createNestedObject();
obj["i"] = emsdevice->unique_id(); // id
obj["t"] = emsdevice->device_type_name(); // type
obj["b"] = emsdevice->brand_to_string(); // brand
obj["n"] = emsdevice->name(); // name
obj["d"] = emsdevice->device_id(); // deviceid
obj["p"] = emsdevice->product_id(); // productid
obj["v"] = emsdevice->version(); // version
}
}
@@ -77,10 +77,10 @@ void WebDataService::all_devices(AsyncWebServerRequest * request) {
uint8_t i = 1;
for (const auto & sensor : EMSESP::sensor_devices()) {
JsonObject obj = sensors.createNestedObject();
obj["no"] = i++;
obj["id"] = sensor.to_string(true);
obj["temp"] = (float)(sensor.temperature_c) / 10;
obj["offset"] = (float)(sensor.offset()) / 10;
obj["n"] = i++; // no
obj["i"] = sensor.to_string(true); // id
obj["t"] = (float)(sensor.temperature_c) / 10; // temp
obj["o"] = (float)(sensor.offset()) / 10; // offset
}
}
@@ -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 return_code = 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);
return_code = 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];
return_code = 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];
return_code = 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);
return_code = 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 (return_code != CommandRet::OK) {
EMSESP::logger().err(F("Write command failed %s (%s)"), (const char *)output["message"], Command::return_code_string(return_code).c_str());
} else {
EMSESP::logger().debug(F("Write command successful"));
}
response->setCode((return_code == CommandRet::OK) ? 200 : 204);
response->setLength();
request->send(response);
return;
}

View File

@@ -36,7 +36,11 @@ class WebDataService {
public:
WebDataService(AsyncWebServer * server, SecurityManager * securityManager);
// make all functions public so we can test in the debug and standalone mode
#ifndef EMSESP_STANDALONE
private:
#endif
// GET
void all_devices(AsyncWebServerRequest * request);
void scan_devices(AsyncWebServerRequest * request);

View File

@@ -46,7 +46,11 @@ void WebLogService::forbidden(AsyncWebServerRequest * request) {
// start event source service
void WebLogService::start() {
uuid::log::Logger::register_handler(this, uuid::log::Level::INFO); // default is INFO
EMSESP::webSettingsService.read([&](WebSettings & settings) {
maximum_log_messages_ = settings.weblog_buffer;
compact_ = settings.weblog_compact;
uuid::log::Logger::register_handler(this, (uuid::log::Level)settings.weblog_level);
});
}
uuid::log::Level WebLogService::log_level() const {
@@ -54,6 +58,12 @@ uuid::log::Level WebLogService::log_level() const {
}
void WebLogService::log_level(uuid::log::Level level) {
EMSESP::webSettingsService.update(
[&](WebSettings & settings) {
settings.weblog_level = level;
return StateUpdateResult::CHANGED;
},
"local");
uuid::log::Logger::register_handler(this, level);
}
@@ -66,6 +76,26 @@ void WebLogService::maximum_log_messages(size_t count) {
while (log_messages_.size() > maximum_log_messages_) {
log_messages_.pop_front();
}
EMSESP::webSettingsService.update(
[&](WebSettings & settings) {
settings.weblog_buffer = count;
return StateUpdateResult::CHANGED;
},
"local");
}
bool WebLogService::compact() {
return compact_;
}
void WebLogService::compact(bool compact) {
compact_ = compact;
EMSESP::webSettingsService.update(
[&](WebSettings & settings) {
settings.weblog_compact = compact;
return StateUpdateResult::CHANGED;
},
"local");
}
WebLogService::QueuedLogMessage::QueuedLogMessage(unsigned long id, std::shared_ptr<uuid::log::Message> && content)
@@ -83,8 +113,12 @@ void WebLogService::operator<<(std::shared_ptr<uuid::log::Message> message) {
EMSESP::esp8266React.getNTPSettingsService()->read([&](NTPSettings & settings) {
if (!settings.enabled || (time(nullptr) < 1500000000L)) {
time_offset_ = 0;
} else if (!time_offset_) {
time_offset_ = time(nullptr) - uuid::get_uptime_sec();
} else {
uint32_t offset = time(nullptr) - uuid::get_uptime_sec();
// if NTP is more that 1 sec apart, correct setting
if (time_offset_ < offset - 1 || time_offset_ > offset + 1) {
time_offset_ = offset;
}
}
});
}
@@ -94,26 +128,25 @@ void WebLogService::loop() {
return;
}
// put a small delay in
const uint64_t now = uuid::get_uptime_ms();
if (now < last_transmit_ || now - last_transmit_ < REFRESH_SYNC) {
return;
}
// see if we've advanced
if (log_messages_.back().id_ <= log_message_id_tail_) {
return;
}
// put a small delay in
if (uuid::get_uptime_ms() - last_transmit_ < REFRESH_SYNC) {
return;
}
last_transmit_ = uuid::get_uptime_ms();
// flush
for (auto it = log_messages_.begin(); it != log_messages_.end(); it++) {
if (it->id_ > log_message_id_tail_) {
transmit(*it);
for (const auto & message : log_messages_) {
if (message.id_ > log_message_id_tail_) {
log_message_id_tail_ = message.id_;
transmit(message);
return;
}
}
log_message_id_tail_ = log_messages_.back().id_;
last_transmit_ = uuid::get_uptime_ms();
}
// convert time to real offset
@@ -123,14 +156,14 @@ char * WebLogService::messagetime(char * out, const uint64_t t) {
} else {
time_t t1 = time_offset_ + t / 1000ULL;
strftime(out, 25, "%F %T", localtime(&t1));
snprintf_P(out, 25, "%s.%03d", out, (uint16_t)(t % 1000));
snprintf(out, 25, "%s.%03d", out, (uint16_t)(t % 1000));
}
return out;
}
// send to web eventsource
void WebLogService::transmit(const QueuedLogMessage & message) {
DynamicJsonDocument jsonDocument = DynamicJsonDocument(EMSESP_JSON_SIZE_SMALL);
DynamicJsonDocument jsonDocument = DynamicJsonDocument(EMSESP_JSON_SIZE_MEDIUM);
JsonObject logEvent = jsonDocument.to<JsonObject>();
char time_string[25];
@@ -144,33 +177,30 @@ void WebLogService::transmit(const QueuedLogMessage & message) {
char * buffer = new char[len + 1];
if (buffer) {
serializeJson(jsonDocument, buffer, len + 1);
events_.send(buffer, "message", millis());
events_.send(buffer, "message", message.id_);
}
delete[] buffer;
}
// send the complete log buffer to the API, filtering on log level
// send the complete log buffer to the API, not filtering on log level
void WebLogService::fetchLog(AsyncWebServerRequest * request) {
MsgpackAsyncJsonResponse * response = new MsgpackAsyncJsonResponse(false, EMSESP_JSON_SIZE_XXLARGE_DYN); // 8kb buffer
MsgpackAsyncJsonResponse * response = new MsgpackAsyncJsonResponse(false, EMSESP_JSON_SIZE_XXLARGE_DYN); // 16kb buffer
JsonObject root = response->getRoot();
JsonArray log = root.createNestedArray("events");
for (const auto & message : log_messages_) {
if (message.content_->level <= log_level()) {
JsonObject logEvent = log.createNestedObject();
char time_string[25];
logEvent["t"] = messagetime(time_string, message.content_->uptime_ms);
logEvent["l"] = message.content_->level;
logEvent["i"] = message.id_;
logEvent["n"] = message.content_->name;
logEvent["m"] = message.content_->text;
}
}
JsonArray log = root.createNestedArray("events");
log_message_id_tail_ = log_messages_.back().id_;
last_transmit_ = uuid::get_uptime_ms();
for (const auto & message : log_messages_) {
JsonObject logEvent = log.createNestedObject();
char time_string[25];
logEvent["t"] = messagetime(time_string, message.content_->uptime_ms);
logEvent["l"] = message.content_->level;
logEvent["i"] = message.id_;
logEvent["n"] = message.content_->name;
logEvent["m"] = message.content_->text;
}
log_message_id_tail_ = log_messages_.back().id_;
response->setLength();
request->send(response);
}
@@ -189,6 +219,9 @@ void WebLogService::setValues(AsyncWebServerRequest * request, JsonVariant & jso
uint8_t max_messages = body["max_messages"];
maximum_log_messages(max_messages);
bool comp = body["compact"];
compact(comp);
request->send(200); // OK
}
@@ -198,6 +231,7 @@ void WebLogService::getValues(AsyncWebServerRequest * request) {
JsonObject root = response->getRoot();
root["level"] = log_level();
root["max_messages"] = maximum_log_messages();
root["compact"] = compact();
response->setLength();
request->send(response);
}

View File

@@ -35,7 +35,7 @@ namespace emsesp {
class WebLogService : public uuid::log::Handler {
public:
static constexpr size_t MAX_LOG_MESSAGES = 50;
static constexpr size_t REFRESH_SYNC = 200;
static constexpr size_t REFRESH_SYNC = 50;
WebLogService(AsyncWebServer * server, SecurityManager * securityManager);
@@ -44,6 +44,8 @@ class WebLogService : public uuid::log::Handler {
void log_level(uuid::log::Level level);
size_t maximum_log_messages() const;
void maximum_log_messages(size_t count);
bool compact();
void compact(bool compact);
void loop();
virtual void operator<<(std::shared_ptr<uuid::log::Message> message);
@@ -77,6 +79,7 @@ class WebLogService : public uuid::log::Handler {
unsigned long log_message_id_tail_ = 0; // last event shown on the screen after fetch
std::list<QueuedLogMessage> log_messages_; // Queued log messages, in the order they were received
time_t time_offset_ = 0;
bool compact_ = true;
};
} // namespace emsesp

View File

@@ -38,7 +38,6 @@ WebSettingsService::WebSettingsService(AsyncWebServer * server, FS * fs, Securit
void WebSettings::read(WebSettings & settings, JsonObject & root) {
root["tx_mode"] = settings.tx_mode;
root["tx_delay"] = settings.tx_delay;
root["ems_bus_id"] = settings.ems_bus_id;
root["syslog_enabled"] = settings.syslog_enabled;
root["syslog_level"] = settings.syslog_level;
@@ -51,6 +50,7 @@ void WebSettings::read(WebSettings & settings, JsonObject & root) {
root["shower_alert"] = settings.shower_alert;
root["rx_gpio"] = settings.rx_gpio;
root["tx_gpio"] = settings.tx_gpio;
root["phy_type"] = settings.phy_type;
root["dallas_gpio"] = settings.dallas_gpio;
root["dallas_parasite"] = settings.dallas_parasite;
root["led_gpio"] = settings.led_gpio;
@@ -64,14 +64,17 @@ void WebSettings::read(WebSettings & settings, JsonObject & root) {
root["dallas_format"] = settings.dallas_format;
root["bool_format"] = settings.bool_format;
root["enum_format"] = settings.enum_format;
root["weblog_level"] = settings.weblog_level;
root["weblog_buffer"] = settings.weblog_buffer;
root["weblog_compact"] = settings.weblog_compact;
for (uint8_t i = 0; i < NUM_SENSOR_NAMES; i++) {
for (uint8_t i = 0; i < MAX_NUM_SENSOR_NAMES; i++) {
char buf[20];
snprintf_P(buf, sizeof(buf), PSTR("sensor_id%d"), i);
snprintf(buf, sizeof(buf), "sensor_id%d", i);
root[buf] = settings.sensor[i].id;
snprintf_P(buf, sizeof(buf), PSTR("sensor_name%d"), i);
snprintf(buf, sizeof(buf), "sensor_name%d", i);
root[buf] = settings.sensor[i].name;
snprintf_P(buf, sizeof(buf), PSTR("sensor_offset%d"), i);
snprintf(buf, sizeof(buf), "sensor_offset%d", i);
root[buf] = settings.sensor[i].offset;
}
}
@@ -79,7 +82,7 @@ void WebSettings::read(WebSettings & settings, JsonObject & root) {
// call on initialization and also when settings are updated via web or console
StateUpdateResult WebSettings::update(JsonObject & root, WebSettings & settings) {
// load default GPIO configuration based on board profile
std::vector<uint8_t> data; // led, dallas, rx, tx, button
std::vector<uint8_t> data; // led, dallas, rx, tx, button, phy_type
String old_board_profile = settings.board_profile;
settings.board_profile = root["board_profile"] | EMSESP_DEFAULT_BOARD_PROFILE;
@@ -92,6 +95,7 @@ StateUpdateResult WebSettings::update(JsonObject & root, WebSettings & settings)
uint8_t default_rx_gpio = data[2];
uint8_t default_tx_gpio = data[3];
uint8_t default_pbutton_gpio = data[4];
uint8_t default_phy_type = data[5];
if (old_board_profile != settings.board_profile) {
EMSESP::logger().info(F("EMS-ESP version %s"), EMSESP_APP_VERSION);
@@ -111,9 +115,6 @@ StateUpdateResult WebSettings::update(JsonObject & root, WebSettings & settings)
prev = settings.tx_mode;
settings.tx_mode = root["tx_mode"] | EMSESP_DEFAULT_TX_MODE;
check_flag(prev, settings.tx_mode, ChangeFlags::UART);
prev = settings.tx_delay;
settings.tx_delay = root["tx_delay"] | EMSESP_DEFAULT_TX_DELAY;
check_flag(prev, settings.tx_delay, ChangeFlags::UART);
prev = settings.rx_gpio;
settings.rx_gpio = root["rx_gpio"] | default_rx_gpio;
check_flag(prev, settings.rx_gpio, ChangeFlags::UART);
@@ -146,9 +147,6 @@ StateUpdateResult WebSettings::update(JsonObject & root, WebSettings & settings)
settings.syslog_port = root["syslog_port"] | EMSESP_DEFAULT_SYSLOG_PORT;
check_flag(prev, settings.syslog_port, ChangeFlags::SYSLOG);
settings.trace_raw = root["trace_raw"] | EMSESP_DEFAULT_TRACELOG_RAW;
EMSESP::trace_raw(settings.trace_raw);
// adc
prev = settings.analog_enabled;
settings.analog_enabled = root["analog_enabled"] | EMSESP_DEFAULT_ANALOG_ENABLED;
@@ -183,13 +181,19 @@ StateUpdateResult WebSettings::update(JsonObject & root, WebSettings & settings)
settings.hide_led = root["hide_led"] | EMSESP_DEFAULT_HIDE_LED;
check_flag(prev, settings.hide_led, ChangeFlags::LED);
//
// next ones are settings that don't need any follow-up actions
//
// these need reboots to be applied
settings.ems_bus_id = root["ems_bus_id"] | EMSESP_DEFAULT_EMS_BUS_ID;
settings.master_thermostat = root["master_thermostat"] | EMSESP_DEFAULT_MASTER_THERMOSTAT;
settings.low_clock = root["low_clock"] | false;
;
settings.phy_type = root["phy_type"] | default_phy_type; // use whatever came from the board profile
settings.trace_raw = root["trace_raw"] | EMSESP_DEFAULT_TRACELOG_RAW;
EMSESP::trace_raw(settings.trace_raw);
// doesn't need any follow-up actions
settings.notoken_api = root["notoken_api"] | EMSESP_DEFAULT_NOTOKEN_API;
settings.solar_maxflow = root["solar_maxflow"] | EMSESP_DEFAULT_SOLAR_MAXFLOW;
@@ -202,13 +206,17 @@ StateUpdateResult WebSettings::update(JsonObject & root, WebSettings & settings)
settings.enum_format = root["enum_format"] | EMSESP_DEFAULT_ENUM_FORMAT;
EMSESP::enum_format(settings.enum_format);
for (uint8_t i = 0; i < NUM_SENSOR_NAMES; i++) {
settings.weblog_level = root["weblog_level"] | EMSESP_DEFAULT_WEBLOG_LEVEL;
settings.weblog_buffer = root["weblog_buffer"] | EMSESP_DEFAULT_WEBLOG_BUFFER;
settings.weblog_compact = root["weblog_compact"] | EMSESP_DEFAULT_WEBLOG_COMPACT;
for (uint8_t i = 0; i < MAX_NUM_SENSOR_NAMES; i++) {
char buf[20];
snprintf_P(buf, sizeof(buf), PSTR("sensor_id%d"), i);
snprintf(buf, sizeof(buf), "sensor_id%d", i);
settings.sensor[i].id = root[buf] | EMSESP_DEFAULT_SENSOR_NAME;
snprintf_P(buf, sizeof(buf), PSTR("sensor_name%d"), i);
snprintf(buf, sizeof(buf), "sensor_name%d", i);
settings.sensor[i].name = root[buf] | EMSESP_DEFAULT_SENSOR_NAME;
snprintf_P(buf, sizeof(buf), PSTR("sensor_offset%d"), i);
snprintf(buf, sizeof(buf), "sensor_offset%d", i);
settings.sensor[i].offset = root[buf] | 0;
}
@@ -272,8 +280,8 @@ void WebSettingsService::board_profile(AsyncWebServerRequest * request, JsonVari
root["rx_gpio"] = data[2];
root["tx_gpio"] = data[3];
root["pbutton_gpio"] = data[4];
root["phy_type"] = data[5];
} else {
delete response;
AsyncWebServerResponse * response = request->beginResponse(200);
request->send(response);
return;

View File

@@ -28,7 +28,7 @@
#define EMSESP_SETTINGS_SERVICE_PATH "/rest/emsespSettings"
#define EMSESP_BOARD_PROFILE_SERVICE_PATH "/rest/boardProfile"
#define NUM_SENSOR_NAMES 10
#define MAX_NUM_SENSOR_NAMES 20
namespace emsesp {
@@ -38,7 +38,6 @@ enum { ENUM_FORMAT_TEXT = 1, ENUM_FORMAT_NUMBER };
class WebSettings {
public:
uint8_t tx_mode;
uint8_t tx_delay;
uint8_t ems_bus_id;
uint8_t master_thermostat;
bool shower_timer;
@@ -61,15 +60,19 @@ class WebSettings {
uint8_t pbutton_gpio;
uint8_t solar_maxflow;
String board_profile;
uint8_t phy_type;
uint8_t dallas_format;
uint8_t bool_format;
uint8_t enum_format;
int8_t weblog_level;
uint8_t weblog_buffer;
bool weblog_compact;
struct {
String id;
String name;
int16_t offset;
} sensor[NUM_SENSOR_NAMES];
} sensor[MAX_NUM_SENSOR_NAMES];
static void read(WebSettings & settings, JsonObject & root);
static StateUpdateResult update(JsonObject & root, WebSettings & settings);

View File

@@ -35,7 +35,7 @@ void WebStatusService::WiFiEvent(WiFiEvent_t event, WiFiEventInfo_t info) {
switch (event) {
case SYSTEM_EVENT_STA_DISCONNECTED:
EMSESP::logger().info(F("WiFi Disconnected. Reason code=%d"), info.disconnected.reason);
EMSESP::system_.network_connected(false);
WiFi.disconnect(true);
break;
case SYSTEM_EVENT_STA_GOT_IP:
@@ -44,16 +44,24 @@ void WebStatusService::WiFiEvent(WiFiEvent_t event, WiFiEventInfo_t info) {
#endif
EMSESP::esp8266React.getNetworkSettingsService()->read([&](NetworkSettings & networkSettings) {
if (!networkSettings.enableIPv6) {
EMSESP::system_.network_connected(true);
EMSESP::system_.send_heartbeat();
EMSESP::system_.syslog_start();
}
});
mDNS_start();
break;
case SYSTEM_EVENT_ETH_START:
EMSESP::logger().info(F("Ethernet initialized"));
ETH.setHostname(EMSESP::system_.hostname().c_str());
// configure for static IP
EMSESP::esp8266React.getNetworkSettingsService()->read([&](NetworkSettings & networkSettings) {
if (networkSettings.staticIPConfig) {
ETH.config(networkSettings.localIP, networkSettings.gatewayIP, networkSettings.subnetMask, networkSettings.dnsIP1, networkSettings.dnsIP2);
}
});
break;
case SYSTEM_EVENT_ETH_GOT_IP:
@@ -64,25 +72,23 @@ void WebStatusService::WiFiEvent(WiFiEvent_t event, WiFiEventInfo_t info) {
#endif
EMSESP::esp8266React.getNetworkSettingsService()->read([&](NetworkSettings & networkSettings) {
if (!networkSettings.enableIPv6) {
EMSESP::system_.network_connected(true);
EMSESP::system_.send_heartbeat();
EMSESP::system_.syslog_start();
}
});
EMSESP::system_.ethernet_connected(true);
mDNS_start();
}
break;
case SYSTEM_EVENT_ETH_DISCONNECTED:
EMSESP::logger().info(F("Ethernet Disconnected"));
EMSESP::system_.ethernet_connected(false);
EMSESP::system_.network_connected(false);
break;
case SYSTEM_EVENT_ETH_STOP:
EMSESP::logger().info(F("Ethernet Stopped"));
EMSESP::system_.ethernet_connected(false);
EMSESP::system_.network_connected(false);
break;
#ifndef EMSESP_STANDALONE
@@ -108,9 +114,9 @@ void WebStatusService::WiFiEvent(WiFiEvent_t event, WiFiEventInfo_t info) {
} else {
EMSESP::logger().info(F("WiFi Connected with IP=%s, hostname=%s"), WiFi.localIPv6().toString().c_str(), WiFi.getHostname());
}
EMSESP::system_.network_connected(true);
EMSESP::system_.send_heartbeat();
EMSESP::system_.syslog_start();
mDNS_start();
break;
#endif
@@ -133,4 +139,24 @@ void WebStatusService::webStatusService(AsyncWebServerRequest * request) {
request->send(response);
}
// start the multicast UDP service so EMS-ESP is discoverable via .local
void WebStatusService::mDNS_start() {
#ifndef EMSESP_STANDALONE
if (!MDNS.begin(EMSESP::system_.hostname().c_str())) {
EMSESP::logger().warning(F("Failed to start mDNS responder service"));
return;
}
std::string address_s = EMSESP::system_.hostname() + ".local";
MDNS.addService("http", "tcp", 80); // add our web server and rest API
MDNS.addService("telnet", "tcp", 23); // add our telnet console
MDNS.addServiceTxt("http", "tcp", "version", EMSESP_APP_VERSION);
MDNS.addServiceTxt("http", "tcp", "address", address_s.c_str());
#endif
EMSESP::logger().info(F("mDNS responder service started"));
}
} // namespace emsesp

View File

@@ -25,6 +25,8 @@
#include <SecurityManager.h>
#include <AsyncMqttClient.h>
#include <ESPmDNS.h>
#define EMSESP_STATUS_SERVICE_PATH "/rest/emsespStatus"
namespace emsesp {
@@ -36,6 +38,7 @@ class WebStatusService {
private:
void webStatusService(AsyncWebServerRequest * request);
void WiFiEvent(WiFiEvent_t event, WiFiEventInfo_t info);
void mDNS_start();
};
} // namespace emsesp