From 3fe088483dfdaa25692016da0457352b0b83db99 Mon Sep 17 00:00:00 2001 From: "Bronk, Mateusz" Date: Sat, 19 Nov 2022 17:07:20 +0100 Subject: [PATCH 1/3] IRMQTTServer extended with new A/C common fields Adds iFeel/sensorTemp/command support. Signed-off-by: Mateusz Bronk --- examples/IRMQTTServer/IRMQTTServer.h | 20 ++++- examples/IRMQTTServer/IRMQTTServer.ino | 105 ++++++++++++++++++++++++- 2 files changed, 119 insertions(+), 6 deletions(-) diff --git a/examples/IRMQTTServer/IRMQTTServer.h b/examples/IRMQTTServer/IRMQTTServer.h index 716aab60b..67b12a8e8 100644 --- a/examples/IRMQTTServer/IRMQTTServer.h +++ b/examples/IRMQTTServer/IRMQTTServer.h @@ -253,6 +253,9 @@ const uint16_t kMinUnknownSize = 2 * 10; #define KEY_JSON "json" #define KEY_RESEND "resend" #define KEY_VCC "vcc" +#define KEY_COMMAND "command" +#define KEY_SENSORTEMP "sensortemp" +#define KEY_IFEEL "ifeel" // HTML arguments we will parse for IR code information. #define KEY_TYPE "type" // KEY_PROTOCOL is also checked too. @@ -260,11 +263,17 @@ const uint16_t kMinUnknownSize = 2 * 10; #define KEY_BITS "bits" #define KEY_REPEAT "repeats" #define KEY_CHANNEL "channel" // Which IR TX channel to send on. +#define KEY_SENSORTEMP_DISABLED "sensortemp_disabled" // For HTML form only, + // not sent via MQTT + // nor JSON // GPIO html/config keys #define KEY_TX_GPIO "tx" #define KEY_RX_GPIO "rx" +// Miscelaneous constants +#define TOGGLE_JS_FN_NAME "ToggleInputBasedOnCheckbox" + // Text for Last Will & Testament status messages. const char* const kLwtOnline = "Online"; const char* const kLwtOffline = "Offline"; @@ -358,7 +367,8 @@ static const char kClimateTopics[] PROGMEM = "(" KEY_PROTOCOL "|" KEY_MODEL "|" KEY_POWER "|" KEY_MODE "|" KEY_TEMP "|" KEY_FANSPEED "|" KEY_SWINGV "|" KEY_SWINGH "|" KEY_QUIET "|" KEY_TURBO "|" KEY_LIGHT "|" KEY_BEEP "|" KEY_ECONO "|" KEY_SLEEP "|" - KEY_FILTER "|" KEY_CLEAN "|" KEY_CELSIUS "|" KEY_RESEND + KEY_FILTER "|" KEY_CLEAN "|" KEY_CELSIUS "|" KEY_RESEND "|" KEY_COMMAND "|" + "|" KEY_SENSORTEMP "|" KEY_IFEEL #if MQTT_CLIMATE_JSON "|" KEY_JSON #endif // MQTT_CLIMATE_JSON @@ -367,6 +377,7 @@ static const char* const kMqttTopics[] = { KEY_PROTOCOL, KEY_MODEL, KEY_POWER, KEY_MODE, KEY_TEMP, KEY_FANSPEED, KEY_SWINGV, KEY_SWINGH, KEY_QUIET, KEY_TURBO, KEY_LIGHT, KEY_BEEP, KEY_ECONO, KEY_SLEEP, KEY_FILTER, KEY_CLEAN, KEY_CELSIUS, KEY_RESEND, + KEY_COMMAND, KEY_SENSORTEMP, KEY_IFEEL KEY_JSON}; // KEY_JSON needs to be the last one. @@ -410,7 +421,8 @@ int8_t getDefaultTxGpio(void); String genStatTopic(const uint16_t channel = 0); String listOfTxGpios(void); bool hasUnsafeHTMLChars(String input); -String htmlHeader(const String title, const String h1_text = ""); +String htmlHeader(const String title, const String h1_text = "", + const String headScriptsJS = ""); String htmlEnd(void); String htmlButton(const String url, const String button, const String text = ""); @@ -418,9 +430,13 @@ String htmlMenu(void); void handleRoot(void); String addJsReloadUrl(const String url, const uint16_t timeout_s, const bool notify); +String getJsToggleCheckbox(const String functionName = TOGGLE_JS_FN_NAME); void handleExamples(void); String htmlOptionItem(const String value, const String text, bool selected); String htmlSelectBool(const String name, const bool def); +String htmlDisableCheckbox(const String name, const String targetControlId, + const bool checked, + const String toggleJsFnName = TOGGLE_JS_FN_NAME); String htmlSelectClimateProtocol(const String name, const decode_type_t def); String htmlSelectAcStateProtocol(const String name, const decode_type_t def, const bool simple); diff --git a/examples/IRMQTTServer/IRMQTTServer.ino b/examples/IRMQTTServer/IRMQTTServer.ino index 6701ef4bf..4a0fc2ee0 100644 --- a/examples/IRMQTTServer/IRMQTTServer.ino +++ b/examples/IRMQTTServer/IRMQTTServer.ino @@ -955,6 +955,18 @@ String htmlSelectBool(const String name, const bool def) { return html; } +String htmlDisableCheckbox(const String name, const String targetControlId, + const bool checked, const String toggleJsFnName) { + String html = F(""); + return html; +} + String htmlSelectClimateProtocol(const String name, const decode_type_t def) { String html = F(""); + for (uint8_t i = 0; + i <= (int8_t)stdAc::ac_command_t::kLastAcCommandEnum; + i++) { + String mode = IRac::commandTypeToString((stdAc::ac_command_t)i); + html += htmlOptionItem(mode, mode, (stdAc::ac_command_t)i == def); + } + html += F(""); + return html; +} + String htmlSelectUint(const String name, const uint16_t max, const uint16_t def) { String html = F("" + "" D_STR_SENSORTEMP "" + "") + + htmlDisableCheckbox(KEY_SENSORTEMP_DISABLED, KEY_SENSORTEMP, + noSensorTemp) + + F("" "" D_STR_FAN "") + htmlSelectFanspeed(KEY_FANSPEED, climate[chan]->next.fanspeed) + F("" @@ -1138,6 +1193,9 @@ void handleAirCon(void) { "" D_STR_QUIET "") + htmlSelectBool(KEY_QUIET, climate[chan]->next.quiet) + F("" + "" D_STR_IFEEL "") + + htmlSelectBool(KEY_IFEEL, climate[chan]->next.iFeel) + + F("" "" D_STR_TURBO "") + htmlSelectBool(KEY_TURBO, climate[chan]->next.turbo) + F("" @@ -2976,6 +3034,7 @@ void sendJsonState(const stdAc::state_t state, const String topic, DynamicJsonDocument json(kJsonAcStateMaxSize); json[KEY_PROTOCOL] = typeToString(state.protocol); json[KEY_MODEL] = state.model; + json[KEY_COMMAND] = IRac::commandToString(state.command); json[KEY_POWER] = IRac::boolToString(state.power); json[KEY_MODE] = IRac::opmodeToString(state.mode, ha_mode); // Home Assistant wants mode to be off if power is also off & vice-versa. @@ -2985,10 +3044,12 @@ void sendJsonState(const stdAc::state_t state, const String topic, } json[KEY_CELSIUS] = IRac::boolToString(state.celsius); json[KEY_TEMP] = state.degrees; + json[KEY_SENSORTEMP] = state.sensorTemperature; json[KEY_FANSPEED] = IRac::fanspeedToString(state.fanspeed); json[KEY_SWINGV] = IRac::swingvToString(state.swingv); json[KEY_SWINGH] = IRac::swinghToString(state.swingh); json[KEY_QUIET] = IRac::boolToString(state.quiet); + json[KEY_IFEEL] = IRac::boolToString(state.iFeel); json[KEY_TURBO] = IRac::boolToString(state.turbo); json[KEY_ECONO] = IRac::boolToString(state.econo); json[KEY_LIGHT] = IRac::boolToString(state.light); @@ -3026,6 +3087,10 @@ stdAc::state_t jsonToState(const stdAc::state_t current, const char *str) { result.model = IRac::strToModel(json[KEY_MODEL].as()); else if (validJsonInt(json, KEY_MODEL)) result.model = json[KEY_MODEL]; + if (validJsonStr(json, KEY_COMMAND)) + result.command = IRac::strToCommand(json[KEY_COMMAND].as()); + else if (validJsonInt(json, KEY_COMMAND)) + result.command = json[KEY_COMMAND]; if (validJsonStr(json, KEY_MODE)) result.mode = IRac::strToOpmode(json[KEY_MODE]); if (validJsonStr(json, KEY_FANSPEED)) @@ -3036,10 +3101,14 @@ stdAc::state_t jsonToState(const stdAc::state_t current, const char *str) { result.swingh = IRac::strToSwingH(json[KEY_SWINGH]); if (json.containsKey(KEY_TEMP)) result.degrees = json[KEY_TEMP]; + if (json.containsKey(KEY_SENSORTEMP)) + result.sensorTemperature = json[KEY_SENSORTEMP]; if (validJsonInt(json, KEY_SLEEP)) result.sleep = json[KEY_SLEEP]; if (validJsonStr(json, KEY_POWER)) result.power = IRac::strToBool(json[KEY_POWER]); + if (validJsonStr(json, KEY_IFEEL)) + result.iFeel = IRac::strToBool(json[KEY_IFEEL]); if (validJsonStr(json, KEY_QUIET)) result.quiet = IRac::strToBool(json[KEY_QUIET]); if (validJsonStr(json, KEY_TURBO)) @@ -3071,6 +3140,8 @@ void updateClimate(stdAc::state_t *state, const String str, state->protocol = strToDecodeType(payload.c_str()); } else if (str.equals(prefix + F(KEY_MODEL))) { state->model = IRac::strToModel(payload.c_str()); + } else if (str.equals(prefix + F(KEY_COMMAND))) { + state->command = IRac::strToCommandType(payload.c_str()); } else if (str.equals(prefix + F(KEY_POWER))) { state->power = IRac::strToBool(payload.c_str()); #if MQTT_CLIMATE_HA_MODE @@ -3085,12 +3156,24 @@ void updateClimate(stdAc::state_t *state, const String str, #endif // MQTT_CLIMATE_HA_MODE } else if (str.equals(prefix + F(KEY_TEMP))) { state->degrees = payload.toFloat(); + } else if (str.equals(prefix + F(KEY_SENSORTEMP))) { + state->sensorTemperature = payload.toFloat(); + } else if (str.equals(prefix + F(KEY_SENSORTEMP_DISABLED))) { + // The "disabled" html form field appears after the actual sensorTemp field + // and the spec guarantees the form POST field order preserves body order + // => this will always execute after KEY_SENSORTEMP has been parsed already + if (IRac::strToBool(payload.c_str())) { + // UI control was disabled, ignore the value + state->sensorTemperature = kNoTempValue; + } } else if (str.equals(prefix + F(KEY_FANSPEED))) { state->fanspeed = IRac::strToFanspeed(payload.c_str()); } else if (str.equals(prefix + F(KEY_SWINGV))) { state->swingv = IRac::strToSwingV(payload.c_str()); } else if (str.equals(prefix + F(KEY_SWINGH))) { state->swingh = IRac::strToSwingH(payload.c_str()); + } else if (str.equals(prefix + F(KEY_IFEEL))) { + state->iFeel = IRac::strToBool(payload.c_str()); } else if (str.equals(prefix + F(KEY_QUIET))) { state->quiet = IRac::strToBool(payload.c_str()); } else if (str.equals(prefix + F(KEY_TURBO))) { @@ -3128,6 +3211,11 @@ bool sendClimate(const String topic_prefix, const bool retain, diff = true; success &= sendInt(topic_prefix + KEY_MODEL, next.model, retain); } + if (prev.command != next.command || forceMQTT) { + String command_str = IRac::commandTypeToString(next.command); + diff = true; + success &= sendString(topic_prefix + KEY_COMMAND, command_str, retain); + } #ifdef MQTT_CLIMATE_HA_MODE String mode_str = IRac::opmodeToString(next.mode, MQTT_CLIMATE_HA_MODE); #else // MQTT_CLIMATE_HA_MODE @@ -3161,6 +3249,11 @@ bool sendClimate(const String topic_prefix, const bool retain, diff = true; success &= sendBool(topic_prefix + KEY_CELSIUS, next.celsius, retain); } + if (prev.sensorTemperature != next.sensorTemperature || forceMQTT) { + diff = true; + success &= sendFloat(topic_prefix + KEY_SENSORTEMP, + next.sensorTemperature, retain); + } if (prev.fanspeed != next.fanspeed || forceMQTT) { diff = true; success &= sendString(topic_prefix + KEY_FANSPEED, @@ -3176,6 +3269,10 @@ bool sendClimate(const String topic_prefix, const bool retain, success &= sendString(topic_prefix + KEY_SWINGH, IRac::swinghToString(next.swingh), retain); } + if (prev.iFeel != next.iFeel || forceMQTT) { + diff = true; + success &= sendBool(topic_prefix + KEY_IFEEL, next.iFeel, retain); + } if (prev.quiet != next.quiet || forceMQTT) { diff = true; success &= sendBool(topic_prefix + KEY_QUIET, next.quiet, retain); From d2960b798e05db03848a22daccb072ce272e3c59 Mon Sep 17 00:00:00 2001 From: Mateusz Bronk Date: Sat, 31 Dec 2022 21:12:14 +0100 Subject: [PATCH 2/3] IRMQTTServer build fix on Windows (naive) Signed-off-by: Mateusz Bronk --- examples/IRMQTTServer/IRMQTTServer.ino | 50 +++++++++++++------------- 1 file changed, 25 insertions(+), 25 deletions(-) diff --git a/examples/IRMQTTServer/IRMQTTServer.ino b/examples/IRMQTTServer/IRMQTTServer.ino index 4a0fc2ee0..7bfcae137 100644 --- a/examples/IRMQTTServer/IRMQTTServer.ino +++ b/examples/IRMQTTServer/IRMQTTServer.ino @@ -947,7 +947,7 @@ void handleExamples(void) { #endif // EXAMPLES_ENABLE String htmlSelectBool(const String name, const bool def) { - String html = F(""); for (uint16_t i = 0; i < 2; i++) html += htmlOptionItem(IRac::boolToString(i), IRac::boolToString(i), i == def); @@ -957,18 +957,18 @@ String htmlSelectBool(const String name, const bool def) { String htmlDisableCheckbox(const String name, const String targetControlId, const bool checked, const String toggleJsFnName) { - String html = F(""); + html += "/>"); return html; } String htmlSelectClimateProtocol(const String name, const decode_type_t def) { - String html = F(""); for (uint8_t i = 1; i <= decode_type_t::kLastDecodeType; i++) { if (IRac::isProtocolSupported((decode_type_t)i)) { html += htmlOptionItem(String(i), typeToString((decode_type_t)i), @@ -980,7 +980,7 @@ String htmlSelectClimateProtocol(const String name, const decode_type_t def) { } String htmlSelectModel(const String name, const int16_t def) { - String html = F(""); for (int16_t i = -1; i <= 6; i++) { String num = String(i); String text; @@ -997,7 +997,7 @@ String htmlSelectModel(const String name, const int16_t def) { } String htmlSelectCommandType(const String name, const stdAc::ac_command_t def) { - String html = F(""); for (uint8_t i = 0; i <= (int8_t)stdAc::ac_command_t::kLastAcCommandEnum; i++) { @@ -1010,7 +1010,7 @@ String htmlSelectCommandType(const String name, const stdAc::ac_command_t def) { String htmlSelectUint(const String name, const uint16_t max, const uint16_t def) { - String html = F(""); for (uint16_t i = 0; i < max; i++) { String num = String(i); html += htmlOptionItem(num, num, i == def); @@ -1021,7 +1021,7 @@ String htmlSelectUint(const String name, const uint16_t max, String htmlSelectGpio(const String name, const int16_t def, const int8_t list[], const int16_t length) { - String html = F(": "); for (int16_t i = 0; i < length; i++) { String num = String(list[i]); html += htmlOptionItem(num, list[i] == kGpioUnused ? F("Unused") : num, @@ -1033,7 +1033,7 @@ String htmlSelectGpio(const String name, const int16_t def, } String htmlSelectMode(const String name, const stdAc::opmode_t def) { - String html = F(""); for (int8_t i = -1; i <= (int8_t)stdAc::opmode_t::kLastOpmodeEnum; i++) { String mode = IRac::opmodeToString((stdAc::opmode_t)i); html += htmlOptionItem(mode, mode, (stdAc::opmode_t)i == def); @@ -1043,7 +1043,7 @@ String htmlSelectMode(const String name, const stdAc::opmode_t def) { } String htmlSelectFanspeed(const String name, const stdAc::fanspeed_t def) { - String html = F(""); for (int8_t i = 0; i <= (int8_t)stdAc::fanspeed_t::kLastFanspeedEnum; i++) { String speed = IRac::fanspeedToString((stdAc::fanspeed_t)i); html += htmlOptionItem(speed, speed, (stdAc::fanspeed_t)i == def); @@ -1053,7 +1053,7 @@ String htmlSelectFanspeed(const String name, const stdAc::fanspeed_t def) { } String htmlSelectSwingv(const String name, const stdAc::swingv_t def) { - String html = F(""); for (int8_t i = -1; i <= (int8_t)stdAc::swingv_t::kLastSwingvEnum; i++) { String swing = IRac::swingvToString((stdAc::swingv_t)i); html += htmlOptionItem(swing, swing, (stdAc::swingv_t)i == def); @@ -1063,7 +1063,7 @@ String htmlSelectSwingv(const String name, const stdAc::swingv_t def) { } String htmlSelectSwingh(const String name, const stdAc::swingh_t def) { - String html = F(""); for (int8_t i = -1; i <= (int8_t)stdAc::swingh_t::kLastSwinghEnum; i++) { String swing = IRac::swinghToString((stdAc::swingh_t)i); html += htmlOptionItem(swing, swing, (stdAc::swingh_t)i == def); @@ -1110,7 +1110,7 @@ String htmlButton(const String url, const String button, const String text) { String getJsToggleCheckbox(const String functionName) { const String javascript = - F(" function ") + functionName + F("(checkbox, targetInputId) {\n" + String(F(" function ")) + functionName + F("(checkbox, targetInputId) {\n" " var targetControl = document.getElementById(targetInputId);\n" " targetControl.disabled = checkbox.checked;\n" " if (!targetControl.disabled) { targetControl.focus(); }\n" @@ -1124,10 +1124,10 @@ void handleAirCon(void) { getJsToggleCheckbox()); html += htmlMenu(); if (kNrOfIrTxGpios > 1) { - html += F("
" "" - "" @@ -1137,11 +1137,11 @@ void handleAirCon(void) { } if (climate[chan] != NULL) { bool noSensorTemp = (climate[chan]->next.sensorTemperature == kNoTempValue); - html += F("

Current Settings

" + html += String(F("

Current Settings

" "" - "") + + "") + F("
Climate #") + + "
Climate #")) + htmlSelectUint(KEY_CHANNEL, kNrOfIrTxGpios, chan) + F("" "
" "
" D_STR_PROTOCOL "") + htmlSelectClimateProtocol(KEY_PROTOCOL, @@ -1344,8 +1344,8 @@ void handleInfo(void) { String html = htmlHeader(F("IR MQTT server info")); html += htmlMenu(); html += - F("

General

" - "

Hostname: ") + String(Hostname) + F("
" + String(F("

General

" + "

Hostname: ")) + String(Hostname) + F("
" "IP address: ") + WiFi.localIP().toString() + F("
" "MAC address: ") + WiFi.macAddress() + F("
" "Booted: ") + timeSince(1) + F("
") + @@ -2701,8 +2701,8 @@ void mqttCallback(char* topic, byte* payload, unsigned int length) { void sendMQTTDiscovery(const char *topic) { if (mqtt_client.publish( topic, String( - F("{" - "\"~\":\"") + MqttClimate + F("\"," + String(F("{" + "\"~\":\"")) + MqttClimate + F("\"," "\"name\":\"") + MqttHAName + F("\"," #if (!MQTT_CLIMATE_HA_MODE) // Typically we don't need or use the power command topic if we are using @@ -2778,8 +2778,8 @@ void loop(void) { boot = false; } else { mqttLog(String( - F("IRMQTTServer just (re)connected to MQTT. " - "Lost connection about ") + String(F("IRMQTTServer just (re)connected to MQTT. " + "Lost connection about ")) + timeSince(lastConnectedTime)).c_str()); } lastConnectedTime = now; From c17fe218d0b50a3ebbb150343d8354115069ee7a Mon Sep 17 00:00:00 2001 From: David Conran Date: Thu, 12 Jan 2023 09:20:06 +1000 Subject: [PATCH 3/3] Review fixed Fix typo and bump version number as this is a significant change of operation & functionality --- examples/IRMQTTServer/IRMQTTServer.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/IRMQTTServer/IRMQTTServer.h b/examples/IRMQTTServer/IRMQTTServer.h index 67b12a8e8..ef6cd5bcf 100644 --- a/examples/IRMQTTServer/IRMQTTServer.h +++ b/examples/IRMQTTServer/IRMQTTServer.h @@ -271,7 +271,7 @@ const uint16_t kMinUnknownSize = 2 * 10; #define KEY_TX_GPIO "tx" #define KEY_RX_GPIO "rx" -// Miscelaneous constants +// Miscellaneous constants #define TOGGLE_JS_FN_NAME "ToggleInputBasedOnCheckbox" // Text for Last Will & Testament status messages. @@ -299,7 +299,7 @@ const uint16_t kJsonAcStateMaxSize = 1024; // Bytes // ----------------- End of User Configuration Section ------------------------- // Constants -#define _MY_VERSION_ "v1.7.1" +#define _MY_VERSION_ "v1.8.0" const uint8_t kRebootTime = 15; // Seconds const uint8_t kQuickDisplayTime = 2; // Seconds