From 78cd3627a6850c42f723f35d61857a39a8fb1414 Mon Sep 17 00:00:00 2001 From: David Date: Tue, 23 Apr 2019 03:55:42 +1000 Subject: [PATCH 1/5] Move MQTT server connection parameters to WifiManager settings. * Add mqtt server, port, username & password to WifiManager settings. * Save a `/config.json` file into the SPIFFS to store these over reboots. * Bucketloads of debugging added. Testing seems to show it works. Fixes #669 --- examples/IRMQTTServer/IRMQTTServer.ino | 174 +++++++++++++++++++++---- examples/IRMQTTServer/platformio.ini | 1 + 2 files changed, 153 insertions(+), 22 deletions(-) diff --git a/examples/IRMQTTServer/IRMQTTServer.ino b/examples/IRMQTTServer/IRMQTTServer.ino index a7234b638..e3aa8e41c 100644 --- a/examples/IRMQTTServer/IRMQTTServer.ino +++ b/examples/IRMQTTServer/IRMQTTServer.ino @@ -24,8 +24,9 @@ * * - Arduino IDE: * o Install the following libraries via Library Manager - * - WiFiManager (https://github.com/tzapu/WiFiManager) (Version >= 0.14) + * - ArduinoJson (https://arduinojson.org/) * - PubSubClient (https://pubsubclient.knolleary.net/) + * - WiFiManager (https://github.com/tzapu/WiFiManager) (Version >= 0.14) * o You MUST change to have the following (or larger) value: * (with REPORT_RAW_UNKNOWNS 1024 or more is recommended) * #define MQTT_MAX_PACKET_SIZE 768 @@ -228,6 +229,8 @@ #endif // MQTT_ENABLE #include +#include +#include #include #include #include @@ -301,13 +304,6 @@ const char* kHtmlPassword = "esp8266"; // <=- CHANGE_ME (required) // ----------------------- MQTT Related Settings ------------------------------- #if MQTT_ENABLE -// Address of your MQTT server. -#define MQTT_SERVER "10.0.0.4" // <=- CHANGE_ME -const uint16_t kMqttPort = 1883; // Default port used by MQTT servers. -// Set if your MQTT server requires a Username & Password to connect -// ... and it probably should if you want to be more secure. -const char* kMqttUsername = ""; // <=- CHANGE_ME (optional) -const char* kMqttPassword = ""; // <=- CHANGE_ME (optional) const uint32_t kMqttReconnectTime = 5000; // Delay(ms) between reconnect tries. #define MQTTprefix HOSTNAME // Change this if you want the MQTT topic to be @@ -409,7 +405,8 @@ const uint8_t kSendTableSize = sizeof(gpioTable); // Firmware uploads are blocked until the user changes kHtmlPassword to a // different value than this. const char* kDefaultPassword = "esp8266"; // Do NOT change this. - +// Name of the json config file in SPIFFS. +const char* kConfigFile = "/config.json"; // Globals ESP8266WebServer server(kHttpPort); #ifdef IR_RX @@ -418,7 +415,9 @@ decode_results capture; // Somewhere to store inbound IR messages. #endif // IR_RX MDNSResponder mdns; WiFiClient espClient; + WiFiManager wifiManager; +bool flagSaveWifiConfig = false; uint16_t *codeArray; uint32_t lastReconnectAttempt = 0; // MQTT last attempt reconnection number @@ -455,12 +454,21 @@ uint32_t lastDisconnectedTime = 0; uint32_t mqttDisconnectCounter = 0; uint32_t mqttSentCounter = 0; uint32_t mqttRecvCounter = 0; - bool wasConnected = true; +const uint8_t kMaxMqttServerSize = 40; +char MqttServer[kMaxMqttServerSize + 1] = ""; +const uint8_t kMaxMqttPortSize = 6; // Largest value of uint16_t is "65535". +uint16_t MqttPort = 1883; +const uint8_t kMaxMqttUsernameSize = 20; +char MqttUsername[kMaxMqttUsernameSize + 1] = ""; +const uint8_t kMaxMqttPasswordSize = 40; +char MqttPassword[kMaxMqttPasswordSize + 1] = ""; + // MQTT client parameters void callback(char* topic, byte* payload, unsigned int length); -PubSubClient mqtt_client(MQTT_SERVER, kMqttPort, callback, espClient); +PubSubClient mqtt_client(espClient); + // Create a unique MQTT client id. String mqtt_clientid = MQTTprefix + String(ESP.getChipId(), HEX); @@ -501,6 +509,81 @@ void debug(String str) { #endif // DEBUG } +// callback notifying us of the need to save the wifi config +void saveWifiConfigCallback(void) { + debug("saveWifiConfigCallback called."); + flagSaveWifiConfig = true; +} + +void saveWifiConfig() { + debug("Saving the wifi config."); + DynamicJsonBuffer jsonBuffer; + JsonObject& json = jsonBuffer.createObject(); +#if MQTT_ENABLE + json["mqtt_server"] = MqttServer; + json["mqtt_port"] = String(MqttPort).c_str(); + json["mqtt_username"] = MqttUsername; + json["mqtt_pass"] = MqttPassword; +#endif // MQTT_ENABLE + if (SPIFFS.begin()) { + File configFile = SPIFFS.open(kConfigFile, "w"); + if (!configFile) { + debug("Failed to open config file for writing."); + } else { + debug("Writing out the config file."); + json.printTo(configFile); + configFile.close(); + debug("Finished writing config file."); + } + SPIFFS.end(); + } +} + +void loadWifiConfigFile() { + debug("Trying to mount SPIFFS"); + if (SPIFFS.begin()) { + debug("mounted file system"); + if (SPIFFS.exists(kConfigFile)) { + debug("config file exists"); + + File configFile = SPIFFS.open(kConfigFile, "r"); + if (configFile) { + debug("Opened config file"); + size_t size = configFile.size(); + // Allocate a buffer to store contents of the file. + std::unique_ptr buf(new char[size]); + + configFile.readBytes(buf.get(), size); + DynamicJsonBuffer jsonBuffer; + JsonObject& json = jsonBuffer.parseObject(buf.get()); + if (json.success()) { + debug("Json config file parsed ok."); +#if MQTT_ENABLE + if (json["mqtt_server"] != NULL) + strncpy(MqttServer, json["mqtt_server"], kMaxMqttServerSize); + if (json["mqtt_port"] != NULL) + MqttPort = atoi(json["mqtt_port"]); + if (json["mqtt_username"] != NULL) + strncpy(MqttUsername, json["mqtt_username"], kMaxMqttUsernameSize); + if (json["mqtt_password"] != NULL) + strncpy(MqttPassword, json["mqtt_password"], kMaxMqttPasswordSize); +#endif // MQTT_ENABLE + } else { + debug("Failed to load json config"); + } + debug("Closing the config file."); + configFile.close(); + } + } else { + debug("Config file doesn't exist!"); + } + debug("Unmounting SPIFFS."); + SPIFFS.end(); + } else { + debug("Failed to mount SPIFFS"); + } +} + String msToHumanString(uint32_t const msecs) { uint32_t totalseconds = msecs / 1000; if (totalseconds == 0) return "Now"; @@ -1189,7 +1272,7 @@ void handleInfo() { "

" #if MQTT_ENABLE "

MQTT Information

" - "

Server: " MQTT_SERVER ":" + String(kMqttPort) + " (" + + "

Server: " + String(MqttServer) + ":" + String(MqttPort) + " (" + (mqtt_client.connected() ? "Connected " + timeSince(lastDisconnectedTime) : "Disconnected " + timeSince(lastConnectedTime)) + ")
" @@ -1267,9 +1350,21 @@ void handleReset() { "

Resetting the WiFiManager config back to defaults.

" "

Device restarting. Try connecting in a few seconds.

" ""); - // Do the reset. + // Do the reset. +#if MQTT_ENABLE + mqttLog("Wiping all saved config settings."); +#endif // MQTT_ENABLE + debug("Trying to mount SPIFFS"); + if (SPIFFS.begin()) { + debug("Removing JSON config file"); + SPIFFS.remove(kConfigFile); + SPIFFS.end(); + } + delay(1000); + debug("Reseting wifiManager's settings."); wifiManager.resetSettings(); - delay(10); + delay(1000); + debug("rebooting..."); ESP.restart(); delay(1000); } @@ -1840,9 +1935,29 @@ void handleNotFound() { void setup_wifi() { delay(10); + loadWifiConfigFile(); // We start by connecting to a WiFi network - wifiManager.setTimeout(300); // Time out after 5 mins. + // Set up additional parameters for WiFiManager config menu page. +#if MQTT_ENABLE + wifiManager.setSaveConfigCallback(saveWifiConfigCallback); + WiFiManagerParameter custom_mqtt_text( + "
MQTT Broker details
"); + wifiManager.addParameter(&custom_mqtt_text); + WiFiManagerParameter custom_mqtt_server( + "mqtt_server", "mqtt server", MqttServer, kMaxMqttServerSize); + wifiManager.addParameter(&custom_mqtt_server); + WiFiManagerParameter custom_mqtt_port( + "mqtt_port", "mqtt port", String(MqttPort).c_str(), kMaxMqttPortSize); + wifiManager.addParameter(&custom_mqtt_port); + WiFiManagerParameter custom_mqtt_user( + "mqtt_user", "mqtt username", MqttUsername, kMaxMqttUsernameSize); + wifiManager.addParameter(&custom_mqtt_user); + WiFiManagerParameter custom_mqtt_pass( + "mqtt_pass", "mqtt password", MqttPassword, kMaxMqttPasswordSize); + wifiManager.addParameter(&custom_mqtt_pass); + #endif // MQTT_ENABLE + #if USE_STATIC_IP // Use a static IP config rather than the one supplied via DHCP. wifiManager.setSTAStaticIPConfig(kIPAddress, kGateway, kSubnetMask); @@ -1853,13 +1968,22 @@ void setup_wifi() { wifiManager.setRemoveDuplicateAPs(HIDE_DUPLIATE_NETWORKS); if (!wifiManager.autoConnect()) { - debug("Wifi failed to connect and hit timeout."); + debug("Wifi failed to connect and hit timeout. Rebooting..."); delay(3000); // Reboot. A.k.a. "Have you tried turning it Off and On again?" ESP.reset(); delay(5000); } +#if MQTT_ENABLE + strncpy(MqttServer, custom_mqtt_server.getValue(), kMaxMqttServerSize); + MqttPort = atoi(custom_mqtt_port.getValue()); + strncpy(MqttUsername, custom_mqtt_user.getValue(), kMaxMqttUsernameSize); + strncpy(MqttPassword, custom_mqtt_pass.getValue(), kMaxMqttPasswordSize); +#endif // MQTT_ENABLE + if (flagSaveWifiConfig) { + saveWifiConfig(); + } debug("WiFi connected. IP address: " + WiFi.localIP().toString()); } @@ -1943,6 +2067,9 @@ void setup(void) { #if MQTT_ENABLE // MQTT Discovery url server.on("/send_discovery", handleSendMqttDiscovery); + // Finish setup of the mqtt clent object. + mqtt_client.setServer(MqttServer, MqttPort); + mqtt_client.setCallback(callback); #endif // MQTT_ENABLE #if FIRMWARE_OTA @@ -2044,15 +2171,18 @@ bool reconnect() { while (!mqtt_client.connected() && tries <= 3) { int connected = false; // Attempt to connect - debug("Attempting MQTT connection to " MQTT_SERVER ":" + String(kMqttPort) + - "... "); - if (kMqttUsername && kMqttPassword) - connected = mqtt_client.connect(mqtt_clientid.c_str(), kMqttUsername, - kMqttPassword, MQTTstatus, QOS, true, + debug("Attempting MQTT connection to " + String(MqttServer) + ":" + + String(MqttPort) + "... "); + if (strcmp(MqttUsername, "") && strcmp(MqttPassword, "")) { + debug("Using mqtt username/password to connect."); + connected = mqtt_client.connect(mqtt_clientid.c_str(), MqttUsername, + MqttPassword, MQTTstatus, QOS, true, LWT_OFFLINE); - else + } else { + debug("Using password-less mqtt connection."); connected = mqtt_client.connect(mqtt_clientid.c_str(), MQTTstatus, QOS, true, LWT_OFFLINE); + } if (connected) { // Once connected, publish an announcement... mqttLog("(Re)Connected."); diff --git a/examples/IRMQTTServer/platformio.ini b/examples/IRMQTTServer/platformio.ini index f06c608ca..243b36a99 100644 --- a/examples/IRMQTTServer/platformio.ini +++ b/examples/IRMQTTServer/platformio.ini @@ -9,6 +9,7 @@ lib_deps_builtin = lib_deps_external = PubSubClient WifiManager@0.14 + ArduinoJson [env:nodemcuv2] platform = espressif8266 From f820880767876d5040abc82eda3c2ed007010e6f Mon Sep 17 00:00:00 2001 From: David Date: Wed, 24 Apr 2019 15:27:41 +1000 Subject: [PATCH 2/5] Move config for IRMQTTServer to WifiManager first boot. - Use WifiManager to do the one-time setup for the program. e.g. Set MQTT parameters, HTTP passwords, hostnames etc. This should allow a fairly standard build (except for GPIOs) for most people. - Move hardcoded examples to their own page. - Lots of hacks to try to reduce heap fragmentation/memory use. - Move user config settings to IRMQTTServer.h file. - General code cleanup and grouping. - Bump version. Almost ready for release. NOTE: Previous users may need to fully wipe/reset the SPIFFS/WifiManager settings by visiting `http:///reset` Fixes #669 --- examples/IRMQTTServer/IRMQTTServer.h | 258 ++++++ examples/IRMQTTServer/IRMQTTServer.ino | 1115 ++++++++++++------------ 2 files changed, 793 insertions(+), 580 deletions(-) create mode 100644 examples/IRMQTTServer/IRMQTTServer.h diff --git a/examples/IRMQTTServer/IRMQTTServer.h b/examples/IRMQTTServer/IRMQTTServer.h new file mode 100644 index 000000000..85ac0e1d3 --- /dev/null +++ b/examples/IRMQTTServer/IRMQTTServer.h @@ -0,0 +1,258 @@ +/* + * Send & receive arbitrary IR codes via a web server or MQTT. + * Copyright David Conran 2016, 2017, 2018, 2019 + */ +#ifndef EXAMPLES_IRMQTTSERVER_IRMQTTSERVER_H_ +#define EXAMPLES_IRMQTTSERVER_IRMQTTSERVER_H_ + +#include +#include +#include +#include +#include +#include + +// ---------------- Start of User Configuration Section ------------------------ + +#ifndef MQTT_ENABLE +#define MQTT_ENABLE true // Whether or not MQTT is used at all. +#endif // MQTT_ENABLE + +// ---------------------- Board Related Settings ------------------------------- +// NOTE: Make sure you set your Serial Monitor to the same speed. +#define BAUD_RATE 115200 // Serial port Baud rate. + +// GPIO the IR LED is connected to/controlled by. GPIO 4 = D2. +#define IR_LED 4 // <=- CHANGE_ME (optional) +// define IR_LED 3 // For an ESP-01 we suggest you use RX/GPIO3/Pin 7. + +// GPIO the IR RX module is connected to/controlled by. e.g. GPIO 14 = D5. +// Comment this out to disable receiving/decoding IR messages entirely. +#define IR_RX 14 // <=- CHANGE_ME (optional) +#define IR_RX_PULLUP false + +// --------------------- Network Related Settings ------------------------------ +const uint16_t kHttpPort = 80; // The TCP port the HTTP server is listening on. +// Change to 'true'/'false' if you do/don't want these features or functions. +#define USE_STATIC_IP false // Change to 'true' if you don't want to use DHCP. +// We obtain our network config via DHCP by default but allow an easy way to +// use a static IP config. +#if USE_STATIC_IP +const IPAddress kIPAddress = IPAddress(10, 0, 1, 78); +const IPAddress kGateway = IPAddress(10, 0, 1, 1); +const IPAddress kSubnetMask = IPAddress(255, 255, 255, 0); +#endif // USE_STATIC_IP + +// See: https://github.com/tzapu/WiFiManager#filter-networks for these settings. +#define HIDE_DUPLIATE_NETWORKS false // Should WifiManager hide duplicate SSIDs +// #define MIN_SIGNAL_STRENGTH 20 // Minimum WiFi signal stength (percentage) + // before we will connect. + // The unset default is 8%. + // (Uncomment to enable) + +// ----------------------- HTTP Related Settings ------------------------------- +#define FIRMWARE_OTA true // Allow remote update of the firmware via http. + // Less secure if enabled. + // Note: Firmware OTA is also disabled until + // a password is set. +#define HTML_PASSWORD_ENABLE false // Protect access to the HTML interface. + // Note: OTA update is always passworded. +// If you do not set a password, Firmware OTA updates will be blocked. + +// ----------------------- MQTT Related Settings ------------------------------- +#if MQTT_ENABLE +const uint32_t kMqttReconnectTime = 5000; // Delay(ms) between reconnect tries. + +#define MQTT_ACK "sent" // Sub-topic we send back acknowledgements on. +#define MQTT_SEND "send" // Sub-topic we get new commands from. +#define MQTT_RECV "received" // Topic we send received IRs to. +#define MQTT_LOG "log" // Topic we send log messages to. +#define MQTT_LWT "status" // Topic for the Last Will & Testament. +#define MQTT_CLIMATE "ac" // Sub-topic for the climate topics. +#define MQTT_CLIMATE_CMND "cmnd" // Sub-topic for the climate command topics. +#define MQTT_CLIMATE_STAT "stat" // Sub-topic for the climate stat topics. +#define MQTTbroadcastInterval 10 * 60 // Seconds between rebroadcasts + +#define QOS 1 // MQTT broker should queue up any unreceived messages for us +// #define QOS 0 // MQTT broker WON'T queue up messages for us. Fire & Forget. +#endif // MQTT_ENABLE + +// ------------------------ IR Capture Settings -------------------------------- +// Let's use a larger than normal buffer so we can handle AirCon remote codes. +const uint16_t kCaptureBufferSize = 1024; +#if DECODE_AC +// Some A/C units have gaps in their protocols of ~40ms. e.g. Kelvinator +// A value this large may swallow repeats of some protocols +const uint8_t kCaptureTimeout = 50; // Milliseconds +#else // DECODE_AC +// Suits most messages, while not swallowing many repeats. +const uint8_t kCaptureTimeout = 15; // Milliseconds +#endif // DECODE_AC +// Ignore unknown messages with <10 pulses (see also REPORT_UNKNOWNS) +const uint16_t kMinUnknownSize = 2 * 10; +#define REPORT_UNKNOWNS false // Report inbound IR messages that we don't know. +#define REPORT_RAW_UNKNOWNS false // Report the whole buffer, recommended: + // MQTT_MAX_PACKET_SIZE of 1024 or more + +// ------------------------ Advanced Usage Only -------------------------------- +// Change if you need multiple independent send gpio/topics. +const uint8_t gpioTable[] = { + IR_LED, // Default GPIO. e.g. ir_server/send or ir_server/send_0 + // Uncomment the following as needed. + // NOTE: Remember to disable DEBUG if you are using one of the serial pins. + // 5, // GPIO 5 / D1 e.g. ir_server/send_1 + // 14, // GPIO 14 / D5 e.g. ir_server/send_2 + // 16, // GPIO 16 / D0 e.g. ir_server/send_3 +}; + +#define KEY_PROTOCOL "protocol" +#define KEY_MODEL "model" +#define KEY_POWER "power" +#define KEY_MODE "mode" +#define KEY_TEMP "temp" +#define KEY_FANSPEED "fanspeed" +#define KEY_SWINGV "swingv" +#define KEY_SWINGH "swingh" +#define KEY_QUIET "quiet" +#define KEY_TURBO "turbo" +#define KEY_LIGHT "light" +#define KEY_BEEP "beep" +#define KEY_ECONO "econo" +#define KEY_SLEEP "sleep" +#define KEY_CLOCK "clock" +#define KEY_FILTER "filter" +#define KEY_CLEAN "clean" +#define KEY_CELSIUS "use_celsius" + +// HTML arguments we will parse for IR code information. +#define KEY_TYPE "type" // KEY_PROTOCOL is also checked too. +#define KEY_CODE "code" +#define KEY_BITS "bits" +#define KEY_REPEAT "repeats" + +// Text for Last Will & Testament status messages. +const char* kLwtOnline = "Online"; +const char* kLwtOffline = "Offline"; + +const uint8_t kHostnameLength = 30; +const uint8_t kPortLength = 5; // Largest value of uint16_t is "65535". +const uint8_t kUsernameLength = 15; +const uint8_t kPasswordLength = 20; + +// -------------------------- Debug Settings ----------------------------------- +// Disable debug output if any of the IR pins are on the TX (D1) pin. +// Note: This is a crude method to catch the common use cases. +// See `isSerialGpioUsedByIr()` for the better method. +#if (IR_LED != 1 && IR_RX != 1) +#ifndef DEBUG +#define DEBUG true // Change to 'false' to disable all serial output. +#endif // DEBUG +#else // (IR_LED != 1 && IR_RX != 1) +#undef DEBUG +#define DEBUG false +#endif + +// ----------------- End of User Configuration Section ------------------------- + +// Constants +#define _MY_VERSION_ "v1.0.0-gamma" + +const uint8_t kSendTableSize = sizeof(gpioTable); +// JSON stuff +// Name of the json config file in SPIFFS. +const char* kConfigFile = "/config.json"; +const char* kMqttServerKey = "mqtt_server"; +const char* kMqttPortKey = "mqtt_port"; +const char* kMqttUserKey = "mqtt_user"; +const char* kMqttPassKey = "mqtt_pass"; +const char* kMqttPrefixKey = "mqtt_prefix"; +const char* kHostnameKey = "hostname"; +const char* kHttpUserKey = "http_user"; +const char* kHttpPassKey = "http_pass"; + +#if MQTT_ENABLE +const uint32_t kBroadcastPeriodMs = MQTTbroadcastInterval * 1000; // mSeconds. +const uint32_t kStatListenPeriodMs = 5 * 1000; // mSeconds + +void mqttCallback(char* topic, byte* payload, unsigned int length); +String listOfCommandTopics(void); +void handleSendMqttDiscovery(void); +void subscribing(const String topic_name); +void unsubscribing(const String topic_name); +void mqttLog(const String mesg); +bool reconnect(void); +void receivingMQTT(String const topic_name, String const callback_str); +void callback(char* topic, byte* payload, unsigned int length); +void sendMQTTDiscovery(const char *topic); +void doBroadcast(TimerMs *timer, const uint32_t interval, + const commonAcState_t state, const bool retain, + const bool force); +#endif // MQTT_ENABLE +bool isSerialGpioUsedByIr(void); +void debug(const char *str); +void saveWifiConfigCallback(void); +void saveWifiConfig(void); +void loadWifiConfigFile(void); +String msToHumanString(uint32_t const msecs); +String timeElapsed(uint32_t const msec); +String timeSince(uint32_t const start); +String listOfSendGpios(void); +bool hasUnsafeHTMLChars(String input); +String htmlMenu(void); +void handleRoot(void); +String addJsReloadUrl(const String url, const uint16_t timeout_s, + const bool notify); +void handleExamples(void); +String boolToString(const bool value); +String opmodeToString(const stdAc::opmode_t mode); +String fanspeedToString(const stdAc::fanspeed_t speed); +String swingvToString(const stdAc::swingv_t swingv); +String swinghToString(const stdAc::swingh_t swingh); +String htmlSelectBool(const String name, const bool def); +String htmlSelectProtocol(const String name, const decode_type_t def); +String htmlSelectModel(const String name, const int16_t def); +String htmlSelectMode(const String name, const stdAc::opmode_t def); +String htmlSelectFanspeed(const String name, const stdAc::fanspeed_t def); +String htmlSelectSwingv(const String name, const stdAc::swingv_t def); +String htmlSelectSwingh(const String name, const stdAc::swingh_t def); +void handleAirCon(void); +void handleAirConSet(void); +void handleAdmin(void); +void handleInfo(void); +void handleReset(void); +void handleReboot(void); +bool parseStringAndSendAirCon(IRsend *irsend, const uint16_t irType, + const String str); +uint16_t countValuesInStr(const String str, char sep); +uint16_t * newCodeArray(const uint16_t size); +#if SEND_GLOBALCACHE +bool parseStringAndSendGC(IRsend *irsend, const String str); +#endif // SEND_GLOBALCACHE +#if SEND_PRONTO +bool parseStringAndSendPronto(IRsend *irsend, const String str, + uint16_t repeats); +#endif // SEND_PRONTO +#if SEND_RAW +bool parseStringAndSendRaw(IRsend *irsend, const String str); +#endif // SEND_RAW +void handleIr(void); +void handleNotFound(void); +void setup_wifi(void); +void init_vars(void); +void setup(void); +void loop(void); +uint64_t getUInt64fromHex(char const *str); +bool sendIRCode(IRsend *irsend, int const ir_type, + uint64_t const code, char const * code_str, uint16_t bits, + uint16_t repeat); +bool sendInt(const String topic, const int32_t num, const bool retain); +bool sendBool(const String topic, const bool on, const bool retain); +bool sendString(const String topic, const String str, const bool retain); +bool sendFloat(const String topic, const float_t temp, const bool retain); +commonAcState_t updateClimate(commonAcState_t current, const String str, + const String prefix, const String payload); +bool cmpClimate(const commonAcState_t a, const commonAcState_t b); +bool sendClimate(const commonAcState_t prev, const commonAcState_t next, + const String topic_prefix, const bool retain, + const bool forceMQTT, const bool forceIR); +#endif // EXAMPLES_IRMQTTSERVER_IRMQTTSERVER_H_ diff --git a/examples/IRMQTTServer/IRMQTTServer.ino b/examples/IRMQTTServer/IRMQTTServer.ino index e3aa8e41c..9dc7361a4 100644 --- a/examples/IRMQTTServer/IRMQTTServer.ino +++ b/examples/IRMQTTServer/IRMQTTServer.ino @@ -1,26 +1,31 @@ /* * Send & receive arbitrary IR codes via a web server or MQTT. - * Copyright David Conran 2016, 2017, 2018 + * Copyright David Conran 2016, 2017, 2018, 2019 * - * NOTE: An IR LED circuit *MUST* be connected to ESP8266 GPIO4 (D2) if - * you want to send IR messages. See IR_LED below. - * A compatible IR RX modules *MUST* be connected to ESP8266 GPIO14 (D5) - * if you want to capture & decode IR nessages. See IR_RX below. + * Copyright: + * Code for this has been borrowed from lots of other OpenSource projects & + * resources. I'm *NOT* claiming complete Copyright ownership of all the code. + * Likewise, feel free to borrow from this as much as you want. + * + * NOTE: An IR LED circuit SHOULD be connected to ESP8266 GPIO4 (D2) if + * you want to send IR messages. + * A compatible IR RX modules SHOULD be connected to ESP8266 GPIO14 (D5) + * if you want to capture & decode IR nessages. + * See 'IR_LED' & 'IR_RX' in IRMQTTServer.h. * - * WARN: This is very advanced & complicated example code. Not for beginners. - * You are strongly suggested to try & look at other example code first. + * WARN: This is *very* advanced & complicated example code. Not for beginners. + * You are strongly suggested to try & look at other example code first + * to understand how this library works. * * # Instructions * * ## Before First Boot (i.e. Compile time) - * - Either: - * o Set the MQTT_SERVER define below to the address of your MQTT server. - * or - * o Disable MQTT (see '#define MQTT_ENABLE' below). + * - Disable MQTT if desired. (see '#define MQTT_ENABLE' in IRMQTTServer.h). * * - Site specific settings: - * o Search for 'CHANGE_ME' for the things you probably need to change for - * your particular situation. + * o Search for 'CHANGE_ME' in IRMQTTServer.h for the things you probably + * need to change for your particular situation. + * o All user changable settings are in the file IRMQTTServer.h. * * - Arduino IDE: * o Install the following libraries via Library Manager @@ -38,15 +43,16 @@ * The ESP8266 board will boot into the WiFiManager's AP mode. * i.e. It will create a WiFi Access Point with a SSID like: "ESP123456" etc. * Connect to that SSID. Then point your browser to http://192.168.4.1/ and - * configure the ESP8266 to connect to your desired WiFi network. - * It will remember the new WiFi connection details on next boot. + * configure the ESP8266 to connect to your desired WiFi network and associated + * required settings. It will remember these details on next boot if the device + * connects successfully. * More information can be found here: * https://github.com/tzapu/WiFiManager#how-it-works * - * If you need to reset the WiFi settings, visit: - * http:///reset + * If you need to reset the WiFi and saved settings to go back to "First Boot", + * visit: http:///reset * - * ## Normal Use (After setup) + * ## Normal Use (After initial setup) * Enter 'http:// - * - * Copyright Notice: - * Code for this has been borrowed from lots of other OpenSource projects & - * resources. I'm *NOT* claiming complete Copyright ownership of all the code. - * Likewise, feel free to borrow from this as much as you want. */ -// ---------------- Start of User Configuration Section ------------------------ - -#ifndef MQTT_ENABLE -#define MQTT_ENABLE true // Whether or not MQTT is used at all. -#endif // MQTT_ENABLE +#include "IRMQTTServer.h" #include #include #include @@ -251,162 +248,9 @@ // -------------------------------------------------------------------- #include #endif // MQTT_ENABLE -#include +#include // NOLINT(build/include) #include -// ---------------------- Board Related Settings ------------------------------- -// NOTE: Make sure you set your Serial Monitor to the same speed. -#define BAUD_RATE 115200 // Serial port Baud rate. - -// GPIO the IR LED is connected to/controlled by. GPIO 4 = D2. -#define IR_LED 4 // <=- CHANGE_ME (optional) -// define IR_LED 3 // For an ESP-01 we suggest you use RX/GPIO3/Pin 7. - -// GPIO the IR RX module is connected to/controlled by. e.g. GPIO 14 = D5. -// Comment this out to disable receiving/decoding IR messages entirely. -#define IR_RX 14 // <=- CHANGE_ME (optional) -#define IR_RX_PULLUP false - -// --------------------- Network Related Settings ------------------------------ -const uint16_t kHttpPort = 80; // The TCP port the HTTP server is listening on. -// Name of the device you want in mDNS. -// NOTE: Changing this will change the MQTT path too unless you override it -// via MQTTprefix below. -#define HOSTNAME "ir_server" // <=- CHANGE_ME (optional) -// Change to 'true'/'false' if you do/don't want these features or functions. -#define USE_STATIC_IP false // Change to 'true' if you don't want to use DHCP. -// We obtain our network config via DHCP by default but allow an easy way to -// use a static IP config. -#if USE_STATIC_IP -const IPAddress kIPAddress = IPAddress(10, 0, 1, 78); -const IPAddress kGateway = IPAddress(10, 0, 1, 1); -const IPAddress kSubnetMask = IPAddress(255, 255, 255, 0); -#endif // USE_STATIC_IP - -// See: https://github.com/tzapu/WiFiManager#filter-networks for these settings. -#define HIDE_DUPLIATE_NETWORKS false // Should WifiManager hide duplicate SSIDs -// #define MIN_SIGNAL_STRENGTH 20 // Minimum WiFi signal stength (percentage) - // before we will connect. - // The unset default is 8%. - // (Uncomment to enable) - -// ----------------------- HTTP Related Settings ------------------------------- -// 'kHtmlUsername' & 'kHtmlPassword' are used by the following two items: -#define FIRMWARE_OTA true // Allow remote update of the firmware via http. - // Less secure if enabled. - // Note: Firmware OTA is also disabled until - // 'kHtmlPassword' is changed from the default. -#define HTML_PASSWORD_ENABLE false // Protect access to the HTML interface. - // Note: OTA update is always passworded. -const char* kHtmlUsername = "admin"; // <=- CHANGE_ME (optional) -const char* kHtmlPassword = "esp8266"; // <=- CHANGE_ME (required) -// If you do not change 'kHtmlPassword', Firmware OTA updates will be blocked. - -// ----------------------- MQTT Related Settings ------------------------------- -#if MQTT_ENABLE -const uint32_t kMqttReconnectTime = 5000; // Delay(ms) between reconnect tries. - -#define MQTTprefix HOSTNAME // Change this if you want the MQTT topic to be - // independent of the hostname. -#define MQTTack MQTTprefix "/sent" // Topic we send back acknowledgements on. -#define MQTTcommand MQTTprefix "/send" // Topic we get new commands from. -#define MQTTrecv MQTTprefix "/received" // Topic we send received IRs to. -#define MQTTlog MQTTprefix "/log" // Topic we send log messages to. -#define MQTTstatus MQTTprefix "/status" // Topic for the Last Will & Testament. -#define MQTTclimateprefix MQTTprefix "/ac" - -#define MQTTcmndprefix "/cmnd/" -#define MQTTstatprefix "/stat/" -#define MQTTwildcard "+" -#define MQTTdiscovery "homeassistant/climate/" HOSTNAME "/config" -#define MQTTHomeAssistantName HOSTNAME "_aircon" -#define MQTTbroadcastInterval 10 * 60 // Seconds between rebroadcasts - -#define QOS 1 // MQTT broker should queue up any unreceived messages for us -// #define QOS 0 // MQTT broker WON'T queue up messages for us. Fire & Forget. -#endif // MQTT_ENABLE - -// ------------------------ IR Capture Settings -------------------------------- -// Let's use a larger than normal buffer so we can handle AirCon remote codes. -const uint16_t kCaptureBufferSize = 1024; -#if DECODE_AC -// Some A/C units have gaps in their protocols of ~40ms. e.g. Kelvinator -// A value this large may swallow repeats of some protocols -const uint8_t kCaptureTimeout = 50; // Milliseconds -#else // DECODE_AC -// Suits most messages, while not swallowing many repeats. -const uint8_t kCaptureTimeout = 15; // Milliseconds -#endif // DECODE_AC -// Ignore unknown messages with <10 pulses (see also REPORT_UNKNOWNS) -const uint16_t kMinUnknownSize = 2 * 10; -#define REPORT_UNKNOWNS false // Report inbound IR messages that we don't know. -#define REPORT_RAW_UNKNOWNS false // Report the whole buffer, recommended: - // MQTT_MAX_PACKET_SIZE of 1024 or more - -// ------------------------ Advanced Usage Only -------------------------------- -// Change if you need multiple independent send gpio/topics. -const uint8_t gpioTable[] = { - IR_LED, // Default GPIO. e.g. ir_server/send or ir_server/send_0 - // Uncomment the following as needed. - // NOTE: Remember to disable DEBUG if you are using one of the serial pins. - // 5, // GPIO 5 / D1 e.g. ir_server/send_1 - // 14, // GPIO 14 / D5 e.g. ir_server/send_2 - // 16, // GPIO 16 / D0 e.g. ir_server/send_3 -}; - -#define KEY_PROTOCOL "protocol" -#define KEY_MODEL "model" -#define KEY_POWER "power" -#define KEY_MODE "mode" -#define KEY_TEMP "temp" -#define KEY_FANSPEED "fanspeed" -#define KEY_SWINGV "swingv" -#define KEY_SWINGH "swingh" -#define KEY_QUIET "quiet" -#define KEY_TURBO "turbo" -#define KEY_LIGHT "light" -#define KEY_BEEP "beep" -#define KEY_ECONO "econo" -#define KEY_SLEEP "sleep" -#define KEY_CLOCK "clock" -#define KEY_FILTER "filter" -#define KEY_CLEAN "clean" -#define KEY_CELSIUS "use_celsius" - -// -------------------------- Debug Settings ----------------------------------- -// Disable debug output if any of the IR pins are on the TX (D1) pin. -// Note: This is a crude method to catch the common use cases. -// See `isSerialGpioUsedByIr()` for the better method. -#if (IR_LED != 1 && IR_RX != 1) -#ifndef DEBUG -#define DEBUG true // Change to 'false' to disable all serial output. -#endif // DEBUG -#else // (IR_LED != 1 && IR_RX != 1) -#undef DEBUG -#define DEBUG false -#endif - -// ----------------- End of User Configuration Section ------------------------- - -// Constants -#define _MY_VERSION_ "v1.0.0-beta" -// HTML arguments we will parse for IR code information. -#define argType "type" -#define argData "code" -#define argBits "bits" -#define argRepeat "repeats" - -// Text for Last Will & Testament status messages. -#define LWT_ONLINE "Online" -#define LWT_OFFLINE "Offline" - -const uint8_t kSendTableSize = sizeof(gpioTable); -// This is what the default password is. People should NEVER use this password. -// Firmware uploads are blocked until the user changes kHtmlPassword to a -// different value than this. -const char* kDefaultPassword = "esp8266"; // Do NOT change this. -// Name of the json config file in SPIFFS. -const char* kConfigFile = "/config.json"; // Globals ESP8266WebServer server(kHttpPort); #ifdef IR_RX @@ -415,10 +259,11 @@ decode_results capture; // Somewhere to store inbound IR messages. #endif // IR_RX MDNSResponder mdns; WiFiClient espClient; - WiFiManager wifiManager; bool flagSaveWifiConfig = false; - +char HttpUsername[kUsernameLength + 1] = "admin"; // Default HTT username. +char HttpPassword[kPasswordLength + 1] = ""; // No HTTP password by default. +char Hostname[kHostnameLength + 1] = "ir_server"; // Default hostname. uint16_t *codeArray; uint32_t lastReconnectAttempt = 0; // MQTT last attempt reconnection number bool boot = true; @@ -446,6 +291,7 @@ bool lastClimateSucceeded = false; bool hasClimateBeenSent = false; // Has the Climate ever been sent? #if MQTT_ENABLE +PubSubClient mqtt_client(espClient); String lastMqttCmd = "None"; String lastMqttCmdTopic = "None"; uint32_t lastMqttCmdTime = 0; @@ -456,21 +302,23 @@ uint32_t mqttSentCounter = 0; uint32_t mqttRecvCounter = 0; bool wasConnected = true; -const uint8_t kMaxMqttServerSize = 40; -char MqttServer[kMaxMqttServerSize + 1] = ""; -const uint8_t kMaxMqttPortSize = 6; // Largest value of uint16_t is "65535". -uint16_t MqttPort = 1883; -const uint8_t kMaxMqttUsernameSize = 20; -char MqttUsername[kMaxMqttUsernameSize + 1] = ""; -const uint8_t kMaxMqttPasswordSize = 40; -char MqttPassword[kMaxMqttPasswordSize + 1] = ""; - -// MQTT client parameters -void callback(char* topic, byte* payload, unsigned int length); -PubSubClient mqtt_client(espClient); - -// Create a unique MQTT client id. -String mqtt_clientid = MQTTprefix + String(ESP.getChipId(), HEX); +char MqttServer[kHostnameLength + 1] = "10.0.0.4"; +char MqttPort[kPortLength + 1] = "1883"; +char MqttUsername[kUsernameLength + 1] = ""; +char MqttPassword[kPasswordLength + 1] = ""; +char MqttPrefix[kHostnameLength + 1] = ""; + +String MqttAck; // Sub-topic we send back acknowledgements on. +String MqttSend; // Sub-topic we get new commands from. +String MqttRecv; // Topic we send received IRs to. +String MqttLog; // Topic we send log messages to. +String MqttLwt; // Topic for the Last Will & Testament. +String MqttClimate; // Sub-topic for the climate topics. +String MqttClimateCmnd; // Sub-topic for the climate command topics. +String MqttClimateStat; // Sub-topic for the climate stat topics. +String MqttDiscovery; +String MqttHAName; +String MqttClientId; // Primative lock file for gating MQTT state broadcasts. bool lockMqttBroadcast = true; @@ -479,12 +327,8 @@ bool hasBroadcastBeenSent = false; TimerMs lastDiscovery = TimerMs(); // When we last sent a Discovery. bool hasDiscoveryBeenSent = false; TimerMs statListenTime = TimerMs(); // How long we've been listening for. - -const uint32_t kBroadcastPeriodMs = MQTTbroadcastInterval * 1000; // mSeconds. -const uint32_t kStatListenPeriodMs = 5 * 1000; // mSeconds #endif // MQTT_ENABLE -#if DEBUG bool isSerialGpioUsedByIr(void) { const uint8_t kSerialTxGpio = 1; // The GPIO serial output is sent too. // Note: *DOES NOT* control Serial output. @@ -498,14 +342,13 @@ bool isSerialGpioUsedByIr(void) { return true; // Serial port is in use for IR sending. Abort. return false; // Not in use as far as we can tell. } -#endif // DEBUG // Debug messages get sent to the serial port. -void debug(String str) { +void debug(const char *str) { #if DEBUG if (isSerialGpioUsedByIr()) return; // Abort. uint32_t now = millis(); - Serial.printf("%07u.%03u: %s\n", now / 1000, now % 1000, str.c_str()); + Serial.printf("%07u.%03u: %s\n", now / 1000, now % 1000, str); #endif // DEBUG } @@ -515,16 +358,21 @@ void saveWifiConfigCallback(void) { flagSaveWifiConfig = true; } -void saveWifiConfig() { +void saveWifiConfig(void) { debug("Saving the wifi config."); DynamicJsonBuffer jsonBuffer; JsonObject& json = jsonBuffer.createObject(); #if MQTT_ENABLE - json["mqtt_server"] = MqttServer; - json["mqtt_port"] = String(MqttPort).c_str(); - json["mqtt_username"] = MqttUsername; - json["mqtt_pass"] = MqttPassword; + json[kMqttServerKey] = MqttServer; + json[kMqttPortKey] = MqttPort; + json[kMqttUserKey] = MqttUsername; + json[kMqttPassKey] = MqttPassword; + json[kMqttPrefixKey] = MqttPrefix; #endif // MQTT_ENABLE + json[kHostnameKey] = Hostname; + json[kHttpUserKey] = HttpUsername; + json[kHttpPassKey] = HttpPassword; + if (SPIFFS.begin()) { File configFile = SPIFFS.open(kConfigFile, "w"); if (!configFile) { @@ -539,7 +387,7 @@ void saveWifiConfig() { } } -void loadWifiConfigFile() { +void loadWifiConfigFile(void) { debug("Trying to mount SPIFFS"); if (SPIFFS.begin()) { debug("mounted file system"); @@ -559,15 +407,16 @@ void loadWifiConfigFile() { if (json.success()) { debug("Json config file parsed ok."); #if MQTT_ENABLE - if (json["mqtt_server"] != NULL) - strncpy(MqttServer, json["mqtt_server"], kMaxMqttServerSize); - if (json["mqtt_port"] != NULL) - MqttPort = atoi(json["mqtt_port"]); - if (json["mqtt_username"] != NULL) - strncpy(MqttUsername, json["mqtt_username"], kMaxMqttUsernameSize); - if (json["mqtt_password"] != NULL) - strncpy(MqttPassword, json["mqtt_password"], kMaxMqttPasswordSize); + strncpy(MqttServer, json[kMqttServerKey] | "", kHostnameLength); + strncpy(MqttPort, json[kMqttPortKey] | "1883", kPortLength); + strncpy(MqttUsername, json[kMqttUserKey] | "", kUsernameLength); + strncpy(MqttPassword, json[kMqttPassKey] | "", kPasswordLength); + strncpy(MqttPrefix, json[kMqttPrefixKey] | "", kHostnameLength); #endif // MQTT_ENABLE + strncpy(Hostname, json[kHostnameKey] | "", kHostnameLength); + strncpy(HttpUsername, json[kHttpUserKey] | "", kUsernameLength); + strncpy(HttpPassword, json[kHttpPassKey] | "", kPasswordLength); + debug("Recovered Json fields."); } else { debug("Failed to load json config"); } @@ -596,19 +445,23 @@ String msToHumanString(uint32_t const msecs) { String result = ""; if (days) result += String(days) + " day"; - if (days > 1) result += "s"; - if (hours) result += " " + String(hours) + " hour"; - if (hours > 1) result += "s"; - if (minutes) result += " " + String(minutes) + " minute"; - if (minutes > 1) result += "s"; - if (seconds) result += " " + String(seconds) + " second"; - if (seconds > 1) result += "s"; + if (days > 1) result += 's'; + if (hours) result += ' ' + String(hours) + " hour"; + if (hours > 1) result += 's'; + if (minutes) result += ' ' + String(minutes) + " minute"; + if (minutes > 1) result += 's'; + if (seconds) result += ' ' + String(seconds) + " second"; + if (seconds > 1) result += 's'; result.trim(); return result; } String timeElapsed(uint32_t const msec) { - return msToHumanString(msec) + " ago"; + String result = msToHumanString(msec); + if (result.equalsIgnoreCase("Now")) + return result; + else + return result + " ago"; } String timeSince(uint32_t const start) { @@ -628,22 +481,11 @@ String listOfSendGpios(void) { String result = String(gpioTable[0]); if (kSendTableSize > 1) result += " (default)"; for (uint8_t i = 1; i < kSendTableSize; i++) { - result += ", " + String(gpioTable[1]); + result += ", " + String(gpioTable[i]); } return result; } -#if MQTT_ENABLE -// Return a string containing the comma separated list of MQTT command topics. -String listOfCommandTopics(void) { - String result = MQTTcommand; - for (uint8_t i = 0; i < kSendTableSize; i++) { - result += ", " MQTTcommand "_" + String(gpioTable[1]); - } - return result; -} -#endif // MQTT_ENABLE - // Quick and dirty check for any unsafe chars in a string // that may cause HTML shenanigans. e.g. An XSS. bool hasUnsafeHTMLChars(String input) { @@ -664,6 +506,10 @@ String htmlMenu(void) { "onclick='window.location=\"/aircon\"'>" "Aircon" "" + "" " " - "Send a Climate MQTT discovery message to Home Assistant.

"); + "Send a Climate MQTT discovery message to Home Assistant.

" #endif // MQTT_ENABLE - html += F( " Simple reboot of the ESP8266. (No changes)

" + " A simple reboot of the ESP8266. " + "ie. No changes

" " Warning: " - "Resets WiFi back to original settings. ie. Back to AP mode.

"); + "Resets the device back to original settings. " + "ie. Goes back to AP/Setup mode.
"); #if FIRMWARE_OTA html += F("

Update firmware

" "Warning:
"); if (!strlen(HttpPassword)) // Deny if password not set - html += F("OTA firmware is disabled until you set a password.
"); + html += F("OTA firmware is disabled until you set a password. " + "You will need to wipe & reset to set one." + "

"); else // default password has been changed, so allow it. html += F( "Updating your firmware may screw up your access to the device. " @@ -1084,7 +1085,7 @@ void handleAdmin(void) { "" ""); #endif // FIRMWARE_OTA - html += ""; + html += F(""); server.send(200, "text/html", html); } @@ -1797,18 +1798,18 @@ void setup_wifi(void) { kHostnameKey, kHostnameKey, Hostname, kHostnameLength); wifiManager.addParameter(&custom_hostname); WiFiManagerParameter custom_authentication_text( - "

Web/OTA authentication
"); + "

Web/OTA authentication
"); wifiManager.addParameter(&custom_authentication_text); WiFiManagerParameter custom_http_username( kHttpUserKey, "username", HttpUsername, kUsernameLength); wifiManager.addParameter(&custom_http_username); WiFiManagerParameter custom_http_password( - kHttpPassKey, "password", HttpPassword, kPasswordLength, + kHttpPassKey, "password (No OTA if blank)", HttpPassword, kPasswordLength, " type='password'"); wifiManager.addParameter(&custom_http_password); #if MQTT_ENABLE WiFiManagerParameter custom_mqtt_text( - "
MQTT Broker details
"); + "

MQTT Broker details
"); wifiManager.addParameter(&custom_mqtt_text); WiFiManagerParameter custom_mqtt_server( kMqttServerKey, "mqtt server", MqttServer, kHostnameLength); @@ -1825,7 +1826,7 @@ void setup_wifi(void) { " type='password'"); wifiManager.addParameter(&custom_mqtt_pass); WiFiManagerParameter custom_prefix_text( - "
MQTT Prefix
"); + "

MQTT Prefix
"); wifiManager.addParameter(&custom_prefix_text); WiFiManagerParameter custom_mqtt_prefix( kMqttPrefixKey, "Leave empty to use Hostname", MqttPrefix,