├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── build-helper.py ├── data ├── css │ ├── app.css │ └── foundation.min.css ├── editor.html ├── icons │ ├── android-144x144.png │ ├── android-192x192.png │ ├── android-36x36.png │ ├── android-48x48.png │ ├── android-72x72.png │ ├── android-96x96.png │ ├── apple-114x114.png │ ├── apple-120x120.png │ ├── apple-144x144.png │ ├── apple-152x152.png │ ├── apple-180x180.png │ ├── apple-57x57.png │ ├── apple-60x60.png │ ├── apple-72x72.png │ ├── apple-76x76.png │ ├── apple-precomposed.png │ ├── apple-touch-icon.png │ ├── favicon-16x16.png │ ├── favicon-32x32.png │ ├── favicon-96x96.png │ └── favicon.ico ├── img │ ├── loading.gif │ └── logo_white.png ├── index.html └── js │ ├── app.js │ ├── jquery-2.1.4.min.js │ ├── nprogress.js │ └── sparkline.js ├── gulpfile.js ├── misc └── common.sh ├── package.json ├── platformio.ini └── src ├── config.cpp ├── config.h ├── plugins ├── AnalogPlugin.cpp ├── AnalogPlugin.h ├── DHTPlugin.cpp ├── DHTPlugin.h ├── OneWirePlugin.cpp ├── OneWirePlugin.h ├── Plugin.cpp ├── Plugin.h ├── S0Plugin.cpp ├── S0Plugin.h ├── WifiPlugin.cpp └── WifiPlugin.h ├── urlfunctions.cpp ├── urlfunctions.h ├── vzero.ino ├── webserver.cpp └── webserver.h /.gitignore: -------------------------------------------------------------------------------- 1 | .pioenvs 2 | .clang_complete 3 | .gcc-flags.json 4 | .gcc-flags.json.piolibdeps.piolibdeps.piolibdeps.piolibdeps.piolibdeps.piolibdeps.piolibdeps.piolibdeps.piolibdeps 5 | .piolibdeps 6 | .vscode/c_cpp_properties.json 7 | .vscode/.browse.c_cpp.db* 8 | .vscode/launch.json 9 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: python 3 | python: 4 | - "2.7" 5 | 6 | cache: 7 | directories: 8 | - "~/.platformio" 9 | 10 | env: 11 | matrix: 12 | - PLATFORMIO=1 13 | - ARDUINO=1 14 | 15 | before_install: 16 | # arduino prereqs 17 | - /sbin/start-stop-daemon --start --quiet --pidfile /tmp/custom_xvfb_99.pid --make-pidfile --background --exec /usr/bin/Xvfb -- :99 -ac -screen 0 1280x1024x16 18 | - export DISPLAY=:99.0 19 | - sleep 3 # give xvfb some time to start 20 | 21 | install: 22 | # install arduino ide 23 | - | 24 | if [ "$ARDUINO" ]; then 25 | wget http://downloads.arduino.cc/arduino-1.6.9-linux64.tar.xz 26 | tar xf arduino-1.6.9-linux64.tar.xz 27 | mv arduino-1.6.9 $HOME/arduino_ide 28 | export PATH="$HOME/arduino_ide:$PATH" 29 | which arduino 30 | fi 31 | 32 | # install arduino core 33 | - | 34 | if [ "$ARDUINO" ]; then 35 | cd $HOME/arduino_ide/hardware 36 | mkdir esp8266com 37 | cd esp8266com 38 | git clone https://github.com/esp8266/Arduino.git esp8266 39 | cd esp8266/tools 40 | python get.py 41 | fi 42 | 43 | # install platformio 44 | # - if [ "$PLATFORMIO" ]; then pip install -U platformio; fi 45 | # use pio 3.5 dev version 46 | - if [ "$PLATFORMIO" ]; then pip install -U https://github.com/platformio/platformio-core/archive/develop.zip; fi 47 | 48 | # install arduino libraries 49 | - | 50 | if [ "$ARDUINO" ]; then 51 | mkdir -p $HOME/Arduino/libraries 52 | git clone https://github.com/bblanchon/ArduinoJson $HOME/Arduino/libraries/ArduinoJson 53 | git clone https://github.com/adafruit/DHT-sensor-library.git $HOME/Arduino/libraries/DHT 54 | git clone https://github.com/PaulStoffregen/OneWire.git $HOME/Arduino/libraries/OneWire 55 | git clone https://github.com/milesburton/Arduino-Temperature-Control-Library $HOME/Arduino/libraries/DallasTemperature 56 | git clone https://github.com/me-no-dev/ESPAsyncTCP $HOME/Arduino/libraries/ESPAsyncTCP 57 | git clone https://github.com/me-no-dev/ESPAsyncWebServer $HOME/Arduino/libraries/ESPAsyncWebServer 58 | fi 59 | 60 | script: 61 | # arduino build 62 | - | 63 | if [ "$ARDUINO" ]; then 64 | cd $TRAVIS_BUILD_DIR 65 | source misc/common.sh 66 | ls -l 67 | 68 | arduino --board esp8266com:esp8266:generic --save-prefs 69 | arduino --get-pref sketchbook.path 70 | build_sketch $PWD/src/vzero.ino esp8266 71 | fi 72 | 73 | # platformio build 74 | - if [ "$PLATFORMIO" ]; then platformio ci --project-conf ./platformio.ini .; fi 75 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 2, June 1991 3 | 4 | Copyright (C) 1989, 1991 Free Software Foundation, Inc., 5 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 6 | Everyone is permitted to copy and distribute verbatim copies 7 | of this license document, but changing it is not allowed. 8 | 9 | Preamble 10 | 11 | The licenses for most software are designed to take away your 12 | freedom to share and change it. By contrast, the GNU General Public 13 | License is intended to guarantee your freedom to share and change free 14 | software--to make sure the software is free for all its users. This 15 | General Public License applies to most of the Free Software 16 | Foundation's software and to any other program whose authors commit to 17 | using it. (Some other Free Software Foundation software is covered by 18 | the GNU Lesser General Public License instead.) You can apply it to 19 | your programs, too. 20 | 21 | When we speak of free software, we are referring to freedom, not 22 | price. Our General Public Licenses are designed to make sure that you 23 | have the freedom to distribute copies of free software (and charge for 24 | this service if you wish), that you receive source code or can get it 25 | if you want it, that you can change the software or use pieces of it 26 | in new free programs; and that you know you can do these things. 27 | 28 | To protect your rights, we need to make restrictions that forbid 29 | anyone to deny you these rights or to ask you to surrender the rights. 30 | These restrictions translate to certain responsibilities for you if you 31 | distribute copies of the software, or if you modify it. 32 | 33 | For example, if you distribute copies of such a program, whether 34 | gratis or for a fee, you must give the recipients all the rights that 35 | you have. You must make sure that they, too, receive or can get the 36 | source code. And you must show them these terms so they know their 37 | rights. 38 | 39 | We protect your rights with two steps: (1) copyright the software, and 40 | (2) offer you this license which gives you legal permission to copy, 41 | distribute and/or modify the software. 42 | 43 | Also, for each author's protection and ours, we want to make certain 44 | that everyone understands that there is no warranty for this free 45 | software. If the software is modified by someone else and passed on, we 46 | want its recipients to know that what they have is not the original, so 47 | that any problems introduced by others will not reflect on the original 48 | authors' reputations. 49 | 50 | Finally, any free program is threatened constantly by software 51 | patents. We wish to avoid the danger that redistributors of a free 52 | program will individually obtain patent licenses, in effect making the 53 | program proprietary. To prevent this, we have made it clear that any 54 | patent must be licensed for everyone's free use or not licensed at all. 55 | 56 | The precise terms and conditions for copying, distribution and 57 | modification follow. 58 | 59 | GNU GENERAL PUBLIC LICENSE 60 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 61 | 62 | 0. This License applies to any program or other work which contains 63 | a notice placed by the copyright holder saying it may be distributed 64 | under the terms of this General Public License. The "Program", below, 65 | refers to any such program or work, and a "work based on the Program" 66 | means either the Program or any derivative work under copyright law: 67 | that is to say, a work containing the Program or a portion of it, 68 | either verbatim or with modifications and/or translated into another 69 | language. (Hereinafter, translation is included without limitation in 70 | the term "modification".) Each licensee is addressed as "you". 71 | 72 | Activities other than copying, distribution and modification are not 73 | covered by this License; they are outside its scope. The act of 74 | running the Program is not restricted, and the output from the Program 75 | is covered only if its contents constitute a work based on the 76 | Program (independent of having been made by running the Program). 77 | Whether that is true depends on what the Program does. 78 | 79 | 1. You may copy and distribute verbatim copies of the Program's 80 | source code as you receive it, in any medium, provided that you 81 | conspicuously and appropriately publish on each copy an appropriate 82 | copyright notice and disclaimer of warranty; keep intact all the 83 | notices that refer to this License and to the absence of any warranty; 84 | and give any other recipients of the Program a copy of this License 85 | along with the Program. 86 | 87 | You may charge a fee for the physical act of transferring a copy, and 88 | you may at your option offer warranty protection in exchange for a fee. 89 | 90 | 2. You may modify your copy or copies of the Program or any portion 91 | of it, thus forming a work based on the Program, and copy and 92 | distribute such modifications or work under the terms of Section 1 93 | above, provided that you also meet all of these conditions: 94 | 95 | a) You must cause the modified files to carry prominent notices 96 | stating that you changed the files and the date of any change. 97 | 98 | b) You must cause any work that you distribute or publish, that in 99 | whole or in part contains or is derived from the Program or any 100 | part thereof, to be licensed as a whole at no charge to all third 101 | parties under the terms of this License. 102 | 103 | c) If the modified program normally reads commands interactively 104 | when run, you must cause it, when started running for such 105 | interactive use in the most ordinary way, to print or display an 106 | announcement including an appropriate copyright notice and a 107 | notice that there is no warranty (or else, saying that you provide 108 | a warranty) and that users may redistribute the program under 109 | these conditions, and telling the user how to view a copy of this 110 | License. (Exception: if the Program itself is interactive but 111 | does not normally print such an announcement, your work based on 112 | the Program is not required to print an announcement.) 113 | 114 | These requirements apply to the modified work as a whole. If 115 | identifiable sections of that work are not derived from the Program, 116 | and can be reasonably considered independent and separate works in 117 | themselves, then this License, and its terms, do not apply to those 118 | sections when you distribute them as separate works. But when you 119 | distribute the same sections as part of a whole which is a work based 120 | on the Program, the distribution of the whole must be on the terms of 121 | this License, whose permissions for other licensees extend to the 122 | entire whole, and thus to each and every part regardless of who wrote it. 123 | 124 | Thus, it is not the intent of this section to claim rights or contest 125 | your rights to work written entirely by you; rather, the intent is to 126 | exercise the right to control the distribution of derivative or 127 | collective works based on the Program. 128 | 129 | In addition, mere aggregation of another work not based on the Program 130 | with the Program (or with a work based on the Program) on a volume of 131 | a storage or distribution medium does not bring the other work under 132 | the scope of this License. 133 | 134 | 3. You may copy and distribute the Program (or a work based on it, 135 | under Section 2) in object code or executable form under the terms of 136 | Sections 1 and 2 above provided that you also do one of the following: 137 | 138 | a) Accompany it with the complete corresponding machine-readable 139 | source code, which must be distributed under the terms of Sections 140 | 1 and 2 above on a medium customarily used for software interchange; or, 141 | 142 | b) Accompany it with a written offer, valid for at least three 143 | years, to give any third party, for a charge no more than your 144 | cost of physically performing source distribution, a complete 145 | machine-readable copy of the corresponding source code, to be 146 | distributed under the terms of Sections 1 and 2 above on a medium 147 | customarily used for software interchange; or, 148 | 149 | c) Accompany it with the information you received as to the offer 150 | to distribute corresponding source code. (This alternative is 151 | allowed only for noncommercial distribution and only if you 152 | received the program in object code or executable form with such 153 | an offer, in accord with Subsection b above.) 154 | 155 | The source code for a work means the preferred form of the work for 156 | making modifications to it. For an executable work, complete source 157 | code means all the source code for all modules it contains, plus any 158 | associated interface definition files, plus the scripts used to 159 | control compilation and installation of the executable. However, as a 160 | special exception, the source code distributed need not include 161 | anything that is normally distributed (in either source or binary 162 | form) with the major components (compiler, kernel, and so on) of the 163 | operating system on which the executable runs, unless that component 164 | itself accompanies the executable. 165 | 166 | If distribution of executable or object code is made by offering 167 | access to copy from a designated place, then offering equivalent 168 | access to copy the source code from the same place counts as 169 | distribution of the source code, even though third parties are not 170 | compelled to copy the source along with the object code. 171 | 172 | 4. You may not copy, modify, sublicense, or distribute the Program 173 | except as expressly provided under this License. Any attempt 174 | otherwise to copy, modify, sublicense or distribute the Program is 175 | void, and will automatically terminate your rights under this License. 176 | However, parties who have received copies, or rights, from you under 177 | this License will not have their licenses terminated so long as such 178 | parties remain in full compliance. 179 | 180 | 5. You are not required to accept this License, since you have not 181 | signed it. However, nothing else grants you permission to modify or 182 | distribute the Program or its derivative works. These actions are 183 | prohibited by law if you do not accept this License. Therefore, by 184 | modifying or distributing the Program (or any work based on the 185 | Program), you indicate your acceptance of this License to do so, and 186 | all its terms and conditions for copying, distributing or modifying 187 | the Program or works based on it. 188 | 189 | 6. Each time you redistribute the Program (or any work based on the 190 | Program), the recipient automatically receives a license from the 191 | original licensor to copy, distribute or modify the Program subject to 192 | these terms and conditions. You may not impose any further 193 | restrictions on the recipients' exercise of the rights granted herein. 194 | You are not responsible for enforcing compliance by third parties to 195 | this License. 196 | 197 | 7. If, as a consequence of a court judgment or allegation of patent 198 | infringement or for any other reason (not limited to patent issues), 199 | conditions are imposed on you (whether by court order, agreement or 200 | otherwise) that contradict the conditions of this License, they do not 201 | excuse you from the conditions of this License. If you cannot 202 | distribute so as to satisfy simultaneously your obligations under this 203 | License and any other pertinent obligations, then as a consequence you 204 | may not distribute the Program at all. For example, if a patent 205 | license would not permit royalty-free redistribution of the Program by 206 | all those who receive copies directly or indirectly through you, then 207 | the only way you could satisfy both it and this License would be to 208 | refrain entirely from distribution of the Program. 209 | 210 | If any portion of this section is held invalid or unenforceable under 211 | any particular circumstance, the balance of the section is intended to 212 | apply and the section as a whole is intended to apply in other 213 | circumstances. 214 | 215 | It is not the purpose of this section to induce you to infringe any 216 | patents or other property right claims or to contest validity of any 217 | such claims; this section has the sole purpose of protecting the 218 | integrity of the free software distribution system, which is 219 | implemented by public license practices. Many people have made 220 | generous contributions to the wide range of software distributed 221 | through that system in reliance on consistent application of that 222 | system; it is up to the author/donor to decide if he or she is willing 223 | to distribute software through any other system and a licensee cannot 224 | impose that choice. 225 | 226 | This section is intended to make thoroughly clear what is believed to 227 | be a consequence of the rest of this License. 228 | 229 | 8. If the distribution and/or use of the Program is restricted in 230 | certain countries either by patents or by copyrighted interfaces, the 231 | original copyright holder who places the Program under this License 232 | may add an explicit geographical distribution limitation excluding 233 | those countries, so that distribution is permitted only in or among 234 | countries not thus excluded. In such case, this License incorporates 235 | the limitation as if written in the body of this License. 236 | 237 | 9. The Free Software Foundation may publish revised and/or new versions 238 | of the General Public License from time to time. Such new versions will 239 | be similar in spirit to the present version, but may differ in detail to 240 | address new problems or concerns. 241 | 242 | Each version is given a distinguishing version number. If the Program 243 | specifies a version number of this License which applies to it and "any 244 | later version", you have the option of following the terms and conditions 245 | either of that version or of any later version published by the Free 246 | Software Foundation. If the Program does not specify a version number of 247 | this License, you may choose any version ever published by the Free Software 248 | Foundation. 249 | 250 | 10. If you wish to incorporate parts of the Program into other free 251 | programs whose distribution conditions are different, write to the author 252 | to ask for permission. For software which is copyrighted by the Free 253 | Software Foundation, write to the Free Software Foundation; we sometimes 254 | make exceptions for this. Our decision will be guided by the two goals 255 | of preserving the free status of all derivatives of our free software and 256 | of promoting the sharing and reuse of software generally. 257 | 258 | NO WARRANTY 259 | 260 | 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY 261 | FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN 262 | OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES 263 | PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED 264 | OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 265 | MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS 266 | TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE 267 | PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, 268 | REPAIR OR CORRECTION. 269 | 270 | 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 271 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR 272 | REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, 273 | INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING 274 | OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED 275 | TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY 276 | YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER 277 | PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE 278 | POSSIBILITY OF SUCH DAMAGES. 279 | 280 | END OF TERMS AND CONDITIONS 281 | 282 | How to Apply These Terms to Your New Programs 283 | 284 | If you develop a new program, and you want it to be of the greatest 285 | possible use to the public, the best way to achieve this is to make it 286 | free software which everyone can redistribute and change under these terms. 287 | 288 | To do so, attach the following notices to the program. It is safest 289 | to attach them to the start of each source file to most effectively 290 | convey the exclusion of warranty; and each file should have at least 291 | the "copyright" line and a pointer to where the full notice is found. 292 | 293 | {description} 294 | Copyright (C) {year} {fullname} 295 | 296 | This program is free software; you can redistribute it and/or modify 297 | it under the terms of the GNU General Public License as published by 298 | the Free Software Foundation; either version 2 of the License, or 299 | (at your option) any later version. 300 | 301 | This program is distributed in the hope that it will be useful, 302 | but WITHOUT ANY WARRANTY; without even the implied warranty of 303 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 304 | GNU General Public License for more details. 305 | 306 | You should have received a copy of the GNU General Public License along 307 | with this program; if not, write to the Free Software Foundation, Inc., 308 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 309 | 310 | Also add information on how to contact you by electronic and paper mail. 311 | 312 | If the program is interactive, make it output a short notice like this 313 | when it starts in an interactive mode: 314 | 315 | Gnomovision version 69, Copyright (C) year name of author 316 | Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 317 | This is free software, and you are welcome to redistribute it 318 | under certain conditions; type `show c' for details. 319 | 320 | The hypothetical commands `show w' and `show c' should show the appropriate 321 | parts of the General Public License. Of course, the commands you use may 322 | be called something other than `show w' and `show c'; they could even be 323 | mouse-clicks or menu items--whatever suits your program. 324 | 325 | You should also get your employer (if you work as a programmer) or your 326 | school, if any, to sign a "copyright disclaimer" for the program, if 327 | necessary. Here is a sample; alter the names: 328 | 329 | Yoyodyne, Inc., hereby disclaims all copyright interest in the program 330 | `Gnomovision' (which makes passes at compilers) written by James Hacker. 331 | 332 | {signature of Ty Coon}, 1 April 1989 333 | Ty Coon, President of Vice 334 | 335 | This General Public License does not permit incorporating your program into 336 | proprietary programs. If your program is a subroutine library, you may 337 | consider it more useful to permit linking proprietary applications with the 338 | library. If this is what you want to do, use the GNU Lesser General 339 | Public License instead of this License. 340 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # vzero 2 | VZero - the Wireless zero-config controller for volkszaehler.org 3 | 4 | [![Build Status](https://travis-ci.org/andig/vzero.svg?branch=master)](https://travis-ci.org/andig/vzero) 5 | 6 | ## Plugins 7 | 8 | VZero has an extensible plugin framework. Out of the box the following sensor plugins are supported: 9 | 10 | - analog reading (e.g. battery voltage) 11 | - DHT (temperature and humidity) 12 | - 1wire (temperature) 13 | - wifi (signal strength) 14 | 15 | Already planned is support for IO events (S0). 16 | 17 | ## API description 18 | 19 | The VZero frontend uses a json API to communicate with the Arduino backend. 20 | 21 | ### Actions 22 | 23 | - `/api/wlan` set WiFi configuration (`GET`) 24 | - `/api/restart` restart (`POST`) 25 | - `/api/settings` save settings (restarts) (`POST`) 26 | 27 | ### Other services 28 | 29 | - `/api/scan` WiFi scan (`GET`) 30 | - `/api/status` system health (`GET`) 31 | - `/api/plugin` overview of plugins and sensors (`GET`) 32 | - `/api//` individual sensors (`GET`) 33 | 34 | ## Screenshots 35 | 36 | ### Welcome Screen 37 | 38 | After initial startup, the welcome screen allows to customize WiFi credentials: 39 | ![Welcome Screen](/../gh-pages/img/1.png?raw=true) 40 | 41 | In addition, the Volkszaehler middleware can be configured- either connecting to http://demo.volkszaehler.org which is the default or any other middleware. 42 | 43 | ### Home Screen 44 | 45 | The home screen presents an overview of the configured sensors: 46 | ![Home Screen](/../gh-pages/img/2.png?raw=true) 47 | 48 | ### Sensors Screen 49 | 50 | The sensors screen is used to connect available sensors to the middleware: 51 | ![Sensors Screen](/../gh-pages/img/3.png?raw=true) 52 | 53 | ### Status Screen 54 | 55 | The status screen shows health information of the VZero and allows to restart the device: 56 | ![Sensors Screen](/../gh-pages/img/4.png?raw=true) 57 | 58 | -------------------------------------------------------------------------------- /build-helper.py: -------------------------------------------------------------------------------- 1 | Import("projenv") 2 | # set CPP compiler options only 3 | projenv.Append(CXXFLAGS=["-Wno-reorder"]) 4 | -------------------------------------------------------------------------------- /data/css/app.css: -------------------------------------------------------------------------------- 1 | html, body, [type="text"] { 2 | color: #666; 3 | } 4 | header { 5 | width: 100%; 6 | padding: 1.5rem 0 1rem 0; 7 | /*margin-bottom: 10px;*/ 8 | color: #FFF; 9 | background-color: #0292C0; 10 | border-bottom: 1px solid #047A96; 11 | } 12 | header p { 13 | margin: 5px 0 0 0; 14 | } 15 | .label { 16 | margin-right: 10px; 17 | } 18 | .template { 19 | display: none; 20 | } 21 | .menu-container { 22 | padding: 5px 0 5px 0; 23 | margin-bottom: 1rem; 24 | border-bottom: 1px solid #CACACA; 25 | } 26 | .footer-container { 27 | position: fixed; 28 | bottom: 0px; 29 | width: 100%; 30 | height: auto; 31 | max-height: 9rem; 32 | padding: 0.5rem 0 0.5rem 0; 33 | vertical-align: bottom; 34 | border-top: 1px solid #CACACA; 35 | background-color: #fff; 36 | } 37 | .em { 38 | font-weight: bold; 39 | } 40 | div.state-menu { 41 | padding-left: 0 42 | } 43 | .loader { 44 | position: absolute; top: 50%; text-align: center; width: 100%; 45 | } 46 | .content { 47 | display: none; 48 | } 49 | .info .message { 50 | font-family: consolas; 51 | } -------------------------------------------------------------------------------- /data/css/foundation.min.css: -------------------------------------------------------------------------------- 1 | @charset "UTF-8";/*! normalize.css v3.0.3 | MIT License | github.com/necolas/normalize.css */button,img,legend{border:0}body,button,legend{padding:0}.button.dropdown::after,.small-pull-1,.small-pull-10,.small-pull-11,.small-pull-2,.small-pull-3,.small-pull-4,.small-pull-5,.small-pull-6,.small-pull-7,.small-pull-8,.small-pull-9,.small-push-1,.small-push-10,.small-push-11,.small-push-2,.small-push-3,.small-push-4,.small-push-5,.small-push-7,.small-push-8,.small-push-9,sub,sup{position:relative}.dropdown-pane,.invisible{visibility:hidden}html{font-family:sans-serif;-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%}article,aside,details,figcaption,figure,footer,header,hgroup,main,menu,nav,section,summary{display:block}audio,canvas,progress,video{display:inline-block;vertical-align:baseline}audio:not([controls]){display:none;height:0}[hidden],template{display:none}a:active,a:hover{outline:0}abbr[title]{border-bottom:1px dotted}b,optgroup,strong{font-weight:700}dfn{font-style:italic}mark{background:#ff0;color:#000}small{font-size:80%}sub,sup{font-size:75%;line-height:0;vertical-align:baseline}.button,img{vertical-align:middle}sup{top:-.5em}sub{bottom:-.25em}img{max-width:100%;height:auto;-ms-interpolation-mode:bicubic;display:inline-block}svg:not(:root){overflow:hidden}figure{margin:1em 40px}pre,textarea{overflow:auto}code,kbd,pre,samp{font-family:monospace,monospace;font-size:1em}button,input,optgroup,select,textarea{color:inherit;font:inherit;margin:0}a,b,em,i,small,strong{line-height:inherit}dl,ol,p,ul{line-height:1.6}button,select{text-transform:none}button,html input[type=button],input[type=reset],input[type=submit]{-webkit-appearance:button;cursor:pointer}button[disabled],html input[disabled]{cursor:default}button::-moz-focus-inner,input::-moz-focus-inner{border:0;padding:0}input{line-height:normal}input[type=checkbox],input[type=radio]{box-sizing:border-box;padding:0}input[type=number]::-webkit-inner-spin-button,input[type=number]::-webkit-outer-spin-button{height:auto}input[type=search]{-webkit-appearance:textfield;box-sizing:content-box}input[type=search]::-webkit-search-cancel-button,input[type=search]::-webkit-search-decoration{-webkit-appearance:none}.foundation-mq{font-family:"small=0em&medium=40em&large=64em&xlarge=75em&xxlarge=90em"}body,h1,h2,h3,h4,h5,h6{font-family:"Helvetica Neue",Helvetica,Roboto,Arial,sans-serif;font-weight:400;color:#222}html{font-size:100%;box-sizing:border-box}*,:after,:before{box-sizing:inherit}body{margin:0;line-height:1.5;background:#fefefe;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}select{width:100%}#map_canvas embed,#map_canvas img,#map_canvas object,.map_canvas embed,.map_canvas img,.map_canvas object,.mqa-display embed,.mqa-display img,.mqa-display object{max-width:none!important}button{overflow:visible;-webkit-appearance:none;-moz-appearance:none;background:0 0;border-radius:3px;line-height:1}.row{max-width:62.5rem;margin-left:auto;margin-right:auto}.row::after,.row::before{content:' ';display:table}.row::after{clear:both}.row.collapse>.column,.row.collapse>.columns{padding-left:0;padding-right:0}.row .row{margin-left:-.625rem;margin-right:-.625rem}@media screen and (min-width:40em){.row .row{margin-left:-.9375rem;margin-right:-.9375rem}}.row .row.collapse{margin-left:0;margin-right:0}.row.expanded{max-width:none}.column,.columns{padding-left:.625rem;padding-right:.625rem;width:100%;float:left}@media screen and (min-width:40em){.column,.columns{padding-left:.9375rem;padding-right:.9375rem}}.column:last-child:not(:first-child),.columns:last-child:not(:first-child){float:right}.column.end:last-child:last-child,.end.columns:last-child:last-child{float:left}.column.row.row,.row.row.columns{float:none}.row .column.row.row,.row .row.row.columns{padding-left:0;padding-right:0;margin-left:0;margin-right:0}.small-1{width:8.33333%}.small-push-1{left:8.33333%}.small-pull-1{left:-8.33333%}.small-offset-0{margin-left:0}.small-2{width:16.66667%}.small-push-2{left:16.66667%}.small-pull-2{left:-16.66667%}.small-offset-1{margin-left:8.33333%}.small-3{width:25%}.small-push-3{left:25%}.small-pull-3{left:-25%}.small-offset-2{margin-left:16.66667%}.small-4{width:33.33333%}.small-push-4{left:33.33333%}.small-pull-4{left:-33.33333%}.small-offset-3{margin-left:25%}.small-5{width:41.66667%}.small-push-5{left:41.66667%}.small-pull-5{left:-41.66667%}.small-offset-4{margin-left:33.33333%}.small-6{width:50%}.small-push-6{position:relative;left:50%}.small-pull-6{left:-50%}.small-offset-5{margin-left:41.66667%}.small-7{width:58.33333%}.small-push-7{left:58.33333%}.small-pull-7{left:-58.33333%}.small-offset-6{margin-left:50%}.small-8{width:66.66667%}.small-push-8{left:66.66667%}.small-pull-8{left:-66.66667%}.small-offset-7{margin-left:58.33333%}.small-9{width:75%}.small-push-9{left:75%}.small-pull-9{left:-75%}.small-offset-8{margin-left:66.66667%}.small-10{width:83.33333%}.small-push-10{left:83.33333%}.small-pull-10{left:-83.33333%}.small-offset-9{margin-left:75%}.small-11{width:91.66667%}.small-push-11{left:91.66667%}.small-pull-11{left:-91.66667%}.small-offset-10{margin-left:83.33333%}.small-12{width:100%}.small-offset-11{margin-left:91.66667%}.small-up-1>.column,.small-up-1>.columns{width:100%;float:left}.small-up-1>.column:nth-of-type(1n),.small-up-1>.columns:nth-of-type(1n){clear:none}.small-up-1>.column:nth-of-type(1n+1),.small-up-1>.columns:nth-of-type(1n+1){clear:both}.small-up-1>.column:last-child,.small-up-1>.columns:last-child{float:left}.small-up-2>.column,.small-up-2>.columns{width:50%;float:left}.small-up-2>.column:nth-of-type(1n),.small-up-2>.columns:nth-of-type(1n){clear:none}.small-up-2>.column:nth-of-type(2n+1),.small-up-2>.columns:nth-of-type(2n+1){clear:both}.small-up-2>.column:last-child,.small-up-2>.columns:last-child{float:left}.small-up-3>.column,.small-up-3>.columns{width:33.33333%;float:left}.small-up-3>.column:nth-of-type(1n),.small-up-3>.columns:nth-of-type(1n){clear:none}.small-up-3>.column:nth-of-type(3n+1),.small-up-3>.columns:nth-of-type(3n+1){clear:both}.small-up-3>.column:last-child,.small-up-3>.columns:last-child{float:left}.small-up-4>.column,.small-up-4>.columns{width:25%;float:left}.small-up-4>.column:nth-of-type(1n),.small-up-4>.columns:nth-of-type(1n){clear:none}.small-up-4>.column:nth-of-type(4n+1),.small-up-4>.columns:nth-of-type(4n+1){clear:both}.small-up-4>.column:last-child,.small-up-4>.columns:last-child{float:left}.small-up-5>.column,.small-up-5>.columns{width:20%;float:left}.small-up-5>.column:nth-of-type(1n),.small-up-5>.columns:nth-of-type(1n){clear:none}.small-up-5>.column:nth-of-type(5n+1),.small-up-5>.columns:nth-of-type(5n+1){clear:both}.small-up-5>.column:last-child,.small-up-5>.columns:last-child{float:left}.small-up-6>.column,.small-up-6>.columns{width:16.66667%;float:left}.small-up-6>.column:nth-of-type(1n),.small-up-6>.columns:nth-of-type(1n){clear:none}.small-up-6>.column:nth-of-type(6n+1),.small-up-6>.columns:nth-of-type(6n+1){clear:both}.small-up-6>.column:last-child,.small-up-6>.columns:last-child{float:left}.small-up-7>.column,.small-up-7>.columns{width:14.28571%;float:left}.small-up-7>.column:nth-of-type(1n),.small-up-7>.columns:nth-of-type(1n){clear:none}.small-up-7>.column:nth-of-type(7n+1),.small-up-7>.columns:nth-of-type(7n+1){clear:both}.small-up-7>.column:last-child,.small-up-7>.columns:last-child{float:left}.small-up-8>.column,.small-up-8>.columns{width:12.5%;float:left}.small-up-8>.column:nth-of-type(1n),.small-up-8>.columns:nth-of-type(1n){clear:none}.small-up-8>.column:nth-of-type(8n+1),.small-up-8>.columns:nth-of-type(8n+1){clear:both}.small-up-8>.column:last-child,.small-up-8>.columns:last-child{float:left}.small-collapse>.column,.small-collapse>.columns{padding-left:0;padding-right:0}.small-uncollapse>.column,.small-uncollapse>.columns{padding-left:.625rem;padding-right:.625rem}.small-centered{float:none;margin-left:auto;margin-right:auto}.small-pull-0,.small-push-0,.small-uncentered{position:static;margin-left:0;margin-right:0}@media screen and (min-width:40em){.medium-pull-1,.medium-pull-10,.medium-pull-11,.medium-pull-2,.medium-pull-3,.medium-pull-4,.medium-pull-5,.medium-pull-6,.medium-pull-7,.medium-pull-8,.medium-pull-9,.medium-push-1,.medium-push-10,.medium-push-11,.medium-push-2,.medium-push-3,.medium-push-4,.medium-push-5,.medium-push-7,.medium-push-8,.medium-push-9{position:relative}.medium-1{width:8.33333%}.medium-push-1{left:8.33333%}.medium-pull-1{left:-8.33333%}.medium-offset-0{margin-left:0}.medium-2{width:16.66667%}.medium-push-2{left:16.66667%}.medium-pull-2{left:-16.66667%}.medium-offset-1{margin-left:8.33333%}.medium-3{width:25%}.medium-push-3{left:25%}.medium-pull-3{left:-25%}.medium-offset-2{margin-left:16.66667%}.medium-4{width:33.33333%}.medium-push-4{left:33.33333%}.medium-pull-4{left:-33.33333%}.medium-offset-3{margin-left:25%}.medium-5{width:41.66667%}.medium-push-5{left:41.66667%}.medium-pull-5{left:-41.66667%}.medium-offset-4{margin-left:33.33333%}.medium-6{width:50%}.medium-push-6{position:relative;left:50%}.medium-pull-6{left:-50%}.medium-offset-5{margin-left:41.66667%}.medium-7{width:58.33333%}.medium-push-7{left:58.33333%}.medium-pull-7{left:-58.33333%}.medium-offset-6{margin-left:50%}.medium-8{width:66.66667%}.medium-push-8{left:66.66667%}.medium-pull-8{left:-66.66667%}.medium-offset-7{margin-left:58.33333%}.medium-9{width:75%}.medium-push-9{left:75%}.medium-pull-9{left:-75%}.medium-offset-8{margin-left:66.66667%}.medium-10{width:83.33333%}.medium-push-10{left:83.33333%}.medium-pull-10{left:-83.33333%}.medium-offset-9{margin-left:75%}.medium-11{width:91.66667%}.medium-push-11{left:91.66667%}.medium-pull-11{left:-91.66667%}.medium-offset-10{margin-left:83.33333%}.medium-12{width:100%}.medium-offset-11{margin-left:91.66667%}.medium-up-1>.column,.medium-up-1>.columns{width:100%;float:left}.medium-up-1>.column:nth-of-type(1n),.medium-up-1>.columns:nth-of-type(1n){clear:none}.medium-up-1>.column:nth-of-type(1n+1),.medium-up-1>.columns:nth-of-type(1n+1){clear:both}.medium-up-1>.column:last-child,.medium-up-1>.columns:last-child{float:left}.medium-up-2>.column,.medium-up-2>.columns{width:50%;float:left}.medium-up-2>.column:nth-of-type(1n),.medium-up-2>.columns:nth-of-type(1n){clear:none}.medium-up-2>.column:nth-of-type(2n+1),.medium-up-2>.columns:nth-of-type(2n+1){clear:both}.medium-up-2>.column:last-child,.medium-up-2>.columns:last-child{float:left}.medium-up-3>.column,.medium-up-3>.columns{width:33.33333%;float:left}.medium-up-3>.column:nth-of-type(1n),.medium-up-3>.columns:nth-of-type(1n){clear:none}.medium-up-3>.column:nth-of-type(3n+1),.medium-up-3>.columns:nth-of-type(3n+1){clear:both}.medium-up-3>.column:last-child,.medium-up-3>.columns:last-child{float:left}.medium-up-4>.column,.medium-up-4>.columns{width:25%;float:left}.medium-up-4>.column:nth-of-type(1n),.medium-up-4>.columns:nth-of-type(1n){clear:none}.medium-up-4>.column:nth-of-type(4n+1),.medium-up-4>.columns:nth-of-type(4n+1){clear:both}.medium-up-4>.column:last-child,.medium-up-4>.columns:last-child{float:left}.medium-up-5>.column,.medium-up-5>.columns{width:20%;float:left}.medium-up-5>.column:nth-of-type(1n),.medium-up-5>.columns:nth-of-type(1n){clear:none}.medium-up-5>.column:nth-of-type(5n+1),.medium-up-5>.columns:nth-of-type(5n+1){clear:both}.medium-up-5>.column:last-child,.medium-up-5>.columns:last-child{float:left}.medium-up-6>.column,.medium-up-6>.columns{width:16.66667%;float:left}.medium-up-6>.column:nth-of-type(1n),.medium-up-6>.columns:nth-of-type(1n){clear:none}.medium-up-6>.column:nth-of-type(6n+1),.medium-up-6>.columns:nth-of-type(6n+1){clear:both}.medium-up-6>.column:last-child,.medium-up-6>.columns:last-child{float:left}.medium-up-7>.column,.medium-up-7>.columns{width:14.28571%;float:left}.medium-up-7>.column:nth-of-type(1n),.medium-up-7>.columns:nth-of-type(1n){clear:none}.medium-up-7>.column:nth-of-type(7n+1),.medium-up-7>.columns:nth-of-type(7n+1){clear:both}.medium-up-7>.column:last-child,.medium-up-7>.columns:last-child{float:left}.medium-up-8>.column,.medium-up-8>.columns{width:12.5%;float:left}.medium-up-8>.column:nth-of-type(1n),.medium-up-8>.columns:nth-of-type(1n){clear:none}.medium-up-8>.column:nth-of-type(8n+1),.medium-up-8>.columns:nth-of-type(8n+1){clear:both}.medium-up-8>.column:last-child,.medium-up-8>.columns:last-child{float:left}.medium-collapse>.column,.medium-collapse>.columns{padding-left:0;padding-right:0}.medium-uncollapse>.column,.medium-uncollapse>.columns{padding-left:.9375rem;padding-right:.9375rem}.medium-centered{float:none;margin-left:auto;margin-right:auto}.medium-pull-0,.medium-push-0,.medium-uncentered{position:static;margin-left:0;margin-right:0}}@media screen and (min-width:64em){.large-pull-1,.large-pull-10,.large-pull-11,.large-pull-2,.large-pull-3,.large-pull-4,.large-pull-5,.large-pull-6,.large-pull-7,.large-pull-8,.large-pull-9,.large-push-1,.large-push-10,.large-push-11,.large-push-2,.large-push-3,.large-push-4,.large-push-5,.large-push-7,.large-push-8,.large-push-9{position:relative}.large-1{width:8.33333%}.large-push-1{left:8.33333%}.large-pull-1{left:-8.33333%}.large-offset-0{margin-left:0}.large-2{width:16.66667%}.large-push-2{left:16.66667%}.large-pull-2{left:-16.66667%}.large-offset-1{margin-left:8.33333%}.large-3{width:25%}.large-push-3{left:25%}.large-pull-3{left:-25%}.large-offset-2{margin-left:16.66667%}.large-4{width:33.33333%}.large-push-4{left:33.33333%}.large-pull-4{left:-33.33333%}.large-offset-3{margin-left:25%}.large-5{width:41.66667%}.large-push-5{left:41.66667%}.large-pull-5{left:-41.66667%}.large-offset-4{margin-left:33.33333%}.large-6{width:50%}.large-push-6{position:relative;left:50%}.large-pull-6{left:-50%}.large-offset-5{margin-left:41.66667%}.large-7{width:58.33333%}.large-push-7{left:58.33333%}.large-pull-7{left:-58.33333%}.large-offset-6{margin-left:50%}.large-8{width:66.66667%}.large-push-8{left:66.66667%}.large-pull-8{left:-66.66667%}.large-offset-7{margin-left:58.33333%}.large-9{width:75%}.large-push-9{left:75%}.large-pull-9{left:-75%}.large-offset-8{margin-left:66.66667%}.large-10{width:83.33333%}.large-push-10{left:83.33333%}.large-pull-10{left:-83.33333%}.large-offset-9{margin-left:75%}.large-11{width:91.66667%}.large-push-11{left:91.66667%}.large-pull-11{left:-91.66667%}.large-offset-10{margin-left:83.33333%}.large-12{width:100%}.large-offset-11{margin-left:91.66667%}.large-up-1>.column,.large-up-1>.columns{width:100%;float:left}.large-up-1>.column:nth-of-type(1n),.large-up-1>.columns:nth-of-type(1n){clear:none}.large-up-1>.column:nth-of-type(1n+1),.large-up-1>.columns:nth-of-type(1n+1){clear:both}.large-up-1>.column:last-child,.large-up-1>.columns:last-child{float:left}.large-up-2>.column,.large-up-2>.columns{width:50%;float:left}.large-up-2>.column:nth-of-type(1n),.large-up-2>.columns:nth-of-type(1n){clear:none}.large-up-2>.column:nth-of-type(2n+1),.large-up-2>.columns:nth-of-type(2n+1){clear:both}.large-up-2>.column:last-child,.large-up-2>.columns:last-child{float:left}.large-up-3>.column,.large-up-3>.columns{width:33.33333%;float:left}.large-up-3>.column:nth-of-type(1n),.large-up-3>.columns:nth-of-type(1n){clear:none}.large-up-3>.column:nth-of-type(3n+1),.large-up-3>.columns:nth-of-type(3n+1){clear:both}.large-up-3>.column:last-child,.large-up-3>.columns:last-child{float:left}.large-up-4>.column,.large-up-4>.columns{width:25%;float:left}.large-up-4>.column:nth-of-type(1n),.large-up-4>.columns:nth-of-type(1n){clear:none}.large-up-4>.column:nth-of-type(4n+1),.large-up-4>.columns:nth-of-type(4n+1){clear:both}.large-up-4>.column:last-child,.large-up-4>.columns:last-child{float:left}.large-up-5>.column,.large-up-5>.columns{width:20%;float:left}.large-up-5>.column:nth-of-type(1n),.large-up-5>.columns:nth-of-type(1n){clear:none}.large-up-5>.column:nth-of-type(5n+1),.large-up-5>.columns:nth-of-type(5n+1){clear:both}.large-up-5>.column:last-child,.large-up-5>.columns:last-child{float:left}.large-up-6>.column,.large-up-6>.columns{width:16.66667%;float:left}.large-up-6>.column:nth-of-type(1n),.large-up-6>.columns:nth-of-type(1n){clear:none}.large-up-6>.column:nth-of-type(6n+1),.large-up-6>.columns:nth-of-type(6n+1){clear:both}.large-up-6>.column:last-child,.large-up-6>.columns:last-child{float:left}.large-up-7>.column,.large-up-7>.columns{width:14.28571%;float:left}.large-up-7>.column:nth-of-type(1n),.large-up-7>.columns:nth-of-type(1n){clear:none}.large-up-7>.column:nth-of-type(7n+1),.large-up-7>.columns:nth-of-type(7n+1){clear:both}.large-up-7>.column:last-child,.large-up-7>.columns:last-child{float:left}.large-up-8>.column,.large-up-8>.columns{width:12.5%;float:left}.large-up-8>.column:nth-of-type(1n),.large-up-8>.columns:nth-of-type(1n){clear:none}.large-up-8>.column:nth-of-type(8n+1),.large-up-8>.columns:nth-of-type(8n+1){clear:both}.large-up-8>.column:last-child,.large-up-8>.columns:last-child{float:left}.large-collapse>.column,.large-collapse>.columns{padding-left:0;padding-right:0}.large-uncollapse>.column,.large-uncollapse>.columns{padding-left:.9375rem;padding-right:.9375rem}.large-centered{float:none;margin-left:auto;margin-right:auto}.large-pull-0,.large-push-0,.large-uncentered{position:static;margin-left:0;margin-right:0}}.breadcrumbs::after,.button-group::after,.clearfix::after,.off-canvas-wrapper-inner::after,.pagination::after,.tabs::after,.title-bar::after,.top-bar::after,hr{clear:both}ol,ul{margin-left:1.25rem}blockquote,dd,div,dl,dt,form,h1,h2,h3,h4,h5,h6,li,ol,p,pre,td,th,ul{margin:0;padding:0}dl,ol,p,ul{margin-bottom:1rem}p{font-size:inherit;text-rendering:optimizeLegibility}em,i{font-style:italic}h1,h2,h3,h4,h5,h6{font-style:normal;text-rendering:optimizeLegibility;margin-top:0;margin-bottom:.5rem;line-height:1.4}code,kbd{background-color:#e6e6e6;color:#0a0a0a;font-family:Consolas,"Liberation Mono",Courier,monospace}h1 small,h2 small,h3 small,h4 small,h5 small,h6 small{color:#cacaca;line-height:0}h1{font-size:1.5rem}h2{font-size:1.25rem}h3{font-size:1.1875rem}h4{font-size:1.125rem}h5{font-size:1.0625rem}h6{font-size:1rem}@media screen and (min-width:40em){h1{font-size:3rem}h2{font-size:2.5rem}h3{font-size:1.9375rem}h4{font-size:1.5625rem}h5{font-size:1.25rem}h6{font-size:1rem}}a{background-color:transparent;color:#2ba6cb;text-decoration:none;cursor:pointer}a:focus,a:hover{color:#258faf}a img{border:0}hr{box-sizing:content-box;max-width:62.5rem;height:0;border-right:0;border-top:0;border-bottom:1px solid #cacaca;border-left:0;margin:1.25rem auto}dl,ol,ul{list-style-position:outside}li{font-size:inherit}ul{list-style-type:disc}.accordion,.menu,.tabs{list-style-type:none}ol ol,ol ul,ul ol,ul ul{margin-left:1.25rem;margin-bottom:0}dl dt{margin-bottom:.3rem;font-weight:700}.subheader,code,label{font-weight:400}blockquote{margin:0 0 1rem;padding:.5625rem 1.25rem 0 1.1875rem;border-left:1px solid #cacaca}blockquote,blockquote p{line-height:1.6;color:#8a8a8a}cite{display:block;font-size:.8125rem;color:#8a8a8a}cite:before{content:'\2014 \0020'}abbr{color:#222;cursor:help;border-bottom:1px dotted #0a0a0a}code{border:1px solid #cacaca;padding:.125rem .3125rem .0625rem}kbd{padding:.125rem .25rem 0;margin:0}.subheader{margin-top:.2rem;margin-bottom:.5rem;line-height:1.4;color:#8a8a8a}.lead{font-size:125%;line-height:1.6}.button,.stat{line-height:1}.stat{font-size:2.5rem}p+.stat{margin-top:-1rem}.no-bullet{margin-left:0;list-style:none}.text-left{text-align:left}.text-right{text-align:right}.text-center{text-align:center}.text-justify{text-align:justify}@media screen and (min-width:40em){.medium-text-left{text-align:left}.medium-text-right{text-align:right}.medium-text-center{text-align:center}.medium-text-justify{text-align:justify}}@media screen and (min-width:64em){.large-text-left{text-align:left}.large-text-right{text-align:right}.large-text-center{text-align:center}.large-text-justify{text-align:justify}}.button,.input-group-label,.menu.icon-top>li>a{text-align:center}.show-for-print{display:none!important}@media print{blockquote,img,pre,tr{page-break-inside:avoid}*{background:0 0!important;color:#000!important;box-shadow:none!important;text-shadow:none!important}.show-for-print{display:block!important}.hide-for-print{display:none!important}table.show-for-print{display:table!important}thead.show-for-print{display:table-header-group!important}tbody.show-for-print{display:table-row-group!important}tr.show-for-print{display:table-row!important}td.show-for-print,th.show-for-print{display:table-cell!important}a,a:visited{text-decoration:underline}a[href]:after{content:" (" attr(href) ")"}.ir a:after,a[href^='javascript:']:after,a[href^='#']:after{content:''}abbr[title]:after{content:" (" attr(title) ")"}blockquote,pre{border:1px solid #999}thead{display:table-header-group}img{max-width:100%!important}@page{margin:.5cm}h2,h3,p{orphans:3;widows:3}h2,h3{page-break-after:avoid}}.button{display:inline-block;cursor:pointer;-webkit-appearance:none;transition:background-color .25s ease-out,color .25s ease-out;border:1px solid transparent;border-radius:3px;padding:.85em 1em;margin:0 0 1rem;font-size:.9rem;background-color:#2ba6cb;color:#fff}[data-whatinput=mouse] .button{outline:0}.button:focus,.button:hover{background-color:#258dad;color:#fff}.button.tiny{font-size:.6rem}.button.small{font-size:.75rem}.button.large{font-size:1.25rem}.button.expanded{display:block;width:100%;margin-left:0;margin-right:0}.button.primary{background-color:#2ba6cb;color:#fff}.button.primary:focus,.button.primary:hover{background-color:#2285a2;color:#fff}.button.secondary{background-color:#e9e9e9;color:#000}.button.secondary:focus,.button.secondary:hover{background-color:#bababa;color:#000}.button.success{background-color:#5da423;color:#fff}.button.success:focus,.button.success:hover{background-color:#4a831c;color:#fff}.button.alert{background-color:#c60f13;color:#fff}.button.alert:focus,.button.alert:hover{background-color:#9e0c0f;color:#fff}.button.warning{background-color:#ffae00;color:#fff}.button.warning:focus,.button.warning:hover{background-color:#cc8b00;color:#fff}.button.hollow{border:1px solid #2ba6cb;color:#2ba6cb}.button.hollow,.button.hollow:focus,.button.hollow:hover{background-color:transparent}.button.hollow:focus,.button.hollow:hover{border-color:#165366;color:#165366}.button.hollow.primary{border:1px solid #2ba6cb;color:#2ba6cb}.button.hollow.primary:focus,.button.hollow.primary:hover{border-color:#165366;color:#165366}.button.hollow.secondary{border:1px solid #e9e9e9;color:#e9e9e9}.button.hollow.secondary:focus,.button.hollow.secondary:hover{border-color:#757575;color:#757575}.button.hollow.success{border:1px solid #5da423;color:#5da423}.button.hollow.success:focus,.button.hollow.success:hover{border-color:#2f5212;color:#2f5212}.button.hollow.alert{border:1px solid #c60f13;color:#c60f13}.button.hollow.alert:focus,.button.hollow.alert:hover{border-color:#63080a;color:#63080a}.button.hollow.warning{border:1px solid #ffae00;color:#ffae00}.button.hollow.warning:focus,.button.hollow.warning:hover{border-color:#805700;color:#805700}.button.disabled,.button[disabled]{opacity:.25;cursor:not-allowed;pointer-events:none}.button.dropdown::after{content:'';width:0;height:0;border:.4em inset;border-color:#fefefe transparent transparent;border-top-style:solid;top:.4em;float:right;margin-left:1em;display:inline-block}.button.arrow-only::after{margin-left:0;float:none;top:.2em}[type=text],[type=password],[type=date],[type=datetime],[type=datetime-local],[type=month],[type=week],[type=email],[type=number],[type=search],[type=tel],[type=time],[type=url],[type=color],textarea{display:block;box-sizing:border-box;width:100%;height:2.4375rem;padding:.5rem;border:1px solid #cacaca;margin:0 0 1rem;font-family:inherit;font-size:1rem;color:#0a0a0a;background-color:#fefefe;box-shadow:inset 0 1px 2px rgba(10,10,10,.1);border-radius:3px;transition:box-shadow .5s,border-color .25s ease-in-out;-webkit-appearance:none;-moz-appearance:none}[type=text]:focus,[type=password]:focus,[type=date]:focus,[type=datetime]:focus,[type=datetime-local]:focus,[type=month]:focus,[type=week]:focus,[type=email]:focus,[type=number]:focus,[type=search]:focus,[type=tel]:focus,[type=time]:focus,[type=url]:focus,[type=color]:focus,textarea:focus{border:1px solid #8a8a8a;background-color:#fefefe;outline:0;box-shadow:0 0 5px #cacaca;transition:box-shadow .5s,border-color .25s ease-in-out}textarea{min-height:50px;max-width:100%}textarea[rows]{height:auto}input:disabled,input[readonly],textarea:disabled,textarea[readonly]{background-color:#e6e6e6;cursor:default}[type=submit],[type=button]{border-radius:3px;-webkit-appearance:none;-moz-appearance:none}input[type=search]{box-sizing:border-box}[type=file],[type=checkbox],[type=radio]{margin:0 0 1rem}[type=checkbox]+label,[type=radio]+label{display:inline-block;margin-left:.5rem;margin-right:1rem;margin-bottom:0;vertical-align:baseline}label>[type=checkbox],label>[type=label]{margin-right:.5rem}[type=file]{width:100%}label{display:block;margin:0;font-size:.875rem;line-height:1.8;color:#0a0a0a}.form-error,.menu-text,.switch{font-weight:700}label.middle{margin:0 0 1rem;padding:.5625rem 0}.help-text{margin-top:-.5rem;font-size:.8125rem;font-style:italic;color:#333}.input-group{display:table;width:100%;margin-bottom:1rem}.input-group-button a,.input-group-button button,.input-group-button input,fieldset{margin:0}.input-group>:first-child{border-radius:3px 0 0 3px}.input-group>:last-child>*{border-radius:0 3px 3px 0}.input-group-button,.input-group-field,.input-group-label{display:table-cell;margin:0;vertical-align:middle}.input-group-label{width:1%;height:100%;padding:0 1rem;background:#e6e6e6;color:#0a0a0a;border:1px solid #cacaca}.input-group-label:first-child{border-right:0}.input-group-label:last-child{border-left:0}.input-group-field{border-radius:0;height:2.5rem}.fieldset,select{border:1px solid #cacaca}.input-group-button{height:100%;padding-top:0;padding-bottom:0;text-align:center;width:1%}fieldset{border:0;padding:0}legend{margin-bottom:.5rem}.fieldset{padding:1.25rem;margin:1.125rem 0}.fieldset legend{background:#fefefe;padding:0 .1875rem;margin:0 0 0 -.1875rem}select{height:2.4375rem;padding:.5rem;margin:0 0 1rem;font-size:1rem;font-family:inherit;line-height:normal;color:#0a0a0a;background-color:#fefefe;border-radius:3px;-webkit-appearance:none;-moz-appearance:none;background-image:url('data:image/svg+xml;utf8,');background-size:9px 6px;background-position:right .5rem center;background-repeat:no-repeat}.form-error,.is-invalid-label{color:#c60f13}@media screen and (min-width:0\0){select{background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAYCAYAAACbU/80AAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAIpJREFUeNrEkckNgDAMBBfRkEt0ObRBBdsGXUDgmQfK4XhH2m8czQAAy27R3tsw4Qfe2x8uOO6oYLb6GlOor3GF+swURAOmUJ+RwtEJs9WvTGEYxBXqI1MQAZhCfUQKRzDMVj+TwrAIV6jvSUEkYAr1LSkcyTBb/V+KYfX7xAeusq3sLDtGH3kEGACPWIflNZfhRQAAAABJRU5ErkJggg==)}}select:disabled{background-color:#e6e6e6;cursor:default}select::-ms-expand{display:none}select[multiple]{height:auto}.is-invalid-input:not(:focus){background-color:rgba(198,15,19,.1);border-color:#c60f13}.form-error{display:none;margin-top:-.5rem;margin-bottom:1rem;font-size:.75rem}.form-error.is-visible{display:block}.hide{display:none!important}@media screen and (min-width:0em) and (max-width:39.9375em){.hide-for-small-only{display:none!important}}@media screen and (max-width:0em),screen and (min-width:40em){.show-for-small-only{display:none!important}}@media screen and (min-width:40em){.hide-for-medium{display:none!important}}@media screen and (max-width:39.9375em){.show-for-medium{display:none!important}}@media screen and (min-width:40em) and (max-width:63.9375em){.hide-for-medium-only{display:none!important}}@media screen and (max-width:39.9375em),screen and (min-width:64em){.show-for-medium-only{display:none!important}}@media screen and (min-width:64em){.hide-for-large{display:none!important}}@media screen and (max-width:63.9375em){.show-for-large{display:none!important}}@media screen and (min-width:64em) and (max-width:74.9375em){.hide-for-large-only{display:none!important}}@media screen and (max-width:63.9375em),screen and (min-width:75em){.show-for-large-only{display:none!important}}.show-for-sr,.show-on-focus{position:absolute!important;width:1px;height:1px;overflow:hidden;clip:rect(0,0,0,0)}.show-on-focus:active,.show-on-focus:focus{position:static!important;height:auto;width:auto;overflow:visible;clip:auto}.hide-for-portrait,.show-for-landscape{display:block!important}@media screen and (orientation:landscape){.hide-for-portrait,.show-for-landscape{display:block!important}.hide-for-landscape,.show-for-portrait{display:none!important}}.hide-for-landscape,.show-for-portrait{display:none!important}@media screen and (orientation:portrait){.hide-for-portrait,.show-for-landscape{display:none!important}.hide-for-landscape,.show-for-portrait{display:block!important}}.float-left{float:left!important}.float-right{float:right!important}.float-center{display:block;margin-left:auto;margin-right:auto}.clearfix::after,.clearfix::before{content:' ';display:table}.accordion{background:#fefefe;border:1px solid #e6e6e6;border-radius:3px;margin-left:0}.accordion-title{display:block;padding:1.25rem 1rem;line-height:1;font-size:.75rem;color:#2ba6cb;position:relative;border-bottom:1px solid #e6e6e6}.accordion-title:focus,.accordion-title:hover{background-color:#e6e6e6}:last-child>.accordion-title{border-bottom-width:0}.accordion-title::before{content:'+';position:absolute;right:1rem;top:50%;margin-top:-.5rem}.is-active>.accordion-title::before{content:'–'}.accordion-content{padding:1rem;display:none;border-bottom:1px solid #e6e6e6;background-color:#fefefe}.is-accordion-submenu-parent>a{position:relative}.is-accordion-submenu-parent>a::after{content:'';display:block;width:0;height:0;border:6px inset;border-color:#2ba6cb transparent transparent;border-top-style:solid;position:absolute;top:50%;margin-top:-4px;right:1rem}.is-accordion-submenu-parent[aria-expanded=true]>a::after{-webkit-transform-origin:50% 50%;-ms-transform-origin:50% 50%;transform-origin:50% 50%;-webkit-transform:scaleY(-1);-ms-transform:scaleY(-1);transform:scaleY(-1)}.breadcrumbs{list-style:none;margin:0 0 1rem}.breadcrumbs::after,.breadcrumbs::before{content:' ';display:table}.breadcrumbs li{float:left;color:#0a0a0a;font-size:.6875rem;cursor:default;text-transform:uppercase}.breadcrumbs li:not(:last-child)::after{color:#cacaca;content:"/";margin:0 .75rem;position:relative;top:1px;opacity:1}.breadcrumbs a{color:#2ba6cb}.breadcrumbs a:hover{text-decoration:underline}.breadcrumbs .disabled{color:#cacaca}.button-group{margin-bottom:1rem;font-size:.9rem}.button-group::after,.button-group::before{content:' ';display:table}.button-group .button{float:left;margin:0;font-size:inherit}.button-group .button:not(:last-child){border-right:1px solid #fefefe}.button-group.tiny{font-size:.6rem}.button-group.small{font-size:.75rem}.button-group.large{font-size:1.25rem}.button-group.expanded{display:table;table-layout:fixed;width:100%}.button-group.expanded::after,.button-group.expanded::before{display:none}.button-group.expanded .button{display:table-cell;float:none}.button-group.primary .button{background-color:#2ba6cb;color:#fff}.button-group.primary .button:focus,.button-group.primary .button:hover{background-color:#2285a2;color:#fff}.button-group.secondary .button{background-color:#e9e9e9;color:#000}.button-group.secondary .button:focus,.button-group.secondary .button:hover{background-color:#bababa;color:#000}.button-group.success .button{background-color:#5da423;color:#fff}.button-group.success .button:focus,.button-group.success .button:hover{background-color:#4a831c;color:#fff}.button-group.alert .button{background-color:#c60f13;color:#fff}.button-group.alert .button:focus,.button-group.alert .button:hover{background-color:#9e0c0f;color:#fff}.button-group.warning .button{background-color:#ffae00;color:#fff}.button-group.warning .button:focus,.button-group.warning .button:hover{background-color:#cc8b00;color:#fff}.button-group.stacked .button,.button-group.stacked-for-small .button{width:100%}.button-group.stacked .button:not(:last-child),.button-group.stacked-for-small .button:not(:last-child){border-right:1px solid}@media screen and (min-width:40em){.button-group.stacked-for-small .button{width:auto}.button-group.stacked-for-small .button:not(:last-child){border-right:1px solid #fefefe}}.callout{margin:0 0 1rem;padding:1rem;border:1px solid rgba(10,10,10,.25);border-radius:3px;position:relative;color:#222;background-color:#fff}.callout>:first-child{margin-top:0}.callout>:last-child{margin-bottom:0}.callout.primary{background-color:#def2f8}.callout.secondary{background-color:#fcfcfc}.callout.success{background-color:#e6f7d9}.callout.alert{background-color:#fcd6d6}.callout.warning{background-color:#fff3d9}.callout.small{padding:.5rem}.callout.large{padding:3rem}.close-button{position:absolute;color:#8a8a8a;right:1rem;top:.5rem;font-size:2em;line-height:1;cursor:pointer}[data-whatinput=mouse] .close-button{outline:0}.close-button:focus,.close-button:hover{color:#0a0a0a}.is-drilldown{position:relative;overflow:hidden}.is-drilldown-submenu{position:absolute;top:0;left:100%;z-index:-1;height:100%;width:100%;background:#fefefe;transition:-webkit-transform .15s linear;transition:transform .15s linear}.is-drilldown-submenu-parent>a::after,.js-drilldown-back::before{width:0;content:'';display:block;height:0}.is-drilldown-submenu.is-active{z-index:1;display:block;-webkit-transform:translateX(-100%);-ms-transform:translateX(-100%);transform:translateX(-100%)}.is-drilldown-submenu.is-closing{-webkit-transform:translateX(100%);-ms-transform:translateX(100%);transform:translateX(100%)}.is-drilldown-submenu-parent>a{position:relative}.is-drilldown-submenu-parent>a::after{border:6px inset;border-color:transparent transparent transparent #2ba6cb;border-left-style:solid;position:absolute;top:50%;margin-top:-6px;right:1rem}.js-drilldown-back::before{border:6px inset;border-color:transparent #2ba6cb transparent transparent;border-right-style:solid;float:left;margin-right:.75rem;margin-left:.6rem;margin-top:14px}.dropdown-pane{background-color:#fefefe;border:1px solid #cacaca;display:block;padding:1rem;position:absolute;width:300px;z-index:10;border-radius:3px}.dropdown-pane.is-open{visibility:visible}.dropdown-pane.tiny{width:100px}.dropdown-pane.small{width:200px}.dropdown-pane.large{width:400px}[data-whatinput=mouse] .dropdown.menu a{outline:0}.dropdown.menu .is-dropdown-submenu-parent{position:relative}.dropdown.menu .is-dropdown-submenu-parent a::after{float:right;margin-top:3px;margin-left:10px}.dropdown.menu .is-dropdown-submenu-parent.is-down-arrow a{padding-right:1.5rem;position:relative}.dropdown.menu .is-dropdown-submenu-parent.is-down-arrow>a::after{content:'';display:block;width:0;height:0;border:5px inset;border-color:#2ba6cb transparent transparent;border-top-style:solid;position:absolute;top:.825rem;right:5px}.dropdown.menu .is-dropdown-submenu-parent.is-left-arrow>a::after{content:'';display:block;width:0;height:0;border:5px inset;border-color:transparent #2ba6cb transparent transparent;border-right-style:solid;float:left;margin-left:0;margin-right:10px}.is-dropdown-menu.vertical.align-right,.menu.align-right>li{float:right}.dropdown.menu .is-dropdown-submenu-parent.is-right-arrow>a::after{content:'';display:block;width:0;height:0;border:5px inset;border-color:transparent transparent transparent #2ba6cb;border-left-style:solid}.dropdown.menu .is-dropdown-submenu-parent.is-left-arrow.opens-inner .submenu{right:0;left:auto}.dropdown.menu .is-dropdown-submenu-parent.is-right-arrow.opens-inner .submenu{left:0;right:auto}.dropdown.menu .is-dropdown-submenu-parent.opens-inner .submenu{top:100%}.no-js .dropdown.menu ul{display:none}.dropdown.menu .submenu{display:none;position:absolute;top:0;left:100%;min-width:200px;z-index:1;background:#fefefe;border:1px solid #cacaca;margin-top:-1px}.dropdown.menu .submenu>li{width:100%}.dropdown.menu .submenu.first-sub{top:100%;left:0;right:auto}.dropdown.menu .submenu.js-dropdown-active,.dropdown.menu .submenu:not(.js-dropdown-nohover)>.is-dropdown-submenu-parent:hover>.dropdown.menu .submenu{display:block}.dropdown.menu .is-dropdown-submenu-parent.opens-left .submenu{left:auto;right:100%}.dropdown.menu.align-right .submenu.first-sub{top:100%;left:auto;right:0}.is-dropdown-menu.vertical{width:100px}.is-dropdown-menu.vertical>li .submenu{top:0;left:100%}.label{display:inline-block;padding:.33333rem .5rem;font-size:.8rem;line-height:1;white-space:nowrap;cursor:default;border-radius:3px;background:#2ba6cb;color:#fefefe}.label.secondary{background:#e9e9e9;color:#0a0a0a}.label.success{background:#5da423;color:#fefefe}.label.alert{background:#c60f13;color:#fefefe}.label.warning{background:#ffae00;color:#fefefe}.media-object{margin-bottom:1rem;display:block}.media-object img{max-width:none}@media screen and (min-width:0em) and (max-width:39.9375em){.media-object.stack-for-small .media-object-section{display:block;padding:0 0 1rem}.media-object.stack-for-small .media-object-section img{width:100%}}.media-object-section{display:table-cell;vertical-align:top}.media-object-section:first-child{padding-right:1rem}.media-object-section:last-child:not(+.media-object-section:first-child){padding-left:1rem}.media-object-section.middle{vertical-align:middle}.media-object-section.bottom{vertical-align:bottom}.menu>li,.menu>li>a>i,.menu>li>a>img,.menu>li>a>span{vertical-align:middle}.menu{margin:0}[data-whatinput=mouse] .menu>li{outline:0}.menu>li:not(.menu-text)>a{display:block;padding:.7rem 1rem;line-height:1}.menu a,.menu button,.menu input{margin-bottom:0}.menu>li>a>i,.menu>li>a>img{display:inline-block;margin-right:.25rem}.menu>li{display:table-cell}.menu.vertical>li{display:block}@media screen and (min-width:40em){.menu.medium-horizontal>li{display:table-cell}.menu.medium-vertical>li{display:block}}@media screen and (min-width:64em){.menu.large-horizontal>li{display:table-cell}.menu.large-vertical>li{display:block}}.menu.simple a{padding:0;margin-right:1rem}.menu.expanded{display:table;table-layout:fixed;width:100%}.menu.expanded>li:first-child:last-child{width:100%}.menu.icon-top>li>a>i,.menu.icon-top>li>a>img{display:block;margin:0 auto .25rem}.menu.nested{margin-left:1rem}.menu-text{color:inherit;line-height:1;padding:.7rem 1rem}.no-js [data-responsive-menu] ul{display:none}body,html{height:100%}.off-canvas-wrapper{width:100%;overflow-x:hidden;position:relative;-webkit-backface-visibility:hidden;backface-visibility:hidden;-webkit-overflow-scrolling:auto}.off-canvas-wrapper-inner{position:relative;width:100%;transition:-webkit-transform .5s ease;transition:transform .5s ease}.off-canvas-wrapper-inner::after,.off-canvas-wrapper-inner::before{content:' ';display:table}.off-canvas-content{min-height:100%;background:#fefefe;transition:-webkit-transform .5s ease;transition:transform .5s ease;-webkit-backface-visibility:hidden;backface-visibility:hidden;z-index:1;box-shadow:0 0 10px rgba(10,10,10,.5)}.js-off-canvas-exit{display:none;position:absolute;top:0;left:0;width:100%;height:100%;background:rgba(254,254,254,.25);cursor:pointer;transition:background .5s ease}.off-canvas,.pagination a:hover,.pagination button:hover{background:#e6e6e6}.is-off-canvas-open .js-off-canvas-exit{display:block}.off-canvas{position:absolute;z-index:-1;max-height:100%;overflow-y:auto;-webkit-transform:translateX(0);-ms-transform:translateX(0);transform:translateX(0)}[data-whatinput=mouse] .off-canvas{outline:0}.off-canvas.position-left{left:-250px;top:0;width:250px}.is-open-left{-webkit-transform:translateX(250px);-ms-transform:translateX(250px);transform:translateX(250px)}.off-canvas.position-right{right:-250px;top:0;width:250px}.is-open-right{-webkit-transform:translateX(-250px);-ms-transform:translateX(-250px);transform:translateX(-250px)}@media screen and (min-width:40em){.position-left.reveal-for-medium{left:0;z-index:auto;position:fixed}.position-left.reveal-for-medium~.off-canvas-content{margin-left:250px}.position-right.reveal-for-medium{right:0;z-index:auto;position:fixed}.position-right.reveal-for-medium~.off-canvas-content{margin-right:250px}}@media screen and (min-width:64em){.position-left.reveal-for-large{left:0;z-index:auto;position:fixed}.position-left.reveal-for-large~.off-canvas-content{margin-left:250px}.position-right.reveal-for-large{right:0;z-index:auto;position:fixed}.position-right.reveal-for-large~.off-canvas-content{margin-right:250px}}.pagination{margin-left:0;margin-bottom:1rem}.pagination::after,.pagination::before{content:' ';display:table}.pagination li{font-size:.875rem;margin-right:.0625rem;display:none;border-radius:3px}.pagination li:first-child,.pagination li:last-child{display:inline-block}@media screen and (min-width:40em){.pagination li{display:inline-block}.reveal{min-height:0}}.pagination a,.pagination button{color:#0a0a0a;display:block;padding:.1875rem .625rem;border-radius:3px}.pagination .current{padding:.1875rem .625rem;background:#2ba6cb;color:#fefefe;cursor:default}.pagination .disabled{padding:.1875rem .625rem;color:#cacaca;cursor:default}.pagination .disabled:hover{background:0 0}.pagination .ellipsis::after{content:'…';padding:.1875rem .625rem;color:#0a0a0a}.pagination-previous a::before,.pagination-previous.disabled::before{content:'«';display:inline-block;margin-right:.5rem}.pagination-next a::after,.pagination-next.disabled::after{content:'»';display:inline-block;margin-left:.5rem}.slider{position:relative;height:.5rem;margin-top:1.25rem;margin-bottom:2.25rem;background-color:#e6e6e6;cursor:pointer;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;-ms-touch-action:none;touch-action:none}.slider-fill{position:absolute;top:0;left:0;display:inline-block;max-width:100%;height:.5rem;background-color:#cacaca;transition:all .2s ease-in-out}.slider-fill.is-dragging{transition:all 0s linear}.slider-handle{top:50%;-webkit-transform:translateY(-50%);-ms-transform:translateY(-50%);transform:translateY(-50%);position:absolute;left:0;z-index:1;display:inline-block;width:1.4rem;height:1.4rem;background-color:#2ba6cb;transition:all .2s ease-in-out;-ms-touch-action:manipulation;touch-action:manipulation;border-radius:3px}[data-whatinput=mouse] .slider-handle{outline:0}.slider-handle:hover{background-color:#258dad}.slider-handle.is-dragging{transition:all 0s linear}.slider.disabled,.slider[disabled]{opacity:.25;cursor:not-allowed}.slider.vertical{display:inline-block;width:.5rem;height:12.5rem;margin:0 1.25rem;-webkit-transform:scale(1,-1);-ms-transform:scale(1,-1);transform:scale(1,-1)}.slider.vertical .slider-fill{top:0;width:.5rem;max-height:100%}.slider.vertical .slider-handle{position:absolute;top:0;left:50%;width:1.4rem;height:1.4rem;-webkit-transform:translateX(-50%);-ms-transform:translateX(-50%);transform:translateX(-50%)}body.is-reveal-open{overflow:hidden}.reveal-overlay{display:none;position:fixed;top:0;bottom:0;left:0;right:0;z-index:1005;background-color:rgba(10,10,10,.45);overflow-y:scroll}.reveal{display:none;z-index:1006;padding:1rem;border:1px solid #cacaca;margin:100px auto 0;background-color:#fefefe;border-radius:3px;position:absolute;overflow-y:auto}.switch-paddle,.switch-paddle::after{display:block;transition:all .25s ease-out}[data-whatinput=mouse] .reveal{outline:0}.reveal .column,.reveal .columns{min-width:0}.reveal>:last-child{margin-bottom:0}.reveal.collapse{padding:0}caption,tbody td,tbody th{padding:.5rem .625rem .625rem}@media screen and (min-width:40em){.reveal{width:600px;max-width:62.5rem}.reveal .reveal{left:auto;right:auto;margin:0 auto}.reveal.tiny{width:30%;max-width:62.5rem}.reveal.small{width:50%;max-width:62.5rem}.reveal.large{width:90%;max-width:62.5rem}}.reveal.full{top:0;left:0;width:100%;height:100%;height:100vh;min-height:100vh;max-width:none;margin-left:0;border:0}.switch{margin-bottom:1rem;outline:0;position:relative;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;color:#fefefe;font-size:.875rem}.switch-input{opacity:0;position:absolute}.switch-paddle{background:#cacaca;cursor:pointer;position:relative;width:4rem;height:2rem;border-radius:3px;color:inherit;font-weight:inherit}.title-bar-title,caption{font-weight:700}input+.switch-paddle{margin:0}.switch-paddle::after{background:#fefefe;content:'';position:absolute;height:1.5rem;left:.25rem;top:.25rem;width:1.5rem;-webkit-transform:translate3d(0,0,0);transform:translate3d(0,0,0);border-radius:3px}input:checked~.switch-paddle{background:#2ba6cb}input:checked~.switch-paddle::after{left:2.25rem}[data-whatinput=mouse] input:focus~.switch-paddle{outline:0}.switch-active,.switch-inactive{position:absolute;top:50%;-webkit-transform:translateY(-50%);-ms-transform:translateY(-50%);transform:translateY(-50%)}.switch-active{left:8%;display:none}input:checked+label>.switch-active{display:block}.switch-inactive{right:15%}input:checked+label>.switch-inactive{display:none}.switch.tiny .switch-paddle{width:3rem;height:1.5rem;font-size:.625rem}.switch.tiny .switch-paddle::after{width:1rem;height:1rem}.switch.tiny input:checked~.switch-paddle:after{left:1.75rem}.switch.small .switch-paddle{width:3.5rem;height:1.75rem;font-size:.75rem}.switch.small .switch-paddle::after{width:1.25rem;height:1.25rem}.switch.small input:checked~.switch-paddle:after{left:2rem}.switch.large .switch-paddle{width:5rem;height:2.5rem;font-size:1rem}.switch.large .switch-paddle::after{width:2rem;height:2rem}.switch.large input:checked~.switch-paddle:after{left:2.75rem}table{border-collapse:collapse;border-spacing:0;margin-bottom:1rem;border-radius:3px}tbody,tfoot,thead{border:1px solid #f1f1f1;background-color:#fefefe}tfoot,thead{background:#f8f8f8;color:#222}tfoot tr,thead tr{background:0 0}tfoot td,tfoot th,thead td,thead th{padding:.5rem .625rem .625rem;font-weight:700;text-align:left}tbody tr:nth-child(even){background-color:#f1f1f1}@media screen and (max-width:63.9375em){table.stack tfoot,table.stack thead{display:none}table.stack td,table.stack th,table.stack tr{display:block}table.stack td{border-top:0}}.tabs,.tabs-content{border:1px solid #e6e6e6}table.scroll{display:block;width:100%;overflow-x:auto}table.hover tr:hover{background-color:#f9f9f9}table.hover tr:nth-of-type(even):hover{background-color:#ececec}.tabs{margin:0;background:#fefefe}.tabs::after,.tabs::before{content:' ';display:table}.tabs.vertical>li{width:auto;float:none;display:block}.tabs-title,.title-bar-left{float:left}.tabs.simple>li>a{padding:0}.tabs.simple>li>a:hover{background:0 0}.tabs.primary{background:#2ba6cb}.tabs.primary>li>a{color:#fefefe}.tabs.primary>li>a:focus,.tabs.primary>li>a:hover{background:#299ec1}.tabs-title>a{display:block;padding:1.25rem 1.5rem;line-height:1;font-size:12px;color:#2ba6cb}.tabs-title>a:hover{background:#fefefe}.tabs-title>a:focus,.tabs-title>a[aria-selected=true]{background:#e6e6e6}.tabs-content{background:#fefefe;transition:all .5s ease;border-top:0}.tabs-content.vertical{border:1px solid #e6e6e6;border-left:0}.tabs-panel{display:none;padding:1rem}.title-bar,.top-bar{padding:.5rem}.tabs-panel.is-active{display:block}.title-bar{background:#0a0a0a;color:#fefefe}.title-bar::after,.title-bar::before{content:' ';display:table}.menu-icon,.title-bar-title{display:inline-block;vertical-align:middle}.title-bar .menu-icon{margin-left:.25rem;margin-right:.5rem}.title-bar-right{float:right;text-align:right}.menu-icon{position:relative;cursor:pointer;width:20px;height:16px}.menu-icon::after{content:'';position:absolute;display:block;width:100%;height:2px;background:#fefefe;top:0;left:0;box-shadow:0 7px 0 #fefefe,0 14px 0 #fefefe}.menu-icon:hover::after{background:#cacaca;box-shadow:0 7px 0 #cacaca,0 14px 0 #cacaca}.top-bar::after,.top-bar::before{content:' ';display:table}.top-bar,.top-bar ul{background-color:#e6e6e6}.top-bar a{color:#2ba6cb}.top-bar input{width:200px;margin-right:1rem}.top-bar input.button{width:auto}@media screen and (max-width:39.9375em){.stacked-for-small .top-bar-left,.stacked-for-small .top-bar-right{width:100%}}@media screen and (max-width:63.9375em){.stacked-for-medium .top-bar-left,.stacked-for-medium .top-bar-right{width:100%}}@media screen and (max-width:74.9375em){.stacked-for-large .top-bar-left,.stacked-for-large .top-bar-right{width:100%}}@media screen and (min-width:0em) and (max-width:39.9375em){.top-bar-left,.top-bar-right{width:100%}}.top-bar-left{float:left}.top-bar-right{float:right} -------------------------------------------------------------------------------- /data/editor.html: -------------------------------------------------------------------------------- 1 | ESP Editor
4 | -------------------------------------------------------------------------------- /data/icons/android-144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andig/vzero/e40ee7302bef20109f7aa9692f3b00c1ab4f725c/data/icons/android-144x144.png -------------------------------------------------------------------------------- /data/icons/android-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andig/vzero/e40ee7302bef20109f7aa9692f3b00c1ab4f725c/data/icons/android-192x192.png -------------------------------------------------------------------------------- /data/icons/android-36x36.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andig/vzero/e40ee7302bef20109f7aa9692f3b00c1ab4f725c/data/icons/android-36x36.png -------------------------------------------------------------------------------- /data/icons/android-48x48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andig/vzero/e40ee7302bef20109f7aa9692f3b00c1ab4f725c/data/icons/android-48x48.png -------------------------------------------------------------------------------- /data/icons/android-72x72.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andig/vzero/e40ee7302bef20109f7aa9692f3b00c1ab4f725c/data/icons/android-72x72.png -------------------------------------------------------------------------------- /data/icons/android-96x96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andig/vzero/e40ee7302bef20109f7aa9692f3b00c1ab4f725c/data/icons/android-96x96.png -------------------------------------------------------------------------------- /data/icons/apple-114x114.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andig/vzero/e40ee7302bef20109f7aa9692f3b00c1ab4f725c/data/icons/apple-114x114.png -------------------------------------------------------------------------------- /data/icons/apple-120x120.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andig/vzero/e40ee7302bef20109f7aa9692f3b00c1ab4f725c/data/icons/apple-120x120.png -------------------------------------------------------------------------------- /data/icons/apple-144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andig/vzero/e40ee7302bef20109f7aa9692f3b00c1ab4f725c/data/icons/apple-144x144.png -------------------------------------------------------------------------------- /data/icons/apple-152x152.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andig/vzero/e40ee7302bef20109f7aa9692f3b00c1ab4f725c/data/icons/apple-152x152.png -------------------------------------------------------------------------------- /data/icons/apple-180x180.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andig/vzero/e40ee7302bef20109f7aa9692f3b00c1ab4f725c/data/icons/apple-180x180.png -------------------------------------------------------------------------------- /data/icons/apple-57x57.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andig/vzero/e40ee7302bef20109f7aa9692f3b00c1ab4f725c/data/icons/apple-57x57.png -------------------------------------------------------------------------------- /data/icons/apple-60x60.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andig/vzero/e40ee7302bef20109f7aa9692f3b00c1ab4f725c/data/icons/apple-60x60.png -------------------------------------------------------------------------------- /data/icons/apple-72x72.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andig/vzero/e40ee7302bef20109f7aa9692f3b00c1ab4f725c/data/icons/apple-72x72.png -------------------------------------------------------------------------------- /data/icons/apple-76x76.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andig/vzero/e40ee7302bef20109f7aa9692f3b00c1ab4f725c/data/icons/apple-76x76.png -------------------------------------------------------------------------------- /data/icons/apple-precomposed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andig/vzero/e40ee7302bef20109f7aa9692f3b00c1ab4f725c/data/icons/apple-precomposed.png -------------------------------------------------------------------------------- /data/icons/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andig/vzero/e40ee7302bef20109f7aa9692f3b00c1ab4f725c/data/icons/apple-touch-icon.png -------------------------------------------------------------------------------- /data/icons/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andig/vzero/e40ee7302bef20109f7aa9692f3b00c1ab4f725c/data/icons/favicon-16x16.png -------------------------------------------------------------------------------- /data/icons/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andig/vzero/e40ee7302bef20109f7aa9692f3b00c1ab4f725c/data/icons/favicon-32x32.png -------------------------------------------------------------------------------- /data/icons/favicon-96x96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andig/vzero/e40ee7302bef20109f7aa9692f3b00c1ab4f725c/data/icons/favicon-96x96.png -------------------------------------------------------------------------------- /data/icons/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andig/vzero/e40ee7302bef20109f7aa9692f3b00c1ab4f725c/data/icons/favicon.ico -------------------------------------------------------------------------------- /data/img/loading.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andig/vzero/e40ee7302bef20109f7aa9692f3b00c1ab4f725c/data/img/loading.gif -------------------------------------------------------------------------------- /data/img/logo_white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andig/vzero/e40ee7302bef20109f7aa9692f3b00c1ab4f725c/data/img/logo_white.png -------------------------------------------------------------------------------- /data/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | VZero 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 |
27 |
28 | 29 |

VZERO - The wireless zero-config controller by volkszaehler.org

30 |
31 |
32 | 33 |
34 |

loading...

35 |
36 | 37 |
38 | 49 | 50 |
51 |

Welcome to VZero

52 |

Congratulations - you've successfully started your device!

53 |

Your VZero device was not yet able to connect to a WiFi network and is currently running as Access Point. 54 | Please configure WiFi credentials below and restart the device.

55 |

After restarting the device will connect to the specified WiFi network and should be accessible under this address: (address updating).

56 |

To access the device you will have to connect to the same WiFi network as the VZero.

57 |

58 |
59 | 60 |
61 |

Settings

62 |
63 | 64 |
65 |

WiFi Configuration

66 |
67 |
68 |
SSID:
69 |
70 |
71 |
72 |
Password:
73 |
74 |
75 |
76 |
77 |
Note: Setting new WiFi credentials settings will restart VZero.
78 |
79 |
80 |
81 | 82 | 83 |
84 |

Success!

85 |

Congratulations - VZero is now connected to a wireless network.

86 |

Before VZero can start logging data a middleware must be configured. Please review the middleware settings below.

87 |
88 | 89 |
90 |

Middleware Configuration

91 |
92 |
93 |
Middleware:
94 |
95 |
96 |
97 |
98 |
Note: Updating the middleware settings will restart VZero.
99 |
100 |
101 |
102 | 103 | 104 |
105 |

Home

106 | 107 |
108 |

109 |

 

110 |
111 | 112 | 115 |
116 | 117 | 118 |
119 |

Plugins & Sensors

120 |

Plugins

121 |

The VZero device is configured with the following data aquisition plugins:

122 |

Nothing configured

123 | 124 |
125 |
126 |
127 |
128 |
129 | 130 |
131 |

Sensors

132 |

Nothing configured

133 | 134 |
135 |
136 |
137 |   138 |
139 |
140 | 141 | 142 | 143 | 144 |
145 |
146 | 147 |
148 | 149 |
150 |
151 | 152 | 153 |
154 |

Status

155 | 156 |
157 |
Uptime:
158 |
159 |
160 | 166 |
167 |
Serial:
168 |
169 |
170 |
171 |
WiFi:
172 |
173 |
174 |
175 |
IP Address:
176 |
177 |
178 |
179 |
Flash Memory:
180 |
n/a
181 |
182 |
183 |
Free Memory:
184 |
n/a
185 |
186 |
187 |
188 | 189 |
190 |
191 | 192 |
193 |

API

194 |

VZero exposes the following JSON APIs:

195 | 196 |

APIs using HTTP GET

197 |
198 | 199 | 200 | 201 |
202 | 203 |

APIs using HTTP POST

204 |
205 | 206 | 207 |
208 |
209 | 210 | 211 |
212 |
213 |
Success
214 |
.
215 |
216 |
217 |
Warning
218 |
.
219 |
220 |
221 |
Error
222 |
.
223 |
224 |
225 |
Log
226 |
.
227 |
228 | 244 |
245 |
246 | 247 | 248 | 249 | 250 | 251 | 252 | -------------------------------------------------------------------------------- /data/js/app.js: -------------------------------------------------------------------------------- 1 | (function($, window, document) { 2 | 3 | var options = { 4 | timeout: 5000, // ajax timeout 5 | heartBeat: { 6 | interval: 10000 // heartbeat interval 7 | }, 8 | sensors: { 9 | interval: 30000 // sensor interval 10 | }, 11 | messages: { 12 | timeout: 10000 // message fade interval 13 | }, 14 | api: "", // api url 15 | test: { 16 | // stage: "wifi" // setup state for testing 17 | // stage: "middleware" // setup state for testing 18 | } 19 | }; 20 | 21 | var sparkdata = [], 22 | sparkline; 23 | 24 | function getUrlParams() { 25 | var vars = {}; 26 | decodeURIComponent(window.location.search).replace(/[?&]+([^=&]+)=([^&]*)/gi, function(m,key,value) { 27 | vars[key] = value; 28 | }); 29 | return vars; 30 | } 31 | 32 | function hashCode(str) { 33 | var hash = 0, i, chr, len; 34 | if (str.length === 0) return hash; 35 | for (i = 0, len = str.length; i < len; i++) { 36 | chr = str.charCodeAt(i); 37 | hash = ((hash << 5) - hash) + chr; 38 | hash |= 0; // Convert to 32bit integer 39 | } 40 | return hash; 41 | } 42 | 43 | function ajax(url, settings) { 44 | var log = (settings.data) ? url + "
" + JSON.stringify(settings.data) : url; 45 | var hash = notify("info", "API Log", log, true); 46 | return $.ajax(url, settings).done(function(json) { 47 | $("." + hash + " .message").html($("." + hash + " .message").html() + "
" + JSON.stringify(json)); 48 | }); 49 | } 50 | 51 | function template(tpl, target) { 52 | return $(".template" + tpl).clone().removeClass("template").appendTo(target); 53 | } 54 | 55 | function initializePlugins() { 56 | return $.getJSON(getApi("/api/plugins")).done(function(json) { 57 | // add plugins 58 | $.each(json, function(i, plugin) { 59 | var unit, 60 | el = template(".plugin", ".state-plugins:first()").addClass("plugin-" + plugin.name); 61 | 62 | $(".state-plugins .not-configured").remove(); 63 | 64 | if (plugin.name == "1wire") { 65 | unit = "°C"; 66 | plugin.title = "1-Wire"; 67 | el.find(".description").html("1-Wire is a device communications bus system designed by Dallas Semiconductor Corp. that provides low-speed data, signaling, and power over a single signal. (Source: Wikipedia)"); 68 | } 69 | else if (plugin.name == "analog") { 70 | unit = "V"; 71 | plugin.title = "Analog"; 72 | el.find(".description").html("Analog plugin uses the built-in analog to digital (ADC) converter to measure analog voltages."); 73 | } 74 | else if (plugin.name == "gpio") { 75 | unit = "Imp"; 76 | plugin.title = "GPIO"; 77 | el.find(".description").html("GPIO plugin is used to register and count digital pulses."); 78 | } 79 | else if (plugin.name == "wifi") { 80 | unit = "dbm"; 81 | plugin.title = "WiFi"; 82 | el.find(".description").html("WiFi plugin measures the received signal strength indicator (RSSI) if the WiFi signal."); 83 | } 84 | else if (plugin.name == "dht") { 85 | unit = "°C"; 86 | plugin.title = "DHT"; 87 | el.find(".description").html("DHT sensors are basic, ultra low-cost digital temperature and humidity sensors."); 88 | } 89 | else { 90 | unit = ""; 91 | plugin.title = plugin.name; 92 | } 93 | el.find(".name, .title").text(plugin.title); 94 | 95 | if (json && json.length) { 96 | $(".state-plugins .not-configured").remove(); 97 | } 98 | 99 | // add sensors 100 | $.each(plugin.sensors, function(j, sensor) { 101 | $(".state-sensors .not-configured").remove(); 102 | 103 | // update sensor, check unit again 104 | sensor.plugin = plugin.name; 105 | sensor.unit = unit; 106 | getSensorType(sensor); 107 | 108 | // home screen 109 | template(".sensor-home", ".state-home").addClass("sensor-" + sensor.plugin + "-" + sensor.addr); 110 | // plugins screen 111 | var el = template(".sensor", ".state-sensors").addClass("sensor-" + sensor.plugin + "-" + sensor.addr); 112 | updateSensorUI(sensor); 113 | }); 114 | }); 115 | }); 116 | } 117 | 118 | function updateSensorUI(sensor) { 119 | var el = $(".sensor-" + sensor.plugin + "-" + sensor.addr); 120 | el.data(sensor); 121 | el.find(".name").text(sensor.addr); 122 | el.find(".value").text(sensor.value); 123 | el.find(".unit").text(sensor.unit); 124 | 125 | el.find(".sensor-monitor").unbind().click(function() { 126 | // "Monitor"); 127 | window.open(getFrontend() + "?uuid[]=" + sensor.uuid, "frontend"); 128 | }); 129 | 130 | if (sensor.uuid) { 131 | el.find(".sensor-connect").addClass("hide"); 132 | el.find(".sensor-monitor, .sensor-disconnect, .sensor-delete").removeClass("hide"); 133 | el.find(".sensor-disconnect").unbind().click(function() { 134 | disconnectSensor(sensor); 135 | }); 136 | el.find(".sensor-delete").unbind().click(function() { 137 | disconnectSensor(sensor, true); 138 | }); 139 | } 140 | else { 141 | el.find(".sensor-monitor, .sensor-disconnect, .sensor-delete").addClass("hide"); 142 | el.find(".sensor-connect").removeClass("hide").unbind().click(function() { 143 | connectSensor(sensor); 144 | }); 145 | } 146 | 147 | // update monitor all button 148 | if ($(".state-sensors .sensor:not(.template)").length) { 149 | $(".state-sensors .sensor-frontend").removeClass("secondary"); 150 | } 151 | else { 152 | $(".state-sensors .sensor-frontend").addClass("secondary"); 153 | } 154 | 155 | $(".state-sensors .sensor:not(.template)").each(function(i, el) { 156 | console.log($(el).data()); 157 | }); 158 | } 159 | 160 | function connectSensor(sensor) { 161 | // 1) check if sensor already exists at middleware 162 | var deferredUuid = $.ajax(getMiddleware() + "/iot/" + sensor.hash + ".json").then(function(json) { 163 | if (json.entities && json.entities.length === 1) { 164 | // exactly one match - resolve uuid 165 | return $.Deferred().resolveWith(this, [json.entities[0].uuid]); 166 | } 167 | if (json.entities === undefined) { 168 | // wrong mw version 169 | console.warn("FOOOOO"); // @TODO 170 | notify("warning", "Middleware failed", "Could not perform middleware operation. Check middleware version (needs d63104b)."); 171 | } 172 | 173 | // 2) if not create channel 174 | var data = { 175 | operation: "add", 176 | type: getSensorType(sensor), 177 | title: sensor.addr, 178 | owner: sensor.hash, // remember sensor hash 179 | style: "lines", 180 | resolution: 1 181 | }; 182 | 183 | return $.ajax(getMiddleware() + "/channel.json?" + $.param(data)).then(function(json) { 184 | if (json.entity !== undefined && json.entity.uuid !== undefined) { 185 | return $.Deferred().resolveWith(this, [json.entity.uuid]); 186 | } 187 | notify("error", "Sensor not connected", "Could not connect sensor " + sensor.addr + " to the middleware. Middleware says: '" + json.exception.message + "'"); 188 | return $.Deferred().reject(); 189 | }, function() { 190 | notify("error", "Sensor not connected", "Could not connect sensor " + sensor.addr + " to the middleware."); 191 | return $.Deferred().reject(); 192 | }); 193 | }, function() { 194 | notify("error", "Middleware error", "Could not connect to middleware."); 195 | }); 196 | 197 | // 3) update sensor config with uuid 198 | deferredUuid.done(function(uuid) { 199 | sensor.uuid = uuid; 200 | $.ajax(getSensorApi(sensor) + "?" + $.param({ 201 | uuid: sensor.uuid 202 | })) 203 | .done(function(json) { 204 | notify("success", "Sensor associated", "The sensor " + sensor.addr + " is now successfully connected. Sensor data will be directly logged to the middleware."); 205 | updateSensorUI(sensor); 206 | }) 207 | .fail(function() { 208 | notify("error", "Sensor not connected", "Failed to update sensor " + sensor.addr + " with middleware identifier."); 209 | }); 210 | }); 211 | } 212 | 213 | function disconnectSensor(sensor, fullDelete) { 214 | var deferred = (fullDelete) ? 215 | $.ajax(getMiddleware() + "/channel/" + sensor.uuid + ".json?" + $.param({ 216 | operation: "delete" 217 | })) : 218 | $.Deferred().resolveWith(this, [{}]); 219 | 220 | deferred.done(function(json) { 221 | if (json.exception !== undefined) { 222 | var msg = "Could not delete sensor " + sensor.addr + " from middleware. Sensor will be disconnected instead. "; 223 | msg += "Middleware says: '" + json.exception.message + "'"; 224 | notify("error", "Sensor not deleted", msg); 225 | } 226 | 227 | // clear uuid from vzero 228 | $.ajax(getSensorApi(sensor) + "?" + $.param({ 229 | uuid: "" 230 | })) 231 | .done(function(json) { 232 | delete sensor.uuid; 233 | notify("success", "Sensor disconnected", "The sensor " + sensor.addr + " has been successfully disconnected. Sensor data will no longer be logged to the middleware."); 234 | updateSensorUI(sensor); 235 | }) 236 | .fail(function() { 237 | notify("error", "Sensor not disconnected", "Failed to delete middleware identifier from sensor " + sensor.addr + "."); 238 | }); 239 | }) 240 | .fail(function() { 241 | notify("error", "Sensor not disconnected", "Could not disconnect sensor " + sensor.addr + " from the middleware."); 242 | }); 243 | } 244 | 245 | function updateSensors() { 246 | $.ajax(getApi("/api/plugins"), { 247 | timeout: options.timeout 248 | }) 249 | .done(function(json) { 250 | // plugins 251 | $.each(json, function(i, plugin) { 252 | // sensors 253 | $.each(plugin.sensors, function(j, sensor) { 254 | $(".sensor-" + plugin.name + "-" + sensor.addr + " .value").text(sensor.value); 255 | }); 256 | }); 257 | }) 258 | .fail(function() { 259 | notify("warning", "No connection", "Could not update sensors from VZero."); 260 | }); 261 | } 262 | 263 | function getSensorType(sensor) { 264 | switch (sensor.plugin) { 265 | case "1wire": 266 | return "temperature"; 267 | case "dht": 268 | if (sensor.addr == "temp") { 269 | sensor.unit = "°C"; 270 | return "temperature"; 271 | } 272 | sensor.unit = "%"; 273 | return "humidity"; 274 | case "analog": 275 | return "voltage"; 276 | case "wifi": 277 | return "rssi"; 278 | default: 279 | return "powersensor"; 280 | } 281 | } 282 | 283 | function getApi(api) { 284 | return options.api + api; 285 | } 286 | 287 | function getSensorApi(data) { 288 | return getApi("/api/" + data.plugin + "/" + data.addr); 289 | } 290 | 291 | function getMiddleware() { 292 | var mw = $(".middleware").val(); 293 | if (!mw) { 294 | mw = "http://localhost:8888/vz/htdocs/middleware.php"; 295 | $(".middleware").val(mw); 296 | } 297 | if (mw.length && mw[mw.length-1] == '/') { 298 | mw = mw.substring(0, mw.length-1); 299 | } 300 | return mw; 301 | } 302 | 303 | function getFrontend() { 304 | var mw = getMiddleware(); 305 | var i = mw.indexOf("middleware"); 306 | if (i >= 0) { 307 | mw = mw.substring(0, i-1); 308 | } 309 | if (mw.length && mw[mw.length-1] == '/') { 310 | mw = mw.substring(0, mw.length-1); 311 | } 312 | mw += "/frontend"; 313 | return mw; 314 | } 315 | 316 | function heartBeat(initial) { 317 | return $.ajax(getApi('/api/status' + (initial ? "?initial=1" : "")), { 318 | timeout: options.timeout 319 | }) 320 | .done(function(json) { 321 | // [0: "DEFAULT", 1: "WDT", 2: "EXCEPTION", 3: "SOFT_WDT", 4: "SOFT_RESTART", 5: "DEEP_SLEEP_AWAKE", 6: "EXT_SYS_RST"] 322 | if (json.resetcode == 1 || json.resetcode == 3) { 323 | notify("error", "Unexpected restart", "The VZero has experienced an unexpected restart, triggered by the built-in watch dog timer."); 324 | } 325 | else if (json.resetcode == 2) { 326 | notify("error", "Unexpected restart", "The VZero has experienced an unexpected restart, caused by an exception."); 327 | } 328 | else if (json.resetcode == 4 && json.uptime < 30000) { 329 | notify("warning", "Restart", "The VZero was restarted."); 330 | } 331 | 332 | var heap = json.heap; 333 | if (heap < 8192) { 334 | notify("warning", "Low memory", "Available memory has reached a critical limit. VZero might become unstable."); 335 | } 336 | if (heap > 1024) { 337 | heap = Math.round(heap / 1024) + 'kB'; 338 | } 339 | if (json.minheap) { 340 | heap += " (min " + Math.round(json.minheap / 1024) + 'kB' + ")"; 341 | } 342 | $(".heap").text(heap); 343 | 344 | var flash = json.flash; 345 | if (flash > 1024) { 346 | flash = Math.round(flash / 1024) + 'kB'; 347 | } 348 | $(".flash").text(flash); 349 | 350 | var date = new Date(json.uptime); 351 | var hours = parseInt(date / 3.6e6) % 24; 352 | $(".uptime").text( 353 | (hours < 10 ? "0" + hours : hours) +":"+ ("0"+date.getMinutes()).slice(-2) +":"+ ("0"+date.getSeconds()).slice(-2) 354 | ); 355 | 356 | if (sparkline) { 357 | sparkdata.push(json.heap); 358 | if (sparkdata.length > 50) { 359 | sparkdata.shift(); 360 | } 361 | sparkline.draw(sparkdata); 362 | } 363 | return $.Deferred().resolveWith(this, [json]); 364 | }) 365 | .fail(function() { 366 | notify("warning", "No connection", "Could not update status from VZero."); 367 | }); 368 | } 369 | 370 | function notify(type, title, message, force) { 371 | var hash = "hash" + hashCode(message); 372 | if (force) hash += Math.floor(Math.random() * Number.MAX_SAFE_INTEGER); 373 | var el = $("." + hash); 374 | if (el.length > 0) { 375 | el.data({created: Date.now()}); 376 | return hash; 377 | } 378 | // unhide message area 379 | $(".footer-container").removeClass("hide"); 380 | // add message 381 | el = template("." + type, ".messages").addClass(hash).data({created: Date.now()}); 382 | el.find(".title").text(title); 383 | el.find(".message").text(message); 384 | return hash; 385 | } 386 | 387 | function menu(sel) { 388 | $(".menu a").removeClass("em"); 389 | $(".menu a[href=#" + sel + "]").addClass("em"); 390 | 391 | $("body > .column.row:not(.state-always)").addClass("hide"); 392 | $(".column.row.state-" + sel + ", .column.row.state-menu").removeClass("hide"); 393 | } 394 | 395 | function connectDevice() { 396 | heartBeat(true).done(function(json) { 397 | $('.loader').remove(); 398 | $('.content > *').unwrap(); 399 | 400 | var addr = "http://vzero-" + json.serial + ".local"; 401 | $("a.address").text(addr); 402 | $("a.address").attr("href", addr); 403 | 404 | $('title').text($('title').text() + " (" + json.serial + ")"); 405 | $('.ip').text(json.ip); 406 | $('.wifimode').text(json.wifimode); 407 | $('.serial').text(json.serial); 408 | $('.build').text(json.build); 409 | $('.ssid').val(json.ssid); 410 | $('.pass').val(json.pass); 411 | $('.middleware').val(json.middleware); 412 | 413 | // initial setup - wifi 414 | if ($(".wifimode").text() == "Access Point" || options.test.stage == "wifi") { 415 | $(".column.row:not(.state-always), .menu-container").addClass("hide"); 416 | $(".state-initial-wifi").removeClass("hide"); 417 | } 418 | // initial setup - middleware 419 | else if ($(".middleware").val().trim() === "" || options.test.stage == "middleware") { 420 | $(".middleware").val("http://demo.volkszaehler.org/middleware.php"); 421 | $(".column.row:not(.state-always), .menu-container").addClass("hide"); 422 | $(".state-initial-middleware").removeClass("hide"); 423 | } 424 | else { 425 | menu(window.location.hash.substr(1) || "home"); 426 | $(".menu a").click(function() { 427 | menu($(this).attr("href").slice(1)); 428 | }); 429 | 430 | sparkline = new Sparkline($(".spark")[0], { 431 | height: 30, 432 | width: $(".spark").width(), 433 | lineColor: "#666", 434 | endColor: "#0292C0", 435 | min: 0 436 | }); 437 | 438 | // read plugins and setup sensor update 439 | initializePlugins().done(function() { 440 | window.setInterval(function() { 441 | updateSensors(); 442 | }, options.sensors.interval); 443 | }); 444 | 445 | // setup heartbeat update 446 | window.setInterval(function() { 447 | heartBeat(); 448 | }, options.heartBeat.interval); 449 | 450 | window.setInterval(function() { 451 | var now = Date.now(); 452 | $(".messages .row").each(function(i, el) { 453 | if (now - $(el).data().created > options.messages.timeout) { 454 | $(el).fadeOut("slow", function() { 455 | $(el).remove(); 456 | if ($(".messages .row").length === 0) { 457 | $(".messages").addClass("hide"); 458 | } 459 | }); 460 | } 461 | }); 462 | }, 2000); 463 | } 464 | }).fail(function() { 465 | window.setTimeout(connectDevice, 200); 466 | }); 467 | } 468 | 469 | // dom ready 470 | $(function() { 471 | // url parameters 472 | var params = getUrlParams(); 473 | $.extend(options, params); 474 | console.warn(options); 475 | 476 | // js loaded - update UI 477 | $('.loader .subheader').text("connecting..."); 478 | connectDevice(); 479 | }); 480 | })(window.jQuery, window, document); 481 | -------------------------------------------------------------------------------- /data/js/nprogress.js: -------------------------------------------------------------------------------- 1 | /* NProgress, (c) 2013, 2014 Rico Sta. Cruz - http://ricostacruz.com/nprogress 2 | * @license MIT */ 3 | 4 | ;(function(root, factory) { 5 | 6 | if (typeof define === 'function' && define.amd) { 7 | define(factory); 8 | } else if (typeof exports === 'object') { 9 | module.exports = factory(); 10 | } else { 11 | root.NProgress = factory(); 12 | } 13 | 14 | })(this, function() { 15 | var NProgress = {}; 16 | 17 | NProgress.version = '0.2.0'; 18 | 19 | var Settings = NProgress.settings = { 20 | minimum: 0.08, 21 | easing: 'linear', 22 | positionUsing: '', 23 | speed: 350, 24 | trickle: true, 25 | trickleSpeed: 250, 26 | showSpinner: true, 27 | barSelector: '[role="bar"]', 28 | spinnerSelector: '[role="spinner"]', 29 | parent: 'body', 30 | template: '
' 31 | }; 32 | 33 | /** 34 | * Updates configuration. 35 | * 36 | * NProgress.configure({ 37 | * minimum: 0.1 38 | * }); 39 | */ 40 | NProgress.configure = function(options) { 41 | var key, value; 42 | for (key in options) { 43 | value = options[key]; 44 | if (value !== undefined && options.hasOwnProperty(key)) Settings[key] = value; 45 | } 46 | 47 | return this; 48 | }; 49 | 50 | /** 51 | * Last number. 52 | */ 53 | 54 | NProgress.status = null; 55 | 56 | /** 57 | * Sets the progress bar status, where `n` is a number from `0.0` to `1.0`. 58 | * 59 | * NProgress.set(0.4); 60 | * NProgress.set(1.0); 61 | */ 62 | 63 | NProgress.set = function(n) { 64 | var started = NProgress.isStarted(); 65 | 66 | n = clamp(n, Settings.minimum, 1); 67 | NProgress.status = (n === 1 ? null : n); 68 | 69 | var progress = NProgress.render(!started), 70 | bar = progress.querySelector(Settings.barSelector), 71 | speed = Settings.speed, 72 | ease = Settings.easing; 73 | 74 | progress.offsetWidth; /* Repaint */ 75 | 76 | queue(function(next) { 77 | // Set positionUsing if it hasn't already been set 78 | if (Settings.positionUsing === '') Settings.positionUsing = NProgress.getPositioningCSS(); 79 | 80 | // Add transition 81 | css(bar, barPositionCSS(n, speed, ease)); 82 | 83 | if (n === 1) { 84 | // Fade out 85 | css(progress, { 86 | transition: 'none', 87 | opacity: 1 88 | }); 89 | progress.offsetWidth; /* Repaint */ 90 | 91 | setTimeout(function() { 92 | css(progress, { 93 | transition: 'all ' + speed + 'ms linear', 94 | opacity: 0 95 | }); 96 | setTimeout(function() { 97 | NProgress.remove(); 98 | next(); 99 | }, speed); 100 | }, speed); 101 | } else { 102 | setTimeout(next, speed); 103 | } 104 | }); 105 | 106 | return this; 107 | }; 108 | 109 | NProgress.isStarted = function() { 110 | return typeof NProgress.status === 'number'; 111 | }; 112 | 113 | /** 114 | * Shows the progress bar. 115 | * This is the same as setting the status to 0%, except that it doesn't go backwards. 116 | * 117 | * NProgress.start(); 118 | * 119 | */ 120 | NProgress.start = function() { 121 | if (!NProgress.status) NProgress.set(0); 122 | 123 | var work = function() { 124 | setTimeout(function() { 125 | if (!NProgress.status) return; 126 | NProgress.trickle(); 127 | work(); 128 | }, Settings.trickleSpeed); 129 | }; 130 | 131 | if (Settings.trickle) work(); 132 | 133 | return this; 134 | }; 135 | 136 | /** 137 | * Hides the progress bar. 138 | * This is the *sort of* the same as setting the status to 100%, with the 139 | * difference being `done()` makes some placebo effect of some realistic motion. 140 | * 141 | * NProgress.done(); 142 | * 143 | * If `true` is passed, it will show the progress bar even if its hidden. 144 | * 145 | * NProgress.done(true); 146 | */ 147 | 148 | NProgress.done = function(force) { 149 | if (!force && !NProgress.status) return this; 150 | 151 | return NProgress.inc(0.3 + 0.5 * Math.random()).set(1); 152 | }; 153 | 154 | /** 155 | * Increments by a random amount. 156 | */ 157 | 158 | NProgress.inc = function(amount) { 159 | var n = NProgress.status; 160 | 161 | if (!n) { 162 | return NProgress.start(); 163 | } else if(n > 1) { 164 | return; 165 | } else { 166 | if (typeof amount !== 'number') { 167 | if (n >= 0 && n < 0.25) { 168 | // Start out between 3 - 6% increments 169 | amount = (Math.random() * (5 - 3 + 1) + 3) / 100; 170 | } else if (n >= 0.25 && n < 0.65) { 171 | // increment between 0 - 3% 172 | amount = (Math.random() * 3) / 100; 173 | } else if (n >= 0.65 && n < 0.9) { 174 | // increment between 0 - 2% 175 | amount = (Math.random() * 2) / 100; 176 | } else if (n >= 0.9 && n < 0.99) { 177 | // finally, increment it .5 % 178 | amount = 0.005; 179 | } else { 180 | // after 99%, don't increment: 181 | amount = 0; 182 | } 183 | } 184 | 185 | n = clamp(n + amount, 0, 0.994); 186 | return NProgress.set(n); 187 | } 188 | }; 189 | 190 | NProgress.trickle = function() { 191 | return NProgress.inc(); 192 | }; 193 | 194 | /** 195 | * Waits for all supplied jQuery promises and 196 | * increases the progress as the promises resolve. 197 | * 198 | * @param $promise jQUery Promise 199 | */ 200 | (function() { 201 | var initial = 0, current = 0; 202 | 203 | NProgress.promise = function($promise) { 204 | if (!$promise || $promise.state() === "resolved") { 205 | return this; 206 | } 207 | 208 | if (current === 0) { 209 | NProgress.start(); 210 | } 211 | 212 | initial++; 213 | current++; 214 | 215 | $promise.always(function() { 216 | current--; 217 | if (current === 0) { 218 | initial = 0; 219 | NProgress.done(); 220 | } else { 221 | NProgress.set((initial - current) / initial); 222 | } 223 | }); 224 | 225 | return this; 226 | }; 227 | 228 | })(); 229 | 230 | /** 231 | * (Internal) renders the progress bar markup based on the `template` 232 | * setting. 233 | */ 234 | 235 | NProgress.render = function(fromStart) { 236 | if (NProgress.isRendered()) return document.getElementById('nprogress'); 237 | 238 | addClass(document.documentElement, 'nprogress-busy'); 239 | 240 | var progress = document.createElement('div'); 241 | progress.id = 'nprogress'; 242 | progress.innerHTML = Settings.template; 243 | 244 | var bar = progress.querySelector(Settings.barSelector), 245 | perc = fromStart ? '-100' : toBarPerc(NProgress.status || 0), 246 | parent = document.querySelector(Settings.parent), 247 | spinner; 248 | 249 | css(bar, { 250 | transition: 'all 0 linear', 251 | transform: 'translate3d(' + perc + '%,0,0)' 252 | }); 253 | 254 | if (!Settings.showSpinner) { 255 | spinner = progress.querySelector(Settings.spinnerSelector); 256 | spinner && removeElement(spinner); 257 | } 258 | 259 | if (parent != document.body) { 260 | addClass(parent, 'nprogress-custom-parent'); 261 | } 262 | 263 | parent.appendChild(progress); 264 | return progress; 265 | }; 266 | 267 | /** 268 | * Removes the element. Opposite of render(). 269 | */ 270 | 271 | NProgress.remove = function() { 272 | removeClass(document.documentElement, 'nprogress-busy'); 273 | removeClass(document.querySelector(Settings.parent), 'nprogress-custom-parent'); 274 | var progress = document.getElementById('nprogress'); 275 | progress && removeElement(progress); 276 | }; 277 | 278 | /** 279 | * Checks if the progress bar is rendered. 280 | */ 281 | 282 | NProgress.isRendered = function() { 283 | return !!document.getElementById('nprogress'); 284 | }; 285 | 286 | /** 287 | * Determine which positioning CSS rule to use. 288 | */ 289 | 290 | NProgress.getPositioningCSS = function() { 291 | // Sniff on document.body.style 292 | var bodyStyle = document.body.style; 293 | 294 | // Sniff prefixes 295 | var vendorPrefix = ('WebkitTransform' in bodyStyle) ? 'Webkit' : 296 | ('MozTransform' in bodyStyle) ? 'Moz' : 297 | ('msTransform' in bodyStyle) ? 'ms' : 298 | ('OTransform' in bodyStyle) ? 'O' : ''; 299 | 300 | if (vendorPrefix + 'Perspective' in bodyStyle) { 301 | // Modern browsers with 3D support, e.g. Webkit, IE10 302 | return 'translate3d'; 303 | } else if (vendorPrefix + 'Transform' in bodyStyle) { 304 | // Browsers without 3D support, e.g. IE9 305 | return 'translate'; 306 | } else { 307 | // Browsers without translate() support, e.g. IE7-8 308 | return 'margin'; 309 | } 310 | }; 311 | 312 | /** 313 | * Helpers 314 | */ 315 | 316 | function clamp(n, min, max) { 317 | if (n < min) return min; 318 | if (n > max) return max; 319 | return n; 320 | } 321 | 322 | /** 323 | * (Internal) converts a percentage (`0..1`) to a bar translateX 324 | * percentage (`-100%..0%`). 325 | */ 326 | 327 | function toBarPerc(n) { 328 | return (-1 + n) * 100; 329 | } 330 | 331 | 332 | /** 333 | * (Internal) returns the correct CSS for changing the bar's 334 | * position given an n percentage, and speed and ease from Settings 335 | */ 336 | 337 | function barPositionCSS(n, speed, ease) { 338 | var barCSS; 339 | 340 | if (Settings.positionUsing === 'translate3d') { 341 | barCSS = { transform: 'translate3d('+toBarPerc(n)+'%,0,0)' }; 342 | } else if (Settings.positionUsing === 'translate') { 343 | barCSS = { transform: 'translate('+toBarPerc(n)+'%,0)' }; 344 | } else { 345 | barCSS = { 'margin-left': toBarPerc(n)+'%' }; 346 | } 347 | 348 | barCSS.transition = 'all '+speed+'ms '+ease; 349 | 350 | return barCSS; 351 | } 352 | 353 | /** 354 | * (Internal) Queues a function to be executed. 355 | */ 356 | 357 | var queue = (function() { 358 | var pending = []; 359 | 360 | function next() { 361 | var fn = pending.shift(); 362 | if (fn) { 363 | fn(next); 364 | } 365 | } 366 | 367 | return function(fn) { 368 | pending.push(fn); 369 | if (pending.length == 1) next(); 370 | }; 371 | })(); 372 | 373 | /** 374 | * (Internal) Applies css properties to an element, similar to the jQuery 375 | * css method. 376 | * 377 | * While this helper does assist with vendor prefixed property names, it 378 | * does not perform any manipulation of values prior to setting styles. 379 | */ 380 | 381 | var css = (function() { 382 | var cssPrefixes = [ 'Webkit', 'O', 'Moz', 'ms' ], 383 | cssProps = {}; 384 | 385 | function camelCase(string) { 386 | return string.replace(/^-ms-/, 'ms-').replace(/-([\da-z])/gi, function(match, letter) { 387 | return letter.toUpperCase(); 388 | }); 389 | } 390 | 391 | function getVendorProp(name) { 392 | var style = document.body.style; 393 | if (name in style) return name; 394 | 395 | var i = cssPrefixes.length, 396 | capName = name.charAt(0).toUpperCase() + name.slice(1), 397 | vendorName; 398 | while (i--) { 399 | vendorName = cssPrefixes[i] + capName; 400 | if (vendorName in style) return vendorName; 401 | } 402 | 403 | return name; 404 | } 405 | 406 | function getStyleProp(name) { 407 | name = camelCase(name); 408 | return cssProps[name] || (cssProps[name] = getVendorProp(name)); 409 | } 410 | 411 | function applyCss(element, prop, value) { 412 | prop = getStyleProp(prop); 413 | element.style[prop] = value; 414 | } 415 | 416 | return function(element, properties) { 417 | var args = arguments, 418 | prop, 419 | value; 420 | 421 | if (args.length == 2) { 422 | for (prop in properties) { 423 | value = properties[prop]; 424 | if (value !== undefined && properties.hasOwnProperty(prop)) applyCss(element, prop, value); 425 | } 426 | } else { 427 | applyCss(element, args[1], args[2]); 428 | } 429 | } 430 | })(); 431 | 432 | /** 433 | * (Internal) Determines if an element or space separated list of class names contains a class name. 434 | */ 435 | 436 | function hasClass(element, name) { 437 | var list = typeof element == 'string' ? element : classList(element); 438 | return list.indexOf(' ' + name + ' ') >= 0; 439 | } 440 | 441 | /** 442 | * (Internal) Adds a class to an element. 443 | */ 444 | 445 | function addClass(element, name) { 446 | var oldList = classList(element), 447 | newList = oldList + name; 448 | 449 | if (hasClass(oldList, name)) return; 450 | 451 | // Trim the opening space. 452 | element.className = newList.substring(1); 453 | } 454 | 455 | /** 456 | * (Internal) Removes a class from an element. 457 | */ 458 | 459 | function removeClass(element, name) { 460 | var oldList = classList(element), 461 | newList; 462 | 463 | if (!hasClass(element, name)) return; 464 | 465 | // Replace the class name. 466 | newList = oldList.replace(' ' + name + ' ', ' '); 467 | 468 | // Trim the opening and closing spaces. 469 | element.className = newList.substring(1, newList.length - 1); 470 | } 471 | 472 | /** 473 | * (Internal) Gets a space separated list of the class names on the element. 474 | * The list is wrapped with a single space on each end to facilitate finding 475 | * matches within the list. 476 | */ 477 | 478 | function classList(element) { 479 | return (' ' + (element && element.className || '') + ' ').replace(/\s+/gi, ' '); 480 | } 481 | 482 | /** 483 | * (Internal) Removes an element from the DOM. 484 | */ 485 | 486 | function removeElement(element) { 487 | element && element.parentNode && element.parentNode.removeChild(element); 488 | } 489 | 490 | return NProgress; 491 | }); 492 | -------------------------------------------------------------------------------- /data/js/sparkline.js: -------------------------------------------------------------------------------- 1 | (function (root, factory) { 2 | if (typeof define === 'function' && define.amd) { 3 | // AMD. Register as an anonymous module. 4 | define(factory); 5 | } else if (typeof exports === 'object') { 6 | // Node. Does not work with strict CommonJS, but 7 | // only CommonJS-like enviroments that support module.exports, 8 | // like Node. 9 | module.exports = factory(); 10 | } else { 11 | // Browser globals (root is window) 12 | root.Sparkline = factory(); 13 | } 14 | }(this, function () { 15 | 16 | 17 | function extend(specific, general){ 18 | var obj = {}; 19 | for(var key in general){ 20 | obj[key] = key in specific ? specific[key] : general[key]; 21 | } 22 | return obj; 23 | } 24 | 25 | function Sparkline(element, options){ 26 | this.element = element; 27 | this.options = extend(options || {}, Sparkline.options); 28 | 29 | init: { 30 | this.element.innerHTML = ""; 31 | this.canvas = this.element.firstChild; 32 | this.context = this.canvas.getContext("2d"); 33 | this.ratio = window.devicePixelRatio || 1; 34 | if(this.options.tooltip){ 35 | this.canvas.style.position = "relative"; 36 | this.canvas.onmousemove = showTooltip.bind(this); 37 | } 38 | } 39 | } 40 | 41 | Sparkline.options = { 42 | width: 100, 43 | height: null, 44 | lineColor: "black", 45 | lineWidth: 1, 46 | startColor: "transparent", 47 | endColor: "red", 48 | maxColor: "transparent", 49 | minColor: "transparent", 50 | minValue: null, 51 | maxValue: null, 52 | dotRadius: 2.5, 53 | tooltip: null 54 | }; 55 | 56 | Sparkline.init = function(element, options){ 57 | return new Sparkline(element, options); 58 | }; 59 | 60 | Sparkline.draw = function(element, points, options){ 61 | var sparkline = new Sparkline(element, options); 62 | sparkline.draw(points); 63 | return sparkline; 64 | }; 65 | 66 | function getY(minValue, maxValue, offsetY, height, index){ 67 | var range = maxValue - minValue; 68 | if(range === 0){ 69 | return offsetY + height/2; 70 | }else{ 71 | return (offsetY + height) - ((this[index] - minValue) / range)*height; 72 | } 73 | } 74 | 75 | function drawDot(radius, color, x, y){ 76 | this.beginPath(); 77 | this.fillStyle = color; 78 | this.arc(x, y, radius, 0, Math.PI*2, false); 79 | this.fill(); 80 | } 81 | 82 | function showTooltip(e){ 83 | var x = e.offsetX || e.layerX || 0; 84 | var delta = ((this.options.width - this.options.dotRadius*2) / (this.points.length - 1)); 85 | var index = minmax(0, Math.round((x - this.options.dotRadius)/delta), this.points.length - 1); 86 | 87 | this.canvas.title = this.options.tooltip(this.points[index], index, this.points); 88 | } 89 | 90 | Sparkline.prototype.draw = function(points){ 91 | points = points || []; 92 | this.points = points; 93 | 94 | this.canvas.width = this.options.width * this.ratio; 95 | this.canvas.style.width = this.options.width + 'px'; 96 | 97 | var pxHeight = this.options.height || this.element.offsetHeight; 98 | this.canvas.height = pxHeight * this.ratio; 99 | this.canvas.style.height = pxHeight + 'px'; 100 | 101 | var offsetX = this.options.dotRadius*this.ratio; 102 | var offsetY = this.options.dotRadius*this.ratio; 103 | var width = this.canvas.width - offsetX*2; 104 | var height = this.canvas.height - offsetY*2; 105 | 106 | var minValue = this.options.minValue || Math.min.apply(Math, points); 107 | var maxValue = this.options.maxValue || Math.max.apply(Math, points); 108 | var minX = offsetX; 109 | var maxX = offsetX; 110 | 111 | var x = offsetX; 112 | var y = getY.bind(points, minValue, maxValue, offsetY, height); 113 | var delta = width / (points.length - 1); 114 | 115 | var dot = drawDot.bind(this.context, this.options.dotRadius*this.ratio); 116 | 117 | 118 | this.context.beginPath(); 119 | this.context.strokeStyle = this.options.lineColor; 120 | this.context.lineWidth = this.options.lineWidth*this.ratio; 121 | 122 | this.context.moveTo(x, y(0)); 123 | for(var i=1; i", 14 | "devDependencies": { 15 | "del": "^2.2.0", 16 | "gulp": "^3.8.10", 17 | "gulp-gzip": "^1.2.0" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /platformio.ini: -------------------------------------------------------------------------------- 1 | [platformio] 2 | env_default=esp8266 3 | env_default=esp32 4 | 5 | [common_env_data] 6 | lib_deps= 7 | ESP Async WebServer@^1.1 8 | OneWire@^2.3 9 | Adafruit Unified Sensor@^1.0 10 | DHT sensor library@^1.3 11 | DallasTemperature@^3.7 12 | ArduinoJson@^5.1 13 | 14 | [env:esp8266] 15 | #platform=espressif8266 16 | platform=https://github.com/platformio/platform-espressif8266.git#feature/stage 17 | board=esp12e 18 | framework=arduino 19 | extra_scripts=build-helper.py 20 | # lib_compat_mode=1 allows non-git versions of DHT and ESPAsyncWebServer@^1.1 21 | lib_compat_mode=light 22 | lib_ldf_mode=deep 23 | build_flags=-Tesp8266.flash.4m1m.ld 24 | upload_port=vzero-edd834.local 25 | #targets=upload 26 | lib_deps= 27 | ${common_env_data.lib_deps} 28 | ESPAsyncTCP@^1.1.2 29 | 30 | [env:esp32] 31 | # use stage version for added SPIFFS support 32 | #platform=espressif32 33 | platform=https://github.com/platformio/platform-espressif32.git 34 | board=featheresp32 35 | framework=arduino 36 | # lib_compat_mode=2 to avoid including ESPAsyncTCP 37 | extra_scripts=build-helper.py 38 | lib_compat_mode=strict 39 | lib_ldf_mode=deep 40 | lib_deps= 41 | ${common_env_data.lib_deps} 42 | # AsyncTCP@^1.0 43 | https://github.com/me-no-dev/AsyncTCP -------------------------------------------------------------------------------- /src/config.cpp: -------------------------------------------------------------------------------- 1 | /** 2 | * Config file 3 | */ 4 | 5 | #ifdef ESP8266 6 | #include 7 | #endif 8 | 9 | #ifdef ESP32 10 | #include 11 | #include "SPIFFS.h" 12 | #include 13 | #endif 14 | 15 | #include 16 | #include 17 | #include 18 | #include 19 | 20 | #include "config.h" 21 | 22 | #ifdef PLUGIN_ONEWIRE 23 | #include "plugins/OneWirePlugin.h" 24 | #endif 25 | 26 | #ifdef PLUGIN_DHT 27 | #include "plugins/DHTPlugin.h" 28 | #endif 29 | 30 | #ifdef PLUGIN_ANALOG 31 | #include "plugins/AnalogPlugin.h" 32 | #endif 33 | 34 | #ifdef PLUGIN_WIFI 35 | #include "plugins/WifiPlugin.h" 36 | #endif 37 | 38 | // default AP SSID 39 | const char* ap_default_ssid = "VZERO"; 40 | 41 | // global vars 42 | #ifdef ESP8266 43 | rst_info* g_resetInfo; 44 | #endif 45 | String net_hostname = "vzero"; 46 | uint32_t g_minFreeHeap = -1; 47 | 48 | // global settings 49 | String g_ssid = ""; 50 | String g_pass = ""; 51 | String g_middleware = ""; 52 | 53 | 54 | #ifdef DEBUG 55 | void debug_plain(const char *msg) { 56 | ets_printf(msg); 57 | } 58 | 59 | /** 60 | * Verbose debug output 61 | */ 62 | void debug_message(const char *module, const char *format, ...) { 63 | #define BUFFER_SIZE 150 64 | char buf[BUFFER_SIZE]; 65 | ets_printf("%08d [%-6s] ", millis(), module); 66 | // snprintf(buf, BUFFER_SIZE, "%6.3f [%-6s] ", millis()/1000.0, module); 67 | 68 | va_list args; 69 | va_start(args, format); 70 | vsnprintf(buf, BUFFER_SIZE, format, args); 71 | ets_printf(buf); 72 | va_end(args); 73 | 74 | if (ESP.getFreeHeap() < g_minFreeHeap) { 75 | g_minFreeHeap = ESP.getFreeHeap(); 76 | } 77 | } 78 | #endif 79 | 80 | long getChipId() 81 | { 82 | #ifdef ESP8266 83 | return ESP.getChipId(); 84 | #endif 85 | #ifdef ESP32 86 | long chipId; 87 | ESP.getEfuseMac(); 88 | return chipId; 89 | #endif 90 | } 91 | 92 | /** 93 | * Hash builder initialized with unique module identifiers 94 | */ 95 | MD5Builder getHashBuilder() 96 | { 97 | uint8_t mac[6]; 98 | 99 | MD5Builder md5; 100 | md5.begin(); 101 | 102 | uint64_t chipId = getChipId(); 103 | md5.add((uint8_t*)&chipId, 4); 104 | 105 | #ifdef ESP8266 106 | uint32_t flashId = ESP.getFlashChipId(); 107 | md5.add((uint8_t*)&flashId, 2); 108 | #endif 109 | 110 | WiFi.macAddress(&mac[0]); 111 | md5.add(&mac[0], 6); 112 | 113 | return md5; 114 | } 115 | 116 | /** 117 | * Unique module identifier 118 | */ 119 | String getHash() 120 | { 121 | MD5Builder md5 = getHashBuilder(); 122 | md5.calculate(); 123 | return md5.toString(); 124 | } 125 | 126 | /** 127 | * Load config 128 | */ 129 | bool loadConfig() 130 | { 131 | DEBUG_MSG(CORE, "loading config\n"); 132 | File configFile = SPIFFS.open(F("/config.json"), "r"); 133 | if (!configFile) { 134 | DEBUG_MSG(CORE, "open config failed"); 135 | return false; 136 | } 137 | 138 | size_t size = configFile.size(); 139 | std::unique_ptr buf(new char[size]); 140 | configFile.readBytes(buf.get(), size); 141 | configFile.close(); 142 | 143 | String arg; 144 | StaticJsonBuffer<512> jsonBuffer; 145 | JsonObject& json = jsonBuffer.parseObject(buf.get()); 146 | arg = json["ssid"].as(); 147 | if (arg) g_ssid = arg; 148 | arg = json["password"].as(); 149 | if (arg) g_pass = arg; 150 | arg = json["middleware"].as(); 151 | if (arg) g_middleware = arg; 152 | 153 | DEBUG_MSG(CORE, "ssid: %s\n", g_ssid.c_str()); 154 | // DEBUG_MSG(CORE, "config psk: %s\n", g_pass.c_str()); 155 | DEBUG_MSG(CORE, "middleware: %s\n", g_middleware.c_str()); 156 | 157 | return true; 158 | } 159 | 160 | /** 161 | * Save config 162 | */ 163 | bool saveConfig() 164 | { 165 | File configFile = SPIFFS.open(F("/config.json"), "w"); 166 | if (!configFile) { 167 | DEBUG_MSG(CORE, "save config failed"); 168 | return false; 169 | } 170 | 171 | StaticJsonBuffer<512> jsonBuffer; 172 | JsonObject& json = jsonBuffer.createObject(); 173 | json["ssid"] = g_ssid; 174 | json["password"] = g_pass; 175 | json["middleware"] = g_middleware; 176 | 177 | json.printTo(configFile); 178 | configFile.close(); 179 | 180 | return true; 181 | } 182 | 183 | /** 184 | * Start enabled plugins 185 | */ 186 | void startPlugins() 187 | { 188 | DEBUG_MSG(CORE, "starting plugins\n"); 189 | #ifdef PLUGIN_ONEWIRE 190 | new OneWirePlugin(ONEWIRE_PIN); 191 | #endif 192 | #ifdef PLUGIN_DHT 193 | new DHTPlugin(DHT_PIN, DHT_TYPE); 194 | #endif 195 | #ifdef PLUGIN_ANALOG 196 | new AnalogPlugin(); 197 | #endif 198 | #ifdef PLUGIN_WIFI 199 | new WifiPlugin(); 200 | #endif 201 | } 202 | 203 | #ifdef ESP8266 204 | int getResetReason(int core) 205 | { 206 | return (int)g_resetInfo->reason; 207 | } 208 | 209 | const char* getResetReasonStr(int core) 210 | { 211 | return ESP.getResetReason().c_str(); 212 | } 213 | #endif 214 | 215 | #ifdef ESP32 216 | int getResetReason(int core) 217 | { 218 | return (int)rtc_get_reset_reason(core); 219 | } 220 | 221 | const char* getResetReasonStr(int core) 222 | { 223 | switch (rtc_get_reset_reason(core)) 224 | { 225 | case 1 : return "Vbat power on reset"; 226 | case 3 : return "Software reset digital core"; 227 | case 4 : return "Legacy watch dog reset digital core"; 228 | case 5 : return "Deep Sleep reset digital core"; 229 | case 6 : return "Reset by SLC module, reset digital core"; 230 | case 7 : return "Timer Group0 Watch dog reset digital core"; 231 | case 8 : return "Timer Group1 Watch dog reset digital core"; 232 | case 9 : return "RTC Watch dog Reset digital core"; 233 | case 10 : return "Instrusion tested to reset CPU"; 234 | case 11 : return "Time Group reset CPU"; 235 | case 12 : return "Software reset CPU"; 236 | case 13 : return "RTC Watch dog Reset CPU"; 237 | case 14 : return "for APP CPU, reseted by PRO CPU"; 238 | case 15 : return "Reset when the vdd voltage is not stable"; 239 | case 16 : return "RTC Watch dog reset digital core and rtc module"; 240 | default : return ""; 241 | } 242 | } 243 | #endif 244 | -------------------------------------------------------------------------------- /src/config.h: -------------------------------------------------------------------------------- 1 | /** 2 | * Config file 3 | */ 4 | 5 | #include 6 | #include 7 | 8 | #ifdef ESP8266 9 | extern "C" { 10 | #include 11 | #include 12 | } 13 | #endif 14 | 15 | /* 16 | * Configuration 17 | */ 18 | #define DEBUG 19 | 20 | #ifdef DEBUG 21 | void debug_plain(const char *msg); 22 | void debug_message(const char *module, const char *format, ...); 23 | 24 | #define DEBUG_PLAIN(msg) debug_plain(msg) 25 | #define DEBUG_MSG(module, format, ...) debug_message(module, format, ##__VA_ARGS__ ) 26 | #else 27 | #define DEBUG_PLAIN(msg) 28 | #define DEBUG_MSG(...) if (ESP.getFreeHeap() < g_minFreeHeap) { g_minFreeHeap = ESP.getFreeHeap(); } 29 | #endif 30 | 31 | #ifdef ESP8266 32 | #define PANIC(...) panic() 33 | #endif 34 | #ifdef ESP32 35 | #define PANIC(...) abort() 36 | #endif 37 | 38 | /* 39 | * Plugins 40 | */ 41 | #define OTA_SERVER 42 | #define CAPTIVE_PORTAL 43 | #define PLUGIN_ONEWIRE 44 | #define PLUGIN_DHT 45 | #define PLUGIN_ANALOG 46 | #define PLUGIN_WIFI 47 | 48 | // #define SPIFFS_EDITOR 49 | 50 | // settings 51 | #define ONEWIRE_PIN 14 52 | #define DHT_PIN 14 53 | #define DHT_TYPE DHT11 54 | 55 | /* 56 | * Sleep mode 57 | */ 58 | // #define DEEP_SLEEP 59 | // wakeup 5 seconds earlier 60 | #define SLEEP_SAFETY_MARGIN 1 * 1000 61 | // minimum deep sleep duration (must be bigger than SLEEP_SAFETY_MARGIN) 62 | #define MIN_SLEEP_DURATION_MS 20 * 1000 63 | // duration after boot during which no deep sleep can happen 64 | #define STARTUP_ONLINE_DURATION_MS 120 * 1000 65 | // client disconnect timeout 66 | #define WIFI_CLIENT_TIMEOUT 120 * 1000 67 | 68 | // memory management 69 | #define HTTP_MIN_HEAP 4096 70 | 71 | // other defines 72 | #define CORE "core" // module name 73 | #define BUILD "0.4.0" // version 74 | #define WIFI_CONNECT_TIMEOUT 10000 75 | #define OPTIMISTIC_YIELD_TIME 10000 76 | 77 | // ESP32 specifics 78 | #ifdef ESP32 79 | #define REASON_DEEP_SLEEP_AWAKE 5 80 | #endif 81 | 82 | /* 83 | * Variables 84 | */ 85 | 86 | // default WiFi connection information. 87 | extern const char* ap_default_ssid; // default SSID 88 | extern const char* ap_default_psk; // default PSK 89 | 90 | // global vars 91 | #ifdef ESP8266 92 | extern rst_info* g_resetInfo; 93 | #endif 94 | extern String net_hostname; 95 | extern uint32_t g_minFreeHeap; 96 | 97 | // global settings 98 | extern String g_ssid; 99 | extern String g_pass; 100 | extern String g_middleware; 101 | 102 | 103 | /* 104 | * Functions 105 | */ 106 | 107 | #ifdef DEBUG 108 | void validateFlash(); 109 | #endif 110 | 111 | void startPlugins(); 112 | 113 | long getChipId(); 114 | 115 | MD5Builder getHashBuilder(); 116 | String getHash(); 117 | 118 | bool loadConfig(); 119 | bool saveConfig(); 120 | 121 | int getResetReason(int core); 122 | const char* getResetReasonStr(int core); 123 | -------------------------------------------------------------------------------- /src/plugins/AnalogPlugin.cpp: -------------------------------------------------------------------------------- 1 | #include "AnalogPlugin.h" 2 | 3 | 4 | #define SLEEP_PERIOD 10 * 1000 5 | 6 | 7 | /* 8 | * Virtual 9 | */ 10 | 11 | AnalogPlugin::AnalogPlugin() : Plugin(1, 1) { 12 | loadConfig(); 13 | } 14 | 15 | String AnalogPlugin::getName() { 16 | return "analog"; 17 | } 18 | 19 | int8_t AnalogPlugin::getSensorByAddr(const char* addr_c) { 20 | if (strcmp(addr_c, "a0") == 0) 21 | return 0; 22 | return -1; 23 | } 24 | 25 | bool AnalogPlugin::getAddr(char* addr_c, int8_t sensor) { 26 | if (sensor >= _devs) 27 | return false; 28 | strcpy(addr_c, "a0"); 29 | return true; 30 | } 31 | 32 | float AnalogPlugin::getValue(int8_t sensor) { 33 | return analogRead(A0) / 1023.0; 34 | } 35 | 36 | /** 37 | * Loop (idle -> uploading) 38 | */ 39 | void AnalogPlugin::loop() { 40 | Plugin::loop(); 41 | 42 | if (_status == PLUGIN_IDLE && elapsed(SLEEP_PERIOD)) { 43 | _status = PLUGIN_UPLOADING; 44 | } 45 | if (_status == PLUGIN_UPLOADING) { 46 | if (isUploadSafe()) { 47 | upload(); 48 | _status = PLUGIN_IDLE; 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/plugins/AnalogPlugin.h: -------------------------------------------------------------------------------- 1 | #ifndef ANALOG_PLUGIN_H 2 | #define ANALOG_PLUGIN_H 3 | 4 | #include "Plugin.h" 5 | 6 | 7 | class AnalogPlugin : public Plugin { 8 | public: 9 | AnalogPlugin(); 10 | String getName() override; 11 | int8_t getSensorByAddr(const char* addr_c) override; 12 | bool getAddr(char* addr_c, int8_t sensor) override; 13 | float getValue(int8_t sensor) override; 14 | void loop() override; 15 | }; 16 | 17 | #endif 18 | -------------------------------------------------------------------------------- /src/plugins/DHTPlugin.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include "DHTPlugin.h" 3 | 4 | 5 | #define SLEEP_PERIOD 10 * 1000 6 | #define REQUEST_WAIT_DURATION 1 * 1000 7 | 8 | 9 | /* 10 | * Virtual 11 | */ 12 | 13 | DHTPlugin::DHTPlugin(uint8_t pin, uint8_t type) : _dht(pin, type), Plugin(2, 2) { 14 | DEBUG_MSG("dht", "plugin started\n"); 15 | loadConfig(); 16 | _dht.begin(); 17 | } 18 | 19 | String DHTPlugin::getName() { 20 | return "dht"; 21 | } 22 | 23 | int8_t DHTPlugin::getSensorByAddr(const char* addr_c) { 24 | if (strcmp(addr_c, "temp") == 0) 25 | return 0; 26 | else if (strcmp(addr_c, "humidity") == 0) 27 | return 1; 28 | return -1; 29 | } 30 | 31 | bool DHTPlugin::getAddr(char* addr_c, int8_t sensor) { 32 | if (sensor >= _devs) 33 | return false; 34 | if (sensor == 0) 35 | strcpy(addr_c, "temp"); 36 | else if (sensor == 1) 37 | strcpy(addr_c, "humidity"); 38 | return true; 39 | } 40 | 41 | float DHTPlugin::getValue(int8_t sensor) { 42 | if (sensor >= _devs) 43 | return NAN; 44 | return _devices[sensor].val; 45 | } 46 | 47 | /** 48 | * Loop (idle -> uploading) 49 | */ 50 | void DHTPlugin::loop() { 51 | Plugin::loop(); 52 | 53 | if (_status == PLUGIN_IDLE && elapsed(SLEEP_PERIOD - REQUEST_WAIT_DURATION)) { 54 | _status = PLUGIN_UPLOADING; 55 | } 56 | if (_status == PLUGIN_UPLOADING) { 57 | // force reading- valid for 2 seconds 58 | if (_dht.read(true)) { 59 | _devices[0].val = _dht.readTemperature(); 60 | _devices[1].val = _dht.readHumidity(); 61 | 62 | if (isUploadSafe()) { 63 | upload(); 64 | _status = PLUGIN_IDLE; 65 | } 66 | } 67 | else { 68 | _devices[0].val = NAN; 69 | _devices[1].val = NAN; 70 | 71 | // retry failed read only after SLEEP_PERIOD 72 | _status = PLUGIN_IDLE; 73 | DEBUG_MSG("dht", "failed reading sensors\n"); 74 | } 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/plugins/DHTPlugin.h: -------------------------------------------------------------------------------- 1 | #ifndef DHT_PLUGIN_H 2 | #define DHT_PLUGIN_H 3 | 4 | #include 5 | #include "Plugin.h" 6 | 7 | 8 | class DHTPlugin : public Plugin { 9 | public: 10 | DHTPlugin(uint8_t pin, uint8_t type); 11 | String getName() override; 12 | int8_t getSensorByAddr(const char* addr_c) override; 13 | bool getAddr(char* addr_c, int8_t sensor) override; 14 | float getValue(int8_t sensor) override; 15 | void loop() override; 16 | 17 | protected: 18 | DHT _dht; 19 | }; 20 | 21 | #endif 22 | -------------------------------------------------------------------------------- /src/plugins/OneWirePlugin.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include "OneWirePlugin.h" 3 | 4 | #ifdef ESP32 5 | #include 6 | #endif 7 | 8 | 9 | #define TEMPERATURE_PRECISION 9 10 | 11 | // plugin states 12 | #define PLUGIN_REQUESTING PLUGIN_UPLOADING + 1 13 | 14 | #define SLEEP_PERIOD 60 * 1000 15 | #define REQUEST_WAIT_DURATION 1 * 1000 16 | 17 | 18 | /* 19 | * Static 20 | */ 21 | 22 | unsigned long hex2int(const char *a, unsigned int len) 23 | { 24 | unsigned long val = 0; 25 | for (unsigned int i = 0; i < len; i++) { 26 | if (a[i] <= 57) 27 | val += (a[i]-48)*(1<<(4*(len-1-i))); 28 | else 29 | val += (a[i]-55)*(1<<(4*(len-1-i))); 30 | } 31 | return val; 32 | } 33 | 34 | bool OneWirePlugin::addrCompare(const uint8_t* a, const uint8_t* b) { 35 | for (uint8_t i=0; i<7; i++) { 36 | if (a[i] != b[i]) { 37 | return(false); 38 | } 39 | } 40 | return(true); 41 | } 42 | 43 | void OneWirePlugin::addrToStr(char* ptr, const uint8_t* addr) { 44 | for (uint8_t b=0; b<7; b++) { 45 | sprintf(ptr, "%02X", addr[b]); 46 | ptr += 2; 47 | if (b == 0) { 48 | *ptr++ = '-'; // hyphen after first hex number 49 | } 50 | } 51 | *ptr = '\0'; 52 | } 53 | 54 | void OneWirePlugin::strToAddr(const char* ptr, uint8_t* addr) { 55 | for (uint8_t b=0; b<7; b++) { 56 | addr[b] = (uint8_t)hex2int(ptr, 2); 57 | ptr += 2; 58 | if (b == 0 && *ptr == '-') { 59 | ptr++; // hyphen after first hex number 60 | } 61 | } 62 | } 63 | 64 | 65 | /* 66 | * Virtual 67 | */ 68 | 69 | OneWirePlugin::OneWirePlugin(byte pin) : _devices(), ow(pin), sensors(&ow), Plugin(0, 0) { 70 | loadConfig(); 71 | 72 | // locate _devices on the bus 73 | DEBUG_MSG("1wire", "looking for 1-Wire devices...\n"); 74 | sensors.begin(); 75 | sensors.setWaitForConversion(false); 76 | setupSensors(); 77 | 78 | // report parasite power 79 | DEBUG_MSG("1wire", "parasite power: %s\n", (sensors.isParasitePowerMode()) ? "on" : "off"); 80 | } 81 | 82 | String OneWirePlugin::getName() { 83 | return "1wire"; 84 | } 85 | 86 | int8_t OneWirePlugin::getSensorByAddr(const char* addr_c) { 87 | DeviceAddress addr; 88 | strToAddr(addr_c, addr); 89 | 90 | for (int8_t i=0; i<_devs; i++) { 91 | if (addrCompare(addr, _devices[i].addr)) { 92 | return i; 93 | } 94 | } 95 | return -1; 96 | } 97 | 98 | bool OneWirePlugin::getAddr(char* addr_c, int8_t sensor) { 99 | if (sensor >= _devs) 100 | return false; 101 | addrToStr((char*)addr_c, _devices[sensor].addr); 102 | return true; 103 | } 104 | 105 | bool OneWirePlugin::getUuid(char* uuid_c, int8_t sensor) { 106 | if (sensor >= _devs) 107 | return false; 108 | strcpy(uuid_c, _devices[sensor].uuid); 109 | return true; 110 | } 111 | 112 | bool OneWirePlugin::setUuid(const char* uuid_c, int8_t sensor) { 113 | if (sensor >= _devs) 114 | return false; 115 | if (strlen(_devices[sensor].uuid) + strlen(uuid_c) != 36) // erase before update 116 | return false; 117 | strcpy(_devices[sensor].uuid, uuid_c); 118 | return saveConfig(); 119 | } 120 | 121 | float OneWirePlugin::getValue(int8_t sensor) { 122 | if (sensor >= _devs) 123 | return NAN; 124 | return _devices[sensor].val; 125 | } 126 | 127 | void OneWirePlugin::getPluginJson(JsonObject* json) { 128 | JsonObject& config = json->createNestedObject("settings"); 129 | config[F("interval")] = 30; 130 | Plugin::getPluginJson(json); 131 | } 132 | 133 | bool OneWirePlugin::loadConfig() { 134 | File configFile = SPIFFS.open(F("/1wire.config"), "r"); 135 | if (configFile.size() == sizeof(_devices)) { 136 | DEBUG_MSG("1wire", "reading config file\n"); 137 | configFile.read((uint8_t*)_devices, sizeof(_devices)); 138 | 139 | // find first empty device slot 140 | DeviceAddress addr = {}; 141 | char addr_c[20]; 142 | addrToStr(addr_c, addr); 143 | _devs = getSensorByAddr(addr_c) + 1; 144 | } 145 | configFile.close(); 146 | return true; 147 | } 148 | 149 | bool OneWirePlugin::saveConfig() { 150 | DEBUG_MSG("1wire", "saving config\n"); 151 | File configFile = SPIFFS.open(F("/1wire.config"), "w"); 152 | if (!configFile) { 153 | DEBUG_MSG("1wire", "failed to open config file for writing\n"); 154 | return false; 155 | } 156 | 157 | configFile.write((uint8_t*)_devices, sizeof(_devices)); 158 | configFile.close(); 159 | return true; 160 | } 161 | 162 | /** 163 | * Loop (idle -> requesting -> reading) 164 | */ 165 | void OneWirePlugin::loop() { 166 | Plugin::loop(); 167 | 168 | // exit if no sensors found 169 | if (!_devs) 170 | return; 171 | 172 | if (_status == PLUGIN_IDLE && elapsed(SLEEP_PERIOD - REQUEST_WAIT_DURATION)) { 173 | DEBUG_MSG("1wire", "requesting temp\n"); 174 | _status = PLUGIN_REQUESTING; 175 | sensors.requestTemperatures(); 176 | } 177 | else if (_status == PLUGIN_REQUESTING && elapsed(REQUEST_WAIT_DURATION)) { 178 | DEBUG_MSG("1wire", "reading temp\n"); 179 | _status = PLUGIN_UPLOADING; 180 | readTemperatures(); 181 | } 182 | if (_status == PLUGIN_UPLOADING) { 183 | if (isUploadSafe()) { 184 | upload(); 185 | _status = PLUGIN_IDLE; 186 | } 187 | } 188 | } 189 | 190 | void OneWirePlugin::setupSensors() { 191 | DEBUG_MSG("1wire", "found %d devices\n", sensors.getDeviceCount()); 192 | 193 | DeviceAddress addr; 194 | for (int8_t i=0; i= 0) { 202 | DEBUG_MSG("1wire", "(known)\n"); 203 | } 204 | else { 205 | sensorIndex = addSensor(addr); 206 | DEBUG_MSG("1wire", "(new at %d)\n", sensorIndex); 207 | } 208 | 209 | // set precision 210 | sensors.setResolution(addr, TEMPERATURE_PRECISION); 211 | } 212 | } 213 | 214 | for (int8_t i=0; i<_devs; i++) { 215 | _devices[i].val = NAN; 216 | } 217 | } 218 | 219 | /* 220 | * Private 221 | */ 222 | 223 | int8_t OneWirePlugin::getSensorIndex(const uint8_t* addr) { 224 | for (int8_t i=0; i<_devs; ++i) { 225 | if (addrCompare(addr, _devices[i].addr)) { 226 | return i; 227 | } 228 | } 229 | return(-1); 230 | } 231 | 232 | int8_t OneWirePlugin::addSensor(const uint8_t* addr) { 233 | if (_devs >= MAX_SENSORS) { 234 | DEBUG_MSG("1wire", "too many devices\n"); 235 | return -1; 236 | } 237 | for (uint8_t i=0; i<8; i++) { 238 | _devices[_devs].addr[i] = addr[i]; 239 | } 240 | return(_devs++); 241 | } 242 | 243 | void OneWirePlugin::readTemperatures() { 244 | for (int8_t i=0; i<_devs; i++) { 245 | _devices[i].val = sensors.getTempC(_devices[i].addr); 246 | optimistic_yield(OPTIMISTIC_YIELD_TIME); 247 | 248 | if (_devices[i].val == DEVICE_DISCONNECTED_C) { 249 | DEBUG_MSG("1wire", "device %s disconnected\n", _devices[i].addr); 250 | continue; 251 | } 252 | } 253 | } 254 | -------------------------------------------------------------------------------- /src/plugins/OneWirePlugin.h: -------------------------------------------------------------------------------- 1 | #ifndef ONEWIRE_PLUGIN_H 2 | #define ONEWIRE_PLUGIN_H 3 | 4 | #include 5 | #include 6 | #include "Plugin.h" 7 | 8 | 9 | #define MAX_SENSORS 10 10 | 11 | 12 | struct DeviceStructOneWire { 13 | DeviceAddress addr; 14 | char uuid[UUID_LENGTH+1]; 15 | float val; 16 | }; 17 | 18 | 19 | class OneWirePlugin : public Plugin { 20 | public: 21 | static bool addrCompare(const uint8_t* a, const uint8_t* b); 22 | static void addrToStr(char* ptr, const uint8_t* addr); 23 | static void strToAddr(const char* ptr, uint8_t* addr); 24 | 25 | OneWirePlugin(byte pin); 26 | String getName() override; 27 | int8_t getSensorByAddr(const char* addr_c) override; 28 | bool getAddr(char* addr_c, int8_t sensor) override; 29 | bool getUuid(char* uuid_c, int8_t sensor) override; 30 | bool setUuid(const char* uuid_c, int8_t sensor) override; 31 | float getValue(int8_t sensor) override; 32 | void getPluginJson(JsonObject* json) override; 33 | bool loadConfig() override; 34 | bool saveConfig() override; 35 | void loop() override; 36 | 37 | private: 38 | OneWire ow; 39 | DallasTemperature sensors; 40 | DeviceStructOneWire _devices[MAX_SENSORS]; 41 | 42 | int8_t getSensorIndex(const uint8_t* addr); 43 | int8_t addSensor(const uint8_t* addr); 44 | void setupSensors(); 45 | void readTemperatures(); 46 | }; 47 | 48 | #endif 49 | -------------------------------------------------------------------------------- /src/plugins/Plugin.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include "Plugin.h" 5 | 6 | #ifdef ESP32 7 | #include 8 | #endif 9 | 10 | #define MAX_PLUGINS 5 11 | 12 | 13 | /* 14 | * Static 15 | */ 16 | 17 | int8_t Plugin::instances = 0; 18 | Plugin* Plugin::plugins[MAX_PLUGINS] = {}; 19 | HTTPClient Plugin::http; 20 | 21 | void Plugin::each(CallbackFunction callback) { 22 | for (int8_t i=0; i MAX_PLUGINS) { 35 | DEBUG_MSG("plugin", "too many plugins - panic"); 36 | PANIC(); 37 | } 38 | 39 | Plugin::plugins[Plugin::instances++] = this; 40 | Plugin::http.setReuse(true); // allow reuse (if server supports it) 41 | 42 | // buffer size 43 | _size = maxDevices * sizeof(DeviceStruct); 44 | 45 | if (maxDevices > 0) { 46 | // DEBUG_MSG("plugin", "alloc %d -> %d\n", maxDevices, _size); 47 | _devices = (DeviceStruct*)malloc(_size); 48 | if (_devices == NULL) 49 | PANIC(); 50 | } 51 | } 52 | 53 | Plugin::~Plugin() { 54 | } 55 | 56 | String Plugin::getName() { 57 | return "abstract"; 58 | } 59 | 60 | int8_t Plugin::getSensors() { 61 | return _devs; 62 | } 63 | 64 | int8_t Plugin::getSensorByAddr(const char* addr_c) { 65 | return -1; 66 | } 67 | 68 | bool Plugin::getAddr(char* addr_c, int8_t sensor) { 69 | return false; 70 | } 71 | 72 | bool Plugin::getUuid(char* uuid_c, int8_t sensor) { 73 | if (sensor >= _devs) 74 | return false; 75 | strcpy(uuid_c, _devices[sensor].uuid); 76 | return true; 77 | } 78 | 79 | bool Plugin::setUuid(const char* uuid_c, int8_t sensor) { 80 | if (sensor >= _devs) 81 | return false; 82 | if (strlen(_devices[sensor].uuid) + strlen(uuid_c) != UUID_LENGTH) // erase before update 83 | return false; 84 | strcpy(_devices[sensor].uuid, uuid_c); 85 | return saveConfig(); 86 | } 87 | 88 | String Plugin::getHash(int8_t sensor) { 89 | char addr_c[32]; 90 | if (getAddr(&addr_c[0], sensor)) { 91 | MD5Builder md5 = ::getHashBuilder(); 92 | md5.add(getName()); 93 | md5.add(addr_c); 94 | md5.calculate(); 95 | return md5.toString(); 96 | } 97 | return ""; 98 | } 99 | 100 | float Plugin::getValue(int8_t sensor) { 101 | return NAN; 102 | } 103 | 104 | void Plugin::getPluginJson(JsonObject* json) { 105 | JsonArray& sensorlist = json->createNestedArray("sensors"); 106 | for (int8_t i=0; i 0) { 174 | float val = getValue(i); 175 | 176 | if (isnan(val)) 177 | break; 178 | 179 | dtostrf(val, -4, 2, val_c); 180 | 181 | String uri = g_middleware + F("/data/") + String(uuid_c) + F(".json?value=") + String(val_c); 182 | http.begin(uri); 183 | int httpCode = http.POST(""); 184 | if (httpCode > 0) { 185 | http.getString(); 186 | } 187 | DEBUG_MSG(getName().c_str(), "POST %d %s\n", httpCode, uri.c_str()); 188 | http.end(); 189 | } 190 | } 191 | } 192 | 193 | bool Plugin::isUploadSafe() { 194 | // no upload in AP mode, no logging 195 | if ((WiFi.getMode() & WIFI_STA) == 0) 196 | return false; 197 | bool isSafe = WiFi.status() == WL_CONNECTED && ESP.getFreeHeap() >= HTTP_MIN_HEAP; 198 | if (!isSafe) { 199 | DEBUG_MSG(getName().c_str(), "cannot upload (wifi: %d mem:%d)\n", WiFi.status(), ESP.getFreeHeap()); 200 | } 201 | return isSafe; 202 | } 203 | 204 | bool Plugin::elapsed(uint32_t duration) { 205 | if (_timestamp == 0 || millis() - _timestamp >= duration) { 206 | _timestamp = millis(); 207 | return true; 208 | } 209 | _duration = duration; 210 | return false; 211 | } 212 | 213 | uint32_t Plugin::getMaxSleepDuration() { 214 | if (_timestamp == 0) 215 | return -1; 216 | uint32_t elapsed = millis() - _timestamp; 217 | if (elapsed < _duration) 218 | return _duration - elapsed; 219 | return 0; 220 | } 221 | -------------------------------------------------------------------------------- /src/plugins/Plugin.h: -------------------------------------------------------------------------------- 1 | #ifndef PLUGIN_H 2 | #define PLUGIN_H 3 | 4 | #include 5 | #ifdef ESP8266 6 | #include 7 | #include 8 | #endif 9 | #ifdef ESP32 10 | #include 11 | #include 12 | #endif 13 | #include 14 | #include "../config.h" 15 | 16 | 17 | #define MAX_PLUGINS 5 18 | 19 | // plugin states 20 | #define PLUGIN_IDLE 0 21 | #define PLUGIN_UPLOADING 1 22 | 23 | #define UUID_LENGTH 36 24 | #define JSON_NULL static_cast(NULL) 25 | 26 | struct DeviceStruct { 27 | char uuid[UUID_LENGTH+1]; 28 | float val; 29 | }; 30 | 31 | class Plugin { 32 | public: 33 | typedef std::function CallbackFunction; 34 | 35 | Plugin(int8_t maxDevices, int8_t actualDevices); 36 | virtual ~Plugin(); 37 | static void each(CallbackFunction callback); 38 | 39 | /** 40 | * Get plugin name 41 | */ 42 | virtual String getName(); 43 | 44 | /** 45 | * Get number of sensors for plugin 46 | */ 47 | virtual int8_t getSensors(); 48 | 49 | /** 50 | * Get sensor index by sensor name (e.g. /analog/) 51 | * Reversed by getAddr 52 | */ 53 | virtual int8_t getSensorByAddr(const char* addr_c); 54 | 55 | /** 56 | * Get senor name by sensor index 57 | * Reversed by getSensorByAddr 58 | */ 59 | virtual bool getAddr(char* addr_c, int8_t sensor); 60 | 61 | /** 62 | * Get middleware entity UUID for sensor 63 | */ 64 | virtual bool getUuid(char* uuid_c, int8_t sensor); 65 | 66 | /** 67 | * Set middleware entity UUID for sensor 68 | */ 69 | virtual bool setUuid(const char* uuid_c, int8_t sensor); 70 | 71 | /** 72 | * Get sensor hash value 73 | * Used to uniquely identify a sensor entity at the middleware even if 74 | * UUID has been erased from config 75 | */ 76 | virtual String getHash(int8_t sensor); 77 | 78 | /** 79 | * Get sensor value. Returns NAN is sensor not connected. 80 | */ 81 | virtual float getValue(int8_t sensor); 82 | 83 | /** 84 | * Get plugin json inluding all sensors 85 | */ 86 | virtual void getPluginJson(JsonObject* json); 87 | 88 | /** 89 | * Get senor json 90 | */ 91 | virtual void getSensorJson(JsonObject* json, int8_t sensor); 92 | 93 | /** 94 | * Load plugin configuration 95 | */ 96 | virtual bool loadConfig(); 97 | 98 | /** 99 | * Save plugin configuration 100 | */ 101 | virtual bool saveConfig(); 102 | 103 | /** 104 | * Plugin loop function called from main loop() 105 | */ 106 | virtual void loop(); 107 | virtual uint32_t getMaxSleepDuration(); 108 | 109 | protected: 110 | static HTTPClient http; // synchronous use only 111 | uint32_t _timestamp; 112 | uint32_t _duration; 113 | uint8_t _status; 114 | int8_t _devs; 115 | uint16_t _size; 116 | DeviceStruct* _devices; 117 | 118 | virtual void upload(); 119 | virtual bool isUploadSafe(); 120 | virtual bool elapsed(uint32_t duration); 121 | 122 | private: 123 | static int8_t instances; 124 | static Plugin* plugins[]; 125 | }; 126 | 127 | #endif 128 | -------------------------------------------------------------------------------- /src/plugins/S0Plugin.cpp: -------------------------------------------------------------------------------- 1 | #include "S0Plugin.h" 2 | 3 | #ifdef ESP32 4 | #include 5 | #endif 6 | 7 | 8 | #define SLEEP_PERIOD 10 * 1000 9 | 10 | /* 11 | class InterruptHandler { 12 | private: 13 | int _pin; 14 | 15 | public: 16 | S0Plugin* _plugin; 17 | InterruptHandler(S0Plugin* plugin, int pin, int mode) : _plugin(plugin), _pin(pin) { 18 | attachInterrupt(pin, _s_interruptHandler, mode); 19 | } 20 | 21 | static void _s_interruptHandler() { 22 | // reinterpret_cast(arg)->_plugin->handleInterrupt(1); 23 | } 24 | }; 25 | */ 26 | 27 | #define PREFIX "gpio" 28 | 29 | /* 30 | * Static 31 | */ 32 | 33 | S0Plugin* S0Plugin::_instance; 34 | 35 | /* 36 | * Virtual 37 | */ 38 | 39 | S0Plugin::S0Plugin(int8_t pin) : Plugin(1, 1), _pin(pin) { 40 | loadConfig(); 41 | 42 | _instance = this; 43 | for (int i=0; i= _devs) 74 | return false; 75 | String pin = PREFIX + String(_pin, 10); 76 | strcpy(addr_c, pin.c_str()); 77 | return true; 78 | } 79 | 80 | float S0Plugin::getValue(int8_t sensor) { 81 | uint16_t val = _power[sensor]; 82 | return val; 83 | } 84 | 85 | /** 86 | * Loop (idle -> uploading) 87 | */ 88 | void S0Plugin::loop() { 89 | Plugin::loop(); 90 | 91 | if (_status == PLUGIN_IDLE && elapsed(SLEEP_PERIOD)) { 92 | _status = PLUGIN_UPLOADING; 93 | } 94 | if (_status == PLUGIN_UPLOADING) { 95 | if (isUploadSafe()) { 96 | upload(); 97 | _status = PLUGIN_IDLE; 98 | } 99 | } 100 | } 101 | 102 | void S0Plugin::handleInterrupt(int8_t pin) { 103 | uint32_t ts = millis(); 104 | _eventCnt[pin]++; 105 | if (_eventTs[pin] > 0) { 106 | _power[pin] = 1.0e6 / (ts - _eventTs[pin]); 107 | String pwr = String(_power[pin], 2); 108 | DEBUG_MSG("s0", "pwr %sW %d\n", pwr.c_str(), _eventCnt[pin]); 109 | } 110 | _eventTs[pin] = ts; 111 | } 112 | 113 | void S0Plugin::_s_interrupt12() { 114 | _instance->handleInterrupt(12); 115 | } 116 | 117 | void S0Plugin::_s_interrupt14() { 118 | _instance->handleInterrupt(14); 119 | } 120 | -------------------------------------------------------------------------------- /src/plugins/S0Plugin.h: -------------------------------------------------------------------------------- 1 | #ifndef S0_PLUGIN_H 2 | #define S0_PLUGIN_H 3 | 4 | #include "Plugin.h" 5 | 6 | 7 | class S0Plugin : public Plugin { 8 | public: 9 | S0Plugin(int8_t pin); 10 | String getName() override; 11 | int8_t getSensorByAddr(const char* addr_c) override; 12 | bool getAddr(char* addr_c, int8_t sensor) override; 13 | float getValue(int8_t sensor) override; 14 | void loop() override; 15 | void handleInterrupt(int8_t pin); 16 | static void _s_interrupt12(); 17 | static void _s_interrupt14(); 18 | private: 19 | static S0Plugin* _instance; 20 | int8_t _pin = -1; 21 | uint32_t _eventTs[16] = {0}; 22 | uint16_t _eventCnt[16] = {0}; 23 | float _power[16] = {NAN}; 24 | }; 25 | 26 | #endif 27 | -------------------------------------------------------------------------------- /src/plugins/WifiPlugin.cpp: -------------------------------------------------------------------------------- 1 | #include "WifiPlugin.h" 2 | 3 | 4 | #define SLEEP_PERIOD 10 * 1000 5 | 6 | 7 | /* 8 | * Virtual 9 | */ 10 | 11 | WifiPlugin::WifiPlugin() : Plugin(1, 1) { 12 | loadConfig(); 13 | } 14 | 15 | String WifiPlugin::getName() { 16 | return "wifi"; 17 | } 18 | 19 | int8_t WifiPlugin::getSensorByAddr(const char* addr_c) { 20 | if (strcmp(addr_c, "wlan") == 0) 21 | return 0; 22 | return -1; 23 | } 24 | 25 | bool WifiPlugin::getAddr(char* addr_c, int8_t sensor) { 26 | if (sensor >= _devs) 27 | return false; 28 | strcpy(addr_c, "wlan"); 29 | return true; 30 | } 31 | 32 | float WifiPlugin::getValue(int8_t sensor) { 33 | return WiFi.RSSI(); 34 | } 35 | 36 | /** 37 | * Loop (idle -> uploading) 38 | */ 39 | void WifiPlugin::loop() { 40 | Plugin::loop(); 41 | 42 | if (_status == PLUGIN_IDLE && elapsed(SLEEP_PERIOD)) { 43 | _status = PLUGIN_UPLOADING; 44 | } 45 | if (_status == PLUGIN_UPLOADING) { 46 | if (isUploadSafe()) { 47 | upload(); 48 | _status = PLUGIN_IDLE; 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/plugins/WifiPlugin.h: -------------------------------------------------------------------------------- 1 | #ifndef WIFI_PLUGIN_H 2 | #define WIFI_PLUGIN_H 3 | 4 | #include "Plugin.h" 5 | 6 | 7 | class WifiPlugin : public Plugin { 8 | public: 9 | WifiPlugin(); 10 | String getName() override; 11 | int8_t getSensorByAddr(const char* addr_c) override; 12 | bool getAddr(char* addr_c, int8_t sensor) override; 13 | float getValue(int8_t sensor) override; 14 | void loop() override; 15 | }; 16 | 17 | #endif 18 | -------------------------------------------------------------------------------- /src/urlfunctions.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | ESP8266 Hello World urlencode by Steve Nelson 3 | URLEncoding is used all the time with internet urls. This is how urls handle funny characters 4 | in a URL. For example a space is: %20 5 | 6 | These functions simplify the process of encoding and decoding the urlencoded format. 7 | 8 | It has been tested on an esp12e (NodeMCU development board) 9 | This example code is in the public domain, use it however you want. 10 | 11 | Prerequisite Examples: 12 | https://github.com/zenmanenergy/ESP8266-Arduino-Examples/tree/master/helloworld_serial 13 | */ 14 | 15 | 16 | #include 17 | 18 | 19 | String urlencode(String str) 20 | { 21 | String encodedString=""; 22 | char c; 23 | char code0; 24 | char code1; 25 | char code2; 26 | for (unsigned int i=0; i < str.length(); i++){ 27 | c=str.charAt(i); 28 | if (c == ' ') { 29 | encodedString+= '+'; 30 | } 31 | else if (isalnum(c)) { 32 | encodedString+=c; 33 | } 34 | else { 35 | code1=(c & 0xf)+'0'; 36 | if ((c & 0xf) >9){ 37 | code1=(c & 0xf) - 10 + 'A'; 38 | } 39 | c=(c>>4)&0xf; 40 | code0=c+'0'; 41 | if (c > 9){ 42 | code0=c - 10 + 'A'; 43 | } 44 | code2='\0'; 45 | encodedString+='%'; 46 | encodedString+=code0; 47 | encodedString+=code1; 48 | //encodedString+=code2; 49 | } 50 | // yield(); 51 | } 52 | 53 | return encodedString; 54 | } 55 | 56 | -------------------------------------------------------------------------------- /src/urlfunctions.h: -------------------------------------------------------------------------------- 1 | /* 2 | ESP8266 Hello World urlencode by Steve Nelson 3 | URLEncoding is used all the time with internet urls. This is how urls handle funny characters 4 | in a URL. For example a space is: %20 5 | 6 | These functions simplify the process of encoding and decoding the urlencoded format. 7 | 8 | It has been tested on an esp12e (NodeMCU development board) 9 | This example code is in the public domain, use it however you want. 10 | 11 | Prerequisite Examples: 12 | https://github.com/zenmanenergy/ESP8266-Arduino-Examples/tree/master/helloworld_serial 13 | */ 14 | 15 | String urlencode(String str); 16 | 17 | -------------------------------------------------------------------------------- /src/vzero.ino: -------------------------------------------------------------------------------- 1 | /** 2 | * VZero - Zero Config Volkszaehler Controller 3 | * 4 | * @author Andreas Goetz 5 | */ 6 | 7 | #ifdef ESP8266 8 | #include 9 | #include 10 | #endif 11 | 12 | #ifdef ESP32 13 | #include 14 | #include 15 | #include 16 | #endif 17 | 18 | #include 19 | 20 | #include "config.h" 21 | #include "webserver.h" 22 | #include "plugins/Plugin.h" 23 | 24 | #ifdef OTA_SERVER 25 | #include 26 | #endif 27 | 28 | #ifdef CAPTIVE_PORTAL 29 | #include 30 | const byte DNS_PORT = 53; 31 | DNSServer dnsServer; 32 | #endif 33 | 34 | enum operation_t { 35 | OPERATION_NORMAL = 0, // deep sleep forbidden 36 | OPERATION_SLEEP = 1 // deep sleep allowed 37 | }; 38 | 39 | /** 40 | * Get operation mode 41 | * 42 | * Return NORMAL if: 43 | * - deep sleep disabled 44 | * - not waking up from deep sleep 45 | * - not running as access point 46 | */ 47 | operation_t getOperationMode() 48 | { 49 | #ifndef DEEP_SLEEP 50 | return OPERATION_NORMAL; 51 | #endif 52 | 53 | if (getResetReason(0) != REASON_DEEP_SLEEP_AWAKE) 54 | return OPERATION_NORMAL; 55 | if ((WiFi.getMode() & WIFI_STA) == 0) 56 | return OPERATION_NORMAL; 57 | return OPERATION_SLEEP; 58 | } 59 | 60 | /** 61 | * (Re)connect WiFi - give ESP 10 seconds to connect to station 62 | */ 63 | wl_status_t wifiConnect() { 64 | DEBUG_MSG("wifi", "waiting for connection"); 65 | unsigned long startTime = millis(); 66 | while (WiFi.status() != WL_CONNECTED && millis() - startTime < WIFI_CONNECT_TIMEOUT) { 67 | DEBUG_PLAIN("."); 68 | delay(100); 69 | } 70 | DEBUG_PLAIN("\n"); 71 | return WiFi.status(); 72 | } 73 | 74 | /** 75 | * Get max deep sleep window in ms 76 | */ 77 | uint32_t getDeepSleepDurationMs() 78 | { 79 | #ifndef DEEP_SLEEP 80 | return 0; 81 | #else 82 | // don't sleep if access point 83 | if ((WiFi.getMode() & WIFI_STA) == 0) 84 | return 0; 85 | // don't sleep during initial startup 86 | if (g_resetInfo->reason != 5 && millis() < STARTUP_ONLINE_DURATION_MS) 87 | return 0; 88 | // don't sleep if client connected 89 | if (millis() - g_lastAccessTime < WIFI_CLIENT_TIMEOUT) 90 | return 0; 91 | 92 | // check if deep sleep possible 93 | uint32_t maxSleep = -1; // max uint32_t 94 | Plugin::each([&maxSleep](Plugin* plugin) { 95 | uint32_t sleep = plugin->getMaxSleepDuration(); 96 | // DEBUG_MSG(CORE, "sleep window is %u for %s\n", sleep, Plugin::get(pluginIndex)->getName().c_str()); 97 | if (sleep < maxSleep) 98 | maxSleep = sleep; 99 | }); 100 | 101 | // valid sleep window found? 102 | if (maxSleep == -1 || maxSleep < MIN_SLEEP_DURATION_MS) 103 | return 0; 104 | 105 | return maxSleep - SLEEP_SAFETY_MARGIN; 106 | #endif 107 | } 108 | 109 | /** 110 | * Start ota server 111 | */ 112 | void start_ota() { 113 | #ifdef OTA_SERVER 114 | if (MDNS.begin(net_hostname.c_str())) { 115 | DEBUG_MSG(CORE, "mDNS responder started at %s.local\n", net_hostname.c_str()); 116 | } 117 | else { 118 | DEBUG_MSG(CORE, "error setting up mDNS responder\n"); 119 | } 120 | 121 | // start OTA server 122 | DEBUG_MSG(CORE, "starting OTA server\n"); 123 | ArduinoOTA.setHostname(net_hostname.c_str()); 124 | ArduinoOTA.onStart([]() { 125 | DEBUG_MSG(CORE, "OTA start\n"); 126 | }); 127 | ArduinoOTA.onEnd([]() { 128 | DEBUG_MSG(CORE, "OTA end\n"); 129 | 130 | // save config after OTA Update 131 | if (SPIFFS.begin()) { 132 | File configFile = SPIFFS.open(F("/config.json"), "r"); 133 | if (!configFile) { 134 | DEBUG_MSG(CORE, "config wiped by OTA - saving\n"); 135 | saveConfig(); 136 | } 137 | else { 138 | configFile.close(); 139 | } 140 | SPIFFS.end(); 141 | } 142 | }); 143 | ArduinoOTA.onError([](ota_error_t error) { 144 | DEBUG_MSG(CORE, "OTA error [%u]\n", error); 145 | }); 146 | ArduinoOTA.begin(); 147 | #endif 148 | } 149 | 150 | #ifndef ESP32 151 | // use the internal hardware buffer 152 | static void _u0_putc(char c) { 153 | while(((U0S >> USTXC) & 0x7F) == 0x7F); 154 | U0F = c; 155 | } 156 | #endif 157 | 158 | /** 159 | * Setup 160 | */ 161 | void setup() 162 | { 163 | // hardware serial 164 | Serial.begin(115200); 165 | #ifndef ESP32 166 | ets_install_putc1((void *) &_u0_putc); 167 | system_set_os_print(1); 168 | 169 | g_resetInfo = ESP.getResetInfoPtr(); 170 | #endif 171 | 172 | DEBUG_PLAIN("\n"); 173 | DEBUG_MSG(CORE, "Booting...\n"); 174 | DEBUG_MSG(CORE, "Cause %d: %s\n", getResetReason(0), getResetReasonStr(0)); 175 | DEBUG_MSG(CORE, "Chip ID: %05X\n", getChipId()); 176 | 177 | #ifndef ESP32 178 | // set hostname 179 | net_hostname += "-" + String(getChipId(), HEX); 180 | WiFi.hostname(net_hostname); 181 | DEBUG_MSG(CORE, "Hostname: %s\n", net_hostname.c_str()); 182 | #endif 183 | 184 | // initialize file system 185 | if (!SPIFFS.begin()) { 186 | DEBUG_MSG(CORE, "failed mounting file system\n"); 187 | return; 188 | } 189 | 190 | // check WiFi connection 191 | if (WiFi.getMode() != WIFI_STA) { 192 | WiFi.mode(WIFI_STA); 193 | delay(10); 194 | } 195 | 196 | // configuration changed - set new credentials 197 | if (loadConfig() && g_ssid != "" && (String(WiFi.SSID()) != g_ssid || String(WiFi.psk()) != g_pass)) { 198 | DEBUG_MSG("wifi", "connect: %s\n", WiFi.SSID().c_str()); 199 | WiFi.begin(g_ssid.c_str(), g_pass.c_str()); 200 | } 201 | else { 202 | // reconnect to sdk-configured station 203 | DEBUG_MSG("wifi", "reconnect: %s\n", WiFi.SSID().c_str()); 204 | WiFi.begin(); 205 | } 206 | 207 | // Check connection 208 | if (wifiConnect() == WL_CONNECTED) { 209 | DEBUG_MSG("wifi", "IP address: %d.%d.%d.%d\n", WiFi.localIP()[0], WiFi.localIP()[1], WiFi.localIP()[2], WiFi.localIP()[3]); 210 | } 211 | else { 212 | // go into AP mode 213 | DEBUG_MSG("wifi", "could not connect to WiFi - going into AP mode\n"); 214 | 215 | WiFi.mode(WIFI_AP); // WIFI_AP_STA 216 | delay(10); 217 | 218 | WiFi.softAP(ap_default_ssid); 219 | DEBUG_MSG("wifi", "IP address: %d.%d.%d.%d\n", WiFi.softAPIP()[0], WiFi.softAPIP()[1], WiFi.softAPIP()[2], WiFi.softAPIP()[3]); 220 | 221 | #ifdef CAPTIVE_PORTAL 222 | // start DNS server for any domain 223 | DEBUG_MSG("wifi", "starting captive DNS server\n"); 224 | dnsServer.start(DNS_PORT, "*", WiFi.softAPIP()); 225 | #endif 226 | } 227 | 228 | // start OTA - both in AP as in STA mode 229 | if (getOperationMode() == OPERATION_NORMAL) { 230 | start_ota(); 231 | } 232 | 233 | // start plugins (before web server) 234 | startPlugins(); 235 | 236 | // start web server if not in battery mode 237 | if (getOperationMode() == OPERATION_NORMAL) { 238 | webserver_start(); 239 | } 240 | } 241 | 242 | uint32_t _minFreeHeap = 0; 243 | uint32_t _freeHeap = 0; 244 | long _tsMillis = 0; 245 | long _loopMillis = 0; 246 | 247 | /** 248 | * Loop 249 | */ 250 | void loop() 251 | { 252 | // loop duration 253 | _tsMillis = millis(); 254 | 255 | #ifdef CAPTIVE_PORTAL 256 | dnsServer.processNextRequest(); 257 | #endif 258 | 259 | #ifdef OTA_SERVER 260 | if (getOperationMode() == OPERATION_NORMAL) { 261 | ArduinoOTA.handle(); 262 | } 263 | #endif 264 | 265 | // call plugin's loop method 266 | Plugin::each([](Plugin* plugin) { 267 | plugin->loop(); 268 | yield(); 269 | }); 270 | 271 | // check if deep sleep possible 272 | uint32_t sleep = getDeepSleepDurationMs(); 273 | if (sleep > 0) { 274 | DEBUG_MSG(CORE, "going to deep sleep for %ums\n", sleep); 275 | ESP.deepSleep(sleep * 1000); 276 | } 277 | 278 | // trigger restart? 279 | if (g_restartTime > 0 && millis() >= g_restartTime) { 280 | DEBUG_MSG(CORE, "restarting...\n"); 281 | g_restartTime = 0; 282 | ESP.restart(); 283 | } 284 | 285 | // check WLAN if not AP 286 | if ((WiFi.getMode() & WIFI_AP) == 0) { 287 | if (WiFi.status() != WL_CONNECTED) { 288 | DEBUG_MSG(CORE, "wifi connection lost\n"); 289 | WiFi.reconnect(); 290 | if (wifiConnect() != WL_CONNECTED) { 291 | DEBUG_MSG(CORE, "could not reconnect wifi - restarting\n"); 292 | ESP.restart(); 293 | } 294 | } 295 | } 296 | 297 | // loop duration without debug 298 | _loopMillis = millis() - _tsMillis; 299 | 300 | if (g_minFreeHeap != _minFreeHeap || ESP.getFreeHeap() != _freeHeap) { 301 | _freeHeap = ESP.getFreeHeap(); 302 | if (_freeHeap < g_minFreeHeap) 303 | g_minFreeHeap = _freeHeap; 304 | _minFreeHeap = g_minFreeHeap; 305 | 306 | #ifdef ESP8266 307 | umm_info(NULL, 0); 308 | DEBUG_MSG(CORE, "heap min: %d (%d blk, %d tot)\n", g_minFreeHeap, ummHeapInfo.maxFreeContiguousBlocks * 8, _freeHeap); 309 | #endif 310 | #ifdef ESP32 311 | DEBUG_MSG(CORE, "heap min: %d (%d tot)\n", g_minFreeHeap, _freeHeap); 312 | #endif 313 | 314 | // loop duration 315 | DEBUG_MSG(CORE, "loop %ums\n", _loopMillis); 316 | } 317 | 318 | delay(1000); 319 | } 320 | -------------------------------------------------------------------------------- /src/webserver.cpp: -------------------------------------------------------------------------------- 1 | /** 2 | * Web server 3 | */ 4 | 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | 11 | #include "config.h" 12 | #include "webserver.h" 13 | #include "urlfunctions.h" 14 | #include "plugins/Plugin.h" 15 | 16 | #ifdef ESP8266 17 | #include 18 | #endif 19 | 20 | #ifdef ESP32 21 | #include 22 | #include "SPIFFS.h" 23 | #endif 24 | 25 | #ifdef SPIFFS_EDITOR 26 | #include "SPIFFSEditor.h" 27 | #endif 28 | 29 | 30 | #define CACHE_HEADER "max-age=86400" 31 | #define CORS_HEADER "Access-Control-Allow-Origin" 32 | 33 | #define CONTENT_TYPE_JSON "application/json" 34 | #define CONTENT_TYPE_PLAIN "text/plain" 35 | #define CONTENT_TYPE_HTML "text/html" 36 | 37 | uint32_t g_restartTime = 0; 38 | uint32_t g_lastAccessTime = 0; 39 | 40 | AsyncWebServer g_server(80); 41 | 42 | 43 | void requestRestart() 44 | { 45 | g_restartTime = millis() + 100; 46 | } 47 | 48 | void jsonResponse(AsyncWebServerRequest *request, int res, JsonVariant json) 49 | { 50 | // touch 51 | g_lastAccessTime = millis(); 52 | 53 | AsyncResponseStream *response = request->beginResponseStream(F(CONTENT_TYPE_JSON)); 54 | response->addHeader(F(CORS_HEADER), "*"); 55 | json.printTo(*response); 56 | request->send(response); 57 | } 58 | 59 | String getIP() 60 | { 61 | IPAddress ip = (WiFi.getMode() & WIFI_STA) ? WiFi.localIP() : WiFi.softAPIP(); 62 | return ip.toString(); 63 | } 64 | 65 | #ifdef CAPTIVE_PORTAL 66 | class CaptiveRequestHandler : public AsyncWebHandler { 67 | public: 68 | CaptiveRequestHandler() { 69 | } 70 | 71 | bool canHandle(AsyncWebServerRequest *request){ 72 | // redirect if not in wifi client mode (through filter) 73 | // and request for different host (due to DNS * response) 74 | if (request->host() != WiFi.softAPIP().toString()) 75 | return true; 76 | else 77 | return false; 78 | } 79 | 80 | void handleRequest(AsyncWebServerRequest *request) { 81 | DEBUG_MSG(SERVER, "captive request to %s\n", request->url().c_str()); 82 | String location = "http://" + WiFi.softAPIP().toString(); 83 | if (request->host() == net_hostname + ".local") 84 | location += request->url(); 85 | request->redirect(location); 86 | } 87 | }; 88 | #endif 89 | 90 | class PluginRequestHandler : public AsyncWebHandler { 91 | public: 92 | PluginRequestHandler(const char* uri, Plugin* plugin, const int8_t sensor) : _uri(uri), _plugin(plugin), _sensor(sensor) { 93 | } 94 | 95 | bool canHandle(AsyncWebServerRequest *request){ 96 | if (request->method() != HTTP_GET && request->method() != HTTP_POST) 97 | return false; 98 | if (!request->url().startsWith(_uri)) 99 | return false; 100 | return true; 101 | } 102 | 103 | void handleRequest(AsyncWebServerRequest *request) { 104 | DynamicJsonBuffer jsonBuffer; 105 | JsonObject& json = jsonBuffer.createObject(); 106 | int res = 400; // JSON error 107 | 108 | // GET - get sensor value 109 | if (request->method() == HTTP_GET && request->params() == 0) { 110 | float val = _plugin->getValue(_sensor); 111 | if (isnan(val)) 112 | json["value"] = JSON_NULL; 113 | else { 114 | json["value"] = val; 115 | res = 200; 116 | } 117 | } 118 | // POST - set sensor UUID 119 | else if ((request->method() == HTTP_POST || request->method() == HTTP_GET) 120 | && request->params() == 1 && request->hasParam("uuid")) { 121 | String uuid = request->getParam(0)->value(); 122 | if (_plugin->setUuid(uuid.c_str(), _sensor)) { 123 | _plugin->getSensorJson(&json, _sensor); 124 | res = 200; 125 | } 126 | } 127 | 128 | jsonResponse(request, res, json); 129 | } 130 | 131 | protected: 132 | String _uri; 133 | Plugin* _plugin; 134 | int8_t _sensor; 135 | }; 136 | 137 | /** 138 | * Handle set request from http server. 139 | */ 140 | void handleNotFound(AsyncWebServerRequest *request) 141 | { 142 | DEBUG_MSG(SERVER, "file not found %s\n", request->url().c_str()); 143 | request->send(404, F(CONTENT_TYPE_PLAIN), F("File not found")); 144 | } 145 | 146 | /** 147 | * Handle set request from http server. 148 | */ 149 | void handleSettings(AsyncWebServerRequest *request) 150 | { 151 | DEBUG_MSG(SERVER, "%s (%d args)\n", request->url().c_str(), request->params()); 152 | 153 | String resp = F(""); 154 | String ssid = ""; 155 | String pass = ""; 156 | int result = 400; 157 | 158 | // read ssid and psk 159 | if (request->hasParam("ssid", true) && request->hasParam("pass", true)) { 160 | ssid = request->getParam("ssid", true)->value(); 161 | pass = request->getParam("pass", true)->value(); 162 | if (ssid != "") { 163 | g_ssid = ssid; 164 | g_pass = pass; 165 | result = 200; 166 | } 167 | } 168 | if (request->hasParam("middleware", true)) { 169 | g_middleware = request->getParam("middleware", true)->value(); 170 | result = 200; 171 | } 172 | if (result == 400) { 173 | request->send(result, F(CONTENT_TYPE_PLAIN), F("Bad request\n\n")); 174 | return; 175 | } 176 | 177 | if (saveConfig()) { 178 | resp += F("

Settings saved.

"); 179 | } 180 | else { 181 | resp += F("

Failed to save config file.

"); 182 | result = 400; 183 | } 184 | resp += F(""); 185 | if (result == 200) { 186 | requestRestart(); 187 | } 188 | request->send(result, F(CONTENT_TYPE_HTML), resp); 189 | } 190 | 191 | /** 192 | * Status JSON api 193 | */ 194 | void handleGetStatus(AsyncWebServerRequest *request) 195 | { 196 | DEBUG_MSG(SERVER, "%s (%d args)\n", request->url().c_str(), request->params()); 197 | 198 | StaticJsonBuffer<512> jsonBuffer; 199 | JsonObject& json = jsonBuffer.createObject(); 200 | 201 | if (request->hasParam("initial")) { 202 | char buf[8]; 203 | sprintf(buf, "%06x", getChipId()); 204 | #ifdef ESP8266 205 | json[F("cpu")] = "ESP8266"; 206 | #endif 207 | #ifdef ESP32 208 | json[F("cpu")] = "ESP32"; 209 | #endif 210 | json[F("serial")] = buf; 211 | json[F("build")] = BUILD; 212 | json[F("ssid")] = g_ssid; 213 | // json[F("pass")] = g_pass; 214 | json[F("middleware")] = g_middleware; 215 | #ifndef ESP32 216 | json[F("flash")] = ESP.getFlashChipRealSize(); 217 | #endif 218 | json[F("wifimode")] = (WiFi.getMode() & WIFI_STA) ? "Connected" : "Access Point"; 219 | json[F("ip")] = getIP(); 220 | } 221 | 222 | long heap = ESP.getFreeHeap(); 223 | json[F("uptime")] = millis(); 224 | json[F("heap")] = heap; 225 | json[F("minheap")] = g_minFreeHeap; 226 | json[F("resetcode")] = getResetReason(0); 227 | #ifdef ESP32 228 | json[F("resetcode1")] = getResetReason(1); 229 | #endif 230 | // json[F("gpio")] = (uint32_t)(((GPI | GPO) & 0xFFFF) | ((GP16I & 0x01) << 16)); 231 | 232 | // reset free heap 233 | g_minFreeHeap = heap; 234 | 235 | jsonResponse(request, 200, json); 236 | } 237 | 238 | /** 239 | * Get plugin information 240 | */ 241 | void handleGetPlugins(AsyncWebServerRequest *request) 242 | { 243 | DEBUG_MSG(SERVER, "%s (%d args)\n", request->url().c_str(), request->params()); 244 | 245 | DynamicJsonBuffer jsonBuffer; 246 | JsonArray& json = jsonBuffer.createArray(); 247 | 248 | Plugin::each([&json](Plugin* plugin) { 249 | JsonObject& obj = json.createNestedObject(); 250 | obj[F("name")] = plugin->getName(); 251 | plugin->getPluginJson(&obj); 252 | }); 253 | 254 | jsonResponse(request, 200, json); 255 | } 256 | 257 | /** 258 | * Setup handlers for each plugin and sensor 259 | * Structure is /api// 260 | */ 261 | void registerPlugins() 262 | { 263 | Plugin::each([](Plugin* plugin) { 264 | DEBUG_MSG(SERVER, "register plugin: %s\n", plugin->getName().c_str()); 265 | 266 | // register one handler per sensor 267 | String baseUri = "/api/" + plugin->getName() + "/"; 268 | for (int8_t sensor=0; sensorgetSensors(); sensor++) { 269 | String uri = String(baseUri); 270 | char addr_c[20]; 271 | plugin->getAddr(addr_c, sensor); 272 | uri += addr_c; 273 | DEBUG_MSG(SERVER, "register sensor: %s\n", uri.c_str()); 274 | 275 | g_server.addHandler(new PluginRequestHandler(uri.c_str(), plugin, sensor)); 276 | } 277 | }); 278 | } 279 | 280 | void handleWifiScan(AsyncWebServerRequest *request) 281 | { 282 | String json = "["; 283 | 284 | int n = WiFi.scanComplete(); 285 | DEBUG_MSG(SERVER, "scanning wifi (%d)\n", n); 286 | 287 | if (n == WIFI_SCAN_FAILED){ 288 | WiFi.scanNetworks(true); 289 | } 290 | else if (n > 0) { // scan finished 291 | for (int i = 0; i < n; ++i) { 292 | if (i) json += ","; 293 | json += "{"; 294 | json += "\"rssi\":" + String(WiFi.RSSI(i)); 295 | json += ",\"ssid\":\"" + WiFi.SSID(i) + "\""; 296 | // json += ",\"bssid\":\""+WiFi.BSSIDstr(i)+"\""; 297 | // json += ",\"channel\":"+String(WiFi.channel(i)); 298 | json += ",\"secure\":" + String(WiFi.encryptionType(i)); 299 | #ifndef ESP32 300 | json += ",\"hidden\":" + String(WiFi.isHidden(i) ? "true" : "false"); 301 | #endif 302 | json += "}"; 303 | } 304 | // save scan result memory 305 | WiFi.scanDelete(); 306 | // WiFi.scanNetworks(true); 307 | } 308 | json += "]"; 309 | 310 | AsyncWebServerResponse *response = request->beginResponse(200, F(CONTENT_TYPE_JSON), json); 311 | response->addHeader(F(CORS_HEADER), "*"); 312 | request->send(response); 313 | } 314 | 315 | /** 316 | * Initialize web server and add requests 317 | */ 318 | void webserver_start() 319 | { 320 | // not found 321 | g_server.onNotFound(handleNotFound); 322 | 323 | #ifdef CAPTIVE_PORTAL 324 | // handle captive requests 325 | g_server.addHandler(new CaptiveRequestHandler()).setFilter(ON_AP_FILTER); 326 | #endif 327 | 328 | // CDN 329 | g_server.on("/js/jquery-2.1.4.min.js", HTTP_GET, [](AsyncWebServerRequest *request) { 330 | request->redirect(F("http://code.jquery.com/jquery-2.1.4.min.js")); 331 | }).setFilter(ON_STA_FILTER); 332 | g_server.on("/css/foundation.min.css", HTTP_GET, [](AsyncWebServerRequest *request) { 333 | request->redirect(F("http://cdnjs.cloudflare.com/ajax/libs/foundation/6.2.3/foundation.min.css")); 334 | }).setFilter(ON_STA_FILTER); 335 | 336 | // GET 337 | g_server.on("/api/status", HTTP_GET, handleGetStatus); 338 | g_server.on("/api/plugins", HTTP_GET, handleGetPlugins); 339 | g_server.on("/api/scan", HTTP_GET, handleWifiScan); 340 | 341 | // POST 342 | g_server.on("/settings", HTTP_POST, handleSettings); 343 | g_server.on("/restart", HTTP_POST, [](AsyncWebServerRequest *request) { 344 | // AsyncWebServerResponse *response = request->beginResponse(302); 345 | // response->addHeader("Location", net_hostname + ".local"); 346 | // request->send(response); 347 | 348 | request->send(200, F(CONTENT_TYPE_HTML), F("Restarting...
")); 349 | requestRestart(); 350 | }); 351 | 352 | // make sure config.json is not served! 353 | g_server.on("/config.json", HTTP_GET, [](AsyncWebServerRequest *request) { 354 | request->send(400); 355 | }); 356 | 357 | // catch-all 358 | g_server.serveStatic("/", SPIFFS, "/", CACHE_HEADER).setDefaultFile("index.html"); 359 | 360 | // sensor api 361 | registerPlugins(); 362 | 363 | #ifdef SPIFFS_EDITOR 364 | g_server.addHandler(new SPIFFSEditor()); 365 | #endif 366 | 367 | // start server 368 | g_server.begin(); 369 | } 370 | -------------------------------------------------------------------------------- /src/webserver.h: -------------------------------------------------------------------------------- 1 | /** 2 | * Web server 3 | */ 4 | 5 | #include "config.h" 6 | 7 | #define SERVER "websrv" 8 | 9 | 10 | // timestamp to trigger restart on 11 | extern uint32_t g_restartTime; 12 | // timestamp of last client access 13 | extern uint32_t g_lastAccessTime; 14 | 15 | /** 16 | * Start web server 17 | */ 18 | void webserver_start(); 19 | --------------------------------------------------------------------------------