Examples: IRMQTTServer extended with new A/C common fields (#1940)

Adds iFeel/sensorTemp/command support.

IRMQTTServer build fix on Windows (naive)

bump version number as this is a significant change of operation & functionality

Signed-off-by: Mateusz Bronk <bronk.m+gh@gmail.com>
Co-authored-by: Mateusz Bronk <bronk.m+gh@gmail.com>
Co-authored-by: David Conran <crankyoldgit@users.noreply.github.com>
This commit is contained in:
Mateusz Bronk
2023-01-12 04:51:23 +01:00
committed by GitHub
parent ddc9ec1a25
commit 721c5b8f13
2 changed files with 140 additions and 27 deletions

View File

@@ -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"
// Miscellaneous constants
#define TOGGLE_JS_FN_NAME "ToggleInputBasedOnCheckbox"
// Text for Last Will & Testament status messages.
const char* const kLwtOnline = "Online";
const char* const kLwtOffline = "Offline";
@@ -290,7 +299,7 @@ const uint16_t kJsonAcStateMaxSize = 1024; // Bytes
// ----------------- End of User Configuration Section -------------------------
// Constants
#define _MY_VERSION_ "v1.7.2"
#define _MY_VERSION_ "v1.8.0"
const uint8_t kRebootTime = 15; // Seconds
const uint8_t kQuickDisplayTime = 2; // Seconds
@@ -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);

View File

@@ -947,7 +947,7 @@ void handleExamples(void) {
#endif // EXAMPLES_ENABLE
String htmlSelectBool(const String name, const bool def) {
String html = F("<select name='") + name + F("'>");
String html = String(F("<select name='")) + name + F("'>");
for (uint16_t i = 0; i < 2; i++)
html += htmlOptionItem(IRac::boolToString(i), IRac::boolToString(i),
i == def);
@@ -955,8 +955,20 @@ 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 = String(F("<input type='checkbox' name='")) + name + F("' id='")
+ name + F("' onclick=\"") + toggleJsFnName + F("(this, '") +
targetControlId + F("')\"");
if (checked) {
html += F(" checked");
}
html += "/><label for='" + name + F("'>Disabled</label>");
return html;
}
String htmlSelectClimateProtocol(const String name, const decode_type_t def) {
String html = F("<select name='") + name + F("'>");
String html = String(F("<select name='")) + name + 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),
@@ -968,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("<select name='") + name + F("'>");
String html = String(F("<select name='")) + name + F("'>");
for (int16_t i = -1; i <= 6; i++) {
String num = String(i);
String text;
@@ -984,9 +996,21 @@ String htmlSelectModel(const String name, const int16_t def) {
return html;
}
String htmlSelectCommandType(const String name, const stdAc::ac_command_t def) {
String html = String(F("<select name='")) + name + 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("</select>");
return html;
}
String htmlSelectUint(const String name, const uint16_t max,
const uint16_t def) {
String html = F("<select name='") + name + F("'>");
String html = String(F("<select name='")) + name + F("'>");
for (uint16_t i = 0; i < max; i++) {
String num = String(i);
html += htmlOptionItem(num, num, i == def);
@@ -997,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(": <select name='") + name + F("'>");
String html = String(F(": <select name='")) + name + F("'>");
for (int16_t i = 0; i < length; i++) {
String num = String(list[i]);
html += htmlOptionItem(num, list[i] == kGpioUnused ? F("Unused") : num,
@@ -1009,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("<select name='") + name + F("'>");
String html = String(F("<select name='")) + name + 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);
@@ -1019,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("<select name='") + name + F("'>");
String html = String(F("<select name='")) + name + 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);
@@ -1029,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("<select name='") + name + F("'>");
String html = String(F("<select name='")) + name + 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);
@@ -1039,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("<select name='") + name + F("'>");
String html = String(F("<select name='")) + name + 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);
@@ -1048,14 +1072,20 @@ String htmlSelectSwingh(const String name, const stdAc::swingh_t def) {
return html;
}
String htmlHeader(const String title, const String h1_text) {
String htmlHeader(const String title, const String h1_text,
const String headScriptsJS) {
String html = F("<html><head><title>");
html += title;
html += F("</title><meta http-equiv=\"Content-Type\" "
"content=\"text/html;charset=utf-8\">"
"<meta name=\"viewport\" content=\"width=device-width,"
"initial-scale=1.0,minimum-scale=1.0,maximum-scale=5.0\">"
"</head><body><center><h1>");
"initial-scale=1.0,minimum-scale=1.0,maximum-scale=5.0\">");
if (headScriptsJS.length()) {
html += F("<script type=\"text/javascript\">\n");
html += headScriptsJS;
html += F("</script>\n");
}
html += F("</head><body><center><h1>");
if (h1_text.length())
html += h1_text;
else
@@ -1078,15 +1108,26 @@ String htmlButton(const String url, const String button, const String text) {
return html;
}
String getJsToggleCheckbox(const String functionName) {
const String javascript =
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"
" }\n");
return javascript;
}
// Admin web page
void handleAirCon(void) {
String html = htmlHeader(F("Air Conditioner Control"));
String html = htmlHeader(F("Air Conditioner Control"), "",
getJsToggleCheckbox());
html += htmlMenu();
if (kNrOfIrTxGpios > 1) {
html += F("<form method='POST' action='/aircon/set'"
html += String(F("<form method='POST' action='/aircon/set'"
" enctype='multipart/form-data'>"
"<table>"
"<tr><td><b>Climate #</b></td><td>") +
"<tr><td><b>Climate #</b></td><td>")) +
htmlSelectUint(KEY_CHANNEL, kNrOfIrTxGpios, chan) +
F("<input type='submit' value='Change'>"
"</td></tr>"
@@ -1095,11 +1136,12 @@ void handleAirCon(void) {
"<hr>");
}
if (climate[chan] != NULL) {
html += F("<h3>Current Settings</h3>"
bool noSensorTemp = (climate[chan]->next.sensorTemperature == kNoTempValue);
html += String(F("<h3>Current Settings</h3>"
"<form method='POST' action='/aircon/set'"
" enctype='multipart/form-data'>"
"<input type='hidden' name='" KEY_CHANNEL "' value='") + String(chan) +
F("'>") +
"<input type='hidden' name='" KEY_CHANNEL "' value='")) + String(chan)
+ F("'>") +
F("<table style='width:33%'>"
"<tr><td>" D_STR_PROTOCOL "</td><td>") +
htmlSelectClimateProtocol(KEY_PROTOCOL,
@@ -1108,6 +1150,9 @@ void handleAirCon(void) {
"<tr><td>" D_STR_MODEL "</td><td>") +
htmlSelectModel(KEY_MODEL, climate[chan]->next.model) +
F("</td></tr>"
"<tr><td>" D_STR_COMMAND "</td><td>") +
htmlSelectCommandType(KEY_COMMAND, climate[chan]->next.command) +
F("</td></tr>"
"<tr><td>" D_STR_POWER "</td><td>") +
htmlSelectBool(KEY_POWER, climate[chan]->next.power) +
F("</td></tr>"
@@ -1126,6 +1171,16 @@ void handleAirCon(void) {
(!climate[chan]->next.celsius ? " selected='selected'" : "") +
F(">F</option>"
"</select></td></tr>"
"<tr><td>" D_STR_SENSORTEMP "</td><td>"
"<input type='number' name='" KEY_SENSORTEMP "' "
"id='" KEY_SENSORTEMP "' min='16' max='90' step='0.5' value='") +
(noSensorTemp ? String(climate[chan]->next.degrees, 1) :
String(climate[chan]->next.sensorTemperature, 1)) +
F("'") +
(noSensorTemp ? " disabled" : "") + F(">") +
htmlDisableCheckbox(KEY_SENSORTEMP_DISABLED, KEY_SENSORTEMP,
noSensorTemp) +
F("</td></tr>"
"<tr><td>" D_STR_FAN "</td><td>") +
htmlSelectFanspeed(KEY_FANSPEED, climate[chan]->next.fanspeed) +
F("</td></tr>"
@@ -1138,6 +1193,9 @@ void handleAirCon(void) {
"<tr><td>" D_STR_QUIET "</td><td>") +
htmlSelectBool(KEY_QUIET, climate[chan]->next.quiet) +
F("</td></tr>"
"<tr><td>" D_STR_IFEEL "</td><td>") +
htmlSelectBool(KEY_IFEEL, climate[chan]->next.iFeel) +
F("</td></tr>"
"<tr><td>" D_STR_TURBO "</td><td>") +
htmlSelectBool(KEY_TURBO, climate[chan]->next.turbo) +
F("</td></tr>"
@@ -1286,8 +1344,8 @@ void handleInfo(void) {
String html = htmlHeader(F("IR MQTT server info"));
html += htmlMenu();
html +=
F("<h3>General</h3>"
"<p>Hostname: ") + String(Hostname) + F("<br>"
String(F("<h3>General</h3>"
"<p>Hostname: ")) + String(Hostname) + F("<br>"
"IP address: ") + WiFi.localIP().toString() + F("<br>"
"MAC address: ") + WiFi.macAddress() + F("<br>"
"Booted: ") + timeSince(1) + F("<br>") +
@@ -2643,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
@@ -2720,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;
@@ -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<char*>());
else if (validJsonInt(json, KEY_MODEL))
result.model = json[KEY_MODEL];
if (validJsonStr(json, KEY_COMMAND))
result.command = IRac::strToCommand(json[KEY_COMMAND].as<char*>());
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
@@ -3089,12 +3160,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))) {
@@ -3132,6 +3215,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
@@ -3165,6 +3253,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,
@@ -3180,6 +3273,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);