From f46b002c5aed6d49e9e375de682c489a55be1606 Mon Sep 17 00:00:00 2001 From: Proddy Date: Tue, 29 Aug 2023 22:32:36 +0200 Subject: [PATCH] API call for shower coldshot - #1267 --- src/locale_common.h | 1 + src/locale_translations.h | 1 + src/shower.cpp | 103 ++++++++++++++++++++++++++++---------- src/shower.h | 9 ++-- src/test/test.cpp | 29 ++++++++--- src/test/test.h | 4 +- 6 files changed, 109 insertions(+), 38 deletions(-) diff --git a/src/locale_common.h b/src/locale_common.h index 3e373dab1..3eb878b5e 100644 --- a/src/locale_common.h +++ b/src/locale_common.h @@ -80,6 +80,7 @@ MAKE_WORD(info) MAKE_WORD(settings) MAKE_WORD(value) MAKE_WORD(entities) +MAKE_WORD(coldshot) // device types - lowercase, used in MQTT MAKE_WORD(boiler) diff --git a/src/locale_translations.h b/src/locale_translations.h index 8814ad57c..4a9f2e314 100644 --- a/src/locale_translations.h +++ b/src/locale_translations.h @@ -70,6 +70,7 @@ MAKE_WORD_TRANSLATION(system_info_cmd, "show system status", "Zeige System-Statu MAKE_WORD_TRANSLATION(schedule_cmd, "enable schedule item", "Aktiviere Zeitplan", "activeer tijdschema item", "", "aktywuj wybrany harmonogram", "", "", "program öğesini etkinleştir", "abilitare l'elemento programmato") // TODO translate MAKE_WORD_TRANSLATION(entity_cmd, "set custom value on ems", "Sende eigene Entitäten zu EMS", "verstuur custom waarde naar EMS", "", "wyślij własną wartość na EMS", "", "", "", "imposta valori personalizzati su EMS") // TODO translate MAKE_WORD_TRANSLATION(commands_response, "get response", "Hole Antwort", "Verzoek om antwoord", "", "", "", "", "") // TODO translate +MAKE_WORD_TRANSLATION(coldshot_cmd, "send a cold shot of water", "", "", "", "", "", "", "", "") // TODO translate // tags MAKE_WORD_TRANSLATION(tag_boiler_data_ww, "dhw", "WW", "dhw", "VV", "CWU", "dhw", "ecs", "SKS", "dhw") diff --git a/src/shower.cpp b/src/shower.cpp index 06432200f..f596bd95d 100644 --- a/src/shower.cpp +++ b/src/shower.cpp @@ -22,6 +22,8 @@ namespace emsesp { uuid::log::Logger Shower::logger_{F_(shower), uuid::log::Facility::CONSOLE}; +static bool force_coldshot = false; + void Shower::start() { EMSESP::webSettingsService.read([&](WebSettings & settings) { shower_timer_ = settings.shower_timer; @@ -30,7 +32,27 @@ void Shower::start() { shower_alert_coldshot_ = settings.shower_alert_coldshot * 1000; // convert from seconds }); - set_shower_state(false, true); // turns shower to off and creates HA topic if not already done + Command::add( + EMSdevice::DeviceType::BOILER, + F_(coldshot), + [&](const char * value, const int8_t id, JsonObject & output) { + LOG_INFO("Forcing coldshot..."); + if (shower_state_) { + output["message"] = "OK"; + force_coldshot = true; + } else { + output["message"] = "Coldshot failed. Shower not active"; + LOG_WARNING("Coldshot failed. Shower not active"); + force_coldshot = false; + } + return true; + }, + FL_(coldshot_cmd), + CommandFlag::ADMIN_ONLY); + + if (shower_timer_) { + set_shower_state(false, true); // turns shower to off and creates HA topic if not already done + } } void Shower::loop() { @@ -57,10 +79,10 @@ void Shower::loop() { // first check to see if hot water has been on long enough to be recognized as a Shower/Bath if (!shower_state_ && (time_now - timer_start_) > SHOWER_MIN_DURATION) { set_shower_state(true); - LOG_DEBUG("[Shower] hot water still running, starting shower timer"); + LOG_DEBUG("hot water still running, starting shower timer"); } // check if the shower has been on too long - else if ((time_now - timer_start_) > shower_alert_trigger_) { + else if ((shower_alert_ && ((time_now - timer_start_) > shower_alert_trigger_)) || force_coldshot) { shower_alert_start(); } } @@ -79,11 +101,11 @@ void Shower::loop() { if (duration_ > SHOWER_MIN_DURATION) { StaticJsonDocument doc; - char s[50]; - snprintf(s, 50, "%d minutes and %d seconds", (uint8_t)(duration_ / 60000), (uint8_t)((duration_ / 1000) % 60)); - doc["duration"] = s; + // char s[50]; + // snprintf(s, 50, "%02u:%02u:%02u", (uint8_t)(duration_ / 3600000UL), (uint8_t)(duration_ / 60000UL), (uint8_t)((duration_ / 1000UL) % 60)); + doc["duration"] = (uint8_t)(duration_ / 1000UL); // seconds Mqtt::queue_publish("shower_data", doc.as()); - LOG_DEBUG("[Shower] finished with duration %d", duration_); + LOG_INFO("finished with duration %d", duration_); } } @@ -106,21 +128,22 @@ void Shower::loop() { } } +// turn off hot water to send a shot of cold +void Shower::shower_alert_start() { + LOG_DEBUG("Shower Alert started"); + (void)Command::call(EMSdevice::DeviceType::BOILER, "wwtapactivated", "false"); + doing_cold_shot_ = true; + force_coldshot = false; + alert_timer_start_ = uuid::get_uptime(); // timer starts now +} + // turn back on the hot water for the shower void Shower::shower_alert_stop() { if (doing_cold_shot_) { LOG_DEBUG("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("Shower Alert started"); - (void)Command::call(EMSdevice::DeviceType::BOILER, "wwtapactivated", "false"); - doing_cold_shot_ = true; - alert_timer_start_ = uuid::get_uptime(); // timer starts now + force_coldshot = false; } } @@ -128,15 +151,11 @@ void Shower::shower_alert_start() { // 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; - } - // sets the state shower_state_ = state; // only publish if that state has changed - static bool old_shower_state_; + static bool old_shower_state_ = false; if ((shower_state_ == old_shower_state_) && !force) { return; } @@ -149,10 +168,15 @@ void Shower::set_shower_state(bool state, bool force) { // send out HA MQTT Discovery config topic if ((Mqtt::ha_enabled()) && (!ha_configdone_ || force)) { StaticJsonDocument doc; + char topic[Mqtt::MQTT_TOPIC_MAX_SIZE]; + char str[70]; + char stat_t[50]; + // + // shower_active topic + // doc["name"] = "Shower Active"; - char str[70]; if (Mqtt::entity_format() == Mqtt::entityFormat::MULTI_SHORT) { snprintf(str, sizeof(str), "%s_shower_active", Mqtt::basename().c_str()); } else { @@ -161,7 +185,6 @@ void Shower::set_shower_state(bool state, bool force) { doc["uniq_id"] = str; doc["object_id"] = str; - char stat_t[50]; snprintf(stat_t, sizeof(stat_t), "%s/shower_active", Mqtt::basename().c_str()); doc["stat_t"] = stat_t; @@ -181,13 +204,39 @@ void Shower::set_shower_state(bool state, bool force) { JsonArray ids = dev.createNestedArray("ids"); ids.add(Mqtt::basename()); - // add "availability" section - Mqtt::add_avty_to_doc(stat_t, doc.as()); + Mqtt::add_avty_to_doc(stat_t, doc.as()); // add "availability" section - char topic[Mqtt::MQTT_TOPIC_MAX_SIZE]; snprintf(topic, sizeof(topic), "binary_sensor/%s/shower_active/config", Mqtt::basename().c_str()); - ha_configdone_ = Mqtt::queue_ha(topic, doc.as()); // publish the config payload with retain flag + + // + // shower_duaration topic + // + doc.clear(); + + snprintf(str, sizeof(str), "%s_shower_duration", Mqtt::basename().c_str()); + + doc["uniq_id"] = str; + doc["object_id"] = str; + + snprintf(stat_t, sizeof(stat_t), "%s/shower_data", Mqtt::basename().c_str()); + doc["stat_t"] = stat_t; + + doc["name"] = "Shower Duration"; + doc["val_tpl"] = "{{value_json.duration if value_json.duration is defined else 0}}"; + doc["unit_of_meas"] = "s"; + doc["stat_cla"] = "measurement"; + doc["dev_cla"] = "duration"; + doc["ent_cat"] = "diagnostic"; + + JsonObject dev2 = doc.createNestedObject("dev"); + JsonArray ids2 = dev2.createNestedArray("ids"); + ids2.add(Mqtt::basename()); + + Mqtt::add_avty_to_doc(stat_t, doc.as(), "value_json.duration is defined"); // add "availability" section + + snprintf(topic, sizeof(topic), "sensor/%s/shower_duration/config", Mqtt::basename().c_str()); + Mqtt::queue_ha(topic, doc.as()); // publish the config payload with retain flag } } diff --git a/src/shower.h b/src/shower.h index e9e866c52..bb2013b6a 100644 --- a/src/shower.h +++ b/src/shower.h @@ -30,6 +30,9 @@ class Shower { void set_shower_state(bool state, bool force = false); + // commands + static bool command_coldshot(const char * value, const int8_t id); + private: static uuid::log::Logger logger_; @@ -46,9 +49,9 @@ class Shower { uint32_t shower_alert_coldshot_; // default 10 seconds for cold water before turning back hot water bool ha_configdone_ = false; // for HA MQTT Discovery bool shower_state_; - uint32_t timer_start_; // ms - uint32_t timer_pause_; // ms - uint32_t duration_; // ms + uint32_t timer_start_; // ms + uint32_t timer_pause_; // ms + uint32_t duration_; // ms // cold shot uint32_t alert_timer_start_; // ms diff --git a/src/test/test.cpp b/src/test/test.cpp index 556500516..0bb5c8754 100644 --- a/src/test/test.cpp +++ b/src/test/test.cpp @@ -266,7 +266,7 @@ void Test::run_test(uuid::console::Shell & shell, const std::string & cmd, const shell.printfln("Testing adding a general boiler & thermostat..."); run_test("general"); // shell.invoke_command("show devices"); - // shell.invoke_command("show values"); + shell.invoke_command("show values"); shell.invoke_command("call system publish"); // shell.invoke_command("show mqtt"); ok = true; @@ -280,6 +280,21 @@ void Test::run_test(uuid::console::Shell & shell, const std::string & cmd, const ok = true; } + if (command == "coldshot") { + shell.printfln("Testing coldshot..."); + run_test("general"); + +#ifdef EMSESP_STANDALONE + AsyncWebServerRequest request; + request.method(HTTP_GET); + request.url("/api/boiler/coldshot"); + EMSESP::webAPIService.webAPIService_get(&request); +#else + shell.invoke_command("call boiler coldshot"); +#endif + ok = true; + } + if (command == "string2minutes") { shell.printfln("Testing string2minutes()..."); std::string time_s = "12:00"; @@ -1005,10 +1020,10 @@ void Test::run_test(uuid::console::Shell & shell, const std::string & cmd, const 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 - EMSESP::mqtt_.incoming("ems-esp/thermostat/mode"); // empty payload + EMSESP::mqtt_.incoming("ems-esp/boiler/syspress"); // empty payload + EMSESP::mqtt_.incoming("ems-esp/thermostat/mode"); // empty payload EMSESP::mqtt_.incoming("ems-esp/system/publish"); - EMSESP::mqtt_.incoming("ems-esp/thermostat/seltemp"); // empty payload + EMSESP::mqtt_.incoming("ems-esp/thermostat/seltemp"); // empty payload EMSESP::mqtt_.incoming("ems-esp/boiler/wwseltemp", "59"); EMSESP::mqtt_.incoming("ems-esp/boiler/wwseltemp"); @@ -1204,7 +1219,7 @@ void Test::run_test(uuid::console::Shell & shell, const std::string & cmd, const uart_telegram("30 00 FF 0A 02 6A 04"); // SM100 pump on 1 uart_telegram("30 00 FF 00 02 64 00 00 00 04 00 00 FF 00 00 1E 0B 09 64 00 00 00 00"); // SM100 modulation - uart_telegram("30 00 FF 0A 02 6A 03"); // SM100 pump off 0 + uart_telegram("30 00 FF 0A 02 6A 03"); // SM100 pump off 0 shell.invoke_command("show"); ok = true; @@ -1446,7 +1461,7 @@ void Test::run_test(uuid::console::Shell & shell, const std::string & cmd, const shell.invoke_command("call"); shell.invoke_command("call system info"); - EMSESP::mqtt_.incoming("ems-esp/system", "{\"cmd\":\"info\"}"); // this should fail + EMSESP::mqtt_.incoming("ems-esp/system", "{\"cmd\":\"info\"}"); // this should fail EMSESP::mqtt_.incoming("ems-esp/thermostat", "{\"cmd\":\"temp\",\"data\":23.45}"); // this should work just fine EMSESP::mqtt_.incoming("ems-esp/thermostat", "{\"cmd\":\"TeMP\",\"data\":23.45}"); // test mix cased cmd @@ -1559,7 +1574,7 @@ void Test::run_test(uuid::console::Shell & shell, const std::string & cmd, const // EMSESP::mqtt_.incoming(system_topic, "{\"cmd\":\"pin\",\"id\":12,\"data\":\"1\"}"); EMSESP::mqtt_.incoming(thermostat_topic, "{\"cmd\":\"wwmode\",\"data\":\"auto\"}"); - EMSESP::mqtt_.incoming(thermostat_topic, "{\"cmd\":\"mode\",\"data\":\"typo\",\"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\":\"seltemp\",\"data\":19.5,\"hc\":1}"); // data as number diff --git a/src/test/test.h b/src/test/test.h index 36823b166..b0c495a8e 100644 --- a/src/test/test.h +++ b/src/test/test.h @@ -26,11 +26,12 @@ namespace emsesp { +// #define EMSESP_DEBUG_DEFAULT "general" + // #define EMSESP_DEBUG_DEFAULT "thermostat" // #define EMSESP_DEBUG_DEFAULT "solar" // #define EMSESP_DEBUG_DEFAULT "web" // #define EMSESP_DEBUG_DEFAULT "mqtt" -#define EMSESP_DEBUG_DEFAULT "general" // #define EMSESP_DEBUG_DEFAULT "boiler" // #define EMSESP_DEBUG_DEFAULT "mqtt2" // #define EMSESP_DEBUG_DEFAULT "mqtt_nested" @@ -53,6 +54,7 @@ namespace emsesp { // #define EMSESP_DEBUG_DEFAULT "custom" // #define EMSESP_DEBUG_DEFAULT "entity_dump" // #define EMSESP_DEBUG_DEFAULT "memory" +#define EMSESP_DEBUG_DEFAULT "coldshot" class Test { public: