Browse Source

Sync xoseperez/dev

alexa
Max Prokhorov 6 years ago
committed by GitHub
parent
commit
b93a3555c6
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
185 changed files with 26396 additions and 16655 deletions
  1. +4
    -4
      .github/stale.yml
  2. +26
    -3
      .travis.yml
  3. +115
    -0
      CHANGELOG.md
  4. +39
    -19
      README.md
  5. +125
    -49
      code/build.sh
  6. +1
    -1
      code/eagle.flash.1m0m1s.ld
  7. +3
    -3
      code/eagle.flash.1m0m2s.ld
  8. +20
    -0
      code/eagle.flash.2m1m4s.ld
  9. +1
    -1
      code/eagle.flash.4m1m4s.ld
  10. +1
    -1
      code/eagle.flash.4m3m4e.ld
  11. +1
    -1
      code/eagle.flash.512k0m1s.ld
  12. +73
    -32
      code/espurna/alexa.ino
  13. +86
    -57
      code/espurna/api.ino
  14. +9
    -3
      code/espurna/button.ino
  15. +3
    -6
      code/espurna/config/all.h
  16. +28
    -3
      code/espurna/config/arduino.h
  17. +138
    -2
      code/espurna/config/defaults.h
  18. +5
    -0
      code/espurna/config/dependencies.h
  19. +212
    -18
      code/espurna/config/general.h
  20. +573
    -20
      code/espurna/config/hardware.h
  21. +58
    -16
      code/espurna/config/progmem.h
  22. +128
    -60
      code/espurna/config/prototypes.h
  23. +196
    -45
      code/espurna/config/sensors.h
  24. +18
    -3
      code/espurna/config/types.h
  25. +1
    -2
      code/espurna/config/version.h
  26. +73
    -0
      code/espurna/config/webui.h
  27. BIN
      code/espurna/data/index.all.html.gz
  28. BIN
      code/espurna/data/index.light.html.gz
  29. BIN
      code/espurna/data/index.rfbridge.html.gz
  30. BIN
      code/espurna/data/index.rfm69.html.gz
  31. BIN
      code/espurna/data/index.sensor.html.gz
  32. BIN
      code/espurna/data/index.small.html.gz
  33. +18
    -2
      code/espurna/debug.ino
  34. +6
    -1
      code/espurna/domoticz.ino
  35. +53
    -1
      code/espurna/eeprom.ino
  36. +156
    -0
      code/espurna/encoder.ino
  37. +32
    -6
      code/espurna/espurna.ino
  38. +40
    -0
      code/espurna/filters/LastFilter.h
  39. +33
    -16
      code/espurna/homeassistant.ino
  40. +2
    -2
      code/espurna/i2c.ino
  41. +10
    -5
      code/espurna/influxdb.ino
  42. +364
    -69
      code/espurna/ir.ino
  43. +6
    -2
      code/espurna/led.ino
  44. +65
    -0
      code/espurna/libs/RFM69Wrap.h
  45. +101
    -81
      code/espurna/light.ino
  46. +195
    -0
      code/espurna/migrate.ino
  47. +15
    -7
      code/espurna/mqtt.ino
  48. +2
    -2
      code/espurna/nofuss.ino
  49. +2
    -2
      code/espurna/ntp.ino
  50. +10
    -7
      code/espurna/ota.ino
  51. +19
    -17
      code/espurna/pwm.c
  52. +145
    -38
      code/espurna/relay.ino
  53. +15
    -14
      code/espurna/rfbridge.ino
  54. +284
    -0
      code/espurna/rfm69.ino
  55. +2
    -2
      code/espurna/scheduler.ino
  56. +403
    -113
      code/espurna/sensor.ino
  57. +34
    -2
      code/espurna/sensors/AnalogSensor.h
  58. +6
    -6
      code/espurna/sensors/BaseSensor.h
  59. +9
    -5
      code/espurna/sensors/CSE7766Sensor.h
  60. +11
    -3
      code/espurna/sensors/DHTSensor.h
  61. +15
    -0
      code/espurna/sensors/ECH1560Sensor.h
  62. +8
    -3
      code/espurna/sensors/EmonSensor.h
  63. +29
    -12
      code/espurna/sensors/EventSensor.h
  64. +4
    -2
      code/espurna/sensors/HLW8012Sensor.h
  65. +189
    -0
      code/espurna/sensors/MICS2710Sensor.h
  66. +144
    -0
      code/espurna/sensors/MICS5525Sensor.h
  67. +125
    -0
      code/espurna/sensors/NTCSensor.h
  68. +39
    -11
      code/espurna/sensors/PMSX003Sensor.h
  69. +150
    -17
      code/espurna/sensors/PZEM004TSensor.h
  70. +173
    -0
      code/espurna/sensors/SDS011Sensor.h
  71. +0
    -2
      code/espurna/sensors/SI7021Sensor.h
  72. +39
    -32
      code/espurna/sensors/SonarSensor.h
  73. +21
    -6
      code/espurna/sensors/V9261FSensor.h
  74. +31
    -19
      code/espurna/settings.ino
  75. +1
    -1
      code/espurna/ssdp.ino
  76. +3118
    -3081
      code/espurna/static/index.all.html.gz.h
  77. +2973
    -2953
      code/espurna/static/index.light.html.gz.h
  78. +2578
    -2552
      code/espurna/static/index.rfbridge.html.gz.h
  79. +4057
    -0
      code/espurna/static/index.rfm69.html.gz.h
  80. +2634
    -2591
      code/espurna/static/index.sensor.html.gz.h
  81. +2535
    -2508
      code/espurna/static/index.small.html.gz.h
  82. +17
    -5
      code/espurna/system.ino
  83. +41
    -3
      code/espurna/telnet.ino
  84. +39
    -11
      code/espurna/thinkspeak.ino
  85. +117
    -47
      code/espurna/utils.ino
  86. +35
    -48
      code/espurna/web.ino
  87. +16
    -5
      code/espurna/wifi.ino
  88. +5
    -18
      code/espurna/ws.ino
  89. +4
    -5
      code/extra_scripts.py
  90. +79
    -83
      code/gulpfile.js
  91. +180
    -63
      code/html/custom.css
  92. +325
    -134
      code/html/custom.js
  93. +293
    -108
      code/html/index.html
  94. +460
    -0
      code/html/vendor/datatables-1.10.16.css
  95. +178
    -0
      code/html/vendor/datatables-1.10.16.min.js
  96. BIN
      code/html/vendor/images/sort_asc.png
  97. BIN
      code/html/vendor/images/sort_asc_disabled.png
  98. BIN
      code/html/vendor/images/sort_both.png
  99. BIN
      code/html/vendor/images/sort_desc.png
  100. BIN
      code/html/vendor/images/sort_desc_disabled.png

+ 4
- 4
.github/stale.yml View File

@ -1,9 +1,9 @@
# Number of days of inactivity before an Issue or Pull Request becomes stale
daysUntilStale: 120
daysUntilStale: 60
# Number of days of inactivity before a stale Issue or Pull Request is closed.
# Set to false to disable. If disabled, issues still need to be closed manually, but will remain marked as stale.
daysUntilClose: 30
daysUntilClose: 7
# Issues or Pull Requests with these labels will never be considered stale. Set to `[]` to disable
exemptLabels:
@ -23,7 +23,7 @@ staleLabel: stale
# Comment to post when marking as stale. Set to `false` to disable
markComment: >
This issue has been automatically marked as stale because it has not had
recent activity. It will be closed in 30 days if no further activity occurs.
recent activity. It will be closed in 7 days if no further activity occurs.
Thank you for your contributions.
# Comment to post when removing the stale label.
@ -32,7 +32,7 @@ markComment: >
# Comment to post when closing a stale Issue or Pull Request.
closeComment: >
This issue will be auto-closed because there hasn't been any activity for a few months. Feel free to open a new one if you still experience this problem.
This issue will be auto-closed because there hasn't been any activity for two months. Feel free to open a new one if you still experience this problem.
# Limit the number of actions per hour, from 1-30. Default is 30
limitPerRun: 30


+ 26
- 3
.travis.yml View File

@ -2,14 +2,36 @@ language: python
python:
- '2.7'
sudo: false
conditions: v1
cache:
directories:
- "~/.npm"
- "~/.platformio"
- "$TRAVIS_BUILD_DIR/code/.piolibdeps"
install:
- pip install -U platformio
- cd code ; npm install --only=dev ; cd ..
- npm install -g npm@latest
- cd code && npm ci && cd ..
env:
global:
- BUILDER_TOTAL_THREADS=4
script:
- cd code && ./build.sh && cd ..
- cd code && ./build.sh -p && cd ..
stages:
- name: Test
- name: Release
if: tag IS present AND tag =~ ^\d+\.\d+\.\d+$
jobs:
include:
- stage: Test
script: cd code && ./build.sh travis01
- script: cd code && ./build.sh travis02
- script: cd code && ./build.sh travis03
- stage: Release
env: BUILDER_THREAD=0
- env: BUILDER_THREAD=1
- env: BUILDER_THREAD=2
- env: BUILDER_THREAD=3
before_deploy:
- mv firmware/*/espurna-*.bin firmware/
deploy:
@ -20,8 +42,9 @@ deploy:
file: firmware/espurna-*.bin
skip_cleanup: true
on:
all_branches: true
tags: true
repo: xoseperez/espurna
condition: $TRAVIS_BUILD_STAGE_NAME = Release
notifications:
pushover:
api_key:


+ 115
- 0
CHANGELOG.md View File

@ -3,6 +3,121 @@
The format is based on [Keep a Changelog](http://keepachangelog.com/)
and this project adheres to [Semantic Versioning](http://semver.org/).
## [1.13.3] 2018-10-08
### Fixed
- Honour build time settings for MQTT on fresh install (#719)
- Fix custom_crash_callback declaration for Arduino IDE 1.8.6 (#1169)
- Fix eneUnits key in web UI (#1177)
- Fix HA names (#1183)
- API is now restful (issue a PUT to change a relay status). It can be disabled from web UI (#1192)
- Remove static array to prevent out of bound in relay.ino (#1217)
- Remove duplicate call to EEPROMr.begin (#1214)
- Fix issue when SPIFFS_SUPPORT is enabled (#1225)
- Fix quoting units_of_measurement in HA config output (#1227)
- Fix "Clear counts" on rfm69 does not reset node count properly (thanks to @Trickx, #1239)
- Fix homecube 3rd led setting (thanks to @mcspr)
- Fix typo in static IP hint text (@thanks to @zafrirron)
- Fix hostname/password length requirements (thanks to @mcspr and @djwmarcx)
- Do not quote numbers in MQTT JSON payloads
- Fix telnet client object deletion (thanks to @mcspr)
- Call wakeUp PMS on first reading cycle to avoid not data in a long period (thanks to @Yonsm)
- Small fixes and windows support for ESPurna OTA Manager (thanks to @mcspr)
- Fix for YiDian XS-SSA05 configs (thanks to @ducky64)
- Send MQTT messages only for button events with assigned actions (thanks to @Valcob)
- Avoid EEPROM commits on callbacks (#1214)
### Added
- Option to report energy based on delta since last report (#369)
- Support for IR-MQTT bridge, also in RAW mode (#556, #907)
- Allow faster sensor reading intervals, down to 1 second (#848)
- Support for Xiaomi Smart Desk Lamp (#884)
- Retry up to 3 times on bad response to Thingspeak server (#1213)
- Support for apparent power and power factor on CSE7/XX sensor (#1215)
- Support for encoders
- Support for Allterco Shelly2
- Added SDS011 sensor support (thanks to @derlucas)
- Added password check to telnet (option to disable it)
- Added PHYX support (thanks to @whitebird)
- Added config command that outputs the configuration in JSON
- Support for MICS-2710, MICS-5525 and MICS-4514, gas sensors
- Support for iWoole LED Table Lamp (thanks to @CollinShorts)
- Command to output free stack
- Password management from web UI (thanks to @mcspr)
- Added BESTEK MRJ1011 support (thanks to @InduPrakash)
- Support for EXS WiFi Relay 5.0 (thanks to @cheise, #1218)
- Allowing disabling or single heartbeat on MQTT connect or repeat (default) (#1196)
- Command to save settings when SETTINGS_AUTOSAVE is off
### Changed
- Upgraded to JustWifi 2.0.2
- Upgraded to FauxmoESP 3.0.1
- Upgraded to DebounceEvent 2.0.4 to properly support BUTTON_SWITCH
- Split `info` command output into `info` and `wifi`. Refactor output.
- Custom HA payloads (thanks to @Yonsm)
## [1.13.2] 2018-08-27
### Fixed
- Fix relay overflow window length
- Fix TravisCI release condition (thanks to @mcspr, [#1042](https://github.com/xoseperez/espurna/issues/1042))
- Fix Sonoff RFBridge build in Arduino IDE ([#1043](https://github.com/xoseperez/espurna/issues/1043))
- Using corrent path separator in gulpfile.js (thanks to @InduPrakash, [#1045](https://github.com/xoseperez/espurna/issues/1045))
- Fix KMC70011 LED logic (thanks to @zerog2k, [#1056](https://github.com/xoseperez/espurna/issues/1056))
- Fix Luani HVIO to use 1MB flash size and toggle switch (thanks to @BauerPh, [#1065](https://github.com/xoseperez/espurna/issues/1065) and [#1068](https://github.com/xoseperez/espurna/issues/1068))
- Fix switches in Microsoft Edge (thanks to @Valcob, [#1066](https://github.com/xoseperez/espurna/issues/1066))
- Fix build.sh error handling (thanks to @mcspr, [#1075](https://github.com/xoseperez/espurna/issues/1075))
- Correctly init Serial on RELAY_PROVIDER_STM ([#1130](https://github.com/xoseperez/espurna/issues/1130))
- Disconnect before running WPS and SmartConfig discovery ([#1146](https://github.com/xoseperez/espurna/issues/1146))
- Fix sort fields in OTA manager
### Added
- Support for YJZK 1Ch and 3CH switches (thanks to @CollinShorts and @q32103940, [#1047](https://github.com/xoseperez/espurna/issues/1047))
- Support for AG-L4 color desk lamp (thanks to @zerog2k, [#1050](https://github.com/xoseperez/espurna/issues/1050))
- Option to cofigure ON/OFF payload at build time ([#1085](https://github.com/xoseperez/espurna/issues/1085))
- Option to change default payload for HA ([#1085](https://github.com/xoseperez/espurna/issues/1085))
- Support for Allterco Shelly1 (thanks to @abmantis, [#1128](https://github.com/xoseperez/espurna/issues/1128))
- Support for HomeCube 16A (thanks to @hyteoo, [#1106](https://github.com/xoseperez/espurna/issues/1106))
- Support for multiple sonar sensors (thanks to @ruimarinho, [#1116](https://github.com/xoseperez/espurna/issues/1116))
- Support for hardware serial on PMSX003 device (thanks to @ruimarinho, [#1122](https://github.com/xoseperez/espurna/issues/1122))
- Support for Lohas 9W bulbs (thanks to @steveway, [#1135](https://github.com/xoseperez/espurna/issues/1135))
- Show literal for webUI image in info ([#1142](https://github.com/xoseperez/espurna/issues/1142))
- Add RFBRIDGE code to full webUI image ([#1157](https://github.com/xoseperez/espurna/issues/1157))
- Handle events in EventSensor
- Option to remove API_SUPPORT at build time
- Option to save total energy in EEPROM after X reports, disabled by default
- Support for DHT12 sensor (thanks to Altan Altay)
- Support for 2MB flash boards
### Changed
- Update PlatformIO support to 3.6.X branch
- Explicitly disable ATC on RFM69 gateway ([#938](https://github.com/xoseperez/espurna/issues/938))
- Reduce memory footprint of API calls ([#1133](https://github.com/xoseperez/espurna/issues/1133))
- Init relay GPIO when in inverse mode to be OFF ([#1078](https://github.com/xoseperez/espurna/issues/1078))
## [1.13.1] 2018-07-10
### Fixed
- Build issues with Arduino IDE ([#975](https://github.com/xoseperez/espurna/issues/975))
- Right web interface image for with RF Bridge
- Full web interface image if light and sensor together ([#981](https://github.com/xoseperez/espurna/issues/981))
- Some devices still not using DOUT flash mode
- Crash on loading malformed configuration file
- Mismatch between memory size and layout size for some boards (this might require reflashing)
- Wrong settings report after factory reset
- Memory leak in JustWifi library
- New buttons not rendering right in Safari ([#1028](https://github.com/xoseperez/espurna/issues/1028))
### Added
- Support for RFM69GW board (see http://tinkerman.cat/rfm69-wifi-gateway/)
- Support for Sonoff IFAN02
- Support for NTC sensors ([#1001](https://github.com/xoseperez/espurna/issues/1001))
- Support for single-pin latched relays ([#1039](https://github.com/xoseperez/espurna/issues/1039))
- Check binary flash mode in web upgrade
- Sampling to AnalogSensor
- Parallel builds in Travis (thanks to @lobradov)
### Changed
- Reworked platformio.ini, build.sh files (thanks to @gn0st1c and @mcspr)
## [1.13.0] 2018-06-22
### Fixed
- Fixed PZEM004T compilation issues, working when using hardware serial ([#837](https://github.com/xoseperez/espurna/issues/837))


+ 39
- 19
README.md View File

@ -3,11 +3,12 @@
ESPurna ("spark" in Catalan) is a custom firmware for ESP8285/ESP8266 based smart switches, lights and sensors.
It uses the Arduino Core for ESP8266 framework and a number of 3rd party libraries.
[![version](https://img.shields.io/badge/version-1.13.1a-brightgreen.svg)](CHANGELOG.md)
[![version](https://img.shields.io/badge/version-1.13.3-brightgreen.svg)](CHANGELOG.md)
[![branch](https://img.shields.io/badge/branch-dev-orange.svg)](https://github.com/xoseperez/espurna/tree/dev/)
[![travis](https://travis-ci.org/xoseperez/espurna.svg?branch=dev)](https://travis-ci.org/xoseperez/espurna)
[![codacy](https://img.shields.io/codacy/grade/c9496e25cf07434cba786b462cb15f49/dev.svg)](https://www.codacy.com/app/xoseperez/espurna/dashboard)
[![license](https://img.shields.io/github/license/xoseperez/espurna.svg)](LICENSE)
[![travis](https://travis-ci.org/xoseperez/espurna.svg?branch=dev)](https://travis-ci.org/xoseperez/espurna)
[![codacy](https://api.codacy.com/project/badge/Grade/c9496e25cf07434cba786b462cb15f49)](https://www.codacy.com/app/xoseperez/espurna/dashboard)
[![downloads](https://img.shields.io/github/downloads/xoseperez/espurna/total.svg)](https://github.com/xoseperez/espurna/releases)
<br />
[![donate](https://img.shields.io/badge/donate-PayPal-blue.svg)](https://www.paypal.com/cgi-bin/webscr?cmd=_donations&business=xose%2eperez%40gmail%2ecom&lc=US&no_note=0&currency_code=EUR&bn=PP%2dDonationsBF%3abtn_donate_LG%2egif%3aNonHostedGuest)
[![gitter](https://img.shields.io/gitter/room/tinkermant-cat/espurna.svg)](https://gitter.im/tinkerman-cat/espurna)
@ -15,8 +16,16 @@ It uses the Arduino Core for ESP8266 framework and a number of 3rd party librari
---
## Contributors
**Without your help this project would not be possible**. I (@xoseperez) simply can't spend all the time I wish on ESPurna but luckly I recieve a lot of contributions, bug fixes, enhancement suggestions,... from people all around the world. I would like to thank each and every one of you. The [contributors](https://github.com/xoseperez/espurna/graphs/contributors) page shows the ones that have done a PR in the past, but I also get contributions in the issues, by email or via the [gitter ESPurna channel](https://gitter.im/tinkerman-cat/espurna), those I also want to thank.
**Thank you all very much**.
## Notice
> Ladies and gentlemen in the embedded world, use [PlatformIO](https://platformio.org/). If I could offer you only one tip for the future, [PlatformIO](https://platformio.org/) would be it.
> Please use the [gitter ESPurna channel](https://gitter.im/tinkerman-cat/espurna) for support and questions, you have better chances to get fast answers from me or other ESPurna users. Open an issue here only if you feel there is a bug or you want to request an enhancement. Thank you.
## Features
@ -41,6 +50,7 @@ It uses the Arduino Core for ESP8266 framework and a number of 3rd party librari
* Support for **relay synchronization** (all equal, only one ON, one and only on ON)
* Support for **MQTT groups** to sync switches between devices
* Support for **delayed ON/OFF**
* Support for **latched relays**
* **MQTT** enabled
* **SSL/TLS support** (not on regular builds, see [#64](https://github.com/xoseperez/espurna/issues/64))
* Switch on/off and toggle relays, group topics (sync relays between different devices)
@ -67,9 +77,10 @@ It uses the Arduino Core for ESP8266 framework and a number of 3rd party librari
* Support for [direct control of the encoder/decoder bypassing the EFM8BB1](https://github.com/xoseperez/espurna/wiki/Hardware-Itead-Sonoff-RF-Bridge---Direct-Hack)
* Support for [different **sensors**](Sensors)
* Environment
* **DHT11 / DHT22 / DHT21 / AM2301 / Itead's SI7021**
* **DHT11 / DHT12 / DHT22 / DHT21 / AM2301 / Itead's SI7021**
* **BMP280** and **BME280** temperature, humidity (BME280) and pressure sensor by Bosch
* **TMP35** and **TMP36** analog temperature sensors
* **NTC** temperature sensors
* **SI7021** temperature and humidity sensor
* **SHT3X** temperature and humidity sensor over I2C (Wemos shield)
* **AM2320** temperature and humidity sensor over I2C
@ -80,6 +91,7 @@ It uses the Arduino Core for ESP8266 framework and a number of 3rd party librari
* **BH1750** luminosity sensor
* **GUVAS12SD** UV sensor
* **GEIGER COUNTER** by RH Electronics
* **HC-SR04**, **SRF05**, **SRF06**, **DYP-ME007**, **JSN-SR04T** & **Parallax PING)))™** distance sensors
* Power monitoring
* **HLW8012** using the [HLW8012 Library](https://bitbucket.org/xoseperez/hlw8012) (Sonoff POW)
* **CSE7766** and **CSE7759B** power monitor chips
@ -87,8 +99,8 @@ It uses the Arduino Core for ESP8266 framework and a number of 3rd party librari
* Non-invasive **current sensor** using **internal ADC** or **ADC712** or **ADC121** or **ADS1115**
* **V9261F** power monitor chip
* **PZEM0004T** power monitor board
* Raw analog and digital sensors
* Simple pulse counter
* Raw **analog** and **digital** sensors
* Simple **pulse counter** with **event triggering** option
* Support for (almost) any UART based sensor via the **UART-to-MQTT module**
* Support for different units (Fahrenheit or Celsius, Watts or Kilowatts, Joules or kWh)
* Support for LED lights
@ -206,36 +218,40 @@ Here is the list of supported hardware. For more information please refer to the
||||
|---|---|---|
|![Tinkerman Espurna H](images/devices/tinkerman-espurna-h.jpg)|||
|**Tinkerman ESPurna H**|||
|![Tinkerman Espurna H](images/devices/tinkerman-espurna-h.jpg)|![Tinkerman RFM69GW](images/devices/tinkerman-rfm69gw.jpg)||
|**Tinkerman ESPurna H**|**Tinkerman RFM69GW**||
|![Itead Sonoff RF Bridge](images/devices/itead-sonoff-rfbridge.jpg)|![Itead Sonoff RF](images/devices/itead-sonoff-rf.jpg)|![Itead Sonoff 4CH](images/devices/itead-sonoff-4ch.jpg)|
|**Itead Sonoff RF Bridge**|**Itead Sonoff RF**|**Itead Sonoff 4CH**|
|![Itead Sonoff 4CH Pro](images/devices/itead-sonoff-4ch-pro.jpg)|||
|**Itead Sonoff 4CH Pro**|||
|![Itead Sonoff S31](images/devices/itead-sonoff-s31.jpg)|![BlitzWolf BW-SPP2](images/devices/blitzwolf-bw-shp2.jpg)|![Power meters based on V9261F](images/devices/generic-v9261f.jpg)|
|**Itead Sonoff S31**|**Blitzwolf BW-SHP2<br />(also by HomeCube, Coosa, Goosund)**|**Power meters based on V9261F**|
|**Itead Sonoff S31**|**Blitzwolf BW-SHP2<br />(also by Coosa, Goosund, HomeCube, Teckin)**|**Power meters based on V9261F**|
|![Itead Sonoff POW](images/devices/itead-sonoff-pow.jpg)|![Itead Sonoff POW](images/devices/itead-sonoff-pow-r2.jpg)|![Vanzavanzu Smart WiFi Plug Mini](images/devices/vanzavanzu-smart-wifi-plug-mini.jpg)|
|**Itead Sonoff POW**|**Itead Sonoff POW R2**|**Vanzavanzu Smart WiFi Plug Mini**|
|![Itead Sonoff Basic](images/devices/itead-sonoff-basic.jpg)|![Itead Sonoff Dual/Dual R2](images/devices/itead-sonoff-dual.jpg)|![Itead Sonoff TH10/TH16](images/devices/itead-sonoff-th.jpg)|
|**Itead Sonoff Basic**|**Itead Sonoff Dual/Dual R2**|**Itead Sonoff TH10/TH16**|
|![Electrodragon WiFi IOT](images/devices/electrodragon-wifi-iot.jpg)|![OpenEnergyMonitor WiFi MQTT Relay / Thermostat](images/devices/openenergymonitor-mqtt-relay.jpg)||
|**Electrodragon WiFi IOT**|**OpenEnergyMonitor WiFi MQTT Relay / Thermostat**||
|![Itead S20](images/devices/itead-s20.jpg)|![WorkChoice EcoPlug](images/devices/workchoice-ecoplug.jpg)|![Neo Coolcam NAS WR01W](images/devices/neo-coolcam-wifi.jpg)|
|**Itead S20**|**WorkChoice EcoPlug**|**Neo Coolcam NAS WR01W**|
|![Itead S20](images/devices/itead-s20.jpg)|![Itead S20](images/devices/itead-s26.jpg)|![Neo Coolcam NAS WR01W](images/devices/neo-coolcam-wifi.jpg)|
|**Itead S20**|**Itead S26**|**Neo Coolcam NAS WR01W**|
|![Schuko Wifi Plug](images/devices/schuko-wifi-plug.jpg)|![KMC 70011](images/devices/kmc-70011.jpg)|![Xenon SM-PW702U](images/devices/xenon-sm-pw702u.jpg)|
|**Schuko Wifi Plug**|**KMC 70011**|**Xenon SM-PW702U**|
|![Maxcio W-US002S](images/devices/maxcio-w-us002s.jpg)|![HEYGO HY02](images/devices/heygo-hy02.jpg)|![YiDian XS-SSA05](images/devices/yidian-xs-ssa05.jpg)|
|**Maxcio W-US002S**|**HEYGO HY02**|**YiDian XS-SSA05**|
|![WiOn 50055](images/devices/wion-50055.jpg)|![LINGAN SWA1](images/devices/lingan-swa1.jpg)||
|**WiOn 50055**|**LINGAN SWA1**||
|![WiOn 50055](images/devices/wion-50055.jpg)|![LINGAN SWA1](images/devices/lingan-swa1.jpg)|![HomeCube 16A](images/devices/homecube-16a.jpg)|
|**WiOn 50055**|**LINGAN SWA1**|**HomeCube 16A**|
|![WorkChoice EcoPlug](images/devices/workchoice-ecoplug.jpg)|![Bestek MRJ1011](images/devices/bestek-mrj1011.jpg)|![Tonbux XS-SSA01](images/devices/tonbux-xs-ssa01.jpg)|
|**WorkChoice EcoPlug**|**Bestek MRJ1011**|**Tonbux XS-SSA01**|
|![Tonbux PowerStrip02](images/devices/tonbux-powerstrip02.jpg)|![ForNorm Power Strip](images/devices/fornorm-power-strip.jpg)|![Zhilde ZLD-EU55-W](images/devices/zhilde-zld-eu55-w.jpg)|
|**Tonbux PowerStrip02**|**Fornorm Power Strip**|**Zhilde ZLD-EU55-W**|
|![Itead Sonoff Touch](images/devices/itead-sonoff-touch.jpg)|![Itead Sonoff T1](images/devices/itead-sonoff-t1.jpg)|![YJZK 2-gang switch](images/devices/yjzk-2gang-switch.jpg)|
|**Itead Sonoff Touch**|**Itead Sonoff T1**|**YJZK 2-gang switch**|
|![Itead Sonoff Touch](images/devices/itead-sonoff-touch.jpg)|![Itead Sonoff T1](images/devices/itead-sonoff-t1.jpg)|![YJZK switch](images/devices/yjzk-2gang-switch.jpg)|
|**Itead Sonoff Touch**|**Itead Sonoff T1**|**YJZK 1/2/3-gangs switch**|
|![Itead Slampher](images/devices/itead-slampher.jpg)|![Arilux E27](images/devices/arilux-e27.jpg)|![Itead Sonoff B1](images/devices/itead-sonoff-b1.jpg)|
|**Itead Slampher**|**Arilux E27**|**Itead Sonoff B1**|
|![AI-Thinker Wifi Light / Noduino OpenLight](images/devices/aithinker-ai-light.jpg)|![Authometion LYT8266](images/devices/authometion-lyt8266.jpg)||
|**AI-Thinker Wifi Light / Noduino OpenLight**|**Authometion LYT8266**||
|![AI-Thinker Wifi Light / Noduino OpenLight](images/devices/aithinker-ai-light.jpg)|![Authometion LYT8266](images/devices/authometion-lyt8266.jpg)|![AG-L4](images/devices/ag-l4.jpg)|
|**AI-Thinker Wifi Light / Noduino OpenLight**|**Authometion LYT8266**|**AG-L4**|
|![Lohas 9W](images/devices/lohas-9w.jpg)|![Xiaomi Smart Desk Lamp](images/devices/xiaomi-smart-desk-lamp.jpg)|![iWoole LED Table Lamp](images/devices/iwoole-led-table-lamp.jpg)|
|**Lohas 9W**|**Xiaomi Smart Desk Lamp**|**iWoole LED Table Lamp**|
|![Itead Sonoff LED](images/devices/itead-sonoff-led.jpg)|![Itead BN-SZ01](images/devices/itead-bn-sz01.jpg)|![InterMitTech QuinLED 2.6](images/devices/intermittech-quinled-2.6.jpg)|
|**Itead Sonoff LED**|**Itead BN-SZ01**|**InterMitTech QuinLED 2.6**|
|![Arilux AL-LC01 (RGB)](images/devices/arilux-al-lc01.jpg)|![Arilux AL-LC02 (RGBW)](images/devices/arilux-al-lc02.jpg)|![Arilux AL-LC06 (RGBWWCW)](images/devices/arilux-al-lc06.jpg)|
@ -246,6 +262,8 @@ Here is the list of supported hardware. For more information please refer to the
|**Itead Sonoff SV**|**Itead 1CH Inching**|**Itead Motor Clockwise/Anticlockwise**|
|![Jan Goedeke Wifi Relay (NO/NC)](images/devices/jangoe-wifi-relay.jpg)|![Jorge García Wifi + Relays Board Kit](images/devices/jorgegarcia-wifi-relays.jpg)|![EXS Wifi Relay v3.1](images/devices/exs-wifi-relay-v31.jpg)|
|**Jan Goedeke Wifi Relay (NO/NC)**|**Jorge García Wifi + Relays Board Kit**|**EXS Wifi Relay v3.1**|
|![EXS Wifi Relay v5.0](images/devices/exs-wifi-relay-v50.jpg)|![Allterco Shelly1](images/devices/allterco-shelly1.jpg)|![Allterco Shelly2](images/devices/allterco-shelly2.jpg)|
|**EXS Wifi Relay v5.0**|**Alterco Shelly1**|**Alterco Shelly2**|
|![ManCaveMade ESP-Live](images/devices/mancavemade-esp-live.jpg)|![Wemos D1 Mini Relay Shield](images/devices/wemos-d1-mini-relayshield.jpg)|![Witty Cloud](images/devices/witty-cloud.jpg)|
|**ManCaveMade ESP-Live**|**Wemos D1 Mini Relay Shield**|**Witty Cloud**|
|![IKE ESPike](images/devices/ike-espike.jpg)|![Pilotak ESP DIN](images/devices/pilotak-esp-din.jpg)|![Arniex Swifitch](images/devices/arniex-swifitch.jpg)|
@ -256,8 +274,10 @@ Here is the list of supported hardware. For more information please refer to the
|**Generic DHT11 v1.0**|**Generic DS18B20 v1.0**|**Bruno Horta's OnOfre**|
|![Allnet ESP8266-UP-Relay](images/devices/allnet-esp8266-up-relay.jpg)|![RH Electronics Geiger Counter](images/devices/generic-geiger-diy.png)|![Luani HVIO](images/devices/luani-hvio.jpg)|
|**Allnet ESP8266-UP-Relay**|**RH Electronics Geiger Counter**|**Luani HVIO**|
|![Tonbux Mosquito Killer](images/devices/tonbux-mosquito-killer.jpg)|||
|**Tonbux Mosquito Killer**||||
|![Phyx ESP12 RGBW](images/devices/phyx-esp12-rgbw.jpg)|||
|**Phyx ESP12 RGBW**||||
|![Tonbux Mosquito Killer](images/devices/tonbux-mosquito-killer.jpg)|![Itead Sonoff IFAN02](images/devices/itead-sonoff-ifan02.jpg)||
|**Tonbux Mosquito Killer**|**Itead Sonoff IFAN02**|||
**Other supported boards (beta):**
KMC 4 Outlet, Gosund WS1, Smart Dual Plug, MakerFocus Intelligent Module LM33 for Lamps


+ 125
- 49
code/build.sh View File

@ -1,73 +1,149 @@
#!/bin/bash
set -e
# Welcome
echo "--------------------------------------------------------------"
echo "ESPURNA FIRMWARE BUILDER"
# Utility
is_git() {
command -v git >/dev/null 2>&1 || return 1
command git rev-parse >/dev/null 2>&1 || return 1
return 0
}
# Script settings
destination=../firmware
version=$(grep APP_VERSION espurna/config/version.h | awk '{print $3}' | sed 's/"//g')
if is_git; then
git_revision=$(git rev-parse --short HEAD)
git_version=${version}-${git_revision}
else
git_revision=
git_version=$version
fi
par_build=false
par_thread=${BUILDER_THREAD:-0}
par_total_threads=${BUILDER_TOTAL_THREADS:-4}
if [ ${par_thread} -ne ${par_thread} -o \
${par_total_threads} -ne ${par_total_threads} ]; then
echo "Parallel threads should be a number."
exit
fi
if [ ${par_thread} -ge ${par_total_threads} ]; then
echo "Current thread is greater than total threads. Doesn't make sense"
exit
fi
# Available environments
travis=$(grep env: platformio.ini | grep travis | sed 's/\[env://' | sed 's/\]/ /' | sort)
available=$(grep env: platformio.ini | grep -v ota | grep -v ssl | grep -v travis | sed 's/\[env://' | sed 's/\]/ /' | sort)
list_envs() {
grep env: platformio.ini | sed 's/\[env:\(.*\)\]/\1/g'
}
# Parameters
environments=$@
if [ "$environments" == "list" ]; then
travis=$(list_envs | grep travis | sort)
available=$(list_envs | grep -Ev -- '-ota$|-ssl$|^travis' | sort)
# Build tools settings
export PLATFORMIO_BUILD_FLAGS="${PLATFORMIO_BUILD_FLAGS} -DAPP_REVISION='\"$git_revision\"'"
# Functions
print_available() {
echo "--------------------------------------------------------------"
echo "Available environments:"
for environment in $available; do
echo "* $environment"
done
exit
fi
}
# Environments to build
if [ $# -eq 0 ]; then
environments=$available
print_environments() {
echo "--------------------------------------------------------------"
echo "Current environments:"
for environment in $environments; do
echo "* $environment"
done
}
# Hook to build travis test envs
if [[ "${TRAVIS_BRANCH}" != "" ]]; then
re='^[0-9]+\.[0-9]+\.[0-9]+$'
if ! [[ ${TRAVIS_BRANCH} =~ $re ]]; then
environments=$travis
fi
set_default_environments() {
# Hook to build in parallel when using travis
if [[ "${TRAVIS_BUILD_STAGE_NAME}" = "Release" ]] && ${par_build}; then
environments=$(echo ${available} | \
awk -v par_thread=${par_thread} -v par_total_threads=${par_total_threads} \
'{ for (i = 1; i <= NF; i++) if (++j % par_total_threads == par_thread ) print $i; }')
return
fi
fi
# Only build travisN
if [[ "${TRAVIS_BUILD_STAGE_NAME}" = "Test" ]]; then
environments=$travis
return
fi
# Get current version
version=$(grep APP_VERSION espurna/config/version.h | awk '{print $3}' | sed 's/"//g')
echo "Building for version $version"
# Fallback to all available environments
environments=$available
}
# Create output folder
mkdir -p firmware
build_webui() {
# Build system uses gulpscript.js to build web interface
if [ ! -e node_modules/gulp/bin/gulp.js ]; then
echo "--------------------------------------------------------------"
echo "Installing dependencies..."
npm install --only=dev
fi
if [ ! -e node_modules/gulp/bin/gulp.js ]; then
# Recreate web interface (espurna/data/index.html.*.gz.h)
echo "--------------------------------------------------------------"
echo "Installing dependencies..."
npm install --only=dev
fi
echo "Building web interface..."
node node_modules/gulp/bin/gulp.js || exit
}
echo "--------------------------------------------------------------"
echo "Get revision..."
revision=$(git rev-parse HEAD)
revision=${revision:0:7}
cp espurna/config/version.h espurna/config/version.h.original
sed -i -e "s/APP_REVISION \".*\"/APP_REVISION \"$revision\"/g" espurna/config/version.h
build_environments() {
echo "--------------------------------------------------------------"
echo "Building firmware images..."
mkdir -p ../firmware/espurna-$version
# Recreate web interface
echo "--------------------------------------------------------------"
echo "Building web interface..."
node node_modules/gulp/bin/gulp.js || exit
for environment in $environments; do
echo -n "* espurna-$version-$environment.bin --- "
platformio run --silent --environment $environment || exit 1
stat -c %s .pioenvs/$environment/firmware.bin
[[ "${TRAVIS_BUILD_STAGE_NAME}" = "Test" ]] || \
mv .pioenvs/$environment/firmware.bin $destination/espurna-$version/espurna-$version-$environment.bin
done
echo "--------------------------------------------------------------"
}
# Build all the required firmware images
echo "--------------------------------------------------------------"
echo "Building firmware images..."
mkdir -p ../firmware/espurna-$version
for environment in $environments; do
echo "* espurna-$version-$environment.bin"
platformio run --silent --environment $environment || exit 1
mv .pioenvs/$environment/firmware.bin ../firmware/espurna-$version/espurna-$version-$environment.bin
# Parameters
while getopts "lpd:" opt; do
case $opt in
l)
print_available
exit
;;
p)
par_build=true
;;
d)
destination=$OPTARG
;;
esac
done
shift $((OPTIND-1))
# Welcome
echo "--------------------------------------------------------------"
echo "ESPURNA FIRMWARE BUILDER"
echo "Building for version ${git_version}"
# Environments to build
environments=$@
if [ $# -eq 0 ]; then
set_default_environments
fi
if ${CI:-false}; then
print_environments
fi
mv espurna/config/version.h.original espurna/config/version.h
build_webui
build_environments

+ 1
- 1
code/eagle.flash.1m0m1s.ld View File

@ -1,4 +1,4 @@
/* Flash Split for 1M chips */
/* Flash Split for 1M chips, no SPIFFS, 1 sector for EEPROM */
/* sketch 999KB */
/* eeprom 4KB */
/* reserved 16KB */


+ 3
- 3
code/eagle.flash.1m0m2s.ld View File

@ -1,4 +1,4 @@
/* Flash Split for 1M chips, no SPIFFS */
/* Flash Split for 1M chips, no SPIFFS, 2 sectors for EEPROM */
/* sketch 995KB */
/* eeprom 8KB */
/* reserved 16KB */
@ -13,7 +13,7 @@ MEMORY
PROVIDE ( _SPIFFS_start = 0x402FA000 );
PROVIDE ( _SPIFFS_end = 0x402FA000 );
PROVIDE ( _SPIFFS_page = 0 );
PROVIDE ( _SPIFFS_block = 0 );
PROVIDE ( _SPIFFS_page = 0x0 );
PROVIDE ( _SPIFFS_block = 0x0 );
INCLUDE "../ld/eagle.app.v6.common.ld"

+ 20
- 0
code/eagle.flash.2m1m4s.ld View File

@ -0,0 +1,20 @@
/* Flash Split for 2M chips, ~1M SPIFFS, 4 sectors for EEPROM */
/* sketch 1019KB */
/* spiffs 992KB */
/* eeprom 16KB */
/* reserved 16KB */
MEMORY
{
dport0_0_seg : org = 0x3FF00000, len = 0x10
dram0_0_seg : org = 0x3FFE8000, len = 0x14000
iram1_0_seg : org = 0x40100000, len = 0x8000
irom0_0_seg : org = 0x40201010, len = 0xfeff0
}
PROVIDE ( _SPIFFS_start = 0x40300000 );
PROVIDE ( _SPIFFS_end = 0x403F8000 );
PROVIDE ( _SPIFFS_page = 0x100 );
PROVIDE ( _SPIFFS_block = 0x2000 );
INCLUDE "../ld/eagle.app.v6.common.ld"

+ 1
- 1
code/eagle.flash.4m1m4s.ld View File

@ -1,4 +1,4 @@
/* Flash Split for 4M chips */
/* Flash Split for 4M chips, ~1M for SPIFFS, 4 sectors for EEPROM */
/* sketch 1019KB */
/* empty/ota? 2048KB */
/* spiffs 992KB */


+ 1
- 1
code/eagle.flash.4m3m4e.ld View File

@ -1,4 +1,4 @@
/* Flash Split for 4M chips */
/* Flash Split for 4M chips, ~3M for SPIFFS, 4 sectors for EEPROM */
/* sketch 1019KB */
/* spiffs 3040KB */
/* eeprom 16KB */


+ 1
- 1
code/eagle.flash.512k0m1s.ld View File

@ -1,4 +1,4 @@
/* Flash Split for 512K chips */
/* Flash Split for 512K chips, no SPIFFS, 1 sector for EEPROM */
/* sketch 487KB */
/* eeprom 4KB */
/* reserved 16KB */


+ 73
- 32
code/espurna/alexa.ino View File

@ -11,13 +11,13 @@ Copyright (C) 2016-2018 by Xose Pérez <xose dot perez at gmail dot com>
#include <fauxmoESP.h>
fauxmoESP alexa;
struct AlexaDevChange {
AlexaDevChange(unsigned char device_id, bool state) : device_id(device_id), state(state) {};
unsigned char device_id = 0;
bool state = false;
};
#include <queue>
static std::queue<AlexaDevChange> _alexa_dev_changes;
typedef struct {
unsigned char device_id;
bool state;
unsigned char value;
} alexa_queue_element_t;
static std::queue<alexa_queue_element_t> _alexa_queue;
// -----------------------------------------------------------------------------
// ALEXA
@ -29,15 +29,19 @@ bool _alexaWebSocketOnReceive(const char * key, JsonVariant& value) {
void _alexaWebSocketOnSend(JsonObject& root) {
root["alexaVisible"] = 1;
root["alexaEnabled"] = getSetting("alexaEnabled", ALEXA_ENABLED).toInt() == 1;
root["alexaEnabled"] = alexaEnabled();
}
void _alexaConfigure() {
alexa.enable(getSetting("alexaEnabled", ALEXA_ENABLED).toInt() == 1);
alexa.enable(wifiConnected() && alexaEnabled());
}
// -----------------------------------------------------------------------------
bool alexaEnabled() {
return (getSetting("alexaEnabled", ALEXA_ENABLED).toInt() == 1);
}
void alexaSetup() {
// Backwards compatibility
@ -46,36 +50,60 @@ void alexaSetup() {
// Load & cache settings
_alexaConfigure();
#if WEB_SUPPORT
// Uses hostname as base name for all devices
// TODO: use custom switch name when available
String hostname = getSetting("hostname");
// Lights
#if RELAY_PROVIDER == RELAY_PROVIDER_LIGHT
// Global switch
alexa.addDevice(hostname.c_str());
// For each channel
for (unsigned char i = 1; i <= lightChannels(); i++) {
alexa.addDevice((hostname + " " + i).c_str());
}
// Relays
#else
unsigned int relays = relayCount();
if (relays == 1) {
alexa.addDevice(hostname.c_str());
} else {
for (unsigned int i=1; i<=relays; i++) {
alexa.addDevice((hostname + " " + i).c_str());
}
}
// Websockets
#endif
// Websockets
#if WEB_SUPPORT
wsOnSendRegister(_alexaWebSocketOnSend);
wsOnAfterParseRegister(_alexaConfigure);
wsOnReceiveRegister(_alexaWebSocketOnReceive);
#endif
unsigned int relays = relayCount();
String hostname = getSetting("hostname");
if (relays == 1) {
alexa.addDevice(hostname.c_str());
} else {
for (unsigned int i=0; i<relays; i++) {
alexa.addDevice((hostname + "_" + i).c_str());
// Register wifi callback
wifiRegister([](justwifi_messages_t code, char * parameter) {
if ((MESSAGE_CONNECTED == code) || (MESSAGE_DISCONNECTED == code)) {
_alexaConfigure();
}
}
alexa.onSetState([&](unsigned char device_id, const char * name, bool state) {
AlexaDevChange change(device_id, state);
_alexa_dev_changes.push(change);
});
alexa.onGetState([](unsigned char device_id, const char * name) {
return relayStatus(device_id);
// Callback
alexa.onSetState([&](unsigned char device_id, const char * name, bool state, unsigned char value) {
alexa_queue_element_t element;
element.device_id = device_id;
element.state = state;
element.value = value;
_alexa_queue.push(element);
});
// Register loop
// Register main callbacks
espurnaRegisterLoop(alexaLoop);
espurnaRegisterReload(_alexaConfigure);
}
@ -83,11 +111,24 @@ void alexaLoop() {
alexa.handle();
while (!_alexa_dev_changes.empty()) {
AlexaDevChange& change = _alexa_dev_changes.front();
DEBUG_MSG_P(PSTR("[ALEXA] Device #%u state: %s\n"), change.device_id, change.state ? "ON" : "OFF");
relayStatus(change.device_id, change.state);
_alexa_dev_changes.pop();
while (!_alexa_queue.empty()) {
alexa_queue_element_t element = _alexa_queue.front();
DEBUG_MSG_P(PSTR("[ALEXA] Device #%u state: %s value: %d\n"), element.device_id, element.state ? "ON" : "OFF", element.value);
#if RELAY_PROVIDER == RELAY_PROVIDER_LIGHT
if (0 == element.device_id) {
relayStatus(0, element.state);
} else {
lightState(element.device_id - 1, element.state);
lightChannel(element.device_id - 1, element.value);
lightUpdate(true, true);
}
#else
relayStatus(element.device_id, element.state);
#endif
_alexa_queue.pop();
}
}


+ 86
- 57
code/espurna/api.ino View File

@ -6,7 +6,7 @@ Copyright (C) 2016-2018 by Xose Pérez <xose dot perez at gmail dot com>
*/
#if WEB_SUPPORT
#if API_SUPPORT
#include <ESPAsyncTCP.h>
#include <ESPAsyncWebServer.h>
@ -19,6 +19,7 @@ typedef struct {
api_put_callback_f putFn = NULL;
} web_api_t;
std::vector<web_api_t> _apis;
bool _api_restful = API_RESTFUL;
// -----------------------------------------------------------------------------
@ -27,11 +28,18 @@ bool _apiWebSocketOnReceive(const char * key, JsonVariant& value) {
}
void _apiWebSocketOnSend(JsonObject& root) {
root["apiVisible"] = 1;
root["apiEnabled"] = getSetting("apiEnabled", API_ENABLED).toInt() == 1;
root["apiKey"] = getSetting("apiKey");
root["apiRealTime"] = getSetting("apiRealTime", API_REAL_TIME_VALUES).toInt() == 1;
root["apiRestFul"] = _api_restful;
}
void _apiConfigure() {
_api_restful = getSetting("apiRestFul", API_RESTFUL).toInt() == 1;
}
// -----------------------------------------------------------------------------
// API
// -----------------------------------------------------------------------------
@ -70,52 +78,6 @@ bool _asJson(AsyncWebServerRequest *request) {
return asJson;
}
ArRequestHandlerFunction _bindAPI(unsigned int apiID) {
return [apiID](AsyncWebServerRequest *request) {
webLog(request);
if (!_authAPI(request)) return;
web_api_t api = _apis[apiID];
// Check if its a PUT
if (api.putFn != NULL) {
if (request->hasParam("value", request->method() == HTTP_PUT)) {
AsyncWebParameter* p = request->getParam("value", request->method() == HTTP_PUT);
(api.putFn)((p->value()).c_str());
}
}
// Get response from callback
char value[API_BUFFER_SIZE] = {0};
(api.getFn)(value, API_BUFFER_SIZE);
// The response will be a 404 NOT FOUND if the resource is not available
if (0 == value[0]) {
DEBUG_MSG_P(PSTR("[API] Sending 404 response\n"));
request->send(404);
return;
}
DEBUG_MSG_P(PSTR("[API] Sending response '%s'\n"), value);
// Format response according to the Accept header
if (_asJson(request)) {
char buffer[64];
if (isNumber(value)) {
snprintf_P(buffer, sizeof(buffer), PSTR("{ \"%s\": %s }"), api.key, value);
} else {
snprintf_P(buffer, sizeof(buffer), PSTR("{ \"%s\": \"%s\" }"), api.key, value);
}
request->send(200, "application/json", buffer);
} else {
request->send(200, "text/plain", value);
}
};
}
void _onAPIs(AsyncWebServerRequest *request) {
webLog(request);
@ -172,31 +134,98 @@ void _onRPC(AsyncWebServerRequest *request) {
}
bool _apiRequestCallback(AsyncWebServerRequest *request) {
String url = request->url();
// Main API entry point
if (url.equals("/api") || url.equals("/apis")) {
_onAPIs(request);
return true;
}
// Main RPC entry point
if (url.equals("/rpc")) {
_onRPC(request);
return true;
}
// Not API request
if (!url.startsWith("/api/")) return false;
for (unsigned char i=0; i < _apis.size(); i++) {
// Search API url
web_api_t api = _apis[i];
if (!url.endsWith(api.key)) continue;
// Log and check credentials
webLog(request);
if (!_authAPI(request)) return false;
// Check if its a PUT
if (api.putFn != NULL) {
if (!_api_restful || (request->method() == HTTP_PUT)) {
if (request->hasParam("value", request->method() == HTTP_PUT)) {
AsyncWebParameter* p = request->getParam("value", request->method() == HTTP_PUT);
(api.putFn)((p->value()).c_str());
}
}
}
// Get response from callback
char value[API_BUFFER_SIZE] = {0};
(api.getFn)(value, API_BUFFER_SIZE);
// The response will be a 404 NOT FOUND if the resource is not available
if (0 == value[0]) {
DEBUG_MSG_P(PSTR("[API] Sending 404 response\n"));
request->send(404);
return false;
}
DEBUG_MSG_P(PSTR("[API] Sending response '%s'\n"), value);
// Format response according to the Accept header
if (_asJson(request)) {
char buffer[64];
if (isNumber(value)) {
snprintf_P(buffer, sizeof(buffer), PSTR("{ \"%s\": %s }"), api.key, value);
} else {
snprintf_P(buffer, sizeof(buffer), PSTR("{ \"%s\": \"%s\" }"), api.key, value);
}
request->send(200, "application/json", buffer);
} else {
request->send(200, "text/plain", value);
}
return true;
}
return false;
}
// -----------------------------------------------------------------------------
void apiRegister(const char * key, api_get_callback_f getFn, api_put_callback_f putFn) {
// Store it
web_api_t api;
char buffer[40];
snprintf_P(buffer, sizeof(buffer), PSTR("/api/%s"), key);
api.key = strdup(key);
api.getFn = getFn;
api.putFn = putFn;
_apis.push_back(api);
// Bind call
unsigned int methods = HTTP_GET;
if (putFn != NULL) methods += HTTP_PUT;
webServer()->on(buffer, methods, _bindAPI(_apis.size() - 1));
}
void apiSetup() {
webServer()->on("/apis", HTTP_GET, _onAPIs);
webServer()->on("/rpc", HTTP_GET, _onRPC);
_apiConfigure();
wsOnSendRegister(_apiWebSocketOnSend);
wsOnReceiveRegister(_apiWebSocketOnReceive);
webRequestRegister(_apiRequestCallback);
espurnaRegisterReload(_apiConfigure);
}
#endif // WEB_SUPPORT
#endif // API_SUPPORT

+ 9
- 3
code/espurna/button.ino View File

@ -10,6 +10,8 @@ Copyright (C) 2016-2018 by Xose Pérez <xose dot perez at gmail dot com>
// BUTTON
// -----------------------------------------------------------------------------
#if BUTTON_SUPPORT
#include <DebounceEvent.h>
#include <vector>
@ -95,12 +97,14 @@ void buttonEvent(unsigned int id, unsigned char event) {
DEBUG_MSG_P(PSTR("[BUTTON] Button #%u event %u\n"), id, event);
if (event == 0) return;
unsigned char action = buttonAction(id, event);
#if MQTT_SUPPORT
buttonMQTT(id, event);
if (action != BUTTON_MODE_NONE || BUTTON_MQTT_SEND_ALL_EVENTS) {
buttonMQTT(id, event);
}
#endif
unsigned char action = buttonAction(id, event);
if (action == BUTTON_MODE_TOGGLE) {
if (_buttons[id].relayID > 0) {
relayToggle(_buttons[id].relayID - 1);
@ -266,3 +270,5 @@ void buttonLoop() {
#endif
}
#endif // BUTTON_SUPPORT

+ 3
- 6
code/espurna/config/all.h View File

@ -29,12 +29,9 @@
#include "hardware.h"
#include "defaults.h"
#include "general.h"
#include "dependencies.h"
#include "debug.h"
#include "prototypes.h"
#include "sensors.h"
#include "dependencies.h"
#include "webui.h"
#include "progmem.h"
#include "debug.h"
#ifdef USE_CORE_VERSION_H
#include "core_version.h"
#endif

+ 28
- 3
code/espurna/config/arduino.h View File

@ -8,9 +8,11 @@
//--------------------------------------------------------------------------------
//#define NODEMCU_LOLIN
//#define WEMOS_D1_MINI
//#define WEMOS_D1_MINI_RELAYSHIELD
//#define TINKERMAN_ESPURNA_H06
//#define TINKERMAN_ESPURNA_H08
//#define TINKERMAN_RFM69GW
//#define ITEAD_SONOFF_BASIC
//#define ITEAD_SONOFF_RF
//#define ITEAD_SONOFF_TH
@ -70,6 +72,7 @@
//#define MAXCIO_WUS002S
//#define YIDIAN_XSSSA05
//#define TONBUX_XSSSA06
//#define TONBUX_XSSSA01
//#define GREEN_ESP8266RELAY
//#define IKE_ESPIKE
//#define ARNIEX_SWIFITCH
@ -87,31 +90,48 @@
//#define PILOTAK_ESP_DIN_V1
//#define BLITZWOLF_BWSHP2
//#define BH_ONOFRE
//#define ITEAD_SONOFF_IFAN02
//#define GENERIC_AG_L4
//#define ALLTERCO_SHELLY1
//#define LOHAS_9W
//#define YJZK_SWITCH_1CH
//#define YJZK_SWITCH_3CH
//#define XIAOMI_SMART_DESK_LAMP
//#define ALLTERCO_SHELLY2
//#define PHYX_ESP12_RGB
//#define IWOOLE_LED_TABLE_LAMP
//#define EXS_WIFI_RELAY_V50
//--------------------------------------------------------------------------------
// Features (values below are non-default values)
//--------------------------------------------------------------------------------
//#define ALEXA_SUPPORT 0
//#define API_SUPPORT 0
//#define BROKER_SUPPORT 0
//#define BUTTON_SUPPORT 0
//#define DEBUG_SERIAL_SUPPORT 0
//#define DEBUG_TELNET_SUPPORT 0
//#define DEBUG_UDP_SUPPORT 1
//#define DEBUG_WEB_SUPPORT 0
//#define DOMOTICZ_SUPPORT 0
//#define ENCODER_SUPPORT 1
//#define HOMEASSISTANT_SUPPORT 0
//#define I2C_SUPPORT 1
//#define INFLUXDB_SUPPORT 1
//#define IR_SUPPORT 1
//#define LED_SUPPORT 0
//#define LLMNR_SUPPORT 1 // Only with Arduino Core 2.4.0
//#define MDNS_SERVER_SUPPORT 0
//#define MDNS_CLIENT_SUPPORT 1
//#define MDNS_SERVER_SUPPORT 0
//#define MQTT_SUPPORT 0
//#define NETBIOS_SUPPORT 1 // Only with Arduino Core 2.4.0
//#define NOFUSS_SUPPORT 1
//#define NTP_SUPPORT 0
//#define RFM69_SUPPORT 1
//#define RF_SUPPORT 1
//#define SCHEDULER_SUPPORT 0
//#define SENSOR_SUPPORT 1
//#define SPIFFS_SUPPORT 1
//#define SSDP_SUPPORT 1
//#define TELNET_SUPPORT 0
@ -137,14 +157,19 @@
//#define EMON_ADS1X15_SUPPORT 1
//#define EMON_ANALOG_SUPPORT 1
//#define EVENTS_SUPPORT 1
//#define GEIGER_SUPPORT 1
//#define GUVAS12SD_SUPPORT 1
//#define HCSR04_SUPPORT 1
//#define HLW8012_SUPPORT 1
//#define MHZ19_SUPPORT 1
//#define MICS2710_SUPPORT 1
//#define MICS5525_SUPPORT 1
//#define NTC_SUPPORT 1
//#define PMSX003_SUPPORT 1
//#define PZEM004T_SUPPORT 1
//#define SDS011_SUPPORT 1
//#define SENSEAIR_SUPPORT 1
//#define SHT3X_I2C_SUPPORT 1
//#define SI7021_SUPPORT 1
//#define SONAR_SUPPORT 1
//#define TMP3X_SUPPORT 1
//#define V9261F_SUPPORT 1
//#define GEIGER_SUPPORT 1

+ 138
- 2
code/espurna/config/defaults.h View File

@ -208,6 +208,138 @@
#define BUTTON8_RELAY 0
#endif
// -----------------------------------------------------------------------------
// Encoders
// -----------------------------------------------------------------------------
#ifndef ENCODER1_PIN1
#define ENCODER1_PIN1 GPIO_NONE
#endif
#ifndef ENCODER2_PIN1
#define ENCODER2_PIN1 GPIO_NONE
#endif
#ifndef ENCODER3_PIN1
#define ENCODER3_PIN1 GPIO_NONE
#endif
#ifndef ENCODER4_PIN1
#define ENCODER4_PIN1 GPIO_NONE
#endif
#ifndef ENCODER5_PIN1
#define ENCODER5_PIN1 GPIO_NONE
#endif
#ifndef ENCODER1_PIN2
#define ENCODER1_PIN2 GPIO_NONE
#endif
#ifndef ENCODER2_PIN2
#define ENCODER2_PIN2 GPIO_NONE
#endif
#ifndef ENCODER3_PIN2
#define ENCODER3_PIN2 GPIO_NONE
#endif
#ifndef ENCODER4_PIN2
#define ENCODER4_PIN2 GPIO_NONE
#endif
#ifndef ENCODER5_PIN2
#define ENCODER5_PIN2 GPIO_NONE
#endif
#ifndef ENCODER1_BUTTON_PIN
#define ENCODER1_BUTTON_PIN GPIO_NONE
#endif
#ifndef ENCODER2_BUTTON_PIN
#define ENCODER2_BUTTON_PIN GPIO_NONE
#endif
#ifndef ENCODER3_BUTTON_PIN
#define ENCODER3_BUTTON_PIN GPIO_NONE
#endif
#ifndef ENCODER4_BUTTON_PIN
#define ENCODER4_BUTTON_PIN GPIO_NONE
#endif
#ifndef ENCODER5_BUTTON_PIN
#define ENCODER5_BUTTON_PIN GPIO_NONE
#endif
#ifndef ENCODER1_BUTTON_LOGIC
#define ENCODER1_BUTTON_LOGIC HIGH
#endif
#ifndef ENCODER2_BUTTON_LOGIC
#define ENCODER2_BUTTON_LOGIC HIGH
#endif
#ifndef ENCODER3_BUTTON_LOGIC
#define ENCODER3_BUTTON_LOGIC HIGH
#endif
#ifndef ENCODER4_BUTTON_LOGIC
#define ENCODER4_BUTTON_LOGIC HIGH
#endif
#ifndef ENCODER5_BUTTON_LOGIC
#define ENCODER5_BUTTON_LOGIC HIGH
#endif
#ifndef ENCODER1_BUTTON_MODE
#define ENCODER1_BUTTON_MODE INPUT_PULLUP
#endif
#ifndef ENCODER2_BUTTON_MODE
#define ENCODER2_BUTTON_MODE INPUT_PULLUP
#endif
#ifndef ENCODER3_BUTTON_MODE
#define ENCODER3_BUTTON_MODE INPUT_PULLUP
#endif
#ifndef ENCODER4_BUTTON_MODE
#define ENCODER4_BUTTON_MODE INPUT_PULLUP
#endif
#ifndef ENCODER5_BUTTON_MODE
#define ENCODER5_BUTTON_MODE INPUT_PULLUP
#endif
#ifndef ENCODER1_MODE
#define ENCODER1_MODE 1
#endif
#ifndef ENCODER2_MODE
#define ENCODER2_MODE 1
#endif
#ifndef ENCODER3_MODE
#define ENCODER3_MODE 1
#endif
#ifndef ENCODER4_MODE
#define ENCODER4_MODE 1
#endif
#ifndef ENCODER5_MODE
#define ENCODER5_MODE 1
#endif
#ifndef ENCODER1_CHANNEL1
#define ENCODER1_CHANNEL1 0
#endif
#ifndef ENCODER2_CHANNEL1
#define ENCODER2_CHANNEL1 0
#endif
#ifndef ENCODER3_CHANNEL1
#define ENCODER3_CHANNEL1 0
#endif
#ifndef ENCODER4_CHANNEL1
#define ENCODER4_CHANNEL1 0
#endif
#ifndef ENCODER5_CHANNEL1
#define ENCODER5_CHANNEL1 0
#endif
#ifndef ENCODER1_CHANNEL2
#define ENCODER1_CHANNEL2 1
#endif
#ifndef ENCODER2_CHANNEL2
#define ENCODER2_CHANNEL2 1
#endif
#ifndef ENCODER3_CHANNEL2
#define ENCODER3_CHANNEL2 1
#endif
#ifndef ENCODER4_CHANNEL2
#define ENCODER4_CHANNEL2 1
#endif
#ifndef ENCODER5_CHANNEL2
#define ENCODER5_CHANNEL2 1
#endif
// -----------------------------------------------------------------------------
// Relays
// -----------------------------------------------------------------------------
@ -424,9 +556,13 @@
// General
// -----------------------------------------------------------------------------
// Default hostname will be ESPURNA-XXXXXX, where XXXXXX is last 3 octets of chipID
// Device name (DNS, SoftAP SSID, ALEXA etc.)
// If empty, default will be ESPURNA-XXXXXX, where XXXXXX is last 3 octets of chipID
// When set, must be 1..31 characters. See:
// https://github.com/xoseperez/espurna/issues/921
// https://github.com/xoseperez/espurna/issues/1151
#ifndef HOSTNAME
#define HOSTNAME ""
#define HOSTNAME ""
#endif
// Relay providers


+ 5
- 0
code/espurna/config/dependencies.h View File

@ -15,6 +15,11 @@
#define DEBUG_WEB_SUPPORT 0
#endif
#if not WEB_SUPPORT
#undef API_SUPPORT
#define API_SUPPORT 0 // API support requires web support
#endif
#if not WEB_SUPPORT
#undef SSDP_SUPPORT
#define SSDP_SUPPORT 0 // SSDP support requires web support


+ 212
- 18
code/espurna/config/general.h View File

@ -9,8 +9,11 @@
#define DEVICE_NAME MANUFACTURER "_" DEVICE // Concatenate both to get a unique device name
// When defined, ADMIN_PASS must be 8..63 printable ASCII characters. See:
// https://en.wikipedia.org/wiki/Wi-Fi_Protected_Access#Target_users_(authentication_key_distribution)
// https://github.com/xoseperez/espurna/issues/1151
#ifndef ADMIN_PASS
#define ADMIN_PASS "fibonacci" // Default password (WEB, OTA, WIFI)
#define ADMIN_PASS "fibonacci" // Default password (WEB, OTA, WIFI SoftAP)
#endif
#ifndef USE_PASSWORD
@ -107,6 +110,10 @@
#define TELNET_STA 0 // By default, disallow connections via STA interface
#endif
#ifndef TELNET_PASSWORD
#define TELNET_PASSWORD 1 // Request password to start telnet session by default
#endif
#define TELNET_PORT 23 // Port to listen to telnet clients
#define TELNET_MAX_CLIENTS 1 // Max number of concurrent telnet clients
@ -141,7 +148,8 @@
// EEPROM
//------------------------------------------------------------------------------
#define EEPROM_SIZE 4096 // EEPROM size in bytes
#define EEPROM_SIZE SPI_FLASH_SEC_SIZE // EEPROM size in bytes (1 sector = 4096 bytes)
//#define EEPROM_RORATE_SECTORS 2 // Number of sectors to use for EEPROM rotation
// If not defined the firmware will use a number based
// on the number of available sectors
@ -158,8 +166,17 @@
// HEARTBEAT
//------------------------------------------------------------------------------
#ifndef HEARTBEAT_ENABLED
#define HEARTBEAT_ENABLED 1
#define HEARTBEAT_NONE 0 // Never send heartbeat
#define HEARTBEAT_ONCE 1 // Send it only once upon MQTT connection
#define HEARTBEAT_REPEAT 2 // Send it upon MQTT connection and every HEARTBEAT_INTERVAL
// Backwards compatibility check
#if defined(HEARTBEAT_ENABLED) && (HEARTBEAT_ENABLED == 0)
#define HEARTBEAT_MODE HEARTBEAT_NONE
#endif
#ifndef HEARTBEAT_MODE
#define HEARTBEAT_MODE HEARTBEAT_REPEAT
#endif
#ifndef HEARTBEAT_INTERVAL
@ -170,6 +187,7 @@
// Topics that will be reported in heartbeat
#define HEARTBEAT_REPORT_STATUS 1
#define HEARTBEAT_REPORT_SSID 1
#define HEARTBEAT_REPORT_IP 1
#define HEARTBEAT_REPORT_MAC 1
#define HEARTBEAT_REPORT_RSSI 1
@ -201,6 +219,10 @@
// BUTTON
//------------------------------------------------------------------------------
#ifndef BUTTON_SUPPORT
#define BUTTON_SUPPORT 1
#endif
#ifndef BUTTON_DEBOUNCE_DELAY
#define BUTTON_DEBOUNCE_DELAY 50 // Debounce delay (ms)
#endif
@ -215,6 +237,24 @@
#ifndef BUTTON_LNGLNGCLICK_DELAY
#define BUTTON_LNGLNGCLICK_DELAY 10000 // Time in ms holding the button down to get a long-long click
#define BUTTON_MQTT_SEND_ALL_EVENTS 0 // 0 - to send only events the are bound to actions
// 1 - to send all button events to MQTT
#endif
//------------------------------------------------------------------------------
// ENCODER
//------------------------------------------------------------------------------
#ifndef ENCODER_SUPPORT
#define ENCODER_SUPPORT 0
#endif
//------------------------------------------------------------------------------
// LED
//------------------------------------------------------------------------------
#ifndef LED_SUPPORT
#define LED_SUPPORT 1
#endif
//------------------------------------------------------------------------------
@ -261,6 +301,14 @@
#define RELAY_SAVE_DELAY 1000
#endif
// Configure the MQTT payload for ON/OFF
#ifndef RELAY_MQTT_ON
#define RELAY_MQTT_ON "1"
#endif
#ifndef RELAY_MQTT_OFF
#define RELAY_MQTT_OFF "0"
#endif
// -----------------------------------------------------------------------------
// WIFI
// -----------------------------------------------------------------------------
@ -281,6 +329,9 @@
#define WIFI_AP_CAPTIVE 1 // Captive portal enabled when in AP mode
#endif
#ifndef WIFI_FALLBACK_APMODE
#define WIFI_FALLBACK_APMODE 1 // Fallback to AP mode if no STA connection
#endif
#ifndef WIFI_SLEEP_MODE
#define WIFI_SLEEP_MODE WIFI_NONE_SLEEP // WIFI_NONE_SLEEP, WIFI_LIGHT_SLEEP or WIFI_MODEM_SLEEP
@ -411,11 +462,20 @@
// API
// -----------------------------------------------------------------------------
#ifndef API_SUPPORT
#define API_SUPPORT 1 // API (REST & RPC) support built in
#endif
// This will only be enabled if WEB_SUPPORT is 1 (this is the default value)
#ifndef API_ENABLED
#define API_ENABLED 0 // Do not enable API by default
#endif
#ifndef API_RESTFUL
#define API_RESTFUL 1 // A restful API requires changes to be issued as PUT requests
// Setting this to 0 will allow using GET to change relays, for instance
#endif
#ifndef API_BUFFER_SIZE
#define API_BUFFER_SIZE 15 // Size of the buffer for HTTP GET API responses
#endif
@ -672,6 +732,7 @@
#define MQTT_TOPIC_LED "led"
#define MQTT_TOPIC_BUTTON "button"
#define MQTT_TOPIC_IP "ip"
#define MQTT_TOPIC_SSID "ssid"
#define MQTT_TOPIC_VERSION "version"
#define MQTT_TOPIC_UPTIME "uptime"
#define MQTT_TOPIC_DATETIME "datetime"
@ -694,6 +755,9 @@
#define MQTT_TOPIC_LOADAVG "loadavg"
#define MQTT_TOPIC_BOARD "board"
#define MQTT_TOPIC_PULSE "pulse"
#define MQTT_TOPIC_SPEED "speed"
#define MQTT_TOPIC_IRIN "irin"
#define MQTT_TOPIC_IROUT "irout"
// Light module
#define MQTT_TOPIC_CHANNEL "channel"
@ -736,7 +800,7 @@
// -----------------------------------------------------------------------------
#ifndef SETTINGS_AUTOSAVE
#define SETTINGS_AUTOSAVE 1 // Autosave settings o force manual commit
#define SETTINGS_AUTOSAVE 1 // Autosave settings or force manual commit
#endif
#define SETTINGS_MAX_LIST_COUNT 10 // Maximum index for settings lists
@ -771,7 +835,7 @@
#endif
#if LIGHT_PROVIDER == LIGHT_PROVIDER_DIMMER
#define LIGHT_MAX_PWM 10000 // 5000 * 200ns => 1 kHz
#define LIGHT_MAX_PWM 10000 // 10000 * 200ns => 2 kHz
#endif
#endif // LIGHT_MAX_PWM
@ -866,6 +930,22 @@
#define HOMEASSISTANT_ENABLED 0 // Integration not enabled by default
#define HOMEASSISTANT_PREFIX "homeassistant" // Default MQTT prefix
#ifndef HOMEASSISTANT_PAYLOAD_ON
#define HOMEASSISTANT_PAYLOAD_ON "1" // Payload for ON and available messages
#endif
#ifndef HOMEASSISTANT_PAYLOAD_OFF
#define HOMEASSISTANT_PAYLOAD_OFF "0" // Payload for OFF and unavailable messages
#endif
#ifndef HOMEASSISTANT_PAYLOAD_AVAILABLE
#define HOMEASSISTANT_PAYLOAD_AVAILABLE "1" // Payload for available messages
#endif
#ifndef HOMEASSISTANT_PAYLOAD_NOT_AVAILABLE
#define HOMEASSISTANT_PAYLOAD_NOT_AVAILABLE "0" // Payload for available messages
#endif
// -----------------------------------------------------------------------------
// INFLUXDB
// -----------------------------------------------------------------------------
@ -935,6 +1015,11 @@
#define THINGSPEAK_URL "/update"
#define THINGSPEAK_MIN_INTERVAL 15000 // Minimum interval between POSTs (in millis)
#define THINGSPEAK_FIELDS 8 // Number of fields
#ifndef THINGSPEAK_TRIES
#define THINGSPEAK_TRIES 3 // Number of tries when sending data (minimum 1)
#endif
// -----------------------------------------------------------------------------
// SCHEDULER
@ -1021,32 +1106,53 @@
#define RF_RECEIVE_DELAY 500 // Interval between recieving in ms (avoid debouncing)
#endif
#ifndef RF_RAW_SUPPORT
#define RF_RAW_SUPPORT 0 // RF raw codes require a specific firmware for the EFM8BB1
// https://github.com/rhx/RF-Bridge-EFM8BB1
// https://github.com/rhx/RF-Bridge-EFM8BB1
#endif
// -----------------------------------------------------------------------------
// IR
// IR Bridge
// -----------------------------------------------------------------------------
#ifndef IR_SUPPORT
#define IR_SUPPORT 0 // Do not build with IR support by default (10.25Kb)
#endif
#ifndef IR_PIN
#define IR_PIN 4 // IR LED
//#define IR_RX_PIN 5 // GPIO the receiver is connected to
//#define IR_TX_PIN 4 // GPIO the transmitter is connected to
#ifndef IR_USE_RAW
#define IR_USE_RAW 0 // Use raw codes
#endif
#ifndef IR_BUFFER_SIZE
#define IR_BUFFER_SIZE 1024
#endif
#ifndef IR_TIMEOUT
#define IR_TIMEOUT 15U
#endif
#ifndef IR_REPEAT
#define IR_REPEAT 1
#endif
#ifndef IR_DELAY
#define IR_DELAY 100
#endif
#ifndef IR_DEBOUNCE
#define IR_DEBOUNCE 500 // IR debounce time in milliseconds
#endif
// 24 Buttons Set of the IR Remote
#ifndef IR_BUTTON_SET
#define IR_BUTTON_SET 1 // IR button set to use (see below)
#define IR_BUTTON_SET 0 // IR button set to use (see below)
#endif
//Remote Buttons SET 1 (for the original Remote shipped with the controller)
#if IR_SUPPORT
// -----------------------------------------------------------------------------
// Remote Buttons SET 1 (for the original Remote shipped with the controller)
#if IR_BUTTON_SET == 1
/*
@ -1067,7 +1173,7 @@
#define IR_BUTTON_COUNT 24
const unsigned long IR_BUTTON[IR_BUTTON_COUNT][3] PROGMEM = {
const uint32_t IR_BUTTON[IR_BUTTON_COUNT][3] PROGMEM = {
{ 0xFF906F, IR_BUTTON_MODE_BRIGHTER, 1 },
{ 0xFFB847, IR_BUTTON_MODE_BRIGHTER, 0 },
@ -1192,7 +1298,29 @@
//{ 0xE0E08877, IR_BUTTON_MODE_TOGGLE, 9 } //Extra Button
};
#endif
#endif // IR_SUPPORT
//Remote Buttons SET 4
#if IR_BUTTON_SET == 4
/*
+------+------+------+
| OFF | SRC | MUTE |
+------+------+------+
...
+------+------+------+
*/
#define IR_BUTTON_COUNT 1
const unsigned long IR_BUTTON[IR_BUTTON_COUNT][3] PROGMEM = {
{ 0xFFB24D, IR_BUTTON_MODE_TOGGLE, 0 } // Toggle Relay #0
};
#endif
#ifndef IR_BUTTON_COUNT
#define IR_BUTTON_COUNT 0
#endif
//--------------------------------------------------------------------------------
// Custom RF module
@ -1211,3 +1339,69 @@
#define RF_DEBOUNCE 500
#define RF_LEARN_TIMEOUT 60000
//--------------------------------------------------------------------------------
// Custom RFM69 to MQTT bridge
// Check http://tinkerman.cat/rfm69-wifi-gateway/
// Enable support by passing RFM69_SUPPORT=1 build flag
//--------------------------------------------------------------------------------
#ifndef RFM69_SUPPORT
#define RFM69_SUPPORT 0
#endif
#ifndef RFM69_MAX_TOPICS
#define RFM69_MAX_TOPICS 50
#endif
#ifndef RFM69_MAX_NODES
#define RFM69_MAX_NODES 255
#endif
#ifndef RFM69_DEFAULT_TOPIC
#define RFM69_DEFAULT_TOPIC "/rfm69gw/{node}/{key}"
#endif
#ifndef RFM69_NODE_ID
#define RFM69_NODE_ID 1
#endif
#ifndef RFM69_GATEWAY_ID
#define RFM69_GATEWAY_ID 1
#endif
#ifndef RFM69_NETWORK_ID
#define RFM69_NETWORK_ID 164
#endif
#ifndef RFM69_PROMISCUOUS
#define RFM69_PROMISCUOUS 0
#endif
#ifndef RFM69_PROMISCUOUS_SENDS
#define RFM69_PROMISCUOUS_SENDS 0
#endif
#ifndef RFM69_FREQUENCY
#define RFM69_FREQUENCY RF69_868MHZ
#endif
#ifndef RFM69_ENCRYPTKEY
#define RFM69_ENCRYPTKEY "fibonacci0123456"
#endif
#ifndef RFM69_CS_PIN
#define RFM69_CS_PIN SS
#endif
#ifndef RFM69_IRQ_PIN
#define RFM69_IRQ_PIN 5
#endif
#ifndef RFM69_RESET_PIN
#define RFM69_RESET_PIN 7
#endif
#ifndef RFM69_IS_RFM69HW
#define RFM69_IS_RFM69HW 0
#endif

+ 573
- 20
code/espurna/config/hardware.h View File

@ -15,7 +15,7 @@
// RELAY#_TYPE: Relay can be RELAY_TYPE_NORMAL, RELAY_TYPE_INVERSE, RELAY_TYPE_LATCHED or RELAY_TYPE_LATCHED_INVERSE
// LED#_PIN: GPIO for the n-th LED (1-based, up to 8 LEDs)
// LED#_PIN_INVERSE: LED has inversed logic (lit when pulled down)
// LED#_MODE: Check general.h for LED_MODE_%
// LED#_MODE: Check types.h for LED_MODE_%
// LED#_RELAY: Linked relay (1-based)
//
// Besides, other hardware specific information should be stated here
@ -37,9 +37,11 @@
// Disable non-core modules
#define ALEXA_SUPPORT 0
#define BROKER_SUPPORT 0
#define BUTTON_SUPPORT 0
#define DOMOTICZ_SUPPORT 0
#define HOMEASSISTANT_SUPPORT 0
#define I2C_SUPPORT 0
#define MDNS_SERVER_SUPPORT 0
#define MQTT_SUPPORT 0
#define NTP_SUPPORT 0
#define SCHEDULER_SUPPORT 0
@ -47,6 +49,14 @@
#define THINGSPEAK_SUPPORT 0
#define WEB_SUPPORT 0
// Extra light-weight image
//#define DEBUG_SERIAL_SUPPORT 0
//#define DEBUG_TELNET_SUPPORT 0
//#define DEBUG_WEB_SUPPORT 0
//#define LED_SUPPORT 0
//#define TELNET_SUPPORT 0
//#define TERMINAL_SUPPORT 0
// -----------------------------------------------------------------------------
// Development boards
// -----------------------------------------------------------------------------
@ -62,11 +72,11 @@
#define BUTTON1_MODE BUTTON_PUSHBUTTON | BUTTON_DEFAULT_HIGH
#define BUTTON1_RELAY 1
// Relays
// Hidden button will enter AP mode if dblclick and reset the device when long-long-clicked
#define RELAY1_PIN 12
#define RELAY1_TYPE RELAY_TYPE_NORMAL
// LEDs
// Light
#define LED1_PIN 2
#define LED1_PIN_INVERSE 1
@ -76,6 +86,26 @@
#define MANUFACTURER "NODEMCU"
#define DEVICE "BASIC"
#elif defined(WEMOS_D1_MINI)
// Info
#define MANUFACTURER "WEMOS"
#define DEVICE "D1_MINI"
// Buttons
// No buttons on the D1 MINI alone, but defining it without adding a button doen't create problems
#define BUTTON1_PIN 0 // Connect a pushbutton between D3 and GND,
// it's the same as using a Wemos one button shield
#define BUTTON1_MODE BUTTON_PUSHBUTTON | BUTTON_DEFAULT_HIGH
#define BUTTON1_RELAY 1
// LEDs
#define LED1_PIN 2
#define LED1_PIN_INVERSE 1
#define I2C_SDA_PIN 4 // D2
#define I2C_SCL_PIN 5 // D1
#elif defined(WEMOS_D1_MINI_RELAYSHIELD)
// Info
@ -196,6 +226,30 @@
#define RELAY1_PIN 12
#define RELAY1_TYPE RELAY_TYPE_INVERSE
// Check http://tinkerman.cat/rfm69-wifi-gateway/
#elif defined(TINKERMAN_RFM69GW)
// Info
#define MANUFACTURER "TINKERMAN"
#define DEVICE "RFM69GW"
// Buttons
#define BUTTON1_PIN 0
#define BUTTON1_MODE BUTTON_PUSHBUTTON | BUTTON_DEFAULT_HIGH
// RFM69GW
#define RFM69_SUPPORT 1
// Disable non-core modules
#define ALEXA_SUPPORT 0
#define DOMOTICZ_SUPPORT 0
#define HOMEASSISTANT_SUPPORT 0
#define I2C_SUPPORT 0
#define SCHEDULER_SUPPORT 0
#define SENSOR_SUPPORT 0
#define THINGSPEAK_SUPPORT 0
// -----------------------------------------------------------------------------
// Itead Studio boards
// -----------------------------------------------------------------------------
@ -846,10 +900,66 @@
#define CSE7766_SUPPORT 1
#define CSE7766_PIN 1
#elif defined(ITEAD_SONOFF_IFAN02)
// Info
#define MANUFACTURER "ITEAD"
#define DEVICE "SONOFF_IFAN02"
// These are virtual buttons triggered by the remote
#define BUTTON1_PIN 0
#define BUTTON2_PIN 9
#define BUTTON3_PIN 10
#define BUTTON4_PIN 14
#define BUTTON1_MODE BUTTON_PUSHBUTTON | BUTTON_DEFAULT_HIGH
#define BUTTON2_MODE BUTTON_PUSHBUTTON | BUTTON_DEFAULT_HIGH
#define BUTTON3_MODE BUTTON_PUSHBUTTON | BUTTON_DEFAULT_HIGH
#define BUTTON4_MODE BUTTON_PUSHBUTTON | BUTTON_DEFAULT_HIGH
// Relays
#define RELAY1_PIN 12
#define RELAY2_PIN 5
#define RELAY3_PIN 4
#define RELAY4_PIN 15
#define RELAY1_TYPE RELAY_TYPE_NORMAL
#define RELAY2_TYPE RELAY_TYPE_NORMAL
#define RELAY3_TYPE RELAY_TYPE_NORMAL
#define RELAY4_TYPE RELAY_TYPE_NORMAL
// LEDs
#define LED1_PIN 13
#define LED1_PIN_INVERSE 1
// -----------------------------------------------------------------------------
// YJZK
// -----------------------------------------------------------------------------
#elif defined(YJZK_SWITCH_1CH)
// Info
#define MANUFACTURER "YJZK"
#define DEVICE "SWITCH_1CH"
// Buttons
#define BUTTON1_PIN 0
#define BUTTON1_MODE BUTTON_PUSHBUTTON | BUTTON_DEFAULT_HIGH
#define BUTTON1_PRESS BUTTON_MODE_TOGGLE
#define BUTTON1_CLICK BUTTON_MODE_NONE
#define BUTTON1_DBLCLICK BUTTON_MODE_NONE
#define BUTTON1_LNGCLICK BUTTON_MODE_NONE
#define BUTTON1_LNGLNGCLICK BUTTON_MODE_RESET
#define BUTTON1_RELAY 1
// Relays
#define RELAY1_PIN 12
#define RELAY1_TYPE RELAY_TYPE_NORMAL
// LEDs
#define LED1_PIN 13
#define LED1_PIN_INVERSE 0
#elif defined(YJZK_SWITCH_2CH)
// Info
@ -861,7 +971,18 @@
#define BUTTON2_PIN 9
#define BUTTON1_MODE BUTTON_PUSHBUTTON | BUTTON_DEFAULT_HIGH
#define BUTTON1_PRESS BUTTON_MODE_TOGGLE
#define BUTTON1_CLICK BUTTON_MODE_NONE
#define BUTTON1_DBLCLICK BUTTON_MODE_NONE
#define BUTTON1_LNGCLICK BUTTON_MODE_NONE
#define BUTTON1_LNGLNGCLICK BUTTON_MODE_RESET
#define BUTTON2_MODE BUTTON_PUSHBUTTON | BUTTON_DEFAULT_HIGH
#define BUTTON2_PRESS BUTTON_MODE_TOGGLE
#define BUTTON2_CLICK BUTTON_MODE_NONE
#define BUTTON2_DBLCLICK BUTTON_MODE_NONE
#define BUTTON2_LNGCLICK BUTTON_MODE_NONE
#define BUTTON2_LNGLNGCLICK BUTTON_MODE_RESET
#define BUTTON1_RELAY 1
#define BUTTON2_RELAY 2
@ -877,6 +998,57 @@
#define LED1_PIN 13
#define LED1_PIN_INVERSE 0
// YJZK 3CH switch
// Also Lixin Touch Wifi 3M
#elif defined(YJZK_SWITCH_3CH)
// Info
#define MANUFACTURER "YJZK"
#define DEVICE "SWITCH_3CH"
// Buttons
#define BUTTON1_PIN 0
#define BUTTON2_PIN 9
#define BUTTON3_PIN 10
#define BUTTON1_MODE BUTTON_PUSHBUTTON | BUTTON_DEFAULT_HIGH
#define BUTTON1_PRESS BUTTON_MODE_TOGGLE
#define BUTTON1_CLICK BUTTON_MODE_NONE
#define BUTTON1_DBLCLICK BUTTON_MODE_NONE
#define BUTTON1_LNGCLICK BUTTON_MODE_NONE
#define BUTTON1_LNGLNGCLICK BUTTON_MODE_RESET
#define BUTTON2_MODE BUTTON_PUSHBUTTON | BUTTON_DEFAULT_HIGH
#define BUTTON2_PRESS BUTTON_MODE_TOGGLE
#define BUTTON2_CLICK BUTTON_MODE_NONE
#define BUTTON2_DBLCLICK BUTTON_MODE_NONE
#define BUTTON2_LNGCLICK BUTTON_MODE_NONE
#define BUTTON2_LNGLNGCLICK BUTTON_MODE_RESET
#define BUTTON3_MODE BUTTON_PUSHBUTTON | BUTTON_DEFAULT_HIGH
#define BUTTON3_PRESS BUTTON_MODE_TOGGLE
#define BUTTON3_CLICK BUTTON_MODE_NONE
#define BUTTON3_DBLCLICK BUTTON_MODE_NONE
#define BUTTON3_LNGCLICK BUTTON_MODE_NONE
#define BUTTON3_LNGLNGCLICK BUTTON_MODE_RESET
#define BUTTON1_RELAY 1
#define BUTTON2_RELAY 2
#define BUTTON3_RELAY 3
// Relays
#define RELAY1_PIN 12
#define RELAY2_PIN 5
#define RELAY3_PIN 4
#define RELAY1_TYPE RELAY_TYPE_NORMAL
#define RELAY2_TYPE RELAY_TYPE_NORMAL
#define RELAY3_TYPE RELAY_TYPE_NORMAL
// LEDs
#define LED1_PIN 13
#define LED1_PIN_INVERSE 0
// -----------------------------------------------------------------------------
// Electrodragon boards
@ -984,7 +1156,7 @@
// IR
#define IR_SUPPORT 1
#define IR_PIN 4
#define IR_RX_PIN 4
#define IR_BUTTON_SET 1
#elif defined(MAGICHOME_LED_CONTROLLER_20)
@ -1013,7 +1185,7 @@
// IR
#define IR_SUPPORT 1
#define IR_PIN 4
#define IR_RX_PIN 4
#define IR_BUTTON_SET 1
// -----------------------------------------------------------------------------
@ -1213,6 +1385,36 @@
#define RELAY1_TYPE RELAY_TYPE_LATCHED
#define RELAY1_RESET_PIN 12
// -----------------------------------------------------------------------------
// EX-Store Wifi Relay v5.0
// -----------------------------------------------------------------------------
#elif defined(EXS_WIFI_RELAY_V50)
// Info
#define MANUFACTURER "EXS"
#define DEVICE "WIFI_RELAY_V50"
// Buttons
#define BUTTON1_PIN 5
#define BUTTON2_PIN 4
#define BUTTON1_RELAY 1
#define BUTTON2_RELAY 2
#define BUTTON1_MODE BUTTON_SWITCH | BUTTON_DEFAULT_HIGH | BUTTON_SET_PULLUP
#define BUTTON2_MODE BUTTON_SWITCH | BUTTON_DEFAULT_HIGH | BUTTON_SET_PULLUP
// Relays
#define RELAY1_PIN 14
#define RELAY1_TYPE RELAY_TYPE_LATCHED
#define RELAY1_RESET_PIN 16
#define RELAY2_PIN 13
#define RELAY2_TYPE RELAY_TYPE_LATCHED
#define RELAY2_RESET_PIN 12
// LEDs
#define LED1_PIN 15
#define LED1_PIN_INVERSE 0
// -----------------------------------------------------------------------------
// V9261F
// -----------------------------------------------------------------------------
@ -1516,7 +1718,7 @@
// LEDs
#define LED1_PIN 13
#define LED1_PIN_INVERSE 0
#define LED1_PIN_INVERSE 1
// HLW8012
#ifndef HLW8012_SUPPORT
@ -1718,6 +1920,29 @@
#define HLW8012_CURRENT_R 0.002 // Current resistor
#define HLW8012_VOLTAGE_R_UP ( 2 * 1000000 ) // Upstream voltage resistor
// -----------------------------------------------------------------------------
// Maxcio W-DE004
// -----------------------------------------------------------------------------
#elif defined(MAXCIO_WDE004)
// Info
#define MANUFACTURER "MAXCIO"
#define DEVICE "WDE004"
// Buttons
#define BUTTON1_PIN 1
#define BUTTON1_MODE BUTTON_PUSHBUTTON | BUTTON_DEFAULT_HIGH
#define BUTTON1_RELAY 1
// Relays
#define RELAY1_PIN 14
#define RELAY1_TYPE RELAY_TYPE_NORMAL
// LEDs
#define LED1_PIN 13
#define LED1_PIN_INVERSE 1
// -----------------------------------------------------------------------------
// YiDian XS-SSA05
// -----------------------------------------------------------------------------
@ -1730,7 +1955,7 @@
// Buttons
#define BUTTON1_PIN 13
#define BUTTON1_MODE BUTTON_PUSHBUTTON
#define BUTTON1_MODE BUTTON_PUSHBUTTON | BUTTON_DEFAULT_HIGH
#define BUTTON1_RELAY 1
// Relays
@ -1738,8 +1963,13 @@
#define RELAY1_TYPE RELAY_TYPE_NORMAL
// LEDs
#define LED1_PIN 4
#define LED1_PIN_INVERSE 0
#define LED1_PIN 0 // red
#define LED1_PIN_INVERSE 1
#define LED1_MODE LED_MODE_WIFI
#define LED2_PIN 15 // blue
#define LED2_PIN_INVERSE 1
#define LED2_MODE LED_MODE_RELAY
// HLW8012
#ifndef HLW8012_SUPPORT
@ -1749,8 +1979,34 @@
#define HLW8012_CF1_PIN 14
#define HLW8012_CF_PIN 5
#define HLW8012_CURRENT_R 0.001 // Current resistor
#define HLW8012_VOLTAGE_R_UP ( 2 * 1200000 ) // Upstream voltage resistor
#define HLW8012_SEL_CURRENT LOW
#define HLW8012_CURRENT_RATIO 25740
#define HLW8012_VOLTAGE_RATIO 313400
#define HLW8012_POWER_RATIO 3414290
#define HLW8012_INTERRUPT_ON FALLING
// -----------------------------------------------------------------------------
// TONBUX XS-SSA01
// -----------------------------------------------------------------------------
#elif defined(TONBUX_XSSSA01)
// Info
#define MANUFACTURER "TONBUX"
#define DEVICE "XSSSA01"
// Buttons
#define BUTTON1_PIN 4
#define BUTTON1_MODE BUTTON_PUSHBUTTON | BUTTON_DEFAULT_HIGH
#define BUTTON1_RELAY 1
// Relays
#define RELAY1_PIN 14
#define RELAY1_TYPE RELAY_TYPE_NORMAL
// LEDs
#define LED1_PIN 13
#define LED1_PIN_INVERSE 0
// -----------------------------------------------------------------------------
// TONBUX XS-SSA06
@ -2110,13 +2366,19 @@
// Buttons
#define BUTTON1_PIN 12
#define BUTTON1_RELAY 1
#define BUTTON1_MODE BUTTON_PUSHBUTTON | BUTTON_DEFAULT_HIGH
#define BUTTON1_MODE BUTTON_SWITCH | BUTTON_DEFAULT_HIGH //Hardware Pullup
#define BUTTON1_PRESS BUTTON_MODE_NONE
#define BUTTON1_CLICK BUTTON_MODE_TOGGLE
#define BUTTON1_DBLCLICK BUTTON_MODE_NONE
#define BUTTON1_LNGCLICK BUTTON_MODE_NONE
#define BUTTON1_LNGLNGCLICK BUTTON_MODE_NONE
#define BUTTON2_PIN 13
#define BUTTON2_RELAY 2
#define BUTTON2_MODE BUTTON_PUSHBUTTON | BUTTON_DEFAULT_HIGH
#define BUTTON2_MODE BUTTON_SWITCH | BUTTON_DEFAULT_HIGH //Hardware Pullup
#define BUTTON2_CLICK BUTTON_MODE_TOGGLE
// Relays
#define RELAY1_PIN 4
@ -2314,6 +2576,59 @@
#define HLW8012_POWER_RATIO 3414290
#define HLW8012_INTERRUPT_ON FALLING
// ----------------------------------------------------------------------------------------
// Homecube 16A is similar but some pins differ and it also has RGB LEDs
// https://www.amazon.de/gp/product/B07D7RVF56/ref=oh_aui_detailpage_o00_s01?ie=UTF8&psc=1
// ----------------------------------------------------------------------------------------
#elif defined(HOMECUBE_16A)
// Info
#define MANUFACTURER "HOMECUBE"
#define DEVICE "16A"
// Buttons
#define BUTTON1_PIN 13
#define BUTTON1_MODE BUTTON_PUSHBUTTON | BUTTON_DEFAULT_HIGH
#define BUTTON1_RELAY 1
// Relays
#define RELAY1_PIN 15
#define RELAY1_TYPE RELAY_TYPE_NORMAL
// LEDs
//LED Pin 4 - ESP8266 onboard LED
//Red LED: 0
//Green LED: 12
//Blue LED: 2
// Blue
#define LED1_PIN 2
#define LED1_PIN_INVERSE 0
// Green
#define LED2_PIN 12
#define LED2_PIN_INVERSE 1
#define LED2_MODE LED_MODE_RELAY
// Red
#define LED3_PIN 0
#define LED3_PIN_INVERSE 0
#define LED3_MODE LED_MODE_OFF
// HJL01 / BL0937
#ifndef HLW8012_SUPPORT
#define HLW8012_SUPPORT 1
#endif
#define HLW8012_SEL_PIN 16
#define HLW8012_CF1_PIN 14
#define HLW8012_CF_PIN 5
#define HLW8012_SEL_CURRENT LOW
#define HLW8012_CURRENT_RATIO 25740
#define HLW8012_VOLTAGE_RATIO 313400
#define HLW8012_POWER_RATIO 3414290
#define HLW8012_INTERRUPT_ON FALLING
// -----------------------------------------------------------------------------
// VANZAVANZU Smart Outlet Socket (based on BL0937 or HJL-01)
// https://www.amazon.com/Smart-Plug-Wifi-Mini-VANZAVANZU/dp/B078PHD6S5
@ -2359,6 +2674,230 @@
#define HLW8012_POWER_RATIO 3414290
#define HLW8012_INTERRUPT_ON FALLING
// -----------------------------------------------------------------------------
#elif defined(GENERIC_AG_L4)
// Info
#define MANUFACTURER "GENERIC"
#define DEVICE "AG_L4"
#define RELAY_PROVIDER RELAY_PROVIDER_LIGHT
#define LIGHT_PROVIDER LIGHT_PROVIDER_DIMMER
#define DUMMY_RELAY_COUNT 1
// button 1: "power" button
#define BUTTON1_PIN 4
#define BUTTON1_RELAY 1
#define BUTTON1_MODE BUTTON_PUSHBUTTON | BUTTON_SET_PULLUP | BUTTON_DEFAULT_HIGH
#define BUTTON1_PRESS BUTTON_MODE_TOGGLE
#define BUTTON1_CLICK BUTTON_MODE_NONE
#define BUTTON1_DBLCLICK BUTTON_MODE_NONE
#define BUTTON1_LNGCLICK BUTTON_MODE_NONE
#define BUTTON1_LNGLNGCLICK BUTTON_MODE_RESET
// button 2: "wifi" button
#define BUTTON2_PIN 2
#define BUTTON2_MODE BUTTON_PUSHBUTTON | BUTTON_DEFAULT_HIGH
#define BUTTON2_PRESS BUTTON_MODE_TOGGLE
#define BUTTON2_CLICK BUTTON_MODE_NONE
#define BUTTON2_DBLCLICK BUTTON_MODE_NONE
#define BUTTON2_LNGCLICK BUTTON_MODE_NONE
#define BUTTON2_LNGLNGCLICK BUTTON_MODE_NONE
// LEDs
#define LED1_PIN 5 // red status led
#define LED1_PIN_INVERSE 0
#define LED2_PIN 16 // master light power
#define LED2_PIN_INVERSE 1
#define LED2_MODE LED_MODE_RELAY
// Light
#define LIGHT_CHANNELS 3
#define LIGHT_CH1_PIN 14 // RED
#define LIGHT_CH2_PIN 13 // GREEN
#define LIGHT_CH3_PIN 12 // BLUE
#define LIGHT_CH1_INVERSE 0
#define LIGHT_CH2_INVERSE 0
#define LIGHT_CH3_INVERSE 0
// -----------------------------------------------------------------------------
#elif defined(ALLTERCO_SHELLY1)
// Info
#define MANUFACTURER "ALLTERCO"
#define DEVICE "SHELLY1"
// Buttons
#define BUTTON1_PIN 5
#define BUTTON1_MODE BUTTON_SWITCH
#define BUTTON1_RELAY 1
// Relays
#define RELAY1_PIN 4
#define RELAY1_TYPE RELAY_TYPE_NORMAL
#elif defined(ALLTERCO_SHELLY2)
// Info
#define MANUFACTURER "ALLTERCO"
#define DEVICE "SHELLY2"
// Buttons
#define BUTTON1_PIN 12
#define BUTTON2_PIN 14
#define BUTTON1_MODE BUTTON_SWITCH
#define BUTTON2_MODE BUTTON_SWITCH
#define BUTTON1_RELAY 1
#define BUTTON2_RELAY 2
// Relays
#define RELAY1_PIN 4
#define RELAY1_TYPE RELAY_TYPE_NORMAL
#define RELAY2_PIN 5
#define RELAY2_TYPE RELAY_TYPE_NORMAL
// -----------------------------------------------------------------------------
#elif defined(LOHAS_9W)
// Info
#define MANUFACTURER "LOHAS"
#define DEVICE "E27_9W"
#define RELAY_PROVIDER RELAY_PROVIDER_LIGHT
#define LIGHT_PROVIDER LIGHT_PROVIDER_MY92XX
#define DUMMY_RELAY_COUNT 1
// Light
#define LIGHT_CHANNELS 5
#define MY92XX_MODEL MY92XX_MODEL_MY9231
#define MY92XX_CHIPS 2
#define MY92XX_DI_PIN 13
#define MY92XX_DCKI_PIN 15
#define MY92XX_COMMAND MY92XX_COMMAND_DEFAULT
#define MY92XX_MAPPING 0, 1, 2, 3, 4
#define LIGHT_WHITE_FACTOR (0.1) // White LEDs are way more bright in the B1
// -----------------------------------------------------------------------------
#elif defined(XIAOMI_SMART_DESK_LAMP)
// Info
#define MANUFACTURER "XIAOMI"
#define DEVICE "SMART_DESK_LAMP"
// Buttons
#define BUTTON1_PIN 2
#define BUTTON2_PIN 14
#define BUTTON1_MODE BUTTON_PUSHBUTTON | BUTTON_DEFAULT_HIGH | BUTTON_SET_PULLUP
#define BUTTON2_MODE BUTTON_PUSHBUTTON | BUTTON_DEFAULT_HIGH | BUTTON_SET_PULLUP
// This button doubles as switch here and as encoder mode switch below
// Clicking it (for less than 500ms) will turn the light on and off
// Double and Long clicks will not work as these are used to modify the encoder action
#define BUTTON1_RELAY 1
#define BUTTON_LNGCLICK_DELAY 500
#define BUTTON1_DBLCLICK BUTTON_MODE_NONE
#define BUTTON1_LNGCLICK BUTTON_MODE_NONE
#define BUTTON1_LNGLNGCLICK BUTTON_MODE_NONE
// Hidden button will enter AP mode if dblclick and reset the device when long-long-clicked
#define BUTTON2_DBLCLICK BUTTON_MODE_AP
#define BUTTON2_LNGLNGCLICK BUTTON_MODE_RESET
// Light
#define LIGHT_PROVIDER LIGHT_PROVIDER_DIMMER
#define DUMMY_RELAY_COUNT 1
#define LIGHT_STEP 8
#define LIGHT_CHANNELS 2
#define LIGHT_CH1_PIN 5 // warm white
#define LIGHT_CH1_INVERSE 0
#define LIGHT_CH2_PIN 4 // cold white
#define LIGHT_CH2_INVERSE 0
// Encoder
// If mode is ENCODER_MODE_RATIO, the value ratio between both channels is changed
// when the button is not pressed, and the overall brightness when pressed
// If mode is ENCODER_MODE_CHANNEL, the first channel value is changed
// when the button is not pressed, and the second channel when pressed
// If no ENCODERX_BUTTON_PIN defined it will only change the value of the first defined channel
#define ENCODER_SUPPORT 1
#define ENCODER1_PIN1 12
#define ENCODER1_PIN2 13
#define ENCODER1_BUTTON_PIN 2 // active low by default, with software pullup
#define ENCODER1_CHANNEL1 0 // please note this value is 0-based (LIGHT_CH1 above)
#define ENCODER1_CHANNEL2 1 // please note this value is 0-based (LIGHT_CH2 above)
#define ENCODER1_MODE ENCODER_MODE_RATIO
#elif defined(PHYX_ESP12_RGB)
// Info
#define MANUFACTURER "PHYX"
#define DEVICE "ESP12_RGB"
#define RELAY_PROVIDER RELAY_PROVIDER_LIGHT
#define LIGHT_PROVIDER LIGHT_PROVIDER_DIMMER
#define DUMMY_RELAY_COUNT 1
// Light
#define LIGHT_CHANNELS 3
#define LIGHT_CH1_PIN 4 // RED
#define LIGHT_CH2_PIN 14 // GREEN
#define LIGHT_CH3_PIN 12 // BLUE
#define LIGHT_CH1_INVERSE 0
#define LIGHT_CH2_INVERSE 0
#define LIGHT_CH3_INVERSE 0
// -----------------------------------------------------------------------------
// iWoole LED Table Lamp
// http://iwoole.com/newst-led-smart-night-light-7w-smart-table-light-rgbw-wifi-app-remote-control-110v-220v-us-eu-plug-smart-lamp-google-home-decore-p00022p1.html
// -----------------------------------------------------------------------------
#elif defined(IWOOLE_LED_TABLE_LAMP)
// Info
#define MANUFACTURER "IWOOLE"
#define DEVICE "LED_TABLE_LAMP"
#define RELAY_PROVIDER RELAY_PROVIDER_LIGHT
#define LIGHT_PROVIDER LIGHT_PROVIDER_DIMMER
#define DUMMY_RELAY_COUNT 1
// Light
#define LIGHT_CHANNELS 4
#define LIGHT_CH1_PIN 12 // RED
#define LIGHT_CH2_PIN 5 // GREEN
#define LIGHT_CH3_PIN 14 // BLUE
#define LIGHT_CH4_PIN 4 // WHITE
#define LIGHT_CH1_INVERSE 0
#define LIGHT_CH2_INVERSE 0
#define LIGHT_CH3_INVERSE 0
#define LIGHT_CH4_INVERSE 0
// -----------------------------------------------------------------------------
// Bestek Smart Plug with 2 USB ports
// https://www.bestekcorp.com/bestek-smart-plug-works-with-amazon-alexa-google-assistant-and-ifttt-with-2-usb
// -----------------------------------------------------------------------------
#elif defined(BESTEK_MRJ1011)
// Info
#define MANUFACTURER "BESTEK"
#define DEVICE "MRJ1011"
// Buttons
#define BUTTON1_PIN 13
#define BUTTON1_MODE BUTTON_PUSHBUTTON | BUTTON_SET_PULLUP | BUTTON_DEFAULT_HIGH
#define BUTTON1_RELAY 1
// Relay
#define RELAY1_PIN 12
#define RELAY1_TYPE RELAY_TYPE_NORMAL
// LED
#define LED1_PIN 4
#define LED1_PIN_INVERSE 1
// -----------------------------------------------------------------------------
// TEST boards (do not use!!)
// -----------------------------------------------------------------------------
@ -2402,7 +2941,7 @@
#define SHT3X_I2C_SUPPORT 1
#define SI7021_SUPPORT 1
#define PMSX003_SUPPORT 1
#define SENSEAIR_SUPPORT 1
#define SENSEAIR_SUPPORT1
// A bit of lights - pin 5
@ -2435,12 +2974,21 @@
#define ECH1560_MISO_PIN 11
#define ECH1560_INVERTED 12
// MICS-2710 & MICS-5525 test
#define MICS2710_SUPPORT 1
#define MICS5525_SUPPORT 1
#elif defined(TRAVIS02)
// Relay provider dual
#define MANUFACTURER "TravisCI"
#define DEVICE "Virtual board 02"
// Some buttons - pin 0
#define BUTTON1_PIN 0
#define BUTTON1_MODE BUTTON_PUSHBUTTON | BUTTON_DEFAULT_HIGH
#define BUTTON1_RELAY 1
// A bit of CSE7766 - pin 1
#ifndef CSE7766_SUPPORT
#define CSE7766_SUPPORT 1
@ -2456,7 +3004,7 @@
// IR - pin 4
#define IR_SUPPORT 1
#define IR_PIN 4
#define IR_RX_PIN 4
#define IR_BUTTON_SET 1
// A bit of DHT - pin 5
@ -2472,10 +3020,10 @@
#define EVENTS_SUPPORT 1
#define EVENTS_PIN 6
// HC-RS04
#define HCSR04_SUPPORT 1
#define HCSR04_TRIGGER 7
#define HCSR04_ECHO 8
// Sonar
#define SONAR_SUPPORT 1
#define SONAR_TRIGGER 7
#define SONAR_ECHO 8
// MHZ19
#define MHZ19_SUPPORT 1
@ -2500,7 +3048,7 @@
#define NOFUSS_SUPPORT 1
#define UART_MQTT_SUPPORT 1
#define INFLUXDB_SUPPORT 1
#define IR_SUPPORT 1
#define IR_SUPPORT 1
#elif defined(TRAVIS03)
@ -2508,6 +3056,11 @@
#define MANUFACTURER "TravisCI"
#define DEVICE "Virtual board 03"
// Some buttons - pin 0
#define BUTTON1_PIN 0
#define BUTTON1_MODE BUTTON_PUSHBUTTON | BUTTON_DEFAULT_HIGH
#define BUTTON1_RELAY 1
// MY9231 Light - pins 1,2
#define RELAY_PROVIDER RELAY_PROVIDER_LIGHT
#define LIGHT_PROVIDER LIGHT_PROVIDER_MY92XX


+ 58
- 16
code/espurna/config/progmem.h View File

@ -31,9 +31,15 @@ PROGMEM const char espurna_modules[] =
#if ALEXA_SUPPORT
"ALEXA "
#endif
#if API_SUPPORT
"API "
#endif
#if BROKER_SUPPORT
"BROKER "
#endif
#if BUTTON_SUPPORT
"BUTTON "
#endif
#if DEBUG_SERIAL_SUPPORT
"DEBUG_SERIAL "
#endif
@ -49,6 +55,9 @@ PROGMEM const char espurna_modules[] =
#if DOMOTICZ_SUPPORT
"DOMOTICZ "
#endif
#if ENCODER_SUPPORT
"ENCODER "
#endif
#if HOMEASSISTANT_SUPPORT
"HOMEASSISTANT "
#endif
@ -58,15 +67,21 @@ PROGMEM const char espurna_modules[] =
#if INFLUXDB_SUPPORT
"INFLUXDB "
#endif
#if IR_SUPPORT
"IR "
#endif
#if LED_SUPPORT
"LED "
#endif
#if LLMNR_SUPPORT
"LLMNR "
#endif
#if MDNS_SERVER_SUPPORT
"MDNS_SERVER "
#endif
#if MDNS_CLIENT_SUPPORT
"MDNS_CLIENT "
#endif
#if MDNS_SERVER_SUPPORT
"MDNS_SERVER "
#endif
#if MQTT_SUPPORT
"MQTT "
#endif
@ -79,6 +94,9 @@ PROGMEM const char espurna_modules[] =
#if NTP_SUPPORT
"NTP "
#endif
#if RFM69_SUPPORT
"RFM69 "
#endif
#if RF_SUPPORT
"RF "
#endif
@ -163,21 +181,30 @@ PROGMEM const char espurna_sensors[] =
#if GUVAS12SD_SUPPORT
"GUVAS12SD "
#endif
#if HCSR04_SUPPORT
"HCSR04 "
#endif
#if HLW8012_SUPPORT
"HLW8012 "
#endif
#if MHZ19_SUPPORT
"MHZ19 "
#endif
#if MICS2710_SUPPORT
"MICS2710 "
#endif
#if MICS5525_SUPPORT
"MICS5525 "
#endif
#if NTC_SUPPORT
"NTC "
#endif
#if PMSX003_SUPPORT
"PMSX003 "
#endif
#if PZEM004T_SUPPORT
"PZEM004T "
#endif
#if SDS011_SUPPORT
"SDS011 "
#endif
#if SENSEAIR_SUPPORT
"SENSEAIR "
#endif
@ -187,6 +214,9 @@ PROGMEM const char espurna_sensors[] =
#if SI7021_SUPPORT
"SI7021 "
#endif
#if SONAR_SUPPORT
"SONAR "
#endif
#if TMP3X_SUPPORT
"TMP3X "
#endif
@ -198,12 +228,14 @@ PROGMEM const char espurna_sensors[] =
PROGMEM const unsigned char magnitude_decimals[] = {
0,
1, 0, 2,
3, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0,
0, 0, 0,
0, 0, 3, 3,
4, 4 // Geiger Counter decimals
1, 0, 2, // THP
3, 0, 0, 0, 0, 0, 0, 0, // Power decimals
0, 0, 0, // analog, digital, event
0, 0, 0, // PM
0, 0, 3, 3, 0,
4, 4, // Geiger Counter decimals
0,
0, 0, 0 // NO2, CO, Ohms
};
PROGMEM const char magnitude_unknown_topic[] = "unknown";
@ -220,7 +252,7 @@ PROGMEM const char magnitude_energy_topic[] = "energy";
PROGMEM const char magnitude_energy_delta_topic[] = "energy_delta";
PROGMEM const char magnitude_analog_topic[] = "analog";
PROGMEM const char magnitude_digital_topic[] = "digital";
PROGMEM const char magnitude_events_topic[] = "events";
PROGMEM const char magnitude_event_topic[] = "event";
PROGMEM const char magnitude_pm1dot0_topic[] = "pm1dot0";
PROGMEM const char magnitude_pm2dot5_topic[] = "pm2dot5";
PROGMEM const char magnitude_pm10_topic[] = "pm10";
@ -231,17 +263,23 @@ PROGMEM const char magnitude_distance_topic[] = "distance";
PROGMEM const char magnitude_hcho_topic[] = "hcho";
PROGMEM const char magnitude_geiger_cpm_topic[] = "ldr_cpm"; // local dose rate [Counts per minute]
PROGMEM const char magnitude_geiger_sv_topic[] = "ldr_uSvh"; // local dose rate [µSievert per hour]
PROGMEM const char magnitude_count_topic[] = "count";
PROGMEM const char magnitude_no2_topic[] = "no2";
PROGMEM const char magnitude_co_topic[] = "co";
PROGMEM const char magnitude_resistance_topic[] = "resistance";
PROGMEM const char* const magnitude_topics[] = {
magnitude_unknown_topic, magnitude_temperature_topic, magnitude_humidity_topic,
magnitude_pressure_topic, magnitude_current_topic, magnitude_voltage_topic,
magnitude_active_power_topic, magnitude_apparent_power_topic, magnitude_reactive_power_topic,
magnitude_power_factor_topic, magnitude_energy_topic, magnitude_energy_delta_topic,
magnitude_analog_topic, magnitude_digital_topic, magnitude_events_topic,
magnitude_analog_topic, magnitude_digital_topic, magnitude_event_topic,
magnitude_pm1dot0_topic, magnitude_pm2dot5_topic, magnitude_pm10_topic,
magnitude_co2_topic, magnitude_lux_topic, magnitude_uv_topic,
magnitude_distance_topic, magnitude_hcho_topic,
magnitude_geiger_cpm_topic, magnitude_geiger_sv_topic // Geiger Counter topics
magnitude_geiger_cpm_topic, magnitude_geiger_sv_topic,
magnitude_count_topic,
magnitude_no2_topic, magnitude_co_topic, magnitude_resistance_topic
};
PROGMEM const char magnitude_empty[] = "";
@ -263,6 +301,7 @@ PROGMEM const char magnitude_distance[] = "m";
PROGMEM const char magnitude_mgm3[] = "mg/m³";
PROGMEM const char magnitude_geiger_cpm[] = "cpm"; // Counts per Minute: Unit of local dose rate (Geiger counting)
PROGMEM const char magnitude_geiger_sv[] = "µSv/h"; // µSievert per hour: 2nd unit of local dose rate (Geiger counting)
PROGMEM const char magnitude_resistance[] = "ohm";
PROGMEM const char* const magnitude_units[] = {
@ -274,7 +313,10 @@ PROGMEM const char* const magnitude_units[] = {
magnitude_ugm3, magnitude_ugm3, magnitude_ugm3,
magnitude_ppm, magnitude_lux, magnitude_uv,
magnitude_distance, magnitude_mgm3,
magnitude_geiger_cpm, magnitude_geiger_sv // Geiger counter units
magnitude_geiger_cpm, magnitude_geiger_sv, // Geiger counter units
magnitude_empty, //
magnitude_ppm, magnitude_ppm, // NO2 & CO2
magnitude_resistance
};
#endif

+ 128
- 60
code/espurna/config/prototypes.h View File

@ -2,78 +2,81 @@
#include <ArduinoJson.h>
#include <functional>
#include <pgmspace.h>
#include <core_version.h>
extern "C" {
#include "user_interface.h"
}
// -----------------------------------------------------------------------------
// EEPROM_ROTATE
// -----------------------------------------------------------------------------
#include <EEPROM_Rotate.h>
EEPROM_Rotate EEPROMr;
// -----------------------------------------------------------------------------
// WebServer
// API
// -----------------------------------------------------------------------------
#include <ESPAsyncWebServer.h>
AsyncWebServer * webServer();
#if WEB_SUPPORT
typedef std::function<void(char *, size_t)> api_get_callback_f;
typedef std::function<void(const char *)> api_put_callback_f;
void apiRegister(const char * key, api_get_callback_f getFn, api_put_callback_f putFn = NULL);
#else
#define api_get_callback_f void *
#define api_put_callback_f void *
#endif
// -----------------------------------------------------------------------------
// API
// Broker
// -----------------------------------------------------------------------------
typedef std::function<void(char *, size_t)> api_get_callback_f;
typedef std::function<void(const char *)> api_put_callback_f;
void apiRegister(const char * key, api_get_callback_f getFn, api_put_callback_f putFn = NULL);
#if BROKER_SUPPORT
void brokerRegister(void (*)(const char *, unsigned char, const char *));
#endif
// -----------------------------------------------------------------------------
// WebSockets
// Debug
// -----------------------------------------------------------------------------
typedef std::function<void(JsonObject&)> ws_on_send_callback_f;
void wsOnSendRegister(ws_on_send_callback_f callback);
void wsSend(ws_on_send_callback_f sender);
typedef std::function<void(uint32_t, const char *, JsonObject&)> ws_on_action_callback_f;
void wsOnActionRegister(ws_on_action_callback_f callback);
typedef std::function<void(void)> ws_on_after_parse_callback_f;
void wsOnAfterParseRegister(ws_on_after_parse_callback_f callback);
void debugSend(const char * format, ...);
void debugSend_P(PGM_P format, ...);
extern "C" {
void custom_crash_callback(struct rst_info*, uint32_t, uint32_t);
}
typedef std::function<bool(const char *, JsonVariant&)> ws_on_receive_callback_f;
void wsOnReceiveRegister(ws_on_receive_callback_f callback);
// Core version 2.4.2 and higher changed the cont_t structure to a pointer:
// https://github.com/esp8266/Arduino/commit/5d5ea92a4d004ab009d5f642629946a0cb8893dd#diff-3fa12668b289ccb95b7ab334833a4ba8L35
// Core version 2.5.0 introduced EspClass helper method:
// https://github.com/esp8266/Arduino/commit/0e0e34c614fe8a47544c9998201b1d9b3c24eb18
extern "C" {
#include <cont.h>
#if defined(ARDUINO_ESP8266_RELEASE_2_3_0) \
|| defined(ARDUINO_ESP8266_RELEASE_2_4_0) \
|| defined(ARDUINO_ESP8266_RELEASE_2_4_1)
extern cont_t g_cont;
#define getFreeStack() cont_get_free_stack(&g_cont)
#elif defined(ARDUINO_ESP8266_RELEASE_2_4_2)
extern cont_t* g_pcont;
#define getFreeStack() cont_get_free_stack(g_pcont)
#else
#define getFreeStack() ESP.getFreeContStack()
#endif
}
// -----------------------------------------------------------------------------
// WIFI
// Domoticz
// -----------------------------------------------------------------------------
#include "JustWifi.h"
typedef std::function<void(justwifi_messages_t code, char * parameter)> wifi_callback_f;
void wifiRegister(wifi_callback_f callback);
#if DOMOTICZ_SUPPORT
template<typename T> void domoticzSend(const char * key, T value);
template<typename T> void domoticzSend(const char * key, T nvalue, const char * svalue);
#endif
// -----------------------------------------------------------------------------
// MQTT
// EEPROM_ROTATE
// -----------------------------------------------------------------------------
typedef std::function<void(unsigned int, const char *, const char *)> mqtt_callback_f;
void mqttRegister(mqtt_callback_f callback);
String mqttMagnitude(char * topic);
#include <EEPROM_Rotate.h>
EEPROM_Rotate EEPROMr;
// -----------------------------------------------------------------------------
// Broker
// -----------------------------------------------------------------------------
void brokerRegister(void (*)(const char *, unsigned char, const char *));
void eepromSectorsDebug();
// -----------------------------------------------------------------------------
// Settings
// GPIO
// -----------------------------------------------------------------------------
#include <Embedis.h>
template<typename T> bool setSetting(const String& key, T value);
template<typename T> bool setSetting(const String& key, unsigned int index, T value);
template<typename T> String getSetting(const String& key, T defaultValue);
template<typename T> String getSetting(const String& key, unsigned int index, T defaultValue);
void settingsGetJson(JsonObject& data);
bool settingsRestoreJson(JsonObject& data);
void settingsRegisterCommand(const String& name, void (*call)(Embedis*));
void settingsInject(void *data, size_t len);
Stream & settingsSerial();
bool gpioValid(unsigned char gpio);
bool gpioGetLock(unsigned char gpio);
bool gpioReleaseLock(unsigned char gpio);
// -----------------------------------------------------------------------------
// I2C
@ -101,23 +104,47 @@ int16_t i2c_read_int16_le(uint8_t address, uint8_t reg);
void i2c_read_buffer(uint8_t address, uint8_t * buffer, size_t len);
// -----------------------------------------------------------------------------
// GPIO
// MQTT
// -----------------------------------------------------------------------------
bool gpioValid(unsigned char gpio);
bool gpioGetLock(unsigned char gpio);
bool gpioReleaseLock(unsigned char gpio);
#if MQTT_SUPPORT
typedef std::function<void(unsigned int, const char *, const char *)> mqtt_callback_f;
void mqttRegister(mqtt_callback_f callback);
String mqttMagnitude(char * topic);
#else
#define mqtt_callback_f void *
#endif
// -----------------------------------------------------------------------------
// Debug
// OTA
// -----------------------------------------------------------------------------
void debugSend(const char * format, ...);
void debugSend_P(PGM_P format, ...);
#include "ESPAsyncTCP.h"
// -----------------------------------------------------------------------------
// Domoticz
// RFM69
// -----------------------------------------------------------------------------
template<typename T> void domoticzSend(const char * key, T value);
template<typename T> void domoticzSend(const char * key, T nvalue, const char * svalue);
typedef struct {
unsigned long messageID;
unsigned char packetID;
unsigned char senderID;
unsigned char targetID;
char * key;
char * value;
int16_t rssi;
} packet_t;
// -----------------------------------------------------------------------------
// Settings
// -----------------------------------------------------------------------------
#include <Embedis.h>
template<typename T> bool setSetting(const String& key, T value);
template<typename T> bool setSetting(const String& key, unsigned int index, T value);
template<typename T> String getSetting(const String& key, T defaultValue);
template<typename T> String getSetting(const String& key, unsigned int index, T defaultValue);
void settingsGetJson(JsonObject& data);
bool settingsRestoreJson(JsonObject& data);
void settingsRegisterCommand(const String& name, void (*call)(Embedis*));
void settingsInject(void *data, size_t len);
Stream & settingsSerial();
// -----------------------------------------------------------------------------
// Utils
@ -125,5 +152,46 @@ template<typename T> void domoticzSend(const char * key, T nvalue, const char *
char * ltrim(char * s);
void nice_delay(unsigned long ms);
#define ARRAYINIT(type, name, ...) \
type name[] = {__VA_ARGS__};
#define ARRAYINIT(type, name, ...) type name[] = {__VA_ARGS__};
// -----------------------------------------------------------------------------
// WebServer
// -----------------------------------------------------------------------------
#if WEB_SUPPORT
#include <ESPAsyncWebServer.h>
AsyncWebServer * webServer();
#else
#define AsyncWebServerRequest void
#define ArRequestHandlerFunction void
#define AsyncWebSocketClient void
#define AsyncWebSocket void
#define AwsEventType void *
#endif
typedef std::function<bool(AsyncWebServerRequest *request)> web_request_callback_f;
void webRequestRegister(web_request_callback_f callback);
// -----------------------------------------------------------------------------
// WebSockets
// -----------------------------------------------------------------------------
#if WEB_SUPPORT
typedef std::function<void(JsonObject&)> ws_on_send_callback_f;
void wsOnSendRegister(ws_on_send_callback_f callback);
void wsSend(ws_on_send_callback_f sender);
typedef std::function<void(uint32_t, const char *, JsonObject&)> ws_on_action_callback_f;
void wsOnActionRegister(ws_on_action_callback_f callback);
typedef std::function<bool(const char *, JsonVariant&)> ws_on_receive_callback_f;
void wsOnReceiveRegister(ws_on_receive_callback_f callback);
#else
#define ws_on_send_callback_f void *
#define ws_on_action_callback_f void *
#define ws_on_receive_callback_f void *
#endif
// -----------------------------------------------------------------------------
// WIFI
// -----------------------------------------------------------------------------
#include "JustWifi.h"
typedef std::function<void(justwifi_messages_t code, char * parameter)> wifi_callback_f;
void wifiRegister(wifi_callback_f callback);

+ 196
- 45
code/espurna/config/sensors.h View File

@ -5,13 +5,13 @@
#define SENSOR_DEBUG 0 // Debug sensors
#define SENSOR_READ_INTERVAL 6 // Read data from sensors every 6 seconds
#define SENSOR_READ_MIN_INTERVAL 6 // Minimum read interval
#define SENSOR_READ_MIN_INTERVAL 1 // Minimum read interval
#define SENSOR_READ_MAX_INTERVAL 3600 // Maximum read interval
#define SENSOR_INIT_INTERVAL 10000 // Try to re-init non-ready sensors every 10s
#define SENSOR_REPORT_EVERY 10 // Report every this many readings
#define SENSOR_REPORT_MIN_EVERY 1 // Minimum every value
#define SENSOR_REPORT_MAX_EVERY 12 // Maximum
#define SENSOR_REPORT_MAX_EVERY 60 // Maximum
#define SENSOR_USE_INDEX 0 // Use the index in topic (i.e. temperature/0)
// even if just one sensor (0 for backwards compatibility)
@ -36,6 +36,18 @@
#define HUMIDITY_MIN_CHANGE 0 // Minimum humidity change to report
#endif
#ifndef ENERGY_MAX_CHANGE
#define ENERGY_MAX_CHANGE 0 // Maximum energy change to report (if >0 it will allways report when delta(E) is greater than this)
#endif
#ifndef SENSOR_SAVE_EVERY
#define SENSOR_SAVE_EVERY 0 // Save accumulating values to EEPROM (atm only energy)
// A 0 means do not save and it's the default value
// A number different from 0 means it should store the value in EEPROM
// after these many reports
// Warning: this might wear out flash fast!
#endif
#define SENSOR_PUBLISH_ADDRESSES 0 // Publish sensor addresses
#define SENSOR_ADDRESS_TOPIC "address" // Topic to publish sensor addresses
@ -79,6 +91,14 @@
#define ANALOG_SUPPORT 0
#endif
#ifndef ANALOG_SAMPLES
#define ANALOG_SAMPLES 10 // Number of samples
#endif
#ifndef ANALOG_DELAY
#define ANALOG_DELAY 0 // Delay between samples in micros
#endif
//------------------------------------------------------------------------------
// BH1750
// Enable support by passing BH1750_SUPPORT=1 build flag
@ -225,7 +245,7 @@
#define EMON_FILTER_SPEED 512 // Mobile average filter speed
#define EMON_MAINS_VOLTAGE 230 // Mains voltage
#define EMON_REFERENCE_VOLTAGE 3.3 // Reference voltage of the ADC
#define EMON_CURRENT_RATIO 30 // Current ratio in the clamp (30V/1A)
#define EMON_CURRENT_RATIO 30 // Current ratio in the clamp (30A/1V)
#define EMON_REPORT_CURRENT 0 // Report current
#define EMON_REPORT_POWER 1 // Report power
#define EMON_REPORT_ENERGY 1 // Report energy
@ -273,6 +293,11 @@
#define EVENTS_SUPPORT 0 // Do not build with counter support by default
#endif
#ifndef EVENTS_TRIGGER
#define EVENTS_TRIGGER 1 // 1 to trigger callback on events,
// 0 to only count them and report periodically
#endif
#ifndef EVENTS_PIN
#define EVENTS_PIN 2 // GPIO to monitor
#endif
@ -282,7 +307,7 @@
#endif
#ifndef EVENTS_INTERRUPT_MODE
#define EVENTS_INTERRUPT_MODE RISING // RISING, FALLING, BOTH
#define EVENTS_INTERRUPT_MODE RISING // RISING, FALLING, CHANGE
#endif
#define EVENTS_DEBOUNCE 50 // Do not register events within less than 50 millis
@ -305,7 +330,7 @@
#endif
#ifndef GEIGER_INTERRUPT_MODE
#define GEIGER_INTERRUPT_MODE RISING // RISING, FALLING, BOTH
#define GEIGER_INTERRUPT_MODE RISING // RISING, FALLING, CHANGE
#endif
#define GEIGER_DEBOUNCE 25 // Do not register events within less than 25 millis.
@ -329,23 +354,6 @@
#define GUVAS12SD_PIN 14
#endif
//------------------------------------------------------------------------------
// HC-SR04
// Enable support by passing HCSR04_SUPPORT=1 build flag
//------------------------------------------------------------------------------
#ifndef HCSR04_SUPPORT
#define HCSR04_SUPPORT 0
#endif
#ifndef HCSR04_TRIGGER
#define HCSR04_TRIGGER 12 // GPIO for the trigger pin (output)
#endif
#ifndef HCSR04_ECHO
#define HCSR04_ECHO 14 // GPIO for the echo pin (input)
#endif
//------------------------------------------------------------------------------
// HLW8012 Energy monitor IC
// Enable support by passing HLW8012_SUPPORT=1 build flag
@ -422,6 +430,99 @@
#define MHZ19_TX_PIN 15
#endif
//------------------------------------------------------------------------------
// MICS-2710 (and MICS-4514) NO2 sensor
// Enable support by passing MICS2710_SUPPORT=1 build flag
//------------------------------------------------------------------------------
#ifndef MICS2710_SUPPORT
#define MICS2710_SUPPORT 0
#endif
#ifndef MICS2710_NOX_PIN
#define MICS2710_NOX_PIN 0
#endif
#ifndef MICS2710_PRE_PIN
#define MICS2710_PRE_PIN 4
#endif
#define MICS2710_PREHEAT_TIME 10000 // 10s preheat for NOX read
#define MICS2710_RL 820 // RL, load resistor
#define MICS2710_R0 2200 // R0 calibration value for NO2 sensor,
// Typical value as per datasheet
//------------------------------------------------------------------------------
// MICS-5525 (and MICS-4514) CO sensor
// Enable support by passing MICS5525_SUPPORT=1 build flag
//------------------------------------------------------------------------------
#ifndef MICS5525_SUPPORT
#define MICS5525_SUPPORT 0
#endif
#ifndef MICS5525_RED_PIN
#define MICS5525_RED_PIN 0
#endif
#define MICS5525_RL 820 // RL, load resistor
#define MICS5525_R0 750000 // R0 calibration value for NO2 sensor,
// Typical value as per datasheet
//------------------------------------------------------------------------------
// NTC sensor
// Enable support by passing NTC_SUPPORT=1 build flag
//--------------------------------------------------------------------------------
#ifndef NTC_SUPPORT
#define NTC_SUPPORT 0
#endif
#ifndef NTC_SAMPLES
#define NTC_SAMPLES 10 // Number of samples
#endif
#ifndef NTC_DELAY
#define NTC_DELAY 0 // Delay between samples in micros
#endif
#ifndef NTC_R_UP
#define NTC_R_UP 0 // Resistor upstream, set to 0 if none
#endif
#ifndef NTC_R_DOWN
#define NTC_R_DOWN 10000 // Resistor downstream, set to 0 if none
#endif
#ifndef NTC_T0
#define NTC_T0 298.15 // 25 Celsius
#endif
#ifndef NTC_R0
#define NTC_R0 10000 // Resistance at T0
#endif
#ifndef NTC_BETA
#define NTC_BETA 3977 // Beta coeficient
#endif
//------------------------------------------------------------------------------
// SDS011 particulates sensor
// Enable support by passing SDS011_SUPPORT=1 build flag
//------------------------------------------------------------------------------
#ifndef SDS011_SUPPORT
#define SDS011_SUPPORT 0
#endif
#ifndef SDS011_RX_PIN
#define SDS011_RX_PIN 14
#endif
#ifndef SDS011_TX_PIN
#define SDS011_TX_PIN 12
#endif
//------------------------------------------------------------------------------
// SenseAir CO2 sensor
// Enable support by passing SENSEAIR_SUPPORT=1 build flag
@ -459,14 +560,21 @@
#define PMS_SMART_SLEEP 0
#endif
#ifndef PMS_USE_SOFT
#define PMS_USE_SOFT 0 // If PMS_USE_SOFT == 1, DEBUG_SERIAL_SUPPORT must be 0
#endif
#ifndef PMS_RX_PIN
#define PMS_RX_PIN 13
#define PMS_RX_PIN 13 // Software serial RX GPIO (if PMS_USE_SOFT == 1)
#endif
#ifndef PMS_TX_PIN
#define PMS_TX_PIN 15
#define PMS_TX_PIN 15 // Software serial TX GPIO (if PMS_USE_SOFT == 1)
#endif
#ifndef PMS_HW_PORT
#define PMS_HW_PORT Serial // Hardware serial port (if PMS_USE_SOFT == 0)
#endif
//------------------------------------------------------------------------------
// PZEM004T based power monitor
// Enable support by passing PZEM004T_SUPPORT=1 build flag
@ -492,9 +600,21 @@
#define PZEM004T_HW_PORT Serial // Hardware serial port (if PZEM004T_USE_SOFT == 0)
#endif
#ifndef PZEM004T_ADDRESSES
#define PZEM004T_ADDRESSES "192.168.1.1" // Device(s) address(es), separated by space, "192.168.1.1 192.168.1.2 192.168.1.3"
#endif
#ifndef PZEM004T_READ_INTERVAL
#define PZEM004T_READ_INTERVAL 1500 // Read interval between same device
#endif
#ifndef PZEM004T_MAX_DEVICES
#define PZEM004T_MAX_DEVICES 3
#endif
//------------------------------------------------------------------------------
// SHT3X I2C (Wemos) temperature & humidity sensor
// Enable support by passing SHT3X_SUPPORT=1 build flag
// Enable support by passing SHT3X_I2C_SUPPORT=1 build flag
//------------------------------------------------------------------------------
#ifndef SHT3X_I2C_SUPPORT
@ -518,6 +638,31 @@
#define SI7021_ADDRESS 0x00 // 0x00 means auto
#endif
//------------------------------------------------------------------------------
// Sonar
// Enable support by passing SONAR_SUPPORT=1 build flag
//------------------------------------------------------------------------------
#ifndef SONAR_SUPPORT
#define SONAR_SUPPORT 0
#endif
#ifndef SONAR_TRIGGER
#define SONAR_TRIGGER 12 // GPIO for the trigger pin (output)
#endif
#ifndef SONAR_ECHO
#define SONAR_ECHO 14 // GPIO for the echo pin (input)
#endif
#ifndef SONAR_MAX_DISTANCE
#define SONAR_MAX_DISTANCE MAX_SENSOR_DISTANCE // Max sensor distance in cm
#endif
#ifndef SONAR_ITERATIONS
#define SONAR_ITERATIONS 5 // Number of iterations to ping for
#endif // error correction.
//------------------------------------------------------------------------------
// TMP3X analog temperature sensor
// Enable support by passing TMP3X_SUPPORT=1 build flag
@ -578,14 +723,18 @@
EVENTS_SUPPORT || \
GEIGER_SUPPORT || \
GUVAS12SD_SUPPORT || \
HCSR04_SUPPORT || \
HLW8012_SUPPORT || \
MICS2710_SUPPORT || \
MICS5525_SUPPORT || \
MHZ19_SUPPORT || \
NTC_SUPPORT || \
SDS011_SUPPORT || \
SENSEAIR_SUPPORT || \
PMSX003_SUPPORT || \
PZEM004T_SUPPORT || \
SHT3X_I2C_SUPPORT || \
SI7021_SUPPORT || \
SONAR_SUPPORT || \
TMP3X_SUPPORT || \
V9261F_SUPPORT \
)
@ -629,12 +778,6 @@
#if SENSOR_SUPPORT
#if SENSOR_DEBUG
#include "../config/debug.h"
#endif
#include "../sensors/BaseSensor.h"
#if AM2320_SUPPORT
#include "../sensors/AM2320Sensor.h"
#endif
@ -652,12 +795,10 @@
#endif
#if CSE7766_SUPPORT
#include <SoftwareSerial.h>
#include "../sensors/CSE7766Sensor.h"
#endif
#if DALLAS_SUPPORT
#include <OneWire.h>
#include "../sensors/DallasSensor.h"
#endif
@ -690,39 +831,46 @@
#endif
#if GEIGER_SUPPORT
#include "../sensors/GeigerSensor.h" // The main file for geiger counting module
#include "../sensors/GeigerSensor.h"
#endif
#if GUVAS12SD_SUPPORT
#include "../sensors/GUVAS12SDSensor.h"
#endif
#if HCSR04_SUPPORT
#include "../sensors/HCSR04Sensor.h"
#endif
#if HLW8012_SUPPORT
#include <HLW8012.h>
#include "../sensors/HLW8012Sensor.h"
#endif
#if MHZ19_SUPPORT
#include <SoftwareSerial.h>
#include "../sensors/MHZ19Sensor.h"
#endif
#if MICS2710_SUPPORT
#include "../sensors/MICS2710Sensor.h"
#endif
#if MICS5525_SUPPORT
#include "../sensors/MICS5525Sensor.h"
#endif
#if NTC_SUPPORT
#include "../sensors/NTCSensor.h"
#endif
#if SDS011_SUPPORT
#include "../sensors/SDS011Sensor.h"
#endif
#if SENSEAIR_SUPPORT
#include <SoftwareSerial.h>
#include "../sensors/SenseAirSensor.h"
#endif
#if PMSX003_SUPPORT
#include <SoftwareSerial.h>
#include "../sensors/PMSX003Sensor.h"
#endif
#if PZEM004T_SUPPORT
#include <SoftwareSerial.h>
#include "../sensors/PZEM004TSensor.h"
#endif
@ -734,12 +882,15 @@
#include "../sensors/SHT3XI2CSensor.h"
#endif
#if SONAR_SUPPORT
#include "../sensors/SonarSensor.h"
#endif
#if TMP3X_SUPPORT
#include "../sensors/TMP3XSensor.h"
#endif
#if V9261F_SUPPORT
#include <SoftwareSerial.h>
#include "../sensors/V9261FSensor.h"
#endif


+ 18
- 3
code/espurna/config/types.h View File

@ -48,6 +48,13 @@
#define BUTTON_SET_PULLUP 4
#endif
//------------------------------------------------------------------------------
// ENCODER
//------------------------------------------------------------------------------
#define ENCODER_MODE_CHANNEL 0
#define ENCODER_MODE_RATIO 1
//------------------------------------------------------------------------------
// RELAY
//------------------------------------------------------------------------------
@ -265,9 +272,13 @@
#define SENSOR_GUVAS12SD_ID 0x20
#define SENSOR_CSE7766_ID 0x21
#define SENSOR_TMP3X_ID 0x22
#define SENSOR_HCSR04_ID 0x23
#define SENSOR_SONAR_ID 0x23
#define SENSOR_SENSEAIR_ID 0x24
#define SENSOR_GEIGER_ID 0x25
#define SENSOR_NTC_ID 0x26
#define SENSOR_SDS011_ID 0x27
#define SENSOR_MICS2710_ID 0x28
#define SENSOR_MICS5525_ID 0x29
//--------------------------------------------------------------------------------
// Magnitudes
@ -287,7 +298,7 @@
#define MAGNITUDE_ENERGY_DELTA 11
#define MAGNITUDE_ANALOG 12
#define MAGNITUDE_DIGITAL 13
#define MAGNITUDE_EVENTS 14
#define MAGNITUDE_EVENT 14
#define MAGNITUDE_PM1dot0 15
#define MAGNITUDE_PM2dot5 16
#define MAGNITUDE_PM10 17
@ -298,5 +309,9 @@
#define MAGNITUDE_HCHO 22
#define MAGNITUDE_GEIGER_CPM 23
#define MAGNITUDE_GEIGER_SIEVERT 24
#define MAGNITUDE_COUNT 25
#define MAGNITUDE_NO2 26
#define MAGNITUDE_CO 27
#define MAGNITUDE_RESISTANCE 28
#define MAGNITUDE_MAX 25
#define MAGNITUDE_MAX 29

+ 1
- 2
code/espurna/config/version.h View File

@ -1,6 +1,5 @@
#define APP_NAME "ESPURNA"
#define APP_VERSION "1.13.1a"
#define APP_REVISION "d543fee"
#define APP_VERSION "1.13.3"
#define APP_AUTHOR "xose.perez@gmail.com"
#define APP_WEBSITE "http://tinkerman.cat"
#define CFG_VERSION 3

+ 73
- 0
code/espurna/config/webui.h View File

@ -0,0 +1,73 @@
// -----------------------------------------------------------------------------
// WEB UI IMAGE
// -----------------------------------------------------------------------------
#define WEBUI_IMAGE_SMALL 0
#define WEBUI_IMAGE_LIGHT 1
#define WEBUI_IMAGE_SENSOR 2
#define WEBUI_IMAGE_RFBRIDGE 4
#define WEBUI_IMAGE_RFM69 8
#define WEBUI_IMAGE_FULL 15
#if LIGHT_PROVIDER != LIGHT_PROVIDER_NONE
#ifdef WEBUI_IMAGE
#undef WEBUI_IMAGE
#define WEBUI_IMAGE WEBUI_IMAGE_FULL
#else
#define WEBUI_IMAGE WEBUI_IMAGE_LIGHT
#endif
#endif
#if SENSOR_SUPPORT == 1
#ifndef WEBUI_IMAGE
#define WEBUI_IMAGE WEBUI_IMAGE_SENSOR
#else
#undef WEBUI_IMAGE
#define WEBUI_IMAGE WEBUI_IMAGE_FULL
#endif
#endif
#if defined(ITEAD_SONOFF_RFBRIDGE)
#ifndef WEBUI_IMAGE
#define WEBUI_IMAGE WEBUI_IMAGE_RFBRIDGE
#else
#undef WEBUI_IMAGE
#define WEBUI_IMAGE WEBUI_IMAGE_FULL
#endif
#endif
#if RFM69_SUPPORT == 1
#ifndef WEBUI_IMAGE
#define WEBUI_IMAGE WEBUI_IMAGE_RFM69
#else
#undef WEBUI_IMAGE
#define WEBUI_IMAGE WEBUI_IMAGE_FULL
#endif
#endif
#ifndef WEBUI_IMAGE
#define WEBUI_IMAGE WEBUI_IMAGE_SMALL
#endif
#include <pgmspace.h>
PROGMEM const char espurna_webui[] =
#if WEBUI_IMAGE == WEBUI_IMAGE_SMALL
"SMALL"
#endif
#if WEBUI_IMAGE == WEBUI_IMAGE_LIGHT
"LIGHT"
#endif
#if WEBUI_IMAGE == WEBUI_IMAGE_SENSOR
"SENSOR"
#endif
#if WEBUI_IMAGE == WEBUI_IMAGE_RFBRIDGE
"RFBRIDGE"
#endif
#if WEBUI_IMAGE == WEBUI_IMAGE_RFM69
"RFM69"
#endif
#if WEBUI_IMAGE == WEBUI_IMAGE_FULL
"FULL"
#endif
"";

BIN
code/espurna/data/index.all.html.gz View File


BIN
code/espurna/data/index.light.html.gz View File


BIN
code/espurna/data/index.rfbridge.html.gz View File


BIN
code/espurna/data/index.rfm69.html.gz View File


BIN
code/espurna/data/index.sensor.html.gz View File


BIN
code/espurna/data/index.small.html.gz View File


+ 18
- 2
code/espurna/debug.ino View File

@ -209,6 +209,11 @@ void debugSetup() {
*/
extern "C" void custom_crash_callback(struct rst_info * rst_info, uint32_t stack_start, uint32_t stack_end ) {
// Do not record crash data when resetting the board
if (checkNeedsReset()) {
return;
}
// This method assumes EEPROM has already been initialized
// which is the first thing ESPurna does
@ -231,9 +236,13 @@ extern "C" void custom_crash_callback(struct rst_info * rst_info, uint32_t stack
EEPROMr.put(SAVE_CRASH_EEPROM_OFFSET + SAVE_CRASH_STACK_START, stack_start);
EEPROMr.put(SAVE_CRASH_EEPROM_OFFSET + SAVE_CRASH_STACK_END, stack_end);
// write stack trace to EEPROM
// starting address of Embedis data plus reserve
const uint16_t settings_start = SPI_FLASH_SEC_SIZE - settingsSize() - 0x10;
// write stack trace to EEPROM and avoid overwriting settings
int16_t current_address = SAVE_CRASH_EEPROM_OFFSET + SAVE_CRASH_STACK_TRACE;
for (uint32_t i = stack_start; i < stack_end; i++) {
if (current_address >= settings_start) break;
byte* byteValue = (byte*) i;
EEPROMr.write(current_address++, *byteValue);
}
@ -273,16 +282,23 @@ void debugDumpCrashInfo() {
EEPROMr.get(SAVE_CRASH_EEPROM_OFFSET + SAVE_CRASH_EPC3, epc3);
EEPROMr.get(SAVE_CRASH_EEPROM_OFFSET + SAVE_CRASH_EXCVADDR, excvaddr);
EEPROMr.get(SAVE_CRASH_EEPROM_OFFSET + SAVE_CRASH_DEPC, depc);
DEBUG_MSG_P(PSTR("[DEBUG] epc1=0x%08x epc2=0x%08x epc3=0x%08x\n"), epc1, epc2, epc3);
DEBUG_MSG_P(PSTR("[DEBUG] excvaddr=0x%08x depc=0x%08x\n"), excvaddr, depc);
uint32_t stack_start, stack_end;
EEPROMr.get(SAVE_CRASH_EEPROM_OFFSET + SAVE_CRASH_STACK_START, stack_start);
EEPROMr.get(SAVE_CRASH_EEPROM_OFFSET + SAVE_CRASH_STACK_END, stack_end);
DEBUG_MSG_P(PSTR("[DEBUG] >>>stack>>>\n[DEBUG] "));
DEBUG_MSG_P(PSTR("[DEBUG] sp=0x%08x end=0x%08x\n"), stack_start, stack_end);
int16_t current_address = SAVE_CRASH_EEPROM_OFFSET + SAVE_CRASH_STACK_TRACE;
int16_t stack_len = stack_end - stack_start;
uint32_t stack_trace;
DEBUG_MSG_P(PSTR("[DEBUG] >>>stack>>>\n[DEBUG] "));
for (int16_t i = 0; i < stack_len; i += 0x10) {
DEBUG_MSG_P(PSTR("%08x: "), stack_start + i);
for (byte j = 0; j < 4; j++) {


+ 6
- 1
code/espurna/domoticz.ino View File

@ -157,13 +157,18 @@ unsigned int domoticzIdx(unsigned char relayID) {
}
void domoticzSetup() {
_domoticzConfigure();
#if WEB_SUPPORT
wsOnSendRegister(_domoticzWebSocketOnSend);
wsOnAfterParseRegister(_domoticzConfigure);
wsOnReceiveRegister(_domoticzWebSocketOnReceive);
#endif
// Callbacks
mqttRegister(_domoticzMqtt);
espurnaRegisterReload(_domoticzConfigure);
}
bool domoticzEnabled() {


+ 53
- 1
code/espurna/eeprom.ino View File

@ -8,7 +8,12 @@ EEPROM MODULE
// -----------------------------------------------------------------------------
bool eepromRotate(bool value) {
bool _eeprom_commit = false;
uint32_t _eeprom_commit_count = 0;
bool _eeprom_last_commit_result = false;
void eepromRotate(bool value) {
// Enable/disable EEPROM rotation only if we are using more sectors than the
// reserved by the memory layout
if (EEPROMr.size() > EEPROMr.reserved()) {
@ -21,6 +26,10 @@ bool eepromRotate(bool value) {
}
}
uint32_t eepromCurrent() {
return EEPROMr.current();
}
String eepromSectors() {
String response;
for (uint32_t i = 0; i < EEPROMr.size(); i++) {
@ -30,10 +39,44 @@ String eepromSectors() {
return response;
}
void eepromSectorsDebug() {
DEBUG_MSG_P(PSTR("[MAIN] EEPROM sectors: %s\n"), (char *) eepromSectors().c_str());
DEBUG_MSG_P(PSTR("[MAIN] EEPROM current: %lu\n"), eepromCurrent());
}
bool _eepromCommit() {
_eeprom_commit_count++;
_eeprom_last_commit_result = EEPROMr.commit();
return _eeprom_last_commit_result;
}
void eepromCommit() {
_eeprom_commit = true;
}
#if TERMINAL_SUPPORT
void _eepromInitCommands() {
settingsRegisterCommand(F("EEPROM"), [](Embedis* e) {
infoMemory("EEPROM", SPI_FLASH_SEC_SIZE, SPI_FLASH_SEC_SIZE - settingsSize());
eepromSectorsDebug();
if (_eeprom_commit_count > 0) {
DEBUG_MSG_P(PSTR("[MAIN] Commits done: %lu\n"), _eeprom_commit_count);
DEBUG_MSG_P(PSTR("[MAIN] Last result: %s\n"), _eeprom_last_commit_result ? "OK" : "ERROR");
}
DEBUG_MSG_P(PSTR("+OK\n"));
});
settingsRegisterCommand(F("EEPROM.COMMIT"), [](Embedis* e) {
const bool res = _eepromCommit();
if (res) {
DEBUG_MSG_P(PSTR("+OK\n"));
} else {
DEBUG_MSG_P(PSTR("-ERROR\n"));
}
});
settingsRegisterCommand(F("EEPROM.DUMP"), [](Embedis* e) {
EEPROMr.dump(settingsSerial());
DEBUG_MSG_P(PSTR("\n+OK\n"));
@ -60,6 +103,13 @@ void _eepromInitCommands() {
// -----------------------------------------------------------------------------
void eepromLoop() {
if (_eeprom_commit) {
_eepromCommit();
_eeprom_commit = false;
}
}
void eepromSetup() {
#ifdef EEPROM_ROTATE_SECTORS
@ -83,4 +133,6 @@ void eepromSetup() {
_eepromInitCommands();
#endif
espurnaRegisterLoop(eepromLoop);
}

+ 156
- 0
code/espurna/encoder.ino View File

@ -0,0 +1,156 @@
/*
ENCODER MODULE
Copyright (C) 2018 by Xose Pérez <xose dot perez at gmail dot com>
*/
#if ENCODER_SUPPORT && (LIGHT_PROVIDER != LIGHT_PROVIDER_NONE)
#include <Encoder.h>
#include <vector>
typedef struct {
Encoder * encoder;
unsigned char button_pin;
unsigned char button_logic;
unsigned char button_mode;
unsigned char mode;
unsigned char channel1; // default
unsigned char channel2; // only if button defined and pressed
} encoder_t;
std::vector<encoder_t> _encoders;
void _encoderConfigure() {
// Clean previous encoders
for (unsigned char i=0; i<_encoders.size(); i++) {
free(_encoders[i].encoder);
}
_encoders.clear();
// Load encoders
#if (ENCODER1_PIN1 != GPIO_NONE) && (ENCODER1_PIN2 != GPIO_NONE)
{
_encoders.push_back({
new Encoder(ENCODER1_PIN1, ENCODER1_PIN2),
ENCODER1_BUTTON_PIN, ENCODER1_BUTTON_LOGIC, ENCODER1_BUTTON_MODE, ENCODER1_MODE,
ENCODER1_CHANNEL1, ENCODER1_CHANNEL2
});
}
#endif
#if (ENCODER2_PIN1 != GPIO_NONE) && (ENCODER2_PIN2 != GPIO_NONE)
{
_encoders.push_back({
new Encoder(ENCODER2_PIN1, ENCODER2_PIN2),
ENCODER2_BUTTON_PIN, ENCODER2_BUTTON_LOGIC, ENCODER2_BUTTON_MODE, ENCODER2_MODE,
ENCODER2_CHANNEL1, ENCODER2_CHANNEL2
});
}
#endif
#if (ENCODER3_PIN1 != GPIO_NONE) && (ENCODER3_PIN2 != GPIO_NONE)
{
_encoders.push_back({
new Encoder(ENCODER3_PIN1, ENCODER3_PIN2),
ENCODER3_BUTTON_PIN, ENCODER3_BUTTON_LOGIC, ENCODER3_BUTTON_MODE, ENCODER3_MODE,
ENCODER3_CHANNEL1, ENCODER3_CHANNEL2
});
}
#endif
#if (ENCODER4_PIN1 != GPIO_NONE) && (ENCODER4_PIN2 != GPIO_NONE)
{
_encoders.push_back({
new Encoder(ENCODER4_PIN1, ENCODER4_PIN2),
ENCODER4_BUTTON_PIN, ENCODER4_BUTTON_LOGIC, ENCODER4_BUTTON_MODE, ENCODER4_MODE,
ENCODER4_CHANNEL1, ENCODER4_CHANNEL2
});
}
#endif
#if (ENCODER5_PIN1 != GPIO_NONE) && (ENCODER5_PIN2 != GPIO_NONE)
{
_encoders.push_back({
new Encoder(ENCODER5_PIN1, ENCODER5_PIN2),
ENCODER5_BUTTON_PIN, ENCODER5_BUTTON_LOGIC, ENCODER5_BUTTON_MODE, ENCODER5_MODE,
ENCODER5_CHANNEL1, ENCODER5_CHANNEL2
});
}
#endif
// Setup encoders
for (unsigned char i=0; i<_encoders.size(); i++) {
if (GPIO_NONE != _encoders[i].button_pin) {
pinMode(_encoders[i].button_pin, _encoders[i].button_mode);
}
}
}
void _encoderLoop() {
// for each encoder
for (unsigned char i=0; i<_encoders.size(); i++) {
// get encoder
encoder_t encoder = _encoders[i];
// read encoder
long delta = encoder.encoder->read();
encoder.encoder->write(0);
if (0 == delta) continue;
DEBUG_MSG_P(PSTR("[ENCODER] Delta: %d\n"), delta);
// action
if (encoder.button_pin == GPIO_NONE) {
// if there is no button, the encoder driver the CHANNEL1
lightChannelStep(encoder.channel1, delta);
} else {
// check if button is pressed
bool pressed = (digitalRead(encoder.button_pin) != encoder.button_logic);
if (ENCODER_MODE_CHANNEL == encoder.mode) {
// the button controls what channel we are changing
lightChannelStep(pressed ? encoder.channel2 : encoder.channel1, delta);
} if (ENCODER_MODE_RATIO == encoder.mode) {
// the button controls if we are changing the channel ratio or the overall brightness
if (pressed) {
lightChannelStep(encoder.channel1, delta);
lightChannelStep(encoder.channel2, -delta);
} else {
lightBrightnessStep(delta);
}
}
}
lightUpdate(true, true);
}
}
// -----------------------------------------------------------------------------
void encoderSetup() {
// Configure encoders
_encoderConfigure();
// Main callbacks
espurnaRegisterLoop(_encoderLoop);
espurnaRegisterReload(_encoderConfigure);
DEBUG_MSG_P(PSTR("[ENCODER] Number of encoders: %u\n"), _encoders.size());
}
#endif // ENCODER_SUPPORT && (LIGHT_PROVIDER != LIGHT_PROVIDER_NONE)

+ 32
- 6
code/espurna/espurna.ino View File

@ -23,15 +23,26 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
#include <vector>
std::vector<void (*)()> _loop_callbacks;
std::vector<void (*)()> _reload_callbacks;
// -----------------------------------------------------------------------------
// REGISTER
// GENERAL CALLBACKS
// -----------------------------------------------------------------------------
void espurnaRegisterLoop(void (*callback)()) {
_loop_callbacks.push_back(callback);
}
void espurnaRegisterReload(void (*callback)()) {
_reload_callbacks.push_back(callback);
}
void espurnaReload() {
for (unsigned char i = 0; i < _reload_callbacks.size(); i++) {
(_reload_callbacks[i])();
}
}
// -----------------------------------------------------------------------------
// BOOTING
// -----------------------------------------------------------------------------
@ -42,6 +53,9 @@ void setup() {
// Basic modules, will always run
// -------------------------------------------------------------------------
// Cache initial free heap value
getInitialFreeHeap();
// Serial debug
#if DEBUG_SUPPORT
debugSetup();
@ -49,7 +63,7 @@ void setup() {
// Init EEPROM
eepromSetup();
// Init Serial, SPIFFS and system check
systemSetup();
@ -87,20 +101,29 @@ void setup() {
#if WEB_SUPPORT
webSetup();
wsSetup();
apiSetup();
#if DEBUG_WEB_SUPPORT
debugWebSetup();
#endif
#endif
#if API_SUPPORT
apiSetup();
#endif
// lightSetup must be called before relaySetup
#if LIGHT_PROVIDER != LIGHT_PROVIDER_NONE
lightSetup();
#endif
relaySetup();
buttonSetup();
ledSetup();
#if BUTTON_SUPPORT
buttonSetup();
#endif
#if ENCODER_SUPPORT && (LIGHT_PROVIDER != LIGHT_PROVIDER_NONE)
encoderSetup();
#endif
#if LED_SUPPORT
ledSetup();
#endif
#if MQTT_SUPPORT
mqttSetup();
#endif
@ -140,6 +163,9 @@ void setup() {
#if THINGSPEAK_SUPPORT
tspkSetup();
#endif
#if RFM69_SUPPORT
rfm69Setup();
#endif
#if RF_SUPPORT
rfSetup();
#endif


+ 40
- 0
code/espurna/filters/LastFilter.h View File

@ -0,0 +1,40 @@
// -----------------------------------------------------------------------------
// Last Filter
// Copyright (C) 2017-2018 by Xose Pérez <xose dot perez at gmail dot com>
// -----------------------------------------------------------------------------
#if SENSOR_SUPPORT
#pragma once
#include "BaseFilter.h"
class LastFilter : public BaseFilter {
public:
void add(double value) {
_value = value;
}
unsigned char count() {
return 1;
}
void reset() {
_value = 0;
}
double result() {
return _value;
}
void resize(unsigned char size) {}
protected:
double _value = 0;
};
#endif // SENSOR_SUPPORT

+ 33
- 16
code/espurna/homeassistant.ino View File

@ -13,6 +13,17 @@ Copyright (C) 2017-2018 by Xose Pérez <xose dot perez at gmail dot com>
bool _haEnabled = false;
bool _haSendFlag = false;
// -----------------------------------------------------------------------------
// UTILS
// -----------------------------------------------------------------------------
String _haFixName(String name) {
for (unsigned char i=0; i<name.length(); i++) {
if (!isalnum(name.charAt(i))) name.setCharAt(i, '_');
}
return name;
}
// -----------------------------------------------------------------------------
// SENSORS
// -----------------------------------------------------------------------------
@ -22,7 +33,7 @@ bool _haSendFlag = false;
void _haSendMagnitude(unsigned char i, JsonObject& config) {
unsigned char type = magnitudeType(i);
config["name"] = getSetting("hostname") + String(" ") + magnitudeTopic(type);
config["name"] = _haFixName(getSetting("hostname") + String(" ") + magnitudeTopic(type));
config.set("platform", "mqtt");
config["state_topic"] = mqttTopic(magnitudeTopicIndex(i).c_str(), false);
config["unit_of_measurement"] = magnitudeUnits(type);
@ -64,20 +75,20 @@ void _haSendSwitch(unsigned char i, JsonObject& config) {
String name = getSetting("hostname");
if (relayCount() > 1) {
name += String(" #") + String(i);
name += String("_") + String(i);
}
config.set("name", name);
config.set("name", _haFixName(name));
config.set("platform", "mqtt");
if (relayCount()) {
config["state_topic"] = mqttTopic(MQTT_TOPIC_RELAY, i, false);
config["command_topic"] = mqttTopic(MQTT_TOPIC_RELAY, i, true);
config["payload_on"] = String("1");
config["payload_off"] = String("0");
config["payload_on"] = String(HOMEASSISTANT_PAYLOAD_ON);
config["payload_off"] = String(HOMEASSISTANT_PAYLOAD_OFF);
config["availability_topic"] = mqttTopic(MQTT_TOPIC_STATUS, false);
config["payload_available"] = String("1");
config["payload_not_available"] = String("0");
config["payload_available"] = String(HOMEASSISTANT_PAYLOAD_AVAILABLE);
config["payload_not_available"] = String(HOMEASSISTANT_PAYLOAD_NOT_AVAILABLE);
}
#if LIGHT_PROVIDER != LIGHT_PROVIDER_NONE
@ -152,7 +163,7 @@ String _haGetConfig() {
JsonObject& config = jsonBuffer.createObject();
_haSendSwitch(i, config);
output += type + ":\n";
output += "\n" + type + ":\n";
bool first = true;
for (auto kv : config) {
if (first) {
@ -163,7 +174,6 @@ String _haGetConfig() {
}
output += kv.key + String(": ") + kv.value.as<String>() + String("\n");
}
output += "\n";
jsonBuffer.clear();
@ -177,7 +187,7 @@ String _haGetConfig() {
JsonObject& config = jsonBuffer.createObject();
_haSendMagnitude(i, config);
output += "sensor:\n";
output += "\nsensor:\n";
bool first = true;
for (auto kv : config) {
if (first) {
@ -186,7 +196,9 @@ String _haGetConfig() {
} else {
output += " ";
}
output += kv.key + String(": ") + kv.value.as<String>() + String("\n");
String value = kv.value.as<String>();
value.replace("%", "'%'");
output += kv.key + String(": ") + value + String("\n");
}
output += "\n";
@ -254,10 +266,12 @@ void _haWebSocketOnAction(uint32_t client_id, const char * action, JsonObject& d
#if TERMINAL_SUPPORT
void _haInitCommands() {
settingsRegisterCommand(F("HA.CONFIG"), [](Embedis* e) {
DEBUG_MSG(_haGetConfig().c_str());
DEBUG_MSG_P(PSTR("+OK\n"));
});
settingsRegisterCommand(F("HA.SEND"), [](Embedis* e) {
setSetting("haEnabled", "1");
_haConfigure();
@ -266,6 +280,7 @@ void _haInitCommands() {
#endif
DEBUG_MSG_P(PSTR("+OK\n"));
});
settingsRegisterCommand(F("HA.CLEAR"), [](Embedis* e) {
setSetting("haEnabled", "0");
_haConfigure();
@ -274,6 +289,7 @@ void _haInitCommands() {
#endif
DEBUG_MSG_P(PSTR("+OK\n"));
});
}
#endif
@ -286,20 +302,21 @@ void haSetup() {
#if WEB_SUPPORT
wsOnSendRegister(_haWebSocketOnSend);
wsOnAfterParseRegister(_haConfigure);
wsOnActionRegister(_haWebSocketOnAction);
wsOnReceiveRegister(_haWebSocketOnReceive);
#endif
#if TERMINAL_SUPPORT
_haInitCommands();
#endif
// On MQTT connect check if we have something to send
mqttRegister([](unsigned int type, const char * topic, const char * payload) {
if (type == MQTT_CONNECT_EVENT) _haSend();
});
#if TERMINAL_SUPPORT
_haInitCommands();
#endif
// Main callbacks
espurnaRegisterReload(_haConfigure);
}


+ 2
- 2
code/espurna/i2c.ino View File

@ -230,7 +230,7 @@ uint16_t i2c_read_uint16(uint8_t address, uint8_t reg) {
void i2c_read_buffer(uint8_t address, uint8_t * buffer, size_t len) {
Wire.beginTransmission((uint8_t) address);
Wire.requestFrom(address, (uint8_t) len);
for (int i=0; i<len; i++) buffer[i] = Wire.read();
for (size_t i=0; i<len; i++) buffer[i] = Wire.read();
Wire.endTransmission();
}
@ -332,7 +332,7 @@ unsigned char i2cFind(size_t size, unsigned char * addresses) {
unsigned char i2cFindAndLock(size_t size, unsigned char * addresses) {
unsigned char start = 0;
unsigned char address = 0;
while (address = i2cFind(size, addresses, start)) {
while ((address = i2cFind(size, addresses, start))) {
if (i2cGetLock(address)) break;
start++;
}


+ 10
- 5
code/espurna/influxdb.ino View File

@ -51,7 +51,7 @@ bool idbSend(const char * topic, const char * payload) {
#endif
char * host = strdup(h.c_str());
unsigned int port = getSetting("idbPort", INFLUXDB_PORT).toInt();
DEBUG_MSG("[INFLUXDB] Sending to %s:%u\n", host, port);
DEBUG_MSG_P(PSTR("[INFLUXDB] Sending to %s:%u\n"), host, port);
bool success = false;
@ -60,7 +60,7 @@ bool idbSend(const char * topic, const char * payload) {
char data[128];
snprintf(data, sizeof(data), "%s,device=%s value=%s", topic, getSetting("hostname").c_str(), String(payload).c_str());
DEBUG_MSG("[INFLUXDB] Data: %s\n", data);
DEBUG_MSG_P(PSTR("[INFLUXDB] Data: %s\n"), data);
char request[256];
snprintf(request, sizeof(request), "POST /write?db=%s&u=%s&p=%s HTTP/1.1\r\nHost: %s:%u\r\nContent-Length: %d\r\n\r\n%s",
@ -74,14 +74,14 @@ bool idbSend(const char * topic, const char * payload) {
if (_idb_client.connected()) _idb_client.stop();
success = true;
} else {
DEBUG_MSG("[INFLUXDB] Sent failed\n");
DEBUG_MSG_P(PSTR("[INFLUXDB] Sent failed\n"));
}
_idb_client.stop();
while (_idb_client.connected()) yield();
} else {
DEBUG_MSG("[INFLUXDB] Connection failed\n");
DEBUG_MSG_P(PSTR("[INFLUXDB] Connection failed\n"));
}
free(host);
@ -100,12 +100,17 @@ bool idbEnabled() {
}
void idbSetup() {
_idbConfigure();
#if WEB_SUPPORT
wsOnSendRegister(_idbWebSocketOnSend);
wsOnAfterParseRegister(_idbConfigure);
wsOnReceiveRegister(_idbWebSocketOnReceive);
#endif
// Main callbacks
espurnaRegisterReload(_idbConfigure);
}
#endif

+ 364
- 69
code/espurna/ir.ino View File

@ -2,122 +2,417 @@
IR MODULE
Copyright (C) 2016-2018 by Xose Pérez <xose dot perez at gmail dot com>
Copyright (C) 2018 by Alexander Kolesnikov (raw and MQTT implementation)
Copyright (C) 2017-2018 by François Déchery
Copyright (C) 2016-2018 by Xose Pérez <xose dot perez at gmail dot com>
-----------------------------------------------------------------------------
Configuration
-----------------------------------------------------------------------------
To enable transmit functions define IR_TX_PIN
To enable receiver functions define IR_RX_PIN
MQTT input topic: {root}/irin
MQTT output topic: {root}/irout/set
--------------------------------------------------------------------------------
MQTT messages
--------------------------------------------------------------------------------
Decoded messages:
Transmitting:
Payload: 2:121944:32:1 (<type>:<code>:<bits>[:<repeat>])
The repeat value is optional and defaults to 1
Receiving:
Payload: 2:121944:32 (<type>:<code>:<bits>)
Raw messages:
Transmitting:
Payload: 1000,1000,1000,1000,1000,DELAY,COUNT,FREQ:500,500,500,500,500
| IR codes | | IR repeat codes |
codes - time in microseconds when IR LED On/Off. First value - ON, second - Off ...
DELAY - delay in milliseconds between sending repeats
COUNT - how many repeats send. Max 120.
FREQ - modulation frequency. Usually 38kHz. You may set 38, it means 38kHz or set 38000, it meant same.
Repeat codes is optional. You may omit ":" and codes. In this case if repeat count > 0 we repeat main code.
Receiving:
Payload: 1000,1000,1000,1000,1000
| IR codes |
--------------------------------------------------------------------------------
*/
#if IR_SUPPORT
#include <IRremoteESP8266.h>
#include <IRrecv.h>
IRrecv * _ir_recv;
decode_results _ir_results;
unsigned long _ir_last_toggle = 0;
#if defined(IR_RX_PIN)
// -----------------------------------------------------------------------------
// PRIVATE
// -----------------------------------------------------------------------------
#include <IRrecv.h>
IRrecv _ir_receiver(IR_RX_PIN, IR_BUFFER_SIZE, IR_TIMEOUT, true);
decode_results _ir_results;
#endif // defined(IR_RX_PIN)
#if defined(IR_TX_PIN)
#include <IRsend.h>
IRsend _ir_sender(IR_TX_PIN);
#if IR_USE_RAW
uint16_t _ir_freq = 38; // IR modulation freq. for sending codes and repeat codes
uint8_t _ir_repeat_size = 0; // size of repeat array
uint16_t * _ir_raw; // array for sending codes and repeat codes
#else
uint8_t _ir_type = 0; // Type of encoding
uint64_t _ir_code = 0; // Code to transmit
uint16_t _ir_bits = 0; // Code bits
#endif
void _irProcessCode(unsigned long code) {
uint8_t _ir_repeat = 0; // count of times repeating of repeat_code
uint32_t _ir_delay = IR_DELAY; // delay between repeat codes
static unsigned long last_code;
boolean found = false;
#endif // defined(IR_TX_PIN)
// Repeat last valid code
DEBUG_MSG_P(PSTR("[IR] Received 0x%06X\n"), code);
if (code == 0xFFFFFFFF) {
DEBUG_MSG_P(PSTR("[IR] Processing 0x%06X\n"), code);
code = last_code;
// MQTT to IR
#if MQTT_SUPPORT && defined(IR_TX_PIN)
void _irMqttCallback(unsigned int type, const char * topic, const char * payload) {
if (type == MQTT_CONNECT_EVENT) {
mqttSubscribe(MQTT_TOPIC_IROUT);
}
for (unsigned char i = 0; i < IR_BUTTON_COUNT ; i++) {
if (type == MQTT_MESSAGE_EVENT) {
String t = mqttMagnitude((char *) topic);
unsigned long button_code = pgm_read_dword(&IR_BUTTON[i][0]);
if (code == button_code) {
// Match topic
if (t.equals(MQTT_TOPIC_IROUT)) {
unsigned long button_mode = pgm_read_dword(&IR_BUTTON[i][1]);
unsigned long button_value = pgm_read_dword(&IR_BUTTON[i][2]);
String data = String(payload);
unsigned int len = data.length();
int col = data.indexOf(":"); // position of ":" which means repeat_code
if (button_mode == IR_BUTTON_MODE_STATE) {
relayStatus(0, button_value);
}
#if IR_USE_RAW
if (button_mode == IR_BUTTON_MODE_TOGGLE) {
unsigned char count = 1; // count of code values for allocating array
if (millis() - _ir_last_toggle > 250){
relayToggle(button_value);
_ir_last_toggle = millis();
} else {
DEBUG_MSG_P(PSTR("[IR] Ignoring repeated code\n"));
}
}
if (col > 2) { // count & validating repeat code
_ir_repeat_size = 1;
// count & validate repeat-string
for(unsigned int i = col+1; i < len; i++) {
if (i < len-1) {
if ( payload[i] == ',' && isDigit(payload[i+1]) && i>0 ) { //validate string
_ir_repeat_size++;
} else if (!isDigit(payload[i])) {
// Error in repeat_code. Use comma separated unsigned integer values.
// Last three is repeat delay, repeat count(<120) and frequency.
// After all you may write ':' and specify repeat code followed by comma.
DEBUG_MSG_P(PSTR("[IR] Error in repeat code.\n"));
return;
}
}
}
#if LIGHT_PROVIDER != LIGHT_PROVIDER_NONE
len = col; //cut repeat code from main code processing
} // end of counting & validating repeat code
// count & validate main code string
for(unsigned int i = 0; i < len; i++) {
if (i<len-1) {
if ( payload[i] == ',' && isDigit(payload[i+1]) && i>0 ) { //validate string
count++;
} else if (!isDigit(payload[i])) {
// Error in main code. Use comma separated unsigned integer values.
// Last three is repeat delay, repeat count(<120) and frequency.
// After all you may write ':' and specify repeat code followed by comma.
DEBUG_MSG_P(PSTR("[IR] Error in main code.\n"));
return;
}
}
if (button_mode == IR_BUTTON_MODE_BRIGHTER) {
lightBrightnessStep(button_value ? 1 : -1);
nice_delay(150); //debounce
}
if (button_mode == IR_BUTTON_MODE_RGB) {
lightColor(button_value);
_ir_raw = (uint16_t*)calloc(count, sizeof(uint16_t)); // allocating array for main codes
String value = ""; // for populating values of array from comma separated string
int j = 0; // for populating values of array from comma separated string
// populating main code array from part of MQTT string
for (unsigned int i = 0; i < len; i++) {
if (payload[i] != ',') {
value = value + data[i];
}
if ((payload[i] == ',') || (i == len - 1)) {
_ir_raw[j]= value.toInt();
value = "";
j++;
}
}
/*
#if LIGHT_PROVIDER == LIGHT_PROVIDER_FASTLED
if (button_mode == IR_BUTTON_MODE_EFFECT) {
_buttonAnimMode(button_value);
// if count>3 then we have values, repeat delay, count and modulation frequency
_ir_repeat=0;
if (count>3) {
if (_ir_raw[count-2] <= 120) { // if repeat count > 120 it's to long and ussualy unusual. maybe we get raw code without this parameters and just use defaults for freq.
_ir_freq = _ir_raw[count-1];
_ir_repeat = _ir_raw[count-2];
_ir_delay = _ir_raw[count-3];
count = count - 3;
}
}
DEBUG_MSG_P(PSTR("[IR] Raw IR output %d codes, repeat %d times on %d(k)Hz freq.\n"), count, _ir_repeat, _ir_freq);
#if defined(IR_RX_PIN)
_ir_receiver.disableIRIn();
#endif
*/
_ir_sender.sendRaw(_ir_raw, count, _ir_freq);
if (_ir_repeat==0) { // no repeat, cleaning array, enabling receiver
free(_ir_raw);
#if defined(IR_RX_PIN)
_ir_receiver.enableIRIn();
#endif
} else if (col>2) { // repeat with repeat_code
DEBUG_MSG_P(PSTR("[IR] Repeat codes count: %d\n"), _ir_repeat_size);
free(_ir_raw);
_ir_raw = (uint16_t*)calloc(_ir_repeat_size, sizeof(uint16_t));
String value = ""; // for populating values of array from comma separated string
int j = 0; // for populating values of array from comma separated string
len = data.length(); //redifining length to full lenght
// populating repeat code array from part of MQTT string
for (unsigned int i = col+1; i < len; i++) {
value = value + data[i];
if ((payload[i] == ',') || (i == len - 1)) {
_ir_raw[j]= value.toInt();
value = "";
j++;
}
}
} else { // if repeat code not specified (col<=2) repeat with current main code
_ir_repeat_size = count;
}
#else
_ir_repeat = 0;
if (col > 0) {
_ir_type = data.toInt();
_ir_code = strtoul(data.substring(col+1).c_str(), NULL, 10);
col = data.indexOf(":", col+1);
if (col > 0) {
_ir_bits = data.substring(col+1).toInt();
col = data.indexOf(":", col+1);
if (col > 2) {
_ir_repeat = data.substring(col+1).toInt();
} else {
_ir_repeat = IR_REPEAT;
}
}
}
/*
if (button_mode == IR_BUTTON_MODE_HSV) {
lightColor(button_value);
if (_ir_repeat > 0) {
DEBUG_MSG_P(PSTR("[IR] IROUT: %d:%lu:%d:%d\n"), _ir_type, (unsigned long) _ir_code, _ir_bits, _ir_repeat);
} else {
DEBUG_MSG_P(PSTR("[IR] Wrong MQTT payload format (%s)\n"), payload);
}
*/
lightUpdate(true, true);
#endif // IR_USE_RAW
} // end of match topic
} // end of MQTT message
} //end of function
void _irTXLoop() {
static uint32_t last = 0;
if ((_ir_repeat > 0) && (millis() - last > _ir_delay)) {
last = millis();
// Send message
#if IR_USE_RAW
_ir_sender.sendRaw(_ir_raw, _ir_repeat_size, _ir_freq);
#else
_ir_sender.send(_ir_type, _ir_code, _ir_bits);
#endif
// Update repeat count
--_ir_repeat;
if (0 == _ir_repeat) {
#if IR_USE_RAW
free(_ir_raw);
#endif
#if defined(IR_RX_PIN)
_ir_receiver.enableIRIn();
#endif
}
}
}
#endif // MQTT_SUPPORT && defined(IR_TX_PIN)
// Receiving
#if defined(IR_RX_PIN)
void _irProcess(unsigned char type, unsigned long code) {
#if IR_BUTTON_SET > 0
boolean found = false;
for (unsigned char i = 0; i < IR_BUTTON_COUNT ; i++) {
uint32_t button_code = pgm_read_dword(&IR_BUTTON[i][0]);
if (code == button_code) {
unsigned long button_mode = pgm_read_dword(&IR_BUTTON[i][1]);
unsigned long button_value = pgm_read_dword(&IR_BUTTON[i][2]);
if (button_mode == IR_BUTTON_MODE_STATE) {
relayStatus(0, button_value);
}
if (button_mode == IR_BUTTON_MODE_TOGGLE) {
relayToggle(button_value);
}
#if LIGHT_PROVIDER != LIGHT_PROVIDER_NONE
if (button_mode == IR_BUTTON_MODE_BRIGHTER) {
lightBrightnessStep(button_value ? 1 : -1);
nice_delay(150); //debounce
}
if (button_mode == IR_BUTTON_MODE_RGB) {
lightColor(button_value);
}
/*
#if LIGHT_PROVIDER == LIGHT_PROVIDER_FASTLED
if (button_mode == IR_BUTTON_MODE_EFFECT) {
_buttonAnimMode(button_value);
}
#endif
*/
/*
if (button_mode == IR_BUTTON_MODE_HSV) {
lightColor(button_value);
}
*/
lightUpdate(true, true);
found = true;
last_code = code;
break;
#endif
found = true;
break;
}
}
}
}
if (!found) {
DEBUG_MSG_P(PSTR("[IR] Code does not match any action\n"));
}
if (!found) {
DEBUG_MSG_P(PSTR("[IR] Ignoring code\n"));
}
#endif
}
void _irRXLoop() {
if (_ir_receiver.decode(&_ir_results)) {
_ir_receiver.resume(); // Receive the next value
// Debounce
static unsigned long last_time = 0;
if (millis() - last_time < IR_DEBOUNCE) return;
last_time = millis();
#if IR_USE_RAW
// Check code
if (_ir_results.rawlen < 1) return;
char * payload;
String value = "";
for (int i = 1; i < _ir_results.rawlen; i++) {
if (i>1) value = value + ",";
value = value + String(_ir_results.rawbuf[i] * RAWTICK);
}
payload = const_cast<char*>(value.c_str());
#else
// Check code
if (_ir_results.value < 1) return;
if (_ir_results.decode_type < 1) return;
if (_ir_results.bits < 1) return;
char payload[32];
snprintf_P(payload, sizeof(payload), PSTR("%u:%lu:%u"), _ir_results.decode_type, (unsigned long) _ir_results.value, _ir_results.bits);
#endif
DEBUG_MSG_P(PSTR("[IR] IRIN: %s\n"), payload);
#if not IR_USE_RAW
_irProcess(_ir_results.decode_type, (unsigned long) _ir_results.value);
#endif
#if MQTT_SUPPORT
if (strlen(payload)>0) {
mqttSend(MQTT_TOPIC_IRIN, (const char *) payload);
}
#endif
}
}
#endif // defined(IR_RX_PIN)
// -----------------------------------------------------------------------------
// PUBLIC API
// -----------------------------------------------------------------------------
void _irLoop() {
#if defined(IR_RX_PIN)
_irRXLoop();
#endif
#if MQTT_SUPPORT && defined(IR_TX_PIN)
_irTXLoop();
#endif
}
void irSetup() {
_ir_recv = new IRrecv(IR_PIN);
_ir_recv->enableIRIn();
#if defined(IR_RX_PIN)
_ir_receiver.enableIRIn();
DEBUG_MSG_P(PSTR("[IR] Receiver initialized \n"));
#endif
// Register loop
espurnaRegisterLoop(irLoop);
#if MQTT_SUPPORT && defined(IR_TX_PIN)
_ir_sender.begin();
mqttRegister(_irMqttCallback);
DEBUG_MSG_P(PSTR("[IR] Transmitter initialized \n"));
#endif
}
espurnaRegisterLoop(_irLoop);
void irLoop() {
if (_ir_recv->decode(&_ir_results)) {
unsigned long code = _ir_results.value;
_irProcessCode(code);
_ir_recv->resume(); // Receive the next value
}
}
#endif // IR_SUPPORT

+ 6
- 2
code/espurna/led.ino View File

@ -10,6 +10,8 @@ Copyright (C) 2016-2018 by Xose Pérez <xose dot perez at gmail dot com>
// LED
// -----------------------------------------------------------------------------
#if LED_SUPPORT
typedef struct {
unsigned char pin;
bool reverse;
@ -168,14 +170,14 @@ void ledSetup() {
#if WEB_SUPPORT
wsOnSendRegister(_ledWebSocketOnSend);
wsOnAfterParseRegister(_ledConfigure);
wsOnReceiveRegister(_ledWebSocketOnReceive);
#endif
DEBUG_MSG_P(PSTR("[LED] Number of leds: %d\n"), _leds.size());
// Register loop
// Main callbacks
espurnaRegisterLoop(ledLoop);
espurnaRegisterReload(_ledConfigure);
}
@ -290,3 +292,5 @@ void ledLoop() {
_led_update = false;
}
#endif // LED_SUPPORT

+ 65
- 0
code/espurna/libs/RFM69Wrap.h View File

@ -0,0 +1,65 @@
/*
RFM69Wrap
RFM69 by Felix Ruso (http://LowPowerLab.com/contact) wrapper for ESP8266
Copyright (C) 2016-2018 by Xose Pérez <xose dot perez at gmail dot com>
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#pragma once
#include <RFM69.h>
#include <RFM69_ATC.h>
#include <SPI.h>
class RFM69Wrap: public RFM69_ATC {
public:
RFM69Wrap(uint8_t slaveSelectPin=RF69_SPI_CS, uint8_t interruptPin=RF69_IRQ_PIN, bool isRFM69HW=false, uint8_t interruptNum=0):
RFM69_ATC(slaveSelectPin, interruptPin, isRFM69HW, interruptNum) {};
protected:
// overriding SPI_CLOCK for ESP8266
void select() {
noInterrupts();
#if defined (SPCR) && defined (SPSR)
// save current SPI settings
_SPCR = SPCR;
_SPSR = SPSR;
#endif
// set RFM69 SPI settings
SPI.setDataMode(SPI_MODE0);
SPI.setBitOrder(MSBFIRST);
#if defined(__arm__)
SPI.setClockDivider(SPI_CLOCK_DIV16);
#elif defined(ARDUINO_ARCH_ESP8266)
SPI.setClockDivider(SPI_CLOCK_DIV2); // speeding it up for the ESP8266
#else
SPI.setClockDivider(SPI_CLOCK_DIV4);
#endif
digitalWrite(_slaveSelectPin, LOW);
}
};

+ 101
- 81
code/espurna/light.ino View File

@ -145,13 +145,9 @@ void _generateBrightness() {
} else {
// Don't apply brightness, it is already in the target:
// Apply brightness equally to all channels
for (unsigned char i=0; i < _light_channel.size(); i++) {
if (_light_has_color & (i<3)) {
_light_channel[i].value = _light_channel[i].inputValue * brightness;
} else {
_light_channel[i].value = _light_channel[i].inputValue;
}
_light_channel[i].value = _light_channel[i].inputValue * brightness;
}
}
@ -283,13 +279,13 @@ void _fromKelvin(unsigned long kelvin) {
if (!_light_has_color) return;
_light_mireds = constrain(round(1000000UL / kelvin), LIGHT_MIN_MIREDS, LIGHT_MAX_MIREDS);
if (_light_use_cct) {
_setRGBInputValue(LIGHT_MAX_VALUE, LIGHT_MAX_VALUE, LIGHT_MAX_VALUE);
return;
}
_light_mireds = constrain(round(1000000UL / kelvin), LIGHT_MIN_MIREDS, LIGHT_MAX_MIREDS);
// Calculate colors
kelvin /= 100;
unsigned int red = (kelvin <= 66)
@ -569,6 +565,7 @@ void _lightMQTTCallback(unsigned int type, const char * topic, const char * payl
}
void lightMQTT() {
char buffer[20];
if (_light_has_color) {
@ -584,13 +581,10 @@ void lightMQTT() {
_toHSV(buffer, sizeof(buffer));
mqttSend(MQTT_TOPIC_COLOR_HSV, buffer);
// Brightness
snprintf_P(buffer, sizeof(buffer), PSTR("%d"), _light_brightness);
mqttSend(MQTT_TOPIC_BRIGHTNESS, buffer);
// Mireds
snprintf_P(buffer, sizeof(buffer), PSTR("%d"), _light_mireds);
mqttSend(MQTT_TOPIC_MIRED, buffer);
}
// Channels
@ -599,6 +593,10 @@ void lightMQTT() {
mqttSend(MQTT_TOPIC_CHANNEL, i, buffer);
}
// Brightness
snprintf_P(buffer, sizeof(buffer), PSTR("%d"), _light_brightness);
mqttSend(MQTT_TOPIC_BRIGHTNESS, buffer);
}
void lightMQTTGroup() {
@ -735,12 +733,16 @@ unsigned int lightChannel(unsigned char id) {
return 0;
}
void lightChannel(unsigned char id, unsigned int value) {
void lightChannel(unsigned char id, int value) {
if (id <= _light_channel.size()) {
_light_channel[id].inputValue = constrain(value, 0, LIGHT_MAX_VALUE);
}
}
void lightChannelStep(unsigned char id, int steps) {
lightChannel(id, lightChannel(id) + steps * LIGHT_STEP);
}
unsigned int lightBrightness() {
return _light_brightness;
}
@ -783,7 +785,6 @@ void _lightWebSocketOnSend(JsonObject& root) {
}
if (useRGB) {
root["rgb"] = lightColor(true);
root["brightness"] = lightBrightness();
} else {
root["hsv"] = lightColor(false);
}
@ -792,9 +793,11 @@ void _lightWebSocketOnSend(JsonObject& root) {
for (unsigned char id=0; id < _light_channel.size(); id++) {
channels.add(lightChannel(id));
}
root["brightness"] = lightBrightness();
}
void _lightWebSocketOnAction(uint32_t client_id, const char * action, JsonObject& data) {
if (_light_has_color) {
if (strcmp(action, "color") == 0) {
if (data.containsKey("rgb")) {
@ -805,10 +808,6 @@ void _lightWebSocketOnAction(uint32_t client_id, const char * action, JsonObject
lightColor(data["hsv"], false);
lightUpdate(true, true);
}
if (data.containsKey("brightness")) {
lightBrightness(data["brightness"]);
lightUpdate(true, true);
}
}
if (_light_use_cct) {
if (strcmp(action, "mireds") == 0) {
@ -824,38 +823,85 @@ void _lightWebSocketOnAction(uint32_t client_id, const char * action, JsonObject
lightUpdate(true, true);
}
}
if (strcmp(action, "brightness") == 0) {
if (data.containsKey("value")) {
lightBrightness(data["value"]);
lightUpdate(true, true);
}
}
}
#endif
#if API_SUPPORT
void _lightAPISetup() {
// API entry points (protected with apikey)
if (_light_has_color) {
apiRegister(MQTT_TOPIC_COLOR_RGB,
[](char * buffer, size_t len) {
if (getSetting("useCSS", LIGHT_USE_CSS).toInt() == 1) {
_toRGB(buffer, len);
} else {
_toLong(buffer, len);
if (_light_has_color) {
apiRegister(MQTT_TOPIC_COLOR_RGB,
[](char * buffer, size_t len) {
if (getSetting("useCSS", LIGHT_USE_CSS).toInt() == 1) {
_toRGB(buffer, len);
} else {
_toLong(buffer, len);
}
},
[](const char * payload) {
lightColor(payload, true);
lightUpdate(true, true);
}
},
[](const char * payload) {
lightColor(payload, true);
lightUpdate(true, true);
}
);
);
apiRegister(MQTT_TOPIC_COLOR_HSV,
[](char * buffer, size_t len) {
_toHSV(buffer, len);
},
[](const char * payload) {
lightColor(payload, false);
lightUpdate(true, true);
}
);
apiRegister(MQTT_TOPIC_COLOR_HSV,
[](char * buffer, size_t len) {
_toHSV(buffer, len);
},
[](const char * payload) {
lightColor(payload, false);
lightUpdate(true, true);
}
);
apiRegister(MQTT_TOPIC_KELVIN,
[](char * buffer, size_t len) {},
[](const char * payload) {
_fromKelvin(atol(payload));
lightUpdate(true, true);
}
);
apiRegister(MQTT_TOPIC_MIRED,
[](char * buffer, size_t len) {},
[](const char * payload) {
_fromMireds(atol(payload));
lightUpdate(true, true);
}
);
}
for (unsigned int id=0; id<_light_channel.size(); id++) {
char key[15];
snprintf_P(key, sizeof(key), PSTR("%s/%d"), MQTT_TOPIC_CHANNEL, id);
apiRegister(key,
[id](char * buffer, size_t len) {
snprintf_P(buffer, len, PSTR("%d"), lightChannel(id));
},
[id](const char * payload) {
lightChannel(id, atoi(payload));
lightUpdate(true, true);
}
);
}
apiRegister(MQTT_TOPIC_BRIGHTNESS,
[](char * buffer, size_t len) {
snprintf_P(buffer, len, PSTR("%d"), _light_brightness);
snprintf_P(buffer, len, PSTR("%d"), _light_brightness);
},
[](const char * payload) {
lightBrightness(atoi(payload));
@ -863,40 +909,9 @@ void _lightAPISetup() {
}
);
apiRegister(MQTT_TOPIC_KELVIN,
[](char * buffer, size_t len) {},
[](const char * payload) {
_fromKelvin(atol(payload));
lightUpdate(true, true);
}
);
apiRegister(MQTT_TOPIC_MIRED,
[](char * buffer, size_t len) {},
[](const char * payload) {
_fromMireds(atol(payload));
lightUpdate(true, true);
}
);
}
for (unsigned int id=0; id<_light_channel.size(); id++) {
char key[15];
snprintf_P(key, sizeof(key), PSTR("%s/%d"), MQTT_TOPIC_CHANNEL, id);
apiRegister(key,
[id](char * buffer, size_t len) {
snprintf_P(buffer, len, PSTR("%d"), lightChannel(id));
},
[id](const char * payload) {
lightChannel(id, atoi(payload));
lightUpdate(true, true);
}
);
}
}
#endif // WEB_SUPPORT
#endif // API_SUPPORT
#if TERMINAL_SUPPORT
@ -1069,16 +1084,13 @@ void lightSetup() {
_lightColorRestore();
#if WEB_SUPPORT
_lightAPISetup();
wsOnSendRegister(_lightWebSocketOnSend);
wsOnActionRegister(_lightWebSocketOnAction);
wsOnReceiveRegister(_lightWebSocketOnReceive);
wsOnAfterParseRegister([]() {
#if LIGHT_SAVE_ENABLED == 0
lightSave();
#endif
_lightConfigure();
});
#endif
#if API_SUPPORT
_lightAPISetup();
#endif
#if MQTT_SUPPORT
@ -1089,6 +1101,14 @@ void lightSetup() {
_lightInitCommands();
#endif
// Main callbacks
espurnaRegisterReload([]() {
#if LIGHT_SAVE_ENABLED == 0
lightSave();
#endif
_lightConfigure();
});
}
#endif // LIGHT_PROVIDER != LIGHT_PROVIDER_NONE

+ 195
- 0
code/espurna/migrate.ino View File

@ -1038,6 +1038,201 @@ void migrate() {
setSetting("hlwSelC", LOW);
setSetting("hlwIntM", FALLING);
#elif defined(TINKERMAN_RFM69GW)
setSetting("board", 80);
setSetting("btnGPIO", 0, 0);
#elif defined(ITEAD_SONOFF_IFAN02)
setSetting("board", 81);
setSetting("btnGPIO", 0, 0);
setSetting("btnGPIO", 1, 9);
setSetting("btnGPIO", 2, 10);
setSetting("btnGPIO", 3, 14);
setSetting("ledGPIO", 1, 13);
setSetting("ledLogic", 1, 1);
setSetting("relayGPIO", 0, 12);
setSetting("relayGPIO", 1, 5);
setSetting("relayGPIO", 2, 4);
setSetting("relayGPIO", 3, 15);
setSetting("relayType", 0, RELAY_TYPE_NORMAL);
setSetting("relayType", 1, RELAY_TYPE_NORMAL);
setSetting("relayType", 2, RELAY_TYPE_NORMAL);
setSetting("relayType", 3, RELAY_TYPE_NORMAL);
#elif defined(GENERIC_AG_L4)
setSetting("board", 82);
setSetting("btnGPIO", 0, 4);
setSetting("btnGPIO", 1, 2);
setSetting("btnRelay", 0, 0);
setSetting("ledGPIO", 0, 5);
setSetting("ledGPIO", 1, 16);
setSetting("ledLogic", 0, 0);
setSetting("ledLogic", 1, 1);
setSetting("relayProvider", RELAY_PROVIDER_LIGHT);
setSetting("lightProvider", LIGHT_PROVIDER_DIMMER);
setSetting("chGPIO", 0, 14);
setSetting("chGPIO", 1, 13);
setSetting("chGPIO", 2, 12);
setSetting("chLogic", 0, 0);
setSetting("chLogic", 1, 0);
setSetting("chLogic", 2, 0);
setSetting("relays", 1);
#elif defined(ALLTERCO_SHELLY1)
setSetting("board", 83);
setSetting("btnGPIO", 0, 5);
setSetting("btnRelay", 0, 0);
setSetting("relayGPIO", 0, 4);
setSetting("relayType", 0, RELAY_TYPE_NORMAL);
#elif defined(LOHAS_9W)
setSetting("board", 84);
setSetting("relayProvider", RELAY_PROVIDER_LIGHT);
setSetting("lightProvider", LIGHT_PROVIDER_MY92XX);
setSetting("myModel", MY92XX_MODEL_MY9231);
setSetting("myChips", 2);
setSetting("myDIGPIO", 13);
setSetting("myDCKIGPIO", 15);
setSetting("relays", 1);
#elif defined(YJZK_SWITCH_1CH)
setSetting("board", 85);
setSetting("ledGPIO", 0, 13);
setSetting("ledLogic", 0, 0);
setSetting("ledWifi", 0);
setSetting("btnGPIO", 0, 0);
setSetting("btnRelay", 0, 0);
setSetting("relayGPIO", 0, 12);
setSetting("relayType", 0, RELAY_TYPE_NORMAL);
#elif defined(YJZK_SWITCH_3CH)
setSetting("board", 86);
setSetting("ledGPIO", 0, 13);
setSetting("ledLogic", 0, 0);
setSetting("ledWifi", 0);
setSetting("btnGPIO", 0, 0);
setSetting("btnGPIO", 1, 9);
setSetting("btnGPIO", 2, 10);
setSetting("btnRelay", 0, 0);
setSetting("btnRelay", 1, 1);
setSetting("btnRelay", 2, 2);
setSetting("relayGPIO", 0, 12);
setSetting("relayGPIO", 1, 5);
setSetting("relayGPIO", 2, 4);
setSetting("relayType", 0, RELAY_TYPE_NORMAL);
setSetting("relayType", 1, RELAY_TYPE_NORMAL);
setSetting("relayType", 2, RELAY_TYPE_NORMAL);
#elif defined(XIAOMI_SMART_DESK_LAMP)
setSetting("board", 87);
setSetting("relayProvider", RELAY_PROVIDER_LIGHT);
setSetting("lightProvider", LIGHT_PROVIDER_DIMMER);
setSetting("relays", 1);
setSetting("chGPIO", 0, 5);
setSetting("chGPIO", 1, 4);
setSetting("chLogic", 0, 0);
setSetting("chLogic", 1, 0);
setSetting("btnGPIO", 0, 2);
setSetting("btnGPIO", 1, 14);
setSetting("btnRelay", 0, 0);
setSetting("btnLngDelay", 500);
setSetting("btnDblClick", 0, BUTTON_MODE_NONE);
setSetting("btnLngClick", 0, BUTTON_MODE_NONE);
setSetting("btnLngLngClick", 0, BUTTON_MODE_NONE);
setSetting("btnDblClick", 1, BUTTON_MODE_AP);
setSetting("btnLngLngClick", 1, BUTTON_MODE_RESET);
setSetting("enc1stGPIO", 0, 12);
setSetting("enc2ndGPIO", 0, 13);
setSetting("encBtnGPIO", 0, 2);
setSetting("encMode", ENCODER_MODE_RATIO);
#elif defined(ALLTERCO_SHELLY2)
setSetting("board", 88);
setSetting("btnGPIO", 0, 12);
setSetting("btnGPIO", 1, 14);
setSetting("btnRelay", 0, 0);
setSetting("btnRelay", 1, 1);
setSetting("relayGPIO", 0, 4);
setSetting("relayGPIO", 1, 5);
setSetting("relayType", 0, RELAY_TYPE_NORMAL);
setSetting("relayType", 1, RELAY_TYPE_NORMAL);
#elif defined(PHYX_ESP12_RGB)
setSetting("board", 89);
setSetting("relayProvider", RELAY_PROVIDER_LIGHT);
setSetting("lightProvider", LIGHT_PROVIDER_DIMMER);
setSetting("relays", 1);
setSetting("chGPIO", 0, 4);
setSetting("chGPIO", 1, 14);
setSetting("chGPIO", 2, 12);
setSetting("chLogic", 0, 0);
setSetting("chLogic", 1, 0);
setSetting("chLogic", 3, 0);
#elif defined(IWOOLE_LED_TABLE_LAMP)
setSetting("board", 90);
setSetting("relayProvider", RELAY_PROVIDER_LIGHT);
setSetting("lightProvider", LIGHT_PROVIDER_DIMMER);
setSetting("chGPIO", 0, 12);
setSetting("chGPIO", 1, 5);
setSetting("chGPIO", 2, 14);
setSetting("chGPIO", 3, 4);
setSetting("chLogic", 0, 0);
setSetting("chLogic", 1, 0);
setSetting("chLogic", 2, 0);
setSetting("chLogic", 3, 0);
setSetting("relays", 1);
#elif defined(EXS_WIFI_RELAY_V50)
setSetting("board", 91);
setSetting("btnGPIO", 0, 5);
setSetting("btnGPIO", 1, 4);
setSetting("btnRelay", 0, 0);
setSetting("btnRelay", 1, 1);
setSetting("relayGPIO", 0, 14);
setSetting("relayGPIO", 1, 13);
setSetting("relayResetGPIO", 0, 16);
setSetting("relayResetGPIO", 1, 12);
setSetting("relayType", 0, RELAY_TYPE_LATCHED);
setSetting("relayType", 0, RELAY_TYPE_LATCHED);
setSetting("ledGPIO", 1, 15);
setSetting("ledLogic", 1, 0);
#elif defined(TONBUX_XSSSA01)
setSetting("board", 92);
setSetting("ledGPIO", 0, 13);
setSetting("ledLogic", 0, 0);
setSetting("btnGPIO", 0, 13);
setSetting("btnRelay", 0, 0);
setSetting("relayGPIO", 0, 5);
setSetting("relayType", 0, RELAY_TYPE_NORMAL);
#else
// Allow users to define new settings without migration config


+ 15
- 7
code/espurna/mqtt.ino View File

@ -291,7 +291,7 @@ unsigned long _mqttNextMessageId() {
EEPROMr.write(EEPROM_MESSAGE_ID + 1, (id >> 16) & 0xFF);
EEPROMr.write(EEPROM_MESSAGE_ID + 2, (id >> 8) & 0xFF);
EEPROMr.write(EEPROM_MESSAGE_ID + 3, (id >> 0) & 0xFF);
EEPROMr.commit();
eepromCommit();
}
id++;
@ -574,7 +574,16 @@ unsigned char _mqttBuildTree(JsonObject& root, char parent) {
JsonObject& elements = root.createNestedObject(element.topic);
unsigned char num = _mqttBuildTree(elements, i);
if (0 == num) {
root.set(element.topic, element.message);
if (isNumber(element.message)) {
double value = atof(element.message);
if (value == int(value)) {
root.set(element.topic, int(value));
} else {
root.set(element.topic, value);
}
} else {
root.set(element.topic, element.message);
}
}
}
}
@ -703,7 +712,6 @@ void mqttUnsubscribe(const char * topic) {
void mqttEnabled(bool status) {
_mqtt_enabled = status;
setSetting("mqttEnabled", status ? 1 : 0);
}
bool mqttEnabled() {
@ -736,7 +744,7 @@ void mqttSetBroker(IPAddress ip, unsigned int port) {
}
void mqttSetBrokerIfNone(IPAddress ip, unsigned int port) {
if (!hasSetting("mqttServer")) mqttSetBroker(ip, port);
if (getSetting("mqttServer", MQTT_SERVER).length() == 0) mqttSetBroker(ip, port);
}
void mqttReset() {
@ -751,7 +759,7 @@ void mqttReset() {
void mqttSetup() {
_mqttBackwards();
DEBUG_MSG_P(PSTR("[MQTT] Async %s, SSL %s, Autoconnect %s\n"),
MQTT_USE_ASYNC ? "ENABLED" : "DISABLED",
ASYNC_TCP_SSL_ENABLED ? "ENABLED" : "DISABLED",
@ -809,7 +817,6 @@ void mqttSetup() {
#if WEB_SUPPORT
wsOnSendRegister(_mqttWebSocketOnSend);
wsOnAfterParseRegister(_mqttConfigure);
wsOnReceiveRegister(_mqttWebSocketOnReceive);
#endif
@ -817,8 +824,9 @@ void mqttSetup() {
_mqttInitCommands();
#endif
// Register loop
// Main callbacks
espurnaRegisterLoop(mqttLoop);
espurnaRegisterReload(_mqttConfigure);
}


+ 2
- 2
code/espurna/nofuss.ino View File

@ -154,7 +154,6 @@ void nofussSetup() {
#if WEB_SUPPORT
wsOnSendRegister(_nofussWebSocketOnSend);
wsOnAfterParseRegister(_nofussConfigure);
wsOnReceiveRegister(_nofussWebSocketOnReceive);
#endif
@ -162,8 +161,9 @@ void nofussSetup() {
_nofussInitCommands();
#endif
// Register loop
// Main callbacks
espurnaRegisterLoop(nofussLoop);
espurnaRegisterReload(_nofussConfigure);
}


+ 2
- 2
code/espurna/ntp.ino View File

@ -179,11 +179,11 @@ void ntpSetup() {
#if WEB_SUPPORT
wsOnSendRegister(_ntpWebSocketOnSend);
wsOnReceiveRegister(_ntpWebSocketOnReceive);
wsOnAfterParseRegister([]() { _ntp_configure = true; });
#endif
// Register loop
// Main callbacks
espurnaRegisterLoop(_ntpLoop);
espurnaRegisterReload([]() { _ntp_configure = true; });
}


+ 10
- 7
code/espurna/ota.ino View File

@ -16,7 +16,7 @@ void _otaConfigure() {
ArduinoOTA.setPort(OTA_PORT);
ArduinoOTA.setHostname(getSetting("hostname").c_str());
#if USE_PASSWORD
ArduinoOTA.setPassword(getSetting("adminPass", ADMIN_PASS).c_str());
ArduinoOTA.setPassword(getAdminPass().c_str());
#endif
}
@ -203,16 +203,13 @@ void otaSetup() {
_otaConfigure();
#if WEB_SUPPORT
wsOnAfterParseRegister(_otaConfigure);
#endif
#if TERMINAL_SUPPORT
_otaInitCommands();
#endif
// Register loop
// Main callbacks
espurnaRegisterLoop(_otaLoop);
espurnaRegisterReload(_otaConfigure);
// -------------------------------------------------------------------------
@ -239,7 +236,13 @@ void otaSetup() {
});
ArduinoOTA.onProgress([](unsigned int progress, unsigned int total) {
DEBUG_MSG_P(PSTR("[OTA] Progress: %u%%\r"), (progress / (total / 100)));
static unsigned int _progOld;
unsigned int _prog = (progress / (total / 100));
if (_prog != _progOld) {
DEBUG_MSG_P(PSTR("[OTA] Progress: %u%%\r"), _prog);
_progOld = _prog;
}
});
ArduinoOTA.onError([](ota_error_t error) {


+ 19
- 17
code/espurna/pwm.c View File

@ -19,27 +19,29 @@
/* Set the following three defines to your needs */
#ifndef SDK_PWM_PERIOD_COMPAT_MODE
#define SDK_PWM_PERIOD_COMPAT_MODE 0
#define SDK_PWM_PERIOD_COMPAT_MODE 0
#endif
#ifndef PWM_MAX_CHANNELS
#define PWM_MAX_CHANNELS 8
#define PWM_MAX_CHANNELS 8
#endif
#define PWM_DEBUG 0
#define PWM_USE_NMI 1
#define PWM_DEBUG 0
#define PWM_USE_NMI 1
/* no user servicable parts beyond this point */
#define PWM_MAX_TICKS 0x7fffff
#define PWM_MAX_TICKS 0x7fffff
#if SDK_PWM_PERIOD_COMPAT_MODE
#define PWM_PERIOD_TO_TICKS(x) (x * 0.2)
#define PWM_DUTY_TO_TICKS(x) (x * 5)
#define PWM_MAX_DUTY (PWM_MAX_TICKS * 0.2)
#define PWM_MAX_PERIOD (PWM_MAX_TICKS * 5)
#define PWM_PERIOD_TO_TICKS(x) (x * 0.2)
#define PWM_DUTY_TO_TICKS(x) (x * 5)
#define PWM_MAX_DUTY (PWM_MAX_TICKS * 0.2)
#define PWM_MAX_PERIOD (PWM_MAX_TICKS * 5)
#else
#define PWM_PERIOD_TO_TICKS(x) (x)
#define PWM_DUTY_TO_TICKS(x) (x)
#define PWM_MAX_DUTY PWM_MAX_TICKS
#define PWM_MAX_PERIOD PWM_MAX_TICKS
#define PWM_PERIOD_TO_TICKS(x) (x)
#define PWM_DUTY_TO_TICKS(x) (x)
#define PWM_MAX_DUTY PWM_MAX_TICKS
#define PWM_MAX_PERIOD PWM_MAX_TICKS
#endif
#include <c_types.h>
@ -48,8 +50,8 @@
#include "libs/pwm.h"
// from SDK hw_timer.c
#define TIMER1_DIVIDE_BY_16 0x0004
#define TIMER1_ENABLE_TIMER 0x0080
#define TIMER1_DIVIDE_BY_16 0x0004
#define TIMER1_ENABLE_TIMER 0x0080
struct pwm_phase {
uint32_t ticks; ///< delay until next phase, in 200ns units
@ -400,7 +402,7 @@ pwm_start(void)
void ICACHE_FLASH_ATTR
pwm_set_duty(uint32_t duty, uint8_t channel)
{
if (channel > PWM_MAX_CHANNELS)
if (channel >= PWM_MAX_CHANNELS)
return;
if (duty > PWM_MAX_DUTY)
@ -412,7 +414,7 @@ pwm_set_duty(uint32_t duty, uint8_t channel)
uint32_t ICACHE_FLASH_ATTR
pwm_get_duty(uint8_t channel)
{
if (channel > PWM_MAX_CHANNELS)
if (channel >= PWM_MAX_CHANNELS)
return 0;
return pwm_duty[channel];
}


+ 145
- 38
code/espurna/relay.ino View File

@ -99,7 +99,7 @@ void _relayProviderStatus(unsigned char id, bool status) {
if (_relays.size() == lightChannels()) {
lightState(id, status);
lightState(true);
} else if (_relays.size() == lightChannels() + 1) {
} else if (_relays.size() == (lightChannels() + 1u)) {
if (id == 0) {
lightState(status);
} else {
@ -121,15 +121,15 @@ void _relayProviderStatus(unsigned char id, bool status) {
} else if (_relays[id].type == RELAY_TYPE_LATCHED || _relays[id].type == RELAY_TYPE_LATCHED_INVERSE) {
bool pulse = RELAY_TYPE_LATCHED ? HIGH : LOW;
digitalWrite(_relays[id].pin, !pulse);
digitalWrite(_relays[id].reset_pin, !pulse);
if (status) {
if (GPIO_NONE != _relays[id].reset_pin) digitalWrite(_relays[id].reset_pin, !pulse);
if (status || (GPIO_NONE == _relays[id].reset_pin)) {
digitalWrite(_relays[id].pin, pulse);
} else {
digitalWrite(_relays[id].reset_pin, pulse);
}
nice_delay(RELAY_LATCHING_PULSE);
digitalWrite(_relays[id].pin, !pulse);
digitalWrite(_relays[id].reset_pin, !pulse);
if (GPIO_NONE != _relays[id].reset_pin) digitalWrite(_relays[id].reset_pin, !pulse);
}
#endif
@ -173,11 +173,19 @@ void _relayProcess(bool mode) {
#endif
if (!_relayRecursive) {
relayPulse(id);
_relaySaveTicker.once_ms(RELAY_SAVE_DELAY, relaySave);
// We will trigger a commit only if
// we care about current relay status on boot
unsigned char boot_mode = getSetting("relayBoot", id, RELAY_BOOT_MODE).toInt();
bool do_commit = ((RELAY_BOOT_SAME == boot_mode) || (RELAY_BOOT_TOGGLE == boot_mode));
_relaySaveTicker.once_ms(RELAY_SAVE_DELAY, relaySave, do_commit);
#if WEB_SUPPORT
wsSend(_relayWebSocketUpdate);
#endif
}
#if DOMOTICZ_SUPPORT
@ -194,7 +202,9 @@ void _relayProcess(bool mode) {
#endif
// Flag relay-based LEDs to update status
ledUpdate(true);
#if LED_SUPPORT
ledUpdate(true);
#endif
_relays[id].report = false;
_relays[id].group_report = false;
@ -203,6 +213,34 @@ void _relayProcess(bool mode) {
}
#if defined(ITEAD_SONOFF_IFAN02)
unsigned char _relay_ifan02_speeds[] = {0, 1, 3, 5};
unsigned char getSpeed() {
unsigned char speed =
(_relays[1].target_status ? 1 : 0) +
(_relays[2].target_status ? 2 : 0) +
(_relays[3].target_status ? 4 : 0);
for (unsigned char i=0; i<4; i++) {
if (_relay_ifan02_speeds[i] == speed) return i;
}
return 0;
}
void setSpeed(unsigned char speed) {
if ((0 <= speed) & (speed <= 3)) {
if (getSpeed() == speed) return;
unsigned char states = _relay_ifan02_speeds[speed];
for (unsigned char i=0; i<3; i++) {
relayStatus(i+1, states & 1 == 1);
states >>= 1;
}
}
}
#endif
// -----------------------------------------------------------------------------
// RELAY
// -----------------------------------------------------------------------------
@ -255,8 +293,8 @@ bool relayStatus(unsigned char id, bool status, bool report, bool group_report)
} else {
unsigned int current_time = millis();
unsigned int fw_end = _relays[id].fw_start + 1000 * RELAY_FLOOD_WINDOW;
unsigned long current_time = millis();
unsigned long fw_end = _relays[id].fw_start + 1000 * RELAY_FLOOD_WINDOW;
unsigned long delay = status ? _relays[id].delay_on : _relays[id].delay_off;
_relays[id].fw_count++;
@ -353,16 +391,41 @@ void relaySync(unsigned char id) {
}
void relaySave() {
void relaySave(bool do_commit) {
// Relay status is stored in a single byte
// This means that, atm,
// we are only storing the status of the first 8 relays.
unsigned char bit = 1;
unsigned char mask = 0;
for (unsigned int i=0; i < _relays.size(); i++) {
unsigned char count = _relays.size();
if (count > 8) count = 8;
for (unsigned int i=0; i < count; i++) {
if (relayStatus(i)) mask += bit;
bit += bit;
}
EEPROMr.write(EEPROM_RELAY_STATUS, mask);
DEBUG_MSG_P(PSTR("[RELAY] Saving mask: %d\n"), mask);
EEPROMr.commit();
DEBUG_MSG_P(PSTR("[RELAY] Setting relay mask: %d\n"), mask);
// The 'do_commit' flag controls wether we are commiting this change or not.
// It is useful to set it to 'false' if the relay change triggering the
// save involves a relay whose boot mode is independent from current mode,
// thus storing the last relay value is not absolutely necessary.
// Nevertheless, we store the value in the EEPROM buffer so it will be written
// on the next commit.
if (do_commit) {
// We are actually enqueuing the commit so it will be
// executed on the main loop, in case this is called from a callback
eepromCommit();
}
}
void relaySave() {
relaySave(true);
}
void relayToggle(unsigned char id, bool report, bool group_report) {
@ -444,27 +507,34 @@ void _relayBoot() {
DEBUG_MSG_P(PSTR("[RELAY] Retrieving mask: %d\n"), mask);
// Walk the relays
bool status = false;
bool status;
for (unsigned int i=0; i<_relays.size(); i++) {
unsigned char boot_mode = getSetting("relayBoot", i, RELAY_BOOT_MODE).toInt();
DEBUG_MSG_P(PSTR("[RELAY] Relay #%d boot mode %d\n"), i, boot_mode);
status = false;
switch (boot_mode) {
case RELAY_BOOT_SAME:
status = ((mask & bit) == bit);
if (i < 8) {
status = ((mask & bit) == bit);
}
break;
case RELAY_BOOT_TOGGLE:
status = ((mask & bit) != bit);
mask ^= bit;
trigger_save = true;
if (i < 8) {
status = ((mask & bit) != bit);
mask ^= bit;
trigger_save = true;
}
break;
case RELAY_BOOT_ON:
status = true;
break;
case RELAY_BOOT_OFF:
default:
status = false;
break;
}
_relays[i].current_status = !status;
_relays[i].target_status = status;
#if RELAY_PROVIDER == RELAY_PROVIDER_STM
@ -478,7 +548,7 @@ void _relayBoot() {
// Save if there is any relay in the RELAY_BOOT_TOGGLE mode
if (trigger_save) {
EEPROMr.write(EEPROM_RELAY_STATUS, mask);
EEPROMr.commit();
eepromCommit();
}
_relayRecursive = false;
@ -488,9 +558,13 @@ void _relayBoot() {
void _relayConfigure() {
for (unsigned int i=0; i<_relays.size(); i++) {
pinMode(_relays[i].pin, OUTPUT);
if (_relays[i].type == RELAY_TYPE_LATCHED || _relays[i].type == RELAY_TYPE_LATCHED_INVERSE) {
if (GPIO_NONE != _relays[i].reset_pin) {
pinMode(_relays[i].reset_pin, OUTPUT);
}
if (_relays[i].type == RELAY_TYPE_INVERSE) {
//set to high to block short opening of relay
digitalWrite(_relays[i].pin, HIGH);
}
_relays[i].pulse = getSetting("relayPulse", i, RELAY_PULSE_MODE).toInt();
_relays[i].pulse_ms = 1000 * getSetting("relayTime", i, RELAY_PULSE_MODE).toFloat();
}
@ -584,7 +658,6 @@ void _relayWebSocketOnAction(uint32_t client_id, const char * action, JsonObject
void relaySetupWS() {
wsOnSendRegister(_relayWebSocketOnStart);
wsOnActionRegister(_relayWebSocketOnAction);
wsOnAfterParseRegister(_relayConfigure);
wsOnReceiveRegister(_relayWebSocketOnReceive);
}
@ -594,15 +667,15 @@ void relaySetupWS() {
// REST API
//------------------------------------------------------------------------------
#if WEB_SUPPORT
#if API_SUPPORT
void relaySetupAPI() {
char key[20];
// API entry points (protected with apikey)
for (unsigned int relayID=0; relayID<relayCount(); relayID++) {
char key[20];
snprintf_P(key, sizeof(key), PSTR("%s/%d"), MQTT_TOPIC_RELAY, relayID);
apiRegister(key,
[relayID](char * buffer, size_t len) {
@ -646,17 +719,27 @@ void relaySetupAPI() {
_relays[relayID].pulse = relayStatus(relayID) ? RELAY_PULSE_ON : RELAY_PULSE_OFF;
relayToggle(relayID, true, false);
return;
}
);
#if defined(ITEAD_SONOFF_IFAN02)
apiRegister(MQTT_TOPIC_SPEED,
[relayID](char * buffer, size_t len) {
snprintf(buffer, len, "%u", getSpeed());
},
[relayID](const char * payload) {
setSpeed(atoi(payload));
}
);
#endif
}
}
#endif // WEB_SUPPORT
#endif // API_SUPPORT
//------------------------------------------------------------------------------
// MQTT
@ -671,7 +754,7 @@ void relayMQTT(unsigned char id) {
// Send state topic
if (_relays[id].report) {
_relays[id].report = false;
mqttSend(MQTT_TOPIC_RELAY, id, _relays[id].current_status ? "1" : "0");
mqttSend(MQTT_TOPIC_RELAY, id, _relays[id].current_status ? RELAY_MQTT_ON : RELAY_MQTT_OFF);
}
// Check group topic
@ -681,14 +764,22 @@ void relayMQTT(unsigned char id) {
if (t.length() > 0) {
bool status = relayStatus(id);
if (getSetting("mqttGroupInv", id, 0).toInt() == 1) status = !status;
mqttSendRaw(t.c_str(), status ? "1" : "0");
mqttSendRaw(t.c_str(), status ? RELAY_MQTT_ON : RELAY_MQTT_OFF);
}
}
// Send speed for IFAN02
#if defined (ITEAD_SONOFF_IFAN02)
char buffer[5];
snprintf(buffer, sizeof(buffer), "%u", getSpeed());
mqttSend(MQTT_TOPIC_SPEED, buffer);
#endif
}
void relayMQTT() {
for (unsigned int id=0; id < _relays.size(); id++) {
mqttSend(MQTT_TOPIC_RELAY, id, _relays[id].current_status ? "1" : "0");
mqttSend(MQTT_TOPIC_RELAY, id, _relays[id].current_status ? RELAY_MQTT_ON : RELAY_MQTT_OFF);
}
}
@ -715,7 +806,7 @@ void relayMQTTCallback(unsigned int type, const char * topic, const char * paylo
if (type == MQTT_CONNECT_EVENT) {
// Send status on connect
#if not HEARTBEAT_REPORT_RELAY
#if (HEARTBEAT_MODE == HEARTBEAT_NONE) or (not HEARTBEAT_REPORT_RELAY)
relayMQTT();
#endif
@ -729,6 +820,10 @@ void relayMQTTCallback(unsigned int type, const char * topic, const char * paylo
snprintf_P(pulse_topic, sizeof(pulse_topic), PSTR("%s/+"), MQTT_TOPIC_PULSE);
mqttSubscribe(pulse_topic);
#if defined(ITEAD_SONOFF_IFAN02)
mqttSubscribe(MQTT_TOPIC_SPEED);
#endif
// Subscribe to group topics
for (unsigned int i=0; i < _relays.size(); i++) {
String t = getSetting("mqttGroup", i, "");
@ -808,6 +903,13 @@ void relayMQTTCallback(unsigned int type, const char * topic, const char * paylo
}
}
// Itead Sonoff IFAN02
#if defined (ITEAD_SONOFF_IFAN02)
if (t.startsWith(MQTT_TOPIC_SPEED)) {
setSpeed(atoi(payload));
}
#endif
}
if (type == MQTT_DISCONNECT_EVENT) {
@ -900,10 +1002,11 @@ void relaySetup() {
// Dummy relays for AI Light, Magic Home LED Controller, H801,
// Sonoff Dual and Sonoff RF Bridge
#if DUMMY_RELAY_COUNT > 0
unsigned int _delay_on[8] = {RELAY1_DELAY_ON, RELAY2_DELAY_ON, RELAY3_DELAY_ON, RELAY4_DELAY_ON, RELAY5_DELAY_ON, RELAY6_DELAY_ON, RELAY7_DELAY_ON, RELAY8_DELAY_ON};
unsigned int _delay_off[8] = {RELAY1_DELAY_OFF, RELAY2_DELAY_OFF, RELAY3_DELAY_OFF, RELAY4_DELAY_OFF, RELAY5_DELAY_OFF, RELAY6_DELAY_OFF, RELAY7_DELAY_OFF, RELAY8_DELAY_OFF};
// No delay_on or off for these devices to easily allow having more than
// 8 channels. This behaviour will be recovered with v2.
for (unsigned char i=0; i < DUMMY_RELAY_COUNT; i++) {
_relays.push_back((relay_t) {0, RELAY_TYPE_NORMAL,0,_delay_on[i], _delay_off[i]});
_relays.push_back((relay_t) {0, RELAY_TYPE_NORMAL, 0, 0, 0});
}
#else
@ -940,12 +1043,12 @@ void relaySetup() {
_relayBoot();
_relayLoop();
espurnaRegisterLoop(_relayLoop);
#if WEB_SUPPORT
relaySetupAPI();
relaySetupWS();
#endif
#if API_SUPPORT
relaySetupAPI();
#endif
#if MQTT_SUPPORT
relaySetupMQTT();
#endif
@ -953,6 +1056,10 @@ void relaySetup() {
_relayInitCommands();
#endif
// Main callbacks
espurnaRegisterLoop(_relayLoop);
espurnaRegisterReload(_relayConfigure);
DEBUG_MSG_P(PSTR("[RELAY] Number of relays: %d\n"), _relays.size());
}

+ 15
- 14
code/espurna/rfbridge.ino View File

@ -297,25 +297,13 @@ void _rfbDecode() {
#endif
}
unsigned char id;
unsigned char status;
bool matched = false;
if (action == RF_CODE_LEARN_OK || action == RF_CODE_RFIN) {
_rfbAck();
_rfbToChar(&_uartbuf[1], buffer);
/* Look for the code, possibly replacing the code with the exact learned one on match
* we want to do this on learn too to be sure that the learned code is the same if it
* is equivalent
*/
DEBUG_MSG_P(PSTR("[RFBRIDGE] Received message '%s'\n"), buffer);
matched = _rfbMatch(buffer, id, status, buffer);
DEBUG_MSG_P(PSTR("[RFBRIDGE] Matched message '%s'\n"), buffer);
#if MQTT_SUPPORT
mqttSend(MQTT_TOPIC_RFIN, buffer);
#endif
}
if (action == RF_CODE_LEARN_OK) {
@ -333,8 +321,17 @@ void _rfbDecode() {
}
if (action == RF_CODE_RFIN) {
DEBUG_MSG_P(PSTR("[RFBRIDGE] Forward message '%s'\n"), buffer);
/* Look for the code, possibly replacing the code with the exact learned one on match
* we want to do this on learn too to be sure that the learned code is the same if it
* is equivalent
*/
unsigned char id;
unsigned char status;
bool matched = _rfbMatch(buffer, id, status, buffer);
if (matched) {
DEBUG_MSG_P(PSTR("[RFBRIDGE] Matched message '%s'\n"), buffer);
_rfbin = true;
if (status == 2) {
relayToggle(id);
@ -343,6 +340,10 @@ void _rfbDecode() {
}
}
#if MQTT_SUPPORT
mqttSend(MQTT_TOPIC_RFIN, buffer);
#endif
}
}


+ 284
- 0
code/espurna/rfm69.ino View File

@ -0,0 +1,284 @@
/*
RFM69 MODULE
Copyright (C) 2016-2017 by Xose Pérez <xose dot perez at gmail dot com>
*/
#if RFM69_SUPPORT
#include "libs/RFM69Wrap.h"
#define RFM69_PACKET_SEPARATOR ':'
// -----------------------------------------------------------------------------
// Locals
// -----------------------------------------------------------------------------
RFM69Wrap * _rfm69_radio;
struct _node_t {
unsigned long count = 0;
unsigned long missing = 0;
unsigned long duplicates = 0;
unsigned char lastPacketID = 0;
};
_node_t _rfm69_node_info[RFM69_MAX_NODES];
unsigned char _rfm69_node_count;
unsigned long _rfm69_packet_count;
// -----------------------------------------------------------------------------
// WEB
// -----------------------------------------------------------------------------
#if WEB_SUPPORT
void _rfm69WebSocketOnSend(JsonObject& root) {
root["rfm69Visible"] = 1;
root["rfm69Topic"] = getSetting("rfm69Topic", RFM69_DEFAULT_TOPIC);
root["packetCount"] = _rfm69_packet_count;
root["nodeCount"] = _rfm69_node_count;
JsonArray& mappings = root.createNestedArray("mapping");
for (unsigned char i=0; i<RFM69_MAX_TOPICS; i++) {
unsigned char node = getSetting("node", i, 0).toInt();
if (0 == node) break;
JsonObject& mapping = mappings.createNestedObject();
mapping["node"] = node;
mapping["key"] = getSetting("key", i, "");
mapping["topic"] = getSetting("topic", i, "");
}
}
bool _rfm69WebSocketOnReceive(const char * key, JsonVariant& value) {
if (strncmp(key, "rfm69", 5) == 0) return true;
if (strncmp(key, "node", 4) == 0) return true;
if (strncmp(key, "key", 3) == 0) return true;
if (strncmp(key, "topic", 5) == 0) return true;
return false;
}
void _rfm69WebSocketOnAction(uint32_t client_id, const char * action, JsonObject& data) {
if (strcmp(action, "clear-counts") == 0) _rfm69Clear();
}
#endif // WEB_SUPPORT
void _rfm69CleanNodes(unsigned char num) {
// Look for the last defined node
int i = 0;
while (i < num) {
if (getSetting("node", i, 0).toInt() == 0) break;
if (getSetting("key", i, "").length() == 0) break;
if (getSetting("topic", i, "").length() == 0) break;
++i;
}
// Delete all other settings
while (i < WIFI_MAX_NETWORKS) {
delSetting("node", i);
delSetting("key", i);
delSetting("topic", i);
++i;
}
}
void _rfm69Configure() {
_rfm69CleanNodes(RFM69_MAX_TOPICS);
}
// -----------------------------------------------------------------------------
// Radio
// -----------------------------------------------------------------------------
void _rfm69Debug(const char * level, packet_t * data) {
DEBUG_MSG_P(
PSTR("[RFM69] %s: messageID:%05d senderID:%03d targetID:%03d packetID:%03d rssi:%-04d key:%s value:%s\n"),
level,
data->messageID,
data->senderID,
data->targetID,
data->packetID,
data->rssi,
data->key,
data->value
);
}
void _rfm69Process(packet_t * data) {
// Is node beyond RFM69_MAX_NODES?
if (data->senderID >= RFM69_MAX_NODES) return;
// Count seen nodes and packets
if (_rfm69_node_info[data->senderID].count == 0) ++_rfm69_node_count;
++_rfm69_packet_count;
// Detect duplicates and missing packets
// packetID==0 means device is not sending packetID info
if (data->packetID > 0) {
if (_rfm69_node_info[data->senderID].count > 0) {
unsigned char gap = data->packetID - _rfm69_node_info[data->senderID].lastPacketID;
if (gap == 0) {
_rfm69_node_info[data->senderID].duplicates = _rfm69_node_info[data->senderID].duplicates + 1;
//_rfm69Debug("DUP", data);
return;
}
if ((gap > 1) && (data->packetID > 1)) {
_rfm69_node_info[data->senderID].missing = _rfm69_node_info[data->senderID].missing + gap - 1;
DEBUG_MSG_P(PSTR("[RFM69] %u missing packets detected\n"), gap - 1);
}
}
}
_rfm69Debug("OK ", data);
_rfm69_node_info[data->senderID].lastPacketID = data->packetID;
_rfm69_node_info[data->senderID].count = _rfm69_node_info[data->senderID].count + 1;
// Send info to websocket clients
{
char buffer[200];
snprintf_P(
buffer,
sizeof(buffer) - 1,
PSTR("{\"nodeCount\": %d, \"packetCount\": %lu, \"packet\": {\"senderID\": %u, \"targetID\": %u, \"packetID\": %u, \"key\": \"%s\", \"value\": \"%s\", \"rssi\": %d, \"duplicates\": %d, \"missing\": %d}}"),
_rfm69_node_count, _rfm69_packet_count,
data->senderID, data->targetID, data->packetID, data->key, data->value, data->rssi,
_rfm69_node_info[data->senderID].duplicates , _rfm69_node_info[data->senderID].missing);
wsSend(buffer);
}
// If we are the target of the message, forward it via MQTT, otherwise quit
if (!RFM69_PROMISCUOUS_SENDS && (RFM69_GATEWAY_ID != data->targetID)) return;
// Try to find a matching mapping
for (unsigned int i=0; i<RFM69_MAX_TOPICS; i++) {
unsigned char node = getSetting("node", i, 0).toInt();
if (0 == node) break;
if ((node == data->senderID) && (getSetting("key", i, "").equals(data->key))) {
mqttSendRaw((char *) getSetting("topic", i, "").c_str(), (char *) String(data->value).c_str());
return;
}
}
// Mapping not found, use default topic
String topic = getSetting("rfm69Topic", RFM69_DEFAULT_TOPIC);
if (topic.length() > 0) {
topic.replace("{node}", String(data->senderID));
topic.replace("{key}", String(data->key));
mqttSendRaw((char *) topic.c_str(), (char *) String(data->value).c_str());
}
}
void _rfm69Loop() {
if (_rfm69_radio->receiveDone()) {
uint8_t senderID = _rfm69_radio->SENDERID;
uint8_t targetID = _rfm69_radio->TARGETID;
int16_t rssi = _rfm69_radio->RSSI;
uint8_t length = _rfm69_radio->DATALEN;
char buffer[length + 1];
strncpy(buffer, (const char *) _rfm69_radio->DATA, length);
buffer[length] = 0;
// Do not send ACKs in promiscuous mode,
// we want to listen without being heard
if (!RFM69_PROMISCUOUS) {
if (_rfm69_radio->ACKRequested()) _rfm69_radio->sendACK();
}
uint8_t parts = 1;
for (uint8_t i=0; i<length; i++) {
if (buffer[i] == RFM69_PACKET_SEPARATOR) ++parts;
}
if (parts > 1) {
char sep[2] = {RFM69_PACKET_SEPARATOR, 0};
uint8_t packetID = 0;
char * key = strtok(buffer, sep);
char * value = strtok(NULL, sep);
if (parts > 2) {
char * packet = strtok(NULL, sep);
packetID = atoi(packet);
}
packet_t message;
message.messageID = ++_rfm69_packet_count;
message.packetID = packetID;
message.senderID = senderID;
message.targetID = targetID;
message.key = key;
message.value = value;
message.rssi = rssi;
_rfm69Process(&message);
}
}
}
void _rfm69Clear() {
for(unsigned int i=0; i<RFM69_MAX_NODES; i++) {
_rfm69_node_info[i].duplicates = 0;
_rfm69_node_info[i].missing = 0;
_rfm69_node_info[i].count = 0;
}
_rfm69_node_count = 0;
_rfm69_packet_count = 0;
}
// -----------------------------------------------------------------------------
// RFM69
// -----------------------------------------------------------------------------
void rfm69Setup() {
delay(10);
_rfm69Configure();
_rfm69_radio = new RFM69Wrap(RFM69_CS_PIN, RFM69_IRQ_PIN, RFM69_IS_RFM69HW, digitalPinToInterrupt(RFM69_IRQ_PIN));
_rfm69_radio->initialize(RFM69_FREQUENCY, RFM69_NODE_ID, RFM69_NETWORK_ID);
_rfm69_radio->encrypt(RFM69_ENCRYPTKEY);
_rfm69_radio->promiscuous(RFM69_PROMISCUOUS);
_rfm69_radio->enableAutoPower(0);
if (RFM69_IS_RFM69HW) _rfm69_radio->setHighPower();
DEBUG_MSG_P(PSTR("[RFM69] Worning at %u MHz\n"), RFM69_FREQUENCY == RF69_433MHZ ? 433 : RFM69_FREQUENCY == RF69_868MHZ ? 868 : 915);
DEBUG_MSG_P(PSTR("[RFM69] Node %u\n"), RFM69_NODE_ID);
DEBUG_MSG_P(PSTR("[RFM69] Network %u\n"), RFM69_NETWORK_ID);
DEBUG_MSG_P(PSTR("[RFM69] Promiscuous mode %s\n"), RFM69_PROMISCUOUS ? "ON" : "OFF");
#if WEB_SUPPORT
wsOnSendRegister(_rfm69WebSocketOnSend);
wsOnReceiveRegister(_rfm69WebSocketOnReceive);
wsOnActionRegister(_rfm69WebSocketOnAction);
#endif
// Main callbacks
espurnaRegisterLoop(_rfm69Loop);
espurnaRegisterReload(_rfm69Configure);
}
#endif // RFM69_SUPPORT

+ 2
- 2
code/espurna/scheduler.ino View File

@ -216,11 +216,11 @@ void schSetup() {
#if WEB_SUPPORT
wsOnSendRegister(_schWebSocketOnSend);
wsOnReceiveRegister(_schWebSocketOnReceive);
wsOnAfterParseRegister(_schConfigure);
#endif
// Register loop
// Main callbacks
espurnaRegisterLoop(_schLoop);
espurnaRegisterReload(_schConfigure);
}


+ 403
- 113
code/espurna/sensor.ino View File

@ -9,6 +9,7 @@ Copyright (C) 2016-2018 by Xose Pérez <xose dot perez at gmail dot com>
#if SENSOR_SUPPORT
#include <vector>
#include "filters/LastFilter.h"
#include "filters/MaxFilter.h"
#include "filters/MedianFilter.h"
#include "filters/MovingAverageFilter.h"
@ -21,9 +22,9 @@ typedef struct {
unsigned char type; // Type of measurement
unsigned char global; // Global index in its type
double current; // Current (last) value, unfiltered
double filtered; // Filtered (averaged) value
double reported; // Last reported value
double min_change; // Minimum value change to report
double max_change; // Maximum value change to report
} sensor_magnitude_t;
std::vector<BaseSensor *> _sensors;
@ -34,12 +35,17 @@ unsigned char _counts[MAGNITUDE_MAX];
bool _sensor_realtime = API_REAL_TIME_VALUES;
unsigned long _sensor_read_interval = 1000 * SENSOR_READ_INTERVAL;
unsigned char _sensor_report_every = SENSOR_REPORT_EVERY;
unsigned char _sensor_save_every = SENSOR_SAVE_EVERY;
unsigned char _sensor_power_units = SENSOR_POWER_UNITS;
unsigned char _sensor_energy_units = SENSOR_ENERGY_UNITS;
unsigned char _sensor_temperature_units = SENSOR_TEMPERATURE_UNITS;
double _sensor_temperature_correction = SENSOR_TEMPERATURE_CORRECTION;
double _sensor_humidity_correction = SENSOR_HUMIDITY_CORRECTION;
#if PZEM004T_SUPPORT
PZEM004TSensor *pzem004t_sensor;
#endif
String _sensor_energy_reset_ts = String();
// -----------------------------------------------------------------------------
@ -100,7 +106,7 @@ bool _sensorWebSocketOnReceive(const char * key, JsonVariant& value) {
if (strncmp(key, "sns", 3) == 0) return true;
if (strncmp(key, "tmp", 3) == 0) return true;
if (strncmp(key, "hum", 3) == 0) return true;
if (strncmp(key, "energy", 6) == 0) return true;
if (strncmp(key, "ene", 3) == 0) return true;
return false;
}
@ -109,11 +115,14 @@ void _sensorWebSocketSendData(JsonObject& root) {
char buffer[10];
bool hasTemperature = false;
bool hasHumidity = false;
bool hasMICS = false;
JsonArray& list = root.createNestedArray("magnitudes");
for (unsigned char i=0; i<_magnitudes.size(); i++) {
sensor_magnitude_t magnitude = _magnitudes[i];
if (magnitude.type == MAGNITUDE_EVENT) continue;
unsigned char decimals = _magnitudeDecimals(magnitude.type);
dtostrf(magnitude.current, 1-sizeof(buffer), decimals, buffer);
@ -125,19 +134,22 @@ void _sensorWebSocketSendData(JsonObject& root) {
element["error"] = magnitude.sensor->error();
if (magnitude.type == MAGNITUDE_ENERGY) {
if (_sensor_energy_reset_ts.length() == 0) _sensorReset();
element["description"] = magnitude.sensor->slot(magnitude.local) + _sensor_energy_reset_ts;
if (_sensor_energy_reset_ts.length() == 0) _sensorResetTS();
element["description"] = magnitude.sensor->slot(magnitude.local) + String(" (since ") + _sensor_energy_reset_ts + String(")");
} else {
element["description"] = magnitude.sensor->slot(magnitude.local);
}
if (magnitude.type == MAGNITUDE_TEMPERATURE) hasTemperature = true;
if (magnitude.type == MAGNITUDE_HUMIDITY) hasHumidity = true;
#if MICS2710_SUPPORT || MICS5525_SUPPORT
if (magnitude.type == MAGNITUDE_CO || magnitude.type == MAGNITUDE_NO2) hasMICS = true;
#endif
}
if (hasTemperature) root["temperatureVisible"] = 1;
if (hasHumidity) root["humidityVisible"] = 1;
if (hasMICS) root["micsVisible"] = 1;
}
@ -191,15 +203,16 @@ void _sensorWebSocketStart(JsonObject& root) {
}
if (_magnitudes.size() > 0) {
root["sensorsVisible"] = 1;
root["snsVisible"] = 1;
//root["apiRealTime"] = _sensor_realtime;
root["pwrUnits"] = _sensor_power_units;
root["energyUnits"] = _sensor_energy_units;
root["eneUnits"] = _sensor_energy_units;
root["tmpUnits"] = _sensor_temperature_units;
root["tmpCorrection"] = _sensor_temperature_correction;
root["humCorrection"] = _sensor_humidity_correction;
root["snsRead"] = _sensor_read_interval / 1000;
root["snsReport"] = _sensor_report_every;
root["snsSave"] = _sensor_save_every;
}
/*
@ -221,6 +234,10 @@ void _sensorWebSocketStart(JsonObject& root) {
}
#endif // WEB_SUPPORT
#if API_SUPPORT
void _sensorAPISetup() {
for (unsigned char magnitude_id=0; magnitude_id<_magnitudes.size(); magnitude_id++) {
@ -233,14 +250,15 @@ void _sensorAPISetup() {
apiRegister(topic.c_str(), [magnitude_id](char * buffer, size_t len) {
sensor_magnitude_t magnitude = _magnitudes[magnitude_id];
unsigned char decimals = _magnitudeDecimals(magnitude.type);
double value = _sensor_realtime ? magnitude.current : magnitude.filtered;
double value = _sensor_realtime ? magnitude.current : magnitude.reported;
dtostrf(value, 1-len, decimals, buffer);
});
}
}
#endif
#endif // API_SUPPORT
#if TERMINAL_SUPPORT
@ -258,6 +276,63 @@ void _sensorInitCommands() {
}
DEBUG_MSG_P(PSTR("+OK\n"));
});
#if PZEM004T_SUPPORT
settingsRegisterCommand(F("PZ.ADDRESS"), [](Embedis* e) {
if (e->argc == 1) {
DEBUG_MSG_P(PSTR("[SENSOR] PZEM004T\n"));
unsigned char dev_count = pzem004t_sensor->getAddressesCount();
for(unsigned char dev = 0; dev < dev_count; dev++) {
DEBUG_MSG_P(PSTR("Device %d Address %s\n"), dev, pzem004t_sensor->getAddress(dev).c_str());
}
DEBUG_MSG_P(PSTR("+OK\n"));
} else if(e->argc == 2) {
IPAddress addr;
if (addr.fromString(String(e->argv[1]))) {
if(pzem004t_sensor->setDeviceAddress(&addr)) {
DEBUG_MSG_P(PSTR("+OK\n"));
}
} else {
DEBUG_MSG_P(PSTR("-ERROR: Invalid address argument\n"));
}
} else {
DEBUG_MSG_P(PSTR("-ERROR: Wrong arguments\n"));
}
});
settingsRegisterCommand(F("PZ.RESET"), [](Embedis* e) {
if(e->argc > 2) {
DEBUG_MSG_P(PSTR("-ERROR: Wrong arguments\n"));
} else {
unsigned char init = e->argc == 2 ? String(e->argv[1]).toInt() : 0;
unsigned char limit = e->argc == 2 ? init +1 : pzem004t_sensor->getAddressesCount();
DEBUG_MSG_P(PSTR("[SENSOR] PZEM004T\n"));
for(unsigned char dev = init; dev < limit; dev++) {
float offset = pzem004t_sensor->resetEnergy(dev);
setSetting("pzEneTotal", dev, offset);
DEBUG_MSG_P(PSTR("Device %d Address %s - Offset: %s\n"), dev, pzem004t_sensor->getAddress(dev).c_str(), String(offset).c_str());
}
DEBUG_MSG_P(PSTR("+OK\n"));
}
});
settingsRegisterCommand(F("PZ.VALUE"), [](Embedis* e) {
if(e->argc > 2) {
DEBUG_MSG_P(PSTR("-ERROR: Wrong arguments\n"));
} else {
unsigned char init = e->argc == 2 ? String(e->argv[1]).toInt() : 0;
unsigned char limit = e->argc == 2 ? init +1 : pzem004t_sensor->getAddressesCount();
DEBUG_MSG_P(PSTR("[SENSOR] PZEM004T\n"));
for(unsigned char dev = init; dev < limit; dev++) {
DEBUG_MSG_P(PSTR("Device %d/%s - Current: %s Voltage: %s Power: %s Energy: %s\n"), //
dev,
pzem004t_sensor->getAddress(dev).c_str(),
String(pzem004t_sensor->value(dev * PZ_MAGNITUDE_CURRENT_INDEX)).c_str(),
String(pzem004t_sensor->value(dev * PZ_MAGNITUDE_VOLTAGE_INDEX)).c_str(),
String(pzem004t_sensor->value(dev * PZ_MAGNITUDE_POWER_ACTIVE_INDEX)).c_str(),
String(pzem004t_sensor->value(dev * PZ_MAGNITUDE_ENERGY_INDEX)).c_str());
}
DEBUG_MSG_P(PSTR("+OK\n"));
}
});
#endif
}
#endif
@ -286,11 +361,18 @@ void _sensorPost() {
}
}
void _sensorReset() {
void _sensorResetTS() {
#if NTP_SUPPORT
if (ntpSynced()) {
_sensor_energy_reset_ts = String(" (since ") + ntpDateTime() + String(")");
if (_sensor_energy_reset_ts.length() == 0) {
_sensor_energy_reset_ts = ntpDateTime(now() - millis() / 1000);
} else {
_sensor_energy_reset_ts = ntpDateTime(now());
}
} else {
_sensor_energy_reset_ts = String();
}
setSetting("snsResetTS", _sensor_energy_reset_ts);
#endif
}
@ -313,17 +395,19 @@ void _sensorLoad() {
*/
#if AM2320_SUPPORT
{
AM2320Sensor * sensor = new AM2320Sensor();
sensor->setAddress(AM2320_ADDRESS);
_sensors.push_back(sensor);
}
#endif
#if AM2320_SUPPORT
{
AM2320Sensor * sensor = new AM2320Sensor();
sensor->setAddress(AM2320_ADDRESS);
_sensors.push_back(sensor);
}
#endif
#if ANALOG_SUPPORT
{
AnalogSensor * sensor = new AnalogSensor();
sensor->setSamples(ANALOG_SAMPLES);
sensor->setDelay(ANALOG_DELAY);
_sensors.push_back(sensor);
}
#endif
@ -444,7 +528,8 @@ void _sensorLoad() {
{
EventSensor * sensor = new EventSensor();
sensor->setGPIO(EVENTS_PIN);
sensor->setMode(EVENTS_PIN_MODE);
sensor->setTrigger(EVENTS_TRIGGER);
sensor->setPinMode(EVENTS_PIN_MODE);
sensor->setDebounceTime(EVENTS_DEBOUNCE);
sensor->setInterruptMode(EVENTS_INTERRUPT_MODE);
_sensors.push_back(sensor);
@ -471,11 +556,13 @@ void _sensorLoad() {
}
#endif
#if HCSR04_SUPPORT
#if SONAR_SUPPORT
{
HCSR04Sensor * sensor = new HCSR04Sensor();
sensor->setTrigger(HCSR04_TRIGGER);
sensor->setEcho(HCSR04_ECHO);
SonarSensor * sensor = new SonarSensor();
sensor->setEcho(SONAR_ECHO);
sensor->setIterations(SONAR_ITERATIONS);
sensor->setMaxDistance(SONAR_MAX_DISTANCE);
sensor->setTrigger(SONAR_TRIGGER);
_sensors.push_back(sensor);
}
#endif
@ -500,6 +587,39 @@ void _sensorLoad() {
}
#endif
#if MICS2710_SUPPORT
{
MICS2710Sensor * sensor = new MICS2710Sensor();
sensor->setAnalogGPIO(MICS2710_NOX_PIN);
sensor->setPreHeatGPIO(MICS2710_PRE_PIN);
sensor->setRL(MICS2710_RL);
_sensors.push_back(sensor);
}
#endif
#if MICS5525_SUPPORT
{
MICS5525Sensor * sensor = new MICS5525Sensor();
sensor->setAnalogGPIO(MICS5525_RED_PIN);
sensor->setRL(MICS5525_RL);
_sensors.push_back(sensor);
}
#endif
#if NTC_SUPPORT
{
NTCSensor * sensor = new NTCSensor();
sensor->setSamples(NTC_SAMPLES);
sensor->setDelay(NTC_DELAY);
sensor->setUpstreamResistor(NTC_R_UP);
sensor->setDownstreamResistor(NTC_R_DOWN);
sensor->setBeta(NTC_BETA);
sensor->setR0(NTC_R0);
sensor->setT0(NTC_T0);
_sensors.push_back(sensor);
}
#endif
#if SENSEAIR_SUPPORT
{
SenseAirSensor * sensor = new SenseAirSensor();
@ -509,11 +629,24 @@ void _sensorLoad() {
}
#endif
#if SDS011_SUPPORT
{
SDS011Sensor * sensor = new SDS011Sensor();
sensor->setRX(SDS011_RX_PIN);
sensor->setTX(SDS011_TX_PIN);
_sensors.push_back(sensor);
}
#endif
#if PMSX003_SUPPORT
{
PMSX003Sensor * sensor = new PMSX003Sensor();
sensor->setRX(PMS_RX_PIN);
sensor->setTX(PMS_TX_PIN);
#if PMS_USE_SOFT
sensor->setRX(PMS_RX_PIN);
sensor->setTX(PMS_TX_PIN);
#else
sensor->setSerial(& PMS_HW_PORT);
#endif
sensor->setType(PMS_TYPE);
_sensors.push_back(sensor);
}
@ -521,13 +654,20 @@ void _sensorLoad() {
#if PZEM004T_SUPPORT
{
PZEM004TSensor * sensor = new PZEM004TSensor();
PZEM004TSensor * sensor = pzem004t_sensor = new PZEM004TSensor();
#if PZEM004T_USE_SOFT
sensor->setRX(PZEM004T_RX_PIN);
sensor->setTX(PZEM004T_TX_PIN);
#else
sensor->setSerial(& PZEM004T_HW_PORT);
#endif
sensor->setAddresses(PZEM004T_ADDRESSES);
// Read saved energy offset
unsigned char dev_count = sensor->getAddressesCount();
for(unsigned char dev = 0; dev < dev_count; dev++) {
float value = getSetting("pzEneTotal", dev, 0).toFloat();
if (value > 0) sensor->resetEnergy(dev, value);
}
_sensors.push_back(sensor);
}
#endif
@ -567,13 +707,23 @@ void _sensorLoad() {
}
void _sensorCallback(unsigned char i, unsigned char type, const char * payload) {
DEBUG_MSG_P(PSTR("[SENSOR] Sensor #%u callback, type %u, payload: '%s'\n"), i, type, payload);
void _sensorCallback(unsigned char i, unsigned char type, double value) {
DEBUG_MSG_P(PSTR("[SENSOR] Sensor #%u callback, type %u, payload: '%s'\n"), i, type, String(value).c_str());
for (unsigned char k=0; k<_magnitudes.size(); k++) {
if ((_sensors[i] == _magnitudes[k].sensor) && (type == _magnitudes[k].type)) {
_sensorReport(k, value);
return;
}
}
}
void _sensorInit() {
_sensors_ready = true;
_sensor_save_every = getSetting("snsSave", 0).toInt();
for (unsigned char i=0; i<_sensors.size(); i++) {
@ -600,17 +750,30 @@ void _sensorInit() {
new_magnitude.type = type;
new_magnitude.global = _counts[type];
new_magnitude.current = 0;
new_magnitude.filtered = 0;
new_magnitude.reported = 0;
new_magnitude.min_change = 0;
if (type == MAGNITUDE_DIGITAL) {
new_magnitude.max_change = 0;
// TODO: find a proper way to extend this to min/max of any magnitude
if (MAGNITUDE_ENERGY == type) {
new_magnitude.max_change = getSetting("eneMaxDelta", ENERGY_MAX_CHANGE).toFloat();
} else if (MAGNITUDE_TEMPERATURE == type) {
new_magnitude.min_change = getSetting("tmpMinDelta", TEMPERATURE_MIN_CHANGE).toFloat();
} else if (MAGNITUDE_HUMIDITY == type) {
new_magnitude.min_change = getSetting("humMinDelta", HUMIDITY_MIN_CHANGE).toFloat();
}
if (MAGNITUDE_ENERGY == type) {
new_magnitude.filter = new LastFilter();
} else if (MAGNITUDE_DIGITAL == type) {
new_magnitude.filter = new MaxFilter();
} else if (type == MAGNITUDE_EVENTS || type == MAGNITUDE_GEIGER_CPM|| type == MAGNITUDE_GEIGER_SIEVERT) { // For geiger counting moving average filter is the most appropriate if needed at all.
} else if (MAGNITUDE_COUNT == type || MAGNITUDE_GEIGER_CPM == type || MAGNITUDE_GEIGER_SIEVERT == type) { // For geiger counting moving average filter is the most appropriate if needed at all.
new_magnitude.filter = new MovingAverageFilter();
} else {
new_magnitude.filter = new MedianFilter();
}
new_magnitude.filter->resize(_sensor_report_every);
_magnitudes.push_back(new_magnitude);
DEBUG_MSG_P(PSTR("[SENSOR] -> %s:%d\n"), magnitudeTopic(type).c_str(), _counts[type]);
@ -620,18 +783,34 @@ void _sensorInit() {
}
// Hook callback
_sensors[i]->onEvent([i](unsigned char type, const char * payload) {
_sensorCallback(i, type, payload);
_sensors[i]->onEvent([i](unsigned char type, double value) {
_sensorCallback(i, type, value);
});
// Custom initializations
#if MICS2710_SUPPORT
if (_sensors[i]->getID() == SENSOR_MICS2710_ID) {
MICS2710Sensor * sensor = (MICS2710Sensor *) _sensors[i];
sensor->setR0(getSetting("snsR0", MICS2710_R0).toInt());
}
#endif // MICS2710_SUPPORT
#if MICS5525_SUPPORT
if (_sensors[i]->getID() == SENSOR_MICS5525_ID) {
MICS5525Sensor * sensor = (MICS5525Sensor *) _sensors[i];
sensor->setR0(getSetting("snsR0", MICS5525_R0).toInt());
}
#endif // MICS5525_SUPPORT
#if EMON_ANALOG_SUPPORT
if (_sensors[i]->getID() == SENSOR_EMON_ANALOG_ID) {
EmonAnalogSensor * sensor = (EmonAnalogSensor *) _sensors[i];
sensor->setCurrentRatio(0, getSetting("pwrRatioC", EMON_CURRENT_RATIO).toFloat());
sensor->setVoltage(getSetting("pwrVoltage", EMON_MAINS_VOLTAGE).toInt());
double value = (_sensor_save_every > 0) ? getSetting("eneTotal", 0).toInt() : 0;
if (value > 0) sensor->resetEnergy(0, value);
}
#endif // EMON_ANALOG_SUPPORT
@ -653,6 +832,9 @@ void _sensorInit() {
value = getSetting("pwrRatioP", HLW8012_POWER_RATIO).toFloat();
if (value > 0) sensor->setPowerRatio(value);
value = (_sensor_save_every > 0) ? getSetting("eneTotal", 0).toInt() : 0;
if (value > 0) sensor->resetEnergy(value);
}
#endif // HLW8012_SUPPORT
@ -674,6 +856,9 @@ void _sensorInit() {
value = getSetting("pwrRatioP", 0).toFloat();
if (value > 0) sensor->setPowerRatio(value);
value = (_sensor_save_every > 0) ? getSetting("eneTotal", 0).toInt() : 0;
if (value > 0) sensor->resetEnergy(value);
}
#endif // CSE7766_SUPPORT
@ -687,16 +872,42 @@ void _sensorConfigure() {
// General sensor settings
_sensor_read_interval = 1000 * constrain(getSetting("snsRead", SENSOR_READ_INTERVAL).toInt(), SENSOR_READ_MIN_INTERVAL, SENSOR_READ_MAX_INTERVAL);
_sensor_report_every = constrain(getSetting("snsReport", SENSOR_REPORT_EVERY).toInt(), SENSOR_REPORT_MIN_EVERY, SENSOR_REPORT_MAX_EVERY);
_sensor_save_every = getSetting("snsSave", SENSOR_SAVE_EVERY).toInt();
_sensor_realtime = getSetting("apiRealTime", API_REAL_TIME_VALUES).toInt() == 1;
_sensor_power_units = getSetting("pwrUnits", SENSOR_POWER_UNITS).toInt();
_sensor_energy_units = getSetting("energyUnits", SENSOR_ENERGY_UNITS).toInt();
_sensor_energy_units = getSetting("eneUnits", SENSOR_ENERGY_UNITS).toInt();
_sensor_temperature_units = getSetting("tmpUnits", SENSOR_TEMPERATURE_UNITS).toInt();
_sensor_temperature_correction = getSetting("tmpCorrection", SENSOR_TEMPERATURE_CORRECTION).toFloat();
_sensor_humidity_correction = getSetting("humCorrection", SENSOR_HUMIDITY_CORRECTION).toFloat();
_sensor_energy_reset_ts = getSetting("snsResetTS", "");
// Specific sensor settings
for (unsigned char i=0; i<_sensors.size(); i++) {
#if MICS2710_SUPPORT
if (_sensors[i]->getID() == SENSOR_MICS2710_ID) {
if (getSetting("snsResetCalibration", 0).toInt() == 1) {
MICS2710Sensor * sensor = (MICS2710Sensor *) _sensors[i];
sensor->calibrate();
setSetting("snsR0", sensor->getR0());
}
}
#endif // MICS2710_SUPPORT
#if MICS5525_SUPPORT
if (_sensors[i]->getID() == SENSOR_MICS5525_ID) {
if (getSetting("snsResetCalibration", 0).toInt() == 1) {
MICS5525Sensor * sensor = (MICS5525Sensor *) _sensors[i];
sensor->calibrate();
setSetting("snsR0", sensor->getR0());
}
}
#endif // MICS5525_SUPPORT
#if EMON_ANALOG_SUPPORT
if (_sensors[i]->getID() == SENSOR_EMON_ANALOG_ID) {
@ -704,7 +915,7 @@ void _sensorConfigure() {
double value;
EmonAnalogSensor * sensor = (EmonAnalogSensor *) _sensors[i];
if (value = getSetting("pwrExpectedP", 0).toInt()) {
if ((value = getSetting("pwrExpectedP", 0).toInt())) {
sensor->expectedPower(0, value);
setSetting("pwrRatioC", sensor->getCurrentRatio(0));
}
@ -716,7 +927,8 @@ void _sensorConfigure() {
if (getSetting("pwrResetE", 0).toInt() == 1) {
sensor->resetEnergy();
_sensorReset();
delSetting("eneTotal");
_sensorResetTS();
}
sensor->setVoltage(getSetting("pwrVoltage", EMON_MAINS_VOLTAGE).toInt());
@ -730,7 +942,8 @@ void _sensorConfigure() {
EmonADC121Sensor * sensor = (EmonADC121Sensor *) _sensors[i];
if (getSetting("pwrResetE", 0).toInt() == 1) {
sensor->resetEnergy();
_sensorReset();
delSetting("eneTotal");
_sensorResetTS();
}
}
#endif
@ -740,7 +953,8 @@ void _sensorConfigure() {
EmonADS1X15Sensor * sensor = (EmonADS1X15Sensor *) _sensors[i];
if (getSetting("pwrResetE", 0).toInt() == 1) {
sensor->resetEnergy();
_sensorReset();
delSetting("eneTotal");
_sensorResetTS();
}
}
#endif
@ -770,7 +984,8 @@ void _sensorConfigure() {
if (getSetting("pwrResetE", 0).toInt() == 1) {
sensor->resetEnergy();
_sensorReset();
delSetting("eneTotal");
_sensorResetTS();
}
if (getSetting("pwrResetCalibration", 0).toInt() == 1) {
@ -791,24 +1006,25 @@ void _sensorConfigure() {
double value;
CSE7766Sensor * sensor = (CSE7766Sensor *) _sensors[i];
if (value = getSetting("pwrExpectedC", 0).toFloat()) {
if ((value = getSetting("pwrExpectedC", 0).toFloat())) {
sensor->expectedCurrent(value);
setSetting("pwrRatioC", sensor->getCurrentRatio());
}
if (value = getSetting("pwrExpectedV", 0).toInt()) {
if ((value = getSetting("pwrExpectedV", 0).toInt())) {
sensor->expectedVoltage(value);
setSetting("pwrRatioV", sensor->getVoltageRatio());
}
if (value = getSetting("pwrExpectedP", 0).toInt()) {
if ((value = getSetting("pwrExpectedP", 0).toInt())) {
sensor->expectedPower(value);
setSetting("pwrRatioP", sensor->getPowerRatio());
}
if (getSetting("pwrResetE", 0).toInt() == 1) {
sensor->resetEnergy();
_sensorReset();
delSetting("eneTotal");
_sensorResetTS();
}
if (getSetting("pwrResetCalibration", 0).toInt() == 1) {
@ -822,6 +1038,22 @@ void _sensorConfigure() {
#endif // CSE7766_SUPPORT
#if PZEM004T_SUPPORT
if (_sensors[i]->getID() == SENSOR_PZEM004T_ID) {
PZEM004TSensor * sensor = (PZEM004TSensor *) _sensors[i];
if (getSetting("pwrResetE", 0).toInt() == 1) {
unsigned char dev_count = sensor->getAddressesCount();
for(unsigned char dev = 0; dev < dev_count; dev++) {
sensor->resetEnergy(dev, 0);
delSetting("pzEneTotal", dev);
}
_sensorResetTS();
}
}
#endif // PZEM004T_SUPPORT
}
// Update filter sizes
@ -829,7 +1061,13 @@ void _sensorConfigure() {
_magnitudes[i].filter->resize(_sensor_report_every);
}
// General processing
if (0 == _sensor_save_every) {
delSetting("eneTotal");
}
// Save settings
delSetting("snsResetCalibration");
delSetting("pwrExpectedP");
delSetting("pwrExpectedC");
delSetting("pwrExpectedV");
@ -839,6 +1077,72 @@ void _sensorConfigure() {
}
void _sensorReport(unsigned char index, double value) {
sensor_magnitude_t magnitude = _magnitudes[index];
unsigned char decimals = _magnitudeDecimals(magnitude.type);
char buffer[10];
dtostrf(value, 1-sizeof(buffer), decimals, buffer);
#if BROKER_SUPPORT
brokerPublish(magnitudeTopic(magnitude.type).c_str(), magnitude.local, buffer);
#endif
#if MQTT_SUPPORT
mqttSend(magnitudeTopicIndex(index).c_str(), buffer);
#if SENSOR_PUBLISH_ADDRESSES
char topic[32];
snprintf(topic, sizeof(topic), "%s/%s", SENSOR_ADDRESS_TOPIC, magnitudeTopic(magnitude.type).c_str());
if (SENSOR_USE_INDEX || (_counts[magnitude.type] > 1)) {
mqttSend(topic, magnitude.global, magnitude.sensor->address(magnitude.local).c_str());
} else {
mqttSend(topic, magnitude.sensor->address(magnitude.local).c_str());
}
#endif // SENSOR_PUBLISH_ADDRESSES
#endif // MQTT_SUPPORT
#if INFLUXDB_SUPPORT
if (SENSOR_USE_INDEX || (_counts[magnitude.type] > 1)) {
idbSend(magnitudeTopic(magnitude.type).c_str(), magnitude.global, buffer);
} else {
idbSend(magnitudeTopic(magnitude.type).c_str(), buffer);
}
#endif // INFLUXDB_SUPPORT
#if THINGSPEAK_SUPPORT
tspkEnqueueMeasurement(index, buffer);
#endif
#if DOMOTICZ_SUPPORT
{
char key[15];
snprintf_P(key, sizeof(key), PSTR("dczMagnitude%d"), index);
if (magnitude.type == MAGNITUDE_HUMIDITY) {
int status;
if (value > 70) {
status = HUMIDITY_WET;
} else if (value > 45) {
status = HUMIDITY_COMFORTABLE;
} else if (value > 30) {
status = HUMIDITY_NORMAL;
} else {
status = HUMIDITY_DRY;
}
char status_buf[5];
itoa(status, status_buf, 10);
domoticzSend(key, buffer, status_buf);
} else {
domoticzSend(key, 0, buffer);
}
}
#endif // DOMOTICZ_SUPPORT
}
// -----------------------------------------------------------------------------
// Public
// -----------------------------------------------------------------------------
@ -919,6 +1223,7 @@ void sensorSetup() {
// Backwards compatibility
moveSetting("powerUnits", "pwrUnits");
moveSetting("energyUnits", "eneUnits");
// Load sensors
_sensorLoad();
@ -927,25 +1232,26 @@ void sensorSetup() {
// Configure stored values
_sensorConfigure();
// Websockets
#if WEB_SUPPORT
// Websockets
wsOnSendRegister(_sensorWebSocketStart);
wsOnReceiveRegister(_sensorWebSocketOnReceive);
wsOnSendRegister(_sensorWebSocketSendData);
wsOnAfterParseRegister(_sensorConfigure);
#endif
// API
// API
#if API_SUPPORT
_sensorAPISetup();
#endif
// Terminal
#if TERMINAL_SUPPORT
_sensorInitCommands();
#endif
// Register loop
// Main callbacks
espurnaRegisterLoop(sensorLoop);
espurnaRegisterReload(_sensorConfigure);
}
@ -968,6 +1274,7 @@ void sensorLoop() {
// Check if we should read new data
static unsigned long last_update = 0;
static unsigned long report_count = 0;
static unsigned long save_count = 0;
if (millis() - last_update > _sensor_read_interval) {
last_update = millis();
@ -975,7 +1282,6 @@ void sensorLoop() {
double current;
double filtered;
char buffer[64];
// Pre-read hook
_sensorPre();
@ -992,6 +1298,10 @@ void sensorLoop() {
if (magnitude.sensor->status()) {
// -------------------------------------------------------------
// Instant value
// -------------------------------------------------------------
current = magnitude.sensor->value(magnitude.local);
// Completely remove spurious values if relay is OFF
@ -1008,22 +1318,30 @@ void sensorLoop() {
}
#endif
// -------------------------------------------------------------
// Processing (filters)
// -------------------------------------------------------------
magnitude.filter->add(current);
// Special case
if (magnitude.type == MAGNITUDE_EVENTS) {
// Special case for MovingAvergaeFilter
if (MAGNITUDE_COUNT == magnitude.type ||
MAGNITUDE_GEIGER_CPM ==magnitude. type ||
MAGNITUDE_GEIGER_SIEVERT == magnitude.type) {
current = magnitude.filter->result();
}
current = _magnitudeProcess(magnitude.type, current);
_magnitudes[i].current = current;
unsigned char decimals = _magnitudeDecimals(magnitude.type);
// -------------------------------------------------------------
// Debug
// -------------------------------------------------------------
#if SENSOR_DEBUG
{
dtostrf(current, 1-sizeof(buffer), decimals, buffer);
char buffer[64];
dtostrf(current, 1-sizeof(buffer), _magnitudeDecimals(magnitude.type), buffer);
DEBUG_MSG_P(PSTR("[SENSOR] %s - %s: %s%s\n"),
magnitude.sensor->slot(magnitude.local).c_str(),
magnitudeTopic(magnitude.type).c_str(),
@ -1033,78 +1351,50 @@ void sensorLoop() {
}
#endif // SENSOR_DEBUG
// Time to report (we do it every _sensor_report_every readings)
if (report_count == 0) {
// -------------------------------------------------------------
// Report
// (we do it every _sensor_report_every readings)
// -------------------------------------------------------------
bool report = (0 == report_count);
if ((MAGNITUDE_ENERGY == magnitude.type) && (magnitude.max_change > 0)) {
// for MAGNITUDE_ENERGY, filtered value is last value
double value = _magnitudeProcess(magnitude.type, current);
report = (fabs(value - magnitude.reported) >= magnitude.max_change);
} // if ((MAGNITUDE_ENERGY == magnitude.type) && (magnitude.max_change > 0))
if (report) {
filtered = magnitude.filter->result();
magnitude.filter->reset();
filtered = _magnitudeProcess(magnitude.type, filtered);
_magnitudes[i].filtered = filtered;
magnitude.filter->reset();
// Check if there is a minimum change threshold to report
if (fabs(filtered - magnitude.reported) >= magnitude.min_change) {
_magnitudes[i].reported = filtered;
dtostrf(filtered, 1-sizeof(buffer), decimals, buffer);
#if BROKER_SUPPORT
brokerPublish(magnitudeTopic(magnitude.type).c_str(), magnitude.local, buffer);
#endif
#if MQTT_SUPPORT
_sensorReport(i, filtered);
} // if (fabs(filtered - magnitude.reported) >= magnitude.min_change)
mqttSend(magnitudeTopicIndex(i).c_str(), buffer);
// -------------------------------------------------------------
// Saving to EEPROM
// (we do it every _sensor_save_every readings)
// -------------------------------------------------------------
#if SENSOR_PUBLISH_ADDRESSES
char topic[32];
snprintf(topic, sizeof(topic), "%s/%s", SENSOR_ADDRESS_TOPIC, magnitudeTopic(magnitude.type).c_str());
if (SENSOR_USE_INDEX || (_counts[magnitude.type] > 1)) {
mqttSend(topic, magnitude.global, magnitude.sensor->address(magnitude.local).c_str());
} else {
mqttSend(topic, magnitude.sensor->address(magnitude.local).c_str());
}
#endif // SENSOR_PUBLISH_ADDRESSES
if (_sensor_save_every > 0) {
#endif // MQTT_SUPPORT
save_count = (save_count + 1) % _sensor_save_every;
#if INFLUXDB_SUPPORT
if (SENSOR_USE_INDEX || (_counts[magnitude.type] > 1)) {
idbSend(magnitudeTopic(magnitude.type).c_str(), magnitude.global, buffer);
} else {
idbSend(magnitudeTopic(magnitude.type).c_str(), buffer);
}
#endif // INFLUXDB_SUPPORT
#if THINGSPEAK_SUPPORT
tspkEnqueueMeasurement(i, buffer);
#endif
#if DOMOTICZ_SUPPORT
{
char key[15];
snprintf_P(key, sizeof(key), PSTR("dczMagnitude%d"), i);
if (magnitude.type == MAGNITUDE_HUMIDITY) {
int status;
if (filtered > 70) {
status = HUMIDITY_WET;
} else if (filtered > 45) {
status = HUMIDITY_COMFORTABLE;
} else if (filtered > 30) {
status = HUMIDITY_NORMAL;
} else {
status = HUMIDITY_DRY;
}
char status_buf[5];
itoa(status, status_buf, 10);
domoticzSend(key, buffer, status_buf);
} else {
domoticzSend(key, 0, buffer);
if (0 == save_count) {
if (MAGNITUDE_ENERGY == magnitude.type) {
setSetting("eneTotal", current);
saveSettings();
}
}
#endif // DOMOTICZ_SUPPORT
} // if (0 == save_count)
} // if (_sensor_save_every > 0)
} // if (fabs(filtered - magnitude.reported) >= magnitude.min_change)
} // if (report_count == 0)
} // if (magnitude.sensor->status())
} // for (unsigned char i=0; i<_magnitudes.size(); i++)


+ 34
- 2
code/espurna/sensors/AnalogSensor.h View File

@ -3,7 +3,7 @@
// Copyright (C) 2017-2018 by Xose Pérez <xose dot perez at gmail dot com>
// -----------------------------------------------------------------------------
#if SENSOR_SUPPORT && ANALOG_SUPPORT
#if SENSOR_SUPPORT && (ANALOG_SUPPORT || NTC_SUPPORT)
#pragma once
@ -27,6 +27,24 @@ class AnalogSensor : public BaseSensor {
_sensor_id = SENSOR_ANALOG_ID;
}
void setSamples(unsigned int samples) {
if (_samples > 0) _samples = samples;
}
void setDelay(unsigned long micros) {
_micros = micros;
}
// ---------------------------------------------------------------------
unsigned int getSamples() {
return _samples;
}
unsigned long getDelay() {
return _micros;
}
// ---------------------------------------------------------------------
// Sensor API
// ---------------------------------------------------------------------
@ -60,10 +78,24 @@ class AnalogSensor : public BaseSensor {
// Current value for slot # index
double value(unsigned char index) {
if (index == 0) return analogRead(0);
if (index == 0) return _read();
return 0;
}
protected:
unsigned int _read() {
if (1 == _samples) return analogRead(0);
unsigned long sum = 0;
for (unsigned int i=0; i<_samples; i++) {
if (i>0) delayMicroseconds(_micros);
sum += analogRead(0);
}
return sum / _samples;
}
unsigned int _samples = 1;
unsigned long _micros = 0;
};


+ 6
- 6
code/espurna/sensors/BaseSensor.h View File

@ -21,7 +21,7 @@
#define SENSOR_ERROR_CALIBRATION 8 // Calibration error or Not calibrated
#define SENSOR_ERROR_OTHER 99 // Any other error
typedef std::function<void(unsigned char, const char *)> TSensorCallback;
typedef std::function<void(unsigned char, double)> TSensorCallback;
class BaseSensor {
@ -46,19 +46,19 @@ class BaseSensor {
virtual void post() {}
// Descriptive name of the sensor
virtual String description() {}
virtual String description() = 0;
// Address of the sensor (it could be the GPIO or I2C address)
virtual String address(unsigned char index) {}
virtual String address(unsigned char index) = 0;
// Descriptive name of the slot # index
virtual String slot(unsigned char index) {};
virtual String slot(unsigned char index) = 0;
// Type for slot # index
virtual unsigned char type(unsigned char index) {}
virtual unsigned char type(unsigned char index) = 0;
// Current value for slot # index
virtual double value(unsigned char index) {}
virtual double value(unsigned char index) = 0;
// Retrieve current instance configuration
virtual void getConfig(JsonObject& root) {};


+ 9
- 5
code/espurna/sensors/CSE7766Sensor.h View File

@ -22,7 +22,7 @@ class CSE7766Sensor : public BaseSensor {
// ---------------------------------------------------------------------
CSE7766Sensor(): BaseSensor(), _data() {
_count = 4;
_count = 6;
_sensor_id = SENSOR_CSE7766_ID;
}
@ -102,8 +102,8 @@ class CSE7766Sensor : public BaseSensor {
_ratioC = _ratioV = _ratioP = 1.0;
}
void resetEnergy() {
_energy = 0;
void resetEnergy(double value = 0) {
_energy = value;
}
// ---------------------------------------------------------------------
@ -161,7 +161,9 @@ class CSE7766Sensor : public BaseSensor {
if (index == 0) return MAGNITUDE_CURRENT;
if (index == 1) return MAGNITUDE_VOLTAGE;
if (index == 2) return MAGNITUDE_POWER_ACTIVE;
if (index == 3) return MAGNITUDE_ENERGY;
if (index == 3) return MAGNITUDE_POWER_APPARENT;
if (index == 4) return MAGNITUDE_POWER_FACTOR;
if (index == 5) return MAGNITUDE_ENERGY;
return MAGNITUDE_NONE;
}
@ -170,7 +172,9 @@ class CSE7766Sensor : public BaseSensor {
if (index == 0) return _current;
if (index == 1) return _voltage;
if (index == 2) return _active;
if (index == 3) return _energy;
if (index == 3) return _voltage * _current;
if (index == 4) return ((_voltage > 0) && (_current > 0)) ? 100 * _active / _voltage / _current : 100;
if (index == 5) return _energy;
return 0;
}


+ 11
- 3
code/espurna/sensors/DHTSensor.h View File

@ -15,6 +15,7 @@
#define DHT_MIN_INTERVAL 2000
#define DHT_CHIP_DHT11 11
#define DHT_CHIP_DHT12 12
#define DHT_CHIP_DHT22 22
#define DHT_CHIP_DHT21 21
#define DHT_CHIP_AM2301 21
@ -145,7 +146,7 @@ class DHTSensor : public BaseSensor {
pinMode(_gpio, OUTPUT);
noInterrupts();
digitalWrite(_gpio, LOW);
if (_type == DHT_CHIP_DHT11) {
if ((_type == DHT_CHIP_DHT11) || (_type == DHT_CHIP_DHT12)) {
nice_delay(20);
} else {
delayMicroseconds(500);
@ -201,6 +202,9 @@ class DHTSensor : public BaseSensor {
// Get humidity from Data[0] and Data[1]
if (_type == DHT_CHIP_DHT11) {
_humidity = dhtData[0];
} else if (_type == DHT_CHIP_DHT12) {
_humidity = dhtData[0];
_humidity += dhtData[1] * 0.1;
} else {
_humidity = dhtData[0] * 256 + dhtData[1];
_humidity /= 10;
@ -209,6 +213,10 @@ class DHTSensor : public BaseSensor {
// Get temp from Data[2] and Data[3]
if (_type == DHT_CHIP_DHT11) {
_temperature = dhtData[2];
} else if (_type == DHT_CHIP_DHT12) {
_temperature = (dhtData[2] & 0x7F);
_temperature += dhtData[3] * 0.1;
if (dhtData[2] & 0x80) _temperature *= -1;
} else {
_temperature = (dhtData[2] & 0x7F) * 256 + dhtData[3];
_temperature /= 10;
@ -221,7 +229,7 @@ class DHTSensor : public BaseSensor {
}
unsigned long _signal(int usTimeOut, bool state) {
unsigned long _signal(unsigned long usTimeOut, bool state) {
unsigned long uSec = 1;
while (digitalRead(_gpio) == state) {
if (++uSec > usTimeOut) return 0;
@ -238,7 +246,7 @@ class DHTSensor : public BaseSensor {
unsigned char _errors = 0;
double _temperature = 0;
unsigned int _humidity = 0;
double _humidity = 0;
};


+ 15
- 0
code/espurna/sensors/ECH1560Sensor.h View File

@ -59,6 +59,12 @@ class ECH1560Sensor : public BaseSensor {
return _inverted;
}
// ---------------------------------------------------------------------
void resetEnergy(double value = 0) {
_energy = value;
}
// ---------------------------------------------------------------------
// Sensor API
// ---------------------------------------------------------------------
@ -106,6 +112,7 @@ class ECH1560Sensor : public BaseSensor {
if (index == 0) return MAGNITUDE_CURRENT;
if (index == 1) return MAGNITUDE_VOLTAGE;
if (index == 2) return MAGNITUDE_POWER_APPARENT;
if (index == 3) return MAGNITUDE_ENERGY;
return MAGNITUDE_NONE;
}
@ -114,6 +121,7 @@ class ECH1560Sensor : public BaseSensor {
if (index == 0) return _current;
if (index == 1) return _voltage;
if (index == 2) return _apparent;
if (index == 3) return _energy;
return 0;
}
@ -260,6 +268,12 @@ class ECH1560Sensor : public BaseSensor {
_apparent = ( (float) byte1 * 255 + (float) byte2 + (float) byte3 / 255.0) / 2;
_current = _apparent / _voltage;
static unsigned long last = 0;
if (last > 0) {
_energy += (_apparent * (millis() - last) / 1000);
}
last = millis();
_dosync = false;
}
@ -287,6 +301,7 @@ class ECH1560Sensor : public BaseSensor {
double _apparent = 0;
double _voltage = 0;
double _current = 0;
double _energy = 0;
unsigned char _data[24];


+ 8
- 3
code/espurna/sensors/EmonSensor.h View File

@ -55,6 +55,11 @@ class EmonSensor : public I2CSensor {
}
}
void resetEnergy(unsigned char channel, double value = 0) {
if (channel >= _channels) return;
_energy[channel] = value;
}
// ---------------------------------------------------------------------
void setVoltage(double voltage) {
@ -146,7 +151,7 @@ class EmonSensor : public I2CSensor {
#endif
}
virtual unsigned int readADC(unsigned char channel) {}
virtual unsigned int readADC(unsigned char channel) = 0;
void calculateFactors(unsigned char channel) {
@ -154,8 +159,8 @@ class EmonSensor : public I2CSensor {
unsigned int s = 1;
unsigned int i = 1;
unsigned int m = s * i;
unsigned int multiplier;
unsigned int m = 1;
unsigned int multiplier = 1;
while (m * _current_factor[channel] < 1) {
multiplier = m;
i = (i == 1) ? 2 : (i == 2) ? 5 : 1;


+ 29
- 12
code/espurna/sensors/EventSensor.h View File

@ -33,12 +33,16 @@ class EventSensor : public BaseSensor {
_gpio = gpio;
}
void setMode(unsigned char mode) {
_mode = mode;
void setTrigger(bool trigger) {
_trigger = trigger;
}
void setInterruptMode(unsigned char mode) {
_interrupt_mode = mode;
void setPinMode(unsigned char pin_mode) {
_pin_mode = pin_mode;
}
void setInterruptMode(unsigned char interrupt_mode) {
_interrupt_mode = interrupt_mode;
}
void setDebounceTime(unsigned long debounce) {
@ -51,8 +55,12 @@ class EventSensor : public BaseSensor {
return _gpio;
}
unsigned char getMode() {
return _mode;
bool getTrigger() {
return _trigger;
}
unsigned char getPinMode() {
return _pin_mode;
}
unsigned char getInterruptMode() {
@ -70,8 +78,9 @@ class EventSensor : public BaseSensor {
// Initialization method, must be idempotent
// Defined outside the class body
void begin() {
pinMode(_gpio, _mode);
pinMode(_gpio, _pin_mode);
_enableInterrupts(true);
_count = _trigger ? 2 : 1;
_ready = true;
}
@ -94,7 +103,8 @@ class EventSensor : public BaseSensor {
// Type for slot # index
unsigned char type(unsigned char index) {
if (index == 0) return MAGNITUDE_EVENTS;
if (index == 0) return MAGNITUDE_COUNT;
if (index == 1) return MAGNITUDE_EVENT;
return MAGNITUDE_NONE;
}
@ -113,8 +123,14 @@ class EventSensor : public BaseSensor {
(void) gpio;
static unsigned long last = 0;
if (millis() - last > _debounce) {
_events = _events + 1;
last = millis();
_events = _events + 1;
if (_trigger) {
if (_callback) _callback(MAGNITUDE_EVENT, digitalRead(gpio));
}
}
}
@ -148,9 +164,10 @@ class EventSensor : public BaseSensor {
volatile unsigned long _events = 0;
unsigned long _debounce = EVENTS_DEBOUNCE;
unsigned char _gpio;
unsigned char _mode;
unsigned char _interrupt_mode;
unsigned char _gpio = GPIO_NONE;
bool _trigger = false;
unsigned char _pin_mode = INPUT;
unsigned char _interrupt_mode = RISING;
};


+ 4
- 2
code/espurna/sensors/HLW8012Sensor.h View File

@ -48,7 +48,8 @@ class HLW8012Sensor : public BaseSensor {
_hlw8012->resetMultipliers();
}
void resetEnergy() {
void resetEnergy(double value = 0) {
_energy_offset = value;
_hlw8012->resetEnergy();
}
@ -200,7 +201,7 @@ class HLW8012Sensor : public BaseSensor {
if (index == 3) return _hlw8012->getReactivePower();
if (index == 4) return _hlw8012->getApparentPower();
if (index == 5) return 100 * _hlw8012->getPowerFactor();
if (index == 6) return _hlw8012->getEnergy();
if (index == 6) return (_energy_offset + _hlw8012->getEnergy());
return 0;
}
@ -261,6 +262,7 @@ class HLW8012Sensor : public BaseSensor {
unsigned char _cf = GPIO_NONE;
unsigned char _cf1 = GPIO_NONE;
bool _sel_current = true;
double _energy_offset = 0;
HLW8012 * _hlw8012 = NULL;


+ 189
- 0
code/espurna/sensors/MICS2710Sensor.h View File

@ -0,0 +1,189 @@
// -----------------------------------------------------------------------------
// MICS-2710 (and MICS-4514) NO2 Analog Sensor
// Copyright (C) 2018 by Xose Pérez <xose dot perez at gmail dot com>
// -----------------------------------------------------------------------------
#if SENSOR_SUPPORT && MICS2710_SUPPORT
#pragma once
// Set ADC to TOUT pin
#undef ADC_MODE_VALUE
#define ADC_MODE_VALUE ADC_TOUT
#include "Arduino.h"
#include "BaseSensor.h"
extern "C" {
#include "../libs/fs_math.h"
}
class MICS2710Sensor : public BaseSensor {
public:
// ---------------------------------------------------------------------
// Public
// ---------------------------------------------------------------------
MICS2710Sensor(): BaseSensor() {
_count = 2;
_sensor_id = SENSOR_MICS2710_ID;
}
void calibrate() {
setR0(_getResistance());
}
// ---------------------------------------------------------------------
void setAnalogGPIO(unsigned char gpio) {
_noxGPIO = gpio;
}
unsigned char getAnalogGPIO() {
return _noxGPIO;
}
void setPreHeatGPIO(unsigned char gpio) {
_preGPIO = gpio;
}
unsigned char getPreHeatGPIO() {
return _preGPIO;
}
void setRL(unsigned long Rl) {
if (Rl > 0) _Rl = Rl;
}
unsigned long getRL() {
return _Rl;
}
void setR0(unsigned long R0) {
if (R0 > 0) _R0 = R0;
}
unsigned long getR0() {
return _R0;
}
// ---------------------------------------------------------------------
// Sensor API
// ---------------------------------------------------------------------
// Initialization method, must be idempotent
void begin() {
// Set NOX as analog input
pinMode(_noxGPIO, INPUT);
// Start pre-heating
pinMode(_preGPIO, OUTPUT);
digitalWrite(_preGPIO, HIGH);
_heating = true;
_start = millis();
_ready = true;
}
// Pre-read hook (usually to populate registers with up-to-date data)
void pre() {
// Check pre-heat time
if (_heating && (millis() - _start > MICS2710_PREHEAT_TIME)) {
digitalWrite(_preGPIO, LOW);
_heating = false;
}
if (_ready) {
_Rs = _getResistance();
}
}
// Descriptive name of the sensor
String description() {
return String("MICS-2710 @ TOUT");
}
// Descriptive name of the slot # index
String slot(unsigned char index) {
return description();
};
// Address of the sensor (it could be the GPIO or I2C address)
String address(unsigned char index) {
return String("0");
}
// Type for slot # index
unsigned char type(unsigned char index) {
if (0 == index) return MAGNITUDE_RESISTANCE;
if (1 == index) return MAGNITUDE_NO2;
return MAGNITUDE_NONE;
}
// Current value for slot # index
double value(unsigned char index) {
if (0 == index) return _Rs;
if (1 == index) return _getPPM();
return 0;
}
private:
unsigned long _getReading() {
return analogRead(_noxGPIO);
}
double _getResistance() {
// get voltage (1 == reference) from analog pin
double voltage = (float) _getReading() / 1024.0;
// schematic: 3v3 - Rs - P - Rl - GND
// V(P) = 3v3 * Rl / (Rs + Rl)
// Rs = 3v3 * Rl / V(P) - Rl = Rl * ( 3v3 / V(P) - 1)
// 3V3 voltage is cancelled
double resistance = (voltage > 0) ? _Rl * ( 1 / voltage - 1 ) : 0;
return resistance;
}
double _getPPM() {
// According to the datasheet (https://www.cdiweb.com/datasheets/e2v/mics-2710.pdf)
// there is an almost linear relation between log(Rs/R0) and log(ppm).
// Regression parameters have been calculated based on the graph
// in the datasheet with these readings:
//
// Rs/R0 NO2(ppm)
// 23 0.20
// 42 0.30
// 90 0.40
// 120 0.50
// 200 0.60
// 410 0.90
// 500 1.00
// 1000 1.30
// 10000 5.00
return fs_pow(10, 0.5170 * fs_log10(_Rs / _R0) - 1.3954);
}
bool _heating = false;
unsigned long _start = 0; // monitors the pre-heating time
unsigned long _R0 = MICS2710_R0; // R0, calikbration value at 25º
unsigned long _Rl = MICS2710_RL; // RL, load resistance
unsigned long _Rs = 0; // cached resistance
unsigned char _noxGPIO = MICS2710_PRE_PIN;
unsigned char _preGPIO = MICS2710_NOX_PIN;
};
#endif // SENSOR_SUPPORT && MICS2710_SUPPORT

+ 144
- 0
code/espurna/sensors/MICS5525Sensor.h View File

@ -0,0 +1,144 @@
// -----------------------------------------------------------------------------
// MICS-5525 (and MICS-4514) CO Analog Sensor
// Copyright (C) 2018 by Xose Pérez <xose dot perez at gmail dot com>
// -----------------------------------------------------------------------------
#if SENSOR_SUPPORT && MICS5525_SUPPORT
#pragma once
// Set ADC to TOUT pin
#undef ADC_MODE_VALUE
#define ADC_MODE_VALUE ADC_TOUT
#include "Arduino.h"
#include "BaseSensor.h"
extern "C" {
#include "../libs/fs_math.h"
}
class MICS5525Sensor : public BaseSensor {
public:
// ---------------------------------------------------------------------
// Public
// ---------------------------------------------------------------------
MICS5525Sensor(): BaseSensor() {
_count = 2;
_sensor_id = SENSOR_MICS5525_ID;
}
void calibrate() {
setR0(_getResistance());
}
// ---------------------------------------------------------------------
void setAnalogGPIO(unsigned char gpio) {
_redGPIO = gpio;
}
unsigned char getAnalogGPIO() {
return _redGPIO;
}
void setRL(unsigned long Rl) {
if (Rl > 0) _Rl = Rl;
}
unsigned long getRL() {
return _Rl;
}
void setR0(unsigned long R0) {
if (R0 > 0) _R0 = R0;
}
unsigned long getR0() {
return _R0;
}
// ---------------------------------------------------------------------
// Sensor API
// ---------------------------------------------------------------------
// Initialization method, must be idempotent
void begin() {
pinMode(_redGPIO, INPUT);
_ready = true;
}
// Pre-read hook (usually to populate registers with up-to-date data)
void pre() {
_Rs = _getResistance();
}
// Descriptive name of the sensor
String description() {
return String("MICS-5525 @ TOUT");
}
// Descriptive name of the slot # index
String slot(unsigned char index) {
return description();
};
// Address of the sensor (it could be the GPIO or I2C address)
String address(unsigned char index) {
return String("0");
}
// Type for slot # index
unsigned char type(unsigned char index) {
if (0 == index) return MAGNITUDE_RESISTANCE;
if (1 == index) return MAGNITUDE_CO;
return MAGNITUDE_NONE;
}
// Current value for slot # index
double value(unsigned char index) {
if (0 == index) return _Rs;
if (1 == index) return _getPPM();
return 0;
}
private:
unsigned long _getReading() {
return analogRead(_redGPIO);
}
double _getResistance() {
// get voltage (1 == reference) from analog pin
double voltage = (float) _getReading() / 1024.0;
// schematic: 3v3 - Rs - P - Rl - GND
// V(P) = 3v3 * Rl / (Rs + Rl)
// Rs = 3v3 * Rl / V(P) - Rl = Rl * ( 3v3 / V(P) - 1)
// 3V3 voltage is cancelled
double resistance = (voltage > 0) ? _Rl * ( 1 / voltage - 1 ) : 0;
return resistance;
}
double _getPPM() {
// According to the datasheet (https://airqualityegg.wikispaces.com/file/view/mics-5525-CO.pdf)
return 764.2976 * fs_pow(2.71828, -7.6389 * ((float) _Rs / _R0));
}
unsigned long _R0 = MICS5525_R0; // R0, calibration value at 25º on air
unsigned long _Rl = MICS5525_RL; // RL, load resistance
unsigned long _Rs = 0; // cached resistance
unsigned char _redGPIO = MICS5525_RED_PIN;
};
#endif // SENSOR_SUPPORT && MICS5525_SUPPORT

+ 125
- 0
code/espurna/sensors/NTCSensor.h View File

@ -0,0 +1,125 @@
// -----------------------------------------------------------------------------
// NTC Sensor (maps to a NTCSensor)
// Copyright (C) 2018 by Xose Pérez <xose dot perez at gmail dot com>
// -----------------------------------------------------------------------------
#if SENSOR_SUPPORT && NTC_SUPPORT
#pragma once
// Set ADC to TOUT pin
#undef ADC_MODE_VALUE
#define ADC_MODE_VALUE ADC_TOUT
#include "Arduino.h"
#include "AnalogSensor.h"
extern "C" {
#include "../libs/fs_math.h"
}
class NTCSensor : public AnalogSensor {
public:
// ---------------------------------------------------------------------
// Public
// ---------------------------------------------------------------------
NTCSensor(): AnalogSensor() {
_count = 1;
_sensor_id = SENSOR_NTC_ID;
}
void setBeta(unsigned long beta) {
if (beta > 0) _beta = beta;
}
void setUpstreamResistor(unsigned long resistance) {
_resistance_up = resistance;
if (_resistance_up > 0) _resistance_down = 0;
}
void setDownstreamResistor(unsigned long resistance) {
_resistance_down = resistance;
if (_resistance_down > 0) _resistance_up = 0;
}
void setR0(unsigned long resistance) {
if (resistance > 0) _R0 = resistance;
}
void setT0(double temperature) {
if (temperature > 0) _T0 = temperature;
}
// ---------------------------------------------------------------------
// ---------------------------------------------------------------------
// Sensor API
// ---------------------------------------------------------------------
// Descriptive name of the sensor
String description() {
return String("NTC @ TOUT");
}
// Descriptive name of the slot # index
String slot(unsigned char index) {
return description();
};
// Address of the sensor (it could be the GPIO or I2C address)
String address(unsigned char index) {
return String("0");
}
// Type for slot # index
unsigned char type(unsigned char index) {
if (index == 0) return MAGNITUDE_TEMPERATURE;
return MAGNITUDE_NONE;
}
// Current value for slot # index
double value(unsigned char index) {
double temperature = 0;
if (index == 0) {
// sampled reading
double read = _read();
// Ru = (1023/c - 1) * Rd
double resistance;
double alpha = (1023.0 / read) - 1;
if (_resistance_down > 0) {
resistance = _resistance_down * alpha;
} else if (0 == alpha) {
resistance = _R0;
} else {
resistance = _resistance_up / alpha;
}
// 1/T = 1/T0 + 1/B * ln(R/R0)
temperature = fs_log(resistance / _R0);
temperature = (1.0 / _T0) + (temperature / _beta);
temperature = 1.0 / temperature - 273.15;
}
return temperature;
}
protected:
unsigned long _beta = NTC_BETA;
unsigned long _resistance_up = NTC_R_UP;
unsigned long _resistance_down = NTC_R_DOWN;
unsigned long _R0 = NTC_R0;
double _T0 = NTC_T0;
};
#endif // SENSOR_SUPPORT && NTC_SUPPORT

+ 39
- 11
code/espurna/sensors/PMSX003Sensor.h View File

@ -14,6 +14,9 @@
#include <SoftwareSerial.h>
// Generic data
#define PMS_BAUD_RATE 9600
// Type of sensor
#define PMS_TYPE_X003 0
#define PMS_TYPE_X003_9 1
@ -46,7 +49,7 @@ const static struct {
class PMSX003 {
protected:
SoftwareSerial *_serial = NULL; // Should initialized by child class
Stream *_serial = NULL; // Should initialized by child class
public:
@ -175,7 +178,13 @@ class PMSX003Sensor : public BaseSensor, PMSX003 {
_dirty = true;
}
// Should call setType after constrcutor immediately to enable corresponding slot count
void setSerial(HardwareSerial * serial) {
_soft = false;
_serial = serial;
_dirty = true;
}
// Should call setType after constructor immediately to enable corresponding slot count
void setType(unsigned char type) {
_type = type;
_count = pms_specs[_type].slot_count;
@ -204,30 +213,45 @@ class PMSX003Sensor : public BaseSensor, PMSX003 {
if (!_dirty) return;
if (_serial) delete _serial;
if (_soft) {
if (_serial) delete _serial;
_serial = new SoftwareSerial(_pin_rx, _pin_tx, false, 64);
static_cast<SoftwareSerial*>(_serial)->enableIntTx(false);
}
if (_soft) {
static_cast<SoftwareSerial*>(_serial)->begin(PMS_BAUD_RATE);
} else {
static_cast<HardwareSerial*>(_serial)->begin(PMS_BAUD_RATE);
}
_serial = new SoftwareSerial(_pin_rx, _pin_tx, false, 64);
_serial->enableIntTx(false);
_serial->begin(9600);
passiveMode();
_startTime = millis();
_ready = true;
_dirty = false;
}
// Descriptive name of the sensor
String description() {
char buffer[28];
snprintf(buffer, sizeof(buffer), "%s @ SwSerial(%u,%u)", pms_specs[_type].name, _pin_rx, _pin_tx);
if (_soft) {
snprintf(buffer, sizeof(buffer), "%s @ SwSerial(%u,%u)", pms_specs[_type].name, _pin_rx, _pin_tx);
} else {
snprintf(buffer, sizeof(buffer), "%s @ HwSerial", pms_specs[_type].name);
}
return String(buffer);
}
// Descriptive name of the slot # index
String slot(unsigned char index) {
char buffer[36] = {0};
snprintf(buffer, sizeof(buffer), "%d @ %s @ SwSerial(%u,%u)", int(index + 1), pms_specs[_type].name, _pin_rx, _pin_tx);
if (_soft) {
snprintf(buffer, sizeof(buffer), "%d @ %s @ SwSerial(%u,%u)", int(index + 1), pms_specs[_type].name, _pin_rx, _pin_tx);
} else {
snprintf(buffer, sizeof(buffer), "%d @ %s @ HwSerial", int(index + 1), pms_specs[_type].name);
}
return String(buffer);
}
@ -269,6 +293,9 @@ class PMSX003Sensor : public BaseSensor, PMSX003 {
}
} else {
readCycle = -1;
if (_readCount == 1) {
wakeUp();
}
}
#endif
@ -301,7 +328,7 @@ class PMSX003Sensor : public BaseSensor, PMSX003 {
#endif
requestRead();
}
// Current value for slot # index
@ -310,6 +337,7 @@ class PMSX003Sensor : public BaseSensor, PMSX003 {
}
protected:
bool _soft = true;
unsigned int _pin_rx;
unsigned int _pin_tx;
unsigned long _startTime;
@ -322,4 +350,4 @@ class PMSX003Sensor : public BaseSensor, PMSX003 {
};
#endif // SENSOR_SUPPORT && PMS_SUPPORT
#endif // SENSOR_SUPPORT && PMSX003_SUPPORT

+ 150
- 17
code/espurna/sensors/PZEM004TSensor.h View File

@ -12,6 +12,13 @@
#include <PZEM004T.h>
#define PZ_MAGNITUDE_COUNT 4
#define PZ_MAGNITUDE_CURRENT_INDEX 0
#define PZ_MAGNITUDE_VOLTAGE_INDEX 1
#define PZ_MAGNITUDE_POWER_ACTIVE_INDEX 2
#define PZ_MAGNITUDE_ENERGY_INDEX 3
class PZEM004TSensor : public BaseSensor {
public:
@ -21,9 +28,7 @@ class PZEM004TSensor : public BaseSensor {
// ---------------------------------------------------------------------
PZEM004TSensor(): BaseSensor() {
_count = 4;
_sensor_id = SENSOR_PZEM004T_ID;
_ip = IPAddress(192,168,1,1);
}
~PZEM004TSensor() {
@ -49,6 +54,53 @@ class PZEM004TSensor : public BaseSensor {
_dirty = true;
}
// Set the devices physical addresses managed by this sensor
void setAddresses(const char *addresses) {
char const * sep = " ";
char tokens[strlen(addresses) + 1];
strlcpy(tokens, addresses, sizeof(tokens));
char *address = tokens;
int i = 0;
address = strtok(address, sep);
while (address != 0 && i++ < PZEM004T_MAX_DEVICES) {
IPAddress addr;
reading_t reading;
reading.current = PZEM_ERROR_VALUE;
reading.voltage = PZEM_ERROR_VALUE;
reading.power = PZEM_ERROR_VALUE;
reading.energy = PZEM_ERROR_VALUE;
if (addr.fromString(address)) {
_devices.push_back(addr);
_energy_offsets.push_back(0);
_readings.push_back(reading);
}
address = strtok(0, sep);
}
_count = _devices.size() * PZ_MAGNITUDE_COUNT;
_dirty = true;
}
// Return the number of devices managed by this sensor
unsigned char getAddressesCount() {
return _devices.size();
}
// Get device physical address based on the device index
String getAddress(unsigned char dev) {
return _devices[dev].toString();
}
// Set the device physical address
bool setDeviceAddress(IPAddress *addr) {
while(_busy) { yield(); };
_busy = true;
bool res = _pzem->setAddress(*addr);
_busy = false;
return res;
}
// ---------------------------------------------------------------------
unsigned char getRX() {
@ -59,13 +111,21 @@ class PZEM004TSensor : public BaseSensor {
return _pin_tx;
}
// ---------------------------------------------------------------------
// If called with value = -1, the offset will be the last energy reading
// otherwise, it will be the value provided
float resetEnergy(unsigned char dev, float value = -1) {
_energy_offsets[dev] = value != -1 ? value : _readings[dev].energy;
return _energy_offsets[dev];
}
// ---------------------------------------------------------------------
// Sensor API
// ---------------------------------------------------------------------
// Initialization method, must be idempotent
void begin() {
if (!_dirty) return;
if (_pzem) delete _pzem;
@ -74,16 +134,15 @@ class PZEM004TSensor : public BaseSensor {
} else {
_pzem = new PZEM004T(_pin_rx, _pin_tx);
}
_pzem->setAddress(_ip);
if(_devices.size() == 1) _pzem->setAddress(_devices[0]);
_ready = true;
_dirty = false;
}
// Descriptive name of the sensor
String description() {
char buffer[28];
char buffer[27];
if (_serial) {
snprintf(buffer, sizeof(buffer), "PZEM004T @ HwSerial");
} else {
@ -94,34 +153,99 @@ class PZEM004TSensor : public BaseSensor {
// Descriptive name of the slot # index
String slot(unsigned char index) {
return description();
int dev = index / PZ_MAGNITUDE_COUNT;
char buffer[25];
snprintf(buffer, sizeof(buffer), "(%u/%s)", dev, getAddress(dev).c_str());
return description() + String(buffer);
};
// Address of the sensor (it could be the GPIO or I2C address)
String address(unsigned char index) {
return _ip.toString();
int dev = index / PZ_MAGNITUDE_COUNT;
return _devices[dev].toString();
}
// Type for slot # index
unsigned char type(unsigned char index) {
if (index == 0) return MAGNITUDE_CURRENT;
if (index == 1) return MAGNITUDE_VOLTAGE;
if (index == 2) return MAGNITUDE_POWER_ACTIVE;
if (index == 3) return MAGNITUDE_ENERGY;
int dev = index / PZ_MAGNITUDE_COUNT;
index = index - (dev * PZ_MAGNITUDE_COUNT);
if (index == PZ_MAGNITUDE_CURRENT_INDEX) return MAGNITUDE_CURRENT;
if (index == PZ_MAGNITUDE_VOLTAGE_INDEX) return MAGNITUDE_VOLTAGE;
if (index == PZ_MAGNITUDE_POWER_ACTIVE_INDEX) return MAGNITUDE_POWER_ACTIVE;
if (index == PZ_MAGNITUDE_ENERGY_INDEX) return MAGNITUDE_ENERGY;
return MAGNITUDE_NONE;
}
// Current value for slot # index
double value(unsigned char index) {
int dev = index / PZ_MAGNITUDE_COUNT;
index = index - (dev * PZ_MAGNITUDE_COUNT);
double response = 0;
if (index == 0) response = _pzem->current(_ip);
if (index == 1) response = _pzem->voltage(_ip);
if (index == 2) response = _pzem->power(_ip);
if (index == 3) response = _pzem->energy(_ip) * 3600;
if (index == PZ_MAGNITUDE_CURRENT_INDEX) response = _readings[dev].current;
if (index == PZ_MAGNITUDE_VOLTAGE_INDEX) response = _readings[dev].voltage;
if (index == PZ_MAGNITUDE_POWER_ACTIVE_INDEX) response = _readings[dev].power;
if (index == PZ_MAGNITUDE_ENERGY_INDEX) response = (_readings[dev].energy * 3600) - _energy_offsets[dev];
if (response < 0) response = 0;
return response;
}
// Post-read hook (usually to reset things)
void post() {
_error = SENSOR_ERROR_OK;
}
// Loop-like method, call it in your main loop
void tick() {
static unsigned char dev = 0;
static unsigned char magnitude = 0;
static unsigned long last_millis = 0;
if (_busy || millis() - last_millis < PZEM004T_READ_INTERVAL) return;
_busy = true;
// Clear buffer in case of late response(Timeout)
while(Serial.available() > 0) Serial.read();
float read;
float* readings_p;
switch(magnitude) {
case PZ_MAGNITUDE_CURRENT_INDEX:
read = _pzem->current(_devices[dev]);
readings_p = &_readings[dev].current;
break;
case PZ_MAGNITUDE_VOLTAGE_INDEX:
read = _pzem->voltage(_devices[dev]);
readings_p = &_readings[dev].voltage;
break;
case PZ_MAGNITUDE_POWER_ACTIVE_INDEX:
read = _pzem->power(_devices[dev]);
readings_p = &_readings[dev].power;
break;
case PZ_MAGNITUDE_ENERGY_INDEX:
read = _pzem->energy(_devices[dev]);
readings_p = &_readings[dev].energy;
break;
default:
_busy = false;
return;
}
if(read == PZEM_ERROR_VALUE) {
_error = SENSOR_ERROR_TIMEOUT;
} else {
*readings_p = read;
}
if(++dev == _devices.size()) {
dev = 0;
last_millis = millis();
if(++magnitude == PZ_MAGNITUDE_COUNT) {
magnitude = 0;
}
}
_busy = false;
}
protected:
// ---------------------------------------------------------------------
@ -130,7 +254,16 @@ class PZEM004TSensor : public BaseSensor {
unsigned int _pin_rx = PZEM004T_RX_PIN;
unsigned int _pin_tx = PZEM004T_TX_PIN;
IPAddress _ip;
bool _busy = false;
typedef struct {
float voltage;
float current;
float power;
float energy;
} reading_t;
std::vector<reading_t> _readings;
std::vector<float> _energy_offsets;
std::vector<IPAddress> _devices;
HardwareSerial * _serial = NULL;
PZEM004T * _pzem = NULL;


+ 173
- 0
code/espurna/sensors/SDS011Sensor.h View File

@ -0,0 +1,173 @@
// -----------------------------------------------------------------------------
// SDS011 particulates sensor
// Based on: https://github.com/ricki-z/SDS011
//
// Uses SoftwareSerial library
// Copyright (C) 2018 by Lucas Pleß <hello at lucas-pless dot com>
// -----------------------------------------------------------------------------
#if SENSOR_SUPPORT && SDS011_SUPPORT
#pragma once
#include "Arduino.h"
#include "BaseSensor.h"
#include <SoftwareSerial.h>
class SDS011Sensor : public BaseSensor {
public:
// ---------------------------------------------------------------------
// Public
// ---------------------------------------------------------------------
SDS011Sensor(): BaseSensor() {
_count = 2;
_sensor_id = SENSOR_SDS011_ID;
}
~SDS011Sensor() {
if (_serial) delete _serial;
}
// ---------------------------------------------------------------------
void setRX(unsigned char pin_rx) {
if (_pin_rx == pin_rx) return;
_pin_rx = pin_rx;
_dirty = true;
}
void setTX(unsigned char pin_tx) {
if (_pin_tx == pin_tx) return;
_pin_tx = pin_tx;
_dirty = true;
}
// ---------------------------------------------------------------------
unsigned char getRX() {
return _pin_rx;
}
unsigned char getTX() {
return _pin_tx;
}
// ---------------------------------------------------------------------
// Sensor API
// ---------------------------------------------------------------------
// Initialization method, must be idempotent
void begin() {
if (!_dirty) return;
if (_serial) delete _serial;
_serial = new SoftwareSerial(_pin_rx, _pin_tx);
_serial->begin(9600);
_ready = true;
_dirty = false;
}
// Descriptive name of the sensor
String description() {
char buffer[28];
snprintf(buffer, sizeof(buffer), "SDS011 @ SwSerial(%u,%u)", _pin_rx, _pin_tx);
return String(buffer);
}
// Descriptive name of the slot # index
String slot(unsigned char index) {
return description();
};
// Address of the sensor (it could be the GPIO or I2C address)
String address(unsigned char index) {
char buffer[6];
snprintf(buffer, sizeof(buffer), "%u:%u", _pin_rx, _pin_tx);
return String(buffer);
}
// Type for slot # index
unsigned char type(unsigned char index) {
if (index == 0) return MAGNITUDE_PM2dot5;
if (index == 1) return MAGNITUDE_PM10;
return MAGNITUDE_NONE;
}
void pre() {
_read();
}
// Current value for slot # index
double value(unsigned char index) {
if (index == 0) return _p2dot5;
if (index == 1) return _p10;
return 0;
}
protected:
// ---------------------------------------------------------------------
// Protected
// ---------------------------------------------------------------------
void _read() {
byte buffer;
int value;
int len = 0;
int pm10_serial = 0;
int pm25_serial = 0;
int checksum_is;
int checksum_ok = 0;
while ((_serial->available() > 0) && (_serial->available() >= (10-len))) {
buffer = _serial->read();
value = int(buffer);
switch (len) {
case (0): if (value != 170) { len = -1; }; break;
case (1): if (value != 192) { len = -1; }; break;
case (2): pm25_serial = value; checksum_is = value; break;
case (3): pm25_serial += (value << 8); checksum_is += value; break;
case (4): pm10_serial = value; checksum_is += value; break;
case (5): pm10_serial += (value << 8); checksum_is += value; break;
case (6): checksum_is += value; break;
case (7): checksum_is += value; break;
case (8): if (value == (checksum_is % 256)) { checksum_ok = 1; } else { len = -1; }; break;
case (9): if (value != 171) { len = -1; }; break;
}
len++;
if (len == 10) {
if(checksum_ok == 1) {
_p10 = (float)pm10_serial/10.0;
_p2dot5 = (float)pm25_serial/10.0;
len = 0; checksum_ok = 0; pm10_serial = 0.0; pm25_serial = 0.0; checksum_is = 0;
_error = SENSOR_ERROR_OK;
} else {
_error = SENSOR_ERROR_CRC;
}
}
yield();
}
}
double _p2dot5 = 0;
double _p10 = 0;
unsigned int _pin_rx;
unsigned int _pin_tx;
SoftwareSerial * _serial = NULL;
};
#endif // SENSOR_SUPPORT && SDS011_SUPPORT

+ 0
- 2
code/espurna/sensors/SI7021Sensor.h View File

@ -10,8 +10,6 @@
#undef I2C_SUPPORT
#define I2C_SUPPORT 1 // Explicitly request I2C support.
#include "Arduino.h"
#include "I2CSensor.h"


code/espurna/sensors/HCSR04Sensor.h → code/espurna/sensors/SonarSensor.h View File

@ -1,16 +1,18 @@
// -----------------------------------------------------------------------------
// HC-SR04 Ultrasonic sensor
// HC-SR04, SRF05, SRF06, DYP-ME007, JSN-SR04T & Parallax PING)))
// Copyright (C) 2018 by Xose Pérez <xose dot perez at gmail dot com>
// Enhancements by Rui Marinho
// -----------------------------------------------------------------------------
#if SENSOR_SUPPORT && HCSR04_SUPPORT
#if SENSOR_SUPPORT && SONAR_SUPPORT
#pragma once
#include "Arduino.h"
#include "BaseSensor.h"
#include "NewPing.h"
class HCSR04Sensor : public BaseSensor {
class SonarSensor : public BaseSensor {
public:
@ -18,17 +20,30 @@ class HCSR04Sensor : public BaseSensor {
// Public
// ---------------------------------------------------------------------
HCSR04Sensor(): BaseSensor() {
SonarSensor(): BaseSensor() {
_count = 1;
_sensor_id = SENSOR_HCSR04_ID;
_sensor_id = SENSOR_SONAR_ID;
}
// ---------------------------------------------------------------------
// Echo pin.
void setEcho(unsigned char echo) {
_echo = echo;
}
// Number of iterations to ping in order to filter out erroneous readings
// using a digital filter.
void setIterations(unsigned int iterations) {
_iterations = iterations;
}
// Max sensor distance in centimeters.
void setMaxDistance(unsigned int distance) {
_max_distance = distance;
}
// Trigger pin.
void setTrigger(unsigned char trigger) {
_trigger = trigger;
}
@ -43,22 +58,28 @@ class HCSR04Sensor : public BaseSensor {
return _trigger;
}
unsigned int getMaxDistance() {
return _max_distance;
}
unsigned int getIterations() {
return _iterations;
}
// ---------------------------------------------------------------------
// Sensor API
// ---------------------------------------------------------------------
// Initialization method, must be idempotent
void begin() {
pinMode(_echo, INPUT);
pinMode(_trigger, OUTPUT);
digitalWrite(_trigger, LOW);
_sonar = new NewPing(getTrigger(), getEcho(), getMaxDistance());
_ready = true;
}
// Descriptive name of the sensor
String description() {
char buffer[24];
snprintf(buffer, sizeof(buffer), "HCSR04 @ GPIO(%u, %u)", _trigger, _echo);
char buffer[23];
snprintf(buffer, sizeof(buffer), "Sonar @ GPIO(%u, %u)", _trigger, _echo);
return String(buffer);
}
@ -80,28 +101,11 @@ class HCSR04Sensor : public BaseSensor {
// Current value for slot # index
double value(unsigned char index) {
if (index == 0) {
// Trigger pulse
digitalWrite(_trigger, HIGH);
delayMicroseconds(10);
digitalWrite(_trigger, LOW);
// Wait for echo pulse low-high-low
while ( digitalRead(_echo) == 0 ) yield();
unsigned long start = micros();
while ( digitalRead(_echo) == 1 ) yield();
unsigned long travel_time = micros() - start;
// Assuming a speed of sound of 340m/s
// Dividing by 2 since it is a round trip
return 340.0 * (double) travel_time / 1000000.0 / 2;
if (index != 0) return 0;
if (getIterations() > 0) {
return NewPing::convert_cm(_sonar->ping_median(getIterations())) / 100.0;
}
return 0;
return _sonar->ping_cm() / 100.0;
}
@ -113,7 +117,10 @@ class HCSR04Sensor : public BaseSensor {
unsigned char _trigger;
unsigned char _echo;
unsigned int _max_distance;
unsigned int _iterations;
NewPing * _sonar = NULL;
};
#endif // SENSOR_SUPPORT && HCSR04_SUPPORT
#endif // SENSOR_SUPPORT && SONAR_SUPPORT

+ 21
- 6
code/espurna/sensors/V9261FSensor.h View File

@ -56,6 +56,12 @@ class V9261FSensor : public BaseSensor {
return _inverted;
}
// ---------------------------------------------------------------------
void resetEnergy(double value = 0) {
_energy = value;
}
// ---------------------------------------------------------------------
// Sensor API
// ---------------------------------------------------------------------
@ -106,6 +112,7 @@ class V9261FSensor : public BaseSensor {
if (index == 3) return MAGNITUDE_POWER_REACTIVE;
if (index == 4) return MAGNITUDE_POWER_APPARENT;
if (index == 5) return MAGNITUDE_POWER_FACTOR;
if (index == 6) return MAGNITUDE_ENERGY;
return MAGNITUDE_NONE;
}
@ -117,6 +124,7 @@ class V9261FSensor : public BaseSensor {
if (index == 3) return _reactive;
if (index == 4) return _apparent;
if (index == 5) return _apparent > 0 ? 100 * _active / _apparent : 100;
if (index == 6) return _energy;
return 0;
}
@ -130,6 +138,7 @@ class V9261FSensor : public BaseSensor {
static unsigned char state = 0;
static unsigned long last = 0;
static unsigned long ts = 0;
static bool found = false;
static unsigned char index = 0;
@ -138,10 +147,10 @@ class V9261FSensor : public BaseSensor {
while (_serial->available()) {
_serial->flush();
found = true;
last = millis();
ts = millis();
}
if (found && (millis() - last > V9261F_SYNC_INTERVAL)) {
if (found && (millis() - ts > V9261F_SYNC_INTERVAL)) {
_serial->flush();
index = 0;
state = 1;
@ -164,7 +173,7 @@ class V9261FSensor : public BaseSensor {
_data[index] = _serial->read();
if (index++ >= 19) {
_serial->flush();
last = millis();
ts = millis();
state = 3;
}
}
@ -208,9 +217,14 @@ class V9261FSensor : public BaseSensor {
_apparent = fs_sqrt(_reactive * _reactive + _active * _active);
if (last > 0) {
_energy += (_active * (millis() - last) / 1000);
}
last = millis();
}
last = millis();
ts = millis();
index = 0;
state = 4;
@ -218,10 +232,10 @@ class V9261FSensor : public BaseSensor {
while (_serial->available()) {
_serial->flush();
last = millis();
ts = millis();
}
if (millis() - last > V9261F_SYNC_INTERVAL) {
if (millis() - ts > V9261F_SYNC_INTERVAL) {
state = 1;
}
@ -249,6 +263,7 @@ class V9261FSensor : public BaseSensor {
double _voltage = 0;
double _current = 0;
double _apparent = 0;
double _energy = 0;
double _ratioP = V9261F_POWER_FACTOR;
double _ratioC = V9261F_CURRENT_FACTOR;


+ 31
- 19
code/espurna/settings.ino View File

@ -22,8 +22,6 @@ EmbedisWrap embedis(_serial, TERMINAL_BUFFER_SIZE);
#endif // SERIAL_RX_ENABLED
#endif // TERMINAL_SUPPORT
bool _settings_save = false;
// -----------------------------------------------------------------------------
// Reverse engineering EEPROM storage format
// -----------------------------------------------------------------------------
@ -31,9 +29,10 @@ bool _settings_save = false;
unsigned long settingsSize() {
unsigned pos = SPI_FLASH_SEC_SIZE - 1;
while (size_t len = EEPROMr.read(pos)) {
if (0xFF == len) break;
pos = pos - len - 2;
}
return SPI_FLASH_SEC_SIZE - pos;
return SPI_FLASH_SEC_SIZE - pos + EEPROM_DATA_END;
}
// -----------------------------------------------------------------------------
@ -42,6 +41,7 @@ unsigned int settingsKeyCount() {
unsigned count = 0;
unsigned pos = SPI_FLASH_SEC_SIZE - 1;
while (size_t len = EEPROMr.read(pos)) {
if (0xFF == len) break;
pos = pos - len - 2;
len = EEPROMr.read(pos);
pos = pos - len - 2;
@ -57,6 +57,7 @@ String settingsKeyName(unsigned int index) {
unsigned count = 0;
unsigned pos = SPI_FLASH_SEC_SIZE - 1;
while (size_t len = EEPROMr.read(pos)) {
if (0xFF == len) break;
pos = pos - len - 2;
if (count == index) {
s.reserve(len);
@ -186,6 +187,7 @@ void _settingsInitCommands() {
settingsRegisterCommand(F("ERASE.CONFIG"), [](Embedis* e) {
DEBUG_MSG_P(PSTR("+OK\n"));
resetReason(CUSTOM_RESET_TERMINAL);
_eepromCommit();
ESP.eraseConfig();
*((int*) 0) = 0; // see https://github.com/esp8266/Arduino/issues/1494
});
@ -228,7 +230,12 @@ void _settingsInitCommands() {
});
settingsRegisterCommand(F("HEAP"), [](Embedis* e) {
DEBUG_MSG_P(PSTR("Free HEAP: %d bytes\n"), getFreeHeap());
infoMemory("Heap", getInitialFreeHeap(), getFreeHeap());
DEBUG_MSG_P(PSTR("+OK\n"));
});
settingsRegisterCommand(F("STACK"), [](Embedis* e) {
infoMemory("Stack", 4096, getFreeStack());
DEBUG_MSG_P(PSTR("+OK\n"));
});
@ -239,10 +246,6 @@ void _settingsInitCommands() {
settingsRegisterCommand(F("INFO"), [](Embedis* e) {
info();
wifiDebug();
//StreamString s;
//WiFi.printDiag(s);
//DEBUG_MSG(s.c_str());
DEBUG_MSG_P(PSTR("+OK\n"));
});
@ -273,7 +276,7 @@ void _settingsInitCommands() {
#if WEB_SUPPORT
settingsRegisterCommand(F("RELOAD"), [](Embedis* e) {
wsReload();
espurnaReload();
DEBUG_MSG_P(PSTR("+OK\n"));
});
#endif
@ -294,6 +297,23 @@ void _settingsInitCommands() {
DEBUG_MSG_P(PSTR("+OK\n"));
});
settingsRegisterCommand(F("CONFIG"), [](Embedis* e) {
DynamicJsonBuffer jsonBuffer;
JsonObject& root = jsonBuffer.createObject();
settingsGetJson(root);
String output;
root.printTo(output);
DEBUG_MSG(output.c_str());
DEBUG_MSG_P(PSTR("\n+OK\n"));
});
#if not SETTINGS_AUTOSAVE
settingsRegisterCommand(F("SAVE"), [](Embedis* e) {
eepromCommit();
DEBUG_MSG_P(PSTR("\n+OK\n"));
});
#endif
}
// -----------------------------------------------------------------------------
@ -346,7 +366,7 @@ bool hasSetting(const String& key, unsigned int index) {
void saveSettings() {
#if not SETTINGS_AUTOSAVE
_settings_save = true;
eepromCommit();
#endif
}
@ -429,8 +449,6 @@ void settingsRegisterCommand(const String& name, void (*call)(Embedis*)) {
void settingsSetup() {
EEPROMr.begin(SPI_FLASH_SEC_SIZE);
_serial.callback([](uint8_t ch) {
#if TELNET_SUPPORT
telnetWrite(ch);
@ -445,7 +463,7 @@ void settingsSetup() {
[](size_t pos) -> char { return EEPROMr.read(pos); },
[](size_t pos, char value) { EEPROMr.write(pos, value); },
#if SETTINGS_AUTOSAVE
[]() { _settings_save = true; }
[]() { eepromCommit(); }
#else
[]() {}
#endif
@ -466,12 +484,6 @@ void settingsSetup() {
void settingsLoop() {
if (_settings_save) {
EEPROMr.commit();
_settings_save = false;
}
#if TERMINAL_SUPPORT
#if DEBUG_SERIAL_SUPPORT


+ 1
- 1
code/espurna/ssdp.ino View File

@ -46,7 +46,7 @@ void ssdpSetup() {
char response[strlen_P(_ssdp_template) + 100];
snprintf_P(response, sizeof(response), _ssdp_template,
WiFi.localIP().toString().c_str(), // ip
ip.toString().c_str(), // ip
webPort(), // port
SSDP_DEVICE_TYPE, // device type
getSetting("hostname").c_str(), // friendlyName


+ 3118
- 3081
code/espurna/static/index.all.html.gz.h
File diff suppressed because it is too large
View File


+ 2973
- 2953
code/espurna/static/index.light.html.gz.h
File diff suppressed because it is too large
View File


+ 2578
- 2552
code/espurna/static/index.rfbridge.html.gz.h
File diff suppressed because it is too large
View File


+ 4057
- 0
code/espurna/static/index.rfm69.html.gz.h
File diff suppressed because it is too large
View File


+ 2634
- 2591
code/espurna/static/index.sensor.html.gz.h
File diff suppressed because it is too large
View File


+ 2535
- 2508
code/espurna/static/index.small.html.gz.h
File diff suppressed because it is too large
View File


+ 17
- 5
code/espurna/system.ino View File

@ -42,7 +42,7 @@ void systemCheck(bool stable) {
}
}
EEPROMr.write(EEPROM_CRASH_COUNTER, value);
EEPROMr.commit();
eepromCommit();
}
bool systemCheck() {
@ -77,6 +77,14 @@ unsigned long systemLoadAverage() {
void systemLoop() {
// -------------------------------------------------------------------------
// User requested reset
// -------------------------------------------------------------------------
if (checkNeedsReset()) {
reset();
}
// -------------------------------------------------------------------------
// Check system stability
// -------------------------------------------------------------------------
@ -89,15 +97,19 @@ void systemLoop() {
// Heartbeat
// -------------------------------------------------------------------------
#if HEARTBEAT_ENABLED
// Heartbeat
#if HEARTBEAT_MODE == HEARTBEAT_ONCE
if (_system_send_heartbeat) {
_system_send_heartbeat = false;
heartbeat();
}
#elif HEARTBEAT_MODE == HEARTBEAT_REPEAT
static unsigned long last_hbeat = 0;
if (_system_send_heartbeat || (last_hbeat == 0) || (millis() - last_hbeat > HEARTBEAT_INTERVAL)) {
_system_send_heartbeat = false;
last_hbeat = millis();
heartbeat();
}
#endif // HEARTBEAT_ENABLED
#endif // HEARTBEAT_MODE == HEARTBEAT_REPEAT
// -------------------------------------------------------------------------
// Load Average calculation
@ -140,7 +152,7 @@ void _systemSetupSpecificHardware() {
// These devices use the hardware UART
// to communicate to secondary microcontrollers
#if defined(ITEAD_SONOFF_RFBRIDGE) || defined(ITEAD_SONOFF_DUAL) || defined(STM_RELAY)
#if defined(ITEAD_SONOFF_RFBRIDGE) || defined(ITEAD_SONOFF_DUAL) || (RELAY_PROVIDER == RELAY_PROVIDER_STM)
Serial.begin(SERIAL_BAUDRATE);
#endif


+ 41
- 3
code/espurna/telnet.ino View File

@ -15,6 +15,9 @@ Parts of the code have been borrowed from Thomas Sarlandie's NetServer
AsyncServer * _telnetServer;
AsyncClient * _telnetClients[TELNET_MAX_CLIENTS];
bool _telnetFirst = true;
#if TELNET_PASSWORD
bool _authenticated[TELNET_MAX_CLIENTS];
#endif
// -----------------------------------------------------------------------------
// Private methods
@ -35,8 +38,8 @@ void _telnetWebSocketOnSend(JsonObject& root) {
void _telnetDisconnect(unsigned char clientId) {
_telnetClients[clientId]->free();
_telnetClients[clientId] = NULL;
delete _telnetClients[clientId];
_telnetClients[clientId] = NULL;
wifiReconnectCheck();
DEBUG_MSG_P(PSTR("[TELNET] Client #%d disconnected\n"), clientId);
}
@ -51,11 +54,24 @@ bool _telnetWrite(unsigned char clientId, void *data, size_t len) {
unsigned char _telnetWrite(void *data, size_t len) {
unsigned char count = 0;
for (unsigned char i = 0; i < TELNET_MAX_CLIENTS; i++) {
if (_telnetWrite(i, data, len)) ++count;
#if TELNET_PASSWORD
// Do not send broadcast messages to unauthenticated clients
if (_authenticated[i]) {
if (_telnetWrite(i, data, len)) ++count;
}
#else
if (_telnetWrite(i, data, len)) ++count;
#endif
}
return count;
}
bool _telnetWrite(unsigned char clientId, const char * message) {
return _telnetWrite(clientId, (void *) message, strlen(message));
}
void _telnetData(unsigned char clientId, void *data, size_t len) {
// Skip first message since it's always garbage
@ -80,7 +96,22 @@ void _telnetData(unsigned char clientId, void *data, size_t len) {
return;
}
// Inject into Embedis stream
// Password
#if TELNET_PASSWORD
if (!_authenticated[clientId]) {
String password = getAdminPass();
if (strncmp(p, password.c_str(), password.length()) == 0) {
DEBUG_MSG_P(PSTR("[TELNET] Client #%d authenticated\n"), clientId);
_telnetWrite(clientId, "Welcome!\n");
_authenticated[clientId] = true;
} else {
_telnetWrite(clientId, "Password: ");
}
return;
}
#endif // TELNET_PASSWORD
// Inject command
settingsInject(data, len);
}
@ -109,6 +140,7 @@ void _telnetNewClient(AsyncClient *client) {
}
for (unsigned char i = 0; i < TELNET_MAX_CLIENTS; i++) {
if (!_telnetClients[i] || !_telnetClients[i]->connected()) {
_telnetClients[i] = client;
@ -143,8 +175,14 @@ void _telnetNewClient(AsyncClient *client) {
debugClearCrashInfo();
#endif
#if TELNET_PASSWORD
_authenticated[i] = false;
_telnetWrite(i, "Password: ");
#endif
_telnetFirst = true;
wifiReconnectCheck();
return;
}


+ 39
- 11
code/espurna/thinkspeak.ino View File

@ -25,10 +25,11 @@ const char THINGSPEAK_REQUEST_TEMPLATE[] PROGMEM =
"%s\r\n";
bool _tspk_enabled = false;
char * _tspk_queue[8] = {NULL};
char * _tspk_queue[THINGSPEAK_FIELDS] = {NULL};
bool _tspk_flush = false;
unsigned long _tspk_last_flush = 0;
unsigned char _tspk_tries = 0;
// -----------------------------------------------------------------------------
@ -97,12 +98,23 @@ void _tspkPost(String data) {
}, 0);
_tspk_client->onData([](void * arg, AsyncClient * c, void * response, size_t len) {
char * b = (char *) response;
b[len] = 0;
char * p = strstr((char *)response, "\r\n\r\n");
unsigned int code = (p != NULL) ? atoi(&p[4]) : 0;
DEBUG_MSG_P(PSTR("[THINGSPEAK] Response value: %d\n"), code);
_tspk_last_flush = millis();
if ((0 == code) && (--_tspk_tries > 0)) {
_tspk_flush = true;
DEBUG_MSG_P(PSTR("[THINGSPEAK] Re-enqueuing\n"));
} else {
_tspkClearQueue();
}
_tspk_client->close(true);
}, NULL);
_tspk_client->onConnect([data](void * arg, AsyncClient * client) {
@ -118,7 +130,7 @@ void _tspkPost(String data) {
}
#endif
DEBUG_MSG_P(PSTR("[THINGSPEAK] POST %s\n"), data.c_str());
DEBUG_MSG_P(PSTR("[THINGSPEAK] POST %s?%s\n"), THINGSPEAK_URL, data.c_str());
char buffer[strlen_P(THINGSPEAK_REQUEST_TEMPLATE) + strlen(THINGSPEAK_URL) + strlen(THINGSPEAK_HOST) + data.length()];
snprintf_P(buffer, sizeof(buffer),
@ -164,7 +176,7 @@ void _tspkPost(String data) {
DEBUG_MSG_P(PSTR("[THINGSPEAK] Warning: certificate doesn't match\n"));
}
DEBUG_MSG_P(PSTR("[THINGSPEAK] POST %s\n"), data.c_str());
DEBUG_MSG_P(PSTR("[THINGSPEAK] POST %s?%s\n"), THINGSPEAK_URL, data.c_str());
char buffer[strlen_P(THINGSPEAK_REQUEST_TEMPLATE) + strlen(THINGSPEAK_URL) + strlen(THINGSPEAK_HOST) + data.length()];
snprintf_P(buffer, sizeof(buffer),
THINGSPEAK_REQUEST_TEMPLATE,
@ -182,6 +194,15 @@ void _tspkPost(String data) {
unsigned int code = (pos > 0) ? response.substring(pos + 4).toInt() : 0;
DEBUG_MSG_P(PSTR("[THINGSPEAK] Response value: %d\n"), code);
_tspk_client.stop();
_tspk_last_flush = millis();
if ((0 == code) && (--_tspk_tries > 0)) {
_tspk_flush = true;
DEBUG_MSG_P(PSTR("[THINGSPEAK] Re-enqueuing\n"));
} else {
_tspkClearQueue();
}
return;
}
@ -199,25 +220,33 @@ void _tspkEnqueue(unsigned char index, char * payload) {
_tspk_queue[index] = strdup(payload);
}
void _tspkClearQueue() {
for (unsigned char id=0; id<THINGSPEAK_FIELDS; id++) {
if (_tspk_queue[id] != NULL) {
free(_tspk_queue[id]);
_tspk_queue[id] = NULL;
}
}
}
void _tspkFlush() {
String data;
_tspk_flush = false;
// Walk the fields
for (unsigned char id=0; id<8; id++) {
String data;
for (unsigned char id=0; id<THINGSPEAK_FIELDS; id++) {
if (_tspk_queue[id] != NULL) {
if (data.length() > 0) data = data + String("&");
data = data + String("field") + String(id+1) + String("=") + String(_tspk_queue[id]);
free(_tspk_queue[id]);
_tspk_queue[id] = NULL;
}
}
// POST data if any
if (data.length() > 0) {
data = data + String("&api_key=") + getSetting("tspkKey");
_tspk_tries = THINGSPEAK_TRIES;
_tspkPost(data);
_tspk_last_flush = millis();
}
}
@ -259,7 +288,6 @@ void tspkSetup() {
#if WEB_SUPPORT
wsOnSendRegister(_tspkWebSocketOnSend);
wsOnAfterParseRegister(_tspkConfigure);
wsOnReceiveRegister(_tspkWebSocketOnReceive);
#endif
@ -268,8 +296,9 @@ void tspkSetup() {
THINGSPEAK_USE_SSL ? "ENABLED" : "DISABLED"
);
// Register loop
// Main callbacks
espurnaRegisterLoop(tspkLoop);
espurnaRegisterReload(_tspkConfigure);
}
@ -278,7 +307,6 @@ void tspkLoop() {
if (!wifiConnected() || (WiFi.getMode() != WIFI_STA)) return;
if (_tspk_flush && (millis() - _tspk_last_flush > THINGSPEAK_MIN_INTERVAL)) {
_tspkFlush();
_tspk_flush = false;
}
}


+ 117
- 47
code/espurna/utils.ino View File

@ -9,6 +9,8 @@ Copyright (C) 2017-2018 by Xose Pérez <xose dot perez at gmail dot com>
#include <Ticker.h>
Ticker _defer_reset;
uint8_t _reset_reason = 0;
String getIdentifier() {
char buffer[20];
snprintf_P(buffer, sizeof(buffer), PSTR("%s-%06X"), APP_NAME, ESP.getChipId());
@ -33,6 +35,10 @@ String getBoardName() {
return getSetting("boardName", DEVICE_NAME);
}
String getAdminPass() {
return getSetting("adminPass", ADMIN_PASS);
}
String getCoreVersion() {
String version = ESP.getCoreVersion();
#ifdef ARDUINO_ESP8266_RELEASE
@ -52,10 +58,6 @@ String getCoreRevision() {
#endif
}
unsigned long maxSketchSpace() {
return (ESP.getFreeSketchSpace() - 0x1000) & 0xFFFFF000;
}
// WTF
// Calling ESP.getFreeHeap() is making the system crash on a specific
// AiLight bulb, but anywhere else...
@ -64,6 +66,18 @@ unsigned int getFreeHeap() {
return ESP.getFreeHeap();
}
unsigned int getInitialFreeHeap() {
static unsigned int _heap = 0;
if (0 == _heap) {
_heap = getFreeHeap();
}
return _heap;
}
unsigned int getUsedHeap() {
return getInitialFreeHeap() - getFreeHeap();
}
String getEspurnaModules() {
return FPSTR(espurna_modules);
}
@ -74,6 +88,10 @@ String getEspurnaSensors() {
}
#endif
String getEspurnaWebUI() {
return FPSTR(espurna_webui);
}
String buildTime() {
const char time_now[] = __TIME__; // hh:mm:ss
@ -117,7 +135,7 @@ unsigned long getUptime() {
}
#if HEARTBEAT_ENABLED
#if HEARTBEAT_MODE != HEARTBEAT_NONE
void heartbeat() {
@ -136,7 +154,7 @@ void heartbeat() {
if (serial) {
DEBUG_MSG_P(PSTR("[MAIN] Uptime: %lu seconds\n"), uptime_seconds);
DEBUG_MSG_P(PSTR("[MAIN] Free heap: %lu bytes\n"), free_heap);
infoMemory("Heap", getInitialFreeHeap(), getFreeHeap());
#if ADC_MODE_VALUE == ADC_VCC
DEBUG_MSG_P(PSTR("[MAIN] Power: %lu mV\n"), ESP.getVcc());
#endif
@ -166,6 +184,9 @@ void heartbeat() {
#if (HEARTBEAT_REPORT_HOSTNAME)
mqttSend(MQTT_TOPIC_HOSTNAME, getSetting("hostname").c_str());
#endif
#if (HEARTBEAT_REPORT_SSID)
mqttSend(MQTT_TOPIC_SSID, WiFi.SSID().c_str());
#endif
#if (HEARTBEAT_REPORT_IP)
mqttSend(MQTT_TOPIC_IP, getIP().c_str());
#endif
@ -219,7 +240,7 @@ void heartbeat() {
}
#endif /// HEARTBEAT_ENABLED
#endif /// HEARTBEAT_MODE != HEARTBEAT_NONE
// -----------------------------------------------------------------------------
// INFO
@ -249,7 +270,7 @@ void _info_print_memory_layout_line(const char * name, unsigned long bytes, bool
if (reset) index = 0;
if (0 == bytes) return;
unsigned int _sectors = info_bytes2sectors(bytes);
DEBUG_MSG_P(PSTR("[INIT] %-20s: %8lu bytes / %4d sectors (%4d to %4d)\n"), name, bytes, _sectors, index, index + _sectors - 1);
DEBUG_MSG_P(PSTR("[MAIN] %-20s: %8lu bytes / %4d sectors (%4d to %4d)\n"), name, bytes, _sectors, index, index + _sectors - 1);
index += _sectors;
}
@ -257,27 +278,50 @@ void _info_print_memory_layout_line(const char * name, unsigned long bytes) {
_info_print_memory_layout_line(name, bytes, false);
}
void infoMemory(const char * name, unsigned int total_memory, unsigned int free_memory) {
DEBUG_MSG_P(
PSTR("[MAIN] %-6s: %5u bytes initially | %5u bytes used (%2u%%) | %5u bytes free (%2u%%)\n"),
name,
total_memory,
total_memory - free_memory,
100 * (total_memory - free_memory) / total_memory,
free_memory,
100 * free_memory / total_memory
);
}
void info() {
DEBUG_MSG_P(PSTR("\n\n"));
DEBUG_MSG_P(PSTR("[INIT] %s %s\n"), (char *) APP_NAME, (char *) APP_VERSION);
DEBUG_MSG_P(PSTR("[INIT] %s\n"), (char *) APP_AUTHOR);
DEBUG_MSG_P(PSTR("[INIT] %s\n\n"), (char *) APP_WEBSITE);
DEBUG_MSG_P(PSTR("[INIT] CPU chip ID: 0x%06X\n"), ESP.getChipId());
DEBUG_MSG_P(PSTR("[INIT] CPU frequency: %u MHz\n"), ESP.getCpuFreqMHz());
DEBUG_MSG_P(PSTR("[INIT] SDK version: %s\n"), ESP.getSdkVersion());
DEBUG_MSG_P(PSTR("[INIT] Core version: %s\n"), getCoreVersion().c_str());
DEBUG_MSG_P(PSTR("[INIT] Core revision: %s\n"), getCoreRevision().c_str());
DEBUG_MSG_P(PSTR("\n\n---8<-------\n\n"));
// -------------------------------------------------------------------------
#if defined(APP_REVISION)
DEBUG_MSG_P(PSTR("[MAIN] " APP_NAME " " APP_VERSION " (" APP_REVISION ")\n"));
#else
DEBUG_MSG_P(PSTR("[MAIN] " APP_NAME " " APP_VERSION "\n"));
#endif
DEBUG_MSG_P(PSTR("[MAIN] " APP_AUTHOR "\n"));
DEBUG_MSG_P(PSTR("[MAIN] " APP_WEBSITE "\n\n"));
DEBUG_MSG_P(PSTR("[MAIN] CPU chip ID: 0x%06X\n"), ESP.getChipId());
DEBUG_MSG_P(PSTR("[MAIN] CPU frequency: %u MHz\n"), ESP.getCpuFreqMHz());
DEBUG_MSG_P(PSTR("[MAIN] SDK version: %s\n"), ESP.getSdkVersion());
DEBUG_MSG_P(PSTR("[MAIN] Core version: %s\n"), getCoreVersion().c_str());
DEBUG_MSG_P(PSTR("[MAIN] Core revision: %s\n"), getCoreRevision().c_str());
DEBUG_MSG_P(PSTR("\n"));
// -------------------------------------------------------------------------
FlashMode_t mode = ESP.getFlashChipMode();
DEBUG_MSG_P(PSTR("[INIT] Flash chip ID: 0x%06X\n"), ESP.getFlashChipId());
DEBUG_MSG_P(PSTR("[INIT] Flash speed: %u Hz\n"), ESP.getFlashChipSpeed());
DEBUG_MSG_P(PSTR("[INIT] Flash mode: %s\n"), mode == FM_QIO ? "QIO" : mode == FM_QOUT ? "QOUT" : mode == FM_DIO ? "DIO" : mode == FM_DOUT ? "DOUT" : "UNKNOWN");
DEBUG_MSG_P(PSTR("[MAIN] Flash chip ID: 0x%06X\n"), ESP.getFlashChipId());
DEBUG_MSG_P(PSTR("[MAIN] Flash speed: %u Hz\n"), ESP.getFlashChipSpeed());
DEBUG_MSG_P(PSTR("[MAIN] Flash mode: %s\n"), mode == FM_QIO ? "QIO" : mode == FM_QOUT ? "QOUT" : mode == FM_DIO ? "DIO" : mode == FM_DOUT ? "DOUT" : "UNKNOWN");
DEBUG_MSG_P(PSTR("\n"));
// -------------------------------------------------------------------------
_info_print_memory_layout_line("Flash size (CHIP)", ESP.getFlashChipRealSize(), true);
_info_print_memory_layout_line("Flash size (SDK)", ESP.getFlashChipSize(), true);
_info_print_memory_layout_line("Reserved", 1 * SPI_FLASH_SEC_SIZE, true);
@ -288,60 +332,81 @@ void info() {
_info_print_memory_layout_line("Reserved", 4 * SPI_FLASH_SEC_SIZE);
DEBUG_MSG_P(PSTR("\n"));
DEBUG_MSG_P(PSTR("[INIT] EEPROM sectors: %s\n"), (char *) eepromSectors().c_str());
DEBUG_MSG_P(PSTR("\n"));
// -------------------------------------------------------------------------
#if SPIFFS_SUPPORT
FSInfo fs_info;
bool fs = SPIFFS.info(fs_info);
if (fs) {
DEBUG_MSG_P(PSTR("[INIT] SPIFFS total size: %8u bytes / %4d sectors\n"), fs_info.totalBytes, sectors(fs_info.totalBytes));
DEBUG_MSG_P(PSTR("[INIT] used size: %8u bytes\n"), fs_info.usedBytes);
DEBUG_MSG_P(PSTR("[INIT] block size: %8u bytes\n"), fs_info.blockSize);
DEBUG_MSG_P(PSTR("[INIT] page size: %8u bytes\n"), fs_info.pageSize);
DEBUG_MSG_P(PSTR("[INIT] max files: %8u\n"), fs_info.maxOpenFiles);
DEBUG_MSG_P(PSTR("[INIT] max length: %8u\n"), fs_info.maxPathLength);
DEBUG_MSG_P(PSTR("[MAIN] SPIFFS total size : %8u bytes / %4d sectors\n"), fs_info.totalBytes, info_bytes2sectors(fs_info.totalBytes));
DEBUG_MSG_P(PSTR("[MAIN] used size : %8u bytes\n"), fs_info.usedBytes);
DEBUG_MSG_P(PSTR("[MAIN] block size : %8u bytes\n"), fs_info.blockSize);
DEBUG_MSG_P(PSTR("[MAIN] page size : %8u bytes\n"), fs_info.pageSize);
DEBUG_MSG_P(PSTR("[MAIN] max files : %8u\n"), fs_info.maxOpenFiles);
DEBUG_MSG_P(PSTR("[MAIN] max length : %8u\n"), fs_info.maxPathLength);
} else {
DEBUG_MSG_P(PSTR("[INIT] No SPIFFS partition\n"));
DEBUG_MSG_P(PSTR("[MAIN] No SPIFFS partition\n"));
}
DEBUG_MSG_P(PSTR("\n"));
#endif
// -------------------------------------------------------------------------
DEBUG_MSG_P(PSTR("[INIT] BOARD: %s\n"), getBoardName().c_str());
DEBUG_MSG_P(PSTR("[INIT] SUPPORT: %s\n"), getEspurnaModules().c_str());
#if SENSOR_SUPPORT
DEBUG_MSG_P(PSTR("[INIT] SENSORS: %s\n"), getEspurnaSensors().c_str());
#endif // SENSOR_SUPPORT
eepromSectorsDebug();
DEBUG_MSG_P(PSTR("\n"));
// -------------------------------------------------------------------------
infoMemory("EEPROM", SPI_FLASH_SEC_SIZE, SPI_FLASH_SEC_SIZE - settingsSize());
infoMemory("Heap", getInitialFreeHeap(), getFreeHeap());
infoMemory("Stack", 4096, getFreeStack());
DEBUG_MSG_P(PSTR("\n"));
// -------------------------------------------------------------------------
DEBUG_MSG_P(PSTR("[MAIN] Boot version: %d\n"), ESP.getBootVersion());
DEBUG_MSG_P(PSTR("[MAIN] Boot mode: %d\n"), ESP.getBootMode());
unsigned char reason = resetReason();
if (reason > 0) {
char buffer[32];
strcpy_P(buffer, custom_reset_string[reason-1]);
DEBUG_MSG_P(PSTR("[INIT] Last reset reason: %s\n"), buffer);
DEBUG_MSG_P(PSTR("[MAIN] Last reset reason: %s\n"), buffer);
} else {
DEBUG_MSG_P(PSTR("[INIT] Last reset reason: %s\n"), (char *) ESP.getResetReason().c_str());
DEBUG_MSG_P(PSTR("[MAIN] Last reset reason: %s\n"), (char *) ESP.getResetReason().c_str());
DEBUG_MSG_P(PSTR("[MAIN] Last reset info: %s\n"), (char *) ESP.getResetInfo().c_str());
}
DEBUG_MSG_P(PSTR("\n"));
DEBUG_MSG_P(PSTR("[INIT] Settings size: %u bytes\n"), settingsSize());
DEBUG_MSG_P(PSTR("[INIT] Free heap: %u bytes\n"), getFreeHeap());
// -------------------------------------------------------------------------
DEBUG_MSG_P(PSTR("[MAIN] Board: %s\n"), getBoardName().c_str());
DEBUG_MSG_P(PSTR("[MAIN] Support: %s\n"), getEspurnaModules().c_str());
#if SENSOR_SUPPORT
DEBUG_MSG_P(PSTR("[MAIN] Sensors: %s\n"), getEspurnaSensors().c_str());
#endif // SENSOR_SUPPORT
DEBUG_MSG_P(PSTR("[MAIN] WebUI image: %s\n"), getEspurnaWebUI().c_str());
DEBUG_MSG_P(PSTR("\n"));
// -------------------------------------------------------------------------
DEBUG_MSG_P(PSTR("[MAIN] Firmware MD5: %s\n"), (char *) ESP.getSketchMD5().c_str());
#if ADC_MODE_VALUE == ADC_VCC
DEBUG_MSG_P(PSTR("[INIT] Power: %u mV\n"), ESP.getVcc());
DEBUG_MSG_P(PSTR("[MAIN] Power: %u mV\n"), ESP.getVcc());
#endif
DEBUG_MSG_P(PSTR("[MAIN] Power saving delay value: %lu ms\n"), systemLoopDelay());
DEBUG_MSG_P(PSTR("[INIT] Power saving delay value: %lu ms\n"), systemLoopDelay());
// -------------------------------------------------------------------------
#if SYSTEM_CHECK_ENABLED
if (!systemCheck()) DEBUG_MSG_P(PSTR("\n[INIT] Device is in SAFE MODE\n"));
if (!systemCheck()) {
DEBUG_MSG_P(PSTR("\n"));
DEBUG_MSG_P(PSTR("[MAIN] Device is in SAFE MODE\n"));
}
#endif
DEBUG_MSG_P(PSTR("\n"));
// -------------------------------------------------------------------------
DEBUG_MSG_P(PSTR("\n\n---8<-------\n\n"));
}
@ -403,17 +468,21 @@ unsigned char resetReason() {
}
void resetReason(unsigned char reason) {
_reset_reason = reason;
EEPROMr.write(EEPROM_CUSTOM_RESET, reason);
EEPROMr.commit();
eepromCommit();
}
void reset(unsigned char reason) {
resetReason(reason);
void reset() {
ESP.restart();
}
void deferredReset(unsigned long delay, unsigned char reason) {
_defer_reset.once_ms(delay, reset, reason);
_defer_reset.once_ms(delay, resetReason, reason);
}
bool checkNeedsReset() {
return _reset_reason > 0;
}
// -----------------------------------------------------------------------------
@ -442,6 +511,7 @@ int __get_adc_mode() {
bool isNumber(const char * s) {
unsigned char len = strlen(s);
if (0 == len) return false;
bool decimal = false;
for (unsigned char i=0; i<len; i++) {
if (s[i] == '-') {


+ 35
- 48
code/espurna/web.ino View File

@ -17,52 +17,17 @@ Copyright (C) 2016-2018 by Xose Pérez <xose dot perez at gmail dot com>
#if WEB_EMBEDDED
#define WEBUI_MODULE_SMALL 0
#define WEBUI_MODULE_LIGHT 1
#define WEBUI_MODULE_SENSOR 2
#define WEBUI_MODULE_RFBRIDGE 4
#define WEBUI_MODULE_ALL 7
#if LIGHT_PROVIDER != LIGHT_PROVIDER_NONE
#ifdef WEBUI_MODULE
#undef WEBUI_MODULE
#define WEBUI_MODULE WEBUI_MODULE_ALL
#else
#define WEBUI_MODULE WEBUI_MODULE_LIGHT
#endif
#endif
#if SENSOR_SUPPORT == 1
#ifndef WEBUI_MODULE
#define WEBUI_MODULE WEBUI_MODULE_SENSOR
#else
#undef WEBUI_MODULE
#define WEBUI_MODULE WEBUI_MODULE_ALL
#endif
#endif
#if defined(ITEAD_SONOFF_RFBRIDGE)
#ifndef WEBUI_MODULE
#define WEBUI_MODULE WEBUI_MODULE_RFBRIDGE
#else
#undef WEBUI_MODULE
#define WEBUI_MODULE WEBUI_MODULE_ALL
#endif
#endif
#ifndef WEBUI_MODULE
#define WEBUI_MODULE WEBUI_MODULE_SMALL
#endif
#if WEBUI_MODULE == WEBUI_MODULE_SMALL
#if WEBUI_IMAGE == WEBUI_IMAGE_SMALL
#include "static/index.small.html.gz.h"
#elif WEBUI_MODULE == WEBUI_MODULE_LIGHT
#elif WEBUI_IMAGE == WEBUI_IMAGE_LIGHT
#include "static/index.light.html.gz.h"
#elif WEBUI_MODULE == WEBUI_MODULE_SENSOR
#elif WEBUI_IMAGE == WEBUI_IMAGE_SENSOR
#include "static/index.sensor.html.gz.h"
#elif WEBUI_MODULE == WEBUI_MODULE_RFBRIDGE
#elif WEBUI_IMAGE == WEBUI_IMAGE_RFBRIDGE
#include "static/index.rfbridge.html.gz.h"
#elif WEBUI_MODULE == WEBUI_MODULE_ALL
#elif WEBUI_IMAGE == WEBUI_IMAGE_RFM69
#include "static/index.rfm69.html.gz.h"
#elif WEBUI_IMAGE == WEBUI_IMAGE_FULL
#include "static/index.all.html.gz.h"
#endif
@ -80,6 +45,8 @@ char _last_modified[50];
std::vector<uint8_t> * _webConfigBuffer;
bool _webConfigSuccess = false;
std::vector<web_request_callback_f> _web_request_callbacks;
// -----------------------------------------------------------------------------
// HOOKS
// -----------------------------------------------------------------------------
@ -126,7 +93,9 @@ void _onGetConfig(AsyncWebServerRequest *request) {
response->printf("{\n\"app\": \"%s\"", APP_NAME);
response->printf(",\n\"version\": \"%s\"", APP_VERSION);
response->printf(",\n\"backup\": \"1\"");
response->printf(",\n\"timestamp\": \"%s\"", ntpDateTime().c_str());
#if NTP_SUPPORT
response->printf(",\n\"timestamp\": \"%s\"", ntpDateTime().c_str());
#endif
// Write the keys line by line (not sorted)
unsigned long count = settingsKeyCount();
@ -361,11 +330,24 @@ void _onUpgradeData(AsyncWebServerRequest *request, String filename, size_t inde
}
}
void _onRequest(AsyncWebServerRequest *request){
// Send request to subscribers
for (unsigned char i = 0; i < _web_request_callbacks.size(); i++) {
bool response = (_web_request_callbacks[i])(request);
if (response) return;
}
// No subscriber handled the request, return a 404
request->send(404);
}
// -----------------------------------------------------------------------------
bool webAuthenticate(AsyncWebServerRequest *request) {
#if USE_PASSWORD
String password = getSetting("adminPass", ADMIN_PASS);
String password = getAdminPass();
char httpPassword[password.length() + 1];
password.toCharArray(httpPassword, password.length() + 1);
return request->authenticate(WEB_USERNAME, httpPassword);
@ -380,6 +362,10 @@ AsyncWebServer * webServer() {
return _server;
}
void webRequestRegister(web_request_callback_f callback) {
_web_request_callbacks.push_back(callback);
}
unsigned int webPort() {
#if ASYNC_TCP_SSL_ENABLED & WEB_SSL_ENABLED
return 443;
@ -408,6 +394,8 @@ void webSetup() {
#if WEB_EMBEDDED
_server->on("/index.html", HTTP_GET, _onHome);
#endif
// Other entry points
_server->on("/reset", HTTP_GET, _onReset);
_server->on("/config", HTTP_GET, _onGetConfig);
_server->on("/config", HTTP_POST | HTTP_PUT, _onPostConfig, _onPostConfigData);
@ -424,10 +412,8 @@ void webSetup() {
});
#endif
// 404
_server->onNotFound([](AsyncWebServerRequest *request){
request->send(404);
});
// Handle other requests, including 404
_server->onNotFound(_onRequest);
// Run server
#if ASYNC_TCP_SSL_ENABLED & WEB_SSL_ENABLED
@ -436,6 +422,7 @@ void webSetup() {
#else
_server->begin();
#endif
DEBUG_MSG_P(PSTR("[WEBSERVER] Webserver running on port %u\n"), port);
}


+ 16
- 5
code/espurna/wifi.ino View File

@ -34,13 +34,13 @@ void _wifiConfigure() {
jw.setHostname(getSetting("hostname").c_str());
#if USE_PASSWORD
jw.setSoftAP(getSetting("hostname").c_str(), getSetting("adminPass", ADMIN_PASS).c_str());
jw.setSoftAP(getSetting("hostname").c_str(), getAdminPass().c_str());
#else
jw.setSoftAP(getSetting("hostname").c_str());
#endif
jw.setConnectTimeout(WIFI_CONNECT_TIMEOUT);
wifiReconnectCheck();
jw.enableAPFallback(true);
jw.enableAPFallback(WIFI_FALLBACK_APMODE);
jw.cleanNetworks();
_wifi_ap_mode = getSetting("apmode", WIFI_AP_FALLBACK).toInt();
@ -226,6 +226,7 @@ void _wifiCallback(justwifi_messages_t code, char * parameter) {
if (MESSAGE_WPS_ERROR == code || MESSAGE_SMARTCONFIG_ERROR == code) {
_wifi_wps_running = false;
_wifi_smartconfig_running = false;
jw.enableAP(true);
}
if (MESSAGE_WPS_SUCCESS == code || MESSAGE_SMARTCONFIG_SUCCESS == code) {
@ -249,6 +250,7 @@ void _wifiCallback(justwifi_messages_t code, char * parameter) {
_wifi_wps_running = false;
_wifi_smartconfig_running = false;
jw.enableAP(true);
}
@ -383,6 +385,11 @@ void _wifiDebugCallback(justwifi_messages_t code, char * parameter) {
void _wifiInitCommands() {
settingsRegisterCommand(F("WIFI"), [](Embedis* e) {
wifiDebug();
DEBUG_MSG_P(PSTR("+OK\n"));
});
settingsRegisterCommand(F("WIFI.RESET"), [](Embedis* e) {
_wifiConfigure();
wifiDisconnect();
@ -487,7 +494,7 @@ void wifiDebug(WiFiMode_t modes) {
if (((modes & WIFI_AP) > 0) && ((WiFi.getMode() & WIFI_AP) > 0)) {
DEBUG_MSG_P(PSTR("[WIFI] -------------------------------------- MODE AP\n"));
DEBUG_MSG_P(PSTR("[WIFI] SSID %s\n"), getSetting("hostname").c_str());
DEBUG_MSG_P(PSTR("[WIFI] PASS %s\n"), getSetting("adminPass", ADMIN_PASS).c_str());
DEBUG_MSG_P(PSTR("[WIFI] PASS %s\n"), getAdminPass().c_str());
DEBUG_MSG_P(PSTR("[WIFI] IP %s\n"), WiFi.softAPIP().toString().c_str());
DEBUG_MSG_P(PSTR("[WIFI] MAC %s\n"), WiFi.softAPmacAddress().c_str());
footer = true;
@ -550,12 +557,16 @@ void wifiStartAP() {
#if defined(JUSTWIFI_ENABLE_WPS)
void wifiStartWPS() {
jw.enableAP(false);
jw.disconnect();
jw.startWPS();
}
#endif // defined(JUSTWIFI_ENABLE_WPS)
#if defined(JUSTWIFI_ENABLE_SMARTCONFIG)
void wifiStartSmartConfig() {
jw.enableAP(false);
jw.disconnect();
jw.startSmartConfig();
}
#endif // defined(JUSTWIFI_ENABLE_SMARTCONFIG)
@ -607,7 +618,6 @@ void wifiSetup() {
#if WEB_SUPPORT
wsOnSendRegister(_wifiWebSocketOnSend);
wsOnReceiveRegister(_wifiWebSocketOnReceive);
wsOnAfterParseRegister(_wifiConfigure);
wsOnActionRegister(_wifiWebSocketOnAction);
#endif
@ -615,8 +625,9 @@ void wifiSetup() {
_wifiInitCommands();
#endif
// Register loop
// Main callbacks
espurnaRegisterLoop(wifiLoop);
espurnaRegisterReload(_wifiConfigure);
}


+ 5
- 18
code/espurna/ws.ino View File

@ -20,7 +20,6 @@ Ticker _web_defer;
std::vector<ws_on_send_callback_f> _ws_on_send_callbacks;
std::vector<ws_on_action_callback_f> _ws_on_action_callbacks;
std::vector<ws_on_after_parse_callback_f> _ws_on_after_parse_callbacks;
std::vector<ws_on_receive_callback_f> _ws_on_receive_callbacks;
// -----------------------------------------------------------------------------
@ -256,7 +255,7 @@ void _wsParse(AsyncWebSocketClient *client, uint8_t * payload, size_t length) {
if (save) {
// Callbacks
wsReload();
espurnaReload();
// This should got to callback as well
// but first change management has to be in place
@ -303,8 +302,7 @@ bool _wsOnReceive(const char * key, JsonVariant& value) {
void _wsOnStart(JsonObject& root) {
#if USE_PASSWORD && WEB_FORCE_PASS_CHANGE
String adminPass = getSetting("adminPass", ADMIN_PASS);
bool changePassword = adminPass.equals(ADMIN_PASS);
bool changePassword = getAdminPass().equals(ADMIN_PASS);
#else
bool changePassword = false;
#endif
@ -329,7 +327,9 @@ void _wsOnStart(JsonObject& root) {
root["app_name"] = APP_NAME;
root["app_version"] = APP_VERSION;
root["app_build"] = buildTime();
root["app_revision"] = APP_REVISION;
#if defined(APP_REVISION)
root["app_revision"] = APP_REVISION;
#endif
root["manufacturer"] = MANUFACTURER;
root["chipid"] = String(chipid);
root["mac"] = WiFi.macAddress();
@ -429,10 +429,6 @@ void wsOnActionRegister(ws_on_action_callback_f callback) {
_ws_on_action_callbacks.push_back(callback);
}
void wsOnAfterParseRegister(ws_on_after_parse_callback_f callback) {
_ws_on_after_parse_callbacks.push_back(callback);
}
void wsSend(ws_on_send_callback_f callback) {
if (_ws.count() > 0) {
DynamicJsonBuffer jsonBuffer;
@ -479,15 +475,6 @@ void wsSend_P(uint32_t client_id, PGM_P payload) {
_ws.text(client_id, buffer);
}
// This method being public makes
// _ws_on_after_parse_callbacks strange here,
// it should belong somewhere else.
void wsReload() {
for (unsigned char i = 0; i < _ws_on_after_parse_callbacks.size(); i++) {
(_ws_on_after_parse_callbacks[i])();
}
}
void wsSetup() {
_ws.onEvent(_wsEvent);


+ 4
- 5
code/extra_scripts.py View File

@ -4,13 +4,9 @@ from __future__ import print_function
import os
import sys
from subprocess import call
import click
from platformio import util
import distutils.spawn
Import("env")
Import("env", "projenv")
# ------------------------------------------------------------------------------
# Utils
@ -86,6 +82,9 @@ def check_size(source, target, env):
# Hooks
# ------------------------------------------------------------------------------
# Always show warnings for project code
projenv.ProcessUnFlags("-w")
remove_float_support()
env.AddPostAction("$BUILD_DIR/${PROGNAME}.bin", check_size)

+ 79
- 83
code/gulpfile.js View File

@ -19,34 +19,38 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
/*eslint quotes: ["error", "single"]*/
/*eslint quotes: ['error', 'single']*/
/*eslint-env es6*/
// -----------------------------------------------------------------------------
// Dependencies
// -----------------------------------------------------------------------------
const fs = require('fs');
const gulp = require('gulp');
const runSequence = require('run-sequence');
const through = require('through2');
const htmlmin = require('gulp-htmlmin');
const uglify = require('gulp-uglify');
const gzip = require('gulp-gzip');
const inline = require('gulp-inline');
const inlineImages = require('gulp-css-base64');
const favicon = require('gulp-base64-favicon');
const crass = require('gulp-crass');
const htmllint = require('gulp-htmllint');
const csslint = require('gulp-csslint');
const crass = require('gulp-crass');
const rename = require('gulp-rename');
const replace = require('gulp-replace');
const remover = require('gulp-remove-code');
const map = require('map-stream');
const rename = require('gulp-rename');
const runSequence = require('run-sequence');
const gzip = require('gulp-gzip');
const path = require('path');
// -----------------------------------------------------------------------------
// Configuration
// -----------------------------------------------------------------------------
const htmlFolder = 'html/';
const configFolder = 'espurna/config/';
const dataFolder = 'espurna/data/';
const staticFolder = 'espurna/static/';
@ -54,51 +58,39 @@ const staticFolder = 'espurna/static/';
// Methods
// -----------------------------------------------------------------------------
var buildHeaderFile = function() {
String.prototype.replaceAll = function(search, replacement) {
var target = this;
return target.split(search).join(replacement);
};
var toHeader = function(name, debug) {
return map(function(file, cb) {
return through.obj(function (source, encoding, callback) {
var parts = file.path.split("/");
var parts = source.path.split(path.sep);
var filename = parts[parts.length - 1];
var destination = staticFolder + filename + ".h";
var safename = "webui_image";
var wstream = fs.createWriteStream(destination);
wstream.on('error', function (err) {
console.error(err);
});
var data = fs.readFileSync(file.path);
wstream.write('#define ' + safename + '_len ' + data.length + '\n');
wstream.write('const uint8_t ' + safename + '[] PROGMEM = {');
for (var i=0; i<data.length; i++) {
if (0 === (i % 20)) {
wstream.write('\n');
}
wstream.write('0x' + ('00' + data[i].toString(16)).slice(-2));
if (i < (data.length - 1)) {
wstream.write(',');
}
var safename = name || filename.split('.').join('_');
// Generate output
var output = '';
output += '#define ' + safename + '_len ' + source.contents.length + '\n';
output += 'const uint8_t ' + safename + '[] PROGMEM = {';
for (var i=0; i<source.contents.length; i++) {
if (i > 0) { output += ','; }
if (0 === (i % 20)) { output += '\n'; }
output += '0x' + ('00' + source.contents[i].toString(16)).slice(-2);
}
output += '\n};';
wstream.write('\n};');
wstream.end();
// clone the contents
var destination = source.clone();
destination.path = source.path + '.h';
destination.contents = Buffer.from(output);
var fstat = fs.statSync(file.path);
console.log("Created '" + filename + "' size: " + fstat.size + " bytes");
if (debug) {
console.info('Image ' + filename + ' \tsize: ' + source.contents.length + ' bytes');
}
cb(0, destination);
callback(null, destination);
});
}
};
var htmllintReporter = function(filepath, issues) {
if (issues.length > 0) {
@ -118,16 +110,17 @@ var htmllintReporter = function(filepath, issues) {
var buildWebUI = function(module) {
var modules = {"light": false, "sensor": false, "rfbridge": false};
if ("all" == module) {
modules["light"] = true;
modules["rfbridge"] = true;
modules["sensor"] = true;
} else if ("small" != module) {
var modules = {'light': false, 'sensor': false, 'rfbridge': false, 'rfm69': false};
if ('all' === module) {
modules['light'] = true;
modules['sensor'] = true;
modules['rfbridge'] = true;
modules['rfm69'] = false; // we will never be adding this except when building RFM69GW
} else if ('small' !== module) {
modules[module] = true;
}
return gulp.src('html/*.html').
return gulp.src(htmlFolder + '*.html').
pipe(htmllint({
'failOnError': true,
'rules': {
@ -137,7 +130,7 @@ var buildWebUI = function(module) {
}, htmllintReporter)).
pipe(favicon()).
pipe(inline({
base: 'html/',
base: htmlFolder,
js: [],
css: [crass, inlineImages],
disabledTypes: ['svg', 'img']
@ -151,8 +144,10 @@ var buildWebUI = function(module) {
})).
pipe(replace('pure-', 'p-')).
pipe(gzip()).
pipe(rename("index." + module + ".html.gz")).
pipe(gulp.dest(dataFolder));
pipe(rename('index.' + module + '.html.gz')).
pipe(gulp.dest(dataFolder)).
pipe(toHeader('webui_image', true)).
pipe(gulp.dest(staticFolder));
};
@ -160,50 +155,51 @@ var buildWebUI = function(module) {
// Tasks
// -----------------------------------------------------------------------------
gulp.task('build_certs', function() {
gulp.task('certs', function() {
gulp.src(dataFolder + 'server.*').
pipe(buildHeaderFile());
pipe(toHeader(debug=false)).
pipe(gulp.dest(staticFolder));
});
gulp.task('csslint', function() {
gulp.src('html/*.css').
gulp.src(htmlFolder + '*.css').
pipe(csslint({ids: false})).
pipe(csslint.formatter());
});
gulp.task('build_webui_small', function() {
return buildWebUI("small");
})
gulp.task('webui_small', function() {
return buildWebUI('small');
});
gulp.task('build_webui_sensor', function() {
return buildWebUI("sensor");
})
gulp.task('webui_sensor', function() {
return buildWebUI('sensor');
});
gulp.task('build_webui_light', function() {
return buildWebUI("light");
})
gulp.task('webui_light', function() {
return buildWebUI('light');
});
gulp.task('build_webui_rfbridge', function() {
return buildWebUI("rfbridge");
})
gulp.task('webui_rfbridge', function() {
return buildWebUI('rfbridge');
});
gulp.task('build_webui_all', function() {
return buildWebUI("all");
})
gulp.task('webui_rfm69', function() {
return buildWebUI('rfm69');
});
gulp.task('buildfs_inline', function(cb) {
runSequence([
'build_webui_small',
'build_webui_sensor',
'build_webui_light',
'build_webui_rfbridge',
'build_webui_all'
], cb);
gulp.task('webui_all', function() {
return buildWebUI('all');
});
gulp.task('buildfs_embeded', ['buildfs_inline'], function() {
gulp.src(dataFolder + 'index.*').
pipe(buildHeaderFile());
gulp.task('webui', function(cb) {
runSequence([
'webui_small',
'webui_sensor',
'webui_light',
'webui_rfbridge',
'webui_rfm69',
'webui_all'
], cb);
});
gulp.task('default', ['buildfs_embeded']);
gulp.task('default', ['webui']);

+ 180
- 63
code/html/custom.css View File

@ -50,10 +50,6 @@ h2 {
display: block;
}
.content {
margin: 0;
}
.page {
margin-top: 10px;
}
@ -98,16 +94,12 @@ div.center {
display: none;
}
#credentials {
font-size: 200%;
height: 100px;
left: 50%;
margin-left: -200px;
margin-top: -50px;
position: fixed;
text-align: center;
top: 50%;
width: 400px;
#password .content {
margin: 0 auto;
}
#layout .content {
margin: 0;
}
div.state {
@ -158,15 +150,22 @@ div.state {
.main-buttons button {
width: 100px;
}
.button-del-schedule {
margin-top: 15px;
}
.button-reboot,
.button-reconnect,
.button-ha-del,
.button-rfb-forget,
.button-del-network,
.button-del-mapping,
.button-del-schedule,
.button-dbg-clear,
.button-upgrade,
.button-clear-filters,
.button-clear-messages,
.button-clear-counts,
.button-settings-factory {
background: rgb(192, 0, 0); /* redish */
}
@ -174,8 +173,9 @@ div.state {
.button-update,
.button-update-password,
.button-add-network,
.button-rfb-learn,
.button-add-mapping,
.button-upgrade-browse,
.button-rfb-learn,
.button-ha-add,
.button-ha-config,
.button-settings-backup,
@ -198,7 +198,14 @@ div.state {
background: rgb(255, 128, 0); /* orange */
}
.button-generate-password {
background: rgb(66, 184, 221); /* blue */
}
.button-upgrade-browse,
.button-clear-filters,
.button-clear-messages,
.button-clear-counts,
.button-dbgcmd,
.button-ha-add,
.button-apikey,
@ -225,58 +232,98 @@ span.slider {
Checkboxes
-------------------------------------------------------------------------- */
.checkbox-container {
width: 130px;
height: 30px;
margin: 3px 0 10px 0px;
position: relative;
border-radius: 4px;
overflow: hidden;
user-select: none;
cursor: pointer;
}
.checkbox-container input {
display: none;
}
.inner-container {
position: absolute;
left: 0;
top: 0;
width: inherit;
height: inherit;
text-transform: uppercase;
font-size: .7em;
letter-spacing: .2em;
.toggleWrapper {
overflow: hidden;
width: auto;
height: 30px;
margin: 0px 0px 10px 0px;
padding: 0px;
border-radius: 4px;
box-shadow: inset 1px 1px #CCC;
}
.inner-container:first-child {
background: #e9e9e9;
color: #a9a9a9;
.toggleWrapper input {
position: absolute;
left: -99em;
}
.inner-container:nth-child(2) {
background: #c00000;
color: white;
clip-path: inset(0 50% 0 0);
transition: .3s cubic-bezier(0,0,0,1);
label[for].toggle {
margin: 0px;
padding: 0px;
}
.toggle {
width: 50%;
position: absolute;
height: inherit;
display: flex;
box-sizing: border-box;
}
.toggle p {
margin: auto;
}
.toggle:nth-child(1) {
right: 0;
letter-spacing:normal;
cursor: pointer;
display: inline-block;
position: relative;
width: 130px;
height: 100%;
background: #e9e9e9;
color: #a9a9a9;
border-radius: 4px;
-webkit-transition: all 200ms cubic-bezier(0.445, 0.05, 0.55, 0.95);
transition: all 200ms cubic-bezier(0.445, 0.05, 0.55, 0.95);
}
.toggle:before,
.toggle:after {
position: absolute;
line-height: 30px;
font-size: .7em;
z-index: 2;
-webkit-transition: all 200ms cubic-bezier(0.445, 0.05, 0.55, 0.95);
transition: all 200ms cubic-bezier(0.445, 0.05, 0.55, 0.95);
}
.toggle:before {
content: "NO";
left: 20px;
}
input[name="relay"] + .toggle:before {
content: "OFF";
}
.toggle:after{
content: "YES";
right: 20px;
}
input[name="relay"] + .toggle:after {
content: "ON";
}
.toggle__handler {
display: inline-block;
position: relative;
z-index: 1;
background: #c00000;
width: 50%;
height: 100%;
border-radius: 4px;
border-top-right-radius: 0px;
border-bottom-right-radius: 0px;
top: 0px;
left: 0px;
-webkit-transition: all 200ms cubic-bezier(0.445, 0.05, 0.55, 0.95);
transition: all 200ms cubic-bezier(0.445, 0.05, 0.55, 0.95);
-webkit-transform: translateX(0px);
transform: translateX(0px);
}
input:checked + .toggle:after {
color: #fff;
}
input:checked + .toggle:before {
color: #a9a9a9;
}
input + .toggle:before {
color: #fff;
}
input:checked + .toggle .toggle__handler {
width: 50%;
background: #00c000;
-webkit-transform: translateX(65px);
transform: translateX(65px);
border-color: #000;
border-top-left-radius: 0px;
border-bottom-left-radius: 0px;
border-top-right-radius: 4px;
border-bottom-right-radius: 4px;
}
input[disabled] + .toggle .toggle__handler {
background: #ccc;
}
/* -----------------------------------------------------------------------------
@ -369,6 +416,26 @@ span.slider {
padding: 10px;
}
/* -----------------------------------------------------------------------------
Table
-------------------------------------------------------------------------- */
.right {
text-align: right;
}
table.dataTable.display tbody td {
text-align: center;
}
#packets_filter {
display: none;
}
.filtered {
color: rgb(202, 60, 60);
}
/* -----------------------------------------------------------------------------
Logs
-------------------------------------------------------------------------- */
@ -377,3 +444,53 @@ span.slider {
height: 400px;
margin-bottom: 10px;
}
/* -----------------------------------------------------------------------------
Password input controls
-------------------------------------------------------------------------- */
.password-reveal {
font-family: EmojiSymbols,Segoe UI Symbol;
background: rgba(0,0,0,0);
display: inline-block;
float: right;
z-index: 50;
margin-top: 6px;
margin-left: -30px;
vertical-align: middle;
font-size: 1.2em;
height: 100%;
}
.password-reveal:after {
content: "👁";
}
input[type="password"] + .password-reveal {
color: rgba(205, 205, 205, 0.3);
}
input[type="text"] + .password-reveal {
color: rgba(66, 184, 221, 0.8);
}
.no-select {
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}
input::-ms-clear,
input::-ms-reveal {
display: none;
}
/* css minifier must not combine these.
* style will not apply otherwise */
input::-ms-input-placeholder {
color: #ccd;
}
input::placeholder {
color: #ccc;
}

+ 325
- 134
code/html/custom.js View File

@ -18,6 +18,11 @@ var useCCT = false;
var now = 0;
var ago = 0;
<!-- removeIf(!rfm69)-->
var packets;
var filters = [];
<!-- endRemoveIf(!rfm69)-->
// -----------------------------------------------------------------------------
// Messages
// -----------------------------------------------------------------------------
@ -41,7 +46,8 @@ function sensorName(id) {
"HLW8012", "V9261F", "ECH1560", "Analog", "Digital",
"Events", "PMSX003", "BMX280", "MHZ19", "SI7021",
"SHT3X I2C", "BH1750", "PZEM004T", "AM2320 I2C", "GUVAS12SD",
"TMP3X", "HC-SR04", "SenseAir", "GeigerTicks", "GeigerCPM"
"TMP3X", "Sonar", "SenseAir", "GeigerTicks", "GeigerCPM",
"NTC", "SDS011", "MICS2710", "MICS5525"
];
if (1 <= id && id <= names.length) {
return names[id - 1];
@ -54,9 +60,11 @@ function magnitudeType(type) {
"Temperature", "Humidity", "Pressure",
"Current", "Voltage", "Active Power", "Apparent Power",
"Reactive Power", "Power Factor", "Energy", "Energy (delta)",
"Analog", "Digital", "Events",
"Analog", "Digital", "Event",
"PM1.0", "PM2.5", "PM10", "CO2", "Lux", "UV", "Distance" , "HCHO",
"Local Dose Rate", "Local Dose Rate"
"Local Dose Rate", "Local Dose Rate",
"Count",
"NO2", "CO", "Resistance"
];
if (1 <= type && type <= types.length) {
return types[type - 1];
@ -140,26 +148,53 @@ function loadTimeZones() {
}
function validateForm(form) {
function validatePassword(password) {
// http://www.the-art-of-web.com/javascript/validate-password/
// at least one lowercase and one uppercase letter or number
// at least five characters (letters, numbers or special characters)
var re_password = /^(?=.*[A-Z\d])(?=.*[a-z])[\w~!@#$%^&*\(\)<>,.\?;:{}\[\]\\|]{5,}$/;
// at least eight characters (letters, numbers or special characters)
// password
var adminPass1 = $("input[name='adminPass']", form).first().val();
if (adminPass1.length > 0 && !re_password.test(adminPass1)) {
alert("The password you have entered is not valid, it must have at least 5 characters, 1 lowercase and 1 uppercase or number!");
return false;
// MUST be 8..63 printable ASCII characters. See:
// https://en.wikipedia.org/wiki/Wi-Fi_Protected_Access#Target_users_(authentication_key_distribution)
// https://github.com/xoseperez/espurna/issues/1151
var re_password = /^(?=.*[A-Z\d])(?=.*[a-z])[\w~!@#$%^&*\(\)<>,.\?;:{}\[\]\\|]{8,63}$/;
return (
(password !== undefined)
&& (typeof password === "string")
&& (password.length > 0)
&& re_password.test(password)
);
}
function validateFormPasswords(form) {
var passwords = $("input[name='adminPass1'],input[name='adminPass2']", form);
var adminPass1 = passwords.first().val(),
adminPass2 = passwords.last().val();
var formValidity = passwords.first()[0].checkValidity();
if (formValidity && (adminPass1.length === 0) && (adminPass2.length === 0)) {
return true;
}
var validPass1 = validatePassword(adminPass1),
validPass2 = validatePassword(adminPass2);
if (formValidity && validPass1 && validPass2) {
return true;
}
if (!formValidity || (adminPass1.length > 0 && !validPass1)) {
alert("The password you have entered is not valid, it must be 8..63 characters and have at least 1 lowercase and 1 uppercase / number!");
}
var adminPass2 = $("input[name='adminPass']", form).last().val();
if (adminPass1 !== adminPass2) {
alert("Passwords are different!");
return false;
}
return false;
}
function validateFormHostname(form) {
// RFCs mandate that a hostname's labels may contain only
// the ASCII letters 'a' through 'z' (case-insensitive),
// the digits '0' through '9', and the hyphen.
@ -167,18 +202,26 @@ function validateForm(form) {
// No other symbols, punctuation characters, or blank spaces are permitted.
// Negative lookbehind does not work in Javascript
// var re_hostname = new RegExp('^(?!-)[A-Za-z0-9-]{1,32}(?<!-)$');
// var re_hostname = new RegExp('^(?!-)[A-Za-z0-9-]{1,31}(?<!-)$');
var re_hostname = new RegExp('^(?!-)[A-Za-z0-9-]{0,31}[A-Za-z0-9]$');
var re_hostname = new RegExp('^(?!-)[A-Za-z0-9-]{0,30}[A-Za-z0-9]$');
var hostname = $("input[name='hostname']", form).val();
if (!re_hostname.test(hostname)) {
alert("Hostname cannot be empty and may only contain the ASCII letters ('A' through 'Z' and 'a' through 'z'), the digits '0' through '9', and the hyphen ('-')! They can neither start or end with an hyphen.");
return false;
var hostname = $("input[name='hostname']", form);
if ("true" !== hostname.attr("hasChanged")) {
return true;
}
if (re_hostname.test(hostname.val())) {
return true;
}
return true;
alert("Hostname cannot be empty and may only contain the ASCII letters ('A' through 'Z' and 'a' through 'z'), the digits '0' through '9', and the hyphen ('-')! They can neither start or end with an hyphen.");
return false;
}
function validateForm(form) {
return validateFormPasswords(form) && validateFormHostname(form);
}
function getValue(element) {
@ -206,9 +249,16 @@ function addValue(data, name, value) {
"dczRelayIdx", "dczMagnitude",
"tspkRelay", "tspkMagnitude",
"ledMode",
"adminPass"
"adminPass",
"node", "key", "topic"
];
// join both adminPass 1 and 2
if (name.startsWith("adminPass")) {
name = "adminPass";
}
if (name in data) {
if (!Array.isArray(data[name])) {
data[name] = [data[name]];
@ -244,26 +294,67 @@ function getData(form) {
}
function randomString(length, chars) {
var mask = "";
if (chars.indexOf("a") > -1) { mask += "abcdefghijklmnopqrstuvwxyz"; }
if (chars.indexOf("A") > -1) { mask += "ABCDEFGHIJKLMNOPQRSTUVWXYZ"; }
if (chars.indexOf("#") > -1) { mask += "0123456789"; }
if (chars.indexOf("@") > -1) { mask += "ABCDEF"; }
if (chars.indexOf("!") > -1) { mask += "~`!@#$%^&*()_+-={}[]:\";'<>?,./|\\"; }
var result = "";
for (var i = length; i > 0; --i) {
result += mask[Math.round(Math.random() * (mask.length - 1))];
function randomString(length, args) {
if (typeof args === "undefined") {
args = {
lowercase: true,
uppercase: true,
numbers: true,
special: true
}
}
return result;
var mask = "";
if (args.lowercase) { mask += "abcdefghijklmnopqrstuvwxyz"; }
if (args.uppercase) { mask += "ABCDEFGHIJKLMNOPQRSTUVWXYZ"; }
if (args.numbers || args.hex) { mask += "0123456789"; }
if (args.hex) { mask += "ABCDEF"; }
if (args.special) { mask += "~`!@#$%^&*()_+-={}[]:\";'<>?,./|\\"; }
var source = new Uint32Array(length);
var result = new Array(length);
window.crypto.getRandomValues(source).forEach(function(value, i) {
result[i] = mask[value % mask.length];
});
return result.join("");
}
function generateAPIKey() {
var apikey = randomString(16, "@#");
var apikey = randomString(16, {hex: true});
$("input[name='apiKey']").val(apikey);
return false;
}
function generatePassword() {
var password = "";
do {
password = randomString(10);
} while (!validatePassword(password));
return password;
}
function toggleVisiblePassword() {
var elem = this.previousElementSibling;
if (elem.type === "password") {
elem.type = "text";
} else {
elem.type = "password";
}
return false;
}
function doGeneratePassword() {
$("input", $("#formPassword"))
.val(generatePassword())
.each(function() {
this.type = "text";
});
return false;
}
function getJson(str) {
try {
return JSON.parse(str);
@ -284,10 +375,18 @@ function sendConfig(data) {
websock.send(JSON.stringify({config: data}));
}
function resetOriginals() {
function setOriginalsFromValues(force) {
var force = (true === force);
$("input,select").each(function() {
$(this).attr("original", $(this).val());
var initial = (undefined === $(this).attr("original"));
if (force || initial) {
$(this).attr("original", $(this).val());
}
});
}
function resetOriginals() {
setOriginalsFromValues(true);
numReboot = numReconnect = numReload = 0;
}
@ -398,7 +497,7 @@ function doUpgrade() {
function doUpdatePassword() {
var form = $("#formPassword");
if (validateForm(form)) {
if (validateFormPasswords(form)) {
sendConfig(getData(form));
}
return false;
@ -452,14 +551,15 @@ function doReconnect(ask) {
function doUpdate() {
var form = $("#formSave");
if (validateForm(form)) {
var forms = $(".form-settings");
if (validateForm(forms)) {
// Get data
sendConfig(getData(form));
sendConfig(getData(forms));
// Empty special fields
$(".pwrExpected").val(0);
$("input[name='snsResetCalibration']").prop("checked", false);
$("input[name='pwrResetCalibration']").prop("checked", false);
$("input[name='pwrResetE']").prop("checked", false);
@ -538,7 +638,7 @@ function doFactoryReset() {
if (response === false) {
return false;
}
websock.send(JSON.stringify({"action": "factory_reset"}));
sendAction("factory_reset", {});
doReload(5000);
return false;
}
@ -574,6 +674,56 @@ function doDebugClear() {
return false;
}
<!-- removeIf(!rfm69)-->
function doClearCounts() {
sendAction("clear-counts", {});
return false;
}
function doClearMessages() {
packets.clear().draw(false);
return false;
}
function doFilter(e) {
var index = packets.cell(this).index();
if (index == 'undefined') return;
var c = index.column;
var column = packets.column(c);
if (filters[c]) {
filters[c] = false;
column.search("");
$(column.header()).removeClass("filtered");
} else {
filters[c] = true;
var data = packets.row(this).data();
if (e.which == 1) {
column.search('^' + data[c] + '$', true, false );
} else {
column.search('^((?!(' + data[c] + ')).)*$', true, false );
}
$(column.header()).addClass("filtered");
}
column.draw();
return false;
}
function doClearFilters() {
for (var i = 0; i < packets.columns()[0].length; i++) {
if (filters[i]) {
filters[i] = false;
var column = packets.column(i);
column.search("");
$(column.header()).removeClass("filtered");
column.draw();
}
}
return false;
}
<!-- endRemoveIf(!rfm69)-->
// -----------------------------------------------------------------------------
// Visualization
// -----------------------------------------------------------------------------
@ -628,6 +778,28 @@ function createMagnitudeList(data, container, template_name) {
}
<!-- endRemoveIf(!sensor)-->
// -----------------------------------------------------------------------------
// RFM69
// -----------------------------------------------------------------------------
<!-- removeIf(!rfm69)-->
function addMapping() {
var template = $("#nodeTemplate .pure-g")[0];
var line = $(template).clone();
var tabindex = $("#mapping > div").length * 3 + 50;
$(line).find("input").each(function() {
$(this).attr("tabindex", tabindex++);
});
$(line).find("button").on('click', delMapping);
line.appendTo("#mapping");
}
function delMapping() {
var parent = $(this).parent().parent();
$(parent).remove();
}
<!-- endRemoveIf(!rfm69)-->
// -----------------------------------------------------------------------------
// Wifi
// -----------------------------------------------------------------------------
@ -657,6 +829,7 @@ function addNetwork() {
$(this).attr("tabindex", tabindex);
tabindex++;
});
$(".password-reveal", line).on("click", toggleVisiblePassword);
$(line).find(".button-del-network").on("click", delNetwork);
$(line).find(".button-more-network").on("click", moreNetwork);
line.appendTo("#networks");
@ -702,11 +875,13 @@ function addSchedule(event) {
});
$(line).find(".button-del-schedule").on("click", delSchedule);
$(line).find(".button-more-schedule").on("click", moreSchedule);
$(line).find("input[name='schUTC']").prop("id", "schUTC" + (numSchedules + 1))
.next().prop("for", "schUTC" + (numSchedules + 1));
$(line).find("input[name='schEnabled']").prop("id", "schEnabled" + (numSchedules + 1))
.next().prop("for", "schEnabled" + (numSchedules + 1));
line.appendTo("#schedules");
$(line).find("input[type='checkbox']").prop("checked", false);
initCheckboxes();
return line;
}
@ -726,7 +901,14 @@ function initRelays(data) {
// Add relay fields
var line = $(template).clone();
$(".id", line).html(i);
$(":checkbox", line).prop('checked', data[i]).attr("data", i);
$(":checkbox", line).prop('checked', data[i]).attr("data", i)
.prop("id", "relay" + i)
.on("change", function (event) {
var id = parseInt($(event.target).attr("data"), 10);
var status = $(event.target).prop("checked");
doToggle(id, status);
});
$("label.toggle", line).prop("for", "relay" + i)
line.appendTo("#relays");
// Populate the relay SELECTs
@ -737,55 +919,13 @@ function initRelays(data) {
}
function initCheckboxes() {
var setCheckbox = function(element, value) {
var container = $(".toggle-container", $(element));
if (value) {
container.css("clipPath", "inset(0 0 0 50%)");
container.css("backgroundColor", "#00c000");
} else {
container.css("clipPath", "inset(0 50% 0 0)");
container.css("backgroundColor", "#c00000");
}
}
$(".checkbox-container")
.each(function() {
var status = $(this).next().prop('checked');
setCheckbox(this, status);
})
.off('click')
.on('click', function() {
var checkbox = $(this).next();
var status = checkbox.prop('checked');
status = !status;
checkbox.prop('checked', status);
setCheckbox(this, status);
if ("relay" == checkbox.attr('name')) {
var id = parseInt(checkbox.attr('data'), 10);
doToggle(id, status);
}
});
}
function createCheckboxes() {
$("input[type='checkbox']").each(function() {
var text_on = $(this).attr("on") || "YES";
var text_off = $(this).attr("off") || "NO";
var toggles = "<div class=\"toggle\"><p>" + text_on + "</p></div><div class=\"toggle\"><p>" + text_off + "</p></div>";
var content = "<div class=\"checkbox-container\"><div class=\"inner-container\">" + toggles
+ "</div><div class=\"inner-container toggle-container\">" + toggles + "</div></div>";
$(this).before(content).hide();
if($(this).prop("name"))$(this).prop("id", $(this).prop("name"));
$(this).parent().addClass("toggleWrapper");
$(this).after('<label for="' + $(this).prop("name") + '" class="toggle"><span class="toggle__handler"></span></label>')
});
@ -844,31 +984,29 @@ function initMagnitudes(data) {
<!-- removeIf(!light)-->
function initColorRGB() {
function initColor(rgb) {
// check if already initialized
var done = $("#colors > div").length;
if (done > 0) { return; }
// add template
var template = $("#colorRGBTemplate").children();
var template = $("#colorTemplate").children();
var line = $(template).clone();
line.appendTo("#colors");
// init color wheel
$("input[name='color']").wheelColorPicker({
sliders: "wrgbp"
sliders: (rgb ? "wrgbp" : "whsvp")
}).on("sliderup", function() {
var value = $(this).wheelColorPicker("getValue", "css");
sendAction("color", {rgb: value});
});
// init bright slider
$("#brightness").on("change", function() {
var value = $(this).val();
var parent = $(this).parents(".pure-g");
$("span", parent).html(value);
sendAction("color", {brightness: value});
if (rgb) {
var value = $(this).wheelColorPicker("getValue", "css");
sendAction("color", {rgb: value});
} else {
var color = $(this).wheelColorPicker("getColor");
var value = parseInt(color.h * 360, 10) + "," + parseInt(color.s * 100, 10) + "," + parseInt(color.v * 100, 10);
sendAction("color", {hsv: value});
}
});
}
@ -889,28 +1027,6 @@ function initCCT() {
});
}
function initColorHSV() {
// check if already initialized
var done = $("#colors > div").length;
if (done > 0) { return; }
// add template
var template = $("#colorHSVTemplate").children();
var line = $(template).clone();
line.appendTo("#colors");
// init color wheel
$("input[name='color']").wheelColorPicker({
sliders: "whsvp"
}).on("sliderup", function() {
var color = $(this).wheelColorPicker("getColor");
var value = parseInt(color.h * 360, 10) + "," + parseInt(color.s * 100, 10) + "," + parseInt(color.v * 100, 10);
sendAction("color", {hsv: value});
});
}
function initChannels(num) {
// check if already initialized
@ -941,7 +1057,7 @@ function initChannels(num) {
sendAction("channel", {id: id, value: value});
};
// add templates
// add channel templates
var i = 0;
var template = $("#channelTemplate").children();
for (i=0; i<max; i++) {
@ -956,11 +1072,25 @@ function initChannels(num) {
}
// Init channel dropdowns
for (i=0; i<num; i++) {
$("select.islight").append(
$("<option></option>").attr("value",i).text("Channel #" + i));
}
// add brightness template
var template = $("#brightnessTemplate").children();
var line = $(template).clone();
line.appendTo("#channels");
// init bright slider
$("#brightness").on("change", function() {
var value = $(this).val();
var parent = $(this).parents(".pure-g");
$("span", parent).html(value);
sendAction("brightness", {value: value});
});
}
<!-- endRemoveIf(!light)-->
@ -1078,6 +1208,50 @@ function processData(data) {
}
<!-- endRemoveIf(!rfbridge)-->
// ---------------------------------------------------------------------
// RFM69
// ---------------------------------------------------------------------
<!-- removeIf(!rfm69)-->
if (key == "packet") {
var packet = data.packet;
var d = new Date();
packets.row.add([
d.toLocaleTimeString('en-US', { hour12: false }),
packet.senderID,
packet.packetID,
packet.targetID,
packet.key,
packet.value,
packet.rssi,
packet.duplicates,
packet.missing,
]).draw(false);
return;
}
if (key == "mapping") {
for (var i in data.mapping) {
// add a new row
addMapping();
// get group
var line = $("#mapping .pure-g")[i];
// fill in the blanks
var mapping = data.mapping[i];
Object.keys(mapping).forEach(function(key) {
var id = "input[name=" + key + "]";
if ($(id, line).length) $(id, line).val(mapping[key]).attr("original", mapping[key]);
});
}
return;
}
<!-- endRemoveIf(!rfm69)-->
// ---------------------------------------------------------------------
// Lights
// ---------------------------------------------------------------------
@ -1085,13 +1259,13 @@ function processData(data) {
<!-- removeIf(!light)-->
if ("rgb" === key) {
initColorRGB();
initColor(true);
$("input[name='color']").wheelColorPicker("setValue", value, true);
return;
}
if ("hsv" === key) {
initColorHSV();
initColor(false);
// wheelColorPicker expects HSV to be between 0 and 1 all of them
var chunks = value.split(",");
var obj = {};
@ -1369,8 +1543,7 @@ function processData(data) {
generateAPIKey();
}
resetOriginals();
initCheckboxes();
setOriginalsFromValues();
}
@ -1384,27 +1557,27 @@ function hasChanged() {
newValue = $(this).val();
originalValue = $(this).attr("original");
}
var hasChanged = $(this).attr("hasChanged") || 0;
var hasChanged = ("true" === $(this).attr("hasChanged"));
var action = $(this).attr("action");
if (typeof originalValue === "undefined") { return; }
if ("none" === action) { return; }
if (newValue !== originalValue) {
if (0 === hasChanged) {
if (!hasChanged) {
++numChanged;
if ("reconnect" === action) { ++numReconnect; }
if ("reboot" === action) { ++numReboot; }
if ("reload" === action) { ++numReload; }
$(this).attr("hasChanged", 1);
$(this).attr("hasChanged", true);
}
} else {
if (1 === hasChanged) {
if (hasChanged) {
--numChanged;
if ("reconnect" === action) { --numReconnect; }
if ("reboot" === action) { --numReboot; }
if ("reload" === action) { --numReload; }
$(this).attr("hasChanged", 0);
$(this).attr("hasChanged", false);
}
}
@ -1479,12 +1652,15 @@ $(function() {
createCheckboxes();
setInterval(function() { keepTime(); }, 1000);
$(".password-reveal").on("click", toggleVisiblePassword);
$("#menuLink").on("click", toggleMenu);
$(".pure-menu-link").on("click", showPanel);
$("progress").attr({ value: 0, max: 100 });
$(".button-update").on("click", doUpdate);
$(".button-update-password").on("click", doUpdatePassword);
$(".button-generate-password").on("click", doGeneratePassword);
$(".button-reboot").on("click", doReboot);
$(".button-reconnect").on("click", doReconnect);
$(".button-wifi-scan").on("click", doScan);
@ -1515,6 +1691,21 @@ $(function() {
$(".button-add-light-schedule").on("click", { schType: 2 }, addSchedule);
<!-- endRemoveIf(!light)-->
<!-- removeIf(!rfm69)-->
$(".button-add-mapping").on('click', addMapping);
$(".button-del-mapping").on('click', delMapping);
$(".button-clear-counts").on('click', doClearCounts);
$(".button-clear-messages").on('click', doClearMessages);
$(".button-clear-filters").on('click', doClearFilters);
$('#packets tbody').on('mousedown', 'td', doFilter);
packets = $('#packets').DataTable({
"paging": false
});
for (var i = 0; i < packets.columns()[0].length; i++) {
filters[i] = false;
}
<!-- endRemoveIf(!rfm69)-->
$(document).on("change", "input", hasChanged);
$(document).on("change", "select", hasChanged);


+ 293
- 108
code/html/index.html View File

@ -11,9 +11,12 @@
<link rel="stylesheet" href="vendor/pure-1.0.0.min.css" />
<link rel="stylesheet" href="vendor/pure-grids-responsive-1.0.0.min.css" />
<link rel="stylesheet" href="vendor/side-menu.css" />
<!-- removeIf(!light)-->
<!-- removeIf(!light) -->
<link rel="stylesheet" href="vendor/jquery.wheelcolorpicker-3.0.3.css" />
<!-- endRemoveIf(!light)-->
<!-- endRemoveIf(!light) -->
<!-- removeIf(!rfm69) -->
<link rel="stylesheet" href="vendor/datatables-1.10.16.css" />
<!-- endRemoveIf(!rfm69) -->
<link rel="stylesheet" href="custom.css" />
<!-- endbuild -->
@ -25,40 +28,45 @@
<div class="content">
<form id="formPassword" class="pure-form" action="/" method="post">
<form id="formPassword" class="pure-form" autocomplete="off">
<div class="panel block">
<div class="panel block" id="panel-password">
<div class="header">
<h1>SECURITY</h1>
<h2>Before using this device you have to change the default password for the user 'admin'. This password will be used for the <strong>AP mode hotspot</strong>, the <strong>web interface</strong> (where you are now) and the <strong>over-the-air updates</strong>.</h2>
<h2>Before using this device you have to change the default password for the user <strong>admin</strong>. This password will be used for the <strong>AP mode hotspot</strong>, the <strong>web interface</strong> (where you are now) and the <strong>over-the-air updates</strong>.</h2>
</div>
<div class="page">
<fieldset>
<div class="pure-g">
<label class="pure-u-1 pure-u-lg-1-4">Admin password</label>
<input name="adminPass" class="pure-u-1 pure-u-lg-3-4" type="password" tabindex="1" autocomplete="false" />
<div class="pure-u-0 pure-u-lg-1-4"></div>
<div class="pure-u-1 pure-u-lg-3-4 hint">
The administrator password is used to access this web interface (user 'admin'), but also to connect to the device when in AP mode or to flash a new firmware over-the-air (OTA).<br />
It must have at least <strong>five characters</strong> (numbers and letters and any of these special characters: _,.;:~!?@#$%^&amp;*&lt;&gt;\|(){}[]) and at least <strong>one lowercase</strong> and <strong>one uppercase</strong> or <strong>one number</strong>.</div>
<label class="pure-u-1 pure-u-lg-1-4" for="adminPass1">New Password</label>
<input class="pure-u-1 pure-u-lg-3-4" name="adminPass1" minlength="8" maxlength="63" type="password" tabindex="1" autocomplete="false" spellcheck="false" required />
<span class="no-select password-reveal"></span>
</div>
<div class="pure-g">
<label class="pure-u-1 pure-u-lg-1-4">Repeat password</label>
<input name="adminPass" class="pure-u-1 pure-u-lg-3-4" type="password" tabindex="2" autocomplete="false" />
<label class="pure-u-1 pure-u-lg-1-4" for="adminPass2">Repeat password</label>
<input class="pure-u-1 pure-u-lg-3-4" name="adminPass2" minlength="8" maxlength="63" type="password" tabindex="2" autocomplete="false" spellcheck="false" required />
<span class="no-select password-reveal"></span>
</div>
</fieldset>
<div class="pure-u-0 pure-u-lg-1-4 more"></div>
<button class="pure-button button-update-password" type="button">Update</button>
<div class="pure-g">
<div class="pure-u-1 pure-u-lg-1 hint">
Password must be <strong>8..63 characters</strong> (numbers and letters and any of these special characters: _,.;:~!?@#$%^&amp;*&lt;&gt;\|(){}[]) and have at least <strong>one lowercase</strong> and <strong>one uppercase</strong> or <strong>one number</strong>.</div>
</div>
<div class="pure-g">
<button class="pure-u-11-24 pure-u-lg-1-4 pure-button button-generate-password" type="button" title="Generate password based on password policy">Generate</button>
<div class="pure-u-2-24 pure-u-lg-1-2"></div>
<button class="pure-u-11-24 pure-u-lg-1-4 pure-button button-update-password" type="button" title="Save new password">Save</button>
</div>
</fieldset>
</div>
</div>
</form>
</div> <!-- content -->
@ -99,11 +107,21 @@
<a href="#" class="pure-menu-link" data="panel-idb">INFLUXDB</a>
</li>
<!-- removeIf(!light)-->
<!-- removeIf(!light) -->
<li class="pure-menu-item module module-color">
<a href="#" class="pure-menu-link" data="panel-color">LIGHTS</a>
</li>
<!-- endRemoveIf(!light)-->
<!-- endRemoveIf(!light) -->
<!-- removeIf(!rfm69) -->
<li class="pure-menu-item module module-rfm69">
<a href="#" class="pure-menu-link" data="panel-mapping">MAPPING</a>
</li>
<li class="pure-menu-item module module-rfm69">
<a href="#" class="pure-menu-link" data="panel-messages">MESSAGES</a>
</li>
<!-- endRemoveIf(!rfm69) -->
<li class="pure-menu-item module module-mqtt">
<a href="#" class="pure-menu-link" data="panel-mqtt">MQTT</a>
@ -113,21 +131,21 @@
<a href="#" class="pure-menu-link" data="panel-ntp">NTP</a>
</li>
<!-- removeIf(!rfbridge)-->
<!-- removeIf(!rfbridge) -->
<li class="pure-menu-item module module-rfb">
<a href="#" class="pure-menu-link" data="panel-rfb">RF</a>
</li>
<!-- endRemoveIf(!rfbridge)-->
<!-- endRemoveIf(!rfbridge) -->
<li class="pure-menu-item module module-sch">
<a href="#" class="pure-menu-link" data="panel-schedule">SCHEDULE</a>
</li>
<!-- removeIf(!sensor)-->
<li class="pure-menu-item module module-sensors">
<a href="#" class="pure-menu-link" data="panel-sensors">SENSORS</a>
<!-- removeIf(!sensor) -->
<li class="pure-menu-item module module-sns">
<a href="#" class="pure-menu-link" data="panel-sns">SENSORS</a>
</li>
<!-- endRemoveIf(!sensor)-->
<!-- endRemoveIf(!sensor) -->
<li class="pure-menu-item module module-relay">
<a href="#" class="pure-menu-link" data="panel-relay">SWITCHES</a>
@ -160,6 +178,7 @@
<div class="footer">
&copy; 2016-2018<br />
Xose Pérez<br/>
<a href="https://twitter.com/xoseperez" target="_blank">@xoseperez</a><br/>
<a href="http://tinkerman.cat" target="_blank">http://tinkerman.cat</a><br/>
<a href="https://github.com/xoseperez/espurna" target="_blank">ESPurna @ GitHub</a><br/>
GPLv3 license<br/>
@ -184,15 +203,27 @@
<div id="relays"></div>
<!-- removeIf(!light)-->
<!-- removeIf(!light) -->
<div id="colors"></div>
<div id="cct"></div>
<div id="channels"></div>
<!-- endRemoveIf(!light)-->
<!-- endRemoveIf(!light) -->
<!-- removeIf(!sensor)-->
<!-- removeIf(!sensor) -->
<div id="magnitudes"></div>
<!-- endRemoveIf(!sensor)-->
<!-- endRemoveIf(!sensor) -->
<!-- removeIf(!rfm69) -->
<div class="pure-g module module-rfm69">
<label class="pure-u-1 pure-u-lg-1-4">Packet count</label>
<div class="pure-u-1 pure-u-lg-1-4"><input class="pure-u-1" type="text" name="packetCount" readonly /></div>
</div>
<div class="pure-g module module-rfm69">
<label class="pure-u-1 pure-u-lg-1-4">Node count</label>
<div class="pure-u-1 pure-u-lg-1-4"><input class="pure-u-1" type="text" name="nodeCount" readonly /></div>
</div>
<!-- endRemoveIf(!rfm69) -->
<div class="pure-u-1 pure-u-lg-1-2 state">
@ -220,10 +251,8 @@
<div class="pure-u-1-2">Firmware version</div>
<div class="pure-u-11-24"><span class="right" name="app_version"></span></div>
<!--
<div class="pure-u-1-2">Firmware revision</div>
<div class="pure-u-11-24"><span class="right" name="app_revision"></span></div>
-->
<div class="pure-u-1-2">Firmware build date</div>
<div class="pure-u-11-24"><span class="right" name="app_build"></span></div>
@ -285,8 +314,7 @@
</div>
</div>
<form id="formSave" class="pure-form" action="/" method="post" enctype="multipart/form-data">
<form id="form-general" class="pure-form form-settings">
<div class="panel" id="panel-general">
<div class="header">
@ -300,7 +328,7 @@
<div class="pure-g">
<label class="pure-u-1 pure-u-lg-1-4">Hostname</label>
<input name="hostname" class="pure-u-1 pure-u-lg-1-4" maxlength="32" type="text" action="reboot" tabindex="1" />
<input name="hostname" class="pure-u-1 pure-u-lg-1-4" maxlength="31" type="text" action="reboot" tabindex="1" />
<div class="pure-u-0 pure-u-lg-1-2"></div>
<div class="pure-u-0 pure-u-lg-1-4"></div>
<div class="pure-u-1 pure-u-lg-3-4 hint">
@ -358,7 +386,9 @@
</fieldset>
</div>
</div>
</form>
<form id="form-relay" class="pure-form form-settings">
<div class="panel" id="panel-relay">
<div class="header">
@ -390,8 +420,10 @@
</div>
</div>
</form>
<!-- removeIf(!light)-->
<!-- removeIf(!light) -->
<form id="form-color" class="pure-form form-settings">
<div class="panel" id="panel-color">
<div class="header">
@ -477,8 +509,10 @@
</fieldset>
</div>
</div>
<!-- endRemoveIf(!light)-->
</form>
<!-- endRemoveIf(!light) -->
<form id="form-admin" class="pure-form form-settings">
<div class="panel" id="panel-admin">
<div class="header">
@ -492,23 +526,23 @@
<div class="pure-g">
<label class="pure-u-1 pure-u-lg-1-4">Settings</label>
<div class="pure-u-1-3 pure-u-lg-1-4"><button class="pure-button button-settings-backup pure-u-23-24">Backup</button></div>
<div class="pure-u-1-3 pure-u-lg-1-4"><button class="pure-button button-settings-restore pure-u-23-24">Restore</button></div>
<div class="pure-u-1-3 pure-u-lg-1-4"><button class="pure-button button-settings-factory pure-u-1">Factory Reset</button></div>
<div class="pure-u-1-3 pure-u-lg-1-4"><button type="button" class="pure-button button-settings-backup pure-u-23-24">Backup</button></div>
<div class="pure-u-1-3 pure-u-lg-1-4"><button type="button" class="pure-button button-settings-restore pure-u-23-24">Restore</button></div>
<div class="pure-u-1-3 pure-u-lg-1-4"><button type="button" class="pure-button button-settings-factory pure-u-1">Factory Reset</button></div>
</div>
<div class="pure-g">
<label class="pure-u-1 pure-u-lg-1-4">Admin password</label>
<input name="adminPass" class="pure-u-1 pure-u-lg-3-4" type="password" action="reboot" tabindex="11" autocomplete="false" />
<input name="adminPass1" class="pure-u-1 pure-u-lg-3-4" placeholder="New password" minlength="8" maxlength="63" type="password" action="reboot" tabindex="11" autocomplete="false" spellcheck="false" />
<span class="no-select password-reveal"></span>
<div class="pure-u-1 pure-u-lg-1-4"></div>
<input name="adminPass2" class="pure-u-1 pure-u-lg-3-4" placeholder="Repeat password" minlength="8" maxlength="63" type="password" action="reboot" tabindex="12" autocomplete="false" spellcheck="false" />
<span class="no-select password-reveal"></span>
<div class="pure-u-0 pure-u-lg-1-4"></div>
<div class="pure-u-1 pure-u-lg-3-4 hint">
The administrator password is used to access this web interface (user 'admin'), but also to connect to the device when in AP mode or to flash a new firmware over-the-air (OTA).<br />
It must have at least <strong>five characters</strong> (numbers and letters and any of these special characters: _,.;:~!?@#$%^&amp;*&lt;&gt;\|(){}[]) and at least <strong>one lowercase</strong> and <strong>one uppercase</strong> or <strong>one number</strong>.</div>
</div>
<div class="pure-g">
<label class="pure-u-1 pure-u-lg-1-4">Repeat password</label>
<input name="adminPass" class="pure-u-1 pure-u-lg-3-4" type="password" action="reboot" tabindex="12" autocomplete="false" />
It must be <strong>8..63 characters</strong> (numbers and letters and any of these special characters: _,.;:~!?@#$%^&amp;*&lt;&gt;\|(){}[]) and have at least <strong>one lowercase</strong> and <strong>one uppercase</strong> or <strong>one number</strong>.</div>
</div>
<div class="pure-g">
@ -527,12 +561,23 @@
<div class="pure-u-1 pure-u-lg-1-4"><input type="checkbox" name="wsAuth" /></div>
</div>
<div class="pure-g">
<div class="pure-g module module-api">
<label class="pure-u-1 pure-u-lg-1-4">Enable HTTP API</label>
<div class="pure-u-1 pure-u-lg-1-4"><input type="checkbox" name="apiEnabled" /></div>
</div>
<div class="pure-g">
<div class="pure-g module module-api">
<label class="pure-u-1 pure-u-lg-1-4">Restful API</label>
<div class="pure-u-1 pure-u-lg-1-4"><input type="checkbox" name="apiRestFul" /></div>
<div class="pure-u-0 pure-u-lg-1-2"></div>
<div class="pure-u-0 pure-u-lg-1-4"></div>
<div class="pure-u-1 pure-u-lg-3-4 hint">
If enabled, API requests to change a status (like a relay) must be done using PUT.
If disabled you can issue them as GET requests (easier from a browser).
</div>
</div>
<div class="pure-g module module-api">
<label class="pure-u-1 pure-u-lg-1-4">Real time API</label>
<div class="pure-u-1 pure-u-lg-1-4"><input type="checkbox" name="apiRealTime" /></div>
<div class="pure-u-0 pure-u-lg-1-2"></div>
@ -543,10 +588,10 @@
</div>
</div>
<div class="pure-g">
<div class="pure-g module module-api">
<label class="pure-u-1 pure-u-lg-1-4">HTTP API Key</label>
<input name="apiKey" class="pure-u-3-4 pure-u-lg-1-2" type="text" tabindex="14" />
<div class=" pure-u-1-4 pure-u-lg-1-4"><button class="pure-button button-apikey pure-u-23-24">Auto</button></div>
<div class="pure-u-1-4 pure-u-lg-1-4"><button type="button" class="pure-button button-apikey pure-u-23-24">Auto</button></div>
<div class="pure-u-0 pure-u-lg-1-4"></div>
<div class="pure-u-1 pure-u-lg-3-4 hint">
This is the key you will have to pass with every HTTP request to the API, either to get or write values.
@ -579,8 +624,8 @@
<div class="pure-g">
<label class="pure-u-1 pure-u-lg-1-4">Upgrade</label>
<input class="pure-u-1-2 pure-u-lg-1-2" name="filename" type="text" readonly />
<div class=" pure-u-1-4 pure-u-lg-1-8"><button class="pure-button button-upgrade-browse pure-u-23-24">Browse</button></div>
<div class=" pure-u-1-4 pure-u-lg-1-8"><button class="pure-button button-upgrade pure-u-23-24">Upgrade</button></div>
<div class="pure-u-1-4 pure-u-lg-1-8"><button type="button" class="pure-button button-upgrade-browse pure-u-23-24">Browse</button></div>
<div class="pure-u-1-4 pure-u-lg-1-8"><button type="button" class="pure-button button-upgrade pure-u-23-24">Upgrade</button></div>
<div class="pure-u-0 pure-u-lg-1-4"></div>
<div class="pure-u-1 pure-u-lg-3-4 hint">The device has <span name="free_size"></span> bytes available for OTA updates. If your image is larger than this consider doing a <a href="https://github.com/xoseperez/espurna/wiki/TwoStepUpdates" target="_blank"><strong>two-step update</strong></a>.</div>
<div class="pure-u-0 pure-u-lg-1-4"></div>
@ -591,7 +636,9 @@
</fieldset>
</div>
</div>
</form>
<form id="form-wifi" class="pure-form form-settings">
<div class="panel" id="panel-wifi">
<div class="header">
@ -614,7 +661,6 @@
ESPurna will scan for visible WiFi SSIDs and try to connect to networks defined below in order of <strong>signal strength</strong>, even if multiple AP share the same SSID.
When disabled, ESPurna will try to connect to the networks in the same order they are listed below.
Disable this option if you are <strong>connecting to a single access point</strong> (or router) or to a <strong>hidden SSID</strong>.
</thead>
</div>
<div class="pure-u-0 pure-u-lg-1-4"></div>
<button class="pure-button button-wifi-scan" type="button">Scan now</button>
@ -634,7 +680,9 @@
</fieldset>
</div>
</div>
</form>
<form id="form-schedule" class="pure-form form-settings">
<div class="panel" id="panel-schedule">
<div class="header">
@ -649,16 +697,95 @@
<div id="schedules"></div>
<button type="button" class="pure-button button-add-switch-schedule module module-relay">Add switch schedule</button>
<!-- removeIf(!light)-->
<!-- removeIf(!light) -->
<button type="button" class="pure-button button-add-light-schedule module module-color">Add channel schedule</button>
<!-- endRemoveIf(!light)-->
<!-- endRemoveIf(!light) -->
</fieldset>
</div>
</div>
</form>
<!-- removeIf(!rfm69) -->
<form id="form-mapping" class="pure-form form-settings">
<div class="panel" id="panel-mapping">
<div class="header">
<h1>MAPPING</h1>
<h2>
Configure the map between nodeID/key and MQTT topic. Messages from the given nodeID with the given key will be forwarded to the specified topic.
You can also configure a default topic using {nodeid} and {key} as placeholders, if the default topic is empty messages without defined map will be discarded.
</h2>
</div>
<div class="page">
<fieldset>
<legend>Default topic</legend>
<div class="pure-g">
<input name="rfm69Topic" type="text" class="pure-u-23-24" value="" size="8" tabindex="41" placeholder="Default MQTT Topic (use {nodeid} and {key} as placeholders)">
</div>
<legend>Specific topics</legend>
<div id="mapping"></div>
<button type="button" class="pure-button button-add-mapping">Add</button>
</fieldset>
</div>
</div>
</form>
<form id="form-messages" class="pure-form">
<div class="panel" id="panel-messages">
<div class="header">
<h1>MESSAGES</h1>
<h2>
Messages being received. Previous messages are not displayed.
You have to keep the page open in order to keep receiving them.
You can filter/unfilter by clicking on the values.
Left click on a value to show only rows that match that value, middle click to show all rows but those matching that value.
Filtered colums have red headers.
</h2>
</div>
<div class="page">
<table id="packets" class="display" cellspacing="0">
<thead>
<tr>
<th>Timestamp</th>
<th>SenderID</th>
<th>PacketID</th>
<th>TargetID</th>
<th>Key</th>
<th>Value</th>
<th>RSSI</th>
<th>Duplicates</th>
<th>Missing</th>
</tr>
</thead>
<tbody>
</tbody>
</table>
<button type="button" class="pure-button button-clear-filters">Clear filters</button>
<button type="button" class="pure-button button-clear-messages">Clear messages</button>
<button type="button" class="pure-button button-clear-counts">Clear counts</button>
</div>
</div>
</form>
<!-- endRemoveIf(!rfm69) -->
<form id="form-mqtt" class="pure-form form-settings">
<div class="panel" id="panel-mqtt">
<div class="header">
@ -687,12 +814,13 @@
<div class="pure-g">
<label class="pure-u-1 pure-u-lg-1-4">MQTT User</label>
<input class="pure-u-1 pure-u-lg-1-4" name="mqttUser" type="text" tabindex="23" placeholder="Leave blank if no user" autocomplete="false" />
<input class="pure-u-1 pure-u-lg-1-4" name="mqttUser" type="text" tabindex="23" placeholder="Leave blank if no user" autocomplete="off" />
</div>
<div class="pure-g">
<label class="pure-u-1 pure-u-lg-1-4">MQTT Password</label>
<input class="pure-u-1 pure-u-lg-1-4" name="mqttPassword" type="password" tabindex="24" placeholder="Leave blank if no pass" autocomplete="false" />
<input class="pure-u-1 pure-u-lg-1-4" name="mqttPassword" type="password" tabindex="24" placeholder="Leave blank if no pass" autocomplete="new-password" spellcheck="false" />
<span class="no-select password-reveal"></span>
</div>
<div class="pure-g">
@ -748,13 +876,13 @@
<div class="pure-u-1 pure-u-lg-3-4 hint">
This is the root topic for this device. The {hostname} and {mac} placeholders will be replaced by the device hostname and MAC address.<br />
- <strong>&lt;root&gt;/relay/#/set</strong> Send a 0 or a 1 as a payload to this topic to switch it on or off. You can also send a 2 to toggle its current state. Replace # with the switch ID (starting from 0). If the board has only one switch it will be 0.<br />
<!-- removeIf(!light)-->
<!-- removeIf(!light) -->
<span class="module module-color">- <strong>&lt;root&gt;/rgb/set</strong> Set the color using this topic, your can either send an "#RRGGBB" value or "RRR,GGG,BBB" (0-255 each).<br /></span>
<span class="module module-color">- <strong>&lt;root&gt;/hsv/set</strong> Set the color using hue (0-360), saturation (0-100) and value (0-100) values, comma separated.<br /></span>
<span class="module module-color">- <strong>&lt;root&gt;/brightness/set</strong> Set the brighness (0-255).<br /></span>
<span class="module module-color">- <strong>&lt;root&gt;/channel/#/set</strong> Set the value for a single color channel (0-255). Replace # with the channel ID (starting from 0 and up to 4 for RGBWC lights).<br /></span>
<span class="module module-color">- <strong>&lt;root&gt;/mired/set</strong> Set the temperature color in mired.<br /></span>
<!-- endRemoveIf(!light)-->
<!-- endRemoveIf(!light) -->
- <strong>&lt;root&gt;/status</strong> The device will report a 1 to this topic every few minutes. Upon MQTT disconnecting this will be set to 0.<br />
- Other values reported (depending on the build) are: <strong>firmware</strong> and <strong>version</strong>, <strong>hostname</strong>, <strong>IP</strong>, <strong>MAC</strong>, signal strenth (<strong>RSSI</strong>), <strong>uptime</strong> (in seconds), <strong>free heap</strong> and <strong>power supply</strong>.
</div>
@ -762,7 +890,8 @@
<div class="pure-g">
<label class="pure-u-1 pure-u-lg-1-4">Use JSON payload</label>
<div class="pure-u-1 pure-u-lg-3-4"><input type="checkbox" name="mqttUseJson" tabindex="32" /></div>
<div class="pure-u-1 pure-u-lg-1-4"><input type="checkbox" name="mqttUseJson" tabindex="32" /></div>
<div class="pure-u-1 pure-u-lg-1-2"></div>
<div class="pure-u-1 pure-u-lg-1-4"></div>
<div class="pure-u-1 pure-u-lg-3-4 hint">
All messages (except the device status) will be included in a JSON payload along with the timestamp and hostname
@ -776,7 +905,9 @@
</div>
</div>
</form>
<form id="form-ntp" class="pure-form form-settings">
<div class="panel" id="panel-ntp">
<div class="header">
@ -820,7 +951,9 @@
</div>
</div>
</form>
<form id="form-domoticz" class="pure-form form-settings">
<div class="panel" id="panel-domoticz">
<div class="header">
@ -859,15 +992,17 @@
<div id="dczRelays"></div>
<!-- removeIf(!sensor)-->
<!-- removeIf(!sensor) -->
<div id="dczMagnitudes"></div>
<!-- endRemoveIf(!sensor)-->
<!-- endRemoveIf(!sensor) -->
</fieldset>
</div>
</div>
</form>
<form id="form-ha" class="pure-form form-settings">
<div class="panel" id="panel-ha">
<div class="header">
@ -889,9 +1024,9 @@
<div class="pure-u-0 pure-u-lg-1-4"></div>
<div class="pure-u-1 pure-u-lg-3-4 hint">
Home Assistant auto-discovery feature. Enable and save to add the device to your HA console.
<!-- removeIf(!light)-->
<!-- removeIf(!light) -->
When using a colour light you might want to disable CSS style so Home Assistant can parse the color.
<!-- endRemoveIf(!light)-->
<!-- endRemoveIf(!light) -->
</div>
</div>
@ -904,7 +1039,7 @@
<div class="pure-g">
<label class="pure-u-1 pure-u-lg-1-4">Configuration</label>
<div class="pure-u-1-4 pure-u-lg-3-4"><button class="pure-button button-ha-config pure-u-1-3">Show</button></div>
<div class="pure-u-1-4 pure-u-lg-3-4"><button type="button" class="pure-button button-ha-config pure-u-1-3">Show</button></div>
<div class="pure-u-0 pure-u-lg-1-4"></div>
<div class="pure-u-1 pure-u-lg-3-4 hint">
These are the settings you should copy to your Home Assistant "configuration.yaml" file.
@ -921,7 +1056,9 @@
</div>
</div>
</form>
<form id="form-thingspeak" class="pure-form form-settings">
<div class="panel" id="panel-thingspeak">
<div class="header">
@ -955,15 +1092,17 @@
<div id="tspkRelays"></div>
<!-- removeIf(!sensor)-->
<!-- removeIf(!sensor) -->
<div id="tspkMagnitudes"></div>
<!-- endRemoveIf(!sensor)-->
<!-- endRemoveIf(!sensor) -->
</fieldset>
</div>
</div>
</form>
<form id="form-idb" class="pure-form form-settings">
<div class="panel" id="panel-idb">
<div class="header">
@ -999,19 +1138,22 @@
<div class="pure-g">
<label class="pure-u-1 pure-u-lg-1-4">Username</label>
<input class="pure-u-1 pure-u-lg-3-4" name="idbUsername" type="text" tabindex="44" autocomplete="false" />
<input class="pure-u-1 pure-u-lg-3-4" name="idbUsername" type="text" tabindex="44" autocomplete="off" />
</div>
<div class="pure-g">
<label class="pure-u-1 pure-u-lg-1-4">Password</label>
<input class="pure-u-1 pure-u-lg-3-4" name="idbPassword" type="password" tabindex="45" autocomplete="false" />
<input class="pure-u-1 pure-u-lg-3-4" name="idbPassword" type="password" tabindex="45" autocomplete="new-password" spellcheck="false" />
<span class="no-select password-reveal"></span>
</div>
</fieldset>
</div>
</div>
</form>
<form id="form-dbg" class="pure-form">
<div class="panel" id="panel-dbg">
<div class="header">
@ -1030,12 +1172,12 @@
Write a command and click send to execute it on the device. The output will be shown in the debug text area below.
</div>
<input name="dbgcmd" class="pure-u-3-4" type="text" tabindex="2" />
<div class=" pure-u-1-4 pure-u-lg-1-4"><button class="pure-button button-dbgcmd pure-u-23-24">Send</button></div>
<div class="pure-u-1-4 pure-u-lg-1-4"><button type="button" class="pure-button button-dbgcmd pure-u-23-24">Send</button></div>
</div>
<div class="pure-g">
<textarea class="pure-u-1 terminal" id="weblog" name="weblog" wrap="off" readonly></textarea>
<div class=" pure-u-1-4 pure-u-lg-1-4"><button class="pure-button button-dbg-clear pure-u-23-24">Clear</button></div>
<div class="pure-u-1-4 pure-u-lg-1-4"><button type="button" class="pure-button button-dbg-clear pure-u-23-24">Clear</button></div>
</div>
</fieldset>
@ -1043,9 +1185,11 @@
</div>
</div>
</form>
<!-- removeIf(!sensor)-->
<div class="panel" id="panel-sensors">
<!-- removeIf(!sensor) -->
<form id="form-sns" class="pure-form form-settings">
<div class="panel" id="panel-sns">
<div class="header">
<h1>SENSOR CONFIGURATION</h1>
@ -1063,23 +1207,30 @@
<div class="pure-g">
<label class="pure-u-1 pure-u-lg-1-4">Read interval</label>
<select class="pure-u-1 pure-u-lg-1-4" name="snsRead">
<option value="1">1 second</option>
<option value="6">6 seconds</option>
<option value="10">10 seconds</option>
<option value="15">15 seconds</option>
<option value="30">30 seconds</option>
<option value="60">1 minute</option>
<option value="300">5 minutes</option>
<option value="600">10 minutes</option>
<option value="900">15 minutes</option>
<option value="1800">30 minutes</option>
<option value="3600">60 minutes</option>
</select>
<div class="pure-u-0 pure-u-lg-1-2"></div>
<div class="pure-u-0 pure-u-lg-1-4"></div>
<div class="pure-u-1 pure-u-lg-3-4 hint">
Select the interval between readings. These will be filtered and averaged for the report. The default and recommended value is 6 seconds.
Select the interval between readings. These will be filtered and averaged for the report.
Please mind some sensors do not have fast refresh intervals. Check the sensor datasheet to know the minimum read interval.
The default and recommended value is 6 seconds.
</div>
</div>
<div class="pure-g">
<label class="pure-u-1 pure-u-lg-1-4">Report every</label>
<div class="pure-u-1 pure-u-lg-1-4"><input name="snsReport" class="pure-u-1" type="number" min="1" step="1" max="12" /></div>
<div class="pure-u-1 pure-u-lg-1-4"><input name="snsReport" class="pure-u-1" type="number" min="1" step="1" max="60" /></div>
<div class="pure-u-0 pure-u-lg-1-2"></div>
<div class="pure-u-0 pure-u-lg-1-4"></div>
<div class="pure-u-1 pure-u-lg-3-4 hint">
@ -1087,6 +1238,18 @@
</div>
</div>
<div class="pure-g module module-pwr">
<label class="pure-u-1 pure-u-lg-1-4">Save every</label>
<div class="pure-u-1 pure-u-lg-1-4"><input name="snsSave" class="pure-u-1" type="number" min="0" step="1" max="200" /></div>
<div class="pure-u-0 pure-u-lg-1-2"></div>
<div class="pure-u-0 pure-u-lg-1-4"></div>
<div class="pure-u-1 pure-u-lg-3-4 hint">
Save aggregated data to EEPROM after these many reports. At the moment this only applies to total energy readings.
Please mind: saving data to EEPROM too often will wear out the flash memory quickly.
Set it to 0 to disable this feature (default value).
</div>
</div>
<div class="pure-g module module-pwr">
<label class="pure-u-1 pure-u-lg-1-4">Power units</label>
<select name="pwrUnits" tabindex="16" class="pure-u-1 pure-u-lg-1-4">
@ -1095,9 +1258,9 @@
</select>
</div>
<div class="pure-g module module-hlw module-cse module-emon module-pzem">
<div class="pure-g module module-pwr">
<label class="pure-u-1 pure-u-lg-1-4">Energy units</label>
<select name="energyUnits" tabindex="16" class="pure-u-1 pure-u-lg-1-4">
<select name="eneUnits" tabindex="16" class="pure-u-1 pure-u-lg-1-4">
<option value="0">Joules (J)</option>
<option value="1">Kilowatts·hour (kWh)</option>
</select>
@ -1131,6 +1294,14 @@
</div>
</div>
<div class="pure-g module module-mics">
<label class="pure-u-1 pure-u-lg-1-4">Calibrate gas sensor</label>
<div class="pure-u-1 pure-u-lg-1-4"><input type="checkbox" name="snsResetCalibration" tabindex="55" /></div>
<div class="pure-u-0 pure-u-lg-1-2"></div>
<div class="pure-u-0 pure-u-lg-1-4"></div>
<div class="pure-u-1 pure-u-lg-3-4 hint">Move this switch to ON and press "Save" to reset gas sensor calibration. Check the sensor datasheet for calibration conditions.</div>
</div>
<legend class="module module-hlw module-cse module-emon">Energy monitor</legend>
<div class="pure-g module module-emon">
@ -1181,9 +1352,11 @@
</div>
</div>
<!-- endRemoveIf(!sensor)-->
</form>
<!-- endRemoveIf(!sensor) -->
<!-- removeIf(!rfbridge)-->
<!-- removeIf(!rfbridge) -->
<form id="form-rfb" class="pure-form form-settings">
<div class="panel" id="panel-rfb">
<div class="header">
@ -1203,10 +1376,10 @@
<div id="rfbNodes"></div>
</fieldset>
</div>
</div>
<!-- endRemoveIf(!rfbridge)-->
</div>
</form>
<!-- endRemoveIf(!rfbridge) -->
</div> <!-- content -->
@ -1214,7 +1387,7 @@
<!-- Templates -->
<!-- removeIf(!rfbridge)-->
<!-- removeIf(!rfbridge) -->
<div id="rfbNodeTemplate" class="template">
<legend>Switch #<span></span></legend>
@ -1236,7 +1409,7 @@
</div>
</div>
<!-- endRemoveIf(!rfbridge)-->
<!-- endRemoveIf(!rfbridge) -->
<div id="networkTemplate" class="template">
@ -1247,12 +1420,13 @@
<div class="pure-u-1-6 pure-u-lg-1-12"><button type="button" class="pure-button button-more-network pure-u-1">...</button></div>
<label class="pure-u-1 pure-u-lg-1-4 more">Password</label>
<input class="pure-u-1 pure-u-lg-3-4 more" name="pass" type="password" action="reconnect" value="" tabindex="0" autocomplete="false" />
<input class="pure-u-1 pure-u-lg-3-4 more" name="pass" type="password" action="reconnect" value="" tabindex="0" autocomplete="new-password" spellcheck="false" />
<span class="no-select password-reveal more"></span>
<label class="pure-u-1 pure-u-lg-1-4 more">Static IP</label>
<input class="pure-u-1 pure-u-lg-3-4 more" name="ip" type="text" action="reconnect" value="" maxlength="15" tabindex="0" autocomplete="false" />
<div class="pure-u-0 pure-u-lg-1-4 more"></div>
<div class="pure-u-1 pure-u-lg-3-4 hint more">Leave empty for DNS negotiation</div>
<div class="pure-u-1 pure-u-lg-3-4 hint more">Leave empty for DHCP negotiation</div>
<label class="pure-u-1 pure-u-lg-1-4 more">Gateway IP</label>
<input class="pure-u-1 pure-u-lg-3-4 more" name="gw" type="text" action="reconnect" value="" maxlength="15" tabindex="0" autocomplete="false" />
@ -1294,6 +1468,7 @@
<label class="pure-u-1 pure-u-lg-1-4">Use UTC time</label>
<div class="pure-u-1 pure-u-lg-3-4"><input type="checkbox" name="schUTC" /></div>
<div class="pure-u-0 pure-u-lg-1-2"></div>
<label class="pure-u-1 pure-u-lg-1-4">And weekday is one of</label>
<div class="pure-u-2-5 pure-u-lg-1-5">
<input class="pure-u-23-24 pure-u-lg-23-24" name="schWDs" type="text" maxlength="15" tabindex="0" value="1,2,3,4,5,6,7" />
@ -1306,7 +1481,7 @@
<label class="pure-u-1 pure-u-lg-1-4">Enabled</label>
<div class="pure-u-1 pure-u-lg-3-4"><input type="checkbox" name="schEnabled" /></div>
<div class="pure-u-0 pure-u-lg-1-4"></div>
<div class="pure-u-1 pure-u-lg-1-2"></div>
<button class="pure-button button-del-schedule" type="button">Delete schedule</button>
</div>
</div>
@ -1324,7 +1499,7 @@
<input type="hidden" name="schType" value="1">
</div>
<!-- removeIf(!light)-->
<!-- removeIf(!light) -->
<div id="lightActionTemplate" class="template">
<label class="pure-u-1 pure-u-lg-1-4">Brightness</label>
<div class="pure-u-1 pure-u-lg-1-5">
@ -1333,12 +1508,12 @@
<select class="pure-u-1 pure-u-lg-1-5 islight" name="schSwitch"></select>
<input type="hidden" name="schType" value="2">
</div>
<!-- endRemoveIf(!light)-->
<!-- endRemoveIf(!light) -->
<div id="relayTemplate" class="template">
<div class="pure-g">
<label class="pure-u-1 pure-u-lg-1-4">Switch #<span class="id"></span></label>
<input name="relay" type="checkbox" on="ON" off="OFF" />
<div><input name="relay" type="checkbox" on="ON" off="OFF" /></div>
</div>
</div>
@ -1393,7 +1568,7 @@
</div>
</div>
<!-- removeIf(!sensor)-->
<!-- removeIf(!sensor) -->
<div id="dczMagnitudeTemplate" class="template">
<div class="pure-g">
<label class="pure-u-1 pure-u-lg-1-4">Magnitude</label>
@ -1401,7 +1576,7 @@
<div class="pure-u-1 pure-u-lg-1-2 hint center"></div>
</div>
</div>
<!-- endRemoveIf(!sensor)-->
<!-- endRemoveIf(!sensor) -->
<div id="tspkRelayTemplate" class="template">
<div class="pure-g">
@ -1410,7 +1585,7 @@
</div>
</div>
<!-- removeIf(!sensor)-->
<!-- removeIf(!sensor) -->
<div id="tspkMagnitudeTemplate" class="template">
<div class="pure-g">
<label class="pure-u-1 pure-u-lg-1-4">Magnitude</label>
@ -1418,15 +1593,18 @@
<div class="pure-u-1 pure-u-lg-1-2 hint center"></div>
</div>
</div>
<!-- endRemoveIf(!sensor)-->
<!-- endRemoveIf(!sensor) -->
<!-- removeIf(!light)-->
<div id="colorRGBTemplate" class="template">
<!-- removeIf(!light) -->
<div id="colorTemplate" class="template">
<div class="pure-g">
<label class="pure-u-1 pure-u-lg-1-4">Color</label>
<input class="pure-u-1 pure-u-lg-1-4" data-wcp-layout="block" name="color" readonly />
</div>
</div>
<div id="brightnessTemplate" class="template">
<div class="pure-g">
<label class="pure-u-1 pure-u-lg-1-4">Brightness</label>
<input type="range" min="0" max="255" class="slider pure-u-lg-1-4" id="brightness">
@ -1434,13 +1612,6 @@
</div>
</div>
<div id="colorHSVTemplate" class="template">
<div class="pure-g">
<label class="pure-u-1 pure-u-lg-1-4">Color</label>
<input class="pure-u-1 pure-u-lg-1-4" data-wcp-layout="block" name="color" readonly />
</div>
</div>
<div id="channelTemplate" class="template">
<div class="pure-g">
<label class="pure-u-1 pure-u-lg-1-4">Channel #</label>
@ -1456,9 +1627,9 @@
<span class="slider mireds pure-u-lg-1-4"></span>
</div>
</div>
<!-- endRemoveIf(!light)-->
<!-- endRemoveIf(!light) -->
<!-- removeIf(!sensor)-->
<!-- removeIf(!sensor) -->
<div id="magnitudeTemplate" class="template">
<div class="pure-g">
<label class="pure-u-1 pure-u-lg-1-4"></label>
@ -1468,7 +1639,18 @@
<div class="pure-u-1 pure-u-lg-1-2 hint center"></div>
</div>
</div>
<!-- endRemoveIf(!sensor)-->
<!-- endRemoveIf(!sensor) -->
<!-- removeIf(!rfm69) -->
<div id="nodeTemplate" class="template">
<div class="pure-g">
<div class="pure-u-md-1-6 pure-u-1-2"><input name="node" type="text" class="pure-u-11-12" value="" size="8" tabindex="0" placeholder="Node ID" autocomplete="false"></div>
<div class="pure-u-md-1-6 pure-u-1-2"><input name="key" type="text" class="pure-u-11-12" value="" size="8" tabindex="0" placeholder="Key"></div>
<div class="pure-u-md-1-2 pure-u-3-4"><input name="topic" type="text" class="pure-md-11-12 pure-u-23-24" value="" size="8" tabindex="0" placeholder="MQTT Topic"></div>
<div class="pure-u-md-1-6 pure-u-1-4"><button type="button" class="pure-button button-del-mapping pure-u-5-6 pure-u-md-5-6">Del</button></div>
</div>
</div>
<!-- endRemoveIf(!rfm69) -->
<iframe id="downloader"></iframe>
<input id="uploader" type="file" />
@ -1478,9 +1660,12 @@
<!-- build:js script.js -->
<script src="vendor/jquery-3.2.1.min.js"></script>
<script src="custom.js"></script>
<!-- removeIf(!light)-->
<!-- removeIf(!light) -->
<script src="vendor/jquery.wheelcolorpicker-3.0.3.min.js"></script>
<!-- endRemoveIf(!light)-->
<!-- endRemoveIf(!light) -->
<!-- removeIf(!rfm69) -->
<script src="vendor/datatables-1.10.16.min.js"></script>
<!-- endRemoveIf(!rfm69) -->
<!-- endbuild -->
</html>

+ 460
- 0
code/html/vendor/datatables-1.10.16.css View File

@ -0,0 +1,460 @@
/*
* This combined file was created by the DataTables downloader builder:
* https://datatables.net/download
*
* To rebuild or modify this file with the latest versions of the included
* software please visit:
* https://datatables.net/download/#dt/dt-1.10.16
*
* Included libraries:
* DataTables 1.10.16
*/
/*
* Table styles
*/
table.dataTable {
width: 100%;
margin: 0 auto;
clear: both;
border-collapse: separate;
border-spacing: 0;
/*
* Header and footer styles
*/
/*
* Body styles
*/
}
table.dataTable thead th,
table.dataTable tfoot th {
font-weight: bold;
}
table.dataTable thead th,
table.dataTable thead td {
padding: 10px 18px;
border-bottom: 1px solid #111;
}
table.dataTable thead th:active,
table.dataTable thead td:active {
outline: none;
}
table.dataTable tfoot th,
table.dataTable tfoot td {
padding: 10px 18px 6px 18px;
border-top: 1px solid #111;
}
table.dataTable thead .sorting,
table.dataTable thead .sorting_asc,
table.dataTable thead .sorting_desc,
table.dataTable thead .sorting_asc_disabled,
table.dataTable thead .sorting_desc_disabled {
cursor: pointer;
*cursor: hand;
background-repeat: no-repeat;
background-position: center right;
}
table.dataTable thead .sorting {
background-image: url("images/sort_both.png");
}
table.dataTable thead .sorting_asc {
background-image: url("images/sort_asc.png");
}
table.dataTable thead .sorting_desc {
background-image: url("images/sort_desc.png");
}
table.dataTable thead .sorting_asc_disabled {
background-image: url("images/sort_asc_disabled.png");
}
table.dataTable thead .sorting_desc_disabled {
background-image: url("images/sort_desc_disabled.png");
}
table.dataTable tbody tr {
background-color: #ffffff;
}
table.dataTable tbody tr.selected {
background-color: #B0BED9;
}
table.dataTable tbody th,
table.dataTable tbody td {
padding: 8px 10px;
}
table.dataTable.row-border tbody th, table.dataTable.row-border tbody td, table.dataTable.display tbody th, table.dataTable.display tbody td {
border-top: 1px solid #ddd;
}
table.dataTable.row-border tbody tr:first-child th,
table.dataTable.row-border tbody tr:first-child td, table.dataTable.display tbody tr:first-child th,
table.dataTable.display tbody tr:first-child td {
border-top: none;
}
table.dataTable.cell-border tbody th, table.dataTable.cell-border tbody td {
border-top: 1px solid #ddd;
border-right: 1px solid #ddd;
}
table.dataTable.cell-border tbody tr th:first-child,
table.dataTable.cell-border tbody tr td:first-child {
border-left: 1px solid #ddd;
}
table.dataTable.cell-border tbody tr:first-child th,
table.dataTable.cell-border tbody tr:first-child td {
border-top: none;
}
table.dataTable.stripe tbody tr.odd, table.dataTable.display tbody tr.odd {
background-color: #f9f9f9;
}
table.dataTable.stripe tbody tr.odd.selected, table.dataTable.display tbody tr.odd.selected {
background-color: #acbad4;
}
table.dataTable.hover tbody tr:hover, table.dataTable.display tbody tr:hover {
background-color: #f6f6f6;
}
table.dataTable.hover tbody tr:hover.selected, table.dataTable.display tbody tr:hover.selected {
background-color: #aab7d1;
}
table.dataTable.order-column tbody tr > .sorting_1,
table.dataTable.order-column tbody tr > .sorting_2,
table.dataTable.order-column tbody tr > .sorting_3, table.dataTable.display tbody tr > .sorting_1,
table.dataTable.display tbody tr > .sorting_2,
table.dataTable.display tbody tr > .sorting_3 {
background-color: #fafafa;
}
table.dataTable.order-column tbody tr.selected > .sorting_1,
table.dataTable.order-column tbody tr.selected > .sorting_2,
table.dataTable.order-column tbody tr.selected > .sorting_3, table.dataTable.display tbody tr.selected > .sorting_1,
table.dataTable.display tbody tr.selected > .sorting_2,
table.dataTable.display tbody tr.selected > .sorting_3 {
background-color: #acbad5;
}
table.dataTable.display tbody tr.odd > .sorting_1, table.dataTable.order-column.stripe tbody tr.odd > .sorting_1 {
background-color: #f1f1f1;
}
table.dataTable.display tbody tr.odd > .sorting_2, table.dataTable.order-column.stripe tbody tr.odd > .sorting_2 {
background-color: #f3f3f3;
}
table.dataTable.display tbody tr.odd > .sorting_3, table.dataTable.order-column.stripe tbody tr.odd > .sorting_3 {
background-color: whitesmoke;
}
table.dataTable.display tbody tr.odd.selected > .sorting_1, table.dataTable.order-column.stripe tbody tr.odd.selected > .sorting_1 {
background-color: #a6b4cd;
}
table.dataTable.display tbody tr.odd.selected > .sorting_2, table.dataTable.order-column.stripe tbody tr.odd.selected > .sorting_2 {
background-color: #a8b5cf;
}
table.dataTable.display tbody tr.odd.selected > .sorting_3, table.dataTable.order-column.stripe tbody tr.odd.selected > .sorting_3 {
background-color: #a9b7d1;
}
table.dataTable.display tbody tr.even > .sorting_1, table.dataTable.order-column.stripe tbody tr.even > .sorting_1 {
background-color: #fafafa;
}
table.dataTable.display tbody tr.even > .sorting_2, table.dataTable.order-column.stripe tbody tr.even > .sorting_2 {
background-color: #fcfcfc;
}
table.dataTable.display tbody tr.even > .sorting_3, table.dataTable.order-column.stripe tbody tr.even > .sorting_3 {
background-color: #fefefe;
}
table.dataTable.display tbody tr.even.selected > .sorting_1, table.dataTable.order-column.stripe tbody tr.even.selected > .sorting_1 {
background-color: #acbad5;
}
table.dataTable.display tbody tr.even.selected > .sorting_2, table.dataTable.order-column.stripe tbody tr.even.selected > .sorting_2 {
background-color: #aebcd6;
}
table.dataTable.display tbody tr.even.selected > .sorting_3, table.dataTable.order-column.stripe tbody tr.even.selected > .sorting_3 {
background-color: #afbdd8;
}
table.dataTable.display tbody tr:hover > .sorting_1, table.dataTable.order-column.hover tbody tr:hover > .sorting_1 {
background-color: #eaeaea;
}
table.dataTable.display tbody tr:hover > .sorting_2, table.dataTable.order-column.hover tbody tr:hover > .sorting_2 {
background-color: #ececec;
}
table.dataTable.display tbody tr:hover > .sorting_3, table.dataTable.order-column.hover tbody tr:hover > .sorting_3 {
background-color: #efefef;
}
table.dataTable.display tbody tr:hover.selected > .sorting_1, table.dataTable.order-column.hover tbody tr:hover.selected > .sorting_1 {
background-color: #a2aec7;
}
table.dataTable.display tbody tr:hover.selected > .sorting_2, table.dataTable.order-column.hover tbody tr:hover.selected > .sorting_2 {
background-color: #a3b0c9;
}
table.dataTable.display tbody tr:hover.selected > .sorting_3, table.dataTable.order-column.hover tbody tr:hover.selected > .sorting_3 {
background-color: #a5b2cb;
}
table.dataTable.no-footer {
border-bottom: 1px solid #111;
}
table.dataTable.nowrap th, table.dataTable.nowrap td {
white-space: nowrap;
}
table.dataTable.compact thead th,
table.dataTable.compact thead td {
padding: 4px 17px 4px 4px;
}
table.dataTable.compact tfoot th,
table.dataTable.compact tfoot td {
padding: 4px;
}
table.dataTable.compact tbody th,
table.dataTable.compact tbody td {
padding: 4px;
}
table.dataTable th.dt-left,
table.dataTable td.dt-left {
text-align: left;
}
table.dataTable th.dt-center,
table.dataTable td.dt-center,
table.dataTable td.dataTables_empty {
text-align: center;
}
table.dataTable th.dt-right,
table.dataTable td.dt-right {
text-align: right;
}
table.dataTable th.dt-justify,
table.dataTable td.dt-justify {
text-align: justify;
}
table.dataTable th.dt-nowrap,
table.dataTable td.dt-nowrap {
white-space: nowrap;
}
table.dataTable thead th.dt-head-left,
table.dataTable thead td.dt-head-left,
table.dataTable tfoot th.dt-head-left,
table.dataTable tfoot td.dt-head-left {
text-align: left;
}
table.dataTable thead th.dt-head-center,
table.dataTable thead td.dt-head-center,
table.dataTable tfoot th.dt-head-center,
table.dataTable tfoot td.dt-head-center {
text-align: center;
}
table.dataTable thead th.dt-head-right,
table.dataTable thead td.dt-head-right,
table.dataTable tfoot th.dt-head-right,
table.dataTable tfoot td.dt-head-right {
text-align: right;
}
table.dataTable thead th.dt-head-justify,
table.dataTable thead td.dt-head-justify,
table.dataTable tfoot th.dt-head-justify,
table.dataTable tfoot td.dt-head-justify {
text-align: justify;
}
table.dataTable thead th.dt-head-nowrap,
table.dataTable thead td.dt-head-nowrap,
table.dataTable tfoot th.dt-head-nowrap,
table.dataTable tfoot td.dt-head-nowrap {
white-space: nowrap;
}
table.dataTable tbody th.dt-body-left,
table.dataTable tbody td.dt-body-left {
text-align: left;
}
table.dataTable tbody th.dt-body-center,
table.dataTable tbody td.dt-body-center {
text-align: center;
}
table.dataTable tbody th.dt-body-right,
table.dataTable tbody td.dt-body-right {
text-align: right;
}
table.dataTable tbody th.dt-body-justify,
table.dataTable tbody td.dt-body-justify {
text-align: justify;
}
table.dataTable tbody th.dt-body-nowrap,
table.dataTable tbody td.dt-body-nowrap {
white-space: nowrap;
}
table.dataTable,
table.dataTable th,
table.dataTable td {
box-sizing: content-box;
}
/*
* Control feature layout
*/
.dataTables_wrapper {
position: relative;
clear: both;
*zoom: 1;
zoom: 1;
}
.dataTables_wrapper .dataTables_length {
float: left;
}
.dataTables_wrapper .dataTables_filter {
float: right;
text-align: right;
}
.dataTables_wrapper .dataTables_filter input {
margin-left: 0.5em;
}
.dataTables_wrapper .dataTables_info {
clear: both;
float: left;
padding-top: 0.755em;
}
.dataTables_wrapper .dataTables_paginate {
float: right;
text-align: right;
padding-top: 0.25em;
}
.dataTables_wrapper .dataTables_paginate .paginate_button {
box-sizing: border-box;
display: inline-block;
min-width: 1.5em;
padding: 0.5em 1em;
margin-left: 2px;
text-align: center;
text-decoration: none !important;
cursor: pointer;
*cursor: hand;
color: #333 !important;
border: 1px solid transparent;
border-radius: 2px;
}
.dataTables_wrapper .dataTables_paginate .paginate_button.current, .dataTables_wrapper .dataTables_paginate .paginate_button.current:hover {
color: #333 !important;
border: 1px solid #979797;
background-color: white;
background: -webkit-gradient(linear, left top, left bottom, color-stop(0%, white), color-stop(100%, #dcdcdc));
/* Chrome,Safari4+ */
background: -webkit-linear-gradient(top, white 0%, #dcdcdc 100%);
/* Chrome10+,Safari5.1+ */
background: -moz-linear-gradient(top, white 0%, #dcdcdc 100%);
/* FF3.6+ */
background: -ms-linear-gradient(top, white 0%, #dcdcdc 100%);
/* IE10+ */
background: -o-linear-gradient(top, white 0%, #dcdcdc 100%);
/* Opera 11.10+ */
background: linear-gradient(to bottom, white 0%, #dcdcdc 100%);
/* W3C */
}
.dataTables_wrapper .dataTables_paginate .paginate_button.disabled, .dataTables_wrapper .dataTables_paginate .paginate_button.disabled:hover, .dataTables_wrapper .dataTables_paginate .paginate_button.disabled:active {
cursor: default;
color: #666 !important;
border: 1px solid transparent;
background: transparent;
box-shadow: none;
}
.dataTables_wrapper .dataTables_paginate .paginate_button:hover {
color: white !important;
border: 1px solid #111;
background-color: #585858;
background: -webkit-gradient(linear, left top, left bottom, color-stop(0%, #585858), color-stop(100%, #111));
/* Chrome,Safari4+ */
background: -webkit-linear-gradient(top, #585858 0%, #111 100%);
/* Chrome10+,Safari5.1+ */
background: -moz-linear-gradient(top, #585858 0%, #111 100%);
/* FF3.6+ */
background: -ms-linear-gradient(top, #585858 0%, #111 100%);
/* IE10+ */
background: -o-linear-gradient(top, #585858 0%, #111 100%);
/* Opera 11.10+ */
background: linear-gradient(to bottom, #585858 0%, #111 100%);
/* W3C */
}
.dataTables_wrapper .dataTables_paginate .paginate_button:active {
outline: none;
background-color: #2b2b2b;
background: -webkit-gradient(linear, left top, left bottom, color-stop(0%, #2b2b2b), color-stop(100%, #0c0c0c));
/* Chrome,Safari4+ */
background: -webkit-linear-gradient(top, #2b2b2b 0%, #0c0c0c 100%);
/* Chrome10+,Safari5.1+ */
background: -moz-linear-gradient(top, #2b2b2b 0%, #0c0c0c 100%);
/* FF3.6+ */
background: -ms-linear-gradient(top, #2b2b2b 0%, #0c0c0c 100%);
/* IE10+ */
background: -o-linear-gradient(top, #2b2b2b 0%, #0c0c0c 100%);
/* Opera 11.10+ */
background: linear-gradient(to bottom, #2b2b2b 0%, #0c0c0c 100%);
/* W3C */
box-shadow: inset 0 0 3px #111;
}
.dataTables_wrapper .dataTables_paginate .ellipsis {
padding: 0 1em;
}
.dataTables_wrapper .dataTables_processing {
position: absolute;
top: 50%;
left: 50%;
width: 100%;
height: 40px;
margin-left: -50%;
margin-top: -25px;
padding-top: 20px;
text-align: center;
font-size: 1.2em;
background-color: white;
background: -webkit-gradient(linear, left top, right top, color-stop(0%, rgba(255, 255, 255, 0)), color-stop(25%, rgba(255, 255, 255, 0.9)), color-stop(75%, rgba(255, 255, 255, 0.9)), color-stop(100%, rgba(255, 255, 255, 0)));
background: -webkit-linear-gradient(left, rgba(255, 255, 255, 0) 0%, rgba(255, 255, 255, 0.9) 25%, rgba(255, 255, 255, 0.9) 75%, rgba(255, 255, 255, 0) 100%);
background: -moz-linear-gradient(left, rgba(255, 255, 255, 0) 0%, rgba(255, 255, 255, 0.9) 25%, rgba(255, 255, 255, 0.9) 75%, rgba(255, 255, 255, 0) 100%);
background: -ms-linear-gradient(left, rgba(255, 255, 255, 0) 0%, rgba(255, 255, 255, 0.9) 25%, rgba(255, 255, 255, 0.9) 75%, rgba(255, 255, 255, 0) 100%);
background: -o-linear-gradient(left, rgba(255, 255, 255, 0) 0%, rgba(255, 255, 255, 0.9) 25%, rgba(255, 255, 255, 0.9) 75%, rgba(255, 255, 255, 0) 100%);
background: linear-gradient(to right, rgba(255, 255, 255, 0) 0%, rgba(255, 255, 255, 0.9) 25%, rgba(255, 255, 255, 0.9) 75%, rgba(255, 255, 255, 0) 100%);
}
.dataTables_wrapper .dataTables_length,
.dataTables_wrapper .dataTables_filter,
.dataTables_wrapper .dataTables_info,
.dataTables_wrapper .dataTables_processing,
.dataTables_wrapper .dataTables_paginate {
color: #333;
}
.dataTables_wrapper .dataTables_scroll {
clear: both;
}
.dataTables_wrapper .dataTables_scroll div.dataTables_scrollBody {
*margin-top: -1px;
-webkit-overflow-scrolling: touch;
}
.dataTables_wrapper .dataTables_scroll div.dataTables_scrollBody > table > thead > tr > th, .dataTables_wrapper .dataTables_scroll div.dataTables_scrollBody > table > thead > tr > td, .dataTables_wrapper .dataTables_scroll div.dataTables_scrollBody > table > tbody > tr > th, .dataTables_wrapper .dataTables_scroll div.dataTables_scrollBody > table > tbody > tr > td {
vertical-align: middle;
}
.dataTables_wrapper .dataTables_scroll div.dataTables_scrollBody > table > thead > tr > th > div.dataTables_sizing,
.dataTables_wrapper .dataTables_scroll div.dataTables_scrollBody > table > thead > tr > td > div.dataTables_sizing, .dataTables_wrapper .dataTables_scroll div.dataTables_scrollBody > table > tbody > tr > th > div.dataTables_sizing,
.dataTables_wrapper .dataTables_scroll div.dataTables_scrollBody > table > tbody > tr > td > div.dataTables_sizing {
height: 0;
overflow: hidden;
margin: 0 !important;
padding: 0 !important;
}
.dataTables_wrapper.no-footer .dataTables_scrollBody {
border-bottom: 1px solid #111;
}
.dataTables_wrapper.no-footer div.dataTables_scrollHead table.dataTable,
.dataTables_wrapper.no-footer div.dataTables_scrollBody > table {
border-bottom: none;
}
.dataTables_wrapper:after {
visibility: hidden;
display: block;
content: "";
clear: both;
height: 0;
}
@media screen and (max-width: 767px) {
.dataTables_wrapper .dataTables_info,
.dataTables_wrapper .dataTables_paginate {
float: none;
text-align: center;
}
.dataTables_wrapper .dataTables_paginate {
margin-top: 0.5em;
}
}
@media screen and (max-width: 640px) {
.dataTables_wrapper .dataTables_length,
.dataTables_wrapper .dataTables_filter {
float: none;
text-align: center;
}
.dataTables_wrapper .dataTables_filter {
margin-top: 0.5em;
}
}

+ 178
- 0
code/html/vendor/datatables-1.10.16.min.js View File

@ -0,0 +1,178 @@
/*
* This combined file was created by the DataTables downloader builder:
* https://datatables.net/download
*
* To rebuild or modify this file with the latest versions of the included
* software please visit:
* https://datatables.net/download/#dt/dt-1.10.16
*
* Included libraries:
* DataTables 1.10.16
*/
/*!
DataTables 1.10.16
©2008-2017 SpryMedia Ltd - datatables.net/license
*/
(function(h){"function"===typeof define&&define.amd?define(["jquery"],function(E){return h(E,window,document)}):"object"===typeof exports?module.exports=function(E,G){E||(E=window);G||(G="undefined"!==typeof window?require("jquery"):require("jquery")(E));return h(G,E,E.document)}:h(jQuery,window,document)})(function(h,E,G,k){function X(a){var b,c,d={};h.each(a,function(e){if((b=e.match(/^([^A-Z]+?)([A-Z])/))&&-1!=="a aa ai ao as b fn i m o s ".indexOf(b[1]+" "))c=e.replace(b[0],b[2].toLowerCase()),
d[c]=e,"o"===b[1]&&X(a[e])});a._hungarianMap=d}function I(a,b,c){a._hungarianMap||X(a);var d;h.each(b,function(e){d=a._hungarianMap[e];if(d!==k&&(c||b[d]===k))"o"===d.charAt(0)?(b[d]||(b[d]={}),h.extend(!0,b[d],b[e]),I(a[d],b[d],c)):b[d]=b[e]})}function Ca(a){var b=m.defaults.oLanguage,c=a.sZeroRecords;!a.sEmptyTable&&(c&&"No data available in table"===b.sEmptyTable)&&F(a,a,"sZeroRecords","sEmptyTable");!a.sLoadingRecords&&(c&&"Loading..."===b.sLoadingRecords)&&F(a,a,"sZeroRecords","sLoadingRecords");
a.sInfoThousands&&(a.sThousands=a.sInfoThousands);(a=a.sDecimal)&&cb(a)}function db(a){A(a,"ordering","bSort");A(a,"orderMulti","bSortMulti");A(a,"orderClasses","bSortClasses");A(a,"orderCellsTop","bSortCellsTop");A(a,"order","aaSorting");A(a,"orderFixed","aaSortingFixed");A(a,"paging","bPaginate");A(a,"pagingType","sPaginationType");A(a,"pageLength","iDisplayLength");A(a,"searching","bFilter");"boolean"===typeof a.sScrollX&&(a.sScrollX=a.sScrollX?"100%":"");"boolean"===typeof a.scrollX&&(a.scrollX=
a.scrollX?"100%":"");if(a=a.aoSearchCols)for(var b=0,c=a.length;b<c;b++)a[b]&&I(m.models.oSearch,a[b])}function eb(a){A(a,"orderable","bSortable");A(a,"orderData","aDataSort");A(a,"orderSequence","asSorting");A(a,"orderDataType","sortDataType");var b=a.aDataSort;"number"===typeof b&&!h.isArray(b)&&(a.aDataSort=[b])}function fb(a){if(!m.__browser){var b={};m.__browser=b;var c=h("<div/>").css({position:"fixed",top:0,left:-1*h(E).scrollLeft(),height:1,width:1,overflow:"hidden"}).append(h("<div/>").css({position:"absolute",
top:1,left:1,width:100,overflow:"scroll"}).append(h("<div/>").css({width:"100%",height:10}))).appendTo("body"),d=c.children(),e=d.children();b.barWidth=d[0].offsetWidth-d[0].clientWidth;b.bScrollOversize=100===e[0].offsetWidth&&100!==d[0].clientWidth;b.bScrollbarLeft=1!==Math.round(e.offset().left);b.bBounding=c[0].getBoundingClientRect().width?!0:!1;c.remove()}h.extend(a.oBrowser,m.__browser);a.oScroll.iBarWidth=m.__browser.barWidth}function gb(a,b,c,d,e,f){var g,j=!1;c!==k&&(g=c,j=!0);for(;d!==
e;)a.hasOwnProperty(d)&&(g=j?b(g,a[d],d,a):a[d],j=!0,d+=f);return g}function Da(a,b){var c=m.defaults.column,d=a.aoColumns.length,c=h.extend({},m.models.oColumn,c,{nTh:b?b:G.createElement("th"),sTitle:c.sTitle?c.sTitle:b?b.innerHTML:"",aDataSort:c.aDataSort?c.aDataSort:[d],mData:c.mData?c.mData:d,idx:d});a.aoColumns.push(c);c=a.aoPreSearchCols;c[d]=h.extend({},m.models.oSearch,c[d]);ja(a,d,h(b).data())}function ja(a,b,c){var b=a.aoColumns[b],d=a.oClasses,e=h(b.nTh);if(!b.sWidthOrig){b.sWidthOrig=
e.attr("width")||null;var f=(e.attr("style")||"").match(/width:\s*(\d+[pxem%]+)/);f&&(b.sWidthOrig=f[1])}c!==k&&null!==c&&(eb(c),I(m.defaults.column,c),c.mDataProp!==k&&!c.mData&&(c.mData=c.mDataProp),c.sType&&(b._sManualType=c.sType),c.className&&!c.sClass&&(c.sClass=c.className),c.sClass&&e.addClass(c.sClass),h.extend(b,c),F(b,c,"sWidth","sWidthOrig"),c.iDataSort!==k&&(b.aDataSort=[c.iDataSort]),F(b,c,"aDataSort"));var g=b.mData,j=Q(g),i=b.mRender?Q(b.mRender):null,c=function(a){return"string"===
typeof a&&-1!==a.indexOf("@")};b._bAttrSrc=h.isPlainObject(g)&&(c(g.sort)||c(g.type)||c(g.filter));b._setter=null;b.fnGetData=function(a,b,c){var d=j(a,b,k,c);return i&&b?i(d,b,a,c):d};b.fnSetData=function(a,b,c){return R(g)(a,b,c)};"number"!==typeof g&&(a._rowReadObject=!0);a.oFeatures.bSort||(b.bSortable=!1,e.addClass(d.sSortableNone));a=-1!==h.inArray("asc",b.asSorting);c=-1!==h.inArray("desc",b.asSorting);!b.bSortable||!a&&!c?(b.sSortingClass=d.sSortableNone,b.sSortingClassJUI=""):a&&!c?(b.sSortingClass=
d.sSortableAsc,b.sSortingClassJUI=d.sSortJUIAscAllowed):!a&&c?(b.sSortingClass=d.sSortableDesc,b.sSortingClassJUI=d.sSortJUIDescAllowed):(b.sSortingClass=d.sSortable,b.sSortingClassJUI=d.sSortJUI)}function Y(a){if(!1!==a.oFeatures.bAutoWidth){var b=a.aoColumns;Ea(a);for(var c=0,d=b.length;c<d;c++)b[c].nTh.style.width=b[c].sWidth}b=a.oScroll;(""!==b.sY||""!==b.sX)&&ka(a);r(a,null,"column-sizing",[a])}function Z(a,b){var c=la(a,"bVisible");return"number"===typeof c[b]?c[b]:null}function $(a,b){var c=
la(a,"bVisible"),c=h.inArray(b,c);return-1!==c?c:null}function aa(a){var b=0;h.each(a.aoColumns,function(a,d){d.bVisible&&"none"!==h(d.nTh).css("display")&&b++});return b}function la(a,b){var c=[];h.map(a.aoColumns,function(a,e){a[b]&&c.push(e)});return c}function Fa(a){var b=a.aoColumns,c=a.aoData,d=m.ext.type.detect,e,f,g,j,i,h,l,q,t;e=0;for(f=b.length;e<f;e++)if(l=b[e],t=[],!l.sType&&l._sManualType)l.sType=l._sManualType;else if(!l.sType){g=0;for(j=d.length;g<j;g++){i=0;for(h=c.length;i<h;i++){t[i]===
k&&(t[i]=B(a,i,e,"type"));q=d[g](t[i],a);if(!q&&g!==d.length-1)break;if("html"===q)break}if(q){l.sType=q;break}}l.sType||(l.sType="string")}}function hb(a,b,c,d){var e,f,g,j,i,n,l=a.aoColumns;if(b)for(e=b.length-1;0<=e;e--){n=b[e];var q=n.targets!==k?n.targets:n.aTargets;h.isArray(q)||(q=[q]);f=0;for(g=q.length;f<g;f++)if("number"===typeof q[f]&&0<=q[f]){for(;l.length<=q[f];)Da(a);d(q[f],n)}else if("number"===typeof q[f]&&0>q[f])d(l.length+q[f],n);else if("string"===typeof q[f]){j=0;for(i=l.length;j<
i;j++)("_all"==q[f]||h(l[j].nTh).hasClass(q[f]))&&d(j,n)}}if(c){e=0;for(a=c.length;e<a;e++)d(e,c[e])}}function M(a,b,c,d){var e=a.aoData.length,f=h.extend(!0,{},m.models.oRow,{src:c?"dom":"data",idx:e});f._aData=b;a.aoData.push(f);for(var g=a.aoColumns,j=0,i=g.length;j<i;j++)g[j].sType=null;a.aiDisplayMaster.push(e);b=a.rowIdFn(b);b!==k&&(a.aIds[b]=f);(c||!a.oFeatures.bDeferRender)&&Ga(a,e,c,d);return e}function ma(a,b){var c;b instanceof h||(b=h(b));return b.map(function(b,e){c=Ha(a,e);return M(a,
c.data,e,c.cells)})}function B(a,b,c,d){var e=a.iDraw,f=a.aoColumns[c],g=a.aoData[b]._aData,j=f.sDefaultContent,i=f.fnGetData(g,d,{settings:a,row:b,col:c});if(i===k)return a.iDrawError!=e&&null===j&&(J(a,0,"Requested unknown parameter "+("function"==typeof f.mData?"{function}":"'"+f.mData+"'")+" for row "+b+", column "+c,4),a.iDrawError=e),j;if((i===g||null===i)&&null!==j&&d!==k)i=j;else if("function"===typeof i)return i.call(g);return null===i&&"display"==d?"":i}function ib(a,b,c,d){a.aoColumns[c].fnSetData(a.aoData[b]._aData,
d,{settings:a,row:b,col:c})}function Ia(a){return h.map(a.match(/(\\.|[^\.])+/g)||[""],function(a){return a.replace(/\\\./g,".")})}function Q(a){if(h.isPlainObject(a)){var b={};h.each(a,function(a,c){c&&(b[a]=Q(c))});return function(a,c,f,g){var j=b[c]||b._;return j!==k?j(a,c,f,g):a}}if(null===a)return function(a){return a};if("function"===typeof a)return function(b,c,f,g){return a(b,c,f,g)};if("string"===typeof a&&(-1!==a.indexOf(".")||-1!==a.indexOf("[")||-1!==a.indexOf("("))){var c=function(a,
b,f){var g,j;if(""!==f){j=Ia(f);for(var i=0,n=j.length;i<n;i++){f=j[i].match(ba);g=j[i].match(U);if(f){j[i]=j[i].replace(ba,"");""!==j[i]&&(a=a[j[i]]);g=[];j.splice(0,i+1);j=j.join(".");if(h.isArray(a)){i=0;for(n=a.length;i<n;i++)g.push(c(a[i],b,j))}a=f[0].substring(1,f[0].length-1);a=""===a?g:g.join(a);break}else if(g){j[i]=j[i].replace(U,"");a=a[j[i]]();continue}if(null===a||a[j[i]]===k)return k;a=a[j[i]]}}return a};return function(b,e){return c(b,e,a)}}return function(b){return b[a]}}function R(a){if(h.isPlainObject(a))return R(a._);
if(null===a)return function(){};if("function"===typeof a)return function(b,d,e){a(b,"set",d,e)};if("string"===typeof a&&(-1!==a.indexOf(".")||-1!==a.indexOf("[")||-1!==a.indexOf("("))){var b=function(a,d,e){var e=Ia(e),f;f=e[e.length-1];for(var g,j,i=0,n=e.length-1;i<n;i++){g=e[i].match(ba);j=e[i].match(U);if(g){e[i]=e[i].replace(ba,"");a[e[i]]=[];f=e.slice();f.splice(0,i+1);g=f.join(".");if(h.isArray(d)){j=0;for(n=d.length;j<n;j++)f={},b(f,d[j],g),a[e[i]].push(f)}else a[e[i]]=d;return}j&&(e[i]=e[i].replace(U,
""),a=a[e[i]](d));if(null===a[e[i]]||a[e[i]]===k)a[e[i]]={};a=a[e[i]]}if(f.match(U))a[f.replace(U,"")](d);else a[f.replace(ba,"")]=d};return function(c,d){return b(c,d,a)}}return function(b,d){b[a]=d}}function Ja(a){return D(a.aoData,"_aData")}function na(a){a.aoData.length=0;a.aiDisplayMaster.length=0;a.aiDisplay.length=0;a.aIds={}}function oa(a,b,c){for(var d=-1,e=0,f=a.length;e<f;e++)a[e]==b?d=e:a[e]>b&&a[e]--; -1!=d&&c===k&&a.splice(d,1)}function ca(a,b,c,d){var e=a.aoData[b],f,g=function(c,d){for(;c.childNodes.length;)c.removeChild(c.firstChild);
c.innerHTML=B(a,b,d,"display")};if("dom"===c||(!c||"auto"===c)&&"dom"===e.src)e._aData=Ha(a,e,d,d===k?k:e._aData).data;else{var j=e.anCells;if(j)if(d!==k)g(j[d],d);else{c=0;for(f=j.length;c<f;c++)g(j[c],c)}}e._aSortData=null;e._aFilterData=null;g=a.aoColumns;if(d!==k)g[d].sType=null;else{c=0;for(f=g.length;c<f;c++)g[c].sType=null;Ka(a,e)}}function Ha(a,b,c,d){var e=[],f=b.firstChild,g,j,i=0,n,l=a.aoColumns,q=a._rowReadObject,d=d!==k?d:q?{}:[],t=function(a,b){if("string"===typeof a){var c=a.indexOf("@");
-1!==c&&(c=a.substring(c+1),R(a)(d,b.getAttribute(c)))}},m=function(a){if(c===k||c===i)j=l[i],n=h.trim(a.innerHTML),j&&j._bAttrSrc?(R(j.mData._)(d,n),t(j.mData.sort,a),t(j.mData.type,a),t(j.mData.filter,a)):q?(j._setter||(j._setter=R(j.mData)),j._setter(d,n)):d[i]=n;i++};if(f)for(;f;){g=f.nodeName.toUpperCase();if("TD"==g||"TH"==g)m(f),e.push(f);f=f.nextSibling}else{e=b.anCells;f=0;for(g=e.length;f<g;f++)m(e[f])}if(b=b.firstChild?b:b.nTr)(b=b.getAttribute("id"))&&R(a.rowId)(d,b);return{data:d,cells:e}}
function Ga(a,b,c,d){var e=a.aoData[b],f=e._aData,g=[],j,i,n,l,q;if(null===e.nTr){j=c||G.createElement("tr");e.nTr=j;e.anCells=g;j._DT_RowIndex=b;Ka(a,e);l=0;for(q=a.aoColumns.length;l<q;l++){n=a.aoColumns[l];i=c?d[l]:G.createElement(n.sCellType);i._DT_CellIndex={row:b,column:l};g.push(i);if((!c||n.mRender||n.mData!==l)&&(!h.isPlainObject(n.mData)||n.mData._!==l+".display"))i.innerHTML=B(a,b,l,"display");n.sClass&&(i.className+=" "+n.sClass);n.bVisible&&!c?j.appendChild(i):!n.bVisible&&c&&i.parentNode.removeChild(i);
n.fnCreatedCell&&n.fnCreatedCell.call(a.oInstance,i,B(a,b,l),f,b,l)}r(a,"aoRowCreatedCallback",null,[j,f,b])}e.nTr.setAttribute("role","row")}function Ka(a,b){var c=b.nTr,d=b._aData;if(c){var e=a.rowIdFn(d);e&&(c.id=e);d.DT_RowClass&&(e=d.DT_RowClass.split(" "),b.__rowc=b.__rowc?qa(b.__rowc.concat(e)):e,h(c).removeClass(b.__rowc.join(" ")).addClass(d.DT_RowClass));d.DT_RowAttr&&h(c).attr(d.DT_RowAttr);d.DT_RowData&&h(c).data(d.DT_RowData)}}function jb(a){var b,c,d,e,f,g=a.nTHead,j=a.nTFoot,i=0===
h("th, td",g).length,n=a.oClasses,l=a.aoColumns;i&&(e=h("<tr/>").appendTo(g));b=0;for(c=l.length;b<c;b++)f=l[b],d=h(f.nTh).addClass(f.sClass),i&&d.appendTo(e),a.oFeatures.bSort&&(d.addClass(f.sSortingClass),!1!==f.bSortable&&(d.attr("tabindex",a.iTabIndex).attr("aria-controls",a.sTableId),La(a,f.nTh,b))),f.sTitle!=d[0].innerHTML&&d.html(f.sTitle),Ma(a,"header")(a,d,f,n);i&&da(a.aoHeader,g);h(g).find(">tr").attr("role","row");h(g).find(">tr>th, >tr>td").addClass(n.sHeaderTH);h(j).find(">tr>th, >tr>td").addClass(n.sFooterTH);
if(null!==j){a=a.aoFooter[0];b=0;for(c=a.length;b<c;b++)f=l[b],f.nTf=a[b].cell,f.sClass&&h(f.nTf).addClass(f.sClass)}}function ea(a,b,c){var d,e,f,g=[],j=[],i=a.aoColumns.length,n;if(b){c===k&&(c=!1);d=0;for(e=b.length;d<e;d++){g[d]=b[d].slice();g[d].nTr=b[d].nTr;for(f=i-1;0<=f;f--)!a.aoColumns[f].bVisible&&!c&&g[d].splice(f,1);j.push([])}d=0;for(e=g.length;d<e;d++){if(a=g[d].nTr)for(;f=a.firstChild;)a.removeChild(f);f=0;for(b=g[d].length;f<b;f++)if(n=i=1,j[d][f]===k){a.appendChild(g[d][f].cell);
for(j[d][f]=1;g[d+i]!==k&&g[d][f].cell==g[d+i][f].cell;)j[d+i][f]=1,i++;for(;g[d][f+n]!==k&&g[d][f].cell==g[d][f+n].cell;){for(c=0;c<i;c++)j[d+c][f+n]=1;n++}h(g[d][f].cell).attr("rowspan",i).attr("colspan",n)}}}}function N(a){var b=r(a,"aoPreDrawCallback","preDraw",[a]);if(-1!==h.inArray(!1,b))C(a,!1);else{var b=[],c=0,d=a.asStripeClasses,e=d.length,f=a.oLanguage,g=a.iInitDisplayStart,j="ssp"==y(a),i=a.aiDisplay;a.bDrawing=!0;g!==k&&-1!==g&&(a._iDisplayStart=j?g:g>=a.fnRecordsDisplay()?0:g,a.iInitDisplayStart=
-1);var g=a._iDisplayStart,n=a.fnDisplayEnd();if(a.bDeferLoading)a.bDeferLoading=!1,a.iDraw++,C(a,!1);else if(j){if(!a.bDestroying&&!kb(a))return}else a.iDraw++;if(0!==i.length){f=j?a.aoData.length:n;for(j=j?0:g;j<f;j++){var l=i[j],q=a.aoData[l];null===q.nTr&&Ga(a,l);l=q.nTr;if(0!==e){var t=d[c%e];q._sRowStripe!=t&&(h(l).removeClass(q._sRowStripe).addClass(t),q._sRowStripe=t)}r(a,"aoRowCallback",null,[l,q._aData,c,j]);b.push(l);c++}}else c=f.sZeroRecords,1==a.iDraw&&"ajax"==y(a)?c=f.sLoadingRecords:
f.sEmptyTable&&0===a.fnRecordsTotal()&&(c=f.sEmptyTable),b[0]=h("<tr/>",{"class":e?d[0]:""}).append(h("<td />",{valign:"top",colSpan:aa(a),"class":a.oClasses.sRowEmpty}).html(c))[0];r(a,"aoHeaderCallback","header",[h(a.nTHead).children("tr")[0],Ja(a),g,n,i]);r(a,"aoFooterCallback","footer",[h(a.nTFoot).children("tr")[0],Ja(a),g,n,i]);d=h(a.nTBody);d.children().detach();d.append(h(b));r(a,"aoDrawCallback","draw",[a]);a.bSorted=!1;a.bFiltered=!1;a.bDrawing=!1}}function S(a,b){var c=a.oFeatures,d=c.bFilter;
c.bSort&&lb(a);d?fa(a,a.oPreviousSearch):a.aiDisplay=a.aiDisplayMaster.slice();!0!==b&&(a._iDisplayStart=0);a._drawHold=b;N(a);a._drawHold=!1}function mb(a){var b=a.oClasses,c=h(a.nTable),c=h("<div/>").insertBefore(c),d=a.oFeatures,e=h("<div/>",{id:a.sTableId+"_wrapper","class":b.sWrapper+(a.nTFoot?"":" "+b.sNoFooter)});a.nHolding=c[0];a.nTableWrapper=e[0];a.nTableReinsertBefore=a.nTable.nextSibling;for(var f=a.sDom.split(""),g,j,i,n,l,q,k=0;k<f.length;k++){g=null;j=f[k];if("<"==j){i=h("<div/>")[0];
n=f[k+1];if("'"==n||'"'==n){l="";for(q=2;f[k+q]!=n;)l+=f[k+q],q++;"H"==l?l=b.sJUIHeader:"F"==l&&(l=b.sJUIFooter);-1!=l.indexOf(".")?(n=l.split("."),i.id=n[0].substr(1,n[0].length-1),i.className=n[1]):"#"==l.charAt(0)?i.id=l.substr(1,l.length-1):i.className=l;k+=q}e.append(i);e=h(i)}else if(">"==j)e=e.parent();else if("l"==j&&d.bPaginate&&d.bLengthChange)g=nb(a);else if("f"==j&&d.bFilter)g=ob(a);else if("r"==j&&d.bProcessing)g=pb(a);else if("t"==j)g=qb(a);else if("i"==j&&d.bInfo)g=rb(a);else if("p"==
j&&d.bPaginate)g=sb(a);else if(0!==m.ext.feature.length){i=m.ext.feature;q=0;for(n=i.length;q<n;q++)if(j==i[q].cFeature){g=i[q].fnInit(a);break}}g&&(i=a.aanFeatures,i[j]||(i[j]=[]),i[j].push(g),e.append(g))}c.replaceWith(e);a.nHolding=null}function da(a,b){var c=h(b).children("tr"),d,e,f,g,j,i,n,l,q,k;a.splice(0,a.length);f=0;for(i=c.length;f<i;f++)a.push([]);f=0;for(i=c.length;f<i;f++){d=c[f];for(e=d.firstChild;e;){if("TD"==e.nodeName.toUpperCase()||"TH"==e.nodeName.toUpperCase()){l=1*e.getAttribute("colspan");
q=1*e.getAttribute("rowspan");l=!l||0===l||1===l?1:l;q=!q||0===q||1===q?1:q;g=0;for(j=a[f];j[g];)g++;n=g;k=1===l?!0:!1;for(j=0;j<l;j++)for(g=0;g<q;g++)a[f+g][n+j]={cell:e,unique:k},a[f+g].nTr=d}e=e.nextSibling}}}function ra(a,b,c){var d=[];c||(c=a.aoHeader,b&&(c=[],da(c,b)));for(var b=0,e=c.length;b<e;b++)for(var f=0,g=c[b].length;f<g;f++)if(c[b][f].unique&&(!d[f]||!a.bSortCellsTop))d[f]=c[b][f].cell;return d}function sa(a,b,c){r(a,"aoServerParams","serverParams",[b]);if(b&&h.isArray(b)){var d={},
e=/(.*?)\[\]$/;h.each(b,function(a,b){var c=b.name.match(e);c?(c=c[0],d[c]||(d[c]=[]),d[c].push(b.value)):d[b.name]=b.value});b=d}var f,g=a.ajax,j=a.oInstance,i=function(b){r(a,null,"xhr",[a,b,a.jqXHR]);c(b)};if(h.isPlainObject(g)&&g.data){f=g.data;var n=h.isFunction(f)?f(b,a):f,b=h.isFunction(f)&&n?n:h.extend(!0,b,n);delete g.data}n={data:b,success:function(b){var c=b.error||b.sError;c&&J(a,0,c);a.json=b;i(b)},dataType:"json",cache:!1,type:a.sServerMethod,error:function(b,c){var d=r(a,null,"xhr",
[a,null,a.jqXHR]);-1===h.inArray(!0,d)&&("parsererror"==c?J(a,0,"Invalid JSON response",1):4===b.readyState&&J(a,0,"Ajax error",7));C(a,!1)}};a.oAjaxData=b;r(a,null,"preXhr",[a,b]);a.fnServerData?a.fnServerData.call(j,a.sAjaxSource,h.map(b,function(a,b){return{name:b,value:a}}),i,a):a.sAjaxSource||"string"===typeof g?a.jqXHR=h.ajax(h.extend(n,{url:g||a.sAjaxSource})):h.isFunction(g)?a.jqXHR=g.call(j,b,i,a):(a.jqXHR=h.ajax(h.extend(n,g)),g.data=f)}function kb(a){return a.bAjaxDataGet?(a.iDraw++,C(a,
!0),sa(a,tb(a),function(b){ub(a,b)}),!1):!0}function tb(a){var b=a.aoColumns,c=b.length,d=a.oFeatures,e=a.oPreviousSearch,f=a.aoPreSearchCols,g,j=[],i,n,l,k=V(a);g=a._iDisplayStart;i=!1!==d.bPaginate?a._iDisplayLength:-1;var t=function(a,b){j.push({name:a,value:b})};t("sEcho",a.iDraw);t("iColumns",c);t("sColumns",D(b,"sName").join(","));t("iDisplayStart",g);t("iDisplayLength",i);var pa={draw:a.iDraw,columns:[],order:[],start:g,length:i,search:{value:e.sSearch,regex:e.bRegex}};for(g=0;g<c;g++)n=b[g],
l=f[g],i="function"==typeof n.mData?"function":n.mData,pa.columns.push({data:i,name:n.sName,searchable:n.bSearchable,orderable:n.bSortable,search:{value:l.sSearch,regex:l.bRegex}}),t("mDataProp_"+g,i),d.bFilter&&(t("sSearch_"+g,l.sSearch),t("bRegex_"+g,l.bRegex),t("bSearchable_"+g,n.bSearchable)),d.bSort&&t("bSortable_"+g,n.bSortable);d.bFilter&&(t("sSearch",e.sSearch),t("bRegex",e.bRegex));d.bSort&&(h.each(k,function(a,b){pa.order.push({column:b.col,dir:b.dir});t("iSortCol_"+a,b.col);t("sSortDir_"+
a,b.dir)}),t("iSortingCols",k.length));b=m.ext.legacy.ajax;return null===b?a.sAjaxSource?j:pa:b?j:pa}function ub(a,b){var c=ta(a,b),d=b.sEcho!==k?b.sEcho:b.draw,e=b.iTotalRecords!==k?b.iTotalRecords:b.recordsTotal,f=b.iTotalDisplayRecords!==k?b.iTotalDisplayRecords:b.recordsFiltered;if(d){if(1*d<a.iDraw)return;a.iDraw=1*d}na(a);a._iRecordsTotal=parseInt(e,10);a._iRecordsDisplay=parseInt(f,10);d=0;for(e=c.length;d<e;d++)M(a,c[d]);a.aiDisplay=a.aiDisplayMaster.slice();a.bAjaxDataGet=!1;N(a);a._bInitComplete||
ua(a,b);a.bAjaxDataGet=!0;C(a,!1)}function ta(a,b){var c=h.isPlainObject(a.ajax)&&a.ajax.dataSrc!==k?a.ajax.dataSrc:a.sAjaxDataProp;return"data"===c?b.aaData||b[c]:""!==c?Q(c)(b):b}function ob(a){var b=a.oClasses,c=a.sTableId,d=a.oLanguage,e=a.oPreviousSearch,f=a.aanFeatures,g='<input type="search" class="'+b.sFilterInput+'"/>',j=d.sSearch,j=j.match(/_INPUT_/)?j.replace("_INPUT_",g):j+g,b=h("<div/>",{id:!f.f?c+"_filter":null,"class":b.sFilter}).append(h("<label/>").append(j)),f=function(){var b=!this.value?
"":this.value;b!=e.sSearch&&(fa(a,{sSearch:b,bRegex:e.bRegex,bSmart:e.bSmart,bCaseInsensitive:e.bCaseInsensitive}),a._iDisplayStart=0,N(a))},g=null!==a.searchDelay?a.searchDelay:"ssp"===y(a)?400:0,i=h("input",b).val(e.sSearch).attr("placeholder",d.sSearchPlaceholder).on("keyup.DT search.DT input.DT paste.DT cut.DT",g?Na(f,g):f).on("keypress.DT",function(a){if(13==a.keyCode)return!1}).attr("aria-controls",c);h(a.nTable).on("search.dt.DT",function(b,c){if(a===c)try{i[0]!==G.activeElement&&i.val(e.sSearch)}catch(d){}});
return b[0]}function fa(a,b,c){var d=a.oPreviousSearch,e=a.aoPreSearchCols,f=function(a){d.sSearch=a.sSearch;d.bRegex=a.bRegex;d.bSmart=a.bSmart;d.bCaseInsensitive=a.bCaseInsensitive};Fa(a);if("ssp"!=y(a)){vb(a,b.sSearch,c,b.bEscapeRegex!==k?!b.bEscapeRegex:b.bRegex,b.bSmart,b.bCaseInsensitive);f(b);for(b=0;b<e.length;b++)wb(a,e[b].sSearch,b,e[b].bEscapeRegex!==k?!e[b].bEscapeRegex:e[b].bRegex,e[b].bSmart,e[b].bCaseInsensitive);xb(a)}else f(b);a.bFiltered=!0;r(a,null,"search",[a])}function xb(a){for(var b=
m.ext.search,c=a.aiDisplay,d,e,f=0,g=b.length;f<g;f++){for(var j=[],i=0,n=c.length;i<n;i++)e=c[i],d=a.aoData[e],b[f](a,d._aFilterData,e,d._aData,i)&&j.push(e);c.length=0;h.merge(c,j)}}function wb(a,b,c,d,e,f){if(""!==b){for(var g=[],j=a.aiDisplay,d=Oa(b,d,e,f),e=0;e<j.length;e++)b=a.aoData[j[e]]._aFilterData[c],d.test(b)&&g.push(j[e]);a.aiDisplay=g}}function vb(a,b,c,d,e,f){var d=Oa(b,d,e,f),f=a.oPreviousSearch.sSearch,g=a.aiDisplayMaster,j,e=[];0!==m.ext.search.length&&(c=!0);j=yb(a);if(0>=b.length)a.aiDisplay=
g.slice();else{if(j||c||f.length>b.length||0!==b.indexOf(f)||a.bSorted)a.aiDisplay=g.slice();b=a.aiDisplay;for(c=0;c<b.length;c++)d.test(a.aoData[b[c]]._sFilterRow)&&e.push(b[c]);a.aiDisplay=e}}function Oa(a,b,c,d){a=b?a:Pa(a);c&&(a="^(?=.*?"+h.map(a.match(/"[^"]+"|[^ ]+/g)||[""],function(a){if('"'===a.charAt(0))var b=a.match(/^"(.*)"$/),a=b?b[1]:a;return a.replace('"',"")}).join(")(?=.*?")+").*$");return RegExp(a,d?"i":"")}function yb(a){var b=a.aoColumns,c,d,e,f,g,j,i,h,l=m.ext.type.search;c=!1;
d=0;for(f=a.aoData.length;d<f;d++)if(h=a.aoData[d],!h._aFilterData){j=[];e=0;for(g=b.length;e<g;e++)c=b[e],c.bSearchable?(i=B(a,d,e,"filter"),l[c.sType]&&(i=l[c.sType](i)),null===i&&(i=""),"string"!==typeof i&&i.toString&&(i=i.toString())):i="",i.indexOf&&-1!==i.indexOf("&")&&(va.innerHTML=i,i=Wb?va.textContent:va.innerText),i.replace&&(i=i.replace(/[\r\n]/g,"")),j.push(i);h._aFilterData=j;h._sFilterRow=j.join(" ");c=!0}return c}function zb(a){return{search:a.sSearch,smart:a.bSmart,regex:a.bRegex,
caseInsensitive:a.bCaseInsensitive}}function Ab(a){return{sSearch:a.search,bSmart:a.smart,bRegex:a.regex,bCaseInsensitive:a.caseInsensitive}}function rb(a){var b=a.sTableId,c=a.aanFeatures.i,d=h("<div/>",{"class":a.oClasses.sInfo,id:!c?b+"_info":null});c||(a.aoDrawCallback.push({fn:Bb,sName:"information"}),d.attr("role","status").attr("aria-live","polite"),h(a.nTable).attr("aria-describedby",b+"_info"));return d[0]}function Bb(a){var b=a.aanFeatures.i;if(0!==b.length){var c=a.oLanguage,d=a._iDisplayStart+
1,e=a.fnDisplayEnd(),f=a.fnRecordsTotal(),g=a.fnRecordsDisplay(),j=g?c.sInfo:c.sInfoEmpty;g!==f&&(j+=" "+c.sInfoFiltered);j+=c.sInfoPostFix;j=Cb(a,j);c=c.fnInfoCallback;null!==c&&(j=c.call(a.oInstance,a,d,e,f,g,j));h(b).html(j)}}function Cb(a,b){var c=a.fnFormatNumber,d=a._iDisplayStart+1,e=a._iDisplayLength,f=a.fnRecordsDisplay(),g=-1===e;return b.replace(/_START_/g,c.call(a,d)).replace(/_END_/g,c.call(a,a.fnDisplayEnd())).replace(/_MAX_/g,c.call(a,a.fnRecordsTotal())).replace(/_TOTAL_/g,c.call(a,
f)).replace(/_PAGE_/g,c.call(a,g?1:Math.ceil(d/e))).replace(/_PAGES_/g,c.call(a,g?1:Math.ceil(f/e)))}function ga(a){var b,c,d=a.iInitDisplayStart,e=a.aoColumns,f;c=a.oFeatures;var g=a.bDeferLoading;if(a.bInitialised){mb(a);jb(a);ea(a,a.aoHeader);ea(a,a.aoFooter);C(a,!0);c.bAutoWidth&&Ea(a);b=0;for(c=e.length;b<c;b++)f=e[b],f.sWidth&&(f.nTh.style.width=v(f.sWidth));r(a,null,"preInit",[a]);S(a);e=y(a);if("ssp"!=e||g)"ajax"==e?sa(a,[],function(c){var f=ta(a,c);for(b=0;b<f.length;b++)M(a,f[b]);a.iInitDisplayStart=
d;S(a);C(a,!1);ua(a,c)},a):(C(a,!1),ua(a))}else setTimeout(function(){ga(a)},200)}function ua(a,b){a._bInitComplete=!0;(b||a.oInit.aaData)&&Y(a);r(a,null,"plugin-init",[a,b]);r(a,"aoInitComplete","init",[a,b])}function Qa(a,b){var c=parseInt(b,10);a._iDisplayLength=c;Ra(a);r(a,null,"length",[a,c])}function nb(a){for(var b=a.oClasses,c=a.sTableId,d=a.aLengthMenu,e=h.isArray(d[0]),f=e?d[0]:d,d=e?d[1]:d,e=h("<select/>",{name:c+"_length","aria-controls":c,"class":b.sLengthSelect}),g=0,j=f.length;g<j;g++)e[0][g]=
new Option("number"===typeof d[g]?a.fnFormatNumber(d[g]):d[g],f[g]);var i=h("<div><label/></div>").addClass(b.sLength);a.aanFeatures.l||(i[0].id=c+"_length");i.children().append(a.oLanguage.sLengthMenu.replace("_MENU_",e[0].outerHTML));h("select",i).val(a._iDisplayLength).on("change.DT",function(){Qa(a,h(this).val());N(a)});h(a.nTable).on("length.dt.DT",function(b,c,d){a===c&&h("select",i).val(d)});return i[0]}function sb(a){var b=a.sPaginationType,c=m.ext.pager[b],d="function"===typeof c,e=function(a){N(a)},
b=h("<div/>").addClass(a.oClasses.sPaging+b)[0],f=a.aanFeatures;d||c.fnInit(a,b,e);f.p||(b.id=a.sTableId+"_paginate",a.aoDrawCallback.push({fn:function(a){if(d){var b=a._iDisplayStart,i=a._iDisplayLength,h=a.fnRecordsDisplay(),l=-1===i,b=l?0:Math.ceil(b/i),i=l?1:Math.ceil(h/i),h=c(b,i),k,l=0;for(k=f.p.length;l<k;l++)Ma(a,"pageButton")(a,f.p[l],l,h,b,i)}else c.fnUpdate(a,e)},sName:"pagination"}));return b}function Sa(a,b,c){var d=a._iDisplayStart,e=a._iDisplayLength,f=a.fnRecordsDisplay();0===f||-1===
e?d=0:"number"===typeof b?(d=b*e,d>f&&(d=0)):"first"==b?d=0:"previous"==b?(d=0<=e?d-e:0,0>d&&(d=0)):"next"==b?d+e<f&&(d+=e):"last"==b?d=Math.floor((f-1)/e)*e:J(a,0,"Unknown paging action: "+b,5);b=a._iDisplayStart!==d;a._iDisplayStart=d;b&&(r(a,null,"page",[a]),c&&N(a));return b}function pb(a){return h("<div/>",{id:!a.aanFeatures.r?a.sTableId+"_processing":null,"class":a.oClasses.sProcessing}).html(a.oLanguage.sProcessing).insertBefore(a.nTable)[0]}function C(a,b){a.oFeatures.bProcessing&&h(a.aanFeatures.r).css("display",
b?"block":"none");r(a,null,"processing",[a,b])}function qb(a){var b=h(a.nTable);b.attr("role","grid");var c=a.oScroll;if(""===c.sX&&""===c.sY)return a.nTable;var d=c.sX,e=c.sY,f=a.oClasses,g=b.children("caption"),j=g.length?g[0]._captionSide:null,i=h(b[0].cloneNode(!1)),n=h(b[0].cloneNode(!1)),l=b.children("tfoot");l.length||(l=null);i=h("<div/>",{"class":f.sScrollWrapper}).append(h("<div/>",{"class":f.sScrollHead}).css({overflow:"hidden",position:"relative",border:0,width:d?!d?null:v(d):"100%"}).append(h("<div/>",
{"class":f.sScrollHeadInner}).css({"box-sizing":"content-box",width:c.sXInner||"100%"}).append(i.removeAttr("id").css("margin-left",0).append("top"===j?g:null).append(b.children("thead"))))).append(h("<div/>",{"class":f.sScrollBody}).css({position:"relative",overflow:"auto",width:!d?null:v(d)}).append(b));l&&i.append(h("<div/>",{"class":f.sScrollFoot}).css({overflow:"hidden",border:0,width:d?!d?null:v(d):"100%"}).append(h("<div/>",{"class":f.sScrollFootInner}).append(n.removeAttr("id").css("margin-left",
0).append("bottom"===j?g:null).append(b.children("tfoot")))));var b=i.children(),k=b[0],f=b[1],t=l?b[2]:null;if(d)h(f).on("scroll.DT",function(){var a=this.scrollLeft;k.scrollLeft=a;l&&(t.scrollLeft=a)});h(f).css(e&&c.bCollapse?"max-height":"height",e);a.nScrollHead=k;a.nScrollBody=f;a.nScrollFoot=t;a.aoDrawCallback.push({fn:ka,sName:"scrolling"});return i[0]}function ka(a){var b=a.oScroll,c=b.sX,d=b.sXInner,e=b.sY,b=b.iBarWidth,f=h(a.nScrollHead),g=f[0].style,j=f.children("div"),i=j[0].style,n=j.children("table"),
j=a.nScrollBody,l=h(j),q=j.style,t=h(a.nScrollFoot).children("div"),m=t.children("table"),o=h(a.nTHead),p=h(a.nTable),s=p[0],r=s.style,u=a.nTFoot?h(a.nTFoot):null,x=a.oBrowser,T=x.bScrollOversize,Xb=D(a.aoColumns,"nTh"),O,K,P,w,Ta=[],y=[],z=[],A=[],B,C=function(a){a=a.style;a.paddingTop="0";a.paddingBottom="0";a.borderTopWidth="0";a.borderBottomWidth="0";a.height=0};K=j.scrollHeight>j.clientHeight;if(a.scrollBarVis!==K&&a.scrollBarVis!==k)a.scrollBarVis=K,Y(a);else{a.scrollBarVis=K;p.children("thead, tfoot").remove();
u&&(P=u.clone().prependTo(p),O=u.find("tr"),P=P.find("tr"));w=o.clone().prependTo(p);o=o.find("tr");K=w.find("tr");w.find("th, td").removeAttr("tabindex");c||(q.width="100%",f[0].style.width="100%");h.each(ra(a,w),function(b,c){B=Z(a,b);c.style.width=a.aoColumns[B].sWidth});u&&H(function(a){a.style.width=""},P);f=p.outerWidth();if(""===c){r.width="100%";if(T&&(p.find("tbody").height()>j.offsetHeight||"scroll"==l.css("overflow-y")))r.width=v(p.outerWidth()-b);f=p.outerWidth()}else""!==d&&(r.width=
v(d),f=p.outerWidth());H(C,K);H(function(a){z.push(a.innerHTML);Ta.push(v(h(a).css("width")))},K);H(function(a,b){if(h.inArray(a,Xb)!==-1)a.style.width=Ta[b]},o);h(K).height(0);u&&(H(C,P),H(function(a){A.push(a.innerHTML);y.push(v(h(a).css("width")))},P),H(function(a,b){a.style.width=y[b]},O),h(P).height(0));H(function(a,b){a.innerHTML='<div class="dataTables_sizing" style="height:0;overflow:hidden;">'+z[b]+"</div>";a.style.width=Ta[b]},K);u&&H(function(a,b){a.innerHTML='<div class="dataTables_sizing" style="height:0;overflow:hidden;">'+
A[b]+"</div>";a.style.width=y[b]},P);if(p.outerWidth()<f){O=j.scrollHeight>j.offsetHeight||"scroll"==l.css("overflow-y")?f+b:f;if(T&&(j.scrollHeight>j.offsetHeight||"scroll"==l.css("overflow-y")))r.width=v(O-b);(""===c||""!==d)&&J(a,1,"Possible column misalignment",6)}else O="100%";q.width=v(O);g.width=v(O);u&&(a.nScrollFoot.style.width=v(O));!e&&T&&(q.height=v(s.offsetHeight+b));c=p.outerWidth();n[0].style.width=v(c);i.width=v(c);d=p.height()>j.clientHeight||"scroll"==l.css("overflow-y");e="padding"+
(x.bScrollbarLeft?"Left":"Right");i[e]=d?b+"px":"0px";u&&(m[0].style.width=v(c),t[0].style.width=v(c),t[0].style[e]=d?b+"px":"0px");p.children("colgroup").insertBefore(p.children("thead"));l.scroll();if((a.bSorted||a.bFiltered)&&!a._drawHold)j.scrollTop=0}}function H(a,b,c){for(var d=0,e=0,f=b.length,g,j;e<f;){g=b[e].firstChild;for(j=c?c[e].firstChild:null;g;)1===g.nodeType&&(c?a(g,j,d):a(g,d),d++),g=g.nextSibling,j=c?j.nextSibling:null;e++}}function Ea(a){var b=a.nTable,c=a.aoColumns,d=a.oScroll,
e=d.sY,f=d.sX,g=d.sXInner,j=c.length,i=la(a,"bVisible"),n=h("th",a.nTHead),l=b.getAttribute("width"),k=b.parentNode,t=!1,m,o,p=a.oBrowser,d=p.bScrollOversize;(m=b.style.width)&&-1!==m.indexOf("%")&&(l=m);for(m=0;m<i.length;m++)o=c[i[m]],null!==o.sWidth&&(o.sWidth=Db(o.sWidthOrig,k),t=!0);if(d||!t&&!f&&!e&&j==aa(a)&&j==n.length)for(m=0;m<j;m++)i=Z(a,m),null!==i&&(c[i].sWidth=v(n.eq(m).width()));else{j=h(b).clone().css("visibility","hidden").removeAttr("id");j.find("tbody tr").remove();var s=h("<tr/>").appendTo(j.find("tbody"));
j.find("thead, tfoot").remove();j.append(h(a.nTHead).clone()).append(h(a.nTFoot).clone());j.find("tfoot th, tfoot td").css("width","");n=ra(a,j.find("thead")[0]);for(m=0;m<i.length;m++)o=c[i[m]],n[m].style.width=null!==o.sWidthOrig&&""!==o.sWidthOrig?v(o.sWidthOrig):"",o.sWidthOrig&&f&&h(n[m]).append(h("<div/>").css({width:o.sWidthOrig,margin:0,padding:0,border:0,height:1}));if(a.aoData.length)for(m=0;m<i.length;m++)t=i[m],o=c[t],h(Eb(a,t)).clone(!1).append(o.sContentPadding).appendTo(s);h("[name]",
j).removeAttr("name");o=h("<div/>").css(f||e?{position:"absolute",top:0,left:0,height:1,right:0,overflow:"hidden"}:{}).append(j).appendTo(k);f&&g?j.width(g):f?(j.css("width","auto"),j.removeAttr("width"),j.width()<k.clientWidth&&l&&j.width(k.clientWidth)):e?j.width(k.clientWidth):l&&j.width(l);for(m=e=0;m<i.length;m++)k=h(n[m]),g=k.outerWidth()-k.width(),k=p.bBounding?Math.ceil(n[m].getBoundingClientRect().width):k.outerWidth(),e+=k,c[i[m]].sWidth=v(k-g);b.style.width=v(e);o.remove()}l&&(b.style.width=
v(l));if((l||f)&&!a._reszEvt)b=function(){h(E).on("resize.DT-"+a.sInstance,Na(function(){Y(a)}))},d?setTimeout(b,1E3):b(),a._reszEvt=!0}function Db(a,b){if(!a)return 0;var c=h("<div/>").css("width",v(a)).appendTo(b||G.body),d=c[0].offsetWidth;c.remove();return d}function Eb(a,b){var c=Fb(a,b);if(0>c)return null;var d=a.aoData[c];return!d.nTr?h("<td/>").html(B(a,c,b,"display"))[0]:d.anCells[b]}function Fb(a,b){for(var c,d=-1,e=-1,f=0,g=a.aoData.length;f<g;f++)c=B(a,f,b,"display")+"",c=c.replace(Yb,
""),c=c.replace(/&nbsp;/g," "),c.length>d&&(d=c.length,e=f);return e}function v(a){return null===a?"0px":"number"==typeof a?0>a?"0px":a+"px":a.match(/\d$/)?a+"px":a}function V(a){var b,c,d=[],e=a.aoColumns,f,g,j,i;b=a.aaSortingFixed;c=h.isPlainObject(b);var n=[];f=function(a){a.length&&!h.isArray(a[0])?n.push(a):h.merge(n,a)};h.isArray(b)&&f(b);c&&b.pre&&f(b.pre);f(a.aaSorting);c&&b.post&&f(b.post);for(a=0;a<n.length;a++){i=n[a][0];f=e[i].aDataSort;b=0;for(c=f.length;b<c;b++)g=f[b],j=e[g].sType||
"string",n[a]._idx===k&&(n[a]._idx=h.inArray(n[a][1],e[g].asSorting)),d.push({src:i,col:g,dir:n[a][1],index:n[a]._idx,type:j,formatter:m.ext.type.order[j+"-pre"]})}return d}function lb(a){var b,c,d=[],e=m.ext.type.order,f=a.aoData,g=0,j,i=a.aiDisplayMaster,h;Fa(a);h=V(a);b=0;for(c=h.length;b<c;b++)j=h[b],j.formatter&&g++,Gb(a,j.col);if("ssp"!=y(a)&&0!==h.length){b=0;for(c=i.length;b<c;b++)d[i[b]]=b;g===h.length?i.sort(function(a,b){var c,e,g,j,i=h.length,k=f[a]._aSortData,m=f[b]._aSortData;for(g=
0;g<i;g++)if(j=h[g],c=k[j.col],e=m[j.col],c=c<e?-1:c>e?1:0,0!==c)return"asc"===j.dir?c:-c;c=d[a];e=d[b];return c<e?-1:c>e?1:0}):i.sort(function(a,b){var c,g,j,i,k=h.length,m=f[a]._aSortData,o=f[b]._aSortData;for(j=0;j<k;j++)if(i=h[j],c=m[i.col],g=o[i.col],i=e[i.type+"-"+i.dir]||e["string-"+i.dir],c=i(c,g),0!==c)return c;c=d[a];g=d[b];return c<g?-1:c>g?1:0})}a.bSorted=!0}function Hb(a){for(var b,c,d=a.aoColumns,e=V(a),a=a.oLanguage.oAria,f=0,g=d.length;f<g;f++){c=d[f];var j=c.asSorting;b=c.sTitle.replace(/<.*?>/g,
"");var i=c.nTh;i.removeAttribute("aria-sort");c.bSortable&&(0<e.length&&e[0].col==f?(i.setAttribute("aria-sort","asc"==e[0].dir?"ascending":"descending"),c=j[e[0].index+1]||j[0]):c=j[0],b+="asc"===c?a.sSortAscending:a.sSortDescending);i.setAttribute("aria-label",b)}}function Ua(a,b,c,d){var e=a.aaSorting,f=a.aoColumns[b].asSorting,g=function(a,b){var c=a._idx;c===k&&(c=h.inArray(a[1],f));return c+1<f.length?c+1:b?null:0};"number"===typeof e[0]&&(e=a.aaSorting=[e]);c&&a.oFeatures.bSortMulti?(c=h.inArray(b,
D(e,"0")),-1!==c?(b=g(e[c],!0),null===b&&1===e.length&&(b=0),null===b?e.splice(c,1):(e[c][1]=f[b],e[c]._idx=b)):(e.push([b,f[0],0]),e[e.length-1]._idx=0)):e.length&&e[0][0]==b?(b=g(e[0]),e.length=1,e[0][1]=f[b],e[0]._idx=b):(e.length=0,e.push([b,f[0]]),e[0]._idx=0);S(a);"function"==typeof d&&d(a)}function La(a,b,c,d){var e=a.aoColumns[c];Va(b,{},function(b){!1!==e.bSortable&&(a.oFeatures.bProcessing?(C(a,!0),setTimeout(function(){Ua(a,c,b.shiftKey,d);"ssp"!==y(a)&&C(a,!1)},0)):Ua(a,c,b.shiftKey,d))})}
function wa(a){var b=a.aLastSort,c=a.oClasses.sSortColumn,d=V(a),e=a.oFeatures,f,g;if(e.bSort&&e.bSortClasses){e=0;for(f=b.length;e<f;e++)g=b[e].src,h(D(a.aoData,"anCells",g)).removeClass(c+(2>e?e+1:3));e=0;for(f=d.length;e<f;e++)g=d[e].src,h(D(a.aoData,"anCells",g)).addClass(c+(2>e?e+1:3))}a.aLastSort=d}function Gb(a,b){var c=a.aoColumns[b],d=m.ext.order[c.sSortDataType],e;d&&(e=d.call(a.oInstance,a,b,$(a,b)));for(var f,g=m.ext.type.order[c.sType+"-pre"],j=0,i=a.aoData.length;j<i;j++)if(c=a.aoData[j],
c._aSortData||(c._aSortData=[]),!c._aSortData[b]||d)f=d?e[j]:B(a,j,b,"sort"),c._aSortData[b]=g?g(f):f}function xa(a){if(a.oFeatures.bStateSave&&!a.bDestroying){var b={time:+new Date,start:a._iDisplayStart,length:a._iDisplayLength,order:h.extend(!0,[],a.aaSorting),search:zb(a.oPreviousSearch),columns:h.map(a.aoColumns,function(b,d){return{visible:b.bVisible,search:zb(a.aoPreSearchCols[d])}})};r(a,"aoStateSaveParams","stateSaveParams",[a,b]);a.oSavedState=b;a.fnStateSaveCallback.call(a.oInstance,a,
b)}}function Ib(a,b,c){var d,e,f=a.aoColumns,b=function(b){if(b&&b.time){var g=r(a,"aoStateLoadParams","stateLoadParams",[a,b]);if(-1===h.inArray(!1,g)&&(g=a.iStateDuration,!(0<g&&b.time<+new Date-1E3*g)&&!(b.columns&&f.length!==b.columns.length))){a.oLoadedState=h.extend(!0,{},b);b.start!==k&&(a._iDisplayStart=b.start,a.iInitDisplayStart=b.start);b.length!==k&&(a._iDisplayLength=b.length);b.order!==k&&(a.aaSorting=[],h.each(b.order,function(b,c){a.aaSorting.push(c[0]>=f.length?[0,c[1]]:c)}));b.search!==
k&&h.extend(a.oPreviousSearch,Ab(b.search));if(b.columns){d=0;for(e=b.columns.length;d<e;d++)g=b.columns[d],g.visible!==k&&(f[d].bVisible=g.visible),g.search!==k&&h.extend(a.aoPreSearchCols[d],Ab(g.search))}r(a,"aoStateLoaded","stateLoaded",[a,b])}}c()};if(a.oFeatures.bStateSave){var g=a.fnStateLoadCallback.call(a.oInstance,a,b);g!==k&&b(g)}else c()}function ya(a){var b=m.settings,a=h.inArray(a,D(b,"nTable"));return-1!==a?b[a]:null}function J(a,b,c,d){c="DataTables warning: "+(a?"table id="+a.sTableId+
" - ":"")+c;d&&(c+=". For more information about this error, please see http://datatables.net/tn/"+d);if(b)E.console&&console.log&&console.log(c);else if(b=m.ext,b=b.sErrMode||b.errMode,a&&r(a,null,"error",[a,d,c]),"alert"==b)alert(c);else{if("throw"==b)throw Error(c);"function"==typeof b&&b(a,d,c)}}function F(a,b,c,d){h.isArray(c)?h.each(c,function(c,d){h.isArray(d)?F(a,b,d[0],d[1]):F(a,b,d)}):(d===k&&(d=c),b[c]!==k&&(a[d]=b[c]))}function Jb(a,b,c){var d,e;for(e in b)b.hasOwnProperty(e)&&(d=b[e],
h.isPlainObject(d)?(h.isPlainObject(a[e])||(a[e]={}),h.extend(!0,a[e],d)):a[e]=c&&"data"!==e&&"aaData"!==e&&h.isArray(d)?d.slice():d);return a}function Va(a,b,c){h(a).on("click.DT",b,function(b){a.blur();c(b)}).on("keypress.DT",b,function(a){13===a.which&&(a.preventDefault(),c(a))}).on("selectstart.DT",function(){return!1})}function z(a,b,c,d){c&&a[b].push({fn:c,sName:d})}function r(a,b,c,d){var e=[];b&&(e=h.map(a[b].slice().reverse(),function(b){return b.fn.apply(a.oInstance,d)}));null!==c&&(b=h.Event(c+
".dt"),h(a.nTable).trigger(b,d),e.push(b.result));return e}function Ra(a){var b=a._iDisplayStart,c=a.fnDisplayEnd(),d=a._iDisplayLength;b>=c&&(b=c-d);b-=b%d;if(-1===d||0>b)b=0;a._iDisplayStart=b}function Ma(a,b){var c=a.renderer,d=m.ext.renderer[b];return h.isPlainObject(c)&&c[b]?d[c[b]]||d._:"string"===typeof c?d[c]||d._:d._}function y(a){return a.oFeatures.bServerSide?"ssp":a.ajax||a.sAjaxSource?"ajax":"dom"}function ha(a,b){var c=[],c=Kb.numbers_length,d=Math.floor(c/2);b<=c?c=W(0,b):a<=d?(c=W(0,
c-2),c.push("ellipsis"),c.push(b-1)):(a>=b-1-d?c=W(b-(c-2),b):(c=W(a-d+2,a+d-1),c.push("ellipsis"),c.push(b-1)),c.splice(0,0,"ellipsis"),c.splice(0,0,0));c.DT_el="span";return c}function cb(a){h.each({num:function(b){return za(b,a)},"num-fmt":function(b){return za(b,a,Wa)},"html-num":function(b){return za(b,a,Aa)},"html-num-fmt":function(b){return za(b,a,Aa,Wa)}},function(b,c){x.type.order[b+a+"-pre"]=c;b.match(/^html\-/)&&(x.type.search[b+a]=x.type.search.html)})}function Lb(a){return function(){var b=
[ya(this[m.ext.iApiIndex])].concat(Array.prototype.slice.call(arguments));return m.ext.internal[a].apply(this,b)}}var m=function(a){this.$=function(a,b){return this.api(!0).$(a,b)};this._=function(a,b){return this.api(!0).rows(a,b).data()};this.api=function(a){return a?new s(ya(this[x.iApiIndex])):new s(this)};this.fnAddData=function(a,b){var c=this.api(!0),d=h.isArray(a)&&(h.isArray(a[0])||h.isPlainObject(a[0]))?c.rows.add(a):c.row.add(a);(b===k||b)&&c.draw();return d.flatten().toArray()};this.fnAdjustColumnSizing=
function(a){var b=this.api(!0).columns.adjust(),c=b.settings()[0],d=c.oScroll;a===k||a?b.draw(!1):(""!==d.sX||""!==d.sY)&&ka(c)};this.fnClearTable=function(a){var b=this.api(!0).clear();(a===k||a)&&b.draw()};this.fnClose=function(a){this.api(!0).row(a).child.hide()};this.fnDeleteRow=function(a,b,c){var d=this.api(!0),a=d.rows(a),e=a.settings()[0],h=e.aoData[a[0][0]];a.remove();b&&b.call(this,e,h);(c===k||c)&&d.draw();return h};this.fnDestroy=function(a){this.api(!0).destroy(a)};this.fnDraw=function(a){this.api(!0).draw(a)};
this.fnFilter=function(a,b,c,d,e,h){e=this.api(!0);null===b||b===k?e.search(a,c,d,h):e.column(b).search(a,c,d,h);e.draw()};this.fnGetData=function(a,b){var c=this.api(!0);if(a!==k){var d=a.nodeName?a.nodeName.toLowerCase():"";return b!==k||"td"==d||"th"==d?c.cell(a,b).data():c.row(a).data()||null}return c.data().toArray()};this.fnGetNodes=function(a){var b=this.api(!0);return a!==k?b.row(a).node():b.rows().nodes().flatten().toArray()};this.fnGetPosition=function(a){var b=this.api(!0),c=a.nodeName.toUpperCase();
return"TR"==c?b.row(a).index():"TD"==c||"TH"==c?(a=b.cell(a).index(),[a.row,a.columnVisible,a.column]):null};this.fnIsOpen=function(a){return this.api(!0).row(a).child.isShown()};this.fnOpen=function(a,b,c){return this.api(!0).row(a).child(b,c).show().child()[0]};this.fnPageChange=function(a,b){var c=this.api(!0).page(a);(b===k||b)&&c.draw(!1)};this.fnSetColumnVis=function(a,b,c){a=this.api(!0).column(a).visible(b);(c===k||c)&&a.columns.adjust().draw()};this.fnSettings=function(){return ya(this[x.iApiIndex])};
this.fnSort=function(a){this.api(!0).order(a).draw()};this.fnSortListener=function(a,b,c){this.api(!0).order.listener(a,b,c)};this.fnUpdate=function(a,b,c,d,e){var h=this.api(!0);c===k||null===c?h.row(b).data(a):h.cell(b,c).data(a);(e===k||e)&&h.columns.adjust();(d===k||d)&&h.draw();return 0};this.fnVersionCheck=x.fnVersionCheck;var b=this,c=a===k,d=this.length;c&&(a={});this.oApi=this.internal=x.internal;for(var e in m.ext.internal)e&&(this[e]=Lb(e));this.each(function(){var e={},g=1<d?Jb(e,a,!0):
a,j=0,i,e=this.getAttribute("id"),n=!1,l=m.defaults,q=h(this);if("table"!=this.nodeName.toLowerCase())J(null,0,"Non-table node initialisation ("+this.nodeName+")",2);else{db(l);eb(l.column);I(l,l,!0);I(l.column,l.column,!0);I(l,h.extend(g,q.data()));var t=m.settings,j=0;for(i=t.length;j<i;j++){var o=t[j];if(o.nTable==this||o.nTHead.parentNode==this||o.nTFoot&&o.nTFoot.parentNode==this){var s=g.bRetrieve!==k?g.bRetrieve:l.bRetrieve;if(c||s)return o.oInstance;if(g.bDestroy!==k?g.bDestroy:l.bDestroy){o.oInstance.fnDestroy();
break}else{J(o,0,"Cannot reinitialise DataTable",3);return}}if(o.sTableId==this.id){t.splice(j,1);break}}if(null===e||""===e)this.id=e="DataTables_Table_"+m.ext._unique++;var p=h.extend(!0,{},m.models.oSettings,{sDestroyWidth:q[0].style.width,sInstance:e,sTableId:e});p.nTable=this;p.oApi=b.internal;p.oInit=g;t.push(p);p.oInstance=1===b.length?b:q.dataTable();db(g);g.oLanguage&&Ca(g.oLanguage);g.aLengthMenu&&!g.iDisplayLength&&(g.iDisplayLength=h.isArray(g.aLengthMenu[0])?g.aLengthMenu[0][0]:g.aLengthMenu[0]);
g=Jb(h.extend(!0,{},l),g);F(p.oFeatures,g,"bPaginate bLengthChange bFilter bSort bSortMulti bInfo bProcessing bAutoWidth bSortClasses bServerSide bDeferRender".split(" "));F(p,g,["asStripeClasses","ajax","fnServerData","fnFormatNumber","sServerMethod","aaSorting","aaSortingFixed","aLengthMenu","sPaginationType","sAjaxSource","sAjaxDataProp","iStateDuration","sDom","bSortCellsTop","iTabIndex","fnStateLoadCallback","fnStateSaveCallback","renderer","searchDelay","rowId",["iCookieDuration","iStateDuration"],
["oSearch","oPreviousSearch"],["aoSearchCols","aoPreSearchCols"],["iDisplayLength","_iDisplayLength"]]);F(p.oScroll,g,[["sScrollX","sX"],["sScrollXInner","sXInner"],["sScrollY","sY"],["bScrollCollapse","bCollapse"]]);F(p.oLanguage,g,"fnInfoCallback");z(p,"aoDrawCallback",g.fnDrawCallback,"user");z(p,"aoServerParams",g.fnServerParams,"user");z(p,"aoStateSaveParams",g.fnStateSaveParams,"user");z(p,"aoStateLoadParams",g.fnStateLoadParams,"user");z(p,"aoStateLoaded",g.fnStateLoaded,"user");z(p,"aoRowCallback",
g.fnRowCallback,"user");z(p,"aoRowCreatedCallback",g.fnCreatedRow,"user");z(p,"aoHeaderCallback",g.fnHeaderCallback,"user");z(p,"aoFooterCallback",g.fnFooterCallback,"user");z(p,"aoInitComplete",g.fnInitComplete,"user");z(p,"aoPreDrawCallback",g.fnPreDrawCallback,"user");p.rowIdFn=Q(g.rowId);fb(p);var u=p.oClasses;h.extend(u,m.ext.classes,g.oClasses);q.addClass(u.sTable);p.iInitDisplayStart===k&&(p.iInitDisplayStart=g.iDisplayStart,p._iDisplayStart=g.iDisplayStart);null!==g.iDeferLoading&&(p.bDeferLoading=
!0,e=h.isArray(g.iDeferLoading),p._iRecordsDisplay=e?g.iDeferLoading[0]:g.iDeferLoading,p._iRecordsTotal=e?g.iDeferLoading[1]:g.iDeferLoading);var v=p.oLanguage;h.extend(!0,v,g.oLanguage);v.sUrl&&(h.ajax({dataType:"json",url:v.sUrl,success:function(a){Ca(a);I(l.oLanguage,a);h.extend(true,v,a);ga(p)},error:function(){ga(p)}}),n=!0);null===g.asStripeClasses&&(p.asStripeClasses=[u.sStripeOdd,u.sStripeEven]);var e=p.asStripeClasses,x=q.children("tbody").find("tr").eq(0);-1!==h.inArray(!0,h.map(e,function(a){return x.hasClass(a)}))&&
(h("tbody tr",this).removeClass(e.join(" ")),p.asDestroyStripes=e.slice());e=[];t=this.getElementsByTagName("thead");0!==t.length&&(da(p.aoHeader,t[0]),e=ra(p));if(null===g.aoColumns){t=[];j=0;for(i=e.length;j<i;j++)t.push(null)}else t=g.aoColumns;j=0;for(i=t.length;j<i;j++)Da(p,e?e[j]:null);hb(p,g.aoColumnDefs,t,function(a,b){ja(p,a,b)});if(x.length){var w=function(a,b){return a.getAttribute("data-"+b)!==null?b:null};h(x[0]).children("th, td").each(function(a,b){var c=p.aoColumns[a];if(c.mData===
a){var d=w(b,"sort")||w(b,"order"),e=w(b,"filter")||w(b,"search");if(d!==null||e!==null){c.mData={_:a+".display",sort:d!==null?a+".@data-"+d:k,type:d!==null?a+".@data-"+d:k,filter:e!==null?a+".@data-"+e:k};ja(p,a)}}})}var T=p.oFeatures,e=function(){if(g.aaSorting===k){var a=p.aaSorting;j=0;for(i=a.length;j<i;j++)a[j][1]=p.aoColumns[j].asSorting[0]}wa(p);T.bSort&&z(p,"aoDrawCallback",function(){if(p.bSorted){var a=V(p),b={};h.each(a,function(a,c){b[c.src]=c.dir});r(p,null,"order",[p,a,b]);Hb(p)}});
z(p,"aoDrawCallback",function(){(p.bSorted||y(p)==="ssp"||T.bDeferRender)&&wa(p)},"sc");var a=q.children("caption").each(function(){this._captionSide=h(this).css("caption-side")}),b=q.children("thead");b.length===0&&(b=h("<thead/>").appendTo(q));p.nTHead=b[0];b=q.children("tbody");b.length===0&&(b=h("<tbody/>").appendTo(q));p.nTBody=b[0];b=q.children("tfoot");if(b.length===0&&a.length>0&&(p.oScroll.sX!==""||p.oScroll.sY!==""))b=h("<tfoot/>").appendTo(q);if(b.length===0||b.children().length===0)q.addClass(u.sNoFooter);
else if(b.length>0){p.nTFoot=b[0];da(p.aoFooter,p.nTFoot)}if(g.aaData)for(j=0;j<g.aaData.length;j++)M(p,g.aaData[j]);else(p.bDeferLoading||y(p)=="dom")&&ma(p,h(p.nTBody).children("tr"));p.aiDisplay=p.aiDisplayMaster.slice();p.bInitialised=true;n===false&&ga(p)};g.bStateSave?(T.bStateSave=!0,z(p,"aoDrawCallback",xa,"state_save"),Ib(p,g,e)):e()}});b=null;return this},x,s,o,u,Xa={},Mb=/[\r\n]/g,Aa=/<.*?>/g,Zb=/^\d{2,4}[\.\/\-]\d{1,2}[\.\/\-]\d{1,2}([T ]{1}\d{1,2}[:\.]\d{2}([\.:]\d{2})?)?$/,$b=RegExp("(\\/|\\.|\\*|\\+|\\?|\\||\\(|\\)|\\[|\\]|\\{|\\}|\\\\|\\$|\\^|\\-)",
"g"),Wa=/[',$£€¥%\u2009\u202F\u20BD\u20a9\u20BArfk]/gi,L=function(a){return!a||!0===a||"-"===a?!0:!1},Nb=function(a){var b=parseInt(a,10);return!isNaN(b)&&isFinite(a)?b:null},Ob=function(a,b){Xa[b]||(Xa[b]=RegExp(Pa(b),"g"));return"string"===typeof a&&"."!==b?a.replace(/\./g,"").replace(Xa[b],"."):a},Ya=function(a,b,c){var d="string"===typeof a;if(L(a))return!0;b&&d&&(a=Ob(a,b));c&&d&&(a=a.replace(Wa,""));return!isNaN(parseFloat(a))&&isFinite(a)},Pb=function(a,b,c){return L(a)?!0:!(L(a)||"string"===
typeof a)?null:Ya(a.replace(Aa,""),b,c)?!0:null},D=function(a,b,c){var d=[],e=0,f=a.length;if(c!==k)for(;e<f;e++)a[e]&&a[e][b]&&d.push(a[e][b][c]);else for(;e<f;e++)a[e]&&d.push(a[e][b]);return d},ia=function(a,b,c,d){var e=[],f=0,g=b.length;if(d!==k)for(;f<g;f++)a[b[f]][c]&&e.push(a[b[f]][c][d]);else for(;f<g;f++)e.push(a[b[f]][c]);return e},W=function(a,b){var c=[],d;b===k?(b=0,d=a):(d=b,b=a);for(var e=b;e<d;e++)c.push(e);return c},Qb=function(a){for(var b=[],c=0,d=a.length;c<d;c++)a[c]&&b.push(a[c]);
return b},qa=function(a){var b;a:{if(!(2>a.length)){b=a.slice().sort();for(var c=b[0],d=1,e=b.length;d<e;d++){if(b[d]===c){b=!1;break a}c=b[d]}}b=!0}if(b)return a.slice();b=[];var e=a.length,f,g=0,d=0;a:for(;d<e;d++){c=a[d];for(f=0;f<g;f++)if(b[f]===c)continue a;b.push(c);g++}return b};m.util={throttle:function(a,b){var c=b!==k?b:200,d,e;return function(){var b=this,g=+new Date,j=arguments;d&&g<d+c?(clearTimeout(e),e=setTimeout(function(){d=k;a.apply(b,j)},c)):(d=g,a.apply(b,j))}},escapeRegex:function(a){return a.replace($b,
"\\$1")}};var A=function(a,b,c){a[b]!==k&&(a[c]=a[b])},ba=/\[.*?\]$/,U=/\(\)$/,Pa=m.util.escapeRegex,va=h("<div>")[0],Wb=va.textContent!==k,Yb=/<.*?>/g,Na=m.util.throttle,Rb=[],w=Array.prototype,ac=function(a){var b,c,d=m.settings,e=h.map(d,function(a){return a.nTable});if(a){if(a.nTable&&a.oApi)return[a];if(a.nodeName&&"table"===a.nodeName.toLowerCase())return b=h.inArray(a,e),-1!==b?[d[b]]:null;if(a&&"function"===typeof a.settings)return a.settings().toArray();"string"===typeof a?c=h(a):a instanceof
h&&(c=a)}else return[];if(c)return c.map(function(){b=h.inArray(this,e);return-1!==b?d[b]:null}).toArray()};s=function(a,b){if(!(this instanceof s))return new s(a,b);var c=[],d=function(a){(a=ac(a))&&(c=c.concat(a))};if(h.isArray(a))for(var e=0,f=a.length;e<f;e++)d(a[e]);else d(a);this.context=qa(c);b&&h.merge(this,b);this.selector={rows:null,cols:null,opts:null};s.extend(this,this,Rb)};m.Api=s;h.extend(s.prototype,{any:function(){return 0!==this.count()},concat:w.concat,context:[],count:function(){return this.flatten().length},
each:function(a){for(var b=0,c=this.length;b<c;b++)a.call(this,this[b],b,this);return this},eq:function(a){var b=this.context;return b.length>a?new s(b[a],this[a]):null},filter:function(a){var b=[];if(w.filter)b=w.filter.call(this,a,this);else for(var c=0,d=this.length;c<d;c++)a.call(this,this[c],c,this)&&b.push(this[c]);return new s(this.context,b)},flatten:function(){var a=[];return new s(this.context,a.concat.apply(a,this.toArray()))},join:w.join,indexOf:w.indexOf||function(a,b){for(var c=b||0,
d=this.length;c<d;c++)if(this[c]===a)return c;return-1},iterator:function(a,b,c,d){var e=[],f,g,j,h,n,l=this.context,m,o,u=this.selector;"string"===typeof a&&(d=c,c=b,b=a,a=!1);g=0;for(j=l.length;g<j;g++){var r=new s(l[g]);if("table"===b)f=c.call(r,l[g],g),f!==k&&e.push(f);else if("columns"===b||"rows"===b)f=c.call(r,l[g],this[g],g),f!==k&&e.push(f);else if("column"===b||"column-rows"===b||"row"===b||"cell"===b){o=this[g];"column-rows"===b&&(m=Ba(l[g],u.opts));h=0;for(n=o.length;h<n;h++)f=o[h],f=
"cell"===b?c.call(r,l[g],f.row,f.column,g,h):c.call(r,l[g],f,g,h,m),f!==k&&e.push(f)}}return e.length||d?(a=new s(l,a?e.concat.apply([],e):e),b=a.selector,b.rows=u.rows,b.cols=u.cols,b.opts=u.opts,a):this},lastIndexOf:w.lastIndexOf||function(a,b){return this.indexOf.apply(this.toArray.reverse(),arguments)},length:0,map:function(a){var b=[];if(w.map)b=w.map.call(this,a,this);else for(var c=0,d=this.length;c<d;c++)b.push(a.call(this,this[c],c));return new s(this.context,b)},pluck:function(a){return this.map(function(b){return b[a]})},
pop:w.pop,push:w.push,reduce:w.reduce||function(a,b){return gb(this,a,b,0,this.length,1)},reduceRight:w.reduceRight||function(a,b){return gb(this,a,b,this.length-1,-1,-1)},reverse:w.reverse,selector:null,shift:w.shift,slice:function(){return new s(this.context,this)},sort:w.sort,splice:w.splice,toArray:function(){return w.slice.call(this)},to$:function(){return h(this)},toJQuery:function(){return h(this)},unique:function(){return new s(this.context,qa(this))},unshift:w.unshift});s.extend=function(a,
b,c){if(c.length&&b&&(b instanceof s||b.__dt_wrapper)){var d,e,f,g=function(a,b,c){return function(){var d=b.apply(a,arguments);s.extend(d,d,c.methodExt);return d}};d=0;for(e=c.length;d<e;d++)f=c[d],b[f.name]="function"===typeof f.val?g(a,f.val,f):h.isPlainObject(f.val)?{}:f.val,b[f.name].__dt_wrapper=!0,s.extend(a,b[f.name],f.propExt)}};s.register=o=function(a,b){if(h.isArray(a))for(var c=0,d=a.length;c<d;c++)s.register(a[c],b);else for(var e=a.split("."),f=Rb,g,j,c=0,d=e.length;c<d;c++){g=(j=-1!==
e[c].indexOf("()"))?e[c].replace("()",""):e[c];var i;a:{i=0;for(var n=f.length;i<n;i++)if(f[i].name===g){i=f[i];break a}i=null}i||(i={name:g,val:{},methodExt:[],propExt:[]},f.push(i));c===d-1?i.val=b:f=j?i.methodExt:i.propExt}};s.registerPlural=u=function(a,b,c){s.register(a,c);s.register(b,function(){var a=c.apply(this,arguments);return a===this?this:a instanceof s?a.length?h.isArray(a[0])?new s(a.context,a[0]):a[0]:k:a})};o("tables()",function(a){var b;if(a){b=s;var c=this.context;if("number"===
typeof a)a=[c[a]];else var d=h.map(c,function(a){return a.nTable}),a=h(d).filter(a).map(function(){var a=h.inArray(this,d);return c[a]}).toArray();b=new b(a)}else b=this;return b});o("table()",function(a){var a=this.tables(a),b=a.context;return b.length?new s(b[0]):a});u("tables().nodes()","table().node()",function(){return this.iterator("table",function(a){return a.nTable},1)});u("tables().body()","table().body()",function(){return this.iterator("table",function(a){return a.nTBody},1)});u("tables().header()",
"table().header()",function(){return this.iterator("table",function(a){return a.nTHead},1)});u("tables().footer()","table().footer()",function(){return this.iterator("table",function(a){return a.nTFoot},1)});u("tables().containers()","table().container()",function(){return this.iterator("table",function(a){return a.nTableWrapper},1)});o("draw()",function(a){return this.iterator("table",function(b){"page"===a?N(b):("string"===typeof a&&(a="full-hold"===a?!1:!0),S(b,!1===a))})});o("page()",function(a){return a===
k?this.page.info().page:this.iterator("table",function(b){Sa(b,a)})});o("page.info()",function(){if(0===this.context.length)return k;var a=this.context[0],b=a._iDisplayStart,c=a.oFeatures.bPaginate?a._iDisplayLength:-1,d=a.fnRecordsDisplay(),e=-1===c;return{page:e?0:Math.floor(b/c),pages:e?1:Math.ceil(d/c),start:b,end:a.fnDisplayEnd(),length:c,recordsTotal:a.fnRecordsTotal(),recordsDisplay:d,serverSide:"ssp"===y(a)}});o("page.len()",function(a){return a===k?0!==this.context.length?this.context[0]._iDisplayLength:
k:this.iterator("table",function(b){Qa(b,a)})});var Sb=function(a,b,c){if(c){var d=new s(a);d.one("draw",function(){c(d.ajax.json())})}if("ssp"==y(a))S(a,b);else{C(a,!0);var e=a.jqXHR;e&&4!==e.readyState&&e.abort();sa(a,[],function(c){na(a);for(var c=ta(a,c),d=0,e=c.length;d<e;d++)M(a,c[d]);S(a,b);C(a,!1)})}};o("ajax.json()",function(){var a=this.context;if(0<a.length)return a[0].json});o("ajax.params()",function(){var a=this.context;if(0<a.length)return a[0].oAjaxData});o("ajax.reload()",function(a,
b){return this.iterator("table",function(c){Sb(c,!1===b,a)})});o("ajax.url()",function(a){var b=this.context;if(a===k){if(0===b.length)return k;b=b[0];return b.ajax?h.isPlainObject(b.ajax)?b.ajax.url:b.ajax:b.sAjaxSource}return this.iterator("table",function(b){h.isPlainObject(b.ajax)?b.ajax.url=a:b.ajax=a})});o("ajax.url().load()",function(a,b){return this.iterator("table",function(c){Sb(c,!1===b,a)})});var Za=function(a,b,c,d,e){var f=[],g,j,i,n,l,m;i=typeof b;if(!b||"string"===i||"function"===
i||b.length===k)b=[b];i=0;for(n=b.length;i<n;i++){j=b[i]&&b[i].split&&!b[i].match(/[\[\(:]/)?b[i].split(","):[b[i]];l=0;for(m=j.length;l<m;l++)(g=c("string"===typeof j[l]?h.trim(j[l]):j[l]))&&g.length&&(f=f.concat(g))}a=x.selector[a];if(a.length){i=0;for(n=a.length;i<n;i++)f=a[i](d,e,f)}return qa(f)},$a=function(a){a||(a={});a.filter&&a.search===k&&(a.search=a.filter);return h.extend({search:"none",order:"current",page:"all"},a)},ab=function(a){for(var b=0,c=a.length;b<c;b++)if(0<a[b].length)return a[0]=
a[b],a[0].length=1,a.length=1,a.context=[a.context[b]],a;a.length=0;return a},Ba=function(a,b){var c,d,e,f=[],g=a.aiDisplay;c=a.aiDisplayMaster;var j=b.search;d=b.order;e=b.page;if("ssp"==y(a))return"removed"===j?[]:W(0,c.length);if("current"==e){c=a._iDisplayStart;for(d=a.fnDisplayEnd();c<d;c++)f.push(g[c])}else if("current"==d||"applied"==d)f="none"==j?c.slice():"applied"==j?g.slice():h.map(c,function(a){return-1===h.inArray(a,g)?a:null});else if("index"==d||"original"==d){c=0;for(d=a.aoData.length;c<
d;c++)"none"==j?f.push(c):(e=h.inArray(c,g),(-1===e&&"removed"==j||0<=e&&"applied"==j)&&f.push(c))}return f};o("rows()",function(a,b){a===k?a="":h.isPlainObject(a)&&(b=a,a="");var b=$a(b),c=this.iterator("table",function(c){var e=b,f;return Za("row",a,function(a){var b=Nb(a);if(b!==null&&!e)return[b];f||(f=Ba(c,e));if(b!==null&&h.inArray(b,f)!==-1)return[b];if(a===null||a===k||a==="")return f;if(typeof a==="function")return h.map(f,function(b){var e=c.aoData[b];return a(b,e._aData,e.nTr)?b:null});
b=Qb(ia(c.aoData,f,"nTr"));if(a.nodeName){if(a._DT_RowIndex!==k)return[a._DT_RowIndex];if(a._DT_CellIndex)return[a._DT_CellIndex.row];b=h(a).closest("*[data-dt-row]");return b.length?[b.data("dt-row")]:[]}if(typeof a==="string"&&a.charAt(0)==="#"){var i=c.aIds[a.replace(/^#/,"")];if(i!==k)return[i.idx]}return h(b).filter(a).map(function(){return this._DT_RowIndex}).toArray()},c,e)},1);c.selector.rows=a;c.selector.opts=b;return c});o("rows().nodes()",function(){return this.iterator("row",function(a,
b){return a.aoData[b].nTr||k},1)});o("rows().data()",function(){return this.iterator(!0,"rows",function(a,b){return ia(a.aoData,b,"_aData")},1)});u("rows().cache()","row().cache()",function(a){return this.iterator("row",function(b,c){var d=b.aoData[c];return"search"===a?d._aFilterData:d._aSortData},1)});u("rows().invalidate()","row().invalidate()",function(a){return this.iterator("row",function(b,c){ca(b,c,a)})});u("rows().indexes()","row().index()",function(){return this.iterator("row",function(a,
b){return b},1)});u("rows().ids()","row().id()",function(a){for(var b=[],c=this.context,d=0,e=c.length;d<e;d++)for(var f=0,g=this[d].length;f<g;f++){var h=c[d].rowIdFn(c[d].aoData[this[d][f]]._aData);b.push((!0===a?"#":"")+h)}return new s(c,b)});u("rows().remove()","row().remove()",function(){var a=this;this.iterator("row",function(b,c,d){var e=b.aoData,f=e[c],g,h,i,n,l;e.splice(c,1);g=0;for(h=e.length;g<h;g++)if(i=e[g],l=i.anCells,null!==i.nTr&&(i.nTr._DT_RowIndex=g),null!==l){i=0;for(n=l.length;i<
n;i++)l[i]._DT_CellIndex.row=g}oa(b.aiDisplayMaster,c);oa(b.aiDisplay,c);oa(a[d],c,!1);0<b._iRecordsDisplay&&b._iRecordsDisplay--;Ra(b);c=b.rowIdFn(f._aData);c!==k&&delete b.aIds[c]});this.iterator("table",function(a){for(var c=0,d=a.aoData.length;c<d;c++)a.aoData[c].idx=c});return this});o("rows.add()",function(a){var b=this.iterator("table",function(b){var c,f,g,h=[];f=0;for(g=a.length;f<g;f++)c=a[f],c.nodeName&&"TR"===c.nodeName.toUpperCase()?h.push(ma(b,c)[0]):h.push(M(b,c));return h},1),c=this.rows(-1);
c.pop();h.merge(c,b);return c});o("row()",function(a,b){return ab(this.rows(a,b))});o("row().data()",function(a){var b=this.context;if(a===k)return b.length&&this.length?b[0].aoData[this[0]]._aData:k;b[0].aoData[this[0]]._aData=a;ca(b[0],this[0],"data");return this});o("row().node()",function(){var a=this.context;return a.length&&this.length?a[0].aoData[this[0]].nTr||null:null});o("row.add()",function(a){a instanceof h&&a.length&&(a=a[0]);var b=this.iterator("table",function(b){return a.nodeName&&
"TR"===a.nodeName.toUpperCase()?ma(b,a)[0]:M(b,a)});return this.row(b[0])});var bb=function(a,b){var c=a.context;if(c.length&&(c=c[0].aoData[b!==k?b:a[0]])&&c._details)c._details.remove(),c._detailsShow=k,c._details=k},Tb=function(a,b){var c=a.context;if(c.length&&a.length){var d=c[0].aoData[a[0]];if(d._details){(d._detailsShow=b)?d._details.insertAfter(d.nTr):d._details.detach();var e=c[0],f=new s(e),g=e.aoData;f.off("draw.dt.DT_details column-visibility.dt.DT_details destroy.dt.DT_details");0<D(g,
"_details").length&&(f.on("draw.dt.DT_details",function(a,b){e===b&&f.rows({page:"current"}).eq(0).each(function(a){a=g[a];a._detailsShow&&a._details.insertAfter(a.nTr)})}),f.on("column-visibility.dt.DT_details",function(a,b){if(e===b)for(var c,d=aa(b),f=0,h=g.length;f<h;f++)c=g[f],c._details&&c._details.children("td[colspan]").attr("colspan",d)}),f.on("destroy.dt.DT_details",function(a,b){if(e===b)for(var c=0,d=g.length;c<d;c++)g[c]._details&&bb(f,c)}))}}};o("row().child()",function(a,b){var c=this.context;
if(a===k)return c.length&&this.length?c[0].aoData[this[0]]._details:k;if(!0===a)this.child.show();else if(!1===a)bb(this);else if(c.length&&this.length){var d=c[0],c=c[0].aoData[this[0]],e=[],f=function(a,b){if(h.isArray(a)||a instanceof h)for(var c=0,k=a.length;c<k;c++)f(a[c],b);else a.nodeName&&"tr"===a.nodeName.toLowerCase()?e.push(a):(c=h("<tr><td/></tr>").addClass(b),h("td",c).addClass(b).html(a)[0].colSpan=aa(d),e.push(c[0]))};f(a,b);c._details&&c._details.detach();c._details=h(e);c._detailsShow&&
c._details.insertAfter(c.nTr)}return this});o(["row().child.show()","row().child().show()"],function(){Tb(this,!0);return this});o(["row().child.hide()","row().child().hide()"],function(){Tb(this,!1);return this});o(["row().child.remove()","row().child().remove()"],function(){bb(this);return this});o("row().child.isShown()",function(){var a=this.context;return a.length&&this.length?a[0].aoData[this[0]]._detailsShow||!1:!1});var bc=/^([^:]+):(name|visIdx|visible)$/,Ub=function(a,b,c,d,e){for(var c=
[],d=0,f=e.length;d<f;d++)c.push(B(a,e[d],b));return c};o("columns()",function(a,b){a===k?a="":h.isPlainObject(a)&&(b=a,a="");var b=$a(b),c=this.iterator("table",function(c){var e=a,f=b,g=c.aoColumns,j=D(g,"sName"),i=D(g,"nTh");return Za("column",e,function(a){var b=Nb(a);if(a==="")return W(g.length);if(b!==null)return[b>=0?b:g.length+b];if(typeof a==="function"){var e=Ba(c,f);return h.map(g,function(b,f){return a(f,Ub(c,f,0,0,e),i[f])?f:null})}var k=typeof a==="string"?a.match(bc):"";if(k)switch(k[2]){case "visIdx":case "visible":b=
parseInt(k[1],10);if(b<0){var m=h.map(g,function(a,b){return a.bVisible?b:null});return[m[m.length+b]]}return[Z(c,b)];case "name":return h.map(j,function(a,b){return a===k[1]?b:null});default:return[]}if(a.nodeName&&a._DT_CellIndex)return[a._DT_CellIndex.column];b=h(i).filter(a).map(function(){return h.inArray(this,i)}).toArray();if(b.length||!a.nodeName)return b;b=h(a).closest("*[data-dt-column]");return b.length?[b.data("dt-column")]:[]},c,f)},1);c.selector.cols=a;c.selector.opts=b;return c});u("columns().header()",
"column().header()",function(){return this.iterator("column",function(a,b){return a.aoColumns[b].nTh},1)});u("columns().footer()","column().footer()",function(){return this.iterator("column",function(a,b){return a.aoColumns[b].nTf},1)});u("columns().data()","column().data()",function(){return this.iterator("column-rows",Ub,1)});u("columns().dataSrc()","column().dataSrc()",function(){return this.iterator("column",function(a,b){return a.aoColumns[b].mData},1)});u("columns().cache()","column().cache()",
function(a){return this.iterator("column-rows",function(b,c,d,e,f){return ia(b.aoData,f,"search"===a?"_aFilterData":"_aSortData",c)},1)});u("columns().nodes()","column().nodes()",function(){return this.iterator("column-rows",function(a,b,c,d,e){return ia(a.aoData,e,"anCells",b)},1)});u("columns().visible()","column().visible()",function(a,b){var c=this.iterator("column",function(b,c){if(a===k)return b.aoColumns[c].bVisible;var f=b.aoColumns,g=f[c],j=b.aoData,i,n,l;if(a!==k&&g.bVisible!==a){if(a){var m=
h.inArray(!0,D(f,"bVisible"),c+1);i=0;for(n=j.length;i<n;i++)l=j[i].nTr,f=j[i].anCells,l&&l.insertBefore(f[c],f[m]||null)}else h(D(b.aoData,"anCells",c)).detach();g.bVisible=a;ea(b,b.aoHeader);ea(b,b.aoFooter);xa(b)}});a!==k&&(this.iterator("column",function(c,e){r(c,null,"column-visibility",[c,e,a,b])}),(b===k||b)&&this.columns.adjust());return c});u("columns().indexes()","column().index()",function(a){return this.iterator("column",function(b,c){return"visible"===a?$(b,c):c},1)});o("columns.adjust()",
function(){return this.iterator("table",function(a){Y(a)},1)});o("column.index()",function(a,b){if(0!==this.context.length){var c=this.context[0];if("fromVisible"===a||"toData"===a)return Z(c,b);if("fromData"===a||"toVisible"===a)return $(c,b)}});o("column()",function(a,b){return ab(this.columns(a,b))});o("cells()",function(a,b,c){h.isPlainObject(a)&&(a.row===k?(c=a,a=null):(c=b,b=null));h.isPlainObject(b)&&(c=b,b=null);if(null===b||b===k)return this.iterator("table",function(b){var d=a,e=$a(c),f=
b.aoData,g=Ba(b,e),j=Qb(ia(f,g,"anCells")),i=h([].concat.apply([],j)),l,n=b.aoColumns.length,m,o,u,s,r,v;return Za("cell",d,function(a){var c=typeof a==="function";if(a===null||a===k||c){m=[];o=0;for(u=g.length;o<u;o++){l=g[o];for(s=0;s<n;s++){r={row:l,column:s};if(c){v=f[l];a(r,B(b,l,s),v.anCells?v.anCells[s]:null)&&m.push(r)}else m.push(r)}}return m}if(h.isPlainObject(a))return[a];c=i.filter(a).map(function(a,b){return{row:b._DT_CellIndex.row,column:b._DT_CellIndex.column}}).toArray();if(c.length||
!a.nodeName)return c;v=h(a).closest("*[data-dt-row]");return v.length?[{row:v.data("dt-row"),column:v.data("dt-column")}]:[]},b,e)});var d=this.columns(b,c),e=this.rows(a,c),f,g,j,i,n,l=this.iterator("table",function(a,b){f=[];g=0;for(j=e[b].length;g<j;g++){i=0;for(n=d[b].length;i<n;i++)f.push({row:e[b][g],column:d[b][i]})}return f},1);h.extend(l.selector,{cols:b,rows:a,opts:c});return l});u("cells().nodes()","cell().node()",function(){return this.iterator("cell",function(a,b,c){return(a=a.aoData[b])&&
a.anCells?a.anCells[c]:k},1)});o("cells().data()",function(){return this.iterator("cell",function(a,b,c){return B(a,b,c)},1)});u("cells().cache()","cell().cache()",function(a){a="search"===a?"_aFilterData":"_aSortData";return this.iterator("cell",function(b,c,d){return b.aoData[c][a][d]},1)});u("cells().render()","cell().render()",function(a){return this.iterator("cell",function(b,c,d){return B(b,c,d,a)},1)});u("cells().indexes()","cell().index()",function(){return this.iterator("cell",function(a,
b,c){return{row:b,column:c,columnVisible:$(a,c)}},1)});u("cells().invalidate()","cell().invalidate()",function(a){return this.iterator("cell",function(b,c,d){ca(b,c,a,d)})});o("cell()",function(a,b,c){return ab(this.cells(a,b,c))});o("cell().data()",function(a){var b=this.context,c=this[0];if(a===k)return b.length&&c.length?B(b[0],c[0].row,c[0].column):k;ib(b[0],c[0].row,c[0].column,a);ca(b[0],c[0].row,"data",c[0].column);return this});o("order()",function(a,b){var c=this.context;if(a===k)return 0!==
c.length?c[0].aaSorting:k;"number"===typeof a?a=[[a,b]]:a.length&&!h.isArray(a[0])&&(a=Array.prototype.slice.call(arguments));return this.iterator("table",function(b){b.aaSorting=a.slice()})});o("order.listener()",function(a,b,c){return this.iterator("table",function(d){La(d,a,b,c)})});o("order.fixed()",function(a){if(!a){var b=this.context,b=b.length?b[0].aaSortingFixed:k;return h.isArray(b)?{pre:b}:b}return this.iterator("table",function(b){b.aaSortingFixed=h.extend(!0,{},a)})});o(["columns().order()",
"column().order()"],function(a){var b=this;return this.iterator("table",function(c,d){var e=[];h.each(b[d],function(b,c){e.push([c,a])});c.aaSorting=e})});o("search()",function(a,b,c,d){var e=this.context;return a===k?0!==e.length?e[0].oPreviousSearch.sSearch:k:this.iterator("table",function(e){e.oFeatures.bFilter&&fa(e,h.extend({},e.oPreviousSearch,{sSearch:a+"",bRegex:null===b?!1:b,bSmart:null===c?!0:c,bCaseInsensitive:null===d?!0:d}),1)})});u("columns().search()","column().search()",function(a,
b,c,d){return this.iterator("column",function(e,f){var g=e.aoPreSearchCols;if(a===k)return g[f].sSearch;e.oFeatures.bFilter&&(h.extend(g[f],{sSearch:a+"",bRegex:null===b?!1:b,bSmart:null===c?!0:c,bCaseInsensitive:null===d?!0:d}),fa(e,e.oPreviousSearch,1))})});o("state()",function(){return this.context.length?this.context[0].oSavedState:null});o("state.clear()",function(){return this.iterator("table",function(a){a.fnStateSaveCallback.call(a.oInstance,a,{})})});o("state.loaded()",function(){return this.context.length?
this.context[0].oLoadedState:null});o("state.save()",function(){return this.iterator("table",function(a){xa(a)})});m.versionCheck=m.fnVersionCheck=function(a){for(var b=m.version.split("."),a=a.split("."),c,d,e=0,f=a.length;e<f;e++)if(c=parseInt(b[e],10)||0,d=parseInt(a[e],10)||0,c!==d)return c>d;return!0};m.isDataTable=m.fnIsDataTable=function(a){var b=h(a).get(0),c=!1;if(a instanceof m.Api)return!0;h.each(m.settings,function(a,e){var f=e.nScrollHead?h("table",e.nScrollHead)[0]:null,g=e.nScrollFoot?
h("table",e.nScrollFoot)[0]:null;if(e.nTable===b||f===b||g===b)c=!0});return c};m.tables=m.fnTables=function(a){var b=!1;h.isPlainObject(a)&&(b=a.api,a=a.visible);var c=h.map(m.settings,function(b){if(!a||a&&h(b.nTable).is(":visible"))return b.nTable});return b?new s(c):c};m.camelToHungarian=I;o("$()",function(a,b){var c=this.rows(b).nodes(),c=h(c);return h([].concat(c.filter(a).toArray(),c.find(a).toArray()))});h.each(["on","one","off"],function(a,b){o(b+"()",function(){var a=Array.prototype.slice.call(arguments);
a[0]=h.map(a[0].split(/\s/),function(a){return!a.match(/\.dt\b/)?a+".dt":a}).join(" ");var d=h(this.tables().nodes());d[b].apply(d,a);return this})});o("clear()",function(){return this.iterator("table",function(a){na(a)})});o("settings()",function(){return new s(this.context,this.context)});o("init()",function(){var a=this.context;return a.length?a[0].oInit:null});o("data()",function(){return this.iterator("table",function(a){return D(a.aoData,"_aData")}).flatten()});o("destroy()",function(a){a=a||
!1;return this.iterator("table",function(b){var c=b.nTableWrapper.parentNode,d=b.oClasses,e=b.nTable,f=b.nTBody,g=b.nTHead,j=b.nTFoot,i=h(e),f=h(f),k=h(b.nTableWrapper),l=h.map(b.aoData,function(a){return a.nTr}),o;b.bDestroying=!0;r(b,"aoDestroyCallback","destroy",[b]);a||(new s(b)).columns().visible(!0);k.off(".DT").find(":not(tbody *)").off(".DT");h(E).off(".DT-"+b.sInstance);e!=g.parentNode&&(i.children("thead").detach(),i.append(g));j&&e!=j.parentNode&&(i.children("tfoot").detach(),i.append(j));
b.aaSorting=[];b.aaSortingFixed=[];wa(b);h(l).removeClass(b.asStripeClasses.join(" "));h("th, td",g).removeClass(d.sSortable+" "+d.sSortableAsc+" "+d.sSortableDesc+" "+d.sSortableNone);f.children().detach();f.append(l);g=a?"remove":"detach";i[g]();k[g]();!a&&c&&(c.insertBefore(e,b.nTableReinsertBefore),i.css("width",b.sDestroyWidth).removeClass(d.sTable),(o=b.asDestroyStripes.length)&&f.children().each(function(a){h(this).addClass(b.asDestroyStripes[a%o])}));c=h.inArray(b,m.settings);-1!==c&&m.settings.splice(c,
1)})});h.each(["column","row","cell"],function(a,b){o(b+"s().every()",function(a){var d=this.selector.opts,e=this;return this.iterator(b,function(f,g,h,i,n){a.call(e[b](g,"cell"===b?h:d,"cell"===b?d:k),g,h,i,n)})})});o("i18n()",function(a,b,c){var d=this.context[0],a=Q(a)(d.oLanguage);a===k&&(a=b);c!==k&&h.isPlainObject(a)&&(a=a[c]!==k?a[c]:a._);return a.replace("%d",c)});m.version="1.10.16";m.settings=[];m.models={};m.models.oSearch={bCaseInsensitive:!0,sSearch:"",bRegex:!1,bSmart:!0};m.models.oRow=
{nTr:null,anCells:null,_aData:[],_aSortData:null,_aFilterData:null,_sFilterRow:null,_sRowStripe:"",src:null,idx:-1};m.models.oColumn={idx:null,aDataSort:null,asSorting:null,bSearchable:null,bSortable:null,bVisible:null,_sManualType:null,_bAttrSrc:!1,fnCreatedCell:null,fnGetData:null,fnSetData:null,mData:null,mRender:null,nTh:null,nTf:null,sClass:null,sContentPadding:null,sDefaultContent:null,sName:null,sSortDataType:"std",sSortingClass:null,sSortingClassJUI:null,sTitle:null,sType:null,sWidth:null,
sWidthOrig:null};m.defaults={aaData:null,aaSorting:[[0,"asc"]],aaSortingFixed:[],ajax:null,aLengthMenu:[10,25,50,100],aoColumns:null,aoColumnDefs:null,aoSearchCols:[],asStripeClasses:null,bAutoWidth:!0,bDeferRender:!1,bDestroy:!1,bFilter:!0,bInfo:!0,bLengthChange:!0,bPaginate:!0,bProcessing:!1,bRetrieve:!1,bScrollCollapse:!1,bServerSide:!1,bSort:!0,bSortMulti:!0,bSortCellsTop:!1,bSortClasses:!0,bStateSave:!1,fnCreatedRow:null,fnDrawCallback:null,fnFooterCallback:null,fnFormatNumber:function(a){return a.toString().replace(/\B(?=(\d{3})+(?!\d))/g,
this.oLanguage.sThousands)},fnHeaderCallback:null,fnInfoCallback:null,fnInitComplete:null,fnPreDrawCallback:null,fnRowCallback:null,fnServerData:null,fnServerParams:null,fnStateLoadCallback:function(a){try{return JSON.parse((-1===a.iStateDuration?sessionStorage:localStorage).getItem("DataTables_"+a.sInstance+"_"+location.pathname))}catch(b){}},fnStateLoadParams:null,fnStateLoaded:null,fnStateSaveCallback:function(a,b){try{(-1===a.iStateDuration?sessionStorage:localStorage).setItem("DataTables_"+a.sInstance+
"_"+location.pathname,JSON.stringify(b))}catch(c){}},fnStateSaveParams:null,iStateDuration:7200,iDeferLoading:null,iDisplayLength:10,iDisplayStart:0,iTabIndex:0,oClasses:{},oLanguage:{oAria:{sSortAscending:": activate to sort column ascending",sSortDescending:": activate to sort column descending"},oPaginate:{sFirst:"First",sLast:"Last",sNext:"Next",sPrevious:"Previous"},sEmptyTable:"No data available in table",sInfo:"Showing _START_ to _END_ of _TOTAL_ entries",sInfoEmpty:"Showing 0 to 0 of 0 entries",
sInfoFiltered:"(filtered from _MAX_ total entries)",sInfoPostFix:"",sDecimal:"",sThousands:",",sLengthMenu:"Show _MENU_ entries",sLoadingRecords:"Loading...",sProcessing:"Processing...",sSearch:"Search:",sSearchPlaceholder:"",sUrl:"",sZeroRecords:"No matching records found"},oSearch:h.extend({},m.models.oSearch),sAjaxDataProp:"data",sAjaxSource:null,sDom:"lfrtip",searchDelay:null,sPaginationType:"simple_numbers",sScrollX:"",sScrollXInner:"",sScrollY:"",sServerMethod:"GET",renderer:null,rowId:"DT_RowId"};
X(m.defaults);m.defaults.column={aDataSort:null,iDataSort:-1,asSorting:["asc","desc"],bSearchable:!0,bSortable:!0,bVisible:!0,fnCreatedCell:null,mData:null,mRender:null,sCellType:"td",sClass:"",sContentPadding:"",sDefaultContent:null,sName:"",sSortDataType:"std",sTitle:null,sType:null,sWidth:null};X(m.defaults.column);m.models.oSettings={oFeatures:{bAutoWidth:null,bDeferRender:null,bFilter:null,bInfo:null,bLengthChange:null,bPaginate:null,bProcessing:null,bServerSide:null,bSort:null,bSortMulti:null,
bSortClasses:null,bStateSave:null},oScroll:{bCollapse:null,iBarWidth:0,sX:null,sXInner:null,sY:null},oLanguage:{fnInfoCallback:null},oBrowser:{bScrollOversize:!1,bScrollbarLeft:!1,bBounding:!1,barWidth:0},ajax:null,aanFeatures:[],aoData:[],aiDisplay:[],aiDisplayMaster:[],aIds:{},aoColumns:[],aoHeader:[],aoFooter:[],oPreviousSearch:{},aoPreSearchCols:[],aaSorting:null,aaSortingFixed:[],asStripeClasses:null,asDestroyStripes:[],sDestroyWidth:0,aoRowCallback:[],aoHeaderCallback:[],aoFooterCallback:[],
aoDrawCallback:[],aoRowCreatedCallback:[],aoPreDrawCallback:[],aoInitComplete:[],aoStateSaveParams:[],aoStateLoadParams:[],aoStateLoaded:[],sTableId:"",nTable:null,nTHead:null,nTFoot:null,nTBody:null,nTableWrapper:null,bDeferLoading:!1,bInitialised:!1,aoOpenRows:[],sDom:null,searchDelay:null,sPaginationType:"two_button",iStateDuration:0,aoStateSave:[],aoStateLoad:[],oSavedState:null,oLoadedState:null,sAjaxSource:null,sAjaxDataProp:null,bAjaxDataGet:!0,jqXHR:null,json:k,oAjaxData:k,fnServerData:null,
aoServerParams:[],sServerMethod:null,fnFormatNumber:null,aLengthMenu:null,iDraw:0,bDrawing:!1,iDrawError:-1,_iDisplayLength:10,_iDisplayStart:0,_iRecordsTotal:0,_iRecordsDisplay:0,oClasses:{},bFiltered:!1,bSorted:!1,bSortCellsTop:null,oInit:null,aoDestroyCallback:[],fnRecordsTotal:function(){return"ssp"==y(this)?1*this._iRecordsTotal:this.aiDisplayMaster.length},fnRecordsDisplay:function(){return"ssp"==y(this)?1*this._iRecordsDisplay:this.aiDisplay.length},fnDisplayEnd:function(){var a=this._iDisplayLength,
b=this._iDisplayStart,c=b+a,d=this.aiDisplay.length,e=this.oFeatures,f=e.bPaginate;return e.bServerSide?!1===f||-1===a?b+d:Math.min(b+a,this._iRecordsDisplay):!f||c>d||-1===a?d:c},oInstance:null,sInstance:null,iTabIndex:0,nScrollHead:null,nScrollFoot:null,aLastSort:[],oPlugins:{},rowIdFn:null,rowId:null};m.ext=x={buttons:{},classes:{},build:"dt/dt-1.10.16",errMode:"alert",feature:[],search:[],selector:{cell:[],column:[],row:[]},internal:{},legacy:{ajax:null},pager:{},renderer:{pageButton:{},header:{}},
order:{},type:{detect:[],search:{},order:{}},_unique:0,fnVersionCheck:m.fnVersionCheck,iApiIndex:0,oJUIClasses:{},sVersion:m.version};h.extend(x,{afnFiltering:x.search,aTypes:x.type.detect,ofnSearch:x.type.search,oSort:x.type.order,afnSortData:x.order,aoFeatures:x.feature,oApi:x.internal,oStdClasses:x.classes,oPagination:x.pager});h.extend(m.ext.classes,{sTable:"dataTable",sNoFooter:"no-footer",sPageButton:"paginate_button",sPageButtonActive:"current",sPageButtonDisabled:"disabled",sStripeOdd:"odd",
sStripeEven:"even",sRowEmpty:"dataTables_empty",sWrapper:"dataTables_wrapper",sFilter:"dataTables_filter",sInfo:"dataTables_info",sPaging:"dataTables_paginate paging_",sLength:"dataTables_length",sProcessing:"dataTables_processing",sSortAsc:"sorting_asc",sSortDesc:"sorting_desc",sSortable:"sorting",sSortableAsc:"sorting_asc_disabled",sSortableDesc:"sorting_desc_disabled",sSortableNone:"sorting_disabled",sSortColumn:"sorting_",sFilterInput:"",sLengthSelect:"",sScrollWrapper:"dataTables_scroll",sScrollHead:"dataTables_scrollHead",
sScrollHeadInner:"dataTables_scrollHeadInner",sScrollBody:"dataTables_scrollBody",sScrollFoot:"dataTables_scrollFoot",sScrollFootInner:"dataTables_scrollFootInner",sHeaderTH:"",sFooterTH:"",sSortJUIAsc:"",sSortJUIDesc:"",sSortJUI:"",sSortJUIAscAllowed:"",sSortJUIDescAllowed:"",sSortJUIWrapper:"",sSortIcon:"",sJUIHeader:"",sJUIFooter:""});var Kb=m.ext.pager;h.extend(Kb,{simple:function(){return["previous","next"]},full:function(){return["first","previous","next","last"]},numbers:function(a,b){return[ha(a,
b)]},simple_numbers:function(a,b){return["previous",ha(a,b),"next"]},full_numbers:function(a,b){return["first","previous",ha(a,b),"next","last"]},first_last_numbers:function(a,b){return["first",ha(a,b),"last"]},_numbers:ha,numbers_length:7});h.extend(!0,m.ext.renderer,{pageButton:{_:function(a,b,c,d,e,f){var g=a.oClasses,j=a.oLanguage.oPaginate,i=a.oLanguage.oAria.paginate||{},n,l,m=0,o=function(b,d){var k,s,u,r,v=function(b){Sa(a,b.data.action,true)};k=0;for(s=d.length;k<s;k++){r=d[k];if(h.isArray(r)){u=
h("<"+(r.DT_el||"div")+"/>").appendTo(b);o(u,r)}else{n=null;l="";switch(r){case "ellipsis":b.append('<span class="ellipsis">&#x2026;</span>');break;case "first":n=j.sFirst;l=r+(e>0?"":" "+g.sPageButtonDisabled);break;case "previous":n=j.sPrevious;l=r+(e>0?"":" "+g.sPageButtonDisabled);break;case "next":n=j.sNext;l=r+(e<f-1?"":" "+g.sPageButtonDisabled);break;case "last":n=j.sLast;l=r+(e<f-1?"":" "+g.sPageButtonDisabled);break;default:n=r+1;l=e===r?g.sPageButtonActive:""}if(n!==null){u=h("<a>",{"class":g.sPageButton+
" "+l,"aria-controls":a.sTableId,"aria-label":i[r],"data-dt-idx":m,tabindex:a.iTabIndex,id:c===0&&typeof r==="string"?a.sTableId+"_"+r:null}).html(n).appendTo(b);Va(u,{action:r},v);m++}}}},s;try{s=h(b).find(G.activeElement).data("dt-idx")}catch(u){}o(h(b).empty(),d);s!==k&&h(b).find("[data-dt-idx="+s+"]").focus()}}});h.extend(m.ext.type.detect,[function(a,b){var c=b.oLanguage.sDecimal;return Ya(a,c)?"num"+c:null},function(a){if(a&&!(a instanceof Date)&&!Zb.test(a))return null;var b=Date.parse(a);
return null!==b&&!isNaN(b)||L(a)?"date":null},function(a,b){var c=b.oLanguage.sDecimal;return Ya(a,c,!0)?"num-fmt"+c:null},function(a,b){var c=b.oLanguage.sDecimal;return Pb(a,c)?"html-num"+c:null},function(a,b){var c=b.oLanguage.sDecimal;return Pb(a,c,!0)?"html-num-fmt"+c:null},function(a){return L(a)||"string"===typeof a&&-1!==a.indexOf("<")?"html":null}]);h.extend(m.ext.type.search,{html:function(a){return L(a)?a:"string"===typeof a?a.replace(Mb," ").replace(Aa,""):""},string:function(a){return L(a)?
a:"string"===typeof a?a.replace(Mb," "):a}});var za=function(a,b,c,d){if(0!==a&&(!a||"-"===a))return-Infinity;b&&(a=Ob(a,b));a.replace&&(c&&(a=a.replace(c,"")),d&&(a=a.replace(d,"")));return 1*a};h.extend(x.type.order,{"date-pre":function(a){return Date.parse(a)||-Infinity},"html-pre":function(a){return L(a)?"":a.replace?a.replace(/<.*?>/g,"").toLowerCase():a+""},"string-pre":function(a){return L(a)?"":"string"===typeof a?a.toLowerCase():!a.toString?"":a.toString()},"string-asc":function(a,b){return a<
b?-1:a>b?1:0},"string-desc":function(a,b){return a<b?1:a>b?-1:0}});cb("");h.extend(!0,m.ext.renderer,{header:{_:function(a,b,c,d){h(a.nTable).on("order.dt.DT",function(e,f,g,h){if(a===f){e=c.idx;b.removeClass(c.sSortingClass+" "+d.sSortAsc+" "+d.sSortDesc).addClass(h[e]=="asc"?d.sSortAsc:h[e]=="desc"?d.sSortDesc:c.sSortingClass)}})},jqueryui:function(a,b,c,d){h("<div/>").addClass(d.sSortJUIWrapper).append(b.contents()).append(h("<span/>").addClass(d.sSortIcon+" "+c.sSortingClassJUI)).appendTo(b);
h(a.nTable).on("order.dt.DT",function(e,f,g,h){if(a===f){e=c.idx;b.removeClass(d.sSortAsc+" "+d.sSortDesc).addClass(h[e]=="asc"?d.sSortAsc:h[e]=="desc"?d.sSortDesc:c.sSortingClass);b.find("span."+d.sSortIcon).removeClass(d.sSortJUIAsc+" "+d.sSortJUIDesc+" "+d.sSortJUI+" "+d.sSortJUIAscAllowed+" "+d.sSortJUIDescAllowed).addClass(h[e]=="asc"?d.sSortJUIAsc:h[e]=="desc"?d.sSortJUIDesc:c.sSortingClassJUI)}})}}});var Vb=function(a){return"string"===typeof a?a.replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,
"&quot;"):a};m.render={number:function(a,b,c,d,e){return{display:function(f){if("number"!==typeof f&&"string"!==typeof f)return f;var g=0>f?"-":"",h=parseFloat(f);if(isNaN(h))return Vb(f);h=h.toFixed(c);f=Math.abs(h);h=parseInt(f,10);f=c?b+(f-h).toFixed(c).substring(2):"";return g+(d||"")+h.toString().replace(/\B(?=(\d{3})+(?!\d))/g,a)+f+(e||"")}}},text:function(){return{display:Vb}}};h.extend(m.ext.internal,{_fnExternApiFunc:Lb,_fnBuildAjax:sa,_fnAjaxUpdate:kb,_fnAjaxParameters:tb,_fnAjaxUpdateDraw:ub,
_fnAjaxDataSrc:ta,_fnAddColumn:Da,_fnColumnOptions:ja,_fnAdjustColumnSizing:Y,_fnVisibleToColumnIndex:Z,_fnColumnIndexToVisible:$,_fnVisbleColumns:aa,_fnGetColumns:la,_fnColumnTypes:Fa,_fnApplyColumnDefs:hb,_fnHungarianMap:X,_fnCamelToHungarian:I,_fnLanguageCompat:Ca,_fnBrowserDetect:fb,_fnAddData:M,_fnAddTr:ma,_fnNodeToDataIndex:function(a,b){return b._DT_RowIndex!==k?b._DT_RowIndex:null},_fnNodeToColumnIndex:function(a,b,c){return h.inArray(c,a.aoData[b].anCells)},_fnGetCellData:B,_fnSetCellData:ib,
_fnSplitObjNotation:Ia,_fnGetObjectDataFn:Q,_fnSetObjectDataFn:R,_fnGetDataMaster:Ja,_fnClearTable:na,_fnDeleteIndex:oa,_fnInvalidate:ca,_fnGetRowElements:Ha,_fnCreateTr:Ga,_fnBuildHead:jb,_fnDrawHead:ea,_fnDraw:N,_fnReDraw:S,_fnAddOptionsHtml:mb,_fnDetectHeader:da,_fnGetUniqueThs:ra,_fnFeatureHtmlFilter:ob,_fnFilterComplete:fa,_fnFilterCustom:xb,_fnFilterColumn:wb,_fnFilter:vb,_fnFilterCreateSearch:Oa,_fnEscapeRegex:Pa,_fnFilterData:yb,_fnFeatureHtmlInfo:rb,_fnUpdateInfo:Bb,_fnInfoMacros:Cb,_fnInitialise:ga,
_fnInitComplete:ua,_fnLengthChange:Qa,_fnFeatureHtmlLength:nb,_fnFeatureHtmlPaginate:sb,_fnPageChange:Sa,_fnFeatureHtmlProcessing:pb,_fnProcessingDisplay:C,_fnFeatureHtmlTable:qb,_fnScrollDraw:ka,_fnApplyToChildren:H,_fnCalculateColumnWidths:Ea,_fnThrottle:Na,_fnConvertToWidth:Db,_fnGetWidestNode:Eb,_fnGetMaxLenString:Fb,_fnStringToCss:v,_fnSortFlatten:V,_fnSort:lb,_fnSortAria:Hb,_fnSortListener:Ua,_fnSortAttachListener:La,_fnSortingClasses:wa,_fnSortData:Gb,_fnSaveState:xa,_fnLoadState:Ib,_fnSettingsFromNode:ya,
_fnLog:J,_fnMap:F,_fnBindAction:Va,_fnCallbackReg:z,_fnCallbackFire:r,_fnLengthOverflow:Ra,_fnRenderer:Ma,_fnDataSource:y,_fnRowAttributes:Ka,_fnCalculateEnd:function(){}});h.fn.dataTable=m;m.$=h;h.fn.dataTableSettings=m.settings;h.fn.dataTableExt=m.ext;h.fn.DataTable=function(a){return h(this).dataTable(a).api()};h.each(m,function(a,b){h.fn.DataTable[a]=b});return h.fn.dataTable});

BIN
code/html/vendor/images/sort_asc.png View File

Before After
Width: 19  |  Height: 19  |  Size: 160 B

BIN
code/html/vendor/images/sort_asc_disabled.png View File

Before After
Width: 19  |  Height: 19  |  Size: 148 B

BIN
code/html/vendor/images/sort_both.png View File

Before After
Width: 19  |  Height: 19  |  Size: 201 B

BIN
code/html/vendor/images/sort_desc.png View File

Before After
Width: 19  |  Height: 19  |  Size: 158 B

BIN
code/html/vendor/images/sort_desc_disabled.png View File

Before After
Width: 19  |  Height: 19  |  Size: 146 B

Some files were not shown because too many files changed in this diff

Loading…
Cancel
Save