diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml index e6d2f76..87180ca 100644 --- a/.github/FUNDING.yml +++ b/.github/FUNDING.yml @@ -9,4 +9,4 @@ community_bridge: # Replace with a single Community Bridge project-name e.g., cl liberapay: # Replace with a single Liberapay username issuehunt: # Replace with a single IssueHunt username otechie: # Replace with a single Otechie username -custom: ['https://www.buymeacoffee.com/gW5rPpsKR','https://www.tindie.com/stores/luma/'] # Up to 4 links +custom: ['https://www.buymeacoffee.com/gW5rPpsKR','https://www.etsy.com/listing/1191709235/haspone-haswitchplate-touchscreen-home','https://www.etsy.com/listing/1177721322/haspone-pcb'] # Up to 4 links diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..65dc176 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +# Ignore VSCode files +**/.vscode/ +.claude \ No newline at end of file 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/3D_Printable_Models/HASwitchPlate_front_single_deep.stl b/3D_Printable_Models/HASwitchPlate_front_single_deep.stl new file mode 100644 index 0000000..0d99677 Binary files /dev/null and b/3D_Printable_Models/HASwitchPlate_front_single_deep.stl differ diff --git a/Arduino_Sketch/.gitignore b/Arduino_Sketch/.gitignore new file mode 100644 index 0000000..5daadf2 --- /dev/null +++ b/Arduino_Sketch/.gitignore @@ -0,0 +1,6 @@ +.pio +.vscode/.browse.c_cpp.db* +.vscode/c_cpp_properties.json +.vscode/launch.json +.vscode/ipch +.claude \ No newline at end of file diff --git a/Arduino_Sketch/HASwitchPlate.ino.d1_mini.bin b/Arduino_Sketch/HASwitchPlate.ino.d1_mini.bin index 0cba906..7a0ecbd 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 82% rename from Arduino_Sketch/HASwitchPlate/HASwitchPlate.ino rename to Arduino_Sketch/HASwitchPlate/HASwitchPlate.cpp index afae363..67e00b9 100644 --- a/Arduino_Sketch/HASwitchPlate/HASwitchPlate.ino +++ b/Arduino_Sketch/HASwitchPlate/HASwitchPlate.cpp @@ -1,3724 +1,4151 @@ -//////////////////////////////////////////////////////////////////////////////////////////////////// -// _____ _____ _____ _____ -// | | | _ | __| _ | -// | | |__ | __| -// |__|__|__|__|_____|__| -// 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[64] = ""; -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.03; // Current HASP 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 HASwitchPlate v")) + String(haspVersion)); - debugPrintln(String(F("SYSTEM: Last reset reason: ")) + String(ESP.getResetInfo())); - debugPrintln(String(F("SYSTEM: heapFree: ")) + String(ESP.getFreeHeap()) + String(F(" heapMaxFreeBlockSize: ")) + String(ESP.getMaxFreeBlockSize())); - 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(); - mqttPingCheck = Ping.ping(mqttServer, 4); - yield(); - webServer.handleClient(); - mqttPortCheck = wifiClient.connect(mqttServer, atoi(mqttPort)); - String mqttCheckResult = "Ping: FAILED"; - String 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 attempt ")) + String(mqttReconnectCount) + String(F(" failed with rc ")) + String(mqttClient.returnCode()) + String(F(" and error: ")) + String(mqttClient.lastError()) + String(F(". 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/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") - - 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 + "/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 mqqtvar1 = getSubtringField(strPayload, ',', 0); - String mqqtvar2 = getSubtringField(strPayload, ',', 1); - String mqqtvar3 = getSubtringField(strPayload, ',', 2); - - beepOnTime = mqqtvar1.toInt(); - beepOffTime = mqqtvar2.toInt(); - beepCounter = mqqtvar3.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 HASP 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-HASP 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 HASP 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-HASP 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() + 5000; - 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("
HASP 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, 63, " maxlength=63"); - 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, 60, " 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", "\" HASP WiFi 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", "\"\\rHASP 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", "\"\\rHASP 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", "\"HASP 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", "\"\\rHASP 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", "\"HASP 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", "\"HASP update:\\rNo update\""); - break; - - case HTTP_UPDATE_OK: - debugPrintln(F("ESPFW: HTTP_UPDATE_OK")); - nextionSetAttr("p[0].b[1].txt", "\"\\rHASP update:\\r\\r Complete!\\rRestarting.\""); - nextionSendCmd("vis 4,1"); - delay(1000); - espReset(); - } - delay(1000); - nextionSendCmd("page " + String(nextionActivePage)); -} - -//////////////////////////////////////////////////////////////////////////////////////////////////// -void espReset() -{ - debugPrintln(F("RESET: HASP 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")); - 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 (!jsonConfigValues["nextionMaxPages"].isNull()) - { - nextionMaxPages = jsonConfigValues["nextionMaxPages"]; - } - 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 - nextionSetAttr("p[0].b[1].txt", "\"Saving\\rconfig\""); - debugPrintln(F("SPIFFS: Saving config")); - DynamicJsonDocument jsonConfigValues(1536); - - 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.close(); - } - 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("

HASP 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("

HASP Admin Username (optional)
HASP 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)) - { // HASP 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("

HASP 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("
HASP Version: ")); - webServer.sendContent(String(haspVersion)); - webServer.sendContent(F("
LCD Model: ")); - if (nextionModel != "") - { - webServer.sendContent(nextionModel); - } - webServer.sendContent(F("
LCD 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, 64); - } - 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)) - { // Handle nextionMaxPages - 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("HASP 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 HASP 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("HASP 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(768); - 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 single character to our debug targets (DON'T USE THIS!) - // 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(); -} - -//////////////////////////////////////////////////////////////////////////////////////////////////// -// 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) 2026 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.09; // 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 +SoftwareSerial debugSerial(-1, 1); // -1==nc for RX, 1==TX pin +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 mqttUpdateEspStateTopic; // MQTT topic for ESP update entity state +String mqttUpdateEspCommandTopic; // MQTT topic for ESP update entity install command +String mqttUpdateLcdStateTopic; // MQTT topic for LCD update entity state +String mqttUpdateLcdCommandTopic; // MQTT topic for LCD update entity install command +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 = "http://haswitchplate.com/update/HASwitchPlate.ino.d1_mini.bin"; +// Default link to compiled Nextion firmware images +String lcdFirmwareUrl = "http://haswitchplate.com/update/HASwitchPlate.tft"; +// Release notes URLs populated from version.json +String espReleaseUrl = "https://github.com/HASwitchPlate/HASPone/releases/latest"; +String lcdReleaseUrl = "https://github.com/HASwitchPlate/HASPone/releases/latest"; + +void setup(); +void loop(); +void mqttConnect(); +void mqttProcessInput(String &strTopic, String &strPayload); +void mqttStatusUpdate(); +void mqttUpdateState(); +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 + MDNS.begin(haspNode); + 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()); + char macStr[18]; + snprintf(macStr, sizeof(macStr), "%02X:%02X:%02X:%02X:%02X:%02X", espMac[0], espMac[1], espMac[2], espMac[3], espMac[4], espMac[5]); + MDNS.addServiceTxt(hMDNSService, "mac", macStr); + MDNS.addServiceTxt(hMDNSService, "mqtt_server", mqttServer); + 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(); + mqttUpdateState(); + } + } +} + +//////////////////////////////////////////////////////////////////////////////////////////////////// +// 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"; + mqttUpdateEspStateTopic = "hasp/" + String(haspNode) + "/update/esp"; + mqttUpdateEspCommandTopic = "hasp/" + String(haspNode) + "/update/esp/install"; + mqttUpdateLcdStateTopic = "hasp/" + String(haspNode) + "/update/lcd"; + mqttUpdateLcdCommandTopic = "hasp/" + String(haspNode) + "/update/lcd/install"; + + 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); + } + if (mqttClient.subscribe(mqttUpdateEspCommandTopic)) + { + debugPrintln(String(F("MQTT: subscribed to ")) + mqttUpdateEspCommandTopic); + } + if (mqttClient.subscribe(mqttUpdateLcdCommandTopic)) + { + debugPrintln(String(F("MQTT: subscribed to ")) + mqttUpdateLcdCommandTopic); + } + + // Publish discovery configuration + mqttDiscovery(); + + // Publish update entity state + mqttUpdateState(); + + // 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\"}"))); + } + else if (strTopic == mqttUpdateEspCommandTopic && strPayload == "INSTALL") + { // Home Assistant update entity triggered ESP firmware install + debugPrintln(F("MQTT: ESP update install triggered from HA")); + espStartOta(espFirmwareUrl); + } + else if (strTopic == mqttUpdateLcdCommandTopic && strPayload == "INSTALL") + { // Home Assistant update entity triggered LCD firmware install + debugPrintln(F("MQTT: LCD update install triggered from HA")); + nextionOtaStartDownload(lcdFirmwareUrl); + } +} + +//////////////////////////////////////////////////////////////////////////////////////////////////// +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 mqttUpdateState() +{ // Publish update entity state for ESP and LCD firmware + // ESP update state + String espUpdatePayload = String(F("{\"installed_version\":\"")) + String(haspVersion) + String(F("\",\"latest_version\":\"")); + if (updateEspAvailable) + { + espUpdatePayload += String(updateEspAvailableVersion); + } + else + { + espUpdatePayload += String(haspVersion); + } + espUpdatePayload += String(F("\",\"title\":\"HASPone ESP8266 firmware\",\"release_url\":\"")) + espReleaseUrl + String(F("\"}")); + mqttClient.publish(mqttUpdateEspStateTopic, espUpdatePayload, true, 1); + debugPrintln(String(F("MQTT OUT: '")) + mqttUpdateEspStateTopic + String(F("' : '")) + espUpdatePayload + String(F("'"))); + + // LCD update state + String lcdUpdatePayload = String(F("{\"installed_version\":\"")) + String(lcdVersion) + String(F("\",\"latest_version\":\"")); + if (updateLcdAvailable) + { + lcdUpdatePayload += String(updateLcdAvailableVersion); + } + else + { + lcdUpdatePayload += String(lcdVersion); + } + lcdUpdatePayload += String(F("\",\"title\":\"HASPone Nextion LCD firmware\",\"release_url\":\"")) + lcdReleaseUrl + String(F("\"}")); + mqttClient.publish(mqttUpdateLcdStateTopic, lcdUpdatePayload, true, 1); + debugPrintln(String(F("MQTT OUT: '")) + mqttUpdateLcdStateTopic + String(F("' : '")) + lcdUpdatePayload + 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\",\"default_entity_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\",\"default_entity_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\",\"default_entity_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\",\"default_entity_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\",\"default_entity_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\",\"default_entity_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\",\"default_entity_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\",\"default_entity_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("'"))); + } + + // update discovery for ESP firmware + mqttDiscoveryTopic = String(hassDiscovery) + String(F("/update/")) + String(haspNode) + String(F("-esp/config")); + mqttDiscoveryPayload = String(F("{\"name\":\"ESP8266 firmware\",\"default_entity_id\":\"")) + String(haspNode) + String(F("_esp8266_firmware\",\"state_topic\":\"")) + mqttUpdateEspStateTopic + String(F("\",\"command_topic\":\"")) + mqttUpdateEspCommandTopic + String(F("\",\"payload_install\":\"INSTALL\",\"availability_topic\":\"")) + mqttStatusTopic + String(F("\",\"payload_available\":\"ON\",\"payload_not_available\":\"OFF\",\"entity_category\":\"config\",\"icon\":\"mdi:cellphone-arrow-down\",\"unique_id\":\"")) + mqttClientId + String(F("-update-esp\",")) + mqttDiscoveryDevice; + mqttClient.publish(mqttDiscoveryTopic, mqttDiscoveryPayload, true, 1); + debugPrintln(String(F("MQTT OUT: '")) + mqttDiscoveryTopic + String(F("' : '")) + String(mqttDiscoveryPayload) + String(F("'"))); + + // update discovery for LCD firmware + mqttDiscoveryTopic = String(hassDiscovery) + String(F("/update/")) + String(haspNode) + String(F("-lcd/config")); + mqttDiscoveryPayload = String(F("{\"name\":\"Nextion LCD firmware\",\"default_entity_id\":\"")) + String(haspNode) + String(F("_nextion_lcd_firmware\",\"state_topic\":\"")) + mqttUpdateLcdStateTopic + String(F("\",\"command_topic\":\"")) + mqttUpdateLcdCommandTopic + String(F("\",\"payload_install\":\"INSTALL\",\"availability_topic\":\"")) + mqttStatusTopic + String(F("\",\"payload_available\":\"ON\",\"payload_not_available\":\"OFF\",\"entity_category\":\"config\",\"icon\":\"mdi:tablet-dashboard\",\"unique_id\":\"")) + mqttClientId + String(F("-update-lcd\",")) + 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 (rebootOnLongPressTimeout > 0) + { + 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 (rebootOnLongPressTimeout != 0 && (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 + JsonDocument nextionCommands; + 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: ")); + 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 >= 0) + { + 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) +{ + static uint8_t lastPercent = 255; + uint8_t progressPercent = (float(progress) / float(total)) * 100; + if (progressPercent != lastPercent) + { + lastPercent = progressPercent; + 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 + WiFi.hostname(haspNode); + WiFi.setAutoReconnect(true); + WiFi.setSleepMode(WIFI_NONE_SLEEP); + 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 >= 0) + { + nextionSendCmd("page " + String(nextionActivePage)); + } + + rebootOnp0b1 = false; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////// +void espWifiReconnect() +{ // Existing WiFi connection dropped, try to reconnect + debugPrintln(F("Reconnecting to WiFi network...")); + WiFi.persistent(false); + WiFi.mode(WIFI_STA); + WiFi.hostname(haspNode); + WiFi.setAutoReconnect(true); + WiFi.setSleepMode(WIFI_NONE_SLEEP); + if (String(wifiSSID) == "") + { + WiFi.begin(); + } + else + { + 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 + if (mqttClient.connected()) + { + mqttClient.disconnect(); + } + mqttClientSecure.stop(); + mqttClientSecure.setBufferSizes(0, 0); // Free MQTT TLS buffers + telnetClient.stop(); + webServer.stop(); + delay(1); + + debugPrintln(String(F("ESPFW: Attempting firmware update from: ")) + espOtaUrl); + + ESPhttpUpdate.rebootOnUpdate(false); + ESPhttpUpdate.onProgress(nextionUpdateProgress); + ESPhttpUpdate.setFollowRedirects(HTTPC_FORCE_FOLLOW_REDIRECTS); + t_httpUpdate_return espOtaUrlReturnCode; + if (espOtaUrl.startsWith(F("https"))) + { + WiFiClientSecure wifiEspOtaClientSecure; + wifiEspOtaClientSecure.setInsecure(); + wifiEspOtaClientSecure.setBufferSizes(4096, 512); + espOtaUrlReturnCode = ESPhttpUpdate.update(wifiEspOtaClientSecure, espOtaUrl); + } + else + { + 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) + { + JsonDocument jsonConfigValues; + 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; + } + } + + if (!jsonConfigValues["rebootOnLongPressTimeout"].isNull()) + { + rebootOnLongPressTimeout = jsonConfigValues["rebootOnLongPressTimeout"]; + } + if (rebootOnLongPressTimeout < 0) + { // Cover off any edge case where this value winds up being negative + debugPrintln(F("SPIFFS: [WARNING] /config.json has rebootOnLongPressTimeout value negative, setting to '10000'")); + rebootOnLongPressTimeout = 10000; + } + if (rebootOnLongPressTimeout > 99000) + { // Cover off any edge case where this value winds up being too high + debugPrintln(F("SPIFFS: [WARNING] /config.json has rebootOnLongPressTimeout value too high, setting to '10000'")); + rebootOnLongPressTimeout = 10000; + } + + 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")); + JsonDocument jsonConfigValues; + + 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; + jsonConfigValues["rebootOnLongPressTimeout"] = rebootOnLongPressTimeout; + + 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)); + debugPrintln(String(F("SPIFFS: rebootOnLongPressTimeout = ")) + String(rebootOnLongPressTimeout)); + + 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:
Hold timeout in msec (required, defaults to \"10000\", 0 disables it)

")); + + 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 ((webServer.arg("rebootOnLongPressTimeout") != String(rebootOnLongPressTimeout)) && (webServer.arg("rebootOnLongPressTimeout").toInt() < 99000) && (webServer.arg("rebootOnLongPressTimeout").toInt() >= 0)) + { + shouldSaveConfig = true; + rebootOnLongPressTimeout = webServer.arg("rebootOnLongPressTimeout").toInt(); + } + + 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; + debugPrintln(String(F("UPDATE: Checking update URL: ")) + FPSTR(UPDATE_URL)); + + // Parse host and path from UPDATE_URL + String url = FPSTR(UPDATE_URL); + String host = url.substring(url.indexOf("://") + 3); + String path = host.substring(host.indexOf('/')); + host = host.substring(0, host.indexOf('/')); + + wifiUpdateClientSecure.setInsecure(); + wifiUpdateClientSecure.setBufferSizes(1024, 512); + wifiUpdateClientSecure.setTimeout(10000); + + if (!wifiUpdateClientSecure.connect(host.c_str(), 443)) + { + debugPrintln(String(F("UPDATE: TLS connect failed, error: ")) + String(wifiUpdateClientSecure.getLastSSLError())); + return false; + } + + wifiUpdateClientSecure.print(String(F("GET ")) + path + String(F(" HTTP/1.0\r\nHost: ")) + host + String(F("\r\nConnection: close\r\nUser-Agent: HASPone\r\nAccept-Encoding: identity\r\n\r\n"))); + + // Parse HTTP headers to get content length + int contentLength = 0; + while (wifiUpdateClientSecure.connected() || wifiUpdateClientSecure.available()) + { + String line = wifiUpdateClientSecure.readStringUntil('\n'); + String lineLower = line; + lineLower.toLowerCase(); + if (lineLower.startsWith("content-length:")) + { + contentLength = lineLower.substring(15).toInt(); + } + if (line == "\r") + { + break; + } + } + + // Read exactly contentLength bytes + String payload; + if (contentLength > 0) + { + payload.reserve(contentLength); + while (payload.length() < (unsigned int)contentLength && (wifiUpdateClientSecure.connected() || wifiUpdateClientSecure.available())) + { + if (wifiUpdateClientSecure.available()) + { + payload += (char)wifiUpdateClientSecure.read(); + } + else + { + yield(); + } + } + } + wifiUpdateClientSecure.stop(); + + if (payload.length() == 0) + { + debugPrintln(F("UPDATE: Empty response body")); + return false; + } + + JsonDocument updateJson; + DeserializationError jsonError = deserializeJson(updateJson, payload); + + 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 (!updateJson["d1_mini"]["release_url"].isNull()) + { + espReleaseUrl = updateJson["d1_mini"]["release_url"].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 (!updateJson[nextionModel]["release_url"].isNull()) + { + lcdReleaseUrl = updateJson[nextionModel]["release_url"].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, 0); // stop 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, 254); // start 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); + 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); + 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 + 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/README.md b/Arduino_Sketch/README.md index 93927d6..4313cc5 100644 --- a/Arduino_Sketch/README.md +++ b/Arduino_Sketch/README.md @@ -1,5 +1,3 @@ # HASwitchPlate Arduino Sketch -Here you'll find the [Arduino source code](./HASwitchPlate/HASwitchPlate.ino) for the microcontroller firmware along with [a pre-compiled binary image](https://github.com/aderusha/HASwitchPlate/raw/master/Arduino_Sketch/HASwitchPlate.ino.d1_mini.bin) which can be [flashed directly to your ESP8266](../Documentation/01_Arduino_Sketch.md#nodemcu-flasher). - -Please [check the Arduino Sketch documentation](../Documentation/01_Arduino_Sketch.md) for additional deployment details. +Here you'll find the [Arduino source code](./HASwitchPlate/HASwitchPlate.ino) for the microcontroller firmware along with [a pre-compiled binary image](https://github.com/HASwitchPlate/HASPone/raw/main/Arduino_Sketch/HASwitchPlate.ino.d1_mini.bin) which can be [flashed directly to your ESP8266](https://github.com/HASwitchPlate/HASPone/wiki/Flashing-HASPone-to-an-ESP8266). diff --git a/Arduino_Sketch/debug/HASwitchPlate.ino.d1_mini.elf b/Arduino_Sketch/debug/HASwitchPlate.ino.d1_mini.elf index 000488e..4a100e0 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 df24b17..380a0cf 100644 --- a/Arduino_Sketch/platformio.ini +++ b/Arduino_Sketch/platformio.ini @@ -8,18 +8,23 @@ ; Please visit documentation for the other options and examples ; https://docs.platformio.org/page/projectconf.html +[platformio] +src_dir = . + [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#72b53316105e6e15ec56b430b151907b4867e66a \ 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 +monitor_speed = 115200 +upload_speed = 921600 \ No newline at end of file diff --git a/Home_Assistant/blueprints/README.md b/Home_Assistant/blueprints/README.md index ffb7574..a37ad6b 100644 --- a/Home_Assistant/blueprints/README.md +++ b/Home_Assistant/blueprints/README.md @@ -2,9 +2,9 @@ --- -## HASP Core Functionality +## HASPone Core Functionality -[![HASP Core functionality](https://my.home-assistant.io/badges/blueprint_import.svg)](https://my.home-assistant.io/redirect/blueprint_import/?blueprint_url=https%3A%2F%2Fgithub.com%2FHASwitchPlate%2FHASPone%2Fblob%2Fmain%2FHome_Assistant%2Fblueprints%2Fhasp_Core_Functionality.yaml) +[![HASPone Core functionality](https://my.home-assistant.io/badges/blueprint_import.svg)](https://my.home-assistant.io/redirect/blueprint_import/?blueprint_url=https%3A%2F%2Fgithub.com%2FHASwitchPlate%2FHASPone%2Fblob%2Fmain%2FHome_Assistant%2Fblueprints%2Fhasp_Core_Functionality.yaml) ![Preview](https://raw.githubusercontent.com/HASwitchPlate/HASPone/main/images/hasp_Core_Functionality.png) @@ -24,7 +24,7 @@ Activates a selected page after a specified period of inactivity. [![Activate Page](https://my.home-assistant.io/badges/blueprint_import.svg)](https://my.home-assistant.io/redirect/blueprint_import/?blueprint_url=https%3A%2F%2Fgithub.com%2FHASwitchPlate%2FHASPone%2Fblob%2Fmain%2FHome_Assistant%2Fblueprints%2Fhasp_Activate_Page.yaml) -A button on the HASP will activate a page when pressed. Can be combined on a button with another blueprint which displays text. +A button on the HASPone will activate a page when pressed. Can be combined on a button with another blueprint which displays text. --- @@ -32,7 +32,7 @@ A button on the HASP will activate a page when pressed. Can be combined on a but [![Create Device Triggers](https://my.home-assistant.io/badges/blueprint_import.svg)](https://my.home-assistant.io/redirect/blueprint_import/?blueprint_url=https%3A%2F%2Fgithub.com%2FHASwitchPlate%2FHASPone%2Fblob%2Fmain%2FHome_Assistant%2Fblueprints%2Fhasp_Create_Device_Triggers.yaml) -Create [Device Triggers](https://www.home-assistant.io/integrations/device_trigger.mqtt/) for each of the HASP buttons defined. Device triggers can be utilized while creating your own automations through the Home Assistant UI. This allows for the easy creation of automations which will be triggered when pressing buttons on your HASP. +Create [Device Triggers](https://www.home-assistant.io/integrations/device_trigger.mqtt/) for each of the HASPone buttons defined. Device triggers can be utilized while creating your own automations through the Home Assistant UI. This allows for the easy creation of automations which will be triggered when pressing buttons on your HASPone. --- @@ -52,7 +52,7 @@ Page 7 controls a selected alarm panel for code entry and arm/disarm. ![Preview](https://raw.githubusercontent.com/HASwitchPlate/HASPone/main/images/hasp_Display_Calendar_with_Icon.png) -A HASP button displays month + date on the right with a calendar icon on the left. +A HASPone button displays month + date on the right with a calendar icon on the left. --- @@ -62,7 +62,7 @@ A HASP button displays month + date on the right with a calendar icon on the lef ![Preview](https://raw.githubusercontent.com/HASwitchPlate/HASPone/main/images/hasp_Display_Clock_with_Icon.png) -A HASP button displays a clock on the right with a clock icon on the left. +A HASPone button displays a clock on the right with a clock icon on the left. --- @@ -72,7 +72,7 @@ A HASP button displays a clock on the right with a clock icon on the left. ![Preview](https://raw.githubusercontent.com/HASwitchPlate/HASPone/main/images/hasp_Display_Clock.png) -A HASP button displays a clock with configurable text options. +A HASPone button displays a clock with configurable text options. --- @@ -82,7 +82,7 @@ A HASP button displays a clock with configurable text options. ![Preview](https://raw.githubusercontent.com/HASwitchPlate/HASPone/main/images/hasp_Display_Dimmer_with_Icon.png) -A HASP button displays a dimmer control on page 4 and 5 with a toggle on/off icon to the left. +A HASPone button displays a dimmer control on page 4 and 5 with a toggle on/off icon to the left. --- @@ -92,7 +92,7 @@ A HASP button displays a dimmer control on page 4 and 5 with a toggle on/off ico ![Preview](https://raw.githubusercontent.com/HASwitchPlate/HASPone/main/images/hasp_Display_Entity_State_or_Attribute.png) -A HASP button displays the state or attribute value of an entity +A HASPone button displays the state or attribute value of an entity --- @@ -112,7 +112,7 @@ Page 8 controls a selected media player with artist and track info, track back/p ![Preview](https://raw.githubusercontent.com/HASwitchPlate/HASPone/main/images/hasp_Display_Template.png) -A button on the HASP will display the output of a template. The template is updated when the state of a selected entity updates. +A button on the HASPone will display the output of a template. The template is updated when the state of a selected entity updates. --- @@ -122,7 +122,7 @@ A button on the HASP will display the output of a template. The template is upd ![Preview](https://raw.githubusercontent.com/HASwitchPlate/HASPone/main/images/hasp_Display_Text.png) -A button on the HASP will display text. This can be useful when combined with other blueprints which perform an action, but don't apply a label to a button. Deploy both blueprints on the same button, and now you have a button that says things and does things. +A button on the HASPone will display text. This can be useful when combined with other blueprints which perform an action, but don't apply a label to a button. Deploy both blueprints on the same button, and now you have a button that says things and does things. --- @@ -132,7 +132,17 @@ A button on the HASP will display text. This can be useful when combined with o ![Preview](https://raw.githubusercontent.com/HASwitchPlate/HASPone/main/images/hasp_Display_Toggle.gif) -Press a button on the HASP to toggle the state of an entity. The button colors and text can change in response to the on/off state or attribute of the selected entity. +Press a button on the HASPone to toggle the state of an entity. The button colors and text can change in response to the on/off state or attribute of the selected entity. + +--- + +### Display Value with Icon and Colors + +[![Display Value with Icon and Colors](https://my.home-assistant.io/badges/blueprint_import.svg)](https://my.home-assistant.io/redirect/blueprint_import/?blueprint_url=https%3A%2F%2Fgithub.com%2FHASwitchPlate%2FHASPone%2Fblob%2Fmain%2FHome_Assistant%2Fblueprints%2Fhasp_Display_Value_with_Icon_and_Colors.yaml) + +![Preview](https://raw.githubusercontent.com/HASwitchPlate/HASPone/main/images/hasp_Display_Value_with_Icon_and_Colors.png) + +A HASPone button displays the current value of an entity (state or attribute) with a dynamic icon on the left and (optional) colors. Up to 5 icons and color ranges are supported. --- @@ -152,7 +162,17 @@ The slider button on page 8 displays a volume control ![Preview](https://raw.githubusercontent.com/HASwitchPlate/HASPone/main/images/hasp_Display_Weather_Condition_with_Icon.png) -A HASP button displays the current weather condition on the right with a matching icon on the left +A HASPone button displays the current weather condition on the right with a matching icon on the left + +--- + +### Display Weather Condition Icon Only + +[![Display Weather Condition Icon Only](https://my.home-assistant.io/badges/blueprint_import.svg)](https://my.home-assistant.io/redirect/blueprint_import/?blueprint_url=https%3A%2F%2Fgithub.com%2FHASwitchPlate%2FHASPone%2Fblob%2Fmain%2FHome_Assistant%2Fblueprints%2Fhasp_Display_Weather_Condition_Icon_Only.yaml) + +![Preview](https://raw.githubusercontent.com/HASwitchPlate/HASPone/main/images/hasp_Display_Weather_Condition_Icon_Only.png) + +A HASPone button displays the current weather condition as an icon --- @@ -162,7 +182,7 @@ A HASP button displays the current weather condition on the right with a matchin ![Preview](https://raw.githubusercontent.com/HASwitchPlate/HASPone/main/images/hasp_Display_Weather_Forecast.png) -A HASP button displays an attribute of a selected weather forecast. You can use this to display tomorrow's condition, or tonight's low temp. Available forecast conditions will vary by weather provider, check your selected provider's state under `Developer Tools` > `States` to get a sense of what your selected provider has to offer. +A HASPone button displays an attribute of a selected weather forecast. You can use this to display tomorrow's condition, or tonight's low temp. Available forecast conditions will vary by weather provider, check your selected provider's state under `Developer Tools` > `States` to get a sense of what your selected provider has to offer. --- @@ -172,7 +192,7 @@ A HASP button displays an attribute of a selected weather forecast. You can use ![Preview](https://raw.githubusercontent.com/HASwitchPlate/HASPone/main/images/hasp_Display_Weather_Temperature_Color_Icon_Only.png) -A HASP button displays the current temperature as an icon that is optionally coloured. +A HASPone button displays the current temperature as an icon that is optionally coloured. --- @@ -182,7 +202,7 @@ A HASP button displays the current temperature as an icon that is optionally col ![Preview](https://raw.githubusercontent.com/HASwitchPlate/HASPone/main/images/hasp_Display_Weather_Temperature_with_Icon_and_Colors.png) -A HASP button displays the current temperature from a selected weather provider on the right with a dynamic thermometer icon on the left and (optional) colors. +A HASPone button displays the current temperature from a selected weather provider on the right with a dynamic thermometer icon on the left and (optional) colors. --- @@ -190,7 +210,7 @@ A HASP button displays the current temperature from a selected weather provider [![Perform Action](https://my.home-assistant.io/badges/blueprint_import.svg)](https://my.home-assistant.io/redirect/blueprint_import/?blueprint_url=https%3A%2F%2Fgithub.com%2FHASwitchPlate%2FHASPone%2Fblob%2Fmain%2FHome_Assistant%2Fblueprints%2Fhasp_Perform_Action.yaml) -A button on the HASP will perform an action when pressed. Can be combined on a button with another blueprint which displays text. +A button on the HASPone will perform an action when pressed. Can be combined on a button with another blueprint which displays text. --- @@ -200,7 +220,7 @@ A button on the HASP will perform an action when pressed. Can be combined on a b ![Preview](https://raw.githubusercontent.com/HASwitchPlate/HASPone/main/images/hasp_Cycle_Automations.gif) -A button on the HASP will toggle through as many as 10 selected automations. This allows the user to assign multiple blueprints to the same button on the HASPone device, and to cycle between them by pressing the selected button. +A button on the HASPone will toggle through as many as 10 selected automations. This allows the user to assign multiple blueprints to the same button on the HASPone device, and to cycle between them by pressing the selected button. Optionally, a timeout can be set to cycle back to a "default" automation after a specified interval, or to continuously cycle through selected automations. @@ -226,27 +246,37 @@ Dim the screen backlight after a specified period of inactivity. [![Apply Theme](https://my.home-assistant.io/badges/blueprint_import.svg)](https://my.home-assistant.io/redirect/blueprint_import/?blueprint_url=https%3A%2F%2Fgithub.com%2FHASwitchPlate%2FHASPone%2Fblob%2Fmain%2FHome_Assistant%2Fblueprints%2Fhasp_Apply_Theme.yaml) -A button on the HASP will have the current device theme or custom colors applied. +A button on the HASPone will have the current device theme or custom colors applied. --- -### HASP Theme Dark on Light +### HASPone Theme Dark on Light -[![HASP Theme Dark on Light](https://my.home-assistant.io/badges/blueprint_import.svg)](https://my.home-assistant.io/redirect/blueprint_import/?blueprint_url=https%3A%2F%2Fgithub.com%2FHASwitchPlate%2FHASPone%2Fblob%2Fmain%2FHome_Assistant%2Fblueprints%2Fhasp_Theme_Dark_on_Light.yaml) +[![HASPone Theme Dark on Light](https://my.home-assistant.io/badges/blueprint_import.svg)](https://my.home-assistant.io/redirect/blueprint_import/?blueprint_url=https%3A%2F%2Fgithub.com%2FHASwitchPlate%2FHASPone%2Fblob%2Fmain%2FHome_Assistant%2Fblueprints%2Fhasp_Theme_Dark_on_Light.yaml) ![Preview](https://raw.githubusercontent.com/HASwitchPlate/HASPone/main/images/hasp_Theme_Dark_on_Light.png) -Press RUN ACTIONS to apply the theme Dark on Light to the selected HASP device +Press RUN ACTIONS to apply the theme Dark on Light to the selected HASPone device --- -### HASP Theme Light on Dark +### HASPone Theme Light on Dark -[![HASP Theme Light on Dark](https://my.home-assistant.io/badges/blueprint_import.svg)](https://my.home-assistant.io/redirect/blueprint_import/?blueprint_url=https%3A%2F%2Fgithub.com%2FHASwitchPlate%2FHASPone%2Fblob%2Fmain%2FHome_Assistant%2Fblueprints%2Fhasp_Theme_Light_on_Dark.yaml) +[![HASPone Theme Light on Dark](https://my.home-assistant.io/badges/blueprint_import.svg)](https://my.home-assistant.io/redirect/blueprint_import/?blueprint_url=https%3A%2F%2Fgithub.com%2FHASwitchPlate%2FHASPone%2Fblob%2Fmain%2FHome_Assistant%2Fblueprints%2Fhasp_Theme_Light_on_Dark.yaml) ![Preview](https://raw.githubusercontent.com/HASwitchPlate/HASPone/main/images/hasp_Theme_Light_on_Dark.png) -Press RUN ACTIONS to apply the theme Light on Dark to the selected HASP device +Press RUN ACTIONS to apply the theme Light on Dark to the selected HASPone device + +--- + +### HASPone Theme Light on Dark Blue + +[![HASPone Theme Light on Dark](https://my.home-assistant.io/badges/blueprint_import.svg)](https://my.home-assistant.io/redirect/blueprint_import/?blueprint_url=https%3A%2F%2Fgithub.com%2FHASwitchPlate%2FHASPone%2Fblob%2Fmain%2FHome_Assistant%2Fblueprints%2Fhasp_Theme_Light_on_BlueDark.yaml) + +![Preview](https://raw.githubusercontent.com/HASwitchPlate/HASPone/main/images/hasp_Theme_Light_on_BlueDark.png) + +Press RUN ACTIONS to apply the theme Light on Dark Blue to the selected HASPone device --- @@ -254,4 +284,4 @@ Press RUN ACTIONS to apply the theme Light on Dark to the selected HASP device [![Remove MQTT Discovery Devices](https://my.home-assistant.io/badges/blueprint_import.svg)](https://my.home-assistant.io/redirect/blueprint_import/?blueprint_url=https%3A%2F%2Fgithub.com%2FHASwitchPlate%2FHASPone%2Fblob%2Fmain%2FHome_Assistant%2Fblueprints%2Fhasp_Remove_MQTT_Discovery_Devices.yaml) -Press RUN ACTIONS to remove retained MQTT discovery messages for a decommissioned HASP. +Press RUN ACTIONS to remove retained MQTT discovery messages for a decommissioned HASPone. diff --git a/Home_Assistant/blueprints/hasp_Activate_Page.yaml b/Home_Assistant/blueprints/hasp_Activate_Page.yaml index e5fefdb..3b21e19 100644 --- a/Home_Assistant/blueprints/hasp_Activate_Page.yaml +++ b/Home_Assistant/blueprints/hasp_Activate_Page.yaml @@ -1,16 +1,16 @@ blueprint: - name: "HASP p[x].b[y] activates a page" + name: "HASPone p[x].b[y] activates a page" description: | - ## Blueprint Version: `1.03.00` + ## Blueprint Version: `1.08.00` # Description - A button on the HASP will activate a page when pressed. Can be combined on a button with another blueprint which displays text. + A button on the HASPone will activate a page when pressed. Can be combined on a button with another blueprint which displays text. - ## HASP Page and Button Reference + ## HASPone Page and Button Reference - The images below show each available HASP page along with the layout of available button objects. + The images below show each available HASPone page along with the layout of available button objects.
@@ -31,16 +31,16 @@ blueprint: domain: automation input: haspdevice: - name: "HASP Device" - description: "Select the HASP device" + name: "HASPone Device" + description: "Select the HASPone 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 page button. Refer to the HASP Page and Button reference above." + name: "HASPone Page" + description: "Select the HASPone page (1-11) for this page button. Refer to the HASPone Page and Button reference above." default: 1 selector: number: @@ -49,8 +49,8 @@ blueprint: mode: slider unit_of_measurement: page haspbutton: - name: "HASP Button" - description: "Select the HASP button for this page button. Refer to the HASP Page and Button reference above." + name: "HASPone Button" + description: "Select the HASPone button for this page button. Refer to the HASPone Page and Button reference above." default: 4 selector: number: @@ -76,7 +76,7 @@ variables: haspdevice: !input haspdevice haspname: >- {%- for entity in device_entities(haspdevice) -%} - {%- if entity|regex_search("^sensor\.") -%} + {%- 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 -%} @@ -90,7 +90,7 @@ trigger_variables: haspdevice: !input haspdevice haspname: >- {%- for entity in device_entities(haspdevice) -%} - {%- if entity|regex_search("^sensor\.") -%} + {%- 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 -%} @@ -107,7 +107,7 @@ trigger_variables: buttonjsonpayload: '{"event_type":"button_short_release","event":"{{haspobject}}","value":"OFF"}' trigger: - - platform: mqtt + - trigger: mqtt topic: "{{jsontopic}}" payload: "{{buttonjsonpayload}}" diff --git a/Home_Assistant/blueprints/hasp_Activate_Page_on_Idle.yaml b/Home_Assistant/blueprints/hasp_Activate_Page_on_Idle.yaml index 38675e9..003da52 100644 --- a/Home_Assistant/blueprints/hasp_Activate_Page_on_Idle.yaml +++ b/Home_Assistant/blueprints/hasp_Activate_Page_on_Idle.yaml @@ -1,145 +1,145 @@ -blueprint: - name: "HASP activates a selected page after a specified period of inactivity" - description: | - - ## Blueprint Version: `1.03.00` - - # Description - - Activates a selected page after a specified period of inactivity. - - ## 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) - -
- - domain: automation - input: - haspdevice: - name: "HASP Device" - description: "Select the HASP device" - selector: - device: - integration: mqtt - manufacturer: "HASwitchPlate" - model: "HASPone v1.0.0" - targetpage: - name: "Page to activate" - description: "Select a destination page for this button to activate." - default: 1 - selector: - number: - min: 1 - max: 11 - mode: slider - unit_of_measurement: page - idletime: - name: "Idle Time" - description: "Idle time in seconds" - default: 30 - selector: - number: - min: 5 - max: 900 - step: 5 - mode: slider - unit_of_measurement: seconds - -mode: restart -max_exceeded: silent - -variables: - haspdevice: !input haspdevice - haspname: >- - {%- for entity in device_entities(haspdevice) -%} - {%- if entity|regex_search("^sensor\.") -%} - {{- entity|regex_replace(find="^sensor\.", replace="", ignorecase=true)|regex_replace(find="_sensor(?:_\d+|)$", replace="", ignorecase=true) -}} - {%- endif -%} - {%- endfor -%} - targetpage: !input targetpage - idletime: !input idletime - pagecommandtopic: '{{ "hasp/" ~ haspname ~ "/command/page" }}' - 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) }} - -trigger_variables: - haspdevice: !input haspdevice - haspname: >- - {%- for entity in device_entities(haspdevice) -%} - {%- if entity|regex_search("^sensor\.") -%} - {{- 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" }}' - targetpage: !input targetpage - pagejsonpayload: '{"event":"page","value":{{targetpage}}}' - -trigger: - - platform: mqtt - topic: "{{jsontopic}}" - -condition: - - condition: template - value_template: "{{ is_state(haspsensor, 'ON') }}" - - condition: template - value_template: >- - {{- - (trigger.payload_json.event is defined) - and - (trigger.payload_json.event == 'page') - and - (trigger.payload_json.value is defined) - and - (trigger.payload_json.value != targetpage) - -}} - -action: - - delay: - seconds: "{{idletime|int}}" - - - condition: template - value_template: >- - {%- set currentpage = namespace() -%} - {%- for entity in device_entities(haspdevice) -%} - {%- if entity|regex_search("^number\..*_active_page(?:_\d+|)$") -%} - {%- set currentpage.entity=entity -%} - {%- endif -%} - {%- endfor -%} - {%- if states(currentpage.entity) == targetpage -%} - {{false}} - {%- else -%} - {{true}} - {%- endif -%} - - - service: mqtt.publish - data: - topic: "{{pagecommandtopic}}" - payload: "{{targetpage}}" - retain: true \ No newline at end of file +blueprint: + name: "HASPone activates a selected page after a specified period of inactivity" + description: | + + ## Blueprint Version: `1.08.00` + + # Description + + Activates a selected page after a specified period of inactivity. + + ## 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) + +
+ + domain: automation + input: + haspdevice: + name: "HASPone Device" + description: "Select the HASPone device" + selector: + device: + integration: mqtt + manufacturer: "HASwitchPlate" + model: "HASPone v1.0.0" + targetpage: + name: "Page to activate" + description: "Select a destination page for this button to activate." + default: 1 + selector: + number: + min: 1 + max: 11 + mode: slider + unit_of_measurement: page + idletime: + name: "Idle Time" + description: "Idle time in seconds" + default: 30 + selector: + number: + min: 5 + max: 900 + step: 5 + mode: slider + unit_of_measurement: seconds + +mode: restart +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 -%} + targetpage: !input targetpage + idletime: !input idletime + pagecommandtopic: '{{ "hasp/" ~ haspname ~ "/command/page" }}' + 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) }} + +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" }}' + targetpage: !input targetpage + pagejsonpayload: '{"event":"page","value":{{targetpage}}}' + +triggers: + - trigger: mqtt + topic: "{{jsontopic}}" + +condition: + - condition: template + value_template: "{{ is_state(haspsensor, 'ON') }}" + - condition: template + value_template: >- + {{- + (trigger.payload_json.event is defined) + and + (trigger.payload_json.event == 'page') + and + (trigger.payload_json.value is defined) + and + (trigger.payload_json.value != targetpage) + -}} + +action: + - delay: + seconds: "{{idletime|int(default=30)}}" + + - condition: template + value_template: >- + {%- set currentpage = namespace() -%} + {%- for entity in device_entities(haspdevice) -%} + {%- if entity|regex_search("^number\..*_active_page(?:_\d+|)$") -%} + {%- set currentpage.entity=entity -%} + {%- endif -%} + {%- endfor -%} + {%- if states(currentpage.entity) == targetpage -%} + {{false}} + {%- else -%} + {{true}} + {%- endif -%} + + - service: mqtt.publish + data: + topic: "{{pagecommandtopic}}" + payload: "{{targetpage}}" + retain: true diff --git a/Home_Assistant/blueprints/hasp_Apply_Theme.yaml b/Home_Assistant/blueprints/hasp_Apply_Theme.yaml index b43fd01..c7f399a 100644 --- a/Home_Assistant/blueprints/hasp_Apply_Theme.yaml +++ b/Home_Assistant/blueprints/hasp_Apply_Theme.yaml @@ -1,18 +1,18 @@ blueprint: - name: "HASP p[x].b[y] has theme colors applied" + name: "HASPone p[x].b[y] has theme colors applied" description: | - ## Blueprint Version: `1.03.00` + ## Blueprint Version: `1.08.00` ## Description - A button on the HASP will have the current device theme or custom colors applied. + A button on the HASPone will have the current device theme or custom colors applied. - ## HASP Page and Button Reference + ## HASPone Page and Button Reference
- The images below show each available HASP page along with the layout of available button objects. + The images below show each available HASPone page along with the layout of available button objects. | Page 0 | Pages 1-3 | Pages 4-5 | |--------|-----------|-----------| @@ -53,16 +53,16 @@ blueprint: domain: automation input: haspdevice: - name: "HASP Device" - description: "Select the HASP device" + name: "HASPone Device" + description: "Select the HASPone device" selector: device: integration: mqtt manufacturer: "HASwitchPlate" model: "HASPone v1.0.0" hasppage: - name: "HASP Page" - description: "Select the HASP page (1-11). Refer to the HASP Page and Button reference above." + name: "HASPone Page" + description: "Select the HASPone page (1-11). Refer to the HASPone Page and Button reference above." default: 1 selector: number: @@ -71,8 +71,8 @@ blueprint: mode: slider unit_of_measurement: page haspbutton: - name: "HASP Button" - description: "Select the HASP button (4-15) to apply color theme to. Refer to the HASP Page and Button reference above." + name: "HASPone Button" + description: "Select the HASPone button (4-15) to apply color theme to. Refer to the HASPone Page and Button reference above." default: 4 selector: number: @@ -124,7 +124,7 @@ variables: haspdevice: !input haspdevice haspname: >- {%- for entity in device_entities(haspdevice) -%} - {%- if entity|regex_search("^sensor\.") -%} + {%- 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 -%} @@ -210,7 +210,7 @@ trigger_variables: haspdevice: !input haspdevice haspname: >- {%- for entity in device_entities(haspdevice) -%} - {%- if entity|regex_search("^sensor\.") -%} + {%- 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 -%} @@ -227,17 +227,17 @@ trigger_variables: unselectedbgtopic: '{{ "hasp/" ~ haspname ~ "/light/unselectedbackgroundcolor/rgb" }}' trigger: - - platform: homeassistant + - trigger: homeassistant event: start - - platform: template + - trigger: template value_template: "{{ is_state(haspsensor, 'ON') }}" - - platform: mqtt + - trigger: mqtt topic: "{{selectedfgtopic}}" - - platform: mqtt + - trigger: mqtt topic: "{{selectedbgtopic}}" - - platform: mqtt + - trigger: mqtt topic: "{{unselectedfgtopic}}" - - platform: mqtt + - trigger: mqtt topic: "{{unselectedbgtopic}}" condition: @@ -247,7 +247,7 @@ condition: action: - choose: ######################################################################### - # RUN ACTIONS or Home Assistant Startup or HASP Connect + # RUN ACTIONS or Home Assistant Startup or HASPone Connect # Apply text style - conditions: - condition: template @@ -283,39 +283,39 @@ action: # Theme: Apply selected foreground color on change - conditions: - condition: template - value_template: "{{ trigger.topic == selectedfgtopic }}" + value_template: "{{ (trigger.topic == selectedfgtopic) and ((selected_fgcolor|int) == -1) }}" sequence: - service: mqtt.publish data: topic: "{{commandtopic}}.pco" - payload: "{{selectedfg}}" + payload: "{{trigger.payload}}" ######################################################################### # Theme: Apply selected background color on change - conditions: - condition: template - value_template: "{{ trigger.topic == selectedbgtopic }}" + value_template: "{{ (trigger.topic == selectedbgtopic) and ((selected_bgcolor|int) == -1) }}" sequence: - service: mqtt.publish data: topic: "{{commandtopic}}.bco" - payload: "{{selectedbg}}" + payload: "{{trigger.payload}}" ######################################################################### # Theme: Apply unselected foreground color on change - conditions: - condition: template - value_template: "{{ trigger.topic == unselectedfgtopic }}" + value_template: "{{ (trigger.topic == unselectedfgtopic) and ((unselected_fgcolor|int) == -1) }}" sequence: - service: mqtt.publish data: topic: "{{commandtopic}}.pco2" - payload: "{{unselectedfg}}" + payload: "{{trigger.payload}}" ######################################################################### # Theme: Apply unselected background color on change - conditions: - condition: template - value_template: "{{ trigger.topic == unselectedbgtopic }}" + value_template: "{{ (trigger.topic == unselectedbgtopic) and ((unselected_bgcolor|int) == -1) }}" sequence: - service: mqtt.publish data: topic: "{{commandtopic}}.bco2" - payload: "{{unselectedbg}}" + payload: "{{trigger.payload}}" 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..f38dabd --- /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.08.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: + - trigger: homeassistant + event: start + - trigger: template + value_template: "{{ is_state(haspsensor, 'ON') }}" + - trigger: mqtt + topic: "{{selectedfgtopic}}" + - trigger: mqtt + topic: "{{selectedbgtopic}}" + - trigger: mqtt + topic: "{{unselectedfgtopic}}" + - trigger: 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 83b135b..b7dad4d 100644 --- a/Home_Assistant/blueprints/hasp_Core_Functionality.yaml +++ b/Home_Assistant/blueprints/hasp_Core_Functionality.yaml @@ -1,18 +1,18 @@ blueprint: - name: "HASP Core functionality" + name: "HASPone Core functionality" description: | - ## Blueprint Version: `1.03.00` + ## Blueprint Version: `1.08.00` ## Description - Provides core HASP functionality. Deploy one copy of this blueprint for each HASP device. + Provides core HASPone functionality. Deploy one copy of this blueprint for each HASPone device. ![Preview](https://raw.githubusercontent.com/HASwitchPlate/HASPone/main/images/hasp_Core_Functionality.png) - ## HASP Page and Button Reference + ## HASPone Page and Button Reference - The images below show each available HASP page along with the layout of available button objects. + The images below show each available HASPone page along with the layout of available button objects.
@@ -30,11 +30,11 @@ blueprint:
- ## HASP Font Reference + ## HASPone 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 + 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 | | :--- | :---------------- | :-------------------| :--------------- | @@ -56,15 +56,15 @@ blueprint: ### 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) + ![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)
domain: automation input: haspdevice: - name: "HASP Device" - description: "Select the HASP device" + name: "HASPone Device" + description: "Select the HASPone device" selector: device: integration: mqtt @@ -78,7 +78,7 @@ blueprint: text: page1font_select: name: "Page select button 1 font" - description: "Select the font for page select button #1. Font 6 might be a good starting point. You can refer to the HASP Font Reference above to see what the available options look like." + description: "Select the font for page select button #1. Font 6 might be a good starting point. You can refer to the HASPone Font Reference above to see what the available options look like." default: "6 - Noto Sans 32" selector: select: @@ -96,7 +96,7 @@ blueprint: - "10 - Noto Sans Bold 80" page1page: name: "Page select button 1 page" - description: "Select the destination page for page select button #1. When you click the left-most page button on the HASP, the HASP will flip to the page number you select here. If this is your first time here, try page 1." + description: "Select the destination page for page select button #1. When you click the left-most page button on the HASPone, the HASPone will flip to the page number you select here. If this is your first time here, try page 1." default: 1 selector: number: @@ -209,8 +209,14 @@ blueprint: selector: boolean: reset_hasp: - name: "First-time HASP setup or reset" - description: "Turn this on if this is a brand-new HASP (or you'd like to reset some default theme settings). Turn it on, hit save, and click RUN ACTIONS to setup your HASP. After the first run you can toggle this back off." + name: "First-time HASPone setup or reset" + description: "Turn this on if this is a brand-new HASPone (or you'd like to reset some default theme settings). Turn it on, hit save, and click RUN ACTIONS to setup your HASPone. After the first run you can toggle this back off." + default: true + selector: + boolean: + maximize_performance: + name: "Maximize HASPone performance" + description: "When enabled, LCD serial speed is set to 921600 and local serial debug is disabled for maximum HASPone performance. Disable this for troubleshooting or development. Press RUN ACTIONS to apply." default: true selector: boolean: @@ -222,27 +228,28 @@ variables: haspdevice: !input haspdevice haspname: >- {%- for entity in device_entities(haspdevice) -%} - {%- if entity|regex_search("^sensor\.") -%} + {%- 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 -%} 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 page_names: !input page_names show_lovelace: !input show_lovelace reset_hasp: !input reset_hasp + maximize_performance: !input maximize_performance activepage: >- {%- set activepage = namespace() -%} {%- for entity in device_entities(haspdevice) -%} @@ -282,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}} @@ -313,7 +320,7 @@ trigger_variables: haspdevice: !input haspdevice haspname: >- {%- for entity in device_entities(haspdevice) -%} - {%- if entity|regex_search("^sensor\.") -%} + {%- 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 -%} @@ -329,20 +336,20 @@ trigger_variables: unselectedfgtopic: '{{ "hasp/" ~ haspname ~ "/light/unselectedforegroundcolor/rgb" }}' unselectedbgtopic: '{{ "hasp/" ~ haspname ~ "/light/unselectedbackgroundcolor/rgb" }}' -trigger: - - platform: template +triggers: + - trigger: template value_template: "{{ is_state(haspsensor, 'ON') }}" - - platform: homeassistant + - trigger: homeassistant event: start - - platform: mqtt + - trigger: mqtt topic: "{{jsontopic}}" - - platform: mqtt + - trigger: mqtt topic: "{{selectedfgtopic}}" - - platform: mqtt + - trigger: mqtt topic: "{{selectedbgtopic}}" - - platform: mqtt + - trigger: mqtt topic: "{{unselectedfgtopic}}" - - platform: mqtt + - trigger: mqtt topic: "{{unselectedbgtopic}}" condition: @@ -352,19 +359,16 @@ condition: action: - choose: ######################################################################### - # Create required helpers and apply HASP defaults when "RUN ACTIONS" is pressed by the user + # Create required helpers and apply HASPone defaults when "RUN ACTIONS" is pressed by the user - conditions: - condition: template value_template: "{{ (trigger is not defined) or (trigger.platform is none) }}" sequence: - # Send page select button config - - service: mqtt.publish + - service: mqtt.publish # publish alwayson payload data: - topic: "{{jsoncommandtopic}}" - payload: >- - [{% for p in range(1,12) %}"p[{{p}}].b[1].font={{page1font}}","p[{{p}}].b[1].txt=\"{{page1text}}\"",{% endfor %} - {% for p in range(1,12) %}"p[{{p}}].b[2].font={{page2font}}","p[{{p}}].b[2].txt=\"{% if page_scroll %}{{page_names.get("page" ~ p)}}{% else %}{{page2text}}{% endif %}\"",{% endfor %} - {% for p in range(1,12) %}"p[{{p}}].b[3].font={{page3font}}","p[{{p}}].b[3].txt=\"{{page3text}}\""{% if not loop.last %},{% endif %}{% endfor %}] + topic: "hasp/{{haspname}}/alwayson" + payload: "ON" + retain: true - choose: ######################################################################### @@ -394,14 +398,14 @@ action: {%- set haspentities.unselectedbackground=entity -%} {%- endif -%} {%- endfor -%} - To [create a Lovelace card](https://www.home-assistant.io/lovelace/) for HASP {{haspname}}, + To [create a Lovelace card](https://www.home-assistant.io/lovelace/) for HASPone {{haspname}}, add a manual card and then paste in the code you see below. ```yaml type: entities - title: HASP {{haspname}} + title: HASPone {{haspname}} show_header_toggle: false @@ -455,13 +459,44 @@ action: action: navigate navigation_path: /config/automation/dashboard - entity: {{haspsensor}} - name: HASP Admin + name: HASPone Admin icon: 'mdi:cellphone-text' tap_action: action: url url_path: http://{{haspIP}} ``` - + - choose: + ######################################################################### + # Set LCD communication serial speed to max and disable local serial debug output + - conditions: + - condition: template + value_template: "{{ maximize_performance }}" + sequence: + - service: mqtt.publish + data: + topic: "hasp/{{haspname}}/command/debugserialenabled" + payload: "false" + - service: mqtt.publish + data: + topic: "hasp/{{haspname}}/command/nextionbaud" + payload: "921600" + default: + - service: mqtt.publish + data: + topic: "hasp/{{haspname}}/command/debugserialenabled" + payload: "true" + - service: mqtt.publish + data: + topic: "hasp/{{haspname}}/command/nextionbaud" + payload: "115200" + # Send page select button config + - service: mqtt.publish + data: + topic: "{{jsoncommandtopic}}" + payload: >- + [{% for p in range(1,12) %}"p[{{p}}].b[1].font={{page1font}}","p[{{p}}].b[1].txt=\"{{page1text}}\"",{% endfor %} + {% for p in range(1,12) %}"p[{{p}}].b[2].font={{page2font}}","p[{{p}}].b[2].txt=\"{% if page_scroll %}{{page_names.get("page" ~ p)}}{% else %}{{page2text}}{% endif %}\"",{% endfor %} + {% for p in range(1,12) %}"p[{{p}}].b[3].font={{page3font}}","p[{{p}}].b[3].txt=\"{{page3text}}\""{% if not loop.last %},{% endif %}{% endfor %}] - choose: ######################################################################### # Push some defaults to the device @@ -573,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: @@ -593,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: @@ -613,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: @@ -633,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 -%}] ######################################################################### @@ -672,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: @@ -692,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: @@ -712,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: @@ -732,16 +770,22 @@ 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: topic: "hasp/{{haspname}}/command" payload: "" + - service: mqtt.publish # publish alwayson payload + data: + topic: "hasp/{{haspname}}/alwayson" + payload: "ON" + retain: true ######################################################################### # Catch incoming JSON messages @@ -807,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 }}" @@ -819,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 @@ -829,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 }}" @@ -841,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 @@ -851,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 }}" @@ -863,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 @@ -873,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 }}" @@ -885,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 9329be5..02016fe 100644 --- a/Home_Assistant/blueprints/hasp_Create_Device_Triggers.yaml +++ b/Home_Assistant/blueprints/hasp_Create_Device_Triggers.yaml @@ -1,18 +1,18 @@ blueprint: - name: "HASP create device triggers" + name: "HASPone create device triggers" description: | - ## Blueprint Version: `1.03.00` + ## Blueprint Version: `1.08.00` # Description - Create [Device Triggers](https://www.home-assistant.io/integrations/device_trigger.mqtt/) for each of the HASP buttons defined below. Device triggers can be utilized while creating your own automations through the Home Assistant UI. + Create [Device Triggers](https://www.home-assistant.io/integrations/device_trigger.mqtt/) for each of the HASPone buttons defined below. Device triggers can be utilized while creating your own automations through the Home Assistant UI. - This allows for the easy creation of automations which will be triggered when pressing buttons on your HASP. + This allows for the easy creation of automations which will be triggered when pressing buttons on your HASPone. - ## HASP Page and Button Reference + ## HASPone Page and Button Reference - The images below show each available HASP page along with the layout of available button objects. + The images below show each available HASPone page along with the layout of available button objects.
@@ -33,15 +33,15 @@ blueprint: domain: automation input: haspdevice: - name: "HASP Device" - description: "Select the HASP device" + name: "HASPone Device" + description: "Select the HASPone device" selector: device: integration: mqtt manufacturer: "HASwitchPlate" model: "HASPone v1.0.0" objects: - name: "HASP buttons" + name: "HASPone buttons" description: "Create one device trigger for each button in this list." default: - p[1].b[4] @@ -59,7 +59,7 @@ variables: objects: !input objects haspname: >- {%- for entity in device_entities(haspdevice) -%} - {%- if entity|regex_search("^sensor\.") -%} + {%- 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 -%} @@ -84,9 +84,9 @@ trigger_variables: {%- endfor -%} trigger: - - platform: template + - trigger: template value_template: "{{ is_state(haspsensor, 'ON') }}" - - platform: homeassistant + - trigger: homeassistant event: start condition: diff --git a/Home_Assistant/blueprints/hasp_Cycle_Automations.yaml b/Home_Assistant/blueprints/hasp_Cycle_Automations.yaml index d273ecc..037396e 100644 --- a/Home_Assistant/blueprints/hasp_Cycle_Automations.yaml +++ b/Home_Assistant/blueprints/hasp_Cycle_Automations.yaml @@ -1,12 +1,12 @@ blueprint: - name: "HASP p[x].b[y] cycles through multiple automations" + name: "HASPone p[x].b[y] cycles through multiple automations" description: | - ## Blueprint Version: `1.03.00` + ## Blueprint Version: `1.08.00` ## Description - A button on the HASP will toggle through as many as 10 selected automations. This allows the user to assign multiple blueprints to the same button on the HASPone device, and to cycle between them by pressing the selected button. + A button on the HASPone will toggle through as many as 10 selected automations. This allows the user to assign multiple blueprints to the same button on the HASPone device, and to cycle between them by pressing the selected button. Optionally, a timeout can be set to cycle back to a "default" automation after a specified interval, or to continuously cycle through selected automations. @@ -20,9 +20,9 @@ blueprint: ![Preview](https://raw.githubusercontent.com/HASwitchPlate/HASPone/main/images/hasp_Cycle_Automations.gif) - ## HASP Page and Button Reference + ## HASPone Page and Button Reference - The images below show each available HASP page along with the layout of available button objects. + The images below show each available HASPone page along with the layout of available button objects.
@@ -43,16 +43,16 @@ blueprint: domain: automation input: haspdevice: - name: "HASP Device" - description: "Select the HASP device" + name: "HASPone Device" + description: "Select the HASPone device" selector: device: integration: mqtt manufacturer: "HASwitchPlate" model: "HASPone v1.0.0" hasppage: - name: "HASP Page" - description: "Select the HASP page (1-11) for the button to be cycled. Refer to the HASP Page and Button reference above." + name: "HASPone Page" + description: "Select the HASPone page (1-11) for the button to be cycled. Refer to the HASPone Page and Button reference above." default: 1 selector: number: @@ -61,8 +61,8 @@ blueprint: mode: slider unit_of_measurement: page haspbutton: - name: "HASP Button" - description: "Select the HASP button (4-15) to be cycled. Refer to the HASP Page and Button reference above." + name: "HASPone Button" + description: "Select the HASPone button (4-15) to be cycled. Refer to the HASPone Page and Button reference above." default: 4 selector: number: @@ -166,7 +166,7 @@ variables: haspdevice: !input haspdevice haspname: >- {%- for entity in device_entities(haspdevice) -%} - {%- if entity|regex_search("^sensor\.") -%} + {%- 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 -%} @@ -232,7 +232,7 @@ trigger_variables: haspdevice: !input haspdevice haspname: >- {%- for entity in device_entities(haspdevice) -%} - {%- if entity|regex_search("^sensor\.") -%} + {%- 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 -%} @@ -249,13 +249,13 @@ trigger_variables: buttonjsonpayload: '{"event_type":"button_short_press","event":"{{haspobject}}","value":"ON"}' trigger: - - platform: template + - trigger: template value_template: "{{ is_state(haspsensor, 'ON') }}" - - platform: homeassistant + - trigger: homeassistant event: start - - platform: event + - trigger: event event_type: automation_reloaded - - platform: mqtt + - trigger: mqtt topic: "{{jsontopic}}" payload: "{{buttonjsonpayload}}" diff --git a/Home_Assistant/blueprints/hasp_Dim_Screen_on_Idle.yaml b/Home_Assistant/blueprints/hasp_Dim_Screen_on_Idle.yaml index b7271e5..be6a261 100755 --- a/Home_Assistant/blueprints/hasp_Dim_Screen_on_Idle.yaml +++ b/Home_Assistant/blueprints/hasp_Dim_Screen_on_Idle.yaml @@ -1,8 +1,8 @@ blueprint: - name: "HASP dim the display screen after a specified period of inactivity" + name: "HASPone dim the display screen after a specified period of inactivity" description: | - ## Blueprint Version: `1.03.00` + ## Blueprint Version: `1.08.00` # Description @@ -11,8 +11,8 @@ blueprint: domain: automation input: haspdevice: - name: "HASP Device" - description: "Select the HASP device" + name: "HASPone Device" + description: "Select the HASPone device" selector: device: integration: mqtt @@ -57,7 +57,7 @@ variables: haspdevice: !input haspdevice haspname: >- {%- for entity in device_entities(haspdevice) -%} - {%- if entity|regex_search("^sensor\.") -%} + {%- 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 -%} @@ -70,7 +70,7 @@ trigger_variables: haspdevice: !input haspdevice haspname: >- {%- for entity in device_entities(haspdevice) -%} - {%- if entity|regex_search("^sensor\.") -%} + {%- 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 -%} @@ -83,7 +83,7 @@ trigger_variables: jsontopic: '{{ "hasp/" ~ haspname ~ "/state/json" }}' trigger: - - platform: mqtt + - trigger: mqtt topic: "{{jsontopic}}" condition: diff --git a/Home_Assistant/blueprints/hasp_Dim_Screen_with_Sun.yaml b/Home_Assistant/blueprints/hasp_Dim_Screen_with_Sun.yaml index 3f8827d..9f06d9b 100644 --- a/Home_Assistant/blueprints/hasp_Dim_Screen_with_Sun.yaml +++ b/Home_Assistant/blueprints/hasp_Dim_Screen_with_Sun.yaml @@ -1,8 +1,8 @@ blueprint: - name: "HASP dims the backlight with the sun" + name: "HASPone dims the backlight with the sun" description: | - ## Blueprint Version: `1.03.00` + ## Blueprint Version: `1.08.00` # Description @@ -11,8 +11,8 @@ blueprint: domain: automation input: haspdevice: - name: "HASP Device" - description: "Select the HASP device" + name: "HASPone Device" + description: "Select the HASPone device" selector: device: integration: mqtt @@ -68,7 +68,7 @@ variables: haspdevice: !input haspdevice haspname: >- {%- for entity in device_entities(haspdevice) -%} - {%- if entity|regex_search("^sensor\.") -%} + {%- 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 -%} @@ -85,7 +85,7 @@ variables: lightcommandtopic: "{{ 'hasp/' ~ haspname ~ '/brightness/set' }}" trigger: - - platform: state + - trigger: state entity_id: sun.sun attribute: elevation diff --git a/Home_Assistant/blueprints/hasp_Display_Alarm_Control_page7.yaml b/Home_Assistant/blueprints/hasp_Display_Alarm_Control_page7.yaml index e531bbc..e40a8b0 100644 --- a/Home_Assistant/blueprints/hasp_Display_Alarm_Control_page7.yaml +++ b/Home_Assistant/blueprints/hasp_Display_Alarm_Control_page7.yaml @@ -1,8 +1,8 @@ blueprint: - name: "HASP p[7].b[all] displays an alarm control panel" + name: "HASPone p[7].b[all] displays an alarm control panel" description: | - ## Blueprint Version: `1.03.00` + ## Blueprint Version: `1.08.00` # Description @@ -10,7 +10,7 @@ blueprint: ![Preview](https://raw.githubusercontent.com/HASwitchPlate/HASPone/main/images/hasp_Display_Alarm_Control_page7.png) - ## HASP Page and Button Reference + ## HASPone Page and Button Reference
@@ -26,8 +26,8 @@ blueprint: domain: automation input: haspdevice: - name: "HASP Device" - description: "Select the HASP device" + name: "HASPone Device" + description: "Select the HASPone device" selector: device: integration: mqtt @@ -87,7 +87,7 @@ variables: haspdevice: !input haspdevice haspname: >- {%- for entity in device_entities(haspdevice) -%} - {%- if entity|regex_search("^sensor\.") -%} + {%- 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 -%} @@ -105,9 +105,7 @@ variables: {{ entity }} {%- endif -%} {%- endfor -%} - haspIP: '{{state_attr(haspsensor, "haspIP")}}' 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")}}' @@ -211,7 +209,7 @@ trigger_variables: haspdevice: !input haspdevice haspname: >- {%- for entity in device_entities(haspdevice) -%} - {%- if entity|regex_search("^sensor\.") -%} + {%- 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 -%} @@ -228,21 +226,21 @@ trigger_variables: unselectedbgtopic: '{{ "hasp/" ~ haspname ~ "/light/unselectedbackgroundcolor/rgb" }}' trigger: - - platform: state + - trigger: state entity_id: !input alarmpanel - - platform: homeassistant + - trigger: homeassistant event: start - - platform: template + - trigger: template value_template: "{{ is_state(haspsensor, 'ON') }}" - - platform: mqtt + - trigger: mqtt topic: "{{jsontopic}}" - - platform: mqtt + - trigger: mqtt topic: "{{selectedfgtopic}}" - - platform: mqtt + - trigger: mqtt topic: "{{selectedbgtopic}}" - - platform: mqtt + - trigger: mqtt topic: "{{unselectedfgtopic}}" - - platform: mqtt + - trigger: mqtt topic: "{{unselectedbgtopic}}" condition: @@ -252,7 +250,7 @@ condition: action: - choose: ######################################################################### - # RUN ACTIONS or Home Assistant Startup or HASP Connect + # RUN ACTIONS or Home Assistant Startup or HASPone Connect - conditions: - condition: template value_template: >- diff --git a/Home_Assistant/blueprints/hasp_Display_Calendar_with_Icon.yaml b/Home_Assistant/blueprints/hasp_Display_Calendar_with_Icon.yaml index e7f0b2e..eabb3bc 100644 --- a/Home_Assistant/blueprints/hasp_Display_Calendar_with_Icon.yaml +++ b/Home_Assistant/blueprints/hasp_Display_Calendar_with_Icon.yaml @@ -1,16 +1,16 @@ blueprint: - name: "HASP p[x].b[y] displays the month + date with a calendar icon" + name: "HASPone p[x].b[y] displays the month + date with a calendar icon" description: | - ## Blueprint Version: `1.03.00` + ## Blueprint Version: `1.08.00` # Description - A HASP button displays month + date on the right with a calendar icon on the left. + A HASPone button displays month + date on the right with a calendar icon on the left. ![Preview](https://raw.githubusercontent.com/HASwitchPlate/HASPone/main/images/hasp_Display_Calendar_with_Icon.png) - ## HASP Page and Button Reference + ## HASPone Page and Button Reference
@@ -22,19 +22,41 @@ blueprint:
+ ## 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" + name: "HASPone Device" + description: "Select the HASPone device" selector: device: integration: mqtt manufacturer: "HASwitchPlate" model: "HASPone v1.0.0" hasppage: - name: "HASP Page" - description: "Select the HASP page (1-3) for the calendar. Refer to the HASP Page and Button reference above." + name: "HASPone Page" + description: "Select the HASPone page (1-3) for the calendar. Refer to the HASPone Page and Button reference above." default: 1 selector: number: @@ -43,8 +65,8 @@ blueprint: mode: slider unit_of_measurement: page haspbutton: - name: "HASP Button" - description: "Select the HASP button (4-7) for the calendar. Refer to the HASP Page and Button reference above." + name: "HASPone Button" + description: "Select the HASPone button (4-7) for the calendar. Refer to the HASPone Page and Button reference above." default: 4 selector: number: @@ -52,6 +74,51 @@ blueprint: max: 7 mode: slider unit_of_measurement: button + 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 + icon_fgcolor: + name: "Icon foreground color" + description: 'Icon 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 mode: parallel max_exceeded: silent @@ -60,20 +127,21 @@ variables: haspdevice: !input haspdevice haspname: >- {%- for entity in device_entities(haspdevice) -%} - {%- if entity|regex_search("^sensor\.") -%} + {%- 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 + selected_fgcolor: !input selected_fgcolor + selected_bgcolor: !input selected_bgcolor + unselected_fgcolor: !input unselected_fgcolor + unselected_bgcolor: !input unselected_bgcolor + icon_fgcolor: !input icon_fgcolor haspobject: '{{ "p[" ~ hasppage ~ "].b[" ~ haspbutton ~ "]" }}' commandtopic: '{{ "hasp/" ~ haspname ~ "/command/" ~ haspobject }}' jsontopic: '{{ "hasp/" ~ haspname ~ "/state/json" }}' 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" }}' activepage: >- {%- set activepage = namespace() -%} {%- for entity in device_entities(haspdevice) -%} @@ -82,54 +150,90 @@ variables: {%- endif -%} {%- endfor -%} {{ states(activepage.entity) | int(default=-1) }} + 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: >- - {%- 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 }} + {%- 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: >- - {%- 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 }} + {%- 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: >- - {%- 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 }} + {%- 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: >- - {%- 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 }} + {%- 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 -%} + iconcolor: >- + {%- if (icon_fgcolor|int) >= 0 -%} + {{ icon_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 -%} text: '{{(now().strftime("%b "))~now().day}}' font: 8 ypos: "{{(haspbutton|int - 4) * 67 + 2}}" # calculate the top pixel position based on the selected button @@ -143,7 +247,7 @@ trigger_variables: haspdevice: !input haspdevice haspname: >- {%- for entity in device_entities(haspdevice) -%} - {%- if entity|regex_search("^sensor\.") -%} + {%- 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 -%} @@ -160,21 +264,21 @@ trigger_variables: unselectedbgtopic: '{{ "hasp/" ~ haspname ~ "/light/unselectedbackgroundcolor/rgb" }}' trigger: - - platform: time + - trigger: time at: "00:00:00" - - platform: homeassistant + - trigger: homeassistant event: start - - platform: template + - trigger: template value_template: "{{ is_state(haspsensor, 'ON') }}" - - platform: mqtt + - trigger: mqtt topic: "{{jsontopic}}" - - platform: mqtt + - trigger: mqtt topic: "{{selectedfgtopic}}" - - platform: mqtt + - trigger: mqtt topic: "{{selectedbgtopic}}" - - platform: mqtt + - trigger: mqtt topic: "{{unselectedfgtopic}}" - - platform: mqtt + - trigger: mqtt topic: "{{unselectedbgtopic}}" condition: @@ -184,7 +288,7 @@ condition: action: - choose: ######################################################################### - # RUN ACTIONS or Home Assistant Startup or HASP Connect + # RUN ACTIONS or Home Assistant Startup or HASPone Connect # Apply styles, place text, and then place icon if our target page is currently active - conditions: - condition: template @@ -212,7 +316,7 @@ action: "{{haspobject}}.bco2={{unselectedbg}}", "{{haspobject}}.txt=\"{{text}} \"" {%- if activepage|int == hasppage|int -%} - ,"delay=1","xstr {{xpos}},{{ypos}},{{iconwidth}},{{iconheight}},{{iconfont}},{{selectedfg}},0,1,1,3,\"{{icon}}\"" + ,"delay=1","xstr {{xpos}},{{ypos}},{{iconwidth}},{{iconheight}},{{iconfont}},{{iconcolor}},0,1,1,3,\"{{icon}}\"" {%- endif -%}] ######################################################################### # Update the calendar text every day at midnight. If the selected page is currently active, also place the icon. @@ -226,7 +330,7 @@ action: payload: >- ["{{haspobject}}.txt=\"{{text}} \"" {%- if activepage|int == hasppage|int -%} - ,"delay=1","xstr {{xpos}},{{ypos}},{{iconwidth}},{{iconheight}},{{iconfont}},{{selectedfg}},0,1,1,3,\"{{icon}}\"" + ,"delay=1","xstr {{xpos}},{{ypos}},{{iconwidth}},{{iconheight}},{{iconfont}},{{iconcolor}},0,1,1,3,\"{{icon}}\"" {%- endif -%}] ######################################################################### # Catch MQTT events @@ -251,7 +355,7 @@ action: - service: mqtt.publish data: topic: "{{jsoncommandtopic}}" - payload: '["delay=1","xstr {{xpos}},{{ypos}},{{iconwidth}},{{iconheight}},{{iconfont}},{{selectedfg}},0,1,1,3,\"{{icon}}\""]' + payload: '["delay=1","xstr {{xpos}},{{ypos}},{{iconwidth}},{{iconheight}},{{iconfont}},{{iconcolor}},0,1,1,3,\"{{icon}}\""]' - conditions: # Page changed to our page, so place the icon on the screen. - condition: template value_template: '{{ (trigger.topic == jsontopic ) and (trigger.payload_json.event == "page" ) and (trigger.payload_json.value == hasppage|int) }}' @@ -259,13 +363,13 @@ action: - service: mqtt.publish data: topic: "{{jsoncommandtopic}}" - payload: '["delay=1","xstr {{xpos}},{{ypos}},{{iconwidth}},{{iconheight}},{{iconfont}},{{selectedfg}},0,1,1,3,\"{{icon}}\""]' + payload: '["delay=1","xstr {{xpos}},{{ypos}},{{iconwidth}},{{iconheight}},{{iconfont}},{{iconcolor}},0,1,1,3,\"{{icon}}\""]' ######################################################################### # 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 }}" + value_template: "{{ (trigger.topic == selectedfgtopic) and ((selected_fgcolor|int) == -1) }}" sequence: - service: mqtt.publish data: @@ -277,12 +381,12 @@ action: - service: mqtt.publish data: topic: "{{jsoncommandtopic}}" - payload: '["delay=1","xstr {{xpos}},{{ypos}},{{iconwidth}},{{iconheight}},{{iconfont}},{{trigger.payload}},0,1,1,3,\"{{icon}}\""]' + payload: '["delay=1","xstr {{xpos}},{{ypos}},{{iconwidth}},{{iconheight}},{{iconfont}},{{iconcolor}},0,1,1,3,\"{{icon}}\""]' ######################################################################### # Theme: Apply selected background color on change - conditions: - condition: template - value_template: "{{ trigger.topic == selectedbgtopic }}" + value_template: "{{ (trigger.topic == selectedbgtopic) and ((selected_bgcolor|int) == -1) }}" sequence: - service: mqtt.publish data: @@ -294,12 +398,12 @@ action: - service: mqtt.publish data: topic: "{{jsoncommandtopic}}" - payload: '["delay=1","xstr {{xpos}},{{ypos}},{{iconwidth}},{{iconheight}},{{iconfont}},{{selectedfg}},0,1,1,3,\"{{icon}}\""]' + payload: '["delay=1","xstr {{xpos}},{{ypos}},{{iconwidth}},{{iconheight}},{{iconfont}},{{iconcolor}},0,1,1,3,\"{{icon}}\""]' ######################################################################### # Theme: Apply unselected foreground color on change - conditions: - condition: template - value_template: "{{ trigger.topic == unselectedfgtopic }}" + value_template: "{{ (trigger.topic == unselectedfgtopic) and ((unselected_fgcolor|int) == -1) }}" sequence: - service: mqtt.publish data: @@ -311,12 +415,12 @@ action: - service: mqtt.publish data: topic: "{{jsoncommandtopic}}" - payload: '["delay=1","xstr {{xpos}},{{ypos}},{{iconwidth}},{{iconheight}},{{iconfont}},{{selectedfg}},0,1,1,3,\"{{icon}}\""]' + payload: '["delay=1","xstr {{xpos}},{{ypos}},{{iconwidth}},{{iconheight}},{{iconfont}},{{iconcolor}},0,1,1,3,\"{{icon}}\""]' ######################################################################### # Theme: Apply unselected background color on change - conditions: - condition: template - value_template: "{{ trigger.topic == unselectedbgtopic }}" + value_template: "{{ (trigger.topic == unselectedbgtopic) and ((unselected_bgcolor|int) == -1) }}" sequence: - service: mqtt.publish data: @@ -328,4 +432,4 @@ action: - service: mqtt.publish data: topic: "{{jsoncommandtopic}}" - payload: '["delay=1","xstr {{xpos}},{{ypos}},{{iconwidth}},{{iconheight}},{{iconfont}},{{selectedfg}},0,1,1,3,\"{{icon}}\""]' + payload: '["delay=1","xstr {{xpos}},{{ypos}},{{iconwidth}},{{iconheight}},{{iconfont}},{{iconcolor}},0,1,1,3,\"{{icon}}\""]' diff --git a/Home_Assistant/blueprints/hasp_Display_Clock.yaml b/Home_Assistant/blueprints/hasp_Display_Clock.yaml index 6d4f57c..a7d8237 100644 --- a/Home_Assistant/blueprints/hasp_Display_Clock.yaml +++ b/Home_Assistant/blueprints/hasp_Display_Clock.yaml @@ -1,18 +1,18 @@ blueprint: - name: "HASP p[x].b[y] displays a clock" + name: "HASPone p[x].b[y] displays a clock" description: | - ## Blueprint Version: `1.03.00` + ## Blueprint Version: `1.08.00` # Description - A HASP button displays a clock with configurable text options. + A HASPone button displays a clock with configurable text options. ![Preview](https://raw.githubusercontent.com/HASwitchPlate/HASPone/main/images/hasp_Display_Clock.png) - ## HASP Page and Button Reference + ## HASPone Page and Button Reference - The images below show each available HASP page along with the layout of available button objects. + The images below show each available HASPone page along with the layout of available button objects.
@@ -30,11 +30,11 @@ blueprint:
- ## HASP Font Reference + ## HASPone 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 + 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 | | :--- | :---------------- | :-------------------| :--------------- | @@ -56,23 +56,23 @@ blueprint: ### 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) + ![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)
domain: automation input: haspdevice: - name: "HASP Device" - description: "Select the HASP device" + name: "HASPone Device" + description: "Select the HASPone device" selector: device: integration: mqtt manufacturer: "HASwitchPlate" model: "HASPone v1.0.0" hasppage: - name: "HASP Page" - description: "Select the HASP page (1-11) for the clock. Refer to the HASP Page and Button reference above." + name: "HASPone Page" + description: "Select the HASPone page (1-11) for the clock. Refer to the HASPone Page and Button reference above." default: 1 selector: number: @@ -81,8 +81,8 @@ blueprint: mode: slider unit_of_measurement: page haspbutton: - name: "HASP Button" - description: "Select the HASP button (4-15) for the clock. Refer to the HASP Page and Button reference above." + name: "HASPone Button" + description: "Select the HASPone button (4-15) for the clock. Refer to the HASPone Page and Button reference above." default: 4 selector: number: @@ -92,7 +92,7 @@ blueprint: unit_of_measurement: button font_select: name: "Clock Font" - description: "Select the font for the clock. Refer to the HASP Font reference above." + description: "Select the font for the clock. Refer to the HASPone Font reference above." default: "8 - Noto Sans 64" selector: select: @@ -152,7 +152,7 @@ variables: haspdevice: !input haspdevice haspname: >- {%- for entity in device_entities(haspdevice) -%} - {%- if entity|regex_search("^sensor\.") -%} + {%- 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 -%} @@ -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 @@ -237,7 +237,7 @@ trigger_variables: haspdevice: !input haspdevice haspname: >- {%- for entity in device_entities(haspdevice) -%} - {%- if entity|regex_search("^sensor\.") -%} + {%- 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 -%} @@ -254,19 +254,19 @@ trigger_variables: unselectedbgtopic: '{{ "hasp/" ~ haspname ~ "/light/unselectedbackgroundcolor/rgb" }}' trigger: - - platform: time_pattern + - trigger: time_pattern seconds: 0 - - platform: template + - trigger: template value_template: "{{ is_state(haspsensor, 'ON') }}" - - platform: homeassistant + - trigger: homeassistant event: start - - platform: mqtt + - trigger: mqtt topic: "{{selectedfgtopic}}" - - platform: mqtt + - trigger: mqtt topic: "{{selectedbgtopic}}" - - platform: mqtt + - trigger: mqtt topic: "{{unselectedfgtopic}}" - - platform: mqtt + - trigger: mqtt topic: "{{unselectedbgtopic}}" condition: @@ -276,7 +276,7 @@ condition: action: - choose: ######################################################################### - # RUN ACTIONS or Home Assistant Startup or HASP Connect + # RUN ACTIONS or Home Assistant Startup or HASPone Connect # Display clock and apply text style - conditions: - condition: template diff --git a/Home_Assistant/blueprints/hasp_Display_Clock_with_Icon.yaml b/Home_Assistant/blueprints/hasp_Display_Clock_with_Icon.yaml index 1bd8dd4..067db5a 100644 --- a/Home_Assistant/blueprints/hasp_Display_Clock_with_Icon.yaml +++ b/Home_Assistant/blueprints/hasp_Display_Clock_with_Icon.yaml @@ -1,16 +1,16 @@ blueprint: - name: "HASP p[x].b[y] displays a clock with a clock icon" + name: "HASPone p[x].b[y] displays a clock with a clock icon" description: | - ## Blueprint Version: `1.03.00` + ## Blueprint Version: `1.08.00` # Description - A HASP button displays a clock on the right with a clock icon on the left. + A HASPone button displays a clock on the right with a clock icon on the left. ![Preview](https://raw.githubusercontent.com/HASwitchPlate/HASPone/main/images/hasp_Display_Clock_with_Icon.png) - ## HASP Page and Button reference + ## HASPone Page and Button reference
@@ -22,19 +22,41 @@ blueprint:
+ ## 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" + name: "HASPone Device" + description: "Select the HASPone device" selector: device: integration: mqtt manufacturer: "HASwitchPlate" model: "HASPone v1.0.0" hasppage: - name: "HASP Page" - description: "Select the HASP page (1-3) for the clock. Refer to the HASP Page and Button reference above." + name: "HASPone Page" + description: "Select the HASPone page (1-3) for the clock. Refer to the HASPone Page and Button reference above." default: 1 selector: number: @@ -43,8 +65,8 @@ blueprint: mode: slider unit_of_measurement: page haspbutton: - name: "HASP Button" - description: "Select the HASP button (4-7) for the clock. Refer to the HASP Page and Button reference above." + name: "HASPone Button" + description: "Select the HASPone button (4-7) for the clock. Refer to the HASPone Page and Button reference above." default: 4 selector: number: @@ -57,6 +79,51 @@ blueprint: default: false 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 + icon_fgcolor: + name: "Icon foreground color" + description: 'Icon 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 mode: parallel max_exceeded: silent @@ -65,13 +132,18 @@ variables: haspdevice: !input haspdevice haspname: >- {%- for entity in device_entities(haspdevice) -%} - {%- if entity|regex_search("^sensor\.") -%} + {%- 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 hour24: !input hour24 + selected_fgcolor: !input selected_fgcolor + selected_bgcolor: !input selected_bgcolor + unselected_fgcolor: !input unselected_fgcolor + unselected_bgcolor: !input unselected_bgcolor + icon_fgcolor: !input icon_fgcolor haspobject: '{{ "p[" ~ hasppage ~ "].b[" ~ haspbutton ~ "]" }}' commandtopic: '{{ "hasp/" ~ haspname ~ "/command/" ~ haspobject }}' jsontopic: '{{ "hasp/" ~ haspname ~ "/state/json" }}' @@ -89,53 +161,85 @@ variables: unselectedfgtopic: '{{ "hasp/" ~ haspname ~ "/light/unselectedforegroundcolor/rgb" }}' unselectedbgtopic: '{{ "hasp/" ~ haspname ~ "/light/unselectedbackgroundcolor/rgb" }}' selectedfg: >- - {%- 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 }} + {%- 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: >- - {%- 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 }} + {%- 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: >- - {%- 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 }} + {%- 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: >- - {%- 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 }} + {%- 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 -%} + iconcolor: >- + {%- if (icon_fgcolor|int) >= 0 -%} + {{ icon_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 -%} text: '{%- if hour24 == true -%}{%- set hourformat="%-H" -%}{%- else %}{%- set hourformat="%I" -%}{%- endif -%}{{(now().strftime(hourformat)|int)~now().strftime(":%M")}}' font: 10 ypos: "{{(haspbutton|int - 4) * 67 + 2}}" # calculate the top pixel position based on the selected button @@ -149,7 +253,7 @@ trigger_variables: haspdevice: !input haspdevice haspname: >- {%- for entity in device_entities(haspdevice) -%} - {%- if entity|regex_search("^sensor\.") -%} + {%- 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 -%} @@ -166,21 +270,21 @@ trigger_variables: unselectedbgtopic: '{{ "hasp/" ~ haspname ~ "/light/unselectedbackgroundcolor/rgb" }}' trigger: - - platform: time_pattern + - trigger: time_pattern seconds: 0 - - platform: homeassistant + - trigger: homeassistant event: start - - platform: template + - trigger: template value_template: "{{ is_state(haspsensor, 'ON') }}" - - platform: mqtt + - trigger: mqtt topic: "{{jsontopic}}" - - platform: mqtt + - trigger: mqtt topic: "{{selectedfgtopic}}" - - platform: mqtt + - trigger: mqtt topic: "{{selectedbgtopic}}" - - platform: mqtt + - trigger: mqtt topic: "{{unselectedfgtopic}}" - - platform: mqtt + - trigger: mqtt topic: "{{unselectedbgtopic}}" condition: @@ -190,7 +294,7 @@ condition: action: - choose: ######################################################################### - # RUN ACTIONS or Home Assistant Startup or HASP Connect + # RUN ACTIONS or Home Assistant Startup or HASPone Connect # Apply styles, place text, and then place icon if our target page is currently active - conditions: - condition: template @@ -218,7 +322,7 @@ action: "{{haspobject}}.bco2={{unselectedbg}}", "{{haspobject}}.txt=\"{{text}} \"" {%- if activepage|int == hasppage|int -%} - ,"delay=1","xstr {{xpos}},{{ypos}},{{iconwidth}},{{iconheight}},{{iconfont}},{{selectedfg}},0,1,1,3,\"{{icon}}\"" + ,"delay=1","xstr {{xpos}},{{ypos}},{{iconwidth}},{{iconheight}},{{iconfont}},{{iconcolor}},0,1,1,3,\"{{icon}}\"" {%- endif -%}] ######################################################################### # Every minute, update the clock text. If the selected page is currently active, also place the icon. @@ -229,7 +333,7 @@ action: - service: mqtt.publish data: topic: "{{jsoncommandtopic}}" - payload: '["{{haspobject}}.txt=\"{{text}} \""{%- if activepage|int == hasppage|int -%},"delay=1","xstr {{xpos}},{{ypos}},{{iconwidth}},{{iconheight}},{{iconfont}},{{selectedfg}},0,1,1,3,\"{{icon}}\""{%- endif -%}]' + payload: '["{{haspobject}}.txt=\"{{text}} \""{%- if activepage|int == hasppage|int -%},"delay=1","xstr {{xpos}},{{ypos}},{{iconwidth}},{{iconheight}},{{iconfont}},{{iconcolor}},0,1,1,3,\"{{icon}}\""{%- endif -%}]' ######################################################################### # Catch MQTT events @@ -254,7 +358,7 @@ action: - service: mqtt.publish data: topic: "{{jsoncommandtopic}}" - payload: '["delay=1","xstr {{xpos}},{{ypos}},{{iconwidth}},{{iconheight}},{{iconfont}},{{selectedfg}},0,1,1,3,\"{{icon}}\""]' + payload: '["delay=1","xstr {{xpos}},{{ypos}},{{iconwidth}},{{iconheight}},{{iconfont}},{{iconcolor}},0,1,1,3,\"{{icon}}\""]' - conditions: # Page changed to our page, so place the icon on the screen. - condition: template value_template: '{{ (trigger.topic == jsontopic ) and (trigger.payload_json.event == "page" ) and (trigger.payload_json.value == hasppage|int) }}' @@ -262,13 +366,13 @@ action: - service: mqtt.publish data: topic: "{{jsoncommandtopic}}" - payload: '["delay=1","xstr {{xpos}},{{ypos}},{{iconwidth}},{{iconheight}},{{iconfont}},{{selectedfg}},0,1,1,3,\"{{icon}}\""]' + payload: '["delay=1","xstr {{xpos}},{{ypos}},{{iconwidth}},{{iconheight}},{{iconfont}},{{iconcolor}},0,1,1,3,\"{{icon}}\""]' ######################################################################### # 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 }}" + value_template: "{{ (trigger.topic == selectedfgtopic) and ((selected_fgcolor|int) == -1) }}" sequence: - service: mqtt.publish data: @@ -280,12 +384,12 @@ action: - service: mqtt.publish data: topic: "{{jsoncommandtopic}}" - payload: '["delay=1","xstr {{xpos}},{{ypos}},{{iconwidth}},{{iconheight}},{{iconfont}},{{trigger.payload}},0,1,1,3,\"{{icon}}\""]' + payload: '["delay=1","xstr {{xpos}},{{ypos}},{{iconwidth}},{{iconheight}},{{iconfont}},{{iconcolor}},0,1,1,3,\"{{icon}}\""]' ######################################################################### # Theme: Apply selected background color on change - conditions: - condition: template - value_template: "{{ trigger.topic == selectedbgtopic }}" + value_template: "{{ (trigger.topic == selectedbgtopic) and ((selected_bgcolor|int) == -1) }}" sequence: - service: mqtt.publish data: @@ -297,12 +401,12 @@ action: - service: mqtt.publish data: topic: "{{jsoncommandtopic}}" - payload: '["delay=1","xstr {{xpos}},{{ypos}},{{iconwidth}},{{iconheight}},{{iconfont}},{{selectedfg}},0,1,1,3,\"{{icon}}\""]' + payload: '["delay=1","xstr {{xpos}},{{ypos}},{{iconwidth}},{{iconheight}},{{iconfont}},{{iconcolor}},0,1,1,3,\"{{icon}}\""]' ######################################################################### # Theme: Apply unselected foreground color on change - conditions: - condition: template - value_template: "{{ trigger.topic == unselectedfgtopic }}" + value_template: "{{ (trigger.topic == unselectedfgtopic) and ((unselected_fgcolor|int) == -1) }}" sequence: - service: mqtt.publish data: @@ -314,12 +418,12 @@ action: - service: mqtt.publish data: topic: "{{jsoncommandtopic}}" - payload: '["delay=1","xstr {{xpos}},{{ypos}},{{iconwidth}},{{iconheight}},{{iconfont}},{{selectedfg}},0,1,1,3,\"{{icon}}\""]' + payload: '["delay=1","xstr {{xpos}},{{ypos}},{{iconwidth}},{{iconheight}},{{iconfont}},{{iconcolor}},0,1,1,3,\"{{icon}}\""]' ######################################################################### # Theme: Apply unselected background color on change - conditions: - condition: template - value_template: "{{ trigger.topic == unselectedbgtopic }}" + value_template: "{{ (trigger.topic == unselectedbgtopic) and ((unselected_bgcolor|int) == -1) }}" sequence: - service: mqtt.publish data: @@ -331,4 +435,4 @@ action: - service: mqtt.publish data: topic: "{{jsoncommandtopic}}" - payload: '["delay=1","xstr {{xpos}},{{ypos}},{{iconwidth}},{{iconheight}},{{iconfont}},{{selectedfg}},0,1,1,3,\"{{icon}}\""]' + payload: '["delay=1","xstr {{xpos}},{{ypos}},{{iconwidth}},{{iconheight}},{{iconfont}},{{iconcolor}},0,1,1,3,\"{{icon}}\""]' 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..543df82 --- /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.08.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: + - trigger: homeassistant + event: start + - trigger: template + value_template: "{{ is_state(haspsensor, 'ON') }}" + - trigger: mqtt + topic: "{{jsontopic}}" + - trigger: mqtt + topic: "{{selectedfgtopic}}" + - trigger: mqtt + topic: "{{selectedbgtopic}}" + - trigger: mqtt + topic: "{{unselectedfgtopic}}" + - trigger: mqtt + topic: "{{unselectedbgtopic}}" + - trigger: 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 13a7d51..3c32494 100644 --- a/Home_Assistant/blueprints/hasp_Display_Dimmer_with_Icon.yaml +++ b/Home_Assistant/blueprints/hasp_Display_Dimmer_with_Icon.yaml @@ -1,16 +1,16 @@ blueprint: - name: "HASP p[x].b[y] displays a dimmer with a toggle on/off icon" + name: "HASPone p[x].b[y] displays a dimmer with a toggle on/off icon" description: | - ## Blueprint Version: `1.03.00` + ## Blueprint Version: `1.08.00` # Description - A HASP button displays a dimmer control on page 4 and 5 with a toggle on/off icon to the left. + A HASPone button displays a dimmer control on page 4 and 5 with a toggle on/off icon to the left. ![Preview](https://raw.githubusercontent.com/HASwitchPlate/HASPone/main/images/hasp_Display_Dimmer_with_Icon.png) - ## HASP Page and Button reference + ## HASPone Page and Button reference
@@ -25,16 +25,16 @@ blueprint: domain: automation input: haspdevice: - name: "HASP Device" - description: "Select the HASP device" + name: "HASPone Device" + description: "Select the HASPone device" selector: device: integration: mqtt manufacturer: "HASwitchPlate" model: "HASPone v1.0.0" hasppage: - name: "HASP Page" - description: "Select the HASP page (4 or 5) for the dimmer. Refer to the HASP Page and Button reference above." + name: "HASPone Page" + description: "Select the HASPone page (4 or 5) for the dimmer. Refer to the HASPone Page and Button reference above." default: 4 selector: number: @@ -43,8 +43,8 @@ blueprint: mode: slider unit_of_measurement: page haspbutton: - name: "HASP Button" - description: "Select the HASP button (7-9) for the dimmer. Refer to the HASP Page and Button reference above." + name: "HASPone Button" + description: "Select the HASPone button (7-9) for the dimmer. Refer to the HASPone Page and Button reference above." default: 7 selector: number: @@ -59,20 +59,20 @@ blueprint: entity: domain: light text_on: - name: "HASP Button Text On" + name: "HASPone Button Text On" description: "Enter text to appear on the button above the dimmer when the selected device is ON." default: "Dimmer" selector: text: text_off: - name: "HASP Button Text Off" + name: "HASPone Button Text Off" description: "Enter text to appear on the button when the selected device is OFF. The default value of {{text_on}} will leave the text unchanged when the device turns on/off" default: "{{text_on}}" selector: text: font_select: - name: "HASP Button Font" - description: "Select the text font for this button label. Refer to the HASP Font reference above." + name: "HASPone Button Font" + description: "Select the text font for this button label. Refer to the HASPone Font reference above." default: "6 - Noto Sans 32" selector: select: @@ -134,7 +134,7 @@ variables: haspdevice: !input haspdevice haspname: >- {%- for entity in device_entities(haspdevice) -%} - {%- if entity|regex_search("^sensor\.") -%} + {%- 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 -%} @@ -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 @@ -235,7 +235,7 @@ trigger_variables: haspdevice: !input haspdevice haspname: >- {%- for entity in device_entities(haspdevice) -%} - {%- if entity|regex_search("^sensor\.") -%} + {%- 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 -%} @@ -251,22 +251,22 @@ trigger_variables: unselectedfgtopic: '{{ "hasp/" ~ haspname ~ "/light/unselectedforegroundcolor/rgb" }}' unselectedbgtopic: '{{ "hasp/" ~ haspname ~ "/light/unselectedbackgroundcolor/rgb" }}' -trigger: - - platform: state +triggers: + - trigger: state entity_id: !input dimmer - - platform: template + - trigger: template value_template: "{{ is_state(haspsensor, 'ON') }}" - - platform: homeassistant + - trigger: homeassistant event: start - - platform: mqtt + - trigger: mqtt topic: "{{jsontopic}}" - - platform: mqtt + - trigger: mqtt topic: "{{selectedfgtopic}}" - - platform: mqtt + - trigger: mqtt topic: "{{selectedbgtopic}}" - - platform: mqtt + - trigger: mqtt topic: "{{unselectedfgtopic}}" - - platform: mqtt + - trigger: mqtt topic: "{{unselectedbgtopic}}" condition: @@ -276,7 +276,7 @@ condition: action: - choose: ######################################################################### - # RUN ACTIONS or Home Assistant Startup or HASP Connect + # RUN ACTIONS or Home Assistant Startup or HASPone Connect # Apply styles, place text, and then place icon if our target page is currently active - conditions: - condition: template @@ -360,7 +360,7 @@ action: - service: homeassistant.toggle entity_id: !input dimmer ######################################################################### - # Primary function: Set the dimmer value when the HASP slider has moved + # Primary function: Set the dimmer value when the HASPone slider has moved - conditions: - condition: template value_template: '{{ (trigger.topic == jsontopic) and (trigger.payload_json.event == dimmerobject ~ ".val") }}' @@ -415,7 +415,7 @@ action: - service: mqtt.publish data: topic: "{{jsoncommandtopic}}" - payload: '["delay=1","xstr {{xpos}},{{ypos}},{{iconwidth}},{{iconheight}},{{iconfont}},{{fgcolor}},0,1,1,3,\"{{icon}}\"","delay=1","vis {{dimmerbutton}},1"]' + payload: '["delay=1","xstr {{xpos}},{{ypos}},{{iconwidth}},{{iconheight}},{{iconfont}},{{selectedfg}},0,1,1,3,\"{{icon}}\"","delay=1","vis {{dimmerbutton}},1"]' ######################################################################### # Theme: Apply unselected foreground color on change - conditions: @@ -432,7 +432,7 @@ action: - service: mqtt.publish data: topic: "{{jsoncommandtopic}}" - payload: '["delay=1","xstr {{xpos}},{{ypos}},{{iconwidth}},{{iconheight}},{{iconfont}},{{fgcolor}},0,1,1,3,\"{{icon}}\"","delay=1","vis {{dimmerbutton}},1"]' + payload: '["delay=1","xstr {{xpos}},{{ypos}},{{iconwidth}},{{iconheight}},{{iconfont}},{{selectedfg}},0,1,1,3,\"{{icon}}\"","delay=1","vis {{dimmerbutton}},1"]' ######################################################################### # Theme: Apply unselected background color on change - conditions: @@ -449,4 +449,4 @@ action: - service: mqtt.publish data: topic: "{{jsoncommandtopic}}" - payload: '["delay=1","xstr {{xpos}},{{ypos}},{{iconwidth}},{{iconheight}},{{iconfont}},{{fgcolor}},0,1,1,3,\"{{icon}}\"","delay=1","vis {{dimmerbutton}},1"]' + payload: '["delay=1","xstr {{xpos}},{{ypos}},{{iconwidth}},{{iconheight}},{{iconfont}},{{selectedfg}},0,1,1,3,\"{{icon}}\"","delay=1","vis {{dimmerbutton}},1"]' 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 00e6193..0847c21 100644 --- a/Home_Assistant/blueprints/hasp_Display_Entity_State_or_Attribute.yaml +++ b/Home_Assistant/blueprints/hasp_Display_Entity_State_or_Attribute.yaml @@ -1,12 +1,12 @@ blueprint: - name: "HASP p[x].b[y] displays the state or attribute value of an entity" + name: "HASPone p[x].b[y] displays the state or attribute value of an entity" description: | - ## Blueprint Version: `1.03.00` + ## Blueprint Version: `1.08.00` # Description - A HASP button displays the state or attribute value of an entity + A HASPone button displays the state or attribute value of an entity ![Preview](https://raw.githubusercontent.com/HASwitchPlate/HASPone/main/images/hasp_Display_Entity_State_or_Attribute.png) @@ -32,9 +32,9 @@ blueprint:
- ## HASP Page and Button Reference + ## HASPone Page and Button Reference - The images below show each available HASP page along with the layout of available button objects. + The images below show each available HASPone page along with the layout of available button objects.
@@ -52,11 +52,11 @@ blueprint:
- ## HASP Font Reference + ## HASPone 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 + 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 | | :--- | :---------------- | :-------------------| :--------------- | @@ -78,23 +78,23 @@ blueprint: ### 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) + ![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)
domain: automation input: haspdevice: - name: "HASP Device" - description: "Select the HASP device" + name: "HASPone Device" + description: "Select the HASPone device" selector: device: integration: mqtt manufacturer: "HASwitchPlate" model: "HASPone v1.0.0" hasppage: - name: "HASP Page" - description: "Select the HASP page (1-11). Refer to the HASP Page and Button reference above." + name: "HASPone Page" + description: "Select the HASPone page (1-11). Refer to the HASPone Page and Button reference above." default: 1 selector: number: @@ -103,8 +103,8 @@ blueprint: mode: slider unit_of_measurement: page haspbutton: - name: "HASP Button" - description: "Select the HASP button (4-15) for the state display. Refer to the HASP Page and Button reference above." + name: "HASPone Button" + description: "Select the HASPone button (4-15) for the state display. Refer to the HASPone Page and Button reference above." default: 4 selector: number: @@ -136,7 +136,7 @@ blueprint: text: font_select: name: "Font" - description: "Select the font for the displayed text. Refer to the HASP Font Reference above." + description: "Select the font for the displayed text. Refer to the HASPone Font Reference above." default: "8 - Noto Sans 64" selector: select: @@ -184,6 +184,42 @@ blueprint: default: false 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 @@ -192,7 +228,7 @@ variables: haspdevice: !input haspdevice haspname: >- {%- for entity in device_entities(haspdevice) -%} - {%- if entity|regex_search("^sensor\.") -%} + {%- 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 -%} @@ -203,13 +239,17 @@ 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 + 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" }}' @@ -238,59 +278,75 @@ variables: unselectedfgtopic: '{{ "hasp/" ~ haspname ~ "/light/unselectedforegroundcolor/rgb" }}' unselectedbgtopic: '{{ "hasp/" ~ haspname ~ "/light/unselectedbackgroundcolor/rgb" }}' selectedfg: >- - {%- 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 }} + {%- 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: >- - {%- 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 }} + {%- 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: >- - {%- 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 }} + {%- 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: >- - {%- 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 }} + {%- 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\.") -%} + {%- 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 -%} @@ -307,21 +363,21 @@ trigger_variables: unselectedbgtopic: '{{ "hasp/" ~ haspname ~ "/light/unselectedbackgroundcolor/rgb" }}' trigger: - - platform: state + - trigger: state entity_id: !input selected_entity - - platform: template + - trigger: template value_template: "{{ is_state(haspsensor, 'ON') }}" - - platform: homeassistant + - trigger: homeassistant event: start - - platform: mqtt + - trigger: mqtt topic: "{{jsontopic}}" - - platform: mqtt + - trigger: mqtt topic: "{{selectedfgtopic}}" - - platform: mqtt + - trigger: mqtt topic: "{{selectedbgtopic}}" - - platform: mqtt + - trigger: mqtt topic: "{{unselectedfgtopic}}" - - platform: mqtt + - trigger: mqtt topic: "{{unselectedbgtopic}}" condition: @@ -331,11 +387,11 @@ condition: action: - choose: ######################################################################### - # RUN ACTIONS or Home Assistant Startup or HASP Connect + # RUN ACTIONS or Home Assistant Startup or HASPone Connect # Display attribute and apply text style - conditions: - condition: template - value_template: >- + value_template: >- {{- (trigger is not defined) or @@ -382,7 +438,7 @@ action: # Theme: Apply selected foreground color on change - conditions: - condition: template - value_template: "{{ trigger.topic == selectedfgtopic }}" + value_template: "{{ (trigger.topic == selectedfgtopic) and ((selected_fgcolor|int) == -1) }}" sequence: - service: mqtt.publish data: @@ -392,7 +448,7 @@ action: # Theme: Apply selected background color on change - conditions: - condition: template - value_template: "{{ trigger.topic == selectedbgtopic }}" + value_template: "{{ (trigger.topic == selectedbgtopic) and ((selected_bgcolor|int) == -1) }}" sequence: - service: mqtt.publish data: @@ -402,7 +458,7 @@ action: # Theme: Apply unselected foreground color on change - conditions: - condition: template - value_template: "{{ trigger.topic == unselectedfgtopic }}" + value_template: "{{ (trigger.topic == unselectedfgtopic) and ((unselected_fgcolor|int) == -1) }}" sequence: - service: mqtt.publish data: @@ -412,10 +468,9 @@ action: # Theme: Apply unselected background color on change - conditions: - condition: template - value_template: "{{ trigger.topic == unselectedbgtopic }}" + 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_Heatpump_Control_page6.yaml b/Home_Assistant/blueprints/hasp_Display_Heatpump_Control_page6.yaml index 693b23c..63b6876 100755 --- a/Home_Assistant/blueprints/hasp_Display_Heatpump_Control_page6.yaml +++ b/Home_Assistant/blueprints/hasp_Display_Heatpump_Control_page6.yaml @@ -1,8 +1,8 @@ blueprint: - name: "HASP p[6].b[all] Page 6 displays Heatpump controls" + name: "HASPone p[6].b[all] Page 6 displays Heatpump controls" description: | - ## Blueprint Version: `1.03.00` + ## Blueprint Version: `1.08.00` # Description @@ -10,7 +10,7 @@ blueprint: ![Preview](https://raw.githubusercontent.com/HASwitchPlate/HASPone/main/images/hasp_Display_Heatpump_Control_page9.png) - ## HASP Page and Button reference + ## HASPone Page and Button reference
@@ -48,8 +48,8 @@ blueprint: domain: automation input: haspdevice: - name: "HASP Device" - description: "Select the HASP device" + name: "HASPone Device" + description: "Select the HASPone device" selector: device: integration: mqtt @@ -121,7 +121,7 @@ max_exceeded: silent variables: haspname: >- {%- for entity in device_entities(haspdevice) -%} - {%- if entity|regex_search("^sensor\.") -%} + {%- 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 -%} @@ -266,7 +266,7 @@ trigger_variables: # heatpump: !input heatpump haspname: >- {%- for entity in device_entities(haspdevice) -%} - {%- if entity|regex_search("^sensor\.") -%} + {%- 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 -%} @@ -283,21 +283,21 @@ trigger_variables: unselectedbgtopic: '{{ "hasp/" ~ haspname ~ "/light/unselectedbackgroundcolor/rgb" }}' trigger: - - platform: state + - trigger: state entity_id: !input heatpump - - platform: template + - trigger: template value_template: "{{ is_state(haspsensor, 'ON') }}" - - platform: homeassistant + - trigger: homeassistant event: start - - platform: mqtt + - trigger: mqtt topic: "{{jsontopic}}" - - platform: mqtt + - trigger: mqtt topic: "{{selectedfgtopic}}" - - platform: mqtt + - trigger: mqtt topic: "{{selectedbgtopic}}" - - platform: mqtt + - trigger: mqtt topic: "{{unselectedfgtopic}}" - - platform: mqtt + - trigger: mqtt topic: "{{unselectedbgtopic}}" condition: @@ -307,7 +307,7 @@ condition: action: - choose: ######################################################################### - # RUN ACTIONS or Home Assistant Startup or HASP Connect + # RUN ACTIONS or Home Assistant Startup or HASPone Connect # Apply styles and place text - conditions: - condition: template @@ -378,7 +378,7 @@ action: sequence: - choose: ######################################################################### - # Set the volume value when the HASP slider has moved + # Set the volume value when the HASPone slider has moved # - conditions: # - condition: template # value_template: '{{ (trigger.topic == jsontopic) and (trigger.payload_json.event == volumeobject ~ ".val") }}' diff --git a/Home_Assistant/blueprints/hasp_Display_Heatpump_Control_page9.yaml b/Home_Assistant/blueprints/hasp_Display_Heatpump_Control_page9.yaml index bf22cf2..c129b18 100755 --- a/Home_Assistant/blueprints/hasp_Display_Heatpump_Control_page9.yaml +++ b/Home_Assistant/blueprints/hasp_Display_Heatpump_Control_page9.yaml @@ -1,8 +1,8 @@ blueprint: - name: "HASP p[9].b[all] Page 9 displays Heatpump controls" + name: "HASPone p[9].b[all] Page 9 displays Heatpump controls" description: | - ## Blueprint Version: `1.03.00` + ## Blueprint Version: `1.08.00` # Description @@ -10,7 +10,7 @@ blueprint: ![Preview](https://raw.githubusercontent.com/HASwitchPlate/HASPone/main/images/hasp_Display_Heatpump_Control_page9.png) - ## HASP Page and Button reference + ## HASPone Page and Button reference
@@ -48,8 +48,8 @@ blueprint: domain: automation input: haspdevice: - name: "HASP Device" - description: "Select the HASP device" + name: "HASPone Device" + description: "Select the HASPone device" selector: device: integration: mqtt @@ -121,7 +121,7 @@ max_exceeded: silent variables: haspname: >- {%- for entity in device_entities(haspdevice) -%} - {%- if entity|regex_search("^sensor\.") -%} + {%- 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 -%} @@ -267,7 +267,7 @@ trigger_variables: # heatpump: !input heatpump haspname: >- {%- for entity in device_entities(haspdevice) -%} - {%- if entity|regex_search("^sensor\.") -%} + {%- 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 -%} @@ -284,21 +284,21 @@ trigger_variables: unselectedbgtopic: '{{ "hasp/" ~ haspname ~ "/light/unselectedbackgroundcolor/rgb" }}' trigger: - - platform: state + - trigger: state entity_id: !input heatpump - - platform: template + - trigger: template value_template: "{{ is_state(haspsensor, 'ON') }}" - - platform: homeassistant + - trigger: homeassistant event: start - - platform: mqtt + - trigger: mqtt topic: "{{jsontopic}}" - - platform: mqtt + - trigger: mqtt topic: "{{selectedfgtopic}}" - - platform: mqtt + - trigger: mqtt topic: "{{selectedbgtopic}}" - - platform: mqtt + - trigger: mqtt topic: "{{unselectedfgtopic}}" - - platform: mqtt + - trigger: mqtt topic: "{{unselectedbgtopic}}" condition: @@ -308,7 +308,7 @@ condition: action: - choose: ######################################################################### - # RUN ACTIONS or Home Assistant Startup or HASP Connect + # RUN ACTIONS or Home Assistant Startup or HASPone Connect # Apply styles and place text - conditions: - condition: template @@ -381,7 +381,7 @@ action: sequence: - choose: ######################################################################### - # Set the volume value when the HASP slider has moved + # Set the volume value when the HASPone slider has moved # - conditions: # - condition: template # value_template: '{{ (trigger.topic == jsontopic) and (trigger.payload_json.event == volumeobject ~ ".val") }}' diff --git a/Home_Assistant/blueprints/hasp_Display_Media_Control_page8.yaml b/Home_Assistant/blueprints/hasp_Display_Media_Control_page8.yaml index 778f816..c8589b9 100644 --- a/Home_Assistant/blueprints/hasp_Display_Media_Control_page8.yaml +++ b/Home_Assistant/blueprints/hasp_Display_Media_Control_page8.yaml @@ -1,8 +1,8 @@ blueprint: - name: "HASP p[8].b[all] Page 8 displays media controls" + name: "HASPone p[8].b[all] Page 8 displays media controls" description: | - ## Blueprint Version: `1.03.00` + ## Blueprint Version: `1.08.00` # Description @@ -10,7 +10,7 @@ blueprint: ![Preview](https://raw.githubusercontent.com/HASwitchPlate/HASPone/main/images/hasp_Display_Media_Control_page8.png) - ## HASP Page and Button reference + ## HASPone Page and Button reference
@@ -26,8 +26,8 @@ blueprint: domain: automation input: haspdevice: - name: "HASP Device" - description: "Select the HASP device" + name: "HASPone Device" + description: "Select the HASPone device" selector: device: integration: mqtt @@ -58,7 +58,7 @@ max_exceeded: silent variables: haspname: >- {%- for entity in device_entities(haspdevice) -%} - {%- if entity|regex_search("^sensor\.") -%} + {%- 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 -%} @@ -145,7 +145,7 @@ trigger_variables: haspdevice: !input haspdevice haspname: >- {%- for entity in device_entities(haspdevice) -%} - {%- if entity|regex_search("^sensor\.") -%} + {%- 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 -%} @@ -162,21 +162,21 @@ trigger_variables: unselectedbgtopic: '{{ "hasp/" ~ haspname ~ "/light/unselectedbackgroundcolor/rgb" }}' trigger: - - platform: state + - trigger: state entity_id: !input mediaplayer - - platform: template + - trigger: template value_template: "{{ is_state(haspsensor, 'ON') }}" - - platform: homeassistant + - trigger: homeassistant event: start - - platform: mqtt + - trigger: mqtt topic: "{{jsontopic}}" - - platform: mqtt + - trigger: mqtt topic: "{{selectedfgtopic}}" - - platform: mqtt + - trigger: mqtt topic: "{{selectedbgtopic}}" - - platform: mqtt + - trigger: mqtt topic: "{{unselectedfgtopic}}" - - platform: mqtt + - trigger: mqtt topic: "{{unselectedbgtopic}}" condition: @@ -186,7 +186,7 @@ condition: action: - choose: ######################################################################### - # RUN ACTIONS or Home Assistant Startup or HASP Connect + # RUN ACTIONS or Home Assistant Startup or HASPone Connect # Apply styles and place text - conditions: - condition: template @@ -248,7 +248,7 @@ action: sequence: - choose: ######################################################################### - # Set the volume value when the HASP slider has moved + # Set the volume value when the HASPone slider has moved - conditions: - condition: template value_template: '{{ (trigger.topic == jsontopic) and (trigger.payload_json.event == volumeobject ~ ".val") }}' diff --git a/Home_Assistant/blueprints/hasp_Display_Template.yaml b/Home_Assistant/blueprints/hasp_Display_Template.yaml index 9acd867..4358e7d 100644 --- a/Home_Assistant/blueprints/hasp_Display_Template.yaml +++ b/Home_Assistant/blueprints/hasp_Display_Template.yaml @@ -1,12 +1,12 @@ blueprint: - name: "HASP p[x].b[y] displays the output of a template" + name: "HASPone p[x].b[y] displays the output of a template" description: | - ## Blueprint Version: `1.03.00` + ## Blueprint Version: `1.08.00` # Description - A button on the HASP will display the output of a template. The template is updated when the state of a selected entity updates. + A button on the HASPone will display the output of a template. The template is updated when the state of a selected entity updates. ![Preview](https://raw.githubusercontent.com/HASwitchPlate/HASPone/main/images/hasp_Display_Template.png) @@ -22,9 +22,9 @@ blueprint:
- ## HASP Page and Button Reference + ## HASPone Page and Button Reference - The images below show each available HASP page along with the layout of available button objects. + The images below show each available HASPone page along with the layout of available button objects.
@@ -42,11 +42,11 @@ blueprint:
- ## HASP Font Reference + ## HASPone 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 + 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 | | :--- | :---------------- | :-------------------| :--------------- | @@ -68,23 +68,45 @@ blueprint: ### 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) + ![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: "HASP Device" - description: "Select the HASP device" + name: "HASPone Device" + description: "Select the HASPone device" selector: device: integration: mqtt manufacturer: "HASwitchPlate" model: "HASPone v1.0.0" hasppage: - name: "HASP Page" - description: "Select the HASP page (1-11). Refer to the HASP Page and Button reference above." + name: "HASPone Page" + description: "Select the HASPone page (1-11). Refer to the HASPone Page and Button reference above." default: 1 selector: number: @@ -93,8 +115,8 @@ blueprint: mode: slider unit_of_measurement: page haspbutton: - name: "HASP Button" - description: "Select the HASP button (4-15) for the template display. Refer to the HASP Page and Button reference above." + name: "HASPone Button" + description: "Select the HASPone button (4-15) for the template display. Refer to the HASPone Page and Button reference above." default: 4 selector: number: @@ -112,10 +134,10 @@ 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 HASP Font Reference above." + description: "Select the font for the displayed text. Refer to the HASPone Font Reference above." default: "8 - Noto Sans 64" selector: select: @@ -157,6 +179,42 @@ blueprint: default: false 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 @@ -165,7 +223,7 @@ variables: haspdevice: !input haspdevice haspname: >- {%- for entity in device_entities(haspdevice) -%} - {%- if entity|regex_search("^sensor\.") -%} + {%- 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 -%} @@ -174,12 +232,16 @@ 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 + 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" }}' @@ -189,59 +251,75 @@ variables: unselectedfgtopic: '{{ "hasp/" ~ haspname ~ "/light/unselectedforegroundcolor/rgb" }}' unselectedbgtopic: '{{ "hasp/" ~ haspname ~ "/light/unselectedbackgroundcolor/rgb" }}' selectedfg: >- - {%- 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 }} + {%- 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: >- - {%- 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 }} + {%- 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: >- - {%- 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 }} + {%- 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: >- - {%- 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 }} + {%- 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\.") -%} + {%- 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 -%} @@ -258,21 +336,21 @@ trigger_variables: unselectedbgtopic: '{{ "hasp/" ~ haspname ~ "/light/unselectedbackgroundcolor/rgb" }}' trigger: - - platform: state + - trigger: state entity_id: !input trigger_entity - - platform: template + - trigger: template value_template: "{{ is_state(haspsensor, 'ON') }}" - - platform: homeassistant + - trigger: homeassistant event: start - - platform: mqtt + - trigger: mqtt topic: "{{jsontopic}}" - - platform: mqtt + - trigger: mqtt topic: "{{selectedfgtopic}}" - - platform: mqtt + - trigger: mqtt topic: "{{selectedbgtopic}}" - - platform: mqtt + - trigger: mqtt topic: "{{unselectedfgtopic}}" - - platform: mqtt + - trigger: mqtt topic: "{{unselectedbgtopic}}" condition: @@ -282,7 +360,7 @@ condition: action: - choose: ######################################################################### - # RUN ACTIONS or Home Assistant Startup or HASP Connect + # RUN ACTIONS or Home Assistant Startup or HASPone Connect # Display template and apply text style - conditions: - condition: template @@ -355,7 +433,7 @@ action: # Theme: Apply selected foreground color on change - conditions: - condition: template - value_template: "{{ trigger.topic == selectedfgtopic }}" + value_template: "{{ (trigger.topic == selectedfgtopic) and ((selected_fgcolor|int) == -1) }}" sequence: - service: mqtt.publish data: @@ -365,7 +443,7 @@ action: # Theme: Apply selected background color on change - conditions: - condition: template - value_template: "{{ trigger.topic == selectedbgtopic }}" + value_template: "{{ (trigger.topic == selectedbgtopic) and ((selected_bgcolor|int) == -1) }}" sequence: - service: mqtt.publish data: @@ -375,7 +453,7 @@ action: # Theme: Apply unselected foreground color on change - conditions: - condition: template - value_template: "{{ trigger.topic == unselectedfgtopic }}" + value_template: "{{ (trigger.topic == unselectedfgtopic) and ((unselected_fgcolor|int) == -1) }}" sequence: - service: mqtt.publish data: @@ -385,7 +463,7 @@ action: # Theme: Apply unselected background color on change - conditions: - condition: template - value_template: "{{ trigger.topic == unselectedbgtopic }}" + value_template: "{{ (trigger.topic == unselectedbgtopic) and ((unselected_bgcolor|int) == -1) }}" sequence: - service: mqtt.publish data: diff --git a/Home_Assistant/blueprints/hasp_Display_Text.yaml b/Home_Assistant/blueprints/hasp_Display_Text.yaml index 522cca1..ac362a2 100644 --- a/Home_Assistant/blueprints/hasp_Display_Text.yaml +++ b/Home_Assistant/blueprints/hasp_Display_Text.yaml @@ -1,19 +1,19 @@ blueprint: - name: "HASP p[x].b[y] displays text" + name: "HASPone p[x].b[y] displays text" description: | - ## Blueprint Version: `1.03.00` + ## Blueprint Version: `1.08.00` ## Description - A button on the HASP will display text. This can be useful when combined with other blueprints which perform an action, but don't apply a label to a button. + A button on the HASPone will display text. This can be useful when combined with other blueprints which perform an action, but don't apply a label to a button. Deploy both blueprints on the same button, and now you have a button that says things things and does things. ![Preview](https://raw.githubusercontent.com/HASwitchPlate/HASPone/main/images/hasp_Display_Text.png) - ## HASP Page and Button Reference + ## HASPone Page and Button Reference - The images below show each available HASP page along with the layout of available button objects. + The images below show each available HASPone page along with the layout of available button objects.
@@ -31,11 +31,11 @@ blueprint:
- ## HASP Font Reference + ## HASPone 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 + 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 | | :--- | :---------------- | :-------------------| :--------------- | @@ -57,7 +57,7 @@ blueprint: ### 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) + ![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)
@@ -86,16 +86,16 @@ blueprint: domain: automation input: haspdevice: - name: "HASP Device" - description: "Select the HASP device" + name: "HASPone Device" + description: "Select the HASPone device" selector: device: integration: mqtt manufacturer: "HASwitchPlate" model: "HASPone v1.0.0" hasppage: - name: "HASP Page" - description: "Select the HASP page (1-11). Refer to the HASP Page and Button reference above." + name: "HASPone Page" + description: "Select the HASPone page (1-11). Refer to the HASPone Page and Button reference above." default: 1 selector: number: @@ -104,8 +104,8 @@ blueprint: mode: slider unit_of_measurement: page haspbutton: - name: "HASP Button" - description: "Select the HASP button (4-15) for the text display. Refer to the HASP Page and Button reference above." + name: "HASPone Button" + description: "Select the HASPone button (4-15) for the text display. Refer to the HASPone Page and Button reference above." default: 4 selector: number: @@ -115,13 +115,13 @@ blueprint: unit_of_measurement: button text: name: "Text to display" - description: "Enter text to be displayed on the HASP." + description: "Enter text to be displayed on the HASPone." default: "Text to display" selector: text: font_select: name: "Font" - description: "Select the font for the displayed text. Refer to the HASP Font Reference above." + description: "Select the font for the displayed text. Refer to the HASPone Font Reference above." default: "8 - Noto Sans 64" selector: select: @@ -207,7 +207,7 @@ variables: haspdevice: !input haspdevice haspname: >- {%- for entity in device_entities(haspdevice) -%} - {%- if entity|regex_search("^sensor\.") -%} + {%- 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 -%} @@ -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 @@ -302,7 +302,7 @@ trigger_variables: haspdevice: !input haspdevice haspname: >- {%- for entity in device_entities(haspdevice) -%} - {%- if entity|regex_search("^sensor\.") -%} + {%- 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 -%} @@ -318,18 +318,18 @@ trigger_variables: unselectedfgtopic: '{{ "hasp/" ~ haspname ~ "/light/unselectedforegroundcolor/rgb" }}' unselectedbgtopic: '{{ "hasp/" ~ haspname ~ "/light/unselectedbackgroundcolor/rgb" }}' -trigger: - - platform: homeassistant +triggers: + - trigger: homeassistant event: start - - platform: template + - trigger: template value_template: "{{ is_state(haspsensor, 'ON') }}" - - platform: mqtt + - trigger: mqtt topic: "{{selectedfgtopic}}" - - platform: mqtt + - trigger: mqtt topic: "{{selectedbgtopic}}" - - platform: mqtt + - trigger: mqtt topic: "{{unselectedfgtopic}}" - - platform: mqtt + - trigger: mqtt topic: "{{unselectedbgtopic}}" condition: @@ -339,7 +339,7 @@ condition: action: - choose: ######################################################################### - # RUN ACTIONS or Home Assistant Startup or HASP Connect + # RUN ACTIONS or Home Assistant Startup or HASPone Connect # Display text and apply text style - conditions: - condition: template @@ -380,39 +380,39 @@ action: # Theme: Apply selected foreground color on change - conditions: - condition: template - value_template: "{{ trigger.topic == selectedfgtopic }}" + value_template: "{{ (trigger.topic == selectedfgtopic) and ((selected_fgcolor|int) == -1) }}" sequence: - service: mqtt.publish data: topic: "{{commandtopic}}.pco" - payload: "{{selectedfg}}" + payload: "{{trigger.payload}}" ######################################################################### # Theme: Apply selected background color on change - conditions: - condition: template - value_template: "{{ trigger.topic == selectedbgtopic }}" + value_template: "{{ (trigger.topic == selectedbgtopic) and ((selected_bgcolor|int) == -1) }}" sequence: - service: mqtt.publish data: topic: "{{commandtopic}}.bco" - payload: "{{selectedbg}}" + payload: "{{trigger.payload}}" ######################################################################### # Theme: Apply unselected foreground color on change - conditions: - condition: template - value_template: "{{ trigger.topic == unselectedfgtopic }}" + value_template: "{{ (trigger.topic == unselectedfgtopic) and ((unselected_fgcolor|int) == -1) }}" sequence: - service: mqtt.publish data: topic: "{{commandtopic}}.pco2" - payload: "{{unselectedfg}}" + payload: "{{trigger.payload}}" ######################################################################### # Theme: Apply unselected background color on change - conditions: - condition: template - value_template: "{{ trigger.topic == unselectedbgtopic }}" + value_template: "{{ (trigger.topic == unselectedbgtopic) and ((unselected_bgcolor|int) == -1) }}" sequence: - service: mqtt.publish data: topic: "{{commandtopic}}.bco2" - payload: "{{unselectedbg}}" + payload: "{{trigger.payload}}" diff --git a/Home_Assistant/blueprints/hasp_Display_Toggle.yaml b/Home_Assistant/blueprints/hasp_Display_Toggle.yaml index 935c36a..94f75c0 100644 --- a/Home_Assistant/blueprints/hasp_Display_Toggle.yaml +++ b/Home_Assistant/blueprints/hasp_Display_Toggle.yaml @@ -1,12 +1,12 @@ blueprint: - name: "HASP p[x].b[y] displays a toggle button" + name: "HASPone p[x].b[y] displays a toggle button" description: | - ## Blueprint Version: `1.03.00` + ## Blueprint Version: `1.08.00` # Description - Press a button on the HASP to toggle the state of an entity. The button colors and text can change in response to the on/off state or attribute of the selected entity. + Press a button on the HASPone to toggle the state of an entity. The button colors and text can change in response to the on/off state or attribute of the selected entity. There are a lot of options below! No worries, the defaults should work in a lot of cases. @@ -45,9 +45,9 @@ blueprint:
- ## HASP Page and Button Reference + ## HASPone Page and Button Reference - The images below show each available HASP page along with the layout of available button objects. + The images below show each available HASPone page along with the layout of available button objects.
@@ -65,11 +65,11 @@ blueprint:
- ## HASP Font Reference + ## HASPone 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 + 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 | | :--- | :---------------- | :-------------------| :--------------- | @@ -91,7 +91,7 @@ blueprint: ### 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) + ![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)
@@ -120,16 +120,16 @@ blueprint: domain: automation input: haspdevice: - name: "HASP Device" - description: "Select the HASP device" + name: "HASPone Device" + description: "Select the HASPone 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 toggle. Refer to the HASP Page and Button reference above." + name: "HASPone Page" + description: "Select the HASPone page (1-11) for this toggle. Refer to the HASPone Page and Button reference above." default: 1 selector: number: @@ -138,8 +138,8 @@ blueprint: mode: slider unit_of_measurement: page haspbutton: - name: "HASP Button" - description: "Select the HASP button for this toggle. Refer to the HASP Page and Button reference above." + name: "HASPone Button" + description: "Select the HASPone button for this toggle. Refer to the HASPone Page and Button reference above." default: 4 selector: number: @@ -226,7 +226,7 @@ blueprint: mode: slider font_select: name: "Font" - description: "Select the font for the displayed text. Refer to the HASP Font Reference above." + description: "Select the font for the displayed text. Refer to the HASPone Font Reference above." default: "8 - Noto Sans 64" selector: select: @@ -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 @@ -276,7 +282,7 @@ variables: haspdevice: !input haspdevice haspname: >- {%- for entity in device_entities(haspdevice) -%} - {%- if entity|regex_search("^sensor\.") -%} + {%- 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 -%} @@ -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" }}' @@ -370,7 +377,7 @@ trigger_variables: haspdevice: !input haspdevice haspname: >- {%- for entity in device_entities(haspdevice) -%} - {%- if entity|regex_search("^sensor\.") -%} + {%- 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 -%} @@ -390,23 +397,23 @@ trigger_variables: unselectedfgtopic: '{{ "hasp/" ~ haspname ~ "/light/unselectedforegroundcolor/rgb" }}' unselectedbgtopic: '{{ "hasp/" ~ haspname ~ "/light/unselectedbackgroundcolor/rgb" }}' -trigger: - - platform: state +triggers: + - trigger: state entity_id: !input entity - - platform: homeassistant + - trigger: homeassistant event: start - - platform: template + - trigger: template value_template: "{{ is_state(haspsensor, 'ON') }}" - - platform: mqtt + - trigger: mqtt topic: "{{jsontopic}}" payload: "{{buttonjsonpayload}}" - - platform: mqtt + - trigger: mqtt topic: "{{selectedfgtopic}}" - - platform: mqtt + - trigger: mqtt topic: "{{selectedbgtopic}}" - - platform: mqtt + - trigger: mqtt topic: "{{unselectedfgtopic}}" - - platform: mqtt + - trigger: mqtt topic: "{{unselectedbgtopic}}" condition: @@ -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 new file mode 100644 index 0000000..33c6f4a --- /dev/null +++ b/Home_Assistant/blueprints/hasp_Display_Value_with_Icon_and_Colors.yaml @@ -0,0 +1,623 @@ +blueprint: + name: "HASPone p[x].b[y] displays the value of a given entity with icons and colors" + description: | + + ## Blueprint Version: `1.08.00` + + # Description + + A HASPone button displays the current value of an entity (state or attribute) with a dynamic icon on the left and (optional) colors. Up to 5 icons and color ranges are supported. + + ![Preview](https://raw.githubusercontent.com/HASwitchPlate/HASPone/main/images/hasp_Display_Value_with_Icon_and_Colors.png) + + If fewer than 5 value ranges are desired, set the unused ranges at the end to a threshold of `999999`. For example, to use 3 ranges one can set the `Value 4/5 lower threshold` and `Value 5/5 lower threshold` to `999999`. + + ## HASPone Page and Button reference + +
+ + This automation is designed to work with the full-width buttons found on pages 1-3 + + | Pages 1-3 | + |-----------| + | ![Pages 1-3](https://raw.githubusercontent.com/HASwitchPlate/HASPone/main/images/NextionUI_p1-p3_4buttons.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 | + | :--- | :---------------- | :-------------------| :--------------- | + | 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 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-3) for the value. Refer to the HASPone Page and Button reference above." + default: 1 + selector: + number: + min: 1 + max: 3 + mode: slider + unit_of_measurement: page + haspbutton: + name: "HASPone Button" + description: "Select the HASPone button (4-7) for the value. Refer to the HASPone Page and Button reference above." + default: 4 + selector: + number: + min: 4 + max: 7 + mode: slider + unit_of_measurement: button + source_entity: + name: "Source entity" + description: "Select the entity providing the value to display" + default: + selector: + entity: + source_attribute: + name: "Source entity state or attribute" + description: "Enter `state` to track the state of the entity above, or enter an attribute name if the sensor has a specific attribute you want to track. Most uses will leave this set to `state`." + default: "state" + selector: + text: + source_prefix: + name: "Display prefix" + description: "Text to insert before the value to be displayed. Enter `none` to disable." + default: "none" + selector: + text: + source_suffix: + name: "Display suffix" + description: "Text to insert after the value to be displayed. Enter `none` to disable." + default: "none" + selector: + text: + font_select: + name: "Font" + description: "Select the font for the displayed text. Refer to the HASPone Font Reference above." + default: "10 - Noto Sans Bold 80" + selector: + select: + options: + - "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" + value_1of5_icon: + name: "Value 1/5 icon" + description: 'Icon to display when the selected value is below the 2/5 threshold. (see "Icons" above for reference)' + default: "" + selector: + text: + value_1of5_color: + name: "Value 1/5 color" + description: 'Icon color when the selected value is below the 2/5 threshold in Nextion RGB565 format (see "Nextion color codes" above for reference). -1 = Current theme foreground color, or 2047 = Ice blue' + default: 2047 + selector: + number: + min: -1 + max: 65535 + mode: slider + value_2of5_threshold: + name: "Value 2/5 lower threshold" + description: "Values above the 2/5 threshold and below the 3/5 threshold will show the 2/5 icon+color. Below this threshold, show the 1/5 icon+color." + default: 0 + selector: + number: + max: 999999 + min: -999999 + mode: box + value_2of5_icon: + name: "Value 2/5 icon" + description: 'Icon to display when the selected value is between the 2/5 and 3/5 thresholds' + default: "" + selector: + text: + value_2of5_color: + name: "Value 2/5 color" + description: "Icon color when the selected value is above the 2/5 threshold and below the 3/5 threshold in Nextion RGB565 format. -1 = Current theme foreground color, or 31 = Blue" + default: 31 + selector: + number: + min: -1 + max: 65535 + mode: slider + value_3of5_threshold: + name: "Value 3/5 lower threshold" + description: "Values above the 3/5 threshold and below the 4/5 threshold will show the 3/5 icon+color." + default: 32 + selector: + number: + max: 999999 + min: -999999 + mode: box + value_3of5_icon: + name: "Value 3/5 icon" + description: 'Icon to display when the selected value is between the 3/5 and 4/5 thresholds' + default: "" + selector: + text: + value_3of5_color: + name: "Value 3/5 color" + description: "Icon color when the selected value is above the 3/5 threshold and below the 4/5 threshold in Nextion RGB565 format. -1 = Current theme foreground color, or 1536 = Green" + default: 1536 + selector: + number: + min: -1 + max: 65535 + mode: slider + value_4of5_threshold: + name: "Value 4/5 lower threshold" + description: "Values above the 4/5 threshold and below the 5/5 threshold will show the 4/5 icon+color." + default: 80 + selector: + number: + max: 999999 + min: -999999 + mode: box + value_4of5_icon: + name: "Value 4/5 icon" + description: 'Icon to display when the selected value is between the 4/5 and 5/5 thresholds' + default: "" + selector: + text: + value_4of5_color: + name: "Value 4/5 color" + description: "Icon color when the selected value is above the 4/5 threshold and below the 5/5 threshold in Nextion RGB565 format. -1 = Current theme foreground color, or 64512 = Orange" + default: 64512 + selector: + number: + min: -1 + max: 65535 + mode: slider + value_5of5_threshold: + name: "Value 5/5 lower threshold" + description: "Values above the 5/5 threshold will show the 5/5 icon+color." + default: 95 + selector: + number: + max: 999999 + min: -999999 + mode: box + value_5of5_icon: + name: "Value 5/5 icon" + description: 'Icon to display when the selected value is above the 5/5 threshold' + default: "" + selector: + text: + value_5of5_color: + name: "Value 5/5 color" + description: "Icon color when the selected value is above the 5/5 threshold in Nextion RGB565 format. -1 = Current theme foreground color, or 63488 = Red" + default: 63488 + selector: + number: + min: -1 + max: 65535 + mode: slider + colortext: + name: "Colorize value text" + description: "Also apply icon colors to text" + default: false + selector: + boolean: + roundvalue: + name: "Round sensor values to nearest integer" + description: "Enable this if you don't want decimal places involved" + default: false + selector: + boolean: + +mode: parallel +max_exceeded: silent + +variables: + 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 -%} + hasppage: !input hasppage + haspbutton: !input haspbutton + source_entity: !input source_entity + source_attribute: !input source_attribute + source_prefix: !input source_prefix + source_suffix: !input source_suffix + font_select: !input font_select + font: '{{ font_select.split(" - ")[0] | int(default=10) }}' + value_1of5_color: !input value_1of5_color + value_1of5_icon: !input value_1of5_icon + value_2of5_threshold: !input value_2of5_threshold + value_2of5_color: !input value_2of5_color + value_2of5_icon: !input value_2of5_icon + value_3of5_threshold: !input value_3of5_threshold + value_3of5_color: !input value_3of5_color + value_3of5_icon: !input value_3of5_icon + value_4of5_threshold: !input value_4of5_threshold + value_4of5_color: !input value_4of5_color + value_4of5_icon: !input value_4of5_icon + value_5of5_threshold: !input value_5of5_threshold + value_5of5_color: !input value_5of5_color + value_5of5_icon: !input value_5of5_icon + colortext: !input colortext + roundvalue: !input roundvalue + haspobject: '{{ "p[" ~ hasppage ~ "].b[" ~ haspbutton ~ "]" }}' + commandtopic: '{{ "hasp/" ~ haspname ~ "/command/" ~ haspobject }}' + jsoncommandtopic: '{{ "hasp/" ~ haspname ~ "/command/json" }}' + jsontopic: '{{ "hasp/" ~ haspname ~ "/state/json" }}' + entity_value: >- + {%- if source_attribute|lower == "state" -%} + {%- if roundvalue == true -%} + {{- states(source_entity) | round(default=0) | int -}} + {%- else -%} + {{- states(source_entity) -}} + {%- endif -%} + {%- else -%} + {%- if roundvalue == true -%} + {{- state_attr(source_entity, source_attribute) | round(default=0) | int -}} + {%- else -%} + {{- state_attr(source_entity, source_attribute) -}} + {%- endif -%} + {%- endif -%} + icon: >- + {%- if entity_value|round(3,default=0) <= value_2of5_threshold|round(3,default=0) -%} + {{ value_1of5_icon }} + {%- elif entity_value|round(3,default=0) < value_3of5_threshold|round(3,default=0) -%} + {{ value_2of5_icon }} + {%- elif entity_value|round(3,default=0) < value_4of5_threshold|round(3,default=0) -%} + {{ value_3of5_icon }} + {%- elif entity_value|round(3,default=0) < value_5of5_threshold|round(3,default=0) -%} + {{ value_4of5_icon }} + {%- else -%} + {{ value_5of5_icon }} + {%- endif -%} + prefixstring: "{% if source_prefix|lower != 'none' %}{{ source_prefix }}{% endif %}" + suffixstring: "{% if source_suffix|lower != 'none' %}{{ source_suffix }}{% endif %}" + text: "{{prefixstring}}{{entity_value}}{{suffixstring}}" + ypos: "{{(haspbutton|int - 4) * 67 + 2}}" + xpos: 0 + iconwidth: 65 + iconheight: 65 + iconfont: 8 + xcen: 2 + ycen: 1 + 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) }} + 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: >- + {%- 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 }} + selectedbg: >- + {%- 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 }} + unselectedfg: >- + {%- 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 }} + unselectedbg: >- + {%- 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 }} + iconcolor: >- + {%- if entity_value|round(3,default=0) <= value_2of5_threshold|round(3,default=0) -%} + {%- set color = value_1of5_color -%} + {%- elif entity_value|round(3,default=0) <= value_3of5_threshold|round(3,default=0) -%} + {%- set color = value_2of5_color -%} + {%- elif entity_value|round(3,default=0) <= value_4of5_threshold|round(3,default=0) -%} + {%- set color = value_3of5_color -%} + {%- elif entity_value|round(3,default=0) <= value_5of5_threshold|round(3,default=0) -%} + {%- set color = value_4of5_color -%} + {%- else -%} + {%- set color = value_5of5_color -%} + {%- endif -%} + {%- if color|int < 0 -%} + {{- selectedfg -}} + {%- else -%} + {{- color|int -}} + {%- endif -%} + textcolor: >- + {%- if colortext == true -%} + {{- iconcolor -}} + {%- else -%} + {{- selectedfg -}} + {%- 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: + - trigger: state + entity_id: !input source_entity + - trigger: template + value_template: "{{ is_state(haspsensor, 'ON') }}" + - trigger: homeassistant + event: start + - trigger: mqtt + topic: "{{jsontopic}}" + - trigger: mqtt + topic: "{{selectedfgtopic}}" + - trigger: mqtt + topic: "{{selectedbgtopic}}" + - trigger: mqtt + topic: "{{unselectedfgtopic}}" + - trigger: mqtt + topic: "{{unselectedbgtopic}}" + +condition: + - condition: template + value_template: "{{ is_state(haspsensor, 'ON') }}" + +action: + - service: mqtt.publish + data: + topic: "debug" + payload: "trigger: {{ trigger }}" + - choose: + ######################################################################### + # RUN ACTIONS or Home Assistant Startup or HASPone Connect + # Apply styles, place text, and then place icon if our target page is currently active + - 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}}.pco={{textcolor}}", + "{{haspobject}}.bco={{selectedbg}}", + "{{haspobject}}.pco2={{unselectedfg}}", + "{{haspobject}}.bco2={{unselectedbg}}", + "{{haspobject}}.txt=\"{{text}} \"" + {%- if activepage|int == hasppage|int -%} + ,"delay=1","xstr {{xpos}},{{ypos}},{{iconwidth}},{{iconheight}},{{iconfont}},{{iconcolor}},0,1,1,3,\"{{icon}}\"" + {%- endif -%}] + ######################################################################### + # Update value if our source entity changed state + - conditions: + - condition: template + value_template: '{{ (trigger.platform == "state") and (trigger.entity_id == source_entity) }}' + sequence: + - service: mqtt.publish + data: + topic: "{{jsoncommandtopic}}" + payload: >- + ["{{haspobject}}.pco={{textcolor}}", + "{{haspobject}}.txt=\"{{text}} \"" + {%- if activepage|int == hasppage|int -%} + ,"delay=1","xstr {{xpos}},{{ypos}},{{iconwidth}},{{iconheight}},{{iconfont}},{{iconcolor}},0,1,1,3,\"{{icon}}\"" + {%- endif -%}] + ######################################################################### + # Catch triggers fired by incoming MQTT messages + - 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: + ######################################################################### + # Icon overlay + - conditions: # Somebody pressed our button which hides the overlaid icon. Put it back. + - condition: template + value_template: '{{ (trigger.topic == jsontopic ) and (trigger.payload_json.event == haspobject ) and (trigger.payload_json.value == "OFF") and (activepage|int == hasppage|int)}}' + sequence: + - service: mqtt.publish + data: + topic: "{{jsoncommandtopic}}" + payload: '["delay=1","xstr {{xpos}},{{ypos}},{{iconwidth}},{{iconheight}},{{iconfont}},{{iconcolor}},0,1,1,3,\"{{icon}}\""]' + - conditions: # Page changed to our page, so place the icon on the screen. + - condition: template + value_template: '{{ (trigger.topic == jsontopic ) and (trigger.payload_json.event == "page" ) and (trigger.payload_json.value == hasppage|int) }}' + sequence: + - service: mqtt.publish + data: + topic: "{{jsoncommandtopic}}" + payload: '["delay=1","xstr {{xpos}},{{ypos}},{{iconwidth}},{{iconheight}},{{iconfont}},{{iconcolor}},0,1,1,3,\"{{icon}}\""]' + ######################################################################### + # 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}}" + - condition: template + value_template: "{{ activepage|int == hasppage|int }}" + - delay: "00:00:00.5" + - service: mqtt.publish + data: + topic: "{{jsoncommandtopic}}" + payload: '["delay=1","xstr {{xpos}},{{ypos}},{{iconwidth}},{{iconheight}},{{iconfont}},{{iconcolor}},0,1,1,3,\"{{icon}}\""]' + ######################################################################### + # 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}}" + - condition: template + value_template: "{{ activepage|int == hasppage|int }}" + - delay: "00:00:00.5" + - service: mqtt.publish + data: + topic: "{{jsoncommandtopic}}" + payload: '["delay=1","xstr {{xpos}},{{ypos}},{{iconwidth}},{{iconheight}},{{iconfont}},{{iconcolor}},0,1,1,3,\"{{icon}}\""]' + ######################################################################### + # 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}}" + - condition: template + value_template: "{{ activepage|int == hasppage|int }}" + - delay: "00:00:00.5" + - service: mqtt.publish + data: + topic: "{{jsoncommandtopic}}" + payload: '["delay=1","xstr {{xpos}},{{ypos}},{{iconwidth}},{{iconheight}},{{iconfont}},{{iconcolor}},0,1,1,3,\"{{icon}}\""]' + ######################################################################### + # 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}}" + - condition: template + value_template: "{{ activepage|int == hasppage|int }}" + - delay: "00:00:00.5" + - service: mqtt.publish + data: + topic: "{{jsoncommandtopic}}" + payload: '["delay=1","xstr {{xpos}},{{ypos}},{{iconwidth}},{{iconheight}},{{iconfont}},{{iconcolor}},0,1,1,3,\"{{icon}}\""]' diff --git a/Home_Assistant/blueprints/hasp_Display_Volume_Control_page8.yaml b/Home_Assistant/blueprints/hasp_Display_Volume_Control_page8.yaml index 8db6304..0840900 100644 --- a/Home_Assistant/blueprints/hasp_Display_Volume_Control_page8.yaml +++ b/Home_Assistant/blueprints/hasp_Display_Volume_Control_page8.yaml @@ -1,8 +1,8 @@ blueprint: - name: "HASP p[8].b[9] The slider button on page 8 displays a volume control" + name: "HASPone p[8].b[9] The slider button on page 8 displays a volume control" description: | - ## Blueprint Version: `1.03.00` + ## Blueprint Version: `1.08.00` # Description @@ -10,7 +10,7 @@ blueprint: ![Preview](https://raw.githubusercontent.com/HASwitchPlate/HASPone/main/images/hasp_Display_Volume_Control_page8.png) - ## HASP Page and Button reference + ## HASPone Page and Button reference
@@ -26,8 +26,8 @@ blueprint: domain: automation input: haspdevice: - name: "HASP Device" - description: "Select the HASP device" + name: "HASPone Device" + description: "Select the HASPone device" selector: device: integration: mqtt @@ -47,7 +47,7 @@ variables: haspdevice: !input haspdevice haspname: >- {%- for entity in device_entities(haspdevice) -%} - {%- if entity|regex_search("^sensor\.") -%} + {%- 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 -%} @@ -89,7 +89,7 @@ trigger_variables: haspdevice: !input haspdevice haspname: >- {%- for entity in device_entities(haspdevice) -%} - {%- if entity|regex_search("^sensor\.") -%} + {%- 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 -%} @@ -105,17 +105,17 @@ trigger_variables: unselectedbgtopic: '{{ "hasp/" ~ haspname ~ "/light/unselectedbackgroundcolor/rgb" }}' trigger: - - platform: state + - trigger: state entity_id: !input volumeentity - - platform: homeassistant + - trigger: homeassistant event: start - - platform: template + - trigger: template value_template: "{{ is_state(haspsensor, 'ON') }}" - - platform: mqtt + - trigger: mqtt topic: "{{volumetopic}}" - - platform: mqtt + - trigger: mqtt topic: "{{unselectedfgtopic}}" - - platform: mqtt + - trigger: mqtt topic: "{{unselectedbgtopic}}" condition: @@ -125,7 +125,7 @@ condition: action: - choose: ######################################################################### - # RUN ACTIONS or Home Assistant Startup or HASP Connect + # RUN ACTIONS or Home Assistant Startup or HASPone Connect # Apply text and style - conditions: - condition: template diff --git a/Home_Assistant/blueprints/hasp_Display_Weather_Condition.yaml b/Home_Assistant/blueprints/hasp_Display_Weather_Condition.yaml index 81f6190..479f468 100644 --- a/Home_Assistant/blueprints/hasp_Display_Weather_Condition.yaml +++ b/Home_Assistant/blueprints/hasp_Display_Weather_Condition.yaml @@ -1,18 +1,18 @@ blueprint: - name: "HASP p[x].b[y] displays the current weather condition" + name: "HASPone p[x].b[y] displays the current weather condition" description: | - ## Blueprint Version: `1.03.00` + ## Blueprint Version: `1.08.00` # Description - A HASP button displays the current weather condition + A HASPone button displays the current weather condition ![Preview](https://raw.githubusercontent.com/HASwitchPlate/HASPone/main/images/hasp_Display_Weather_Condition.png) - ## HASP Page and Button Reference + ## HASPone Page and Button Reference - The images below show each available HASP page along with the layout of available button objects. + The images below show each available HASPone page along with the layout of available button objects.
@@ -30,11 +30,11 @@ blueprint:
- ## HASP Font Reference + ## HASPone 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 + 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 | | :--- | :---------------- | :-------------------| :--------------- | @@ -56,23 +56,45 @@ blueprint: ### 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) + ![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: "HASP Device" - description: "Select the HASP device" + name: "HASPone Device" + description: "Select the HASPone device" selector: device: integration: mqtt manufacturer: "HASwitchPlate" model: "HASPone v1.0.0" hasppage: - name: "HASP Page" - description: "Select the HASP page (1-11) for the weather condition. Refer to the HASP Page and Button reference above." + name: "HASPone Page" + description: "Select the HASPone page (1-11) for the weather condition. Refer to the HASPone Page and Button reference above." default: 1 selector: number: @@ -81,8 +103,8 @@ blueprint: mode: slider unit_of_measurement: page haspbutton: - name: "HASP Button" - description: "Select the HASP button (4-15) for the weather condition. Refer to the HASP Page and Button reference above." + name: "HASPone Button" + description: "Select the HASPone button (4-15) for the weather condition. Refer to the HASPone Page and Button reference above." default: 4 selector: number: @@ -98,7 +120,7 @@ blueprint: domain: weather font_select: name: "Font" - description: "Select the font for the displayed text. Refer to the HASP Font Reference above." + description: "Select the font for the displayed text. Refer to the HASPone Font Reference above." default: "8 - Noto Sans 64" selector: select: @@ -140,6 +162,42 @@ blueprint: default: false 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 @@ -148,7 +206,7 @@ variables: haspdevice: !input haspdevice haspname: >- {%- for entity in device_entities(haspdevice) -%} - {%- if entity|regex_search("^sensor\.") -%} + {%- 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 -%} @@ -156,12 +214,16 @@ 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 + 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" }}' @@ -173,59 +235,75 @@ variables: unselectedfgtopic: '{{ "hasp/" ~ haspname ~ "/light/unselectedforegroundcolor/rgb" }}' unselectedbgtopic: '{{ "hasp/" ~ haspname ~ "/light/unselectedbackgroundcolor/rgb" }}' selectedfg: >- - {%- 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 }} + {%- 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: >- - {%- 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 }} + {%- 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: >- - {%- 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 }} + {%- 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: >- - {%- 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 }} + {%- 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\.") -%} + {%- 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 -%} @@ -241,19 +319,19 @@ trigger_variables: unselectedbgtopic: '{{ "hasp/" ~ haspname ~ "/light/unselectedbackgroundcolor/rgb" }}' trigger: - - platform: state + - trigger: state entity_id: !input weather_provider - - platform: template + - trigger: template value_template: "{{ is_state(haspsensor, 'ON') }}" - - platform: homeassistant + - trigger: homeassistant event: start - - platform: mqtt + - trigger: mqtt topic: "{{selectedfgtopic}}" - - platform: mqtt + - trigger: mqtt topic: "{{selectedbgtopic}}" - - platform: mqtt + - trigger: mqtt topic: "{{unselectedfgtopic}}" - - platform: mqtt + - trigger: mqtt topic: "{{unselectedbgtopic}}" condition: @@ -263,7 +341,7 @@ condition: action: - choose: ######################################################################### - # RUN ACTIONS or Home Assistant Startup or HASP Connect + # RUN ACTIONS or Home Assistant Startup or HASPone Connect # Display weather condition and apply text style - conditions: - condition: template @@ -313,12 +391,10 @@ action: sequence: - choose: ######################################################################### - # Theme: Apply selected foreground color when it changes. - # If the page is currently active, delay a moment before applying the overlay - # so it will fire after any other theme elements being applied. + # Theme: Apply selected foreground color on change - conditions: - condition: template - value_template: "{{ trigger.topic == selectedfgtopic }}" + value_template: "{{ (trigger.topic == selectedfgtopic) and ((selected_fgcolor|int) == -1) }}" sequence: - service: mqtt.publish data: @@ -328,7 +404,7 @@ action: # Theme: Apply selected background color on change - conditions: - condition: template - value_template: "{{ trigger.topic == selectedbgtopic }}" + value_template: "{{ (trigger.topic == selectedbgtopic) and ((selected_bgcolor|int) == -1) }}" sequence: - service: mqtt.publish data: @@ -338,7 +414,7 @@ action: # Theme: Apply unselected foreground color on change - conditions: - condition: template - value_template: "{{ trigger.topic == unselectedfgtopic }}" + value_template: "{{ (trigger.topic == unselectedfgtopic) and ((unselected_fgcolor|int) == -1) }}" sequence: - service: mqtt.publish data: @@ -348,7 +424,7 @@ action: # Theme: Apply unselected background color on change - conditions: - condition: template - value_template: "{{ trigger.topic == unselectedbgtopic }}" + value_template: "{{ (trigger.topic == unselectedbgtopic) and ((unselected_bgcolor|int) == -1) }}" sequence: - service: mqtt.publish data: 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 930b2a5..5a829fc 100644 --- a/Home_Assistant/blueprints/hasp_Display_Weather_Condition_Icon_Only.yaml +++ b/Home_Assistant/blueprints/hasp_Display_Weather_Condition_Icon_Only.yaml @@ -1,39 +1,69 @@ blueprint: - name: "HASP p[x].b[y] displays the current weather condition icon only" + name: "HASPone p[x].b[y] displays the current weather condition icon only" description: | - ## Blueprint Version: `1.03.00` + ## Blueprint Version: `1.08.00` # Description - A HASP button displays the current weather condition icon only + A HASPone button displays the current weather condition icon only ![Preview](https://raw.githubusercontent.com/HASwitchPlate/HASPone/main/images/hasp_Display_Weather_Condition_Icon_Only.png) - ## HASP Page and Button reference + ## 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
- This automation is designed to work with the full-width buttons found on pages 1-3 + Weather icons are available in 6 different sizes + + | Font | Name | + | :--- | :---------------- | + | 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 | + + ### Font examples - | Pages 1-3 | - |-----------| - | ![Pages 1-3](https://raw.githubusercontent.com/HASwitchPlate/HASPone/main/images/NextionUI_p1-p3_4buttons.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)
+ domain: automation input: haspdevice: - name: "HASP Device" - description: "Select the HASP device" + name: "HASPone Device" + description: "Select the HASPone device" selector: device: integration: mqtt manufacturer: "HASwitchPlate" model: "HASPone v1.0.0" hasppage: - name: "HASP Page" - description: "Select the HASP page (1-3) for the temperature" + name: "HASPone Page" + description: "Select the HASPone page (1-3) for the temperature" default: 1 selector: number: @@ -42,8 +72,8 @@ blueprint: mode: slider unit_of_measurement: page haspbutton: - name: "HASP Button" - description: "Select the HASP button (4-7) for the temperature. Refer to the object map in the HASP documentation." + name: "HASPone Button" + description: "Select the HASPone button (4-7) for the temperature. Refer to the HASPone Page and Button reference above." default: 4 selector: number: @@ -57,6 +87,81 @@ blueprint: selector: entity: domain: weather + font_select: + name: "Font" + description: "Select the font for the displayed icon. Refer to the HASPone Font Reference above." + default: "8 - Noto Sans 64" + selector: + select: + options: + - "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: "Icon horizontal alignment" + description: "Horizontal icon alignment: 0=Left 1=Center 2=Right" + default: "1 - Centered" + selector: + select: + options: + - "0 - Left aligned" + - "1 - Centered" + - "2 - Right aligned" + ycen_select: + name: "Icon vertical alignment" + description: "Vertical icon alignment: 0=Top 1=Center 2=Bottom" + default: "1 - Centered" + selector: + select: + options: + - "0 - Top aligned" + - "1 - Centered" + - "2 - Bottom aligned" + 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 + colorize_icon: + name: "Colorize weather icon" + description: "When enabled, weather condition icon will be colored based on the condition (eg, sunny will be yellow). If disabled, use the colors selected above." + default: true + selector: + boolean: mode: parallel max_exceeded: silent @@ -64,7 +169,7 @@ max_exceeded: silent variables: haspname: >- {%- for entity in device_entities(haspdevice) -%} - {%- if entity|regex_search("^sensor\.") -%} + {%- 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 -%} @@ -77,6 +182,17 @@ variables: hasppage: !input hasppage haspbutton: !input haspbutton weather_provider: !input weather_provider + font_select: !input font_select + font: '{{ font_select.split(" - ")[0] | int(default=8) }}' + xcen_select: !input xcen_select + xcen: '{{ xcen_select.split(" - ")[0] | int(default=1) }}' + ycen_select: !input ycen_select + ycen: '{{ ycen_select.split(" - ")[0] | int(default=1) }}' + selected_fgcolor: !input selected_fgcolor + selected_bgcolor: !input selected_bgcolor + unselected_fgcolor: !input unselected_fgcolor + unselected_bgcolor: !input unselected_bgcolor + colorize_icon: !input colorize_icon haspobject: '{{ "p[" ~ hasppage ~ "].b[" ~ haspbutton ~ "]" }}' commandtopic: '{{ "hasp/" ~ haspname ~ "/command/" ~ haspobject }}' jsoncommandtopic: '{{ "hasp/" ~ haspname ~ "/command/json" }}' @@ -90,7 +206,7 @@ variables: {%- elif condition == "fog" -%}  {%- elif condition == "hail" -%} -  +  {%- elif condition == "lightning" -%}  {%- elif condition == "lightning-rainy" -%} @@ -116,32 +232,6 @@ variables: {%- else -%}  {%- endif -%} - text: >- - {{- - states(weather_provider) | - replace("windy-variant","windy") | - replace("clear-night","clear night") | - replace("partlycloudy","partly cloudy") | - replace("lightning-rainy","lightning & rain") | - replace("snowy-rainy","snow & rain") | - title - -}} - font: >- - {%- set weatherlength = text | length -%} - {%- if weatherlength < 7 -%} - 8 - {%- elif weatherlength < 12 -%} - 7 - {%- else -%} - 6 - {%- endif -%} - ypos: "{{(haspbutton|int - 4) * 67 + 2}}" - xpos: 0 - iconwidth: 65 - iconheight: 65 - iconfont: 7 - xcen: 1 - ycen: 1 activepage: >- {%- set activepage = namespace() -%} {%- for entity in device_entities(haspdevice) -%} @@ -155,59 +245,114 @@ variables: unselectedfgtopic: '{{ "hasp/" ~ haspname ~ "/light/unselectedforegroundcolor/rgb" }}' unselectedbgtopic: '{{ "hasp/" ~ haspname ~ "/light/unselectedbackgroundcolor/rgb" }}' selectedfg: >- - {%- 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 }} + {%- 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: >- - {%- 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 }} + {%- 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: >- - {%- 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 }} + {%- 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: >- - {%- set color = namespace() -%} - {%- for entity in device_entities(haspdevice) -%} - {%- if entity|regex_search("^light\..*_unselected_background_color(?:_\d+|)$") -%} - {%- set color.source=entity -%} + {%- 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 -%} + icon_color: >- + {%- if not colorize_icon -%} + {{ selectedfg }} + {%- else -%} + {%- set condition=states(weather_provider) -%} + {%- if condition == "clear-night" -%} + 6350 + {%- elif condition == "cloudy" -%} + 29779 + {%- elif condition == "fog" -%} + 29779 + {%- elif condition == "hail" -%} + 29779 + {%- elif condition == "lightning" -%} + 65504 + {%- elif condition == "lightning-rainy" -%} + 65504 + {%- elif condition == "partlycloudy" -%} + 29779 + {%- elif condition == "pouring" -%} + 29779 + {%- elif condition == "rainy" -%} + 29779 + {%- elif condition == "snowy" -%} + 1535 + {%- elif condition == "snowy-rainy" -%} + 1535 + {%- elif condition == "sunny" -%} + 65504 + {%- elif condition == "windy" -%} + 1535 + {%- elif condition == "windy-variant" -%} + 1535 + {%- elif condition == "exceptional" -%} + 63488 + {%- else -%} + {{ selected_fgcolor }} {%- 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\.") -%} + {%- 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 -%} @@ -224,21 +369,21 @@ trigger_variables: unselectedbgtopic: '{{ "hasp/" ~ haspname ~ "/light/unselectedbackgroundcolor/rgb" }}' trigger: - - platform: state + - trigger: state entity_id: !input weather_provider - - platform: homeassistant + - trigger: homeassistant event: start - - platform: template + - trigger: template value_template: "{{ is_state(haspsensor, 'ON') }}" - - platform: mqtt + - trigger: mqtt topic: "{{jsontopic}}" - - platform: mqtt + - trigger: mqtt topic: "{{selectedfgtopic}}" - - platform: mqtt + - trigger: mqtt topic: "{{selectedbgtopic}}" - - platform: mqtt + - trigger: mqtt topic: "{{unselectedfgtopic}}" - - platform: mqtt + - trigger: mqtt topic: "{{unselectedbgtopic}}" condition: @@ -248,11 +393,11 @@ condition: action: - choose: ######################################################################### - # RUN ACTIONS or Home Assistant Startup or HASP Connect + # RUN ACTIONS or Home Assistant Startup or HASPone Connect # 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 @@ -267,10 +412,10 @@ action: data: topic: "{{jsoncommandtopic}}" payload: >- - ["{{haspobject}}.font={{iconfont}}", + ["{{haspobject}}.font={{font}}", "{{haspobject}}.xcen={{xcen}}", "{{haspobject}}.ycen={{ycen}}", - "{{haspobject}}.pco={{selectedfg}}", + "{{haspobject}}.pco={{icon_color}}", "{{haspobject}}.bco={{selectedbg}}", "{{haspobject}}.pco2={{unselectedfg}}", "{{haspobject}}.bco2={{unselectedbg}}", @@ -285,7 +430,7 @@ action: data: topic: "{{jsoncommandtopic}}" payload: >- - ["{{haspobject}}.pco={{selectedfg}}", + ["{{haspobject}}.pco={{icon_color}}", "{{haspobject}}.font={{font}}", "{{haspobject}}.txt=\"{{icon}} \""] ######################################################################### @@ -300,7 +445,7 @@ action: # Any change to the button will remove the overlaid icon. - conditions: - condition: template - value_template: "{{ trigger.topic == selectedfgtopic }}" + value_template: "{{ (trigger.topic == selectedfgtopic) and not colorize_icon }}" sequence: - service: mqtt.publish data: 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 df2427e..4067648 100644 --- a/Home_Assistant/blueprints/hasp_Display_Weather_Condition_with_Icon.yaml +++ b/Home_Assistant/blueprints/hasp_Display_Weather_Condition_with_Icon.yaml @@ -1,16 +1,16 @@ blueprint: - name: "HASP p[x].b[y] displays the current weather condition with icons" + name: "HASPone p[x].b[y] displays the current weather condition with icons" description: | - ## Blueprint Version: `1.03.00` + ## Blueprint Version: `1.08.00` # Description - A HASP button displays the current weather condition on the right with a matching icon on the left + A HASPone button displays the current weather condition on the right with a matching icon on the left ![Preview](https://raw.githubusercontent.com/HASwitchPlate/HASPone/main/images/hasp_Display_Weather_Condition_with_Icon.png) - ## HASP Page and Button reference + ## HASPone Page and Button reference
@@ -21,19 +21,42 @@ blueprint: | ![Pages 1-3](https://raw.githubusercontent.com/HASwitchPlate/HASPone/main/images/NextionUI_p1-p3_4buttons.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" + name: "HASPone Device" + description: "Select the HASPone device" selector: device: integration: mqtt manufacturer: "HASwitchPlate" model: "HASPone v1.0.0" hasppage: - name: "HASP Page" - description: "Select the HASP page (1-3) for the temperature" + name: "HASPone Page" + description: "Select the HASPone page (1-3) for the temperature" default: 1 selector: number: @@ -42,8 +65,8 @@ blueprint: mode: slider unit_of_measurement: page haspbutton: - name: "HASP Button" - description: "Select the HASP button (4-7) for the temperature. Refer to the object map in the HASP documentation." + name: "HASPone Button" + description: "Select the HASPone button (4-7) for the temperature. Refer to the HASPone Page and Button reference above." default: 4 selector: number: @@ -57,6 +80,48 @@ blueprint: selector: entity: domain: weather + 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 + colorize_icon: + name: "Colorize weather icon" + description: "When enabled, weather condition icon will be colored based on the condition (eg, sunny will be yellow). If disabled, use the colors selected above." + default: true + selector: + boolean: mode: parallel max_exceeded: silent @@ -64,7 +129,7 @@ max_exceeded: silent variables: haspname: >- {%- for entity in device_entities(haspdevice) -%} - {%- if entity|regex_search("^sensor\.") -%} + {%- 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 -%} @@ -77,6 +142,11 @@ variables: hasppage: !input hasppage haspbutton: !input haspbutton weather_provider: !input weather_provider + selected_fgcolor: !input selected_fgcolor + selected_bgcolor: !input selected_bgcolor + unselected_fgcolor: !input unselected_fgcolor + unselected_bgcolor: !input unselected_bgcolor + colorize_icon: !input colorize_icon haspobject: '{{ "p[" ~ hasppage ~ "].b[" ~ haspbutton ~ "]" }}' commandtopic: '{{ "hasp/" ~ haspname ~ "/command/" ~ haspobject }}' jsoncommandtopic: '{{ "hasp/" ~ haspname ~ "/command/json" }}' @@ -90,7 +160,7 @@ variables: {%- elif condition == "fog" -%}  {%- elif condition == "hail" -%} -  +  {%- elif condition == "lightning" -%}  {%- elif condition == "lightning-rainy" -%} @@ -155,59 +225,114 @@ variables: unselectedfgtopic: '{{ "hasp/" ~ haspname ~ "/light/unselectedforegroundcolor/rgb" }}' unselectedbgtopic: '{{ "hasp/" ~ haspname ~ "/light/unselectedbackgroundcolor/rgb" }}' selectedfg: >- - {%- 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 }} + {%- 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: >- - {%- 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 }} + {%- 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: >- - {%- 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 }} + {%- 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: >- - {%- set color = namespace() -%} - {%- for entity in device_entities(haspdevice) -%} - {%- if entity|regex_search("^light\..*_unselected_background_color(?:_\d+|)$") -%} - {%- set color.source=entity -%} + {%- 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 -%} + icon_color: >- + {%- if not colorize_icon -%} + {{ selectedfg }} + {%- else -%} + {%- set condition=states(weather_provider) -%} + {%- if condition == "clear-night" -%} + 6350 + {%- elif condition == "cloudy" -%} + 29779 + {%- elif condition == "fog" -%} + 29779 + {%- elif condition == "hail" -%} + 29779 + {%- elif condition == "lightning" -%} + 65504 + {%- elif condition == "lightning-rainy" -%} + 65504 + {%- elif condition == "partlycloudy" -%} + 29779 + {%- elif condition == "pouring" -%} + 29779 + {%- elif condition == "rainy" -%} + 29779 + {%- elif condition == "snowy" -%} + 1535 + {%- elif condition == "snowy-rainy" -%} + 1535 + {%- elif condition == "sunny" -%} + 65504 + {%- elif condition == "windy" -%} + 1535 + {%- elif condition == "windy-variant" -%} + 1535 + {%- elif condition == "exceptional" -%} + 63488 + {%- else -%} + {{ selected_fgcolor }} {%- 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\.") -%} + {%- 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 -%} @@ -224,21 +349,21 @@ trigger_variables: unselectedbgtopic: '{{ "hasp/" ~ haspname ~ "/light/unselectedbackgroundcolor/rgb" }}' trigger: - - platform: state + - trigger: state entity_id: !input weather_provider - - platform: homeassistant + - trigger: homeassistant event: start - - platform: template + - trigger: template value_template: "{{ is_state(haspsensor, 'ON') }}" - - platform: mqtt + - trigger: mqtt topic: "{{jsontopic}}" - - platform: mqtt + - trigger: mqtt topic: "{{selectedfgtopic}}" - - platform: mqtt + - trigger: mqtt topic: "{{selectedbgtopic}}" - - platform: mqtt + - trigger: mqtt topic: "{{unselectedfgtopic}}" - - platform: mqtt + - trigger: mqtt topic: "{{unselectedbgtopic}}" condition: @@ -248,7 +373,7 @@ condition: action: - choose: ######################################################################### - # RUN ACTIONS or Home Assistant Startup or HASP Connect + # RUN ACTIONS or Home Assistant Startup or HASPone Connect # Apply styles, place text, and then place icon if our target page is currently active - conditions: - condition: template @@ -276,7 +401,7 @@ action: "{{haspobject}}.bco2={{unselectedbg}}", "{{haspobject}}.txt=\"{{text}} \"" {%- if activepage|int == hasppage|int -%} - ,"delay=1","xstr {{xpos}},{{ypos}},{{iconwidth}},{{iconheight}},{{iconfont}},{{selectedfg}},0,1,1,3,\"{{icon}}\"" + ,"delay=1","xstr {{xpos}},{{ypos}},{{iconwidth}},{{iconheight}},{{iconfont}},{{icon_color}},0,1,1,3,\"{{icon}}\"" {%- endif -%}] ######################################################################### # Update weather condition if our weather provider changed state @@ -292,7 +417,7 @@ action: "{{haspobject}}.font={{font}}", "{{haspobject}}.txt=\"{{text}} \"" {%- if activepage|int == hasppage|int -%} - ,"delay=1","xstr {{xpos}},{{ypos}},{{iconwidth}},{{iconheight}},{{iconfont}},{{selectedfg}},0,1,1,3,\"{{icon}}\"" + ,"delay=1","xstr {{xpos}},{{ypos}},{{iconwidth}},{{iconheight}},{{iconfont}},{{icon_color}},0,1,1,3,\"{{icon}}\"" {%- endif -%}] ######################################################################### @@ -318,7 +443,7 @@ action: - service: mqtt.publish data: topic: "{{jsoncommandtopic}}" - payload: '["delay=1","xstr {{xpos}},{{ypos}},{{iconwidth}},{{iconheight}},{{iconfont}},{{selectedfg}},0,1,1,3,\"{{icon}}\""]' + payload: '["delay=1","xstr {{xpos}},{{ypos}},{{iconwidth}},{{iconheight}},{{iconfont}},{{icon_color}},0,1,1,3,\"{{icon}}\""]' - conditions: # Page changed to our page, so place the icon on the screen. - condition: template value_template: '{{ (trigger.topic == jsontopic ) and (trigger.payload_json.event == "page" ) and (trigger.payload_json.value == hasppage|int) }}' @@ -326,13 +451,13 @@ action: - service: mqtt.publish data: topic: "{{jsoncommandtopic}}" - payload: '["delay=1","xstr {{xpos}},{{ypos}},{{iconwidth}},{{iconheight}},{{iconfont}},{{selectedfg}},0,1,1,3,\"{{icon}}\""]' + payload: '["delay=1","xstr {{xpos}},{{ypos}},{{iconwidth}},{{iconheight}},{{iconfont}},{{icon_color}},0,1,1,3,\"{{icon}}\""]' ######################################################################### # 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 }}" + value_template: "{{ (trigger.topic == selectedfgtopic) and ((selected_fgcolor|int) == -1) }}" sequence: - service: mqtt.publish data: @@ -344,12 +469,12 @@ action: - service: mqtt.publish data: topic: "{{jsoncommandtopic}}" - payload: '["delay=1","xstr {{xpos}},{{ypos}},{{iconwidth}},{{iconheight}},{{iconfont}},{{trigger.payload}},0,1,1,3,\"{{icon}}\""]' + payload: '["delay=1","xstr {{xpos}},{{ypos}},{{iconwidth}},{{iconheight}},{{iconfont}},{{icon_color}},0,1,1,3,\"{{icon}}\""]' ######################################################################### # Theme: Apply selected background color on change - conditions: - condition: template - value_template: "{{ trigger.topic == selectedbgtopic }}" + value_template: "{{ (trigger.topic == selectedbgtopic) and ((selected_bgcolor|int) == -1) }}" sequence: - service: mqtt.publish data: @@ -361,12 +486,12 @@ action: - service: mqtt.publish data: topic: "{{jsoncommandtopic}}" - payload: '["delay=1","xstr {{xpos}},{{ypos}},{{iconwidth}},{{iconheight}},{{iconfont}},{{selectedfg}},0,1,1,3,\"{{icon}}\""]' + payload: '["delay=1","xstr {{xpos}},{{ypos}},{{iconwidth}},{{iconheight}},{{iconfont}},{{icon_color}},0,1,1,3,\"{{icon}}\""]' ######################################################################### # Theme: Apply unselected foreground color on change - conditions: - condition: template - value_template: "{{ trigger.topic == unselectedfgtopic }}" + value_template: "{{ (trigger.topic == unselectedfgtopic) and ((unselected_fgcolor|int) == -1) }}" sequence: - service: mqtt.publish data: @@ -378,12 +503,12 @@ action: - service: mqtt.publish data: topic: "{{jsoncommandtopic}}" - payload: '["delay=1","xstr {{xpos}},{{ypos}},{{iconwidth}},{{iconheight}},{{iconfont}},{{selectedfg}},0,1,1,3,\"{{icon}}\""]' + payload: '["delay=1","xstr {{xpos}},{{ypos}},{{iconwidth}},{{iconheight}},{{iconfont}},{{icon_color}},0,1,1,3,\"{{icon}}\""]' ######################################################################### # Theme: Apply unselected background color on change - conditions: - condition: template - value_template: "{{ trigger.topic == unselectedbgtopic }}" + value_template: "{{ (trigger.topic == unselectedbgtopic) and ((unselected_bgcolor|int) == -1) }}" sequence: - service: mqtt.publish data: @@ -395,4 +520,4 @@ action: - service: mqtt.publish data: topic: "{{jsoncommandtopic}}" - payload: '["delay=1","xstr {{xpos}},{{ypos}},{{iconwidth}},{{iconheight}},{{iconfont}},{{selectedfg}},0,1,1,3,\"{{icon}}\""]' + payload: '["delay=1","xstr {{xpos}},{{ypos}},{{iconwidth}},{{iconheight}},{{iconfont}},{{icon_color}},0,1,1,3,\"{{icon}}\""]' diff --git a/Home_Assistant/blueprints/hasp_Display_Weather_Forecast.yaml b/Home_Assistant/blueprints/hasp_Display_Weather_Forecast.yaml index f33055f..db65e7b 100644 --- a/Home_Assistant/blueprints/hasp_Display_Weather_Forecast.yaml +++ b/Home_Assistant/blueprints/hasp_Display_Weather_Forecast.yaml @@ -1,18 +1,20 @@ blueprint: - name: "HASP p[x].b[y] displays the weather forecast" + name: "HASPone p[x].b[y] displays the weather forecast" description: | - ## Blueprint Version: `1.03.00` + ## Blueprint Version: `1.08.00` ## Description - A HASP button displays an attribute of a selected weather forecast. You can use this to display tomorrow's condition, or tonight's low temp. + A HASPone button displays an attribute of a selected weather forecast. You can use this to display tomorrow's condition, or tonight's low temp. Available forecast conditions will vary by weather provider, check your selected provider's state under `Developer Tools` > `States` to get a sense of what your selected provider has to offer. ![Preview](https://raw.githubusercontent.com/HASwitchPlate/HASPone/main/images/hasp_Display_Weather_Forecast.png) - ### HASP Page and Button reference + ## HASPone Page and Button Reference + + The images below show each available HASPone page along with the layout of available button objects.
@@ -30,49 +32,71 @@ blueprint:
- ## HASP Font reference + ## HASPone 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) in 4 sizes and [Webdings](https://en.wikipedia.org/wiki/Webdings#Character_set) in 1 size. + 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 - | Number | Font | Characters per line | Lines per button | - |--------|-------------------|---------------------|------------------| - | 0 | Consolas 24 point | 20 characters | 2 lines | - | 1 | Consolas 32 point | 15 characters | 2 lines | - | 2 | Consolas 48 point | 10 characters | 1 lines | - | 3 | Consolas 80 point | 6 characters | 1 lines | - | 4 | Webdings 56 point | 8 characters | 1 lines | + | 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 | - The HASP also includes [Google's "Noto Sans"](https://github.com/googlefonts/noto-fonts) proportional font in 5 sizes. + ### Icons - | Number | Font | - |--------|----------------------------| - | 5 | Noto Sans Regular 24 point | - | 6 | Noto Sans Regular 32 point | - | 7 | Noto Sans Regular 48 point | - | 8 | Noto Sans Regular 64 point | - | 9 | Noto Sans Regular 80 point | - | 10 | Noto Sans Bold 80 point | + 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) + + ![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: "HASP Device" - description: "Select the HASP device" + name: "HASPone Device" + description: "Select the HASPone device" selector: device: integration: mqtt manufacturer: "HASwitchPlate" model: "HASPone v1.0.0" hasppage: - name: "HASP Page" - description: "Select the HASP page (1-11) for the forecast. Refer to the HASP Page and Button reference above." + 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: @@ -81,8 +105,8 @@ blueprint: mode: slider unit_of_measurement: page haspbutton: - name: "HASP Button" - description: "Select the HASP button (4-15) for the forecast. Refer to the HASP Page and Button reference above." + 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: @@ -96,31 +120,41 @@ 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: name: "Font" - description: "Select the font for the displayed text. Refer to the HASP Font Reference above." + description: "Select the font for the displayed text. Refer to the HASPone Font Reference above." default: "8 - Noto Sans 64" selector: select: @@ -168,6 +202,42 @@ blueprint: default: true 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 @@ -176,13 +246,14 @@ variables: haspdevice: !input haspdevice haspname: >- {%- for entity in device_entities(haspdevice) -%} - {%- if entity|regex_search("^sensor\.") -%} + {%- 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 forecast_attribute: !input forecast_attribute prefix: !input prefix @@ -194,77 +265,88 @@ variables: ycen: '{{ ycen_select.split(" - ")[0] | int }}' wrap: !input wrap title_case: !input title_case + 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" }}' - text: >- - {%- if prefix|lower != "none" -%} - {{ prefix }} - {%- endif -%} - {%- if title_case -%} - {{ state_attr(weather_provider, "forecast")[forecast_index|int(default=0)].get(forecast_attribute) | title }} - {%- else -%} - {{ state_attr(weather_provider, "forecast")[forecast_index|int(default=0)].get(forecast_attribute) }} - {%- endif -%} isbr: "{% if wrap == true %}1{% else %}0{% endif %}" 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: >- - {%- 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 }} + {%- 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: >- - {%- 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 }} + {%- 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: >- - {%- 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 }} + {%- 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: >- - {%- 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 }} + {%- 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\.") -%} + {%- 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 -%} @@ -280,19 +362,19 @@ trigger_variables: unselectedbgtopic: '{{ "hasp/" ~ haspname ~ "/light/unselectedbackgroundcolor/rgb" }}' trigger: - - platform: state + - trigger: state entity_id: !input weather_provider - - platform: template + - trigger: template value_template: "{{ is_state(haspsensor, 'ON') }}" - - platform: homeassistant + - trigger: homeassistant event: start - - platform: mqtt + - trigger: mqtt topic: "{{selectedfgtopic}}" - - platform: mqtt + - trigger: mqtt topic: "{{selectedbgtopic}}" - - platform: mqtt + - trigger: mqtt topic: "{{unselectedfgtopic}}" - - platform: mqtt + - trigger: mqtt topic: "{{unselectedbgtopic}}" condition: @@ -300,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 @@ -352,7 +451,7 @@ action: # Theme: Apply selected foreground color on change - conditions: - condition: template - value_template: "{{ trigger.topic == selectedfgtopic }}" + value_template: "{{ (trigger.topic == selectedfgtopic) and ((selected_fgcolor|int) == -1) }}" sequence: - service: mqtt.publish data: @@ -362,7 +461,7 @@ action: # Theme: Apply selected background color on change - conditions: - condition: template - value_template: "{{ trigger.topic == selectedbgtopic }}" + value_template: "{{ (trigger.topic == selectedbgtopic) and ((selected_bgcolor|int) == -1) }}" sequence: - service: mqtt.publish data: @@ -372,7 +471,7 @@ action: # Theme: Apply unselected foreground color on change - conditions: - condition: template - value_template: "{{ trigger.topic == unselectedfgtopic }}" + value_template: "{{ (trigger.topic == unselectedfgtopic) and ((unselected_fgcolor|int) == -1) }}" sequence: - service: mqtt.publish data: @@ -382,7 +481,7 @@ action: # Theme: Apply unselected background color on change - conditions: - condition: template - value_template: "{{ trigger.topic == unselectedbgtopic }}" + value_template: "{{ (trigger.topic == unselectedbgtopic) and ((unselected_bgcolor|int) == -1) }}" sequence: - service: mqtt.publish data: 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..17f2c8a --- /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.08.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: + - trigger: state + entity_id: !input weather_provider + - trigger: template + value_template: "{{ is_state(haspsensor, 'ON') }}" + - trigger: homeassistant + event: start + - trigger: mqtt + topic: "{{selectedfgtopic}}" + - trigger: mqtt + topic: "{{selectedbgtopic}}" + - trigger: mqtt + topic: "{{unselectedfgtopic}}" + - trigger: 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 d929c06..90725e4 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 @@ -1,16 +1,16 @@ blueprint: - name: "HASP p[x].b[y] displays the current temperature from a weather provider, coloured icon only" + name: "HASPone p[x].b[y] displays the current temperature from a weather provider, coloured icon only" description: | - ## Blueprint Version: `1.03.00` + ## Blueprint Version: `1.08.00` # Description - A HASP button displays the current temperature from a weather provider as an icon that is optionally coloured. + A HASPone button displays the current temperature from a weather provider as an icon that is optionally coloured. ![Preview](https://raw.githubusercontent.com/HASwitchPlate/HASPone/main/images/hasp_Display_Weather_Temperature_Color_Icon_Only.png) - ## HASP Page and Button reference + ## HASPone Page and Button reference
@@ -52,16 +52,16 @@ blueprint: domain: automation input: haspdevice: - name: "HASP Device" - description: "Select the HASP device" + name: "HASPone Device" + description: "Select the HASPone device" selector: device: integration: mqtt manufacturer: "HASwitchPlate" model: "HASPone v1.0.0" hasppage: - name: "HASP Page" - description: "Select the HASP page (1-11) for the temperature icon. Refer to the HASP Page and Button reference above." + name: "HASPone Page" + description: "Select the HASPone page (1-11) for the temperature icon. Refer to the HASPone Page and Button reference above." default: 1 selector: number: @@ -70,8 +70,8 @@ blueprint: mode: slider unit_of_measurement: page haspbutton: - name: "HASP Button" - description: "Select the HASP button (4-15) for the temperature icon. Refer to the HASP Page and Button reference above." + name: "HASPone Button" + description: "Select the HASPone button (4-15) for the temperature icon. Refer to the HASPone Page and Button reference above." default: 4 selector: number: @@ -174,7 +174,7 @@ max_exceeded: silent variables: haspname: >- {%- for entity in device_entities(haspdevice) -%} - {%- if entity|regex_search("^sensor\.") -%} + {%- 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 -%} @@ -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 -%} @@ -291,7 +291,7 @@ trigger_variables: haspdevice: !input haspdevice haspname: >- {%- for entity in device_entities(haspdevice) -%} - {%- if entity|regex_search("^sensor\.") -%} + {%- 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 -%} @@ -307,19 +307,19 @@ trigger_variables: unselectedbgtopic: '{{ "hasp/" ~ haspname ~ "/light/unselectedbackgroundcolor/rgb" }}' trigger: - - platform: state + - trigger: state entity_id: !input weather_provider - - platform: template + - trigger: template value_template: "{{ is_state(haspsensor, 'ON') }}" - - platform: homeassistant + - trigger: homeassistant event: start - - platform: mqtt + - trigger: mqtt topic: "{{selectedfgtopic}}" - - platform: mqtt + - trigger: mqtt topic: "{{selectedbgtopic}}" - - platform: mqtt + - trigger: mqtt topic: "{{unselectedfgtopic}}" - - platform: mqtt + - trigger: mqtt topic: "{{unselectedbgtopic}}" condition: @@ -329,11 +329,11 @@ condition: action: - choose: ######################################################################### - # RUN ACTIONS or Home Assistant Startup or HASP Connect + # RUN ACTIONS or Home Assistant Startup or HASPone Connect # 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 4c30607..cdf82a4 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 @@ -1,16 +1,16 @@ blueprint: - name: "HASP p[x].b[y] displays the current temperature from a weather provider with icon and colors" + name: "HASPone p[x].b[y] displays the current temperature from a weather provider with icon and colors" description: | - ## Blueprint Version: `1.03.00` + ## Blueprint Version: `1.08.00` # Description - A HASP button displays the current temperature from a weather provider on the right with a dynamic thermometer icon on the left and (optional) colors. + A HASPone button displays the current temperature from a weather provider on the right with a dynamic thermometer icon on the left and (optional) colors. ![Preview](https://raw.githubusercontent.com/HASwitchPlate/HASPone/main/images/hasp_Display_Weather_Temperature_with_Icon_and_Colors.png) - ## HASP Page and Button reference + ## HASPone Page and Button reference
@@ -46,16 +46,16 @@ blueprint: domain: automation input: haspdevice: - name: "HASP Device" - description: "Select the HASP device" + name: "HASPone Device" + description: "Select the HASPone device" selector: device: integration: mqtt manufacturer: "HASwitchPlate" model: "HASPone v1.0.0" hasppage: - name: "HASP Page" - description: "Select the HASP page (1-3) for the temperature. Refer to the HASP Page and Button reference above." + name: "HASPone Page" + description: "Select the HASPone page (1-3) for the temperature. Refer to the HASPone Page and Button reference above." default: 1 selector: number: @@ -64,8 +64,8 @@ blueprint: mode: slider unit_of_measurement: page haspbutton: - name: "HASP Button" - description: "Select the HASP button (4-7) for the temperature. Refer to the HASP Page and Button reference above." + name: "HASPone Button" + description: "Select the HASPone button (4-7) for the temperature. Refer to the HASPone Page and Button reference above." default: 4 selector: number: @@ -191,7 +191,7 @@ max_exceeded: silent variables: haspname: >- {%- for entity in device_entities(haspdevice) -%} - {%- if entity|regex_search("^sensor\.") -%} + {%- 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 -%} @@ -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 -%} @@ -338,7 +338,7 @@ trigger_variables: haspdevice: !input haspdevice haspname: >- {%- for entity in device_entities(haspdevice) -%} - {%- if entity|regex_search("^sensor\.") -%} + {%- 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 -%} @@ -355,21 +355,21 @@ trigger_variables: unselectedbgtopic: '{{ "hasp/" ~ haspname ~ "/light/unselectedbackgroundcolor/rgb" }}' trigger: - - platform: state + - trigger: state entity_id: !input weather_provider - - platform: template + - trigger: template value_template: "{{ is_state(haspsensor, 'ON') }}" - - platform: homeassistant + - trigger: homeassistant event: start - - platform: mqtt + - trigger: mqtt topic: "{{jsontopic}}" - - platform: mqtt + - trigger: mqtt topic: "{{selectedfgtopic}}" - - platform: mqtt + - trigger: mqtt topic: "{{selectedbgtopic}}" - - platform: mqtt + - trigger: mqtt topic: "{{unselectedfgtopic}}" - - platform: mqtt + - trigger: mqtt topic: "{{unselectedbgtopic}}" condition: @@ -379,7 +379,7 @@ condition: action: - choose: ######################################################################### - # RUN ACTIONS or Home Assistant Startup or HASP Connect + # RUN ACTIONS or Home Assistant Startup or HASPone Connect # Apply styles, place text, and then place icon if our target page is currently active - conditions: - condition: template diff --git a/Home_Assistant/blueprints/hasp_Perform_Action.yaml b/Home_Assistant/blueprints/hasp_Perform_Action.yaml index d191d63..c9dc2f8 100644 --- a/Home_Assistant/blueprints/hasp_Perform_Action.yaml +++ b/Home_Assistant/blueprints/hasp_Perform_Action.yaml @@ -1,12 +1,14 @@ blueprint: - name: "HASP p[x].b[y] performs an action when pressed" + name: "HASPone p[x].b[y] performs an action when pressed" description: | + ## Blueprint Version: `1.08.00` + # Description - A button on the HASP will perform an action when pressed. Can be combined on a button with another blueprint which displays text. + A button on the HASPone will perform an action when pressed. Can be combined on a button with another blueprint which displays text. - ### HASP Page and Button reference + ### HASPone Page and Button reference
@@ -27,16 +29,16 @@ blueprint: domain: automation input: haspdevice: - name: "HASP Device" - description: "Select the HASP device" + name: "HASPone Device" + description: "Select the HASPone 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 automation. Refer to the HASP Page and Button reference above." + name: "HASPone Page" + description: "Select the HASPone page (1-11) for this automation. Refer to the HASPone Page and Button reference above." default: 1 selector: number: @@ -45,8 +47,8 @@ blueprint: mode: slider unit_of_measurement: page haspbutton: - name: "HASP Button" - description: "Select the HASP button for this automation. Refer to the HASP Page and Button reference above." + name: "HASPone Button" + description: "Select the HASPone button for this automation. Refer to the HASPone Page and Button reference above." default: 4 selector: number: @@ -68,7 +70,7 @@ variables: haspdevice: !input haspdevice haspname: >- {%- for entity in device_entities(haspdevice) -%} - {%- if entity|regex_search("^sensor\.") -%} + {%- 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 -%} @@ -79,7 +81,7 @@ trigger_variables: haspdevice: !input haspdevice haspname: >- {%- for entity in device_entities(haspdevice) -%} - {%- if entity|regex_search("^sensor\.") -%} + {%- 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 -%} @@ -95,8 +97,8 @@ trigger_variables: haspobject: '{{ "p[" ~ hasppage ~ "].b[" ~ haspbutton ~ "]" }}' buttonjsonpayload: '{"event_type":"button_short_press","event":"{{haspobject}}","value":"ON"}' -trigger: - - platform: mqtt +triggers: + - trigger: mqtt topic: "{{jsontopic}}" payload: "{{buttonjsonpayload}}" diff --git a/Home_Assistant/blueprints/hasp_Remove_MQTT_Discovery_Devices.yaml b/Home_Assistant/blueprints/hasp_Remove_MQTT_Discovery_Devices.yaml index 5419246..5f18c9e 100644 --- a/Home_Assistant/blueprints/hasp_Remove_MQTT_Discovery_Devices.yaml +++ b/Home_Assistant/blueprints/hasp_Remove_MQTT_Discovery_Devices.yaml @@ -1,11 +1,17 @@ blueprint: - name: "HASP Remove MQTT discovery messages" - description: "Press RUN ACTIONS to remove retained MQTT discovery messages for a decommissioned HASP" + name: "HASPone Remove MQTT discovery messages" + description: | + + ## Blueprint Version: `1.08.00` + + # Description + + Press RUN ACTIONS to remove retained MQTT discovery messages for a decommissioned HASPone device domain: automation input: haspname: - name: "HASP device name to remove" - description: "Enter the name of the HASP device to remove the MQTT discovery messages" + name: "HASPone device name to remove" + description: "Enter the name of the HASPone device to remove the MQTT discovery messages" mode: single max_exceeded: silent diff --git a/Home_Assistant/blueprints/hasp_Theme_Dark_on_Light.yaml b/Home_Assistant/blueprints/hasp_Theme_Dark_on_Light.yaml index ce6faf2..cb8ec36 100644 --- a/Home_Assistant/blueprints/hasp_Theme_Dark_on_Light.yaml +++ b/Home_Assistant/blueprints/hasp_Theme_Dark_on_Light.yaml @@ -1,18 +1,20 @@ blueprint: - name: "HASP Theme Dark on Light" + name: "HASPone Theme Dark on Light" description: | + ## Blueprint Version: `1.08.00` + ## Description - Press RUN ACTIONS to apply the theme Dark on Light to the selected HASP device + Press RUN ACTIONS to apply the theme Dark on Light to the selected HASPone device ![Preview](https://raw.githubusercontent.com/HASwitchPlate/HASPone/main/images/hasp_Theme_Dark_on_Light.png) domain: automation input: haspdevice: - name: "HASP Device" - description: "Select the HASP device" + name: "HASPone Device" + description: "Select the HASPone device" selector: device: integration: mqtt diff --git a/Home_Assistant/blueprints/hasp_Theme_Light_on_BlueDark.yaml b/Home_Assistant/blueprints/hasp_Theme_Light_on_BlueDark.yaml index ac3c4b7..25d7414 100644 --- a/Home_Assistant/blueprints/hasp_Theme_Light_on_BlueDark.yaml +++ b/Home_Assistant/blueprints/hasp_Theme_Light_on_BlueDark.yaml @@ -1,18 +1,20 @@ blueprint: - name: "HASP Theme Light on Dark Blue" + name: "HASPone Theme Light on Dark Blue" description: | + ## Blueprint Version: `1.08.00` + ## Description - Press RUN ACTIONS to apply the theme Light on Dark Blue to the selected HASP device + Press RUN ACTIONS to apply the theme Light on Dark Blue to the selected HASPone device ![Preview](https://raw.githubusercontent.com/HASwitchPlate/HASPone/main/images/hasp_Theme_Light_on_Dark.png) domain: automation input: haspdevice: - name: "HASP Device" - description: "Select the HASP device" + name: "HASPone Device" + description: "Select the HASPone device" selector: device: integration: mqtt @@ -50,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 18338a7..8087ad0 100644 --- a/Home_Assistant/blueprints/hasp_Theme_Light_on_Dark.yaml +++ b/Home_Assistant/blueprints/hasp_Theme_Light_on_Dark.yaml @@ -1,18 +1,20 @@ blueprint: - name: "HASP Theme Light on Dark" + name: "HASPone Theme Light on Dark" description: | + ## Blueprint Version: `1.08.00` + ## Description - Press RUN ACTIONS to apply the theme Light on Dark to the selected HASP device + Press RUN ACTIONS to apply the theme Light on Dark to the selected HASPone device ![Preview](https://raw.githubusercontent.com/HASwitchPlate/HASPone/main/images/hasp_Theme_Light_on_Dark.png) domain: automation input: haspdevice: - name: "HASP Device" - description: "Select the HASP device" + name: "HASPone Device" + description: "Select the HASPone device" selector: device: integration: mqtt diff --git a/Nextion_HMI/HASwitchPlate-Discovery-Inverted.tft b/Nextion_HMI/HASwitchPlate-Discovery-Inverted.tft new file mode 100644 index 0000000..dd79e62 Binary files /dev/null and b/Nextion_HMI/HASwitchPlate-Discovery-Inverted.tft differ diff --git a/Nextion_HMI/HASwitchPlate-Discovery.tft b/Nextion_HMI/HASwitchPlate-Discovery.tft new file mode 100644 index 0000000..b775477 Binary files /dev/null and b/Nextion_HMI/HASwitchPlate-Discovery.tft differ diff --git a/Nextion_HMI/HASwitchPlate.HMI b/Nextion_HMI/HASwitchPlate.HMI index 7091a17..78a2ac0 100644 Binary files a/Nextion_HMI/HASwitchPlate.HMI and b/Nextion_HMI/HASwitchPlate.HMI differ diff --git a/Nextion_HMI/HASwitchPlate.tft b/Nextion_HMI/HASwitchPlate.tft index 3108873..568b4cf 100644 Binary files a/Nextion_HMI/HASwitchPlate.tft and b/Nextion_HMI/HASwitchPlate.tft differ diff --git a/Nextion_HMI/README.md b/Nextion_HMI/README.md index f6b19b4..39cb7f8 100644 --- a/Nextion_HMI/README.md +++ b/Nextion_HMI/README.md @@ -9,7 +9,10 @@ Please [check the Nextion HMI documentation](../Documentation/02_Nextion_HMI.md) ## Nextion Files Explained * **[HASwitchPlate.hmi](HASwitchPlate.hmi)** This is the "source" file which you can modify in the [Nextion editor](https://nextion.itead.cc/resource/download/nextion-editor/). If you want to make your own Nextion HMI, I'd recommend starting with this file, keeping Page 0 (`p0`), then start your own design on pages 1+. -* **[HASwitchPlate.tft](HASwitchPlate.tft)** This is the compiled Nextion firmware for the HASwitchPlate usable on a standard Nextion 2.4" LCD, model `NX3224T024_011R` -* **[HASwitchPlate-Enhanced.tft](HASwitchPlate-Enhanced.tft)** This is the compiled Nextion firmware for the HASwitchPlate 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.tft](HASwitchPlate.tft)** This is the compiled Nextion firmware for the HASPone usable on a Basic series Nextion 2.4" LCD, model `NX3224T024_011R` +* **[HASwitchPlate-Inverted.tft](HASwitchPlate-Inverted.tft)** Basic series firmware but inverted, usable if the viewing angle on your display works better when mounted upside-down. +* **[HASwitchPlate-Discovery.tft](HASwitchPlate-Discovery.tft)** This is the compiled Nextion firmware for the HASPone usable on a Discovery series Nextion 2.4" LCD, model `NX3224F024_011R`. This is a drop-in replacement for the Basic series, might be cheaper or more readily available than the Basic, and is fully supported by the HASPone project. +* **[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 HASwitchPlate 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/PCB/README.md b/PCB/README.md index b243684..4e2ece4 100644 --- a/PCB/README.md +++ b/PCB/README.md @@ -4,6 +4,6 @@ Above you'll find [KiCad](http://kicad-pcb.org/) [schematic](https://github.com/ I had these manufactured by [AllPCB](https://www.allpcb.com/?Mb_InviteId=34099) and I'm very happy with the results. The quality has been perfect and their shipping time to the US has been under a week across several orders. [If you sign up through this link](https://www.allpcb.com/?Mb_InviteId=34099) they credit me some cash that I can use for future projects which helps me out a bit. -If you aren't looking to buy a zillion boards, I may have some ready for US orders in [my Tindie store](https://www.tindie.com/products/luma/ha-switchplate-hasp-pcb/). +If you aren't looking to buy a zillion boards, I may have some ready for US orders in [my Etsy store](https://www.etsy.com/listing/1177721322/haspone-pcb). ![HA SwitchPlate PCB](https://github.com/aderusha/HASwitchPlate/blob/master/Documentation/Images/HASP_PCB_Front_and_Back.png?raw=true) diff --git a/README.md b/README.md index 9eda7e7..e9f39b6 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # HA SwitchPlate HASPone -The HASPone is a DIY touchscreen controller you can mount into a [standard North American work box](https://www.nema.org/Standards/ComplimentaryDocuments/NEMA%20WD%206%20-%20Dimensions%20for%20Wiring%20Devices%20-%20Excerpt.pdf). It connects to your home automation system over Wifi+MQTT to display useful information and to control your smart devices. The result is an attractive and highly-customizable home controller you can build yourself! +The HASPone is a DIY touchscreen controller you can mount into a [standard North American work box](https://archive.org/details/NEMA-WD-6-2016). It connects to your home automation system over Wifi+MQTT to display useful information and to control your smart devices. The result is an attractive and highly-customizable home controller you can build yourself! ![HA SwitchPlate Models](https://github.com/HASwitchPlate/HASPone/blob/main/images/HASwitchPlate_Three_Model_Variations.png?raw=true) @@ -8,9 +8,9 @@ The HASPone is a DIY touchscreen controller you can mount into a [standard North ![Scene controller](https://github.com/HASwitchPlate/HASPone/blob/main/images/HASwitchPlate_Demo_SceneController.png?raw=true) ![Status display](https://github.com/HASwitchPlate/HASPone/blob/main/images/HASwitchPlate_Demo_Status.png?raw=true) ![Media control](https://github.com/HASwitchPlate/HASPone/blob/main/images/HASwitchPlate_Demo_Media.png?raw=true) ![Alarm Panel](https://github.com/HASwitchPlate/HASPone/blob/main/images/HASwitchPlate_Demo_AlarmPanel.png?raw=true) ![Slider/Dimmer Controls](https://github.com/HASwitchPlate/HASPone/blob/main/images/HASwitchPlate_Demo_Dimmers.png?raw=true) ![HVAC Controls](https://github.com/HASwitchPlate/HASPone/blob/main/images/HASwitchPlate_Demo_HVAC.png?raw=true) ![Light toggles](https://github.com/HASwitchPlate/HASPone/blob/main/images/HASwitchPlate_Demo_LightToggles.png?raw=true) ![Fan controller](https://github.com/HASwitchPlate/HASPone/blob/main/images/HASwitchPlate_Demo_FanControls.png?raw=true) ![WiFi Setup](https://github.com/HASwitchPlate/HASPone/blob/main/images/WiFi_Config_0.png?raw=true) -## [Purchase an assembled unit](https://www.tindie.com/products/luma/ha-switchplate-hasp-single-wide-assembled/) +## [Purchase an assembled unit](https://www.etsy.com/listing/1191709235/haspone-haswitchplate-touchscreen-home) -This build requires some specialist skills and tools. [You can buy an assembled device here](https://www.tindie.com/products/luma/ha-switchplate-hasp-single-wide-assembled/) if you want to get started without picking up a soldering iron. +This build requires some specialist skills and tools. [You can buy an assembled device here](https://www.etsy.com/listing/1191709235/haspone-haswitchplate-touchscreen-home) if you want to get started without picking up a soldering iron. [Buy the PCB here](https://www.etsy.com/listing/1177721322/haspone-pcb) if you want to safely assemble your own HASPone. ## [Build your own HASPone](https://github.com/HASwitchPlate/HASPone/wiki/Building-your-own-HASPone) @@ -26,7 +26,7 @@ Here's a quick look at the setup process: ## Have a question? -Come talk to us in [the HASPone discussion forum](https://github.com/HASwitchPlate/HASPone/discussions) or [chat with us on Discord](https://discord.gg/FvDu8SXSvJ). +Come talk to us in [the HASPone discussion forum](https://github.com/HASwitchPlate/HASPone/discussions) or [chat with us on Discord](https://haswitchplate.com/discord). ## [Buy me a coffee](https://www.buymeacoffee.com/gW5rPpsKR) diff --git a/esphome/haspone.yaml b/esphome/haspone.yaml new file mode 100644 index 0000000..a153071 --- /dev/null +++ b/esphome/haspone.yaml @@ -0,0 +1,96 @@ +# Example ESPhome configuration for use with the HASPone hardware +# +# NOTE ON UART: The HASPone PCB connects the Nextion to GPIO2 (TX) and GPIO13 (RX). +# These pins span two different hardware UARTs (UART1 TX-only and UART0-swapped RX), +# so ESPHome must use SoftwareSerial here. The Arduino firmware works around this by +# using Serial1 for TX and Serial.swap() for RX independently - a trick ESPHome's +# Nextion component cannot replicate since it requires a single UART instance. +# +# TFT updates over SoftwareSerial may be unreliable at high baud rates. For Nextion +# firmware flashing, use the Nextion Editor with a USB-TTL adapter directly, or use +# the Arduino firmware's built-in OTA update mechanism. +# +# Logging is sent over the network (API + web_server) rather than USB serial, since +# UART0 default pins (GPIO1/GPIO3) conflict with the Nextion after boot. + +substitutions: + device_name: "haspone" + friendly_name: "HASPone hardware for ESPhome" + project_version: "0.0.2" + +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 + +# Disable serial logging - UART0 default pins conflict with Nextion, +# and there's no spare hardware UART for USB output. +# Logs are available via the web UI (http://) and ESPHome dashboard (API). +logger: + baud_rate: 0 + +api: + +ota: + - platform: esphome + +wifi: + ssid: !secret wifi_ssid + password: !secret wifi_password + +# Web UI provides device logs, controls, and status at http:// +web_server: + port: 80 + +# HASPone switch controls power to the Nextion via a MOSFET on the PCB. +switch: + - platform: gpio + id: switch_lcdpower + name: "${friendly_name} Nextion Power" + pin: D6 #GPIO12 + restore_mode: ALWAYS_ON + internal: false + +# UART for Nextion communication. +# GPIO2 (UART1 HW TX) + GPIO13 (UART0-swapped HW RX) = SoftwareSerial in ESPHome. +# 115200 is the most reliable baud rate for SoftwareSerial on ESP8266. +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); 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/images/HASwitchPlate_Demo_3x3.png b/images/HASwitchPlate_Demo_3x3.png index 22311b9..3b65429 100644 Binary files a/images/HASwitchPlate_Demo_3x3.png and b/images/HASwitchPlate_Demo_3x3.png differ diff --git a/images/HASwitchPlate_Demo_Status.png b/images/HASwitchPlate_Demo_Status.png index fc3d491..5c5f3de 100644 Binary files a/images/HASwitchPlate_Demo_Status.png and b/images/HASwitchPlate_Demo_Status.png differ diff --git a/images/HASwitchPlate_Three_Model_Variations.png b/images/HASwitchPlate_Three_Model_Variations.png index 5ac228a..df9feb3 100644 Binary files a/images/HASwitchPlate_Three_Model_Variations.png and b/images/HASwitchPlate_Three_Model_Variations.png differ diff --git a/images/HASwitchPlate_rear_box_dimensions.jpg b/images/HASwitchPlate_rear_box_dimensions.jpg new file mode 100644 index 0000000..3ab2fb6 Binary files /dev/null and b/images/HASwitchPlate_rear_box_dimensions.jpg differ diff --git a/images/hasp_Display_Calendar_with_Icon.png b/images/hasp_Display_Calendar_with_Icon.png index bf88166..ee358f8 100644 Binary files a/images/hasp_Display_Calendar_with_Icon.png and b/images/hasp_Display_Calendar_with_Icon.png differ diff --git a/images/hasp_Display_Clock_with_Icon.png b/images/hasp_Display_Clock_with_Icon.png index f7205cc..f53fe6f 100644 Binary files a/images/hasp_Display_Clock_with_Icon.png and b/images/hasp_Display_Clock_with_Icon.png differ diff --git a/images/hasp_Display_Value_with_Icon_and_Colors.png b/images/hasp_Display_Value_with_Icon_and_Colors.png new file mode 100644 index 0000000..d3ee248 Binary files /dev/null and b/images/hasp_Display_Value_with_Icon_and_Colors.png differ diff --git a/images/hasp_Display_Weather_Condition_Icon_Only.png b/images/hasp_Display_Weather_Condition_Icon_Only.png new file mode 100644 index 0000000..a147ec2 Binary files /dev/null and b/images/hasp_Display_Weather_Condition_Icon_Only.png differ diff --git a/images/hasp_Display_Weather_Condition_with_Icon.png b/images/hasp_Display_Weather_Condition_with_Icon.png index 4de9cfe..407c52b 100644 Binary files a/images/hasp_Display_Weather_Condition_with_Icon.png and b/images/hasp_Display_Weather_Condition_with_Icon.png differ diff --git a/images/hasp_Display_Weather_Temperature_with_Icon_and_Colors.png b/images/hasp_Display_Weather_Temperature_with_Icon_and_Colors.png index f7a68ed..3942d01 100644 Binary files a/images/hasp_Display_Weather_Temperature_with_Icon_and_Colors.png and b/images/hasp_Display_Weather_Temperature_with_Icon_and_Colors.png differ diff --git a/images/hasp_Theme_Dark_on_Light.png b/images/hasp_Theme_Dark_on_Light.png index d13ddcd..7a9c7a6 100644 Binary files a/images/hasp_Theme_Dark_on_Light.png and b/images/hasp_Theme_Dark_on_Light.png differ diff --git a/images/hasp_Theme_Light_on_BlueDark.png b/images/hasp_Theme_Light_on_BlueDark.png new file mode 100644 index 0000000..e6767d9 Binary files /dev/null and b/images/hasp_Theme_Light_on_BlueDark.png differ diff --git a/images/hasp_Theme_Light_on_Dark.png b/images/hasp_Theme_Light_on_Dark.png index bdb5654..111276e 100644 Binary files a/images/hasp_Theme_Light_on_Dark.png and b/images/hasp_Theme_Light_on_Dark.png differ diff --git a/release.md b/release.md new file mode 100644 index 0000000..00513bd --- /dev/null +++ b/release.md @@ -0,0 +1,106 @@ +# HASPone v1.09 Release Notes + +## Home Assistant Update Integration + +HASPone now registers as an updatable device in Home Assistant. When a firmware update is available, it appears in the HA **Settings > Updates** dashboard alongside your other device updates. + +- **ESP8266 firmware** and **Nextion LCD firmware** are tracked as separate update entities +- Click **Install** directly from Home Assistant to trigger the OTA update +- Release notes link is included in each update card, configurable per-release via `version.json` +- Update availability is checked automatically every 12 hours + +## Fix for breaking changes in Home Assistant MQTT entity naming + +Big thanks to @SylvainGa for the 1.07 and 1.08 releases, fixing a breaking change in Home Assistant: https://github.com/HASwitchPlate/HASPone/commit/68df52da6bd4ba3a4f2c9e98dcf1b7bcd363443b + +**All blueprints will need to be updated!** - make sure to copy down all files from `Home_Assistant/blueprints/*.yaml` into your Home Assistant installation. + +## Bug Fixes + +### Fixed Nextion ACK timeout logic + +The ACK wait loops in `nextionSetAttr()` and `nextionGetAttr()` used `||` (OR) with an inverted timeout comparison, meaning they would exit immediately instead of waiting for the ACK response. Changed to `&&` (AND) with corrected comparison direction so the loop properly waits for either an ACK or a timeout. + +### Fixed beep on/off state inversion + +The beep feedback had on and off states swapped: `analogWrite(beepPin, 254)` was called during the "off" phase and `analogWrite(beepPin, 0)` during the "on" phase. Corrected so the beep actually sounds during the on interval. + +### Fixed WiFi password display in web UI + +The web configuration page checked `mqttUser` instead of `mqttPassword` when deciding whether to show the password placeholder. The MQTT password field would appear empty even when a password was saved. + +### Fixed page restore after LCD reboot + +`nextionReset()` and `espWifiConnect()` compared `nextionActivePage` with a truthy check, which treated page 0 as "no page set." Changed to `>= 0` so page 0 is correctly restored. + +### Fixed debugPrint() brace scoping + +`debugPrint()` had a misplaced opening brace that put `Serial.print(debugText)` outside the `if (debugSerialEnabled)` block, causing serial output even when debug was disabled. + +## Improvements + +### mDNS actually starts now + +Added the missing `MDNS.begin(haspNode)` call. Without it, the mDNS service registration was configured but the responder itself was never started, so the device was not discoverable on the local network. + +### mDNS now advertises MAC address and MQTT server + +Added `mac` and `mqtt_server` TXT records to the mDNS service advertisement, making device identification easier on the local network. + +### WiFi reconnection hardened + +- `WiFi.hostname()`, `WiFi.setAutoReconnect(true)`, and `WiFi.setSleepMode(WIFI_NONE_SLEEP)` are now set on both initial connection and reconnection +- `espWifiReconnect()` uses saved credentials from WiFiManager when no hardcoded SSID is configured, instead of passing empty strings to `WiFi.begin()` +- WiFi persistence disabled during reconnection to avoid unnecessary flash writes +- All WiFi settings are reapplied after reconnection + +### OTA update reliability improved + +- MQTT client, TLS buffers, telnet, and web server are disconnected/stopped before starting ESP OTA to free memory +- HTTPS OTA buffer increased from 512 to 4096 bytes for more reliable downloads +- Removed manual URL parsing — `ESPhttpUpdate` handles it with `HTTPC_FORCE_FOLLOW_REDIRECTS` + +### Update check rewritten for Cloudflare compatibility + +`updateCheck()` was rewritten to use raw HTTP/1.0 requests with explicit content-length parsing instead of `HTTPClient`. This resolves failures when `version.json` is served behind Cloudflare's edge network. + +### Firmware download URLs switched to HTTP + +Default firmware URLs changed from `https://` to `http://` to reduce memory pressure during OTA downloads on the ESP8266. Previously HTTPS was used through a cloud-hosted VM which would proxy from GitHub, that now runs through CloudFlare. While this does change from HTTPS to HTTP, the security posture doesn't change as the ESP8266 does not have the capacity to pull in and validate the full cert chain so it never did actually check any part of that in previous releases. The update check itself still uses HTTPS. + +### Debug serial output optimized + +`SoftwareSerial debugSerial` is now a global instance instead of being constructed and destroyed on every `debugPrintln()` / `debugPrint()` / `debugPrintCrash()` call. Reduces heap churn during debug output. + +### OTA progress display deduplicated + +`nextionUpdateProgress()` now only sends display updates when the percentage actually changes, avoiding redundant serial commands to the Nextion during firmware uploads. + +### PlatformIO build configuration updated + +Added `monitor_speed = 115200` and `upload_speed = 921600` to `platformio.ini` for faster development iteration. + +## ESPHome Example Updated + +The ESPHome example configuration (`esphome/haspone.yaml`) has been updated with: + +- Serial logging disabled (`baud_rate: 0`) since the UART pins conflict with the Nextion — logs are available via the web UI and ESPHome dashboard instead +- Updated `ota:` to current ESPHome syntax (`platform: esphome`) +- Detailed header comments explaining the hardware UART constraints and why SoftwareSerial is required on this PCB +- Version bumped to 0.0.2 + +## version.json Changes + +All firmware entries now include a `release_url` field pointing to the GitHub releases page. The device reads this on each update check and passes it to Home Assistant so the update card links to the correct release notes. + +## Files Changed + +| File | Description | +|------|-------------| +| `Home_Assistant/blueprints/*.yaml` | New blueprints as of v1.08 for breaking changes in Home Assistant | +| `Arduino_Sketch/HASwitchPlate/HASwitchPlate.cpp` | All firmware changes above | +| `Arduino_Sketch/HASwitchPlate.ino.d1_mini.bin` | Compiled firmware binary | +| `Arduino_Sketch/debug/HASwitchPlate.ino.d1_mini.elf` | Debug symbols | +| `Arduino_Sketch/platformio.ini` | Build config updates | +| `esphome/haspone.yaml` | ESPHome example improvements | +| `update/version.json` | Updated paths, added release_url | diff --git a/update/README.md b/update/README.md index f55feeb..99de1d7 100644 --- a/update/README.md +++ b/update/README.md @@ -2,4 +2,15 @@ Files here are used for the auto-update routine which will periodically check GitHub for new firmware versions. -`version-insecure.json` utilizes an externally-hosted proxy to strip off SSL in order to support versions of HASP prior to the introduction of SSL support in v0.41. +--- + +## Legacy Releases + +If you need to downgrade for whatever reason, paste the link below into the "Update ESP8266 from URL" dialog under "Update Firmware". + +* **HASPone 1.05** `https://raw.githubusercontent.com/HASwitchPlate/HASPone/570eefcb901c8eb40947a5fa53b8760ae21612f2/Arduino_Sketch/HASwitchPlate.ino.d1_mini.bin` +* **HASPone 1.04** `https://raw.githubusercontent.com/HASwitchPlate/HASPone/f20474d8f705f9dc08ac3c8c1d5c3de324d1e6fe/Arduino_Sketch/HASwitchPlate.ino.d1_mini.bin` +* **HASPone 1.03** `https://raw.githubusercontent.com/HASwitchPlate/HASPone/2a8df65334f8b694676e4e2d75cdc7e6daac9b4e/Arduino_Sketch/HASwitchPlate.ino.d1_mini.bin` +* **HASPone 1.02** `https://raw.githubusercontent.com/HASwitchPlate/HASPone/2098a87c3b6910992a1b425628f7b1b8735487db/Arduino_Sketch/HASwitchPlate.ino.d1_mini.bin` +* **HASPone 1.01** `https://raw.githubusercontent.com/HASwitchPlate/HASPone/a1ca2c244bc15feb0360132d86a3048908be5aa6/Arduino_Sketch/HASwitchPlate.ino.d1_mini.bin` +* **HASPone 1.00** `https://raw.githubusercontent.com/HASwitchPlate/HASPone/bdaa5e7d906666df31adba7b503f14c2e3ccaeb5/Arduino_Sketch/HASwitchPlate.ino.d1_mini.bin` \ No newline at end of file diff --git a/update/version.json b/update/version.json index fe05073..aa8f4fe 100644 --- a/update/version.json +++ b/update/version.json @@ -1,18 +1,22 @@ { - "d1_mini": { - "version": "1.03", - "firmware": "https://raw.githubusercontent.com/HASwitchPlate/HASPone/main/Arduino_Sketch/HASwitchPlate.ino.d1_mini.bin" - }, - "NX3224T024_011R": { - "version": 3, - "firmware": "https://raw.githubusercontent.com/HASwitchPlate/HASPone/main/Nextion_HMI/HASwitchPlate.tft" - }, - "NX3224K024_011R": { - "version": 3, - "firmware": "https://raw.githubusercontent.com/HASwitchPlate/HASPone/main/Nextion_HMI/HASwitchPlate-Enhanced.tft" - }, - "TJC3224T024_011R": { - "version": 3, - "firmware": "https://raw.githubusercontent.com/HASwitchPlate/HASPone/main/Nextion_HMI/HASwitchPlate-TJC.tft" - } -} \ No newline at end of file + "d1_mini": { + "version": "1.09", + "firmware": "http://haswitchplate.com/update/HASwitchPlate.ino.d1_mini.bin", + "release_url": "https://github.com/HASwitchPlate/HASPone/releases/latest" + }, + "NX3224T024_011R": { + "version": 3, + "firmware": "http://haswitchplate.com/update/HASwitchPlate.tft", + "release_url": "https://github.com/HASwitchPlate/HASPone/releases/latest" + }, + "NX3224K024_011R": { + "version": 3, + "firmware": "http://haswitchplate.com/update/HASwitchPlate-Enhanced.tft", + "release_url": "https://github.com/HASwitchPlate/HASPone/releases/latest" + }, + "NX3224F024_011R": { + "version": 3, + "firmware": "http://haswitchplate.com/update/HASwitchPlate-Discovery.tft", + "release_url": "https://github.com/HASwitchPlate/HASPone/releases/latest" + } +}