/
HASwitchPlate.ino
3888 lines (3665 loc) · 184 KB
/
HASwitchPlate.ino
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
////////////////////////////////////////////////////////////////////////////////////////////////////
// _____ _____ _____ _____
// | | | _ | __| _ |
// | | |__ | __|
// |__|__|__|__|_____|__|
// 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 <FS.h>
#include <EEPROM.h>
#include <EspSaveCrash.h>
#include <ESP8266WiFi.h>
#include <ESP8266mDNS.h>
#include <WiFiUdp.h>
#include <ArduinoOTA.h>
#include <DNSServer.h>
#include <ESP8266HTTPClient.h>
#include <ESP8266WebServer.h>
#include <ESP8266httpUpdate.h>
#include <ESP8266HTTPUpdateServer.h>
#include <WiFiManager.h>
#include <ArduinoJson.h>
#include <MQTT.h>
#include <SoftwareSerial.h>
#include <ESP8266Ping.h>
////////////////////////////////////////////////////////////////////////////////////////////////////
// These defaults may be overwritten with values saved by the web interface
char wifiSSID[32] = "";
char wifiPass[64] = "";
char mqttServer[128] = "";
char mqttPort[6] = "1883";
char mqttUser[128] = "";
char mqttPassword[128] = "";
char mqttFingerprint[60] = "";
char haspNode[16] = "plate01";
char groupName[16] = "plates";
char hassDiscovery[128] = "homeassistant";
char configUser[32] = "admin";
char configPassword[32] = "";
char motionPinConfig[3] = "0";
char nextionBaud[7] = "115200";
////////////////////////////////////////////////////////////////////////////////////////////////////
const float haspVersion = 1.05; // Current HASPone software release version
const uint16_t mqttMaxPacketSize = 2048; // Size of buffer for incoming MQTT message
byte nextionReturnBuffer[128]; // Byte array to pass around data coming from the panel
uint8_t nextionReturnIndex = 0; // Index for nextionReturnBuffer
int8_t nextionActivePage = -1; // Track active LCD page
bool lcdConnected = false; // Set to true when we've heard something from the LCD
const char wifiConfigPass[9] = "hasplate"; // First-time config WPA2 password
const char wifiConfigAP[14] = "HASwitchPlate"; // First-time config SSID
bool shouldSaveConfig = false; // Flag to save json config to SPIFFS
bool nextionReportPage0 = false; // If false, don't report page 0 sendme
const unsigned long updateCheckInterval = 43200000; // Time in msec between update checks (12 hours)
unsigned long updateCheckTimer = updateCheckInterval; // Timer for update check
unsigned long updateCheckFirstRun = 60000; // First-run check offset
bool updateEspAvailable = false; // Flag for update check to report new ESP FW version
float updateEspAvailableVersion; // Float to hold the new ESP FW version number
bool updateLcdAvailable = false; // Flag for update check to report new LCD FW version
unsigned long debugTimer = 0; // Clock for debug performance profiling
bool debugSerialEnabled = true; // Enable USB serial debug output
const unsigned long debugSerialBaud = 115200; // Desired baud rate for serial debug output
bool debugTelnetEnabled = false; // Enable telnet debug output
bool nextionBufferOverrun = false; // Set to true if an overrun error was encountered
bool nextionAckEnable = false; // Wait for each Nextion command to be acked before continuing
bool nextionAckReceived = false; // Ack was received
bool rebootOnp0b1 = false; // When true, reboot device on button press of p[0].b[1]
const unsigned long nextionAckTimeout = 1000; // Timeout to wait for an ack before throwing error
unsigned long nextionAckTimer = 0; // Timer to track Nextion ack
const unsigned long telnetInputMax = 128; // Size of user input buffer for user telnet session
bool motionEnabled = false; // Motion sensor is enabled
bool mdnsEnabled = true; // mDNS enabled
bool ignoreTouchWhenOff = false; // Ignore touch events when backlight is off and instead send mqtt msg
bool beepEnabled = false; // Keypress beep enabled
unsigned long beepOnTime = 1000; // milliseconds of on-time for beep
unsigned long beepOffTime = 1000; // milliseconds of off-time for beep
unsigned int beepCounter; // Count the number of beeps
uint8_t beepPin = D2; // define beep pin output
uint8_t motionPin = 0; // GPIO input pin for motion sensor if connected and enabled
bool motionActive = false; // Motion is being detected
const unsigned long motionLatchTimeout = 1000; // Latch time for motion sensor
const unsigned long motionBufferTimeout = 100; // Trigger threshold time for motion sensor
unsigned long lcdVersion = 0; // Int to hold current LCD FW version number
unsigned long updateLcdAvailableVersion; // Int to hold the new LCD FW version number
bool lcdVersionQueryFlag = false; // Flag to set if we've queried lcdVersion
const String lcdVersionQuery = "p[0].b[2].val"; // Object ID for lcdVersion in HMI
uint8_t lcdBacklightDim = 0; // Backlight dimmer value
bool lcdBacklightOn = 0; // Backlight on/off
bool lcdBacklightQueryFlag = false; // Flag to set if we've queried lcdBacklightDim
bool startupCompleteFlag = false; // Startup process has completed
const unsigned long statusUpdateInterval = 300000; // Time in msec between publishing MQTT status updates (5 minutes)
unsigned long statusUpdateTimer = 0; // Timer for status update
const unsigned long connectTimeout = 300; // Timeout for WiFi and MQTT connection attempts in seconds
const unsigned long reConnectTimeout = 60; // Timeout for WiFi reconnection attempts in seconds
byte espMac[6]; // Byte array to store our MAC address
bool mqttTlsEnabled = false; // Enable MQTT client TLS connections
bool mqttPingCheck = false; // MQTT broker ping check result
bool mqttPortCheck = false; // MQTT broke port check result
String mqttClientId; // Auto-generated MQTT ClientID
String mqttGetSubtopic; // MQTT subtopic for incoming commands requesting .val
String mqttStateTopic; // MQTT topic for outgoing panel interactions
String mqttStateJSONTopic; // MQTT topic for outgoing panel interactions in JSON format
String mqttCommandTopic; // MQTT topic for incoming panel commands
String mqttGroupCommandTopic; // MQTT topic for incoming group panel commands
String mqttStatusTopic; // MQTT topic for publishing device connectivity state
String mqttSensorTopic; // MQTT topic for publishing device information in JSON format
String mqttLightCommandTopic; // MQTT topic for incoming panel backlight on/off commands
String mqttLightStateTopic; // MQTT topic for outgoing panel backlight on/off state
String mqttLightBrightCommandTopic; // MQTT topic for incoming panel backlight dimmer commands
String mqttLightBrightStateTopic; // MQTT topic for outgoing panel backlight dimmer state
String mqttMotionStateTopic; // MQTT topic for outgoing motion sensor state
String nextionModel; // Record reported model number of LCD panel
const byte nextionSuffix[] = {0xFF, 0xFF, 0xFF}; // Standard suffix for Nextion commands
uint8_t nextionMaxPages = 11; // Maximum number of pages in Nextion project
uint32_t tftFileSize = 0; // Filesize for TFT firmware upload
const uint8_t nextionResetPin = D6; // Pin for Nextion power rail switch (GPIO12/D6)
const unsigned long nextionSpeeds[] = {2400,
4800,
9600,
19200,
31250,
38400,
57600,
115200,
230400,
250000,
256000,
512000,
921600}; // Valid serial speeds for Nextion communication
const uint8_t nextionSpeedsLength = sizeof(nextionSpeeds) / sizeof(nextionSpeeds[0]); // Size of our list of speeds
WiFiClientSecure mqttClientSecure; // TLS-enabled WiFiClient for MQTT
WiFiClient wifiClient; // Standard WiFiClient
MQTTClient mqttClient(mqttMaxPacketSize); // MQTT client
ESP8266WebServer webServer(80); // Admin web server on port 80
ESP8266HTTPUpdateServer httpOTAUpdate; // Arduino OTA server
WiFiServer telnetServer(23); // Telnet server (if enabled)
WiFiClient telnetClient; // Telnet client
MDNSResponder::hMDNSService hMDNSService; // mDNS
EspSaveCrash SaveCrash; // Save crash details to flash
// URL for auto-update check of "version.json"
const char UPDATE_URL[] PROGMEM = "https://raw.githubusercontent.com/HASwitchPlate/HASPone/main/update/version.json";
// Additional CSS style to match Hass theme
const char HASP_STYLE[] PROGMEM = "<style>button{background-color:#03A9F4;}body{width:60%;margin:auto;}input:invalid{border:1px solid red;}input[type=checkbox]{width:20px;}.wrap{text-align:left;display:inline-block;min-width:260px;max-width:1000px}</style>";
// Default link to compiled Arduino firmware image
String espFirmwareUrl = "https://raw.githubusercontent.com/HASwitchPlate/HASPone/main/Arduino_Sketch/HASwitchPlate.ino.d1_mini.bin";
// Default link to compiled Nextion firmware images
String lcdFirmwareUrl = "https://raw.githubusercontent.com/HASwitchPlate/HASPone/main/Nextion_HMI/HASwitchPlate.tft";
////////////////////////////////////////////////////////////////////////////////////////////////////
void setup()
{ // System setup
debugPrint(String(F("\n\n================================================================================\n")));
debugPrintln(String(F("SYSTEM: Starting HASPone v")) + String(haspVersion));
debugPrintln(String(F("SYSTEM: heapFree: ")) + String(ESP.getFreeHeap()) + String(F(" heapMaxFreeBlockSize: ")) + String(ESP.getMaxFreeBlockSize()));
debugPrintln(String(F("SYSTEM: Last reset reason: ")) + String(ESP.getResetInfo()));
if (SaveCrash.count())
{
debugPrint(String(F("SYSTEM: Crashdump data discovered:")));
debugPrintCrash();
}
debugPrint(String(F("================================================================================\n\n")));
pinMode(nextionResetPin, OUTPUT); // Take control over the power switch for the LCD
digitalWrite(nextionResetPin, HIGH); // Power on the LCD
configRead(); // Check filesystem for a saved config.json
Serial.begin(atoi(nextionBaud)); // Serial - LCD RX (after swap), debug TX
Serial1.begin(atoi(nextionBaud)); // Serial1 - LCD TX, no RX
Serial.swap(); // Swap to allow hardware UART comms to LCD
if (!nextionConnect())
{
if (lcdConnected)
{
debugPrintln(F("HMI: LCD responding but initialization wasn't completed. Continuing program load anyway."));
}
else
{
debugPrintln(F("HMI: LCD not responding, continuing program load"));
}
}
espWifiConnect(); // Start up networking
if ((configPassword[0] != '\0') && (configUser[0] != '\0'))
{ // Start the webserver with our assigned password if it's been configured...
httpOTAUpdate.setup(&webServer, "/update", configUser, configPassword);
}
else
{ // or without a password if not
httpOTAUpdate.setup(&webServer, "/update");
}
webServer.on("/", webHandleRoot);
webServer.on("/saveConfig", webHandleSaveConfig);
webServer.on("/resetConfig", webHandleResetConfig);
webServer.on("/resetBacklight", webHandleResetBacklight);
webServer.on("/firmware", webHandleFirmware);
webServer.on("/espfirmware", webHandleEspFirmware);
webServer.on(
"/lcdupload", HTTP_POST, []()
{ webServer.send(200); },
webHandleLcdUpload);
webServer.on("/tftFileSize", webHandleTftFileSize);
webServer.on("/lcddownload", webHandleLcdDownload);
webServer.on("/lcdOtaSuccess", webHandleLcdUpdateSuccess);
webServer.on("/lcdOtaFailure", webHandleLcdUpdateFailure);
webServer.on("/reboot", webHandleReboot);
webServer.onNotFound(webHandleNotFound);
webServer.begin();
debugPrintln(String(F("HTTP: Server started @ http://")) + WiFi.localIP().toString());
espSetupOta(); // Start OTA firmware update
motionSetup(); // Setup motion sensor if configured
mqttConnect(); // Connect to MQTT
if (mdnsEnabled)
{ // Setup mDNS service discovery if enabled
hMDNSService = MDNS.addService(haspNode, "http", "tcp", 80);
if (debugTelnetEnabled)
{
MDNS.addService(haspNode, "telnet", "tcp", 23);
}
MDNS.addServiceTxt(hMDNSService, "app_name", "HASwitchPlate");
MDNS.addServiceTxt(hMDNSService, "app_version", String(haspVersion).c_str());
MDNS.update();
}
if (beepEnabled)
{ // Setup beep/tactile output if configured
pinMode(beepPin, OUTPUT);
}
if (debugTelnetEnabled)
{ // Setup telnet server for remote debug output
telnetServer.setNoDelay(true);
telnetServer.begin();
debugPrintln(String(F("TELNET: debug server enabled at telnet:")) + WiFi.localIP().toString());
}
debugPrintln(F("SYSTEM: System init complete."));
}
////////////////////////////////////////////////////////////////////////////////////////////////////
void loop()
{ // Main execution loop
while ((WiFi.status() != WL_CONNECTED) || (WiFi.localIP().toString() == "0.0.0.0"))
{ // Check WiFi is connected and that we have a valid IP, retry until we do.
if (WiFi.status() == WL_CONNECTED)
{ // If we're currently connected, disconnect so we can try again
WiFi.disconnect();
}
espWifiReconnect();
}
if (!mqttClient.connected())
{ // Check MQTT connection
debugPrintln(String(F("MQTT: not connected, connecting.")));
mqttConnect();
}
nextionHandleInput(); // Nextion serial communications loop
mqttClient.loop(); // MQTT client loop
ArduinoOTA.handle(); // Arduino OTA loop
webServer.handleClient(); // webServer loop
telnetHandleClient(); // telnet client loop
motionHandle(); // motion sensor loop
beepHandle(); // beep feedback loop
if (mdnsEnabled)
{
MDNS.update();
}
if ((millis() - statusUpdateTimer) >= statusUpdateInterval)
{ // Run periodic status update
statusUpdateTimer = millis();
mqttStatusUpdate();
}
if (((millis() - updateCheckTimer) >= updateCheckInterval) && (millis() > updateCheckFirstRun))
{ // Run periodic update check
updateCheckTimer = millis();
if (updateCheck())
{ // Publish new status if updateCheck() worked and reset the timer
statusUpdateTimer = millis();
mqttStatusUpdate();
}
}
}
////////////////////////////////////////////////////////////////////////////////////////////////////
// Functions
////////////////////////////////////////////////////////////////////////////////////////////////////
void mqttConnect()
{ // MQTT connection and subscriptions
static bool mqttFirstConnect = true; // For the first connection, we want to send an OFF/ON state to
// trigger any automations, but skip that if we reconnect while
// still running the sketch
rebootOnp0b1 = true;
static uint8_t mqttReconnectCount = 0;
unsigned long mqttConnectTimer = 0;
const unsigned long mqttConnectTimeout = 5000;
// Check to see if we have a broker configured and notify the user if not
if (strcmp(mqttServer, "") == 0)
{
nextionSendCmd("page 0");
nextionSetAttr("p[0].b[1].font", "6");
nextionSetAttr("p[0].b[1].txt", "\"WiFi Connected!\\r " + String(WiFi.SSID()) + "\\rIP: " + WiFi.localIP().toString() + "\\r\\rConfigure MQTT:\\rhttp://" + WiFi.localIP().toString() + "\"");
while (strcmp(mqttServer, "") == 0)
{ // Handle other stuff while we're waiting for MQTT to be configured
yield();
nextionHandleInput(); // Nextion serial communications loop
ArduinoOTA.handle(); // Arduino OTA loop
webServer.handleClient(); // webServer loop
telnetHandleClient(); // telnet client loop
motionHandle(); // motion sensor loop
beepHandle(); // beep feedback loop
}
}
if (mqttTlsEnabled)
{ // Create MQTT service object with TLS connection
mqttClient.begin(mqttServer, atoi(mqttPort), mqttClientSecure);
if (strcmp(mqttFingerprint, "") == 0)
{
debugPrintln(String(F("MQTT: Configuring MQTT TLS connection without fingerprint validation.")));
mqttClientSecure.setInsecure();
}
else
{
debugPrintln(String(F("MQTT: Configuring MQTT TLS connection with fingerprint validation.")));
mqttClientSecure.allowSelfSignedCerts();
mqttClientSecure.setFingerprint(mqttFingerprint);
}
mqttClientSecure.setBufferSizes(512, 512);
}
else
{ // Create MQTT service object without TLS connection
debugPrintln(String(F("MQTT: Configuring MQTT connection without TLS.")));
mqttClient.begin(mqttServer, atoi(mqttPort), wifiClient);
}
mqttClient.onMessage(mqttProcessInput); // Setup MQTT callback function
// MQTT topic string definitions
mqttStateTopic = "hasp/" + String(haspNode) + "/state";
mqttStateJSONTopic = "hasp/" + String(haspNode) + "/state/json";
mqttCommandTopic = "hasp/" + String(haspNode) + "/command";
mqttGroupCommandTopic = "hasp/" + String(groupName) + "/command";
mqttStatusTopic = "hasp/" + String(haspNode) + "/status";
mqttSensorTopic = "hasp/" + String(haspNode) + "/sensor";
mqttLightCommandTopic = "hasp/" + String(haspNode) + "/light/switch";
mqttLightStateTopic = "hasp/" + String(haspNode) + "/light/state";
mqttLightBrightCommandTopic = "hasp/" + String(haspNode) + "/brightness/set";
mqttLightBrightStateTopic = "hasp/" + String(haspNode) + "/brightness/state";
mqttMotionStateTopic = "hasp/" + String(haspNode) + "/motion/state";
const String mqttCommandSubscription = mqttCommandTopic + "/#";
const String mqttGroupCommandSubscription = mqttGroupCommandTopic + "/#";
const String mqttLightSubscription = mqttLightCommandTopic + "/#";
const String mqttLightBrightSubscription = mqttLightBrightCommandTopic + "/#";
// Generate an MQTT client ID as haspNode + our MAC address
mqttClientId = String(haspNode) + "-" + String(espMac[0], HEX) + String(espMac[1], HEX) + String(espMac[2], HEX) + String(espMac[3], HEX) + String(espMac[4], HEX) + String(espMac[5], HEX);
nextionSendCmd("page 0");
nextionSetAttr("p[0].b[1].font", "6");
nextionSetAttr("p[0].b[1].txt", "\"WiFi Connected!\\r " + String(WiFi.SSID()) + "\\rIP: " + WiFi.localIP().toString() + "\\r\\rMQTT Connecting:\\r " + String(mqttServer) + "\"");
if (mqttTlsEnabled)
{
debugPrintln(String(F("MQTT: Attempting connection to broker ")) + String(mqttServer) + String(F(" on port ")) + String(mqttPort) + String(F(" with TLS enabled as clientID ")) + mqttClientId);
}
else
{
debugPrintln(String(F("MQTT: Attempting connection to broker ")) + String(mqttServer) + String(F(" on port ")) + String(mqttPort) + String(F(" with TLS disabled as clientID ")) + mqttClientId);
}
// Set keepAlive, cleanSession, timeout
mqttClient.setOptions(30, true, mqttConnectTimeout);
// declare LWT
mqttClient.setWill(mqttStatusTopic.c_str(), "OFF", true, 1);
while (!mqttClient.connected())
{ // Loop until we're connected to MQTT
mqttConnectTimer = millis();
mqttClient.connect(mqttClientId.c_str(), mqttUser, mqttPassword, false);
if (mqttClient.connected())
{ // Attempt to connect to broker, setting last will and testament
// Update panel with MQTT status
nextionSetAttr("p[0].b[1].txt", "\"WiFi Connected!\\r " + String(WiFi.SSID()) + "\\rIP: " + WiFi.localIP().toString() + "\\r\\rMQTT Connected:\\r " + String(mqttServer) + "\"");
debugPrintln(F("MQTT: connected"));
// Reset our diagnostic booleans
mqttPingCheck = true;
mqttPortCheck = true;
// Subscribe to our incoming topics
if (mqttClient.subscribe(mqttCommandSubscription))
{
debugPrintln(String(F("MQTT: subscribed to ")) + mqttCommandSubscription);
}
if (mqttClient.subscribe(mqttGroupCommandSubscription))
{
debugPrintln(String(F("MQTT: subscribed to ")) + mqttGroupCommandSubscription);
}
if (mqttClient.subscribe(mqttLightSubscription))
{
debugPrintln(String(F("MQTT: subscribed to ")) + mqttLightSubscription);
}
if (mqttClient.subscribe(mqttLightBrightSubscription))
{
debugPrintln(String(F("MQTT: subscribed to ")) + mqttLightBrightSubscription);
}
// Publish discovery configuration
mqttDiscovery();
// Publish backlight status
if (lcdBacklightOn)
{
debugPrintln(String(F("MQTT OUT: '")) + mqttLightStateTopic + String(F("' : 'ON'")));
mqttClient.publish(mqttLightStateTopic, "ON", true, 1);
}
else
{
debugPrintln(String(F("MQTT OUT: '")) + mqttLightStateTopic + String(F("' : 'OFF'")));
mqttClient.publish(mqttLightStateTopic, "OFF", true, 1);
}
debugPrintln(String(F("MQTT OUT: '")) + mqttLightBrightStateTopic + String(F("' : ")) + String(lcdBacklightDim));
mqttClient.publish(mqttLightBrightStateTopic, String(lcdBacklightDim), true, 1);
if (mqttFirstConnect)
{ // Force any subscribed clients to toggle OFF/ON when we first connect to
// make sure we get a full panel refresh at power on. Sending OFF,
// "ON" will be sent by the mqttStatusTopic subscription action below.
mqttFirstConnect = false;
debugPrintln(String(F("MQTT OUT: '")) + mqttStatusTopic + "' : 'OFF'");
mqttClient.publish(mqttStatusTopic, "OFF", true, 0);
}
if (mqttClient.subscribe(mqttStatusTopic))
{
debugPrintln(String(F("MQTT: subscribed to ")) + mqttStatusTopic);
}
mqttClient.loop();
}
else
{ // Retry until we give up and restart after connectTimeout seconds
mqttReconnectCount++;
if (mqttReconnectCount * mqttConnectTimeout * 6 > (connectTimeout * 1000))
{
debugPrintln(String(F("MQTT: connection attempt ")) + String(mqttReconnectCount) + String(F(" failed with rc: ")) + String(mqttClient.returnCode()) + String(F(" and error: ")) + String(mqttClient.lastError()) + String(F(". Restarting device.")));
espReset();
}
yield();
webServer.handleClient();
String mqttCheckResult = "Ping: FAILED";
String mqttCheckResultNextion = "MQTT Check...";
debugPrintln(String(F("MQTT: connection attempt ")) + String(mqttReconnectCount) + String(F(" failed with rc ")) + String(mqttClient.returnCode()) + String(F(" and error: ")) + String(mqttClient.lastError()));
nextionSetAttr("p[0].b[1].txt", String(F("\"WiFi Connected!\\r ")) + String(WiFi.SSID()) + String(F("\\rIP: ")) + WiFi.localIP().toString() + String(F("\\r\\rMQTT Failed:\\r ")) + String(mqttServer) + String(F("\\rRC: ")) + String(mqttClient.returnCode()) + String(F(" Error: ")) + String(mqttClient.lastError()) + String(F("\\r")) + mqttCheckResultNextion + String(F("\"")));
mqttPingCheck = Ping.ping(mqttServer, 4);
yield();
webServer.handleClient();
mqttPortCheck = wifiClient.connect(mqttServer, atoi(mqttPort));
yield();
webServer.handleClient();
mqttCheckResultNextion = "Ping: ";
if (mqttPingCheck)
{
mqttCheckResult = "Ping: SUCCESS";
mqttCheckResultNextion = "Ping: ";
}
if (mqttPortCheck)
{
mqttCheckResult += " Port: SUCCESS";
mqttCheckResultNextion += " Port: ";
}
else
{
mqttCheckResult += " Port: FAILED";
mqttCheckResultNextion += " Port: ";
}
debugPrintln(String(F("MQTT: connection checks: ")) + mqttCheckResult + String(F(". Trying again in 30 seconds.")));
nextionSetAttr("p[0].b[1].txt", String(F("\"WiFi Connected!\\r ")) + String(WiFi.SSID()) + String(F("\\rIP: ")) + WiFi.localIP().toString() + String(F("\\r\\rMQTT Failed:\\r ")) + String(mqttServer) + String(F("\\rRC: ")) + String(mqttClient.returnCode()) + String(F(" Error: ")) + String(mqttClient.lastError()) + String(F("\\r")) + mqttCheckResultNextion + String(F("\"")));
while (millis() < (mqttConnectTimer + (mqttConnectTimeout * 6)))
{
yield();
nextionHandleInput(); // Nextion serial communications loop
ArduinoOTA.handle(); // Arduino OTA loop
webServer.handleClient(); // webServer loop
telnetHandleClient(); // telnet client loop
motionHandle(); // motion sensor loop
beepHandle(); // beep feedback loop
}
}
}
rebootOnp0b1 = false;
if (nextionActivePage < 0)
{ // We never picked up a message giving us a page number, so we'll just go to the default page
debugPrintln(String(F("DEBUG: NextionActivePage not received from MQTT, setting to 0")));
String mqttButtonJSONEvent = String(F("{\"event\":\"page\",\"value\":0}"));
debugPrintln(String(F("MQTT OUT: '")) + mqttStateJSONTopic + String(F("' : '")) + mqttButtonJSONEvent + String(F("'")));
mqttClient.publish(mqttStateJSONTopic, mqttButtonJSONEvent, false, 0);
String mqttPageTopic = mqttStateTopic + "/page";
debugPrintln(String(F("MQTT OUT: '")) + mqttPageTopic + String(F("' : '0'")));
mqttClient.publish(mqttPageTopic, "0", false, 0);
}
}
////////////////////////////////////////////////////////////////////////////////////////////////////
void mqttProcessInput(String &strTopic, String &strPayload)
{ // Handle incoming commands from MQTT
// strTopic: homeassistant/haswitchplate/devicename/command/p[1].b[4].txt
// strPayload: "Lights On"
// subTopic: p[1].b[4].txt
// Incoming Namespace (replace /device/ with /group/ for group commands)
// '[...]/device/command' -m '' == No command requested, respond with mqttStatusUpdate()
// '[...]/device/command' -m 'dim=50' == nextionSendCmd("dim=50")
// '[...]/device/command/json' -m '["dim=5", "page 1"]' == nextionSendCmd("dim=50"), nextionSendCmd("page 1")
// '[...]/device/command/p[1].b[4].txt' -m '' == nextionGetAttr("p[1].b[4].txt")
// '[...]/device/command/p[1].b[4].txt' -m '"Lights On"' == nextionSetAttr("p[1].b[4].txt", "\"Lights On\"")
// '[...]/device/brightness/set' -m '50' == nextionSendCmd("dims=50")
// '[...]/device/light/switch' -m 'OFF' == nextionSendCmd("dims=0")
// '[...]/device/command/page' -m '1' == nextionSendCmd("page 1")
// '[...]/device/command/statusupdate' -m '' == mqttStatusUpdate()
// '[...]/device/command/discovery' -m '' == call mqttDiscovery()
// '[...]/device/command/lcdupdate' -m 'http://192.168.0.10/local/HASwitchPlate.tft' == nextionOtaStartDownload("http://192.168.0.10/local/HASwitchPlate.tft")
// '[...]/device/command/lcdupdate' -m '' == nextionOtaStartDownload("lcdFirmwareUrl")
// '[...]/device/command/espupdate' -m 'http://192.168.0.10/local/HASwitchPlate.ino.d1_mini.bin' == espStartOta("http://192.168.0.10/local/HASwitchPlate.ino.d1_mini.bin")
// '[...]/device/command/espupdate' -m '' == espStartOta("espFirmwareUrl")
// '[...]/device/command/beep' -m '100,200,3' == beep on for 100msec, off for 200msec, repeat 3 times
// '[...]/device/command/hassdiscovery' -m 'homeassistant' == hassDiscovery = homeassistant
// '[...]/device/command/nextionmaxpages' -m '11' == nextionmaxpages = 11
// '[...]/device/command/nextionbaud' -m '921600' == nextionBaud = 921600
// '[...]/device/command/debugserialenabled' -m 'true' == enable serial debug output
// '[...]/device/command/debugtelnetenabled' -m 'true' == enable telnet debug output
// '[...]/device/command/mdnsenabled' -m 'true' == enable mDNS responder
// '[...]/device/command/beepenabled' -m 'true' == enable beep output on keypress
// '[...]/device/command/ignoretouchwhenoff' -m 'true' == disable actions on keypress
debugPrintln(String(F("MQTT IN: '")) + strTopic + String(F("' : '")) + strPayload + String(F("'")));
if (((strTopic == mqttCommandTopic) || (strTopic == mqttGroupCommandTopic)) && (strPayload == ""))
{ // '[...]/device/command' -m '' = No command requested, respond with mqttStatusUpdate()
mqttStatusUpdate(); // return status JSON via MQTT
}
else if (strTopic == mqttCommandTopic || strTopic == mqttGroupCommandTopic)
{ // '[...]/device/command' -m 'dim=50' == nextionSendCmd("dim=50")
nextionSendCmd(strPayload);
}
else if (strTopic == (mqttCommandTopic + "/page") || strTopic == (mqttGroupCommandTopic + "/page"))
{ // '[...]/device/command/page' -m '1' == nextionSendCmd("page 1")
if (strPayload == "")
{
nextionSendCmd("sendme");
}
else
{
nextionActivePage = strPayload.toInt();
nextionSendCmd("page " + strPayload);
}
}
else if (strTopic == (mqttCommandTopic + "/json") || strTopic == (mqttGroupCommandTopic + "/json"))
{ // '[...]/device/command/json' -m '["dim=5", "page 1"]' = nextionSendCmd("dim=50"), nextionSendCmd("page 1")
if (strPayload != "")
{
nextionParseJson(strPayload); // Send to nextionParseJson()
}
}
else if (strTopic == (mqttCommandTopic + "/statusupdate") || strTopic == (mqttGroupCommandTopic + "/statusupdate"))
{ // '[...]/device/command/statusupdate' == mqttStatusUpdate()
mqttStatusUpdate(); // return status JSON via MQTT
}
else if (strTopic == (mqttCommandTopic + "/discovery") || strTopic == (mqttGroupCommandTopic + "/discovery"))
{ // '[...]/device/command/discovery' == mqttDiscovery()
mqttDiscovery(); // send Home Assistant discovery message via MQTT
}
else if (strTopic == (mqttCommandTopic + "/hassdiscovery") || strTopic == (mqttGroupCommandTopic + "/hassdiscovery"))
{ // '[...]/device/command/hassdiscovery' -m 'homeassistant' == hassDiscovery = homeassistant
strPayload.toCharArray(hassDiscovery, 128); // set hassDiscovery to value provided in payload
configSave();
mqttDiscovery(); // send Home Assistant discovery message on new discovery topic via MQTT
}
else if ((strTopic == (mqttCommandTopic + "/nextionmaxpages") || strTopic == (mqttGroupCommandTopic + "/nextionmaxpages")) && (strPayload.toInt() < 256) && (strPayload.toInt() > 0))
{ // '[...]/device/command/nextionmaxpages' -m '11' == nextionmaxpages = 11
nextionMaxPages = strPayload.toInt(); // set nextionMaxPages to value provided in payload
configSave();
mqttDiscovery(); // send Home Assistant discovery message via MQTT
}
else if ((strTopic == (mqttCommandTopic + "/nextionbaud") || strTopic == (mqttGroupCommandTopic + "/nextionbaud")) &&
((strPayload.toInt() == 2400) ||
(strPayload.toInt() == 4800) ||
(strPayload.toInt() == 9600) ||
(strPayload.toInt() == 19200) ||
(strPayload.toInt() == 31250) ||
(strPayload.toInt() == 38400) ||
(strPayload.toInt() == 57600) ||
(strPayload.toInt() == 115200) ||
(strPayload.toInt() == 230400) ||
(strPayload.toInt() == 250000) ||
(strPayload.toInt() == 256000) ||
(strPayload.toInt() == 512000) ||
(strPayload.toInt() == 921600)))
{ // '[...]/device/command/nextionbaud' -m '921600' == nextionBaud = 921600
strPayload.toCharArray(nextionBaud, 7); // set nextionBaud to value provided in payload
nextionAckEnable = false;
nextionSendCmd("bauds=" + strPayload); // send baud rate to nextion
nextionAckEnable = true;
Serial.flush();
Serial1.flush();
Serial.end();
Serial1.end();
Serial.begin(atoi(nextionBaud)); // Serial - LCD RX (after swap), debug TX
Serial1.begin(atoi(nextionBaud)); // Serial1 - LCD TX, no RX
Serial.swap(); // Swap to allow hardware UART comms to LCD
configSave();
}
else if (strTopic == (mqttCommandTopic + "/debugserialenabled") || strTopic == (mqttGroupCommandTopic + "/debugserialenabled"))
{ // '[...]/device/command/debugserialenabled' -m 'true' == enable serial debug output
if (strPayload.equalsIgnoreCase("true"))
{
debugSerialEnabled = true;
configSave();
}
else if(strPayload.equalsIgnoreCase("false"))
{
debugSerialEnabled = false;
configSave();
}
}
else if (strTopic == (mqttCommandTopic + "/debugtelnetenabled") || strTopic == (mqttGroupCommandTopic + "/debugtelnetenabled"))
{ // '[...]/device/command/debugtelnetenabled' -m 'true' == enable telnet debug output
if (strPayload.equalsIgnoreCase("true"))
{
debugTelnetEnabled = true;
configSave();
}
else if(strPayload.equalsIgnoreCase("false"))
{
debugTelnetEnabled = false;
configSave();
}
}
else if (strTopic == (mqttCommandTopic + "/mdnsenabled") || strTopic == (mqttGroupCommandTopic + "/mdnsenabled"))
{ // '[...]/device/command/mdnsenabled' -m 'true' == enable mDNS responder
if (strPayload.equalsIgnoreCase("true"))
{
mdnsEnabled = true;
configSave();
}
else if(strPayload.equalsIgnoreCase("false"))
{
mdnsEnabled = false;
configSave();
}
}
else if (strTopic == (mqttCommandTopic + "/beepenabled") || strTopic == (mqttGroupCommandTopic + "/beepenabled"))
{ // '[...]/device/command/beepenabled' -m 'true' == enable beep output on keypress
if (strPayload.equalsIgnoreCase("true"))
{
beepEnabled = true;
configSave();
}
else if(strPayload.equalsIgnoreCase("false"))
{
beepEnabled = false;
configSave();
}
}
else if (strTopic == (mqttCommandTopic + "/ignoretouchwhenoff") || strTopic == (mqttGroupCommandTopic + "/ignoretouchwhenoff"))
{ // '[...]/device/command/ignoretouchwhenoff' -m 'true' == disable actions on keypress
if (strPayload.equalsIgnoreCase("true"))
{
ignoreTouchWhenOff = true;
configSave();
}
else if(strPayload.equalsIgnoreCase("false"))
{
ignoreTouchWhenOff = false;
configSave();
}
}
else if (strTopic == (mqttCommandTopic + "/lcdupdate") || strTopic == (mqttGroupCommandTopic + "/lcdupdate"))
{ // '[...]/device/command/lcdupdate' -m 'http://192.168.0.10/local/HASwitchPlate.tft' == nextionOtaStartDownload("http://192.168.0.10/local/HASwitchPlate.tft")
if (strPayload == "")
{
nextionOtaStartDownload(lcdFirmwareUrl);
}
else
{
nextionOtaStartDownload(strPayload);
}
}
else if (strTopic == (mqttCommandTopic + "/espupdate") || strTopic == (mqttGroupCommandTopic + "/espupdate"))
{ // '[...]/device/command/espupdate' -m 'http://192.168.0.10/local/HASwitchPlate.ino.d1_mini.bin' == espStartOta("http://192.168.0.10/local/HASwitchPlate.ino.d1_mini.bin")
if (strPayload == "")
{
espStartOta(espFirmwareUrl);
}
else
{
espStartOta(strPayload);
}
}
else if (strTopic == (mqttCommandTopic + "/reboot") || strTopic == (mqttGroupCommandTopic + "/reboot"))
{ // '[...]/device/command/reboot' == reboot microcontroller
debugPrintln(F("MQTT: Rebooting device"));
espReset();
}
else if (strTopic == (mqttCommandTopic + "/lcdreboot") || strTopic == (mqttGroupCommandTopic + "/lcdreboot"))
{ // '[...]/device/command/lcdreboot' == reboot LCD panel
debugPrintln(F("MQTT: Rebooting LCD"));
nextionReset();
}
else if (strTopic == (mqttCommandTopic + "/factoryreset") || strTopic == (mqttGroupCommandTopic + "/factoryreset"))
{ // '[...]/device/command/factoryreset' == clear all saved settings
configClearSaved();
}
else if (strTopic == (mqttCommandTopic + "/beep") || strTopic == (mqttGroupCommandTopic + "/beep"))
{ // '[...]/device/command/beep' == activate beep function
String mqttvar1 = getSubtringField(strPayload, ',', 0);
String mqttvar2 = getSubtringField(strPayload, ',', 1);
String mqttvar3 = getSubtringField(strPayload, ',', 2);
beepOnTime = mqttvar1.toInt();
beepOffTime = mqttvar2.toInt();
beepCounter = mqttvar3.toInt();
}
else if (strTopic == (mqttCommandTopic + "/crashtest"))
{ // '[...]/device/command/crashtest' -m 'divzero' == divide by zero
if (strPayload == "divzero")
{
debugPrintln(String(F("DEBUG: attempt to divide by zero")));
int result, zero;
zero = 0;
result = 1 / zero;
debugPrintln(String(F("DEBUG: div zero result: ")) + String(result));
}
else if (strPayload == "nullptr")
{ // '[...]/device/command/crashtest' -m 'nullptr' == dereference a null pointer
debugPrintln(String(F("DEBUG: attempt to dereference null pointer")));
int *nullPointer = NULL;
debugPrintln(String(F("DEBUG: dereference null pointer: ")) + String(*nullPointer));
}
else if (strPayload == "wdt")
{ // '[...]/device/command/crashtest' -m 'wdt' == trigger soft WDT
debugPrintln(String(F("DEBUG: enter tight loop and cause WDT")));
while (true)
{
}
}
}
else if (strTopic.startsWith(mqttCommandTopic) && (strPayload == ""))
{ // '[...]/device/command/p[1].b[4].txt' -m '' == nextionGetAttr("p[1].b[4].txt")
String subTopic = strTopic.substring(mqttCommandTopic.length() + 1);
mqttGetSubtopic = "/" + subTopic;
nextionGetAttr(subTopic);
}
else if (strTopic.startsWith(mqttGroupCommandTopic) && (strPayload == ""))
{ // '[...]/group/command/p[1].b[4].txt' -m '' == nextionGetAttr("p[1].b[4].txt")
String subTopic = strTopic.substring(mqttGroupCommandTopic.length() + 1);
mqttGetSubtopic = "/" + subTopic;
nextionGetAttr(subTopic);
}
else if (strTopic.startsWith(mqttCommandTopic))
{ // '[...]/device/command/p[1].b[4].txt' -m '"Lights On"' == nextionSetAttr("p[1].b[4].txt", "\"Lights On\"")
String subTopic = strTopic.substring(mqttCommandTopic.length() + 1);
nextionSetAttr(subTopic, strPayload);
}
else if (strTopic.startsWith(mqttGroupCommandTopic))
{ // '[...]/group/command/p[1].b[4].txt' -m '"Lights On"' == nextionSetAttr("p[1].b[4].txt", "\"Lights On\"")
String subTopic = strTopic.substring(mqttGroupCommandTopic.length() + 1);
nextionSetAttr(subTopic, strPayload);
}
else if (strTopic == mqttLightBrightCommandTopic)
{ // change the brightness from the light topic
nextionSetAttr("dim", strPayload);
nextionSetAttr("dims", "dim");
lcdBacklightDim = strPayload.toInt();
debugPrintln(String(F("MQTT OUT: '")) + mqttLightBrightStateTopic + String(F("' : '")) + strPayload + String(F("'")));
mqttClient.publish(mqttLightBrightStateTopic, strPayload, true, 0);
}
else if (strTopic == mqttLightCommandTopic && strPayload == "OFF")
{ // set the panel dim OFF from the light topic, saving current dim level first
nextionSetAttr("dims", "dim");
nextionSetAttr("dim", "0");
lcdBacklightOn = 0;
debugPrintln(String(F("MQTT OUT: '")) + mqttLightStateTopic + String(F("' : 'OFF'")));
mqttClient.publish(mqttLightStateTopic, "OFF", true, 0);
}
else if (strTopic == mqttLightCommandTopic && strPayload == "ON")
{ // set the panel dim ON from the light topic, restoring saved dim level
nextionSetAttr("dim", "dims");
nextionSetAttr("sleep", "0");
lcdBacklightOn = 1;
debugPrintln(String(F("MQTT OUT: '")) + mqttLightStateTopic + String(F("' : 'ON'")));
mqttClient.publish(mqttLightStateTopic, "ON", true, 0);
}
else if (strTopic == mqttStatusTopic && strPayload == "OFF")
{ // catch a dangling LWT from a previous connection if it appears
debugPrintln(String(F("MQTT OUT: '")) + mqttStatusTopic + String(F("' : 'ON'")));
mqttClient.publish(mqttStatusTopic, "ON", true, 0);
mqttClient.publish(mqttStateJSONTopic, String(F("{\"event_type\":\"hasp_device\",\"event\":\"online\"}")));
debugPrintln(String(F("MQTT OUT: '")) + mqttStateJSONTopic + String(F(" : {\"event_type\":\"hasp_device\",\"event\":\"online\"}")));
}
}
////////////////////////////////////////////////////////////////////////////////////////////////////
void mqttStatusUpdate()
{ // Periodically publish system status
String mqttSensorPayload = "{";
mqttSensorPayload += String(F("\"espVersion\":")) + String(haspVersion) + String(F(","));
if (updateEspAvailable)
{
mqttSensorPayload += String(F("\"updateEspAvailable\":true,"));
}
else
{
mqttSensorPayload += String(F("\"updateEspAvailable\":false,"));
}
if (lcdConnected)
{
mqttSensorPayload += String(F("\"lcdConnected\":true,"));
}
else
{
mqttSensorPayload += String(F("\"lcdConnected\":false,"));
}
mqttSensorPayload += String(F("\"lcdVersion\":\"")) + String(lcdVersion) + String(F("\","));
if (updateLcdAvailable)
{
mqttSensorPayload += String(F("\"updateLcdAvailable\":true,"));
}
else
{
mqttSensorPayload += String(F("\"updateLcdAvailable\":false,"));
}
mqttSensorPayload += String(F("\"espUptime\":")) + String(long(millis() / 1000)) + String(F(","));
mqttSensorPayload += String(F("\"signalStrength\":")) + String(WiFi.RSSI()) + String(F(","));
mqttSensorPayload += String(F("\"haspName\":\"")) + String(haspNode) + String(F("\","));
mqttSensorPayload += String(F("\"haspIP\":\"")) + WiFi.localIP().toString() + String(F("\","));
mqttSensorPayload += String(F("\"haspClientID\":\"")) + mqttClientId + String(F("\","));
mqttSensorPayload += String(F("\"haspMac\":\"")) + String(espMac[0], HEX) + String(F(":")) + String(espMac[1], HEX) + String(F(":")) + String(espMac[2], HEX) + String(F(":")) + String(espMac[3], HEX) + String(F(":")) + String(espMac[4], HEX) + String(F(":")) + String(espMac[5], HEX) + String(F("\","));
mqttSensorPayload += String(F("\"haspManufacturer\":\"HASwitchPlate\",\"haspModel\":\"HASPone v1.0.0\","));
mqttSensorPayload += String(F("\"heapFree\":")) + String(ESP.getFreeHeap()) + String(F(","));
mqttSensorPayload += String(F("\"heapFragmentation\":")) + String(ESP.getHeapFragmentation()) + String(F(","));
mqttSensorPayload += String(F("\"heapMaxFreeBlockSize\":")) + String(ESP.getMaxFreeBlockSize()) + String(F(","));
mqttSensorPayload += String(F("\"espCore\":\"")) + String(ESP.getCoreVersion()) + String(F("\""));
mqttSensorPayload += "}";
// Publish sensor JSON
mqttClient.publish(mqttSensorTopic, mqttSensorPayload, true, 1);
debugPrintln(String(F("MQTT OUT: '")) + mqttSensorTopic + String(F("' : '")) + mqttSensorPayload + String(F("'")));
}
////////////////////////////////////////////////////////////////////////////////////////////////////
void mqttDiscovery()
{ // Publish Home Assistant discovery messages
String macAddress = String(espMac[0], HEX) + String(F(":")) + String(espMac[1], HEX) + String(F(":")) + String(espMac[2], HEX) + String(F(":")) + String(espMac[3], HEX) + String(F(":")) + String(espMac[4], HEX) + String(F(":")) + String(espMac[5], HEX);
// light discovery for backlight
String mqttDiscoveryTopic = String(hassDiscovery) + String(F("/light/")) + String(haspNode) + String(F("/config"));
String mqttDiscoveryPayload = String(F("{\"name\":\"")) + String(haspNode) + String(F(" backlight\",\"command_topic\":\"")) + mqttLightCommandTopic + String(F("\",\"state_topic\":\"")) + mqttLightStateTopic + String(F("\",\"brightness_state_topic\":\"")) + mqttLightBrightStateTopic + String(F("\",\"brightness_command_topic\":\"")) + mqttLightBrightCommandTopic + String(F("\",\"availability_topic\":\"")) + mqttStatusTopic + String(F("\",\"brightness_scale\":100,\"unique_id\":\"")) + mqttClientId + String(F("-backlight\",\"payload_on\":\"ON\",\"payload_off\":\"OFF\",\"payload_available\":\"ON\",\"payload_not_available\":\"OFF\",\"device\":{\"identifiers\":[\"")) + mqttClientId + String(F("\"],\"name\":\"")) + String(haspNode) + String(F("\",\"manufacturer\":\"HASwitchPlate\",\"model\":\"HASPone v1.0.0\",\"sw_version\":")) + String(haspVersion) + String(F("}}"));
mqttClient.publish(mqttDiscoveryTopic, mqttDiscoveryPayload, true, 1);
debugPrintln(String(F("MQTT OUT: '")) + mqttDiscoveryTopic + String(F("' : '")) + String(mqttDiscoveryPayload) + String(F("'")));
// sensor discovery for device telemetry
mqttDiscoveryTopic = String(hassDiscovery) + String(F("/sensor/")) + String(haspNode) + String(F("/config"));
mqttDiscoveryPayload = String(F("{\"name\":\"")) + String(haspNode) + String(F(" sensor\",\"json_attributes_topic\":\"")) + mqttSensorTopic + String(F("\",\"state_topic\":\"")) + mqttStatusTopic + String(F("\",\"unique_id\":\"")) + mqttClientId + String(F("-sensor\",\"icon\":\"mdi:cellphone-text\",\"device\":{\"identifiers\":[\"")) + mqttClientId + String(F("\"],\"name\":\"")) + String(haspNode) + String(F("\",\"manufacturer\":\"HASwitchPlate\",\"model\":\"HASPone v1.0.0\",\"sw_version\":")) + String(haspVersion) + String(F("}}"));
mqttClient.publish(mqttDiscoveryTopic, mqttDiscoveryPayload, true, 1);
debugPrintln(String(F("MQTT OUT: '")) + mqttDiscoveryTopic + String(F("' : '")) + String(mqttDiscoveryPayload) + String(F("'")));
// number discovery for active page
mqttDiscoveryTopic = String(hassDiscovery) + String(F("/number/")) + String(haspNode) + String(F("/config"));
mqttDiscoveryPayload = String(F("{\"name\":\"")) + String(haspNode) + String(F(" active page\",\"command_topic\":\"")) + mqttCommandTopic + String(F("/page\",\"state_topic\":\"")) + mqttStateTopic + String(F("/page\",\"step\":1,\"min\":0,\"max\":")) + String(nextionMaxPages) + String(F(",\"retain\":true,\"optimistic\":true,\"icon\":\"mdi:page-next-outline\",\"unique_id\":\"")) + mqttClientId + String(F("-page\",\"device\":{\"identifiers\":[\"")) + mqttClientId + String(F("\"],\"name\":\"")) + String(haspNode) + String(F("\",\"manufacturer\":\"HASwitchPlate\",\"model\":\"HASPone v1.0.0\",\"sw_version\":")) + String(haspVersion) + String(F("}}"));
mqttClient.publish(mqttDiscoveryTopic, mqttDiscoveryPayload, true, 1);
debugPrintln(String(F("MQTT OUT: '")) + mqttDiscoveryTopic + String(F("' : '")) + String(mqttDiscoveryPayload) + String(F("'")));
// AlwaysOn topic for RGB lights
mqttClient.publish((String(F("hasp/")) + String(haspNode) + String(F("/alwayson"))), "ON", true, 1);
debugPrintln(String(F("MQTT OUT: 'hasp/")) + String(haspNode) + String(F("/alwayson' : 'ON'")));
// rgb light discovery for selectedforegroundcolor
mqttDiscoveryTopic = String(hassDiscovery) + String(F("/light/")) + String(haspNode) + String(F("/selectedforegroundcolor/config"));
mqttDiscoveryPayload = String(F("{\"name\":\"")) + String(haspNode) + String(F(" selected foreground color\",\"command_topic\":\"hasp/")) + String(haspNode) + String(F("/light/selectedforegroundcolor/switch\",\"state_topic\":\"hasp/")) + String(haspNode) + String(F("/alwayson\",\"rgb_command_topic\":\"hasp/")) + String(haspNode) + String(F("/light/selectedforegroundcolor/rgb\",\"rgb_command_template\":\"{{(red|bitwise_and(248)*256)+(green|bitwise_and(252)*8)+(blue|bitwise_and(248)/8)|int }}\",\"retain\":true,\"unique_id\":\"")) + mqttClientId + String(F("-selectedforegroundcolor\",\"device\":{\"identifiers\":[\"")) + mqttClientId + String(F("\"],\"name\":\"")) + String(haspNode) + String(F("\",\"manufacturer\":\"HASwitchPlate\",\"model\":\"HASPone v1.0.0\",\"sw_version\":")) + String(haspVersion) + String(F("}}"));
mqttClient.publish(mqttDiscoveryTopic, mqttDiscoveryPayload, true, 1);
debugPrintln(String(F("MQTT OUT: '")) + mqttDiscoveryTopic + String(F("' : '")) + String(mqttDiscoveryPayload) + String(F("'")));
// rgb light discovery for selectedbackgroundcolor
mqttDiscoveryTopic = String(hassDiscovery) + String(F("/light/")) + String(haspNode) + String(F("/selectedbackgroundcolor/config"));
mqttDiscoveryPayload = String(F("{\"name\":\"")) + String(haspNode) + String(F(" selected background color\",\"command_topic\":\"hasp/")) + String(haspNode) + String(F("/light/selectedbackgroundcolor/switch\",\"state_topic\":\"hasp/")) + String(haspNode) + String(F("/alwayson\",\"rgb_command_topic\":\"hasp/")) + String(haspNode) + String(F("/light/selectedbackgroundcolor/rgb\",\"rgb_command_template\":\"{{(red|bitwise_and(248)*256)+(green|bitwise_and(252)*8)+(blue|bitwise_and(248)/8)|int }}\",\"retain\":true,\"unique_id\":\"")) + mqttClientId + String(F("-selectedbackgroundcolor\",\"device\":{\"identifiers\":[\"")) + mqttClientId + String(F("\"],\"name\":\"")) + String(haspNode) + String(F("\",\"manufacturer\":\"HASwitchPlate\",\"model\":\"HASPone v1.0.0\",\"sw_version\":")) + String(haspVersion) + String(F("}}"));
mqttClient.publish(mqttDiscoveryTopic, mqttDiscoveryPayload, true, 1);
debugPrintln(String(F("MQTT OUT: '")) + mqttDiscoveryTopic + String(F("' : '")) + String(mqttDiscoveryPayload) + String(F("'")));
// rgb light discovery for unselectedforegroundcolor
mqttDiscoveryTopic = String(hassDiscovery) + String(F("/light/")) + String(haspNode) + String(F("/unselectedforegroundcolor/config"));
mqttDiscoveryPayload = String(F("{\"name\":\"")) + String(haspNode) + String(F(" unselected foreground color\",\"command_topic\":\"hasp/")) + String(haspNode) + String(F("/light/unselectedforegroundcolor/switch\",\"state_topic\":\"hasp/")) + String(haspNode) + String(F("/alwayson\",\"rgb_command_topic\":\"hasp/")) + String(haspNode) + String(F("/light/unselectedforegroundcolor/rgb\",\"rgb_command_template\":\"{{(red|bitwise_and(248)*256)+(green|bitwise_and(252)*8)+(blue|bitwise_and(248)/8)|int }}\",\"retain\":true,\"unique_id\":\"")) + mqttClientId + String(F("-unselectedforegroundcolor\",\"device\":{\"identifiers\":[\"")) + mqttClientId + String(F("\"],\"name\":\"")) + String(haspNode) + String(F("\",\"manufacturer\":\"HASwitchPlate\",\"model\":\"HASPone v1.0.0\",\"sw_version\":")) + String(haspVersion) + String(F("}}"));
mqttClient.publish(mqttDiscoveryTopic, mqttDiscoveryPayload, true, 1);
debugPrintln(String(F("MQTT OUT: '")) + mqttDiscoveryTopic + String(F("' : '")) + String(mqttDiscoveryPayload) + String(F("'")));
// rgb light discovery for unselectedbackgroundcolor
mqttDiscoveryTopic = String(hassDiscovery) + String(F("/light/")) + String(haspNode) + String(F("/unselectedbackgroundcolor/config"));
mqttDiscoveryPayload = String(F("{\"name\":\"")) + String(haspNode) + String(F(" unselected background color\",\"command_topic\":\"hasp/")) + String(haspNode) + String(F("/light/unselectedbackgroundcolor/switch\",\"state_topic\":\"hasp/")) + String(haspNode) + String(F("/alwayson\",\"rgb_command_topic\":\"hasp/")) + String(haspNode) + String(F("/light/unselectedbackgroundcolor/rgb\",\"rgb_command_template\":\"{{(red|bitwise_and(248)*256)+(green|bitwise_and(252)*8)+(blue|bitwise_and(248)/8)|int }}\",\"retain\":true,\"unique_id\":\"")) + mqttClientId + String(F("-unselectedbackgroundcolor\",\"device\":{\"identifiers\":[\"")) + mqttClientId + String(F("\"],\"name\":\"")) + String(haspNode) + String(F("\",\"manufacturer\":\"HASwitchPlate\",\"model\":\"HASPone v1.0.0\",\"sw_version\":")) + String(haspVersion) + String(F("}}"));
mqttClient.publish(mqttDiscoveryTopic, mqttDiscoveryPayload, true, 1);
debugPrintln(String(F("MQTT OUT: '")) + mqttDiscoveryTopic + String(F("' : '")) + String(mqttDiscoveryPayload) + String(F("'")));
if (motionEnabled)
{ // binary_sensor for motion
String macAddress = String(espMac[0], HEX) + String(F(":")) + String(espMac[1], HEX) + String(F(":")) + String(espMac[2], HEX) + String(F(":")) + String(espMac[3], HEX) + String(F(":")) + String(espMac[4], HEX) + String(F(":")) + String(espMac[5], HEX);
String mqttDiscoveryTopic = String(hassDiscovery) + String(F("/binary_sensor/")) + String(haspNode) + String(F("-motion/config"));
String mqttDiscoveryPayload = String(F("{\"device_class\":\"motion\",\"name\":\"")) + String(haspNode) + String(F(" motion\",\"state_topic\":\"")) + mqttMotionStateTopic + String(F("\",\"unique_id\":\"")) + mqttClientId + String(F("-motion\",\"payload_on\":\"ON\",\"payload_off\":\"OFF\",\"device\":{\"identifiers\":[\"")) + mqttClientId + String(F("\"],\"name\":\"")) + String(haspNode) + String(F("\",\"manufacturer\":\"HASwitchPlate\",\"model\":\"HASPone v1.0.0\",\"sw_version\":")) + String(haspVersion) + String(F("}}"));
mqttClient.publish(mqttDiscoveryTopic, mqttDiscoveryPayload, true, 1);
debugPrintln(String(F("MQTT OUT: '")) + mqttDiscoveryTopic + String(F("' : '")) + String(mqttDiscoveryPayload) + String(F("'")));
}
}
////////////////////////////////////////////////////////////////////////////////////////////////////
void nextionHandleInput()
{ // Handle incoming serial data from the Nextion panel
// This will collect serial data from the panel and place it into the global buffer
// nextionReturnBuffer[nextionReturnIndex]
unsigned long handlerTimeout = millis() + 100;
bool nextionCommandComplete = false;
static uint8_t nextionTermByteCnt = 0; // counter for our 3 consecutive 0xFFs
while (Serial.available() && !nextionCommandComplete && (millis() < handlerTimeout))
{
byte nextionCommandByte = Serial.read();
if (nextionCommandByte == 0xFF)
{ // check to see if we have one of 3 consecutive 0xFF which indicates the end of a command
nextionTermByteCnt++;
if (nextionTermByteCnt >= 3)
{ // We have received a complete command
lcdConnected = true;
nextionCommandComplete = true;
nextionTermByteCnt = 0; // reset counter
}
}
else
{
nextionTermByteCnt = 0; // reset counter if a non-term byte was encountered
}
nextionReturnBuffer[nextionReturnIndex] = nextionCommandByte;
nextionReturnIndex++;
if (nextionCommandComplete)
{
nextionAckReceived = true;
nextionProcessInput();
}
yield();
}
if (millis() > handlerTimeout)
{
debugPrintln(String(F("HMI ERROR: nextionHandleInput timeout")));
}
}
////////////////////////////////////////////////////////////////////////////////////////////////////
void nextionProcessInput()
{ // Process complete incoming serial command from the Nextion panel
// Command reference: https://www.itead.cc/wiki/Nextion_Instruction_Set#Format_of_Device_Return_Data
// tl;dr: command byte, command data, 0xFF 0xFF 0xFF