diff --git a/3D_Printable_Models/HASwitchPlate_front_2x_decora_hasp.stl b/3D_Printable_Models/HASwitchPlate_front_2x_decora_hasp.stl index 9925377..48eee12 100644 Binary files a/3D_Printable_Models/HASwitchPlate_front_2x_decora_hasp.stl and b/3D_Printable_Models/HASwitchPlate_front_2x_decora_hasp.stl differ diff --git a/3D_Printable_Models/HASwitchPlate_front_2x_hasp_decora.stl b/3D_Printable_Models/HASwitchPlate_front_2x_hasp_decora.stl index c5a02ca..de9df1c 100644 Binary files a/3D_Printable_Models/HASwitchPlate_front_2x_hasp_decora.stl and b/3D_Printable_Models/HASwitchPlate_front_2x_hasp_decora.stl differ diff --git a/3D_Printable_Models/HASwitchPlate_front_2x_hasp_toggle.stl b/3D_Printable_Models/HASwitchPlate_front_2x_hasp_toggle.stl index 7590837..9ee232d 100644 Binary files a/3D_Printable_Models/HASwitchPlate_front_2x_hasp_toggle.stl and b/3D_Printable_Models/HASwitchPlate_front_2x_hasp_toggle.stl differ diff --git a/3D_Printable_Models/HASwitchPlate_front_2x_toggle_hasp.stl b/3D_Printable_Models/HASwitchPlate_front_2x_toggle_hasp.stl index 3c55aa9..0a33c46 100644 Binary files a/3D_Printable_Models/HASwitchPlate_front_2x_toggle_hasp.stl and b/3D_Printable_Models/HASwitchPlate_front_2x_toggle_hasp.stl differ diff --git a/3D_Printable_Models/HASwitchPlate_front_3x_decora_decora_hasp.stl b/3D_Printable_Models/HASwitchPlate_front_3x_decora_decora_hasp.stl index bcb062e..b4a8bbe 100644 Binary files a/3D_Printable_Models/HASwitchPlate_front_3x_decora_decora_hasp.stl and b/3D_Printable_Models/HASwitchPlate_front_3x_decora_decora_hasp.stl differ diff --git a/3D_Printable_Models/HASwitchPlate_front_3x_decora_hasp_decora.stl b/3D_Printable_Models/HASwitchPlate_front_3x_decora_hasp_decora.stl index 8468aca..1dc07d8 100644 Binary files a/3D_Printable_Models/HASwitchPlate_front_3x_decora_hasp_decora.stl and b/3D_Printable_Models/HASwitchPlate_front_3x_decora_hasp_decora.stl differ diff --git a/3D_Printable_Models/HASwitchPlate_front_3x_hasp_decora_decora.stl b/3D_Printable_Models/HASwitchPlate_front_3x_hasp_decora_decora.stl index 21a9ab4..c6da400 100644 Binary files a/3D_Printable_Models/HASwitchPlate_front_3x_hasp_decora_decora.stl and b/3D_Printable_Models/HASwitchPlate_front_3x_hasp_decora_decora.stl differ diff --git a/3D_Printable_Models/HASwitchPlate_front_3x_hasp_toggle_toggle.stl b/3D_Printable_Models/HASwitchPlate_front_3x_hasp_toggle_toggle.stl index 20ce0cd..69faca2 100644 Binary files a/3D_Printable_Models/HASwitchPlate_front_3x_hasp_toggle_toggle.stl and b/3D_Printable_Models/HASwitchPlate_front_3x_hasp_toggle_toggle.stl differ diff --git a/3D_Printable_Models/HASwitchPlate_front_3x_toggle_toggle_hasp.stl b/3D_Printable_Models/HASwitchPlate_front_3x_toggle_toggle_hasp.stl index 13c2959..be9c4fe 100644 Binary files a/3D_Printable_Models/HASwitchPlate_front_3x_toggle_toggle_hasp.stl and b/3D_Printable_Models/HASwitchPlate_front_3x_toggle_toggle_hasp.stl differ diff --git a/3D_Printable_Models/HASwitchPlate_front_4in_box.stl b/3D_Printable_Models/HASwitchPlate_front_4in_box.stl new file mode 100644 index 0000000..6aaf87b Binary files /dev/null and b/3D_Printable_Models/HASwitchPlate_front_4in_box.stl differ diff --git a/3D_Printable_Models/HASwitchPlate_front_4x_hasp_decora_decora_decora.stl b/3D_Printable_Models/HASwitchPlate_front_4x_hasp_decora_decora_decora.stl new file mode 100644 index 0000000..824a5ed Binary files /dev/null and b/3D_Printable_Models/HASwitchPlate_front_4x_hasp_decora_decora_decora.stl differ diff --git a/Arduino_Sketch/.gitignore b/Arduino_Sketch/.gitignore new file mode 100644 index 0000000..89cc49c --- /dev/null +++ b/Arduino_Sketch/.gitignore @@ -0,0 +1,5 @@ +.pio +.vscode/.browse.c_cpp.db* +.vscode/c_cpp_properties.json +.vscode/launch.json +.vscode/ipch diff --git a/Arduino_Sketch/HASwitchPlate.ino.d1_mini.bin b/Arduino_Sketch/HASwitchPlate.ino.d1_mini.bin index 8d89e50..91f8f7b 100644 Binary files a/Arduino_Sketch/HASwitchPlate.ino.d1_mini.bin and b/Arduino_Sketch/HASwitchPlate.ino.d1_mini.bin differ diff --git a/Arduino_Sketch/HASwitchPlate/HASwitchPlate.ino b/Arduino_Sketch/HASwitchPlate/HASwitchPlate.cpp similarity index 93% rename from Arduino_Sketch/HASwitchPlate/HASwitchPlate.ino rename to Arduino_Sketch/HASwitchPlate/HASwitchPlate.cpp index a6282df..c2634b6 100644 --- a/Arduino_Sketch/HASwitchPlate/HASwitchPlate.ino +++ b/Arduino_Sketch/HASwitchPlate/HASwitchPlate.cpp @@ -1,3888 +1,3966 @@ -//////////////////////////////////////////////////////////////////////////////////////////////////// -// _____ _____ _____ _____ -// | | | _ | __| _ | -// | | |__ | __| -// |__|__|__|__|_____|__| -// Home Automation Switch Plate -// https://github.com/aderusha/HASwitchPlate -// -// Copyright (c) 2021 Allen Derusha allen@derusha.org -// -// MIT License -// -// Permission is hereby granted, free of charge, to any person obtaining a copy of this hardware, -// software, and associated documentation files (the "Product"), to deal in the Product without -// restriction, including without limitation the rights to use, copy, modify, merge, publish, -// distribute, sublicense, and/or sell copies of the Product, and to permit persons to whom the -// Product is furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in all copies or -// substantial portions of the Product. -// -// THE PRODUCT IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT -// NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, -// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE PRODUCT OR THE USE OR OTHER DEALINGS IN THE PRODUCT. -//////////////////////////////////////////////////////////////////////////////////////////////////// - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -//////////////////////////////////////////////////////////////////////////////////////////////////// -// These defaults may be overwritten with values saved by the web interface -char wifiSSID[32] = ""; -char wifiPass[64] = ""; -char mqttServer[128] = ""; -char mqttPort[6] = "1883"; -char mqttUser[128] = ""; -char mqttPassword[128] = ""; -char mqttFingerprint[60] = ""; -char haspNode[16] = "plate01"; -char groupName[16] = "plates"; -char hassDiscovery[128] = "homeassistant"; -char configUser[32] = "admin"; -char configPassword[32] = ""; -char motionPinConfig[3] = "0"; -char nextionBaud[7] = "115200"; - -//////////////////////////////////////////////////////////////////////////////////////////////////// - -const float haspVersion = 1.05; // Current HASPone software release version -const uint16_t mqttMaxPacketSize = 2048; // Size of buffer for incoming MQTT message -byte nextionReturnBuffer[128]; // Byte array to pass around data coming from the panel -uint8_t nextionReturnIndex = 0; // Index for nextionReturnBuffer -int8_t nextionActivePage = -1; // Track active LCD page -bool lcdConnected = false; // Set to true when we've heard something from the LCD -const char wifiConfigPass[9] = "hasplate"; // First-time config WPA2 password -const char wifiConfigAP[14] = "HASwitchPlate"; // First-time config SSID -bool shouldSaveConfig = false; // Flag to save json config to SPIFFS -bool nextionReportPage0 = false; // If false, don't report page 0 sendme -const unsigned long updateCheckInterval = 43200000; // Time in msec between update checks (12 hours) -unsigned long updateCheckTimer = updateCheckInterval; // Timer for update check -unsigned long updateCheckFirstRun = 60000; // First-run check offset -bool updateEspAvailable = false; // Flag for update check to report new ESP FW version -float updateEspAvailableVersion; // Float to hold the new ESP FW version number -bool updateLcdAvailable = false; // Flag for update check to report new LCD FW version -unsigned long debugTimer = 0; // Clock for debug performance profiling -bool debugSerialEnabled = true; // Enable USB serial debug output -const unsigned long debugSerialBaud = 115200; // Desired baud rate for serial debug output -bool debugTelnetEnabled = false; // Enable telnet debug output -bool nextionBufferOverrun = false; // Set to true if an overrun error was encountered -bool nextionAckEnable = false; // Wait for each Nextion command to be acked before continuing -bool nextionAckReceived = false; // Ack was received -bool rebootOnp0b1 = false; // When true, reboot device on button press of p[0].b[1] -const unsigned long nextionAckTimeout = 1000; // Timeout to wait for an ack before throwing error -unsigned long nextionAckTimer = 0; // Timer to track Nextion ack -const unsigned long telnetInputMax = 128; // Size of user input buffer for user telnet session -bool motionEnabled = false; // Motion sensor is enabled -bool mdnsEnabled = true; // mDNS enabled -bool ignoreTouchWhenOff = false; // Ignore touch events when backlight is off and instead send mqtt msg -bool beepEnabled = false; // Keypress beep enabled -unsigned long beepOnTime = 1000; // milliseconds of on-time for beep -unsigned long beepOffTime = 1000; // milliseconds of off-time for beep -unsigned int beepCounter; // Count the number of beeps -uint8_t beepPin = D2; // define beep pin output -uint8_t motionPin = 0; // GPIO input pin for motion sensor if connected and enabled -bool motionActive = false; // Motion is being detected -const unsigned long motionLatchTimeout = 1000; // Latch time for motion sensor -const unsigned long motionBufferTimeout = 100; // Trigger threshold time for motion sensor -unsigned long lcdVersion = 0; // Int to hold current LCD FW version number -unsigned long updateLcdAvailableVersion; // Int to hold the new LCD FW version number -bool lcdVersionQueryFlag = false; // Flag to set if we've queried lcdVersion -const String lcdVersionQuery = "p[0].b[2].val"; // Object ID for lcdVersion in HMI -uint8_t lcdBacklightDim = 0; // Backlight dimmer value -bool lcdBacklightOn = 0; // Backlight on/off -bool lcdBacklightQueryFlag = false; // Flag to set if we've queried lcdBacklightDim -bool startupCompleteFlag = false; // Startup process has completed -const unsigned long statusUpdateInterval = 300000; // Time in msec between publishing MQTT status updates (5 minutes) -unsigned long statusUpdateTimer = 0; // Timer for status update -const unsigned long connectTimeout = 300; // Timeout for WiFi and MQTT connection attempts in seconds -const unsigned long reConnectTimeout = 60; // Timeout for WiFi reconnection attempts in seconds -byte espMac[6]; // Byte array to store our MAC address -bool mqttTlsEnabled = false; // Enable MQTT client TLS connections -bool mqttPingCheck = false; // MQTT broker ping check result -bool mqttPortCheck = false; // MQTT broke port check result -String mqttClientId; // Auto-generated MQTT ClientID -String mqttGetSubtopic; // MQTT subtopic for incoming commands requesting .val -String mqttStateTopic; // MQTT topic for outgoing panel interactions -String mqttStateJSONTopic; // MQTT topic for outgoing panel interactions in JSON format -String mqttCommandTopic; // MQTT topic for incoming panel commands -String mqttGroupCommandTopic; // MQTT topic for incoming group panel commands -String mqttStatusTopic; // MQTT topic for publishing device connectivity state -String mqttSensorTopic; // MQTT topic for publishing device information in JSON format -String mqttLightCommandTopic; // MQTT topic for incoming panel backlight on/off commands -String mqttLightStateTopic; // MQTT topic for outgoing panel backlight on/off state -String mqttLightBrightCommandTopic; // MQTT topic for incoming panel backlight dimmer commands -String mqttLightBrightStateTopic; // MQTT topic for outgoing panel backlight dimmer state -String mqttMotionStateTopic; // MQTT topic for outgoing motion sensor state -String nextionModel; // Record reported model number of LCD panel -const byte nextionSuffix[] = {0xFF, 0xFF, 0xFF}; // Standard suffix for Nextion commands -uint8_t nextionMaxPages = 11; // Maximum number of pages in Nextion project -uint32_t tftFileSize = 0; // Filesize for TFT firmware upload -const uint8_t nextionResetPin = D6; // Pin for Nextion power rail switch (GPIO12/D6) -const unsigned long nextionSpeeds[] = {2400, - 4800, - 9600, - 19200, - 31250, - 38400, - 57600, - 115200, - 230400, - 250000, - 256000, - 512000, - 921600}; // Valid serial speeds for Nextion communication -const uint8_t nextionSpeedsLength = sizeof(nextionSpeeds) / sizeof(nextionSpeeds[0]); // Size of our list of speeds - -WiFiClientSecure mqttClientSecure; // TLS-enabled WiFiClient for MQTT -WiFiClient wifiClient; // Standard WiFiClient -MQTTClient mqttClient(mqttMaxPacketSize); // MQTT client -ESP8266WebServer webServer(80); // Admin web server on port 80 -ESP8266HTTPUpdateServer httpOTAUpdate; // Arduino OTA server -WiFiServer telnetServer(23); // Telnet server (if enabled) -WiFiClient telnetClient; // Telnet client -MDNSResponder::hMDNSService hMDNSService; // mDNS -EspSaveCrash SaveCrash; // Save crash details to flash - -// URL for auto-update check of "version.json" -const char UPDATE_URL[] PROGMEM = "https://raw.githubusercontent.com/HASwitchPlate/HASPone/main/update/version.json"; -// Additional CSS style to match Hass theme -const char HASP_STYLE[] PROGMEM = ""; -// Default link to compiled Arduino firmware image -String espFirmwareUrl = "https://raw.githubusercontent.com/HASwitchPlate/HASPone/main/Arduino_Sketch/HASwitchPlate.ino.d1_mini.bin"; -// Default link to compiled Nextion firmware images -String lcdFirmwareUrl = "https://raw.githubusercontent.com/HASwitchPlate/HASPone/main/Nextion_HMI/HASwitchPlate.tft"; - -//////////////////////////////////////////////////////////////////////////////////////////////////// -void setup() -{ // System setup - debugPrint(String(F("\n\n================================================================================\n"))); - debugPrintln(String(F("SYSTEM: Starting HASPone v")) + String(haspVersion)); - debugPrintln(String(F("SYSTEM: heapFree: ")) + String(ESP.getFreeHeap()) + String(F(" heapMaxFreeBlockSize: ")) + String(ESP.getMaxFreeBlockSize())); - debugPrintln(String(F("SYSTEM: Last reset reason: ")) + String(ESP.getResetInfo())); - if (SaveCrash.count()) - { - debugPrint(String(F("SYSTEM: Crashdump data discovered:"))); - debugPrintCrash(); - } - debugPrint(String(F("================================================================================\n\n"))); - - pinMode(nextionResetPin, OUTPUT); // Take control over the power switch for the LCD - digitalWrite(nextionResetPin, HIGH); // Power on the LCD - configRead(); // Check filesystem for a saved config.json - Serial.begin(atoi(nextionBaud)); // Serial - LCD RX (after swap), debug TX - Serial1.begin(atoi(nextionBaud)); // Serial1 - LCD TX, no RX - Serial.swap(); // Swap to allow hardware UART comms to LCD - - if (!nextionConnect()) - { - if (lcdConnected) - { - debugPrintln(F("HMI: LCD responding but initialization wasn't completed. Continuing program load anyway.")); - } - else - { - debugPrintln(F("HMI: LCD not responding, continuing program load")); - } - } - - espWifiConnect(); // Start up networking - - if ((configPassword[0] != '\0') && (configUser[0] != '\0')) - { // Start the webserver with our assigned password if it's been configured... - httpOTAUpdate.setup(&webServer, "/update", configUser, configPassword); - } - else - { // or without a password if not - httpOTAUpdate.setup(&webServer, "/update"); - } - - webServer.on("/", webHandleRoot); - webServer.on("/saveConfig", webHandleSaveConfig); - webServer.on("/resetConfig", webHandleResetConfig); - webServer.on("/resetBacklight", webHandleResetBacklight); - webServer.on("/firmware", webHandleFirmware); - webServer.on("/espfirmware", webHandleEspFirmware); - webServer.on( - "/lcdupload", HTTP_POST, []() - { webServer.send(200); }, - webHandleLcdUpload); - webServer.on("/tftFileSize", webHandleTftFileSize); - webServer.on("/lcddownload", webHandleLcdDownload); - webServer.on("/lcdOtaSuccess", webHandleLcdUpdateSuccess); - webServer.on("/lcdOtaFailure", webHandleLcdUpdateFailure); - webServer.on("/reboot", webHandleReboot); - webServer.onNotFound(webHandleNotFound); - webServer.begin(); - debugPrintln(String(F("HTTP: Server started @ http://")) + WiFi.localIP().toString()); - - espSetupOta(); // Start OTA firmware update - - motionSetup(); // Setup motion sensor if configured - - mqttConnect(); // Connect to MQTT - - if (mdnsEnabled) - { // Setup mDNS service discovery if enabled - hMDNSService = MDNS.addService(haspNode, "http", "tcp", 80); - if (debugTelnetEnabled) - { - MDNS.addService(haspNode, "telnet", "tcp", 23); - } - MDNS.addServiceTxt(hMDNSService, "app_name", "HASwitchPlate"); - MDNS.addServiceTxt(hMDNSService, "app_version", String(haspVersion).c_str()); - MDNS.update(); - } - - if (beepEnabled) - { // Setup beep/tactile output if configured - pinMode(beepPin, OUTPUT); - } - - if (debugTelnetEnabled) - { // Setup telnet server for remote debug output - telnetServer.setNoDelay(true); - telnetServer.begin(); - debugPrintln(String(F("TELNET: debug server enabled at telnet:")) + WiFi.localIP().toString()); - } - - debugPrintln(F("SYSTEM: System init complete.")); -} - -//////////////////////////////////////////////////////////////////////////////////////////////////// -void loop() -{ // Main execution loop - while ((WiFi.status() != WL_CONNECTED) || (WiFi.localIP().toString() == "0.0.0.0")) - { // Check WiFi is connected and that we have a valid IP, retry until we do. - if (WiFi.status() == WL_CONNECTED) - { // If we're currently connected, disconnect so we can try again - WiFi.disconnect(); - } - espWifiReconnect(); - } - - if (!mqttClient.connected()) - { // Check MQTT connection - debugPrintln(String(F("MQTT: not connected, connecting."))); - mqttConnect(); - } - nextionHandleInput(); // Nextion serial communications loop - mqttClient.loop(); // MQTT client loop - ArduinoOTA.handle(); // Arduino OTA loop - webServer.handleClient(); // webServer loop - telnetHandleClient(); // telnet client loop - motionHandle(); // motion sensor loop - beepHandle(); // beep feedback loop - - if (mdnsEnabled) - { - MDNS.update(); - } - - if ((millis() - statusUpdateTimer) >= statusUpdateInterval) - { // Run periodic status update - statusUpdateTimer = millis(); - mqttStatusUpdate(); - } - - if (((millis() - updateCheckTimer) >= updateCheckInterval) && (millis() > updateCheckFirstRun)) - { // Run periodic update check - updateCheckTimer = millis(); - if (updateCheck()) - { // Publish new status if updateCheck() worked and reset the timer - statusUpdateTimer = millis(); - mqttStatusUpdate(); - } - } -} - -//////////////////////////////////////////////////////////////////////////////////////////////////// -// Functions - -//////////////////////////////////////////////////////////////////////////////////////////////////// -void mqttConnect() -{ // MQTT connection and subscriptions - - static bool mqttFirstConnect = true; // For the first connection, we want to send an OFF/ON state to - // trigger any automations, but skip that if we reconnect while - // still running the sketch - rebootOnp0b1 = true; - static uint8_t mqttReconnectCount = 0; - unsigned long mqttConnectTimer = 0; - const unsigned long mqttConnectTimeout = 5000; - - // Check to see if we have a broker configured and notify the user if not - if (strcmp(mqttServer, "") == 0) - { - nextionSendCmd("page 0"); - nextionSetAttr("p[0].b[1].font", "6"); - nextionSetAttr("p[0].b[1].txt", "\"WiFi Connected!\\r " + String(WiFi.SSID()) + "\\rIP: " + WiFi.localIP().toString() + "\\r\\rConfigure MQTT:\\rhttp://" + WiFi.localIP().toString() + "\""); - while (strcmp(mqttServer, "") == 0) - { // Handle other stuff while we're waiting for MQTT to be configured - yield(); - nextionHandleInput(); // Nextion serial communications loop - ArduinoOTA.handle(); // Arduino OTA loop - webServer.handleClient(); // webServer loop - telnetHandleClient(); // telnet client loop - motionHandle(); // motion sensor loop - beepHandle(); // beep feedback loop - } - } - - if (mqttTlsEnabled) - { // Create MQTT service object with TLS connection - mqttClient.begin(mqttServer, atoi(mqttPort), mqttClientSecure); - if (strcmp(mqttFingerprint, "") == 0) - { - debugPrintln(String(F("MQTT: Configuring MQTT TLS connection without fingerprint validation."))); - mqttClientSecure.setInsecure(); - } - else - { - debugPrintln(String(F("MQTT: Configuring MQTT TLS connection with fingerprint validation."))); - mqttClientSecure.allowSelfSignedCerts(); - mqttClientSecure.setFingerprint(mqttFingerprint); - } - mqttClientSecure.setBufferSizes(512, 512); - } - else - { // Create MQTT service object without TLS connection - debugPrintln(String(F("MQTT: Configuring MQTT connection without TLS."))); - mqttClient.begin(mqttServer, atoi(mqttPort), wifiClient); - } - - mqttClient.onMessage(mqttProcessInput); // Setup MQTT callback function - - // MQTT topic string definitions - mqttStateTopic = "hasp/" + String(haspNode) + "/state"; - mqttStateJSONTopic = "hasp/" + String(haspNode) + "/state/json"; - mqttCommandTopic = "hasp/" + String(haspNode) + "/command"; - mqttGroupCommandTopic = "hasp/" + String(groupName) + "/command"; - mqttStatusTopic = "hasp/" + String(haspNode) + "/status"; - mqttSensorTopic = "hasp/" + String(haspNode) + "/sensor"; - mqttLightCommandTopic = "hasp/" + String(haspNode) + "/light/switch"; - mqttLightStateTopic = "hasp/" + String(haspNode) + "/light/state"; - mqttLightBrightCommandTopic = "hasp/" + String(haspNode) + "/brightness/set"; - mqttLightBrightStateTopic = "hasp/" + String(haspNode) + "/brightness/state"; - mqttMotionStateTopic = "hasp/" + String(haspNode) + "/motion/state"; - - const String mqttCommandSubscription = mqttCommandTopic + "/#"; - const String mqttGroupCommandSubscription = mqttGroupCommandTopic + "/#"; - const String mqttLightSubscription = mqttLightCommandTopic + "/#"; - const String mqttLightBrightSubscription = mqttLightBrightCommandTopic + "/#"; - - // Generate an MQTT client ID as haspNode + our MAC address - mqttClientId = String(haspNode) + "-" + String(espMac[0], HEX) + String(espMac[1], HEX) + String(espMac[2], HEX) + String(espMac[3], HEX) + String(espMac[4], HEX) + String(espMac[5], HEX); - nextionSendCmd("page 0"); - nextionSetAttr("p[0].b[1].font", "6"); - nextionSetAttr("p[0].b[1].txt", "\"WiFi Connected!\\r " + String(WiFi.SSID()) + "\\rIP: " + WiFi.localIP().toString() + "\\r\\rMQTT Connecting:\\r " + String(mqttServer) + "\""); - if (mqttTlsEnabled) - { - debugPrintln(String(F("MQTT: Attempting connection to broker ")) + String(mqttServer) + String(F(" on port ")) + String(mqttPort) + String(F(" with TLS enabled as clientID ")) + mqttClientId); - } - else - { - debugPrintln(String(F("MQTT: Attempting connection to broker ")) + String(mqttServer) + String(F(" on port ")) + String(mqttPort) + String(F(" with TLS disabled as clientID ")) + mqttClientId); - } - - // Set keepAlive, cleanSession, timeout - mqttClient.setOptions(30, true, mqttConnectTimeout); - - // declare LWT - mqttClient.setWill(mqttStatusTopic.c_str(), "OFF", true, 1); - - while (!mqttClient.connected()) - { // Loop until we're connected to MQTT - mqttConnectTimer = millis(); - mqttClient.connect(mqttClientId.c_str(), mqttUser, mqttPassword, false); - - if (mqttClient.connected()) - { // Attempt to connect to broker, setting last will and testament - // Update panel with MQTT status - nextionSetAttr("p[0].b[1].txt", "\"WiFi Connected!\\r " + String(WiFi.SSID()) + "\\rIP: " + WiFi.localIP().toString() + "\\r\\rMQTT Connected:\\r " + String(mqttServer) + "\""); - debugPrintln(F("MQTT: connected")); - - // Reset our diagnostic booleans - mqttPingCheck = true; - mqttPortCheck = true; - - // Subscribe to our incoming topics - if (mqttClient.subscribe(mqttCommandSubscription)) - { - debugPrintln(String(F("MQTT: subscribed to ")) + mqttCommandSubscription); - } - if (mqttClient.subscribe(mqttGroupCommandSubscription)) - { - debugPrintln(String(F("MQTT: subscribed to ")) + mqttGroupCommandSubscription); - } - if (mqttClient.subscribe(mqttLightSubscription)) - { - debugPrintln(String(F("MQTT: subscribed to ")) + mqttLightSubscription); - } - if (mqttClient.subscribe(mqttLightBrightSubscription)) - { - debugPrintln(String(F("MQTT: subscribed to ")) + mqttLightBrightSubscription); - } - - // Publish discovery configuration - mqttDiscovery(); - - // Publish backlight status - if (lcdBacklightOn) - { - debugPrintln(String(F("MQTT OUT: '")) + mqttLightStateTopic + String(F("' : 'ON'"))); - mqttClient.publish(mqttLightStateTopic, "ON", true, 1); - } - else - { - debugPrintln(String(F("MQTT OUT: '")) + mqttLightStateTopic + String(F("' : 'OFF'"))); - mqttClient.publish(mqttLightStateTopic, "OFF", true, 1); - } - debugPrintln(String(F("MQTT OUT: '")) + mqttLightBrightStateTopic + String(F("' : ")) + String(lcdBacklightDim)); - mqttClient.publish(mqttLightBrightStateTopic, String(lcdBacklightDim), true, 1); - - if (mqttFirstConnect) - { // Force any subscribed clients to toggle OFF/ON when we first connect to - // make sure we get a full panel refresh at power on. Sending OFF, - // "ON" will be sent by the mqttStatusTopic subscription action below. - mqttFirstConnect = false; - debugPrintln(String(F("MQTT OUT: '")) + mqttStatusTopic + "' : 'OFF'"); - mqttClient.publish(mqttStatusTopic, "OFF", true, 0); - } - - if (mqttClient.subscribe(mqttStatusTopic)) - { - debugPrintln(String(F("MQTT: subscribed to ")) + mqttStatusTopic); - } - mqttClient.loop(); - } - else - { // Retry until we give up and restart after connectTimeout seconds - mqttReconnectCount++; - if (mqttReconnectCount * mqttConnectTimeout * 6 > (connectTimeout * 1000)) - { - debugPrintln(String(F("MQTT: connection attempt ")) + String(mqttReconnectCount) + String(F(" failed with rc: ")) + String(mqttClient.returnCode()) + String(F(" and error: ")) + String(mqttClient.lastError()) + String(F(". Restarting device."))); - espReset(); - } - yield(); - webServer.handleClient(); - - String mqttCheckResult = "Ping: FAILED"; - String mqttCheckResultNextion = "MQTT Check..."; - - debugPrintln(String(F("MQTT: connection attempt ")) + String(mqttReconnectCount) + String(F(" failed with rc ")) + String(mqttClient.returnCode()) + String(F(" and error: ")) + String(mqttClient.lastError())); - nextionSetAttr("p[0].b[1].txt", String(F("\"WiFi Connected!\\r ")) + String(WiFi.SSID()) + String(F("\\rIP: ")) + WiFi.localIP().toString() + String(F("\\r\\rMQTT Failed:\\r ")) + String(mqttServer) + String(F("\\rRC: ")) + String(mqttClient.returnCode()) + String(F(" Error: ")) + String(mqttClient.lastError()) + String(F("\\r")) + mqttCheckResultNextion + String(F("\""))); - - mqttPingCheck = Ping.ping(mqttServer, 4); - yield(); - webServer.handleClient(); - mqttPortCheck = wifiClient.connect(mqttServer, atoi(mqttPort)); - yield(); - webServer.handleClient(); - - mqttCheckResultNextion = "Ping: "; - if (mqttPingCheck) - { - mqttCheckResult = "Ping: SUCCESS"; - mqttCheckResultNextion = "Ping: "; - } - if (mqttPortCheck) - { - mqttCheckResult += " Port: SUCCESS"; - mqttCheckResultNextion += " Port: "; - } - else - { - mqttCheckResult += " Port: FAILED"; - mqttCheckResultNextion += " Port: "; - } - debugPrintln(String(F("MQTT: connection checks: ")) + mqttCheckResult + String(F(". Trying again in 30 seconds."))); - nextionSetAttr("p[0].b[1].txt", String(F("\"WiFi Connected!\\r ")) + String(WiFi.SSID()) + String(F("\\rIP: ")) + WiFi.localIP().toString() + String(F("\\r\\rMQTT Failed:\\r ")) + String(mqttServer) + String(F("\\rRC: ")) + String(mqttClient.returnCode()) + String(F(" Error: ")) + String(mqttClient.lastError()) + String(F("\\r")) + mqttCheckResultNextion + String(F("\""))); - - while (millis() < (mqttConnectTimer + (mqttConnectTimeout * 6))) - { - yield(); - nextionHandleInput(); // Nextion serial communications loop - ArduinoOTA.handle(); // Arduino OTA loop - webServer.handleClient(); // webServer loop - telnetHandleClient(); // telnet client loop - motionHandle(); // motion sensor loop - beepHandle(); // beep feedback loop - } - } - } - rebootOnp0b1 = false; - if (nextionActivePage < 0) - { // We never picked up a message giving us a page number, so we'll just go to the default page - debugPrintln(String(F("DEBUG: NextionActivePage not received from MQTT, setting to 0"))); - String mqttButtonJSONEvent = String(F("{\"event\":\"page\",\"value\":0}")); - debugPrintln(String(F("MQTT OUT: '")) + mqttStateJSONTopic + String(F("' : '")) + mqttButtonJSONEvent + String(F("'"))); - mqttClient.publish(mqttStateJSONTopic, mqttButtonJSONEvent, false, 0); - String mqttPageTopic = mqttStateTopic + "/page"; - debugPrintln(String(F("MQTT OUT: '")) + mqttPageTopic + String(F("' : '0'"))); - mqttClient.publish(mqttPageTopic, "0", false, 0); - } -} - -//////////////////////////////////////////////////////////////////////////////////////////////////// -void mqttProcessInput(String &strTopic, String &strPayload) -{ // Handle incoming commands from MQTT - - // strTopic: homeassistant/haswitchplate/devicename/command/p[1].b[4].txt - // strPayload: "Lights On" - // subTopic: p[1].b[4].txt - - // Incoming Namespace (replace /device/ with /group/ for group commands) - // '[...]/device/command' -m '' == No command requested, respond with mqttStatusUpdate() - // '[...]/device/command' -m 'dim=50' == nextionSendCmd("dim=50") - // '[...]/device/command/json' -m '["dim=5", "page 1"]' == nextionSendCmd("dim=50"), nextionSendCmd("page 1") - // '[...]/device/command/p[1].b[4].txt' -m '' == nextionGetAttr("p[1].b[4].txt") - // '[...]/device/command/p[1].b[4].txt' -m '"Lights On"' == nextionSetAttr("p[1].b[4].txt", "\"Lights On\"") - // '[...]/device/brightness/set' -m '50' == nextionSendCmd("dims=50") - // '[...]/device/light/switch' -m 'OFF' == nextionSendCmd("dims=0") - // '[...]/device/command/page' -m '1' == nextionSendCmd("page 1") - // '[...]/device/command/statusupdate' -m '' == mqttStatusUpdate() - // '[...]/device/command/discovery' -m '' == call mqttDiscovery() - // '[...]/device/command/lcdupdate' -m 'http://192.168.0.10/local/HASwitchPlate.tft' == nextionOtaStartDownload("http://192.168.0.10/local/HASwitchPlate.tft") - // '[...]/device/command/lcdupdate' -m '' == nextionOtaStartDownload("lcdFirmwareUrl") - // '[...]/device/command/espupdate' -m 'http://192.168.0.10/local/HASwitchPlate.ino.d1_mini.bin' == espStartOta("http://192.168.0.10/local/HASwitchPlate.ino.d1_mini.bin") - // '[...]/device/command/espupdate' -m '' == espStartOta("espFirmwareUrl") - // '[...]/device/command/beep' -m '100,200,3' == beep on for 100msec, off for 200msec, repeat 3 times - // '[...]/device/command/hassdiscovery' -m 'homeassistant' == hassDiscovery = homeassistant - // '[...]/device/command/nextionmaxpages' -m '11' == nextionmaxpages = 11 - // '[...]/device/command/nextionbaud' -m '921600' == nextionBaud = 921600 - // '[...]/device/command/debugserialenabled' -m 'true' == enable serial debug output - // '[...]/device/command/debugtelnetenabled' -m 'true' == enable telnet debug output - // '[...]/device/command/mdnsenabled' -m 'true' == enable mDNS responder - // '[...]/device/command/beepenabled' -m 'true' == enable beep output on keypress - // '[...]/device/command/ignoretouchwhenoff' -m 'true' == disable actions on keypress - - debugPrintln(String(F("MQTT IN: '")) + strTopic + String(F("' : '")) + strPayload + String(F("'"))); - - if (((strTopic == mqttCommandTopic) || (strTopic == mqttGroupCommandTopic)) && (strPayload == "")) - { // '[...]/device/command' -m '' = No command requested, respond with mqttStatusUpdate() - mqttStatusUpdate(); // return status JSON via MQTT - } - else if (strTopic == mqttCommandTopic || strTopic == mqttGroupCommandTopic) - { // '[...]/device/command' -m 'dim=50' == nextionSendCmd("dim=50") - nextionSendCmd(strPayload); - } - else if (strTopic == (mqttCommandTopic + "/page") || strTopic == (mqttGroupCommandTopic + "/page")) - { // '[...]/device/command/page' -m '1' == nextionSendCmd("page 1") - if (strPayload == "") - { - nextionSendCmd("sendme"); - } - else - { - nextionActivePage = strPayload.toInt(); - nextionSendCmd("page " + strPayload); - } - } - else if (strTopic == (mqttCommandTopic + "/json") || strTopic == (mqttGroupCommandTopic + "/json")) - { // '[...]/device/command/json' -m '["dim=5", "page 1"]' = nextionSendCmd("dim=50"), nextionSendCmd("page 1") - if (strPayload != "") - { - nextionParseJson(strPayload); // Send to nextionParseJson() - } - } - else if (strTopic == (mqttCommandTopic + "/statusupdate") || strTopic == (mqttGroupCommandTopic + "/statusupdate")) - { // '[...]/device/command/statusupdate' == mqttStatusUpdate() - mqttStatusUpdate(); // return status JSON via MQTT - } - else if (strTopic == (mqttCommandTopic + "/discovery") || strTopic == (mqttGroupCommandTopic + "/discovery")) - { // '[...]/device/command/discovery' == mqttDiscovery() - mqttDiscovery(); // send Home Assistant discovery message via MQTT - } - else if (strTopic == (mqttCommandTopic + "/hassdiscovery") || strTopic == (mqttGroupCommandTopic + "/hassdiscovery")) - { // '[...]/device/command/hassdiscovery' -m 'homeassistant' == hassDiscovery = homeassistant - strPayload.toCharArray(hassDiscovery, 128); // set hassDiscovery to value provided in payload - configSave(); - mqttDiscovery(); // send Home Assistant discovery message on new discovery topic via MQTT - } - else if ((strTopic == (mqttCommandTopic + "/nextionmaxpages") || strTopic == (mqttGroupCommandTopic + "/nextionmaxpages")) && (strPayload.toInt() < 256) && (strPayload.toInt() > 0)) - { // '[...]/device/command/nextionmaxpages' -m '11' == nextionmaxpages = 11 - nextionMaxPages = strPayload.toInt(); // set nextionMaxPages to value provided in payload - configSave(); - mqttDiscovery(); // send Home Assistant discovery message via MQTT - } - else if ((strTopic == (mqttCommandTopic + "/nextionbaud") || strTopic == (mqttGroupCommandTopic + "/nextionbaud")) && - ((strPayload.toInt() == 2400) || - (strPayload.toInt() == 4800) || - (strPayload.toInt() == 9600) || - (strPayload.toInt() == 19200) || - (strPayload.toInt() == 31250) || - (strPayload.toInt() == 38400) || - (strPayload.toInt() == 57600) || - (strPayload.toInt() == 115200) || - (strPayload.toInt() == 230400) || - (strPayload.toInt() == 250000) || - (strPayload.toInt() == 256000) || - (strPayload.toInt() == 512000) || - (strPayload.toInt() == 921600))) - { // '[...]/device/command/nextionbaud' -m '921600' == nextionBaud = 921600 - strPayload.toCharArray(nextionBaud, 7); // set nextionBaud to value provided in payload - nextionAckEnable = false; - nextionSendCmd("bauds=" + strPayload); // send baud rate to nextion - nextionAckEnable = true; - Serial.flush(); - Serial1.flush(); - Serial.end(); - Serial1.end(); - Serial.begin(atoi(nextionBaud)); // Serial - LCD RX (after swap), debug TX - Serial1.begin(atoi(nextionBaud)); // Serial1 - LCD TX, no RX - Serial.swap(); // Swap to allow hardware UART comms to LCD - configSave(); - } - else if (strTopic == (mqttCommandTopic + "/debugserialenabled") || strTopic == (mqttGroupCommandTopic + "/debugserialenabled")) - { // '[...]/device/command/debugserialenabled' -m 'true' == enable serial debug output - if (strPayload.equalsIgnoreCase("true")) - { - debugSerialEnabled = true; - configSave(); - } - else if(strPayload.equalsIgnoreCase("false")) - { - debugSerialEnabled = false; - configSave(); - } - } - else if (strTopic == (mqttCommandTopic + "/debugtelnetenabled") || strTopic == (mqttGroupCommandTopic + "/debugtelnetenabled")) - { // '[...]/device/command/debugtelnetenabled' -m 'true' == enable telnet debug output - if (strPayload.equalsIgnoreCase("true")) - { - debugTelnetEnabled = true; - configSave(); - } - else if(strPayload.equalsIgnoreCase("false")) - { - debugTelnetEnabled = false; - configSave(); - } - } - else if (strTopic == (mqttCommandTopic + "/mdnsenabled") || strTopic == (mqttGroupCommandTopic + "/mdnsenabled")) - { // '[...]/device/command/mdnsenabled' -m 'true' == enable mDNS responder - if (strPayload.equalsIgnoreCase("true")) - { - mdnsEnabled = true; - configSave(); - } - else if(strPayload.equalsIgnoreCase("false")) - { - mdnsEnabled = false; - configSave(); - } - } - else if (strTopic == (mqttCommandTopic + "/beepenabled") || strTopic == (mqttGroupCommandTopic + "/beepenabled")) - { // '[...]/device/command/beepenabled' -m 'true' == enable beep output on keypress - if (strPayload.equalsIgnoreCase("true")) - { - beepEnabled = true; - configSave(); - } - else if(strPayload.equalsIgnoreCase("false")) - { - beepEnabled = false; - configSave(); - } - } - else if (strTopic == (mqttCommandTopic + "/ignoretouchwhenoff") || strTopic == (mqttGroupCommandTopic + "/ignoretouchwhenoff")) - { // '[...]/device/command/ignoretouchwhenoff' -m 'true' == disable actions on keypress - if (strPayload.equalsIgnoreCase("true")) - { - ignoreTouchWhenOff = true; - configSave(); - } - else if(strPayload.equalsIgnoreCase("false")) - { - ignoreTouchWhenOff = false; - configSave(); - } - } - else if (strTopic == (mqttCommandTopic + "/lcdupdate") || strTopic == (mqttGroupCommandTopic + "/lcdupdate")) - { // '[...]/device/command/lcdupdate' -m 'http://192.168.0.10/local/HASwitchPlate.tft' == nextionOtaStartDownload("http://192.168.0.10/local/HASwitchPlate.tft") - if (strPayload == "") - { - nextionOtaStartDownload(lcdFirmwareUrl); - } - else - { - nextionOtaStartDownload(strPayload); - } - } - else if (strTopic == (mqttCommandTopic + "/espupdate") || strTopic == (mqttGroupCommandTopic + "/espupdate")) - { // '[...]/device/command/espupdate' -m 'http://192.168.0.10/local/HASwitchPlate.ino.d1_mini.bin' == espStartOta("http://192.168.0.10/local/HASwitchPlate.ino.d1_mini.bin") - if (strPayload == "") - { - espStartOta(espFirmwareUrl); - } - else - { - espStartOta(strPayload); - } - } - else if (strTopic == (mqttCommandTopic + "/reboot") || strTopic == (mqttGroupCommandTopic + "/reboot")) - { // '[...]/device/command/reboot' == reboot microcontroller - debugPrintln(F("MQTT: Rebooting device")); - espReset(); - } - else if (strTopic == (mqttCommandTopic + "/lcdreboot") || strTopic == (mqttGroupCommandTopic + "/lcdreboot")) - { // '[...]/device/command/lcdreboot' == reboot LCD panel - debugPrintln(F("MQTT: Rebooting LCD")); - nextionReset(); - } - else if (strTopic == (mqttCommandTopic + "/factoryreset") || strTopic == (mqttGroupCommandTopic + "/factoryreset")) - { // '[...]/device/command/factoryreset' == clear all saved settings - configClearSaved(); - } - else if (strTopic == (mqttCommandTopic + "/beep") || strTopic == (mqttGroupCommandTopic + "/beep")) - { // '[...]/device/command/beep' == activate beep function - String mqttvar1 = getSubtringField(strPayload, ',', 0); - String mqttvar2 = getSubtringField(strPayload, ',', 1); - String mqttvar3 = getSubtringField(strPayload, ',', 2); - - beepOnTime = mqttvar1.toInt(); - beepOffTime = mqttvar2.toInt(); - beepCounter = mqttvar3.toInt(); - } - else if (strTopic == (mqttCommandTopic + "/crashtest")) - { // '[...]/device/command/crashtest' -m 'divzero' == divide by zero - if (strPayload == "divzero") - { - debugPrintln(String(F("DEBUG: attempt to divide by zero"))); - int result, zero; - zero = 0; - result = 1 / zero; - debugPrintln(String(F("DEBUG: div zero result: ")) + String(result)); - } - else if (strPayload == "nullptr") - { // '[...]/device/command/crashtest' -m 'nullptr' == dereference a null pointer - debugPrintln(String(F("DEBUG: attempt to dereference null pointer"))); - int *nullPointer = NULL; - debugPrintln(String(F("DEBUG: dereference null pointer: ")) + String(*nullPointer)); - } - else if (strPayload == "wdt") - { // '[...]/device/command/crashtest' -m 'wdt' == trigger soft WDT - debugPrintln(String(F("DEBUG: enter tight loop and cause WDT"))); - while (true) - { - } - } - } - else if (strTopic.startsWith(mqttCommandTopic) && (strPayload == "")) - { // '[...]/device/command/p[1].b[4].txt' -m '' == nextionGetAttr("p[1].b[4].txt") - String subTopic = strTopic.substring(mqttCommandTopic.length() + 1); - mqttGetSubtopic = "/" + subTopic; - nextionGetAttr(subTopic); - } - else if (strTopic.startsWith(mqttGroupCommandTopic) && (strPayload == "")) - { // '[...]/group/command/p[1].b[4].txt' -m '' == nextionGetAttr("p[1].b[4].txt") - String subTopic = strTopic.substring(mqttGroupCommandTopic.length() + 1); - mqttGetSubtopic = "/" + subTopic; - nextionGetAttr(subTopic); - } - else if (strTopic.startsWith(mqttCommandTopic)) - { // '[...]/device/command/p[1].b[4].txt' -m '"Lights On"' == nextionSetAttr("p[1].b[4].txt", "\"Lights On\"") - String subTopic = strTopic.substring(mqttCommandTopic.length() + 1); - nextionSetAttr(subTopic, strPayload); - } - else if (strTopic.startsWith(mqttGroupCommandTopic)) - { // '[...]/group/command/p[1].b[4].txt' -m '"Lights On"' == nextionSetAttr("p[1].b[4].txt", "\"Lights On\"") - String subTopic = strTopic.substring(mqttGroupCommandTopic.length() + 1); - nextionSetAttr(subTopic, strPayload); - } - else if (strTopic == mqttLightBrightCommandTopic) - { // change the brightness from the light topic - nextionSetAttr("dim", strPayload); - nextionSetAttr("dims", "dim"); - lcdBacklightDim = strPayload.toInt(); - debugPrintln(String(F("MQTT OUT: '")) + mqttLightBrightStateTopic + String(F("' : '")) + strPayload + String(F("'"))); - mqttClient.publish(mqttLightBrightStateTopic, strPayload, true, 0); - } - else if (strTopic == mqttLightCommandTopic && strPayload == "OFF") - { // set the panel dim OFF from the light topic, saving current dim level first - nextionSetAttr("dims", "dim"); - nextionSetAttr("dim", "0"); - lcdBacklightOn = 0; - debugPrintln(String(F("MQTT OUT: '")) + mqttLightStateTopic + String(F("' : 'OFF'"))); - mqttClient.publish(mqttLightStateTopic, "OFF", true, 0); - } - else if (strTopic == mqttLightCommandTopic && strPayload == "ON") - { // set the panel dim ON from the light topic, restoring saved dim level - nextionSetAttr("dim", "dims"); - nextionSetAttr("sleep", "0"); - lcdBacklightOn = 1; - debugPrintln(String(F("MQTT OUT: '")) + mqttLightStateTopic + String(F("' : 'ON'"))); - mqttClient.publish(mqttLightStateTopic, "ON", true, 0); - } - else if (strTopic == mqttStatusTopic && strPayload == "OFF") - { // catch a dangling LWT from a previous connection if it appears - debugPrintln(String(F("MQTT OUT: '")) + mqttStatusTopic + String(F("' : 'ON'"))); - mqttClient.publish(mqttStatusTopic, "ON", true, 0); - mqttClient.publish(mqttStateJSONTopic, String(F("{\"event_type\":\"hasp_device\",\"event\":\"online\"}"))); - debugPrintln(String(F("MQTT OUT: '")) + mqttStateJSONTopic + String(F(" : {\"event_type\":\"hasp_device\",\"event\":\"online\"}"))); - } -} - -//////////////////////////////////////////////////////////////////////////////////////////////////// -void mqttStatusUpdate() -{ // Periodically publish system status - String mqttSensorPayload = "{"; - mqttSensorPayload += String(F("\"espVersion\":")) + String(haspVersion) + String(F(",")); - if (updateEspAvailable) - { - mqttSensorPayload += String(F("\"updateEspAvailable\":true,")); - } - else - { - mqttSensorPayload += String(F("\"updateEspAvailable\":false,")); - } - if (lcdConnected) - { - mqttSensorPayload += String(F("\"lcdConnected\":true,")); - } - else - { - mqttSensorPayload += String(F("\"lcdConnected\":false,")); - } - mqttSensorPayload += String(F("\"lcdVersion\":\"")) + String(lcdVersion) + String(F("\",")); - if (updateLcdAvailable) - { - mqttSensorPayload += String(F("\"updateLcdAvailable\":true,")); - } - else - { - mqttSensorPayload += String(F("\"updateLcdAvailable\":false,")); - } - mqttSensorPayload += String(F("\"espUptime\":")) + String(long(millis() / 1000)) + String(F(",")); - mqttSensorPayload += String(F("\"signalStrength\":")) + String(WiFi.RSSI()) + String(F(",")); - mqttSensorPayload += String(F("\"haspName\":\"")) + String(haspNode) + String(F("\",")); - mqttSensorPayload += String(F("\"haspIP\":\"")) + WiFi.localIP().toString() + String(F("\",")); - mqttSensorPayload += String(F("\"haspClientID\":\"")) + mqttClientId + String(F("\",")); - mqttSensorPayload += String(F("\"haspMac\":\"")) + String(espMac[0], HEX) + String(F(":")) + String(espMac[1], HEX) + String(F(":")) + String(espMac[2], HEX) + String(F(":")) + String(espMac[3], HEX) + String(F(":")) + String(espMac[4], HEX) + String(F(":")) + String(espMac[5], HEX) + String(F("\",")); - mqttSensorPayload += String(F("\"haspManufacturer\":\"HASwitchPlate\",\"haspModel\":\"HASPone v1.0.0\",")); - mqttSensorPayload += String(F("\"heapFree\":")) + String(ESP.getFreeHeap()) + String(F(",")); - mqttSensorPayload += String(F("\"heapFragmentation\":")) + String(ESP.getHeapFragmentation()) + String(F(",")); - mqttSensorPayload += String(F("\"heapMaxFreeBlockSize\":")) + String(ESP.getMaxFreeBlockSize()) + String(F(",")); - mqttSensorPayload += String(F("\"espCore\":\"")) + String(ESP.getCoreVersion()) + String(F("\"")); - mqttSensorPayload += "}"; - - // Publish sensor JSON - mqttClient.publish(mqttSensorTopic, mqttSensorPayload, true, 1); - debugPrintln(String(F("MQTT OUT: '")) + mqttSensorTopic + String(F("' : '")) + mqttSensorPayload + String(F("'"))); -} - -//////////////////////////////////////////////////////////////////////////////////////////////////// -void mqttDiscovery() -{ // Publish Home Assistant discovery messages - - String macAddress = String(espMac[0], HEX) + String(F(":")) + String(espMac[1], HEX) + String(F(":")) + String(espMac[2], HEX) + String(F(":")) + String(espMac[3], HEX) + String(F(":")) + String(espMac[4], HEX) + String(F(":")) + String(espMac[5], HEX); - - // light discovery for backlight - String mqttDiscoveryTopic = String(hassDiscovery) + String(F("/light/")) + String(haspNode) + String(F("/config")); - String mqttDiscoveryPayload = String(F("{\"name\":\"")) + String(haspNode) + String(F(" backlight\",\"command_topic\":\"")) + mqttLightCommandTopic + String(F("\",\"state_topic\":\"")) + mqttLightStateTopic + String(F("\",\"brightness_state_topic\":\"")) + mqttLightBrightStateTopic + String(F("\",\"brightness_command_topic\":\"")) + mqttLightBrightCommandTopic + String(F("\",\"availability_topic\":\"")) + mqttStatusTopic + String(F("\",\"brightness_scale\":100,\"unique_id\":\"")) + mqttClientId + String(F("-backlight\",\"payload_on\":\"ON\",\"payload_off\":\"OFF\",\"payload_available\":\"ON\",\"payload_not_available\":\"OFF\",\"device\":{\"identifiers\":[\"")) + mqttClientId + String(F("\"],\"name\":\"")) + String(haspNode) + String(F("\",\"manufacturer\":\"HASwitchPlate\",\"model\":\"HASPone v1.0.0\",\"sw_version\":")) + String(haspVersion) + String(F("}}")); - mqttClient.publish(mqttDiscoveryTopic, mqttDiscoveryPayload, true, 1); - debugPrintln(String(F("MQTT OUT: '")) + mqttDiscoveryTopic + String(F("' : '")) + String(mqttDiscoveryPayload) + String(F("'"))); - - // sensor discovery for device telemetry - mqttDiscoveryTopic = String(hassDiscovery) + String(F("/sensor/")) + String(haspNode) + String(F("/config")); - mqttDiscoveryPayload = String(F("{\"name\":\"")) + String(haspNode) + String(F(" sensor\",\"json_attributes_topic\":\"")) + mqttSensorTopic + String(F("\",\"state_topic\":\"")) + mqttStatusTopic + String(F("\",\"unique_id\":\"")) + mqttClientId + String(F("-sensor\",\"icon\":\"mdi:cellphone-text\",\"device\":{\"identifiers\":[\"")) + mqttClientId + String(F("\"],\"name\":\"")) + String(haspNode) + String(F("\",\"manufacturer\":\"HASwitchPlate\",\"model\":\"HASPone v1.0.0\",\"sw_version\":")) + String(haspVersion) + String(F("}}")); - mqttClient.publish(mqttDiscoveryTopic, mqttDiscoveryPayload, true, 1); - debugPrintln(String(F("MQTT OUT: '")) + mqttDiscoveryTopic + String(F("' : '")) + String(mqttDiscoveryPayload) + String(F("'"))); - - // number discovery for active page - mqttDiscoveryTopic = String(hassDiscovery) + String(F("/number/")) + String(haspNode) + String(F("/config")); - mqttDiscoveryPayload = String(F("{\"name\":\"")) + String(haspNode) + String(F(" active page\",\"command_topic\":\"")) + mqttCommandTopic + String(F("/page\",\"state_topic\":\"")) + mqttStateTopic + String(F("/page\",\"step\":1,\"min\":0,\"max\":")) + String(nextionMaxPages) + String(F(",\"retain\":true,\"optimistic\":true,\"icon\":\"mdi:page-next-outline\",\"unique_id\":\"")) + mqttClientId + String(F("-page\",\"device\":{\"identifiers\":[\"")) + mqttClientId + String(F("\"],\"name\":\"")) + String(haspNode) + String(F("\",\"manufacturer\":\"HASwitchPlate\",\"model\":\"HASPone v1.0.0\",\"sw_version\":")) + String(haspVersion) + String(F("}}")); - mqttClient.publish(mqttDiscoveryTopic, mqttDiscoveryPayload, true, 1); - debugPrintln(String(F("MQTT OUT: '")) + mqttDiscoveryTopic + String(F("' : '")) + String(mqttDiscoveryPayload) + String(F("'"))); - - // AlwaysOn topic for RGB lights - mqttClient.publish((String(F("hasp/")) + String(haspNode) + String(F("/alwayson"))), "ON", true, 1); - debugPrintln(String(F("MQTT OUT: 'hasp/")) + String(haspNode) + String(F("/alwayson' : 'ON'"))); - - // rgb light discovery for selectedforegroundcolor - mqttDiscoveryTopic = String(hassDiscovery) + String(F("/light/")) + String(haspNode) + String(F("/selectedforegroundcolor/config")); - mqttDiscoveryPayload = String(F("{\"name\":\"")) + String(haspNode) + String(F(" selected foreground color\",\"command_topic\":\"hasp/")) + String(haspNode) + String(F("/light/selectedforegroundcolor/switch\",\"state_topic\":\"hasp/")) + String(haspNode) + String(F("/alwayson\",\"rgb_command_topic\":\"hasp/")) + String(haspNode) + String(F("/light/selectedforegroundcolor/rgb\",\"rgb_command_template\":\"{{(red|bitwise_and(248)*256)+(green|bitwise_and(252)*8)+(blue|bitwise_and(248)/8)|int }}\",\"retain\":true,\"unique_id\":\"")) + mqttClientId + String(F("-selectedforegroundcolor\",\"device\":{\"identifiers\":[\"")) + mqttClientId + String(F("\"],\"name\":\"")) + String(haspNode) + String(F("\",\"manufacturer\":\"HASwitchPlate\",\"model\":\"HASPone v1.0.0\",\"sw_version\":")) + String(haspVersion) + String(F("}}")); - mqttClient.publish(mqttDiscoveryTopic, mqttDiscoveryPayload, true, 1); - debugPrintln(String(F("MQTT OUT: '")) + mqttDiscoveryTopic + String(F("' : '")) + String(mqttDiscoveryPayload) + String(F("'"))); - - // rgb light discovery for selectedbackgroundcolor - mqttDiscoveryTopic = String(hassDiscovery) + String(F("/light/")) + String(haspNode) + String(F("/selectedbackgroundcolor/config")); - mqttDiscoveryPayload = String(F("{\"name\":\"")) + String(haspNode) + String(F(" selected background color\",\"command_topic\":\"hasp/")) + String(haspNode) + String(F("/light/selectedbackgroundcolor/switch\",\"state_topic\":\"hasp/")) + String(haspNode) + String(F("/alwayson\",\"rgb_command_topic\":\"hasp/")) + String(haspNode) + String(F("/light/selectedbackgroundcolor/rgb\",\"rgb_command_template\":\"{{(red|bitwise_and(248)*256)+(green|bitwise_and(252)*8)+(blue|bitwise_and(248)/8)|int }}\",\"retain\":true,\"unique_id\":\"")) + mqttClientId + String(F("-selectedbackgroundcolor\",\"device\":{\"identifiers\":[\"")) + mqttClientId + String(F("\"],\"name\":\"")) + String(haspNode) + String(F("\",\"manufacturer\":\"HASwitchPlate\",\"model\":\"HASPone v1.0.0\",\"sw_version\":")) + String(haspVersion) + String(F("}}")); - mqttClient.publish(mqttDiscoveryTopic, mqttDiscoveryPayload, true, 1); - debugPrintln(String(F("MQTT OUT: '")) + mqttDiscoveryTopic + String(F("' : '")) + String(mqttDiscoveryPayload) + String(F("'"))); - - // rgb light discovery for unselectedforegroundcolor - mqttDiscoveryTopic = String(hassDiscovery) + String(F("/light/")) + String(haspNode) + String(F("/unselectedforegroundcolor/config")); - mqttDiscoveryPayload = String(F("{\"name\":\"")) + String(haspNode) + String(F(" unselected foreground color\",\"command_topic\":\"hasp/")) + String(haspNode) + String(F("/light/unselectedforegroundcolor/switch\",\"state_topic\":\"hasp/")) + String(haspNode) + String(F("/alwayson\",\"rgb_command_topic\":\"hasp/")) + String(haspNode) + String(F("/light/unselectedforegroundcolor/rgb\",\"rgb_command_template\":\"{{(red|bitwise_and(248)*256)+(green|bitwise_and(252)*8)+(blue|bitwise_and(248)/8)|int }}\",\"retain\":true,\"unique_id\":\"")) + mqttClientId + String(F("-unselectedforegroundcolor\",\"device\":{\"identifiers\":[\"")) + mqttClientId + String(F("\"],\"name\":\"")) + String(haspNode) + String(F("\",\"manufacturer\":\"HASwitchPlate\",\"model\":\"HASPone v1.0.0\",\"sw_version\":")) + String(haspVersion) + String(F("}}")); - mqttClient.publish(mqttDiscoveryTopic, mqttDiscoveryPayload, true, 1); - debugPrintln(String(F("MQTT OUT: '")) + mqttDiscoveryTopic + String(F("' : '")) + String(mqttDiscoveryPayload) + String(F("'"))); - - // rgb light discovery for unselectedbackgroundcolor - mqttDiscoveryTopic = String(hassDiscovery) + String(F("/light/")) + String(haspNode) + String(F("/unselectedbackgroundcolor/config")); - mqttDiscoveryPayload = String(F("{\"name\":\"")) + String(haspNode) + String(F(" unselected background color\",\"command_topic\":\"hasp/")) + String(haspNode) + String(F("/light/unselectedbackgroundcolor/switch\",\"state_topic\":\"hasp/")) + String(haspNode) + String(F("/alwayson\",\"rgb_command_topic\":\"hasp/")) + String(haspNode) + String(F("/light/unselectedbackgroundcolor/rgb\",\"rgb_command_template\":\"{{(red|bitwise_and(248)*256)+(green|bitwise_and(252)*8)+(blue|bitwise_and(248)/8)|int }}\",\"retain\":true,\"unique_id\":\"")) + mqttClientId + String(F("-unselectedbackgroundcolor\",\"device\":{\"identifiers\":[\"")) + mqttClientId + String(F("\"],\"name\":\"")) + String(haspNode) + String(F("\",\"manufacturer\":\"HASwitchPlate\",\"model\":\"HASPone v1.0.0\",\"sw_version\":")) + String(haspVersion) + String(F("}}")); - mqttClient.publish(mqttDiscoveryTopic, mqttDiscoveryPayload, true, 1); - debugPrintln(String(F("MQTT OUT: '")) + mqttDiscoveryTopic + String(F("' : '")) + String(mqttDiscoveryPayload) + String(F("'"))); - - if (motionEnabled) - { // binary_sensor for motion - String macAddress = String(espMac[0], HEX) + String(F(":")) + String(espMac[1], HEX) + String(F(":")) + String(espMac[2], HEX) + String(F(":")) + String(espMac[3], HEX) + String(F(":")) + String(espMac[4], HEX) + String(F(":")) + String(espMac[5], HEX); - String mqttDiscoveryTopic = String(hassDiscovery) + String(F("/binary_sensor/")) + String(haspNode) + String(F("-motion/config")); - String mqttDiscoveryPayload = String(F("{\"device_class\":\"motion\",\"name\":\"")) + String(haspNode) + String(F(" motion\",\"state_topic\":\"")) + mqttMotionStateTopic + String(F("\",\"unique_id\":\"")) + mqttClientId + String(F("-motion\",\"payload_on\":\"ON\",\"payload_off\":\"OFF\",\"device\":{\"identifiers\":[\"")) + mqttClientId + String(F("\"],\"name\":\"")) + String(haspNode) + String(F("\",\"manufacturer\":\"HASwitchPlate\",\"model\":\"HASPone v1.0.0\",\"sw_version\":")) + String(haspVersion) + String(F("}}")); - mqttClient.publish(mqttDiscoveryTopic, mqttDiscoveryPayload, true, 1); - debugPrintln(String(F("MQTT OUT: '")) + mqttDiscoveryTopic + String(F("' : '")) + String(mqttDiscoveryPayload) + String(F("'"))); - } -} - -//////////////////////////////////////////////////////////////////////////////////////////////////// -void nextionHandleInput() -{ // Handle incoming serial data from the Nextion panel - // This will collect serial data from the panel and place it into the global buffer - // nextionReturnBuffer[nextionReturnIndex] - unsigned long handlerTimeout = millis() + 100; - bool nextionCommandComplete = false; - static uint8_t nextionTermByteCnt = 0; // counter for our 3 consecutive 0xFFs - - while (Serial.available() && !nextionCommandComplete && (millis() < handlerTimeout)) - { - byte nextionCommandByte = Serial.read(); - if (nextionCommandByte == 0xFF) - { // check to see if we have one of 3 consecutive 0xFF which indicates the end of a command - nextionTermByteCnt++; - if (nextionTermByteCnt >= 3) - { // We have received a complete command - lcdConnected = true; - nextionCommandComplete = true; - nextionTermByteCnt = 0; // reset counter - } - } - else - { - nextionTermByteCnt = 0; // reset counter if a non-term byte was encountered - } - nextionReturnBuffer[nextionReturnIndex] = nextionCommandByte; - nextionReturnIndex++; - if (nextionCommandComplete) - { - nextionAckReceived = true; - nextionProcessInput(); - } - yield(); - } - if (millis() > handlerTimeout) - { - debugPrintln(String(F("HMI ERROR: nextionHandleInput timeout"))); - } -} - -//////////////////////////////////////////////////////////////////////////////////////////////////// -void nextionProcessInput() -{ // Process complete incoming serial command from the Nextion panel - // Command reference: https://www.itead.cc/wiki/Nextion_Instruction_Set#Format_of_Device_Return_Data - // tl;dr: command byte, command data, 0xFF 0xFF 0xFF - - if (nextionReturnBuffer[0] == 0x01) - { // Instruction Successful - quietly ignore this as it will be returned after every command issued, - // and processing it + spitting out serial output is a huge drag on performance if serial debug is enabled. - - // debugPrintln(String(F("HMI IN: [Instruction Successful] 0x")) + String(nextionReturnBuffer[0], HEX)); - // if (mqttClient.connected()) - // { - // String mqttButtonJSONEvent = String(F("{\"event\":\"nextion_return_data\",\"return_code\":\"0x")) + String(nextionReturnBuffer[0], HEX) + String(F("\",\"return_code_description\":\"Instruction Successful\"}")); - // mqttClient.publish(mqttStateJSONTopic, mqttButtonJSONEvent); - // debugPrintln(String(F("MQTT OUT: '")) + mqttStateJSONTopic + String(F("' : '")) + mqttButtonJSONEvent + String(F("'"))); - // } - nextionReturnIndex = 0; // Done handling the buffer, reset index back to 0 - return; // skip the rest of the tests below and return immediately - } - - debugPrintln(String(F("HMI IN: [")) + String(nextionReturnIndex) + String(F(" bytes]: ")) + printHex8(nextionReturnBuffer, nextionReturnIndex)); - - if (nextionReturnBuffer[0] == 0x00 && nextionReturnBuffer[1] == 0x00 && nextionReturnBuffer[2] == 0x00) - { // Nextion Startup - debugPrintln(String(F("HMI IN: [Nextion Startup] 0x00 0x00 0x00"))); - if (mqttClient.connected()) - { - String mqttButtonJSONEvent = String(F("{\"event\":\"nextion_return_data\",\"return_code\":\"0x00 0x00 0x00\",\"return_code_description\":\"Nextion Startup\"}")); - mqttClient.publish(mqttStateJSONTopic, mqttButtonJSONEvent); - debugPrintln(String(F("MQTT OUT: '")) + mqttStateJSONTopic + String(F("' : '")) + mqttButtonJSONEvent + String(F("'"))); - } - } - else if (nextionReturnBuffer[0] == 0x00) - { // Invalid Instruction - debugPrintln(String(F("HMI IN: [Invalid Instruction] 0x")) + String(nextionReturnBuffer[0], HEX)); - if (mqttClient.connected()) - { - String mqttButtonJSONEvent = String(F("{\"event\":\"nextion_return_data\",\"return_code\":\"0x")) + String(nextionReturnBuffer[0], HEX) + String(F("\",\"return_code_description\":\"Invalid Instruction\"}")); - mqttClient.publish(mqttStateJSONTopic, mqttButtonJSONEvent); - debugPrintln(String(F("MQTT OUT: '")) + mqttStateJSONTopic + String(F("' : '")) + mqttButtonJSONEvent + String(F("'"))); - } - } - else if (nextionReturnBuffer[0] == 0x02) - { // Invalid Component ID - debugPrintln(String(F("HMI IN: [Invalid Component ID] 0x")) + String(nextionReturnBuffer[0], HEX)); - if (mqttClient.connected()) - { - String mqttButtonJSONEvent = String(F("{\"event\":\"nextion_return_data\",\"return_code\":\"0x")) + String(nextionReturnBuffer[0], HEX) + String(F("\",\"return_code_description\":\"Invalid Component ID\"}")); - mqttClient.publish(mqttStateJSONTopic, mqttButtonJSONEvent); - debugPrintln(String(F("MQTT OUT: '")) + mqttStateJSONTopic + String(F("' : '")) + mqttButtonJSONEvent + String(F("'"))); - } - } - else if (nextionReturnBuffer[0] == 0x03) - { // Invalid Page ID - debugPrintln(String(F("HMI IN: [Invalid Page ID] 0x")) + String(nextionReturnBuffer[0], HEX)); - if (mqttClient.connected()) - { - String mqttButtonJSONEvent = String(F("{\"event\":\"nextion_return_data\",\"return_code\":\"0x")) + String(nextionReturnBuffer[0], HEX) + String(F("\",\"return_code_description\":\"Invalid Page ID\"}")); - mqttClient.publish(mqttStateJSONTopic, mqttButtonJSONEvent); - debugPrintln(String(F("MQTT OUT: '")) + mqttStateJSONTopic + String(F("' : '")) + mqttButtonJSONEvent + String(F("'"))); - } - } - else if (nextionReturnBuffer[0] == 0x04) - { // Invalid Picture ID - debugPrintln(String(F("HMI IN: [Invalid Picture ID] 0x")) + String(nextionReturnBuffer[0], HEX)); - if (mqttClient.connected()) - { - String mqttButtonJSONEvent = String(F("{\"event\":\"nextion_return_data\",\"return_code\":\"0x")) + String(nextionReturnBuffer[0], HEX) + String(F("\",\"return_code_description\":\"Invalid Picture ID\"}")); - mqttClient.publish(mqttStateJSONTopic, mqttButtonJSONEvent); - debugPrintln(String(F("MQTT OUT: '")) + mqttStateJSONTopic + String(F("' : '")) + mqttButtonJSONEvent + String(F("'"))); - } - } - else if (nextionReturnBuffer[0] == 0x05) - { // Invalid Font ID - debugPrintln(String(F("HMI IN: [Invalid Font ID ] 0x")) + String(nextionReturnBuffer[0], HEX)); - if (mqttClient.connected()) - { - String mqttButtonJSONEvent = String(F("{\"event\":\"nextion_return_data\",\"return_code\":\"0x")) + String(nextionReturnBuffer[0], HEX) + String(F("\",\"return_code_description\":\"Invalid Font ID \"}")); - mqttClient.publish(mqttStateJSONTopic, mqttButtonJSONEvent); - debugPrintln(String(F("MQTT OUT: '")) + mqttStateJSONTopic + String(F("' : '")) + mqttButtonJSONEvent + String(F("'"))); - } - } - else if (nextionReturnBuffer[0] == 0x06) - { // Invalid File Operation - debugPrintln(String(F("HMI IN: [Invalid File Operation] 0x")) + String(nextionReturnBuffer[0], HEX)); - if (mqttClient.connected()) - { - String mqttButtonJSONEvent = String(F("{\"event\":\"nextion_return_data\",\"return_code\":\"0x")) + String(nextionReturnBuffer[0], HEX) + String(F("\",\"return_code_description\":\"Invalid File Operation\"}")); - mqttClient.publish(mqttStateJSONTopic, mqttButtonJSONEvent); - debugPrintln(String(F("MQTT OUT: '")) + mqttStateJSONTopic + String(F("' : '")) + mqttButtonJSONEvent + String(F("'"))); - } - } - else if (nextionReturnBuffer[0] == 0x09) - { // Invalid CRC - debugPrintln(String(F("HMI IN: [Invalid CRC] 0x")) + String(nextionReturnBuffer[0], HEX)); - if (mqttClient.connected()) - { - String mqttButtonJSONEvent = String(F("{\"event\":\"nextion_return_data\",\"return_code\":\"0x")) + String(nextionReturnBuffer[0], HEX) + String(F("\",\"return_code_description\":\"Invalid CRC\"}")); - mqttClient.publish(mqttStateJSONTopic, mqttButtonJSONEvent); - debugPrintln(String(F("MQTT OUT: '")) + mqttStateJSONTopic + String(F("' : '")) + mqttButtonJSONEvent + String(F("'"))); - } - } - else if (nextionReturnBuffer[0] == 0x11) - { // Invalid Baud rate Setting - debugPrintln(String(F("HMI IN: [Invalid Baud rate Setting] 0x")) + String(nextionReturnBuffer[0], HEX)); - if (mqttClient.connected()) - { - String mqttButtonJSONEvent = String(F("{\"event\":\"nextion_return_data\",\"return_code\":\"0x")) + String(nextionReturnBuffer[0], HEX) + String(F("\",\"return_code_description\":\"Invalid Baud rate Setting\"}")); - mqttClient.publish(mqttStateJSONTopic, mqttButtonJSONEvent); - debugPrintln(String(F("MQTT OUT: '")) + mqttStateJSONTopic + String(F("' : '")) + mqttButtonJSONEvent + String(F("'"))); - } - } - else if (nextionReturnBuffer[0] == 0x12) - { // Invalid Waveform ID or Channel # - debugPrintln(String(F("HMI IN: [Invalid Waveform ID or Channel #] 0x")) + String(nextionReturnBuffer[0], HEX)); - if (mqttClient.connected()) - { - String mqttButtonJSONEvent = String(F("{\"event\":\"nextion_return_data\",\"return_code\":\"0x")) + String(nextionReturnBuffer[0], HEX) + String(F("\",\"return_code_description\":\"Invalid Waveform ID or Channel #\"}")); - mqttClient.publish(mqttStateJSONTopic, mqttButtonJSONEvent); - debugPrintln(String(F("MQTT OUT: '")) + mqttStateJSONTopic + String(F("' : '")) + mqttButtonJSONEvent + String(F("'"))); - } - } - else if (nextionReturnBuffer[0] == 0x1A) - { // Invalid Variable name or attribute - debugPrintln(String(F("HMI IN: [Invalid Variable name or attribute] 0x")) + String(nextionReturnBuffer[0], HEX)); - if (mqttClient.connected()) - { - String mqttButtonJSONEvent = String(F("{\"event\":\"nextion_return_data\",\"return_code\":\"0x")) + String(nextionReturnBuffer[0], HEX) + String(F("\",\"return_code_description\":\"Invalid Variable name or attribute\"}")); - mqttClient.publish(mqttStateJSONTopic, mqttButtonJSONEvent); - debugPrintln(String(F("MQTT OUT: '")) + mqttStateJSONTopic + String(F("' : '")) + mqttButtonJSONEvent + String(F("'"))); - } - } - else if (nextionReturnBuffer[0] == 0x1B) - { // Invalid Variable Operation - debugPrintln(String(F("HMI IN: [Invalid Variable Operation] 0x")) + String(nextionReturnBuffer[0], HEX)); - if (mqttClient.connected()) - { - String mqttButtonJSONEvent = String(F("{\"event\":\"nextion_return_data\",\"return_code\":\"0x")) + String(nextionReturnBuffer[0], HEX) + String(F("\",\"return_code_description\":\"Invalid Variable Operation\"}")); - mqttClient.publish(mqttStateJSONTopic, mqttButtonJSONEvent); - debugPrintln(String(F("MQTT OUT: '")) + mqttStateJSONTopic + String(F("' : '")) + mqttButtonJSONEvent + String(F("'"))); - } - } - else if (nextionReturnBuffer[0] == 0x1C) - { // Assignment failed to assign - debugPrintln(String(F("HMI IN: [Assignment failed to assign] 0x")) + String(nextionReturnBuffer[0], HEX)); - if (mqttClient.connected()) - { - String mqttButtonJSONEvent = String(F("{\"event\":\"nextion_return_data\",\"return_code\":\"0x")) + String(nextionReturnBuffer[0], HEX) + String(F("\",\"return_code_description\":\"Assignment failed to assign\"}")); - mqttClient.publish(mqttStateJSONTopic, mqttButtonJSONEvent); - debugPrintln(String(F("MQTT OUT: '")) + mqttStateJSONTopic + String(F("' : '")) + mqttButtonJSONEvent + String(F("'"))); - } - } - else if (nextionReturnBuffer[0] == 0x1D) - { // EEPROM Operation failed - debugPrintln(String(F("HMI IN: [EEPROM Operation failed] 0x")) + String(nextionReturnBuffer[0], HEX)); - if (mqttClient.connected()) - { - String mqttButtonJSONEvent = String(F("{\"event\":\"nextion_return_data\",\"return_code\":\"0x")) + String(nextionReturnBuffer[0], HEX) + String(F("\",\"return_code_description\":\"EEPROM Operation failed\"}")); - mqttClient.publish(mqttStateJSONTopic, mqttButtonJSONEvent); - debugPrintln(String(F("MQTT OUT: '")) + mqttStateJSONTopic + String(F("' : '")) + mqttButtonJSONEvent + String(F("'"))); - } - } - else if (nextionReturnBuffer[0] == 0x1E) - { // Invalid Quantity of Parameters - debugPrintln(String(F("HMI IN: [Invalid Quantity of Parameters] 0x")) + String(nextionReturnBuffer[0], HEX)); - if (mqttClient.connected()) - { - String mqttButtonJSONEvent = String(F("{\"event\":\"nextion_return_data\",\"return_code\":\"0x")) + String(nextionReturnBuffer[0], HEX) + String(F("\",\"return_code_description\":\"Invalid Quantity of Parameters\"}")); - mqttClient.publish(mqttStateJSONTopic, mqttButtonJSONEvent); - debugPrintln(String(F("MQTT OUT: '")) + mqttStateJSONTopic + String(F("' : '")) + mqttButtonJSONEvent + String(F("'"))); - } - } - else if (nextionReturnBuffer[0] == 0x1F) - { // IO Operation failed - debugPrintln(String(F("HMI IN: [IO Operation failed] 0x")) + String(nextionReturnBuffer[0], HEX)); - if (mqttClient.connected()) - { - String mqttButtonJSONEvent = String(F("{\"event\":\"nextion_return_data\",\"return_code\":\"0x")) + String(nextionReturnBuffer[0], HEX) + String(F("\",\"return_code_description\":\"IO Operation failed\"}")); - mqttClient.publish(mqttStateJSONTopic, mqttButtonJSONEvent); - debugPrintln(String(F("MQTT OUT: '")) + mqttStateJSONTopic + String(F("' : '")) + mqttButtonJSONEvent + String(F("'"))); - } - } - else if (nextionReturnBuffer[0] == 0x20) - { // Escape Character Invalid - debugPrintln(String(F("HMI IN: [Escape Character Invalid] 0x")) + String(nextionReturnBuffer[0], HEX)); - if (mqttClient.connected()) - { - String mqttButtonJSONEvent = String(F("{\"event\":\"nextion_return_data\",\"return_code\":\"0x")) + String(nextionReturnBuffer[0], HEX) + String(F("\",\"return_code_description\":\"Escape Character Invalid\"}")); - mqttClient.publish(mqttStateJSONTopic, mqttButtonJSONEvent); - debugPrintln(String(F("MQTT OUT: '")) + mqttStateJSONTopic + String(F("' : '")) + mqttButtonJSONEvent + String(F("'"))); - } - } - else if (nextionReturnBuffer[0] == 0x23) - { // Variable name too long - debugPrintln(String(F("HMI IN: [Variable name too long] 0x")) + String(nextionReturnBuffer[0], HEX)); - if (mqttClient.connected()) - { - String mqttButtonJSONEvent = String(F("{\"event\":\"nextion_return_data\",\"return_code\":\"0x")) + String(nextionReturnBuffer[0], HEX) + String(F("\",\"return_code_description\":\"Variable name too long\"}")); - mqttClient.publish(mqttStateJSONTopic, mqttButtonJSONEvent); - debugPrintln(String(F("MQTT OUT: '")) + mqttStateJSONTopic + String(F("' : '")) + mqttButtonJSONEvent + String(F("'"))); - } - } - else if (nextionReturnBuffer[0] == 0x24) - { // Serial Buffer Overflow - debugPrintln(String(F("HMI IN: [Serial Buffer Overflow] 0x")) + String(nextionReturnBuffer[0], HEX)); - if (mqttClient.connected()) - { - String mqttButtonJSONEvent = String(F("{\"event\":\"nextion_return_data\",\"return_code\":\"0x")) + String(nextionReturnBuffer[0], HEX) + String(F("\",\"return_code_description\":\"Serial Buffer Overflow\"}")); - mqttClient.publish(mqttStateJSONTopic, mqttButtonJSONEvent); - debugPrintln(String(F("MQTT OUT: '")) + mqttStateJSONTopic + String(F("' : '")) + mqttButtonJSONEvent + String(F("'"))); - } - } - - else if (nextionReturnBuffer[0] == 0x65) - { // Handle incoming touch command - // 0x65+Page ID+Component ID+TouchEvent+End - // Return this data when the touch event created by the user is pressed. - // Definition of TouchEvent: Press Event 0x01, Release Event 0X00 - // Example: 0x65 0x00 0x02 0x01 0xFF 0xFF 0xFF - // Meaning: Touch Event, Page 0, Object 2, Press - String nextionPage = String(nextionReturnBuffer[1]); - String nextionButtonID = String(nextionReturnBuffer[2]); - byte nextionButtonAction = nextionReturnBuffer[3]; - - if (nextionButtonAction == 0x01) - { - debugPrintln(String(F("HMI IN: [Button ON] 'p[")) + nextionPage + "].b[" + nextionButtonID + "]'"); - if (mqttClient.connected()) - { - // Only process touch events if screen backlight is on and configured to do so. - if (ignoreTouchWhenOff && !lcdBacklightOn) - { - String mqttButtonJSONEvent = String(F("{\"event_type\":\"button_press_disabled\",\"event\":\"p[")) + nextionPage + String(F("].b[")) + nextionButtonID + String(F("]\",\"value\":\"ON\"}")); - mqttClient.publish(mqttStateJSONTopic, mqttButtonJSONEvent); - debugPrintln(String(F("MQTT OUT: '")) + mqttStateJSONTopic + String(F("' : '")) + mqttButtonJSONEvent + String(F("'"))); - } - else - { - String mqttButtonTopic = mqttStateTopic + "/p[" + nextionPage + "].b[" + nextionButtonID + "]"; - mqttClient.publish(mqttButtonTopic, "ON"); - debugPrintln(String(F("MQTT OUT: '")) + mqttButtonTopic + "' : 'ON'"); - String mqttButtonJSONEvent = String(F("{\"event_type\":\"button_short_press\",\"event\":\"p[")) + nextionPage + String(F("].b[")) + nextionButtonID + String(F("]\",\"value\":\"ON\"}")); - mqttClient.publish(mqttStateJSONTopic, mqttButtonJSONEvent); - debugPrintln(String(F("MQTT OUT: '")) + mqttStateJSONTopic + String(F("' : '")) + mqttButtonJSONEvent + String(F("'"))); - } - } - if (beepEnabled) - { - beepOnTime = 500; - beepOffTime = 100; - beepCounter = 1; - } - if (rebootOnp0b1 && (nextionPage == "0") && (nextionButtonID == "1")) - { - debugPrintln(String(F("HMI IN: p[0].b[1] pressed during HASPone configuration, rebooting."))); - espReset(); - } - } - else if (nextionButtonAction == 0x00) - { - debugPrintln(String(F("HMI IN: [Button OFF] 'p[")) + nextionPage + "].b[" + nextionButtonID + "]'"); - if (mqttClient.connected()) - { - // Only process touch events if screen backlight is on and configured to do so. - if (ignoreTouchWhenOff && !lcdBacklightOn) - { - String mqttButtonJSONEvent = String(F("{\"event_type\":\"button_release_disabled\",\"event\":\"p[")) + nextionPage + String(F("].b[")) + nextionButtonID + String(F("]\",\"value\":\"ON\"}")); - mqttClient.publish(mqttStateJSONTopic, mqttButtonJSONEvent); - debugPrintln(String(F("MQTT OUT: '")) + mqttStateJSONTopic + String(F("' : '")) + mqttButtonJSONEvent + String(F("'"))); - } - else - { - String mqttButtonTopic = mqttStateTopic + "/p[" + nextionPage + "].b[" + nextionButtonID + "]"; - mqttClient.publish(mqttButtonTopic, "OFF"); - debugPrintln(String(F("MQTT OUT: '")) + mqttButtonTopic + "' : 'OFF'"); - String mqttButtonJSONEvent = String(F("{\"event_type\":\"button_short_release\",\"event\":\"p[")) + nextionPage + String(F("].b[")) + nextionButtonID + String(F("]\",\"value\":\"OFF\"}")); - mqttClient.publish(mqttStateJSONTopic, mqttButtonJSONEvent); - debugPrintln(String(F("MQTT OUT: '")) + mqttStateJSONTopic + String(F("' : '")) + mqttButtonJSONEvent + String(F("'"))); - // Now see if this object has a .val that might have been updated. Works for sliders, - // two-state buttons, etc, returns 0 for normal buttons - mqttGetSubtopic = "/p[" + nextionPage + "].b[" + nextionButtonID + "].val"; - // This right here is dicey. We're done w/ this command so reset the index allowing this to be kinda-reentrant - // because the call to nextionGetAttr is going to call us back. - nextionReturnIndex = 0; - nextionGetAttr("p[" + nextionPage + "].b[" + nextionButtonID + "].val"); - } - } - } - } - else if (nextionReturnBuffer[0] == 0x66) - { // Handle incoming "sendme" page number - // 0x66+PageNum+End - // Example: 0x66 0x02 0xFF 0xFF 0xFF - // Meaning: page 2 - String nextionPage = String(nextionReturnBuffer[1]); - debugPrintln(String(F("HMI IN: [sendme Page] '")) + nextionPage + String(F("'"))); - if ((nextionPage != "0") || nextionReportPage0) - { // If we have a new page AND ( (it's not "0") OR (we've set the flag to report 0 anyway) ) - - if (mqttClient.connected()) - { - String mqttButtonJSONEvent = String(F("{\"event\":\"page\",\"value\":")) + nextionPage + String(F("}")); - debugPrintln(String(F("MQTT OUT: '")) + mqttStateJSONTopic + String(F("' : '")) + mqttButtonJSONEvent + String(F("'"))); - mqttClient.publish(mqttStateJSONTopic, mqttButtonJSONEvent); - String mqttPageTopic = mqttStateTopic + "/page"; - debugPrintln(String(F("MQTT OUT: '")) + mqttPageTopic + String(F("' : '")) + nextionPage + String(F("'"))); - mqttClient.publish(mqttPageTopic, nextionPage, false, 0); - } - } - } - else if (nextionReturnBuffer[0] == 0x67 || nextionReturnBuffer[0] == 0x68) - { // Handle touch coordinate data - // 0X67+Coordinate X High+Coordinate X Low+Coordinate Y High+Coordinate Y Low+TouchEvent+End - // Example: 0X67 0X00 0X7A 0X00 0X1E 0X01 0XFF 0XFF 0XFF - // Meaning: Coordinate (122,30), Touch Event: Press - // issue Nextion command "sendxy=1" to enable this output - // 0x68 is the same, but returned when the screen touch has awakened the screen from sleep - uint16_t xCoord = nextionReturnBuffer[1]; - xCoord = xCoord * 256 + nextionReturnBuffer[2]; - uint16_t yCoord = nextionReturnBuffer[3]; - yCoord = yCoord * 256 + nextionReturnBuffer[4]; - String xyCoord = String(xCoord) + String(',') + String(yCoord); - byte nextionTouchAction = nextionReturnBuffer[5]; - if (nextionTouchAction == 0x01) - { - debugPrintln(String(F("HMI IN: [Touch ON] '")) + xyCoord + String(F("'"))); - if (mqttClient.connected()) - { - String mqttTouchTopic = mqttStateTopic + "/touchOn"; - mqttClient.publish(mqttTouchTopic, xyCoord); - debugPrintln(String(F("MQTT OUT: '")) + mqttTouchTopic + String(F("' : '")) + xyCoord + String(F("'"))); - String mqttButtonJSONEvent = String(F("{\"event_type\":\"button_short_press\",\"event\":\"touchxy\",\"touch_event\":\"ON\",\"touchx\":\"")) + String(xCoord) + String(F("\",\"touchy\":\"")) + String(yCoord) + String(F("\",\"screen_state\":\"")); - if (nextionReturnBuffer[0] == 0x67) - { - mqttButtonJSONEvent += "awake\"}"; - } - else - { - mqttButtonJSONEvent += "asleep\"}"; - } - mqttClient.publish(mqttStateJSONTopic, mqttButtonJSONEvent); - debugPrintln(String(F("MQTT OUT: '")) + mqttStateJSONTopic + String(F("' : '")) + mqttButtonJSONEvent + String(F("'"))); - } - } - else if (nextionTouchAction == 0x00) - { - debugPrintln(String(F("HMI IN: [Touch OFF] '")) + xyCoord + String(F("'"))); - if (mqttClient.connected()) - { - String mqttTouchTopic = mqttStateTopic + "/touchOff"; - mqttClient.publish(mqttTouchTopic, xyCoord); - debugPrintln(String(F("MQTT OUT: '")) + mqttTouchTopic + String(F("' : '")) + xyCoord + String(F("'"))); - String mqttButtonJSONEvent = String(F("{\"event_type\":\"button_short_press\",\"event\":\"touchxy\",\"touch_event\":\"OFF\",\"touchx\":\"")) + String(xCoord) + String(F("\",\"touchy\":\"")) + String(yCoord) + String(F("\",\"screen_state\":\"")); - if (nextionReturnBuffer[0] == 0x67) - { - mqttButtonJSONEvent += "awake\"}"; - } - else - { - mqttButtonJSONEvent += "asleep\"}"; - } - mqttClient.publish(mqttStateJSONTopic, mqttButtonJSONEvent); - debugPrintln(String(F("MQTT OUT: '")) + mqttStateJSONTopic + String(F("' : '")) + mqttButtonJSONEvent + String(F("'"))); - } - } - } - else if (nextionReturnBuffer[0] == 0x70) - { // Handle get string return - // 0x70+ASCII string+End - // Example: 0x70 0x41 0x42 0x43 0x44 0x31 0x32 0x33 0x34 0xFF 0xFF 0xFF - // Meaning: String data, ABCD1234 - String getString; - for (int i = 1; i < nextionReturnIndex - 3; i++) - { // convert the payload into a string - getString += (char)nextionReturnBuffer[i]; - } - debugPrintln(String(F("HMI IN: [String Return] '")) + getString + String(F("'"))); - if (mqttClient.connected()) - { - if (mqttGetSubtopic == "") - { // If there's no outstanding request for a value, publish to mqttStateTopic - mqttClient.publish(mqttStateTopic, getString); - debugPrintln(String(F("MQTT OUT: '")) + mqttStateTopic + String(F("' : '")) + getString + String(F("'"))); - } - else - { // Otherwise, publish the to saved mqttGetSubtopic and then reset mqttGetSubtopic - String mqttReturnTopic = mqttStateTopic + mqttGetSubtopic; - mqttClient.publish(mqttReturnTopic, getString); - debugPrintln(String(F("MQTT OUT: '")) + mqttReturnTopic + String(F("' : '")) + getString + String(F("'"))); - String mqttButtonJSONEvent = String(F("{\"event\":\"")) + mqttGetSubtopic.substring(1) + String(F("\",\"value\":\"")) + getString + String(F("\"}")); - mqttClient.publish(mqttStateJSONTopic, mqttButtonJSONEvent); - debugPrintln(String(F("MQTT OUT: '")) + mqttStateJSONTopic + String(F("' : '")) + mqttButtonJSONEvent + String(F("'"))); - mqttGetSubtopic = ""; - } - } - } - else if (nextionReturnBuffer[0] == 0x71) - { // Handle get int return - // 0x71+byte1+byte2+byte3+byte4+End (4 byte little endian) - // Example: 0x71 0x7B 0x00 0x00 0x00 0xFF 0xFF 0xFF - // Meaning: Integer data, 123 - long getInt = nextionReturnBuffer[4]; - getInt = getInt * 256 + nextionReturnBuffer[3]; - getInt = getInt * 256 + nextionReturnBuffer[2]; - getInt = getInt * 256 + nextionReturnBuffer[1]; - String getString = String(getInt); - debugPrintln(String(F("HMI IN: [Int Return] '")) + getString + String(F("'"))); - - if (lcdVersionQueryFlag) - { - lcdVersion = getInt; - lcdVersionQueryFlag = false; - debugPrintln(String(F("HMI IN: lcdVersion '")) + String(lcdVersion) + String(F("'"))); - } - else if (lcdBacklightQueryFlag) - { - lcdBacklightDim = getInt; - lcdBacklightQueryFlag = false; - if (lcdBacklightDim > 0) - { - lcdBacklightOn = 1; - } - else - { - lcdBacklightOn = 0; - } - debugPrintln(String(F("HMI IN: lcdBacklightDim '")) + String(lcdBacklightDim) + String(F("'"))); - } - else if (mqttGetSubtopic == "") - { - if (mqttClient.connected()) - { - mqttClient.publish(mqttStateTopic, getString); - debugPrintln(String(F("MQTT OUT: '")) + mqttStateTopic + String(F("' : '")) + getString + String(F("'"))); - } - } - // Otherwise, publish the to saved mqttGetSubtopic and then reset mqttGetSubtopic - else - { - if (mqttClient.connected()) - { - String mqttReturnTopic = mqttStateTopic + mqttGetSubtopic; - mqttClient.publish(mqttReturnTopic, getString); - debugPrintln(String(F("MQTT OUT: '")) + mqttReturnTopic + String(F("' : '")) + getString + String(F("'"))); - String mqttButtonJSONEvent = String(F("{\"event\":\"")) + mqttGetSubtopic.substring(1) + String(F("\",\"value\":")) + getString + String(F("}")); - mqttClient.publish(mqttStateJSONTopic, mqttButtonJSONEvent); - debugPrintln(String(F("MQTT OUT: '")) + mqttStateJSONTopic + String(F("' : '")) + mqttButtonJSONEvent + String(F("'"))); - } - mqttGetSubtopic = ""; - } - } - else if (nextionReturnBuffer[0] == 0x63 && nextionReturnBuffer[1] == 0x6f && nextionReturnBuffer[2] == 0x6d && nextionReturnBuffer[3] == 0x6f && nextionReturnBuffer[4] == 0x6b) - { // Catch 'comok' response to 'connect' command: https://www.itead.cc/blog/nextion-hmi-upload-protocol - String comokField; - uint8_t comokFieldCount = 0; - byte comokFieldSeperator = 0x2c; // "," - - for (uint8_t i = 0; i <= nextionReturnIndex; i++) - { // cycle through each byte looking for our field seperator - if (nextionReturnBuffer[i] == comokFieldSeperator) - { // Found the end of a field, so do something with it. Maybe. - if (comokFieldCount == 2) - { - nextionModel = comokField; - debugPrintln(String(F("HMI IN: nextionModel: ")) + nextionModel); - } - comokFieldCount++; - comokField = ""; - } - else - { - comokField += String(char(nextionReturnBuffer[i])); - } - } - } - else if (nextionReturnBuffer[0] == 0x86) - { // Returned when Nextion enters sleep automatically. Using sleep=1 will not return an 0x86 - // 0x86+End - if (mqttClient.connected()) - { - lcdBacklightOn = 0; - mqttClient.publish(mqttLightStateTopic, "OFF", true, 1); - debugPrintln(String(F("MQTT OUT: '")) + mqttLightStateTopic + String(F("' : 'OFF'"))); - String mqttButtonJSONEvent = String(F("{\"event\":\"sleep\",\"value\":\"ON\"}")); - mqttClient.publish(mqttStateJSONTopic, mqttButtonJSONEvent); - debugPrintln(String(F("MQTT OUT: '")) + mqttStateJSONTopic + String(F("' : '")) + mqttButtonJSONEvent + String(F("'"))); - } - } - else if (nextionReturnBuffer[0] == 0x87) - { // Returned when Nextion leaves sleep automatically. Using sleep=0 will not return an 0x87 - // 0x87+End - if (mqttClient.connected()) - { - lcdBacklightOn = 1; - mqttClient.publish(mqttLightStateTopic, "ON", true, 1); - debugPrintln(String(F("MQTT OUT: '")) + mqttLightStateTopic + String(F("' : 'ON'"))); - mqttClient.publish(mqttLightBrightStateTopic, String(lcdBacklightDim), true, 1); - debugPrintln(String(F("MQTT OUT: '")) + mqttLightBrightStateTopic + String(F("' : ")) + String(lcdBacklightDim)); - String mqttButtonJSONEvent = String(F("{\"event\":\"sleep\",\"value\":\"OFF\"}")); - mqttClient.publish(mqttStateJSONTopic, mqttButtonJSONEvent); - debugPrintln(String(F("MQTT OUT: '")) + mqttStateJSONTopic + String(F("' : '")) + mqttButtonJSONEvent + String(F("'"))); - } - } - else if (nextionReturnBuffer[0] == 0x88) - { // Returned when Nextion powers on - // 0x88+End - debugPrintln(F("HMI: Nextion panel connected.")); - } - nextionReturnIndex = 0; // Done handling the buffer, reset index back to 0 -} - -//////////////////////////////////////////////////////////////////////////////////////////////////// -void nextionSendCmd(const String &nextionCmd) -{ // Send a raw command to the Nextion panel - Serial1.print(nextionCmd); - Serial1.write(nextionSuffix, sizeof(nextionSuffix)); - Serial1.flush(); - debugPrintln(String(F("HMI OUT: ")) + nextionCmd); - - if (nextionAckEnable) - { - nextionAckReceived = false; - nextionAckTimer = millis(); - - while ((!nextionAckReceived) && (millis() - nextionAckTimer < nextionAckTimeout)) - { - nextionHandleInput(); - } - if (!nextionAckReceived) - { - debugPrintln(String(F("HMI ERROR: Nextion Ack timeout"))); - String mqttButtonJSONEvent = String(F("{\"event\":\"nextionError\",\"value\":\"Nextion Ack timeout\"}")); - mqttClient.publish(mqttStateJSONTopic, mqttButtonJSONEvent); - debugPrintln(String(F("MQTT OUT: '")) + mqttStateJSONTopic + String(F("' : '")) + mqttButtonJSONEvent + String(F("'"))); - } - } - else - { - nextionHandleInput(); - } -} - -//////////////////////////////////////////////////////////////////////////////////////////////////// -void nextionSetAttr(const String &hmiAttribute, const String &hmiValue) -{ // Set the value of a Nextion component attribute - Serial1.print(hmiAttribute); - Serial1.print("="); - Serial1.print(hmiValue); - Serial1.write(nextionSuffix, sizeof(nextionSuffix)); - Serial1.flush(); - debugPrintln(String(F("HMI OUT: '")) + hmiAttribute + "=" + hmiValue + String(F("'"))); - if (nextionAckEnable) - { - nextionAckReceived = false; - nextionAckTimer = millis(); - - while ((!nextionAckReceived) || (millis() - nextionAckTimer > nextionAckTimeout)) - { - nextionHandleInput(); - } - if (!nextionAckReceived) - { - debugPrintln(String(F("HMI ERROR: Nextion Ack timeout"))); - String mqttButtonJSONEvent = String(F("{\"event\":\"nextionError\",\"value\":\"Nextion Ack timeout\"}")); - mqttClient.publish(mqttStateJSONTopic, mqttButtonJSONEvent); - debugPrintln(String(F("MQTT OUT: '")) + mqttStateJSONTopic + String(F("' : '")) + mqttButtonJSONEvent + String(F("'"))); - } - } - else - { - nextionHandleInput(); - } -} - -//////////////////////////////////////////////////////////////////////////////////////////////////// -void nextionGetAttr(const String &hmiAttribute) -{ // Get the value of a Nextion component attribute - // This will only send the command to the panel requesting the attribute, the actual - // return of that value will be handled by nextionProcessInput and placed into mqttGetSubtopic - Serial1.print("get "); - Serial1.print(hmiAttribute); - Serial1.write(nextionSuffix, sizeof(nextionSuffix)); - Serial1.flush(); - debugPrintln(String(F("HMI OUT: 'get ")) + hmiAttribute + String(F("'"))); - if (nextionAckEnable) - { - nextionAckReceived = false; - nextionAckTimer = millis(); - - while ((!nextionAckReceived) || (millis() - nextionAckTimer > nextionAckTimeout)) - { - nextionHandleInput(); - } - if (!nextionAckReceived) - { - debugPrintln(String(F("HMI ERROR: Nextion Ack timeout"))); - String mqttButtonJSONEvent = String(F("{\"event\":\"nextionError\",\"value\":\"Nextion Ack timeout\"}")); - mqttClient.publish(mqttStateJSONTopic, mqttButtonJSONEvent); - debugPrintln(String(F("MQTT OUT: '")) + mqttStateJSONTopic + String(F("' : '")) + mqttButtonJSONEvent + String(F("'"))); - } - } - else - { - nextionHandleInput(); - } -} - -//////////////////////////////////////////////////////////////////////////////////////////////////// -void nextionParseJson(const String &strPayload) -{ // Parse an incoming JSON array into individual Nextion commands - DynamicJsonDocument nextionCommands(mqttMaxPacketSize + 1024); - DeserializationError jsonError = deserializeJson(nextionCommands, strPayload); - - if (jsonError) - { // Couldn't parse incoming JSON command - String jsonErrorDescription = String(F("Failed to parse incoming JSON command with error:")) + String(jsonError.c_str()) + String(F(" memoryUsage: ")) + String(nextionCommands.memoryUsage()) + String(F(" capacity: ")) + String(nextionCommands.capacity()); - debugPrintln(String(F("MQTT: [ERROR] ")) + jsonErrorDescription); - mqttClient.publish(mqttStateJSONTopic, String(F("{\"event\":\"jsonError\",\"event_source\":\"nextionParseJson()\",\"event_description\":\"")) + jsonErrorDescription + String(F("\"}"))); - } - else - { - for (uint8_t i = 0; i < nextionCommands.size(); i++) - { - nextionSendCmd(nextionCommands[i]); - } - } -} - -//////////////////////////////////////////////////////////////////////////////////////////////////// -void nextionOtaStartDownload(const String &lcdOtaUrl) -{ // Upload firmware to the Nextion LCD via HTTP download - // based in large part on code posted by indev2 here: - // http://support.iteadstudio.com/support/discussions/topics/11000007686/page/2 - - uint32_t lcdOtaFileSize = 0; - String lcdOtaNextionCmd; - uint32_t lcdOtaChunkCounter = 0; - uint16_t lcdOtaPartNum = 0; - uint32_t lcdOtaTransferred = 0; - uint8_t lcdOtaPercentComplete = 0; - const uint32_t lcdOtaTimeout = 30000; // timeout for receiving new data in milliseconds - static uint32_t lcdOtaTimer = 0; // timer for lcdOtaTimeout - - HTTPClient lcdOtaHttp; - WiFiClientSecure lcdOtaWifiSecure; - WiFiClient lcdOtaWifi; - if (lcdOtaUrl.startsWith(F("https"))) - { - debugPrintln("LCDOTA: Attempting firmware update from HTTPS host: " + lcdOtaUrl); - - lcdOtaHttp.begin(lcdOtaWifiSecure, lcdOtaUrl); - lcdOtaWifiSecure.setInsecure(); - lcdOtaWifiSecure.setBufferSizes(512, 512); - } - else - { - debugPrintln("LCDOTA: Attempting firmware update from HTTP host: " + lcdOtaUrl); - lcdOtaHttp.begin(lcdOtaWifi, lcdOtaUrl); - } - - int lcdOtaHttpReturn = lcdOtaHttp.GET(); - if (lcdOtaHttpReturn > 0) - { // HTTP header has been sent and Server response header has been handled - debugPrintln(String(F("LCDOTA: HTTP GET return code:")) + String(lcdOtaHttpReturn)); - if (lcdOtaHttpReturn == HTTP_CODE_OK) - { // file found at server - int32_t lcdOtaRemaining = lcdOtaHttp.getSize(); // get length of document (is -1 when Server sends no Content-Length header) - lcdOtaFileSize = lcdOtaRemaining; - static uint16_t lcdOtaParts = (lcdOtaRemaining / 4096) + 1; - static const uint16_t lcdOtaBufferSize = 1024; // upload data buffer before sending to UART - static uint8_t lcdOtaBuffer[lcdOtaBufferSize] = {}; - - debugPrintln(String(F("LCDOTA: File found at Server. Size ")) + String(lcdOtaRemaining) + String(F(" bytes in ")) + String(lcdOtaParts) + String(F(" 4k chunks."))); - - WiFiUDP::stopAll(); // Keep mDNS responder and MQTT traffic from breaking things - if (mqttClient.connected()) - { - debugPrintln(F("LCDOTA: LCD firmware upload starting, closing MQTT connection.")); - mqttClient.publish(mqttStatusTopic, "OFF", true, 0); - debugPrintln(String(F("MQTT OUT: '")) + mqttStatusTopic + String(F("' : 'OFF'"))); - mqttClient.disconnect(); - } - - WiFiClient *stream = lcdOtaHttp.getStreamPtr(); // get tcp stream - Serial1.write(nextionSuffix, sizeof(nextionSuffix)); // Send empty command - Serial1.flush(); - nextionHandleInput(); - String lcdOtaNextionCmd = "whmi-wri " + String(lcdOtaFileSize) + "," + String(nextionBaud) + ",0"; - debugPrintln(String(F("LCDOTA: Sending LCD upload command: ")) + lcdOtaNextionCmd); - Serial1.print(lcdOtaNextionCmd); - Serial1.write(nextionSuffix, sizeof(nextionSuffix)); - Serial1.flush(); - - if (nextionOtaResponse()) - { - debugPrintln(F("LCDOTA: LCD upload command accepted.")); - } - else - { - debugPrintln(F("LCDOTA: LCD upload command FAILED. Restarting device.")); - espReset(); - } - debugPrintln(F("LCDOTA: Starting update")); - lcdOtaTimer = millis(); - while (lcdOtaHttp.connected() && (lcdOtaRemaining > 0 || lcdOtaRemaining == -1)) - { // Write incoming data to panel as it arrives - uint16_t lcdOtaHttpSize = stream->available(); // get available data size - - if (lcdOtaHttpSize) - { - uint16_t lcdOtaChunkSize = 0; - if ((lcdOtaHttpSize <= lcdOtaBufferSize) && (lcdOtaHttpSize <= (4096 - lcdOtaChunkCounter))) - { - lcdOtaChunkSize = lcdOtaHttpSize; - } - else if ((lcdOtaBufferSize <= lcdOtaHttpSize) && (lcdOtaBufferSize <= (4096 - lcdOtaChunkCounter))) - { - lcdOtaChunkSize = lcdOtaBufferSize; - } - else - { - lcdOtaChunkSize = 4096 - lcdOtaChunkCounter; - } - stream->readBytes(lcdOtaBuffer, lcdOtaChunkSize); - Serial1.flush(); // make sure any previous writes the UART have completed - Serial1.write(lcdOtaBuffer, lcdOtaChunkSize); // now send buffer to the UART - lcdOtaChunkCounter += lcdOtaChunkSize; - if (lcdOtaChunkCounter >= 4096) - { - Serial1.flush(); - lcdOtaPartNum++; - lcdOtaTransferred += lcdOtaChunkCounter; - lcdOtaPercentComplete = (lcdOtaTransferred * 100) / lcdOtaFileSize; - lcdOtaChunkCounter = 0; - if (nextionOtaResponse()) - { // We've completed a chunk - debugPrintln(String(F("LCDOTA: Part ")) + String(lcdOtaPartNum) + String(F(" OK, ")) + String(lcdOtaPercentComplete) + String(F("% complete"))); - lcdOtaTimer = millis(); - } - else - { - debugPrintln(String(F("LCDOTA: Part ")) + String(lcdOtaPartNum) + String(F(" FAILED, ")) + String(lcdOtaPercentComplete) + String(F("% complete"))); - debugPrintln(F("LCDOTA: failure")); - delay(2000); // extra delay while the LCD does its thing - espReset(); - } - } - else - { - delay(20); - } - if (lcdOtaRemaining > 0) - { - lcdOtaRemaining -= lcdOtaChunkSize; - } - } - delay(10); - if ((lcdOtaTimer > 0) && ((millis() - lcdOtaTimer) > lcdOtaTimeout)) - { // Our timer expired so reset - debugPrintln(F("LCDOTA: ERROR: LCD upload timeout. Restarting.")); - espReset(); - } - } - lcdOtaPartNum++; - lcdOtaTransferred += lcdOtaChunkCounter; - if ((lcdOtaTransferred == lcdOtaFileSize) && nextionOtaResponse()) - { - debugPrintln(String(F("LCDOTA: Success, wrote ")) + String(lcdOtaTransferred) + String(F(" of ")) + String(tftFileSize) + String(F(" bytes."))); - uint32_t lcdOtaDelay = millis(); - debugPrintln(F("LCDOTA: Waiting 5 seconds to allow LCD to apply updates we've sent.")); - while ((millis() - lcdOtaDelay) < 5000) - { // extra 5sec delay while the LCD handles any local firmware updates from new versions of code sent to it - webServer.handleClient(); - yield(); - } - espReset(); - } - else - { - debugPrintln(String(F("LCDOTA: Failure, lcdOtaTransferred: ")) + String(lcdOtaTransferred) + String(F(" lcdOtaFileSize: ")) + String(lcdOtaFileSize)); - espReset(); - } - } - } - else - { - debugPrintln(String(F("LCDOTA: HTTP GET failed, error code ")) + lcdOtaHttp.errorToString(lcdOtaHttpReturn)); - espReset(); - } - lcdOtaHttp.end(); -} - -//////////////////////////////////////////////////////////////////////////////////////////////////// -bool nextionOtaResponse() -{ // Monitor the serial port for a 0x05 response within our timeout - unsigned long nextionCommandTimeout = 2000; // timeout for receiving termination string in milliseconds - unsigned long nextionCommandTimer = millis(); // record current time for our timeout - bool otaSuccessVal = false; - while ((millis() - nextionCommandTimer) < nextionCommandTimeout) - { - if (Serial.available()) - { - byte inByte = Serial.read(); - if (inByte == 0x5) - { - otaSuccessVal = true; - break; - } - } - else - { - delay(1); - } - } - return otaSuccessVal; -} - -//////////////////////////////////////////////////////////////////////////////////////////////////// -bool nextionConnect() -{ - const unsigned long nextionCheckTimeout = 2000; // Max time in msec for nextion connection check - unsigned long nextionCheckTimer = millis(); // Timer for nextion connection checks - - Serial1.write(nextionSuffix, sizeof(nextionSuffix)); - - if (!lcdConnected) - { // Check for some traffic from our LCD - debugPrintln(F("HMI: Waiting for LCD connection")); - while (((millis() - nextionCheckTimer) <= nextionCheckTimeout) && !lcdConnected) - { - nextionHandleInput(); - } - } - if (!lcdConnected) - { // No response from the display using the configured speed, so scan all possible speeds - nextionSetSpeed(); - - nextionCheckTimer = millis(); // Reset our timer - debugPrintln(F("HMI: Waiting again for LCD connection")); - while (((millis() - nextionCheckTimer) <= nextionCheckTimeout) && !lcdConnected) - { - Serial1.write(nextionSuffix, sizeof(nextionSuffix)); - nextionHandleInput(); - } - if (!lcdConnected) - { - debugPrintln(F("HMI: LCD connection timed out")); - return false; - } - } - - // Query backlight status. This should always succeed under simulation or non-HASPone HMI - lcdBacklightQueryFlag = true; - debugPrintln(F("HMI: Querying LCD backlight status")); - Serial1.write(nextionSuffix, sizeof(nextionSuffix)); - nextionSendCmd("get dim"); - while (((millis() - nextionCheckTimer) <= nextionCheckTimeout) && lcdBacklightQueryFlag) - { - nextionHandleInput(); - } - if (lcdBacklightQueryFlag) - { // Our flag is still set, meaning we never got a response. - debugPrintln(F("HMI: LCD backlight query timed out")); - lcdBacklightQueryFlag = false; - return false; - } - - // We are now communicating with the panel successfully. Enable ACK checking for all future commands. - nextionAckEnable = true; - nextionSendCmd("bkcmd=3"); - - // This check depends on the HMI having been designed with a version number in the object - // defined in lcdVersionQuery. It's OK if this fails, it just means the HMI project is - // not utilizing the version capability that the HASPone project makes use of. - lcdVersionQueryFlag = true; - debugPrintln(F("HMI: Querying LCD firmware version number")); - nextionSendCmd("get " + lcdVersionQuery); - while (((millis() - nextionCheckTimer) <= nextionCheckTimeout) && lcdVersionQueryFlag) - { - nextionHandleInput(); - } - if (lcdVersionQueryFlag) - { // Our flag is still set, meaning we never got a response. This should only happen if - // there's a problem. Non-HASPone projects should pass this check with lcdVersion = 0 - debugPrintln(F("HMI: LCD version query timed out")); - lcdVersionQueryFlag = false; - return false; - } - - if (nextionModel.length() == 0) - { // Check for LCD model via `connect`. The Nextion simulator does not support this command, - // so if we're running under that environment this process should timeout. - debugPrintln(F("HMI: Querying LCD model information")); - nextionSendCmd("connect"); - while (((millis() - nextionCheckTimer) <= nextionCheckTimeout) && (nextionModel.length() == 0)) - { - nextionHandleInput(); - } - } - return true; -} - -//////////////////////////////////////////////////////////////////////////////////////////////////// -void nextionSetSpeed() -{ - debugPrintln(String(F("HMI: No Nextion response, attempting to set serial speed to ")) + String(nextionBaud)); - for (unsigned int nextionSpeedsIndex = 0; nextionSpeedsIndex < nextionSpeedsLength; nextionSpeedsIndex++) - { - debugPrintln(String(F("HMI: Sending bauds=")) + String(nextionBaud) + " @" + String(nextionSpeeds[nextionSpeedsIndex]) + " baud"); - Serial1.flush(); - Serial1.begin(nextionSpeeds[nextionSpeedsIndex]); - Serial1.write(nextionSuffix, sizeof(nextionSuffix)); - Serial1.write(nextionSuffix, sizeof(nextionSuffix)); - Serial1.write(nextionSuffix, sizeof(nextionSuffix)); - Serial1.print("bauds=" + String(nextionBaud)); - Serial1.write(nextionSuffix, sizeof(nextionSuffix)); - Serial1.flush(); - } - Serial1.begin(atoi(nextionBaud)); -} - -//////////////////////////////////////////////////////////////////////////////////////////////////// -void nextionReset() -{ - debugPrintln(F("HMI: Rebooting LCD")); - digitalWrite(nextionResetPin, LOW); - Serial1.print("rest"); - Serial1.write(nextionSuffix, sizeof(nextionSuffix)); - Serial1.flush(); - delay(100); - digitalWrite(nextionResetPin, HIGH); - - unsigned long lcdResetTimer = millis(); - const unsigned long lcdResetTimeout = 5000; - - lcdConnected = false; - while (!lcdConnected && (millis() < (lcdResetTimer + lcdResetTimeout))) - { - nextionHandleInput(); - } - if (lcdConnected) - { - debugPrintln(F("HMI: Rebooting LCD completed")); - if (nextionActivePage) - { - nextionSendCmd("page " + String(nextionActivePage)); - } - } - else - { - debugPrintln(F("ERROR: Rebooting LCD completed, but LCD is not responding.")); - } - mqttClient.publish(mqttStatusTopic, "OFF", true, 0); - debugPrintln(String(F("MQTT OUT: '")) + mqttStatusTopic + String(F("' : 'OFF'"))); -} - -//////////////////////////////////////////////////////////////////////////////////////////////////// -void nextionUpdateProgress(const unsigned int &progress, const unsigned int &total) -{ - uint8_t progressPercent = (float(progress) / float(total)) * 100; - nextionSetAttr("p[0].b[4].val", String(progressPercent)); -} - -//////////////////////////////////////////////////////////////////////////////////////////////////// -void espWifiConnect() -{ // Connect to WiFi - rebootOnp0b1 = true; - - nextionSetAttr("p[0].b[1].font", "6"); - if (lcdVersion < 1 || lcdVersion > 2) - { - nextionSendCmd("page 0"); - } - - WiFi.macAddress(espMac); // Read our MAC address and save it to espMac - WiFi.hostname(haspNode); // Assign our hostname before connecting to WiFi - WiFi.setAutoReconnect(true); // Tell WiFi to autoreconnect if connection has dropped - WiFi.setSleepMode(WIFI_NONE_SLEEP); // Disable WiFi sleep modes to prevent occasional disconnects - WiFi.mode(WIFI_STA); // Set the radio to Station - - if (String(wifiSSID) == "") - { // If the sketch has no hard-coded wifiSSID, attempt to use saved creds or use WiFiManager to collect required information from the user. - - // First, check if we have saved wifi creds and try to connect manually. - if (WiFi.SSID() != "") - { - nextionSetAttr("p[0].b[1].txt", "\"WiFi Connecting...\\r " + String(WiFi.SSID()) + "\""); - unsigned long connectTimer = millis() + 10000; - debugPrintln(String(F("WIFI: Connecting to previously-saved SSID: ")) + String(WiFi.SSID())); - WiFi.begin(); - while ((WiFi.status() != WL_CONNECTED) && (millis() < connectTimer)) - { - yield(); - } - - unsigned int connectCounter = 0; - unsigned int connectRetries = 4; - unsigned int connectTime = 10000; - while ((WiFi.status() != WL_CONNECTED) && (connectCounter <= connectRetries)) - { - connectCounter++; - debugPrintln(String(F("WIFI: Connect failed, retry attempt ")) + String(connectCounter)); - WiFi.mode(WIFI_OFF); // Force the radio off, and then - delay(100); - WiFi.mode(WIFI_STA); // toggle it back on again - connectTimer = millis() + connectTime; - WiFi.begin(); - while ((WiFi.status() != WL_CONNECTED) && (millis() < connectTimer)) - { - yield(); - } - } - } - - if (WiFi.status() != WL_CONNECTED) - { // We gave it a shot, still couldn't connect, so let WiFiManager run to make one last - // connection attempt and then flip to AP mode to collect credentials from the user. - - WiFiManagerParameter custom_haspNodeHeader("
HASPone Node"); - WiFiManagerParameter custom_haspNode("haspNode", "
Node Name (required: lowercase letters, numbers, and _ only)", haspNode, 15, " maxlength=15 required pattern='[a-z0-9_]*'"); - WiFiManagerParameter custom_groupName("groupName", "Group Name (required)", groupName, 15, " maxlength=15 required"); - WiFiManagerParameter custom_mqttHeader("

MQTT"); - WiFiManagerParameter custom_mqttServer("mqttServer", "
MQTT Broker (required, IP address is preferred)", mqttServer, 127, " maxlength=127"); - WiFiManagerParameter custom_mqttPort("mqttPort", "MQTT Port (required)", mqttPort, 5, " maxlength=5 type='number'"); - WiFiManagerParameter custom_mqttUser("mqttUser", "MQTT User (optional)", mqttUser, 127, " maxlength=127"); - WiFiManagerParameter custom_mqttPassword("mqttPassword", "MQTT Password (optional)", mqttPassword, 127, " maxlength=127 type='password'"); - String mqttTlsEnabled_value = "F"; - if (mqttTlsEnabled) - { - mqttTlsEnabled_value = "T"; - } - String mqttTlsEnabled_checked = "type=\"checkbox\""; - if (mqttTlsEnabled) - { - mqttTlsEnabled_checked = "type=\"checkbox\" checked=\"true\""; - } - WiFiManagerParameter custom_mqttTlsEnabled("mqttTlsEnabled", "MQTT TLS enabled:", mqttTlsEnabled_value.c_str(), 2, mqttTlsEnabled_checked.c_str()); - WiFiManagerParameter custom_mqttFingerprint("mqttFingerprint", "
MQTT TLS Fingerprint (optional, enter as 01:23:AB:CD, etc)", mqttFingerprint, 59, " min length=59 maxlength=59"); - WiFiManagerParameter custom_configHeader("

Admin access"); - WiFiManagerParameter custom_configUser("configUser", "
Config User (required)", configUser, 15, " maxlength=31"); - WiFiManagerParameter custom_configPassword("configPassword", "Config Password (optional)", configPassword, 31, " maxlength=31 type='password'"); - WiFiManagerParameter custom_hassHeader("

Home Assistant integration"); - WiFiManagerParameter custom_hassDiscovery("hassDiscovery", "
Home Assistant Discovery topic (required, should probably be \"homeassistant\")", hassDiscovery, 127, " maxlength=127"); - - WiFiManager wifiManager; - wifiManager.setSaveConfigCallback(configSaveCallback); // set config save notify callback - wifiManager.setCustomHeadElement(HASP_STYLE); // add custom style - wifiManager.addParameter(&custom_haspNodeHeader); - wifiManager.addParameter(&custom_haspNode); - wifiManager.addParameter(&custom_groupName); - wifiManager.addParameter(&custom_mqttHeader); - wifiManager.addParameter(&custom_mqttServer); - wifiManager.addParameter(&custom_mqttPort); - wifiManager.addParameter(&custom_mqttUser); - wifiManager.addParameter(&custom_mqttPassword); - wifiManager.addParameter(&custom_mqttTlsEnabled); - wifiManager.addParameter(&custom_mqttFingerprint); - wifiManager.addParameter(&custom_configHeader); - wifiManager.addParameter(&custom_configUser); - wifiManager.addParameter(&custom_configPassword); - wifiManager.addParameter(&custom_hassHeader); - wifiManager.addParameter(&custom_hassDiscovery); - - // Timeout config portal after connectTimeout seconds, useful if configured wifi network was temporarily unavailable - wifiManager.setTimeout(connectTimeout); - - wifiManager.setAPCallback(espWifiConfigCallback); - - // Fetches SSID and pass from EEPROM and tries to connect - // If it does not connect it starts an access point with the specified name - // and goes into a blocking loop awaiting configuration. - if (!wifiManager.autoConnect(wifiConfigAP, wifiConfigPass)) - { // Reset and try again - debugPrintln(F("WIFI: Failed to connect and hit timeout")); - espReset(); - } - - // Read updated parameters - strcpy(mqttServer, custom_mqttServer.getValue()); - strcpy(mqttPort, custom_mqttPort.getValue()); - strcpy(mqttUser, custom_mqttUser.getValue()); - strcpy(mqttPassword, custom_mqttPassword.getValue()); - if (strcmp(custom_mqttTlsEnabled.getValue(), "T") == 0) - { - mqttTlsEnabled = true; - } - else - { - mqttTlsEnabled = false; - } - strcpy(mqttFingerprint, custom_mqttFingerprint.getValue()); - strcpy(haspNode, custom_haspNode.getValue()); - strcpy(groupName, custom_groupName.getValue()); - strcpy(configUser, custom_configUser.getValue()); - strcpy(configPassword, custom_configPassword.getValue()); - strcpy(hassDiscovery, custom_hassDiscovery.getValue()); - if (shouldSaveConfig) - { // Save the custom parameters to FS - configSave(); - } - } - } - else - { // wifiSSID has been defined, so attempt to connect to it - debugPrintln(String(F("Connecting to WiFi network: ")) + String(wifiSSID)); - WiFi.mode(WIFI_STA); - WiFi.begin(wifiSSID, wifiPass); - - unsigned long wifiReconnectTimer = millis(); - while (WiFi.status() != WL_CONNECTED) - { - delay(1); - if (millis() >= (wifiReconnectTimer + (connectTimeout * 1000))) - { // If we've been trying to reconnect for connectTimeout seconds, reboot and try again - debugPrintln(F("WIFI: Failed to connect and hit timeout")); - espReset(); - } - } - } - - // If you get here you have connected to WiFi - nextionSetAttr("p[0].b[1].font", "6"); - nextionSetAttr("p[0].b[1].txt", "\"WiFi Connected!\\r " + String(WiFi.SSID()) + "\\rIP: " + WiFi.localIP().toString() + "\""); - debugPrintln(String(F("WIFI: Connected successfully and assigned IP: ")) + WiFi.localIP().toString()); - if (nextionActivePage) - { - nextionSendCmd("page " + String(nextionActivePage)); - } - - rebootOnp0b1 = false; -} - -//////////////////////////////////////////////////////////////////////////////////////////////////// -void espWifiReconnect() -{ // Existing WiFi connection dropped, try to reconnect - debugPrintln(F("Reconnecting to WiFi network...")); - WiFi.mode(WIFI_STA); - WiFi.begin(wifiSSID, wifiPass); - - unsigned long wifiReconnectTimer = millis(); - while (WiFi.status() != WL_CONNECTED) - { - delay(1); - if (millis() >= (wifiReconnectTimer + (reConnectTimeout * 1000))) - { // If we've been trying to reconnect for reConnectTimeout seconds, reboot and try again - debugPrintln(F("WIFI: Failed to reconnect and hit timeout")); - espReset(); - } - } -} - -//////////////////////////////////////////////////////////////////////////////////////////////////// -void espWifiConfigCallback(WiFiManager *myWiFiManager) -{ // Notify the user that we're entering config mode - debugPrintln(F("WIFI: Failed to connect to assigned AP, entering config mode")); - if (lcdVersion < 1 || lcdVersion > 2) - { - nextionSendCmd("page 0"); - } - nextionSetAttr("p[0].b[1].font", "6"); - nextionSetAttr("p[0].b[1].txt", "\" HASPone Setup\\r AP: " + String(wifiConfigAP) + "\\rPassword: " + String(wifiConfigPass) + "\\r\\r\\r\\r\\r\\r\\r http://192.168.4.1\""); - nextionSendCmd("vis 3,1"); -} - -//////////////////////////////////////////////////////////////////////////////////////////////////// -void espSetupOta() -{ // Update ESP firmware from network via Arduino OTA - - ArduinoOTA.setHostname(haspNode); - ArduinoOTA.setPassword(configPassword); - ArduinoOTA.setRebootOnSuccess(false); - - ArduinoOTA.onStart([]() - { - debugPrintln(F("ESP OTA: update start")); - nextionSetAttr("p[0].b[1].txt", "\"\\rHASPone update:\\r\\r\\r \""); - nextionSendCmd("page 0"); - nextionSendCmd("vis 4,1"); - }); - ArduinoOTA.onEnd([]() - { - debugPrintln(F("ESP OTA: update complete")); - nextionSetAttr("p[0].b[1].txt", "\"\\rHASPone update:\\r\\r Complete!\\rRestarting.\""); - nextionSendCmd("vis 4,1"); - delay(1000); - espReset(); - }); - ArduinoOTA.onProgress([](unsigned int progress, unsigned int total) - { nextionUpdateProgress(progress, total); }); - ArduinoOTA.onError([](ota_error_t error) - { - debugPrintln(String(F("ESP OTA: ERROR code ")) + String(error)); - if (error == OTA_AUTH_ERROR) - debugPrintln(F("ESP OTA: ERROR - Auth Failed")); - else if (error == OTA_BEGIN_ERROR) - debugPrintln(F("ESP OTA: ERROR - Begin Failed")); - else if (error == OTA_CONNECT_ERROR) - debugPrintln(F("ESP OTA: ERROR - Connect Failed")); - else if (error == OTA_RECEIVE_ERROR) - debugPrintln(F("ESP OTA: ERROR - Receive Failed")); - else if (error == OTA_END_ERROR) - debugPrintln(F("ESP OTA: ERROR - End Failed")); - nextionSendCmd("vis 4,0"); - nextionSetAttr("p[0].b[1].txt", "\"HASPone update:\\r FAILED\\rerror: " + String(error) + "\""); - delay(1000); - nextionSendCmd("page " + String(nextionActivePage)); - }); - ArduinoOTA.begin(); - debugPrintln(F("ESP OTA: Over the Air firmware update ready")); -} - -//////////////////////////////////////////////////////////////////////////////////////////////////// -void espStartOta(const String &espOtaUrl) -{ // Update ESP firmware from HTTP/HTTPS URL - - nextionSetAttr("p[0].b[1].txt", "\"\\rHASPone update:\\r\\r\\r \""); - nextionSendCmd("page 0"); - nextionSendCmd("vis 4,1"); - - WiFiUDP::stopAll(); // Keep mDNS responder from breaking things - delay(1); - ESPhttpUpdate.rebootOnUpdate(false); - ESPhttpUpdate.onProgress(nextionUpdateProgress); - t_httpUpdate_return espOtaUrlReturnCode; - if (espOtaUrl.startsWith(F("https"))) - { - debugPrintln(String(F("ESPFW: Attempting firmware update from HTTPS host: ")) + espOtaUrl); - WiFiClientSecure wifiEspOtaClientSecure; - wifiEspOtaClientSecure.setInsecure(); - wifiEspOtaClientSecure.setBufferSizes(512, 512); - espOtaUrlReturnCode = ESPhttpUpdate.update(wifiEspOtaClientSecure, espOtaUrl); - } - else - { - debugPrintln(String(F("ESPFW: Attempting firmware update from HTTP host: ")) + espOtaUrl); - espOtaUrlReturnCode = ESPhttpUpdate.update(wifiClient, espOtaUrl); - } - - switch (espOtaUrlReturnCode) - { - case HTTP_UPDATE_FAILED: - debugPrintln(String(F("ESPFW: HTTP_UPDATE_FAILED error ")) + String(ESPhttpUpdate.getLastError()) + " " + ESPhttpUpdate.getLastErrorString()); - nextionSendCmd("vis 4,0"); - nextionSetAttr("p[0].b[1].txt", "\"HASPone update:\\r FAILED\\rerror: " + ESPhttpUpdate.getLastErrorString() + "\""); - break; - - case HTTP_UPDATE_NO_UPDATES: - debugPrintln(F("ESPFW: HTTP_UPDATE_NO_UPDATES")); - nextionSendCmd("vis 4,0"); - nextionSetAttr("p[0].b[1].txt", "\"HASPone update:\\rNo update\""); - break; - - case HTTP_UPDATE_OK: - debugPrintln(F("ESPFW: HTTP_UPDATE_OK")); - nextionSetAttr("p[0].b[1].txt", "\"\\rHASPone update:\\r\\r Complete!\\rRestarting.\""); - nextionSendCmd("vis 4,1"); - delay(1000); - espReset(); - } - delay(1000); - nextionSendCmd("page " + String(nextionActivePage)); -} - -//////////////////////////////////////////////////////////////////////////////////////////////////// -void espReset() -{ - debugPrintln(F("RESET: HASPone reset")); - if (mqttClient.connected()) - { - mqttClient.publish(mqttStateJSONTopic, String(F("{\"event_type\":\"hasp_device\",\"event\":\"offline\"}"))); - debugPrintln(String(F("MQTT OUT: '")) + mqttStateJSONTopic + String(F(" : {\"event_type\":\"hasp_device\",\"event\":\"offline\"}"))); - mqttClient.publish(mqttStatusTopic, "OFF", true, 0); - mqttClient.disconnect(); - debugPrintln(String(F("MQTT OUT: '")) + mqttStatusTopic + String(F("' : 'OFF'"))); - } - debugPrintln(F("HMI: Rebooting LCD")); - digitalWrite(nextionResetPin, LOW); - Serial1.print("rest"); - Serial1.write(nextionSuffix, sizeof(nextionSuffix)); - Serial1.flush(); - delay(500); - ESP.reset(); - delay(5000); -} - -//////////////////////////////////////////////////////////////////////////////////////////////////// -void configRead() -{ // Read saved config.json from SPIFFS - debugPrintln(F("SPIFFS: mounting SPIFFS")); - if (SPIFFS.begin()) - { - if (SPIFFS.exists("/config.json")) - { // File exists, reading and loading - debugPrintln(F("SPIFFS: reading /config.json")); - // debugPrintFile("/config.json"); - File configFile = SPIFFS.open("/config.json", "r"); - if (configFile) - { - DynamicJsonDocument jsonConfigValues(1536); - DeserializationError jsonError = deserializeJson(jsonConfigValues, configFile); - - if (jsonError) - { // Couldn't parse the saved config - debugPrintln(String(F("SPIFFS: [ERROR] Failed to parse /config.json: ")) + String(jsonError.c_str())); - } - else - { - if (!jsonConfigValues["mqttServer"].isNull()) - { - strcpy(mqttServer, jsonConfigValues["mqttServer"]); - } - if (!jsonConfigValues["mqttPort"].isNull()) - { - strcpy(mqttPort, jsonConfigValues["mqttPort"]); - } - if (!jsonConfigValues["mqttUser"].isNull()) - { - strcpy(mqttUser, jsonConfigValues["mqttUser"]); - } - if (!jsonConfigValues["mqttPassword"].isNull()) - { - strcpy(mqttPassword, jsonConfigValues["mqttPassword"]); - } - if (!jsonConfigValues["mqttFingerprint"].isNull()) - { - strcpy(mqttFingerprint, jsonConfigValues["mqttFingerprint"]); - } - if (!jsonConfigValues["haspNode"].isNull()) - { - strcpy(haspNode, jsonConfigValues["haspNode"]); - } - if (!jsonConfigValues["groupName"].isNull()) - { - strcpy(groupName, jsonConfigValues["groupName"]); - } - if (!jsonConfigValues["configUser"].isNull()) - { - strcpy(configUser, jsonConfigValues["configUser"]); - } - if (!jsonConfigValues["configPassword"].isNull()) - { - strcpy(configPassword, jsonConfigValues["configPassword"]); - } - if (!jsonConfigValues["hassDiscovery"].isNull()) - { - strcpy(hassDiscovery, jsonConfigValues["hassDiscovery"]); - } - if (strcmp(hassDiscovery, "") == 0) - { // Cover off any edge case where this value winds up being empty - debugPrintln(F("SPIFFS: [WARNING] /config.json has empty hassDiscovery value, setting to 'homeassistant'")); - strcpy(hassDiscovery, "homeassistant"); - } - if (!jsonConfigValues["nextionBaud"].isNull()) - { - strcpy(nextionBaud, jsonConfigValues["nextionBaud"]); - } - if (strcmp(nextionBaud, "") == 0) - { // Cover off any edge case where this value winds up being empty - debugPrintln(F("SPIFFS: [WARNING] /config.json has empty nextionBaud value, setting to '115200'")); - strcpy(nextionBaud, "115200"); - } - if (!jsonConfigValues["nextionMaxPages"].isNull()) - { - nextionMaxPages = jsonConfigValues["nextionMaxPages"]; - } - if (nextionMaxPages < 1) - { // Cover off any edge case where this value winds up being zero or negative - debugPrintln(F("SPIFFS: [WARNING] /config.json has nextionMaxPages value of zero or negative, setting to '11'")); - nextionMaxPages = 11; - } - if (!jsonConfigValues["motionPinConfig"].isNull()) - { - strcpy(motionPinConfig, jsonConfigValues["motionPinConfig"]); - } - if (!jsonConfigValues["mqttTlsEnabled"].isNull()) - { - if (jsonConfigValues["mqttTlsEnabled"]) - { - mqttTlsEnabled = true; - } - else - { - mqttTlsEnabled = false; - } - } - if (!jsonConfigValues["debugSerialEnabled"].isNull()) - { - if (jsonConfigValues["debugSerialEnabled"]) - { - debugSerialEnabled = true; - } - else - { - debugSerialEnabled = false; - } - } - if (!jsonConfigValues["debugTelnetEnabled"].isNull()) - { - if (jsonConfigValues["debugTelnetEnabled"]) - { - debugTelnetEnabled = true; - } - else - { - debugTelnetEnabled = false; - } - } - if (!jsonConfigValues["mdnsEnabled"].isNull()) - { - if (jsonConfigValues["mdnsEnabled"]) - { - mdnsEnabled = true; - } - else - { - mdnsEnabled = false; - } - } - if (!jsonConfigValues["beepEnabled"].isNull()) - { - if (jsonConfigValues["beepEnabled"]) - { - beepEnabled = true; - } - else - { - beepEnabled = false; - } - } - if (!jsonConfigValues["ignoreTouchWhenOff"].isNull()) - { - if (jsonConfigValues["ignoreTouchWhenOff"]) - { - ignoreTouchWhenOff = true; - } - else - { - ignoreTouchWhenOff = false; - } - } - String jsonConfigValuesStr; - serializeJson(jsonConfigValues, jsonConfigValuesStr); - debugPrintln(String(F("SPIFFS: read ")) + String(configFile.size()) + String(F(" bytes and parsed json:")) + jsonConfigValuesStr); - } - } - else - { - debugPrintln(F("SPIFFS: [ERROR] Failed to read /config.json")); - } - } - else - { - debugPrintln(F("SPIFFS: [WARNING] /config.json not found, will be created on first config save")); - } - } - else - { - debugPrintln(F("SPIFFS: [ERROR] Failed to mount FS")); - } -} - -//////////////////////////////////////////////////////////////////////////////////////////////////// -void configSaveCallback() -{ // Callback notifying us of the need to save config - debugPrintln(F("SPIFFS: Configuration changed, flagging for save")); - shouldSaveConfig = true; -} - -//////////////////////////////////////////////////////////////////////////////////////////////////// -void configSave() -{ // Save the custom parameters to config.json - debugPrintln(F("SPIFFS: Saving config")); - DynamicJsonDocument jsonConfigValues(2048); - - jsonConfigValues["mqttServer"] = mqttServer; - jsonConfigValues["mqttPort"] = mqttPort; - jsonConfigValues["mqttUser"] = mqttUser; - jsonConfigValues["mqttPassword"] = mqttPassword; - jsonConfigValues["mqttTlsEnabled"] = mqttTlsEnabled; - jsonConfigValues["mqttFingerprint"] = mqttFingerprint; - jsonConfigValues["haspNode"] = haspNode; - jsonConfigValues["groupName"] = groupName; - jsonConfigValues["configUser"] = configUser; - jsonConfigValues["configPassword"] = configPassword; - jsonConfigValues["hassDiscovery"] = hassDiscovery; - jsonConfigValues["nextionBaud"] = nextionBaud; - jsonConfigValues["nextionMaxPages"] = nextionMaxPages; - jsonConfigValues["motionPinConfig"] = motionPinConfig; - jsonConfigValues["debugSerialEnabled"] = debugSerialEnabled; - jsonConfigValues["debugTelnetEnabled"] = debugTelnetEnabled; - jsonConfigValues["mdnsEnabled"] = mdnsEnabled; - jsonConfigValues["beepEnabled"] = beepEnabled; - jsonConfigValues["ignoreTouchWhenOff"] = ignoreTouchWhenOff; - - debugPrintln(String(F("SPIFFS: mqttServer = ")) + String(mqttServer)); - debugPrintln(String(F("SPIFFS: mqttPort = ")) + String(mqttPort)); - debugPrintln(String(F("SPIFFS: mqttUser = ")) + String(mqttUser)); - debugPrintln(String(F("SPIFFS: mqttPassword = ")) + String(mqttPassword)); - debugPrintln(String(F("SPIFFS: mqttTlsEnabled = ")) + String(mqttTlsEnabled)); - debugPrintln(String(F("SPIFFS: mqttFingerprint = ")) + String(mqttFingerprint)); - debugPrintln(String(F("SPIFFS: haspNode = ")) + String(haspNode)); - debugPrintln(String(F("SPIFFS: groupName = ")) + String(groupName)); - debugPrintln(String(F("SPIFFS: configUser = ")) + String(configUser)); - debugPrintln(String(F("SPIFFS: configPassword = ")) + String(configPassword)); - debugPrintln(String(F("SPIFFS: hassDiscovery = ")) + String(hassDiscovery)); - debugPrintln(String(F("SPIFFS: nextionBaud = ")) + String(nextionBaud)); - debugPrintln(String(F("SPIFFS: nextionMaxPages = ")) + String(nextionMaxPages)); - debugPrintln(String(F("SPIFFS: motionPinConfig = ")) + String(motionPinConfig)); - debugPrintln(String(F("SPIFFS: debugSerialEnabled = ")) + String(debugSerialEnabled)); - debugPrintln(String(F("SPIFFS: debugTelnetEnabled = ")) + String(debugTelnetEnabled)); - debugPrintln(String(F("SPIFFS: mdnsEnabled = ")) + String(mdnsEnabled)); - debugPrintln(String(F("SPIFFS: beepEnabled = ")) + String(beepEnabled)); - debugPrintln(String(F("SPIFFS: ignoreTouchWhenOff = ")) + String(ignoreTouchWhenOff)); - - File configFile = SPIFFS.open("/config.json", "w"); - if (!configFile) - { - debugPrintln(F("SPIFFS: Failed to open config file for writing")); - } - else - { - serializeJson(jsonConfigValues, configFile); - configFile.print("\n\n\n"); - configFile.flush(); - delay(10); - configFile.close(); - } - debugPrintFile("/config.json"); - shouldSaveConfig = false; -} - -//////////////////////////////////////////////////////////////////////////////////////////////////// -void configClearSaved() -{ // Clear out all local storage - nextionSetAttr("dims", "100"); - nextionSendCmd("page 0"); - nextionSetAttr("p[0].b[1].txt", "\"Resetting\\rsystem...\""); - debugPrintln(F("RESET: Formatting SPIFFS")); - SPIFFS.format(); - debugPrintln(F("RESET: Clearing WiFiManager settings...")); - WiFi.disconnect(); - WiFiManager wifiManager; - wifiManager.resetSettings(); - EEPROM.begin(512); - debugPrintln(F("Clearing EEPROM...")); - for (uint16_t i = 0; i < EEPROM.length(); i++) - { - EEPROM.write(i, 0); - } - nextionSetAttr("p[0].b[1].txt", "\"Rebooting\\rsystem...\""); - debugPrintln(F("RESET: Rebooting device")); - espReset(); -} - -//////////////////////////////////////////////////////////////////////////////////////////////////// -void webHandleNotFound() -{ // webServer 404 - debugPrintln(String(F("HTTP: Sending 404 to client connected from: ")) + webServer.client().remoteIP().toString()); - String httpHeader = FPSTR(HTTP_HEAD_START); - httpHeader.replace("{v}", "HASPone " + String(haspNode) + " 404"); - webServer.setContentLength(CONTENT_LENGTH_UNKNOWN); - webServer.send(404, "text/html", httpHeader); - webServer.sendContent_P(HTTP_SCRIPT); - webServer.sendContent_P(HTTP_STYLE); - webServer.sendContent_P(HASP_STYLE); - webServer.sendContent_P(HTTP_HEAD_END); - webServer.sendContent(F("

404: File Not Found

One of us appears to have done something horribly wrong.
URI: ")); - webServer.sendContent(webServer.uri()); - webServer.sendContent(F("
Method: ")); - webServer.sendContent((webServer.method() == HTTP_GET) ? F("GET") : F("POST")); - webServer.sendContent(F("
Arguments: ")); - webServer.sendContent(String(webServer.args())); - for (uint8_t i = 0; i < webServer.args(); i++) - { - webServer.sendContent(F("
")); - webServer.sendContent(String(webServer.argName(i))); - webServer.sendContent(F(": ")); - webServer.sendContent(String(webServer.arg(i))); - } - webServer.sendContent(""); -} - -//////////////////////////////////////////////////////////////////////////////////////////////////// -void webHandleRoot() -{ // http://plate01/ - if (configPassword[0] != '\0') - { //Request HTTP auth if configPassword is set - if (!webServer.authenticate(configUser, configPassword)) - { - return webServer.requestAuthentication(); - } - } - debugPrintln(String(F("HTTP: Sending root page to client connected from: ")) + webServer.client().remoteIP().toString()); - - String httpHeader = FPSTR(HTTP_HEAD_START); - httpHeader.replace("{v}", "HASPone " + String(haspNode)); - webServer.setContentLength(CONTENT_LENGTH_UNKNOWN); - webServer.send(200, "text/html", httpHeader); - webServer.sendContent(httpHeader); - webServer.sendContent_P(HTTP_SCRIPT); - webServer.sendContent_P(HTTP_STYLE); - webServer.sendContent_P(HASP_STYLE); - webServer.sendContent_P(HTTP_HEAD_END); - - webServer.sendContent(F("

")); - webServer.sendContent(haspNode); - webServer.sendContent(F("

")); - - webServer.sendContent(F("
")); - webServer.sendContent(F("WiFi SSID (required)
WiFi Password (required)")); - webServer.sendContent(F("

HASPone Node Name (required. lowercase letters, numbers, and _ only)
Group Name (required)

MQTT Broker (required, IP address is preferred)
MQTT Port (required)
MQTT User (optional)
MQTT Password (optional)")); - - webServer.sendContent(F("
MQTT TLS enabled:
MQTT TLS Fingerpint (leave blank to disable fingerprint checking)")); - - webServer.sendContent(F("

HASPone Admin Username (optional)
HASPone Admin Password (optional)

Home Assistant discovery topic (required, should probably be \"homeassistant\")
Nextion project page count (required, probably \"11\")

")); - // Big menu of possible serial speeds - if ((lcdVersion != 1) && (lcdVersion != 2)) - { // HASPone lcdVersion 1 and 2 have `bauds=115200` in the pre-init script of page 0. Don't show this option if either of those two versions are running. - webServer.sendContent(F("LCD Serial Speed: 
")); - } - - webServer.sendContent(F("USB serial debug output enabled:
Telnet debug output enabled:
mDNS enabled:
Motion Sensor Pin: ")); - webServer.sendContent(F("
Keypress beep enabled on D2:
Ignore touchevents when backlight is off:

")); - - if (updateEspAvailable) - { - webServer.sendContent(F("

HASPone Update available!

")); - webServer.sendContent(F("
")); - webServer.sendContent(F("
")); - } - - webServer.sendContent(F("
")); - webServer.sendContent(F("
")); - - webServer.sendContent(F("
")); - webServer.sendContent(F("
")); - - webServer.sendContent(F("
")); - webServer.sendContent(F("
")); - - webServer.sendContent(F("
")); - webServer.sendContent(F("
")); - - webServer.sendContent(F("
MQTT Status: ")); - if (mqttClient.connected()) - { // Check MQTT connection - webServer.sendContent(F("Connected")); - } - else - { - webServer.sendContent(F("Disconnected
MQTT return code: ")); - webServer.sendContent(String(mqttClient.returnCode())); - webServer.sendContent(F("
MQTT last error: ")); - webServer.sendContent(String(mqttClient.lastError())); - webServer.sendContent(F("
MQTT broker ping check: ")); - if (mqttPingCheck) - { - webServer.sendContent(F("SUCCESS")); - } - else - { - webServer.sendContent(F("FAILED")); - } - webServer.sendContent(F("
MQTT broker port check: ")); - if (mqttPortCheck) - { - webServer.sendContent(F("SUCCESS")); - } - else - { - webServer.sendContent(F("FAILED")); - } - } - webServer.sendContent(F("
MQTT ClientID: ")); - if (mqttClientId != "") - { - webServer.sendContent(mqttClientId); - } - webServer.sendContent(F("
HASPone FW Version: ")); - webServer.sendContent(String(haspVersion)); - webServer.sendContent(F("
LCD Model: ")); - if (nextionModel != "") - { - webServer.sendContent(nextionModel); - } - webServer.sendContent(F("
LCD FW Version: ")); - webServer.sendContent(String(lcdVersion)); - webServer.sendContent(F("
LCD Active Page: ")); - webServer.sendContent(String(nextionActivePage)); - webServer.sendContent(F("
LCD Serial Speed: ")); - webServer.sendContent(nextionBaud); - webServer.sendContent(F("
CPU Frequency: ")); - webServer.sendContent(String(ESP.getCpuFreqMHz())); - webServer.sendContent(F("MHz")); - webServer.sendContent(F("
Sketch Size: ")); - webServer.sendContent(String(ESP.getSketchSize())); - webServer.sendContent(F(" bytes")); - webServer.sendContent(F("
Free Sketch Space: ")); - webServer.sendContent(String(ESP.getFreeSketchSpace())); - webServer.sendContent(F(" bytes")); - webServer.sendContent(F("
Heap Free: ")); - webServer.sendContent(String(ESP.getFreeHeap())); - webServer.sendContent(F("
Heap Fragmentation: ")); - webServer.sendContent(String(ESP.getHeapFragmentation())); - webServer.sendContent(F("
ESP core version: ")); - webServer.sendContent(ESP.getCoreVersion()); - webServer.sendContent(F("
IP Address: ")); - webServer.sendContent(WiFi.localIP().toString()); - webServer.sendContent(F("
Signal Strength: ")); - webServer.sendContent(String(WiFi.RSSI())); - webServer.sendContent(F("
Uptime: ")); - webServer.sendContent(String(long(millis() / 1000))); - webServer.sendContent(F("
Last reset: ")); - webServer.sendContent(ESP.getResetInfo()); - webServer.sendContent_P(HTTP_END); - webServer.sendContent(""); -} - -//////////////////////////////////////////////////////////////////////////////////////////////////// -void webHandleSaveConfig() -{ // http://plate01/saveConfig - if (configPassword[0] != '\0') - { //Request HTTP auth if configPassword is set - if (!webServer.authenticate(configUser, configPassword)) - { - return webServer.requestAuthentication(); - } - } - debugPrintln(String(F("HTTP: Sending /saveConfig page to client connected from: ")) + webServer.client().remoteIP().toString()); - String httpHeader = FPSTR(HTTP_HEAD_START); - httpHeader.replace("{v}", "HASPone " + String(haspNode) + " Saving configuration"); - webServer.setContentLength(CONTENT_LENGTH_UNKNOWN); - webServer.send(200, "text/html", httpHeader); - webServer.sendContent_P(HTTP_SCRIPT); - webServer.sendContent_P(HTTP_STYLE); - webServer.sendContent_P(HASP_STYLE); - webServer.sendContent_P(HTTP_HEAD_END); - - bool shouldSaveWifi = false; - // Check required values - if ((webServer.arg("wifiSSID") != "") && (webServer.arg("wifiSSID") != String(WiFi.SSID()))) - { // Handle WiFi SSID - shouldSaveConfig = true; - shouldSaveWifi = true; - webServer.arg("wifiSSID").toCharArray(wifiSSID, 32); - } - if ((webServer.arg("wifiPass") != String(wifiPass)) && (webServer.arg("wifiPass") != String("********"))) - { // Handle WiFi password - shouldSaveConfig = true; - shouldSaveWifi = true; - webServer.arg("wifiPass").toCharArray(wifiPass, 64); - } - - if (webServer.arg("haspNode") != "" && webServer.arg("haspNode") != String(haspNode)) - { // Handle haspNode - shouldSaveConfig = true; - String lowerHaspNode = webServer.arg("haspNode"); - lowerHaspNode.toLowerCase(); - lowerHaspNode.toCharArray(haspNode, 16); - } - if (webServer.arg("groupName") != "" && webServer.arg("groupName") != String(groupName)) - { // Handle groupName - shouldSaveConfig = true; - webServer.arg("groupName").toCharArray(groupName, 16); - } - - if (webServer.arg("mqttServer") != "" && webServer.arg("mqttServer") != String(mqttServer)) - { // Handle mqttServer - shouldSaveConfig = true; - webServer.arg("mqttServer").toCharArray(mqttServer, 128); - } - if (webServer.arg("mqttPort") != "" && webServer.arg("mqttPort") != String(mqttPort)) - { // Handle mqttPort - shouldSaveConfig = true; - webServer.arg("mqttPort").toCharArray(mqttPort, 6); - } - if (webServer.arg("mqttUser") != String(mqttUser)) - { // Handle mqttUser - shouldSaveConfig = true; - webServer.arg("mqttUser").toCharArray(mqttUser, 128); - } - if (webServer.arg("mqttPassword") != String("********")) - { // Handle mqttPassword - shouldSaveConfig = true; - webServer.arg("mqttPassword").toCharArray(mqttPassword, 128); - } - if ((webServer.arg("mqttTlsEnabled") == String("on")) && !mqttTlsEnabled) - { // mqttTlsEnabled was disabled but should now be enabled - shouldSaveConfig = true; - mqttTlsEnabled = true; - } - else if ((webServer.arg("mqttTlsEnabled") == String("")) && mqttTlsEnabled) - { // mqttTlsEnabled was enabled but should now be disabled - shouldSaveConfig = true; - mqttTlsEnabled = false; - } - if (webServer.arg("mqttFingerprint") != String(mqttFingerprint)) - { // Handle mqttFingerprint - shouldSaveConfig = true; - webServer.arg("mqttFingerprint").toCharArray(mqttFingerprint, 60); - } - if (webServer.arg("configUser") != String(configUser)) - { // Handle configUser - shouldSaveConfig = true; - webServer.arg("configUser").toCharArray(configUser, 32); - } - if (webServer.arg("configPassword") != String("********")) - { // Handle configPassword - shouldSaveConfig = true; - webServer.arg("configPassword").toCharArray(configPassword, 32); - } - if (webServer.arg("hassDiscovery") != String(hassDiscovery)) - { // Handle hassDiscovery - shouldSaveConfig = true; - webServer.arg("hassDiscovery").toCharArray(hassDiscovery, 128); - } - if ((webServer.arg("nextionMaxPages") != String(nextionMaxPages)) && (webServer.arg("nextionMaxPages").toInt() < 256) && (webServer.arg("nextionMaxPages").toInt() > 0)) - { - shouldSaveConfig = true; - nextionMaxPages = webServer.arg("nextionMaxPages").toInt(); - } - if (webServer.arg("nextionBaud") != String(nextionBaud)) - { // Handle nextionBaud - shouldSaveConfig = true; - webServer.arg("nextionBaud").toCharArray(nextionBaud, 7); - } - if (webServer.arg("motionPinConfig") != String(motionPinConfig)) - { // Handle motionPinConfig - shouldSaveConfig = true; - webServer.arg("motionPinConfig").toCharArray(motionPinConfig, 3); - } - if ((webServer.arg("debugSerialEnabled") == String("on")) && !debugSerialEnabled) - { // debugSerialEnabled was disabled but should now be enabled - shouldSaveConfig = true; - debugSerialEnabled = true; - } - else if ((webServer.arg("debugSerialEnabled") == String("")) && debugSerialEnabled) - { // debugSerialEnabled was enabled but should now be disabled - shouldSaveConfig = true; - debugSerialEnabled = false; - } - if ((webServer.arg("debugTelnetEnabled") == String("on")) && !debugTelnetEnabled) - { // debugTelnetEnabled was disabled but should now be enabled - shouldSaveConfig = true; - debugTelnetEnabled = true; - } - else if ((webServer.arg("debugTelnetEnabled") == String("")) && debugTelnetEnabled) - { // debugTelnetEnabled was enabled but should now be disabled - shouldSaveConfig = true; - debugTelnetEnabled = false; - } - if ((webServer.arg("mdnsEnabled") == String("on")) && !mdnsEnabled) - { // mdnsEnabled was disabled but should now be enabled - shouldSaveConfig = true; - mdnsEnabled = true; - } - else if ((webServer.arg("mdnsEnabled") == String("")) && mdnsEnabled) - { // mdnsEnabled was enabled but should now be disabled - shouldSaveConfig = true; - mdnsEnabled = false; - } - if ((webServer.arg("beepEnabled") == String("on")) && !beepEnabled) - { // beepEnabled was disabled but should now be enabled - shouldSaveConfig = true; - beepEnabled = true; - } - else if ((webServer.arg("beepEnabled") == String("")) && beepEnabled) - { // beepEnabled was enabled but should now be disabled - shouldSaveConfig = true; - beepEnabled = false; - } - if ((webServer.arg("ignoreTouchWhenOff") == String("on")) && !ignoreTouchWhenOff) - { // ignoreTouchWhenOff was disabled but should now be enabled - shouldSaveConfig = true; - ignoreTouchWhenOff = true; - } - else if ((webServer.arg("ignoreTouchWhenOff") == String("")) && ignoreTouchWhenOff) - { // ignoreTouchWhenOff was enabled but should now be disabled - shouldSaveConfig = true; - ignoreTouchWhenOff = false; - } - - if (shouldSaveConfig) - { // Config updated, notify user and trigger write to SPIFFS - - webServer.sendContent(F("")); - webServer.sendContent_P(HTTP_HEAD_END); - webServer.sendContent(F("

")); - webServer.sendContent(haspNode); - webServer.sendContent(F("

")); - webServer.sendContent(F("
Saving updated configuration values and restarting device")); - webServer.sendContent_P(HTTP_END); - webServer.sendContent(F("")); - configSave(); - if (shouldSaveWifi) - { - debugPrintln(String(F("CONFIG: Attempting connection to SSID: ")) + webServer.arg("wifiSSID")); - espWifiConnect(); - } - espReset(); - } - else - { // No change found, notify user and link back to config page - webServer.sendContent(F("")); - webServer.sendContent_P(HTTP_HEAD_END); - webServer.sendContent(F("

")); - webServer.sendContent(haspNode); - webServer.sendContent(F("

")); - webServer.sendContent(F("
No changes found, returning to home page")); - webServer.sendContent_P(HTTP_END); - webServer.sendContent(F("")); - } -} - -//////////////////////////////////////////////////////////////////////////////////////////////////// -void webHandleResetConfig() -{ // http://plate01/resetConfig - if (configPassword[0] != '\0') - { //Request HTTP auth if configPassword is set - if (!webServer.authenticate(configUser, configPassword)) - { - return webServer.requestAuthentication(); - } - } - debugPrintln(String(F("HTTP: Sending /resetConfig page to client connected from: ")) + webServer.client().remoteIP().toString()); - String httpHeader = FPSTR(HTTP_HEAD_START); - httpHeader.replace("{v}", "HASPone " + String(haspNode) + " Resetting configuration"); - webServer.setContentLength(CONTENT_LENGTH_UNKNOWN); - webServer.send(200, "text/html", httpHeader); - webServer.sendContent_P(HTTP_SCRIPT); - webServer.sendContent_P(HTTP_STYLE); - webServer.sendContent_P(HASP_STYLE); - webServer.sendContent_P(HTTP_HEAD_END); - - if (webServer.arg("confirm") == "yes") - { // User has confirmed, so reset everything - webServer.sendContent(F("

")); - webServer.sendContent(haspNode); - webServer.sendContent(F("

Resetting all saved settings and restarting device into WiFi AP mode")); - webServer.sendContent_P(HTTP_END); - webServer.sendContent(""); - delay(100); - configClearSaved(); - } - else - { - webServer.sendContent(F("

Warning

This process will reset all settings to the default values and restart the device. You may need to connect to the WiFi AP displayed on the panel to re-configure the device before accessing it again.")); - webServer.sendContent(F("


")); - webServer.sendContent(F("

")); - webServer.sendContent(F("


")); - webServer.sendContent(F("
")); - webServer.sendContent_P(HTTP_END); - webServer.sendContent(""); - } -} - -//////////////////////////////////////////////////////////////////////////////////////////////////// -void webHandleResetBacklight() -{ // http://plate01/resetBacklight - if (configPassword[0] != '\0') - { //Request HTTP auth if configPassword is set - if (!webServer.authenticate(configUser, configPassword)) - { - return webServer.requestAuthentication(); - } - } - debugPrintln(String(F("HTTP: Sending /resetBacklight page to client connected from: ")) + webServer.client().remoteIP().toString()); - String httpHeader = FPSTR(HTTP_HEAD_START); - httpHeader.replace("{v}", "HASPone " + String(haspNode) + " Backlight reset"); - webServer.setContentLength(CONTENT_LENGTH_UNKNOWN); - webServer.send(200, "text/html", httpHeader); - webServer.sendContent_P(HTTP_SCRIPT); - webServer.sendContent_P(HTTP_STYLE); - webServer.sendContent_P(HASP_STYLE); - webServer.sendContent(F("")); - webServer.sendContent_P(HTTP_HEAD_END); - - webServer.sendContent(F("

")); - webServer.sendContent(haspNode); - webServer.sendContent(F("

")); - webServer.sendContent(F("
Resetting backlight to 100%")); - webServer.sendContent_P(HTTP_END); - webServer.sendContent(""); - debugPrintln(F("HTTP: Resetting backlight to 100%")); - nextionSetAttr("dims", "100"); -} - -//////////////////////////////////////////////////////////////////////////////////////////////////// -void webHandleFirmware() -{ // http://plate01/firmware - if (configPassword[0] != '\0') - { //Request HTTP auth if configPassword is set - if (!webServer.authenticate(configUser, configPassword)) - { - return webServer.requestAuthentication(); - } - } - debugPrintln(String(F("HTTP: Sending /firmware page to client connected from: ")) + webServer.client().remoteIP().toString()); - String httpHeader = FPSTR(HTTP_HEAD_START); - httpHeader.replace("{v}", "HASPone " + String(haspNode) + " Firmware updates"); - webServer.setContentLength(CONTENT_LENGTH_UNKNOWN); - webServer.send(200, "text/html", httpHeader); - webServer.sendContent_P(HTTP_SCRIPT); - webServer.sendContent_P(HTTP_STYLE); - webServer.sendContent_P(HASP_STYLE); - webServer.sendContent_P(HTTP_HEAD_END); - - webServer.sendContent(F("

")); - webServer.sendContent(haspNode); - webServer.sendContent(F(" Firmware updates

Note: If updating firmware for both the ESP8266 and the Nextion LCD, you'll want to update the ESP8266 first followed by the Nextion LCD

")); - - // Display main firmware page - webServer.sendContent(F("
")); - if (updateEspAvailable) - { - webServer.sendContent(F("HASPone ESP8266 update available!")); - } - webServer.sendContent(F("
Update ESP8266 from URL")); - webServer.sendContent(F("


")); - - webServer.sendContent(F("
")); - webServer.sendContent(F("Update ESP8266 from file")); - webServer.sendContent(F("

")); - - webServer.sendContent(F("


WARNING!

")); - webServer.sendContent(F("Nextion LCD firmware updates can be risky. If interrupted, the LCD will display an error message until a successful firmware update has completed. ")); - webServer.sendContent(F("

Note: Failed LCD firmware updates on HASPone hardware prior to v1.0 may require a hard power cycle of the device, via a circuit breaker or by physically disconnecting the device.")); - - webServer.sendContent(F("

")); - if (updateLcdAvailable) - { - webServer.sendContent(F("HASPone LCD update available!")); - } - webServer.sendContent(F("
Update Nextion LCD from URL http only")); - webServer.sendContent(F("


")); - - webServer.sendContent(F("
")); - webServer.sendContent(F("
Update Nextion LCD from file")); - webServer.sendContent(F("

")); - - // Javascript to collect the filesize of the LCD upload and send it to /tftFileSize - webServer.sendContent(F("")); - - webServer.sendContent_P(HTTP_END); - webServer.sendContent(""); -} - -//////////////////////////////////////////////////////////////////////////////////////////////////// -void webHandleEspFirmware() -{ // http://plate01/espfirmware - if (configPassword[0] != '\0') - { //Request HTTP auth if configPassword is set - if (!webServer.authenticate(configUser, configPassword)) - { - return webServer.requestAuthentication(); - } - } - - debugPrintln(String(F("HTTP: Sending /espfirmware page to client connected from: ")) + webServer.client().remoteIP().toString()); - String httpHeader = FPSTR(HTTP_HEAD_START); - httpHeader.replace("{v}", "HASPone " + String(haspNode) + " ESP8266 firmware update"); - webServer.setContentLength(CONTENT_LENGTH_UNKNOWN); - webServer.send(200, "text/html", httpHeader); - webServer.sendContent_P(HTTP_SCRIPT); - webServer.sendContent_P(HTTP_STYLE); - webServer.sendContent_P(HASP_STYLE); - webServer.sendContent(F("")); - webServer.sendContent_P(HTTP_HEAD_END); - webServer.sendContent(F("

")); - webServer.sendContent(haspNode); - webServer.sendContent(F(" ESP8266 firmware update

")); - webServer.sendContent(F("
Updating ESP firmware from: ")); - webServer.sendContent(webServer.arg("espFirmware")); - webServer.sendContent_P(HTTP_END); - webServer.sendContent(""); - - debugPrintln("ESPFW: Attempting ESP firmware update from: " + String(webServer.arg("espFirmware"))); - espStartOta(webServer.arg("espFirmware")); -} - -//////////////////////////////////////////////////////////////////////////////////////////////////// -void webHandleLcdUpload() -{ // http://plate01/lcdupload - // Upload firmware to the Nextion LCD via HTTP upload - - if (configPassword[0] != '\0') - { //Request HTTP auth if configPassword is set - if (!webServer.authenticate(configUser, configPassword)) - { - return webServer.requestAuthentication(); - } - } - - static uint32_t lcdOtaTransferred = 0; - static uint32_t lcdOtaRemaining; - static uint16_t lcdOtaParts; - const uint32_t lcdOtaTimeout = 30000; // timeout for receiving new data in milliseconds - static uint32_t lcdOtaTimer = 0; // timer for upload timeout - - HTTPUpload &upload = webServer.upload(); - - if (tftFileSize == 0) - { - debugPrintln(String(F("LCDOTA: FAILED, no filesize sent."))); - String httpHeader = FPSTR(HTTP_HEAD_START); - httpHeader.replace("{v}", "HASPone " + String(haspNode) + " LCD update error"); - webServer.setContentLength(CONTENT_LENGTH_UNKNOWN); - webServer.send(200, "text/html", httpHeader); - webServer.sendContent_P(HTTP_SCRIPT); - webServer.sendContent_P(HTTP_STYLE); - webServer.sendContent_P(HASP_STYLE); - webServer.sendContent(F("")); - webServer.sendContent_P(HTTP_HEAD_END); - webServer.sendContent(F("

")); - webServer.sendContent(haspNode); - webServer.sendContent(F(" LCD update FAILED

")); - webServer.sendContent(F("No update file size reported. You must use a modern browser with Javascript enabled.")); - webServer.sendContent_P(HTTP_END); - webServer.sendContent(""); - } - else if ((lcdOtaTimer > 0) && ((millis() - lcdOtaTimer) > lcdOtaTimeout)) - { // Our timer expired so reset - debugPrintln(F("LCDOTA: ERROR: LCD upload timeout. Restarting.")); - espReset(); - } - else if (upload.status == UPLOAD_FILE_START) - { - WiFiUDP::stopAll(); // Keep mDNS responder from breaking things - - debugPrintln(String(F("LCDOTA: Attempting firmware upload"))); - debugPrintln(String(F("LCDOTA: upload.filename: ")) + String(upload.filename)); - debugPrintln(String(F("LCDOTA: TFTfileSize: ")) + String(tftFileSize)); - - lcdOtaRemaining = tftFileSize; - lcdOtaParts = (lcdOtaRemaining / 4096) + 1; - debugPrintln(String(F("LCDOTA: File upload beginning. Size ")) + String(lcdOtaRemaining) + String(F(" bytes in ")) + String(lcdOtaParts) + String(F(" 4k chunks."))); - - Serial1.write(nextionSuffix, sizeof(nextionSuffix)); // Send empty command to LCD - Serial1.flush(); - nextionHandleInput(); - - String lcdOtaNextionCmd = "whmi-wri " + String(tftFileSize) + "," + String(nextionBaud) + ",0"; - debugPrintln(String(F("LCDOTA: Sending LCD upload command: ")) + lcdOtaNextionCmd); - Serial1.print(lcdOtaNextionCmd); - Serial1.write(nextionSuffix, sizeof(nextionSuffix)); - Serial1.flush(); - - if (nextionOtaResponse()) - { - debugPrintln(F("LCDOTA: LCD upload command accepted")); - } - else - { - debugPrintln(F("LCDOTA: LCD upload command FAILED.")); - espReset(); - } - lcdOtaTimer = millis(); - } - else if (upload.status == UPLOAD_FILE_WRITE) - { // Handle upload data - static int lcdOtaChunkCounter = 0; - static uint16_t lcdOtaPartNum = 0; - static int lcdOtaPercentComplete = 0; - static const uint16_t lcdOtaBufferSize = 1024; // upload data buffer before sending to UART - static uint8_t lcdOtaBuffer[lcdOtaBufferSize] = {}; - uint16_t lcdOtaUploadIndex = 0; - int32_t lcdOtaPacketRemaining = upload.currentSize; - - while (lcdOtaPacketRemaining > 0) - { // Write incoming data to panel as it arrives - // determine chunk size as lowest value of lcdOtaPacketRemaining, lcdOtaBufferSize, or 4096 - lcdOtaChunkCounter - uint16_t lcdOtaChunkSize = 0; - if ((lcdOtaPacketRemaining <= lcdOtaBufferSize) && (lcdOtaPacketRemaining <= (4096 - lcdOtaChunkCounter))) - { - lcdOtaChunkSize = lcdOtaPacketRemaining; - } - else if ((lcdOtaBufferSize <= lcdOtaPacketRemaining) && (lcdOtaBufferSize <= (4096 - lcdOtaChunkCounter))) - { - lcdOtaChunkSize = lcdOtaBufferSize; - } - else - { - lcdOtaChunkSize = 4096 - lcdOtaChunkCounter; - } - - for (uint16_t i = 0; i < lcdOtaChunkSize; i++) - { // Load up the UART buffer - lcdOtaBuffer[i] = upload.buf[lcdOtaUploadIndex]; - lcdOtaUploadIndex++; - } - Serial1.flush(); // Clear out current UART buffer - Serial1.write(lcdOtaBuffer, lcdOtaChunkSize); // And send the most recent data - lcdOtaChunkCounter += lcdOtaChunkSize; - lcdOtaTransferred += lcdOtaChunkSize; - if (lcdOtaChunkCounter >= 4096) - { - Serial1.flush(); - lcdOtaPartNum++; - lcdOtaPercentComplete = (lcdOtaTransferred * 100) / tftFileSize; - lcdOtaChunkCounter = 0; - if (nextionOtaResponse()) - { - debugPrintln(String(F("LCDOTA: Part ")) + String(lcdOtaPartNum) + String(F(" OK, ")) + String(lcdOtaPercentComplete) + String(F("% complete"))); - } - else - { - debugPrintln(String(F("LCDOTA: Part ")) + String(lcdOtaPartNum) + String(F(" FAILED, ")) + String(lcdOtaPercentComplete) + String(F("% complete"))); - } - } - else - { - delay(10); - } - if (lcdOtaRemaining > 0) - { - lcdOtaRemaining -= lcdOtaChunkSize; - } - if (lcdOtaPacketRemaining > 0) - { - lcdOtaPacketRemaining -= lcdOtaChunkSize; - } - } - - if (lcdOtaTransferred >= tftFileSize) - { - if (nextionOtaResponse()) - { - debugPrintln(String(F("LCDOTA: Success, wrote ")) + String(lcdOtaTransferred) + " of " + String(tftFileSize) + " bytes."); - webServer.sendHeader("Location", "/lcdOtaSuccess"); - webServer.send(303); - uint32_t lcdOtaDelay = millis(); - while ((millis() - lcdOtaDelay) < 5000) - { // extra 5sec delay while the LCD handles any local firmware updates from new versions of code sent to it - webServer.handleClient(); - delay(1); - } - espReset(); - } - else - { - debugPrintln(F("LCDOTA: Failure")); - webServer.sendHeader("Location", "/lcdOtaFailure"); - webServer.send(303); - uint32_t lcdOtaDelay = millis(); - while ((millis() - lcdOtaDelay) < 1000) - { // extra 1sec delay for client to grab failure page - webServer.handleClient(); - delay(1); - } - espReset(); - } - } - lcdOtaTimer = millis(); - } - else if (upload.status == UPLOAD_FILE_END) - { // Upload completed - if (lcdOtaTransferred >= tftFileSize) - { - if (nextionOtaResponse()) - { // YAY WE DID IT - debugPrintln(String(F("LCDOTA: Success, wrote ")) + String(lcdOtaTransferred) + " of " + String(tftFileSize) + " bytes."); - webServer.sendHeader("Location", "/lcdOtaSuccess"); - webServer.send(303); - uint32_t lcdOtaDelay = millis(); - while ((millis() - lcdOtaDelay) < 5000) - { // extra 5sec delay while the LCD handles any local firmware updates from new versions of code sent to it - webServer.handleClient(); - yield(); - } - espReset(); - } - else - { - debugPrintln(F("LCDOTA: Failure")); - webServer.sendHeader("Location", "/lcdOtaFailure"); - webServer.send(303); - uint32_t lcdOtaDelay = millis(); - while ((millis() - lcdOtaDelay) < 1000) - { // extra 1sec delay for client to grab failure page - webServer.handleClient(); - yield(); - } - espReset(); - } - } - } - else if (upload.status == UPLOAD_FILE_ABORTED) - { // Something went kablooey - debugPrintln(F("LCDOTA: ERROR: upload.status returned: UPLOAD_FILE_ABORTED")); - debugPrintln(F("LCDOTA: Failure")); - webServer.sendHeader("Location", "/lcdOtaFailure"); - webServer.send(303); - uint32_t lcdOtaDelay = millis(); - while ((millis() - lcdOtaDelay) < 1000) - { // extra 1sec delay for client to grab failure page - webServer.handleClient(); - yield(); - } - espReset(); - } - else - { // Something went weird, we should never get here... - debugPrintln(String(F("LCDOTA: upload.status returned: ")) + String(upload.status)); - debugPrintln(F("LCDOTA: Failure")); - webServer.sendHeader("Location", "/lcdOtaFailure"); - webServer.send(303); - uint32_t lcdOtaDelay = millis(); - while ((millis() - lcdOtaDelay) < 1000) - { // extra 1sec delay for client to grab failure page - webServer.handleClient(); - yield(); - } - espReset(); - } -} - -//////////////////////////////////////////////////////////////////////////////////////////////////// -void webHandleLcdUpdateSuccess() -{ // http://plate01/lcdOtaSuccess - if (configPassword[0] != '\0') - { //Request HTTP auth if configPassword is set - if (!webServer.authenticate(configUser, configPassword)) - { - return webServer.requestAuthentication(); - } - } - debugPrintln(String(F("HTTP: Sending /lcdOtaSuccess page to client connected from: ")) + webServer.client().remoteIP().toString()); - String httpHeader = FPSTR(HTTP_HEAD_START); - httpHeader.replace("{v}", "HASPone " + String(haspNode) + " LCD firmware update success"); - webServer.setContentLength(CONTENT_LENGTH_UNKNOWN); - webServer.send(200, "text/html", httpHeader); - webServer.sendContent_P(HTTP_SCRIPT); - webServer.sendContent_P(HTTP_STYLE); - webServer.sendContent_P(HASP_STYLE); - webServer.sendContent(F("")); - webServer.sendContent_P(HTTP_HEAD_END); - webServer.sendContent(F("

")); - webServer.sendContent(haspNode); - webServer.sendContent(F(" LCD update success

")); - webServer.sendContent(F("Restarting HASwitchPlate to apply changes...")); - webServer.sendContent_P(HTTP_END); - webServer.sendContent(""); -} - -//////////////////////////////////////////////////////////////////////////////////////////////////// -void webHandleLcdUpdateFailure() -{ // http://plate01/lcdOtaFailure - if (configPassword[0] != '\0') - { //Request HTTP auth if configPassword is set - if (!webServer.authenticate(configUser, configPassword)) - { - return webServer.requestAuthentication(); - } - } - debugPrintln(String(F("HTTP: Sending /lcdOtaFailure page to client connected from: ")) + webServer.client().remoteIP().toString()); - String httpHeader = FPSTR(HTTP_HEAD_START); - httpHeader.replace("{v}", "HASPone " + String(haspNode) + " LCD firmware update failed"); - webServer.setContentLength(CONTENT_LENGTH_UNKNOWN); - webServer.send(200, "text/html", httpHeader); - webServer.sendContent_P(HTTP_SCRIPT); - webServer.sendContent_P(HTTP_STYLE); - webServer.sendContent_P(HASP_STYLE); - webServer.sendContent(F("")); - webServer.sendContent_P(HTTP_HEAD_END); - webServer.sendContent(F("

")); - webServer.sendContent(haspNode); - webServer.sendContent(F(" LCD update failed :(

")); - webServer.sendContent(F("Restarting HASwitchPlate to apply changes...")); - webServer.sendContent_P(HTTP_END); - webServer.sendContent(""); -} - -//////////////////////////////////////////////////////////////////////////////////////////////////// -void webHandleLcdDownload() -{ // http://plate01/lcddownload - if (configPassword[0] != '\0') - { //Request HTTP auth if configPassword is set - if (!webServer.authenticate(configUser, configPassword)) - { - return webServer.requestAuthentication(); - } - } - debugPrintln(String(F("HTTP: Sending /lcddownload page to client connected from: ")) + webServer.client().remoteIP().toString()); - String httpHeader = FPSTR(HTTP_HEAD_START); - httpHeader.replace("{v}", "HASPone " + String(haspNode) + " LCD firmware update"); - webServer.setContentLength(CONTENT_LENGTH_UNKNOWN); - webServer.send(200, "text/html", httpHeader); - webServer.sendContent_P(HTTP_SCRIPT); - webServer.sendContent_P(HTTP_STYLE); - webServer.sendContent_P(HASP_STYLE); - webServer.sendContent_P(HTTP_HEAD_END); - webServer.sendContent(F("

")); - webServer.sendContent(haspNode); - webServer.sendContent(F(" LCD update

")); - webServer.sendContent(F("
Updating LCD firmware from: ")); - webServer.sendContent(webServer.arg("lcdFirmware")); - webServer.sendContent_P(HTTP_END); - webServer.sendContent(""); - nextionOtaStartDownload(webServer.arg("lcdFirmware")); -} - -//////////////////////////////////////////////////////////////////////////////////////////////////// -void webHandleTftFileSize() -{ // http://plate01/tftFileSize - if (configPassword[0] != '\0') - { //Request HTTP auth if configPassword is set - if (!webServer.authenticate(configUser, configPassword)) - { - return webServer.requestAuthentication(); - } - } - debugPrintln(String(F("HTTP: Sending /tftFileSize page to client connected from: ")) + webServer.client().remoteIP().toString()); - String httpHeader = FPSTR(HTTP_HEAD_START); - httpHeader.replace("{v}", "HASPone " + String(haspNode) + " TFT Filesize"); - webServer.setContentLength(CONTENT_LENGTH_UNKNOWN); - webServer.send(200, "text/html", httpHeader); - webServer.sendContent_P(HTTP_HEAD_END); - tftFileSize = webServer.arg("tftFileSize").toInt(); - debugPrintln(String(F("WEB: Received tftFileSize: ")) + String(tftFileSize)); -} - -//////////////////////////////////////////////////////////////////////////////////////////////////// -void webHandleReboot() -{ // http://plate01/reboot - if (configPassword[0] != '\0') - { //Request HTTP auth if configPassword is set - if (!webServer.authenticate(configUser, configPassword)) - { - return webServer.requestAuthentication(); - } - } - debugPrintln(String(F("HTTP: Sending /reboot page to client connected from: ")) + webServer.client().remoteIP().toString()); - String httpHeader = FPSTR(HTTP_HEAD_START); - httpHeader.replace("{v}", "HASPone " + String(haspNode) + " reboot"); - webServer.setContentLength(CONTENT_LENGTH_UNKNOWN); - webServer.send(200, "text/html", httpHeader); - webServer.sendContent_P(HTTP_SCRIPT); - webServer.sendContent_P(HTTP_STYLE); - webServer.sendContent_P(HASP_STYLE); - webServer.sendContent(F("")); - webServer.sendContent_P(HTTP_HEAD_END); - webServer.sendContent(F("

")); - webServer.sendContent(haspNode); - webServer.sendContent(F(" Reboot

")); - webServer.sendContent(F("
Rebooting device")); - webServer.sendContent_P(HTTP_END); - webServer.sendContent(""); - nextionSendCmd("page 0"); - nextionSetAttr("p[0].b[1].txt", "\"Rebooting...\""); - espReset(); -} - -//////////////////////////////////////////////////////////////////////////////////////////////////// -bool updateCheck() -{ // firmware update check - WiFiClientSecure wifiUpdateClientSecure; - HTTPClient updateClient; - debugPrintln(String(F("UPDATE: Checking update URL: ")) + FPSTR(UPDATE_URL)); - - wifiUpdateClientSecure.setInsecure(); - wifiUpdateClientSecure.setBufferSizes(512, 512); - updateClient.begin(wifiUpdateClientSecure, UPDATE_URL); - - int httpCode = updateClient.GET(); // start connection and send HTTP header - if (httpCode != HTTP_CODE_OK) - { - debugPrintln(String(F("UPDATE: Update check failed: ")) + updateClient.errorToString(httpCode)); - return false; - } - - DynamicJsonDocument updateJson(2048); - DeserializationError jsonError = deserializeJson(updateJson, updateClient.getString()); - updateClient.end(); - - if (jsonError) - { // Couldn't parse the returned JSON, so bail - debugPrintln(String(F("UPDATE: JSON parsing failed: ")) + String(jsonError.c_str())); - mqttClient.publish(mqttStateJSONTopic, String(F("{\"event\":\"jsonError\",\"event_source\":\"updateCheck()\",\"event_description\":\"Failed to parse incoming JSON command with error: ")) + String(jsonError.c_str()) + String(F("\"}"))); - return false; - } - else - { - if (!updateJson["d1_mini"]["version"].isNull()) - { - updateEspAvailableVersion = updateJson["d1_mini"]["version"].as(); - debugPrintln(String(F("UPDATE: updateEspAvailableVersion: ")) + String(updateEspAvailableVersion)); - espFirmwareUrl = updateJson["d1_mini"]["firmware"].as(); - if (updateEspAvailableVersion > haspVersion) - { - updateEspAvailable = true; - debugPrintln(String(F("UPDATE: New ESP version available: ")) + String(updateEspAvailableVersion)); - } - } - if (nextionModel && !updateJson[nextionModel]["version"].isNull()) - { - updateLcdAvailableVersion = updateJson[nextionModel]["version"].as(); - debugPrintln(String(F("UPDATE: updateLcdAvailableVersion: ")) + String(updateLcdAvailableVersion)); - lcdFirmwareUrl = updateJson[nextionModel]["firmware"].as(); - if (updateLcdAvailableVersion > lcdVersion) - { - updateLcdAvailable = true; - debugPrintln(String(F("UPDATE: New LCD version available: ")) + String(updateLcdAvailableVersion)); - } - } - debugPrintln(F("UPDATE: Update check completed")); - } - return true; -} - -//////////////////////////////////////////////////////////////////////////////////////////////////// -void motionSetup() -{ - if (strcmp(motionPinConfig, "D0") == 0) - { - motionEnabled = true; - motionPin = D0; - pinMode(motionPin, INPUT); - } - else if (strcmp(motionPinConfig, "D1") == 0) - { - motionEnabled = true; - motionPin = D1; - pinMode(motionPin, INPUT); - } -} - -//////////////////////////////////////////////////////////////////////////////////////////////////// -void motionHandle() -{ // Monitor motion sensor - if (motionEnabled) - { // Check on our motion sensor - static unsigned long motionLatchTimer = 0; // Timer for motion sensor latch - static unsigned long motionBufferTimer = millis(); // Timer for motion sensor buffer - static bool motionActiveBuffer = motionActive; - bool motionRead = digitalRead(motionPin); - - if (motionRead != motionActiveBuffer) - { // if we've changed state - motionBufferTimer = millis(); - motionActiveBuffer = motionRead; - } - else if (millis() > (motionBufferTimer + motionBufferTimeout)) - { - if ((motionActiveBuffer && !motionActive) && (millis() > (motionLatchTimer + motionLatchTimeout))) - { - motionLatchTimer = millis(); - mqttClient.publish(mqttMotionStateTopic, "ON"); - debugPrintln(String(F("MQTT OUT: '")) + mqttMotionStateTopic + String(F("' : 'ON'"))); - motionActive = motionActiveBuffer; - debugPrintln("MOTION: Active"); - } - else if ((!motionActiveBuffer && motionActive) && (millis() > (motionLatchTimer + motionLatchTimeout))) - { - motionLatchTimer = millis(); - mqttClient.publish(mqttMotionStateTopic, "OFF"); - debugPrintln(String(F("MQTT OUT: '")) + mqttMotionStateTopic + String(F("' : 'OFF'"))); - motionActive = motionActiveBuffer; - debugPrintln("MOTION: Inactive"); - } - } - } -} - -//////////////////////////////////////////////////////////////////////////////////////////////////// -void beepHandle() -{ // Handle beep/tactile feedback - if (beepEnabled) - { - static bool beepState = false; // beep currently engaged - static unsigned long beepPrevMillis = 0; // store last time beep was updated - if ((beepState == true) && (millis() - beepPrevMillis >= beepOnTime) && ((beepCounter > 0))) - { - beepState = false; // Turn it off - beepPrevMillis = millis(); // Remember the time - analogWrite(beepPin, 254); // start beep for beepOnTime - if (beepCounter > 0) - { // Update the beep counter. - beepCounter--; - } - } - else if ((beepState == false) && (millis() - beepPrevMillis >= beepOffTime) && ((beepCounter >= 0))) - { - beepState = true; // turn it on - beepPrevMillis = millis(); // Remember the time - analogWrite(beepPin, 0); // stop beep for beepOffTime - } - } -} - -//////////////////////////////////////////////////////////////////////////////////////////////////// -void telnetHandleClient() -{ // Basic telnet client handling code from: https://gist.github.com/tablatronix/4793677ca748f5f584c95ec4a2b10303 - if (debugTelnetEnabled) - { // Only do any of this if we're actually enabled - static unsigned long telnetInputIndex = 0; - if (telnetServer.hasClient()) - { // client is connected - if (!telnetClient || !telnetClient.connected()) - { - if (telnetClient) - { - telnetClient.stop(); // client disconnected - } - telnetClient = telnetServer.available(); // ready for new client - telnetInputIndex = 0; // reset input buffer index - } - else - { - telnetServer.available().stop(); // have client, block new connections - } - } - // Handle client input from telnet connection. - if (telnetClient && telnetClient.connected() && telnetClient.available()) - { // client input processing - static char telnetInputBuffer[telnetInputMax]; - - if (telnetClient.available()) - { - char telnetInputByte = telnetClient.read(); // Read client byte - if (telnetInputByte == 5) - { // If the telnet client sent a bunch of control commands on connection (which end in ENQUIRY/0x05), ignore them and restart the buffer - telnetInputIndex = 0; - } - else if (telnetInputByte == 13) - { // telnet line endings should be CRLF: https://tools.ietf.org/html/rfc5198#appendix-C - // If we get a CR just ignore it - } - else if (telnetInputByte == 10) - { // We've caught a LF (DEC 10), send buffer contents to the Nextion - telnetInputBuffer[telnetInputIndex] = 0; // null terminate our char array - nextionSendCmd(String(telnetInputBuffer)); - telnetInputIndex = 0; - } - else if (telnetInputIndex < telnetInputMax) - { // If we have room left in our buffer add the current byte - telnetInputBuffer[telnetInputIndex] = telnetInputByte; - telnetInputIndex++; - } - } - } - } -} - -//////////////////////////////////////////////////////////////////////////////////////////////////// -void debugPrintln(const String &debugText) -{ // Debug output line of text to our debug targets - const String debugTimeText = "[+" + String(float(millis()) / 1000, 3) + "s] "; - if (debugSerialEnabled) - { - Serial.print(debugTimeText); - Serial.println(debugText); - SoftwareSerial debugSerial(-1, 1); // -1==nc for RX, 1==TX pin - debugSerial.begin(debugSerialBaud); - debugSerial.print(debugTimeText); - debugSerial.println(debugText); - debugSerial.flush(); - } - if (debugTelnetEnabled) - { - if (telnetClient.connected()) - { - telnetClient.print(debugTimeText); - telnetClient.println(debugText); - } - } -} - -//////////////////////////////////////////////////////////////////////////////////////////////////// -void debugPrint(const String &debugText) -{ // Debug output a string to our debug targets. - // Try to avoid using this function if at all possible. When connected to telnet, printing each - // character requires a full TCP round-trip + acknowledgement back and execution halts while this - // happens. Far better to put everything into a line and send it all out in one packet using - // debugPrintln. - if (debugSerialEnabled) - Serial.print(debugText); - { - SoftwareSerial debugSerial(-1, 1); // -1==nc for RX, 1==TX pin - debugSerial.begin(debugSerialBaud); - debugSerial.print(debugText); - debugSerial.flush(); - } - if (debugTelnetEnabled) - { - if (telnetClient.connected()) - { - telnetClient.print(debugText); - } - } -} - -//////////////////////////////////////////////////////////////////////////////////////////////////// -void debugPrintCrash() -{ // Debug output line of text to our debug targets - SoftwareSerial debugSerial(-1, 1); // -1==nc for RX, 1==TX pin - debugSerial.begin(debugSerialBaud); - SaveCrash.print(debugSerial); - SaveCrash.clear(); -} - -//////////////////////////////////////////////////////////////////////////////////////////////////// -void debugPrintFile(const String &fileName) -{ // Debug output line of text to our debug targets - File debugFile = SPIFFS.open(fileName, "r"); - if (debugFile) - { - uint16_t lineCount = 1; - while (debugFile.available()) - { - debugPrintln(F("SPIFFS: file:") + fileName + F(" line:") + String(lineCount) + F(" data:") + debugFile.readStringUntil('\n')); - lineCount++; - } - debugFile.close(); - } - else - { - debugPrintln("SPIFFS: Error opening file for read: " + fileName); - } -} - -//////////////////////////////////////////////////////////////////////////////////////////////////// -// Submitted by benmprojects to handle "beep" commands. Split -// incoming String by separator, return selected field as String -// Original source: https://arduino.stackexchange.com/a/1237 -String getSubtringField(String data, char separator, int index) -{ - int found = 0; - int strIndex[] = {0, -1}; - int maxIndex = data.length(); - - for (int i = 0; i <= maxIndex && found <= index; i++) - { - if (data.charAt(i) == separator || i == maxIndex) - { - found++; - strIndex[0] = strIndex[1] + 1; - strIndex[1] = (i == maxIndex) ? i + 1 : i; - } - } - return found > index ? data.substring(strIndex[0], strIndex[1]) : ""; -} - -//////////////////////////////////////////////////////////////////////////////// -String printHex8(byte *data, uint8_t length) -{ // returns input bytes as printable hex values in the format 0x01 0x23 0xFF - - String hex8String; - for (int i = 0; i < length; i++) - { - hex8String += "0x"; - if (data[i] < 0x10) - { - hex8String += "0"; - } - hex8String += String(data[i], HEX); - if (i != (length - 1)) - { - hex8String += " "; - } - } - // hex8String.toUpperCase(); - return hex8String; -} +//////////////////////////////////////////////////////////////////////////////////////////////////// +// _____ _____ _____ _____ +// | | | _ | __| _ | +// | | |__ | __| +// |__|__|__|__|_____|__| +// Home Automation Switch Plate +// https://github.com/aderusha/HASwitchPlate +// +// Copyright (c) 2024 Allen Derusha allen@derusha.org +// +// MIT License +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of this hardware, +// software, and associated documentation files (the "Product"), to deal in the Product without +// restriction, including without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Product, and to permit persons to whom the +// Product is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all copies or +// substantial portions of the Product. +// +// THE PRODUCT IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT +// NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE PRODUCT OR THE USE OR OTHER DEALINGS IN THE PRODUCT. +//////////////////////////////////////////////////////////////////////////////////////////////////// + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +//////////////////////////////////////////////////////////////////////////////////////////////////// +// These defaults may be overwritten with values saved by the web interface +char wifiSSID[32] = ""; +char wifiPass[64] = ""; +char mqttServer[128] = ""; +char mqttPort[6] = "1883"; +char mqttUser[128] = ""; +char mqttPassword[128] = ""; +char mqttFingerprint[60] = ""; +char haspNode[16] = "plate01"; +char groupName[16] = "plates"; +char hassDiscovery[128] = "homeassistant"; +char configUser[32] = "admin"; +char configPassword[32] = ""; +char motionPinConfig[3] = "0"; +char nextionBaud[7] = "115200"; + +//////////////////////////////////////////////////////////////////////////////////////////////////// +const float haspVersion = 1.06; // Current HASPone software release version +const uint16_t mqttMaxPacketSize = 2048; // Size of buffer for incoming MQTT message +byte nextionReturnBuffer[128]; // Byte array to pass around data coming from the panel +uint8_t nextionReturnIndex = 0; // Index for nextionReturnBuffer +int8_t nextionActivePage = -1; // Track active LCD page +bool lcdConnected = false; // Set to true when we've heard something from the LCD +const char wifiConfigPass[9] = "hasplate"; // First-time config WPA2 password +const char wifiConfigAP[14] = "HASwitchPlate"; // First-time config SSID +bool shouldSaveConfig = false; // Flag to save json config to SPIFFS +bool nextionReportPage0 = false; // If false, don't report page 0 sendme +const unsigned long updateCheckInterval = 43200000; // Time in msec between update checks (12 hours) +unsigned long updateCheckTimer = updateCheckInterval; // Timer for update check +unsigned long updateCheckFirstRun = 30000; // First-run check offset +bool updateEspAvailable = false; // Flag for update check to report new ESP FW version +float updateEspAvailableVersion; // Float to hold the new ESP FW version number +bool updateLcdAvailable = false; // Flag for update check to report new LCD FW version +unsigned long debugTimer = 0; // Clock for debug performance profiling +bool debugSerialEnabled = true; // Enable USB serial debug output +const unsigned long debugSerialBaud = 115200; // Desired baud rate for serial debug output +bool debugTelnetEnabled = false; // Enable telnet debug output +bool nextionBufferOverrun = false; // Set to true if an overrun error was encountered +bool nextionAckEnable = false; // Wait for each Nextion command to be acked before continuing +bool nextionAckReceived = false; // Ack was received +bool rebootOnp0b1 = false; // When true, reboot device on button press of p[0].b[1] +bool rebootOnLongPress = true; // When true, reboot device on long press of any button +unsigned long rebootOnLongPressTimer = 0; // Clock for long press reboot timer +unsigned long rebootOnLongPressTimeout = 10000; // Timeout value for long press reboot timer +const unsigned long nextionAckTimeout = 1000; // Timeout to wait for an ack before throwing error +unsigned long nextionAckTimer = 0; // Timer to track Nextion ack +const unsigned long telnetInputMax = 128; // Size of user input buffer for user telnet session +bool motionEnabled = false; // Motion sensor is enabled +bool mdnsEnabled = true; // mDNS enabled +bool ignoreTouchWhenOff = false; // Ignore touch events when backlight is off and instead send mqtt msg +bool beepEnabled = false; // Keypress beep enabled +unsigned long beepOnTime = 1000; // milliseconds of on-time for beep +unsigned long beepOffTime = 1000; // milliseconds of off-time for beep +unsigned int beepCounter; // Count the number of beeps +uint8_t beepPin = D2; // define beep pin output +uint8_t motionPin = 0; // GPIO input pin for motion sensor if connected and enabled +bool motionActive = false; // Motion is being detected +const unsigned long motionLatchTimeout = 1000; // Latch time for motion sensor +const unsigned long motionBufferTimeout = 100; // Trigger threshold time for motion sensor +unsigned long lcdVersion = 0; // Int to hold current LCD FW version number +unsigned long updateLcdAvailableVersion; // Int to hold the new LCD FW version number +bool lcdVersionQueryFlag = false; // Flag to set if we've queried lcdVersion +const String lcdVersionQuery = "p[0].b[2].val"; // Object ID for lcdVersion in HMI +uint8_t lcdBacklightDim = 0; // Backlight dimmer value +bool lcdBacklightOn = 0; // Backlight on/off +bool lcdBacklightQueryFlag = false; // Flag to set if we've queried lcdBacklightDim +bool startupCompleteFlag = false; // Startup process has completed +const unsigned long statusUpdateInterval = 300000; // Time in msec between publishing MQTT status updates (5 minutes) +unsigned long statusUpdateTimer = 0; // Timer for status update +const unsigned long connectTimeout = 300; // Timeout for WiFi and MQTT connection attempts in seconds +const unsigned long reConnectTimeout = 60; // Timeout for WiFi reconnection attempts in seconds +byte espMac[6]; // Byte array to store our MAC address +bool mqttTlsEnabled = false; // Enable MQTT client TLS connections +bool mqttPingCheck = false; // MQTT broker ping check result +bool mqttPortCheck = false; // MQTT broke port check result +String mqttClientId; // Auto-generated MQTT ClientID +String mqttGetSubtopic; // MQTT subtopic for incoming commands requesting .val +String mqttStateTopic; // MQTT topic for outgoing panel interactions +String mqttStateJSONTopic; // MQTT topic for outgoing panel interactions in JSON format +String mqttCommandTopic; // MQTT topic for incoming panel commands +String mqttGroupCommandTopic; // MQTT topic for incoming group panel commands +String mqttStatusTopic; // MQTT topic for publishing device connectivity state +String mqttSensorTopic; // MQTT topic for publishing device information in JSON format +String mqttLightCommandTopic; // MQTT topic for incoming panel backlight on/off commands +String mqttLightStateTopic; // MQTT topic for outgoing panel backlight on/off state +String mqttLightBrightCommandTopic; // MQTT topic for incoming panel backlight dimmer commands +String mqttLightBrightStateTopic; // MQTT topic for outgoing panel backlight dimmer state +String mqttMotionStateTopic; // MQTT topic for outgoing motion sensor state +String nextionModel; // Record reported model number of LCD panel +const byte nextionSuffix[] = {0xFF, 0xFF, 0xFF}; // Standard suffix for Nextion commands +uint8_t nextionMaxPages = 11; // Maximum number of pages in Nextion project +uint32_t tftFileSize = 0; // Filesize for TFT firmware upload +const uint8_t nextionResetPin = D6; // Pin for Nextion power rail switch (GPIO12/D6) +const unsigned long nextionSpeeds[] = {2400, + 4800, + 9600, + 19200, + 31250, + 38400, + 57600, + 115200, + 230400, + 250000, + 256000, + 512000, + 921600}; // Valid serial speeds for Nextion communication +const uint8_t nextionSpeedsLength = sizeof(nextionSpeeds) / sizeof(nextionSpeeds[0]); // Size of our list of speeds + +WiFiClientSecure mqttClientSecure; // TLS-enabled WiFiClient for MQTT +WiFiClient wifiClient; // Standard WiFiClient +MQTTClient mqttClient(mqttMaxPacketSize); // MQTT client +ESP8266WebServer webServer(80); // Admin web server on port 80 +ESP8266HTTPUpdateServer httpOTAUpdate; // Arduino OTA server +WiFiServer telnetServer(23); // Telnet server (if enabled) +WiFiClient telnetClient; // Telnet client +MDNSResponder::hMDNSService hMDNSService; // mDNS +EspSaveCrash SaveCrash; // Save crash details to flash + +// URL for auto-update check of "version.json" +const char UPDATE_URL[] PROGMEM = "https://haswitchplate.com/update/version.json"; +// Additional CSS style to match Hass theme +const char HASP_STYLE[] PROGMEM = ""; +// Default link to compiled Arduino firmware image +String espFirmwareUrl = "https://haswitchplate.com/update/HASwitchPlate.ino.d1_mini.bin"; +// Default link to compiled Nextion firmware images +String lcdFirmwareUrl = "https://haswitchplate.com/update/HASwitchPlate.tft"; + +void setup(); +void loop(); +void mqttConnect(); +void mqttProcessInput(String &strTopic, String &strPayload); +void mqttStatusUpdate(); +void mqttDiscovery(); +void nextionHandleInput(); +void nextionProcessInput(); +void nextionSendCmd(const String &nextionCmd); +void nextionSetAttr(const String &hmiAttribute, const String &hmiValue); +void nextionGetAttr(const String &hmiAttribute); +void nextionParseJson(const String &strPayload); +void nextionOtaStartDownload(const String &lcdOtaUrl); +bool nextionOtaResponse(); +bool nextionConnect(); +void nextionSetSpeed(); +void nextionReset(); +void nextionUpdateProgress(const unsigned int &progress, const unsigned int &total); +void espWifiConnect(); +void espWifiReconnect(); +void espSetupOta(); +void espStartOta(const String &espOtaUrl); +void espReset(); +void configRead(); +void configSaveCallback(); +void configSave(); +void configClearSaved(); +void webHandleNotFound(); +void webHandleRoot(); +void webHandleSaveConfig(); +void webHandleResetConfig(); +void webHandleShowConfig(); +void webHandleResetBacklight(); +void webHandleFirmware(); +void webHandleEspFirmware(); +void webHandleLcdUpload(); +void webHandleLcdUpdateSuccess(); +void webHandleLcdUpdateFailure(); +void webHandleLcdDownload(); +void webHandleTftFileSize(); +void webHandleReboot(); +void espWifiConfigCallback(WiFiManager *myWiFiManager); +bool updateCheck(); +void motionSetup(); +void motionHandle(); +void beepHandle(); +void telnetHandleClient(); +void debugPrintln(const String &debugText); +void debugPrint(const String &debugText); +void debugPrintCrash(); +void debugPrintFile(const String &fileName); +String getSubtringField(String data, char separator, int index); +String printHex8(byte *data, uint8_t length); + +//////////////////////////////////////////////////////////////////////////////////////////////////// +void setup() +{ // System setup + debugPrint(String(F("\n\n================================================================================\n"))); + debugPrintln(String(F("SYSTEM: Starting HASPone v")) + String(haspVersion)); + debugPrintln(String(F("SYSTEM: heapFree: ")) + String(ESP.getFreeHeap()) + String(F(" heapMaxFreeBlockSize: ")) + String(ESP.getMaxFreeBlockSize())); + debugPrintln(String(F("SYSTEM: Last reset reason: ")) + String(ESP.getResetInfo())); + if (SaveCrash.count()) + { + debugPrint(String(F("SYSTEM: Crashdump data discovered:"))); + debugPrintCrash(); + } + debugPrint(String(F("================================================================================\n\n"))); + + pinMode(nextionResetPin, OUTPUT); // Take control over the power switch for the LCD + digitalWrite(nextionResetPin, HIGH); // Power on the LCD + configRead(); // Check filesystem for a saved config.json + Serial.begin(atoi(nextionBaud)); // Serial - LCD RX (after swap), debug TX + Serial1.begin(atoi(nextionBaud)); // Serial1 - LCD TX, no RX + Serial.swap(); // Swap to allow hardware UART comms to LCD + + if (!nextionConnect()) + { + if (lcdConnected) + { + debugPrintln(F("HMI: LCD responding but initialization wasn't completed. Continuing program load anyway.")); + } + else + { + debugPrintln(F("HMI: LCD not responding, continuing program load")); + } + } + + espWifiConnect(); // Start up networking + + if ((configPassword[0] != '\0') && (configUser[0] != '\0')) + { // Start the webserver with our assigned password if it's been configured... + httpOTAUpdate.setup(&webServer, "/update", configUser, configPassword); + } + else + { // or without a password if not + httpOTAUpdate.setup(&webServer, "/update"); + } + + webServer.on("/", webHandleRoot); + webServer.on("/saveConfig", webHandleSaveConfig); + webServer.on("/resetConfig", webHandleResetConfig); + webServer.on("/resetBacklight", webHandleResetBacklight); + webServer.on("/firmware", webHandleFirmware); + webServer.on("/espfirmware", webHandleEspFirmware); + webServer.on( + "/lcdupload", HTTP_POST, []() + { webServer.send(200); }, + webHandleLcdUpload); + webServer.on("/tftFileSize", webHandleTftFileSize); + webServer.on("/lcddownload", webHandleLcdDownload); + webServer.on("/lcdOtaSuccess", webHandleLcdUpdateSuccess); + webServer.on("/lcdOtaFailure", webHandleLcdUpdateFailure); + webServer.on("/reboot", webHandleReboot); + webServer.onNotFound(webHandleNotFound); + webServer.begin(); + debugPrintln(String(F("HTTP: Server started @ http://")) + WiFi.localIP().toString()); + + espSetupOta(); // Start OTA firmware update + + motionSetup(); // Setup motion sensor if configured + + mqttConnect(); // Connect to MQTT + + if (mdnsEnabled) + { // Setup mDNS service discovery if enabled + hMDNSService = MDNS.addService(haspNode, "http", "tcp", 80); + if (debugTelnetEnabled) + { + MDNS.addService(haspNode, "telnet", "tcp", 23); + } + MDNS.addServiceTxt(hMDNSService, "app_name", "HASwitchPlate"); + MDNS.addServiceTxt(hMDNSService, "app_version", String(haspVersion).c_str()); + MDNS.update(); + } + + if (beepEnabled) + { // Setup beep/tactile output if configured + pinMode(beepPin, OUTPUT); + } + + if (debugTelnetEnabled) + { // Setup telnet server for remote debug output + telnetServer.setNoDelay(true); + telnetServer.begin(); + debugPrintln(String(F("TELNET: debug server enabled at telnet:")) + WiFi.localIP().toString()); + } + + debugPrintln(F("SYSTEM: System init complete.")); +} + +//////////////////////////////////////////////////////////////////////////////////////////////////// +void loop() +{ // Main execution loop + while ((WiFi.status() != WL_CONNECTED) || (WiFi.localIP().toString() == "0.0.0.0")) + { // Check WiFi is connected and that we have a valid IP, retry until we do. + if (WiFi.status() == WL_CONNECTED) + { // If we're currently connected, disconnect so we can try again + WiFi.disconnect(); + } + espWifiReconnect(); + } + + if (!mqttClient.connected()) + { // Check MQTT connection + debugPrintln(String(F("MQTT: not connected, connecting."))); + mqttConnect(); + } + nextionHandleInput(); // Nextion serial communications loop + mqttClient.loop(); // MQTT client loop + ArduinoOTA.handle(); // Arduino OTA loop + webServer.handleClient(); // webServer loop + telnetHandleClient(); // telnet client loop + motionHandle(); // motion sensor loop + beepHandle(); // beep feedback loop + + if (mdnsEnabled) + { + MDNS.update(); + } + + if ((millis() - statusUpdateTimer) >= statusUpdateInterval) + { // Run periodic status update + statusUpdateTimer = millis(); + mqttStatusUpdate(); + } + + if (((millis() - updateCheckTimer) >= updateCheckInterval) && (millis() > updateCheckFirstRun)) + { // Run periodic update check + updateCheckTimer = millis(); + if (updateCheck()) + { // Publish new status if updateCheck() worked and reset the timer + statusUpdateTimer = millis(); + mqttStatusUpdate(); + } + } +} + +//////////////////////////////////////////////////////////////////////////////////////////////////// +// Functions + +//////////////////////////////////////////////////////////////////////////////////////////////////// +void mqttConnect() +{ // MQTT connection and subscriptions + + static bool mqttFirstConnect = true; // For the first connection, we want to send an OFF/ON state to + // trigger any automations, but skip that if we reconnect while + // still running the sketch + rebootOnp0b1 = true; + static uint8_t mqttReconnectCount = 0; + unsigned long mqttConnectTimer = 0; + const unsigned long mqttConnectTimeout = 5000; + + // Check to see if we have a broker configured and notify the user if not + if (strcmp(mqttServer, "") == 0) + { + nextionSendCmd("page 0"); + nextionSetAttr("p[0].b[1].font", "6"); + nextionSetAttr("p[0].b[1].txt", "\"WiFi Connected!\\r " + String(WiFi.SSID()) + "\\rIP: " + WiFi.localIP().toString() + "\\r\\rConfigure MQTT:\\rhttp://" + WiFi.localIP().toString() + "\""); + while (strcmp(mqttServer, "") == 0) + { // Handle other stuff while we're waiting for MQTT to be configured + yield(); + nextionHandleInput(); // Nextion serial communications loop + ArduinoOTA.handle(); // Arduino OTA loop + webServer.handleClient(); // webServer loop + telnetHandleClient(); // telnet client loop + motionHandle(); // motion sensor loop + beepHandle(); // beep feedback loop + } + } + + if (mqttTlsEnabled) + { // Create MQTT service object with TLS connection + mqttClient.begin(mqttServer, atoi(mqttPort), mqttClientSecure); + if (strcmp(mqttFingerprint, "") == 0) + { + debugPrintln(String(F("MQTT: Configuring MQTT TLS connection without fingerprint validation."))); + mqttClientSecure.setInsecure(); + } + else + { + debugPrintln(String(F("MQTT: Configuring MQTT TLS connection with fingerprint validation."))); + mqttClientSecure.allowSelfSignedCerts(); + mqttClientSecure.setFingerprint(mqttFingerprint); + } + mqttClientSecure.setBufferSizes(512, 512); + } + else + { // Create MQTT service object without TLS connection + debugPrintln(String(F("MQTT: Configuring MQTT connection without TLS."))); + mqttClient.begin(mqttServer, atoi(mqttPort), wifiClient); + } + + mqttClient.onMessage(mqttProcessInput); // Setup MQTT callback function + + // MQTT topic string definitions + mqttStateTopic = "hasp/" + String(haspNode) + "/state"; + mqttStateJSONTopic = "hasp/" + String(haspNode) + "/state/json"; + mqttCommandTopic = "hasp/" + String(haspNode) + "/command"; + mqttGroupCommandTopic = "hasp/" + String(groupName) + "/command"; + mqttStatusTopic = "hasp/" + String(haspNode) + "/status"; + mqttSensorTopic = "hasp/" + String(haspNode) + "/sensor"; + mqttLightCommandTopic = "hasp/" + String(haspNode) + "/light/switch"; + mqttLightStateTopic = "hasp/" + String(haspNode) + "/light/state"; + mqttLightBrightCommandTopic = "hasp/" + String(haspNode) + "/brightness/set"; + mqttLightBrightStateTopic = "hasp/" + String(haspNode) + "/brightness/state"; + mqttMotionStateTopic = "hasp/" + String(haspNode) + "/motion/state"; + + const String mqttCommandSubscription = mqttCommandTopic + "/#"; + const String mqttGroupCommandSubscription = mqttGroupCommandTopic + "/#"; + const String mqttLightSubscription = mqttLightCommandTopic + "/#"; + const String mqttLightBrightSubscription = mqttLightBrightCommandTopic + "/#"; + + // Generate an MQTT client ID as haspNode + our MAC address + mqttClientId = String(haspNode) + "-" + String(espMac[0], HEX) + String(espMac[1], HEX) + String(espMac[2], HEX) + String(espMac[3], HEX) + String(espMac[4], HEX) + String(espMac[5], HEX); + nextionSendCmd("page 0"); + nextionSetAttr("p[0].b[1].font", "6"); + nextionSetAttr("p[0].b[1].txt", "\"WiFi Connected!\\r " + String(WiFi.SSID()) + "\\rIP: " + WiFi.localIP().toString() + "\\r\\rMQTT Connecting:\\r " + String(mqttServer) + "\""); + if (mqttTlsEnabled) + { + debugPrintln(String(F("MQTT: Attempting connection to broker ")) + String(mqttServer) + String(F(" on port ")) + String(mqttPort) + String(F(" with TLS enabled as clientID ")) + mqttClientId); + } + else + { + debugPrintln(String(F("MQTT: Attempting connection to broker ")) + String(mqttServer) + String(F(" on port ")) + String(mqttPort) + String(F(" with TLS disabled as clientID ")) + mqttClientId); + } + + // Set keepAlive, cleanSession, timeout + mqttClient.setOptions(30, true, mqttConnectTimeout); + + // declare LWT + mqttClient.setWill(mqttStatusTopic.c_str(), "OFF", true, 1); + + while (!mqttClient.connected()) + { // Loop until we're connected to MQTT + mqttConnectTimer = millis(); + mqttClient.connect(mqttClientId.c_str(), mqttUser, mqttPassword, false); + + if (mqttClient.connected()) + { // Attempt to connect to broker, setting last will and testament + // Update panel with MQTT status + nextionSetAttr("p[0].b[1].txt", "\"WiFi Connected!\\r " + String(WiFi.SSID()) + "\\rIP: " + WiFi.localIP().toString() + "\\r\\rMQTT Connected:\\r " + String(mqttServer) + "\""); + debugPrintln(F("MQTT: connected")); + + // Reset our diagnostic booleans + mqttPingCheck = true; + mqttPortCheck = true; + + // Subscribe to our incoming topics + if (mqttClient.subscribe(mqttCommandSubscription)) + { + debugPrintln(String(F("MQTT: subscribed to ")) + mqttCommandSubscription); + } + if (mqttClient.subscribe(mqttGroupCommandSubscription)) + { + debugPrintln(String(F("MQTT: subscribed to ")) + mqttGroupCommandSubscription); + } + if (mqttClient.subscribe(mqttLightSubscription)) + { + debugPrintln(String(F("MQTT: subscribed to ")) + mqttLightSubscription); + } + if (mqttClient.subscribe(mqttLightBrightSubscription)) + { + debugPrintln(String(F("MQTT: subscribed to ")) + mqttLightBrightSubscription); + } + + // Publish discovery configuration + mqttDiscovery(); + + // Publish backlight status + if (lcdBacklightOn) + { + debugPrintln(String(F("MQTT OUT: '")) + mqttLightStateTopic + String(F("' : 'ON'"))); + mqttClient.publish(mqttLightStateTopic, "ON", true, 1); + } + else + { + debugPrintln(String(F("MQTT OUT: '")) + mqttLightStateTopic + String(F("' : 'OFF'"))); + mqttClient.publish(mqttLightStateTopic, "OFF", true, 1); + } + debugPrintln(String(F("MQTT OUT: '")) + mqttLightBrightStateTopic + String(F("' : ")) + String(lcdBacklightDim)); + mqttClient.publish(mqttLightBrightStateTopic, String(lcdBacklightDim), true, 1); + + if (mqttFirstConnect) + { // Force any subscribed clients to toggle OFF/ON when we first connect to + // make sure we get a full panel refresh at power on. Sending OFF, + // "ON" will be sent by the mqttStatusTopic subscription action below. + mqttFirstConnect = false; + debugPrintln(String(F("MQTT OUT: '")) + mqttStatusTopic + "' : 'OFF'"); + mqttClient.publish(mqttStatusTopic, "OFF", true, 0); + } + + if (mqttClient.subscribe(mqttStatusTopic)) + { + debugPrintln(String(F("MQTT: subscribed to ")) + mqttStatusTopic); + } + mqttClient.loop(); + } + else + { // Retry until we give up and restart after connectTimeout seconds + mqttReconnectCount++; + if (mqttReconnectCount * mqttConnectTimeout * 6 > (connectTimeout * 1000)) + { + debugPrintln(String(F("MQTT: connection attempt ")) + String(mqttReconnectCount) + String(F(" failed with rc: ")) + String(mqttClient.returnCode()) + String(F(" and error: ")) + String(mqttClient.lastError()) + String(F(". Restarting device."))); + espReset(); + } + yield(); + webServer.handleClient(); + + String mqttCheckResult = "Ping: FAILED"; + String mqttCheckResultNextion = "MQTT Check..."; + + debugPrintln(String(F("MQTT: connection attempt ")) + String(mqttReconnectCount) + String(F(" failed with rc ")) + String(mqttClient.returnCode()) + String(F(" and error: ")) + String(mqttClient.lastError())); + nextionSetAttr("p[0].b[1].txt", String(F("\"WiFi Connected!\\r ")) + String(WiFi.SSID()) + String(F("\\rIP: ")) + WiFi.localIP().toString() + String(F("\\r\\rMQTT Failed:\\r ")) + String(mqttServer) + String(F("\\rRC: ")) + String(mqttClient.returnCode()) + String(F(" Error: ")) + String(mqttClient.lastError()) + String(F("\\r")) + mqttCheckResultNextion + String(F("\""))); + + mqttPingCheck = Ping.ping(mqttServer, 4); + yield(); + webServer.handleClient(); + mqttPortCheck = wifiClient.connect(mqttServer, atoi(mqttPort)); + yield(); + webServer.handleClient(); + + mqttCheckResultNextion = "Ping: "; + if (mqttPingCheck) + { + mqttCheckResult = "Ping: SUCCESS"; + mqttCheckResultNextion = "Ping: "; + } + if (mqttPortCheck) + { + mqttCheckResult += " Port: SUCCESS"; + mqttCheckResultNextion += " Port: "; + } + else + { + mqttCheckResult += " Port: FAILED"; + mqttCheckResultNextion += " Port: "; + } + debugPrintln(String(F("MQTT: connection checks: ")) + mqttCheckResult + String(F(". Trying again in 30 seconds."))); + nextionSetAttr("p[0].b[1].txt", String(F("\"WiFi Connected!\\r ")) + String(WiFi.SSID()) + String(F("\\rIP: ")) + WiFi.localIP().toString() + String(F("\\r\\rMQTT Failed:\\r ")) + String(mqttServer) + String(F("\\rRC: ")) + String(mqttClient.returnCode()) + String(F(" Error: ")) + String(mqttClient.lastError()) + String(F("\\r")) + mqttCheckResultNextion + String(F("\""))); + + while (millis() < (mqttConnectTimer + (mqttConnectTimeout * 6))) + { + yield(); + nextionHandleInput(); // Nextion serial communications loop + ArduinoOTA.handle(); // Arduino OTA loop + webServer.handleClient(); // webServer loop + telnetHandleClient(); // telnet client loop + motionHandle(); // motion sensor loop + beepHandle(); // beep feedback loop + } + } + } + rebootOnp0b1 = false; + if (nextionActivePage < 0) + { // We never picked up a message giving us a page number, so we'll just go to the default page + debugPrintln(String(F("DEBUG: NextionActivePage not received from MQTT, setting to 0"))); + String mqttButtonJSONEvent = String(F("{\"event\":\"page\",\"value\":0}")); + debugPrintln(String(F("MQTT OUT: '")) + mqttStateJSONTopic + String(F("' : '")) + mqttButtonJSONEvent + String(F("'"))); + mqttClient.publish(mqttStateJSONTopic, mqttButtonJSONEvent, false, 0); + String mqttPageTopic = mqttStateTopic + "/page"; + debugPrintln(String(F("MQTT OUT: '")) + mqttPageTopic + String(F("' : '0'"))); + mqttClient.publish(mqttPageTopic, "0", false, 0); + } +} + +//////////////////////////////////////////////////////////////////////////////////////////////////// +void mqttProcessInput(String &strTopic, String &strPayload) +{ // Handle incoming commands from MQTT + + // strTopic: homeassistant/haswitchplate/devicename/command/p[1].b[4].txt + // strPayload: "Lights On" + // subTopic: p[1].b[4].txt + + // Incoming Namespace (replace /device/ with /group/ for group commands) + // '[...]/device/command' -m '' == No command requested, respond with mqttStatusUpdate() + // '[...]/device/command' -m 'dim=50' == nextionSendCmd("dim=50") + // '[...]/device/command/json' -m '["dim=5", "page 1"]' == nextionSendCmd("dim=50"), nextionSendCmd("page 1") + // '[...]/device/command/p[1].b[4].txt' -m '' == nextionGetAttr("p[1].b[4].txt") + // '[...]/device/command/p[1].b[4].txt' -m '"Lights On"' == nextionSetAttr("p[1].b[4].txt", "\"Lights On\"") + // '[...]/device/brightness/set' -m '50' == nextionSendCmd("dims=50") + // '[...]/device/light/switch' -m 'OFF' == nextionSendCmd("dims=0") + // '[...]/device/command/page' -m '1' == nextionSendCmd("page 1") + // '[...]/device/command/statusupdate' -m '' == mqttStatusUpdate() + // '[...]/device/command/discovery' -m '' == call mqttDiscovery() + // '[...]/device/command/lcdupdate' -m 'http://192.168.0.10/local/HASwitchPlate.tft' == nextionOtaStartDownload("http://192.168.0.10/local/HASwitchPlate.tft") + // '[...]/device/command/lcdupdate' -m '' == nextionOtaStartDownload("lcdFirmwareUrl") + // '[...]/device/command/espupdate' -m 'http://192.168.0.10/local/HASwitchPlate.ino.d1_mini.bin' == espStartOta("http://192.168.0.10/local/HASwitchPlate.ino.d1_mini.bin") + // '[...]/device/command/espupdate' -m '' == espStartOta("espFirmwareUrl") + // '[...]/device/command/beep' -m '100,200,3' == beep on for 100msec, off for 200msec, repeat 3 times + // '[...]/device/command/hassdiscovery' -m 'homeassistant' == hassDiscovery = homeassistant + // '[...]/device/command/nextionmaxpages' -m '11' == nextionmaxpages = 11 + // '[...]/device/command/nextionbaud' -m '921600' == nextionBaud = 921600 + // '[...]/device/command/debugserialenabled' -m 'true' == enable serial debug output + // '[...]/device/command/debugtelnetenabled' -m 'true' == enable telnet debug output + // '[...]/device/command/mdnsenabled' -m 'true' == enable mDNS responder + // '[...]/device/command/beepenabled' -m 'true' == enable beep output on keypress + // '[...]/device/command/ignoretouchwhenoff' -m 'true' == disable actions on keypress + + debugPrintln(String(F("MQTT IN: '")) + strTopic + String(F("' : '")) + strPayload + String(F("'"))); + + if (((strTopic == mqttCommandTopic) || (strTopic == mqttGroupCommandTopic)) && (strPayload == "")) + { // '[...]/device/command' -m '' = No command requested, respond with mqttStatusUpdate() + mqttStatusUpdate(); // return status JSON via MQTT + } + else if (strTopic == mqttCommandTopic || strTopic == mqttGroupCommandTopic) + { // '[...]/device/command' -m 'dim=50' == nextionSendCmd("dim=50") + nextionSendCmd(strPayload); + } + else if (strTopic == (mqttCommandTopic + "/page") || strTopic == (mqttGroupCommandTopic + "/page")) + { // '[...]/device/command/page' -m '1' == nextionSendCmd("page 1") + if (strPayload == "") + { + nextionSendCmd("sendme"); + } + else + { + nextionActivePage = strPayload.toInt(); + nextionSendCmd("page " + strPayload); + } + } + else if (strTopic == (mqttCommandTopic + "/json") || strTopic == (mqttGroupCommandTopic + "/json")) + { // '[...]/device/command/json' -m '["dim=5", "page 1"]' = nextionSendCmd("dim=50"), nextionSendCmd("page 1") + if (strPayload != "") + { + nextionParseJson(strPayload); // Send to nextionParseJson() + } + } + else if (strTopic == (mqttCommandTopic + "/statusupdate") || strTopic == (mqttGroupCommandTopic + "/statusupdate")) + { // '[...]/device/command/statusupdate' == mqttStatusUpdate() + mqttStatusUpdate(); // return status JSON via MQTT + } + else if (strTopic == (mqttCommandTopic + "/discovery") || strTopic == (mqttGroupCommandTopic + "/discovery")) + { // '[...]/device/command/discovery' == mqttDiscovery() + mqttDiscovery(); // send Home Assistant discovery message via MQTT + } + else if (strTopic == (mqttCommandTopic + "/hassdiscovery") || strTopic == (mqttGroupCommandTopic + "/hassdiscovery")) + { // '[...]/device/command/hassdiscovery' -m 'homeassistant' == hassDiscovery = homeassistant + strPayload.toCharArray(hassDiscovery, 128); // set hassDiscovery to value provided in payload + configSave(); + mqttDiscovery(); // send Home Assistant discovery message on new discovery topic via MQTT + } + else if ((strTopic == (mqttCommandTopic + "/nextionmaxpages") || strTopic == (mqttGroupCommandTopic + "/nextionmaxpages")) && (strPayload.toInt() < 256) && (strPayload.toInt() > 0)) + { // '[...]/device/command/nextionmaxpages' -m '11' == nextionmaxpages = 11 + nextionMaxPages = strPayload.toInt(); // set nextionMaxPages to value provided in payload + configSave(); + mqttDiscovery(); // send Home Assistant discovery message via MQTT + } + else if ((strTopic == (mqttCommandTopic + "/nextionbaud") || strTopic == (mqttGroupCommandTopic + "/nextionbaud")) && + ((strPayload.toInt() == 2400) || + (strPayload.toInt() == 4800) || + (strPayload.toInt() == 9600) || + (strPayload.toInt() == 19200) || + (strPayload.toInt() == 31250) || + (strPayload.toInt() == 38400) || + (strPayload.toInt() == 57600) || + (strPayload.toInt() == 115200) || + (strPayload.toInt() == 230400) || + (strPayload.toInt() == 250000) || + (strPayload.toInt() == 256000) || + (strPayload.toInt() == 512000) || + (strPayload.toInt() == 921600))) + { // '[...]/device/command/nextionbaud' -m '921600' == nextionBaud = 921600 + strPayload.toCharArray(nextionBaud, 7); // set nextionBaud to value provided in payload + nextionAckEnable = false; + nextionSendCmd("bauds=" + strPayload); // send baud rate to nextion + nextionAckEnable = true; + Serial.flush(); + Serial1.flush(); + Serial.end(); + Serial1.end(); + Serial.begin(atoi(nextionBaud)); // Serial - LCD RX (after swap), debug TX + Serial1.begin(atoi(nextionBaud)); // Serial1 - LCD TX, no RX + Serial.swap(); // Swap to allow hardware UART comms to LCD + configSave(); + } + else if (strTopic == (mqttCommandTopic + "/debugserialenabled") || strTopic == (mqttGroupCommandTopic + "/debugserialenabled")) + { // '[...]/device/command/debugserialenabled' -m 'true' == enable serial debug output + if (strPayload.equalsIgnoreCase("true")) + { + debugSerialEnabled = true; + configSave(); + } + else if (strPayload.equalsIgnoreCase("false")) + { + debugSerialEnabled = false; + configSave(); + } + } + else if (strTopic == (mqttCommandTopic + "/debugtelnetenabled") || strTopic == (mqttGroupCommandTopic + "/debugtelnetenabled")) + { // '[...]/device/command/debugtelnetenabled' -m 'true' == enable telnet debug output + if (strPayload.equalsIgnoreCase("true")) + { + debugTelnetEnabled = true; + configSave(); + } + else if (strPayload.equalsIgnoreCase("false")) + { + debugTelnetEnabled = false; + configSave(); + } + } + else if (strTopic == (mqttCommandTopic + "/mdnsenabled") || strTopic == (mqttGroupCommandTopic + "/mdnsenabled")) + { // '[...]/device/command/mdnsenabled' -m 'true' == enable mDNS responder + if (strPayload.equalsIgnoreCase("true")) + { + mdnsEnabled = true; + configSave(); + } + else if (strPayload.equalsIgnoreCase("false")) + { + mdnsEnabled = false; + configSave(); + } + } + else if (strTopic == (mqttCommandTopic + "/beepenabled") || strTopic == (mqttGroupCommandTopic + "/beepenabled")) + { // '[...]/device/command/beepenabled' -m 'true' == enable beep output on keypress + if (strPayload.equalsIgnoreCase("true")) + { + beepEnabled = true; + configSave(); + } + else if (strPayload.equalsIgnoreCase("false")) + { + beepEnabled = false; + configSave(); + } + } + else if (strTopic == (mqttCommandTopic + "/ignoretouchwhenoff") || strTopic == (mqttGroupCommandTopic + "/ignoretouchwhenoff")) + { // '[...]/device/command/ignoretouchwhenoff' -m 'true' == disable actions on keypress + if (strPayload.equalsIgnoreCase("true")) + { + ignoreTouchWhenOff = true; + configSave(); + } + else if (strPayload.equalsIgnoreCase("false")) + { + ignoreTouchWhenOff = false; + configSave(); + } + } + else if (strTopic == (mqttCommandTopic + "/lcdupdate") || strTopic == (mqttGroupCommandTopic + "/lcdupdate")) + { // '[...]/device/command/lcdupdate' -m 'http://192.168.0.10/local/HASwitchPlate.tft' == nextionOtaStartDownload("http://192.168.0.10/local/HASwitchPlate.tft") + if (strPayload == "") + { + nextionOtaStartDownload(lcdFirmwareUrl); + } + else + { + nextionOtaStartDownload(strPayload); + } + } + else if (strTopic == (mqttCommandTopic + "/espupdate") || strTopic == (mqttGroupCommandTopic + "/espupdate")) + { // '[...]/device/command/espupdate' -m 'http://192.168.0.10/local/HASwitchPlate.ino.d1_mini.bin' == espStartOta("http://192.168.0.10/local/HASwitchPlate.ino.d1_mini.bin") + if (strPayload == "") + { + espStartOta(espFirmwareUrl); + } + else + { + espStartOta(strPayload); + } + } + else if (strTopic == (mqttCommandTopic + "/reboot") || strTopic == (mqttGroupCommandTopic + "/reboot")) + { // '[...]/device/command/reboot' == reboot microcontroller + debugPrintln(F("MQTT: Rebooting device")); + espReset(); + } + else if (strTopic == (mqttCommandTopic + "/lcdreboot") || strTopic == (mqttGroupCommandTopic + "/lcdreboot")) + { // '[...]/device/command/lcdreboot' == reboot LCD panel + debugPrintln(F("MQTT: Rebooting LCD")); + nextionReset(); + } + else if (strTopic == (mqttCommandTopic + "/factoryreset") || strTopic == (mqttGroupCommandTopic + "/factoryreset")) + { // '[...]/device/command/factoryreset' == clear all saved settings + configClearSaved(); + } + else if (strTopic == (mqttCommandTopic + "/beep") || strTopic == (mqttGroupCommandTopic + "/beep")) + { // '[...]/device/command/beep' == activate beep function + String mqttvar1 = getSubtringField(strPayload, ',', 0); + String mqttvar2 = getSubtringField(strPayload, ',', 1); + String mqttvar3 = getSubtringField(strPayload, ',', 2); + + beepOnTime = mqttvar1.toInt(); + beepOffTime = mqttvar2.toInt(); + beepCounter = mqttvar3.toInt(); + } + else if (strTopic == (mqttCommandTopic + "/crashtest")) + { // '[...]/device/command/crashtest' -m 'divzero' == divide by zero + if (strPayload == "divzero") + { + debugPrintln(String(F("DEBUG: attempt to divide by zero"))); + int result, zero; + zero = 0; + result = 1 / zero; + debugPrintln(String(F("DEBUG: div zero result: ")) + String(result)); + } + else if (strPayload == "nullptr") + { // '[...]/device/command/crashtest' -m 'nullptr' == dereference a null pointer + debugPrintln(String(F("DEBUG: attempt to dereference null pointer"))); + int *nullPointer = NULL; + debugPrintln(String(F("DEBUG: dereference null pointer: ")) + String(*nullPointer)); + } + else if (strPayload == "wdt") + { // '[...]/device/command/crashtest' -m 'wdt' == trigger soft WDT + debugPrintln(String(F("DEBUG: enter tight loop and cause WDT"))); + while (true) + { + } + } + } + else if (strTopic.startsWith(mqttCommandTopic) && (strPayload == "")) + { // '[...]/device/command/p[1].b[4].txt' -m '' == nextionGetAttr("p[1].b[4].txt") + String subTopic = strTopic.substring(mqttCommandTopic.length() + 1); + mqttGetSubtopic = "/" + subTopic; + nextionGetAttr(subTopic); + } + else if (strTopic.startsWith(mqttGroupCommandTopic) && (strPayload == "")) + { // '[...]/group/command/p[1].b[4].txt' -m '' == nextionGetAttr("p[1].b[4].txt") + String subTopic = strTopic.substring(mqttGroupCommandTopic.length() + 1); + mqttGetSubtopic = "/" + subTopic; + nextionGetAttr(subTopic); + } + else if (strTopic.startsWith(mqttCommandTopic)) + { // '[...]/device/command/p[1].b[4].txt' -m '"Lights On"' == nextionSetAttr("p[1].b[4].txt", "\"Lights On\"") + String subTopic = strTopic.substring(mqttCommandTopic.length() + 1); + nextionSetAttr(subTopic, strPayload); + } + else if (strTopic.startsWith(mqttGroupCommandTopic)) + { // '[...]/group/command/p[1].b[4].txt' -m '"Lights On"' == nextionSetAttr("p[1].b[4].txt", "\"Lights On\"") + String subTopic = strTopic.substring(mqttGroupCommandTopic.length() + 1); + nextionSetAttr(subTopic, strPayload); + } + else if (strTopic == mqttLightBrightCommandTopic) + { // change the brightness from the light topic + nextionSetAttr("dim", strPayload); + nextionSetAttr("dims", "dim"); + lcdBacklightDim = strPayload.toInt(); + debugPrintln(String(F("MQTT OUT: '")) + mqttLightBrightStateTopic + String(F("' : '")) + strPayload + String(F("'"))); + mqttClient.publish(mqttLightBrightStateTopic, strPayload, true, 0); + } + else if (strTopic == mqttLightCommandTopic && strPayload == "OFF") + { // set the panel dim OFF from the light topic, saving current dim level first + nextionSetAttr("dims", "dim"); + nextionSetAttr("dim", "0"); + lcdBacklightOn = 0; + debugPrintln(String(F("MQTT OUT: '")) + mqttLightStateTopic + String(F("' : 'OFF'"))); + mqttClient.publish(mqttLightStateTopic, "OFF", true, 0); + } + else if (strTopic == mqttLightCommandTopic && strPayload == "ON") + { // set the panel dim ON from the light topic, restoring saved dim level + nextionSetAttr("dim", "dims"); + nextionSetAttr("sleep", "0"); + lcdBacklightOn = 1; + debugPrintln(String(F("MQTT OUT: '")) + mqttLightStateTopic + String(F("' : 'ON'"))); + mqttClient.publish(mqttLightStateTopic, "ON", true, 0); + } + else if (strTopic == mqttStatusTopic && strPayload == "OFF") + { // catch a dangling LWT from a previous connection if it appears + debugPrintln(String(F("MQTT OUT: '")) + mqttStatusTopic + String(F("' : 'ON'"))); + mqttClient.publish(mqttStatusTopic, "ON", true, 0); + mqttClient.publish(mqttStateJSONTopic, String(F("{\"event_type\":\"hasp_device\",\"event\":\"online\"}"))); + debugPrintln(String(F("MQTT OUT: '")) + mqttStateJSONTopic + String(F(" : {\"event_type\":\"hasp_device\",\"event\":\"online\"}"))); + } +} + +//////////////////////////////////////////////////////////////////////////////////////////////////// +void mqttStatusUpdate() +{ // Periodically publish system status + String mqttSensorPayload = String(F("{\"espVersion\":")); + mqttSensorPayload += String(haspVersion) + String(F(",")); + if (updateEspAvailable) + { + mqttSensorPayload += String(F("\"updateEspAvailable\":true,")); + } + else + { + mqttSensorPayload += String(F("\"updateEspAvailable\":false,")); + } + if (lcdConnected) + { + mqttSensorPayload += String(F("\"lcdConnected\":true,")); + } + else + { + mqttSensorPayload += String(F("\"lcdConnected\":false,")); + } + mqttSensorPayload += String(F("\"lcdVersion\":\"")) + String(lcdVersion) + String(F("\",")); + if (updateLcdAvailable) + { + mqttSensorPayload += String(F("\"updateLcdAvailable\":true,")); + } + else + { + mqttSensorPayload += String(F("\"updateLcdAvailable\":false,")); + } + mqttSensorPayload += String(F("\"espUptime\":")) + String(long(millis() / 1000)) + String(F(",")); + mqttSensorPayload += String(F("\"signalStrength\":")) + String(WiFi.RSSI()) + String(F(",")); + mqttSensorPayload += String(F("\"haspName\":\"")) + String(haspNode) + String(F("\",")); + mqttSensorPayload += String(F("\"haspIP\":\"")) + WiFi.localIP().toString() + String(F("\",")); + mqttSensorPayload += String(F("\"haspClientID\":\"")) + mqttClientId + String(F("\",")); + mqttSensorPayload += String(F("\"haspMac\":\"")) + String(espMac[0], HEX) + String(F(":")) + String(espMac[1], HEX) + String(F(":")) + String(espMac[2], HEX) + String(F(":")) + String(espMac[3], HEX) + String(F(":")) + String(espMac[4], HEX) + String(F(":")) + String(espMac[5], HEX) + String(F("\",")); + mqttSensorPayload += String(F("\"haspManufacturer\":\"HASwitchPlate\",\"haspModel\":\"HASPone v1.0.0\",")); + mqttSensorPayload += String(F("\"heapFree\":")) + String(ESP.getFreeHeap()) + String(F(",")); + mqttSensorPayload += String(F("\"heapFragmentation\":")) + String(ESP.getHeapFragmentation()) + String(F(",")); + mqttSensorPayload += String(F("\"heapMaxFreeBlockSize\":")) + String(ESP.getMaxFreeBlockSize()) + String(F(",")); + mqttSensorPayload += String(F("\"espCore\":\"")) + String(ESP.getCoreVersion()) + String(F("\"")); + mqttSensorPayload += "}"; + + // Publish sensor JSON + mqttClient.publish(mqttSensorTopic, mqttSensorPayload, true, 1); + debugPrintln(String(F("MQTT OUT: '")) + mqttSensorTopic + String(F("' : '")) + mqttSensorPayload + String(F("'"))); +} + +//////////////////////////////////////////////////////////////////////////////////////////////////// +void mqttDiscovery() +{ // Publish Home Assistant discovery messages + const String mqttDiscoveryDevice = String(F("\"device\":{\"identifiers\":[\"")) + mqttClientId + String(F("\"],\"name\":\"")) + String(haspNode) + String(F("\",\"configuration_url\":\"http://")) + WiFi.localIP().toString() + String(F("\",\"manufacturer\":\"HASwitchPlate\",\"model\":\"HASPone v1.0.0\",\"sw_version\":")) + String(haspVersion) + String(F("},\"origin\":{\"name\":\"HASPone\",\"support_url\":\"https://haswitchplate.com\",\"sw\":")) + String(haspVersion) + String(F("}}")); + + // light discovery for backlight + String mqttDiscoveryTopic = String(hassDiscovery) + String(F("/light/")) + String(haspNode) + String(F("/config")); + String mqttDiscoveryPayload = String(F("{\"name\":\"backlight\",\"object_id\":\"")) + String(haspNode) + String(F("_backlight\",\"command_topic\":\"")) + mqttLightCommandTopic + String(F("\",\"state_topic\":\"")) + mqttLightStateTopic + String(F("\",\"brightness_state_topic\":\"")) + mqttLightBrightStateTopic + String(F("\",\"brightness_command_topic\":\"")) + mqttLightBrightCommandTopic + String(F("\",\"availability_topic\":\"")) + mqttStatusTopic + String(F("\",\"brightness_scale\":100,\"unique_id\":\"")) + mqttClientId + String(F("-backlight\",\"payload_on\":\"ON\",\"payload_off\":\"OFF\",\"payload_available\":\"ON\",\"payload_not_available\":\"OFF\",")) + mqttDiscoveryDevice; + mqttClient.publish(mqttDiscoveryTopic, mqttDiscoveryPayload, true, 1); + debugPrintln(String(F("MQTT OUT: '")) + mqttDiscoveryTopic + String(F("' : '")) + String(mqttDiscoveryPayload) + String(F("'"))); + + // sensor discovery for device telemetry + mqttDiscoveryTopic = String(hassDiscovery) + String(F("/sensor/")) + String(haspNode) + String(F("/config")); + mqttDiscoveryPayload = String(F("{\"name\":\"sensor\",\"object_id\":\"")) + String(haspNode) + String(F("_sensor\",\"json_attributes_topic\":\"")) + mqttSensorTopic + String(F("\",\"state_topic\":\"")) + mqttStatusTopic + String(F("\",\"unique_id\":\"")) + mqttClientId + String(F("-sensor\",\"icon\":\"mdi:cellphone-text\",")) + mqttDiscoveryDevice; + mqttClient.publish(mqttDiscoveryTopic, mqttDiscoveryPayload, true, 1); + debugPrintln(String(F("MQTT OUT: '")) + mqttDiscoveryTopic + String(F("' : '")) + String(mqttDiscoveryPayload) + String(F("'"))); + + // number discovery for active page + mqttDiscoveryTopic = String(hassDiscovery) + String(F("/number/")) + String(haspNode) + String(F("/config")); + mqttDiscoveryPayload = String(F("{\"name\":\"active page\",\"object_id\":\"")) + String(haspNode) + String(F("_active_page\",\"command_topic\":\"")) + mqttCommandTopic + String(F("/page\",\"state_topic\":\"")) + mqttStateTopic + String(F("/page\",\"step\":1,\"min\":0,\"max\":")) + String(nextionMaxPages) + String(F(",\"retain\":true,\"optimistic\":true,\"icon\":\"mdi:page-next-outline\",\"unique_id\":\"")) + mqttClientId + String(F("-page\",")) + mqttDiscoveryDevice; + mqttClient.publish(mqttDiscoveryTopic, mqttDiscoveryPayload, true, 1); + debugPrintln(String(F("MQTT OUT: '")) + mqttDiscoveryTopic + String(F("' : '")) + String(mqttDiscoveryPayload) + String(F("'"))); + + // AlwaysOn topic for RGB lights + mqttClient.publish((String(F("hasp/")) + String(haspNode) + String(F("/alwayson"))), "ON", true, 1); + debugPrintln(String(F("MQTT OUT: 'hasp/")) + String(haspNode) + String(F("/alwayson' : 'ON'"))); + + // rgb light discovery for selectedforegroundcolor + mqttDiscoveryTopic = String(hassDiscovery) + String(F("/light/")) + String(haspNode) + String(F("/selectedforegroundcolor/config")); + mqttDiscoveryPayload = String(F("{\"name\":\"selected foreground color\",\"object_id\":\"")) + String(haspNode) + String(F("_selected_foreground_color\",\"command_topic\":\"hasp/")) + String(haspNode) + String(F("/light/selectedforegroundcolor/switch\",\"state_topic\":\"hasp/")) + String(haspNode) + String(F("/alwayson\",\"rgb_command_topic\":\"hasp/")) + String(haspNode) + String(F("/light/selectedforegroundcolor/rgb\",\"rgb_command_template\":\"{{(red|bitwise_and(248)*256)+(green|bitwise_and(252)*8)+(blue|bitwise_and(248)/8)|int }}\",\"retain\":true,\"unique_id\":\"")) + mqttClientId + String(F("-selectedforegroundcolor\",")) + mqttDiscoveryDevice; + mqttClient.publish(mqttDiscoveryTopic, mqttDiscoveryPayload, true, 1); + debugPrintln(String(F("MQTT OUT: '")) + mqttDiscoveryTopic + String(F("' : '")) + String(mqttDiscoveryPayload) + String(F("'"))); + + // rgb light discovery for selectedbackgroundcolor + mqttDiscoveryTopic = String(hassDiscovery) + String(F("/light/")) + String(haspNode) + String(F("/selectedbackgroundcolor/config")); + mqttDiscoveryPayload = String(F("{\"name\":\"selected background color\",\"object_id\":\"")) + String(haspNode) + String(F("_selected_background_color\",\"command_topic\":\"hasp/")) + String(haspNode) + String(F("/light/selectedbackgroundcolor/switch\",\"state_topic\":\"hasp/")) + String(haspNode) + String(F("/alwayson\",\"rgb_command_topic\":\"hasp/")) + String(haspNode) + String(F("/light/selectedbackgroundcolor/rgb\",\"rgb_command_template\":\"{{(red|bitwise_and(248)*256)+(green|bitwise_and(252)*8)+(blue|bitwise_and(248)/8)|int }}\",\"retain\":true,\"unique_id\":\"")) + mqttClientId + String(F("-selectedbackgroundcolor\",")) + mqttDiscoveryDevice; + mqttClient.publish(mqttDiscoveryTopic, mqttDiscoveryPayload, true, 1); + debugPrintln(String(F("MQTT OUT: '")) + mqttDiscoveryTopic + String(F("' : '")) + String(mqttDiscoveryPayload) + String(F("'"))); + + // rgb light discovery for unselectedforegroundcolor + mqttDiscoveryTopic = String(hassDiscovery) + String(F("/light/")) + String(haspNode) + String(F("/unselectedforegroundcolor/config")); + mqttDiscoveryPayload = String(F("{\"name\":\"unselected foreground color\",\"object_id\":\"")) + String(haspNode) + String(F("_unselected_foreground_color\",\"command_topic\":\"hasp/")) + String(haspNode) + String(F("/light/unselectedforegroundcolor/switch\",\"state_topic\":\"hasp/")) + String(haspNode) + String(F("/alwayson\",\"rgb_command_topic\":\"hasp/")) + String(haspNode) + String(F("/light/unselectedforegroundcolor/rgb\",\"rgb_command_template\":\"{{(red|bitwise_and(248)*256)+(green|bitwise_and(252)*8)+(blue|bitwise_and(248)/8)|int }}\",\"retain\":true,\"unique_id\":\"")) + mqttClientId + String(F("-unselectedforegroundcolor\",")) + mqttDiscoveryDevice; + mqttClient.publish(mqttDiscoveryTopic, mqttDiscoveryPayload, true, 1); + debugPrintln(String(F("MQTT OUT: '")) + mqttDiscoveryTopic + String(F("' : '")) + String(mqttDiscoveryPayload) + String(F("'"))); + + // rgb light discovery for unselectedbackgroundcolor + mqttDiscoveryTopic = String(hassDiscovery) + String(F("/light/")) + String(haspNode) + String(F("/unselectedbackgroundcolor/config")); + mqttDiscoveryPayload = String(F("{\"name\":\"unselected background color\",\"object_id\":\"")) + String(haspNode) + String(F("_unselected_background_color\",\"command_topic\":\"hasp/")) + String(haspNode) + String(F("/light/unselectedbackgroundcolor/switch\",\"state_topic\":\"hasp/")) + String(haspNode) + String(F("/alwayson\",\"rgb_command_topic\":\"hasp/")) + String(haspNode) + String(F("/light/unselectedbackgroundcolor/rgb\",\"rgb_command_template\":\"{{(red|bitwise_and(248)*256)+(green|bitwise_and(252)*8)+(blue|bitwise_and(248)/8)|int }}\",\"retain\":true,\"unique_id\":\"")) + mqttClientId + String(F("-unselectedbackgroundcolor\",")) + mqttDiscoveryDevice; + mqttClient.publish(mqttDiscoveryTopic, mqttDiscoveryPayload, true, 1); + debugPrintln(String(F("MQTT OUT: '")) + mqttDiscoveryTopic + String(F("' : '")) + String(mqttDiscoveryPayload) + String(F("'"))); + + if (motionEnabled) + { // binary_sensor for motion + String mqttDiscoveryTopic = String(hassDiscovery) + String(F("/binary_sensor/")) + String(haspNode) + String(F("-motion/config")); + String mqttDiscoveryPayload = String(F("{\"device_class\":\"motion\",\"name\":\"motion\",\"object_id\":\"")) + String(haspNode) + String(F("_motion\",\"state_topic\":\"")) + mqttMotionStateTopic + String(F("\",\"unique_id\":\"")) + mqttClientId + String(F("-motion\",\"payload_on\":\"ON\",\"payload_off\":\"OFF\",")) + mqttDiscoveryDevice; + mqttClient.publish(mqttDiscoveryTopic, mqttDiscoveryPayload, true, 1); + debugPrintln(String(F("MQTT OUT: '")) + mqttDiscoveryTopic + String(F("' : '")) + String(mqttDiscoveryPayload) + String(F("'"))); + } +} + +//////////////////////////////////////////////////////////////////////////////////////////////////// +void nextionHandleInput() +{ // Handle incoming serial data from the Nextion panel + // This will collect serial data from the panel and place it into the global buffer + // nextionReturnBuffer[nextionReturnIndex] + unsigned long handlerTimeout = millis() + 100; + bool nextionCommandComplete = false; + static uint8_t nextionTermByteCnt = 0; // counter for our 3 consecutive 0xFFs + + while (Serial.available() && !nextionCommandComplete && (millis() < handlerTimeout)) + { + byte nextionCommandByte = Serial.read(); + if (nextionCommandByte == 0xFF) + { // check to see if we have one of 3 consecutive 0xFF which indicates the end of a command + nextionTermByteCnt++; + if (nextionTermByteCnt >= 3) + { // We have received a complete command + lcdConnected = true; + nextionCommandComplete = true; + nextionTermByteCnt = 0; // reset counter + } + } + else + { + nextionTermByteCnt = 0; // reset counter if a non-term byte was encountered + } + nextionReturnBuffer[nextionReturnIndex] = nextionCommandByte; + nextionReturnIndex++; + if (nextionCommandComplete) + { + nextionAckReceived = true; + nextionProcessInput(); + } + yield(); + } + if (millis() > handlerTimeout) + { + debugPrintln(String(F("HMI ERROR: nextionHandleInput timeout"))); + } +} + +//////////////////////////////////////////////////////////////////////////////////////////////////// +void nextionProcessInput() +{ // Process complete incoming serial command from the Nextion panel + // Command reference: https://www.itead.cc/wiki/Nextion_Instruction_Set#Format_of_Device_Return_Data + // tl;dr: command byte, command data, 0xFF 0xFF 0xFF + + if (nextionReturnBuffer[0] == 0x01) + { // Instruction Successful - quietly ignore this as it will be returned after every command issued, + // and processing it + spitting out serial output is a huge drag on performance if serial debug is enabled. + + // debugPrintln(String(F("HMI IN: [Instruction Successful] 0x")) + String(nextionReturnBuffer[0], HEX)); + // if (mqttClient.connected()) + // { + // String mqttButtonJSONEvent = String(F("{\"event\":\"nextion_return_data\",\"return_code\":\"0x")) + String(nextionReturnBuffer[0], HEX) + String(F("\",\"return_code_description\":\"Instruction Successful\"}")); + // mqttClient.publish(mqttStateJSONTopic, mqttButtonJSONEvent); + // debugPrintln(String(F("MQTT OUT: '")) + mqttStateJSONTopic + String(F("' : '")) + mqttButtonJSONEvent + String(F("'"))); + // } + nextionReturnIndex = 0; // Done handling the buffer, reset index back to 0 + return; // skip the rest of the tests below and return immediately + } + + debugPrintln(String(F("HMI IN: [")) + String(nextionReturnIndex) + String(F(" bytes]: ")) + printHex8(nextionReturnBuffer, nextionReturnIndex)); + + if (nextionReturnBuffer[0] == 0x00 && nextionReturnBuffer[1] == 0x00 && nextionReturnBuffer[2] == 0x00) + { // Nextion Startup + debugPrintln(String(F("HMI IN: [Nextion Startup] 0x00 0x00 0x00"))); + if (mqttClient.connected()) + { + String mqttButtonJSONEvent = String(F("{\"event\":\"nextion_return_data\",\"return_code\":\"0x00 0x00 0x00\",\"return_code_description\":\"Nextion Startup\"}")); + mqttClient.publish(mqttStateJSONTopic, mqttButtonJSONEvent); + debugPrintln(String(F("MQTT OUT: '")) + mqttStateJSONTopic + String(F("' : '")) + mqttButtonJSONEvent + String(F("'"))); + } + } + else if (nextionReturnBuffer[0] == 0x00) + { // Invalid Instruction + debugPrintln(String(F("HMI IN: [Invalid Instruction] 0x")) + String(nextionReturnBuffer[0], HEX)); + if (mqttClient.connected()) + { + String mqttButtonJSONEvent = String(F("{\"event\":\"nextion_return_data\",\"return_code\":\"0x")) + String(nextionReturnBuffer[0], HEX) + String(F("\",\"return_code_description\":\"Invalid Instruction\"}")); + mqttClient.publish(mqttStateJSONTopic, mqttButtonJSONEvent); + debugPrintln(String(F("MQTT OUT: '")) + mqttStateJSONTopic + String(F("' : '")) + mqttButtonJSONEvent + String(F("'"))); + } + } + else if (nextionReturnBuffer[0] == 0x02) + { // Invalid Component ID + debugPrintln(String(F("HMI IN: [Invalid Component ID] 0x")) + String(nextionReturnBuffer[0], HEX)); + if (mqttClient.connected()) + { + String mqttButtonJSONEvent = String(F("{\"event\":\"nextion_return_data\",\"return_code\":\"0x")) + String(nextionReturnBuffer[0], HEX) + String(F("\",\"return_code_description\":\"Invalid Component ID\"}")); + mqttClient.publish(mqttStateJSONTopic, mqttButtonJSONEvent); + debugPrintln(String(F("MQTT OUT: '")) + mqttStateJSONTopic + String(F("' : '")) + mqttButtonJSONEvent + String(F("'"))); + } + } + else if (nextionReturnBuffer[0] == 0x03) + { // Invalid Page ID + debugPrintln(String(F("HMI IN: [Invalid Page ID] 0x")) + String(nextionReturnBuffer[0], HEX)); + if (mqttClient.connected()) + { + String mqttButtonJSONEvent = String(F("{\"event\":\"nextion_return_data\",\"return_code\":\"0x")) + String(nextionReturnBuffer[0], HEX) + String(F("\",\"return_code_description\":\"Invalid Page ID\"}")); + mqttClient.publish(mqttStateJSONTopic, mqttButtonJSONEvent); + debugPrintln(String(F("MQTT OUT: '")) + mqttStateJSONTopic + String(F("' : '")) + mqttButtonJSONEvent + String(F("'"))); + } + } + else if (nextionReturnBuffer[0] == 0x04) + { // Invalid Picture ID + debugPrintln(String(F("HMI IN: [Invalid Picture ID] 0x")) + String(nextionReturnBuffer[0], HEX)); + if (mqttClient.connected()) + { + String mqttButtonJSONEvent = String(F("{\"event\":\"nextion_return_data\",\"return_code\":\"0x")) + String(nextionReturnBuffer[0], HEX) + String(F("\",\"return_code_description\":\"Invalid Picture ID\"}")); + mqttClient.publish(mqttStateJSONTopic, mqttButtonJSONEvent); + debugPrintln(String(F("MQTT OUT: '")) + mqttStateJSONTopic + String(F("' : '")) + mqttButtonJSONEvent + String(F("'"))); + } + } + else if (nextionReturnBuffer[0] == 0x05) + { // Invalid Font ID + debugPrintln(String(F("HMI IN: [Invalid Font ID ] 0x")) + String(nextionReturnBuffer[0], HEX)); + if (mqttClient.connected()) + { + String mqttButtonJSONEvent = String(F("{\"event\":\"nextion_return_data\",\"return_code\":\"0x")) + String(nextionReturnBuffer[0], HEX) + String(F("\",\"return_code_description\":\"Invalid Font ID \"}")); + mqttClient.publish(mqttStateJSONTopic, mqttButtonJSONEvent); + debugPrintln(String(F("MQTT OUT: '")) + mqttStateJSONTopic + String(F("' : '")) + mqttButtonJSONEvent + String(F("'"))); + } + } + else if (nextionReturnBuffer[0] == 0x06) + { // Invalid File Operation + debugPrintln(String(F("HMI IN: [Invalid File Operation] 0x")) + String(nextionReturnBuffer[0], HEX)); + if (mqttClient.connected()) + { + String mqttButtonJSONEvent = String(F("{\"event\":\"nextion_return_data\",\"return_code\":\"0x")) + String(nextionReturnBuffer[0], HEX) + String(F("\",\"return_code_description\":\"Invalid File Operation\"}")); + mqttClient.publish(mqttStateJSONTopic, mqttButtonJSONEvent); + debugPrintln(String(F("MQTT OUT: '")) + mqttStateJSONTopic + String(F("' : '")) + mqttButtonJSONEvent + String(F("'"))); + } + } + else if (nextionReturnBuffer[0] == 0x09) + { // Invalid CRC + debugPrintln(String(F("HMI IN: [Invalid CRC] 0x")) + String(nextionReturnBuffer[0], HEX)); + if (mqttClient.connected()) + { + String mqttButtonJSONEvent = String(F("{\"event\":\"nextion_return_data\",\"return_code\":\"0x")) + String(nextionReturnBuffer[0], HEX) + String(F("\",\"return_code_description\":\"Invalid CRC\"}")); + mqttClient.publish(mqttStateJSONTopic, mqttButtonJSONEvent); + debugPrintln(String(F("MQTT OUT: '")) + mqttStateJSONTopic + String(F("' : '")) + mqttButtonJSONEvent + String(F("'"))); + } + } + else if (nextionReturnBuffer[0] == 0x11) + { // Invalid Baud rate Setting + debugPrintln(String(F("HMI IN: [Invalid Baud rate Setting] 0x")) + String(nextionReturnBuffer[0], HEX)); + if (mqttClient.connected()) + { + String mqttButtonJSONEvent = String(F("{\"event\":\"nextion_return_data\",\"return_code\":\"0x")) + String(nextionReturnBuffer[0], HEX) + String(F("\",\"return_code_description\":\"Invalid Baud rate Setting\"}")); + mqttClient.publish(mqttStateJSONTopic, mqttButtonJSONEvent); + debugPrintln(String(F("MQTT OUT: '")) + mqttStateJSONTopic + String(F("' : '")) + mqttButtonJSONEvent + String(F("'"))); + } + } + else if (nextionReturnBuffer[0] == 0x12) + { // Invalid Waveform ID or Channel # + debugPrintln(String(F("HMI IN: [Invalid Waveform ID or Channel #] 0x")) + String(nextionReturnBuffer[0], HEX)); + if (mqttClient.connected()) + { + String mqttButtonJSONEvent = String(F("{\"event\":\"nextion_return_data\",\"return_code\":\"0x")) + String(nextionReturnBuffer[0], HEX) + String(F("\",\"return_code_description\":\"Invalid Waveform ID or Channel #\"}")); + mqttClient.publish(mqttStateJSONTopic, mqttButtonJSONEvent); + debugPrintln(String(F("MQTT OUT: '")) + mqttStateJSONTopic + String(F("' : '")) + mqttButtonJSONEvent + String(F("'"))); + } + } + else if (nextionReturnBuffer[0] == 0x1A) + { // Invalid Variable name or attribute + debugPrintln(String(F("HMI IN: [Invalid Variable name or attribute] 0x")) + String(nextionReturnBuffer[0], HEX)); + if (mqttClient.connected()) + { + String mqttButtonJSONEvent = String(F("{\"event\":\"nextion_return_data\",\"return_code\":\"0x")) + String(nextionReturnBuffer[0], HEX) + String(F("\",\"return_code_description\":\"Invalid Variable name or attribute\"}")); + mqttClient.publish(mqttStateJSONTopic, mqttButtonJSONEvent); + debugPrintln(String(F("MQTT OUT: '")) + mqttStateJSONTopic + String(F("' : '")) + mqttButtonJSONEvent + String(F("'"))); + } + } + else if (nextionReturnBuffer[0] == 0x1B) + { // Invalid Variable Operation + debugPrintln(String(F("HMI IN: [Invalid Variable Operation] 0x")) + String(nextionReturnBuffer[0], HEX)); + if (mqttClient.connected()) + { + String mqttButtonJSONEvent = String(F("{\"event\":\"nextion_return_data\",\"return_code\":\"0x")) + String(nextionReturnBuffer[0], HEX) + String(F("\",\"return_code_description\":\"Invalid Variable Operation\"}")); + mqttClient.publish(mqttStateJSONTopic, mqttButtonJSONEvent); + debugPrintln(String(F("MQTT OUT: '")) + mqttStateJSONTopic + String(F("' : '")) + mqttButtonJSONEvent + String(F("'"))); + } + } + else if (nextionReturnBuffer[0] == 0x1C) + { // Assignment failed to assign + debugPrintln(String(F("HMI IN: [Assignment failed to assign] 0x")) + String(nextionReturnBuffer[0], HEX)); + if (mqttClient.connected()) + { + String mqttButtonJSONEvent = String(F("{\"event\":\"nextion_return_data\",\"return_code\":\"0x")) + String(nextionReturnBuffer[0], HEX) + String(F("\",\"return_code_description\":\"Assignment failed to assign\"}")); + mqttClient.publish(mqttStateJSONTopic, mqttButtonJSONEvent); + debugPrintln(String(F("MQTT OUT: '")) + mqttStateJSONTopic + String(F("' : '")) + mqttButtonJSONEvent + String(F("'"))); + } + } + else if (nextionReturnBuffer[0] == 0x1D) + { // EEPROM Operation failed + debugPrintln(String(F("HMI IN: [EEPROM Operation failed] 0x")) + String(nextionReturnBuffer[0], HEX)); + if (mqttClient.connected()) + { + String mqttButtonJSONEvent = String(F("{\"event\":\"nextion_return_data\",\"return_code\":\"0x")) + String(nextionReturnBuffer[0], HEX) + String(F("\",\"return_code_description\":\"EEPROM Operation failed\"}")); + mqttClient.publish(mqttStateJSONTopic, mqttButtonJSONEvent); + debugPrintln(String(F("MQTT OUT: '")) + mqttStateJSONTopic + String(F("' : '")) + mqttButtonJSONEvent + String(F("'"))); + } + } + else if (nextionReturnBuffer[0] == 0x1E) + { // Invalid Quantity of Parameters + debugPrintln(String(F("HMI IN: [Invalid Quantity of Parameters] 0x")) + String(nextionReturnBuffer[0], HEX)); + if (mqttClient.connected()) + { + String mqttButtonJSONEvent = String(F("{\"event\":\"nextion_return_data\",\"return_code\":\"0x")) + String(nextionReturnBuffer[0], HEX) + String(F("\",\"return_code_description\":\"Invalid Quantity of Parameters\"}")); + mqttClient.publish(mqttStateJSONTopic, mqttButtonJSONEvent); + debugPrintln(String(F("MQTT OUT: '")) + mqttStateJSONTopic + String(F("' : '")) + mqttButtonJSONEvent + String(F("'"))); + } + } + else if (nextionReturnBuffer[0] == 0x1F) + { // IO Operation failed + debugPrintln(String(F("HMI IN: [IO Operation failed] 0x")) + String(nextionReturnBuffer[0], HEX)); + if (mqttClient.connected()) + { + String mqttButtonJSONEvent = String(F("{\"event\":\"nextion_return_data\",\"return_code\":\"0x")) + String(nextionReturnBuffer[0], HEX) + String(F("\",\"return_code_description\":\"IO Operation failed\"}")); + mqttClient.publish(mqttStateJSONTopic, mqttButtonJSONEvent); + debugPrintln(String(F("MQTT OUT: '")) + mqttStateJSONTopic + String(F("' : '")) + mqttButtonJSONEvent + String(F("'"))); + } + } + else if (nextionReturnBuffer[0] == 0x20) + { // Escape Character Invalid + debugPrintln(String(F("HMI IN: [Escape Character Invalid] 0x")) + String(nextionReturnBuffer[0], HEX)); + if (mqttClient.connected()) + { + String mqttButtonJSONEvent = String(F("{\"event\":\"nextion_return_data\",\"return_code\":\"0x")) + String(nextionReturnBuffer[0], HEX) + String(F("\",\"return_code_description\":\"Escape Character Invalid\"}")); + mqttClient.publish(mqttStateJSONTopic, mqttButtonJSONEvent); + debugPrintln(String(F("MQTT OUT: '")) + mqttStateJSONTopic + String(F("' : '")) + mqttButtonJSONEvent + String(F("'"))); + } + } + else if (nextionReturnBuffer[0] == 0x23) + { // Variable name too long + debugPrintln(String(F("HMI IN: [Variable name too long] 0x")) + String(nextionReturnBuffer[0], HEX)); + if (mqttClient.connected()) + { + String mqttButtonJSONEvent = String(F("{\"event\":\"nextion_return_data\",\"return_code\":\"0x")) + String(nextionReturnBuffer[0], HEX) + String(F("\",\"return_code_description\":\"Variable name too long\"}")); + mqttClient.publish(mqttStateJSONTopic, mqttButtonJSONEvent); + debugPrintln(String(F("MQTT OUT: '")) + mqttStateJSONTopic + String(F("' : '")) + mqttButtonJSONEvent + String(F("'"))); + } + } + else if (nextionReturnBuffer[0] == 0x24) + { // Serial Buffer Overflow + debugPrintln(String(F("HMI IN: [Serial Buffer Overflow] 0x")) + String(nextionReturnBuffer[0], HEX)); + if (mqttClient.connected()) + { + String mqttButtonJSONEvent = String(F("{\"event\":\"nextion_return_data\",\"return_code\":\"0x")) + String(nextionReturnBuffer[0], HEX) + String(F("\",\"return_code_description\":\"Serial Buffer Overflow\"}")); + mqttClient.publish(mqttStateJSONTopic, mqttButtonJSONEvent); + debugPrintln(String(F("MQTT OUT: '")) + mqttStateJSONTopic + String(F("' : '")) + mqttButtonJSONEvent + String(F("'"))); + } + } + + else if (nextionReturnBuffer[0] == 0x65) + { // Handle incoming touch command + // 0x65+Page ID+Component ID+TouchEvent+End + // Return this data when the touch event created by the user is pressed. + // Definition of TouchEvent: Press Event 0x01, Release Event 0X00 + // Example: 0x65 0x00 0x02 0x01 0xFF 0xFF 0xFF + // Meaning: Touch Event, Page 0, Object 2, Press + String nextionPage = String(nextionReturnBuffer[1]); + String nextionButtonID = String(nextionReturnBuffer[2]); + byte nextionButtonAction = nextionReturnBuffer[3]; + + if (nextionButtonAction == 0x01) + { + debugPrintln(String(F("HMI IN: [Button ON] 'p[")) + nextionPage + "].b[" + nextionButtonID + "]'"); + if (mqttClient.connected()) + { + // Only process touch events if screen backlight is on and configured to do so. + if (ignoreTouchWhenOff && !lcdBacklightOn) + { + String mqttButtonJSONEvent = String(F("{\"event_type\":\"button_press_disabled\",\"event\":\"p[")) + nextionPage + String(F("].b[")) + nextionButtonID + String(F("]\",\"value\":\"ON\"}")); + mqttClient.publish(mqttStateJSONTopic, mqttButtonJSONEvent); + debugPrintln(String(F("MQTT OUT: '")) + mqttStateJSONTopic + String(F("' : '")) + mqttButtonJSONEvent + String(F("'"))); + } + else + { + String mqttButtonTopic = mqttStateTopic + "/p[" + nextionPage + "].b[" + nextionButtonID + "]"; + mqttClient.publish(mqttButtonTopic, "ON"); + debugPrintln(String(F("MQTT OUT: '")) + mqttButtonTopic + "' : 'ON'"); + String mqttButtonJSONEvent = String(F("{\"event_type\":\"button_short_press\",\"event\":\"p[")) + nextionPage + String(F("].b[")) + nextionButtonID + String(F("]\",\"value\":\"ON\"}")); + mqttClient.publish(mqttStateJSONTopic, mqttButtonJSONEvent); + debugPrintln(String(F("MQTT OUT: '")) + mqttStateJSONTopic + String(F("' : '")) + mqttButtonJSONEvent + String(F("'"))); + } + } + if (beepEnabled) + { + beepOnTime = 500; + beepOffTime = 100; + beepCounter = 1; + } + if (rebootOnp0b1 && (nextionPage == "0") && (nextionButtonID == "1")) + { + debugPrintln(String(F("HMI IN: p[0].b[1] pressed during HASPone configuration, rebooting."))); + espReset(); + } + if (rebootOnLongPress) + { + rebootOnLongPressTimer = millis(); + } + } + else if (nextionButtonAction == 0x00) + { + debugPrintln(String(F("HMI IN: [Button OFF] 'p[")) + nextionPage + "].b[" + nextionButtonID + "]'"); + if (mqttClient.connected()) + { + // Only process touch events if screen backlight is on and configured to do so. + if (ignoreTouchWhenOff && !lcdBacklightOn) + { + String mqttButtonJSONEvent = String(F("{\"event_type\":\"button_release_disabled\",\"event\":\"p[")) + nextionPage + String(F("].b[")) + nextionButtonID + String(F("]\",\"value\":\"ON\"}")); + mqttClient.publish(mqttStateJSONTopic, mqttButtonJSONEvent); + debugPrintln(String(F("MQTT OUT: '")) + mqttStateJSONTopic + String(F("' : '")) + mqttButtonJSONEvent + String(F("'"))); + } + else + { + String mqttButtonTopic = mqttStateTopic + "/p[" + nextionPage + "].b[" + nextionButtonID + "]"; + mqttClient.publish(mqttButtonTopic, "OFF"); + debugPrintln(String(F("MQTT OUT: '")) + mqttButtonTopic + "' : 'OFF'"); + String mqttButtonJSONEvent = String(F("{\"event_type\":\"button_short_release\",\"event\":\"p[")) + nextionPage + String(F("].b[")) + nextionButtonID + String(F("]\",\"value\":\"OFF\"}")); + mqttClient.publish(mqttStateJSONTopic, mqttButtonJSONEvent); + debugPrintln(String(F("MQTT OUT: '")) + mqttStateJSONTopic + String(F("' : '")) + mqttButtonJSONEvent + String(F("'"))); + // Now see if this object has a .val that might have been updated. Works for sliders, + // two-state buttons, etc, returns 0 for normal buttons + mqttGetSubtopic = "/p[" + nextionPage + "].b[" + nextionButtonID + "].val"; + // This right here is dicey. We're done w/ this command so reset the index allowing this to be kinda-reentrant + // because the call to nextionGetAttr is going to call us back. + nextionReturnIndex = 0; + nextionGetAttr("p[" + nextionPage + "].b[" + nextionButtonID + "].val"); + } + } + if (rebootOnLongPress && (millis() - rebootOnLongPressTimer > rebootOnLongPressTimeout)) + { + debugPrintln(String(F("HMI IN: Button long press, rebooting."))); + espReset(); + } + rebootOnLongPressTimer = millis(); + } + } + else if (nextionReturnBuffer[0] == 0x66) + { // Handle incoming "sendme" page number + // 0x66+PageNum+End + // Example: 0x66 0x02 0xFF 0xFF 0xFF + // Meaning: page 2 + String nextionPage = String(nextionReturnBuffer[1]); + debugPrintln(String(F("HMI IN: [sendme Page] '")) + nextionPage + String(F("'"))); + if ((nextionPage != "0") || nextionReportPage0) + { // If we have a new page AND ( (it's not "0") OR (we've set the flag to report 0 anyway) ) + + if (mqttClient.connected()) + { + String mqttButtonJSONEvent = String(F("{\"event\":\"page\",\"value\":")) + nextionPage + String(F("}")); + debugPrintln(String(F("MQTT OUT: '")) + mqttStateJSONTopic + String(F("' : '")) + mqttButtonJSONEvent + String(F("'"))); + mqttClient.publish(mqttStateJSONTopic, mqttButtonJSONEvent); + String mqttPageTopic = mqttStateTopic + "/page"; + debugPrintln(String(F("MQTT OUT: '")) + mqttPageTopic + String(F("' : '")) + nextionPage + String(F("'"))); + mqttClient.publish(mqttPageTopic, nextionPage, false, 0); + } + } + } + else if (nextionReturnBuffer[0] == 0x67 || nextionReturnBuffer[0] == 0x68) + { // Handle touch coordinate data + // 0X67+Coordinate X High+Coordinate X Low+Coordinate Y High+Coordinate Y Low+TouchEvent+End + // Example: 0X67 0X00 0X7A 0X00 0X1E 0X01 0XFF 0XFF 0XFF + // Meaning: Coordinate (122,30), Touch Event: Press + // issue Nextion command "sendxy=1" to enable this output + // 0x68 is the same, but returned when the screen touch has awakened the screen from sleep + uint16_t xCoord = nextionReturnBuffer[1]; + xCoord = xCoord * 256 + nextionReturnBuffer[2]; + uint16_t yCoord = nextionReturnBuffer[3]; + yCoord = yCoord * 256 + nextionReturnBuffer[4]; + String xyCoord = String(xCoord) + String(',') + String(yCoord); + byte nextionTouchAction = nextionReturnBuffer[5]; + if (nextionTouchAction == 0x01) + { + debugPrintln(String(F("HMI IN: [Touch ON] '")) + xyCoord + String(F("'"))); + if (mqttClient.connected()) + { + String mqttTouchTopic = mqttStateTopic + "/touchOn"; + mqttClient.publish(mqttTouchTopic, xyCoord); + debugPrintln(String(F("MQTT OUT: '")) + mqttTouchTopic + String(F("' : '")) + xyCoord + String(F("'"))); + String mqttButtonJSONEvent = String(F("{\"event_type\":\"button_short_press\",\"event\":\"touchxy\",\"touch_event\":\"ON\",\"touchx\":\"")) + String(xCoord) + String(F("\",\"touchy\":\"")) + String(yCoord) + String(F("\",\"screen_state\":\"")); + if (nextionReturnBuffer[0] == 0x67) + { + mqttButtonJSONEvent += "awake\"}"; + } + else + { + mqttButtonJSONEvent += "asleep\"}"; + } + mqttClient.publish(mqttStateJSONTopic, mqttButtonJSONEvent); + debugPrintln(String(F("MQTT OUT: '")) + mqttStateJSONTopic + String(F("' : '")) + mqttButtonJSONEvent + String(F("'"))); + } + } + else if (nextionTouchAction == 0x00) + { + debugPrintln(String(F("HMI IN: [Touch OFF] '")) + xyCoord + String(F("'"))); + if (mqttClient.connected()) + { + String mqttTouchTopic = mqttStateTopic + "/touchOff"; + mqttClient.publish(mqttTouchTopic, xyCoord); + debugPrintln(String(F("MQTT OUT: '")) + mqttTouchTopic + String(F("' : '")) + xyCoord + String(F("'"))); + String mqttButtonJSONEvent = String(F("{\"event_type\":\"button_short_press\",\"event\":\"touchxy\",\"touch_event\":\"OFF\",\"touchx\":\"")) + String(xCoord) + String(F("\",\"touchy\":\"")) + String(yCoord) + String(F("\",\"screen_state\":\"")); + if (nextionReturnBuffer[0] == 0x67) + { + mqttButtonJSONEvent += "awake\"}"; + } + else + { + mqttButtonJSONEvent += "asleep\"}"; + } + mqttClient.publish(mqttStateJSONTopic, mqttButtonJSONEvent); + debugPrintln(String(F("MQTT OUT: '")) + mqttStateJSONTopic + String(F("' : '")) + mqttButtonJSONEvent + String(F("'"))); + } + } + } + else if (nextionReturnBuffer[0] == 0x70) + { // Handle get string return + // 0x70+ASCII string+End + // Example: 0x70 0x41 0x42 0x43 0x44 0x31 0x32 0x33 0x34 0xFF 0xFF 0xFF + // Meaning: String data, ABCD1234 + String getString; + for (int i = 1; i < nextionReturnIndex - 3; i++) + { // convert the payload into a string + getString += (char)nextionReturnBuffer[i]; + } + debugPrintln(String(F("HMI IN: [String Return] '")) + getString + String(F("'"))); + if (mqttClient.connected()) + { + if (mqttGetSubtopic == "") + { // If there's no outstanding request for a value, publish to mqttStateTopic + mqttClient.publish(mqttStateTopic, getString); + debugPrintln(String(F("MQTT OUT: '")) + mqttStateTopic + String(F("' : '")) + getString + String(F("'"))); + } + else + { // Otherwise, publish the to saved mqttGetSubtopic and then reset mqttGetSubtopic + String mqttReturnTopic = mqttStateTopic + mqttGetSubtopic; + mqttClient.publish(mqttReturnTopic, getString); + debugPrintln(String(F("MQTT OUT: '")) + mqttReturnTopic + String(F("' : '")) + getString + String(F("'"))); + String mqttButtonJSONEvent = String(F("{\"event\":\"")) + mqttGetSubtopic.substring(1) + String(F("\",\"value\":\"")) + getString + String(F("\"}")); + mqttClient.publish(mqttStateJSONTopic, mqttButtonJSONEvent); + debugPrintln(String(F("MQTT OUT: '")) + mqttStateJSONTopic + String(F("' : '")) + mqttButtonJSONEvent + String(F("'"))); + mqttGetSubtopic = ""; + } + } + } + else if (nextionReturnBuffer[0] == 0x71) + { // Handle get int return + // 0x71+byte1+byte2+byte3+byte4+End (4 byte little endian) + // Example: 0x71 0x7B 0x00 0x00 0x00 0xFF 0xFF 0xFF + // Meaning: Integer data, 123 + long getInt = nextionReturnBuffer[4]; + getInt = getInt * 256 + nextionReturnBuffer[3]; + getInt = getInt * 256 + nextionReturnBuffer[2]; + getInt = getInt * 256 + nextionReturnBuffer[1]; + String getString = String(getInt); + debugPrintln(String(F("HMI IN: [Int Return] '")) + getString + String(F("'"))); + + if (lcdVersionQueryFlag) + { + lcdVersion = getInt; + lcdVersionQueryFlag = false; + debugPrintln(String(F("HMI IN: lcdVersion '")) + String(lcdVersion) + String(F("'"))); + } + else if (lcdBacklightQueryFlag) + { + lcdBacklightDim = getInt; + lcdBacklightQueryFlag = false; + if (lcdBacklightDim > 0) + { + lcdBacklightOn = 1; + } + else + { + lcdBacklightOn = 0; + } + debugPrintln(String(F("HMI IN: lcdBacklightDim '")) + String(lcdBacklightDim) + String(F("'"))); + } + else if (mqttGetSubtopic == "") + { + if (mqttClient.connected()) + { + mqttClient.publish(mqttStateTopic, getString); + debugPrintln(String(F("MQTT OUT: '")) + mqttStateTopic + String(F("' : '")) + getString + String(F("'"))); + } + } + // Otherwise, publish the to saved mqttGetSubtopic and then reset mqttGetSubtopic + else + { + if (mqttClient.connected()) + { + String mqttReturnTopic = mqttStateTopic + mqttGetSubtopic; + mqttClient.publish(mqttReturnTopic, getString); + debugPrintln(String(F("MQTT OUT: '")) + mqttReturnTopic + String(F("' : '")) + getString + String(F("'"))); + String mqttButtonJSONEvent = String(F("{\"event\":\"")) + mqttGetSubtopic.substring(1) + String(F("\",\"value\":")) + getString + String(F("}")); + mqttClient.publish(mqttStateJSONTopic, mqttButtonJSONEvent); + debugPrintln(String(F("MQTT OUT: '")) + mqttStateJSONTopic + String(F("' : '")) + mqttButtonJSONEvent + String(F("'"))); + } + mqttGetSubtopic = ""; + } + } + else if (nextionReturnBuffer[0] == 0x63 && nextionReturnBuffer[1] == 0x6f && nextionReturnBuffer[2] == 0x6d && nextionReturnBuffer[3] == 0x6f && nextionReturnBuffer[4] == 0x6b) + { // Catch 'comok' response to 'connect' command: https://www.itead.cc/blog/nextion-hmi-upload-protocol + String comokField; + uint8_t comokFieldCount = 0; + byte comokFieldSeperator = 0x2c; // "," + + for (uint8_t i = 0; i <= nextionReturnIndex; i++) + { // cycle through each byte looking for our field seperator + if (nextionReturnBuffer[i] == comokFieldSeperator) + { // Found the end of a field, so do something with it. Maybe. + if (comokFieldCount == 2) + { + nextionModel = comokField; + debugPrintln(String(F("HMI IN: nextionModel: ")) + nextionModel); + } + comokFieldCount++; + comokField = ""; + } + else + { + comokField += String(char(nextionReturnBuffer[i])); + } + } + } + else if (nextionReturnBuffer[0] == 0x86) + { // Returned when Nextion enters sleep automatically. Using sleep=1 will not return an 0x86 + // 0x86+End + if (mqttClient.connected()) + { + lcdBacklightOn = 0; + mqttClient.publish(mqttLightStateTopic, "OFF", true, 1); + debugPrintln(String(F("MQTT OUT: '")) + mqttLightStateTopic + String(F("' : 'OFF'"))); + String mqttButtonJSONEvent = String(F("{\"event\":\"sleep\",\"value\":\"ON\"}")); + mqttClient.publish(mqttStateJSONTopic, mqttButtonJSONEvent); + debugPrintln(String(F("MQTT OUT: '")) + mqttStateJSONTopic + String(F("' : '")) + mqttButtonJSONEvent + String(F("'"))); + } + } + else if (nextionReturnBuffer[0] == 0x87) + { // Returned when Nextion leaves sleep automatically. Using sleep=0 will not return an 0x87 + // 0x87+End + if (mqttClient.connected()) + { + lcdBacklightOn = 1; + mqttClient.publish(mqttLightStateTopic, "ON", true, 1); + debugPrintln(String(F("MQTT OUT: '")) + mqttLightStateTopic + String(F("' : 'ON'"))); + mqttClient.publish(mqttLightBrightStateTopic, String(lcdBacklightDim), true, 1); + debugPrintln(String(F("MQTT OUT: '")) + mqttLightBrightStateTopic + String(F("' : ")) + String(lcdBacklightDim)); + String mqttButtonJSONEvent = String(F("{\"event\":\"sleep\",\"value\":\"OFF\"}")); + mqttClient.publish(mqttStateJSONTopic, mqttButtonJSONEvent); + debugPrintln(String(F("MQTT OUT: '")) + mqttStateJSONTopic + String(F("' : '")) + mqttButtonJSONEvent + String(F("'"))); + } + } + else if (nextionReturnBuffer[0] == 0x88) + { // Returned when Nextion powers on + // 0x88+End + debugPrintln(F("HMI: Nextion panel connected.")); + } + nextionReturnIndex = 0; // Done handling the buffer, reset index back to 0 +} + +//////////////////////////////////////////////////////////////////////////////////////////////////// +void nextionSendCmd(const String &nextionCmd) +{ // Send a raw command to the Nextion panel + Serial1.print(nextionCmd); + Serial1.write(nextionSuffix, sizeof(nextionSuffix)); + Serial1.flush(); + debugPrintln(String(F("HMI OUT: ")) + nextionCmd); + + if (nextionAckEnable) + { + nextionAckReceived = false; + nextionAckTimer = millis(); + + while ((!nextionAckReceived) && (millis() - nextionAckTimer < nextionAckTimeout)) + { + nextionHandleInput(); + } + if (!nextionAckReceived) + { + debugPrintln(String(F("HMI ERROR: Nextion Ack timeout"))); + String mqttButtonJSONEvent = String(F("{\"event\":\"nextionError\",\"value\":\"Nextion Ack timeout\"}")); + mqttClient.publish(mqttStateJSONTopic, mqttButtonJSONEvent); + debugPrintln(String(F("MQTT OUT: '")) + mqttStateJSONTopic + String(F("' : '")) + mqttButtonJSONEvent + String(F("'"))); + } + } + else + { + nextionHandleInput(); + } +} + +//////////////////////////////////////////////////////////////////////////////////////////////////// +void nextionSetAttr(const String &hmiAttribute, const String &hmiValue) +{ // Set the value of a Nextion component attribute + Serial1.print(hmiAttribute); + Serial1.print("="); + Serial1.print(hmiValue); + Serial1.write(nextionSuffix, sizeof(nextionSuffix)); + Serial1.flush(); + debugPrintln(String(F("HMI OUT: '")) + hmiAttribute + "=" + hmiValue + String(F("'"))); + if (nextionAckEnable) + { + nextionAckReceived = false; + nextionAckTimer = millis(); + + while ((!nextionAckReceived) || (millis() - nextionAckTimer > nextionAckTimeout)) + { + nextionHandleInput(); + } + if (!nextionAckReceived) + { + debugPrintln(String(F("HMI ERROR: Nextion Ack timeout"))); + String mqttButtonJSONEvent = String(F("{\"event\":\"nextionError\",\"value\":\"Nextion Ack timeout\"}")); + mqttClient.publish(mqttStateJSONTopic, mqttButtonJSONEvent); + debugPrintln(String(F("MQTT OUT: '")) + mqttStateJSONTopic + String(F("' : '")) + mqttButtonJSONEvent + String(F("'"))); + } + } + else + { + nextionHandleInput(); + } +} + +//////////////////////////////////////////////////////////////////////////////////////////////////// +void nextionGetAttr(const String &hmiAttribute) +{ // Get the value of a Nextion component attribute + // This will only send the command to the panel requesting the attribute, the actual + // return of that value will be handled by nextionProcessInput and placed into mqttGetSubtopic + Serial1.print("get "); + Serial1.print(hmiAttribute); + Serial1.write(nextionSuffix, sizeof(nextionSuffix)); + Serial1.flush(); + debugPrintln(String(F("HMI OUT: 'get ")) + hmiAttribute + String(F("'"))); + if (nextionAckEnable) + { + nextionAckReceived = false; + nextionAckTimer = millis(); + + while ((!nextionAckReceived) || (millis() - nextionAckTimer > nextionAckTimeout)) + { + nextionHandleInput(); + } + if (!nextionAckReceived) + { + debugPrintln(String(F("HMI ERROR: Nextion Ack timeout"))); + String mqttButtonJSONEvent = String(F("{\"event\":\"nextionError\",\"value\":\"Nextion Ack timeout\"}")); + mqttClient.publish(mqttStateJSONTopic, mqttButtonJSONEvent); + debugPrintln(String(F("MQTT OUT: '")) + mqttStateJSONTopic + String(F("' : '")) + mqttButtonJSONEvent + String(F("'"))); + } + } + else + { + nextionHandleInput(); + } +} + +//////////////////////////////////////////////////////////////////////////////////////////////////// +void nextionParseJson(const String &strPayload) +{ // Parse an incoming JSON array into individual Nextion commands + DynamicJsonDocument nextionCommands(mqttMaxPacketSize + 1024); + DeserializationError jsonError = deserializeJson(nextionCommands, strPayload); + + if (jsonError) + { // Couldn't parse incoming JSON command + String jsonErrorDescription = String(F("Failed to parse incoming JSON command with error:")) + String(jsonError.c_str()) + String(F(" memoryUsage: ")) + String(nextionCommands.memoryUsage()) + String(F(" capacity: ")) + String(nextionCommands.capacity()); + debugPrintln(String(F("MQTT: [ERROR] ")) + jsonErrorDescription); + mqttClient.publish(mqttStateJSONTopic, String(F("{\"event\":\"jsonError\",\"event_source\":\"nextionParseJson()\",\"event_description\":\"")) + jsonErrorDescription + String(F("\"}"))); + } + else + { + for (uint8_t i = 0; i < nextionCommands.size(); i++) + { + nextionSendCmd(nextionCommands[i]); + } + } +} + +//////////////////////////////////////////////////////////////////////////////////////////////////// +void nextionOtaStartDownload(const String &lcdOtaUrl) +{ // Upload firmware to the Nextion LCD via HTTP download + + uint32_t lcdOtaFileSize = 0; + String lcdOtaNextionCmd; + uint32_t lcdOtaChunkCounter = 0; + uint16_t lcdOtaPartNum = 0; + uint32_t lcdOtaTransferred = 0; + uint8_t lcdOtaPercentComplete = 0; + const uint32_t lcdOtaTimeout = 30000; // timeout for receiving new data in milliseconds + static uint32_t lcdOtaTimer = 0; // timer for lcdOtaTimeout + + HTTPClient lcdOtaHttp; + WiFiClientSecure lcdOtaWifiSecure; + WiFiClient lcdOtaWifi; + if (lcdOtaUrl.startsWith(F("https"))) + { + debugPrintln("LCDOTA: Attempting firmware update from HTTPS host: " + lcdOtaUrl); + + lcdOtaHttp.begin(lcdOtaWifiSecure, lcdOtaUrl); + lcdOtaWifiSecure.setInsecure(); + lcdOtaWifiSecure.setBufferSizes(512, 512); + } + else + { + debugPrintln("LCDOTA: Attempting firmware update from HTTP host: " + lcdOtaUrl); + lcdOtaHttp.begin(lcdOtaWifi, lcdOtaUrl); + } + + int lcdOtaHttpReturn = lcdOtaHttp.GET(); + if (lcdOtaHttpReturn > 0) + { // HTTP header has been sent and Server response header has been handled + debugPrintln(String(F("LCDOTA: HTTP GET return code:")) + String(lcdOtaHttpReturn)); + if (lcdOtaHttpReturn == HTTP_CODE_OK) + { // file found at server + int32_t lcdOtaRemaining = lcdOtaHttp.getSize(); // get length of document (is -1 when Server sends no Content-Length header) + lcdOtaFileSize = lcdOtaRemaining; + static uint16_t lcdOtaParts = (lcdOtaRemaining / 4096) + 1; + static const uint16_t lcdOtaBufferSize = 1024; // upload data buffer before sending to UART + static uint8_t lcdOtaBuffer[lcdOtaBufferSize] = {}; + + debugPrintln(String(F("LCDOTA: File found at Server. Size ")) + String(lcdOtaRemaining) + String(F(" bytes in ")) + String(lcdOtaParts) + String(F(" 4k chunks."))); + + WiFiUDP::stopAll(); // Keep mDNS responder and MQTT traffic from breaking things + if (mqttClient.connected()) + { + debugPrintln(F("LCDOTA: LCD firmware upload starting, closing MQTT connection.")); + mqttClient.publish(mqttStatusTopic, "OFF", true, 0); + debugPrintln(String(F("MQTT OUT: '")) + mqttStatusTopic + String(F("' : 'OFF'"))); + mqttClient.disconnect(); + } + + WiFiClient *stream = lcdOtaHttp.getStreamPtr(); // get tcp stream + Serial1.write(nextionSuffix, sizeof(nextionSuffix)); // Send empty command + Serial1.flush(); + nextionHandleInput(); + String lcdOtaNextionCmd = "whmi-wri " + String(lcdOtaFileSize) + "," + String(nextionBaud) + ",0"; + debugPrintln(String(F("LCDOTA: Sending LCD upload command: ")) + lcdOtaNextionCmd); + Serial1.print(lcdOtaNextionCmd); + Serial1.write(nextionSuffix, sizeof(nextionSuffix)); + Serial1.flush(); + + if (nextionOtaResponse()) + { + debugPrintln(F("LCDOTA: LCD upload command accepted.")); + } + else + { + debugPrintln(F("LCDOTA: LCD upload command FAILED. Restarting device.")); + espReset(); + } + debugPrintln(F("LCDOTA: Starting update")); + lcdOtaTimer = millis(); + while (lcdOtaHttp.connected() && (lcdOtaRemaining > 0 || lcdOtaRemaining == -1)) + { // Write incoming data to panel as it arrives + uint16_t lcdOtaHttpSize = stream->available(); // get available data size + + if (lcdOtaHttpSize) + { + uint16_t lcdOtaChunkSize = 0; + if ((lcdOtaHttpSize <= lcdOtaBufferSize) && (lcdOtaHttpSize <= (4096 - lcdOtaChunkCounter))) + { + lcdOtaChunkSize = lcdOtaHttpSize; + } + else if ((lcdOtaBufferSize <= lcdOtaHttpSize) && (lcdOtaBufferSize <= (4096 - lcdOtaChunkCounter))) + { + lcdOtaChunkSize = lcdOtaBufferSize; + } + else + { + lcdOtaChunkSize = 4096 - lcdOtaChunkCounter; + } + stream->readBytes(lcdOtaBuffer, lcdOtaChunkSize); + Serial1.flush(); // make sure any previous writes the UART have completed + Serial1.write(lcdOtaBuffer, lcdOtaChunkSize); // now send buffer to the UART + lcdOtaChunkCounter += lcdOtaChunkSize; + if (lcdOtaChunkCounter >= 4096) + { + Serial1.flush(); + lcdOtaPartNum++; + lcdOtaTransferred += lcdOtaChunkCounter; + lcdOtaPercentComplete = (lcdOtaTransferred * 100) / lcdOtaFileSize; + lcdOtaChunkCounter = 0; + if (nextionOtaResponse()) + { // We've completed a chunk + debugPrintln(String(F("LCDOTA: Part ")) + String(lcdOtaPartNum) + String(F(" OK, ")) + String(lcdOtaPercentComplete) + String(F("% complete"))); + lcdOtaTimer = millis(); + } + else + { + debugPrintln(String(F("LCDOTA: Part ")) + String(lcdOtaPartNum) + String(F(" FAILED, ")) + String(lcdOtaPercentComplete) + String(F("% complete"))); + debugPrintln(F("LCDOTA: failure")); + delay(2000); // extra delay while the LCD does its thing + espReset(); + } + } + else + { + delay(20); + } + if (lcdOtaRemaining > 0) + { + lcdOtaRemaining -= lcdOtaChunkSize; + } + } + delay(10); + if ((lcdOtaTimer > 0) && ((millis() - lcdOtaTimer) > lcdOtaTimeout)) + { // Our timer expired so reset + debugPrintln(F("LCDOTA: ERROR: LCD upload timeout. Restarting.")); + espReset(); + } + } + lcdOtaPartNum++; + lcdOtaTransferred += lcdOtaChunkCounter; + if ((lcdOtaTransferred == lcdOtaFileSize) && nextionOtaResponse()) + { + debugPrintln(String(F("LCDOTA: Success, wrote ")) + String(lcdOtaTransferred) + String(F(" of ")) + String(tftFileSize) + String(F(" bytes."))); + uint32_t lcdOtaDelay = millis(); + debugPrintln(F("LCDOTA: Waiting 5 seconds to allow LCD to apply updates we've sent.")); + while ((millis() - lcdOtaDelay) < 5000) + { // extra 5sec delay while the LCD handles any local firmware updates from new versions of code sent to it + webServer.handleClient(); + yield(); + } + espReset(); + } + else + { + debugPrintln(String(F("LCDOTA: Failure, lcdOtaTransferred: ")) + String(lcdOtaTransferred) + String(F(" lcdOtaFileSize: ")) + String(lcdOtaFileSize)); + espReset(); + } + } + } + else + { + debugPrintln(String(F("LCDOTA: HTTP GET failed, error code ")) + lcdOtaHttp.errorToString(lcdOtaHttpReturn)); + espReset(); + } + lcdOtaHttp.end(); +} + +//////////////////////////////////////////////////////////////////////////////////////////////////// +bool nextionOtaResponse() +{ // Monitor the serial port for a 0x05 response within our timeout + unsigned long nextionCommandTimeout = 2000; // timeout for receiving termination string in milliseconds + unsigned long nextionCommandTimer = millis(); // record current time for our timeout + bool otaSuccessVal = false; + while ((millis() - nextionCommandTimer) < nextionCommandTimeout) + { + if (Serial.available()) + { + byte inByte = Serial.read(); + if (inByte == 0x5) + { + otaSuccessVal = true; + break; + } + } + else + { + delay(1); + } + } + return otaSuccessVal; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////// +bool nextionConnect() +{ + const unsigned long nextionCheckTimeout = 2000; // Max time in msec for nextion connection check + unsigned long nextionCheckTimer = millis(); // Timer for nextion connection checks + + Serial1.write(nextionSuffix, sizeof(nextionSuffix)); + + if (!lcdConnected) + { // Check for some traffic from our LCD + debugPrintln(F("HMI: Waiting for LCD connection")); + while (((millis() - nextionCheckTimer) <= nextionCheckTimeout) && !lcdConnected) + { + nextionHandleInput(); + } + } + if (!lcdConnected) + { // No response from the display using the configured speed, so scan all possible speeds + nextionSetSpeed(); + + nextionCheckTimer = millis(); // Reset our timer + debugPrintln(F("HMI: Waiting again for LCD connection")); + while (((millis() - nextionCheckTimer) <= nextionCheckTimeout) && !lcdConnected) + { + Serial1.write(nextionSuffix, sizeof(nextionSuffix)); + nextionHandleInput(); + } + if (!lcdConnected) + { + debugPrintln(F("HMI: LCD connection timed out")); + return false; + } + } + + // Query backlight status. This should always succeed under simulation or non-HASPone HMI + lcdBacklightQueryFlag = true; + debugPrintln(F("HMI: Querying LCD backlight status")); + Serial1.write(nextionSuffix, sizeof(nextionSuffix)); + nextionSendCmd("get dim"); + while (((millis() - nextionCheckTimer) <= nextionCheckTimeout) && lcdBacklightQueryFlag) + { + nextionHandleInput(); + } + if (lcdBacklightQueryFlag) + { // Our flag is still set, meaning we never got a response. + debugPrintln(F("HMI: LCD backlight query timed out")); + lcdBacklightQueryFlag = false; + return false; + } + + // We are now communicating with the panel successfully. Enable ACK checking for all future commands. + nextionAckEnable = true; + nextionSendCmd("bkcmd=3"); + + // This check depends on the HMI having been designed with a version number in the object + // defined in lcdVersionQuery. It's OK if this fails, it just means the HMI project is + // not utilizing the version capability that the HASPone project makes use of. + lcdVersionQueryFlag = true; + debugPrintln(F("HMI: Querying LCD firmware version number")); + nextionSendCmd("get " + lcdVersionQuery); + while (((millis() - nextionCheckTimer) <= nextionCheckTimeout) && lcdVersionQueryFlag) + { + nextionHandleInput(); + } + if (lcdVersionQueryFlag) + { // Our flag is still set, meaning we never got a response. This should only happen if + // there's a problem. Non-HASPone projects should pass this check with lcdVersion = 0 + debugPrintln(F("HMI: LCD version query timed out")); + lcdVersionQueryFlag = false; + return false; + } + + if (nextionModel.length() == 0) + { // Check for LCD model via `connect`. The Nextion simulator does not support this command, + // so if we're running under that environment this process should timeout. + debugPrintln(F("HMI: Querying LCD model information")); + nextionSendCmd("connect"); + while (((millis() - nextionCheckTimer) <= nextionCheckTimeout) && (nextionModel.length() == 0)) + { + nextionHandleInput(); + } + } + return true; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////// +void nextionSetSpeed() +{ + debugPrintln(String(F("HMI: No Nextion response, attempting to set serial speed to ")) + String(nextionBaud)); + for (unsigned int nextionSpeedsIndex = 0; nextionSpeedsIndex < nextionSpeedsLength; nextionSpeedsIndex++) + { + debugPrintln(String(F("HMI: Sending bauds=")) + String(nextionBaud) + " @" + String(nextionSpeeds[nextionSpeedsIndex]) + " baud"); + Serial1.flush(); + Serial1.begin(nextionSpeeds[nextionSpeedsIndex]); + Serial1.write(nextionSuffix, sizeof(nextionSuffix)); + Serial1.write(nextionSuffix, sizeof(nextionSuffix)); + Serial1.write(nextionSuffix, sizeof(nextionSuffix)); + Serial1.print("bauds=" + String(nextionBaud)); + Serial1.write(nextionSuffix, sizeof(nextionSuffix)); + Serial1.flush(); + } + Serial1.begin(atoi(nextionBaud)); +} + +//////////////////////////////////////////////////////////////////////////////////////////////////// +void nextionReset() +{ + debugPrintln(F("HMI: Rebooting LCD")); + digitalWrite(nextionResetPin, LOW); + Serial1.print("rest"); + Serial1.write(nextionSuffix, sizeof(nextionSuffix)); + Serial1.flush(); + delay(100); + digitalWrite(nextionResetPin, HIGH); + + unsigned long lcdResetTimer = millis(); + const unsigned long lcdResetTimeout = 5000; + + lcdConnected = false; + while (!lcdConnected && (millis() < (lcdResetTimer + lcdResetTimeout))) + { + nextionHandleInput(); + } + if (lcdConnected) + { + debugPrintln(F("HMI: Rebooting LCD completed")); + if (nextionActivePage) + { + nextionSendCmd("page " + String(nextionActivePage)); + } + } + else + { + debugPrintln(F("ERROR: Rebooting LCD completed, but LCD is not responding.")); + } + mqttClient.publish(mqttStatusTopic, "OFF", true, 0); + debugPrintln(String(F("MQTT OUT: '")) + mqttStatusTopic + String(F("' : 'OFF'"))); +} + +//////////////////////////////////////////////////////////////////////////////////////////////////// +void nextionUpdateProgress(const unsigned int &progress, const unsigned int &total) +{ + uint8_t progressPercent = (float(progress) / float(total)) * 100; + nextionSetAttr("p[0].b[4].val", String(progressPercent)); +} + +//////////////////////////////////////////////////////////////////////////////////////////////////// +void espWifiConnect() +{ // Connect to WiFi + rebootOnp0b1 = true; + + nextionSetAttr("p[0].b[1].font", "6"); + if (lcdVersion < 1 || lcdVersion > 2) + { + nextionSendCmd("page 0"); + } + + WiFi.persistent(false); + enableWiFiAtBootTime(); + WiFi.macAddress(espMac); // Read our MAC address and save it to espMac + WiFi.hostname(haspNode); // Assign our hostname before connecting to WiFi + WiFi.setAutoReconnect(true); // Tell WiFi to autoreconnect if connection has dropped + WiFi.setSleepMode(WIFI_NONE_SLEEP); // Disable WiFi sleep modes to prevent occasional disconnects + WiFi.mode(WIFI_STA); // Set the radio to Station + + if (String(wifiSSID) == "") + { // If the sketch has no hard-coded wifiSSID, attempt to use saved creds or use WiFiManager to collect required information from the user. + + // First, check if we have saved wifi creds and try to connect manually. + if (WiFi.SSID() != "") + { + nextionSetAttr("p[0].b[1].txt", "\"WiFi Connecting...\\r " + String(WiFi.SSID()) + "\""); + unsigned long connectTimer = millis() + 10000; + + debugPrintln(String(F("WIFI: Connecting to previously-saved SSID: ")) + String(WiFi.SSID())); + + WiFi.begin(); + while ((WiFi.status() != WL_CONNECTED) && (millis() < connectTimer)) + { + yield(); + } + + unsigned int connectCounter = 0; + unsigned int connectRetries = 4; + unsigned int connectTime = 10000; + while ((WiFi.status() != WL_CONNECTED) && (connectCounter <= connectRetries)) + { + connectCounter++; + debugPrintln(String(F("WIFI: Connect failed, retry attempt ")) + String(connectCounter)); + WiFi.mode(WIFI_OFF); // Force the radio off, and then + delay(100); + WiFi.mode(WIFI_STA); // toggle it back on again + connectTimer = millis() + connectTime; + WiFi.begin(); + while ((WiFi.status() != WL_CONNECTED) && (millis() < connectTimer)) + { + yield(); + } + + if (WiFi.localIP().toString() == "(IP unset)") + { // Check if we have our IP yet + debugPrintln(F("WIFI: Failed to lease address from DHCP, disconnecting and trying again")); + WiFi.disconnect(); + } + } + } + + if (WiFi.status() != WL_CONNECTED) + { // We gave it a shot, still couldn't connect, so let WiFiManager run to make one last + // connection attempt and then flip to AP mode to collect credentials from the user. + WiFi.persistent(true); + WiFiManagerParameter custom_haspNodeHeader("
HASPone Node"); + WiFiManagerParameter custom_haspNode("haspNode", "
Node Name (required: lowercase letters, numbers, and _ only)", haspNode, 15, " maxlength=15 required pattern='[a-z0-9_]*'"); + WiFiManagerParameter custom_groupName("groupName", "Group Name (required)", groupName, 15, " maxlength=15 required"); + WiFiManagerParameter custom_mqttHeader("

MQTT"); + WiFiManagerParameter custom_mqttServer("mqttServer", "
MQTT Broker (required, IP address is preferred)", mqttServer, 127, " maxlength=127"); + WiFiManagerParameter custom_mqttPort("mqttPort", "MQTT Port (required)", mqttPort, 5, " maxlength=5 type='number'"); + WiFiManagerParameter custom_mqttUser("mqttUser", "MQTT User (optional)", mqttUser, 127, " maxlength=127"); + WiFiManagerParameter custom_mqttPassword("mqttPassword", "MQTT Password (optional)", mqttPassword, 127, " maxlength=127 type='password'"); + String mqttTlsEnabled_value = "F"; + if (mqttTlsEnabled) + { + mqttTlsEnabled_value = "T"; + } + String mqttTlsEnabled_checked = "type=\"checkbox\""; + if (mqttTlsEnabled) + { + mqttTlsEnabled_checked = "type=\"checkbox\" checked=\"true\""; + } + WiFiManagerParameter custom_mqttTlsEnabled("mqttTlsEnabled", "MQTT TLS enabled:", mqttTlsEnabled_value.c_str(), 2, mqttTlsEnabled_checked.c_str()); + WiFiManagerParameter custom_mqttFingerprint("mqttFingerprint", "
MQTT TLS Fingerprint (optional, enter as 01:23:AB:CD, etc)", mqttFingerprint, 59, " min length=59 maxlength=59"); + WiFiManagerParameter custom_configHeader("

Admin access"); + WiFiManagerParameter custom_configUser("configUser", "
Config User (required)", configUser, 15, " maxlength=31"); + WiFiManagerParameter custom_configPassword("configPassword", "Config Password (optional)", configPassword, 31, " maxlength=31 type='password'"); + WiFiManagerParameter custom_hassHeader("

Home Assistant integration"); + WiFiManagerParameter custom_hassDiscovery("hassDiscovery", "
Home Assistant Discovery topic (required, should probably be \"homeassistant\")", hassDiscovery, 127, " maxlength=127"); + + WiFiManager wifiManager; + wifiManager.setSaveConfigCallback(configSaveCallback); // set config save notify callback + wifiManager.setCustomHeadElement(HASP_STYLE); // add custom style + wifiManager.addParameter(&custom_haspNodeHeader); + wifiManager.addParameter(&custom_haspNode); + wifiManager.addParameter(&custom_groupName); + wifiManager.addParameter(&custom_mqttHeader); + wifiManager.addParameter(&custom_mqttServer); + wifiManager.addParameter(&custom_mqttPort); + wifiManager.addParameter(&custom_mqttUser); + wifiManager.addParameter(&custom_mqttPassword); + wifiManager.addParameter(&custom_mqttTlsEnabled); + wifiManager.addParameter(&custom_mqttFingerprint); + wifiManager.addParameter(&custom_configHeader); + wifiManager.addParameter(&custom_configUser); + wifiManager.addParameter(&custom_configPassword); + wifiManager.addParameter(&custom_hassHeader); + wifiManager.addParameter(&custom_hassDiscovery); + + // Timeout config portal after connectTimeout seconds, useful if configured wifi network was temporarily unavailable + wifiManager.setTimeout(connectTimeout); + + wifiManager.setAPCallback(espWifiConfigCallback); + + // Fetches SSID and pass from EEPROM and tries to connect + // If it does not connect it starts an access point with the specified name + // and goes into a blocking loop awaiting configuration. + if (!wifiManager.autoConnect(wifiConfigAP, wifiConfigPass)) + { // Reset and try again + debugPrintln(F("WIFI: Failed to connect and hit timeout")); + espReset(); + } + + // Read updated parameters + strcpy(mqttServer, custom_mqttServer.getValue()); + strcpy(mqttPort, custom_mqttPort.getValue()); + strcpy(mqttUser, custom_mqttUser.getValue()); + strcpy(mqttPassword, custom_mqttPassword.getValue()); + if (strcmp(custom_mqttTlsEnabled.getValue(), "T") == 0) + { + mqttTlsEnabled = true; + } + else + { + mqttTlsEnabled = false; + } + strcpy(mqttFingerprint, custom_mqttFingerprint.getValue()); + strcpy(haspNode, custom_haspNode.getValue()); + strcpy(groupName, custom_groupName.getValue()); + strcpy(configUser, custom_configUser.getValue()); + strcpy(configPassword, custom_configPassword.getValue()); + strcpy(hassDiscovery, custom_hassDiscovery.getValue()); + if (shouldSaveConfig) + { // Save the custom parameters to FS + configSave(); + } + } + } + else + { // wifiSSID has been defined, so attempt to connect to it + debugPrintln(String(F("Connecting to WiFi network: ")) + String(wifiSSID)); + WiFi.mode(WIFI_STA); + WiFi.begin(wifiSSID, wifiPass); + + unsigned long wifiReconnectTimer = millis(); + while (WiFi.status() != WL_CONNECTED) + { + delay(1); + if (millis() >= (wifiReconnectTimer + (connectTimeout * 1000))) + { // If we've been trying to reconnect for connectTimeout seconds, reboot and try again + debugPrintln(F("WIFI: Failed to connect and hit timeout")); + espReset(); + } + } + } + + // If you get here you have connected to WiFi + nextionSetAttr("p[0].b[1].font", "6"); + nextionSetAttr("p[0].b[1].txt", "\"WiFi Connected!\\r " + String(WiFi.SSID()) + "\\rIP: " + WiFi.localIP().toString() + "\""); + debugPrintln(String(F("WIFI: Connected successfully and assigned IP: ")) + WiFi.localIP().toString()); + if (nextionActivePage) + { + nextionSendCmd("page " + String(nextionActivePage)); + } + + rebootOnp0b1 = false; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////// +void espWifiReconnect() +{ // Existing WiFi connection dropped, try to reconnect + debugPrintln(F("Reconnecting to WiFi network...")); + WiFi.mode(WIFI_STA); + WiFi.begin(wifiSSID, wifiPass); + + unsigned long wifiReconnectTimer = millis(); + while (WiFi.status() != WL_CONNECTED) + { + delay(1); + if (millis() >= (wifiReconnectTimer + (reConnectTimeout * 1000))) + { // If we've been trying to reconnect for reConnectTimeout seconds, reboot and try again + debugPrintln(F("WIFI: Failed to reconnect and hit timeout")); + espReset(); + } + } +} + +//////////////////////////////////////////////////////////////////////////////////////////////////// +void espWifiConfigCallback(WiFiManager *myWiFiManager) +{ // Notify the user that we're entering config mode + debugPrintln(F("WIFI: Failed to connect to assigned AP, entering config mode")); + if (lcdVersion < 1 || lcdVersion > 2) + { + nextionSendCmd("page 0"); + } + nextionSetAttr("p[0].b[1].font", "6"); + nextionSetAttr("p[0].b[1].txt", "\" HASPone Setup\\r AP: " + String(wifiConfigAP) + "\\rPassword: " + String(wifiConfigPass) + "\\r\\r\\r\\r\\r\\r\\r http://192.168.4.1\""); + nextionSendCmd("vis 3,1"); +} + +//////////////////////////////////////////////////////////////////////////////////////////////////// +void espSetupOta() +{ // Update ESP firmware from network via Arduino OTA + + ArduinoOTA.setHostname(haspNode); + ArduinoOTA.setPassword(configPassword); + ArduinoOTA.setRebootOnSuccess(false); + + ArduinoOTA.onStart([]() + { + debugPrintln(F("ESP OTA: update start")); + nextionSetAttr("p[0].b[1].txt", "\"\\rHASPone update:\\r\\r\\r \""); + nextionSendCmd("page 0"); + nextionSendCmd("vis 4,1"); }); + ArduinoOTA.onEnd([]() + { + debugPrintln(F("ESP OTA: update complete")); + nextionSetAttr("p[0].b[1].txt", "\"\\rHASPone update:\\r\\r Complete!\\rRestarting.\""); + nextionSendCmd("vis 4,1"); + delay(1000); + espReset(); }); + ArduinoOTA.onProgress([](unsigned int progress, unsigned int total) + { nextionUpdateProgress(progress, total); }); + ArduinoOTA.onError([](ota_error_t error) + { + debugPrintln(String(F("ESP OTA: ERROR code ")) + String(error)); + if (error == OTA_AUTH_ERROR) + debugPrintln(F("ESP OTA: ERROR - Auth Failed")); + else if (error == OTA_BEGIN_ERROR) + debugPrintln(F("ESP OTA: ERROR - Begin Failed")); + else if (error == OTA_CONNECT_ERROR) + debugPrintln(F("ESP OTA: ERROR - Connect Failed")); + else if (error == OTA_RECEIVE_ERROR) + debugPrintln(F("ESP OTA: ERROR - Receive Failed")); + else if (error == OTA_END_ERROR) + debugPrintln(F("ESP OTA: ERROR - End Failed")); + nextionSendCmd("vis 4,0"); + nextionSetAttr("p[0].b[1].txt", "\"HASPone update:\\r FAILED\\rerror: " + String(error) + "\""); + delay(1000); + nextionSendCmd("page " + String(nextionActivePage)); }); + ArduinoOTA.begin(); + debugPrintln(F("ESP OTA: Over the Air firmware update ready")); +} + +//////////////////////////////////////////////////////////////////////////////////////////////////// +void espStartOta(const String &espOtaUrl) +{ // Update ESP firmware from HTTP/HTTPS URL + + nextionSetAttr("p[0].b[1].txt", "\"\\rHASPone update:\\r\\r\\r \""); + nextionSendCmd("page 0"); + nextionSendCmd("vis 4,1"); + + WiFiUDP::stopAll(); // Keep mDNS responder from breaking things + delay(1); + + int startHost = espOtaUrl.indexOf("://") + 3; // Find the end of 'https://' + int endHost = espOtaUrl.indexOf('/', startHost); // Find the next '/' after 'https://' + String espOtaHost = espOtaUrl.substring(startHost, endHost); // Extract host + String espOtaUri = espOtaUrl.substring(endHost); // Extract URI + ESPhttpUpdate.rebootOnUpdate(false); + ESPhttpUpdate.onProgress(nextionUpdateProgress); + ESPhttpUpdate.setFollowRedirects(HTTPC_FORCE_FOLLOW_REDIRECTS); + t_httpUpdate_return espOtaUrlReturnCode; + if (espOtaUrl.startsWith(F("https"))) + { + debugPrintln(String(F("ESPFW: Attempting firmware update from HTTPS host: ")) + espOtaUrl); + WiFiClientSecure wifiEspOtaClientSecure; + wifiEspOtaClientSecure.setInsecure(); + wifiEspOtaClientSecure.setBufferSizes(512, 512); + espOtaUrlReturnCode = ESPhttpUpdate.update(wifiEspOtaClientSecure, espOtaUrl); + wifiEspOtaClientSecure.stop(); + } + else + { + debugPrintln(String(F("ESPFW: Attempting firmware update from HTTP host: ")) + espOtaUrl); + espOtaUrlReturnCode = ESPhttpUpdate.update(wifiClient, espOtaUrl); + } + + switch (espOtaUrlReturnCode) + { + case HTTP_UPDATE_FAILED: + debugPrintln(String(F("ESPFW: HTTP_UPDATE_FAILED error ")) + String(ESPhttpUpdate.getLastError()) + " " + ESPhttpUpdate.getLastErrorString()); + nextionSendCmd("vis 4,0"); + nextionSetAttr("p[0].b[1].txt", "\"HASPone update:\\r FAILED\\rerror: " + ESPhttpUpdate.getLastErrorString() + "\""); + break; + + case HTTP_UPDATE_NO_UPDATES: + debugPrintln(F("ESPFW: HTTP_UPDATE_NO_UPDATES")); + nextionSendCmd("vis 4,0"); + nextionSetAttr("p[0].b[1].txt", "\"HASPone update:\\rNo update\""); + break; + + case HTTP_UPDATE_OK: + debugPrintln(F("ESPFW: HTTP_UPDATE_OK")); + nextionSetAttr("p[0].b[1].txt", "\"\\rHASPone update:\\r\\r Complete!\\rRestarting.\""); + nextionSendCmd("vis 4,1"); + delay(1000); + espReset(); + } + delay(1000); + nextionSendCmd("page " + String(nextionActivePage)); +} + +//////////////////////////////////////////////////////////////////////////////////////////////////// +void espReset() +{ + debugPrintln(F("RESET: HASPone reset")); + if (mqttClient.connected()) + { + mqttClient.publish(mqttStateJSONTopic, String(F("{\"event_type\":\"hasp_device\",\"event\":\"offline\"}"))); + debugPrintln(String(F("MQTT OUT: '")) + mqttStateJSONTopic + String(F(" : {\"event_type\":\"hasp_device\",\"event\":\"offline\"}"))); + mqttClient.publish(mqttStatusTopic, "OFF", true, 0); + mqttClient.disconnect(); + debugPrintln(String(F("MQTT OUT: '")) + mqttStatusTopic + String(F("' : 'OFF'"))); + } + debugPrintln(F("HMI: Rebooting LCD")); + digitalWrite(nextionResetPin, LOW); + Serial1.print("rest"); + Serial1.write(nextionSuffix, sizeof(nextionSuffix)); + Serial1.flush(); + delay(500); + ESP.reset(); + delay(5000); +} + +//////////////////////////////////////////////////////////////////////////////////////////////////// +void configRead() +{ // Read saved config.json from SPIFFS + debugPrintln(F("SPIFFS: mounting SPIFFS")); + if (SPIFFS.begin()) + { + if (SPIFFS.exists("/config.json")) + { // File exists, reading and loading + debugPrintln(F("SPIFFS: reading /config.json")); + // debugPrintFile("/config.json"); + File configFile = SPIFFS.open("/config.json", "r"); + if (configFile) + { + DynamicJsonDocument jsonConfigValues(1536); + DeserializationError jsonError = deserializeJson(jsonConfigValues, configFile); + + if (jsonError) + { // Couldn't parse the saved config + debugPrintln(String(F("SPIFFS: [ERROR] Failed to parse /config.json: ")) + String(jsonError.c_str())); + } + else + { + if (!jsonConfigValues["mqttServer"].isNull()) + { + strcpy(mqttServer, jsonConfigValues["mqttServer"]); + } + if (!jsonConfigValues["mqttPort"].isNull()) + { + strcpy(mqttPort, jsonConfigValues["mqttPort"]); + } + if (!jsonConfigValues["mqttUser"].isNull()) + { + strcpy(mqttUser, jsonConfigValues["mqttUser"]); + } + if (!jsonConfigValues["mqttPassword"].isNull()) + { + strcpy(mqttPassword, jsonConfigValues["mqttPassword"]); + } + if (!jsonConfigValues["mqttFingerprint"].isNull()) + { + strcpy(mqttFingerprint, jsonConfigValues["mqttFingerprint"]); + } + if (!jsonConfigValues["haspNode"].isNull()) + { + strcpy(haspNode, jsonConfigValues["haspNode"]); + } + if (!jsonConfigValues["groupName"].isNull()) + { + strcpy(groupName, jsonConfigValues["groupName"]); + } + if (!jsonConfigValues["configUser"].isNull()) + { + strcpy(configUser, jsonConfigValues["configUser"]); + } + if (!jsonConfigValues["configPassword"].isNull()) + { + strcpy(configPassword, jsonConfigValues["configPassword"]); + } + if (!jsonConfigValues["hassDiscovery"].isNull()) + { + strcpy(hassDiscovery, jsonConfigValues["hassDiscovery"]); + } + if (strcmp(hassDiscovery, "") == 0) + { // Cover off any edge case where this value winds up being empty + debugPrintln(F("SPIFFS: [WARNING] /config.json has empty hassDiscovery value, setting to 'homeassistant'")); + strcpy(hassDiscovery, "homeassistant"); + } + if (!jsonConfigValues["nextionBaud"].isNull()) + { + strcpy(nextionBaud, jsonConfigValues["nextionBaud"]); + } + if (strcmp(nextionBaud, "") == 0) + { // Cover off any edge case where this value winds up being empty + debugPrintln(F("SPIFFS: [WARNING] /config.json has empty nextionBaud value, setting to '115200'")); + strcpy(nextionBaud, "115200"); + } + if (!jsonConfigValues["nextionMaxPages"].isNull()) + { + nextionMaxPages = jsonConfigValues["nextionMaxPages"]; + } + if (nextionMaxPages < 1) + { // Cover off any edge case where this value winds up being zero or negative + debugPrintln(F("SPIFFS: [WARNING] /config.json has nextionMaxPages value of zero or negative, setting to '11'")); + nextionMaxPages = 11; + } + if (!jsonConfigValues["motionPinConfig"].isNull()) + { + strcpy(motionPinConfig, jsonConfigValues["motionPinConfig"]); + } + if (!jsonConfigValues["mqttTlsEnabled"].isNull()) + { + if (jsonConfigValues["mqttTlsEnabled"]) + { + mqttTlsEnabled = true; + } + else + { + mqttTlsEnabled = false; + } + } + if (!jsonConfigValues["debugSerialEnabled"].isNull()) + { + if (jsonConfigValues["debugSerialEnabled"]) + { + debugSerialEnabled = true; + } + else + { + debugSerialEnabled = false; + } + } + if (!jsonConfigValues["debugTelnetEnabled"].isNull()) + { + if (jsonConfigValues["debugTelnetEnabled"]) + { + debugTelnetEnabled = true; + } + else + { + debugTelnetEnabled = false; + } + } + if (!jsonConfigValues["mdnsEnabled"].isNull()) + { + if (jsonConfigValues["mdnsEnabled"]) + { + mdnsEnabled = true; + } + else + { + mdnsEnabled = false; + } + } + if (!jsonConfigValues["beepEnabled"].isNull()) + { + if (jsonConfigValues["beepEnabled"]) + { + beepEnabled = true; + } + else + { + beepEnabled = false; + } + } + if (!jsonConfigValues["ignoreTouchWhenOff"].isNull()) + { + if (jsonConfigValues["ignoreTouchWhenOff"]) + { + ignoreTouchWhenOff = true; + } + else + { + ignoreTouchWhenOff = false; + } + } + String jsonConfigValuesStr; + serializeJson(jsonConfigValues, jsonConfigValuesStr); + debugPrintln(String(F("SPIFFS: read ")) + String(configFile.size()) + String(F(" bytes and parsed json:")) + jsonConfigValuesStr); + } + } + else + { + debugPrintln(F("SPIFFS: [ERROR] Failed to read /config.json")); + } + } + else + { + debugPrintln(F("SPIFFS: [WARNING] /config.json not found, will be created on first config save")); + } + } + else + { + debugPrintln(F("SPIFFS: [ERROR] Failed to mount FS")); + } +} + +//////////////////////////////////////////////////////////////////////////////////////////////////// +void configSaveCallback() +{ // Callback notifying us of the need to save config + debugPrintln(F("SPIFFS: Configuration changed, flagging for save")); + shouldSaveConfig = true; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////// +void configSave() +{ // Save the custom parameters to config.json + debugPrintln(F("SPIFFS: Saving config")); + DynamicJsonDocument jsonConfigValues(2048); + + jsonConfigValues["mqttServer"] = mqttServer; + jsonConfigValues["mqttPort"] = mqttPort; + jsonConfigValues["mqttUser"] = mqttUser; + jsonConfigValues["mqttPassword"] = mqttPassword; + jsonConfigValues["mqttTlsEnabled"] = mqttTlsEnabled; + jsonConfigValues["mqttFingerprint"] = mqttFingerprint; + jsonConfigValues["haspNode"] = haspNode; + jsonConfigValues["groupName"] = groupName; + jsonConfigValues["configUser"] = configUser; + jsonConfigValues["configPassword"] = configPassword; + jsonConfigValues["hassDiscovery"] = hassDiscovery; + jsonConfigValues["nextionBaud"] = nextionBaud; + jsonConfigValues["nextionMaxPages"] = nextionMaxPages; + jsonConfigValues["motionPinConfig"] = motionPinConfig; + jsonConfigValues["debugSerialEnabled"] = debugSerialEnabled; + jsonConfigValues["debugTelnetEnabled"] = debugTelnetEnabled; + jsonConfigValues["mdnsEnabled"] = mdnsEnabled; + jsonConfigValues["beepEnabled"] = beepEnabled; + jsonConfigValues["ignoreTouchWhenOff"] = ignoreTouchWhenOff; + + debugPrintln(String(F("SPIFFS: mqttServer = ")) + String(mqttServer)); + debugPrintln(String(F("SPIFFS: mqttPort = ")) + String(mqttPort)); + debugPrintln(String(F("SPIFFS: mqttUser = ")) + String(mqttUser)); + debugPrintln(String(F("SPIFFS: mqttPassword = ")) + String(mqttPassword)); + debugPrintln(String(F("SPIFFS: mqttTlsEnabled = ")) + String(mqttTlsEnabled)); + debugPrintln(String(F("SPIFFS: mqttFingerprint = ")) + String(mqttFingerprint)); + debugPrintln(String(F("SPIFFS: haspNode = ")) + String(haspNode)); + debugPrintln(String(F("SPIFFS: groupName = ")) + String(groupName)); + debugPrintln(String(F("SPIFFS: configUser = ")) + String(configUser)); + debugPrintln(String(F("SPIFFS: configPassword = ")) + String(configPassword)); + debugPrintln(String(F("SPIFFS: hassDiscovery = ")) + String(hassDiscovery)); + debugPrintln(String(F("SPIFFS: nextionBaud = ")) + String(nextionBaud)); + debugPrintln(String(F("SPIFFS: nextionMaxPages = ")) + String(nextionMaxPages)); + debugPrintln(String(F("SPIFFS: motionPinConfig = ")) + String(motionPinConfig)); + debugPrintln(String(F("SPIFFS: debugSerialEnabled = ")) + String(debugSerialEnabled)); + debugPrintln(String(F("SPIFFS: debugTelnetEnabled = ")) + String(debugTelnetEnabled)); + debugPrintln(String(F("SPIFFS: mdnsEnabled = ")) + String(mdnsEnabled)); + debugPrintln(String(F("SPIFFS: beepEnabled = ")) + String(beepEnabled)); + debugPrintln(String(F("SPIFFS: ignoreTouchWhenOff = ")) + String(ignoreTouchWhenOff)); + + File configFile = SPIFFS.open("/config.json", "w"); + if (!configFile) + { + debugPrintln(F("SPIFFS: Failed to open config file for writing")); + } + else + { + serializeJson(jsonConfigValues, configFile); + configFile.print("\n\n\n"); + configFile.flush(); + delay(10); + configFile.close(); + } + debugPrintFile("/config.json"); + shouldSaveConfig = false; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////// +void configClearSaved() +{ // Clear out all local storage + nextionSetAttr("dims", "100"); + nextionSendCmd("page 0"); + nextionSetAttr("p[0].b[1].txt", "\"Resetting\\rsystem...\""); + debugPrintln(F("RESET: Formatting SPIFFS")); + SPIFFS.format(); + debugPrintln(F("RESET: Clearing WiFiManager settings...")); + WiFi.disconnect(); + WiFiManager wifiManager; + wifiManager.resetSettings(); + EEPROM.begin(512); + debugPrintln(F("Clearing EEPROM...")); + for (uint16_t i = 0; i < EEPROM.length(); i++) + { + EEPROM.write(i, 0); + } + nextionSetAttr("p[0].b[1].txt", "\"Rebooting\\rsystem...\""); + debugPrintln(F("RESET: Rebooting device")); + espReset(); +} + +//////////////////////////////////////////////////////////////////////////////////////////////////// +void webHandleNotFound() +{ // webServer 404 + debugPrintln(String(F("HTTP: Sending 404 to client connected from: ")) + webServer.client().remoteIP().toString()); + String httpHeader = FPSTR(HTTP_HEAD_START); + httpHeader.replace("{v}", "HASPone " + String(haspNode) + " 404"); + webServer.setContentLength(CONTENT_LENGTH_UNKNOWN); + webServer.send(404, "text/html", httpHeader); + webServer.sendContent_P(HTTP_SCRIPT); + webServer.sendContent_P(HTTP_STYLE); + webServer.sendContent_P(HASP_STYLE); + webServer.sendContent_P(HTTP_HEAD_END); + webServer.sendContent(F("

404: File Not Found

One of us appears to have done something horribly wrong.
URI: ")); + webServer.sendContent(webServer.uri()); + webServer.sendContent(F("
Method: ")); + webServer.sendContent((webServer.method() == HTTP_GET) ? F("GET") : F("POST")); + webServer.sendContent(F("
Arguments: ")); + webServer.sendContent(String(webServer.args())); + for (uint8_t i = 0; i < webServer.args(); i++) + { + webServer.sendContent(F("
")); + webServer.sendContent(String(webServer.argName(i))); + webServer.sendContent(F(": ")); + webServer.sendContent(String(webServer.arg(i))); + } + webServer.sendContent(""); +} + +//////////////////////////////////////////////////////////////////////////////////////////////////// +void webHandleRoot() +{ // http://plate01/ + if (configPassword[0] != '\0') + { // Request HTTP auth if configPassword is set + if (!webServer.authenticate(configUser, configPassword)) + { + return webServer.requestAuthentication(); + } + } + debugPrintln(String(F("HTTP: Sending root page to client connected from: ")) + webServer.client().remoteIP().toString()); + + String httpHeader = FPSTR(HTTP_HEAD_START); + httpHeader.replace("{v}", "HASPone " + String(haspNode)); + webServer.setContentLength(CONTENT_LENGTH_UNKNOWN); + webServer.send(200, "text/html", httpHeader); + webServer.sendContent(httpHeader); + webServer.sendContent_P(HTTP_SCRIPT); + webServer.sendContent_P(HTTP_STYLE); + webServer.sendContent_P(HASP_STYLE); + webServer.sendContent_P(HTTP_HEAD_END); + + webServer.sendContent(F("

")); + webServer.sendContent(haspNode); + webServer.sendContent(F("

")); + + webServer.sendContent(F("
")); + webServer.sendContent(F("WiFi SSID (required)
WiFi Password (required)")); + webServer.sendContent(F("

HASPone Node Name (required. lowercase letters, numbers, and _ only)
Group Name (required)

MQTT Broker (required, IP address is preferred)
MQTT Port (required)
MQTT User (optional)
MQTT Password (optional)")); + + webServer.sendContent(F("
MQTT TLS enabled:
MQTT TLS Fingerpint (leave blank to disable fingerprint checking)")); + + webServer.sendContent(F("

HASPone Admin Username (optional)
HASPone Admin Password (optional)

Home Assistant discovery topic (required, should probably be \"homeassistant\")
Nextion project page count (required, probably \"11\")

")); + // Big menu of possible serial speeds + if ((lcdVersion != 1) && (lcdVersion != 2)) + { // HASPone lcdVersion 1 and 2 have `bauds=115200` in the pre-init script of page 0. Don't show this option if either of those two versions are running. + webServer.sendContent(F("LCD Serial Speed: 
")); + } + + webServer.sendContent(F("USB serial debug output enabled:
Telnet debug output enabled:
mDNS enabled:
Motion Sensor Pin: ")); + webServer.sendContent(F("
Keypress beep enabled on D2:
Ignore touchevents when backlight is off:

")); + + if (updateEspAvailable) + { + webServer.sendContent(F("

HASPone Update available!

")); + webServer.sendContent(F("
")); + webServer.sendContent(F("
")); + } + + webServer.sendContent(F("
")); + webServer.sendContent(F("
")); + + webServer.sendContent(F("
")); + webServer.sendContent(F("
")); + + webServer.sendContent(F("
")); + webServer.sendContent(F("
")); + + webServer.sendContent(F("
")); + webServer.sendContent(F("
")); + + webServer.sendContent(F("
MQTT Status: ")); + if (mqttClient.connected()) + { // Check MQTT connection + webServer.sendContent(F("Connected")); + } + else + { + webServer.sendContent(F("Disconnected
MQTT return code: ")); + webServer.sendContent(String(mqttClient.returnCode())); + webServer.sendContent(F("
MQTT last error: ")); + webServer.sendContent(String(mqttClient.lastError())); + webServer.sendContent(F("
MQTT broker ping check: ")); + if (mqttPingCheck) + { + webServer.sendContent(F("SUCCESS")); + } + else + { + webServer.sendContent(F("FAILED")); + } + webServer.sendContent(F("
MQTT broker port check: ")); + if (mqttPortCheck) + { + webServer.sendContent(F("SUCCESS")); + } + else + { + webServer.sendContent(F("FAILED")); + } + } + webServer.sendContent(F("
MQTT ClientID: ")); + if (mqttClientId != "") + { + webServer.sendContent(mqttClientId); + } + webServer.sendContent(F("
HASPone FW Version: ")); + webServer.sendContent(String(haspVersion)); + webServer.sendContent(F("
LCD Model: ")); + if (nextionModel != "") + { + webServer.sendContent(nextionModel); + } + webServer.sendContent(F("
LCD FW Version: ")); + webServer.sendContent(String(lcdVersion)); + webServer.sendContent(F("
LCD Active Page: ")); + webServer.sendContent(String(nextionActivePage)); + webServer.sendContent(F("
LCD Serial Speed: ")); + webServer.sendContent(nextionBaud); + webServer.sendContent(F("
CPU Frequency: ")); + webServer.sendContent(String(ESP.getCpuFreqMHz())); + webServer.sendContent(F("MHz")); + webServer.sendContent(F("
Sketch Size: ")); + webServer.sendContent(String(ESP.getSketchSize())); + webServer.sendContent(F(" bytes")); + webServer.sendContent(F("
Free Sketch Space: ")); + webServer.sendContent(String(ESP.getFreeSketchSpace())); + webServer.sendContent(F(" bytes")); + webServer.sendContent(F("
Heap Free: ")); + webServer.sendContent(String(ESP.getFreeHeap())); + webServer.sendContent(F("
Heap Fragmentation: ")); + webServer.sendContent(String(ESP.getHeapFragmentation())); + webServer.sendContent(F("
ESP core version: ")); + webServer.sendContent(ESP.getCoreVersion()); + webServer.sendContent(F("
IP Address: ")); + webServer.sendContent(WiFi.localIP().toString()); + webServer.sendContent(F("
Signal Strength: ")); + webServer.sendContent(String(WiFi.RSSI())); + webServer.sendContent(F("
Uptime: ")); + webServer.sendContent(String(long(millis() / 1000))); + webServer.sendContent(F("
Last reset: ")); + webServer.sendContent(ESP.getResetInfo()); + webServer.sendContent_P(HTTP_END); + webServer.sendContent(""); +} + +//////////////////////////////////////////////////////////////////////////////////////////////////// +void webHandleSaveConfig() +{ // http://plate01/saveConfig + if (configPassword[0] != '\0') + { // Request HTTP auth if configPassword is set + if (!webServer.authenticate(configUser, configPassword)) + { + return webServer.requestAuthentication(); + } + } + debugPrintln(String(F("HTTP: Sending /saveConfig page to client connected from: ")) + webServer.client().remoteIP().toString()); + String httpHeader = FPSTR(HTTP_HEAD_START); + httpHeader.replace("{v}", "HASPone " + String(haspNode) + " Saving configuration"); + webServer.setContentLength(CONTENT_LENGTH_UNKNOWN); + webServer.send(200, "text/html", httpHeader); + webServer.sendContent_P(HTTP_SCRIPT); + webServer.sendContent_P(HTTP_STYLE); + webServer.sendContent_P(HASP_STYLE); + webServer.sendContent_P(HTTP_HEAD_END); + + bool shouldSaveWifi = false; + // Check required values + if ((webServer.arg("wifiSSID") != "") && (webServer.arg("wifiSSID") != String(WiFi.SSID()))) + { // Handle WiFi SSID + shouldSaveConfig = true; + shouldSaveWifi = true; + webServer.arg("wifiSSID").toCharArray(wifiSSID, 32); + } + if ((webServer.arg("wifiPass") != String(wifiPass)) && (webServer.arg("wifiPass") != String("********"))) + { // Handle WiFi password + shouldSaveConfig = true; + shouldSaveWifi = true; + webServer.arg("wifiPass").toCharArray(wifiPass, 64); + } + + if (webServer.arg("haspNode") != "" && webServer.arg("haspNode") != String(haspNode)) + { // Handle haspNode + shouldSaveConfig = true; + String lowerHaspNode = webServer.arg("haspNode"); + lowerHaspNode.toLowerCase(); + lowerHaspNode.toCharArray(haspNode, 16); + } + if (webServer.arg("groupName") != "" && webServer.arg("groupName") != String(groupName)) + { // Handle groupName + shouldSaveConfig = true; + webServer.arg("groupName").toCharArray(groupName, 16); + } + + if (webServer.arg("mqttServer") != "" && webServer.arg("mqttServer") != String(mqttServer)) + { // Handle mqttServer + shouldSaveConfig = true; + webServer.arg("mqttServer").toCharArray(mqttServer, 128); + } + if (webServer.arg("mqttPort") != "" && webServer.arg("mqttPort") != String(mqttPort)) + { // Handle mqttPort + shouldSaveConfig = true; + webServer.arg("mqttPort").toCharArray(mqttPort, 6); + } + if (webServer.arg("mqttUser") != String(mqttUser)) + { // Handle mqttUser + shouldSaveConfig = true; + webServer.arg("mqttUser").toCharArray(mqttUser, 128); + } + if (webServer.arg("mqttPassword") != String("********")) + { // Handle mqttPassword + shouldSaveConfig = true; + webServer.arg("mqttPassword").toCharArray(mqttPassword, 128); + } + if ((webServer.arg("mqttTlsEnabled") == String("on")) && !mqttTlsEnabled) + { // mqttTlsEnabled was disabled but should now be enabled + shouldSaveConfig = true; + mqttTlsEnabled = true; + } + else if ((webServer.arg("mqttTlsEnabled") == String("")) && mqttTlsEnabled) + { // mqttTlsEnabled was enabled but should now be disabled + shouldSaveConfig = true; + mqttTlsEnabled = false; + } + if (webServer.arg("mqttFingerprint") != String(mqttFingerprint)) + { // Handle mqttFingerprint + shouldSaveConfig = true; + webServer.arg("mqttFingerprint").toCharArray(mqttFingerprint, 60); + } + if (webServer.arg("configUser") != String(configUser)) + { // Handle configUser + shouldSaveConfig = true; + webServer.arg("configUser").toCharArray(configUser, 32); + } + if (webServer.arg("configPassword") != String("********")) + { // Handle configPassword + shouldSaveConfig = true; + webServer.arg("configPassword").toCharArray(configPassword, 32); + } + if (webServer.arg("hassDiscovery") != String(hassDiscovery)) + { // Handle hassDiscovery + shouldSaveConfig = true; + webServer.arg("hassDiscovery").toCharArray(hassDiscovery, 128); + } + if ((webServer.arg("nextionMaxPages") != String(nextionMaxPages)) && (webServer.arg("nextionMaxPages").toInt() < 256) && (webServer.arg("nextionMaxPages").toInt() > 0)) + { + shouldSaveConfig = true; + nextionMaxPages = webServer.arg("nextionMaxPages").toInt(); + } + if (webServer.arg("nextionBaud") != String(nextionBaud)) + { // Handle nextionBaud + shouldSaveConfig = true; + webServer.arg("nextionBaud").toCharArray(nextionBaud, 7); + } + if (webServer.arg("motionPinConfig") != String(motionPinConfig)) + { // Handle motionPinConfig + shouldSaveConfig = true; + webServer.arg("motionPinConfig").toCharArray(motionPinConfig, 3); + } + if ((webServer.arg("debugSerialEnabled") == String("on")) && !debugSerialEnabled) + { // debugSerialEnabled was disabled but should now be enabled + shouldSaveConfig = true; + debugSerialEnabled = true; + } + else if ((webServer.arg("debugSerialEnabled") == String("")) && debugSerialEnabled) + { // debugSerialEnabled was enabled but should now be disabled + shouldSaveConfig = true; + debugSerialEnabled = false; + } + if ((webServer.arg("debugTelnetEnabled") == String("on")) && !debugTelnetEnabled) + { // debugTelnetEnabled was disabled but should now be enabled + shouldSaveConfig = true; + debugTelnetEnabled = true; + } + else if ((webServer.arg("debugTelnetEnabled") == String("")) && debugTelnetEnabled) + { // debugTelnetEnabled was enabled but should now be disabled + shouldSaveConfig = true; + debugTelnetEnabled = false; + } + if ((webServer.arg("mdnsEnabled") == String("on")) && !mdnsEnabled) + { // mdnsEnabled was disabled but should now be enabled + shouldSaveConfig = true; + mdnsEnabled = true; + } + else if ((webServer.arg("mdnsEnabled") == String("")) && mdnsEnabled) + { // mdnsEnabled was enabled but should now be disabled + shouldSaveConfig = true; + mdnsEnabled = false; + } + if ((webServer.arg("beepEnabled") == String("on")) && !beepEnabled) + { // beepEnabled was disabled but should now be enabled + shouldSaveConfig = true; + beepEnabled = true; + } + else if ((webServer.arg("beepEnabled") == String("")) && beepEnabled) + { // beepEnabled was enabled but should now be disabled + shouldSaveConfig = true; + beepEnabled = false; + } + if ((webServer.arg("ignoreTouchWhenOff") == String("on")) && !ignoreTouchWhenOff) + { // ignoreTouchWhenOff was disabled but should now be enabled + shouldSaveConfig = true; + ignoreTouchWhenOff = true; + } + else if ((webServer.arg("ignoreTouchWhenOff") == String("")) && ignoreTouchWhenOff) + { // ignoreTouchWhenOff was enabled but should now be disabled + shouldSaveConfig = true; + ignoreTouchWhenOff = false; + } + + if (shouldSaveConfig) + { // Config updated, notify user and trigger write to SPIFFS + + webServer.sendContent(F("")); + webServer.sendContent_P(HTTP_HEAD_END); + webServer.sendContent(F("

")); + webServer.sendContent(haspNode); + webServer.sendContent(F("

")); + webServer.sendContent(F("
Saving updated configuration values and restarting device")); + webServer.sendContent_P(HTTP_END); + webServer.sendContent(F("")); + configSave(); + if (shouldSaveWifi) + { + debugPrintln(String(F("CONFIG: Attempting connection to SSID: ")) + webServer.arg("wifiSSID")); + espWifiConnect(); + } + espReset(); + } + else + { // No change found, notify user and link back to config page + webServer.sendContent(F("")); + webServer.sendContent_P(HTTP_HEAD_END); + webServer.sendContent(F("

")); + webServer.sendContent(haspNode); + webServer.sendContent(F("

")); + webServer.sendContent(F("
No changes found, returning to home page")); + webServer.sendContent_P(HTTP_END); + webServer.sendContent(F("")); + } +} + +//////////////////////////////////////////////////////////////////////////////////////////////////// +void webHandleResetConfig() +{ // http://plate01/resetConfig + if (configPassword[0] != '\0') + { // Request HTTP auth if configPassword is set + if (!webServer.authenticate(configUser, configPassword)) + { + return webServer.requestAuthentication(); + } + } + debugPrintln(String(F("HTTP: Sending /resetConfig page to client connected from: ")) + webServer.client().remoteIP().toString()); + String httpHeader = FPSTR(HTTP_HEAD_START); + httpHeader.replace("{v}", "HASPone " + String(haspNode) + " Resetting configuration"); + webServer.setContentLength(CONTENT_LENGTH_UNKNOWN); + webServer.send(200, "text/html", httpHeader); + webServer.sendContent_P(HTTP_SCRIPT); + webServer.sendContent_P(HTTP_STYLE); + webServer.sendContent_P(HASP_STYLE); + webServer.sendContent_P(HTTP_HEAD_END); + + if (webServer.arg("confirm") == "yes") + { // User has confirmed, so reset everything + webServer.sendContent(F("

")); + webServer.sendContent(haspNode); + webServer.sendContent(F("

Resetting all saved settings and restarting device into WiFi AP mode")); + webServer.sendContent_P(HTTP_END); + webServer.sendContent(""); + delay(100); + configClearSaved(); + } + else + { + webServer.sendContent(F("

Warning

This process will reset all settings to the default values and restart the device. You may need to connect to the WiFi AP displayed on the panel to re-configure the device before accessing it again.")); + webServer.sendContent(F("


")); + webServer.sendContent(F("

")); + webServer.sendContent(F("


")); + webServer.sendContent(F("
")); + webServer.sendContent_P(HTTP_END); + webServer.sendContent(""); + } +} + +//////////////////////////////////////////////////////////////////////////////////////////////////// +void webHandleResetBacklight() +{ // http://plate01/resetBacklight + if (configPassword[0] != '\0') + { // Request HTTP auth if configPassword is set + if (!webServer.authenticate(configUser, configPassword)) + { + return webServer.requestAuthentication(); + } + } + debugPrintln(String(F("HTTP: Sending /resetBacklight page to client connected from: ")) + webServer.client().remoteIP().toString()); + String httpHeader = FPSTR(HTTP_HEAD_START); + httpHeader.replace("{v}", "HASPone " + String(haspNode) + " Backlight reset"); + webServer.setContentLength(CONTENT_LENGTH_UNKNOWN); + webServer.send(200, "text/html", httpHeader); + webServer.sendContent_P(HTTP_SCRIPT); + webServer.sendContent_P(HTTP_STYLE); + webServer.sendContent_P(HASP_STYLE); + webServer.sendContent(F("")); + webServer.sendContent_P(HTTP_HEAD_END); + + webServer.sendContent(F("

")); + webServer.sendContent(haspNode); + webServer.sendContent(F("

")); + webServer.sendContent(F("
Resetting backlight to 100%")); + webServer.sendContent_P(HTTP_END); + webServer.sendContent(""); + debugPrintln(F("HTTP: Resetting backlight to 100%")); + nextionSetAttr("dims", "100"); +} + +//////////////////////////////////////////////////////////////////////////////////////////////////// +void webHandleFirmware() +{ // http://plate01/firmware + if (configPassword[0] != '\0') + { // Request HTTP auth if configPassword is set + if (!webServer.authenticate(configUser, configPassword)) + { + return webServer.requestAuthentication(); + } + } + debugPrintln(String(F("HTTP: Sending /firmware page to client connected from: ")) + webServer.client().remoteIP().toString()); + String httpHeader = FPSTR(HTTP_HEAD_START); + httpHeader.replace("{v}", "HASPone " + String(haspNode) + " Firmware updates"); + webServer.setContentLength(CONTENT_LENGTH_UNKNOWN); + webServer.send(200, "text/html", httpHeader); + webServer.sendContent_P(HTTP_SCRIPT); + webServer.sendContent_P(HTTP_STYLE); + webServer.sendContent_P(HASP_STYLE); + webServer.sendContent_P(HTTP_HEAD_END); + + webServer.sendContent(F("

")); + webServer.sendContent(haspNode); + webServer.sendContent(F(" Firmware updates

Note: If updating firmware for both the ESP8266 and the Nextion LCD, you'll want to update the ESP8266 first followed by the Nextion LCD

")); + + // Display main firmware page + webServer.sendContent(F("
")); + if (updateEspAvailable) + { + webServer.sendContent(F("HASPone ESP8266 update available!")); + } + webServer.sendContent(F("
Update ESP8266 from URL")); + webServer.sendContent(F("


")); + + webServer.sendContent(F("
")); + webServer.sendContent(F("Update ESP8266 from file")); + webServer.sendContent(F("

")); + + webServer.sendContent(F("


WARNING!

")); + webServer.sendContent(F("Nextion LCD firmware updates can be risky. If interrupted, the LCD will display an error message until a successful firmware update has completed. ")); + webServer.sendContent(F("

Note: Failed LCD firmware updates on HASPone hardware prior to v1.0 may require a hard power cycle of the device, via a circuit breaker or by physically disconnecting the device.")); + + webServer.sendContent(F("

")); + if (updateLcdAvailable) + { + webServer.sendContent(F("HASPone LCD update available!")); + } + webServer.sendContent(F("
Update Nextion LCD from URL")); + webServer.sendContent(F("


")); + + webServer.sendContent(F("
")); + webServer.sendContent(F("
Update Nextion LCD from file")); + webServer.sendContent(F("

")); + + // Javascript to collect the filesize of the LCD upload and send it to /tftFileSize + webServer.sendContent(F("")); + + webServer.sendContent_P(HTTP_END); + webServer.sendContent(""); +} + +//////////////////////////////////////////////////////////////////////////////////////////////////// +void webHandleEspFirmware() +{ // http://plate01/espfirmware + if (configPassword[0] != '\0') + { // Request HTTP auth if configPassword is set + if (!webServer.authenticate(configUser, configPassword)) + { + return webServer.requestAuthentication(); + } + } + + debugPrintln(String(F("HTTP: Sending /espfirmware page to client connected from: ")) + webServer.client().remoteIP().toString()); + String httpHeader = FPSTR(HTTP_HEAD_START); + httpHeader.replace("{v}", "HASPone " + String(haspNode) + " ESP8266 firmware update"); + webServer.setContentLength(CONTENT_LENGTH_UNKNOWN); + webServer.send(200, "text/html", httpHeader); + webServer.sendContent_P(HTTP_SCRIPT); + webServer.sendContent_P(HTTP_STYLE); + webServer.sendContent_P(HASP_STYLE); + webServer.sendContent(F("")); + webServer.sendContent_P(HTTP_HEAD_END); + webServer.sendContent(F("

")); + webServer.sendContent(haspNode); + webServer.sendContent(F(" ESP8266 firmware update

")); + webServer.sendContent(F("
Updating ESP firmware from: ")); + webServer.sendContent(webServer.arg("espFirmware")); + webServer.sendContent_P(HTTP_END); + webServer.sendContent(""); + + debugPrintln("ESPFW: Attempting ESP firmware update from: " + String(webServer.arg("espFirmware"))); + espStartOta(webServer.arg("espFirmware")); +} + +//////////////////////////////////////////////////////////////////////////////////////////////////// +void webHandleLcdUpload() +{ // http://plate01/lcdupload + // Upload firmware to the Nextion LCD via HTTP upload + + if (configPassword[0] != '\0') + { // Request HTTP auth if configPassword is set + if (!webServer.authenticate(configUser, configPassword)) + { + return webServer.requestAuthentication(); + } + } + + static uint32_t lcdOtaTransferred = 0; + static uint32_t lcdOtaRemaining; + static uint16_t lcdOtaParts; + const uint32_t lcdOtaTimeout = 30000; // timeout for receiving new data in milliseconds + static uint32_t lcdOtaTimer = 0; // timer for upload timeout + + HTTPUpload &upload = webServer.upload(); + + if (tftFileSize == 0) + { + debugPrintln(String(F("LCDOTA: FAILED, no filesize sent."))); + String httpHeader = FPSTR(HTTP_HEAD_START); + httpHeader.replace("{v}", "HASPone " + String(haspNode) + " LCD update error"); + webServer.setContentLength(CONTENT_LENGTH_UNKNOWN); + webServer.send(200, "text/html", httpHeader); + webServer.sendContent_P(HTTP_SCRIPT); + webServer.sendContent_P(HTTP_STYLE); + webServer.sendContent_P(HASP_STYLE); + webServer.sendContent(F("")); + webServer.sendContent_P(HTTP_HEAD_END); + webServer.sendContent(F("

")); + webServer.sendContent(haspNode); + webServer.sendContent(F(" LCD update FAILED

")); + webServer.sendContent(F("No update file size reported. You must use a modern browser with Javascript enabled.")); + webServer.sendContent_P(HTTP_END); + webServer.sendContent(""); + } + else if ((lcdOtaTimer > 0) && ((millis() - lcdOtaTimer) > lcdOtaTimeout)) + { // Our timer expired so reset + debugPrintln(F("LCDOTA: ERROR: LCD upload timeout. Restarting.")); + espReset(); + } + else if (upload.status == UPLOAD_FILE_START) + { + WiFiUDP::stopAll(); // Keep mDNS responder from breaking things + + debugPrintln(String(F("LCDOTA: Attempting firmware upload"))); + debugPrintln(String(F("LCDOTA: upload.filename: ")) + String(upload.filename)); + debugPrintln(String(F("LCDOTA: TFTfileSize: ")) + String(tftFileSize)); + + lcdOtaRemaining = tftFileSize; + lcdOtaParts = (lcdOtaRemaining / 4096) + 1; + debugPrintln(String(F("LCDOTA: File upload beginning. Size ")) + String(lcdOtaRemaining) + String(F(" bytes in ")) + String(lcdOtaParts) + String(F(" 4k chunks."))); + + Serial1.write(nextionSuffix, sizeof(nextionSuffix)); // Send empty command to LCD + Serial1.flush(); + nextionHandleInput(); + + String lcdOtaNextionCmd = "whmi-wri " + String(tftFileSize) + "," + String(nextionBaud) + ",0"; + debugPrintln(String(F("LCDOTA: Sending LCD upload command: ")) + lcdOtaNextionCmd); + Serial1.print(lcdOtaNextionCmd); + Serial1.write(nextionSuffix, sizeof(nextionSuffix)); + Serial1.flush(); + + if (nextionOtaResponse()) + { + debugPrintln(F("LCDOTA: LCD upload command accepted")); + } + else + { + debugPrintln(F("LCDOTA: LCD upload command FAILED.")); + espReset(); + } + lcdOtaTimer = millis(); + } + else if (upload.status == UPLOAD_FILE_WRITE) + { // Handle upload data + static int lcdOtaChunkCounter = 0; + static uint16_t lcdOtaPartNum = 0; + static int lcdOtaPercentComplete = 0; + static const uint16_t lcdOtaBufferSize = 1024; // upload data buffer before sending to UART + static uint8_t lcdOtaBuffer[lcdOtaBufferSize] = {}; + uint16_t lcdOtaUploadIndex = 0; + int32_t lcdOtaPacketRemaining = upload.currentSize; + + while (lcdOtaPacketRemaining > 0) + { // Write incoming data to panel as it arrives + // determine chunk size as lowest value of lcdOtaPacketRemaining, lcdOtaBufferSize, or 4096 - lcdOtaChunkCounter + uint16_t lcdOtaChunkSize = 0; + if ((lcdOtaPacketRemaining <= lcdOtaBufferSize) && (lcdOtaPacketRemaining <= (4096 - lcdOtaChunkCounter))) + { + lcdOtaChunkSize = lcdOtaPacketRemaining; + } + else if ((lcdOtaBufferSize <= lcdOtaPacketRemaining) && (lcdOtaBufferSize <= (4096 - lcdOtaChunkCounter))) + { + lcdOtaChunkSize = lcdOtaBufferSize; + } + else + { + lcdOtaChunkSize = 4096 - lcdOtaChunkCounter; + } + + for (uint16_t i = 0; i < lcdOtaChunkSize; i++) + { // Load up the UART buffer + lcdOtaBuffer[i] = upload.buf[lcdOtaUploadIndex]; + lcdOtaUploadIndex++; + } + Serial1.flush(); // Clear out current UART buffer + Serial1.write(lcdOtaBuffer, lcdOtaChunkSize); // And send the most recent data + lcdOtaChunkCounter += lcdOtaChunkSize; + lcdOtaTransferred += lcdOtaChunkSize; + if (lcdOtaChunkCounter >= 4096) + { + Serial1.flush(); + lcdOtaPartNum++; + lcdOtaPercentComplete = (lcdOtaTransferred * 100) / tftFileSize; + lcdOtaChunkCounter = 0; + if (nextionOtaResponse()) + { + debugPrintln(String(F("LCDOTA: Part ")) + String(lcdOtaPartNum) + String(F(" OK, ")) + String(lcdOtaPercentComplete) + String(F("% complete"))); + } + else + { + debugPrintln(String(F("LCDOTA: Part ")) + String(lcdOtaPartNum) + String(F(" FAILED, ")) + String(lcdOtaPercentComplete) + String(F("% complete"))); + } + } + else + { + delay(10); + } + if (lcdOtaRemaining > 0) + { + lcdOtaRemaining -= lcdOtaChunkSize; + } + if (lcdOtaPacketRemaining > 0) + { + lcdOtaPacketRemaining -= lcdOtaChunkSize; + } + } + + if (lcdOtaTransferred >= tftFileSize) + { + if (nextionOtaResponse()) + { + debugPrintln(String(F("LCDOTA: Success, wrote ")) + String(lcdOtaTransferred) + " of " + String(tftFileSize) + " bytes."); + webServer.sendHeader("Location", "/lcdOtaSuccess"); + webServer.send(303); + uint32_t lcdOtaDelay = millis(); + while ((millis() - lcdOtaDelay) < 5000) + { // extra 5sec delay while the LCD handles any local firmware updates from new versions of code sent to it + webServer.handleClient(); + delay(1); + } + espReset(); + } + else + { + debugPrintln(F("LCDOTA: Failure")); + webServer.sendHeader("Location", "/lcdOtaFailure"); + webServer.send(303); + uint32_t lcdOtaDelay = millis(); + while ((millis() - lcdOtaDelay) < 1000) + { // extra 1sec delay for client to grab failure page + webServer.handleClient(); + delay(1); + } + espReset(); + } + } + lcdOtaTimer = millis(); + } + else if (upload.status == UPLOAD_FILE_END) + { // Upload completed + if (lcdOtaTransferred >= tftFileSize) + { + if (nextionOtaResponse()) + { // YAY WE DID IT + debugPrintln(String(F("LCDOTA: Success, wrote ")) + String(lcdOtaTransferred) + " of " + String(tftFileSize) + " bytes."); + webServer.sendHeader("Location", "/lcdOtaSuccess"); + webServer.send(303); + uint32_t lcdOtaDelay = millis(); + while ((millis() - lcdOtaDelay) < 5000) + { // extra 5sec delay while the LCD handles any local firmware updates from new versions of code sent to it + webServer.handleClient(); + yield(); + } + espReset(); + } + else + { + debugPrintln(F("LCDOTA: Failure")); + webServer.sendHeader("Location", "/lcdOtaFailure"); + webServer.send(303); + uint32_t lcdOtaDelay = millis(); + while ((millis() - lcdOtaDelay) < 1000) + { // extra 1sec delay for client to grab failure page + webServer.handleClient(); + yield(); + } + espReset(); + } + } + } + else if (upload.status == UPLOAD_FILE_ABORTED) + { // Something went kablooey + debugPrintln(F("LCDOTA: ERROR: upload.status returned: UPLOAD_FILE_ABORTED")); + debugPrintln(F("LCDOTA: Failure")); + webServer.sendHeader("Location", "/lcdOtaFailure"); + webServer.send(303); + uint32_t lcdOtaDelay = millis(); + while ((millis() - lcdOtaDelay) < 1000) + { // extra 1sec delay for client to grab failure page + webServer.handleClient(); + yield(); + } + espReset(); + } + else + { // Something went weird, we should never get here... + debugPrintln(String(F("LCDOTA: upload.status returned: ")) + String(upload.status)); + debugPrintln(F("LCDOTA: Failure")); + webServer.sendHeader("Location", "/lcdOtaFailure"); + webServer.send(303); + uint32_t lcdOtaDelay = millis(); + while ((millis() - lcdOtaDelay) < 1000) + { // extra 1sec delay for client to grab failure page + webServer.handleClient(); + yield(); + } + espReset(); + } +} + +//////////////////////////////////////////////////////////////////////////////////////////////////// +void webHandleLcdUpdateSuccess() +{ // http://plate01/lcdOtaSuccess + if (configPassword[0] != '\0') + { // Request HTTP auth if configPassword is set + if (!webServer.authenticate(configUser, configPassword)) + { + return webServer.requestAuthentication(); + } + } + debugPrintln(String(F("HTTP: Sending /lcdOtaSuccess page to client connected from: ")) + webServer.client().remoteIP().toString()); + String httpHeader = FPSTR(HTTP_HEAD_START); + httpHeader.replace("{v}", "HASPone " + String(haspNode) + " LCD firmware update success"); + webServer.setContentLength(CONTENT_LENGTH_UNKNOWN); + webServer.send(200, "text/html", httpHeader); + webServer.sendContent_P(HTTP_SCRIPT); + webServer.sendContent_P(HTTP_STYLE); + webServer.sendContent_P(HASP_STYLE); + webServer.sendContent(F("")); + webServer.sendContent_P(HTTP_HEAD_END); + webServer.sendContent(F("

")); + webServer.sendContent(haspNode); + webServer.sendContent(F(" LCD update success

")); + webServer.sendContent(F("Restarting HASwitchPlate to apply changes...")); + webServer.sendContent_P(HTTP_END); + webServer.sendContent(""); +} + +//////////////////////////////////////////////////////////////////////////////////////////////////// +void webHandleLcdUpdateFailure() +{ // http://plate01/lcdOtaFailure + if (configPassword[0] != '\0') + { // Request HTTP auth if configPassword is set + if (!webServer.authenticate(configUser, configPassword)) + { + return webServer.requestAuthentication(); + } + } + debugPrintln(String(F("HTTP: Sending /lcdOtaFailure page to client connected from: ")) + webServer.client().remoteIP().toString()); + String httpHeader = FPSTR(HTTP_HEAD_START); + httpHeader.replace("{v}", "HASPone " + String(haspNode) + " LCD firmware update failed"); + webServer.setContentLength(CONTENT_LENGTH_UNKNOWN); + webServer.send(200, "text/html", httpHeader); + webServer.sendContent_P(HTTP_SCRIPT); + webServer.sendContent_P(HTTP_STYLE); + webServer.sendContent_P(HASP_STYLE); + webServer.sendContent(F("")); + webServer.sendContent_P(HTTP_HEAD_END); + webServer.sendContent(F("

")); + webServer.sendContent(haspNode); + webServer.sendContent(F(" LCD update failed :(

")); + webServer.sendContent(F("Restarting HASwitchPlate to apply changes...")); + webServer.sendContent_P(HTTP_END); + webServer.sendContent(""); +} + +//////////////////////////////////////////////////////////////////////////////////////////////////// +void webHandleLcdDownload() +{ // http://plate01/lcddownload + if (configPassword[0] != '\0') + { // Request HTTP auth if configPassword is set + if (!webServer.authenticate(configUser, configPassword)) + { + return webServer.requestAuthentication(); + } + } + debugPrintln(String(F("HTTP: Sending /lcddownload page to client connected from: ")) + webServer.client().remoteIP().toString()); + String httpHeader = FPSTR(HTTP_HEAD_START); + httpHeader.replace("{v}", "HASPone " + String(haspNode) + " LCD firmware update"); + webServer.setContentLength(CONTENT_LENGTH_UNKNOWN); + webServer.send(200, "text/html", httpHeader); + webServer.sendContent_P(HTTP_SCRIPT); + webServer.sendContent_P(HTTP_STYLE); + webServer.sendContent_P(HASP_STYLE); + webServer.sendContent_P(HTTP_HEAD_END); + webServer.sendContent(F("

")); + webServer.sendContent(haspNode); + webServer.sendContent(F(" LCD update

")); + webServer.sendContent(F("
Updating LCD firmware from: ")); + webServer.sendContent(webServer.arg("lcdFirmware")); + webServer.sendContent_P(HTTP_END); + webServer.sendContent(""); + nextionOtaStartDownload(webServer.arg("lcdFirmware")); +} + +//////////////////////////////////////////////////////////////////////////////////////////////////// +void webHandleTftFileSize() +{ // http://plate01/tftFileSize + if (configPassword[0] != '\0') + { // Request HTTP auth if configPassword is set + if (!webServer.authenticate(configUser, configPassword)) + { + return webServer.requestAuthentication(); + } + } + debugPrintln(String(F("HTTP: Sending /tftFileSize page to client connected from: ")) + webServer.client().remoteIP().toString()); + String httpHeader = FPSTR(HTTP_HEAD_START); + httpHeader.replace("{v}", "HASPone " + String(haspNode) + " TFT Filesize"); + webServer.setContentLength(CONTENT_LENGTH_UNKNOWN); + webServer.send(200, "text/html", httpHeader); + webServer.sendContent_P(HTTP_HEAD_END); + tftFileSize = webServer.arg("tftFileSize").toInt(); + debugPrintln(String(F("WEB: Received tftFileSize: ")) + String(tftFileSize)); +} + +//////////////////////////////////////////////////////////////////////////////////////////////////// +void webHandleReboot() +{ // http://plate01/reboot + if (configPassword[0] != '\0') + { // Request HTTP auth if configPassword is set + if (!webServer.authenticate(configUser, configPassword)) + { + return webServer.requestAuthentication(); + } + } + debugPrintln(String(F("HTTP: Sending /reboot page to client connected from: ")) + webServer.client().remoteIP().toString()); + String httpHeader = FPSTR(HTTP_HEAD_START); + httpHeader.replace("{v}", "HASPone " + String(haspNode) + " reboot"); + webServer.setContentLength(CONTENT_LENGTH_UNKNOWN); + webServer.send(200, "text/html", httpHeader); + webServer.sendContent_P(HTTP_SCRIPT); + webServer.sendContent_P(HTTP_STYLE); + webServer.sendContent_P(HASP_STYLE); + webServer.sendContent(F("")); + webServer.sendContent_P(HTTP_HEAD_END); + webServer.sendContent(F("

")); + webServer.sendContent(haspNode); + webServer.sendContent(F(" Reboot

")); + webServer.sendContent(F("
Rebooting device")); + webServer.sendContent_P(HTTP_END); + webServer.sendContent(""); + nextionSendCmd("page 0"); + nextionSetAttr("p[0].b[1].txt", "\"Rebooting...\""); + espReset(); +} + +//////////////////////////////////////////////////////////////////////////////////////////////////// +bool updateCheck() +{ // firmware update check + WiFiClientSecure wifiUpdateClientSecure; + HTTPClient updateClient; + debugPrintln(String(F("UPDATE: Checking update URL: ")) + FPSTR(UPDATE_URL)); + + wifiUpdateClientSecure.setInsecure(); + wifiUpdateClientSecure.setBufferSizes(512, 512); + updateClient.begin(wifiUpdateClientSecure, UPDATE_URL); + + int httpCode = updateClient.GET(); // start connection and send HTTP header + if (httpCode != HTTP_CODE_OK) + { + debugPrintln(String(F("UPDATE: Update check failed: ")) + updateClient.errorToString(httpCode)); + return false; + } + + DynamicJsonDocument updateJson(2048); + DeserializationError jsonError = deserializeJson(updateJson, updateClient.getString()); + updateClient.end(); + + if (jsonError) + { // Couldn't parse the returned JSON, so bail + debugPrintln(String(F("UPDATE: JSON parsing failed: ")) + String(jsonError.c_str())); + mqttClient.publish(mqttStateJSONTopic, String(F("{\"event\":\"jsonError\",\"event_source\":\"updateCheck()\",\"event_description\":\"Failed to parse incoming JSON command with error: ")) + String(jsonError.c_str()) + String(F("\"}"))); + return false; + } + else + { + if (!updateJson["d1_mini"]["version"].isNull()) + { + updateEspAvailableVersion = updateJson["d1_mini"]["version"].as(); + debugPrintln(String(F("UPDATE: updateEspAvailableVersion: ")) + String(updateEspAvailableVersion)); + espFirmwareUrl = updateJson["d1_mini"]["firmware"].as(); + if (updateEspAvailableVersion > haspVersion) + { + updateEspAvailable = true; + debugPrintln(String(F("UPDATE: New ESP version available: ")) + String(updateEspAvailableVersion)); + } + } + if (nextionModel && !updateJson[nextionModel]["version"].isNull()) + { + updateLcdAvailableVersion = updateJson[nextionModel]["version"].as(); + debugPrintln(String(F("UPDATE: updateLcdAvailableVersion: ")) + String(updateLcdAvailableVersion)); + lcdFirmwareUrl = updateJson[nextionModel]["firmware"].as(); + if (updateLcdAvailableVersion > lcdVersion) + { + updateLcdAvailable = true; + debugPrintln(String(F("UPDATE: New LCD version available: ")) + String(updateLcdAvailableVersion)); + } + } + debugPrintln(F("UPDATE: Update check completed")); + } + return true; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////// +void motionSetup() +{ + if (strcmp(motionPinConfig, "D0") == 0) + { + motionEnabled = true; + motionPin = D0; + pinMode(motionPin, INPUT); + } + else if (strcmp(motionPinConfig, "D1") == 0) + { + motionEnabled = true; + motionPin = D1; + pinMode(motionPin, INPUT); + } +} + +//////////////////////////////////////////////////////////////////////////////////////////////////// +void motionHandle() +{ // Monitor motion sensor + if (motionEnabled) + { // Check on our motion sensor + static unsigned long motionLatchTimer = 0; // Timer for motion sensor latch + static unsigned long motionBufferTimer = millis(); // Timer for motion sensor buffer + static bool motionActiveBuffer = motionActive; + bool motionRead = digitalRead(motionPin); + + if (motionRead != motionActiveBuffer) + { // if we've changed state + motionBufferTimer = millis(); + motionActiveBuffer = motionRead; + } + else if (millis() > (motionBufferTimer + motionBufferTimeout)) + { + if ((motionActiveBuffer && !motionActive) && (millis() > (motionLatchTimer + motionLatchTimeout))) + { + motionLatchTimer = millis(); + mqttClient.publish(mqttMotionStateTopic, "ON"); + debugPrintln(String(F("MQTT OUT: '")) + mqttMotionStateTopic + String(F("' : 'ON'"))); + motionActive = motionActiveBuffer; + debugPrintln("MOTION: Active"); + } + else if ((!motionActiveBuffer && motionActive) && (millis() > (motionLatchTimer + motionLatchTimeout))) + { + motionLatchTimer = millis(); + mqttClient.publish(mqttMotionStateTopic, "OFF"); + debugPrintln(String(F("MQTT OUT: '")) + mqttMotionStateTopic + String(F("' : 'OFF'"))); + motionActive = motionActiveBuffer; + debugPrintln("MOTION: Inactive"); + } + } + } +} + +//////////////////////////////////////////////////////////////////////////////////////////////////// +void beepHandle() +{ // Handle beep/tactile feedback + if (beepEnabled) + { + static bool beepState = false; // beep currently engaged + static unsigned long beepPrevMillis = 0; // store last time beep was updated + if ((beepState == true) && (millis() - beepPrevMillis >= beepOnTime) && ((beepCounter > 0))) + { + beepState = false; // Turn it off + beepPrevMillis = millis(); // Remember the time + analogWrite(beepPin, 254); // start beep for beepOnTime + if (beepCounter > 0) + { // Update the beep counter. + beepCounter--; + } + } + else if ((beepState == false) && (millis() - beepPrevMillis >= beepOffTime) && ((beepCounter >= 0))) + { + beepState = true; // turn it on + beepPrevMillis = millis(); // Remember the time + analogWrite(beepPin, 0); // stop beep for beepOffTime + } + } +} + +//////////////////////////////////////////////////////////////////////////////////////////////////// +void telnetHandleClient() +{ // Basic telnet client handling code from: https://gist.github.com/tablatronix/4793677ca748f5f584c95ec4a2b10303 + if (debugTelnetEnabled) + { // Only do any of this if we're actually enabled + static unsigned long telnetInputIndex = 0; + if (telnetServer.hasClient()) + { // client is connected + if (!telnetClient || !telnetClient.connected()) + { + if (telnetClient) + { + telnetClient.stop(); // client disconnected + } + telnetClient = telnetServer.accept(); // ready for new client + telnetInputIndex = 0; // reset input buffer index + } + else + { + telnetServer.accept().stop(); // have client, block new connections + } + } + // Handle client input from telnet connection. + if (telnetClient && telnetClient.connected() && telnetClient.available()) + { // client input processing + static char telnetInputBuffer[telnetInputMax]; + + if (telnetClient.available()) + { + char telnetInputByte = telnetClient.read(); // Read client byte + if (telnetInputByte == 5) + { // If the telnet client sent a bunch of control commands on connection (which end in ENQUIRY/0x05), ignore them and restart the buffer + telnetInputIndex = 0; + } + else if (telnetInputByte == 13) + { // telnet line endings should be CRLF: https://tools.ietf.org/html/rfc5198#appendix-C + // If we get a CR just ignore it + } + else if (telnetInputByte == 10) + { // We've caught a LF (DEC 10), send buffer contents to the Nextion + telnetInputBuffer[telnetInputIndex] = 0; // null terminate our char array + nextionSendCmd(String(telnetInputBuffer)); + telnetInputIndex = 0; + } + else if (telnetInputIndex < telnetInputMax) + { // If we have room left in our buffer add the current byte + telnetInputBuffer[telnetInputIndex] = telnetInputByte; + telnetInputIndex++; + } + } + } + } +} + +//////////////////////////////////////////////////////////////////////////////////////////////////// +void debugPrintln(const String &debugText) +{ // Debug output line of text to our debug targets + const String debugTimeText = "[+" + String(float(millis()) / 1000, 3) + "s] "; + if (debugSerialEnabled) + { + Serial.print(debugTimeText); + Serial.println(debugText); + SoftwareSerial debugSerial(-1, 1); // -1==nc for RX, 1==TX pin + debugSerial.begin(debugSerialBaud); + debugSerial.print(debugTimeText); + debugSerial.println(debugText); + debugSerial.flush(); + } + if (debugTelnetEnabled) + { + if (telnetClient.connected()) + { + telnetClient.print(debugTimeText); + telnetClient.println(debugText); + } + } +} + +//////////////////////////////////////////////////////////////////////////////////////////////////// +void debugPrint(const String &debugText) +{ // Debug output a string to our debug targets. + // Try to avoid using this function if at all possible. When connected to telnet, printing each + // character requires a full TCP round-trip + acknowledgement back and execution halts while this + // happens. Far better to put everything into a line and send it all out in one packet using + // debugPrintln. + if (debugSerialEnabled) + Serial.print(debugText); + { + SoftwareSerial debugSerial(-1, 1); // -1==nc for RX, 1==TX pin + debugSerial.begin(debugSerialBaud); + debugSerial.print(debugText); + debugSerial.flush(); + } + if (debugTelnetEnabled) + { + if (telnetClient.connected()) + { + telnetClient.print(debugText); + } + } +} + +//////////////////////////////////////////////////////////////////////////////////////////////////// +void debugPrintCrash() +{ // Debug output line of text to our debug targets + SoftwareSerial debugSerial(-1, 1); // -1==nc for RX, 1==TX pin + debugSerial.begin(debugSerialBaud); + SaveCrash.print(debugSerial); + SaveCrash.clear(); +} + +//////////////////////////////////////////////////////////////////////////////////////////////////// +void debugPrintFile(const String &fileName) +{ // Debug output line of text to our debug targets + File debugFile = SPIFFS.open(fileName, "r"); + if (debugFile) + { + uint16_t lineCount = 1; + while (debugFile.available()) + { + debugPrintln(F("SPIFFS: file:") + fileName + F(" line:") + String(lineCount) + F(" data:") + debugFile.readStringUntil('\n')); + lineCount++; + } + debugFile.close(); + } + else + { + debugPrintln("SPIFFS: Error opening file for read: " + fileName); + } +} + +//////////////////////////////////////////////////////////////////////////////////////////////////// +// Submitted by benmprojects to handle "beep" commands. Split +// incoming String by separator, return selected field as String +// Original source: https://arduino.stackexchange.com/a/1237 +String getSubtringField(String data, char separator, int index) +{ + int found = 0; + int strIndex[] = {0, -1}; + int maxIndex = data.length(); + + for (int i = 0; i <= maxIndex && found <= index; i++) + { + if (data.charAt(i) == separator || i == maxIndex) + { + found++; + strIndex[0] = strIndex[1] + 1; + strIndex[1] = (i == maxIndex) ? i + 1 : i; + } + } + return found > index ? data.substring(strIndex[0], strIndex[1]) : ""; +} + +//////////////////////////////////////////////////////////////////////////////// +String printHex8(byte *data, uint8_t length) +{ // returns input bytes as printable hex values in the format 0x01 0x23 0xFF + + String hex8String; + for (int i = 0; i < length; i++) + { + hex8String += "0x"; + if (data[i] < 0x10) + { + hex8String += "0"; + } + hex8String += String(data[i], HEX); + if (i != (length - 1)) + { + hex8String += " "; + } + } + // hex8String.toUpperCase(); + return hex8String; +} diff --git a/Arduino_Sketch/debug/HASwitchPlate.ino.d1_mini.elf b/Arduino_Sketch/debug/HASwitchPlate.ino.d1_mini.elf index 3c73273..6133b1c 100644 Binary files a/Arduino_Sketch/debug/HASwitchPlate.ino.d1_mini.elf and b/Arduino_Sketch/debug/HASwitchPlate.ino.d1_mini.elf differ diff --git a/Arduino_Sketch/platformio.ini b/Arduino_Sketch/platformio.ini index cc3a315..5148c63 100644 --- a/Arduino_Sketch/platformio.ini +++ b/Arduino_Sketch/platformio.ini @@ -9,17 +9,17 @@ ; https://docs.platformio.org/page/projectconf.html [env:d1_mini] -platform = https://github.com/platformio/platform-espressif8266.git @ ^3.2.0 +platform = https://github.com/platformio/platform-espressif8266.git @ ^4.2.1 board = d1_mini framework = arduino board_build.f_cpu = 160000000L board_build.ldscript = eagle.flash.4m1m.ld build_flags = - -D PIO_FRAMEWORK_ARDUINO_ESPRESSIF_SDK22x_191122 + -D PIO_FRAMEWORK_ARDUINO_ESPRESSIF_SDK305 -D PIO_FRAMEWORK_ARDUINO_LWIP2_HIGHER_BANDWIDTH lib_deps = - bblanchon/ArduinoJson @ ^6.18.5 - 256dpi/MQTT @ ^2.5.0 - dancol90/ESP8266Ping @ ^1.0 - krzychb/EspSaveCrash @ ^1.2.0 - https://github.com/tzapu/WiFiManager.git#8452df79bbc55265d6a999d7384204220f4d22c6 \ No newline at end of file + 256dpi/MQTT @ ^2.5.2 + dancol90/ESP8266Ping @ ^1.1.0 + bblanchon/ArduinoJson @ ^7.0.4 + krzychb/EspSaveCrash @ ^1.3.0 + https://github.com/tzapu/WiFiManager.git#e978bc059c522404c01e06cd136fcf23234eb784 \ No newline at end of file diff --git a/Home_Assistant/blueprints/hasp_Activate_Page.yaml b/Home_Assistant/blueprints/hasp_Activate_Page.yaml index 01d03d5..bfb7078 100644 --- a/Home_Assistant/blueprints/hasp_Activate_Page.yaml +++ b/Home_Assistant/blueprints/hasp_Activate_Page.yaml @@ -2,7 +2,7 @@ blueprint: name: "HASPone p[x].b[y] activates a page" description: | - ## Blueprint Version: `1.05.00` + ## Blueprint Version: `1.06.00` # Description diff --git a/Home_Assistant/blueprints/hasp_Activate_Page_on_Idle.yaml b/Home_Assistant/blueprints/hasp_Activate_Page_on_Idle.yaml index 9166c1a..e0d1bd6 100644 --- a/Home_Assistant/blueprints/hasp_Activate_Page_on_Idle.yaml +++ b/Home_Assistant/blueprints/hasp_Activate_Page_on_Idle.yaml @@ -2,7 +2,7 @@ blueprint: name: "HASPone activates a selected page after a specified period of inactivity" description: | - ## Blueprint Version: `1.05.00` + ## Blueprint Version: `1.06.00` # Description @@ -122,7 +122,7 @@ condition: action: - delay: - seconds: "{{idletime|int}}" + seconds: "{{idletime|int(default=30)}}" - condition: template value_template: >- @@ -142,4 +142,4 @@ action: data: topic: "{{pagecommandtopic}}" payload: "{{targetpage}}" - retain: true \ No newline at end of file + retain: true diff --git a/Home_Assistant/blueprints/hasp_Apply_Theme.yaml b/Home_Assistant/blueprints/hasp_Apply_Theme.yaml index 3d9c40d..def243b 100644 --- a/Home_Assistant/blueprints/hasp_Apply_Theme.yaml +++ b/Home_Assistant/blueprints/hasp_Apply_Theme.yaml @@ -2,7 +2,7 @@ blueprint: name: "HASPone p[x].b[y] has theme colors applied" description: | - ## Blueprint Version: `1.05.00` + ## Blueprint Version: `1.06.00` ## Description diff --git a/Home_Assistant/blueprints/hasp_Apply_Theme_Multiple_Buttons.yaml b/Home_Assistant/blueprints/hasp_Apply_Theme_Multiple_Buttons.yaml new file mode 100644 index 0000000..646fb9d --- /dev/null +++ b/Home_Assistant/blueprints/hasp_Apply_Theme_Multiple_Buttons.yaml @@ -0,0 +1,326 @@ +blueprint: + name: "HASPone buttons have theme colors applied" + description: | + + ## Blueprint Version: `1.06.00` + + ## Description + + Several buttons on the HASPone will have the current device theme or custom colors applied. + + ## HASPone Page and Button Reference + +
+ + The images below show each available HASPone page along with the layout of available button objects. + + | Page 0 | Pages 1-3 | Pages 4-5 | + |--------|-----------|-----------| + | ![Page 0](https://raw.githubusercontent.com/HASwitchPlate/HASPone/main/images/NextionUI_p0_Init_Screen.png) | ![Pages 1-3](https://raw.githubusercontent.com/HASwitchPlate/HASPone/main/images/NextionUI_p1-p3_4buttons.png) | ![Pages 4-5](https://raw.githubusercontent.com/HASwitchPlate/HASPone/main/images/NextionUI_p4-p5_3sliders.png) | + + | Page 6 | Page 7 | Page 8 | + |--------|--------|--------| + | ![Page 6](https://raw.githubusercontent.com/HASwitchPlate/HASPone/main/images/NextionUI_p6_8buttons.png) | ![Page 7](https://raw.githubusercontent.com/HASwitchPlate/HASPone/main/images/NextionUI_p7_12buttons.png) | ![Page 8](https://raw.githubusercontent.com/HASwitchPlate/HASPone/main/images/NextionUI_p8_5buttons+1slider.png) | + + | Page 9 | Page 10 | Page 11 | + |--------|---------|---------| + | ![Page 9](https://raw.githubusercontent.com/HASwitchPlate/HASPone/main/images/NextionUI_p9_9buttons.png) | ![Page 10](https://raw.githubusercontent.com/HASwitchPlate/HASPone/main/images/NextionUI_p10_5buttons.png) | ![Page 11](https://raw.githubusercontent.com/HASwitchPlate/HASPone/main/images/NextionUI_p11_1button+1slider.png) + +
+ + ## Nextion color codes + +
+ + The Nextion environment utilizes RGB 565 encoding. [Use this handy convertor](https://nodtem66.github.io/nextion-hmi-color-convert/index.html) to select your colors and convert to the RGB 565 format. + + Here are some example colors: + + | Color | Code | + |--------|-------| + | White | 65535 | + | Black | 0 | + | Grey | 25388 | + | Red | 63488 | + | Green | 2016 | + | Blue | 31 | + | Yellow | 65504 | + | Orange | 64512 | + | Brown | 48192 | + +
+ + domain: automation + input: + haspdevice: + name: "HASPone Device" + description: "Select the HASPone device" + selector: + device: + integration: mqtt + manufacturer: "HASwitchPlate" + model: "HASPone v1.0.0" + objects: + name: "HASPone buttons" + description: "Apply the current theme or colors defined below to all of the objects in this list" + default: + - p[1].b[4] + - p[1].b[5] + - p[1].b[6] + - p[1].b[7] + selector: + object: + selected_fgcolor: + name: "Selected foreground color" + description: 'Selected foreground color in Nextion RGB565 format (see "Nextion color codes" above for reference). -1 = Current theme selected foreground color.' + default: -1 + selector: + number: + min: -1 + max: 65535 + mode: slider + selected_bgcolor: + name: "Selected background color" + description: 'Selected background color in Nextion RGB565 format (see "Nextion color codes" above for reference). -1 = Current theme selected background color.' + default: -1 + selector: + number: + min: -1 + max: 65535 + mode: slider + unselected_fgcolor: + name: "Unselected foreground color" + description: 'Unselected foreground color in Nextion RGB565 format (see "Nextion color codes" above for reference). -1 = Current theme unselected foreground color.' + default: -1 + selector: + number: + min: -1 + max: 65535 + mode: slider + unselected_bgcolor: + name: "Unselected background color" + description: 'Unselected background color in Nextion RGB565 format (see "Nextion color codes" above for reference). -1 = Current theme unselected background color.' + default: -1 + selector: + number: + min: -1 + max: 65535 + mode: slider + +mode: parallel +max_exceeded: silent + +variables: + haspdevice: !input haspdevice + haspname: >- + {%- for entity in device_entities(haspdevice) -%} + {%- if entity|regex_search("^sensor\..+_sensor(?:_\d+|)$") -%} + {{- entity|regex_replace(find="^sensor\.", replace="", ignorecase=true)|regex_replace(find="_sensor(?:_\d+|)$", replace="", ignorecase=true) -}} + {%- endif -%} + {%- endfor -%} + objects: !input objects + selected_fgcolor: !input selected_fgcolor + selected_bgcolor: !input selected_bgcolor + unselected_fgcolor: !input unselected_fgcolor + unselected_bgcolor: !input unselected_bgcolor + # haspobject: '{{ "p[" ~ hasppage ~ "].b[" ~ haspbutton ~ "]" }}' + # commandtopic: '{{ "hasp/" ~ haspname ~ "/command/" ~ haspobject }}' + jsoncommandtopic: '{{ "hasp/" ~ haspname ~ "/command/json" }}' + selectedfgtopic: '{{ "hasp/" ~ haspname ~ "/light/selectedforegroundcolor/rgb" }}' + selectedbgtopic: '{{ "hasp/" ~ haspname ~ "/light/selectedbackgroundcolor/rgb" }}' + unselectedfgtopic: '{{ "hasp/" ~ haspname ~ "/light/unselectedforegroundcolor/rgb" }}' + unselectedbgtopic: '{{ "hasp/" ~ haspname ~ "/light/unselectedbackgroundcolor/rgb" }}' + selectedfg: >- + {%- if (selected_fgcolor|int) >= 0 -%} + {{ selected_fgcolor }} + {%- else -%} + {%- set color = namespace() -%} + {%- for entity in device_entities(haspdevice) -%} + {%- if entity|regex_search("^light\..*_selected_foreground_color(?:_\d+|)$") -%} + {%- set color.source=entity -%} + {%- endif -%} + {%- endfor -%} + {%- set brightness = state_attr(color.source, "brightness")|int(default=255) / 255 -%} + {%- set red=(state_attr(color.source, "rgb_color")[0] * brightness)|int(default=0) -%} + {%- set green=(state_attr(color.source, "rgb_color")[1] * brightness)|int(default=0) -%} + {%- set blue=(state_attr(color.source, "rgb_color")[2] * brightness)|int(default=0) -%} + {{ (red|bitwise_and(248)*256) + (green|bitwise_and(252)*8) + (blue|bitwise_and(248)/8)|int }} + {%- endif -%} + selectedbg: >- + {%- if (selected_bgcolor|int) >= 0 -%} + {{ selected_bgcolor }} + {%- else -%} + {%- set color = namespace() -%} + {%- for entity in device_entities(haspdevice) -%} + {%- if entity|regex_search("^light\..*_selected_background_color(?:_\d+|)$") -%} + {%- set color.source=entity -%} + {%- endif -%} + {%- endfor -%} + {%- set brightness = state_attr(color.source, "brightness")|int(default=255) / 255 -%} + {%- set red=(state_attr(color.source, "rgb_color")[0] * brightness)|int(default=0) -%} + {%- set green=(state_attr(color.source, "rgb_color")[1] * brightness)|int(default=0) -%} + {%- set blue=(state_attr(color.source, "rgb_color")[2] * brightness)|int(default=0) -%} + {{ (red|bitwise_and(248)*256) + (green|bitwise_and(252)*8) + (blue|bitwise_and(248)/8)|int }} + {%- endif -%} + unselectedfg: >- + {%- if (unselected_fgcolor|int) >= 0 -%} + {{ unselected_fgcolor }} + {%- else -%} + {%- set color = namespace() -%} + {%- for entity in device_entities(haspdevice) -%} + {%- if entity|regex_search("^light\..*_unselected_foreground_color(?:_\d+|)$") -%} + {%- set color.source=entity -%} + {%- endif -%} + {%- endfor -%} + {%- set brightness = state_attr(color.source, "brightness")|int(default=255) / 255 -%} + {%- set red=(state_attr(color.source, "rgb_color")[0] * brightness)|int(default=0) -%} + {%- set green=(state_attr(color.source, "rgb_color")[1] * brightness)|int(default=0) -%} + {%- set blue=(state_attr(color.source, "rgb_color")[2] * brightness)|int(default=0) -%} + {{ (red|bitwise_and(248)*256) + (green|bitwise_and(252)*8) + (blue|bitwise_and(248)/8)|int }} + {%- endif -%} + unselectedbg: >- + {%- if (unselected_bgcolor|int) >= 0 -%} + {{ unselected_bgcolor }} + {%- else -%} + {%- set color = namespace() -%} + {%- for entity in device_entities(haspdevice) -%} + {%- if entity|regex_search("^light\..*_unselected_background_color(?:_\d+|)$") -%} + {%- set color.source=entity -%} + {%- endif -%} + {%- endfor -%} + {%- set brightness = state_attr(color.source, "brightness")|int(default=255) / 255 -%} + {%- set red=(state_attr(color.source, "rgb_color")[0] * brightness)|int(default=0) -%} + {%- set green=(state_attr(color.source, "rgb_color")[1] * brightness)|int(default=0) -%} + {%- set blue=(state_attr(color.source, "rgb_color")[2] * brightness)|int(default=0) -%} + {{ (red|bitwise_and(248)*256) + (green|bitwise_and(252)*8) + (blue|bitwise_and(248)/8)|int }} + {%- endif -%} + +trigger_variables: + haspdevice: !input haspdevice + haspname: >- + {%- for entity in device_entities(haspdevice) -%} + {%- if entity|regex_search("^sensor\..+_sensor(?:_\d+|)$") -%} + {{- entity|regex_replace(find="^sensor\.", replace="", ignorecase=true)|regex_replace(find="_sensor(?:_\d+|)$", replace="", ignorecase=true) -}} + {%- endif -%} + {%- endfor -%} + haspsensor: >- + {%- for entity in device_entities(haspdevice) -%} + {%- if entity|regex_search("^sensor\..+_sensor(?:_\d+|)$") -%} + {{ entity }} + {%- endif -%} + {%- endfor -%} + jsontopic: '{{ "hasp/" ~ haspname ~ "/state/json" }}' + selectedfgtopic: '{{ "hasp/" ~ haspname ~ "/light/selectedforegroundcolor/rgb" }}' + selectedbgtopic: '{{ "hasp/" ~ haspname ~ "/light/selectedbackgroundcolor/rgb" }}' + unselectedfgtopic: '{{ "hasp/" ~ haspname ~ "/light/unselectedforegroundcolor/rgb" }}' + unselectedbgtopic: '{{ "hasp/" ~ haspname ~ "/light/unselectedbackgroundcolor/rgb" }}' + +trigger: + - platform: homeassistant + event: start + - platform: template + value_template: "{{ is_state(haspsensor, 'ON') }}" + - platform: mqtt + topic: "{{selectedfgtopic}}" + - platform: mqtt + topic: "{{selectedbgtopic}}" + - platform: mqtt + topic: "{{unselectedfgtopic}}" + - platform: mqtt + topic: "{{unselectedbgtopic}}" + +condition: + - condition: template + value_template: "{{ is_state(haspsensor, 'ON') }}" + +action: + - choose: + ######################################################################### + # RUN ACTIONS or Home Assistant Startup or HASPone Connect + # Apply text style + - conditions: + - condition: template + value_template: >- + {{- + (trigger is not defined) + or + (trigger.platform is none) + or + ((trigger.platform == 'homeassistant') and (trigger.event == 'start')) + or + ((trigger.platform == 'template') and (trigger.entity_id == haspsensor) and (trigger.to_state.state == 'ON')) + -}} + sequence: + - repeat: + count: "{{objects|length}}" + sequence: + - service: mqtt.publish + data: + topic: "{{jsoncommandtopic}}" + payload: >- + [ + "{{objects[repeat.index-1]}}.pco={{selectedfg}}", + "{{objects[repeat.index-1]}}.bco={{selectedbg}}", + "{{objects[repeat.index-1]}}.pco2={{unselectedfg}}", + "{{objects[repeat.index-1]}}.bco2={{unselectedbg}}" + ] + + ######################################################################### + # Catch triggers fired by incoming MQTT messages + - conditions: + - condition: template + value_template: '{{ trigger.platform == "mqtt" }}' + sequence: + - choose: + ######################################################################### + # Theme: Apply selected foreground color on change + - conditions: + - condition: template + value_template: "{{ (trigger.topic == selectedfgtopic) and ((selected_fgcolor|int) == -1) }}" + sequence: + - repeat: + count: "{{objects|length}}" + sequence: + - service: mqtt.publish + data: + topic: '{{ "hasp/" ~ haspname ~ "/command/" ~ objects[repeat.index-1] ~ ".pco" }}' + payload: "{{trigger.payload}}" + ######################################################################### + # Theme: Apply selected background color on change + - conditions: + - condition: template + value_template: "{{ (trigger.topic == selectedbgtopic) and ((selected_bgcolor|int) == -1) }}" + sequence: + - repeat: + count: "{{objects|length}}" + sequence: + - service: mqtt.publish + data: + topic: '{{ "hasp/" ~ haspname ~ "/command/" ~ objects[repeat.index-1] ~ ".bco" }}' + payload: "{{trigger.payload}}" + ######################################################################### + # Theme: Apply unselected foreground color on change + - conditions: + - condition: template + value_template: "{{ (trigger.topic == unselectedfgtopic) and ((unselected_fgcolor|int) == -1) }}" + sequence: + - repeat: + count: "{{objects|length}}" + sequence: + - service: mqtt.publish + data: + topic: '{{ "hasp/" ~ haspname ~ "/command/" ~ objects[repeat.index-1] ~ ".pco2" }}' + payload: "{{trigger.payload}}" + ######################################################################### + # Theme: Apply unselected background color on change + - conditions: + - condition: template + value_template: "{{ (trigger.topic == unselectedbgtopic) and ((unselected_bgcolor|int) == -1) }}" + sequence: + - repeat: + count: "{{objects|length}}" + sequence: + - service: mqtt.publish + data: + topic: '{{ "hasp/" ~ haspname ~ "/command/" ~ objects[repeat.index-1] ~ ".bco2" }}' + payload: "{{trigger.payload}}" diff --git a/Home_Assistant/blueprints/hasp_Core_Functionality.yaml b/Home_Assistant/blueprints/hasp_Core_Functionality.yaml index 5e7c4d9..b90db11 100644 --- a/Home_Assistant/blueprints/hasp_Core_Functionality.yaml +++ b/Home_Assistant/blueprints/hasp_Core_Functionality.yaml @@ -2,7 +2,7 @@ blueprint: name: "HASPone Core functionality" description: | - ## Blueprint Version: `1.05.00` + ## Blueprint Version: `1.06.00` ## Description @@ -234,15 +234,15 @@ variables: {%- endfor -%} page1text: !input page1text page1font_select: !input page1font_select - page1font: "{{ page1font_select.split(' - ')[0] | int }}" + page1font: "{{ page1font_select.split(' - ')[0] | int(default=6) }}" page1page: !input page1page page2text: !input page2text page2font_select: !input page2font_select - page2font: "{{ page2font_select.split(' - ')[0] | int }}" + page2font: "{{ page2font_select.split(' - ')[0] | int(default=6) }}" page2page: !input page2page page3text: !input page3text page3font_select: !input page3font_select - page3font: "{{ page3font_select.split(' - ')[0] | int }}" + page3font: "{{ page3font_select.split(' - ')[0] | int(default=6) }}" page3page: !input page3page page_scroll: !input page_scroll page_scroll_list: !input page_scroll_list @@ -289,29 +289,29 @@ variables: page_list: '{{page_scroll_list.split(",")}}' page_previous: > {%- set page = namespace() -%} - {%- set page.previous = page_list[(page_list|length)-1]|int -%} - {%- set page.next = page_list[0]|int -%} + {%- set page.previous = page_list[(page_list|length)-1]|int(default=10) -%} + {%- set page.next = page_list[0]|int(default=1) -%} {%- for item in page_list -%} - {%- if item|int == activepage -%} + {%- if item|int(default=1) == activepage -%} {%- if not loop.first -%} - {%- set page.previous = loop.previtem|int -%} + {%- set page.previous = loop.previtem|int(default=1) -%} {%- endif -%} {%- if not loop.last -%} - {%- set page.next = loop.nextitem|int -%} + {%- set page.next = loop.nextitem|int(default=1) -%} {%- endif -%} {%- endif -%} {%- endfor -%}{{page.previous}} page_next: > {%- set page = namespace() -%} - {%- set page.previous = page_list[(page_list|length)-1]|int -%} - {%- set page.next = page_list[0]|int -%} + {%- set page.previous = page_list[(page_list|length)-1]|int(default=10) -%} + {%- set page.next = page_list[0]|int(default=1) -%} {%- for item in page_list -%} - {%- if item|int == activepage -%} + {%- if item|int(default=1) == activepage -%} {%- if not loop.first -%} - {%- set page.previous = loop.previtem|int -%} + {%- set page.previous = loop.previtem|int(default=1) -%} {%- endif -%} {%- if not loop.last -%} - {%- set page.next = loop.nextitem|int -%} + {%- set page.next = loop.nextitem|int(default=1) -%} {%- endif -%} {%- endif -%} {%- endfor -%}{{page.next}} @@ -488,7 +488,7 @@ action: - service: mqtt.publish data: topic: "hasp/{{haspname}}/command/nextionbaud" - payload: "115200" + payload: "115200" # Send page select button config - service: mqtt.publish data: @@ -608,11 +608,11 @@ action: {%- set red=(state_attr(color.source, "rgb_color")[0] * brightness)|int(default=0) -%} {%- set green=(state_attr(color.source, "rgb_color")[1] * brightness)|int(default=0) -%} {%- set blue=(state_attr(color.source, "rgb_color")[2] * brightness)|int(default=0) -%} - {%- set colorcode = (red|bitwise_and(248)*256) + (green|bitwise_and(252)*8) + (blue|bitwise_and(248)/8)|int -%} - [{%- if not page_scroll -%}{%- for p in range(1,12) %}{%- if p == page1page|int %}"p[{{p}}].b[1].pco={{colorcode}}"{%- else -%}"p[{{p}}].b[1].pco2={{colorcode}}"{%- endif -%},{%- endfor -%} + {%- set colorcode = (red|bitwise_and(248)*256) + (green|bitwise_and(252)*8) + (blue|bitwise_and(248)/8)|int(default=0) -%} + [{%- if not page_scroll -%}{%- for p in range(1,12) %}{%- if p == page1page|int(default=1) %}"p[{{p}}].b[1].pco={{colorcode}}"{%- else -%}"p[{{p}}].b[1].pco2={{colorcode}}"{%- endif -%},{%- endfor -%} {%- else -%}{%- for p in range(1,12) %}"p[{{p}}].b[1].pco2={{colorcode}}",{%- endfor -%}{%- endif -%} - {%- for p in range(1,12) %}{%- if p == page2page|int %}"p[{{p}}].b[2].pco={{colorcode}}"{%- else -%}"p[{{p}}].b[2].pco2={{colorcode}}"{%- endif -%},{%- endfor -%} - {%- if not page_scroll -%}{%- for p in range(1,12) %}{%- if p == page3page|int %}"p[{{p}}].b[3].pco={{colorcode}}"{%- else -%}"p[{{p}}].b[3].pco2={{colorcode}}"{%- endif -%}{% if not loop.last %},{% endif %}{%- endfor -%} + {%- for p in range(1,12) %}{%- if p == page2page|int(default=2) %}"p[{{p}}].b[2].pco={{colorcode}}"{%- else -%}"p[{{p}}].b[2].pco2={{colorcode}}"{%- endif -%},{%- endfor -%} + {%- if not page_scroll -%}{%- for p in range(1,12) %}{%- if p == page3page|int(default=3) %}"p[{{p}}].b[3].pco={{colorcode}}"{%- else -%}"p[{{p}}].b[3].pco2={{colorcode}}"{%- endif -%}{% if not loop.last %},{% endif %}{%- endfor -%} {%- else -%}{%- for p in range(1,12) %}"p[{{p}}].b[3].pco2={{colorcode}}"{% if not loop.last %},{% endif %}{%- endfor -%}{%- endif -%}] - service: mqtt.publish # apply selected background color to page select buttons data: @@ -628,11 +628,11 @@ action: {%- set red=(state_attr(color.source, "rgb_color")[0] * brightness)|int(default=0) -%} {%- set green=(state_attr(color.source, "rgb_color")[1] * brightness)|int(default=0) -%} {%- set blue=(state_attr(color.source, "rgb_color")[2] * brightness)|int(default=0) -%} - {%- set colorcode = (red|bitwise_and(248)*256) + (green|bitwise_and(252)*8) + (blue|bitwise_and(248)/8)|int -%} - [{%- if not page_scroll -%}{%- for p in range(1,12) %}{%- if p == page1page|int %}"p[{{p}}].b[1].bco={{colorcode}}"{%- else -%}"p[{{p}}].b[1].bco2={{colorcode}}"{%- endif -%},{%- endfor -%} + {%- set colorcode = (red|bitwise_and(248)*256) + (green|bitwise_and(252)*8) + (blue|bitwise_and(248)/8)|int(default=65535) -%} + [{%- if not page_scroll -%}{%- for p in range(1,12) %}{%- if p == page1page|int(default=1) %}"p[{{p}}].b[1].bco={{colorcode}}"{%- else -%}"p[{{p}}].b[1].bco2={{colorcode}}"{%- endif -%},{%- endfor -%} {% else %}{%- for p in range(1,12) %}"p[{{p}}].b[1].bco2={{colorcode}}",{%- endfor -%}{%- endif -%} - {%- for p in range(1,12) %}{%- if p == page2page|int %}"p[{{p}}].b[2].bco={{colorcode}}"{%- else -%}"p[{{p}}].b[2].bco2={{colorcode}}"{%- endif -%},{%- endfor -%} - {%- if not page_scroll -%}{%- for p in range(1,12) %}{%- if p == page3page|int %}"p[{{p}}].b[3].bco={{colorcode}}"{%- else -%}"p[{{p}}].b[3].bco2={{colorcode}}"{%- endif -%}{% if not loop.last %},{% endif %}{%- endfor -%} + {%- for p in range(1,12) %}{%- if p == page2page|int(default=2) %}"p[{{p}}].b[2].bco={{colorcode}}"{%- else -%}"p[{{p}}].b[2].bco2={{colorcode}}"{%- endif -%},{%- endfor -%} + {%- if not page_scroll -%}{%- for p in range(1,12) %}{%- if p == page3page|int(default=3) %}"p[{{p}}].b[3].bco={{colorcode}}"{%- else -%}"p[{{p}}].b[3].bco2={{colorcode}}"{%- endif -%}{% if not loop.last %},{% endif %}{%- endfor -%} {% else %}{%- for p in range(1,12) %}"p[{{p}}].b[3].bco2={{colorcode}}"{% if not loop.last %},{% endif %}{%- endfor -%}{%- endif -%}] - service: mqtt.publish # apply unselected foreground color to page select buttons data: @@ -648,11 +648,11 @@ action: {%- set red=(state_attr(color.source, "rgb_color")[0] * brightness)|int(default=0) -%} {%- set green=(state_attr(color.source, "rgb_color")[1] * brightness)|int(default=0) -%} {%- set blue=(state_attr(color.source, "rgb_color")[2] * brightness)|int(default=0) -%} - {%- set colorcode = (red|bitwise_and(248)*256) + (green|bitwise_and(252)*8) + (blue|bitwise_and(248)/8)|int -%} - [{%- if not page_scroll -%}{%- for p in range(1,12) %}{%- if p == page1page|int %}"p[{{p}}].b[1].pco2={{colorcode}}"{%- else -%}"p[{{p}}].b[1].pco={{colorcode}}"{%- endif -%},{%- endfor -%} + {%- set colorcode = (red|bitwise_and(248)*256) + (green|bitwise_and(252)*8) + (blue|bitwise_and(248)/8)|int(default=59164) -%} + [{%- if not page_scroll -%}{%- for p in range(1,12) %}{%- if p == page1page|int(default=1) %}"p[{{p}}].b[1].pco2={{colorcode}}"{%- else -%}"p[{{p}}].b[1].pco={{colorcode}}"{%- endif -%},{%- endfor -%} {% else %}{%- for p in range(1,12) %}"p[{{p}}].b[1].pco={{colorcode}}",{%- endfor -%}{%- endif -%} - {%- for p in range(1,12) %}{%- if p == page2page|int %}"p[{{p}}].b[2].pco2={{colorcode}}"{%- else -%}"p[{{p}}].b[2].pco={{colorcode}}"{%- endif -%},{%- endfor -%} - {%- if not page_scroll -%}{%- for p in range(1,12) %}{%- if p == page3page|int %}"p[{{p}}].b[3].pco2={{colorcode}}"{%- else -%}"p[{{p}}].b[3].pco={{colorcode}}"{%- endif -%}{% if not loop.last %},{% endif %}{%- endfor -%} + {%- for p in range(1,12) %}{%- if p == page2page|int(default=2) %}"p[{{p}}].b[2].pco2={{colorcode}}"{%- else -%}"p[{{p}}].b[2].pco={{colorcode}}"{%- endif -%},{%- endfor -%} + {%- if not page_scroll -%}{%- for p in range(1,12) %}{%- if p == page3page|int(default=3) %}"p[{{p}}].b[3].pco2={{colorcode}}"{%- else -%}"p[{{p}}].b[3].pco={{colorcode}}"{%- endif -%}{% if not loop.last %},{% endif %}{%- endfor -%} {% else %}{%- for p in range(1,12) %}"p[{{p}}].b[3].pco={{colorcode}}"{% if not loop.last %},{% endif %}{%- endfor -%}{%- endif -%}] - service: mqtt.publish # apply unselected background color to page select buttons data: @@ -668,11 +668,11 @@ action: {%- set red=(state_attr(color.source, "rgb_color")[0] * brightness)|int(default=0) -%} {%- set green=(state_attr(color.source, "rgb_color")[1] * brightness)|int(default=0) -%} {%- set blue=(state_attr(color.source, "rgb_color")[2] * brightness)|int(default=0) -%} - {%- set colorcode = (red|bitwise_and(248)*256) + (green|bitwise_and(252)*8) + (blue|bitwise_and(248)/8)|int -%} - [{%- if not page_scroll -%}{%- for p in range(1,12) %}{%- if p == page1page|int %}"p[{{p}}].b[1].bco2={{colorcode}}"{%- else -%}"p[{{p}}].b[1].bco={{colorcode}}"{%- endif -%},{%- endfor -%} + {%- set colorcode = (red|bitwise_and(248)*256) + (green|bitwise_and(252)*8) + (blue|bitwise_and(248)/8)|int(default=16904) -%} + [{%- if not page_scroll -%}{%- for p in range(1,12) %}{%- if p == page1page|int(default=1) %}"p[{{p}}].b[1].bco2={{colorcode}}"{%- else -%}"p[{{p}}].b[1].bco={{colorcode}}"{%- endif -%},{%- endfor -%} {% else %}{%- for p in range(1,12) %}"p[{{p}}].b[1].bco={{colorcode}}",{%- endfor -%}{%- endif -%} - {%- for p in range(1,12) %}{%- if p == page2page|int %}"p[{{p}}].b[2].bco2={{colorcode}}"{%- else -%}"p[{{p}}].b[2].bco={{colorcode}}"{%- endif -%},{%- endfor -%} - {%- if not page_scroll -%}{%- for p in range(1,12) %}{%- if p == page3page|int %}"p[{{p}}].b[3].bco2={{colorcode}}"{%- else -%}"p[{{p}}].b[3].bco={{colorcode}}"{%- endif -%}{% if not loop.last %},{% endif %}{%- endfor -%} + {%- for p in range(1,12) %}{%- if p == page2page|int(default=2) %}"p[{{p}}].b[2].bco2={{colorcode}}"{%- else -%}"p[{{p}}].b[2].bco={{colorcode}}"{%- endif -%},{%- endfor -%} + {%- if not page_scroll -%}{%- for p in range(1,12) %}{%- if p == page3page|int(default=3) %}"p[{{p}}].b[3].bco2={{colorcode}}"{%- else -%}"p[{{p}}].b[3].bco={{colorcode}}"{%- endif -%}{% if not loop.last %},{% endif %}{%- endfor -%} {% else %}{%- for p in range(1,12) %}"p[{{p}}].b[3].bco={{colorcode}}"{% if not loop.last %},{% endif %}{%- endfor -%}{%- endif -%}] ######################################################################### @@ -707,11 +707,12 @@ action: {%- set red=(state_attr(color.source, "rgb_color")[0] * brightness)|int(default=0) -%} {%- set green=(state_attr(color.source, "rgb_color")[1] * brightness)|int(default=0) -%} {%- set blue=(state_attr(color.source, "rgb_color")[2] * brightness)|int(default=0) -%} - {%- set colorcode = (red|bitwise_and(248)*256) + (green|bitwise_and(252)*8) + (blue|bitwise_and(248)/8)|int -%} - [{%- if not page_scroll -%}{%- for p in range(1,12) %}{%- if p == page1page|int %}"p[{{p}}].b[1].pco={{colorcode}}"{%- else -%}"p[{{p}}].b[1].pco2={{colorcode}}"{%- endif -%},{%- endfor -%} + {%- set colorcode = (red|bitwise_and(248)*256) + (green|bitwise_and(252)*8) + (blue|bitwise_and(248)/8)|int(default=0) -%} + [{%- if not page_scroll -%}{%- for p in range(1,12) %}{%- if p == page1page|int(default=1) %}"p[{{p}}].b[1].pco={{colorcode}}"{%- else -%}"p[{{p}}].b[1].pco2={{colorcode}}"{%- endif -%},{%- endfor -%} {%- else -%}{%- for p in range(1,12) %}"p[{{p}}].b[1].pco2={{colorcode}}",{%- endfor -%}{%- endif -%} - {%- for p in range(1,12) %}{%- if p == page2page|int %}"p[{{p}}].b[2].pco={{colorcode}}"{%- else -%}"p[{{p}}].b[2].pco2={{colorcode}}"{%- endif -%},{%- endfor -%} - {%- if not page_scroll -%}{%- for p in range(1,12) %}{%- if p == page3page|int %}"p[{{p}}].b[3].pco={{colorcode}}"{%- else -%}"p[{{p}}].b[3].pco2={{colorcode}}"{%- endif -%}{% if not loop.last %},{% endif %}{%- endfor -%} + {%- if not page_scroll -%}{%- for p in range(1,12) %}{%- if p == page2page|int(default=2) %}"p[{{p}}].b[2].pco={{colorcode}}"{%- else -%}"p[{{p}}].b[2].pco2={{colorcode}}"{%- endif -%},{%- endfor -%} + {%- else -%}{%- for p in range(1,12) %}"p[{{p}}].b[2].pco={{colorcode}}",{%- endfor -%}{%- endif -%} + {%- if not page_scroll -%}{%- for p in range(1,12) %}{%- if p == page3page|int(default=3) %}"p[{{p}}].b[3].pco={{colorcode}}"{%- else -%}"p[{{p}}].b[3].pco2={{colorcode}}"{%- endif -%}{% if not loop.last %},{% endif %}{%- endfor -%} {%- else -%}{%- for p in range(1,12) %}"p[{{p}}].b[3].pco2={{colorcode}}"{% if not loop.last %},{% endif %}{%- endfor -%}{%- endif -%}] - service: mqtt.publish # apply selected background color to page select buttons data: @@ -727,11 +728,12 @@ action: {%- set red=(state_attr(color.source, "rgb_color")[0] * brightness)|int(default=0) -%} {%- set green=(state_attr(color.source, "rgb_color")[1] * brightness)|int(default=0) -%} {%- set blue=(state_attr(color.source, "rgb_color")[2] * brightness)|int(default=0) -%} - {%- set colorcode = (red|bitwise_and(248)*256) + (green|bitwise_and(252)*8) + (blue|bitwise_and(248)/8)|int -%} - [{%- if not page_scroll -%}{%- for p in range(1,12) %}{%- if p == page1page|int %}"p[{{p}}].b[1].bco={{colorcode}}"{%- else -%}"p[{{p}}].b[1].bco2={{colorcode}}"{%- endif -%},{%- endfor -%} + {%- set colorcode = (red|bitwise_and(248)*256) + (green|bitwise_and(252)*8) + (blue|bitwise_and(248)/8)|int(default=65535) -%} + [{%- if not page_scroll -%}{%- for p in range(1,12) %}{%- if p == page1page|int(default=1) %}"p[{{p}}].b[1].bco={{colorcode}}"{%- else -%}"p[{{p}}].b[1].bco2={{colorcode}}"{%- endif -%},{%- endfor -%} {% else %}{%- for p in range(1,12) %}"p[{{p}}].b[1].bco2={{colorcode}}",{%- endfor -%}{%- endif -%} - {%- for p in range(1,12) %}{%- if p == page2page|int %}"p[{{p}}].b[2].bco={{colorcode}}"{%- else -%}"p[{{p}}].b[2].bco2={{colorcode}}"{%- endif -%},{%- endfor -%} - {%- if not page_scroll -%}{%- for p in range(1,12) %}{%- if p == page3page|int %}"p[{{p}}].b[3].bco={{colorcode}}"{%- else -%}"p[{{p}}].b[3].bco2={{colorcode}}"{%- endif -%}{% if not loop.last %},{% endif %}{%- endfor -%} + {%- if not page_scroll -%}{%- for p in range(1,12) %}{%- if p == page2page|int(default=2) %}"p[{{p}}].b[2].bco={{colorcode}}"{%- else -%}"p[{{p}}].b[2].bco2={{colorcode}}"{%- endif -%},{%- endfor -%} + {% else %}{%- for p in range(1,12) %}"p[{{p}}].b[2].bco={{colorcode}}",{%- endfor -%}{%- endif -%} + {%- if not page_scroll -%}{%- for p in range(1,12) %}{%- if p == page3page|int(default=3) %}"p[{{p}}].b[3].bco={{colorcode}}"{%- else -%}"p[{{p}}].b[3].bco2={{colorcode}}"{%- endif -%}{% if not loop.last %},{% endif %}{%- endfor -%} {% else %}{%- for p in range(1,12) %}"p[{{p}}].b[3].bco2={{colorcode}}"{% if not loop.last %},{% endif %}{%- endfor -%}{%- endif -%}] - service: mqtt.publish # apply unselected foreground color to page select buttons data: @@ -747,11 +749,12 @@ action: {%- set red=(state_attr(color.source, "rgb_color")[0] * brightness)|int(default=0) -%} {%- set green=(state_attr(color.source, "rgb_color")[1] * brightness)|int(default=0) -%} {%- set blue=(state_attr(color.source, "rgb_color")[2] * brightness)|int(default=0) -%} - {%- set colorcode = (red|bitwise_and(248)*256) + (green|bitwise_and(252)*8) + (blue|bitwise_and(248)/8)|int -%} - [{%- if not page_scroll -%}{%- for p in range(1,12) %}{%- if p == page1page|int %}"p[{{p}}].b[1].pco2={{colorcode}}"{%- else -%}"p[{{p}}].b[1].pco={{colorcode}}"{%- endif -%},{%- endfor -%} + {%- set colorcode = (red|bitwise_and(248)*256) + (green|bitwise_and(252)*8) + (blue|bitwise_and(248)/8)|int(default=59164) -%} + [{%- if not page_scroll -%}{%- for p in range(1,12) %}{%- if p == page1page|int(default=1) %}"p[{{p}}].b[1].pco2={{colorcode}}"{%- else -%}"p[{{p}}].b[1].pco={{colorcode}}"{%- endif -%},{%- endfor -%} {% else %}{%- for p in range(1,12) %}"p[{{p}}].b[1].pco={{colorcode}}",{%- endfor -%}{%- endif -%} - {%- for p in range(1,12) %}{%- if p == page2page|int %}"p[{{p}}].b[2].pco2={{colorcode}}"{%- else -%}"p[{{p}}].b[2].pco={{colorcode}}"{%- endif -%},{%- endfor -%} - {%- if not page_scroll -%}{%- for p in range(1,12) %}{%- if p == page3page|int %}"p[{{p}}].b[3].pco2={{colorcode}}"{%- else -%}"p[{{p}}].b[3].pco={{colorcode}}"{%- endif -%}{% if not loop.last %},{% endif %}{%- endfor -%} + {%- if not page_scroll -%}{%- for p in range(1,12) %}{%- if p == page2page|int(default=2) %}"p[{{p}}].b[2].pco2={{colorcode}}"{%- else -%}"p[{{p}}].b[2].pco={{colorcode}}"{%- endif -%},{%- endfor -%} + {% else %}{%- for p in range(1,12) %}"p[{{p}}].b[2].pco2={{colorcode}}",{%- endfor -%}{%- endif -%} + {%- if not page_scroll -%}{%- for p in range(1,12) %}{%- if p == page3page|int(default=3) %}"p[{{p}}].b[3].pco2={{colorcode}}"{%- else -%}"p[{{p}}].b[3].pco={{colorcode}}"{%- endif -%}{% if not loop.last %},{% endif %}{%- endfor -%} {% else %}{%- for p in range(1,12) %}"p[{{p}}].b[3].pco={{colorcode}}"{% if not loop.last %},{% endif %}{%- endfor -%}{%- endif -%}] - service: mqtt.publish # apply unselected background color to page select buttons data: @@ -767,11 +770,12 @@ action: {%- set red=(state_attr(color.source, "rgb_color")[0] * brightness)|int(default=0) -%} {%- set green=(state_attr(color.source, "rgb_color")[1] * brightness)|int(default=0) -%} {%- set blue=(state_attr(color.source, "rgb_color")[2] * brightness)|int(default=0) -%} - {%- set colorcode = (red|bitwise_and(248)*256) + (green|bitwise_and(252)*8) + (blue|bitwise_and(248)/8)|int -%} - [{%- if not page_scroll -%}{%- for p in range(1,12) %}{%- if p == page1page|int %}"p[{{p}}].b[1].bco2={{colorcode}}"{%- else -%}"p[{{p}}].b[1].bco={{colorcode}}"{%- endif -%},{%- endfor -%} + {%- set colorcode = (red|bitwise_and(248)*256) + (green|bitwise_and(252)*8) + (blue|bitwise_and(248)/8)|int(default=16904) -%} + [{%- if not page_scroll -%}{%- for p in range(1,12) %}{%- if p == page1page|int(default=1) %}"p[{{p}}].b[1].bco2={{colorcode}}"{%- else -%}"p[{{p}}].b[1].bco={{colorcode}}"{%- endif -%},{%- endfor -%} {% else %}{%- for p in range(1,12) %}"p[{{p}}].b[1].bco={{colorcode}}",{%- endfor -%}{%- endif -%} - {%- for p in range(1,12) %}{%- if p == page2page|int %}"p[{{p}}].b[2].bco2={{colorcode}}"{%- else -%}"p[{{p}}].b[2].bco={{colorcode}}"{%- endif -%},{%- endfor -%} - {%- if not page_scroll -%}{%- for p in range(1,12) %}{%- if p == page3page|int %}"p[{{p}}].b[3].bco2={{colorcode}}"{%- else -%}"p[{{p}}].b[3].bco={{colorcode}}"{%- endif -%}{% if not loop.last %},{% endif %}{%- endfor -%} + {%- if not page_scroll -%}{%- for p in range(1,12) %}{%- if p == page2page|int(default=2) %}"p[{{p}}].b[2].bco2={{colorcode}}"{%- else -%}"p[{{p}}].b[2].bco={{colorcode}}"{%- endif -%},{%- endfor -%} + {% else %}{%- for p in range(1,12) %}"p[{{p}}].b[2].bco2={{colorcode}}",{%- endfor -%}{%- endif -%} + {%- if not page_scroll -%}{%- for p in range(1,12) %}{%- if p == page3page|int(default=3) %}"p[{{p}}].b[3].bco2={{colorcode}}"{%- else -%}"p[{{p}}].b[3].bco={{colorcode}}"{%- endif -%}{% if not loop.last %},{% endif %}{%- endfor -%} {% else %}{%- for p in range(1,12) %}"p[{{p}}].b[3].bco={{colorcode}}"{% if not loop.last %},{% endif %}{%- endfor -%}{%- endif -%}] - service: mqtt.publish # request sensor update data: @@ -847,9 +851,9 @@ action: data: topic: "{{jsoncommandtopic}}" payload: >- - [{%- for p in range(1,12) %}{%- if p == page1page|int %}"p[{{p}}].b[1].pco={{trigger.payload}}"{%- else -%}"p[{{p}}].b[1].pco2={{trigger.payload}}"{%- endif -%},{%- endfor -%} - {%- for p in range(1,12) %}{%- if p == page2page|int %}"p[{{p}}].b[2].pco={{trigger.payload}}"{%- else -%}"p[{{p}}].b[2].pco2={{trigger.payload}}"{%- endif -%},{%- endfor -%} - {%- for p in range(1,12) %}{%- if p == page3page|int %}"p[{{p}}].b[3].pco={{trigger.payload}}"{%- else -%}"p[{{p}}].b[3].pco2={{trigger.payload}}"{%- endif -%}{% if not loop.last %},{% endif %}{%- endfor -%}] + [{%- for p in range(1,12) %}{%- if p == page1page|int(default=1) %}"p[{{p}}].b[1].pco={{trigger.payload}}"{%- else -%}"p[{{p}}].b[1].pco2={{trigger.payload}}"{%- endif -%},{%- endfor -%} + {%- for p in range(1,12) %}{%- if p == page2page|int(default=2) %}"p[{{p}}].b[2].pco={{trigger.payload}}"{%- else -%}"p[{{p}}].b[2].pco2={{trigger.payload}}"{%- endif -%},{%- endfor -%} + {%- for p in range(1,12) %}{%- if p == page3page|int(default=3) %}"p[{{p}}].b[3].pco={{trigger.payload}}"{%- else -%}"p[{{p}}].b[3].pco2={{trigger.payload}}"{%- endif -%}{% if not loop.last %},{% endif %}{%- endfor -%}] - conditions: - condition: template value_template: "{{ (trigger.platform == 'mqtt') and (trigger.topic == selectedfgtopic) and page_scroll }}" @@ -859,7 +863,7 @@ action: topic: "{{jsoncommandtopic}}" payload: >- [{%- for p in range(1,12) %}"p[{{p}}].b[1].pco2={{trigger.payload}}",{%- endfor -%} - {%- for p in range(1,12) %}{%- if p == page2page|int %}"p[{{p}}].b[2].pco={{trigger.payload}}"{%- else -%}"p[{{p}}].b[2].pco2={{trigger.payload}}"{%- endif -%},{%- endfor -%} + {%- for p in range(1,12) %}"p[{{p}}].b[2].pco={{trigger.payload}}",{%- endfor -%} {%- for p in range(1,12) %}"p[{{p}}].b[3].pco2={{trigger.payload}}"{% if not loop.last %},{% endif %}{%- endfor -%}] - conditions: - condition: template @@ -869,9 +873,9 @@ action: data: topic: "{{jsoncommandtopic}}" payload: >- - [{%- for p in range(1,12) %}{%- if p == page1page|int %}"p[{{p}}].b[1].bco={{trigger.payload}}"{%- else -%}"p[{{p}}].b[1].bco2={{trigger.payload}}"{%- endif -%},{%- endfor -%} - {%- for p in range(1,12) %}{%- if p == page2page|int %}"p[{{p}}].b[2].bco={{trigger.payload}}"{%- else -%}"p[{{p}}].b[2].bco2={{trigger.payload}}"{%- endif -%},{%- endfor -%} - {%- for p in range(1,12) %}{%- if p == page3page|int %}"p[{{p}}].b[3].bco={{trigger.payload}}"{%- else -%}"p[{{p}}].b[3].bco2={{trigger.payload}}"{%- endif -%}{% if not loop.last %},{% endif %}{%- endfor -%}] + [{%- for p in range(1,12) %}{%- if p == page1page|int(default=1) %}"p[{{p}}].b[1].bco={{trigger.payload}}"{%- else -%}"p[{{p}}].b[1].bco2={{trigger.payload}}"{%- endif -%},{%- endfor -%} + {%- for p in range(1,12) %}{%- if p == page2page|int(default=2) %}"p[{{p}}].b[2].bco={{trigger.payload}}"{%- else -%}"p[{{p}}].b[2].bco2={{trigger.payload}}"{%- endif -%},{%- endfor -%} + {%- for p in range(1,12) %}{%- if p == page3page|int(default=3) %}"p[{{p}}].b[3].bco={{trigger.payload}}"{%- else -%}"p[{{p}}].b[3].bco2={{trigger.payload}}"{%- endif -%}{% if not loop.last %},{% endif %}{%- endfor -%}] - conditions: - condition: template value_template: "{{ (trigger.platform == 'mqtt') and (trigger.topic == selectedbgtopic) and page_scroll }}" @@ -881,7 +885,7 @@ action: topic: "{{jsoncommandtopic}}" payload: >- [{%- for p in range(1,12) %}"p[{{p}}].b[1].bco2={{trigger.payload}}",{%- endfor -%} - {%- for p in range(1,12) %}{%- if p == page2page|int %}"p[{{p}}].b[2].bco={{trigger.payload}}"{%- else -%}"p[{{p}}].b[2].bco2={{trigger.payload}}"{%- endif -%},{%- endfor -%} + {%- for p in range(1,12) %}"p[{{p}}].b[2].bco={{trigger.payload}}",{%- endfor -%} {%- for p in range(1,12) %}"p[{{p}}].b[3].bco2={{trigger.payload}}"{% if not loop.last %},{% endif %}{%- endfor -%}] - conditions: - condition: template @@ -891,9 +895,9 @@ action: data: topic: "{{jsoncommandtopic}}" payload: >- - [{%- for p in range(1,12) %}{%- if p == page1page|int %}"p[{{p}}].b[1].pco2={{trigger.payload}}"{%- else -%}"p[{{p}}].b[1].pco={{trigger.payload}}"{%- endif -%},{%- endfor -%} - {%- for p in range(1,12) %}{%- if p == page2page|int %}"p[{{p}}].b[2].pco2={{trigger.payload}}"{%- else -%}"p[{{p}}].b[2].pco={{trigger.payload}}"{%- endif -%},{%- endfor -%} - {%- for p in range(1,12) %}{%- if p == page3page|int %}"p[{{p}}].b[3].pco2={{trigger.payload}}"{%- else -%}"p[{{p}}].b[3].pco={{trigger.payload}}"{%- endif -%}{% if not loop.last %},{% endif %}{%- endfor -%}] + [{%- for p in range(1,12) %}{%- if p == page1page|int(default=1) %}"p[{{p}}].b[1].pco2={{trigger.payload}}"{%- else -%}"p[{{p}}].b[1].pco={{trigger.payload}}"{%- endif -%},{%- endfor -%} + {%- for p in range(1,12) %}{%- if p == page2page|int(default=2) %}"p[{{p}}].b[2].pco2={{trigger.payload}}"{%- else -%}"p[{{p}}].b[2].pco={{trigger.payload}}"{%- endif -%},{%- endfor -%} + {%- for p in range(1,12) %}{%- if p == page3page|int(default=3) %}"p[{{p}}].b[3].pco2={{trigger.payload}}"{%- else -%}"p[{{p}}].b[3].pco={{trigger.payload}}"{%- endif -%}{% if not loop.last %},{% endif %}{%- endfor -%}] - conditions: - condition: template value_template: "{{ (trigger.platform == 'mqtt') and (trigger.topic == unselectedfgtopic) and page_scroll }}" @@ -903,7 +907,7 @@ action: topic: "{{jsoncommandtopic}}" payload: >- [{%- for p in range(1,12) %}"p[{{p}}].b[1].pco={{trigger.payload}}",{%- endfor -%} - {%- for p in range(1,12) %}{%- if p == page2page|int %}"p[{{p}}].b[2].pco2={{trigger.payload}}"{%- else -%}"p[{{p}}].b[2].pco={{trigger.payload}}"{%- endif -%},{%- endfor -%} + {%- for p in range(1,12) %}"p[{{p}}].b[2].pco2={{trigger.payload}}",{%- endfor -%} {%- for p in range(1,12) %}"p[{{p}}].b[3].pco={{trigger.payload}}"{% if not loop.last %},{% endif %}{%- endfor -%}] - conditions: - condition: template @@ -913,9 +917,9 @@ action: data: topic: "{{jsoncommandtopic}}" payload: >- - [{%- for p in range(1,12) %}{%- if p == page1page|int %}"p[{{p}}].b[1].bco2={{trigger.payload}}"{%- else -%}"p[{{p}}].b[1].bco={{trigger.payload}}"{%- endif -%},{%- endfor -%} - {%- for p in range(1,12) %}{%- if p == page2page|int %}"p[{{p}}].b[2].bco2={{trigger.payload}}"{%- else -%}"p[{{p}}].b[2].bco={{trigger.payload}}"{%- endif -%},{%- endfor -%} - {%- for p in range(1,12) %}{%- if p == page3page|int %}"p[{{p}}].b[3].bco2={{trigger.payload}}"{%- else -%}"p[{{p}}].b[3].bco={{trigger.payload}}"{%- endif -%}{% if not loop.last %},{% endif %}{%- endfor -%}] + [{%- for p in range(1,12) %}{%- if p == page1page|int(default=1) %}"p[{{p}}].b[1].bco2={{trigger.payload}}"{%- else -%}"p[{{p}}].b[1].bco={{trigger.payload}}"{%- endif -%},{%- endfor -%} + {%- for p in range(1,12) %}{%- if p == page2page|int(default=2) %}"p[{{p}}].b[2].bco2={{trigger.payload}}"{%- else -%}"p[{{p}}].b[2].bco={{trigger.payload}}"{%- endif -%},{%- endfor -%} + {%- for p in range(1,12) %}{%- if p == page3page|int(default=3) %}"p[{{p}}].b[3].bco2={{trigger.payload}}"{%- else -%}"p[{{p}}].b[3].bco={{trigger.payload}}"{%- endif -%}{% if not loop.last %},{% endif %}{%- endfor -%}] - conditions: - condition: template value_template: "{{ (trigger.platform == 'mqtt') and (trigger.topic == unselectedbgtopic) and page_scroll }}" @@ -925,5 +929,5 @@ action: topic: "{{jsoncommandtopic}}" payload: >- [{%- for p in range(1,12) %}"p[{{p}}].b[1].bco={{trigger.payload}}",{%- endfor -%} - {%- for p in range(1,12) %}{%- if p == page2page|int %}"p[{{p}}].b[2].bco2={{trigger.payload}}"{%- else -%}"p[{{p}}].b[2].bco={{trigger.payload}}"{%- endif -%},{%- endfor -%} + {%- for p in range(1,12) %}"p[{{p}}].b[2].bco2={{trigger.payload}}",{%- endfor -%} {%- for p in range(1,12) %}"p[{{p}}].b[3].bco={{trigger.payload}}"{% if not loop.last %},{% endif %}{%- endfor -%}] diff --git a/Home_Assistant/blueprints/hasp_Create_Device_Triggers.yaml b/Home_Assistant/blueprints/hasp_Create_Device_Triggers.yaml index f517a56..3c09747 100644 --- a/Home_Assistant/blueprints/hasp_Create_Device_Triggers.yaml +++ b/Home_Assistant/blueprints/hasp_Create_Device_Triggers.yaml @@ -2,7 +2,7 @@ blueprint: name: "HASPone create device triggers" description: | - ## Blueprint Version: `1.05.00` + ## Blueprint Version: `1.06.00` # Description diff --git a/Home_Assistant/blueprints/hasp_Cycle_Automations.yaml b/Home_Assistant/blueprints/hasp_Cycle_Automations.yaml index a333188..85ad573 100644 --- a/Home_Assistant/blueprints/hasp_Cycle_Automations.yaml +++ b/Home_Assistant/blueprints/hasp_Cycle_Automations.yaml @@ -2,7 +2,7 @@ blueprint: name: "HASPone p[x].b[y] cycles through multiple automations" description: | - ## Blueprint Version: `1.05.00` + ## Blueprint Version: `1.06.00` ## Description diff --git a/Home_Assistant/blueprints/hasp_Dim_Screen_on_Idle.yaml b/Home_Assistant/blueprints/hasp_Dim_Screen_on_Idle.yaml index dcfb74e..0548dae 100755 --- a/Home_Assistant/blueprints/hasp_Dim_Screen_on_Idle.yaml +++ b/Home_Assistant/blueprints/hasp_Dim_Screen_on_Idle.yaml @@ -2,7 +2,7 @@ blueprint: name: "HASPone dim the display screen after a specified period of inactivity" description: | - ## Blueprint Version: `1.05.00` + ## Blueprint Version: `1.06.00` # Description diff --git a/Home_Assistant/blueprints/hasp_Dim_Screen_with_Sun.yaml b/Home_Assistant/blueprints/hasp_Dim_Screen_with_Sun.yaml index 5c0d478..4787fd6 100644 --- a/Home_Assistant/blueprints/hasp_Dim_Screen_with_Sun.yaml +++ b/Home_Assistant/blueprints/hasp_Dim_Screen_with_Sun.yaml @@ -2,7 +2,7 @@ blueprint: name: "HASPone dims the backlight with the sun" description: | - ## Blueprint Version: `1.05.00` + ## Blueprint Version: `1.06.00` # Description diff --git a/Home_Assistant/blueprints/hasp_Display_Alarm_Control_page7.yaml b/Home_Assistant/blueprints/hasp_Display_Alarm_Control_page7.yaml index 1866fa1..40964c4 100644 --- a/Home_Assistant/blueprints/hasp_Display_Alarm_Control_page7.yaml +++ b/Home_Assistant/blueprints/hasp_Display_Alarm_Control_page7.yaml @@ -2,7 +2,7 @@ blueprint: name: "HASPone p[7].b[all] displays an alarm control panel" description: | - ## Blueprint Version: `1.05.00` + ## Blueprint Version: `1.06.00` # Description diff --git a/Home_Assistant/blueprints/hasp_Display_Calendar_with_Icon.yaml b/Home_Assistant/blueprints/hasp_Display_Calendar_with_Icon.yaml index 6a3ed23..c78fed3 100644 --- a/Home_Assistant/blueprints/hasp_Display_Calendar_with_Icon.yaml +++ b/Home_Assistant/blueprints/hasp_Display_Calendar_with_Icon.yaml @@ -2,7 +2,7 @@ blueprint: name: "HASPone p[x].b[y] displays the month + date with a calendar icon" description: | - ## Blueprint Version: `1.05.00` + ## Blueprint Version: `1.06.00` # Description diff --git a/Home_Assistant/blueprints/hasp_Display_Clock.yaml b/Home_Assistant/blueprints/hasp_Display_Clock.yaml index 587ca8a..b1ae774 100644 --- a/Home_Assistant/blueprints/hasp_Display_Clock.yaml +++ b/Home_Assistant/blueprints/hasp_Display_Clock.yaml @@ -2,7 +2,7 @@ blueprint: name: "HASPone p[x].b[y] displays a clock" description: | - ## Blueprint Version: `1.05.00` + ## Blueprint Version: `1.06.00` # Description @@ -165,11 +165,11 @@ variables: hasppage: !input hasppage haspbutton: !input haspbutton font_select: !input font_select - font: '{{ font_select.split(" - ")[0] | int }}' + font: '{{ font_select.split(" - ")[0] | int(default=8) }}' xcen_select: !input xcen_select - xcen: '{{ xcen_select.split(" - ")[0] | int }}' + xcen: '{{ xcen_select.split(" - ")[0] | int(default=1) }}' ycen_select: !input ycen_select - ycen: '{{ ycen_select.split(" - ")[0] | int }}' + ycen: '{{ ycen_select.split(" - ")[0] | int(default=1) }}' hour24: !input hour24 ampm: !input ampm wrap: !input wrap diff --git a/Home_Assistant/blueprints/hasp_Display_Clock_with_Icon.yaml b/Home_Assistant/blueprints/hasp_Display_Clock_with_Icon.yaml index 52872d6..c841d46 100644 --- a/Home_Assistant/blueprints/hasp_Display_Clock_with_Icon.yaml +++ b/Home_Assistant/blueprints/hasp_Display_Clock_with_Icon.yaml @@ -2,7 +2,7 @@ blueprint: name: "HASPone p[x].b[y] displays a clock with a clock icon" description: | - ## Blueprint Version: `1.05.00` + ## Blueprint Version: `1.06.00` # Description diff --git a/Home_Assistant/blueprints/hasp_Display_Color_Swatches.yaml b/Home_Assistant/blueprints/hasp_Display_Color_Swatches.yaml new file mode 100644 index 0000000..80aa862 --- /dev/null +++ b/Home_Assistant/blueprints/hasp_Display_Color_Swatches.yaml @@ -0,0 +1,1216 @@ +blueprint: + name: "HASP p[x].b[y] displays color swatches" + description: | + + ## Blueprint Version: `1.06.00` + + ## Description + + Display color select swatches and dimmer for RGB light control + + ![Preview](https://raw.githubusercontent.com/HASwitchPlate/HASPone/blueprint-dev/images/hasp_Display_Color_Swatches.png) + + Creates a labeled button somewhere on pages 1 through 10. When pressed, user is shown a set of 12 color swatches along with dimmer controls. When complete, user selects "return" to navigate back to the previous page. + + --- + + # ⚠️ WARNING ⚠️ + + ## All HASPone blueprints must be updated to version 1.05 or later before deploying this blueprint! + + --- + + ## HASP Page and Button Reference + + The images below show each available HASP page along with the layout of available button objects. + +
+ + | Page 0 | Pages 1-3 | Pages 4-5 | + |--------|-----------|-----------| + | ![Page 0](https://raw.githubusercontent.com/HASwitchPlate/HASPone/main/images/NextionUI_p0_Init_Screen.png) | ![Pages 1-3](https://raw.githubusercontent.com/HASwitchPlate/HASPone/main/images/NextionUI_p1-p3_4buttons.png) | ![Pages 4-5](https://raw.githubusercontent.com/HASwitchPlate/HASPone/main/images/NextionUI_p4-p5_3sliders.png) | + + | Page 6 | Page 7 | Page 8 | + |--------|--------|--------| + | ![Page 6](https://raw.githubusercontent.com/HASwitchPlate/HASPone/main/images/NextionUI_p6_8buttons.png) | ![Page 7](https://raw.githubusercontent.com/HASwitchPlate/HASPone/main/images/NextionUI_p7_12buttons.png) | ![Page 8](https://raw.githubusercontent.com/HASwitchPlate/HASPone/main/images/NextionUI_p8_5buttons+1slider.png) | + + | Page 9 | Page 10 | Page 11 | + |--------|---------|---------| + | ![Page 9](https://raw.githubusercontent.com/HASwitchPlate/HASPone/main/images/NextionUI_p9_9buttons.png) | ![Page 10](https://raw.githubusercontent.com/HASwitchPlate/HASPone/main/images/NextionUI_p10_5buttons.png) | ![Page 11](https://raw.githubusercontent.com/HASwitchPlate/HASPone/main/images/NextionUI_p11_1button+1slider.png) + +
+ + ## HASP Font Reference + +
+ + The Nextion display supports monospaced and proportional fonts. For monospace fonts, the HASP project includes [Consolas](https://docs.microsoft.com/en-us/typography/font-list/consolas) monospace in 4 sizes, [Webdings](https://en.wikipedia.org/wiki/Webdings#Character_set) in 1 size, and [Google's "Noto Sans"](https://github.com/googlefonts/noto-fonts) proportional in 5 sizes + + | Font | Name | Characters per line | Lines per button | + | :--- | :---------------- | :-------------------| :--------------- | + | 0 | Consolas 24 | 20 characters | 2 lines | + | 1 | Consolas 32 | 15 characters | 2 lines | + | 2 | Consolas 48 | 10 characters | 1 line | + | 3 | Consolas 80 | 6 characters | 1 line | + | 4 | Webdings 56 | 8 characters | 1 line | + | 5 | Noto Sans 24 | Proportional | 2 lines | + | 6 | Noto Sans 32 | Proportional | 2 lines | + | 7 | Noto Sans 48 | Proportional | 1 line | + | 8 | Noto Sans 64 | Proportional | 1 line | + | 9 | Noto Sans 80 | Proportional | 1 line | + | 10 | Noto Sans Bold 80 | Proportional | 1 line | + + ### Icons + + Fonts 5-10 also include [1400+ icons which you can copy and paste from here](https://htmlpreview.github.io/?https://github.com/HASwitchPlate/HASPone/blob/main/images/hasp-fontawesome5.html) + + ### Font examples + + ![HASP Fonts 0-3](https://raw.githubusercontent.com/HASwitchPlate/HASPone/main/images/NextionUI_Fonts_0-3.png) ![HASP Fonts 4-7](https://raw.githubusercontent.com/HASwitchPlate/HASPone/main/images/NextionUI_Fonts_4-7.png) ![HASP Fonts 8-10](https://raw.githubusercontent.com/HASwitchPlate/HASPone/main/images/NextionUI_Fonts_8-10.png) + +
+ + ## Nextion color codes + +
+ + The Nextion environment utilizes RGB 565 encoding. [Use this handy convertor](https://nodtem66.github.io/nextion-hmi-color-convert/index.html) to select your colors and convert to the RGB 565 format. + + Here are some example colors: + + | Color | Code | + |--------|-------| + | White | 65535 | + | Black | 0 | + | Grey | 25388 | + | Red | 63488 | + | Green | 2016 | + | Blue | 31 | + | Yellow | 65504 | + | Orange | 64512 | + | Brown | 48192 | + +
+ + domain: automation + input: + haspdevice: + name: "HASP Device" + description: "Select the HASP device" + selector: + device: + integration: mqtt + manufacturer: "HASwitchPlate" + model: "HASPone v1.0.0" + hasppage: + name: "HASP Page" + description: "Select the HASP page (1-11) for this button. Refer to the HASP Page and Button reference above." + default: 1 + selector: + number: + min: 1 + max: 11 + mode: slider + unit_of_measurement: page + haspbutton: + name: "HASP Button" + description: "Select the HASP button. Refer to the HASP Page and Button reference above." + default: 4 + selector: + number: + min: 4 + max: 15 + mode: slider + unit_of_measurement: button + text: + name: "Button text" + description: "Enter text to be displayed on the button." + default: "Color Light" + selector: + text: + colorlight: + name: "Color-capable Light to control" + description: "Select a light device which supports color" + selector: + entity: + domain: light + colors: + name: "Colors" + description: "Define the color shown on the display and the value sent to the controlled light for each of the 12 available buttons. `nextion_color` defines the Nextion Color Code sent to the HASPone device. `color_mode` defines the light.turn_on color mode parameter used. `color_value` is the color information sent to the light" + default: + button01: + nextion_color: 65098 + color_mode: rgb_color + color_value: + - 255 + - 202 + - 85 + button02: + nextion_color: 53021 + color_mode: rgb_color + color_value: + - 206 + - 228 + - 239 + button03: + nextion_color: 12953 + color_mode: rgb_color + color_value: + - 50 + - 80 + - 206 + button04: + nextion_color: 51655 + color_mode: rgb_color + color_value: + - 204 + - 58 + - 58 + button05: + nextion_color: 65400 + color_mode: rgb_color + color_value: + - 255 + - 238 + - 199 + button06: + nextion_color: 59294 + color_mode: rgb_color + color_value: + - 230 + - 240 + - 244 + button07: + nextion_color: 35965 + color_mode: rgb_color + color_value: + - 137 + - 142 + - 239 + button08: + nextion_color: 41561 + color_mode: rgb_color + color_value: + - 164 + - 73 + - 206 + button09: + nextion_color: 65501 + color_mode: rgb_color + color_value: + - 255 + - 250 + - 238 + button10: + nextion_color: 65535 + color_mode: rgb_color + color_value: + - 255 + - 255 + - 251 + button11: + nextion_color: 3372 + color_mode: rgb_color + color_value: + - 15 + - 165 + - 104 + button12: + nextion_color: 41065 + color_mode: rgb_color + color_value: + - 165 + - 15 + - 76 + selector: + object: + font_select: + name: "Font" + description: "Select the font for the displayed text. Refer to the HASP Font Reference above." + default: "8 - Noto Sans 64" + selector: + select: + options: + - "0 - Consolas 24" + - "1 - Consolas 32" + - "2 - Consolas 48" + - "3 - Consolas 80" + - "4 - Webdings 56" + - "5 - Noto Sans 24" + - "6 - Noto Sans 32" + - "7 - Noto Sans 48" + - "8 - Noto Sans 64" + - "9 - Noto Sans 80" + - "10 - Noto Sans Bold 80" + xcen_select: + name: "Text horizontal alignment" + description: "Horizontal text alignment: 0=Left 1=Center 2=Right" + default: "1 - Centered" + selector: + select: + options: + - "0 - Left aligned" + - "1 - Centered" + - "2 - Right aligned" + ycen_select: + name: "Text vertical alignment" + description: "Vertical text alignment: 0=Top 1=Center 2=Bottom" + default: "1 - Centered" + selector: + select: + options: + - "0 - Top aligned" + - "1 - Centered" + - "2 - Bottom aligned" + wrap: + name: "Text wrap" + description: "Enable line-wrapping text if too long to fit in the button." + default: false + selector: + boolean: + text_enable: + name: "Text enabled" + description: "Enable text and styling on selected button. Disable this if using some other blueprint to label this button." + default: true + selector: + boolean: + icon_on: + name: '"On" state icon' + description: 'Enter the icon to be shown when the selected entity is "on"' + default: "" + selector: + text: + icon_off: + name: '"Off" state icon' + description: 'Enter the icon to be shown when the selected entity is "off"' + default: "" + selector: + text: + selected_fgcolor: + name: "Button selected foreground color" + description: 'Selected foreground color in Nextion RGB565 format for the control and return buttons (see "Nextion color codes" above for reference). -1 = Current theme selected foreground color.' + default: -1 + selector: + number: + min: -1 + max: 65535 + mode: slider + selected_bgcolor: + name: "Button selected background color" + description: 'Selected background color in Nextion RGB565 format for the control and return buttons (see "Nextion color codes" above for reference). -1 = Current theme selected background color.' + default: -1 + selector: + number: + min: -1 + max: 65535 + mode: slider + unselected_fgcolor: + name: "Button unselected foreground color" + description: 'Unselected foreground color in Nextion RGB565 format for the control and return buttons (see "Nextion color codes" above for reference). -1 = Current theme unselected foreground color.' + default: -1 + selector: + number: + min: -1 + max: 65535 + mode: slider + unselected_bgcolor: + name: "Button unselected background color" + description: 'Unselected background color in Nextion RGB565 format for the control and return buttons (see "Nextion color codes" above for reference). -1 = Current theme unselected background color.' + default: -1 + selector: + number: + min: -1 + max: 65535 + mode: slider + discoveryprefix: + name: "Home Assistant MQTT discovery prefix" + description: 'In nearly all cases this should be "homeassistant"' + default: "homeassistant" + selector: + text: + +mode: parallel +max_exceeded: silent + +variables: + haspdevice: !input haspdevice + hasppage: !input hasppage + haspbutton: !input haspbutton + text: !input text + font_select: !input font_select + font: '{{ font_select.split(" - ")[0] | int }}' + xcen_select: !input xcen_select + xcen: '{{ xcen_select.split(" - ")[0] | int }}' + ycen_select: !input ycen_select + ycen: '{{ ycen_select.split(" - ")[0] | int }}' + wrap: !input wrap + isbr: "{% if wrap == true %}1{% else %}0{% endif %}" + text_enable: !input text_enable + colorlight: !input colorlight + colors: !input colors + icon_on: !input icon_on + icon_off: !input icon_off + icon: '{% if states(colorlight) == "on" %}{{icon_on}}{% else %}{{icon_off}}{% endif %}' + selected_fgcolor: !input selected_fgcolor + selected_bgcolor: !input selected_bgcolor + unselected_fgcolor: !input unselected_fgcolor + unselected_bgcolor: !input unselected_bgcolor + discoveryprefix: !input discoveryprefix + haspname: >- + {%- for entity in device_entities(haspdevice) -%} + {%- if entity|regex_search("^sensor\..+_sensor(?:_\d+|)$") -%} + {{- entity|regex_replace(find="^sensor\.", replace="", ignorecase=true)|regex_replace(find="_sensor(?:_\d+|)$", replace="", ignorecase=true) -}} + {%- endif -%} + {%- endfor -%} + haspobject: '{{ "p[" ~ hasppage ~ "].b[" ~ haspbutton ~ "]" }}' + commandtopic: '{{ "hasp/" ~ haspname ~ "/command/" ~ haspobject }}' + jsontopic: '{{ "hasp/" ~ haspname ~ "/state/json" }}' + jsoncommandtopic: '{{ "hasp/" ~ haspname ~ "/command/json" }}' + activepage: >- + {%- set activepage = namespace() -%} + {%- for entity in device_entities(haspdevice) -%} + {%- if entity|regex_search("^number\..*_active_page(?:_\d+|)$") -%} + {%- set activepage.entity=entity -%} + {%- endif -%} + {%- endfor -%} + {{ states(activepage.entity) | int(default=-1) }} + haspsensor: >- + {%- for entity in device_entities(haspdevice) -%} + {%- if entity|regex_search("^sensor\..+_sensor(?:_\d+|)$") -%} + {{ entity }} + {%- endif -%} + {%- endfor -%} + haspClientId: '{{state_attr(haspsensor, "haspClientID")}}' + haspMac: '{{state_attr(haspsensor, "haspMac")}}' + haspManufacturer: '{{state_attr(haspsensor, "haspManufacturer")}}' + haspModel: '{{state_attr(haspsensor, "haspModel")}}' + sw_version: '{{state_attr(haspsensor, "espVersion")}}' + helper: >- + {%- for entity in device_entities(haspdevice) -%} + {%- if entity|regex_search("^sensor\..+_returnpage_helper(?:_\d+|)$") -%} + {{ entity }} + {%- endif -%} + {%- endfor -%} + helperActiveColorLight: '{{state_attr(helper, "activeEntity")}}' + helperSourceAutomation: '{{state_attr(helper, "sourceAutomation")}}' + selectedfgtopic: '{{ "hasp/" ~ haspname ~ "/light/selectedforegroundcolor/rgb" }}' + selectedbgtopic: '{{ "hasp/" ~ haspname ~ "/light/selectedbackgroundcolor/rgb" }}' + unselectedfgtopic: '{{ "hasp/" ~ haspname ~ "/light/unselectedforegroundcolor/rgb" }}' + unselectedbgtopic: '{{ "hasp/" ~ haspname ~ "/light/unselectedbackgroundcolor/rgb" }}' + selectedfg: >- + {%- if (selected_fgcolor|int) >= 0 -%} + {{ selected_fgcolor }} + {%- else -%} + {%- set color = namespace() -%} + {%- for entity in device_entities(haspdevice) -%} + {%- if entity|regex_search("^light\..*_selected_foreground_color(?:_\d+|)$") -%} + {%- set color.source=entity -%} + {%- endif -%} + {%- endfor -%} + {%- set brightness = state_attr(color.source, "brightness")|int(default=255) / 255 -%} + {%- set red=(state_attr(color.source, "rgb_color")[0] * brightness)|int(default=0) -%} + {%- set green=(state_attr(color.source, "rgb_color")[1] * brightness)|int(default=0) -%} + {%- set blue=(state_attr(color.source, "rgb_color")[2] * brightness)|int(default=0) -%} + {{ (red|bitwise_and(248)*256) + (green|bitwise_and(252)*8) + (blue|bitwise_and(248)/8)|int }} + {%- endif -%} + selectedbg: >- + {%- if (selected_bgcolor|int) >= 0 -%} + {{ selected_bgcolor }} + {%- else -%} + {%- set color = namespace() -%} + {%- for entity in device_entities(haspdevice) -%} + {%- if entity|regex_search("^light\..*_selected_background_color(?:_\d+|)$") -%} + {%- set color.source=entity -%} + {%- endif -%} + {%- endfor -%} + {%- set brightness = state_attr(color.source, "brightness")|int(default=255) / 255 -%} + {%- set red=(state_attr(color.source, "rgb_color")[0] * brightness)|int(default=0) -%} + {%- set green=(state_attr(color.source, "rgb_color")[1] * brightness)|int(default=0) -%} + {%- set blue=(state_attr(color.source, "rgb_color")[2] * brightness)|int(default=0) -%} + {{ (red|bitwise_and(248)*256) + (green|bitwise_and(252)*8) + (blue|bitwise_and(248)/8)|int }} + {%- endif -%} + unselectedfg: >- + {%- if (unselected_fgcolor|int) >= 0 -%} + {{ unselected_fgcolor }} + {%- else -%} + {%- set color = namespace() -%} + {%- for entity in device_entities(haspdevice) -%} + {%- if entity|regex_search("^light\..*_unselected_foreground_color(?:_\d+|)$") -%} + {%- set color.source=entity -%} + {%- endif -%} + {%- endfor -%} + {%- set brightness = state_attr(color.source, "brightness")|int(default=255) / 255 -%} + {%- set red=(state_attr(color.source, "rgb_color")[0] * brightness)|int(default=0) -%} + {%- set green=(state_attr(color.source, "rgb_color")[1] * brightness)|int(default=0) -%} + {%- set blue=(state_attr(color.source, "rgb_color")[2] * brightness)|int(default=0) -%} + {{ (red|bitwise_and(248)*256) + (green|bitwise_and(252)*8) + (blue|bitwise_and(248)/8)|int }} + {%- endif -%} + unselectedbg: >- + {%- if (unselected_bgcolor|int) >= 0 -%} + {{ unselected_bgcolor }} + {%- else -%} + {%- set color = namespace() -%} + {%- for entity in device_entities(haspdevice) -%} + {%- if entity|regex_search("^light\..*_unselected_background_color(?:_\d+|)$") -%} + {%- set color.source=entity -%} + {%- endif -%} + {%- endfor -%} + {%- set brightness = state_attr(color.source, "brightness")|int(default=255) / 255 -%} + {%- set red=(state_attr(color.source, "rgb_color")[0] * brightness)|int(default=0) -%} + {%- set green=(state_attr(color.source, "rgb_color")[1] * brightness)|int(default=0) -%} + {%- set blue=(state_attr(color.source, "rgb_color")[2] * brightness)|int(default=0) -%} + {{ (red|bitwise_and(248)*256) + (green|bitwise_and(252)*8) + (blue|bitwise_and(248)/8)|int }} + {%- endif -%} + brightness: '{% if state_attr(colorlight,"brightness") is none %}0{% else %}{{state_attr(colorlight,"brightness")}}{% endif %}' + +trigger_variables: + haspdevice: !input haspdevice + haspname: >- + {%- for entity in device_entities(haspdevice) -%} + {%- if entity|regex_search("^sensor\..+_sensor(?:_\d+|)$") -%} + {{- entity|regex_replace(find="^sensor\.", replace="", ignorecase=true)|regex_replace(find="_sensor(?:_\d+|)$", replace="", ignorecase=true) -}} + {%- endif -%} + {%- endfor -%} + haspsensor: >- + {%- for entity in device_entities(haspdevice) -%} + {%- if entity|regex_search("^sensor\..+_sensor(?:_\d+|)$") -%} + {{ entity }} + {%- endif -%} + {%- endfor -%} + jsontopic: '{{ "hasp/" ~ haspname ~ "/state/json" }}' + selectedfgtopic: '{{ "hasp/" ~ haspname ~ "/light/selectedforegroundcolor/rgb" }}' + selectedbgtopic: '{{ "hasp/" ~ haspname ~ "/light/selectedbackgroundcolor/rgb" }}' + unselectedfgtopic: '{{ "hasp/" ~ haspname ~ "/light/unselectedforegroundcolor/rgb" }}' + unselectedbgtopic: '{{ "hasp/" ~ haspname ~ "/light/unselectedbackgroundcolor/rgb" }}' + +trigger: + - platform: homeassistant + event: start + - platform: template + value_template: "{{ is_state(haspsensor, 'ON') }}" + - platform: mqtt + topic: "{{jsontopic}}" + - platform: mqtt + topic: "{{selectedfgtopic}}" + - platform: mqtt + topic: "{{selectedbgtopic}}" + - platform: mqtt + topic: "{{unselectedfgtopic}}" + - platform: mqtt + topic: "{{unselectedbgtopic}}" + - platform: state + entity_id: !input colorlight + +condition: + - condition: template + value_template: "{{ is_state(haspsensor, 'ON') }}" + +action: + - choose: + ######################################################################### + # RUN ACTIONS or Home Assistant Startup or HASP Connect + - conditions: + - condition: template + value_template: >- + {{ + (trigger is not defined) + or + (trigger.platform is none) + or + ((trigger.platform == 'homeassistant') and (trigger.event == 'start')) + or + ((trigger.platform == 'template') and (trigger.entity_id == haspsensor) and (trigger.to_state.state == 'ON')) + }} + sequence: + # Create returnpage helper + - service: mqtt.publish + data: + topic: "{{discoveryprefix}}/sensor/{{haspname}}/returnpage/config" + payload: >- + {"name":"{{haspname}} returnpage helper", + "json_attributes_topic":"hasp/{{haspname}}/returnpage/command", + "state_topic":"hasp/{{haspname}}/status", + "availability":{"topic":"hasp/{{haspname}}/alwayson","payload_available":"ON"}, + "retain":true, + "optimistic":true, + "min":1, + "max":10, + "icon":"mdi:palette", + "unique_id":"{{haspClientId}}-returnpage", + "device":{ + "identifiers":["{{haspClientId}}"], + "name":"{{haspname}}", + "manufacturer":"{{haspManufacturer}}", + "model":"{{haspModel}}", + "sw_version":{{sw_version}} + }} + retain: true + # Make sure returnpage is available + - service: mqtt.publish + data: + topic: "hasp/{{haspname}}/alwayson" + payload: "ON" + retain: true + - choose: + ######################################################################### + # Display text and apply text style on source button + - conditions: + - condition: template + value_template: "{{ text_enable }}" + sequence: + - service: mqtt.publish + data: + topic: "{{jsoncommandtopic}}" + payload: >- + [ + "{{haspobject}}.font={{font}}", + "{{haspobject}}.xcen={{xcen}}", + "{{haspobject}}.ycen={{ycen}}", + "{{haspobject}}.isbr={{isbr}}", + "{{haspobject}}.pco={{selectedfg}}", + "{{haspobject}}.bco={{selectedbg}}", + "{{haspobject}}.pco2={{unselectedfg}}", + "{{haspobject}}.bco2={{unselectedbg}}", + "{{haspobject}}.txt=\"{{text}}\"" + ] + ######################################################################### + # Colorlight brightness has changed + - conditions: + - condition: template + value_template: >- + {{ + (trigger.platform == 'state') + and + (trigger.entity_id == colorlight) + and + (trigger.from_state.attributes.brightness is defined) + and + (trigger.to_state.attributes.brightness is defined) + and + (trigger.from_state.attributes.brightness != trigger.to_state.attributes.brightness) + and + (activepage == 11) + and + (helperSourceAutomation == this.entity_id) + }} + sequence: + # Update slider + - service: mqtt.publish + data: + topic: "{{jsoncommandtopic}}" + payload: '["p[11].b[5].val={{state_attr(colorlight,"brightness")}}"]' + ######################################################################### + # Colorlight state has changed + - conditions: + - condition: template + value_template: >- + {{ + (trigger.platform == 'state') + and + (trigger.entity_id == colorlight) + and + (trigger.from_state.state is defined) + and + (trigger.to_state.state is defined) + and + (trigger.from_state.state != trigger.to_state.state) + and + (activepage == 11) + and + (helperSourceAutomation == this.entity_id) + }} + sequence: + # Update button and slider + - service: mqtt.publish + data: + topic: "{{jsoncommandtopic}}" + payload: >- + [ + "p[11].b[4].txt=\"{{icon}}\"", + {% if is_state(colorlight,'on') %} + "p[11].b[4].pco={{selectedfg}}", + "p[11].b[4].bco={{selectedbg}}", + "p[11].b[4].pco2={{unselectedfg}}", + "p[11].b[4].bco2={{unselectedbg}}", + {% else %} + "p[11].b[4].pco={{unselectedfg}}", + "p[11].b[4].bco={{unselectedbg}}", + "p[11].b[4].pco2={{selectedfg}}", + "p[11].b[4].bco2={{selectedbg}}", + {% endif %} + "p[11].b[5].val={{brightness}}" + ] + ######################################################################### + # Catch MQTT events + - conditions: + - condition: template + value_template: '{{ trigger.platform == "mqtt" }}' + sequence: + - choose: + ######################################################################### + # Catch incoming JSON messages + - conditions: + - condition: template + value_template: "{{ (trigger.topic == jsontopic) and trigger.payload_json is defined }}" + sequence: + - choose: + ######################################################################### + # Source button was pressed, record returnpage helper info and change to page 11 + - conditions: + - condition: template + value_template: >- + {{ + (trigger.payload_json.event_type == "button_short_release") + and + (trigger.payload_json.event == haspobject) + and + (trigger.payload_json.value == "OFF") + }} + sequence: + - service: mqtt.publish + data: + topic: "{{jsoncommandtopic}}" + payload: >- + [ + "p[11].b[4].txt=\"{{icon}}\"", + {% if is_state(colorlight,'on') %} + "p[11].b[4].pco={{selectedfg}}", + "p[11].b[4].bco={{selectedbg}}", + "p[11].b[4].pco2={{unselectedfg}}", + "p[11].b[4].bco2={{unselectedbg}}", + {% else %} + "p[11].b[4].pco={{unselectedfg}}", + "p[11].b[4].bco={{unselectedbg}}", + "p[11].b[4].pco2={{selectedfg}}", + "p[11].b[4].bco2={{selectedbg}}", + {% endif %} + "p[11].b[5].val={{brightness}}", + "p[11].b[5].pco={{selectedbg}}", + "p[11].b[5].bco={{unselectedbg}}", + "p[11].b[5].bco1={{unselectedbg}}", + "page 11" + ] + # push current automation instance and entity to returnpage sensor + - service: mqtt.publish + data: + topic: "hasp/{{haspname}}/returnpage/command" + payload: >- + { + "activeEntity":"{{colorlight}}", + "sourceAutomation":"{{this.entity_id}}" + } + ######################################################################### + # Page changed to our page, so place the color swatches on the screen. + - conditions: + - condition: template + value_template: >- + {{ + (trigger.payload_json.event == "page") + and + (trigger.payload_json.value == 11) + and + (helperSourceAutomation == this.entity_id) + }} + sequence: + - service: mqtt.publish + data: + topic: "{{jsoncommandtopic}}" + payload: >- + [ + "sendxy=1", + "fill 0,63,59,51,{{colors['button01']['nextion_color']}}", + "fill 60,63,59,51,{{colors['button02']['nextion_color']}}", + "fill 120,63,59,51,{{colors['button03']['nextion_color']}}", + "fill 180,63,60,51,{{colors['button04']['nextion_color']}}", + "fill 0,115,59,51,{{colors['button05']['nextion_color']}}", + "fill 60,115,59,51,{{colors['button06']['nextion_color']}}", + "fill 120,115,59,51,{{colors['button07']['nextion_color']}}", + "fill 180,115,60,51,{{colors['button08']['nextion_color']}}", + "fill 0,167,59,51,{{colors['button09']['nextion_color']}}", + "fill 60,167,59,51,{{colors['button10']['nextion_color']}}", + "fill 120,167,59,51,{{colors['button11']['nextion_color']}}", + "fill 180,167,60,51,{{colors['button12']['nextion_color']}}", + "fill 0,219,240,51,{{selectedbg}}" + ] + - service: mqtt.publish + data: + topic: "{{jsoncommandtopic}}" + payload: >- + [ + "xstr 10,215,90,60,8,{{selectedfg}},0,0,0,3,\"\"", + "xstr 75,210,230,60,8,{{selectedfg}},0,0,0,3,\"Return\"" + ] + ######################################################################### + # Page changed to some other page, clean up after ourselves + - conditions: + - condition: template + value_template: >- + {{ + (trigger.payload_json.event == "page") + and + (trigger.payload_json.value != 11) + and + (helperSourceAutomation == this.entity_id) + }} + sequence: + - service: mqtt.publish + data: + topic: "hasp/{{haspname}}/returnpage/command" + payload: "{}" + ######################################################################### + # Power button button was pressed, toggle power on colorlight + - conditions: + - condition: template + value_template: >- + {{ + (activepage == 11) + and + (helperSourceAutomation == this.entity_id) + and + (trigger.payload_json.event_type == "button_short_release") + and + (trigger.payload_json.event == "p[11].b[4]") + and + (trigger.payload_json.value == "OFF") + }} + sequence: + - service: light.toggle + target: + entity_id: "{{colorlight}}" + ######################################################################### + # Dimmer slider was moved, send brightness value to colorlight + - conditions: + - condition: template + value_template: >- + {{ + (activepage == 11) + and + (helperSourceAutomation == this.entity_id) + and + (trigger.payload_json.event == "p[11].b[5].val") + and + (trigger.payload_json.value is defined) + }} + sequence: + - service: light.turn_on + target: + entity_id: "{{colorlight}}" + data: + brightness: "{{trigger.payload_json.value|int(default=0)}}" + + ######################################################################### + # Return button was pressed, return user to original page and disable touch events + - conditions: + - condition: template + value_template: >- + {{ + (activepage == 11) + and + (helperSourceAutomation == this.entity_id) + and + (trigger.payload_json.event_type == "button_short_press") + and + (trigger.payload_json.event == "touchxy") + and + (trigger.payload_json.touch_event == "OFF") + and + (trigger.payload_json.touchy|int(default=-1) >= 219) + and + (trigger.payload_json.touchy|int(default=-1) <= 270) + }} + sequence: + - service: mqtt.publish + data: + topic: "{{jsoncommandtopic}}" + payload: '["page {{hasppage}}","sendxy=0"]' + ######################################################################### + # button01 was pressed, send color command + - conditions: + - condition: template + value_template: >- + {{ + (activepage == 11) + and + (helperSourceAutomation == this.entity_id) + and + (trigger.payload_json.event_type == "button_short_press") + and + (trigger.payload_json.event == "touchxy") + and + (trigger.payload_json.touch_event == "ON") + and + (trigger.payload_json.touchy|int(default=-1) >= 63) + and + (trigger.payload_json.touchy|int(default=-1) <= 114) + and + (trigger.payload_json.touchx|int(default=-1) >= 0) + and + (trigger.payload_json.touchx|int(default=-1) <= 59) + }} + sequence: + - service: light.turn_on + target: + entity_id: "{{colorlight}}" + data: '{ "{{colors["button01"]["color_mode"]}}": {{colors["button01"]["color_value"]}} }' + ######################################################################### + # button02 was pressed, send color command + - conditions: + - condition: template + value_template: >- + {{ + (activepage == 11) + and + (helperSourceAutomation == this.entity_id) + and + (trigger.payload_json.event_type == "button_short_press") + and + (trigger.payload_json.event == "touchxy") + and + (trigger.payload_json.touch_event == "ON") + and + (trigger.payload_json.touchy|int(default=-1) >= 63) + and + (trigger.payload_json.touchy|int(default=-1) <= 114) + and + (trigger.payload_json.touchx|int(default=-1) >= 60) + and + (trigger.payload_json.touchx|int(default=-1) <= 119) + }} + sequence: + - service: light.turn_on + target: + entity_id: "{{colorlight}}" + data: '{ "{{colors["button02"]["color_mode"]}}": {{colors["button02"]["color_value"]}} }' + ######################################################################### + # button03 was pressed, send color command + - conditions: + - condition: template + value_template: >- + {{ + (activepage == 11) + and + (helperSourceAutomation == this.entity_id) + and + (trigger.payload_json.event_type == "button_short_press") + and + (trigger.payload_json.event == "touchxy") + and + (trigger.payload_json.touch_event == "ON") + and + (trigger.payload_json.touchy|int(default=-1) >= 63) + and + (trigger.payload_json.touchy|int(default=-1) <= 114) + and + (trigger.payload_json.touchx|int(default=-1) >= 120) + and + (trigger.payload_json.touchx|int(default=-1) <= 179) + }} + sequence: + - service: light.turn_on + target: + entity_id: "{{colorlight}}" + data: '{ "{{colors["button03"]["color_mode"]}}": {{colors["button03"]["color_value"]}} }' + ######################################################################### + # button04 was pressed, send color command + - conditions: + - condition: template + value_template: >- + {{ + (activepage == 11) + and + (helperSourceAutomation == this.entity_id) + and + (trigger.payload_json.event_type == "button_short_press") + and + (trigger.payload_json.event == "touchxy") + and + (trigger.payload_json.touch_event == "ON") + and + (trigger.payload_json.touchy|int(default=-1) >= 63) + and + (trigger.payload_json.touchy|int(default=-1) <= 114) + and + (trigger.payload_json.touchx|int(default=-1) >= 180) + and + (trigger.payload_json.touchx|int(default=-1) <= 240) + }} + sequence: + - service: light.turn_on + target: + entity_id: "{{colorlight}}" + data: '{ "{{colors["button04"]["color_mode"]}}": {{colors["button04"]["color_value"]}} }' + ######################################################################### + # button05 was pressed, send color command + - conditions: + - condition: template + value_template: >- + {{ + (activepage == 11) + and + (helperSourceAutomation == this.entity_id) + and + (trigger.payload_json.event_type == "button_short_press") + and + (trigger.payload_json.event == "touchxy") + and + (trigger.payload_json.touch_event == "ON") + and + (trigger.payload_json.touchy|int(default=-1) >= 115) + and + (trigger.payload_json.touchy|int(default=-1) <= 166) + and + (trigger.payload_json.touchx|int(default=-1) >= 0) + and + (trigger.payload_json.touchx|int(default=-1) <= 59) + }} + sequence: + - service: light.turn_on + target: + entity_id: "{{colorlight}}" + data: '{ "{{colors["button05"]["color_mode"]}}": {{colors["button05"]["color_value"]}} }' + ######################################################################### + # button06 was pressed, send color command + - conditions: + - condition: template + value_template: >- + {{ + (activepage == 11) + and + (helperSourceAutomation == this.entity_id) + and + (trigger.payload_json.event_type == "button_short_press") + and + (trigger.payload_json.event == "touchxy") + and + (trigger.payload_json.touch_event == "ON") + and + (trigger.payload_json.touchy|int(default=-1) >= 115) + and + (trigger.payload_json.touchy|int(default=-1) <= 166) + and + (trigger.payload_json.touchx|int(default=-1) >= 60) + and + (trigger.payload_json.touchx|int(default=-1) <= 119) + }} + sequence: + - service: light.turn_on + target: + entity_id: "{{colorlight}}" + data: '{ "{{colors["button06"]["color_mode"]}}": {{colors["button06"]["color_value"]}} }' + ######################################################################### + # button07 was pressed, send color command + - conditions: + - condition: template + value_template: >- + {{ + (activepage == 11) + and + (helperSourceAutomation == this.entity_id) + and + (trigger.payload_json.event_type == "button_short_press") + and + (trigger.payload_json.event == "touchxy") + and + (trigger.payload_json.touch_event == "ON") + and + (trigger.payload_json.touchy|int(default=-1) >= 115) + and + (trigger.payload_json.touchy|int(default=-1) <= 166) + and + (trigger.payload_json.touchx|int(default=-1) >= 120) + and + (trigger.payload_json.touchx|int(default=-1) <= 179) + }} + sequence: + - service: light.turn_on + target: + entity_id: "{{colorlight}}" + data: '{ "{{colors["button07"]["color_mode"]}}": {{colors["button07"]["color_value"]}} }' + ######################################################################### + # button08 was pressed, send color command + - conditions: + - condition: template + value_template: >- + {{ + (activepage == 11) + and + (helperSourceAutomation == this.entity_id) + and + (trigger.payload_json.event_type == "button_short_press") + and + (trigger.payload_json.event == "touchxy") + and + (trigger.payload_json.touch_event == "ON") + and + (trigger.payload_json.touchy|int(default=-1) >= 115) + and + (trigger.payload_json.touchy|int(default=-1) <= 166) + and + (trigger.payload_json.touchx|int(default=-1) >= 180) + and + (trigger.payload_json.touchx|int(default=-1) <= 240) + }} + sequence: + - service: light.turn_on + target: + entity_id: "{{colorlight}}" + data: '{ "{{colors["button08"]["color_mode"]}}": {{colors["button08"]["color_value"]}} }' + ######################################################################### + # button09 was pressed, send color command + - conditions: + - condition: template + value_template: >- + {{ + (activepage == 11) + and + (helperSourceAutomation == this.entity_id) + and + (trigger.payload_json.event_type == "button_short_press") + and + (trigger.payload_json.event == "touchxy") + and + (trigger.payload_json.touch_event == "ON") + and + (trigger.payload_json.touchy|int(default=-1) >= 167) + and + (trigger.payload_json.touchy|int(default=-1) <= 218) + and + (trigger.payload_json.touchx|int(default=-1) >= 0) + and + (trigger.payload_json.touchx|int(default=-1) <= 59) + }} + sequence: + - service: light.turn_on + target: + entity_id: "{{colorlight}}" + data: '{ "{{colors["button09"]["color_mode"]}}": {{colors["button09"]["color_value"]}} }' + ######################################################################### + # button10 was pressed, send color command + - conditions: + - condition: template + value_template: >- + {{ + (activepage == 11) + and + (helperSourceAutomation == this.entity_id) + and + (trigger.payload_json.event_type == "button_short_press") + and + (trigger.payload_json.event == "touchxy") + and + (trigger.payload_json.touch_event == "ON") + and + (trigger.payload_json.touchy|int(default=-1) >= 167) + and + (trigger.payload_json.touchy|int(default=-1) <= 218) + and + (trigger.payload_json.touchx|int(default=-1) >= 60) + and + (trigger.payload_json.touchx|int(default=-1) <= 119) + }} + sequence: + - service: light.turn_on + target: + entity_id: "{{colorlight}}" + data: '{ "{{colors["button10"]["color_mode"]}}": {{colors["button10"]["color_value"]}} }' + ######################################################################### + # button11 was pressed, send color command + - conditions: + - condition: template + value_template: >- + {{ + (activepage == 11) + and + (helperSourceAutomation == this.entity_id) + and + (trigger.payload_json.event_type == "button_short_press") + and + (trigger.payload_json.event == "touchxy") + and + (trigger.payload_json.touch_event == "ON") + and + (trigger.payload_json.touchy|int(default=-1) >= 167) + and + (trigger.payload_json.touchy|int(default=-1) <= 218) + and + (trigger.payload_json.touchx|int(default=-1) >= 120) + and + (trigger.payload_json.touchx|int(default=-1) <= 179) + }} + sequence: + - service: light.turn_on + target: + entity_id: "{{colorlight}}" + data: '{ "{{colors["button11"]["color_mode"]}}": {{colors["button11"]["color_value"]}} }' + ######################################################################### + # button12 was pressed, send color command + - conditions: + - condition: template + value_template: >- + {{ + (activepage == 11) + and + (helperSourceAutomation == this.entity_id) + and + (trigger.payload_json.event_type == "button_short_press") + and + (trigger.payload_json.event == "touchxy") + and + (trigger.payload_json.touch_event == "ON") + and + (trigger.payload_json.touchy|int(default=-1) >= 167) + and + (trigger.payload_json.touchy|int(default=-1) <= 218) + and + (trigger.payload_json.touchx|int(default=-1) >= 180) + and + (trigger.payload_json.touchx|int(default=-1) <= 240) + }} + sequence: + - service: light.turn_on + target: + entity_id: "{{colorlight}}" + data: '{ "{{colors["button12"]["color_mode"]}}": {{colors["button12"]["color_value"]}} }' + ######################################################################### + # Theme: Apply selected foreground color when it changes. + # Any change to the button will remove the overlaid icon. + - conditions: + - condition: template + value_template: "{{ trigger.topic == selectedfgtopic }}" + sequence: + - service: mqtt.publish + data: + topic: "{{commandtopic}}.pco" + payload: "{{trigger.payload}}" + + ######################################################################### + # Theme: Apply selected background color on change + - conditions: + - condition: template + value_template: "{{ trigger.topic == selectedbgtopic }}" + sequence: + - service: mqtt.publish + data: + topic: "{{commandtopic}}.bco" + payload: "{{trigger.payload}}" + + ######################################################################### + # Theme: Apply unselected foreground color on change + - conditions: + - condition: template + value_template: "{{ trigger.topic == unselectedfgtopic }}" + sequence: + - service: mqtt.publish + data: + topic: "{{commandtopic}}.pco2" + payload: "{{trigger.payload}}" + ######################################################################### + # Theme: Apply unselected background color on change + - conditions: + - condition: template + value_template: "{{ trigger.topic == unselectedbgtopic }}" + sequence: + - service: mqtt.publish + data: + topic: "{{commandtopic}}.bco2" + payload: "{{trigger.payload}}" diff --git a/Home_Assistant/blueprints/hasp_Display_Dimmer_with_Icon.yaml b/Home_Assistant/blueprints/hasp_Display_Dimmer_with_Icon.yaml index 659b6bd..e1401ad 100644 --- a/Home_Assistant/blueprints/hasp_Display_Dimmer_with_Icon.yaml +++ b/Home_Assistant/blueprints/hasp_Display_Dimmer_with_Icon.yaml @@ -2,7 +2,7 @@ blueprint: name: "HASPone p[x].b[y] displays a dimmer with a toggle on/off icon" description: | - ## Blueprint Version: `1.05.00` + ## Blueprint Version: `1.06.00` # Description @@ -144,11 +144,11 @@ variables: text_on: !input text_on text_off: !input text_off font_select: !input font_select - font: '{{ font_select.split(" - ")[0] | int }}' + font: '{{ font_select.split(" - ")[0] | int(default=6) }}' xcen_select: !input xcen_select - xcen: '{{ xcen_select.split(" - ")[0] | int }}' + xcen: '{{ xcen_select.split(" - ")[0] | int(default=1) }}' ycen_select: !input ycen_select - ycen: '{{ ycen_select.split(" - ")[0] | int }}' + ycen: '{{ ycen_select.split(" - ")[0] | int(default=1) }}' wrap: !input wrap icon_on: !input icon_on icon_off: !input icon_off diff --git a/Home_Assistant/blueprints/hasp_Display_Entity_State_or_Attribute.yaml b/Home_Assistant/blueprints/hasp_Display_Entity_State_or_Attribute.yaml index c8f4848..c962ef5 100644 --- a/Home_Assistant/blueprints/hasp_Display_Entity_State_or_Attribute.yaml +++ b/Home_Assistant/blueprints/hasp_Display_Entity_State_or_Attribute.yaml @@ -2,7 +2,7 @@ blueprint: name: "HASPone p[x].b[y] displays the state or attribute value of an entity" description: | - ## Blueprint Version: `1.05.00` + ## Blueprint Version: `1.06.00` # Description @@ -239,11 +239,11 @@ variables: prefix: !input prefix suffix: !input suffix font_select: !input font_select - font: '{{ font_select.split(" - ")[0] | int }}' + font: '{{ font_select.split(" - ")[0] | int(default=8) }}' xcen_select: !input xcen_select - xcen: '{{ xcen_select.split(" - ")[0] | int }}' + xcen: '{{ xcen_select.split(" - ")[0] | int(default=1) }}' ycen_select: !input ycen_select - ycen: '{{ ycen_select.split(" - ")[0] | int }}' + ycen: '{{ ycen_select.split(" - ")[0] | int(default=1) }}' wrap: !input wrap title_case: !input title_case selected_fgcolor: !input selected_fgcolor @@ -391,7 +391,7 @@ action: # Display attribute and apply text style - conditions: - condition: template - value_template: >- + value_template: >- {{- (trigger is not defined) or @@ -474,4 +474,3 @@ action: data: topic: "{{commandtopic}}.bco2" payload: "{{trigger.payload}}" - diff --git a/Home_Assistant/blueprints/hasp_Display_Heatpump_Control_page6.yaml b/Home_Assistant/blueprints/hasp_Display_Heatpump_Control_page6.yaml index 78b879f..9491b53 100755 --- a/Home_Assistant/blueprints/hasp_Display_Heatpump_Control_page6.yaml +++ b/Home_Assistant/blueprints/hasp_Display_Heatpump_Control_page6.yaml @@ -2,7 +2,7 @@ blueprint: name: "HASPone p[6].b[all] Page 6 displays Heatpump controls" description: | - ## Blueprint Version: `1.05.00` + ## Blueprint Version: `1.06.00` # Description diff --git a/Home_Assistant/blueprints/hasp_Display_Heatpump_Control_page9.yaml b/Home_Assistant/blueprints/hasp_Display_Heatpump_Control_page9.yaml index b03590a..a883eb4 100755 --- a/Home_Assistant/blueprints/hasp_Display_Heatpump_Control_page9.yaml +++ b/Home_Assistant/blueprints/hasp_Display_Heatpump_Control_page9.yaml @@ -2,7 +2,7 @@ blueprint: name: "HASPone p[9].b[all] Page 9 displays Heatpump controls" description: | - ## Blueprint Version: `1.05.00` + ## Blueprint Version: `1.06.00` # Description diff --git a/Home_Assistant/blueprints/hasp_Display_Media_Control_page8.yaml b/Home_Assistant/blueprints/hasp_Display_Media_Control_page8.yaml index 02cf2ca..3643a78 100644 --- a/Home_Assistant/blueprints/hasp_Display_Media_Control_page8.yaml +++ b/Home_Assistant/blueprints/hasp_Display_Media_Control_page8.yaml @@ -2,7 +2,7 @@ blueprint: name: "HASPone p[8].b[all] Page 8 displays media controls" description: | - ## Blueprint Version: `1.05.00` + ## Blueprint Version: `1.06.00` # Description diff --git a/Home_Assistant/blueprints/hasp_Display_Template.yaml b/Home_Assistant/blueprints/hasp_Display_Template.yaml index 6803888..25910c0 100644 --- a/Home_Assistant/blueprints/hasp_Display_Template.yaml +++ b/Home_Assistant/blueprints/hasp_Display_Template.yaml @@ -2,7 +2,7 @@ blueprint: name: "HASPone p[x].b[y] displays the output of a template" description: | - ## Blueprint Version: `1.05.00` + ## Blueprint Version: `1.06.00` # Description @@ -134,7 +134,7 @@ blueprint: description: "Enter a well-formed [Home Assistant template](https://www.home-assistant.io/docs/configuration/templating/) string. The variable `trigger_entity` will contain the entity name selected above." default: 'Forecast: {{state_attr("weather.home", "forecast")[0].condition|title}}' selector: - text: + template: font_select: name: "Font" description: "Select the font for the displayed text. Refer to the HASPone Font Reference above." @@ -232,11 +232,11 @@ variables: trigger_entity: !input trigger_entity text: !input template font_select: !input font_select - font: '{{ font_select.split(" - ")[0] | int }}' + font: '{{ font_select.split(" - ")[0] | int(default=8) }}' xcen_select: !input xcen_select - xcen: '{{ xcen_select.split(" - ")[0] | int }}' + xcen: '{{ xcen_select.split(" - ")[0] | int(default=1) }}' ycen_select: !input ycen_select - ycen: '{{ ycen_select.split(" - ")[0] | int }}' + ycen: '{{ ycen_select.split(" - ")[0] | int(default=1) }}' wrap: !input wrap selected_fgcolor: !input selected_fgcolor selected_bgcolor: !input selected_bgcolor diff --git a/Home_Assistant/blueprints/hasp_Display_Text.yaml b/Home_Assistant/blueprints/hasp_Display_Text.yaml index d189e8e..bdf3b40 100644 --- a/Home_Assistant/blueprints/hasp_Display_Text.yaml +++ b/Home_Assistant/blueprints/hasp_Display_Text.yaml @@ -2,7 +2,7 @@ blueprint: name: "HASPone p[x].b[y] displays text" description: | - ## Blueprint Version: `1.05.00` + ## Blueprint Version: `1.06.00` ## Description @@ -215,11 +215,11 @@ variables: haspbutton: !input haspbutton text: !input text font_select: !input font_select - font: '{{ font_select.split(" - ")[0] | int }}' + font: '{{ font_select.split(" - ")[0] | int(default=8) }}' xcen_select: !input xcen_select - xcen: '{{ xcen_select.split(" - ")[0] | int }}' + xcen: '{{ xcen_select.split(" - ")[0] | int(default=1) }}' ycen_select: !input ycen_select - ycen: '{{ ycen_select.split(" - ")[0] | int }}' + ycen: '{{ ycen_select.split(" - ")[0] | int(default=1) }}' wrap: !input wrap selected_fgcolor: !input selected_fgcolor selected_bgcolor: !input selected_bgcolor diff --git a/Home_Assistant/blueprints/hasp_Display_Toggle.yaml b/Home_Assistant/blueprints/hasp_Display_Toggle.yaml index 141b61d..7a24605 100644 --- a/Home_Assistant/blueprints/hasp_Display_Toggle.yaml +++ b/Home_Assistant/blueprints/hasp_Display_Toggle.yaml @@ -2,7 +2,7 @@ blueprint: name: "HASPone p[x].b[y] displays a toggle button" description: | - ## Blueprint Version: `1.05.00` + ## Blueprint Version: `1.06.00` # Description @@ -268,6 +268,12 @@ blueprint: default: false selector: boolean: + text_enable: + name: "Text enabled" + description: "Enable text, font, and colors. If disabled, no output will be sent to the button but the toggle actions will still be activated on press. Useful to combine with other blueprints that might place output on this button." + default: true + selector: + boolean: mode: parallel max_exceeded: silent @@ -294,12 +300,13 @@ variables: off_fgcolor: !input off_fgcolor off_bgcolor: !input off_bgcolor font_select: !input font_select - font: '{{ font_select.split(" - ")[0] | int }}' + font: '{{ font_select.split(" - ")[0] | int(default=8) }}' xcen_select: !input xcen_select - xcen: '{{ xcen_select.split(" - ")[0] | int }}' + xcen: '{{ xcen_select.split(" - ")[0] | int(default=1) }}' ycen_select: !input ycen_select - ycen: '{{ ycen_select.split(" - ")[0] | int }}' + ycen: '{{ ycen_select.split(" - ")[0] | int(default=1) }}' wrap: !input wrap + text_enable: !input text_enable haspobject: '{{ "p[" ~ hasppage ~ "].b[" ~ haspbutton ~ "]" }}' commandtopic: '{{ "hasp/" ~ haspname ~ "/command/" ~ haspobject }}' jsontopic: '{{ "hasp/" ~ haspname ~ "/state/json" }}' @@ -429,6 +436,8 @@ action: or ((trigger.platform == 'template') and (trigger.entity_id == haspsensor) and (trigger.to_state.state == 'ON')) -}} + + sequence: - service: mqtt.publish data: @@ -450,7 +459,7 @@ action: # Update display if our entity has changed state - conditions: # Update display if our entity has changed state - condition: template - value_template: '{{ (trigger.platform == "state") and (trigger.entity_id == entity) }}' + value_template: '{{ (trigger.platform == "state") and (trigger.entity_id == entity) and (text_enable == true) }}' sequence: - service: mqtt.publish data: @@ -493,7 +502,7 @@ action: # Theme: Apply selected foreground color when it changes - conditions: - condition: template - value_template: "{{ trigger.topic == selectedfgtopic }}" + value_template: "{{ (trigger.topic == selectedfgtopic) and (text_enable == true) }}" sequence: - service: mqtt.publish data: @@ -503,7 +512,7 @@ action: # Theme: Apply selected background color on change - conditions: - condition: template - value_template: "{{ trigger.topic == selectedbgtopic }}" + value_template: "{{ (trigger.topic == selectedbgtopic) and (text_enable == true) }}" sequence: - service: mqtt.publish data: @@ -513,7 +522,7 @@ action: # Theme: Apply unselected foreground color on change - conditions: - condition: template - value_template: "{{ trigger.topic == unselectedfgtopic }}" + value_template: "{{ (trigger.topic == unselectedfgtopic) and (text_enable == true) }}" sequence: - service: mqtt.publish data: @@ -523,7 +532,7 @@ action: # Theme: Apply unselected background color on change - conditions: - condition: template - value_template: "{{ trigger.topic == unselectedbgtopic }}" + value_template: "{{ (trigger.topic == unselectedbgtopic) and (text_enable == true) }}" sequence: - service: mqtt.publish data: diff --git a/Home_Assistant/blueprints/hasp_Display_Value_with_Icon_and_Colors.yaml b/Home_Assistant/blueprints/hasp_Display_Value_with_Icon_and_Colors.yaml index e116b71..9ed1f15 100644 --- a/Home_Assistant/blueprints/hasp_Display_Value_with_Icon_and_Colors.yaml +++ b/Home_Assistant/blueprints/hasp_Display_Value_with_Icon_and_Colors.yaml @@ -2,7 +2,7 @@ blueprint: name: "HASPone p[x].b[y] displays the value of a given entity with icons and colors" description: | - ## Blueprint Version: `1.05.00` + ## Blueprint Version: `1.06.00` # Description diff --git a/Home_Assistant/blueprints/hasp_Display_Volume_Control_page8.yaml b/Home_Assistant/blueprints/hasp_Display_Volume_Control_page8.yaml index 81e6996..543300f 100644 --- a/Home_Assistant/blueprints/hasp_Display_Volume_Control_page8.yaml +++ b/Home_Assistant/blueprints/hasp_Display_Volume_Control_page8.yaml @@ -2,7 +2,7 @@ blueprint: name: "HASPone p[8].b[9] The slider button on page 8 displays a volume control" description: | - ## Blueprint Version: `1.05.00` + ## Blueprint Version: `1.06.00` # Description diff --git a/Home_Assistant/blueprints/hasp_Display_Weather_Condition.yaml b/Home_Assistant/blueprints/hasp_Display_Weather_Condition.yaml index b2967b8..1c8f5e2 100644 --- a/Home_Assistant/blueprints/hasp_Display_Weather_Condition.yaml +++ b/Home_Assistant/blueprints/hasp_Display_Weather_Condition.yaml @@ -2,7 +2,7 @@ blueprint: name: "HASPone p[x].b[y] displays the current weather condition" description: | - ## Blueprint Version: `1.05.00` + ## Blueprint Version: `1.06.00` # Description @@ -214,11 +214,11 @@ variables: haspbutton: !input haspbutton weather_provider: !input weather_provider font_select: !input font_select - font: '{{ font_select.split(" - ")[0] | int }}' + font: '{{ font_select.split(" - ")[0] | int(default=8) }}' xcen_select: !input xcen_select - xcen: '{{ xcen_select.split(" - ")[0] | int }}' + xcen: '{{ xcen_select.split(" - ")[0] | int(default=1) }}' ycen_select: !input ycen_select - ycen: '{{ ycen_select.split(" - ")[0] | int }}' + ycen: '{{ ycen_select.split(" - ")[0] | int(default=1) }}' wrap: !input wrap selected_fgcolor: !input selected_fgcolor selected_bgcolor: !input selected_bgcolor diff --git a/Home_Assistant/blueprints/hasp_Display_Weather_Condition_Icon_Only.yaml b/Home_Assistant/blueprints/hasp_Display_Weather_Condition_Icon_Only.yaml index 35f74fc..66b70dd 100644 --- a/Home_Assistant/blueprints/hasp_Display_Weather_Condition_Icon_Only.yaml +++ b/Home_Assistant/blueprints/hasp_Display_Weather_Condition_Icon_Only.yaml @@ -2,7 +2,7 @@ blueprint: name: "HASPone p[x].b[y] displays the current weather condition icon only" description: | - ## Blueprint Version: `1.05.00` + ## Blueprint Version: `1.06.00` # Description @@ -183,11 +183,11 @@ variables: haspbutton: !input haspbutton weather_provider: !input weather_provider font_select: !input font_select - font: '{{ font_select.split(" - ")[0] | int }}' + font: '{{ font_select.split(" - ")[0] | int(default=8) }}' xcen_select: !input xcen_select - xcen: '{{ xcen_select.split(" - ")[0] | int }}' + xcen: '{{ xcen_select.split(" - ")[0] | int(default=1) }}' ycen_select: !input ycen_select - ycen: '{{ ycen_select.split(" - ")[0] | int }}' + ycen: '{{ ycen_select.split(" - ")[0] | int(default=1) }}' selected_fgcolor: !input selected_fgcolor selected_bgcolor: !input selected_bgcolor unselected_fgcolor: !input unselected_fgcolor @@ -346,7 +346,7 @@ variables: {%- else -%} {{ selected_fgcolor }} {%- endif -%} - {%- endif -%} + {%- endif -%} trigger_variables: haspdevice: !input haspdevice @@ -397,7 +397,7 @@ action: # Apply styles, place text, and then place icon if our target page is currently active - conditions: - condition: template - value_template: >- + value_template: >- {{- (trigger is not defined) or diff --git a/Home_Assistant/blueprints/hasp_Display_Weather_Condition_with_Icon.yaml b/Home_Assistant/blueprints/hasp_Display_Weather_Condition_with_Icon.yaml index 5ba6dfd..1c5a07f 100644 --- a/Home_Assistant/blueprints/hasp_Display_Weather_Condition_with_Icon.yaml +++ b/Home_Assistant/blueprints/hasp_Display_Weather_Condition_with_Icon.yaml @@ -2,7 +2,7 @@ blueprint: name: "HASPone p[x].b[y] displays the current weather condition with icons" description: | - ## Blueprint Version: `1.05.00` + ## Blueprint Version: `1.06.00` # Description diff --git a/Home_Assistant/blueprints/hasp_Display_Weather_Forecast.yaml b/Home_Assistant/blueprints/hasp_Display_Weather_Forecast.yaml index 620d45b..21d5b26 100644 --- a/Home_Assistant/blueprints/hasp_Display_Weather_Forecast.yaml +++ b/Home_Assistant/blueprints/hasp_Display_Weather_Forecast.yaml @@ -2,7 +2,7 @@ blueprint: name: "HASPone p[x].b[y] displays the weather forecast" description: | - ## Blueprint Version: `1.05.00` + ## Blueprint Version: `1.06.00` ## Description @@ -120,26 +120,36 @@ blueprint: selector: entity: domain: weather + forecast_interval: + name: "Forecast interval" + description: 'Forecast interval, one of "hourly", "twice daily", or "daily". Not all weather providers will offer all options.' + default: "daily" + selector: + select: + options: + - "hourly" + - "twice_daily" + - "daily" forecast_index: name: "Forecast index" - description: 'Weather forecasts are provided at intervals determined by your weather source. The next time interval will be index "0". Increment this number for future forecasts' + description: 'Select a specific forecast, the next time interval will be index "0". Increment this number for future forecasts' default: 0 selector: number: min: 0 - max: 10 + max: 48 mode: slider unit_of_measurement: index forecast_attribute: name: "Enter the desired forecast attribute" - description: 'Type in the name of the desired forecast attribute for your provider. "condition" is a common attribute for many providers.' + description: 'Type in the name of the desired forecast attribute for your provider. "condition" is a common attribute for many providers.' default: "condition" selector: text: prefix: name: "Forecast display prefix" description: 'Prefix for forecast display, maybe something like "tonight: " or "tomorrow: ". Leave blank for no prefix. Use "\\r" for a newline.' - default: + default: selector: text: font_select: @@ -243,6 +253,7 @@ variables: hasppage: !input hasppage haspbutton: !input haspbutton weather_provider: !input weather_provider + forecast_interval: !input forecast_interval forecast_index: !input forecast_index forecast_attribute: !input forecast_attribute prefix: !input prefix @@ -261,15 +272,6 @@ variables: haspobject: '{{ "p[" ~ hasppage ~ "].b[" ~ haspbutton ~ "]" }}' commandtopic: '{{ "hasp/" ~ haspname ~ "/command/" ~ haspobject }}' jsoncommandtopic: '{{ "hasp/" ~ haspname ~ "/command/json" }}' - text: >- - {%- if prefix|lower != "none" -%} - {{ prefix }} - {%- endif -%} - {%- if title_case -%} - {{ state_attr(weather_provider, "forecast")[forecast_index|int(default=0)].get(forecast_attribute)|replace("windy-variant","windy")|replace("clear-night","clear night")|replace("partlycloudy","partly cloudy")|replace("lightning-rainy","lightning & rain")|replace("snowy-rainy","snow & rain") | title }} - {%- else -%} - {{ state_attr(weather_provider, "forecast")[forecast_index|int(default=0)].get(forecast_attribute)|replace("windy-variant","windy")|replace("clear-night","clear night")|replace("partlycloudy","partly cloudy")|replace("lightning-rainy","lightning & rain")|replace("snowy-rainy","snow & rain") }} - {%- endif -%} isbr: "{% if wrap == true %}1{% else %}0{% endif %}" selectedfgtopic: '{{ "hasp/" ~ haspname ~ "/light/selectedforegroundcolor/rgb" }}' selectedbgtopic: '{{ "hasp/" ~ haspname ~ "/light/selectedbackgroundcolor/rgb" }}' @@ -380,6 +382,23 @@ condition: value_template: "{{ is_state(haspsensor, 'ON') }}" action: + - service: weather.get_forecasts + target: + entity_id: !input weather_provider + data: + type: "{{forecast_interval}}" + response_variable: weather_forecast + - variables: + text: >- + {%- if prefix|lower != "none" -%} + {{ prefix }} + {%- endif -%} + {%- if title_case -%} + {{ weather_forecast[weather_provider]['forecast'][forecast_index][forecast_attribute]|replace("windy-variant","windy")|replace("clear-night","clear night")|replace("partlycloudy","partly cloudy")|replace("lightning-rainy","lightning & rain")|replace("snowy-rainy","snow & rain") | title }} + {%- else -%} + {{ weather_forecast[weather_provider]['forecast'][forecast_index][forecast_attribute]|replace("windy-variant","windy")|replace("clear-night","clear night")|replace("partlycloudy","partly cloudy")|replace("lightning-rainy","lightning & rain")|replace("snowy-rainy","snow & rain") }} + {%- endif -%} + - choose: ######################################################################### # Display attribute and apply text style when "RUN ACTIONS" is pressed by the user diff --git a/Home_Assistant/blueprints/hasp_Display_Weather_Forecast_High_Low.yaml b/Home_Assistant/blueprints/hasp_Display_Weather_Forecast_High_Low.yaml new file mode 100644 index 0000000..ee6e6d4 --- /dev/null +++ b/Home_Assistant/blueprints/hasp_Display_Weather_Forecast_High_Low.yaml @@ -0,0 +1,459 @@ +blueprint: + name: "HASPone p[x].b[y] displays the weather forecast High and Low temperature" + description: | + + ## Blueprint Version: `1.06.00` + + ## Description + + A HASPone button displays the high and low temperatures from a selected weather forecast. + + ![Preview](https://raw.githubusercontent.com/HASwitchPlate/HASPone/main/images/hasp_Display_Weather_Forecast.png) + + ## HASPone Page and Button Reference + + The images below show each available HASPone page along with the layout of available button objects. + +
+ + | Page 0 | Pages 1-3 | Pages 4-5 | + |--------|-----------|-----------| + | ![Page 0](https://raw.githubusercontent.com/HASwitchPlate/HASPone/main/images/NextionUI_p0_Init_Screen.png) | ![Pages 1-3](https://raw.githubusercontent.com/HASwitchPlate/HASPone/main/images/NextionUI_p1-p3_4buttons.png) | ![Pages 4-5](https://raw.githubusercontent.com/HASwitchPlate/HASPone/main/images/NextionUI_p4-p5_3sliders.png) | + + | Page 6 | Page 7 | Page 8 | + |--------|--------|--------| + | ![Page 6](https://raw.githubusercontent.com/HASwitchPlate/HASPone/main/images/NextionUI_p6_8buttons.png) | ![Page 7](https://raw.githubusercontent.com/HASwitchPlate/HASPone/main/images/NextionUI_p7_12buttons.png) | ![Page 8](https://raw.githubusercontent.com/HASwitchPlate/HASPone/main/images/NextionUI_p8_5buttons+1slider.png) | + + | Page 9 | Page 10 | Page 11 | + |--------|---------|---------| + | ![Page 9](https://raw.githubusercontent.com/HASwitchPlate/HASPone/main/images/NextionUI_p9_9buttons.png) | ![Page 10](https://raw.githubusercontent.com/HASwitchPlate/HASPone/main/images/NextionUI_p10_5buttons.png) | ![Page 11](https://raw.githubusercontent.com/HASwitchPlate/HASPone/main/images/NextionUI_p11_1button+1slider.png) + +
+ + ## HASPone Font Reference + +
+ + The Nextion display supports monospaced and proportional fonts. For monospace fonts, the HASPone project includes [Consolas](https://docs.microsoft.com/en-us/typography/font-list/consolas) monospace in 4 sizes, [Webdings](https://en.wikipedia.org/wiki/Webdings#Character_set) in 1 size, and [Google's "Noto Sans"](https://github.com/googlefonts/noto-fonts) proportional in 5 sizes + + | Font | Name | Characters per line | Lines per button | + | :--- | :---------------- | :-------------------| :--------------- | + | 0 | Consolas 24 | 20 characters | 2 lines | + | 1 | Consolas 32 | 15 characters | 2 lines | + | 2 | Consolas 48 | 10 characters | 1 line | + | 3 | Consolas 80 | 6 characters | 1 line | + | 4 | Webdings 56 | 8 characters | 1 line | + | 5 | Noto Sans 24 | Proportional | 2 lines | + | 6 | Noto Sans 32 | Proportional | 2 lines | + | 7 | Noto Sans 48 | Proportional | 1 line | + | 8 | Noto Sans 64 | Proportional | 1 line | + | 9 | Noto Sans 80 | Proportional | 1 line | + | 10 | Noto Sans Bold 80 | Proportional | 1 line | + + ### Icons + + Fonts 5-10 also include [1400+ icons which you can copy and paste from here](https://htmlpreview.github.io/?https://github.com/HASwitchPlate/HASPone/blob/main/images/hasp-fontawesome5.html) + + ### Font examples + + ![HASPone Fonts 0-3](https://raw.githubusercontent.com/HASwitchPlate/HASPone/main/images/NextionUI_Fonts_0-3.png) ![HASPone Fonts 4-7](https://raw.githubusercontent.com/HASwitchPlate/HASPone/main/images/NextionUI_Fonts_4-7.png) ![HASPone Fonts 8-10](https://raw.githubusercontent.com/HASwitchPlate/HASPone/main/images/NextionUI_Fonts_8-10.png) + +
+ + ## Nextion color codes + +
+ + The Nextion environment utilizes RGB 565 encoding. [Use this handy convertor](https://nodtem66.github.io/nextion-hmi-color-convert/index.html) to select your colors and convert to the RGB 565 format. + + Here are some example colors: + + | Color | Code | + |--------|-------| + | White | 65535 | + | Black | 0 | + | Grey | 25388 | + | Red | 63488 | + | Green | 2016 | + | Blue | 31 | + | Yellow | 65504 | + | Orange | 64512 | + | Brown | 48192 | + +
+ + domain: automation + input: + haspdevice: + name: "HASPone Device" + description: "Select the HASPone device" + selector: + device: + integration: mqtt + manufacturer: "HASwitchPlate" + model: "HASPone v1.0.0" + hasppage: + name: "HASPone Page" + description: "Select the HASPone page (1-11) for the forecast. Refer to the HASPone Page and Button reference above." + default: 1 + selector: + number: + min: 1 + max: 11 + mode: slider + unit_of_measurement: page + haspbutton: + name: "HASPone Button" + description: "Select the HASPone button (4-15) for the forecast. Refer to the HASPone Page and Button reference above." + default: 4 + selector: + number: + min: 4 + max: 15 + mode: slider + unit_of_measurement: button + weather_provider: + name: "Weather provider" + description: "Select the weather provider to obtain the forecast" + selector: + entity: + domain: weather + forecast_interval: + name: "Forecast interval" + description: 'Forecast interval, one of "hourly", "twice daily", or "daily". Not all weather providers will offer all options.' + default: "daily" + selector: + select: + options: + - "hourly" + - "twice_daily" + - "daily" + forecast_index: + name: "Forecast index" + description: 'Select a specific forecast, the next time interval will be index "0". Increment this number for future forecasts' + default: 0 + selector: + number: + min: 0 + max: 48 + mode: slider + unit_of_measurement: index + font_select: + name: "Font" + description: "Select the font for the displayed text. You probably want to leave this as 10, refer to the HASPone Font Reference above." + default: "10 - Noto Sans Bold 80" + selector: + select: + options: + - "0 - Consolas 24" + - "1 - Consolas 32" + - "2 - Consolas 48" + - "3 - Consolas 80" + - "4 - Webdings 56" + - "5 - Noto Sans 24" + - "6 - Noto Sans 32" + - "7 - Noto Sans 48" + - "8 - Noto Sans 64" + - "9 - Noto Sans 80" + - "10 - Noto Sans Bold 80" + xcen_select: + name: "Text horizontal alignment" + description: "Horizontal text alignment: 0=Left 1=Center 2=Right" + default: "1 - Centered" + selector: + select: + options: + - "0 - Left aligned" + - "1 - Centered" + - "2 - Right aligned" + ycen_select: + name: "Text vertical alignment" + description: "Vertical text alignment: 0=Top 1=Center 2=Bottom" + default: "1 - Centered" + selector: + select: + options: + - "0 - Top aligned" + - "1 - Centered" + - "2 - Bottom aligned" + wrap: + name: "Text wrap" + default: false + description: "Enable line-wrapping text if too long to fit in the button." + selector: + boolean: + selected_fgcolor: + name: "Selected foreground color" + description: 'Selected foreground color in Nextion RGB565 format (see "Nextion color codes" above for reference). -1 = Current theme selected foreground color.' + default: -1 + selector: + number: + min: -1 + max: 65535 + mode: slider + selected_bgcolor: + name: "Selected background color" + description: 'Selected background color in Nextion RGB565 format (see "Nextion color codes" above for reference). -1 = Current theme selected background color.' + default: -1 + selector: + number: + min: -1 + max: 65535 + mode: slider + unselected_fgcolor: + name: "Unselected foreground color" + description: 'Unselected foreground color in Nextion RGB565 format (see "Nextion color codes" above for reference). -1 = Current theme unselected foreground color.' + default: -1 + selector: + number: + min: -1 + max: 65535 + mode: slider + unselected_bgcolor: + name: "Unselected background color" + description: 'Unselected background color in Nextion RGB565 format (see "Nextion color codes" above for reference). -1 = Current theme unselected background color.' + default: -1 + selector: + number: + min: -1 + max: 65535 + mode: slider + +mode: parallel +max_exceeded: silent + +variables: + haspdevice: !input haspdevice + haspname: >- + {%- for entity in device_entities(haspdevice) -%} + {%- if entity|regex_search("^sensor\..+_sensor(?:_\d+|)$") -%} + {{- entity|regex_replace(find="^sensor\.", replace="", ignorecase=true)|regex_replace(find="_sensor(?:_\d+|)$", replace="", ignorecase=true) -}} + {%- endif -%} + {%- endfor -%} + hasppage: !input hasppage + haspbutton: !input haspbutton + weather_provider: !input weather_provider + forecast_interval: !input forecast_interval + forecast_index: !input forecast_index + font_select: !input font_select + font: '{{ font_select.split(" - ")[0] | int }}' + xcen_select: !input xcen_select + xcen: '{{ xcen_select.split(" - ")[0] | int }}' + ycen_select: !input ycen_select + ycen: '{{ ycen_select.split(" - ")[0] | int }}' + wrap: !input wrap + selected_fgcolor: !input selected_fgcolor + selected_bgcolor: !input selected_bgcolor + unselected_fgcolor: !input unselected_fgcolor + unselected_bgcolor: !input unselected_bgcolor + haspobject: '{{ "p[" ~ hasppage ~ "].b[" ~ haspbutton ~ "]" }}' + commandtopic: '{{ "hasp/" ~ haspname ~ "/command/" ~ haspobject }}' + jsoncommandtopic: '{{ "hasp/" ~ haspname ~ "/command/json" }}' + selectedfgtopic: '{{ "hasp/" ~ haspname ~ "/light/selectedforegroundcolor/rgb" }}' + selectedbgtopic: '{{ "hasp/" ~ haspname ~ "/light/selectedbackgroundcolor/rgb" }}' + unselectedfgtopic: '{{ "hasp/" ~ haspname ~ "/light/unselectedforegroundcolor/rgb" }}' + unselectedbgtopic: '{{ "hasp/" ~ haspname ~ "/light/unselectedbackgroundcolor/rgb" }}' + selectedfg: >- + {%- if (selected_fgcolor|int) >= 0 -%} + {{ selected_fgcolor }} + {%- else -%} + {%- set color = namespace() -%} + {%- for entity in device_entities(haspdevice) -%} + {%- if entity|regex_search("^light\..*_selected_foreground_color(?:_\d+|)$") -%} + {%- set color.source=entity -%} + {%- endif -%} + {%- endfor -%} + {%- set brightness = state_attr(color.source, "brightness")|int(default=255) / 255 -%} + {%- set red=(state_attr(color.source, "rgb_color")[0] * brightness)|int(default=0) -%} + {%- set green=(state_attr(color.source, "rgb_color")[1] * brightness)|int(default=0) -%} + {%- set blue=(state_attr(color.source, "rgb_color")[2] * brightness)|int(default=0) -%} + {{ (red|bitwise_and(248)*256) + (green|bitwise_and(252)*8) + (blue|bitwise_and(248)/8)|int }} + {%- endif -%} + selectedbg: >- + {%- if (selected_bgcolor|int) >= 0 -%} + {{ selected_bgcolor }} + {%- else -%} + {%- set color = namespace() -%} + {%- for entity in device_entities(haspdevice) -%} + {%- if entity|regex_search("^light\..*_selected_background_color(?:_\d+|)$") -%} + {%- set color.source=entity -%} + {%- endif -%} + {%- endfor -%} + {%- set brightness = state_attr(color.source, "brightness")|int(default=255) / 255 -%} + {%- set red=(state_attr(color.source, "rgb_color")[0] * brightness)|int(default=0) -%} + {%- set green=(state_attr(color.source, "rgb_color")[1] * brightness)|int(default=0) -%} + {%- set blue=(state_attr(color.source, "rgb_color")[2] * brightness)|int(default=0) -%} + {{ (red|bitwise_and(248)*256) + (green|bitwise_and(252)*8) + (blue|bitwise_and(248)/8)|int }} + {%- endif -%} + unselectedfg: >- + {%- if (unselected_fgcolor|int) >= 0 -%} + {{ unselected_fgcolor }} + {%- else -%} + {%- set color = namespace() -%} + {%- for entity in device_entities(haspdevice) -%} + {%- if entity|regex_search("^light\..*_unselected_foreground_color(?:_\d+|)$") -%} + {%- set color.source=entity -%} + {%- endif -%} + {%- endfor -%} + {%- set brightness = state_attr(color.source, "brightness")|int(default=255) / 255 -%} + {%- set red=(state_attr(color.source, "rgb_color")[0] * brightness)|int(default=0) -%} + {%- set green=(state_attr(color.source, "rgb_color")[1] * brightness)|int(default=0) -%} + {%- set blue=(state_attr(color.source, "rgb_color")[2] * brightness)|int(default=0) -%} + {{ (red|bitwise_and(248)*256) + (green|bitwise_and(252)*8) + (blue|bitwise_and(248)/8)|int }} + {%- endif -%} + unselectedbg: >- + {%- if (unselected_bgcolor|int) >= 0 -%} + {{ unselected_bgcolor }} + {%- else -%} + {%- set color = namespace() -%} + {%- for entity in device_entities(haspdevice) -%} + {%- if entity|regex_search("^light\..*_unselected_background_color(?:_\d+|)$") -%} + {%- set color.source=entity -%} + {%- endif -%} + {%- endfor -%} + {%- set brightness = state_attr(color.source, "brightness")|int(default=255) / 255 -%} + {%- set red=(state_attr(color.source, "rgb_color")[0] * brightness)|int(default=0) -%} + {%- set green=(state_attr(color.source, "rgb_color")[1] * brightness)|int(default=0) -%} + {%- set blue=(state_attr(color.source, "rgb_color")[2] * brightness)|int(default=0) -%} + {{ (red|bitwise_and(248)*256) + (green|bitwise_and(252)*8) + (blue|bitwise_and(248)/8)|int }} + {%- endif -%} + +trigger_variables: + haspdevice: !input haspdevice + haspname: >- + {%- for entity in device_entities(haspdevice) -%} + {%- if entity|regex_search("^sensor\..+_sensor(?:_\d+|)$") -%} + {{- entity|regex_replace(find="^sensor\.", replace="", ignorecase=true)|regex_replace(find="_sensor(?:_\d+|)$", replace="", ignorecase=true) -}} + {%- endif -%} + {%- endfor -%} + haspsensor: >- + {%- for entity in device_entities(haspdevice) -%} + {%- if entity|regex_search("^sensor\..+_sensor(?:_\d+|)$") -%} + {{ entity }} + {%- endif -%} + {%- endfor -%} + selectedfgtopic: '{{ "hasp/" ~ haspname ~ "/light/selectedforegroundcolor/rgb" }}' + selectedbgtopic: '{{ "hasp/" ~ haspname ~ "/light/selectedbackgroundcolor/rgb" }}' + unselectedfgtopic: '{{ "hasp/" ~ haspname ~ "/light/unselectedforegroundcolor/rgb" }}' + unselectedbgtopic: '{{ "hasp/" ~ haspname ~ "/light/unselectedbackgroundcolor/rgb" }}' + +trigger: + - platform: state + entity_id: !input weather_provider + - platform: template + value_template: "{{ is_state(haspsensor, 'ON') }}" + - platform: homeassistant + event: start + - platform: mqtt + topic: "{{selectedfgtopic}}" + - platform: mqtt + topic: "{{selectedbgtopic}}" + - platform: mqtt + topic: "{{unselectedfgtopic}}" + - platform: mqtt + topic: "{{unselectedbgtopic}}" + +condition: + - condition: template + value_template: "{{ is_state(haspsensor, 'ON') }}" + +action: + - service: weather.get_forecasts + target: + entity_id: !input weather_provider + data: + type: "{{forecast_interval}}" + response_variable: weather_forecast + - variables: + temphigh: '{{ weather_forecast[weather_provider]["forecast"][forecast_index]["temperature"] }}' + templow: '{{ weather_forecast[weather_provider]["forecast"][forecast_index]["templow"] }}' + text: "{{templow|int(default=0)}}° {{temphigh|int(default=0)}}°" + + - choose: + ######################################################################### + # Display attribute and apply text style when "RUN ACTIONS" is pressed by the user + - conditions: + - condition: template + value_template: >- + {{- + (trigger is not defined) + or + (trigger.platform is none) + or + ((trigger.platform == 'homeassistant') and (trigger.event == 'start')) + or + ((trigger.platform == 'template') and (trigger.entity_id == haspsensor) and (trigger.to_state.state == 'ON')) + -}} + sequence: + - service: mqtt.publish + data: + topic: "{{jsoncommandtopic}}" + payload: >- + [ + "{{haspobject}}.font={{font}}", + "{{haspobject}}.xcen={{xcen}}", + "{{haspobject}}.ycen={{ycen}}", + "{{haspobject}}.isbr={{isbr}}", + "{{haspobject}}.pco={{selectedfg}}", + "{{haspobject}}.bco={{selectedbg}}", + "{{haspobject}}.pco2={{unselectedfg}}", + "{{haspobject}}.bco2={{unselectedbg}}", + "{{haspobject}}.txt=\"{{text}}\"" + ] + ######################################################################### + # Update forecast if our weather provider changed state + - conditions: + - condition: template + value_template: '{{ (trigger.platform == "state") and (trigger.entity_id == weather_provider) }}' + sequence: + - service: mqtt.publish + data: + topic: "{{commandtopic}}.txt" + payload: '"{{text}}"' + ######################################################################### + # Catch triggers fired by incoming MQTT messages + - conditions: + - condition: template + value_template: '{{ trigger.platform == "mqtt" }}' + sequence: + - choose: + ######################################################################### + # Theme: Apply selected foreground color on change + - conditions: + - condition: template + value_template: "{{ (trigger.topic == selectedfgtopic) and ((selected_fgcolor|int) == -1) }}" + sequence: + - service: mqtt.publish + data: + topic: "{{commandtopic}}.pco" + payload: "{{trigger.payload}}" + ######################################################################### + # Theme: Apply selected background color on change + - conditions: + - condition: template + value_template: "{{ (trigger.topic == selectedbgtopic) and ((selected_bgcolor|int) == -1) }}" + sequence: + - service: mqtt.publish + data: + topic: "{{commandtopic}}.bco" + payload: "{{trigger.payload}}" + ######################################################################### + # Theme: Apply unselected foreground color on change + - conditions: + - condition: template + value_template: "{{ (trigger.topic == unselectedfgtopic) and ((unselected_fgcolor|int) == -1) }}" + sequence: + - service: mqtt.publish + data: + topic: "{{commandtopic}}.pco2" + payload: "{{trigger.payload}}" + ######################################################################### + # Theme: Apply unselected background color on change + - conditions: + - condition: template + value_template: "{{ (trigger.topic == unselectedbgtopic) and ((unselected_bgcolor|int) == -1) }}" + sequence: + - service: mqtt.publish + data: + topic: "{{commandtopic}}.bco2" + payload: "{{trigger.payload}}" diff --git a/Home_Assistant/blueprints/hasp_Display_Weather_Temperature_Color_Icon_Only.yaml b/Home_Assistant/blueprints/hasp_Display_Weather_Temperature_Color_Icon_Only.yaml index 013685e..12aabec 100644 --- a/Home_Assistant/blueprints/hasp_Display_Weather_Temperature_Color_Icon_Only.yaml +++ b/Home_Assistant/blueprints/hasp_Display_Weather_Temperature_Color_Icon_Only.yaml @@ -2,7 +2,7 @@ blueprint: name: "HASPone p[x].b[y] displays the current temperature from a weather provider, coloured icon only" description: | - ## Blueprint Version: `1.05.00` + ## Blueprint Version: `1.06.00` # Description @@ -201,7 +201,7 @@ variables: jsoncommandtopic: '{{ "hasp/" ~ haspname ~ "/command/json" }}' temperature: '{{ state_attr(weather_provider, "temperature") }}' icon: >- - {%- set temp = temperature|int -%} + {%- set temp = temperature|int(default=0) -%} {%- if temp <= thermometer_quarter_threshold|int -%}  {%- elif temp < thermometer_half_threshold|int -%} @@ -333,7 +333,7 @@ action: # Apply styles, place text, and then place icon if our target page is currently active - conditions: - condition: template - value_template: >- + value_template: >- {{- (trigger is not defined) or diff --git a/Home_Assistant/blueprints/hasp_Display_Weather_Temperature_with_Icon_and_Colors.yaml b/Home_Assistant/blueprints/hasp_Display_Weather_Temperature_with_Icon_and_Colors.yaml index 393f253..65c6500 100644 --- a/Home_Assistant/blueprints/hasp_Display_Weather_Temperature_with_Icon_and_Colors.yaml +++ b/Home_Assistant/blueprints/hasp_Display_Weather_Temperature_with_Icon_and_Colors.yaml @@ -2,7 +2,7 @@ blueprint: name: "HASPone p[x].b[y] displays the current temperature from a weather provider with icon and colors" description: | - ## Blueprint Version: `1.05.00` + ## Blueprint Version: `1.06.00` # Description @@ -224,10 +224,10 @@ variables: {%- if roundtemp == true -%} {{- state_attr(weather_provider, "temperature") | round(default=0) -}} {%- else -%} - {{- state_attr(weather_provider, "temperature") -}} + {{- state_attr(weather_provider, "temperature") | float(default=0) -}} {%- endif -%} icon: >- - {%- set temp = temperature|int -%} + {%- set temp = temperature|int(default=0) -%} {%- if temp <= thermometer_quarter_threshold|int -%}  {%- elif temp < thermometer_half_threshold|int -%} @@ -310,7 +310,7 @@ variables: {%- set blue=(state_attr(color.source, "rgb_color")[2] * brightness)|int(default=0) -%} {{ (red|bitwise_and(248)*256) + (green|bitwise_and(252)*8) + (blue|bitwise_and(248)/8)|int }} tempcolor: >- - {%- set temp = temperature|int -%} + {%- set temp = temperature|int(default=0) -%} {%- if temp <= thermometer_quarter_threshold|int -%} {%- set color = thermometer_empty_color -%} {%- elif temp < thermometer_half_threshold|int -%} diff --git a/Home_Assistant/blueprints/hasp_Perform_Action.yaml b/Home_Assistant/blueprints/hasp_Perform_Action.yaml index 751a530..4bab829 100644 --- a/Home_Assistant/blueprints/hasp_Perform_Action.yaml +++ b/Home_Assistant/blueprints/hasp_Perform_Action.yaml @@ -2,7 +2,7 @@ blueprint: name: "HASPone p[x].b[y] performs an action when pressed" description: | - ## Blueprint Version: `1.05.00` + ## Blueprint Version: `1.06.00` # Description diff --git a/Home_Assistant/blueprints/hasp_Remove_MQTT_Discovery_Devices.yaml b/Home_Assistant/blueprints/hasp_Remove_MQTT_Discovery_Devices.yaml index 59604ef..8db18c8 100644 --- a/Home_Assistant/blueprints/hasp_Remove_MQTT_Discovery_Devices.yaml +++ b/Home_Assistant/blueprints/hasp_Remove_MQTT_Discovery_Devices.yaml @@ -2,7 +2,7 @@ blueprint: name: "HASPone Remove MQTT discovery messages" description: | - ## Blueprint Version: `1.05.00` + ## Blueprint Version: `1.06.00` # Description diff --git a/Home_Assistant/blueprints/hasp_Theme_Dark_on_Light.yaml b/Home_Assistant/blueprints/hasp_Theme_Dark_on_Light.yaml index eef093e..3d891a4 100644 --- a/Home_Assistant/blueprints/hasp_Theme_Dark_on_Light.yaml +++ b/Home_Assistant/blueprints/hasp_Theme_Dark_on_Light.yaml @@ -2,7 +2,7 @@ blueprint: name: "HASPone Theme Dark on Light" description: | - ## Blueprint Version: `1.05.00` + ## Blueprint Version: `1.06.00` ## Description diff --git a/Home_Assistant/blueprints/hasp_Theme_Light_on_BlueDark.yaml b/Home_Assistant/blueprints/hasp_Theme_Light_on_BlueDark.yaml index 2bd23b6..9aa6a58 100644 --- a/Home_Assistant/blueprints/hasp_Theme_Light_on_BlueDark.yaml +++ b/Home_Assistant/blueprints/hasp_Theme_Light_on_BlueDark.yaml @@ -2,7 +2,7 @@ blueprint: name: "HASPone Theme Light on Dark Blue" description: | - ## Blueprint Version: `1.05.00` + ## Blueprint Version: `1.06.00` ## Description @@ -52,7 +52,7 @@ variables: {%- endfor -%} selected_foreground_brightness: "255" selected_foreground_color: "[255, 255, 255]" - selected_background_brightness: "1" + selected_background_brightness: "32" selected_background_color: "[0, 0, 255]" unselected_foreground_brightness: "224" unselected_foreground_color: "[255, 255, 255]" diff --git a/Home_Assistant/blueprints/hasp_Theme_Light_on_Dark.yaml b/Home_Assistant/blueprints/hasp_Theme_Light_on_Dark.yaml index 8e9129b..a52de8a 100644 --- a/Home_Assistant/blueprints/hasp_Theme_Light_on_Dark.yaml +++ b/Home_Assistant/blueprints/hasp_Theme_Light_on_Dark.yaml @@ -2,7 +2,7 @@ blueprint: name: "HASPone Theme Light on Dark" description: | - ## Blueprint Version: `1.05.00` + ## Blueprint Version: `1.06.00` ## Description diff --git a/Nextion_HMI/README.md b/Nextion_HMI/README.md index 299b6a8..39cb7f8 100644 --- a/Nextion_HMI/README.md +++ b/Nextion_HMI/README.md @@ -15,4 +15,4 @@ Please [check the Nextion HMI documentation](../Documentation/02_Nextion_HMI.md) * **[HASwitchPlate-Discovery-Inverted.tft](HASwitchPlate-Discovery-Inverted.tft)** Discovery series firmware but inverted, usable if the viewing angle on your display works better when mounted upside-down. * **[HASwitchPlate-Enhanced.tft](HASwitchPlate-Enhanced.tft)** This is the compiled Nextion firmware for the HASPone usable on an enhanced Nextion 2.4" LCD, model `NX4024K032_011R`. This panel will not fit in the provided 3D printed enclosure and no enhanced features are used in this project. **Don't buy this panel**, but if you did (*and you shouldn't*), you can use this firmware. * **[HASwitchPlate-TJC.hmi](HASwitchPlate-TJC.hmi)** This is the "source" file for the Chinese-market TJC LCD model `TJC3224T024_011`. This file cannot be used with the english language editor. If you purchase this panel, you will need to use the Chinese-language "USART HMI" editor to modify this file. **Don't buy this panel**. -* **[HASwitchPlate-TJC.tft](HASwitchPlate-TJC.tft)** This is the compiled Nextion firmware for the HASPone usable on a Chinese market TJC 2.4" LCD, model `TJC3224T024_011`. +* **[HASwitchPlate-TJC.tft](HASwitchPlate-TJC.tft)** This is the compiled Nextion firmware for the HASPone usable on a Chinese market TJC 2.4" LCD, model `TJC3224T024_011`. \ No newline at end of file diff --git a/esphome/haspone.yaml b/esphome/haspone.yaml new file mode 100644 index 0000000..61b4d3d --- /dev/null +++ b/esphome/haspone.yaml @@ -0,0 +1,75 @@ +# Example ESPhome configuration for use with the HASPone hardware + +substitutions: + device_name: "haspone" + friendly_name: "HASPone hardware for ESPhome" + project_version: "0.0.1" + +esphome: + name: ${device_name} + friendly_name: ${friendly_name} + comment: "http://haswitchplate.com" + project: + name: "esphome.${device_name}" + version: ${project_version} + on_boot: + then: + - switch.turn_on: switch_lcdpower # Power up the Nextion on boot + +esp8266: + board: d1_mini + +logger: + +api: + +ota: + +wifi: + ssid: !secret wifi_ssid + password: !secret wifi_password + +web_server: + port: 80 + +# HASPone switch controls power to the Nextion. Set HIGH at power on to enable the device at boot. +switch: + - platform: gpio + id: switch_lcdpower + name: "${friendly_name} Nextion Power" + pin: D6 #GPIO12 + restore_mode: ALWAYS_ON + internal: false + +# UART for HASPone communication to Nextion. This will utilize software serial and might not work well for TFT updates. +uart: + id: uart_nextion + tx_pin: D4 #GPIO2 + rx_pin: D7 #GPIO13 + baud_rate: 115200 + +# Nextion display device +display: + - platform: nextion + id: display_nextion + uart_id: uart_nextion + on_touch: + then: + lambda: |- + ESP_LOGD("nextion.on_touch", "Nextion touch event detected!"); + ESP_LOGD("nextion.on_touch", "Page Id: %i", page_id); + ESP_LOGD("nextion.on_touch", "Component Id: %i", component_id); + ESP_LOGD("nextion.on_touch", "Event type: %s", touch_event ? "Press" : "Release"); + +# Nextion backlight control +number: + - platform: template + id: number_brightness + name: "${friendly_name} Nextion Brightness" + min_value: 0 + max_value: 100 + step: 1 + initial_value: 100 + optimistic: true + set_action: + - lambda: id(display_nextion)->set_backlight_brightness(x/100); \ No newline at end of file diff --git a/esphome/readme.md b/esphome/readme.md new file mode 100644 index 0000000..e3ffff6 --- /dev/null +++ b/esphome/readme.md @@ -0,0 +1,3 @@ +# HASPone for ESPhome + +Here you'll find an example ESPhome configuration for use with the HASPone hardware, this should be compatible with existing ESPhome-native Nextion projects for a 2.8" panel. \ No newline at end of file diff --git a/images/HASP PCB Back.png b/images/HASP PCB Back.png new file mode 100644 index 0000000..a022c45 Binary files /dev/null and b/images/HASP PCB Back.png differ diff --git a/images/HASP PCB Front and Back.png b/images/HASP PCB Front and Back.png new file mode 100644 index 0000000..04c2fa1 Binary files /dev/null and b/images/HASP PCB Front and Back.png differ diff --git a/images/HASP PCB Front.png b/images/HASP PCB Front.png new file mode 100644 index 0000000..8b7b603 Binary files /dev/null and b/images/HASP PCB Front.png differ diff --git a/update/version.json b/update/version.json index 836a28a..1696c38 100644 --- a/update/version.json +++ b/update/version.json @@ -1,18 +1,18 @@ { "d1_mini": { - "version": "1.05", - "firmware": "https://raw.githubusercontent.com/HASwitchPlate/HASPone/main/Arduino_Sketch/HASwitchPlate.ino.d1_mini.bin" + "version": "1.06", + "firmware": "https://haswitchplate.com/update/main/HASwitchPlate.ino.d1_mini.bin" }, "NX3224T024_011R": { "version": 3, - "firmware": "https://raw.githubusercontent.com/HASwitchPlate/HASPone/main/Nextion_HMI/HASwitchPlate.tft" + "firmware": "https://haswitchplate.com/update/main/HASwitchPlate.tft" }, "NX3224K024_011R": { "version": 3, - "firmware": "https://raw.githubusercontent.com/HASwitchPlate/HASPone/main/Nextion_HMI/HASwitchPlate-Enhanced.tft" + "firmware": "https://haswitchplate.com/update/main/HASwitchPlate-Enhanced.tft" }, "NX3224F024_011R": { "version": 3, - "firmware": "https://raw.githubusercontent.com/HASwitchPlate/HASPone/main/Nextion_HMI/HASwitchPlate-Discovery.tft" + "firmware": "https://haswitchplate.com/update/main/HASwitchPlate-Discovery.tft" } } \ No newline at end of file