├── .gitattributes ├── .gitignore ├── .gitmodules ├── CHANGELOG.md ├── LICENSE ├── README.md ├── code ├── .gitignore ├── .travis.yml ├── astyle.conf ├── build-all ├── build-fs ├── debug ├── deploy ├── espurna │ ├── IOTappStory.ino │ ├── button.ino │ ├── config │ │ ├── all.h │ │ ├── arduino.h │ │ ├── data.h │ │ ├── debug.h │ │ ├── general.h │ │ ├── hardware.h │ │ ├── prototypes.h │ │ ├── sensors.h │ │ └── version.h │ ├── data │ │ └── index.html.gz │ ├── debug.ino │ ├── dht.ino │ ├── domoticz.ino │ ├── ds18b20.ino │ ├── emon.ino │ ├── espurna.ino │ ├── espurna.ino.generic.bin │ ├── espurna.ino.nodemcu.bin │ ├── fauxmo.ino │ ├── i2c.ino │ ├── led.ino │ ├── light.ino │ ├── mqtt.ino │ ├── nofuss.ino │ ├── ntp.ino │ ├── ota.ino │ ├── pow.ino │ ├── relay.ino │ ├── rf.ino │ ├── settings.ino │ ├── web.ino │ └── wifi.ino ├── gulpfile.js ├── html │ ├── checkboxes.css │ ├── checkboxes.js │ ├── custom.css │ ├── custom.js │ ├── favicon.ico │ ├── grids-responsive-min.css │ ├── images │ │ ├── border-off.png │ │ ├── border-on.png │ │ ├── handle-center.png │ │ ├── handle-left.png │ │ ├── handle-right.png │ │ ├── label-off.png │ │ └── label-on.png │ ├── index.html │ ├── jquery-1.12.3.min.js │ ├── jquery.wheelcolorpicker-3.0.2.min.js │ ├── pure-min.css │ ├── side-menu.css │ └── wheelcolorpicker.css ├── lib │ └── readme.txt ├── package.json ├── pio_hooks.py └── platformio.ini └── images └── devices ├── 1ch-inching.jpg ├── aithinker-ailight.jpg ├── d1mini.jpg ├── electrodragon-relay-board.jpg ├── jangoe-wifi-relay.png ├── jorgegarcia-wifi-relays-board-kit.jpg ├── motor-switch.jpg ├── mqtt-relay.jpg ├── s20.jpg ├── slampher.jpg ├── sonoff-4ch.jpg ├── sonoff-basic.jpg ├── sonoff-dual.jpg ├── sonoff-led.jpg ├── sonoff-pow.jpg ├── sonoff-rf.jpg ├── sonoff-sv.jpg ├── sonoff-th10-th16.jpg ├── sonoff-touch.jpg └── workchoice-ecoplug.jpg /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | 4 | # Custom for Visual Studio 5 | *.cs diff=csharp 6 | 7 | # Standard to msysgit 8 | *.doc diff=astextplain 9 | *.DOC diff=astextplain 10 | *.docx diff=astextplain 11 | *.DOCX diff=astextplain 12 | *.dot diff=astextplain 13 | *.DOT diff=astextplain 14 | *.pdf diff=astextplain 15 | *.PDF diff=astextplain 16 | *.rtf diff=astextplain 17 | *.RTF diff=astextplain 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Windows image file caches 2 | Thumbs.db 3 | ehthumbs.db 4 | 5 | # Folder config file 6 | Desktop.ini 7 | 8 | # Recycle Bin used on file shares 9 | $RECYCLE.BIN/ 10 | 11 | # Windows Installer files 12 | *.cab 13 | *.msi 14 | *.msm 15 | *.msp 16 | 17 | # Windows shortcuts 18 | *.lnk 19 | 20 | # ========================= 21 | # Operating System Files 22 | # ========================= 23 | 24 | # OSX 25 | # ========================= 26 | 27 | .DS_Store 28 | .AppleDouble 29 | .LSOverride 30 | 31 | # Thumbnails 32 | ._* 33 | 34 | # Files that might appear in the root of a volume 35 | .DocumentRevisions-V100 36 | .fseventsd 37 | .Spotlight-V100 38 | .TemporaryItems 39 | .Trashes 40 | .VolumeIcon.icns 41 | 42 | # Directories potentially created on remote AFP share 43 | .AppleDB 44 | .AppleDesktop 45 | Network Trash Folder 46 | Temporary Items 47 | .apdisk 48 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SensorsIot/Espurna-Framework/ed8c754fd86a704670ef38187313f0dab3e1ecba/.gitmodules -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # ESPurna change log 2 | 3 | The format is based on [Keep a Changelog](http://keepachangelog.com/) 4 | and this project adheres to [Semantic Versioning](http://semver.org/). 5 | 6 | ## [1.6.9] 2017-03-12 7 | ### Added 8 | - Two stage read for DS18B20 devices. Thanks to Izik Dubnov. 9 | - Option to report the relay status via MQTT periodically 10 | - Terminal commands to change relay status an light color 11 | - Added debug via UDP (disabled by default) 12 | - Moved debug strings to PROGMEM. ~1.5KByes memory freed 13 | - Avoid broadcasting websocket messages if no clients connected 14 | 15 | ### Fix 16 | - Fixing use after free bug that leads to corrupted auth credentials. Thanks to David Guillen 17 | 18 | ## [1.6.8] 2017-03-01 19 | ### Added 20 | - Issue #85. Heartbeat reports now free heap, uptime and VCC every 5 minutes 21 | 22 | ### Changed 23 | - Wait two minutes instead of one in AP mode before trying to reconnect to the router 24 | - Issue #92. Debug log enabled by default in Arduino IDE 25 | - Issue #91. Using AsyncMqttClient as default MQTT client again 26 | 27 | ### Fix 28 | - Report data from all sensors via websocket even if no MQTT connection 29 | - Issue #92. Fix unknown reference in Arduino IDE 30 | - Split data.h contents into 1k lines, otherwise Arduino IDE chokes on them 31 | - Discard empty MQTT topic while subscribing 32 | 33 | ## [1.6.7] 2017-02-25 34 | ### Added 35 | - Support for OpenLight / AI-Light by AI-Thinker based on MY9291 LED driver 36 | - Issue #87. Factory reset when physical button pressed for >10 seconds 37 | 38 | ## [1.6.6] 2017-02-23 39 | ### Fix 40 | - Issue #82. Fix critical bug on Sonoff Dual 41 | 42 | ## [1.6.5] 2017-02-22 43 | ### Added 44 | - Option to backup and restore settings from the web interface 45 | - Footer in the web interface 46 | 47 | ### Changed 48 | - Using PubSubClient as MQTT client by default (please read the documentation) 49 | - Double & long clicks do nothing except for the first defined button 50 | 51 | ### Fix 52 | - Issue #79. Fix bug in WiFi led notification & MQTT connectivity (using PubSubClient) 53 | - Issue #73. Fix bug when building without Domoticz support 54 | - Fix Gulp tasks dependencies 55 | 56 | ## [1.6.4] 2017-02-20 57 | ### Added 58 | - Option to embed the web interface in the firmware, disabled by default 59 | - Change relay status with a GET request (browser friendly) 60 | - Support for PROGMEM debug messages (only wifi module has been changed) 61 | - Option to disable mDNS, enabled by default 62 | - Show current web server port in debug log 63 | - Issue #75. Link relays to LEDs 64 | - Issue #76. Using http://espurna.local when in AP mode 65 | 66 | ### Changed 67 | - Images and favicon is now embedded in the HTML 68 | - Authentication challenge only in /auth request. All static contents are un-authenticated 69 | - HTTP response code when out of websocket slots changed from 423 to 429 70 | 71 | ### Fix 72 | - Memory leak in MQTT connection method 73 | - Wait 60 seconds before retrying to connect when in AP mode 74 | - Issue #24 & #74. Update ESPAsyncTCP and ESPAsyncWebServer to latest GIT version that supports MSS defragmenting 75 | - Issue #73. Fixes for windows machines 76 | 77 | ### Removed 78 | - Captive portal removed, mDNS resolution for AP mode too 79 | 80 | ## [1.6.3] 2017-02-15 81 | ### Added 82 | - Issue #69. Temperature unit configuration from the web interface 83 | - Issue #55. WebServer port configurable from the web interface, defaults to 80 84 | - Expand network configuration when adding a new network 85 | 86 | ### Changed 87 | - Merged web contents except images in a single compressed file for reliability 88 | - Update support for Itead Motor Clockwise/Anticlockwise board 89 | - Scan for strongest network only if more than 1 network configured 90 | 91 | ### Fix 92 | - Issue #71. Added default values for netmask and DNS in web configuration 93 | - Fixed Itead 1CH self-locking/inching board definition 94 | - Fixed PlatformIO environments for ESP8285 boards (4CH and Touch) 95 | 96 | ## [1.6.2] 2017-02-10 97 | ### Fix 98 | - Check if there is an MQTT broker defined before the MQTT_MAX_TRIES check 99 | 100 | ## [1.6.1] 2017-02-10 101 | ### Added 102 | - Added support for [Jorge Garcia's Wifi+Relay Board Kit](https://www.tindie.com/products/jorgegarciadev/wifi--relays-board-kit/) 103 | - Reporting current and energy incrementals to a separate counters in Domoticz (thanks to Toni Arte) 104 | - Force WiFi reconnect after MQTT_MAX_TRIES fails trying to connect to MQTT broker 105 | 106 | ## [1.6.0] 2017-02-05 107 | ### Added 108 | - Added support for toggle switches 109 | - Allow reset the board via an MQTT message 110 | - Allow reset the board via an RPC (HTTP) message 111 | - Added support for ADC121 I2C for current monitoring (Check [http://tinkerman.cat/power-monitoring-sonoff-th-adc121/](http://tinkerman.cat/power-monitoring-sonoff-th-adc121/)) 112 | - Reporting voltage to Domoticz (only HLW8012) 113 | - Map button events to actions (toggle relay, AP mode, reset, pulse mode) 114 | 115 | ### Changed 116 | - Reporting energy incrementals (Domoticz, MQTT) 117 | 118 | ### Removed 119 | - Removed current monitor bypass when relay is OFF 120 | - Removed energy API entry point 121 | 122 | ## [1.5.4] 2017-02-03 123 | ### Fixed 124 | - Issue #50. Fix type bug in window variable when calculating energy for HLW8012 devices (Sonoff POW) 125 | 126 | ## [1.5.3] 2017-02-02 127 | ### Fixed 128 | - Issue #50 and #54. Fixed domoticz MQTT message format 129 | 130 | ### Added 131 | - Energy calculation and aggregation. API entry points and MQTT messages. 132 | 133 | ## [1.5.2] 2017-01-29 134 | ### Fixed 135 | - Fix bug in emon topic payload 136 | 137 | ## [1.5.1] 2017-01-28 138 | ### Added 139 | - OpenEnergyMonitor WiFi MQTT Relay / Thermostat support (thanks to Denis French) 140 | 141 | ### Fixed 142 | - NTP connection refresh upon wifi connection 143 | - Filesystem image build using local gulp installation 144 | 145 | ## [1.5.0] 2017-01-21 146 | ### Added 147 | - Pulse mode. Allows to define a pulse time after which the relay will switch back 148 | - API entry points for sensor data (power, current, voltage, temperature and humidity) 149 | - Export sensor data to Domoticz (power, current, voltage, temperature and humidity) 150 | - Configurable (in code) mapping between buttons and relays 151 | - MQTT messages for button events 152 | - Added support for Itead Studio 1CH inching/self locking smart switch board 153 | - Added support for Jan Goedeke Wifi Relay boards (both NC and NO versions) 154 | - Notify OTA updates to websocket clients, automatically reload page 155 | - Support for pulse mode notification LED and button 156 | - Revert relay state mode on boot (thanks to Minh Phuong Ly) 157 | 158 | ### Fixed 159 | - MQTT will topic 160 | - Crash with HLW812 interrupts while trying to create a WIFI connection 161 | - Issue #20 Better inline documentation for Alexa and Domoticz default settings 162 | - Issue #39 Fixed autoconnect issue with static IP (fixed in JustWifi library) 163 | - Issue #41 Added password requirements to initial password change page 164 | 165 | ### Changed 166 | - Changed LED pattern for WIFI notifications (shorter pulses) 167 | 168 | ## [1.4.4] 2017-01-13 169 | ### Added 170 | - Adding current, voltage, apparent and reactive power reports to Sonoff POW (Web & MQTT) 171 | 172 | ### Fixed 173 | - Issue #35 Fixed frequent MQTT connection drops after WIFI reconnect 174 | - Defer wifi disconnection from web interface to allow request to return 175 | 176 | ### Changed 177 | - Move all Arduino IDE configuration values to their own file 178 | - Using latest HLW8012 library in interrupt mode 179 | 180 | ## [1.4.3] 2017-01-11 181 | ### Fixed 182 | - Issue #6 Using forked Time library to prevent conflict with Arduino Core for ESP8266 time.h file in windows machines 183 | 184 | ## [1.4.2] 2017-01-09 185 | ### Added 186 | - Support for inverse logic relays 187 | 188 | ### Fixed 189 | - Issue #31. Fixed error in relay identification from MQTT messages 190 | 191 | ## [1.4.1] 2017-01-05 192 | ### Added 193 | - Alexa support by default on all devices 194 | - Added support for Wemos D1 Mini board with official Relay Shield 195 | 196 | ### Fixed 197 | - Multi-packet websocket frames 198 | 199 | ## [1.4.0] 2016-12-31 200 | ### Added 201 | - Domoticz support via MQTT (https://www.domoticz.com/wiki/MQTT) 202 | - Support for static IP connections 203 | 204 | ### Fixed 205 | - Issue #16. Enforce minimum password strength in web interface 206 | 207 | ### Changed 208 | - Using default client_id provided by AsyncMqttClient 209 | - Allow up to 5 different WIFI networks 210 | 211 | ### Removed 212 | - File system version file 213 | 214 | ## [1.3.1] 2016-12-31 215 | ### Fixed 216 | - data_dir fix for PlatformIO 217 | 218 | ## [1.3.0] 2016-12-30 219 | ### Changed 220 | - Arduino IDE support (changes in the folder structure and documentation) 221 | 222 | ## [1.2.0] 2016-12-27 223 | ### Added 224 | - Force password changing if it's the default one 225 | - Added Last-Modified header to static contents 226 | - Added DNS captive portal for AP mode 227 | - Added support for Sonoff 4CH 228 | - Added support for WorkChoice ecoPlug (ECOPLUG). Thanks to David Myers 229 | - Added support for Sonoff SV 230 | - Added support for Sonoff Touch 231 | - Comment out hardware selection in hardware.h if using Arduino IDE 232 | - Added support for MQTT get/set suffixes (/status, /set, ...) 233 | - Added support for LED notifications via MQTT 234 | - Added EEPROM check commands to terminal interface 235 | 236 | ### Changed 237 | - Using unreleased AsyncMqttClient with stability improvements 238 | - Better decoupling between MQTT and relays/websockets 239 | - Skipping retained MQTT messages (configurable) 240 | 241 | ### Fixed 242 | - Issue #11 Compile error when building sonoff-dual-debug 243 | - Issue #14 MQTT Connection with Username an Password not working 244 | - Issue #17 Moved static variable 'pending' to class variable 245 | 246 | ## [1.1.0] 2016-12-06 247 | ### Added 248 | - Added support for DS18B20 temperature sensor. Thanks to Francesco Boscarino 249 | - Added reset command from console 250 | - Added support for multirelay boards like Sonoff DUAL or Electrodragon ESP Relay Board 251 | 252 | ### Changed 253 | - Not using espressif8266_stage in default environment 254 | - Relay MQTT topics 255 | - API entry points 256 | 257 | ### Removed 258 | - Old non protected API 259 | 260 | ## [1.0.3] 2016-11-29 261 | 262 | ### Added 263 | - WeMo emulation through the fauxmoESP library (control your switch from Alexa!) 264 | - REST API for relay management 265 | - Better dependency definitions in platformio.ini 266 | - Option to define inverse logic to on-board LED 267 | - Built data folder included in repo 268 | 269 | ### Changed 270 | - Using non-interrupt driven mode for HLW8012 271 | - Better documentation 272 | - Small changes to web interface 273 | - Same admin password for web, OTA and WIFI AP mode 274 | 275 | ### Fixed 276 | - Prevent fauxmoESP to be compiled by default 277 | 278 | ### Removed 279 | - Removed ESPurna board to its own repo 280 | 281 | ## [1.0.1] 2016-11-13 282 | 283 | ### Added 284 | - Basic authentication and CSRF to websocket requests 285 | 286 | ## [1.0.0] 2016-11-13 287 | 288 | ### Added 289 | - Using ESPAsyncWebServer (for web & websockets) and AsyncMqttClient 290 | 291 | ## [0.9.9] 2016-11-12 292 | 293 | ### Added 294 | - Preliminary support for Sonoff POW 295 | - Replace AJAX requests with websockets 296 | - Using sprites for images 297 | - Hostname can be changed 298 | - Added initial relay state mode 299 | - Reconnect and reset buttons on web interface 300 | 301 | ### Changed 302 | - Changed long click to reset and double click to AP mode 303 | - Using officially supported platformio.ini file by default 304 | 305 | ### Fixed 306 | - Removed unnecessary memory inefficient code 307 | - Temprary fix for Adafruit DHT library (see https://github.com/adafruit/DHT-sensor-library/issues/62) 308 | 309 | ## [0.9.8] 2016-10-06 310 | 311 | ### Added 312 | - Using PureCMS for the web interface 313 | - Using gulp to build the filesystem files 314 | - Using Embedis for configuration 315 | 316 | ### Changed 317 | - Updated JustWifi library 318 | - Web interface changes 319 | - Using custom platformio.ini file 320 | - Loads of changes in modules 321 | - Added DEBUG_MSG 322 | 323 | ### Fixed 324 | - Clean gulp builder script 325 | 326 | ## [0.9.7] 2016-08-28 327 | 328 | ### Changed 329 | - Moving wifi management to library (JustWifi) 330 | - Split code into modules 331 | 332 | ## [0.9.6] 2016-08-12 333 | 334 | ### Added 335 | - Added heartbeat, version and fsversion MQTT messages 336 | 337 | ### Changed 338 | - GZip 3rd party contents 339 | 340 | ## [0.9.5] 2016-07-31 341 | - Initial stable version 342 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | This is a fork of Xose's Library. I just added IOTappStory compatibility. 2 | 3 | # ESPurna Firmware 4 | 5 | ESPurna ("spark" in Catalan) is a custom firmware for ESP8266 based smart switches. 6 | It was originally developed with the **[IteadStudio Sonoff](https://www.itead.cc/sonoff-wifi-wireless-switch.html)** in mind but now it supports a growing number of ESP8266-based boards. 7 | It uses the Arduino Core for ESP8266 framework and a number of 3rd party libraries. 8 | 9 | **Current Release Version is 1.6.9**, read the [changelog](https://bitbucket.org/xoseperez/espurna/src/master/CHANGELOG.md). 10 | 11 | ## Features 12 | 13 | * Support for **multiple ESP8266-based boards** ([check list](https://bitbucket.org/xoseperez/espurna/wiki/Hardware)) 14 | * Wifi **AP Mode** or **STA mode** 15 | * Up to 5 different networks can be defined 16 | * Supports static IP 17 | * Scans for strongest network if more than one defined 18 | * Defaults to AP mode (also available after double clicking the main button) 19 | * Switch management 20 | * Support for **push buttons** and **toggle switches** 21 | * Configurable **status on boot** (always ON, always OFF, same as before or toggle) 22 | * Support for **pulse mode** (normally ON or normally OFF) with configurable time 23 | * Support for **relay synchronization** (all equal, only one ON, one and only on ON) 24 | * **MQTT** enabled 25 | * Switch on/off and toggle relays 26 | * Report button event notifications 27 | * Enable/disable pulse mode 28 | * Change LED notification mode 29 | * Remote reset the board 30 | * **Alexa** integration using the [FauxmoESP Library](https://bitbucket.org/xoseperez/fauxmoesp) 31 | * [**Domoticz**](https://domoticz.com/) integration via MQTT 32 | * [**Home Assistant**](https://home-assistant.io/) integration via MQTT 33 | * Support for different **sensors** 34 | * DHT11 / DHT22 / DHT21 / AM2301 (supports celsius & fahrenheit reporting) 35 | * DS18B20 (supports celsius & fahrenheit reporting) 36 | * HLW8012 using the [HLW8012 Library](https://bitbucket.org/xoseperez/hlw8012) (Sonoff POW) 37 | * Non-invasive current sensor using the [EmonLiteESP Library](https://bitbucket.org/xoseperez/emonliteesp) (requires some hacking) 38 | * Fast asynchronous **HTTP Server** 39 | * Configurable port 40 | * Basic authentication 41 | * Web-based configuration 42 | * Relay switching and sensor data from the web interface 43 | * Websockets-based communication between the device and the browser 44 | * Backup and restore settings option 45 | * **REST API** (enable/disable from web interface) 46 | * GET and PUT relay status 47 | * GET sensor data (power, current, voltage, temperature and humidity) depending on the available hardware 48 | * **RPC API** (enable/disable from web interface) 49 | * Remote reset the board 50 | * **Over-The-Air** (OTA) updates even for 1Mb boards 51 | * Manually from PlatformIO or Arduino IDE 52 | * Automatic updates through the [NoFUSS Library](https://bitbucket.org/xoseperez/nofuss) 53 | * **Command line configuration** 54 | * Button interface 55 | * Click to toggle relays 56 | * Double click to enter AP mode (only main button) 57 | * Long click (>1 second) to reboot device (only main button) 58 | * Extra long click (>10 seconds) to go back to factory settings (only main button) 59 | 60 | ## Documentation 61 | 62 | For more information please refer to the [ESPurna Wiki](https://bitbucket.org/xoseperez/espurna/wiki/Home). 63 | 64 | ## Supported hardware 65 | 66 | Here is the list of supported hardware. For more information please refer to the [ESPurna Wiki Hardware page](https://bitbucket.org/xoseperez/espurna/wiki/Hardware). 67 | 68 | |||| 69 | |-|-|-| 70 | |![IteadStudio S20](images/devices/s20.jpg)|![WorkChoice EcoPlug](images/devices/workchoice-ecoplug.jpg)|![IteadStudio Sonoff Touch](images/devices/sonoff-touch.jpg)| 71 | |**IteadStudio S20**|**WorkChoice EcoPlug**|**IteadStudio Sonoff Touch**| 72 | |![IteadStudio Slampher](images/devices/slampher.jpg)|![AI-Thinker Wifi Light / Noduino OpenLight](images/devices/aithinker-ailight.jpg)|| 73 | |**IteadStudio Slampher**|**AI-Thinker Wifi Light / Noduino OpenLight**|| 74 | |![IteadStudio Sonoff Basic](images/devices/sonoff-basic.jpg)|![IteadStudio Sonoff RF](images/devices/sonoff-rf.jpg)|![Electrodragon Relay Board](images/devices/electrodragon-relay-board.jpg)| 75 | |**IteadStudio Sonoff Basic**|**IteadStudio Sonoff RF**|**Electrodragon Relay Board**| 76 | |![IteadStudio Sonoff Dual](images/devices/sonoff-dual.jpg)|![IteadStudio Sonoff POW](images/devices/sonoff-pow.jpg)|![IteadStudio Sonoff TH10/TH16](images/devices/sonoff-th10-th16.jpg)| 77 | |**IteadStudio Sonoff Dual**|**IteadStudio Sonoff POW**|**IteadStudio Sonoff TH10/TH16**| 78 | |![IteadStudio Sonoff 4CH](images/devices/sonoff-4ch.jpg)|![OpenEnergyMonitor WiFi MQTT Relay / Thermostat](images/devices/mqtt-relay.jpg)|| 79 | |**IteadStudio Sonoff 4CH**|**OpenEnergyMonitor WiFi MQTT Relay / Thermostat**|| 80 | |![IteadStudio Sonoff SV](images/devices/sonoff-sv.jpg)|![IteadStudio 1CH Inching](images/devices/1ch-inching.jpg)|![IteadStudio Motor Clockwise/Anticlockwise](images/devices/motor-switch.jpg)| 81 | |**IteadStudio Sonoff SV**|**IteadStudio 1CH Inching**|**IteadStudio Motor Clockwise/Anticlockwise**| 82 | |![Wemos D1 Mini Relay Shield](images/devices/d1mini.jpg)|![Jan Goedeke Wifi Relay (NO/NC)](images/devices/jangoe-wifi-relay.png)|![Jorge García Wifi + Relays Board Kit](images/devices/jorgegarcia-wifi-relays-board-kit.jpg)| 83 | |**Wemos D1 Mini Relay Shield**|**Jan Goedeke Wifi Relay (NO/NC)**|**Jorge García Wifi + Relays Board Kit**| 84 | 85 | ## License 86 | 87 | Copyright (C) 2016-2017 by Xose Pérez (@xoseperez) 88 | 89 | This program is free software: you can redistribute it and/or modify 90 | it under the terms of the GNU General Public License as published by 91 | the Free Software Foundation, either version 3 of the License, or 92 | (at your option) any later version. 93 | 94 | This program is distributed in the hope that it will be useful, 95 | but WITHOUT ANY WARRANTY; without even the implied warranty of 96 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 97 | GNU General Public License for more details. 98 | 99 | You should have received a copy of the GNU General Public License 100 | along with this program. If not, see . 101 | -------------------------------------------------------------------------------- /code/.gitignore: -------------------------------------------------------------------------------- 1 | .clang_complete 2 | .gcc-flags.json 3 | .pioenvs 4 | .piolibdeps 5 | -------------------------------------------------------------------------------- /code/.travis.yml: -------------------------------------------------------------------------------- 1 | # Continuous Integration (CI) is the practice, in software 2 | # engineering, of merging all developer working copies with a shared mainline 3 | # several times a day < http://docs.platformio.org/en/latest/ci/index.html > 4 | # 5 | # Documentation: 6 | # 7 | # * Travis CI Embedded Builds with PlatformIO 8 | # < https://docs.travis-ci.com/user/integration/platformio/ > 9 | # 10 | # * PlatformIO integration with Travis CI 11 | # < http://docs.platformio.org/en/latest/ci/travis.html > 12 | # 13 | # * User Guide for `platformio ci` command 14 | # < http://docs.platformio.org/en/latest/userguide/cmd_ci.html > 15 | # 16 | # 17 | # Please choice one of the following templates (proposed below) and uncomment 18 | # it (remove "# " before each line) or use own configuration according to the 19 | # Travis CI documentation (see above). 20 | # 21 | 22 | 23 | # 24 | # Template #1: General project. Test it using existing `platformio.ini`. 25 | # 26 | 27 | language: python 28 | python: 29 | - "2.7" 30 | 31 | sudo: false 32 | cache: 33 | directories: 34 | - "~/.platformio" 35 | 36 | install: 37 | - pip install -U platformio 38 | 39 | script: 40 | - platformio run 41 | 42 | 43 | # 44 | # Template #2: The project is intended to by used as a library with examples 45 | # 46 | 47 | # language: python 48 | # python: 49 | # - "2.7" 50 | # 51 | # sudo: false 52 | # cache: 53 | # directories: 54 | # - "~/.platformio" 55 | # 56 | # env: 57 | # - PLATFORMIO_CI_SRC=path/to/test/file.c 58 | # - PLATFORMIO_CI_SRC=examples/file.ino 59 | # - PLATFORMIO_CI_SRC=path/to/test/directory 60 | # 61 | # install: 62 | # - pip install -U platformio 63 | # 64 | # script: 65 | # - platformio ci --lib="." --board=TYPE_1 --board=TYPE_2 --board=TYPE_N 66 | -------------------------------------------------------------------------------- /code/astyle.conf: -------------------------------------------------------------------------------- 1 | style=attach 2 | indent=spaces=4 3 | indent-classes 4 | indent-switches 5 | indent-namespaces 6 | indent-continuation=4 7 | #indent-preproc-define 8 | #indent-preproc-cond 9 | #indent-preproc-block 10 | indent-col1-comments 11 | pad-oper 12 | pad-comma 13 | pad-header 14 | align-pointer=type 15 | align-reference=type 16 | add-brackets 17 | convert-tabs 18 | suffix=none 19 | -------------------------------------------------------------------------------- /code/build-all: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Environments to build 4 | ENVIRONMENTS="sonoff-debug sonoff-dht22-debug sonoff-ds18b20-debug sonoff-pow-debug sonoff-dual-debug sonoff-4ch-debug 1ch-inching-debug electrodragon-debug ecoplug-debug jangoe-debug ai-light-debug led-controller-debug" 5 | 6 | # Get current version 7 | version=`cat espurna/config/version.h | grep APP_VERSION | awk '{print $3}' | sed 's/"//g'` 8 | echo $version 9 | 10 | # Create output folder 11 | mkdir -p firmware 12 | 13 | # Build all the required firmwares 14 | for environment in $ENVIRONMENTS; do 15 | platformio run -e $environment 16 | mv .pioenvs/$environment/firmware.bin firmware/espurna-$version-$environment.bin 17 | done 18 | 19 | platformio run -vv -t uploadfs -e node-debug 20 | mv .pioenvs/node-debug/spiffs.bin firmware/espurna-$version-spiffs.bin 21 | -------------------------------------------------------------------------------- /code/build-fs: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | node node_modules/gulp/bin/gulp.js 3 | -------------------------------------------------------------------------------- /code/debug: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # ------------------------------------------------------------------------------ 4 | # CONFIGURATION 5 | # ------------------------------------------------------------------------------ 6 | 7 | ENVIRONMENT="d1-debug" 8 | ADDR2LINE=$HOME/.platformio/packages/toolchain-xtensa/bin/xtensa-lx106-elf-addr2line 9 | DECODER=utils/EspStackTraceDecoder.jar 10 | DECODER_ORIGIN=https://github.com/littleyoda/EspStackTraceDecoder/releases/download/untagged-83b6db3208da17a0f1fd/EspStackTraceDecoder.jar 11 | FILE="/tmp/.trace" 12 | 13 | # ------------------------------------------------------------------------------ 14 | # END CONFIGURATION - DO NOT EDIT FURTHER 15 | # ------------------------------------------------------------------------------ 16 | 17 | # remove default trace file 18 | rm -rf $FILE 19 | 20 | function help { 21 | echo 22 | echo "Syntax: $0 [-e ] [-d ]" 23 | echo 24 | } 25 | 26 | # get environment from command line 27 | while [[ $# -gt 1 ]]; do 28 | 29 | key="$1" 30 | 31 | case $key in 32 | -e) 33 | ENVIRONMENT="$2" 34 | shift 35 | ;; 36 | -d) 37 | FILE="$2" 38 | shift 39 | ;; 40 | esac 41 | 42 | shift # past argument or value 43 | 44 | done 45 | 46 | # check environment folder 47 | if [ $ENVIRONMENT == "" ]; then 48 | echo "No environment defined" 49 | help 50 | exit 1 51 | fi 52 | ELF=.pioenvs/$ENVIRONMENT/firmware.elf 53 | if [ ! -f $ELF ]; then 54 | echo "Could not find ELF file for the selected environment: $ELF" 55 | exit 2 56 | fi 57 | 58 | # get decode 59 | if [ ! -f $DECODER ]; then 60 | folder=$(dirname "$DECODER") 61 | if [ $folder != "." ]; then 62 | mkdir -p $folder 63 | fi 64 | echo "Downloading decoder..." 65 | wget -q $DECODER_ORIGIN -O "$DECODER" 66 | fi 67 | 68 | # get trace interactively 69 | if [ ! -f $FILE ]; then 70 | echo "Paste stack trace and end with a blank line:" 71 | trace=$(sed '/^$/q') 72 | echo $trace > $FILE 73 | fi 74 | 75 | java -jar $DECODER $ADDR2LINE $ELF $FILE 76 | -------------------------------------------------------------------------------- /code/deploy: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # ESPurna Deploy Script 4 | # 5 | # Copyright (C) 2016 by Xose Pérez 6 | # 7 | # This program is free software: you can redistribute it and/or modify 8 | # it under the terms of the GNU General Public License as published by 9 | # the Free Software Foundation, either version 3 of the License, or 10 | # (at your option) any later version. 11 | # 12 | # This program is distributed in the hope that it will be useful, 13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | # GNU General Public License for more details. 16 | # 17 | # You should have received a copy of the GNU General Public License 18 | # along with this program. If not, see . 19 | # 20 | 21 | MQTT_HOST=192.168.1.10 22 | 23 | function help() { 24 | echo "Syntax: $0 " 25 | devices 26 | } 27 | 28 | function devices() { 29 | echo "Defined devices:" 30 | cat platformio.ini | grep 'device]' | sed 's/\[env:/ - /g' | sed 's/\-device]//g' 31 | } 32 | 33 | function valid_ip() { 34 | local stat=0 35 | rx='([1-9]?[0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])' 36 | if [[ $ip =~ ^$rx\.$rx\.$rx\.$rx$ ]]; then 37 | stat=1 38 | fi 39 | return $stat 40 | } 41 | 42 | # Check arguments 43 | if [ "$#" -ne 2 ]; then 44 | help 45 | exit 1 46 | fi 47 | device=$1-device 48 | target=$2 49 | 50 | # Get IP 51 | topic=`cat platformio.ini | grep $device -A 2 | grep "topic" | cut -d' ' -f3` 52 | if [ "$topic" == "" ]; then 53 | echo "Unknown device $device or topic not defined" 54 | devices 55 | exit 2 56 | fi 57 | 58 | ip=`mosquitto_sub -t $topic -h $MQTT_HOST -N -C 1` 59 | if valid_ip $ip; then 60 | echo "Could not get a valid IP from MQTT broker" 61 | exit 3 62 | fi 63 | 64 | platformio run -vv -e $device --target $target --upload-port $ip 65 | -------------------------------------------------------------------------------- /code/espurna/IOTappStory.ino: -------------------------------------------------------------------------------- 1 | 2 | #include 3 | 4 | //---------- IOTappStory FUNCTIONS ---------- 5 | byte iotUpdaterSketch(String server, String url, String firmware, bool immediately) { 6 | byte retValue; 7 | DEBUG_MSG("\n\nUpdating Sketch from...\n\n"); 8 | 9 | DEBUG_MSG("Update_server %s\n", server.c_str()); 10 | DEBUG_MSG("UPDATE_URL %s\n", url.c_str()); 11 | DEBUG_MSG("FIRMWARE_VERSION %s\n", firmware.c_str()); 12 | t_httpUpdate_return ret = ESPhttpUpdate.update(server, 80, url, firmware); 13 | switch (ret) { 14 | case HTTP_UPDATE_FAILED: 15 | DEBUG_MSG("SKETCH_UPDATE_FAILD Error (%d): %s\n", ESPhttpUpdate.getLastError(), ESPhttpUpdate.getLastErrorString().c_str()); 16 | retValue = 'F'; 17 | break; 18 | 19 | case HTTP_UPDATE_NO_UPDATES: 20 | DEBUG_MSG("---------- SKETCH_UPDATE_NO_UPDATES ------------------\n"); 21 | retValue = 'A'; 22 | break; 23 | 24 | case HTTP_UPDATE_OK: 25 | DEBUG_MSG("SKETCH_UPDATE_OK\n\n"); 26 | retValue = 'U'; 27 | break; 28 | } 29 | return retValue; 30 | } 31 | 32 | byte iotUpdaterSPIFFS(String server, String url, String firmware, bool immediately) { 33 | byte retValue; 34 | DEBUG_MSG("Updating SPIFFS from...\n"); 35 | DEBUG_MSG("Update_server %s\n", server.c_str()); 36 | DEBUG_MSG("UPDATE_URL %s \n", url.c_str()); 37 | DEBUG_MSG("FIRMWARE_VERSION %s\n", firmware.c_str()); 38 | 39 | t_httpUpdate_return retspiffs = ESPhttpUpdate.updateSpiffs("http://" + String(server + url), firmware); 40 | switch (retspiffs) { 41 | case HTTP_UPDATE_FAILED: 42 | DEBUG_MSG("SPIFFS_UPDATE_FAILD Error (%d): %s\n", ESPhttpUpdate.getLastError(), ESPhttpUpdate.getLastErrorString().c_str()); 43 | retValue = 'F'; 44 | break; 45 | 46 | case HTTP_UPDATE_NO_UPDATES: 47 | DEBUG_MSG("---------- SPIFFS_UPDATE_NO_UPDATES ------------------\n"); 48 | retValue = 'A'; 49 | break; 50 | 51 | case HTTP_UPDATE_OK: 52 | DEBUG_MSG("SPIFFS_UPDATE_OK\n"); 53 | retValue = 'U'; 54 | break; 55 | } 56 | return retValue; 57 | } 58 | 59 | 60 | 61 | void IOTappStory(bool spiffs) { 62 | // update from IOTappStory.com 63 | bool updateHappened = false; 64 | byte res1, res2; 65 | 66 | // sendSysLogMessage(7, 1, config.boardName, FIRMWARE, 10, counter++, "------------- IOTappStory -------------------"); 67 | int wCounter = 0; 68 | while (!wifiConnected()&& wCounter++<30) delay(500); 69 | if (wifiConnected()) { 70 | // DEBUG_MSG("IP = %s \n\n\n", WiFi.localIP().toString().c_str()); 71 | 72 | ESPhttpUpdate.rebootOnUpdate(false); 73 | 74 | res1 = iotUpdaterSketch(IOTappStory1, IOTappStoryPHP1, APP_NAME " " APP_VERSION, true); 75 | if (res1 == 'F') { 76 | // String message = IOTappStory1 + ": Update not succesful"; 77 | // sendSysLogMessage(2, 1, config.boardName, FIRMWARE, 10, counter++, message); 78 | res2 = iotUpdaterSketch(IOTappStory2, IOTappStoryPHP2, APP_NAME, true) ; 79 | if (res2 == 'F') { 80 | // message = IOTappStory2 + ": Update not succesful"; 81 | // sendSysLogMessage(2, 1, config.boardName, FIRMWARE, 10, counter++, message); 82 | } 83 | } 84 | if (res1 == 'U' || res2 == 'U') updateHappened = true; 85 | 86 | DEBUG_MSG("\n\n"); 87 | 88 | if (spiffs) { 89 | res1 = iotUpdaterSPIFFS(IOTappStory1, IOTappStoryPHP1, APP_NAME " " APP_VERSION, true); 90 | if (res1 == 'F') { 91 | // String message = IOTappStory1 + ": Update not succesful"; 92 | // sendSysLogMessage(2, 1, config.boardName, FIRMWARE, 10, counter++, message); 93 | res2 = iotUpdaterSPIFFS(IOTappStory2, IOTappStoryPHP2, APP_NAME, true); 94 | if (res2 == 'F') { 95 | // message = IOTappStory2 + ": Update not succesful"; 96 | // sendSysLogMessage(2, 1, config.boardName, FIRMWARE, 10, counter++, message); 97 | } 98 | } 99 | } 100 | if (res1 == 'U' || res2 == 'U') updateHappened = true; 101 | 102 | DEBUG_MSG("\nReturning from IOTAppstory\n\n\n\n"); 103 | 104 | if (updateHappened) { 105 | ESP.restart(); 106 | } 107 | } else DEBUG_MSG("Wi-Fi not connected...\n"); 108 | } 109 | 110 | 111 | void IOTappStory() { 112 | IOTappStory(false); 113 | } 114 | -------------------------------------------------------------------------------- /code/espurna/button.ino: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | BUTTON MODULE 4 | 5 | Copyright (C) 2016-2017 by Xose Pérez 6 | 7 | */ 8 | 9 | // ----------------------------------------------------------------------------- 10 | // BUTTON 11 | // ----------------------------------------------------------------------------- 12 | 13 | #include 14 | #include 15 | 16 | typedef struct { 17 | DebounceEvent * button; 18 | unsigned long actions; 19 | unsigned int relayID; 20 | } button_t; 21 | 22 | std::vector _buttons; 23 | 24 | #ifdef MQTT_TOPIC_BUTTON 25 | void buttonMQTT(unsigned char id, uint8_t event) { 26 | if (id >= _buttons.size()) return; 27 | char payload[2]; 28 | sprintf(payload, "%d", event); 29 | mqttSend(MQTT_TOPIC_BUTTON, id, payload); 30 | } 31 | #endif 32 | 33 | unsigned char buttonAction(unsigned char id, unsigned char event) { 34 | if (id >= _buttons.size()) return BUTTON_MODE_NONE; 35 | unsigned long actions = _buttons[id].actions; 36 | if (event == BUTTON_EVENT_PRESSED) return (actions) & 0x0F; 37 | if (event == BUTTON_EVENT_CLICK) return (actions >> 4) & 0x0F; 38 | if (event == BUTTON_EVENT_DBLCLICK) return (actions >> 8) & 0x0F; 39 | if (event == BUTTON_EVENT_LNGCLICK) return (actions >> 12) & 0x0F; 40 | if (event == BUTTON_EVENT_LNGLNGCLICK) return (actions >> 16) & 0x0F; 41 | return BUTTON_MODE_NONE; 42 | } 43 | 44 | unsigned long buttonStore(unsigned char pressed, unsigned char click, unsigned char dblclick, unsigned char lngclick, unsigned char lnglngclick) { 45 | unsigned int value; 46 | value = pressed; 47 | value += click << 4; 48 | value += dblclick << 8; 49 | value += lngclick << 12; 50 | value += lnglngclick << 16; 51 | return value; 52 | } 53 | 54 | uint8_t mapEvent(uint8_t event, uint8_t count, uint16_t length) { 55 | if (event == EVENT_PRESSED) return BUTTON_EVENT_PRESSED; 56 | if (event == EVENT_CHANGED) return BUTTON_EVENT_CLICK; 57 | if (event == EVENT_RELEASED) { 58 | if (count == 1) { 59 | if (length > BUTTON_LNGLNGCLICK_LENGTH) return BUTTON_EVENT_LNGLNGCLICK; 60 | if (length > BUTTON_LNGCLICK_LENGTH) return BUTTON_EVENT_LNGCLICK; 61 | return BUTTON_EVENT_CLICK; 62 | } 63 | if (count == 2) return BUTTON_EVENT_DBLCLICK; 64 | } 65 | } 66 | 67 | void buttonEvent(unsigned int id, unsigned char event) { 68 | 69 | DEBUG_MSG_P(PSTR("[BUTTON] Pressed #%d, event: %d\n"), id, event); 70 | if (event == 0) return; 71 | 72 | #ifdef MQTT_TOPIC_BUTTON 73 | buttonMQTT(id, event); 74 | #endif 75 | 76 | unsigned char action = buttonAction(id, event); 77 | 78 | if (action == BUTTON_MODE_TOGGLE) { 79 | if (_buttons[id].relayID > 0) { 80 | relayToggle(_buttons[id].relayID - 1); 81 | } 82 | } 83 | if (action == BUTTON_MODE_AP) createAP(); 84 | if (action == BUTTON_MODE_RESET) ESP.restart(); 85 | if (action == BUTTON_MODE_PULSE) relayPulseToggle(); 86 | if (action == BUTTON_MODE_FACTORY) { 87 | DEBUG_MSG_P(PSTR("\n\nFACTORY RESET\n\n")); 88 | settingsFactoryReset(); 89 | ESP.restart(); 90 | } 91 | 92 | } 93 | 94 | void buttonSetup() { 95 | 96 | #ifdef SONOFF_DUAL 97 | 98 | unsigned int actions = buttonStore(BUTTON_MODE_NONE, BUTTON_MODE_TOGGLE, BUTTON_MODE_NONE, BUTTON_MODE_NONE, BUTTON_MODE_NONE); 99 | _buttons.push_back({new DebounceEvent(0, BUTTON_PUSHBUTTON), 0, 1}); 100 | _buttons.push_back({new DebounceEvent(0, BUTTON_PUSHBUTTON), 0, 2}); 101 | _buttons.push_back({new DebounceEvent(0, BUTTON_PUSHBUTTON), actions, BUTTON3_RELAY}); 102 | 103 | #else 104 | 105 | #ifdef BUTTON1_PIN 106 | { 107 | unsigned int actions = buttonStore(BUTTON1_PRESS, BUTTON1_CLICK, BUTTON1_DBLCLICK, BUTTON1_LNGCLICK, BUTTON1_LNGLNGCLICK); 108 | _buttons.push_back({new DebounceEvent(BUTTON1_PIN, BUTTON1_MODE), actions, BUTTON1_RELAY}); 109 | } 110 | #endif 111 | #ifdef BUTTON2_PIN 112 | { 113 | unsigned int actions = buttonStore(BUTTON2_PRESS, BUTTON2_CLICK, BUTTON2_DBLCLICK, BUTTON2_LNGCLICK, BUTTON2_LNGLNGCLICK); 114 | _buttons.push_back({new DebounceEvent(BUTTON2_PIN, BUTTON2_MODE), actions, BUTTON2_RELAY}); 115 | } 116 | #endif 117 | #ifdef BUTTON3_PIN 118 | { 119 | unsigned int actions = buttonStore(BUTTON3_PRESS, BUTTON3_CLICK, BUTTON3_DBLCLICK, BUTTON3_LNGCLICK, BUTTON3_LNGLNGCLICK); 120 | _buttons.push_back({new DebounceEvent(BUTTON3_PIN, BUTTON3_MODE), actions, BUTTON3_RELAY}); 121 | } 122 | #endif 123 | #ifdef BUTTON4_PIN 124 | { 125 | unsigned int actions = buttonStore(BUTTON4_PRESS, BUTTON4_CLICK, BUTTON4_DBLCLICK, BUTTON4_LNGCLICK, BUTTON4_LNGLNGCLICK); 126 | _buttons.push_back({new DebounceEvent(BUTTON4_PIN, BUTTON4_MODE), actions, BUTTON4_RELAY}); 127 | } 128 | #endif 129 | 130 | #endif 131 | 132 | DEBUG_MSG_P(PSTR("[BUTTON] Number of buttons: %d\n"), _buttons.size()); 133 | 134 | } 135 | 136 | void buttonLoop() { 137 | 138 | #ifdef SONOFF_DUAL 139 | 140 | if (Serial.available() >= 4) { 141 | 142 | unsigned char value; 143 | if (Serial.read() == 0xA0) { 144 | if (Serial.read() == 0x04) { 145 | value = Serial.read(); 146 | if (Serial.read() == 0xA1) { 147 | 148 | // RELAYs and BUTTONs are synchonized in the SIL F330 149 | // The on-board BUTTON2 should toggle RELAY0 value 150 | // Since we are not passing back RELAY2 value 151 | // (in the relayStatus method) it will only be present 152 | // here if it has actually been pressed 153 | if ((value & 4) == 4) { 154 | buttonEvent(2, BUTTON_EVENT_CLICK); 155 | return; 156 | } 157 | 158 | // Otherwise check if any of the other two BUTTONs 159 | // (in the header) has been pressent, but we should 160 | // ensure that we only toggle one of them to avoid 161 | // the synchronization going mad 162 | // This loop is generic for any PSB-04 module 163 | for (unsigned int i=0; i 0; 166 | 167 | // Cjeck if the status for that relay has changed 168 | if (relayStatus(i) != status) { 169 | buttonEvent(i, BUTTON_EVENT_CLICK); 170 | break; 171 | } 172 | 173 | } 174 | 175 | } 176 | } 177 | } 178 | 179 | } 180 | 181 | #else 182 | 183 | for (unsigned int i=0; i < _buttons.size(); i++) { 184 | if (unsigned char event = _buttons[i].button->loop()) { 185 | unsigned char count = _buttons[i].button->getEventCount(); 186 | unsigned long length = _buttons[i].button->getEventLength(); 187 | unsigned char mapped = mapEvent(event, count, length); 188 | buttonEvent(i, mapped); 189 | } 190 | } 191 | 192 | #endif 193 | 194 | } 195 | -------------------------------------------------------------------------------- /code/espurna/config/all.h: -------------------------------------------------------------------------------- 1 | #include "version.h" 2 | #include "arduino.h" 3 | #include "prototypes.h" 4 | #include "general.h" 5 | #include "hardware.h" 6 | #include "sensors.h" 7 | #include "debug.h" 8 | 9 | /* 10 | If you want to modify the stock configuration but you don't want to touch 11 | the repo files you can uncomment the following line and add a "custom.h" 12 | file to this same folder. 13 | Check https://bitbucket.org/xoseperez/espurna/issues/104/general_customh 14 | for an example on how to use this file. 15 | */ 16 | //#include "custom.h" 17 | -------------------------------------------------------------------------------- /code/espurna/config/arduino.h: -------------------------------------------------------------------------------- 1 | //-------------------------------------------------------------------------------- 2 | // These settings are normally provided by PlatformIO 3 | // Uncomment the appropiate line(s) to build from the Arduino IDE 4 | //-------------------------------------------------------------------------------- 5 | 6 | //-------------------------------------------------------------------------------- 7 | // General 8 | //-------------------------------------------------------------------------------- 9 | 10 | #ifndef DEBUG_PORT 11 | #define DEBUG_PORT Serial 12 | #endif 13 | 14 | // Uncomment and configure these lines to enable remote debug via udpDebug 15 | // To receive the messages on the destination computer use nc: 16 | // nc -ul 8111 17 | 18 | //#define DEBUG_UDP_IP IPAddress(192, 168, 1, 100) 19 | //#define DEBUG_UDP_PORT 8111 20 | 21 | //-------------------------------------------------------------------------------- 22 | // Hardware 23 | //-------------------------------------------------------------------------------- 24 | 25 | #ifdef ARDUINO_ESP8266_ESP01 // Generic ESP's 26 | #define SONOFF 27 | #else 28 | #define PIR_SENSOR 29 | #endif 30 | 31 | // #define PIR_SENSOR 32 | //#define D1_RELAYSHIELD 33 | //#define NODEMCUV2 34 | //#define SONOFF 35 | //#define SONOFF_TH 36 | //#define SLAMPHER 37 | //#define S20 38 | //#define SONOFF_TOUCH 39 | //#define SONOFF_SV 40 | //#define SONOFF_POW 41 | //#define SONOFF_DUAL 42 | //#define SONOFF_4CH 43 | //#define ESP_RELAY_BOARD 44 | //#define ECOPLUG 45 | //#define WIFI_RELAY_NC 46 | //#define WIFI_RELAY_NO 47 | //#define MQTT_RELAY 48 | //#define WIFI_RELAYS_BOARD_KIT 49 | 50 | //-------------------------------------------------------------------------------- 51 | // Features (values below are non-default values) 52 | //-------------------------------------------------------------------------------- 53 | 54 | //#define ENABLE_DHT 1 55 | //#define ENABLE_DS18B20 1 56 | //#define ENABLE_EMON 1 57 | //#define ENABLE_HLW8018 1 58 | //#define ENABLE_RF 0 59 | #define ENABLE_FAUXMO 1 60 | //#define ENABLE_NOFUSS 0 61 | //#define ENABLE_DOMOTICZ 0 62 | #define ENABLE_IOTAPPSTORY 1 63 | -------------------------------------------------------------------------------- /code/espurna/config/debug.h: -------------------------------------------------------------------------------- 1 | #define DEBUG_MESSAGE_MAX_LENGTH 80 2 | 3 | #ifdef SONOFF_DUAL 4 | #undef DEBUG_PORT 5 | #endif 6 | 7 | #if defined(DEBUG_PORT) | defined(DEBUG_UDP_IP) 8 | #define DEBUG_MSG(...) debugSend(__VA_ARGS__) 9 | #define DEBUG_MSG_P(...) debugSend_P(__VA_ARGS__) 10 | #endif 11 | 12 | #ifndef DEBUG_MSG 13 | #define DEBUG_MSG(...) 14 | #define DEBUG_MSG_P(...) 15 | #endif 16 | -------------------------------------------------------------------------------- /code/espurna/config/general.h: -------------------------------------------------------------------------------- 1 | //------------------------------------------------------------------------------ 2 | // GENERAL 3 | //------------------------------------------------------------------------------ 4 | 5 | #define SERIAL_BAUDRATE 115200 6 | #define HOSTNAME DEVICE 7 | #define BUFFER_SIZE 1024 8 | #define HEARTBEAT_INTERVAL 300000 9 | #define UPTIME_OVERFLOW 4294967295 10 | 11 | //-------------------------------------------------------------------------------- 12 | // EEPROM 13 | //-------------------------------------------------------------------------------- 14 | 15 | #define EEPROM_RELAY_STATUS 0 16 | #define EEPROM_ENERGY_COUNT 1 17 | #define EEPROM_DATA_END 5 18 | 19 | //-------------------------------------------------------------------------------- 20 | // BUTTON 21 | //-------------------------------------------------------------------------------- 22 | 23 | #define BUTTON_LNGCLICK_LENGTH 1000 24 | #define BUTTON_LNGLNGCLICK_LENGTH 10000 25 | 26 | #define BUTTON_EVENT_NONE 0 27 | #define BUTTON_EVENT_PRESSED 1 28 | #define BUTTON_EVENT_CLICK 2 29 | #define BUTTON_EVENT_DBLCLICK 3 30 | #define BUTTON_EVENT_LNGCLICK 4 31 | #define BUTTON_EVENT_LNGLNGCLICK 5 32 | 33 | #define BUTTON_MODE_NONE 0 34 | #define BUTTON_MODE_TOGGLE 1 35 | #define BUTTON_MODE_AP 2 36 | #define BUTTON_MODE_RESET 3 37 | #define BUTTON_MODE_PULSE 4 38 | #define BUTTON_MODE_FACTORY 5 39 | 40 | #define BUTTON_DEFAULT_MODE BUTTON_MODE_TOGGLE 41 | 42 | //-------------------------------------------------------------------------------- 43 | // RELAY 44 | //-------------------------------------------------------------------------------- 45 | 46 | #define RELAY_MODE_OFF 0 47 | #define RELAY_MODE_ON 1 48 | #define RELAY_MODE_SAME 2 49 | #define RELAY_MODE_TOOGLE 3 50 | 51 | #define RELAY_SYNC_ANY 0 52 | #define RELAY_SYNC_NONE_OR_ONE 1 53 | #define RELAY_SYNC_ONE 2 54 | #define RELAY_SYNC_SAME 3 55 | 56 | #define RELAY_PULSE_NONE 0 57 | #define RELAY_PULSE_OFF 1 58 | #define RELAY_PULSE_ON 2 59 | 60 | #define RELAY_PROVIDER_RELAY 0 61 | #define RELAY_PROVIDER_DUAL 1 62 | #define RELAY_PROVIDER_LIGHT 2 63 | 64 | // Pulse time in seconds 65 | #define RELAY_PULSE_TIME 1 66 | 67 | // 0 means OFF, 1 ON and 2 whatever was before 68 | #define RELAY_MODE RELAY_MODE_OFF 69 | 70 | // 0 means ANY, 1 zero or one and 2 one and only one 71 | #define RELAY_SYNC RELAY_SYNC_ANY 72 | 73 | // 0 means no pulses, 1 means normally off, 2 normally on 74 | #define RELAY_PULSE_MODE RELAY_PULSE_NONE 75 | 76 | //-------------------------------------------------------------------------------- 77 | // I18N 78 | //-------------------------------------------------------------------------------- 79 | 80 | #define TMP_CELSIUS 0 81 | #define TMP_FAHRENHEIT 1 82 | #define TMP_UNITS TMP_CELSIUS 83 | 84 | //-------------------------------------------------------------------------------- 85 | // LED 86 | //-------------------------------------------------------------------------------- 87 | 88 | // All defined LEDs in the board can be managed through MQTT 89 | // except the first one when LED_AUTO is set to 1. 90 | // If LED_AUTO is set to 1 the board will use first defined LED to show wifi status. 91 | #define LED_AUTO 1 92 | 93 | // ----------------------------------------------------------------------------- 94 | // WIFI & WEB 95 | // ----------------------------------------------------------------------------- 96 | 97 | #define WIFI_RECONNECT_INTERVAL 120000 98 | #define WIFI_MAX_NETWORKS 5 99 | #define ADMIN_PASS "admin" 100 | #define FORCE_CHANGE_PASS 1 101 | #define HTTP_USERNAME "admin" 102 | #define WS_BUFFER_SIZE 5 103 | #define WS_TIMEOUT 1800000 104 | #define WEBSERVER_PORT 80 105 | #define DNS_PORT 53 106 | #define ENABLE_MDNS 1 107 | 108 | #define WEB_MODE_NORMAL 0 109 | #define WEB_MODE_PASSWORD 1 110 | 111 | #define AP_MODE AP_MODE_ALONE 112 | 113 | // This option builds the firmware with the web interface embedded. 114 | // You first have to build the data.h file that holds the contents 115 | // of the web interface by running "gulp buildfs_embed" 116 | 117 | #ifndef EMBEDDED_WEB 118 | #define EMBEDDED_WEB 1 119 | #endif 120 | 121 | // ----------------------------------------------------------------------------- 122 | // OTA & NOFUSS 123 | // ----------------------------------------------------------------------------- 124 | 125 | #define OTA_PORT 8266 126 | #define NOFUSS_SERVER "" 127 | #define NOFUSS_INTERVAL 3600000 128 | 129 | // ----------------------------------------------------------------------------- 130 | // MQTT 131 | // ----------------------------------------------------------------------------- 132 | 133 | #ifndef MQTT_USE_ASYNC 134 | #define MQTT_USE_ASYNC 1 135 | #endif 136 | 137 | #define MQTT_SERVER "192.168.0.203" 138 | #define MQTT_PORT 1883 139 | #define MQTT_TOPIC "LAB/{identifier}" 140 | #define MQTT_RETAIN true 141 | #define MQTT_QOS 1 142 | #define MQTT_KEEPALIVE 30 143 | #define MQTT_RECONNECT_DELAY 10000 144 | #define MQTT_TRY_INTERVAL 30000 145 | #define MQTT_MAX_TRIES 12 146 | #define MQTT_SKIP_RETAINED 1 147 | #define MQTT_SKIP_TIME 1000 148 | 149 | #define MQTT_TOPIC_ACTION "/action" 150 | #define MQTT_TOPIC_RELAY "/relay" 151 | #define MQTT_TOPIC_LED "/led" 152 | #define MQTT_TOPIC_ALEXA "/alexa" 153 | #define MQTT_TOPIC_COLOR "/color" 154 | #define MQTT_TOPIC_BUTTON "/button" 155 | #define MQTT_TOPIC_IP "/ip" 156 | #define MQTT_TOPIC_VERSION "/version" 157 | #define MQTT_TOPIC_UPTIME "/uptime" 158 | #define MQTT_TOPIC_FREEHEAP "/freeheap" 159 | #define MQTT_TOPIC_VCC "/vcc" 160 | #define MQTT_TOPIC_STATUS "/status" 161 | #define MQTT_TOPIC_MAC "/mac" 162 | #define MQTT_TOPIC_APP "/app" 163 | #define MQTT_TOPIC_INTERVAL "/interval" 164 | #define MQTT_TOPIC_HOSTNAME "/hostname" 165 | 166 | // Periodic reports 167 | #define MQTT_REPORT_STATUS 1 168 | #define MQTT_REPORT_IP 1 169 | #define MQTT_REPORT_MAC 1 170 | #define MQTT_REPORT_UPTIME 1 171 | #define MQTT_REPORT_FREEHEAP 1 172 | #define MQTT_REPORT_VCC 1 173 | #define MQTT_REPORT_RELAY 1 174 | #define MQTT_REPORT_HOSTNAME 1 175 | #define MQTT_REPORT_APP 1 176 | #define MQTT_REPORT_VERSION 1 177 | #define MQTT_REPORT_INTERVAL 0 178 | 179 | #define MQTT_STATUS_ONLINE "1" 180 | #define MQTT_STATUS_OFFLINE "0" 181 | 182 | #define MQTT_ACTION_RESET "reset" 183 | 184 | #define MQTT_CONNECT_EVENT 0 185 | #define MQTT_DISCONNECT_EVENT 1 186 | #define MQTT_MESSAGE_EVENT 2 187 | 188 | // Custom get and set postfixes 189 | // Use something like "/status" or "/set", with leading slash 190 | #define MQTT_USE_GETTER "" 191 | #define MQTT_USE_SETTER "" 192 | 193 | // ----------------------------------------------------------------------------- 194 | // I2C 195 | // ----------------------------------------------------------------------------- 196 | 197 | #define ENABLE_I2C 0 198 | #define I2C_SDA_PIN 4 199 | #define I2C_SCL_PIN 14 200 | #define I2C_CLOCK_STRETCH_TIME 200 201 | #define I2C_SCL_FREQUENCY 1000 202 | 203 | // ----------------------------------------------------------------------------- 204 | // LIGHT 205 | // ----------------------------------------------------------------------------- 206 | 207 | #define LIGHT_PROVIDER_NONE 0 208 | #define LIGHT_PROVIDER_WS2812 1 209 | #define LIGHT_PROVIDER_RGB 2 210 | #define LIGHT_PROVIDER_RGBW 3 211 | #define LIGHT_PROVIDER_MY9192 4 212 | 213 | #define LIGHT_DEFAULT_COLOR "#000080" 214 | #define LIGHT_SAVE_DELAY 5 215 | 216 | #define MY9291_DI_PIN 13 217 | #define MY9291_DCKI_PIN 15 218 | #define MY9291_COMMAND MY9291_COMMAND_DEFAULT 219 | 220 | // Shared settings between RGB and RGBW lights 221 | #define RGBW_INVERSE_LOGIC 1 222 | #define RGBW_RED_PIN 14 223 | #define RGBW_GREEN_PIN 5 224 | #define RGBW_BLUE_PIN 12 225 | #define RGBW_WHITE_PIN 13 226 | 227 | // ----------------------------------------------------------------------------- 228 | // DOMOTICZ 229 | // ----------------------------------------------------------------------------- 230 | 231 | #ifndef ENABLE_DOMOTICZ 232 | #define ENABLE_DOMOTICZ 0 233 | #endif 234 | #define DOMOTICZ_IN_TOPIC "domoticz/in" 235 | #define DOMOTICZ_OUT_TOPIC "domoticz/out" 236 | 237 | // ----------------------------------------------------------------------------- 238 | // NTP 239 | // ----------------------------------------------------------------------------- 240 | 241 | #define NTP_SERVER "ch.pool.ntp.org" 242 | #define NTP_TIME_OFFSET 1 243 | #define NTP_DAY_LIGHT true 244 | #define NTP_UPDATE_INTERVAL 1800 245 | 246 | // ----------------------------------------------------------------------------- 247 | // FAUXMO 248 | // ----------------------------------------------------------------------------- 249 | 250 | // This setting defines whether Alexa support should be built into the firmware 251 | #ifndef ENABLE_FAUXMO 252 | #define ENABLE_FAUXMO 1 253 | #endif 254 | 255 | // This is default value for the fauxmoEnabled setting that defines whether 256 | // this device should be discoberable and respond to Alexa commands. 257 | // Both ENABLE_FAUXMO and fauxmoEnabled should be 1 for Alexa support to work. 258 | #define FAUXMO_ENABLED 1 259 | 260 | // ----------------------------------------------------------------------------- 261 | // IOTAPPSTORY 262 | // ----------------------------------------------------------------------------- 263 | 264 | #define IOTappStory1 "iotappstory.com" 265 | #define IOTappStoryPHP1 "/ota/esp8266-v1.php" 266 | #define IOTappStory2 "192.168.0.200" 267 | #define IOTappStoryPHP2 "/iotappstory/iotappstoryv20.php" 268 | 269 | -------------------------------------------------------------------------------- /code/espurna/config/prototypes.h: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | typedef std::function apiGetCallbackFunction; 9 | typedef std::function apiPutCallbackFunction; 10 | void apiRegister(const char * url, const char * key, apiGetCallbackFunction getFn, apiPutCallbackFunction putFn = NULL); 11 | void mqttRegister(void (*callback)(unsigned int, const char *, const char *)); 12 | String mqttSubtopic(char * topic); 13 | template bool setSetting(const String& key, T value); 14 | template String getSetting(const String& key, T defaultValue); 15 | template void domoticzSend(const char * key, T value); 16 | template void domoticzSend(const char * key, T nvalue, const char * svalue); 17 | -------------------------------------------------------------------------------- /code/espurna/config/sensors.h: -------------------------------------------------------------------------------- 1 | //-------------------------------------------------------------------------------- 2 | // Custom RF module 3 | // Check http://tinkerman.cat/adding-rf-to-a-non-rf-itead-sonoff/ 4 | // Enable support by passing ENABLE_RF=1 build flag 5 | //-------------------------------------------------------------------------------- 6 | 7 | #define RF_PIN 14 8 | #define RF_CHANNEL 31 9 | #define RF_DEVICE 1 10 | 11 | //-------------------------------------------------------------------------------- 12 | // DHTXX temperature/humidity sensor 13 | // Enable support by passing ENABLE_DHT=1 build flag 14 | //-------------------------------------------------------------------------------- 15 | 16 | #define DHT_PIN 14 17 | #define DHT_UPDATE_INTERVAL 60000 18 | #define DHT_TYPE DHT22 19 | #define DHT_TIMING 11 20 | #define DHT_TEMPERATURE_TOPIC "/temperature" 21 | #define DHT_HUMIDITY_TOPIC "/humidity" 22 | 23 | #define HUMIDITY_NORMAL 0 24 | #define HUMIDITY_COMFORTABLE 1 25 | #define HUMIDITY_DRY 2 26 | #define HUMIDITY_WET 3 27 | 28 | //-------------------------------------------------------------------------------- 29 | // DS18B20 temperature sensor 30 | // Enable support by passing ENABLE_DS18B20=1 build flag 31 | //-------------------------------------------------------------------------------- 32 | 33 | #define DS_PIN 14 34 | #define DS_UPDATE_INTERVAL 60000 35 | #define DS_TEMPERATURE_TOPIC "/temperature" 36 | 37 | //-------------------------------------------------------------------------------- 38 | // Custom current sensor 39 | // Check http://tinkerman.cat/your-laundry-is-done/ 40 | // Check http://tinkerman.cat/power-monitoring-sonoff-th-adc121/ 41 | // Enable support by passing ENABLE_EMON=1 build flag 42 | //-------------------------------------------------------------------------------- 43 | 44 | #define EMON_ANALOG_PROVIDER 0 45 | #define EMON_ADC121_PROVIDER 1 46 | 47 | // If you select EMON_ADC121_PROVIDER you need to enable and configure I2C in general.h 48 | #define EMON_PROVIDER EMON_ANALOG_PROVIDER 49 | 50 | #if EMON_PROVIDER == EMON_ANALOG_PROVIDER 51 | #define EMON_CURRENT_PIN 0 52 | #define EMON_ADC_BITS 10 53 | #define EMON_REFERENCE_VOLTAGE 1.0 54 | #define EMON_CURRENT_PRECISION 1 55 | #define EMON_CURRENT_OFFSET 0.25 56 | #if ENABLE_EMON 57 | #undef ENABLE_ADC_VCC 58 | #define ENABLE_ADC_VCC 0 59 | #endif 60 | #endif 61 | 62 | #if EMON_PROVIDER == EMON_ADC121_PROVIDER 63 | #define EMON_ADC121_ADDRESS 0x50 64 | #define EMON_ADC_BITS 12 65 | #define EMON_REFERENCE_VOLTAGE 3.3 66 | #define EMON_CURRENT_PRECISION 2 67 | #define EMON_CURRENT_OFFSET 0.10 68 | #endif 69 | 70 | #define EMON_CURRENT_RATIO 30 71 | #define EMON_SAMPLES 1000 72 | #define EMON_INTERVAL 10000 73 | #define EMON_MEASUREMENTS 6 74 | #define EMON_MAINS_VOLTAGE 230 75 | #define EMON_APOWER_TOPIC "/apower" 76 | #define EMON_ENERGY_TOPIC "/energy" 77 | #define EMON_CURRENT_TOPIC "/current" 78 | 79 | //-------------------------------------------------------------------------------- 80 | // HLW8012 power sensor (Sonoff POW) 81 | // Enable support by passing ENABLE_POW=1 build flag 82 | // Enabled by default when selecting SONOFF_POW hardware 83 | //-------------------------------------------------------------------------------- 84 | 85 | #define POW_SEL_PIN 5 86 | #define POW_CF1_PIN 13 87 | #define POW_CF_PIN 14 88 | #define POW_USE_INTERRUPTS 1 89 | #define POW_SEL_CURRENT HIGH 90 | #define POW_CURRENT_R 0.001 91 | #define POW_VOLTAGE_R_UP ( 5 * 470000 ) // Real: 2280k 92 | #define POW_VOLTAGE_R_DOWN ( 1000 ) // Real 1.009k 93 | #define POW_POWER_TOPIC "/power" 94 | #define POW_CURRENT_TOPIC "/current" 95 | #define POW_VOLTAGE_TOPIC "/voltage" 96 | #define POW_APOWER_TOPIC "/apower" 97 | #define POW_RPOWER_TOPIC "/rpower" 98 | #define POW_PFACTOR_TOPIC "/pfactor" 99 | #define POW_ENERGY_TOPIC "/energy" 100 | #define POW_UPDATE_INTERVAL 5000 101 | #define POW_REPORT_EVERY 12 102 | 103 | //-------------------------------------------------------------------------------- 104 | // Internal power montior 105 | // Enable support by passing ENABLE_ADC_VCC=1 build flag 106 | // Do not enable this if using the analog GPIO for any other thing 107 | //-------------------------------------------------------------------------------- 108 | 109 | #ifndef ENABLE_ADC_VCC 110 | #define ENABLE_ADC_VCC 1 111 | #endif 112 | 113 | #if ENABLE_ADC_VCC 114 | ADC_MODE(ADC_VCC); 115 | #endif 116 | -------------------------------------------------------------------------------- /code/espurna/config/version.h: -------------------------------------------------------------------------------- 1 | #define APP_NAME "ESPurna" 2 | #define APP_VERSION "1.6.9" 3 | #define APP_AUTHOR "xose.perez@gmail.com" 4 | #define APP_WEBSITE "http://tinkerman.cat" 5 | -------------------------------------------------------------------------------- /code/espurna/data/index.html.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SensorsIot/Espurna-Framework/ed8c754fd86a704670ef38187313f0dab3e1ecba/code/espurna/data/index.html.gz -------------------------------------------------------------------------------- /code/espurna/debug.ino: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | DEBUG MODULE 4 | 5 | Copyright (C) 2016-2017 by Xose Pérez 6 | 7 | */ 8 | 9 | #include 10 | #include 11 | 12 | #ifdef DEBUG_UDP_IP 13 | #include 14 | WiFiUDP udpDebug; 15 | #endif 16 | 17 | void debugSend(const char * format, ...) { 18 | 19 | char buffer[DEBUG_MESSAGE_MAX_LENGTH+1]; 20 | 21 | va_list args; 22 | va_start(args, format); 23 | ets_vsnprintf(buffer, DEBUG_MESSAGE_MAX_LENGTH, format, args); 24 | va_end(args); 25 | 26 | #ifdef DEBUG_PORT 27 | DEBUG_PORT.printf(buffer); 28 | #endif 29 | 30 | #ifdef DEBUG_UDP_IP 31 | udpDebug.beginPacket(DEBUG_UDP_IP, DEBUG_UDP_PORT); 32 | udpDebug.write(buffer); 33 | udpDebug.endPacket(); 34 | #endif 35 | 36 | } 37 | 38 | void debugSend_P(PGM_P format, ...) { 39 | 40 | char buffer[DEBUG_MESSAGE_MAX_LENGTH+1]; 41 | 42 | char f[DEBUG_MESSAGE_MAX_LENGTH+1]; 43 | memcpy_P(f, format, DEBUG_MESSAGE_MAX_LENGTH); 44 | 45 | va_list args; 46 | va_start(args, format); 47 | ets_vsnprintf(buffer, DEBUG_MESSAGE_MAX_LENGTH, f, args); 48 | va_end(args); 49 | 50 | #ifdef DEBUG_PORT 51 | DEBUG_PORT.printf(buffer); 52 | #endif 53 | 54 | #ifdef DEBUG_UDP_IP 55 | udpDebug.beginPacket(DEBUG_UDP_IP, DEBUG_UDP_PORT); 56 | udpDebug.write(buffer); 57 | udpDebug.endPacket(); 58 | #endif 59 | 60 | } 61 | -------------------------------------------------------------------------------- /code/espurna/dht.ino: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | DHT MODULE 4 | 5 | Copyright (C) 2016-2017 by Xose Pérez 6 | 7 | */ 8 | 9 | #if ENABLE_DHT 10 | 11 | #include 12 | #include 13 | 14 | DHT dht(DHT_PIN, DHT_TYPE, DHT_TIMING); 15 | 16 | double _dhtTemperature = 0; 17 | unsigned int _dhtHumidity = 0; 18 | 19 | // ----------------------------------------------------------------------------- 20 | // Values 21 | // ----------------------------------------------------------------------------- 22 | 23 | double getDHTTemperature() { 24 | return _dhtTemperature; 25 | } 26 | 27 | unsigned int getDHTHumidity() { 28 | return _dhtHumidity; 29 | } 30 | 31 | void dhtSetup() { 32 | dht.begin(); 33 | apiRegister("/api/temperature", "temperature", [](char * buffer, size_t len) { 34 | dtostrf(_dhtTemperature, len-1, 1, buffer); 35 | }); 36 | apiRegister("/api/humidity", "humidity", [](char * buffer, size_t len) { 37 | snprintf(buffer, len, "%d", _dhtHumidity); 38 | }); 39 | } 40 | 41 | void dhtLoop() { 42 | 43 | // Check if we should read new data 44 | static unsigned long last_update = 0; 45 | if ((millis() - last_update > DHT_UPDATE_INTERVAL) || (last_update == 0)) { 46 | last_update = millis(); 47 | 48 | unsigned char tmpUnits = getSetting("tmpUnits", TMP_UNITS).toInt(); 49 | 50 | // Read sensor data 51 | double h = dht.readHumidity(); 52 | double t = dht.readTemperature(tmpUnits == TMP_FAHRENHEIT); 53 | 54 | // Check if readings are valid 55 | if (isnan(h) || isnan(t)) { 56 | 57 | DEBUG_MSG_P(PSTR("[DHT] Error reading sensor\n")); 58 | 59 | } else { 60 | 61 | _dhtTemperature = t; 62 | _dhtHumidity = h; 63 | 64 | char temperature[6]; 65 | char humidity[6]; 66 | dtostrf(t, 4, 1, temperature); 67 | itoa((unsigned int) h, humidity, 10); 68 | 69 | DEBUG_MSG_P(PSTR("[DHT] Temperature: %s%s\n"), temperature, (tmpUnits == TMP_CELSIUS) ? "ºC" : "ºF"); 70 | DEBUG_MSG_P(PSTR("[DHT] Humidity: %s\n"), humidity); 71 | 72 | // Send MQTT messages 73 | mqttSend(getSetting("dhtTmpTopic", DHT_TEMPERATURE_TOPIC).c_str(), temperature); 74 | mqttSend(getSetting("dhtHumTopic", DHT_HUMIDITY_TOPIC).c_str(), humidity); 75 | 76 | // Send to Domoticz 77 | #if ENABLE_DOMOTICZ 78 | { 79 | domoticzSend("dczTmpIdx", 0, temperature); 80 | int status; 81 | if (h > 70) { 82 | status = HUMIDITY_WET; 83 | } else if (h > 45) { 84 | status = HUMIDITY_COMFORTABLE; 85 | } else if (h > 30) { 86 | status = HUMIDITY_NORMAL; 87 | } else { 88 | status = HUMIDITY_DRY; 89 | } 90 | char buffer[2]; 91 | sprintf(buffer, "%d", status); 92 | domoticzSend("dczHumIdx", humidity, buffer); 93 | } 94 | #endif 95 | 96 | // Update websocket clients 97 | char buffer[100]; 98 | sprintf_P(buffer, PSTR("{\"dhtVisible\": 1, \"dhtTmp\": %s, \"dhtHum\": %s, \"tmpUnits\": %d}"), temperature, humidity, tmpUnits); 99 | wsSend(buffer); 100 | 101 | } 102 | 103 | } 104 | 105 | } 106 | 107 | #endif 108 | -------------------------------------------------------------------------------- /code/espurna/domoticz.ino: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | DOMOTICZ MODULE 4 | 5 | Copyright (C) 2016-2017 by Xose Pérez 6 | 7 | */ 8 | 9 | #if ENABLE_DOMOTICZ 10 | 11 | template void domoticzSend(const char * key, T nvalue, const char * svalue) { 12 | unsigned int idx = getSetting(key).toInt(); 13 | if (idx > 0) { 14 | char payload[128]; 15 | snprintf(payload, 128, "{\"idx\": %d, \"nvalue\": %s, \"svalue\": \"%s\"}", idx, String(nvalue).c_str(), svalue); 16 | mqttSendRaw(getSetting("dczTopicIn", DOMOTICZ_IN_TOPIC).c_str(), payload); 17 | } 18 | } 19 | 20 | template void domoticzSend(const char * key, T nvalue) { 21 | domoticzSend(key, nvalue, ""); 22 | } 23 | 24 | #endif 25 | -------------------------------------------------------------------------------- /code/espurna/ds18b20.ino: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | DS18B20 MODULE 4 | 5 | Copyright (C) 2016-2017 by Xose Pérez 6 | 7 | */ 8 | 9 | #if ENABLE_DS18B20 10 | 11 | #include 12 | #include 13 | 14 | OneWire oneWire(DS_PIN); 15 | DallasTemperature ds18b20(&oneWire); 16 | 17 | bool _dsIsConnected = false; 18 | double _dsTemperature = 0; 19 | char _dsTemperatureStr[6]; 20 | 21 | // ----------------------------------------------------------------------------- 22 | // DS18B20 23 | // ----------------------------------------------------------------------------- 24 | 25 | double getDSTemperature() { 26 | return _dsTemperature; 27 | } 28 | 29 | const char* getDSTemperatureStr() { 30 | if (!_dsIsConnected) 31 | return "NOT CONNECTED"; 32 | 33 | return _dsTemperatureStr; 34 | } 35 | 36 | void dsSetup() { 37 | ds18b20.begin(); 38 | ds18b20.setWaitForConversion(false); 39 | 40 | apiRegister("/api/temperature", "temperature", [](char * buffer, size_t len) { 41 | dtostrf(_dsTemperature, len-1, 1, buffer); 42 | }); 43 | } 44 | 45 | void dsLoop() { 46 | 47 | // Check if we should read new data 48 | static unsigned long last_update = 0; 49 | static bool requested = false; 50 | if ((millis() - last_update > DS_UPDATE_INTERVAL) || (last_update == 0)) { 51 | if (!requested) { 52 | ds18b20.requestTemperatures(); 53 | requested = true; 54 | 55 | /* Requesting takes time, 56 | * so data will probably not be available in this round */ 57 | return; 58 | } 59 | 60 | /* Check if requested data is already available */ 61 | if (!ds18b20.isConversionComplete()) { 62 | return; 63 | } 64 | 65 | requested = false; 66 | last_update = millis(); 67 | 68 | unsigned char tmpUnits = getSetting("tmpUnits", TMP_UNITS).toInt(); 69 | 70 | // Read sensor data 71 | double t = (tmpUnits == TMP_CELSIUS) ? ds18b20.getTempCByIndex(0) : ds18b20.getTempFByIndex(0); 72 | 73 | // Check if readings are valid 74 | if (isnan(t) || t < -50) { 75 | 76 | DEBUG_MSG_P(PSTR("[DS18B20] Error reading sensor\n")); 77 | 78 | } else { 79 | 80 | _dsTemperature = t; 81 | 82 | if ((tmpUnits == TMP_CELSIUS && _dsTemperature == DEVICE_DISCONNECTED_C) || 83 | (tmpUnits == TMP_FAHRENHEIT && _dsTemperature == DEVICE_DISCONNECTED_F)) 84 | _dsIsConnected = false; 85 | else 86 | _dsIsConnected = true; 87 | 88 | dtostrf(t, 5, 1, _dsTemperatureStr); 89 | 90 | DEBUG_MSG_P(PSTR("[DS18B20] Temperature: %s%s\n"), 91 | getDSTemperatureStr(), 92 | (_dsIsConnected ? ((tmpUnits == TMP_CELSIUS) ? "ºC" : "ºF") : "")); 93 | 94 | // Send MQTT messages 95 | mqttSend(getSetting("dsTmpTopic", DS_TEMPERATURE_TOPIC).c_str(), _dsTemperatureStr); 96 | 97 | // Send to Domoticz 98 | #if ENABLE_DOMOTICZ 99 | domoticzSend("dczTmpIdx", 0, _dsTemperatureStr); 100 | #endif 101 | 102 | // Update websocket clients 103 | char buffer[100]; 104 | sprintf_P(buffer, PSTR("{\"dsVisible\": 1, \"dsTmp\": %s, \"tmpUnits\": %d}"), getDSTemperatureStr(), tmpUnits); 105 | wsSend(buffer); 106 | 107 | } 108 | 109 | } 110 | 111 | } 112 | 113 | #endif 114 | -------------------------------------------------------------------------------- /code/espurna/emon.ino: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | EMON MODULE 4 | 5 | Copyright (C) 2016-2017 by Xose Pérez 6 | 7 | */ 8 | 9 | #if ENABLE_EMON 10 | 11 | #include 12 | #include 13 | #if EMON_PROVIDER == EMON_ADC121_PROVIDER 14 | #include "brzo_i2c.h" 15 | #endif 16 | 17 | // ADC121 Registers 18 | #define ADC121_REG_RESULT 0x00 19 | #define ADC121_REG_ALERT 0x01 20 | #define ADC121_REG_CONFIG 0x02 21 | #define ADC121_REG_LIMITL 0x03 22 | #define ADC121_REG_LIMITH 0x04 23 | #define ADC121_REG_HYST 0x05 24 | #define ADC121_REG_CONVL 0x06 25 | #define ADC121_REG_CONVH 0x07 26 | 27 | EmonLiteESP emon; 28 | double _current = 0; 29 | unsigned int _power = 0; 30 | 31 | // ----------------------------------------------------------------------------- 32 | // Provider 33 | // ----------------------------------------------------------------------------- 34 | 35 | unsigned int currentCallback() { 36 | 37 | #if EMON_PROVIDER == EMON_ANALOG_PROVIDER 38 | return analogRead(EMON_CURRENT_PIN); 39 | #endif 40 | 41 | #if EMON_PROVIDER == EMON_ADC121_PROVIDER 42 | uint8_t buffer[2]; 43 | brzo_i2c_start_transaction(EMON_ADC121_ADDRESS, I2C_SCL_FREQUENCY); 44 | buffer[0] = ADC121_REG_RESULT; 45 | brzo_i2c_write(buffer, 1, false); 46 | brzo_i2c_read(buffer, 2, false); 47 | brzo_i2c_end_transaction(); 48 | unsigned int value; 49 | value = (buffer[0] & 0x0F) << 8; 50 | value |= buffer[1]; 51 | return value; 52 | #endif 53 | 54 | } 55 | 56 | // ----------------------------------------------------------------------------- 57 | // EMON 58 | // ----------------------------------------------------------------------------- 59 | 60 | void setCurrentRatio(float value) { 61 | emon.setCurrentRatio(value); 62 | } 63 | 64 | unsigned int getPower() { 65 | return _power; 66 | } 67 | 68 | double getCurrent() { 69 | return _current; 70 | } 71 | 72 | void powerMonitorSetup() { 73 | 74 | // backwards compatibility 75 | String tmp; 76 | tmp = getSetting("pwMainsVoltage", EMON_MAINS_VOLTAGE); 77 | setSetting("emonMains", tmp); 78 | delSetting("pwMainsVoltage"); 79 | tmp = getSetting("pwCurrentRatio", EMON_CURRENT_RATIO); 80 | setSetting("emonRatio", tmp); 81 | delSetting("pwCurrentRatio"); 82 | 83 | emon.initCurrent( 84 | currentCallback, 85 | EMON_ADC_BITS, 86 | EMON_REFERENCE_VOLTAGE, 87 | getSetting("emonRatio", EMON_CURRENT_RATIO).toFloat() 88 | ); 89 | emon.setPrecision(EMON_CURRENT_PRECISION); 90 | 91 | #if EMON_PROVIDER == EMON_ADC121_PROVIDER 92 | uint8_t buffer[2]; 93 | buffer[0] = ADC121_REG_CONFIG; 94 | buffer[1] = 0x00; 95 | brzo_i2c_start_transaction(EMON_ADC121_ADDRESS, I2C_SCL_FREQUENCY); 96 | brzo_i2c_write(buffer, 2, false); 97 | brzo_i2c_end_transaction(); 98 | #endif 99 | 100 | apiRegister("/api/power", "power", [](char * buffer, size_t len) { 101 | snprintf(buffer, len, "%d", _power); 102 | }); 103 | 104 | } 105 | 106 | void powerMonitorLoop() { 107 | 108 | static unsigned long next_measurement = millis(); 109 | static bool warmup = true; 110 | static byte measurements = 0; 111 | static double max = 0; 112 | static double min = 0; 113 | static double sum = 0; 114 | 115 | if (warmup) { 116 | warmup = false; 117 | emon.warmup(); 118 | } 119 | 120 | if (millis() > next_measurement) { 121 | 122 | // Safety check: do not read current if relay is OFF 123 | // You could be monitoring another line with the current clamp... 124 | //if (!relayStatus(0)) { 125 | // _current = 0; 126 | //} else { 127 | _current = emon.getCurrent(EMON_SAMPLES); 128 | _current -= EMON_CURRENT_OFFSET; 129 | if (_current < 0) _current = 0; 130 | //} 131 | 132 | if (measurements == 0) { 133 | max = min = _current; 134 | } else { 135 | if (_current > max) max = _current; 136 | if (_current < min) min = _current; 137 | } 138 | sum += _current; 139 | ++measurements; 140 | 141 | float mainsVoltage = getSetting("emonMains", EMON_MAINS_VOLTAGE).toFloat(); 142 | 143 | char current[6]; 144 | dtostrf(_current, 5, 2, current); 145 | DEBUG_MSG_P(PSTR("[ENERGY] Current: %sA\n"), current); 146 | DEBUG_MSG_P(PSTR("[ENERGY] Power: %dW\n"), int(_current * mainsVoltage)); 147 | 148 | // Update websocket clients 149 | char text[64]; 150 | sprintf_P(text, PSTR("{\"emonVisible\": 1, \"powApparentPower\": %d}"), int(_current * mainsVoltage)); 151 | wsSend(text); 152 | 153 | // Send MQTT messages averaged every EMON_MEASUREMENTS 154 | if (measurements == EMON_MEASUREMENTS) { 155 | 156 | // Calculate average current (removing max and min values) and create C-string 157 | double average = (sum - max - min) / (measurements - 2); 158 | dtostrf(average, 5, 2, current); 159 | char *c = current; 160 | while ((unsigned char) *c == ' ') ++c; 161 | 162 | // Calculate average apparent power from current and create C-string 163 | _power = (int) (average * mainsVoltage); 164 | char power[6]; 165 | snprintf(power, 6, "%d", _power); 166 | 167 | // Calculate energy increment (ppower times time) and create C-string 168 | double energy_inc = (double) _power * EMON_INTERVAL * EMON_MEASUREMENTS / 1000.0 / 3600.0; 169 | char energy_buf[11]; 170 | dtostrf(energy_inc, 10, 3, energy_buf); 171 | char *e = energy_buf; 172 | while ((unsigned char) *e == ' ') ++e; 173 | 174 | // Report values to MQTT broker 175 | mqttSend(getSetting("emonPowerTopic", EMON_APOWER_TOPIC).c_str(), power); 176 | mqttSend(getSetting("emonCurrTopic", EMON_CURRENT_TOPIC).c_str(), c); 177 | mqttSend(getSetting("emonEnergyTopic", EMON_ENERGY_TOPIC).c_str(), e); 178 | 179 | // Report values to Domoticz 180 | #if ENABLE_DOMOTICZ 181 | { 182 | char buffer[20]; 183 | snprintf(buffer, 20, "%s;%s", power, e); 184 | domoticzSend("dczPowIdx", 0, buffer); 185 | snprintf(buffer, 20, "%s", e); 186 | domoticzSend("dczEnergyIdx", 0, buffer); 187 | snprintf(buffer, 20, "%s", c); 188 | domoticzSend("dczCurrentIdx", 0, buffer); 189 | } 190 | #endif 191 | 192 | // Reset counters 193 | sum = measurements = 0; 194 | 195 | } 196 | 197 | next_measurement += EMON_INTERVAL; 198 | 199 | } 200 | 201 | } 202 | 203 | #endif 204 | -------------------------------------------------------------------------------- /code/espurna/espurna.ino: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | ESPurna 4 | 5 | Copyright (C) 2016-2017 by Xose Pérez 6 | 7 | This program is free software: you can redistribute it and/or modify 8 | it under the terms of the GNU General Public License as published by 9 | the Free Software Foundation, either version 3 of the License, or 10 | (at your option) any later version. 11 | 12 | This program is distributed in the hope that it will be useful, 13 | but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | GNU General Public License for more details. 16 | 17 | You should have received a copy of the GNU General Public License 18 | along with this program. If not, see . 19 | 20 | */ 21 | 22 | #include "config/all.h" 23 | #include 24 | 25 | // ----------------------------------------------------------------------------- 26 | // METHODS 27 | // ----------------------------------------------------------------------------- 28 | 29 | String getIdentifier() { 30 | char identifier[20]; 31 | sprintf(identifier, "%s_%06X", DEVICE, ESP.getChipId()); 32 | return String(identifier); 33 | } 34 | 35 | void heartbeat() { 36 | 37 | static unsigned long last_uptime = 0; 38 | static unsigned char uptime_overflows = 0; 39 | 40 | if (millis() < last_uptime) ++uptime_overflows; 41 | last_uptime = millis(); 42 | unsigned long uptime_seconds = uptime_overflows * (UPTIME_OVERFLOW / 1000) + (last_uptime / 1000); 43 | unsigned int free_heap = ESP.getFreeHeap(); 44 | 45 | DEBUG_MSG_P(PSTR("[MAIN] Time: %s\n"), (char *) NTP.getTimeDateString().c_str()); 46 | if (!mqttConnected()) { 47 | DEBUG_MSG_P(PSTR("[MAIN] Uptime: %ld seconds\n"), uptime_seconds); 48 | DEBUG_MSG_P(PSTR("[MAIN] Free heap: %d bytes\n"), free_heap); 49 | #if ENABLE_ADC_VCC 50 | DEBUG_MSG_P(PSTR("[MAIN] Power: %d mV\n"), ESP.getVcc()); 51 | #endif 52 | } 53 | 54 | #if (MQTT_REPORT_INTERVAL) 55 | mqttSend(MQTT_TOPIC_INTERVAL, HEARTBEAT_INTERVAL / 1000); 56 | #endif 57 | #if (MQTT_REPORT_APP) 58 | mqttSend(MQTT_TOPIC_APP, APP_NAME); 59 | #endif 60 | #if (MQTT_REPORT_VERSION) 61 | mqttSend(MQTT_TOPIC_VERSION, APP_VERSION); 62 | #endif 63 | #if (MQTT_REPORT_HOSTNAME) 64 | mqttSend(MQTT_TOPIC_HOSTNAME, getSetting("hostname").c_str()); 65 | #endif 66 | #if (MQTT_REPORT_IP) 67 | mqttSend(MQTT_TOPIC_IP, getIP().c_str()); 68 | #endif 69 | #if (MQTT_REPORT_MAC) 70 | mqttSend(MQTT_TOPIC_MAC, WiFi.macAddress().c_str()); 71 | #endif 72 | #if (MQTT_REPORT_UPTIME) 73 | mqttSend(MQTT_TOPIC_UPTIME, String(uptime_seconds).c_str()); 74 | #endif 75 | #if (MQTT_REPORT_FREEHEAP) 76 | mqttSend(MQTT_TOPIC_FREEHEAP, String(free_heap).c_str()); 77 | #endif 78 | #if (MQTT_REPORT_RELAY) 79 | relayMQTT(); 80 | #endif 81 | #if (MQTT_REPORT_VCC) 82 | #if ENABLE_ADC_VCC 83 | mqttSend(MQTT_TOPIC_VCC, String(ESP.getVcc()).c_str()); 84 | #endif 85 | #endif 86 | #if (MQTT_REPORT_STATUS) 87 | mqttSend(MQTT_TOPIC_STATUS, MQTT_STATUS_ONLINE); 88 | #endif 89 | 90 | } 91 | 92 | void hardwareSetup() { 93 | EEPROM.begin(4096); 94 | Serial.begin(SERIAL_BAUDRATE); 95 | #if not EMBEDDED_WEB 96 | SPIFFS.begin(); 97 | #endif 98 | } 99 | 100 | void hardwareLoop() { 101 | 102 | // Heartbeat 103 | static unsigned long last_uptime = 0; 104 | if ((millis() - last_uptime > HEARTBEAT_INTERVAL) || (last_uptime == 0)) { 105 | last_uptime = millis(); 106 | heartbeat(); 107 | } 108 | 109 | } 110 | 111 | // ----------------------------------------------------------------------------- 112 | // BOOTING 113 | // ----------------------------------------------------------------------------- 114 | 115 | void welcome() { 116 | 117 | DEBUG_MSG_P(PSTR("%s %s\n"), (char *) APP_NAME, (char *) APP_VERSION); 118 | DEBUG_MSG_P(PSTR("%s\n%s\n\n"), (char *) APP_AUTHOR, (char *) APP_WEBSITE); 119 | DEBUG_MSG_P(PSTR("ChipID: %06X\n"), ESP.getChipId()); 120 | DEBUG_MSG_P(PSTR("CPU frequency: %d MHz\n"), ESP.getCpuFreqMHz()); 121 | DEBUG_MSG_P(PSTR("Last reset reason: %s\n"), (char *) ESP.getResetReason().c_str()); 122 | DEBUG_MSG_P(PSTR("Memory size: %d bytes\n"), ESP.getFlashChipSize()); 123 | DEBUG_MSG_P(PSTR("Free heap: %d bytes\n"), ESP.getFreeHeap()); 124 | DEBUG_MSG_P(PSTR("Firmware size: %d bytes\n"), ESP.getSketchSize()); 125 | DEBUG_MSG_P(PSTR("Free firmware space: %d bytes\n"), ESP.getFreeSketchSpace()); 126 | 127 | #if not EMBEDDED_WEB 128 | FSInfo fs_info; 129 | if (SPIFFS.info(fs_info)) { 130 | DEBUG_MSG_P(PSTR("File system total size: %d bytes\n"), fs_info.totalBytes); 131 | DEBUG_MSG_P(PSTR(" used size : %d bytes\n"), fs_info.usedBytes); 132 | DEBUG_MSG_P(PSTR(" block size: %d bytes\n"), fs_info.blockSize); 133 | DEBUG_MSG_P(PSTR(" page size : %d bytes\n"), fs_info.pageSize); 134 | DEBUG_MSG_P(PSTR(" max files : %d\n"), fs_info.maxOpenFiles); 135 | DEBUG_MSG_P(PSTR(" max length: %d\n"), fs_info.maxPathLength); 136 | } 137 | #endif 138 | 139 | DEBUG_MSG_P(PSTR("\n\n")); 140 | 141 | } 142 | 143 | void setup() { 144 | 145 | hardwareSetup(); 146 | welcome(); 147 | settingsSetup(); 148 | if (getSetting("hostname").length() == 0) { 149 | setSetting("hostname", getIdentifier()); 150 | saveSettings(); 151 | } 152 | webSetup(); 153 | #if LIGHT_PROVIDER != LIGHT_PROVIDER_NONE 154 | lightSetup(); 155 | #endif 156 | relaySetup(); 157 | buttonSetup(); 158 | ledSetup(); 159 | wifiSetup(); 160 | // otaSetup(); 161 | mqttSetup(); 162 | ntpSetup(); 163 | 164 | #if ENABLE_I2C 165 | i2cSetup(); 166 | #endif 167 | #if ENABLE_FAUXMO 168 | fauxmoSetup(); 169 | #endif 170 | #if ENABLE_NOFUSS 171 | nofussSetup(); 172 | #endif 173 | #if ENABLE_IOTAPPSTORY 174 | IOTappStory(); 175 | #endif 176 | #if ENABLE_POW 177 | powSetup(); 178 | #endif 179 | #if ENABLE_DS18B20 180 | dsSetup(); 181 | #endif 182 | #if ENABLE_DHT 183 | dhtSetup(); 184 | #endif 185 | #if ENABLE_RF 186 | rfSetup(); 187 | #endif 188 | #if ENABLE_EMON 189 | powerMonitorSetup(); 190 | #endif 191 | 192 | } 193 | 194 | void loop() { 195 | 196 | hardwareLoop(); 197 | buttonLoop(); 198 | ledLoop(); 199 | wifiLoop(); 200 | otaLoop(); 201 | mqttLoop(); 202 | ntpLoop(); 203 | 204 | #if ENABLE_FAUXMO 205 | fauxmoLoop(); 206 | #endif 207 | #ifndef SONOFF_DUAL 208 | settingsLoop(); 209 | #endif 210 | #if ENABLE_NOFUSS 211 | nofussLoop(); 212 | #endif 213 | #if ENABLE_POW 214 | powLoop(); 215 | #endif 216 | #if ENABLE_DS18B20 217 | dsLoop(); 218 | #endif 219 | #if ENABLE_DHT 220 | dhtLoop(); 221 | #endif 222 | #if ENABLE_RF 223 | rfLoop(); 224 | #endif 225 | #if ENABLE_EMON 226 | powerMonitorLoop(); 227 | #endif 228 | 229 | } 230 | -------------------------------------------------------------------------------- /code/espurna/espurna.ino.generic.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SensorsIot/Espurna-Framework/ed8c754fd86a704670ef38187313f0dab3e1ecba/code/espurna/espurna.ino.generic.bin -------------------------------------------------------------------------------- /code/espurna/espurna.ino.nodemcu.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SensorsIot/Espurna-Framework/ed8c754fd86a704670ef38187313f0dab3e1ecba/code/espurna/espurna.ino.nodemcu.bin -------------------------------------------------------------------------------- /code/espurna/fauxmo.ino: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | FAUXMO MODULE 4 | 5 | Copyright (C) 2016-2017 by Xose Pérez 6 | 7 | */ 8 | 9 | #if ENABLE_FAUXMO 10 | 11 | #include 12 | 13 | fauxmoESP fauxmo; 14 | 15 | // ----------------------------------------------------------------------------- 16 | // FAUXMO 17 | // ----------------------------------------------------------------------------- 18 | 19 | 20 | 21 | #ifdef MQTT_TOPIC_ALEXA 22 | void alexaMQTT(uint8_t event) { 23 | char payload[2]; 24 | sprintf(payload, "%d", event); 25 | mqttSend(MQTT_TOPIC_ALEXA, 1, payload); 26 | } 27 | #endif 28 | 29 | void fauxmoConfigure() { 30 | fauxmo.enable(getSetting("fauxmoEnabled", FAUXMO_ENABLED).toInt() == 1); 31 | } 32 | 33 | void fauxmoSetup() { 34 | fauxmoConfigure(); 35 | unsigned int relays = relayCount(); 36 | String hostname = getSetting("hostname", HOSTNAME); 37 | if (relays == 1) { 38 | fauxmo.addDevice(hostname.c_str()); 39 | } else { 40 | for (unsigned int i=0; i 6 | 7 | */ 8 | 9 | #if ENABLE_I2C 10 | 11 | #include "brzo_i2c.h" 12 | 13 | void i2cScan() { 14 | 15 | uint8_t address; 16 | uint8_t response; 17 | uint8_t buffer[1]; 18 | int nDevices = 0; 19 | 20 | for (address = 1; address < 128; address++) { 21 | 22 | brzo_i2c_start_transaction(address, I2C_SCL_FREQUENCY); 23 | brzo_i2c_ACK_polling(1000); 24 | response = brzo_i2c_end_transaction(); 25 | 26 | if (response == 0) { 27 | DEBUG_MSG_P(PSTR("[I2C] Device found at address 0x%02X\n"), address); 28 | nDevices++; 29 | } else if (response != 32) { 30 | //DEBUG_MSG_P(PSTR("[I2C] Unknown error at address 0x%02X\n"), address); 31 | } 32 | } 33 | 34 | if (nDevices == 0) DEBUG_MSG_P(PSTR("[I2C] No devices found")); 35 | 36 | } 37 | 38 | void i2cSetup() { 39 | brzo_i2c_setup(I2C_SDA_PIN, I2C_SCL_PIN, I2C_CLOCK_STRETCH_TIME); 40 | i2cScan(); 41 | } 42 | 43 | #endif 44 | -------------------------------------------------------------------------------- /code/espurna/led.ino: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | LED MODULE 4 | 5 | Copyright (C) 2016-2017 by Xose Pérez 6 | 7 | */ 8 | 9 | // ----------------------------------------------------------------------------- 10 | // LED 11 | // ----------------------------------------------------------------------------- 12 | 13 | typedef struct { 14 | unsigned char pin; 15 | bool reverse; 16 | } led_t; 17 | 18 | std::vector _leds; 19 | bool ledAuto; 20 | 21 | bool ledStatus(unsigned char id) { 22 | if (id >= _leds.size()) return false; 23 | bool status = digitalRead(_leds[id].pin); 24 | return _leds[id].reverse ? !status : status; 25 | } 26 | 27 | bool ledStatus(unsigned char id, bool status) { 28 | if (id >= _leds.size()) return false; 29 | bool s = _leds[id].reverse ? !status : status; 30 | digitalWrite(_leds[id].pin, _leds[id].reverse ? !status : status); 31 | return status; 32 | } 33 | 34 | bool ledToggle(unsigned char id) { 35 | if (id >= _leds.size()) return false; 36 | return ledStatus(id, !ledStatus(id)); 37 | } 38 | 39 | void ledBlink(unsigned char id, unsigned long delayOff, unsigned long delayOn) { 40 | if (id >= _leds.size()) return; 41 | static unsigned long next = millis(); 42 | if (next < millis()) { 43 | next += (ledToggle(id) ? delayOn : delayOff); 44 | } 45 | } 46 | 47 | #if WIFI_LED 48 | void showStatus() { 49 | if (wifiConnected()) { 50 | if (WiFi.getMode() == WIFI_AP) { 51 | ledBlink(WIFI_LED - 1, 2500, 2500); 52 | } else { 53 | ledBlink(WIFI_LED - 1, 4900, 100); 54 | } 55 | } else { 56 | ledBlink(WIFI_LED - 1, 500, 500); 57 | } 58 | } 59 | #endif 60 | 61 | void ledMQTTCallback(unsigned int type, const char * topic, const char * payload) { 62 | 63 | static bool isFirstMessage = true; 64 | 65 | if (type == MQTT_CONNECT_EVENT) { 66 | char buffer[strlen(MQTT_TOPIC_LED) + 3]; 67 | sprintf(buffer, "%s/+", MQTT_TOPIC_LED); 68 | mqttSubscribe(buffer); 69 | } 70 | 71 | if (type == MQTT_MESSAGE_EVENT) { 72 | 73 | // Match topic 74 | String t = mqttSubtopic((char *) topic); 75 | if (!t.startsWith(MQTT_TOPIC_LED)) return; 76 | 77 | // Get led ID 78 | unsigned int ledID = t.substring(strlen(MQTT_TOPIC_LED)+1).toInt(); 79 | if (ledID >= ledCount()) { 80 | DEBUG_MSG_P(PSTR("[LED] Wrong ledID (%d)\n"), ledID); 81 | return; 82 | } 83 | 84 | // get value 85 | unsigned int value = (char)payload[0] - '0'; 86 | bool bitAuto = (value & 0x02) > 0; 87 | bool bitState = (value & 0x01) > 0; 88 | 89 | // Check ledAuto 90 | if (ledID == 0) { 91 | ledAuto = bitAuto ? bitState : false; 92 | setSetting("ledAuto", String() + (ledAuto ? "1" : "0")); 93 | if (bitAuto) return; 94 | } 95 | 96 | // Action to perform 97 | ledStatus(ledID, bitState); 98 | 99 | } 100 | 101 | } 102 | 103 | unsigned char ledCount() { 104 | return _leds.size(); 105 | } 106 | 107 | void ledConfigure() { 108 | ledAuto = getSetting("ledAuto", String() + LED_AUTO).toInt() == 1; 109 | } 110 | 111 | void ledSetup() { 112 | 113 | #ifdef LED1_PIN 114 | _leds.push_back((led_t) { LED1_PIN, LED1_PIN_INVERSE }); 115 | #endif 116 | #ifdef LED2_PIN 117 | _leds.push_back((led_t) { LED2_PIN, LED2_PIN_INVERSE }); 118 | #endif 119 | #ifdef LED3_PIN 120 | _leds.push_back((led_t) { LED3_PIN, LED3_PIN_INVERSE }); 121 | #endif 122 | #ifdef LED4_PIN 123 | _leds.push_back((led_t) { LED4_PIN, LED4_PIN_INVERSE }); 124 | #endif 125 | 126 | for (unsigned int i=0; i < _leds.size(); i++) { 127 | pinMode(_leds[i].pin, OUTPUT); 128 | ledStatus(i, false); 129 | } 130 | 131 | ledConfigure(); 132 | 133 | mqttRegister(ledMQTTCallback); 134 | 135 | DEBUG_MSG_P(PSTR("[LED] Number of leds: %d\n"), _leds.size()); 136 | DEBUG_MSG_P(PSTR("[LED] Led auto indicator is %s\n"), ledAuto ? "ON" : "OFF" ); 137 | 138 | } 139 | 140 | void ledLoop() { 141 | #if WIFI_LED 142 | if (ledAuto) showStatus(); 143 | #endif 144 | } 145 | -------------------------------------------------------------------------------- /code/espurna/light.ino: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | LIGHT MODULE 4 | 5 | Copyright (C) 2016-2017 by Xose Pérez 6 | 7 | */ 8 | 9 | #if LIGHT_PROVIDER != LIGHT_PROVIDER_NONE 10 | 11 | #include 12 | Ticker colorTicker; 13 | bool _lightState = false; 14 | unsigned int _lightColor[3] = {0}; 15 | 16 | #if LIGHT_PROVIDER == LIGHT_PROVIDER_MY9192 17 | #include 18 | my9291 * _my9291; 19 | #endif 20 | 21 | // ----------------------------------------------------------------------------- 22 | // UTILS 23 | // ----------------------------------------------------------------------------- 24 | 25 | void color_rgb2array(const char * rgb, unsigned int * array) { 26 | 27 | char * p = (char *) rgb; 28 | if (p[0] == '#') ++p; 29 | 30 | unsigned long value = strtol(p, NULL, 16); 31 | array[0] = (value >> 16) & 0xFF; 32 | array[1] = (value >> 8) & 0xFF; 33 | array[2] = (value) & 0xFF; 34 | 35 | } 36 | 37 | void color_array2rgb(unsigned int * array, char * rgb) { 38 | unsigned long value = array[0]; 39 | value = (value << 8) + array[1]; 40 | value = (value << 8) + array[2]; 41 | sprintf(rgb, "#%06X", value); 42 | } 43 | 44 | // ----------------------------------------------------------------------------- 45 | // PROVIDER 46 | // ----------------------------------------------------------------------------- 47 | 48 | void lightColorProvider(unsigned int red, unsigned int green, unsigned int blue) { 49 | 50 | #if (LIGHT_PROVIDER == LIGHT_PROVIDER_MY9192) || (LIGHT_PROVIDER == LIGHT_PROVIDER_RGBW) 51 | unsigned int white = 0; 52 | 53 | // If all set to the same value use white instead 54 | if ((red == green) && (green == blue)) { 55 | white = red; 56 | red = green = blue = 0; 57 | } 58 | #endif 59 | 60 | #if LIGHT_PROVIDER == LIGHT_PROVIDER_MY9192 61 | _my9291->setColor((my9291_color_t) { red, green, blue, white }); 62 | #endif 63 | 64 | #if (LIGHT_PROVIDER == LIGHT_PROVIDER_RGB) || (LIGHT_PROVIDER == LIGHT_PROVIDER_RGBW) 65 | if (RGBW_INVERSE_LOGIC) { 66 | analogWrite(RGBW_RED_PIN, red); 67 | analogWrite(RGBW_GREEN_PIN, green); 68 | analogWrite(RGBW_BLUE_PIN, blue); 69 | #if (LIGHT_PROVIDER == LIGHT_PROVIDER_RGBW) 70 | analogWrite(RGBW_WHITE_PIN, white); 71 | #endif 72 | } else { 73 | analogWrite(RGBW_RED_PIN, 255 - red); 74 | analogWrite(RGBW_GREEN_PIN, 255 - green); 75 | analogWrite(RGBW_BLUE_PIN, 255 - blue); 76 | #if (LIGHT_PROVIDER == LIGHT_PROVIDER_RGBW) 77 | analogWrite(RGBW_WHITE_PIN, 255 - white); 78 | #endif 79 | } 80 | #endif 81 | 82 | } 83 | 84 | // ----------------------------------------------------------------------------- 85 | // LIGHT MANAGEMENT 86 | // ----------------------------------------------------------------------------- 87 | 88 | void lightColor(const char * rgb, bool save, bool forward) { 89 | 90 | color_rgb2array(rgb, _lightColor); 91 | lightColorProvider(_lightColor[0], _lightColor[1], _lightColor[2]); 92 | 93 | // Delay saving to EEPROM 5 seconds to avoid wearing it out unnecessarily 94 | if (save) colorTicker.once(LIGHT_SAVE_DELAY, lightColorSave); 95 | 96 | // Report color to MQTT broker 97 | if (forward) mqttSend(MQTT_TOPIC_COLOR, rgb); 98 | 99 | // Report color to WS clients 100 | char message[20]; 101 | sprintf(message, "{\"color\": \"%s\"}", rgb); 102 | wsSend(message); 103 | 104 | } 105 | 106 | String lightColor() { 107 | char rgb[8]; 108 | color_array2rgb(_lightColor, rgb); 109 | return String(rgb); 110 | } 111 | 112 | void lightState(bool state) { 113 | if (state) { 114 | lightColorProvider(_lightColor[0], _lightColor[1], _lightColor[2]); 115 | } else { 116 | lightColorProvider(0, 0, 0); 117 | } 118 | _lightState = state; 119 | } 120 | 121 | bool lightState() { 122 | return _lightState; 123 | } 124 | 125 | // ----------------------------------------------------------------------------- 126 | // PERSISTANCE 127 | // ----------------------------------------------------------------------------- 128 | 129 | void lightColorSave() { 130 | setSetting("color", lightColor()); 131 | saveSettings(); 132 | } 133 | 134 | void lightColorRetrieve() { 135 | lightColor(getSetting("color", LIGHT_DEFAULT_COLOR).c_str(), false, true); 136 | } 137 | 138 | // ----------------------------------------------------------------------------- 139 | // MQTT 140 | // ----------------------------------------------------------------------------- 141 | 142 | void lightMQTTCallback(unsigned int type, const char * topic, const char * payload) { 143 | 144 | 145 | if (type == MQTT_CONNECT_EVENT) { 146 | mqttSubscribe(MQTT_TOPIC_COLOR); 147 | } 148 | 149 | if (type == MQTT_MESSAGE_EVENT) { 150 | 151 | // Match topic 152 | String t = mqttSubtopic((char *) topic); 153 | if (!t.equals(MQTT_TOPIC_COLOR)) return; 154 | 155 | lightColor(payload, true, mqttForward()); 156 | 157 | } 158 | 159 | } 160 | 161 | // ----------------------------------------------------------------------------- 162 | // SETUP 163 | // ----------------------------------------------------------------------------- 164 | 165 | void lightSetup() { 166 | 167 | #if LIGHT_PROVIDER == LIGHT_PROVIDER_MY9192 168 | _my9291 = new my9291(MY9291_DI_PIN, MY9291_DCKI_PIN, MY9291_COMMAND); 169 | #endif 170 | 171 | #if (LIGHT_PROVIDER == LIGHT_PROVIDER_RGB) || (LIGHT_PROVIDER == LIGHT_PROVIDER_RGBW) 172 | pinMode(RGBW_RED_PIN, OUTPUT); 173 | pinMode(RGBW_GREEN_PIN, OUTPUT); 174 | pinMode(RGBW_BLUE_PIN, OUTPUT); 175 | #if LIGHT_PROVIDER == LIGHT_PROVIDER_RGBW 176 | pinMode(RGBW_WHITE_PIN, OUTPUT); 177 | #endif 178 | lightColorProvider(0, 0, 0); 179 | #endif 180 | 181 | lightColorRetrieve(); 182 | 183 | mqttRegister(lightMQTTCallback); 184 | 185 | } 186 | 187 | #endif // LIGHT_PROVIDER != LIGHT_PROVIDER_NONE 188 | -------------------------------------------------------------------------------- /code/espurna/mqtt.ino: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | MQTT MODULE 4 | 5 | Copyright (C) 2016-2017 by Xose Pérez 6 | 7 | */ 8 | 9 | #include 10 | #include 11 | 12 | const char *mqtt_user = 0; 13 | const char *mqtt_pass = 0; 14 | 15 | #if MQTT_USE_ASYNC 16 | #include 17 | AsyncMqttClient mqtt; 18 | #else 19 | #include 20 | WiFiClient mqttWiFiClient; 21 | PubSubClient mqtt(mqttWiFiClient); 22 | bool _mqttConnected = false; 23 | #endif 24 | 25 | String mqttTopic; 26 | bool _mqttForward; 27 | char *_mqttUser = 0; 28 | char *_mqttPass = 0; 29 | std::vector _mqtt_callbacks; 30 | #if MQTT_SKIP_RETAINED 31 | unsigned long mqttConnectedAt = 0; 32 | #endif 33 | 34 | // ----------------------------------------------------------------------------- 35 | // Public API 36 | // ----------------------------------------------------------------------------- 37 | 38 | bool mqttConnected() { 39 | return mqtt.connected(); 40 | } 41 | 42 | void mqttDisconnect() { 43 | mqtt.disconnect(); 44 | } 45 | 46 | void buildTopics() { 47 | // Replace identifier 48 | mqttTopic = getSetting("mqttTopic", MQTT_TOPIC); 49 | mqttTopic.replace("{identifier}", getSetting("hostname")); 50 | } 51 | 52 | bool mqttForward() { 53 | return _mqttForward; 54 | } 55 | 56 | String mqttSubtopic(char * topic) { 57 | 58 | String response; 59 | 60 | String t = String(topic); 61 | String mqttSetter = getSetting("mqttSetter", MQTT_USE_SETTER); 62 | 63 | if (t.startsWith(mqttTopic) && t.endsWith(mqttSetter)) { 64 | response = t.substring(mqttTopic.length(), t.length() - mqttSetter.length()); 65 | } 66 | 67 | return response; 68 | 69 | } 70 | 71 | void mqttSendRaw(const char * topic, const char * message) { 72 | if (mqtt.connected()) { 73 | DEBUG_MSG_P(PSTR("[MQTT] Sending %s => %s\n"), topic, message); 74 | #if MQTT_USE_ASYNC 75 | mqtt.publish(topic, MQTT_QOS, MQTT_RETAIN, message); 76 | #else 77 | mqtt.publish(topic, message, MQTT_RETAIN); 78 | #endif 79 | } 80 | } 81 | 82 | void mqttSend(const char * topic, const char * message) { 83 | String mqttGetter = getSetting("mqttGetter", MQTT_USE_GETTER); 84 | String path = mqttTopic + String(topic) + mqttGetter; 85 | mqttSendRaw(path.c_str(), message); 86 | } 87 | 88 | void mqttSend(const char * topic, unsigned int index, const char * message) { 89 | String mqttGetter = getSetting("mqttGetter", MQTT_USE_GETTER); 90 | String path = mqttTopic + String(topic) + String ("/") + String(index) + mqttGetter; 91 | mqttSendRaw(path.c_str(), message); 92 | } 93 | 94 | void mqttSubscribeRaw(const char * topic) { 95 | if (mqtt.connected() && (strlen(topic) > 0)) { 96 | DEBUG_MSG_P(PSTR("[MQTT] Subscribing to %s\n"), topic); 97 | mqtt.subscribe(topic, MQTT_QOS); 98 | } 99 | } 100 | 101 | void mqttSubscribe(const char * topic) { 102 | String mqttSetter = getSetting("mqttSetter", MQTT_USE_SETTER); 103 | String path = mqttTopic + String(topic) + mqttSetter; 104 | mqttSubscribeRaw(path.c_str()); 105 | } 106 | 107 | // ----------------------------------------------------------------------------- 108 | // Callbacks 109 | // ----------------------------------------------------------------------------- 110 | 111 | void mqttRegister(void (*callback)(unsigned int, const char *, const char *)) { 112 | _mqtt_callbacks.push_back(callback); 113 | } 114 | 115 | void _mqttOnConnect() { 116 | 117 | DEBUG_MSG_P(PSTR("[MQTT] Connected!\n")); 118 | 119 | #if MQTT_SKIP_RETAINED 120 | mqttConnectedAt = millis(); 121 | #endif 122 | 123 | // Build MQTT topics 124 | buildTopics(); 125 | 126 | // Send first Heartbeat 127 | heartbeat(); 128 | 129 | // Subscribe to system topics 130 | mqttSubscribe(MQTT_TOPIC_ACTION); 131 | 132 | // Send connect event to subscribers 133 | for (unsigned char i = 0; i < _mqtt_callbacks.size(); i++) { 134 | (*_mqtt_callbacks[i])(MQTT_CONNECT_EVENT, NULL, NULL); 135 | } 136 | 137 | } 138 | 139 | void _mqttOnDisconnect() { 140 | 141 | DEBUG_MSG_P(PSTR("[MQTT] Disconnected!\n")); 142 | 143 | // Send disconnect event to subscribers 144 | for (unsigned char i = 0; i < _mqtt_callbacks.size(); i++) { 145 | (*_mqtt_callbacks[i])(MQTT_DISCONNECT_EVENT, NULL, NULL); 146 | } 147 | 148 | } 149 | 150 | void _mqttOnMessage(char* topic, char* payload, unsigned int len) { 151 | 152 | char message[len + 1]; 153 | strlcpy(message, (char *) payload, len + 1); 154 | 155 | DEBUG_MSG_P(PSTR("[MQTT] Received %s => %s"), topic, message); 156 | #if MQTT_SKIP_RETAINED 157 | if (millis() - mqttConnectedAt < MQTT_SKIP_TIME) { 158 | DEBUG_MSG_P(PSTR(" - SKIPPED\n")); 159 | return; 160 | } 161 | #endif 162 | DEBUG_MSG_P(PSTR("\n")); 163 | 164 | // Check system topics 165 | String t = mqttSubtopic((char *) topic); 166 | if (t.equals(MQTT_TOPIC_ACTION)) { 167 | if (strcmp(message, MQTT_ACTION_RESET) == 0) { 168 | ESP.restart(); 169 | } 170 | } 171 | 172 | // Send message event to subscribers 173 | for (unsigned char i = 0; i < _mqtt_callbacks.size(); i++) { 174 | (*_mqtt_callbacks[i])(MQTT_MESSAGE_EVENT, topic, message); 175 | } 176 | 177 | } 178 | 179 | void mqttConnect() { 180 | if (!mqtt.connected()) { 181 | 182 | if (getSetting("mqttServer", MQTT_SERVER).length() == 0) return; 183 | // Last option: reconnect to wifi after MQTT_MAX_TRIES attemps in a row 184 | #if MQTT_MAX_TRIES > 0 185 | static unsigned int tries = 0; 186 | static unsigned long last_try = millis(); 187 | if (millis() - last_try < MQTT_TRY_INTERVAL) { 188 | if (++tries > MQTT_MAX_TRIES) { 189 | DEBUG_MSG_P(PSTR("[MQTT] MQTT_MAX_TRIES met, disconnecting from WiFi\n")); 190 | wifiDisconnect(); 191 | tries = 0; 192 | return; 193 | } 194 | } else { 195 | tries = 0; 196 | } 197 | last_try = millis(); 198 | #endif 199 | 200 | mqtt.disconnect(); 201 | if (_mqttUser) free(_mqttUser); 202 | if (_mqttPass) free(_mqttPass); 203 | 204 | char * host = strdup(getSetting("mqttServer", MQTT_SERVER).c_str()); 205 | unsigned int port = getSetting("mqttPort", MQTT_PORT).toInt(); 206 | _mqttUser = strdup(getSetting("mqttUser").c_str()); 207 | _mqttPass = strdup(getSetting("mqttPassword").c_str()); 208 | 209 | DEBUG_MSG_P(PSTR("[MQTT] Connecting to broker at %s:%d"), host, port); 210 | mqtt.setServer(host, port); 211 | #if MQTT_USE_ASYNC 212 | mqtt.setKeepAlive(MQTT_KEEPALIVE).setCleanSession(false); 213 | mqtt.setWill((mqttTopic + MQTT_TOPIC_STATUS).c_str(), MQTT_QOS, MQTT_RETAIN, "0"); 214 | if ((strlen(_mqttUser) > 0) && (strlen(_mqttPass) > 0)) { 215 | DEBUG_MSG_P(PSTR(" as user '%s'."), _mqttUser); 216 | mqtt.setCredentials(_mqttUser, _mqttPass); 217 | } 218 | DEBUG_MSG_P(PSTR("\n")); 219 | mqtt.connect(); 220 | #else 221 | bool response; 222 | 223 | if ((strlen(_mqttUser) > 0) && (strlen(_mqttPass) > 0)) { 224 | DEBUG_MSG_P(PSTR(" as user '%s'\n"), _mqttUser); 225 | response = mqtt.connect(getIdentifier().c_str(), _mqttUser, _mqttPass, (mqttTopic + MQTT_TOPIC_STATUS).c_str(), MQTT_QOS, MQTT_RETAIN, "0"); 226 | } else { 227 | DEBUG_MSG_P(PSTR("\n")); 228 | response = mqtt.connect(getIdentifier().c_str(), (mqttTopic + MQTT_TOPIC_STATUS).c_str(), MQTT_QOS, MQTT_RETAIN, "0"); 229 | } 230 | 231 | if (response) { 232 | _mqttOnConnect(); 233 | _mqttConnected = true; 234 | } else { 235 | DEBUG_MSG_P(PSTR("[MQTT] Connection failed\n")); 236 | } 237 | 238 | #endif 239 | free(host); 240 | 241 | String mqttSetter = getSetting("mqttSetter", MQTT_USE_SETTER); 242 | String mqttGetter = getSetting("mqttGetter", MQTT_USE_GETTER); 243 | bool _mqttForward = !mqttGetter.equals(mqttSetter); 244 | 245 | } 246 | } 247 | 248 | void mqttSetup() { 249 | #if MQTT_USE_ASYNC 250 | mqtt.onConnect([](bool sessionPresent) { 251 | _mqttOnConnect(); 252 | }); 253 | mqtt.onDisconnect([](AsyncMqttClientDisconnectReason reason) { 254 | _mqttOnDisconnect(); 255 | }); 256 | mqtt.onMessage([](char* topic, char* payload, AsyncMqttClientMessageProperties properties, size_t len, size_t index, size_t total) { 257 | _mqttOnMessage(topic, payload, len); 258 | }); 259 | #else 260 | mqtt.setCallback([](char* topic, byte* payload, unsigned int length) { 261 | _mqttOnMessage(topic, (char *) payload, length); 262 | }); 263 | #endif 264 | buildTopics(); 265 | } 266 | 267 | void mqttLoop() { 268 | 269 | static unsigned long lastPeriod = 0; 270 | 271 | if (WiFi.status() == WL_CONNECTED) { 272 | 273 | if (!mqtt.connected()) { 274 | 275 | #if not MQTT_USE_ASYNC 276 | if (_mqttConnected) { 277 | _mqttOnDisconnect(); 278 | _mqttConnected = false; 279 | } 280 | #endif 281 | 282 | unsigned long currPeriod = millis() / MQTT_RECONNECT_DELAY; 283 | if (currPeriod != lastPeriod) { 284 | lastPeriod = currPeriod; 285 | mqttConnect(); 286 | } 287 | 288 | #if not MQTT_USE_ASYNC 289 | } else { 290 | mqtt.loop(); 291 | #endif 292 | 293 | } 294 | 295 | } 296 | 297 | } 298 | -------------------------------------------------------------------------------- /code/espurna/nofuss.ino: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | NOFUSS MODULE 4 | 5 | Copyright (C) 2016-2017 by Xose Pérez 6 | 7 | */ 8 | 9 | #if ENABLE_NOFUSS 10 | 11 | #include "NoFUSSClient.h" 12 | 13 | // ----------------------------------------------------------------------------- 14 | // NOFUSS 15 | // ----------------------------------------------------------------------------- 16 | 17 | void nofussSetup() { 18 | 19 | NoFUSSClient.setServer(getSetting("nofussServer", NOFUSS_SERVER)); 20 | NoFUSSClient.setDevice(DEVICE); 21 | NoFUSSClient.setVersion(APP_VERSION); 22 | 23 | NoFUSSClient.onMessage([](nofuss_t code) { 24 | 25 | if (code == NOFUSS_START) { 26 | DEBUG_MSG_P(PSTR("[NoFUSS] Start\n")); 27 | } 28 | 29 | if (code == NOFUSS_UPTODATE) { 30 | DEBUG_MSG_P(PSTR("[NoFUSS] Already in the last version\n")); 31 | } 32 | 33 | if (code == NOFUSS_PARSE_ERROR) { 34 | DEBUG_MSG_P(PSTR("[NoFUSS] Error parsing server response\n")); 35 | } 36 | 37 | if (code == NOFUSS_UPDATING) { 38 | DEBUG_MSG_P(PSTR("[NoFUSS] Updating"); 39 | DEBUG_MSG_P(PSTR(" New version: %s\n"), (char *) NoFUSSClient.getNewVersion().c_str()); 40 | DEBUG_MSG_P(PSTR(" Firmware: %s\n"), (char *) NoFUSSClient.getNewFirmware().c_str()); 41 | DEBUG_MSG_P(PSTR(" File System: %s"), (char *) NoFUSSClient.getNewFileSystem().c_str()); 42 | } 43 | 44 | if (code == NOFUSS_FILESYSTEM_UPDATE_ERROR) { 45 | DEBUG_MSG_P(PSTR("[NoFUSS] File System Update Error: %s\n"), (char *) NoFUSSClient.getErrorString().c_str()); 46 | } 47 | 48 | if (code == NOFUSS_FILESYSTEM_UPDATED) { 49 | DEBUG_MSG_P(PSTR("[NoFUSS] File System Updated\n")); 50 | } 51 | 52 | if (code == NOFUSS_FIRMWARE_UPDATE_ERROR) { 53 | DEBUG_MSG_P(PSTR("[NoFUSS] Firmware Update Error: %s\n"), (char *) NoFUSSClient.getErrorString().c_str()); 54 | } 55 | 56 | if (code == NOFUSS_FIRMWARE_UPDATED) { 57 | DEBUG_MSG_P(PSTR("[NoFUSS] Firmware Updated\n")); 58 | } 59 | 60 | if (code == NOFUSS_RESET) { 61 | DEBUG_MSG_P(PSTR("[NoFUSS] Resetting board\n")); 62 | } 63 | 64 | if (code == NOFUSS_END) { 65 | DEBUG_MSG_P(PSTR("[NoFUSS] End\n")); 66 | } 67 | 68 | }); 69 | 70 | } 71 | 72 | void nofussLoop() { 73 | 74 | static unsigned long last_check = 0; 75 | if (!wifiConnected()) return; 76 | unsigned long interval = getSetting("nofussInterval", NOFUSS_INTERVAL).toInt(); 77 | if ((last_check > 0) && ((millis() - last_check) < interval)) return; 78 | last_check = millis(); 79 | NoFUSSClient.handle(); 80 | 81 | } 82 | 83 | #endif 84 | -------------------------------------------------------------------------------- /code/espurna/ntp.ino: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | NTP MODULE 4 | 5 | Copyright (C) 2016-2017 by Xose Pérez 6 | 7 | */ 8 | 9 | #include 10 | #include 11 | #include 12 | 13 | // ----------------------------------------------------------------------------- 14 | // NTP 15 | // ----------------------------------------------------------------------------- 16 | 17 | void ntpConnect() { 18 | NTP.begin(NTP_SERVER, NTP_TIME_OFFSET, NTP_DAY_LIGHT); 19 | NTP.setInterval(NTP_UPDATE_INTERVAL); 20 | } 21 | 22 | void ntpSetup() { 23 | 24 | NTP.onNTPSyncEvent([](NTPSyncEvent_t error) { 25 | if (error) { 26 | if (error == noResponse) { 27 | DEBUG_MSG_P(PSTR("[NTP] Error: NTP server not reachable\n")); 28 | } else if (error == invalidAddress) { 29 | DEBUG_MSG_P(PSTR("[NTP] Error: Invalid NTP server address\n")); 30 | } 31 | } else { 32 | DEBUG_MSG_P(PSTR("[NTP] Time: %s\n"), (char *) NTP.getTimeDateString(NTP.getLastNTPSync()).c_str()); 33 | } 34 | }); 35 | 36 | } 37 | 38 | void ntpLoop() { 39 | now(); 40 | } 41 | -------------------------------------------------------------------------------- /code/espurna/ota.ino: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | OTA MODULE 4 | 5 | Copyright (C) 2016-2017 by Xose Pérez 6 | 7 | */ 8 | 9 | #include "ArduinoOTA.h" 10 | 11 | // ----------------------------------------------------------------------------- 12 | // OTA 13 | // ----------------------------------------------------------------------------- 14 | 15 | void otaConfigure() { 16 | ArduinoOTA.setPort(OTA_PORT); 17 | ArduinoOTA.setHostname(getSetting("hostname", HOSTNAME).c_str()); 18 | ArduinoOTA.setPassword(getSetting("adminPass", ADMIN_PASS).c_str()); 19 | } 20 | 21 | void otaSetup() { 22 | 23 | otaConfigure(); 24 | 25 | ArduinoOTA.onStart([]() { 26 | DEBUG_MSG_P(PSTR("[OTA] Start\n")); 27 | wsSend("{\"message\": \"OTA update started\"}"); 28 | }); 29 | 30 | ArduinoOTA.onEnd([]() { 31 | DEBUG_MSG_P(PSTR("\n[OTA] End\n")); 32 | wsSend("{\"action\": \"reload\"}"); 33 | delay(100); 34 | }); 35 | 36 | ArduinoOTA.onProgress([](unsigned int progress, unsigned int total) { 37 | DEBUG_MSG_P(PSTR("[OTA] Progress: %u%%\r"), (progress / (total / 100))); 38 | }); 39 | 40 | ArduinoOTA.onError([](ota_error_t error) { 41 | #if DEBUG_PORT 42 | DEBUG_MSG_P(PSTR("\n[OTA] Error[%u]: "), error); 43 | if (error == OTA_AUTH_ERROR) DEBUG_MSG_P(PSTR("Auth Failed\n")); 44 | else if (error == OTA_BEGIN_ERROR) DEBUG_MSG_P(PSTR("Begin Failed\n")); 45 | else if (error == OTA_CONNECT_ERROR) DEBUG_MSG_P(PSTR("Connect Failed\n")); 46 | else if (error == OTA_RECEIVE_ERROR) DEBUG_MSG_P(PSTR("Receive Failed\n")); 47 | else if (error == OTA_END_ERROR) DEBUG_MSG_P(PSTR("End Failed\n")); 48 | #endif 49 | }); 50 | 51 | ArduinoOTA.begin(); 52 | 53 | } 54 | 55 | void otaLoop() { 56 | ArduinoOTA.handle(); 57 | } 58 | -------------------------------------------------------------------------------- /code/espurna/pow.ino: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | POW MODULE 4 | Support for Sonoff POW HLW8012-based power monitor 5 | 6 | Copyright (C) 2016-2017 by Xose Pérez 7 | 8 | */ 9 | 10 | #if ENABLE_POW 11 | 12 | #include 13 | #include 14 | #include 15 | #include 16 | 17 | HLW8012 hlw8012; 18 | bool _powEnabled = false; 19 | 20 | // ----------------------------------------------------------------------------- 21 | // POW 22 | // ----------------------------------------------------------------------------- 23 | 24 | // When using interrupts we have to call the library entry point 25 | // whenever an interrupt is triggered 26 | void hlw8012_cf1_interrupt() { 27 | hlw8012.cf1_interrupt(); 28 | } 29 | 30 | void hlw8012_cf_interrupt() { 31 | hlw8012.cf_interrupt(); 32 | } 33 | 34 | void powEnable(bool status) { 35 | _powEnabled = status; 36 | if (_powEnabled) { 37 | #if POW_USE_INTERRUPTS == 1 38 | attachInterrupt(POW_CF1_PIN, hlw8012_cf1_interrupt, CHANGE); 39 | attachInterrupt(POW_CF_PIN, hlw8012_cf_interrupt, CHANGE); 40 | #endif 41 | DEBUG_MSG_P(PSTR("[POW] Enabled\n")); 42 | } else { 43 | #if POW_USE_INTERRUPTS == 1 44 | detachInterrupt(POW_CF1_PIN); 45 | detachInterrupt(POW_CF_PIN); 46 | #endif 47 | DEBUG_MSG_P(PSTR("[POW] Disabled\n")); 48 | } 49 | } 50 | 51 | // ----------------------------------------------------------------------------- 52 | 53 | void powSaveCalibration() { 54 | setSetting("powPowerMult", hlw8012.getPowerMultiplier()); 55 | setSetting("powCurrentMult", hlw8012.getCurrentMultiplier()); 56 | setSetting("powVoltageMult", hlw8012.getVoltageMultiplier()); 57 | } 58 | 59 | void powRetrieveCalibration() { 60 | double value; 61 | value = getSetting("powPowerMult", 0).toFloat(); 62 | if (value > 0) hlw8012.setPowerMultiplier((int) value); 63 | value = getSetting("powCurrentMult", 0).toFloat(); 64 | if (value > 0) hlw8012.setCurrentMultiplier((int) value); 65 | value = getSetting("powVoltageMult", 0).toFloat(); 66 | if (value > 0) hlw8012.setVoltageMultiplier((int) value); 67 | } 68 | 69 | void powSetExpectedActivePower(unsigned int power) { 70 | if (power > 0) { 71 | hlw8012.expectedActivePower(power); 72 | powSaveCalibration(); 73 | } 74 | } 75 | 76 | void powSetExpectedCurrent(double current) { 77 | if (current > 0) { 78 | hlw8012.expectedCurrent(current); 79 | powSaveCalibration(); 80 | } 81 | } 82 | 83 | void powSetExpectedVoltage(unsigned int voltage) { 84 | if (voltage > 0) { 85 | hlw8012.expectedVoltage(voltage); 86 | powSaveCalibration(); 87 | } 88 | } 89 | 90 | void powReset() { 91 | hlw8012.resetMultipliers(); 92 | powSaveCalibration(); 93 | } 94 | 95 | // ----------------------------------------------------------------------------- 96 | 97 | unsigned int getActivePower() { 98 | return hlw8012.getActivePower(); 99 | } 100 | 101 | unsigned int getApparentPower() { 102 | return hlw8012.getApparentPower(); 103 | } 104 | 105 | unsigned int getReactivePower() { 106 | return hlw8012.getReactivePower(); 107 | } 108 | 109 | double getCurrent() { 110 | return hlw8012.getCurrent(); 111 | } 112 | 113 | unsigned int getVoltage() { 114 | return hlw8012.getVoltage(); 115 | } 116 | 117 | unsigned int getPowerFactor() { 118 | return (int) (100 * hlw8012.getPowerFactor()); 119 | } 120 | 121 | // ----------------------------------------------------------------------------- 122 | 123 | void powSetup() { 124 | 125 | // Initialize HLW8012 126 | // void begin(unsigned char cf_pin, unsigned char cf1_pin, unsigned char sel_pin, unsigned char currentWhen = HIGH, bool use_interrupts = false, unsigned long pulse_timeout = PULSE_TIMEOUT); 127 | // * cf_pin, cf1_pin and sel_pin are GPIOs to the HLW8012 IC 128 | // * currentWhen is the value in sel_pin to select current sampling 129 | // * set use_interrupts to true to use interrupts to monitor pulse widths 130 | // * leave pulse_timeout to the default value, recommended when using interrupts 131 | #if POW_USE_INTERRUPTS 132 | hlw8012.begin(POW_CF_PIN, POW_CF1_PIN, POW_SEL_PIN, POW_SEL_CURRENT, true); 133 | #else 134 | hlw8012.begin(POW_CF_PIN, POW_CF1_PIN, POW_SEL_PIN, POW_SEL_CURRENT, false, 1000000); 135 | #endif 136 | 137 | // These values are used to calculate current, voltage and power factors as per datasheet formula 138 | // These are the nominal values for the Sonoff POW resistors: 139 | // * The CURRENT_RESISTOR is the 1milliOhm copper-manganese resistor in series with the main line 140 | // * The VOLTAGE_RESISTOR_UPSTREAM are the 5 470kOhm resistors in the voltage divider that feeds the V2P pin in the HLW8012 141 | // * The VOLTAGE_RESISTOR_DOWNSTREAM is the 1kOhm resistor in the voltage divider that feeds the V2P pin in the HLW8012 142 | hlw8012.setResistors(POW_CURRENT_R, POW_VOLTAGE_R_UP, POW_VOLTAGE_R_DOWN); 143 | 144 | // Retrieve calibration values 145 | powRetrieveCalibration(); 146 | 147 | // API definitions 148 | apiRegister("/api/power", "power", [](char * buffer, size_t len) { 149 | snprintf(buffer, len, "%d", getActivePower()); 150 | }); 151 | apiRegister("/api/current", "current", [](char * buffer, size_t len) { 152 | dtostrf(getCurrent(), len-1, 2, buffer); 153 | }); 154 | apiRegister("/api/voltage", "voltage", [](char * buffer, size_t len) { 155 | snprintf(buffer, len, "%d", getVoltage()); 156 | }); 157 | 158 | } 159 | 160 | void powLoop() { 161 | 162 | static unsigned long last_update = 0; 163 | static unsigned char report_count = POW_REPORT_EVERY; 164 | 165 | static unsigned long power_sum = 0; 166 | static double current_sum = 0; 167 | static unsigned long voltage_sum = 0; 168 | static bool powWasEnabled = false; 169 | 170 | // POW is disabled while there is no internet connection 171 | // When the HLW8012 measurements are enabled back we reset the timer 172 | if (!_powEnabled) { 173 | powWasEnabled = false; 174 | return; 175 | } 176 | if (!powWasEnabled) { 177 | last_update = millis(); 178 | powWasEnabled = true; 179 | } 180 | 181 | if (millis() - last_update > POW_UPDATE_INTERVAL) { 182 | 183 | last_update = millis(); 184 | 185 | unsigned int power = getActivePower(); 186 | unsigned int voltage = getVoltage(); 187 | double current = getCurrent(); 188 | unsigned int apparent = getApparentPower(); 189 | unsigned int factor = getPowerFactor(); 190 | unsigned int reactive = getReactivePower(); 191 | 192 | power_sum += power; 193 | current_sum += current; 194 | voltage_sum += voltage; 195 | 196 | DynamicJsonBuffer jsonBuffer; 197 | JsonObject& root = jsonBuffer.createObject(); 198 | 199 | root["powVisible"] = 1; 200 | root["powActivePower"] = power; 201 | root["powCurrent"] = current; 202 | root["powVoltage"] = voltage; 203 | root["powApparentPower"] = apparent; 204 | root["powReactivePower"] = reactive; 205 | root["powPowerFactor"] = factor; 206 | 207 | String output; 208 | root.printTo(output); 209 | wsSend(output.c_str()); 210 | 211 | if (--report_count == 0) { 212 | 213 | power = power_sum / POW_REPORT_EVERY; 214 | current = current_sum / POW_REPORT_EVERY; 215 | voltage = voltage_sum / POW_REPORT_EVERY; 216 | apparent = current * voltage; 217 | reactive = (apparent > power) ? sqrt(apparent * apparent - power * power) : 0; 218 | factor = (apparent > 0) ? 100 * power / apparent : 100; 219 | if (factor > 100) factor = 100; 220 | 221 | // Calculate energy increment (ppower times time) and create C-string 222 | double energy_inc = (double) power * POW_REPORT_EVERY * POW_UPDATE_INTERVAL / 1000.0 / 3600.0; 223 | char energy_buf[11]; 224 | dtostrf(energy_inc, 11, 3, energy_buf); 225 | char *e = energy_buf; 226 | while ((unsigned char) *e == ' ') ++e; 227 | 228 | // Report values to MQTT broker 229 | mqttSend(getSetting("powPowerTopic", POW_POWER_TOPIC).c_str(), String(power).c_str()); 230 | mqttSend(getSetting("powEnergyTopic", POW_ENERGY_TOPIC).c_str(), e); 231 | mqttSend(getSetting("powCurrentTopic", POW_CURRENT_TOPIC).c_str(), String(current).c_str()); 232 | mqttSend(getSetting("powVoltageTopic", POW_VOLTAGE_TOPIC).c_str(), String(voltage).c_str()); 233 | mqttSend(getSetting("powAPowerTopic", POW_APOWER_TOPIC).c_str(), String(apparent).c_str()); 234 | mqttSend(getSetting("powRPowerTopic", POW_RPOWER_TOPIC).c_str(), String(reactive).c_str()); 235 | mqttSend(getSetting("powPFactorTopic", POW_PFACTOR_TOPIC).c_str(), String(factor).c_str()); 236 | 237 | // Report values to Domoticz 238 | #if ENABLE_DOMOTICZ 239 | { 240 | char buffer[20]; 241 | snprintf(buffer, 20, "%d;%s", power, e); 242 | domoticzSend("dczPowIdx", 0, buffer); 243 | snprintf(buffer, 20, "%s", e); 244 | domoticzSend("dczEnergyIdx", 0, buffer); 245 | snprintf(buffer, 20, "%d", voltage); 246 | domoticzSend("dczVoltIdx", 0, buffer); 247 | snprintf(buffer, 20, "%s", String(current).c_str()); 248 | domoticzSend("dczCurrentIdx", 0, buffer); 249 | } 250 | #endif 251 | 252 | // Reset counters 253 | power_sum = current_sum = voltage_sum = 0; 254 | report_count = POW_REPORT_EVERY; 255 | 256 | } 257 | 258 | // Toggle between current and voltage monitoring 259 | #if POW_USE_INTERRUPTS == 0 260 | hlw8012.toggleMode(); 261 | #endif 262 | 263 | } 264 | 265 | } 266 | 267 | #endif 268 | -------------------------------------------------------------------------------- /code/espurna/relay.ino: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | RELAY MODULE 4 | 5 | Copyright (C) 2016-2017 by Xose Pérez 6 | 7 | */ 8 | 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | 15 | typedef struct { 16 | unsigned char pin; 17 | bool reverse; 18 | unsigned char led; 19 | } relay_t; 20 | std::vector _relays; 21 | Ticker pulseTicker; 22 | bool recursive = false; 23 | 24 | #if RELAY_PROVIDER == RELAY_PROVIDER_DUAL 25 | unsigned char _dual_status = 0; 26 | #endif 27 | 28 | // ----------------------------------------------------------------------------- 29 | // RELAY PROVIDERS 30 | // ----------------------------------------------------------------------------- 31 | 32 | void relayProviderStatus(unsigned char id, bool status) { 33 | 34 | #if RELAY_PROVIDER == RELAY_PROVIDER_DUAL 35 | _dual_status ^= (1 << id); 36 | Serial.flush(); 37 | Serial.write(0xA0); 38 | Serial.write(0x04); 39 | Serial.write(_dual_status); 40 | Serial.write(0xA1); 41 | Serial.flush(); 42 | #endif 43 | 44 | #if RELAY_PROVIDER == RELAY_PROVIDER_LIGHT 45 | lightState(status); 46 | #endif 47 | 48 | #if RELAY_PROVIDER == RELAY_PROVIDER_RELAY 49 | digitalWrite(_relays[id].pin, _relays[id].reverse ? !status : status); 50 | #endif 51 | 52 | } 53 | 54 | bool relayProviderStatus(unsigned char id) { 55 | 56 | #if RELAY_PROVIDER == RELAY_PROVIDER_DUAL 57 | if (id >= 2) return false; 58 | return ((_dual_status & (1 << id)) > 0); 59 | #endif 60 | 61 | #if RELAY_PROVIDER == RELAY_PROVIDER_LIGHT 62 | return lightState(); 63 | #endif 64 | 65 | #if RELAY_PROVIDER == RELAY_PROVIDER_RELAY 66 | if (id >= _relays.size()) return false; 67 | bool status = (digitalRead(_relays[id].pin) == HIGH); 68 | return _relays[id].reverse ? !status : status; 69 | #endif 70 | 71 | } 72 | 73 | // ----------------------------------------------------------------------------- 74 | // RELAY 75 | // ----------------------------------------------------------------------------- 76 | 77 | String relayString() { 78 | DynamicJsonBuffer jsonBuffer; 79 | JsonObject& root = jsonBuffer.createObject(); 80 | JsonArray& relay = root.createNestedArray("relayStatus"); 81 | for (unsigned char i=0; i= _relays.size()) return false; 151 | 152 | bool changed = false; 153 | 154 | if (relayStatus(id) != status) { 155 | 156 | DEBUG_MSG_P(PSTR("[RELAY] %d => %s\n"), id, status ? "ON" : "OFF"); 157 | changed = true; 158 | 159 | relayProviderStatus(id, status); 160 | 161 | if (_relays[id].led > 0) { 162 | ledStatus(_relays[id].led - 1, status); 163 | } 164 | 165 | if (report) relayMQTT(id); 166 | if (!recursive) { 167 | relayPulse(id); 168 | relaySync(id); 169 | relaySave(); 170 | relayWS(); 171 | } 172 | 173 | #if ENABLE_DOMOTICZ 174 | relayDomoticzSend(id); 175 | #endif 176 | 177 | } 178 | 179 | return changed; 180 | 181 | } 182 | 183 | bool relayStatus(unsigned char id, bool status) { 184 | return relayStatus(id, status, true); 185 | } 186 | 187 | void relaySync(unsigned char id) { 188 | 189 | if (_relays.size() > 1) { 190 | 191 | recursive = true; 192 | 193 | byte relaySync = getSetting("relaySync", RELAY_SYNC).toInt(); 194 | bool status = relayStatus(id); 195 | 196 | // If RELAY_SYNC_SAME all relays should have the same state 197 | if (relaySync == RELAY_SYNC_SAME) { 198 | for (unsigned short i=0; i<_relays.size(); i++) { 199 | if (i != id) relayStatus(i, status); 200 | } 201 | 202 | // If NONE_OR_ONE or ONE and setting ON we should set OFF all the others 203 | } else if (status) { 204 | if (relaySync != RELAY_SYNC_ANY) { 205 | for (unsigned short i=0; i<_relays.size(); i++) { 206 | if (i != id) relayStatus(i, false); 207 | } 208 | } 209 | 210 | // If ONLY_ONE and setting OFF we should set ON the other one 211 | } else { 212 | if (relaySync == RELAY_SYNC_ONE) { 213 | unsigned char i = (id + 1) % _relays.size(); 214 | relayStatus(i, true); 215 | } 216 | } 217 | 218 | recursive = false; 219 | 220 | } 221 | 222 | } 223 | 224 | void relaySave() { 225 | unsigned char bit = 1; 226 | unsigned char mask = 0; 227 | for (unsigned int i=0; i < _relays.size(); i++) { 228 | if (relayStatus(i)) mask += bit; 229 | bit += bit; 230 | } 231 | EEPROM.write(EEPROM_RELAY_STATUS, mask); 232 | EEPROM.commit(); 233 | } 234 | 235 | void relayRetrieve(bool invert) { 236 | recursive = true; 237 | unsigned char bit = 1; 238 | unsigned char mask = invert ? ~EEPROM.read(EEPROM_RELAY_STATUS) : EEPROM.read(EEPROM_RELAY_STATUS); 239 | for (unsigned int i=0; i < _relays.size(); i++) { 240 | relayStatus(i, ((mask & bit) == bit)); 241 | bit += bit; 242 | } 243 | if (invert) { 244 | EEPROM.write(EEPROM_RELAY_STATUS, mask); 245 | EEPROM.commit(); 246 | } 247 | recursive = false; 248 | } 249 | 250 | void relayToggle(unsigned char id) { 251 | if (id >= _relays.size()) return; 252 | relayStatus(id, !relayStatus(id)); 253 | } 254 | 255 | unsigned char relayCount() { 256 | return _relays.size(); 257 | } 258 | 259 | //------------------------------------------------------------------------------ 260 | // REST API 261 | //------------------------------------------------------------------------------ 262 | 263 | void relaySetupAPI() { 264 | 265 | // API entry points (protected with apikey) 266 | for (unsigned int relayID=0; relayID= 0) { 356 | unsigned long value = root["nvalue"]; 357 | DEBUG_MSG_P(PSTR("[DOMOTICZ] Received value %d for IDX %d\n"), value, idx); 358 | relayStatus(relayID, value == 1); 359 | } 360 | 361 | } 362 | 363 | } 364 | 365 | }); 366 | } 367 | 368 | #endif 369 | 370 | //------------------------------------------------------------------------------ 371 | // MQTT 372 | //------------------------------------------------------------------------------ 373 | 374 | void relayMQTT(unsigned char id) { 375 | if (id >= _relays.size()) return; 376 | mqttSend(MQTT_TOPIC_RELAY, id, relayStatus(id) ? "1" : "0"); 377 | } 378 | 379 | void relayMQTT() { 380 | for (unsigned int i=0; i < _relays.size(); i++) { 381 | relayMQTT(i); 382 | } 383 | } 384 | 385 | void relayMQTTCallback(unsigned int type, const char * topic, const char * payload) { 386 | 387 | if (type == MQTT_CONNECT_EVENT) { 388 | 389 | #if not MQTT_REPORT_RELAY 390 | relayMQTT(); 391 | #endif 392 | 393 | char buffer[strlen(MQTT_TOPIC_RELAY) + 3]; 394 | sprintf(buffer, "%s/+", MQTT_TOPIC_RELAY); 395 | mqttSubscribe(buffer); 396 | 397 | } 398 | 399 | if (type == MQTT_MESSAGE_EVENT) { 400 | 401 | // Match topic 402 | String t = mqttSubtopic((char *) topic); 403 | if (!t.startsWith(MQTT_TOPIC_RELAY)) return; 404 | 405 | // Get value 406 | unsigned int value = (char)payload[0] - '0'; 407 | 408 | // Pulse topic 409 | if (t.endsWith("pulse")) { 410 | relayPulseMode(value, mqttForward()); 411 | return; 412 | } 413 | 414 | // Get relay ID 415 | unsigned int relayID = t.substring(strlen(MQTT_TOPIC_RELAY)+1).toInt(); 416 | if (relayID >= relayCount()) { 417 | DEBUG_MSG_P(PSTR("[RELAY] Wrong relayID (%d)\n"), relayID); 418 | return; 419 | } 420 | 421 | // Action to perform 422 | if (value == 2) { 423 | relayToggle(relayID); 424 | } else { 425 | relayStatus(relayID, value > 0, mqttForward()); 426 | } 427 | 428 | } 429 | 430 | } 431 | 432 | void relaySetupMQTT() { 433 | mqttRegister(relayMQTTCallback); 434 | } 435 | 436 | //------------------------------------------------------------------------------ 437 | // Setup 438 | //------------------------------------------------------------------------------ 439 | 440 | void relaySetup() { 441 | 442 | #ifdef SONOFF_DUAL 443 | 444 | // Two dummy relays for the dual 445 | _relays.push_back((relay_t) {0, 0}); 446 | _relays.push_back((relay_t) {0, 0}); 447 | 448 | #elif AI_LIGHT 449 | 450 | // One dummy relay for the AI Thinker Light 451 | _relays.push_back((relay_t) {0, 0}); 452 | 453 | #else 454 | 455 | #ifdef RELAY1_PIN 456 | _relays.push_back((relay_t) { RELAY1_PIN, RELAY1_PIN_INVERSE, RELAY1_LED }); 457 | #endif 458 | #ifdef RELAY2_PIN 459 | _relays.push_back((relay_t) { RELAY2_PIN, RELAY2_PIN_INVERSE, RELAY2_LED }); 460 | #endif 461 | #ifdef RELAY3_PIN 462 | _relays.push_back((relay_t) { RELAY3_PIN, RELAY3_PIN_INVERSE, RELAY3_LED }); 463 | #endif 464 | #ifdef RELAY4_PIN 465 | _relays.push_back((relay_t) { RELAY4_PIN, RELAY4_PIN_INVERSE, RELAY4_LED }); 466 | #endif 467 | 468 | #endif 469 | 470 | byte relayMode = getSetting("relayMode", RELAY_MODE).toInt(); 471 | for (unsigned int i=0; i < _relays.size(); i++) { 472 | pinMode(_relays[i].pin, OUTPUT); 473 | if (relayMode == RELAY_MODE_OFF) relayStatus(i, false); 474 | if (relayMode == RELAY_MODE_ON) relayStatus(i, true); 475 | } 476 | if (relayMode == RELAY_MODE_SAME) relayRetrieve(false); 477 | if (relayMode == RELAY_MODE_TOOGLE) relayRetrieve(true); 478 | 479 | relaySetupAPI(); 480 | relaySetupMQTT(); 481 | #if ENABLE_DOMOTICZ 482 | relayDomoticzSetup(); 483 | #endif 484 | 485 | DEBUG_MSG_P(PSTR("[RELAY] Number of relays: %d\n"), _relays.size()); 486 | 487 | } 488 | -------------------------------------------------------------------------------- /code/espurna/rf.ino: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | RF MODULE 4 | 5 | Copyright (C) 2016-2017 by Xose Pérez 6 | 7 | */ 8 | 9 | #if ENABLE_RF 10 | 11 | #include 12 | 13 | unsigned long rfCode = 0; 14 | unsigned long rfCodeON = 0; 15 | unsigned long rfCodeOFF = 0; 16 | 17 | // ----------------------------------------------------------------------------- 18 | // RF 19 | // ----------------------------------------------------------------------------- 20 | 21 | void rfLoop() { 22 | return; 23 | if (rfCode == 0) return; 24 | DEBUG_MSG_P(PSTR("[RF] Received code: %lu\n"), rfCode); 25 | if (rfCode == rfCodeON) relayStatus(0, true); 26 | if (rfCode == rfCodeOFF) relayStatus(0, false); 27 | rfCode = 0; 28 | } 29 | 30 | void rfBuildCodes() { 31 | 32 | unsigned long code = 0; 33 | 34 | // channel 35 | unsigned int channel = getSetting("rfChannel", RF_CHANNEL).toInt(); 36 | for (byte i = 0; i < 5; i++) { 37 | code *= 3; 38 | if (channel & 1) code += 1; 39 | channel >>= 1; 40 | } 41 | 42 | // device 43 | unsigned int device = getSetting("rfDevice", RF_DEVICE).toInt(); 44 | for (byte i = 0; i < 5; i++) { 45 | code *= 3; 46 | if (device != i) code += 2; 47 | } 48 | 49 | // status 50 | code *= 9; 51 | rfCodeOFF = code + 2; 52 | rfCodeON = code + 6; 53 | 54 | DEBUG_MSG_P(PSTR("[RF] Code ON : %lu\n"), rfCodeON); 55 | DEBUG_MSG_P(PSTR("[RF] Code OFF: %lu\n"), rfCodeOFF); 56 | 57 | } 58 | 59 | void rfCallback(unsigned long code, unsigned int period) { 60 | rfCode = code; 61 | } 62 | 63 | void rfSetup() { 64 | 65 | pinMode(RF_PIN, INPUT_PULLUP); 66 | rfBuildCodes(); 67 | RemoteReceiver::init(RF_PIN, 3, rfCallback); 68 | RemoteReceiver::disable(); 69 | DEBUG_MSG_P(PSTR("[RF] Disabled\n")); 70 | 71 | static WiFiEventHandler e1 = WiFi.onStationModeDisconnected([](const WiFiEventStationModeDisconnected& event) { 72 | RemoteReceiver::disable(); 73 | DEBUG_MSG_P(PSTR("[RF] Disabled\n")); 74 | }); 75 | 76 | static WiFiEventHandler e2 = WiFi.onSoftAPModeStationDisconnected([](const WiFiEventSoftAPModeStationDisconnected& event) { 77 | RemoteReceiver::disable(); 78 | DEBUG_MSG_P(PSTR("[RF] Disabled\n")); 79 | }); 80 | 81 | static WiFiEventHandler e3 = WiFi.onStationModeConnected([](const WiFiEventStationModeConnected& event) { 82 | RemoteReceiver::enable(); 83 | DEBUG_MSG_P(PSTR("[RF] Enabled\n")); 84 | }); 85 | 86 | static WiFiEventHandler e4 = WiFi.onSoftAPModeStationConnected([](const WiFiEventSoftAPModeStationConnected& event) { 87 | RemoteReceiver::enable(); 88 | DEBUG_MSG_P(PSTR("[RF] Enabled\n")); 89 | }); 90 | 91 | } 92 | 93 | #endif 94 | -------------------------------------------------------------------------------- /code/espurna/settings.ino: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | SETTINGS MODULE 4 | 5 | Copyright (C) 2016-2017 by Xose Pérez 6 | 7 | */ 8 | 9 | #include "Embedis.h" 10 | #include 11 | #include "spi_flash.h" 12 | #include 13 | 14 | #define AUTO_SAVE 1 15 | 16 | Embedis embedis(Serial); 17 | 18 | // ----------------------------------------------------------------------------- 19 | // Settings 20 | // ----------------------------------------------------------------------------- 21 | 22 | unsigned long settingsSize() { 23 | unsigned pos = SPI_FLASH_SEC_SIZE - 1; 24 | while (size_t len = EEPROM.read(pos)) { 25 | pos = pos - len - 2; 26 | } 27 | return SPI_FLASH_SEC_SIZE - pos; 28 | } 29 | 30 | unsigned int settingsKeyCount() { 31 | unsigned count = 0; 32 | unsigned pos = SPI_FLASH_SEC_SIZE - 1; 33 | while (size_t len = EEPROM.read(pos)) { 34 | pos = pos - len - 2; 35 | count ++; 36 | } 37 | return count / 2; 38 | } 39 | 40 | String settingsKeyName(unsigned int index) { 41 | 42 | String s; 43 | 44 | unsigned count = 0; 45 | unsigned stop = index * 2 + 1; 46 | unsigned pos = SPI_FLASH_SEC_SIZE - 1; 47 | while (size_t len = EEPROM.read(pos)) { 48 | pos = pos - len - 2; 49 | count++; 50 | if (count == stop) { 51 | s.reserve(len); 52 | for (unsigned char i = 0 ; i < len; i++) { 53 | s += (char) EEPROM.read(pos + i + 1); 54 | } 55 | break; 56 | } 57 | } 58 | 59 | return s; 60 | 61 | } 62 | 63 | void settingsFactoryReset() { 64 | for (unsigned int i = 0; i < SPI_FLASH_SEC_SIZE; i++) { 65 | EEPROM.write(i, 0xFF); 66 | } 67 | EEPROM.commit(); 68 | } 69 | 70 | void settingsSetup() { 71 | 72 | EEPROM.begin(SPI_FLASH_SEC_SIZE); 73 | 74 | Embedis::dictionary( F("EEPROM"), 75 | SPI_FLASH_SEC_SIZE, 76 | [](size_t pos) -> char { return EEPROM.read(pos); }, 77 | [](size_t pos, char value) { EEPROM.write(pos, value); }, 78 | #if AUTO_SAVE 79 | []() { EEPROM.commit(); } 80 | #else 81 | []() {} 82 | #endif 83 | ); 84 | 85 | Embedis::hardware( F("WIFI"), [](Embedis* e) { 86 | StreamString s; 87 | WiFi.printDiag(s); 88 | e->response(s); 89 | }, 0); 90 | 91 | Embedis::command( F("RECONNECT"), [](Embedis* e) { 92 | wifiConfigure(); 93 | wifiDisconnect(); 94 | e->response(Embedis::OK); 95 | }); 96 | 97 | Embedis::command( F("RESET"), [](Embedis* e) { 98 | e->response(Embedis::OK); 99 | ESP.restart(); 100 | }); 101 | 102 | Embedis::command( F("FACTORY.RESET"), [](Embedis* e) { 103 | settingsFactoryReset(); 104 | e->response(Embedis::OK); 105 | }); 106 | 107 | Embedis::command( F("HEAP"), [](Embedis* e) { 108 | e->stream->printf("Free HEAP: %d bytes\n", ESP.getFreeHeap()); 109 | e->response(Embedis::OK); 110 | }); 111 | 112 | Embedis::command( F("RELAY"), [](Embedis* e) { 113 | if (e->argc < 2) { 114 | return e->response(Embedis::ARGS_ERROR); 115 | } 116 | int id = String(e->argv[1]).toInt(); 117 | if (e->argc > 2) { 118 | int value = String(e->argv[2]).toInt(); 119 | if (value == 2) { 120 | relayToggle(id); 121 | } else { 122 | relayStatus(id, value == 1); 123 | } 124 | } 125 | e->stream->printf("Status: %s\n", relayStatus(id) ? "true" : "false"); 126 | e->response(Embedis::OK); 127 | }); 128 | 129 | #if LIGHT_PROVIDER != LIGHT_PROVIDER_NONE 130 | Embedis::command( F("COLOR"), [](Embedis* e) { 131 | if (e->argc > 1) { 132 | String color = String(e->argv[1]); 133 | lightColor(color.c_str(), true, true); 134 | } 135 | e->stream->printf("Color: %s\n", lightColor().c_str()); 136 | e->response(Embedis::OK); 137 | }); 138 | #endif 139 | 140 | Embedis::command( F("EEPROM"), [](Embedis* e) { 141 | unsigned long freeEEPROM = SPI_FLASH_SEC_SIZE - settingsSize(); 142 | e->stream->printf("Number of keys: %d\n", settingsKeyCount()); 143 | e->stream->printf("Free EEPROM: %d bytes (%d%%)\n", freeEEPROM, 100 * freeEEPROM / SPI_FLASH_SEC_SIZE); 144 | e->response(Embedis::OK); 145 | }); 146 | 147 | Embedis::command( F("DUMP"), [](Embedis* e) { 148 | unsigned int size = settingsKeyCount(); 149 | for (unsigned int i=0; istream->printf("+%s => %s\n", key.c_str(), value.c_str()); 153 | } 154 | e->response(Embedis::OK); 155 | }); 156 | 157 | Embedis::command( F("DUMP.RAW"), [](Embedis* e) { 158 | for (unsigned int i = 0; i < SPI_FLASH_SEC_SIZE; i++) { 159 | if (i % 16 == 0) e->stream->printf("\n[%04X] ", i); 160 | e->stream->printf("%02X ", EEPROM.read(i)); 161 | } 162 | e->stream->printf("\n"); 163 | e->response(Embedis::OK); 164 | }); 165 | 166 | DEBUG_MSG_P(PSTR("[SETTINGS] EEPROM size: %d bytes\n"), SPI_FLASH_SEC_SIZE); 167 | DEBUG_MSG_P(PSTR("[SETTINGS] Settings size: %d bytes\n"), settingsSize()); 168 | 169 | } 170 | 171 | void settingsLoop() { 172 | embedis.process(); 173 | } 174 | 175 | template String getSetting(const String& key, T defaultValue) { 176 | String value; 177 | if (!Embedis::get(key, value)) value = String(defaultValue); 178 | return value; 179 | } 180 | 181 | String getSetting(const String& key) { 182 | return getSetting(key, ""); 183 | } 184 | 185 | template bool setSetting(const String& key, T value) { 186 | return Embedis::set(key, String(value)); 187 | } 188 | 189 | bool delSetting(const String& key) { 190 | return Embedis::del(key); 191 | } 192 | 193 | void saveSettings() { 194 | DEBUG_MSG_P(PSTR("[SETTINGS] Saving\n")); 195 | #if not AUTO_SAVE 196 | EEPROM.commit(); 197 | #endif 198 | } 199 | -------------------------------------------------------------------------------- /code/espurna/wifi.ino: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | WIFI MODULE 4 | 5 | Copyright (C) 2016-2017 by Xose Pérez 6 | 7 | */ 8 | 9 | #include "JustWifi.h" 10 | 11 | // ----------------------------------------------------------------------------- 12 | // WIFI 13 | // ----------------------------------------------------------------------------- 14 | 15 | String getIP() { 16 | if (WiFi.getMode() == WIFI_AP) { 17 | return WiFi.softAPIP().toString(); 18 | } 19 | return WiFi.localIP().toString(); 20 | } 21 | 22 | String getNetwork() { 23 | if (WiFi.getMode() == WIFI_AP) { 24 | return jw.getAPSSID(); 25 | } 26 | return WiFi.SSID(); 27 | } 28 | 29 | void wifiDisconnect() { 30 | #if ENABLE_POW 31 | powEnable(false); 32 | #endif 33 | jw.disconnect(); 34 | } 35 | 36 | void resetConnectionTimeout() { 37 | jw.resetReconnectTimeout(); 38 | } 39 | 40 | bool wifiConnected() { 41 | return jw.connected(); 42 | } 43 | 44 | bool createAP() { 45 | jw.disconnect(); 46 | jw.resetReconnectTimeout(); 47 | return jw.createAP(); 48 | } 49 | 50 | void wifiConfigure() { 51 | 52 | jw.setHostname(getSetting("hostname", HOSTNAME).c_str()); 53 | jw.setSoftAP(getSetting("hostname", HOSTNAME).c_str(), getSetting("adminPass", ADMIN_PASS).c_str()); 54 | jw.setReconnectTimeout(WIFI_RECONNECT_INTERVAL); 55 | jw.setAPMode(AP_MODE); 56 | jw.cleanNetworks(); 57 | 58 | int i; 59 | for (i = 0; i< WIFI_MAX_NETWORKS; i++) { 60 | if (getSetting("ssid" + String(i)).length() == 0) break; 61 | if (getSetting("ip" + String(i)).length() == 0) { 62 | jw.addNetwork( 63 | getSetting("ssid" + String(i)).c_str(), 64 | getSetting("pass" + String(i)).c_str() 65 | ); 66 | } else { 67 | jw.addNetwork( 68 | getSetting("ssid" + String(i)).c_str(), 69 | getSetting("pass" + String(i)).c_str(), 70 | getSetting("ip" + String(i)).c_str(), 71 | getSetting("gw" + String(i)).c_str(), 72 | getSetting("mask" + String(i)).c_str(), 73 | getSetting("dns" + String(i)).c_str() 74 | ); 75 | } 76 | } 77 | 78 | // Scan for best network only if we have more than 1 defined 79 | jw.scanNetworks(i>1); 80 | 81 | } 82 | 83 | void wifiStatus() { 84 | 85 | if (WiFi.getMode() == WIFI_AP_STA) { 86 | DEBUG_MSG_P(PSTR("[WIFI] MODE AP + STA --------------------------------\n")); 87 | } else if (WiFi.getMode() == WIFI_AP) { 88 | DEBUG_MSG_P(PSTR("[WIFI] MODE AP --------------------------------------\n")); 89 | } else if (WiFi.getMode() == WIFI_STA) { 90 | DEBUG_MSG_P(PSTR("[WIFI] MODE STA -------------------------------------\n")); 91 | } else { 92 | DEBUG_MSG_P(PSTR("[WIFI] MODE OFF -------------------------------------\n")); 93 | DEBUG_MSG_P(PSTR("[WIFI] No connection\n")); 94 | } 95 | 96 | if ((WiFi.getMode() & WIFI_AP) == WIFI_AP) { 97 | DEBUG_MSG_P(PSTR("[WIFI] SSID %s\n"), jw.getAPSSID().c_str()); 98 | DEBUG_MSG_P(PSTR("[WIFI] PASS %s\n"), getSetting("adminPass", ADMIN_PASS).c_str()); 99 | DEBUG_MSG_P(PSTR("[WIFI] IP %s\n"), WiFi.softAPIP().toString().c_str()); 100 | DEBUG_MSG_P(PSTR("[WIFI] MAC %s\n"), WiFi.softAPmacAddress().c_str()); 101 | } 102 | 103 | if ((WiFi.getMode() & WIFI_STA) == WIFI_STA) { 104 | DEBUG_MSG_P(PSTR("[WIFI] SSID %s\n"), WiFi.SSID().c_str()); 105 | DEBUG_MSG_P(PSTR("[WIFI] IP %s\n"), WiFi.localIP().toString().c_str()); 106 | DEBUG_MSG_P(PSTR("[WIFI] MAC %s\n"), WiFi.macAddress().c_str()); 107 | DEBUG_MSG_P(PSTR("[WIFI] GW %s\n"), WiFi.gatewayIP().toString().c_str()); 108 | DEBUG_MSG_P(PSTR("[WIFI] DNS %s\n"), WiFi.dnsIP().toString().c_str()); 109 | DEBUG_MSG_P(PSTR("[WIFI] MASK %s\n"), WiFi.subnetMask().toString().c_str()); 110 | DEBUG_MSG_P(PSTR("[WIFI] HOST %s\n"), WiFi.hostname().c_str()); 111 | } 112 | 113 | DEBUG_MSG_P(PSTR("[WIFI] ----------------------------------------------\n")); 114 | 115 | } 116 | 117 | void wifiSetup() { 118 | wifiConfigure(); 119 | 120 | // Message callbacks 121 | jw.onMessage([](justwifi_messages_t code, char * parameter) { 122 | 123 | #ifdef DEBUG_PORT 124 | 125 | if (code == MESSAGE_SCANNING) { 126 | DEBUG_MSG_P(PSTR("[WIFI] Scanning\n")); 127 | } 128 | 129 | if (code == MESSAGE_SCAN_FAILED) { 130 | DEBUG_MSG_P(PSTR("[WIFI] Scan failed\n")); 131 | } 132 | 133 | if (code == MESSAGE_NO_NETWORKS) { 134 | DEBUG_MSG_P(PSTR("[WIFI] No networks found\n")); 135 | } 136 | 137 | if (code == MESSAGE_NO_KNOWN_NETWORKS) { 138 | DEBUG_MSG_P(PSTR("[WIFI] No known networks found\n")); 139 | } 140 | 141 | if (code == MESSAGE_FOUND_NETWORK) { 142 | DEBUG_MSG_P(PSTR("[WIFI] %s\n"), parameter); 143 | } 144 | 145 | if (code == MESSAGE_CONNECTING) { 146 | DEBUG_MSG_P(PSTR("[WIFI] Connecting to %s\n"), parameter); 147 | } 148 | 149 | if (code == MESSAGE_CONNECT_WAITING) { 150 | // too much noise 151 | } 152 | 153 | if (code == MESSAGE_CONNECT_FAILED) { 154 | DEBUG_MSG_P(PSTR("[WIFI] Could not connect to %s\n"), parameter); 155 | } 156 | 157 | if (code == MESSAGE_CONNECTED) { 158 | wifiStatus(); 159 | } 160 | 161 | if (code == MESSAGE_ACCESSPOINT_CREATED) { 162 | wifiStatus(); 163 | } 164 | 165 | if (code == MESSAGE_DISCONNECTED) { 166 | DEBUG_MSG_P(PSTR("[WIFI] Disconnected\n")); 167 | } 168 | 169 | if (code == MESSAGE_ACCESSPOINT_CREATING) { 170 | DEBUG_MSG_P(PSTR("[WIFI] Creating access point\n")); 171 | } 172 | 173 | if (code == MESSAGE_ACCESSPOINT_FAILED) { 174 | DEBUG_MSG_P(PSTR("[WIFI] Could not create access point\n")); 175 | } 176 | 177 | #endif 178 | 179 | // Configure mDNS 180 | #if ENABLE_MDNS 181 | if (code == MESSAGE_CONNECTED || code == MESSAGE_ACCESSPOINT_CREATED) { 182 | if (MDNS.begin(WiFi.getMode() == WIFI_AP ? APP_NAME : (char *) WiFi.hostname().c_str())) { 183 | MDNS.addService("http", "tcp", 80); 184 | DEBUG_MSG_P(PSTR("[MDNS] OK\n")); 185 | } else { 186 | DEBUG_MSG_P(PSTR("[MDNS] FAIL\n")); 187 | } 188 | } 189 | #endif 190 | 191 | // NTP connection reset 192 | if (code == MESSAGE_CONNECTED) { 193 | ntpConnect(); 194 | } 195 | 196 | // Manage POW 197 | #if ENABLE_POW 198 | if (code == MESSAGE_CONNECTED) { 199 | powEnable(true); 200 | } 201 | if (code == MESSAGE_DISCONNECTED) { 202 | powEnable(false); 203 | } 204 | #endif 205 | 206 | 207 | }); 208 | 209 | } 210 | 211 | void wifiLoop() { 212 | jw.loop(); 213 | } 214 | -------------------------------------------------------------------------------- /code/gulpfile.js: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | ESP8266 file system builder 4 | 5 | Copyright (C) 2016 by Xose Pérez 6 | 7 | This program is free software: you can redistribute it and/or modify 8 | it under the terms of the GNU General Public License as published by 9 | the Free Software Foundation, either version 3 of the License, or 10 | (at your option) any later version. 11 | 12 | This program is distributed in the hope that it will be useful, 13 | but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | GNU General Public License for more details. 16 | 17 | You should have received a copy of the GNU General Public License 18 | along with this program. If not, see . 19 | 20 | */ 21 | 22 | // ----------------------------------------------------------------------------- 23 | // File system builder 24 | // ----------------------------------------------------------------------------- 25 | 26 | const fs = require('fs'); 27 | const gulp = require('gulp'); 28 | const plumber = require('gulp-plumber'); 29 | const htmlmin = require('gulp-htmlmin'); 30 | const cleancss = require('gulp-clean-css'); 31 | const uglify = require('gulp-uglify'); 32 | const gzip = require('gulp-gzip'); 33 | const del = require('del'); 34 | const useref = require('gulp-useref'); 35 | const gulpif = require('gulp-if'); 36 | const inline = require('gulp-inline'); 37 | const inlineImages = require('gulp-css-base64'); 38 | const favicon = require('gulp-base64-favicon'); 39 | 40 | const dataFolder = 'espurna/data/'; 41 | 42 | gulp.task('clean', function() { 43 | del([ dataFolder + '*']); 44 | return true; 45 | }); 46 | 47 | gulp.task('files', ['clean'], function() { 48 | return gulp.src([ 49 | 'html/**/*.{jpg,jpeg,png,ico,gif}', 50 | 'html/fsversion' 51 | ]) 52 | .pipe(gulp.dest(dataFolder)); 53 | }); 54 | 55 | gulp.task('buildfs_embed', ['buildfs_inline'], function() { 56 | 57 | var source = dataFolder + 'index.html.gz'; 58 | var destination = dataFolder + '../config/data.h'; 59 | 60 | var wstream = fs.createWriteStream(destination); 61 | wstream.on('error', function (err) { 62 | console.log(err); 63 | }); 64 | 65 | var data = fs.readFileSync(source); 66 | 67 | wstream.write('#define index_html_gz_len ' + data.length + '\n'); 68 | wstream.write('const uint8_t index_html_gz[] PROGMEM = {') 69 | 70 | for (i=0; i"); 62 | this.container = this.elem.parent(); 63 | this.offLabel = $("").appendTo(this.container); 64 | this.offSpan = this.offLabel.children('span'); 65 | this.onLabel = $("").appendTo(this.container); 66 | this.onBorder = $("
").appendTo(this.container); 67 | this.offBorder = $("
").appendTo(this.container); 68 | this.onSpan = this.onLabel.children('span'); 69 | this.handle = $("
").appendTo(this.container); 70 | this.handleCenter = $("
").appendTo(this.handle); 71 | this.handleRight = $("
").appendTo(this.handle); 72 | return true; 73 | }; 74 | 75 | iOSCheckbox.prototype.disableTextSelection = function() { 76 | if ($.browser.msie) { 77 | return $([this.handle, this.offLabel, this.onLabel, this.container]).attr("unselectable", "on"); 78 | } 79 | }; 80 | 81 | iOSCheckbox.prototype._getDimension = function(elem, dimension) { 82 | if ($.fn.actual != null) { 83 | return elem.actual(dimension); 84 | } else { 85 | return elem[dimension](); 86 | } 87 | }; 88 | 89 | iOSCheckbox.prototype.optionallyResize = function(mode) { 90 | var newWidth, offLabelWidth, offSpan, onLabelWidth, onSpan; 91 | 92 | onSpan = this.onLabel.find('span'); 93 | onLabelWidth = this._getDimension(onSpan, "width"); 94 | onLabelWidth += parseInt(onSpan.css('padding-left'), 10); 95 | offSpan = this.offLabel.find('span'); 96 | offLabelWidth = this._getDimension(offSpan, "width"); 97 | offLabelWidth += parseInt(offSpan.css('padding-right'), 10); 98 | if (mode === "container") { 99 | newWidth = onLabelWidth > offLabelWidth ? onLabelWidth : offLabelWidth; 100 | newWidth += this._getDimension(this.handle, "width") + this.handleMargin; 101 | return this.container.css({ 102 | width: newWidth 103 | }); 104 | } else { 105 | newWidth = onLabelWidth > offLabelWidth ? onLabelWidth : offLabelWidth; 106 | this.handleCenter.css({ 107 | width: newWidth + 4 108 | }); 109 | return this.handle.css({ 110 | width: newWidth + 7 111 | }); 112 | } 113 | }; 114 | 115 | iOSCheckbox.prototype.onMouseDown = function(event) { 116 | var x; 117 | 118 | event.preventDefault(); 119 | if (this.isDisabled()) { 120 | return; 121 | } 122 | x = event.pageX || event.originalEvent.changedTouches[0].pageX; 123 | iOSCheckbox.currentlyClicking = this.handle; 124 | iOSCheckbox.dragStartPosition = x; 125 | return iOSCheckbox.handleLeftOffset = parseInt(this.handle.css('left'), 10) || 0; 126 | }; 127 | 128 | iOSCheckbox.prototype.onDragMove = function(event, x) { 129 | var newWidth, p; 130 | 131 | if (iOSCheckbox.currentlyClicking !== this.handle) { 132 | return; 133 | } 134 | p = (x + iOSCheckbox.handleLeftOffset - iOSCheckbox.dragStartPosition) / this.rightSide; 135 | if (p < 0) { 136 | p = 0; 137 | } 138 | if (p > 1) { 139 | p = 1; 140 | } 141 | newWidth = p * this.rightSide; 142 | this.handle.css({ 143 | left: newWidth 144 | }); 145 | this.onLabel.css({ 146 | width: newWidth + this.handleRadius 147 | }); 148 | this.offSpan.css({ 149 | marginRight: -newWidth 150 | }); 151 | return this.onSpan.css({ 152 | marginLeft: -(1 - p) * this.rightSide 153 | }); 154 | }; 155 | 156 | iOSCheckbox.prototype.onDragEnd = function(event, x) { 157 | var p; 158 | 159 | if (iOSCheckbox.currentlyClicking !== this.handle) { 160 | return; 161 | } 162 | if (this.isDisabled()) { 163 | return; 164 | } 165 | if (iOSCheckbox.dragging) { 166 | p = (x - iOSCheckbox.dragStartPosition) / this.rightSide; 167 | this.elem.prop('checked', p >= 0.5).change(); 168 | } else { 169 | this.elem.prop('checked', !this.elem.prop('checked')).change(); 170 | } 171 | iOSCheckbox.currentlyClicking = null; 172 | iOSCheckbox.dragging = null; 173 | if (typeof this.onChange === "function") { 174 | this.onChange(this.elem, this.elem.prop('checked')); 175 | } 176 | return this.didChange(); 177 | }; 178 | 179 | iOSCheckbox.prototype.refresh = function() { 180 | return this.didChange(); 181 | }; 182 | 183 | iOSCheckbox.prototype.didChange = function() { 184 | var new_left; 185 | 186 | if (this.isDisabled()) { 187 | this.container.addClass(this.disabledClass); 188 | return false; 189 | } else { 190 | this.container.removeClass(this.disabledClass); 191 | } 192 | new_left = this.elem.prop('checked') ? this.rightSide + 2 : 0; 193 | this.handle.animate({ 194 | left: new_left 195 | }, this.duration); 196 | this.onLabel.animate({ 197 | width: new_left + this.handleRadius 198 | }, this.duration); 199 | this.offSpan.animate({ 200 | marginRight: -new_left 201 | }, this.duration); 202 | return this.onSpan.animate({ 203 | marginLeft: new_left - this.rightSide 204 | }, this.duration); 205 | }; 206 | 207 | iOSCheckbox.prototype.attachEvents = function() { 208 | var localMouseMove, localMouseUp, self; 209 | 210 | self = this; 211 | localMouseMove = function(event) { 212 | return self.onGlobalMove.apply(self, arguments); 213 | }; 214 | localMouseUp = function(event) { 215 | self.onGlobalUp.apply(self, arguments); 216 | $(document).unbind('mousemove touchmove', localMouseMove); 217 | return $(document).unbind('mouseup touchend', localMouseUp); 218 | }; 219 | this.elem.change(function() { 220 | return self.refresh(); 221 | }); 222 | return this.container.bind('mousedown touchstart', function(event) { 223 | self.onMouseDown.apply(self, arguments); 224 | $(document).bind('mousemove touchmove', localMouseMove); 225 | return $(document).bind('mouseup touchend', localMouseUp); 226 | }); 227 | }; 228 | 229 | iOSCheckbox.prototype.initialPosition = function() { 230 | var containerWidth, offset; 231 | 232 | containerWidth = this._getDimension(this.container, "width"); 233 | this.offLabel.css({ 234 | width: containerWidth - this.containerRadius - 4 235 | }); 236 | this.offBorder.css({ 237 | left: containerWidth - 4 238 | }); 239 | offset = this.containerRadius + 1; 240 | if ($.browser.msie && $.browser.version < 7) { 241 | offset -= 3; 242 | } 243 | this.rightSide = containerWidth - this._getDimension(this.handle, "width") - offset; 244 | if (this.elem.is(':checked')) { 245 | this.handle.css({ 246 | left: this.rightSide 247 | }); 248 | this.onLabel.css({ 249 | width: this.rightSide + this.handleRadius 250 | }); 251 | this.offSpan.css({ 252 | marginRight: -this.rightSide, 253 | }); 254 | } else { 255 | this.onLabel.css({ 256 | width: 0 257 | }); 258 | this.onSpan.css({ 259 | marginLeft: -this.rightSide 260 | }); 261 | } 262 | if (this.isDisabled()) { 263 | return this.container.addClass(this.disabledClass); 264 | } 265 | }; 266 | 267 | iOSCheckbox.prototype.onGlobalMove = function(event) { 268 | var x; 269 | 270 | if (!(!this.isDisabled() && iOSCheckbox.currentlyClicking)) { 271 | return; 272 | } 273 | event.preventDefault(); 274 | x = event.pageX || event.originalEvent.changedTouches[0].pageX; 275 | if (!iOSCheckbox.dragging && (Math.abs(iOSCheckbox.dragStartPosition - x) > this.dragThreshold)) { 276 | iOSCheckbox.dragging = true; 277 | } 278 | return this.onDragMove(event, x); 279 | }; 280 | 281 | iOSCheckbox.prototype.onGlobalUp = function(event) { 282 | var x; 283 | 284 | if (!iOSCheckbox.currentlyClicking) { 285 | return; 286 | } 287 | event.preventDefault(); 288 | x = event.pageX || event.originalEvent.changedTouches[0].pageX; 289 | this.onDragEnd(event, x); 290 | return false; 291 | }; 292 | 293 | iOSCheckbox.defaults = { 294 | duration: 200, 295 | checkedLabel: 'ON', 296 | uncheckedLabel: 'OFF', 297 | resizeHandle: true, 298 | resizeContainer: true, 299 | disabledClass: 'iPhoneCheckDisabled', 300 | containerClass: 'iPhoneCheckContainer', 301 | labelOnClass: 'iPhoneCheckLabelOn', 302 | labelOffClass: 'iPhoneCheckLabelOff', 303 | handleClass: 'iPhoneCheckHandle', 304 | handleCenterClass: 'iPhoneCheckHandleCenter', 305 | handleRightClass: 'iPhoneCheckHandleRight', 306 | dragThreshold: 5, 307 | handleMargin: 15, 308 | handleRadius: 4, 309 | containerRadius: 5, 310 | dataName: "iphoneStyle", 311 | onChange: function() {} 312 | }; 313 | 314 | return iOSCheckbox; 315 | 316 | })(); 317 | 318 | $.iphoneStyle = this.iOSCheckbox = iOSCheckbox; 319 | 320 | $.fn.iphoneStyle = function() { 321 | var args, checkbox, dataName, existingControl, method, params, _i, _len, _ref, _ref1, _ref2, _ref3; 322 | 323 | args = 1 <= arguments.length ? __slice.call(arguments, 0) : []; 324 | dataName = (_ref = (_ref1 = args[0]) != null ? _ref1.dataName : void 0) != null ? _ref : iOSCheckbox.defaults.dataName; 325 | _ref2 = this.filter(':checkbox'); 326 | for (_i = 0, _len = _ref2.length; _i < _len; _i++) { 327 | checkbox = _ref2[_i]; 328 | existingControl = $(checkbox).data(dataName); 329 | if (existingControl != null) { 330 | method = args[0], params = 2 <= args.length ? __slice.call(args, 1) : []; 331 | if ((_ref3 = existingControl[method]) != null) { 332 | _ref3.apply(existingControl, params); 333 | } 334 | } else { 335 | new iOSCheckbox(checkbox, args[0]); 336 | } 337 | } 338 | return this; 339 | }; 340 | 341 | $.fn.iOSCheckbox = function(options) { 342 | var opts; 343 | 344 | if (options == null) { 345 | options = {}; 346 | } 347 | opts = $.extend({}, options, { 348 | resizeHandle: false, 349 | disabledClass: 'iOSCheckDisabled', 350 | containerClass: 'iOSCheckContainer', 351 | labelOnClass: 'iOSCheckLabelOn', 352 | labelOffClass: 'iOSCheckLabelOff', 353 | handleClass: 'iOSCheckHandle', 354 | handleCenterClass: 'iOSCheckHandleCenter', 355 | handleRightClass: 'iOSCheckHandleRight', 356 | dataName: 'iOSCheckbox' 357 | }); 358 | return this.iphoneStyle(opts); 359 | }; 360 | 361 | }).call(this); 362 | -------------------------------------------------------------------------------- /code/html/custom.css: -------------------------------------------------------------------------------- 1 | #menu .pure-menu-heading { 2 | font-size: 100%; 3 | padding: .5em .5em; 4 | } 5 | .header h2 { 6 | font-size: 1em; 7 | } 8 | .panel { 9 | display: none; 10 | } 11 | .footer { 12 | position: absolute; 13 | bottom: 0; 14 | left: 0; 15 | right: 0; 16 | padding: 10px; 17 | font-size: 80%; 18 | color: #999; 19 | } 20 | #menu .footer a { 21 | text-decoration: none; 22 | padding: 0px; 23 | } 24 | .content { 25 | margin: 0px; 26 | } 27 | .page { 28 | margin-top: 40px; 29 | } 30 | .pure-button { 31 | color: white; 32 | padding: 8px 12px; 33 | border-radius: 4px; 34 | text-shadow: 0 1px 1px rgba(0, 0, 0, 0.2); 35 | } 36 | .main-buttons { 37 | margin: 50px auto; 38 | text-align: center; 39 | } 40 | .main-buttons button { 41 | width: 100px; 42 | margin: 5px auto; 43 | } 44 | .button-update-password, 45 | .button-update { 46 | background: #1f8dd6; 47 | } 48 | .button-reset { 49 | background: rgb(202, 60, 60); 50 | } 51 | .button-reconnect { 52 | background: rgb(202, 60, 60); 53 | } 54 | .button-apikey { 55 | background: rgb(0, 202, 0); 56 | margin-left: 5px; 57 | } 58 | .button-add-network { 59 | background: rgb(28, 184, 65); 60 | } 61 | .button-del-network { 62 | background: rgb(202, 60, 60); 63 | } 64 | .button-more-network { 65 | background: rgb(223, 117, 20); 66 | } 67 | .button-settings-backup, 68 | .button-settings-restore { 69 | background: rgb(0, 202, 0); 70 | } 71 | .pure-g { 72 | margin-bottom: 20px; 73 | } 74 | legend { 75 | font-weight: bold; 76 | } 77 | .l-box { 78 | padding-right: 1px; 79 | } 80 | .pure-form input[type=text][disabled] { 81 | color: #777777; 82 | } 83 | div.hint { 84 | font-size: 80%; 85 | color: #ccc; 86 | } 87 | .break { 88 | margin-top: 5px; 89 | } 90 | #networks .pure-g { 91 | padding-bottom: 10px; 92 | margin-bottom: 5px; 93 | border-bottom: 2px dashed #e5e5e5; 94 | } 95 | #networks div.more { 96 | display: none; 97 | } 98 | .module { 99 | display: none; 100 | } 101 | .template { 102 | display: none; 103 | } 104 | .pure-form .center { 105 | margin: .5em 0 .2em; 106 | } 107 | .webmode { 108 | display: none; 109 | } 110 | #credentials { 111 | font-size: 200%; 112 | text-align: center; 113 | height: 100px; 114 | width: 400px; 115 | position: fixed; 116 | top: 50%; 117 | left: 50%; 118 | margin-top: -50px; 119 | margin-left: -200px; 120 | } 121 | -------------------------------------------------------------------------------- /code/html/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SensorsIot/Espurna-Framework/ed8c754fd86a704670ef38187313f0dab3e1ecba/code/html/favicon.ico -------------------------------------------------------------------------------- /code/html/grids-responsive-min.css: -------------------------------------------------------------------------------- 1 | @media screen and (min-width:35.5em){.pure-u-sm-1,.pure-u-sm-1-1,.pure-u-sm-1-2,.pure-u-sm-1-3,.pure-u-sm-2-3,.pure-u-sm-1-4,.pure-u-sm-3-4,.pure-u-sm-1-5,.pure-u-sm-2-5,.pure-u-sm-3-5,.pure-u-sm-4-5,.pure-u-sm-5-5,.pure-u-sm-1-6,.pure-u-sm-5-6,.pure-u-sm-1-8,.pure-u-sm-3-8,.pure-u-sm-5-8,.pure-u-sm-7-8,.pure-u-sm-1-12,.pure-u-sm-5-12,.pure-u-sm-7-12,.pure-u-sm-11-12,.pure-u-sm-1-24,.pure-u-sm-2-24,.pure-u-sm-3-24,.pure-u-sm-4-24,.pure-u-sm-5-24,.pure-u-sm-6-24,.pure-u-sm-7-24,.pure-u-sm-8-24,.pure-u-sm-9-24,.pure-u-sm-10-24,.pure-u-sm-11-24,.pure-u-sm-12-24,.pure-u-sm-13-24,.pure-u-sm-14-24,.pure-u-sm-15-24,.pure-u-sm-16-24,.pure-u-sm-17-24,.pure-u-sm-18-24,.pure-u-sm-19-24,.pure-u-sm-20-24,.pure-u-sm-21-24,.pure-u-sm-22-24,.pure-u-sm-23-24,.pure-u-sm-24-24{display:inline-block;*display:inline;zoom:1;letter-spacing:normal;word-spacing:normal;vertical-align:top;text-rendering:auto}.pure-u-sm-1-24{width:4.1667%;*width:4.1357%}.pure-u-sm-1-12,.pure-u-sm-2-24{width:8.3333%;*width:8.3023%}.pure-u-sm-1-8,.pure-u-sm-3-24{width:12.5%;*width:12.469%}.pure-u-sm-1-6,.pure-u-sm-4-24{width:16.6667%;*width:16.6357%}.pure-u-sm-1-5{width:20%;*width:19.969%}.pure-u-sm-5-24{width:20.8333%;*width:20.8023%}.pure-u-sm-1-4,.pure-u-sm-6-24{width:25%;*width:24.969%}.pure-u-sm-7-24{width:29.1667%;*width:29.1357%}.pure-u-sm-1-3,.pure-u-sm-8-24{width:33.3333%;*width:33.3023%}.pure-u-sm-3-8,.pure-u-sm-9-24{width:37.5%;*width:37.469%}.pure-u-sm-2-5{width:40%;*width:39.969%}.pure-u-sm-5-12,.pure-u-sm-10-24{width:41.6667%;*width:41.6357%}.pure-u-sm-11-24{width:45.8333%;*width:45.8023%}.pure-u-sm-1-2,.pure-u-sm-12-24{width:50%;*width:49.969%}.pure-u-sm-13-24{width:54.1667%;*width:54.1357%}.pure-u-sm-7-12,.pure-u-sm-14-24{width:58.3333%;*width:58.3023%}.pure-u-sm-3-5{width:60%;*width:59.969%}.pure-u-sm-5-8,.pure-u-sm-15-24{width:62.5%;*width:62.469%}.pure-u-sm-2-3,.pure-u-sm-16-24{width:66.6667%;*width:66.6357%}.pure-u-sm-17-24{width:70.8333%;*width:70.8023%}.pure-u-sm-3-4,.pure-u-sm-18-24{width:75%;*width:74.969%}.pure-u-sm-19-24{width:79.1667%;*width:79.1357%}.pure-u-sm-4-5{width:80%;*width:79.969%}.pure-u-sm-5-6,.pure-u-sm-20-24{width:83.3333%;*width:83.3023%}.pure-u-sm-7-8,.pure-u-sm-21-24{width:87.5%;*width:87.469%}.pure-u-sm-11-12,.pure-u-sm-22-24{width:91.6667%;*width:91.6357%}.pure-u-sm-23-24{width:95.8333%;*width:95.8023%}.pure-u-sm-1,.pure-u-sm-1-1,.pure-u-sm-5-5,.pure-u-sm-24-24{width:100%}}@media screen and (min-width:48em){.pure-u-md-1,.pure-u-md-1-1,.pure-u-md-1-2,.pure-u-md-1-3,.pure-u-md-2-3,.pure-u-md-1-4,.pure-u-md-3-4,.pure-u-md-1-5,.pure-u-md-2-5,.pure-u-md-3-5,.pure-u-md-4-5,.pure-u-md-5-5,.pure-u-md-1-6,.pure-u-md-5-6,.pure-u-md-1-8,.pure-u-md-3-8,.pure-u-md-5-8,.pure-u-md-7-8,.pure-u-md-1-12,.pure-u-md-5-12,.pure-u-md-7-12,.pure-u-md-11-12,.pure-u-md-1-24,.pure-u-md-2-24,.pure-u-md-3-24,.pure-u-md-4-24,.pure-u-md-5-24,.pure-u-md-6-24,.pure-u-md-7-24,.pure-u-md-8-24,.pure-u-md-9-24,.pure-u-md-10-24,.pure-u-md-11-24,.pure-u-md-12-24,.pure-u-md-13-24,.pure-u-md-14-24,.pure-u-md-15-24,.pure-u-md-16-24,.pure-u-md-17-24,.pure-u-md-18-24,.pure-u-md-19-24,.pure-u-md-20-24,.pure-u-md-21-24,.pure-u-md-22-24,.pure-u-md-23-24,.pure-u-md-24-24{display:inline-block;*display:inline;zoom:1;letter-spacing:normal;word-spacing:normal;vertical-align:top;text-rendering:auto}.pure-u-md-1-24{width:4.1667%;*width:4.1357%}.pure-u-md-1-12,.pure-u-md-2-24{width:8.3333%;*width:8.3023%}.pure-u-md-1-8,.pure-u-md-3-24{width:12.5%;*width:12.469%}.pure-u-md-1-6,.pure-u-md-4-24{width:16.6667%;*width:16.6357%}.pure-u-md-1-5{width:20%;*width:19.969%}.pure-u-md-5-24{width:20.8333%;*width:20.8023%}.pure-u-md-1-4,.pure-u-md-6-24{width:25%;*width:24.969%}.pure-u-md-7-24{width:29.1667%;*width:29.1357%}.pure-u-md-1-3,.pure-u-md-8-24{width:33.3333%;*width:33.3023%}.pure-u-md-3-8,.pure-u-md-9-24{width:37.5%;*width:37.469%}.pure-u-md-2-5{width:40%;*width:39.969%}.pure-u-md-5-12,.pure-u-md-10-24{width:41.6667%;*width:41.6357%}.pure-u-md-11-24{width:45.8333%;*width:45.8023%}.pure-u-md-1-2,.pure-u-md-12-24{width:50%;*width:49.969%}.pure-u-md-13-24{width:54.1667%;*width:54.1357%}.pure-u-md-7-12,.pure-u-md-14-24{width:58.3333%;*width:58.3023%}.pure-u-md-3-5{width:60%;*width:59.969%}.pure-u-md-5-8,.pure-u-md-15-24{width:62.5%;*width:62.469%}.pure-u-md-2-3,.pure-u-md-16-24{width:66.6667%;*width:66.6357%}.pure-u-md-17-24{width:70.8333%;*width:70.8023%}.pure-u-md-3-4,.pure-u-md-18-24{width:75%;*width:74.969%}.pure-u-md-19-24{width:79.1667%;*width:79.1357%}.pure-u-md-4-5{width:80%;*width:79.969%}.pure-u-md-5-6,.pure-u-md-20-24{width:83.3333%;*width:83.3023%}.pure-u-md-7-8,.pure-u-md-21-24{width:87.5%;*width:87.469%}.pure-u-md-11-12,.pure-u-md-22-24{width:91.6667%;*width:91.6357%}.pure-u-md-23-24{width:95.8333%;*width:95.8023%}.pure-u-md-1,.pure-u-md-1-1,.pure-u-md-5-5,.pure-u-md-24-24{width:100%}}@media screen and (min-width:64em){.pure-u-lg-1,.pure-u-lg-1-1,.pure-u-lg-1-2,.pure-u-lg-1-3,.pure-u-lg-2-3,.pure-u-lg-1-4,.pure-u-lg-3-4,.pure-u-lg-1-5,.pure-u-lg-2-5,.pure-u-lg-3-5,.pure-u-lg-4-5,.pure-u-lg-5-5,.pure-u-lg-1-6,.pure-u-lg-5-6,.pure-u-lg-1-8,.pure-u-lg-3-8,.pure-u-lg-5-8,.pure-u-lg-7-8,.pure-u-lg-1-12,.pure-u-lg-5-12,.pure-u-lg-7-12,.pure-u-lg-11-12,.pure-u-lg-1-24,.pure-u-lg-2-24,.pure-u-lg-3-24,.pure-u-lg-4-24,.pure-u-lg-5-24,.pure-u-lg-6-24,.pure-u-lg-7-24,.pure-u-lg-8-24,.pure-u-lg-9-24,.pure-u-lg-10-24,.pure-u-lg-11-24,.pure-u-lg-12-24,.pure-u-lg-13-24,.pure-u-lg-14-24,.pure-u-lg-15-24,.pure-u-lg-16-24,.pure-u-lg-17-24,.pure-u-lg-18-24,.pure-u-lg-19-24,.pure-u-lg-20-24,.pure-u-lg-21-24,.pure-u-lg-22-24,.pure-u-lg-23-24,.pure-u-lg-24-24{display:inline-block;*display:inline;zoom:1;letter-spacing:normal;word-spacing:normal;vertical-align:top;text-rendering:auto}.pure-u-lg-1-24{width:4.1667%;*width:4.1357%}.pure-u-lg-1-12,.pure-u-lg-2-24{width:8.3333%;*width:8.3023%}.pure-u-lg-1-8,.pure-u-lg-3-24{width:12.5%;*width:12.469%}.pure-u-lg-1-6,.pure-u-lg-4-24{width:16.6667%;*width:16.6357%}.pure-u-lg-1-5{width:20%;*width:19.969%}.pure-u-lg-5-24{width:20.8333%;*width:20.8023%}.pure-u-lg-1-4,.pure-u-lg-6-24{width:25%;*width:24.969%}.pure-u-lg-7-24{width:29.1667%;*width:29.1357%}.pure-u-lg-1-3,.pure-u-lg-8-24{width:33.3333%;*width:33.3023%}.pure-u-lg-3-8,.pure-u-lg-9-24{width:37.5%;*width:37.469%}.pure-u-lg-2-5{width:40%;*width:39.969%}.pure-u-lg-5-12,.pure-u-lg-10-24{width:41.6667%;*width:41.6357%}.pure-u-lg-11-24{width:45.8333%;*width:45.8023%}.pure-u-lg-1-2,.pure-u-lg-12-24{width:50%;*width:49.969%}.pure-u-lg-13-24{width:54.1667%;*width:54.1357%}.pure-u-lg-7-12,.pure-u-lg-14-24{width:58.3333%;*width:58.3023%}.pure-u-lg-3-5{width:60%;*width:59.969%}.pure-u-lg-5-8,.pure-u-lg-15-24{width:62.5%;*width:62.469%}.pure-u-lg-2-3,.pure-u-lg-16-24{width:66.6667%;*width:66.6357%}.pure-u-lg-17-24{width:70.8333%;*width:70.8023%}.pure-u-lg-3-4,.pure-u-lg-18-24{width:75%;*width:74.969%}.pure-u-lg-19-24{width:79.1667%;*width:79.1357%}.pure-u-lg-4-5{width:80%;*width:79.969%}.pure-u-lg-5-6,.pure-u-lg-20-24{width:83.3333%;*width:83.3023%}.pure-u-lg-7-8,.pure-u-lg-21-24{width:87.5%;*width:87.469%}.pure-u-lg-11-12,.pure-u-lg-22-24{width:91.6667%;*width:91.6357%}.pure-u-lg-23-24{width:95.8333%;*width:95.8023%}.pure-u-lg-1,.pure-u-lg-1-1,.pure-u-lg-5-5,.pure-u-lg-24-24{width:100%}}@media screen and (min-width:80em){.pure-u-xl-1,.pure-u-xl-1-1,.pure-u-xl-1-2,.pure-u-xl-1-3,.pure-u-xl-2-3,.pure-u-xl-1-4,.pure-u-xl-3-4,.pure-u-xl-1-5,.pure-u-xl-2-5,.pure-u-xl-3-5,.pure-u-xl-4-5,.pure-u-xl-5-5,.pure-u-xl-1-6,.pure-u-xl-5-6,.pure-u-xl-1-8,.pure-u-xl-3-8,.pure-u-xl-5-8,.pure-u-xl-7-8,.pure-u-xl-1-12,.pure-u-xl-5-12,.pure-u-xl-7-12,.pure-u-xl-11-12,.pure-u-xl-1-24,.pure-u-xl-2-24,.pure-u-xl-3-24,.pure-u-xl-4-24,.pure-u-xl-5-24,.pure-u-xl-6-24,.pure-u-xl-7-24,.pure-u-xl-8-24,.pure-u-xl-9-24,.pure-u-xl-10-24,.pure-u-xl-11-24,.pure-u-xl-12-24,.pure-u-xl-13-24,.pure-u-xl-14-24,.pure-u-xl-15-24,.pure-u-xl-16-24,.pure-u-xl-17-24,.pure-u-xl-18-24,.pure-u-xl-19-24,.pure-u-xl-20-24,.pure-u-xl-21-24,.pure-u-xl-22-24,.pure-u-xl-23-24,.pure-u-xl-24-24{display:inline-block;*display:inline;zoom:1;letter-spacing:normal;word-spacing:normal;vertical-align:top;text-rendering:auto}.pure-u-xl-1-24{width:4.1667%;*width:4.1357%}.pure-u-xl-1-12,.pure-u-xl-2-24{width:8.3333%;*width:8.3023%}.pure-u-xl-1-8,.pure-u-xl-3-24{width:12.5%;*width:12.469%}.pure-u-xl-1-6,.pure-u-xl-4-24{width:16.6667%;*width:16.6357%}.pure-u-xl-1-5{width:20%;*width:19.969%}.pure-u-xl-5-24{width:20.8333%;*width:20.8023%}.pure-u-xl-1-4,.pure-u-xl-6-24{width:25%;*width:24.969%}.pure-u-xl-7-24{width:29.1667%;*width:29.1357%}.pure-u-xl-1-3,.pure-u-xl-8-24{width:33.3333%;*width:33.3023%}.pure-u-xl-3-8,.pure-u-xl-9-24{width:37.5%;*width:37.469%}.pure-u-xl-2-5{width:40%;*width:39.969%}.pure-u-xl-5-12,.pure-u-xl-10-24{width:41.6667%;*width:41.6357%}.pure-u-xl-11-24{width:45.8333%;*width:45.8023%}.pure-u-xl-1-2,.pure-u-xl-12-24{width:50%;*width:49.969%}.pure-u-xl-13-24{width:54.1667%;*width:54.1357%}.pure-u-xl-7-12,.pure-u-xl-14-24{width:58.3333%;*width:58.3023%}.pure-u-xl-3-5{width:60%;*width:59.969%}.pure-u-xl-5-8,.pure-u-xl-15-24{width:62.5%;*width:62.469%}.pure-u-xl-2-3,.pure-u-xl-16-24{width:66.6667%;*width:66.6357%}.pure-u-xl-17-24{width:70.8333%;*width:70.8023%}.pure-u-xl-3-4,.pure-u-xl-18-24{width:75%;*width:74.969%}.pure-u-xl-19-24{width:79.1667%;*width:79.1357%}.pure-u-xl-4-5{width:80%;*width:79.969%}.pure-u-xl-5-6,.pure-u-xl-20-24{width:83.3333%;*width:83.3023%}.pure-u-xl-7-8,.pure-u-xl-21-24{width:87.5%;*width:87.469%}.pure-u-xl-11-12,.pure-u-xl-22-24{width:91.6667%;*width:91.6357%}.pure-u-xl-23-24{width:95.8333%;*width:95.8023%}.pure-u-xl-1,.pure-u-xl-1-1,.pure-u-xl-5-5,.pure-u-xl-24-24{width:100%}} 2 | -------------------------------------------------------------------------------- /code/html/images/border-off.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SensorsIot/Espurna-Framework/ed8c754fd86a704670ef38187313f0dab3e1ecba/code/html/images/border-off.png -------------------------------------------------------------------------------- /code/html/images/border-on.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SensorsIot/Espurna-Framework/ed8c754fd86a704670ef38187313f0dab3e1ecba/code/html/images/border-on.png -------------------------------------------------------------------------------- /code/html/images/handle-center.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SensorsIot/Espurna-Framework/ed8c754fd86a704670ef38187313f0dab3e1ecba/code/html/images/handle-center.png -------------------------------------------------------------------------------- /code/html/images/handle-left.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SensorsIot/Espurna-Framework/ed8c754fd86a704670ef38187313f0dab3e1ecba/code/html/images/handle-left.png -------------------------------------------------------------------------------- /code/html/images/handle-right.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SensorsIot/Espurna-Framework/ed8c754fd86a704670ef38187313f0dab3e1ecba/code/html/images/handle-right.png -------------------------------------------------------------------------------- /code/html/images/label-off.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SensorsIot/Espurna-Framework/ed8c754fd86a704670ef38187313f0dab3e1ecba/code/html/images/label-off.png -------------------------------------------------------------------------------- /code/html/images/label-on.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SensorsIot/Espurna-Framework/ed8c754fd86a704670ef38187313f0dab3e1ecba/code/html/images/label-on.png -------------------------------------------------------------------------------- /code/html/side-menu.css: -------------------------------------------------------------------------------- 1 | body { 2 | color: #777; 3 | } 4 | 5 | .pure-img-responsive { 6 | max-width: 100%; 7 | height: auto; 8 | } 9 | 10 | /* 11 | Add transition to containers so they can push in and out. 12 | */ 13 | #layout, 14 | #menu, 15 | .menu-link { 16 | -webkit-transition: all 0.2s ease-out; 17 | -moz-transition: all 0.2s ease-out; 18 | -ms-transition: all 0.2s ease-out; 19 | -o-transition: all 0.2s ease-out; 20 | transition: all 0.2s ease-out; 21 | } 22 | 23 | /* 24 | This is the parent `
` that contains the menu and the content area. 25 | */ 26 | #layout { 27 | position: relative; 28 | padding-left: 0; 29 | } 30 | #layout.active #menu { 31 | left: 150px; 32 | width: 150px; 33 | } 34 | 35 | #layout.active .menu-link { 36 | left: 150px; 37 | } 38 | /* 39 | The content `
` is where all your content goes. 40 | */ 41 | .content { 42 | margin: 0 auto; 43 | padding: 0 2em; 44 | max-width: 800px; 45 | margin-bottom: 50px; 46 | line-height: 1.6em; 47 | } 48 | 49 | .header { 50 | margin: 0; 51 | color: #333; 52 | text-align: center; 53 | padding: 2.5em 2em 0; 54 | border-bottom: 1px solid #eee; 55 | } 56 | .header h1 { 57 | margin: 0.2em 0; 58 | font-size: 3em; 59 | font-weight: 300; 60 | } 61 | .header h2 { 62 | font-weight: 300; 63 | color: #ccc; 64 | padding: 0; 65 | margin-top: 0; 66 | } 67 | 68 | .content-subhead { 69 | margin: 50px 0 20px 0; 70 | font-weight: 300; 71 | color: #888; 72 | } 73 | 74 | 75 | 76 | /* 77 | The `#menu` `
` is the parent `
` that contains the `.pure-menu` that 78 | appears on the left side of the page. 79 | */ 80 | 81 | #menu { 82 | margin-left: -150px; /* "#menu" width */ 83 | width: 150px; 84 | position: fixed; 85 | top: 0; 86 | left: 0; 87 | bottom: 0; 88 | z-index: 1000; /* so the menu or its navicon stays above all content */ 89 | background: #191818; 90 | overflow-y: auto; 91 | -webkit-overflow-scrolling: touch; 92 | } 93 | /* 94 | All anchors inside the menu should be styled like this. 95 | */ 96 | #menu a { 97 | color: #999; 98 | border: none; 99 | padding: 0.6em 0 0.6em 0.6em; 100 | } 101 | 102 | /* 103 | Remove all background/borders, since we are applying them to #menu. 104 | */ 105 | #menu .pure-menu, 106 | #menu .pure-menu ul { 107 | border: none; 108 | background: transparent; 109 | } 110 | 111 | /* 112 | Add that light border to separate items into groups. 113 | */ 114 | #menu .pure-menu ul, 115 | #menu .pure-menu .menu-item-divided { 116 | border-top: 1px solid #333; 117 | } 118 | /* 119 | Change color of the anchor links on hover/focus. 120 | */ 121 | #menu .pure-menu li a:hover, 122 | #menu .pure-menu li a:focus { 123 | background: #333; 124 | } 125 | 126 | /* 127 | This styles the selected menu item `
  • `. 128 | */ 129 | #menu .pure-menu-selected, 130 | #menu .pure-menu-heading { 131 | background: #1f8dd6; 132 | } 133 | /* 134 | This styles a link within a selected menu item `
  • `. 135 | */ 136 | #menu .pure-menu-selected a { 137 | color: #fff; 138 | } 139 | 140 | /* 141 | This styles the menu heading. 142 | */ 143 | #menu .pure-menu-heading { 144 | font-size: 110%; 145 | color: #fff; 146 | margin: 0; 147 | } 148 | 149 | /* -- Dynamic Button For Responsive Menu -------------------------------------*/ 150 | 151 | /* 152 | The button to open/close the Menu is custom-made and not part of Pure. Here's 153 | how it works: 154 | */ 155 | 156 | /* 157 | `.menu-link` represents the responsive menu toggle that shows/hides on 158 | small screens. 159 | */ 160 | .menu-link { 161 | position: fixed; 162 | display: block; /* show this only on small screens */ 163 | top: 0; 164 | left: 0; /* "#menu width" */ 165 | background: #000; 166 | background: rgba(0,0,0,0.7); 167 | font-size: 10px; /* change this value to increase/decrease button size */ 168 | z-index: 10; 169 | width: 2em; 170 | height: auto; 171 | padding: 2.1em 1.6em; 172 | } 173 | 174 | .menu-link:hover, 175 | .menu-link:focus { 176 | background: #000; 177 | } 178 | 179 | .menu-link span { 180 | position: relative; 181 | display: block; 182 | } 183 | 184 | .menu-link span, 185 | .menu-link span:before, 186 | .menu-link span:after { 187 | background-color: #fff; 188 | width: 100%; 189 | height: 0.2em; 190 | } 191 | 192 | .menu-link span:before, 193 | .menu-link span:after { 194 | position: absolute; 195 | margin-top: -0.6em; 196 | content: " "; 197 | } 198 | 199 | .menu-link span:after { 200 | margin-top: 0.6em; 201 | } 202 | 203 | 204 | /* -- Responsive Styles (Media Queries) ------------------------------------- */ 205 | 206 | /* 207 | Hides the menu at `48em`, but modify this based on your app's needs. 208 | */ 209 | @media (min-width: 48em) { 210 | 211 | .header, 212 | .content { 213 | padding-left: 2em; 214 | padding-right: 2em; 215 | } 216 | 217 | #layout { 218 | padding-left: 150px; /* left col width "#menu" */ 219 | left: 0; 220 | } 221 | #menu { 222 | left: 150px; 223 | } 224 | 225 | .menu-link { 226 | position: fixed; 227 | left: 150px; 228 | display: none; 229 | } 230 | 231 | #layout.active .menu-link { 232 | left: 150px; 233 | } 234 | } 235 | 236 | @media (max-width: 48em) { 237 | /* Only apply this when the window is small. Otherwise, the following 238 | case results in extra padding on the left: 239 | * Make the window small. 240 | * Tap the menu to trigger the active state. 241 | * Make the window large again. 242 | */ 243 | #layout.active { 244 | position: relative; 245 | left: 150px; 246 | } 247 | } 248 | 249 | -------------------------------------------------------------------------------- /code/html/wheelcolorpicker.css: -------------------------------------------------------------------------------- 1 | /** 2 | * jQuery Wheel Color Picker 3 | * Base Stylesheet 4 | * 5 | * http://www.jar2.net/projects/jquery-wheelcolorpicker 6 | * 7 | * Copyright © 2011-2016 Fajar Chandra. All rights reserved. 8 | * Released under MIT License. 9 | * http://www.opensource.org/licenses/mit-license.php 10 | * 11 | * Note: Width, height, left, and top properties are handled by the 12 | * plugin. These values might change on the fly. 13 | */ 14 | 15 | .jQWCP-wWidget { 16 | position: absolute; 17 | width: 250px; 18 | height: 180px; 19 | background: #eee; 20 | box-shadow: 1px 1px 4px rgba(0,0,0,.5); 21 | border-radius: 4px; 22 | border: solid 1px #aaa; 23 | padding: 10px; 24 | z-index: 1001; 25 | } 26 | 27 | .jQWCP-wWidget.jQWCP-block { 28 | position: relative; 29 | border-color: #aaa; 30 | box-shadow: inset 1px 1px 1px #ccc; 31 | } 32 | 33 | .jQWCP-wWheel { 34 | background-size: contain; 35 | position: relative; 36 | float: left; 37 | width: 180px; 38 | height: 180px; 39 | -webkit-border-radius: 90px; 40 | -moz-border-radius: 50%; 41 | border-radius: 50%; 42 | border: solid 1px #aaa; 43 | margin: -1px; 44 | margin-right: 10px; 45 | transition: border .15s; 46 | cursor: crosshair; 47 | } 48 | 49 | .jQWCP-wWheel:hover { 50 | border-color: #666; 51 | } 52 | 53 | .jQWCP-wWheelOverlay { 54 | position: absolute; 55 | top: 0; 56 | left: 0; 57 | width: 100%; 58 | height: 100%; 59 | background: #000; 60 | opacity: 0; 61 | -webkit-border-radius: 90px; 62 | -moz-border-radius: 50%; 63 | border-radius: 50%; 64 | } 65 | 66 | .jQWCP-wWheelCursor { 67 | width: 8px; 68 | height: 8px; 69 | position: absolute; 70 | top: 50%; 71 | left: 50%; 72 | margin: -6px -6px; 73 | cursor: crosshair; 74 | border: solid 2px #fff; 75 | box-shadow: 1px 1px 2px #000; 76 | border-radius: 50%; 77 | } 78 | 79 | .jQWCP-slider-wrapper, 80 | .jQWCP-wPreview { 81 | position: relative; 82 | width: 20px; 83 | height: 180px; 84 | float: left; 85 | margin-right: 10px; 86 | } 87 | 88 | .jQWCP-wWheel:last-child, 89 | .jQWCP-slider-wrapper:last-child, 90 | .jQWCP-wPreview:last-child { 91 | margin-right: 0; 92 | } 93 | 94 | .jQWCP-slider, 95 | .jQWCP-wPreviewBox { 96 | position: absolute; 97 | width: 100%; 98 | height: 100%; 99 | left: 0; 100 | top: 0; 101 | box-sizing: border-box; 102 | border: solid 1px #aaa; 103 | margin: -1px; 104 | -moz-border-radius: 4px; 105 | border-radius: 4px; 106 | transition: border .15s; 107 | } 108 | 109 | .jQWCP-slider { 110 | cursor: crosshair; 111 | } 112 | 113 | .jQWCP-slider-wrapper:hover .jQWCP-slider { 114 | border-color: #666; 115 | } 116 | 117 | .jQWCP-scursor { 118 | position: absolute; 119 | left: 0; 120 | top: 0; 121 | right: 0; 122 | height: 6px; 123 | margin: -5px -1px -5px -3px; 124 | cursor: crosshair; 125 | border: solid 2px #fff; 126 | box-shadow: 1px 1px 2px #000; 127 | border-radius: 4px; 128 | } 129 | 130 | .jQWCP-wAlphaSlider, 131 | .jQWCP-wPreviewBox { 132 | background: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQAQMAAAAlPW0iAAAABlBMVEVAQEB/f39eaJUuAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAB3RJTUUH3QYRBDgK9dKdMgAAABl0RVh0Q29tbWVudABDcmVhdGVkIHdpdGggR0lNUFeBDhcAAAARSURBVAjXY/jPwIAVYRf9DwB+vw/x6vMT1wAAAABJRU5ErkJggg==') center center; 133 | } 134 | 135 | .jQWCP-overlay { 136 | position: fixed; 137 | top: 0; 138 | left: 0; 139 | bottom: 0; 140 | right: 0; 141 | z-index: 1000; 142 | } 143 | 144 | /*********************/ 145 | 146 | /* Mobile layout */ 147 | 148 | .jQWCP-mobile.jQWCP-wWidget { 149 | position: fixed; 150 | bottom: 0; 151 | left: 0 !important; 152 | top: auto !important; 153 | width: 100%; 154 | height: 75%; 155 | max-height: 240px; 156 | box-sizing: border-box; 157 | border-radius: 0; 158 | } 159 | -------------------------------------------------------------------------------- /code/lib/readme.txt: -------------------------------------------------------------------------------- 1 | 2 | This directory is intended for the project specific (private) libraries. 3 | PlatformIO will compile them to static libraries and link to executable file. 4 | 5 | The source code of each library should be placed in separate directory, like 6 | "lib/private_lib/[here are source files]". 7 | 8 | For example, see how can be organized `Foo` and `Bar` libraries: 9 | 10 | |--lib 11 | | |--Bar 12 | | | |--docs 13 | | | |--examples 14 | | | |--src 15 | | | |- Bar.c 16 | | | |- Bar.h 17 | | |--Foo 18 | | | |- Foo.c 19 | | | |- Foo.h 20 | | |- readme.txt --> THIS FILE 21 | |- platformio.ini 22 | |--src 23 | |- main.c 24 | 25 | Then in `src/main.c` you should use: 26 | 27 | #include 28 | #include 29 | 30 | // rest H/C/CPP code 31 | 32 | PlatformIO will find your libraries automatically, configure preprocessor's 33 | include paths and build them. 34 | 35 | See additional options for PlatformIO Library Dependency Finder `lib_*`: 36 | 37 | http://docs.platformio.org/en/latest/projectconf.html#lib-install 38 | 39 | -------------------------------------------------------------------------------- /code/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "esp8266-filesystem-builder", 3 | "version": "0.1.0", 4 | "description": "Gulp based build system for ESP8266 file system files", 5 | "main": "gulpfile.js", 6 | "author": "Xose Pérez ", 7 | "license": "GPL-3.0", 8 | "devDependencies": { 9 | "del": "^2.2.1", 10 | "gulp": "^3.9.1", 11 | "gulp-base64-favicon": "^1.0.2", 12 | "gulp-clean-css": "^2.0.10", 13 | "gulp-css-base64": "^1.3.4", 14 | "gulp-gzip": "^1.4.0", 15 | "gulp-htmlmin": "^2.0.0", 16 | "gulp-if": "^2.0.1", 17 | "gulp-inline": "^0.1.1", 18 | "gulp-plumber": "^1.1.0", 19 | "gulp-uglify": "^1.5.3", 20 | "gulp-useref": "^3.1.2" 21 | }, 22 | "dependencies": {} 23 | } 24 | -------------------------------------------------------------------------------- /code/pio_hooks.py: -------------------------------------------------------------------------------- 1 | #!/bin/python 2 | 3 | import subprocess 4 | import socket 5 | from SCons.Script import DefaultEnvironment 6 | env = DefaultEnvironment() 7 | 8 | def before_build_spiffs(source, target, env): 9 | env.Execute("node node_modules/gulp/bin/gulp.js") 10 | 11 | env.AddPreAction(".pioenvs/%s/spiffs.bin" % env['PIOENV'], before_build_spiffs) 12 | -------------------------------------------------------------------------------- /code/platformio.ini: -------------------------------------------------------------------------------- 1 | [platformio] 2 | env_default = d1-debug 3 | src_dir = espurna 4 | data_dir = espurna/data 5 | 6 | [common] 7 | build_flags = -g -DDEBUG_PORT=Serial -DMQTT_MAX_PACKET_SIZE=400 8 | build_flags_1m128 = ${common.build_flags} -Wl,-Tesp8266.flash.1m128.ld 9 | lib_deps = 10 | DHT sensor library 11 | Adafruit Unified Sensor 12 | https://github.com/xoseperez/Time 13 | ArduinoJson 14 | https://github.com/me-no-dev/ESPAsyncTCP#36b6b5a 15 | https://github.com/me-no-dev/ESPAsyncWebServer#bab5457 16 | https://github.com/marvinroger/async-mqtt-client#f1b4576 17 | PubSubClient 18 | Embedis 19 | NtpClientLib 20 | OneWire 21 | DallasTemperature 22 | Brzo I2C 23 | https://bitbucket.org/xoseperez/justwifi.git#1.1.3 24 | https://bitbucket.org/xoseperez/hlw8012.git#1.0.0 25 | https://bitbucket.org/xoseperez/fauxmoesp.git#2.1.0 26 | https://bitbucket.org/xoseperez/nofuss.git#0.2.2 27 | https://bitbucket.org/xoseperez/emonliteesp.git#0.1.2 28 | https://bitbucket.org/xoseperez/debounceevent.git#2.0.0 29 | https://github.com/xoseperez/my9291#1.0.0 30 | https://github.com/xoseperez/RemoteSwitch-arduino-library.git 31 | lib_ignore = 32 | 33 | # ------------------------------------------------------------------------------ 34 | 35 | [env:d1-debug] 36 | platform = espressif8266 37 | framework = arduino 38 | board = d1_mini 39 | lib_deps = ${common.lib_deps} 40 | lib_ignore = ${common.lib_ignore} 41 | extra_script = pio_hooks.py 42 | build_flags = ${common.build_flags} -DD1_RELAYSHIELD -DDEBUG_FAUXMO=Serial -DNOWSAUTH 43 | 44 | [env:d1-debug-ota] 45 | platform = espressif8266 46 | framework = arduino 47 | board = d1_mini 48 | lib_deps = ${common.lib_deps} 49 | lib_ignore = ${common.lib_ignore} 50 | extra_script = pio_hooks.py 51 | build_flags = ${common.build_flags} -DD1_RELAYSHIELD -DDEBUG_FAUXMO=Serial -DNOWSAUTH 52 | upload_speed = 115200 53 | upload_port = "192.168.4.1" 54 | upload_flags = --auth=fibonacci --port 8266 55 | 56 | [env:node-debug] 57 | platform = espressif8266 58 | framework = arduino 59 | board = nodemcuv2 60 | lib_deps = ${common.lib_deps} 61 | lib_ignore = ${common.lib_ignore} 62 | extra_script = pio_hooks.py 63 | build_flags = ${common.build_flags} -DNODEMCUV2 -DDEBUG_FAUXMO=Serial -DNOWSAUTH 64 | 65 | [env:node-debug-ota] 66 | platform = espressif8266 67 | framework = arduino 68 | board = nodemcuv2 69 | lib_deps = ${common.lib_deps} 70 | lib_ignore = ${common.lib_ignore} 71 | extra_script = pio_hooks.py 72 | build_flags = ${common.build_flags} -DNODEMCUV2 -DDEBUG_FAUXMO=Serial -DNOWSAUTH 73 | upload_speed = 115200 74 | upload_port = "192.168.4.1" 75 | upload_flags = --auth=fibonacci --port 8266 76 | 77 | [env:sonoff-debug] 78 | platform = espressif8266 79 | framework = arduino 80 | board = esp01_1m 81 | lib_deps = ${common.lib_deps} 82 | lib_ignore = ${common.lib_ignore} 83 | extra_script = pio_hooks.py 84 | build_flags = ${common.build_flags_1m128} -DSONOFF 85 | 86 | [env:sonoff-debug-ota] 87 | platform = espressif8266 88 | framework = arduino 89 | board = esp01_1m 90 | lib_deps = ${common.lib_deps} 91 | lib_ignore = ${common.lib_ignore} 92 | extra_script = pio_hooks.py 93 | build_flags = ${common.build_flags_1m128} -DSONOFF 94 | upload_speed = 115200 95 | upload_port = "192.168.4.1" 96 | upload_flags = --auth=fibonacci --port 8266 97 | 98 | [env:sonoff-dht22-debug] 99 | platform = espressif8266 100 | framework = arduino 101 | board = esp01_1m 102 | lib_deps = ${common.lib_deps} 103 | lib_ignore = ${common.lib_ignore} 104 | extra_script = pio_hooks.py 105 | build_flags = ${common.build_flags_1m128} -DSONOFF -DENABLE_DHT=1 106 | 107 | [env:sonoff-ds18b20-debug] 108 | platform = espressif8266 109 | framework = arduino 110 | board = esp01_1m 111 | lib_deps = ${common.lib_deps} 112 | lib_ignore = ${common.lib_ignore} 113 | extra_script = pio_hooks.py 114 | build_flags = ${common.build_flags_1m128} -DSONOFF -DENABLE_DS18B20=1 115 | 116 | [env:sonoff-pow-debug] 117 | platform = espressif8266 118 | framework = arduino 119 | board = esp01_1m 120 | lib_deps = ${common.lib_deps} 121 | lib_ignore = ${common.lib_ignore} 122 | extra_script = pio_hooks.py 123 | build_flags = ${common.build_flags_1m128} -DSONOFF_POW 124 | 125 | [env:sonoff-pow-debug-ota] 126 | platform = espressif8266 127 | framework = arduino 128 | board = esp01_1m 129 | lib_deps = ${common.lib_deps} 130 | lib_ignore = ${common.lib_ignore} 131 | extra_script = pio_hooks.py 132 | build_flags = ${common.build_flags_1m128} -DSONOFF_POW 133 | upload_speed = 115200 134 | upload_port = "192.168.4.1" 135 | upload_flags = --auth=fibonacci --port 8266 136 | 137 | [env:sonoff-dual-debug] 138 | platform = espressif8266 139 | framework = arduino 140 | board = esp01_1m 141 | lib_deps = ${common.lib_deps} 142 | lib_ignore = ${common.lib_ignore} 143 | extra_script = pio_hooks.py 144 | build_flags = ${common.build_flags_1m128} -DSONOFF_DUAL 145 | 146 | [env:sonoff-dual-debug-ota] 147 | platform = espressif8266 148 | framework = arduino 149 | board = esp01_1m 150 | lib_deps = ${common.lib_deps} 151 | lib_ignore = ${common.lib_ignore} 152 | extra_script = pio_hooks.py 153 | build_flags = ${common.build_flags_1m128} -DSONOFF_DUAL 154 | upload_speed = 115200 155 | upload_port = "192.168.4.1" 156 | upload_flags = --auth=fibonacci --port 8266 157 | 158 | [env:sonoff-4ch-debug] 159 | platform = espressif8266 160 | framework = arduino 161 | board = esp8285 162 | lib_deps = ${common.lib_deps} 163 | lib_ignore = ${common.lib_ignore} 164 | extra_script = pio_hooks.py 165 | build_flags = ${common.build_flags_1m128} -DSONOFF_4CH 166 | 167 | [env:sonoff-4ch-debug-ota] 168 | platform = espressif8266 169 | framework = arduino 170 | board = esp8285 171 | lib_deps = ${common.lib_deps} 172 | lib_ignore = ${common.lib_ignore} 173 | extra_script = pio_hooks.py 174 | build_flags = ${common.build_flags_1m128} -DSONOFF_4CH 175 | upload_speed = 115200 176 | upload_port = "192.168.4.1" 177 | upload_flags = --auth=fibonacci --port 8266 178 | 179 | [env:sonoff-touch-debug] 180 | platform = espressif8266 181 | framework = arduino 182 | board = esp8285 183 | lib_deps = ${common.lib_deps} 184 | lib_ignore = ${common.lib_ignore} 185 | extra_script = pio_hooks.py 186 | build_flags = ${common.build_flags_1m128} -DSONOFF_TOUCH 187 | 188 | [env:sonoff-touch-debug-ota] 189 | platform = espressif8266 190 | framework = arduino 191 | board = esp8285 192 | lib_deps = ${common.lib_deps} 193 | lib_ignore = ${common.lib_ignore} 194 | extra_script = pio_hooks.py 195 | build_flags = ${common.build_flags_1m128} -DSONOFF_TOUCH 196 | upload_speed = 115200 197 | upload_port = "192.168.4.1" 198 | upload_flags = --auth=fibonacci --port 8266 199 | 200 | [env:slampher-debug] 201 | platform = espressif8266 202 | framework = arduino 203 | board = esp01_1m 204 | lib_deps = ${common.lib_deps} 205 | lib_ignore = ${common.lib_ignore} 206 | extra_script = pio_hooks.py 207 | build_flags = ${common.build_flags_1m128} -DSLAMPHER 208 | 209 | [env:slampher-debug-ota] 210 | platform = espressif8266 211 | framework = arduino 212 | board = esp01_1m 213 | lib_deps = ${common.lib_deps} 214 | lib_ignore = ${common.lib_ignore} 215 | extra_script = pio_hooks.py 216 | build_flags = ${common.build_flags_1m128} -DSLAMPHER 217 | upload_speed = 115200 218 | upload_port = "192.168.4.1" 219 | upload_flags = --auth=fibonacci --port 8266 220 | 221 | [env:s20-debug] 222 | platform = espressif8266 223 | framework = arduino 224 | board = esp01_1m 225 | lib_deps = ${common.lib_deps} 226 | lib_ignore = ${common.lib_ignore} 227 | extra_script = pio_hooks.py 228 | build_flags = ${common.build_flags_1m128} -DS20 229 | 230 | [env:s20-debug-ota] 231 | platform = espressif8266 232 | framework = arduino 233 | board = esp01_1m 234 | lib_deps = ${common.lib_deps} 235 | lib_ignore = ${common.lib_ignore} 236 | extra_script = pio_hooks.py 237 | build_flags = ${common.build_flags_1m128} -DS20 238 | upload_speed = 115200 239 | upload_port = "192.168.4.1" 240 | upload_flags = --auth=fibonacci --port 8266 241 | 242 | [env:1ch-inching-debug] 243 | platform = espressif8266 244 | framework = arduino 245 | board = esp01_1m 246 | lib_deps = ${common.lib_deps} 247 | lib_ignore = ${common.lib_ignore} 248 | extra_script = pio_hooks.py 249 | build_flags = ${common.build_flags_1m128} -DITEAD_1CH_INCHING 250 | 251 | [env:1ch-inching-debug-ota] 252 | platform = espressif8266 253 | framework = arduino 254 | board = esp01_1m 255 | lib_deps = ${common.lib_deps} 256 | lib_ignore = ${common.lib_ignore} 257 | extra_script = pio_hooks.py 258 | build_flags = ${common.build_flags_1m128} -DITEAD_1CH_INCHING 259 | upload_speed = 115200 260 | upload_port = "192.168.4.1" 261 | upload_flags = --auth=fibonacci --port 8266 262 | 263 | [env:motor-debug] 264 | platform = espressif8266 265 | framework = arduino 266 | board = esp01_1m 267 | lib_deps = ${common.lib_deps} 268 | lib_ignore = ${common.lib_ignore} 269 | extra_script = pio_hooks.py 270 | build_flags = ${common.build_flags_1m128} -DITEAD_MOTOR 271 | 272 | [env:motor-debug-ota] 273 | platform = espressif8266 274 | framework = arduino 275 | board = esp01_1m 276 | lib_deps = ${common.lib_deps} 277 | lib_ignore = ${common.lib_ignore} 278 | extra_script = pio_hooks.py 279 | build_flags = ${common.build_flags_1m128} -DITEAD_MOTOR 280 | upload_speed = 115200 281 | upload_port = "192.168.4.1" 282 | upload_flags = --auth=fibonacci --port 8266 283 | 284 | [env:electrodragon-debug] 285 | platform = espressif8266 286 | framework = arduino 287 | board = esp12e 288 | lib_deps = ${common.lib_deps} 289 | lib_ignore = ${common.lib_ignore} 290 | extra_script = pio_hooks.py 291 | build_flags = ${common.build_flags} -DESP_RELAY_BOARD -DENABLE_DHT=1 292 | 293 | [env:electrodragon-debug-ota] 294 | platform = espressif8266 295 | framework = arduino 296 | board = esp12e 297 | lib_deps = ${common.lib_deps} 298 | lib_ignore = ${common.lib_ignore} 299 | extra_script = pio_hooks.py 300 | build_flags = ${common.build_flags} -DESP_RELAY_BOARD -DENABLE_DHT=1 301 | upload_speed = 115200 302 | upload_port = "192.168.4.1" 303 | upload_flags = --auth=fibonacci --port 8266 304 | 305 | [env:ecoplug-debug] 306 | platform = espressif8266 307 | framework = arduino 308 | board = esp01_1m 309 | lib_deps = ${common.lib_deps} 310 | lib_ignore = ${common.lib_ignore} 311 | extra_script = pio_hooks.py 312 | build_flags = ${common.build_flags_1m128} -DECOPLUG 313 | 314 | [env:ecoplug-debug-ota] 315 | platform = espressif8266 316 | framework = arduino 317 | board = esp01_1m 318 | lib_deps = ${common.lib_deps} 319 | lib_ignore = ${common.lib_ignore} 320 | extra_script = pio_hooks.py 321 | build_flags = ${common.build_flags_1m128} -DECOPLUG 322 | upload_speed = 115200 323 | upload_port = "192.168.4.1" 324 | upload_flags = --auth=fibonacci --port 8266 325 | 326 | [env:jangoe-debug] 327 | platform = espressif8266 328 | framework = arduino 329 | board = esp12e 330 | lib_deps = ${common.lib_deps} 331 | lib_ignore = ${common.lib_ignore} 332 | extra_script = pio_hooks.py 333 | build_flags = ${common.build_flags} -DWIFI_RELAY_NC 334 | 335 | [env:jangoe-debug-ota] 336 | platform = espressif8266 337 | framework = arduino 338 | board = esp12e 339 | lib_deps = ${common.lib_deps} 340 | lib_ignore = ${common.lib_ignore} 341 | extra_script = pio_hooks.py 342 | build_flags = ${common.build_flags} -DWIFI_RELAY_NC 343 | upload_speed = 115200 344 | upload_port = "192.168.4.1" 345 | upload_flags = --auth=fibonacci --port 8266 346 | 347 | [env:mqtt-relay-debug] 348 | platform = espressif8266 349 | framework = arduino 350 | board = esp_wroom_02 351 | lib_deps = ${common.lib_deps} 352 | lib_ignore = ${common.lib_ignore} 353 | extra_script = pio_hooks.py 354 | build_flags = ${common.build_flags} -DMQTT_RELAY -DENABLE_DS18B20=1 355 | 356 | [env:mqtt-relay-debug-ota] 357 | platform = espressif8266 358 | framework = arduino 359 | board = esp_wroom_02 360 | lib_deps = ${common.lib_deps} 361 | lib_ignore = ${common.lib_ignore} 362 | extra_script = pio_hooks.py 363 | build_flags = ${common.build_flags} -DMQTT_RELAY -DENABLE_DS18B20=1 364 | upload_speed = 115200 365 | upload_port = "192.168.4.1" 366 | upload_flags = --auth=fibonacci --port 8266 367 | 368 | [env:wifi-relays-debug] 369 | platform = espressif8266 370 | framework = arduino 371 | board = esp01_1m 372 | lib_deps = ${common.lib_deps} 373 | lib_ignore = ${common.lib_ignore} 374 | extra_script = pio_hooks.py 375 | build_flags = ${common.build_flags_1m128} -DWIFI_RELAYS_BOARD_KIT 376 | 377 | [env:wifi-relays-debug-ota] 378 | platform = espressif8266 379 | framework = arduino 380 | board = esp01_1m 381 | lib_deps = ${common.lib_deps} 382 | lib_ignore = ${common.lib_ignore} 383 | extra_script = pio_hooks.py 384 | build_flags = ${common.build_flags_1m128} -DWIFI_RELAYS_BOARD_KIT 385 | upload_speed = 115200 386 | upload_port = "192.168.4.1" 387 | upload_flags = --auth=fibonacci --port 8266 388 | 389 | [env:ai-light-debug] 390 | platform = espressif8266 391 | framework = arduino 392 | board = esp8285 393 | lib_deps = ${common.lib_deps} 394 | lib_ignore = ${common.lib_ignore} 395 | extra_script = pio_hooks.py 396 | build_flags = ${common.build_flags_1m128} -DAI_LIGHT 397 | 398 | [env:ai-light-debug-ota] 399 | platform = espressif8266 400 | framework = arduino 401 | board = esp8285 402 | lib_deps = ${common.lib_deps} 403 | lib_ignore = ${common.lib_ignore} 404 | extra_script = pio_hooks.py 405 | build_flags = ${common.build_flags_1m128} -DAI_LIGHT 406 | upload_speed = 115200 407 | upload_port = "192.168.4.1" 408 | upload_flags = --auth=fibonacci --port 8266 409 | 410 | [env:led-controller-debug] 411 | platform = espressif8266 412 | framework = arduino 413 | board = esp12e 414 | lib_deps = ${common.lib_deps} 415 | lib_ignore = ${common.lib_ignore} 416 | extra_script = pio_hooks.py 417 | build_flags = ${common.build_flags} -DLED_CONTROLLER 418 | 419 | [env:led-controller-debug-ota] 420 | platform = espressif8266 421 | framework = arduino 422 | board = esp12e 423 | lib_deps = ${common.lib_deps} 424 | lib_ignore = ${common.lib_ignore} 425 | extra_script = pio_hooks.py 426 | build_flags = ${common.build_flags} -DLED_CONTROLLER 427 | upload_speed = 115200 428 | upload_port = "192.168.4.1" 429 | upload_flags = --auth=fibonacci --port 8266 430 | -------------------------------------------------------------------------------- /images/devices/1ch-inching.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SensorsIot/Espurna-Framework/ed8c754fd86a704670ef38187313f0dab3e1ecba/images/devices/1ch-inching.jpg -------------------------------------------------------------------------------- /images/devices/aithinker-ailight.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SensorsIot/Espurna-Framework/ed8c754fd86a704670ef38187313f0dab3e1ecba/images/devices/aithinker-ailight.jpg -------------------------------------------------------------------------------- /images/devices/d1mini.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SensorsIot/Espurna-Framework/ed8c754fd86a704670ef38187313f0dab3e1ecba/images/devices/d1mini.jpg -------------------------------------------------------------------------------- /images/devices/electrodragon-relay-board.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SensorsIot/Espurna-Framework/ed8c754fd86a704670ef38187313f0dab3e1ecba/images/devices/electrodragon-relay-board.jpg -------------------------------------------------------------------------------- /images/devices/jangoe-wifi-relay.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SensorsIot/Espurna-Framework/ed8c754fd86a704670ef38187313f0dab3e1ecba/images/devices/jangoe-wifi-relay.png -------------------------------------------------------------------------------- /images/devices/jorgegarcia-wifi-relays-board-kit.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SensorsIot/Espurna-Framework/ed8c754fd86a704670ef38187313f0dab3e1ecba/images/devices/jorgegarcia-wifi-relays-board-kit.jpg -------------------------------------------------------------------------------- /images/devices/motor-switch.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SensorsIot/Espurna-Framework/ed8c754fd86a704670ef38187313f0dab3e1ecba/images/devices/motor-switch.jpg -------------------------------------------------------------------------------- /images/devices/mqtt-relay.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SensorsIot/Espurna-Framework/ed8c754fd86a704670ef38187313f0dab3e1ecba/images/devices/mqtt-relay.jpg -------------------------------------------------------------------------------- /images/devices/s20.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SensorsIot/Espurna-Framework/ed8c754fd86a704670ef38187313f0dab3e1ecba/images/devices/s20.jpg -------------------------------------------------------------------------------- /images/devices/slampher.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SensorsIot/Espurna-Framework/ed8c754fd86a704670ef38187313f0dab3e1ecba/images/devices/slampher.jpg -------------------------------------------------------------------------------- /images/devices/sonoff-4ch.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SensorsIot/Espurna-Framework/ed8c754fd86a704670ef38187313f0dab3e1ecba/images/devices/sonoff-4ch.jpg -------------------------------------------------------------------------------- /images/devices/sonoff-basic.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SensorsIot/Espurna-Framework/ed8c754fd86a704670ef38187313f0dab3e1ecba/images/devices/sonoff-basic.jpg -------------------------------------------------------------------------------- /images/devices/sonoff-dual.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SensorsIot/Espurna-Framework/ed8c754fd86a704670ef38187313f0dab3e1ecba/images/devices/sonoff-dual.jpg -------------------------------------------------------------------------------- /images/devices/sonoff-led.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SensorsIot/Espurna-Framework/ed8c754fd86a704670ef38187313f0dab3e1ecba/images/devices/sonoff-led.jpg -------------------------------------------------------------------------------- /images/devices/sonoff-pow.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SensorsIot/Espurna-Framework/ed8c754fd86a704670ef38187313f0dab3e1ecba/images/devices/sonoff-pow.jpg -------------------------------------------------------------------------------- /images/devices/sonoff-rf.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SensorsIot/Espurna-Framework/ed8c754fd86a704670ef38187313f0dab3e1ecba/images/devices/sonoff-rf.jpg -------------------------------------------------------------------------------- /images/devices/sonoff-sv.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SensorsIot/Espurna-Framework/ed8c754fd86a704670ef38187313f0dab3e1ecba/images/devices/sonoff-sv.jpg -------------------------------------------------------------------------------- /images/devices/sonoff-th10-th16.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SensorsIot/Espurna-Framework/ed8c754fd86a704670ef38187313f0dab3e1ecba/images/devices/sonoff-th10-th16.jpg -------------------------------------------------------------------------------- /images/devices/sonoff-touch.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SensorsIot/Espurna-Framework/ed8c754fd86a704670ef38187313f0dab3e1ecba/images/devices/sonoff-touch.jpg -------------------------------------------------------------------------------- /images/devices/workchoice-ecoplug.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SensorsIot/Espurna-Framework/ed8c754fd86a704670ef38187313f0dab3e1ecba/images/devices/workchoice-ecoplug.jpg --------------------------------------------------------------------------------