-
Notifications
You must be signed in to change notification settings - Fork 835
/
IRMQTTServer.ino
3507 lines (3350 loc) · 132 KB
/
IRMQTTServer.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
/*
* Send & receive arbitrary IR codes via a web server or MQTT.
* Copyright David Conran 2016-2021
*
* Copyright:
* Code for this has been borrowed from lots of other OpenSource projects &
* resources. I'm *NOT* claiming complete Copyright ownership of all the code.
* Likewise, feel free to borrow from this as much as you want.
*
* NOTE: An IR LED circuit SHOULD be connected to the ESP if
* you want to send IR messages. e.g. GPIO4 (D2)
* A compatible IR RX modules SHOULD be connected to ESP
* if you want to capture & decode IR nessages. e.g. GPIO14 (D5)
* See 'IR_RX' in IRMQTTServer.h.
* GPIOs are configurable from the http://<your_esp's_ip_address>/gpio
* page.
*
* WARN: This is *very* advanced & complicated example code. Not for beginners.
* You are strongly suggested to try & look at other example code first
* to understand how this library works.
*
* # Instructions
*
* ## Before First Boot (i.e. Compile time)
* - Disable MQTT if desired. (see '#define MQTT_ENABLE' in IRMQTTServer.h).
*
* - The MQTT server IP is detected automatically through mDNS (aka avahi,
* bonjour, zeroconf) if the server advertises _mqtt._tcp. Disable this if
* desired (see '#define MQTT_SERVER_AUTODETECT_ENABLE' in IRMQTTServer.h).
*
* - Site specific settings:
* o Search for 'CHANGE_ME' in IRMQTTServer.h for the things you probably
* need to change for your particular situation.
* o All user changable settings are in the file IRMQTTServer.h.
*
* - Arduino IDE:
* o Install the following libraries via Library Manager
* - ArduinoJson (https://arduinojson.org/) (Version >= 6.0)
* - PubSubClient (https://pubsubclient.knolleary.net/) (Version >= 2.8.0)
* - WiFiManager (https://github.com/tzapu/WiFiManager)
* (ESP8266: Version >= 0.14, ESP32: 'master' branch.)
* o Use the smallest non-zero FILESYSTEM size you can for your board.
* (See the Tools -> Flash Size menu)
*
* - PlatformIO IDE:
* If you are using PlatformIO, this should already been done for you in
* the accompanying platformio.ini file.
*
* ## First Boot (Initial setup)
* The ESP board will boot into the WiFiManager's AP mode.
* i.e. It will create a WiFi Access Point with a SSID like: "ESP123456" etc.
* Connect to that SSID. Then point your browser to http://192.168.4.1/ and
* configure the ESP to connect to your desired WiFi network and associated
* required settings. It will remember these details on next boot if the device
* connects successfully.
* More information can be found here:
* https://github.com/tzapu/WiFiManager#how-it-works
*
* If you need to reset the WiFi and saved settings to go back to "First Boot",
* visit: http://<your_esp's_ip_address>/reset
*
* ## Normal Use (After initial setup)
* Enter 'http://<your_esp's_ip_address/' in your browser & follow the
* instructions there to send IR codes via HTTP/HTML.
* Visit the http://<your_esp's_ip_address>/gpio page to configure the GPIOs
* for the IR LED(s) and/or IR RX demodulator.
*
* You can send URLs like the following, with similar data type limitations as
* the MQTT formating in the next section. e.g:
* http://<your_esp's_ip_address>/ir?type=7&code=E0E09966
* http://<your_esp's_ip_address>/ir?type=4&code=0xf50&bits=12
* http://<your_esp's_ip_address>/ir?code=C1A2E21D&repeats=8&type=19
* http://<your_esp's_ip_address>/ir?type=31&code=40000,1,1,96,24,24,24,48,24,24,24,24,24,48,24,24,24,24,24,48,24,24,24,24,24,24,24,24,1058
* http://<your_esp's_ip_address>/ir?type=18&code=190B8050000000E0190B8070000010f0
* http://<your_esp's_ip_address>/ir?repeats=1&type=25&code=0000,006E,0022,0002,0155,00AA,0015,0040,0015,0040,0015,0015,0015,0015,0015,0015,0015,0015,0015,0015,0015,0040,0015,0040,0015,0015,0015,0040,0015,0015,0015,0015,0015,0015,0015,0040,0015,0015,0015,0015,0015,0040,0015,0040,0015,0015,0015,0015,0015,0015,0015,0015,0015,0015,0015,0040,0015,0015,0015,0015,0015,0040,0015,0040,0015,0040,0015,0040,0015,0040,0015,0640,0155,0055,0015,0E40
* If you have enabled more than 1 TX GPIO, you can use the "channel" argument:
* http://<your_esp's_ip_address>/ir?channel=0&type=7&code=E0E09966
* http://<your_esp's_ip_address>/ir?channel=1&type=7&code=E0E09966
*
* or
*
* Send a MQTT message to the topic 'ir_server/send'
* (or 'ir_server/send_1' etc if you have enabled more than 1 TX GPIO)
* using the following format (Order is important):
* protocol_num,hexcode
* e.g. 7,E0E09966
* which is: Samsung(7), Power On code, default bit size,
* default nr. of repeats.
*
* protocol_num,hexcode,bits
* e.g. 4,f50,12
* which is: Sony(4), Power Off code, 12 bits & default nr. of repeats.
*
* protocol_num,hexcode,bits,repeats
* e.g. 19,C1A2E21D,0,8
* which is: Sherwood(19), Vol Up, default bit size & repeated 8 times.
*
* 30,frequency,raw_string
* e.g. 30,38000,9000,4500,500,1500,500,750,500,750
* which is: Raw (30) @ 38kHz with a raw code of
* "9000,4500,500,1500,500,750,500,750"
*
* 31,code_string
* e.g. 31,40000,1,1,96,24,24,24,48,24,24,24,24,24,48,24,24,24,24,24,48,24,24,24,24,24,24,24,24,1058
* which is: GlobalCache (31) & "40000,1,1,96,..." (Sony Vol Up)
*
* 25,Rrepeats,hex_code_string
* e.g. 25,R1,0000,006E,0022,0002,0155,00AA,0015,0040,0015,0040,0015,0015,0015,0015,0015,0015,0015,0015,0015,0015,0015,0040,0015,0040,0015,0015,0015,0040,0015,0015,0015,0015,0015,0015,0015,0040,0015,0015,0015,0015,0015,0040,0015,0040,0015,0015,0015,0015,0015,0015,0015,0015,0015,0015,0015,0040,0015,0015,0015,0015,0015,0040,0015,0040,0015,0040,0015,0040,0015,0040,0015,0640,0155,0055,0015,0E40
* which is: Pronto (25), 1 repeat, & "0000 006E 0022 0002 ..."
* aka a "Sherwood Amp Tape Input" message.
*
* ac_protocol_num,really_long_hexcode
* e.g. 18,190B8050000000E0190B8070000010F0
* which is: Kelvinator (18) Air Con on, Low Fan, 25 deg etc.
* NOTE: Ensure you zero-pad to the correct number of digits for the
* bit/byte size you want to send as some A/C units have units
* have different sized messages. e.g. Fujitsu A/C units.
*
* Sequences.
* You can send a sequence of IR messages via MQTT using the above methods
* if you separate them with a ';' character. In addition you can add a
* pause/gap between sequenced messages by using 'P' followed immediately by
* the number of milliseconds you wish to wait (up to a max of kMaxPauseMs).
* e.g. 7,E0E09966;4,f50,12
* Send a Samsung(7) TV Power on code, followed immediately by a Sony(4)
* TV power off message.
* or: 19,C1A28877;P500;19,C1A25AA5;P500;19,C1A2E21D,0,30
* Turn on a Sherwood(19) Amplifier, Wait 1/2 a second, Switch the
* Amplifier to Video input 2, wait 1/2 a second, then send the Sherwood
* Amp the "Volume Up" message 30 times.
*
* In short:
* No spaces after/before commas.
* Values are comma separated.
* The first value is always in Decimal.
* For simple protocols, the next value (hexcode) is always hexadecimal.
* The optional bit size is in decimal.
* CAUTION: Some AC protocols DO NOT use the really_long_hexcode method.
* e.g. < 64bit AC protocols.
*
* Unix command line usage example:
* # Install a MQTT client
* $ sudo apt install mosquitto-clients
* # Send a 32-bit NEC code of 0x1234abcd via MQTT.
* $ mosquitto_pub -h 10.0.0.4 -t ir_server/send -m '3,1234abcd,32'
*
* This server will send (back) what ever IR message it just transmitted to
* the MQTT topic 'ir_server/sent' to confirm it has been performed. This works
* for messages requested via MQTT or via HTTP.
*
* Unix command line usage example:
* # Listen to MQTT acknowledgements.
* $ mosquitto_sub -h 10.0.0.4 -t ir_server/sent
*
* Incoming IR messages (from an IR remote control) will be transmitted to
* the MQTT topic 'ir_server/received'. The MQTT message will be formatted
* similar to what is required to for the 'sent' topic.
* e.g. "3,C1A2F00F,32" (Protocol,Value,Bits) for simple codes
* or "18,110B805000000060110B807000001070" (Protocol,Value) for complex codes
* Note: If the protocol is listed as -1, then that is an UNKNOWN IR protocol.
* You can't use that to recreate/resend an IR message. It's only for
* matching purposes and shouldn't be trusted.
*
* Unix command line usage example:
* # Listen via MQTT for IR messages captured by this server.
* $ mosquitto_sub -h 10.0.0.4 -t ir_server/received
*
* Note: General logging messages are also sent to 'ir_server/log' from
* time to time.
*
* ## Climate (AirCon) interface. (Advanced use)
* You can now control Air Conditioner devices that have full/detailed support
* from the IRremoteESP8266 library. See the "Aircon" page for list of supported
* devices. You can do this via HTTP/HTML or via MQTT.
*
* NOTE: It will only change the attributes you change/set. It's up to you to
* maintain a consistent set of attributes for your particular aircon.
*
* TIP: Use "-1" for 'model' if your A/C doesn't have a specific `setModel()`
* or IR class attribute. Most don't. Some do.
* e.g. PANASONIC_AC, FUJITSU_AC, WHIRLPOOL_AC
*
* ### via MQTT:
* The code listen for commands (via wildcard) on the MQTT topics at the
* `ir_server/ac/cmnd/+` level (or ir_server/ac_1/cmnd/+` if multiple TX GPIOs)
* such as:
* i.e. protocol, model, power, mode, temp, fanspeed, swingv, swingh, quiet,
* turbo, light, beep, econo, sleep, filter, clean, use_celsius
* e.g. ir_server/ac/cmnd/power, ir_server/ac/cmnd/temp,
* ir_server/ac_0/cmnd/mode, ir_server/ac_2/cmnd/fanspeed, etc.
* It will process them, and if successful and it caused a change, it will
* acknowledge this via the relevant state topic for that command.
* e.g. If the aircon/climate changes from power off to power on, it will
* send an "on" payload to "ir_server/ac/stat/power"
*
* There is a special command available to force the ESP to resend the current
* A/C state in an IR message. To do so use the `resend` command MQTT topic,
* e.g. `ir_server/ac/cmnd/resend` with a payload message of `resend`.
* There is no corresponding "stat" message update for this particular topic,
* but a log message is produced indicating it was received.
*
* NOTE: These "stat" messages have the MQTT retain flag set to on. Thus the
* MQTT broker will remember them until reset/restarted etc.
*
* The code will also periodically broadcast all possible aircon/climate state
* attributes to their corresponding "ir_server/ac/stat" topics. This ensures
* any updates to the ESP's knowledge that may have been lost in transmission
* are re-communicated. e.g. The MQTT broker being offline.
* This also helps with Home Assistant MQTT discovery.
*
* The program on boot & first successful connection to the MQTT broker, will
* try to re-acquire any previous aircon/climate state information and act
* accordingly. This will typically result in A/C IR message being sent as and
* saved state will probably be different from the defaults.
*
* NOTE: Command attributes are processed sequentially.
* e.g. Going from "25C, cool, fan low" to "27C, heat, fan high" may go
* via "27C, cool, fan low" & "27C, heat, fan low" depending on the order
* of arrival & processing of the MQTT commands.
*
* ### Home Assistant (HA) MQTT climate integration
* After you have set the Protocol (required) & Model (if needed) and any of
* the other misc aircon settings you desire, you can then add the following to
* your Home Assistant configuration, and it should allow you to
* control most of the important settings. Google Home/Assistant (via HA)
* can also control the device, but you will need to configure Home Assistant
* via it's documentation for that. It has even more limited control.
* It's far beyond the scope of these instructions to guide you through setting
* up HA and Google Home integration. See https://www.home-assistant.io/
*
* In HA's configuration.yaml, add:
*
* #### New format (Post Home Assistant 2022.6 release)
*
* mqtt:
* climate:
* - name: Living Room Aircon
* modes:
* - "off"
* - "auto"
* - "cool"
* - "heat"
* - "dry"
* - "fan_only"
* fan_modes:
* - "Auto"
* - "Min"
* - "Low"
* - "Medium"
* - "High"
* - "Max"
* swing_modes:
* - "Off"
* - "Auto"
* - "Highest"
* - "High"
* - "Middle"
* - "Low"
* - "Lowest"
* power_command_topic: "ir_server/ac/cmnd/power"
* mode_command_topic: "ir_server/ac/cmnd/mode"
* mode_state_topic: "ir_server/ac/stat/mode"
* temperature_command_topic: "ir_server/ac/cmnd/temp"
* temperature_state_topic: "ir_server/ac/stat/temp"
* fan_mode_command_topic: "ir_server/ac/cmnd/fanspeed"
* fan_mode_state_topic: "ir_server/ac/stat/fanspeed"
* swing_mode_command_topic: "ir_server/ac/cmnd/swingv"
* swing_mode_state_topic: "ir_server/ac/stat/swingv"
* min_temp: 16
* max_temp: 32
* temp_step: 1
* retain: false
*
* #### Old format (Pre Home Assistant 2022.6 release)
*
* climate:
* - platform: mqtt
* name: Living Room Aircon
* modes:
* - "off"
* - "auto"
* - "cool"
* - "heat"
* - "dry"
* - "fan_only"
* fan_modes:
* - "Auto"
* - "Min"
* - "Low"
* - "Medium"
* - "High"
* - "Max"
* swing_modes:
* - "Off"
* - "Auto"
* - "Highest"
* - "High"
* - "Middle"
* - "Low"
* - "Lowest"
* power_command_topic: "ir_server/ac/cmnd/power"
* mode_command_topic: "ir_server/ac/cmnd/mode"
* mode_state_topic: "ir_server/ac/stat/mode"
* temperature_command_topic: "ir_server/ac/cmnd/temp"
* temperature_state_topic: "ir_server/ac/stat/temp"
* fan_mode_command_topic: "ir_server/ac/cmnd/fanspeed"
* fan_mode_state_topic: "ir_server/ac/stat/fanspeed"
* swing_mode_command_topic: "ir_server/ac/cmnd/swingv"
* swing_mode_state_topic: "ir_server/ac/stat/swingv"
* min_temp: 16
* max_temp: 32
* temp_step: 1
* retain: false
*
* #### Home Assistant MQTT Discovery
* There is an option for this: 'Send MQTT Discovery' under the 'Admin' menu.
* It will produce a single MQTT Climate Discovery message for Home Assistant
* provided you have everything configured correctly here and in HA.
* This message has MQTT RETAIN set on it, so it only ever needs to be sent
* once or if the config details change etc.
*
* If you no longer want it, manually remove it from your MQTT broker.
* e.g.
* `mosquitto_pub -t homeassistant/climate/ir_server/config -n -r -d`
*
* NOTE: If you have multiple TX GPIOs configured, it *ONLY* works for the
* first TX GPIO climate. You will need to manually configure the others.
*
* ### via HTTP:
* Use the "http://<your_esp's_ip_address>/aircon/set" URL and pass on
* the arguments as needed to control your device. See the `KEY_*` #defines
* in the code for all the parameters.
* i.e. protocol, model, power, mode, temp, fanspeed, swingv, swingh, quiet,
* turbo, light, beep, econo, sleep, filter, clean, use_celsius, channel
* Example:
* http://<your_esp's_ip_address>/aircon/set?channel=0&protocol=PANASONIC_AC&model=LKE&power=on&mode=auto&fanspeed=min&temp=23
*
* NOTE: If you don't set the channel, the first GPIO (Channel 0) is used.
*
* ## Debugging & Logging
* If DEBUG is turned on, there is additional information printed on the Serial
* Port. Serial Port output may be disabled if the GPIO is used for IR.
*
* If MQTT is enabled, some information/logging is sent to the MQTT topic:
* `ir_server/log`
*
* ## Updates
* You can upload new firmware Over The Air (OTA) via the form on the device's
* "Admin" page. No need to connect to the device again via USB. \o/
* Your settings should be remembered between updates. \o/ \o/
*
* On boards with 1 Meg of flash should use an SPIFFS size of 64k if you want a
* hope of being able to load a firmware via OTA.
* Boards with only 512k flash have no chance of OTA with this firmware.
*
* ## Security
* <security-hat="on">
* There is NO authentication set on the HTTP/HTML interface by default (see
* `HTML_PASSWORD_ENABLE` to change that), and there is NO SSL/TLS (encryption)
* used by this example code.
* i.e. All usernames & passwords are sent in clear text.
* All communication to the MQTT server is in clear text.
* e.g. This on/using the public Internet is a 'Really Bad Idea<tm>'!
* You should NOT have or use this code or device exposed on an untrusted and/or
* unprotected network.
* If you allow access to OTA firmware updates, then a 'Bad Guy<tm>' could
* potentially compromise your network. OTA updates are password protected by
* default. If you are sufficiently paranoid, you SHOULD disable uploading
* firmware via OTA. (see 'FIRMWARE_OTA')
* You SHOULD also set/change all usernames & passwords.
* For extra bonus points: Use a separate untrusted SSID/vlan/network/ segment
* for your IoT stuff, including this device.
* Caveat Emptor. You have now been suitably warned.
* </security-hat>
*/
#include "IRMQTTServer.h"
#include <Arduino.h>
#include <ArduinoJson.h>
#if defined(ESP8266)
#include <ESP8266WiFi.h>
#include <ESP8266WebServer.h>
#include <ESP8266mDNS.h>
#endif // ESP8266
#if defined(ESP32)
#include <ESPmDNS.h>
#include <WebServer.h>
#include <WiFi.h>
#include <Update.h>
#endif // ESP32
#include <WiFiClient.h>
#include <DNSServer.h>
#include <WiFiManager.h>
#include <IRremoteESP8266.h>
#include <IRrecv.h>
#include <IRsend.h>
#include <IRtext.h>
#include <IRtimer.h>
#include <IRutils.h>
#include <IRac.h>
#if MQTT_ENABLE
#include <PubSubClient.h>
#endif // MQTT_ENABLE
#include <algorithm> // NOLINT(build/include)
#include <memory>
#include <string>
#ifdef ESP32
#ifdef F
#undef F
#endif // F
#define F(string) string
#endif // ESP32
using irutils::msToString;
#if REPORT_VCC
ADC_MODE(ADC_VCC);
#endif // REPORT_VCC
#ifdef SHT3X_SUPPORT
#include <WEMOS_SHT3X.h>
#endif
// Globals
uint8_t _sanity = 0;
#if defined(ESP8266)
ESP8266WebServer server(kHttpPort);
#endif // ESP8266
#if defined(ESP32)
WebServer server(kHttpPort);
#endif // ESP32
#if MDNS_ENABLE
MDNSResponder mdns;
#endif // MDNS_ENABLE
WiFiClient espClient;
WiFiManager wifiManager;
bool flagSaveWifiConfig = false;
char HttpUsername[kUsernameLength + 1] = "admin"; // Default HTTP username.
char HttpPassword[kPasswordLength + 1] = ""; // No HTTP password by default.
char Hostname[kHostnameLength + 1] = "ir_server"; // Default hostname.
uint16_t *codeArray;
uint32_t lastReconnectAttempt = 0; // MQTT last attempt reconnection number
bool boot = true;
volatile bool lockIr = false; // Primitive locking for gating the IR LED.
uint32_t sendReqCounter = 0;
bool lastSendSucceeded = false; // Store the success status of the last send.
uint32_t lastSendTime = 0;
int8_t offset; // The calculated period offset for this chip and library.
IRsend *IrSendTable[kNrOfIrTxGpios];
int8_t txGpioTable[kNrOfIrTxGpios] = {kDefaultIrLed};
String lastClimateSource;
#if IR_RX
IRrecv *irrecv = NULL;
decode_results capture; // Somewhere to store inbound IR messages.
int8_t rx_gpio = kDefaultIrRx;
String lastIrReceived = FPSTR("None");
uint32_t lastIrReceivedTime = 0;
uint32_t irRecvCounter = 0;
#endif // IR_RX
// Climate stuff
IRac *climate[kNrOfIrTxGpios];
String channel_re = FPSTR("("); // Will be built later.
uint16_t chan = 0; // The channel to use for the aircon HTML page.
TimerMs lastClimateIr = TimerMs(); // When we last sent the IR Climate mesg.
uint32_t irClimateCounter = 0; // How many have we sent?
// Store the success status of the last climate send.
bool lastClimateSucceeded = false;
bool hasClimateBeenSent = false; // Has the Climate ever been sent?
#if MQTT_ENABLE
PubSubClient mqtt_client(espClient);
String lastMqttCmd = FPSTR("None");
String lastMqttCmdTopic = FPSTR("None");
uint32_t lastMqttCmdTime = 0;
uint32_t lastConnectedTime = 0;
uint32_t lastDisconnectedTime = 0;
uint32_t mqttDisconnectCounter = 0;
uint32_t mqttSentCounter = 0;
uint32_t mqttRecvCounter = 0;
bool wasConnected = true;
char MqttServer[kHostnameLength + 1] = "10.0.0.4";
char MqttPort[kPortLength + 1] = "1883";
char MqttUsername[kUsernameLength + 1] = "";
char MqttPassword[kPasswordLength + 1] = "";
char MqttPrefix[kHostnameLength + 1] = "";
String MqttAck; // Sub-topic we send back acknowledgements on.
String MqttSend; // Sub-topic we get new commands from.
String MqttRecv; // Topic we send received IRs to.
String MqttLog; // Topic we send log messages to.
String MqttLwt; // Topic for the Last Will & Testament.
String MqttClimate; // Sub-topic for the climate topics.
String MqttClimateCmnd; // Sub-topic for the climate command topics.
#if MQTT_DISCOVERY_ENABLE
String MqttDiscovery;
String MqttUniqueId;
#if SHT3X_SUPPORT && SHT3X_MQTT_DISCOVERY_ENABLE
String MqttDiscoverySensor;
#endif // SHT3X_SUPPORT && SHT3X_MQTT_DISCOVERY_ENABLE
#endif // MQTT_DISCOVERY_ENABLE
String MqttHAName;
String MqttClientId;
#if SHT3X_SUPPORT
String MqttSensorStat;
#endif // SHT3X_SUPPORT
// Primative lock file for gating MQTT state broadcasts.
bool lockMqttBroadcast = true;
TimerMs lastBroadcast = TimerMs(); // When we last sent a broadcast.
bool hasBroadcastBeenSent = false;
#if MQTT_DISCOVERY_ENABLE
TimerMs lastDiscovery = TimerMs(); // When we last sent a Discovery.
bool hasDiscoveryBeenSent = false;
#endif // MQTT_DISCOVERY_ENABLE
TimerMs statListenTime = TimerMs(); // How long we've been listening for.
#endif // MQTT_ENABLE
bool isSerialGpioUsedByIr(void) {
const int8_t kSerialTxGpio = 1; // The GPIO serial output is sent to.
// Note: *DOES NOT* control Serial output.
#if defined(ESP32)
const int8_t kSerialRxGpio = 3; // The GPIO serial input is received on.
#endif // ESP32
// Ensure we are not trodding on anything IR related.
#if IR_RX
switch (rx_gpio) {
#if defined(ESP32)
case kSerialRxGpio:
#endif // ESP32
case kSerialTxGpio:
return true; // Serial port is in use by IR capture. Abort.
}
#endif // IR_RX
for (uint16_t i = 0; i < kNrOfIrTxGpios; i++)
switch (txGpioTable[i]) {
#if defined(ESP32)
case kSerialRxGpio:
#endif // ESP32
case kSerialTxGpio:
return true; // Serial port is in use for IR sending. Abort.
}
return false; // Not in use as far as we can tell.
}
#if SHT3X_SUPPORT
SHT3X TemperatureSensor(SHT3X_I2C_ADDRESS);
TimerMs statSensorReadTime = TimerMs();
#endif // SHT3X_SUPPORT
// Debug messages get sent to the serial port.
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wunused-parameter"
void debug(const char *str) {
#if DEBUG
if (isSerialGpioUsedByIr()) return; // Abort.
uint32_t now = millis();
Serial.printf("%07u.%03u: %s\n", now / 1000, now % 1000, str);
#endif // DEBUG
}
#pragma GCC diagnostic pop
// callback notifying us of the need to save the wifi config
void saveWifiConfigCallback(void) {
debug("saveWifiConfigCallback called.");
flagSaveWifiConfig = true;
}
// Forcibly mount the FILESYSTEM. Formatting the FILESYSTEM if needed.
//
// Returns:
// A boolean indicating success or failure.
bool mountSpiffs(void) {
debug("Mounting " FILESYSTEMSTR " ...");
if (FILESYSTEM.begin()) return true; // We mounted it okay.
// We failed the first time.
debug("Failed to mount " FILESYSTEMSTR "!\n"
"Formatting SPIFFS and trying again...");
FILESYSTEM.format();
if (!FILESYSTEM.begin()) { // Did we fail?
debug("DANGER: Failed to mount " FILESYSTEMSTR " even after formatting!");
delay(10000); // Make sure the debug message doesn't just float by.
return false;
}
return true; // Success!
}
bool saveConfig(void) {
debug("Saving the config.");
bool success = false;
DynamicJsonDocument json(kJsonConfigMaxSize);
#if MQTT_ENABLE
json[kMqttServerKey] = MqttServer;
json[kMqttPortKey] = MqttPort;
json[kMqttUserKey] = MqttUsername;
json[kMqttPassKey] = MqttPassword;
json[kMqttPrefixKey] = MqttPrefix;
#endif // MQTT_ENABLE
json[kHostnameKey] = Hostname;
json[kHttpUserKey] = HttpUsername;
json[kHttpPassKey] = HttpPassword;
#if IR_RX
json[KEY_RX_GPIO] = static_cast<int>(rx_gpio);
#endif // IR_RX
for (uint16_t i = 0; i < kNrOfIrTxGpios; i++) {
const String key = KEY_TX_GPIO + String(i);
json[key] = static_cast<int>(txGpioTable[i]);
}
if (mountSpiffs()) {
File configFile = FILESYSTEM.open(kConfigFile, "w");
if (!configFile) {
debug("Failed to open config file for writing.");
} else {
debug("Writing out the config file.");
serializeJson(json, configFile);
configFile.close();
debug("Finished writing config file.");
success = true;
}
FILESYSTEM.end();
}
return success;
}
bool loadConfigFile(void) {
bool success = false;
if (mountSpiffs()) {
debug("mounted the file system");
if (FILESYSTEM.exists(kConfigFile)) {
debug("config file exists");
File configFile = FILESYSTEM.open(kConfigFile, "r");
if (configFile) {
debug("Opened config file");
size_t size = configFile.size();
// Allocate a buffer to store contents of the file.
std::unique_ptr<char[]> buf(new char[size]);
configFile.readBytes(buf.get(), size);
DynamicJsonDocument json(kJsonConfigMaxSize);
if (!deserializeJson(json, buf.get(), kJsonConfigMaxSize)) {
debug("Json config file parsed ok.");
#if MQTT_ENABLE
strncpy(MqttServer, json[kMqttServerKey] | "", kHostnameLength);
strncpy(MqttPort, json[kMqttPortKey] | "1883", kPortLength);
strncpy(MqttUsername, json[kMqttUserKey] | "", kUsernameLength);
strncpy(MqttPassword, json[kMqttPassKey] | "", kPasswordLength);
strncpy(MqttPrefix, json[kMqttPrefixKey] | "", kHostnameLength);
#endif // MQTT_ENABLE
strncpy(Hostname, json[kHostnameKey] | "", kHostnameLength);
strncpy(HttpUsername, json[kHttpUserKey] | "", kUsernameLength);
strncpy(HttpPassword, json[kHttpPassKey] | "", kPasswordLength);
// Read in the GPIO settings.
#if IR_RX
// Single RX gpio
rx_gpio = json[KEY_RX_GPIO] | kDefaultIrRx;
#endif // IR_RX
// Potentially multiple TX gpios
for (uint16_t i = 0; i < kNrOfIrTxGpios; i++)
txGpioTable[i] = json[String(KEY_TX_GPIO + String(i)).c_str()] |
kDefaultIrLed;
debug("Recovered Json fields.");
success = true;
} else {
debug("Failed to load json config");
}
debug("Closing the config file.");
configFile.close();
}
} else {
debug("Config file doesn't exist!");
}
debug("Unmounting " FILESYSTEMSTR);
FILESYSTEM.end();
}
return success;
}
String timeElapsed(uint32_t const msec) {
String result = msToString(msec);
if (result.equalsIgnoreCase(D_STR_NOW))
return result;
else
return result + F(" ago");
}
String timeSince(uint32_t const start) {
if (start == 0)
return F("Never");
uint32_t diff = 0;
uint32_t now = millis();
if (start < now)
diff = now - start;
else
diff = UINT32_MAX - start + now;
return msToString(diff) + F(" ago");
}
String gpioToString(const int16_t gpio) {
if (gpio == kGpioUnused)
return F(D_STR_UNUSED);
else
return String(gpio);
}
int8_t getDefaultTxGpio(void) {
for (int16_t i = 0; i < kNrOfIrTxGpios; i++)
if (txGpioTable[i] != kGpioUnused) return txGpioTable[i];
return kGpioUnused;
}
// Return a string containing the comma separated list of sending gpios.
String listOfTxGpios(void) {
bool found = false;
String result = "";
for (uint16_t i = 0; i < kNrOfIrTxGpios; i++) {
if (i) result += ", ";
result += gpioToString(txGpioTable[i]);
if (!found && txGpioTable[i] == getDefaultTxGpio()) {
result += F(" (default)");
found = true;
}
}
return result;
}
String htmlMenu(void) {
String html = F("<center>");
html += htmlButton(kUrlRoot, F("Home"));
html += htmlButton(kUrlAircon, F("Aircon"));
#if EXAMPLES_ENABLE
html += htmlButton(kUrlExamples, F("Examples"));
#endif // EXAMPLES_ENABLE
html += htmlButton(kUrlInfo, F("System Info"));
html += htmlButton(kUrlAdmin, F("Admin"));
html += F("</center><hr>");
return html;
}
String htmlOptionItem(const String value, const String text, bool selected) {
String html = F("<option value='");
html += value + '\'';
if (selected) html += F(" selected='selected'");
html += '>' + text + F("</option>");
return html;
}
String htmlSelectAcStateProtocol(const String name, const decode_type_t def,
const bool simple) {
String html = "<select name='" + name + "'>";
for (uint8_t i = 1; i <= decode_type_t::kLastDecodeType; i++) {
if (simple ^ hasACState((decode_type_t)i)) {
switch (i) {
case decode_type_t::RAW:
case decode_type_t::PRONTO:
case decode_type_t::GLOBALCACHE:
break;
default:
html += htmlOptionItem(String(i), typeToString((decode_type_t)i),
i == def);
}
}
}
html += F("</select>");
return html;
}
// Root web page with example usage etc.
void handleRoot(void) {
#if HTML_PASSWORD_ENABLE
if (!server.authenticate(HttpUsername, HttpPassword)) {
debug("Basic HTTP authentication failure for /.");
return server.requestAuthentication();
}
#endif
String html = htmlHeader(F("ESP IR MQTT Server"));
html += F("<center><small><i>" _MY_VERSION_ "</i></small></center>");
html += htmlMenu();
html += F(
"<h3>Send a simple IR message</h3><p>"
"<form method='POST' action='/ir' enctype='multipart/form-data'>"
D_STR_PROTOCOL ": ");
html += htmlSelectAcStateProtocol(KEY_TYPE, decode_type_t::NEC, true);
html += F(
" " D_STR_CODE ": 0x<input type='text' name='" KEY_CODE "' min='0' "
"value='0' size='16' maxlength='16'> "
D_STR_BITS ": "
"<select name='" KEY_BITS "'>"
"<option selected='selected' value='0'>Default</option>"); // Default
for (uint8_t i = 0; i < sizeof(kCommonBitSizes); i++) {
String num = String(kCommonBitSizes[i]);
html += F("<option value='");
html += num;
html += F("'>");
html += num;
html += F("</option>");
}
html += F(
"</select>"
" " D_STR_REPEAT ": <input type='number' name='" KEY_REPEAT "' min='0' "
"max='99' value='0' size='2' maxlength='2'>"
" <input type='submit' value='Send " D_STR_CODE "'>"
"</form>"
"<br><hr>"
"<h3>Send a complex (Air Conditioner) IR message</h3><p>"
"<form method='POST' action='/ir' enctype='multipart/form-data'>"
D_STR_PROTOCOL ": ");
html += htmlSelectAcStateProtocol(KEY_TYPE, decode_type_t::KELVINATOR, false);
html += F(
" State " D_STR_CODE ": 0x"
"<input type='text' name='" KEY_CODE "' size='");
html += String(kStateSizeMax * 2);
html += F("' maxlength='");
html += String(kStateSizeMax * 2);
html += F("'"
" value='"
#if EXAMPLES_ENABLE
"190B8050000000E0190B8070000010F0"
#endif // EXAMPLES_ENABLE
"'>"
" <input type='submit' value='Send A/C " D_STR_CODE "'>"
"</form>"
"<br><hr>"
"<h3>Send an IRremote Raw IR message</h3><p>"
"<form method='POST' action='/ir' enctype='multipart/form-data'>"
"<input type='hidden' name='" KEY_TYPE "' value='30'>"
"String: (freq,array data) <input type='text' name='" KEY_CODE "'"
" size='132' value='"
#if EXAMPLES_ENABLE
"38000,4420,4420,520,1638,520,1638,520,1638,520,520,520,520,520,"
"520,520,520,520,520,520,1638,520,1638,520,1638,520,520,520,"
"520,520,520,520,520,520,520,520,520,520,1638,520,520,520,520,520,"
"520,520,520,520,520,520,520,520,1638,520,520,520,1638,520,1638,520,"
"1638,520,1638,520,1638,520,1638,520"
#endif // EXAMPLES_ENABLE
"'>"
" <input type='submit' value='Send Raw'>"
"</form>"
"<br><hr>"
"<h3>Send a <a href='https://irdb.globalcache.com/'>GlobalCache</a>"
" IR message</h3><p>"
"<form method='POST' action='/ir' enctype='multipart/form-data'>"
"<input type='hidden' name='" KEY_TYPE "' value='31'>"
"String: 1:1,1,<input type='text' name='" KEY_CODE "' size='132'"
" value='"
#if EXAMPLES_ENABLE
"38000,1,1,170,170,20,63,20,63,20,63,20,20,20,20,20,20,20,20,20,"
"20,20,63,20,63,20,63,20,20,20,20,20,20,20,20,20,20,20,20,20,63,20,"
"20,20,20,20,20,20,20,20,20,20,20,20,63,20,20,20,63,20,63,20,63,20,"
"63,20,63,20,63,20,1798"
#endif // EXAMPLES_ENABLE
"'>"
" <input type='submit' value='Send GlobalCache'>"
"</form>"
"<br><hr>"
"<h3>Send a <a href='http://www.remotecentral.com/cgi-bin/files/rcfiles.cgi"
"?area=pronto&db=discrete'>Pronto code</a> IR message</h3><p>"
"<form method='POST' action='/ir' enctype='multipart/form-data'>"
"<input type='hidden' name='" KEY_TYPE "' value='25'>"
"String (comma separated): <input type='text' name='" KEY_CODE "'"
" size='132' value='"
#if EXAMPLES_ENABLE
"0000,0067,0000,0015,0060,0018,0018,0018,0030,0018,0030,0018,"
"0030,0018,0018,0018,0030,0018,0018,0018,0018,0018,0030,0018,0018,"
"0018,0030,0018,0030,0018,0030,0018,0018,0018,0018,0018,0030,0018,"
"0018,0018,0018,0018,0030,0018,0018,03f6"
#endif // EXAMPLES_ENABLE
"'>"
" " D_STR_REPEAT ": <input type='number' name='" KEY_REPEAT "' min='0' "
"max='99' value='0' size='2' maxlength='2'>"
" <input type='submit' value='Send Pronto'>"
"</form>"
"<br>");
html += htmlEnd();
server.send(200, "text/html", html);
}
String addJsReloadUrl(const String url, const uint16_t timeout_s,
const bool notify) {
String html = F(
"<script type=\"text/javascript\">\n"
"<!--\n"
" function Redirect() {\n"
" window.location=\"");
html += url;
html += F("\";\n"
" }\n"
"\n");
if (notify && timeout_s) {
html += F(" document.write(\"You will be redirected to the main page in ");
html += String(timeout_s);
html += F(" " D_STR_SECONDS ".\");\n");
}
html += F(" setTimeout('Redirect()', ");
html += String(timeout_s * 1000); // Convert to mSecs
html += F(");\n"
"//-->\n"
"</script>\n");
return html;
}
#if EXAMPLES_ENABLE
// Web page with hardcoded example usage etc.
void handleExamples(void) {
#if HTML_PASSWORD_ENABLE
if (!server.authenticate(HttpUsername, HttpPassword)) {
debug("Basic HTTP authentication failure for /examples.");
return server.requestAuthentication();
}
#endif
String html = htmlHeader(F("IR MQTT examples"));
html += htmlMenu();
html += F(
"<h3>Hardcoded examples</h3>"
"<p><a href=\"ir?" KEY_CODE "=38000,1,69,341,171,21,64,21,64,21,21,21,21,"
"21,21,21,21,21,21,21,64,21,64,21,21,21,64,21,21,21,21,21,21,21,64,21,"
"21,21,64,21,21,21,21,21,21,21,64,21,21,21,21,21,21,21,21,21,64,21,64,"
"21,64,21,21,21,64,21,64,21,64,21,1600,341,85,21,3647"
"&" KEY_TYPE "=31\">Sherwood Amp " D_STR_ON " (GlobalCache)</a></p>"
"<p><a href=\"ir?" KEY_CODE "=38000,8840,4446,546,1664,546,1664,546,546,"
"546,546,546,546,546,546,546,546,546,1664,546,1664,546,546,546,1664,"
"546,546,546,546,546,546,546,1664,546,546,546,1664,546,546,546,1664,"
"546,1664,546,1664,546,546,546,546,546,546,546,546,546,1664,546,546,"
"546,546,546,546,546,1664,546,1664,546,1664,546,41600,8840,2210,546"
"&" KEY_TYPE "=30\">Sherwood Amp " D_STR_OFF " (Raw)</a></p>"
"<p><a href=\"ir?" KEY_CODE "=0000,006E,0022,0002,0155,00AA,0015,0040,0015,"
"0040,0015,0015,0015,0015,0015,0015,0015,0015,0015,0015,0015,0040,0015,"
"0040,0015,0015,0015,0040,0015,0015,0015,0015,0015,0015,0015,0040,0015,"
"0015,0015,0015,0015,0040,0015,0040,0015,0015,0015,0015,0015,0015,0015,"
"0015,0015,0015,0015,0040,0015,0015,0015,0015,0015,0040,0015,0040,0015,"
"0040,0015,0040,0015,0040,0015,0640,0155,0055,0015,0E40"
"&" KEY_TYPE "=25&" KEY_REPEAT "=1\">"
"Sherwood Amp Input TAPE (Pronto)</a></p>"
"<p><a href=\"ir?" KEY_TYPE "=7&" KEY_CODE "=E0E09966\">TV " D_STR_ON
" (Samsung)</a></p>"
"<p><a href=\"ir?" KEY_TYPE "=4&" KEY_CODE "=0xf50&bits=12\">" D_STR_POWER
" " D_STR_OFF " (Sony 12 " D_STR_BITS ")</a></p>"
"<p><a href=\"aircon/set?protocol=PANASONIC_AC&"
KEY_MODEL "=LKE&"
KEY_POWER "=on&"
KEY_MODE "=auto&"
KEY_FANSPEED "=min&"
KEY_TEMP "=23\">"
"Panasonic A/C " D_STR_MODEL " LKE, " D_STR_ON ", " D_STR_AUTO " "
D_STR_MODE ", " D_STR_MIN " " D_STR_FAN ", 23C"
" <i>(via HTTP aircon interface)</i></a></p>"
"<p><a href=\"aircon/set?" KEY_TEMP "=27\">"
"Change just the " D_STR_TEMP " to 27C <i>"
"(via HTTP aircon interface)</i></a></p>"
"<p><a href=\"aircon/set?" KEY_POWER "=off&" KEY_MODE "=off\">"
"Turn " D_STR_OFF " the current A/C <i>("
"via HTTP aircon interface)</i></a></p>"
"<br><hr>");
html += htmlEnd();
server.send(200, "text/html", html);
}
#endif // EXAMPLES_ENABLE
String htmlSelectBool(const String name, const bool def) {
String html = String(F("<select name='")) + name + F("'>");
for (uint16_t i = 0; i < 2; i++)
html += htmlOptionItem(IRac::boolToString(i), IRac::boolToString(i),
i == def);
html += F("</select>");
return html;
}
String htmlDisableCheckbox(const String name, const String targetControlId,
const bool checked, const String toggleJsFnName) {
String html = String(F("<input type='checkbox' name='")) + name + F("' id='")
+ name + F("' onclick=\"") + toggleJsFnName + F("(this, '") +
targetControlId + F("')\"");
if (checked) {
html += F(" checked");
}
html += "/><label for='" + name + F("'>Disabled</label>");
return html;
}
String htmlSelectClimateProtocol(const String name, const decode_type_t def) {
String html = String(F("<select name='")) + name + F("'>");
for (uint8_t i = 1; i <= decode_type_t::kLastDecodeType; i++) {
if (IRac::isProtocolSupported((decode_type_t)i)) {
html += htmlOptionItem(String(i), typeToString((decode_type_t)i),
i == def);
}
}
html += F("</select>");
return html;
}
String htmlSelectModel(const String name, const int16_t def) {
String html = String(F("<select name='")) + name + F("'>");
for (int16_t i = -1; i <= 6; i++) {
String num = String(i);