From d392cf2f77d2e05f978562acfa410ff115aea672 Mon Sep 17 00:00:00 2001 From: arendst Date: Sat, 28 Jan 2017 14:41:01 +0100 Subject: [PATCH] v3.9.4 --- api/arduino/sonoff.ino.bin | Bin 0 -> 406768 bytes api/upload-arduino.php | 15 + arduino/espupload.py | 112 ++ arduino/version 2.3.0/boards.txt | 1900 +++++++++++++++++++ arduino/version 2.3.0/platform.txt | 130 ++ sonoff/README.md | 33 + sonoff/_releasenotes.ino | 514 ++++++ sonoff/sonoff.ino | 2760 ++++++++++++++++++++++++++++ sonoff/sonoff_template.h | 248 +++ sonoff/support.h | 17 + sonoff/support.ino | 1005 ++++++++++ sonoff/user_config.h | 147 ++ sonoff/user_config_override.h | 23 + sonoff/webserver.ino | 1521 +++++++++++++++ sonoff/xdrv_domoticz.ino | 376 ++++ sonoff/xdrv_wemohue.ino | 233 +++ sonoff/xdrv_ws2812.ino | 465 +++++ sonoff/xsns_bh1750.ino | 110 ++ sonoff/xsns_bmp.ino | 477 +++++ sonoff/xsns_dht.ino | 214 +++ sonoff/xsns_dht2.ino | 93 + sonoff/xsns_ds18b20.ino | 206 +++ sonoff/xsns_ds18x20.ino | 201 ++ sonoff/xsns_hlw8012.ino | 412 +++++ sonoff/xsns_htu21.ino | 271 +++ 25 files changed, 11483 insertions(+) create mode 100644 api/arduino/sonoff.ino.bin create mode 100644 api/upload-arduino.php create mode 100644 arduino/espupload.py create mode 100644 arduino/version 2.3.0/boards.txt create mode 100644 arduino/version 2.3.0/platform.txt create mode 100644 sonoff/README.md create mode 100644 sonoff/_releasenotes.ino create mode 100644 sonoff/sonoff.ino create mode 100644 sonoff/sonoff_template.h create mode 100644 sonoff/support.h create mode 100644 sonoff/support.ino create mode 100644 sonoff/user_config.h create mode 100644 sonoff/user_config_override.h create mode 100644 sonoff/webserver.ino create mode 100644 sonoff/xdrv_domoticz.ino create mode 100644 sonoff/xdrv_wemohue.ino create mode 100644 sonoff/xdrv_ws2812.ino create mode 100644 sonoff/xsns_bh1750.ino create mode 100644 sonoff/xsns_bmp.ino create mode 100644 sonoff/xsns_dht.ino create mode 100644 sonoff/xsns_dht2.ino create mode 100644 sonoff/xsns_ds18b20.ino create mode 100644 sonoff/xsns_ds18x20.ino create mode 100644 sonoff/xsns_hlw8012.ino create mode 100644 sonoff/xsns_htu21.ino diff --git a/api/arduino/sonoff.ino.bin b/api/arduino/sonoff.ino.bin new file mode 100644 index 0000000000000000000000000000000000000000..4f5a63e87ea8a657cc91311a9dfd729b2167c59f GIT binary patch literal 406768 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

rf(3x671?ICk$bY!sa_A6UV!^(ux#V(le#`f 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'fL>6HR#TOIfFWnT27vd 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

{!csQ*R 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¬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 T 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 O?< \ No newline at end of file diff --git a/arduino/espupload.py b/arduino/espupload.py new file mode 100644 index 000000000..0b2766cd0 --- /dev/null +++ b/arduino/espupload.py @@ -0,0 +1,112 @@ +#!/usr/bin/python +# +# espupload by Theo Arends - 20170103 +# +# Uploads binary file to OTA server +# +# Execute: espupload -i -p -f +# +# Needs pycurl +# - pip install pycurl + +import sys +import os +import optparse +import logging +import pycurl + +HOST_ADDR = "domus1" +HOST_PORT = 80 +HOST_URL = "/api/upload-arduino.php" + +def upload(hostAddr, hostPort, filename): + url = 'http://%s:%d%s' % (hostAddr, hostPort, HOST_URL) + c = pycurl.Curl() + c.setopt(c.URL, url) + # The "Expect:" is there to suppress "Expect: 100-continue" behaviour that is + # the default in libcurl when posting large bodies (and fails on lighttpd). + c.setopt(c.HTTPHEADER, ["Expect:"]) + c.setopt(c.HTTPPOST, [('file', (c.FORM_FILE, filename, )), ]) + c.perform() + c.close() + +def parser(): + parser = optparse.OptionParser( + usage = "%prog [options]", + description = "Upload image to over the air Host server for the esp8266 module with OTA support." + ) + + # destination ip and port + group = optparse.OptionGroup(parser, "Destination") + group.add_option("-i", "--host_ip", + dest = "host_ip", + action = "store", + help = "Host IP Address.", + default = HOST_ADDR + ) + group.add_option("-p", "--host_port", + dest = "host_port", + type = "int", + help = "Host server ota Port. Default 80", + default = HOST_PORT + ) + parser.add_option_group(group) + + # image + group = optparse.OptionGroup(parser, "Image") + group.add_option("-f", "--file", + dest = "image", + help = "Image file.", + metavar="FILE", + default = None + ) + parser.add_option_group(group) + + # output group + group = optparse.OptionGroup(parser, "Output") + group.add_option("-d", "--debug", + dest = "debug", + help = "Show debug output. And override loglevel with debug.", + action = "store_true", + default = False + ) + parser.add_option_group(group) + + (options, args) = parser.parse_args() + + return options +# end parser + +def main(args): + # get options + options = parser() + + # adapt log level + loglevel = logging.WARNING + if (options.debug): + loglevel = logging.DEBUG + # end if + + # logging + logging.basicConfig(level = loglevel, format = '%(asctime)-8s [%(levelname)s]: %(message)s', datefmt = '%H:%M:%S') + + logging.debug("Options: %s", str(options)) + + if (not options.host_ip or not options.image): + logging.critical("Not enough arguments.") + + return 1 + # end if + + if not os.path.exists(options.image): + logging.critical('Sorry: the file %s does not exist', options.image) + + return 1 + # end if + + upload(options.host_ip, options.host_port, options.image) +# end main + +if __name__ == '__main__': + sys.exit(main(sys.argv)) +# end if \ No newline at end of file diff --git a/arduino/version 2.3.0/boards.txt b/arduino/version 2.3.0/boards.txt new file mode 100644 index 000000000..3cc742521 --- /dev/null +++ b/arduino/version 2.3.0/boards.txt @@ -0,0 +1,1900 @@ +menu.UploadSpeed=Upload Speed +menu.CpuFrequency=CPU Frequency +menu.FlashSize=Flash Size +menu.FlashMode=Flash Mode +menu.FlashFreq=Flash Frequency +menu.UploadTool=Upload Using +menu.ResetMethod=Reset Method +menu.ESPModule=Module +menu.Debug=Debug port +menu.DebugLevel=Debug Level +menu.LwIPVariant=lwIP Variant + +############################################################## +generic.name=Generic ESP8266 Module + +generic.upload.tool=esptool +generic.upload.speed=115200 +generic.upload.resetmethod=ck +generic.upload.maximum_size=434160 +generic.upload.maximum_data_size=81920 +generic.upload.wait_for_upload_port=true +generic.serial.disableDTR=true +generic.serial.disableRTS=true + +generic.build.mcu=esp8266 +generic.build.f_cpu=80000000L +generic.build.board=ESP8266_ESP01 +generic.build.core=esp8266 +generic.build.variant=generic +generic.build.flash_mode=qio +generic.build.spiffs_pagesize=256 +generic.build.debug_port= +generic.build.debug_level= + +generic.menu.UploadTool.esptool=Serial +generic.menu.UploadTool.esptool.upload.tool=esptool +generic.menu.UploadTool.esptool.upload.verbose=-vv +generic.menu.UploadTool.espupload=OTA_upload +generic.menu.UploadTool.espupload.upload.tool=espupload + +generic.menu.CpuFrequency.80=80 MHz +generic.menu.CpuFrequency.80.build.f_cpu=80000000L +generic.menu.CpuFrequency.160=160 MHz +generic.menu.CpuFrequency.160.build.f_cpu=160000000L + +generic.menu.FlashFreq.40=40MHz +generic.menu.FlashFreq.40.build.flash_freq=40 +generic.menu.FlashFreq.80=80MHz +generic.menu.FlashFreq.80.build.flash_freq=80 + +generic.menu.FlashMode.dio=DIO +generic.menu.FlashMode.dio.build.flash_mode=dio +generic.menu.FlashMode.qio=QIO +generic.menu.FlashMode.qio.build.flash_mode=qio +generic.menu.FlashMode.dout=DOUT +generic.menu.FlashMode.dout.build.flash_mode=dout +generic.menu.FlashMode.qout=QOUT +generic.menu.FlashMode.qout.build.flash_mode=qout + +generic.menu.UploadSpeed.115200=115200 +generic.menu.UploadSpeed.115200.upload.speed=115200 +generic.menu.UploadSpeed.9600=9600 +generic.menu.UploadSpeed.9600.upload.speed=9600 +generic.menu.UploadSpeed.57600=57600 +generic.menu.UploadSpeed.57600.upload.speed=57600 +generic.menu.UploadSpeed.256000.windows=256000 +generic.menu.UploadSpeed.256000.upload.speed=256000 +generic.menu.UploadSpeed.230400.linux=230400 +generic.menu.UploadSpeed.230400.macosx=230400 +generic.menu.UploadSpeed.230400.upload.speed=230400 +generic.menu.UploadSpeed.460800.linux=460800 +generic.menu.UploadSpeed.460800.macosx=460800 +generic.menu.UploadSpeed.460800.upload.speed=460800 +generic.menu.UploadSpeed.512000.windows=512000 +generic.menu.UploadSpeed.512000.upload.speed=512000 +generic.menu.UploadSpeed.921600=921600 +generic.menu.UploadSpeed.921600.upload.speed=921600 + +generic.menu.FlashSize.512K64=512K (64K SPIFFS) +generic.menu.FlashSize.512K64.build.flash_size=512K +generic.menu.FlashSize.512K64.build.flash_ld=eagle.flash.512k64.ld +generic.menu.FlashSize.512K64.build.spiffs_start=0x6B000 +generic.menu.FlashSize.512K64.build.spiffs_end=0x7B000 +generic.menu.FlashSize.512K64.build.spiffs_blocksize=4096 +generic.menu.FlashSize.512K64.upload.maximum_size=434160 + +generic.menu.FlashSize.512K128=512K (128K SPIFFS) +generic.menu.FlashSize.512K128.build.flash_size=512K +generic.menu.FlashSize.512K128.build.flash_ld=eagle.flash.512k128.ld +generic.menu.FlashSize.512K128.build.spiffs_start=0x5B000 +generic.menu.FlashSize.512K128.build.spiffs_end=0x7B000 +generic.menu.FlashSize.512K128.build.spiffs_blocksize=4096 +generic.menu.FlashSize.512K128.upload.maximum_size=368624 + +generic.menu.FlashSize.512K0=512K (no SPIFFS) +generic.menu.FlashSize.512K0.build.flash_size=512K +generic.menu.FlashSize.512K0.build.flash_ld=eagle.flash.512k0.ld +generic.menu.FlashSize.512K0.upload.maximum_size=499696 + +generic.menu.FlashSize.1M512=1M (512K SPIFFS) +generic.menu.FlashSize.1M512.build.flash_size=1M +generic.menu.FlashSize.1M512.build.flash_ld=eagle.flash.1m512.ld +generic.menu.FlashSize.1M512.build.spiffs_start=0x7B000 +generic.menu.FlashSize.1M512.build.spiffs_end=0xFB000 +generic.menu.FlashSize.1M512.build.spiffs_blocksize=8192 +generic.menu.FlashSize.1M512.upload.maximum_size=499696 + +generic.menu.FlashSize.1M256=1M (256K SPIFFS) +generic.menu.FlashSize.1M256.build.flash_size=1M +generic.menu.FlashSize.1M256.build.flash_ld=eagle.flash.1m256.ld +generic.menu.FlashSize.1M256.build.spiffs_start=0xBB000 +generic.menu.FlashSize.1M256.build.spiffs_end=0xFB000 +generic.menu.FlashSize.1M256.build.spiffs_blocksize=4096 +generic.menu.FlashSize.1M256.upload.maximum_size=761840 + +generic.menu.FlashSize.1M192=1M (192K SPIFFS) +generic.menu.FlashSize.1M192.build.flash_size=1M +generic.menu.FlashSize.1M192.build.flash_ld=eagle.flash.1m192.ld +generic.menu.FlashSize.1M192.build.spiffs_start=0xCB000 +generic.menu.FlashSize.1M192.build.spiffs_end=0xFB000 +generic.menu.FlashSize.1M192.build.spiffs_blocksize=4096 +generic.menu.FlashSize.1M192.upload.maximum_size=827376 + +generic.menu.FlashSize.1M160=1M (160K SPIFFS) +generic.menu.FlashSize.1M160.build.flash_size=1M +generic.menu.FlashSize.1M160.build.flash_ld=eagle.flash.1m160.ld +generic.menu.FlashSize.1M160.build.spiffs_start=0xD3000 +generic.menu.FlashSize.1M160.build.spiffs_end=0xFB000 +generic.menu.FlashSize.1M160.build.spiffs_blocksize=4096 +generic.menu.FlashSize.1M160.upload.maximum_size=860144 + +generic.menu.FlashSize.1M144=1M (144K SPIFFS) +generic.menu.FlashSize.1M144.build.flash_size=1M +generic.menu.FlashSize.1M144.build.flash_ld=eagle.flash.1m144.ld +generic.menu.FlashSize.1M144.build.spiffs_start=0xD7000 +generic.menu.FlashSize.1M144.build.spiffs_end=0xFB000 +generic.menu.FlashSize.1M144.build.spiffs_blocksize=4096 +generic.menu.FlashSize.1M144.upload.maximum_size=876528 + +generic.menu.FlashSize.1M128=1M (128K SPIFFS) +generic.menu.FlashSize.1M128.build.flash_size=1M +generic.menu.FlashSize.1M128.build.flash_ld=eagle.flash.1m128.ld +generic.menu.FlashSize.1M128.build.spiffs_start=0xDB000 +generic.menu.FlashSize.1M128.build.spiffs_end=0xFB000 +generic.menu.FlashSize.1M128.build.spiffs_blocksize=4096 +generic.menu.FlashSize.1M128.upload.maximum_size=892912 + +generic.menu.FlashSize.1M64=1M (64K SPIFFS) +generic.menu.FlashSize.1M64.build.flash_size=1M +generic.menu.FlashSize.1M64.build.flash_ld=eagle.flash.1m64.ld +generic.menu.FlashSize.1M64.build.spiffs_start=0xEB000 +generic.menu.FlashSize.1M64.build.spiffs_end=0xFB000 +generic.menu.FlashSize.1M64.build.spiffs_blocksize=4096 +generic.menu.FlashSize.1M64.upload.maximum_size=958448 + +generic.menu.FlashSize.2M=2M (1M SPIFFS) +generic.menu.FlashSize.2M.build.flash_size=2M +generic.menu.FlashSize.2M.build.flash_ld=eagle.flash.2m.ld +generic.menu.FlashSize.2M.build.spiffs_start=0x100000 +generic.menu.FlashSize.2M.build.spiffs_end=0x1FB000 +generic.menu.FlashSize.2M.build.spiffs_blocksize=8192 +generic.menu.FlashSize.2M.upload.maximum_size=1044464 + +generic.menu.FlashSize.4M1M=4M (1M SPIFFS) +generic.menu.FlashSize.4M1M.build.flash_size=4M +generic.menu.FlashSize.4M1M.build.flash_ld=eagle.flash.4m1m.ld +generic.menu.FlashSize.4M1M.build.spiffs_start=0x300000 +generic.menu.FlashSize.4M1M.build.spiffs_end=0x3FB000 +generic.menu.FlashSize.4M1M.build.spiffs_blocksize=8192 +generic.menu.FlashSize.4M1M.build.spiffs_pagesize=256 +generic.menu.FlashSize.4M1M.upload.maximum_size=1044464 + +generic.menu.FlashSize.4M3M=4M (3M SPIFFS) +generic.menu.FlashSize.4M3M.build.flash_size=4M +generic.menu.FlashSize.4M3M.build.flash_ld=eagle.flash.4m.ld +generic.menu.FlashSize.4M3M.build.spiffs_start=0x100000 +generic.menu.FlashSize.4M3M.build.spiffs_end=0x3FB000 +generic.menu.FlashSize.4M3M.build.spiffs_blocksize=8192 +generic.menu.FlashSize.4M3M.upload.maximum_size=1044464 + +generic.menu.ResetMethod.ck=ck +generic.menu.ResetMethod.ck.upload.resetmethod=ck +generic.menu.ResetMethod.nodemcu=nodemcu +generic.menu.ResetMethod.nodemcu.upload.resetmethod=nodemcu + +generic.menu.Debug.Disabled=Disabled +generic.menu.Debug.Disabled.build.debug_port= +generic.menu.Debug.Serial=Serial +generic.menu.Debug.Serial.build.debug_port=-DDEBUG_ESP_PORT=Serial +generic.menu.Debug.Serial1=Serial1 +generic.menu.Debug.Serial1.build.debug_port=-DDEBUG_ESP_PORT=Serial1 + +generic.menu.DebugLevel.None____=None +generic.menu.DebugLevel.None____.build.debug_level= +generic.menu.DebugLevel.Core____=Core +generic.menu.DebugLevel.Core____.build.debug_level=-DDEBUG_ESP_CORE +generic.menu.DebugLevel.SSL_____=Core + SSL +generic.menu.DebugLevel.SSL_____.build.debug_level=-DDEBUG_ESP_CORE -DDEBUG_ESP_SSL +generic.menu.DebugLevel.SSL_MEM_=Core + SSL + TLS Mem +generic.menu.DebugLevel.SSL_MEM_.build.debug_level=-DDEBUG_ESP_CORE -DDEBUG_ESP_SSL -DDEBUG_TLS_MEM +generic.menu.DebugLevel.WiFic___=Core + WiFi +generic.menu.DebugLevel.WiFic___.build.debug_level=-DDEBUG_ESP_CORE -DDEBUG_ESP_WIFI +generic.menu.DebugLevel.WiFi____=WiFi +generic.menu.DebugLevel.WiFi____.build.debug_level=-DDEBUG_ESP_WIFI +generic.menu.DebugLevel.HTTPClient=HTTPClient +generic.menu.DebugLevel.HTTPClient.build.debug_level=-DDEBUG_ESP_HTTP_CLIENT +generic.menu.DebugLevel.HTTPClient2=HTTPClient + SSL +generic.menu.DebugLevel.HTTPClient2.build.debug_level=-DDEBUG_ESP_HTTP_CLIENT -DDEBUG_ESP_SSL +generic.menu.DebugLevel.HTTPUpdate=HTTPUpdate +generic.menu.DebugLevel.HTTPUpdate.build.debug_level=-DDEBUG_ESP_HTTP_UPDATE +generic.menu.DebugLevel.HTTPUpdate2=HTTPClient + HTTPUpdate +generic.menu.DebugLevel.HTTPUpdate2.build.debug_level=-DDEBUG_ESP_HTTP_UPDATE -DDEBUG_ESP_HTTP_UPDATE +generic.menu.DebugLevel.HTTPUpdate3=HTTPClient + HTTPUpdate + Updater +generic.menu.DebugLevel.HTTPUpdate3.build.debug_level=-DDEBUG_ESP_HTTP_UPDATE -DDEBUG_ESP_HTTP_UPDATE -DDEBUG_ESP_UPDATER +generic.menu.DebugLevel.HTTPServer=HTTPServer +generic.menu.DebugLevel.HTTPServer.build.debug_level=-DDEBUG_ESP_HTTP_SERVER +generic.menu.DebugLevel.UPDATER=Updater +generic.menu.DebugLevel.UPDATER.build.debug_level=-DDEBUG_ESP_UPDATER +generic.menu.DebugLevel.OTA_____=OTA +generic.menu.DebugLevel.OTA_____.build.debug_level=-DDEBUG_ESP_OTA +generic.menu.DebugLevel.OTA2____=OTA + Updater +generic.menu.DebugLevel.OTA2____.build.debug_level=-DDEBUG_ESP_OTA -DDEBUG_ESP_UPDATER +generic.menu.DebugLevel.all_____=All +generic.menu.DebugLevel.all_____.build.debug_level=-DDEBUG_ESP_CORE -DDEBUG_ESP_SSL -DDEBUG_ESP_WIFI -DDEBUG_ESP_HTTP_CLIENT -DDEBUG_ESP_HTTP_UPDATE -DDEBUG_ESP_HTTP_SERVER -DDEBUG_ESP_UPDATER -DDEBUG_ESP_OTA -DDEBUG_TLS_MEM + +# disabled because espressif's bootloader refuses to write above 4M +# generic.menu.FlashSize.8M=8M (7M SPIFFS) +# generic.menu.FlashSize.8M.build.flash_size=1M +# generic.menu.FlashSize.8M.build.flash_ld=eagle.flash.8m.ld +# generic.menu.FlashSize.8M.build.spiffs_start=0x100000 +# generic.menu.FlashSize.8M.build.spiffs_end=0x800000 +# generic.menu.FlashSize.8M.build.spiffs_blocksize=8192 +# generic.menu.FlashSize.16M=16M (15M SPIFFS) +# generic.menu.FlashSize.16M.build.flash_size=1M +# generic.menu.FlashSize.16M.build.flash_ld=eagle.flash.16m.ld +# generic.menu.FlashSize.16M.build.spiffs_start=0x100000 +# generic.menu.FlashSize.16M.build.spiffs_end=0x1000000 +# generic.menu.FlashSize.16M.build.spiffs_blocksize=8192 + +############################################################## +# ESP8285 chip has built-in 1MB flash + +esp8285.name=Generic ESP8285 Module + +esp8285.upload.tool=esptool +esp8285.upload.speed=115200 +esp8285.upload.resetmethod=ck +esp8285.upload.maximum_size=434160 +esp8285.upload.maximum_data_size=81920 +esp8285.upload.wait_for_upload_port=true +esp8285.serial.disableDTR=true +esp8285.serial.disableRTS=true + +esp8285.build.mcu=esp8266 +esp8285.build.f_cpu=80000000L +esp8285.build.board=ESP8266_ESP01 +esp8285.build.core=esp8266 +esp8285.build.variant=generic +esp8285.build.flash_mode=dout +esp8285.build.flash_freq=40 +esp8285.build.spiffs_pagesize=256 +esp8285.build.debug_port= +esp8285.build.debug_level= + +esp8285.menu.UploadTool.esptool=Serial +esp8285.menu.UploadTool.esptool.upload.tool=esptool +esp8285.menu.UploadTool.esptool.upload.verbose=-vv +esp8285.menu.UploadTool.espupload=OTA_upload +esp8285.menu.UploadTool.espupload.upload.tool=espupload + +esp8285.menu.CpuFrequency.80=80 MHz +esp8285.menu.CpuFrequency.80.build.f_cpu=80000000L +esp8285.menu.CpuFrequency.160=160 MHz +esp8285.menu.CpuFrequency.160.build.f_cpu=160000000L + +esp8285.menu.UploadSpeed.115200=115200 +esp8285.menu.UploadSpeed.115200.upload.speed=115200 +esp8285.menu.UploadSpeed.9600=9600 +esp8285.menu.UploadSpeed.9600.upload.speed=9600 +esp8285.menu.UploadSpeed.57600=57600 +esp8285.menu.UploadSpeed.57600.upload.speed=57600 +esp8285.menu.UploadSpeed.256000.windows=256000 +esp8285.menu.UploadSpeed.256000.upload.speed=256000 +esp8285.menu.UploadSpeed.230400.linux=230400 +esp8285.menu.UploadSpeed.230400.macosx=230400 +esp8285.menu.UploadSpeed.230400.upload.speed=230400 +esp8285.menu.UploadSpeed.460800.linux=460800 +esp8285.menu.UploadSpeed.460800.macosx=460800 +esp8285.menu.UploadSpeed.460800.upload.speed=460800 +esp8285.menu.UploadSpeed.512000.windows=512000 +esp8285.menu.UploadSpeed.512000.upload.speed=512000 +esp8285.menu.UploadSpeed.921600=921600 +esp8285.menu.UploadSpeed.921600.upload.speed=921600 + +esp8285.menu.FlashSize.1M512=1M (512K SPIFFS) +esp8285.menu.FlashSize.1M512.build.flash_size=1M +esp8285.menu.FlashSize.1M512.build.flash_ld=eagle.flash.1m512.ld +esp8285.menu.FlashSize.1M512.build.spiffs_start=0x7B000 +esp8285.menu.FlashSize.1M512.build.spiffs_end=0xFB000 +esp8285.menu.FlashSize.1M512.build.spiffs_blocksize=8192 +esp8285.menu.FlashSize.1M512.upload.maximum_size=499696 + +esp8285.menu.FlashSize.1M256=1M (256K SPIFFS) +esp8285.menu.FlashSize.1M256.build.flash_size=1M +esp8285.menu.FlashSize.1M256.build.flash_ld=eagle.flash.1m256.ld +esp8285.menu.FlashSize.1M256.build.spiffs_start=0xBB000 +esp8285.menu.FlashSize.1M256.build.spiffs_end=0xFB000 +esp8285.menu.FlashSize.1M256.build.spiffs_blocksize=4096 +esp8285.menu.FlashSize.1M256.upload.maximum_size=761840 + +esp8285.menu.FlashSize.1M192=1M (192K SPIFFS) +esp8285.menu.FlashSize.1M192.build.flash_size=1M +esp8285.menu.FlashSize.1M192.build.flash_ld=eagle.flash.1m192.ld +esp8285.menu.FlashSize.1M192.build.spiffs_start=0xCB000 +esp8285.menu.FlashSize.1M192.build.spiffs_end=0xFB000 +esp8285.menu.FlashSize.1M192.build.spiffs_blocksize=4096 +esp8285.menu.FlashSize.1M192.upload.maximum_size=827376 + +esp8285.menu.FlashSize.1M160=1M (160K SPIFFS) +esp8285.menu.FlashSize.1M160.build.flash_size=1M +esp8285.menu.FlashSize.1M160.build.flash_ld=eagle.flash.1m160.ld +esp8285.menu.FlashSize.1M160.build.spiffs_start=0xD3000 +esp8285.menu.FlashSize.1M160.build.spiffs_end=0xFB000 +esp8285.menu.FlashSize.1M160.build.spiffs_blocksize=4096 +esp8285.menu.FlashSize.1M160.upload.maximum_size=860144 + +esp8285.menu.FlashSize.1M144=1M (144K SPIFFS) +esp8285.menu.FlashSize.1M144.build.flash_size=1M +esp8285.menu.FlashSize.1M144.build.flash_ld=eagle.flash.1m144.ld +esp8285.menu.FlashSize.1M144.build.spiffs_start=0xD7000 +esp8285.menu.FlashSize.1M144.build.spiffs_end=0xFB000 +esp8285.menu.FlashSize.1M144.build.spiffs_blocksize=4096 +esp8285.menu.FlashSize.1M144.upload.maximum_size=876528 + +esp8285.menu.FlashSize.1M128=1M (128K SPIFFS) +esp8285.menu.FlashSize.1M128.build.flash_size=1M +esp8285.menu.FlashSize.1M128.build.flash_ld=eagle.flash.1m128.ld +esp8285.menu.FlashSize.1M128.build.spiffs_start=0xDB000 +esp8285.menu.FlashSize.1M128.build.spiffs_end=0xFB000 +esp8285.menu.FlashSize.1M128.build.spiffs_blocksize=4096 +esp8285.menu.FlashSize.1M128.upload.maximum_size=892912 + +esp8285.menu.FlashSize.1M64=1M (64K SPIFFS) +esp8285.menu.FlashSize.1M64.build.flash_size=1M +esp8285.menu.FlashSize.1M64.build.flash_ld=eagle.flash.1m64.ld +esp8285.menu.FlashSize.1M64.build.spiffs_start=0xEB000 +esp8285.menu.FlashSize.1M64.build.spiffs_end=0xFB000 +esp8285.menu.FlashSize.1M64.build.spiffs_blocksize=4096 +esp8285.menu.FlashSize.1M64.upload.maximum_size=958448 + + +############################################################## + +espduino.name=ESPDuino (ESP-13 Module) + +espduino.upload.tool=esptool +espduino.upload.speed=115200 +espduino.upload.resetmethod=ck +espduino.upload.maximum_size=1044464 +espduino.upload.maximum_data_size=81920 +espduino.upload.wait_for_upload_port=true +espduino.serial.disableDTR=true +espduino.serial.disableRTS=true + +espduino.build.mcu=esp8266 +espduino.build.f_cpu=80000000L +espduino.build.board=ESP8266_ESP13 +espduino.build.core=esp8266 +espduino.build.variant=ESPDuino +espduino.build.flash_mode=dio +espduino.build.flash_size=4M +espduino.build.flash_freq=40 +espduino.build.debug_port= +espduino.build.debug_level= + +espduino.menu.CpuFrequency.80=80 MHz +espduino.menu.CpuFrequency.80.build.f_cpu=80000000L +espduino.menu.CpuFrequency.160=160 MHz +espduino.menu.CpuFrequency.160.build.f_cpu=160000000L + +espduino.menu.UploadSpeed.115200=115200 +espduino.menu.UploadSpeed.115200.upload.speed=115200 +espduino.menu.UploadSpeed.9600=9600 +espduino.menu.UploadSpeed.9600.upload.speed=9600 +espduino.menu.UploadSpeed.57600=57600 +espduino.menu.UploadSpeed.57600.upload.speed=57600 +espduino.menu.UploadSpeed.256000.windows=256000 +espduino.menu.UploadSpeed.256000.upload.speed=256000 +espduino.menu.UploadSpeed.230400.linux=230400 +espduino.menu.UploadSpeed.230400.macosx=230400 +espduino.menu.UploadSpeed.230400.macosx=230400 +espduino.menu.UploadSpeed.230400.upload.speed=230400 +espduino.menu.UploadSpeed.460800.linux=460800 +espduino.menu.UploadSpeed.460800.macosx=460800 +espduino.menu.UploadSpeed.460800.upload.speed=460800 +espduino.menu.UploadSpeed.512000.windows=512000 +espduino.menu.UploadSpeed.512000.upload.speed=512000 +espduino.menu.UploadSpeed.921600=921600 +espduino.menu.UploadSpeed.921600.upload.speed=921600 + +espduino.menu.FlashSize.4M3M=4M (3M SPIFFS) +espduino.menu.FlashSize.4M3M.build.flash_size=4M +espduino.menu.FlashSize.4M3M.build.flash_ld=eagle.flash.4m.ld +espduino.menu.FlashSize.4M3M.build.spiffs_start=0x100000 +espduino.menu.FlashSize.4M3M.build.spiffs_end=0x3FB000 +espduino.menu.FlashSize.4M3M.build.spiffs_blocksize=8192 +espduino.menu.FlashSize.4M3M.build.spiffs_pagesize=256 + +espduino.menu.FlashSize.4M1M=4M (1M SPIFFS) +espduino.menu.FlashSize.4M1M.build.flash_size=4M +espduino.menu.FlashSize.4M1M.build.flash_ld=eagle.flash.4m1m.ld +espduino.menu.FlashSize.4M1M.build.spiffs_start=0x300000 +espduino.menu.FlashSize.4M1M.build.spiffs_end=0x3FB000 +espduino.menu.FlashSize.4M1M.build.spiffs_blocksize=8192 +espduino.menu.FlashSize.4M1M.build.spiffs_pagesize=256 +############################################################## +huzzah.name=Adafruit HUZZAH ESP8266 + +huzzah.upload.tool=esptool +huzzah.upload.speed=115200 +huzzah.upload.resetmethod=nodemcu +huzzah.upload.maximum_size=1044464 +huzzah.upload.maximum_data_size=81920 +huzzah.upload.wait_for_upload_port=true +huzzah.serial.disableDTR=true +huzzah.serial.disableRTS=true + +huzzah.build.mcu=esp8266 +huzzah.build.f_cpu=80000000L +huzzah.build.board=ESP8266_ESP12 +huzzah.build.core=esp8266 +huzzah.build.variant=adafruit +huzzah.build.flash_mode=qio +huzzah.build.flash_size=4M +huzzah.build.flash_freq=40 +huzzah.build.debug_port= +huzzah.build.debug_level= + +huzzah.menu.CpuFrequency.80=80 MHz +huzzah.menu.CpuFrequency.80.build.f_cpu=80000000L +huzzah.menu.CpuFrequency.160=160 MHz +huzzah.menu.CpuFrequency.160.build.f_cpu=160000000L + +huzzah.menu.UploadSpeed.115200=115200 +huzzah.menu.UploadSpeed.115200.upload.speed=115200 +huzzah.menu.UploadSpeed.9600=9600 +huzzah.menu.UploadSpeed.9600.upload.speed=9600 +huzzah.menu.UploadSpeed.57600=57600 +huzzah.menu.UploadSpeed.57600.upload.speed=57600 +huzzah.menu.UploadSpeed.256000=256000 +huzzah.menu.UploadSpeed.256000.upload.speed=256000 +huzzah.menu.UploadSpeed.921600=921600 +huzzah.menu.UploadSpeed.921600.upload.speed=921600 + +huzzah.menu.FlashSize.4M3M=4M (3M SPIFFS) +huzzah.menu.FlashSize.4M3M.build.flash_size=4M +huzzah.menu.FlashSize.4M3M.build.flash_ld=eagle.flash.4m.ld +huzzah.menu.FlashSize.4M3M.build.spiffs_start=0x100000 +huzzah.menu.FlashSize.4M3M.build.spiffs_end=0x3FB000 +huzzah.menu.FlashSize.4M3M.build.spiffs_blocksize=8192 +huzzah.menu.FlashSize.4M3M.build.spiffs_pagesize=256 + +huzzah.menu.FlashSize.4M1M=4M (1M SPIFFS) +huzzah.menu.FlashSize.4M1M.build.flash_size=4M +huzzah.menu.FlashSize.4M1M.build.flash_ld=eagle.flash.4m1m.ld +huzzah.menu.FlashSize.4M1M.build.spiffs_start=0x300000 +huzzah.menu.FlashSize.4M1M.build.spiffs_end=0x3FB000 +huzzah.menu.FlashSize.4M1M.build.spiffs_blocksize=8192 +huzzah.menu.FlashSize.4M1M.build.spiffs_pagesize=256 + +############################################################## +espresso_lite_v1.name=ESPresso Lite 1.0 +espresso_lite_v1.upload.tool=esptool +espresso_lite_v1.upload.speed=115200 +espresso_lite_v1.upload.maximum_size=1044464 +espresso_lite_v1.upload.maximum_data_size=81920 +espresso_lite_v1.upload.wait_for_upload_port=true + +espresso_lite_v1.build.mcu=esp8266 +espresso_lite_v1.build.f_cpu=80000000L +espresso_lite_v1.build.board=ESP8266_ESPRESSO_LITE_V1 +espresso_lite_v1.build.core=esp8266 +espresso_lite_v1.build.variant=espresso_lite_v1 +espresso_lite_v1.build.flash_mode=dio +espresso_lite_v1.build.flash_size=4M +espresso_lite_v1.build.flash_freq=40 + +espresso_lite_v1.menu.CpuFrequency.80=80 MHz +espresso_lite_v1.menu.CpuFrequency.80.build.f_cpu=80000000L +espresso_lite_v1.menu.CpuFrequency.160=160 MHz +espresso_lite_v1.menu.CpuFrequency.160.build.f_cpu=160000000L + +espresso_lite_v1.menu.UploadSpeed.115200=115200 +espresso_lite_v1.menu.UploadSpeed.115200.upload.speed=115200 +espresso_lite_v1.menu.UploadSpeed.57600=57600 +espresso_lite_v1.menu.UploadSpeed.57600.upload.speed=57600 +espresso_lite_v1.menu.UploadSpeed.256000.windows=256000 +espresso_lite_v1.menu.UploadSpeed.256000.upload.speed=256000 +espresso_lite_v1.menu.UploadSpeed.230400.linux=230400 +espresso_lite_v1.menu.UploadSpeed.230400.macosx=230400 +espresso_lite_v1.menu.UploadSpeed.230400.macosx=230400 +espresso_lite_v1.menu.UploadSpeed.230400.upload.speed=230400 +espresso_lite_v1.menu.UploadSpeed.460800.linux=460800 +espresso_lite_v1.menu.UploadSpeed.460800.macosx=460800 +espresso_lite_v1.menu.UploadSpeed.460800.upload.speed=460800 +espresso_lite_v1.menu.UploadSpeed.512000.windows=512000 +espresso_lite_v1.menu.UploadSpeed.512000.upload.speed=512000 +espresso_lite_v1.menu.UploadSpeed.921600=921600 +espresso_lite_v1.menu.UploadSpeed.921600.upload.speed=921600 + +espresso_lite_v1.menu.FlashSize.4M3M=4M (3M SPIFFS) +espresso_lite_v1.menu.FlashSize.4M3M.build.flash_size=4M +espresso_lite_v1.menu.FlashSize.4M3M.build.flash_ld=eagle.flash.4m.ld +espresso_lite_v1.menu.FlashSize.4M3M.build.spiffs_start=0x100000 +espresso_lite_v1.menu.FlashSize.4M3M.build.spiffs_end=0x3FB000 +espresso_lite_v1.menu.FlashSize.4M3M.build.spiffs_blocksize=8192 +espresso_lite_v1.menu.FlashSize.4M3M.upload.maximum_size=1044464 + +espresso_lite_v1.menu.FlashSize.4M1M=4M (1M SPIFFS) +espresso_lite_v1.menu.FlashSize.4M1M.build.flash_size=4M +espresso_lite_v1.menu.FlashSize.4M1M.build.flash_ld=eagle.flash.4m1m.ld +espresso_lite_v1.menu.FlashSize.4M1M.build.spiffs_start=0x300000 +espresso_lite_v1.menu.FlashSize.4M1M.build.spiffs_end=0x3FB000 +espresso_lite_v1.menu.FlashSize.4M1M.build.spiffs_blocksize=8192 +espresso_lite_v1.menu.FlashSize.4M1M.build.spiffs_pagesize=256 +espresso_lite_v1.menu.FlashSize.4M1M.upload.maximum_size=1044464 + +espresso_lite_v1.menu.ResetMethod.nodemcu=nodemcu +espresso_lite_v1.menu.ResetMethod.nodemcu.upload.resetmethod=nodemcu +espresso_lite_v1.menu.ResetMethod.ck=ck +espresso_lite_v1.menu.ResetMethod.ck.upload.resetmethod=ck + +espresso_lite_v1.menu.Debug.Disabled=Disabled +espresso_lite_v1.menu.Debug.Disabled.build.debug_port= +espresso_lite_v1.menu.Debug.Serial=Serial +espresso_lite_v1.menu.Debug.Serial.build.debug_port=-DDEBUG_ESP_PORT=Serial +espresso_lite_v1.menu.Debug.Serial1=Serial1 +espresso_lite_v1.menu.Debug.Serial1.build.debug_port=-DDEBUG_ESP_PORT=Serial1 + +espresso_lite_v1.menu.DebugLevel.None____=None +espresso_lite_v1.menu.DebugLevel.None____.build.debug_level= +espresso_lite_v1.menu.DebugLevel.Core____=Core +espresso_lite_v1.menu.DebugLevel.Core____.build.debug_level=-DDEBUG_ESP_CORE +espresso_lite_v1.menu.DebugLevel.SSL_____=Core + SSL +espresso_lite_v1.menu.DebugLevel.SSL_____.build.debug_level=-DDEBUG_ESP_CORE -DDEBUG_ESP_SSL +espresso_lite_v1.menu.DebugLevel.SSL_MEM_=Core + SSL + TLS Mem +espresso_lite_v1.menu.DebugLevel.SSL_MEM_.build.debug_level=-DDEBUG_ESP_CORE -DDEBUG_ESP_SSL -DDEBUG_TLS_MEM +espresso_lite_v1.menu.DebugLevel.WiFic___=Core + WiFi +espresso_lite_v1.menu.DebugLevel.WiFic___.build.debug_level=-DDEBUG_ESP_CORE -DDEBUG_ESP_WIFI +espresso_lite_v1.menu.DebugLevel.WiFi____=WiFi +espresso_lite_v1.menu.DebugLevel.WiFi____.build.debug_level=-DDEBUG_ESP_WIFI +espresso_lite_v1.menu.DebugLevel.HTTPClient=HTTPClient +espresso_lite_v1.menu.DebugLevel.HTTPClient.build.debug_level=-DDEBUG_ESP_HTTP_CLIENT +espresso_lite_v1.menu.DebugLevel.HTTPClient2=HTTPClient + SSL +espresso_lite_v1.menu.DebugLevel.HTTPClient2.build.debug_level=-DDEBUG_ESP_HTTP_CLIENT -DDEBUG_ESP_SSL +espresso_lite_v1.menu.DebugLevel.HTTPUpdate=HTTPUpdate +espresso_lite_v1.menu.DebugLevel.HTTPUpdate.build.debug_level=-DDEBUG_ESP_HTTP_UPDATE +espresso_lite_v1.menu.DebugLevel.HTTPUpdate2=HTTPClient + HTTPUpdate +espresso_lite_v1.menu.DebugLevel.HTTPUpdate2.build.debug_level=-DDEBUG_ESP_HTTP_UPDATE -DDEBUG_ESP_HTTP_UPDATE +espresso_lite_v1.menu.DebugLevel.HTTPUpdate3=HTTPClient + HTTPUpdate + Updater +espresso_lite_v1.menu.DebugLevel.HTTPUpdate3.build.debug_level=-DDEBUG_ESP_HTTP_UPDATE -DDEBUG_ESP_HTTP_UPDATE -DDEBUG_ESP_UPDATER +espresso_lite_v1.menu.DebugLevel.HTTPServer=HTTPServer +espresso_lite_v1.menu.DebugLevel.HTTPServer.build.debug_level=-DDEBUG_ESP_HTTP_SERVER +espresso_lite_v1.menu.DebugLevel.UPDATER=Updater +espresso_lite_v1.menu.DebugLevel.UPDATER.build.debug_level=-DDEBUG_ESP_UPDATER +espresso_lite_v1.menu.DebugLevel.OTA_____=OTA +espresso_lite_v1.menu.DebugLevel.OTA_____.build.debug_level=-DDEBUG_ESP_OTA +espresso_lite_v1.menu.DebugLevel.OTA2____=OTA + Updater +espresso_lite_v1.menu.DebugLevel.OTA2____.build.debug_level=-DDEBUG_ESP_OTA -DDEBUG_ESP_UPDATER +espresso_lite_v1.menu.DebugLevel.all_____=All +espresso_lite_v1.menu.DebugLevel.all_____.build.debug_level=-DDEBUG_ESP_CORE -DDEBUG_ESP_SSL -DDEBUG_ESP_WIFI -DDEBUG_ESP_HTTP_CLIENT -DDEBUG_ESP_HTTP_UPDATE -DDEBUG_ESP_HTTP_SERVER -DDEBUG_ESP_UPDATER -DDEBUG_ESP_OTA -DDEBUG_TLS_MEM + +espresso_lite_v1.build.debug_port= +espresso_lite_v1.build.debug_level= + +############################################################## +espresso_lite_v2.name=ESPresso Lite 2.0 +espresso_lite_v2.upload.tool=esptool +espresso_lite_v2.upload.speed=115200 +espresso_lite_v2.upload.maximum_size=1044464 +espresso_lite_v2.upload.maximum_data_size=81920 +espresso_lite_v2.upload.wait_for_upload_port=true + +espresso_lite_v2.build.mcu=esp8266 +espresso_lite_v2.build.f_cpu=80000000L +espresso_lite_v2.build.board=ESP8266_ESPRESSO_LITE_V2 +espresso_lite_v2.build.core=esp8266 +espresso_lite_v2.build.variant=espresso_lite_v2 +espresso_lite_v2.build.flash_mode=dio +espresso_lite_v2.build.flash_size=4M +espresso_lite_v2.build.flash_freq=40 + +espresso_lite_v2.menu.CpuFrequency.80=80 MHz +espresso_lite_v2.menu.CpuFrequency.80.build.f_cpu=80000000L +espresso_lite_v2.menu.CpuFrequency.160=160 MHz +espresso_lite_v2.menu.CpuFrequency.160.build.f_cpu=160000000L + +espresso_lite_v2.menu.UploadSpeed.115200=115200 +espresso_lite_v2.menu.UploadSpeed.115200.upload.speed=115200 +espresso_lite_v2.menu.UploadSpeed.57600=57600 +espresso_lite_v2.menu.UploadSpeed.57600.upload.speed=57600 +espresso_lite_v2.menu.UploadSpeed.256000.windows=256000 +espresso_lite_v2.menu.UploadSpeed.256000.upload.speed=256000 +espresso_lite_v2.menu.UploadSpeed.230400.linux=230400 +espresso_lite_v2.menu.UploadSpeed.230400.macosx=230400 +espresso_lite_v2.menu.UploadSpeed.230400.macosx=230400 +espresso_lite_v2.menu.UploadSpeed.230400.upload.speed=230400 +espresso_lite_v2.menu.UploadSpeed.460800.linux=460800 +espresso_lite_v2.menu.UploadSpeed.460800.macosx=460800 +espresso_lite_v2.menu.UploadSpeed.460800.upload.speed=460800 +espresso_lite_v2.menu.UploadSpeed.512000.windows=512000 +espresso_lite_v2.menu.UploadSpeed.512000.upload.speed=512000 +espresso_lite_v2.menu.UploadSpeed.921600=921600 +espresso_lite_v2.menu.UploadSpeed.921600.upload.speed=921600 + +espresso_lite_v2.menu.FlashSize.4M3M=4M (3M SPIFFS) +espresso_lite_v2.menu.FlashSize.4M3M.build.flash_size=4M +espresso_lite_v2.menu.FlashSize.4M3M.build.flash_ld=eagle.flash.4m.ld +espresso_lite_v2.menu.FlashSize.4M3M.build.spiffs_start=0x100000 +espresso_lite_v2.menu.FlashSize.4M3M.build.spiffs_end=0x3FB000 +espresso_lite_v2.menu.FlashSize.4M3M.build.spiffs_blocksize=8192 +espresso_lite_v2.menu.FlashSize.4M3M.upload.maximum_size=1044464 + +espresso_lite_v2.menu.FlashSize.4M1M=4M (1M SPIFFS) +espresso_lite_v2.menu.FlashSize.4M1M.build.flash_size=4M +espresso_lite_v2.menu.FlashSize.4M1M.build.flash_ld=eagle.flash.4m1m.ld +espresso_lite_v2.menu.FlashSize.4M1M.build.spiffs_start=0x300000 +espresso_lite_v2.menu.FlashSize.4M1M.build.spiffs_end=0x3FB000 +espresso_lite_v2.menu.FlashSize.4M1M.build.spiffs_blocksize=8192 +espresso_lite_v2.menu.FlashSize.4M1M.build.spiffs_pagesize=256 +espresso_lite_v2.menu.FlashSize.4M1M.upload.maximum_size=1044464 + +espresso_lite_v2.menu.ResetMethod.ck=ck +espresso_lite_v2.menu.ResetMethod.ck.upload.resetmethod=ck +espresso_lite_v2.menu.ResetMethod.nodemcu=nodemcu +espresso_lite_v2.menu.ResetMethod.nodemcu.upload.resetmethod=nodemcu + +espresso_lite_v2.menu.Debug.Disabled=Disabled +espresso_lite_v2.menu.Debug.Disabled.build.debug_port= +espresso_lite_v2.menu.Debug.Serial=Serial +espresso_lite_v2.menu.Debug.Serial.build.debug_port=-DDEBUG_ESP_PORT=Serial +espresso_lite_v2.menu.Debug.Serial1=Serial1 +espresso_lite_v2.menu.Debug.Serial1.build.debug_port=-DDEBUG_ESP_PORT=Serial1 + +espresso_lite_v2.menu.DebugLevel.None____=None +espresso_lite_v2.menu.DebugLevel.None____.build.debug_level= +espresso_lite_v2.menu.DebugLevel.Core____=Core +espresso_lite_v2.menu.DebugLevel.Core____.build.debug_level=-DDEBUG_ESP_CORE +espresso_lite_v2.menu.DebugLevel.SSL_____=Core + SSL +espresso_lite_v2.menu.DebugLevel.SSL_____.build.debug_level=-DDEBUG_ESP_CORE -DDEBUG_ESP_SSL +espresso_lite_v2.menu.DebugLevel.SSL_MEM_=Core + SSL + TLS Mem +espresso_lite_v2.menu.DebugLevel.SSL_MEM_.build.debug_level=-DDEBUG_ESP_CORE -DDEBUG_ESP_SSL -DDEBUG_TLS_MEM +espresso_lite_v2.menu.DebugLevel.WiFic___=Core + WiFi +espresso_lite_v2.menu.DebugLevel.WiFic___.build.debug_level=-DDEBUG_ESP_CORE -DDEBUG_ESP_WIFI +espresso_lite_v2.menu.DebugLevel.WiFi____=WiFi +espresso_lite_v2.menu.DebugLevel.WiFi____.build.debug_level=-DDEBUG_ESP_WIFI +espresso_lite_v2.menu.DebugLevel.HTTPClient=HTTPClient +espresso_lite_v2.menu.DebugLevel.HTTPClient.build.debug_level=-DDEBUG_ESP_HTTP_CLIENT +espresso_lite_v2.menu.DebugLevel.HTTPClient2=HTTPClient + SSL +espresso_lite_v2.menu.DebugLevel.HTTPClient2.build.debug_level=-DDEBUG_ESP_HTTP_CLIENT -DDEBUG_ESP_SSL +espresso_lite_v2.menu.DebugLevel.HTTPUpdate=HTTPUpdate +espresso_lite_v2.menu.DebugLevel.HTTPUpdate.build.debug_level=-DDEBUG_ESP_HTTP_UPDATE +espresso_lite_v2.menu.DebugLevel.HTTPUpdate2=HTTPClient + HTTPUpdate +espresso_lite_v2.menu.DebugLevel.HTTPUpdate2.build.debug_level=-DDEBUG_ESP_HTTP_UPDATE -DDEBUG_ESP_HTTP_UPDATE +espresso_lite_v2.menu.DebugLevel.HTTPUpdate3=HTTPClient + HTTPUpdate + Updater +espresso_lite_v2.menu.DebugLevel.HTTPUpdate3.build.debug_level=-DDEBUG_ESP_HTTP_UPDATE -DDEBUG_ESP_HTTP_UPDATE -DDEBUG_ESP_UPDATER +espresso_lite_v2.menu.DebugLevel.HTTPServer=HTTPServer +espresso_lite_v2.menu.DebugLevel.HTTPServer.build.debug_level=-DDEBUG_ESP_HTTP_SERVER +espresso_lite_v2.menu.DebugLevel.UPDATER=Updater +espresso_lite_v2.menu.DebugLevel.UPDATER.build.debug_level=-DDEBUG_ESP_UPDATER +espresso_lite_v2.menu.DebugLevel.OTA_____=OTA +espresso_lite_v2.menu.DebugLevel.OTA_____.build.debug_level=-DDEBUG_ESP_OTA +espresso_lite_v2.menu.DebugLevel.OTA2____=OTA + Updater +espresso_lite_v2.menu.DebugLevel.OTA2____.build.debug_level=-DDEBUG_ESP_OTA -DDEBUG_ESP_UPDATER +espresso_lite_v2.menu.DebugLevel.all_____=All +espresso_lite_v2.menu.DebugLevel.all_____.build.debug_level=-DDEBUG_ESP_CORE -DDEBUG_ESP_SSL -DDEBUG_ESP_WIFI -DDEBUG_ESP_HTTP_CLIENT -DDEBUG_ESP_HTTP_UPDATE -DDEBUG_ESP_HTTP_SERVER -DDEBUG_ESP_UPDATER -DDEBUG_ESP_OTA -DDEBUG_TLS_MEM + +espresso_lite_v2.build.debug_port= +espresso_lite_v2.build.debug_level= + +############################################################## +phoenix_v1.name=Phoenix 1.0 +phoenix_v1.upload.tool=esptool +phoenix_v1.upload.speed=115200 +phoenix_v1.upload.maximum_size=1044464 +phoenix_v1.upload.maximum_data_size=81920 +phoenix_v1.upload.wait_for_upload_port=true + +phoenix_v1.build.mcu=esp8266 +phoenix_v1.build.f_cpu=80000000L +phoenix_v1.build.board=ESP8266_PHOENIX_V1 +phoenix_v1.build.core=esp8266 +phoenix_v1.build.variant=phoenix_v1 +phoenix_v1.build.flash_mode=dio +phoenix_v1.build.flash_size=4M +phoenix_v1.build.flash_freq=40 + +phoenix_v1.menu.CpuFrequency.80=80 MHz +phoenix_v1.menu.CpuFrequency.80.build.f_cpu=80000000L +phoenix_v1.menu.CpuFrequency.160=160 MHz +phoenix_v1.menu.CpuFrequency.160.build.f_cpu=160000000L + +phoenix_v1.menu.UploadSpeed.115200=115200 +phoenix_v1.menu.UploadSpeed.115200.upload.speed=115200 +phoenix_v1.menu.UploadSpeed.57600=57600 +phoenix_v1.menu.UploadSpeed.57600.upload.speed=57600 +phoenix_v1.menu.UploadSpeed.256000.windows=256000 +phoenix_v1.menu.UploadSpeed.256000.upload.speed=256000 +phoenix_v1.menu.UploadSpeed.230400.linux=230400 +phoenix_v1.menu.UploadSpeed.230400.macosx=230400 +phoenix_v1.menu.UploadSpeed.230400.macosx=230400 +phoenix_v1.menu.UploadSpeed.230400.upload.speed=230400 +phoenix_v1.menu.UploadSpeed.460800.linux=460800 +phoenix_v1.menu.UploadSpeed.460800.macosx=460800 +phoenix_v1.menu.UploadSpeed.460800.upload.speed=460800 +phoenix_v1.menu.UploadSpeed.512000.windows=512000 +phoenix_v1.menu.UploadSpeed.512000.upload.speed=512000 +phoenix_v1.menu.UploadSpeed.921600=921600 +phoenix_v1.menu.UploadSpeed.921600.upload.speed=921600 + +phoenix_v1.menu.FlashSize.4M3M=4M (3M SPIFFS) +phoenix_v1.menu.FlashSize.4M3M.build.flash_size=4M +phoenix_v1.menu.FlashSize.4M3M.build.flash_ld=eagle.flash.4m.ld +phoenix_v1.menu.FlashSize.4M3M.build.spiffs_start=0x100000 +phoenix_v1.menu.FlashSize.4M3M.build.spiffs_end=0x3FB000 +phoenix_v1.menu.FlashSize.4M3M.build.spiffs_blocksize=8192 +phoenix_v1.menu.FlashSize.4M3M.upload.maximum_size=1044464 + +phoenix_v1.menu.FlashSize.4M1M=4M (1M SPIFFS) +phoenix_v1.menu.FlashSize.4M1M.build.flash_size=4M +phoenix_v1.menu.FlashSize.4M1M.build.flash_ld=eagle.flash.4m1m.ld +phoenix_v1.menu.FlashSize.4M1M.build.spiffs_start=0x300000 +phoenix_v1.menu.FlashSize.4M1M.build.spiffs_end=0x3FB000 +phoenix_v1.menu.FlashSize.4M1M.build.spiffs_blocksize=8192 +phoenix_v1.menu.FlashSize.4M1M.build.spiffs_pagesize=256 +phoenix_v1.menu.FlashSize.4M1M.upload.maximum_size=1044464 + +phoenix_v1.menu.ResetMethod.nodemcu=nodemcu +phoenix_v1.menu.ResetMethod.nodemcu.upload.resetmethod=nodemcu +phoenix_v1.menu.ResetMethod.ck=ck +phoenix_v1.menu.ResetMethod.ck.upload.resetmethod=ck + +phoenix_v1.menu.Debug.Disabled=Disabled +phoenix_v1.menu.Debug.Disabled.build.debug_port= +phoenix_v1.menu.Debug.Serial=Serial +phoenix_v1.menu.Debug.Serial.build.debug_port=-DDEBUG_ESP_PORT=Serial +phoenix_v1.menu.Debug.Serial1=Serial1 +phoenix_v1.menu.Debug.Serial1.build.debug_port=-DDEBUG_ESP_PORT=Serial1 + +phoenix_v1.menu.DebugLevel.None____=None +phoenix_v1.menu.DebugLevel.None____.build.debug_level= +phoenix_v1.menu.DebugLevel.Core____=Core +phoenix_v1.menu.DebugLevel.Core____.build.debug_level=-DDEBUG_ESP_CORE +phoenix_v1.menu.DebugLevel.SSL_____=Core + SSL +phoenix_v1.menu.DebugLevel.SSL_____.build.debug_level=-DDEBUG_ESP_CORE -DDEBUG_ESP_SSL +phoenix_v1.menu.DebugLevel.SSL_MEM_=Core + SSL + TLS Mem +phoenix_v1.menu.DebugLevel.SSL_MEM_.build.debug_level=-DDEBUG_ESP_CORE -DDEBUG_ESP_SSL -DDEBUG_TLS_MEM +phoenix_v1.menu.DebugLevel.WiFic___=Core + WiFi +phoenix_v1.menu.DebugLevel.WiFic___.build.debug_level=-DDEBUG_ESP_CORE -DDEBUG_ESP_WIFI +phoenix_v1.menu.DebugLevel.WiFi____=WiFi +phoenix_v1.menu.DebugLevel.WiFi____.build.debug_level=-DDEBUG_ESP_WIFI +phoenix_v1.menu.DebugLevel.HTTPClient=HTTPClient +phoenix_v1.menu.DebugLevel.HTTPClient.build.debug_level=-DDEBUG_ESP_HTTP_CLIENT +phoenix_v1.menu.DebugLevel.HTTPClient2=HTTPClient + SSL +phoenix_v1.menu.DebugLevel.HTTPClient2.build.debug_level=-DDEBUG_ESP_HTTP_CLIENT -DDEBUG_ESP_SSL +phoenix_v1.menu.DebugLevel.HTTPUpdate=HTTPUpdate +phoenix_v1.menu.DebugLevel.HTTPUpdate.build.debug_level=-DDEBUG_ESP_HTTP_UPDATE +phoenix_v1.menu.DebugLevel.HTTPUpdate2=HTTPClient + HTTPUpdate +phoenix_v1.menu.DebugLevel.HTTPUpdate2.build.debug_level=-DDEBUG_ESP_HTTP_UPDATE -DDEBUG_ESP_HTTP_UPDATE +phoenix_v1.menu.DebugLevel.HTTPUpdate3=HTTPClient + HTTPUpdate + Updater +phoenix_v1.menu.DebugLevel.HTTPUpdate3.build.debug_level=-DDEBUG_ESP_HTTP_UPDATE -DDEBUG_ESP_HTTP_UPDATE -DDEBUG_ESP_UPDATER +phoenix_v1.menu.DebugLevel.HTTPServer=HTTPServer +phoenix_v1.menu.DebugLevel.HTTPServer.build.debug_level=-DDEBUG_ESP_HTTP_SERVER +phoenix_v1.menu.DebugLevel.UPDATER=Updater +phoenix_v1.menu.DebugLevel.UPDATER.build.debug_level=-DDEBUG_ESP_UPDATER +phoenix_v1.menu.DebugLevel.OTA_____=OTA +phoenix_v1.menu.DebugLevel.OTA_____.build.debug_level=-DDEBUG_ESP_OTA +phoenix_v1.menu.DebugLevel.OTA2____=OTA + Updater +phoenix_v1.menu.DebugLevel.OTA2____.build.debug_level=-DDEBUG_ESP_OTA -DDEBUG_ESP_UPDATER +phoenix_v1.menu.DebugLevel.all_____=All +phoenix_v1.menu.DebugLevel.all_____.build.debug_level=-DDEBUG_ESP_CORE -DDEBUG_ESP_SSL -DDEBUG_ESP_WIFI -DDEBUG_ESP_HTTP_CLIENT -DDEBUG_ESP_HTTP_UPDATE -DDEBUG_ESP_HTTP_SERVER -DDEBUG_ESP_UPDATER -DDEBUG_ESP_OTA -DDEBUG_TLS_MEM + +phoenix_v1.build.debug_port= +phoenix_v1.build.debug_level= + +############################################################## +phoenix_v2.name=Phoenix 2.0 +phoenix_v2.upload.tool=esptool +phoenix_v2.upload.speed=115200 +phoenix_v2.upload.maximum_size=1044464 +phoenix_v2.upload.maximum_data_size=81920 +phoenix_v2.upload.wait_for_upload_port=true + +phoenix_v2.build.mcu=esp8266 +phoenix_v2.build.f_cpu=80000000L +phoenix_v2.build.board=ESP8266_PHOENIX_V2 +phoenix_v2.build.core=esp8266 +phoenix_v2.build.variant=phoenix_v2 +phoenix_v2.build.flash_mode=dio +phoenix_v2.build.flash_size=4M +phoenix_v2.build.flash_freq=40 + +phoenix_v2.menu.CpuFrequency.80=80 MHz +phoenix_v2.menu.CpuFrequency.80.build.f_cpu=80000000L +phoenix_v2.menu.CpuFrequency.160=160 MHz +phoenix_v2.menu.CpuFrequency.160.build.f_cpu=160000000L + +phoenix_v2.menu.UploadSpeed.115200=115200 +phoenix_v2.menu.UploadSpeed.115200.upload.speed=115200 +phoenix_v2.menu.UploadSpeed.57600=57600 +phoenix_v2.menu.UploadSpeed.57600.upload.speed=57600 +phoenix_v2.menu.UploadSpeed.256000.windows=256000 +phoenix_v2.menu.UploadSpeed.256000.upload.speed=256000 +phoenix_v2.menu.UploadSpeed.230400.linux=230400 +phoenix_v2.menu.UploadSpeed.230400.macosx=230400 +phoenix_v2.menu.UploadSpeed.230400.macosx=230400 +phoenix_v2.menu.UploadSpeed.230400.upload.speed=230400 +phoenix_v2.menu.UploadSpeed.460800.linux=460800 +phoenix_v2.menu.UploadSpeed.460800.macosx=460800 +phoenix_v2.menu.UploadSpeed.460800.upload.speed=460800 +phoenix_v2.menu.UploadSpeed.512000.windows=512000 +phoenix_v2.menu.UploadSpeed.512000.upload.speed=512000 +phoenix_v2.menu.UploadSpeed.921600=921600 +phoenix_v2.menu.UploadSpeed.921600.upload.speed=921600 + +phoenix_v2.menu.FlashSize.4M3M=4M (3M SPIFFS) +phoenix_v2.menu.FlashSize.4M3M.build.flash_size=4M +phoenix_v2.menu.FlashSize.4M3M.build.flash_ld=eagle.flash.4m.ld +phoenix_v2.menu.FlashSize.4M3M.build.spiffs_start=0x100000 +phoenix_v2.menu.FlashSize.4M3M.build.spiffs_end=0x3FB000 +phoenix_v2.menu.FlashSize.4M3M.build.spiffs_blocksize=8192 +phoenix_v2.menu.FlashSize.4M3M.upload.maximum_size=1044464 + +phoenix_v2.menu.FlashSize.4M1M=4M (1M SPIFFS) +phoenix_v2.menu.FlashSize.4M1M.build.flash_size=4M +phoenix_v2.menu.FlashSize.4M1M.build.flash_ld=eagle.flash.4m1m.ld +phoenix_v2.menu.FlashSize.4M1M.build.spiffs_start=0x300000 +phoenix_v2.menu.FlashSize.4M1M.build.spiffs_end=0x3FB000 +phoenix_v2.menu.FlashSize.4M1M.build.spiffs_blocksize=8192 +phoenix_v2.menu.FlashSize.4M1M.build.spiffs_pagesize=256 +phoenix_v2.menu.FlashSize.4M1M.upload.maximum_size=1044464 + +phoenix_v2.menu.ResetMethod.ck=ck +phoenix_v2.menu.ResetMethod.ck.upload.resetmethod=ck +phoenix_v2.menu.ResetMethod.nodemcu=nodemcu +phoenix_v2.menu.ResetMethod.nodemcu.upload.resetmethod=nodemcu + +phoenix_v2.menu.Debug.Disabled=Disabled +phoenix_v2.menu.Debug.Disabled.build.debug_port= +phoenix_v2.menu.Debug.Serial=Serial +phoenix_v2.menu.Debug.Serial.build.debug_port=-DDEBUG_ESP_PORT=Serial +phoenix_v2.menu.Debug.Serial1=Serial1 +phoenix_v2.menu.Debug.Serial1.build.debug_port=-DDEBUG_ESP_PORT=Serial1 + +phoenix_v2.menu.DebugLevel.None____=None +phoenix_v2.menu.DebugLevel.None____.build.debug_level= +phoenix_v2.menu.DebugLevel.Core____=Core +phoenix_v2.menu.DebugLevel.Core____.build.debug_level=-DDEBUG_ESP_CORE +phoenix_v2.menu.DebugLevel.SSL_____=Core + SSL +phoenix_v2.menu.DebugLevel.SSL_____.build.debug_level=-DDEBUG_ESP_CORE -DDEBUG_ESP_SSL +phoenix_v2.menu.DebugLevel.SSL_MEM_=Core + SSL + TLS Mem +phoenix_v2.menu.DebugLevel.SSL_MEM_.build.debug_level=-DDEBUG_ESP_CORE -DDEBUG_ESP_SSL -DDEBUG_TLS_MEM +phoenix_v2.menu.DebugLevel.WiFic___=Core + WiFi +phoenix_v2.menu.DebugLevel.WiFic___.build.debug_level=-DDEBUG_ESP_CORE -DDEBUG_ESP_WIFI +phoenix_v2.menu.DebugLevel.WiFi____=WiFi +phoenix_v2.menu.DebugLevel.WiFi____.build.debug_level=-DDEBUG_ESP_WIFI +phoenix_v2.menu.DebugLevel.HTTPClient=HTTPClient +phoenix_v2.menu.DebugLevel.HTTPClient.build.debug_level=-DDEBUG_ESP_HTTP_CLIENT +phoenix_v2.menu.DebugLevel.HTTPClient2=HTTPClient + SSL +phoenix_v2.menu.DebugLevel.HTTPClient2.build.debug_level=-DDEBUG_ESP_HTTP_CLIENT -DDEBUG_ESP_SSL +phoenix_v2.menu.DebugLevel.HTTPUpdate=HTTPUpdate +phoenix_v2.menu.DebugLevel.HTTPUpdate.build.debug_level=-DDEBUG_ESP_HTTP_UPDATE +phoenix_v2.menu.DebugLevel.HTTPUpdate2=HTTPClient + HTTPUpdate +phoenix_v2.menu.DebugLevel.HTTPUpdate2.build.debug_level=-DDEBUG_ESP_HTTP_UPDATE -DDEBUG_ESP_HTTP_UPDATE +phoenix_v2.menu.DebugLevel.HTTPUpdate3=HTTPClient + HTTPUpdate + Updater +phoenix_v2.menu.DebugLevel.HTTPUpdate3.build.debug_level=-DDEBUG_ESP_HTTP_UPDATE -DDEBUG_ESP_HTTP_UPDATE -DDEBUG_ESP_UPDATER +phoenix_v2.menu.DebugLevel.HTTPServer=HTTPServer +phoenix_v2.menu.DebugLevel.HTTPServer.build.debug_level=-DDEBUG_ESP_HTTP_SERVER +phoenix_v2.menu.DebugLevel.UPDATER=Updater +phoenix_v2.menu.DebugLevel.UPDATER.build.debug_level=-DDEBUG_ESP_UPDATER +phoenix_v2.menu.DebugLevel.OTA_____=OTA +phoenix_v2.menu.DebugLevel.OTA_____.build.debug_level=-DDEBUG_ESP_OTA +phoenix_v2.menu.DebugLevel.OTA2____=OTA + Updater +phoenix_v2.menu.DebugLevel.OTA2____.build.debug_level=-DDEBUG_ESP_OTA -DDEBUG_ESP_UPDATER +phoenix_v2.menu.DebugLevel.all_____=All +phoenix_v2.menu.DebugLevel.all_____.build.debug_level=-DDEBUG_ESP_CORE -DDEBUG_ESP_SSL -DDEBUG_ESP_WIFI -DDEBUG_ESP_HTTP_CLIENT -DDEBUG_ESP_HTTP_UPDATE -DDEBUG_ESP_HTTP_SERVER -DDEBUG_ESP_UPDATER -DDEBUG_ESP_OTA -DDEBUG_TLS_MEM + +phoenix_v2.build.debug_port= +phoenix_v2.build.debug_level= + +############################################################## +nodemcu.name=NodeMCU 0.9 (ESP-12 Module) + +nodemcu.upload.tool=esptool +nodemcu.upload.speed=115200 +nodemcu.upload.resetmethod=nodemcu +nodemcu.upload.maximum_size=1044464 +nodemcu.upload.maximum_data_size=81920 +nodemcu.upload.wait_for_upload_port=true +nodemcu.serial.disableDTR=true +nodemcu.serial.disableRTS=true + +nodemcu.build.mcu=esp8266 +nodemcu.build.f_cpu=80000000L +nodemcu.build.board=ESP8266_NODEMCU +nodemcu.build.core=esp8266 +nodemcu.build.variant=nodemcu +nodemcu.build.flash_mode=qio +nodemcu.build.flash_size=4M +nodemcu.build.flash_freq=40 +nodemcu.build.debug_port= +nodemcu.build.debug_level= + +nodemcu.menu.CpuFrequency.80=80 MHz +nodemcu.menu.CpuFrequency.80.build.f_cpu=80000000L +nodemcu.menu.CpuFrequency.160=160 MHz +nodemcu.menu.CpuFrequency.160.build.f_cpu=160000000L + +nodemcu.menu.UploadTool.esptool=Serial +nodemcu.menu.UploadTool.esptool.upload.tool=esptool +nodemcu.menu.UploadTool.esptool.upload.verbose=-vv +nodemcu.menu.UploadTool.espupload=OTA_upload +nodemcu.menu.UploadTool.espupload.upload.tool=espupload + +nodemcu.menu.UploadSpeed.115200=115200 +nodemcu.menu.UploadSpeed.115200.upload.speed=115200 +nodemcu.menu.UploadSpeed.9600=9600 +nodemcu.menu.UploadSpeed.9600.upload.speed=9600 +nodemcu.menu.UploadSpeed.57600=57600 +nodemcu.menu.UploadSpeed.57600.upload.speed=57600 +nodemcu.menu.UploadSpeed.256000.windows=256000 +nodemcu.menu.UploadSpeed.256000.upload.speed=256000 +nodemcu.menu.UploadSpeed.230400.linux=230400 +nodemcu.menu.UploadSpeed.230400.macosx=230400 +nodemcu.menu.UploadSpeed.230400.macosx=230400 +nodemcu.menu.UploadSpeed.230400.upload.speed=230400 +nodemcu.menu.UploadSpeed.460800.linux=460800 +nodemcu.menu.UploadSpeed.460800.macosx=460800 +nodemcu.menu.UploadSpeed.460800.upload.speed=460800 +nodemcu.menu.UploadSpeed.512000.windows=512000 +nodemcu.menu.UploadSpeed.512000.upload.speed=512000 +nodemcu.menu.UploadSpeed.921600=921600 +nodemcu.menu.UploadSpeed.921600.upload.speed=921600 + +nodemcu.menu.FlashSize.4M3M=4M (3M SPIFFS) +nodemcu.menu.FlashSize.4M3M.build.flash_size=4M +nodemcu.menu.FlashSize.4M3M.build.flash_ld=eagle.flash.4m.ld +nodemcu.menu.FlashSize.4M3M.build.spiffs_start=0x100000 +nodemcu.menu.FlashSize.4M3M.build.spiffs_end=0x3FB000 +nodemcu.menu.FlashSize.4M3M.build.spiffs_blocksize=8192 +nodemcu.menu.FlashSize.4M3M.build.spiffs_pagesize=256 + +nodemcu.menu.FlashSize.4M1M=4M (1M SPIFFS) +nodemcu.menu.FlashSize.4M1M.build.flash_size=4M +nodemcu.menu.FlashSize.4M1M.build.flash_ld=eagle.flash.4m1m.ld +nodemcu.menu.FlashSize.4M1M.build.spiffs_start=0x300000 +nodemcu.menu.FlashSize.4M1M.build.spiffs_end=0x3FB000 +nodemcu.menu.FlashSize.4M1M.build.spiffs_blocksize=8192 +nodemcu.menu.FlashSize.4M1M.build.spiffs_pagesize=256 + +############################################################## +nodemcuv2.name=NodeMCU 1.0 (ESP-12E Module) + +nodemcuv2.upload.tool=esptool +nodemcuv2.upload.speed=115200 +nodemcuv2.upload.resetmethod=nodemcu +nodemcuv2.upload.maximum_size=1044464 +nodemcuv2.upload.maximum_data_size=81920 +nodemcuv2.upload.wait_for_upload_port=true +nodemcuv2.serial.disableDTR=true +nodemcuv2.serial.disableRTS=true + +nodemcuv2.build.mcu=esp8266 +nodemcuv2.build.f_cpu=80000000L +nodemcuv2.build.board=ESP8266_NODEMCU +nodemcuv2.build.core=esp8266 +nodemcuv2.build.variant=nodemcu +nodemcuv2.build.flash_mode=dio +nodemcuv2.build.flash_size=4M +nodemcuv2.build.flash_freq=40 +nodemcuv2.build.debug_port= +nodemcuv2.build.debug_level= + +nodemcuv2.menu.CpuFrequency.80=80 MHz +nodemcuv2.menu.CpuFrequency.80.build.f_cpu=80000000L +nodemcuv2.menu.CpuFrequency.160=160 MHz +nodemcuv2.menu.CpuFrequency.160.build.f_cpu=160000000L + +nodemcuv2.menu.UploadSpeed.115200=115200 +nodemcuv2.menu.UploadSpeed.115200.upload.speed=115200 +nodemcuv2.menu.UploadSpeed.9600=9600 +nodemcuv2.menu.UploadSpeed.9600.upload.speed=9600 +nodemcuv2.menu.UploadSpeed.57600=57600 +nodemcuv2.menu.UploadSpeed.57600.upload.speed=57600 +nodemcuv2.menu.UploadSpeed.256000.windows=256000 +nodemcuv2.menu.UploadSpeed.256000.upload.speed=256000 +nodemcuv2.menu.UploadSpeed.230400.linux=230400 +nodemcuv2.menu.UploadSpeed.230400.macosx=230400 +nodemcuv2.menu.UploadSpeed.230400.macosx=230400 +nodemcuv2.menu.UploadSpeed.230400.upload.speed=230400 +nodemcuv2.menu.UploadSpeed.460800.linux=460800 +nodemcuv2.menu.UploadSpeed.460800.macosx=460800 +nodemcuv2.menu.UploadSpeed.460800.upload.speed=460800 +nodemcuv2.menu.UploadSpeed.512000.windows=512000 +nodemcuv2.menu.UploadSpeed.512000.upload.speed=512000 +nodemcuv2.menu.UploadSpeed.921600=921600 +nodemcuv2.menu.UploadSpeed.921600.upload.speed=921600 + +nodemcuv2.menu.FlashSize.4M3M=4M (3M SPIFFS) +nodemcuv2.menu.FlashSize.4M3M.build.flash_size=4M +nodemcuv2.menu.FlashSize.4M3M.build.flash_ld=eagle.flash.4m.ld +nodemcuv2.menu.FlashSize.4M3M.build.spiffs_start=0x100000 +nodemcuv2.menu.FlashSize.4M3M.build.spiffs_end=0x3FB000 +nodemcuv2.menu.FlashSize.4M3M.build.spiffs_blocksize=8192 +nodemcuv2.menu.FlashSize.4M3M.build.spiffs_pagesize=256 + +nodemcuv2.menu.FlashSize.4M1M=4M (1M SPIFFS) +nodemcuv2.menu.FlashSize.4M1M.build.flash_size=4M +nodemcuv2.menu.FlashSize.4M1M.build.flash_ld=eagle.flash.4m1m.ld +nodemcuv2.menu.FlashSize.4M1M.build.spiffs_start=0x300000 +nodemcuv2.menu.FlashSize.4M1M.build.spiffs_end=0x3FB000 +nodemcuv2.menu.FlashSize.4M1M.build.spiffs_blocksize=8192 +nodemcuv2.menu.FlashSize.4M1M.build.spiffs_pagesize=256 + + +############################################################## +modwifi.name=Olimex MOD-WIFI-ESP8266(-DEV) + +modwifi.upload.tool=esptool +modwifi.upload.speed=115200 +modwifi.upload.resetmethod=ck +modwifi.upload.maximum_size=1044464 +modwifi.upload.maximum_data_size=81920 +modwifi.upload.wait_for_upload_port=true +modwifi.serial.disableDTR=true +modwifi.serial.disableRTS=true + +modwifi.build.mcu=esp8266 +modwifi.build.f_cpu=80000000L +modwifi.build.board=MOD_WIFI_ESP8266 +modwifi.build.core=esp8266 +modwifi.build.variant=generic +# Winbond W25Q16 flash +modwifi.build.flash_mode=qio +modwifi.build.flash_size=2M +modwifi.build.flash_freq=40 +modwifi.build.flash_ld=eagle.flash.2m.ld +modwifi.build.spiffs_start=0x100000 +modwifi.build.spiffs_end=0x1FB000 +modwifi.build.spiffs_pagesize=256 +modwifi.build.spiffs_blocksize=8192 +modwifi.build.debug_port= +modwifi.build.debug_level= + +modwifi.menu.CpuFrequency.80=80 MHz +modwifi.menu.CpuFrequency.80.build.f_cpu=80000000L +modwifi.menu.CpuFrequency.160=160 MHz +modwifi.menu.CpuFrequency.160.build.f_cpu=160000000L + +modwifi.menu.UploadSpeed.115200=115200 +modwifi.menu.UploadSpeed.115200.upload.speed=115200 +modwifi.menu.UploadSpeed.9600=9600 +modwifi.menu.UploadSpeed.9600.upload.speed=9600 +modwifi.menu.UploadSpeed.57600=57600 +modwifi.menu.UploadSpeed.57600.upload.speed=57600 +modwifi.menu.UploadSpeed.256000.windows=256000 +modwifi.menu.UploadSpeed.256000.upload.speed=256000 +modwifi.menu.UploadSpeed.230400.linux=230400 +modwifi.menu.UploadSpeed.230400.macosx=230400 +modwifi.menu.UploadSpeed.230400.macosx=230400 +modwifi.menu.UploadSpeed.230400.upload.speed=230400 +modwifi.menu.UploadSpeed.460800.linux=460800 +modwifi.menu.UploadSpeed.460800.macosx=460800 +modwifi.menu.UploadSpeed.460800.upload.speed=460800 +modwifi.menu.UploadSpeed.512000.windows=512000 +modwifi.menu.UploadSpeed.512000.upload.speed=512000 +modwifi.menu.UploadSpeed.921600=921600 +modwifi.menu.UploadSpeed.921600.upload.speed=921600 + +############################################################## +thing.name=SparkFun ESP8266 Thing + +thing.upload.tool=esptool +thing.upload.speed=921600 +thing.upload.resetmethod=ck +thing.upload.maximum_size=434160 +thing.upload.maximum_data_size=81920 +thing.upload.wait_for_upload_port=true +thing.serial.disableDTR=true +thing.serial.disableRTS=true + +thing.build.mcu=esp8266 +thing.build.f_cpu=80000000L +thing.build.board=ESP8266_THING +thing.build.core=esp8266 +thing.build.variant=thing +thing.build.flash_mode=qio +# flash chip: AT25SF041 (512 kbyte, 4Mbit) +thing.build.flash_size=512K +thing.build.flash_ld=eagle.flash.512k64.ld +thing.build.flash_freq=40 +thing.build.spiffs_start=0x6B000 +thing.build.spiffs_end=0x7B000 +thing.build.spiffs_blocksize=4096 +thing.build.spiffs_pagesize=256 +thing.build.debug_port= +thing.build.debug_level= + +thing.menu.CpuFrequency.80=80 MHz +thing.menu.CpuFrequency.80.build.f_cpu=80000000L +thing.menu.CpuFrequency.160=160 MHz +thing.menu.CpuFrequency.160.build.f_cpu=160000000L + +thing.menu.UploadSpeed.115200=115200 +thing.menu.UploadSpeed.115200.upload.speed=115200 +thing.menu.UploadSpeed.9600=9600 +thing.menu.UploadSpeed.9600.upload.speed=9600 +thing.menu.UploadSpeed.57600=57600 +thing.menu.UploadSpeed.57600.upload.speed=57600 +thing.menu.UploadSpeed.256000.windows=256000 +thing.menu.UploadSpeed.256000.upload.speed=256000 +thing.menu.UploadSpeed.230400.linux=230400 +thing.menu.UploadSpeed.230400.macosx=230400 +thing.menu.UploadSpeed.230400.upload.speed=230400 +thing.menu.UploadSpeed.460800.linux=460800 +thing.menu.UploadSpeed.460800.macosx=460800 +thing.menu.UploadSpeed.460800.upload.speed=460800 +thing.menu.UploadSpeed.512000.windows=512000 +thing.menu.UploadSpeed.512000.upload.speed=512000 +thing.menu.UploadSpeed.921600=921600 +thing.menu.UploadSpeed.921600.upload.speed=921600 + +############################################################## +thingdev.name=SparkFun ESP8266 Thing Dev + +thingdev.upload.tool=esptool +thingdev.upload.speed=921600 +thingdev.upload.resetmethod=nodemcu +thingdev.upload.maximum_size=434160 +thingdev.upload.maximum_data_size=81920 +thingdev.upload.wait_for_upload_port=true +thingdev.serial.disableDTR=true +thingdev.serial.disableRTS=true + +thingdev.build.mcu=esp8266 +thingdev.build.f_cpu=80000000L +thingdev.build.board=ESP8266_THING_DEV +thingdev.build.core=esp8266 +thingdev.build.variant=thing +thingdev.build.flash_mode=dio +# flash chip: AT25SF041 (512 kbyte, 4Mbit) +thingdev.build.flash_size=512K +thingdev.build.flash_ld=eagle.flash.512k64.ld +thingdev.build.flash_freq=40 +thingdev.build.debug_port= +thingdev.build.debug_level= + +thingdev.menu.CpuFrequency.80=80 MHz +thingdev.menu.CpuFrequency.80.build.f_cpu=80000000L +thingdev.menu.CpuFrequency.160=160 MHz +thingdev.menu.CpuFrequency.160.build.f_cpu=160000000L + +thingdev.menu.UploadSpeed.115200=115200 +thingdev.menu.UploadSpeed.115200.upload.speed=115200 +thingdev.menu.UploadSpeed.9600=9600 +thingdev.menu.UploadSpeed.9600.upload.speed=9600 +thingdev.menu.UploadSpeed.57600=57600 +thingdev.menu.UploadSpeed.57600.upload.speed=57600 +thingdev.menu.UploadSpeed.256000.windows=256000 +thingdev.menu.UploadSpeed.256000.upload.speed=256000 +thingdev.menu.UploadSpeed.230400.linux=230400 +thingdev.menu.UploadSpeed.230400.macosx=230400 +thingdev.menu.UploadSpeed.230400.upload.speed=230400 +thingdev.menu.UploadSpeed.460800.linux=460800 +thingdev.menu.UploadSpeed.460800.macosx=460800 +thingdev.menu.UploadSpeed.460800.upload.speed=460800 +thingdev.menu.UploadSpeed.512000.windows=512000 +thingdev.menu.UploadSpeed.512000.upload.speed=512000 +thingdev.menu.UploadSpeed.921600=921600 +thingdev.menu.UploadSpeed.921600.upload.speed=921600 + +############################################################## +esp210.name=SweetPea ESP-210 + +esp210.upload.tool=esptool +esp210.upload.speed=115200 +esp210.upload.resetmethod=ck +esp210.upload.maximum_size=1044464 +esp210.upload.maximum_data_size=81920 +esp210.upload.wait_for_upload_port=true +esp210.serial.disableDTR=true +esp210.serial.disableRTS=true + +esp210.build.mcu=esp8266 +esp210.build.f_cpu=80000000L +esp210.build.board=ESP8266_ESP210 +esp210.build.core=esp8266 +esp210.build.variant=generic +esp210.build.flash_mode=qio +esp210.build.flash_size=4M +esp210.build.flash_freq=40 +esp210.build.debug_port= +esp210.build.debug_level= + +esp210.menu.CpuFrequency.80=80 MHz +esp210.menu.CpuFrequency.80.build.f_cpu=80000000L +esp210.menu.CpuFrequency.160=160 MHz +esp210.menu.CpuFrequency.160.build.f_cpu=160000000L + +esp210.menu.UploadSpeed.57600=57600 +esp210.menu.UploadSpeed.57600.upload.speed=57600 +esp210.menu.UploadSpeed.115200=115200 +esp210.menu.UploadSpeed.115200.upload.speed=115200 +esp210.menu.UploadSpeed.256000.windows=256000 +esp210.menu.UploadSpeed.256000.upload.speed=256000 +esp210.menu.UploadSpeed.230400.linux=230400 +esp210.menu.UploadSpeed.230400.macosx=230400 +esp210.menu.UploadSpeed.230400.macosx=230400 +esp210.menu.UploadSpeed.230400.upload.speed=230400 +esp210.menu.UploadSpeed.460800.linux=460800 +esp210.menu.UploadSpeed.460800.macosx=460800 +esp210.menu.UploadSpeed.460800.upload.speed=460800 +esp210.menu.UploadSpeed.512000.windows=512000 +esp210.menu.UploadSpeed.512000.upload.speed=512000 +esp210.menu.UploadSpeed.921600=921600 +esp210.menu.UploadSpeed.921600.upload.speed=921600 + +esp210.menu.FlashSize.4M3M=4M (3M SPIFFS) +esp210.menu.FlashSize.4M3M.build.flash_size=4M +esp210.menu.FlashSize.4M3M.build.flash_ld=eagle.flash.4m.ld +esp210.menu.FlashSize.4M3M.build.spiffs_start=0x100000 +esp210.menu.FlashSize.4M3M.build.spiffs_end=0x3FB000 +esp210.menu.FlashSize.4M3M.build.spiffs_blocksize=8192 +esp210.menu.FlashSize.4M3M.build.spiffs_pagesize=256 + +esp210.menu.FlashSize.4M1M=4M (1M SPIFFS) +esp210.menu.FlashSize.4M1M.build.flash_size=4M +esp210.menu.FlashSize.4M1M.build.flash_ld=eagle.flash.4m1m.ld +esp210.menu.FlashSize.4M1M.build.spiffs_start=0x300000 +esp210.menu.FlashSize.4M1M.build.spiffs_end=0x3FB000 +esp210.menu.FlashSize.4M1M.build.spiffs_blocksize=8192 +esp210.menu.FlashSize.4M1M.build.spiffs_pagesize=256 + +############################################################## +# wifio.name=Wifio +# +# wifio.upload.tool=esptool +# wifio.upload.speed=115200 +# wifio.upload.resetmethod=wifio +# wifio.upload.maximum_size=524288 +# wifio.upload.wait_for_upload_port=true +# +# wifio.build.mcu=esp8266 +# wifio.build.f_cpu=80000000L +# wifio.build.board=ESP8266_WIFIO +# wifio.build.core=esp8266 +# wifio.build.variant=wifio +# wifio.build.flash_mode=qio +# wifio.build.flash_size=512K +# wifio.build.flash_freq=40 +# wifio.build.flash_ld=eagle.flash.512k64.ld +# wifio.build.spiffs_start=0x6B000 +# wifio.build.spiffs_end=0x7B000 +# +# wifio.menu.CpuFrequency.80=80MHz +# wifio.menu.CpuFrequency.80.build.f_cpu=80000000L +# wifio.menu.CpuFrequency.160=160MHz +# wifio.menu.CpuFrequency.160.build.f_cpu=160000000L +# +# wifio.upload.tool=esptool +# + +############################################################## +d1_mini.name=WeMos D1 R2 & mini + +d1_mini.upload.tool=esptool +d1_mini.upload.speed=460800 +d1_mini.upload.resetmethod=nodemcu +d1_mini.upload.maximum_size=1044464 +d1_mini.upload.maximum_data_size=81920 +d1_mini.upload.wait_for_upload_port=true +d1_mini.serial.disableDTR=true +d1_mini.serial.disableRTS=true + +d1_mini.build.mcu=esp8266 +d1_mini.build.f_cpu=80000000L +d1_mini.build.board=ESP8266_WEMOS_D1MINI +d1_mini.build.core=esp8266 +d1_mini.build.variant=d1_mini +d1_mini.build.flash_mode=dio +d1_mini.build.flash_size=4M +d1_mini.build.flash_freq=40 +d1_mini.build.debug_port= +d1_mini.build.debug_level= + +d1_mini.menu.UploadTool.esptool=Serial +d1_mini.menu.UploadTool.esptool.upload.tool=esptool +d1_mini.menu.UploadTool.esptool.upload.verbose=-vv +d1_mini.menu.UploadTool.espupload=OTA_upload +d1_mini.menu.UploadTool.espupload.upload.tool=espupload + +d1_mini.menu.CpuFrequency.80=80 MHz +d1_mini.menu.CpuFrequency.80.build.f_cpu=80000000L +d1_mini.menu.CpuFrequency.160=160 MHz +d1_mini.menu.CpuFrequency.160.build.f_cpu=160000000L + +d1_mini.menu.UploadSpeed.921600=921600 +d1_mini.menu.UploadSpeed.921600.upload.speed=921600 +d1_mini.menu.UploadSpeed.115200=115200 +d1_mini.menu.UploadSpeed.115200.upload.speed=115200 +d1_mini.menu.UploadSpeed.9600=9600 +d1_mini.menu.UploadSpeed.9600.upload.speed=9600 +d1_mini.menu.UploadSpeed.57600=57600 +d1_mini.menu.UploadSpeed.57600.upload.speed=57600 +d1_mini.menu.UploadSpeed.256000.windows=256000 +d1_mini.menu.UploadSpeed.256000.upload.speed=256000 +d1_mini.menu.UploadSpeed.230400.linux=230400 +d1_mini.menu.UploadSpeed.230400.macosx=230400 +d1_mini.menu.UploadSpeed.230400.macosx=230400 +d1_mini.menu.UploadSpeed.230400.upload.speed=230400 +d1_mini.menu.UploadSpeed.460800.linux=460800 +d1_mini.menu.UploadSpeed.460800.macosx=460800 +d1_mini.menu.UploadSpeed.460800.upload.speed=460800 +d1_mini.menu.UploadSpeed.512000.windows=512000 +d1_mini.menu.UploadSpeed.512000.upload.speed=512000 + + +d1_mini.menu.FlashSize.4M3M=4M (3M SPIFFS) +d1_mini.menu.FlashSize.4M3M.build.flash_size=4M +d1_mini.menu.FlashSize.4M3M.build.flash_ld=eagle.flash.4m.ld +d1_mini.menu.FlashSize.4M3M.build.spiffs_start=0x100000 +d1_mini.menu.FlashSize.4M3M.build.spiffs_end=0x3FB000 +d1_mini.menu.FlashSize.4M3M.build.spiffs_blocksize=8192 +d1_mini.menu.FlashSize.4M3M.build.spiffs_pagesize=256 + +d1_mini.menu.FlashSize.4M1M=4M (1M SPIFFS) +d1_mini.menu.FlashSize.4M1M.build.flash_size=4M +d1_mini.menu.FlashSize.4M1M.build.flash_ld=eagle.flash.4m1m.ld +d1_mini.menu.FlashSize.4M1M.build.spiffs_start=0x300000 +d1_mini.menu.FlashSize.4M1M.build.spiffs_end=0x3FB000 +d1_mini.menu.FlashSize.4M1M.build.spiffs_blocksize=8192 +d1_mini.menu.FlashSize.4M1M.build.spiffs_pagesize=256 + +############################################################## +d1.name=WeMos D1(Retired) + +d1.upload.tool=esptool +d1.upload.speed=460800 +d1.upload.resetmethod=nodemcu +d1.upload.maximum_size=1044464 +d1.upload.maximum_data_size=81920 +d1.upload.wait_for_upload_port=true +d1.serial.disableDTR=true +d1.serial.disableRTS=true + +d1.build.mcu=esp8266 +d1.build.f_cpu=80000000L +d1.build.board=ESP8266_WEMOS_D1MINI +d1.build.core=esp8266 +d1.build.variant=d1 +d1.build.flash_mode=dio +d1.build.flash_size=4M +d1.build.flash_freq=40 +d1.build.debug_port= +d1.build.debug_level= + +d1.menu.CpuFrequency.80=80 MHz +d1.menu.CpuFrequency.80.build.f_cpu=80000000L +d1.menu.CpuFrequency.160=160 MHz +d1.menu.CpuFrequency.160.build.f_cpu=160000000L + +d1.menu.UploadSpeed.921600=921600 +d1.menu.UploadSpeed.921600.upload.speed=921600 +d1.menu.UploadSpeed.115200=115200 +d1.menu.UploadSpeed.115200.upload.speed=115200 +d1.menu.UploadSpeed.9600=9600 +d1.menu.UploadSpeed.9600.upload.speed=9600 +d1.menu.UploadSpeed.57600=57600 +d1.menu.UploadSpeed.57600.upload.speed=57600 +d1.menu.UploadSpeed.256000.windows=256000 +d1.menu.UploadSpeed.256000.upload.speed=256000 +d1.menu.UploadSpeed.230400.linux=230400 +d1.menu.UploadSpeed.230400.macosx=230400 +d1.menu.UploadSpeed.230400.macosx=230400 +d1.menu.UploadSpeed.230400.upload.speed=230400 +d1.menu.UploadSpeed.460800.linux=460800 +d1.menu.UploadSpeed.460800.macosx=460800 +d1.menu.UploadSpeed.460800.upload.speed=460800 +d1.menu.UploadSpeed.512000.windows=512000 +d1.menu.UploadSpeed.512000.upload.speed=512000 + + +d1.menu.FlashSize.4M3M=4M (3M SPIFFS) +d1.menu.FlashSize.4M3M.build.flash_size=4M +d1.menu.FlashSize.4M3M.build.flash_ld=eagle.flash.4m.ld +d1.menu.FlashSize.4M3M.build.spiffs_start=0x100000 +d1.menu.FlashSize.4M3M.build.spiffs_end=0x3FB000 +d1.menu.FlashSize.4M3M.build.spiffs_blocksize=8192 +d1.menu.FlashSize.4M3M.build.spiffs_pagesize=256 + +d1.menu.FlashSize.4M1M=4M (1M SPIFFS) +d1.menu.FlashSize.4M1M.build.flash_size=4M +d1.menu.FlashSize.4M1M.build.flash_ld=eagle.flash.4m1m.ld +d1.menu.FlashSize.4M1M.build.spiffs_start=0x300000 +d1.menu.FlashSize.4M1M.build.spiffs_end=0x3FB000 +d1.menu.FlashSize.4M1M.build.spiffs_blocksize=8192 +d1.menu.FlashSize.4M1M.build.spiffs_pagesize=256 + + +############################################################## + +espino.name=ESPino (ESP-12 Module) + +espino.upload.tool=esptool +espino.upload.speed=115200 +espino.upload.resetmethod=ck +espino.upload.maximum_size=1044464 +espino.upload.maximum_data_size=81920 +espino.upload.wait_for_upload_port=true +espino.serial.disableDTR=true +espino.serial.disableRTS=true + +espino.build.mcu=esp8266 +espino.build.f_cpu=80000000L +espino.build.board=ESP8266_ESP12 +espino.build.core=esp8266 +espino.build.variant=espino +espino.build.flash_mode=qio +espino.build.flash_size=4M +espino.build.flash_freq=40 +espino.build.spiffs_pagesize=256 +espino.build.debug_port= +espino.build.debug_level= + +espino.menu.CpuFrequency.80=80 MHz +espino.menu.CpuFrequency.80.build.f_cpu=80000000L +espino.menu.CpuFrequency.160=160 MHz +espino.menu.CpuFrequency.160.build.f_cpu=160000000L + +espino.menu.FlashMode.dio=DIO +espino.menu.FlashMode.dio.build.flash_mode=dio +espino.menu.FlashMode.qio=QIO +espino.menu.FlashMode.qio.build.flash_mode=qio + +espino.menu.UploadSpeed.115200=115200 +espino.menu.UploadSpeed.115200.upload.speed=115200 +espino.menu.UploadSpeed.9600=9600 +espino.menu.UploadSpeed.9600.upload.speed=9600 +espino.menu.UploadSpeed.57600=57600 +espino.menu.UploadSpeed.57600.upload.speed=57600 +espino.menu.UploadSpeed.256000.windows=256000 +espino.menu.UploadSpeed.256000.upload.speed=256000 +espino.menu.UploadSpeed.230400.linux=230400 +espino.menu.UploadSpeed.230400.macosx=230400 +espino.menu.UploadSpeed.230400.upload.speed=230400 +espino.menu.UploadSpeed.460800.linux=460800 +espino.menu.UploadSpeed.460800.macosx=460800 +espino.menu.UploadSpeed.460800.upload.speed=460800 +espino.menu.UploadSpeed.512000.windows=512000 +espino.menu.UploadSpeed.512000.upload.speed=512000 +espino.menu.UploadSpeed.921600=921600 +espino.menu.UploadSpeed.921600.upload.speed=921600 + +espino.menu.FlashSize.4M1M=4M (1M SPIFFS) +espino.menu.FlashSize.4M1M.build.flash_size=4M +espino.menu.FlashSize.4M1M.build.flash_ld=eagle.flash.4m1m.ld +espino.menu.FlashSize.4M1M.build.spiffs_start=0x300000 +espino.menu.FlashSize.4M1M.build.spiffs_end=0x3FB000 +espino.menu.FlashSize.4M1M.build.spiffs_blocksize=8192 +espino.menu.FlashSize.4M1M.build.spiffs_pagesize=256 +espino.menu.FlashSize.4M1M.upload.maximum_size=1044464 + +espino.menu.FlashSize.4M3M=4M (3M SPIFFS) +espino.menu.FlashSize.4M3M.build.flash_size=4M +espino.menu.FlashSize.4M3M.build.flash_ld=eagle.flash.4m.ld +espino.menu.FlashSize.4M3M.build.spiffs_start=0x100000 +espino.menu.FlashSize.4M3M.build.spiffs_end=0x3FB000 +espino.menu.FlashSize.4M3M.build.spiffs_blocksize=8192 +espino.menu.FlashSize.4M3M.upload.maximum_size=1044464 + +espino.menu.ResetMethod.ck=ck +espino.menu.ResetMethod.ck.upload.resetmethod=ck +espino.menu.ResetMethod.nodemcu=nodemcu +espino.menu.ResetMethod.nodemcu.upload.resetmethod=nodemcu + +############################################################## +espinotee.name=ThaiEasyElec's ESPino + +espinotee.upload.tool=esptool +espinotee.upload.speed=115200 +espinotee.upload.resetmethod=nodemcu +espinotee.upload.maximum_size=1044464 +espinotee.upload.maximum_data_size=81920 +espinotee.upload.wait_for_upload_port=true +espinotee.serial.disableDTR=true +espinotee.serial.disableRTS=true + +espinotee.build.mcu=esp8266 +espinotee.build.f_cpu=80000000L +espinotee.build.board=ESP8266_ESP13 +espinotee.build.core=esp8266 +espinotee.build.variant=espinotee +espinotee.build.flash_mode=qio +espinotee.build.flash_size=4M +espinotee.build.flash_freq=40 +espinotee.build.debug_port= +espinotee.build.debug_level= + +espinotee.menu.CpuFrequency.80=80 MHz +espinotee.menu.CpuFrequency.80.build.f_cpu=80000000L +espinotee.menu.CpuFrequency.160=160 MHz +espinotee.menu.CpuFrequency.160.build.f_cpu=160000000L + +espinotee.menu.UploadSpeed.115200=115200 +espinotee.menu.UploadSpeed.115200.upload.speed=115200 +espinotee.menu.UploadSpeed.9600=9600 +espinotee.menu.UploadSpeed.9600.upload.speed=9600 +espinotee.menu.UploadSpeed.57600=57600 +espinotee.menu.UploadSpeed.57600.upload.speed=57600 +espinotee.menu.UploadSpeed.256000.windows=256000 +espinotee.menu.UploadSpeed.256000.upload.speed=256000 +espinotee.menu.UploadSpeed.230400.linux=230400 +espinotee.menu.UploadSpeed.230400.macosx=230400 +espinotee.menu.UploadSpeed.230400.macosx=230400 +espinotee.menu.UploadSpeed.230400.upload.speed=230400 +espinotee.menu.UploadSpeed.460800.linux=460800 +espinotee.menu.UploadSpeed.460800.macosx=460800 +espinotee.menu.UploadSpeed.460800.upload.speed=460800 +espinotee.menu.UploadSpeed.512000.windows=512000 +espinotee.menu.UploadSpeed.512000.upload.speed=512000 +espinotee.menu.UploadSpeed.921600=921600 +espinotee.menu.UploadSpeed.921600.upload.speed=921600 + +espinotee.menu.FlashSize.4M3M=4M (3M SPIFFS) +espinotee.menu.FlashSize.4M3M.build.flash_size=4M +espinotee.menu.FlashSize.4M3M.build.flash_ld=eagle.flash.4m.ld +espinotee.menu.FlashSize.4M3M.build.spiffs_start=0x100000 +espinotee.menu.FlashSize.4M3M.build.spiffs_end=0x3FB000 +espinotee.menu.FlashSize.4M3M.build.spiffs_blocksize=8192 +espinotee.menu.FlashSize.4M3M.build.spiffs_pagesize=256 + +espinotee.menu.FlashSize.4M1M=4M (1M SPIFFS) +espinotee.menu.FlashSize.4M1M.build.flash_size=4M +espinotee.menu.FlashSize.4M1M.build.flash_ld=eagle.flash.4m1m.ld +espinotee.menu.FlashSize.4M1M.build.spiffs_start=0x300000 +espinotee.menu.FlashSize.4M1M.build.spiffs_end=0x3FB000 +espinotee.menu.FlashSize.4M1M.build.spiffs_blocksize=8192 +espinotee.menu.FlashSize.4M1M.build.spiffs_pagesize=256 + +############################################################## +wifinfo.name=WifInfo + +wifinfo.upload.tool=esptool +wifinfo.upload.speed=115200 +wifinfo.upload.resetmethod=nodemcu +wifinfo.upload.maximum_size=434160 +wifinfo.upload.maximum_data_size=81920 +wifinfo.upload.wait_for_upload_port=true +wifinfo.serial.disableDTR=true +wifinfo.serial.disableRTS=true + +wifinfo.build.mcu=esp8266 +wifinfo.build.core=esp8266 +wifinfo.build.variant=wifinfo +wifinfo.build.board=WIFINFO +wifinfo.build.spiffs_pagesize=256 +wifinfo.build.debug_port=Serial1 +wifinfo.build.debug_level=Wifinfo + +wifinfo.menu.Debug.Disabled=Disabled +wifinfo.menu.Debug.Disabled.build.debug_port= +wifinfo.menu.Debug.Serial=Serial +wifinfo.menu.Debug.Serial.build.debug_port=-DDEBUG_ESP_PORT=Serial +wifinfo.menu.Debug.Serial1=Serial1 +wifinfo.menu.Debug.Serial1.build.debug_port=-DDEBUG_ESP_PORT=Serial1 + +wifinfo.menu.DebugLevel.None=None +wifinfo.menu.DebugLevel.None.build.debug_level= +wifinfo.menu.DebugLevel.Wifinfo=Wifinfo +wifinfo.menu.DebugLevel.Wifinfo.build.debug_level=-DDEBUG_ESP_WIFINFO + +#wifinfo.menu.ESPModule.ESP07512=ESP07 (1M/512K SPIFFS) +#wifinfo.menu.ESPModule.ESP07512.build.board=ESP8266_ESP07 +#wifinfo.menu.ESPModule.ESP07512.build.flash_size=1M +#wifinfo.menu.ESPModule.ESP07512.build.flash_ld=eagle.flash.1m512.ld +#wifinfo.menu.ESPModule.ESP07512.build.spiffs_start=0x7B000 +#wifinfo.menu.ESPModule.ESP07512.build.spiffs_end=0xFB000 +#wifinfo.menu.ESPModule.ESP07512.build.spiffs_blocksize=8192 +#wifinfo.menu.ESPModule.ESP07512.upload.maximum_size=499696 + +#wifinfo.menu.ESPModule.ESP07256=ESP07 (1M/256K SPIFFS) +#wifinfo.menu.ESPModule.ESP07256.build.board=ESP8266_ESP07 +#wifinfo.menu.ESPModule.ESP07256.build.flash_size=1M +#wifinfo.menu.ESPModule.ESP07256.build.flash_ld=eagle.flash.1m256.ld +#wifinfo.menu.ESPModule.ESP07256.build.spiffs_start=0xBB000 +#wifinfo.menu.ESPModule.ESP07256.build.spiffs_end=0xFB000 +##wifinfo.menu.ESPModule.ESP07256.build.spiffs_blocksize=4096 +#wifinfo.menu.ESPModule.ESP07256.upload.maximum_size=761840 + +wifinfo.menu.ESPModule.ESP07192=ESP07 (1M/192K SPIFFS) +wifinfo.menu.ESPModule.ESP07192.build.board=ESP8266_ESP07 +wifinfo.menu.ESPModule.ESP07192.build.flash_size=1M +wifinfo.menu.ESPModule.ESP07192.build.flash_ld=eagle.flash.1m192.ld +wifinfo.menu.ESPModule.ESP07192.build.spiffs_start=0xCB000 +wifinfo.menu.ESPModule.ESP07192.build.spiffs_end=0xFB000 +wifinfo.menu.ESPModule.ESP07192.build.spiffs_blocksize=4096 +wifinfo.menu.ESPModule.ESP07192.upload.maximum_size=827376 + +#wifinfo.menu.ESPModule.ESP07160=ESP07 (1M/160K SPIFFS) +#wifinfo.menu.ESPModule.ESP07160.build.board=ESP8266_ESP07 +#wifinfo.menu.ESPModule.ESP07160.build.flash_size=1M +#wifinfo.menu.ESPModule.ESP07160.build.flash_ld=eagle.flash.1m160.ld +#wifinfo.menu.ESPModule.ESP07160.build.spiffs_start=0xD3000 +#wifinfo.menu.ESPModule.ESP07160.build.spiffs_end=0xFB000 +#wifinfo.menu.ESPModule.ESP07160.build.spiffs_blocksize=4096 +#wifinfo.menu.ESPModule.ESP07160.upload.maximum_size=860144 +# +#wifinfo.menu.ESPModule.ESP07144=ESP07 (1M/144K SPIFFS) +#wifinfo.menu.ESPModule.ESP07144.build.board=ESP8266_ESP07 +#wifinfo.menu.ESPModule.ESP07144.build.flash_size=1M +#wifinfo.menu.ESPModule.ESP07144.build.flash_ld=eagle.flash.1m144.ld +#wifinfo.menu.ESPModule.ESP07144.build.spiffs_start=0xD7000 +#wifinfo.menu.ESPModule.ESP07144.build.spiffs_end=0xFB000 +#wifinfo.menu.ESPModule.ESP07144.build.spiffs_blocksize=4096 +#wifinfo.menu.ESPModule.ESP07144.upload.maximum_size=876528 +# +#wifinfo.menu.ESPModule.ESP07=ESP07 (1M/64K SPIFFS) +#wifinfo.menu.ESPModule.ESP07.build.board=ESP8266_ESP07 +#wifinfo.menu.ESPModule.ESP07.build.flash_size=1M +#wifinfo.menu.ESPModule.ESP07.build.flash_ld=eagle.flash.1m64.ld +#wifinfo.menu.ESPModule.ESP07.build.spiffs_start=0xEB000 +#wifinfo.menu.ESPModule.ESP07.build.spiffs_end=0xFB000 +#wifinfo.menu.ESPModule.ESP07.build.spiffs_blocksize=4096 +#wifinfo.menu.ESPModule.ESP07.upload.maximum_size=958448 + +wifinfo.menu.ESPModule.ESP12=ESP12 (4M/1M SPIFFS) +wifinfo.menu.ESPModule.ESP12.build.board=ESP8266_ESP12 +wifinfo.menu.ESPModule.ESP12.build.flash_size=4M +wifinfo.menu.ESPModule.ESP12.build.flash_ld=eagle.flash.4m1m.ld +wifinfo.menu.ESPModule.ESP12.build.spiffs_start=0x300000 +wifinfo.menu.ESPModule.ESP12.build.spiffs_end=0x3FB000 +wifinfo.menu.ESPModule.ESP12.build.spiffs_blocksize=8192 +wifinfo.menu.ESPModule.ESP12.build.spiffs_pagesize=256 +wifinfo.menu.ESPModule.ESP12.upload.maximum_size=1044464 + +wifinfo.menu.CpuFrequency.160=160 MHz +wifinfo.menu.CpuFrequency.160.build.f_cpu=160000000L +wifinfo.menu.CpuFrequency.80=80 MHz +wifinfo.menu.CpuFrequency.80.build.f_cpu=80000000L + +wifinfo.menu.FlashFreq.40=40MHz +wifinfo.menu.FlashFreq.40.build.flash_freq=40 +wifinfo.menu.FlashFreq.80=80MHz +wifinfo.menu.FlashFreq.80.build.flash_freq=80 + +wifinfo.menu.FlashMode.qio=QIO +wifinfo.menu.FlashMode.qio.build.flash_mode=qio +wifinfo.menu.FlashMode.dio=DIO +wifinfo.menu.FlashMode.dio.build.flash_mode=dio + +wifinfo.menu.UploadSpeed.115200=115200 +wifinfo.menu.UploadSpeed.115200.upload.speed=115200 +wifinfo.menu.UploadSpeed.9600=9600 +wifinfo.menu.UploadSpeed.9600.upload.speed=9600 +wifinfo.menu.UploadSpeed.57600=57600 +wifinfo.menu.UploadSpeed.57600.upload.speed=57600 +wifinfo.menu.UploadSpeed.256000.windows=256000 +wifinfo.menu.UploadSpeed.256000.upload.speed=256000 +wifinfo.menu.UploadSpeed.230400.linux=230400 +wifinfo.menu.UploadSpeed.230400.macosx=230400 +wifinfo.menu.UploadSpeed.230400.upload.speed=230400 +wifinfo.menu.UploadSpeed.460800.linux=460800 +wifinfo.menu.UploadSpeed.460800.macosx=460800 +wifinfo.menu.UploadSpeed.460800.upload.speed=460800 +wifinfo.menu.UploadSpeed.512000.windows=512000 +wifinfo.menu.UploadSpeed.512000.upload.speed=512000 +wifinfo.menu.UploadSpeed.921600=921600 +wifinfo.menu.UploadSpeed.921600.upload.speed=921600 + + +############################################################## +coredev.name=Core Development Module + +coredev.upload.tool=esptool +coredev.upload.speed=115200 +coredev.upload.resetmethod=ck +coredev.upload.maximum_size=434160 +coredev.upload.maximum_data_size=81920 +coredev.upload.wait_for_upload_port=true +coredev.serial.disableDTR=true +coredev.serial.disableRTS=true + +coredev.build.mcu=esp8266 +coredev.build.f_cpu=80000000L +coredev.build.board=ESP8266_ESP01 +coredev.build.core=esp8266 +coredev.build.variant=generic +coredev.build.flash_mode=qio +coredev.build.spiffs_pagesize=256 +coredev.build.debug_port= +coredev.build.debug_level= +coredev.build.lwip_lib=-llwip +coredev.build.lwip_flags= + + +coredev.menu.LwIPVariant.Espressif=Espressif (xcc) +coredev.menu.LwIPVariant.Espressif.build.lwip_lib=-llwip +coredev.menu.LwIPVariant.Espressif.build.lwip_flags= +coredev.menu.LwIPVariant.Prebuilt=Prebuilt Source (gcc) +coredev.menu.LwIPVariant.Prebuilt.build.lwip_lib=-llwip_gcc +coredev.menu.LwIPVariant.Prebuilt.build.lwip_flags=-DLWIP_OPEN_SRC +coredev.menu.LwIPVariant.OpenSource=Open Source (gcc) +coredev.menu.LwIPVariant.OpenSource.build.lwip_lib=-llwip_src +coredev.menu.LwIPVariant.OpenSource.build.lwip_flags=-DLWIP_OPEN_SRC +coredev.menu.LwIPVariant.OpenSource.recipe.hooks.sketch.prebuild.1.pattern=make -C "{runtime.platform.path}/tools/sdk/lwip/src" install TOOLS_PATH="{runtime.tools.xtensa-lx106-elf-gcc.path}/bin/xtensa-lx106-elf-" + +coredev.menu.CpuFrequency.80=80 MHz +coredev.menu.CpuFrequency.80.build.f_cpu=80000000L +coredev.menu.CpuFrequency.160=160 MHz +coredev.menu.CpuFrequency.160.build.f_cpu=160000000L + +coredev.menu.FlashFreq.40=40MHz +coredev.menu.FlashFreq.40.build.flash_freq=40 +coredev.menu.FlashFreq.80=80MHz +coredev.menu.FlashFreq.80.build.flash_freq=80 + +coredev.menu.FlashMode.dio=DIO +coredev.menu.FlashMode.dio.build.flash_mode=dio +coredev.menu.FlashMode.qio=QIO +coredev.menu.FlashMode.qio.build.flash_mode=qio +coredev.menu.FlashMode.dout=DOUT +coredev.menu.FlashMode.dout.build.flash_mode=dout +coredev.menu.FlashMode.qout=QOUT +coredev.menu.FlashMode.qout.build.flash_mode=qout + +coredev.menu.UploadSpeed.115200=115200 +coredev.menu.UploadSpeed.115200.upload.speed=115200 +coredev.menu.UploadSpeed.9600=9600 +coredev.menu.UploadSpeed.9600.upload.speed=9600 +coredev.menu.UploadSpeed.57600=57600 +coredev.menu.UploadSpeed.57600.upload.speed=57600 +coredev.menu.UploadSpeed.256000.windows=256000 +coredev.menu.UploadSpeed.256000.upload.speed=256000 +coredev.menu.UploadSpeed.230400.linux=230400 +coredev.menu.UploadSpeed.230400.macosx=230400 +coredev.menu.UploadSpeed.230400.upload.speed=230400 +coredev.menu.UploadSpeed.460800.linux=460800 +coredev.menu.UploadSpeed.460800.macosx=460800 +coredev.menu.UploadSpeed.460800.upload.speed=460800 +coredev.menu.UploadSpeed.512000.windows=512000 +coredev.menu.UploadSpeed.512000.upload.speed=512000 +coredev.menu.UploadSpeed.921600=921600 +coredev.menu.UploadSpeed.921600.upload.speed=921600 + +coredev.menu.FlashSize.512K64=512K (64K SPIFFS) +coredev.menu.FlashSize.512K64.build.flash_size=512K +coredev.menu.FlashSize.512K64.build.flash_ld=eagle.flash.512k64.ld +coredev.menu.FlashSize.512K64.build.spiffs_start=0x6B000 +coredev.menu.FlashSize.512K64.build.spiffs_end=0x7B000 +coredev.menu.FlashSize.512K64.build.spiffs_blocksize=4096 +coredev.menu.FlashSize.512K64.upload.maximum_size=434160 + +coredev.menu.FlashSize.512K128=512K (128K SPIFFS) +coredev.menu.FlashSize.512K128.build.flash_size=512K +coredev.menu.FlashSize.512K128.build.flash_ld=eagle.flash.512k128.ld +coredev.menu.FlashSize.512K128.build.spiffs_start=0x5B000 +coredev.menu.FlashSize.512K128.build.spiffs_end=0x7B000 +coredev.menu.FlashSize.512K128.build.spiffs_blocksize=4096 +coredev.menu.FlashSize.512K128.upload.maximum_size=368624 + +coredev.menu.FlashSize.512K0=512K (no SPIFFS) +coredev.menu.FlashSize.512K0.build.flash_size=512K +coredev.menu.FlashSize.512K0.build.flash_ld=eagle.flash.512k0.ld +coredev.menu.FlashSize.512K0.upload.maximum_size=499696 + +coredev.menu.FlashSize.1M512=1M (512K SPIFFS) +coredev.menu.FlashSize.1M512.build.flash_size=1M +coredev.menu.FlashSize.1M512.build.flash_ld=eagle.flash.1m512.ld +coredev.menu.FlashSize.1M512.build.spiffs_start=0x7B000 +coredev.menu.FlashSize.1M512.build.spiffs_end=0xFB000 +coredev.menu.FlashSize.1M512.build.spiffs_blocksize=8192 +coredev.menu.FlashSize.1M512.upload.maximum_size=499696 + +coredev.menu.FlashSize.1M256=1M (256K SPIFFS) +coredev.menu.FlashSize.1M256.build.flash_size=1M +coredev.menu.FlashSize.1M256.build.flash_ld=eagle.flash.1m256.ld +coredev.menu.FlashSize.1M256.build.spiffs_start=0xBB000 +coredev.menu.FlashSize.1M256.build.spiffs_end=0xFB000 +coredev.menu.FlashSize.1M256.build.spiffs_blocksize=4096 +coredev.menu.FlashSize.1M256.upload.maximum_size=761840 + +coredev.menu.FlashSize.1M192=1M (192K SPIFFS) +coredev.menu.FlashSize.1M192.build.flash_size=1M +coredev.menu.FlashSize.1M192.build.flash_ld=eagle.flash.1m192.ld +coredev.menu.FlashSize.1M192.build.spiffs_start=0xCB000 +coredev.menu.FlashSize.1M192.build.spiffs_end=0xFB000 +coredev.menu.FlashSize.1M192.build.spiffs_blocksize=4096 +coredev.menu.FlashSize.1M192.upload.maximum_size=827376 + +coredev.menu.FlashSize.1M160=1M (160K SPIFFS) +coredev.menu.FlashSize.1M160.build.flash_size=1M +coredev.menu.FlashSize.1M160.build.flash_ld=eagle.flash.1m160.ld +coredev.menu.FlashSize.1M160.build.spiffs_start=0xD3000 +coredev.menu.FlashSize.1M160.build.spiffs_end=0xFB000 +coredev.menu.FlashSize.1M160.build.spiffs_blocksize=4096 +coredev.menu.FlashSize.1M160.upload.maximum_size=860144 + +coredev.menu.FlashSize.1M144=1M (144K SPIFFS) +coredev.menu.FlashSize.1M144.build.flash_size=1M +coredev.menu.FlashSize.1M144.build.flash_ld=eagle.flash.1m144.ld +coredev.menu.FlashSize.1M144.build.spiffs_start=0xD7000 +coredev.menu.FlashSize.1M144.build.spiffs_end=0xFB000 +coredev.menu.FlashSize.1M144.build.spiffs_blocksize=4096 +coredev.menu.FlashSize.1M144.upload.maximum_size=876528 + +coredev.menu.FlashSize.1M128=1M (128K SPIFFS) +coredev.menu.FlashSize.1M128.build.flash_size=1M +coredev.menu.FlashSize.1M128.build.flash_ld=eagle.flash.1m128.ld +coredev.menu.FlashSize.1M128.build.spiffs_start=0xDB000 +coredev.menu.FlashSize.1M128.build.spiffs_end=0xFB000 +coredev.menu.FlashSize.1M128.build.spiffs_blocksize=4096 +coredev.menu.FlashSize.1M128.upload.maximum_size=892912 + +coredev.menu.FlashSize.1M64=1M (64K SPIFFS) +coredev.menu.FlashSize.1M64.build.flash_size=1M +coredev.menu.FlashSize.1M64.build.flash_ld=eagle.flash.1m64.ld +coredev.menu.FlashSize.1M64.build.spiffs_start=0xEB000 +coredev.menu.FlashSize.1M64.build.spiffs_end=0xFB000 +coredev.menu.FlashSize.1M64.build.spiffs_blocksize=4096 +coredev.menu.FlashSize.1M64.upload.maximum_size=958448 + +coredev.menu.FlashSize.2M=2M (1M SPIFFS) +coredev.menu.FlashSize.2M.build.flash_size=2M +coredev.menu.FlashSize.2M.build.flash_ld=eagle.flash.2m.ld +coredev.menu.FlashSize.2M.build.spiffs_start=0x100000 +coredev.menu.FlashSize.2M.build.spiffs_end=0x1FB000 +coredev.menu.FlashSize.2M.build.spiffs_blocksize=8192 +coredev.menu.FlashSize.2M.upload.maximum_size=1044464 + +coredev.menu.FlashSize.4M1M=4M (1M SPIFFS) +coredev.menu.FlashSize.4M1M.build.flash_size=4M +coredev.menu.FlashSize.4M1M.build.flash_ld=eagle.flash.4m1m.ld +coredev.menu.FlashSize.4M1M.build.spiffs_start=0x300000 +coredev.menu.FlashSize.4M1M.build.spiffs_end=0x3FB000 +coredev.menu.FlashSize.4M1M.build.spiffs_blocksize=8192 +coredev.menu.FlashSize.4M1M.build.spiffs_pagesize=256 +coredev.menu.FlashSize.4M1M.upload.maximum_size=1044464 + +coredev.menu.FlashSize.4M3M=4M (3M SPIFFS) +coredev.menu.FlashSize.4M3M.build.flash_size=4M +coredev.menu.FlashSize.4M3M.build.flash_ld=eagle.flash.4m.ld +coredev.menu.FlashSize.4M3M.build.spiffs_start=0x100000 +coredev.menu.FlashSize.4M3M.build.spiffs_end=0x3FB000 +coredev.menu.FlashSize.4M3M.build.spiffs_blocksize=8192 +coredev.menu.FlashSize.4M3M.upload.maximum_size=1044464 + +coredev.menu.ResetMethod.ck=ck +coredev.menu.ResetMethod.ck.upload.resetmethod=ck +coredev.menu.ResetMethod.nodemcu=nodemcu +coredev.menu.ResetMethod.nodemcu.upload.resetmethod=nodemcu + +coredev.menu.Debug.Disabled=Disabled +coredev.menu.Debug.Disabled.build.debug_port= +coredev.menu.Debug.Serial=Serial +coredev.menu.Debug.Serial.build.debug_port=-DDEBUG_ESP_PORT=Serial +coredev.menu.Debug.Serial1=Serial1 +coredev.menu.Debug.Serial1.build.debug_port=-DDEBUG_ESP_PORT=Serial1 + +coredev.menu.DebugLevel.None____=None +coredev.menu.DebugLevel.None____.build.debug_level= +coredev.menu.DebugLevel.Core____=Core +coredev.menu.DebugLevel.Core____.build.debug_level=-DDEBUG_ESP_CORE +coredev.menu.DebugLevel.SSL_____=Core + SSL +coredev.menu.DebugLevel.SSL_____.build.debug_level=-DDEBUG_ESP_CORE -DDEBUG_ESP_SSL +coredev.menu.DebugLevel.SSL_MEM_=Core + SSL + TLS Mem +coredev.menu.DebugLevel.SSL_MEM_.build.debug_level=-DDEBUG_ESP_CORE -DDEBUG_ESP_SSL -DDEBUG_TLS_MEM +coredev.menu.DebugLevel.WiFic___=Core + WiFi +coredev.menu.DebugLevel.WiFic___.build.debug_level=-DDEBUG_ESP_CORE -DDEBUG_ESP_WIFI +coredev.menu.DebugLevel.WiFi____=WiFi +coredev.menu.DebugLevel.WiFi____.build.debug_level=-DDEBUG_ESP_WIFI +coredev.menu.DebugLevel.HTTPClient=HTTPClient +coredev.menu.DebugLevel.HTTPClient.build.debug_level=-DDEBUG_ESP_HTTP_CLIENT +coredev.menu.DebugLevel.HTTPClient2=HTTPClient + SSL +coredev.menu.DebugLevel.HTTPClient2.build.debug_level=-DDEBUG_ESP_HTTP_CLIENT -DDEBUG_ESP_SSL +coredev.menu.DebugLevel.HTTPUpdate=HTTPUpdate +coredev.menu.DebugLevel.HTTPUpdate.build.debug_level=-DDEBUG_ESP_HTTP_UPDATE +coredev.menu.DebugLevel.HTTPUpdate2=HTTPClient + HTTPUpdate +coredev.menu.DebugLevel.HTTPUpdate2.build.debug_level=-DDEBUG_ESP_HTTP_UPDATE -DDEBUG_ESP_HTTP_UPDATE +coredev.menu.DebugLevel.HTTPUpdate3=HTTPClient + HTTPUpdate + Updater +coredev.menu.DebugLevel.HTTPUpdate3.build.debug_level=-DDEBUG_ESP_HTTP_UPDATE -DDEBUG_ESP_HTTP_UPDATE -DDEBUG_ESP_UPDATER +coredev.menu.DebugLevel.HTTPServer=HTTPServer +coredev.menu.DebugLevel.HTTPServer.build.debug_level=-DDEBUG_ESP_HTTP_SERVER +coredev.menu.DebugLevel.UPDATER=Updater +coredev.menu.DebugLevel.UPDATER.build.debug_level=-DDEBUG_ESP_UPDATER +coredev.menu.DebugLevel.OTA_____=OTA +coredev.menu.DebugLevel.OTA_____.build.debug_level=-DDEBUG_ESP_OTA +coredev.menu.DebugLevel.OTA2____=OTA + Updater +coredev.menu.DebugLevel.OTA2____.build.debug_level=-DDEBUG_ESP_OTA -DDEBUG_ESP_UPDATER +coredev.menu.DebugLevel.all_____=All +coredev.menu.DebugLevel.all_____.build.debug_level=-DDEBUG_ESP_CORE -DDEBUG_ESP_SSL -DDEBUG_ESP_WIFI -DDEBUG_ESP_HTTP_CLIENT -DDEBUG_ESP_HTTP_UPDATE -DDEBUG_ESP_HTTP_SERVER -DDEBUG_ESP_UPDATER -DDEBUG_ESP_OTA -DDEBUG_TLS_MEM diff --git a/arduino/version 2.3.0/platform.txt b/arduino/version 2.3.0/platform.txt new file mode 100644 index 000000000..d9c60b3f1 --- /dev/null +++ b/arduino/version 2.3.0/platform.txt @@ -0,0 +1,130 @@ + +# ESP8266 platform +# ------------------------------ + +# For more info: +# https://github.com/arduino/Arduino/wiki/Arduino-IDE-1.5---3rd-party-Hardware-specification + +name=ESP8266 Modules +version=2.2.0 + + + + +compiler.warning_flags=-w +compiler.warning_flags.none=-w +compiler.warning_flags.default= +compiler.warning_flags.more=-Wall +compiler.warning_flags.all=-Wall -Wextra + +build.lwip_lib=-llwip_gcc +build.lwip_flags=-DLWIP_OPEN_SRC + +compiler.path={runtime.tools.xtensa-lx106-elf-gcc.path}/bin/ +compiler.sdk.path={runtime.platform.path}/tools/sdk +compiler.cpreprocessor.flags=-D__ets__ -DICACHE_FLASH -U__STRICT_ANSI__ "-I{compiler.sdk.path}/include" "-I{compiler.sdk.path}/lwip/include" "-I{build.path}/core" + +compiler.c.cmd=xtensa-lx106-elf-gcc +compiler.c.flags=-c {compiler.warning_flags} -Os -g -Wpointer-arith -Wno-implicit-function-declaration -Wl,-EL -fno-inline-functions -nostdlib -mlongcalls -mtext-section-literals -falign-functions=4 -MMD -std=gnu99 -ffunction-sections -fdata-sections + +compiler.S.cmd=xtensa-lx106-elf-gcc +compiler.S.flags=-c -g -x assembler-with-cpp -MMD -mlongcalls + +compiler.c.elf.flags=-g {compiler.warning_flags} -Os -nostdlib -Wl,--no-check-sections -u call_user_start -Wl,-static "-L{compiler.sdk.path}/lib" "-L{compiler.sdk.path}/ld" "-T{build.flash_ld}" -Wl,--gc-sections -Wl,-wrap,system_restart_local -Wl,-wrap,register_chipv6_phy + +compiler.c.elf.cmd=xtensa-lx106-elf-gcc +compiler.c.elf.libs=-lm -lgcc -lhal -lphy -lpp -lnet80211 -lwpa -lcrypto -lmain -lwps -laxtls -lsmartconfig -lmesh -lwpa2 {build.lwip_lib} -lstdc++ + +compiler.cpp.cmd=xtensa-lx106-elf-g++ +compiler.cpp.flags=-c {compiler.warning_flags} -Os -g -mlongcalls -mtext-section-literals -fno-exceptions -fno-rtti -falign-functions=4 -std=c++11 -MMD -ffunction-sections -fdata-sections + +compiler.as.cmd=xtensa-lx106-elf-as + +compiler.ar.cmd=xtensa-lx106-elf-ar +compiler.ar.flags=cru + +compiler.elf2hex.cmd=esptool +compiler.elf2hex.flags= + +compiler.size.cmd=xtensa-lx106-elf-size + +compiler.esptool.cmd=esptool +compiler.esptool.cmd.windows=esptool.exe + +# This can be overriden in boards.txt +build.extra_flags=-DESP8266 + +# These can be overridden in platform.local.txt +compiler.c.extra_flags= +compiler.c.elf.extra_flags= +compiler.S.extra_flags= +compiler.cpp.extra_flags= +compiler.ar.extra_flags= +compiler.objcopy.eep.extra_flags= +compiler.elf2hex.extra_flags= + +## generate file with git version number +## needs bash, git, and echo + +## windows-compatible version may be added later + + +## Compile c files +recipe.c.o.pattern="{compiler.path}{compiler.c.cmd}" {compiler.cpreprocessor.flags} {compiler.c.flags} -DF_CPU={build.f_cpu} {build.lwip_flags} {build.debug_port} {build.debug_level} -DARDUINO={runtime.ide.version} -DARDUINO_{build.board} -DARDUINO_ARCH_{build.arch} -DARDUINO_BOARD="{build.board}" {compiler.c.extra_flags} {build.extra_flags} {includes} "{source_file}" -o "{object_file}" + +## Compile c++ files +recipe.cpp.o.pattern="{compiler.path}{compiler.cpp.cmd}" {compiler.cpreprocessor.flags} {compiler.cpp.flags} -DF_CPU={build.f_cpu} {build.lwip_flags} {build.debug_port} {build.debug_level} -DARDUINO={runtime.ide.version} -DARDUINO_{build.board} -DARDUINO_ARCH_{build.arch} -DARDUINO_BOARD="{build.board}" {compiler.cpp.extra_flags} {build.extra_flags} {includes} "{source_file}" -o "{object_file}" + +## Compile S files +recipe.S.o.pattern="{compiler.path}{compiler.c.cmd}" {compiler.cpreprocessor.flags} {compiler.S.flags} -DF_CPU={build.f_cpu} {build.lwip_flags} {build.debug_port} {build.debug_level} -DARDUINO={runtime.ide.version} -DARDUINO_{build.board} -DARDUINO_ARCH_{build.arch} -DARDUINO_BOARD="{build.board}" {compiler.c.extra_flags} {build.extra_flags} {includes} "{source_file}" -o "{object_file}" + +## Create archives +recipe.ar.pattern="{compiler.path}{compiler.ar.cmd}" {compiler.ar.flags} {compiler.ar.extra_flags} "{build.path}/arduino.ar" "{object_file}" + +## Combine gc-sections, archives, and objects +recipe.c.combine.pattern="{compiler.path}{compiler.c.elf.cmd}" {compiler.c.elf.flags} {compiler.c.elf.extra_flags} -o "{build.path}/{build.project_name}.elf" -Wl,--start-group {object_files} "{build.path}/arduino.ar" {compiler.c.elf.libs} -Wl,--end-group "-L{build.path}" + +## Create eeprom +recipe.objcopy.eep.pattern= + +## Create hex +#recipe.objcopy.hex.pattern="{compiler.path}{compiler.elf2hex.cmd}" {compiler.elf2hex.flags} {compiler.elf2hex.extra_flags} "{build.path}/{build.project_name}.elf" "{build.path}/{build.project_name}.hex" + +recipe.objcopy.hex.pattern="{runtime.tools.esptool.path}/{compiler.esptool.cmd}" -eo "{runtime.platform.path}/bootloaders/eboot/eboot.elf" -bo "{build.path}/{build.project_name}.bin" -bm {build.flash_mode} -bf {build.flash_freq} -bz {build.flash_size} -bs .text -bp 4096 -ec -eo "{build.path}/{build.project_name}.elf" -bs .irom0.text -bs .text -bs .data -bs .rodata -bc -ec + +## Save hex +recipe.output.tmp_file={build.project_name}.bin +recipe.output.save_file={build.project_name}.{build.variant}.bin + +## Compute size +recipe.size.pattern="{compiler.path}{compiler.size.cmd}" -A "{build.path}/{build.project_name}.elf" +recipe.size.regex=^(?:\.irom0\.text|\.text|\.data|\.rodata|)\s+([0-9]+).* +recipe.size.regex.data=^(?:\.data|\.rodata|\.bss)\s+([0-9]+).* +#recipe.size.regex.eeprom=^(?:\.eeprom)\s+([0-9]+).* + +# ------------------------------ + +tools.esptool.cmd=esptool +tools.esptool.cmd.windows=esptool.exe +tools.esptool.path={runtime.tools.esptool.path} +tools.esptool.network_cmd=python +tools.esptool.network_cmd.windows=python.exe + +tools.esptool.upload.protocol=esp +tools.esptool.upload.params.verbose=-vv +tools.esptool.upload.params.quiet= +tools.esptool.upload.pattern="{path}/{cmd}" {upload.verbose} -cd {upload.resetmethod} -cb {upload.speed} -cp "{serial.port}" -ca 0x00000 -cf "{build.path}/{build.project_name}.bin" +tools.esptool.upload.network_pattern="{network_cmd}" "{runtime.platform.path}/tools/espota.py" -i "{serial.port}" -p "{network.port}" "--auth={network.password}" -f "{build.path}/{build.project_name}.bin" + +tools.mkspiffs.cmd=mkspiffs +tools.mkspiffs.cmd.windows=mkspiffs.exe +tools.mkspiffs.path={runtime.tools.mkspiffs.path} + +tools.espupload.cmd=python +tools.espupload.cmd.windows=python.exe +tools.espupload.path={runtime.platform.path}/tools + +tools.espupload.upload.protocol=espupload +tools.espupload.upload.params.verbose= +tools.espupload.upload.params.quiet= +tools.espupload.upload.pattern="{cmd}" "{path}/espupload.py" -f "{build.path}/{build.project_name}.bin" diff --git a/sonoff/README.md b/sonoff/README.md new file mode 100644 index 000000000..ba72cf54b --- /dev/null +++ b/sonoff/README.md @@ -0,0 +1,33 @@ +## Sonoff-MQTT-OTA-Arduino - TASMOTA NextGen +Provide ESP8266 based Sonoff by [iTead Studio](https://www.itead.cc/) and ElectroDragon IoT Relay with Serial, Web and MQTT control allowing 'Over the Air' or OTA firmware updates using Arduino IDE. + +Current version is **3.9.4** - See ```_releasenotes.ino``` for change information. + +- This version provides all (Sonoff) modules in one file and starts up with Sonoff Basic. +- Once uploaded select module using the configuration webpage or the commands ```Modules``` and ```Module```. +- After reboot select config menu again or use commands ```GPIOs``` and ```GPIO``` to change GPIO with desired sensor. +- Some features still need to be ironed out. + +Sonoff +See [Wiki](https://github.com/arendst/Sonoff-MQTT-OTA-Arduino/wiki) for more information.
+See [Community](https://groups.google.com/d/forum/sonoffusers) for forum and more user experience. + +Starting with version 2.0.0 the following devices are supported: +- [iTead Sonoff Basic](http://sonoff.itead.cc/en/products/sonoff/sonoff-basic) +- [iTead Sonoff RF](http://sonoff.itead.cc/en/products/sonoff/sonoff-rf) +- [iTead Sonoff SV](https://www.itead.cc/sonoff-sv.html) +Sonoff +- [iTead Sonoff TH10/TH16 with temperature sensor](http://sonoff.itead.cc/en/products/sonoff/sonoff-th) +- [iTead Sonoff Dual](http://sonoff.itead.cc/en/products/sonoff/sonoff-dual) +- [iTead Sonoff Pow](http://sonoff.itead.cc/en/products/sonoff/sonoff-pow) +- [iTead Sonoff 4CH](http://sonoff.itead.cc/en/products/sonoff/sonoff-4ch) +- [iTead S20 Smart Socket](http://sonoff.itead.cc/en/products/residential/s20-socket) +- [iTead Slampher](http://sonoff.itead.cc/en/products/residential/slampher-rf) +- [iTead Sonoff Touch](http://sonoff.itead.cc/en/products/residential/sonoff-touch) +- [iTead Sonoff Led](http://sonoff.itead.cc/en/products/appliances/sonoff-led) +- [iTead 1 Channel Switch 5V / 12V](https://www.itead.cc/smart-home/inching-self-locking-wifi-wireless-switch.html) +- [iTead Motor Clockwise/Anticlockwise](https://www.itead.cc/smart-home/motor-reversing-wifi-wireless-switch.html) +- [Electrodragon IoT Relay Board](http://www.electrodragon.com/product/wifi-iot-relay-board-based-esp8266/) + +Sonoff +Sonoff diff --git a/sonoff/_releasenotes.ino b/sonoff/_releasenotes.ino new file mode 100644 index 000000000..f81ae8d25 --- /dev/null +++ b/sonoff/_releasenotes.ino @@ -0,0 +1,514 @@ +/* 3.9.4 20170127 + * Fix Sonoff Dual Relay switching (#287) + * + * 3.9.3 20170127 + * Add confirmation before Restart via webpage + * Expand Domoticz Configuration webpage with Key, Switch and Sensor Index and + * add commands DomoticzSwitchIdx and DomoticzSensorIdx (#86) (#174) (#219) + * Fix default DHT11 sensor driver selection + * Fix LedPower status after button press (#279) + * Add command Sleep 0 - 250 mSec for optional light sleep mode to lower energy consumption (#272) + * (Expect overall button/key/switch misses and wrong values on Sonoff Pow) + * Add Hue brightness extension (#281) + * Fix Hue brightness and change to call by reference (#283) + * + * 3.9.2 20170124 + * Add confirmation before Reset Configuration via webpage (#244) + * Add WS2812 features (see Wiki commands) + * + * 3.9.1 20170124 + * Change PowerOnState function to only trigger when Power On (and not just restart) (#238) + * Move HLW interrupts back to RAM and make WS2812_DMA optional as it generates Exception on Pow (#264) + * Add charset=utf-8 to webpages (#266) + * Update Hue emulation (#268) + * Fix status module number + * Add support for domoticz Dimmer on Sonoff_Led and WS2812 + * Fix possible ESP8285 flash problem by updating Flash Chip Mode to DOUT during web upload + * + * 3.2.6a 20170120 + * Fix Sonoff Pow compile error (#255) + * Move HLW interrupts back to ROM (Needed for WS2812 DMA interrupts) + * Removed all IO config from user_config.h as this will be done by commands or webpage + * Removed MessageFormat and supports JSON only except POWER/LIGHT status + * Add command LedPower to control main led (#247) + * Add more FriendlyNames for Hue (#254) + * Add DMA support for WS2812 when using pin 3 while other pins work just as well in my case... + * Add HUE emulation for Alexa (#229) + * Add basic WS2812 support (#229) + * Fix Wemo when MQTT is disabled (#245) + * Revert ButtonTopic and change SwitchTopic1 - 4 to one SwitchTopic + * Rename MqttUnits to Units + * Add Mqtt command to enable/disable MQTT + * + * 3.2.2a 20170115 + * Add dynamic (Sonoff) Module, user GPIO and sensor selection (one size fits (almost) all) + * Add support for Sonoff LED + * Add Seriallog disable after 600 seconds for Sonoff Dual and 4 Channel + * Add ButtonTopic2 - 4, SwitchTopic1 - 4 and SwitchRetain + * + * 3.2.2 20170113 + * Fix PowerOnState 2 functionality after re-applying power (#230) + * + * 3.2.1 20170113 + * Fix some failed command decoding (#228) + * Removed passwords from status messages (#216) + * + * 3.2.0 20170111 + * Add I2C BH1750 sensor (#222) + * Sensor rewrite preparing for online selection + * + * 3.1.16 20170109 + * Fix Domoticz possible error condition + * Remove Wifi password from connection message (#216) + * Add Configure Other menu item to web page (#209) + * Add command FriendlyName, field Friendly Name and define FRIENDLY_NAME to be used by Alexa + * eliminating current use of MQTT_CLIENT_ID (#209) + * Add friendlyname to webpage replacing former hostname + * + * 3.1.15 20170108 + * Fix Domoticz send key regression with Toggle command + * + * 3.1.14 20170107 + * Add support for command TOGGLE (define MQTT_CMND_TOGGLE) when ButtonTopic is in use and not equal to Topic (#207) + * + * 3.1.13 20170107 + * Fix web console command input when SUB_PREFIX contains '/' (#152) + * Add command response to web command (#200) + * Add option to disable MQTT as define USE_MQTT in user_config.h (#200) + * + * 3.1.12 20170106 + * Add OTA retry to solve possible HTTP transient errors (#204) + * Fix MQTT host discovery + * + * 3.1.11 20170105 + * Add mDNS to advertise webserver as .local/ + * + * 3.1.10 20170105 + * Fix ButtonTopic when SUB_PREFIX = PUB_PREFIX + * Add workaround for possible MQTT queueing when SUB_PREFIX = PUB_PREFIX + * Add optional MQTT host discovery using define USE_DISCOVERY in user_config.h (#115) + * + * 3.1.9 20170104 + * Fix Power Blink start position (toggled) + * Change PulseTime increments: 1 .. 111 in 0.1 sec (max 11 seconds) and 112 .. 64900 in seconds (= 12 seconds until 18 hours) (#188) + * Add support for SUB_PREFIX = PUB_PREFIX (#190) + * + * 3.1.8 20170103 + * Add retain flag to LWT offline and only send "tele/sonoff/LWT Offline" (#179) + * Change retained LWT Online message to only send "tele/sonoff/LWT Online" + * + * 3.1.7 20161231 + * Add retained message LWT Online when sonoff makes MQTT connection (#179) + * + * 3.1.6 20161230 + * Add blinking using commands BlinkTime, BlinkCount and Power Blink|3|BlinkOff|4 (#165) + * + * 3.1.5 20161228 + * Fix serial space command exception (28) + * + * 3.1.4 20161227 + * Fix MQTT subscribe regression exception (3) (#162) + * Fix serial empty command exception (28) + * + * 3.1.3 20161225 + * Extent Domoticz configuration webpage with optional indices (#153) + * Fix multi relay legacy tele message from tele/sonoff/2/POWER to tele/sonoff/POWER2 + * Add support for iTead Motor Clockwise/Anticlockwise + * + * 3.1.2 20161224 + * Extent command PowerOnState with toggle at power on (option 2 is now option 3!) (#156) + * + * 3.1.1 20161223 + * Add support for Sonoff Touch and Sonoff 4CH (#40) + * Update DomoticzIdx and DomoticzKeyIdx with relay/key index (DomoticzIdx1/DomoticzKeyIdx1) + * Add command PowerOnState to control relay(s) at power on (#154) + * + * 3.1.0 20161221 + * Add Sonoff Pow measurement smoothing + * Fix serial command topic preamble error (#151) + * Fix 2.x to 3.x migration inconsistencies (#146) + * + * 3.0.9 20161218 + * Add Sonoff Pow voltage reading when relay is on but no load present (#123) + * + * 3.0.8 20161218 + * Add temperature conversion to Fahrenheit as option in user_config.h (TEMP_CONVERSION) (#145) + * + * 3.0.7 20161217 + * Add user_config_override.h to be used by user to override some defaults in user_config.h (#58) + * Fix Sonoff Pow low power (down to 4W) intermittent measurements (#123) + * + * 3.0.6 20161217 + * Fix MQTT_CLIENT_ID starting with % sign as in "%06X" (#142) + * Add auto power off after PulseTime * 0.1 Sec to relay 1 (#134) + * + * 3.0.5 20161215 + * Add more control over LED with command LedState options (#136, #143) + * LED_OFF (0), LED_POWER (1), LED_MQTTSUB (2), LED_POWER_MQTTSUB (3), LED_MQTTPUB (4), LED_POWER_MQTTPUB (5), LED_MQTT (6), LED_POWER_MQTT (7) + * Add option WIFI_RETRY (4) to command WifiConfig to allow connection retry to other AP without restart (#73) + * + * 3.0.4 20161211 + * Fix intermittent Domoticz update misses (#133) + * + * 3.0.3 20161210 + * Fix compiler warnings (#132) + * Remove redundant code + * Fix Domoticz pushbutton support + * + * 3.0.2 20161209 + * Add pushbutton to SwitchMode (#130) + * + * 3.0.1 20161209 + * Fix initial config + * + * 3.0.0 20161208 + * Migrate and clean-up flash layout + * Settings from version 2.x are saved but settings from version 3.x can not be used with version 2.x + * Change SEND_TELEMETRY_RSSI to SEND_TELEMETRY_WIFI and add AP and SSID to telemetry + * Split long JSON messages + * Fix inconsistent status messages + * Fix all status messages to return JSON if enabled + * Remove relay index in cmnd/sonoff//POWER now changed + * to cmnd/sonoff/POWER for single relay units + * and cmnd/sonoff/POWER for multi relay units like Sonoff dual + * Add retain option to Power/Light status controlled by command PowerRetain On|Off (#126) + * + * 2.1.2 20161204 + * Add support for second wifi AP (#73) + * Update command WifiConfig + * Fix possible WifiManager hang + * + * 2.1.1a 20161203 + * Fix scan for wifi networks if WeMo is enabled + * Fix syslog setting using web page + * + * 2.1.1 20161202 + * Add support for ElectroDragon second relay and button (only toggle with optional ButtonTopic) (#110) + * + * 2.1.0 20161202 + * Add optional EXPERIMENTAL TLS to MQTT (#49) + * Fix MQTT payload handling (#111) + * Optimzed WeMo code + * + * 2.0.21a 20161201 + * Fix WeMo PowerPlug emulation + * + * 2.0.21 20161130 + * Add Belkin WeMo PowerPlug emulation enabled with USE_WEMO_EMULATION in user_config.h (Heiko Krupp) (#105, #109) + * + * 2.0.20 20161130 + * Relax MQTTClient naming but only allows hexadecimal uppercase numbers (#107) + * Add I2C support with command I2CScan + * Add I2C sensor driver for HTU21 as alternate sensor using TH10/16 connectors (Heiko Krupp) (#105) + * Add I2C sensor driver for BMP085/BMP180/BMP280/BME280 as alternate sensor using TH10/16 connectors + * + * 2.0.19a 20161127 + * Add support for ButtonTopic and ButtonRetain to wall switch function + * Add pullup to SWITCH_PIN and command SwitchMode to syntax + * + * 2.0.18 20161126 + * Add SUB_PREFIX multi level support allowing 'cmnd' or 'cmnd/level2/level3' + * Add wall switch function to GPIO14 and command SwitchMode (Alex Scott) (#103) + * + * 2.0.17 20161123 + * Calibrate HLWPCAL from 12345 to 12530 + * Add alternative sensor driver DHT2 using Adafruit DHT library + * Add define MESSAGE_FORMAT to user_config.h + * Throttle console messages + * Shorten JSON messages + * Fix possible Panic + * Fix User mode webserver security + * + * 2.0.16 20161118 + * Add alternative sensor driver DS18x20 using OneWire library (#95) + * Change sensor MQTT message from tele/sonoff/TEMPERATURE to tele/sonoff/DHT/TEMPERATURE or + * tele/sonoff/DS18B20/TEMPERATURE or tele/sonoff/DS18x20/1/TEMPERATURE + * Add sensors to root webpage and auto refresh every 4 seconds (#92) + * Add optional JSON messageformat to all telemetry data + * Enforce minimum TelePeriod to be 10 seconds + * Fix Energy Yesterday reset after restart + * Add Energy Today restore after controlled restart + * + * 2.0.15 20161116 + * Change TODAY_POWER and PERIOD_POWER to TODAY_ENERGY and PERIOD_ENERGY + * Fix serial regression + * Fix syslog hangs when loghost is unavailable + * + * 2.0.14 20161115 + * Add HLW threshold delay + * Fix HLW intermittent current deviation + * Fix button functionality during wificonfig + * Add CRC check to DS18B20 sensor (#88) + * + * 2.0.13 20161113 + * Add additional upload error code descriptions + * Add PlatformIO support (#80) + * + * 2.0.12 20161113 + * Fix Serial and Web response regression when no MQTT connection available + * Fix Sonoff Dual power telemetric data for second relay + * Removed MQTT password from Information web page + * Hide MQTT password from Configure MQTT web page + * + * 2.0.11 20161111 + * Rewrite button and web toggle code + * Fix NTP sync + * Add HLW calibration commands HLWPCAL, HLWUCAL and HLWICAL (need define USE_POWERCALIBRATION) + * Fix power threshold tests + * + * 2.0.10 20161109 + * Add additional Domoticz define (#63) + * Add defines MQTT_STATUS_ON and MQTT_STATUS_OFF in user_config.h to select status On/Off string + * Fix status response differences (#65) + * Fix divide by zero exception (#70) + * Fix syslog loop exception + * + * 2.0.9 20161108 + * clarify MODULE in user_config.h + * Fix hlw false values + * + * 2.0.8 20161108 + * Add initial status after power on + * Seperate driver files + * Fix hlw code and calibrate Pow + * Move user config defines to user_config.h (#61) + * + * 2.0.7 20161030 + * Make Ticker mandatory + * Add Domoticz support (Increase MQTT_MAX_PACKET_SIZE to 400) (#54) + * Add command MessageFormat 0|1 to select either legacy or JSON output + * + * 2.0.6 20161024 + * Add Sonoff Pow power factor + * Initial support for up to four relays using iTEAD PSB (4Channel) + * - Currently only supports one button (All buttons behave the same) + * - Use command MODEL 4 to select four relay option + * (After first power on it will support 2 relays like Sonoff Dual) + * Fix ledstate + * Add command Status 9 to display Sonoff Pow thresholds + * Add commands PowerLow, PowerHigh, VoltageLow, VoltageHigh, CurrentLow and CurrentHigh for use + * with Sonoff Pow thresholds + * + * 2.0.5 20161018 + * Add updates to user_config.h - moved SEND_TELEMETRY_DS18B20 and SEND_TELEMETRY_DHT to module area. + * As Sonoff TH10/16 does not have the logic installed for GPIO04 You'll have to select ONE of both + * Add Sonoff Pow support (experimental until Pow tested) + * Add command Status 8 to display Sonoff Pow energy values + * Add command MqttUnits On|Off to add units to values + * Change web main page header character size + * Change On/Off to ON/OFF status messages to satisfy openHAB + * Change TEMP to TEMPERATURE and HUM to HUMIDITY + * + * 2.0.4 20161009 + * Add MQTT_BUTTON_RETAIN, SAVE_DATA and SAVE_STATE defines to user_config.h (#35) + * Update ButtonRetain to remove retained message(s) from broker when turned off + * Add Retain for second relay on Sonoff Dual + * Provide power status messages with device topic index if requested + * + * 2.0.3 20161008 + * Update wifi initialization + * Add command BUTTONRETAIN for optional MQTT retain on button press (#35) + * Add command SAVESTATE to disable power state save. May be used with MQTT retain + * + * 2.0.2 20161006 + * Fix wifi issue 2186 + * + * 2.0.1 20161002 + * Fix button press + * + * 2.0.0 20161002 + * Update Sonoff TH10/16 sensor pins (My TH10 only has GPIO14 connected) + * Add full support for Sonoff dual + * + * 1.0.35 20160929 + * Add more lines to console + * Add timeout and disable MQTT on web upload + * Add command SAVEDATA to control parameter save (for flash wear afficionados) (#30) + * + * 1.0.34 20160926 + * Fix button press six and seven + * Add more information to webserver + * + * 1.0.33 20160915 + * Better WPS error message + * Separate webserver code from support.ino into webserver.ino + * Fix webserver User by removing unwanted restart option + * + * 1.0.32 20160913 + * Add Wifi Protected Setup (WPS) as third option for initial config + * Add command WIFICONFIG replacing deprecated command SMARTCONFIG + * Add option WIFICONFIG 3 to start WPSconfig + * Add option WIFICONFIG 0 to start saved Wifi config tool (WPSconfig, Smartconfig or Wifimanager) + * Change button behaviour - See Wiki + * + * 1.0.31 20160907 + * Fix DS18B20 misread if teleperiod = 2 + * Tuned sensor code + * Updated prefered ElectroDragon connection to Relay 1 and Button 1 + * Moved SONOFF and ELECTRO_DRAGON port config to user_config.h + * + * 1.0.30 20160902 + * Fix command TELEPERIOD 0 + * Add ESP- tag to UDP log message for easy rsyslogd filtering + * Add ElectroDragon (Relay 2 only) functionality. Select with #define MODULE ELECTRO_DRAGON + * Add ? as null message alternative + * Add DHT temperature and humidity telemetry support. Enable with #define SEND_TELEMETRY_DHT + * Add DS18B20 temperature telemetry support. Enable with #define SEND_TELEMETRY_DS18B20 + * Restrict HOSTNAME, MQTTCLIENT, TOPIC and BUTTONTOPIC in topic mode only + * + * 1.0.29 20160831 + * Allow UPGRADE, OTAURL, RESTART, RESET, MQTTHOST, MQTTPORT, MQTTUSER, MQTTPASSWORD and WEBSERVER also in group mode + * + * 1.0.28 20160831 + * Add webserver state to status 5 + * Add optional PUB_PREFIX2 (tele) for telemetry usage + * Add command TELEPERIOD + * Fix syntax message + * Change memory status display + * + * 1.0.27 20160831 + * Add sketch flash size + * Add console to webserver + * Add command weblog + * Change WifiManager web pages to minimal + * Change display default hostname and MQTT client id in webserver + * Change HTTP command interface to http://sonoff-1234/cm?cmnd=light 2 + * Change HEARTBEAT to UPTIME + * + * 1.0.26 20160829 + * Add define USE_WEBSERVER to disable web server code in source + * Add file upload as alternative for ota upload to webserver + * Add information to webserver + * Add command hostname + * Add command logport + * Change HTTP command interface to http://sonoff-1234/cmd?cmnd=light 2 + * Change button behaviour with regards to Smartconfig and OTA upload. See README.md + * Enforce default hostname to either "%s-%04d" or user defined without any % + * Enforce default mqtt client id to either "DVES_%06X" or user defined without any % + * + * 1.0.25 20160822 + * Remove config system halts to keep ota available + * + * 1.0.24 20160821 + * Add test for MQTT_SUBTOPIC + * Change log range to LOG_LEVEL_ALL + * Change MQTT introduction messages + * Moved MQTT_MAX_PACKET_SIZE warning message to introduction messages + * + * 1.0.23 20160821 + * Add option USE_SPIFFS to move config from flash to spiffs + * Add webserver with options 0 (off), 1 (user) and 2 (admin) + * Add HTTP command interface (http://sonoff-1234/c?cmnd=light 2) + * Add wifimanager countdown counter + * Add command line webpage + * Add relay control to wifimanager + * Add restart option 99 to force restart + * Fix wifi hostname + * Fix NETBIOS hostname problem by reducing default hostname length + * Fix possible exception if WIFI_HOSTNAME is changed + * Fix upgrade messages + * Reduce memory use by redesigning config routines + * Split syntax message + * Rename define SERIAL_IO to USE_SERIAL + * + * 1.0.22 20160814 + * Add all MQTT parameters for configuration + * Add wifimanager to configure Wifi and MQTT via web server + * Change NTP time handling + * Fix Smartconfig parameter buffer overflow + * Fix PlatformIO warnings + * + * 1.0.21 20160808 + * Remove semaphore as subscription flooding (more than 15 subscriptions per second) is managed by SDK (LmacRxBlk:1) + * Add optional RTC interrupt (define USE_TICKER) to keep RTC synced during subscription flooding + * Remove heartbeatflag + * + * 1.0.20 20160805 + * Add semaphore to handle out of memory when too many subscriptions requested + * Use Daylight Saving (DST) parameters from user_config.h when timezone = 99 + * Add status 7 option displaying RTC information + * Add ledstate to status 0 + * + * 1.0.19 20160803 + * Fix possible MQTT_CLIENT_ID induced Exception(28) + * + * 1.0.18 20160803 + * Moved Cfg_Default + * Fix negative data handling + * Remove MQTT information from status 1 and add labels to status 1 + * Add mac address to status 5 + * Add MQTT ClientId, UserId and Password to status 6 + * + * 1.0.17 20160731 + * Better variable range checking + * Change ambiguous connection messages + * Add timestamp to serial message + * + * 1.0.16 20160729 + * Moved wifi, rtc, syslog and config to support.ino + * Fixed button action when buttontopic is used. Introduced with 1.0.15 + * Better buffer overflow checks (strlcpy) + * + * 1.0.15 20160728 + * Removed pubsubclient config changes from sonoff.ino as it doesn't work + * reapply MQTT_MAX_PACKET_SIZE 256 and MQTT_KEEPALIVE 120 to PubSubClient.h + * Add status 0 option displaying all status messages + * Change MQTT_MAX_PACKET_SIZE from 1024 to 256 + * Add buffer overflow checks (snprintf and strncpy) + * Implemented common string sizes + * + * 1.0.14 20160722 + * Seperate user config from sonoff.ino to user_config.h (pucebaboon) + * Change defaults from sidnas2 to domus1 + * Add MQTT status message as status 6 (pucebaboon) + * Add status type to message (pucebaboon) + * Add pubsubclient config changes to sonoff.ino (pucebaboon) + * + * 1.0.13 20160702 + * Add Ledstate 1 option to show power state on led + * + * 1.0.12 20160529 + * Allow disable of button topic using "0" + * + * 1.0.11 20160524 + * Provide button response if MQTT connection lost + * + * 1.0.10 20160520 + * Add optional button topic to assist external MQTT clients + * Change version notation + * Reset default values + * + * 1.0.9 20160503 + * Add more blinks + * Add reset 2 option erasing flash + * Add status 5 option displaying network info + * Add syslog check for Wifi connection + * Resize mqtt_publish log array + * Change Wifi smartconfig active from 100 to 60 seconds + * Update Wifi initialization + * + * 1.0.8 20160430 + * Remove use of Wifi config data from SDK + * Add status 3 (syslog info) and status 4 (flash info) + * Add restart option to button (5 quick presses) + * + * 1.0.7 20160420 + * Add UDP syslog support + * Change HOST command to MQTTHOST command + * Add commands SYSLOG, SERIALLOG and LOGHOST + * Change hostname to lower case to distinguise between open-sdk version + * Add support for ESP-12F used in my modified wkaku power socket switch + * Fix timezone command + * Add RTC month names for future use + * Modify button code + * Remove initialization errors by better use of MQTT loop + * + * 1.0.6 20160406 + * Removed Wifi AP mode (#1) + * Add test for Arduino IDE version >= 1.6.8 + * Fix RTC time sync code + * + * 1.0.5 20160310 + * Initial public release + * Show debug info by selecting option from IDE Tools Debug port: Serial + */ diff --git a/sonoff/sonoff.ino b/sonoff/sonoff.ino new file mode 100644 index 000000000..3dc393b4b --- /dev/null +++ b/sonoff/sonoff.ino @@ -0,0 +1,2760 @@ +/* + * Sonoff and ElectroDragon by Theo Arends + * + * ==================================================== + * Prerequisites: + * - Change libraries/PubSubClient/src/PubSubClient.h + * #define MQTT_MAX_PACKET_SIZE 400 + * + * - Select IDE Tools - Flash size: "1M (64K SPIFFS)" + * ==================================================== +*/ + +#define VERSION 0x03090400 // 3.9.4 + +enum log_t {LOG_LEVEL_NONE, LOG_LEVEL_ERROR, LOG_LEVEL_INFO, LOG_LEVEL_DEBUG, LOG_LEVEL_DEBUG_MORE, LOG_LEVEL_ALL}; +enum week_t {Last, First, Second, Third, Fourth}; +enum dow_t {Sun=1, Mon, Tue, Wed, Thu, Fri, Sat}; +enum month_t {Jan=1, Feb, Mar, Apr, May, Jun, Jul, Aug, Sep, Oct, Nov, Dec}; +enum wifi_t {WIFI_RESTART, WIFI_SMARTCONFIG, WIFI_MANAGER, WIFI_WPSCONFIG, WIFI_RETRY, MAX_WIFI_OPTION}; +enum swtch_t {TOGGLE, FOLLOW, FOLLOW_INV, PUSHBUTTON, PUSHBUTTON_INV, MAX_SWITCH_OPTION}; +enum led_t {LED_OFF, LED_POWER, LED_MQTTSUB, LED_POWER_MQTTSUB, LED_MQTTPUB, LED_POWER_MQTTPUB, LED_MQTT, LED_POWER_MQTT, MAX_LED_OPTION}; + +#include "sonoff_template.h" + +#include "user_config.h" +#include "user_config_override.h" + +/*********************************************************************************************\ + * Enable feature by removing leading // or disable feature by adding leading // +\*********************************************************************************************/ + +//#define USE_SPIFFS // Switch persistent configuration from flash to spiffs (+24k code, +0.6k mem) + +/*********************************************************************************************\ + * Not released yet +\*********************************************************************************************/ + +#define FEATURE_POWER_LIMIT + +/*********************************************************************************************\ + * No user configurable items below +\*********************************************************************************************/ + +#define MODULE SONOFF_BASIC // [Module] Select default model + +#ifndef SWITCH_MODE +#define SWITCH_MODE TOGGLE // TOGGLE, FOLLOW or FOLLOW_INV (the wall switch state) +#endif + +#ifndef MQTT_FINGERPRINT +#define MQTT_FINGERPRINT "A5 02 FF 13 99 9F 8B 39 8E F1 83 4F 11 23 65 0B 32 36 FC 07" +#endif + +#ifndef USE_DHT2 +#define USE_DHT // Default DHT11 sensor needs no external library +#endif + +#ifndef USE_DS18x20 +#define USE_DS18B20 // Default DS18B20 sensor needs no external library +#endif + +#ifndef WS2812_LEDS +#define WS2812_LEDS 30 // [Pixels] Number of LEDs +#endif + +#define DEF_WIFI_HOSTNAME "%s-%04d" // Expands to - + +#define HLW_PREF_PULSE 12530 // was 4975us = 201Hz = 1000W +#define HLW_UREF_PULSE 1950 // was 1666us = 600Hz = 220V +#define HLW_IREF_PULSE 3500 // was 1666us = 600Hz = 4.545A + +#define VALUE_UNITS 0 // Default do not show value units (Hr, Sec, V, A, W etc.) +#define MQTT_SUBTOPIC "POWER" // Default MQTT subtopic (POWER or LIGHT) +#define MQTT_RETRY_SECS 10 // Seconds to retry MQTT connection +#define APP_POWER 0 // Default saved power state Off +#define MAX_DEVICE 1 // Max number of devices +#define WS2812_MAX_LEDS 256 // Max number of LEDs + +#define MAX_POWER_HOLD 10 // Time in SECONDS to allow max agreed power (Pow) +#define MAX_POWER_WINDOW 30 // Time in SECONDS to disable allow max agreed power (Pow) +#define SAFE_POWER_HOLD 10 // Time in SECONDS to allow max unit safe power (Pow) +#define SAFE_POWER_WINDOW 30 // Time in MINUTES to disable allow max unit safe power (Pow) +#define MAX_POWER_RETRY 5 // Retry count allowing agreed power limit overflow (Pow) + +#define STATES 10 // loops per second +#define SYSLOG_TIMER 600 // Seconds to restore syslog_level +#define SERIALLOG_TIMER 600 // Seconds to disable SerialLog +#define OTA_ATTEMPTS 5 // Number of times to try fetching the new firmware + +#define INPUT_BUFFER_SIZE 100 // Max number of characters in serial buffer +#define TOPSZ 60 // Max number of characters in topic string +#define MESSZ 240 // Max number of characters in JSON message string +#define LOGSZ 128 // Max number of characters in log string +#ifdef USE_MQTT_TLS + #define MAX_LOG_LINES 10 // Max number of lines in weblog +#else + #define MAX_LOG_LINES 70 // Max number of lines in weblog +#endif + +#define APP_BAUDRATE 115200 // Default serial baudrate +#define MAX_STATUS 9 + +enum butt_t {PRESSED, NOT_PRESSED}; + +#include "support.h" // Global support +#include // RTC +#include // MQTT, Ota, WifiManager +#include // MQTT, Ota +#include // Ota +#include // MQTT +#ifdef USE_WEBSERVER + #include // WifiManager, Webserver + #include // WifiManager +#endif // USE_WEBSERVER +#ifdef USE_DISCOVERY + #include // MQTT, Webserver +#endif // USE_DISCOVERY +#ifdef USE_SPIFFS + #include // Config +#endif // USE_SPIFFS +#ifdef USE_I2C + #include // I2C support library +#endif // USE_I2C + +typedef void (*rtcCallback)(); + +extern "C" uint32_t _SPIFFS_start; +extern "C" uint32_t _SPIFFS_end; + +#define MAX_BUTTON_COMMANDS 5 // Max number of button commands supported +const char commands[MAX_BUTTON_COMMANDS][14] PROGMEM = { + {"wificonfig 1"}, // Press button three times + {"wificonfig 2"}, // Press button four times + {"wificonfig 3"}, // Press button five times + {"restart 1"}, // Press button six times + {"upgrade 1"}}; // Press button seven times + +const char wificfg[5][12] PROGMEM = { "Restart", "Smartconfig", "Wifimanager", "WPSconfig", "Retry" }; + +struct SYSCFG2 { // Version 2.x (old) + unsigned long cfg_holder; + unsigned long saveFlag; + unsigned long version; + byte seriallog_level; + byte syslog_level; + char syslog_host[32]; + char sta_ssid1[32]; + char sta_pwd1[64]; + char otaUrl[80]; + char mqtt_host[32]; + char mqtt_grptopic[32]; + char mqtt_topic[32]; + char mqtt_topic2[32]; + char mqtt_subtopic[32]; + int8_t timezone; + uint8_t power; + uint8_t ledstate; + uint16_t mqtt_port; + char mqtt_client[33]; + char mqtt_user[33]; + char mqtt_pwd[33]; + uint8_t webserver; + unsigned long bootcount; + char hostname[33]; + uint16_t syslog_port; + byte weblog_level; + uint16_t tele_period; + uint8_t sta_config; + int16_t savedata; + byte model; + byte mqtt_retain; + byte savestate; + unsigned long hlw_pcal; + unsigned long hlw_ucal; + unsigned long hlw_ical; + unsigned long hlw_kWhyesterday; + byte value_units; + uint16_t hlw_pmin; + uint16_t hlw_pmax; + uint16_t hlw_umin; + uint16_t hlw_umax; + uint16_t hlw_imin; + uint16_t hlw_imax; + uint16_t hlw_mpl; // MaxPowerLimit + uint16_t hlw_mplh; // MaxPowerLimitHold + uint16_t hlw_mplw; // MaxPowerLimitWindow + uint16_t hlw_mspl; // MaxSafePowerLimit + uint16_t hlw_msplh; // MaxSafePowerLimitHold + uint16_t hlw_msplw; // MaxSafePowerLimitWindow + uint16_t hlw_mkwh; // MaxEnergy + uint16_t hlw_mkwhs; // MaxEnergyStart + char domoticz_in_topic[33]; + char domoticz_out_topic[33]; + uint16_t domoticz_update_timer; + unsigned long domoticz_relay_idx[4]; + unsigned long domoticz_key_idx[4]; + byte message_format; // Not used since 3.2.6a + unsigned long hlw_kWhtoday; + uint16_t hlw_kWhdoy; + uint8_t switchmode; + char mqtt_fingerprint[60]; + byte sta_active; + char sta_ssid2[33]; + char sta_pwd2[65]; + +} sysCfg2; + +struct SYSCFG { + unsigned long cfg_holder; + unsigned long saveFlag; + unsigned long version; + unsigned long bootcount; + byte migflg; + int16_t savedata; + byte savestate; + byte model; + int8_t timezone; + char otaUrl[101]; + char ex_friendlyname[33]; // Not used since 3.2.5 - see below + + byte serial_enable; + byte seriallog_level; + uint8_t sta_config; + byte sta_active; + char sta_ssid[2][33]; + char sta_pwd[2][65]; + char hostname[33]; + char syslog_host[33]; + uint16_t syslog_port; + byte syslog_level; + uint8_t webserver; + byte weblog_level; + + char mqtt_fingerprint[60]; + char mqtt_host[33]; + uint16_t mqtt_port; + char mqtt_client[33]; + char mqtt_user[33]; + char mqtt_pwd[33]; + char mqtt_topic[33]; + char button_topic[33]; + char mqtt_grptopic[33]; + char mqtt_subtopic[33]; + byte mqtt_button_retain; + byte mqtt_power_retain; + byte value_units; + byte message_format; // Not used since 3.2.6a + uint16_t tele_period; + + uint8_t power; + uint8_t ledstate; + uint8_t switchmode; + + char domoticz_in_topic[33]; + char domoticz_out_topic[33]; + uint16_t domoticz_update_timer; + unsigned long domoticz_relay_idx[4]; + unsigned long domoticz_key_idx[4]; + + unsigned long hlw_pcal; + unsigned long hlw_ucal; + unsigned long hlw_ical; + unsigned long hlw_kWhtoday; + unsigned long hlw_kWhyesterday; + uint16_t hlw_kWhdoy; + uint16_t hlw_pmin; + uint16_t hlw_pmax; + uint16_t hlw_umin; + uint16_t hlw_umax; + uint16_t hlw_imin; + uint16_t hlw_imax; + uint16_t hlw_mpl; // MaxPowerLimit + uint16_t hlw_mplh; // MaxPowerLimitHold + uint16_t hlw_mplw; // MaxPowerLimitWindow + uint16_t hlw_mspl; // MaxSafePowerLimit + uint16_t hlw_msplh; // MaxSafePowerLimitHold + uint16_t hlw_msplw; // MaxSafePowerLimitWindow + uint16_t hlw_mkwh; // MaxEnergy + uint16_t hlw_mkwhs; // MaxEnergyStart + + uint16_t pulsetime; + uint8_t poweronstate; + uint16_t blinktime; + uint16_t blinkcount; + + uint16_t ws_pixels; + uint8_t ws_red; + uint8_t ws_green; + uint8_t ws_blue; + uint8_t ws_ledtable; + uint8_t ws_dimmer; + uint8_t ws_fade; + uint8_t ws_speed; + uint8_t ws_scheme; + uint8_t ws_width; + uint16_t ws_wakeup; + + char friendlyname[4][33]; + char switch_topic[33]; + byte mqtt_switch_retain; + uint8_t mqtt_enabled; + uint8_t sleep; + + uint16_t domoticz_switch_idx[4]; + uint16_t domoticz_sensor_idx[12]; + + uint8_t module; + mytmplt my_module; + + uint16_t led_pixels; + uint8_t led_color[5]; + uint8_t led_table; + uint8_t led_dimmer[3]; + uint8_t led_fade; + uint8_t led_speed; + uint8_t led_scheme; + uint8_t led_width; + uint16_t led_wakeup; + +} sysCfg; + +struct TIME_T { + uint8_t Second; + uint8_t Minute; + uint8_t Hour; + uint8_t Wday; // day of week, sunday is day 1 + uint8_t Day; + uint8_t Month; + char MonthName[4]; + uint16_t DayOfYear; + uint16_t Year; + unsigned long Valid; +} rtcTime; + +struct TimeChangeRule +{ + uint8_t week; // 1=First, 2=Second, 3=Third, 4=Fourth, or 0=Last week of the month + uint8_t dow; // day of week, 1=Sun, 2=Mon, ... 7=Sat + uint8_t month; // 1=Jan, 2=Feb, ... 12=Dec + uint8_t hour; // 0-23 + int offset; // offset from UTC in minutes +}; + +TimeChangeRule myDST = { TIME_DST }; // Daylight Saving Time +TimeChangeRule mySTD = { TIME_STD }; // Standard Time + +int Baudrate = APP_BAUDRATE; // Serial interface baud rate +byte SerialInByte; // Received byte +int SerialInByteCounter = 0; // Index in receive buffer +char serialInBuf[INPUT_BUFFER_SIZE + 2]; // Receive buffer +byte Hexcode = 0; // Sonoff dual input flag +uint16_t ButtonCode = 0; // Sonoff dual received code +int16_t savedatacounter; // Counter and flag for config save to Flash or Spiffs +char Version[16]; // Version string from VERSION define +char Hostname[33]; // Composed Wifi hostname +char MQTTClient[33]; // Composed MQTT Clientname +uint8_t mqttcounter = 0; // MQTT connection retry counter +unsigned long timerxs = 0; // State loop timer +int state = 0; // State per second flag +int mqttflag = 2; // MQTT connection messages flag +int otaflag = 0; // OTA state flag +int otaok = 0; // OTA result +int restartflag = 0; // Sonoff restart flag +int wificheckflag = WIFI_RESTART; // Wifi state flag +int uptime = 0; // Current uptime in hours +int tele_period = 0; // Tele period timer +String Log[MAX_LOG_LINES]; // Web log buffer +byte logidx = 0; // Index in Web log buffer +byte Maxdevice = MAX_DEVICE; // Max number of devices supported +int status_update_timer = 0; // Refresh initial status +uint16_t pulse_timer = 0; // Power off timer +uint16_t blink_timer = 0; // Power cycle timer +uint16_t blink_counter = 0; // Number of blink cycles +uint8_t blink_power; // Blink power state +uint8_t blink_mask = 0; // Blink relay active mask +uint8_t blink_powersave; // Blink start power save state +uint16_t mqtt_cmnd_publish = 0; // ignore flag for publish command + +#ifdef USE_MQTT_TLS + WiFiClientSecure espClient; // Wifi Secure Client +#else + WiFiClient espClient; // Wifi Client +#endif +PubSubClient mqttClient(espClient); // MQTT Client +WiFiUDP portUDP; // UDP Syslog and Alexa + +uint8_t power; // Current copy of sysCfg.power +byte syslog_level; // Current copy of sysCfg.syslog_level +uint16_t syslog_timer = 0; // Timer to re-enable syslog_level +byte seriallog_level; // Current copy of sysCfg.seriallog_level +uint16_t seriallog_timer = 0; // Timer to disable Seriallog + +int blinks = 201; // Number of LED blinks +uint8_t blinkstate = 0; // LED state + +uint8_t lastbutton[4] = { NOT_PRESSED, NOT_PRESSED, NOT_PRESSED, NOT_PRESSED }; // Last button states +uint8_t holdcount = 0; // Timer recording button hold +uint8_t multiwindow = 0; // Max time between button presses to record press count +uint8_t multipress = 0; // Number of button presses within multiwindow +uint8_t lastwallswitch[4]; // Last wall switch states + +mytmplt my_module; // Active copy of GPIOs +uint8_t pin[GPIO_MAX]; // Possible pin configurations +uint8_t rel_inverted[4] = { 0 }; // Relay inverted flag (1 = (0 = On, 1 = Off)) +uint8_t led_inverted[4] = { 0 }; // LED inverted flag (1 = (0 = On, 1 = Off)) +uint8_t swt_flg = 0; // Any external switch configured +uint8_t dht_type = 0; // DHT type (DHT11, DHT21 or DHT22) +uint8_t hlw_flg = 0; // Power monitor configured +uint8_t i2c_flg = 0; // I2C configured + +boolean mDNSbegun = false; + +byte hlw_pminflg = 0; +byte hlw_pmaxflg = 0; +byte hlw_uminflg = 0; +byte hlw_umaxflg = 0; +byte hlw_iminflg = 0; +byte hlw_imaxflg = 0; +byte power_steady_cntr; +#ifdef FEATURE_POWER_LIMIT + byte hlw_mkwh_state = 0; + byte hlw_mplr_counter = 0; + uint16_t hlw_mplh_counter = 0; + uint16_t hlw_mplw_counter = 0; +#endif // FEATURE_POWER_LIMIT + +/********************************************************************************************/ + +void CFG_DefaultSet() +{ + memset(&sysCfg, 0x00, sizeof(SYSCFG)); + + sysCfg.cfg_holder = CFG_HOLDER; + sysCfg.saveFlag = 0; + sysCfg.version = VERSION; + sysCfg.bootcount = 0; + sysCfg.migflg = 0; + sysCfg.savedata = SAVE_DATA; + sysCfg.savestate = SAVE_STATE; + sysCfg.module = MODULE; + sysCfg.model = 0; + sysCfg.timezone = APP_TIMEZONE; + strlcpy(sysCfg.otaUrl, OTA_URL, sizeof(sysCfg.otaUrl)); + strlcpy(sysCfg.ex_friendlyname, FRIENDLY_NAME1, sizeof(sysCfg.ex_friendlyname)); + + sysCfg.seriallog_level = SERIAL_LOG_LEVEL; + sysCfg.sta_active = 0; + strlcpy(sysCfg.sta_ssid[0], STA_SSID1, sizeof(sysCfg.sta_ssid[0])); + strlcpy(sysCfg.sta_pwd[0], STA_PASS1, sizeof(sysCfg.sta_pwd[0])); + strlcpy(sysCfg.sta_ssid[1], STA_SSID2, sizeof(sysCfg.sta_ssid[1])); + strlcpy(sysCfg.sta_pwd[1], STA_PASS2, sizeof(sysCfg.sta_pwd[1])); + strlcpy(sysCfg.hostname, WIFI_HOSTNAME, sizeof(sysCfg.hostname)); + sysCfg.sta_config = WIFI_CONFIG_TOOL; + strlcpy(sysCfg.syslog_host, SYS_LOG_HOST, sizeof(sysCfg.syslog_host)); + sysCfg.syslog_port = SYS_LOG_PORT; + sysCfg.syslog_level = SYS_LOG_LEVEL; + sysCfg.webserver = WEB_SERVER; + sysCfg.weblog_level = WEB_LOG_LEVEL; + + strlcpy(sysCfg.mqtt_fingerprint, MQTT_FINGERPRINT, sizeof(sysCfg.mqtt_fingerprint)); + strlcpy(sysCfg.mqtt_host, MQTT_HOST, sizeof(sysCfg.mqtt_host)); + sysCfg.mqtt_port = MQTT_PORT; + strlcpy(sysCfg.mqtt_client, MQTT_CLIENT_ID, sizeof(sysCfg.mqtt_client)); + strlcpy(sysCfg.mqtt_user, MQTT_USER, sizeof(sysCfg.mqtt_user)); + strlcpy(sysCfg.mqtt_pwd, MQTT_PASS, sizeof(sysCfg.mqtt_pwd)); + strlcpy(sysCfg.mqtt_topic, MQTT_TOPIC, sizeof(sysCfg.mqtt_topic)); + strlcpy(sysCfg.button_topic, "0", sizeof(sysCfg.button_topic)); + strlcpy(sysCfg.mqtt_grptopic, MQTT_GRPTOPIC, sizeof(sysCfg.mqtt_grptopic)); + strlcpy(sysCfg.mqtt_subtopic, MQTT_SUBTOPIC, sizeof(sysCfg.mqtt_subtopic)); + sysCfg.mqtt_button_retain = MQTT_BUTTON_RETAIN; + sysCfg.mqtt_power_retain = MQTT_POWER_RETAIN; + sysCfg.value_units = VALUE_UNITS; + sysCfg.message_format = 0; + sysCfg.tele_period = TELE_PERIOD; + + sysCfg.power = APP_POWER; + sysCfg.poweronstate = APP_POWERON_STATE; + sysCfg.pulsetime = APP_PULSETIME; + sysCfg.ledstate = APP_LEDSTATE; + sysCfg.switchmode = SWITCH_MODE; + sysCfg.blinktime = APP_BLINKTIME; + sysCfg.blinkcount = APP_BLINKCOUNT; + sysCfg.sleep = APP_SLEEP; + + strlcpy(sysCfg.domoticz_in_topic, DOMOTICZ_IN_TOPIC, sizeof(sysCfg.domoticz_in_topic)); + strlcpy(sysCfg.domoticz_out_topic, DOMOTICZ_OUT_TOPIC, sizeof(sysCfg.domoticz_out_topic)); + sysCfg.domoticz_update_timer = DOMOTICZ_UPDATE_TIMER; + for (byte i = 0; i < 4; i++) { + sysCfg.domoticz_relay_idx[i] = 0; + sysCfg.domoticz_key_idx[i] = 0; + sysCfg.domoticz_switch_idx[i] = 0; + } + for (byte i = 0; i < 12; i++) sysCfg.domoticz_sensor_idx[i] = 0; + + sysCfg.hlw_pcal = HLW_PREF_PULSE; + sysCfg.hlw_ucal = HLW_UREF_PULSE; + sysCfg.hlw_ical = HLW_IREF_PULSE; + sysCfg.hlw_kWhtoday = 0; + sysCfg.hlw_kWhyesterday = 0; + sysCfg.hlw_kWhdoy = 0; + sysCfg.hlw_pmin = 0; + sysCfg.hlw_pmax = 0; + sysCfg.hlw_umin = 0; + sysCfg.hlw_umax = 0; + sysCfg.hlw_imin = 0; + sysCfg.hlw_imax = 0; + sysCfg.hlw_mpl = 0; // MaxPowerLimit + sysCfg.hlw_mplh = MAX_POWER_HOLD; + sysCfg.hlw_mplw = MAX_POWER_WINDOW; + sysCfg.hlw_mspl = 0; // MaxSafePowerLimit + sysCfg.hlw_msplh = SAFE_POWER_HOLD; + sysCfg.hlw_msplw = SAFE_POWER_WINDOW; + sysCfg.hlw_mkwh = 0; // MaxEnergy + sysCfg.hlw_mkwhs = 0; // MaxEnergyStart + + sysCfg.ws_pixels = WS2812_LEDS; + sysCfg.ws_red = 255; + sysCfg.ws_green = 0; + sysCfg.ws_blue = 0; + sysCfg.ws_ledtable = 0; + sysCfg.ws_dimmer = 8; + sysCfg.ws_fade = 0; + sysCfg.ws_speed = 1; + sysCfg.ws_scheme = 0; + sysCfg.ws_width = 1; + sysCfg.ws_wakeup = 0; + + strlcpy(sysCfg.friendlyname[0], FRIENDLY_NAME1, sizeof(sysCfg.friendlyname[0])); + strlcpy(sysCfg.friendlyname[1], FRIENDLY_NAME2, sizeof(sysCfg.friendlyname[1])); + strlcpy(sysCfg.friendlyname[2], FRIENDLY_NAME3, sizeof(sysCfg.friendlyname[2])); + strlcpy(sysCfg.friendlyname[3], FRIENDLY_NAME4, sizeof(sysCfg.friendlyname[3])); + + for (byte i = 0; i < MAX_GPIO_PIN; i++) sysCfg.my_module.gp.io[i] = 0; + + sysCfg.led_pixels = 0; + for (byte i = 0; i < 5; i++) sysCfg.led_color[i] = 255; + sysCfg.led_table = 0; + for (byte i = 0; i < 3; i++) sysCfg.led_dimmer[i] = 10; + sysCfg.led_fade = 0; + sysCfg.led_speed = 0; + sysCfg.led_scheme = 0; + sysCfg.led_width = 0; + sysCfg.led_wakeup = 0; + + strlcpy(sysCfg.switch_topic, "0", sizeof(sysCfg.switch_topic)); + sysCfg.mqtt_switch_retain = MQTT_SWITCH_RETAIN; + sysCfg.mqtt_enabled = MQTT_USE; + +} + +void CFG_Default() +{ + addLog_P(LOG_LEVEL_NONE, PSTR("Config: Use default configuration")); + CFG_DefaultSet(); + CFG_Save(); +} + +void CFG_Migrate_Part2() +{ + addLog_P(LOG_LEVEL_NONE, PSTR("Config: Migrating configuration")); + CFG_DefaultSet(); + + sysCfg.seriallog_level = sysCfg2.seriallog_level; + sysCfg.syslog_level = sysCfg2.syslog_level; + strlcpy(sysCfg.syslog_host, sysCfg2.syslog_host, sizeof(sysCfg.syslog_host)); + strlcpy(sysCfg.sta_ssid[0], sysCfg2.sta_ssid1, sizeof(sysCfg.sta_ssid[0])); + strlcpy(sysCfg.sta_pwd[0], sysCfg2.sta_pwd1, sizeof(sysCfg.sta_pwd[0])); + strlcpy(sysCfg.otaUrl, sysCfg2.otaUrl, sizeof(sysCfg.otaUrl)); + strlcpy(sysCfg.mqtt_host, sysCfg2.mqtt_host, sizeof(sysCfg.mqtt_host)); + strlcpy(sysCfg.mqtt_grptopic, sysCfg2.mqtt_grptopic, sizeof(sysCfg.mqtt_grptopic)); + strlcpy(sysCfg.mqtt_topic, sysCfg2.mqtt_topic, sizeof(sysCfg.mqtt_topic)); + strlcpy(sysCfg.button_topic, sysCfg2.mqtt_topic2, sizeof(sysCfg.button_topic)); + strlcpy(sysCfg.mqtt_subtopic, sysCfg2.mqtt_subtopic, sizeof(sysCfg.mqtt_subtopic)); + sysCfg.timezone = sysCfg2.timezone; + sysCfg.power = sysCfg2.power; + if (sysCfg2.version >= 0x01000D00) { // 1.0.13 + sysCfg.ledstate = sysCfg2.ledstate; + } + if (sysCfg2.version >= 0x01001600) { // 1.0.22 + sysCfg.mqtt_port = sysCfg2.mqtt_port; + strlcpy(sysCfg.mqtt_client, sysCfg2.mqtt_client, sizeof(sysCfg.mqtt_client)); + strlcpy(sysCfg.mqtt_user, sysCfg2.mqtt_user, sizeof(sysCfg.mqtt_user)); + strlcpy(sysCfg.mqtt_pwd, sysCfg2.mqtt_pwd, sizeof(sysCfg.mqtt_pwd)); + strlcpy(sysCfg.ex_friendlyname, sysCfg2.mqtt_client, sizeof(sysCfg.ex_friendlyname)); + } + if (sysCfg2.version >= 0x01001700) { // 1.0.23 + sysCfg.webserver = sysCfg2.webserver; + } + if (sysCfg2.version >= 0x01001A00) { // 1.0.26 + sysCfg.bootcount = sysCfg2.bootcount; + strlcpy(sysCfg.hostname, sysCfg2.hostname, sizeof(sysCfg.hostname)); + sysCfg.syslog_port = sysCfg2.syslog_port; + } + if (sysCfg2.version >= 0x01001B00) { // 1.0.27 + sysCfg.weblog_level = sysCfg2.weblog_level; + } + if (sysCfg2.version >= 0x01001C00) { // 1.0.28 + sysCfg.tele_period = sysCfg2.tele_period; + if ((sysCfg.tele_period > 0) && (sysCfg.tele_period < 10)) sysCfg.tele_period = 10; // Do not allow periods < 10 seconds + } + if (sysCfg2.version >= 0x01002000) { // 1.0.32 + sysCfg.sta_config = sysCfg2.sta_config; + } + if (sysCfg2.version >= 0x01002300) { // 1.0.35 + sysCfg.savedata = sysCfg2.savedata; + } + if (sysCfg2.version >= 0x02000000) { // 2.0.0 + sysCfg.model = sysCfg2.model; + } + if (sysCfg2.version >= 0x02000300) { // 2.0.3 + sysCfg.mqtt_button_retain = sysCfg2.mqtt_retain; + sysCfg.savestate = sysCfg2.savestate; + } + if (sysCfg2.version >= 0x02000500) { // 2.0.5 + sysCfg.hlw_pcal = sysCfg2.hlw_pcal; + sysCfg.hlw_ucal = sysCfg2.hlw_ucal; + sysCfg.hlw_ical = sysCfg2.hlw_ical; + sysCfg.hlw_kWhyesterday = sysCfg2.hlw_kWhyesterday; + sysCfg.value_units = sysCfg2.value_units; + } + if (sysCfg2.version >= 0x02000600) { // 2.0.6 + sysCfg.hlw_pmin = sysCfg2.hlw_pmin; + sysCfg.hlw_pmax = sysCfg2.hlw_pmax; + sysCfg.hlw_umin = sysCfg2.hlw_umin; + sysCfg.hlw_umax = sysCfg2.hlw_umax; + sysCfg.hlw_imin = sysCfg2.hlw_imin; + sysCfg.hlw_imax = sysCfg2.hlw_imax; + } + if (sysCfg2.version >= 0x02000700) { // 2.0.7 + sysCfg.message_format = 0; + strlcpy(sysCfg.domoticz_in_topic, sysCfg2.domoticz_in_topic, sizeof(sysCfg.domoticz_in_topic)); + strlcpy(sysCfg.domoticz_out_topic, sysCfg2.domoticz_out_topic, sizeof(sysCfg.domoticz_out_topic)); + sysCfg.domoticz_update_timer = sysCfg2.domoticz_update_timer; + for (byte i = 0; i < 4; i++) { + sysCfg.domoticz_relay_idx[i] = sysCfg2.domoticz_relay_idx[i]; + sysCfg.domoticz_key_idx[i] = sysCfg2.domoticz_key_idx[i]; + } + + sysCfg.hlw_mpl = sysCfg2.hlw_mpl; // MaxPowerLimit + sysCfg.hlw_mplh = sysCfg2.hlw_mplh; + sysCfg.hlw_mplw = sysCfg2.hlw_mplw; + sysCfg.hlw_mspl = sysCfg2.hlw_mspl; // MaxSafePowerLimit + sysCfg.hlw_msplh = sysCfg2.hlw_msplh; + sysCfg.hlw_msplw = sysCfg2.hlw_msplw; + sysCfg.hlw_mkwh = sysCfg2.hlw_mkwh; // MaxEnergy + sysCfg.hlw_mkwhs = sysCfg2.hlw_mkwhs; // MaxEnergyStart + } + if (sysCfg2.version >= 0x02001000) { // 2.0.16 + sysCfg.hlw_kWhtoday = sysCfg2.hlw_kWhtoday; + sysCfg.hlw_kWhdoy = sysCfg2.hlw_kWhdoy; + } + if (sysCfg2.version >= 0x02001200) { // 2.0.18 + sysCfg.switchmode = sysCfg2.switchmode; + } + if (sysCfg2.version >= 0x02010000) { // 2.1.0 + strlcpy(sysCfg.mqtt_fingerprint, sysCfg2.mqtt_fingerprint, sizeof(sysCfg.mqtt_fingerprint)); + } + if (sysCfg2.version >= 0x02010200) { // 2.1.2 + sysCfg.sta_active = sysCfg2.sta_active; + strlcpy(sysCfg.sta_ssid[1], sysCfg2.sta_ssid2, sizeof(sysCfg.sta_ssid[1])); + strlcpy(sysCfg.sta_pwd[1], sysCfg2.sta_pwd2, sizeof(sysCfg.sta_pwd[1])); + } + CFG_Save(); +} + +void CFG_Delta() +{ + if (sysCfg.version != VERSION) { // Fix version dependent changes + if (sysCfg.version < 0x03000600) { // 3.0.6 - Add parameter + sysCfg.pulsetime = APP_PULSETIME; + } + if (sysCfg.version < 0x03010100) { // 3.1.1 - Add parameter + sysCfg.poweronstate = APP_POWERON_STATE; + } + if (sysCfg.version < 0x03010200) { // 3.1.2 - Add parameter + if (sysCfg.poweronstate == 2) sysCfg.poweronstate = 3; + } + if (sysCfg.version < 0x03010600) { // 3.1.6 - Add parameter + sysCfg.blinktime = APP_BLINKTIME; + sysCfg.blinkcount = APP_BLINKCOUNT; + } + if (sysCfg.version < 0x03011000) { // 3.1.16 - Add parameter + getClient(sysCfg.ex_friendlyname, sysCfg.mqtt_client, sizeof(sysCfg.ex_friendlyname)); + } + if (sysCfg.version < 0x03020400) { // 3.2.4 - Add parameter + sysCfg.ws_pixels = WS2812_LEDS; + sysCfg.ws_red = 255; + sysCfg.ws_green = 0; + sysCfg.ws_blue = 0; + sysCfg.ws_ledtable = 0; + sysCfg.ws_dimmer = 8; + sysCfg.ws_fade = 0; + sysCfg.ws_speed = 1; + sysCfg.ws_scheme = 0; + sysCfg.ws_width = 1; + sysCfg.ws_wakeup = 0; + } + if (sysCfg.version < 0x03020500) { // 3.2.5 - Add parameter + strlcpy(sysCfg.friendlyname[0], sysCfg.ex_friendlyname, sizeof(sysCfg.friendlyname[0])); + strlcpy(sysCfg.friendlyname[1], FRIENDLY_NAME2, sizeof(sysCfg.friendlyname[1])); + strlcpy(sysCfg.friendlyname[2], FRIENDLY_NAME3, sizeof(sysCfg.friendlyname[2])); + strlcpy(sysCfg.friendlyname[3], FRIENDLY_NAME4, sizeof(sysCfg.friendlyname[3])); + } + if (sysCfg.version < 0x03020800) { // 3.2.8 - Add parameter + strlcpy(sysCfg.switch_topic, sysCfg.button_topic, sizeof(sysCfg.switch_topic)); + sysCfg.mqtt_switch_retain = MQTT_SWITCH_RETAIN; + sysCfg.mqtt_enabled = MQTT_USE; + } + if (sysCfg.version < 0x03020C00) { // 3.2.12 - Add parameter + sysCfg.sleep = APP_SLEEP; + } + if (sysCfg.version < 0x03090204) { // 3.9.2d - Add parameter + for (byte i = 0; i < 4; i++) sysCfg.domoticz_switch_idx[i] = 0; + for (byte i = 0; i < 12; i++) sysCfg.domoticz_sensor_idx[i] = 0; + + sysCfg.module = MODULE; + for (byte i = 0; i < MAX_GPIO_PIN; i++) sysCfg.my_module.gp.io[i] = 0; + + sysCfg.led_pixels = 0; + for (byte i = 0; i < 5; i++) sysCfg.led_color[i] = 255; + sysCfg.led_table = 0; + for (byte i = 0; i < 3; i++) sysCfg.led_dimmer[i] = 10; + sysCfg.led_fade = 0; + sysCfg.led_speed = 0; + sysCfg.led_scheme = 0; + sysCfg.led_width = 0; + sysCfg.led_wakeup = 0; + } + + sysCfg.version = VERSION; + } +} + +/********************************************************************************************/ + +void getClient(char* output, const char* input, byte size) +{ + char *token; + uint8_t digits = 0; + + if (strstr(input, "%")) { + strlcpy(output, input, size); + token = strtok(output, "%"); + if (strstr(input, "%") == input) { + output[0] = '\0'; + } else { + token = strtok(NULL, ""); + } + if (token != NULL) { + digits = atoi(token); + if (digits) { + snprintf_P(output, size, PSTR("%s%c0%dX"), output, '%', digits); + snprintf_P(output, size, output, ESP.getChipId()); + } + } + } + if (!digits) strlcpy(output, input, size); +} + +void setRelay(uint8_t power) +{ + if ((sysCfg.module == SONOFF_DUAL) || (sysCfg.module == CH4)) { + Serial.write(0xA0); + Serial.write(0x04); + Serial.write(power); + Serial.write(0xA1); + Serial.write('\n'); + Serial.flush(); + } else { + if (sysCfg.module == SONOFF_LED) { + sl_setColor(power &1); + } else { + for (byte i = 0; i < Maxdevice; i++) { + if (pin[GPIO_REL1 +i] < 99) digitalWrite(pin[GPIO_REL1 +i], power & 0x1); + power >>= 1; + } + } + } + power_steady_cntr = 2; +} + +void setLed(uint8_t state) +{ + if (state) state = 1; + digitalWrite(pin[GPIO_LED1], (led_inverted[0]) ? !state : state); +} + +void sl_setDim(uint8_t *my_color) +{ + float newDim, fmyCld, fmyWrm, fmyRed, fmyGrn, fmyBlu; + + newDim = 100 / (float)sysCfg.led_dimmer[0]; + fmyCld = (float)sysCfg.led_color[0] / newDim; + newDim = 100 / (float)sysCfg.led_dimmer[1]; + fmyWrm = (float)sysCfg.led_color[1] / newDim; + newDim = 100 / (float)sysCfg.led_dimmer[2]; + fmyRed = (float)sysCfg.led_color[2] / newDim; + fmyGrn = (float)sysCfg.led_color[3] / newDim; + fmyBlu = (float)sysCfg.led_color[4] / newDim; + my_color[0] = (uint8_t)fmyCld; + my_color[1] = (uint8_t)fmyWrm; + my_color[2] = (uint8_t)fmyRed; + my_color[3] = (uint8_t)fmyGrn; + my_color[4] = (uint8_t)fmyBlu; +} + +void sl_setColor(byte type) +{ +// 0 = Off +// 1 = On +// 2 = Dim cold +// 3 = Dim Warm +// 4 = Dim color + + uint8_t my_color[5]; + + sl_setDim(my_color); + if (type == 0) { + for (byte i = 0; i < 5; i++) { + if (pin[GPIO_PWM0 +i] < 99) analogWrite(pin[GPIO_PWM0 +i], 0); + } + } + else if (type == 1) { + for (byte i = 0; i < 5; i++) { + if (pin[GPIO_PWM0 +i] < 99) analogWrite(pin[GPIO_PWM0 +i], my_color[i]); + } + } + else if (type == 2) { // Cold + if (pin[GPIO_PWM0] < 99) analogWrite(pin[GPIO_PWM0], my_color[0]); + } + else if (type == 3) { // Warm + if (pin[GPIO_PWM1] < 99) analogWrite(pin[GPIO_PWM1], my_color[1]); + } + else if (type == 4) { // Color + for (byte i = 2; i < 5; i++) { + if (pin[GPIO_PWM0 +i] < 99) analogWrite(pin[GPIO_PWM0 +i], my_color[i]); + } + } +} + +void json2legacy(char* stopic, char* svalue) +{ + char *p, *token; + uint16_t i, j; + + if (!strstr(svalue, "{\"")) return; // No JSON + +// stopic = stat/sonoff/RESULT +// svalue = {"POWER2":"ON"} +// --> stopic = "stat/sonoff/POWER2", svalue = "ON" +// svalue = {"Upgrade":{"Version":"2.1.2", "OtaUrl":"%s"}} +// --> stopic = "stat/sonoff/UPGRADE", svalue = "2.1.2" +// svalue = {"SerialLog":2} +// --> stopic = "stat/sonoff/SERIALLOG", svalue = "2" +// svalue = {"POWER":""} +// --> stopic = "stat/sonoff/POWER", svalue = "" + + token = strtok(svalue, "{\""); // Topic + p = strrchr(stopic, '/') +1; + i = p - stopic; + for (j = 0; j < strlen(token)+1; j++) stopic[i+j] = toupper(token[j]); + token = strtok(NULL, "\""); // : or :3} or :3, or :{ + if (strstr(token, ":{")) { + token = strtok(NULL, "\""); // Subtopic + token = strtok(NULL, "\""); // : or :3} or :3, + } + if (strlen(token) > 1) { + token++; + p = strchr(token, ','); + if (!p) p = strchr(token, '}'); + i = p - token; + token[i] = '\0'; // Value + } else { + token = strtok(NULL, "\""); // Value or , or } + if ((token[0] == ',') || (token[0] == '}')) { // Empty parameter + token = NULL; + } + } + if (token == NULL) { + svalue[0] = '\0'; + } else { + memcpy(svalue, token, strlen(token)+1); + } +} + +uint32_t Atoh(char *s) +{ + uint32_t value = 0, digit; + int8_t c; + + while((c = *s++)){ + if('0' <= c && c <= '9') + digit = c - '0'; + else if('A' <= c && c <= 'F') + digit = c - 'A' + 10; + else if('a' <= c && c<= 'f') + digit = c - 'a' + 10; + else break; + value = (value << 4) | digit; + } + return value; +} + +/********************************************************************************************/ + +void mqtt_publish_sec(const char* topic, const char* data, boolean retained) +{ + char log[TOPSZ+MESSZ]; + + if (sysCfg.mqtt_enabled) { + if (mqttClient.publish(topic, data, retained)) { + snprintf_P(log, sizeof(log), PSTR("MQTT: %s = %s%s"), topic, data, (retained) ? " (retained)" : ""); +// mqttClient.loop(); // Do not use here! Will block previous publishes + } else { + snprintf_P(log, sizeof(log), PSTR("RSLT: %s = %s"), topic, data); + } + } else { + snprintf_P(log, sizeof(log), PSTR("RSLT: %s = %s"), strrchr(topic,'/')+1, data); + } + + addLog(LOG_LEVEL_INFO, log); + if (sysCfg.ledstate &0x04) blinks++; +} + +void mqtt_publish(const char* topic, const char* data, boolean retained) +{ + char *me; + + if (!strcmp(SUB_PREFIX,PUB_PREFIX)) { + me = strstr(topic,SUB_PREFIX); + if (me == topic) mqtt_cmnd_publish += 8; + } + mqtt_publish_sec(topic, data, retained); +} + +void mqtt_publish(const char* topic, const char* data) +{ + mqtt_publish(topic, data, false); +} + +void mqtt_publishPowerState(byte device) +{ + char stopic[TOPSZ], svalue[MESSZ], sdevice[10]; + + if ((device < 1) || (device > Maxdevice)) device = 1; + snprintf_P(sdevice, sizeof(sdevice), PSTR("%d"), device); + snprintf_P(stopic, sizeof(stopic), PSTR("%s/%s/RESULT"), PUB_PREFIX, sysCfg.mqtt_topic); + snprintf_P(svalue, sizeof(svalue), PSTR("{\"%s%s\":\"%s\"}"), + sysCfg.mqtt_subtopic, (Maxdevice > 1) ? sdevice : "", (power & (0x01 << (device -1))) ? MQTT_STATUS_ON : MQTT_STATUS_OFF); + mqtt_publish(stopic, svalue); + json2legacy(stopic, svalue); + mqtt_publish(stopic, svalue, sysCfg.mqtt_power_retain); +} + +void mqtt_publishPowerBlinkState(byte device) +{ + char stopic[TOPSZ], svalue[MESSZ], sdevice[10]; + + if ((device < 1) || (device > Maxdevice)) device = 1; + snprintf_P(sdevice, sizeof(sdevice), PSTR("%d"), device); + snprintf_P(stopic, sizeof(stopic), PSTR("%s/%s/RESULT"), PUB_PREFIX, sysCfg.mqtt_topic); + snprintf_P(svalue, sizeof(svalue), PSTR("{\"%s%s\":\"BLINK %s\"}"), + sysCfg.mqtt_subtopic, (Maxdevice > 1) ? sdevice : "", (blink_mask & (0x01 << (device -1))) ? MQTT_STATUS_ON : MQTT_STATUS_OFF); + mqtt_publish(stopic, svalue); +} + +void mqtt_connected() +{ + char stopic[TOPSZ], svalue[MESSZ]; + + if (sysCfg.mqtt_enabled) { + snprintf_P(stopic, sizeof(stopic), PSTR("%s/%s/#"), SUB_PREFIX, sysCfg.mqtt_topic); + mqttClient.subscribe(stopic); + mqttClient.loop(); // Solve LmacRxBlk:1 messages + snprintf_P(stopic, sizeof(stopic), PSTR("%s/%s/#"), SUB_PREFIX, sysCfg.mqtt_grptopic); + mqttClient.subscribe(stopic); + mqttClient.loop(); // Solve LmacRxBlk:1 messages + snprintf_P(stopic, sizeof(stopic), PSTR("%s/%s/#"), SUB_PREFIX, MQTTClient); // Fall back topic + mqttClient.subscribe(stopic); + mqttClient.loop(); // Solve LmacRxBlk:1 messages +#ifdef USE_DOMOTICZ + domoticz_mqttSubscribe(); +#endif // USE_DOMOTICZ + } + + if (mqttflag) { + snprintf_P(stopic, sizeof(stopic), PSTR("%s/%s/RESULT"), PUB_PREFIX2, sysCfg.mqtt_topic); + snprintf_P(svalue, sizeof(svalue), PSTR("{\"Info1\":{\"Module\":\"%s\", \"Version\":\"%s\", \"FallbackTopic\":\"%s\", \"GroupTopic\":\"%s\"}}"), + my_module.name, Version, MQTTClient, sysCfg.mqtt_grptopic); + mqtt_publish(stopic, svalue); +#ifdef USE_WEBSERVER + if (sysCfg.webserver) { + snprintf_P(svalue, sizeof(svalue), PSTR("{\"Info2\":{\"WebserverMode\":\"%s\", \"Hostname\":\"%s\", \"IPaddress\":\"%s\"}}"), + (sysCfg.webserver == 2) ? "Admin" : "User", Hostname, WiFi.localIP().toString().c_str()); + mqtt_publish(stopic, svalue); + } +#endif // USE_WEBSERVER + if (sysCfg.mqtt_enabled && (MQTT_MAX_PACKET_SIZE < (TOPSZ+MESSZ))) { + snprintf_P(svalue, sizeof(svalue), PSTR("{\"Warning1\":\"Change MQTT_MAX_PACKET_SIZE in libraries/PubSubClient.h to at least %d\"}"), TOPSZ+MESSZ); + mqtt_publish(stopic, svalue); + } + if (!spiffsPresent()) { + snprintf_P(svalue, sizeof(svalue), PSTR("{\"Warning2\":\"No persistent config. Please reflash with at least 16K SPIFFS\"}")); + mqtt_publish(stopic, svalue); + } + if (sysCfg.tele_period) tele_period = sysCfg.tele_period -9; + status_update_timer = 2; +#ifdef USE_DOMOTICZ + domoticz_setUpdateTimer(2); +#endif // USE_DOMOTICZ + } + mqttflag = 0; +} + +void mqtt_reconnect() +{ + char stopic[TOPSZ], svalue[TOPSZ], log[LOGSZ]; + + mqttcounter = MQTT_RETRY_SECS; + + if (!sysCfg.mqtt_enabled) { + mqtt_connected(); + return; + } +#if defined(USE_WEMO_EMULATION) || defined(USE_HUE_EMULATION) + UDP_Disconnect(); +#endif // USE_WEMO_EMULATION || USE_HUE_EMULATION + if (mqttflag > 1) { +#ifdef USE_MQTT_TLS + addLog_P(LOG_LEVEL_INFO, PSTR("MQTT: Verify TLS fingerprint...")); + if (!espClient.connect(sysCfg.mqtt_host, sysCfg.mqtt_port)) { + snprintf_P(log, sizeof(log), PSTR("MQTT: TLS CONNECT FAILED USING WRONG MQTTHost (%s) or MQTTPort (%d). Retry in %d seconds"), + sysCfg.mqtt_host, sysCfg.mqtt_port, mqttcounter); + addLog(LOG_LEVEL_DEBUG, log); + return; + } + if (espClient.verify(sysCfg.mqtt_fingerprint, sysCfg.mqtt_host)) { + addLog_P(LOG_LEVEL_INFO, PSTR("MQTT: Verified")); + } else { + addLog_P(LOG_LEVEL_DEBUG, PSTR("MQTT: WARNING - Insecure connection due to invalid Fingerprint")); + } +#endif // USE_MQTT_TLS + mqttClient.setCallback(mqttDataCb); + mqttflag = 1; + mqttcounter = 1; + return; + } + + addLog_P(LOG_LEVEL_INFO, PSTR("MQTT: Attempting connection...")); +#ifndef USE_MQTT_TLS +#ifdef USE_DISCOVERY +#ifdef MQTT_HOST_DISCOVERY + mdns_discoverMQTTServer(); +#endif // MQTT_HOST_DISCOVERY +#endif // USE_DISCOVERY +#endif // USE_MQTT_TLS + mqttClient.setServer(sysCfg.mqtt_host, sysCfg.mqtt_port); + snprintf_P(stopic, sizeof(stopic), PSTR("%s/%s/LWT"), PUB_PREFIX2, sysCfg.mqtt_topic); + snprintf_P(svalue, sizeof(svalue), PSTR("Offline")); + if (mqttClient.connect(MQTTClient, sysCfg.mqtt_user, sysCfg.mqtt_pwd, stopic, 1, true, svalue)) { + addLog_P(LOG_LEVEL_INFO, PSTR("MQTT: Connected")); + mqttcounter = 0; + snprintf_P(svalue, sizeof(svalue), PSTR("Online")); + mqtt_publish(stopic, svalue, true); + mqtt_connected(); + } else { + snprintf_P(log, sizeof(log), PSTR("MQTT: CONNECT FAILED, rc %d. Retry in %d seconds"), mqttClient.state(), mqttcounter); + addLog(LOG_LEVEL_DEBUG, log); + } +} + +void mqttDataCb(char* topic, byte* data, unsigned int data_len) +{ + char *str; + char svalue[MESSZ]; + + if (!strcmp(SUB_PREFIX,PUB_PREFIX)) { + str = strstr(topic,SUB_PREFIX); + if ((str == topic) && mqtt_cmnd_publish) { + if (mqtt_cmnd_publish > 8) mqtt_cmnd_publish -= 8; else mqtt_cmnd_publish = 0; + return; + } + } + + uint16_t i = 0, grpflg = 0, index; + char topicBuf[TOPSZ], dataBuf[data_len+1], dataBufUc[MESSZ]; + char *p, *mtopic = NULL, *type = NULL; + char stopic[TOPSZ], stemp1[TOPSZ], stemp2[10]; + + strncpy(topicBuf, topic, sizeof(topicBuf)); + memcpy(dataBuf, data, sizeof(dataBuf)); + dataBuf[sizeof(dataBuf)-1] = 0; + + snprintf_P(svalue, sizeof(svalue), PSTR("RSLT: Receive topic %s, data size %d, data %s"), topicBuf, data_len, dataBuf); + addLog(LOG_LEVEL_DEBUG_MORE, svalue); +// if (LOG_LEVEL_DEBUG_MORE <= seriallog_level) Serial.println(dataBuf); + +#ifdef USE_DOMOTICZ + if (sysCfg.mqtt_enabled) { + if (domoticz_mqttData(topicBuf, sizeof(topicBuf), dataBuf, sizeof(dataBuf))) return; + } +#endif // USE_DOMOTICZ + + memmove(topicBuf, topicBuf+sizeof(SUB_PREFIX), sizeof(topicBuf)-sizeof(SUB_PREFIX)); // Remove SUB_PREFIX + + i = 0; + for (str = strtok_r(topicBuf, "/", &p); str && i < 2; str = strtok_r(NULL, "/", &p)) { + switch (i++) { + case 0: // Topic / GroupTopic / DVES_123456 + mtopic = str; + break; + case 1: // TopicIndex / Text + type = str; + } + } + if (!strcmp(mtopic, sysCfg.mqtt_grptopic)) grpflg = 1; + + index = 1; + if (type != NULL) { + for (i = 0; i < strlen(type); i++) { + type[i] = toupper(type[i]); + if (isdigit(type[i])) { + index = atoi(type +i); + break; + } + } + type[i] = '\0'; + } + + for (i = 0; i <= sizeof(dataBufUc); i++) dataBufUc[i] = toupper(dataBuf[i]); + + snprintf_P(svalue, sizeof(svalue), PSTR("RSLT: DataCb Topic %s, Group %d, Index %d, Type %s, Data %s (%s)"), + mtopic, grpflg, index, type, dataBuf, dataBufUc); + addLog(LOG_LEVEL_DEBUG, svalue); + + snprintf_P(stopic, sizeof(stopic), PSTR("%s/%s/RESULT"), PUB_PREFIX, sysCfg.mqtt_topic); + if (type != NULL) { + snprintf_P(svalue, sizeof(svalue), PSTR("{\"Command\":\"Error\"}")); + if (sysCfg.ledstate &0x02) blinks++; + + if (!strcmp(dataBufUc,"?")) data_len = 0; + int16_t payload = atoi(dataBuf); // -32766 - 32767 + uint16_t payload16 = atoi(dataBuf); // 0 - 65535 + if (!strcmp(dataBufUc,"OFF") || !strcmp(dataBufUc,"STOP")) payload = 0; + if (!strcmp(dataBufUc,"ON") || !strcmp(dataBufUc,"START") || !strcmp(dataBufUc,"USER")) payload = 1; + if (!strcmp(dataBufUc,"TOGGLE") || !strcmp(dataBufUc,"ADMIN")) payload = 2; + if (!strcmp(dataBufUc,"BLINK")) payload = 3; + if (!strcmp(dataBufUc,"BLINKOFF")) payload = 4; + + if ((!strcmp(type,"POWER") || !strcmp(type,"LIGHT")) && (index > 0) && (index <= Maxdevice)) { + snprintf_P(sysCfg.mqtt_subtopic, sizeof(sysCfg.mqtt_subtopic), PSTR("%s"), type); + if ((data_len == 0) || (payload > 4)) payload = 9; + do_cmnd_power(index, payload); + return; + } + else if (!strcmp(type,"STATUS")) { + if ((data_len == 0) || (payload < 0) || (payload > MAX_STATUS)) payload = 99; + publish_status(payload); + return; + } + else if ((sysCfg.module != MOTOR) && !strcmp(type,"POWERONSTATE")) { + if ((data_len > 0) && (payload >= 0) && (payload <= 3)) { + sysCfg.poweronstate = payload; + } + snprintf_P(svalue, sizeof(svalue), PSTR("{\"PowerOnState\":%d}"), sysCfg.poweronstate); + } + else if (!strcmp(type,"PULSETIME")) { + if (data_len > 0) { + sysCfg.pulsetime = payload16; // 0 - 65535 + pulse_timer = 0; + } + snprintf_P(svalue, sizeof(svalue), PSTR("{\"PulseTime\":%d}"), sysCfg.pulsetime); + } + else if (!strcmp(type,"BLINKTIME")) { + if ((data_len > 0) && (payload > 2) && (payload <= 3600)) { + sysCfg.blinktime = payload; + if (blink_timer) blink_timer = sysCfg.blinktime; + } + snprintf_P(svalue, sizeof(svalue), PSTR("{\"BlinkTime\":%d}"), sysCfg.blinktime); + } + else if (!strcmp(type,"BLINKCOUNT")) { + if (data_len > 0) { + sysCfg.blinkcount = payload16; // 0 - 65535 + if (blink_counter) blink_counter = sysCfg.blinkcount *2; + } + snprintf_P(svalue, sizeof(svalue), PSTR("{\"BlinkCount\":%d}"), sysCfg.blinkcount); + } + +/*** Sonoff Led Commands *********************************************************************/ + +/* + else if ((sysCfg.module == SONOFF_LED) && !strcmp(type,"COLOR"))) { + if ((data_len > 0) && (payload >= 0) && (payload <= 255)) { + sysCfg.led_color[index -1] = payload; + sl_setColor(4); + } + snprintf_P(svalue, sizeof(svalue), PSTR("{\"Color\":\"%s\"}"), sysCfg.led_color[index -1]); + } + else if ((sysCfg.module == SONOFF_LED) && !strcmp(type,"CWRGB") && (index > 0) && (index <= 5)) { + if ((data_len > 0) && (payload >= 0) && (payload <= 255)) { + sysCfg.led_color[index -1] = payload; + sl_setColor(1); + } + snprintf_P(svalue, sizeof(svalue), PSTR("{\"CWRGB%d\":%d}"), index, sysCfg.led_color[index -1]); + } +*/ + else if ((sysCfg.module == SONOFF_LED) && !strcmp(type,"DIMMER") && (index > 0) && (index <= 3)) { + if ((data_len > 0) && (payload >= 0) && (payload <= 100)) { + sysCfg.led_dimmer[index -1] = payload; + power = 1; +#ifdef USE_DOMOTICZ + mqtt_publishDomoticzPowerState(index); +#endif // USE_DOMOTICZ + sl_setColor(index +1); + } + snprintf_P(svalue, sizeof(svalue), PSTR("{\"Dimmer%d\":%d}"), index, sysCfg.led_dimmer[index -1]); + } + +/*********************************************************************************************/ + + else if (!strcmp(type,"SAVEDATA")) { + if ((data_len > 0) && (payload >= 0) && (payload <= 3600)) { + sysCfg.savedata = payload; + savedatacounter = sysCfg.savedata; + } + if (sysCfg.savestate) sysCfg.power = power; + CFG_Save(); + if (sysCfg.savedata > 1) snprintf_P(stemp1, sizeof(stemp1), PSTR("Every %d seconds"), sysCfg.savedata); + snprintf_P(svalue, sizeof(svalue), PSTR("{\"SaveData\":\"%s\"}"), (sysCfg.savedata) ? (sysCfg.savedata > 1) ? stemp1 : MQTT_STATUS_ON : MQTT_STATUS_OFF); + } + else if (!strcmp(type,"SAVESTATE")) { + if ((data_len > 0) && (payload >= 0) && (payload <= 1)) { + sysCfg.savestate = payload; + } + snprintf_P(svalue, sizeof(svalue), PSTR("{\"SaveState\":\"%s\"}"), (sysCfg.savestate) ? MQTT_STATUS_ON : MQTT_STATUS_OFF); + } + else if (!strcmp(type,"MODULE")) { + if ((data_len > 0) && (payload > 0) && (payload <= MAXMODULE)) { + sysCfg.module = payload -1; + restartflag = 2; + } + snprintf_P(stemp1, sizeof(stemp1), modules[sysCfg.module].name); + snprintf_P(svalue, sizeof(svalue), PSTR("{\"Module\":\"%s (%d)\"}"), stemp1, sysCfg.module +1); + } + else if (!strcmp(type,"MODULES")) { + snprintf_P(svalue, sizeof(svalue), PSTR("{\"Modules1\":\""), svalue); + byte jsflg = 0; + for (byte i = 0; i < 11; i++) { + if (jsflg) snprintf_P(svalue, sizeof(svalue), PSTR("%s, "), svalue); + jsflg = 1; + snprintf_P(stemp1, sizeof(stemp1), modules[i].name); + snprintf_P(svalue, sizeof(svalue), PSTR("%s%s (%d)"), svalue, stemp1, i +1); + } + snprintf_P(svalue, sizeof(svalue), PSTR("%s\"}"), svalue); + mqtt_publish(stopic, svalue); + snprintf_P(svalue, sizeof(svalue), PSTR("{\"Modules2\":\""), svalue); + jsflg = 0; + for (byte i = 11; i < MAXMODULE; i++) { + if (jsflg) snprintf_P(svalue, sizeof(svalue), PSTR("%s, "), svalue); + jsflg = 1; + snprintf_P(stemp1, sizeof(stemp1), modules[i].name); + snprintf_P(svalue, sizeof(svalue), PSTR("%s%s (%d)"), svalue, stemp1, i +1); + } + snprintf_P(svalue, sizeof(svalue), PSTR("%s\"}"), svalue); + } + else if (!strcmp(type,"GPIO") && (index < MAX_GPIO_PIN)) { + mytmplt cmodule; + memcpy_P(&cmodule, &modules[sysCfg.module], sizeof(cmodule)); + if ((data_len > 0) && (cmodule.gp.io[index] == GPIO_USER) && (payload >= GPIO_SENSOR_START) && (payload < GPIO_SENSOR_END)) { + for (byte i = 0; i < MAX_GPIO_PIN; i++) { + if ((cmodule.gp.io[i] == GPIO_USER) && (sysCfg.my_module.gp.io[i] == payload)) sysCfg.my_module.gp.io[i] = 0; + } + sysCfg.my_module.gp.io[index] = payload; + restartflag = 2; + } + snprintf_P(svalue, sizeof(svalue), PSTR("{"), svalue); + byte jsflg = 0; + for (byte i = 0; i < MAX_GPIO_PIN; i++) { + if (cmodule.gp.io[i] == GPIO_USER) { + if (jsflg) snprintf_P(svalue, sizeof(svalue), PSTR("%s, "), svalue); + jsflg = 1; + snprintf_P(stemp1, sizeof(stemp1), sensors[sysCfg.my_module.gp.io[i]]); + snprintf_P(svalue, sizeof(svalue), PSTR("%s\"GPIO%d\":%d (%s)"), svalue, i, sysCfg.my_module.gp.io[i], stemp1); + } + } + if (jsflg) { + snprintf_P(svalue, sizeof(svalue), PSTR("%s}"), svalue); + } else { + snprintf_P(svalue, sizeof(svalue), PSTR("{\"GPIO\":\"Not supported\"}")); + } + } + else if (!strcmp(type,"GPIOS")) { + snprintf_P(svalue, sizeof(svalue), PSTR("{\"GPIOs\":\""), svalue); + byte jsflg = 0; + for (byte i = 0; i < GPIO_SENSOR_END; i++) { + if (jsflg) snprintf_P(svalue, sizeof(svalue), PSTR("%s, "), svalue); + jsflg = 1; + snprintf_P(stemp1, sizeof(stemp1), sensors[i]); + snprintf_P(svalue, sizeof(svalue), PSTR("%s%s (%d)"), svalue, stemp1, i); + } + snprintf_P(svalue, sizeof(svalue), PSTR("%s\"}"), svalue); + } + else if (!strcmp(type,"SLEEP")) { + if ((data_len > 0) && (payload >= 0) && (payload < 251)) { + sysCfg.sleep = payload; + restartflag = 2; + } + snprintf_P(svalue, sizeof(svalue), PSTR("{\"Sleep\":\"%d%s\"}"), sysCfg.sleep, (sysCfg.value_units) ? " mS" : ""); + } + else if (!strcmp(type,"UPGRADE") || !strcmp(type,"UPLOAD")) { + if ((data_len > 0) && (payload == 1)) { + otaflag = 3; + snprintf_P(svalue, sizeof(svalue), PSTR("{\"Upgrade\":\"Version %s from %s\"}"), Version, sysCfg.otaUrl); + } else { + snprintf_P(svalue, sizeof(svalue), PSTR("{\"Upgrade\":\"Option 1 to upgrade\"}")); + } + } + else if (!strcmp(type,"OTAURL")) { + if ((data_len > 0) && (data_len < sizeof(sysCfg.otaUrl))) + strlcpy(sysCfg.otaUrl, (payload == 1) ? OTA_URL : dataBuf, sizeof(sysCfg.otaUrl)); + snprintf_P(svalue, sizeof(svalue), PSTR("{\"OtaUrl\":\"%s\"}"), sysCfg.otaUrl); + } + else if (!strcmp(type,"SERIALLOG")) { + if ((data_len > 0) && (payload >= LOG_LEVEL_NONE) && (payload <= LOG_LEVEL_ALL)) { + sysCfg.seriallog_level = payload; + seriallog_level = payload; + seriallog_timer = 0; + snprintf_P(svalue, sizeof(svalue), PSTR("{\"SerialLog\":%d}"), sysCfg.syslog_level); + } + snprintf_P(svalue, sizeof(svalue), PSTR("{\"SerialLog\":\"%d (Setting %d)\"}"), seriallog_level, sysCfg.seriallog_level); + } + else if (!strcmp(type,"SYSLOG")) { + if ((data_len > 0) && (payload >= LOG_LEVEL_NONE) && (payload <= LOG_LEVEL_ALL)) { + sysCfg.syslog_level = payload; + syslog_level = payload; + syslog_timer = 0; + snprintf_P(svalue, sizeof(svalue), PSTR("{\"SysLog\":%d}"), sysCfg.syslog_level); + } else { + snprintf_P(svalue, sizeof(svalue), PSTR("{\"SysLog\":\"%d (Setting %d)\"}"), syslog_level, sysCfg.syslog_level); + } + } + else if (!strcmp(type,"LOGHOST")) { + if ((data_len > 0) && (data_len < sizeof(sysCfg.syslog_host))) { + strlcpy(sysCfg.syslog_host, (payload == 1) ? SYS_LOG_HOST : dataBuf, sizeof(sysCfg.syslog_host)); + } + snprintf_P(svalue, sizeof(svalue), PSTR("{\"LogHost\":\"%s\"}"), sysCfg.syslog_host); + } + else if (!strcmp(type,"LOGPORT")) { + if ((data_len > 0) && (payload > 0) && (payload < 32766)) { + sysCfg.syslog_port = (payload == 1) ? SYS_LOG_PORT : payload; + } + snprintf_P(svalue, sizeof(svalue), PSTR("{\"LogPort\":%d}"), sysCfg.syslog_port); + } + else if (!strcmp(type,"AP")) { + if ((data_len > 0) && (payload >= 0) && (payload <= 2)) { + switch (payload) { + case 0: // Toggle + sysCfg.sta_active ^= 1; + break; + case 1: // AP1 + case 2: // AP2 + sysCfg.sta_active = payload -1; + } + restartflag = 2; + } + snprintf_P(svalue, sizeof(svalue), PSTR("{\"Ap\":\"%d (%s)\"}"), sysCfg.sta_active +1, sysCfg.sta_ssid[sysCfg.sta_active]); + } + else if (!strcmp(type,"SSID") && (index > 0) && (index <= 2)) { + if ((data_len > 0) && (data_len < sizeof(sysCfg.sta_ssid[0]))) { + strlcpy(sysCfg.sta_ssid[index -1], (payload == 1) ? (index == 1) ? STA_SSID1 : STA_SSID2 : dataBuf, sizeof(sysCfg.sta_ssid[0])); + sysCfg.sta_active = 0; + restartflag = 2; + } + snprintf_P(svalue, sizeof(svalue), PSTR("{\"SSid%d\":\"%s\"}"), index, sysCfg.sta_ssid[index -1]); + } + else if (!strcmp(type,"PASSWORD") && (index > 0) && (index <= 2)) { + if ((data_len > 0) && (data_len < sizeof(sysCfg.sta_pwd[0]))) { + strlcpy(sysCfg.sta_pwd[index -1], (payload == 1) ? (index == 1) ? STA_PASS1 : STA_PASS2 : dataBuf, sizeof(sysCfg.sta_pwd[0])); + sysCfg.sta_active = 0; + restartflag = 2; + } + snprintf_P(svalue, sizeof(svalue), PSTR("{\"Password%d\":\"%s\"}"), index, sysCfg.sta_pwd[index -1]); + } + else if (!grpflg && !strcmp(type,"HOSTNAME")) { + if ((data_len > 0) && (data_len < sizeof(sysCfg.hostname))) { + strlcpy(sysCfg.hostname, (payload == 1) ? WIFI_HOSTNAME : dataBuf, sizeof(sysCfg.hostname)); + if (strstr(sysCfg.hostname,"%")) + strlcpy(sysCfg.hostname, DEF_WIFI_HOSTNAME, sizeof(sysCfg.hostname)); + restartflag = 2; + } + snprintf_P(svalue, sizeof(svalue), PSTR("{\"Hostname\":\"%s\"}"), sysCfg.hostname); + } + else if (!strcmp(type,"WIFICONFIG") || !strcmp(type,"SMARTCONFIG")) { + if ((data_len > 0) && (payload >= WIFI_RESTART) && (payload < MAX_WIFI_OPTION)) { + sysCfg.sta_config = payload; + wificheckflag = sysCfg.sta_config; + snprintf_P(stemp1, sizeof(stemp1), wificfg[sysCfg.sta_config]); + snprintf_P(svalue, sizeof(svalue), PSTR("{\"WifiConfig\":\"%s selected\"}"), stemp1); + if (WIFI_State() != WIFI_RESTART) { +// snprintf_P(svalue, sizeof(svalue), PSTR("%s after restart"), svalue); + restartflag = 2; + } + } else { + snprintf_P(stemp1, sizeof(stemp1), wificfg[sysCfg.sta_config]); + snprintf_P(svalue, sizeof(svalue), PSTR("{\"WifiConfig\":\"%d (%s)\"}"), sysCfg.sta_config, stemp1); + } + } + else if (!strcmp(type,"FRIENDLYNAME") && (index > 0) && (index <= 4)) { + if ((data_len > 0) && (data_len < sizeof(sysCfg.friendlyname[0]))) { + if (index == 1) + strlcpy(sysCfg.friendlyname[0], (payload == 1) ? FRIENDLY_NAME1 : dataBuf, sizeof(sysCfg.friendlyname[0])); + else if (index == 2) + strlcpy(sysCfg.friendlyname[1], (payload == 1) ? FRIENDLY_NAME2 : dataBuf, sizeof(sysCfg.friendlyname[1])); + else if (index == 3) + strlcpy(sysCfg.friendlyname[2], (payload == 1) ? FRIENDLY_NAME3 : dataBuf, sizeof(sysCfg.friendlyname[2])); + else if (index == 4) + strlcpy(sysCfg.friendlyname[3], (payload == 1) ? FRIENDLY_NAME4 : dataBuf, sizeof(sysCfg.friendlyname[3])); + } + snprintf_P(svalue, sizeof(svalue), PSTR("{\"FriendlyName%d\":\"%s\"}"), index, sysCfg.friendlyname[index -1]); + } + else if (swt_flg && !strcmp(type,"SWITCHMODE")) { + if ((data_len > 0) && (payload >= 0) && (payload < MAX_SWITCH_OPTION)) { + sysCfg.switchmode = payload; + } + snprintf_P(svalue, sizeof(svalue), PSTR("{\"SwitchMode\":%d}"), sysCfg.switchmode); + } +#ifdef USE_WEBSERVER + else if (!strcmp(type,"WEBSERVER")) { + if ((data_len > 0) && (payload >= 0) && (payload <= 2)) { + sysCfg.webserver = payload; + } + if (sysCfg.webserver) { + snprintf_P(svalue, sizeof(svalue), PSTR("{\"Webserver\":\"Active for %s on %s with IP address %s\"}"), + (sysCfg.webserver == 2) ? "ADMIN" : "USER", Hostname, WiFi.localIP().toString().c_str()); + } else { + snprintf_P(svalue, sizeof(svalue), PSTR("{\"Webserver\":\"%s\"}"), MQTT_STATUS_OFF); + } + } + else if (!strcmp(type,"WEBLOG")) { + if ((data_len > 0) && (payload >= LOG_LEVEL_NONE) && (payload <= LOG_LEVEL_ALL)) { + sysCfg.weblog_level = payload; + } + snprintf_P(svalue, sizeof(svalue), PSTR("{\"WebLog\":%d}"), sysCfg.weblog_level); + } +#endif // USE_WEBSERVER + else if (!strcmp(type,"UNITS")) { + if ((data_len > 0) && (payload >= 0) && (payload <= 1)) { + sysCfg.value_units = payload; + } + snprintf_P(svalue, sizeof(svalue), PSTR("{\"Units\":\"%s\"}"), (sysCfg.value_units) ? MQTT_STATUS_ON : MQTT_STATUS_OFF); + } + else if (!strcmp(type,"MQTT")) { + if ((data_len > 0) && (payload >= 0) && (payload <= 1)) { + sysCfg.mqtt_enabled = payload; + restartflag = 2; + } + snprintf_P(svalue, sizeof(svalue), PSTR("{\"Mqtt\":\"%s\"}"), (sysCfg.mqtt_enabled) ? MQTT_STATUS_ON : MQTT_STATUS_OFF); + } + else if (!strcmp(type,"TELEPERIOD")) { + if ((data_len > 0) && (payload >= 0) && (payload < 3601)) { + sysCfg.tele_period = (payload == 1) ? TELE_PERIOD : payload; + if ((sysCfg.tele_period > 0) && (sysCfg.tele_period < 10)) sysCfg.tele_period = 10; // Do not allow periods < 10 seconds + tele_period = sysCfg.tele_period; + } + snprintf_P(svalue, sizeof(svalue), PSTR("{\"TelePeriod\":\"%d%s\"}"), sysCfg.tele_period, (sysCfg.value_units) ? " Sec" : ""); + } + else if (!strcmp(type,"RESTART")) { + switch (payload) { + case 1: + restartflag = 2; + snprintf_P(svalue, sizeof(svalue), PSTR("{\"Restart\":\"Restarting\"}")); + break; + case 99: + addLog_P(LOG_LEVEL_INFO, PSTR("APP: Restarting")); + ESP.restart(); + break; + default: + snprintf_P(svalue, sizeof(svalue), PSTR("{\"Restart\":\"1 to restart\"}")); + } + } + else if (!strcmp(type,"RESET")) { + switch (payload) { + case 1: + restartflag = 211; + snprintf_P(svalue, sizeof(svalue), PSTR("{\"Reset\":\"Reset and Restarting\"}")); + break; + case 2: + restartflag = 212; + snprintf_P(svalue, sizeof(svalue), PSTR("{\"Reset\":\"Erase, Reset and Restarting\"}")); + break; + default: + snprintf_P(svalue, sizeof(svalue), PSTR("{\"Reset\":\"1 to reset\"}")); + } + } + else if (!strcmp(type,"TIMEZONE")) { + if ((data_len > 0) && (((payload >= -12) && (payload <= 12)) || (payload == 99))) { + sysCfg.timezone = payload; + } + snprintf_P(svalue, sizeof(svalue), PSTR("{\"Timezone\":\"%d%s\"}"), sysCfg.timezone, (sysCfg.value_units) ? " Hr" : ""); + } + else if (!strcmp(type,"LEDPOWER")) { + if ((data_len > 0) && (payload >= 0) && (payload <= 2)) { + sysCfg.ledstate &= 8; + switch (payload) { + case 0: // Off + case 1: // On + sysCfg.ledstate = payload << 3; + break; + case 2: // Toggle + sysCfg.ledstate ^= 8; + break; + } + blinks = 0; + setLed(sysCfg.ledstate &8); + } + snprintf_P(svalue, sizeof(svalue), PSTR("{\"LedPower\":\"%s\"}"), (sysCfg.ledstate &8) ? MQTT_STATUS_ON : MQTT_STATUS_OFF); + } + else if (!strcmp(type,"LEDSTATE")) { + if ((data_len > 0) && (payload >= 0) && (payload < MAX_LED_OPTION)) { + sysCfg.ledstate = payload; + if (!sysCfg.ledstate) setLed(led_inverted[0]); + } + snprintf_P(svalue, sizeof(svalue), PSTR("{\"LedState\":%d}"), sysCfg.ledstate); + } + else if (!strcmp(type,"CFGDUMP")) { + CFG_Dump(); + snprintf_P(svalue, sizeof(svalue), PSTR("{\"CfgDump\":\"Done\"}")); + } +#ifdef USE_I2C + else if (i2c_flg && !strcmp(type,"I2CSCAN")) { + i2c_scan(svalue, sizeof(svalue)); + } +#endif // USE_I2C + +/*** MQTT Commands ***************************************************************************/ + + else if (sysCfg.mqtt_enabled && !strcmp(type,"MQTTHOST")) { + if ((data_len > 0) && (data_len < sizeof(sysCfg.mqtt_host))) { + strlcpy(sysCfg.mqtt_host, (payload == 1) ? MQTT_HOST : dataBuf, sizeof(sysCfg.mqtt_host)); + restartflag = 2; + } + snprintf_P(svalue, sizeof(svalue), PSTR("{\"MqttHost\",\"%s\"}"), sysCfg.mqtt_host); + } + else if (sysCfg.mqtt_enabled && !strcmp(type,"MQTTPORT")) { + if ((data_len > 0) && (payload > 0) && (payload < 32766)) { + sysCfg.mqtt_port = (payload == 1) ? MQTT_PORT : payload; + restartflag = 2; + } + snprintf_P(svalue, sizeof(svalue), PSTR("{\"MqttPort\":%d}"), sysCfg.mqtt_port); + } +#ifdef USE_MQTT_TLS + else if (sysCfg.mqtt_enabled && !strcmp(type,"MQTTFINGERPRINT")) { + if ((data_len > 0) && (data_len < sizeof(sysCfg.mqtt_fingerprint))) { + strlcpy(sysCfg.mqtt_fingerprint, (payload == 1) ? MQTT_FINGERPRINT : dataBuf, sizeof(sysCfg.mqtt_fingerprint)); + restartflag = 2; + } + snprintf_P(svalue, sizeof(svalue), PSTR("{\"MqttFingerprint\":\"%s\"}"), sysCfg.mqtt_fingerprint); + } +#endif + else if (sysCfg.mqtt_enabled && !grpflg && !strcmp(type,"MQTTCLIENT")) { + if ((data_len > 0) && (data_len < sizeof(sysCfg.mqtt_client))) { + strlcpy(sysCfg.mqtt_client, (payload == 1) ? MQTT_CLIENT_ID : dataBuf, sizeof(sysCfg.mqtt_client)); + restartflag = 2; + } + snprintf_P(svalue, sizeof(svalue), PSTR("{\"MqttClient\":\"%s\"}"), sysCfg.mqtt_client); + } + else if (sysCfg.mqtt_enabled && !strcmp(type,"MQTTUSER")) { + if ((data_len > 0) && (data_len < sizeof(sysCfg.mqtt_user))) { + strlcpy(sysCfg.mqtt_user, (payload == 1) ? MQTT_USER : dataBuf, sizeof(sysCfg.mqtt_user)); + restartflag = 2; + } + snprintf_P(svalue, sizeof(svalue), PSTR("[\"MqttUser\":\"%s\"}"), sysCfg.mqtt_user); + } + else if (sysCfg.mqtt_enabled && !strcmp(type,"MQTTPASSWORD")) { + if ((data_len > 0) && (data_len < sizeof(sysCfg.mqtt_pwd))) { + strlcpy(sysCfg.mqtt_pwd, (payload == 1) ? MQTT_PASS : dataBuf, sizeof(sysCfg.mqtt_pwd)); + restartflag = 2; + } + snprintf_P(svalue, sizeof(svalue), PSTR("{\"MqttPassword\":\"%s\"}"), sysCfg.mqtt_pwd); + } +#ifdef USE_DOMOTICZ + else if (sysCfg.mqtt_enabled && !strncmp(type,"DOMOTICZ...",8)) { + domoticz_commands(type, index, dataBuf, data_len, payload, svalue, sizeof(svalue)); + } +#endif // USE_DOMOTICZ + else if (sysCfg.mqtt_enabled && !strcmp(type,"GROUPTOPIC")) { + if ((data_len > 0) && (data_len < sizeof(sysCfg.mqtt_grptopic))) { + for(i = 0; i <= data_len; i++) + if ((dataBuf[i] == '/') || (dataBuf[i] == '+') || (dataBuf[i] == '#')) dataBuf[i] = '_'; + if (!strcmp(dataBuf, MQTTClient)) payload = 1; + strlcpy(sysCfg.mqtt_grptopic, (payload == 1) ? MQTT_GRPTOPIC : dataBuf, sizeof(sysCfg.mqtt_grptopic)); + restartflag = 2; + } + snprintf_P(svalue, sizeof(svalue), PSTR("{\"GroupTopic\":\"%s\"}"), sysCfg.mqtt_grptopic); + } + else if (sysCfg.mqtt_enabled && !grpflg && !strcmp(type,"TOPIC")) { + if ((data_len > 0) && (data_len < sizeof(sysCfg.mqtt_topic))) { + for(i = 0; i <= data_len; i++) + if ((dataBuf[i] == '/') || (dataBuf[i] == '+') || (dataBuf[i] == '#')) dataBuf[i] = '_'; + if (!strcmp(dataBuf, MQTTClient)) payload = 1; + strlcpy(sysCfg.mqtt_topic, (payload == 1) ? MQTT_TOPIC : dataBuf, sizeof(sysCfg.mqtt_topic)); + restartflag = 2; + } + snprintf_P(svalue, sizeof(svalue), PSTR("{\"Topic\":\"%s\"}"), sysCfg.mqtt_topic); + } + else if (sysCfg.mqtt_enabled && !grpflg && !strcmp(type,"BUTTONTOPIC")) { + if ((data_len > 0) && (data_len < sizeof(sysCfg.button_topic))) { + for(i = 0; i <= data_len; i++) + if ((dataBuf[i] == '/') || (dataBuf[i] == '+') || (dataBuf[i] == '#')) dataBuf[i] = '_'; + if (!strcmp(dataBuf, MQTTClient)) payload = 1; + strlcpy(sysCfg.button_topic, (payload == 1) ? sysCfg.mqtt_topic : dataBuf, sizeof(sysCfg.button_topic)); + } + snprintf_P(svalue, sizeof(svalue), PSTR("{\"ButtonTopic\":\"%s\"}"), sysCfg.button_topic); + } + else if (sysCfg.mqtt_enabled && !grpflg && !strcmp(type,"SWITCHTOPIC")) { + if ((data_len > 0) && (data_len < sizeof(sysCfg.switch_topic))) { + for(i = 0; i <= data_len; i++) + if ((dataBuf[i] == '/') || (dataBuf[i] == '+') || (dataBuf[i] == '#')) dataBuf[i] = '_'; + if (!strcmp(dataBuf, MQTTClient)) payload = 1; + strlcpy(sysCfg.switch_topic, (payload == 1) ? sysCfg.mqtt_topic : dataBuf, sizeof(sysCfg.switch_topic)); + } + snprintf_P(svalue, sizeof(svalue), PSTR("{\"SwitchTopic\":\"%s\"}"), sysCfg.switch_topic); + } + else if (sysCfg.mqtt_enabled && !strcmp(type,"BUTTONRETAIN")) { + if ((data_len > 0) && (payload >= 0) && (payload <= 1)) { + strlcpy(sysCfg.button_topic, sysCfg.mqtt_topic, sizeof(sysCfg.button_topic)); + if (!payload) { + for(i = 1; i <= Maxdevice; i++) { + send_button_power(0, i, 3); // Clear MQTT retain in broker + } + } + sysCfg.mqtt_button_retain = payload; + } + snprintf_P(svalue, sizeof(svalue), PSTR("{\"ButtonRetain\":\"%s\"}"), (sysCfg.mqtt_button_retain) ? MQTT_STATUS_ON : MQTT_STATUS_OFF); + } + else if (sysCfg.mqtt_enabled && !strcmp(type,"SWITCHRETAIN")) { + if ((data_len > 0) && (payload >= 0) && (payload <= 1)) { +// strlcpy(sysCfg.button_topic, sysCfg.mqtt_topic, sizeof(sysCfg.button_topic)); + if (!payload) { + for(i = 1; i <= 4; i++) { + send_button_power(1, i, 3); // Clear MQTT retain in broker + } + } + sysCfg.mqtt_switch_retain = payload; + } + snprintf_P(svalue, sizeof(svalue), PSTR("{\"SwitchRetain\":\"%s\"}"), (sysCfg.mqtt_switch_retain) ? MQTT_STATUS_ON : MQTT_STATUS_OFF); + } + else if (sysCfg.mqtt_enabled && (!strcmp(type,"POWERRETAIN") || !strcmp(type,"LIGHTRETAIN"))) { + if ((data_len > 0) && (payload >= 0) && (payload <= 1)) { + if (!payload) { + for(i = 1; i <= Maxdevice; i++) { // Clear MQTT retain in broker + snprintf_P(stemp2, sizeof(stemp2), PSTR("%d"), i); + snprintf_P(stemp1, sizeof(stemp1), PSTR("%s/%s/POWER%s"), PUB_PREFIX, sysCfg.mqtt_topic, (Maxdevice > 1) ? stemp2 : ""); + mqtt_publish(stemp1, "", sysCfg.mqtt_power_retain); + snprintf_P(stemp1, sizeof(stemp1), PSTR("%s/%s/LIGHT%s"), PUB_PREFIX, sysCfg.mqtt_topic, (Maxdevice > 1) ? stemp2 : ""); + mqtt_publish(stemp1, "", sysCfg.mqtt_power_retain); + } + } + sysCfg.mqtt_power_retain = payload; + } + snprintf_P(stemp1, sizeof(stemp1), PSTR("%s"), (!strcmp(sysCfg.mqtt_subtopic,"POWER")) ? "Power" : "Light"); + snprintf_P(svalue, sizeof(svalue), PSTR("{\"%sRetain\":\"%s\"}"), stemp1, (sysCfg.mqtt_power_retain) ? MQTT_STATUS_ON : MQTT_STATUS_OFF); + } + +/*** HLW Power management Commands ***********************************************************/ + + else if (hlw_flg && !strcmp(type,"POWERLOW")) { + if ((data_len > 0) && (payload >= 0) && (payload < 3601)) { + sysCfg.hlw_pmin = payload; + } + snprintf_P(svalue, sizeof(svalue), PSTR("{\"PowerLow\":\"%d%s\"}"), sysCfg.hlw_pmin, (sysCfg.value_units) ? " W" : ""); + } + else if (hlw_flg && !strcmp(type,"POWERHIGH")) { + if ((data_len > 0) && (payload >= 0) && (payload < 3601)) { + sysCfg.hlw_pmax = payload; + } + snprintf_P(svalue, sizeof(svalue), PSTR("{\"PowerHigh\":\"%d%s\"}"), sysCfg.hlw_pmax, (sysCfg.value_units) ? " W" : ""); + } + else if (hlw_flg && !strcmp(type,"VOLTAGELOW")) { + if ((data_len > 0) && (payload >= 0) && (payload < 501)) { + sysCfg.hlw_umin = payload; + } + snprintf_P(svalue, sizeof(svalue), PSTR("{\"VoltageLow\":\"%d%s\"}"), sysCfg.hlw_umin, (sysCfg.value_units) ? " V" : ""); + } + else if (hlw_flg && !strcmp(type,"VOLTAGEHIGH")) { + if ((data_len > 0) && (payload >= 0) && (payload < 501)) { + sysCfg.hlw_umax = payload; + } + snprintf_P(svalue, sizeof(svalue), PSTR("[\"VoltageHigh\":\"%d%s\"}"), sysCfg.hlw_umax, (sysCfg.value_units) ? " V" : ""); + } + else if (hlw_flg && !strcmp(type,"CURRENTLOW")) { + if ((data_len > 0) && (payload >= 0) && (payload < 16001)) { + sysCfg.hlw_imin = payload; + } + snprintf_P(svalue, sizeof(svalue), PSTR("{\"CurrentLow\":\"%d%s\"}"), sysCfg.hlw_imin, (sysCfg.value_units) ? " mA" : ""); + } + else if (hlw_flg && !strcmp(type,"CURRENTHIGH")) { + if ((data_len > 0) && (payload >= 0) && (payload < 16001)) { + sysCfg.hlw_imax = payload; + } + snprintf_P(svalue, sizeof(svalue), PSTR("{\"CurrentHigh\":\"%d%s\"}"), sysCfg.hlw_imax, (sysCfg.value_units) ? " mA" : ""); + } + else if (hlw_flg && !strcmp(type,"HLWPCAL")) { + if ((data_len > 0) && (payload > 0) && (payload < 32001)) { + sysCfg.hlw_pcal = (payload == 1) ? HLW_PREF_PULSE : payload; + } + snprintf_P(svalue, sizeof(svalue), PSTR("(\"HlwPcal\":\"%d%s\"}"), sysCfg.hlw_pcal, (sysCfg.value_units) ? " uS" : ""); + } + else if (hlw_flg && !strcmp(type,"HLWUCAL")) { + if ((data_len > 0) && (payload > 0) && (payload < 32001)) { + sysCfg.hlw_ucal = (payload == 1) ? HLW_UREF_PULSE : payload; + } + snprintf_P(svalue, sizeof(svalue), PSTR("{\"HlwUcal\":\"%d%s\"}"), sysCfg.hlw_ucal, (sysCfg.value_units) ? " uS" : ""); + } + else if (hlw_flg && !strcmp(type,"HLWICAL")) { + if ((data_len > 0) && (payload > 0) && (payload < 32001)) { + sysCfg.hlw_ical = (payload == 1) ? HLW_IREF_PULSE : payload; + } + snprintf_P(svalue, sizeof(svalue), PSTR("{\"HlwIcal\":\"%d%s\"}"), sysCfg.hlw_ical, (sysCfg.value_units) ? " uS" : ""); + } +#ifdef FEATURE_POWER_LIMIT + else if (hlw_flg && !strcmp(type,"MAXPOWER")) { + if ((data_len > 0) && (payload >= 0) && (payload < 3601)) { + sysCfg.hlw_mpl = payload; + } + snprintf_P(svalue, sizeof(svalue), PSTR("{\"MaxPower\":\"%d%s\"}"), sysCfg.hlw_mpl, (sysCfg.value_units) ? " W" : ""); + } + else if (hlw_flg && !strcmp(type,"MAXPOWERHOLD")) { + if ((data_len > 0) && (payload >= 0) && (payload < 3601)) { + sysCfg.hlw_mplh = (payload == 1) ? MAX_POWER_HOLD : payload; + } + snprintf_P(svalue, sizeof(svalue), PSTR("{\"MaxPowerHold\":\"%d%s\"}"), sysCfg.hlw_mplh, (sysCfg.value_units) ? " Sec" : ""); + } + else if (hlw_flg && !strcmp(type,"MAXPOWERWINDOW")) { + if ((data_len > 0) && (payload >= 0) && (payload < 3601)) { + sysCfg.hlw_mplw = (payload == 1) ? MAX_POWER_WINDOW : payload; + } + snprintf_P(svalue, sizeof(svalue), PSTR("{\"MaxPowerWindow\":\"%d%s\"}"), sysCfg.hlw_mplw, (sysCfg.value_units) ? " Sec" : ""); + } + else if (hlw_flg && !strcmp(type,"SAFEPOWER")) { + if ((data_len > 0) && (payload >= 0) && (payload < 3601)) { + sysCfg.hlw_mspl = payload; + } + snprintf_P(svalue, sizeof(svalue), PSTR("{\"SafePower\":\"%d%s\"}"), sysCfg.hlw_mspl, (sysCfg.value_units) ? " W" : ""); + } + else if (hlw_flg && !strcmp(type,"SAFEPOWERHOLD")) { + if ((data_len > 0) && (payload >= 0) && (payload < 3601)) { + sysCfg.hlw_msplh = (payload == 1) ? SAFE_POWER_HOLD : payload; + } + snprintf_P(svalue, sizeof(svalue), PSTR("{\"SafePowerHold\":\"%d%s\"}"), sysCfg.hlw_msplh, (sysCfg.value_units) ? " Sec" : ""); + } + else if (hlw_flg && !strcmp(type,"SAFEPOWERWINDOW")) { + if ((data_len > 0) && (payload >= 0) && (payload < 1440)) { + sysCfg.hlw_msplw = (payload == 1) ? SAFE_POWER_WINDOW : payload; + } + snprintf_P(svalue, sizeof(svalue), PSTR("{\"SafePowerWindow\":\"%d%s\"}"), sysCfg.hlw_msplw, (sysCfg.value_units) ? " Min" : ""); + } + else if (hlw_flg && !strcmp(type,"MAXENERGY")) { + if ((data_len > 0) && (payload >= 0) && (payload < 3601)) { + sysCfg.hlw_mkwh = payload; + hlw_mkwh_state = 3; + } + snprintf_P(svalue, sizeof(svalue), PSTR("{\"MaxEnergy\":\"%d%s\"}"), sysCfg.hlw_mkwh, (sysCfg.value_units) ? " Wh" : ""); + } + else if (hlw_flg && !strcmp(type,"MAXENERGYSTART")) { + if ((data_len > 0) && (payload >= 0) && (payload < 24)) { + sysCfg.hlw_mkwhs = payload; + } + snprintf_P(svalue, sizeof(svalue), PSTR("{\"MaxEnergyStart\":\"%d%s\"}"), sysCfg.hlw_mkwhs, (sysCfg.value_units) ? " Hr" : ""); + } +#endif // FEATURE_POWER_LIMIT + +/*** WS2812 Commands *************************************************************************/ + +#ifdef USE_WS2812 + else if ((pin[GPIO_WS2812] < 99) && !strcmp(type,"PIXELS")) { + if ((data_len > 0) && (payload > 0) && (payload <= WS2812_MAX_LEDS)) { + sysCfg.ws_pixels = payload; + ws2812_pixels(); + } + snprintf_P(svalue, sizeof(svalue), PSTR("{\"Pixels\":%d}"), sysCfg.ws_pixels); + } + else if ((pin[GPIO_WS2812] < 99) && !strcmp(type,"LED") && (index > 0) && (index <= sysCfg.ws_pixels)) { + if (data_len == 6) { + ws2812_setColor(index, dataBufUc); + } + ws2812_getColor(index, svalue, sizeof(svalue)); + } + else if ((pin[GPIO_WS2812] < 99) && !strcmp(type,"COLOR")) { + if (data_len == 6) { + ws2812_setColor(0, dataBufUc); + power = 1; + } + ws2812_getColor(0, svalue, sizeof(svalue)); + } + else if ((pin[GPIO_WS2812] < 99) && !strcmp(type,"DIMMER")) { + if ((data_len > 0) && (payload >= 0) && (payload <= 100)) { + sysCfg.ws_dimmer = payload; + power = 1; +#ifdef USE_DOMOTICZ + mqtt_publishDomoticzPowerState(index); +#endif // USE_DOMOTICZ + } + snprintf_P(svalue, sizeof(svalue), PSTR("{\"Dimmer\":%d}"), sysCfg.ws_dimmer); + } + else if ((pin[GPIO_WS2812] < 99) && !strcmp(type,"LEDTABLE")) { + if ((data_len > 0) && (payload >= 0) && (payload <= 2)) { + switch (payload) { + case 0: // Off + case 1: // On + sysCfg.ws_ledtable = payload; + break; + case 2: // Toggle + sysCfg.ws_ledtable ^= 1; + break; + } + ws2812_update(); + } + snprintf_P(svalue, sizeof(svalue), PSTR("{\"LedTable\":\"%s\"}"), (sysCfg.ws_ledtable) ? MQTT_STATUS_ON : MQTT_STATUS_OFF); + } + else if ((pin[GPIO_WS2812] < 99) && !strcmp(type,"FADE")) { + if ((data_len > 0) && (payload >= 0) && (payload <= 2)) { + switch (payload) { + case 0: // Off + case 1: // On + sysCfg.ws_fade = payload; + break; + case 2: // Toggle + sysCfg.ws_fade ^= 1; + break; + } + } + snprintf_P(svalue, sizeof(svalue), PSTR("{\"Fade\":\"%s\"}"), (sysCfg.ws_fade) ? MQTT_STATUS_ON : MQTT_STATUS_OFF); + } + else if ((pin[GPIO_WS2812] < 99) && !strcmp(type,"SPEED")) { // 1 - fast, 5 - slow + if ((data_len > 0) && (payload > 0) && (payload <= 5)) { + sysCfg.ws_speed = payload; + } + snprintf_P(svalue, sizeof(svalue), PSTR("{\"Speed\":%d}"), sysCfg.ws_speed); + } + else if ((pin[GPIO_WS2812] < 99) && !strcmp(type,"WIDTH")) { + if ((data_len > 0) && (payload >= 0) && (payload <= 4)) { + sysCfg.ws_width = payload; + } + snprintf_P(svalue, sizeof(svalue), PSTR("{\"Width\":%d}"), sysCfg.ws_width); + } + else if ((pin[GPIO_WS2812] < 99) && !strcmp(type,"WAKEUP")) { + if ((data_len > 0) && (payload > 0) && (payload < 3601)) { + sysCfg.ws_wakeup = payload; + if (sysCfg.ws_scheme == 1) sysCfg.ws_scheme = 0; + } + snprintf_P(svalue, sizeof(svalue), PSTR("{\"WakeUp\":%d}"), sysCfg.ws_wakeup); + } + else if ((pin[GPIO_WS2812] < 99) && !strcmp(type,"SCHEME")) { + if ((data_len > 0) && (payload >= 0) && (payload <= 9)) { + sysCfg.ws_scheme = payload; + if (sysCfg.ws_scheme == 1) ws2812_resetWakupState(); + power = 1; + ws2812_resetStripTimer(); + } + snprintf_P(svalue, sizeof(svalue), PSTR("{\"Scheme\":%d}"), sysCfg.ws_scheme); + } +#endif // USE_WS2812 + +/*********************************************************************************************/ + + else { + type = NULL; + } + } + if (type == NULL) { + blinks = 201; + snprintf_P(svalue, sizeof(svalue), PSTR("{\"Commands\":\"Status, SaveData, SaveSate, Sleep, Upgrade, Otaurl, Restart, Reset, WifiConfig, Seriallog, Syslog, LogHost, LogPort, SSId1, SSId2, Password1, Password2, AP%s\"}"), (!grpflg) ? ", Hostname, Module, Modules, GPIO, GPIOs" : ""); + mqtt_publish(stopic, svalue); + + if (sysCfg.mqtt_enabled) { + snprintf_P(svalue, sizeof(svalue), PSTR("{\"Commands\":\"Mqtt, MqttHost, MqttPort, MqttUser, MqttPassword%s, GroupTopic, Units, Timezone, LedState, LedPower, TelePeriod\"}"), (!grpflg) ? ", MqttClient, Topic, ButtonTopic, ButtonRetain, SwitchTopic, SwitchRetain, PowerRetain" : ""); + } else { + snprintf_P(svalue, sizeof(svalue), PSTR("{\"Commands\":\"Mqtt, Units, Timezone, LedState, LedPower, TelePeriod\"}"), (!grpflg) ? ", MqttClient" : ""); + } + mqtt_publish(stopic, svalue); + + snprintf_P(svalue, sizeof(svalue), PSTR("{\"Commands\":\"%s%s, PulseTime, BlinkTime, BlinkCount"), (Maxdevice == 1) ? "Power, Light" : "Power1, Power2, Light1 Light2", (sysCfg.module != MOTOR) ? ", PowerOnState" : ""); +#ifdef USE_WEBSERVER + snprintf_P(svalue, sizeof(svalue), PSTR("%s, Weblog, Webserver"), svalue); +#endif + if (swt_flg) snprintf_P(svalue, sizeof(svalue), PSTR("%s, SwitchMode"), svalue); +#ifdef USE_I2C + if (i2c_flg) snprintf_P(svalue, sizeof(svalue), PSTR("%s, I2CScan"), svalue); +#endif // USE_I2C +#ifdef USE_WS2812 + if (pin[GPIO_WS2812] < 99) snprintf_P(svalue, sizeof(svalue), PSTR("%s, Pixels, Led, Color, Dimmer, Scheme, Fade, Speed, Width, Wakeup, LedTable"), svalue); +#endif + snprintf_P(svalue, sizeof(svalue), PSTR("%s\"}"), svalue); + +#ifdef USE_DOMOTICZ + mqtt_publish(stopic, svalue); + snprintf_P(svalue, sizeof(svalue), PSTR("{\"Commands\":\"DomoticzInTopic, DomoticzOutTopic, DomoticzIdx, DomoticzKeyIdx, DomoticzSwitchIdx, DomoticzSensorIdx, DomoticzUpdateTimer\"}")); +#endif // USE_DOMOTICZ + + if (hlw_flg) { + mqtt_publish(stopic, svalue); + snprintf_P(svalue, sizeof(svalue), PSTR("{\"Commands\":\"PowerLow, PowerHigh, VoltageLow, VoltageHigh, CurrentLow, CurrentHigh")); +#ifdef FEATURE_POWER_LIMIT + snprintf_P(svalue, sizeof(svalue), PSTR("%s, SafePower, SafePowerHold, SafePowerWindow, MaxPower, MaxPowerHold, MaxPowerWindow, MaxEnergy, MaxEnergyStart"), svalue); +#endif // FEATURE_POWER_LIMIT + snprintf_P(svalue, sizeof(svalue), PSTR("%s\"}"), svalue); + } + } + mqtt_publish(stopic, svalue); +} + +/********************************************************************************************/ + +void send_button_power(byte key, byte device, byte state) +{ +// key 0 = button_topic +// key 1 = switch_topic + + char stopic[TOPSZ], svalue[TOPSZ], stemp1[10]; + + if (device > Maxdevice) device = 1; + snprintf_P(stemp1, sizeof(stemp1), PSTR("%d"), device); + snprintf_P(stopic, sizeof(stopic), PSTR("%s/%s/%s%s"), + SUB_PREFIX, (key) ? sysCfg.switch_topic : sysCfg.button_topic, sysCfg.mqtt_subtopic, (Maxdevice > 1) ? stemp1 : ""); + + if (state == 3) { + svalue[0] = '\0'; + } else { + if (!strcmp(sysCfg.mqtt_topic,(key) ? sysCfg.switch_topic : sysCfg.button_topic) && (state == 2)) { + state = ~(power >> (device -1)) & 0x01; + } + snprintf_P(svalue, sizeof(svalue), PSTR("%s"), (state) ? (state == 2) ? MQTT_CMND_TOGGLE : MQTT_STATUS_ON : MQTT_STATUS_OFF); + } +#ifdef USE_DOMOTICZ + if (!(domoticz_button(key, device, state, strlen(svalue)))) { + mqtt_publish_sec(stopic, svalue, (key) ? sysCfg.mqtt_switch_retain : sysCfg.mqtt_button_retain); + } +#else + mqtt_publish_sec(stopic, svalue, (key) ? sysCfg.mqtt_switch_retain : sysCfg.mqtt_button_retain); +#endif // USE_DOMOTICZ +} + +void do_cmnd_power(byte device, byte state) +{ +// device = Relay number 1 and up +// state 0 = Relay Off +// state 1 = Relay on (turn off after sysCfg.pulsetime * 100 mSec if enabled) +// state 2 = Toggle relay +// state 3 = Blink relay +// state 4 = Stop blinking relay +// state 9 = Show power state + + if ((device < 1) || (device > Maxdevice)) device = 1; + byte mask = 0x01 << (device -1); + pulse_timer = 0; + if (state <= 2) { + if ((blink_mask & mask)) { + blink_mask &= (0xFF ^ mask); // Clear device mask + mqtt_publishPowerBlinkState(device); + } + switch (state) { + case 0: { // Off + power &= (0xFF ^ mask); + break; } + case 1: // On + power |= mask; + break; + case 2: // Toggle + power ^= mask; + } + setRelay(power); +#ifdef USE_DOMOTICZ + domoticz_updatePowerState(device); +#endif // USE_DOMOTICZ + if (device == 1) pulse_timer = (power & mask) ? sysCfg.pulsetime : 0; + } + else if (state == 3) { // Blink + if (!(blink_mask & mask)) { + blink_powersave = (blink_powersave & (0xFF ^ mask)) | (power & mask); // Save state + blink_power = (power >> (device -1))&1; // Prep to Toggle + } + blink_timer = 1; + blink_counter = ((!sysCfg.blinkcount) ? 64000 : (sysCfg.blinkcount *2)) +1; + blink_mask |= mask; // Set device mask + mqtt_publishPowerBlinkState(device); + return; + } + else if (state == 4) { // No Blink + byte flag = (blink_mask & mask); + blink_mask &= (0xFF ^ mask); // Clear device mask + mqtt_publishPowerBlinkState(device); + if (flag) do_cmnd_power(device, (blink_powersave >> (device -1))&1); // Restore state + return; + } + mqtt_publishPowerState(device); +} + +void stop_all_power_blink() +{ + byte i, mask; + + for (i = 1; i <= Maxdevice; i++) { + mask = 0x01 << (i -1); + if (blink_mask & mask) { + blink_mask &= (0xFF ^ mask); // Clear device mask + mqtt_publishPowerBlinkState(i); + do_cmnd_power(i, (blink_powersave >> (i -1))&1); // Restore state + } + } +} + +void do_cmnd(char *cmnd) +{ + char stopic[TOPSZ], svalue[128]; + char *start; + char *token; + + token = strtok(cmnd, " "); + if (token != NULL) { + start = strrchr(token, '/'); // Skip possible cmnd/sonoff/ preamble + if (start) token = start; + } + snprintf_P(stopic, sizeof(stopic), PSTR("%s/%s/%s"), SUB_PREFIX, sysCfg.mqtt_topic, token); + token = strtok(NULL, ""); + snprintf_P(svalue, sizeof(svalue), PSTR("%s"), (token == NULL) ? "" : token); + mqttDataCb(stopic, (byte*)svalue, strlen(svalue)); +} + +void publish_status(uint8_t payload) +{ + char stopic[TOPSZ], svalue[MESSZ], stemp1[TOPSZ], stemp2[10], stemp3[10]; + float ped, pi, pc; + uint16_t pe, pw, pu; + + // Workaround MQTT - TCP/IP stack queueing when SUB_PREFIX = PUB_PREFIX + snprintf_P(stopic, sizeof(stopic), PSTR("%s/%s/RESULT"), + (!strcmp(SUB_PREFIX,PUB_PREFIX) && (!payload)) ? PUB_PREFIX2 : PUB_PREFIX, sysCfg.mqtt_topic); + + if ((!sysCfg.mqtt_enabled) && (payload == 6)) payload = 99; + + if ((payload == 0) || (payload == 99)) { + snprintf_P(svalue, sizeof(svalue), PSTR("{\"Status\":{\"Module\":%d, \"FriendlyName\":\"%s\", \"Topic\":\"%s\", \"ButtonTopic\":\"%s\", \"Subtopic\":\"%s\", \"Power\":%d, \"PowerOnState\":%d, \"LedState\":%d, \"SaveData\":%d, \"SaveState\":%d, \"ButtonRetain\":%d, \"PowerRetain\":%d}}"), + sysCfg.module +1, sysCfg.friendlyname[0], sysCfg.mqtt_topic, sysCfg.button_topic, sysCfg.mqtt_subtopic, power, sysCfg.poweronstate, sysCfg.ledstate, sysCfg.savedata, sysCfg.savestate, sysCfg.mqtt_button_retain, sysCfg.mqtt_power_retain); + if (payload == 0) mqtt_publish(stopic, svalue); + } + + if (hlw_flg) { + if ((payload == 0) || (payload == 8)) { + hlw_readEnergy(0, ped, pe, pw, pu, pi, pc); + dtostrf(pi, 1, 3, stemp1); + dtostrf(ped, 1, 3, stemp2); + dtostrf(pc, 1, 2, stemp3); + snprintf_P(svalue, sizeof(svalue), PSTR("{\"StatusPWR\":{\"Voltage\":%d, \"Current\":\"%s\", \"Power\":%d, \"Today\":\"%s\", \"Factor\":\"%s\"}}"), + pu, stemp1, pw, stemp2, stemp3); + if (payload == 0) mqtt_publish(stopic, svalue); + } + + if ((payload == 0) || (payload == 9)) { + snprintf_P(svalue, sizeof(svalue), PSTR("{\"StatusPWRThreshold\":{\"PowerLow\":%d, \"PowerHigh\":%d, \"VoltageLow\":%d, \"VoltageHigh\":%d, \"CurrentLow\":%d, \"CurrentHigh\":%d}}"), + sysCfg.hlw_pmin, sysCfg.hlw_pmax, sysCfg.hlw_umin, sysCfg.hlw_umax, sysCfg.hlw_imin, sysCfg.hlw_imax); + if (payload == 0) mqtt_publish(stopic, svalue); + } + } + + if ((payload == 0) || (payload == 1)) { + snprintf_P(svalue, sizeof(svalue), PSTR("{\"StatusPRM\":{\"Baudrate\":%d, \"GroupTopic\":\"%s\", \"OtaUrl\":\"%s\", \"Uptime\":%d, \"Sleep\":%d, \"BootCount\":%d, \"SaveCount\":%d}}"), + Baudrate, sysCfg.mqtt_grptopic, sysCfg.otaUrl, uptime, sysCfg.sleep, sysCfg.bootcount, sysCfg.saveFlag); + if (payload == 0) mqtt_publish(stopic, svalue); + } + + if ((payload == 0) || (payload == 2)) { + snprintf_P(svalue, sizeof(svalue), PSTR("{\"StatusFWR\":{\"Program\":\"%s\", \"Boot\":%d, \"SDK\":\"%s\"}}"), + Version, ESP.getBootVersion(), ESP.getSdkVersion()); + if (payload == 0) mqtt_publish(stopic, svalue); + } + + if ((payload == 0) || (payload == 3)) { + snprintf_P(svalue, sizeof(svalue), PSTR("{\"StatusLOG\":{\"Seriallog\":%d, \"Weblog\":%d, \"Syslog\":%d, \"LogHost\":\"%s\", \"SSId1\":\"%s\", \"SSId2\":\"%s\", \"TelePeriod\":%d}}"), + sysCfg.seriallog_level, sysCfg.weblog_level, sysCfg.syslog_level, sysCfg.syslog_host, sysCfg.sta_ssid[0], sysCfg.sta_ssid[1], sysCfg.tele_period); + if (payload == 0) mqtt_publish(stopic, svalue); + } + + if ((payload == 0) || (payload == 4)) { + snprintf_P(svalue, sizeof(svalue), PSTR("{\"StatusMEM\":{\"ProgramSize\":%d, \"Free\":%d, \"Heap\":%d, \"SpiffsStart\":%d, \"SpiffsSize\":%d, \"FlashSize\":%d, \"ProgramFlashSize\":%d}}"), + ESP.getSketchSize()/1024, ESP.getFreeSketchSpace()/1024, ESP.getFreeHeap()/1024, ((uint32_t)&_SPIFFS_start - 0x40200000)/1024, + (((uint32_t)&_SPIFFS_end - 0x40200000) - ((uint32_t)&_SPIFFS_start - 0x40200000))/1024, ESP.getFlashChipRealSize()/1024, ESP.getFlashChipSize()/1024); + if (payload == 0) mqtt_publish(stopic, svalue); + } + + if ((payload == 0) || (payload == 5)) { + snprintf_P(svalue, sizeof(svalue), PSTR("{\"StatusNET\":{\"Host\":\"%s\", \"IP\":\"%s\", \"Gateway\":\"%s\", \"Subnetmask\":\"%s\", \"Mac\":\"%s\", \"Webserver\":%d, \"WifiConfig\":%d}}"), + Hostname, WiFi.localIP().toString().c_str(), WiFi.gatewayIP().toString().c_str(), WiFi.subnetMask().toString().c_str(), + WiFi.macAddress().c_str(), sysCfg.webserver, sysCfg.sta_config); + if (payload == 0) mqtt_publish(stopic, svalue); + } + + if (((payload == 0) || (payload == 6)) && sysCfg.mqtt_enabled) { + snprintf_P(svalue, sizeof(svalue), PSTR("{\"StatusMQT\":{\"Host\":\"%s\", \"Port\":%d, \"ClientMask\":\"%s\", \"Client\":\"%s\", \"User\":\"%s\", \"MAX_PACKET_SIZE\":%d, \"KEEPALIVE\":%d}}"), + sysCfg.mqtt_host, sysCfg.mqtt_port, sysCfg.mqtt_client, MQTTClient, sysCfg.mqtt_user, MQTT_MAX_PACKET_SIZE, MQTT_KEEPALIVE); + if (payload == 0) mqtt_publish(stopic, svalue); + } + + if ((payload == 0) || (payload == 7)) { + snprintf_P(svalue, sizeof(svalue), PSTR("{\"StatusTIM\":{\"UTC\":\"%s\", \"Local\":\"%s\", \"StartDST\":\"%s\", \"EndDST\":\"%s\", \"Timezone\":%d}}"), + rtc_time(0).c_str(), rtc_time(1).c_str(), rtc_time(2).c_str(), rtc_time(3).c_str(), sysCfg.timezone); + } + + mqtt_publish(stopic, svalue); +} + +/********************************************************************************************/ + +void every_second_cb() +{ + // 1 second rtc interrupt routine + // Keep this code small (every_second is to large - it'll trip exception) + +#ifdef FEATURE_POWER_LIMIT + if (rtcTime.Valid) { + if (rtc_loctime() == rtc_midnight()) { + hlw_mkwh_state = 3; + } + if ((rtcTime.Hour == sysCfg.hlw_mkwhs) && (hlw_mkwh_state == 3)) { + hlw_mkwh_state = 0; + } + } +#endif // FEATURE_POWER_LIMIT +} + +void every_second() +{ + char log[LOGSZ], stopic[TOPSZ], svalue[MESSZ], stime[21]; + + snprintf_P(stime, sizeof(stime), PSTR("%04d-%02d-%02dT%02d:%02d:%02d"), + rtcTime.Year, rtcTime.Month, rtcTime.Day, rtcTime.Hour, rtcTime.Minute, rtcTime.Second); + + if (pulse_timer > 111) pulse_timer--; + + if (seriallog_timer) { + seriallog_timer--; + if (!seriallog_timer) { + if (seriallog_level) { + addLog_P(LOG_LEVEL_INFO, PSTR("APP: Serial logging disabled")); + } + seriallog_level = 0; + } + } + + if (syslog_timer) { // Restore syslog level + syslog_timer--; + if (!syslog_timer) { + syslog_level = sysCfg.syslog_level; + if (sysCfg.syslog_level) { + addLog_P(LOG_LEVEL_INFO, PSTR("SYSL: Syslog logging re-enabled")); // Might trigger disable again (on purpose) + } + } + } + +#ifdef USE_DOMOTICZ + domoticz_mqttUpdate(); +#endif // USE_DOMOTICZ + + if (status_update_timer) { + status_update_timer--; + if (!status_update_timer) { + for (byte i = 1; i <= Maxdevice; i++) mqtt_publishPowerState(i); + } + } + + if (sysCfg.tele_period) { + tele_period++; + if (tele_period == sysCfg.tele_period -1) { + if (pin[GPIO_DSB]) { +#ifdef USE_DS18B20 + dsb_readTempPrep(); +#endif // USE_DS18B20 +#ifdef USE_DS18x20 + ds18x20_search(); // Check for changes in sensors number + ds18x20_convert(); // Start Conversion, takes up to one second +#endif // USE_DS18x20 + } +#ifdef USE_DHT + if (dht_type) dht_readPrep(); +#endif // USE_DHT +#ifdef USE_I2C + if (i2c_flg) { +#ifdef USE_HTU + htu_detect(); +#endif // USE_HTU +#ifdef USE_BMP + bmp_detect(); +#endif // USE_BMP +#ifdef USE_BH1750 + bh1750_detect(); +#endif // USE_BH1750 + } +#endif // USE_I2C + } + if (tele_period >= sysCfg.tele_period) { + tele_period = 0; + + snprintf_P(stopic, sizeof(stopic), PSTR("%s/%s/TELEMETRY"), PUB_PREFIX2, sysCfg.mqtt_topic); + snprintf_P(svalue, sizeof(svalue), PSTR("{\"Time\":\"%s\", \"Uptime\":%d"), stime, uptime); + for (byte i = 0; i < Maxdevice; i++) { + if (Maxdevice == 1) { // Legacy + snprintf_P(svalue, sizeof(svalue), PSTR("%s, \"%s\":"), svalue, sysCfg.mqtt_subtopic); + } else { + snprintf_P(svalue, sizeof(svalue), PSTR("%s, \"%s%d\":"), svalue, sysCfg.mqtt_subtopic, i +1); + } + snprintf_P(svalue, sizeof(svalue), PSTR("%s\"%s\""), svalue, (power & (0x01 << i)) ? MQTT_STATUS_ON : MQTT_STATUS_OFF); + } + snprintf_P(svalue, sizeof(svalue), PSTR("%s, \"Wifi\":{\"AP\":%d, \"SSID\":\"%s\", \"RSSI\":%d}}"), + svalue, sysCfg.sta_active +1, sysCfg.sta_ssid[sysCfg.sta_active], WIFI_getRSSIasQuality(WiFi.RSSI())); + mqtt_publish(stopic, svalue); + + uint8_t djson = 0; + snprintf_P(svalue, sizeof(svalue), PSTR("{\"Time\":\"%s\""), stime); + if (pin[GPIO_DSB] < 99) { +#ifdef USE_DS18B20 + dsb_mqttPresent(svalue, sizeof(svalue), &djson, 0); +#endif // USE_DS18B20 +#ifdef USE_DS18x20 + ds18x20_mqttPresent(svalue, sizeof(svalue), &djson, 0); +#endif // USE_DS18x20 + } +#if defined(USE_DHT) || defined(USE_DHT2) + if (dht_type) dht_mqttPresent(svalue, sizeof(svalue), &djson, 1); +#endif // USE_DHT/2 +#ifdef USE_I2C + if (i2c_flg) { +#ifdef USE_HTU + htu_mqttPresent(svalue, sizeof(svalue), &djson, 1); +#endif // USE_HTU +#ifdef USE_BMP + bmp_mqttPresent(svalue, sizeof(svalue), &djson, 2); +#endif // USE_BMP +#ifdef USE_BH1750 + bh1750_mqttPresent(svalue, sizeof(svalue), &djson, 3); +#endif // USE_BH1750 + } +#endif // USE_I2C + if (djson) { + snprintf_P(svalue, sizeof(svalue), PSTR("%s}"), svalue); + mqtt_publish(stopic, svalue); + } + + if (hlw_flg) hlw_mqttPresent(4); + } + } + + if (hlw_flg) hlw_margin_chk(); + + if ((rtcTime.Minute == 2) && (rtcTime.Second == 30)) { + uptime++; + snprintf_P(stopic, sizeof(stopic), PSTR("%s/%s/TELEMETRY"), PUB_PREFIX2, sysCfg.mqtt_topic); + snprintf_P(svalue, sizeof(svalue), PSTR("{\"Time\":\"%s\", \"Uptime\":%d}"), stime, uptime); + mqtt_publish(stopic, svalue); + } +} + +void stateloop() +{ + uint8_t button, flag, switchflag, power_now; + char scmnd[20], log[LOGSZ], stopic[TOPSZ], svalue[MESSZ]; + + timerxs = millis() + (1000 / STATES); + state++; + if (state == STATES) { // Every second + state = 0; + every_second(); + } + + if (mqtt_cmnd_publish) mqtt_cmnd_publish--; // Clean up + + if ((pulse_timer > 0) && (pulse_timer < 112)) { + pulse_timer--; + if (!pulse_timer) do_cmnd_power(1, 0); + } + + if (blink_mask) { + blink_timer--; + if (!blink_timer) { + blink_timer = sysCfg.blinktime; + blink_counter--; + if (!blink_counter) { + stop_all_power_blink(); + } else { + blink_power ^= 1; + power_now = (power & (0xFF ^ blink_mask)) | ((blink_power) ? blink_mask : 0); + setRelay(power_now); + } + } + } + +#ifdef USE_WS2812 + if (pin[GPIO_WS2812] < 99) ws2812_animate(); +#endif // USE_WS2812 + + if ((sysCfg.module == SONOFF_DUAL) || (sysCfg.module == CH4)) { + if (ButtonCode) { + snprintf_P(log, sizeof(log), PSTR("APP: Button code %04X"), ButtonCode); + addLog(LOG_LEVEL_DEBUG, log); + button = PRESSED; + if (ButtonCode == 0xF500) holdcount = (STATES *4) -1; + ButtonCode = 0; + } else { + button = NOT_PRESSED; + } + } else { + button = digitalRead(pin[GPIO_KEY1]); + } + if ((button == PRESSED) && (lastbutton[0] == NOT_PRESSED)) { + multipress = (multiwindow) ? multipress +1 : 1; + snprintf_P(log, sizeof(log), PSTR("APP: Multipress %d"), multipress); + addLog(LOG_LEVEL_DEBUG, log); + blinks = 201; + multiwindow = STATES /2; // 1/2 second multi press window + } + lastbutton[0] = button; + if (button == NOT_PRESSED) { + holdcount = 0; + } else { + holdcount++; + if (holdcount == (STATES *4)) { // 4 seconds button hold + snprintf_P(scmnd, sizeof(scmnd), PSTR("reset 1")); + multipress = 0; + do_cmnd(scmnd); + } + } + if (multiwindow) { + multiwindow--; + } else { + if ((!restartflag) && (!holdcount) && (multipress > 0) && (multipress < MAX_BUTTON_COMMANDS +3)) { + if ((sysCfg.module == SONOFF_DUAL) || (sysCfg.module == CH4)) { + flag = ((multipress == 1) || (multipress == 2)); + } else { + flag = (multipress == 1); + } + if (flag && sysCfg.mqtt_enabled && mqttClient.connected() && strcmp(sysCfg.button_topic, "0")) { + send_button_power(0, multipress, 2); // Execute command via MQTT using ButtonTopic to sync external clients + } else + { + if ((multipress == 1) || (multipress == 2)) { + if (WIFI_State()) { // WPSconfig, Smartconfig or Wifimanager active + restartflag = 1; + } else { + do_cmnd_power(multipress, 2); // Execute command internally + } + } else { + snprintf_P(scmnd, sizeof(scmnd), commands[multipress -3]); + do_cmnd(scmnd); + } + } + multipress = 0; + } + } + + for (byte i = 1; i < Maxdevice; i++) if (pin[GPIO_KEY1 +i] < 99) { + button = digitalRead(pin[GPIO_KEY1 +i]); + if ((button == PRESSED) && (lastbutton[i] == NOT_PRESSED)) { + if (sysCfg.mqtt_enabled && mqttClient.connected() && strcmp(sysCfg.button_topic, "0")) { + send_button_power(0, i +1, 2); // Execute commend via MQTT + } else { + do_cmnd_power(i +1, 2); // Execute command internally + } + } + lastbutton[i] = button; + } + + for (byte i = 0; i < Maxdevice; i++) if (pin[GPIO_SWT1 +i] < 99) { + button = digitalRead(pin[GPIO_SWT1 +i]); + if (button != lastwallswitch[i]) { + switchflag = 3; + switch (sysCfg.switchmode) { + case TOGGLE: + switchflag = 2; // Toggle + break; + case FOLLOW: + switchflag = button & 0x01; // Follow wall switch state + break; + case FOLLOW_INV: + switchflag = ~button & 0x01; // Follow inverted wall switch state + break; + case PUSHBUTTON: + if ((button == PRESSED) && (lastwallswitch[i] == NOT_PRESSED)) switchflag = 2; // Toggle with pushbutton to Gnd + break; + case PUSHBUTTON_INV: + if ((button == NOT_PRESSED) && (lastwallswitch[i] == PRESSED)) switchflag = 2; // Toggle with releasing pushbutton from Gnd + } + if (switchflag < 3) { + if (sysCfg.mqtt_enabled && mqttClient.connected() && strcmp(sysCfg.switch_topic,"0")) { + send_button_power(1, i +1, switchflag); // Execute commend via MQTT + } else { + do_cmnd_power(i +1, switchflag); // Execute command internally + } + } + lastwallswitch[i] = button; + } + } + + if (!(state % ((STATES/10)*2))) { + if (blinks || restartflag || otaflag) { + if (restartflag || otaflag) { + blinkstate = 1; // Stay lit + } else { + blinkstate ^= 1; // Blink + } + if ((!(sysCfg.ledstate &0x08)) && ((sysCfg.ledstate &0x06) || (blinks > 200) || (blinkstate))) { + setLed(blinkstate); + } + if (!blinkstate) { + blinks--; + if (blinks == 200) blinks = 0; + } + } else { + if (sysCfg.ledstate &0x01) setLed(power); + } + } + + switch (state) { + case (STATES/10)*2: + if (otaflag) { + otaflag--; + if (otaflag <= 0) { + otaflag = 12; + ESPhttpUpdate.rebootOnUpdate(false); + // Try multiple times to get the update, in case we have a transient issue. + // e.g. Someone issued "cmnd/sonoffs/update 1" and all the devices + // are hammering the OTAURL. + for (byte i = 0; i < OTA_ATTEMPTS && !otaok; i++) { + // Delay an increasing pseudo-random time for each interation. + // Starting at 0 (no delay) up to a maximum of OTA_ATTEMPTS-1 seconds. + delay((ESP.getChipId() % 1000) * i); + otaok = (ESPhttpUpdate.update(sysCfg.otaUrl) == HTTP_UPDATE_OK); + } + } + if (otaflag == 10) { // Allow MQTT to reconnect + otaflag = 0; + snprintf_P(stopic, sizeof(stopic), PSTR("%s/%s/UPGRADE"), PUB_PREFIX, sysCfg.mqtt_topic); + if (otaok) { + snprintf_P(svalue, sizeof(svalue), PSTR("Successful. Restarting")); + restartflag = 2; + } else { + snprintf_P(svalue, sizeof(svalue), PSTR("Failed %s"), ESPhttpUpdate.getLastErrorString().c_str()); + } + mqtt_publish(stopic, svalue); + } + } + break; + case (STATES/10)*4: + if (savedatacounter) { + savedatacounter--; + if (savedatacounter <= 0) { + if (sysCfg.savestate) { + if (!((sysCfg.pulsetime > 0) && (sysCfg.pulsetime < 30) && ((sysCfg.power &0xFE) == (power &0xFE)))) sysCfg.power = power; + } + CFG_Save(); + savedatacounter = sysCfg.savedata; + } + } + if (restartflag) { + if (restartflag == 211) { + CFG_Default(); + restartflag = 2; + } + if (restartflag == 212) { + CFG_Erase(); + CFG_Default(); + restartflag = 2; + } + if (sysCfg.savestate) sysCfg.power = power; + if (hlw_flg) hlw_savestate(); + CFG_Save(); + restartflag--; + if (restartflag <= 0) { + addLog_P(LOG_LEVEL_INFO, PSTR("APP: Restarting")); + ESP.restart(); + } + } + break; + case (STATES/10)*6: + WIFI_Check(wificheckflag); + wificheckflag = WIFI_RESTART; + break; + case (STATES/10)*8: + if (WiFi.status() == WL_CONNECTED) { + if (sysCfg.mqtt_enabled) { + if (!mqttClient.connected()) { + if (!mqttcounter) { + mqtt_reconnect(); + } else { + mqttcounter--; + } + } + } else { + if (!mqttcounter) { + mqtt_reconnect(); + } + } + } + break; + } +} + +void serial() +{ + char log[LOGSZ]; + + while (Serial.available()) { + yield(); + SerialInByte = Serial.read(); + + // Sonoff dual 19200 baud serial interface + if (Hexcode) { + Hexcode--; + if (Hexcode) { + ButtonCode = (ButtonCode << 8) | SerialInByte; + SerialInByte = 0; + } else { + if (SerialInByte != 0xA1) ButtonCode = 0; // 0xA1 - End of Sonoff dual button code + } + } + if (SerialInByte == 0xA0) { // 0xA0 - Start of Sonoff dual button code + SerialInByte = 0; + ButtonCode = 0; + Hexcode = 3; + } + + if (SerialInByte > 127) { // binary data... + SerialInByteCounter = 0; + Serial.flush(); + return; + } + if (isprint(SerialInByte)) { + if (SerialInByteCounter < INPUT_BUFFER_SIZE) { // add char to string if it still fits + serialInBuf[SerialInByteCounter++] = SerialInByte; + } else { + SerialInByteCounter = 0; + } + } + if (SerialInByte == '\n') { + serialInBuf[SerialInByteCounter] = 0; // serial data completed + if (seriallog_level < LOG_LEVEL_INFO) seriallog_level = LOG_LEVEL_INFO; + snprintf_P(log, sizeof(log), PSTR("CMND: %s"), serialInBuf); + addLog(LOG_LEVEL_INFO, log); + do_cmnd(serialInBuf); + SerialInByteCounter = 0; + Serial.flush(); + return; + } + } +} + +/********************************************************************************************/ + +void GPIO_init() +{ + char log[LOGSZ]; + uint8_t mpin; + mytmplt def_module; + + if (!sysCfg.module) sysCfg.module = SONOFF_BASIC; // Sonoff Basic + + memcpy_P(&def_module, &modules[sysCfg.module], sizeof(def_module)); + strlcpy(my_module.name, def_module.name, sizeof(my_module.name)); + for (byte i = 0; i < MAX_GPIO_PIN; i++) { + if (sysCfg.my_module.gp.io[i] > GPIO_NONE) my_module.gp.io[i] = sysCfg.my_module.gp.io[i]; + if ((def_module.gp.io[i] > GPIO_NONE) && (def_module.gp.io[i] < GPIO_USER)) my_module.gp.io[i] = def_module.gp.io[i]; + } + for (byte i = 0; i < GPIO_MAX; i++) pin[i] = 99; + for (byte i = 0; i < MAX_GPIO_PIN; i++) { + mpin = my_module.gp.io[i]; + +// snprintf_P(log, sizeof(log), PSTR("DBG: gpio pin %d, mpin %d"), i, mpin); +// addLog(LOG_LEVEL_DEBUG, log); + + if (mpin) { + if ((mpin >= GPIO_REL1_INV) && (mpin <= GPIO_REL4_INV)) { + rel_inverted[mpin - GPIO_REL1_INV] = 1; + mpin -= 4; + } + else if ((mpin >= GPIO_LED1_INV) && (mpin <= GPIO_LED4_INV)) { + led_inverted[mpin - GPIO_LED1_INV] = 1; + mpin -= 4; + } + else if (mpin == GPIO_DHT11) dht_type = DHT11; + else if (mpin == GPIO_DHT21) { + dht_type = DHT21; + mpin--; + } + else if (mpin == GPIO_DHT22) { + dht_type = DHT22; + mpin -= 2; + } + pin[mpin] = i; + } + } + + Maxdevice = 1; + switch (sysCfg.module) { + case SONOFF_DUAL: + case ELECTRODRAGON: + Maxdevice = 2; + break; + case SONOFF_4CH: + case CH4: + Maxdevice = 4; + break; + } + + swt_flg = ((pin[GPIO_SWT1] < 99) || (pin[GPIO_SWT2] < 99) || (pin[GPIO_SWT3] < 99) || (pin[GPIO_SWT4] < 99)); + + if ((sysCfg.module == SONOFF_DUAL) || (sysCfg.module == CH4)) { + Baudrate = 19200; + } else { + if (sysCfg.module == SONOFF_LED) { + for (byte i = 0; i < 5; i++) { + if (pin[GPIO_PWM0 +i] < 99) pinMode(pin[GPIO_PWM0 +i], OUTPUT); + } + } else { + for (byte i = 0; i < 4; i++) { + if (pin[GPIO_KEY1 +i] < 99) pinMode(pin[GPIO_KEY1 +i], INPUT_PULLUP); + if (pin[GPIO_REL1 +i] < 99) pinMode(pin[GPIO_REL1 +i], OUTPUT); + } + } + } + for (byte i = 0; i < 4; i++) { + if (pin[GPIO_LED1 +i] < 99) { + pinMode(pin[GPIO_LED1 +i], OUTPUT); + digitalWrite(pin[GPIO_LED1 +i], led_inverted[i]); + } + if (pin[GPIO_SWT1 +i] < 99) { + pinMode(pin[GPIO_SWT1 +i], INPUT_PULLUP); + lastwallswitch[i] = digitalRead(pin[GPIO_SWT1 +i]); // set global now so doesn't change the saved power state on first switch check + } + } + setLed(sysCfg.ledstate &8); + + hlw_flg = ((pin[GPIO_HLW_SEL] < 99) && (pin[GPIO_HLW_CF1] < 99) && (pin[GPIO_HLW_CF] < 99)); + if (hlw_flg) hlw_init(); + +#if defined(USE_DHT) || defined(USE_DHT2) + if (dht_type) dht_init(); +#endif // USE_DHT/2 + +#ifdef USE_I2C + i2c_flg = ((pin[GPIO_I2C_SCL] < 99) && (pin[GPIO_I2C_SDA] < 99)); + if (i2c_flg) Wire.begin(pin[GPIO_I2C_SDA],pin[GPIO_I2C_SCL]); +#endif // USE_I2C + +#ifdef USE_WS2812 + if (pin[GPIO_WS2812] < 99) ws2812_init(); +#endif // USE_WS2812 +} + +void setup() +{ + char log[LOGSZ]; + byte idx; + + Serial.begin(Baudrate); + delay(10); + Serial.println(); + seriallog_level = LOG_LEVEL_INFO; // Allow specific serial messages until config loaded + + snprintf_P(Version, sizeof(Version), PSTR("%d.%d.%d"), VERSION >> 24 & 0xff, VERSION >> 16 & 0xff, VERSION >> 8 & 0xff); + if (VERSION & 0x1f) { + idx = strlen(Version); + Version[idx] = 96 + (VERSION & 0x1f); + Version[idx +1] = 0; + } + if (!spiffsPresent()) + addLog_P(LOG_LEVEL_ERROR, PSTR("SPIFFS: ERROR - No spiffs present. Please reflash with at least 16K SPIFFS")); +#ifdef USE_SPIFFS + initSpiffs(); +#endif + CFG_Load(); + CFG_Delta(); + + sysCfg.bootcount++; + snprintf_P(log, sizeof(log), PSTR("APP: Bootcount %d"), sysCfg.bootcount); + addLog(LOG_LEVEL_DEBUG, log); + savedatacounter = sysCfg.savedata; + seriallog_timer = SERIALLOG_TIMER; + seriallog_level = sysCfg.seriallog_level; + syslog_level = sysCfg.syslog_level; + + GPIO_init(); + + if (Serial.baudRate() != Baudrate) { + if (seriallog_level) { + snprintf_P(log, sizeof(log), PSTR("APP: Change baudrate to %d and Serial logging will be disabled in %d seconds"), Baudrate, seriallog_timer); + addLog(LOG_LEVEL_INFO, log); + } + delay(100); + Serial.flush(); + Serial.begin(Baudrate); + delay(10); + Serial.println(); + } + + if (strstr(sysCfg.hostname, "%")) strlcpy(sysCfg.hostname, DEF_WIFI_HOSTNAME, sizeof(sysCfg.hostname)); + if (!strcmp(sysCfg.hostname, DEF_WIFI_HOSTNAME)) { + snprintf_P(Hostname, sizeof(Hostname)-1, sysCfg.hostname, sysCfg.mqtt_topic, ESP.getChipId() & 0x1FFF); + } else { + snprintf_P(Hostname, sizeof(Hostname)-1, sysCfg.hostname); + } + WIFI_Connect(Hostname); + + getClient(MQTTClient, sysCfg.mqtt_client, sizeof(MQTTClient)); + + if (sysCfg.module == MOTOR) sysCfg.poweronstate = 1; // Needs always on else in limbo! + if (ESP.getResetReason() == "Power on") { + if (sysCfg.poweronstate == 0) { // All off + power = 0; + setRelay(power); + } + else if (sysCfg.poweronstate == 1) { // All on + power = ((0x00FF << Maxdevice) >> 8); + setRelay(power); + } + else if (sysCfg.poweronstate == 2) { // All saved state toggle + power = (sysCfg.power & ((0x00FF << Maxdevice) >> 8)) ^ 0xFF; + if (sysCfg.savestate) setRelay(power); + } + else if (sysCfg.poweronstate == 3) { // All saved state + power = sysCfg.power & ((0x00FF << Maxdevice) >> 8); + if (sysCfg.savestate) setRelay(power); + } + } else { + power = sysCfg.power & ((0x00FF << Maxdevice) >> 8); + if (sysCfg.savestate) setRelay(power); + } + blink_powersave = power; + + rtc_init(every_second_cb); + + snprintf_P(log, sizeof(log), PSTR("APP: Project %s %s (Topic %s, Fallback %s, GroupTopic %s) Version %s"), + PROJECT, sysCfg.friendlyname[0], sysCfg.mqtt_topic, MQTTClient, sysCfg.mqtt_grptopic, Version); + addLog(LOG_LEVEL_INFO, log); +} + +void loop() +{ +#ifdef USE_WEBSERVER + pollDnsWeb(); +#endif // USE_WEBSERVER + +#if defined(USE_WEMO_EMULATION) || defined(USE_HUE_EMULATION) + pollUDP(); +#endif // USE_WEMO_EMULATION || USE_HUE_EMULATION + + if (millis() >= timerxs) stateloop(); + if (sysCfg.mqtt_enabled) mqttClient.loop(); + if (Serial.available()) serial(); + yield(); + + if (sysCfg.sleep) delay(sysCfg.sleep); +} diff --git a/sonoff/sonoff_template.h b/sonoff/sonoff_template.h new file mode 100644 index 000000000..315f26b3f --- /dev/null +++ b/sonoff/sonoff_template.h @@ -0,0 +1,248 @@ +/*********************************************************************************************\ + * Template parameters +\*********************************************************************************************/ + +#define GPIO_SENSOR_START 0 +#define GPIO_NONE 0 // Not used +#define GPIO_DHT11 1 // DHT11 +#define GPIO_DHT21 2 // DHT21 +#define GPIO_AM2301 2 // AM2301 +#define GPIO_DHT22 3 // DHT22 +#define GPIO_AM2302 3 // AM2302 +#define GPIO_AM2321 3 // AM2321 +#define GPIO_DSB 4 // Single wire DS18B20 or DS18S20 +#define GPIO_I2C_SCL 5 // I2C SCL +#define GPIO_I2C_SDA 6 // I2C SDA +#define GPIO_WS2812 7 // WS2812 Led string +#define GPIO_SWT1 8 // User connected external switches +#define GPIO_SENSOR_END 9 + +#define GPIO_SWT2 9 +#define GPIO_SWT3 10 +#define GPIO_SWT4 11 +#define GPIO_KEY1 12 // Button usually connected to GPIO0 +#define GPIO_KEY2 13 +#define GPIO_KEY3 14 +#define GPIO_KEY4 15 +#define GPIO_REL1 16 // Relays +#define GPIO_REL2 17 +#define GPIO_REL3 18 +#define GPIO_REL4 19 +#define GPIO_REL1_INV 20 +#define GPIO_REL2_INV 21 +#define GPIO_REL3_INV 22 +#define GPIO_REL4_INV 23 +#define GPIO_LED1 24 // Leds +#define GPIO_LED2 25 +#define GPIO_LED3 26 +#define GPIO_LED4 27 +#define GPIO_LED1_INV 28 +#define GPIO_LED2_INV 29 +#define GPIO_LED3_INV 30 +#define GPIO_LED4_INV 31 +#define GPIO_PWM0 32 // Cold +#define GPIO_PWM1 33 // Warm +#define GPIO_PWM2 34 // Red (swapped with Blue from original) +#define GPIO_PWM3 35 // Green +#define GPIO_PWM4 36 // Blue (swapped with Red from original) +#define GPIO_RXD 37 // Serial interface +#define GPIO_TXD 38 // Serial interface +#define GPIO_HLW_SEL 39 // HLW8012 Sel output (Sonoff Pow) +#define GPIO_HLW_CF1 40 // HLW8012 CF1 voltage / current (Sonoff Pow) +#define GPIO_HLW_CF 41 // HLW8012 CF power (Sonoff Pow) +#define GPIO_USER 42 // User configurable +#define GPIO_MAX 43 + +/********************************************************************************************/ + +enum module_t {SONOFF_BASIC, SONOFF_RF, SONOFF_SV, SONOFF_TH, SONOFF_DUAL, SONOFF_POW, SONOFF_4CH, S20, SLAMPHER, SONOFF_TOUCH, SONOFF_LED, CH1, CH4, MOTOR, ELECTRODRAGON, USER_TEST, MAXMODULE}; + +/********************************************************************************************/ + +#define MAX_GPIO_PIN 17 // Number of supported GPIO + +#define DHT11 11 +#define DHT21 21 +#define AM2301 21 +#define DHT22 22 +#define AM2302 22 +#define AM2321 22 + +typedef struct MYIO { + uint8_t io[MAX_GPIO_PIN]; +} myio; + +typedef struct MYTMPLT { + char name[16]; + myio gp; +} mytmplt; + +const char sensors[GPIO_SENSOR_END][8] PROGMEM = + { "None", "DHT11", "AM2301", "DHT22", "DS18x20", "I2C SCL", "I2C SDA", "WS2812", "Switch" }; + +const mytmplt modules[MAXMODULE] PROGMEM = { + { "Sonoff Basic", // Sonoff Basic + GPIO_KEY1, // GPIO00 Button + 0, 0, + GPIO_USER, // GPIO03 Serial TXD and Optional sensor + GPIO_USER, // GPIO04 Optional sensor + 0, 0, 0, 0, 0, 0, 0, + GPIO_REL1, // GPIO12 Red Led and Relay (0 = Off, 1 = On) + GPIO_LED1_INV, // GPIO13 Green Led (0 = On, 1 = Off) + GPIO_USER, // GPIO14 Optional sensor + 0, 0 + }, + { "Sonoff RF", // Sonoff RF + GPIO_KEY1, // GPIO00 Button + 0, 0, + GPIO_USER, // GPIO03 Serial TXD and Optional sensor + GPIO_USER, // GPIO04 Optional sensor + 0, 0, 0, 0, 0, 0, 0, + GPIO_REL1, // GPIO12 Red Led and Relay (0 = Off, 1 = On) + GPIO_LED1_INV, // GPIO13 Green Led (0 = On, 1 = Off) + GPIO_USER, // GPIO14 Optional sensor + 0, 0 + }, + { "Sonoff SV", // Sonoff SV + GPIO_KEY1, // GPIO00 Button + 0, 0, + GPIO_USER, // GPIO03 Serial TXD and Optional sensor + GPIO_USER, // GPIO04 Optional sensor + 0, 0, 0, 0, 0, 0, 0, + GPIO_REL1, // GPIO12 Red Led and Relay (0 = Off, 1 = On) + GPIO_LED1_INV, // GPIO13 Green Led (0 = On, 1 = Off) + GPIO_USER, // GPIO14 Optional sensor + 0, 0 + }, + { "Sonoff TH", // Sonoff TH10/16 + GPIO_KEY1, // GPIO00 Button + 0, 0, + GPIO_USER, // GPIO03 Serial TXD and Optional sensor + GPIO_USER, // GPIO04 Optional sensor + 0, 0, 0, 0, 0, 0, 0, + GPIO_REL1, // GPIO12 Red Led and Relay (0 = Off, 1 = On) + GPIO_LED1_INV, // GPIO13 Green Led (0 = On, 1 = Off) + GPIO_USER, // GPIO14 Optional sensor + 0, 0 + }, + { "Sonoff Dual", // Sonoff Dual + 0, + GPIO_TXD, // GPIO01 Relay control + 0, + GPIO_RXD, // GPIO03 Relay control + 0, 0, 0, 0, 0, 0, 0, 0, 0, + GPIO_LED1_INV, // GPIO13 Blue Led (0 = On, 1 = Off) + 0, 0, 0 + }, + { "Sonoff Pow", // Sonoff Pow + GPIO_KEY1, // GPIO00 Button + 0, 0, 0, 0, + GPIO_HLW_SEL, // GPIO05 HLW8012 Sel output + 0, 0, 0, 0, 0, 0, + GPIO_REL1, // GPIO12 Red Led and Relay (0 = Off, 1 = On) + GPIO_HLW_CF1, // GPIO13 HLW8012 CF1 voltage / current + GPIO_HLW_CF, // GPIO14 HLW8012 CF power + GPIO_LED1, // GPIO15 Green Led (0 = On, 1 = Off) + 0 + }, + { "Sonoff 4CH", // Sonoff 4CH + GPIO_KEY1, // GPIO00 Button 1 + 0, + GPIO_USER, // GPIO02 Optional sensor + 0, + GPIO_REL3, // GPIO04 Sonoff 4CH Red Led and Relay 3 (0 = Off, 1 = On) + GPIO_REL2, // GPIO05 Sonoff 4CH Red Led and Relay 2 (0 = Off, 1 = On) + 0, + GPIO_USER, // GPIO07 Optional sensor + GPIO_USER, // GPIO08 Optional sensor + GPIO_KEY2, // GPIO09 Button 2 + GPIO_KEY3, // GPIO10 Button 3 + 0, + GPIO_REL1, // GPIO12 Red Led and Relay 1 (0 = Off, 1 = On) + GPIO_LED1_INV, // GPIO13 Green Led (0 = On, 1 = Off) + GPIO_KEY4, // GPIO14 Button 4 + GPIO_REL4, // GPIO15 Red Led and Relay 4 (0 = Off, 1 = On) + 0 + }, + { "S20 Socket", // S20 Smart Socket + GPIO_KEY1, // GPIO00 Button + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + GPIO_REL1, // GPIO12 Red Led and Relay (0 = Off, 1 = On) + GPIO_LED1_INV, // GPIO13 Green Led (0 = On, 1 = Off) + 0, 0, 0 + }, + { "Slampher", // Slampher + GPIO_KEY1, // GPIO00 Button + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + GPIO_REL1, // GPIO12 Red Led and Relay (0 = Off, 1 = On) + GPIO_LED1_INV, // GPIO13 Blue Led (0 = On, 1 = Off) + 0, 0, 0 + }, + { "Sonoff Touch", // Sonoff Touch + GPIO_KEY1, // GPIO00 Button + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + GPIO_REL1, // GPIO12 Red Led and Relay (0 = Off, 1 = On) + GPIO_LED1_INV, // GPIO13 Blue Led (0 = On, 1 = Off) + 0, 0, 0 + }, + { "Sonoff LED", // Sonoff LED + GPIO_KEY1, // GPIO00 Button + 0, 0, 0, + GPIO_PWM3, // GPIO04 Green light + GPIO_PWM2, // GPIO05 Red light + 0, 0, 0, 0, 0, 0, + GPIO_PWM0, // GPIO12 Cold light + GPIO_LED1_INV, // GPIO13 Blue Led (0 = On, 1 = Off) + GPIO_PWM1, // GPIO14 Warm light + GPIO_PWM4, // GPIO15 Blue light + 0 + }, + { "1 Channel", // 1 Channel Inching/Latching Relay + GPIO_KEY1, // GPIO00 Button + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + GPIO_REL1, // GPIO12 Red Led and Relay (0 = Off, 1 = On) + GPIO_LED1_INV, // GPIO13 Green Led (0 = On, 1 = Off) + 0, 0, 0 + }, + { "4 Channel", // 4 Channel Inching/Latching Relays + 0, + GPIO_TXD, // GPIO01 Relay control + 0, + GPIO_RXD, // GPIO03 Relay control + 0, 0, 0, 0, 0, 0, 0, 0, 0, + GPIO_LED1_INV, // GPIO13 Blue Led (0 = On, 1 = Off) + 0, 0, 0 + }, + { "Motor C/AC", // Motor Clockwise / Anti clockwise + GPIO_KEY1, // GPIO00 Button + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + GPIO_REL1, // GPIO12 Red Led and Relay (0 = Off, 1 = On) + GPIO_LED1_INV, // GPIO13 Green Led (0 = On, 1 = Off) + 0, 0, 0 + }, + { "ElectroDragon", // ElectroDragon IoT Relay Board + GPIO_KEY2, // GPIO00 Button 2 + 0, + GPIO_KEY1, // GPIO02 Button 1 + GPIO_USER, // GPIO03 Serial TXD and Optional sensor + GPIO_USER, // GPIO04 Optional sensor + 0, 0, 0, 0, 0, 0, 0, + GPIO_REL2, // GPIO12 Red Led and Relay 2 (0 = Off, 1 = On) + GPIO_REL1, // GPIO13 Red Led and Relay 1 (0 = Off, 1 = On) + GPIO_USER, // GPIO14 Optional sensor + 0, + GPIO_LED1 // GPIO16 Green/Blue Led (1 = On, 0 = Off) + }, + { "User Test", // Sonoff Basic User Test + GPIO_KEY1, // GPIO00 Button + 0, 0, + GPIO_USER, // GPIO03 Serial TXD and Optional sensor + GPIO_USER, // GPIO04 Optional sensor + 0, 0, 0, 0, 0, 0, 0, + GPIO_REL1, // GPIO12 Red Led and Relay (0 = Off, 1 = On) + GPIO_LED1_INV, // GPIO13 Green Led (0 = On, 1 = Off) + GPIO_USER, // GPIO14 Optional sensor + 0, 0 + } +}; + diff --git a/sonoff/support.h b/sonoff/support.h new file mode 100644 index 000000000..b33a56a5f --- /dev/null +++ b/sonoff/support.h @@ -0,0 +1,17 @@ +#ifdef __cplusplus +extern "C" { +#endif + +#ifndef __SUPPORT_H__ +#define __SUPPORT_H__ + +#include "user_interface.h" + +/* Function prototypes. */ +void WIFI_wps_status_cb(wps_cb_status status); + +#endif // ifndef __SUPPORT_H__ + +#ifdef __cplusplus +} +#endif diff --git a/sonoff/support.ino b/sonoff/support.ino new file mode 100644 index 000000000..21585b824 --- /dev/null +++ b/sonoff/support.ino @@ -0,0 +1,1005 @@ +/* +Copyright (c) 2017 Theo Arends. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +- Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. +- Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. +*/ + +/*********************************************************************************************\ + * Config - Flash or Spiffs +\*********************************************************************************************/ + +extern "C" { +#include "spi_flash.h" +} + +#define SPIFFS_START ((uint32_t)&_SPIFFS_start - 0x40200000) / SPI_FLASH_SEC_SIZE +#define SPIFFS_END ((uint32_t)&_SPIFFS_end - 0x40200000) / SPI_FLASH_SEC_SIZE + +// Version 2.x config +#define SPIFFS_CONFIG2 "/config.ini" +#define CFG_LOCATION2 SPIFFS_END - 2 + +// Version 3.x config +#define SPIFFS_CONFIG "/cfg.ini" +#define CFG_LOCATION SPIFFS_END - 4 + +uint32_t _cfgHash = 0; +int spiffsflag = 0; + +boolean spiffsPresent() +{ + return (SPIFFS_END - SPIFFS_START); +} + +uint32_t getHash() +{ + uint32_t hash = 0; + uint8_t *bytes = (uint8_t*)&sysCfg; + + for (uint16_t i = 0; i < sizeof(SYSCFG); i++) hash += bytes[i]*(i+1); + return hash; +} + +/*********************************************************************************************\ + * Config Save - Save parameters to Flash or Spiffs ONLY if any parameter has changed +\*********************************************************************************************/ +void CFG_Save() +{ + char log[LOGSZ]; + + if ((getHash() != _cfgHash) && (spiffsPresent())) { + if (!spiffsflag) { +#ifdef USE_SPIFFS + sysCfg.saveFlag++; + File f = SPIFFS.open(SPIFFS_CONFIG, "r+"); + if (f) { + uint8_t *bytes = (uint8_t*)&sysCfg; + for (int i = 0; i < sizeof(SYSCFG); i++) f.write(bytes[i]); + f.close(); + snprintf_P(log, sizeof(log), PSTR("Config: Saved configuration (%d bytes) to spiffs count %d"), sizeof(SYSCFG), sysCfg.saveFlag); + addLog(LOG_LEVEL_DEBUG, log); + } else { + addLog_P(LOG_LEVEL_ERROR, PSTR("Config: ERROR - Saving configuration failed")); + } + } else { +#endif // USE_SPIFFS + noInterrupts(); + if (sysCfg.saveFlag == 0) { // Handle default and rollover + spi_flash_erase_sector(CFG_LOCATION + (sysCfg.saveFlag &1)); + spi_flash_write((CFG_LOCATION + (sysCfg.saveFlag &1)) * SPI_FLASH_SEC_SIZE, (uint32*)&sysCfg, sizeof(SYSCFG)); + } + sysCfg.saveFlag++; + spi_flash_erase_sector(CFG_LOCATION + (sysCfg.saveFlag &1)); + spi_flash_write((CFG_LOCATION + (sysCfg.saveFlag &1)) * SPI_FLASH_SEC_SIZE, (uint32*)&sysCfg, sizeof(SYSCFG)); + interrupts(); + snprintf_P(log, sizeof(log), PSTR("Config: Saved configuration (%d bytes) to flash at %X and count %d"), sizeof(SYSCFG), CFG_LOCATION + (sysCfg.saveFlag &1), sysCfg.saveFlag); + addLog(LOG_LEVEL_DEBUG, log); + } + _cfgHash = getHash(); + } +} + +void CFG_Load() +{ + char log[LOGSZ]; + + if (spiffsPresent()) { + if (!spiffsflag) { +#ifdef USE_SPIFFS + File f = SPIFFS.open(SPIFFS_CONFIG, "r+"); + if (f) { + uint8_t *bytes = (uint8_t*)&sysCfg; + for (int i = 0; i < sizeof(SYSCFG); i++) bytes[i] = f.read(); + f.close(); + snprintf_P(log, sizeof(log), PSTR("Config: Loaded configuration from spiffs count %d"), sysCfg.saveFlag); + addLog(LOG_LEVEL_DEBUG, log); + } else { + addLog_P(LOG_LEVEL_ERROR, PSTR("Config: ERROR - Loading configuration failed")); + } + } else { +#endif // USE_SPIFFS + struct SYSCFGH { + unsigned long cfg_holder; + unsigned long saveFlag; + } _sysCfgH; + + noInterrupts(); + spi_flash_read((CFG_LOCATION) * SPI_FLASH_SEC_SIZE, (uint32*)&sysCfg, sizeof(SYSCFG)); + spi_flash_read((CFG_LOCATION + 1) * SPI_FLASH_SEC_SIZE, (uint32*)&_sysCfgH, sizeof(SYSCFGH)); + if (sysCfg.saveFlag < _sysCfgH.saveFlag) + spi_flash_read((CFG_LOCATION + 1) * SPI_FLASH_SEC_SIZE, (uint32*)&sysCfg, sizeof(SYSCFG)); + interrupts(); + snprintf_P(log, sizeof(log), PSTR("Config: Loaded configuration from flash at %X and count %d"), CFG_LOCATION + (sysCfg.saveFlag &1), sysCfg.saveFlag); + addLog(LOG_LEVEL_DEBUG, log); + } + } +// snprintf_P(log, sizeof(log), PSTR("Config: Check 1 for migration (%08X)"), sysCfg.version); +// addLog(LOG_LEVEL_NONE, log); + if (sysCfg.cfg_holder != CFG_HOLDER) { + if ((sysCfg.version < 0x03000000) || (sysCfg.version > 0x73000000)) { + CFG_Migrate(); // Config may be present with versions below 3.0.0 + } else { + CFG_Default(); + } + } + _cfgHash = getHash(); +} + +void CFG_Migrate() +{ + char log[LOGSZ]; + + if (spiffsPresent()) { + if (!spiffsflag) { +#ifdef USE_SPIFFS + File f = SPIFFS.open(SPIFFS_CONFIG2, "r+"); + if (f) { + uint8_t *bytes = (uint8_t*)&sysCfg2; + for (int i = 0; i < sizeof(SYSCFG2); i++) bytes[i] = f.read(); + f.close(); + snprintf_P(log, sizeof(log), PSTR("Config: Loaded previous configuration from spiffs count %d"), sysCfg2.saveFlag); + addLog(LOG_LEVEL_DEBUG, log); + } else { + addLog_P(LOG_LEVEL_ERROR, PSTR("Config: ERROR - Loading previous configuration failed")); + } + } else { +#endif // USE_SPIFFS + struct SYSCFGH { + unsigned long cfg_holder; + unsigned long saveFlag; + } _sysCfgH; + + noInterrupts(); + spi_flash_read((CFG_LOCATION2) * SPI_FLASH_SEC_SIZE, (uint32*)&sysCfg2, sizeof(SYSCFG2)); + spi_flash_read((CFG_LOCATION2 + 1) * SPI_FLASH_SEC_SIZE, (uint32*)&_sysCfgH, sizeof(SYSCFGH)); + if (sysCfg2.saveFlag < _sysCfgH.saveFlag) + spi_flash_read((CFG_LOCATION2 + 1) * SPI_FLASH_SEC_SIZE, (uint32*)&sysCfg2, sizeof(SYSCFG2)); + interrupts(); + snprintf_P(log, sizeof(log), PSTR("Config: Loaded previous configuration from flash at %X and count %d"), CFG_LOCATION2 + (sysCfg2.saveFlag &1), sysCfg2.saveFlag); + addLog(LOG_LEVEL_DEBUG, log); + } + } +// snprintf_P(log, sizeof(log), PSTR("Config: Check 2 for migration (%08X)"), sysCfg2.version); +// addLog(LOG_LEVEL_NONE, log); + if ((sysCfg2.version > 0x01000000) && (sysCfg2.version < 0x03000000)) { + CFG_Migrate_Part2(); // Config is present between version 1.0.0 and 3.0.0 + } else { + CFG_Default(); + } + _cfgHash = getHash(); +} + +void CFG_Erase() +{ + char log[LOGSZ]; + SpiFlashOpResult result; + + uint32_t _sectorStart = (ESP.getSketchSize() / SPI_FLASH_SEC_SIZE) + 1; + uint32_t _sectorEnd = ESP.getFlashChipRealSize() / SPI_FLASH_SEC_SIZE; + boolean _serialoutput = (LOG_LEVEL_DEBUG_MORE <= seriallog_level); + + snprintf_P(log, sizeof(log), PSTR("Config: Erasing %d flash sectors"), _sectorEnd - _sectorStart); + addLog(LOG_LEVEL_DEBUG, log); + + for (uint32_t _sector = _sectorStart; _sector < _sectorEnd; _sector++) { + noInterrupts(); + result = spi_flash_erase_sector(_sector); + interrupts(); + if (_serialoutput) { + Serial.print(F("Flash: Erased sector ")); + Serial.print(_sector); + if (result == SPI_FLASH_RESULT_OK) { + Serial.println(F(" OK")); + } else { + Serial.println(F(" Error")); + } + delay(10); + } + } +} + +void CFG_Dump() +{ + #define CFG_COLS 16 + + char log[LOGSZ]; + uint16_t idx, maxrow, row, col; + + uint8_t *buffer = (uint8_t *) &sysCfg; + maxrow = ((sizeof(SYSCFG)+CFG_COLS)/CFG_COLS); + + for (row = 0; row < maxrow; row++) { + idx = row * CFG_COLS; + snprintf_P(log, sizeof(log), PSTR("%04X:"), idx); + for (col = 0; col < CFG_COLS; col++) { + if (!(col%4)) snprintf_P(log, sizeof(log), PSTR("%s "), log); + snprintf_P(log, sizeof(log), PSTR("%s %02X"), log, buffer[idx + col]); + } + snprintf_P(log, sizeof(log), PSTR("%s |"), log); + for (col = 0; col < CFG_COLS; col++) { +// if (!(col%4)) snprintf_P(log, sizeof(log), PSTR("%s "), log); + snprintf_P(log, sizeof(log), PSTR("%s%c"), log, ((buffer[idx + col] > 0x20) && (buffer[idx + col] < 0x7F)) ? (char)buffer[idx + col] : ' '); + } + snprintf_P(log, sizeof(log), PSTR("%s|"), log); + addLog(LOG_LEVEL_INFO, log); + } +} + +#ifdef USE_SPIFFS +void initSpiffs() +{ + spiffsflag = 0; + if (!spiffsPresent()) { + spiffsflag = 1; + } else { + if (!SPIFFS.begin()) { + addLog_P(LOG_LEVEL_ERROR, PSTR("SPIFFS: WARNING - Failed to mount file system. Will use flash")); + spiffsflag = 2; + } else { + addLog_P(LOG_LEVEL_DEBUG, PSTR("SPIFFS: Mount successful")); + File f = SPIFFS.open(SPIFFS_CONFIG, "r"); + if (!f) { + addLog_P(LOG_LEVEL_DEBUG, PSTR("SPIFFS: Formatting...")); + SPIFFS.format(); + addLog_P(LOG_LEVEL_DEBUG, PSTR("SPIFFS: Formatted")); + File f = SPIFFS.open(SPIFFS_CONFIG, "w"); + if (f) { + for (int i = 0; i < sizeof(SYSCFG); i++) f.write(0); + f.close(); + } else { + addLog_P(LOG_LEVEL_ERROR, PSTR("SPIFFS: WARNING - Failed to init config file. Will use flash")); + spiffsflag = 3; + } + } + } + } +} +#endif // USE_SPIFFS + +/* +void setFlashChipMode(byte mode) +{ + char log[LOGSZ]; + uint32_t data; + + uint8_t * bytes = (uint8_t *) &data; + // read first 4 byte (magic byte + flash config) + if (spi_flash_read(0x0000, &data, 4) == SPI_FLASH_RESULT_OK) { + + snprintf_P(log, sizeof(log), PSTR("FLSH: Magic byte and flash config %08X"), data); + addLog(LOG_LEVEL_DEBUG, log); + + if (bytes[2] != mode) { + bytes[2] = mode &3; +// spi_flash_write(0x0000, &data, 4); + } + } +} +*/ +/*********************************************************************************************\ + * Wifi +\*********************************************************************************************/ + +#define WIFI_CONFIG_SEC 60 // seconds before restart +#define WIFI_MANAGER_SEC 120 // seconds before restart +#define WIFI_CHECK_SEC 20 // seconds +#define WIFI_RETRY_SEC 30 // seconds + +uint8_t _wificounter, _wifiretry, _wifistatus, _wpsresult, _wificonfigflag = 0, _wifiConfigCounter = 0; + +int WIFI_getRSSIasQuality(int RSSI) +{ + int quality = 0; + + if (RSSI <= -100) { + quality = 0; + } else if (RSSI >= -50) { + quality = 100; + } else { + quality = 2 * (RSSI + 100); + } + return quality; +} + +boolean WIFI_configCounter() +{ + if (_wifiConfigCounter) _wifiConfigCounter = WIFI_MANAGER_SEC; + return (_wifiConfigCounter); +} + +extern "C" { +#include "user_interface.h" +} + +void WIFI_wps_status_cb(wps_cb_status status); + +void WIFI_wps_status_cb(wps_cb_status status) +{ + char log[LOGSZ]; + +/* from user_interface.h: + enum wps_cb_status { + WPS_CB_ST_SUCCESS = 0, + WPS_CB_ST_FAILED, + WPS_CB_ST_TIMEOUT, + WPS_CB_ST_WEP, // WPS failed because that WEP is not supported + WPS_CB_ST_SCAN_ERR, // can not find the target WPS AP + }; +*/ + + _wpsresult = status; + if (_wpsresult == WPS_CB_ST_SUCCESS) { + wifi_wps_disable(); + } else { + snprintf_P(log, sizeof(log), PSTR("WPSconfig: FAILED with status %d"), _wpsresult); + addLog(LOG_LEVEL_DEBUG, log); + _wifiConfigCounter = 2; + } +} + +boolean WIFI_WPSConfigDone(void) +{ + return (!_wpsresult); +} + +boolean WIFI_beginWPSConfig(void) +{ + _wpsresult = 99; + if (!wifi_wps_disable()) return false; + if (!wifi_wps_enable(WPS_TYPE_PBC)) return false; // so far only WPS_TYPE_PBC is supported (SDK 2.0.0) + if (!wifi_set_wps_cb((wps_st_cb_t) &WIFI_wps_status_cb)) return false; + if (!wifi_wps_start()) return false; + return true; +} + +void WIFI_config(uint8_t type) +{ + if (!_wificonfigflag) { + if (type == WIFI_RETRY) return; +#if defined(USE_WEMO_EMULATION) || defined(USE_HUE_EMULATION) + UDP_Disconnect(); +#endif // USE_WEMO_EMULATION || USE_HUE_EMULATION + WiFi.disconnect(); // Solve possible Wifi hangs + _wificonfigflag = type; + _wifiConfigCounter = WIFI_CONFIG_SEC; // Allow up to WIFI_CONFIG_SECS seconds for phone to provide ssid/pswd + _wificounter = _wifiConfigCounter +5; + blinks = 1999; + if (_wificonfigflag == WIFI_RESTART) { + restartflag = 2; + } + else if (_wificonfigflag == WIFI_SMARTCONFIG) { + addLog_P(LOG_LEVEL_INFO, PSTR("Smartconfig: Active for 1 minute")); + WiFi.beginSmartConfig(); + } + else if (_wificonfigflag == WIFI_WPSCONFIG) { + if (WIFI_beginWPSConfig()) { + addLog_P(LOG_LEVEL_INFO, PSTR("WPSconfig: Active for 1 minute")); + } else { + addLog_P(LOG_LEVEL_INFO, PSTR("WPSconfig: Failed to start")); + _wifiConfigCounter = 3; + } + } +#ifdef USE_WEBSERVER + else if (_wificonfigflag == WIFI_MANAGER) { + addLog_P(LOG_LEVEL_INFO, PSTR("Wifimanager: Active for 1 minute")); + beginWifiManager(); + } +#endif // USE_WEBSERVER + } +} + +void WIFI_begin(uint8_t flag) +{ + const char PhyMode[] = " BGN"; + char log[LOGSZ]; + +#if defined(USE_WEMO_EMULATION) || defined(USE_HUE_EMULATION) + UDP_Disconnect(); +#endif // USE_WEMO_EMULATION || USE_HUE_EMULATION + if (!strncmp(ESP.getSdkVersion(),"1.5.3",5)) { + addLog_P(LOG_LEVEL_DEBUG, "Wifi: Patch issue 2186"); + WiFi.mode(WIFI_OFF); // See https://github.com/esp8266/Arduino/issues/2186 + } + WiFi.disconnect(); + WiFi.mode(WIFI_STA); // Disable AP mode + if (sysCfg.sleep) wifi_set_sleep_type(LIGHT_SLEEP_T); // Allow light sleep during idle times +// if (WiFi.getPhyMode() != WIFI_PHY_MODE_11N) WiFi.setPhyMode(WIFI_PHY_MODE_11N); + if (!WiFi.getAutoConnect()) WiFi.setAutoConnect(true); +// WiFi.setAutoReconnect(true); + switch (flag) { + case 0: // AP1 + case 1: // AP2 + sysCfg.sta_active = flag; + break; + case 2: // Toggle + sysCfg.sta_active ^= 1; + } // 3: Current AP + if (strlen(sysCfg.sta_ssid[1]) == 0) sysCfg.sta_active = 0; + WiFi.begin(sysCfg.sta_ssid[sysCfg.sta_active], sysCfg.sta_pwd[sysCfg.sta_active]); + snprintf_P(log, sizeof(log), PSTR("Wifi: Connecting to AP%d %s in mode 11%c as %s..."), + sysCfg.sta_active +1, sysCfg.sta_ssid[sysCfg.sta_active], PhyMode[WiFi.getPhyMode() & 0x3], Hostname); + addLog(LOG_LEVEL_INFO, log); +} + +void WIFI_check_ip() +{ + if ((WiFi.status() == WL_CONNECTED) && (static_cast(WiFi.localIP()) != 0)) { + _wificounter = WIFI_CHECK_SEC; + _wifiretry = WIFI_RETRY_SEC; + addLog_P((_wifistatus != WL_CONNECTED) ? LOG_LEVEL_INFO : LOG_LEVEL_DEBUG_MORE, PSTR("Wifi: Connected")); + _wifistatus = WL_CONNECTED; + } else { + _wifistatus = WiFi.status(); + switch (_wifistatus) { + case WL_CONNECTED: + addLog_P(LOG_LEVEL_INFO, PSTR("Wifi: Connect failed as no IP address received")); + _wifistatus = 0; + _wifiretry = WIFI_RETRY_SEC; + break; + case WL_NO_SSID_AVAIL: + addLog_P(LOG_LEVEL_INFO, PSTR("Wifi: Connect failed as AP cannot be reached")); + if (_wifiretry > (WIFI_RETRY_SEC / 2)) _wifiretry = WIFI_RETRY_SEC / 2; + else if (_wifiretry) _wifiretry = 0; + break; + case WL_CONNECT_FAILED: + addLog_P(LOG_LEVEL_INFO, PSTR("Wifi: Connect failed with AP incorrect password")); + if (_wifiretry > (WIFI_RETRY_SEC / 2)) _wifiretry = WIFI_RETRY_SEC / 2; + else if (_wifiretry) _wifiretry = 0; + break; + default: // WL_IDLE_STATUS and WL_DISCONNECTED + if (!_wifiretry || (_wifiretry == (WIFI_RETRY_SEC / 2))) { + addLog_P(LOG_LEVEL_INFO, PSTR("Wifi: Connect failed with AP timeout")); + } else { + addLog_P(LOG_LEVEL_DEBUG, PSTR("Wifi: Attempting connection...")); + } + } + if (_wifiretry) { + if (_wifiretry == WIFI_RETRY_SEC) WIFI_begin(3); // Select default SSID + if (_wifiretry == (WIFI_RETRY_SEC / 2)) WIFI_begin(2); // Select alternate SSID + _wificounter = 1; + _wifiretry--; + } else { + WIFI_config(sysCfg.sta_config); + _wificounter = 1; + _wifiretry = WIFI_RETRY_SEC; + } + } +} + +void WIFI_Check(uint8_t param) +{ + char log[LOGSZ]; + + _wificounter--; + switch (param) { + case WIFI_SMARTCONFIG: + case WIFI_MANAGER: + case WIFI_WPSCONFIG: + WIFI_config(param); + break; + default: + if (_wifiConfigCounter) { + _wifiConfigCounter--; + _wificounter = _wifiConfigCounter +5; + if (_wifiConfigCounter) { + if ((_wificonfigflag == WIFI_SMARTCONFIG) && WiFi.smartConfigDone()) _wifiConfigCounter = 0; + if ((_wificonfigflag == WIFI_WPSCONFIG) && WIFI_WPSConfigDone()) _wifiConfigCounter = 0; + if (!_wifiConfigCounter) { + if (strlen(WiFi.SSID().c_str())) strlcpy(sysCfg.sta_ssid[0], WiFi.SSID().c_str(), sizeof(sysCfg.sta_ssid[0])); + if (strlen(WiFi.psk().c_str())) strlcpy(sysCfg.sta_pwd[0], WiFi.psk().c_str(), sizeof(sysCfg.sta_pwd[0])); + sysCfg.sta_active = 0; + snprintf_P(log, sizeof(log), PSTR("Wificonfig: SSID1 %s and Password1 %s"), sysCfg.sta_ssid[0], sysCfg.sta_pwd[0]); + addLog(LOG_LEVEL_INFO, log); + } + } + if (!_wifiConfigCounter) { + if (_wificonfigflag == WIFI_SMARTCONFIG) WiFi.stopSmartConfig(); + restartflag = 2; + } + } else { + if (_wificounter <= 0) { + addLog_P(LOG_LEVEL_DEBUG_MORE, PSTR("Wifi: Checking connection...")); + _wificounter = WIFI_CHECK_SEC; + WIFI_check_ip(); + } + if ((WiFi.status() == WL_CONNECTED) && (static_cast(WiFi.localIP()) != 0) && !_wificonfigflag) { +#ifdef USE_DISCOVERY + if (!mDNSbegun) { + mDNSbegun = MDNS.begin(Hostname); + snprintf_P(log, sizeof(log), PSTR("mDNS: %s"), (mDNSbegun)?"Initialized":"Failed"); + addLog(LOG_LEVEL_INFO, log); + } +#endif // USE_DISCOVERY +#ifdef USE_WEBSERVER + if (sysCfg.webserver) { + startWebserver(sysCfg.webserver, WiFi.localIP()); +#ifdef USE_DISCOVERY +#ifdef WEBSERVER_ADVERTISE + MDNS.addService("http", "tcp", 80); +#endif // WEBSERVER_ADVERTISE +#endif // USE_DISCOVERY + } else { + stopWebserver(); + } +#if defined(USE_WEMO_EMULATION) || defined(USE_HUE_EMULATION) + UDP_Connect(); +#endif // USE_WEMO_EMULATION || USE_HUE_EMULATION +#endif // USE_WEBSERVER + } else { +#if defined(USE_WEMO_EMULATION) || defined(USE_HUE_EMULATION) + UDP_Disconnect(); +#endif // USE_WEMO_EMULATION || USE_HUE_EMULATION + mDNSbegun = false; + } + } + } +} + +int WIFI_State() +{ + int state; + + if ((WiFi.status() == WL_CONNECTED) && (static_cast(WiFi.localIP()) != 0)) state = WIFI_RESTART; + if (_wificonfigflag) state = _wificonfigflag; + return state; +} + +void WIFI_Connect(char *Hostname) +{ + WiFi.persistent(false); // Solve possible wifi init errors + WiFi.hostname(Hostname); + _wifistatus = 0; + _wifiretry = WIFI_RETRY_SEC; + _wificounter = 1; +} + +#ifdef USE_DISCOVERY +/*********************************************************************************************\ + * mDNS +\*********************************************************************************************/ + +#ifdef MQTT_HOST_DISCOVERY +boolean mdns_discoverMQTTServer() +{ + char log[LOGSZ], ip_str[20]; + int n; + + if (!mDNSbegun) return false; + + n = MDNS.queryService("mqtt", "tcp"); // Search for mqtt service + + snprintf_P(log, sizeof(log), PSTR("mDNS: Query done with %d mqtt services found"), n); + addLog(LOG_LEVEL_INFO, log); + + if (n > 0) { + // Note: current strategy is to get the first MQTT service (even when many are found) + IPtoCharArray(MDNS.IP(0), ip_str, 20); + + snprintf_P(log, sizeof(log), PSTR("mDNS: Service found on %s ip %s port %d"), + MDNS.hostname(0).c_str(), ip_str, MDNS.port(0)); + addLog(LOG_LEVEL_INFO, log); + + strlcpy(sysCfg.mqtt_host, ip_str, sizeof(sysCfg.mqtt_host)); + sysCfg.mqtt_port = MDNS.port(0); + } + + return n > 0; +} +#endif // MQTT_HOST_DISCOVERY + +void IPtoCharArray(IPAddress address, char *ip_str, size_t size) +{ + String str = String(address[0]); + str += "."; + str += String(address[1]); + str += "."; + str += String(address[2]); + str += "."; + str += String(address[3]); + str.toCharArray(ip_str, size); +} +#endif // USE_DISCOVERY + +/*********************************************************************************************\ + * Basic I2C routines +\*********************************************************************************************/ + +#ifdef USE_I2C +#define I2C_RETRY_COUNTER 3 + +int32_t i2c_read(uint8_t addr, uint8_t reg, uint8_t size) +{ + char log[LOGSZ]; + byte x = 0; + int32_t data = 0; + + do { + Wire.beginTransmission(addr); // start transmission to device + Wire.write(reg); // sends register address to read from + if (Wire.endTransmission(false) == 0) { // Try to become I2C Master, send data and collect bytes, keep master status for next request... + Wire.requestFrom((int)addr, (int)size); // send data n-bytes read + if (Wire.available() == size) + for(byte i = 0; i < size; i++) { + data <<= 8; + data |= Wire.read(); // receive DATA + } + } + x++; + } while (Wire.endTransmission(true) != 0 && x <= I2C_RETRY_COUNTER); // end transmission + +// snprintf_P(log, sizeof(log), PSTR("I2C: received %X, retries %d"), data, x -1); +// addLog(LOG_LEVEL_DEBUG_MORE, log); + + return data; +} + +uint8_t i2c_read8(uint8_t addr, uint8_t reg) +{ + return i2c_read(addr, reg, 1); +} + +uint16_t i2c_read16(uint8_t addr, uint8_t reg) +{ + return i2c_read(addr, reg, 2); +} + +int16_t i2c_readS16(uint8_t addr, uint8_t reg) +{ + return (int16_t)i2c_read(addr, reg, 2); +} + +uint16_t i2c_read16_LE(uint8_t addr, uint8_t reg) +{ + uint16_t temp = i2c_read(addr, reg, 2); + return (temp >> 8) | (temp << 8); +} + +int16_t i2c_readS16_LE(uint8_t addr, uint8_t reg) +{ + return (int16_t)i2c_read16_LE(addr, reg); +} + +int32_t i2c_read24(uint8_t addr, uint8_t reg) +{ + return i2c_read(addr, reg, 3); +} + +void i2c_write8(uint8_t addr, uint8_t reg, uint8_t val) +{ + byte x = I2C_RETRY_COUNTER; + + do { + Wire.beginTransmission((uint8_t)addr); // start transmission to device + Wire.write(reg); // sends register address to read from + Wire.write(val); // write data + x--; + } while (Wire.endTransmission(true) != 0 && x != 0); // end transmission +} + +void i2c_scan(char *devs, unsigned int devs_len) +{ + byte error, address, any = 0; + char tstr[10]; + + snprintf_P(devs, devs_len, PSTR("{\"I2Cscan\":\"Device(s) found at")); + for (address = 1; address <= 127; address++) { + Wire.beginTransmission(address); + error = Wire.endTransmission(); + if (error == 0) { + snprintf_P(tstr, sizeof(tstr), PSTR(" 0x%2x"), address); + strncat(devs, tstr, devs_len); + any = 1; + } + else if (error == 4) snprintf_P(devs, devs_len, PSTR("{\"I2Cscan\":\"Unknow error at 0x%2x\"}"), address); + } + if (any) { + strncat(devs, "\"}", devs_len); + } else { + snprintf_P(devs, devs_len, PSTR("{\"I2Cscan\":\"No devices found\"}")); + } +} +#endif // USE_I2C + +/*********************************************************************************************\ + * Real Time Clock + * + * Sources: Time by Michael Margolis and Paul Stoffregen (https://github.com/PaulStoffregen/Time) + * Timezone by Jack Christensen (https://github.com/JChristensen/Timezone) +\*********************************************************************************************/ + +extern "C" { +#include "sntp.h" +} + +#define SECS_PER_MIN ((uint32_t)(60UL)) +#define SECS_PER_HOUR ((uint32_t)(3600UL)) +#define SECS_PER_DAY ((uint32_t)(SECS_PER_HOUR * 24UL)) +#define LEAP_YEAR(Y) (((1970+Y)>0) && !((1970+Y)%4) && (((1970+Y)%100) || !((1970+Y)%400))) + +Ticker tickerRTC; + +static const uint8_t monthDays[] = { 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 }; // API starts months from 1, this array starts from 0 +static const char monthNames[37] = { "JanFebMrtAprMayJunJulAugSepOctNovDec" }; + +uint32_t utctime = 0, loctime = 0, dsttime = 0, stdtime = 0, ntptime = 0, midnight = 1451602800; + +rtcCallback rtcCb = NULL; + +void breakTime(uint32_t timeInput, TIME_T &tm) +{ +// break the given timeInput into time components +// this is a more compact version of the C library localtime function +// note that year is offset from 1970 !!! + + uint8_t year, month, monthLength; + uint32_t time; + unsigned long days; + + time = timeInput; + tm.Second = time % 60; + time /= 60; // now it is minutes + tm.Minute = time % 60; + time /= 60; // now it is hours + tm.Hour = time % 24; + time /= 24; // now it is days + tm.Wday = ((time + 4) % 7) + 1; // Sunday is day 1 + + year = 0; + days = 0; + while((unsigned)(days += (LEAP_YEAR(year) ? 366 : 365)) <= time) { + year++; + } + tm.Year = year; // year is offset from 1970 + + days -= LEAP_YEAR(year) ? 366 : 365; + time -= days; // now it is days in this year, starting at 0 + tm.DayOfYear = time; + + days = 0; + month = 0; + monthLength = 0; + for (month = 0; month < 12; month++) { + if (month == 1) { // february + if (LEAP_YEAR(year)) { + monthLength = 29; + } else { + monthLength = 28; + } + } else { + monthLength = monthDays[month]; + } + + if (time >= monthLength) { + time -= monthLength; + } else { + break; + } + } + strlcpy(tm.MonthName, monthNames + (month *3), 4); + tm.Month = month + 1; // jan is month 1 + tm.Day = time + 1; // day of month + tm.Valid = (timeInput > 1451602800); // 2016-01-01 +} + +uint32_t makeTime(TIME_T &tm) +{ +// assemble time elements into time_t +// note year argument is offset from 1970 + + int i; + uint32_t seconds; + + // seconds from 1970 till 1 jan 00:00:00 of the given year + seconds = tm.Year * (SECS_PER_DAY * 365); + for (i = 0; i < tm.Year; i++) { + if (LEAP_YEAR(i)) { + seconds += SECS_PER_DAY; // add extra days for leap years + } + } + + // add days for this year, months start from 1 + for (i = 1; i < tm.Month; i++) { + if ((i == 2) && LEAP_YEAR(tm.Year)) { + seconds += SECS_PER_DAY * 29; + } else { + seconds += SECS_PER_DAY * monthDays[i-1]; // monthDay array starts from 0 + } + } + seconds+= (tm.Day - 1) * SECS_PER_DAY; + seconds+= tm.Hour * SECS_PER_HOUR; + seconds+= tm.Minute * SECS_PER_MIN; + seconds+= tm.Second; + return seconds; +} + +uint32_t toTime_t(TimeChangeRule r, int yr) +{ + TIME_T tm; + uint32_t t; + uint8_t m, w; // temp copies of r.month and r.week + + m = r.month; + w = r.week; + if (w == 0) { // Last week = 0 + if (++m > 12) { // for "Last", go to the next month + m = 1; + yr++; + } + w = 1; // and treat as first week of next month, subtract 7 days later + } + + tm.Hour = r.hour; + tm.Minute = 0; + tm.Second = 0; + tm.Day = 1; + tm.Month = m; + tm.Year = yr - 1970; + t = makeTime(tm); // First day of the month, or first day of next month for "Last" rules + breakTime(t, tm); + t += (7 * (w - 1) + (r.dow - tm.Wday + 7) % 7) * SECS_PER_DAY; + if (r.week == 0) t -= 7 * SECS_PER_DAY; //back up a week if this is a "Last" rule + return t; +} + +String rtc_time(int type) +{ + char stime[25]; // Skip newline + + uint32_t time = utctime; + if (type == 1) time = loctime; + if (type == 2) time = dsttime; + if (type == 3) time = stdtime; + snprintf_P(stime, sizeof(stime), PSTR("%s"), sntp_get_real_time(time)); + return String(stime); +} + +uint32_t rtc_loctime() +{ + return loctime; +} + +uint32_t rtc_midnight() +{ + return midnight; +} + +void rtc_second() +{ + char log[LOGSZ]; + byte ntpsync; + uint32_t stdoffset, dstoffset; + TIME_T tmpTime; + + ntpsync = 0; + if (rtcTime.Year < 2016) { + if (WiFi.status() == WL_CONNECTED) { + ntpsync = 1; // Initial NTP sync + } + } else { + if ((rtcTime.Minute == 1) && (rtcTime.Second == 1)) { + ntpsync = 1; // Hourly NTP sync at xx:01:01 + } + } + if (ntpsync) { + ntptime = sntp_get_current_timestamp(); + if (ntptime) { + utctime = ntptime; + breakTime(utctime, tmpTime); + rtcTime.Year = tmpTime.Year + 1970; + dsttime = toTime_t(myDST, rtcTime.Year); + stdtime = toTime_t(mySTD, rtcTime.Year); + snprintf_P(log, sizeof(log), PSTR("RTC: (UTC) %s"), rtc_time(0).c_str()); + addLog(LOG_LEVEL_DEBUG, log); + snprintf_P(log, sizeof(log), PSTR("RTC: (DST) %s"), rtc_time(2).c_str()); + addLog(LOG_LEVEL_DEBUG, log); + snprintf_P(log, sizeof(log), PSTR("RTC: (STD) %s"), rtc_time(3).c_str()); + addLog(LOG_LEVEL_DEBUG, log); + } + } + utctime++; + loctime = utctime; + if (loctime > 1451602800) { // 2016-01-01 + if (sysCfg.timezone == 99) { + dstoffset = myDST.offset * SECS_PER_MIN; + stdoffset = mySTD.offset * SECS_PER_MIN; + if ((utctime >= (dsttime - stdoffset)) && (utctime < (stdtime - dstoffset))) { + loctime += dstoffset; // Daylight Saving Time + } else { + loctime += stdoffset; // Standard Time + } + } else { + loctime += sysCfg.timezone * SECS_PER_HOUR; + } + } + breakTime(loctime, rtcTime); + if (!rtcTime.Hour && !rtcTime.Minute && !rtcTime.Second && rtcTime.Valid) { + midnight = loctime; + } + rtcTime.Year += 1970; + if (rtcCb) rtcCb(); +} + +void rtc_init(rtcCallback cb) +{ + rtcCb = cb; + sntp_setservername(0, (char*)NTP_SERVER1); + sntp_setservername(1, (char*)NTP_SERVER2); + sntp_setservername(2, (char*)NTP_SERVER3); + sntp_stop(); + sntp_set_timezone(0); // UTC time + sntp_init(); + utctime = 0; + breakTime(utctime, rtcTime); + tickerRTC.attach(1, rtc_second); +} + +/*********************************************************************************************\ + * Syslog +\*********************************************************************************************/ + +void syslog(const char *message) +{ + char str[TOPSZ+MESSZ]; + + if (portUDP.beginPacket(sysCfg.syslog_host, sysCfg.syslog_port)) { + snprintf_P(str, sizeof(str), PSTR("%s ESP-%s"), Hostname, message); + portUDP.write(str); + portUDP.endPacket(); + } else { + syslog_level = 0; + syslog_timer = SYSLOG_TIMER; + snprintf_P(str, sizeof(str), PSTR("SYSL: Syslog Host not found so logging disabled for %d seconds. Consider syslog 0"), SYSLOG_TIMER); + addLog(LOG_LEVEL_INFO, str); + } +} + +void addLog(byte loglevel, const char *line) +{ + char mxtime[9]; + + snprintf_P(mxtime, sizeof(mxtime), PSTR("%02d:%02d:%02d"), rtcTime.Hour, rtcTime.Minute, rtcTime.Second); + +#ifdef DEBUG_ESP_PORT + DEBUG_ESP_PORT.printf("%s %s\n", mxtime, line); +#endif // DEBUG_ESP_PORT + if (loglevel <= seriallog_level) Serial.printf("%s %s\n", mxtime, line); +#ifdef USE_WEBSERVER + if (loglevel <= sysCfg.weblog_level) { + Log[logidx] = String(mxtime) + " " + String(line); + logidx++; + if (logidx > MAX_LOG_LINES -1) logidx = 0; + } +#endif // USE_WEBSERVER + if ((WiFi.status() == WL_CONNECTED) && (loglevel <= syslog_level)) syslog(line); +} + +void addLog_P(byte loglevel, const char *formatP) +{ + char mess[MESSZ]; + + snprintf_P(mess, sizeof(mess), formatP); + addLog(loglevel, mess); +} + +/*********************************************************************************************\ + * +\*********************************************************************************************/ diff --git a/sonoff/user_config.h b/sonoff/user_config.h new file mode 100644 index 000000000..a0cae5c9a --- /dev/null +++ b/sonoff/user_config.h @@ -0,0 +1,147 @@ +/*********************************************************************************************\ + * User specific configuration parameters + * + * ATTENTION: Changes to most PARAMETER defines will only override flash settings if you change + * define CFG_HOLDER. + * Most parameters can be changed online using commands via MQTT, WebConsole or serial + * + * Corresponding MQTT/Serial/Console commands in [brackets] +\*********************************************************************************************/ + +// -- Project ------------------------------------- +#define PROJECT "sonoff" // PROJECT is used as the default topic delimiter and OTA file name + // As an IDE restriction it needs to be the same as the main .ino file + +#define CFG_HOLDER 0x20161209 // [Reset 1] Change this value to load following default configuration parameters +#define SAVE_DATA 1 // [SaveData] Save changed parameters to Flash (0 = disable, 1 - 3600 seconds) +#define SAVE_STATE 1 // [SaveState] Save changed power state to Flash (0 = disable, 1 = enable) + +// -- Wifi ---------------------------------------- +#define STA_SSID1 "indebuurt1" // [Ssid1] Wifi SSID +#define STA_PASS1 "VnsqrtnrsddbrN" // [Password1] Wifi password +#define STA_SSID2 "indebuurt2" // [Ssid2] Optional alternate AP Wifi SSID +#define STA_PASS2 "VnsqrtnrsddbrN" // [Password2] Optional alternate AP Wifi password +#define WIFI_HOSTNAME "%s-%04d" // [Hostname] Expands to - +#define WIFI_CONFIG_TOOL WIFI_WPSCONFIG // [WifiConfig] Default tool if wifi fails to connect + // (WIFI_RESTART, WIFI_SMARTCONFIG, WIFI_MANAGER, WIFI_WPSCONFIG, WIFI_RETRY) +// -- Syslog -------------------------------------- +#define SYS_LOG_HOST "domus1" // [LogHost] (Linux) syslog host +#define SYS_LOG_PORT 514 // [LogPort] default syslog UDP port +#define SYS_LOG_LEVEL LOG_LEVEL_NONE // [SysLog] +#define SERIAL_LOG_LEVEL LOG_LEVEL_INFO // [SerialLog] +#define WEB_LOG_LEVEL LOG_LEVEL_INFO // [WebLog] + +// -- Ota ----------------------------------------- +#define OTA_URL "http://domus1:80/api/arduino/" PROJECT ".ino.bin" // [OtaUrl] + +// -- MQTT ---------------------------------------- +#define MQTT_USE 1 // [Mqtt] Select default MQTT use (0 = Off, 1 = On) +// !!! TLS uses a LOT OF MEMORY (20k) so be careful to enable other options at the same time !!! +//#define USE_MQTT_TLS // EXPERIMENTAL Use TLS for MQTT connection (+53k code, +20k mem) + // Needs Fingerprint, TLS Port, UserId and Password +#ifdef USE_MQTT_TLS + #define MQTT_HOST "m20.cloudmqtt.com" // [MqttHost] + #define MQTT_FINGERPRINT "A5 02 FF 13 99 9F 8B 39 8E F1 83 4F 11 23 65 0B 32 36 FC 07" // [MqttFingerprint] + #define MQTT_PORT 20123 // [MqttPort] MQTT TLS port + #define MQTT_USER "cloudmqttuser" // [MqttUser] Mandatory user + #define MQTT_PASS "cloudmqttpassword" // [MqttPassword] Mandatory password +#else + #define MQTT_HOST "domus1" // [MqttHost] + #define MQTT_PORT 1883 // [MqttPort] MQTT port (10123 on CloudMQTT) + #define MQTT_USER "DVES_USER" // [MqttUser] Optional user + #define MQTT_PASS "DVES_PASS" // [MqttPassword] Optional password +#endif + +#define MQTT_CLIENT_ID "DVES_%06X" // [MqttClient] Also fall back topic using Chip Id = last 6 characters of MAC address + +#define SUB_PREFIX "cmnd" // Sonoff devices subscribe to:- SUB_PREFIX/MQTT_TOPIC and SUB_PREFIX/MQTT_GRPTOPIC +#define PUB_PREFIX "stat" // Sonoff devices publish to:- PUB_PREFIX/MQTT_TOPIC +#define PUB_PREFIX2 "tele" // Sonoff devices publish telemetry data to:- PUB_PREFIX2/MQTT_TOPIC/UPTIME, POWER/LIGHT and TIME + // May be named the same as PUB_PREFIX +#define MQTT_TOPIC PROJECT // [Topic] (unique) MQTT device topic +#define MQTT_GRPTOPIC "sonoffs" // [GroupTopic] MQTT Group topic +#define MQTT_BUTTON_RETAIN 0 // [ButtonRetain] Button may send retain flag (0 = off, 1 = on) +#define MQTT_POWER_RETAIN 0 // [PowerRetain] Power status message may send retain flag (0 = off, 1 = on) +#define MQTT_SWITCH_RETAIN 0 // [SwitchRetain] Switch may send retain flag (0 = off, 1 = on) + +#define MQTT_STATUS_OFF "OFF" // Command or Status result when turned off (needs to be a string like "0" or "Off") +#define MQTT_STATUS_ON "ON" // Command or Status result when turned on (needs to be a string like "1" or "On") +#define MQTT_CMND_TOGGLE "TOGGLE" // Command to send when toggling (needs to be a string like "2" or "Toggle") + +// -- MQTT - Telemetry ---------------------------- +#define TELE_PERIOD 300 // [TelePeriod] Telemetry (0 = disable, 10 - 3600 seconds) + +// -- MQTT - Domoticz ----------------------------- +#define USE_DOMOTICZ // Enable Domoticz (+5k code, +0.3k mem) - Disable by // + #define DOMOTICZ_IN_TOPIC "domoticz/in" // [DomoticzInTopic] + #define DOMOTICZ_OUT_TOPIC "domoticz/out" // [DomoticzOutTopic] + #define DOMOTICZ_UPDATE_TIMER 0 // [DomoticzUpdateTimer] Send relay status (0 = disable, 1 - 3600 seconds) (Optional) + +// -- HTTP ---------------------------------------- +#define USE_WEBSERVER // Enable web server and wifi manager (+43k code, +2k mem) - Disable by // + #define FRIENDLY_NAME1 "Sonoff" // [FriendlyName1] Friendlyname up to 32 characters used by webpages and Alexa + #define FRIENDLY_NAME2 "Sonoff2" // [FriendlyName2] Friendlyname up to 32 characters used by Alexa + #define FRIENDLY_NAME3 "Sonoff3" // [FriendlyName3] Friendlyname up to 32 characters used by Alexa + #define FRIENDLY_NAME4 "Sonoff4" // [FriendlyName4] Friendlyname up to 32 characters used by Alexa + #define WEB_SERVER 2 // [WebServer] Web server (0 = Off, 1 = Start as User, 2 = Start as Admin) +// #define USE_WEMO_EMULATION // Enable Belkin WeMo PowerSwitch emulation for Alexa (+4k code, +2k mem) +// #define USE_HUE_EMULATION // Enable Hue Bridge emulation for Alexa (+5k code, +2k mem) + +// -- mDNS ---------------------------------------- +#define USE_DISCOVERY // Enable mDNS for the following services (+8k code, +0.3k mem) + #define WEBSERVER_ADVERTISE // Provide access to webserver by name .local/ + #define MQTT_HOST_DISCOVERY // Find MQTT host server (overrides MQTT_HOST if found) + +// -- Time - Up to three NTP servers in your region +#define NTP_SERVER1 "pool.ntp.org" +#define NTP_SERVER2 "nl.pool.ntp.org" +#define NTP_SERVER3 "0.nl.pool.ntp.org" + +// -- Time - Start Daylight Saving Time and timezone offset from UTC in minutes +#define TIME_DST Last, Sun, Mar, 2, +120 // Last sunday in march at 02:00 +120 minutes + +// -- Time - Start Standard Time and timezone offset from UTC in minutes +#define TIME_STD Last, Sun, Oct, 3, +60 // Last sunday in october 02:00 +60 minutes + +// -- Application --------------------------------- +#define APP_TIMEZONE 1 // [Timezone] +1 hour (Amsterdam) (-12 .. 12 = hours from UTC, 99 = use TIME_DST/TIME_STD) +#define APP_LEDSTATE LED_POWER // [LedState] Function of led (LED_OFF, LED_POWER, LED_MQTTSUB, LED_POWER_MQTTSUB, LED_MQTTPUB, LED_POWER_MQTTPUB, LED_MQTT, LED_POWER_MQTT) +#define APP_PULSETIME 0 // [PulseTime] Time in 0.1 Sec to turn off power for relay 1 (0 = disabled) +#define APP_POWERON_STATE 3 // [PowerOnState] Power On Relay state (0 = Off, 1 = On, 2 = Toggle Saved state, 3 = Saved state) +#define APP_BLINKTIME 10 // [BlinkTime] Time in 0.1 Sec to blink/toggle power for relay 1 +#define APP_BLINKCOUNT 10 // [BlinkCount] Number of blinks (0 = 32000) +#define APP_SLEEP 0 // [Sleep] Sleep time to lower energy consumption (0 = Off, 1 - 250 mSec) + +#define SWITCH_MODE TOGGLE // [SwitchMode] TOGGLE, FOLLOW, FOLLOW_INV, PUSHBUTTON or PUSHBUTTON_INV (the wall switch state) +#define WS2812_LEDS 30 // [Pixels] Number of WS2812 LEDs to start with + +#define TEMP_CONVERSION 0 // Convert temperature to (0 = Celsius or 1 = Fahrenheit) +#define TEMP_RESOLUTION 1 // Maximum number of decimals (0 - 3) showing sensor Temperature +#define HUMIDITY_RESOLUTION 1 // Maximum number of decimals (0 - 3) showing sensor Humidity +#define PRESSURE_RESOLUTION 1 // Maximum number of decimals (0 - 3) showing sensor Pressure + +// -- Sensor code selection ----------------------- +//#define USE_DHT2 // Optional using Adafruit DHT library +//#define USE_DS18x20 // Optional using OneWire library for multiple DS18B20 and/or DS18S20 + +#define USE_I2C // I2C Support (+10k code, 0.2k mem) + #define USE_BH1750 // Add I2C code for BH1750 sensor + #define USE_BMP // Add I2C code for BMP/BME280 sensor + #define USE_HTU // Add I2C code for HTU21 sensor + +#define USE_WS2812 // WS2812 Led string support (+8k code, +1k mem) +// #define USE_WS2812_DMA // DMA supports only GPIO03 (= Serial TXD) (+1k mem) + // When USE_WS2812_DMA is enabled expect Exceptions on Pow + +/*********************************************************************************************\ + * No user configurable items below +\*********************************************************************************************/ + +#if defined(USE_WEMO_EMULATION) && defined(USE_HUE_EMULATION) + #error "Select either USE_WEMO_EMULATION or USE_HUE_EMULATION" +#endif + +#if (ARDUINO < 10610) + #error "This software is supported with Arduino IDE starting from 1.6.10 and ESP8266 Release 2.3.0" +#endif + diff --git a/sonoff/user_config_override.h b/sonoff/user_config_override.h new file mode 100644 index 000000000..b83e88add --- /dev/null +++ b/sonoff/user_config_override.h @@ -0,0 +1,23 @@ +/*****************************************************************************************************\ + * User specific configuration parameters to override user_config.h + * + * ATTENTION: - Changes to most PARAMETER defines will only override flash settings if you change + * define CFG_HOLDER. + * - Expect compiler warnings when no ifdef/undef/endif sequence is used. + * - You still need to update user_config.h for major defines MODULE and USE_MQTT_TLS. + * - Changing MODULE defines are not being tested for validity as they are in user_config.h. + * - Most parameters can be changed online using commands via MQTT, WebConsole or serial. + * - So I see no use in this but anyway, your on your own. +\*****************************************************************************************************/ + +// Examples +//#ifdef CFG_HOLDER +//#undef CFG_HOLDER +//#endif +//#define CFG_HOLDER 0x20161210 + +//#ifdef STA_SSID1 +//#undef STA_SSID1 +//#endif +//#define STA_SSID1 "yourssid1" + diff --git a/sonoff/webserver.ino b/sonoff/webserver.ino new file mode 100644 index 000000000..aa677d8ba --- /dev/null +++ b/sonoff/webserver.ino @@ -0,0 +1,1521 @@ +/* +Copyright (c) 2017 Theo Arends. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +- Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. +- Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. +*/ + +#ifdef USE_WEBSERVER +/*********************************************************************************************\ + * Web server and WiFi Manager + * + * Enables configuration and reconfiguration of WiFi credentials using a Captive Portal + * Source by AlexT (https://github.com/tzapu) +\*********************************************************************************************/ + +#define STR_HELPER(x) #x +#define STR(x) STR_HELPER(x) + +const char HTTP_HEAD[] PROGMEM = + "" + "" + "" + "" + "{v}" + "" + "" + "" + "" + "
" + "

{ha} Module

{h}

"; +const char HTTP_MSG_RSTRT[] PROGMEM = + "
Device will restart in a few seconds

"; +const char HTTP_BTN_MENU1[] PROGMEM = + "
" + "
" + "
" + "
"; +const char HTTP_BTN_RSTRT[] PROGMEM = + "
"; +const char HTTP_BTN_MENU2[] PROGMEM = + "
" + "
"; +const char HTTP_BTN_MENU3[] PROGMEM = + "
" +#ifdef USE_DOMOTICZ + "
" +#endif // USE_DOMOTICZ + ""; +const char HTTP_BTN_MENU4[] PROGMEM = + "
" + "
" + "
"; +const char HTTP_BTN_MAIN[] PROGMEM = + "

"; +const char HTTP_BTN_CONF[] PROGMEM = + "

"; +const char HTTP_FORM_MODULE[] PROGMEM = + "
 Module parameters 
" + "" + "
Module type ({mt})
" + "
AP1 SSId (" STA_SSID1 ")

" + "
AP1 Password

" + "
AP2 SSId (" STA_SSID2 ")

" + "
AP2 Password

" + "
Hostname ({h0})

"; +const char HTTP_FORM_MQTT[] PROGMEM = + "
 MQTT parameters " + "" + "
Host (" MQTT_HOST ")

" + "
Port (" STR(MQTT_PORT) ")

" + "
Client Id ({m0})

" + "
User (" MQTT_USER ")

" + "
Password

" + "
Topic (" MQTT_TOPIC ")

"; +const char HTTP_FORM_LOG1[] PROGMEM = + "
 Logging parameters " + ""; +const char HTTP_FORM_LOG2[] PROGMEM = + "
{b0} level ({b1})

"; +const char HTTP_FORM_LOG3[] PROGMEM = + "
Syslog host (" SYS_LOG_HOST ")

" + "
Syslog port (" STR(SYS_LOG_PORT) ")

" + "
Telemetric period (" STR(TELE_PERIOD) ")

"; +const char HTTP_FORM_OTHER[] PROGMEM = + "
 Other parameters " + "" + "
MQTT enable
"; +const char HTTP_FORM_END[] PROGMEM = + "
"; +const char HTTP_FORM_UPG[] PROGMEM = + "
" + "
 Upgrade by web server " + "
" + "
OTA Url

" + "
" + "


" + "
 Upgrade by file upload " + "
" + "

" +// "
" + "
" + "
" + "
" + ""; +const char HTTP_FORM_CMND[] PROGMEM = + "


" + "
" + "
" +// "
" + "
"; +const char HTTP_COUNTER[] PROGMEM = + "
"; +const char HTTP_END[] PROGMEM = + "" + "" + ""; +#ifdef USE_WEMO_EMULATION +const char WEMO_EVENTSERVICE_XML[] PROGMEM = + "" + "" + "" + "SetBinaryState" + "" + "" + "" + "BinaryState" + "BinaryState" + "in" + "" + "" + "" + "" + "BinaryState" + "Boolean" + "0" + "" + "" + "level" + "string" + "0" + "" + "" + "" + "\r\n" + "\r\n"; +const char WEMO_SETUP_XML[] PROGMEM = + "" + "" + "" + "urn:Belkin:device:controllee:1" + "{x1}" + "Belkin International Inc." + "Sonoff Socket" + "3.1415" + "uuid:{x2}" + "{x3}" + "0" + "" + "" + "urn:Belkin:service:basicevent:1" + "urn:Belkin:serviceId:basicevent1" + "/upnp/control/basicevent1" + "/upnp/event/basicevent1" + "/eventservice.xml" + "" + "" + "" + "\r\n" + "\r\n"; +#endif // USE_WEMO_EMULATION +#ifdef USE_HUE_EMULATION +const char HUE_DESCRIPTION_XML[] PROGMEM = + "" + "" + "" + "1" + "0" + "" + "http://{x1}/" + "" + "urn:schemas-upnp-org:device:Basic:1" + "Amazon-Echo-HA-Bridge ({x1})" + "Royal Philips Electronics" + "Philips hue bridge 2012" + "929000226503" + "uuid:{x2}" + "" + "\r\n" + "\r\n"; + +const char HUE_LIGHT_STATUS_JSON[] PROGMEM = + "{\"state\":" + "{\"on\":{state}," + "\"bri\":{b}," + "\"hue\":{h}," + "\"sat\":{s}," + "\"effect\":\"none\"," + "\"ct\":0," + "\"alert\":\"none\"," + "\"reachable\":true" + "}," + "\"type\":\"Dimmable light\"," + "\"name\":\"{j1}\"," + "\"modelid\":\"LWB004\"," + "\"manufacturername\":\"Philips\"," + "\"uniqueid\":\"{j2}\"," + "\"swversion\":\"66012040\"" + "}"; + +const char HUE_LIGHT_RESPONSE_JSON[] PROGMEM = + "{\"success\":{\"{api}/{id}/{cmd}\":{res}}}"; +#endif // USE_HUE_EMULATION + +#define DNS_PORT 53 +enum http_t {HTTP_OFF, HTTP_USER, HTTP_ADMIN, HTTP_MANAGER}; + +DNSServer *dnsServer; +ESP8266WebServer *webServer; + +boolean _removeDuplicateAPs = true; +int _minimumQuality = -1, _httpflag = HTTP_OFF, _uploaderror = 0, _colcount; + +void startWebserver(int type, IPAddress ipweb) +{ + char log[LOGSZ]; + + if (!_httpflag) { + if (!webServer) { + webServer = new ESP8266WebServer(80); + webServer->on("/", handleRoot); + webServer->on("/cn", handleConfig); + webServer->on("/md", handleModule); + webServer->on("/w1", handleWifi1); + webServer->on("/w0", handleWifi0); + if (sysCfg.mqtt_enabled) { + webServer->on("/mq", handleMqtt); +#ifdef USE_DOMOTICZ + webServer->on("/dm", handleDomoticz); +#endif // USE_DOMOTICZ + } + webServer->on("/lg", handleLog); + webServer->on("/co", handleOther); + webServer->on("/sv", handleSave); + webServer->on("/rt", handleReset); + webServer->on("/up", handleUpgrade); + webServer->on("/u1", handleUpgradeStart); + webServer->on("/u2", HTTP_POST, handleUploadDone, handleUploadLoop); + webServer->on("/cm", handleCmnd); + webServer->on("/cs", handleConsole); + webServer->on("/ax", handleAjax); + webServer->on("/in", handleInfo); + webServer->on("/rb", handleRestart); + webServer->on("/fwlink", handleRoot); // Microsoft captive portal. Maybe not needed. Might be handled by notFound handler. +#ifdef USE_WEMO_EMULATION + webServer->on("/upnp/control/basicevent1", HTTP_POST, handleUPnPevent); + webServer->on("/eventservice.xml", handleUPnPservice); + webServer->on("/setup.xml", handleUPnPsetup); +#endif // USE_WEMO_EMULATION +#ifdef USE_HUE_EMULATION + webServer->on("/description.xml", handleUPnPsetup); +#endif // USE_HUE_EMULATION + webServer->onNotFound(handleNotFound); + } + webServer->begin(); // Web server start + } + if (_httpflag != type) { + snprintf_P(log, sizeof(log), PSTR("HTTP: Webserver active on %s%s with IP address %s"), + Hostname, (mDNSbegun)?".local":"", ipweb.toString().c_str()); + addLog(LOG_LEVEL_INFO, log); + } + if (type) _httpflag = type; +} + +void stopWebserver() +{ + if (_httpflag) { + webServer->close(); + _httpflag = HTTP_OFF; + addLog_P(LOG_LEVEL_INFO, PSTR("HTTP: Webserver stopped")); + } +} + +void beginWifiManager() +{ + // setup AP + if ((WiFi.status() == WL_CONNECTED) && (static_cast(WiFi.localIP()) != 0)) { + WiFi.mode(WIFI_AP_STA); + addLog_P(LOG_LEVEL_DEBUG, PSTR("Wifimanager: Set AccessPoint and keep Station")); + } else { + WiFi.mode(WIFI_AP); + addLog_P(LOG_LEVEL_DEBUG, PSTR("Wifimanager: Set AccessPoint")); + } + + stopWebserver(); + + dnsServer = new DNSServer(); + WiFi.softAP(Hostname); + delay(500); // Without delay I've seen the IP address blank + /* Setup the DNS server redirecting all the domains to the apIP */ + dnsServer->setErrorReplyCode(DNSReplyCode::NoError); + dnsServer->start(DNS_PORT, "*", WiFi.softAPIP()); + + startWebserver(HTTP_MANAGER, WiFi.softAPIP()); +} + +void pollDnsWeb() +{ + if (dnsServer) dnsServer->processNextRequest(); + if (webServer) webServer->handleClient(); +} + +void showPage(String &page) +{ + page.replace("{ha}", my_module.name); + page.replace("{h}", String(sysCfg.friendlyname[0])); +// page.replace("{ha}", Hostname); + if (_httpflag == HTTP_MANAGER) { + if (WIFI_configCounter()) { + page.replace("", ""); + page += FPSTR(HTTP_COUNTER); + } + } + page += FPSTR(HTTP_END); + + webServer->sendHeader("Cache-Control", "no-cache, no-store, must-revalidate"); + webServer->sendHeader("Pragma", "no-cache"); + webServer->sendHeader("Expires", "-1"); + webServer->send(200, "text/html", page); +} + +void handleRoot() +{ + addLog_P(LOG_LEVEL_DEBUG, PSTR("HTTP: Handle root")); + + if (captivePortal()) { // If captive portal redirect instead of displaying the page. + return; + } + + if (_httpflag == HTTP_MANAGER) { + handleWifi0(); + } else { + + String page = FPSTR(HTTP_HEAD); +// page.replace("", "setTimeout(function(){window.location.reload(1);},4000);"); // Repeats POST on All + page.replace("", "setTimeout(function(){window.location.replace(\"/\");},4000);"); // OK on All + page.replace("{v}", "Main menu"); + + if (Maxdevice) { + if (strlen(webServer->arg("o").c_str())) { + do_cmnd_power(atoi(webServer->arg("o").c_str()), 2); + } + + page += F(""); + for (byte idx = 1; idx <= Maxdevice; idx++) { + page += F(""); + } + page += F("
"); + page += (power & (0x01 << (idx -1))) ? "ON" : "OFF"; + page += F("


"); + } + + String tpage = ""; + if (hlw_flg) tpage += hlw_webPresent(); +#ifdef USE_DS18B20 + if (pin[GPIO_DSB] < 99) tpage += dsb_webPresent(); +#endif // USE_DS18B20 +#ifdef USE_DS18x20 + if (pin[GPIO_DSB] < 99) page += ds18x20_webPresent(); +#endif // USE_DS18x20 +#if defined(USE_DHT) || defined(USE_DHT2) + if (dht_type) tpage += dht_webPresent(); +#endif // USE_DHT/2 +#ifdef USE_I2C + if (i2c_flg) { + tpage += htu_webPresent(); + tpage += bmp_webPresent(); + tpage += bh1750_webPresent(); + } +#endif // USE_I2C + if (tpage.length() > 0) { + page += F(""); + page += tpage; + page += F("

"); + } + + if (_httpflag == HTTP_ADMIN) { + page += FPSTR(HTTP_BTN_MENU1); + page += FPSTR(HTTP_BTN_RSTRT); + } + showPage(page); + +#ifdef USE_DS18x20 + ds18x20_search(); // Check for changes in sensors number + ds18x20_convert(); // Start Conversion, takes up to one second +#endif // USE_DS18x20 + } +} + +void handleConfig() +{ + if (_httpflag == HTTP_USER) { + handleRoot(); + return; + } + addLog_P(LOG_LEVEL_DEBUG, PSTR("HTTP: Handle config")); + + String page = FPSTR(HTTP_HEAD); + page.replace("{v}", "Configuration"); + page += FPSTR(HTTP_BTN_MENU2); + if (sysCfg.mqtt_enabled) page += FPSTR(HTTP_BTN_MENU3); + page += FPSTR(HTTP_BTN_MENU4); + page += FPSTR(HTTP_BTN_MAIN); + showPage(page); +} + +void handleModule() +{ + if (_httpflag == HTTP_USER) { + handleRoot(); + return; + } + + char stemp[20]; + + addLog_P(LOG_LEVEL_DEBUG, PSTR("HTTP: Handle Module config")); + + String page = FPSTR(HTTP_HEAD); + page.replace("{v}", "Config module"); + page += FPSTR(HTTP_FORM_MODULE); + + snprintf_P(stemp, sizeof(stemp), modules[MODULE].name); + page.replace("{mt}", stemp); + + for (byte i = 0; i < MAXMODULE; i++) { + page += F(""); + } + page += F("
"); + + mytmplt cmodule; + memcpy_P(&cmodule, &modules[sysCfg.module], sizeof(cmodule)); + for (byte i = 0; i < MAX_GPIO_PIN; i++) { + if (cmodule.gp.io[i] == GPIO_USER) { + page += F("
GPIO"); page += String(i); page += F("
"); + } + } + + page += FPSTR(HTTP_FORM_END); + page += FPSTR(HTTP_BTN_CONF); + showPage(page); +} + +void handleWifi1() +{ + handleWifi(true); +} + +void handleWifi0() +{ + handleWifi(false); +} + +void handleWifi(boolean scan) +{ + if (_httpflag == HTTP_USER) { + handleRoot(); + return; + } + char log[LOGSZ]; + + addLog_P(LOG_LEVEL_DEBUG, PSTR("HTTP: Handle Wifi config")); + + String page = FPSTR(HTTP_HEAD); + page.replace("{v}", "Configure Wifi"); + + if (scan) { +#if defined(USE_WEMO_EMULATION) || defined(USE_HUE_EMULATION) + UDP_Disconnect(); +#endif // USE_WEMO_EMULATION || USE_HUE_EMULATION + int n = WiFi.scanNetworks(); + addLog_P(LOG_LEVEL_DEBUG, PSTR("Wifi: Scan done")); + + if (n == 0) { + addLog_P(LOG_LEVEL_DEBUG, PSTR("Wifi: No networks found")); + page += F("No networks found. Refresh to scan again."); + } else { + //sort networks + int indices[n]; + for (int i = 0; i < n; i++) { + indices[i] = i; + } + + // RSSI SORT + for (int i = 0; i < n; i++) { + for (int j = i + 1; j < n; j++) { + if (WiFi.RSSI(indices[j]) > WiFi.RSSI(indices[i])) { + std::swap(indices[i], indices[j]); + } + } + } + + // remove duplicates ( must be RSSI sorted ) + if (_removeDuplicateAPs) { + String cssid; + for (int i = 0; i < n; i++) { + if (indices[i] == -1) continue; + cssid = WiFi.SSID(indices[i]); + for (int j = i + 1; j < n; j++) { + if (cssid == WiFi.SSID(indices[j])) { + snprintf_P(log, sizeof(log), PSTR("Wifi: Duplicate AccessPoint %s"), WiFi.SSID(indices[j]).c_str()); + addLog(LOG_LEVEL_DEBUG, log); + indices[j] = -1; // set dup aps to index -1 + } + } + } + } + + //display networks in page + for (int i = 0; i < n; i++) { + if (indices[i] == -1) continue; // skip dups + snprintf_P(log, sizeof(log), PSTR("Wifi: SSID %s, RSSI %d"), WiFi.SSID(indices[i]).c_str(), WiFi.RSSI(indices[i])); + addLog(LOG_LEVEL_DEBUG, log); + int quality = WIFI_getRSSIasQuality(WiFi.RSSI(indices[i])); + + if (_minimumQuality == -1 || _minimumQuality < quality) { + String item = FPSTR(HTTP_LNK_ITEM); + String rssiQ; + rssiQ += quality; + item.replace("{v}", WiFi.SSID(indices[i])); + item.replace("{r}", rssiQ); + if (WiFi.encryptionType(indices[i]) != ENC_TYPE_NONE) { + item.replace("{i}", "l"); + } else { + item.replace("{i}", ""); + } + page += item; + delay(0); + } else { + addLog_P(LOG_LEVEL_DEBUG, PSTR("Wifi: Skipping due to low quality")); + } + + } + page += "
"; + } + } else { + page += FPSTR(HTTP_LNK_SCAN); + } + + page += FPSTR(HTTP_FORM_WIFI); + + char str[33]; + if (!strcmp(WIFI_HOSTNAME, DEF_WIFI_HOSTNAME)) { + snprintf_P(str, sizeof(str), PSTR(DEF_WIFI_HOSTNAME), sysCfg.mqtt_topic, ESP.getChipId() & 0x1FFF); + } else { + snprintf_P(str, sizeof(str), PSTR(WIFI_HOSTNAME)); + } + page.replace("{h0}", str); + page.replace("{h1}", String(sysCfg.hostname)); + page.replace("{s1}", String(sysCfg.sta_ssid[0])); + page.replace("{p1}", String(sysCfg.sta_pwd[0])); + page.replace("{s2}", String(sysCfg.sta_ssid[1])); + page.replace("{p2}", String(sysCfg.sta_pwd[1])); + page += FPSTR(HTTP_FORM_END); + if (_httpflag == HTTP_MANAGER) { + page += FPSTR(HTTP_BTN_RSTRT); + } else { + page += FPSTR(HTTP_BTN_CONF); + } + showPage(page); +} + +void handleMqtt() +{ + if (_httpflag == HTTP_USER) { + handleRoot(); + return; + } + addLog_P(LOG_LEVEL_DEBUG, PSTR("HTTP: Handle MQTT config")); + + String page = FPSTR(HTTP_HEAD); + page.replace("{v}", "Configure MQTT"); + page += FPSTR(HTTP_FORM_MQTT); + char str[sizeof(sysCfg.mqtt_client)]; + getClient(str, MQTT_CLIENT_ID, sizeof(sysCfg.mqtt_client)); + page.replace("{m0}", str); + page.replace("{m1}", String(sysCfg.mqtt_host)); + page.replace("{m2}", String(sysCfg.mqtt_port)); + page.replace("{m3}", String(sysCfg.mqtt_client)); + page.replace("{m4}", String(sysCfg.mqtt_user)); + page.replace("{m5}", String(sysCfg.mqtt_pwd)); + page.replace("{m6}", String(sysCfg.mqtt_topic)); + page += FPSTR(HTTP_FORM_END); + page += FPSTR(HTTP_BTN_CONF); + showPage(page); +} + +void handleLog() +{ + if (_httpflag == HTTP_USER) { + handleRoot(); + return; + } + addLog_P(LOG_LEVEL_DEBUG, PSTR("HTTP: Handle Log config")); + + String page = FPSTR(HTTP_HEAD); + page.replace("{v}", "Config logging"); + page += FPSTR(HTTP_FORM_LOG1); + for (byte idx = 0; idx < 3; idx++) { + page += FPSTR(HTTP_FORM_LOG2); + switch (idx) { + case 0: + page.replace("{b0}", "Serial log"); + page.replace("{b1}", STR(SERIAL_LOG_LEVEL)); + page.replace("{b2}", "ls"); + for (byte i = LOG_LEVEL_NONE; i < LOG_LEVEL_ALL; i++) { + page.replace("{a" + String(i), (i == sysCfg.seriallog_level) ? " selected " : " "); + } + break; + case 1: + page.replace("{b0}", "Web log"); + page.replace("{b1}", STR(WEB_LOG_LEVEL)); + page.replace("{b2}", "lw"); + for (byte i = LOG_LEVEL_NONE; i < LOG_LEVEL_ALL; i++) { + page.replace("{a" + String(i), (i == sysCfg.weblog_level) ? " selected " : " "); + } + break; + case 2: + page.replace("{b0}", "Syslog"); + page.replace("{b1}", STR(SYS_LOG_LEVEL)); + page.replace("{b2}", "ll"); + for (byte i = LOG_LEVEL_NONE; i < LOG_LEVEL_ALL; i++) { + page.replace("{a" + String(i), (i == sysCfg.syslog_level) ? " selected " : " "); + } + break; + } + } + page += FPSTR(HTTP_FORM_LOG3); + page.replace("{l2}", String(sysCfg.syslog_host)); + page.replace("{l3}", String(sysCfg.syslog_port)); + page.replace("{l4}", String(sysCfg.tele_period)); + page += FPSTR(HTTP_FORM_END); + page += FPSTR(HTTP_BTN_CONF); + showPage(page); +} + +void handleOther() +{ + if (_httpflag == HTTP_USER) { + handleRoot(); + return; + } + addLog_P(LOG_LEVEL_DEBUG, PSTR("HTTP: Handle other config")); + + String page = FPSTR(HTTP_HEAD); + page.replace("{v}", "Configure Other"); + page += FPSTR(HTTP_FORM_OTHER); + page.replace("{r1}", (sysCfg.mqtt_enabled) ? " checked" : ""); + for (int i = 0; i < Maxdevice; i++) { + page += F("
Friendly Name {1 ({2)

"); + page.replace("{1", String(i +1)); + if (i == 0) page.replace("{2", FRIENDLY_NAME1); + else if (i == 1) page.replace("{2", FRIENDLY_NAME2); + else if (i == 2) page.replace("{2", FRIENDLY_NAME3); + else if (i == 3) page.replace("{2", FRIENDLY_NAME4); + page.replace("{3", String(sysCfg.friendlyname[i])); + } + page += FPSTR(HTTP_FORM_END); + page += FPSTR(HTTP_BTN_CONF); + showPage(page); +} + +void handleSave() +{ + if (_httpflag == HTTP_USER) { + handleRoot(); + return; + } + char log[LOGSZ], stemp[20]; + byte what = 0, restart; + String result = ""; + + addLog_P(LOG_LEVEL_DEBUG, PSTR("HTTP: Parameter save")); + + if (strlen(webServer->arg("w").c_str())) what = atoi(webServer->arg("w").c_str()); + switch (what) { + case 1: + strlcpy(sysCfg.hostname, (!strlen(webServer->arg("h").c_str())) ? WIFI_HOSTNAME : webServer->arg("h").c_str(), sizeof(sysCfg.hostname)); + if (strstr(sysCfg.hostname,"%")) strlcpy(sysCfg.hostname, DEF_WIFI_HOSTNAME, sizeof(sysCfg.hostname)); + strlcpy(sysCfg.sta_ssid[0], (!strlen(webServer->arg("s1").c_str())) ? STA_SSID1 : webServer->arg("s1").c_str(), sizeof(sysCfg.sta_ssid[0])); + strlcpy(sysCfg.sta_pwd[0], (!strlen(webServer->arg("p1").c_str())) ? STA_PASS1 : webServer->arg("p1").c_str(), sizeof(sysCfg.sta_pwd[0])); + strlcpy(sysCfg.sta_ssid[1], (!strlen(webServer->arg("s2").c_str())) ? STA_SSID2 : webServer->arg("s2").c_str(), sizeof(sysCfg.sta_ssid[1])); + strlcpy(sysCfg.sta_pwd[1], (!strlen(webServer->arg("p2").c_str())) ? STA_PASS2 : webServer->arg("p2").c_str(), sizeof(sysCfg.sta_pwd[1])); + snprintf_P(log, sizeof(log), PSTR("HTTP: Wifi Hostname %s, SSID1 %s, Password1 %s, SSID2 %s, Password2 %s"), + sysCfg.hostname, sysCfg.sta_ssid[0], sysCfg.sta_pwd[0], sysCfg.sta_ssid[1], sysCfg.sta_pwd[1]); + addLog(LOG_LEVEL_INFO, log); + result += F("
Trying to connect device to network
If it fails reconnect to try again"); + break; + case 2: + strlcpy(sysCfg.mqtt_host, (!strlen(webServer->arg("mh").c_str())) ? MQTT_HOST : webServer->arg("mh").c_str(), sizeof(sysCfg.mqtt_host)); + sysCfg.mqtt_port = (!strlen(webServer->arg("ml").c_str())) ? MQTT_PORT : atoi(webServer->arg("ml").c_str()); + strlcpy(sysCfg.mqtt_client, (!strlen(webServer->arg("mc").c_str())) ? MQTT_CLIENT_ID : webServer->arg("mc").c_str(), sizeof(sysCfg.mqtt_client)); + strlcpy(sysCfg.mqtt_user, (!strlen(webServer->arg("mu").c_str())) ? MQTT_USER : webServer->arg("mu").c_str(), sizeof(sysCfg.mqtt_user)); + strlcpy(sysCfg.mqtt_pwd, (!strlen(webServer->arg("mp").c_str())) ? MQTT_PASS : webServer->arg("mp").c_str(), sizeof(sysCfg.mqtt_pwd)); + strlcpy(sysCfg.mqtt_topic, (!strlen(webServer->arg("mt").c_str())) ? MQTT_TOPIC : webServer->arg("mt").c_str(), sizeof(sysCfg.mqtt_topic)); + snprintf_P(log, sizeof(log), PSTR("HTTP: MQTT Host %s, Port %d, Client %s, User %s, Password %s, Topic %s"), + sysCfg.mqtt_host, sysCfg.mqtt_port, sysCfg.mqtt_client, sysCfg.mqtt_user, sysCfg.mqtt_pwd, sysCfg.mqtt_topic); + addLog(LOG_LEVEL_INFO, log); + break; + case 3: + sysCfg.seriallog_level = (!strlen(webServer->arg("ls").c_str())) ? SERIAL_LOG_LEVEL : atoi(webServer->arg("ls").c_str()); + sysCfg.weblog_level = (!strlen(webServer->arg("lw").c_str())) ? WEB_LOG_LEVEL : atoi(webServer->arg("lw").c_str()); + sysCfg.syslog_level = (!strlen(webServer->arg("ll").c_str())) ? SYS_LOG_LEVEL : atoi(webServer->arg("ll").c_str()); + syslog_level = sysCfg.syslog_level; + syslog_timer = 0; + strlcpy(sysCfg.syslog_host, (!strlen(webServer->arg("lh").c_str())) ? SYS_LOG_HOST : webServer->arg("lh").c_str(), sizeof(sysCfg.syslog_host)); + sysCfg.syslog_port = (!strlen(webServer->arg("lp").c_str())) ? SYS_LOG_PORT : atoi(webServer->arg("lp").c_str()); + sysCfg.tele_period = (!strlen(webServer->arg("lt").c_str())) ? TELE_PERIOD : atoi(webServer->arg("lt").c_str()); + snprintf_P(log, sizeof(log), PSTR("HTTP: Logging Seriallog %d, Weblog %d, Syslog %d, Host %s, Port %d, TelePeriod %d"), + sysCfg.seriallog_level, sysCfg.weblog_level, sysCfg.syslog_level, sysCfg.syslog_host, sysCfg.syslog_port, sysCfg.tele_period); + addLog(LOG_LEVEL_INFO, log); + break; +#ifdef USE_DOMOTICZ + case 4: + domoticz_saveSettings(); + break; +#endif // USE_DOMOTICZ + case 5: + sysCfg.mqtt_enabled = webServer->hasArg("r1"); + strlcpy(sysCfg.friendlyname[0], (!strlen(webServer->arg("a1").c_str())) ? FRIENDLY_NAME1 : webServer->arg("a1").c_str(), sizeof(sysCfg.friendlyname[0])); + strlcpy(sysCfg.friendlyname[1], (!strlen(webServer->arg("a2").c_str())) ? FRIENDLY_NAME2 : webServer->arg("a2").c_str(), sizeof(sysCfg.friendlyname[1])); + strlcpy(sysCfg.friendlyname[2], (!strlen(webServer->arg("a3").c_str())) ? FRIENDLY_NAME3 : webServer->arg("a3").c_str(), sizeof(sysCfg.friendlyname[2])); + strlcpy(sysCfg.friendlyname[3], (!strlen(webServer->arg("a4").c_str())) ? FRIENDLY_NAME4 : webServer->arg("a4").c_str(), sizeof(sysCfg.friendlyname[3])); + snprintf_P(log, sizeof(log), PSTR("HTTP: Other MQTT Enable %s, Friendly Names %s, %s, %s and %s"), + (sysCfg.mqtt_enabled) ? MQTT_STATUS_ON : MQTT_STATUS_OFF, sysCfg.friendlyname[0], sysCfg.friendlyname[1], sysCfg.friendlyname[2], sysCfg.friendlyname[3]); + addLog(LOG_LEVEL_INFO, log); + break; + case 6: + sysCfg.module = (!strlen(webServer->arg("mt").c_str())) ? MODULE : atoi(webServer->arg("mt").c_str()); + mytmplt cmodule; + memcpy_P(&cmodule, &modules[sysCfg.module], sizeof(cmodule)); + String gpios = ""; + for (byte i = 0; i < MAX_GPIO_PIN; i++) { + if (cmodule.gp.io[i] == GPIO_USER) { + snprintf_P(stemp, sizeof(stemp), PSTR("g%d"), i); + sysCfg.my_module.gp.io[i] = (!strlen(webServer->arg(stemp).c_str())) ? 0 : atoi(webServer->arg(stemp).c_str()); + gpios += F(", GPIO"); gpios += String(i); gpios += F(" "); gpios += String(sysCfg.my_module.gp.io[i]); + } + } + snprintf_P(stemp, sizeof(stemp), modules[sysCfg.module].name); + snprintf_P(log, sizeof(log), PSTR("HTTP: %s Module%s"), stemp, gpios.c_str()); + addLog(LOG_LEVEL_INFO, log); + break; + } + + restart = (!strlen(webServer->arg("r").c_str())) ? 1 : atoi(webServer->arg("r").c_str()); + if (restart) { + String page = FPSTR(HTTP_HEAD); + page.replace("{v}", "Save parameters"); + page += F("
Parameters saved
"); + page += result; + page += F("
"); + page += FPSTR(HTTP_MSG_RSTRT); + if (_httpflag == HTTP_MANAGER) { + _httpflag = HTTP_ADMIN; + } else { + page += FPSTR(HTTP_BTN_MAIN); + } + showPage(page); + + restartflag = 2; + } else { + handleConfig(); + } +} + +void handleReset() +{ + if (_httpflag == HTTP_USER) { + handleRoot(); + return; + } + char svalue[MESSZ]; + + addLog_P(LOG_LEVEL_DEBUG, PSTR("HTTP: Reset parameters")); + + String page = FPSTR(HTTP_HEAD); + page.replace("{v}", "Default parameters"); + page += F("
Parameters reset to default
"); + page += FPSTR(HTTP_MSG_RSTRT); + page += FPSTR(HTTP_BTN_MAIN); + showPage(page); + + snprintf_P(svalue, sizeof(svalue), PSTR("reset 1")); + do_cmnd(svalue); +} + +void handleUpgrade() +{ + if (_httpflag == HTTP_USER) { + handleRoot(); + return; + } + addLog_P(LOG_LEVEL_DEBUG, PSTR("HTTP: Handle upgrade")); + + String page = FPSTR(HTTP_HEAD); + page.replace("{v}", "Firmware upgrade"); + page += FPSTR(HTTP_FORM_UPG); + page.replace("{o1}", String(sysCfg.otaUrl)); + page += FPSTR(HTTP_BTN_MAIN); + showPage(page); + + _uploaderror = 0; +} + +void handleUpgradeStart() +{ + if (_httpflag == HTTP_USER) { + handleRoot(); + return; + } + char svalue[MESSZ]; + + addLog_P(LOG_LEVEL_DEBUG, PSTR("HTTP: Firmware upgrade start")); + WIFI_configCounter(); + + if (strlen(webServer->arg("o").c_str())) { + snprintf_P(svalue, sizeof(svalue), PSTR("otaurl %s"), webServer->arg("o").c_str()); + do_cmnd(svalue); + } + + String page = FPSTR(HTTP_HEAD); + page.replace("{v}", "Info"); + page += F("
Upgrade started ...
"); + page += FPSTR(HTTP_MSG_RSTRT); + page += FPSTR(HTTP_BTN_MAIN); + showPage(page); + + snprintf_P(svalue, sizeof(svalue), PSTR("upgrade 1")); + do_cmnd(svalue); +} + +void handleUploadDone() +{ + if (_httpflag == HTTP_USER) { + handleRoot(); + return; + } + + addLog_P(LOG_LEVEL_DEBUG, PSTR("HTTP: Firmware upload done")); + WIFI_configCounter(); + restartflag = 0; + mqttcounter = 0; + + String page = FPSTR(HTTP_HEAD); + page.replace("{v}", "Info"); + page += F("

"); + page += FPSTR(HTTP_BTN_MAIN); + showPage(page); +} + +void handleUploadLoop() +{ + // Based on ESP8266HTTPUpdateServer.cpp uses ESP8266WebServer Parsing.cpp and Cores Updater.cpp (Update) + char log[LOGSZ]; + boolean _serialoutput = (LOG_LEVEL_DEBUG <= seriallog_level); + + if (_httpflag == HTTP_USER) return; + if (_uploaderror) { + Update.end(); + return; + } + + HTTPUpload& upload = webServer->upload(); + + if (upload.status == UPLOAD_FILE_START) { + restartflag = 60; + mqttcounter = 60; + if (upload.filename.c_str()[0] == 0) + { + _uploaderror = 1; + return; + } +#if defined(USE_WEMO_EMULATION) || defined(USE_HUE_EMULATION) + UDP_Disconnect(); +#endif // USE_WEMO_EMULATION || USE_HUE_EMULATION + if (sysCfg.mqtt_enabled) mqttClient.disconnect(); + + snprintf_P(log, sizeof(log), PSTR("Upload: File %s ..."), upload.filename.c_str()); + addLog(LOG_LEVEL_INFO, log); + + uint32_t maxSketchSpace = (ESP.getFreeSketchSpace() - 0x1000) & 0xFFFFF000; + if (!Update.begin(maxSketchSpace)) { //start with max available size + if (_serialoutput) Update.printError(Serial); + _uploaderror = 2; + return; + } + _colcount = 0; + } else if (!_uploaderror && (upload.status == UPLOAD_FILE_WRITE)) { + if (upload.totalSize == 0) + { + if (upload.buf[0] != 0xE9) { + addLog_P(LOG_LEVEL_DEBUG, PSTR("Upload: File magic header does not start with 0xE9")); + _uploaderror = 3; + return; + } + uint32_t bin_flash_size = ESP.magicFlashChipSize((upload.buf[3] & 0xf0) >> 4); + if(bin_flash_size > ESP.getFlashChipRealSize()) { + addLog_P(LOG_LEVEL_DEBUG, PSTR("Upload: File flash size is larger than device flash size")); + _uploaderror = 4; + return; + } + if ((sysCfg.module == SONOFF_TOUCH) || (sysCfg.module == SONOFF_4CH) || (ESP.getFlashChipMode() == 3)) { + upload.buf[2] = 3; // DOUT - ESP8285 + } else { + upload.buf[2] = 2; // DIO - ESP8266 + } +// snprintf_P(log, sizeof(log), PSTR("Upload: Flash Chip Mode %02X"), upload.buf[2]); +// addLog(LOG_LEVEL_DEBUG, log); + } + if (!_uploaderror && (Update.write(upload.buf, upload.currentSize) != upload.currentSize)) { + if (_serialoutput) Update.printError(Serial); + _uploaderror = 5; + return; + } + if (_serialoutput) { + Serial.printf("."); + _colcount++; + if (!(_colcount % 80)) Serial.println(); + } + } else if(!_uploaderror && (upload.status == UPLOAD_FILE_END)){ + if (_serialoutput && (_colcount % 80)) Serial.println(); + if (Update.end(true)) { // true to set the size to the current progress + snprintf_P(log, sizeof(log), PSTR("Upload: Successful %u bytes. Restarting"), upload.totalSize); + addLog(LOG_LEVEL_INFO, log); + } else { + if (_serialoutput) Update.printError(Serial); + _uploaderror = 6; + return; + } + } else if(upload.status == UPLOAD_FILE_ABORTED) { + addLog_P(LOG_LEVEL_DEBUG, PSTR("Upload: Update was aborted")); + restartflag = 0; + mqttcounter = 0; + _uploaderror = 7; + Update.end(); + } + delay(0); +} + +void handleCmnd() +{ + if (_httpflag == HTTP_USER) { + handleRoot(); + return; + } + char svalue[MESSZ]; + + addLog_P(LOG_LEVEL_DEBUG, PSTR("HTTP: Handle cmnd")); + + byte curridx = logidx; + if (strlen(webServer->arg(0).c_str())) { + snprintf_P(svalue, sizeof(svalue), webServer->arg(0).c_str()); + do_cmnd(svalue); + } + + String message = ""; + if (logidx != curridx) { + byte counter = curridx; + do { + if (Log[counter].length()) { + if (message.length()) message += F("\n"); + if (sysCfg.mqtt_enabled) { + // [14:49:36 MQTT: stat/wemos5/RESULT = {"POWER":"OFF"}] > [RESULT = {"POWER":"OFF"}] +// message += Log[counter].substring(17 + strlen(PUB_PREFIX) + strlen(sysCfg.mqtt_topic)); + message += Log[counter].substring(Log[counter].lastIndexOf("/",Log[counter].indexOf("="))+1); + } else { + // [14:49:36 RSLT: RESULT = {"POWER":"OFF"}] > [RESULT = {"POWER":"OFF"}] + message += Log[counter].substring(Log[counter].indexOf(": ")+2); + } + } + counter++; + if (counter > MAX_LOG_LINES -1) counter = 0; + } while (counter != logidx); + } else { + message = F("Enable weblog 2 if response expected\n"); + } + + webServer->sendHeader("Cache-Control", "no-cache, no-store, must-revalidate"); + webServer->sendHeader("Pragma", "no-cache"); + webServer->sendHeader("Expires", "-1"); + webServer->send(200, "text/plain", message); +} + +void handleConsole() +{ + if (_httpflag == HTTP_USER) { + handleRoot(); + return; + } + char svalue[MESSZ]; + + addLog_P(LOG_LEVEL_DEBUG, PSTR("HTTP: Handle console")); + + if (strlen(webServer->arg("c1").c_str())) { + snprintf_P(svalue, sizeof(svalue), webServer->arg("c1").c_str()); + do_cmnd(svalue); + } + + String page = FPSTR(HTTP_HEAD); + page.replace("{v}", "Console"); + page.replace("", ""); + page += FPSTR(HTTP_FORM_CMND); + page += FPSTR(HTTP_BTN_MAIN); + showPage(page); +} + +void handleAjax() +{ + if (_httpflag == HTTP_USER) { + handleRoot(); + return; + } + String message = ""; + uint16_t size = 0; + + int maxSize = ESP.getFreeHeap() - 6000; + + byte counter = logidx; + do { + counter--; + if (counter == 255) counter = MAX_LOG_LINES -1; + size += Log[counter].length(); + } while ((counter != logidx) && (size < maxSize)); + do { + if (Log[counter].length()) { + if (message.length()) message += F("\n"); + message += Log[counter]; + } + counter++; + if (counter > MAX_LOG_LINES -1) counter = 0; + } while (counter != logidx); + webServer->sendHeader("Cache-Control", "no-cache, no-store, must-revalidate"); + webServer->sendHeader("Pragma", "no-cache"); + webServer->sendHeader("Expires", "-1"); + webServer->send(200, "text/plain", message); +} + +void handleInfo() +{ + if (_httpflag == HTTP_USER) { + handleRoot(); + return; + } + addLog_P(LOG_LEVEL_DEBUG, PSTR("HTTP: Handle info")); + + int freeMem = ESP.getFreeHeap(); + + String page = FPSTR(HTTP_HEAD); + page.replace("{v}", "Information"); +// page += F("
 Information "); + page += F(""); + page += F(""); + page += F(""); + page += F(""); +// page += F(""); + page += F(""); + page += F(""); + page += F(""); + page += F(""); + for (byte i = 0; i < Maxdevice; i++) { + page += F(""); + } + page += F(""); +// page += F(""); + page += F(""); + page += F(""); + if (static_cast(WiFi.localIP()) != 0) { + page += F(""); + page += F(""); + page += F(""); + } + if (static_cast(WiFi.softAPIP()) != 0) { + page += F(""); + page += F(""); + page += F(""); + } + page += F(""); + if (sysCfg.mqtt_enabled) { + page += F(""); + page += F(""); + page += F(""); + page += F(""); +// page += F(""); + page += F(""); + page += F(""); + } else { + page += F(""); + } + page += F(""); + page += F(""); + page += F(""); + page += F(""); + page += F(""); + page += F(""); + page += F(""); + page += F(""); + page += F("
Program version"); page += Version; page += F("
Core/SDK version"); page += ESP.getCoreVersion(); page += F("/"); page += String(ESP.getSdkVersion()); page += F("
Boot version"); page += String(ESP.getBootVersion()); page += F("
Uptime"); page += String(uptime); page += F(" Hours
Flash write count"); page += String(sysCfg.saveFlag); page += F("
Boot count"); page += String(sysCfg.bootcount); page += F("
Reset reason"); page += ESP.getResetReason(); page += F("
Friendly name "); + page += i +1; + page += F(""); page += String(sysCfg.friendlyname[i]); page += F("
 
SSId (RSSI)"); page += (sysCfg.sta_active)? sysCfg.sta_ssid2 : sysCfg.sta_ssid1; page += F(" ("); page += WIFI_getRSSIasQuality(WiFi.RSSI()); page += F("%)
AP"); page += String(sysCfg.sta_active +1); page += F(" SSId (RSSI)"); page += sysCfg.sta_ssid[sysCfg.sta_active]; page += F(" ("); page += WIFI_getRSSIasQuality(WiFi.RSSI()); page += F("%)
Hostname"); page += Hostname; page += F("
IP address"); page += WiFi.localIP().toString(); page += F("
Gateway"); page += WiFi.gatewayIP().toString(); page += F("
MAC address"); page += WiFi.macAddress(); page += F("
AP IP address"); page += WiFi.softAPIP().toString(); page += F("
AP Gateway"); page += WiFi.softAPIP().toString(); page += F("
AP MAC address"); page += WiFi.softAPmacAddress(); page += F("
 
MQTT Host"); page += sysCfg.mqtt_host; page += F("
MQTT Port"); page += String(sysCfg.mqtt_port); page += F("
MQTT Client and
 Fallback Topic
"); page += MQTTClient; page += F("
MQTT User"); page += sysCfg.mqtt_user; page += F("
MQTT Password"); page += sysCfg.mqtt_pwd; page += F("
MQTT Topic"); page += sysCfg.mqtt_topic; page += F("
MQTT Group Topic"); page += sysCfg.mqtt_grptopic; page += F("
MQTTDisabled
 
ESP Chip id"); page += String(ESP.getChipId()); page += F("
Flash Chip id"); page += String(ESP.getFlashChipId()); page += F("
Flash size"); page += String(ESP.getFlashChipRealSize() / 1024); page += F("kB
Program flash size"); page += String(ESP.getFlashChipSize() / 1024); page += F("kB
Program size"); page += String(ESP.getSketchSize() / 1024); page += F("kB
Free program space"); page += String(ESP.getFreeSketchSpace() / 1024); page += F("kB
Free memory"); page += String(freeMem / 1024); page += F("kB
"); +// page += F("
"); + page += FPSTR(HTTP_BTN_MAIN); + showPage(page); +} + +void handleRestart() +{ + if (_httpflag == HTTP_USER) { + handleRoot(); + return; + } + addLog_P(LOG_LEVEL_DEBUG, PSTR("HTTP: Restarting")); + + String page = FPSTR(HTTP_HEAD); + page.replace("{v}", "Info"); + page += FPSTR(HTTP_MSG_RSTRT); + if (_httpflag == HTTP_MANAGER) { + _httpflag = HTTP_ADMIN; + } else { + page += FPSTR(HTTP_BTN_MAIN); + } + showPage(page); + + restartflag = 2; +} + +/********************************************************************************************/ + +#ifdef USE_WEMO_EMULATION +void handleUPnPevent() +{ + addLog_P(LOG_LEVEL_DEBUG, PSTR("HTTP: Handle WeMo basic event")); + + String request = webServer->arg(0); + if(request.indexOf("State>1 0) do_cmnd_power(1, 1); + if(request.indexOf("State>0 0) do_cmnd_power(1, 0); + webServer->send(200, "text/plain", ""); +} + +void handleUPnPservice() +{ + addLog_P(LOG_LEVEL_DEBUG, PSTR("HTTP: Handle WeMo event service")); + + String eventservice_xml = FPSTR(WEMO_EVENTSERVICE_XML); + webServer->send(200, "text/plain", eventservice_xml); +} + +void handleUPnPsetup() +{ + addLog_P(LOG_LEVEL_DEBUG, PSTR("HTTP: Handle WeMo setup")); + + String setup_xml = FPSTR(WEMO_SETUP_XML); +// setup_xml.replace("{x1}", String(MQTTClient)); + setup_xml.replace("{x1}", String(sysCfg.friendlyname[0])); + setup_xml.replace("{x2}", wemo_UUID()); + setup_xml.replace("{x3}", wemo_serial()); + webServer->send(200, "text/xml", setup_xml); +} +#endif // USE_WEMO_EMULATION + +/********************************************************************************************/ + +#ifdef USE_HUE_EMULATION +String hue_deviceId(uint8_t id) +{ + char deviceid[16]; + + snprintf_P(deviceid, sizeof(deviceid), PSTR("5CCF7F%03X-%0d"), ESP.getChipId(), id); + return String(deviceid); +} + +void handleUPnPsetup() +{ + addLog_P(LOG_LEVEL_DEBUG, PSTR("HTTP: Handle Hue Bridge setup")); + + String description_xml = FPSTR(HUE_DESCRIPTION_XML); + description_xml.replace("{x1}", WiFi.localIP().toString()); + description_xml.replace("{x2}", hue_UUID()); + webServer->send(200, "text/xml", description_xml); +} + +void hue_todo(String *path) +{ + char log[LOGSZ]; + + snprintf_P(log, sizeof(log), PSTR("HTTP: HUE API not implemented (%s)"),path->c_str()); + addLog(LOG_LEVEL_DEBUG_MORE, log); +} + +void hue_lights(String *path) +{ + String response; + uint8_t device = 1; + int16_t pos = 0; + uint8_t bri = 0; + char id[4]; + + path->remove(0,path->indexOf("/lights")); // Remove until /lights + if (path->endsWith("/lights")) // Got /lights + { + response = "{\""; + for (uint8_t i = 1; i <= Maxdevice; i++) + { + response += i; + response += "\":"; + response += FPSTR(HUE_LIGHT_STATUS_JSON); + if (i < Maxdevice) response += ",\""; + response.replace("{state}", (power & (0x01 << (i-1))) ? "true" : "false"); + response.replace("{j1}", sysCfg.friendlyname[i-1]); + response.replace("{j2}", hue_deviceId(i)); +#ifdef USE_WS2812 + ws2812_replaceHSB(&response); +#else + response.replace("{h}", "0"); + response.replace("{s}", "0"); + response.replace("{b}", "0"); +#endif // USE_WS2812 + } + response += "}"; + webServer->send(200, "application/json", response); + } + else if (path->endsWith("/state")) // Got ID/state + { + path->remove(0,8); // Remove /lights/ + path->remove(path->indexOf("/state")); // Remove /state + device = atoi(path->c_str()); + if ((device < 1) || (device > Maxdevice)) device = 1; + response = "["; + response += FPSTR(HUE_LIGHT_RESPONSE_JSON); + response.replace("{api}", "/lights"); + response.replace("{id}", String(device)); + response.replace("{cmd}", "state/on"); + if (webServer->args() == 1) + { + String json = webServer->arg(0); +// Serial.print("HUE API: POST "); Serial.println(json.c_str()); + if (json.indexOf("\"on\":") >= 0) // Got "on" command + { + if (json.indexOf("false") >= 0) // false -> turn device off + { + do_cmnd_power(device, 0); + response.replace("{res}", "false"); + } + else if(json.indexOf("true") >= 0) // true -> Turn device on + { + do_cmnd_power(device, 1); + response.replace("{res}", "true"); + } + else + { + response.replace("{res}", (power & (0x01 << (device-1))) ? "true" : "false"); + } + } +#ifdef USE_WS2812 + if ((pos=json.indexOf("\"bri\":")) >= 0) + { + bri=atoi(json.substring(pos+6).c_str()); + ws2812_changeBrightness(bri); + response += ","; + response += FPSTR(HUE_LIGHT_RESPONSE_JSON); + response.replace("{api}", "/lights"); + response.replace("{id}", String(device)); + response.replace("{cmd}", "state/bri"); + response.replace("{res}", String(bri)); + } +#endif // USE_WS2812 + response += "]"; + webServer->send(200, "application/json", response); + } + else webServer->send(406, "application/json", "{}"); + } + else if(path->indexOf("/lights/") >= 0) // Got /lights/ID + { + path->remove(0,8); // Remove /lights/ + device = atoi(path->c_str()); + if ((device < 1) || (device > Maxdevice)) device = 1; + response = FPSTR(HUE_LIGHT_STATUS_JSON); + response.replace("{state}", (power & (0x01 << (device -1))) ? "true" : "false"); + response.replace("{j1}", sysCfg.friendlyname[device -1]); + response.replace("{j2}", hue_deviceId(device)); +#ifdef USE_WS2812 + ws2812_replaceHSB(&response); +#else + response.replace("{h}", "0"); + response.replace("{s}", "0"); + response.replace("{b}", "0"); +#endif // USE_WS2812 + webServer->send(200, "application/json", response); + } + else webServer->send(406, "application/json", "{}"); +} + +void handle_hue_api(String *path) +{ + /* HUE API uses /api// syntax. The userid is created by the echo device and + * on original HUE the pressed button allows for creation of this user. We simply ignore the + * user part and allow every caller as with Web or WeMo. + * + * (c) Heiko Krupp, 2017 + */ + + char log[LOGSZ]; + + path->remove(0, 4); // remove /api + if (path->endsWith("/invalid/")) {} // Just ignore + else if (path->endsWith("/config")) hue_todo(path); + else if(path->indexOf("/lights") >= 0) hue_lights(path); + else if(path->endsWith("/groups")) hue_todo(path); + else if(path->endsWith("/schedules")) hue_todo(path); + else if(path->endsWith("/sensors")) hue_todo(path); + else if(path->endsWith("/scenes")) hue_todo(path); + else if(path->endsWith("/rules")) hue_todo(path); + else + { + snprintf_P(log, sizeof(log), PSTR("HTTP: Handle Hue API (%s)"),path->c_str()); + addLog(LOG_LEVEL_DEBUG_MORE, log); + webServer->send(406, "application/json", "{}"); + } +} +#endif // USE_HUE_EMULATION + +/********************************************************************************************/ + +void handleNotFound() +{ + if (captivePortal()) { // If captive portal redirect instead of displaying the error page. + return; + } + +#ifdef USE_HUE_EMULATION + String path = webServer->uri(); + if (path.startsWith("/api")) + handle_hue_api(&path); + else { +#endif // USE_HUE_EMULATION + + String message = "File Not Found\n\n"; + message += "URI: "; + message += webServer->uri(); + message += "\nMethod: "; + message += ( webServer->method() == HTTP_GET ) ? "GET" : "POST"; + message += "\nArguments: "; + message += webServer->args(); + message += "\n"; + for ( uint8_t i = 0; i < webServer->args(); i++ ) { + message += " " + webServer->argName ( i ) + ": " + webServer->arg ( i ) + "\n"; + } + + webServer->sendHeader("Cache-Control", "no-cache, no-store, must-revalidate"); + webServer->sendHeader("Pragma", "no-cache"); + webServer->sendHeader("Expires", "-1"); + webServer->send(404, "text/plain", message); +#ifdef USE_HUE_EMULATION + addLog_P(LOG_LEVEL_DEBUG_MORE, message.c_str()); + } +#endif // USE_HUE_EMULATION +} + +/* Redirect to captive portal if we got a request for another domain. Return true in that case so the page handler do not try to handle the request again. */ +boolean captivePortal() +{ + if (!isIp(webServer->hostHeader())) { + addLog_P(LOG_LEVEL_DEBUG, PSTR("HTTP: Request redirected to captive portal")); + + webServer->sendHeader("Location", String("http://") + webServer->client().localIP().toString(), true); + webServer->send(302, "text/plain", ""); // Empty content inhibits Content-length header so we have to close the socket ourselves. + webServer->client().stop(); // Stop is needed because we sent no content length + return true; + } + return false; +} + +/** Is this an IP? */ +boolean isIp(String str) +{ + for (uint16_t i = 0; i < str.length(); i++) { + int c = str.charAt(i); + if (c != '.' && (c < '0' || c > '9')) { + return false; + } + } + return true; +} + +#endif // USE_WEBSERVER diff --git a/sonoff/xdrv_domoticz.ino b/sonoff/xdrv_domoticz.ino new file mode 100644 index 000000000..b00c5c217 --- /dev/null +++ b/sonoff/xdrv_domoticz.ino @@ -0,0 +1,376 @@ +/* +Copyright (c) 2017 Theo Arends. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +- Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. +- Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. +*/ + +#ifdef USE_DOMOTICZ + +#define DOMOTICZ_MAX_SENSORS 5 + +const char HTTP_FORM_DOMOTICZ[] PROGMEM = + "
 Domoticz parameters 
" + "" + "
" + "" + ""; + +const char domoticz_sensors[DOMOTICZ_MAX_SENSORS][14] PROGMEM = + { "Temp", "Temp,Hum", "Temp,Hum,Baro", "Power,Energy", "Illuminance" }; + +int domoticz_update_timer = 0; +byte domoticz_update_flag = 1; + +unsigned long getKeyIntValue(const char *json, const char *key) +{ + char *p, *b, log[LOGSZ]; + int i; + + // search key + p = strstr(json, key); + if (!p) return 0; + // search following separator : + b = strchr(p + strlen(key), ':'); + if (!b) return 0; + // Only the following chars are allowed between key and separator : + for(i = b - json + strlen(key); i < p-json; i++) { + switch (json[i]) { + case ' ': + case '\n': + case '\t': + case '\r': + continue; + default: + return 0; + } + } + b++; + // Allow integers as string too (used in "svalue" : "9") + while ((b[0] == ' ') || (b[0] == '"')) b++; + // Convert to integer + return atoi(b); +} + +void mqtt_publishDomoticzPowerState(byte device) +{ + char svalue[MESSZ]; + + if (sysCfg.domoticz_relay_idx[device -1] && (strlen(sysCfg.domoticz_in_topic) != 0)) { + if ((device < 1) || (device > Maxdevice)) device = 1; + + if (sysCfg.module == SONOFF_LED) { + snprintf_P(svalue, sizeof(svalue), PSTR("{\"idx\":%d,\"nvalue\":2,\"svalue\":\"%d\"}"), + sysCfg.domoticz_relay_idx[device -1], sysCfg.led_dimmer[device -1]); + mqtt_publish(sysCfg.domoticz_in_topic, svalue); + } + else if ((device == 1) && (pin[GPIO_WS2812] < 99)) { + snprintf_P(svalue, sizeof(svalue), PSTR("{\"idx\":%d,\"nvalue\":2,\"svalue\":\"%d\"}"), + sysCfg.domoticz_relay_idx[device -1], sysCfg.ws_dimmer); + mqtt_publish(sysCfg.domoticz_in_topic, svalue); + } + snprintf_P(svalue, sizeof(svalue), PSTR("{\"idx\":%d,\"nvalue\":%d,\"svalue\":\"\"}"), + sysCfg.domoticz_relay_idx[device -1], (power & (0x01 << (device -1))) ? 1 : 0); + mqtt_publish(sysCfg.domoticz_in_topic, svalue); + } +} + +void domoticz_updatePowerState(byte device) +{ + if (domoticz_update_flag) mqtt_publishDomoticzPowerState(device); + domoticz_update_flag = 1; +} + +void domoticz_mqttUpdate() +{ + if ((sysCfg.domoticz_update_timer || domoticz_update_timer) && sysCfg.domoticz_relay_idx[0]) { + domoticz_update_timer--; + if (domoticz_update_timer <= 0) { + domoticz_update_timer = sysCfg.domoticz_update_timer; + for (byte i = 1; i <= Maxdevice; i++) mqtt_publishDomoticzPowerState(i); + } + } +} + +void domoticz_setUpdateTimer(uint16_t value) +{ + domoticz_update_timer = value; +} + +void domoticz_mqttSubscribe() +{ + if (sysCfg.domoticz_relay_idx[0] && (strlen(sysCfg.domoticz_out_topic) != 0)) { + char stopic[TOPSZ]; + snprintf_P(stopic, sizeof(stopic), PSTR("%s/#"), sysCfg.domoticz_out_topic); // domoticz topic + mqttClient.subscribe(stopic); + mqttClient.loop(); // Solve LmacRxBlk:1 messages + } +} + +boolean domoticz_update() +{ + return domoticz_update_flag; +} + +boolean domoticz_mqttData(char *topicBuf, uint16_t stopicBuf, char *dataBuf, uint16_t sdataBuf) +{ + char log[LOGSZ], stemp1[10]; + unsigned long idx = 0; + int16_t nvalue, found = 0; + + domoticz_update_flag = 1; + if (!strncmp(topicBuf, sysCfg.domoticz_out_topic, strlen(sysCfg.domoticz_out_topic)) != 0) { + if (sdataBuf < 20) return 1; + idx = getKeyIntValue(dataBuf,"\"idx\""); + nvalue = getKeyIntValue(dataBuf,"\"nvalue\""); + + snprintf_P(log, sizeof(log), PSTR("DMTZ: idx %d, nvalue %d"), idx, nvalue); + addLog(LOG_LEVEL_DEBUG_MORE, log); + + if (nvalue >= 0 && nvalue <= 2) { + for (byte i = 0; i < Maxdevice; i++) { + if ((idx > 0) && (idx == sysCfg.domoticz_relay_idx[i])) { + snprintf_P(stemp1, sizeof(stemp1), PSTR("%d"), i +1); + if (nvalue == 2) { + nvalue = getKeyIntValue(dataBuf,"\"svalue1\""); + if ((pin[GPIO_WS2812] < 99) && (sysCfg.ws_dimmer == nvalue)) return 1; + if ((sysCfg.module == SONOFF_LED) && (sysCfg.led_dimmer[i] == nvalue)) return 1; + snprintf_P(topicBuf, stopicBuf, PSTR("%s/%s/DIMMER%s"), + SUB_PREFIX, sysCfg.mqtt_topic, (Maxdevice > 1) ? stemp1 : ""); + snprintf_P(dataBuf, sdataBuf, PSTR("%d"), nvalue); + found = 1; + } else { + if (((power >> i) &1) == nvalue) return 1; + snprintf_P(topicBuf, stopicBuf, PSTR("%s/%s/%s%s"), + SUB_PREFIX, sysCfg.mqtt_topic, sysCfg.mqtt_subtopic, (Maxdevice > 1) ? stemp1 : ""); + snprintf_P(dataBuf, sdataBuf, PSTR("%d"), nvalue); + found = 1; + } + break; + } + } + } + if (!found) return 1; + + snprintf_P(log, sizeof(log), PSTR("DMTZ: Receive topic %s, data %s"), topicBuf, dataBuf); + addLog(LOG_LEVEL_DEBUG_MORE, log); + + domoticz_update_flag = 0; + } + return 0; +} + +/*********************************************************************************************\ + * Commands +\*********************************************************************************************/ + +void domoticz_commands(char *type, uint16_t index, char *dataBuf, uint16_t data_len, int16_t payload, char *svalue, uint16_t ssvalue) +{ + if (!strcmp(type,"DOMOTICZINTOPIC")) { + if ((data_len > 0) && (data_len < sizeof(sysCfg.domoticz_in_topic))) { + strlcpy(sysCfg.domoticz_in_topic, (payload == 1) ? DOMOTICZ_IN_TOPIC : dataBuf, sizeof(sysCfg.domoticz_in_topic)); + restartflag = 2; + } + snprintf_P(svalue, ssvalue, PSTR("{\"DomoticzInTopic\":\"%s\"}"), sysCfg.domoticz_in_topic); + } + else if (!strcmp(type,"DOMOTICZOUTTOPIC")) { + if ((data_len > 0) && (data_len < sizeof(sysCfg.domoticz_out_topic))) { + strlcpy(sysCfg.domoticz_out_topic, (payload == 1) ? DOMOTICZ_OUT_TOPIC : dataBuf, sizeof(sysCfg.domoticz_out_topic)); + restartflag = 2; + } + snprintf_P(svalue, ssvalue, PSTR("{\"DomoticzOutTopic\":\"%s\"}"), sysCfg.domoticz_out_topic); + } + else if (!strcmp(type,"DOMOTICZIDX") && (index > 0) && (index <= Maxdevice)) { + if ((data_len > 0) && (payload >= 0)) { + sysCfg.domoticz_relay_idx[index -1] = payload; + restartflag = 2; + } + snprintf_P(svalue, ssvalue, PSTR("{\"DomoticzIdx%d\":%d}"), index, sysCfg.domoticz_relay_idx[index -1]); + } + else if (!strcmp(type,"DOMOTICZKEYIDX") && (index > 0) && (index <= Maxdevice)) { + if ((data_len > 0) && (payload >= 0)) { + sysCfg.domoticz_key_idx[index -1] = payload; + } + snprintf_P(svalue, ssvalue, PSTR("{\"DomoticzKeyIdx%d\":%d}"), index, sysCfg.domoticz_key_idx[index -1]); + } + else if (!strcmp(type,"DOMOTICZSWITCHIDX") && (index > 0) && (index <= Maxdevice)) { + if ((data_len > 0) && (payload >= 0)) { + sysCfg.domoticz_switch_idx[index -1] = payload; + } + snprintf_P(svalue, ssvalue, PSTR("{\"DomoticzSwitchIdx%d\":%d}"), index, sysCfg.domoticz_key_idx[index -1]); + } + else if (!strcmp(type,"DOMOTICZSENSORIDX") && (index > 0) && (index <= DOMOTICZ_MAX_SENSORS)) { + if ((data_len > 0) && (payload >= 0)) { + sysCfg.domoticz_sensor_idx[index -1] = payload; + } + snprintf_P(svalue, ssvalue, PSTR("{\"DomoticzSensorIdx%d\":%d}"), index, sysCfg.domoticz_sensor_idx[index -1]); + } + else if (!strcmp(type,"DOMOTICZUPDATETIMER")) { + if ((data_len > 0) && (payload >= 0) && (payload < 3601)) { + sysCfg.domoticz_update_timer = payload; + } + snprintf_P(svalue, ssvalue, PSTR("{\"DomoticzUpdateTimer\":%d}"), sysCfg.domoticz_update_timer); + } +} + +boolean domoticz_button(byte key, byte device, byte state, byte svalflg) +{ + if ((sysCfg.domoticz_key_idx[device -1] || sysCfg.domoticz_switch_idx[device -1]) && (svalflg)) { + char svalue[MESSZ]; + + snprintf_P(svalue, sizeof(svalue), PSTR("{\"command\":\"switchlight\",\"idx\":%d,\"switchcmd\":\"%s\"}"), + (key) ? sysCfg.domoticz_switch_idx[device -1] : sysCfg.domoticz_key_idx[device -1], (state) ? (state == 2) ? "Toggle" : "On" : "Off"); + mqtt_publish(sysCfg.domoticz_in_topic, svalue); + return 1; + } else { + return 0; + } +} + +/*********************************************************************************************\ + * Sensors +\*********************************************************************************************/ + +uint8_t dom_hum_stat(char *hum) +{ + uint8_t h = atoi(hum); + return (!h) ? 0 : (h < 40) ? 2 : (h > 70) ? 3 : 1; +} + +void dom_sensor(byte idx, char *data) +{ + char dmess[64]; + + if (sysCfg.domoticz_sensor_idx[idx] && (strlen(sysCfg.domoticz_in_topic) != 0)) { + snprintf_P(dmess, sizeof(dmess), PSTR("{\"idx\":%d,\"nvalue\":0,\"svalue\":\"%s\"}"), + sysCfg.domoticz_sensor_idx[idx], data); + mqtt_publish(sysCfg.domoticz_in_topic, dmess); + } +} + +void domoticz_sensor1(char *temp) +{ + dom_sensor(0, temp); +} + +void domoticz_sensor2(char *temp, char *hum) +{ + char data[16]; + snprintf_P(data, sizeof(data), PSTR("%s;%s;%d"), temp, hum, dom_hum_stat(hum)); + dom_sensor(1, data); +} + +void domoticz_sensor3(char *temp, char *hum, char *baro) +{ + char data[32]; + snprintf_P(data, sizeof(data), PSTR("%s;%s;%d;%s;5"), temp, hum, dom_hum_stat(hum), baro); + dom_sensor(2, data); +} + +void domoticz_sensor4(uint16_t power, char *energy) +{ + char data[16]; + snprintf_P(data, sizeof(data), PSTR("%d;%s"), power, energy); + dom_sensor(3, data); +} + +void domoticz_sensor5(uint16_t lux) +{ + char data[8]; + snprintf_P(data, sizeof(data), PSTR("%d"), lux); + dom_sensor(4, data); +} + +/*********************************************************************************************\ + * Presentation +\*********************************************************************************************/ + +void handleDomoticz() +{ + if (_httpflag == HTTP_USER) { + handleRoot(); + return; + } + addLog_P(LOG_LEVEL_DEBUG, PSTR("HTTP: Handle Domoticz config")); + + char stemp[20]; + + String page = FPSTR(HTTP_HEAD); + page.replace("{v}", "Configure Domoticz"); + page += FPSTR(HTTP_FORM_DOMOTICZ); + page.replace("{d1}", String(sysCfg.domoticz_in_topic)); + page.replace("{d2}", String(sysCfg.domoticz_out_topic)); + for (int i = 0; i < Maxdevice; i++) { + page += F(""); + page += F(""); + page += F(""); + page.replace("{1", String(i +1)); + page.replace("{2", String((int)sysCfg.domoticz_relay_idx[i])); + page.replace("{3", String((int)sysCfg.domoticz_key_idx[i])); + page.replace("{4", String((int)sysCfg.domoticz_switch_idx[i])); + } + for (int i = 0; i < DOMOTICZ_MAX_SENSORS; i++) { + page += F(""); + page.replace("{1", String(i +1)); + snprintf_P(stemp, sizeof(stemp), domoticz_sensors[i]); + page.replace("{2", stemp); + page.replace("{4", String((int)sysCfg.domoticz_sensor_idx[i])); + } + page += F(""); + page.replace("{d7}", String((int)sysCfg.domoticz_update_timer)); + page += F("
In topic (" DOMOTICZ_IN_TOPIC ")
Out topic (" DOMOTICZ_OUT_TOPIC ")
Idx {1
Key idx {1
Switch idx {1
Sensor idx {1 - {2
Update timer (" STR(DOMOTICZ_UPDATE_TIMER) ")
"); + page += FPSTR(HTTP_FORM_END); + page += FPSTR(HTTP_BTN_CONF); + showPage(page); +} + +void domoticz_saveSettings() +{ + char log[LOGSZ], stemp[20]; + + strlcpy(sysCfg.domoticz_in_topic, (!strlen(webServer->arg("it").c_str())) ? DOMOTICZ_IN_TOPIC : webServer->arg("it").c_str(), sizeof(sysCfg.domoticz_in_topic)); + strlcpy(sysCfg.domoticz_out_topic, (!strlen(webServer->arg("ot").c_str())) ? DOMOTICZ_OUT_TOPIC : webServer->arg("ot").c_str(), sizeof(sysCfg.domoticz_out_topic)); + for (byte i = 0; i < 4; i++) { + snprintf_P(stemp, sizeof(stemp), PSTR("r%d"), i +1); + sysCfg.domoticz_relay_idx[i] = (!strlen(webServer->arg(stemp).c_str())) ? 0 : atoi(webServer->arg(stemp).c_str()); + snprintf_P(stemp, sizeof(stemp), PSTR("k%d"), i +1); + sysCfg.domoticz_key_idx[i] = (!strlen(webServer->arg(stemp).c_str())) ? 0 : atoi(webServer->arg(stemp).c_str()); + snprintf_P(stemp, sizeof(stemp), PSTR("s%d"), i +1); + sysCfg.domoticz_switch_idx[i] = (!strlen(webServer->arg(stemp).c_str())) ? 0 : atoi(webServer->arg(stemp).c_str()); + } + for (byte i = 0; i < DOMOTICZ_MAX_SENSORS; i++) { + snprintf_P(stemp, sizeof(stemp), PSTR("l%d"), i +1); + sysCfg.domoticz_sensor_idx[i] = (!strlen(webServer->arg(stemp).c_str())) ? 0 : atoi(webServer->arg(stemp).c_str()); + } + sysCfg.domoticz_update_timer = (!strlen(webServer->arg("ut").c_str())) ? DOMOTICZ_UPDATE_TIMER : atoi(webServer->arg("ut").c_str()); + snprintf_P(log, sizeof(log), PSTR("HTTP: Domoticz in %s, out %s, idx %d, %d, %d, %d, update timer %d"), + sysCfg.domoticz_in_topic, sysCfg.domoticz_out_topic, + sysCfg.domoticz_relay_idx[0], sysCfg.domoticz_relay_idx[1], sysCfg.domoticz_relay_idx[2], sysCfg.domoticz_relay_idx[3], + sysCfg.domoticz_update_timer); + addLog(LOG_LEVEL_INFO, log); + snprintf_P(log, sizeof(log), PSTR("HTTP: key %d, %d, %d, %d, switch %d, %d, %d, %d, sensor %d, %d, %d, %d, %d"), + sysCfg.domoticz_key_idx[0], sysCfg.domoticz_key_idx[1], sysCfg.domoticz_key_idx[2], sysCfg.domoticz_key_idx[3], + sysCfg.domoticz_switch_idx[0], sysCfg.domoticz_switch_idx[1], sysCfg.domoticz_switch_idx[2], sysCfg.domoticz_switch_idx[3], + sysCfg.domoticz_sensor_idx[0], sysCfg.domoticz_sensor_idx[1], sysCfg.domoticz_sensor_idx[2], sysCfg.domoticz_sensor_idx[3], sysCfg.domoticz_sensor_idx[4]); + addLog(LOG_LEVEL_INFO, log); +} +#endif // USE_DOMOTICZ + diff --git a/sonoff/xdrv_wemohue.ino b/sonoff/xdrv_wemohue.ino new file mode 100644 index 000000000..7f2e47752 --- /dev/null +++ b/sonoff/xdrv_wemohue.ino @@ -0,0 +1,233 @@ +/* +Copyright (c) 2017 Heiko Krupp and Theo Arends. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +- Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. +- Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. +*/ + +#if defined(USE_WEMO_EMULATION) || defined(USE_HUE_EMULATION) + +#define UDP_BUFFER_SIZE 200 // Max UDP buffer size needed for M-SEARCH message + +boolean udpConnected = false; + +char packetBuffer[UDP_BUFFER_SIZE]; // buffer to hold incoming UDP packet +IPAddress ipMulticast(239, 255, 255, 250); // Simple Service Discovery Protocol (SSDP) +uint32_t portMulticast = 1900; // Multicast address and port + +#ifdef USE_WEMO_EMULATION +/*********************************************************************************************\ + * WeMo UPNP support routines +\*********************************************************************************************/ +const char WEMO_MSEARCH[] PROGMEM = + "HTTP/1.1 200 OK\r\n" + "CACHE-CONTROL: max-age=86400\r\n" + "DATE: Fri, 15 Apr 2016 04:56:29 GMT\r\n" + "EXT:\r\n" + "LOCATION: http://{r1}:80/setup.xml\r\n" + "OPT: \"http://schemas.upnp.org/upnp/1/0/\"; ns=01\r\n" + "01-NLS: b9200ebb-736d-4b93-bf03-835149d13983\r\n" + "SERVER: Unspecified, UPnP/1.0, Unspecified\r\n" + "ST: urn:Belkin:device:**\r\n" + "USN: uuid:{r2}::urn:Belkin:device:**\r\n" + "X-User-Agent: redsonic\r\n" + "\r\n"; + +String wemo_serial() +{ + char serial[15]; + snprintf_P(serial, sizeof(serial), PSTR("201612K%07d"), ESP.getChipId()); + return String(serial); +} + +String wemo_UUID() +{ + char uuid[26]; + snprintf_P(uuid, sizeof(uuid), PSTR("Socket-1_0-%s"), wemo_serial().c_str()); + return String(uuid); +} + +void wemo_respondToMSearch() +{ + char message[TOPSZ], log[LOGSZ]; + + if (portUDP.beginPacket(portUDP.remoteIP(), portUDP.remotePort())) { + String response = FPSTR(WEMO_MSEARCH); + response.replace("{r1}", WiFi.localIP().toString()); + response.replace("{r2}", wemo_UUID()); + portUDP.write(response.c_str()); + portUDP.endPacket(); + snprintf_P(message, sizeof(message), PSTR("Response sent")); + } else { + snprintf_P(message, sizeof(message), PSTR("Failed to send response")); + } + snprintf_P(log, sizeof(log), PSTR("UPnP: Wemo %s to %s:%d"), + message, portUDP.remoteIP().toString().c_str(), portUDP.remotePort()); + addLog(LOG_LEVEL_DEBUG, log); +} +#endif // USE_WEMO_EMULATION + +#ifdef USE_HUE_EMULATION +/*********************************************************************************************\ + * Hue Bridge UPNP support routines + * Need to send 3 response packets with varying ST and USN +\*********************************************************************************************/ +const char HUE_RESPONSE[] PROGMEM = + "HTTP/1.0 200 OK\r\n" + "HOST: 239.255.255.250:1900\r\n" + "CACHE-CONTROL: max-age=100\r\n" + "EXT:\r\n" + "LOCATION: http://{r1}:80/description.xml\r\n" + "SERVER: FreeRTOS/7.4.2 UPnP/1.0 IpBridge/1.15.0\r\n" + "hue-bridgeid: {r2}\r\n"; +const char HUE_ST1[] PROGMEM = + "ST: upnp:rootdevice\r\n"; +const char HUE_USN1[] PROGMEM = + "USN: uuid:{r3}::upnp:rootdevice\r\n" + "\r\n"; + +const char HUE_ST2[] PROGMEM = + "ST: uuid:{r3}\r\n"; +const char HUE_USN2[] PROGMEM = + "USN: uuid:{r3}\r\n" + "\r\n"; + +const char HUE_ST3[] PROGMEM = + "ST: urn:schemas-upnp-org:device:basic:1\r\n"; + +String hue_bridgeid() +{ + char bridgeid[16]; + snprintf_P(bridgeid, sizeof(bridgeid), PSTR("5CCF7FFFFE%03X"), ESP.getChipId()); + return String(bridgeid); +} + +String hue_UUID() +{ + char serial[36]; + snprintf_P(serial, sizeof(serial), PSTR("f6543a06-da50-11ba-8d8f-5ccf7f%03x"), ESP.getChipId()); + return String(serial); +} + +void hue_respondToMSearch() +{ + char message[TOPSZ], log[LOGSZ]; + + if (portUDP.beginPacket(portUDP.remoteIP(), portUDP.remotePort())) { + String response = FPSTR(HUE_RESPONSE); + String response_st=FPSTR(HUE_ST1); + String response_usn=FPSTR(HUE_USN1); + response += response_st + response_usn; + response.replace("{r1}", WiFi.localIP().toString()); + response.replace("{r2}", hue_bridgeid()); + response.replace("{r3}", hue_UUID()); + portUDP.write(response.c_str()); + portUDP.endPacket(); + snprintf_P(message, sizeof(message), PSTR("Response1 sent")); +// addLog(LOG_LEVEL_DEBUG_MORE, response.c_str()); + + response = FPSTR(HUE_RESPONSE); + response_st=FPSTR(HUE_ST2); + response_usn=FPSTR(HUE_USN2); + response += response_st + response_usn; + response.replace("{r1}", WiFi.localIP().toString()); + response.replace("{r2}", hue_bridgeid()); + response.replace("{r3}", hue_UUID()); + portUDP.write(response.c_str()); + portUDP.endPacket(); + snprintf_P(message, sizeof(message), PSTR("Response2 sent")); +// addLog(LOG_LEVEL_DEBUG_MORE, response.c_str()); + + response = FPSTR(HUE_RESPONSE); + response_st=FPSTR(HUE_ST3); + response += response_st + response_usn; + response.replace("{r1}", WiFi.localIP().toString()); + response.replace("{r2}", hue_bridgeid()); + response.replace("{r3}", hue_UUID()); + portUDP.write(response.c_str()); + portUDP.endPacket(); + snprintf_P(message, sizeof(message), PSTR("Response3 sent")); +// addLog(LOG_LEVEL_DEBUG_MORE, response.c_str()); + + } else { + snprintf_P(message, sizeof(message), PSTR("Failed to send response")); + } + snprintf_P(log, sizeof(log), PSTR("UPnP: HUE %s to %s:%d"), + message, portUDP.remoteIP().toString().c_str(), portUDP.remotePort()); + addLog(LOG_LEVEL_DEBUG, log); +} +#endif // USE_HUE_EMULATION + +/********************************************************************************************/ + +#if defined(USE_WEMO_EMULATION) || defined(USE_HUE_EMULATION) +boolean UDP_Disconnect() +{ + if (udpConnected) { + WiFiUDP::stopAll(); + addLog_P(LOG_LEVEL_DEBUG, PSTR("UPnP: Multicast disabled")); + udpConnected = false; + } + return udpConnected; +} + +boolean UDP_Connect() +{ + if (!udpConnected) { + if (portUDP.beginMulticast(WiFi.localIP(), ipMulticast, portMulticast)) { + addLog_P(LOG_LEVEL_INFO, PSTR("UPnP: Multicast (re)joined")); + udpConnected = true; + } else { + addLog_P(LOG_LEVEL_INFO, PSTR("UPnP: Multicast join failed")); + udpConnected = false; + } + } + return udpConnected; +} + +void pollUDP() +{ + if (udpConnected) { + if (portUDP.parsePacket()) { + int len = portUDP.read(packetBuffer, UDP_BUFFER_SIZE -1); + if (len > 0) packetBuffer[len] = 0; + String request = packetBuffer; +// addLog_P(LOG_LEVEL_DEBUG_MORE, packetBuffer); + if (request.indexOf("M-SEARCH") >= 0) { +#ifdef USE_WEMO_EMULATION + if (request.indexOf("urn:Belkin:device:**") > 0) { + wemo_respondToMSearch(); + } +#endif // USE_WEMO_EMULATION +#ifdef USE_HUE_EMULATION + if (request.indexOf("ST: urn:schemas-upnp-org:device:basic:1") > 0 || + request.indexOf("ST: upnp:rootdevice") > 0 || + request.indexOf("ST: ssdp:all") > 0) { + hue_respondToMSearch(); + } +#endif // USE_HUE_EMULATION + } + } + } +} +#endif // USE_WEMO_EMULATION || USE_HUE_EMULATION +#endif // USE_WEMO_EMULATION || USE_HUE_EMULATION + diff --git a/sonoff/xdrv_ws2812.ino b/sonoff/xdrv_ws2812.ino new file mode 100644 index 000000000..cddd8c66e --- /dev/null +++ b/sonoff/xdrv_ws2812.ino @@ -0,0 +1,465 @@ +/* +Copyright (c) 2017 Theo Arends. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +- Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. +- Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. +*/ + +#ifdef USE_WS2812 +/*********************************************************************************************\ + * WS2812 Leds using NeopixelBus library +\*********************************************************************************************/ + +#include + +#ifdef USE_WS2812_DMA + NeoPixelBus *strip = NULL; +#else + NeoPixelBus *strip = NULL; +#endif // USE_WS2812_DMA + +#define COLOR_SATURATION 254.0f + +struct wsColor { + uint8_t red, green, blue; +}; + +struct ColorScheme { + wsColor* colors; + uint8_t count; +}; + +wsColor incandescent[2] = { 255, 140, 20, 0, 0, 0 }; +wsColor rgb[3] = { 255, 0, 0, 0, 255, 0, 0, 0, 255 }; +wsColor christmas[2] = { 255, 0, 0, 0, 255, 0 }; +wsColor hanukkah[2] = { 0, 0, 255, 255, 255, 255 }; +wsColor kwanzaa[3] = { 255, 0, 0, 0, 0, 0, 0, 255, 0 }; +wsColor rainbow[7] = { 255, 0, 0, 255, 128, 0, 255, 255, 0, 0, 255, 0, 0, 0, 255, 128, 0, 255, 255, 0, 255 }; +wsColor fire[3] = { 255, 0, 0, 255, 102, 0, 255, 192, 0 }; +ColorScheme schemes[7] = { + incandescent, 2, + rgb, 3, + christmas, 2, + hanukkah, 2, + kwanzaa, 3, + rainbow, 7, + fire, 3 }; + +uint8_t widthValues[5] = { + 1, // Small + 2, // Medium + 4, // Large + 8, // Largest + 255 }; // All +uint8_t repeatValues[5] = { + 8, // Small + 6, // Medium + 4, // Large + 2, // Largest + 1 }; // All +uint8_t speedValues[6] = { + 0, // None + 18, // Slowest + 14, // Slower + 10, // Slow + 6, // Fast + 2 }; // Fastest + +uint8_t ledTable[] = { + 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 2, 2, 2, 2, 2, 2, 2, 2, 3, 3, 3, 3, 3, 4, 4, + 4, 4, 4, 5, 5, 5, 5, 6, 6, 6, 6, 7, 7, 7, 7, 8, + 8, 8, 9, 9, 9, 10, 10, 10, 11, 11, 12, 12, 12, 13, 13, 14, + 14, 15, 15, 15, 16, 16, 17, 17, 18, 18, 19, 19, 20, 20, 21, 22, + 22, 23, 23, 24, 25, 25, 26, 26, 27, 28, 28, 29, 30, 30, 31, 32, + 33, 33, 34, 35, 36, 36, 37, 38, 39, 40, 40, 41, 42, 43, 44, 45, + 46, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, + 61, 62, 63, 64, 65, 67, 68, 69, 70, 71, 72, 73, 75, 76, 77, 78, + 80, 81, 82, 83, 85, 86, 87, 89, 90, 91, 93, 94, 95, 97, 98, 99, + 101,102,104,105,107,108,110,111,113,114,116,117,119,121,122,124, + 125,127,129,130,132,134,135,137,139,141,142,144,146,148,150,151, + 153,155,157,159,161,163,165,166,168,170,172,174,176,178,180,182, + 184,186,189,191,193,195,197,199,201,204,206,208,210,212,215,217, + 219,221,224,226,228,231,233,235,238,240,243,245,248,250,253,255 }; + +uint8_t lany = 0; +RgbColor dcolor, tcolor, lcolor; + +uint8_t wakeupDimmer = 0; +uint16_t wakeupCntr = 0; +unsigned long stripTimerCntr = 0; // Bars and Gradient + +void ws2812_setDim(uint8_t myDimmer) +{ + float newDim = 100 / (float)myDimmer; + float fmyRed = (float)sysCfg.ws_red / newDim; + float fmyGrn = (float)sysCfg.ws_green / newDim; + float fmyBlu = (float)sysCfg.ws_blue / newDim; + dcolor.R = (uint8_t)fmyRed; + dcolor.G = (uint8_t)fmyGrn; + dcolor.B = (uint8_t)fmyBlu; +} + +void ws2812_setColor(uint16_t led, char* colstr) +{ + HtmlColor hcolor; + char log[LOGSZ], lcolstr[8]; + + snprintf_P(lcolstr, sizeof(lcolstr), PSTR("#%s"), colstr); + uint8_t result = hcolor.Parse((char *)lcolstr, 7); + if (result) { + if (led) { + strip->SetPixelColor(led -1, RgbColor(hcolor)); // Led 1 is strip Led 0 -> substract offset 1 + strip->Show(); + } else { + dcolor = RgbColor(hcolor); + +// snprintf_P(log, sizeof(log), PSTR("DBG: Red %02X, Green %02X, Blue %02X"), dcolor.R, dcolor.G, dcolor.B); +// addLog(LOG_LEVEL_DEBUG, log); + + uint16_t temp = dcolor.R; + if (temp < dcolor.G) temp = dcolor.G; + if (temp < dcolor.B) temp = dcolor.B; + float mDim = (float)temp / 2.55; + sysCfg.ws_dimmer = (uint8_t)mDim; + + float newDim = 100 / mDim; + float fmyRed = (float)dcolor.R * newDim; + float fmyGrn = (float)dcolor.G * newDim; + float fmyBlu = (float)dcolor.B * newDim; + sysCfg.ws_red = (uint8_t)fmyRed; + sysCfg.ws_green = (uint8_t)fmyGrn; + sysCfg.ws_blue = (uint8_t)fmyBlu; + + lany = 1; + } + } +} + +void ws2812_replaceHSB(String *response) +{ + ws2812_setDim(sysCfg.ws_dimmer); + HsbColor hsb=HsbColor(dcolor); + response->replace("{h}", String((uint16_t)(65535.0f * hsb.H))); + response->replace("{s}", String((uint8_t)(COLOR_SATURATION * hsb.S))); + response->replace("{b}", String((uint8_t)(COLOR_SATURATION * hsb.B))); +} + +void ws2812_changeBrightness(uint8_t bri) +{ + char rgb[7]; + + //sysCfg.ws_ledtable=1; // Switch on Gamma Correction for "natural" brightness controll + ws2812_setDim(sysCfg.ws_dimmer); + HsbColor hsb = HsbColor(dcolor); + if (!bri) bri=1; + if (bri==255) bri=252; + hsb.B=(float)(bri/COLOR_SATURATION); + RgbColor tmp = RgbColor(hsb); + sprintf(rgb,"%02X%02X%02X", tmp.R, tmp.G, tmp.B); + ws2812_setColor(0,rgb); +} + +void ws2812_getColor(uint16_t led, char* svalue, uint16_t ssvalue) +{ + RgbColor mcolor; + char stemp[20]; + + if (led) { + mcolor = strip->GetPixelColor(led -1); + snprintf_P(stemp, sizeof(stemp), PSTR("Led%d"), led); + } else { + ws2812_setDim(sysCfg.ws_dimmer); + mcolor = dcolor; + snprintf_P(stemp, sizeof(stemp), PSTR("Color")); + } + uint32_t color = (uint32_t)mcolor.R << 16; + color += (uint32_t)mcolor.G << 8; + color += (uint32_t)mcolor.B; + snprintf_P(svalue, ssvalue, PSTR("{\"%s\":\"%06X\"}"), stemp, color); +} + +void ws2812_stripShow() +{ + RgbColor c; + + if (sysCfg.ws_ledtable) { + for (uint16_t i = 0; i < sysCfg.ws_pixels; i++) { + c = strip->GetPixelColor(i); + strip->SetPixelColor(i, RgbColor(ledTable[c.R], ledTable[c.G], ledTable[c.B])); + } + } + strip->Show(); +} + +void ws2812_resetWakupState() +{ + wakeupDimmer = 0; + wakeupCntr = 0; +} + +void ws2812_resetStripTimer() +{ + stripTimerCntr = 0; +} + +int mod(int a, int b) +{ + int ret = a % b; + if (ret < 0) ret += b; + return ret; +} + +void ws2812_clock() +{ + RgbColor c; + + strip->ClearTo(0); // Reset strip + float newDim = 100 / (float)sysCfg.ws_dimmer; + float f1 = 255 / newDim; + uint8_t i1 = (uint8_t)f1; + float f2 = 127 / newDim; + uint8_t i2 = (uint8_t)f2; + float f3 = 63 / newDim; + uint8_t i3 = (uint8_t)f3; + + int j = sysCfg.ws_pixels; + int clksize = 600 / j; + int i = (rtcTime.Second * 10) / clksize; + + c = strip->GetPixelColor(mod(i, j)); c.B = i1; strip->SetPixelColor(mod(i, j), c); + i = (rtcTime.Minute * 10) / clksize; + c = strip->GetPixelColor(mod(i -1, j)); c.G = i3; strip->SetPixelColor(mod(i -1, j), c); + c = strip->GetPixelColor(mod(i, j)); c.G = i1; strip->SetPixelColor(mod(i, j), c); + c = strip->GetPixelColor(mod(i +1, j)); c.G = i3; strip->SetPixelColor(mod(i +1, j), c); + i = (rtcTime.Hour % 12) * (50 / clksize); + c = strip->GetPixelColor(mod(i -2, j)); c.R = i3; strip->SetPixelColor(mod(i -2, j), c); + c = strip->GetPixelColor(mod(i -1, j)); c.R = i2; strip->SetPixelColor(mod(i -1, j), c); + c = strip->GetPixelColor(mod(i, j)); c.R = i1; strip->SetPixelColor(mod(i, j), c); + c = strip->GetPixelColor(mod(i +1, j)); c.R = i2; strip->SetPixelColor(mod(i +1, j), c); + c = strip->GetPixelColor(mod(i +2, j)); c.R = i3; strip->SetPixelColor(mod(i +2, j), c); + ws2812_stripShow(); +} + +void ws2812_gradientColor(struct wsColor* mColor, uint8_t range, uint8_t gradRange, uint8_t i) +{ +/* + * Compute the color of a pixel at position i using a gradient of the color scheme. + * This function is used internally by the gradient function. + */ + ColorScheme scheme = schemes[sysCfg.ws_scheme -3]; + uint8_t curRange = i / range; + uint8_t rangeIndex = i % range; + uint8_t colorIndex = rangeIndex / gradRange; + uint8_t start = colorIndex; + uint8_t end = colorIndex +1; + if (curRange % 2 != 0) { + start = (scheme.count -1) - start; + end = (scheme.count -1) - end; + } + float newDim = 100 / (float)sysCfg.ws_dimmer; + float fmyRed = (float)map(rangeIndex % gradRange, 0, gradRange, scheme.colors[start].red, scheme.colors[end].red) / newDim; + float fmyGrn = (float)map(rangeIndex % gradRange, 0, gradRange, scheme.colors[start].green, scheme.colors[end].green) / newDim; + float fmyBlu = (float)map(rangeIndex % gradRange, 0, gradRange, scheme.colors[start].blue, scheme.colors[end].blue) / newDim; + mColor->red = (uint8_t)fmyRed; + mColor->green = (uint8_t)fmyGrn; + mColor->blue = (uint8_t)fmyBlu; +} + +void ws2812_gradient() +{ +/* + * This routine courtesy Tony DiCola (Adafruit) + * Display a gradient of colors for the current color scheme. + * Repeat is the number of repetitions of the gradient (pick a multiple of 2 for smooth looping of the gradient). + */ + RgbColor c; + + ColorScheme scheme = schemes[sysCfg.ws_scheme -3]; + if (scheme.count < 2) return; + + uint8_t repeat = repeatValues[sysCfg.ws_width]; // number of scheme.count per ledcount + uint8_t range = (uint8_t)ceil((float)sysCfg.ws_pixels / (float)repeat); + uint8_t gradRange = (uint8_t)ceil((float)range / (float)(scheme.count - 1)); + uint8_t offset = speedValues[sysCfg.ws_speed] > 0 ? stripTimerCntr / speedValues[sysCfg.ws_speed] : 0; + + wsColor oldColor, currentColor; + ws2812_gradientColor(&oldColor, range, gradRange, offset); + currentColor = oldColor; + for (uint16_t i = 0; i < sysCfg.ws_pixels; i++) { + if (repeatValues[sysCfg.ws_width] > 1) ws2812_gradientColor(¤tColor, range, gradRange, i +offset); + if (sysCfg.ws_speed > 0) { + // Blend old and current color based on time for smooth movement. + c.R = map(stripTimerCntr % speedValues[sysCfg.ws_speed], 0, speedValues[sysCfg.ws_speed], oldColor.red, currentColor.red); + c.G = map(stripTimerCntr % speedValues[sysCfg.ws_speed], 0, speedValues[sysCfg.ws_speed], oldColor.green, currentColor.green); + c.B = map(stripTimerCntr % speedValues[sysCfg.ws_speed], 0, speedValues[sysCfg.ws_speed], oldColor.blue, currentColor.blue); + } + else { + // No animation, just use the current color. + c.R = currentColor.red; + c.G = currentColor.green; + c.B = currentColor.blue; + } + strip->SetPixelColor(i, c); + oldColor = currentColor; + } + ws2812_stripShow(); +} + +void ws2812_bars() +{ +/* + * This routine courtesy Tony DiCola (Adafruit) + * Display solid bars of color for the current color scheme. + * Width is the width of each bar in pixels/lights. + */ + RgbColor c; + uint16_t i; + + ColorScheme scheme = schemes[sysCfg.ws_scheme -3]; + + uint8_t maxSize = sysCfg.ws_pixels / scheme.count; + if (widthValues[sysCfg.ws_width] > maxSize) maxSize = 0; + + uint8_t offset = speedValues[sysCfg.ws_speed] > 0 ? stripTimerCntr / speedValues[sysCfg.ws_speed] : 0; + + wsColor mcolor[scheme.count]; + memcpy(mcolor, scheme.colors, sizeof(mcolor)); + float newDim = 100 / (float)sysCfg.ws_dimmer; + for (i = 0; i < scheme.count; i++) { + float fmyRed = (float)mcolor[i].red / newDim; + float fmyGrn = (float)mcolor[i].green / newDim; + float fmyBlu = (float)mcolor[i].blue / newDim; + mcolor[i].red = (uint8_t)fmyRed; + mcolor[i].green = (uint8_t)fmyGrn; + mcolor[i].blue = (uint8_t)fmyBlu; + } + uint8_t colorIndex = offset % scheme.count; + for (i = 0; i < sysCfg.ws_pixels; i++) { + if (maxSize) + colorIndex = ((i + offset) % (scheme.count * widthValues[sysCfg.ws_width])) / widthValues[sysCfg.ws_width]; + c.R = mcolor[colorIndex].red; + c.G = mcolor[colorIndex].green; + c.B = mcolor[colorIndex].blue; + strip->SetPixelColor(i, c); + } + ws2812_stripShow(); +} + +void ws2812_animate() +{ + char log[LOGSZ]; + uint8_t fadeValue; + + stripTimerCntr++; + if (power == 0) { // Power Off + stripTimerCntr = 0; + tcolor = 0; + } + else { + switch (sysCfg.ws_scheme) { + case 0: // Power On + ws2812_setDim(sysCfg.ws_dimmer); + if (sysCfg.ws_fade == 0) { + tcolor = dcolor; + } else { + if (tcolor != dcolor) { + if (tcolor.R < dcolor.R) tcolor.R += ((dcolor.R - tcolor.R) >> sysCfg.ws_speed) +1; + if (tcolor.G < dcolor.G) tcolor.G += ((dcolor.G - tcolor.G) >> sysCfg.ws_speed) +1; + if (tcolor.B < dcolor.B) tcolor.B += ((dcolor.B - tcolor.B) >> sysCfg.ws_speed) +1; + if (tcolor.R > dcolor.R) tcolor.R -= ((tcolor.R - dcolor.R) >> sysCfg.ws_speed) +1; + if (tcolor.G > dcolor.G) tcolor.G -= ((tcolor.G - dcolor.G) >> sysCfg.ws_speed) +1; + if (tcolor.B > dcolor.B) tcolor.B -= ((tcolor.B - dcolor.B) >> sysCfg.ws_speed) +1; + } + } + break; + case 1: // Wake up light + wakeupCntr++; + if (wakeupDimmer == 0) { + tcolor = 0; + wakeupDimmer++; + } + else { + if (wakeupCntr > ((sysCfg.ws_wakeup * STATES) / sysCfg.ws_dimmer)) { + wakeupCntr = 0; + wakeupDimmer++; + if (wakeupDimmer <= sysCfg.ws_dimmer) { + ws2812_setDim(wakeupDimmer); + tcolor = dcolor; + } else + sysCfg.ws_scheme = 0; + } + } + break; + case 2: // Clock + if ((state == (STATES/10)*2) || (lany != 2)) ws2812_clock(); + lany = 2; + break; + default: + if (sysCfg.ws_fade == 1) ws2812_gradient(); else ws2812_bars(); + lany = 1; + break; + } + } + + if ((sysCfg.ws_scheme <= 1) || (!(power &1))) { + if ((lcolor != tcolor) || lany) { + lany = 0; + lcolor = tcolor; + +// snprintf_P(log, sizeof(log), PSTR("DBG: StripPixels %d, CfgPixels %d, Red %02X, Green %02X, Blue %02X"), strip->PixelCount(), sysCfg.ws_pixels, lcolor.R, lcolor.G, lcolor.B); +// addLog(LOG_LEVEL_DEBUG, log); + + if (sysCfg.ws_ledtable) { + for (uint16_t i = 0; i < sysCfg.ws_pixels; i++) strip->SetPixelColor(i, RgbColor(ledTable[lcolor.R],ledTable[lcolor.G],ledTable[lcolor.B])); + } else { + for (uint16_t i = 0; i < sysCfg.ws_pixels; i++) strip->SetPixelColor(i, lcolor); + } + strip->Show(); + } + } +} + +void ws2812_update() +{ + lany = 1; +} + +void ws2812_pixels() +{ + strip->ClearTo(0); + strip->Show(); + tcolor = 0; + lany = 1; +} + +void ws2812_init() +{ +#ifdef USE_WS2812_DMA + strip = new NeoPixelBus(WS2812_MAX_LEDS); // For Esp8266, the Pin is omitted and it uses GPIO3 due to DMA hardware use. +#else + strip = new NeoPixelBus(WS2812_MAX_LEDS, pin[GPIO_WS2812]); +#endif // USE_WS2812_DMA + strip->Begin(); + ws2812_pixels(); +} +#endif // USE_WS2812 diff --git a/sonoff/xsns_bh1750.ino b/sonoff/xsns_bh1750.ino new file mode 100644 index 000000000..405e4a1f1 --- /dev/null +++ b/sonoff/xsns_bh1750.ino @@ -0,0 +1,110 @@ +/* +Copyright (c) 2017 Theo Arends. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +- Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. +- Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. +*/ + +#ifdef USE_I2C +#ifdef USE_BH1750 +/*********************************************************************************************\ + * BH1750 - Ambient Light Intensity +\*********************************************************************************************/ + +#define BH1750_ADDR1 0x23 +#define BH1750_ADDR2 0x5C + +#define BH1750_CONTINUOUS_HIGH_RES_MODE 0x10 // Start measurement at 1lx resolution. Measurement time is approx 120ms. + +uint8_t bh1750addr, bh1750type = 0; +char bh1750stype[7]; + +uint16_t bh1750_readLux(void) +{ + Wire.requestFrom(bh1750addr, (uint8_t)2); + byte msb = Wire.read(); + byte lsb = Wire.read(); + uint16_t value = ((msb << 8) | lsb) / 1.2; + return value; +} + +boolean bh1750_detect() +{ + if (bh1750type) return true; + + char log[LOGSZ]; + uint8_t status; + boolean success = false; + + bh1750addr = BH1750_ADDR1; + Wire.beginTransmission(bh1750addr); + Wire.write(BH1750_CONTINUOUS_HIGH_RES_MODE); + status = Wire.endTransmission(); + if (status) { + bh1750addr = BH1750_ADDR2; + Wire.beginTransmission(bh1750addr); + Wire.write(BH1750_CONTINUOUS_HIGH_RES_MODE); + status = Wire.endTransmission(); + } + if (!status) { + success = true; + bh1750type = 1; + snprintf_P(bh1750stype, sizeof(bh1750stype), PSTR("BH1750")); + } + if (success) { + snprintf_P(log, sizeof(log), PSTR("I2C: %s found at address 0x%x"), bh1750stype, bh1750addr); + addLog(LOG_LEVEL_DEBUG, log); + } else { + bh1750type = 0; + } + return success; +} + +/*********************************************************************************************\ + * Presentation +\*********************************************************************************************/ + +void bh1750_mqttPresent(char* svalue, uint16_t ssvalue, uint8_t* djson, uint8_t domidx) +{ + if (!bh1750type) return; + + uint16_t l = bh1750_readLux(); + snprintf_P(svalue, ssvalue, PSTR("%s, \"%s\":{\"Illuminance\":%d}"), svalue, bh1750stype, l); + *djson = 1; +#ifdef USE_DOMOTICZ + domoticz_sensor5(l); +#endif // USE_DOMOTICZ +} + +#ifdef USE_WEBSERVER +String bh1750_webPresent() +{ + String page = ""; + if (bh1750type) { + uint16_t l = bh1750_readLux(); + page += F("Illuminance: "); page += String(l); page += F(" lx"); + } + return page; +} +#endif // USE_WEBSERVER +#endif // USE_BH1750 +#endif // USE_I2C + diff --git a/sonoff/xsns_bmp.ino b/sonoff/xsns_bmp.ino new file mode 100644 index 000000000..91f00eb55 --- /dev/null +++ b/sonoff/xsns_bmp.ino @@ -0,0 +1,477 @@ +/* + Copyright (c) 2017 Heiko Krupp and Theo Arends. All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + - Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + - Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + POSSIBILITY OF SUCH DAMAGE. +*/ + +#ifdef USE_I2C +#ifdef USE_BMP +/*********************************************************************************************\ + * BMP085, BMP180, BMP280, BME280 - Pressure and Temperature and Humidy (BME280 only) + * + * Source: Heiko Krupp and Adafruit Industries +\*********************************************************************************************/ + +#define BMP_ADDR 0x77 + +#define BMP180_CHIPID 0x55 +#define BMP280_CHIPID 0x58 +#define BME280_CHIPID 0x60 + +#define BMP_REGISTER_CHIPID 0xD0 + +uint8_t bmpaddr, bmptype = 0; +char bmpstype[7]; + +/*********************************************************************************************\ + * BMP085 and BME180 + * + * Programmer : Heiko Krupp with changes from Theo Arends +\*********************************************************************************************/ + +#define BMP180_REG_CONTROL 0xF4 +#define BMP180_REG_RESULT 0xF6 +#define BMP180_TEMPERATURE 0x2E +#define BMP180_PRESSURE3 0xF4 // Max. oversampling -> OSS = 3 + +#define BMP180_AC1 0xAA +#define BMP180_AC2 0xAC +#define BMP180_AC3 0xAE +#define BMP180_AC4 0xB0 +#define BMP180_AC5 0xB2 +#define BMP180_AC6 0xB4 +#define BMP180_VB1 0xB6 +#define BMP180_VB2 0xB8 +#define BMP180_MB 0xBA +#define BMP180_MC 0xBC +#define BMP180_MD 0xBE + +#define BMP180_OSS 3 + +int16_t cal_ac1,cal_ac2,cal_ac3,cal_b1,cal_b2,cal_mc,cal_md; +uint16_t cal_ac4,cal_ac5,cal_ac6; +int32_t bmp180_b5 = 0; + +boolean bmp180_calibration() +{ + cal_ac1 = i2c_read16(bmpaddr, BMP180_AC1); + cal_ac2 = i2c_read16(bmpaddr, BMP180_AC2); + cal_ac3 = i2c_read16(bmpaddr, BMP180_AC3); + cal_ac4 = i2c_read16(bmpaddr, BMP180_AC4); + cal_ac5 = i2c_read16(bmpaddr, BMP180_AC5); + cal_ac6 = i2c_read16(bmpaddr, BMP180_AC6); + cal_b1 = i2c_read16(bmpaddr, BMP180_VB1); + cal_b2 = i2c_read16(bmpaddr, BMP180_VB2); + cal_mc = i2c_read16(bmpaddr, BMP180_MC); + cal_md = i2c_read16(bmpaddr, BMP180_MD); + + // Check for Errors in calibration data. Value never is 0x0000 or 0xFFFF + if(!cal_ac1 | !cal_ac2 | !cal_ac3 | !cal_ac4 | !cal_ac5 | + !cal_ac6 | !cal_b1 | !cal_b2 | !cal_mc | !cal_md) + return false; + + if((cal_ac1==0xFFFF)| + (cal_ac2==0xFFFF)| + (cal_ac3==0xFFFF)| + (cal_ac4==0xFFFF)| + (cal_ac5==0xFFFF)| + (cal_ac6==0xFFFF)| + (cal_b1==0xFFFF)| + (cal_b2==0xFFFF)| + (cal_mc==0xFFFF)| + (cal_md==0xFFFF)) + return false; + + return true; +} + +double bmp180_readTemperature() +{ + i2c_write8(bmpaddr, BMP180_REG_CONTROL, BMP180_TEMPERATURE); + delay(5); // 5ms conversion time + int ut = i2c_read16(bmpaddr, BMP180_REG_RESULT); + int32_t x1 = (ut - (int32_t)cal_ac6) * ((int32_t)cal_ac5) >> 15; + int32_t x2 = ((int32_t)cal_mc << 11) / (x1+(int32_t)cal_md); + bmp180_b5=x1+x2; + + return ((bmp180_b5+8)>>4)/10.0; +} + +double bmp180_readPressure() +{ + int32_t p; + uint8_t msb,lsb,xlsb; + + i2c_write8(bmpaddr, BMP180_REG_CONTROL, BMP180_PRESSURE3); // Highest resolution + delay(2 + (4 << BMP180_OSS)); // 26ms conversion time at ultra high resolution + uint32_t up = i2c_read24(bmpaddr, BMP180_REG_RESULT); + up >>= (8 - BMP180_OSS); + + int32_t b6 = bmp180_b5 - 4000; + int32_t x1 = ((int32_t)cal_b2 * ( (b6 * b6)>>12 )) >> 11; + int32_t x2 = ((int32_t)cal_ac2 * b6) >> 11; + int32_t x3 = x1 + x2; + int32_t b3 = ((((int32_t)cal_ac1*4 + x3) << BMP180_OSS) + 2)>>2; + + x1 = ((int32_t)cal_ac3 * b6) >> 13; + x2 = ((int32_t)cal_b1 * ((b6 * b6) >> 12)) >> 16; + x3 = ((x1 + x2) + 2) >> 2; + uint32_t b4 = ((uint32_t)cal_ac4 * (uint32_t)(x3 + 32768)) >> 15; + uint32_t b7 = ((uint32_t)up - b3) * (uint32_t)( 50000UL >> BMP180_OSS); + + if (b7 < 0x80000000) { + p = (b7 * 2) / b4; + } else { + p = (b7 / b4) * 2; + } + + x1 = (p >> 8) * (p >> 8); + x1 = (x1 * 3038) >> 16; + x2 = (-7357 * p) >> 16; + + p += ((x1 + x2 + (int32_t)3791)>>4); + return p/100.0; // convert to mbar +} + +double bmp180_calcSealevelPressure(float pAbs, float altitude_meters) +{ + double pressure = pAbs*100.0; + return (double)(pressure / pow(1.0-altitude_meters/44330, 5.255))/100.0; +} + +/*********************************************************************************************\ + * BMP280 and BME280 + * + * Programmer : BMP280/BME280 Datasheet and Adafruit with changes by Theo Arends +\*********************************************************************************************/ + +#define BME280_REGISTER_CONTROLHUMID 0xF2 +#define BME280_REGISTER_CONTROL 0xF4 +#define BME280_REGISTER_PRESSUREDATA 0xF7 +#define BME280_REGISTER_TEMPDATA 0xFA +#define BME280_REGISTER_HUMIDDATA 0xFD + +#define BME280_REGISTER_DIG_T1 0x88 +#define BME280_REGISTER_DIG_T2 0x8A +#define BME280_REGISTER_DIG_T3 0x8C +#define BME280_REGISTER_DIG_P1 0x8E +#define BME280_REGISTER_DIG_P2 0x90 +#define BME280_REGISTER_DIG_P3 0x92 +#define BME280_REGISTER_DIG_P4 0x94 +#define BME280_REGISTER_DIG_P5 0x96 +#define BME280_REGISTER_DIG_P6 0x98 +#define BME280_REGISTER_DIG_P7 0x9A +#define BME280_REGISTER_DIG_P8 0x9C +#define BME280_REGISTER_DIG_P9 0x9E +#define BME280_REGISTER_DIG_H1 0xA1 +#define BME280_REGISTER_DIG_H2 0xE1 +#define BME280_REGISTER_DIG_H3 0xE3 +#define BME280_REGISTER_DIG_H4 0xE4 +#define BME280_REGISTER_DIG_H5 0xE5 +#define BME280_REGISTER_DIG_H6 0xE7 + +struct bme280_calib_data +{ + uint16_t dig_T1; + int16_t dig_T2; + int16_t dig_T3; + uint16_t dig_P1; + int16_t dig_P2; + int16_t dig_P3; + int16_t dig_P4; + int16_t dig_P5; + int16_t dig_P6; + int16_t dig_P7; + int16_t dig_P8; + int16_t dig_P9; + uint8_t dig_H1; + int16_t dig_H2; + uint8_t dig_H3; + int16_t dig_H4; + int16_t dig_H5; + int8_t dig_H6; +} _bme280_calib; + +int32_t t_fine; + +boolean bmp280_calibrate() +{ +// if (i2c_read8(bmpaddr, BMP_REGISTER_CHIPID) != BMP280_CHIPID) return false; + + _bme280_calib.dig_T1 = i2c_read16_LE(bmpaddr, BME280_REGISTER_DIG_T1); + _bme280_calib.dig_T2 = i2c_readS16_LE(bmpaddr, BME280_REGISTER_DIG_T2); + _bme280_calib.dig_T3 = i2c_readS16_LE(bmpaddr, BME280_REGISTER_DIG_T3); + _bme280_calib.dig_P1 = i2c_read16_LE(bmpaddr, BME280_REGISTER_DIG_P1); + _bme280_calib.dig_P2 = i2c_readS16_LE(bmpaddr, BME280_REGISTER_DIG_P2); + _bme280_calib.dig_P3 = i2c_readS16_LE(bmpaddr, BME280_REGISTER_DIG_P3); + _bme280_calib.dig_P4 = i2c_readS16_LE(bmpaddr, BME280_REGISTER_DIG_P4); + _bme280_calib.dig_P5 = i2c_readS16_LE(bmpaddr, BME280_REGISTER_DIG_P5); + _bme280_calib.dig_P6 = i2c_readS16_LE(bmpaddr, BME280_REGISTER_DIG_P6); + _bme280_calib.dig_P7 = i2c_readS16_LE(bmpaddr, BME280_REGISTER_DIG_P7); + _bme280_calib.dig_P8 = i2c_readS16_LE(bmpaddr, BME280_REGISTER_DIG_P8); + _bme280_calib.dig_P9 = i2c_readS16_LE(bmpaddr, BME280_REGISTER_DIG_P9); + +// i2c_write8(bmpaddr, BME280_REGISTER_CONTROL, 0x3F); // Temp 1x oversampling, Press 16x oversampling, normal mode (Adafruit) + i2c_write8(bmpaddr, BME280_REGISTER_CONTROL, 0xB7); // 16x oversampling, normal mode (Adafruit) + + return true; +} + +boolean bme280_calibrate() +{ +// if (i2c_read8(bmpaddr, BMP_REGISTER_CHIPID) != BME280_CHIPID) return false; + + _bme280_calib.dig_T1 = i2c_read16_LE(bmpaddr, BME280_REGISTER_DIG_T1); + _bme280_calib.dig_T2 = i2c_readS16_LE(bmpaddr, BME280_REGISTER_DIG_T2); + _bme280_calib.dig_T3 = i2c_readS16_LE(bmpaddr, BME280_REGISTER_DIG_T3); + _bme280_calib.dig_P1 = i2c_read16_LE(bmpaddr, BME280_REGISTER_DIG_P1); + _bme280_calib.dig_P2 = i2c_readS16_LE(bmpaddr, BME280_REGISTER_DIG_P2); + _bme280_calib.dig_P3 = i2c_readS16_LE(bmpaddr, BME280_REGISTER_DIG_P3); + _bme280_calib.dig_P4 = i2c_readS16_LE(bmpaddr, BME280_REGISTER_DIG_P4); + _bme280_calib.dig_P5 = i2c_readS16_LE(bmpaddr, BME280_REGISTER_DIG_P5); + _bme280_calib.dig_P6 = i2c_readS16_LE(bmpaddr, BME280_REGISTER_DIG_P6); + _bme280_calib.dig_P7 = i2c_readS16_LE(bmpaddr, BME280_REGISTER_DIG_P7); + _bme280_calib.dig_P8 = i2c_readS16_LE(bmpaddr, BME280_REGISTER_DIG_P8); + _bme280_calib.dig_P9 = i2c_readS16_LE(bmpaddr, BME280_REGISTER_DIG_P9); + _bme280_calib.dig_H1 = i2c_read8(bmpaddr, BME280_REGISTER_DIG_H1); + _bme280_calib.dig_H2 = i2c_readS16_LE(bmpaddr, BME280_REGISTER_DIG_H2); + _bme280_calib.dig_H3 = i2c_read8(bmpaddr, BME280_REGISTER_DIG_H3); + _bme280_calib.dig_H4 = (i2c_read8(bmpaddr, BME280_REGISTER_DIG_H4) << 4) | (i2c_read8(bmpaddr, BME280_REGISTER_DIG_H4 + 1) & 0xF); + _bme280_calib.dig_H5 = (i2c_read8(bmpaddr, BME280_REGISTER_DIG_H5 + 1) << 4) | (i2c_read8(bmpaddr, BME280_REGISTER_DIG_H5) >> 4); + _bme280_calib.dig_H6 = (int8_t)i2c_read8(bmpaddr, BME280_REGISTER_DIG_H6); + + // Set before CONTROL_meas (DS 5.4.3) + i2c_write8(bmpaddr, BME280_REGISTER_CONTROLHUMID, 0x05); // 16x oversampling (Adafruit) + i2c_write8(bmpaddr, BME280_REGISTER_CONTROL, 0xB7); // 16x oversampling, normal mode (Adafruit) + + return true; +} + +double bmp280_readTemperature(void) +{ + int32_t var1, var2; + + int32_t adc_T = i2c_read24(bmpaddr, BME280_REGISTER_TEMPDATA); + adc_T >>= 4; + + var1 = ((((adc_T>>3) - ((int32_t)_bme280_calib.dig_T1 <<1))) * ((int32_t)_bme280_calib.dig_T2)) >> 11; + var2 = (((((adc_T>>4) - ((int32_t)_bme280_calib.dig_T1)) * ((adc_T>>4) - ((int32_t)_bme280_calib.dig_T1))) >> 12) * + ((int32_t)_bme280_calib.dig_T3)) >> 14; + t_fine = var1 + var2; + double T = (t_fine * 5 + 128) >> 8; + return T / 100.0; +} + +double bmp280_readPressure(void) +{ + int64_t var1, var2, p; + +// Must be done first to get the t_fine variable set up +// bmp280_readTemperature(); + + int32_t adc_P = i2c_read24(bmpaddr, BME280_REGISTER_PRESSUREDATA); + adc_P >>= 4; + + var1 = ((int64_t)t_fine) - 128000; + var2 = var1 * var1 * (int64_t)_bme280_calib.dig_P6; + var2 = var2 + ((var1 * (int64_t)_bme280_calib.dig_P5) << 17); + var2 = var2 + (((int64_t)_bme280_calib.dig_P4) << 35); + var1 = ((var1 * var1 * (int64_t)_bme280_calib.dig_P3) >> 8) + ((var1 * (int64_t)_bme280_calib.dig_P2) << 12); + var1 = (((((int64_t)1) << 47) + var1)) * ((int64_t)_bme280_calib.dig_P1) >> 33; + if (var1 == 0) { + return 0; // avoid exception caused by division by zero + } + p = 1048576 - adc_P; + p = (((p << 31) - var2) * 3125) / var1; + var1 = (((int64_t)_bme280_calib.dig_P9) * (p >> 13) * (p >> 13)) >> 25; + var2 = (((int64_t)_bme280_calib.dig_P8) * p) >> 19; + p = ((p + var1 + var2) >> 8) + (((int64_t)_bme280_calib.dig_P7) << 4); + return (double)p / 25600.0; +} + +double bme280_readHumidity(void) +{ + int32_t v_x1_u32r; + +// Must be done first to get the t_fine variable set up +// bmp280_readTemperature(); + + int32_t adc_H = i2c_read16(bmpaddr, BME280_REGISTER_HUMIDDATA); + + v_x1_u32r = (t_fine - ((int32_t)76800)); + + v_x1_u32r = (((((adc_H << 14) - (((int32_t)_bme280_calib.dig_H4) << 20) - + (((int32_t)_bme280_calib.dig_H5) * v_x1_u32r)) + ((int32_t)16384)) >> 15) * + (((((((v_x1_u32r * ((int32_t)_bme280_calib.dig_H6)) >> 10) * + (((v_x1_u32r * ((int32_t)_bme280_calib.dig_H3)) >> 11) + ((int32_t)32768))) >> 10) + + ((int32_t)2097152)) * ((int32_t)_bme280_calib.dig_H2) + 8192) >> 14)); + v_x1_u32r = (v_x1_u32r - (((((v_x1_u32r >> 15) * (v_x1_u32r >> 15)) >> 7) * + ((int32_t)_bme280_calib.dig_H1)) >> 4)); + v_x1_u32r = (v_x1_u32r < 0) ? 0 : v_x1_u32r; + v_x1_u32r = (v_x1_u32r > 419430400) ? 419430400 : v_x1_u32r; + double h = (v_x1_u32r >> 12); + return h / 1024.0; +} + +/*********************************************************************************************\ + * BMP +\*********************************************************************************************/ + +double bmp_convertCtoF(double c) +{ + return c * 1.8 + 32; +} + +double bmp_readTemperature(bool S) +{ + double t = NAN; + + switch (bmptype) { + case BMP180_CHIPID: + t = bmp180_readTemperature(); + break; + case BMP280_CHIPID: + case BME280_CHIPID: + t = bmp280_readTemperature(); + } + if (!isnan(t)) { + if(S) t = bmp_convertCtoF(t); + return t; + } + return 0; +} + +double bmp_readPressure(void) +{ + switch (bmptype) { + case BMP180_CHIPID: + return bmp180_readPressure(); + case BMP280_CHIPID: + case BME280_CHIPID: + return bmp280_readPressure(); + } + return 0; +} + +double bmp_readHumidity(void) +{ + switch (bmptype) { + case BMP180_CHIPID: + case BMP280_CHIPID: + break; + case BME280_CHIPID: + return bme280_readHumidity(); + } + return 0; +} + +boolean bmp_detect() +{ + if (bmptype) return true; + + char log[LOGSZ]; + boolean success = false; + + bmpaddr = BMP_ADDR; + bmptype = i2c_read8(bmpaddr, BMP_REGISTER_CHIPID); + if (!bmptype) { + bmpaddr--; + bmptype = i2c_read8(bmpaddr, BMP_REGISTER_CHIPID); + } + snprintf_P(bmpstype, sizeof(bmpstype), PSTR("BMP")); + switch (bmptype) { + case BMP180_CHIPID: + success = bmp180_calibration(); + snprintf_P(bmpstype, sizeof(bmpstype), PSTR("BMP180")); + break; + case BMP280_CHIPID: + success = bmp280_calibrate(); + snprintf_P(bmpstype, sizeof(bmpstype), PSTR("BMP280")); + break; + case BME280_CHIPID: + success = bme280_calibrate(); + snprintf_P(bmpstype, sizeof(bmpstype), PSTR("BME280")); + } + if (success) { + snprintf_P(log, sizeof(log), PSTR("I2C: %s found at address 0x%x"), bmpstype, bmpaddr); + addLog(LOG_LEVEL_DEBUG, log); + } else { + bmptype = 0; + } + return success; +} + +/*********************************************************************************************\ + * Presentation +\*********************************************************************************************/ + +void bmp_mqttPresent(char* svalue, uint16_t ssvalue, uint8_t* djson, uint8_t domidx) +{ + if (!bmptype) return; + + char stemp1[10], stemp2[10], stemp3[10]; + + double t = bmp_readTemperature(TEMP_CONVERSION); + double p = bmp_readPressure(); + double h = bmp_readHumidity(); + dtostrf(t, 1, TEMP_RESOLUTION &3, stemp1); + dtostrf(p, 1, PRESSURE_RESOLUTION &3, stemp2); + dtostrf(h, 1, HUMIDITY_RESOLUTION &3, stemp3); + if (!strcmp(bmpstype,"BME280")) { + snprintf_P(svalue, ssvalue, PSTR("%s, \"%s\":{\"Temperature\":\"%s\", \"Humidity\":\"%s\", \"Pressure\":\"%s\"}"), + svalue, bmpstype, stemp1, stemp3, stemp2); + } else { + snprintf_P(svalue, ssvalue, PSTR("%s, \"%s\":{\"Temperature\":\"%s\", \"Pressure\":\"%s\"}"), + svalue, bmpstype, stemp1, stemp2); + } + *djson = 1; +#ifdef USE_DOMOTICZ + domoticz_sensor3(stemp1, stemp3, stemp2); +#endif // USE_DOMOTICZ +} + +#ifdef USE_WEBSERVER +String bmp_webPresent() +{ + String page = ""; + if (bmptype) { + char itemp[10], iconv[10]; + + snprintf_P(iconv, sizeof(iconv), PSTR("°%c"), (TEMP_CONVERSION) ? 'F' : 'C'); + double t_bmp = bmp_readTemperature(TEMP_CONVERSION); + double p_bmp = bmp_readPressure(); + double h_bmp = bmp_readHumidity(); + dtostrf(t_bmp, 1, TEMP_RESOLUTION &3, itemp); + page += F("BMP Temperature: "); page += itemp; page += iconv; page += F(""); + if (!strcmp(bmpstype,"BME280")) { + dtostrf(h_bmp, 1, HUMIDITY_RESOLUTION &3, itemp); + page += F("BMP Humidity: "); page += itemp; page += F("%"); + } + dtostrf(p_bmp, 1, PRESSURE_RESOLUTION &3, itemp); + page += F("BMP Pressure: "); page += itemp; page += F(" hPa"); + } + return page; +} +#endif // USE_WEBSERVER +#endif // USE_BMP +#endif // USE_I2C + diff --git a/sonoff/xsns_dht.ino b/sonoff/xsns_dht.ino new file mode 100644 index 000000000..4809c83ac --- /dev/null +++ b/sonoff/xsns_dht.ino @@ -0,0 +1,214 @@ +/* +Copyright (c) 2017 Theo Arends. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +- Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. +- Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. +*/ + +#ifdef USE_DHT +/*********************************************************************************************\ + * DHT11, DHT21 (AM2301), DHT22 (AM2302, AM2321) - Temperature and Humidy + * + * Reading temperature or humidity takes about 250 milliseconds! + * Sensor readings may also be up to 2 seconds 'old' (its a very slow sensor) + * Source: Adafruit Industries https://github.com/adafruit/DHT-sensor-library +\*********************************************************************************************/ + +#define MIN_INTERVAL 2000 + +uint8_t data[5]; +uint32_t _lastreadtime, _maxcycles; +bool _lastresult; +float mt, mh = 0; + +void dht_readPrep() +{ + digitalWrite(pin[GPIO_DHT11], HIGH); +} + +uint32_t dht_expectPulse(bool level) +{ + uint32_t count = 0; + + while (digitalRead(pin[GPIO_DHT11]) == level) + if (count++ >= _maxcycles) return 0; + return count; +} + +boolean dht_read() +{ + char log[LOGSZ]; + uint32_t cycles[80]; + uint32_t currenttime = millis(); + + if ((currenttime - _lastreadtime) < 2000) { + return _lastresult; + } + _lastreadtime = currenttime; + + data[0] = data[1] = data[2] = data[3] = data[4] = 0; + +// digitalWrite(pin[GPIO_DHT11], HIGH); +// delay(250); + + pinMode(pin[GPIO_DHT11], OUTPUT); + digitalWrite(pin[GPIO_DHT11], LOW); + delay(20); + + noInterrupts(); + digitalWrite(pin[GPIO_DHT11], HIGH); + delayMicroseconds(40); + pinMode(pin[GPIO_DHT11], INPUT_PULLUP); + delayMicroseconds(10); + if (dht_expectPulse(LOW) == 0) { + addLog_P(LOG_LEVEL_DEBUG, PSTR("DHT: Timeout waiting for start signal low pulse")); + _lastresult = false; + return _lastresult; + } + if (dht_expectPulse(HIGH) == 0) { + addLog_P(LOG_LEVEL_DEBUG, PSTR("DHT: Timeout waiting for start signal high pulse")); + _lastresult = false; + return _lastresult; + } + for (int i=0; i<80; i+=2) { + cycles[i] = dht_expectPulse(LOW); + cycles[i+1] = dht_expectPulse(HIGH); + } + interrupts(); + + for (int i=0; i<40; ++i) { + uint32_t lowCycles = cycles[2*i]; + uint32_t highCycles = cycles[2*i+1]; + if ((lowCycles == 0) || (highCycles == 0)) { + addLog_P(LOG_LEVEL_DEBUG, PSTR("DHT: Timeout waiting for pulse")); + _lastresult = false; + return _lastresult; + } + data[i/8] <<= 1; + if (highCycles > lowCycles) data[i/8] |= 1; + } + + snprintf_P(log, sizeof(log), PSTR("DHT: Received %02X, %02X, %02X, %02X, %02X =? %02X"), + data[0], data[1], data[2], data[3], data[4], (data[0] + data[1] + data[2] + data[3]) & 0xFF); + addLog(LOG_LEVEL_DEBUG, log); + + if (data[4] == ((data[0] + data[1] + data[2] + data[3]) & 0xFF)) { + _lastresult = true; + return _lastresult; + } else { + addLog_P(LOG_LEVEL_DEBUG, PSTR("DHT: Checksum failure")); + _lastresult = false; + return _lastresult; + } +} + +float dht_convertCtoF(float c) +{ + return c * 1.8 + 32; +} + +boolean dht_readTempHum(bool S, float &t, float &h) +{ + if (!mh) { + t = NAN; + h = NAN; + } else { + t = mt; + h = mh; + } + + if (dht_read()) { + switch (dht_type) { + case DHT11: + h = data[0]; + t = data[2]; + if(S) t = dht_convertCtoF(t); + break; + case DHT22: + case DHT21: + h = data[0]; + h *= 256; + h += data[1]; + h *= 0.1; + t = data[2] & 0x7F; + t *= 256; + t += data[3]; + t *= 0.1; + if (data[2] & 0x80) t *= -1; + if(S) t = dht_convertCtoF(t); + break; + } + if (!isnan(t)) mt = t; + if (!isnan(h)) mh = h; + } + return (!isnan(t) && !isnan(h)); +} + +void dht_init() +{ + char log[LOGSZ]; + _maxcycles = microsecondsToClockCycles(1000); // 1 millisecond timeout for + // reading pulses from DHT sensor. + pinMode(pin[GPIO_DHT11], INPUT_PULLUP); + _lastreadtime = -MIN_INTERVAL; + + snprintf_P(log, sizeof(log), PSTR("DHT: Max clock cycles %d"), _maxcycles); + addLog(LOG_LEVEL_DEBUG, log); +} + +/*********************************************************************************************\ + * Presentation +\*********************************************************************************************/ + +void dht_mqttPresent(char* svalue, uint16_t ssvalue, uint8_t* djson, uint8_t domidx) +{ + char stemp1[10], stemp2[10]; + float t, h; + + if (dht_readTempHum(TEMP_CONVERSION, t, h)) { // Read temperature + dtostrf(t, 1, TEMP_RESOLUTION &3, stemp1); + dtostrf(h, 1, HUMIDITY_RESOLUTION &3, stemp2); + snprintf_P(svalue, ssvalue, PSTR("%s, \"DHT\":{\"Temperature\":\"%s\", \"Humidity\":\"%s\"}"), svalue, stemp1, stemp2); + *djson = 1; +#ifdef USE_DOMOTICZ + domoticz_sensor2(stemp1, stemp2); +#endif // USE_DOMOTICZ + } +} + +#ifdef USE_WEBSERVER +String dht_webPresent() +{ + char stemp[10], sconv[10]; + float t, h; + String page = ""; + + if (dht_readTempHum(TEMP_CONVERSION, t, h)) { // Read temperature as Celsius (the default) + snprintf_P(sconv, sizeof(sconv), PSTR("°%c"), (TEMP_CONVERSION) ? 'F' : 'C'); + dtostrf(t, 1, TEMP_RESOLUTION &3, stemp); + page += F("DHT Temperature: "); page += stemp; page += sconv; page += F(""); + dtostrf(h, 1, HUMIDITY_RESOLUTION &3, stemp); + page += F("DHT Humidity: "); page += stemp; page += F("%"); + } + return page; +} +#endif // USE_WEBSERVER +#endif // USE_DHT diff --git a/sonoff/xsns_dht2.ino b/sonoff/xsns_dht2.ino new file mode 100644 index 000000000..9239182a8 --- /dev/null +++ b/sonoff/xsns_dht2.ino @@ -0,0 +1,93 @@ +/* +Copyright (c) 2017 Theo Arends. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +- Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. +- Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. +*/ + +#ifdef USE_DHT2 +/*********************************************************************************************\ + * DHT11, DHT21 (AM2301), DHT22 (AM2302, AM2321) - Temperature and Humidy + * + * Reading temperature or humidity takes about 250 milliseconds! + * Sensor readings may also be up to 2 seconds 'old' (its a very slow sensor) +\*********************************************************************************************/ + +// WARNING: To use this DHT library you'll need to delete files DHT_U.cpp and DHT_U.h if present + +#include "DHT.h" + +DHT dht2(pin[GPIO_DHT11], dht_type); + +float dht2_t, dht2_h = 0; + +boolean dht_readTempHum(bool S, float &t, float &h) +{ + h = dht2.readHumidity(); + t = dht2.readTemperature(S); + if (!isnan(t)) dht2_t = t; else if (dht2_h) t = dht2_t; + if (!isnan(h)) dht2_h = h; else if (dht2_h) h = dht2_h; + return (!isnan(t) && !isnan(h)); +} + +void dht_init() +{ + dht2.begin(); +} + +/*********************************************************************************************\ + * Presentation +\*********************************************************************************************/ + +void dht_mqttPresent(char* svalue, uint16_t ssvalue, uint8_t* djson, uint8_t domidx) +{ + char stemp1[10], stemp2[10]; + float t, h; + + if (dht_readTempHum(TEMP_CONVERSION, t, h)) { // Read temperature + dtostrf(t, 1, TEMP_RESOLUTION &3, stemp1); + dtostrf(h, 1, HUMIDITY_RESOLUTION &3, stemp2); + snprintf_P(svalue, ssvalue, PSTR("%s, \"DHT\":{\"Temperature\":\"%s\", \"Humidity\":\"%s\"}"), svalue, stemp1, stemp2); + *djson = 1; +#ifdef USE_DOMOTICZ + domoticz_sensor2(stemp1, stemp2); +#endif // USE_DOMOTICZ + } +} + +#ifdef USE_WEBSERVER +String dht_webPresent() +{ + char stemp[10], sconv[10]; + float t, h; + String page = ""; + + if (dht_readTempHum(TEMP_CONVERSION, t, h)) { // Read temperature as Celsius (the default) + snprintf_P(sconv, sizeof(sconv), PSTR("°%c"), (TEMP_CONVERSION) ? 'F' : 'C'); + dtostrf(t, 1, TEMP_RESOLUTION &3, stemp); + page += F("DHT Temperature: "); page += stemp; page += sconv; page += F(""); + dtostrf(h, 1, HUMIDITY_RESOLUTION &3, stemp); + page += F("DHT Humidity: "); page += stemp; page += F("%"); + } + return page; +} +#endif // USE_WEBSERVER +#endif // USE_DHT2 diff --git a/sonoff/xsns_ds18b20.ino b/sonoff/xsns_ds18b20.ino new file mode 100644 index 000000000..eec7d5cb2 --- /dev/null +++ b/sonoff/xsns_ds18b20.ino @@ -0,0 +1,206 @@ +/* +Copyright (c) 2017 Theo Arends. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +- Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. +- Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. +*/ + +#ifdef USE_DS18B20 +/*********************************************************************************************\ + * DS18B20 - Temperature + * + * Source: Marinus vd Broek https://github.com/ESP8266nu/ESPEasy and AlexTransit (CRC) +\*********************************************************************************************/ + +uint8_t dsb_reset() +{ + uint8_t r; + uint8_t retries = 125; + + pinMode(pin[GPIO_DSB], INPUT); + do { // wait until the wire is high... just in case + if (--retries == 0) return 0; + delayMicroseconds(2); + } while (!digitalRead(pin[GPIO_DSB])); + pinMode(pin[GPIO_DSB], OUTPUT); + digitalWrite(pin[GPIO_DSB], LOW); + delayMicroseconds(492); // Dallas spec. = Min. 480uSec. Arduino 500uSec. + pinMode(pin[GPIO_DSB], INPUT); // Float + delayMicroseconds(40); + r = !digitalRead(pin[GPIO_DSB]); + delayMicroseconds(420); + return r; +} + +uint8_t dsb_read_bit(void) +{ + uint8_t r; + + pinMode(pin[GPIO_DSB], OUTPUT); + digitalWrite(pin[GPIO_DSB], LOW); + delayMicroseconds(3); + pinMode(pin[GPIO_DSB], INPUT); // let pin float, pull up will raise + delayMicroseconds(10); + r = digitalRead(pin[GPIO_DSB]); + delayMicroseconds(53); + return r; +} + +uint8_t dsb_read(void) +{ + uint8_t bitMask; + uint8_t r = 0; + + for (bitMask = 0x01; bitMask; bitMask <<= 1) + if (dsb_read_bit()) r |= bitMask; + return r; +} + +void dsb_write_bit(uint8_t v) +{ + if (v & 1) { + digitalWrite(pin[GPIO_DSB], LOW); + pinMode(pin[GPIO_DSB], OUTPUT); + delayMicroseconds(10); + digitalWrite(pin[GPIO_DSB], HIGH); + delayMicroseconds(55); + } else { + digitalWrite(pin[GPIO_DSB], LOW); + pinMode(pin[GPIO_DSB], OUTPUT); + delayMicroseconds(65); + digitalWrite(pin[GPIO_DSB], HIGH); + delayMicroseconds(5); + } +} + +void dsb_write(uint8_t ByteToWrite) +{ + uint8_t bitMask; + + for (bitMask = 0x01; bitMask; bitMask <<= 1) + dsb_write_bit((bitMask & ByteToWrite) ? 1 : 0); +} + +uint8 dsb_crc(uint8 inp, uint8 crc) +{ + inp ^= crc; + crc = 0; + if (inp & 0x1) crc ^= 0x5e; + if (inp & 0x2) crc ^= 0xbc; + if (inp & 0x4) crc ^= 0x61; + if (inp & 0x8) crc ^= 0xc2; + if (inp & 0x10) crc ^= 0x9d; + if (inp & 0x20) crc ^= 0x23; + if (inp & 0x40) crc ^= 0x46; + if (inp & 0x80) crc ^= 0x8c; + return crc; +} + +void dsb_readTempPrep() +{ + dsb_reset(); + dsb_write(0xCC); // Skip ROM + dsb_write(0x44); // Start conversion +} + +float dsb_convertCtoF(float c) +{ + return c * 1.8 + 32; +} + +boolean dsb_readTemp(bool S, float &t) +{ + int16_t DSTemp; + byte msb, lsb, crc; + + t = NAN; + + if (!dsb_read_bit()) { //check measurement end + addLog_P(LOG_LEVEL_DEBUG, PSTR("DSB: Sensor busy")); + return false; + } +/* + dsb_reset(); + dsb_write(0xCC); // Skip ROM + dsb_write(0x44); // Start conversion + delay(800); +*/ + dsb_reset(); + dsb_write(0xCC); // Skip ROM + dsb_write(0xBE); // Read scratchpad + lsb = dsb_read(); + msb = dsb_read(); + crc = dsb_crc(lsb, crc); + crc = dsb_crc(msb, crc); + crc = dsb_crc(dsb_read(), crc); + crc = dsb_crc(dsb_read(), crc); + crc = dsb_crc(dsb_read(), crc); + crc = dsb_crc(dsb_read(), crc); + crc = dsb_crc(dsb_read(), crc); + crc = dsb_crc(dsb_read(), crc); + crc = dsb_crc(dsb_read(), crc); + dsb_reset(); + if (crc) { //check crc + addLog_P(LOG_LEVEL_DEBUG, PSTR("DSB: Sensor CRC error")); + } else { + DSTemp = (msb << 8) + lsb; + t = (float(DSTemp) * 0.0625); + if(S) t = dsb_convertCtoF(t); + } + return !isnan(t); +} + +/*********************************************************************************************\ + * Presentation +\*********************************************************************************************/ + +void dsb_mqttPresent(char* svalue, uint16_t ssvalue, uint8_t* djson, uint8_t domidx) +{ + char stemp1[10]; + float t; + + if (dsb_readTemp(TEMP_CONVERSION, t)) { // Check if read failed + dtostrf(t, 1, TEMP_RESOLUTION &3, stemp1); + snprintf_P(svalue, ssvalue, PSTR("%s, \"DS18B20\":{\"Temperature\":\"%s\"}"), svalue, stemp1); + *djson = 1; +#ifdef USE_DOMOTICZ + domoticz_sensor1(stemp1); +#endif // USE_DOMOTICZ + } +} + +#ifdef USE_WEBSERVER +String dsb_webPresent() +{ + // Needs TelePeriod to refresh data (Do not do it here as it takes too much time) + char stemp[10], sconv[10]; + float st; + String page = ""; + + if (dsb_readTemp(TEMP_CONVERSION, st)) { // Check if read failed + snprintf_P(sconv, sizeof(sconv), PSTR("°%c"), (TEMP_CONVERSION) ? 'F' : 'C'); + dtostrf(st, 1, TEMP_RESOLUTION &3, stemp); + page += F("DSB Temperature: "); page += stemp; page += sconv; page += F(""); + } + return page; +} +#endif // USE_WEBSERVER +#endif // USE_DS18B20 diff --git a/sonoff/xsns_ds18x20.ino b/sonoff/xsns_ds18x20.ino new file mode 100644 index 000000000..1205d76ea --- /dev/null +++ b/sonoff/xsns_ds18x20.ino @@ -0,0 +1,201 @@ +/* +Copyright (c) 2017 Theo Arends. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +- Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. +- Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. +*/ + +#ifdef USE_DS18x20 +/*********************************************************************************************\ + * DS18B20 - Temperature +\*********************************************************************************************/ + +#define W1_SKIP_ROM 0xCC +#define W1_CONVERT_TEMP 0x44 +#define W1_READ_SCRATCHPAD 0xBE + +#define DS18X20_MAX_SENSORS 8 + +#include + +OneWire ds(pin[GPIO_DSB]); + +uint8_t ds18x20_addr[DS18X20_MAX_SENSORS][8]; +uint8_t ds18x20_idx[DS18X20_MAX_SENSORS]; +uint8_t ds18x20_snsrs = 0; + +void ds18x20_search() +{ + uint8_t num_sensors=0; + uint8_t sensor = 0; + uint8_t i; + + ds.reset_search(); + for (num_sensors = 0; num_sensors < DS18X20_MAX_SENSORS; num_sensors) + { + if (!ds.search(ds18x20_addr[num_sensors])) { + ds.reset_search(); + break; + } + // If CRC Ok and Type DS18S20 or DS18B20 + if ((OneWire::crc8(ds18x20_addr[num_sensors], 7) == ds18x20_addr[num_sensors][7]) && + ((ds18x20_addr[num_sensors][0]==0x10) || (ds18x20_addr[num_sensors][0]==0x28))) + num_sensors++; + } + for (int i = 0; i < num_sensors; i++) ds18x20_idx[i] = i; + for (int i = 0; i < num_sensors; i++) { + for (int j = i + 1; j < num_sensors; j++) { + if (uint32_t(ds18x20_addr[ds18x20_idx[i]]) > uint32_t(ds18x20_addr[ds18x20_idx[j]])) { + std::swap(ds18x20_idx[i], ds18x20_idx[j]); + } + } + } + ds18x20_snsrs = num_sensors; +} + +uint8_t ds18x20_sensors() +{ + return ds18x20_snsrs; +} + +String ds18x20_address(uint8_t sensor) +{ + char addrStr[20]; + uint8_t i; + + for (i = 0; i < 8; i++) sprintf(addrStr+2*i, "%02X", ds18x20_addr[ds18x20_idx[sensor]][i]); + return String(addrStr); +} + +String ds18x20_type(uint8_t sensor) +{ + char typeStr[10]; + + switch(ds18x20_addr[ds18x20_idx[sensor]][0]) { + case 0x10: + strcpy(typeStr, "DS18S20"); + break; + case 0x28: + strcpy(typeStr, "DS18B20"); + break; + default: + strcpy(typeStr, "DS18x20"); + } + return String(typeStr); +} + +void ds18x20_convert() +{ + ds.reset(); + ds.write(W1_SKIP_ROM); // Address all Sensors on Bus + ds.write(W1_CONVERT_TEMP); // start conversion, no parasite power on at the end +// delay(750); // 750ms should be enough for 12bit conv +} + +float ds18x20_convertCtoF(float c) +{ + return c * 1.8 + 32; +} + +boolean ds18x20_read(uint8_t sensor, bool S, float &t) +{ + byte data[12]; + uint8_t sign = 1; + uint8_t i = 0; + float temp9 = 0.0; + uint8_t present = 0; + + t = NAN; + + ds.reset(); + ds.select(ds18x20_addr[ds18x20_idx[sensor]]); + ds.write(W1_READ_SCRATCHPAD); // Read Scratchpad + + for (i = 0; i < 9; i++) data[i] = ds.read(); + if (OneWire::crc8(data, 8) == data[8]) { + switch(ds18x20_addr[ds18x20_idx[sensor]][0]) { + case 0x10: // DS18S20 + if (data[1] > 0x80) sign = -1; // App-Note fix possible sign error + if (data[0] & 1) { + temp9 = ((data[0] >> 1) + 0.5) * sign; + } else { + temp9 = (data[0] >> 1) * sign; + } + t = (temp9 - 0.25) + ((16.0 - data[6]) / 16.0); + if(S) t = ds18x20_convertCtoF(t); + break; + case 0x28: // DS18B20 + t = ((data[1] << 8) + data[0]) * 0.0625; + if(S) t = ds18x20_convertCtoF(t); + break; + } + } + return (!isnan(t)); +} + +/*********************************************************************************************\ + * Presentation +\*********************************************************************************************/ + +void ds18x20_mqttPresent(char* svalue, uint16_t ssvalue, uint8_t* djson, uint8_t domidx) +{ + char stemp1[10], stemp2[10]; + float t; + + byte dsxflg = 0; + for (byte i = 0; i < ds18x20_sensors(); i++) { + if (ds18x20_read(i, TEMP_CONVERSION, t)) { // Check if read failed + dtostrf(t, 1, TEMP_RESOLUTION &3, stemp2); + if (!dsxflg) { + snprintf_P(svalue, ssvalue, PSTR("%s, \"DS18x20\":{"), svalue); + *djson = 1; + stemp1[0] = '\0'; + } + dsxflg++; + snprintf_P(svalue, ssvalue, PSTR("%s%s\"DS%d\":{\"Type\":\"%s\", \"Address\":\"%s\", \"Temperature\":\"%s\"}"), + svalue, stemp1, i +1, ds18x20_type(i).c_str(), ds18x20_address(i).c_str(), stemp2); + strcpy(stemp1, ", "); +#ifdef USE_DOMOTICZ + if (dsxflg == 1) domoticz_sensor1(stemp2); +#endif // USE_DOMOTICZ + } + } + if (dsxflg) snprintf_P(svalue, ssvalue, PSTR("%s}"), svalue); +} + +#ifdef USE_WEBSERVER +String ds18x20_webPresent() +{ + char stemp[10], sconv[10]; + float t; + String page = ""; + + snprintf_P(sconv, sizeof(sconv), PSTR("°%c"), (TEMP_CONVERSION) ? 'F' : 'C'); + for (byte i = 0; i < ds18x20_sensors(); i++) { + if (ds18x20_read(i, TEMP_CONVERSION, t)) { // Check if read failed + dtostrf(t, 1, TEMP_RESOLUTION &3, stemp); + page += F("DS"); page += String(i +1); page += F(" Temperature: "); page += stemp; page += sconv; page += F(""); + } + } + return page; +} +#endif // USE_WEBSERVER +#endif // USE_DS18x20 diff --git a/sonoff/xsns_hlw8012.ino b/sonoff/xsns_hlw8012.ino new file mode 100644 index 000000000..de4f0e8cb --- /dev/null +++ b/sonoff/xsns_hlw8012.ino @@ -0,0 +1,412 @@ +/* +Copyright (c) 2017 Theo Arends. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +- Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. +- Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. +*/ + +/*********************************************************************************************\ + * HLW8012 - Energy + * + * Based on Source: Shenzhen Heli Technology Co., Ltd +\*********************************************************************************************/ + +#define HLW_PREF 10000 // 1000.0W +#define HLW_UREF 2200 // 220.0V +#define HLW_IREF 4545 // 4.545A + +byte hlw_SELflag, hlw_cf_timer, hlw_cf1_timer, hlw_fifth_second, hlw_startup; +unsigned long hlw_cf_plen, hlw_cf_last; +unsigned long hlw_cf1_plen, hlw_cf1_last, hlw_cf1_ptot, hlw_cf1_pcnt, hlw_cf1u_plen, hlw_cf1i_plen; +unsigned long hlw_Ecntr, hlw_EDcntr, hlw_kWhtoday; +uint32_t hlw_lasttime; + +unsigned long hlw_cf1u_pcntmax, hlw_cf1i_pcntmax; + +Ticker tickerHLW; + +#ifndef USE_WS2812_DMA // Collides with Neopixelbus but solves exception +void hlw_cf_interrupt() ICACHE_RAM_ATTR; +void hlw_cf1_interrupt() ICACHE_RAM_ATTR; +#endif // USE_WS2812_DMA + +void hlw_cf_interrupt() // Service Power +{ + hlw_cf_plen = micros() - hlw_cf_last; + hlw_cf_last = micros(); + if (hlw_cf_plen > 4000000) hlw_cf_plen = 0; // Just powered on + hlw_cf_timer = 15; // Support down to 4W which takes about 3 seconds + hlw_EDcntr++; + hlw_Ecntr++; +} + +void hlw_cf1_interrupt() // Service Voltage and Current +{ + hlw_cf1_plen = micros() - hlw_cf1_last; + hlw_cf1_last = micros(); + if ((hlw_cf1_timer > 2) && (hlw_cf1_timer < 8)) { // Allow for 300 mSec set-up time and measure for up to 1 second + hlw_cf1_ptot += hlw_cf1_plen; + hlw_cf1_pcnt++; + if (hlw_cf1_pcnt == 10) hlw_cf1_timer = 8; // We need up to ten samples within 1 second (low current could take up to 0.3 second) + } +} + +void hlw_200mS() +{ + unsigned long hlw_len, hlw_temp; + + hlw_fifth_second++; + if (hlw_fifth_second == 5) { + hlw_fifth_second = 0; + + if (hlw_EDcntr) { + hlw_len = 1000000 / hlw_EDcntr; + hlw_EDcntr = 0; + hlw_temp = (HLW_PREF * sysCfg.hlw_pcal) / hlw_len; + hlw_kWhtoday += (hlw_temp * 100) / 36; + } + if (rtc_loctime() == rtc_midnight()) { + sysCfg.hlw_kWhyesterday = hlw_kWhtoday; + hlw_kWhtoday = 0; + } + if (hlw_startup && rtcTime.Valid && (rtcTime.DayOfYear == sysCfg.hlw_kWhdoy)) { + hlw_kWhtoday = sysCfg.hlw_kWhtoday; + hlw_startup = 0; + } + } + + if (hlw_cf_timer) { + hlw_cf_timer--; + if (!hlw_cf_timer) hlw_cf_plen = 0; // No load for over three seconds + } + + hlw_cf1_timer++; + if (hlw_cf1_timer >= 8) { + hlw_cf1_timer = 0; + hlw_SELflag = (hlw_SELflag) ? 0 : 1; + digitalWrite(pin[GPIO_HLW_SEL], hlw_SELflag); + + if (hlw_cf1_pcnt) { + hlw_cf1_plen = hlw_cf1_ptot / hlw_cf1_pcnt; + } else { + hlw_cf1_plen = 0; + } + if (hlw_SELflag) { + hlw_cf1u_plen = hlw_cf1_plen; + hlw_cf1u_pcntmax = hlw_cf1_pcnt; + } else { + hlw_cf1i_plen = hlw_cf1_plen; + hlw_cf1i_pcntmax = hlw_cf1_pcnt; + } + hlw_cf1_ptot = 0; + hlw_cf1_pcnt = 0; + } +} + +void hlw_savestate() +{ + sysCfg.hlw_kWhdoy = (rtcTime.Valid) ? rtcTime.DayOfYear : 0; + sysCfg.hlw_kWhtoday = hlw_kWhtoday; +} + +boolean hlw_readEnergy(byte option, float &ed, uint16_t &e, uint16_t &w, uint16_t &u, float &i, float &c) +{ + unsigned long hlw_len, hlw_temp, hlw_w, hlw_u, hlw_i; + int hlw_period, hlw_interval; + +//char log[LOGSZ]; +//snprintf_P(log, sizeof(log), PSTR("HLW: CF %d, CF1U %d (%d), CF1I %d (%d)"), hlw_cf_plen, hlw_cf1u_plen, hlw_cf1u_pcntmax, hlw_cf1i_plen, hlw_cf1i_pcntmax); +//addLog(LOG_LEVEL_DEBUG, log); + + if (hlw_kWhtoday) { + ed = (float)hlw_kWhtoday / 100000000; + } else { + ed = 0; + } + + if (option) { + if (!hlw_lasttime) { + hlw_period = sysCfg.tele_period; + } else { + hlw_period = rtc_loctime() - hlw_lasttime; + } + hlw_lasttime = rtc_loctime(); + hlw_interval = 3600 / hlw_period; + if (hlw_Ecntr) { + hlw_len = hlw_period * 1000000 / hlw_Ecntr; + hlw_Ecntr = 0; + hlw_temp = ((HLW_PREF * sysCfg.hlw_pcal) / hlw_len) / hlw_interval; + e = hlw_temp / 10; + } else { + e = 0; + } + } + + if (hlw_cf_plen) { + hlw_w = (HLW_PREF * sysCfg.hlw_pcal) / hlw_cf_plen; + w = hlw_w / 10; + } else { + w = 0; + } + if (hlw_cf1u_plen && (w || (power &1))) { + hlw_u = (HLW_UREF * sysCfg.hlw_ucal) / hlw_cf1u_plen; + u = hlw_u / 10; + } else { + u = 0; + } + if (hlw_cf1i_plen && w) { + hlw_i = (HLW_IREF * sysCfg.hlw_ical) / hlw_cf1i_plen; + i = (float)hlw_i / 1000; + } else { + i = 0; + } + if (hlw_i && hlw_u && hlw_w && w) { + hlw_temp = (hlw_w * 100) / ((hlw_u * hlw_i) / 1000); + if (hlw_temp > 100) { + hlw_temp = 100; + } + c = (float)hlw_temp / 100; + } else { + c = 0; + } + + return true; +} + +void hlw_init() +{ + if (!sysCfg.hlw_pcal || (sysCfg.hlw_pcal == 4975)) { + sysCfg.hlw_pcal = HLW_PREF_PULSE; + sysCfg.hlw_ucal = HLW_UREF_PULSE; + sysCfg.hlw_ical = HLW_IREF_PULSE; + } + + hlw_cf_plen = 0; + hlw_cf_last = 0; + hlw_cf1_plen = 0; + hlw_cf1_last = 0; + hlw_cf1u_plen = 0; + hlw_cf1i_plen = 0; + hlw_cf1u_pcntmax = 0; + hlw_cf1i_pcntmax = 0; + + hlw_Ecntr = 0; + hlw_EDcntr = 0; + hlw_kWhtoday = 0; + + hlw_SELflag = 0; // Voltage; + + pinMode(pin[GPIO_HLW_SEL], OUTPUT); + digitalWrite(pin[GPIO_HLW_SEL], hlw_SELflag); + pinMode(pin[GPIO_HLW_CF1], INPUT_PULLUP); + attachInterrupt(pin[GPIO_HLW_CF1], hlw_cf1_interrupt, FALLING); + pinMode(pin[GPIO_HLW_CF], INPUT_PULLUP); + attachInterrupt(pin[GPIO_HLW_CF], hlw_cf_interrupt, FALLING); + + hlw_startup = 1; + hlw_lasttime = 0; + hlw_fifth_second = 0; + hlw_cf_timer = 0; + hlw_cf1_timer = 0; + tickerHLW.attach_ms(200, hlw_200mS); +} + +/********************************************************************************************/ + +boolean hlw_margin(byte type, uint16_t margin, uint16_t value, byte &flag, byte &saveflag) +{ + byte change; + + if (!margin) return false; + change = saveflag; + if (type) { + flag = (value > margin); + } else { + flag = (value < margin); + } + saveflag = flag; + return (change != saveflag); +} + +void hlw_margin_chk() +{ + char log[LOGSZ], stopic[TOPSZ], svalue[MESSZ]; + float ped, pi, pc; + uint16_t uped, piv, pe, pw, pu; + byte flag, jsonflg; + + if (power_steady_cntr) { + power_steady_cntr--; + return; + } + + hlw_readEnergy(0, ped, pe, pw, pu, pi, pc); + if (power && (sysCfg.hlw_pmin || sysCfg.hlw_pmax || sysCfg.hlw_umin || sysCfg.hlw_umax || sysCfg.hlw_imin || sysCfg.hlw_imax)) { + piv = (uint16_t)(pi * 1000); + +// snprintf_P(log, sizeof(log), PSTR("HLW: W %d, U %d, I %d"), pw, pu, piv); +// addLog(LOG_LEVEL_DEBUG, log); + + snprintf_P(stopic, sizeof(stopic), PSTR("%s/%s/TELEMETRY"), PUB_PREFIX2, sysCfg.mqtt_topic); + snprintf_P(svalue, sizeof(svalue), PSTR("{")); + jsonflg = 0; + if (hlw_margin(0, sysCfg.hlw_pmin, pw, flag, hlw_pminflg)) { + snprintf_P(svalue, sizeof(svalue), PSTR("%s%s\"PowerLow\":\"%s\""), svalue, (jsonflg)?", ":"", (flag) ? MQTT_STATUS_ON : MQTT_STATUS_OFF); + jsonflg = 1; + } + if (hlw_margin(1, sysCfg.hlw_pmax, pw, flag, hlw_pmaxflg)) { + snprintf_P(svalue, sizeof(svalue), PSTR("%s%s\"PowerHigh\":\"%s\""), svalue, (jsonflg)?", ":"", (flag) ? MQTT_STATUS_ON : MQTT_STATUS_OFF); + jsonflg = 1; + } + if (hlw_margin(0, sysCfg.hlw_umin, pu, flag, hlw_uminflg)) { + snprintf_P(svalue, sizeof(svalue), PSTR("%s%s\"VoltageLow\":\"%s\""), svalue, (jsonflg)?", ":"", (flag) ? MQTT_STATUS_ON : MQTT_STATUS_OFF); + jsonflg = 1; + } + if (hlw_margin(1, sysCfg.hlw_umax, pw, flag, hlw_umaxflg)) { + snprintf_P(svalue, sizeof(svalue), PSTR("%s%s\"VoltageHigh\":\"%s\""), svalue, (jsonflg)?", ":"", (flag) ? MQTT_STATUS_ON : MQTT_STATUS_OFF); + jsonflg = 1; + } + if (hlw_margin(0, sysCfg.hlw_imin, piv, flag, hlw_iminflg)) { + snprintf_P(svalue, sizeof(svalue), PSTR("%s%s\"CurrentLow\":\"%s\""), svalue, (jsonflg)?", ":"", (flag) ? MQTT_STATUS_ON : MQTT_STATUS_OFF); + jsonflg = 1; + } + if (hlw_margin(1, sysCfg.hlw_imax, piv, flag, hlw_imaxflg)) { + snprintf_P(svalue, sizeof(svalue), PSTR("%s%s\"CurrentHigh\":\"%s\""), svalue, (jsonflg)?", ":"", (flag) ? MQTT_STATUS_ON : MQTT_STATUS_OFF); + jsonflg = 1; + } + if (jsonflg) { + snprintf_P(svalue, sizeof(svalue), PSTR("%s}"), svalue); + mqtt_publish(stopic, svalue); + } + } + +#ifdef FEATURE_POWER_LIMIT + snprintf_P(stopic, sizeof(stopic), PSTR("%s/%s/RESULT"), PUB_PREFIX, sysCfg.mqtt_topic); + // Max Power + if (sysCfg.hlw_mpl) { + if (pw > sysCfg.hlw_mpl) { + if (!hlw_mplh_counter) { + hlw_mplh_counter = sysCfg.hlw_mplh; + } else { + hlw_mplh_counter--; + if (!hlw_mplh_counter) { + snprintf_P(svalue, sizeof(svalue), PSTR("{\"MaxPowerReached\":\"%d%s\"}"), pw, (sysCfg.value_units) ? " W" : ""); + mqtt_publish(stopic, svalue); + do_cmnd_power(1, 0); + if (!hlw_mplr_counter) hlw_mplr_counter = MAX_POWER_RETRY +1; + hlw_mplw_counter = sysCfg.hlw_mplw; + } + } + } + else if (power && (pw <= sysCfg.hlw_mpl)) { + hlw_mplh_counter = 0; + hlw_mplr_counter = 0; + hlw_mplw_counter = 0; + } + if (!power) { + if (hlw_mplw_counter) { + hlw_mplw_counter--; + } else { + if (hlw_mplr_counter) { + hlw_mplr_counter--; + if (hlw_mplr_counter) { + snprintf_P(svalue, sizeof(stopic), PSTR("{\"PowerMonitor\":\"%s\"}"), MQTT_STATUS_ON); + mqtt_publish(stopic, svalue); + do_cmnd_power(1, 1); + } else { + snprintf_P(svalue, sizeof(stopic), PSTR("{\"MaxPowerReachedRetry\":\"%s\"}"), MQTT_STATUS_OFF); + mqtt_publish(stopic, svalue); + } + } + } + } + } + + // Max Energy + if (sysCfg.hlw_mkwh) { + uped = (uint16_t)(ped * 1000); + if (!hlw_mkwh_state && (rtcTime.Hour == sysCfg.hlw_mkwhs)) { + hlw_mkwh_state = 1; + snprintf_P(svalue, sizeof(stopic), PSTR("{\"EnergyMonitor\":\"%s\"}"), MQTT_STATUS_ON); + mqtt_publish(stopic, svalue); + do_cmnd_power(1, 1); + } + else if ((hlw_mkwh_state == 1) && (uped >= sysCfg.hlw_mkwh)) { + hlw_mkwh_state = 2; + dtostrf(ped, 1, 3, svalue); + snprintf_P(svalue, sizeof(svalue), PSTR("{\"MaxEnergyReached\":\"%s%s\"}"), svalue, (sysCfg.value_units) ? " kWh" : ""); + mqtt_publish(stopic, svalue); + do_cmnd_power(1, 0); + } + } +#endif // FEATURE_POWER_LIMIT +} + +/*********************************************************************************************\ + * Presentation +\*********************************************************************************************/ + +void hlw_mqttPresent(uint8_t domidx) +{ + char stopic[TOPSZ], svalue[MESSZ], stime[21], stemp0[10], stemp1[10], stemp2[10], stemp3[10]; + float ped, pi, pc; + uint16_t pe, pw, pu; + + snprintf_P(stime, sizeof(stime), PSTR("%04d-%02d-%02dT%02d:%02d:%02d"), + rtcTime.Year, rtcTime.Month, rtcTime.Day, rtcTime.Hour, rtcTime.Minute, rtcTime.Second); + hlw_readEnergy(1, ped, pe, pw, pu, pi, pc); + dtostrf((float)sysCfg.hlw_kWhyesterday / 100000000, 1, 3, stemp0); + dtostrf(ped, 1, 3, stemp1); + dtostrf(pc, 1, 2, stemp2); + dtostrf(pi, 1, 3, stemp3); + snprintf_P(stopic, sizeof(stopic), PSTR("%s/%s/TELEMETRY"), PUB_PREFIX2, sysCfg.mqtt_topic); + snprintf_P(svalue, sizeof(svalue), PSTR("{\"Time\":\"%s\", \"Energy\":{\"Yesterday\":\"%s\", \"Today\":\"%s\", \"Period\":%d, \"Power\":%d, \"Factor\":\"%s\", \"Voltage\":%d, \"Current\":\"%s\"}}"), + stime, stemp0, stemp1, pe, pw, stemp2, pu, stemp3); + mqtt_publish(stopic, svalue); +#ifdef USE_DOMOTICZ + dtostrf(ped * 1000, 1, 1, stemp1); + domoticz_sensor4(pw, stemp1); +#endif // USE_DOMOTICZ +} + +#ifdef USE_WEBSERVER +String hlw_webPresent() +{ + char stemp[10]; + float ped, pi, pc; + uint16_t pe, pw, pu; + String page = ""; + + hlw_readEnergy(0, ped, pe, pw, pu, pi, pc); + page += F("Voltage: "); page += String(pu); page += F(" V"); + dtostrf(pi, 1, 3, stemp); + page += F("Current: "); page += stemp; page += F(" A"); + page += F("Power: "); page += String(pw); page += F(" W"); + dtostrf(pc, 1, 2, stemp); + page += F("Power Factor: "); page += stemp; page += F(""); + dtostrf(ped, 1, 3, stemp); + page += F("Energy Today: "); page += stemp; page += F(" kWh"); + dtostrf((float)sysCfg.hlw_kWhyesterday / 100000000, 1, 3, stemp); + page += F("Energy Yesterday: "); page += stemp; page += F(" kWh"); + return page; +} +#endif // USE_WEBSERVER + diff --git a/sonoff/xsns_htu21.ino b/sonoff/xsns_htu21.ino new file mode 100644 index 000000000..ddb9cc6bc --- /dev/null +++ b/sonoff/xsns_htu21.ino @@ -0,0 +1,271 @@ +/* + Copyright (c) 2017 Heiko Krupp. All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + - Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + - Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + POSSIBILITY OF SUCH DAMAGE. +*/ + +#ifdef USE_I2C +#ifdef USE_HTU +/*********************************************************************************************\ + * HTU21 - Temperature and Humidy + * + * Source: Heiko Krupp +\*********************************************************************************************/ + +#define HTU21_ADDR 0x40 + +#define HTU21_CHIPID 0x32 + +#define HTU21_READTEMP 0xE3 +#define HTU21_READHUM 0xE5 +#define HTU21_WRITEREG 0xE6 +#define HTU21_READREG 0xE7 +#define HTU21_RESET 0xFE +#define HTU21_HEATER_WRITE 0x51 +#define HTU21_HEATER_READ 0x11 +#define HTU21_SERIAL2_READ1 0xFC /* Read 3rd two Serial bytes */ +#define HTU21_SERIAL2_READ2 0xC9 /* Read 4th two Serial bytes */ + +#define HTU21_HEATER_ON 0x04 +#define HTU21_HEATER_OFF 0xFB + +#define HTU21_RES_RH12_T14 0x00 // Default +#define HTU21_RES_RH8_T12 0x01 +#define HTU21_RES_RH10_T13 0x80 +#define HTU21_RES_RH11_T11 0x81 + +#define HTU21_MAX_HUM 16 // 16ms max time +#define HTU21_MAX_TEMP 50 // 50ms max time + +#define HTU21_CRC8_POLYNOM 0x13100 + +uint8_t htuaddr, htutype = 0; +char htustype[7]; + +uint8_t check_crc8(uint16_t data) +{ + for (uint8_t bit = 0; bit < 16; bit++) + { + if (data & 0x8000) + data = (data << 1) ^ HTU21_CRC8_POLYNOM; + else + data <<= 1; + } + return data >>= 8; +} + +uint8_t htu21_readDeviceID(void) +{ + uint16_t deviceID = 0; + uint8_t checksum = 0; + + Wire.beginTransmission(HTU21_ADDR); + Wire.write(HTU21_SERIAL2_READ1); + Wire.write(HTU21_SERIAL2_READ2); + Wire.endTransmission(); + + Wire.requestFrom(HTU21_ADDR, 3); + deviceID = Wire.read() << 8; + deviceID |= Wire.read(); + checksum = Wire.read(); + if (check_crc8(deviceID) == checksum) { + deviceID = deviceID >> 8; + } else { + deviceID = 0; + } + return (uint8_t)deviceID; +} + +void htu21_setRes(uint8_t resolution) +{ + uint8_t current = i2c_read8(HTU21_ADDR, HTU21_READREG); + current &= 0x7E; // Replace current resolution bits with 0 + current |= resolution; // Add new resolution bits to register + i2c_write8(HTU21_ADDR, HTU21_WRITEREG, current); +} + +void htu21_reset(void) +{ + Wire.beginTransmission(HTU21_ADDR); + Wire.write(HTU21_RESET); + Wire.endTransmission(); + delay(15); // Reset takes 15ms +} + +void htu21_heater(uint8_t heater) +{ + uint8_t current = i2c_read8(HTU21_ADDR, HTU21_READREG); + + switch(heater) + { + case HTU21_HEATER_ON : current |= heater; + break; + case HTU21_HEATER_OFF : current &= heater; + break; + default : current &= heater; + break; + } + i2c_write8(HTU21_ADDR, HTU21_WRITEREG, current); +} + +boolean htu21_init() +{ + htu21_reset(); + htu21_heater(HTU21_HEATER_OFF); + htu21_setRes(HTU21_RES_RH12_T14); + return true; +} + +float htu21_convertCtoF(float c) +{ + return c * 1.8 + 32; +} + +float htu21_readHumidity(void) +{ + uint8_t checksum=0; + uint16_t sensorval=0; + float humidity=0.0; + + Wire.beginTransmission(HTU21_ADDR); + Wire.write(HTU21_READHUM); + if(Wire.endTransmission() != 0) return 0.0; // In case of error + delay(HTU21_MAX_HUM); // HTU21 time at max resolution + + Wire.requestFrom(HTU21_ADDR, 3); + if(3 <= Wire.available()) + { + sensorval = Wire.read() << 8; // MSB + sensorval |= Wire.read(); // LSB + checksum = Wire.read(); + } + if(check_crc8(sensorval) != checksum) return 0.0; // Checksum mismatch + + sensorval ^= 0x02; // clear status bits + humidity = 0.001907 * (float)sensorval - 6; + + if(humidity>100) return 100.0; + if(humidity<0) return 0.01; + + return humidity; +} + +float htu21_readTemperature(bool S) +{ + uint8_t checksum=0; + uint16_t sensorval=0; + float t; + + Wire.beginTransmission(HTU21_ADDR); + Wire.write(HTU21_READTEMP); + if(Wire.endTransmission() != 0) return 0.0; // In case of error + delay(HTU21_MAX_TEMP); // HTU21 time at max resolution + + Wire.requestFrom(HTU21_ADDR, 3); + if(3 == Wire.available()) + { + sensorval = Wire.read() << 8; // MSB + sensorval |= Wire.read(); // LSB + checksum = Wire.read(); + } + if(check_crc8(sensorval) != checksum) return 0.0; // Checksum mismatch + + t = (0.002681 * (float)sensorval - 46.85); + if(S) t = htu21_convertCtoF(t); + return t; +} + +float htu21_compensatedHumidity(float humidity, float temperature) +{ + if(humidity == 0.00 && temperature == 0.00) return 0.0; + if(temperature > 0.00 && temperature < 80.00) + return (-0.15)*(25-temperature)+humidity; +} + +uint8_t htu_detect() +{ + if (htutype) return true; + + char log[LOGSZ]; + boolean success = false; + + htuaddr = HTU21_ADDR; + htutype = htu21_readDeviceID(); + snprintf_P(htustype, sizeof(htustype), PSTR("HTU")); + switch (htutype) { + case HTU21_CHIPID: + success = htu21_init(); + snprintf_P(htustype, sizeof(htustype), PSTR("HTU21")); + } + if (success) { + snprintf_P(log, sizeof(log), PSTR("I2C: %s found at address 0x%x"), htustype, htuaddr); + addLog(LOG_LEVEL_DEBUG, log); + } else { + htutype = 0; + } + return success; +} + +/*********************************************************************************************\ + * Presentation +\*********************************************************************************************/ + +void htu_mqttPresent(char* svalue, uint16_t ssvalue, uint8_t* djson, uint8_t domidx) +{ + if (!htutype) return; + + char stemp1[10], stemp2[10]; + + float t = htu21_readTemperature(TEMP_CONVERSION); + float h = htu21_readHumidity(); + h = htu21_compensatedHumidity(h, t); + dtostrf(t, 1, TEMP_RESOLUTION &3, stemp1); + dtostrf(h, 1, HUMIDITY_RESOLUTION &3, stemp2); + snprintf_P(svalue, ssvalue, PSTR("%s, \"%s\":{\"Temperature\":\"%s\", \"Humidity\":\"%s\"}"), svalue, htustype, stemp1, stemp2); + *djson = 1; +#ifdef USE_DOMOTICZ + domoticz_sensor2(stemp1, stemp2); +#endif // USE_DOMOTICZ +} + +#ifdef USE_WEBSERVER +String htu_webPresent() +{ + String page = ""; + if (htutype) { + char itemp[10], iconv[10]; + + snprintf_P(iconv, sizeof(iconv), PSTR("°%c"), (TEMP_CONVERSION) ? 'F' : 'C'); + float t_htu21 = htu21_readTemperature(TEMP_CONVERSION); + float h_htu21 = htu21_readHumidity(); + h_htu21 = htu21_compensatedHumidity(h_htu21, t_htu21); + dtostrf(t_htu21, 1, TEMP_RESOLUTION &3, itemp); + page += F("HTU Temperature: "); page += itemp; page += iconv; page += F(""); + dtostrf(h_htu21, 1, HUMIDITY_RESOLUTION &3, itemp); + page += F("HTU Humidity: "); page += itemp; page += F("%"); + } + return page; +} +#endif // USE_WEBSERVER +#endif // USE_HTU +#endif // USE_I2C +
Upload "); + if (_uploaderror) { + page += F("failed"); + if (_uploaderror == 1) { + page += F("

No file selected"); + } else if (_uploaderror == 2) { + page += F("

File size is larger than available free space"); + } else if (_uploaderror == 3) { + page += F("

File magic header does not start with 0xE9"); + } else if (_uploaderror == 4) { + page += F("

File flash size is larger than device flash size"); + } else if (_uploaderror == 5) { + page += F("

File upload buffer miscompare"); + } else if (_uploaderror == 6) { + page += F("

Upload failed. Enable logging option 3 for more information"); + } else if (_uploaderror == 7) { + page += F("

Upload aborted"); + } else { + page += F("

Upload error code "); + page += String(_uploaderror); + } + if (Update.hasError()) { + page += F("

Update error code (see Updater.cpp) "); + page += String(Update.getError()); + } + } else { + page += F("successful


Device will restart in a few seconds"); + restartflag = 2; + } + page += F("