From 9a1e3f5f8aa1c1b938fbf4c14b72bde697e57586 Mon Sep 17 00:00:00 2001 From: ariel s Date: Sat, 22 Jul 2023 21:53:27 +0300 Subject: [PATCH] test --- platformio.ini | 16 +- src/IRMQTTServer.h | 515 ++++++ src/main.cpp | 4098 ++++++++++++++++++++++++++++++++++++++++++-- 3 files changed, 4446 insertions(+), 183 deletions(-) create mode 100644 src/IRMQTTServer.h diff --git a/platformio.ini b/platformio.ini index 2734187..05e47bb 100644 --- a/platformio.ini +++ b/platformio.ini @@ -1,16 +1,12 @@ ; PlatformIO Project Configuration File -; -; Build options: build flags, source filter -; Upload options: custom upload port, speed and extra flags -; Library options: dependencies, extra library storages -; Advanced options: extra scripting -; -; Please visit documentation for the other options and examples -; https://docs.platformio.org/page/projectconf.html [env:nodemcuv2] platform = espressif8266 board = nodemcuv2 framework = arduino -lib_deps = crankyoldgit/IRremoteESP8266@^2.8.5 -monitor_speed = 115200 \ No newline at end of file +lib_deps = + WifiManager@>=0.16.0 + crankyoldgit/IRremoteESP8266@^2.8.5 + PubSubClient@>=2.8 + ArduinoJson@>=6.0 +monitor_speed = 115200 diff --git a/src/IRMQTTServer.h b/src/IRMQTTServer.h new file mode 100644 index 0000000..da54eef --- /dev/null +++ b/src/IRMQTTServer.h @@ -0,0 +1,515 @@ +/* + * Send & receive arbitrary IR codes via a web server or MQTT. + * Copyright David Conran 2016-2021 + */ +#ifndef EXAMPLES_IRMQTTSERVER_IRMQTTSERVER_H_ +#define EXAMPLES_IRMQTTSERVER_IRMQTTSERVER_H_ + +#if defined(ESP8266) +#include +#else +#include +#endif +#if defined(ESP8266) +#include +#endif // ESP8266 +#include +#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 + +#ifndef EXAMPLES_ENABLE +// Whether or not examples are included. `false` saves ~2.5K of program space. +#define EXAMPLES_ENABLE true +#endif // EXAMPLES_ENABLE + +// Uncomment one of the following to manually override what +// type of persistent storage is used. +// Warning: Changing filesystems will cause all previous locally +// saved configuration data to be lost. +// #define FILESYSTEM SPIFFS +// #define FILESYSTEM LittleFS +#ifndef FILESYSTEM +// Set the default filesystem if none was specified. +#ifdef ESP8266 +#define FILESYSTEM LittleFS +#else +#define FILESYSTEM SPIFFS +#endif // defined(ESP8266) +#endif // FILESYSTEM + +#if (FILESYSTEM == LittleFS) +#define FILESYSTEMSTR "LittleFS" +#else +#define FILESYSTEMSTR "SPIFFS" +#endif +// ---------------------- Board Related Settings ------------------------------- +// NOTE: Make sure you set your Serial Monitor to the same speed. +#define BAUD_RATE 115200 // Serial port Baud rate. + +// Change if you need multiple independent send gpios & topics. (MQTT only) +const uint8_t kNrOfIrTxGpios = 1; +// Default GPIO the IR LED is connected to/controlled by. GPIO 4 = D2. +// For an ESP-01 we suggest you use RX/GPIO3/Pin 7. i.e. kDefaultIrLed = 3 +// Note: A value of -1 means unused. +const int8_t kDefaultIrLed = 4; // <=- CHANGE_ME (optional) + +// **DANGER** Optional flag to invert the output. (default = false) +// `false`: The LED is illuminated when the GPIO is HIGH. +// `true`: The LED is illuminated when GPIO is LOW rather than HIGH. +// Setting this to something other than the default could +// easily destroy your IR LED if you are overdriving it. +// Unless you *REALLY* know what you are doing, don't change this. +const bool kInvertTxOutput = false; + +// Default GPIO the IR demodulator is connected to/controlled by. GPIO 14 = D5. +// Note: GPIO 16 won't work on the ESP8266 as it does not have interrupts. +const int8_t kDefaultIrRx = 14; // <=- CHANGE_ME (optional) + +// Enable/disable receiving/decoding IR messages entirely. +// Note: IR_RX costs about 40k+ of program memory. +#define IR_RX true + +// Should we use PULLUP on the IR Rx gpio? +#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_DUPLICATE_NETWORKS false // Make 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) +// Do you want/need mdns enabled? (https://en.wikipedia.org/wiki/Multicast_DNS) +#ifndef MDNS_ENABLE +#define MDNS_ENABLE true // `false` to disable and save ~21k of program space. +#endif // MDNS_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 & GPIO updates are always + // passworded. +// If you do not set a password, Firmware OTA & GPIO updates will be blocked. + +// ----------------------- MQTT Related Settings ------------------------------- +#if MQTT_ENABLE +#ifndef MQTT_BUFFER_SIZE +// A value of 768 handles most cases easily. Use 1024 or more when using +// `REPORT_RAW_UNKNOWNS` is recommended. +#define MQTT_BUFFER_SIZE 768 // Default MQTT packet buffer size. +#endif // MQTT_BUFFER_SIZE +const uint16_t kMqttBufferSize = MQTT_BUFFER_SIZE; // Packet Buffer size. +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. +// Sub-topic for the temperature/humidity sensor stat topics. +#define MQTT_SENSOR_STAT "sensor" +// Enable sending/receiving climate via JSON. `true` cost ~5k of program space. +#define MQTT_CLIMATE_JSON false + +// Use Home Assistant-style operation modes. +// TL;DR: Power and Mode are linked together. One changes the other. +// i.e. +// - When power is set to "off", the mode is set to "off". +// - When the mode changes from "off" to something else, power is set to "on". +// See: https://www.home-assistant.io/components/climate.mqtt/#modes +// *** WARNING *** +// This setting will cause IRMQTTServer to forget what the previous operation +// mode was. e.g. a power "on" -> "off" -> "on" will cause it to use the +// default mode for your A/C, not the previous mode. +// Typically this is "Auto" or "Cool" mode. +// Change to false, if your home automation system doesn't like this, or if +// you want IRMQTTServer to be the authoritative source for controling your +// A/C. +#define MQTT_CLIMATE_HA_MODE true + +// Do we send an IR message when we reboot and recover the existing A/C state? +// If set to `false` you may miss requested state changes while the ESP was +// down. If set to `true`, it will resend the previous desired state sent to the +// A/C. Depending on your circumstances, you may need to change this. +#define MQTT_CLIMATE_IR_SEND_ON_RESTART false +#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. + +// Enable(true)/Disable(false) the option to send a MQTT Discovery message for +// the AirCon/Climate system to Home Assistant. Note: `false` saves ~1.5k. +#define MQTT_DISCOVERY_ENABLE true +// Enable(true)/Disable(false) the option to clear any settings stored in MQTT +// for this device's current config. e.g. Climate states using MQTT retain. +// In theory, you shouldn't need this as you can always clean up by hand, hence +// it is disabled by default. Note: `false` saves ~1.2k. +#define MQTT_CLEAR_ENABLE false + +#ifndef MQTT_SERVER_AUTODETECT_ENABLE +// Whether or not MQTT Server IP is detected through mDNS +#define MQTT_SERVER_AUTODETECT_ENABLE true +#endif // MQTT_SERVER_AUTODETECT_ENABLE +#endif // MQTT_ENABLE + +// ------------------------ IR Capture Settings -------------------------------- +// Should we stop listening for IR messages when we send a message via IR? +// Set this to `true` if your IR demodulator is picking up self transmissions. +// Use `false` if it isn't or can't see the self-sent transmissions +// Using `true` may mean some incoming IR messages are lost or garbled. +// i.e. `false` is better if you can get away with it. +#define DISABLE_CAPTURE_WHILE_TRANSMITTING true +// 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_BUFFER_SIZE of 1024 or more + +// Should we use and report individual A/C settings we capture via IR if we +// can understand the individual settings of the remote. +// e.g. Aquire the A/C settings from an actual A/C IR remote and override +// any local settings set via MQTT/HTTP etc. +#ifndef USE_DECODED_AC_SETTINGS +#define USE_DECODED_AC_SETTINGS true // `false` to disable. `true` to enable. +#endif // USE_DECODED_AC_SETTINGS +// Should we allow or ignore an A/C IR remote to override the A/C protocol/model +// as set via MQTT or HTTP? +// e.g. If `true`, you can use any fully supported A/C remote to control +// another brand's or model's A/C unit. `false` means change to the new +// protocol/model if we support it via `USE_DECODED_AC_SETTINGS`. +#define IGNORE_DECODED_AC_PROTOCOL true +// Do we (re-)send the captured & decoded A/C message via the IR_LED? +// `false` if you don't want to repeat the captured message. +// e.g. Useful if the IR demodulator is located in the path between the remote +// and the A/C unit so the command isn't sent twice. +// `true` if you want it sent anyway. +// e.g. The IR demodulator is in a completely different location than than the +// actual a/c unit. +#define REPLAY_DECODED_AC_MESSAGE false + +// ------------------------ SHT-3x Support ------------------------------------- +// To enable SHT-3x sensor support (such as the Lolin SHT30 Shield), connected +// to GPIOs 4 and 5 (D2 and D1), do the following: +// - uncomment the line in platformio.ini to enable the SHT-3x library +// - uncomment the following #define line +// #define SHT3X_SUPPORT true + +// Default address for SHT-3x sensor. +#define SHT3X_I2C_ADDRESS 0x44 +// Requires MQTT_DISCOVERY_ENABLE to be true as well. +// If set, will send HA MQTT Discovery messages for the SHT-3x sensor. +#define SHT3X_MQTT_DISCOVERY_ENABLE true +// I2C SDA pin for SHT-3x sensor (D2). +#define SHT3X_I2C_SDA 4 +// I2C SCL pin for SHT-3x sensor (D1). +#define SHT3X_I2C_SCL 5 +// Check frequency for SHT-3x sensor (in seconds). +#define SHT3X_CHECK_FREQ 60 + +// ------------------------ Advanced Usage Only -------------------------------- + +// Reports the input voltage to the ESP chip. **NOT** the input voltage +// to the development board (e.g. NodeMCU, D1 Mini etc) which are typically +// powered by USB (5V) which is then lowered to 3V via a Low Drop Out (LDO) +// Voltage regulator. Hence, this feature is turned off by default as it +// make little sense for most users as it really isn't the actual input voltage. +// E.g. For purposes of monitoring a battery etc. +// Note: Turning on the feature costs ~250 bytes of prog space. +#define REPORT_VCC false // Do we report Vcc via html info page & MQTT? + +// Keywords for MQTT topics, html arguments, or config file. +#define KEY_PROTOCOL "protocol" +#define KEY_MODEL "model" +#define KEY_POWER "power" +#define KEY_MODE "mode" +#define KEY_TEMP "temp" +#define KEY_HUMIDITY "humidity" +#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_FILTER "filter" +#define KEY_CLEAN "clean" +#define KEY_CELSIUS "use_celsius" +#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. +#define KEY_CODE "code" +#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"; + +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; + +// -------------------------- Json Settings ------------------------------------ + +const uint16_t kJsonConfigMaxSize = 512; // Bytes +const uint16_t kJsonAcStateMaxSize = 1024; // Bytes + +// -------------------------- Debug Settings ----------------------------------- +// Debug output is disabled if any of the IR pins are on the TX (D1) pin. +// See `isSerialGpioUsedByIr()`. +// Note: Debug costs ~6k of program space. +#ifndef DEBUG +#define DEBUG false // Change to 'true' for serial debug output. +#endif // DEBUG + +// ----------------- End of User Configuration Section ------------------------- + +// Constants +#define _MY_VERSION_ "v1.8.2" + +const uint8_t kRebootTime = 15; // Seconds +const uint8_t kQuickDisplayTime = 2; // Seconds + +// Common bit sizes for the simple protocols. +const uint8_t kCommonBitSizes[] = { + 12, 13, 15, 16, 20, 24, 28, 32, 35, 36, 42, 48, 56, 64}; +// Gpio related +#if defined(ESP8266) +const int8_t kTxGpios[] = {-1, 0, 1, 2, 3, 4, 5, 12, 13, 14, 15, 16}; +const int8_t kRxGpios[] = {-1, 0, 1, 2, 3, 4, 5, 12, 13, 14, 15}; +#endif // ESP8266 +#if defined(ESP32) +// Ref: https://randomnerdtutorials.com/esp32-pinout-reference-gpios/ +const int8_t kTxGpios[] = { + -1, 0, 1, 2, 3, 4, 5, 12, 13, 14, 15, 16, 17, 18, 19, 21, 22, 23, + 25, 26, 27, 32, 33}; +const int8_t kRxGpios[] = { + -1, 1, 2, 3, 4, 5, 12, 13, 14, 15, 16, 17, 18, 19, 21, 22, 23, + 25, 26, 27, 32, 33, 34, 35, 36, 39}; +#endif // ESP32 + +// JSON stuff +// Name of the json config file in SPIFFS. +const char* const kConfigFile = "/config.json"; +const char* const kMqttServerKey = "mqtt_server"; +const char* const kMqttPortKey = "mqtt_port"; +const char* const kMqttUserKey = "mqtt_user"; +const char* const kMqttPassKey = "mqtt_pass"; +const char* const kMqttPrefixKey = "mqtt_prefix"; +const char* const kHostnameKey = "hostname"; +const char* const kHttpUserKey = "http_user"; +const char* const kHttpPassKey = "http_pass"; +const char* const kCommandDelimiter = ","; + +// URLs +const char* const kUrlRoot = "/"; +const char* const kUrlAdmin = "/admin"; +const char* const kUrlAircon = "/aircon"; +const char* const kUrlSendDiscovery = "/send_discovery"; +const char* const kUrlExamples = "/examples"; +const char* const kUrlGpio = "/gpio"; +const char* const kUrlGpioSet = "/gpio/set"; +const char* const kUrlInfo = "/info"; +const char* const kUrlReboot = "/quitquitquit"; +const char* const kUrlWipe = "/reset"; +const char* const kUrlClearMqtt = "/clear_retained"; + +#if MQTT_ENABLE +const uint32_t kBroadcastPeriodMs = MQTTbroadcastInterval * 1000; // mSeconds. +// How long should we listen to recover for previous states? +// Default is 5 seconds per IR TX GPIOs (channels) used. +const uint32_t kStatListenPeriodMs = 5 * 1000 * kNrOfIrTxGpios; // mSeconds +const int32_t kMaxPauseMs = 10000; // 10 Seconds. +const char* const kSequenceDelimiter = ";"; +const char kPauseChar = 'P'; +#if defined(ESP8266) +const uint32_t kChipId = ESP.getChipId(); +#endif // ESP8266 +#if defined(ESP32) +const uint32_t kChipId = ESP.getEfuseMac(); // Discard the top 16 bits. +#endif // ESP32 + +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_COMMAND "|" + "|" KEY_SENSORTEMP "|" KEY_IFEEL +#if MQTT_CLIMATE_JSON + "|" KEY_JSON +#endif // MQTT_CLIMATE_JSON + ")
"; +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. + + +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 char* str); +bool mountSpiffs(void); +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, String id_channel); +void doBroadcast(TimerMs *timer, const uint32_t interval, + IRac *climates[], const bool retain, + const bool force); +#if MQTT_CLIMATE_JSON +stdAc::state_t jsonToState(const stdAc::state_t current, const char *str); +void sendJsonState(const stdAc::state_t state, const String topic, + const bool retain = false, + const bool ha_mode = MQTT_CLIMATE_HA_MODE); +#endif // MQTT_CLIMATE_JSON +#endif // MQTT_ENABLE +#if REPORT_VCC +String vccToString(void); +#endif // REPORT_VCC +bool isSerialGpioUsedByIr(void); +void debug(const char *str); +void saveWifiConfigCallback(void); +void saveWifiConfig(void); +void loadWifiConfigFile(void); +void doRestart(const char* str, const bool serial_only = false); +String msToHumanString(uint32_t const msecs); +String timeElapsed(uint32_t const msec); +String timeSince(uint32_t const start); +String gpioToString(const int16_t gpio); +uint8_t getDefaultIrSendIdx(void); +IRsend* getDefaultIrSendPtr(void); +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 = "", + const String headScriptsJS = ""); +String htmlEnd(void); +String htmlButton(const String url, const String button, + const String text = ""); +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); +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 decode_type_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 +#if SHT3X_SUPPORT +void sendMQTTDiscoverySensor(const char *topic, String type); +#endif // SH3X_SUPPORT +void handleIr(void); +void handleNotFound(void); +void setup_wifi(void); +void init_vars(void); +void setup(void); +void loop(void); +uint32_t maxSketchSpace(void); +uint64_t getUInt64fromHex(char const *str); +bool sendIRCode(IRsend *irsend, decode_type_t 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); +void updateClimate(stdAc::state_t *current, const String str, + const String prefix, const String payload); +bool cmpClimate(const stdAc::state_t a, const stdAc::state_t b); +bool sendClimate(const String topic_prefix, const bool retain, + const bool forceMQTT, const bool forceIR, + const bool enableIR = true, IRac *ac = NULL); +bool decodeCommonAc(const decode_results *decode); +#endif // EXAMPLES_IRMQTTSERVER_IRMQTTSERVER_H_ \ No newline at end of file diff --git a/src/main.cpp b/src/main.cpp index be34d4d..505ff7b 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1,186 +1,3938 @@ /* - * IRremoteESP8266: IRrecvDumpV2 - dump details of IR codes with IRrecv - * An IR detector/demodulator must be connected to the input kRecvPin. + * Send & receive arbitrary IR codes via a web server or MQTT. + * Copyright David Conran 2016-2021 * - * Copyright 2009 Ken Shirriff, http://arcfn.com - * Copyright 2017-2019 David Conran + * 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. * - * Example circuit diagram: - * https://github.com/crankyoldgit/IRremoteESP8266/wiki#ir-receiving + * NOTE: An IR LED circuit SHOULD be connected to the ESP if + * you want to send IR messages. e.g. GPIO4 (D2) + * A compatible IR RX modules SHOULD be connected to ESP + * if you want to capture & decode IR nessages. e.g. GPIO14 (D5) + * See 'IR_RX' in IRMQTTServer.h. + * GPIOs are configurable from the http:///gpio + * page. * - * Changes: - * Version 1.2 October, 2020 - * - Enable easy setting of the decoding tolerance value. - * Version 1.0 October, 2019 - * - Internationalisation (i18n) support. - * - Stop displaying the legacy raw timing info. - * Version 0.5 June, 2019 - * - Move A/C description to IRac.cpp. - * Version 0.4 July, 2018 - * - Minor improvements and more A/C unit support. - * Version 0.3 November, 2017 - * - Support for A/C decoding for some protocols. - * Version 0.2 April, 2017 - * - Decode from a copy of the data so we can start capturing faster thus - * reduce the likelihood of miscaptures. - * Based on Ken Shirriff's IrsendDemo Version 0.1 July, 2009, + * 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) + * - Disable MQTT if desired. (see '#define MQTT_ENABLE' in IRMQTTServer.h). + * + * - The MQTT server IP is detected automatically through mDNS (aka avahi, + * bonjour, zeroconf) if the server advertises _mqtt._tcp. Disable this if + * desired (see '#define MQTT_SERVER_AUTODETECT_ENABLE' in IRMQTTServer.h). + * + * - Site specific settings: + * 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 + * - ArduinoJson (https://arduinojson.org/) (Version >= 6.0) + * - PubSubClient (https://pubsubclient.knolleary.net/) (Version >= 2.8.0) + * - WiFiManager (https://github.com/tzapu/WiFiManager) + * (ESP8266: Version >= 0.14, ESP32: 'master' branch.) + * o Use the smallest non-zero FILESYSTEM size you can for your board. + * (See the Tools -> Flash Size menu) + * + * - PlatformIO IDE: + * If you are using PlatformIO, this should already been done for you in + * the accompanying platformio.ini file. + * + * ## First Boot (Initial setup) + * The ESP 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 ESP 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 and saved settings to go back to "First Boot", + * visit: http:///reset + * + * ## Normal Use (After initial setup) + * Enter 'http:///gpio page to configure the GPIOs + * for the IR LED(s) and/or IR RX demodulator. + * + * You can send URLs like the following, with similar data type limitations as + * the MQTT formating in the next section. e.g: + * http:///ir?type=7&code=E0E09966 + * http:///ir?type=4&code=0xf50&bits=12 + * http:///ir?code=C1A2E21D&repeats=8&type=19 + * http:///ir?type=31&code=40000,1,1,96,24,24,24,48,24,24,24,24,24,48,24,24,24,24,24,48,24,24,24,24,24,24,24,24,1058 + * http:///ir?type=18&code=190B8050000000E0190B8070000010f0 + * http:///ir?repeats=1&type=25&code=0000,006E,0022,0002,0155,00AA,0015,0040,0015,0040,0015,0015,0015,0015,0015,0015,0015,0015,0015,0015,0015,0040,0015,0040,0015,0015,0015,0040,0015,0015,0015,0015,0015,0015,0015,0040,0015,0015,0015,0015,0015,0040,0015,0040,0015,0015,0015,0015,0015,0015,0015,0015,0015,0015,0015,0040,0015,0015,0015,0015,0015,0040,0015,0040,0015,0040,0015,0040,0015,0040,0015,0640,0155,0055,0015,0E40 + * If you have enabled more than 1 TX GPIO, you can use the "channel" argument: + * http:///ir?channel=0&type=7&code=E0E09966 + * http:///ir?channel=1&type=7&code=E0E09966 + * + * or + * + * Send a MQTT message to the topic 'ir_server/send' + * (or 'ir_server/send_1' etc if you have enabled more than 1 TX GPIO) + * using the following format (Order is important): + * protocol_num,hexcode + * e.g. 7,E0E09966 + * which is: Samsung(7), Power On code, default bit size, + * default nr. of repeats. + * + * protocol_num,hexcode,bits + * e.g. 4,f50,12 + * which is: Sony(4), Power Off code, 12 bits & default nr. of repeats. + * + * protocol_num,hexcode,bits,repeats + * e.g. 19,C1A2E21D,0,8 + * which is: Sherwood(19), Vol Up, default bit size & repeated 8 times. + * + * 30,frequency,raw_string + * e.g. 30,38000,9000,4500,500,1500,500,750,500,750 + * which is: Raw (30) @ 38kHz with a raw code of + * "9000,4500,500,1500,500,750,500,750" + * + * 31,code_string + * e.g. 31,40000,1,1,96,24,24,24,48,24,24,24,24,24,48,24,24,24,24,24,48,24,24,24,24,24,24,24,24,1058 + * which is: GlobalCache (31) & "40000,1,1,96,..." (Sony Vol Up) + * + * 25,Rrepeats,hex_code_string + * e.g. 25,R1,0000,006E,0022,0002,0155,00AA,0015,0040,0015,0040,0015,0015,0015,0015,0015,0015,0015,0015,0015,0015,0015,0040,0015,0040,0015,0015,0015,0040,0015,0015,0015,0015,0015,0015,0015,0040,0015,0015,0015,0015,0015,0040,0015,0040,0015,0015,0015,0015,0015,0015,0015,0015,0015,0015,0015,0040,0015,0015,0015,0015,0015,0040,0015,0040,0015,0040,0015,0040,0015,0040,0015,0640,0155,0055,0015,0E40 + * which is: Pronto (25), 1 repeat, & "0000 006E 0022 0002 ..." + * aka a "Sherwood Amp Tape Input" message. + * + * ac_protocol_num,really_long_hexcode + * e.g. 18,190B8050000000E0190B8070000010F0 + * which is: Kelvinator (18) Air Con on, Low Fan, 25 deg etc. + * NOTE: Ensure you zero-pad to the correct number of digits for the + * bit/byte size you want to send as some A/C units have units + * have different sized messages. e.g. Fujitsu A/C units. + * + * Sequences. + * You can send a sequence of IR messages via MQTT using the above methods + * if you separate them with a ';' character. In addition you can add a + * pause/gap between sequenced messages by using 'P' followed immediately by + * the number of milliseconds you wish to wait (up to a max of kMaxPauseMs). + * e.g. 7,E0E09966;4,f50,12 + * Send a Samsung(7) TV Power on code, followed immediately by a Sony(4) + * TV power off message. + * or: 19,C1A28877;P500;19,C1A25AA5;P500;19,C1A2E21D,0,30 + * Turn on a Sherwood(19) Amplifier, Wait 1/2 a second, Switch the + * Amplifier to Video input 2, wait 1/2 a second, then send the Sherwood + * Amp the "Volume Up" message 30 times. + * + * In short: + * No spaces after/before commas. + * Values are comma separated. + * The first value is always in Decimal. + * For simple protocols, the next value (hexcode) is always hexadecimal. + * The optional bit size is in decimal. + * CAUTION: Some AC protocols DO NOT use the really_long_hexcode method. + * e.g. < 64bit AC protocols. + * + * Unix command line usage example: + * # Install a MQTT client + * $ sudo apt install mosquitto-clients + * # Send a 32-bit NEC code of 0x1234abcd via MQTT. + * $ mosquitto_pub -h 10.0.0.4 -t ir_server/send -m '3,1234abcd,32' + * + * This server will send (back) what ever IR message it just transmitted to + * the MQTT topic 'ir_server/sent' to confirm it has been performed. This works + * for messages requested via MQTT or via HTTP. + * + * Unix command line usage example: + * # Listen to MQTT acknowledgements. + * $ mosquitto_sub -h 10.0.0.4 -t ir_server/sent + * + * Incoming IR messages (from an IR remote control) will be transmitted to + * the MQTT topic 'ir_server/received'. The MQTT message will be formatted + * similar to what is required to for the 'sent' topic. + * e.g. "3,C1A2F00F,32" (Protocol,Value,Bits) for simple codes + * or "18,110B805000000060110B807000001070" (Protocol,Value) for complex codes + * Note: If the protocol is listed as -1, then that is an UNKNOWN IR protocol. + * You can't use that to recreate/resend an IR message. It's only for + * matching purposes and shouldn't be trusted. + * + * Unix command line usage example: + * # Listen via MQTT for IR messages captured by this server. + * $ mosquitto_sub -h 10.0.0.4 -t ir_server/received + * + * Note: General logging messages are also sent to 'ir_server/log' from + * time to time. + * + * ## Climate (AirCon) interface. (Advanced use) + * You can now control Air Conditioner devices that have full/detailed support + * from the IRremoteESP8266 library. See the "Aircon" page for list of supported + * devices. You can do this via HTTP/HTML or via MQTT. + * + * NOTE: It will only change the attributes you change/set. It's up to you to + * maintain a consistent set of attributes for your particular aircon. + * + * TIP: Use "-1" for 'model' if your A/C doesn't have a specific `setModel()` + * or IR class attribute. Most don't. Some do. + * e.g. PANASONIC_AC, FUJITSU_AC, WHIRLPOOL_AC + * + * ### via MQTT: + * The code listen for commands (via wildcard) on the MQTT topics at the + * `ir_server/ac/cmnd/+` level (or ir_server/ac_1/cmnd/+` if multiple TX GPIOs) + * such as: + * i.e. protocol, model, power, mode, temp, fanspeed, swingv, swingh, quiet, + * turbo, light, beep, econo, sleep, filter, clean, use_celsius + * e.g. ir_server/ac/cmnd/power, ir_server/ac/cmnd/temp, + * ir_server/ac_0/cmnd/mode, ir_server/ac_2/cmnd/fanspeed, etc. + + * It will process them, and if successful and it caused a change, it will + * acknowledge this via the relevant state topic for that command. + * e.g. If the aircon/climate changes from power off to power on, it will + * send an "on" payload to "ir_server/ac/stat/power" + * + * There is a special command available to force the ESP to resend the current + * A/C state in an IR message. To do so use the `resend` command MQTT topic, + * e.g. `ir_server/ac/cmnd/resend` with a payload message of `resend`. + * There is no corresponding "stat" message update for this particular topic, + * but a log message is produced indicating it was received. + * + * NOTE: These "stat" messages have the MQTT retain flag set to on. Thus the + * MQTT broker will remember them until reset/restarted etc. + * + * The code will also periodically broadcast all possible aircon/climate state + * attributes to their corresponding "ir_server/ac/stat" topics. This ensures + * any updates to the ESP's knowledge that may have been lost in transmission + * are re-communicated. e.g. The MQTT broker being offline. + * This also helps with Home Assistant MQTT discovery. + * + * The program on boot & first successful connection to the MQTT broker, will + * try to re-acquire any previous aircon/climate state information and act + * accordingly. This will typically result in A/C IR message being sent as and + * saved state will probably be different from the defaults. + * + * NOTE: Command attributes are processed sequentially. + * e.g. Going from "25C, cool, fan low" to "27C, heat, fan high" may go + * via "27C, cool, fan low" & "27C, heat, fan low" depending on the order + * of arrival & processing of the MQTT commands. + * + * ### Home Assistant (HA) MQTT climate integration + * After you have set the Protocol (required) & Model (if needed) and any of + * the other misc aircon settings you desire, you can then add the following to + * your Home Assistant configuration, and it should allow you to + * control most of the important settings. Google Home/Assistant (via HA) + * can also control the device, but you will need to configure Home Assistant + * via it's documentation for that. It has even more limited control. + * It's far beyond the scope of these instructions to guide you through setting + * up HA and Google Home integration. See https://www.home-assistant.io/ + * + * In HA's configuration.yaml, add: + * + * #### New format (Post Home Assistant 2022.6 release) + * + * mqtt: + * climate: + * - name: Living Room Aircon + * modes: + * - "off" + * - "auto" + * - "cool" + * - "heat" + * - "dry" + * - "fan_only" + * fan_modes: + * - "Auto" + * - "Min" + * - "Low" + * - "Medium" + * - "High" + * - "Max" + * swing_modes: + * - "Off" + * - "Auto" + * - "Highest" + * - "High" + * - "Middle" + * - "Low" + * - "Lowest" + * power_command_topic: "ir_server/ac/cmnd/power" + * mode_command_topic: "ir_server/ac/cmnd/mode" + * mode_state_topic: "ir_server/ac/stat/mode" + * temperature_command_topic: "ir_server/ac/cmnd/temp" + * temperature_state_topic: "ir_server/ac/stat/temp" + * fan_mode_command_topic: "ir_server/ac/cmnd/fanspeed" + * fan_mode_state_topic: "ir_server/ac/stat/fanspeed" + * swing_mode_command_topic: "ir_server/ac/cmnd/swingv" + * swing_mode_state_topic: "ir_server/ac/stat/swingv" + * min_temp: 16 + * max_temp: 32 + * temp_step: 1 + * retain: false + * + * #### Old format (Pre Home Assistant 2022.6 release) + * + * climate: + * - platform: mqtt + * name: Living Room Aircon + * modes: + * - "off" + * - "auto" + * - "cool" + * - "heat" + * - "dry" + * - "fan_only" + * fan_modes: + * - "Auto" + * - "Min" + * - "Low" + * - "Medium" + * - "High" + * - "Max" + * swing_modes: + * - "Off" + * - "Auto" + * - "Highest" + * - "High" + * - "Middle" + * - "Low" + * - "Lowest" + * power_command_topic: "ir_server/ac/cmnd/power" + * mode_command_topic: "ir_server/ac/cmnd/mode" + * mode_state_topic: "ir_server/ac/stat/mode" + * temperature_command_topic: "ir_server/ac/cmnd/temp" + * temperature_state_topic: "ir_server/ac/stat/temp" + * fan_mode_command_topic: "ir_server/ac/cmnd/fanspeed" + * fan_mode_state_topic: "ir_server/ac/stat/fanspeed" + * swing_mode_command_topic: "ir_server/ac/cmnd/swingv" + * swing_mode_state_topic: "ir_server/ac/stat/swingv" + * min_temp: 16 + * max_temp: 32 + * temp_step: 1 + * retain: false + * + * #### Home Assistant MQTT Discovery + * There is an option for this: 'Send MQTT Discovery' under the 'Admin' menu. + * It will produce a single MQTT Climate Discovery message for Home Assistant + * provided you have everything configured correctly here and in HA. + * This message has MQTT RETAIN set on it, so it only ever needs to be sent + * once or if the config details change etc. + * + * If you no longer want it, manually remove it from your MQTT broker. + * e.g. + * `mosquitto_pub -t homeassistant/climate/ir_server/config -n -r -d` + * + * NOTE: If you have multiple TX GPIOs configured, it *ONLY* works for the + * first TX GPIO climate. You will need to manually configure the others. + * + * ### via HTTP: + * Use the "http:///aircon/set" URL and pass on + * the arguments as needed to control your device. See the `KEY_*` #defines + * in the code for all the parameters. + * i.e. protocol, model, power, mode, temp, fanspeed, swingv, swingh, quiet, + * turbo, light, beep, econo, sleep, filter, clean, use_celsius, channel + * Example: + * http:///aircon/set?channel=0&protocol=PANASONIC_AC&model=LKE&power=on&mode=auto&fanspeed=min&temp=23 + * + * NOTE: If you don't set the channel, the first GPIO (Channel 0) is used. + * + * ## Debugging & Logging + * If DEBUG is turned on, there is additional information printed on the Serial + * Port. Serial Port output may be disabled if the GPIO is used for IR. + * + * If MQTT is enabled, some information/logging is sent to the MQTT topic: + * `ir_server/log` + * + * ## Updates + * You can upload new firmware Over The Air (OTA) via the form on the device's + * "Admin" page. No need to connect to the device again via USB. \o/ + * Your settings should be remembered between updates. \o/ \o/ + * + * On boards with 1 Meg of flash should use an SPIFFS size of 64k if you want a + * hope of being able to load a firmware via OTA. + * Boards with only 512k flash have no chance of OTA with this firmware. + * + * ## Security + * + * There is NO authentication set on the HTTP/HTML interface by default (see + * `HTML_PASSWORD_ENABLE` to change that), and there is NO SSL/TLS (encryption) + * used by this example code. + * i.e. All usernames & passwords are sent in clear text. + * All communication to the MQTT server is in clear text. + * e.g. This on/using the public Internet is a 'Really Bad Idea'! + * You should NOT have or use this code or device exposed on an untrusted and/or + * unprotected network. + * If you allow access to OTA firmware updates, then a 'Bad Guy' could + * potentially compromise your network. OTA updates are password protected by + * default. If you are sufficiently paranoid, you SHOULD disable uploading + * firmware via OTA. (see 'FIRMWARE_OTA') + * You SHOULD also set/change all usernames & passwords. + * For extra bonus points: Use a separate untrusted SSID/vlan/network/ segment + * for your IoT stuff, including this device. + * Caveat Emptor. You have now been suitably warned. + * */ - +#include "IRMQTTServer.h" #include -#include -#include -#include -#include -#include -#include -// ==================== start of TUNEABLE PARAMETERS ==================== -// An IR detector/demodulator is connected to GPIO pin 14 -// e.g. D5 on a NodeMCU board. -// Note: GPIO 16 won't work on the ESP8266 as it does not have interrupts. -// Note: GPIO 14 won't work on the ESP32-C3 as it causes the board to reboot. -#ifdef ARDUINO_ESP32C3_DEV -const uint16_t kRecvPin = 10; // 14 on a ESP32-C3 causes a boot loop. -#else // ARDUINO_ESP32C3_DEV -const uint16_t kRecvPin = 14; -#endif // ARDUINO_ESP32C3_DEV - -// The Serial connection baud rate. -// i.e. Status message will be sent to the PC at this baud rate. -// Try to avoid slow speeds like 9600, as you will miss messages and -// cause other problems. 115200 (or faster) is recommended. -// NOTE: Make sure you set your Serial Monitor to the same speed. -const uint32_t kBaudRate = 115200; - -// As this program is a special purpose capture/decoder, let us use a larger -// than normal buffer so we can handle Air Conditioner remote codes. -const uint16_t kCaptureBufferSize = 1024; - -// kTimeout is the Nr. of milli-Seconds of no-more-data before we consider a -// message ended. -// This parameter is an interesting trade-off. The longer the timeout, the more -// complex a message it can capture. e.g. Some device protocols will send -// multiple message packets in quick succession, like Air Conditioner remotes. -// Air Coniditioner protocols often have a considerable gap (20-40+ms) between -// packets. -// The downside of a large timeout value is a lot of less complex protocols -// send multiple messages when the remote's button is held down. The gap between -// them is often also around 20+ms. This can result in the raw data be 2-3+ -// times larger than needed as it has captured 2-3+ messages in a single -// capture. Setting a low timeout value can resolve this. -// So, choosing the best kTimeout value for your use particular case is -// quite nuanced. Good luck and happy hunting. -// NOTE: Don't exceed kMaxTimeoutMs. Typically 130ms. -#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 kTimeout = 50; -#else // DECODE_AC -// Suits most messages, while not swallowing many repeats. -const uint8_t kTimeout = 15; -#endif // DECODE_AC -// Alternatives: -// const uint8_t kTimeout = 90; -// Suits messages with big gaps like XMP-1 & some aircon units, but can -// accidentally swallow repeated messages in the rawData[] output. -// -// const uint8_t kTimeout = kMaxTimeoutMs; -// This will set it to our currently allowed maximum. -// Values this high are problematic because it is roughly the typical boundary -// where most messages repeat. -// e.g. It will stop decoding a message and start sending it to serial at -// precisely the time when the next message is likely to be transmitted, -// and may miss it. - -// Set the smallest sized "UNKNOWN" message packets we actually care about. -// This value helps reduce the false-positive detection rate of IR background -// noise as real messages. The chances of background IR noise getting detected -// as a message increases with the length of the kTimeout value. (See above) -// The downside of setting this message too large is you can miss some valid -// short messages for protocols that this library doesn't yet decode. -// -// Set higher if you get lots of random short UNKNOWN messages when nothing -// should be sending a message. -// Set lower if you are sure your setup is working, but it doesn't see messages -// from your device. (e.g. Other IR remotes work.) -// NOTE: Set this value very high to effectively turn off UNKNOWN detection. -const uint16_t kMinUnknownSize = 12; - -// How much percentage lee way do we give to incoming signals in order to match -// it? -// e.g. +/- 25% (default) to an expected value of 500 would mean matching a -// value between 375 & 625 inclusive. -// Note: Default is 25(%). Going to a value >= 50(%) will cause some protocols -// to no longer match correctly. In normal situations you probably do not -// need to adjust this value. Typically that's when the library detects -// your remote's message some of the time, but not all of the time. -const uint8_t kTolerancePercentage = kTolerance; // kTolerance is normally 25% - -// Legacy (No longer supported!) -// -// Change to `true` if you miss/need the old "Raw Timing[]" display. -#define LEGACY_TIMING_INFO false -// ==================== end of TUNEABLE PARAMETERS ==================== - -// Use turn on the save buffer feature for more complete capture coverage. -IRrecv irrecv(kRecvPin, kCaptureBufferSize, kTimeout, true); -decode_results results; // Somewhere to store the results - -// This section of code runs only once at start-up. -void setup() -{ +#include #if defined(ESP8266) - Serial.begin(kBaudRate, SERIAL_8N1, SERIAL_TX_ONLY); -#else // ESP8266 - Serial.begin(kBaudRate, SERIAL_8N1); -#endif // ESP8266 - while (!Serial) // Wait for the serial connection to be establised. - delay(50); - // Perform a low level sanity checks that the compiler performs bit field - // packing as we expect and Endianness is as we expect. - assert(irutils::lowLevelSanityCheck() == 0); +#include +#include +#include +#endif // ESP8266 +#if defined(ESP32) +#include +#include +#include +#include +#endif // ESP32 +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#if MQTT_ENABLE +#include +#endif // MQTT_ENABLE +#include // NOLINT(build/include) +#include +#include - Serial.printf("\n" D_STR_IRRECVDUMP_STARTUP "\n", kRecvPin); -#if DECODE_HASH - // Ignore messages with less than minimum on or off pulses. - irrecv.setUnknownThreshold(kMinUnknownSize); -#endif // DECODE_HASH - irrecv.setTolerance(kTolerancePercentage); // Override the default tolerance. - irrecv.enableIRIn(); // Start the receiver +#ifdef ESP32 +#ifdef F +#undef F +#endif // F +#define F(string) string +#endif // ESP32 +using irutils::msToString; + +#if REPORT_VCC +ADC_MODE(ADC_VCC); +#endif // REPORT_VCC + +#ifdef SHT3X_SUPPORT +#include +#endif + +// Globals +uint8_t _sanity = 0; +#if defined(ESP8266) +ESP8266WebServer server(kHttpPort); +#endif // ESP8266 +#if defined(ESP32) +WebServer server(kHttpPort); +#endif // ESP32 +#if MDNS_ENABLE +MDNSResponder mdns; +#endif // MDNS_ENABLE +WiFiClient espClient; +WiFiManager wifiManager; +bool flagSaveWifiConfig = false; +char HttpUsername[kUsernameLength + 1] = "admin"; // Default HTTP 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; +volatile bool lockIr = false; // Primitive locking for gating the IR LED. +uint32_t sendReqCounter = 0; +bool lastSendSucceeded = false; // Store the success status of the last send. +uint32_t lastSendTime = 0; +int8_t offset; // The calculated period offset for this chip and library. +IRsend *IrSendTable[kNrOfIrTxGpios]; +int8_t txGpioTable[kNrOfIrTxGpios] = {kDefaultIrLed}; +String lastClimateSource; +#if IR_RX +IRrecv *irrecv = NULL; +decode_results capture; // Somewhere to store inbound IR messages. +int8_t rx_gpio = kDefaultIrRx; +String lastIrReceived = FPSTR("None"); +uint32_t lastIrReceivedTime = 0; +uint32_t irRecvCounter = 0; +#endif // IR_RX + +// Climate stuff +IRac *climate[kNrOfIrTxGpios]; +String channel_re = FPSTR("("); // Will be built later. +uint16_t chan = 0; // The channel to use for the aircon HTML page. + +TimerMs lastClimateIr = TimerMs(); // When we last sent the IR Climate mesg. +uint32_t irClimateCounter = 0; // How many have we sent? +// Store the success status of the last climate send. +bool lastClimateSucceeded = false; +bool hasClimateBeenSent = false; // Has the Climate ever been sent? + +#if MQTT_ENABLE +PubSubClient mqtt_client(espClient); +String lastMqttCmd = FPSTR("None"); +String lastMqttCmdTopic = FPSTR("None"); +uint32_t lastMqttCmdTime = 0; +uint32_t lastConnectedTime = 0; +uint32_t lastDisconnectedTime = 0; +uint32_t mqttDisconnectCounter = 0; +uint32_t mqttSentCounter = 0; +uint32_t mqttRecvCounter = 0; +bool wasConnected = true; + +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. +#if MQTT_DISCOVERY_ENABLE +String MqttDiscovery; +String MqttUniqueId; +#if SHT3X_SUPPORT && SHT3X_MQTT_DISCOVERY_ENABLE +String MqttDiscoverySensor; +#endif // SHT3X_SUPPORT && SHT3X_MQTT_DISCOVERY_ENABLE +#endif // MQTT_DISCOVERY_ENABLE +String MqttHAName; +String MqttClientId; +#if SHT3X_SUPPORT +String MqttSensorStat; +#endif // SHT3X_SUPPORT + +// Primative lock file for gating MQTT state broadcasts. +bool lockMqttBroadcast = true; +TimerMs lastBroadcast = TimerMs(); // When we last sent a broadcast. +bool hasBroadcastBeenSent = false; +#if MQTT_DISCOVERY_ENABLE +TimerMs lastDiscovery = TimerMs(); // When we last sent a Discovery. +bool hasDiscoveryBeenSent = false; +#endif // MQTT_DISCOVERY_ENABLE +TimerMs statListenTime = TimerMs(); // How long we've been listening for. +#endif // MQTT_ENABLE + +bool isSerialGpioUsedByIr(void) +{ + const int8_t kSerialTxGpio = 1; // The GPIO serial output is sent to. + // Note: *DOES NOT* control Serial output. +#if defined(ESP32) + const int8_t kSerialRxGpio = 3; // The GPIO serial input is received on. +#endif // ESP32 + // Ensure we are not trodding on anything IR related. +#if IR_RX + switch (rx_gpio) + { +#if defined(ESP32) + case kSerialRxGpio: +#endif // ESP32 + case kSerialTxGpio: + return true; // Serial port is in use by IR capture. Abort. + } +#endif // IR_RX + for (uint16_t i = 0; i < kNrOfIrTxGpios; i++) + switch (txGpioTable[i]) + { +#if defined(ESP32) + case kSerialRxGpio: +#endif // ESP32 + case kSerialTxGpio: + return true; // Serial port is in use for IR sending. Abort. + } + return false; // Not in use as far as we can tell. } -// The repeating section of the code -void loop() +#if SHT3X_SUPPORT +SHT3X TemperatureSensor(SHT3X_I2C_ADDRESS); +TimerMs statSensorReadTime = TimerMs(); +#endif // SHT3X_SUPPORT + +// Debug messages get sent to the serial port. +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wunused-parameter" +void debug(const char *str) { - // Check if the IR code has been received. - if (irrecv.decode(&results)) - { - // Display a crude timestamp. - uint32_t now = millis(); - Serial.printf(D_STR_TIMESTAMP " : %06u.%03u\n", now / 1000, now % 1000); - // Check if we got an IR message that was to big for our capture buffer. - if (results.overflow) - Serial.printf(D_WARN_BUFFERFULL "\n", kCaptureBufferSize); - // Display the library version the message was captured with. - Serial.println(D_STR_LIBRARY " : v" _IRREMOTEESP8266_VERSION_STR "\n"); - // Display the tolerance percentage if it has been change from the default. - if (kTolerancePercentage != kTolerance) - Serial.printf(D_STR_TOLERANCE " : %d%%\n", kTolerancePercentage); - // Display the basic output of what we found. - Serial.print(resultToHumanReadableBasic(&results)); - // Display any extra A/C info if we have it. - String description = IRAcUtils::resultAcToString(&results); - if (description.length()) - Serial.println(D_STR_MESGDESC ": " + description); - yield(); // Feed the WDT as the text output can take a while to print. -#if LEGACY_TIMING_INFO - // Output legacy RAW timing info of the result. - Serial.println(resultToTimingInfo(&results)); - yield(); // Feed the WDT (again) -#endif // LEGACY_TIMING_INFO - // Output the results as source code - Serial.println(resultToSourceCode(&results)); - Serial.println(); // Blank line between entries - yield(); // Feed the WDT (again) +#if DEBUG + if (isSerialGpioUsedByIr()) + return; // Abort. + uint32_t now = millis(); + Serial.printf("%07u.%03u: %s\n", now / 1000, now % 1000, str); +#endif // DEBUG +} +#pragma GCC diagnostic pop + +// callback notifying us of the need to save the wifi config +void saveWifiConfigCallback(void) +{ + debug("saveWifiConfigCallback called."); + flagSaveWifiConfig = true; +} + +// Forcibly mount the FILESYSTEM. Formatting the FILESYSTEM if needed. +// +// Returns: +// A boolean indicating success or failure. +bool mountSpiffs(void) +{ + debug("Mounting " FILESYSTEMSTR " ..."); + if (FILESYSTEM.begin()) + return true; // We mounted it okay. + // We failed the first time. + debug("Failed to mount " FILESYSTEMSTR "!\n" + "Formatting SPIFFS and trying again..."); + FILESYSTEM.format(); + if (!FILESYSTEM.begin()) + { // Did we fail? + debug("DANGER: Failed to mount " FILESYSTEMSTR " even after formatting!"); + + delay(10000); // Make sure the debug message doesn't just float by. + return false; } -} \ No newline at end of file + return true; // Success! +} + +bool saveConfig(void) +{ + debug("Saving the config."); + bool success = false; + DynamicJsonDocument json(kJsonConfigMaxSize); +#if MQTT_ENABLE + 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 IR_RX + json[KEY_RX_GPIO] = static_cast(rx_gpio); +#endif // IR_RX + for (uint16_t i = 0; i < kNrOfIrTxGpios; i++) + { + const String key = KEY_TX_GPIO + String(i); + json[key] = static_cast(txGpioTable[i]); + } + + if (mountSpiffs()) + { + File configFile = FILESYSTEM.open(kConfigFile, "w"); + if (!configFile) + { + debug("Failed to open config file for writing."); + } + else + { + debug("Writing out the config file."); + serializeJson(json, configFile); + configFile.close(); + debug("Finished writing config file."); + success = true; + } + FILESYSTEM.end(); + } + return success; +} + +bool loadConfigFile(void) +{ + bool success = false; + if (mountSpiffs()) + { + debug("mounted the file system"); + if (FILESYSTEM.exists(kConfigFile)) + { + debug("config file exists"); + File configFile = FILESYSTEM.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); + DynamicJsonDocument json(kJsonConfigMaxSize); + if (!deserializeJson(json, buf.get(), kJsonConfigMaxSize)) + { + debug("Json config file parsed ok."); +#if MQTT_ENABLE + 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); + // Read in the GPIO settings. +#if IR_RX + // Single RX gpio + rx_gpio = json[KEY_RX_GPIO] | kDefaultIrRx; +#endif // IR_RX + // Potentially multiple TX gpios + for (uint16_t i = 0; i < kNrOfIrTxGpios; i++) + txGpioTable[i] = json[String(KEY_TX_GPIO + String(i)).c_str()] | + kDefaultIrLed; + debug("Recovered Json fields."); + success = true; + } + else + { + debug("Failed to load json config"); + } + debug("Closing the config file."); + configFile.close(); + } + } + else + { + debug("Config file doesn't exist!"); + } + debug("Unmounting " FILESYSTEMSTR); + FILESYSTEM.end(); + } + return success; +} + +String timeElapsed(uint32_t const msec) +{ + String result = msToString(msec); + if (result.equalsIgnoreCase(D_STR_NOW)) + return result; + else + return result + F(" ago"); +} + +String timeSince(uint32_t const start) +{ + if (start == 0) + return F("Never"); + uint32_t diff = 0; + uint32_t now = millis(); + if (start < now) + diff = now - start; + else + diff = UINT32_MAX - start + now; + return msToString(diff) + F(" ago"); +} + +String gpioToString(const int16_t gpio) +{ + if (gpio == kGpioUnused) + return F(D_STR_UNUSED); + else + return String(gpio); +} + +int8_t getDefaultTxGpio(void) +{ + for (int16_t i = 0; i < kNrOfIrTxGpios; i++) + if (txGpioTable[i] != kGpioUnused) + return txGpioTable[i]; + return kGpioUnused; +} + +// Return a string containing the comma separated list of sending gpios. +String listOfTxGpios(void) +{ + bool found = false; + String result = ""; + for (uint16_t i = 0; i < kNrOfIrTxGpios; i++) + { + if (i) + result += ", "; + result += gpioToString(txGpioTable[i]); + if (!found && txGpioTable[i] == getDefaultTxGpio()) + { + result += F(" (default)"); + found = true; + } + } + return result; +} + +String htmlMenu(void) +{ + String html = F("
"); + html += htmlButton(kUrlRoot, F("Home")); + html += htmlButton(kUrlAircon, F("Aircon")); +#if EXAMPLES_ENABLE + html += htmlButton(kUrlExamples, F("Examples")); +#endif // EXAMPLES_ENABLE + html += htmlButton(kUrlInfo, F("System Info")); + html += htmlButton(kUrlAdmin, F("Admin")); + html += F("

"); + return html; +} + +String htmlOptionItem(const String value, const String text, bool selected) +{ + String html = F("