diff --git a/interface/src/project/EMSESPHelp.tsx b/interface/src/project/EMSESPHelp.tsx index 8709c6f7c..bec31b637 100644 --- a/interface/src/project/EMSESPHelp.tsx +++ b/interface/src/project/EMSESPHelp.tsx @@ -17,7 +17,7 @@ class EMSESPHelp extends Component {

- Check for news and updates on the {'Wiki'}. + Check for news and updates on the {'Documentation site'}. For live community chat go to {'Gitter'}. diff --git a/interface/src/project/EMSESPSettingsController.tsx b/interface/src/project/EMSESPSettingsController.tsx index 332447dfc..f8be4938a 100644 --- a/interface/src/project/EMSESPSettingsController.tsx +++ b/interface/src/project/EMSESPSettingsController.tsx @@ -48,7 +48,7 @@ function EMSESPSettingsControllerForm(props: EMSESPSettingsControllerFormProps) - Customize EMS-ESP by modifying the default settings here. Refer to the {'Wiki'} for descriptions of each setting. + Customize EMS-ESP by modifying the default settings here. Refer to the {'Documentation'} for descriptions of each setting.

@@ -181,6 +181,20 @@ function EMSESPSettingsControllerForm(props: EMSESPSettingsControllerFormProps) label="Shower Alert" />

+ + API + + + } + label="Enable WEB API (for write commands)" + /> +

Syslog diff --git a/interface/src/project/EMSESPtypes.ts b/interface/src/project/EMSESPtypes.ts index 2f59596b1..861ff81fa 100644 --- a/interface/src/project/EMSESPtypes.ts +++ b/interface/src/project/EMSESPtypes.ts @@ -13,6 +13,7 @@ export interface EMSESPSettings { dallas_parasite: boolean; led_gpio: number; hide_led: boolean; + api_enabled: boolean; } export enum busConnectionStatus { diff --git a/lib/framework/MqttSettingsService.cpp b/lib/framework/MqttSettingsService.cpp index e5a4928a1..53348bf8e 100644 --- a/lib/framework/MqttSettingsService.cpp +++ b/lib/framework/MqttSettingsService.cpp @@ -4,9 +4,9 @@ namespace emsesp { class EMSESP { public: - static System system_; - static Mqtt mqtt_; - static Sensors sensors_; + static System system_; + static Mqtt mqtt_; + static Sensor sensor_; }; } // namespace emsesp @@ -169,7 +169,7 @@ void MqttSettingsService::configureMqtt() { _mqttClient.connect(); } - emsesp::EMSESP::sensors_.reload(); + emsesp::EMSESP::sensor_.reload(); } void MqttSettings::read(MqttSettings & settings, JsonObject & root) { diff --git a/lib/framework/MqttSettingsService.h b/lib/framework/MqttSettingsService.h index f138989bb..83e8f9d5a 100644 --- a/lib/framework/MqttSettingsService.h +++ b/lib/framework/MqttSettingsService.h @@ -10,7 +10,7 @@ #include "../../src/system.h" #include "../../src/mqtt.h" -#include "../../src/sensors.h" +#include "../../src/sensor.h" #define MQTT_RECONNECTION_DELAY 1000 diff --git a/lib_standalone/ESP8266React.h b/lib_standalone/ESP8266React.h index 7183c4a36..36e2e6851 100644 --- a/lib_standalone/ESP8266React.h +++ b/lib_standalone/ESP8266React.h @@ -23,6 +23,7 @@ class DummySettings { bool shower_timer = false; bool shower_alert = false; bool hide_led = false; + bool api_enabled = true; uint16_t publish_time = 10; // seconds uint8_t mqtt_format = 3; // 1=single, 2=nested, 3=ha, 4=custom uint8_t mqtt_qos = 0; diff --git a/src/EMSESPAPIService.cpp b/src/EMSESPAPIService.cpp index c1df17702..ec8ebcbdd 100644 --- a/src/EMSESPAPIService.cpp +++ b/src/EMSESPAPIService.cpp @@ -30,9 +30,13 @@ EMSESPAPIService::EMSESPAPIService(AsyncWebServer * server) { // http://ems-esp/api?device=boiler&cmd=wwtemp&data=20&id=1 void EMSESPAPIService::emsespAPIService(AsyncWebServerRequest * request) { + // see if the API is enabled + bool api_enabled; + EMSESP::emsespSettingsService.read([&](EMSESPSettings & settings) { api_enabled = settings.api_enabled; }); + // must have device and cmd parameters if ((!request->hasParam(F_(device))) || (!request->hasParam(F_(cmd)))) { - request->send(400, "text/plain", F("invalid syntax")); + request->send(400, "text/plain", F("Invalid syntax")); return; } @@ -40,26 +44,16 @@ void EMSESPAPIService::emsespAPIService(AsyncWebServerRequest * request) { String device = request->getParam(F_(device))->value(); uint8_t device_type = EMSdevice::device_name_2_device_type(device.c_str()); if (device_type == emsesp::EMSdevice::DeviceType::UNKNOWN) { - request->send(400, "text/plain", F("invalid device")); + request->send(400, "text/plain", F("Invalid device")); return; } // get cmd, we know we have one String cmd = request->getParam(F_(cmd))->value(); - // first test for special service commands - // e.g. http://ems-esp/api?device=system&cmd=info - if (device.equals("system")) { - if (cmd.equals("info")) { - request->send(200, "text/plain", System::export_settings()); - EMSESP::logger().info(F("Sent settings json to web UI")); - return; - } - } - // look up command in our list if (!Command::find(device_type, cmd.c_str())) { - request->send(400, "text/plain", F("invalid cmd")); + request->send(400, "text/plain", F("Invalid cmd")); return; } @@ -73,31 +67,54 @@ void EMSESPAPIService::emsespAPIService(AsyncWebServerRequest * request) { id = request->getParam(F_(id))->value(); } + DynamicJsonDocument doc(EMSESP_MAX_JSON_SIZE_LARGE); + JsonObject output = doc.to(); + bool ok = false; + // execute the command - bool ok = false; if (data.isEmpty()) { - ok = Command::call_command(device_type, cmd.c_str(), nullptr, -1); // command only - } else if (id.isEmpty()) { - ok = Command::call_command(device_type, cmd.c_str(), data.c_str(), -1); // only ID + ok = Command::call(device_type, cmd.c_str(), nullptr, -1, output); // command only } else { - ok = Command::call_command(device_type, cmd.c_str(), data.c_str(), id.toInt()); // has cmd, data and id + if (api_enabled) { + // we only allow commands with parameters if the API is enabled + if (id.isEmpty()) { + ok = Command::call(device_type, cmd.c_str(), data.c_str(), -1, output); // only ID + } else { + ok = Command::call(device_type, cmd.c_str(), data.c_str(), id.toInt(), output); // has cmd, data and id + } + } else { + request->send(401, "text/plain", F("Unauthorized")); + return; + } } // debug #if defined(EMSESP_DEBUG) - std::string output(200, '\0'); - snprintf_P(&output[0], - output.capacity() + 1, + std::string debug(200, '\0'); + snprintf_P(&debug[0], + debug.capacity() + 1, PSTR("API: device=%s cmd=%s data=%s id=%s [%s]"), device.c_str(), cmd.c_str(), data.c_str(), id.c_str(), - ok ? F("OK") : F("Failed")); - EMSESP::logger().info(output.c_str()); + ok ? F("OK") : F("Invalid")); + EMSESP::logger().info(debug.c_str()); + if (output.size()) { + char buffer2[EMSESP_MAX_JSON_SIZE_LARGE]; + serializeJson(doc, buffer2); + EMSESP::logger().info("output (max 255 chars): %s", buffer2); + } #endif - request->send(200, "text/plain", ok ? F("OK") : F("Failed")); + // if we have returned data in JSON format, send this to the WEB + if (output.size()) { + char buffer[EMSESP_MAX_JSON_SIZE_LARGE]; + serializeJson(doc, buffer); + request->send(200, "text/plain", buffer); + } else { + request->send(200, "text/plain", ok ? F("OK") : F("Invalid")); + } } } // namespace emsesp \ No newline at end of file diff --git a/src/EMSESPAPIService.h b/src/EMSESPAPIService.h index 90d45c11d..1ecbc4490 100644 --- a/src/EMSESPAPIService.h +++ b/src/EMSESPAPIService.h @@ -33,7 +33,6 @@ class EMSESPAPIService { private: void emsespAPIService(AsyncWebServerRequest * request); - }; } // namespace emsesp diff --git a/src/EMSESPSettingsService.cpp b/src/EMSESPSettingsService.cpp index cf4cbf018..7541fdb32 100644 --- a/src/EMSESPSettingsService.cpp +++ b/src/EMSESPSettingsService.cpp @@ -42,6 +42,7 @@ void EMSESPSettings::read(EMSESPSettings & settings, JsonObject & root) { root["dallas_parasite"] = settings.dallas_parasite; root["led_gpio"] = settings.led_gpio; root["hide_led"] = settings.hide_led; + root["api_enabled"] = settings.api_enabled; } StateUpdateResult EMSESPSettings::update(JsonObject & root, EMSESPSettings & settings) { @@ -59,6 +60,7 @@ StateUpdateResult EMSESPSettings::update(JsonObject & root, EMSESPSettings & set settings.dallas_parasite = root["dallas_parasite"] | EMSESP_DEFAULT_DALLAS_PARASITE; settings.led_gpio = root["led_gpio"] | EMSESP_DEFAULT_LED_GPIO; settings.hide_led = root["hide_led"] | EMSESP_DEFAULT_HIDE_LED; + settings.api_enabled = root["api_enabled"] | EMSESP_DEFAULT_API_ENABLED; return StateUpdateResult::CHANGED; } @@ -70,8 +72,8 @@ void EMSESPSettingsService::onUpdate() { // EMSESP::system_.syslog_init(); // changing SysLog will require a restart EMSESP::init_tx(); System::set_led(); - Sensors sensors_; // Dallas sensors - sensors_.start(); + Sensor sensor_; // Dallas sensors + sensor_.start(); } void EMSESPSettingsService::begin() { diff --git a/src/EMSESPSettingsService.h b/src/EMSESPSettingsService.h index a23d344ca..7be982e6c 100644 --- a/src/EMSESPSettingsService.h +++ b/src/EMSESPSettingsService.h @@ -35,6 +35,7 @@ #define EMSESP_DEFAULT_SHOWER_ALERT false #define EMSESP_DEFAULT_HIDE_LED false #define EMSESP_DEFAULT_DALLAS_PARASITE false +#define EMSESP_DEFAULT_API_ENABLED true // Default GPIO PIN definitions #if defined(ESP8266) @@ -75,6 +76,7 @@ class EMSESPSettings { bool dallas_parasite; uint8_t led_gpio; bool hide_led; + bool api_enabled; static void read(EMSESPSettings & settings, JsonObject & root); static StateUpdateResult update(JsonObject & root, EMSESPSettings & settings); diff --git a/src/command.cpp b/src/command.cpp index 7694caf29..cbc3dad94 100644 --- a/src/command.cpp +++ b/src/command.cpp @@ -29,12 +29,15 @@ std::vector Command::cmdfunctions_; // calls a command, context is the device_type // id may be used to represent a heating circuit for example // returns false if error or not found -bool Command::call_command(const uint8_t device_type, const char * cmd, const char * value, const int8_t id) { +bool Command::call(const uint8_t device_type, const char * cmd, const char * value, const int8_t id, JsonObject & output) { #ifdef EMSESP_DEBUG - if (id == -1) { - LOG_DEBUG(F("[DEBUG] Calling command %s, value %s, id is default"), cmd, value); + std::string dname = EMSdevice::device_type_2_device_name(device_type); + if (value == nullptr) { + LOG_DEBUG(F("[DEBUG] Calling command %s in %s"), cmd, dname.c_str()); + } else if (id == -1) { + LOG_DEBUG(F("[DEBUG] Calling command %s, value %s, id is default in %s"), cmd, value, dname.c_str()); } else { - LOG_DEBUG(F("[DEBUG] Calling command %s, value %s, id is %d"), cmd, value, id); + LOG_DEBUG(F("[DEBUG] Calling command %s, value %s, id is %d in %s"), cmd, value, id, dname.c_str()); } #endif @@ -42,23 +45,37 @@ bool Command::call_command(const uint8_t device_type, const char * cmd, const ch for (const auto & cf : cmdfunctions_) { if (cf.device_type_ == device_type) { const char * cf_cmd = uuid::read_flash_string(cf.cmd_).c_str(); + // find a matching command and call it if (strcmp(cf_cmd, cmd) == 0) { - return ((cf.cmdfunction_)(value, id)); // call function, data needs to be a string and can be null + if (cf.cmdfunction_json_) { + // check if json object is empty, if so quit + if (output.isNull()) { + LOG_WARNING(F("Ignore call for command %s in %s because no json"), cmd, EMSdevice::device_type_2_device_name(device_type).c_str()); + } + return ((cf.cmdfunction_json_)(value, id, output)); + } else { + return ((cf.cmdfunction_)(value, id)); + } } } } } - return false; + return false; // command not found } -// add a command to the list -void Command::add_command(const uint8_t device_type, const uint8_t device_id, const __FlashStringHelper * cmd, cmdfunction_p cb) { - cmdfunctions_.emplace_back(device_type, device_id, cmd, cb); +// add a command to the list, which does not return json +void Command::add(const uint8_t device_type, const uint8_t device_id, const __FlashStringHelper * cmd, cmdfunction_p cb) { + cmdfunctions_.emplace_back(device_type, cmd, cb, nullptr); // see if we need to subscribe Mqtt::register_command(device_type, device_id, cmd, cb); } +// add a command to the list, which does return json object as output +void Command::add_with_json(const uint8_t device_type, const __FlashStringHelper * cmd, cmdfunction_json_p cb) { + cmdfunctions_.emplace_back(device_type, cmd, nullptr, cb); +} + // see if a command exists for that device type bool Command::find(const uint8_t device_type, const char * cmd) { for (const auto & cf : cmdfunctions_) { @@ -70,7 +87,7 @@ bool Command::find(const uint8_t device_type, const char * cmd) { } // output list of all commands to console for a specific DeviceType -void Command::show_commands(uuid::console::Shell & shell, uint8_t device_type) { +void Command::show(uuid::console::Shell & shell, uint8_t device_type) { for (const auto & cf : Command::commands()) { if (cf.device_type_ == device_type) { shell.printf("%s ", uuid::read_flash_string(cf.cmd_).c_str()); @@ -81,9 +98,9 @@ void Command::show_commands(uuid::console::Shell & shell, uint8_t device_type) { // determines the device_type from the shell context we're in uint8_t Command::context_2_device_type(unsigned int context) { - if (context == ShellContext::MAIN) { - return EMSdevice::DeviceType::SERVICEKEY; - } + // if (context == ShellContext::MAIN) { + // return EMSdevice::DeviceType::SERVICEKEY; + // } if (context == ShellContext::BOILER) { return EMSdevice::DeviceType::BOILER; } @@ -99,34 +116,36 @@ uint8_t Command::context_2_device_type(unsigned int context) { if (context == ShellContext::THERMOSTAT) { return EMSdevice::DeviceType::THERMOSTAT; } + if (context == ShellContext::SENSOR) { + return EMSdevice::DeviceType::SENSOR; + } return EMSdevice::DeviceType::UNKNOWN; // unknown type } // show command per current context -void Command::show_commands(uuid::console::Shell & shell) { - show_commands(shell, context_2_device_type(shell.context())); +void Command::show(uuid::console::Shell & shell) { + show(shell, context_2_device_type(shell.context())); } - // output list of all commands to console -void Command::show_all_commands(uuid::console::Shell & shell) { +void Command::show_all(uuid::console::Shell & shell) { // show system first shell.printf("%s: ", EMSdevice::device_type_2_device_name(EMSdevice::DeviceType::SERVICEKEY).c_str()); - show_commands(shell, EMSdevice::DeviceType::SERVICEKEY); + show(shell, EMSdevice::DeviceType::SERVICEKEY); // do this in the order of factory classes to keep a consistent order when displaying for (const auto & device_class : EMSFactory::device_handlers()) { for (const auto & emsdevice : EMSESP::emsdevices) { if ((emsdevice) && (emsdevice->device_type() == device_class.first)) { shell.printf("%s: ", EMSdevice::device_type_2_device_name(device_class.first).c_str()); - show_commands(shell, device_class.first); + show(shell, device_class.first); } } } } -// given a context, automatically add the commands to the console +// Add the console 'call' command to the given context void Command::add_context_commands(unsigned int context) { // if we're adding commands for a thermostat or mixing, then include an additional optional paramter called heating circuit flash_string_vector params; @@ -147,7 +166,7 @@ void Command::add_context_commands(unsigned int context) { if (arguments.empty()) { // list options shell.print("Available commands: "); - show_commands(shell); + show(shell); shell.println(); return; } @@ -155,16 +174,35 @@ void Command::add_context_commands(unsigned int context) { // determine the device_type from the shell context uint8_t device_type = context_2_device_type(shell.context()); + // validate the command const char * cmd = arguments[0].c_str(); + if (!find(device_type, cmd)) { + shell.print(F("Unknown command. Available commands are: ")); + show(shell); + shell.println(); + return; + } + + DynamicJsonDocument doc(EMSESP_MAX_JSON_SIZE_LARGE); + JsonObject output = doc.to(); + + bool ok = false; if (arguments.size() == 1) { - // no value specified - (void)Command::call_command(device_type, cmd, nullptr, -1); + // no value specified, just the cmd + ok = Command::call(device_type, cmd, nullptr, -1, output); } else if (arguments.size() == 2) { // has a value but no id - (void)Command::call_command(device_type, cmd, arguments.back().c_str(), -1); + ok = Command::call(device_type, cmd, arguments.back().c_str(), -1, output); } else { // use value, which could be an id or hc - (void)Command::call_command(device_type, cmd, arguments[1].c_str(), atoi(arguments[2].c_str())); + ok = Command::call(device_type, cmd, arguments[1].c_str(), atoi(arguments[2].c_str()), output); + } + + if (ok) { + shell.print(F("output: ")); + serializeJson(doc, shell); + shell.println(); + shell.println(); } }, [&](Shell & shell __attribute__((unused)), const std::vector & arguments) -> std::vector { @@ -182,5 +220,4 @@ void Command::add_context_commands(unsigned int context) { }); } - } // namespace emsesp \ No newline at end of file diff --git a/src/command.h b/src/command.h index f020ff2d5..ff8580c59 100644 --- a/src/command.h +++ b/src/command.h @@ -34,21 +34,22 @@ using uuid::console::Shell; namespace emsesp { -using cmdfunction_p = std::function; +using cmdfunction_p = std::function; +using cmdfunction_json_p = std::function; class Command { public: struct CmdFunction { uint8_t device_type_; // DeviceType:: - uint8_t device_id_; const __FlashStringHelper * cmd_; cmdfunction_p cmdfunction_; + cmdfunction_json_p cmdfunction_json_; - CmdFunction(uint8_t device_type, uint8_t device_id, const __FlashStringHelper * cmd, cmdfunction_p cmdfunction) + CmdFunction(const uint8_t device_type, const __FlashStringHelper * cmd, cmdfunction_p cmdfunction, cmdfunction_json_p cmdfunction_json) : device_type_(device_type) - , device_id_(device_id) , cmd_(cmd) - , cmdfunction_(cmdfunction) { + , cmdfunction_(cmdfunction) + , cmdfunction_json_(cmdfunction_json) { } }; @@ -56,10 +57,11 @@ class Command { return cmdfunctions_; } - static bool call_command(const uint8_t device_type, const char * cmd, const char * value, const int8_t id); - static void add_command(const uint8_t device_type, const uint8_t device_id, const __FlashStringHelper * cmd, cmdfunction_p cb); - static void show_all_commands(uuid::console::Shell & shell); - static void show_commands(uuid::console::Shell & shell); + static bool call(const uint8_t device_type, const char * cmd, const char * value, const int8_t id, JsonObject & output); + static void add(const uint8_t device_type, const uint8_t device_id, const __FlashStringHelper * cmd, cmdfunction_p cb); + static void add_with_json(const uint8_t device_type, const __FlashStringHelper * cmd, cmdfunction_json_p cb); + static void show_all(uuid::console::Shell & shell); + static void show(uuid::console::Shell & shell); static void add_context_commands(unsigned int context); static bool find(const uint8_t device_type, const char * cmd); @@ -68,7 +70,7 @@ class Command { private: static uuid::log::Logger logger_; - static void show_commands(uuid::console::Shell & shell, uint8_t device_type); + static void show(uuid::console::Shell & shell, uint8_t device_type); static uint8_t context_2_device_type(unsigned int context); }; diff --git a/src/console.cpp b/src/console.cpp index 288fa599c..3eeb9d981 100644 --- a/src/console.cpp +++ b/src/console.cpp @@ -153,7 +153,7 @@ void EMSESPShell::add_console_commands() { commands->add_command(ShellContext::MAIN, CommandFlags::USER, flash_string_vector{F_(show), F_(commands)}, - [](Shell & shell, const std::vector & arguments __attribute__((unused))) { Command::show_all_commands(shell); }); + [](Shell & shell, const std::vector & arguments __attribute__((unused))) { Command::show_all(shell); }); commands->add_command( ShellContext::MAIN, @@ -466,8 +466,10 @@ void Console::load_standard_commands(unsigned int context) { // load the commands (console & mqtt topics) for this specific context - Command::add_context_commands(context); - + // unless it's main (the root) + if (context != ShellContext::MAIN) { + Command::add_context_commands(context); + } } // prompt, change per context @@ -485,6 +487,9 @@ std::string EMSESPShell::context_text() { case ShellContext::THERMOSTAT: return std::string{"/thermostat"}; + case ShellContext::SENSOR: + return std::string{"/sensor"}; + default: return std::string{}; } @@ -577,7 +582,6 @@ void Console::start() { shell->log_level(uuid::log::Level::DEBUG); #endif - #if defined(EMSESP_STANDALONE) // always start in su/admin mode when running tests shell->add_flags(CommandFlags::ADMIN); diff --git a/src/console.h b/src/console.h index 1b4fd7304..9149e951d 100644 --- a/src/console.h +++ b/src/console.h @@ -79,7 +79,8 @@ enum ShellContext : uint8_t { BOILER, THERMOSTAT, SOLAR, - MIXING + MIXING, + SENSOR }; diff --git a/src/devices/boiler.cpp b/src/devices/boiler.cpp index 1daa9e6b5..83c75e231 100644 --- a/src/devices/boiler.cpp +++ b/src/devices/boiler.cpp @@ -69,6 +69,12 @@ Boiler::Boiler(uint8_t device_type, int8_t device_id, uint8_t product_id, const register_mqtt_cmd(F("burnperiod"), [&](const char * value, const int8_t id) { return set_burn_period(value, id); }); register_mqtt_cmd(F("pumpdelay"), [&](const char * value, const int8_t id) { return set_pump_delay(value, id); }); + // API call + // Command::add_with_json(this->device_type(), F("info"), Boiler::command_info); + Command::add_with_json(this->device_type(), F("info"), [&](const char * value, const int8_t id, JsonObject & object) { + return command_info(value, id, object); + }); + EMSESP::esp8266React.getMqttSettingsService()->read([&](MqttSettings & settings) { mqtt_format_ = settings.mqtt_format; // single, nested or ha @@ -140,208 +146,217 @@ void Boiler::device_info_web(JsonArray & root) { render_value_json(root, "", F("Heat Pump modulation"), pumpMod2_, F_(percent)); } +bool Boiler::command_info(const char * value, const int8_t id, JsonObject & output) { + return (export_values(output)); +} + +// creates JSON doc from values +// returns false if empty +bool Boiler::export_values(JsonObject & output) { + char s[10]; // for formatting strings + + if (Helpers::hasValue(wWComfort_)) { + if (wWComfort_ == 0x00) { + output["wWComfort"] = "Hot"; + } else if (wWComfort_ == 0xD8) { + output["wWComfort"] = "Eco"; + } else if (wWComfort_ == 0xEC) { + output["wWComfort"] = "Intelligent"; + } + } + + if (Helpers::hasValue(wWSelTemp_)) { + output["wWSelTemp"] = wWSelTemp_; + } + if (Helpers::hasValue(wWSetTmp_)) { + output["wWSetTemp"] = wWSetTmp_; + } + if (Helpers::hasValue(wWDisinfectTemp_)) { + output["wWDisinfectionTemp"] = wWDisinfectTemp_; + } + if (Helpers::hasValue(selFlowTemp_)) { + output["selFlowTemp"] = selFlowTemp_; + } + if (Helpers::hasValue(selBurnPow_)) { + output["selBurnPow"] = selBurnPow_; + } + if (Helpers::hasValue(curBurnPow_)) { + output["curBurnPow"] = curBurnPow_; + } + if (Helpers::hasValue(pumpMod_)) { + output["pumpMod"] = pumpMod_; + } + if (Helpers::hasValue(pumpMod2_)) { + output["pumpMod2"] = pumpMod2_; + } + if (wWType_ == 0) { // no output if not set + output["wWType"] = F("off"); + } else if (wWType_ == 1) { + output["wWType"] = F("flow"); + } else if (wWType_ == 2) { + output["wWType"] = F("buffered flow"); + } else if (wWType_ == 3) { + output["wWType"] = F("buffer"); + } else if (wWType_ == 4) { + output["wWType"] = F("layered buffer"); + } + if (Helpers::hasValue(wWChargeType_, EMS_VALUE_BOOL)) { + output["wWChargeType"] = wWChargeType_ ? "valve" : "pump"; + } + if (Helpers::hasValue(wWCircPump_, EMS_VALUE_BOOL)) { + output["wWCircPump"] = Helpers::render_value(s, wWCircPump_, EMS_VALUE_BOOL); + } + if (Helpers::hasValue(wWCircPumpMode_)) { + output["wWCiPuMode"] = wWCircPumpMode_; + } + if (Helpers::hasValue(wWCirc_, EMS_VALUE_BOOL)) { + output["wWCirc"] = Helpers::render_value(s, wWCirc_, EMS_VALUE_BOOL); + } + if (Helpers::hasValue(extTemp_)) { + output["outdoorTemp"] = (float)extTemp_ / 10; + } + if (Helpers::hasValue(wWCurTmp_)) { + output["wWCurTmp"] = (float)wWCurTmp_ / 10; + } + if (Helpers::hasValue(wWCurTmp2_)) { + output["wWCurTmp2"] = (float)wWCurTmp2_ / 10; + } + if (Helpers::hasValue(wWCurFlow_)) { + output["wWCurFlow"] = (float)wWCurFlow_ / 10; + } + if (Helpers::hasValue(curFlowTemp_)) { + output["curFlowTemp"] = (float)curFlowTemp_ / 10; + } + if (Helpers::hasValue(retTemp_)) { + output["retTemp"] = (float)retTemp_ / 10; + } + if (Helpers::hasValue(switchTemp_)) { + output["switchTemp"] = (float)switchTemp_ / 10; + } + if (Helpers::hasValue(sysPress_)) { + output["sysPress"] = (float)sysPress_ / 10; + } + if (Helpers::hasValue(boilTemp_)) { + output["boilTemp"] = (float)boilTemp_ / 10; + } + if (Helpers::hasValue(wwStorageTemp1_)) { + output["wwStorageTemp1"] = (float)wwStorageTemp1_ / 10; + } + if (Helpers::hasValue(wwStorageTemp2_)) { + output["wwStorageTemp2"] = (float)wwStorageTemp2_ / 10; + } + if (Helpers::hasValue(exhaustTemp_)) { + output["exhaustTemp"] = (float)exhaustTemp_ / 10; + } + if (Helpers::hasValue(wWActivated_, EMS_VALUE_BOOL)) { + output["wWActivated"] = Helpers::render_value(s, wWActivated_, EMS_VALUE_BOOL); + } + if (Helpers::hasValue(wWOneTime_, EMS_VALUE_BOOL)) { + output["wWOnetime"] = Helpers::render_value(s, wWOneTime_, EMS_VALUE_BOOL); + } + if (Helpers::hasValue(wWDisinfecting_, EMS_VALUE_BOOL)) { + output["wWDisinfecting"] = Helpers::render_value(s, wWDisinfecting_, EMS_VALUE_BOOL); + } + if (Helpers::hasValue(wWReadiness_, EMS_VALUE_BOOL)) { + output["wWReady"] = Helpers::render_value(s, wWReadiness_, EMS_VALUE_BOOL); + } + if (Helpers::hasValue(wWRecharging_, EMS_VALUE_BOOL)) { + output["wWRecharge"] = Helpers::render_value(s, wWRecharging_, EMS_VALUE_BOOL); + } + if (Helpers::hasValue(wWTemperatureOK_, EMS_VALUE_BOOL)) { + output["wWTempOK"] = Helpers::render_value(s, wWTemperatureOK_, EMS_VALUE_BOOL); + } + if (Helpers::hasValue(wWCirc_, EMS_VALUE_BOOL)) { + output["wWCirc"] = Helpers::render_value(s, wWCirc_, EMS_VALUE_BOOL); + } + if (Helpers::hasValue(burnGas_, EMS_VALUE_BOOL)) { + output["burnGas"] = Helpers::render_value(s, burnGas_, EMS_VALUE_BOOL); + } + if (Helpers::hasValue(flameCurr_)) { + output["flameCurr"] = (float)(int16_t)flameCurr_ / 10; + } + if (Helpers::hasValue(heatPmp_, EMS_VALUE_BOOL)) { + output["heatPump"] = Helpers::render_value(s, heatPmp_, EMS_VALUE_BOOL); + } + if (Helpers::hasValue(fanWork_, EMS_VALUE_BOOL)) { + output["fanWork"] = Helpers::render_value(s, fanWork_, EMS_VALUE_BOOL); + } + if (Helpers::hasValue(ignWork_, EMS_VALUE_BOOL)) { + output["ignWork"] = Helpers::render_value(s, ignWork_, EMS_VALUE_BOOL); + } + if (Helpers::hasValue(wWHeat_, EMS_VALUE_BOOL)) { + output["wWHeat"] = Helpers::render_value(s, wWHeat_, EMS_VALUE_BOOL); + } + if (Helpers::hasValue(heating_activated_, EMS_VALUE_BOOL)) { + output["heatingActivated"] = Helpers::render_value(s, heating_activated_, EMS_VALUE_BOOL); + } + if (Helpers::hasValue(heating_temp_)) { + output["heatingTemp"] = heating_temp_; + } + if (Helpers::hasValue(pump_mod_max_)) { + output["pumpModMax"] = pump_mod_max_; + } + if (Helpers::hasValue(pump_mod_min_)) { + output["pumpModMin"] = pump_mod_min_; + } + if (Helpers::hasValue(pumpDelay_)) { + output["pumpDelay"] = pumpDelay_; + } + if (Helpers::hasValue(burnPeriod_)) { + output["burnMinPeriod"] = burnPeriod_; + } + if (Helpers::hasValue(burnPowermin_)) { + output["burnMinPower"] = burnPowermin_; + } + if (Helpers::hasValue(burnPowermax_)) { + output["burnMaxPower"] = burnPowermax_; + } + if (Helpers::hasValue(boilTemp_on_)) { + output["boilHystOn"] = boilTemp_on_; + } + if (Helpers::hasValue(boilTemp_off_)) { + output["boilHystOff"] = boilTemp_off_; + } + if (Helpers::hasValue(setFlowTemp_)) { + output["setFlowTemp"] = setFlowTemp_; + } + if (Helpers::hasValue(setWWPumpPow_)) { + output["wWSetPumpPower"] = setWWPumpPow_; + } + if (Helpers::hasValue(wWStarts_)) { + output["wWStarts"] = wWStarts_; + } + if (Helpers::hasValue(wWWorkM_)) { + output["wWWorkM"] = wWWorkM_; + } + if (Helpers::hasValue(UBAuptime_)) { + output["UBAuptime"] = UBAuptime_; + } + if (Helpers::hasValue(burnStarts_)) { + output["burnStarts"] = burnStarts_; + } + if (Helpers::hasValue(burnWorkMin_)) { + output["burnWorkMin"] = burnWorkMin_; + } + if (Helpers::hasValue(heatWorkMin_)) { + output["heatWorkMin"] = heatWorkMin_; + } + if (Helpers::hasValue(serviceCode_)) { + output["serviceCode"] = serviceCodeChar_; + output["serviceCodeNumber"] = serviceCode_; + } + + return (output.size()); +} + // publish values via MQTT void Boiler::publish_values() { // const size_t capacity = JSON_OBJECT_SIZE(56); // must recalculate if more objects addded https://arduinojson.org/v6/assistant/ // DynamicJsonDocument doc(capacity); StaticJsonDocument doc; - - char s[10]; // for formatting strings - - if (Helpers::hasValue(wWComfort_)) { - if (wWComfort_ == 0x00) { - doc["wWComfort"] = "Hot"; - } else if (wWComfort_ == 0xD8) { - doc["wWComfort"] = "Eco"; - } else if (wWComfort_ == 0xEC) { - doc["wWComfort"] = "Intelligent"; - } - } - - if (Helpers::hasValue(wWSelTemp_)) { - doc["wWSelTemp"] = wWSelTemp_; - } - if (Helpers::hasValue(wWSetTmp_)) { - doc["wWSetTemp"] = wWSetTmp_; - } - if (Helpers::hasValue(wWDisinfectTemp_)) { - doc["wWDisinfectionTemp"] = wWDisinfectTemp_; - } - if (Helpers::hasValue(selFlowTemp_)) { - doc["selFlowTemp"] = selFlowTemp_; - } - if (Helpers::hasValue(selBurnPow_)) { - doc["selBurnPow"] = selBurnPow_; - } - if (Helpers::hasValue(curBurnPow_)) { - doc["curBurnPow"] = curBurnPow_; - } - if (Helpers::hasValue(pumpMod_)) { - doc["pumpMod"] = pumpMod_; - } - if (Helpers::hasValue(pumpMod2_)) { - doc["pumpMod2"] = pumpMod2_; - } - if (wWType_ == 0) { // no output if not set - doc["wWType"] = F("off"); - } else if (wWType_ == 1) { - doc["wWType"] = F("flow"); - } else if (wWType_ == 2) { - doc["wWType"] = F("buffered flow"); - } else if (wWType_ == 3) { - doc["wWType"] = F("buffer"); - } else if (wWType_ == 4) { - doc["wWType"] = F("layered buffer"); - } - if (Helpers::hasValue(wWChargeType_, EMS_VALUE_BOOL)) { - doc["wWChargeType"] = wWChargeType_ ? "valve" : "pump"; - } - if (Helpers::hasValue(wWCircPump_, EMS_VALUE_BOOL)) { - doc["wWCircPump"] = Helpers::render_value(s, wWCircPump_, EMS_VALUE_BOOL); - } - if (Helpers::hasValue(wWCircPumpMode_)) { - doc["wWCiPuMode"] = wWCircPumpMode_; - } - if (Helpers::hasValue(wWCirc_, EMS_VALUE_BOOL)) { - doc["wWCirc"] = Helpers::render_value(s, wWCirc_, EMS_VALUE_BOOL); - } - if (Helpers::hasValue(extTemp_)) { - doc["outdoorTemp"] = (float)extTemp_ / 10; - } - if (Helpers::hasValue(wWCurTmp_)) { - doc["wWCurTmp"] = (float)wWCurTmp_ / 10; - } - if (Helpers::hasValue(wWCurTmp2_)) { - doc["wWCurTmp2"] = (float)wWCurTmp2_ / 10; - } - if (Helpers::hasValue(wWCurFlow_)) { - doc["wWCurFlow"] = (float)wWCurFlow_ / 10; - } - if (Helpers::hasValue(curFlowTemp_)) { - doc["curFlowTemp"] = (float)curFlowTemp_ / 10; - } - if (Helpers::hasValue(retTemp_)) { - doc["retTemp"] = (float)retTemp_ / 10; - } - if (Helpers::hasValue(switchTemp_)) { - doc["switchTemp"] = (float)switchTemp_ / 10; - } - if (Helpers::hasValue(sysPress_)) { - doc["sysPress"] = (float)sysPress_ / 10; - } - if (Helpers::hasValue(boilTemp_)) { - doc["boilTemp"] = (float)boilTemp_ / 10; - } - if (Helpers::hasValue(wwStorageTemp1_)) { - doc["wwStorageTemp1"] = (float)wwStorageTemp1_ / 10; - } - if (Helpers::hasValue(wwStorageTemp2_)) { - doc["wwStorageTemp2"] = (float)wwStorageTemp2_ / 10; - } - if (Helpers::hasValue(exhaustTemp_)) { - doc["exhaustTemp"] = (float)exhaustTemp_ / 10; - } - if (Helpers::hasValue(wWActivated_, EMS_VALUE_BOOL)) { - doc["wWActivated"] = Helpers::render_value(s, wWActivated_, EMS_VALUE_BOOL); - } - if (Helpers::hasValue(wWOneTime_, EMS_VALUE_BOOL)) { - doc["wWOnetime"] = Helpers::render_value(s, wWOneTime_, EMS_VALUE_BOOL); - } - if (Helpers::hasValue(wWDisinfecting_, EMS_VALUE_BOOL)) { - doc["wWDisinfecting"] = Helpers::render_value(s, wWDisinfecting_, EMS_VALUE_BOOL); - } - if (Helpers::hasValue(wWReadiness_, EMS_VALUE_BOOL)) { - doc["wWReady"] = Helpers::render_value(s, wWReadiness_, EMS_VALUE_BOOL); - } - if (Helpers::hasValue(wWRecharging_, EMS_VALUE_BOOL)) { - doc["wWRecharge"] = Helpers::render_value(s, wWRecharging_, EMS_VALUE_BOOL); - } - if (Helpers::hasValue(wWTemperatureOK_, EMS_VALUE_BOOL)) { - doc["wWTempOK"] = Helpers::render_value(s, wWTemperatureOK_, EMS_VALUE_BOOL); - } - if (Helpers::hasValue(wWCirc_, EMS_VALUE_BOOL)) { - doc["wWCirc"] = Helpers::render_value(s, wWCirc_, EMS_VALUE_BOOL); - } - if (Helpers::hasValue(burnGas_, EMS_VALUE_BOOL)) { - doc["burnGas"] = Helpers::render_value(s, burnGas_, EMS_VALUE_BOOL); - } - if (Helpers::hasValue(flameCurr_)) { - doc["flameCurr"] = (float)(int16_t)flameCurr_ / 10; - } - if (Helpers::hasValue(heatPmp_, EMS_VALUE_BOOL)) { - doc["heatPump"] = Helpers::render_value(s, heatPmp_, EMS_VALUE_BOOL); - } - if (Helpers::hasValue(fanWork_, EMS_VALUE_BOOL)) { - doc["fanWork"] = Helpers::render_value(s, fanWork_, EMS_VALUE_BOOL); - } - if (Helpers::hasValue(ignWork_, EMS_VALUE_BOOL)) { - doc["ignWork"] = Helpers::render_value(s, ignWork_, EMS_VALUE_BOOL); - } - if (Helpers::hasValue(wWHeat_, EMS_VALUE_BOOL)) { - doc["wWHeat"] = Helpers::render_value(s, wWHeat_, EMS_VALUE_BOOL); - } - if (Helpers::hasValue(heating_activated_, EMS_VALUE_BOOL)) { - doc["heatingActivated"] = Helpers::render_value(s, heating_activated_, EMS_VALUE_BOOL); - } - if (Helpers::hasValue(heating_temp_)) { - doc["heatingTemp"] = heating_temp_; - } - if (Helpers::hasValue(pump_mod_max_)) { - doc["pumpModMax"] = pump_mod_max_; - } - if (Helpers::hasValue(pump_mod_min_)) { - doc["pumpModMin"] = pump_mod_min_; - } - if (Helpers::hasValue(pumpDelay_)) { - doc["pumpDelay"] = pumpDelay_; - } - if (Helpers::hasValue(burnPeriod_)) { - doc["burnMinPeriod"] = burnPeriod_; - } - if (Helpers::hasValue(burnPowermin_)) { - doc["burnMinPower"] = burnPowermin_; - } - if (Helpers::hasValue(burnPowermax_)) { - doc["burnMaxPower"] = burnPowermax_; - } - if (Helpers::hasValue(boilTemp_on_)) { - doc["boilHystOn"] = boilTemp_on_; - } - if (Helpers::hasValue(boilTemp_off_)) { - doc["boilHystOff"] = boilTemp_off_; - } - if (Helpers::hasValue(setFlowTemp_)) { - doc["setFlowTemp"] = setFlowTemp_; - } - if (Helpers::hasValue(setWWPumpPow_)) { - doc["wWSetPumpPower"] = setWWPumpPow_; - } - if (Helpers::hasValue(wWStarts_)) { - doc["wWStarts"] = wWStarts_; - } - if (Helpers::hasValue(wWWorkM_)) { - doc["wWWorkM"] = wWWorkM_; - } - if (Helpers::hasValue(UBAuptime_)) { - doc["UBAuptime"] = UBAuptime_; - } - if (Helpers::hasValue(burnStarts_)) { - doc["burnStarts"] = burnStarts_; - } - if (Helpers::hasValue(burnWorkMin_)) { - doc["burnWorkMin"] = burnWorkMin_; - } - if (Helpers::hasValue(heatWorkMin_)) { - doc["heatWorkMin"] = heatWorkMin_; - } - if (Helpers::hasValue(serviceCode_)) { - doc["serviceCode"] = serviceCodeChar_; - doc["serviceCodeNumber"] = serviceCode_; - } - - // if we have data, publish it - if (!doc.isNull()) { - Mqtt::publish(F("boiler_data"), doc); + JsonObject output = doc.to(); + if (export_values(output)) { + Mqtt::publish(F("boiler_data"), doc.as()); } } @@ -801,6 +816,7 @@ bool Boiler::set_heating_activated(const char * value, const int8_t id) { if (!Helpers::value2bool(value, v)) { return false; } + LOG_INFO(F("Setting boiler heating "), v ? "on" : "off"); if (get_toggle_fetch(EMS_TYPE_UBAParametersPlus)) { write_command(EMS_TYPE_UBAParametersPlus, 0, v ? 0x01 : 0, EMS_TYPE_UBAParametersPlus); diff --git a/src/devices/boiler.h b/src/devices/boiler.h index d1c63d244..c2bf4ee6c 100644 --- a/src/devices/boiler.h +++ b/src/devices/boiler.h @@ -50,6 +50,7 @@ class Boiler : public EMSdevice { void console_commands(Shell & shell, unsigned int context); void register_mqtt_ha_config(); void check_active(); + bool export_values(JsonObject & doc); uint8_t last_boilerState = 0xFF; // remember last state of heating and warm water on/off uint8_t mqtt_format_; // single, nested or ha @@ -76,7 +77,7 @@ class Boiler : public EMSdevice { // MC10Status uint16_t wwMixTemperature_ = EMS_VALUE_USHORT_NOTSET; // mengertemperatuur - uint16_t wwBufferBoilerTemperature_ = EMS_VALUE_USHORT_NOTSET; // bufferboilertemperatuur + uint16_t wwBufferBoilerTemperature_ = EMS_VALUE_USHORT_NOTSET; // bufferboilertemperature // UBAMonitorFast - 0x18 on EMS1 uint8_t selFlowTemp_ = EMS_VALUE_UINT_NOTSET; // Selected flow temperature @@ -146,6 +147,8 @@ class Boiler : public EMSdevice { uint8_t heating_active_ = EMS_VALUE_BOOL_NOTSET; // Central heating is on/off uint8_t pumpMod2_ = EMS_VALUE_UINT_NOTSET; // heatpump modulation from 0xE3 (heatpumps) + bool command_info(const char * value, const int8_t id, JsonObject & output); + void process_UBAParameterWW(std::shared_ptr telegram); void process_UBAMonitorFast(std::shared_ptr telegram); void process_UBATotalUptime(std::shared_ptr telegram); diff --git a/src/devices/mixing.cpp b/src/devices/mixing.cpp index 2026c158f..ca6a901e3 100644 --- a/src/devices/mixing.cpp +++ b/src/devices/mixing.cpp @@ -53,10 +53,24 @@ Mixing::Mixing(uint8_t device_type, uint8_t device_id, uint8_t product_id, const if (flags == EMSdevice::EMS_DEVICE_FLAG_IPM) { register_telegram_type(0x010C, F("IPMSetMessage"), false, [&](std::shared_ptr t) { process_IPMStatusMessage(t); }); } + + // API call + Command::add_with_json(this->device_type(), F("info"), [&](const char * value, const int8_t id, JsonObject & object) { + return command_info(value, id, object); + }); } // add context submenu void Mixing::add_context_menu() { + // TODO support for multiple mixing units from a single menu, similar to set master with thermostat + /* + EMSESPShell::commands->add_command(ShellContext::MAIN, + CommandFlags::USER, + flash_string_vector{F_(mixing)}, + [&](Shell & shell, const std::vector & arguments __attribute__((unused))) { + Mixing::console_commands(shell, ShellContext::MIXING); + }); + */ } // output json to web UI @@ -89,7 +103,24 @@ bool Mixing::updated_values() { } // add console commands -void Mixing::console_commands() { +void Mixing::console_commands(Shell & shell, unsigned int context) { + EMSESPShell::commands->add_command(ShellContext::MIXING, + CommandFlags::ADMIN, + flash_string_vector{F_(read)}, + flash_string_vector{F_(typeid_mandatory)}, + [=](Shell & shell __attribute__((unused)), const std::vector & arguments) { + uint16_t type_id = Helpers::hextoint(arguments.front().c_str()); + EMSESP::set_read_id(type_id); + EMSESP::send_read_request(type_id, device_id()); + }); + + EMSESPShell::commands->add_command(ShellContext::MIXING, + CommandFlags::USER, + flash_string_vector{F_(show)}, + [&](Shell & shell, const std::vector & arguments __attribute__((unused))) { show_values(shell); }); + + // enter the context + Console::enter_custom_context(shell, context); } // display all values into the shell console @@ -117,49 +148,66 @@ void Mixing::show_values(uuid::console::Shell & shell) { shell.println(); } +bool Mixing::command_info(const char * value, const int8_t id, JsonObject & output) { + return (export_values(output)); +} + // publish values via MQTT // ideally we should group up all the mixing units together into a nested JSON but for now we'll send them individually void Mixing::publish_values() { StaticJsonDocument doc; - char s[5]; // for formatting strings + JsonObject output = doc.to(); + if (export_values(output)) { + char topic[30]; + char s[5]; + strlcpy(topic, "mixing_data", 30); + strlcat(topic, Helpers::itoa(s, device_id() - 0x20 + 1), 30); // append hc to topic + Mqtt::publish(topic, doc.as()); + } +} + +// creates JSON doc from values +// returns false if empty +bool Mixing::export_values(JsonObject & output) { + char s[5]; // for formatting strings switch (type_) { case Type::HC: - doc["type"] = "hc"; + output["type"] = "hc"; if (Helpers::hasValue(flowTemp_)) { - doc["flowTemp"] = (float)flowTemp_ / 10; + output["flowTemp"] = (float)flowTemp_ / 10; } if (Helpers::hasValue(flowSetTemp_)) { - doc["flowSetTemp"] = flowSetTemp_; + output["flowSetTemp"] = flowSetTemp_; } if (Helpers::hasValue(pump_)) { - doc["pumpStatus"] = Helpers::render_value(s, pump_, EMS_VALUE_BOOL); + output["pumpStatus"] = Helpers::render_value(s, pump_, EMS_VALUE_BOOL); } if (Helpers::hasValue(status_)) { - doc["valveStatus"] = status_; + output["valveStatus"] = status_; } break; + case Type::WWC: - doc["type"] = "wwc"; + output["type"] = "wwc"; if (Helpers::hasValue(flowTemp_)) { - doc["wwTemp"] = (float)flowTemp_ / 10; + output["wwTemp"] = (float)flowTemp_ / 10; } if (Helpers::hasValue(pump_)) { - doc["pumpStatus"] = pump_; + output["pumpStatus"] = pump_; } if (Helpers::hasValue(status_)) { - doc["tempStatus"] = status_; + output["tempStatus"] = status_; } break; + case Type::NONE: default: - return; + return false; + break; } - char topic[30]; - strlcpy(topic, "mixing_data", 30); - strlcat(topic, Helpers::itoa(s, device_id() - 0x20 + 1), 30); // append hc to topic - Mqtt::publish(topic, doc); + return output.size(); } // heating circuits 0x02D7, 0x02D8 etc... diff --git a/src/devices/mixing.h b/src/devices/mixing.h index 2dac113d2..9795d7ff0 100644 --- a/src/devices/mixing.h +++ b/src/devices/mixing.h @@ -25,6 +25,7 @@ #include #include "emsdevice.h" +#include "emsesp.h" #include "telegram.h" #include "helpers.h" #include "mqtt.h" @@ -44,7 +45,9 @@ class Mixing : public EMSdevice { private: static uuid::log::Logger logger_; - void console_commands(); + void console_commands(Shell & shell, unsigned int context); + bool export_values(JsonObject & doc); + bool command_info(const char * value, const int8_t id, JsonObject & output); void process_MMPLUSStatusMessage_HC(std::shared_ptr telegram); void process_MMPLUSStatusMessage_WWC(std::shared_ptr telegram); @@ -66,7 +69,8 @@ class Mixing : public EMSdevice { int8_t status_ = EMS_VALUE_UINT_NOTSET; uint8_t flowSetTemp_ = EMS_VALUE_UINT_NOTSET; Type type_ = Type::NONE; - bool changed_ = false; + + bool changed_ = false; }; } // namespace emsesp diff --git a/src/devices/solar.cpp b/src/devices/solar.cpp index f563b02c3..4b6064b57 100644 --- a/src/devices/solar.cpp +++ b/src/devices/solar.cpp @@ -47,10 +47,28 @@ Solar::Solar(uint8_t device_type, uint8_t device_id, uint8_t product_id, const s register_telegram_type(0x0103, F("ISM1StatusMessage"), true, [&](std::shared_ptr t) { process_ISM1StatusMessage(t); }); register_telegram_type(0x0101, F("ISM1Set"), false, [&](std::shared_ptr t) { process_ISM1Set(t); }); } + + // API call + Command::add_with_json(this->device_type(), F("info"), [&](const char * value, const int8_t id, JsonObject & object) { + return command_info(value, id, object); + }); +} + +bool Solar::command_info(const char * value, const int8_t id, JsonObject & output) { + return (export_values(output)); } // context submenu void Solar::add_context_menu() { + // TODO support for multiple solar units from a single menu, similar to set master with thermostat + /* + EMSESPShell::commands->add_command(ShellContext::MAIN, + CommandFlags::USER, + flash_string_vector{F_(solar)}, + [&](Shell & shell, const std::vector & arguments __attribute__((unused))) { + Solar::console_commands(shell, ShellContext::SOLAR); + }); + */ } // print to web @@ -109,69 +127,74 @@ void Solar::show_values(uuid::console::Shell & shell) { // publish values via MQTT void Solar::publish_values() { StaticJsonDocument doc; + JsonObject output = doc.to(); + if (export_values(output)) { + Mqtt::publish(F("sm_data"), doc.as()); + } +} +// creates JSON doc from values +// returns false if empty +bool Solar::export_values(JsonObject & output) { char s[10]; // for formatting strings if (Helpers::hasValue(collectorTemp_)) { - doc["collectorTemp"] = (float)collectorTemp_ / 10; + output["collectorTemp"] = (float)collectorTemp_ / 10; } if (Helpers::hasValue(tankBottomTemp_)) { - doc["tankBottomTemp"] = (float)tankBottomTemp_ / 10; + output["tankBottomTemp"] = (float)tankBottomTemp_ / 10; } if (Helpers::hasValue(tankBottomTemp2_)) { - doc["tankBottomTemp2"] = (float)tankBottomTemp2_ / 10; + output["tankBottomTemp2"] = (float)tankBottomTemp2_ / 10; } if (Helpers::hasValue(heatExchangerTemp_)) { - doc["heatExchangerTemp"] = (float)heatExchangerTemp_ / 10; + output["heatExchangerTemp"] = (float)heatExchangerTemp_ / 10; } if (Helpers::hasValue(solarPumpModulation_)) { - doc["solarPumpModulation"] = solarPumpModulation_; + output["solarPumpModulation"] = solarPumpModulation_; } if (Helpers::hasValue(cylinderPumpModulation_)) { - doc["cylinderPumpModulation"] = cylinderPumpModulation_; + output["cylinderPumpModulation"] = cylinderPumpModulation_; } if (Helpers::hasValue(solarPump_, EMS_VALUE_BOOL)) { - doc["solarPump"] = Helpers::render_value(s, solarPump_, EMS_VALUE_BOOL); + output["solarPump"] = Helpers::render_value(s, solarPump_, EMS_VALUE_BOOL); } if (Helpers::hasValue(valveStatus_, EMS_VALUE_BOOL)) { - doc["valveStatus"] = Helpers::render_value(s, valveStatus_, EMS_VALUE_BOOL); + output["valveStatus"] = Helpers::render_value(s, valveStatus_, EMS_VALUE_BOOL); } if (Helpers::hasValue(pumpWorkMin_)) { - doc["pumpWorkMin"] = pumpWorkMin_; + output["pumpWorkMin"] = pumpWorkMin_; } if (Helpers::hasValue(tankHeated_, EMS_VALUE_BOOL)) { - doc["tankHeated"] = Helpers::render_value(s, tankHeated_, EMS_VALUE_BOOL); + output["tankHeated"] = Helpers::render_value(s, tankHeated_, EMS_VALUE_BOOL); } if (Helpers::hasValue(collectorShutdown_, EMS_VALUE_BOOL)) { - doc["collectorShutdown"] = Helpers::render_value(s, collectorShutdown_, EMS_VALUE_BOOL); + output["collectorShutdown"] = Helpers::render_value(s, collectorShutdown_, EMS_VALUE_BOOL); } if (Helpers::hasValue(energyLastHour_)) { - doc["energyLastHour"] = (float)energyLastHour_ / 10; + output["energyLastHour"] = (float)energyLastHour_ / 10; } if (Helpers::hasValue(energyToday_)) { - doc["energyToday"] = energyToday_; + output["energyToday"] = energyToday_; } if (Helpers::hasValue(energyTotal_)) { - doc["energyTotal"] = (float)energyTotal_ / 10; + output["energyTotal"] = (float)energyTotal_ / 10; } - // if we have data, publish it - if (!doc.isNull()) { - Mqtt::publish(F("sm_data"), doc); - } + return output.size(); } // check to see if values have been updated @@ -184,7 +207,24 @@ bool Solar::updated_values() { } // add console commands -void Solar::console_commands() { +void Solar::console_commands(Shell & shell, unsigned int context) { + EMSESPShell::commands->add_command(ShellContext::SOLAR, + CommandFlags::ADMIN, + flash_string_vector{F_(read)}, + flash_string_vector{F_(typeid_mandatory)}, + [=](Shell & shell __attribute__((unused)), const std::vector & arguments) { + uint16_t type_id = Helpers::hextoint(arguments.front().c_str()); + EMSESP::set_read_id(type_id); + EMSESP::send_read_request(type_id, device_id()); + }); + + EMSESPShell::commands->add_command(ShellContext::SOLAR, + CommandFlags::USER, + flash_string_vector{F_(show)}, + [&](Shell & shell, const std::vector & arguments __attribute__((unused))) { show_values(shell); }); + + // enter the context + Console::enter_custom_context(shell, context); } // SM10Monitor - type 0x97 diff --git a/src/devices/solar.h b/src/devices/solar.h index 248cd71ec..e2f928232 100644 --- a/src/devices/solar.h +++ b/src/devices/solar.h @@ -25,6 +25,7 @@ #include #include "emsdevice.h" +#include "emsesp.h" #include "telegram.h" #include "helpers.h" #include "mqtt.h" @@ -44,7 +45,9 @@ class Solar : public EMSdevice { private: static uuid::log::Logger logger_; - void console_commands(); + void console_commands(Shell & shell, unsigned int context); + bool export_values(JsonObject & doc); + bool command_info(const char * value, const int8_t id, JsonObject & output); int16_t collectorTemp_ = EMS_VALUE_SHORT_NOTSET; // TS1: Temperature sensor for collector array 1 int16_t tankBottomTemp_ = EMS_VALUE_SHORT_NOTSET; // TS2: Temperature sensor 1 cylinder, bottom (solar thermal system) @@ -65,7 +68,8 @@ class Solar : public EMSdevice { uint8_t availabilityFlag_ = EMS_VALUE_BOOL_NOTSET; uint8_t configFlag_ = EMS_VALUE_BOOL_NOTSET; uint8_t userFlag_ = EMS_VALUE_BOOL_NOTSET; - bool changed_ = false; + + bool changed_ = false; void process_SM10Monitor(std::shared_ptr telegram); void process_SM100Monitor(std::shared_ptr telegram); diff --git a/src/devices/thermostat.cpp b/src/devices/thermostat.cpp index fbb205de9..9a04e7e8d 100644 --- a/src/devices/thermostat.cpp +++ b/src/devices/thermostat.cpp @@ -248,21 +248,34 @@ bool Thermostat::updated_values() { return false; } +// info API command +// returns the same MQTT publish payload in Nested format +bool Thermostat::command_info(const char * value, const int8_t id, JsonObject & output) { + return (export_values(MQTT_format::NESTED, output)); +} + // publish values via MQTT void Thermostat::publish_values() { - // only publish on the master thermostat if (EMSESP::actual_master_thermostat() != this->device_id()) { return; } - uint8_t flags = this->model(); - StaticJsonDocument doc; - JsonObject rootThermostat = doc.to(); - JsonObject dataThermostat; + JsonObject output = doc.to(); + export_values(mqtt_format_, output); + + if (mqtt_format_ == MQTT_format::NESTED) { + Mqtt::publish(F("thermostat_data"), output); + } +} + +// creates JSON doc from values +// returns false if empty +bool Thermostat::export_values(uint8_t mqtt_format, JsonObject & rootThermostat) { + uint8_t flags = this->model(); + JsonObject dataThermostat; // add external temp and other stuff specific to the RC30 and RC35 - // if ((flags == EMS_DEVICE_FLAG_RC35 || flags == EMS_DEVICE_FLAG_RC30_1) && (mqtt_format_ == MQTT_format::SINGLE || mqtt_format_ == MQTT_format::CUSTOM)) { if (flags == EMS_DEVICE_FLAG_RC35 || flags == EMS_DEVICE_FLAG_RC30_1) { if (datetime_.size()) { rootThermostat["time"] = datetime_.c_str(); @@ -302,9 +315,9 @@ void Thermostat::publish_values() { } // send this specific data using the thermostat_data topic - if (mqtt_format_ != MQTT_format::NESTED) { - Mqtt::publish(F("thermostat_data"), doc); - rootThermostat = doc.to(); // clear object + if (mqtt_format != MQTT_format::NESTED) { + Mqtt::publish(F("thermostat_data"), rootThermostat); + rootThermostat.clear(); // clear object } } @@ -315,8 +328,7 @@ void Thermostat::publish_values() { has_data = true; // if the MQTT format is 'nested' or 'ha' then create the parent object hc - // if (mqtt_format_ != MQTT_format::SINGLE) { - if ((mqtt_format_ == MQTT_format::NESTED) || (mqtt_format_ == MQTT_format::HA)) { + if ((mqtt_format == MQTT_format::NESTED) || (mqtt_format == MQTT_format::HA)) { char hc_name[10]; // hc{1-4} strlcpy(hc_name, "hc", 10); char s[3]; @@ -397,11 +409,11 @@ void Thermostat::publish_values() { dataThermostat["summertemp"] = hc->summertemp; } - // when using HA always send the mode otherwise it'll may break the component/widget and report an error - if ((Helpers::hasValue(hc->mode)) || (mqtt_format_ == MQTT_format::HA)) { + // mode - always force showing this when in HA so not to break HA's climate component + if ((Helpers::hasValue(hc->mode)) || (mqtt_format == MQTT_format::HA)) { uint8_t hc_mode = hc->get_mode(flags); // if we're sending to HA the only valid mode types are heat, auto and off - if (mqtt_format_ == MQTT_format::HA) { + if (mqtt_format == MQTT_format::HA) { if ((hc_mode == HeatingCircuit::Mode::MANUAL) || (hc_mode == HeatingCircuit::Mode::DAY)) { hc_mode = HeatingCircuit::Mode::HEAT; } else if ((hc_mode == HeatingCircuit::Mode::NIGHT) || (hc_mode == HeatingCircuit::Mode::OFF)) { @@ -425,15 +437,13 @@ void Thermostat::publish_values() { // if format is single, send immediately and clear object for next hc // the topic will have the hc number appended - // if (mqtt_format_ == MQTT_format::SINGLE) { - if ((mqtt_format_ == MQTT_format::SINGLE) || (mqtt_format_ == MQTT_format::CUSTOM)) { + if ((mqtt_format == MQTT_format::SINGLE) || (mqtt_format == MQTT_format::CUSTOM)) { char topic[30]; char s[3]; strlcpy(topic, "thermostat_data", 30); strlcat(topic, Helpers::itoa(s, hc->hc_num()), 30); // append hc to topic - Mqtt::publish(topic, doc); - rootThermostat = doc.to(); // clear object - } else if (mqtt_format_ == MQTT_format::HA) { + rootThermostat.clear(); // clear object + } else if (mqtt_format == MQTT_format::HA) { // see if we have already registered this with HA MQTT Discovery, if not send the config if (!hc->ha_registered()) { register_mqtt_ha_config(hc->hc_num()); @@ -442,19 +452,12 @@ void Thermostat::publish_values() { // send the thermostat topic and payload data std::string topic(100, '\0'); snprintf_P(&topic[0], topic.capacity() + 1, PSTR("homeassistant/climate/ems-esp/hc%d/state"), hc->hc_num()); - Mqtt::publish(topic, doc); + Mqtt::publish(topic, rootThermostat); } } } - if (!has_data) { - return; // nothing to send, quit - } - - // if we're using nested json, send all in one go under one topic called thermostat_data - if (mqtt_format_ == MQTT_format::NESTED) { - Mqtt::publish(F("thermostat_data"), doc); - } + return (has_data); } // returns the heating circuit object based on the hc number @@ -599,7 +602,7 @@ void Thermostat::register_mqtt_ha_config(uint8_t hc_num) { std::string topic(100, '\0'); // e.g homeassistant/climate/hc1/thermostat/config snprintf_P(&topic[0], topic.capacity() + 1, PSTR("homeassistant/climate/ems-esp/hc%d/config"), hc_num); // Mqtt::publish(topic); // empty payload, this remove any previous config sent to HA - Mqtt::publish_retain(topic, doc, true); // publish the config payload with retain flag + Mqtt::publish_retain(topic, doc.as(), true); // publish the config payload with retain flag // subscribe to the temp and mode commands snprintf_P(&topic[0], topic.capacity() + 1, PSTR("homeassistant/climate/ems-esp/hc%d/cmd_temp"), hc_num); @@ -1922,8 +1925,13 @@ bool Thermostat::set_manualtemp(const char * value, const int8_t id) { return set_temperature_value(value, id, HeatingCircuit::Mode::MANUAL); } -// commands for MQTT and Console +// API commands for MQTT and Console void Thermostat::add_commands() { + // API call + Command::add_with_json(this->device_type(), F("info"), [&](const char * value, const int8_t id, JsonObject & object) { + return command_info(value, id, object); + }); + // if this thermostat doesn't support write, don't add the commands if ((this->flags() & EMSdevice::EMS_DEVICE_FLAG_NO_WRITE) == EMSdevice::EMS_DEVICE_FLAG_NO_WRITE) { return; diff --git a/src/devices/thermostat.h b/src/devices/thermostat.h index 39fa79b60..78dc51a60 100644 --- a/src/devices/thermostat.h +++ b/src/devices/thermostat.h @@ -108,6 +108,7 @@ class Thermostat : public EMSdevice { void console_commands(Shell & shell, unsigned int context); void add_commands(); + bool export_values(uint8_t mqtt_format, JsonObject & doc); // specific thermostat characteristics, stripping the option bits at pos 6 and 7 inline uint8_t model() const { @@ -221,6 +222,7 @@ class Thermostat : public EMSdevice { std::shared_ptr heating_circuit(const uint8_t hc_num); void register_mqtt_ha_config(uint8_t hc_num); + bool command_info(const char * value, const int8_t id, JsonObject & output); void process_RCOutdoorTemp(std::shared_ptr telegram); void process_IBASettings(std::shared_ptr telegram); diff --git a/src/emsdevice.cpp b/src/emsdevice.cpp index af6fb9180..72039285b 100644 --- a/src/emsdevice.cpp +++ b/src/emsdevice.cpp @@ -111,6 +111,10 @@ uint8_t EMSdevice::device_name_2_device_type(const char * topic) { return DeviceType::MIXING; } + if (strcmp(topic, "sensor") == 0) { + return DeviceType::SENSOR; + } + return DeviceType::UNKNOWN; } @@ -286,7 +290,7 @@ void EMSdevice::register_mqtt_topic(const std::string & topic, mqtt_subfunction_ // add command to library void EMSdevice::register_mqtt_cmd(const __FlashStringHelper * cmd, cmdfunction_p f) { - Command::add_command(this->device_type_, this->device_id_, cmd, f); + Command::add(this->device_type_, this->device_id_, cmd, f); } // register a call back function for a specific telegram type diff --git a/src/emsdevice.h b/src/emsdevice.h index 6293ed5ed..d26e05a11 100644 --- a/src/emsdevice.h +++ b/src/emsdevice.h @@ -229,6 +229,7 @@ class EMSdevice { enum DeviceType : uint8_t { SERVICEKEY = 0, // this is us (EMS-ESP) + SENSOR, // for internal dallas sensors BOILER, THERMOSTAT, MIXING, diff --git a/src/emsesp.cpp b/src/emsesp.cpp index 795da3842..16521d95b 100644 --- a/src/emsesp.cpp +++ b/src/emsesp.cpp @@ -51,7 +51,7 @@ TxService EMSESP::txservice_; // outgoing Telegram Tx handler Mqtt EMSESP::mqtt_; // mqtt handler System EMSESP::system_; // core system services Console EMSESP::console_; // telnet and serial console -Sensors EMSESP::sensors_; // Dallas sensors +Sensor EMSESP::sensor_; // Dallas sensors Shower EMSESP::shower_; // Shower logic // static/common variables @@ -311,8 +311,8 @@ void EMSESP::publish_other_values() { void EMSESP::publish_sensor_values(const bool force) { if (Mqtt::connected()) { - if (sensors_.updated_values() || force) { - sensors_.publish_values(); + if (sensor_.updated_values() || force) { + sensor_.publish_values(); } } } @@ -337,7 +337,7 @@ void EMSESP::publish_response(std::shared_ptr telegram) { doc["value"] = value; } - Mqtt::publish(F("response"), doc); + Mqtt::publish(F("response"), doc.as()); } // search for recognized device_ids : Me, All, otherwise print hex value @@ -845,7 +845,7 @@ void EMSESP::start() { mqtt_.start(); // mqtt init system_.start(); // starts syslog, uart, sets version, initializes LED. Requires pre-loaded settings. shower_.start(); // initialize shower timer and shower alert - sensors_.start(); // dallas external sensors + sensor_.start(); // dallas external sensors webServer.begin(); // start web server emsdevices.reserve(5); // reserve space for initially 5 devices to avoid mem @@ -868,7 +868,7 @@ void EMSESP::loop() { system_.loop(); // does LED and checks system health, and syslog service shower_.loop(); // check for shower on/off - sensors_.loop(); // this will also send out via MQTT + sensor_.loop(); // this will also send out via MQTT mqtt_.loop(); // sends out anything in the queue via MQTT console_.loop(); // telnet/serial console rxservice_.loop(); // process any incoming Rx telegrams diff --git a/src/emsesp.h b/src/emsesp.h index 6bf913c5e..181c84ce7 100644 --- a/src/emsesp.h +++ b/src/emsesp.h @@ -44,7 +44,7 @@ #include "telegram.h" #include "mqtt.h" #include "system.h" -#include "sensors.h" +#include "sensor.h" #include "console.h" #include "shower.h" #include "roomcontrol.h" @@ -108,8 +108,8 @@ class EMSESP { static void incoming_telegram(uint8_t * data, const uint8_t length); - static const std::vector sensor_devices() { - return sensors_.devices(); + static const std::vector sensor_devices() { + return sensor_.devices(); } enum Watch : uint8_t { WATCH_OFF, WATCH_ON, WATCH_RAW }; @@ -153,7 +153,7 @@ class EMSESP { // services static Mqtt mqtt_; static System system_; - static Sensors sensors_; + static Sensor sensor_; static Console console_; static Shower shower_; static RxService rxservice_; diff --git a/src/locale_EN.h b/src/locale_EN.h index 169d18f6c..783ffa1f9 100644 --- a/src/locale_EN.h +++ b/src/locale_EN.h @@ -88,7 +88,7 @@ MAKE_PSTR_WORD(connect) MAKE_PSTR_WORD(heatpump) // dallas sensors -MAKE_PSTR_WORD(sensors) +MAKE_PSTR_WORD(sensor) MAKE_PSTR(kwh, "kWh") MAKE_PSTR(wh, "Wh") @@ -99,7 +99,8 @@ MAKE_PSTR(hostname_fmt, "WiFi Hostname = %s") MAKE_PSTR(mark_interval_fmt, "Mark interval = %lus") MAKE_PSTR(wifi_ssid_fmt, "WiFi SSID = %s") MAKE_PSTR(wifi_password_fmt, "WiFi Password = %S") -MAKE_PSTR(system_heartbeat_fmt, "MQTT Heartbeat is %s") +MAKE_PSTR(mqtt_heartbeat_fmt, "MQTT Heartbeat is %s") +MAKE_PSTR(mqtt_format_fmt, "MQTT Format is %d") MAKE_PSTR(cmd_optional, "[cmd]") MAKE_PSTR(deep_optional, "[deep]") MAKE_PSTR(tx_mode_fmt, "Tx mode = %d") diff --git a/src/mqtt.cpp b/src/mqtt.cpp index d3cc9e36c..2466baf19 100644 --- a/src/mqtt.cpp +++ b/src/mqtt.cpp @@ -28,7 +28,6 @@ AsyncMqttClient * Mqtt::mqttClient_; std::string Mqtt::hostname_; uint8_t Mqtt::mqtt_qos_; bool Mqtt::mqtt_retain_; -uint8_t Mqtt::bus_id_; uint32_t Mqtt::publish_time_boiler_; uint32_t Mqtt::publish_time_thermostat_; uint32_t Mqtt::publish_time_solar_; @@ -148,9 +147,10 @@ void Mqtt::loop() { 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()); - bool system_heartbeat; - EMSESP::esp8266React.getMqttSettingsService()->read([&](MqttSettings & settings) { system_heartbeat = settings.system_heartbeat; }); - shell.printfln(F_(system_heartbeat_fmt), system_heartbeat ? F_(enabled) : F_(disabled)); + EMSESP::esp8266React.getMqttSettingsService()->read([&](MqttSettings & settings) { + shell.printfln(F_(mqtt_heartbeat_fmt), settings.system_heartbeat ? F_(enabled) : F_(disabled)); + shell.printfln(F_(mqtt_format_fmt), settings.mqtt_format); + }); shell.printfln(F("MQTT publish fails count: %lu"), mqtt_publish_fails_); shell.println(); @@ -253,14 +253,19 @@ void Mqtt::on_message(const char * topic, const char * payload, size_t len) { bool cmd_known = false; JsonVariant data = doc["data"]; + + JsonObject output; // empty object + if (data.is()) { - cmd_known = Command::call_command(mf.device_type_, command, data.as(), n); + cmd_known = Command::call(mf.device_type_, command, data.as(), n, output); } else if (data.is()) { char data_str[10]; - cmd_known = Command::call_command(mf.device_type_, command, Helpers::itoa(data_str, (int16_t)data.as()), n); + cmd_known = Command::call(mf.device_type_, command, Helpers::itoa(data_str, (int16_t)data.as()), n, output); } else if (data.is()) { char data_str[10]; - cmd_known = Command::call_command(mf.device_type_, command, Helpers::render_value(data_str, (float)data.as(), 2), n); + cmd_known = Command::call(mf.device_type_, command, Helpers::render_value(data_str, (float)data.as(), 2), n, output); + } else if (data.isNull()) { + cmd_known = Command::call(mf.device_type_, command, "", n, output); } if (!cmd_known) { @@ -336,8 +341,6 @@ void Mqtt::start() { mqtt_retain_ = mqttSettings.mqtt_retain; }); - EMSESP::emsespSettingsService.read([&](EMSESPSettings & settings) { bus_id_ = settings.ems_bus_id; }); - mqttClient_->onConnect([this](bool sessionPresent) { on_connect(); }); mqttClient_->onDisconnect([this](AsyncMqttClientDisconnectReason reason) { @@ -443,7 +446,7 @@ void Mqtt::on_connect() { #ifndef EMSESP_STANDALONE doc["ip"] = WiFi.localIP().toString(); #endif - publish(F("info"), doc); + publish(F("info"), doc.as()); publish_retain(F("status"), "online", true); // say we're alive to the Last Will topic, with retain on @@ -451,10 +454,6 @@ void Mqtt::on_connect() { resubscribe(); // in case this is a reconnect, re-subscribe again to all MQTT topics - // these commands respond to the topic "system" and take a payload like {cmd:"", data:"", id:""} - Command::add_command(EMSdevice::DeviceType::SERVICEKEY, bus_id_, F("pin"), System::command_pin); - Command::add_command(EMSdevice::DeviceType::SERVICEKEY, bus_id_, F("send"), System::command_send); - LOG_INFO(F("MQTT connected")); } @@ -470,12 +469,14 @@ std::shared_ptr Mqtt::queue_message(const uint8_t operation, std::shared_ptr message; if ((strncmp(topic.c_str(), "homeassistant/", 13) == 0)) { // leave topic as it is - message = std::make_shared(operation, topic, std::move(payload), retain); + // message = std::make_shared(operation, topic, std::move(payload), retain); + message = std::make_shared(operation, topic, payload, retain); + } else { // prefix the hostname std::string full_topic(50, '\0'); snprintf_P(&full_topic[0], full_topic.capacity() + 1, PSTR("%s/%s"), Mqtt::hostname_.c_str(), topic.c_str()); - message = std::make_shared(operation, full_topic, std::move(payload), retain); + message = std::make_shared(operation, full_topic, payload, retain); } // if the queue is full, make room but removing the last one @@ -507,7 +508,7 @@ void Mqtt::publish(const __FlashStringHelper * topic, const std::string & payloa queue_publish_message(uuid::read_flash_string(topic), payload, mqtt_retain_); } -void Mqtt::publish(const __FlashStringHelper * topic, const JsonDocument & payload) { +void Mqtt::publish(const __FlashStringHelper * topic, const JsonObject & payload) { publish(uuid::read_flash_string(topic), payload); } @@ -516,17 +517,17 @@ void Mqtt::publish_retain(const __FlashStringHelper * topic, const std::string & queue_publish_message(uuid::read_flash_string(topic), payload, retain); } -void Mqtt::publish_retain(const std::string & topic, const JsonDocument & payload, bool retain) { +void Mqtt::publish_retain(const std::string & topic, const JsonObject & payload, bool retain) { std::string payload_text; serializeJson(payload, payload_text); // convert json to string queue_publish_message(topic, payload_text, retain); } -void Mqtt::publish_retain(const __FlashStringHelper * topic, const JsonDocument & payload, bool retain) { +void Mqtt::publish_retain(const __FlashStringHelper * topic, const JsonObject & payload, bool retain) { publish_retain(uuid::read_flash_string(topic), payload, retain); } -void Mqtt::publish(const std::string & topic, const JsonDocument & payload) { +void Mqtt::publish(const std::string & topic, const JsonObject & payload) { std::string payload_text; serializeJson(payload, payload_text); // convert json to string queue_publish_message(topic, payload_text, mqtt_retain_); diff --git a/src/mqtt.h b/src/mqtt.h index 1abafd70e..a1bf1725d 100644 --- a/src/mqtt.h +++ b/src/mqtt.h @@ -45,7 +45,7 @@ using uuid::console::Shell; namespace emsesp { using mqtt_subfunction_p = std::function; -using cmdfunction_p = std::function; +using cmdfunction_p = std::function; struct MqttMessage { ~MqttMessage() = default; @@ -55,7 +55,8 @@ struct MqttMessage { const std::string payload; const bool retain; - MqttMessage(const uint8_t operation, const std::string & topic, const std::string && payload, bool retain) + // MqttMessage(const uint8_t operation, const std::string & topic, const std::string && payload, bool retain) + MqttMessage(const uint8_t operation, const std::string & topic, const std::string & payload, bool retain) : operation(operation) , topic(topic) , payload(payload) @@ -89,16 +90,16 @@ class Mqtt { static void register_command(const uint8_t device_type, const uint8_t device_id, const __FlashStringHelper * cmd, cmdfunction_p cb); static void publish(const std::string & topic, const std::string & payload); - static void publish(const std::string & topic, const JsonDocument & payload); - static void publish(const __FlashStringHelper * topic, const JsonDocument & payload); + static void publish(const std::string & topic, const JsonObject & payload); + static void publish(const __FlashStringHelper * topic, const JsonObject & payload); static void publish(const __FlashStringHelper * topic, const std::string & payload); static void publish(const std::string & topic, const bool value); static void publish(const __FlashStringHelper * topi, const bool value); static void publish(const std::string & topic); - static void publish_retain(const std::string & topic, const JsonDocument & payload, bool retain); + static void publish_retain(const std::string & topic, const JsonObject & payload, bool retain); static void publish_retain(const __FlashStringHelper * topic, const std::string & payload, bool retain); - static void publish_retain(const __FlashStringHelper * topic, const JsonDocument & payload, bool retain); + static void publish_retain(const __FlashStringHelper * topic, const JsonObject & payload, bool retain); static void show_topic_handlers(uuid::console::Shell & shell, const uint8_t device_type); static void show_mqtt(uuid::console::Shell & shell); @@ -194,7 +195,6 @@ class Mqtt { static uint8_t mqtt_qos_; static bool mqtt_retain_; static uint32_t publish_time_; - static uint8_t bus_id_; static uint32_t publish_time_boiler_; static uint32_t publish_time_thermostat_; static uint32_t publish_time_solar_; diff --git a/src/sensors.cpp b/src/sensor.cpp similarity index 86% rename from src/sensors.cpp rename to src/sensor.cpp index d98152bfb..42db2b18c 100644 --- a/src/sensors.cpp +++ b/src/sensor.cpp @@ -18,7 +18,7 @@ // code originally written by nomis - https://github.com/nomis -#include "sensors.h" +#include "sensor.h" #include "emsesp.h" #ifdef ESP32 @@ -29,10 +29,10 @@ namespace emsesp { -uuid::log::Logger Sensors::logger_{F_(sensors), uuid::log::Facility::DAEMON}; +uuid::log::Logger Sensor::logger_{F_(sensor), uuid::log::Facility::DAEMON}; // start the 1-wire -void Sensors::start() { +void Sensor::start() { reload(); #ifndef EMSESP_STANDALONE @@ -40,10 +40,15 @@ void Sensors::start() { bus_.begin(dallas_gpio_); } #endif + + // API call + Command::add_with_json(EMSdevice::DeviceType::SENSOR, F("info"), [&](const char * value, const int8_t id, JsonObject & object) { + return command_info(value, id, object); + }); } // load the MQTT settings -void Sensors::reload() { +void Sensor::reload() { // copy over values from MQTT so we don't keep on quering the filesystem EMSESP::esp8266React.getMqttSettingsService()->read([&](MqttSettings & settings) { mqtt_format_ = settings.mqtt_format; // single, nested or ha @@ -60,7 +65,7 @@ void Sensors::reload() { } } -void Sensors::loop() { +void Sensor::loop() { #ifndef EMSESP_STANDALONE uint32_t time_now = uuid::get_uptime(); @@ -157,7 +162,7 @@ void Sensors::loop() { #endif } -bool Sensors::temperature_convert_complete() { +bool Sensor::temperature_convert_complete() { #ifndef EMSESP_STANDALONE if (parasite_) { return true; // don't care, use the minimum time in loop @@ -171,7 +176,7 @@ bool Sensors::temperature_convert_complete() { #pragma GCC diagnostic push #pragma GCC diagnostic ignored "-Wunused-parameter" -float Sensors::get_temperature_c(const uint8_t addr[]) { +float Sensor::get_temperature_c(const uint8_t addr[]) { #ifndef EMSESP_STANDALONE if (!bus_.reset()) { LOG_ERROR(F("Bus reset failed before reading scratchpad from %s"), Device(addr).to_string().c_str()); @@ -233,20 +238,20 @@ float Sensors::get_temperature_c(const uint8_t addr[]) { #pragma GCC diagnostic pop -const std::vector Sensors::devices() const { +const std::vector Sensor::devices() const { return devices_; } -Sensors::Device::Device(const uint8_t addr[]) +Sensor::Device::Device(const uint8_t addr[]) : id_(((uint64_t)addr[0] << 56) | ((uint64_t)addr[1] << 48) | ((uint64_t)addr[2] << 40) | ((uint64_t)addr[3] << 32) | ((uint64_t)addr[4] << 24) | ((uint64_t)addr[5] << 16) | ((uint64_t)addr[6] << 8) | (uint64_t)addr[7]) { } -uint64_t Sensors::Device::id() const { +uint64_t Sensor::Device::id() const { return id_; } -std::string Sensors::Device::to_string() const { +std::string Sensor::Device::to_string() const { std::string str(20, '\0'); snprintf_P(&str[0], str.capacity() + 1, @@ -260,7 +265,7 @@ std::string Sensors::Device::to_string() const { } // check to see if values have been updated -bool Sensors::updated_values() { +bool Sensor::updated_values() { if (changed_) { changed_ = false; return true; @@ -268,9 +273,35 @@ bool Sensors::updated_values() { return false; } +bool Sensor::command_info(const char * value, const int8_t id, JsonObject & output) { + return (export_values(output)); +} + +// creates JSON doc from values +// returns false if empty +// e.g. sensors = {"sensor1":{"id":"28-EA41-9497-0E03-5F","temp":"23.30"},"sensor2":{"id":"28-233D-9497-0C03-8B","temp":"24.0"}} +bool Sensor::export_values(JsonObject & output) { + if (devices_.size() == 0) { + return false; + } + uint8_t i = 1; // sensor count + for (const auto & device : devices_) { + char s[7]; + char sensorID[10]; // sensor{1-n} + strlcpy(sensorID, "sensor", 10); + strlcat(sensorID, Helpers::itoa(s, i), 10); + JsonObject dataSensor = output.createNestedObject(sensorID); + dataSensor["id"] = device.to_string(); + dataSensor["temp"] = Helpers::render_value(s, device.temperature_c, 1); + i++; + } + + return true; +} + // send all dallas sensor values as a JSON package to MQTT // assumes there are devices -void Sensors::publish_values() { +void Sensor::publish_values() { uint8_t num_devices = devices_.size(); if (num_devices == 0) { @@ -288,7 +319,7 @@ void Sensors::publish_values() { char topic[60]; // sensors{1-n} strlcpy(topic, "sensor_", 50); // create topic, e.g. home/ems-esp/sensor_28-EA41-9497-0E03-5F strlcat(topic, device.to_string().c_str(), 60); - Mqtt::publish(topic, doc); + Mqtt::publish(topic, doc.as()); doc.clear(); // clear json doc so we can reuse the buffer again } return; @@ -342,7 +373,7 @@ void Sensors::publish_values() { config["uniq_id"] = str; snprintf_P(&topic[0], 50, PSTR("homeassistant/sensor/ems-esp/sensor%d/config"), i); - Mqtt::publish_retain(topic, config, false); // publish the config payload with no retain flag + Mqtt::publish_retain(topic, config.as(), false); // publish the config payload with no retain flag registered_ha_[i] = true; } @@ -351,9 +382,10 @@ void Sensors::publish_values() { } if ((mqtt_format_ == MQTT_format::NESTED) || (mqtt_format_ == MQTT_format::CUSTOM)) { - Mqtt::publish(F("sensors"), doc); + Mqtt::publish(F("sensors"), doc.as()); } else if (mqtt_format_ == MQTT_format::HA) { - Mqtt::publish(F("homeassistant/sensor/ems-esp/state"), doc); + Mqtt::publish(F("homeassistant/sensor/ems-esp/state"), doc.as()); } } + } // namespace emsesp \ No newline at end of file diff --git a/src/sensors.h b/src/sensor.h similarity index 93% rename from src/sensors.h rename to src/sensor.h index 8da3a18a4..e9e0b09a3 100644 --- a/src/sensors.h +++ b/src/sensor.h @@ -18,8 +18,8 @@ // code originally written by nomis - https://github.com/nomis -#ifndef EMSESP_SENSORS_H -#define EMSESP_SENSORS_H +#ifndef EMSESP_SENSOR_H +#define EMSESP_SENSOR_H #include #include @@ -36,7 +36,7 @@ namespace emsesp { -class Sensors { +class Sensor { public: class Device { public: @@ -53,8 +53,8 @@ class Sensors { bool registered_ = false; }; - Sensors() = default; - ~Sensors() = default; + Sensor() = default; + ~Sensor() = default; void start(); void loop(); @@ -62,6 +62,9 @@ class Sensors { void reload(); bool updated_values(); + bool command_info(const char * value, const int8_t id, JsonObject & output); + bool export_values(JsonObject & doc); + const std::vector devices() const; private: diff --git a/src/shower.cpp b/src/shower.cpp index 4451e0a71..f6fc1be51 100644 --- a/src/shower.cpp +++ b/src/shower.cpp @@ -129,7 +129,7 @@ void Shower::publish_values() { doc["duration"] = s; } - Mqtt::publish(F("shower_data"), doc); + Mqtt::publish(F("shower_data"), doc.as()); } } // namespace emsesp diff --git a/src/system.cpp b/src/system.cpp index eb8e70ede..7e2d33988 100644 --- a/src/system.cpp +++ b/src/system.cpp @@ -152,8 +152,16 @@ void System::start() { EMSESP::esp8266React.getWiFiSettingsService()->read( [&](WiFiSettings & wifiSettings) { LOG_INFO(F("System %s booted (EMS-ESP version %s)"), wifiSettings.hostname.c_str(), EMSESP_APP_VERSION); }); - syslog_init(); // init SysLog - set_led(); // init LED + syslog_init(); // init SysLog + set_led(); // init LED + + // these commands respond to the topic "system" and take a payload like {cmd:"", data:"", id:""} + EMSESP::emsespSettingsService.read([&](EMSESPSettings & settings) { + Command::add(EMSdevice::DeviceType::SERVICEKEY, settings.ems_bus_id, F("pin"), System::command_pin); + Command::add(EMSdevice::DeviceType::SERVICEKEY, settings.ems_bus_id, F("send"), System::command_send); + Command::add_with_json(EMSdevice::DeviceType::SERVICEKEY, F("info"), System::command_info); + }); + EMSESP::init_tx(); // start UART } @@ -241,7 +249,7 @@ void System::send_heartbeat() { doc["rxfails"] = EMSESP::rxservice_.telegram_error_count(); doc["adc"] = analog_; //analogRead(A0); - Mqtt::publish_retain(F("heartbeat"), doc, false); // send to MQTT with retain off. This will add to MQTT queue. + Mqtt::publish_retain(F("heartbeat"), doc.as(), false); // send to MQTT with retain off. This will add to MQTT queue. } // measure and moving average adc @@ -806,14 +814,14 @@ bool System::check_upgrade() { // export all settings to JSON text // http://ems-esp/api?device=system&cmd=info -String System::export_settings() { - StaticJsonDocument doc; - -#ifndef EMSESP_STANDALONE - JsonObject root = doc.to(); +// value and id are ignored +bool System::command_info(const char * value, const int8_t id, JsonObject & output) { +#ifdef EMSESP_STANDALONE + output["test"] = "testing"; +#else EMSESP::esp8266React.getWiFiSettingsService()->read([&](WiFiSettings & settings) { - JsonObject node = root.createNestedObject("WIFI"); + JsonObject node = output.createNestedObject("WIFI"); node["ssid"] = settings.ssid; // node["password"] = settings.password; node["hostname"] = settings.hostname; @@ -826,7 +834,7 @@ String System::export_settings() { }); EMSESP::esp8266React.getAPSettingsService()->read([&](APSettings & settings) { - JsonObject node = root.createNestedObject("AP"); + JsonObject node = output.createNestedObject("AP"); node["provision_mode"] = settings.provisionMode; node["ssid"] = settings.ssid; // node["password"] = settings.password; @@ -836,7 +844,7 @@ String System::export_settings() { }); EMSESP::esp8266React.getMqttSettingsService()->read([&](MqttSettings & settings) { - JsonObject node = root.createNestedObject("MQTT"); + JsonObject node = output.createNestedObject("MQTT"); node["enabled"] = settings.enabled; node["host"] = settings.host; node["port"] = settings.port; @@ -859,7 +867,7 @@ String System::export_settings() { }); EMSESP::esp8266React.getNTPSettingsService()->read([&](NTPSettings & settings) { - JsonObject node = root.createNestedObject("NTP"); + JsonObject node = output.createNestedObject("NTP"); node["enabled"] = settings.enabled; node["server"] = settings.server; node["tz_label"] = settings.tzLabel; @@ -867,18 +875,33 @@ String System::export_settings() { }); EMSESP::esp8266React.getOTASettingsService()->read([&](OTASettings & settings) { - JsonObject node = root.createNestedObject("OTA"); + JsonObject node = output.createNestedObject("OTA"); node["enabled"] = settings.enabled; node["port"] = settings.port; // node["password"] = settings.password; }); + EMSESP::emsespSettingsService.read([&](EMSESPSettings & settings) { + JsonObject node = output.createNestedObject("Settings"); + node["tx_mode"] = settings.tx_mode; + node["ems_bus_id"] = settings.ems_bus_id; + node["syslog_level"] = settings.syslog_level; + node["syslog_mark_interval"] = settings.syslog_mark_interval; + node["syslog_host"] = settings.syslog_host; + 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; + }); + #endif - - String buffer; - serializeJsonPretty(doc, buffer); - - return buffer; + return true; } + } // namespace emsesp diff --git a/src/system.h b/src/system.h index 22c13a141..e23931512 100644 --- a/src/system.h +++ b/src/system.h @@ -50,12 +50,13 @@ class System { static bool command_pin(const char * value, const int8_t id); static bool command_send(const char * value, const int8_t id); + static bool command_info(const char * value, const int8_t id, JsonObject & output); + static uint8_t free_mem(); static void upload_status(bool in_progress); static bool upload_status(); static void show_mem(const char * note); static void set_led(); - static String export_settings(); bool check_upgrade(); void syslog_init(); diff --git a/src/test/test.cpp b/src/test/test.cpp index 04507f840..2be27f10b 100644 --- a/src/test/test.cpp +++ b/src/test/test.cpp @@ -28,7 +28,7 @@ namespace emsesp { // used with the 'test' command, under su/admin void Test::run_test(uuid::console::Shell & shell, const std::string & command) { if (command == "default") { - run_test(shell, "mqtt"); // add the default test case here + run_test(shell, "cmd"); // add the default test case here } if (command.empty()) { @@ -565,6 +565,45 @@ void Test::run_test(uuid::console::Shell & shell, const std::string & command) { EMSESP::txservice_.flush_tx_queue(); } + if (command == "cmd") { + shell.printfln(F("Testing Commands...")); + + // change MQTT format + EMSESP::esp8266React.getMqttSettingsService()->updateWithoutPropagation([&](MqttSettings & mqttSettings) { + mqttSettings.mqtt_format = MQTT_format::SINGLE; + // mqttSettings.mqtt_format = MQTT_format::NESTED; + // mqttSettings.mqtt_format = MQTT_format::HA; + return StateUpdateResult::CHANGED; + }); + + EMSESP::add_context_menus(); // need to add this as it happens later in the code + shell.invoke_command("su"); + shell.invoke_command("system"); + shell.invoke_command("call"); + shell.invoke_command("call info"); + shell.invoke_command("exit"); + + char system_topic[Mqtt::MQTT_TOPIC_MAX_SIZE]; + strcpy(system_topic, "ems-esp/system"); + EMSESP::mqtt_.incoming(system_topic, "{\"cmd\":\"info\"}"); // this should fail + + // add a thermostat with 3 HCs + std::string version("1.2.3"); + EMSESP::add_device(0x10, 192, version, EMSdevice::Brand::JUNKERS); // FW120 + uart_telegram({0x90, 0x00, 0xFF, 0x00, 0x00, 0x6F, 0x00, 0xCF, 0x21, 0x2E, 0x20, 0x00, 0x2E, 0x24, + 0x03, 0x25, 0x03, 0x03, 0x01, 0x03, 0x25, 0x00, 0xC8, 0x00, 0x00, 0x11, 0x01, 0x03}); // HC1 + uart_telegram({0x90, 0x00, 0xFF, 0x00, 0x00, 0x70, 0x00, 0xCF, 0x22, 0x2F, 0x10, 0x00, 0x2E, 0x24, + 0x03, 0x25, 0x03, 0x03, 0x01, 0x03, 0x25, 0x00, 0xC8, 0x00, 0x00, 0x11, 0x01, 0x03}); // HC2 + uart_telegram({0x90, 0x00, 0xFF, 0x00, 0x00, 0x71, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}); // HC3 + EMSESP::add_context_menus(); // need to add this as it happens later in the code + shell.invoke_command("thermostat"); + shell.invoke_command("show"); + shell.invoke_command("call"); + shell.invoke_command("call info"); + shell.invoke_command("exit"); + shell.invoke_command("show mqtt"); + } + if (command == "pin") { shell.printfln(F("Testing pin...")); @@ -579,6 +618,11 @@ void Test::run_test(uuid::console::Shell & shell, const std::string & command) { if (command == "mqtt") { shell.printfln(F("Testing MQTT...")); + // EMSESP::esp8266React.getMqttSettingsService()->updateWithoutPropagation([&](MqttSettings & mqttSettings) { + // mqttSettings.mqtt_format = MQTT_format::SINGLE; + // return StateUpdateResult::CHANGED; + // }); + // add a boiler // question: do we need to set the mask? std::string version("1.2.3"); @@ -700,6 +744,8 @@ void Test::run_test(uuid::console::Shell & shell, const std::string & command) { // check for error "[emsesp] No telegram type handler found for ID 0x255 (src 0x20, dest 0x00)" rx_telegram({0xA0, 0x00, 0xFF, 0x00, 0x01, 0x55, 0x00, 0x1A}); + + EMSESP::add_context_menus(); // need to add this as it happens later in the code } // finally dump to console