diff --git a/.gitignore b/.gitignore index 81102fd7d..559d0aabf 100644 --- a/.gitignore +++ b/.gitignore @@ -1,36 +1,20 @@ # vscode .vscode -.vscode/.browse.c_cpp.db* -.vscode/c_cpp_properties.json -.vscode/launch.json -.vscode/* # platformio .pio -.clang_complete -.gcc-flags.json lib/readme.txt # web stuff compiled -src/websrc/css/required.css -src/websrc/js/required.js -src/websrc/fonts -src/websrc/gzipped src/websrc/temp -*.gz.h +src/webh/*.gz.h # NPM directories -node_modules/ -jspm_packages/ -.npm +node_modules -# Output of 'npm pack' -*.tgz - -# dotenv environment variables file -.env +# OS specific +.DS_Store # project specfic -.DS_Store scripts/stackdmp.txt -*.bin +firmware diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 000000000..fc4d9ac79 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,86 @@ +os: linux +language: python +python: + - "2.7" + +cache: + directories: + - ${HOME}/.pio + +env: + global: + # - BUILDER_TOTAL_THREADS=4 + - BUILDER_TOTAL_THREADS=1 + - OWNER=${TRAVIS_REPO_SLUG%/*} + - DEV=${OWNER/proddy/dev} + - BRANCH=${TRAVIS_BRANCH/dev/} + - TAG=${DEV}${BRANCH:+_}${BRANCH} + +install: + - env | grep TRAVIS + - set -e + - pip install -U platformio + - pio platform update -p + - set +e + +branches: + except: + - /^travis-.*-build$/ + +script: + - ./scripts/build.sh +# - ./scripts/build.sh -p + +stages: + - name: Release +# if: type IN (cron, api) + +jobs: + include: + - stage: Release +# env: BUILDER_THREAD=0 +# - env: BUILDER_THREAD=1 +# - env: BUILDER_THREAD=2 +# - env: BUILDER_THREAD=3 + +before_deploy: + - export FIRMWARE_VERSION=$(grep -E '^#define APP_VERSION' ./src/version.h | awk '{print $3}' | sed 's/"//g') + - git tag -f travis-${TAG}-build + - git remote add gh + https://${OWNER}:${GITHUB_TOKEN}@github.com/${TRAVIS_REPO_SLUG}.git + - git push gh :travis-${TAG}-build || true + - git push -f gh travis-${TAG}-build + - git remote remove gh + +deploy: + provider: releases + edge: + branch: master + token: ${GITHUB_TOKEN} + file_glob: true + # file: "firmware/*.bin" + file: "*.bin" + name: latest development build + release_notes: + Version $FIRMWARE_VERSION. + Automatic firmware builds of the current EMS-ESP branch built on $(date +'%F %T %Z') from commit $TRAVIS_COMMIT. + Warning, this is a development build and not fully tested. Use at your own risk. + cleanup: false + prerelease: true + overwrite: true + target_commitish: $TRAVIS_COMMIT + on: + tags: false + branch: dev + +notifications: + email: + on_success: change + on_failure: change + + webhooks: + urls: + - https://webhooks.gitter.im/e/57e15f7798656d888194 + on_success: always + on_failure: never + on_start: never diff --git a/CHANGELOG.md b/CHANGELOG.md index dd11f622c..861e47c09 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,13 +5,52 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [1.9.4] 2019-12-15 + +There are breaking changes in this release. Make you sure you adjust the MQTT topics as described in the wiki. + +### Added + +- Added `publish_always` forcing MQTT topics to be always sent regardless if the data hasn't changed +- Support for DHW once (OneTime water) heating command via MQTT [issue 195](https://github.com/proddy/EMS-ESP/issues/195) +- Added scripts to automatically build firmware images on every Commit/Pull and nightly builds using TravisCI +- Added option to WebUI to also download the latest development build +- Added build scripts for automated CI with TravisCI +- Implemented timezone support and automatic adjustment, also taking daylight saving times into account +- Added `kick` command to reset core services like NTP, Web, Web Sockets +- Added WiFi static IP (setting done in WebUI only) +- `log w ` for watching a specific telegram type ID +- initial support for EMS+ GB125s and MC110's (https://github.com/proddy/EMS-ESP/wiki/MC110-controller) +- Buderus RFM200 receiver + +### Fixed + +- Stability for some Wemos clones by decreasing wifi Tx strength and adding small delay + +### Changed + +- Debug log times show real internet time (if NTP enabled) +- `system` shows local time instead of UTC +- fixed version numbers of libraries in `platformio.ini` +- Normalized Heating modes to `off`, `manual`, `auto`, `night` and `day` to keep generic and not Home Assistant specific (like `heat`) +- Keeping Thermostat day/night modes separate from off/auto/manual, and setting this for the Junkers FR50 +- Removed `publish_always` +- Changed NTP interval from 1 hour to 12 hours +- Refactored EMS device library to make it support multi-EMS devices easier (e.g. multiple thermostats) +- `autodetect deep` removed and replaced with `autodetect scan` for scanning known devices. +- MQTT data will be sent when new data arrives. So `publish_time` is used to force a publish at a given frequency (2 mins is default), or 0 for off. + +### Removed + +- thermostat scan and autodetect deep functions +- removed Event Logging to SPIFFS (worried about wearing). Replaced with SysLog. + ## [1.9.3] 2019-10-26 ### Added - Report # TCP dropouts in the `system` command. These could be due to WiFI or MQTT disconnected. - Added temp and mode to the MQTT `thermostat_cmd` topic -- build scripts for automated CI with TravisCI ### Fixed @@ -174,7 +213,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - publish dallas external temp sensors to MQTT (thanks @JewelZB) - shower timer and shower alert options available via set commands - added support for warm water modes Hot, Comfort and Intelligent [(issue 67)](https://github.com/proddy/EMS-ESP/issues/67) -- added `set publish_time` to set how often to publish MQTT +- added `set publish_time` to set how often to force a publish of MQTT - support for SM10 Solar Module including MQTT [(issue 77)](https://github.com/proddy/EMS-ESP/issues/77) - `refresh` command to force a fetch of all known data from the connected EMS devices @@ -359,7 +398,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed - Renamed project from EMS-ESP-Boiler to EMS-ESP since it's kinda EMS generic now -- Support for RC20F and RFM20 (https://github.com/proddy/EMS-ESP/issues/18) +- Support for RC20RF and RFM20 (https://github.com/proddy/EMS-ESP/issues/18) - Moved all EMS device information into a separate file `ems_devices.h` so no longer need to touch `ems.h` - Telnet commands can be strings now and output is suspended when typing @@ -380,7 +419,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Scanning known EMS Devices now ignores duplicates (https://github.com/proddy/EMS-ESP/pull/30) - ServiceCode stored as a two byte char -- Support for RC20F and RFM20 (https://github.com/proddy/EMS-ESP/issues/18) +- Support for RC20RF and RFM20 (https://github.com/proddy/EMS-ESP/issues/18) ## [1.2.3] 2019-01-03 diff --git a/README.md b/README.md index c80759829..3d58fb72c 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ [![release-date](https://img.shields.io/github/release-date/proddy/EMS-ESP.svg?label=Released)](https://github.com/proddy/EMS-ESP/commits/master)
[![license](https://img.shields.io/github/license/proddy/EMS-ESP.svg)](LICENSE) -[![travis](https://travis-ci.com/proddy/EMS-ESP.svg?branch=master)](https://travis-ci.com/proddy/EMS-ESP) +[![travis](https://travis-ci.com/proddy/EMS-ESP.svg?branch=dev)](https://travis-ci.com/proddy/EMS-ESP) [![Codacy Badge](https://api.codacy.com/project/badge/Grade/b8880625bdf841d4adb2829732030887)](https://app.codacy.com/app/proddy/EMS-ESP?utm_source=github.com&utm_medium=referral&utm_content=proddy/EMS-ESP&utm_campaign=Badge_Grade_Settings) [![downloads](https://img.shields.io/github/downloads/proddy/EMS-ESP/total.svg)](https://github.com/proddy/EMS-ESP/releases)
@@ -50,4 +50,4 @@ Follow [these instructions](https://github.com/proddy/EMS-ESP/wiki/Building-and- The firmware fully supports BBQKees' [EMS Gateway](https://shop.hotgoodies.nl/ems/) board with integrated Wemos D1 ESP8266. | ![on boiler](https://github.com/proddy/EMS-ESP/raw/master/doc/ems%20gateway/on-boiler.jpg) | ![kit](https://github.com/proddy/EMS-ESP/raw/master/doc/ems%20gateway/ems-kit-2.jpg) | ![basic circuit](https://github.com/proddy/EMS-ESP/raw/master/doc/ems%20gateway/ems-board-white.jpg) | -| ------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------- | \ No newline at end of file +| ------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------- | diff --git a/doc/home assistant/climate.yaml b/doc/home assistant/climate.yaml index 80e2c0783..b070f06b2 100644 --- a/doc/home assistant/climate.yaml +++ b/doc/home assistant/climate.yaml @@ -4,14 +4,16 @@ - "auto" - "heat" - "off" + mode_command_topic: "home/ems-esp/thermostat_cmd_mode1" temperature_command_topic: "home/ems-esp/thermostat_cmd_temp1" mode_state_topic: "home/ems-esp/thermostat_data" current_temperature_topic: "home/ems-esp/thermostat_data" temperature_state_topic: "home/ems-esp/thermostat_data" + + mode_state_template: "{% if value_json.hc1.mode in ['manual', 'day'] %} heat {% elif value_json.hc1.mode in ['night', 'off'] %} off {% else %} auto {% endif %}" - mode_state_template: "{{ value_json.hc1.mode }}" current_temperature_template: "{{ value_json.hc1.currtemp }}" temperature_state_template: "{{ value_json.hc1.seltemp }}" diff --git a/doc/home assistant/sensor.yaml b/doc/home assistant/sensor.yaml index f63171665..c91d367d8 100644 --- a/doc/home assistant/sensor.yaml +++ b/doc/home assistant/sensor.yaml @@ -20,16 +20,6 @@ # boiler -- platform: mqtt - state_topic: 'home/ems-esp/boiler_data' - name: 'Tap Water' - value_template: '{{ value_json.tapwaterActive }}' - -- platform: mqtt - state_topic: 'home/ems-esp/boiler_data' - name: 'Heating' - value_template: '{{ value_json.heatingActive }}' - - platform: mqtt state_topic: 'home/ems-esp/boiler_data' name: 'Warm Water selected temperature' diff --git a/doc/home assistant/ui-lovelace.yaml b/doc/home assistant/ui-lovelace.yaml index 59f96d36e..4f4458658 100644 --- a/doc/home assistant/ui-lovelace.yaml +++ b/doc/home assistant/ui-lovelace.yaml @@ -1,20 +1,26 @@ -title: Home -views: - - - title: Heating + - id: ems-esp_id0 + title: Heating cards: - - type: entities + - id: ems-esp_id1 + type: glance + entities: + - entity: binary_sensor.tap_water + icon: mdi:fire + - entity: binary_sensor.heating + icon: mdi:radiator + - id: ems-esp_id2 + type: entities title: Boiler show_header_toggle: false entities: - sensor.boiler_boottime - sensor.boiler_updated + - sensor.ems_esp_status - type: divider - sensor.warm_water_selected_temperature - sensor.warm_water_current_temperature - sensor.warm_water_activated - sensor.warm_water_3_way_valve - - sensor.warm_water_tapwater_flow_rate - type: divider - sensor.boiler_temperature - sensor.return_temperature @@ -27,36 +33,43 @@ views: - sensor.system_pressure - sensor.burner_max_power - sensor.burner_current_power - - - type: vertical-stack + + - id: ems-esp_id3 + type: vertical-stack cards: - - type: entities - title: Shower Monitor - show_header_toggle: false - entities: - - switch.shower_timer - - switch.long_shower_alert - - type: divider - - sensor.last_shower_duration - - sensor.showertime_time - - type: entity-button - icon: mdi:shower-head - name: send a cold shot of shower water - entity: script.shower_coldshot - tap_action: - action: call-service - service: script.turn_on - service_data: - entity_id: script.shower_coldshot + - type: entities + title: Shower Monitor + show_header_toggle: false + entities: + - switch.shower_timer + - switch.long_shower_alert + - type: divider + - sensor.last_shower_duration + - sensor.showertime_time + - type: entity-button + icon: mdi:shower-head + name: send a cold shot of shower water + entity: script.shower_coldshot + tap_action: + action: call-service + service: script.turn_on + service_data: + entity_id: script.shower_coldshot + + - type: history-graph + entities: + - sensor.ems_esp_wifi + - sensor.ems_esp_freemem - - type: vertical-stack - cards: - - type: history-graph - entities: - - sensor.current_room_temperature - - sensor.dark_sky_temperature - - type: thermostat - entity: climate.thermostat - - type: thermostat - name: WarmWater - entity: climate.boiler + - id: ems-esp_id4 + type: vertical-stack + cards: + - type: history-graph + entities: + - sensor.pc_room_sensor_temperature + - sensor.current_room_temperature + - sensor.dark_sky_temperature + - type: thermostat + entity: climate.thermostat + - type: thermostat + entity: climate.boiler diff --git a/doc/schematics/Schematic_EMS-ESP-supercap.png b/doc/schematics/Schematic_EMS-ESP-supercap.png deleted file mode 100644 index 561a5f462..000000000 Binary files a/doc/schematics/Schematic_EMS-ESP-supercap.png and /dev/null differ diff --git a/doc/schematics/Schematic_EMS-ESP.png b/doc/schematics/Schematic_EMS-ESP.png new file mode 100644 index 000000000..56f08f387 Binary files /dev/null and b/doc/schematics/Schematic_EMS-ESP.png differ diff --git a/platformio.ini b/platformio.ini index b818c6f33..747006030 100644 --- a/platformio.ini +++ b/platformio.ini @@ -3,7 +3,8 @@ ; [platformio] -default_envs = debug +default_envs = release +;default_envs = debug [common] ; custom build options are: @@ -11,25 +12,48 @@ default_envs = debug ; -DTESTS ; -DCRASH ; -DFORCE_SERIAL -custom_flags = +; -DMYESP_DEBUG +;custom_flags = -DFORCE_SERIAL -DMYESP_DEBUG +custom_flags = -;general_flags = -DNO_GLOBAL_EEPROM -DPIO_FRAMEWORK_ARDUINO_LWIP2_HIGHER_BANDWIDTH -DBEARSSL_SSL_BASIC +# Available lwIP variants (macros): +# -DPIO_FRAMEWORK_ARDUINO_LWIP_HIGHER_BANDWIDTH = v1.4 Higher Bandwidth (default) +# -DPIO_FRAMEWORK_ARDUINO_LWIP2_LOW_MEMORY = v2 Lower Memory +# -DPIO_FRAMEWORK_ARDUINO_LWIP2_HIGHER_BANDWIDTH = v2 Higher Bandwidth +# -DPIO_FRAMEWORK_ARDUINO_LWIP2_HIGHER_BANDWIDTH_LOW_FLASH +# -DPIO_FRAMEWORK_ARDUINO_LWIP2_IPV6_LOW_MEMORY +# Other flags +# -DVTABLES_IN_FLASH +# -DNO_GLOBAL_EEPROM +# -DBEARSSL_SSL_BASIC general_flags = -DNO_GLOBAL_EEPROM +# From https://github.com/esp8266/Arduino/blob/master/tools/sdk/ld +# eagle.flash.4m1m.ld = 1019 KB sketch, 1000 KB SPIFFS. 4KB EEPROM, 4KB RFCAL, 12KB WIFI stack, 2052 KB OTA & buffer +# eagle.flash.4m2m.ld = same as above but with 2024 KB SPIFFS +# eagle.flash.4m.ld = same as above but with no SPIFFS storage +build_flags_4m1m = -Wl,-Teagle.flash.4m1m.ld +build_flags = ${common.general_flags} ${common.build_flags_4m1m} + [env] framework = arduino +;platform = espressif8266@2.2.2 ; arduino core 2.5.2 platform = espressif8266 +;platform = https://github.com/platformio/platform-espressif8266#develop +;platform = https://github.com/platformio/platform-espressif8266#feature/stage lib_deps = - https://github.com/bakercp/CRC32 - https://github.com/rlogiacco/CircularBuffer + https://github.com/rlogiacco/CircularBuffer https://github.com/PaulStoffregen/OneWire - https://github.com/xoseperez/justwifi - https://github.com/marvinroger/async-mqtt-client - https://github.com/xoseperez/eeprom_rotate - https://github.com/bblanchon/ArduinoJson + https://github.com/me-no-dev/ESPAsyncWebServer https://github.com/me-no-dev/ESPAsyncUDP - https://github.com/me-no-dev/ESPAsyncTCP - https://github.com/me-no-dev/ESPAsyncWebServer#b0c6144 + uuid-common@^1.1.0 + uuid-log@^2.1.1 + uuid-syslog@^2.0.4 ; https://github.com/nomis/mcu-uuid-syslog + JustWifi@2.0.2 ; https://github.com/xoseperez/justwifi + AsyncMqttClient@0.8.2 ; https://github.com/marvinroger/async-mqtt-client + EEPROM_Rotate@0.9.2 ; https://github.com/xoseperez/eeprom_rotate + ArduinoJson@6.13.0 ; https://github.com/bblanchon/ArduinoJson + ESPAsyncTCP@1.2.2 ; https://github.com/me-no-dev/ESPAsyncTCP upload_speed = 921600 monitor_speed = 115200 @@ -37,35 +61,54 @@ monitor_speed = 115200 ;upload_port = /dev/cu.wchusbserial14403 ;upload_port = /dev/cu.usbserial-1440 -; comment next 2 lines is not using OTA +; comment out this section if using USB and not OTA for firmware uploads upload_protocol = espota upload_port = ems-esp.local -# Special build for CI test +# +# These following targets are used by TravisCI to build the firmware versions on Release +# Do not modify +# [env:travis] board = esp12e -build_flags = ${common.general_flags} +build_flags = ${common.build_flags} +extra_scripts = scripts/main_script.py [env:esp12e] board = esp12e -build_flags = ${common.general_flags} +build_flags = ${common.build_flags} +extra_scripts = scripts/main_script.py [env:d1_mini] board = d1_mini -build_flags = ${common.general_flags} +build_flags = ${common.build_flags} +extra_scripts = scripts/main_script.py [env:nodemcuv2] board = nodemcuv2 -build_flags = ${common.general_flags} +build_flags = ${common.build_flags} +extra_scripts = scripts/main_script.py [env:nodemcu] board = nodemcu -build_flags = ${common.general_flags} +build_flags = ${common.build_flags} +extra_scripts = scripts/main_script.py +# +# These two targets below (release and debug) can be modified +# [env:debug] board = d1_mini build_type = debug -build_flags = ${common.general_flags} ${common.custom_flags} -extra_scripts = - pre:scripts/rename_fw.py - pre:scripts/buildweb.py +build_flags = ${common.build_flags} ${common.custom_flags} +extra_scripts = + pre:scripts/pre_script.py + scripts/main_script.py + +[env:release] +board = d1_mini +build_type = release +build_flags = ${common.build_flags} ${common.custom_flags} +extra_scripts = + pre:scripts/pre_script.py + scripts/main_script.py diff --git a/scripts/build.sh b/scripts/build.sh old mode 100644 new mode 100755 index d07a51e09..f8492d9af --- a/scripts/build.sh +++ b/scripts/build.sh @@ -11,16 +11,13 @@ is_git() { } stat_bytes() { - echo "size is:" - case "$(uname -s)" in - Darwin) stat -f %z "$1";; - *) stat -c %s "$1";; - esac + filesize=`du -k "$1" | cut -f1;` + echo 'size:' $filesize 'bytes' } # Available environments list_envs() { - grep env: ../platformio.ini | sed 's/\[env:\(.*\)\]/\1/g' + grep env: platformio.ini | sed 's/\[env:\(.*\)\]/\1/g' } print_available() { @@ -59,7 +56,7 @@ set_default_environments() { } build_webui() { - cd ../tools/webfilesbuilder + cd ./tools/webfilesbuilder # Build system uses gulpscript.js to build web interface if [ ! -e node_modules/gulp/bin/gulp.js ]; then @@ -73,26 +70,22 @@ build_webui() { echo "Building web interface..." node node_modules/gulp/bin/gulp.js || exit - # TODO: do something if webui files are different - # for now, just print in travis log - if ${TRAVIS:-false}; then - git --no-pager diff --stat - fi - cd ../.. } build_environments() { echo "--------------------------------------------------------------" echo "Building firmware images..." - mkdir -p $destination/EMS-ESP-$version + # don't move to firmware folder until Travis fixed (see https://github.com/travis-ci/dpl/issues/846#issuecomment-547157406) + # mkdir -p $destination for environment in $environments; do - echo -n "* EMS-ESP-$version-$environment.bin --- " + echo "* EMS-ESP-$version-$environment.bin" platformio run --silent --environment $environment || exit 1 stat_bytes .pio/build/$environment/firmware.bin - [[ "${TRAVIS_BUILD_STAGE_NAME}" = "Test" ]] || \ - mv .pio/build/$environment/firmware.bin $destination/EMS-ESP-$version/EMS-ESP-$version-$environment.bin + # mv .pio/build/$environment/firmware.bin $destination/EMS-ESP-$version-$environment.bin + # mv .pio/build/$environment/firmware.bin EMS-ESP-$version-$environment.bin + mv .pio/build/$environment/firmware.bin EMS-ESP-dev-$environment.bin done echo "--------------------------------------------------------------" } @@ -101,7 +94,7 @@ build_environments() { ####### MAIN destination=firmware -version_file=../src/version.h +version_file=./src/version.h version=$(grep -E '^#define APP_VERSION' $version_file | awk '{print $3}' | sed 's/"//g') if ${TRAVIS:-false}; then @@ -115,6 +108,8 @@ else git_tag= fi +echo $git_tag + if [[ -n $git_tag ]]; then new_version=${version/-*} sed -i -e "s@$version@$new_version@" $version_file @@ -139,7 +134,7 @@ fi travis=$(list_envs | grep travis | sort) # get all taregts, excluding travis and debug -available=$(list_envs | grep -Ev -- 'travis|debug' | sort) +available=$(list_envs | grep -Ev -- 'travis|debug|release' | sort) export PLATFORMIO_BUILD_FLAGS="${PLATFORMIO_BUILD_FLAGS}" @@ -183,7 +178,7 @@ fi # for debugging echo "* git_revision = $git_revision" echo "* git_tag = $git_tag" -echo "* TRAVIS_EVENT_TYPE = $TRAVIS_EVENT_TYPE" +echo "* TRAVIS_COMMIT = $TRAVIS_COMMIT" echo "* TRAVIS_TAG = $TRAVIS_TAG" echo "* TRAVIS_BRANCH = $TRAVIS_BRANCH" echo "* TRAVIS_BUILD_STAGE_NAME = $TRAVIS_BUILD_STAGE_NAME" diff --git a/scripts/buildweb.py b/scripts/buildweb.py deleted file mode 100644 index 96c41fa10..000000000 --- a/scripts/buildweb.py +++ /dev/null @@ -1,3 +0,0 @@ -Import("env") - -env.Execute("node ./tools/webfilesbuilder/node_modules/gulp/bin/gulp.js --silent --cwd ./tools/webfilesbuilder") diff --git a/scripts/checkcode.py b/scripts/checkcode.py deleted file mode 100755 index 53da63054..000000000 --- a/scripts/checkcode.py +++ /dev/null @@ -1,26 +0,0 @@ -#!/usr/bin/env python -from subprocess import call -import os -Import("env") - -def code_check(source, target, env): - print("\n** Starting cppcheck...") - call(["cppcheck", os.getcwd()+"/.", "--force", "--enable=all"]) - print("\n** Finished cppcheck...\n") - print("\n** Starting cpplint...") - call(["cpplint", "--extensions=ino,cpp,h", "--filter=-legal/copyright,-build/include,-whitespace", - "--linelength=120", "--recursive", "src", "lib/myESP"]) - print("\n** Finished cpplint...") - -#my_flags = env.ParseFlags(env['BUILD_FLAGS']) -#defines = {k: v for (k, v) in my_flags.get("CPPDEFINES")} -# print defines -# print env.Dump() - -# built in targets: (buildprog, size, upload, program, buildfs, uploadfs, uploadfsota) -env.AddPreAction("buildprog", code_check) -# env.AddPostAction(.....) - -# see http://docs.platformio.org/en/latest/projectconf/advanced_scripting.html#before-pre-and-after-post-actions -# env.Replace(PROGNAME="firmware_%s" % defines.get("VERSION")) -# env.Replace(PROGNAME="firmware_%s" % env['BOARD']) diff --git a/scripts/main_script.py b/scripts/main_script.py new file mode 100644 index 000000000..61b873da9 --- /dev/null +++ b/scripts/main_script.py @@ -0,0 +1,37 @@ +#!/usr/bin/env python +Import("env") + +class Color(object): + BLACK = '\x1b[1;30m' + RED = '\x1b[1;31m' + GREEN = '\x1b[1;32m' + YELLOW = '\x1b[1;33m' + BLUE = '\x1b[1;34m' + MAGENTA = '\x1b[1;35m' + CYAN = '\x1b[1;36m' + WHITE = '\x1b[1;37m' + LIGHT_GREY = '\x1b[0;30m' + LIGHT_RED = '\x1b[0;31m' + LIGHT_GREEN = '\x1b[0;32m' + LIGHT_YELLOW = '\x1b[0;33m' + LIGHT_BLUE = '\x1b[0;34m' + LIGHT_MAGENTA = '\x1b[0;35m' + LIGHT_CYAN = '\x1b[0;36m' + LIGHT_WHITE = '\x1b[0;37m' + + +def clr(color, text): + return color + str(text) + '\x1b[0m' + +def remove_float_support(): + flags = " ".join(env['LINKFLAGS']) + print(clr(Color.BLUE, "** LINKFLAGS = %ss" % flags)) + flags = flags.replace("-u _printf_float", "") + flags = flags.replace("-u _scanf_float", "") + newflags = flags.split() + + env.Replace( + LINKFLAGS=newflags + ) + +remove_float_support() diff --git a/scripts/rename_fw.py b/scripts/pre_script.py similarity index 55% rename from scripts/rename_fw.py rename to scripts/pre_script.py index 594b6d4d4..ca9db79ba 100755 --- a/scripts/rename_fw.py +++ b/scripts/pre_script.py @@ -4,10 +4,25 @@ import os import re Import("env") -def build_web(source, target, env): - print("\n** Build web...") - call(["gulp", "-f", os.getcwd()+"/tools/webfilesbuilder/gulpfile.js"]) +def build_web(): + print("** Building web...") + env.Execute( + "node ./tools/webfilesbuilder/node_modules/gulp/bin/gulp.js --cwd ./tools/webfilesbuilder") +def code_check(source, target, env): + print("** Starting cppcheck...") + call(["cppcheck", os.getcwd()+"/.", "--force", "--enable=all"]) + print("\n** Finished cppcheck...\n") + print("\n** Starting cpplint...") + call(["cpplint", "--extensions=ino,cpp,h", "--filter=-legal/copyright,-build/include,-whitespace", + "--linelength=120", "--recursive", "src", "lib/myESP"]) + print("\n** Finished cpplint...") + + +# build web files +build_web() + +# extract application details bag = {} exprs = [ (re.compile(r'^#define APP_VERSION\s+"(\S+)"'), 'app_version'), @@ -31,16 +46,9 @@ with open('./src/ems-esp.cpp', 'r') as f: app_version = bag.get('app_version') app_name = bag.get('app_name') app_hostname = bag.get('app_hostname') - board = env['BOARD'] branch = env['PIOENV'] -# build the web files -env.AddPreAction("buildprog", build_web) - # build filename, replacing . with _ for the version -#env.Replace(PROGNAME="firmware_%s" % branch + "_" + app_version.replace(".", "_")) -#env.Replace(PROGNAME=app_name + "-" + app_version.replace(".", "_") + "-" + board + "-" + branch) -env.Replace(PROGNAME=app_name + "-" + app_version.replace(".", "_") + "-" + board) - - +env.Replace(PROGNAME=app_name + "-" + + app_version.replace(".", "_") + "-" + board) diff --git a/src/MyESP.cpp b/src/MyESP.cpp index c75ccee53..eba5cde68 100644 --- a/src/MyESP.cpp +++ b/src/MyESP.cpp @@ -27,6 +27,8 @@ union system_rtcmem_t { static char * _general_password = nullptr; static bool _shouldRestart = false; +static char _debug_buffer[TELNET_MAX_BUFFER_LENGTH]; + uint8_t RtcmemSize = (sizeof(RtcmemData) / 4u); auto Rtcmem = reinterpret_cast(RTCMEM_ADDR); @@ -44,9 +46,10 @@ MyESP::MyESP() { _suspendOutput = false; _ota_pre_callback_f = nullptr; _ota_post_callback_f = nullptr; - _load_average = 100; // calculated load average - _general_serial = true; // serial is set to on as default - _general_log_events = true; // all logs are written to an event log in SPIFFS + _load_average = 100; // calculated load average + _general_serial = true; // serial is set to on as default + _general_log_events = false; // all logs are not sent to syslog by default + _general_log_ip = nullptr; _have_ntp_time = false; // telnet @@ -77,9 +80,13 @@ MyESP::MyESP() { _mqtt_will_offline_payload = strdup(MQTT_WILL_OFFLINE_PAYLOAD); // network - _network_password = nullptr; - _network_ssid = nullptr; - _network_wmode = 1; // default AP + _network_password = nullptr; + _network_ssid = nullptr; + _network_wmode = 1; // default AP + _network_staticip = nullptr; + _network_gatewayip = nullptr; + _network_nmask = nullptr; + _network_dnsip = nullptr; _wifi_callback_f = nullptr; _wifi_connected = false; @@ -97,8 +104,9 @@ MyESP::MyESP() { // ntp _ntp_server = strdup(MYESP_NTP_SERVER); - _ntp_interval = 60; + _ntp_interval = NTP_INTERVAL_DEFAULT; _ntp_enabled = false; + _ntp_timezone = NTP_TIMEZONE_DEFAULT; // get the build time _buildTime = _getBuildTime(); @@ -136,14 +144,14 @@ void MyESP::myDebug(const char * format, ...) { char test[1]; int len = ets_vsnprintf(test, 1, format, args) + 1; + if (len > TELNET_MAX_BUFFER_LENGTH - 1) { + len = TELNET_MAX_BUFFER_LENGTH; + } - char * buffer = new char[len]; - ets_vsnprintf(buffer, len, format, args); + ets_vsnprintf(_debug_buffer, len, format, args); va_end(args); - SerialAndTelnet.println(buffer); - - delete[] buffer; + SerialAndTelnet.println(_debug_buffer); } // for flashmemory. Must use PSTR() @@ -158,9 +166,11 @@ void MyESP::myDebug_P(PGM_P format_P, ...) { va_start(args, format_P); char test[1]; int len = ets_vsnprintf(test, 1, format, args) + 1; + if (len > TELNET_MAX_BUFFER_LENGTH - 1) { + len = TELNET_MAX_BUFFER_LENGTH; + } - char * buffer = new char[len]; - ets_vsnprintf(buffer, len, format, args); + ets_vsnprintf(_debug_buffer, len, format, args); va_end(args); @@ -171,9 +181,7 @@ void MyESP::myDebug_P(PGM_P format_P, ...) { SerialAndTelnet.print(timestamp); #endif - SerialAndTelnet.println(buffer); - - delete[] buffer; + SerialAndTelnet.println(_debug_buffer); } // use Serial? @@ -200,9 +208,7 @@ uint32_t MyESP::_getInitialFreeHeap() { // called when WiFi is connected, and used to start OTA, MQTT void MyESP::_wifiCallback(justwifi_messages_t code, char * parameter) { if (code == MESSAGE_CONNECTED) { -#if defined(ESP8266) - WiFi.setSleepMode(WIFI_NONE_SLEEP); // added to possibly fix wifi dropouts in arduino core 2.5.0 -#endif + _wifi_connected = true; jw.enableAPFallback(false); // Disable AP mode after initial connect was successful - test for https://github.com/proddy/EMS-ESP/issues/187 @@ -237,9 +243,6 @@ void MyESP::_wifiCallback(justwifi_messages_t code, char * parameter) { } */ - // MQTT Setup - _mqtt_setup(); - // if we don't want Serial anymore, turn it off if (!_general_serial) { myDebug_P(PSTR("[SYSTEM] Disabling serial port communication")); @@ -251,16 +254,14 @@ void MyESP::_wifiCallback(justwifi_messages_t code, char * parameter) { // NTP now that we have a WiFi connection if (_ntp_enabled) { - NTP.Ntp(_ntp_server, _ntp_interval); // set up NTP server - myDebug_P(PSTR("[NTP] NTP internet time enabled via server %s"), _ntp_server); + NTP.Ntp(_ntp_server, _ntp_interval, _ntp_timezone); // set up NTP server + myDebug_P(PSTR("[NTP] NTP internet time enabled via server %s with timezone %d"), _ntp_server, _ntp_timezone); } // call any final custom stuff if (_wifi_callback_f) { _wifi_callback_f(); } - - _wifi_connected = true; } if (code == MESSAGE_ACCESSPOINT_CREATED) { @@ -353,7 +354,9 @@ void MyESP::_mqttOnMessage(char * topic, char * payload, size_t len) { char message[len + 1]; strlcpy(message, (char *)payload, len + 1); - // myDebug_P(PSTR("[MQTT] Received %s => %s"), topic, message); // enable for debugging +#ifdef MYESP_DEBUG + myDebug_P(PSTR("[MQTT] Received %s => %s"), topic, message); +#endif // topics are in format MQTT_BASE/HOSTNAME/TOPIC char * topic_magnitude = strrchr(topic, '/'); // strip out everything until last / @@ -380,11 +383,13 @@ bool MyESP::mqttSubscribe(const char * topic) { char * topic_s = _mqttTopic(topic); uint16_t packet_id = mqttClient.subscribe(topic_s, _mqtt_qos); - // myDebug_P(PSTR("[MQTT] Subscribing to %s"), topic_s); +#ifdef MYESP_DEBUG + myDebug_P(PSTR("[MQTT] Subscribing to %s"), topic_s); +#endif if (packet_id) { // add to mqtt log - _addMQTTLog(topic_s, "", 2); // type of 2 means Subscribe. Has an empty payload for now + _addMQTTLog(topic_s, "", MYESP_MQTTLOGTYPE_SUBSCRIBE); // Has an empty payload for now return true; } else { myDebug_P(PSTR("[MQTT] Error subscribing to %s, error %d"), _mqttTopic(topic), packet_id); @@ -412,11 +417,13 @@ bool MyESP::mqttPublish(const char * topic, const char * payload) { // returns true if all good bool MyESP::mqttPublish(const char * topic, const char * payload, bool retain) { if (mqttClient.connected() && (strlen(topic) > 0)) { - //myDebug_P(PSTR("[MQTT] Sending publish to %s with payload %s"), _mqttTopic(topic), payload); // for debugging +#ifdef MYESP_DEBUG + myDebug_P(PSTR("[MQTT] Sending publish to %s with payload %s"), _mqttTopic(topic), payload); +#endif uint16_t packet_id = mqttClient.publish(_mqttTopic(topic), _mqtt_qos, retain, payload); if (packet_id) { - _addMQTTLog(topic, payload, 1); // add to the log, using type of 1 for Publish + _addMQTTLog(topic, payload, MYESP_MQTTLOGTYPE_PUBLISH); // add to the log return true; } else { myDebug_P(PSTR("[MQTT] Error publishing to %s with payload %s [error %d]"), _mqttTopic(topic), payload, packet_id); @@ -428,9 +435,9 @@ bool MyESP::mqttPublish(const char * topic, const char * payload, bool retain) { // MQTT onConnect - when a connect is established void MyESP::_mqttOnConnect() { - myDebug_P(PSTR("[MQTT] MQTT connected established")); - _mqtt_reconnect_delay = MQTT_RECONNECT_DELAY_MIN; + myDebug_P(PSTR("[MQTT] MQTT connected")); + _mqtt_reconnect_delay = MQTT_RECONNECT_DELAY_MIN; _mqtt_last_connection = millis(); // say we're alive to the Last Will topic @@ -445,7 +452,7 @@ void MyESP::_mqttOnConnect() { mqttPublish(MQTT_TOPIC_START, MQTT_TOPIC_START_PAYLOAD, false); // send heartbeat if enabled - _heartbeatCheck(true); + _heartbeatCheck(); // call custom function to handle mqtt receives (_mqtt_callback_f)(MQTT_CONNECT_EVENT, nullptr, nullptr); @@ -453,17 +460,11 @@ void MyESP::_mqttOnConnect() { // MQTT setup void MyESP::_mqtt_setup() { - if (!_mqtt_enabled) { - myDebug_P(PSTR("[MQTT] is disabled")); - } - mqttClient.onConnect([this](bool sessionPresent) { _mqttOnConnect(); }); mqttClient.onDisconnect([this](AsyncMqttClientDisconnectReason reason) { if (reason == AsyncMqttClientDisconnectReason::TCP_DISCONNECTED) { myDebug_P(PSTR("[MQTT] TCP Disconnected")); - _increaseSystemDropoutCounter(); // +1 to number of disconnects - (_mqtt_callback_f)(MQTT_DISCONNECT_EVENT, nullptr, nullptr); // call callback with disconnect } if (reason == AsyncMqttClientDisconnectReason::MQTT_IDENTIFIER_REJECTED) { myDebug_P(PSTR("[MQTT] Identifier Rejected")); @@ -481,6 +482,10 @@ void MyESP::_mqtt_setup() { // Reset reconnection delay _mqtt_last_connection = millis(); _mqtt_connecting = false; + + _increaseSystemDropoutCounter(); // +1 to number of disconnects + myDebug_P(PSTR("[MQTT] Disconnected! (count %d)"), _getSystemDropoutCounter()); + (_mqtt_callback_f)(MQTT_DISCONNECT_EVENT, nullptr, nullptr); // call callback with disconnect }); //mqttClient.onSubscribe([this](uint16_t packetId, uint8_t qos) { myDebug_P(PSTR("[MQTT] Subscribe ACK for PID %d"), packetId); }); @@ -489,15 +494,43 @@ void MyESP::_mqtt_setup() { mqttClient.onMessage([this](char * topic, char * payload, AsyncMqttClientMessageProperties properties, size_t len, size_t index, size_t total) { _mqttOnMessage(topic, payload, len); }); + + mqttClient.setServer(_mqtt_ip, _mqtt_port); + mqttClient.setClientId(_general_hostname); + mqttClient.setKeepAlive(_mqtt_keepalive); + mqttClient.setCleanSession(false); + + // last will + if (_hasValue(_mqtt_will_topic)) { + mqttClient.setWill(_mqttTopic(_mqtt_will_topic), 1, true, + _mqtt_will_offline_payload); // retain always true + } + + // set credentials if we have them + if (_hasValue(_mqtt_user)) { + mqttClient.setCredentials(_mqtt_user, _mqtt_password); + } + + _mqtt_connecting = false; + _mqtt_last_connection = millis(); } // WiFI setup void MyESP::_wifi_setup() { jw.setHostname(_general_hostname); // Set WIFI hostname - jw.subscribe([this](justwifi_messages_t code, char * parameter) { _wifiCallback(code, parameter); }); jw.setConnectTimeout(MYESP_WIFI_CONNECT_TIMEOUT); jw.setReconnectTimeout(MYESP_WIFI_RECONNECT_INTERVAL); +#if defined(ESP8266) + WiFi.setSleepMode(WIFI_NONE_SLEEP); // added to possibly fix wifi dropouts in arduino core 2.5.0 + // ref: https://github.com/esp8266/Arduino/issues/6471 + // ref: https://github.com/esp8266/Arduino/issues/6366 + // high tx power causing weird behavior, slighly lowering from 20.5 to 20.0 may help stability + WiFi.setOutputPower(20.0); // in DBM +#endif + + jw.subscribe([this](justwifi_messages_t code, char * parameter) { _wifiCallback(code, parameter); }); + /// wmode 1 is AP, 0 is client if (_network_wmode == 1) { jw.enableAP(true); @@ -505,11 +538,19 @@ void MyESP::_wifi_setup() { jw.enableAP(false); } - jw.enableAPFallback(true); // AP mode only as fallback - jw.enableSTA(true); // Enable STA mode (connecting to a router) - jw.enableScan(false); // Configure it to not scan available networks and connect in order of dBm - jw.cleanNetworks(); // Clean existing network configuration - jw.addNetwork(_network_ssid, _network_password); // Add a network + jw.enableAPFallback(true); // AP mode only as fallback + jw.enableSTA(true); // Enable STA mode (connecting to a router) + jw.enableScan(false); // Configure it to not scan available networks and connect in order of dBm + jw.cleanNetworks(); // Clean existing network configuration + + if (_hasValue(_network_staticip)) { +#if MYESP_DEBUG + myDebug_P(PSTR("[WIFI] Using fixed IP")); +#endif + jw.addNetwork(_network_ssid, _network_password, _network_staticip, _network_gatewayip, _network_nmask, _network_dnsip); // fixed IP + } else { + jw.addNetwork(_network_ssid, _network_password); // use DHCP + } } // set the callback function for the OTA onstart @@ -613,7 +654,9 @@ void MyESP::_telnetConnected() { } void MyESP::_telnetDisconnected() { - // myDebug_P(PSTR("[TELNET] Telnet connection closed")); +#ifdef MYESP_DEBUG + myDebug_P(PSTR("[TELNET] Telnet connection closed")); +#endif if (_telnet_callback_f) { (_telnet_callback_f)(TELNET_EVENT_DISCONNECT); // call callback } @@ -632,6 +675,27 @@ void MyESP::_telnet_setup() { memset(_command, 0, TELNET_MAX_COMMAND_LENGTH); } +// restart some services like web, mqtt, ntp etc... +void MyESP::_kick() { + myDebug_P(PSTR("Kicking services...")); + + // NTP - fetch new time + if (_ntp_enabled) { + myDebug_P(PSTR(" - Requesting NTP time")); + NTP.getNtpTime(); + } + + // kick web + myDebug_P(PSTR(" - Restarting web server and web sockets")); + _ws->enable(false); + //_webServer->reset(); + _ws->enable(true); + + // kick mqtt + myDebug_P(PSTR(" - Restarting MQTT")); + mqttClient.disconnect(); +} + // Show help of commands void MyESP::_consoleShowHelp() { myDebug_P(PSTR("")); @@ -652,7 +716,7 @@ void MyESP::_consoleShowHelp() { myDebug_P(PSTR("*")); myDebug_P(PSTR("* Commands:")); myDebug_P(PSTR("* ?/help=show commands, CTRL-D/quit=close telnet session")); - myDebug_P(PSTR("* set, system, restart, mqttlog")); + myDebug_P(PSTR("* set, system, restart, mqttlog, kick, save")); #ifdef CRASH myDebug_P(PSTR("* crash ")); @@ -689,13 +753,15 @@ void MyESP::_printSetCommands() { myDebug_P(PSTR(" set [value]")); myDebug_P(PSTR(" set mqtt_enabled ")); myDebug_P(PSTR(" set [value]")); - myDebug_P(PSTR(" set mqtt_heartbeat ")); + myDebug_P(PSTR(" set mqtt_heartbeat (every 2 mins)")); myDebug_P(PSTR(" set mqtt_base [string]")); myDebug_P(PSTR(" set mqtt_port [number]")); - myDebug_P(PSTR(" set mqtt_qos [0,1,2,3]")); + myDebug_P(PSTR(" set mqtt_qos [0-3]")); myDebug_P(PSTR(" set mqtt_keepalive [seconds]")); myDebug_P(PSTR(" set mqtt_retain [on | off]")); myDebug_P(PSTR(" set ntp_enabled ")); + myDebug_P(PSTR(" set ntp_interval [minutes]")); + myDebug_P(PSTR(" set ntp_timezone [n]")); myDebug_P(PSTR(" set serial ")); myDebug_P(PSTR(" set log_events ")); @@ -724,6 +790,9 @@ void MyESP::_printSetCommands() { } } myDebug_P(PSTR("")); + if (_hasValue(_network_staticip)) { + myDebug_P(PSTR(" wifi_staticip=%s"), _network_staticip); + } myDebug_P(PSTR(" mqtt_enabled=%s"), (_mqtt_enabled) ? "on" : "off"); if (_hasValue(_mqtt_ip)) { myDebug_P(PSTR(" mqtt_ip=%s"), _mqtt_ip); @@ -760,7 +829,15 @@ void MyESP::_printSetCommands() { #endif myDebug_P(PSTR(" ntp_enabled=%s"), (_ntp_enabled) ? "on" : "off"); + myDebug_P(PSTR(" ntp_interval=%d"), _ntp_interval); + myDebug_P(PSTR(" ntp_timezone=%d"), _ntp_timezone); + myDebug_P(PSTR(" log_events=%s"), (_general_log_events) ? "on" : "off"); + if (_hasValue(_general_log_ip)) { + myDebug_P(PSTR(" log_ip=%s"), _general_log_ip); + } else { + myDebug_P(PSTR(" log_ip=")); + } // print any custom settings if (_fs_setlist_callback_f) { @@ -773,8 +850,8 @@ void MyESP::_printSetCommands() { // reset / restart void MyESP::resetESP() { myDebug_P(PSTR("* Restart ESP...")); - _deferredReset(500, CUSTOM_RESET_TERMINAL); end(); + _deferredReset(500, CUSTOM_RESET_TERMINAL); #if defined(ARDUINO_ARCH_ESP32) ESP.restart(); #else @@ -849,8 +926,14 @@ bool MyESP::_changeSetting(uint8_t wc, const char * setting, const char * value) save_config = fs_setSettingValue(&_mqtt_heartbeat, value, false); } else if (strcmp(setting, "ntp_enabled") == 0) { save_config = fs_setSettingValue(&_ntp_enabled, value, false); + } else if (strcmp(setting, "ntp_interval") == 0) { + save_config = fs_setSettingValue(&_ntp_interval, value, NTP_INTERVAL_DEFAULT); + } else if (strcmp(setting, "ntp_timezone") == 0) { + save_config = fs_setSettingValue(&_ntp_timezone, value, NTP_TIMEZONE_DEFAULT); } else if (strcmp(setting, "log_events") == 0) { save_config = fs_setSettingValue(&_general_log_events, value, false); + } else if (strcmp(setting, "log_ip") == 0) { + save_config = fs_setSettingValue(&_general_log_ip, value, ""); } else { // finally check for any custom commands if (_fs_setlist_callback_f) { @@ -946,6 +1029,12 @@ void MyESP::_telnetCommand(char * commandLine) { return; } + // kick command + if ((strcmp(ptrToCommandName, "kick") == 0) && (wc == 1)) { + _kick(); + return; + } + // restart command if (((strcmp(ptrToCommandName, "restart") == 0) || (strcmp(ptrToCommandName, "reboot") == 0)) && (wc == 1)) { resetESP(); @@ -964,6 +1053,13 @@ void MyESP::_telnetCommand(char * commandLine) { return; } + // save everything + if ((strcmp(ptrToCommandName, "save") == 0) && (wc == 1)) { + _fs_writeConfig(); + _fs_createCustomConfig(); + return; + } + // show system stats if ((strcmp(ptrToCommandName, "quit") == 0) && (wc == 1)) { myDebug_P(PSTR("[TELNET] exiting telnet session")); @@ -1061,7 +1157,6 @@ void MyESP::_setSystemBootStatus(uint8_t status) { data.value = Rtcmem->sys; data.parts.boot_status = status; Rtcmem->sys = data.value; - // myDebug("*** setting boot status to %d", data.parts.boot_status); } uint8_t MyESP::_getSystemStabilityCounter() { @@ -1282,8 +1377,9 @@ void MyESP::showSystemStats() { myDebug_P(PSTR(" [MQTT] is disconnected")); } - if (_ntp_enabled) { - myDebug_P(PSTR(" [NTP] Time in UTC is %02d:%02d:%02d"), to_hour(now()), to_minute(now()), to_second(now())); + if (_have_ntp_time) { + uint32_t real_time = getSystemTime(); + myDebug_P(PSTR(" [NTP] Local Time is %02d:%02d:%02d %s (%d)"), to_hour(real_time), to_minute(real_time), to_second(real_time), NTP.tcr->abbrev, real_time); } #ifdef CRASH @@ -1346,16 +1442,7 @@ void MyESP::showSystemStats() { myDebug_P(PSTR(" [MEM] Firmware size: %d"), ESP.getSketchSize()); myDebug_P(PSTR(" [MEM] Max OTA size: %d"), (ESP.getFreeSketchSpace() - 0x1000) & 0xFFFFF000); myDebug_P(PSTR(" [MEM] OTA Reserved: %d"), 4 * SPI_FLASH_SEC_SIZE); - - uint32_t total_memory = _getInitialFreeHeap(); - uint32_t free_memory = ESP.getFreeHeap(); - - myDebug(" [MEM] Free Heap: %d bytes initially | %d bytes used (%2u%%) | %d bytes free (%2u%%)", - total_memory, - total_memory - free_memory, - 100 * (total_memory - free_memory) / total_memory, - free_memory, - 100 * free_memory / total_memory); + _printHeap(" [MEM]"); myDebug_P(PSTR("")); } @@ -1363,14 +1450,20 @@ void MyESP::showSystemStats() { /* * Send heartbeat via MQTT with all system data */ -void MyESP::_heartbeatCheck(bool force = false) { +void MyESP::_heartbeatCheck(bool force) { static uint32_t last_heartbeat = 0; if ((millis() - last_heartbeat > MYESP_HEARTBEAT_INTERVAL) || force) { last_heartbeat = millis(); - // _printHeap("Heartbeat"); // for heartbeat debugging + // print to log if force is set, so at bootup + if (force) { + _printHeap("[SYSTEM]"); + } +#ifdef MYESP_DEBUG + _printHeap("[HEARTBEAT] "); +#endif if (!isMQTTConnected() || !(_mqtt_heartbeat)) { return; } @@ -1379,8 +1472,8 @@ void MyESP::_heartbeatCheck(bool force = false) { uint32_t free_memory = ESP.getFreeHeap(); uint8_t mem_available = 100 * free_memory / total_memory; // as a % - StaticJsonDocument<200> doc; - JsonObject rootHeartbeat = doc.to(); + StaticJsonDocument doc; + JsonObject rootHeartbeat = doc.to(); rootHeartbeat["version"] = _app_version; rootHeartbeat["IP"] = WiFi.localIP().toString(); @@ -1393,17 +1486,36 @@ void MyESP::_heartbeatCheck(bool force = false) { char data[300] = {0}; serializeJson(doc, data, sizeof(data)); - // myDebugLog("Publishing hearbeat via MQTT"); - (void)mqttPublish(MQTT_TOPIC_HEARTBEAT, data, false); // send to MQTT with retain off } } +/* + * Print out heartbeat + */ +void MyESP::heartbeatPrint() { + static int i = 0; + + uint32_t total_memory = _getInitialFreeHeap(); + uint32_t free_memory = ESP.getFreeHeap(); + + myDebug("[%d] uptime:%d bytesfree:%d (%2u%%), load:%d, dropouts:%d", + i++, + _getUptime(), + free_memory, + 100 * free_memory / total_memory, + getSystemLoadAverage(), + _getSystemDropoutCounter() + + ); +} + // handler for Telnet void MyESP::_telnetHandle() { SerialAndTelnet.handle(); static uint8_t charsRead = 0; + // read asynchronously until full command input while (SerialAndTelnet.available()) { char c = SerialAndTelnet.read(); @@ -1486,26 +1598,8 @@ void MyESP::_mqttConnect() { _mqtt_reconnect_delay = MQTT_RECONNECT_DELAY_MAX; } - mqttClient.setServer(_mqtt_ip, _mqtt_port); - mqttClient.setClientId(_general_hostname); - mqttClient.setKeepAlive(_mqtt_keepalive); - mqttClient.setCleanSession(false); - - // last will - if (_hasValue(_mqtt_will_topic)) { - //myDebug_P(PSTR("[MQTT] Setting last will topic %s"), _mqttTopic(_mqtt_will_topic)); - mqttClient.setWill(_mqttTopic(_mqtt_will_topic), 1, true, - _mqtt_will_offline_payload); // retain always true - } - - if (_hasValue(_mqtt_user)) { - myDebug_P(PSTR("[MQTT] Connecting to MQTT using user %s..."), _mqtt_user); - mqttClient.setCredentials(_mqtt_user, _mqtt_password); - } else { - myDebug_P(PSTR("[MQTT] Connecting to MQTT...")); - } - // Connect to the MQTT broker + myDebug_P(PSTR("[MQTT] Connecting to MQTT...")); mqttClient.connect(); } @@ -1540,18 +1634,20 @@ char * MyESP::_mqttTopic(const char * topic) { // validates a file in SPIFFS, loads it into the json buffer and returns true if ok size_t MyESP::_fs_validateConfigFile(const char * filename, size_t maxsize, JsonDocument & doc) { +#ifdef MYESP_DEBUG + myDebug_P(PSTR("[FS] Checking file %s"), filename); +#endif + // see if we can open it File file = SPIFFS.open(filename, "r"); if (!file) { - myDebug_P(PSTR("[FS] File %s not found"), filename); + myDebug_P(PSTR("[FS] Cannot open config file %s for reading"), filename); return 0; } // check size size_t size = file.size(); - // myDebug_P(PSTR("[FS] Checking file %s (%d bytes)"), filename, size); // remove for debugging - if (size > maxsize) { file.close(); myDebug_P(PSTR("[FS] Error. File %s size %d is too large (max %d)"), filename, size, maxsize); @@ -1572,7 +1668,6 @@ size_t MyESP::_fs_validateConfigFile(const char * filename, size_t maxsize, Json delete[] buffer; return 0; } - // now read into the given json DeserializationError error = deserializeJson(doc, buffer); if (error) { @@ -1581,107 +1676,15 @@ size_t MyESP::_fs_validateConfigFile(const char * filename, size_t maxsize, Json return 0; } - // serializeJsonPretty(doc, Serial); // enable for debugging +#ifdef MYESP_DEBUG + serializeJsonPretty(doc, Serial); +#endif file.close(); delete[] buffer; return size; } -// validates the event log file in SPIFFS -// returns true if all OK -size_t MyESP::_fs_validateLogFile(const char * filename) { - // exit if we have disabled logging - if (!_general_log_events) { - return 0; - } - - // see if we can open it - File eventlog = SPIFFS.open(filename, "r"); - if (!eventlog) { - myDebug_P(PSTR("[FS] File %s not found"), filename); - return 0; - } - - // check sizes - size_t size = eventlog.size(); - size_t maxsize = ESP.getFreeHeap() - 2000; // reserve some buffer - // myDebug_P(PSTR("[FS] Checking file %s (%d/%d bytes)"), filename, size, maxsize); // remove for debugging - if (size > maxsize) { - eventlog.close(); - myDebug_P(PSTR("[FS] File %s size %d is too large"), filename, size); - return 0; - } else if (size == 0) { - eventlog.close(); - myDebug_P(PSTR("[FS] Corrupted file %s"), filename); - return 0; - } - - /* - // check integrity by reading file from SPIFFS into the char array - char * buffer = new char[size + 2]; // reserve some memory to read in the file - size_t real_size = file.readBytes(buffer, size); - if (real_size != size) { - file.close(); - myDebug_P(PSTR("[FS] Error, file %s sizes don't match (%d/%d), looks corrupted"), filename, real_size, size); - delete[] buffer; - return false; - } - file.close(); - delete[] buffer; - */ - - /* - File configFile = SPIFFS.open(filename, "r"); - myDebug_P(PSTR("[FS] File: ")); - while (configFile.available()) { - SerialAndTelnet.print((char)configFile.read()); - } - myDebug_P(PSTR("[FS] end")); // newline - configFile.close(); - */ - - // parse it to check JSON validity - // its slow but the only reliable way to check integrity of the file - uint16_t char_count = 0; - bool abort = false; - char char_buffer[MYESP_JSON_LOG_MAXSIZE]; - StaticJsonDocument doc; - - // eventlog.seek(0); - while (eventlog.available() && !abort) { - char c = eventlog.read(); // read a char - - // see if we have reached the end of the string - if (c == '\0' || c == '\n') { - char_buffer[char_count] = '\0'; // terminate and add it to the list - // Serial.printf("Got line: %s\n", char_buffer); // for debugging - // validate it by looking at JSON structure - DeserializationError error = deserializeJson(doc, char_buffer); - if (error) { - myDebug_P(PSTR("[FS] Event log has a corrupted entry (error %s)"), error.c_str()); - abort = true; - } - char_count = 0; // start new record - } else { - // add the char to the buffer if recording, checking for overrun - if (char_count < MYESP_JSON_LOG_MAXSIZE) { - char_buffer[char_count++] = c; - } else { - abort = true; // reached limit of our line buffer - } - } - } - - eventlog.close(); - - if (abort) { - return 0; - } - - return size; -} - // format File System void MyESP::_fs_eraseConfig() { myDebug_P(PSTR("[FS] Performing a factory reset...")); @@ -1702,12 +1705,6 @@ void MyESP::setSettings(fs_loadsave_callback_f loadsave, fs_setlist_callback_f s // load system config from SPIFFS // returns false on error or the file needs to be recreated bool MyESP::_fs_loadConfig() { - // see if old file exists and delete it - if (SPIFFS.exists("/config.json")) { - SPIFFS.remove("/config.json"); - myDebug_P(PSTR("[FS] Removed old config version")); - } - StaticJsonDocument doc; // set to true to print out contents of file @@ -1721,12 +1718,17 @@ bool MyESP::_fs_loadConfig() { _network_ssid = strdup(network["ssid"] | ""); _network_password = strdup(network["password"] | ""); _network_wmode = network["wmode"]; // 0 is client, 1 is AP + _network_staticip = strdup(network["staticip"] | ""); + _network_gatewayip = strdup(network["gatewayip"] | ""); + _network_nmask = strdup(network["nmask"] | ""); + _network_dnsip = strdup(network["dnsip"] | ""); JsonObject general = doc["general"]; _general_password = strdup(general["password"] | MYESP_HTTP_PASSWORD); _ws->setAuthentication("admin", _general_password); _general_hostname = strdup(general["hostname"]); - _general_log_events = general["log_events"]; + _general_log_events = general["log_events"] | false; + _general_log_ip = strdup(general["log_ip"] | ""); // serial is only on when booting #ifdef FORCE_SERIAL @@ -1752,8 +1754,9 @@ bool MyESP::_fs_loadConfig() { _ntp_server = strdup(ntp["server"] | ""); _ntp_interval = ntp["interval"] | 60; if (_ntp_interval < 2) - _ntp_interval = 60; - _ntp_enabled = ntp["enabled"]; + _ntp_interval = NTP_INTERVAL_DEFAULT; + _ntp_enabled = ntp["enabled"]; + _ntp_timezone = ntp["timezone"] | NTP_TIMEZONE_DEFAULT; myDebug_P(PSTR("[FS] System config loaded (%d bytes)"), size); @@ -1800,6 +1803,18 @@ bool MyESP::fs_setSettingValue(uint8_t * setting, const char * value, uint8_t va return true; } +// saves a signed 8-bit integer into a config setting, using default value if non set +// returns true if successful +bool MyESP::fs_setSettingValue(int8_t * setting, const char * value, int8_t value_default) { + if (_hasValue(value)) { + *setting = (int8_t)atoi(value); + } else { + *setting = value_default; // use the default value + } + + return true; +} + // saves a bool into a config setting, using default value if non set // returns true if successful bool MyESP::fs_setSettingValue(bool * setting, const char * value, bool value_default) { @@ -1830,6 +1845,7 @@ bool MyESP::_fs_loadCustomConfig() { if (_fs_loadsave_callback_f) { const JsonObject & json = doc["settings"]; + if (!(_fs_loadsave_callback_f)(MYESP_FSACTION_LOAD, json)) { myDebug_P(PSTR("[FS] Error reading custom config")); return false; @@ -1852,6 +1868,7 @@ bool MyESP::fs_saveCustomConfig(JsonObject root) { // open for writing File configFile = SPIFFS.open(MYESP_CUSTOMCONFIG_FILE, "w"); + if (!configFile) { myDebug_P(PSTR("[FS] Failed to open custom config for writing")); ok = false; @@ -1861,20 +1878,7 @@ bool MyESP::fs_saveCustomConfig(JsonObject root) { configFile.close(); if (n) { - /* - // reload the settings, not sure why? - if (_fs_loadsave_callback_f) { - if (!(_fs_loadsave_callback_f)(MYESP_FSACTION_LOAD, root)) { - myDebug_P(PSTR("[FS] Error parsing custom config json")); - } - } - */ - - if (_general_log_events) { - _writeEvent("INFO", "system", "Custom config stored in the SPIFFS", ""); - } - - // myDebug_P(PSTR("[FS] custom config saved")); + writeLogEvent(MYESP_SYSLOG_INFO, "Custom config stored in the SPIFFS"); ok = true; } } @@ -1888,15 +1892,16 @@ bool MyESP::fs_saveCustomConfig(JsonObject root) { // save system config to spiffs bool MyESP::fs_saveConfig(JsonObject root) { - bool ok = false; - // call any custom functions before handling SPIFFS if (_ota_pre_callback_f) { (_ota_pre_callback_f)(); } + bool ok = false; + // open for writing File configFile = SPIFFS.open(MYESP_CONFIG_FILE, "w"); + if (!configFile) { myDebug_P(PSTR("[FS] Failed to open system config for writing")); ok = false; @@ -1906,14 +1911,13 @@ bool MyESP::fs_saveConfig(JsonObject root) { configFile.close(); if (n) { - if (_general_log_events) { - _writeEvent("INFO", "system", "System config stored in the SPIFFS", ""); - } - // myDebug_P(PSTR("[FS] system config saved")); + writeLogEvent(MYESP_SYSLOG_INFO, "System config stored in the SPIFFS"); ok = true; } - // serializeJsonPretty(root, Serial); // for debugging +#ifdef MYESP_DEBUG + serializeJsonPretty(root, Serial); +#endif } if (_ota_post_callback_f) { @@ -1930,16 +1934,22 @@ bool MyESP::_fs_writeConfig() { root["command"] = "configfile"; // header, important! - JsonObject network = doc.createNestedObject("network"); - network["ssid"] = _network_ssid; - network["password"] = _network_password; - network["wmode"] = _network_wmode; + JsonObject network = doc.createNestedObject("network"); + network["ssid"] = _network_ssid; + network["password"] = _network_password; + network["wmode"] = _network_wmode; + network["staticip"] = _network_staticip; + network["gatewayip"] = _network_gatewayip; + network["nmask"] = _network_nmask; + network["dnsip"] = _network_dnsip; JsonObject general = doc.createNestedObject("general"); general["password"] = _general_password; general["serial"] = _general_serial; general["hostname"] = _general_hostname; general["log_events"] = _general_log_events; + general["log_ip"] = _general_log_ip; + general["version"] = _app_version; JsonObject mqtt = doc.createNestedObject("mqtt"); mqtt["enabled"] = _mqtt_enabled; @@ -1950,14 +1960,14 @@ bool MyESP::_fs_writeConfig() { mqtt["qos"] = _mqtt_qos; mqtt["keepalive"] = _mqtt_keepalive; mqtt["retain"] = _mqtt_retain; - - mqtt["password"] = _mqtt_password; - mqtt["base"] = _mqtt_base; + mqtt["password"] = _mqtt_password; + mqtt["base"] = _mqtt_base; JsonObject ntp = doc.createNestedObject("ntp"); ntp["server"] = _ntp_server; ntp["interval"] = _ntp_interval; ntp["enabled"] = _ntp_enabled; + ntp["timezone"] = _ntp_timezone; bool ok = fs_saveConfig(root); // save it @@ -1993,50 +2003,34 @@ void MyESP::_fs_setup() { (_ota_pre_callback_f)(); // call custom function } + // check SPIFFS is OK if (!SPIFFS.begin()) { - myDebug_P(PSTR("[FS] Formatting filesystem...")); if (SPIFFS.format()) { - if (_general_log_events) { - _writeEvent("WARN", "system", "File system formatted", ""); - } + myDebug_P(PSTR("[FS] File system formatted")); } else { myDebug_P(PSTR("[FS] Failed to format file system")); } } + // see if old files exists from previous versions and delete it + if (SPIFFS.exists(MYESP_OLD_CONFIG_FILE)) { + SPIFFS.remove(MYESP_OLD_CONFIG_FILE); + myDebug_P(PSTR("[FS] Removed old config settings")); + } + if (SPIFFS.exists(MYESP_OLD_EVENTLOG_FILE)) { + SPIFFS.remove(MYESP_OLD_EVENTLOG_FILE); + myDebug_P(PSTR("[FS] Removed old event log")); + } + // load the main system config file if we can. Otherwise create it and expect user to configure in web interface if (!_fs_loadConfig()) { myDebug_P(PSTR("[FS] Creating a new system config")); - _fs_writeConfig(); // create the initial config file + _fs_writeConfig(); } // load system and custom config if (!_fs_loadCustomConfig()) { - _fs_createCustomConfig(); // create the initial config file - } - - /* - // fill event log with tests - SPIFFS.remove(MYESP_EVENTLOG_FILE); - File fs = SPIFFS.open(MYESP_EVENTLOG_FILE, "w"); - fs.close(); - char logs[100]; - for (uint8_t i = 1; i < 143; i++) { - sprintf(logs, "Record #%d", i); - _writeEvent("WARN", "system", "test data", logs); - } - */ - - // validate the event log. Sometimes it can can corrupted. - size_t size = _fs_validateLogFile(MYESP_EVENTLOG_FILE); - if (size) { - myDebug_P(PSTR("[FS] Event log loaded (%d bytes)"), size); - } else { - myDebug_P(PSTR("[FS] Resetting event log")); - SPIFFS.remove(MYESP_EVENTLOG_FILE); - if (_general_log_events) { - _writeEvent("WARN", "system", "Event Log", "Log was erased due to probable file corruption"); - } + _fs_createCustomConfig(); } if (_ota_post_callback_f) { @@ -2260,124 +2254,25 @@ void MyESP::crashInfo() { } #endif -// write a log entry to SPIFFS -// assumes we have "log_events" on -void MyESP::_writeEvent(const char * type, const char * src, const char * desc, const char * data) { - // this will also create the file if its doesn't exist - File eventlog = SPIFFS.open(MYESP_EVENTLOG_FILE, "a"); - if (!eventlog) { - //Serial.println("[SYSTEM] Error opening event log for writing"); // for debugging - eventlog.close(); +// write a log entry to SysLog via UDP +void MyESP::writeLogEvent(const uint8_t type, const char * msg) { + if (!_general_log_events) { return; } - StaticJsonDocument root; - root["type"] = type; - root["src"] = src; - root["desc"] = desc; - root["data"] = data; - root["time"] = now(); // is relative if we're not using NTP + static uuid::log::Logger logger{F("eventlog")}; - // Serialize JSON to file - size_t n = serializeJson(root, eventlog); - eventlog.print("\n"); // this indicates end of the entry - - if (!n) { - //Serial.println("[SYSTEM] Error writing to event log"); // for debugging + // uuid::log::INFO + // uuid::log::ERROR + if (type == MYESP_SYSLOG_INFO) { + logger.info(msg); + } else if (type == MYESP_SYSLOG_ERROR) { + logger.err(msg); } - eventlog.close(); -} - -// send a paged list (10 items) to the ws -void MyESP::_sendEventLog(uint8_t page) { - if (_ota_pre_callback_f) { - (_ota_pre_callback_f)(); // call custom function - } - - File eventlog; - // if its missing create it, it'll be empty though - if (!SPIFFS.exists(MYESP_EVENTLOG_FILE)) { - myDebug_P(PSTR("[FS] Event log is missing. Creating it.")); - eventlog = SPIFFS.open(MYESP_EVENTLOG_FILE, "w"); - eventlog.close(); - } - - eventlog = SPIFFS.open(MYESP_EVENTLOG_FILE, "r"); - - // the size of the json will be quite big so best not to use stack (StaticJsonDocument) - // it only covers 10 log entries - DynamicJsonDocument doc(MYESP_JSON_MAXSIZE); - JsonObject root = doc.to(); - root["command"] = "eventlist"; - root["page"] = page; - - JsonArray list = doc.createNestedArray("list"); - - size_t static lastPos; - // if first page, reset the file pointer - if (page == 1) { - lastPos = 0; - } - - eventlog.seek(lastPos); // move to position in file - - uint8_t char_count = 0; - uint8_t line_count = 0; - bool abort = false; - char char_buffer[MYESP_JSON_LOG_MAXSIZE]; - float pages; - - // start at top and read until we find the page we want (sets of 10) - while (eventlog.available() && !abort) { - char c = eventlog.read(); - - // see if we have reached the end of the string - if (c == '\0' || c == '\n') { - char_buffer[char_count] = '\0'; // terminate and add it to the list - // Serial.printf("Got line %d: %s\n", line_count+1, char_buffer); // for debugging - list.add(char_buffer); - // increment line counter and check if we've reached 10 records, if so abort - if (++line_count == 10) { - abort = true; - } - char_count = 0; // start new record - } else { - // add the char to the buffer if recording, checking for overrun - if (char_count < MYESP_JSON_LOG_MAXSIZE) { - char_buffer[char_count++] = c; - } else { - abort = true; // reached limit of our line buffer - } - } - } - - lastPos = eventlog.position(); // remember last position for next cycle - - // calculate remaining pages, as needed for footable - if (eventlog.available()) { - float totalPagesRoughly = eventlog.size() / (float)(lastPos / page); - pages = totalPagesRoughly < page ? page + 1 : totalPagesRoughly; - } else { - pages = page; // this was the last page - } - - eventlog.close(); // close SPIFFS - - root["haspages"] = ceil(pages); - - char buffer[MYESP_JSON_MAXSIZE]; - size_t len = serializeJson(root, buffer); - - //Serial.printf("\nEVENTLOG: page %d, length=%d\n", page, len); // turn on for debugging - //serializeJson(root, Serial); // turn on for debugging - - _ws->textAll(buffer, len); - _ws->textAll("{\"command\":\"result\",\"resultof\":\"eventlist\",\"result\": true}"); - - if (_ota_post_callback_f) { - (_ota_post_callback_f)(); // call custom function - } +#ifdef MYESP_DEBUG + Serial.printf("%d: %s\n", type, msg); +#endif } // Handles WebSocket Events @@ -2417,8 +2312,8 @@ void MyESP::_onWsEvent(AsyncWebSocket * server, AsyncWebSocketClient * client, A // handle ws from browser void MyESP::_procMsg(AsyncWebSocketClient * client, size_t sz) { // We should always get a JSON object from browser, so parse it - StaticJsonDocument<400> doc; - char json[sz + 1]; + StaticJsonDocument doc; + char json[sz + 1]; memcpy(json, (char *)(client->_tempObject), sz); json[sz] = '\0'; @@ -2432,7 +2327,9 @@ void MyESP::_procMsg(AsyncWebSocketClient * client, size_t sz) { } const char * command = doc["command"]; - // Serial.printf("*** Got command: %s\n", command); // turn on for debugging +#ifdef MYESP_DEBUG + myDebug("*** Got command: %s\n", command); +#endif // Check whatever the command is and act accordingly if (strcmp(command, "configfile") == 0) { @@ -2449,29 +2346,10 @@ void MyESP::_procMsg(AsyncWebSocketClient * client, size_t sz) { _formatreq = true; } else if (strcmp(command, "forcentp") == 0) { NTP.getNtpTime(); - } else if (strcmp(command, "geteventlog") == 0) { - uint8_t page = doc["page"]; - _sendEventLog(page); - } else if (strcmp(command, "clearevent") == 0) { - if (_ota_pre_callback_f) { - (_ota_pre_callback_f)(); // call custom function - } - if (SPIFFS.remove(MYESP_EVENTLOG_FILE)) { - _writeEvent("WARN", "system", "Event log cleared", ""); - } else { - myDebug_P(PSTR("[WEB] Could not clear event log")); - } - if (_ota_post_callback_f) { - (_ota_post_callback_f)(); // call custom function - } } else if (strcmp(command, "scan") == 0) { WiFi.scanNetworksAsync(std::bind(&MyESP::_printScanResult, this, std::placeholders::_1), true); } else if (strcmp(command, "gettime") == 0) { _timerequest = true; - } else if (strcmp(command, "settime") == 0) { - time_t t = doc["epoch"]; - setTime(t); - _timerequest = true; } else if (strcmp(command, "getconf") == 0) { _fs_sendConfig(); } @@ -2500,7 +2378,10 @@ bool MyESP::_fs_sendConfig() { } configFile.close(); - //Serial.printf("_fs_sendConfig() sending system (%d): %s\n", size, json); // turn on for debugging +#ifdef MYESP_DEBUG + myDebug("_fs_sendConfig() sending system (%d): %s\n", size, json); +#endif + _ws->textAll(json, size); configFile = SPIFFS.open(MYESP_CUSTOMCONFIG_FILE, "r"); @@ -2518,7 +2399,10 @@ bool MyESP::_fs_sendConfig() { } configFile.close(); - //Serial.printf("_fs_sendConfig() sending custom (%d): %s\n", size, json); // turn on for debugging +#ifdef MYESP_DEBUG + myDebug("_fs_sendConfig() sending custom (%d): %s\n", size, json); +#endif + _ws->textAll(json, size); return true; @@ -2526,25 +2410,28 @@ bool MyESP::_fs_sendConfig() { // send custom status via ws void MyESP::_sendCustomStatus() { - // StaticJsonDocument<300> doc; - DynamicJsonDocument doc(MYESP_JSON_MAXSIZE); + DynamicJsonDocument doc(MYESP_JSON_MAXSIZE_LARGE); JsonObject root = doc.to(); - root["command"] = "custom_status"; - root["version"] = _app_version; - root["customname"] = _app_name; - root["appurl"] = _app_url; - root["updateurl"] = _app_updateurl; + root["command"] = "custom_status"; + root["version"] = _app_version; + root["customname"] = _app_name; + root["appurl"] = _app_url; + root["updateurl"] = _app_updateurl; + root["updateurl_dev"] = _app_updateurl_dev; // add specific custom stuff if (_web_callback_f) { (_web_callback_f)(root); } - char buffer[MYESP_JSON_MAXSIZE]; + char buffer[MYESP_JSON_MAXSIZE_LARGE]; size_t len = serializeJson(root, buffer); - // Serial.printf("_sendCustomStatus() sending: %s\n", buffer); // turn on for debugging + +#ifdef MYESP_DEBUG + myDebug("_sendCustomStatus() sending: %s\n", buffer); +#endif _ws->textAll(buffer, len); } @@ -2648,9 +2535,9 @@ void MyESP::_printScanResult(int networksFound) { } } - StaticJsonDocument<400> doc; - JsonObject root = doc.to(); - root["command"] = "ssidlist"; + StaticJsonDocument doc; + JsonObject root = doc.to(); + root["command"] = "ssidlist"; JsonArray list = doc.createNestedArray("list"); for (int i = 0; i <= 5 && i < networksFound; ++i) { @@ -2660,7 +2547,7 @@ void MyESP::_printScanResult(int networksFound) { item["rssi"] = WiFi.RSSI(indices[i]); } - char buffer[400]; + char buffer[MYESP_JSON_MAXSIZE_MEDIUM]; size_t len = serializeJson(root, buffer); _ws->textAll(buffer, len); } @@ -2695,27 +2582,32 @@ void MyESP::_webserver_setup() { } if (!index) { ETS_UART_INTR_DISABLE(); // disable all UART interrupts to be safe - _writeEvent("INFO", "system", "Firmware update started", ""); - //Serial.printf("[SYSTEM] Firmware update started: %s\n", filename.c_str()); // enable for debugging + //_writeLogEvent(MYESP_SYSLOG_INFO, "Firmware update started"); Update.runAsync(true); if (!Update.begin((ESP.getFreeSketchSpace() - 0x1000) & 0xFFFFF000)) { - _writeEvent("ERRO", "system", "Not enough space to update", ""); - //Update.printError(Serial); // enable for debugging + //_writeLogEvent(MYESP_SYSLOG_ERROR, "Not enough space to update"); +#ifdef MYESP_DEBUG + Update.printError(Serial); +#endif } } if (!Update.hasError()) { if (Update.write(data, len) != len) { - _writeEvent("ERRO", "system", "Writing to flash failed", ""); - //Update.printError(Serial); // enable for debugging + //_writeLogEvent(MYESP_SYSLOG_ERROR, "Writing to flash failed"); +#ifdef MYESP_DEBUG + Update.printError(Serial); +#endif } } if (final) { if (Update.end(true)) { - _writeEvent("INFO", "system", "Firmware update finished", ""); + //_writeLogEvent(MYESP_SYSLOG_INFO, "Firmware update finished"); _shouldRestart = !Update.hasError(); } else { - _writeEvent("ERRO", "system", "Firmware update failed", ""); - //Update.printError(Serial); // enable for debugging + //_writeLogEvent(MYESP_SYSLOG_ERROR, "Firmware update failed"); +#ifdef MYESP_DEBUG + Update.printError(Serial); +#endif } } }); @@ -2771,12 +2663,12 @@ void MyESP::_webserver_setup() { } // print memory -void MyESP::_printHeap(const char * s) { +void MyESP::_printHeap(const char * prefix) { uint32_t total_memory = _getInitialFreeHeap(); uint32_t free_memory = ESP.getFreeHeap(); - myDebug(" [%s] Free Heap: %d bytes initially | %d bytes used (%2u%%) | %d bytes free (%2u%%)", - s, + myDebug("%s Free Heap: %d bytes initially | %d bytes used (%2u%%) | %d bytes free (%2u%%)", + prefix, total_memory, total_memory - free_memory, 100 * (total_memory - free_memory) / total_memory, @@ -2790,8 +2682,13 @@ void MyESP::_printMQTTLog() { uint8_t i; for (i = 0; i < MYESP_MQTTLOG_MAX; i++) { - if ((MQTT_log[i].topic != nullptr) && (MQTT_log[i].type == 1)) { - myDebug_P(PSTR(" Topic:%s Payload:%s"), MQTT_log[i].topic, MQTT_log[i].payload); + if ((MQTT_log[i].topic != nullptr) && (MQTT_log[i].type == MYESP_MQTTLOGTYPE_PUBLISH)) { + myDebug_P(PSTR(" Timestamp:%02d:%02d:%02d Topic:%s Payload:%s"), + to_hour(MQTT_log[i].timestamp), + to_minute(MQTT_log[i].timestamp), + to_second(MQTT_log[i].timestamp), + MQTT_log[i].topic, + MQTT_log[i].payload); } } @@ -2799,12 +2696,8 @@ void MyESP::_printMQTTLog() { myDebug_P(PSTR("MQTT subscriptions:")); for (i = 0; i < MYESP_MQTTLOG_MAX; i++) { - if ((MQTT_log[i].topic != nullptr) && (MQTT_log[i].type == 2)) { - if (_hasValue(MQTT_log[i].payload)) { - myDebug_P(PSTR(" Topic:%s Last Payload:%s"), MQTT_log[i].topic, MQTT_log[i].payload); - } else { - myDebug_P(PSTR(" Topic:%s"), MQTT_log[i].topic); - } + if ((MQTT_log[i].topic != nullptr) && (MQTT_log[i].type == MYESP_MQTTLOGTYPE_SUBSCRIBE)) { + myDebug_P(PSTR(" Topic:%s"), MQTT_log[i].topic); } } @@ -2812,13 +2705,14 @@ void MyESP::_printMQTTLog() { } // add an MQTT log entry to our buffer -// type 0=none, 1=publish, 2=subscribe -void MyESP::_addMQTTLog(const char * topic, const char * payload, const uint8_t type) { +void MyESP::_addMQTTLog(const char * topic, const char * payload, const MYESP_MQTTLOGTYPE_t type) { static uint8_t logCount = 0; uint8_t logPointer = 0; bool found = false; - // myDebug("_addMQTTLog [#%d] %s (%d) [%s] (%d)", logCount, topic, strlen(topic), payload, strlen(payload)); // for debugging +#ifdef MYESP_DEBUG + myDebug("_addMQTTLog [#%d] %s (%d) [%s] (%d)", logCount, topic, strlen(topic), payload, strlen(payload)); +#endif // find the topic // topics must be unique for either publish or subscribe @@ -2847,7 +2741,7 @@ void MyESP::_addMQTTLog(const char * topic, const char * payload, const uint8_t } // and add new record - MQTT_log[logPointer].type = type; // 0=none, 1=publish, 2=subscribe + MQTT_log[logPointer].type = type; MQTT_log[logPointer].topic = strdup(topic); MQTT_log[logPointer].payload = strdup(payload); MQTT_log[logPointer].timestamp = now(); @@ -2855,16 +2749,26 @@ void MyESP::_addMQTTLog(const char * topic, const char * payload, const uint8_t // send UTC time via ws void MyESP::_sendTime() { - StaticJsonDocument<100> doc; - JsonObject root = doc.to(); - root["command"] = "gettime"; - root["epoch"] = now(); + StaticJsonDocument doc; + JsonObject root = doc.to(); + root["command"] = "gettime"; + root["epoch"] = now(); - char buffer[100]; + char buffer[MYESP_JSON_MAXSIZE_SMALL]; size_t len = serializeJson(root, buffer); _ws->textAll(buffer, len); } +// returns real time from the internet/NTP if availble +// otherwise elapsed system time +unsigned long MyESP::getSystemTime() { + if (_have_ntp_time) { + return now(); + } + + return millis(); // elapsed time +} + // bootup sequence // quickly flash LED until we get a Wifi connection, or AP established void MyESP::_bootupSequence() { @@ -2874,9 +2778,7 @@ void MyESP::_bootupSequence() { if (boot_status == MYESP_BOOTSTATUS_BOOTED) { if ((_ntp_enabled) && (now() > 10000) && !_have_ntp_time) { _have_ntp_time = true; - if (_general_log_events) { - _writeEvent("INFO", "system", "System booted", ""); - } + writeLogEvent(MYESP_SYSLOG_INFO, "System booted"); } return; } @@ -2906,20 +2808,48 @@ void MyESP::_bootupSequence() { // write a log message if we're not using NTP, otherwise wait for the internet time to arrive if (!_ntp_enabled) { - if (_general_log_events) { - _writeEvent("INFO", "system", "System booted", ""); - } + writeLogEvent(MYESP_SYSLOG_INFO, "System booted"); } } } +// set up SysLog +void MyESP::_syslog_setup() { + // if not enabled or IP is empty, don't bother + if ((!_hasValue(_general_log_ip)) || (!_general_log_events)) { + return; + } + + static uuid::log::Logger logger{F("setup")}; + + syslog.start(); + syslog.hostname(_general_hostname); + syslog.log_level(uuid::log::DEBUG); // default log level + syslog.mark_interval(3600); + + IPAddress syslog_ip; + syslog_ip.fromString(_general_log_ip); + syslog.destination(syslog_ip); + + logger.info(F("Application started")); + + myDebug_P(PSTR("[SYSLOG] System event logging enabled")); +} + // setup MyESP -void MyESP::begin(const char * app_hostname, const char * app_name, const char * app_version, const char * app_url, const char * app_updateurl) { +void MyESP::begin(const char * app_hostname, const char * app_name, const char * app_version, const char * app_url, const char * app_url_api) { _general_hostname = strdup(app_hostname); _app_name = strdup(app_name); _app_version = strdup(app_version); _app_url = strdup(app_url); - _app_updateurl = strdup(app_updateurl); + + char s[100]; + strlcpy(s, app_url_api, sizeof(s)); + strlcat(s, "/releases/latest", sizeof(s)); // append "/releases/latest" + _app_updateurl = strdup(s); + strlcpy(s, app_url_api, sizeof(s)); + strlcat(s, "/releases/tags/travis-dev-build", sizeof(s)); // append "/releases/tags/travis-dev-build" + _app_updateurl_dev = strdup(s); _telnet_setup(); // Telnet setup, called first to set Serial @@ -2935,11 +2865,13 @@ void MyESP::begin(const char * app_hostname, const char * app_name, const char * _eeprom_setup(); // set up EEPROM for storing crash data, if compiled with -DCRASH _fs_setup(); // SPIFFS setup, do this first to get values _wifi_setup(); // WIFI setup + _mqtt_setup(); // MQTT setup _ota_setup(); // init OTA + _syslog_setup(); // SysLog _webserver_setup(); // init web server _setSystemCheck(false); // reset system check - _heartbeatCheck(true); // force heartbeat + _heartbeatCheck(true); // force heartbeat, will send out message to log too _setSystemDropoutCounter(0); // reset # TCP dropouts @@ -2955,7 +2887,8 @@ void MyESP::loop() { _heartbeatCheck(); _bootupSequence(); // see if a reset was pressed during bootup - jw.loop(); // WiFi + jw.loop(); // WiFi + ArduinoOTA.handle(); // OTA ESP.wdtFeed(); // feed the watchdog... @@ -2964,6 +2897,10 @@ void MyESP::loop() { _mqttConnect(); // MQTT + // SysLog + uuid::loop(); + syslog.loop(); + if (_timerequest) { _timerequest = false; _sendTime(); @@ -2979,15 +2916,13 @@ void MyESP::loop() { } if (_shouldRestart) { - if (_general_log_events) { - _writeEvent("INFO", "system", "System is restarting", ""); - } + writeLogEvent(MYESP_SYSLOG_INFO, "System is restarting"); myDebug("[SYSTEM] Restarting..."); _deferredReset(500, CUSTOM_RESET_TERMINAL); ESP.restart(); } - yield(); // ... and breath. + delay(MYESP_DELAY); // some time to WiFi and everything else to catch up, calls yield, and also prevent overheating } MyESP myESP; diff --git a/src/MyESP.h b/src/MyESP.h index 8e7456627..b79bd64c6 100644 --- a/src/MyESP.h +++ b/src/MyESP.h @@ -1,5 +1,5 @@ /* - * MyESP.h + * MyESP.h - does all the basics like WiFI/MQTT/NTP/Debug logs etc * * Paul Derbyshire - first version December 2018 */ @@ -9,16 +9,23 @@ #ifndef MyESP_h #define MyESP_h -#define MYESP_VERSION "1.2.12" +#define MYESP_VERSION "1.2.22" #include #include -#include // https://github.com/marvinroger/async-mqtt-client and for ESP32 see https://github.com/marvinroger/async-mqtt-client/issues/127 -#include +#include #include #include -#include // https://github.com/xoseperez/justwifi +#include +// SysLog +#include +#include +#include +static uuid::syslog::SyslogService syslog; +enum MYESP_SYSLOG_LEVEL : uint8_t { MYESP_SYSLOG_INFO, MYESP_SYSLOG_ERROR }; + +// local libraries #include "Ntp.h" #include "TelnetSpy.h" // modified from https://github.com/yasheena/telnetspy @@ -54,7 +61,8 @@ extern struct rst_info resetInfo; #define MYESP_CONFIG_FILE "/myesp.json" #define MYESP_CUSTOMCONFIG_FILE "/customconfig.json" -#define MYESP_EVENTLOG_FILE "/eventlog.json" +#define MYESP_OLD_EVENTLOG_FILE "/eventlog.json" // depreciated +#define MYESP_OLD_CONFIG_FILE "/config.json" // depreciated #define MYESP_HTTP_USERNAME "admin" // HTTP username #define MYESP_HTTP_PASSWORD "admin" // default password @@ -67,6 +75,9 @@ extern struct rst_info resetInfo; #define MYESP_WIFI_CONNECT_TIMEOUT 20000 // Connecting timeout for WIFI in ms (20 seconds) #define MYESP_WIFI_RECONNECT_INTERVAL 600000 // If could not connect to WIFI, retry after this time in ms. 10 minutes +// set to value >0 if the ESP is overheating or there are timing issues. Recommend a value of 1. +#define MYESP_DELAY 1 + // MQTT #define MQTT_PORT 1883 // MQTT port #define MQTT_RECONNECT_DELAY_MIN 2000 // Try to reconnect in 3 seconds upon disconnection @@ -92,9 +103,11 @@ extern struct rst_info resetInfo; #define MQTT_DISCONNECT_EVENT 1 #define MQTT_MESSAGE_EVENT 2 -#define MYESP_JSON_MAXSIZE 2000 // for large Dynamic json files -#define MYESP_MQTTLOG_MAX 60 // max number of log entries for MQTT publishes and subscribes -#define MYESP_JSON_LOG_MAXSIZE 300 // max size of an JSON log entry +#define MYESP_JSON_MAXSIZE_LARGE 2000 // for large Dynamic json files +#define MYESP_JSON_MAXSIZE_MEDIUM 800 // for medium Dynamic json files +#define MYESP_JSON_MAXSIZE_SMALL 200 // for smaller Static json documents + +#define MYESP_MQTTLOG_MAX 60 // max number of log entries for MQTT publishes and subscribes #define MYESP_MQTT_PAYLOAD_ON '1' // for MQTT switch on #define MYESP_MQTT_PAYLOAD_OFF '0' // for MQTT switch off @@ -102,6 +115,7 @@ extern struct rst_info resetInfo; // Telnet #define TELNET_SERIAL_BAUD 115200 #define TELNET_MAX_COMMAND_LENGTH 80 // length of a command +#define TELNET_MAX_BUFFER_LENGTH 700 // max length of telnet string #define TELNET_EVENT_CONNECT 1 #define TELNET_EVENT_DISCONNECT 0 #define TELNET_EVENT_SHOWCMD 10 @@ -142,10 +156,9 @@ PROGMEM const char * const custom_reset_string[] = {custom_reset_hardware, cus #define CUSTOM_RESET_FACTORY 5 // Factory reset #define CUSTOM_RESET_MAX 5 -// SPIFFS +// SPIFFS - max allocation is 1000 KB // https://arduinojson.org/v6/assistant/ -#define MYESP_SPIFFS_MAXSIZE_CONFIG 800 // max size for a config file -#define MYESP_SPIFFS_MAXSIZE_EVENTLOG 20000 // max size for the eventlog in bytes +#define MYESP_SPIFFS_MAXSIZE_CONFIG 999 // max size for a config file // CRASH /** @@ -207,14 +220,16 @@ typedef struct { char description[100]; } command_t; -typedef enum { MYESP_FSACTION_SET, MYESP_FSACTION_LIST, MYESP_FSACTION_SAVE, MYESP_FSACTION_LOAD } MYESP_FSACTION; +typedef enum { MYESP_FSACTION_SET, MYESP_FSACTION_LIST, MYESP_FSACTION_SAVE, MYESP_FSACTION_LOAD } MYESP_FSACTION_t; typedef enum { MYESP_BOOTSTATUS_POWERON = 0, MYESP_BOOTSTATUS_BOOTED = 1, MYESP_BOOTSTATUS_BOOTING = 2, MYESP_BOOTSTATUS_RESETNEEDED = 3 -} MYESP_BOOTSTATUS; // boot messages +} MYESP_BOOTSTATUS_t; // boot messages + +typedef enum { MYESP_MQTTLOGTYPE_NONE, MYESP_MQTTLOGTYPE_PUBLISH, MYESP_MQTTLOGTYPE_SUBSCRIBE } MYESP_MQTTLOGTYPE_t; // for storing all MQTT publish messages typedef struct { @@ -222,16 +237,16 @@ typedef struct { char * topic; char * payload; time_t timestamp; -} _MQTT_Log; +} _MQTT_Log_t; -typedef std::function mqtt_callback_f; -typedef std::function wifi_callback_f; -typedef std::function ota_callback_f; -typedef std::function telnetcommand_callback_f; -typedef std::function telnet_callback_f; -typedef std::function fs_loadsave_callback_f; -typedef std::function fs_setlist_callback_f; -typedef std::function web_callback_f; +typedef std::function mqtt_callback_f; +typedef std::function wifi_callback_f; +typedef std::function ota_callback_f; +typedef std::function telnetcommand_callback_f; +typedef std::function telnet_callback_f; +typedef std::function fs_loadsave_callback_f; +typedef std::function fs_setlist_callback_f; +typedef std::function web_callback_f; // calculates size of an 2d array at compile time template @@ -240,12 +255,9 @@ constexpr size_t ArraySize(T (&)[N]) { } #define MYESP_UPTIME_OVERFLOW 4294967295 // Uptime overflow value - -// web min and max length of wifi ssid and password -#define MYESP_MAX_STR_LEN 16 - -#define MYESP_BOOTUP_FLASHDELAY 50 // flash duration for LED at bootup sequence -#define MYESP_BOOTUP_DELAY 2000 // time before we open the window to reset. This is to stop resetting values when uploading firmware via USB +#define MYESP_MAX_STR_LEN 16 // web min and max length of wifi ssid and password +#define MYESP_BOOTUP_FLASHDELAY 50 // flash duration for LED at bootup sequence +#define MYESP_BOOTUP_DELAY 2000 // time before we open the window to reset. This is to stop resetting values when uploading firmware via USB // class definition class MyESP { @@ -261,9 +273,6 @@ class MyESP { MyESP(); ~MyESP(); - // write event called from within lambda classs - static void _writeEvent(const char * type, const char * src, const char * desc, const char * data); - // wifi void setWIFICallback(void (*callback)()); void setWIFI(wifi_callback_f callback); @@ -288,6 +297,9 @@ class MyESP { bool getUseSerial(); void setUseSerial(bool toggle); + // syslog + void writeLogEvent(const uint8_t type, const char * msg); + // FS void setSettings(fs_loadsave_callback_f loadsave, fs_setlist_callback_f setlist, bool useSerial = true); bool fs_saveConfig(JsonObject root); @@ -295,6 +307,7 @@ class MyESP { bool fs_setSettingValue(char ** setting, const char * value, const char * value_default); bool fs_setSettingValue(uint16_t * setting, const char * value, uint16_t value_default); bool fs_setSettingValue(uint8_t * setting, const char * value, uint8_t value_default); + bool fs_setSettingValue(int8_t * setting, const char * value, int8_t value_default); bool fs_setSettingValue(bool * setting, const char * value, bool value_default); // Web @@ -306,17 +319,19 @@ class MyESP { void crashInfo(); // general - void end(); - void loop(); - void begin(const char * app_hostname, const char * app_name, const char * app_version, const char * app_url, const char * app_updateurl); - void resetESP(); - int getWifiQuality(); - void showSystemStats(); - bool getHeartbeat(); - uint32_t getSystemLoadAverage(); - uint32_t getSystemResetReason(); - uint8_t getSystemBootStatus(); - bool _have_ntp_time; + void end(); + void loop(); + void begin(const char * app_hostname, const char * app_name, const char * app_version, const char * app_url, const char * app_url_api); + void resetESP(); + int getWifiQuality(); + void showSystemStats(); + bool getHeartbeat(); + uint32_t getSystemLoadAverage(); + uint32_t getSystemResetReason(); + uint8_t getSystemBootStatus(); + bool _have_ntp_time; + unsigned long getSystemTime(); + void heartbeatPrint(); private: // mqtt @@ -328,10 +343,10 @@ class MyESP { char * _mqttTopic(const char * topic); // mqtt log - _MQTT_Log MQTT_log[MYESP_MQTTLOG_MAX]; // log for publish and subscribe messages + _MQTT_Log_t MQTT_log[MYESP_MQTTLOG_MAX]; // log for publish and subscribe messages void _printMQTTLog(); - void _addMQTTLog(const char * topic, const char * payload, const uint8_t type); + void _addMQTTLog(const char * topic, const char * payload, const MYESP_MQTTLOGTYPE_t type); AsyncMqttClient mqttClient; // the MQTT class uint32_t _mqtt_reconnect_delay; @@ -359,6 +374,10 @@ class MyESP { char * _network_ssid; char * _network_password; uint8_t _network_wmode; + char * _network_staticip; + char * _network_gatewayip; + char * _network_nmask; + char * _network_dnsip; bool _wifi_connected; String _getESPhostname(); @@ -371,7 +390,7 @@ class MyESP { // crash void _eeprom_setup(); - // telnet & debug + // telnet TelnetSpy SerialAndTelnet; void _telnetConnected(); void _telnetDisconnected(); @@ -385,6 +404,9 @@ class MyESP { telnet_callback_f _telnet_callback_f; // callback for connect/disconnect bool _changeSetting(uint8_t wc, const char * setting, const char * value); + // syslog + void _syslog_setup(); + // fs and settings void _fs_setup(); bool _fs_loadConfig(); @@ -407,9 +429,11 @@ class MyESP { char * _app_version; char * _app_url; char * _app_updateurl; + char * _app_updateurl_dev; bool _suspendOutput; bool _general_serial; bool _general_log_events; + char * _general_log_ip; char * _buildTime; bool _timerequest; bool _formatreq; @@ -417,6 +441,7 @@ class MyESP { char * _getBuildTime(); bool _hasValue(const char * s); void _printHeap(const char * s); + void _kick(); // reset reason and rtcmem bool _rtcmem_status; @@ -455,15 +480,12 @@ class MyESP { uint32_t _getUsedHeap(); // heartbeat - void _heartbeatCheck(bool force); + void _heartbeatCheck(bool force = false); // web web_callback_f _web_callback_f; const char * _http_username; - // log - void _sendEventLog(uint8_t page); - // web void _onWsEvent(AsyncWebSocket * server, AsyncWebSocketClient * client, AwsEventType type, void * arg, uint8_t * data, size_t len); void _procMsg(AsyncWebSocketClient * client, size_t sz); @@ -472,14 +494,12 @@ class MyESP { void _printScanResult(int networksFound); void _sendTime(); void _webserver_setup(); - void _webRootPage(); - void _webResetPage(); - void _webResetAllPage(); // ntp - char * _ntp_server; - uint8_t _ntp_interval; - bool _ntp_enabled; + char * _ntp_server; + uint16_t _ntp_interval; + bool _ntp_enabled; + uint8_t _ntp_timezone; }; extern MyESP myESP; diff --git a/src/Ntp.cpp b/src/Ntp.cpp index 679bbae7b..bb3154670 100644 --- a/src/Ntp.cpp +++ b/src/Ntp.cpp @@ -3,16 +3,86 @@ */ #include "Ntp.h" +#include "MyESP.h" -char * NtpClient::TimeServerName; -time_t NtpClient::syncInterval; -IPAddress NtpClient::timeServer; -AsyncUDP NtpClient::udpListener; -byte NtpClient::NTPpacket[NTP_PACKET_SIZE]; +char * NtpClient::TimeServerName; +Timezone * NtpClient::tz; +TimeChangeRule * NtpClient::tcr; +time_t NtpClient::syncInterval; +IPAddress NtpClient::timeServer; +AsyncUDP NtpClient::udpListener; +byte NtpClient::NTPpacket[NTP_PACKET_SIZE]; -void ICACHE_FLASH_ATTR NtpClient::Ntp(const char * server, time_t syncMins) { +// references: +// https://github.com/filipdanic/compact-timezone-list/blob/master/index.js +// https://github.com/sanohin/google-timezones-json/blob/master/timezones.json +// https://github.com/dmfilipenko/timezones.json/blob/master/timezones.json +// https://home.kpn.nl/vanadovv/time/TZworld.html +// https://www.timeanddate.com/time/zones/ + +// Australia Eastern Time Zone (Sydney, Melbourne) +TimeChangeRule aEDT = {"AEDT", First, Sun, Oct, 2, 660}; // UTC + 11 hours +TimeChangeRule aEST = {"AEST", First, Sun, Apr, 3, 600}; // UTC + 10 hours +Timezone ausET(aEDT, aEST); + +// Moscow Standard Time (MSK, does not observe DST) +TimeChangeRule msk = {"MSK", Last, Sun, Mar, 1, 180}; +Timezone MSK(msk); + +// Turkey +TimeChangeRule trt = {"TRT", Last, Sun, Mar, 1, 180}; +Timezone TRT(trt); + +// Central European Time (Frankfurt, Paris) +TimeChangeRule CEST = {"CEST", Last, Sun, Mar, 2, 120}; // Central European Summer Time +TimeChangeRule CET = {"CET ", Last, Sun, Oct, 3, 60}; // Central European Standard Time +Timezone CE(CEST, CET); + +// United Kingdom (London, Belfast) +TimeChangeRule BST = {"BST", Last, Sun, Mar, 1, 60}; // British Summer Time +TimeChangeRule GMT = {"GMT", Last, Sun, Oct, 2, 0}; // Standard Time +Timezone UK(BST, GMT); + +// UTC +TimeChangeRule utcRule = {"UTC", Last, Sun, Mar, 1, 0}; // UTC +Timezone UTC(utcRule); + +// US Eastern Time Zone (New York, Detroit) +TimeChangeRule usEDT = {"EDT", Second, Sun, Mar, 2, -240}; // Eastern Daylight Time = UTC - 4 hours +TimeChangeRule usEST = {"EST", First, Sun, Nov, 2, -300}; // Eastern Standard Time = UTC - 5 hours +Timezone usET(usEDT, usEST); + +// US Central Time Zone (Chicago, Houston) +TimeChangeRule usCDT = {"CDT", Second, Sun, Mar, 2, -300}; +TimeChangeRule usCST = {"CST", First, Sun, Nov, 2, -360}; +Timezone usCT(usCDT, usCST); + +// US Mountain Time Zone (Denver, Salt Lake City) +TimeChangeRule usMDT = {"MDT", Second, Sun, Mar, 2, -360}; +TimeChangeRule usMST = {"MST", First, Sun, Nov, 2, -420}; +Timezone usMT(usMDT, usMST); + +// Arizona is US Mountain Time Zone but does not use DST +Timezone usAZ(usMST); + +// US Pacific Time Zone (Las Vegas, Los Angeles) +TimeChangeRule usPDT = {"PDT", Second, Sun, Mar, 2, -420}; +TimeChangeRule usPST = {"PST", First, Sun, Nov, 2, -480}; +Timezone usPT(usPDT, usPST); + +// build index of all timezones +Timezone * timezones[] = {&ausET, &MSK, &CE, &UK, &UTC, &usET, &usCT, &usMT, &usAZ, &usPT, &TRT}; + +void ICACHE_FLASH_ATTR NtpClient::Ntp(const char * server, time_t syncMins, uint8_t tz_index) { TimeServerName = strdup(server); syncInterval = syncMins * 60; // convert to seconds + + // check for out of bounds + if (tz_index >= NTP_TIMEZONE_MAX) { + tz_index = NTP_TIMEZONE_DEFAULT; + } + tz = timezones[tz_index]; // set timezone + WiFi.hostByName(TimeServerName, timeServer); setSyncProvider(getNtpTime); setSyncInterval(syncInterval); @@ -35,10 +105,23 @@ time_t ICACHE_FLASH_ATTR NtpClient::getNtpTime() { NTPpacket[15] = 52; if (udpListener.connect(timeServer, 123)) { udpListener.onPacket([](AsyncUDPPacket packet) { - unsigned long highWord = word(packet.data()[40], packet.data()[41]); - unsigned long lowWord = word(packet.data()[42], packet.data()[43]); - time_t UnixUTCtime = (highWord << 16 | lowWord) - 2208988800UL; - setTime(UnixUTCtime); + unsigned long highWord = word(packet.data()[40], packet.data()[41]); + unsigned long lowWord = word(packet.data()[42], packet.data()[43]); + time_t UnixUTCtime = (highWord << 16 | lowWord) - 2208988800UL; + time_t adjustedtime = (*tz).toLocal(UnixUTCtime, &tcr); + + myESP.myDebug("[NTP] Internet time: %02d:%02d:%02d UTC on %d/%d. Local time: %02d:%02d:%02d %s", + to_hour(UnixUTCtime), + to_minute(UnixUTCtime), + to_second(UnixUTCtime), + to_day(UnixUTCtime), + to_month(UnixUTCtime), + to_hour(adjustedtime), + to_minute(adjustedtime), + to_second(adjustedtime), + tcr->abbrev); + + setTime(adjustedtime); }); } udpListener.write(NTPpacket, sizeof(NTPpacket)); diff --git a/src/Ntp.h b/src/Ntp.h index 7eca60b9e..05a55b530 100644 --- a/src/Ntp.h +++ b/src/Ntp.h @@ -10,18 +10,24 @@ #include #include -#include "TimeLib.h" // customized version of the time library +#include "TimeLib.h" // customized version of the Time library +#include "Timezone.h" -#define NTP_PACKET_SIZE 48 // NTP time is in the first 48 bytes of message +#define NTP_PACKET_SIZE 48 // NTP time is in the first 48 bytes of the message +#define NTP_INTERVAL_DEFAULT 720 // every 12 hours +#define NTP_TIMEZONE_DEFAULT 2 // CE +#define NTP_TIMEZONE_MAX 11 class NtpClient { public: - void ICACHE_FLASH_ATTR Ntp(const char * server, time_t syncMins); + void ICACHE_FLASH_ATTR Ntp(const char * server, time_t syncMins, uint8_t tz_index); ICACHE_FLASH_ATTR virtual ~NtpClient(); - static char * TimeServerName; - static IPAddress timeServer; - static time_t syncInterval; + static char * TimeServerName; + static IPAddress timeServer; + static time_t syncInterval; + static Timezone * tz; + static TimeChangeRule * tcr; static AsyncUDP udpListener; diff --git a/src/TelnetSpy.cpp b/src/TelnetSpy.cpp index 07b625c7d..dfda8af91 100644 --- a/src/TelnetSpy.cpp +++ b/src/TelnetSpy.cpp @@ -24,16 +24,6 @@ extern "C" { static TelnetSpy * actualObject = NULL; -static void TelnetSpy_putc(char c) { - if (actualObject) { - actualObject->write(c); - } -} - -static void TelnetSpy_ignore_putc(char c) { - ; -} - TelnetSpy::TelnetSpy() { port = TELNETSPY_PORT; telnetServer = NULL; @@ -437,19 +427,8 @@ void TelnetSpy::setDebugOutput(bool en) { if (debugOutput) { actualObject = this; - -#ifdef ESP8266 - os_install_putc1((void *)TelnetSpy_putc); // Set system printing (os_printf) to TelnetSpy - system_set_os_print(true); -#endif - } else { if (actualObject == this) { -#ifdef ESP8266 - system_set_os_print(false); - os_install_putc1((void *)TelnetSpy_ignore_putc); // Ignore system printing -#endif - actualObject = NULL; } } diff --git a/src/TelnetSpy.h b/src/TelnetSpy.h index 1f7b901c7..a7884adeb 100644 --- a/src/TelnetSpy.h +++ b/src/TelnetSpy.h @@ -151,13 +151,13 @@ #ifndef TelnetSpy_h #define TelnetSpy_h -#define TELNETSPY_BUFFER_LEN 3000 +#define TELNETSPY_BUFFER_LEN 1000 // was 3000 #define TELNETSPY_MIN_BLOCK_SIZE 64 #define TELNETSPY_COLLECTING_TIME 100 #define TELNETSPY_MAX_BLOCK_SIZE 512 #define TELNETSPY_PING_TIME 1500 #define TELNETSPY_PORT 23 -#define TELNETSPY_CAPTURE_OS_PRINT true +#define TELNETSPY_CAPTURE_OS_PRINT false #define TELNETSPY_WELCOME_MSG "Connection established via Telnet.\n" #define TELNETSPY_REJECT_MSG "Telnet: Only one connection possible.\n" diff --git a/src/TimeLib.cpp b/src/TimeLib.cpp index e59b24dc2..0595323c4 100644 --- a/src/TimeLib.cpp +++ b/src/TimeLib.cpp @@ -1,3 +1,8 @@ +/* + * customized version of Time library, originally Copyright (c) Michael Margolis 2009-2014 + * modified by Paul S https://github.com/PaulStoffregen/Time + */ + #include "TimeLib.h" static tmElements_t tm; // a cache of time elements @@ -96,6 +101,34 @@ void breakTime(time_t timeInput, tmElements_t & tm) { tm.Day = time + 1; // day of month } +// assemble time elements into time_t +time_t makeTime(const tmElements_t & tm) { + int i; + uint32_t seconds; + + // seconds from 1970 till 1 jan 00:00:00 of the given year + seconds = tm.Year * (SECS_PER_DAY * 365); + for (i = 0; i < tm.Year; i++) { + if (LEAP_YEAR(i)) { + seconds += SECS_PER_DAY; // add extra days for leap years + } + } + + // add days for this year, months start from 1 + for (i = 1; i < tm.Month; i++) { + if ((i == 2) && LEAP_YEAR(tm.Year)) { + seconds += SECS_PER_DAY * 29; + } else { + seconds += SECS_PER_DAY * monthDays[i - 1]; // monthDay array starts from 0 + } + } + seconds += (tm.Day - 1) * SECS_PER_DAY; + seconds += tm.Hour * SECS_PER_HOUR; + seconds += tm.Minute * SECS_PER_MIN; + seconds += tm.Second; + return (time_t)seconds; +} + void refreshCache(time_t t) { if (t != cacheTime) { breakTime(t, tm); @@ -118,6 +151,26 @@ uint8_t to_hour(time_t t) { // the hour for the given time return tm.Hour; } +uint8_t to_day(time_t t) { // the day for the given time (0-6) + refreshCache(t); + return tm.Day; +} + +uint8_t to_month(time_t t) { // the month for the given time + refreshCache(t); + return tm.Month; +} + +uint8_t to_weekday(time_t t) { + refreshCache(t); + return tm.Wday; +} + +uint16_t to_year(time_t t) { // the year for the given time + refreshCache(t); + return tm.Year + 1970; +} + void setTime(time_t t) { sysTime = (uint32_t)t; nextSyncTime = (uint32_t)t + syncInterval; diff --git a/src/TimeLib.h b/src/TimeLib.h index 0531e270a..604caa0e1 100644 --- a/src/TimeLib.h +++ b/src/TimeLib.h @@ -1,3 +1,8 @@ +/* + * customized version of Time library, originally Copyright (c) Michael Margolis 2009-2014 + * modified by Paul S https://github.com/PaulStoffregen/Time + */ + #ifndef _TimeLib_h #define _TimeLib_h @@ -38,8 +43,13 @@ void setSyncProvider(getExternalTime getTimeFunction); // identify the e void setSyncInterval(time_t interval); // set the number of seconds between re-sync time_t makeTime(const tmElements_t & tm); // convert time elements into time_t -uint8_t to_hour(time_t t); // the hour for the given time -uint8_t to_minute(time_t t); // the minute for the given time -uint8_t to_second(time_t t); // the second for the given time +uint8_t to_hour(time_t t); // the hour for the given time +uint8_t to_minute(time_t t); // the minute for the given time +uint8_t to_second(time_t t); // the second for the given time +uint8_t to_day(time_t t); // the day for the given time (0-6) +uint8_t to_month(time_t t); // the month for the given time +uint8_t to_weekday(time_t t); // weekday, sunday is day 1 +uint16_t to_year(time_t t); // the year for the given time + } #endif diff --git a/src/Timezone.cpp b/src/Timezone.cpp new file mode 100644 index 000000000..4aaa88e2b --- /dev/null +++ b/src/Timezone.cpp @@ -0,0 +1,200 @@ +/*----------------------------------------------------------------------* + * Arduino Timezone Library * + * Jack Christensen Mar 2012 * + * * + * Stripped down for myESP by Paul Derbyshire * + * * + * Arduino Timezone Library Copyright (C) 2018 by Jack Christensen and * + * licensed under GNU GPL v3.0, https://www.gnu.org/licenses/gpl.html * + *----------------------------------------------------------------------*/ + +#include "Timezone.h" + +/*----------------------------------------------------------------------* + * Create a Timezone object from the given time change rules. * + *----------------------------------------------------------------------*/ +Timezone::Timezone(TimeChangeRule dstStart, TimeChangeRule stdStart) + : m_dst(dstStart) + , m_std(stdStart) { + initTimeChanges(); +} + +/*----------------------------------------------------------------------* + * Create a Timezone object for a zone that does not observe * + * daylight time. * + *----------------------------------------------------------------------*/ +Timezone::Timezone(TimeChangeRule stdTime) + : m_dst(stdTime) + , m_std(stdTime) { + initTimeChanges(); +} + +/*----------------------------------------------------------------------* + * Convert the given UTC time to local time, standard or * + * daylight time, as appropriate. * + *----------------------------------------------------------------------*/ +time_t Timezone::toLocal(time_t utc) { + // recalculate the time change points if needed + if (to_year(utc) != to_year(m_dstUTC)) + calcTimeChanges(to_year(utc)); + + if (utcIsDST(utc)) + return utc + m_dst.offset * SECS_PER_MIN; + else + return utc + m_std.offset * SECS_PER_MIN; +} + +/*----------------------------------------------------------------------* + * Convert the given UTC time to local time, standard or * + * daylight time, as appropriate, and return a pointer to the time * + * change rule used to do the conversion. The caller must take care * + * not to alter this rule. * + *----------------------------------------------------------------------*/ +time_t Timezone::toLocal(time_t utc, TimeChangeRule ** tcr) { + // recalculate the time change points if needed + if (to_year(utc) != to_year(m_dstUTC)) + calcTimeChanges(to_year(utc)); + + if (utcIsDST(utc)) { + *tcr = &m_dst; + return utc + m_dst.offset * SECS_PER_MIN; + } else { + *tcr = &m_std; + return utc + m_std.offset * SECS_PER_MIN; + } +} + +/*----------------------------------------------------------------------* + * Convert the given local time to UTC time. * + * * + * WARNING: * + * This function is provided for completeness, but should seldom be * + * needed and should be used sparingly and carefully. * + * * + * Ambiguous situations occur after the Standard-to-DST and the * + * DST-to-Standard time transitions. When changing to DST, there is * + * one hour of local time that does not exist, since the clock moves * + * forward one hour. Similarly, when changing to standard time, there * + * is one hour of local times that occur twice since the clock moves * + * back one hour. * + * * + * This function does not test whether it is passed an erroneous time * + * value during the Local -> DST transition that does not exist. * + * If passed such a time, an incorrect UTC time value will be returned. * + * * + * If passed a local time value during the DST -> Local transition * + * that occurs twice, it will be treated as the earlier time, i.e. * + * the time that occurs before the transistion. * + * * + * Calling this function with local times during a transition interval * + * should be avoided! * + *----------------------------------------------------------------------*/ +time_t Timezone::toUTC(time_t local) { + // recalculate the time change points if needed + if (to_year(local) != to_year(m_dstLoc)) + calcTimeChanges(to_year(local)); + + if (locIsDST(local)) + return local - m_dst.offset * SECS_PER_MIN; + else + return local - m_std.offset * SECS_PER_MIN; +} + +/*----------------------------------------------------------------------* + * Determine whether the given UTC time_t is within the DST interval * + * or the Standard time interval. * + *----------------------------------------------------------------------*/ +bool Timezone::utcIsDST(time_t utc) { + // recalculate the time change points if needed + if (to_year(utc) != to_year(m_dstUTC)) + calcTimeChanges(to_year(utc)); + + if (m_stdUTC == m_dstUTC) // daylight time not observed in this tz + return false; + else if (m_stdUTC > m_dstUTC) // northern hemisphere + return (utc >= m_dstUTC && utc < m_stdUTC); + else // southern hemisphere + return !(utc >= m_stdUTC && utc < m_dstUTC); +} + +/*----------------------------------------------------------------------* + * Determine whether the given Local time_t is within the DST interval * + * or the Standard time interval. * + *----------------------------------------------------------------------*/ +bool Timezone::locIsDST(time_t local) { + // recalculate the time change points if needed + if (to_year(local) != to_year(m_dstLoc)) + calcTimeChanges(to_year(local)); + + if (m_stdUTC == m_dstUTC) // daylight time not observed in this tz + return false; + else if (m_stdLoc > m_dstLoc) // northern hemisphere + return (local >= m_dstLoc && local < m_stdLoc); + else // southern hemisphere + return !(local >= m_stdLoc && local < m_dstLoc); +} + +/*----------------------------------------------------------------------* + * Calculate the DST and standard time change points for the given * + * given year as local and UTC time_t values. * + *----------------------------------------------------------------------*/ +void Timezone::calcTimeChanges(int yr) { + m_dstLoc = toTime_t(m_dst, yr); + m_stdLoc = toTime_t(m_std, yr); + m_dstUTC = m_dstLoc - m_std.offset * SECS_PER_MIN; + m_stdUTC = m_stdLoc - m_dst.offset * SECS_PER_MIN; +} + +/*----------------------------------------------------------------------* + * Initialize the DST and standard time change points. * + *----------------------------------------------------------------------*/ +void Timezone::initTimeChanges() { + m_dstLoc = 0; + m_stdLoc = 0; + m_dstUTC = 0; + m_stdUTC = 0; +} + +/*----------------------------------------------------------------------* + * Convert the given time change rule to a time_t value * + * for the given year. * + *----------------------------------------------------------------------*/ +time_t Timezone::toTime_t(TimeChangeRule r, int yr) { + uint8_t m = r.month; // temp copies of r.month and r.week + uint8_t w = r.week; + if (w == 0) // is this a "Last week" rule? + { + if (++m > 12) // yes, for "Last", go to the next month + { + m = 1; + ++yr; + } + w = 1; // and treat as first week of next month, subtract 7 days later + } + + // calculate first day of the month, or for "Last" rules, first day of the next month + tmElements_t tm; + tm.Hour = r.hour; + tm.Minute = 0; + tm.Second = 0; + tm.Day = 1; + tm.Month = m; + tm.Year = yr - 1970; + time_t t = makeTime(tm); + + // add offset from the first of the month to r.dow, and offset for the given week + t += ((r.dow - to_weekday(t) + 7) % 7 + (w - 1) * 7) * SECS_PER_DAY; + // back up a week if this is a "Last" rule + if (r.week == 0) + t -= 7 * SECS_PER_DAY; + return t; +} + +/*----------------------------------------------------------------------* + * Read or update the daylight and standard time rules from RAM. * + *----------------------------------------------------------------------*/ +void Timezone::setRules(TimeChangeRule dstStart, TimeChangeRule stdStart) { + m_dst = dstStart; + m_std = stdStart; + initTimeChanges(); // force calcTimeChanges() at next conversion call +} diff --git a/src/Timezone.h b/src/Timezone.h new file mode 100644 index 000000000..2e8a93248 --- /dev/null +++ b/src/Timezone.h @@ -0,0 +1,53 @@ +/*----------------------------------------------------------------------* + * Arduino Timezone Library * + * Jack Christensen Mar 2012 * + * * + * Arduino Timezone Library Copyright (C) 2018 by Jack Christensen and * + * licensed under GNU GPL v3.0, https://www.gnu.org/licenses/gpl.html * + *----------------------------------------------------------------------*/ + +#ifndef TIMEZONE_H_INCLUDED +#define TIMEZONE_H_INCLUDED +#include +#include + +// convenient constants for TimeChangeRules +enum week_t { Last, First, Second, Third, Fourth }; +enum dow_t { Sun = 1, Mon, Tue, Wed, Thu, Fri, Sat }; +enum month_t { Jan = 1, Feb, Mar, Apr, May, Jun, Jul, Aug, Sep, Oct, Nov, Dec }; + +// structure to describe rules for when daylight/summer time begins, +// or when standard time begins. +struct TimeChangeRule { + char abbrev[6]; // five chars max + uint8_t week; // First, Second, Third, Fourth, or Last week of the month + uint8_t dow; // day of week, 1=Sun, 2=Mon, ... 7=Sat + uint8_t month; // 1=Jan, 2=Feb, ... 12=Dec + uint8_t hour; // 0-23 + int offset; // offset from UTC in minutes +}; + +class Timezone { + public: + Timezone(TimeChangeRule dstStart, TimeChangeRule stdStart); + Timezone(TimeChangeRule stdTime); + time_t toLocal(time_t utc); + time_t toLocal(time_t utc, TimeChangeRule ** tcr); + time_t toUTC(time_t local); + bool utcIsDST(time_t utc); + bool locIsDST(time_t local); + void setRules(TimeChangeRule dstStart, TimeChangeRule stdStart); + + private: + void calcTimeChanges(int yr); + void initTimeChanges(); + time_t toTime_t(TimeChangeRule r, int yr); + TimeChangeRule m_dst; // rule for start of dst or summer time for any year + TimeChangeRule m_std; // rule for start of standard time for any year + time_t m_dstUTC; // dst start for given/current year, given in UTC + time_t m_stdUTC; // std time start for given/current year, given in UTC + time_t m_dstLoc; // dst start for given/current year, given in local time + time_t m_stdLoc; // std time start for given/current year, given in local time +}; + +#endif diff --git a/src/custom.htm b/src/custom.htm index d7ca2187c..ad1e4d745 100644 --- a/src/custom.htm +++ b/src/custom.htm @@ -114,9 +114,9 @@
+ data-content="How often to send MQTT topics with stats. 0 is automatic. (in seconds)"> -
@@ -131,7 +131,7 @@
@@ -257,6 +257,7 @@
- +
+
\ No newline at end of file diff --git a/src/custom.js b/src/custom.js index 93eca92fd..5d8a16fd7 100644 --- a/src/custom.js +++ b/src/custom.js @@ -8,7 +8,7 @@ var custom_config = { "listen_mode": false, "shower_timer": false, "shower_alert": false, - "publish_time": 120, + "publish_time": 0, "tx_mode": 1 } }; @@ -23,15 +23,19 @@ function listcustom() { if (custom_config.settings.led) { $("input[name=\"led\"][value=\"1\"]").prop("checked", true); } + if (custom_config.settings.dallas_parasite) { $("input[name=\"dallas_parasite\"][value=\"1\"]").prop("checked", true); } + if (custom_config.settings.listen_mode) { $("input[name=\"listen_mode\"][value=\"1\"]").prop("checked", true); } + if (custom_config.settings.shower_timer) { $("input[name=\"shower_timer\"][value=\"1\"]").prop("checked", true); } + if (custom_config.settings.shower_alert) { $("input[name=\"shower_alert\"][value=\"1\"]").prop("checked", true); } @@ -95,13 +99,13 @@ function listCustomStats() { var l = document.createElement("li"); var type = obj[i].type; var color = ""; - if (type === 1) { + if (type === "UBAMaster") { color = "list-group-item-success"; - } else if (type === 2) { + } else if (type === "Thermostat") { color = "list-group-item-info"; - } else if (type === 3) { + } else if (type === "Solar Module") { color = "list-group-item-warning"; - } else if (type === 4) { + } else if (type === "Heat Pump") { color = "list-group-item-success"; } l.innerHTML = obj[i].model + " (Version:" + obj[i].version + " ProductID:" + obj[i].productid + " DeviceID:0x" + obj[i].deviceid + ")"; diff --git a/src/ds18.cpp b/src/ds18.cpp index 02d9d7139..fbaa1ed55 100644 --- a/src/ds18.cpp +++ b/src/ds18.cpp @@ -182,10 +182,10 @@ int16_t DS18::getRawValue(unsigned char index) { return raw; } -// return real value as a double +// return real value as a float // The raw temperature data is in units of sixteenths of a degree, so the value must be divided by 16 in order to convert it to degrees. -double DS18::getValue(unsigned char index) { - double value = (float)getRawValue(index) / 16.0; +float DS18::getValue(unsigned char index) { + float value = (float)getRawValue(index) / 16.0; return value; } diff --git a/src/ds18.h b/src/ds18.h index e6e97b300..3e2dd717e 100644 --- a/src/ds18.h +++ b/src/ds18.h @@ -39,7 +39,7 @@ class DS18 { uint8_t setup(uint8_t gpio, bool parasite); void loop(); char * getDeviceString(char * s, unsigned char index); - double getValue(unsigned char index); + float getValue(unsigned char index); int16_t getRawValue(unsigned char index); // raw values, needs / 16 protected: diff --git a/src/ems-esp.cpp b/src/ems-esp.cpp index f8aa35e81..d9b29c308 100644 --- a/src/ems-esp.cpp +++ b/src/ems-esp.cpp @@ -22,7 +22,6 @@ DS18 ds18; // public libraries #include // https://github.com/bblanchon/ArduinoJson -#include // https://github.com/bakercp/CRC32 // standard arduino libs #include // https://github.com/esp8266/Arduino/tree/master/libraries/Ticker @@ -31,16 +30,15 @@ DS18 ds18; #define APP_NAME "EMS-ESP" #define APP_HOSTNAME "ems-esp" #define APP_URL "https://github.com/proddy/EMS-ESP" -#define APP_UPDATEURL "https://api.github.com/repos/proddy/EMS-ESP/releases/latest" - -// set to value >0 if the ESP is overheating or there are timing issues. Recommend a value of 1. -#define EMSESP_DELAY 0 // initially set to 0 for no delay. Change to 1 if getting WDT resets from wifi +#define APP_URL_API "https://api.github.com/repos/proddy/EMS-ESP" // timers, all values are in seconds -#define DEFAULT_PUBLISHTIME 120 // every 2 minutes publish MQTT values, including Dallas sensors +#define DEFAULT_PUBLISHTIME 0 Ticker publishValuesTimer; Ticker publishSensorValuesTimer; +bool _need_first_publish = true; // this ensures on boot we always send out MQTT messages + #define SYSTEMCHECK_TIME 30 // every 30 seconds check if EMS can be reached Ticker systemCheckTimer; @@ -50,16 +48,6 @@ Ticker regularUpdatesTimer; #define LEDCHECK_TIME 500 // every 1/2 second blink the heartbeat LED Ticker ledcheckTimer; -// thermostat scan - for debugging -Ticker scanThermostat; -#define SCANTHERMOSTAT_TIME 1 -uint8_t scanThermostat_count = 0; - -// ems bus scan -Ticker scanDevices; -#define SCANDEVICES_TIME 350 // ms -uint8_t scanDevices_count; - Ticker showerColdShotStopTimer; // if using the shower timer, change these settings @@ -70,7 +58,6 @@ Ticker showerColdShotStopTimer; #define SHOWER_MAX_DURATION 420000 // in ms. 7 minutes, before trigger a shot of cold water // set this if using an external temperature sensor like a DS18B20 -// D5 is the default on a bbqkees board #define EMSESP_DALLAS_GPIO D5 #define EMSESP_DALLAS_PARASITE false @@ -80,8 +67,7 @@ Ticker showerColdShotStopTimer; #define EMSESP_LED_GPIO LED_BUILTIN typedef struct { - uint32_t timestamp; // for internal timings, via millis() - uint8_t dallas_sensors; // count of dallas sensors + uint8_t dallas_sensors; // count of dallas sensors // custom params bool shower_timer; // true if we want to report back on shower times @@ -112,11 +98,11 @@ static const command_t project_cmds[] PROGMEM = { {true, "listen_mode ", "when set to on all automatic Tx are disabled"}, {true, "shower_timer ", "send MQTT notification on all shower durations"}, {true, "shower_alert ", "stop hot water to send 3 cold burst warnings after max shower time is exceeded"}, - {true, "publish_time ", "set frequency for publishing data to MQTT (0=off)"}, - {true, "tx_mode ", "changes Tx logic. 1=ems generic, 2=ems+, 3=Junkers HT3"}, + {true, "publish_time ", "set frequency for publishing data to MQTT (0=automatic)"}, + {true, "tx_mode ", "changes Tx logic. 1=EMS generic, 2=EMS+, 3=HT3"}, - {false, "info", "show current captured on the devices"}, - {false, "log ", "set logging mode to none, basic, thermostat only, solar module only, raw, jabber or verbose"}, + {false, "info", "show current values deciphered from the EMS messages"}, + {false, "log ", "insert a test telegram on to the EMS bus"}, @@ -124,14 +110,13 @@ static const command_t project_cmds[] PROGMEM = { {false, "publish", "publish all values to MQTT"}, {false, "refresh", "fetch values from the EMS devices"}, - {false, "devices [all]", "list all supported and detected EMS devices"}, + {false, "devices", "list detected EMS devices"}, {false, "queue", "show current Tx queue"}, - {false, "autodetect [quick | deep]", "detect EMS devices and attempt to automatically set boiler and thermostat types"}, + {false, "autodetect [scan]", "detect EMS devices and attempt to automatically set boiler and thermostat types"}, {false, "send XX ...", "send raw telegram data to EMS bus (XX are hex values)"}, {false, "thermostat read ", "send read request to the thermostat for heating circuit hc 1-4"}, {false, "thermostat temp [hc] ", "set current thermostat temperature"}, - {false, "thermostat mode [hc] ", "set mode (0=low/night, 1=manual/day, 2=auto) for heating circuit hc 1-4"}, - {false, "thermostat scan ", "probe thermostat on all type id responses"}, + {false, "thermostat mode [hc] ", "set mode (0=off, 1=manual, 2=auto) for heating circuit hc 1-4"}, {false, "boiler read ", "send read request to boiler"}, {false, "boiler wwtemp ", "set boiler warm water temperature"}, {false, "boiler tapwater ", "set boiler warm tap water on/off"}, @@ -152,51 +137,81 @@ void myDebugLog(const char * s) { } } -// figures out the thermostat mode -// returns 0xFF=unknown, 0=low, 1=manual, 2=auto, 3=night, 4=day +// figures out the thermostat mode (manual/auto) depending on the thermostat type +// returns {EMS_THERMOSTAT_MODE_UNKNOWN, EMS_THERMOSTAT_MODE_OFF, EMS_THERMOSTAT_MODE_MANUAL, EMS_THERMOSTAT_MODE_AUTO, EMS_THERMOSTAT_MODE_NIGHT, EMS_THERMOSTAT_MODE_DAY} // hc_num is 1 to 4 -uint8_t _getThermostatMode(uint8_t hc_num) { - int thermoMode = EMS_VALUE_INT_NOTSET; - uint8_t model = ems_getThermostatModel(); +_EMS_THERMOSTAT_MODE _getThermostatMode(uint8_t hc_num) { + _EMS_THERMOSTAT_MODE thermoMode = EMS_THERMOSTAT_MODE_UNKNOWN; - uint8_t mode = EMS_Thermostat.hc[hc_num - 1].mode; + uint8_t mode = EMS_Thermostat.hc[hc_num - 1].mode; + uint8_t model = ems_getThermostatModel(); - if (model == EMS_MODEL_RC20) { + if (model == EMS_DEVICE_FLAG_RC20) { if (mode == 0) { - thermoMode = 0; // low + thermoMode = EMS_THERMOSTAT_MODE_OFF; } else if (mode == 1) { - thermoMode = 1; // manual + thermoMode = EMS_THERMOSTAT_MODE_MANUAL; } else if (mode == 2) { - thermoMode = 2; // auto + thermoMode = EMS_THERMOSTAT_MODE_AUTO; } - } else if (model == EMS_MODEL_RC300) { + } else if (model == EMS_DEVICE_FLAG_RC300) { if (mode == 0) { - thermoMode = 1; // manual + thermoMode = EMS_THERMOSTAT_MODE_MANUAL; } else if (mode == 1) { - thermoMode = 2; // auto + thermoMode = EMS_THERMOSTAT_MODE_AUTO; } - } else if (model == EMS_MODEL_FW100 || model == EMS_MODEL_FW120) { - if (mode == 3) { - thermoMode = 4; + } else if (model == EMS_DEVICE_FLAG_JUNKERS) { + if (mode == 1) { + thermoMode = EMS_THERMOSTAT_MODE_MANUAL; } else if (mode == 2) { - thermoMode = 3; - } else if (mode == 1) { - thermoMode = 0; + thermoMode = EMS_THERMOSTAT_MODE_AUTO; } } else { // default for all other thermostats if (mode == 0) { - thermoMode = 3; // night + thermoMode = EMS_THERMOSTAT_MODE_NIGHT; } else if (mode == 1) { - thermoMode = 4; // day + thermoMode = EMS_THERMOSTAT_MODE_DAY; } else if (mode == 2) { - thermoMode = 2; // auto + thermoMode = EMS_THERMOSTAT_MODE_AUTO; } } return thermoMode; } -// Show command - display stats on an 's' command +// figures out the thermostat day/night mode depending on the thermostat type +// returns {EMS_THERMOSTAT_MODE_NIGHT, EMS_THERMOSTAT_MODE_DAY} +// hc_num is 1 to 4 +_EMS_THERMOSTAT_MODE _getThermostatDayMode(uint8_t hc_num) { + _EMS_THERMOSTAT_MODE thermoMode = EMS_THERMOSTAT_MODE_UNKNOWN; + uint8_t model = ems_getThermostatModel(); + + uint8_t mode = EMS_Thermostat.hc[hc_num - 1].day_mode; + + if (model == EMS_DEVICE_FLAG_JUNKERS) { + if (mode == 3) { + thermoMode = EMS_THERMOSTAT_MODE_DAY; + } else if (mode == 2) { + thermoMode = EMS_THERMOSTAT_MODE_NIGHT; + } + } else if (model == EMS_DEVICE_FLAG_RC35) { + if (mode == 0) { + thermoMode = EMS_THERMOSTAT_MODE_NIGHT; + } else if (mode == 1) { + thermoMode = EMS_THERMOSTAT_MODE_DAY; + } + } else if (model == EMS_DEVICE_FLAG_RC300) { + if (mode == 0) { + thermoMode = EMS_THERMOSTAT_MODE_NIGHT; + } else if (mode == 1) { + thermoMode = EMS_THERMOSTAT_MODE_DAY; + } + } + + return thermoMode; +} + +// Info - display stats on an 'info' command void showInfo() { // General stats from EMS bus @@ -214,27 +229,31 @@ void showInfo() { myDebug_P(PSTR(" System logging set to Solar Module only")); } else if (sysLog == EMS_SYS_LOGGING_JABBER) { myDebug_P(PSTR(" System logging set to Jabber")); + } else if (sysLog == EMS_SYS_LOGGING_WATCH) { + myDebug_P(PSTR(" System logging set to Watch")); } else { myDebug_P(PSTR(" System logging set to None")); } - myDebug_P(PSTR(" LED is %s, Listen mode is %s"), EMSESP_Settings.led ? "on" : "off", EMSESP_Settings.listen_mode ? "on" : "off"); + myDebug_P(PSTR(" LED: %s, Listen mode: %s"), EMSESP_Settings.led ? "on" : "off", EMSESP_Settings.listen_mode ? "on" : "off"); if (EMSESP_Settings.dallas_sensors > 0) { myDebug_P(PSTR(" %d external temperature sensor%s found"), EMSESP_Settings.dallas_sensors, (EMSESP_Settings.dallas_sensors == 1) ? "" : "s"); } - myDebug_P(PSTR(" Boiler is %s, Thermostat is %s, Solar Module is %s, Mixing Module is %s, Shower Timer is %s, Shower Alert is %s"), + myDebug_P(PSTR(" Boiler: %s, Thermostat: %s, Solar Module: %s, Mixing Module: %s"), (ems_getBoilerEnabled() ? "enabled" : "disabled"), (ems_getThermostatEnabled() ? "enabled" : "disabled"), (ems_getSolarModuleEnabled() ? "enabled" : "disabled"), - (ems_getMixingDeviceEnabled() ? "enabled" : "disabled"), + (ems_getMixingDeviceEnabled() ? "enabled" : "disabled")); + + myDebug_P(PSTR(" Shower Timer: %s, Shower Alert: %s"), ((EMSESP_Settings.shower_timer) ? "enabled" : "disabled"), ((EMSESP_Settings.shower_alert) ? "enabled" : "disabled")); myDebug_P(PSTR("\n%sEMS Bus stats:%s"), COLOR_BOLD_ON, COLOR_BOLD_OFF); if (ems_getBusConnected()) { - myDebug_P(PSTR(" Bus is connected, protocol: %s"), ((EMS_Sys_Status.emsIDMask == 0x80) ? "Junkers HT3" : "Buderus")); + myDebug_P(PSTR(" Bus is connected, protocol: %s"), ((EMS_Sys_Status.emsIDMask == 0x80) ? "HT3" : "Buderus")); myDebug_P(PSTR(" Rx: # successful read requests=%d, # CRC errors=%d"), EMS_Sys_Status.emsRxPgks, EMS_Sys_Status.emxCrcErr); if (ems_getTxCapable()) { @@ -253,7 +272,7 @@ void showInfo() { myDebug_P(PSTR("%sBoiler stats:%s"), COLOR_BOLD_ON, COLOR_BOLD_OFF); // version details - myDebug_P(PSTR(" Boiler: %s"), ems_getBoilerDescription(buffer_type)); + myDebug_P(PSTR(" Boiler: %s"), ems_getDeviceDescription(EMS_DEVICE_TYPE_BOILER, buffer_type)); // active stats if (ems_getBusConnected()) { @@ -346,7 +365,7 @@ void showInfo() { if (ems_getSolarModuleEnabled()) { myDebug_P(PSTR("")); // newline myDebug_P(PSTR("%sSolar Module stats:%s"), COLOR_BOLD_ON, COLOR_BOLD_OFF); - myDebug_P(PSTR(" Solar module: %s"), ems_getSolarModuleDescription(buffer_type)); + myDebug_P(PSTR(" Solar module: %s"), ems_getDeviceDescription(EMS_DEVICE_TYPE_SOLAR, buffer_type)); _renderShortValue("Collector temperature", "C", EMS_SolarModule.collectorTemp); _renderShortValue("Bottom temperature", "C", EMS_SolarModule.bottomTemp); _renderIntValue("Pump modulation", "%", EMS_SolarModule.pumpModulation); @@ -366,7 +385,7 @@ void showInfo() { if (ems_getHeatPumpEnabled()) { myDebug_P(PSTR("")); // newline myDebug_P(PSTR("%sHeat Pump stats:%s"), COLOR_BOLD_ON, COLOR_BOLD_OFF); - myDebug_P(PSTR(" Heat Pump module: %s"), ems_getHeatPumpDescription(buffer_type)); + myDebug_P(PSTR(" Heat Pump module: %s"), ems_getDeviceDescription(EMS_DEVICE_TYPE_HEATPUMP, buffer_type)); _renderIntValue("Pump modulation", "%", EMS_HeatPump.HPModulation); _renderIntValue("Pump speed", "%", EMS_HeatPump.HPSpeed); } @@ -375,23 +394,21 @@ void showInfo() { if (ems_getThermostatEnabled()) { myDebug_P(PSTR("")); // newline myDebug_P(PSTR("%sThermostat stats:%s"), COLOR_BOLD_ON, COLOR_BOLD_OFF); - myDebug_P(PSTR(" Thermostat: %s"), ems_getThermostatDescription(buffer_type, false)); + myDebug_P(PSTR(" Thermostat: %s"), ems_getDeviceDescription(EMS_DEVICE_TYPE_THERMOSTAT, buffer_type, false)); // Render Thermostat Date & Time uint8_t model = ems_getThermostatModel(); - if ((model != EMS_MODEL_EASY)) { + if ((model != EMS_DEVICE_FLAG_EASY)) { myDebug_P(PSTR(" Thermostat time is %s"), EMS_Thermostat.datetime); } uint8_t _m_setpoint, _m_curr; switch (model) { - case EMS_MODEL_EASY: + case EMS_DEVICE_FLAG_EASY: _m_setpoint = 10; // *100 _m_curr = 10; // *100 break; - case EMS_MODEL_FR10: - case EMS_MODEL_FW100: - case EMS_MODEL_FW120: + case EMS_DEVICE_FLAG_JUNKERS: _m_setpoint = 1; // *10 _m_curr = 1; // *10 break; @@ -411,7 +428,7 @@ void showInfo() { // Render Day/Night/Holiday Temperature on RC35s // there is no single setpoint temp, but one for day, night and vacation - if (model == EMS_MODEL_RC35) { + if (model == EMS_DEVICE_FLAG_RC35) { if (EMS_Thermostat.hc[hc_num - 1].summer_mode) { myDebug_P(PSTR(" Program is set to Summer mode")); } else if (EMS_Thermostat.hc[hc_num - 1].holiday_mode) { @@ -423,20 +440,27 @@ void showInfo() { _renderIntValue(" Vacation temperature", "C", EMS_Thermostat.hc[hc_num - 1].holidaytemp, 2); // convert to a single byte * 2 } - // Render Termostat Mode, if we have a mode - uint8_t thermoMode = _getThermostatMode(hc_num); // 0xFF=unknown, 0=off, 1=manual, 2=auto, 3=night, 4=day - if (thermoMode == 0) { + // Render Thermostat Mode + _EMS_THERMOSTAT_MODE thermoMode; + thermoMode = _getThermostatMode(hc_num); + if (thermoMode == EMS_THERMOSTAT_MODE_OFF) { myDebug_P(PSTR(" Mode is set to off")); - } else if (thermoMode == 1) { + } else if (thermoMode == EMS_THERMOSTAT_MODE_MANUAL) { myDebug_P(PSTR(" Mode is set to manual")); - } else if (thermoMode == 2) { + } else if (thermoMode == EMS_THERMOSTAT_MODE_AUTO) { myDebug_P(PSTR(" Mode is set to auto")); - } else if (thermoMode == 3) { + } else if (thermoMode == EMS_THERMOSTAT_MODE_NIGHT) { myDebug_P(PSTR(" Mode is set to night")); - } else if (thermoMode == 4) { + } else if (thermoMode == EMS_THERMOSTAT_MODE_DAY) { myDebug_P(PSTR(" Mode is set to day")); - } else { - myDebug_P(PSTR(" Mode is unknown")); + } + + // Render Thermostat Day Mode + thermoMode = _getThermostatDayMode(hc_num); + if (thermoMode == EMS_THERMOSTAT_MODE_NIGHT) { + myDebug_P(PSTR(" Day Mode is set to night")); + } else if (thermoMode == EMS_THERMOSTAT_MODE_DAY) { + myDebug_P(PSTR(" Day Mode is set to day")); } } } @@ -469,13 +493,6 @@ void showInfo() { } } - // show the Shower Info - if (EMSESP_Settings.shower_timer) { - myDebug_P(PSTR("")); // newline - myDebug_P(PSTR("%sShower stats:%s"), COLOR_BOLD_ON, COLOR_BOLD_OFF); - myDebug_P(PSTR(" Shower is %s"), (EMSESP_Shower.showerOn ? "running" : "off")); - } - myDebug_P(PSTR("")); // newline } @@ -486,6 +503,10 @@ void publishSensorValues() { return; } + if (!EMSESP_Settings.dallas_sensors) { + return; // no sensors attached + } + StaticJsonDocument<200> doc; JsonObject sensors = doc.to(); @@ -495,7 +516,7 @@ void publishSensorValues() { // see if the sensor values have changed, if so send it on for (uint8_t i = 0; i < EMSESP_Settings.dallas_sensors; i++) { // round to 2 decimal places. from https://arduinojson.org/v6/faq/how-to-configure-the-serialization-of-floats/ - double sensorValue = (int)(ds18.getValue(i) * 100 + 0.5) / 100.0; + float sensorValue = (int)(ds18.getValue(i) * 100 + 0.5) / 100.0; if (sensorValue != DS18_DISCONNECTED && sensorValue != DS18_CRC_ERROR) { sprintf(label, PAYLOAD_EXTERNAL_SENSORS, (i + 1)); sensors[label] = sensorValue; @@ -503,167 +524,147 @@ void publishSensorValues() { } } - if (hasdata) { - char data[200] = {0}; - serializeJson(doc, data, sizeof(data)); - myDebugLog("Publishing external sensor data via MQTT"); - myESP.mqttPublish(TOPIC_EXTERNAL_SENSORS, data); + if (!hasdata) { + return; // nothing to send } + + char data[200] = {0}; + serializeJson(doc, data, sizeof(data)); + myDebugLog("Publishing external sensor data via MQTT"); + myESP.mqttPublish(TOPIC_EXTERNAL_SENSORS, data); } // send values via MQTT -// a json object is created for the boiler and one for the thermostat -// CRC check is done to see if there are changes in the values since the last send to avoid too much wifi traffic -// a check is done against the previous values and if there are changes only then they are published. Unless force=true -void publishValues(bool force) { - // don't send if MQTT is not connected - if (!myESP.isMQTTConnected()) { - return; - } - - // don't publish is publish time is set to 0 - if (EMSESP_Settings.publish_time == 0) { +// a json object is created for each device type +void publishEMSValues(bool force) { + // don't send if MQTT is not connected or EMS bus is not connected + if (!myESP.isMQTTConnected() || (!ems_getBusConnected())) { return; } char s[20] = {0}; // for formatting strings StaticJsonDocument doc; char data[MQTT_MAX_PAYLOAD_SIZE] = {0}; - CRC32 crc; - uint32_t fchecksum; uint8_t jsonSize; - static uint8_t last_boilerActive = 0xFF; // for remembering last setting of the tap water or heating on/off - static uint32_t previousBoilerPublishCRC = 0; // CRC check for boiler values - static uint32_t previousThermostatPublishCRC; // CRC check for thermostat values - static uint32_t previousMixingPublishCRC; // CRC check for mixing values - static uint32_t previousSMPublishCRC = 0; // CRC check for Solar Module values (e.g. SM10) + static uint8_t last_boilerActive = 0xFF; // for remembering last setting of the tap water or heating on/off - JsonObject rootBoiler = doc.to(); + // do we have boiler changes? + if (ems_getBoilerEnabled() && (ems_Device_has_flags(EMS_DEVICE_UPDATE_FLAG_BOILER) || force)) { + JsonObject rootBoiler = doc.to(); - if (EMS_Boiler.wWComfort == EMS_VALUE_UBAParameterWW_wwComfort_Hot) { - rootBoiler["wWComfort"] = "Hot"; - } else if (EMS_Boiler.wWComfort == EMS_VALUE_UBAParameterWW_wwComfort_Eco) { - rootBoiler["wWComfort"] = "Eco"; - } else if (EMS_Boiler.wWComfort == EMS_VALUE_UBAParameterWW_wwComfort_Intelligent) { - rootBoiler["wWComfort"] = "Intelligent"; - } - - if (EMS_Boiler.wWSelTemp != EMS_VALUE_INT_NOTSET) - rootBoiler["wWSelTemp"] = EMS_Boiler.wWSelTemp; - if (EMS_Boiler.wWDesiredTemp != EMS_VALUE_INT_NOTSET) - rootBoiler["wWDesiredTemp"] = EMS_Boiler.wWDesiredTemp; - if (EMS_Boiler.selFlowTemp != EMS_VALUE_INT_NOTSET) - rootBoiler["selFlowTemp"] = EMS_Boiler.selFlowTemp; - if (EMS_Boiler.selBurnPow != EMS_VALUE_INT_NOTSET) - rootBoiler["selBurnPow"] = EMS_Boiler.selBurnPow; - if (EMS_Boiler.curBurnPow != EMS_VALUE_INT_NOTSET) - rootBoiler["curBurnPow"] = EMS_Boiler.curBurnPow; - if (EMS_Boiler.pumpMod != EMS_VALUE_INT_NOTSET) - rootBoiler["pumpMod"] = EMS_Boiler.pumpMod; - if (EMS_Boiler.wWCircPump != EMS_VALUE_INT_NOTSET) - rootBoiler["wWCircPump"] = EMS_Boiler.wWCircPump; - - if (EMS_Boiler.extTemp != EMS_VALUE_SHORT_NOTSET) - rootBoiler["outdoorTemp"] = (double)EMS_Boiler.extTemp / 10; - if (EMS_Boiler.wWCurTmp != EMS_VALUE_USHORT_NOTSET) - rootBoiler["wWCurTmp"] = (double)EMS_Boiler.wWCurTmp / 10; - if (EMS_Boiler.wWCurFlow != EMS_VALUE_INT_NOTSET) - rootBoiler["wWCurFlow"] = (double)EMS_Boiler.wWCurFlow / 10; - if (EMS_Boiler.curFlowTemp != EMS_VALUE_USHORT_NOTSET) - rootBoiler["curFlowTemp"] = (double)EMS_Boiler.curFlowTemp / 10; - if (EMS_Boiler.retTemp != EMS_VALUE_USHORT_NOTSET) - rootBoiler["retTemp"] = (double)EMS_Boiler.retTemp / 10; - if (EMS_Boiler.switchTemp != EMS_VALUE_USHORT_NOTSET) - rootBoiler["switchTemp"] = (double)EMS_Boiler.switchTemp / 10; - if (EMS_Boiler.sysPress != EMS_VALUE_INT_NOTSET) - rootBoiler["sysPress"] = (double)EMS_Boiler.sysPress / 10; - if (EMS_Boiler.boilTemp != EMS_VALUE_USHORT_NOTSET) - rootBoiler["boilTemp"] = (double)EMS_Boiler.boilTemp / 10; - - if (EMS_Boiler.wWActivated != EMS_VALUE_INT_NOTSET) - rootBoiler["wWActivated"] = _bool_to_char(s, EMS_Boiler.wWActivated); - - if (EMS_Boiler.burnGas != EMS_VALUE_INT_NOTSET) - rootBoiler["burnGas"] = _bool_to_char(s, EMS_Boiler.burnGas); - - if (EMS_Boiler.flameCurr != EMS_VALUE_USHORT_NOTSET) - rootBoiler["flameCurr"] = (double)(int16_t)EMS_Boiler.flameCurr / 10; - - if (EMS_Boiler.heatPmp != EMS_VALUE_INT_NOTSET) - rootBoiler["heatPmp"] = _bool_to_char(s, EMS_Boiler.heatPmp); - - if (EMS_Boiler.fanWork != EMS_VALUE_INT_NOTSET) - rootBoiler["fanWork"] = _bool_to_char(s, EMS_Boiler.fanWork); - - if (EMS_Boiler.ignWork != EMS_VALUE_INT_NOTSET) - rootBoiler["ignWork"] = _bool_to_char(s, EMS_Boiler.ignWork); - - if (EMS_Boiler.wWCirc != EMS_VALUE_INT_NOTSET) - rootBoiler["wWCirc"] = _bool_to_char(s, EMS_Boiler.wWCirc); - - if (EMS_Boiler.heating_temp != EMS_VALUE_INT_NOTSET) - rootBoiler["heating_temp"] = EMS_Boiler.heating_temp; - if (EMS_Boiler.pump_mod_max != EMS_VALUE_INT_NOTSET) - rootBoiler["pump_mod_max"] = EMS_Boiler.pump_mod_max; - if (EMS_Boiler.pump_mod_min != EMS_VALUE_INT_NOTSET) - rootBoiler["pump_mod_min"] = EMS_Boiler.pump_mod_min; - - if (EMS_Boiler.wWHeat != EMS_VALUE_INT_NOTSET) - rootBoiler["wWHeat"] = _bool_to_char(s, EMS_Boiler.wWHeat); - - // **** also add burnStarts, burnWorkMin, heatWorkMin - if (abs(EMS_Boiler.wWStarts) != EMS_VALUE_LONG_NOTSET) - rootBoiler["wWStarts"] = (double)EMS_Boiler.wWStarts; - if (abs(EMS_Boiler.wWWorkM) != EMS_VALUE_LONG_NOTSET) - rootBoiler["wWWorkM"] = (double)EMS_Boiler.wWWorkM; - if (abs(EMS_Boiler.UBAuptime) != EMS_VALUE_LONG_NOTSET) - rootBoiler["UBAuptime"] = (double)EMS_Boiler.UBAuptime; - - // **** also add burnStarts, burnWorkMin, heatWorkMin - if (abs(EMS_Boiler.burnStarts) != EMS_VALUE_LONG_NOTSET) - rootBoiler["burnStarts"] = (double)EMS_Boiler.burnStarts; - if (abs(EMS_Boiler.burnWorkMin) != EMS_VALUE_LONG_NOTSET) - rootBoiler["burnWorkMin"] = (double)EMS_Boiler.burnWorkMin; - if (abs(EMS_Boiler.heatWorkMin) != EMS_VALUE_LONG_NOTSET) - rootBoiler["heatWorkMin"] = (double)EMS_Boiler.heatWorkMin; - - if (EMS_Boiler.serviceCode != EMS_VALUE_USHORT_NOTSET) { - rootBoiler["ServiceCode"] = EMS_Boiler.serviceCodeChar; - rootBoiler["ServiceCodeNumber"] = EMS_Boiler.serviceCode; - } - - serializeJson(doc, data, sizeof(data)); - - // check for empty json - jsonSize = measureJson(doc); - if (jsonSize > 2) { - // calculate hash and send values if something has changed, to save unnecessary wifi traffic - for (uint8_t i = 0; i < (jsonSize - 1); i++) { - crc.update(data[i]); + if (EMS_Boiler.wWComfort == EMS_VALUE_UBAParameterWW_wwComfort_Hot) { + rootBoiler["wWComfort"] = "Hot"; + } else if (EMS_Boiler.wWComfort == EMS_VALUE_UBAParameterWW_wwComfort_Eco) { + rootBoiler["wWComfort"] = "Eco"; + } else if (EMS_Boiler.wWComfort == EMS_VALUE_UBAParameterWW_wwComfort_Intelligent) { + rootBoiler["wWComfort"] = "Intelligent"; } - fchecksum = crc.finalize(); - if ((previousBoilerPublishCRC != fchecksum) || force) { - previousBoilerPublishCRC = fchecksum; - myDebugLog("Publishing boiler data via MQTT"); - // send values via MQTT - myESP.mqttPublish(TOPIC_BOILER_DATA, data); + if (EMS_Boiler.wWSelTemp != EMS_VALUE_INT_NOTSET) + rootBoiler["wWSelTemp"] = EMS_Boiler.wWSelTemp; + if (EMS_Boiler.wWDesiredTemp != EMS_VALUE_INT_NOTSET) + rootBoiler["wWDesiredTemp"] = EMS_Boiler.wWDesiredTemp; + if (EMS_Boiler.selFlowTemp != EMS_VALUE_INT_NOTSET) + rootBoiler["selFlowTemp"] = EMS_Boiler.selFlowTemp; + if (EMS_Boiler.selBurnPow != EMS_VALUE_INT_NOTSET) + rootBoiler["selBurnPow"] = EMS_Boiler.selBurnPow; + if (EMS_Boiler.curBurnPow != EMS_VALUE_INT_NOTSET) + rootBoiler["curBurnPow"] = EMS_Boiler.curBurnPow; + if (EMS_Boiler.pumpMod != EMS_VALUE_INT_NOTSET) + rootBoiler["pumpMod"] = EMS_Boiler.pumpMod; + if (EMS_Boiler.wWCircPump != EMS_VALUE_BOOL_NOTSET) + rootBoiler["wWCircPump"] = EMS_Boiler.wWCircPump; + + if (EMS_Boiler.extTemp != EMS_VALUE_SHORT_NOTSET) + rootBoiler["outdoorTemp"] = (float)EMS_Boiler.extTemp / 10; + if (EMS_Boiler.wWCurTmp != EMS_VALUE_USHORT_NOTSET) + rootBoiler["wWCurTmp"] = (float)EMS_Boiler.wWCurTmp / 10; + if (EMS_Boiler.wWCurFlow != EMS_VALUE_INT_NOTSET) + rootBoiler["wWCurFlow"] = (float)EMS_Boiler.wWCurFlow / 10; + if (EMS_Boiler.curFlowTemp != EMS_VALUE_USHORT_NOTSET) + rootBoiler["curFlowTemp"] = (float)EMS_Boiler.curFlowTemp / 10; + if (EMS_Boiler.retTemp != EMS_VALUE_USHORT_NOTSET) + rootBoiler["retTemp"] = (float)EMS_Boiler.retTemp / 10; + if (EMS_Boiler.switchTemp != EMS_VALUE_USHORT_NOTSET) + rootBoiler["switchTemp"] = (float)EMS_Boiler.switchTemp / 10; + if (EMS_Boiler.sysPress != EMS_VALUE_INT_NOTSET) + rootBoiler["sysPress"] = (float)EMS_Boiler.sysPress / 10; + if (EMS_Boiler.boilTemp != EMS_VALUE_USHORT_NOTSET) + rootBoiler["boilTemp"] = (float)EMS_Boiler.boilTemp / 10; + + if (EMS_Boiler.wWActivated != EMS_VALUE_BOOL_NOTSET) + rootBoiler["wWActivated"] = _bool_to_char(s, EMS_Boiler.wWActivated); + + if (EMS_Boiler.wWActivated != EMS_VALUE_BOOL_NOTSET) + rootBoiler["wWOnetime"] = _bool_to_char(s, EMS_Boiler.wWOneTime); + + if (EMS_Boiler.burnGas != EMS_VALUE_BOOL_NOTSET) + rootBoiler["burnGas"] = _bool_to_char(s, EMS_Boiler.burnGas); + + if (EMS_Boiler.flameCurr != EMS_VALUE_USHORT_NOTSET) + rootBoiler["flameCurr"] = (float)(int16_t)EMS_Boiler.flameCurr / 10; + + if (EMS_Boiler.heatPmp != EMS_VALUE_BOOL_NOTSET) + rootBoiler["heatPmp"] = _bool_to_char(s, EMS_Boiler.heatPmp); + + if (EMS_Boiler.fanWork != EMS_VALUE_BOOL_NOTSET) + rootBoiler["fanWork"] = _bool_to_char(s, EMS_Boiler.fanWork); + + if (EMS_Boiler.ignWork != EMS_VALUE_BOOL_NOTSET) + rootBoiler["ignWork"] = _bool_to_char(s, EMS_Boiler.ignWork); + + if (EMS_Boiler.wWCirc != EMS_VALUE_BOOL_NOTSET) + rootBoiler["wWCirc"] = _bool_to_char(s, EMS_Boiler.wWCirc); + + if (EMS_Boiler.heating_temp != EMS_VALUE_INT_NOTSET) + rootBoiler["heating_temp"] = EMS_Boiler.heating_temp; + if (EMS_Boiler.pump_mod_max != EMS_VALUE_INT_NOTSET) + rootBoiler["pump_mod_max"] = EMS_Boiler.pump_mod_max; + if (EMS_Boiler.pump_mod_min != EMS_VALUE_INT_NOTSET) + rootBoiler["pump_mod_min"] = EMS_Boiler.pump_mod_min; + + if (EMS_Boiler.wWHeat != EMS_VALUE_BOOL_NOTSET) + rootBoiler["wWHeat"] = _bool_to_char(s, EMS_Boiler.wWHeat); + + if (abs(EMS_Boiler.wWStarts) != EMS_VALUE_LONG_NOTSET) + rootBoiler["wWStarts"] = (float)EMS_Boiler.wWStarts; + if (abs(EMS_Boiler.wWWorkM) != EMS_VALUE_LONG_NOTSET) + rootBoiler["wWWorkM"] = (float)EMS_Boiler.wWWorkM; + if (abs(EMS_Boiler.UBAuptime) != EMS_VALUE_LONG_NOTSET) + rootBoiler["UBAuptime"] = (float)EMS_Boiler.UBAuptime; + + if (abs(EMS_Boiler.burnStarts) != EMS_VALUE_LONG_NOTSET) + rootBoiler["burnStarts"] = (float)EMS_Boiler.burnStarts; + if (abs(EMS_Boiler.burnWorkMin) != EMS_VALUE_LONG_NOTSET) + rootBoiler["burnWorkMin"] = (float)EMS_Boiler.burnWorkMin; + if (abs(EMS_Boiler.heatWorkMin) != EMS_VALUE_LONG_NOTSET) + rootBoiler["heatWorkMin"] = (float)EMS_Boiler.heatWorkMin; + + if (EMS_Boiler.serviceCode != EMS_VALUE_USHORT_NOTSET) { + rootBoiler["ServiceCode"] = EMS_Boiler.serviceCodeChar; + rootBoiler["ServiceCodeNumber"] = EMS_Boiler.serviceCode; } - } - // see if the heating or hot tap water has changed, if so send - // last_boilerActive stores heating in bit 1 and tap water in bit 2 - if ((last_boilerActive != ((EMS_Boiler.tapwaterActive << 1) + EMS_Boiler.heatingActive)) || force) { - myDebugLog("Publishing hot water and heating states via MQTT"); - myESP.mqttPublish(TOPIC_BOILER_TAPWATER_ACTIVE, EMS_Boiler.tapwaterActive == 1 ? "1" : "0"); - myESP.mqttPublish(TOPIC_BOILER_HEATING_ACTIVE, EMS_Boiler.heatingActive == 1 ? "1" : "0"); + serializeJson(doc, data, sizeof(data)); + myDebugLog("Publishing boiler data via MQTT"); + myESP.mqttPublish(TOPIC_BOILER_DATA, data); - last_boilerActive = ((EMS_Boiler.tapwaterActive << 1) + EMS_Boiler.heatingActive); // remember last state + // see if the heating or hot tap water has changed, if so send + // last_boilerActive stores heating in bit 1 and tap water in bit 2 + if ((last_boilerActive != ((EMS_Boiler.tapwaterActive << 1) + EMS_Boiler.heatingActive)) || force) { + myDebugLog("Publishing hot water and heating states via MQTT"); + myESP.mqttPublish(TOPIC_BOILER_TAPWATER_ACTIVE, EMS_Boiler.tapwaterActive == 1 ? "1" : "0"); + myESP.mqttPublish(TOPIC_BOILER_HEATING_ACTIVE, EMS_Boiler.heatingActive == 1 ? "1" : "0"); + + last_boilerActive = ((EMS_Boiler.tapwaterActive << 1) + EMS_Boiler.heatingActive); // remember last state + } + + ems_Device_remove_flags(EMS_DEVICE_UPDATE_FLAG_BOILER); // unset flag } // handle the thermostat values - if (ems_getThermostatEnabled()) { + if (ems_getThermostatEnabled() && (ems_Device_has_flags(EMS_DEVICE_UPDATE_FLAG_THERMOSTAT) || force)) { doc.clear(); JsonObject rootThermostat = doc.to(); @@ -677,31 +678,31 @@ void publishValues(bool force) { strlcpy(hc, THERMOSTAT_HC, sizeof(hc)); strlcat(hc, _int_to_char(s, thermostat->hc), sizeof(hc)); JsonObject dataThermostat = rootThermostat.createNestedObject(hc); + uint8_t model = ems_getThermostatModel(); // different logic depending on thermostat types - if (ems_getThermostatModel() == EMS_MODEL_EASY) { + if (model == EMS_DEVICE_FLAG_EASY) { if (thermostat->setpoint_roomTemp != EMS_VALUE_SHORT_NOTSET) - dataThermostat[THERMOSTAT_SELTEMP] = (double)thermostat->setpoint_roomTemp / 100; + dataThermostat[THERMOSTAT_SELTEMP] = (float)thermostat->setpoint_roomTemp / 100; if (thermostat->curr_roomTemp != EMS_VALUE_SHORT_NOTSET) - dataThermostat[THERMOSTAT_CURRTEMP] = (double)thermostat->curr_roomTemp / 100; - } else if ((ems_getThermostatModel() == EMS_MODEL_FR10) || (ems_getThermostatModel() == EMS_MODEL_FW100) - || (ems_getThermostatModel() == EMS_MODEL_FW120)) { + dataThermostat[THERMOSTAT_CURRTEMP] = (float)thermostat->curr_roomTemp / 100; + } else if (model == EMS_DEVICE_FLAG_JUNKERS) { if (thermostat->setpoint_roomTemp != EMS_VALUE_SHORT_NOTSET) - dataThermostat[THERMOSTAT_SELTEMP] = (double)thermostat->setpoint_roomTemp / 10; + dataThermostat[THERMOSTAT_SELTEMP] = (float)thermostat->setpoint_roomTemp / 10; if (thermostat->curr_roomTemp != EMS_VALUE_SHORT_NOTSET) - dataThermostat[THERMOSTAT_CURRTEMP] = (double)thermostat->curr_roomTemp / 10; + dataThermostat[THERMOSTAT_CURRTEMP] = (float)thermostat->curr_roomTemp / 10; } else { if (thermostat->setpoint_roomTemp != EMS_VALUE_SHORT_NOTSET) - dataThermostat[THERMOSTAT_SELTEMP] = (double)thermostat->setpoint_roomTemp / 2; + dataThermostat[THERMOSTAT_SELTEMP] = (float)thermostat->setpoint_roomTemp / 2; if (thermostat->curr_roomTemp != EMS_VALUE_SHORT_NOTSET) - dataThermostat[THERMOSTAT_CURRTEMP] = (double)thermostat->curr_roomTemp / 10; + dataThermostat[THERMOSTAT_CURRTEMP] = (float)thermostat->curr_roomTemp / 10; if (thermostat->daytemp != EMS_VALUE_INT_NOTSET) - dataThermostat[THERMOSTAT_DAYTEMP] = (double)thermostat->daytemp / 2; + dataThermostat[THERMOSTAT_DAYTEMP] = (float)thermostat->daytemp / 2; if (thermostat->nighttemp != EMS_VALUE_INT_NOTSET) - dataThermostat[THERMOSTAT_NIGHTTEMP] = (double)thermostat->nighttemp / 2; + dataThermostat[THERMOSTAT_NIGHTTEMP] = (float)thermostat->nighttemp / 2; if (thermostat->holidaytemp != EMS_VALUE_INT_NOTSET) - dataThermostat[THERMOSTAT_HOLIDAYTEMP] = (double)thermostat->holidaytemp / 2; + dataThermostat[THERMOSTAT_HOLIDAYTEMP] = (float)thermostat->holidaytemp / 2; if (thermostat->heatingtype != EMS_VALUE_INT_NOTSET) dataThermostat[THERMOSTAT_HEATINGTYPE] = thermostat->heatingtype; @@ -710,47 +711,31 @@ void publishValues(bool force) { dataThermostat[THERMOSTAT_CIRCUITCALCTEMP] = thermostat->circuitcalctemp; } - uint8_t thermoMode = _getThermostatMode(hc_v); // 0xFF=unknown, 0=low, 1=manual, 2=auto, 3=night, 4=day - - // Termostat Mode - if (thermoMode == 0) { + // Thermostat Mode + _EMS_THERMOSTAT_MODE thermoMode = _getThermostatMode(hc_v); + if (thermoMode == EMS_THERMOSTAT_MODE_OFF) { dataThermostat[THERMOSTAT_MODE] = "off"; - } else if (thermoMode == 1) { - dataThermostat[THERMOSTAT_MODE] = "heat"; - } else if (thermoMode == 2) { + } else if (thermoMode == EMS_THERMOSTAT_MODE_MANUAL) { + dataThermostat[THERMOSTAT_MODE] = "manual"; + } else if (thermoMode == EMS_THERMOSTAT_MODE_AUTO) { dataThermostat[THERMOSTAT_MODE] = "auto"; - } else if (thermoMode == 3) { - dataThermostat[THERMOSTAT_MODE] = "off"; // for night - } else if (thermoMode == 4) { - dataThermostat[THERMOSTAT_MODE] = "heat"; // for day - } else { - dataThermostat[THERMOSTAT_MODE] = "auto"; // default to auto so HA doesn't complain + } else if (thermoMode == EMS_THERMOSTAT_MODE_DAY) { + dataThermostat[THERMOSTAT_MODE] = "day"; + } else if (thermoMode == EMS_THERMOSTAT_MODE_NIGHT) { + dataThermostat[THERMOSTAT_MODE] = "night"; } } } data[0] = '\0'; // reset data for next package serializeJson(doc, data, sizeof(data)); - - // check for empty json - jsonSize = measureJson(doc); - if (jsonSize > 2) { - // calculate new CRC - crc.reset(); - for (uint8_t i = 0; i < (jsonSize - 1); i++) { - crc.update(data[i]); - } - fchecksum = crc.finalize(); - if ((previousThermostatPublishCRC != fchecksum) || force) { - previousThermostatPublishCRC = fchecksum; - myDebugLog("Publishing thermostat data via MQTT"); - myESP.mqttPublish(TOPIC_THERMOSTAT_DATA, data); - } - } + myDebugLog("Publishing thermostat data via MQTT"); + myESP.mqttPublish(TOPIC_THERMOSTAT_DATA, data); + ems_Device_remove_flags(EMS_DEVICE_UPDATE_FLAG_THERMOSTAT); // unset flag } // handle the thermostat values - if (ems_getMixingDeviceEnabled()) { + if (ems_getMixingDeviceEnabled() && (ems_Device_has_flags(EMS_DEVICE_UPDATE_FLAG_MIXING) || force)) { doc.clear(); JsonObject rootMixing = doc.to(); @@ -766,7 +751,7 @@ void publishValues(bool force) { JsonObject dataMixing = rootMixing.createNestedObject(hc); if (mixing->flowTemp != EMS_VALUE_SHORT_NOTSET) - dataMixing["flowTemp"] = (double)mixing->flowTemp / 10; + dataMixing["flowTemp"] = (float)mixing->flowTemp / 10; if (mixing->pumpMod != EMS_VALUE_INT_NOTSET) dataMixing["pumpMod"] = mixing->pumpMod; if (mixing->valveStatus != EMS_VALUE_INT_NOTSET) @@ -776,80 +761,52 @@ void publishValues(bool force) { data[0] = '\0'; // reset data for next package serializeJson(doc, data, sizeof(data)); - - // check for empty json - jsonSize = measureJson(doc); - if (jsonSize > 2) { - // calculate new CRC - crc.reset(); - for (uint8_t i = 0; i < (jsonSize - 1); i++) { - crc.update(data[i]); - } - fchecksum = crc.finalize(); - if ((previousMixingPublishCRC != fchecksum) || force) { - previousMixingPublishCRC = fchecksum; - myDebugLog("Publishing mixing device data via MQTT"); - myESP.mqttPublish(TOPIC_MIXING_DATA, data); - } - } + myDebugLog("Publishing mixing device data via MQTT"); + myESP.mqttPublish(TOPIC_MIXING_DATA, data); + ems_Device_remove_flags(EMS_DEVICE_UPDATE_FLAG_MIXING); // unset flag } // For SM10 and SM100 Solar Modules - if (ems_getSolarModuleEnabled()) { + if (ems_getSolarModuleEnabled() && (ems_Device_has_flags(EMS_DEVICE_UPDATE_FLAG_SOLAR) || force)) { // build new json object doc.clear(); JsonObject rootSM = doc.to(); if (EMS_SolarModule.collectorTemp != EMS_VALUE_SHORT_NOTSET) - rootSM[SM_COLLECTORTEMP] = (double)EMS_SolarModule.collectorTemp / 10; + rootSM[SM_COLLECTORTEMP] = (float)EMS_SolarModule.collectorTemp / 10; if (EMS_SolarModule.bottomTemp != EMS_VALUE_SHORT_NOTSET) - rootSM[SM_BOTTOMTEMP] = (double)EMS_SolarModule.bottomTemp / 10; + rootSM[SM_BOTTOMTEMP] = (float)EMS_SolarModule.bottomTemp / 10; if (EMS_SolarModule.pumpModulation != EMS_VALUE_INT_NOTSET) rootSM[SM_PUMPMODULATION] = EMS_SolarModule.pumpModulation; - if (EMS_SolarModule.pump != EMS_VALUE_INT_NOTSET) { + if (EMS_SolarModule.pump != EMS_VALUE_BOOL_NOTSET) { rootSM[SM_PUMP] = _bool_to_char(s, EMS_SolarModule.pump); } if (EMS_SolarModule.pumpWorkMin != EMS_VALUE_LONG_NOTSET) { - rootSM[SM_PUMPWORKMIN] = (double)EMS_SolarModule.pumpWorkMin; + rootSM[SM_PUMPWORKMIN] = (float)EMS_SolarModule.pumpWorkMin; } if (EMS_SolarModule.EnergyLastHour != EMS_VALUE_USHORT_NOTSET) - rootSM[SM_ENERGYLASTHOUR] = (double)EMS_SolarModule.EnergyLastHour / 10; + rootSM[SM_ENERGYLASTHOUR] = (float)EMS_SolarModule.EnergyLastHour / 10; if (EMS_SolarModule.EnergyToday != EMS_VALUE_USHORT_NOTSET) rootSM[SM_ENERGYTODAY] = EMS_SolarModule.EnergyToday; if (EMS_SolarModule.EnergyTotal != EMS_VALUE_USHORT_NOTSET) - rootSM[SM_ENERGYTOTAL] = (double)EMS_SolarModule.EnergyTotal / 10; + rootSM[SM_ENERGYTOTAL] = (float)EMS_SolarModule.EnergyTotal / 10; data[0] = '\0'; // reset data for next package serializeJson(doc, data, sizeof(data)); - - // check for empty json - jsonSize = measureJson(doc); - if (jsonSize > 2) { - // calculate new CRC - crc.reset(); - for (uint8_t i = 0; i < (jsonSize - 1); i++) { - crc.update(data[i]); - } - fchecksum = crc.finalize(); - if ((previousSMPublishCRC != fchecksum) || force) { - previousSMPublishCRC = fchecksum; - myDebugLog("Publishing SM data via MQTT"); - - // send values via MQTT - myESP.mqttPublish(TOPIC_SM_DATA, data); - } - } + myDebugLog("Publishing SM data via MQTT"); + myESP.mqttPublish(TOPIC_SM_DATA, data); + ems_Device_remove_flags(EMS_DEVICE_UPDATE_FLAG_SOLAR); // unset flag } // handle HeatPump - if (ems_getHeatPumpEnabled()) { + if (ems_getHeatPumpEnabled() && (ems_Device_has_flags(EMS_DEVICE_UPDATE_FLAG_HEATPUMP) || force)) { // build new json object doc.clear(); JsonObject rootSM = doc.to(); @@ -862,27 +819,15 @@ void publishValues(bool force) { data[0] = '\0'; // reset data for next package serializeJson(doc, data, sizeof(data)); - myDebugLog("Publishing HeatPump data via MQTT"); - - // send values via MQTT myESP.mqttPublish(TOPIC_HP_DATA, data); + ems_Device_remove_flags(EMS_DEVICE_UPDATE_FLAG_HEATPUMP); // unset flag } } -// publish external dallas sensor temperature values to MQTT -void do_publishSensorValues() { - if ((EMSESP_Settings.dallas_sensors) && (EMSESP_Settings.publish_time)) { - publishSensorValues(); - } -} - -// call PublishValues without forcing, so using CRC to see if we really need to publish +// call PublishValues without forcing void do_publishValues() { - // don't publish if we're not connected to the EMS bus - if ((ems_getBusConnected()) && myESP.isMQTTConnected() && EMSESP_Settings.publish_time) { - publishValues(true); // force publish - } + publishEMSValues(true); // force publish } // callback to light up the LED, called via Ticker every second @@ -898,15 +843,6 @@ void do_ledcheck() { } } -// Thermostat scan -void do_scanThermostat() { - if (ems_getBusConnected()) { - myDebug_P(PSTR("> Scanning thermostat message type #0x%02X..."), scanThermostat_count); - ems_doReadCommand(scanThermostat_count, EMS_Thermostat.device_id); - scanThermostat_count++; - } -} - // do a system health check every now and then to see if we all connections void do_systemCheck() { if (!ems_getBusConnected() && !myESP.getUseSerial()) { @@ -918,64 +854,13 @@ void do_systemCheck() { // only if we have a EMS connection void do_regularUpdates() { if (ems_getBusConnected() && !ems_getTxDisabled()) { - myDebugLog("Starting scheduled query from EMS devices"); + myDebugLog("Fetching data from EMS devices"); ems_getThermostatValues(); ems_getBoilerValues(); ems_getSolarModuleValues(); } } -// stop devices scan and restart all other timers -void stopDeviceScan() { - publishValuesTimer.attach(EMSESP_Settings.publish_time, do_publishValues); // post MQTT EMS values - publishSensorValuesTimer.attach(EMSESP_Settings.publish_time, do_publishSensorValues); // post MQTT sensor values - regularUpdatesTimer.attach(REGULARUPDATES_TIME, do_regularUpdates); // regular reads from the EMS - systemCheckTimer.attach(SYSTEMCHECK_TIME, do_systemCheck); // check if Boiler is online - scanThermostat_count = 0; - scanThermostat.detach(); -} - -// EMS device scan -void do_scanDevices() { - if (scanDevices_count == 0) { - // we're at the finish line - myDebug_P(PSTR("Finished the deep EMS device scan.")); - stopDeviceScan(); - ems_printDevices(); - ems_setLogging(EMS_SYS_LOGGING_NONE); - return; - } - - if (ems_getBusConnected()) { - ems_doReadCommand(EMS_TYPE_Version, scanDevices_count++); // ask for version - } -} - -// initiate a force scan by sending a version command to all type ids -void startDeviceScan() { - publishValuesTimer.detach(); - systemCheckTimer.detach(); - regularUpdatesTimer.detach(); - publishSensorValuesTimer.detach(); - scanDevices_count = 1; // starts at 1 - ems_clearDeviceList(); // empty the current list - ems_setLogging(EMS_SYS_LOGGING_NONE); - myDebug_P(PSTR("Starting a deep EMS device scan. This can take up to 2 minutes. Please wait...")); - scanThermostat.attach_ms(SCANDEVICES_TIME, do_scanDevices); -} - -// initiate a force scan by sending type read requests from 0 to FF to the thermostat -// used to analyze responses for debugging -void startThermostatScan(uint8_t start) { - ems_setLogging(EMS_SYS_LOGGING_THERMOSTAT); - publishValuesTimer.detach(); - systemCheckTimer.detach(); - regularUpdatesTimer.detach(); - scanThermostat_count = start; - myDebug_P(PSTR("Starting a deep message scan on thermostat")); - scanThermostat.attach(SCANTHERMOSTAT_TIME, do_scanThermostat); -} - // turn back on the hot water for the shower void _showerColdShotStop() { if (EMSESP_Shower.doingColdShot) { @@ -1008,7 +893,7 @@ void runUnitTest(uint8_t test_num) { } // callback for loading/saving settings to the file system (SPIFFS) -bool LoadSaveCallback(MYESP_FSACTION action, JsonObject settings) { +bool LoadSaveCallback(MYESP_FSACTION_t action, JsonObject settings) { if (action == MYESP_FSACTION_LOAD) { // check for valid json if (settings.isNull()) { @@ -1082,7 +967,7 @@ bool do_publishShowerData() { // callback for custom settings when showing Stored Settings with the 'set' command // wc is number of arguments after the 'set' command // returns true if the setting was recognized and changed and should be saved back to SPIFFs -bool SetListCallback(MYESP_FSACTION action, uint8_t wc, const char * setting, const char * value) { +bool SetListCallback(MYESP_FSACTION_t action, uint8_t wc, const char * setting, const char * value) { bool ok = false; if (action == MYESP_FSACTION_SET) { @@ -1200,7 +1085,11 @@ bool SetListCallback(MYESP_FSACTION action, uint8_t wc, const char * setting, co myDebug_P(PSTR(" listen_mode=%s"), EMSESP_Settings.listen_mode ? "on" : "off"); myDebug_P(PSTR(" shower_timer=%s"), EMSESP_Settings.shower_timer ? "on" : "off"); myDebug_P(PSTR(" shower_alert=%s"), EMSESP_Settings.shower_alert ? "on" : "off"); - myDebug_P(PSTR(" publish_time=%d"), EMSESP_Settings.publish_time); + if (EMSESP_Settings.publish_time) { + myDebug_P(PSTR(" publish_time=%d"), EMSESP_Settings.publish_time); + } else { + myDebug_P(PSTR(" publish_time=0 (always publish when data received)"), EMSESP_Settings.publish_time); + } } return ok; @@ -1244,9 +1133,9 @@ void _showCommands(uint8_t event) { // we set the logging here void TelnetCallback(uint8_t event) { if (event == TELNET_EVENT_CONNECT) { - ems_setLogging(EMS_SYS_LOGGING_DEFAULT, true); + ems_setLogging(EMS_SYS_LOGGING_DEFAULT); } else if (event == TELNET_EVENT_DISCONNECT) { - ems_setLogging(EMS_SYS_LOGGING_NONE, true); + ems_setLogging(EMS_SYS_LOGGING_NONE); } else if ((event == TELNET_EVENT_SHOWCMD) || (event == TELNET_EVENT_SHOWSET)) { _showCommands(event); } @@ -1266,7 +1155,7 @@ void TelnetCommandCallback(uint8_t wc, const char * commandLine) { if (strcmp(first_cmd, "publish") == 0) { do_publishValues(); - do_publishSensorValues(); + publishSensorValues(); do_publishShowerData(); ok = true; } @@ -1277,14 +1166,7 @@ void TelnetCommandCallback(uint8_t wc, const char * commandLine) { } if (strcmp(first_cmd, "devices") == 0) { - if (wc == 2) { - char * second_cmd = _readWord(); - if (strcmp(second_cmd, "all") == 0) { - ems_printAllDevices(); // verbose - } - } else { - ems_printDevices(); - } + ems_printDevices(); ok = true; } @@ -1296,22 +1178,19 @@ void TelnetCommandCallback(uint8_t wc, const char * commandLine) { if (strcmp(first_cmd, "autodetect") == 0) { if (wc == 2) { char * second_cmd = _readWord(); - if (strcmp(second_cmd, "deep") == 0) { - startDeviceScan(); - ok = true; - } else if (strcmp(second_cmd, "quick") == 0) { - ems_clearDeviceList(); - ems_doReadCommand(EMS_TYPE_UBADevices, EMS_Boiler.device_id); + if (strcmp(second_cmd, "scan") == 0) { + ems_scanDevices(); // known device scan ok = true; } } else { - ems_scanDevices(); // normal known device scan + ems_clearDeviceList(); + ems_doReadCommand(EMS_TYPE_UBADevices, EMS_Boiler.device_id); ok = true; } } // logging - if ((strcmp(first_cmd, "log") == 0) && (wc == 2)) { + if ((strcmp(first_cmd, "log") == 0) && (wc >= 2)) { char * second_cmd = _readWord(); if (strcmp(second_cmd, "v") == 0) { ems_setLogging(EMS_SYS_LOGGING_VERBOSE); @@ -1334,6 +1213,9 @@ void TelnetCommandCallback(uint8_t wc, const char * commandLine) { } else if (strcmp(second_cmd, "j") == 0) { ems_setLogging(EMS_SYS_LOGGING_JABBER); ok = true; + } else if ((strcmp(second_cmd, "w") == 0) && (wc == 3)) { + ems_setLogging(EMS_SYS_LOGGING_WATCH, _readHexNumber()); // get type_id + ok = true; } } @@ -1357,9 +1239,6 @@ void TelnetCommandCallback(uint8_t wc, const char * commandLine) { } else if (strcmp(second_cmd, "read") == 0) { ems_doReadCommand(_readHexNumber(), EMS_Thermostat.device_id); ok = true; - } else if (strcmp(second_cmd, "scan") == 0) { - startThermostatScan(_readIntNumber()); - ok = true; } } @@ -1464,11 +1343,11 @@ void MQTTCallback(unsigned int type, const char * topic, const char * message) { char topic_s[50]; char buffer[4]; for (uint8_t hc = 1; hc <= EMS_THERMOSTAT_MAXHC; hc++) { - strlcpy(topic_s, TOPIC_THERMOSTAT_CMD_TEMP, sizeof(topic_s)); + strlcpy(topic_s, TOPIC_THERMOSTAT_CMD_TEMP_HA, sizeof(topic_s)); strlcat(topic_s, itoa(hc, buffer, 10), sizeof(topic_s)); myESP.mqttSubscribe(topic_s); - strlcpy(topic_s, TOPIC_THERMOSTAT_CMD_MODE, sizeof(topic_s)); + strlcpy(topic_s, TOPIC_THERMOSTAT_CMD_MODE_HA, sizeof(topic_s)); strlcat(topic_s, itoa(hc, buffer, 10), sizeof(topic_s)); myESP.mqttSubscribe(topic_s); } @@ -1481,8 +1360,9 @@ void MQTTCallback(unsigned int type, const char * topic, const char * message) { // this is used for example for comfort, flowtemp myESP.mqttSubscribe(TOPIC_BOILER_CMD); - // these two need to be unqiue topics + // these three need to be unqiue topics myESP.mqttSubscribe(TOPIC_BOILER_CMD_WWACTIVATED); + myESP.mqttSubscribe(TOPIC_BOILER_CMD_WWONETIME); myESP.mqttSubscribe(TOPIC_BOILER_CMD_WWTEMP); // generic incoming MQTT command for EMS-ESP @@ -1596,11 +1476,21 @@ void MQTTCallback(unsigned int type, const char * topic, const char * message) { return; } + // wwOneTime + if (strcmp(topic, TOPIC_BOILER_CMD_WWONETIME) == 0) { + if (message[0] == '1' || strcmp(message, "on") == 0) { + ems_setWarmWaterOnetime(true); + } else if (message[0] == '0' || strcmp(message, "off") == 0) { + ems_setWarmWaterOnetime(false); + } + return; + } + // boiler wwtemp changes if (strcmp(topic, TOPIC_BOILER_CMD_WWTEMP) == 0) { uint8_t t = atoi((char *)message); ems_setWarmWaterTemp(t); - publishValues(true); + publishEMSValues(true); return; } @@ -1610,7 +1500,7 @@ void MQTTCallback(unsigned int type, const char * topic, const char * message) { if (hc) { float f = strtof((char *)message, 0); ems_setThermostatTemp(f, hc); - publishValues(true); // publish back immediately + publishEMSValues(true); // publish back immediately return; } @@ -1643,7 +1533,7 @@ void MQTTCallback(unsigned int type, const char * topic, const char * message) { if (hc) { float f = doc["data"]; ems_setThermostatTemp(f, hc); - publishValues(true); // publish back immediately + publishEMSValues(true); // publish back immediately return; } @@ -1687,7 +1577,6 @@ void MQTTCallback(unsigned int type, const char * topic, const char * message) { } } - // Init callback, which is used to set functions and call methods after a wifi connection has been established void WIFICallback() { // This is where we enable the UART service to scan the incoming serial Tx/Rx bus signals @@ -1716,29 +1605,38 @@ void WebCallback(JsonObject root) { } } else { emsbus["ok"] = false; - emsbus["msg"] = "EMS Bus is not connected. Check event logs for errors."; + emsbus["msg"] = "EMS Bus is not connected."; } } // send over EMS devices JsonArray list = emsbus.createNestedArray("devices"); + char buffer[50]; + + for (std::list<_Detected_Device>::iterator it = Devices.begin(); it != Devices.end(); ++it) { + JsonObject item = list.createNestedObject(); + + (void)ems_getDeviceTypeDescription((it)->device_id, buffer); + item["type"] = buffer; + + if ((it)->known == true) { + item["model"] = (it)->device_desc_p; + } else { + item["model"] = EMS_MODELTYPE_UNKNOWN_STRING; + } - for (std::list<_Generic_Device>::iterator it = Devices.begin(); it != Devices.end(); ++it) { - JsonObject item = list.createNestedObject(); - item["type"] = (it)->model_type; - item["model"] = (it)->model_string; item["version"] = (it)->version; item["productid"] = (it)->product_id; - char buffer[10]; + char tmp_hex[10]; // copy of my _hextoa() function from ems.cpp, to convert device_id into a 0xNN hex value string - char * p = buffer; + char * p = tmp_hex; byte nib1 = ((it)->device_id >> 4) & 0x0F; byte nib2 = ((it)->device_id >> 0) & 0x0F; *p++ = nib1 < 0xA ? '0' + nib1 : 'A' + nib1 - 0xA; *p++ = nib2 < 0xA ? '0' + nib2 : 'A' + nib2 - 0xA; *p = '\0'; // null terminate just in case - item["deviceid"] = buffer; + item["deviceid"] = tmp_hex; } // send over Thermostat data @@ -1748,41 +1646,37 @@ void WebCallback(JsonObject root) { thermostat["ok"] = true; char buffer[200]; - thermostat["tm"] = ems_getThermostatDescription(buffer, true); + thermostat["tm"] = ems_getDeviceDescription(EMS_DEVICE_TYPE_THERMOSTAT, buffer, true); uint8_t hc_num = EMS_THERMOSTAT_DEFAULTHC; // default to HC1 + uint8_t model = ems_getThermostatModel(); // Render Current & Setpoint Room Temperature - if (ems_getThermostatModel() == EMS_MODEL_EASY) { + if (model == EMS_DEVICE_FLAG_EASY) { if (EMS_Thermostat.hc[hc_num - 1].setpoint_roomTemp != EMS_VALUE_SHORT_NOTSET) - thermostat["ts"] = (double)EMS_Thermostat.hc[hc_num - 1].setpoint_roomTemp / 100; + thermostat["ts"] = (float)EMS_Thermostat.hc[hc_num - 1].setpoint_roomTemp / 100; if (EMS_Thermostat.hc[hc_num - 1].curr_roomTemp != EMS_VALUE_SHORT_NOTSET) - thermostat["tc"] = (double)EMS_Thermostat.hc[hc_num - 1].curr_roomTemp / 100; - } else if ((ems_getThermostatModel() == EMS_MODEL_FR10) || (ems_getThermostatModel() == EMS_MODEL_FW100) - || (ems_getThermostatModel() == EMS_MODEL_FW120)) { + thermostat["tc"] = (float)EMS_Thermostat.hc[hc_num - 1].curr_roomTemp / 100; + } else if (model == EMS_DEVICE_FLAG_JUNKERS) { if (EMS_Thermostat.hc[hc_num - 1].setpoint_roomTemp != EMS_VALUE_SHORT_NOTSET) - thermostat["ts"] = (double)EMS_Thermostat.hc[hc_num - 1].setpoint_roomTemp / 10; + thermostat["ts"] = (float)EMS_Thermostat.hc[hc_num - 1].setpoint_roomTemp / 10; if (EMS_Thermostat.hc[hc_num - 1].curr_roomTemp != EMS_VALUE_SHORT_NOTSET) - thermostat["tc"] = (double)EMS_Thermostat.hc[hc_num - 1].curr_roomTemp / 10; + thermostat["tc"] = (float)EMS_Thermostat.hc[hc_num - 1].curr_roomTemp / 10; } else { if (EMS_Thermostat.hc[hc_num - 1].setpoint_roomTemp != EMS_VALUE_SHORT_NOTSET) - thermostat["ts"] = (double)EMS_Thermostat.hc[hc_num - 1].setpoint_roomTemp / 2; + thermostat["ts"] = (float)EMS_Thermostat.hc[hc_num - 1].setpoint_roomTemp / 2; if (EMS_Thermostat.hc[hc_num - 1].curr_roomTemp != EMS_VALUE_SHORT_NOTSET) - thermostat["tc"] = (double)EMS_Thermostat.hc[hc_num - 1].curr_roomTemp / 10; + thermostat["tc"] = (float)EMS_Thermostat.hc[hc_num - 1].curr_roomTemp / 10; } - // Render Termostat Mode, if we have a mode - uint8_t thermoMode = _getThermostatMode(hc_num); // 0xFF=unknown, 0=off, 1=manual, 2=auto, 3=night, 4=day - if (thermoMode == 0) { + // Render Thermostat Mode + _EMS_THERMOSTAT_MODE thermoMode = _getThermostatMode(hc_num); + if (thermoMode == EMS_THERMOSTAT_MODE_OFF) { thermostat["tmode"] = "off"; - } else if (thermoMode == 1) { - thermostat["tmode"] = "heat"; - } else if (thermoMode == 2) { + } else if (thermoMode == EMS_THERMOSTAT_MODE_MANUAL) { + thermostat["tmode"] = "manual"; + } else if (thermoMode == EMS_THERMOSTAT_MODE_AUTO) { thermostat["tmode"] = "auto"; - } else if (thermoMode == 3) { - thermostat["tmode"] = "night"; - } else if (thermoMode == 4) { - thermostat["tmode"] = "day"; } } else { thermostat["ok"] = false; @@ -1793,7 +1687,7 @@ void WebCallback(JsonObject root) { boiler["ok"] = true; char buffer[200]; - boiler["bm"] = ems_getBoilerDescription(buffer, true); + boiler["bm"] = ems_getDeviceDescription(EMS_DEVICE_TYPE_BOILER, buffer, true); boiler["b1"] = (EMS_Boiler.tapwaterActive ? "running" : "off"); boiler["b2"] = (EMS_Boiler.heatingActive ? "active" : "off"); @@ -1805,10 +1699,10 @@ void WebCallback(JsonObject root) { boiler["b4"] = EMS_Boiler.curFlowTemp / 10; if (EMS_Boiler.boilTemp != EMS_VALUE_USHORT_NOTSET) - boiler["b5"] = (double)EMS_Boiler.boilTemp / 10; + boiler["b5"] = (float)EMS_Boiler.boilTemp / 10; if (EMS_Boiler.retTemp != EMS_VALUE_USHORT_NOTSET) - boiler["b6"] = (double)EMS_Boiler.retTemp / 10; + boiler["b6"] = (float)EMS_Boiler.retTemp / 10; } else { boiler["ok"] = false; @@ -1820,30 +1714,30 @@ void WebCallback(JsonObject root) { sm["ok"] = true; char buffer[200]; - sm["sm"] = ems_getSolarModuleDescription(buffer, true); + sm["sm"] = ems_getDeviceDescription(EMS_DEVICE_TYPE_SOLAR, buffer, true); if (EMS_SolarModule.collectorTemp != EMS_VALUE_SHORT_NOTSET) - sm["sm1"] = (double)EMS_SolarModule.collectorTemp / 10; // Collector temperature oC + sm["sm1"] = (float)EMS_SolarModule.collectorTemp / 10; // Collector temperature oC if (EMS_SolarModule.bottomTemp != EMS_VALUE_SHORT_NOTSET) - sm["sm2"] = (double)EMS_SolarModule.bottomTemp / 10; // Bottom temperature oC + sm["sm2"] = (float)EMS_SolarModule.bottomTemp / 10; // Bottom temperature oC if (EMS_SolarModule.pumpModulation != EMS_VALUE_INT_NOTSET) sm["sm3"] = EMS_SolarModule.pumpModulation; // Pump modulation % - if (EMS_SolarModule.pump != EMS_VALUE_INT_NOTSET) { + if (EMS_SolarModule.pump != EMS_VALUE_BOOL_NOTSET) { char s[10]; sm["sm4"] = _bool_to_char(s, EMS_SolarModule.pump); // Pump active on/off } if (EMS_SolarModule.EnergyLastHour != EMS_VALUE_USHORT_NOTSET) - sm["sm5"] = (double)EMS_SolarModule.EnergyLastHour / 10; // Energy last hour Wh + sm["sm5"] = (float)EMS_SolarModule.EnergyLastHour / 10; // Energy last hour Wh if (EMS_SolarModule.EnergyToday != EMS_VALUE_USHORT_NOTSET) // Energy today Wh sm["sm6"] = EMS_SolarModule.EnergyToday; if (EMS_SolarModule.EnergyTotal != EMS_VALUE_USHORT_NOTSET) // Energy total KWh - sm["sm7"] = (double)EMS_SolarModule.EnergyTotal / 10; + sm["sm7"] = (float)EMS_SolarModule.EnergyTotal / 10; } else { sm["ok"] = false; } @@ -1853,7 +1747,7 @@ void WebCallback(JsonObject root) { if (ems_getHeatPumpEnabled()) { hp["ok"] = true; char buffer[200]; - hp["hm"] = ems_getHeatPumpDescription(buffer, true); + hp["hm"] = ems_getDeviceDescription(EMS_DEVICE_TYPE_HEATPUMP, buffer, true); if (EMS_HeatPump.HPModulation != EMS_VALUE_INT_NOTSET) hp["hp1"] = EMS_HeatPump.HPModulation; // Pump modulation % @@ -1877,7 +1771,6 @@ void initEMSESP() { EMSESP_Settings.led = true; // LED is on by default EMSESP_Settings.listen_mode = false; EMSESP_Settings.publish_time = DEFAULT_PUBLISHTIME; - EMSESP_Settings.timestamp = millis(); EMSESP_Settings.dallas_sensors = 0; EMSESP_Settings.led_gpio = EMSESP_LED_GPIO; EMSESP_Settings.dallas_gpio = EMSESP_DALLAS_GPIO; @@ -1897,6 +1790,7 @@ void initEMSESP() { * Shower Logic */ void showerCheck() { + uint32_t time_now = millis(); // if already in cold mode, ignore all this logic until we're out of the cold blast if (!EMSESP_Shower.doingColdShot) { // is the hot water running? @@ -1904,7 +1798,7 @@ void showerCheck() { // if heater was previously off, start the timer if (EMSESP_Shower.timerStart == 0) { // hot water just started... - EMSESP_Shower.timerStart = EMSESP_Settings.timestamp; + EMSESP_Shower.timerStart = time_now; EMSESP_Shower.timerPause = 0; // remove any last pauses EMSESP_Shower.doingColdShot = false; EMSESP_Shower.duration = 0; @@ -1912,13 +1806,12 @@ void showerCheck() { } else { // hot water has been on for a while // first check to see if hot water has been on long enough to be recognized as a Shower/Bath - if (!EMSESP_Shower.showerOn && (EMSESP_Settings.timestamp - EMSESP_Shower.timerStart) > SHOWER_MIN_DURATION) { + if (!EMSESP_Shower.showerOn && (time_now - EMSESP_Shower.timerStart) > SHOWER_MIN_DURATION) { EMSESP_Shower.showerOn = true; myDebugLog("[Shower] hot water still running, starting shower timer"); } // check if the shower has been on too long - else if ((((EMSESP_Settings.timestamp - EMSESP_Shower.timerStart) > SHOWER_MAX_DURATION) && !EMSESP_Shower.doingColdShot) - && EMSESP_Settings.shower_alert) { + else if ((((time_now - EMSESP_Shower.timerStart) > SHOWER_MAX_DURATION) && !EMSESP_Shower.doingColdShot) && EMSESP_Settings.shower_alert) { myDebugLog("[Shower] exceeded max shower time"); _showerColdShotStart(); } @@ -1926,11 +1819,11 @@ void showerCheck() { } else { // hot water is off // if it just turned off, record the time as it could be a short pause if ((EMSESP_Shower.timerStart) && (EMSESP_Shower.timerPause == 0)) { - EMSESP_Shower.timerPause = EMSESP_Settings.timestamp; + EMSESP_Shower.timerPause = time_now; } // if shower has been off for longer than the wait time - if ((EMSESP_Shower.timerPause) && ((EMSESP_Settings.timestamp - EMSESP_Shower.timerPause) > SHOWER_PAUSE_TIME)) { + if ((EMSESP_Shower.timerPause) && ((time_now - EMSESP_Shower.timerPause) > SHOWER_PAUSE_TIME)) { // it is over the wait period, so assume that the shower has finished and calculate the total time and publish // because its unsigned long, can't have negative so check if length is less than OFFSET_TIME if ((EMSESP_Shower.timerPause - EMSESP_Shower.timerStart) > SHOWER_OFFSET_TIME) { @@ -1976,7 +1869,7 @@ void setup() { myESP.setSettings(LoadSaveCallback, SetListCallback, false); // default is Serial off myESP.setWeb(WebCallback); // web custom settings myESP.setOTA(OTACallback_pre, OTACallback_post); // OTA callback which is called when OTA is starting and stopping - myESP.begin(APP_HOSTNAME, APP_NAME, APP_VERSION, APP_URL, APP_UPDATEURL); + myESP.begin(APP_HOSTNAME, APP_NAME, APP_VERSION, APP_URL, APP_URL_API); // at this point we have all the settings from our internall SPIFFS config file // fire up the UART now @@ -2000,9 +1893,10 @@ void setup() { } // set timers for MQTT publish + // only if publish_time is not 0 (automatic mode) if (EMSESP_Settings.publish_time) { - publishValuesTimer.attach(EMSESP_Settings.publish_time, do_publishValues); // post MQTT EMS values - publishSensorValuesTimer.attach(EMSESP_Settings.publish_time, do_publishSensorValues); // post MQTT dallas sensor values + publishValuesTimer.attach(EMSESP_Settings.publish_time, do_publishValues); // post MQTT EMS values + publishSensorValuesTimer.attach(EMSESP_Settings.publish_time, publishSensorValues); // post MQTT dallas sensor values } // set pin for LED @@ -2022,31 +1916,26 @@ void setup() { // Main loop // void loop() { - EMSESP_Settings.timestamp = millis(); + myESP.loop(); // handle telnet, mqtt, wifi etc - // the main loop - myESP.loop(); - - // check Dallas sensors, using same schedule as publish_time (default 2 mins) + // check Dallas sensors, using same schedule as publish_time (default 2 mins in DS18_READ_INTERVAL) // these values are published to MQTT separately via the timer publishSensorValuesTimer if (EMSESP_Settings.dallas_sensors) { ds18.loop(); } - // publish all the values to MQTT, only if the values have changed - // although we don't want to publish when doing a deep scan of the thermostat - if (ems_getEmsRefreshed() && (scanThermostat_count == 0)) { - publishValues(false); - do_publishSensorValues(); - ems_setEmsRefreshed(false); // reset + // publish EMS data to MQTT + // because of the force=false argument, it will see if there is anything received that must be published + publishEMSValues(false); + + // if we have an EMS connect go and fetch some data and MQTT publish it + if (_need_first_publish) { + publishSensorValues(); + _need_first_publish = false; // reset flag } // do shower logic, if enabled if (EMSESP_Settings.shower_timer) { showerCheck(); } - - if (EMSESP_DELAY) { - delay(EMSESP_DELAY); // some time to WiFi and everything else to catch up, and prevent overheating - } } diff --git a/src/ems.cpp b/src/ems.cpp index cd3cebf24..fe9416bab 100644 --- a/src/ems.cpp +++ b/src/ems.cpp @@ -12,194 +12,19 @@ #include "ems_utils.h" #include "emsuart.h" #include // https://github.com/rlogiacco/CircularBuffer +#include #ifdef TESTS #include "test_data.h" uint8_t _TEST_DATA_max = ArraySize(TEST_DATA); #endif -_EMS_Sys_Status EMS_Sys_Status; // EMS Status +_EMS_Sys_Status EMS_Sys_Status; // EMS Status +CircularBuffer<_EMS_TxTelegram, EMS_TX_TELEGRAM_QUEUE_MAX> EMS_TxQueue; // FIFO queue for Tx send buffer +std::list<_Detected_Device> Devices; // for storing all detected EMS devices -CircularBuffer<_EMS_TxTelegram, EMS_TX_TELEGRAM_QUEUE_MAX> EMS_TxQueue; // FIFO queue for Tx send buffer - -// for storing all detected EMS devices -std::list<_Generic_Device> Devices; - -// macros used in the _process* functions -#define _toByte(i) (EMS_RxTelegram->data[i]) -#define _toShort(i) ((EMS_RxTelegram->data[i] << 8) + EMS_RxTelegram->data[i + 1]) -#define _toLong(i) ((EMS_RxTelegram->data[i] << 16) + (EMS_RxTelegram->data[i + 1] << 8) + (EMS_RxTelegram->data[i + 2])) -#define _bitRead(i, bit) (((EMS_RxTelegram->data[i]) >> (bit)) & 0x01) - -// -// process callbacks per type -// - -// generic -void _process_Version(_EMS_RxTelegram * EMS_RxTelegram); -void _process_UBADevices(_EMS_RxTelegram * EMS_RxTelegram); - -// EMS master/Boiler devices -void _process_UBAMonitorFast(_EMS_RxTelegram * EMS_RxTelegram); -void _process_UBAMonitorSlow(_EMS_RxTelegram * EMS_RxTelegram); -void _process_UBAMonitorWWMessage(_EMS_RxTelegram * EMS_RxTelegram); -void _process_UBAParameterWW(_EMS_RxTelegram * EMS_RxTelegram); -void _process_UBATotalUptimeMessage(_EMS_RxTelegram * EMS_RxTelegram); -void _process_UBAParametersMessage(_EMS_RxTelegram * EMS_RxTelegram); -void _process_SetPoints(_EMS_RxTelegram * EMS_RxTelegram); - -// SM10 -void _process_SM10Monitor(_EMS_RxTelegram * EMS_RxTelegram); - -// SM100 -void _process_SM100Monitor(_EMS_RxTelegram * EMS_RxTelegram); -void _process_SM100Status(_EMS_RxTelegram * EMS_RxTelegram); -void _process_SM100Status2(_EMS_RxTelegram * EMS_RxTelegram); -void _process_SM100Energy(_EMS_RxTelegram * EMS_RxTelegram); - -// ISM1 -void _process_ISM1StatusMessage(_EMS_RxTelegram * EMS_RxTelegram); -void _process_ISM1Set(_EMS_RxTelegram * EMS_RxTelegram); - -// HeatPump HP -void _process_HPMonitor1(_EMS_RxTelegram * EMS_RxTelegram); -void _process_HPMonitor2(_EMS_RxTelegram * EMS_RxTelegram); - -// Common for most thermostats -void _process_RCTime(_EMS_RxTelegram * EMS_RxTelegram); -void _process_RCOutdoorTempMessage(_EMS_RxTelegram * EMS_RxTelegram); - -// RC10 -void _process_RC10Set(_EMS_RxTelegram * EMS_RxTelegram); -void _process_RC10StatusMessage(_EMS_RxTelegram * EMS_RxTelegram); - -// RC20 -void _process_RC20Set(_EMS_RxTelegram * EMS_RxTelegram); -void _process_RC20StatusMessage(_EMS_RxTelegram * EMS_RxTelegram); - -// RC30 -void _process_RC30Set(_EMS_RxTelegram * EMS_RxTelegram); -void _process_RC30StatusMessage(_EMS_RxTelegram * EMS_RxTelegram); - -// RC35 -void _process_RC35Set(_EMS_RxTelegram * EMS_RxTelegram); -void _process_RC35StatusMessage(_EMS_RxTelegram * EMS_RxTelegram); - -// Easy type devices like C100 -void _process_EasyStatusMessage(_EMS_RxTelegram * EMS_RxTelegram); - -// RC1010, RC300, RC310 -void _process_RCPLUSStatusMessage(_EMS_RxTelegram * EMS_RxTelegram); -void _process_RCPLUSSetMessage(_EMS_RxTelegram * EMS_RxTelegram); -void _process_RCPLUSStatusMode(_EMS_RxTelegram * EMS_RxTelegram); - -// Junkers FR10 & FW100 -void _process_JunkersStatusMessage(_EMS_RxTelegram * EMS_RxTelegram); - -// Mixers MM100 -void _process_MMPLUSStatusMessage(_EMS_RxTelegram * EMS_RxTelegram); - -/** - * Recognized EMS types and the functions they call to process the telegrams - * Format: MODEL ID, TYPE ID, Description, function, emsplus - */ -const _EMS_Type EMS_Types[] = { - - // common - {EMS_MODEL_ALL, EMS_TYPE_Version, "Version", _process_Version}, - {EMS_MODEL_ALL, EMS_TYPE_UBADevices, "UBADevices", _process_UBADevices}, - - // Boiler commands - {EMS_MODEL_UBA, EMS_TYPE_UBAMonitorFast, "UBAMonitorFast", _process_UBAMonitorFast}, - {EMS_MODEL_UBA, EMS_TYPE_UBAMonitorSlow, "UBAMonitorSlow", _process_UBAMonitorSlow}, - {EMS_MODEL_UBA, EMS_TYPE_UBAMonitorWWMessage, "UBAMonitorWWMessage", _process_UBAMonitorWWMessage}, - {EMS_MODEL_UBA, EMS_TYPE_UBAParameterWW, "UBAParameterWW", _process_UBAParameterWW}, - {EMS_MODEL_UBA, EMS_TYPE_UBATotalUptimeMessage, "UBATotalUptimeMessage", _process_UBATotalUptimeMessage}, - {EMS_MODEL_UBA, EMS_TYPE_UBAMaintenanceSettingsMessage, "UBAMaintenanceSettingsMessage", nullptr}, - {EMS_MODEL_UBA, EMS_TYPE_UBAParametersMessage, "UBAParametersMessage", _process_UBAParametersMessage}, - {EMS_MODEL_UBA, EMS_TYPE_UBASetPoints, "UBASetPoints", _process_SetPoints}, - - // SM devices - {EMS_MODEL_SM, EMS_TYPE_SM10Monitor, "SM10Monitor", _process_SM10Monitor}, - {EMS_MODEL_SM, EMS_TYPE_SM100Monitor, "SM100Monitor", _process_SM100Monitor}, - {EMS_MODEL_SM, EMS_TYPE_SM100Status, "SM100Status", _process_SM100Status}, - {EMS_MODEL_SM, EMS_TYPE_SM100Status2, "SM100Status2", _process_SM100Status2}, - {EMS_MODEL_SM, EMS_TYPE_SM100Energy, "SM100Energy", _process_SM100Energy}, - {EMS_MODEL_SM, EMS_TYPE_ISM1StatusMessage, "ISM1StatusMessage", _process_ISM1StatusMessage}, - {EMS_MODEL_SM, EMS_TYPE_ISM1Set, "ISM1Set", _process_ISM1Set}, - - // heatpunps - {EMS_MODEL_HP, EMS_TYPE_HPMonitor1, "HeatPumpMonitor1", _process_HPMonitor1}, - {EMS_MODEL_HP, EMS_TYPE_HPMonitor2, "HeatPumpMonitor2", _process_HPMonitor2}, - - // RC10 - {EMS_MODEL_RC10, EMS_TYPE_RCTime, "RCTime", _process_RCTime}, - {EMS_MODEL_RC10, EMS_TYPE_RC10Set, "RC10Set", _process_RC10Set}, - {EMS_MODEL_RC10, EMS_TYPE_RC10StatusMessage, "RC10StatusMessage", _process_RC10StatusMessage}, - - // RC20 and RC20F - {EMS_MODEL_RC20, EMS_TYPE_RCOutdoorTempMessage, "RCOutdoorTempMessage", _process_RCOutdoorTempMessage}, - {EMS_MODEL_RC20, EMS_TYPE_RCTime, "RCTime", _process_RCTime}, - {EMS_MODEL_RC20, EMS_TYPE_RC20Set, "RC20Set", _process_RC20Set}, - {EMS_MODEL_RC20, EMS_TYPE_RC20StatusMessage, "RC20StatusMessage", _process_RC20StatusMessage}, - - {EMS_MODEL_RC20F, EMS_TYPE_RCOutdoorTempMessage, "RCOutdoorTempMessage", _process_RCOutdoorTempMessage}, - {EMS_MODEL_RC20F, EMS_TYPE_RCTime, "RCTime", _process_RCTime}, - {EMS_MODEL_RC20F, EMS_TYPE_RC20Set, "RC20Set", _process_RC20Set}, - {EMS_MODEL_RC20F, EMS_TYPE_RC20StatusMessage, "RC20StatusMessage", _process_RC20StatusMessage}, - - // RC30 - {EMS_MODEL_RC30, EMS_TYPE_RCOutdoorTempMessage, "RCOutdoorTempMessage", _process_RCOutdoorTempMessage}, - {EMS_MODEL_RC30, EMS_TYPE_RCTime, "RCTime", _process_RCTime}, - {EMS_MODEL_RC30, EMS_TYPE_RC30Set, "RC30Set", _process_RC30Set}, - {EMS_MODEL_RC30, EMS_TYPE_RC30StatusMessage, "RC30StatusMessage", _process_RC30StatusMessage}, - - // RC35 - {EMS_MODEL_RC35, EMS_TYPE_RCOutdoorTempMessage, "RCOutdoorTempMessage", _process_RCOutdoorTempMessage}, - {EMS_MODEL_RC35, EMS_TYPE_RCTime, "RCTime", _process_RCTime}, - {EMS_MODEL_RC35, EMS_TYPE_RC35Set_HC1, "RC35Set_HC1", _process_RC35Set}, - {EMS_MODEL_RC35, EMS_TYPE_RC35StatusMessage_HC1, "RC35StatusMessage_HC1", _process_RC35StatusMessage}, - {EMS_MODEL_RC35, EMS_TYPE_RC35Set_HC2, "RC35Set_HC2", _process_RC35Set}, - {EMS_MODEL_RC35, EMS_TYPE_RC35StatusMessage_HC2, "RC35StatusMessage_HC2", _process_RC35StatusMessage}, - {EMS_MODEL_RC35, EMS_TYPE_RC35Set_HC3, "RC35Set_HC2", _process_RC35Set}, - {EMS_MODEL_RC35, EMS_TYPE_RC35StatusMessage_HC3, "RC35StatusMessage_HC3", _process_RC35StatusMessage}, - {EMS_MODEL_RC35, EMS_TYPE_RC35Set_HC4, "RC35Set_HC4", _process_RC35Set}, - {EMS_MODEL_RC35, EMS_TYPE_RC35StatusMessage_HC4, "RC35StatusMessage_HC4", _process_RC35StatusMessage}, - - // ES73 - {EMS_MODEL_ES73, EMS_TYPE_RCOutdoorTempMessage, "RCOutdoorTempMessage", _process_RCOutdoorTempMessage}, - {EMS_MODEL_ES73, EMS_TYPE_RCTime, "RCTime", _process_RCTime}, - {EMS_MODEL_ES73, EMS_TYPE_RC35Set_HC1, "RC35Set", _process_RC35Set}, - {EMS_MODEL_ES73, EMS_TYPE_RC35StatusMessage_HC1, "RC35StatusMessage", _process_RC35StatusMessage}, - - // Easy - {EMS_MODEL_EASY, EMS_TYPE_EasyStatusMessage, "EasyStatusMessage", _process_EasyStatusMessage}, - - // Nefit 1010, RC300, RC310 (EMS Plus) - {EMS_MODEL_ALL, EMS_TYPE_RCPLUSStatusMessage_HC1, "RCPLUSStatusMessage_HC1", _process_RCPLUSStatusMessage}, - {EMS_MODEL_ALL, EMS_TYPE_RCPLUSStatusMessage_HC2, "RCPLUSStatusMessage_HC2", _process_RCPLUSStatusMessage}, - {EMS_MODEL_ALL, EMS_TYPE_RCPLUSStatusMessage_HC3, "RCPLUSStatusMessage_HC3", _process_RCPLUSStatusMessage}, - {EMS_MODEL_ALL, EMS_TYPE_RCPLUSStatusMessage_HC4, "RCPLUSStatusMessage_HC4", _process_RCPLUSStatusMessage}, - {EMS_MODEL_ALL, EMS_TYPE_RCPLUSSet, "RCPLUSSetMessage", _process_RCPLUSSetMessage}, - {EMS_MODEL_ALL, EMS_TYPE_RCPLUSStatusMode, "RCPLUSStatusMode", _process_RCPLUSStatusMode}, - - // Junkers FR10 - {EMS_MODEL_ALL, EMS_TYPE_JunkersStatusMessage, "JunkersStatusMessage", _process_JunkersStatusMessage}, - - // Mixing devices - {EMS_MODEL_MM100, EMS_TYPE_MMPLUSStatusMessage_HC1, "MMPLUSStatusMessage_HC1", _process_MMPLUSStatusMessage}, - {EMS_MODEL_MM100, EMS_TYPE_MMPLUSStatusMessage_HC2, "MMPLUSStatusMessage_HC2", _process_MMPLUSStatusMessage}, - -}; - -// calculate sizes of arrays at compile -uint8_t _EMS_Types_max = ArraySize(EMS_Types); // number of defined types -uint8_t _Boiler_Devices_max = ArraySize(Boiler_Devices); // number of boiler models -uint8_t _SolarModule_Devices_max = ArraySize(SolarModule_Devices); // number of solar module types -uint8_t _Other_Devices_max = ArraySize(Other_Devices); // number of other ems devices -uint8_t _Thermostat_Devices_max = ArraySize(Thermostat_Devices); // number of defined thermostat types -uint8_t _HeatPump_Devices_max = ArraySize(HeatPump_Devices); // number of defined heatpump types -uint8_t _Mixing_Devices_max = ArraySize(Mixing_Devices); // number of mixing device types +uint8_t _EMS_Devices_max = ArraySize(EMS_Devices); +uint8_t _EMS_Devices_Types_max = ArraySize(EMS_Devices_Types); // these structs contain the data we store from the specific EMS devices _EMS_Boiler EMS_Boiler; // for boiler @@ -207,7 +32,6 @@ _EMS_Thermostat EMS_Thermostat; // for thermostat _EMS_SolarModule EMS_SolarModule; // for solar modules _EMS_HeatPump EMS_HeatPump; // for heatpumps _EMS_Mixing EMS_Mixing; // for mixing devices -_EMS_Other EMS_Other; // for other known EMS devices // CRC lookup table with poly 12 for faster checking const uint8_t ems_crc_table[] = {0x00, 0x02, 0x04, 0x06, 0x08, 0x0A, 0x0C, 0x0E, 0x10, 0x12, 0x14, 0x16, 0x18, 0x1A, 0x1C, 0x1E, 0x20, 0x22, 0x24, 0x26, @@ -228,26 +52,45 @@ const uint8_t TX_WRITE_TIMEOUT_COUNT = 2; // 3 retries before timeout const uint32_t EMS_BUS_TIMEOUT = 15000; // timeout in ms before recognizing the ems bus is offline (15 seconds) const uint32_t EMS_POLL_TIMEOUT = 5000000; // timeout in microseconds before recognizing the ems bus is offline (5 seconds) +/* + * Add one or more flags to the current flags. + */ +void ems_Device_add_flags(unsigned int flags) { + EMS_Sys_Status.emsRefreshedFlags |= flags; +} +/* + * Check if the current flags include all of the specified flags. + */ +bool ems_Device_has_flags(unsigned int flags) { + return (EMS_Sys_Status.emsRefreshedFlags & flags) == flags; +} +/* + * Remove one or more flags from the current flags. + */ +void ems_Device_remove_flags(unsigned int flags) { + EMS_Sys_Status.emsRefreshedFlags &= ~flags; +} + // init stats and counters and buffers void ems_init() { ems_clearDeviceList(); // init the device map // overall status - EMS_Sys_Status.emsRxPgks = 0; - EMS_Sys_Status.emsTxPkgs = 0; - EMS_Sys_Status.emxCrcErr = 0; - EMS_Sys_Status.emsRxStatus = EMS_RX_STATUS_IDLE; - EMS_Sys_Status.emsTxStatus = EMS_TX_REV_DETECT; - EMS_Sys_Status.emsRefreshed = false; - EMS_Sys_Status.emsPollEnabled = false; // start up with Poll disabled - EMS_Sys_Status.emsBusConnected = false; - EMS_Sys_Status.emsRxTimestamp = 0; - EMS_Sys_Status.emsTxCapable = false; - EMS_Sys_Status.emsTxDisabled = false; - EMS_Sys_Status.emsPollFrequency = 0; - EMS_Sys_Status.txRetryCount = 0; - EMS_Sys_Status.emsIDMask = 0x00; - EMS_Sys_Status.emsPollAck[0] = EMS_ID_ME; + EMS_Sys_Status.emsRxPgks = 0; + EMS_Sys_Status.emsTxPkgs = 0; + EMS_Sys_Status.emxCrcErr = 0; + EMS_Sys_Status.emsRxStatus = EMS_RX_STATUS_IDLE; + EMS_Sys_Status.emsTxStatus = EMS_TX_REV_DETECT; + EMS_Sys_Status.emsRefreshedFlags = EMS_DEVICE_UPDATE_FLAG_NONE; + EMS_Sys_Status.emsPollEnabled = false; // start up with Poll disabled + EMS_Sys_Status.emsBusConnected = false; + EMS_Sys_Status.emsRxTimestamp = 0; + EMS_Sys_Status.emsTxCapable = false; + EMS_Sys_Status.emsTxDisabled = false; + EMS_Sys_Status.emsPollFrequency = 0; + EMS_Sys_Status.txRetryCount = 0; + EMS_Sys_Status.emsIDMask = 0x00; + EMS_Sys_Status.emsPollAck[0] = EMS_ID_ME; // thermostat strlcpy(EMS_Thermostat.datetime, "?", sizeof(EMS_Thermostat.datetime)); @@ -258,7 +101,7 @@ void ems_init() { for (uint8_t i = 0; i < EMS_THERMOSTAT_MAXHC; i++) { EMS_Thermostat.hc[i].hc = i + 1; EMS_Thermostat.hc[i].active = false; - EMS_Thermostat.hc[i].mode = EMS_VALUE_INT_NOTSET; // night, day, auto + EMS_Thermostat.hc[i].mode = EMS_VALUE_INT_NOTSET; EMS_Thermostat.hc[i].day_mode = EMS_VALUE_INT_NOTSET; EMS_Thermostat.hc[i].summer_mode = EMS_VALUE_INT_NOTSET; EMS_Thermostat.hc[i].holiday_mode = EMS_VALUE_INT_NOTSET; @@ -278,30 +121,27 @@ void ems_init() { EMS_Mixing.hc[i].flowTemp = EMS_VALUE_SHORT_NOTSET; EMS_Mixing.hc[i].pumpMod = EMS_VALUE_INT_NOTSET; EMS_Mixing.hc[i].valveStatus = EMS_VALUE_INT_NOTSET; - EMS_Mixing.hc[i].device_id = EMS_ID_NONE; - EMS_Mixing.hc[i].model_id = EMS_MODEL_NONE; - EMS_Mixing.hc[i].product_id = EMS_ID_NONE; } // UBAParameterWW - EMS_Boiler.wWActivated = EMS_VALUE_INT_NOTSET; // Warm Water activated - EMS_Boiler.wWSelTemp = EMS_VALUE_INT_NOTSET; // Warm Water selected temperature - EMS_Boiler.wWCircPump = EMS_VALUE_INT_NOTSET; // Warm Water circulation pump available - EMS_Boiler.wWDesiredTemp = EMS_VALUE_INT_NOTSET; // Warm Water desired temperature to prevent infection - EMS_Boiler.wWComfort = EMS_VALUE_INT_NOTSET; + EMS_Boiler.wWActivated = EMS_VALUE_BOOL_NOTSET; // Warm Water activated + EMS_Boiler.wWSelTemp = EMS_VALUE_INT_NOTSET; // Warm Water selected temperature + EMS_Boiler.wWCircPump = EMS_VALUE_BOOL_NOTSET; // Warm Water circulation pump available + EMS_Boiler.wWDesiredTemp = EMS_VALUE_INT_NOTSET; // Warm Water desired temperature to prevent infection + EMS_Boiler.wWComfort = EMS_VALUE_INT_NOTSET; // WW comfort mode // UBAMonitorFast EMS_Boiler.selFlowTemp = EMS_VALUE_INT_NOTSET; // Selected flow temperature EMS_Boiler.curFlowTemp = EMS_VALUE_USHORT_NOTSET; // Current flow temperature EMS_Boiler.retTemp = EMS_VALUE_USHORT_NOTSET; // Return temperature - EMS_Boiler.burnGas = EMS_VALUE_INT_NOTSET; // Gas on/off - EMS_Boiler.fanWork = EMS_VALUE_INT_NOTSET; // Fan on/off - EMS_Boiler.ignWork = EMS_VALUE_INT_NOTSET; // Ignition on/off - EMS_Boiler.heatPmp = EMS_VALUE_INT_NOTSET; // Boiler pump on/off + EMS_Boiler.burnGas = EMS_VALUE_BOOL_NOTSET; // Gas on/off + EMS_Boiler.fanWork = EMS_VALUE_BOOL_NOTSET; // Fan on/off + EMS_Boiler.ignWork = EMS_VALUE_BOOL_NOTSET; // Ignition on/off + EMS_Boiler.heatPmp = EMS_VALUE_BOOL_NOTSET; // Boiler pump on/off EMS_Boiler.wWHeat = EMS_VALUE_INT_NOTSET; // 3-way valve on WW - EMS_Boiler.wWCirc = EMS_VALUE_INT_NOTSET; // Circulation on/off - EMS_Boiler.selBurnPow = EMS_VALUE_INT_NOTSET; // Burner max power - EMS_Boiler.curBurnPow = EMS_VALUE_INT_NOTSET; // Burner current power + EMS_Boiler.wWCirc = EMS_VALUE_BOOL_NOTSET; // Circulation on/off + EMS_Boiler.selBurnPow = EMS_VALUE_INT_NOTSET; // Burner max power % + EMS_Boiler.curBurnPow = EMS_VALUE_INT_NOTSET; // Burner current power % EMS_Boiler.flameCurr = EMS_VALUE_USHORT_NOTSET; // Flame current in micro amps EMS_Boiler.sysPress = EMS_VALUE_INT_NOTSET; // System pressure strlcpy(EMS_Boiler.serviceCodeChar, "??", sizeof(EMS_Boiler.serviceCodeChar)); @@ -310,7 +150,7 @@ void ems_init() { // UBAMonitorSlow EMS_Boiler.extTemp = EMS_VALUE_SHORT_NOTSET; // Outside temperature EMS_Boiler.boilTemp = EMS_VALUE_USHORT_NOTSET; // Boiler temperature - EMS_Boiler.pumpMod = EMS_VALUE_INT_NOTSET; // Pump modulation + EMS_Boiler.pumpMod = EMS_VALUE_INT_NOTSET; // Pump modulation % EMS_Boiler.burnStarts = EMS_VALUE_LONG_NOTSET; // # burner restarts EMS_Boiler.burnWorkMin = EMS_VALUE_LONG_NOTSET; // Total burner operating time EMS_Boiler.heatWorkMin = EMS_VALUE_LONG_NOTSET; // Total heat operating time @@ -320,26 +160,25 @@ void ems_init() { EMS_Boiler.wWStarts = EMS_VALUE_LONG_NOTSET; // Warm Water # starts EMS_Boiler.wWWorkM = EMS_VALUE_LONG_NOTSET; // Warm Water # minutes EMS_Boiler.wWOneTime = EMS_VALUE_INT_NOTSET; // Warm Water one time function on/off - EMS_Boiler.wWCurFlow = EMS_VALUE_INT_NOTSET; + EMS_Boiler.wWCurFlow = EMS_VALUE_INT_NOTSET; // WW current flow temp // UBATotalUptimeMessage EMS_Boiler.UBAuptime = EMS_VALUE_LONG_NOTSET; // Total UBA working hours // UBAParametersMessage EMS_Boiler.heating_temp = EMS_VALUE_INT_NOTSET; // Heating temperature setting on the boiler - EMS_Boiler.pump_mod_max = EMS_VALUE_INT_NOTSET; // Boiler circuit pump modulation max. power - EMS_Boiler.pump_mod_min = EMS_VALUE_INT_NOTSET; // Boiler circuit pump modulation min. power + EMS_Boiler.pump_mod_max = EMS_VALUE_INT_NOTSET; // Boiler circuit pump modulation max. power % + EMS_Boiler.pump_mod_min = EMS_VALUE_INT_NOTSET; // Boiler circuit pump modulation min. power % // Solar Module values EMS_SolarModule.collectorTemp = EMS_VALUE_SHORT_NOTSET; // collector temp from SM10/SM100 EMS_SolarModule.bottomTemp = EMS_VALUE_SHORT_NOTSET; // bottom temp from SM10/SM100 EMS_SolarModule.pumpModulation = EMS_VALUE_INT_NOTSET; // modulation solar pump SM10/SM100 - EMS_SolarModule.pump = EMS_VALUE_INT_NOTSET; // pump active + EMS_SolarModule.pump = EMS_VALUE_BOOL_NOTSET; // pump active EMS_SolarModule.EnergyLastHour = EMS_VALUE_USHORT_NOTSET; EMS_SolarModule.EnergyToday = EMS_VALUE_USHORT_NOTSET; EMS_SolarModule.EnergyTotal = EMS_VALUE_USHORT_NOTSET; EMS_SolarModule.device_id = EMS_ID_NONE; - EMS_SolarModule.model_id = EMS_MODEL_NONE; EMS_SolarModule.product_id = EMS_ID_NONE; EMS_SolarModule.pumpWorkMin = EMS_VALUE_LONG_NOTSET; EMS_SolarModule.setpoint_maxBottomTemp = EMS_VALUE_SHORT_NOTSET; @@ -348,19 +187,17 @@ void ems_init() { EMS_HeatPump.HPModulation = EMS_VALUE_INT_NOTSET; EMS_HeatPump.HPSpeed = EMS_VALUE_INT_NOTSET; EMS_HeatPump.device_id = EMS_ID_NONE; - EMS_HeatPump.model_id = EMS_MODEL_NONE; EMS_HeatPump.product_id = EMS_ID_NONE; // calculated values - EMS_Boiler.tapwaterActive = EMS_VALUE_INT_NOTSET; // Hot tap water is on/off - EMS_Boiler.heatingActive = EMS_VALUE_INT_NOTSET; // Central heating is on/off + EMS_Boiler.tapwaterActive = EMS_VALUE_BOOL_NOTSET; // Hot tap water is on/off + EMS_Boiler.heatingActive = EMS_VALUE_BOOL_NOTSET; // Central heating is on/off // set boiler type EMS_Boiler.product_id = EMS_ID_NONE; strlcpy(EMS_Boiler.version, "?", sizeof(EMS_Boiler.version)); // set thermostat model - EMS_Thermostat.model_id = EMS_MODEL_NONE; EMS_Thermostat.product_id = EMS_ID_NONE; strlcpy(EMS_Thermostat.version, "?", sizeof(EMS_Thermostat.version)); @@ -378,14 +215,6 @@ bool ems_getPoll() { return EMS_Sys_Status.emsPollEnabled; } -bool ems_getEmsRefreshed() { - return EMS_Sys_Status.emsRefreshed; -} - -void ems_setEmsRefreshed(bool b) { - EMS_Sys_Status.emsRefreshed = b; -} - bool ems_getBoilerEnabled() { return (EMS_Boiler.device_id != EMS_ID_NONE); } @@ -407,11 +236,11 @@ bool ems_getHeatPumpEnabled() { } uint8_t ems_getThermostatModel() { - return (EMS_Thermostat.model_id); + return (EMS_Thermostat.device_flags & 0x7F); // strip 7th bit } uint8_t ems_getSolarModuleModel() { - return (EMS_SolarModule.model_id); + return (EMS_SolarModule.device_flags); } void ems_setTxDisabled(bool b) { @@ -444,12 +273,9 @@ _EMS_SYS_LOGGING ems_getLogging() { return EMS_Sys_Status.emsLogging; } -void ems_setLogging(_EMS_SYS_LOGGING loglevel, bool silent) { +void ems_setLogging(_EMS_SYS_LOGGING loglevel, uint16_t type_id) { if (loglevel <= EMS_SYS_LOGGING_JABBER) { EMS_Sys_Status.emsLogging = loglevel; - if (silent) { - return; // don't print to telnet/serial - } if (loglevel == EMS_SYS_LOGGING_NONE) { myDebug_P(PSTR("System Logging set to None")); @@ -465,6 +291,9 @@ void ems_setLogging(_EMS_SYS_LOGGING loglevel, bool silent) { myDebug_P(PSTR("System Logging set to Raw mode")); } else if (loglevel == EMS_SYS_LOGGING_JABBER) { myDebug_P(PSTR("System Logging set to Jabber mode")); + } else if (loglevel == EMS_SYS_LOGGING_WATCH) { + EMS_Sys_Status.emsLogging_typeID = type_id; + myDebug_P(PSTR("System Logging set to Watch mode")); } } } @@ -496,30 +325,78 @@ uint8_t _crcCalculator(uint8_t * data, uint8_t len) { return crc; } -/** - * Find the pointer to the EMS_Types array for a given type ID - * or -1 if not found - */ -int _ems_findType(uint16_t type) { - uint8_t i = 0; - bool typeFound = false; - // scan through known ID types - while (i < _EMS_Types_max) { - if (EMS_Types[i].type == type) { - typeFound = true; // we have a match - break; - } - i++; +// unsigned short +void _setValue(_EMS_RxTelegram * EMS_RxTelegram, uint16_t * param_op, uint8_t index) { + if (index >= EMS_RxTelegram->data_length) { + return; } - return (typeFound ? i : -1); + uint16_t value = (EMS_RxTelegram->data[index] << 8) + EMS_RxTelegram->data[index + 1]; + + // check for undefined/unset values, 0x8000 + if (value == EMS_VALUE_USHORT_NOTSET) { + return; + } + + *param_op = value; +} + +// signed short +void _setValue(_EMS_RxTelegram * EMS_RxTelegram, int16_t * param_op, uint8_t index) { + if (index >= EMS_RxTelegram->data_length) { + return; + } + + int16_t value = (EMS_RxTelegram->data[index] << 8) + EMS_RxTelegram->data[index + 1]; + + // check for undefined/unset values, 0x8000 + if ((value == EMS_VALUE_SHORT_NOTSET) || (EMS_RxTelegram->data[index] == 0x7D)) { + return; + } + + *param_op = value; +} + +// Byte +void _setValue(_EMS_RxTelegram * EMS_RxTelegram, uint8_t * param_op, uint8_t index) { + if (index >= EMS_RxTelegram->data_length) { + return; + } + + *param_op = (uint8_t)EMS_RxTelegram->data[index]; +} + +// convert signed short to single 8 byte, for setpoint thermostat temperatures that don't store their temps in 2 bytes +void _setValue8(_EMS_RxTelegram * EMS_RxTelegram, int16_t * param_op, uint8_t index) { + if (index >= EMS_RxTelegram->data_length) { + return; + } + + *param_op = EMS_RxTelegram->data[index]; +} + +// Long +void _setValue(_EMS_RxTelegram * EMS_RxTelegram, uint32_t * param_op, uint8_t index) { + if (index >= EMS_RxTelegram->data_length) { + return; + } + + *param_op = (uint32_t)((EMS_RxTelegram->data[index] << 16) + (EMS_RxTelegram->data[index + 1] << 8) + (EMS_RxTelegram->data[index + 2])); +} + +// bit from a byte +void _setValue(_EMS_RxTelegram * EMS_RxTelegram, uint8_t * param_op, uint8_t index, uint8_t bit) { + if (index >= EMS_RxTelegram->data_length) { + return; + } + + *param_op = (uint8_t)(((EMS_RxTelegram->data[index]) >> (bit)) & 0x01); } void ems_setTxMode(uint8_t mode) { EMS_Sys_Status.emsTxMode = mode; } - /** * debug print a telegram to telnet/serial including the CRC */ @@ -530,19 +407,50 @@ void _debugPrintTelegram(const char * prefix, _EMS_RxTelegram * EMS_RxTelegram, uint8_t data_len = EMS_RxTelegram->data_length; // length of data block uint8_t length = EMS_RxTelegram->length; // includes CRC + // get elapsed system time or internet time if available + uint8_t t_sec, t_min, t_hour; + uint16_t t_msec; + unsigned long timestamp = EMS_RxTelegram->timestamp; + bool haveNTPtime = (timestamp > 1572307205); // after Jan 1st 1970 + + if (haveNTPtime) { + t_sec = timestamp % 60; + timestamp /= 60; // now it is minutes + t_min = timestamp % 60; + timestamp /= 60; // now it is hours + t_hour = timestamp % 24; + } else { + t_hour = timestamp / 3600000; + t_min = (timestamp / 60000) % 60; + t_sec = (timestamp / 1000) % 60; + t_msec = timestamp % 1000; + } + strlcpy(output_str, "(", sizeof(output_str)); - strlcat(output_str, COLOR_CYAN, sizeof(output_str)); - strlcat(output_str, _smallitoa((uint8_t)((EMS_RxTelegram->timestamp / 3600000) % 24), buffer), sizeof(output_str)); + + if (!raw) + strlcat(output_str, COLOR_CYAN, sizeof(output_str)); + + strlcat(output_str, _smallitoa(t_hour, buffer), sizeof(output_str)); strlcat(output_str, ":", sizeof(output_str)); - strlcat(output_str, _smallitoa((uint8_t)((EMS_RxTelegram->timestamp / 60000) % 60), buffer), sizeof(output_str)); + strlcat(output_str, _smallitoa(t_min, buffer), sizeof(output_str)); strlcat(output_str, ":", sizeof(output_str)); - strlcat(output_str, _smallitoa((uint8_t)((EMS_RxTelegram->timestamp / 1000) % 60), buffer), sizeof(output_str)); - strlcat(output_str, ".", sizeof(output_str)); - strlcat(output_str, _smallitoa3(EMS_RxTelegram->timestamp % 1000, buffer), sizeof(output_str)); - strlcat(output_str, COLOR_RESET, sizeof(output_str)); + strlcat(output_str, _smallitoa(t_sec, buffer), sizeof(output_str)); + + // internet time doesn't have millisecond precision, so ignore it + if (!haveNTPtime) { + strlcat(output_str, ".", sizeof(output_str)); + strlcat(output_str, _smallitoa3(t_msec, buffer), sizeof(output_str)); + } + + if (!raw) + strlcat(output_str, COLOR_RESET, sizeof(output_str)); + strlcat(output_str, ") ", sizeof(output_str)); - strlcat(output_str, color, sizeof(output_str)); + if (!raw) + strlcat(output_str, color, sizeof(output_str)); + strlcat(output_str, prefix, sizeof(output_str)); if (!raw) { @@ -554,18 +462,23 @@ void _debugPrintTelegram(const char * prefix, _EMS_RxTelegram * EMS_RxTelegram, strlcat(output_str, " ", sizeof(output_str)); // add space } - strlcat(output_str, "(CRC=", sizeof(output_str)); - strlcat(output_str, _hextoa(data[length - 1], buffer), sizeof(output_str)); - strlcat(output_str, ")", sizeof(output_str)); + if (!raw) { + strlcat(output_str, "(CRC=", sizeof(output_str)); + strlcat(output_str, _hextoa(data[length - 1], buffer), sizeof(output_str)); + strlcat(output_str, ")", sizeof(output_str)); - // print number of data bytes only if its a valid telegram - if (data_len) { - strlcat(output_str, " #data=", sizeof(output_str)); - strlcat(output_str, itoa(data_len, buffer, 10), sizeof(output_str)); + // print number of data bytes only if its a valid telegram + if (data_len) { + strlcat(output_str, " #data=", sizeof(output_str)); + strlcat(output_str, itoa(data_len, buffer, 10), sizeof(output_str)); + } + + strlcat(output_str, COLOR_RESET, sizeof(output_str)); + } else { + // send it the SysLog + myESP.writeLogEvent(MYESP_SYSLOG_INFO, output_str); } - strlcat(output_str, COLOR_RESET, sizeof(output_str)); - myDebug(output_str); } @@ -589,12 +502,6 @@ void _ems_sendTelegram() { // we don't remove from the queue yet _EMS_TxTelegram EMS_TxTelegram = EMS_TxQueue.first(); - // if there is no destination, also delete it from the queue - if (EMS_TxTelegram.dest == EMS_ID_NONE) { - EMS_TxQueue.shift(); // remove from queue - return; - } - // if we're in raw mode just fire and forget if (EMS_TxTelegram.action == EMS_TX_TELEGRAM_RAW) { EMS_TxTelegram.data[EMS_TxTelegram.length - 1] = _crcCalculator(EMS_TxTelegram.data, EMS_TxTelegram.length); // add the CRC @@ -603,8 +510,8 @@ void _ems_sendTelegram() { _EMS_RxTelegram EMS_RxTelegram; // create new Rx object EMS_RxTelegram.length = EMS_TxTelegram.length; // full length of telegram EMS_RxTelegram.telegram = EMS_TxTelegram.data; - EMS_RxTelegram.data_length = 0; // ignore #data= - EMS_RxTelegram.timestamp = millis(); // now + EMS_RxTelegram.data_length = 0; // ignore #data= + EMS_RxTelegram.timestamp = myESP.getSystemTime(); // now _debugPrintTelegram("Sending raw: ", &EMS_RxTelegram, COLOR_CYAN, true); } @@ -675,7 +582,7 @@ void _ems_sendTelegram() { EMS_RxTelegram.length = EMS_TxTelegram.length; // complete length of telegram incl CRC EMS_RxTelegram.data_length = 0; // ignore the data length for read and writes. only used for incoming. EMS_RxTelegram.telegram = EMS_TxTelegram.data; - EMS_RxTelegram.timestamp = millis(); // now + EMS_RxTelegram.timestamp = myESP.getSystemTime(); // now _debugPrintTelegram(s, &EMS_RxTelegram, COLOR_CYAN); } @@ -692,7 +599,6 @@ void _ems_sendTelegram() { } } - /** * Takes the last write command and turns into a validate request * placing it on the Tx queue @@ -719,9 +625,8 @@ void _createValidate() { new_EMS_TxTelegram.action = EMS_TX_TELEGRAM_VALIDATE; // copy old Write record - new_EMS_TxTelegram.type_validate = EMS_TxTelegram.type; // save the original type in the type_validate, increase we need to re-try - new_EMS_TxTelegram.type = EMS_TxTelegram.type_validate; // new type is the validate type - + new_EMS_TxTelegram.type_validate = EMS_TxTelegram.type; // save the original type in the type_validate, increase we need to re-try + new_EMS_TxTelegram.type = EMS_TxTelegram.type_validate; // new type is the validate type new_EMS_TxTelegram.dest = EMS_TxTelegram.dest; new_EMS_TxTelegram.comparisonValue = EMS_TxTelegram.comparisonValue; new_EMS_TxTelegram.comparisonPostRead = EMS_TxTelegram.comparisonPostRead; @@ -729,8 +634,9 @@ void _createValidate() { // this is what is different new_EMS_TxTelegram.offset = EMS_TxTelegram.comparisonOffset; // location of byte to fetch - new_EMS_TxTelegram.dataValue = 1; // fetch single byte + new_EMS_TxTelegram.dataValue = 1; // fetch one byte new_EMS_TxTelegram.length = EMS_MIN_TELEGRAM_LENGTH; // is always 6 bytes long (including CRC at end) + new_EMS_TxTelegram.timestamp = millis(); // remove old telegram from queue and add this new read one EMS_TxQueue.shift(); // remove from queue @@ -745,16 +651,6 @@ void ems_dumpBuffer(const char * prefix, uint8_t * telegram, uint8_t length) { static char output_str[200] = {0}; static char buffer[16] = {0}; - /* - // we only care about known devices - if (length) { - uint8_t dev = telegram[0] & 0x7F; - if (!((dev == 0x04)||(dev == 0x08)||(dev == 0x09)||(dev == 0x0a) - ||(dev == 0x01)||(dev == 0x0b)||(dev == 0x10))) - return; - } -*/ - strlcpy(output_str, "(", sizeof(output_str)); strlcat(output_str, COLOR_CYAN, sizeof(output_str)); strlcat(output_str, _smallitoa((uint8_t)((timestamp / 3600000) % 24), buffer), sizeof(output_str)); @@ -776,7 +672,6 @@ void ems_dumpBuffer(const char * prefix, uint8_t * telegram, uint8_t length) { strlcat(output_str, _hextoa(EMS_Sys_Status.emsTxStatus, buffer), sizeof(output_str)); strlcat(output_str, ": ", sizeof(output_str)); - // print whole buffer, don't interpret any data for (int i = 0; i < (length); i++) { strlcat(output_str, _hextoa(telegram[i], buffer), sizeof(output_str)); @@ -814,7 +709,7 @@ void ems_parseTelegram(uint8_t * telegram, uint8_t length) { } /* - * It may happen that we where interrupted (for instance by WIFI activity) and the + * It may happen that we were interrupted (for instance by WIFI activity) and the * buffer isn't valid anymore, so we must not answer at all... */ if (EMS_Sys_Status.emsRxStatus != EMS_RX_STATUS_IDLE) { @@ -825,7 +720,7 @@ void ems_parseTelegram(uint8_t * telegram, uint8_t length) { } /* - * check if we just received a single byte + * check if we just received one byte * it could well be a Poll request from the boiler for us, which will have a value of 0x8B (0x0B | 0x80) * or either a return code like 0x01 or 0x04 from the last Write command */ @@ -850,7 +745,7 @@ void ems_parseTelegram(uint8_t * telegram, uint8_t length) { } } } else if (EMS_Sys_Status.emsTxStatus == EMS_TX_STATUS_WAIT) { - // this may be a single byte 01 (success) or 04 (error) from a recent write command? + // this may be a byte 01 (success) or 04 (error) from a recent write command? if (value == EMS_TX_SUCCESS) { EMS_Sys_Status.emsTxPkgs++; // got a success 01. Send a validate to check the value of the last write @@ -878,7 +773,7 @@ void ems_parseTelegram(uint8_t * telegram, uint8_t length) { static _EMS_RxTelegram EMS_RxTelegram; // create the Rx package EMS_RxTelegram.telegram = telegram; - EMS_RxTelegram.timestamp = millis(); + EMS_RxTelegram.timestamp = myESP.getSystemTime(); EMS_RxTelegram.length = length; EMS_RxTelegram.src = telegram[0] & 0x7F; // removing 8th bit as we deal with both reads and writes here @@ -920,9 +815,12 @@ void ems_parseTelegram(uint8_t * telegram, uint8_t length) { } // if we are in raw logging mode then just print out the telegram as it is + // else if we're watching a specific type ID show it and also log an event to the SysLog // but still continue to process it if ((EMS_Sys_Status.emsLogging == EMS_SYS_LOGGING_RAW)) { _debugPrintTelegram("", &EMS_RxTelegram, COLOR_WHITE, true); + } else if ((EMS_Sys_Status.emsLogging == EMS_SYS_LOGGING_WATCH) && (EMS_RxTelegram.type == EMS_Sys_Status.emsLogging_typeID)) { + _debugPrintTelegram("", &EMS_RxTelegram, COLOR_WHITE, true); } // Assume at this point we have something that vaguely resembles a telegram in the format [src] [dest] [type] [offset] [data] [crc] @@ -937,7 +835,7 @@ void ems_parseTelegram(uint8_t * telegram, uint8_t length) { // here we know its a valid incoming telegram of at least 6 bytes // we use this to see if we always have a connection to the boiler, in case of drop outs - EMS_Sys_Status.emsRxTimestamp = EMS_RxTelegram.timestamp; // timestamp of last read + EMS_Sys_Status.emsRxTimestamp = millis(); // timestamp of last read EMS_Sys_Status.emsBusConnected = true; // now lets process it and see what to do next @@ -957,51 +855,23 @@ void _printMessage(_EMS_RxTelegram * EMS_RxTelegram) { char output_str[200] = {0}; char buffer[16] = {0}; char color_s[20] = {0}; + char type_s[30]; // source - if (src == EMS_Boiler.device_id) { - strlcpy(output_str, "Boiler", sizeof(output_str)); - } else if (src == EMS_Thermostat.device_id) { - strlcpy(output_str, "Thermostat", sizeof(output_str)); - } else if (src == EMS_ID_SM) { - strlcpy(output_str, "SM", sizeof(output_str)); - } else if (src == EMS_ID_HP) { - strlcpy(output_str, "HP", sizeof(output_str)); - } else if (src == EMS_ID_GATEWAY) { - strlcpy(output_str, "Gateway", sizeof(output_str)); - } else { - strlcpy(output_str, "0x", sizeof(output_str)); - strlcat(output_str, _hextoa(src, buffer), sizeof(output_str)); - } - + ems_getDeviceTypeDescription(src, type_s); + strlcpy(output_str, type_s, sizeof(output_str)); strlcat(output_str, " -> ", sizeof(output_str)); // destination + (void)ems_getDeviceTypeDescription(dest, type_s); + strlcat(output_str, type_s, sizeof(output_str)); + if (dest == EMS_ID_ME) { - strlcat(output_str, "me", sizeof(output_str)); - strlcpy(color_s, COLOR_YELLOW, sizeof(color_s)); + strlcpy(color_s, COLOR_YELLOW, sizeof(color_s)); // me } else if (dest == EMS_ID_NONE) { - strlcat(output_str, "all", sizeof(output_str)); - strlcpy(color_s, COLOR_GREEN, sizeof(color_s)); - } else if (dest == EMS_Boiler.device_id) { - strlcat(output_str, "Boiler", sizeof(output_str)); - strlcpy(color_s, COLOR_MAGENTA, sizeof(color_s)); - } else if (dest == EMS_ID_SM) { - strlcat(output_str, "SM", sizeof(output_str)); - strlcpy(color_s, COLOR_MAGENTA, sizeof(color_s)); - } else if (dest == EMS_ID_HP) { - strlcat(output_str, "HP", sizeof(output_str)); - strlcpy(color_s, COLOR_MAGENTA, sizeof(color_s)); - } else if (dest == EMS_ID_GATEWAY) { - strlcat(output_str, "Gateway", sizeof(output_str)); - strlcpy(color_s, COLOR_MAGENTA, sizeof(color_s)); - } else if (dest == EMS_Thermostat.device_id) { - strlcat(output_str, "Thermostat", sizeof(output_str)); - strlcpy(color_s, COLOR_MAGENTA, sizeof(color_s)); + strlcpy(color_s, COLOR_GREEN, sizeof(color_s)); // broadcast } else { - strlcat(output_str, "0x", sizeof(output_str)); - strlcat(output_str, _hextoa(dest, buffer), sizeof(output_str)); - strlcpy(color_s, COLOR_MAGENTA, sizeof(color_s)); + strlcpy(color_s, COLOR_MAGENTA, sizeof(color_s)); // everything else } if (length) { @@ -1018,7 +888,6 @@ void _printMessage(_EMS_RxTelegram * EMS_RxTelegram) { strlcat(output_str, ", ", sizeof(output_str)); - if (EMS_Sys_Status.emsLogging == EMS_SYS_LOGGING_THERMOSTAT) { // only print ones to/from thermostat if logging is set to thermostat only if ((src == EMS_Thermostat.device_id) || (dest == EMS_Thermostat.device_id)) { @@ -1035,66 +904,6 @@ void _printMessage(_EMS_RxTelegram * EMS_RxTelegram) { } } - -/** - * print detailed telegram - * and then call its callback if there is one defined - */ -void _ems_processTelegram(_EMS_RxTelegram * EMS_RxTelegram) { - // print out the telegram for verbose mode - if (EMS_Sys_Status.emsLogging >= EMS_SYS_LOGGING_THERMOSTAT) { - _printMessage(EMS_RxTelegram); - } - - // ignore telegrams that don't have any data - if (EMS_RxTelegram->data_length == 0) { - return; - } - - // header - uint8_t dest = EMS_RxTelegram->dest; - uint16_t type = EMS_RxTelegram->type; - - // see if we recognize the type first by scanning our known EMS types list - bool typeFound = false; - uint8_t i = 0; - - while (i < _EMS_Types_max) { - if (EMS_Types[i].type == type) { - // is it a broadcast or something sent to us? - // we don't really care where it is from - if ((dest == EMS_ID_NONE) || (dest == EMS_ID_ME)) { - typeFound = true; - break; - } - } - i++; - } - - // if it's a common type (across ems devices) or something specifically for us process it. - // dest will be EMS_ID_NONE and offset 0x00 for a broadcast message - if (typeFound) { - if ((EMS_Types[i].processType_cb) != nullptr) { - // print non-verbose message - if ((EMS_Sys_Status.emsLogging == EMS_SYS_LOGGING_BASIC) || (EMS_Sys_Status.emsLogging == EMS_SYS_LOGGING_VERBOSE)) { - myDebug_P(PSTR("<--- %s(0x%02X)"), EMS_Types[i].typeString, type); - } - // call callback function to process the telegram, only if there is data - if (EMS_RxTelegram->emsplus) { - // if EMS+ always proces it - (void)EMS_Types[i].processType_cb(EMS_RxTelegram); - } else { - // only if the offset is 0 as we want to handle full telegrams and not partial - if (EMS_RxTelegram->offset == EMS_ID_NONE) { - (void)EMS_Types[i].processType_cb(EMS_RxTelegram); - } - } - } - } - - EMS_Sys_Status.emsTxStatus = EMS_TX_STATUS_IDLE; -} - /** * Remove current Tx telegram from queue and release lock on Tx */ @@ -1105,133 +914,6 @@ void _removeTxQueue() { EMS_Sys_Status.emsTxStatus = EMS_TX_STATUS_IDLE; } -/** - * deciphers the telegram packet, which has already been checked for valid CRC and has a complete header - * length is only data bytes, excluding the BRK - * We only remove from the Tx queue if the read or write was successful - */ -void _processType(_EMS_RxTelegram * EMS_RxTelegram) { - uint8_t * telegram = EMS_RxTelegram->telegram; - - // if its an echo of ourselves from the master UBA, ignore. This should never happen mind you - if (EMS_RxTelegram->src == EMS_ID_ME) { - if (EMS_Sys_Status.emsLogging == EMS_SYS_LOGGING_JABBER) - _debugPrintTelegram("echo: ", EMS_RxTelegram, COLOR_WHITE); - return; - } - - // if its a broadcast and we didn't just send anything, process it and exit - if (EMS_Sys_Status.emsTxStatus == EMS_TX_STATUS_IDLE) { - _ems_processTelegram(EMS_RxTelegram); - return; - } - - // release the lock on the TxQueue - EMS_Sys_Status.emsTxStatus = EMS_TX_STATUS_IDLE; - - // at this point we can assume TxStatus was EMS_TX_STATUS_WAIT as we just sent a read or validate telegram - // for READ or VALIDATE the dest (telegram[1]) is always us, so check for this - // and if not we probably didn't get any response so remove the last Tx from the queue and process the telegram anyway - if ((telegram[1] & 0x7F) != EMS_ID_ME) { - _removeTxQueue(); - _ems_processTelegram(EMS_RxTelegram); - return; - } - - // first double check we actually have something in the Tx queue that we're waiting upon - if (EMS_TxQueue.isEmpty()) { - _ems_processTelegram(EMS_RxTelegram); - return; - } - - // get the Tx telegram we just sent - _EMS_TxTelegram EMS_TxTelegram = EMS_TxQueue.first(); - - // check action - // if READ, match the current inbound telegram to what we just sent - // if WRITE, should not happen - // if VALIDATE, check the contents - if (EMS_TxTelegram.action == EMS_TX_TELEGRAM_READ) { - // remove MSB from src/dest - if (((EMS_RxTelegram->src & 0x7F) == (EMS_TxTelegram.dest & 0x7F)) && (EMS_RxTelegram->type == EMS_TxTelegram.type)) { - // all checks out, read was successful, remove tx from queue and continue to process telegram - _removeTxQueue(); - EMS_Sys_Status.emsRxPgks++; // increment Rx happy counter - EMS_Sys_Status.emsTxCapable = true; // we're able to transmit a telegram on the Tx - ems_setEmsRefreshed(EMS_TxTelegram.forceRefresh); // does mqtt need refreshing? - } else { - // read not OK, we didn't get back a telegram we expected. - // first see if we got a response back from the sender saying its an unknown command - if (EMS_RxTelegram->data_length == 0) { - _removeTxQueue(); - } else { - // leave on queue and try again, but continue to process what we received as it may be important - EMS_Sys_Status.txRetryCount++; - // if retried too many times, give up and remove it - if (EMS_Sys_Status.txRetryCount >= TX_WRITE_TIMEOUT_COUNT) { - if (EMS_Sys_Status.emsLogging >= EMS_SYS_LOGGING_BASIC) { - myDebug_P(PSTR("-> Read failed. Giving up and removing write from queue")); - } - _removeTxQueue(); - } else { - if (EMS_Sys_Status.emsLogging >= EMS_SYS_LOGGING_BASIC) { - myDebug_P(PSTR("-> Read failed. Retrying (%d/%d)..."), EMS_Sys_Status.txRetryCount, TX_WRITE_TIMEOUT_COUNT); - } - } - } - } - _ems_processTelegram(EMS_RxTelegram); // process it always - } - - if (EMS_TxTelegram.action == EMS_TX_TELEGRAM_WRITE) { - // should not get here, since this is handled earlier receiving a 01 or 04 - myDebug_P(PSTR("-> Write error - panic! should never get here")); - } - - if (EMS_TxTelegram.action == EMS_TX_TELEGRAM_VALIDATE) { - // this is a read telegram which we use to validate the last write - - // data block starts at position 5 for EMS1.0 and 6 for EMS2.0. - // See https://github.com/proddy/EMS-ESP/wiki/RC3xx-Thermostats - uint8_t dataReceived = (EMS_RxTelegram->emsplus) ? telegram[6] : telegram[4]; - - if (EMS_TxTelegram.comparisonValue == dataReceived) { - // validate was successful, the write changed the value - _removeTxQueue(); // now we can remove the Tx validate command the queue - if (EMS_Sys_Status.emsLogging >= EMS_SYS_LOGGING_BASIC) { - myDebug_P(PSTR("-> Validate confirmed, last Write to 0x%02X was successful"), EMS_TxTelegram.dest); - } - // follow up with the post read command - ems_doReadCommand(EMS_TxTelegram.comparisonPostRead, EMS_TxTelegram.dest, true); - } else { - // write failed - if (EMS_Sys_Status.emsLogging >= EMS_SYS_LOGGING_BASIC) { - myDebug_P(PSTR("-> Write failed. Compared set value 0x%02X with received value of 0x%02X"), EMS_TxTelegram.comparisonValue, dataReceived); - } - if (++EMS_Sys_Status.txRetryCount > TX_WRITE_TIMEOUT_COUNT) { - if (EMS_Sys_Status.emsLogging >= EMS_SYS_LOGGING_BASIC) { - myDebug_P(PSTR("-> Write failed. Giving up, removing from queue")); - } - _removeTxQueue(); - } else { - // retry, turn the validate back into a write and try again - if (EMS_Sys_Status.emsLogging >= EMS_SYS_LOGGING_BASIC) { - myDebug_P(PSTR("-> Write didn't work, retrying (%d/%d)..."), EMS_Sys_Status.txRetryCount, TX_WRITE_TIMEOUT_COUNT); - } - EMS_TxTelegram.action = EMS_TX_TELEGRAM_WRITE; - EMS_TxTelegram.dataValue = EMS_TxTelegram.comparisonValue; // restore old value - EMS_TxTelegram.offset = EMS_TxTelegram.comparisonOffset; // restore old value - EMS_TxTelegram.type = EMS_TxTelegram.type_validate; // restore old value, we swapped them to save the original type - - EMS_TxQueue.shift(); // remove validate from queue - EMS_TxQueue.unshift(EMS_TxTelegram); // add back to queue making it next in line - } - } - } - - ems_tx_pollAck(); // send Acknowledgement back to free the EMS bus since we have the telegram -} - /** * Check if hot tap water or heating is active * using a quick hack for checking the heating. Selected Flow Temp >= 70 @@ -1239,12 +921,12 @@ void _processType(_EMS_RxTelegram * EMS_RxTelegram) { void _checkActive() { // hot tap water, using flow to check instead of the burner power if (EMS_Boiler.wWCurFlow != EMS_VALUE_INT_NOTSET && EMS_Boiler.burnGas != EMS_VALUE_INT_NOTSET) { - EMS_Boiler.tapwaterActive = ((EMS_Boiler.wWCurFlow != 0) && (EMS_Boiler.burnGas == EMS_VALUE_INT_ON)); + EMS_Boiler.tapwaterActive = ((EMS_Boiler.wWCurFlow != 0) && (EMS_Boiler.burnGas == EMS_VALUE_BOOL_ON)); } // heating if (EMS_Boiler.selFlowTemp != EMS_VALUE_INT_NOTSET && EMS_Boiler.burnGas != EMS_VALUE_INT_NOTSET) { - EMS_Boiler.heatingActive = ((EMS_Boiler.selFlowTemp >= EMS_BOILER_SELFLOWTEMP_HEATING) && (EMS_Boiler.burnGas == EMS_VALUE_INT_ON)); + EMS_Boiler.heatingActive = ((EMS_Boiler.selFlowTemp >= EMS_BOILER_SELFLOWTEMP_HEATING) && (EMS_Boiler.burnGas == EMS_VALUE_BOOL_ON)); } } @@ -1253,13 +935,11 @@ void _checkActive() { * received only after requested (not broadcasted) */ void _process_UBAParameterWW(_EMS_RxTelegram * EMS_RxTelegram) { - EMS_Boiler.wWActivated = (_toByte(1) == 0xFF); // 0xFF means on - EMS_Boiler.wWSelTemp = _toByte(2); - EMS_Boiler.wWCircPump = (_toByte(6) == 0xFF); // 0xFF means on - EMS_Boiler.wWDesiredTemp = _toByte(8); - EMS_Boiler.wWComfort = _toByte(EMS_OFFSET_UBAParameterWW_wwComfort); - - EMS_Sys_Status.emsRefreshed = true; // when we receieve this, lets force an MQTT publish + _setValue(EMS_RxTelegram, &EMS_Boiler.wWActivated, 1); // 0xFF means on + _setValue(EMS_RxTelegram, &EMS_Boiler.wWCircPump, 6); // 0xFF means on + _setValue(EMS_RxTelegram, &EMS_Boiler.wWSelTemp, 2); + _setValue(EMS_RxTelegram, &EMS_Boiler.wWDesiredTemp, 8); + _setValue(EMS_RxTelegram, &EMS_Boiler.wWComfort, EMS_OFFSET_UBAParameterWW_wwComfort); } /** @@ -1267,17 +947,16 @@ void _process_UBAParameterWW(_EMS_RxTelegram * EMS_RxTelegram) { * received only after requested (not broadcasted) */ void _process_UBATotalUptimeMessage(_EMS_RxTelegram * EMS_RxTelegram) { - EMS_Boiler.UBAuptime = _toLong(0); - EMS_Sys_Status.emsRefreshed = true; // when we receieve this, lets force an MQTT publish + _setValue(EMS_RxTelegram, &EMS_Boiler.UBAuptime, 0); } /** * UBAParametersMessage - type 0x16 */ void _process_UBAParametersMessage(_EMS_RxTelegram * EMS_RxTelegram) { - EMS_Boiler.heating_temp = _toByte(1); - EMS_Boiler.pump_mod_max = _toByte(9); - EMS_Boiler.pump_mod_min = _toByte(10); + _setValue(EMS_RxTelegram, &EMS_Boiler.heating_temp, 1); + _setValue(EMS_RxTelegram, &EMS_Boiler.pump_mod_max, 9); + _setValue(EMS_RxTelegram, &EMS_Boiler.pump_mod_min, 10); } /** @@ -1285,11 +964,33 @@ void _process_UBAParametersMessage(_EMS_RxTelegram * EMS_RxTelegram) { * received every 10 seconds */ void _process_UBAMonitorWWMessage(_EMS_RxTelegram * EMS_RxTelegram) { - EMS_Boiler.wWCurTmp = _toShort(1); - EMS_Boiler.wWStarts = _toLong(13); - EMS_Boiler.wWWorkM = _toLong(10); - EMS_Boiler.wWOneTime = _bitRead(5, 1); - EMS_Boiler.wWCurFlow = _toByte(9); + _setValue(EMS_RxTelegram, &EMS_Boiler.wWCurTmp, 1); + _setValue(EMS_RxTelegram, &EMS_Boiler.wWStarts, 13); + _setValue(EMS_RxTelegram, &EMS_Boiler.wWWorkM, 10); + _setValue(EMS_RxTelegram, &EMS_Boiler.wWOneTime, 5, 1); + _setValue(EMS_RxTelegram, &EMS_Boiler.wWCurFlow, 9); +} + +/** + * Activate / De-activate One Time warm water 0x35 + * true = on, false = off + */ +void ems_setWarmWaterOnetime(bool activated) { + myDebug_P(PSTR("Setting boiler warm water OneTime loading %s"), activated ? "on" : "off"); + + _EMS_TxTelegram EMS_TxTelegram = EMS_TX_TELEGRAM_NEW; // create new Tx + EMS_TxTelegram.timestamp = millis(); // set timestamp + EMS_Sys_Status.txRetryCount = 0; // reset retry counter + + EMS_TxTelegram.action = EMS_TX_TELEGRAM_WRITE; + EMS_TxTelegram.dest = EMS_Boiler.device_id; + EMS_TxTelegram.type = EMS_TYPE_UBAFlags; + EMS_TxTelegram.offset = EMS_OFFSET_UBAParameterWW_wwOneTime; + EMS_TxTelegram.length = EMS_MIN_TELEGRAM_LENGTH; + EMS_TxTelegram.type_validate = EMS_ID_NONE; // don't validate + EMS_TxTelegram.dataValue = (activated ? 0x22 : 0x02); // 0x22 is on, 0x02 is off for RC20RF + + EMS_TxQueue.push(EMS_TxTelegram); } /** @@ -1297,32 +998,60 @@ void _process_UBAMonitorWWMessage(_EMS_RxTelegram * EMS_RxTelegram) { * received every 10 seconds */ void _process_UBAMonitorFast(_EMS_RxTelegram * EMS_RxTelegram) { - EMS_Boiler.selFlowTemp = _toByte(0); - EMS_Boiler.curFlowTemp = _toShort(1); - EMS_Boiler.retTemp = _toShort(13); + _setValue(EMS_RxTelegram, &EMS_Boiler.selFlowTemp, 0); + _setValue(EMS_RxTelegram, &EMS_Boiler.curFlowTemp, 1); + _setValue(EMS_RxTelegram, &EMS_Boiler.selBurnPow, 3); // burn power max setting + _setValue(EMS_RxTelegram, &EMS_Boiler.curBurnPow, 4); - EMS_Boiler.burnGas = _bitRead(7, 0); - EMS_Boiler.fanWork = _bitRead(7, 2); - EMS_Boiler.ignWork = _bitRead(7, 3); - EMS_Boiler.heatPmp = _bitRead(7, 5); - EMS_Boiler.wWHeat = _bitRead(7, 6); - EMS_Boiler.wWCirc = _bitRead(7, 7); + _setValue(EMS_RxTelegram, &EMS_Boiler.burnGas, 7, 0); + _setValue(EMS_RxTelegram, &EMS_Boiler.fanWork, 7, 2); + _setValue(EMS_RxTelegram, &EMS_Boiler.ignWork, 7, 3); + _setValue(EMS_RxTelegram, &EMS_Boiler.heatPmp, 7, 5); + _setValue(EMS_RxTelegram, &EMS_Boiler.wWHeat, 7, 6); + _setValue(EMS_RxTelegram, &EMS_Boiler.wWCirc, 7, 7); - EMS_Boiler.curBurnPow = _toByte(4); - EMS_Boiler.selBurnPow = _toByte(3); // burn power max setting - - EMS_Boiler.flameCurr = _toShort(15); - - // read the service code / installation status as appears on the display - EMS_Boiler.serviceCodeChar[0] = char(_toByte(18)); // ascii character 1 - EMS_Boiler.serviceCodeChar[1] = char(_toByte(19)); // ascii character 2 - EMS_Boiler.serviceCodeChar[2] = '\0'; // null terminate string - - // read error code - EMS_Boiler.serviceCode = _toShort(20); + _setValue(EMS_RxTelegram, &EMS_Boiler.boilTemp, 11); // 0x8000 if not available + _setValue(EMS_RxTelegram, &EMS_Boiler.retTemp, 13); + _setValue(EMS_RxTelegram, &EMS_Boiler.flameCurr, 15); + _setValue(EMS_RxTelegram, &EMS_Boiler.serviceCode, 20); // system pressure. FF means missing - EMS_Boiler.sysPress = _toByte(17); // this is *10 + _setValue(EMS_RxTelegram, &EMS_Boiler.sysPress, 17); // is *10 + + // read the service code / installation status as appears on the display + if (EMS_RxTelegram->data_length > 18) { + EMS_Boiler.serviceCodeChar[0] = char(EMS_RxTelegram->data[18]); // ascii character 1 + EMS_Boiler.serviceCodeChar[1] = char(EMS_RxTelegram->data[19]); // ascii character 2 + EMS_Boiler.serviceCodeChar[2] = '\0'; // null terminate string + } + + // at this point do a quick check to see if the hot water or heating is active + _checkActive(); +} + +/** + * UBAMonitorFast2 - type 0xE4 - central heating monitor + */ +void _process_UBAMonitorFast2(_EMS_RxTelegram * EMS_RxTelegram) { + _setValue(EMS_RxTelegram, &EMS_Boiler.selFlowTemp, 6); + _setValue(EMS_RxTelegram, &EMS_Boiler.burnGas, 11, 0); + _setValue(EMS_RxTelegram, &EMS_Boiler.wWHeat, 11, 2); + _setValue(EMS_RxTelegram, &EMS_Boiler.curBurnPow, 10); + _setValue(EMS_RxTelegram, &EMS_Boiler.selBurnPow, 9); + _setValue(EMS_RxTelegram, &EMS_Boiler.curFlowTemp, 7); // 0x8000 if not available + _setValue(EMS_RxTelegram, &EMS_Boiler.flameCurr, 19); + + // read the service code / installation status as appears on the display + if (EMS_RxTelegram->data_length > 4) { + EMS_Boiler.serviceCodeChar[0] = char(EMS_RxTelegram->data[4]); // ascii character 1 + EMS_Boiler.serviceCodeChar[1] = char(EMS_RxTelegram->data[5]); // ascii character 2 + EMS_Boiler.serviceCodeChar[2] = '\0'; + } + + // still to figure out: + // EMS_Boiler.serviceCode + // EMS_Boiler.retTemp + // EMS_Boiler.sysPress // at this point do a quick check to see if the hot water or heating is active _checkActive(); @@ -1331,15 +1060,39 @@ void _process_UBAMonitorFast(_EMS_RxTelegram * EMS_RxTelegram) { /** * UBAMonitorSlow - type 0x19 - central heating monitor part 2 (27 bytes long) * received every 60 seconds + * e.g. 08 00 19 00 80 00 02 41 80 00 00 00 00 00 03 91 7B 05 B8 40 00 00 00 04 92 AD 00 5E EE 80 00 (CRC=C9) #data=27 + * 08 0B 19 00 FF EA 02 47 80 00 00 00 00 62 03 CA 24 2C D6 23 00 00 00 27 4A B6 03 6E 43 + * 00 01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 17 19 20 21 22 23 24 */ void _process_UBAMonitorSlow(_EMS_RxTelegram * EMS_RxTelegram) { - EMS_Boiler.extTemp = _toShort(0); // 0x8000 if not available - EMS_Boiler.boilTemp = _toShort(2); // 0x8000 if not available - EMS_Boiler.pumpMod = _toByte(9); - EMS_Boiler.burnStarts = _toLong(10); - EMS_Boiler.burnWorkMin = _toLong(13); - EMS_Boiler.heatWorkMin = _toLong(19); - EMS_Boiler.switchTemp = _toShort(25); + _setValue(EMS_RxTelegram, &EMS_Boiler.extTemp, 0); + _setValue(EMS_RxTelegram, &EMS_Boiler.boilTemp, 2); + _setValue(EMS_RxTelegram, &EMS_Boiler.switchTemp, 25); // only if there is a mixer + _setValue(EMS_RxTelegram, &EMS_Boiler.pumpMod, 9); + _setValue(EMS_RxTelegram, &EMS_Boiler.burnStarts, 10); + _setValue(EMS_RxTelegram, &EMS_Boiler.burnWorkMin, 13); + _setValue(EMS_RxTelegram, &EMS_Boiler.heatWorkMin, 19); +} + +/** + * UBAMonitorSlow2 - type 0xE5 - central heating monitor + */ +void _process_UBAMonitorSlow2(_EMS_RxTelegram * EMS_RxTelegram) { + _setValue(EMS_RxTelegram, &EMS_Boiler.fanWork, 2, 2); + _setValue(EMS_RxTelegram, &EMS_Boiler.ignWork, 2, 3); + _setValue(EMS_RxTelegram, &EMS_Boiler.heatPmp, 2, 5); + _setValue(EMS_RxTelegram, &EMS_Boiler.wWCirc, 2, 7); + _setValue(EMS_RxTelegram, &EMS_Boiler.burnStarts, 10); + _setValue(EMS_RxTelegram, &EMS_Boiler.burnWorkMin, 13); + _setValue(EMS_RxTelegram, &EMS_Boiler.heatWorkMin, 19); + _setValue(EMS_RxTelegram, &EMS_Boiler.pumpMod, 25); // or is it switchTemp ? +} + +/** + * UBAOutdoorTemp - type 0xD1 - external temperature + */ +void _process_UBAOutdoorTemp(_EMS_RxTelegram * EMS_RxTelegram) { + _setValue(EMS_RxTelegram, &EMS_Boiler.extTemp, 0); } /** @@ -1349,13 +1102,11 @@ void _process_UBAMonitorSlow(_EMS_RxTelegram * EMS_RxTelegram) { * e.g. 17 0B 91 00 80 1E 00 CB 27 00 00 00 00 05 01 00 CB 00 (CRC=47), #data=14 */ void _process_RC10StatusMessage(_EMS_RxTelegram * EMS_RxTelegram) { - uint8_t hc = EMS_THERMOSTAT_DEFAULTHC - 1; // use HC1 + uint8_t hc = EMS_THERMOSTAT_DEFAULTHC - 1; // use HC1 + EMS_Thermostat.hc[hc].active = true; - EMS_Thermostat.hc[hc].active = true; - EMS_Thermostat.hc[hc].setpoint_roomTemp = _toByte(EMS_OFFSET_RC10StatusMessage_setpoint); // is * 2 - EMS_Thermostat.hc[hc].curr_roomTemp = _toShort(EMS_OFFSET_RC10StatusMessage_curr); // is * 10 - - EMS_Sys_Status.emsRefreshed = true; // triggers a send the values back via MQTT + _setValue8(EMS_RxTelegram, &EMS_Thermostat.hc[hc].setpoint_roomTemp, EMS_OFFSET_RC10StatusMessage_setpoint); // is * 2, force as single byte + _setValue(EMS_RxTelegram, &EMS_Thermostat.hc[hc].curr_roomTemp, EMS_OFFSET_RC10StatusMessage_curr); // is * 10 } /** @@ -1364,13 +1115,11 @@ void _process_RC10StatusMessage(_EMS_RxTelegram * EMS_RxTelegram) { * received every 60 seconds */ void _process_RC20StatusMessage(_EMS_RxTelegram * EMS_RxTelegram) { - uint8_t hc = EMS_THERMOSTAT_DEFAULTHC - 1; // use HC1 + uint8_t hc = EMS_THERMOSTAT_DEFAULTHC - 1; // use HC1 + EMS_Thermostat.hc[hc].active = true; - EMS_Thermostat.hc[hc].active = true; - EMS_Thermostat.hc[hc].setpoint_roomTemp = _toByte(EMS_OFFSET_RC20StatusMessage_setpoint); // is * 2 - EMS_Thermostat.hc[hc].curr_roomTemp = _toShort(EMS_OFFSET_RC20StatusMessage_curr); // is * 10 - - EMS_Sys_Status.emsRefreshed = true; // triggers a send the values back via MQTT + _setValue8(EMS_RxTelegram, &EMS_Thermostat.hc[hc].setpoint_roomTemp, EMS_OFFSET_RC20StatusMessage_setpoint); // is * 2, force as single byte + _setValue(EMS_RxTelegram, &EMS_Thermostat.hc[hc].curr_roomTemp, EMS_OFFSET_RC20StatusMessage_curr); // is * 10 } /** @@ -1378,13 +1127,11 @@ void _process_RC20StatusMessage(_EMS_RxTelegram * EMS_RxTelegram) { * For reading the temp values only * received every 60 seconds */ void _process_RC30StatusMessage(_EMS_RxTelegram * EMS_RxTelegram) { - uint8_t hc = EMS_THERMOSTAT_DEFAULTHC - 1; // use HC1 + uint8_t hc = EMS_THERMOSTAT_DEFAULTHC - 1; // use HC1 + EMS_Thermostat.hc[hc].active = true; - EMS_Thermostat.hc[hc].active = true; - EMS_Thermostat.hc[hc].setpoint_roomTemp = _toByte(EMS_OFFSET_RC30StatusMessage_setpoint); // is * 2 - EMS_Thermostat.hc[hc].curr_roomTemp = _toShort(EMS_OFFSET_RC30StatusMessage_curr); // note, its 2 bytes here - - EMS_Sys_Status.emsRefreshed = true; // triggers a send the values back via MQTT + _setValue8(EMS_RxTelegram, &EMS_Thermostat.hc[hc].setpoint_roomTemp, EMS_OFFSET_RC30StatusMessage_setpoint); // is * 2, force as single byte + _setValue(EMS_RxTelegram, &EMS_Thermostat.hc[hc].curr_roomTemp, EMS_OFFSET_RC30StatusMessage_curr); } /** @@ -1424,25 +1171,19 @@ void _process_RC35StatusMessage(_EMS_RxTelegram * EMS_RxTelegram) { return; } - uint8_t hc_num = _getHeatingCircuit(EMS_RxTelegram) - 1; // which HC is it, 0-3 + uint8_t hc_num = _getHeatingCircuit(EMS_RxTelegram); // which HC is it, 0-3 // ignore if the value is 0 (see https://github.com/proddy/EMS-ESP/commit/ccc30738c00f12ae6c89177113bd15af9826b836) if (EMS_RxTelegram->data[EMS_OFFSET_RC35StatusMessage_setpoint] != 0x00) { - EMS_Thermostat.hc[hc_num].setpoint_roomTemp = _toByte(EMS_OFFSET_RC35StatusMessage_setpoint); // is * 2 + _setValue8(EMS_RxTelegram, &EMS_Thermostat.hc[hc_num].setpoint_roomTemp, EMS_OFFSET_RC35StatusMessage_setpoint); // is * 2, force to single byte } // ignore if the value is unset. Hopefully it will be picked up via a later message - if (EMS_RxTelegram->data[EMS_OFFSET_RC35StatusMessage_curr] != 0x7D) { - EMS_Thermostat.hc[hc_num].curr_roomTemp = _toShort(EMS_OFFSET_RC35StatusMessage_curr); // is * 10 - } - - EMS_Thermostat.hc[hc_num].day_mode = _bitRead(EMS_OFFSET_RC35StatusMessage_mode, 1); // get day mode flag - EMS_Thermostat.hc[hc_num].summer_mode = _bitRead(EMS_OFFSET_RC35StatusMessage_mode, 0); // summer mode? - EMS_Thermostat.hc[hc_num].holiday_mode = _bitRead(EMS_OFFSET_RC35StatusMessage_mode1, 5); // holiday mode? - - EMS_Thermostat.hc[hc_num].circuitcalctemp = _toByte(EMS_OFFSET_RC35Set_circuitcalctemp); // calculated temperature - - EMS_Sys_Status.emsRefreshed = true; // triggers a send the values back via MQTT + _setValue(EMS_RxTelegram, &EMS_Thermostat.hc[hc_num].curr_roomTemp, EMS_OFFSET_RC35StatusMessage_curr); // is * 10 + _setValue(EMS_RxTelegram, &EMS_Thermostat.hc[hc_num].day_mode, EMS_OFFSET_RC35StatusMessage_mode, 1); + _setValue(EMS_RxTelegram, &EMS_Thermostat.hc[hc_num].summer_mode, EMS_OFFSET_RC35StatusMessage_mode, 0); + _setValue(EMS_RxTelegram, &EMS_Thermostat.hc[hc_num].holiday_mode, EMS_OFFSET_RC35StatusMessage_mode1, 5); + _setValue(EMS_RxTelegram, &EMS_Thermostat.hc[hc_num].circuitcalctemp, EMS_OFFSET_RC35Set_circuitcalctemp); } /** @@ -1450,13 +1191,11 @@ void _process_RC35StatusMessage(_EMS_RxTelegram * EMS_RxTelegram) { * The Easy has a digital precision of its floats to 2 decimal places, so values must be divided by 100 */ void _process_EasyStatusMessage(_EMS_RxTelegram * EMS_RxTelegram) { - uint8_t hc = EMS_THERMOSTAT_DEFAULTHC - 1; // use HC1 + uint8_t hc = EMS_THERMOSTAT_DEFAULTHC - 1; // use HC1 + EMS_Thermostat.hc[hc].active = true; - EMS_Thermostat.hc[hc].active = true; - EMS_Thermostat.hc[hc].curr_roomTemp = _toShort(EMS_OFFSET_EasyStatusMessage_curr); // is *100 - EMS_Thermostat.hc[hc].setpoint_roomTemp = _toShort(EMS_OFFSET_EasyStatusMessage_setpoint); // is *100 - - EMS_Sys_Status.emsRefreshed = true; // triggers a send the values back via MQTT + _setValue(EMS_RxTelegram, &EMS_Thermostat.hc[hc].curr_roomTemp, EMS_OFFSET_EasyStatusMessage_curr); // is * 100 + _setValue(EMS_RxTelegram, &EMS_Thermostat.hc[hc].setpoint_roomTemp, EMS_OFFSET_EasyStatusMessage_setpoint); // is * 100 } void _process_MMPLUSStatusMessage(_EMS_RxTelegram * EMS_RxTelegram) { @@ -1466,12 +1205,9 @@ void _process_MMPLUSStatusMessage(_EMS_RxTelegram * EMS_RxTelegram) { } EMS_Mixing.hc[hc].active = true; - if (EMS_RxTelegram->data_length == 1) { - } else if (EMS_RxTelegram->data_length > 8) { - EMS_Mixing.hc[hc].flowTemp = _toShort(EMS_OFFSET_MMPLUSStatusMessage_flow_temp); - EMS_Mixing.hc[hc].pumpMod = _toByte(EMS_OFFSET_MMPLUSStatusMessage_pump_mod); - EMS_Mixing.hc[hc].valveStatus = _toByte(EMS_OFFSET_MMPLUSStatusMessage_valve_status); - } + _setValue(EMS_RxTelegram, &EMS_Mixing.hc[hc].flowTemp, EMS_OFFSET_MMPLUSStatusMessage_flow_temp); + _setValue(EMS_RxTelegram, &EMS_Mixing.hc[hc].pumpMod, EMS_OFFSET_MMPLUSStatusMessage_pump_mod); + _setValue(EMS_RxTelegram, &EMS_Mixing.hc[hc].valveStatus, EMS_OFFSET_MMPLUSStatusMessage_valve_status); } /** @@ -1486,23 +1222,24 @@ void _process_RCPLUSStatusMessage(_EMS_RxTelegram * EMS_RxTelegram) { } EMS_Thermostat.hc[hc].active = true; - // handle single data values + // handle single data values. data will always be at position data[0] if (EMS_RxTelegram->data_length == 1) { switch (EMS_RxTelegram->offset) { - case EMS_OFFSET_RCPLUSStatusMessage_curr: // setpoint target temp - EMS_Thermostat.hc[hc].curr_roomTemp = _toShort(0); // value is * 10 + case EMS_OFFSET_RCPLUSStatusMessage_curr: // setpoint target temp + _setValue(EMS_RxTelegram, &EMS_Thermostat.hc[hc].curr_roomTemp, 0); // value is * 10 break; - case EMS_OFFSET_RCPLUSStatusMessage_setpoint: // current target temp - EMS_Thermostat.hc[hc].setpoint_roomTemp = _toByte(0); // value is * 2 + case EMS_OFFSET_RCPLUSStatusMessage_setpoint: // current target temp + EMS_Thermostat.hc[hc].setpoint_roomTemp = EMS_RxTelegram->data[0]; // convert to single byte, value is * 2 break; - case EMS_OFFSET_RCPLUSStatusMessage_currsetpoint: // current setpoint temp, e.g. Thermostat -> all, telegram: 10 00 FF 06 01 A5 22 - EMS_Thermostat.hc[hc].setpoint_roomTemp = _toByte(0); // value is * 2 + case EMS_OFFSET_RCPLUSStatusMessage_currsetpoint: // current setpoint temp, e.g. Thermostat -> all, telegram: 10 00 FF 06 01 A5 22 + EMS_Thermostat.hc[hc].setpoint_roomTemp = EMS_RxTelegram->data[0]; // convert to single byte, value is * 2 break; - case EMS_OFFSET_RCPLUSStatusMessage_mode: // thermostat mode auto/manual - // manual : 10 00 FF 0A 01 A5 02 - // auto : Thermostat -> all, type 0x01A5 telegram: 10 00 FF 0A 01 A5 03 - EMS_Thermostat.hc[hc].mode = _bitRead(0, 0); // bit 1, mode (auto=1 or manual=0) - EMS_Thermostat.hc[hc].day_mode = _bitRead(0, 1); // get day mode flag + case EMS_OFFSET_RCPLUSStatusMessage_mode: // thermostat mode auto/manual + // manual : 10 00 FF 0A 01 A5 02 + // auto : 10 00 FF 0A 01 A5 03 + _setValue(EMS_RxTelegram, &EMS_Thermostat.hc[hc].mode, 0, + 0); // bit 1, mode (auto=1 or manual=0). Note this may be bit 2 - still need to validate + _setValue(EMS_RxTelegram, &EMS_Thermostat.hc[hc].day_mode, 0, 1); // get day mode flag break; } @@ -1510,37 +1247,34 @@ void _process_RCPLUSStatusMessage(_EMS_RxTelegram * EMS_RxTelegram) { // the whole telegram // e.g. Thermostat -> all, telegram: 10 00 FF 00 01 A5 00 D7 21 00 00 00 00 30 01 84 01 01 03 01 84 01 F1 00 00 11 01 00 08 63 00 // 10 00 FF 00 01 A5 80 00 01 30 28 00 30 28 01 54 03 03 01 01 54 02 A8 00 00 11 01 03 FF FF 00 - EMS_Thermostat.hc[hc].curr_roomTemp = _toShort(EMS_OFFSET_RCPLUSStatusMessage_curr); // value is * 10 - EMS_Thermostat.hc[hc].setpoint_roomTemp = _toByte(EMS_OFFSET_RCPLUSStatusMessage_setpoint); // value is * 2 - EMS_Thermostat.hc[hc].day_mode = _bitRead(EMS_OFFSET_RCPLUSStatusMessage_mode, 1); // get day mode flag - EMS_Thermostat.hc[hc].mode = _bitRead(EMS_OFFSET_RCPLUSStatusMessage_mode, 0); // bit 1, mode (auto=1 or manual=0) + _setValue(EMS_RxTelegram, &EMS_Thermostat.hc[hc].curr_roomTemp, EMS_OFFSET_RCPLUSStatusMessage_curr); // value is * 10 + _setValue8(EMS_RxTelegram, &EMS_Thermostat.hc[hc].setpoint_roomTemp, EMS_OFFSET_RCPLUSStatusMessage_setpoint); // convert to single byte, value is * 2 + _setValue(EMS_RxTelegram, &EMS_Thermostat.hc[hc].day_mode, EMS_OFFSET_RCPLUSStatusMessage_mode, 1); + _setValue(EMS_RxTelegram, &EMS_Thermostat.hc[hc].mode, EMS_OFFSET_RCPLUSStatusMessage_mode, 0); // bit 1, mode (auto=1 or manual=0) } - - EMS_Sys_Status.emsRefreshed = true; // triggers a send the values back via MQTT } /** * type 0x01AF - summer/winter mode from the Nefit RC1010 thermostat (0x18) and RC300/310s on 0x10 */ void _process_RCPLUSStatusMode(_EMS_RxTelegram * EMS_RxTelegram) { - // _toByte(0); // 0x00=OFF 0x01=Automatic 0x02=Forced + // data[0] // 0x00=OFF 0x01=Automatic 0x02=Forced } /** - * FR10 Junkers - type x006F + * FR10/FR50/FR100 Junkers - type x006F + * e.g. for FR10: 90 00 FF 00 00 6F 03 01 00 BE 00 BF + * for FW100: 90 00 FF 00 00 6F 03 02 00 D7 00 DA F3 34 00 C4 */ void _process_JunkersStatusMessage(_EMS_RxTelegram * EMS_RxTelegram) { if (EMS_RxTelegram->offset == 0 && EMS_RxTelegram->data_length > 1) { uint8_t hc = EMS_THERMOSTAT_DEFAULTHC - 1; // use HC1 EMS_Thermostat.hc[hc].active = true; - // e.g. for FR10: 90 00 FF 00 00 6F 03 01 00 BE 00 BF - // e.g. for FW100: 90 00 FF 00 00 6F 03 02 00 D7 00 DA F3 34 00 C4 - - EMS_Thermostat.hc[hc].curr_roomTemp = _toShort(EMS_OFFSET_JunkersStatusMessage_curr); // value is * 10 - EMS_Thermostat.hc[hc].setpoint_roomTemp = _toShort(EMS_OFFSET_JunkersStatusMessage_setpoint); // value is * 10 - EMS_Thermostat.hc[hc].mode = _toByte(EMS_OFFSET_JunkersStatusMessage_mode); - EMS_Sys_Status.emsRefreshed = true; // triggers a send the values back via MQTT + _setValue(EMS_RxTelegram, &EMS_Thermostat.hc[hc].curr_roomTemp, EMS_OFFSET_JunkersStatusMessage_curr); // value is * 10 + _setValue(EMS_RxTelegram, &EMS_Thermostat.hc[hc].setpoint_roomTemp, EMS_OFFSET_JunkersStatusMessage_setpoint); // value is * 10 + _setValue(EMS_RxTelegram, &EMS_Thermostat.hc[hc].day_mode, EMS_OFFSET_JunkersStatusMessage_daymode); // 3 = day, 2 = night + _setValue(EMS_RxTelegram, &EMS_Thermostat.hc[hc].mode, EMS_OFFSET_JunkersStatusMessage_mode); // 1 = manual, 2 = auto } } @@ -1556,29 +1290,24 @@ void _process_RCPLUSSetMessage(_EMS_RxTelegram * EMS_RxTelegram) { uint8_t hc = EMS_THERMOSTAT_DEFAULTHC - 1; // use HC1 EMS_Thermostat.hc[hc].active = true; - // check for single values + // check for one data value // but ignore values of 0xFF, e.g. 10 00 FF 08 01 B9 FF - if ((EMS_RxTelegram->data_length == 1) && (_toByte(0) != 0xFF)) { - // check for setpoint temps, e.g. Thermostat -> all, type 0x01B9, telegram: 10 00 FF 08 01 B9 26 (CRC=1A) #data=1 + if ((EMS_RxTelegram->data_length == 1) && (EMS_RxTelegram->data[0] != 0xFF)) { + // check for setpoint temps, e.g. Thermostat -> all, type 0x01B9, telegram: 10 00 FF 08 01 B9 26 if ((EMS_RxTelegram->offset == EMS_OFFSET_RCPLUSSet_temp_setpoint) || (EMS_RxTelegram->offset == EMS_OFFSET_RCPLUSSet_manual_setpoint)) { - EMS_Thermostat.hc[hc].setpoint_roomTemp = _toByte(0); // value is * 2 - EMS_Sys_Status.emsRefreshed = true; // triggers a send the values back via MQTT - - // check for mode, eg. 10 00 FF 08 01 B9 FF + _setValue8(EMS_RxTelegram, &EMS_Thermostat.hc[hc].setpoint_roomTemp, 0); // single byte conversion, value is * 2 } else if (EMS_RxTelegram->offset == EMS_OFFSET_RCPLUSSet_mode) { - EMS_Thermostat.hc[hc].mode = (_toByte(0) == 0xFF); // Auto = xFF, Manual = x00 (auto=1 or manual=0) - EMS_Sys_Status.emsRefreshed = true; // triggers a send the values back via MQTT + // check for mode, eg. 10 00 FF 08 01 B9 FF + EMS_Thermostat.hc[hc].mode = (EMS_RxTelegram->data[0] == 0xFF); // Auto = xFF, Manual = x00 (auto=1 or manual=0) } - return; // quit } // check for long broadcasts if (EMS_RxTelegram->offset == 0) { - EMS_Thermostat.hc[hc].mode = _toByte(EMS_OFFSET_RCPLUSSet_mode); - EMS_Thermostat.hc[hc].daytemp = _toByte(EMS_OFFSET_RCPLUSSet_temp_comfort2); // is * 2 - EMS_Thermostat.hc[hc].nighttemp = _toByte(EMS_OFFSET_RCPLUSSet_temp_eco); // is * 2 - EMS_Sys_Status.emsRefreshed = true; // triggers a send the values back via MQTT + _setValue(EMS_RxTelegram, &EMS_Thermostat.hc[hc].mode, EMS_OFFSET_RCPLUSSet_mode); + _setValue(EMS_RxTelegram, &EMS_Thermostat.hc[hc].daytemp, EMS_OFFSET_RCPLUSSet_temp_comfort2); // is * 2 + _setValue(EMS_RxTelegram, &EMS_Thermostat.hc[hc].nighttemp, EMS_OFFSET_RCPLUSSet_temp_eco); // is * 2 } } @@ -1595,10 +1324,9 @@ void _process_RC10Set(_EMS_RxTelegram * EMS_RxTelegram) { * received only after requested */ void _process_RC20Set(_EMS_RxTelegram * EMS_RxTelegram) { - uint8_t hc = EMS_THERMOSTAT_DEFAULTHC - 1; // use HC1 - + uint8_t hc = EMS_THERMOSTAT_DEFAULTHC - 1; // use HC1 EMS_Thermostat.hc[hc].active = true; - EMS_Thermostat.hc[hc].mode = _toByte(EMS_OFFSET_RC20Set_mode); // fixed for HC1 + _setValue(EMS_RxTelegram, &EMS_Thermostat.hc[hc].mode, EMS_OFFSET_RC20Set_mode); // note, fixed for HC1 } /** @@ -1606,13 +1334,12 @@ void _process_RC20Set(_EMS_RxTelegram * EMS_RxTelegram) { * received only after requested */ void _process_RC30Set(_EMS_RxTelegram * EMS_RxTelegram) { - uint8_t hc = EMS_THERMOSTAT_DEFAULTHC - 1; // use HC1 - + uint8_t hc = EMS_THERMOSTAT_DEFAULTHC - 1; // use HC1 EMS_Thermostat.hc[hc].active = true; - EMS_Thermostat.hc[hc].mode = _toByte(EMS_OFFSET_RC30Set_mode); // fixed for HC1 + _setValue(EMS_RxTelegram, &EMS_Thermostat.hc[hc].mode, EMS_OFFSET_RC30Set_mode); // note, fixed for HC1 } -// return which heating circuit it is, 1-4 +// return which heating circuit it is, 0-3 for HC1 to HC4 // based on type 0x3E (HC1), 0x48 (HC2), 0x52 (HC3), 0x5C (HC4) uint8_t _getHeatingCircuit(_EMS_RxTelegram * EMS_RxTelegram) { uint8_t hc_num; @@ -1636,7 +1363,8 @@ uint8_t _getHeatingCircuit(_EMS_RxTelegram * EMS_RxTelegram) { break; } - EMS_Thermostat.hc[hc_num - 1].active = true; + hc_num--; + EMS_Thermostat.hc[hc_num].active = true; return (hc_num); } @@ -1650,7 +1378,7 @@ uint8_t _getHeatingCircuit(_EMS_RxTelegram * EMS_RxTelegram) { * 10 0B 5B 00 00 13 15 26 00 28 00 02 00 05 05 2D 01 01 04 4B 05 4B 01 00 3C FF 11 05 05 03 02 */ void _process_RC35Set(_EMS_RxTelegram * EMS_RxTelegram) { - // check to see what type is it + // check to see we have a valid type // heating: 1 radiator, 2 convectors, 3 floors, 4 room supply if (EMS_RxTelegram->data[0] == 0x00) { return; @@ -1658,13 +1386,11 @@ void _process_RC35Set(_EMS_RxTelegram * EMS_RxTelegram) { uint8_t hc_num = _getHeatingCircuit(EMS_RxTelegram); // which HC is it? - EMS_Thermostat.hc[hc_num - 1].mode = _toByte(EMS_OFFSET_RC35Set_mode); // night, day, auto - EMS_Thermostat.hc[hc_num - 1].daytemp = _toByte(EMS_OFFSET_RC35Set_temp_day); // is * 2 - EMS_Thermostat.hc[hc_num - 1].nighttemp = _toByte(EMS_OFFSET_RC35Set_temp_night); // is * 2 - EMS_Thermostat.hc[hc_num - 1].holidaytemp = _toByte(EMS_OFFSET_RC35Set_temp_holiday); // is * 2 - EMS_Thermostat.hc[hc_num - 1].heatingtype = _toByte(EMS_OFFSET_RC35Set_heatingtype); // byte 0 bit floor heating = 3 - - EMS_Sys_Status.emsRefreshed = true; // triggers a send the values back via MQTT + _setValue(EMS_RxTelegram, &EMS_Thermostat.hc[hc_num].mode, EMS_OFFSET_RC35Set_mode); // night, day, auto + _setValue(EMS_RxTelegram, &EMS_Thermostat.hc[hc_num].daytemp, EMS_OFFSET_RC35Set_temp_day); // is * 2 + _setValue(EMS_RxTelegram, &EMS_Thermostat.hc[hc_num].nighttemp, EMS_OFFSET_RC35Set_temp_night); // is * 2 + _setValue(EMS_RxTelegram, &EMS_Thermostat.hc[hc_num].holidaytemp, EMS_OFFSET_RC35Set_temp_holiday); // is * 2 + _setValue(EMS_RxTelegram, &EMS_Thermostat.hc[hc_num].heatingtype, EMS_OFFSET_RC35Set_heatingtype); // byte 0 bit floor heating = 3 } /** @@ -1678,12 +1404,10 @@ void _process_RCOutdoorTempMessage(_EMS_RxTelegram * EMS_RxTelegram) { * SM10Monitor - type 0x97 */ void _process_SM10Monitor(_EMS_RxTelegram * EMS_RxTelegram) { - EMS_SolarModule.collectorTemp = _toShort(2); // collector temp from SM10, is *10 - EMS_SolarModule.bottomTemp = _toShort(5); // bottom temp from SM10, is *10 - EMS_SolarModule.pumpModulation = _toByte(4); // modulation solar pump - EMS_SolarModule.pump = _bitRead(7, 1); // active if bit 1 is set - - EMS_Sys_Status.emsRefreshed = true; // triggers a send the values back via MQTT + _setValue(EMS_RxTelegram, &EMS_SolarModule.collectorTemp, 2); // collector temp from SM10, is *10 + _setValue(EMS_RxTelegram, &EMS_SolarModule.bottomTemp, 5); // bottom temp from SM10, is *10 + _setValue(EMS_RxTelegram, &EMS_SolarModule.pumpModulation, 4); // modulation solar pump + _setValue(EMS_RxTelegram, &EMS_SolarModule.pump, 7, 1); // active if bit 1 is set } /* @@ -1698,13 +1422,8 @@ void _process_SM100Monitor(_EMS_RxTelegram * EMS_RxTelegram) { return; } - EMS_SolarModule.collectorTemp = _toShort(0); // collector temp from SM100, is *10 - - if (EMS_RxTelegram->data_length > 2) { - EMS_SolarModule.bottomTemp = _toShort(2); // bottom temp from SM100, is *10 - } - - EMS_Sys_Status.emsRefreshed = true; // triggers a send the values back via MQTT + _setValue(EMS_RxTelegram, &EMS_SolarModule.collectorTemp, 0); // is *10 + _setValue(EMS_RxTelegram, &EMS_SolarModule.bottomTemp, 2); // is *10 } /* @@ -1713,30 +1432,22 @@ void _process_SM100Monitor(_EMS_RxTelegram * EMS_RxTelegram) { * 30 00 FF 09 02 64 1E = 30% */ void _process_SM100Status(_EMS_RxTelegram * EMS_RxTelegram) { - // check for complete telegram if (EMS_RxTelegram->offset == 0) { - EMS_SolarModule.pumpModulation = _toByte(9); // modulation solar pump + _setValue(EMS_RxTelegram, &EMS_SolarModule.pumpModulation, 9); // check for complete telegram } else if (EMS_RxTelegram->offset == 0x09) { - // or short telegram with a single byte with offset 09 - EMS_SolarModule.pumpModulation = _toByte(0); // modulation solar pump + _setValue(EMS_RxTelegram, &EMS_SolarModule.pumpModulation, 0); // data at offset 09 } - - EMS_Sys_Status.emsRefreshed = true; // triggers a send the values back via MQTT } /* * SM100Status2 - type 0x026A EMS+ for pump on/off at offset 0x0A */ void _process_SM100Status2(_EMS_RxTelegram * EMS_RxTelegram) { - // check for complete telegram if (EMS_RxTelegram->offset == 0) { - EMS_SolarModule.pump = _bitRead(10, 2); // 03=off 04=on at offset 10 which is byte 10 + _setValue(EMS_RxTelegram, &EMS_SolarModule.pump, 10, 2); // 03=off 04=on } else if (EMS_RxTelegram->offset == 0x0A) { - // or short telegram with a single byte with offset 0A - EMS_SolarModule.pump = _bitRead(0, 2); // 03=off 04=on + _setValue(EMS_RxTelegram, &EMS_SolarModule.pump, 0, 2); // 03=off 04=on at offset 0A } - - EMS_Sys_Status.emsRefreshed = true; // triggers a send the values back via MQTT } /* @@ -1744,47 +1455,41 @@ void _process_SM100Status2(_EMS_RxTelegram * EMS_RxTelegram) { * e.g. 30 00 FF 00 02 8E 00 00 00 00 00 00 06 C5 00 00 76 35 */ void _process_SM100Energy(_EMS_RxTelegram * EMS_RxTelegram) { - EMS_SolarModule.EnergyLastHour = _toShort(2); // last hour / 10 in Wh - EMS_SolarModule.EnergyToday = _toShort(6); // todays in Wh - EMS_SolarModule.EnergyTotal = _toShort(10); // total / 10 in kWh - - EMS_Sys_Status.emsRefreshed = true; // triggers a send the values back via MQTT + _setValue(EMS_RxTelegram, &EMS_SolarModule.EnergyLastHour, 2); // last hour / 10 in Wh + _setValue(EMS_RxTelegram, &EMS_SolarModule.EnergyToday, 6); // todays in Wh + _setValue(EMS_RxTelegram, &EMS_SolarModule.EnergyTotal, 10); // total / 10 in kWh } /* * Type 0xE3 - HeatPump Monitor 1 */ void _process_HPMonitor1(_EMS_RxTelegram * EMS_RxTelegram) { - EMS_HeatPump.HPModulation = _toByte(13); // modulation % - - EMS_Sys_Status.emsRefreshed = true; // triggers a send the values back via MQTT + _setValue(EMS_RxTelegram, &EMS_HeatPump.HPModulation, 13); // % } /* * Type 0xE5 - HeatPump Monitor 2 */ void _process_HPMonitor2(_EMS_RxTelegram * EMS_RxTelegram) { - EMS_HeatPump.HPSpeed = _toByte(25); // speed % - - EMS_Sys_Status.emsRefreshed = true; // triggers a send the values back via MQTT + _setValue(EMS_RxTelegram, &EMS_HeatPump.HPSpeed, 25); // % } /* * Junkers ISM1 Solar Module - type 0x0003 EMS+ for energy readings + * e.g. B0 00 FF 00 00 03 32 00 00 00 00 13 00 D6 00 00 00 FB D0 F0 */ void _process_ISM1StatusMessage(_EMS_RxTelegram * EMS_RxTelegram) { if (EMS_RxTelegram->offset == 0) { - // e.g. B0 00 FF 00 00 03 32 00 00 00 00 13 00 D6 00 00 00 FB D0 F0 - EMS_SolarModule.collectorTemp = _toShort(4); // Collector Temperature - EMS_SolarModule.bottomTemp = _toShort(6); // Temperature Bottom of Solar Boiler - EMS_SolarModule.EnergyLastHour = _toShort(2); // Solar Energy produced in last hour - is * 10 and handled in ems-esp.cpp - EMS_SolarModule.pump = _bitRead(8, 0); // Solar pump on (1) or off (0) - EMS_SolarModule.pumpWorkMin = _toLong(10); + _setValue(EMS_RxTelegram, &EMS_SolarModule.collectorTemp, 4); // Collector Temperature + _setValue(EMS_RxTelegram, &EMS_SolarModule.bottomTemp, 6); // Temperature Bottom of Solar Boiler + _setValue(EMS_RxTelegram, &EMS_SolarModule.EnergyLastHour, 2); // Solar Energy produced in last hour - is * 10 and handled in ems-esp.cpp + _setValue(EMS_RxTelegram, &EMS_SolarModule.pump, 8, 0); // Solar pump on (1) or off (0) + _setValue(EMS_RxTelegram, &EMS_SolarModule.pumpWorkMin, 10); } if (EMS_RxTelegram->offset == 4) { // e.g. B0 00 FF 04 00 03 02 E5 - EMS_SolarModule.collectorTemp = _toShort(0); // Collector Temperature + _setValue(EMS_RxTelegram, &EMS_SolarModule.collectorTemp, 0); // Collector Temperature } } @@ -1796,7 +1501,7 @@ void _process_ISM1Set(_EMS_RxTelegram * EMS_RxTelegram) { if (EMS_RxTelegram->offset == 6) { // e.g. 90 30 FF 06 00 01 50 (CRC=2C) // to implement: change max solar boiler temperature - EMS_SolarModule.setpoint_maxBottomTemp = _toByte(0); + EMS_SolarModule.setpoint_maxBottomTemp = EMS_RxTelegram->data[0]; } } @@ -1828,7 +1533,7 @@ void _process_SetPoints(_EMS_RxTelegram * EMS_RxTelegram) { * common for all thermostats */ void _process_RCTime(_EMS_RxTelegram * EMS_RxTelegram) { - if ((EMS_Thermostat.model_id == EMS_MODEL_EASY)) { + if ((EMS_Thermostat.device_flags == EMS_DEVICE_FLAG_EASY)) { return; // not supported } @@ -1836,17 +1541,17 @@ void _process_RCTime(_EMS_RxTelegram * EMS_RxTelegram) { char time_sp[25]; char buffer[4]; - strlcpy(time_sp, _smallitoa(_toByte(2), buffer), sizeof(time_sp)); // hour + strlcpy(time_sp, _smallitoa(EMS_RxTelegram->data[2], buffer), sizeof(time_sp)); // hour strlcat(time_sp, ":", sizeof(time_sp)); - strlcat(time_sp, _smallitoa(_toByte(4), buffer), sizeof(time_sp)); // minute + strlcat(time_sp, _smallitoa(EMS_RxTelegram->data[4], buffer), sizeof(time_sp)); // minute strlcat(time_sp, ":", sizeof(time_sp)); - strlcat(time_sp, _smallitoa(_toByte(5), buffer), sizeof(time_sp)); // second + strlcat(time_sp, _smallitoa(EMS_RxTelegram->data[5], buffer), sizeof(time_sp)); // second strlcat(time_sp, " ", sizeof(time_sp)); - strlcat(time_sp, _smallitoa(_toByte(3), buffer), sizeof(time_sp)); // day + strlcat(time_sp, _smallitoa(EMS_RxTelegram->data[3], buffer), sizeof(time_sp)); // day strlcat(time_sp, "/", sizeof(time_sp)); - strlcat(time_sp, _smallitoa(_toByte(1), buffer), sizeof(time_sp)); // month + strlcat(time_sp, _smallitoa(EMS_RxTelegram->data[1], buffer), sizeof(time_sp)); // month strlcat(time_sp, "/", sizeof(time_sp)); - strlcat(time_sp, itoa(_toByte(0) + 2000, buffer, 10), sizeof(time_sp)); // year + strlcat(time_sp, itoa(EMS_RxTelegram->data[0] + 2000, buffer, 10), sizeof(time_sp)); // year strlcpy(EMS_Thermostat.datetime, time_sp, sizeof(time_sp)); // store } @@ -1864,84 +1569,57 @@ void ems_clearDeviceList() { /* * add an EMS device to our list of detected devices if its unique - * model_type : 1=boiler, 2=thermostat, 3=sm, 4=other, 5=unknown + * returns true if already in list */ -void _addDevice(uint8_t model_type, uint8_t src, uint8_t product_id, char * version, uint8_t i) { - _Generic_Device device; +bool _addDevice(_EMS_DEVICE_TYPE device_type, uint8_t product_id, uint8_t device_id, const char * device_desc_p, const char * version) { + _Detected_Device device; // check for duplicates - bool found = false; - for (std::list<_Generic_Device>::iterator it = Devices.begin(); it != Devices.end(); ++it) { - if (((it)->product_id == product_id) && ((it)->device_id == src)) { - found = true; // it already exists in the list + // a combi of product_id and device_id make it unique + for (std::list<_Detected_Device>::iterator it = Devices.begin(); it != Devices.end(); ++it) { + if (((it)->product_id == product_id) && ((it)->device_id == device_id)) { + return (true); // it already exists in the list, don't add } } - char device_type[500]; - strlcpy(device_type, "EMS Device recognized as ", sizeof(device_type)); + // create a new record and add it to list + device.device_type = device_type; + device.product_id = product_id; + device.device_id = device_id; + device.device_desc_p = device_desc_p; // pointer to the description in the EMS_Devices table + strlcpy(device.version, version, sizeof(device.version)); + device.known = (device_type != EMS_DEVICE_TYPE_UNKNOWN); + Devices.push_back(device); - switch (model_type) { - case EMS_MODELTYPE_BOILER: - strlcat(device_type, "Boiler", sizeof(device_type)); - strlcpy(device.model_string, Boiler_Devices[i].model_string, sizeof(device.model_string)); - break; - case EMS_MODELTYPE_THERMOSTAT: - strlcat(device_type, "Thermostat", sizeof(device_type)); - strlcpy(device.model_string, Thermostat_Devices[i].model_string, sizeof(device.model_string)); - break; - case EMS_MODELTYPE_SM: - strlcat(device_type, "Solar Module", sizeof(device_type)); - strlcpy(device.model_string, SolarModule_Devices[i].model_string, sizeof(device.model_string)); - break; - case EMS_MODELTYPE_HP: - strlcat(device_type, "Heat Pump", sizeof(device_type)); - strlcpy(device.model_string, HeatPump_Devices[i].model_string, sizeof(device.model_string)); - break; - case EMS_MODELTYPE_MIXING: - strlcat(device_type, "Mixing Device", sizeof(device_type)); - strlcpy(device.model_string, Mixing_Devices[i].model_string, sizeof(device.model_string)); - break; - case EMS_MODELTYPE_OTHER: - strlcat(device_type, "Other", sizeof(device_type)); - strlcpy(device.model_string, Other_Devices[i].model_string, sizeof(device.model_string)); - break; - case EMS_MODELTYPE_UNKNOWN: - default: - strlcat(device_type, "?", sizeof(device_type)); - strlcpy(device.model_string, EMS_MODELTYPE_UNKNOWN_STRING, sizeof(device.model_string)); - break; + char line[500]; + strlcpy(line, "New EMS device recognized as a ", sizeof(line)); + + // get type as a string + char type_s[50]; + if (ems_getDeviceTypeDescription(device_id, type_s)) { + strlcat(line, type_s, sizeof(line)); + } else { + strlcat(line, "?", sizeof(line)); } char tmp[6] = {0}; // for formatting numbers - strlcat(device_type, ": ", sizeof(device_type)); - strlcat(device_type, device.model_string, sizeof(device_type)); - strlcat(device_type, " (DeviceID:0x", sizeof(device_type)); - strlcat(device_type, _hextoa(src, tmp), sizeof(device_type)); - - strlcat(device_type, " ProductID:", sizeof(device_type)); - strlcat(device_type, itoa(product_id, tmp, 10), sizeof(device_type)); - - strlcat(device_type, " Version:", sizeof(device_type)); - strlcat(device_type, version, sizeof(device_type)); - - strlcat(device_type, ")", sizeof(device_type)); - - // if already exists mention it - if (found) { - strlcat(device_type, " **already active**", sizeof(device_type)); - myDebug(device_type); // print it - return; // exit + if (device_desc_p != nullptr) { + strlcat(line, ": ", sizeof(line)); + strlcat(line, device_desc_p, sizeof(line)); } - myDebug(device_type); // print it + strlcat(line, " (DeviceID:0x", sizeof(line)); + strlcat(line, _hextoa(device_id, tmp), sizeof(line)); + strlcat(line, " ProductID:", sizeof(line)); + strlcat(line, itoa(product_id, tmp, 10), sizeof(line)); + strlcat(line, " Version:", sizeof(line)); + strlcat(line, version, sizeof(line)); + strlcat(line, ")", sizeof(line)); - // create a new record and add it to list - device.model_type = model_type; - device.product_id = product_id; - device.device_id = src; - strlcpy(device.version, version, sizeof(device.version)); - Devices.push_back(device); + myDebug(line); // print it + + return false; // added, wasn't a duplicate } /** @@ -1969,7 +1647,7 @@ void _process_UBADevices(_EMS_RxTelegram * EMS_RxTelegram) { if ((byte & 0x01) && ((saved_byte & 0x01) == 0)) { uint8_t device_id = ((data_byte + 1) * 8) + bit; if (device_id != EMS_ID_ME) { - myDebug("[EMS] Detected new EMS Device with ID 0x%02X", device_id); + // myDebug("[EMS] Detected new EMS Device with ID 0x%02X", device_id); if (!ems_getTxDisabled()) { ems_doReadCommand(EMS_TYPE_Version, device_id); // get version, but ignore ourselves } @@ -1984,7 +1662,7 @@ void _process_UBADevices(_EMS_RxTelegram * EMS_RxTelegram) { } /** - * type 0x02 - get the firmware version and type of an EMS device + * type 0x02 - get the version and type of an EMS device * look up known devices via the product id and make it active if not already setup */ void _process_Version(_EMS_RxTelegram * EMS_RxTelegram) { @@ -2006,167 +1684,78 @@ void _process_Version(_EMS_RxTelegram * EMS_RxTelegram) { } } - uint8_t product_id = _toByte(offset); + uint8_t device_id = EMS_RxTelegram->src; // device ID - // print version as "%02d.%02d" + // get version as XX.XX char version[10] = {0}; char buf[6] = {0}; - strlcpy(version, _smallitoa(_toByte(offset + 1), buf), sizeof(version)); + strlcpy(version, _smallitoa(EMS_RxTelegram->data[offset + 1], buf), sizeof(version)); strlcat(version, ".", sizeof(version)); - strlcat(version, _smallitoa(_toByte(offset + 2), buf), sizeof(version)); + strlcat(version, _smallitoa(EMS_RxTelegram->data[offset + 2], buf), sizeof(version)); - // see if its a known boiler - int i = 0; - bool typeFound = false; - while (i < _Boiler_Devices_max) { - if ((Boiler_Devices[i].product_id == product_id) && ((EMS_RxTelegram->src & 0x7F) == EMS_ID_BOILER)) { + // scan through known devices matching the productid + uint8_t product_id = EMS_RxTelegram->data[offset]; + uint8_t i = 0; + bool typeFound = false; + while (i < _EMS_Devices_max) { + if (EMS_Devices[i].product_id == product_id) { typeFound = true; // we have a matching product id. i is the index. break; } i++; } - if (typeFound) { - // its a boiler, add to list - _addDevice(EMS_MODELTYPE_BOILER, EMS_RxTelegram->src, product_id, version, i); // type 1 = boiler - - // if its a boiler set it, unless it already has been set by checking for a productID - // it will take the first one found in the list - if ((EMS_Boiler.device_id == EMS_ID_NONE) || ((EMS_Boiler.device_id == EMS_ID_BOILER) && EMS_Boiler.product_id == EMS_ID_NONE)) { - /* - myDebug_P(PSTR("* Setting Boiler to model %s (DeviceID:0x%02X ProductID:%d Version:%s)"), - Boiler_Devices[i].model_string, - EMS_ID_BOILER, - product_id, - version); - */ - - EMS_Boiler.device_id = EMS_ID_BOILER; - EMS_Boiler.product_id = Boiler_Devices[i].product_id; - strlcpy(EMS_Boiler.version, version, sizeof(EMS_Boiler.version)); - - ems_getBoilerValues(); // get Boiler values that we would usually have to wait for - } + // if not found, just add it + if (!typeFound) { + (void)_addDevice(EMS_DEVICE_TYPE_UNKNOWN, product_id, device_id, nullptr, version); return; } - // its not a boiler, maybe its a known thermostat? - i = 0; - while (i < _Thermostat_Devices_max) { - if (Thermostat_Devices[i].product_id == product_id) { - typeFound = true; // we have a matching product id. i is the index. - break; - } - i++; + const char * device_desc_p = (EMS_Devices[i].device_desc); // pointer to the full description of the device + _EMS_DEVICE_TYPE type = EMS_Devices[i].type; // type + + // we recognized it, see if we already have it in our recognized list + if (_addDevice(type, product_id, device_id, device_desc_p, version)) { + return; // already in list } - if (typeFound) { - // its a known thermostat, add to list - _addDevice(EMS_MODELTYPE_THERMOSTAT, EMS_RxTelegram->src, product_id, version, i); + uint8_t flags = EMS_Devices[i].flags; // its a new entry, set the specifics - // if we don't have a thermostat set, use this one. it will pick the first one. - if (((EMS_Thermostat.device_id == EMS_ID_NONE) || (EMS_Thermostat.model_id == EMS_MODEL_NONE) - || (EMS_Thermostat.device_id == Thermostat_Devices[i].device_id)) - && EMS_Thermostat.product_id == EMS_ID_NONE) { - /* - myDebug_P(PSTR("* Setting Thermostat to %s (DeviceID:0x%02X ProductID:%d Version:%s)"), - Thermostat_Devices[i].model_string, - Thermostat_Devices[i].device_id, - product_id, - version); - */ - - EMS_Thermostat.model_id = Thermostat_Devices[i].model_id; - EMS_Thermostat.device_id = EMS_RxTelegram->src; - EMS_Thermostat.write_supported = Thermostat_Devices[i].write_supported; - EMS_Thermostat.product_id = product_id; - strlcpy(EMS_Thermostat.version, version, sizeof(EMS_Thermostat.version)); - - // get Thermostat values (if supported) - ems_getThermostatValues(); - } - return; - } - - - // look for Solar Modules - i = 0; - while (i < _SolarModule_Devices_max) { - if (SolarModule_Devices[i].product_id == product_id) { - typeFound = true; // we have a matching product id. i is the index. - break; - } - i++; - } - - if (typeFound) { - // its a known SM, add to list - _addDevice(EMS_MODELTYPE_SM, EMS_RxTelegram->src, product_id, version, i); - - // myDebug_P(PSTR("Solar Module support enabled.")); - EMS_SolarModule.device_id = EMS_ID_SM; - EMS_SolarModule.product_id = product_id; + if (type == EMS_DEVICE_TYPE_BOILER) { + EMS_Boiler.device_id = device_id; + EMS_Boiler.product_id = product_id; + EMS_Boiler.device_flags = flags; + EMS_Boiler.device_desc_p = device_desc_p; + strlcpy(EMS_Boiler.version, version, sizeof(EMS_Boiler.version)); + ems_getBoilerValues(); // get Boiler values that we would usually have to wait for + } else if (type == EMS_DEVICE_TYPE_THERMOSTAT) { + EMS_Thermostat.device_id = device_id; + EMS_Thermostat.device_flags = (flags & 0x7F); // remove 7th bit + EMS_Thermostat.write_supported = (flags & EMS_DEVICE_FLAG_NO_WRITE) == 0; + EMS_Thermostat.product_id = product_id; + EMS_Thermostat.device_desc_p = device_desc_p; + strlcpy(EMS_Thermostat.version, version, sizeof(EMS_Thermostat.version)); + ems_getThermostatValues(); // get Thermostat values + } else if (type == EMS_DEVICE_TYPE_SOLAR) { + EMS_SolarModule.device_id = device_id; + EMS_SolarModule.product_id = product_id; + EMS_SolarModule.device_flags = flags; + EMS_SolarModule.device_desc_p = device_desc_p; strlcpy(EMS_SolarModule.version, version, sizeof(EMS_SolarModule.version)); - - // fetch Solar Module values - ems_getSolarModuleValues(); - return; - } - - // look for heatpumps - i = 0; - while (i < _HeatPump_Devices_max) { - if (HeatPump_Devices[i].product_id == product_id) { - typeFound = true; // we have a matching product id. i is the index. - break; - } - i++; - } - - if (typeFound) { - // its a known SM, add to list - _addDevice(EMS_MODELTYPE_HP, EMS_RxTelegram->src, product_id, version, i); - - // myDebug_P(PSTR("Heat Pump support enabled.")); - EMS_HeatPump.device_id = EMS_ID_HP; - EMS_HeatPump.product_id = product_id; + ems_getSolarModuleValues(); // fetch Solar Module values + } else if (type == EMS_DEVICE_TYPE_HEATPUMP) { + EMS_HeatPump.device_id = device_id; + EMS_HeatPump.product_id = product_id; + EMS_HeatPump.device_flags = flags; + EMS_HeatPump.device_desc_p = device_desc_p; strlcpy(EMS_HeatPump.version, version, sizeof(EMS_HeatPump.version)); - return; - } - - // look for mixing devices - i = 0; - while (i < _Mixing_Devices_max) { - if (Mixing_Devices[i].product_id == product_id) { - typeFound = true; - break; - } - i++; - } - - if (typeFound) { - _addDevice(EMS_MODELTYPE_MIXING, EMS_RxTelegram->src, product_id, version, i); - ems_doReadCommand(EMS_TYPE_MMPLUSStatusMessage_HC1, EMS_RxTelegram->src); - EMS_Mixing.detected = true; - return; - } - - // finally look for the other EMS devices - i = 0; - while (i < _Other_Devices_max) { - if (Other_Devices[i].product_id == product_id) { - typeFound = true; // we have a matching product id. i is the index. - break; - } - i++; - } - - if (typeFound) { - // its a known other device, add to list - _addDevice(EMS_MODELTYPE_OTHER, EMS_RxTelegram->src, product_id, version, i); - } else { - // didn't recognize, add to list anyway - _addDevice(EMS_MODELTYPE_UNKNOWN, EMS_RxTelegram->src, product_id, version, 0); + } else if (type == EMS_DEVICE_TYPE_MIXING) { + EMS_Mixing.device_id = device_id; + EMS_Mixing.product_id = product_id; + EMS_Mixing.device_desc_p = device_desc_p; + EMS_Mixing.device_flags = flags; + EMS_Mixing.detected = true; + ems_doReadCommand(EMS_TYPE_MMPLUSStatusMessage_HC1, device_id); // fetch MM values } } @@ -2235,30 +1824,30 @@ void ems_printTxQueue() { /** * Generic function to return various settings from the thermostat + * This is called manually to fetch values which don't come from broadcast messages */ void ems_getThermostatValues() { if (!ems_getThermostatEnabled()) { return; } - uint8_t model_id = EMS_Thermostat.model_id; - uint8_t device_id = EMS_Thermostat.device_id; + uint8_t device_flags = EMS_Thermostat.device_flags; + uint8_t device_id = EMS_Thermostat.device_id; uint8_t statusMsg, opMode; - switch (model_id) { - case EMS_MODEL_RC20: + switch (device_flags) { + case EMS_DEVICE_FLAG_RC20: ems_doReadCommand(EMS_TYPE_RC20StatusMessage, device_id); // to get the temps ems_doReadCommand(EMS_TYPE_RC20Set, device_id); // to get the mode break; - case EMS_MODEL_RC30: + case EMS_DEVICE_FLAG_RC30: ems_doReadCommand(EMS_TYPE_RC30StatusMessage, device_id); // to get the temps ems_doReadCommand(EMS_TYPE_RC30Set, device_id); // to get the mode break; - case EMS_MODEL_EASY: + case EMS_DEVICE_FLAG_EASY: ems_doReadCommand(EMS_TYPE_EasyStatusMessage, device_id); break; - case EMS_MODEL_RC35: - case EMS_MODEL_ES73: + case EMS_DEVICE_FLAG_RC35: for (uint8_t hc_num = 1; hc_num <= EMS_THERMOSTAT_MAXHC; hc_num++) { if (hc_num == 1) { statusMsg = EMS_TYPE_RC35StatusMessage_HC1; @@ -2277,7 +1866,7 @@ void ems_getThermostatValues() { ems_doReadCommand(opMode, device_id); // to get the mode } break; - case EMS_MODEL_RC300: + case EMS_DEVICE_FLAG_RC300: ems_doReadCommand(EMS_TYPE_RCPLUSStatusMessage_HC1, device_id); ems_doReadCommand(EMS_TYPE_RCPLUSStatusMessage_HC2, device_id); ems_doReadCommand(EMS_TYPE_RCPLUSStatusMessage_HC3, device_id); @@ -2305,189 +1894,107 @@ void ems_getBoilerValues() { */ void ems_getSolarModuleValues() { if (ems_getSolarModuleEnabled()) { - if (EMS_SolarModule.product_id == EMS_PRODUCTID_SM10) { + if (EMS_SolarModule.device_flags == EMS_DEVICE_FLAG_SM10) { ems_doReadCommand(EMS_TYPE_SM10Monitor, EMS_ID_SM); // fetch all from SM10Monitor - } else if ((EMS_SolarModule.product_id == EMS_PRODUCTID_SM100) || (EMS_SolarModule.product_id == EMS_PRODUCTID_SM50)) { + } else if (EMS_SolarModule.device_flags == EMS_DEVICE_FLAG_SM100) { ems_doReadCommand(EMS_TYPE_SM100Monitor, EMS_ID_SM); // fetch all from SM100Monitor } } } /** - * returns current thermostat type as a string - * by looking up the product_id + * takes a device_id and tries to find the corresponding type name (e.g. Boiler) + * If it can't find it, it will use the hex value and function returns false */ -char * ems_getThermostatDescription(char * buffer, bool name_only) { - uint8_t size = 128; - if (!ems_getThermostatEnabled()) { - strlcpy(buffer, "", size); - } else { - int i = 0; - bool found = false; - char tmp[6] = {0}; +bool ems_getDeviceTypeDescription(uint8_t device_id, char * buffer) { + uint8_t i = 0; + bool typeFound = false; - // scan through known ID types - while (i < _Thermostat_Devices_max) { - if (Thermostat_Devices[i].product_id == EMS_Thermostat.product_id) { - found = true; // we have a match - break; - } - i++; + // scan through known ID types + while (i < _EMS_Devices_Types_max) { + if (EMS_Devices_Types[i].device_id == device_id) { + typeFound = true; // we have a match + break; } - - if (found) { - strlcpy(buffer, Thermostat_Devices[i].model_string, size); - if (name_only) { - return buffer; // only interested in the model name - } - } else { - strlcpy(buffer, "DeviceID:0x", size); - strlcat(buffer, _hextoa(EMS_Thermostat.device_id, tmp), size); - } - - strlcat(buffer, " (ProductID:", size); - if (EMS_Thermostat.product_id == EMS_ID_NONE) { - strlcat(buffer, "?", size); - } else { - strlcat(buffer, itoa(EMS_Thermostat.product_id, tmp, 10), size); - } - strlcat(buffer, " Version:", size); - strlcat(buffer, EMS_Thermostat.version, size); - strlcat(buffer, ")", size); + i++; } - return buffer; + if (typeFound) { + strlcpy(buffer, EMS_Devices_Types[i].device_type_string, 30); + return true; + } else { + // print as hex value + char hexbuffer[16] = {0}; + strlcpy(buffer, "0x", 30); + strlcat(buffer, _hextoa(device_id, hexbuffer), 30); + return false; + } } -/** - * returns current boiler type as a string - */ -char * ems_getBoilerDescription(char * buffer, bool name_only) { - uint8_t size = 128; - if (!ems_getBoilerEnabled()) { - strlcpy(buffer, "", size); - } else { - int i = 0; - bool found = false; - char tmp[6] = {0}; - - // scan through known ID types - while (i < _Boiler_Devices_max) { - if (Boiler_Devices[i].product_id == EMS_Boiler.product_id) { - found = true; // we have a match - break; - } - i++; - } - if (found) { - strlcpy(buffer, Boiler_Devices[i].model_string, size); - if (name_only) { - return buffer; // only interested in the model name - } - } else { - strlcpy(buffer, "DeviceID:0x", size); - strlcat(buffer, _hextoa(EMS_Boiler.device_id, tmp), size); - } - - strlcat(buffer, " (ProductID:", size); - if (EMS_Boiler.product_id == EMS_ID_NONE) { - strlcat(buffer, "?", size); - } else { - strlcat(buffer, itoa(EMS_Boiler.product_id, tmp, 10), size); - } - strlcat(buffer, " Version:", size); - strlcat(buffer, EMS_Boiler.version, size); - strlcat(buffer, ")", size); - } - - return buffer; -} /** - * returns current Solar Module type as a string + * returns current device details as a string for known thermostat,boiler,solar and heatpump */ -char * ems_getSolarModuleDescription(char * buffer, bool name_only) { - uint8_t size = 128; - if (!ems_getSolarModuleEnabled()) { - strlcpy(buffer, "", size); - } else { - int i = 0; - bool found = false; - char tmp[6] = {0}; +char * ems_getDeviceDescription(_EMS_DEVICE_TYPE device_type, char * buffer, bool name_only) { + const uint8_t size = 128; + bool enabled = false; + uint8_t device_id; + uint8_t product_id; + char * version; + const char * device_desc_p; - // scan through known ID types - while (i < _SolarModule_Devices_max) { - if (SolarModule_Devices[i].product_id == EMS_SolarModule.product_id) { - found = true; // we have a match - break; - } - i++; - } - if (found) { - strlcpy(buffer, SolarModule_Devices[i].model_string, size); - if (name_only) { - return buffer; // only interested in the model name - } - } else { - strlcpy(buffer, "DeviceID:0x", size); - strlcat(buffer, _hextoa(EMS_SolarModule.device_id, tmp), size); - } - - strlcat(buffer, " (ProductID:", size); - if (EMS_SolarModule.product_id == EMS_ID_NONE) { - strlcat(buffer, "?", size); - } else { - strlcat(buffer, itoa(EMS_SolarModule.product_id, tmp, 10), size); - } - strlcat(buffer, " Version:", size); - strlcat(buffer, EMS_SolarModule.version, size); - strlcat(buffer, ")", size); + if (device_type == EMS_DEVICE_TYPE_THERMOSTAT) { + enabled = ems_getThermostatEnabled(); + device_id = EMS_Thermostat.device_id; + product_id = EMS_Thermostat.product_id; + device_desc_p = EMS_Thermostat.device_desc_p; + version = EMS_Thermostat.version; + } else if (device_type == EMS_DEVICE_TYPE_BOILER) { + enabled = ems_getBoilerEnabled(); + device_id = EMS_Boiler.device_id; + product_id = EMS_Boiler.product_id; + device_desc_p = EMS_Boiler.device_desc_p; + version = EMS_Boiler.version; + } else if (device_type == EMS_DEVICE_TYPE_SOLAR) { + enabled = ems_getSolarModuleEnabled(); + device_id = EMS_SolarModule.device_id; + product_id = EMS_SolarModule.product_id; + device_desc_p = EMS_SolarModule.device_desc_p; + version = EMS_SolarModule.version; + } else if (device_type == EMS_DEVICE_TYPE_HEATPUMP) { + enabled = ems_getHeatPumpEnabled(); + device_id = EMS_HeatPump.device_id; + product_id = EMS_HeatPump.product_id; + device_desc_p = EMS_HeatPump.device_desc_p; + version = EMS_HeatPump.version; } - return buffer; -} - -/** - * returns current Heat Pump type as a string - */ -char * ems_getHeatPumpDescription(char * buffer, bool name_only) { - uint8_t size = 128; - if (!ems_getHeatPumpEnabled()) { + if (!enabled) { strlcpy(buffer, "", size); - } else { - int i = 0; - bool found = false; - char tmp[6] = {0}; - - // scan through known ID types - while (i < _HeatPump_Devices_max) { - if (HeatPump_Devices[i].product_id == EMS_HeatPump.product_id) { - found = true; // we have a match - break; - } - i++; - } - if (found) { - strlcpy(buffer, HeatPump_Devices[i].model_string, size); - if (name_only) { - return buffer; // only interested in the model name - } - } else { - strlcpy(buffer, "DeviceID:0x", size); - strlcat(buffer, _hextoa(EMS_HeatPump.device_id, tmp), size); - } - - strlcat(buffer, " (ProductID:", size); - if (EMS_HeatPump.product_id == EMS_ID_NONE) { - strlcat(buffer, "?", size); - } else { - strlcat(buffer, itoa(EMS_HeatPump.product_id, tmp, 10), size); - } - strlcat(buffer, " Version:", size); - strlcat(buffer, EMS_HeatPump.version, size); - strlcat(buffer, ")", size); + return buffer; } + // assume at this point we have a known device. + // get device description + if (device_desc_p == nullptr) { + strlcpy(buffer, EMS_MODELTYPE_UNKNOWN_STRING, size); + } else { + strlcpy(buffer, device_desc_p, size); + } + + if (name_only) { + return buffer; // only interested in the model name + } + + strlcat(buffer, " (DeviceID:0x", size); + char tmp[6] = {0}; + strlcat(buffer, _hextoa(device_id, tmp), size); + strlcat(buffer, " ProductID:", size); + strlcat(buffer, itoa(product_id, tmp, 10), size); + strlcat(buffer, " Version:", size); + strlcat(buffer, version, size); + strlcat(buffer, ")", size); + return buffer; } @@ -2499,26 +2006,21 @@ void ems_scanDevices() { std::list Device_Ids; // create a new list - // add boiler device_id which is always 0x08 - Device_Ids.push_back(EMS_ID_BOILER); - - // copy over thermostats - for (_Thermostat_Device tt : Thermostat_Devices) { - Device_Ids.push_back(tt.device_id); - } - - // copy over others - for (_Other_Device ot : Other_Devices) { - Device_Ids.push_back(ot.device_id); - } - - Device_Ids.push_back(EMS_ID_HP); // add heat pump - Device_Ids.push_back(EMS_ID_SM); // add solar module + Device_Ids.push_back(EMS_ID_BOILER); // UBAMaster/Boilers - 0x08 + Device_Ids.push_back(EMS_ID_HP); // HeatPump - 0x38 + Device_Ids.push_back(EMS_ID_SM); // Solar Module - 0x30 + Device_Ids.push_back(0x09); // Controllers - 0x09 + Device_Ids.push_back(0x02); // Connect - 0x02 + Device_Ids.push_back(0x48); // Gateway - 0x48 + Device_Ids.push_back(0x20); // Mixing Devices - 0x20, 0x21 + Device_Ids.push_back(0x21); // Mixing Devices - 0x20, 0x21 + Device_Ids.push_back(0x10); // Thermostats - 0x10, 0x17, 0x18 + Device_Ids.push_back(0x17); // Thermostats - 0x10, 0x17, 0x18 + Device_Ids.push_back(0x18); // Thermostats - 0x10, 0x17, 0x18 // remove duplicates and reserved IDs (like our own device) Device_Ids.sort(); - Device_Ids.unique(); - Device_Ids.remove(EMS_MODEL_NONE); + // Device_Ids.unique(); // send the read command with Version command for (uint8_t device_id : Device_Ids) { @@ -2526,81 +2028,10 @@ void ems_scanDevices() { } } -/** - * Print out all handled types - */ -void ems_printAllDevices() { - uint8_t i; - - myDebug_P(PSTR("\nThese %d devices are supported as boiler units:"), _Boiler_Devices_max); - for (i = 0; i < _Boiler_Devices_max; i++) { - myDebug_P(PSTR(" %s%s%s (DeviceID:0x%02X ProductID:%d)"), - COLOR_BOLD_ON, - Boiler_Devices[i].model_string, - COLOR_BOLD_OFF, - EMS_ID_BOILER, - Boiler_Devices[i].product_id); - } - - myDebug_P(PSTR("\nThese %d devices are supported under solar module devices:"), _SolarModule_Devices_max); - for (i = 0; i < _SolarModule_Devices_max; i++) { - myDebug_P(PSTR(" %s%s%s (DeviceID:0x%02X ProductID:%d)"), - COLOR_BOLD_ON, - SolarModule_Devices[i].model_string, - COLOR_BOLD_OFF, - EMS_ID_SM, - SolarModule_Devices[i].product_id); - } - - myDebug_P(PSTR("\nThese %d devices are supported under heat pump devices:"), _HeatPump_Devices_max); - for (i = 0; i < _HeatPump_Devices_max; i++) { - myDebug_P(PSTR(" %s%s%s (DeviceID:0x%02X ProductID:%d)"), - COLOR_BOLD_ON, - HeatPump_Devices[i].model_string, - COLOR_BOLD_OFF, - EMS_ID_HP, - HeatPump_Devices[i].product_id); - } - - myDebug_P(PSTR("\nThese %d devices are supported as other EMS devices:"), _Other_Devices_max); - for (i = 0; i < _Other_Devices_max; i++) { - myDebug_P(PSTR(" %s%s%s (DeviceID:0x%02X ProductID:%d)"), - COLOR_BOLD_ON, - Other_Devices[i].model_string, - COLOR_BOLD_OFF, - Other_Devices[i].device_id, - Other_Devices[i].product_id); - } - - myDebug_P(PSTR("\nThe following telegram type IDs are supported:")); - for (i = 0; i < _EMS_Types_max; i++) { - if ((EMS_Types[i].model_id == EMS_MODEL_ALL) || (EMS_Types[i].model_id == EMS_MODEL_UBA)) { - myDebug_P(PSTR(" type 0x%04X (%s)"), EMS_Types[i].type, EMS_Types[i].typeString); - } - } - - myDebug_P(PSTR("\nThese %d thermostat devices are supported:"), _Thermostat_Devices_max); - for (i = 0; i < _Thermostat_Devices_max; i++) { - myDebug_P(PSTR(" %s%s%s (DeviceID:0x%02X ProductID:%d) can write:%c"), - COLOR_BOLD_ON, - Thermostat_Devices[i].model_string, - COLOR_BOLD_OFF, - Thermostat_Devices[i].device_id, - Thermostat_Devices[i].product_id, - (Thermostat_Devices[i].write_supported) ? 'y' : 'n'); - } - - // print out known devices - ems_printDevices(); - - myDebug_P(PSTR("")); // newline -} - /** * print out contents of the device list that was captured */ void ems_printDevices() { - // print out the device map, which is sent from the UBA master and shows all the connected IDs char s[100]; char buffer[16] = {0}; @@ -2630,28 +2061,32 @@ void ems_printDevices() { // print out the ones we recognized if (!Devices.empty()) { - bool unknown = false; + bool have_unknowns = false; + char device_string[100]; myDebug_P(PSTR("and %d were recognized by EMS-ESP as:"), Devices.size()); - for (std::list<_Generic_Device>::iterator it = Devices.begin(); it != Devices.end(); ++it) { + for (std::list<_Detected_Device>::iterator it = Devices.begin(); it != Devices.end(); ++it) { + if ((it)->known) { + strlcpy(device_string, (it)->device_desc_p, sizeof(device_string)); + } else { + strlcpy(device_string, EMS_MODELTYPE_UNKNOWN_STRING, sizeof(device_string)); // Unknown + have_unknowns = true; + } + myDebug_P(PSTR(" %s%s%s (DeviceID:0x%02X ProductID:%d Version:%s)"), COLOR_BOLD_ON, - (it)->model_string, + device_string, COLOR_BOLD_OFF, (it)->device_id, (it)->product_id, (it)->version); - // check for unknowns - if (strcmp((it)->model_string, EMS_MODELTYPE_UNKNOWN_STRING) == 0) { - unknown = true; - } } myDebug_P(PSTR("")); // newline - if (unknown) { - myDebug_P(PSTR("You have a device is that is not known by EMS-ESP. Please report this as a GitHub issue so we can expand the EMS device library.")); + if (have_unknowns) { + myDebug_P( + PSTR("You have a device is that is not yet known by EMS-ESP. Please report this as a GitHub issue so we can expand the EMS device library.")); } - } else { myDebug_P(PSTR("No were devices recognized. This may be because Tx is disabled or failing.")); } @@ -2659,53 +2094,6 @@ void ems_printDevices() { myDebug_P(PSTR("")); // newline } -/** - * Send a command to UART Tx to Read from another device - * Read commands when sent must respond by the destination (target) immediately (or within 10ms) - */ -void ems_doReadCommand(uint16_t type, uint8_t dest, bool forceRefresh) { - // if not a valid type of boiler is not accessible then quits - if ((type == EMS_ID_NONE) || (dest == EMS_ID_NONE)) { - return; - } - - // if we're preventing all outbound traffic, quit - if (EMS_Sys_Status.emsTxDisabled) { - if (ems_getLogging() != EMS_SYS_LOGGING_NONE) { - myDebug_P(PSTR("in Listen Mode. All Tx is disabled.")); - } - return; - } - - _EMS_TxTelegram EMS_TxTelegram = EMS_TX_TELEGRAM_NEW; // create new Tx - EMS_TxTelegram.timestamp = millis(); // set timestamp - EMS_Sys_Status.txRetryCount = 0; // reset retry counter - - // see if its a known type - int i = _ems_findType(type); - - if ((ems_getLogging() == EMS_SYS_LOGGING_BASIC) || (ems_getLogging() == EMS_SYS_LOGGING_VERBOSE)) { - if (i == -1) { - myDebug_P(PSTR("Requesting type (0x%02X) from dest 0x%02X"), type, dest); - } else { - myDebug_P(PSTR("Requesting type %s(0x%02X) from dest 0x%02X"), EMS_Types[i].typeString, type, dest); - } - } - EMS_TxTelegram.action = EMS_TX_TELEGRAM_READ; // read command - EMS_TxTelegram.dest = dest; // 8th bit will be set to indicate a read - EMS_TxTelegram.offset = 0; // 0 for all data - EMS_TxTelegram.type = type; - EMS_TxTelegram.length = EMS_MIN_TELEGRAM_LENGTH; // EMS 1.0: 6 bytes long (including CRC at end), EMS+ will add 2 bytes. includes CRC - EMS_TxTelegram.dataValue = EMS_MAX_TELEGRAM_LENGTH; // for a read this is the # bytes we want back - EMS_TxTelegram.type_validate = EMS_ID_NONE; - EMS_TxTelegram.comparisonValue = 0; - EMS_TxTelegram.comparisonOffset = 0; - EMS_TxTelegram.comparisonPostRead = EMS_ID_NONE; - EMS_TxTelegram.forceRefresh = forceRefresh; // send to MQTT after a successful read - - EMS_TxQueue.push(EMS_TxTelegram); -} - /** * Send a raw telegram to the bus * telegram is a string of hex values @@ -2766,6 +2154,7 @@ void ems_sendRawTelegram(char * telegram) { */ void ems_setThermostatTemp(float temperature, uint8_t hc_num, uint8_t temptype) { if (!ems_getThermostatEnabled()) { + myDebug_P(PSTR("Thermostat not online.")); return; } @@ -2783,7 +2172,7 @@ void ems_setThermostatTemp(float temperature, uint8_t hc_num, uint8_t temptype) EMS_TxTelegram.timestamp = millis(); // set timestamp EMS_Sys_Status.txRetryCount = 0; // reset retry counter - uint8_t model_id = EMS_Thermostat.model_id; + uint8_t model = ems_getThermostatModel(); uint8_t device_id = EMS_Thermostat.device_id; EMS_TxTelegram.action = EMS_TX_TELEGRAM_WRITE; @@ -2795,38 +2184,49 @@ void ems_setThermostatTemp(float temperature, uint8_t hc_num, uint8_t temptype) hc_num, temptype); - if (model_id == EMS_MODEL_RC20) { + if (model == EMS_DEVICE_FLAG_RC20) { EMS_TxTelegram.type = EMS_TYPE_RC20Set; EMS_TxTelegram.offset = EMS_OFFSET_RC20Set_temp; EMS_TxTelegram.comparisonPostRead = EMS_TYPE_RC20StatusMessage; EMS_TxTelegram.type_validate = EMS_TxTelegram.type; - } else if (model_id == EMS_MODEL_RC10) { + } else if (model == EMS_DEVICE_FLAG_RC10) { EMS_TxTelegram.type = EMS_TYPE_RC10Set; EMS_TxTelegram.offset = EMS_OFFSET_RC10Set_temp; EMS_TxTelegram.comparisonPostRead = EMS_TYPE_RC10StatusMessage; EMS_TxTelegram.type_validate = EMS_TxTelegram.type; - } else if (model_id == EMS_MODEL_RC30) { + } else if (model == EMS_DEVICE_FLAG_RC30) { EMS_TxTelegram.type = EMS_TYPE_RC30Set; EMS_TxTelegram.offset = EMS_OFFSET_RC30Set_temp; EMS_TxTelegram.comparisonPostRead = EMS_TYPE_RC30StatusMessage; EMS_TxTelegram.type_validate = EMS_TxTelegram.type; - } else if (model_id == EMS_MODEL_RC300) { - EMS_TxTelegram.type = EMS_TYPE_RCPLUSSet; // for 3000 and 1010, e.g. 0B 10 FF (0A | 08) 01 89 2B - // check mode + } else if (model == EMS_DEVICE_FLAG_RC300) { + // check mode to determine offset if (EMS_Thermostat.hc[hc_num - 1].mode == 1) { // auto EMS_TxTelegram.offset = 0x08; // auto offset } else if (EMS_Thermostat.hc[hc_num - 1].mode == 0) { // manuaL EMS_TxTelegram.offset = 0x0A; // manual offset } - // EMS_TxTelegram.type_validate = EMS_TYPE_RCPLUSStatusMessage; // validate by reading from a different telegram + + if (hc_num == 1) { + EMS_TxTelegram.type = EMS_TYPE_RCPLUSSet; // for 3000 and 1010, e.g. 0B 10 FF (0A | 08) 01 89 2B + EMS_TxTelegram.comparisonPostRead = EMS_TYPE_RCPLUSStatusMessage_HC1; + } else if (hc_num == 2) { + EMS_TxTelegram.type = EMS_TYPE_RCPLUSSet + 1; + EMS_TxTelegram.comparisonPostRead = EMS_TYPE_RCPLUSStatusMessage_HC2; + } else if (hc_num == 3) { + EMS_TxTelegram.type = EMS_TYPE_RCPLUSSet + 2; + EMS_TxTelegram.comparisonPostRead = EMS_TYPE_RCPLUSStatusMessage_HC3; + } else if (hc_num == 4) { + EMS_TxTelegram.type = EMS_TYPE_RCPLUSSet + 3; + EMS_TxTelegram.comparisonPostRead = EMS_TYPE_RCPLUSStatusMessage_HC4; + } + EMS_TxTelegram.type_validate = EMS_ID_NONE; // validate by reading from a different telegram - EMS_TxTelegram.comparisonPostRead = EMS_TYPE_RCPLUSStatusMessage_HC1 + hc_num - 1; // after write, do a full fetch of all values - - } else if ((model_id == EMS_MODEL_RC35) || (model_id == EMS_MODEL_ES73)) { + } else if (model == EMS_DEVICE_FLAG_RC35) { switch (temptype) { case 1: // change the night temp EMS_TxTelegram.offset = EMS_OFFSET_RC35Set_temp_night; @@ -2868,18 +2268,18 @@ void ems_setThermostatTemp(float temperature, uint8_t hc_num, uint8_t temptype) EMS_TxTelegram.comparisonOffset = EMS_TxTelegram.offset; EMS_TxTelegram.comparisonValue = EMS_TxTelegram.dataValue; - EMS_TxTelegram.forceRefresh = false; // send to MQTT is done automatically in EMS_TYPE_RC*StatusMessage EMS_TxQueue.push(EMS_TxTelegram); } /** * Set the thermostat working mode - * (0=low/night, 1=manual/day, 2=auto/clock), 0xA8 on a RC20 and 0xA7 on RC30 + * 0xA8 on a RC20 and 0xA7 on RC30 * 0x01B9 for EMS+ 300/1000/3000, Auto=0xFF Manual=0x00. See https://github.com/proddy/EMS-ESP/wiki/RC3xx-Thermostats * hc_num is 1 to 4 */ void ems_setThermostatMode(uint8_t mode, uint8_t hc_num) { if (!ems_getThermostatEnabled()) { + myDebug_P(PSTR("Thermostat not online.")); return; } @@ -2893,14 +2293,14 @@ void ems_setThermostatMode(uint8_t mode, uint8_t hc_num) { return; } - uint8_t model_id = EMS_Thermostat.model_id; + uint8_t model = ems_getThermostatModel(); uint8_t device_id = EMS_Thermostat.device_id; uint8_t set_mode; // RC300/1000/3000 have different settings - if (model_id == EMS_MODEL_RC300) { + if (model == EMS_DEVICE_FLAG_RC300) { if (mode == 1) { - set_mode = 0; // manual/heat + set_mode = 0; // manual } else { set_mode = 0xFF; // auto } @@ -2908,17 +2308,13 @@ void ems_setThermostatMode(uint8_t mode, uint8_t hc_num) { set_mode = mode; } - // 0=off, 1=manual, 2=auto, 3=night, 4=day + // 0=off, 1=manual, 2=auto if (mode == 0) { myDebug_P(PSTR("Setting thermostat mode to off for heating circuit %d"), hc_num); } else if (set_mode == 1) { myDebug_P(PSTR("Setting thermostat mode to manual for heating circuit %d"), hc_num); } else if (set_mode == 2) { myDebug_P(PSTR("Setting thermostat mode to auto for heating circuit %d"), hc_num); - } else if (set_mode == 3) { - myDebug_P(PSTR("Setting thermostat mode to night for heating circuit %d"), hc_num); - } else if (set_mode == 4) { - myDebug_P(PSTR("Setting thermostat mode to day for heating circuit %d"), hc_num); } _EMS_TxTelegram EMS_TxTelegram = EMS_TX_TELEGRAM_NEW; // create new Tx @@ -2931,19 +2327,19 @@ void ems_setThermostatMode(uint8_t mode, uint8_t hc_num) { EMS_TxTelegram.dataValue = set_mode; // handle different thermostat types - if (model_id == EMS_MODEL_RC20) { + if (model == EMS_DEVICE_FLAG_RC20) { EMS_TxTelegram.type = EMS_TYPE_RC20Set; EMS_TxTelegram.offset = EMS_OFFSET_RC20Set_mode; - EMS_TxTelegram.type_validate = EMS_TxTelegram.type; + EMS_TxTelegram.type_validate = EMS_TYPE_RC20Set; EMS_TxTelegram.comparisonPostRead = EMS_TYPE_RC20StatusMessage; - } else if (model_id == EMS_MODEL_RC30) { + } else if (model == EMS_DEVICE_FLAG_RC30) { EMS_TxTelegram.type = EMS_TYPE_RC30Set; EMS_TxTelegram.offset = EMS_OFFSET_RC30Set_mode; - EMS_TxTelegram.type_validate = EMS_TxTelegram.type; + EMS_TxTelegram.type_validate = EMS_TYPE_RC30Set; EMS_TxTelegram.comparisonPostRead = EMS_TYPE_RC30StatusMessage; - } else if ((model_id == EMS_MODEL_RC35) || (model_id == EMS_MODEL_ES73)) { + } else if (model == EMS_DEVICE_FLAG_RC35) { if (hc_num == 1) { EMS_TxTelegram.type = EMS_TYPE_RC35Set_HC1; EMS_TxTelegram.comparisonPostRead = EMS_TYPE_RC35StatusMessage_HC1; @@ -2960,18 +2356,28 @@ void ems_setThermostatMode(uint8_t mode, uint8_t hc_num) { EMS_TxTelegram.offset = EMS_OFFSET_RC35Set_mode; EMS_TxTelegram.type_validate = EMS_TxTelegram.type; - } else if (model_id == EMS_MODEL_RC300) { - EMS_TxTelegram.type = EMS_TYPE_RCPLUSSet; // for 3000 and 1010, e.g. 48 10 FF 00 01 B9 00 for manual + } else if (model == EMS_DEVICE_FLAG_RC300) { EMS_TxTelegram.offset = EMS_OFFSET_RCPLUSSet_mode; - // EMS_TxTelegram.type_validate = EMS_TYPE_RCPLUSStatusMessage_HC1; // validate by reading from a different telegram - EMS_TxTelegram.type_validate = EMS_ID_NONE; // don't validate after the write - EMS_TxTelegram.comparisonPostRead = EMS_TYPE_RCPLUSStatusMessage_HC1 + hc_num - 1; // after write, do a full fetch of all values + if (hc_num == 1) { + EMS_TxTelegram.type = EMS_TYPE_RCPLUSSet; + EMS_TxTelegram.comparisonPostRead = EMS_TYPE_RCPLUSStatusMessage_HC1; + } else if (hc_num == 2) { + EMS_TxTelegram.type = EMS_TYPE_RCPLUSSet + 1; + EMS_TxTelegram.comparisonPostRead = EMS_TYPE_RCPLUSStatusMessage_HC2; + } else if (hc_num == 3) { + EMS_TxTelegram.type = EMS_TYPE_RCPLUSSet + 2; + EMS_TxTelegram.comparisonPostRead = EMS_TYPE_RCPLUSStatusMessage_HC3; + } else if (hc_num == 4) { + EMS_TxTelegram.type = EMS_TYPE_RCPLUSSet + 3; + EMS_TxTelegram.comparisonPostRead = EMS_TYPE_RCPLUSStatusMessage_HC4; + } + + EMS_TxTelegram.type_validate = EMS_ID_NONE; // don't validate after the write } EMS_TxTelegram.comparisonOffset = EMS_TxTelegram.offset; EMS_TxTelegram.comparisonValue = EMS_TxTelegram.dataValue; - EMS_TxTelegram.forceRefresh = false; // send to MQTT is done automatically in 0xA8 process EMS_TxQueue.push(EMS_TxTelegram); } @@ -2996,13 +2402,12 @@ void ems_setWarmWaterTemp(uint8_t temperature) { EMS_TxTelegram.type = EMS_TYPE_UBAParameterWW; EMS_TxTelegram.offset = EMS_OFFSET_UBAParameterWW_wwtemp; EMS_TxTelegram.length = EMS_MIN_TELEGRAM_LENGTH; - EMS_TxTelegram.dataValue = temperature; // value to compare against. must be a single int + EMS_TxTelegram.dataValue = temperature; // int value to compare against EMS_TxTelegram.type_validate = EMS_TYPE_UBAParameterWW; // validate EMS_TxTelegram.comparisonOffset = EMS_OFFSET_UBAParameterWW_wwtemp; EMS_TxTelegram.comparisonValue = temperature; EMS_TxTelegram.comparisonPostRead = EMS_TYPE_UBAParameterWW; - EMS_TxTelegram.forceRefresh = false; // no need to send since this is done by 0x33 process EMS_TxQueue.push(EMS_TxTelegram); } @@ -3022,13 +2427,12 @@ void ems_setFlowTemp(uint8_t temperature) { EMS_TxTelegram.type = EMS_TYPE_UBASetPoints; EMS_TxTelegram.offset = EMS_OFFSET_UBASetPoints_flowtemp; EMS_TxTelegram.length = EMS_MIN_TELEGRAM_LENGTH; - EMS_TxTelegram.dataValue = temperature; // value to compare against. must be a single int + EMS_TxTelegram.dataValue = temperature; // int value to compare against EMS_TxTelegram.type_validate = EMS_TYPE_UBASetPoints; // validate EMS_TxTelegram.comparisonOffset = EMS_OFFSET_UBASetPoints_flowtemp; EMS_TxTelegram.comparisonValue = temperature; EMS_TxTelegram.comparisonPostRead = EMS_TYPE_UBASetPoints; - EMS_TxTelegram.forceRefresh = false; EMS_TxQueue.push(EMS_TxTelegram); } @@ -3113,7 +2517,6 @@ void ems_setWarmTapWaterActivated(bool activated) { EMS_TxTelegram.comparisonOffset = 0; // 1st byte EMS_TxTelegram.comparisonValue = (activated ? 0 : 1); // value is 1 if in Test mode (not activated) EMS_TxTelegram.comparisonPostRead = EMS_TxTelegram.type; - EMS_TxTelegram.forceRefresh = true; // send new value to MQTT after successful write // create header EMS_TxTelegram.data[0] = EMS_ID_ME; // src @@ -3213,3 +2616,322 @@ void ems_testTelegram(uint8_t test_num) { myDebug_P(PSTR("Firmware not compiled with test data. Use -DTESTS")); #endif } + +/** + * Recognized EMS types and the functions they call to process the telegrams + */ +const _EMS_Type EMS_Types[] = { + + // common + {EMS_DEVICE_UPDATE_FLAG_NONE, EMS_TYPE_Version, "Version", _process_Version}, + {EMS_DEVICE_UPDATE_FLAG_NONE, EMS_TYPE_UBADevices, "UBADevices", _process_UBADevices}, + {EMS_DEVICE_UPDATE_FLAG_NONE, EMS_TYPE_RCTime, "RCTime", _process_RCTime}, + {EMS_DEVICE_UPDATE_FLAG_THERMOSTAT, EMS_TYPE_RCOutdoorTempMessage, "RCOutdoorTempMessage", _process_RCOutdoorTempMessage}, + + // UBA/Boiler + {EMS_DEVICE_UPDATE_FLAG_BOILER, EMS_TYPE_UBAMonitorFast, "UBAMonitorFast", _process_UBAMonitorFast}, + {EMS_DEVICE_UPDATE_FLAG_BOILER, EMS_TYPE_UBAMonitorSlow, "UBAMonitorSlow", _process_UBAMonitorSlow}, + {EMS_DEVICE_UPDATE_FLAG_BOILER, EMS_TYPE_UBAMonitorWWMessage, "UBAMonitorWWMessage", _process_UBAMonitorWWMessage}, + {EMS_DEVICE_UPDATE_FLAG_BOILER, EMS_TYPE_UBAParameterWW, "UBAParameterWW", _process_UBAParameterWW}, + {EMS_DEVICE_UPDATE_FLAG_BOILER, EMS_TYPE_UBATotalUptimeMessage, "UBATotalUptimeMessage", _process_UBATotalUptimeMessage}, + {EMS_DEVICE_UPDATE_FLAG_BOILER, EMS_TYPE_UBAParametersMessage, "UBAParametersMessage", _process_UBAParametersMessage}, + {EMS_DEVICE_UPDATE_FLAG_BOILER, EMS_TYPE_UBASetPoints, "UBASetPoints", _process_SetPoints}, + + // UBA/Boiler EMS+ + {EMS_DEVICE_UPDATE_FLAG_BOILER, EMS_TYPE_UBAOutdoorTemp, "UBAOutdoorTemp", _process_UBAOutdoorTemp}, + {EMS_DEVICE_UPDATE_FLAG_BOILER, EMS_TYPE_UBAMonitorFast2, "UBAMonitorFast2", _process_UBAMonitorFast2}, + {EMS_DEVICE_UPDATE_FLAG_BOILER, EMS_TYPE_UBAMonitorSlow2, "UBAMonitorSlow2", _process_UBAMonitorSlow2}, + + // Solar Module devices + {EMS_DEVICE_UPDATE_FLAG_SOLAR, EMS_TYPE_SM10Monitor, "SM10Monitor", _process_SM10Monitor}, + {EMS_DEVICE_UPDATE_FLAG_SOLAR, EMS_TYPE_SM100Monitor, "SM100Monitor", _process_SM100Monitor}, + {EMS_DEVICE_UPDATE_FLAG_SOLAR, EMS_TYPE_SM100Status, "SM100Status", _process_SM100Status}, + {EMS_DEVICE_UPDATE_FLAG_SOLAR, EMS_TYPE_SM100Status2, "SM100Status2", _process_SM100Status2}, + {EMS_DEVICE_UPDATE_FLAG_SOLAR, EMS_TYPE_SM100Energy, "SM100Energy", _process_SM100Energy}, + {EMS_DEVICE_UPDATE_FLAG_SOLAR, EMS_TYPE_ISM1StatusMessage, "ISM1StatusMessage", _process_ISM1StatusMessage}, + {EMS_DEVICE_UPDATE_FLAG_SOLAR, EMS_TYPE_ISM1Set, "ISM1Set", _process_ISM1Set}, + + // heatpumps + {EMS_DEVICE_UPDATE_FLAG_HEATPUMP, EMS_TYPE_HPMonitor1, "HeatPumpMonitor1", _process_HPMonitor1}, + {EMS_DEVICE_UPDATE_FLAG_HEATPUMP, EMS_TYPE_HPMonitor2, "HeatPumpMonitor2", _process_HPMonitor2}, + + // RC10 + {EMS_DEVICE_UPDATE_FLAG_THERMOSTAT, EMS_TYPE_RC10Set, "RC10Set", _process_RC10Set}, + {EMS_DEVICE_UPDATE_FLAG_THERMOSTAT, EMS_TYPE_RC10StatusMessage, "RC10StatusMessage", _process_RC10StatusMessage}, + + // RC20 and RC20RF + {EMS_DEVICE_UPDATE_FLAG_THERMOSTAT, EMS_TYPE_RC20Set, "RC20Set", _process_RC20Set}, + {EMS_DEVICE_UPDATE_FLAG_THERMOSTAT, EMS_TYPE_RC20StatusMessage, "RC20StatusMessage", _process_RC20StatusMessage}, + + // RC30 + {EMS_DEVICE_UPDATE_FLAG_THERMOSTAT, EMS_TYPE_RC30Set, "RC30Set", _process_RC30Set}, + {EMS_DEVICE_UPDATE_FLAG_THERMOSTAT, EMS_TYPE_RC30StatusMessage, "RC30StatusMessage", _process_RC30StatusMessage}, + + // RC35 and ES71 + {EMS_DEVICE_UPDATE_FLAG_THERMOSTAT, EMS_TYPE_RC35Set_HC1, "RC35Set_HC1", _process_RC35Set}, + {EMS_DEVICE_UPDATE_FLAG_THERMOSTAT, EMS_TYPE_RC35StatusMessage_HC1, "RC35StatusMessage_HC1", _process_RC35StatusMessage}, + {EMS_DEVICE_UPDATE_FLAG_THERMOSTAT, EMS_TYPE_RC35Set_HC2, "RC35Set_HC2", _process_RC35Set}, + {EMS_DEVICE_UPDATE_FLAG_THERMOSTAT, EMS_TYPE_RC35StatusMessage_HC2, "RC35StatusMessage_HC2", _process_RC35StatusMessage}, + {EMS_DEVICE_UPDATE_FLAG_THERMOSTAT, EMS_TYPE_RC35Set_HC3, "RC35Set_HC2", _process_RC35Set}, + {EMS_DEVICE_UPDATE_FLAG_THERMOSTAT, EMS_TYPE_RC35StatusMessage_HC3, "RC35StatusMessage_HC3", _process_RC35StatusMessage}, + {EMS_DEVICE_UPDATE_FLAG_THERMOSTAT, EMS_TYPE_RC35Set_HC4, "RC35Set_HC4", _process_RC35Set}, + {EMS_DEVICE_UPDATE_FLAG_THERMOSTAT, EMS_TYPE_RC35StatusMessage_HC4, "RC35StatusMessage_HC4", _process_RC35StatusMessage}, + + // Easy + {EMS_DEVICE_UPDATE_FLAG_THERMOSTAT, EMS_TYPE_EasyStatusMessage, "EasyStatusMessage", _process_EasyStatusMessage}, + + // Nefit 1010, RC300, RC310 (EMS Plus) + {EMS_DEVICE_UPDATE_FLAG_THERMOSTAT, EMS_TYPE_RCPLUSStatusMessage_HC1, "RCPLUSStatusMessage_HC1", _process_RCPLUSStatusMessage}, + {EMS_DEVICE_UPDATE_FLAG_THERMOSTAT, EMS_TYPE_RCPLUSStatusMessage_HC2, "RCPLUSStatusMessage_HC2", _process_RCPLUSStatusMessage}, + {EMS_DEVICE_UPDATE_FLAG_THERMOSTAT, EMS_TYPE_RCPLUSStatusMessage_HC3, "RCPLUSStatusMessage_HC3", _process_RCPLUSStatusMessage}, + {EMS_DEVICE_UPDATE_FLAG_THERMOSTAT, EMS_TYPE_RCPLUSStatusMessage_HC4, "RCPLUSStatusMessage_HC4", _process_RCPLUSStatusMessage}, + {EMS_DEVICE_UPDATE_FLAG_THERMOSTAT, EMS_TYPE_RCPLUSSet, "RCPLUSSetMessage", _process_RCPLUSSetMessage}, + {EMS_DEVICE_UPDATE_FLAG_THERMOSTAT, EMS_TYPE_RCPLUSStatusMode, "RCPLUSStatusMode", _process_RCPLUSStatusMode}, + + // Junkers FR10 + {EMS_DEVICE_UPDATE_FLAG_THERMOSTAT, EMS_TYPE_JunkersStatusMessage, "JunkersStatusMessage", _process_JunkersStatusMessage}, + + // Mixing devices + {EMS_DEVICE_UPDATE_FLAG_MIXING, EMS_TYPE_MMPLUSStatusMessage_HC1, "MMPLUSStatusMessage_HC1", _process_MMPLUSStatusMessage}, + {EMS_DEVICE_UPDATE_FLAG_MIXING, EMS_TYPE_MMPLUSStatusMessage_HC2, "MMPLUSStatusMessage_HC2", _process_MMPLUSStatusMessage} + +}; + +// calculate sizes of arrays at compile time +uint8_t _EMS_Types_max = ArraySize(EMS_Types); + +/** + * Find the pointer to the EMS_Types array for a given type ID + * or -1 if not found + */ +int8_t _ems_findType(uint16_t type) { + uint8_t i = 0; + bool typeFound = false; + // scan through known ID types + while (i < _EMS_Types_max) { + if (EMS_Types[i].type == type) { + typeFound = true; // we have a match + break; + } + i++; + } + + return (typeFound ? i : -1); +} + +/** + * print detailed telegram + * and then call its callback if there is one defined + */ +void _ems_processTelegram(_EMS_RxTelegram * EMS_RxTelegram) { + // print out the telegram for verbose mode + if (EMS_Sys_Status.emsLogging >= EMS_SYS_LOGGING_THERMOSTAT) { + _printMessage(EMS_RxTelegram); + } + + // ignore telegrams that don't have any data + if (EMS_RxTelegram->data_length == 0) { + return; + } + + // we're only interested in broadcast messages (dest is 0x00) or ones for us (dest is 0x0B) + uint8_t dest = EMS_RxTelegram->dest; + if ((dest != EMS_ID_NONE) && (dest != EMS_ID_ME)) { + return; + } + + // see if we recognize the type first by scanning our known EMS types list + uint16_t type = EMS_RxTelegram->type; + int8_t i = _ems_findType(type); + if (i == -1) { + return; // not found + } + + // if it's a common type (across ems devices) or something specifically for us process it. + // dest will be EMS_ID_NONE and offset 0x00 for a broadcast message + if ((EMS_Types[i].processType_cb) != nullptr) { + // print non-verbose message + if (EMS_Sys_Status.emsLogging == EMS_SYS_LOGGING_BASIC) { + myDebug_P(PSTR("<--- %s(0x%02X)"), EMS_Types[i].typeString, type); + } + // call callback function to process the telegram + (void)EMS_Types[i].processType_cb(EMS_RxTelegram); + + // see if we need to flag something has changed + ems_Device_add_flags(EMS_Types[i].device_flag); + } + + EMS_Sys_Status.emsTxStatus = EMS_TX_STATUS_IDLE; +} + +/** + * deciphers the telegram packet, which has already been checked for valid CRC and has a complete header + * length is only data bytes, excluding the BRK + * We only remove from the Tx queue if the read or write was successful + */ +void _processType(_EMS_RxTelegram * EMS_RxTelegram) { + uint8_t * telegram = EMS_RxTelegram->telegram; + + // if its an echo of ourselves from the master UBA, ignore. This should never happen mind you + if (EMS_RxTelegram->src == EMS_ID_ME) { + if (EMS_Sys_Status.emsLogging == EMS_SYS_LOGGING_JABBER) + _debugPrintTelegram("echo: ", EMS_RxTelegram, COLOR_WHITE); + return; + } + + // if its a broadcast and we didn't just send anything, process it and exit + if (EMS_Sys_Status.emsTxStatus == EMS_TX_STATUS_IDLE) { + _ems_processTelegram(EMS_RxTelegram); + return; + } + + // release the lock on the TxQueue + EMS_Sys_Status.emsTxStatus = EMS_TX_STATUS_IDLE; + + // at this point we can assume TxStatus was EMS_TX_STATUS_WAIT as we just sent a read or validate telegram + // for READ or VALIDATE the dest (telegram[1]) is always us, so check for this + // and if not we probably didn't get any response so remove the last Tx from the queue and process the telegram anyway + if ((telegram[1] & 0x7F) != EMS_ID_ME) { + _removeTxQueue(); + _ems_processTelegram(EMS_RxTelegram); + return; + } + + // first double check we actually have something in the Tx queue that we're waiting upon + if (EMS_TxQueue.isEmpty()) { + _ems_processTelegram(EMS_RxTelegram); + return; + } + + // get the Tx telegram we just sent + _EMS_TxTelegram EMS_TxTelegram = EMS_TxQueue.first(); + + // check action + // if READ, match the current inbound telegram to what we just sent + // if WRITE, should not happen + // if VALIDATE, check the contents + if (EMS_TxTelegram.action == EMS_TX_TELEGRAM_READ) { + // remove MSB from src/dest + if (((EMS_RxTelegram->src & 0x7F) == (EMS_TxTelegram.dest & 0x7F)) && (EMS_RxTelegram->type == EMS_TxTelegram.type)) { + // all checks out, read was successful, remove tx from queue and continue to process telegram + _removeTxQueue(); + EMS_Sys_Status.emsRxPgks++; // increment Rx happy counter + EMS_Sys_Status.emsTxCapable = true; // we're able to transmit a telegram on the Tx + } else { + // read not OK, we didn't get back a telegram we expected. + // first see if we got a response back from the sender saying its an unknown command + if (EMS_RxTelegram->data_length == 0) { + _removeTxQueue(); + } else { + // leave on queue and try again, but continue to process what we received as it may be important + EMS_Sys_Status.txRetryCount++; + // if retried too many times, give up and remove it + if (EMS_Sys_Status.txRetryCount >= TX_WRITE_TIMEOUT_COUNT) { + if (EMS_Sys_Status.emsLogging >= EMS_SYS_LOGGING_BASIC) { + myDebug_P(PSTR("-> Read failed. Giving up and removing write from queue")); + } + _removeTxQueue(); + } else { + if (EMS_Sys_Status.emsLogging >= EMS_SYS_LOGGING_BASIC) { + myDebug_P(PSTR("-> Read failed. Retrying (%d/%d)..."), EMS_Sys_Status.txRetryCount, TX_WRITE_TIMEOUT_COUNT); + } + } + } + } + _ems_processTelegram(EMS_RxTelegram); // process it always + } + + if (EMS_TxTelegram.action == EMS_TX_TELEGRAM_WRITE) { + // should not get here, since this is handled earlier receiving a 01 or 04 + myDebug_P(PSTR("-> Write error - panic! should never get here")); + } + + if (EMS_TxTelegram.action == EMS_TX_TELEGRAM_VALIDATE) { + // this is a read telegram which we use to validate the last write + + // data block starts at position 5 for EMS1.0 and 6 for EMS2.0. + // See https://github.com/proddy/EMS-ESP/wiki/RC3xx-Thermostats + uint8_t dataReceived = (EMS_RxTelegram->emsplus) ? telegram[6] : telegram[4]; + + if (EMS_TxTelegram.comparisonValue == dataReceived) { + // validate was successful, the write changed the value + _removeTxQueue(); // now we can remove the Tx validate command the queue + if (EMS_Sys_Status.emsLogging >= EMS_SYS_LOGGING_BASIC) { + myDebug_P(PSTR("-> Validate confirmed, last Write to 0x%02X was successful"), EMS_TxTelegram.dest); + } + // follow up with the post read command + ems_doReadCommand(EMS_TxTelegram.comparisonPostRead, EMS_TxTelegram.dest); + } else { + // write failed + if (EMS_Sys_Status.emsLogging >= EMS_SYS_LOGGING_BASIC) { + myDebug_P(PSTR("-> Write failed. Compared set value 0x%02X with received value of 0x%02X"), EMS_TxTelegram.comparisonValue, dataReceived); + } + if (++EMS_Sys_Status.txRetryCount > TX_WRITE_TIMEOUT_COUNT) { + if (EMS_Sys_Status.emsLogging >= EMS_SYS_LOGGING_BASIC) { + myDebug_P(PSTR("-> Write failed. Giving up, removing from queue")); + } + _removeTxQueue(); + } else { + // retry, turn the validate back into a write and try again + if (EMS_Sys_Status.emsLogging >= EMS_SYS_LOGGING_BASIC) { + myDebug_P(PSTR("-> Write didn't work, retrying (%d/%d)..."), EMS_Sys_Status.txRetryCount, TX_WRITE_TIMEOUT_COUNT); + } + EMS_TxTelegram.action = EMS_TX_TELEGRAM_WRITE; + EMS_TxTelegram.dataValue = EMS_TxTelegram.comparisonValue; // restore old value + EMS_TxTelegram.offset = EMS_TxTelegram.comparisonOffset; // restore old value + EMS_TxTelegram.type = EMS_TxTelegram.type_validate; // restore old value, we swapped them to save the original type + + EMS_TxQueue.shift(); // remove validate from queue + EMS_TxQueue.unshift(EMS_TxTelegram); // add back to queue making it next in line + } + } + } + + ems_tx_pollAck(); // send Acknowledgement back to free the EMS bus since we have the telegram +} + +/** + * Send a command to UART Tx to Read from another device + * Read commands when sent must respond by the destination (target) immediately (or within 10ms) + */ +void ems_doReadCommand(uint16_t type, uint8_t dest) { + // if not a valid type of boiler is not accessible then quits + if ((type == EMS_ID_NONE) || (dest == EMS_ID_NONE)) { + return; + } + + // if we're preventing all outbound traffic, quit + if (EMS_Sys_Status.emsTxDisabled) { + if (ems_getLogging() != EMS_SYS_LOGGING_NONE) { + myDebug_P(PSTR("in Listen Mode. All Tx is disabled.")); + } + return; + } + + _EMS_TxTelegram EMS_TxTelegram = EMS_TX_TELEGRAM_NEW; // create new Tx + EMS_TxTelegram.timestamp = millis(); // set timestamp + EMS_Sys_Status.txRetryCount = 0; // reset retry counter + + // see if its a known type + int8_t i = _ems_findType(type); + + if ((ems_getLogging() == EMS_SYS_LOGGING_BASIC) || (ems_getLogging() == EMS_SYS_LOGGING_VERBOSE)) { + if (i == -1) { + myDebug_P(PSTR("Requesting type (0x%02X) from dest 0x%02X"), type, dest); + } else { + myDebug_P(PSTR("Requesting type %s(0x%02X) from dest 0x%02X"), EMS_Types[i].typeString, type, dest); + } + } + EMS_TxTelegram.action = EMS_TX_TELEGRAM_READ; // read command + EMS_TxTelegram.dest = dest; // 8th bit will be set to indicate a read + EMS_TxTelegram.offset = 0; // 0 for all data + EMS_TxTelegram.type = type; + EMS_TxTelegram.length = EMS_MIN_TELEGRAM_LENGTH; // EMS 1.0: 6 bytes long (including CRC at end), EMS+ will add 2 bytes. includes CRC + EMS_TxTelegram.dataValue = EMS_MAX_TELEGRAM_LENGTH; // for a read this is the # bytes we want back + EMS_TxTelegram.type_validate = EMS_ID_NONE; + EMS_TxTelegram.comparisonValue = 0; + EMS_TxTelegram.comparisonOffset = 0; + EMS_TxTelegram.comparisonPostRead = EMS_ID_NONE; + + EMS_TxQueue.push(EMS_TxTelegram); +} diff --git a/src/ems.h b/src/ems.h index 2e93f0d17..03f619291 100644 --- a/src/ems.h +++ b/src/ems.h @@ -19,29 +19,18 @@ #define EMS_ID_NONE 0x00 // used as a dest in broadcast messages and empty device IDs -// Fixed EMS IDs -#define EMS_ID_ME 0x0B // our device, hardcoded as the "Service Key" -#define EMS_ID_BOILER 0x08 // all UBA Boilers have 0x08 -#define EMS_ID_SM 0x30 // Solar Module SM10, SM100 and ISM1 -#define EMS_ID_HP 0x38 // HeatPump -#define EMS_ID_GATEWAY 0x48 // KM200 Web Gateway - -// Product IDs -#define EMS_PRODUCTID_SM10 73 // SM10 solar module -#define EMS_PRODUCTID_SM50 162 // SM50 solar module -#define EMS_PRODUCTID_SM100 163 // SM100 solar module -#define EMS_PRODUCTID_ISM1 101 // Junkers ISM1 solar module - #define EMS_MIN_TELEGRAM_LENGTH 6 // minimal length for a validation telegram, including CRC #define EMS_MAX_TELEGRAM_LENGTH 32 // max length of a telegram, including CRC, for Rx and Tx. // default values for null values -#define EMS_VALUE_INT_ON 1 // boolean true -#define EMS_VALUE_INT_OFF 0 // boolean false +#define EMS_VALUE_BOOL_ON 0x01 // boolean true +#define EMS_VALUE_BOOL_ON2 0xFF // boolean true, EMS sometimes uses 0xFF for TRUE +#define EMS_VALUE_BOOL_OFF 0x00 // boolean false #define EMS_VALUE_INT_NOTSET 0xFF // for 8-bit unsigned ints/bytes #define EMS_VALUE_SHORT_NOTSET -32768 // for 2-byte signed shorts #define EMS_VALUE_USHORT_NOTSET 0x8000 // for 2-byte unsigned shorts #define EMS_VALUE_LONG_NOTSET 0xFFFFFF // for 3-byte longs +#define EMS_VALUE_BOOL_NOTSET 0xFE // random number that's not 0, 1 or FF // thermostat specific #define EMS_THERMOSTAT_MAXHC 4 // max number of heating circuits @@ -49,6 +38,30 @@ #define EMS_THERMOSTAT_WRITE_YES true #define EMS_THERMOSTAT_WRITE_NO false +// Device Flags +#define EMS_DEVICE_FLAG_NONE 0 // no flags set +#define EMS_DEVICE_FLAG_SM10 10 // solar module1 +#define EMS_DEVICE_FLAG_SM100 11 // solar module2 + +// group flags specific for thermostats +#define EMS_DEVICE_FLAG_NO_WRITE 0x80 // top bit set if write not supported +#define EMS_DEVICE_FLAG_EASY 1 +#define EMS_DEVICE_FLAG_RC10 2 +#define EMS_DEVICE_FLAG_RC20 3 +#define EMS_DEVICE_FLAG_RC30 4 +#define EMS_DEVICE_FLAG_RC35 5 +#define EMS_DEVICE_FLAG_RC300 6 +#define EMS_DEVICE_FLAG_JUNKERS 7 + +typedef enum { + EMS_THERMOSTAT_MODE_UNKNOWN, + EMS_THERMOSTAT_MODE_OFF, + EMS_THERMOSTAT_MODE_MANUAL, + EMS_THERMOSTAT_MODE_AUTO, + EMS_THERMOSTAT_MODE_NIGHT, + EMS_THERMOSTAT_MODE_DAY +} _EMS_THERMOSTAT_MODE; + // trigger settings to determine if hot tap water or the heating is active #define EMS_BOILER_BURNPOWER_TAPWATER 100 #define EMS_BOILER_SELFLOWTEMP_HEATING 30 // was 70, changed to 30 for https://github.com/proddy/EMS-ESP/issues/193 @@ -63,16 +76,6 @@ #define EMS_SYS_DEVICEMAP_LENGTH 15 // size of the 0x07 telegram data part which stores all active EMS devices -// define the model types -// which get rendered to html colors in the web interface in file custom.js in function listCustomStats() -#define EMS_MODELTYPE_BOILER 1 // success color -#define EMS_MODELTYPE_THERMOSTAT 2 // info color -#define EMS_MODELTYPE_SM 3 // warning color -#define EMS_MODELTYPE_HP 4 // success color -#define EMS_MODELTYPE_OTHER 5 // no color -#define EMS_MODELTYPE_UNKNOWN 6 // no color -#define EMS_MODELTYPE_MIXING 7 - #define EMS_MODELTYPE_UNKNOWN_STRING "unknown?" // model type text to use when discovering an unknown device /* EMS UART transfer status */ @@ -105,6 +108,7 @@ typedef enum { typedef enum { EMS_SYS_LOGGING_NONE, // no messages EMS_SYS_LOGGING_RAW, // raw data mode + EMS_SYS_LOGGING_WATCH, // watch a specific type ID EMS_SYS_LOGGING_BASIC, // only basic read/write messages EMS_SYS_LOGGING_THERMOSTAT, // only telegrams sent from thermostat EMS_SYS_LOGGING_SOLARMODULE, // only telegrams sent from thermostat @@ -121,7 +125,8 @@ typedef struct { uint16_t emxCrcErr; // CRC errors bool emsPollEnabled; // flag enable the response to poll messages _EMS_SYS_LOGGING emsLogging; // logging - bool emsRefreshed; // fresh data, needs to be pushed out to MQTT + uint16_t emsLogging_typeID; // the typeID to watch + uint8_t emsRefreshedFlags; // fresh data, needs to be pushed out to MQTT bool emsBusConnected; // is there an active bus uint32_t emsRxTimestamp; // timestamp of last EMS message received uint32_t emsPollFrequency; // time between EMS polls @@ -146,24 +151,23 @@ typedef struct { uint8_t comparisonValue; // value to compare against during a validate command uint8_t comparisonOffset; // offset of where the byte is we want to compare too during validation uint16_t comparisonPostRead; // after a successful write, do a read from this type ID - bool forceRefresh; // should we send to MQTT after a successful Tx? - uint32_t timestamp; // when created + unsigned long timestamp; // when created uint8_t data[EMS_MAX_TELEGRAM_LENGTH]; } _EMS_TxTelegram; // The Rx receive package typedef struct { - uint32_t timestamp; // timestamp from millis() - uint8_t * telegram; // the full data package - uint8_t data_length; // length in bytes of the data - uint8_t length; // full length of the complete telegram - uint8_t src; // source ID - uint8_t dest; // destination ID - uint16_t type; // type ID as a double byte to support EMS+ - uint8_t offset; // offset - uint8_t * data; // pointer to where telegram data starts - bool emsplus; // true if ems+/ems 2.0 - uint8_t emsplus_type; // FF, F7 or F9 + unsigned long timestamp; // timestamp from millis() + uint8_t * telegram; // the full data package + uint8_t data_length; // length in bytes of the data + uint8_t length; // full length of the complete telegram + uint8_t src; // source ID + uint8_t dest; // destination ID + uint16_t type; // type ID as a 2-byte to support EMS+ + uint8_t offset; // offset + uint8_t * data; // pointer to where telegram data starts + bool emsplus; // true if ems+/ems 2.0 + uint8_t emsplus_type; // FF, F7 or F9 } _EMS_RxTelegram; // default empty Tx, must match struct @@ -178,64 +182,71 @@ const _EMS_TxTelegram EMS_TX_TELEGRAM_NEW = { 0, // comparisonValue 0, // comparisonOffset EMS_ID_NONE, // comparisonPostRead - false, // forceRefresh 0, // timestamp {0x00} // data }; -// where defintions are stored -typedef struct { - uint8_t product_id; - char model_string[70]; -} _Boiler_Device; +// flags for triggering changes when EMS data is received +typedef enum : uint8_t { + EMS_DEVICE_UPDATE_FLAG_NONE = 0, + EMS_DEVICE_UPDATE_FLAG_BOILER = (1 << 0), + EMS_DEVICE_UPDATE_FLAG_THERMOSTAT = (1 << 1), + EMS_DEVICE_UPDATE_FLAG_MIXING = (1 << 2), + EMS_DEVICE_UPDATE_FLAG_SOLAR = (1 << 3), + EMS_DEVICE_UPDATE_FLAG_HEATPUMP = (1 << 4) +} _EMS_DEVICE_UPDATE_FLAG; -typedef struct { - uint8_t product_id; - char model_string[50]; -} _SolarModule_Device; +typedef enum : uint8_t { + EMS_DEVICE_TYPE_NONE = 0, + EMS_DEVICE_TYPE_SERVICEKEY, + EMS_DEVICE_TYPE_BOILER, + EMS_DEVICE_TYPE_THERMOSTAT, + EMS_DEVICE_TYPE_MIXING, + EMS_DEVICE_TYPE_SOLAR, + EMS_DEVICE_TYPE_HEATPUMP, + EMS_DEVICE_TYPE_GATEWAY, + EMS_DEVICE_TYPE_OTHER, + EMS_DEVICE_TYPE_SWITCH, + EMS_DEVICE_TYPE_CONTROLLER, + EMS_DEVICE_TYPE_CONNECT, + EMS_DEVICE_TYPE_UNKNOWN +} _EMS_DEVICE_TYPE; +// to store all known EMS devices to date typedef struct { - uint8_t product_id; - uint8_t device_id; - char model_string[50]; -} _Other_Device; + uint8_t product_id; + _EMS_DEVICE_TYPE type; + char device_desc[100]; + uint8_t flags; +} _EMS_Device; +// to store mapping of device_ids to their string name typedef struct { - uint8_t product_id; - char model_string[50]; -} _HeatPump_Device; + uint8_t device_id; + _EMS_DEVICE_TYPE device_type; + char device_type_string[30]; +} _EMS_Device_Types; +// for storing all recognised EMS devices typedef struct { - uint8_t model_id; - uint8_t product_id; - uint8_t device_id; - char model_string[50]; - bool write_supported; -} _Thermostat_Device; - -typedef struct { - uint8_t product_id; - char model_string[50]; -} _Mixing_Device; - -// for consolidating all types -typedef struct { - uint8_t model_type; // 1=boiler, 2=thermostat, 3=sm, 4=other, 5=unknown - uint8_t product_id; - uint8_t device_id; - char version[10]; - char model_string[50]; -} _Generic_Device; - + _EMS_DEVICE_TYPE device_type; // type (see above) + uint8_t product_id; // product id + uint8_t device_id; // device_id + const char * device_desc_p; // pointer to description string in EMS_Devices table + char version[10]; // the version number XX.XX + bool known; // is this a known device? +} _Detected_Device; /* * Telegram package defintions */ typedef struct { // settings - uint8_t device_id; // this is typically always 0x08 - uint8_t product_id; - char version[10]; + uint8_t device_id; // this is typically always 0x08 + uint8_t device_flags; + const char * device_desc_p; + uint8_t product_id; + char version[10]; // UBAParameterWW uint8_t wWActivated; // Warm Water activated @@ -295,30 +306,19 @@ typedef struct { * Telegram package defintions for Other EMS devices */ typedef struct { - uint8_t device_id; // the device ID of the Heat Pump (e.g. 0x30) - uint8_t model_id; // Solar Module / Heat Pump model - uint8_t product_id; - char version[10]; - - uint8_t HPModulation; // heatpump modulation in % - uint8_t HPSpeed; // speed 0-100 % + uint8_t device_id; // the device ID of the Heat Pump (e.g. 0x30) + uint8_t device_flags; + const char * device_desc_p; + uint8_t product_id; + char version[10]; + uint8_t HPModulation; // heatpump modulation in % + uint8_t HPSpeed; // speed 0-100 % } _EMS_HeatPump; +// Mixing Module per HC typedef struct { - uint8_t device_id; - uint8_t model_id; - uint8_t product_id; - char version[10]; -} _EMS_Other; - -typedef struct { - uint8_t device_id; - uint8_t model_id; - uint8_t product_id; - char version[10]; - uint8_t hc; // heating circuit 1, 2, 3 or 4 - bool active; // true if there is data for this HC - + uint8_t hc; // heating circuit 1, 2, 3 or 4 + bool active; // true if there is data for this HC uint16_t flowTemp; uint8_t pumpMod; uint8_t valveStatus; @@ -326,26 +326,31 @@ typedef struct { // Mixer data typedef struct { + uint8_t device_id; + uint8_t device_flags; + const char * device_desc_p; + uint8_t product_id; + char version[10]; bool detected; _EMS_Mixing_HC hc[EMS_THERMOSTAT_MAXHC]; // array for the 4 heating circuits } _EMS_Mixing; -// SM Solar Module - SM10/SM100/ISM1 +// Solar Module - SM10/SM100/ISM1 typedef struct { - uint8_t device_id; // the device ID of the Solar Module - uint8_t model_id; // Solar Module - uint8_t product_id; - char version[10]; - - int16_t collectorTemp; // collector temp - int16_t bottomTemp; // bottom temp - uint8_t pumpModulation; // modulation solar pump - uint8_t pump; // pump active - int16_t setpoint_maxBottomTemp; // setpoint for maximum collector temp - uint16_t EnergyLastHour; - uint16_t EnergyToday; - uint16_t EnergyTotal; - uint32_t pumpWorkMin; // Total solar pump operating time + uint8_t device_id; // the device ID of the Solar Module + uint8_t device_flags; // Solar Module flags + const char * device_desc_p; + uint8_t product_id; + char version[10]; + int16_t collectorTemp; // collector temp + int16_t bottomTemp; // bottom temp + uint8_t pumpModulation; // modulation solar pump + uint8_t pump; // pump active + int16_t setpoint_maxBottomTemp; // setpoint for maximum collector temp + uint16_t EnergyLastHour; + uint16_t EnergyToday; + uint16_t EnergyTotal; + uint32_t pumpWorkMin; // Total solar pump operating time } _EMS_SolarModule; // heating circuit @@ -367,8 +372,9 @@ typedef struct { // Thermostat data typedef struct { - uint8_t device_id; // the device ID of the thermostat - uint8_t model_id; // thermostat model + uint8_t device_id; // the device ID of the thermostat + uint8_t device_flags; // thermostat model flags + const char * device_desc_p; uint8_t product_id; char version[10]; char datetime[25]; // HH:MM:SS DD/MM/YYYY @@ -381,48 +387,41 @@ typedef void (*EMS_processType_cb)(_EMS_RxTelegram * EMS_RxTelegram); // Definition for each EMS type, including the relative callback function typedef struct { - uint8_t model_id; - uint16_t type; // long to support EMS+ types - const char typeString[50]; - EMS_processType_cb processType_cb; + _EMS_DEVICE_UPDATE_FLAG device_flag; + uint16_t type; + const char typeString[30]; + EMS_processType_cb processType_cb; } _EMS_Type; // function definitions -extern void ems_dumpBuffer(const char * prefix, uint8_t * telegram, uint8_t length); -extern void ems_parseTelegram(uint8_t * telegram, uint8_t len); -void ems_init(); -void ems_doReadCommand(uint16_t type, uint8_t dest, bool forceRefresh = false); -void ems_sendRawTelegram(char * telegram); -void ems_scanDevices(); -void ems_printAllDevices(); -void ems_printDevices(); -uint8_t ems_printDevices_s(char * buffer, uint16_t len); -void ems_printTxQueue(); -void ems_testTelegram(uint8_t test_num); -void ems_startupTelegrams(); -bool ems_checkEMSBUSAlive(); -void ems_clearDeviceList(); - -void ems_setThermostatTemp(float temperature, uint8_t hc, uint8_t temptype = 0); -void ems_setThermostatMode(uint8_t mode, uint8_t hc); -void ems_setWarmWaterTemp(uint8_t temperature); -void ems_setFlowTemp(uint8_t temperature); -void ems_setWarmWaterActivated(bool activated); -void ems_setWarmTapWaterActivated(bool activated); -void ems_setPoll(bool b); -void ems_setLogging(_EMS_SYS_LOGGING loglevel, bool silent = false); -void ems_setEmsRefreshed(bool b); -void ems_setWarmWaterModeComfort(uint8_t comfort); -void ems_setModels(); -void ems_setTxDisabled(bool b); -void ems_setTxMode(uint8_t mode); - -uint8_t _getHeatingCircuit(_EMS_RxTelegram * EMS_RxTelegram); - -char * ems_getThermostatDescription(char * buffer, bool name_only = false); -char * ems_getBoilerDescription(char * buffer, bool name_only = false); -char * ems_getSolarModuleDescription(char * buffer, bool name_only = false); -char * ems_getHeatPumpDescription(char * buffer, bool name_only = false); +void ems_dumpBuffer(const char * prefix, uint8_t * telegram, uint8_t length); +void ems_parseTelegram(uint8_t * telegram, uint8_t len); +void ems_init(); +void ems_doReadCommand(uint16_t type, uint8_t dest); +void ems_sendRawTelegram(char * telegram); +void ems_scanDevices(); +void ems_printDevices(); +uint8_t ems_printDevices_s(char * buffer, uint16_t len); +void ems_printTxQueue(); +void ems_testTelegram(uint8_t test_num); +void ems_startupTelegrams(); +bool ems_checkEMSBUSAlive(); +void ems_clearDeviceList(); +void ems_setThermostatTemp(float temperature, uint8_t hc, uint8_t temptype = 0); +void ems_setThermostatMode(uint8_t mode, uint8_t hc); +void ems_setWarmWaterTemp(uint8_t temperature); +void ems_setFlowTemp(uint8_t temperature); +void ems_setWarmWaterActivated(bool activated); +void ems_setWarmWaterOnetime(bool activated); +void ems_setWarmTapWaterActivated(bool activated); +void ems_setPoll(bool b); +void ems_setLogging(_EMS_SYS_LOGGING loglevel, uint16_t type_id = 0); +void ems_setWarmWaterModeComfort(uint8_t comfort); +void ems_setModels(); +void ems_setTxDisabled(bool b); +void ems_setTxMode(uint8_t mode); +char * ems_getDeviceDescription(_EMS_DEVICE_TYPE device_type, char * buffer, bool name_only = false); +bool ems_getDeviceTypeDescription(uint8_t device_id, char * buffer); void ems_getThermostatValues(); void ems_getBoilerValues(); void ems_getSolarModuleValues(); @@ -435,13 +434,15 @@ bool ems_getSolarModuleEnabled(); bool ems_getHeatPumpEnabled(); bool ems_getBusConnected(); _EMS_SYS_LOGGING ems_getLogging(); -bool ems_getEmsRefreshed(); uint8_t ems_getThermostatModel(); uint8_t ems_getSolarModuleModel(); void ems_discoverModels(); bool ems_getTxCapable(); uint32_t ems_getPollFrequency(); bool ems_getTxDisabled(); +void ems_Device_add_flags(unsigned int flags); +bool ems_Device_has_flags(unsigned int flags); +void ems_Device_remove_flags(unsigned int flags); // private functions uint8_t _crcCalculator(uint8_t * data, uint8_t len); @@ -449,6 +450,7 @@ void _processType(_EMS_RxTelegram * EMS_RxTelegram); void _debugPrintPackage(const char * prefix, _EMS_RxTelegram * EMS_RxTelegram, const char * color); void _ems_clearTxData(); void _removeTxQueue(); +uint8_t _getHeatingCircuit(_EMS_RxTelegram * EMS_RxTelegram); // global so can referenced in other classes extern _EMS_Sys_Status EMS_Sys_Status; @@ -456,7 +458,6 @@ extern _EMS_Boiler EMS_Boiler; extern _EMS_Thermostat EMS_Thermostat; extern _EMS_SolarModule EMS_SolarModule; extern _EMS_HeatPump EMS_HeatPump; -extern _EMS_Other EMS_Other; extern _EMS_Mixing EMS_Mixing; -extern std::list<_Generic_Device> Devices; +extern std::list<_Detected_Device> Devices; diff --git a/src/ems_devices.h b/src/ems_devices.h index af8307694..3d09e6ec0 100644 --- a/src/ems_devices.h +++ b/src/ems_devices.h @@ -11,6 +11,43 @@ #include "ems.h" +// Fixed EMS Device IDs +#define EMS_ID_ME 0x0B // our device, hardcoded as the "Service Key" +#define EMS_ID_BOILER 0x08 // all UBA Boilers have 0x08 +#define EMS_ID_SM 0x30 // Solar Module SM10, SM100 and ISM1 +#define EMS_ID_HP 0x38 // HeatPump +#define EMS_ID_GATEWAY 0x48 // Gateway e.g. KM200 Web Gateway +#define EMS_ID_MIXING1 0x20 // Mixing +#define EMS_ID_MIXING2 0x21 // Mixing +#define EMS_ID_SWITCH 0x11 // Switch +#define EMS_ID_CONTROLLER 0x09 // Controller +#define EMS_ID_CONNECT1 0x02 // Connect +#define EMS_ID_CONNECT2 0x50 // Connect +#define EMS_ID_THERMOSTAT1 0x10 // Thermostat +#define EMS_ID_THERMOSTAT2 0x17 // Thermostat +#define EMS_ID_THERMOSTAT3 0x18 // Thermostat + +// mapping for EMS_Devices_Type +const _EMS_Device_Types EMS_Devices_Types[] = { + + {EMS_ID_BOILER, EMS_DEVICE_TYPE_BOILER, "UBAMaster"}, + {EMS_ID_THERMOSTAT1, EMS_DEVICE_TYPE_THERMOSTAT, "Thermostat"}, + {EMS_ID_THERMOSTAT2, EMS_DEVICE_TYPE_THERMOSTAT, "Thermostat"}, + {EMS_ID_THERMOSTAT3, EMS_DEVICE_TYPE_THERMOSTAT, "Thermostat"}, + {EMS_ID_SM, EMS_DEVICE_TYPE_SOLAR, "Solar Module"}, + {EMS_ID_HP, EMS_DEVICE_TYPE_HEATPUMP, "Heat Pump"}, + {EMS_ID_GATEWAY, EMS_DEVICE_TYPE_GATEWAY, "Gateway"}, + {EMS_ID_ME, EMS_DEVICE_TYPE_SERVICEKEY, "Me"}, + {EMS_ID_NONE, EMS_DEVICE_TYPE_NONE, "All"}, + {EMS_ID_MIXING1, EMS_DEVICE_TYPE_MIXING, "Mixing Module"}, + {EMS_ID_MIXING2, EMS_DEVICE_TYPE_MIXING, "Mixing Module"}, + {EMS_ID_SWITCH, EMS_DEVICE_TYPE_SWITCH, "Switching Module"}, + {EMS_ID_CONTROLLER, EMS_DEVICE_TYPE_CONTROLLER, "Controller"}, + {EMS_ID_CONNECT1, EMS_DEVICE_TYPE_CONNECT, "Connect"}, + {EMS_ID_CONNECT2, EMS_DEVICE_TYPE_CONNECT, "Connect"} + +}; + /* * Common Type */ @@ -26,13 +63,20 @@ #define EMS_TYPE_UBAMaintenanceStatusMessage 0x1C // is an automatic monitor broadcast #define EMS_TYPE_UBAParameterWW 0x33 #define EMS_TYPE_UBATotalUptimeMessage 0x14 +#define EMS_TYPE_UBAFlags 0x35 #define EMS_TYPE_UBAMaintenanceSettingsMessage 0x15 #define EMS_TYPE_UBAParametersMessage 0x16 #define EMS_TYPE_UBASetPoints 0x1A #define EMS_TYPE_UBAFunctionTest 0x1D +// EMS+ specific +#define EMS_TYPE_UBAOutdoorTemp 0xD1 // external temp +#define EMS_TYPE_UBAMonitorFast2 0xE4 // Monitor fast for newer EMS+ +#define EMS_TYPE_UBAMonitorSlow2 0xE5 // Monitor slow for newer EMS+ + #define EMS_OFFSET_UBAParameterWW_wwtemp 2 // WW Temperature #define EMS_OFFSET_UBAParameterWW_wwactivated 1 // WW Activated +#define EMS_OFFSET_UBAParameterWW_wwOneTime 0x00 // WW OneTime loading #define EMS_OFFSET_UBAParameterWW_wwComfort 9 // WW is in comfort or eco mode #define EMS_VALUE_UBAParameterWW_wwComfort_Hot 0x00 // the value for hot #define EMS_VALUE_UBAParameterWW_wwComfort_Eco 0xD8 // the value for eco @@ -87,7 +131,6 @@ #define EMS_OFFSET_RC30Set_mode 23 // position of thermostat mode #define EMS_OFFSET_RC30Set_temp 28 // position of thermostat setpoint temperature - // RC35 specific #define EMS_TYPE_RC35StatusMessage_HC1 0x3E // is an automatic thermostat broadcast giving us temps on HC1 #define EMS_TYPE_RC35StatusMessage_HC2 0x48 // is an automatic thermostat broadcast giving us temps on HC2 @@ -137,9 +180,10 @@ #define EMS_OFFSET_RCPLUSSet_temp_setpoint 8 // temp setpoint, when changing of templevel (in auto) value is reset and set to FF #define EMS_OFFSET_RCPLUSSet_manual_setpoint 10 // manual setpoint -// Junkers FR10, FW100 (EMS Plus) +// Junkers FR10, FR50, FW100 (EMS Plus) #define EMS_TYPE_JunkersStatusMessage 0x6F // is an automatic thermostat broadcast giving us temps -#define EMS_OFFSET_JunkersStatusMessage_mode 0 // current mode +#define EMS_OFFSET_JunkersStatusMessage_daymode 0 // 3 = day, 2 = night +#define EMS_OFFSET_JunkersStatusMessage_mode 1 // current mode, 1 = manual, 2 = auto #define EMS_OFFSET_JunkersStatusMessage_setpoint 2 // setpoint temp #define EMS_OFFSET_JunkersStatusMessage_curr 4 // current temp @@ -152,147 +196,102 @@ #define EMS_OFFSET_MMPLUSStatusMessage_pump_mod 5 // pump modulation #define EMS_OFFSET_MMPLUSStatusMessage_valve_status 2 // valve in percent - -// Known EMS devices -typedef enum { - EMS_MODEL_NONE, // unset - EMS_MODEL_ALL, // common for all devices - - // heatpump - EMS_MODEL_HP, - - // solar module - EMS_MODEL_SM, - - // boiler - EMS_MODEL_UBA, - - // and the thermostats - EMS_MODEL_ES73, - EMS_MODEL_RC10, - EMS_MODEL_RC20, - EMS_MODEL_RC20F, - EMS_MODEL_RC30, - EMS_MODEL_RC35, - EMS_MODEL_EASY, - EMS_MODEL_RC300, - EMS_MODEL_CW100, - EMS_MODEL_1010, - EMS_MODEL_OT, - EMS_MODEL_FW100, - EMS_MODEL_FR10, - EMS_MODEL_FR100, - EMS_MODEL_FR110, - EMS_MODEL_FW120, - - // mixing devices - EMS_MODEL_MM100 - -} _EMS_MODEL_ID; - -// EMS types for known boilers. This list will be extended when new devices are recognized. -// The device_id is always 0x08 -// format is PRODUCT ID, DESCRIPTION -const _Boiler_Device Boiler_Devices[] = { - - {72, "MC10 Module"}, - {123, "Buderus GBx72/Nefit Trendline/Junkers Cerapur/Worcester Greenstar Si"}, - {133, "Buderus GB125"}, - {115, "Nefit Topline/Buderus GB162"}, - {203, "Buderus Logamax U122/Junkers Cerapur"}, - {208, "Buderus Logamax plus/GB192/Bosch Condens GC9000"}, - {64, "Sieger BK13,BK15/Nefit Smartline/Buderus GB1x2"}, - {95, "Bosch Condens 2500/Buderus Logamax GB062/Junkers Heatronic 3"}, - {122, "Nefit Proline"}, - {170, "Buderus Logano GB212"}, - {172, "Nefit Enviline"} - -}; - /* - * Known Solar Module types, device id is 0x30 - * format is PRODUCT ID, DESCRIPTION + * Table of all known EMS Devices + * ProductID, DeviceType, Description, Flags */ -const _SolarModule_Device SolarModule_Devices[] = { +static const _EMS_Device EMS_Devices[] = { - {EMS_PRODUCTID_SM10, "SM10 Solar Module"}, - {EMS_PRODUCTID_SM100, "SM100 Solar Module"}, - {EMS_PRODUCTID_ISM1, "Junkers ISM1 Solar Module"}, - {EMS_PRODUCTID_SM50, "SM50 Solar Module"} + // + // UBA Masters - typically with device_id of 0x08 + // + {72, EMS_DEVICE_TYPE_BOILER, "MC10 Module", EMS_DEVICE_FLAG_NONE}, + {123, EMS_DEVICE_TYPE_BOILER, "Buderus GBx72/Nefit Trendline/Junkers Cerapur/Worcester Greenstar Si/27i", EMS_DEVICE_FLAG_NONE}, + {133, EMS_DEVICE_TYPE_BOILER, "Buderus GB125/Logamatic MC110", EMS_DEVICE_FLAG_NONE}, + {115, EMS_DEVICE_TYPE_BOILER, "Nefit Topline/Buderus GB162", EMS_DEVICE_FLAG_NONE}, + {203, EMS_DEVICE_TYPE_BOILER, "Buderus Logamax U122/Junkers Cerapur", EMS_DEVICE_FLAG_NONE}, + {208, EMS_DEVICE_TYPE_BOILER, "Buderus Logamax plus/GB192/Bosch Condens GC9000", EMS_DEVICE_FLAG_NONE}, + {64, EMS_DEVICE_TYPE_BOILER, "Sieger BK13,BK15/Nefit Smartline/Buderus GB1x2", EMS_DEVICE_FLAG_NONE}, + {234, EMS_DEVICE_TYPE_BOILER, "Buderus Logamax Plus GB122", EMS_DEVICE_FLAG_NONE}, + {95, EMS_DEVICE_TYPE_BOILER, "Bosch Condens 2500/Buderus Logamax GB062/Junkers Cerapur Top/Worcester Greenstar i/Generic HT3", EMS_DEVICE_FLAG_NONE}, + {122, EMS_DEVICE_TYPE_BOILER, "Nefit Proline", EMS_DEVICE_FLAG_NONE}, + {170, EMS_DEVICE_TYPE_BOILER, "Buderus Logano GB212", EMS_DEVICE_FLAG_NONE}, + {172, EMS_DEVICE_TYPE_BOILER, "Nefit Enviline", EMS_DEVICE_FLAG_NONE}, -}; + // + // Solar Modules - type 0x30 + // + {73, EMS_DEVICE_TYPE_SOLAR, "SM10 Solar Module", EMS_DEVICE_FLAG_SM10}, + {163, EMS_DEVICE_TYPE_SOLAR, "SM100 Solar Module", EMS_DEVICE_FLAG_SM100}, + {101, EMS_DEVICE_TYPE_SOLAR, "Junkers ISM1 Solar Module", EMS_DEVICE_FLAG_SM100}, + {162, EMS_DEVICE_TYPE_SOLAR, "SM50 Solar Module", EMS_DEVICE_FLAG_SM100}, -/* - * Mixing Units - * Typically device id is 0x20 or 0x21 - * format is PRODUCT ID, DESCRIPTION - */ -const _Mixing_Device Mixing_Devices[] = { - {160, "MM100 Mixing Module"}, - {69, "MM10 Mixer Module"}, - {159, "MM50 Mixing Module"}, -}; + // + // Mixing Devices - type 0x20 or 0x21 + // + {160, EMS_DEVICE_TYPE_MIXING, "MM100 Mixing Module", EMS_DEVICE_FLAG_NONE}, + {161, EMS_DEVICE_TYPE_MIXING, "MM200 Mixing Module", EMS_DEVICE_FLAG_NONE}, + {69, EMS_DEVICE_TYPE_MIXING, "MM10 Mixer Module", EMS_DEVICE_FLAG_NONE}, + {159, EMS_DEVICE_TYPE_MIXING, "MM50 Mixing Module", EMS_DEVICE_FLAG_NONE}, + {79, EMS_DEVICE_TYPE_MIXING, "MM100 Mixer Module", EMS_DEVICE_FLAG_NONE}, + {80, EMS_DEVICE_TYPE_MIXING, "MM200 Mixer Module", EMS_DEVICE_FLAG_NONE}, + {78, EMS_DEVICE_TYPE_MIXING, "MM400 Mixer Module", EMS_DEVICE_FLAG_NONE}, -// Other EMS devices which are not considered boilers, thermostats or solar modules -// format is PRODUCT ID, DEVICE ID, DESCRIPTION -const _Other_Device Other_Devices[] = { + // + // HeatPump - type 0x38 + // + {252, EMS_DEVICE_TYPE_HEATPUMP, "HeatPump Module", EMS_DEVICE_FLAG_NONE}, + {200, EMS_DEVICE_TYPE_HEATPUMP, "HeatPump Module", EMS_DEVICE_FLAG_NONE}, - {71, 0x11, "WM10 Switch Module"}, + // + // Other devices, like 0x11 for Switching, 0x09 for controllers, 0x02 for Connect, 0x48 for Gateway + // + {71, EMS_DEVICE_TYPE_SWITCH, "WM10 Switch Module", EMS_DEVICE_FLAG_NONE}, // 0x11 + {68, EMS_DEVICE_TYPE_CONTROLLER, "BC10/RFM20 Receiver", EMS_DEVICE_FLAG_NONE}, // 0x09 + {218, EMS_DEVICE_TYPE_CONTROLLER, "Junkers M200/Buderus RFM200 Receiver", EMS_DEVICE_FLAG_NONE}, // 0x50 + {190, EMS_DEVICE_TYPE_CONTROLLER, "BC10 Base Controller", EMS_DEVICE_FLAG_NONE}, // 0x09 + {114, EMS_DEVICE_TYPE_CONTROLLER, "BC10 Base Controller", EMS_DEVICE_FLAG_NONE}, // 0x09 + {125, EMS_DEVICE_TYPE_CONTROLLER, "BC25 Base Controller", EMS_DEVICE_FLAG_NONE}, // 0x09 + {169, EMS_DEVICE_TYPE_CONTROLLER, "BC40 Base Controller", EMS_DEVICE_FLAG_NONE}, // 0x09 + {152, EMS_DEVICE_TYPE_CONTROLLER, "Controller", EMS_DEVICE_FLAG_NONE}, // 0x09 + {95, EMS_DEVICE_TYPE_CONTROLLER, "HT3 Controller", EMS_DEVICE_FLAG_NONE}, // 0x09 + {230, EMS_DEVICE_TYPE_CONTROLLER, "BC Base Controller", EMS_DEVICE_FLAG_NONE}, // 0x09 + {205, EMS_DEVICE_TYPE_CONNECT, "Nefit Moduline Easy Connect", EMS_DEVICE_FLAG_NONE}, // 0x02 + {206, EMS_DEVICE_TYPE_CONNECT, "Bosch Easy Connect", EMS_DEVICE_FLAG_NONE}, // 0x02 + {171, EMS_DEVICE_TYPE_CONNECT, "EMS-OT OpenTherm converter", EMS_DEVICE_FLAG_NONE}, // 0x02 + {189, EMS_DEVICE_TYPE_GATEWAY, "Web Gateway KM200", EMS_DEVICE_FLAG_NONE}, // 0x48 - {68, 0x09, "BC10/RFM20 Receiver"}, - {190, 0x09, "BC10 Base Controller"}, - {114, 0x09, "BC10 Base Controller"}, - {125, 0x09, "BC25 Base Controller"}, - {169, 0x09, "BC40 Base Controller"}, - {152, 0x09, "Junkers Controller"}, - {230, 0x09, "BC Base Controller"}, - - {205, 0x02, "Nefit Moduline Easy Connect"}, - {206, 0x02, "Bosch Easy Connect"}, - {171, 0x02, "EMS-OT OpenTherm converter"}, - - {189, EMS_ID_GATEWAY, "Web Gateway KM200"} - -}; - -// heatpump, device ID 0x38 -// format is PRODUCT ID, DEVICE ID, DESCRIPTION -const _HeatPump_Device HeatPump_Devices[] = { - - {252, "HeatPump Module"}, - {200, "HeatPump Module"} - -}; - -/* - * Known thermostat types and their capabilities - * format is MODEL_ID, PRODUCT ID, DEVICE ID, DESCRIPTION - */ -const _Thermostat_Device Thermostat_Devices[] = { + // + // Thermostats, typically device id of 0x10, 0x17 and 0x18 + // // Easy devices - not currently supporting write operations - {EMS_MODEL_EASY, 202, 0x18, "Logamatic TC100/Nefit Moduline Easy", EMS_THERMOSTAT_WRITE_NO}, - {EMS_MODEL_EASY, 203, 0x18, "Bosch EasyControl CT200", EMS_THERMOSTAT_WRITE_NO}, - {EMS_MODEL_CW100, 157, 0x18, "Bosch CW100", EMS_THERMOSTAT_WRITE_NO}, + {202, EMS_DEVICE_TYPE_THERMOSTAT, "Logamatic TC100/Nefit Moduline Easy", EMS_DEVICE_FLAG_EASY | EMS_DEVICE_FLAG_NO_WRITE}, // 0x18, cannot write + {203, EMS_DEVICE_TYPE_THERMOSTAT, "Bosch EasyControl CT200", EMS_DEVICE_FLAG_EASY | EMS_DEVICE_FLAG_NO_WRITE}, // 0x18, cannot write + {157, EMS_DEVICE_TYPE_THERMOSTAT, "Buderus RC200/Bosch CW100/Junkers CW100", EMS_DEVICE_FLAG_NO_WRITE}, // 0x18, cannot write // Buderus/Nefit - {EMS_MODEL_RC10, 79, 0x17, "RC10/Moduline 100", EMS_THERMOSTAT_WRITE_YES}, - {EMS_MODEL_RC20, 77, 0x17, "RC20/Moduline 300", EMS_THERMOSTAT_WRITE_YES}, - {EMS_MODEL_RC20F, 93, 0x18, "RC20F", EMS_THERMOSTAT_WRITE_YES}, - {EMS_MODEL_RC30, 67, 0x10, "RC30", EMS_THERMOSTAT_WRITE_YES}, - {EMS_MODEL_RC30, 78, 0x10, "RC30/Moduline 400", EMS_THERMOSTAT_WRITE_YES}, - {EMS_MODEL_RC35, 86, 0x10, "RC35", EMS_THERMOSTAT_WRITE_YES}, - {EMS_MODEL_RC300, 158, 0x10, "RC300/RC310/Moduline 3000/Bosch CW400", EMS_THERMOSTAT_WRITE_YES}, - {EMS_MODEL_1010, 165, 0x18, "RC100/Moduline 1010", EMS_THERMOSTAT_WRITE_NO}, + {79, EMS_DEVICE_TYPE_THERMOSTAT, "RC10/Moduline 100", EMS_DEVICE_FLAG_RC10}, // 0x17 + {77, EMS_DEVICE_TYPE_THERMOSTAT, "RC20/Moduline 300", EMS_DEVICE_FLAG_RC20}, // 0x17 + {93, EMS_DEVICE_TYPE_THERMOSTAT, "RC20RF", EMS_DEVICE_FLAG_RC20}, // 0x18 + {67, EMS_DEVICE_TYPE_THERMOSTAT, "RC30", EMS_DEVICE_FLAG_RC30}, // 0x10 + {78, EMS_DEVICE_TYPE_THERMOSTAT, "RC30/Moduline 400", EMS_DEVICE_FLAG_RC30}, // 0x10 + {86, EMS_DEVICE_TYPE_THERMOSTAT, "RC35", EMS_DEVICE_FLAG_RC35}, // 0x10 + {158, EMS_DEVICE_TYPE_THERMOSTAT, "RC300/RC310/Moduline 3000/Bosch CW400/W-B Sense II", EMS_DEVICE_FLAG_RC300}, // 0x10 + {165, EMS_DEVICE_TYPE_THERMOSTAT, "RC100/Moduline 1010", EMS_DEVICE_FLAG_RC300 | EMS_DEVICE_FLAG_NO_WRITE}, // 0x18, cannot write // Sieger - {EMS_MODEL_ES73, 76, 0x10, "Sieger ES73", EMS_THERMOSTAT_WRITE_YES}, + {076, EMS_DEVICE_TYPE_THERMOSTAT, "Sieger ES73", EMS_DEVICE_FLAG_RC35}, // 0x10 // Junkers - {EMS_MODEL_FW100, 105, 0x10, "Junkers FW100", EMS_THERMOSTAT_WRITE_NO}, - {EMS_MODEL_FR10, 111, 0x18, "Junkers FR10", EMS_THERMOSTAT_WRITE_NO}, - {EMS_MODEL_FR100, 105, 0x18, "Junkers FR100", EMS_THERMOSTAT_WRITE_NO}, - {EMS_MODEL_FR110, 108, 0x18, "Junkers FR110", EMS_THERMOSTAT_WRITE_NO}, - {EMS_MODEL_FW120, 192, 0x10, "Junkers FW120", EMS_THERMOSTAT_WRITE_NO} + {105, EMS_DEVICE_TYPE_THERMOSTAT, "Junkers FW100", EMS_DEVICE_FLAG_JUNKERS | EMS_DEVICE_FLAG_NO_WRITE}, // 0x10, cannot write + {106, EMS_DEVICE_TYPE_THERMOSTAT, "Junkers FW200", EMS_DEVICE_FLAG_JUNKERS | EMS_DEVICE_FLAG_NO_WRITE}, // 0x10, cannot write + {107, EMS_DEVICE_TYPE_THERMOSTAT, "Junkers FR100", EMS_DEVICE_FLAG_JUNKERS | EMS_DEVICE_FLAG_NO_WRITE}, // 0x10, cannot write + {108, EMS_DEVICE_TYPE_THERMOSTAT, "Junkers FR110", EMS_DEVICE_FLAG_JUNKERS | EMS_DEVICE_FLAG_NO_WRITE}, // 0x10, cannot write + {111, EMS_DEVICE_TYPE_THERMOSTAT, "Junkers FR10", EMS_DEVICE_FLAG_JUNKERS | EMS_DEVICE_FLAG_NO_WRITE}, // 0x10, cannot write + {191, EMS_DEVICE_TYPE_THERMOSTAT, "Junkers FR120", EMS_DEVICE_FLAG_JUNKERS | EMS_DEVICE_FLAG_NO_WRITE}, // 0x10, cannot write + {192, EMS_DEVICE_TYPE_THERMOSTAT, "Junkers FW120", EMS_DEVICE_FLAG_JUNKERS | EMS_DEVICE_FLAG_NO_WRITE}, // 0x10, cannot write + {147, EMS_DEVICE_TYPE_THERMOSTAT, "Junkers FR50", EMS_DEVICE_FLAG_JUNKERS | EMS_DEVICE_FLAG_NO_WRITE} // 0x10, cannot write + }; diff --git a/src/ems_utils.cpp b/src/ems_utils.cpp index bc68a8436..4d61cdfef 100644 --- a/src/ems_utils.cpp +++ b/src/ems_utils.cpp @@ -25,11 +25,11 @@ char * _float_to_char(char * a, float f, uint8_t precision) { // convert bool to text. bools are stored as bytes char * _bool_to_char(char * s, uint8_t value) { - if (value == EMS_VALUE_INT_ON) { + if ((value == EMS_VALUE_BOOL_ON) || (value == EMS_VALUE_BOOL_ON2)) { strlcpy(s, "on", sizeof(s)); - } else if (value == EMS_VALUE_INT_OFF) { + } else if (value == EMS_VALUE_BOOL_OFF) { strlcpy(s, "off", sizeof(s)); - } else { + } else { // EMS_VALUE_BOOL_NOTSET strlcpy(s, "?", sizeof(s)); } return s; @@ -51,22 +51,24 @@ char * _short_to_char(char * s, int16_t value, uint8_t decimals) { return (s); } - // do floating point - char s2[10] = {0}; // check for negative values if (value < 0) { strlcpy(s, "-", 10); value *= -1; // convert to positive + } else { + strlcpy(s, "", 10); } + // do floating point + char s2[10] = {0}; if (decimals == 2) { // divide by 2 - strlcpy(s, ltoa(value / 2, s2, 10), 10); + strlcat(s, ltoa(value / 2, s2, 10), 10); strlcat(s, ".", 10); strlcat(s, ((value & 0x01) ? "5" : "0"), 10); } else { - strlcpy(s, ltoa(value / (decimals * 10), s2, 10), 10); + strlcat(s, ltoa(value / (decimals * 10), s2, 10), 10); strlcat(s, ".", 10); strlcat(s, ltoa(value % (decimals * 10), s2, 10), 10); } @@ -108,7 +110,7 @@ char * _ushort_to_char(char * s, uint16_t value, uint8_t decimals) { } // takes a signed short value (2 bytes), converts to a fraction and prints it -// decimals: 0 = no division, 1=divide value by 10, 2=divide by 2, 10=divide value by 100 +// decimals: 0=no division, 1=divide value by 10 (default), 2=divide by 2, 10=divide value by 100 void _renderShortValue(const char * prefix, const char * postfix, int16_t value, uint8_t decimals) { static char buffer[200] = {0}; static char s[20] = {0}; @@ -270,7 +272,7 @@ uint8_t _readIntNumber() { return atoi(numTextPtr); } -// used to read the next string from an input buffer and convert to a double +// used to read the next string from an input buffer and convert to a float float _readFloatNumber() { char * numTextPtr = strtok(nullptr, ", \n"); if (numTextPtr == nullptr) { diff --git a/src/emsuart.h b/src/emsuart.h index 3e3e5550d..99198562f 100644 --- a/src/emsuart.h +++ b/src/emsuart.h @@ -13,7 +13,7 @@ #define EMSUART_CONFIG 0x1C // 8N1 (8 bits, no stop bits, 1 parity) #define EMSUART_BAUD 9600 // uart baud rate for the EMS circuit -#define EMS_MAXBUFFERS 5 // buffers for circular filling to avoid collisions +#define EMS_MAXBUFFERS 3 // buffers for circular filling to avoid collisions #define EMS_MAXBUFFERSIZE (EMS_MAX_TELEGRAM_LENGTH + 2) // max size of the buffer. EMS packets are max 32 bytes, plus extra 2 for BRKs #define EMSUART_BIT_TIME 104 // bit time @9600 baud diff --git a/src/my_config.h b/src/my_config.h index dfcd59578..a36662d9d 100644 --- a/src/my_config.h +++ b/src/my_config.h @@ -44,6 +44,7 @@ #define TOPIC_BOILER_CMD "boiler_cmd" // for receiving boiler commands via MQTT #define TOPIC_BOILER_CMD_WWACTIVATED "boiler_cmd_wwactivated" // change water on/off +#define TOPIC_BOILER_CMD_WWONETIME "boiler_cmd_wwonetime" // warm warter one time loading #define TOPIC_BOILER_CMD_WWTEMP "boiler_cmd_wwtemp" // wwtemp changes via MQTT #define TOPIC_BOILER_CMD_COMFORT "comfort" // ww comfort setting via MQTT #define TOPIC_BOILER_CMD_FLOWTEMP "flowtemp" // flowtemp value via MQTT diff --git a/src/test_data.h b/src/test_data.h index c47805977..697ab0004 100644 --- a/src/test_data.h +++ b/src/test_data.h @@ -54,7 +54,8 @@ static const char * TEST_DATA[] = { "88 00 19 00 00 DC 80 00 80 00 FF FF 00 00 00 21 9A 06 E1 7C 00 00 00 06 C2 13 00 1E 90 80 00", // test 49 - check max length "30 00 FF 00 02 8E 00 00 41 82 00 00 28 36 00 00 82 21", // test 50 - SM100 "10 00 FF 08 01 B9 26", // test 51 - EMS+ 0x1B9 set temp - "10 00 F7 00 FF 01 B9 21 E9" // test 52 - EMS+ 0x1B9 F7 test + "10 00 F7 00 FF 01 B9 21 E9", // test 52 - EMS+ 0x1B9 F7 test + "08 00 D1 00 00 80" // test 53 - outdoor temp }; diff --git a/src/version.h b/src/version.h index 7c86e901c..7e3e390c1 100644 --- a/src/version.h +++ b/src/version.h @@ -1 +1 @@ -#define APP_VERSION "1.9.3" +#define APP_VERSION "1.9.4" diff --git a/src/websrc/3rdparty/css/bootstrap-3.3.7.min.css b/src/websrc/3rdparty/css/bootstrap-3.3.7.min.css deleted file mode 100644 index ed3905e0e..000000000 --- a/src/websrc/3rdparty/css/bootstrap-3.3.7.min.css +++ /dev/null @@ -1,6 +0,0 @@ -/*! - * Bootstrap v3.3.7 (http://getbootstrap.com) - * Copyright 2011-2016 Twitter, Inc. - * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) - *//*! normalize.css v3.0.3 | MIT License | github.com/necolas/normalize.css */html{font-family:sans-serif;-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%}body{margin:0}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{background-color:transparent}a:active,a:hover{outline:0}abbr[title]{border-bottom:1px dotted}b,strong{font-weight:700}dfn{font-style:italic}h1{margin:.67em 0;font-size:2em}mark{color:#000;background:#ff0}small{font-size:80%}sub,sup{position:relative;font-size:75%;line-height:0;vertical-align:baseline}sup{top:-.5em}sub{bottom:-.25em}img{border:0}svg:not(:root){overflow:hidden}figure{margin:1em 40px}hr{height:0;-webkit-box-sizing:content-box;-moz-box-sizing:content-box;box-sizing:content-box}pre{overflow:auto}code,kbd,pre,samp{font-family:monospace,monospace;font-size:1em}button,input,optgroup,select,textarea{margin:0;font:inherit;color:inherit}button{overflow:visible}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{padding:0;border:0}input{line-height:normal}input[type=checkbox],input[type=radio]{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;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-box-sizing:content-box;-moz-box-sizing:content-box;box-sizing:content-box;-webkit-appearance:textfield}input[type=search]::-webkit-search-cancel-button,input[type=search]::-webkit-search-decoration{-webkit-appearance:none}fieldset{padding:.35em .625em .75em;margin:0 2px;border:1px solid silver}legend{padding:0;border:0}textarea{overflow:auto}optgroup{font-weight:700}table{border-spacing:0;border-collapse:collapse}td,th{padding:0}/*! Source: https://github.com/h5bp/html5-boilerplate/blob/master/src/css/main.css */@media print{*,:after,:before{color:#000!important;text-shadow:none!important;background:0 0!important;-webkit-box-shadow:none!important;box-shadow:none!important}a,a:visited{text-decoration:underline}a[href]:after{content:" (" attr(href) ")"}abbr[title]:after{content:" (" attr(title) ")"}a[href^="javascript:"]:after,a[href^="#"]:after{content:""}blockquote,pre{border:1px solid #999;page-break-inside:avoid}thead{display:table-header-group}img,tr{page-break-inside:avoid}img{max-width:100%!important}h2,h3,p{orphans:3;widows:3}h2,h3{page-break-after:avoid}.navbar{display:none}.btn>.caret,.dropup>.btn>.caret{border-top-color:#000!important}.label{border:1px solid #000}.table{border-collapse:collapse!important}.table td,.table th{background-color:#fff!important}.table-bordered td,.table-bordered th{border:1px solid #ddd!important}}@font-face{font-family:'Glyphicons Halflings';src:url(../fonts/glyphicons-halflings-regular.eot);src:url(../fonts/glyphicons-halflings-regular.eot?#iefix) format('embedded-opentype'),url(../fonts/glyphicons-halflings-regular.woff2) format('woff2'),url(../fonts/glyphicons-halflings-regular.woff) format('woff'),url(../fonts/glyphicons-halflings-regular.ttf) format('truetype'),url(../fonts/glyphicons-halflings-regular.svg#glyphicons_halflingsregular) format('svg')}.glyphicon{position:relative;top:1px;display:inline-block;font-family:'Glyphicons Halflings';font-style:normal;font-weight:400;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.glyphicon-asterisk:before{content:"\002a"}.glyphicon-plus:before{content:"\002b"}.glyphicon-eur:before,.glyphicon-euro:before{content:"\20ac"}.glyphicon-minus:before{content:"\2212"}.glyphicon-cloud:before{content:"\2601"}.glyphicon-envelope:before{content:"\2709"}.glyphicon-pencil:before{content:"\270f"}.glyphicon-glass:before{content:"\e001"}.glyphicon-music:before{content:"\e002"}.glyphicon-search:before{content:"\e003"}.glyphicon-heart:before{content:"\e005"}.glyphicon-star:before{content:"\e006"}.glyphicon-star-empty:before{content:"\e007"}.glyphicon-user:before{content:"\e008"}.glyphicon-film:before{content:"\e009"}.glyphicon-th-large:before{content:"\e010"}.glyphicon-th:before{content:"\e011"}.glyphicon-th-list:before{content:"\e012"}.glyphicon-ok:before{content:"\e013"}.glyphicon-remove:before{content:"\e014"}.glyphicon-zoom-in:before{content:"\e015"}.glyphicon-zoom-out:before{content:"\e016"}.glyphicon-off:before{content:"\e017"}.glyphicon-signal:before{content:"\e018"}.glyphicon-cog:before{content:"\e019"}.glyphicon-trash:before{content:"\e020"}.glyphicon-home:before{content:"\e021"}.glyphicon-file:before{content:"\e022"}.glyphicon-time:before{content:"\e023"}.glyphicon-road:before{content:"\e024"}.glyphicon-download-alt:before{content:"\e025"}.glyphicon-download:before{content:"\e026"}.glyphicon-upload:before{content:"\e027"}.glyphicon-inbox:before{content:"\e028"}.glyphicon-play-circle:before{content:"\e029"}.glyphicon-repeat:before{content:"\e030"}.glyphicon-refresh:before{content:"\e031"}.glyphicon-list-alt:before{content:"\e032"}.glyphicon-lock:before{content:"\e033"}.glyphicon-flag:before{content:"\e034"}.glyphicon-headphones:before{content:"\e035"}.glyphicon-volume-off:before{content:"\e036"}.glyphicon-volume-down:before{content:"\e037"}.glyphicon-volume-up:before{content:"\e038"}.glyphicon-qrcode:before{content:"\e039"}.glyphicon-barcode:before{content:"\e040"}.glyphicon-tag:before{content:"\e041"}.glyphicon-tags:before{content:"\e042"}.glyphicon-book:before{content:"\e043"}.glyphicon-bookmark:before{content:"\e044"}.glyphicon-print:before{content:"\e045"}.glyphicon-camera:before{content:"\e046"}.glyphicon-font:before{content:"\e047"}.glyphicon-bold:before{content:"\e048"}.glyphicon-italic:before{content:"\e049"}.glyphicon-text-height:before{content:"\e050"}.glyphicon-text-width:before{content:"\e051"}.glyphicon-align-left:before{content:"\e052"}.glyphicon-align-center:before{content:"\e053"}.glyphicon-align-right:before{content:"\e054"}.glyphicon-align-justify:before{content:"\e055"}.glyphicon-list:before{content:"\e056"}.glyphicon-indent-left:before{content:"\e057"}.glyphicon-indent-right:before{content:"\e058"}.glyphicon-facetime-video:before{content:"\e059"}.glyphicon-picture:before{content:"\e060"}.glyphicon-map-marker:before{content:"\e062"}.glyphicon-adjust:before{content:"\e063"}.glyphicon-tint:before{content:"\e064"}.glyphicon-edit:before{content:"\e065"}.glyphicon-share:before{content:"\e066"}.glyphicon-check:before{content:"\e067"}.glyphicon-move:before{content:"\e068"}.glyphicon-step-backward:before{content:"\e069"}.glyphicon-fast-backward:before{content:"\e070"}.glyphicon-backward:before{content:"\e071"}.glyphicon-play:before{content:"\e072"}.glyphicon-pause:before{content:"\e073"}.glyphicon-stop:before{content:"\e074"}.glyphicon-forward:before{content:"\e075"}.glyphicon-fast-forward:before{content:"\e076"}.glyphicon-step-forward:before{content:"\e077"}.glyphicon-eject:before{content:"\e078"}.glyphicon-chevron-left:before{content:"\e079"}.glyphicon-chevron-right:before{content:"\e080"}.glyphicon-plus-sign:before{content:"\e081"}.glyphicon-minus-sign:before{content:"\e082"}.glyphicon-remove-sign:before{content:"\e083"}.glyphicon-ok-sign:before{content:"\e084"}.glyphicon-question-sign:before{content:"\e085"}.glyphicon-info-sign:before{content:"\e086"}.glyphicon-screenshot:before{content:"\e087"}.glyphicon-remove-circle:before{content:"\e088"}.glyphicon-ok-circle:before{content:"\e089"}.glyphicon-ban-circle:before{content:"\e090"}.glyphicon-arrow-left:before{content:"\e091"}.glyphicon-arrow-right:before{content:"\e092"}.glyphicon-arrow-up:before{content:"\e093"}.glyphicon-arrow-down:before{content:"\e094"}.glyphicon-share-alt:before{content:"\e095"}.glyphicon-resize-full:before{content:"\e096"}.glyphicon-resize-small:before{content:"\e097"}.glyphicon-exclamation-sign:before{content:"\e101"}.glyphicon-gift:before{content:"\e102"}.glyphicon-leaf:before{content:"\e103"}.glyphicon-fire:before{content:"\e104"}.glyphicon-eye-open:before{content:"\e105"}.glyphicon-eye-close:before{content:"\e106"}.glyphicon-warning-sign:before{content:"\e107"}.glyphicon-plane:before{content:"\e108"}.glyphicon-calendar:before{content:"\e109"}.glyphicon-random:before{content:"\e110"}.glyphicon-comment:before{content:"\e111"}.glyphicon-magnet:before{content:"\e112"}.glyphicon-chevron-up:before{content:"\e113"}.glyphicon-chevron-down:before{content:"\e114"}.glyphicon-retweet:before{content:"\e115"}.glyphicon-shopping-cart:before{content:"\e116"}.glyphicon-folder-close:before{content:"\e117"}.glyphicon-folder-open:before{content:"\e118"}.glyphicon-resize-vertical:before{content:"\e119"}.glyphicon-resize-horizontal:before{content:"\e120"}.glyphicon-hdd:before{content:"\e121"}.glyphicon-bullhorn:before{content:"\e122"}.glyphicon-bell:before{content:"\e123"}.glyphicon-certificate:before{content:"\e124"}.glyphicon-thumbs-up:before{content:"\e125"}.glyphicon-thumbs-down:before{content:"\e126"}.glyphicon-hand-right:before{content:"\e127"}.glyphicon-hand-left:before{content:"\e128"}.glyphicon-hand-up:before{content:"\e129"}.glyphicon-hand-down:before{content:"\e130"}.glyphicon-circle-arrow-right:before{content:"\e131"}.glyphicon-circle-arrow-left:before{content:"\e132"}.glyphicon-circle-arrow-up:before{content:"\e133"}.glyphicon-circle-arrow-down:before{content:"\e134"}.glyphicon-globe:before{content:"\e135"}.glyphicon-wrench:before{content:"\e136"}.glyphicon-tasks:before{content:"\e137"}.glyphicon-filter:before{content:"\e138"}.glyphicon-briefcase:before{content:"\e139"}.glyphicon-fullscreen:before{content:"\e140"}.glyphicon-dashboard:before{content:"\e141"}.glyphicon-paperclip:before{content:"\e142"}.glyphicon-heart-empty:before{content:"\e143"}.glyphicon-link:before{content:"\e144"}.glyphicon-phone:before{content:"\e145"}.glyphicon-pushpin:before{content:"\e146"}.glyphicon-usd:before{content:"\e148"}.glyphicon-gbp:before{content:"\e149"}.glyphicon-sort:before{content:"\e150"}.glyphicon-sort-by-alphabet:before{content:"\e151"}.glyphicon-sort-by-alphabet-alt:before{content:"\e152"}.glyphicon-sort-by-order:before{content:"\e153"}.glyphicon-sort-by-order-alt:before{content:"\e154"}.glyphicon-sort-by-attributes:before{content:"\e155"}.glyphicon-sort-by-attributes-alt:before{content:"\e156"}.glyphicon-unchecked:before{content:"\e157"}.glyphicon-expand:before{content:"\e158"}.glyphicon-collapse-down:before{content:"\e159"}.glyphicon-collapse-up:before{content:"\e160"}.glyphicon-log-in:before{content:"\e161"}.glyphicon-flash:before{content:"\e162"}.glyphicon-log-out:before{content:"\e163"}.glyphicon-new-window:before{content:"\e164"}.glyphicon-record:before{content:"\e165"}.glyphicon-save:before{content:"\e166"}.glyphicon-open:before{content:"\e167"}.glyphicon-saved:before{content:"\e168"}.glyphicon-import:before{content:"\e169"}.glyphicon-export:before{content:"\e170"}.glyphicon-send:before{content:"\e171"}.glyphicon-floppy-disk:before{content:"\e172"}.glyphicon-floppy-saved:before{content:"\e173"}.glyphicon-floppy-remove:before{content:"\e174"}.glyphicon-floppy-save:before{content:"\e175"}.glyphicon-floppy-open:before{content:"\e176"}.glyphicon-credit-card:before{content:"\e177"}.glyphicon-transfer:before{content:"\e178"}.glyphicon-cutlery:before{content:"\e179"}.glyphicon-header:before{content:"\e180"}.glyphicon-compressed:before{content:"\e181"}.glyphicon-earphone:before{content:"\e182"}.glyphicon-phone-alt:before{content:"\e183"}.glyphicon-tower:before{content:"\e184"}.glyphicon-stats:before{content:"\e185"}.glyphicon-sd-video:before{content:"\e186"}.glyphicon-hd-video:before{content:"\e187"}.glyphicon-subtitles:before{content:"\e188"}.glyphicon-sound-stereo:before{content:"\e189"}.glyphicon-sound-dolby:before{content:"\e190"}.glyphicon-sound-5-1:before{content:"\e191"}.glyphicon-sound-6-1:before{content:"\e192"}.glyphicon-sound-7-1:before{content:"\e193"}.glyphicon-copyright-mark:before{content:"\e194"}.glyphicon-registration-mark:before{content:"\e195"}.glyphicon-cloud-download:before{content:"\e197"}.glyphicon-cloud-upload:before{content:"\e198"}.glyphicon-tree-conifer:before{content:"\e199"}.glyphicon-tree-deciduous:before{content:"\e200"}.glyphicon-cd:before{content:"\e201"}.glyphicon-save-file:before{content:"\e202"}.glyphicon-open-file:before{content:"\e203"}.glyphicon-level-up:before{content:"\e204"}.glyphicon-copy:before{content:"\e205"}.glyphicon-paste:before{content:"\e206"}.glyphicon-alert:before{content:"\e209"}.glyphicon-equalizer:before{content:"\e210"}.glyphicon-king:before{content:"\e211"}.glyphicon-queen:before{content:"\e212"}.glyphicon-pawn:before{content:"\e213"}.glyphicon-bishop:before{content:"\e214"}.glyphicon-knight:before{content:"\e215"}.glyphicon-baby-formula:before{content:"\e216"}.glyphicon-tent:before{content:"\26fa"}.glyphicon-blackboard:before{content:"\e218"}.glyphicon-bed:before{content:"\e219"}.glyphicon-apple:before{content:"\f8ff"}.glyphicon-erase:before{content:"\e221"}.glyphicon-hourglass:before{content:"\231b"}.glyphicon-lamp:before{content:"\e223"}.glyphicon-duplicate:before{content:"\e224"}.glyphicon-piggy-bank:before{content:"\e225"}.glyphicon-scissors:before{content:"\e226"}.glyphicon-bitcoin:before{content:"\e227"}.glyphicon-btc:before{content:"\e227"}.glyphicon-xbt:before{content:"\e227"}.glyphicon-yen:before{content:"\00a5"}.glyphicon-jpy:before{content:"\00a5"}.glyphicon-ruble:before{content:"\20bd"}.glyphicon-rub:before{content:"\20bd"}.glyphicon-scale:before{content:"\e230"}.glyphicon-ice-lolly:before{content:"\e231"}.glyphicon-ice-lolly-tasted:before{content:"\e232"}.glyphicon-education:before{content:"\e233"}.glyphicon-option-horizontal:before{content:"\e234"}.glyphicon-option-vertical:before{content:"\e235"}.glyphicon-menu-hamburger:before{content:"\e236"}.glyphicon-modal-window:before{content:"\e237"}.glyphicon-oil:before{content:"\e238"}.glyphicon-grain:before{content:"\e239"}.glyphicon-sunglasses:before{content:"\e240"}.glyphicon-text-size:before{content:"\e241"}.glyphicon-text-color:before{content:"\e242"}.glyphicon-text-background:before{content:"\e243"}.glyphicon-object-align-top:before{content:"\e244"}.glyphicon-object-align-bottom:before{content:"\e245"}.glyphicon-object-align-horizontal:before{content:"\e246"}.glyphicon-object-align-left:before{content:"\e247"}.glyphicon-object-align-vertical:before{content:"\e248"}.glyphicon-object-align-right:before{content:"\e249"}.glyphicon-triangle-right:before{content:"\e250"}.glyphicon-triangle-left:before{content:"\e251"}.glyphicon-triangle-bottom:before{content:"\e252"}.glyphicon-triangle-top:before{content:"\e253"}.glyphicon-console:before{content:"\e254"}.glyphicon-superscript:before{content:"\e255"}.glyphicon-subscript:before{content:"\e256"}.glyphicon-menu-left:before{content:"\e257"}.glyphicon-menu-right:before{content:"\e258"}.glyphicon-menu-down:before{content:"\e259"}.glyphicon-menu-up:before{content:"\e260"}*{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}:after,:before{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}html{font-size:10px;-webkit-tap-highlight-color:rgba(0,0,0,0)}body{font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;font-size:14px;line-height:1.42857143;color:#333;background-color:#fff}button,input,select,textarea{font-family:inherit;font-size:inherit;line-height:inherit}a{color:#337ab7;text-decoration:none}a:focus,a:hover{color:#23527c;text-decoration:underline}a:focus{outline:5px auto -webkit-focus-ring-color;outline-offset:-2px}figure{margin:0}img{vertical-align:middle}.carousel-inner>.item>a>img,.carousel-inner>.item>img,.img-responsive,.thumbnail a>img,.thumbnail>img{display:block;max-width:100%;height:auto}.img-rounded{border-radius:6px}.img-thumbnail{display:inline-block;max-width:100%;height:auto;padding:4px;line-height:1.42857143;background-color:#fff;border:1px solid #ddd;border-radius:4px;-webkit-transition:all .2s ease-in-out;-o-transition:all .2s ease-in-out;transition:all .2s ease-in-out}.img-circle{border-radius:50%}hr{margin-top:20px;margin-bottom:20px;border:0;border-top:1px solid #eee}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);border:0}.sr-only-focusable:active,.sr-only-focusable:focus{position:static;width:auto;height:auto;margin:0;overflow:visible;clip:auto}[role=button]{cursor:pointer}.h1,.h2,.h3,.h4,.h5,.h6,h1,h2,h3,h4,h5,h6{font-family:inherit;font-weight:500;line-height:1.1;color:inherit}.h1 .small,.h1 small,.h2 .small,.h2 small,.h3 .small,.h3 small,.h4 .small,.h4 small,.h5 .small,.h5 small,.h6 .small,.h6 small,h1 .small,h1 small,h2 .small,h2 small,h3 .small,h3 small,h4 .small,h4 small,h5 .small,h5 small,h6 .small,h6 small{font-weight:400;line-height:1;color:#777}.h1,.h2,.h3,h1,h2,h3{margin-top:20px;margin-bottom:10px}.h1 .small,.h1 small,.h2 .small,.h2 small,.h3 .small,.h3 small,h1 .small,h1 small,h2 .small,h2 small,h3 .small,h3 small{font-size:65%}.h4,.h5,.h6,h4,h5,h6{margin-top:10px;margin-bottom:10px}.h4 .small,.h4 small,.h5 .small,.h5 small,.h6 .small,.h6 small,h4 .small,h4 small,h5 .small,h5 small,h6 .small,h6 small{font-size:75%}.h1,h1{font-size:36px}.h2,h2{font-size:30px}.h3,h3{font-size:24px}.h4,h4{font-size:18px}.h5,h5{font-size:14px}.h6,h6{font-size:12px}p{margin:0 0 10px}.lead{margin-bottom:20px;font-size:16px;font-weight:300;line-height:1.4}@media (min-width:768px){.lead{font-size:21px}}.small,small{font-size:85%}.mark,mark{padding:.2em;background-color:#fcf8e3}.text-left{text-align:left}.text-right{text-align:right}.text-center{text-align:center}.text-justify{text-align:justify}.text-nowrap{white-space:nowrap}.text-lowercase{text-transform:lowercase}.text-uppercase{text-transform:uppercase}.text-capitalize{text-transform:capitalize}.text-muted{color:#777}.text-primary{color:#337ab7}a.text-primary:focus,a.text-primary:hover{color:#286090}.text-success{color:#3c763d}a.text-success:focus,a.text-success:hover{color:#2b542c}.text-info{color:#31708f}a.text-info:focus,a.text-info:hover{color:#245269}.text-warning{color:#8a6d3b}a.text-warning:focus,a.text-warning:hover{color:#66512c}.text-danger{color:#a94442}a.text-danger:focus,a.text-danger:hover{color:#843534}.bg-primary{color:#fff;background-color:#337ab7}a.bg-primary:focus,a.bg-primary:hover{background-color:#286090}.bg-success{background-color:#dff0d8}a.bg-success:focus,a.bg-success:hover{background-color:#c1e2b3}.bg-info{background-color:#d9edf7}a.bg-info:focus,a.bg-info:hover{background-color:#afd9ee}.bg-warning{background-color:#fcf8e3}a.bg-warning:focus,a.bg-warning:hover{background-color:#f7ecb5}.bg-danger{background-color:#f2dede}a.bg-danger:focus,a.bg-danger:hover{background-color:#e4b9b9}.page-header{padding-bottom:9px;margin:40px 0 20px;border-bottom:1px solid #eee}ol,ul{margin-top:0;margin-bottom:10px}ol ol,ol ul,ul ol,ul ul{margin-bottom:0}.list-unstyled{padding-left:0;list-style:none}.list-inline{padding-left:0;margin-left:-5px;list-style:none}.list-inline>li{display:inline-block;padding-right:5px;padding-left:5px}dl{margin-top:0;margin-bottom:20px}dd,dt{line-height:1.42857143}dt{font-weight:700}dd{margin-left:0}@media (min-width:768px){.dl-horizontal dt{float:left;width:160px;overflow:hidden;clear:left;text-align:right;text-overflow:ellipsis;white-space:nowrap}.dl-horizontal dd{margin-left:180px}}abbr[data-original-title],abbr[title]{cursor:help;border-bottom:1px dotted #777}.initialism{font-size:90%;text-transform:uppercase}blockquote{padding:10px 20px;margin:0 0 20px;font-size:17.5px;border-left:5px solid #eee}blockquote ol:last-child,blockquote p:last-child,blockquote ul:last-child{margin-bottom:0}blockquote .small,blockquote footer,blockquote small{display:block;font-size:80%;line-height:1.42857143;color:#777}blockquote .small:before,blockquote footer:before,blockquote small:before{content:'\2014 \00A0'}.blockquote-reverse,blockquote.pull-right{padding-right:15px;padding-left:0;text-align:right;border-right:5px solid #eee;border-left:0}.blockquote-reverse .small:before,.blockquote-reverse footer:before,.blockquote-reverse small:before,blockquote.pull-right .small:before,blockquote.pull-right footer:before,blockquote.pull-right small:before{content:''}.blockquote-reverse .small:after,.blockquote-reverse footer:after,.blockquote-reverse small:after,blockquote.pull-right .small:after,blockquote.pull-right footer:after,blockquote.pull-right small:after{content:'\00A0 \2014'}address{margin-bottom:20px;font-style:normal;line-height:1.42857143}code,kbd,pre,samp{font-family:Menlo,Monaco,Consolas,"Courier New",monospace}code{padding:2px 4px;font-size:90%;color:#c7254e;background-color:#f9f2f4;border-radius:4px}kbd{padding:2px 4px;font-size:90%;color:#fff;background-color:#333;border-radius:3px;-webkit-box-shadow:inset 0 -1px 0 rgba(0,0,0,.25);box-shadow:inset 0 -1px 0 rgba(0,0,0,.25)}kbd kbd{padding:0;font-size:100%;font-weight:700;-webkit-box-shadow:none;box-shadow:none}pre{display:block;padding:9.5px;margin:0 0 10px;font-size:13px;line-height:1.42857143;color:#333;word-break:break-all;word-wrap:break-word;background-color:#f5f5f5;border:1px solid #ccc;border-radius:4px}pre code{padding:0;font-size:inherit;color:inherit;white-space:pre-wrap;background-color:transparent;border-radius:0}.pre-scrollable{max-height:340px;overflow-y:scroll}.container{padding-right:15px;padding-left:15px;margin-right:auto;margin-left:auto}@media (min-width:768px){.container{width:750px}}@media (min-width:992px){.container{width:970px}}@media (min-width:1200px){.container{width:1170px}}.container-fluid{padding-right:15px;padding-left:15px;margin-right:auto;margin-left:auto}.row{margin-right:-15px;margin-left:-15px}.col-lg-1,.col-lg-10,.col-lg-11,.col-lg-12,.col-lg-2,.col-lg-3,.col-lg-4,.col-lg-5,.col-lg-6,.col-lg-7,.col-lg-8,.col-lg-9,.col-md-1,.col-md-10,.col-md-11,.col-md-12,.col-md-2,.col-md-3,.col-md-4,.col-md-5,.col-md-6,.col-md-7,.col-md-8,.col-md-9,.col-sm-1,.col-sm-10,.col-sm-11,.col-sm-12,.col-sm-2,.col-sm-3,.col-sm-4,.col-sm-5,.col-sm-6,.col-sm-7,.col-sm-8,.col-sm-9,.col-xs-1,.col-xs-10,.col-xs-11,.col-xs-12,.col-xs-2,.col-xs-3,.col-xs-4,.col-xs-5,.col-xs-6,.col-xs-7,.col-xs-8,.col-xs-9{position:relative;min-height:1px;padding-right:15px;padding-left:15px}.col-xs-1,.col-xs-10,.col-xs-11,.col-xs-12,.col-xs-2,.col-xs-3,.col-xs-4,.col-xs-5,.col-xs-6,.col-xs-7,.col-xs-8,.col-xs-9{float:left}.col-xs-12{width:100%}.col-xs-11{width:91.66666667%}.col-xs-10{width:83.33333333%}.col-xs-9{width:75%}.col-xs-8{width:66.66666667%}.col-xs-7{width:58.33333333%}.col-xs-6{width:50%}.col-xs-5{width:41.66666667%}.col-xs-4{width:33.33333333%}.col-xs-3{width:25%}.col-xs-2{width:16.66666667%}.col-xs-1{width:8.33333333%}.col-xs-pull-12{right:100%}.col-xs-pull-11{right:91.66666667%}.col-xs-pull-10{right:83.33333333%}.col-xs-pull-9{right:75%}.col-xs-pull-8{right:66.66666667%}.col-xs-pull-7{right:58.33333333%}.col-xs-pull-6{right:50%}.col-xs-pull-5{right:41.66666667%}.col-xs-pull-4{right:33.33333333%}.col-xs-pull-3{right:25%}.col-xs-pull-2{right:16.66666667%}.col-xs-pull-1{right:8.33333333%}.col-xs-pull-0{right:auto}.col-xs-push-12{left:100%}.col-xs-push-11{left:91.66666667%}.col-xs-push-10{left:83.33333333%}.col-xs-push-9{left:75%}.col-xs-push-8{left:66.66666667%}.col-xs-push-7{left:58.33333333%}.col-xs-push-6{left:50%}.col-xs-push-5{left:41.66666667%}.col-xs-push-4{left:33.33333333%}.col-xs-push-3{left:25%}.col-xs-push-2{left:16.66666667%}.col-xs-push-1{left:8.33333333%}.col-xs-push-0{left:auto}.col-xs-offset-12{margin-left:100%}.col-xs-offset-11{margin-left:91.66666667%}.col-xs-offset-10{margin-left:83.33333333%}.col-xs-offset-9{margin-left:75%}.col-xs-offset-8{margin-left:66.66666667%}.col-xs-offset-7{margin-left:58.33333333%}.col-xs-offset-6{margin-left:50%}.col-xs-offset-5{margin-left:41.66666667%}.col-xs-offset-4{margin-left:33.33333333%}.col-xs-offset-3{margin-left:25%}.col-xs-offset-2{margin-left:16.66666667%}.col-xs-offset-1{margin-left:8.33333333%}.col-xs-offset-0{margin-left:0}@media (min-width:768px){.col-sm-1,.col-sm-10,.col-sm-11,.col-sm-12,.col-sm-2,.col-sm-3,.col-sm-4,.col-sm-5,.col-sm-6,.col-sm-7,.col-sm-8,.col-sm-9{float:left}.col-sm-12{width:100%}.col-sm-11{width:91.66666667%}.col-sm-10{width:83.33333333%}.col-sm-9{width:75%}.col-sm-8{width:66.66666667%}.col-sm-7{width:58.33333333%}.col-sm-6{width:50%}.col-sm-5{width:41.66666667%}.col-sm-4{width:33.33333333%}.col-sm-3{width:25%}.col-sm-2{width:16.66666667%}.col-sm-1{width:8.33333333%}.col-sm-pull-12{right:100%}.col-sm-pull-11{right:91.66666667%}.col-sm-pull-10{right:83.33333333%}.col-sm-pull-9{right:75%}.col-sm-pull-8{right:66.66666667%}.col-sm-pull-7{right:58.33333333%}.col-sm-pull-6{right:50%}.col-sm-pull-5{right:41.66666667%}.col-sm-pull-4{right:33.33333333%}.col-sm-pull-3{right:25%}.col-sm-pull-2{right:16.66666667%}.col-sm-pull-1{right:8.33333333%}.col-sm-pull-0{right:auto}.col-sm-push-12{left:100%}.col-sm-push-11{left:91.66666667%}.col-sm-push-10{left:83.33333333%}.col-sm-push-9{left:75%}.col-sm-push-8{left:66.66666667%}.col-sm-push-7{left:58.33333333%}.col-sm-push-6{left:50%}.col-sm-push-5{left:41.66666667%}.col-sm-push-4{left:33.33333333%}.col-sm-push-3{left:25%}.col-sm-push-2{left:16.66666667%}.col-sm-push-1{left:8.33333333%}.col-sm-push-0{left:auto}.col-sm-offset-12{margin-left:100%}.col-sm-offset-11{margin-left:91.66666667%}.col-sm-offset-10{margin-left:83.33333333%}.col-sm-offset-9{margin-left:75%}.col-sm-offset-8{margin-left:66.66666667%}.col-sm-offset-7{margin-left:58.33333333%}.col-sm-offset-6{margin-left:50%}.col-sm-offset-5{margin-left:41.66666667%}.col-sm-offset-4{margin-left:33.33333333%}.col-sm-offset-3{margin-left:25%}.col-sm-offset-2{margin-left:16.66666667%}.col-sm-offset-1{margin-left:8.33333333%}.col-sm-offset-0{margin-left:0}}@media (min-width:992px){.col-md-1,.col-md-10,.col-md-11,.col-md-12,.col-md-2,.col-md-3,.col-md-4,.col-md-5,.col-md-6,.col-md-7,.col-md-8,.col-md-9{float:left}.col-md-12{width:100%}.col-md-11{width:91.66666667%}.col-md-10{width:83.33333333%}.col-md-9{width:75%}.col-md-8{width:66.66666667%}.col-md-7{width:58.33333333%}.col-md-6{width:50%}.col-md-5{width:41.66666667%}.col-md-4{width:33.33333333%}.col-md-3{width:25%}.col-md-2{width:16.66666667%}.col-md-1{width:8.33333333%}.col-md-pull-12{right:100%}.col-md-pull-11{right:91.66666667%}.col-md-pull-10{right:83.33333333%}.col-md-pull-9{right:75%}.col-md-pull-8{right:66.66666667%}.col-md-pull-7{right:58.33333333%}.col-md-pull-6{right:50%}.col-md-pull-5{right:41.66666667%}.col-md-pull-4{right:33.33333333%}.col-md-pull-3{right:25%}.col-md-pull-2{right:16.66666667%}.col-md-pull-1{right:8.33333333%}.col-md-pull-0{right:auto}.col-md-push-12{left:100%}.col-md-push-11{left:91.66666667%}.col-md-push-10{left:83.33333333%}.col-md-push-9{left:75%}.col-md-push-8{left:66.66666667%}.col-md-push-7{left:58.33333333%}.col-md-push-6{left:50%}.col-md-push-5{left:41.66666667%}.col-md-push-4{left:33.33333333%}.col-md-push-3{left:25%}.col-md-push-2{left:16.66666667%}.col-md-push-1{left:8.33333333%}.col-md-push-0{left:auto}.col-md-offset-12{margin-left:100%}.col-md-offset-11{margin-left:91.66666667%}.col-md-offset-10{margin-left:83.33333333%}.col-md-offset-9{margin-left:75%}.col-md-offset-8{margin-left:66.66666667%}.col-md-offset-7{margin-left:58.33333333%}.col-md-offset-6{margin-left:50%}.col-md-offset-5{margin-left:41.66666667%}.col-md-offset-4{margin-left:33.33333333%}.col-md-offset-3{margin-left:25%}.col-md-offset-2{margin-left:16.66666667%}.col-md-offset-1{margin-left:8.33333333%}.col-md-offset-0{margin-left:0}}@media (min-width:1200px){.col-lg-1,.col-lg-10,.col-lg-11,.col-lg-12,.col-lg-2,.col-lg-3,.col-lg-4,.col-lg-5,.col-lg-6,.col-lg-7,.col-lg-8,.col-lg-9{float:left}.col-lg-12{width:100%}.col-lg-11{width:91.66666667%}.col-lg-10{width:83.33333333%}.col-lg-9{width:75%}.col-lg-8{width:66.66666667%}.col-lg-7{width:58.33333333%}.col-lg-6{width:50%}.col-lg-5{width:41.66666667%}.col-lg-4{width:33.33333333%}.col-lg-3{width:25%}.col-lg-2{width:16.66666667%}.col-lg-1{width:8.33333333%}.col-lg-pull-12{right:100%}.col-lg-pull-11{right:91.66666667%}.col-lg-pull-10{right:83.33333333%}.col-lg-pull-9{right:75%}.col-lg-pull-8{right:66.66666667%}.col-lg-pull-7{right:58.33333333%}.col-lg-pull-6{right:50%}.col-lg-pull-5{right:41.66666667%}.col-lg-pull-4{right:33.33333333%}.col-lg-pull-3{right:25%}.col-lg-pull-2{right:16.66666667%}.col-lg-pull-1{right:8.33333333%}.col-lg-pull-0{right:auto}.col-lg-push-12{left:100%}.col-lg-push-11{left:91.66666667%}.col-lg-push-10{left:83.33333333%}.col-lg-push-9{left:75%}.col-lg-push-8{left:66.66666667%}.col-lg-push-7{left:58.33333333%}.col-lg-push-6{left:50%}.col-lg-push-5{left:41.66666667%}.col-lg-push-4{left:33.33333333%}.col-lg-push-3{left:25%}.col-lg-push-2{left:16.66666667%}.col-lg-push-1{left:8.33333333%}.col-lg-push-0{left:auto}.col-lg-offset-12{margin-left:100%}.col-lg-offset-11{margin-left:91.66666667%}.col-lg-offset-10{margin-left:83.33333333%}.col-lg-offset-9{margin-left:75%}.col-lg-offset-8{margin-left:66.66666667%}.col-lg-offset-7{margin-left:58.33333333%}.col-lg-offset-6{margin-left:50%}.col-lg-offset-5{margin-left:41.66666667%}.col-lg-offset-4{margin-left:33.33333333%}.col-lg-offset-3{margin-left:25%}.col-lg-offset-2{margin-left:16.66666667%}.col-lg-offset-1{margin-left:8.33333333%}.col-lg-offset-0{margin-left:0}}table{background-color:transparent}caption{padding-top:8px;padding-bottom:8px;color:#777;text-align:left}th{text-align:left}.table{width:100%;max-width:100%;margin-bottom:20px}.table>tbody>tr>td,.table>tbody>tr>th,.table>tfoot>tr>td,.table>tfoot>tr>th,.table>thead>tr>td,.table>thead>tr>th{padding:8px;line-height:1.42857143;vertical-align:top;border-top:1px solid #ddd}.table>thead>tr>th{vertical-align:bottom;border-bottom:2px solid #ddd}.table>caption+thead>tr:first-child>td,.table>caption+thead>tr:first-child>th,.table>colgroup+thead>tr:first-child>td,.table>colgroup+thead>tr:first-child>th,.table>thead:first-child>tr:first-child>td,.table>thead:first-child>tr:first-child>th{border-top:0}.table>tbody+tbody{border-top:2px solid #ddd}.table .table{background-color:#fff}.table-condensed>tbody>tr>td,.table-condensed>tbody>tr>th,.table-condensed>tfoot>tr>td,.table-condensed>tfoot>tr>th,.table-condensed>thead>tr>td,.table-condensed>thead>tr>th{padding:5px}.table-bordered{border:1px solid #ddd}.table-bordered>tbody>tr>td,.table-bordered>tbody>tr>th,.table-bordered>tfoot>tr>td,.table-bordered>tfoot>tr>th,.table-bordered>thead>tr>td,.table-bordered>thead>tr>th{border:1px solid #ddd}.table-bordered>thead>tr>td,.table-bordered>thead>tr>th{border-bottom-width:2px}.table-striped>tbody>tr:nth-of-type(odd){background-color:#f9f9f9}.table-hover>tbody>tr:hover{background-color:#f5f5f5}table col[class*=col-]{position:static;display:table-column;float:none}table td[class*=col-],table th[class*=col-]{position:static;display:table-cell;float:none}.table>tbody>tr.active>td,.table>tbody>tr.active>th,.table>tbody>tr>td.active,.table>tbody>tr>th.active,.table>tfoot>tr.active>td,.table>tfoot>tr.active>th,.table>tfoot>tr>td.active,.table>tfoot>tr>th.active,.table>thead>tr.active>td,.table>thead>tr.active>th,.table>thead>tr>td.active,.table>thead>tr>th.active{background-color:#f5f5f5}.table-hover>tbody>tr.active:hover>td,.table-hover>tbody>tr.active:hover>th,.table-hover>tbody>tr:hover>.active,.table-hover>tbody>tr>td.active:hover,.table-hover>tbody>tr>th.active:hover{background-color:#e8e8e8}.table>tbody>tr.success>td,.table>tbody>tr.success>th,.table>tbody>tr>td.success,.table>tbody>tr>th.success,.table>tfoot>tr.success>td,.table>tfoot>tr.success>th,.table>tfoot>tr>td.success,.table>tfoot>tr>th.success,.table>thead>tr.success>td,.table>thead>tr.success>th,.table>thead>tr>td.success,.table>thead>tr>th.success{background-color:#dff0d8}.table-hover>tbody>tr.success:hover>td,.table-hover>tbody>tr.success:hover>th,.table-hover>tbody>tr:hover>.success,.table-hover>tbody>tr>td.success:hover,.table-hover>tbody>tr>th.success:hover{background-color:#d0e9c6}.table>tbody>tr.info>td,.table>tbody>tr.info>th,.table>tbody>tr>td.info,.table>tbody>tr>th.info,.table>tfoot>tr.info>td,.table>tfoot>tr.info>th,.table>tfoot>tr>td.info,.table>tfoot>tr>th.info,.table>thead>tr.info>td,.table>thead>tr.info>th,.table>thead>tr>td.info,.table>thead>tr>th.info{background-color:#d9edf7}.table-hover>tbody>tr.info:hover>td,.table-hover>tbody>tr.info:hover>th,.table-hover>tbody>tr:hover>.info,.table-hover>tbody>tr>td.info:hover,.table-hover>tbody>tr>th.info:hover{background-color:#c4e3f3}.table>tbody>tr.warning>td,.table>tbody>tr.warning>th,.table>tbody>tr>td.warning,.table>tbody>tr>th.warning,.table>tfoot>tr.warning>td,.table>tfoot>tr.warning>th,.table>tfoot>tr>td.warning,.table>tfoot>tr>th.warning,.table>thead>tr.warning>td,.table>thead>tr.warning>th,.table>thead>tr>td.warning,.table>thead>tr>th.warning{background-color:#fcf8e3}.table-hover>tbody>tr.warning:hover>td,.table-hover>tbody>tr.warning:hover>th,.table-hover>tbody>tr:hover>.warning,.table-hover>tbody>tr>td.warning:hover,.table-hover>tbody>tr>th.warning:hover{background-color:#faf2cc}.table>tbody>tr.danger>td,.table>tbody>tr.danger>th,.table>tbody>tr>td.danger,.table>tbody>tr>th.danger,.table>tfoot>tr.danger>td,.table>tfoot>tr.danger>th,.table>tfoot>tr>td.danger,.table>tfoot>tr>th.danger,.table>thead>tr.danger>td,.table>thead>tr.danger>th,.table>thead>tr>td.danger,.table>thead>tr>th.danger{background-color:#f2dede}.table-hover>tbody>tr.danger:hover>td,.table-hover>tbody>tr.danger:hover>th,.table-hover>tbody>tr:hover>.danger,.table-hover>tbody>tr>td.danger:hover,.table-hover>tbody>tr>th.danger:hover{background-color:#ebcccc}.table-responsive{min-height:.01%;overflow-x:auto}@media screen and (max-width:767px){.table-responsive{width:100%;margin-bottom:15px;overflow-y:hidden;-ms-overflow-style:-ms-autohiding-scrollbar;border:1px solid #ddd}.table-responsive>.table{margin-bottom:0}.table-responsive>.table>tbody>tr>td,.table-responsive>.table>tbody>tr>th,.table-responsive>.table>tfoot>tr>td,.table-responsive>.table>tfoot>tr>th,.table-responsive>.table>thead>tr>td,.table-responsive>.table>thead>tr>th{white-space:nowrap}.table-responsive>.table-bordered{border:0}.table-responsive>.table-bordered>tbody>tr>td:first-child,.table-responsive>.table-bordered>tbody>tr>th:first-child,.table-responsive>.table-bordered>tfoot>tr>td:first-child,.table-responsive>.table-bordered>tfoot>tr>th:first-child,.table-responsive>.table-bordered>thead>tr>td:first-child,.table-responsive>.table-bordered>thead>tr>th:first-child{border-left:0}.table-responsive>.table-bordered>tbody>tr>td:last-child,.table-responsive>.table-bordered>tbody>tr>th:last-child,.table-responsive>.table-bordered>tfoot>tr>td:last-child,.table-responsive>.table-bordered>tfoot>tr>th:last-child,.table-responsive>.table-bordered>thead>tr>td:last-child,.table-responsive>.table-bordered>thead>tr>th:last-child{border-right:0}.table-responsive>.table-bordered>tbody>tr:last-child>td,.table-responsive>.table-bordered>tbody>tr:last-child>th,.table-responsive>.table-bordered>tfoot>tr:last-child>td,.table-responsive>.table-bordered>tfoot>tr:last-child>th{border-bottom:0}}fieldset{min-width:0;padding:0;margin:0;border:0}legend{display:block;width:100%;padding:0;margin-bottom:20px;font-size:21px;line-height:inherit;color:#333;border:0;border-bottom:1px solid #e5e5e5}label{display:inline-block;max-width:100%;margin-bottom:5px;font-weight:700}input[type=search]{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}input[type=checkbox],input[type=radio]{margin:4px 0 0;margin-top:1px\9;line-height:normal}input[type=file]{display:block}input[type=range]{display:block;width:100%}select[multiple],select[size]{height:auto}input[type=file]:focus,input[type=checkbox]:focus,input[type=radio]:focus{outline:5px auto -webkit-focus-ring-color;outline-offset:-2px}output{display:block;padding-top:7px;font-size:14px;line-height:1.42857143;color:#555}.form-control{display:block;width:100%;height:34px;padding:6px 12px;font-size:14px;line-height:1.42857143;color:#555;background-color:#fff;background-image:none;border:1px solid #ccc;border-radius:4px;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075);box-shadow:inset 0 1px 1px rgba(0,0,0,.075);-webkit-transition:border-color ease-in-out .15s,-webkit-box-shadow ease-in-out .15s;-o-transition:border-color ease-in-out .15s,box-shadow ease-in-out .15s;transition:border-color ease-in-out .15s,box-shadow ease-in-out .15s}.form-control:focus{border-color:#66afe9;outline:0;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 8px rgba(102,175,233,.6);box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 8px rgba(102,175,233,.6)}.form-control::-moz-placeholder{color:#999;opacity:1}.form-control:-ms-input-placeholder{color:#999}.form-control::-webkit-input-placeholder{color:#999}.form-control::-ms-expand{background-color:transparent;border:0}.form-control[disabled],.form-control[readonly],fieldset[disabled] .form-control{background-color:#eee;opacity:1}.form-control[disabled],fieldset[disabled] .form-control{cursor:not-allowed}textarea.form-control{height:auto}input[type=search]{-webkit-appearance:none}@media screen and (-webkit-min-device-pixel-ratio:0){input[type=date].form-control,input[type=time].form-control,input[type=datetime-local].form-control,input[type=month].form-control{line-height:34px}.input-group-sm input[type=date],.input-group-sm input[type=time],.input-group-sm input[type=datetime-local],.input-group-sm input[type=month],input[type=date].input-sm,input[type=time].input-sm,input[type=datetime-local].input-sm,input[type=month].input-sm{line-height:30px}.input-group-lg input[type=date],.input-group-lg input[type=time],.input-group-lg input[type=datetime-local],.input-group-lg input[type=month],input[type=date].input-lg,input[type=time].input-lg,input[type=datetime-local].input-lg,input[type=month].input-lg{line-height:46px}}.form-group{margin-bottom:15px}.checkbox,.radio{position:relative;display:block;margin-top:10px;margin-bottom:10px}.checkbox label,.radio label{min-height:20px;padding-left:20px;margin-bottom:0;font-weight:400;cursor:pointer}.checkbox input[type=checkbox],.checkbox-inline input[type=checkbox],.radio input[type=radio],.radio-inline input[type=radio]{position:absolute;margin-top:4px\9;margin-left:-20px}.checkbox+.checkbox,.radio+.radio{margin-top:-5px}.checkbox-inline,.radio-inline{position:relative;display:inline-block;padding-left:20px;margin-bottom:0;font-weight:400;vertical-align:middle;cursor:pointer}.checkbox-inline+.checkbox-inline,.radio-inline+.radio-inline{margin-top:0;margin-left:10px}fieldset[disabled] input[type=checkbox],fieldset[disabled] input[type=radio],input[type=checkbox].disabled,input[type=checkbox][disabled],input[type=radio].disabled,input[type=radio][disabled]{cursor:not-allowed}.checkbox-inline.disabled,.radio-inline.disabled,fieldset[disabled] .checkbox-inline,fieldset[disabled] .radio-inline{cursor:not-allowed}.checkbox.disabled label,.radio.disabled label,fieldset[disabled] .checkbox label,fieldset[disabled] .radio label{cursor:not-allowed}.form-control-static{min-height:34px;padding-top:7px;padding-bottom:7px;margin-bottom:0}.form-control-static.input-lg,.form-control-static.input-sm{padding-right:0;padding-left:0}.input-sm{height:30px;padding:5px 10px;font-size:12px;line-height:1.5;border-radius:3px}select.input-sm{height:30px;line-height:30px}select[multiple].input-sm,textarea.input-sm{height:auto}.form-group-sm .form-control{height:30px;padding:5px 10px;font-size:12px;line-height:1.5;border-radius:3px}.form-group-sm select.form-control{height:30px;line-height:30px}.form-group-sm select[multiple].form-control,.form-group-sm textarea.form-control{height:auto}.form-group-sm .form-control-static{height:30px;min-height:32px;padding:6px 10px;font-size:12px;line-height:1.5}.input-lg{height:46px;padding:10px 16px;font-size:18px;line-height:1.3333333;border-radius:6px}select.input-lg{height:46px;line-height:46px}select[multiple].input-lg,textarea.input-lg{height:auto}.form-group-lg .form-control{height:46px;padding:10px 16px;font-size:18px;line-height:1.3333333;border-radius:6px}.form-group-lg select.form-control{height:46px;line-height:46px}.form-group-lg select[multiple].form-control,.form-group-lg textarea.form-control{height:auto}.form-group-lg .form-control-static{height:46px;min-height:38px;padding:11px 16px;font-size:18px;line-height:1.3333333}.has-feedback{position:relative}.has-feedback .form-control{padding-right:42.5px}.form-control-feedback{position:absolute;top:0;right:0;z-index:2;display:block;width:34px;height:34px;line-height:34px;text-align:center;pointer-events:none}.form-group-lg .form-control+.form-control-feedback,.input-group-lg+.form-control-feedback,.input-lg+.form-control-feedback{width:46px;height:46px;line-height:46px}.form-group-sm .form-control+.form-control-feedback,.input-group-sm+.form-control-feedback,.input-sm+.form-control-feedback{width:30px;height:30px;line-height:30px}.has-success .checkbox,.has-success .checkbox-inline,.has-success .control-label,.has-success .help-block,.has-success .radio,.has-success .radio-inline,.has-success.checkbox label,.has-success.checkbox-inline label,.has-success.radio label,.has-success.radio-inline label{color:#3c763d}.has-success .form-control{border-color:#3c763d;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075);box-shadow:inset 0 1px 1px rgba(0,0,0,.075)}.has-success .form-control:focus{border-color:#2b542c;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #67b168;box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #67b168}.has-success .input-group-addon{color:#3c763d;background-color:#dff0d8;border-color:#3c763d}.has-success .form-control-feedback{color:#3c763d}.has-warning .checkbox,.has-warning .checkbox-inline,.has-warning .control-label,.has-warning .help-block,.has-warning .radio,.has-warning .radio-inline,.has-warning.checkbox label,.has-warning.checkbox-inline label,.has-warning.radio label,.has-warning.radio-inline label{color:#8a6d3b}.has-warning .form-control{border-color:#8a6d3b;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075);box-shadow:inset 0 1px 1px rgba(0,0,0,.075)}.has-warning .form-control:focus{border-color:#66512c;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #c0a16b;box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #c0a16b}.has-warning .input-group-addon{color:#8a6d3b;background-color:#fcf8e3;border-color:#8a6d3b}.has-warning .form-control-feedback{color:#8a6d3b}.has-error .checkbox,.has-error .checkbox-inline,.has-error .control-label,.has-error .help-block,.has-error .radio,.has-error .radio-inline,.has-error.checkbox label,.has-error.checkbox-inline label,.has-error.radio label,.has-error.radio-inline label{color:#a94442}.has-error .form-control{border-color:#a94442;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075);box-shadow:inset 0 1px 1px rgba(0,0,0,.075)}.has-error .form-control:focus{border-color:#843534;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #ce8483;box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #ce8483}.has-error .input-group-addon{color:#a94442;background-color:#f2dede;border-color:#a94442}.has-error .form-control-feedback{color:#a94442}.has-feedback label~.form-control-feedback{top:25px}.has-feedback label.sr-only~.form-control-feedback{top:0}.help-block{display:block;margin-top:5px;margin-bottom:10px;color:#737373}@media (min-width:768px){.form-inline .form-group{display:inline-block;margin-bottom:0;vertical-align:middle}.form-inline .form-control{display:inline-block;width:auto;vertical-align:middle}.form-inline .form-control-static{display:inline-block}.form-inline .input-group{display:inline-table;vertical-align:middle}.form-inline .input-group .form-control,.form-inline .input-group .input-group-addon,.form-inline .input-group .input-group-btn{width:auto}.form-inline .input-group>.form-control{width:100%}.form-inline .control-label{margin-bottom:0;vertical-align:middle}.form-inline .checkbox,.form-inline .radio{display:inline-block;margin-top:0;margin-bottom:0;vertical-align:middle}.form-inline .checkbox label,.form-inline .radio label{padding-left:0}.form-inline .checkbox input[type=checkbox],.form-inline .radio input[type=radio]{position:relative;margin-left:0}.form-inline .has-feedback .form-control-feedback{top:0}}.form-horizontal .checkbox,.form-horizontal .checkbox-inline,.form-horizontal .radio,.form-horizontal .radio-inline{padding-top:7px;margin-top:0;margin-bottom:0}.form-horizontal .checkbox,.form-horizontal .radio{min-height:27px}.form-horizontal .form-group{margin-right:-15px;margin-left:-15px}@media (min-width:768px){.form-horizontal .control-label{padding-top:7px;margin-bottom:0;text-align:right}}.form-horizontal .has-feedback .form-control-feedback{right:15px}@media (min-width:768px){.form-horizontal .form-group-lg .control-label{padding-top:11px;font-size:18px}}@media (min-width:768px){.form-horizontal .form-group-sm .control-label{padding-top:6px;font-size:12px}}.btn{display:inline-block;padding:6px 12px;margin-bottom:0;font-size:14px;font-weight:400;line-height:1.42857143;text-align:center;white-space:nowrap;vertical-align:middle;-ms-touch-action:manipulation;touch-action:manipulation;cursor:pointer;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;background-image:none;border:1px solid transparent;border-radius:4px}.btn.active.focus,.btn.active:focus,.btn.focus,.btn:active.focus,.btn:active:focus,.btn:focus{outline:5px auto -webkit-focus-ring-color;outline-offset:-2px}.btn.focus,.btn:focus,.btn:hover{color:#333;text-decoration:none}.btn.active,.btn:active{background-image:none;outline:0;-webkit-box-shadow:inset 0 3px 5px rgba(0,0,0,.125);box-shadow:inset 0 3px 5px rgba(0,0,0,.125)}.btn.disabled,.btn[disabled],fieldset[disabled] .btn{cursor:not-allowed;filter:alpha(opacity=65);-webkit-box-shadow:none;box-shadow:none;opacity:.65}a.btn.disabled,fieldset[disabled] a.btn{pointer-events:none}.btn-default{color:#333;background-color:#fff;border-color:#ccc}.btn-default.focus,.btn-default:focus{color:#333;background-color:#e6e6e6;border-color:#8c8c8c}.btn-default:hover{color:#333;background-color:#e6e6e6;border-color:#adadad}.btn-default.active,.btn-default:active,.open>.dropdown-toggle.btn-default{color:#333;background-color:#e6e6e6;border-color:#adadad}.btn-default.active.focus,.btn-default.active:focus,.btn-default.active:hover,.btn-default:active.focus,.btn-default:active:focus,.btn-default:active:hover,.open>.dropdown-toggle.btn-default.focus,.open>.dropdown-toggle.btn-default:focus,.open>.dropdown-toggle.btn-default:hover{color:#333;background-color:#d4d4d4;border-color:#8c8c8c}.btn-default.active,.btn-default:active,.open>.dropdown-toggle.btn-default{background-image:none}.btn-default.disabled.focus,.btn-default.disabled:focus,.btn-default.disabled:hover,.btn-default[disabled].focus,.btn-default[disabled]:focus,.btn-default[disabled]:hover,fieldset[disabled] .btn-default.focus,fieldset[disabled] .btn-default:focus,fieldset[disabled] .btn-default:hover{background-color:#fff;border-color:#ccc}.btn-default .badge{color:#fff;background-color:#333}.btn-primary{color:#fff;background-color:#337ab7;border-color:#2e6da4}.btn-primary.focus,.btn-primary:focus{color:#fff;background-color:#286090;border-color:#122b40}.btn-primary:hover{color:#fff;background-color:#286090;border-color:#204d74}.btn-primary.active,.btn-primary:active,.open>.dropdown-toggle.btn-primary{color:#fff;background-color:#286090;border-color:#204d74}.btn-primary.active.focus,.btn-primary.active:focus,.btn-primary.active:hover,.btn-primary:active.focus,.btn-primary:active:focus,.btn-primary:active:hover,.open>.dropdown-toggle.btn-primary.focus,.open>.dropdown-toggle.btn-primary:focus,.open>.dropdown-toggle.btn-primary:hover{color:#fff;background-color:#204d74;border-color:#122b40}.btn-primary.active,.btn-primary:active,.open>.dropdown-toggle.btn-primary{background-image:none}.btn-primary.disabled.focus,.btn-primary.disabled:focus,.btn-primary.disabled:hover,.btn-primary[disabled].focus,.btn-primary[disabled]:focus,.btn-primary[disabled]:hover,fieldset[disabled] .btn-primary.focus,fieldset[disabled] .btn-primary:focus,fieldset[disabled] .btn-primary:hover{background-color:#337ab7;border-color:#2e6da4}.btn-primary .badge{color:#337ab7;background-color:#fff}.btn-success{color:#fff;background-color:#5cb85c;border-color:#4cae4c}.btn-success.focus,.btn-success:focus{color:#fff;background-color:#449d44;border-color:#255625}.btn-success:hover{color:#fff;background-color:#449d44;border-color:#398439}.btn-success.active,.btn-success:active,.open>.dropdown-toggle.btn-success{color:#fff;background-color:#449d44;border-color:#398439}.btn-success.active.focus,.btn-success.active:focus,.btn-success.active:hover,.btn-success:active.focus,.btn-success:active:focus,.btn-success:active:hover,.open>.dropdown-toggle.btn-success.focus,.open>.dropdown-toggle.btn-success:focus,.open>.dropdown-toggle.btn-success:hover{color:#fff;background-color:#398439;border-color:#255625}.btn-success.active,.btn-success:active,.open>.dropdown-toggle.btn-success{background-image:none}.btn-success.disabled.focus,.btn-success.disabled:focus,.btn-success.disabled:hover,.btn-success[disabled].focus,.btn-success[disabled]:focus,.btn-success[disabled]:hover,fieldset[disabled] .btn-success.focus,fieldset[disabled] .btn-success:focus,fieldset[disabled] .btn-success:hover{background-color:#5cb85c;border-color:#4cae4c}.btn-success .badge{color:#5cb85c;background-color:#fff}.btn-info{color:#fff;background-color:#5bc0de;border-color:#46b8da}.btn-info.focus,.btn-info:focus{color:#fff;background-color:#31b0d5;border-color:#1b6d85}.btn-info:hover{color:#fff;background-color:#31b0d5;border-color:#269abc}.btn-info.active,.btn-info:active,.open>.dropdown-toggle.btn-info{color:#fff;background-color:#31b0d5;border-color:#269abc}.btn-info.active.focus,.btn-info.active:focus,.btn-info.active:hover,.btn-info:active.focus,.btn-info:active:focus,.btn-info:active:hover,.open>.dropdown-toggle.btn-info.focus,.open>.dropdown-toggle.btn-info:focus,.open>.dropdown-toggle.btn-info:hover{color:#fff;background-color:#269abc;border-color:#1b6d85}.btn-info.active,.btn-info:active,.open>.dropdown-toggle.btn-info{background-image:none}.btn-info.disabled.focus,.btn-info.disabled:focus,.btn-info.disabled:hover,.btn-info[disabled].focus,.btn-info[disabled]:focus,.btn-info[disabled]:hover,fieldset[disabled] .btn-info.focus,fieldset[disabled] .btn-info:focus,fieldset[disabled] .btn-info:hover{background-color:#5bc0de;border-color:#46b8da}.btn-info .badge{color:#5bc0de;background-color:#fff}.btn-warning{color:#fff;background-color:#f0ad4e;border-color:#eea236}.btn-warning.focus,.btn-warning:focus{color:#fff;background-color:#ec971f;border-color:#985f0d}.btn-warning:hover{color:#fff;background-color:#ec971f;border-color:#d58512}.btn-warning.active,.btn-warning:active,.open>.dropdown-toggle.btn-warning{color:#fff;background-color:#ec971f;border-color:#d58512}.btn-warning.active.focus,.btn-warning.active:focus,.btn-warning.active:hover,.btn-warning:active.focus,.btn-warning:active:focus,.btn-warning:active:hover,.open>.dropdown-toggle.btn-warning.focus,.open>.dropdown-toggle.btn-warning:focus,.open>.dropdown-toggle.btn-warning:hover{color:#fff;background-color:#d58512;border-color:#985f0d}.btn-warning.active,.btn-warning:active,.open>.dropdown-toggle.btn-warning{background-image:none}.btn-warning.disabled.focus,.btn-warning.disabled:focus,.btn-warning.disabled:hover,.btn-warning[disabled].focus,.btn-warning[disabled]:focus,.btn-warning[disabled]:hover,fieldset[disabled] .btn-warning.focus,fieldset[disabled] .btn-warning:focus,fieldset[disabled] .btn-warning:hover{background-color:#f0ad4e;border-color:#eea236}.btn-warning .badge{color:#f0ad4e;background-color:#fff}.btn-danger{color:#fff;background-color:#d9534f;border-color:#d43f3a}.btn-danger.focus,.btn-danger:focus{color:#fff;background-color:#c9302c;border-color:#761c19}.btn-danger:hover{color:#fff;background-color:#c9302c;border-color:#ac2925}.btn-danger.active,.btn-danger:active,.open>.dropdown-toggle.btn-danger{color:#fff;background-color:#c9302c;border-color:#ac2925}.btn-danger.active.focus,.btn-danger.active:focus,.btn-danger.active:hover,.btn-danger:active.focus,.btn-danger:active:focus,.btn-danger:active:hover,.open>.dropdown-toggle.btn-danger.focus,.open>.dropdown-toggle.btn-danger:focus,.open>.dropdown-toggle.btn-danger:hover{color:#fff;background-color:#ac2925;border-color:#761c19}.btn-danger.active,.btn-danger:active,.open>.dropdown-toggle.btn-danger{background-image:none}.btn-danger.disabled.focus,.btn-danger.disabled:focus,.btn-danger.disabled:hover,.btn-danger[disabled].focus,.btn-danger[disabled]:focus,.btn-danger[disabled]:hover,fieldset[disabled] .btn-danger.focus,fieldset[disabled] .btn-danger:focus,fieldset[disabled] .btn-danger:hover{background-color:#d9534f;border-color:#d43f3a}.btn-danger .badge{color:#d9534f;background-color:#fff}.btn-link{font-weight:400;color:#337ab7;border-radius:0}.btn-link,.btn-link.active,.btn-link:active,.btn-link[disabled],fieldset[disabled] .btn-link{background-color:transparent;-webkit-box-shadow:none;box-shadow:none}.btn-link,.btn-link:active,.btn-link:focus,.btn-link:hover{border-color:transparent}.btn-link:focus,.btn-link:hover{color:#23527c;text-decoration:underline;background-color:transparent}.btn-link[disabled]:focus,.btn-link[disabled]:hover,fieldset[disabled] .btn-link:focus,fieldset[disabled] .btn-link:hover{color:#777;text-decoration:none}.btn-group-lg>.btn,.btn-lg{padding:10px 16px;font-size:18px;line-height:1.3333333;border-radius:6px}.btn-group-sm>.btn,.btn-sm{padding:5px 10px;font-size:12px;line-height:1.5;border-radius:3px}.btn-group-xs>.btn,.btn-xs{padding:1px 5px;font-size:12px;line-height:1.5;border-radius:3px}.btn-block{display:block;width:100%}.btn-block+.btn-block{margin-top:5px}input[type=button].btn-block,input[type=reset].btn-block,input[type=submit].btn-block{width:100%}.fade{opacity:0;-webkit-transition:opacity .15s linear;-o-transition:opacity .15s linear;transition:opacity .15s linear}.fade.in{opacity:1}.collapse{display:none}.collapse.in{display:block}tr.collapse.in{display:table-row}tbody.collapse.in{display:table-row-group}.collapsing{position:relative;height:0;overflow:hidden;-webkit-transition-timing-function:ease;-o-transition-timing-function:ease;transition-timing-function:ease;-webkit-transition-duration:.35s;-o-transition-duration:.35s;transition-duration:.35s;-webkit-transition-property:height,visibility;-o-transition-property:height,visibility;transition-property:height,visibility}.caret{display:inline-block;width:0;height:0;margin-left:2px;vertical-align:middle;border-top:4px dashed;border-top:4px solid\9;border-right:4px solid transparent;border-left:4px solid transparent}.dropdown,.dropup{position:relative}.dropdown-toggle:focus{outline:0}.dropdown-menu{position:absolute;top:100%;left:0;z-index:1000;display:none;float:left;min-width:160px;padding:5px 0;margin:2px 0 0;font-size:14px;text-align:left;list-style:none;background-color:#fff;-webkit-background-clip:padding-box;background-clip:padding-box;border:1px solid #ccc;border:1px solid rgba(0,0,0,.15);border-radius:4px;-webkit-box-shadow:0 6px 12px rgba(0,0,0,.175);box-shadow:0 6px 12px rgba(0,0,0,.175)}.dropdown-menu.pull-right{right:0;left:auto}.dropdown-menu .divider{height:1px;margin:9px 0;overflow:hidden;background-color:#e5e5e5}.dropdown-menu>li>a{display:block;padding:3px 20px;clear:both;font-weight:400;line-height:1.42857143;color:#333;white-space:nowrap}.dropdown-menu>li>a:focus,.dropdown-menu>li>a:hover{color:#262626;text-decoration:none;background-color:#f5f5f5}.dropdown-menu>.active>a,.dropdown-menu>.active>a:focus,.dropdown-menu>.active>a:hover{color:#fff;text-decoration:none;background-color:#337ab7;outline:0}.dropdown-menu>.disabled>a,.dropdown-menu>.disabled>a:focus,.dropdown-menu>.disabled>a:hover{color:#777}.dropdown-menu>.disabled>a:focus,.dropdown-menu>.disabled>a:hover{text-decoration:none;cursor:not-allowed;background-color:transparent;background-image:none;filter:progid:DXImageTransform.Microsoft.gradient(enabled=false)}.open>.dropdown-menu{display:block}.open>a{outline:0}.dropdown-menu-right{right:0;left:auto}.dropdown-menu-left{right:auto;left:0}.dropdown-header{display:block;padding:3px 20px;font-size:12px;line-height:1.42857143;color:#777;white-space:nowrap}.dropdown-backdrop{position:fixed;top:0;right:0;bottom:0;left:0;z-index:990}.pull-right>.dropdown-menu{right:0;left:auto}.dropup .caret,.navbar-fixed-bottom .dropdown .caret{content:"";border-top:0;border-bottom:4px dashed;border-bottom:4px solid\9}.dropup .dropdown-menu,.navbar-fixed-bottom .dropdown .dropdown-menu{top:auto;bottom:100%;margin-bottom:2px}@media (min-width:768px){.navbar-right .dropdown-menu{right:0;left:auto}.navbar-right .dropdown-menu-left{right:auto;left:0}}.btn-group,.btn-group-vertical{position:relative;display:inline-block;vertical-align:middle}.btn-group-vertical>.btn,.btn-group>.btn{position:relative;float:left}.btn-group-vertical>.btn.active,.btn-group-vertical>.btn:active,.btn-group-vertical>.btn:focus,.btn-group-vertical>.btn:hover,.btn-group>.btn.active,.btn-group>.btn:active,.btn-group>.btn:focus,.btn-group>.btn:hover{z-index:2}.btn-group .btn+.btn,.btn-group .btn+.btn-group,.btn-group .btn-group+.btn,.btn-group .btn-group+.btn-group{margin-left:-1px}.btn-toolbar{margin-left:-5px}.btn-toolbar .btn,.btn-toolbar .btn-group,.btn-toolbar .input-group{float:left}.btn-toolbar>.btn,.btn-toolbar>.btn-group,.btn-toolbar>.input-group{margin-left:5px}.btn-group>.btn:not(:first-child):not(:last-child):not(.dropdown-toggle){border-radius:0}.btn-group>.btn:first-child{margin-left:0}.btn-group>.btn:first-child:not(:last-child):not(.dropdown-toggle){border-top-right-radius:0;border-bottom-right-radius:0}.btn-group>.btn:last-child:not(:first-child),.btn-group>.dropdown-toggle:not(:first-child){border-top-left-radius:0;border-bottom-left-radius:0}.btn-group>.btn-group{float:left}.btn-group>.btn-group:not(:first-child):not(:last-child)>.btn{border-radius:0}.btn-group>.btn-group:first-child:not(:last-child)>.btn:last-child,.btn-group>.btn-group:first-child:not(:last-child)>.dropdown-toggle{border-top-right-radius:0;border-bottom-right-radius:0}.btn-group>.btn-group:last-child:not(:first-child)>.btn:first-child{border-top-left-radius:0;border-bottom-left-radius:0}.btn-group .dropdown-toggle:active,.btn-group.open .dropdown-toggle{outline:0}.btn-group>.btn+.dropdown-toggle{padding-right:8px;padding-left:8px}.btn-group>.btn-lg+.dropdown-toggle{padding-right:12px;padding-left:12px}.btn-group.open .dropdown-toggle{-webkit-box-shadow:inset 0 3px 5px rgba(0,0,0,.125);box-shadow:inset 0 3px 5px rgba(0,0,0,.125)}.btn-group.open .dropdown-toggle.btn-link{-webkit-box-shadow:none;box-shadow:none}.btn .caret{margin-left:0}.btn-lg .caret{border-width:5px 5px 0;border-bottom-width:0}.dropup .btn-lg .caret{border-width:0 5px 5px}.btn-group-vertical>.btn,.btn-group-vertical>.btn-group,.btn-group-vertical>.btn-group>.btn{display:block;float:none;width:100%;max-width:100%}.btn-group-vertical>.btn-group>.btn{float:none}.btn-group-vertical>.btn+.btn,.btn-group-vertical>.btn+.btn-group,.btn-group-vertical>.btn-group+.btn,.btn-group-vertical>.btn-group+.btn-group{margin-top:-1px;margin-left:0}.btn-group-vertical>.btn:not(:first-child):not(:last-child){border-radius:0}.btn-group-vertical>.btn:first-child:not(:last-child){border-top-left-radius:4px;border-top-right-radius:4px;border-bottom-right-radius:0;border-bottom-left-radius:0}.btn-group-vertical>.btn:last-child:not(:first-child){border-top-left-radius:0;border-top-right-radius:0;border-bottom-right-radius:4px;border-bottom-left-radius:4px}.btn-group-vertical>.btn-group:not(:first-child):not(:last-child)>.btn{border-radius:0}.btn-group-vertical>.btn-group:first-child:not(:last-child)>.btn:last-child,.btn-group-vertical>.btn-group:first-child:not(:last-child)>.dropdown-toggle{border-bottom-right-radius:0;border-bottom-left-radius:0}.btn-group-vertical>.btn-group:last-child:not(:first-child)>.btn:first-child{border-top-left-radius:0;border-top-right-radius:0}.btn-group-justified{display:table;width:100%;table-layout:fixed;border-collapse:separate}.btn-group-justified>.btn,.btn-group-justified>.btn-group{display:table-cell;float:none;width:1%}.btn-group-justified>.btn-group .btn{width:100%}.btn-group-justified>.btn-group .dropdown-menu{left:auto}[data-toggle=buttons]>.btn input[type=checkbox],[data-toggle=buttons]>.btn input[type=radio],[data-toggle=buttons]>.btn-group>.btn input[type=checkbox],[data-toggle=buttons]>.btn-group>.btn input[type=radio]{position:absolute;clip:rect(0,0,0,0);pointer-events:none}.input-group{position:relative;display:table;border-collapse:separate}.input-group[class*=col-]{float:none;padding-right:0;padding-left:0}.input-group .form-control{position:relative;z-index:2;float:left;width:100%;margin-bottom:0}.input-group .form-control:focus{z-index:3}.input-group-lg>.form-control,.input-group-lg>.input-group-addon,.input-group-lg>.input-group-btn>.btn{height:46px;padding:10px 16px;font-size:18px;line-height:1.3333333;border-radius:6px}select.input-group-lg>.form-control,select.input-group-lg>.input-group-addon,select.input-group-lg>.input-group-btn>.btn{height:46px;line-height:46px}select[multiple].input-group-lg>.form-control,select[multiple].input-group-lg>.input-group-addon,select[multiple].input-group-lg>.input-group-btn>.btn,textarea.input-group-lg>.form-control,textarea.input-group-lg>.input-group-addon,textarea.input-group-lg>.input-group-btn>.btn{height:auto}.input-group-sm>.form-control,.input-group-sm>.input-group-addon,.input-group-sm>.input-group-btn>.btn{height:30px;padding:5px 10px;font-size:12px;line-height:1.5;border-radius:3px}select.input-group-sm>.form-control,select.input-group-sm>.input-group-addon,select.input-group-sm>.input-group-btn>.btn{height:30px;line-height:30px}select[multiple].input-group-sm>.form-control,select[multiple].input-group-sm>.input-group-addon,select[multiple].input-group-sm>.input-group-btn>.btn,textarea.input-group-sm>.form-control,textarea.input-group-sm>.input-group-addon,textarea.input-group-sm>.input-group-btn>.btn{height:auto}.input-group .form-control,.input-group-addon,.input-group-btn{display:table-cell}.input-group .form-control:not(:first-child):not(:last-child),.input-group-addon:not(:first-child):not(:last-child),.input-group-btn:not(:first-child):not(:last-child){border-radius:0}.input-group-addon,.input-group-btn{width:1%;white-space:nowrap;vertical-align:middle}.input-group-addon{padding:6px 12px;font-size:14px;font-weight:400;line-height:1;color:#555;text-align:center;background-color:#eee;border:1px solid #ccc;border-radius:4px}.input-group-addon.input-sm{padding:5px 10px;font-size:12px;border-radius:3px}.input-group-addon.input-lg{padding:10px 16px;font-size:18px;border-radius:6px}.input-group-addon input[type=checkbox],.input-group-addon input[type=radio]{margin-top:0}.input-group .form-control:first-child,.input-group-addon:first-child,.input-group-btn:first-child>.btn,.input-group-btn:first-child>.btn-group>.btn,.input-group-btn:first-child>.dropdown-toggle,.input-group-btn:last-child>.btn-group:not(:last-child)>.btn,.input-group-btn:last-child>.btn:not(:last-child):not(.dropdown-toggle){border-top-right-radius:0;border-bottom-right-radius:0}.input-group-addon:first-child{border-right:0}.input-group .form-control:last-child,.input-group-addon:last-child,.input-group-btn:first-child>.btn-group:not(:first-child)>.btn,.input-group-btn:first-child>.btn:not(:first-child),.input-group-btn:last-child>.btn,.input-group-btn:last-child>.btn-group>.btn,.input-group-btn:last-child>.dropdown-toggle{border-top-left-radius:0;border-bottom-left-radius:0}.input-group-addon:last-child{border-left:0}.input-group-btn{position:relative;font-size:0;white-space:nowrap}.input-group-btn>.btn{position:relative}.input-group-btn>.btn+.btn{margin-left:-1px}.input-group-btn>.btn:active,.input-group-btn>.btn:focus,.input-group-btn>.btn:hover{z-index:2}.input-group-btn:first-child>.btn,.input-group-btn:first-child>.btn-group{margin-right:-1px}.input-group-btn:last-child>.btn,.input-group-btn:last-child>.btn-group{z-index:2;margin-left:-1px}.nav{padding-left:0;margin-bottom:0;list-style:none}.nav>li{position:relative;display:block}.nav>li>a{position:relative;display:block;padding:10px 15px}.nav>li>a:focus,.nav>li>a:hover{text-decoration:none;background-color:#eee}.nav>li.disabled>a{color:#777}.nav>li.disabled>a:focus,.nav>li.disabled>a:hover{color:#777;text-decoration:none;cursor:not-allowed;background-color:transparent}.nav .open>a,.nav .open>a:focus,.nav .open>a:hover{background-color:#eee;border-color:#337ab7}.nav .nav-divider{height:1px;margin:9px 0;overflow:hidden;background-color:#e5e5e5}.nav>li>a>img{max-width:none}.nav-tabs{border-bottom:1px solid #ddd}.nav-tabs>li{float:left;margin-bottom:-1px}.nav-tabs>li>a{margin-right:2px;line-height:1.42857143;border:1px solid transparent;border-radius:4px 4px 0 0}.nav-tabs>li>a:hover{border-color:#eee #eee #ddd}.nav-tabs>li.active>a,.nav-tabs>li.active>a:focus,.nav-tabs>li.active>a:hover{color:#555;cursor:default;background-color:#fff;border:1px solid #ddd;border-bottom-color:transparent}.nav-tabs.nav-justified{width:100%;border-bottom:0}.nav-tabs.nav-justified>li{float:none}.nav-tabs.nav-justified>li>a{margin-bottom:5px;text-align:center}.nav-tabs.nav-justified>.dropdown .dropdown-menu{top:auto;left:auto}@media (min-width:768px){.nav-tabs.nav-justified>li{display:table-cell;width:1%}.nav-tabs.nav-justified>li>a{margin-bottom:0}}.nav-tabs.nav-justified>li>a{margin-right:0;border-radius:4px}.nav-tabs.nav-justified>.active>a,.nav-tabs.nav-justified>.active>a:focus,.nav-tabs.nav-justified>.active>a:hover{border:1px solid #ddd}@media (min-width:768px){.nav-tabs.nav-justified>li>a{border-bottom:1px solid #ddd;border-radius:4px 4px 0 0}.nav-tabs.nav-justified>.active>a,.nav-tabs.nav-justified>.active>a:focus,.nav-tabs.nav-justified>.active>a:hover{border-bottom-color:#fff}}.nav-pills>li{float:left}.nav-pills>li>a{border-radius:4px}.nav-pills>li+li{margin-left:2px}.nav-pills>li.active>a,.nav-pills>li.active>a:focus,.nav-pills>li.active>a:hover{color:#fff;background-color:#337ab7}.nav-stacked>li{float:none}.nav-stacked>li+li{margin-top:2px;margin-left:0}.nav-justified{width:100%}.nav-justified>li{float:none}.nav-justified>li>a{margin-bottom:5px;text-align:center}.nav-justified>.dropdown .dropdown-menu{top:auto;left:auto}@media (min-width:768px){.nav-justified>li{display:table-cell;width:1%}.nav-justified>li>a{margin-bottom:0}}.nav-tabs-justified{border-bottom:0}.nav-tabs-justified>li>a{margin-right:0;border-radius:4px}.nav-tabs-justified>.active>a,.nav-tabs-justified>.active>a:focus,.nav-tabs-justified>.active>a:hover{border:1px solid #ddd}@media (min-width:768px){.nav-tabs-justified>li>a{border-bottom:1px solid #ddd;border-radius:4px 4px 0 0}.nav-tabs-justified>.active>a,.nav-tabs-justified>.active>a:focus,.nav-tabs-justified>.active>a:hover{border-bottom-color:#fff}}.tab-content>.tab-pane{display:none}.tab-content>.active{display:block}.nav-tabs .dropdown-menu{margin-top:-1px;border-top-left-radius:0;border-top-right-radius:0}.navbar{position:relative;min-height:50px;margin-bottom:20px;border:1px solid transparent}@media (min-width:768px){.navbar{border-radius:4px}}@media (min-width:768px){.navbar-header{float:left}}.navbar-collapse{padding-right:15px;padding-left:15px;overflow-x:visible;-webkit-overflow-scrolling:touch;border-top:1px solid transparent;-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,.1);box-shadow:inset 0 1px 0 rgba(255,255,255,.1)}.navbar-collapse.in{overflow-y:auto}@media (min-width:768px){.navbar-collapse{width:auto;border-top:0;-webkit-box-shadow:none;box-shadow:none}.navbar-collapse.collapse{display:block!important;height:auto!important;padding-bottom:0;overflow:visible!important}.navbar-collapse.in{overflow-y:visible}.navbar-fixed-bottom .navbar-collapse,.navbar-fixed-top .navbar-collapse,.navbar-static-top .navbar-collapse{padding-right:0;padding-left:0}}.navbar-fixed-bottom .navbar-collapse,.navbar-fixed-top .navbar-collapse{max-height:340px}@media (max-device-width:480px) and (orientation:landscape){.navbar-fixed-bottom .navbar-collapse,.navbar-fixed-top .navbar-collapse{max-height:200px}}.container-fluid>.navbar-collapse,.container-fluid>.navbar-header,.container>.navbar-collapse,.container>.navbar-header{margin-right:-15px;margin-left:-15px}@media (min-width:768px){.container-fluid>.navbar-collapse,.container-fluid>.navbar-header,.container>.navbar-collapse,.container>.navbar-header{margin-right:0;margin-left:0}}.navbar-static-top{z-index:1000;border-width:0 0 1px}@media (min-width:768px){.navbar-static-top{border-radius:0}}.navbar-fixed-bottom,.navbar-fixed-top{position:fixed;right:0;left:0;z-index:1030}@media (min-width:768px){.navbar-fixed-bottom,.navbar-fixed-top{border-radius:0}}.navbar-fixed-top{top:0;border-width:0 0 1px}.navbar-fixed-bottom{bottom:0;margin-bottom:0;border-width:1px 0 0}.navbar-brand{float:left;height:50px;padding:15px 15px;font-size:18px;line-height:20px}.navbar-brand:focus,.navbar-brand:hover{text-decoration:none}.navbar-brand>img{display:block}@media (min-width:768px){.navbar>.container .navbar-brand,.navbar>.container-fluid .navbar-brand{margin-left:-15px}}.navbar-toggle{position:relative;float:right;padding:9px 10px;margin-top:8px;margin-right:15px;margin-bottom:8px;background-color:transparent;background-image:none;border:1px solid transparent;border-radius:4px}.navbar-toggle:focus{outline:0}.navbar-toggle .icon-bar{display:block;width:22px;height:2px;border-radius:1px}.navbar-toggle .icon-bar+.icon-bar{margin-top:4px}@media (min-width:768px){.navbar-toggle{display:none}}.navbar-nav{margin:7.5px -15px}.navbar-nav>li>a{padding-top:10px;padding-bottom:10px;line-height:20px}@media (max-width:767px){.navbar-nav .open .dropdown-menu{position:static;float:none;width:auto;margin-top:0;background-color:transparent;border:0;-webkit-box-shadow:none;box-shadow:none}.navbar-nav .open .dropdown-menu .dropdown-header,.navbar-nav .open .dropdown-menu>li>a{padding:5px 15px 5px 25px}.navbar-nav .open .dropdown-menu>li>a{line-height:20px}.navbar-nav .open .dropdown-menu>li>a:focus,.navbar-nav .open .dropdown-menu>li>a:hover{background-image:none}}@media (min-width:768px){.navbar-nav{float:left;margin:0}.navbar-nav>li{float:left}.navbar-nav>li>a{padding-top:15px;padding-bottom:15px}}.navbar-form{padding:10px 15px;margin-top:8px;margin-right:-15px;margin-bottom:8px;margin-left:-15px;border-top:1px solid transparent;border-bottom:1px solid transparent;-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,.1),0 1px 0 rgba(255,255,255,.1);box-shadow:inset 0 1px 0 rgba(255,255,255,.1),0 1px 0 rgba(255,255,255,.1)}@media (min-width:768px){.navbar-form .form-group{display:inline-block;margin-bottom:0;vertical-align:middle}.navbar-form .form-control{display:inline-block;width:auto;vertical-align:middle}.navbar-form .form-control-static{display:inline-block}.navbar-form .input-group{display:inline-table;vertical-align:middle}.navbar-form .input-group .form-control,.navbar-form .input-group .input-group-addon,.navbar-form .input-group .input-group-btn{width:auto}.navbar-form .input-group>.form-control{width:100%}.navbar-form .control-label{margin-bottom:0;vertical-align:middle}.navbar-form .checkbox,.navbar-form .radio{display:inline-block;margin-top:0;margin-bottom:0;vertical-align:middle}.navbar-form .checkbox label,.navbar-form .radio label{padding-left:0}.navbar-form .checkbox input[type=checkbox],.navbar-form .radio input[type=radio]{position:relative;margin-left:0}.navbar-form .has-feedback .form-control-feedback{top:0}}@media (max-width:767px){.navbar-form .form-group{margin-bottom:5px}.navbar-form .form-group:last-child{margin-bottom:0}}@media (min-width:768px){.navbar-form{width:auto;padding-top:0;padding-bottom:0;margin-right:0;margin-left:0;border:0;-webkit-box-shadow:none;box-shadow:none}}.navbar-nav>li>.dropdown-menu{margin-top:0;border-top-left-radius:0;border-top-right-radius:0}.navbar-fixed-bottom .navbar-nav>li>.dropdown-menu{margin-bottom:0;border-top-left-radius:4px;border-top-right-radius:4px;border-bottom-right-radius:0;border-bottom-left-radius:0}.navbar-btn{margin-top:8px;margin-bottom:8px}.navbar-btn.btn-sm{margin-top:10px;margin-bottom:10px}.navbar-btn.btn-xs{margin-top:14px;margin-bottom:14px}.navbar-text{margin-top:15px;margin-bottom:15px}@media (min-width:768px){.navbar-text{float:left;margin-right:15px;margin-left:15px}}@media (min-width:768px){.navbar-left{float:left!important}.navbar-right{float:right!important;margin-right:-15px}.navbar-right~.navbar-right{margin-right:0}}.navbar-default{background-color:#f8f8f8;border-color:#e7e7e7}.navbar-default .navbar-brand{color:#777}.navbar-default .navbar-brand:focus,.navbar-default .navbar-brand:hover{color:#5e5e5e;background-color:transparent}.navbar-default .navbar-text{color:#777}.navbar-default .navbar-nav>li>a{color:#777}.navbar-default .navbar-nav>li>a:focus,.navbar-default .navbar-nav>li>a:hover{color:#333;background-color:transparent}.navbar-default .navbar-nav>.active>a,.navbar-default .navbar-nav>.active>a:focus,.navbar-default .navbar-nav>.active>a:hover{color:#555;background-color:#e7e7e7}.navbar-default .navbar-nav>.disabled>a,.navbar-default .navbar-nav>.disabled>a:focus,.navbar-default .navbar-nav>.disabled>a:hover{color:#ccc;background-color:transparent}.navbar-default .navbar-toggle{border-color:#ddd}.navbar-default .navbar-toggle:focus,.navbar-default .navbar-toggle:hover{background-color:#ddd}.navbar-default .navbar-toggle .icon-bar{background-color:#888}.navbar-default .navbar-collapse,.navbar-default .navbar-form{border-color:#e7e7e7}.navbar-default .navbar-nav>.open>a,.navbar-default .navbar-nav>.open>a:focus,.navbar-default .navbar-nav>.open>a:hover{color:#555;background-color:#e7e7e7}@media (max-width:767px){.navbar-default .navbar-nav .open .dropdown-menu>li>a{color:#777}.navbar-default .navbar-nav .open .dropdown-menu>li>a:focus,.navbar-default .navbar-nav .open .dropdown-menu>li>a:hover{color:#333;background-color:transparent}.navbar-default .navbar-nav .open .dropdown-menu>.active>a,.navbar-default .navbar-nav .open .dropdown-menu>.active>a:focus,.navbar-default .navbar-nav .open .dropdown-menu>.active>a:hover{color:#555;background-color:#e7e7e7}.navbar-default .navbar-nav .open .dropdown-menu>.disabled>a,.navbar-default .navbar-nav .open .dropdown-menu>.disabled>a:focus,.navbar-default .navbar-nav .open .dropdown-menu>.disabled>a:hover{color:#ccc;background-color:transparent}}.navbar-default .navbar-link{color:#777}.navbar-default .navbar-link:hover{color:#333}.navbar-default .btn-link{color:#777}.navbar-default .btn-link:focus,.navbar-default .btn-link:hover{color:#333}.navbar-default .btn-link[disabled]:focus,.navbar-default .btn-link[disabled]:hover,fieldset[disabled] .navbar-default .btn-link:focus,fieldset[disabled] .navbar-default .btn-link:hover{color:#ccc}.navbar-inverse{background-color:#222;border-color:#080808}.navbar-inverse .navbar-brand{color:#9d9d9d}.navbar-inverse .navbar-brand:focus,.navbar-inverse .navbar-brand:hover{color:#fff;background-color:transparent}.navbar-inverse .navbar-text{color:#9d9d9d}.navbar-inverse .navbar-nav>li>a{color:#9d9d9d}.navbar-inverse .navbar-nav>li>a:focus,.navbar-inverse .navbar-nav>li>a:hover{color:#fff;background-color:transparent}.navbar-inverse .navbar-nav>.active>a,.navbar-inverse .navbar-nav>.active>a:focus,.navbar-inverse .navbar-nav>.active>a:hover{color:#fff;background-color:#080808}.navbar-inverse .navbar-nav>.disabled>a,.navbar-inverse .navbar-nav>.disabled>a:focus,.navbar-inverse .navbar-nav>.disabled>a:hover{color:#444;background-color:transparent}.navbar-inverse .navbar-toggle{border-color:#333}.navbar-inverse .navbar-toggle:focus,.navbar-inverse .navbar-toggle:hover{background-color:#333}.navbar-inverse .navbar-toggle .icon-bar{background-color:#fff}.navbar-inverse .navbar-collapse,.navbar-inverse .navbar-form{border-color:#101010}.navbar-inverse .navbar-nav>.open>a,.navbar-inverse .navbar-nav>.open>a:focus,.navbar-inverse .navbar-nav>.open>a:hover{color:#fff;background-color:#080808}@media (max-width:767px){.navbar-inverse .navbar-nav .open .dropdown-menu>.dropdown-header{border-color:#080808}.navbar-inverse .navbar-nav .open .dropdown-menu .divider{background-color:#080808}.navbar-inverse .navbar-nav .open .dropdown-menu>li>a{color:#9d9d9d}.navbar-inverse .navbar-nav .open .dropdown-menu>li>a:focus,.navbar-inverse .navbar-nav .open .dropdown-menu>li>a:hover{color:#fff;background-color:transparent}.navbar-inverse .navbar-nav .open .dropdown-menu>.active>a,.navbar-inverse .navbar-nav .open .dropdown-menu>.active>a:focus,.navbar-inverse .navbar-nav .open .dropdown-menu>.active>a:hover{color:#fff;background-color:#080808}.navbar-inverse .navbar-nav .open .dropdown-menu>.disabled>a,.navbar-inverse .navbar-nav .open .dropdown-menu>.disabled>a:focus,.navbar-inverse .navbar-nav .open .dropdown-menu>.disabled>a:hover{color:#444;background-color:transparent}}.navbar-inverse .navbar-link{color:#9d9d9d}.navbar-inverse .navbar-link:hover{color:#fff}.navbar-inverse .btn-link{color:#9d9d9d}.navbar-inverse .btn-link:focus,.navbar-inverse .btn-link:hover{color:#fff}.navbar-inverse .btn-link[disabled]:focus,.navbar-inverse .btn-link[disabled]:hover,fieldset[disabled] .navbar-inverse .btn-link:focus,fieldset[disabled] .navbar-inverse .btn-link:hover{color:#444}.breadcrumb{padding:8px 15px;margin-bottom:20px;list-style:none;background-color:#f5f5f5;border-radius:4px}.breadcrumb>li{display:inline-block}.breadcrumb>li+li:before{padding:0 5px;color:#ccc;content:"/\00a0"}.breadcrumb>.active{color:#777}.pagination{display:inline-block;padding-left:0;margin:20px 0;border-radius:4px}.pagination>li{display:inline}.pagination>li>a,.pagination>li>span{position:relative;float:left;padding:6px 12px;margin-left:-1px;line-height:1.42857143;color:#337ab7;text-decoration:none;background-color:#fff;border:1px solid #ddd}.pagination>li:first-child>a,.pagination>li:first-child>span{margin-left:0;border-top-left-radius:4px;border-bottom-left-radius:4px}.pagination>li:last-child>a,.pagination>li:last-child>span{border-top-right-radius:4px;border-bottom-right-radius:4px}.pagination>li>a:focus,.pagination>li>a:hover,.pagination>li>span:focus,.pagination>li>span:hover{z-index:2;color:#23527c;background-color:#eee;border-color:#ddd}.pagination>.active>a,.pagination>.active>a:focus,.pagination>.active>a:hover,.pagination>.active>span,.pagination>.active>span:focus,.pagination>.active>span:hover{z-index:3;color:#fff;cursor:default;background-color:#337ab7;border-color:#337ab7}.pagination>.disabled>a,.pagination>.disabled>a:focus,.pagination>.disabled>a:hover,.pagination>.disabled>span,.pagination>.disabled>span:focus,.pagination>.disabled>span:hover{color:#777;cursor:not-allowed;background-color:#fff;border-color:#ddd}.pagination-lg>li>a,.pagination-lg>li>span{padding:10px 16px;font-size:18px;line-height:1.3333333}.pagination-lg>li:first-child>a,.pagination-lg>li:first-child>span{border-top-left-radius:6px;border-bottom-left-radius:6px}.pagination-lg>li:last-child>a,.pagination-lg>li:last-child>span{border-top-right-radius:6px;border-bottom-right-radius:6px}.pagination-sm>li>a,.pagination-sm>li>span{padding:5px 10px;font-size:12px;line-height:1.5}.pagination-sm>li:first-child>a,.pagination-sm>li:first-child>span{border-top-left-radius:3px;border-bottom-left-radius:3px}.pagination-sm>li:last-child>a,.pagination-sm>li:last-child>span{border-top-right-radius:3px;border-bottom-right-radius:3px}.pager{padding-left:0;margin:20px 0;text-align:center;list-style:none}.pager li{display:inline}.pager li>a,.pager li>span{display:inline-block;padding:5px 14px;background-color:#fff;border:1px solid #ddd;border-radius:15px}.pager li>a:focus,.pager li>a:hover{text-decoration:none;background-color:#eee}.pager .next>a,.pager .next>span{float:right}.pager .previous>a,.pager .previous>span{float:left}.pager .disabled>a,.pager .disabled>a:focus,.pager .disabled>a:hover,.pager .disabled>span{color:#777;cursor:not-allowed;background-color:#fff}.label{display:inline;padding:.2em .6em .3em;font-size:75%;font-weight:700;line-height:1;color:#fff;text-align:center;white-space:nowrap;vertical-align:baseline;border-radius:.25em}a.label:focus,a.label:hover{color:#fff;text-decoration:none;cursor:pointer}.label:empty{display:none}.btn .label{position:relative;top:-1px}.label-default{background-color:#777}.label-default[href]:focus,.label-default[href]:hover{background-color:#5e5e5e}.label-primary{background-color:#337ab7}.label-primary[href]:focus,.label-primary[href]:hover{background-color:#286090}.label-success{background-color:#5cb85c}.label-success[href]:focus,.label-success[href]:hover{background-color:#449d44}.label-info{background-color:#5bc0de}.label-info[href]:focus,.label-info[href]:hover{background-color:#31b0d5}.label-warning{background-color:#f0ad4e}.label-warning[href]:focus,.label-warning[href]:hover{background-color:#ec971f}.label-danger{background-color:#d9534f}.label-danger[href]:focus,.label-danger[href]:hover{background-color:#c9302c}.badge{display:inline-block;min-width:10px;padding:3px 7px;font-size:12px;font-weight:700;line-height:1;color:#fff;text-align:center;white-space:nowrap;vertical-align:middle;background-color:#777;border-radius:10px}.badge:empty{display:none}.btn .badge{position:relative;top:-1px}.btn-group-xs>.btn .badge,.btn-xs .badge{top:0;padding:1px 5px}a.badge:focus,a.badge:hover{color:#fff;text-decoration:none;cursor:pointer}.list-group-item.active>.badge,.nav-pills>.active>a>.badge{color:#337ab7;background-color:#fff}.list-group-item>.badge{float:right}.list-group-item>.badge+.badge{margin-right:5px}.nav-pills>li>a>.badge{margin-left:3px}.jumbotron{padding-top:30px;padding-bottom:30px;margin-bottom:30px;color:inherit;background-color:#eee}.jumbotron .h1,.jumbotron h1{color:inherit}.jumbotron p{margin-bottom:15px;font-size:21px;font-weight:200}.jumbotron>hr{border-top-color:#d5d5d5}.container .jumbotron,.container-fluid .jumbotron{padding-right:15px;padding-left:15px;border-radius:6px}.jumbotron .container{max-width:100%}@media screen and (min-width:768px){.jumbotron{padding-top:48px;padding-bottom:48px}.container .jumbotron,.container-fluid .jumbotron{padding-right:60px;padding-left:60px}.jumbotron .h1,.jumbotron h1{font-size:63px}}.thumbnail{display:block;padding:4px;margin-bottom:20px;line-height:1.42857143;background-color:#fff;border:1px solid #ddd;border-radius:4px;-webkit-transition:border .2s ease-in-out;-o-transition:border .2s ease-in-out;transition:border .2s ease-in-out}.thumbnail a>img,.thumbnail>img{margin-right:auto;margin-left:auto}a.thumbnail.active,a.thumbnail:focus,a.thumbnail:hover{border-color:#337ab7}.thumbnail .caption{padding:9px;color:#333}.alert{padding:15px;margin-bottom:20px;border:1px solid transparent;border-radius:4px}.alert h4{margin-top:0;color:inherit}.alert .alert-link{font-weight:700}.alert>p,.alert>ul{margin-bottom:0}.alert>p+p{margin-top:5px}.alert-dismissable,.alert-dismissible{padding-right:35px}.alert-dismissable .close,.alert-dismissible .close{position:relative;top:-2px;right:-21px;color:inherit}.alert-success{color:#3c763d;background-color:#dff0d8;border-color:#d6e9c6}.alert-success hr{border-top-color:#c9e2b3}.alert-success .alert-link{color:#2b542c}.alert-info{color:#31708f;background-color:#d9edf7;border-color:#bce8f1}.alert-info hr{border-top-color:#a6e1ec}.alert-info .alert-link{color:#245269}.alert-warning{color:#8a6d3b;background-color:#fcf8e3;border-color:#faebcc}.alert-warning hr{border-top-color:#f7e1b5}.alert-warning .alert-link{color:#66512c}.alert-danger{color:#a94442;background-color:#f2dede;border-color:#ebccd1}.alert-danger hr{border-top-color:#e4b9c0}.alert-danger .alert-link{color:#843534}@-webkit-keyframes progress-bar-stripes{from{background-position:40px 0}to{background-position:0 0}}@-o-keyframes progress-bar-stripes{from{background-position:40px 0}to{background-position:0 0}}@keyframes progress-bar-stripes{from{background-position:40px 0}to{background-position:0 0}}.progress{height:20px;margin-bottom:20px;overflow:hidden;background-color:#f5f5f5;border-radius:4px;-webkit-box-shadow:inset 0 1px 2px rgba(0,0,0,.1);box-shadow:inset 0 1px 2px rgba(0,0,0,.1)}.progress-bar{float:left;width:0;height:100%;font-size:12px;line-height:20px;color:#fff;text-align:center;background-color:#337ab7;-webkit-box-shadow:inset 0 -1px 0 rgba(0,0,0,.15);box-shadow:inset 0 -1px 0 rgba(0,0,0,.15);-webkit-transition:width .6s ease;-o-transition:width .6s ease;transition:width .6s ease}.progress-bar-striped,.progress-striped .progress-bar{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);-webkit-background-size:40px 40px;background-size:40px 40px}.progress-bar.active,.progress.active .progress-bar{-webkit-animation:progress-bar-stripes 2s linear infinite;-o-animation:progress-bar-stripes 2s linear infinite;animation:progress-bar-stripes 2s linear infinite}.progress-bar-success{background-color:#5cb85c}.progress-striped .progress-bar-success{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent)}.progress-bar-info{background-color:#5bc0de}.progress-striped .progress-bar-info{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent)}.progress-bar-warning{background-color:#f0ad4e}.progress-striped .progress-bar-warning{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent)}.progress-bar-danger{background-color:#d9534f}.progress-striped .progress-bar-danger{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent)}.media{margin-top:15px}.media:first-child{margin-top:0}.media,.media-body{overflow:hidden;zoom:1}.media-body{width:10000px}.media-object{display:block}.media-object.img-thumbnail{max-width:none}.media-right,.media>.pull-right{padding-left:10px}.media-left,.media>.pull-left{padding-right:10px}.media-body,.media-left,.media-right{display:table-cell;vertical-align:top}.media-middle{vertical-align:middle}.media-bottom{vertical-align:bottom}.media-heading{margin-top:0;margin-bottom:5px}.media-list{padding-left:0;list-style:none}.list-group{padding-left:0;margin-bottom:20px}.list-group-item{position:relative;display:block;padding:10px 15px;margin-bottom:-1px;background-color:#fff;border:1px solid #ddd}.list-group-item:first-child{border-top-left-radius:4px;border-top-right-radius:4px}.list-group-item:last-child{margin-bottom:0;border-bottom-right-radius:4px;border-bottom-left-radius:4px}a.list-group-item,button.list-group-item{color:#555}a.list-group-item .list-group-item-heading,button.list-group-item .list-group-item-heading{color:#333}a.list-group-item:focus,a.list-group-item:hover,button.list-group-item:focus,button.list-group-item:hover{color:#555;text-decoration:none;background-color:#f5f5f5}button.list-group-item{width:100%;text-align:left}.list-group-item.disabled,.list-group-item.disabled:focus,.list-group-item.disabled:hover{color:#777;cursor:not-allowed;background-color:#eee}.list-group-item.disabled .list-group-item-heading,.list-group-item.disabled:focus .list-group-item-heading,.list-group-item.disabled:hover .list-group-item-heading{color:inherit}.list-group-item.disabled .list-group-item-text,.list-group-item.disabled:focus .list-group-item-text,.list-group-item.disabled:hover .list-group-item-text{color:#777}.list-group-item.active,.list-group-item.active:focus,.list-group-item.active:hover{z-index:2;color:#fff;background-color:#337ab7;border-color:#337ab7}.list-group-item.active .list-group-item-heading,.list-group-item.active .list-group-item-heading>.small,.list-group-item.active .list-group-item-heading>small,.list-group-item.active:focus .list-group-item-heading,.list-group-item.active:focus .list-group-item-heading>.small,.list-group-item.active:focus .list-group-item-heading>small,.list-group-item.active:hover .list-group-item-heading,.list-group-item.active:hover .list-group-item-heading>.small,.list-group-item.active:hover .list-group-item-heading>small{color:inherit}.list-group-item.active .list-group-item-text,.list-group-item.active:focus .list-group-item-text,.list-group-item.active:hover .list-group-item-text{color:#c7ddef}.list-group-item-success{color:#3c763d;background-color:#dff0d8}a.list-group-item-success,button.list-group-item-success{color:#3c763d}a.list-group-item-success .list-group-item-heading,button.list-group-item-success .list-group-item-heading{color:inherit}a.list-group-item-success:focus,a.list-group-item-success:hover,button.list-group-item-success:focus,button.list-group-item-success:hover{color:#3c763d;background-color:#d0e9c6}a.list-group-item-success.active,a.list-group-item-success.active:focus,a.list-group-item-success.active:hover,button.list-group-item-success.active,button.list-group-item-success.active:focus,button.list-group-item-success.active:hover{color:#fff;background-color:#3c763d;border-color:#3c763d}.list-group-item-info{color:#31708f;background-color:#d9edf7}a.list-group-item-info,button.list-group-item-info{color:#31708f}a.list-group-item-info .list-group-item-heading,button.list-group-item-info .list-group-item-heading{color:inherit}a.list-group-item-info:focus,a.list-group-item-info:hover,button.list-group-item-info:focus,button.list-group-item-info:hover{color:#31708f;background-color:#c4e3f3}a.list-group-item-info.active,a.list-group-item-info.active:focus,a.list-group-item-info.active:hover,button.list-group-item-info.active,button.list-group-item-info.active:focus,button.list-group-item-info.active:hover{color:#fff;background-color:#31708f;border-color:#31708f}.list-group-item-warning{color:#8a6d3b;background-color:#fcf8e3}a.list-group-item-warning,button.list-group-item-warning{color:#8a6d3b}a.list-group-item-warning .list-group-item-heading,button.list-group-item-warning .list-group-item-heading{color:inherit}a.list-group-item-warning:focus,a.list-group-item-warning:hover,button.list-group-item-warning:focus,button.list-group-item-warning:hover{color:#8a6d3b;background-color:#faf2cc}a.list-group-item-warning.active,a.list-group-item-warning.active:focus,a.list-group-item-warning.active:hover,button.list-group-item-warning.active,button.list-group-item-warning.active:focus,button.list-group-item-warning.active:hover{color:#fff;background-color:#8a6d3b;border-color:#8a6d3b}.list-group-item-danger{color:#a94442;background-color:#f2dede}a.list-group-item-danger,button.list-group-item-danger{color:#a94442}a.list-group-item-danger .list-group-item-heading,button.list-group-item-danger .list-group-item-heading{color:inherit}a.list-group-item-danger:focus,a.list-group-item-danger:hover,button.list-group-item-danger:focus,button.list-group-item-danger:hover{color:#a94442;background-color:#ebcccc}a.list-group-item-danger.active,a.list-group-item-danger.active:focus,a.list-group-item-danger.active:hover,button.list-group-item-danger.active,button.list-group-item-danger.active:focus,button.list-group-item-danger.active:hover{color:#fff;background-color:#a94442;border-color:#a94442}.list-group-item-heading{margin-top:0;margin-bottom:5px}.list-group-item-text{margin-bottom:0;line-height:1.3}.panel{margin-bottom:20px;background-color:#fff;border:1px solid transparent;border-radius:4px;-webkit-box-shadow:0 1px 1px rgba(0,0,0,.05);box-shadow:0 1px 1px rgba(0,0,0,.05)}.panel-body{padding:15px}.panel-heading{padding:10px 15px;border-bottom:1px solid transparent;border-top-left-radius:3px;border-top-right-radius:3px}.panel-heading>.dropdown .dropdown-toggle{color:inherit}.panel-title{margin-top:0;margin-bottom:0;font-size:16px;color:inherit}.panel-title>.small,.panel-title>.small>a,.panel-title>a,.panel-title>small,.panel-title>small>a{color:inherit}.panel-footer{padding:10px 15px;background-color:#f5f5f5;border-top:1px solid #ddd;border-bottom-right-radius:3px;border-bottom-left-radius:3px}.panel>.list-group,.panel>.panel-collapse>.list-group{margin-bottom:0}.panel>.list-group .list-group-item,.panel>.panel-collapse>.list-group .list-group-item{border-width:1px 0;border-radius:0}.panel>.list-group:first-child .list-group-item:first-child,.panel>.panel-collapse>.list-group:first-child .list-group-item:first-child{border-top:0;border-top-left-radius:3px;border-top-right-radius:3px}.panel>.list-group:last-child .list-group-item:last-child,.panel>.panel-collapse>.list-group:last-child .list-group-item:last-child{border-bottom:0;border-bottom-right-radius:3px;border-bottom-left-radius:3px}.panel>.panel-heading+.panel-collapse>.list-group .list-group-item:first-child{border-top-left-radius:0;border-top-right-radius:0}.panel-heading+.list-group .list-group-item:first-child{border-top-width:0}.list-group+.panel-footer{border-top-width:0}.panel>.panel-collapse>.table,.panel>.table,.panel>.table-responsive>.table{margin-bottom:0}.panel>.panel-collapse>.table caption,.panel>.table caption,.panel>.table-responsive>.table caption{padding-right:15px;padding-left:15px}.panel>.table-responsive:first-child>.table:first-child,.panel>.table:first-child{border-top-left-radius:3px;border-top-right-radius:3px}.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child,.panel>.table:first-child>tbody:first-child>tr:first-child,.panel>.table:first-child>thead:first-child>tr:first-child{border-top-left-radius:3px;border-top-right-radius:3px}.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child td:first-child,.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child th:first-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child td:first-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child th:first-child,.panel>.table:first-child>tbody:first-child>tr:first-child td:first-child,.panel>.table:first-child>tbody:first-child>tr:first-child th:first-child,.panel>.table:first-child>thead:first-child>tr:first-child td:first-child,.panel>.table:first-child>thead:first-child>tr:first-child th:first-child{border-top-left-radius:3px}.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child td:last-child,.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child th:last-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child td:last-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child th:last-child,.panel>.table:first-child>tbody:first-child>tr:first-child td:last-child,.panel>.table:first-child>tbody:first-child>tr:first-child th:last-child,.panel>.table:first-child>thead:first-child>tr:first-child td:last-child,.panel>.table:first-child>thead:first-child>tr:first-child th:last-child{border-top-right-radius:3px}.panel>.table-responsive:last-child>.table:last-child,.panel>.table:last-child{border-bottom-right-radius:3px;border-bottom-left-radius:3px}.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child,.panel>.table:last-child>tbody:last-child>tr:last-child,.panel>.table:last-child>tfoot:last-child>tr:last-child{border-bottom-right-radius:3px;border-bottom-left-radius:3px}.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child td:first-child,.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child th:first-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child td:first-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child th:first-child,.panel>.table:last-child>tbody:last-child>tr:last-child td:first-child,.panel>.table:last-child>tbody:last-child>tr:last-child th:first-child,.panel>.table:last-child>tfoot:last-child>tr:last-child td:first-child,.panel>.table:last-child>tfoot:last-child>tr:last-child th:first-child{border-bottom-left-radius:3px}.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child td:last-child,.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child th:last-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child td:last-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child th:last-child,.panel>.table:last-child>tbody:last-child>tr:last-child td:last-child,.panel>.table:last-child>tbody:last-child>tr:last-child th:last-child,.panel>.table:last-child>tfoot:last-child>tr:last-child td:last-child,.panel>.table:last-child>tfoot:last-child>tr:last-child th:last-child{border-bottom-right-radius:3px}.panel>.panel-body+.table,.panel>.panel-body+.table-responsive,.panel>.table+.panel-body,.panel>.table-responsive+.panel-body{border-top:1px solid #ddd}.panel>.table>tbody:first-child>tr:first-child td,.panel>.table>tbody:first-child>tr:first-child th{border-top:0}.panel>.table-bordered,.panel>.table-responsive>.table-bordered{border:0}.panel>.table-bordered>tbody>tr>td:first-child,.panel>.table-bordered>tbody>tr>th:first-child,.panel>.table-bordered>tfoot>tr>td:first-child,.panel>.table-bordered>tfoot>tr>th:first-child,.panel>.table-bordered>thead>tr>td:first-child,.panel>.table-bordered>thead>tr>th:first-child,.panel>.table-responsive>.table-bordered>tbody>tr>td:first-child,.panel>.table-responsive>.table-bordered>tbody>tr>th:first-child,.panel>.table-responsive>.table-bordered>tfoot>tr>td:first-child,.panel>.table-responsive>.table-bordered>tfoot>tr>th:first-child,.panel>.table-responsive>.table-bordered>thead>tr>td:first-child,.panel>.table-responsive>.table-bordered>thead>tr>th:first-child{border-left:0}.panel>.table-bordered>tbody>tr>td:last-child,.panel>.table-bordered>tbody>tr>th:last-child,.panel>.table-bordered>tfoot>tr>td:last-child,.panel>.table-bordered>tfoot>tr>th:last-child,.panel>.table-bordered>thead>tr>td:last-child,.panel>.table-bordered>thead>tr>th:last-child,.panel>.table-responsive>.table-bordered>tbody>tr>td:last-child,.panel>.table-responsive>.table-bordered>tbody>tr>th:last-child,.panel>.table-responsive>.table-bordered>tfoot>tr>td:last-child,.panel>.table-responsive>.table-bordered>tfoot>tr>th:last-child,.panel>.table-responsive>.table-bordered>thead>tr>td:last-child,.panel>.table-responsive>.table-bordered>thead>tr>th:last-child{border-right:0}.panel>.table-bordered>tbody>tr:first-child>td,.panel>.table-bordered>tbody>tr:first-child>th,.panel>.table-bordered>thead>tr:first-child>td,.panel>.table-bordered>thead>tr:first-child>th,.panel>.table-responsive>.table-bordered>tbody>tr:first-child>td,.panel>.table-responsive>.table-bordered>tbody>tr:first-child>th,.panel>.table-responsive>.table-bordered>thead>tr:first-child>td,.panel>.table-responsive>.table-bordered>thead>tr:first-child>th{border-bottom:0}.panel>.table-bordered>tbody>tr:last-child>td,.panel>.table-bordered>tbody>tr:last-child>th,.panel>.table-bordered>tfoot>tr:last-child>td,.panel>.table-bordered>tfoot>tr:last-child>th,.panel>.table-responsive>.table-bordered>tbody>tr:last-child>td,.panel>.table-responsive>.table-bordered>tbody>tr:last-child>th,.panel>.table-responsive>.table-bordered>tfoot>tr:last-child>td,.panel>.table-responsive>.table-bordered>tfoot>tr:last-child>th{border-bottom:0}.panel>.table-responsive{margin-bottom:0;border:0}.panel-group{margin-bottom:20px}.panel-group .panel{margin-bottom:0;border-radius:4px}.panel-group .panel+.panel{margin-top:5px}.panel-group .panel-heading{border-bottom:0}.panel-group .panel-heading+.panel-collapse>.list-group,.panel-group .panel-heading+.panel-collapse>.panel-body{border-top:1px solid #ddd}.panel-group .panel-footer{border-top:0}.panel-group .panel-footer+.panel-collapse .panel-body{border-bottom:1px solid #ddd}.panel-default{border-color:#ddd}.panel-default>.panel-heading{color:#333;background-color:#f5f5f5;border-color:#ddd}.panel-default>.panel-heading+.panel-collapse>.panel-body{border-top-color:#ddd}.panel-default>.panel-heading .badge{color:#f5f5f5;background-color:#333}.panel-default>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#ddd}.panel-primary{border-color:#337ab7}.panel-primary>.panel-heading{color:#fff;background-color:#337ab7;border-color:#337ab7}.panel-primary>.panel-heading+.panel-collapse>.panel-body{border-top-color:#337ab7}.panel-primary>.panel-heading .badge{color:#337ab7;background-color:#fff}.panel-primary>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#337ab7}.panel-success{border-color:#d6e9c6}.panel-success>.panel-heading{color:#3c763d;background-color:#dff0d8;border-color:#d6e9c6}.panel-success>.panel-heading+.panel-collapse>.panel-body{border-top-color:#d6e9c6}.panel-success>.panel-heading .badge{color:#dff0d8;background-color:#3c763d}.panel-success>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#d6e9c6}.panel-info{border-color:#bce8f1}.panel-info>.panel-heading{color:#31708f;background-color:#d9edf7;border-color:#bce8f1}.panel-info>.panel-heading+.panel-collapse>.panel-body{border-top-color:#bce8f1}.panel-info>.panel-heading .badge{color:#d9edf7;background-color:#31708f}.panel-info>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#bce8f1}.panel-warning{border-color:#faebcc}.panel-warning>.panel-heading{color:#8a6d3b;background-color:#fcf8e3;border-color:#faebcc}.panel-warning>.panel-heading+.panel-collapse>.panel-body{border-top-color:#faebcc}.panel-warning>.panel-heading .badge{color:#fcf8e3;background-color:#8a6d3b}.panel-warning>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#faebcc}.panel-danger{border-color:#ebccd1}.panel-danger>.panel-heading{color:#a94442;background-color:#f2dede;border-color:#ebccd1}.panel-danger>.panel-heading+.panel-collapse>.panel-body{border-top-color:#ebccd1}.panel-danger>.panel-heading .badge{color:#f2dede;background-color:#a94442}.panel-danger>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#ebccd1}.embed-responsive{position:relative;display:block;height:0;padding:0;overflow:hidden}.embed-responsive .embed-responsive-item,.embed-responsive embed,.embed-responsive iframe,.embed-responsive object,.embed-responsive video{position:absolute;top:0;bottom:0;left:0;width:100%;height:100%;border:0}.embed-responsive-16by9{padding-bottom:56.25%}.embed-responsive-4by3{padding-bottom:75%}.well{min-height:20px;padding:19px;margin-bottom:20px;background-color:#f5f5f5;border:1px solid #e3e3e3;border-radius:4px;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.05);box-shadow:inset 0 1px 1px rgba(0,0,0,.05)}.well blockquote{border-color:#ddd;border-color:rgba(0,0,0,.15)}.well-lg{padding:24px;border-radius:6px}.well-sm{padding:9px;border-radius:3px}.close{float:right;font-size:21px;font-weight:700;line-height:1;color:#000;text-shadow:0 1px 0 #fff;filter:alpha(opacity=20);opacity:.2}.close:focus,.close:hover{color:#000;text-decoration:none;cursor:pointer;filter:alpha(opacity=50);opacity:.5}button.close{-webkit-appearance:none;padding:0;cursor:pointer;background:0 0;border:0}.modal-open{overflow:hidden}.modal{position:fixed;top:0;right:0;bottom:0;left:0;z-index:1050;display:none;overflow:hidden;-webkit-overflow-scrolling:touch;outline:0}.modal.fade .modal-dialog{-webkit-transition:-webkit-transform .3s ease-out;-o-transition:-o-transform .3s ease-out;transition:transform .3s ease-out;-webkit-transform:translate(0,-25%);-ms-transform:translate(0,-25%);-o-transform:translate(0,-25%);transform:translate(0,-25%)}.modal.in .modal-dialog{-webkit-transform:translate(0,0);-ms-transform:translate(0,0);-o-transform:translate(0,0);transform:translate(0,0)}.modal-open .modal{overflow-x:hidden;overflow-y:auto}.modal-dialog{position:relative;width:auto;margin:10px}.modal-content{position:relative;background-color:#fff;-webkit-background-clip:padding-box;background-clip:padding-box;border:1px solid #999;border:1px solid rgba(0,0,0,.2);border-radius:6px;outline:0;-webkit-box-shadow:0 3px 9px rgba(0,0,0,.5);box-shadow:0 3px 9px rgba(0,0,0,.5)}.modal-backdrop{position:fixed;top:0;right:0;bottom:0;left:0;z-index:1040;background-color:#000}.modal-backdrop.fade{filter:alpha(opacity=0);opacity:0}.modal-backdrop.in{filter:alpha(opacity=50);opacity:.5}.modal-header{padding:15px;border-bottom:1px solid #e5e5e5}.modal-header .close{margin-top:-2px}.modal-title{margin:0;line-height:1.42857143}.modal-body{position:relative;padding:15px}.modal-footer{padding:15px;text-align:right;border-top:1px solid #e5e5e5}.modal-footer .btn+.btn{margin-bottom:0;margin-left:5px}.modal-footer .btn-group .btn+.btn{margin-left:-1px}.modal-footer .btn-block+.btn-block{margin-left:0}.modal-scrollbar-measure{position:absolute;top:-9999px;width:50px;height:50px;overflow:scroll}@media (min-width:768px){.modal-dialog{width:600px;margin:30px auto}.modal-content{-webkit-box-shadow:0 5px 15px rgba(0,0,0,.5);box-shadow:0 5px 15px rgba(0,0,0,.5)}.modal-sm{width:300px}}@media (min-width:992px){.modal-lg{width:900px}}.tooltip{position:absolute;z-index:1070;display:block;font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;font-size:12px;font-style:normal;font-weight:400;line-height:1.42857143;text-align:left;text-align:start;text-decoration:none;text-shadow:none;text-transform:none;letter-spacing:normal;word-break:normal;word-spacing:normal;word-wrap:normal;white-space:normal;filter:alpha(opacity=0);opacity:0;line-break:auto}.tooltip.in{filter:alpha(opacity=90);opacity:.9}.tooltip.top{padding:5px 0;margin-top:-3px}.tooltip.right{padding:0 5px;margin-left:3px}.tooltip.bottom{padding:5px 0;margin-top:3px}.tooltip.left{padding:0 5px;margin-left:-3px}.tooltip-inner{max-width:200px;padding:3px 8px;color:#fff;text-align:center;background-color:#000;border-radius:4px}.tooltip-arrow{position:absolute;width:0;height:0;border-color:transparent;border-style:solid}.tooltip.top .tooltip-arrow{bottom:0;left:50%;margin-left:-5px;border-width:5px 5px 0;border-top-color:#000}.tooltip.top-left .tooltip-arrow{right:5px;bottom:0;margin-bottom:-5px;border-width:5px 5px 0;border-top-color:#000}.tooltip.top-right .tooltip-arrow{bottom:0;left:5px;margin-bottom:-5px;border-width:5px 5px 0;border-top-color:#000}.tooltip.right .tooltip-arrow{top:50%;left:0;margin-top:-5px;border-width:5px 5px 5px 0;border-right-color:#000}.tooltip.left .tooltip-arrow{top:50%;right:0;margin-top:-5px;border-width:5px 0 5px 5px;border-left-color:#000}.tooltip.bottom .tooltip-arrow{top:0;left:50%;margin-left:-5px;border-width:0 5px 5px;border-bottom-color:#000}.tooltip.bottom-left .tooltip-arrow{top:0;right:5px;margin-top:-5px;border-width:0 5px 5px;border-bottom-color:#000}.tooltip.bottom-right .tooltip-arrow{top:0;left:5px;margin-top:-5px;border-width:0 5px 5px;border-bottom-color:#000}.popover{position:absolute;top:0;left:0;z-index:1060;display:none;max-width:276px;padding:1px;font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;font-size:14px;font-style:normal;font-weight:400;line-height:1.42857143;text-align:left;text-align:start;text-decoration:none;text-shadow:none;text-transform:none;letter-spacing:normal;word-break:normal;word-spacing:normal;word-wrap:normal;white-space:normal;background-color:#fff;-webkit-background-clip:padding-box;background-clip:padding-box;border:1px solid #ccc;border:1px solid rgba(0,0,0,.2);border-radius:6px;-webkit-box-shadow:0 5px 10px rgba(0,0,0,.2);box-shadow:0 5px 10px rgba(0,0,0,.2);line-break:auto}.popover.top{margin-top:-10px}.popover.right{margin-left:10px}.popover.bottom{margin-top:10px}.popover.left{margin-left:-10px}.popover-title{padding:8px 14px;margin:0;font-size:14px;background-color:#f7f7f7;border-bottom:1px solid #ebebeb;border-radius:5px 5px 0 0}.popover-content{padding:9px 14px}.popover>.arrow,.popover>.arrow:after{position:absolute;display:block;width:0;height:0;border-color:transparent;border-style:solid}.popover>.arrow{border-width:11px}.popover>.arrow:after{content:"";border-width:10px}.popover.top>.arrow{bottom:-11px;left:50%;margin-left:-11px;border-top-color:#999;border-top-color:rgba(0,0,0,.25);border-bottom-width:0}.popover.top>.arrow:after{bottom:1px;margin-left:-10px;content:" ";border-top-color:#fff;border-bottom-width:0}.popover.right>.arrow{top:50%;left:-11px;margin-top:-11px;border-right-color:#999;border-right-color:rgba(0,0,0,.25);border-left-width:0}.popover.right>.arrow:after{bottom:-10px;left:1px;content:" ";border-right-color:#fff;border-left-width:0}.popover.bottom>.arrow{top:-11px;left:50%;margin-left:-11px;border-top-width:0;border-bottom-color:#999;border-bottom-color:rgba(0,0,0,.25)}.popover.bottom>.arrow:after{top:1px;margin-left:-10px;content:" ";border-top-width:0;border-bottom-color:#fff}.popover.left>.arrow{top:50%;right:-11px;margin-top:-11px;border-right-width:0;border-left-color:#999;border-left-color:rgba(0,0,0,.25)}.popover.left>.arrow:after{right:1px;bottom:-10px;content:" ";border-right-width:0;border-left-color:#fff}.carousel{position:relative}.carousel-inner{position:relative;width:100%;overflow:hidden}.carousel-inner>.item{position:relative;display:none;-webkit-transition:.6s ease-in-out left;-o-transition:.6s ease-in-out left;transition:.6s ease-in-out left}.carousel-inner>.item>a>img,.carousel-inner>.item>img{line-height:1}@media all and (transform-3d),(-webkit-transform-3d){.carousel-inner>.item{-webkit-transition:-webkit-transform .6s ease-in-out;-o-transition:-o-transform .6s ease-in-out;transition:transform .6s ease-in-out;-webkit-backface-visibility:hidden;backface-visibility:hidden;-webkit-perspective:1000px;perspective:1000px}.carousel-inner>.item.active.right,.carousel-inner>.item.next{left:0;-webkit-transform:translate3d(100%,0,0);transform:translate3d(100%,0,0)}.carousel-inner>.item.active.left,.carousel-inner>.item.prev{left:0;-webkit-transform:translate3d(-100%,0,0);transform:translate3d(-100%,0,0)}.carousel-inner>.item.active,.carousel-inner>.item.next.left,.carousel-inner>.item.prev.right{left:0;-webkit-transform:translate3d(0,0,0);transform:translate3d(0,0,0)}}.carousel-inner>.active,.carousel-inner>.next,.carousel-inner>.prev{display:block}.carousel-inner>.active{left:0}.carousel-inner>.next,.carousel-inner>.prev{position:absolute;top:0;width:100%}.carousel-inner>.next{left:100%}.carousel-inner>.prev{left:-100%}.carousel-inner>.next.left,.carousel-inner>.prev.right{left:0}.carousel-inner>.active.left{left:-100%}.carousel-inner>.active.right{left:100%}.carousel-control{position:absolute;top:0;bottom:0;left:0;width:15%;font-size:20px;color:#fff;text-align:center;text-shadow:0 1px 2px rgba(0,0,0,.6);background-color:rgba(0,0,0,0);filter:alpha(opacity=50);opacity:.5}.carousel-control.left{background-image:-webkit-linear-gradient(left,rgba(0,0,0,.5) 0,rgba(0,0,0,.0001) 100%);background-image:-o-linear-gradient(left,rgba(0,0,0,.5) 0,rgba(0,0,0,.0001) 100%);background-image:-webkit-gradient(linear,left top,right top,from(rgba(0,0,0,.5)),to(rgba(0,0,0,.0001)));background-image:linear-gradient(to right,rgba(0,0,0,.5) 0,rgba(0,0,0,.0001) 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#80000000', endColorstr='#00000000', GradientType=1);background-repeat:repeat-x}.carousel-control.right{right:0;left:auto;background-image:-webkit-linear-gradient(left,rgba(0,0,0,.0001) 0,rgba(0,0,0,.5) 100%);background-image:-o-linear-gradient(left,rgba(0,0,0,.0001) 0,rgba(0,0,0,.5) 100%);background-image:-webkit-gradient(linear,left top,right top,from(rgba(0,0,0,.0001)),to(rgba(0,0,0,.5)));background-image:linear-gradient(to right,rgba(0,0,0,.0001) 0,rgba(0,0,0,.5) 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#00000000', endColorstr='#80000000', GradientType=1);background-repeat:repeat-x}.carousel-control:focus,.carousel-control:hover{color:#fff;text-decoration:none;filter:alpha(opacity=90);outline:0;opacity:.9}.carousel-control .glyphicon-chevron-left,.carousel-control .glyphicon-chevron-right,.carousel-control .icon-next,.carousel-control .icon-prev{position:absolute;top:50%;z-index:5;display:inline-block;margin-top:-10px}.carousel-control .glyphicon-chevron-left,.carousel-control .icon-prev{left:50%;margin-left:-10px}.carousel-control .glyphicon-chevron-right,.carousel-control .icon-next{right:50%;margin-right:-10px}.carousel-control .icon-next,.carousel-control .icon-prev{width:20px;height:20px;font-family:serif;line-height:1}.carousel-control .icon-prev:before{content:'\2039'}.carousel-control .icon-next:before{content:'\203a'}.carousel-indicators{position:absolute;bottom:10px;left:50%;z-index:15;width:60%;padding-left:0;margin-left:-30%;text-align:center;list-style:none}.carousel-indicators li{display:inline-block;width:10px;height:10px;margin:1px;text-indent:-999px;cursor:pointer;background-color:#000\9;background-color:rgba(0,0,0,0);border:1px solid #fff;border-radius:10px}.carousel-indicators .active{width:12px;height:12px;margin:0;background-color:#fff}.carousel-caption{position:absolute;right:15%;bottom:20px;left:15%;z-index:10;padding-top:20px;padding-bottom:20px;color:#fff;text-align:center;text-shadow:0 1px 2px rgba(0,0,0,.6)}.carousel-caption .btn{text-shadow:none}@media screen and (min-width:768px){.carousel-control .glyphicon-chevron-left,.carousel-control .glyphicon-chevron-right,.carousel-control .icon-next,.carousel-control .icon-prev{width:30px;height:30px;margin-top:-10px;font-size:30px}.carousel-control .glyphicon-chevron-left,.carousel-control .icon-prev{margin-left:-10px}.carousel-control .glyphicon-chevron-right,.carousel-control .icon-next{margin-right:-10px}.carousel-caption{right:20%;left:20%;padding-bottom:30px}.carousel-indicators{bottom:20px}}.btn-group-vertical>.btn-group:after,.btn-group-vertical>.btn-group:before,.btn-toolbar:after,.btn-toolbar:before,.clearfix:after,.clearfix:before,.container-fluid:after,.container-fluid:before,.container:after,.container:before,.dl-horizontal dd:after,.dl-horizontal dd:before,.form-horizontal .form-group:after,.form-horizontal .form-group:before,.modal-footer:after,.modal-footer:before,.modal-header:after,.modal-header:before,.nav:after,.nav:before,.navbar-collapse:after,.navbar-collapse:before,.navbar-header:after,.navbar-header:before,.navbar:after,.navbar:before,.pager:after,.pager:before,.panel-body:after,.panel-body:before,.row:after,.row:before{display:table;content:" "}.btn-group-vertical>.btn-group:after,.btn-toolbar:after,.clearfix:after,.container-fluid:after,.container:after,.dl-horizontal dd:after,.form-horizontal .form-group:after,.modal-footer:after,.modal-header:after,.nav:after,.navbar-collapse:after,.navbar-header:after,.navbar:after,.pager:after,.panel-body:after,.row:after{clear:both}.center-block{display:block;margin-right:auto;margin-left:auto}.pull-right{float:right!important}.pull-left{float:left!important}.hide{display:none!important}.show{display:block!important}.invisible{visibility:hidden}.text-hide{font:0/0 a;color:transparent;text-shadow:none;background-color:transparent;border:0}.hidden{display:none!important}.affix{position:fixed}@-ms-viewport{width:device-width}.visible-lg,.visible-md,.visible-sm,.visible-xs{display:none!important}.visible-lg-block,.visible-lg-inline,.visible-lg-inline-block,.visible-md-block,.visible-md-inline,.visible-md-inline-block,.visible-sm-block,.visible-sm-inline,.visible-sm-inline-block,.visible-xs-block,.visible-xs-inline,.visible-xs-inline-block{display:none!important}@media (max-width:767px){.visible-xs{display:block!important}table.visible-xs{display:table!important}tr.visible-xs{display:table-row!important}td.visible-xs,th.visible-xs{display:table-cell!important}}@media (max-width:767px){.visible-xs-block{display:block!important}}@media (max-width:767px){.visible-xs-inline{display:inline!important}}@media (max-width:767px){.visible-xs-inline-block{display:inline-block!important}}@media (min-width:768px) and (max-width:991px){.visible-sm{display:block!important}table.visible-sm{display:table!important}tr.visible-sm{display:table-row!important}td.visible-sm,th.visible-sm{display:table-cell!important}}@media (min-width:768px) and (max-width:991px){.visible-sm-block{display:block!important}}@media (min-width:768px) and (max-width:991px){.visible-sm-inline{display:inline!important}}@media (min-width:768px) and (max-width:991px){.visible-sm-inline-block{display:inline-block!important}}@media (min-width:992px) and (max-width:1199px){.visible-md{display:block!important}table.visible-md{display:table!important}tr.visible-md{display:table-row!important}td.visible-md,th.visible-md{display:table-cell!important}}@media (min-width:992px) and (max-width:1199px){.visible-md-block{display:block!important}}@media (min-width:992px) and (max-width:1199px){.visible-md-inline{display:inline!important}}@media (min-width:992px) and (max-width:1199px){.visible-md-inline-block{display:inline-block!important}}@media (min-width:1200px){.visible-lg{display:block!important}table.visible-lg{display:table!important}tr.visible-lg{display:table-row!important}td.visible-lg,th.visible-lg{display:table-cell!important}}@media (min-width:1200px){.visible-lg-block{display:block!important}}@media (min-width:1200px){.visible-lg-inline{display:inline!important}}@media (min-width:1200px){.visible-lg-inline-block{display:inline-block!important}}@media (max-width:767px){.hidden-xs{display:none!important}}@media (min-width:768px) and (max-width:991px){.hidden-sm{display:none!important}}@media (min-width:992px) and (max-width:1199px){.hidden-md{display:none!important}}@media (min-width:1200px){.hidden-lg{display:none!important}}.visible-print{display:none!important}@media print{.visible-print{display:block!important}table.visible-print{display:table!important}tr.visible-print{display:table-row!important}td.visible-print,th.visible-print{display:table-cell!important}}.visible-print-block{display:none!important}@media print{.visible-print-block{display:block!important}}.visible-print-inline{display:none!important}@media print{.visible-print-inline{display:inline!important}}.visible-print-inline-block{display:none!important}@media print{.visible-print-inline-block{display:inline-block!important}}@media print{.hidden-print{display:none!important}} -/*# sourceMappingURL=bootstrap.min.css.map */ \ No newline at end of file diff --git a/src/websrc/3rdparty/css/bootstrap-3.4.1.min.css b/src/websrc/3rdparty/css/bootstrap-3.4.1.min.css new file mode 100644 index 000000000..5b96335ff --- /dev/null +++ b/src/websrc/3rdparty/css/bootstrap-3.4.1.min.css @@ -0,0 +1,6 @@ +/*! + * Bootstrap v3.4.1 (https://getbootstrap.com/) + * Copyright 2011-2019 Twitter, Inc. + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) + *//*! normalize.css v3.0.3 | MIT License | github.com/necolas/normalize.css */html{font-family:sans-serif;-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%}body{margin:0}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{background-color:transparent}a:active,a:hover{outline:0}abbr[title]{border-bottom:none;text-decoration:underline;-webkit-text-decoration:underline dotted;-moz-text-decoration:underline dotted;text-decoration:underline dotted}b,strong{font-weight:700}dfn{font-style:italic}h1{font-size:2em;margin:.67em 0}mark{background:#ff0;color:#000}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sup{top:-.5em}sub{bottom:-.25em}img{border:0}svg:not(:root){overflow:hidden}figure{margin:1em 40px}hr{-webkit-box-sizing:content-box;-moz-box-sizing:content-box;box-sizing:content-box;height:0}pre{overflow:auto}code,kbd,pre,samp{font-family:monospace,monospace;font-size:1em}button,input,optgroup,select,textarea{color:inherit;font:inherit;margin:0}button{overflow:visible}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]{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;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;-webkit-box-sizing:content-box;-moz-box-sizing:content-box;box-sizing:content-box}input[type=search]::-webkit-search-cancel-button,input[type=search]::-webkit-search-decoration{-webkit-appearance:none}fieldset{border:1px solid silver;margin:0 2px;padding:.35em .625em .75em}legend{border:0;padding:0}textarea{overflow:auto}optgroup{font-weight:700}table{border-collapse:collapse;border-spacing:0}td,th{padding:0}/*! Source: https://github.com/h5bp/html5-boilerplate/blob/master/src/css/main.css */@media print{*,:after,:before{color:#000!important;text-shadow:none!important;background:0 0!important;-webkit-box-shadow:none!important;box-shadow:none!important}a,a:visited{text-decoration:underline}a[href]:after{content:" (" attr(href) ")"}abbr[title]:after{content:" (" attr(title) ")"}a[href^="#"]:after,a[href^="javascript:"]:after{content:""}blockquote,pre{border:1px solid #999;page-break-inside:avoid}thead{display:table-header-group}img,tr{page-break-inside:avoid}img{max-width:100%!important}h2,h3,p{orphans:3;widows:3}h2,h3{page-break-after:avoid}.navbar{display:none}.btn>.caret,.dropup>.btn>.caret{border-top-color:#000!important}.label{border:1px solid #000}.table{border-collapse:collapse!important}.table td,.table th{background-color:#fff!important}.table-bordered td,.table-bordered th{border:1px solid #ddd!important}}@font-face{font-family:"Glyphicons Halflings";src:url(../fonts/glyphicons-halflings-regular.eot);src:url(../fonts/glyphicons-halflings-regular.eot?#iefix) format("embedded-opentype"),url(../fonts/glyphicons-halflings-regular.woff2) format("woff2"),url(../fonts/glyphicons-halflings-regular.woff) format("woff"),url(../fonts/glyphicons-halflings-regular.ttf) format("truetype"),url(../fonts/glyphicons-halflings-regular.svg#glyphicons_halflingsregular) format("svg")}.glyphicon{position:relative;top:1px;display:inline-block;font-family:"Glyphicons Halflings";font-style:normal;font-weight:400;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.glyphicon-asterisk:before{content:"\002a"}.glyphicon-plus:before{content:"\002b"}.glyphicon-eur:before,.glyphicon-euro:before{content:"\20ac"}.glyphicon-minus:before{content:"\2212"}.glyphicon-cloud:before{content:"\2601"}.glyphicon-envelope:before{content:"\2709"}.glyphicon-pencil:before{content:"\270f"}.glyphicon-glass:before{content:"\e001"}.glyphicon-music:before{content:"\e002"}.glyphicon-search:before{content:"\e003"}.glyphicon-heart:before{content:"\e005"}.glyphicon-star:before{content:"\e006"}.glyphicon-star-empty:before{content:"\e007"}.glyphicon-user:before{content:"\e008"}.glyphicon-film:before{content:"\e009"}.glyphicon-th-large:before{content:"\e010"}.glyphicon-th:before{content:"\e011"}.glyphicon-th-list:before{content:"\e012"}.glyphicon-ok:before{content:"\e013"}.glyphicon-remove:before{content:"\e014"}.glyphicon-zoom-in:before{content:"\e015"}.glyphicon-zoom-out:before{content:"\e016"}.glyphicon-off:before{content:"\e017"}.glyphicon-signal:before{content:"\e018"}.glyphicon-cog:before{content:"\e019"}.glyphicon-trash:before{content:"\e020"}.glyphicon-home:before{content:"\e021"}.glyphicon-file:before{content:"\e022"}.glyphicon-time:before{content:"\e023"}.glyphicon-road:before{content:"\e024"}.glyphicon-download-alt:before{content:"\e025"}.glyphicon-download:before{content:"\e026"}.glyphicon-upload:before{content:"\e027"}.glyphicon-inbox:before{content:"\e028"}.glyphicon-play-circle:before{content:"\e029"}.glyphicon-repeat:before{content:"\e030"}.glyphicon-refresh:before{content:"\e031"}.glyphicon-list-alt:before{content:"\e032"}.glyphicon-lock:before{content:"\e033"}.glyphicon-flag:before{content:"\e034"}.glyphicon-headphones:before{content:"\e035"}.glyphicon-volume-off:before{content:"\e036"}.glyphicon-volume-down:before{content:"\e037"}.glyphicon-volume-up:before{content:"\e038"}.glyphicon-qrcode:before{content:"\e039"}.glyphicon-barcode:before{content:"\e040"}.glyphicon-tag:before{content:"\e041"}.glyphicon-tags:before{content:"\e042"}.glyphicon-book:before{content:"\e043"}.glyphicon-bookmark:before{content:"\e044"}.glyphicon-print:before{content:"\e045"}.glyphicon-camera:before{content:"\e046"}.glyphicon-font:before{content:"\e047"}.glyphicon-bold:before{content:"\e048"}.glyphicon-italic:before{content:"\e049"}.glyphicon-text-height:before{content:"\e050"}.glyphicon-text-width:before{content:"\e051"}.glyphicon-align-left:before{content:"\e052"}.glyphicon-align-center:before{content:"\e053"}.glyphicon-align-right:before{content:"\e054"}.glyphicon-align-justify:before{content:"\e055"}.glyphicon-list:before{content:"\e056"}.glyphicon-indent-left:before{content:"\e057"}.glyphicon-indent-right:before{content:"\e058"}.glyphicon-facetime-video:before{content:"\e059"}.glyphicon-picture:before{content:"\e060"}.glyphicon-map-marker:before{content:"\e062"}.glyphicon-adjust:before{content:"\e063"}.glyphicon-tint:before{content:"\e064"}.glyphicon-edit:before{content:"\e065"}.glyphicon-share:before{content:"\e066"}.glyphicon-check:before{content:"\e067"}.glyphicon-move:before{content:"\e068"}.glyphicon-step-backward:before{content:"\e069"}.glyphicon-fast-backward:before{content:"\e070"}.glyphicon-backward:before{content:"\e071"}.glyphicon-play:before{content:"\e072"}.glyphicon-pause:before{content:"\e073"}.glyphicon-stop:before{content:"\e074"}.glyphicon-forward:before{content:"\e075"}.glyphicon-fast-forward:before{content:"\e076"}.glyphicon-step-forward:before{content:"\e077"}.glyphicon-eject:before{content:"\e078"}.glyphicon-chevron-left:before{content:"\e079"}.glyphicon-chevron-right:before{content:"\e080"}.glyphicon-plus-sign:before{content:"\e081"}.glyphicon-minus-sign:before{content:"\e082"}.glyphicon-remove-sign:before{content:"\e083"}.glyphicon-ok-sign:before{content:"\e084"}.glyphicon-question-sign:before{content:"\e085"}.glyphicon-info-sign:before{content:"\e086"}.glyphicon-screenshot:before{content:"\e087"}.glyphicon-remove-circle:before{content:"\e088"}.glyphicon-ok-circle:before{content:"\e089"}.glyphicon-ban-circle:before{content:"\e090"}.glyphicon-arrow-left:before{content:"\e091"}.glyphicon-arrow-right:before{content:"\e092"}.glyphicon-arrow-up:before{content:"\e093"}.glyphicon-arrow-down:before{content:"\e094"}.glyphicon-share-alt:before{content:"\e095"}.glyphicon-resize-full:before{content:"\e096"}.glyphicon-resize-small:before{content:"\e097"}.glyphicon-exclamation-sign:before{content:"\e101"}.glyphicon-gift:before{content:"\e102"}.glyphicon-leaf:before{content:"\e103"}.glyphicon-fire:before{content:"\e104"}.glyphicon-eye-open:before{content:"\e105"}.glyphicon-eye-close:before{content:"\e106"}.glyphicon-warning-sign:before{content:"\e107"}.glyphicon-plane:before{content:"\e108"}.glyphicon-calendar:before{content:"\e109"}.glyphicon-random:before{content:"\e110"}.glyphicon-comment:before{content:"\e111"}.glyphicon-magnet:before{content:"\e112"}.glyphicon-chevron-up:before{content:"\e113"}.glyphicon-chevron-down:before{content:"\e114"}.glyphicon-retweet:before{content:"\e115"}.glyphicon-shopping-cart:before{content:"\e116"}.glyphicon-folder-close:before{content:"\e117"}.glyphicon-folder-open:before{content:"\e118"}.glyphicon-resize-vertical:before{content:"\e119"}.glyphicon-resize-horizontal:before{content:"\e120"}.glyphicon-hdd:before{content:"\e121"}.glyphicon-bullhorn:before{content:"\e122"}.glyphicon-bell:before{content:"\e123"}.glyphicon-certificate:before{content:"\e124"}.glyphicon-thumbs-up:before{content:"\e125"}.glyphicon-thumbs-down:before{content:"\e126"}.glyphicon-hand-right:before{content:"\e127"}.glyphicon-hand-left:before{content:"\e128"}.glyphicon-hand-up:before{content:"\e129"}.glyphicon-hand-down:before{content:"\e130"}.glyphicon-circle-arrow-right:before{content:"\e131"}.glyphicon-circle-arrow-left:before{content:"\e132"}.glyphicon-circle-arrow-up:before{content:"\e133"}.glyphicon-circle-arrow-down:before{content:"\e134"}.glyphicon-globe:before{content:"\e135"}.glyphicon-wrench:before{content:"\e136"}.glyphicon-tasks:before{content:"\e137"}.glyphicon-filter:before{content:"\e138"}.glyphicon-briefcase:before{content:"\e139"}.glyphicon-fullscreen:before{content:"\e140"}.glyphicon-dashboard:before{content:"\e141"}.glyphicon-paperclip:before{content:"\e142"}.glyphicon-heart-empty:before{content:"\e143"}.glyphicon-link:before{content:"\e144"}.glyphicon-phone:before{content:"\e145"}.glyphicon-pushpin:before{content:"\e146"}.glyphicon-usd:before{content:"\e148"}.glyphicon-gbp:before{content:"\e149"}.glyphicon-sort:before{content:"\e150"}.glyphicon-sort-by-alphabet:before{content:"\e151"}.glyphicon-sort-by-alphabet-alt:before{content:"\e152"}.glyphicon-sort-by-order:before{content:"\e153"}.glyphicon-sort-by-order-alt:before{content:"\e154"}.glyphicon-sort-by-attributes:before{content:"\e155"}.glyphicon-sort-by-attributes-alt:before{content:"\e156"}.glyphicon-unchecked:before{content:"\e157"}.glyphicon-expand:before{content:"\e158"}.glyphicon-collapse-down:before{content:"\e159"}.glyphicon-collapse-up:before{content:"\e160"}.glyphicon-log-in:before{content:"\e161"}.glyphicon-flash:before{content:"\e162"}.glyphicon-log-out:before{content:"\e163"}.glyphicon-new-window:before{content:"\e164"}.glyphicon-record:before{content:"\e165"}.glyphicon-save:before{content:"\e166"}.glyphicon-open:before{content:"\e167"}.glyphicon-saved:before{content:"\e168"}.glyphicon-import:before{content:"\e169"}.glyphicon-export:before{content:"\e170"}.glyphicon-send:before{content:"\e171"}.glyphicon-floppy-disk:before{content:"\e172"}.glyphicon-floppy-saved:before{content:"\e173"}.glyphicon-floppy-remove:before{content:"\e174"}.glyphicon-floppy-save:before{content:"\e175"}.glyphicon-floppy-open:before{content:"\e176"}.glyphicon-credit-card:before{content:"\e177"}.glyphicon-transfer:before{content:"\e178"}.glyphicon-cutlery:before{content:"\e179"}.glyphicon-header:before{content:"\e180"}.glyphicon-compressed:before{content:"\e181"}.glyphicon-earphone:before{content:"\e182"}.glyphicon-phone-alt:before{content:"\e183"}.glyphicon-tower:before{content:"\e184"}.glyphicon-stats:before{content:"\e185"}.glyphicon-sd-video:before{content:"\e186"}.glyphicon-hd-video:before{content:"\e187"}.glyphicon-subtitles:before{content:"\e188"}.glyphicon-sound-stereo:before{content:"\e189"}.glyphicon-sound-dolby:before{content:"\e190"}.glyphicon-sound-5-1:before{content:"\e191"}.glyphicon-sound-6-1:before{content:"\e192"}.glyphicon-sound-7-1:before{content:"\e193"}.glyphicon-copyright-mark:before{content:"\e194"}.glyphicon-registration-mark:before{content:"\e195"}.glyphicon-cloud-download:before{content:"\e197"}.glyphicon-cloud-upload:before{content:"\e198"}.glyphicon-tree-conifer:before{content:"\e199"}.glyphicon-tree-deciduous:before{content:"\e200"}.glyphicon-cd:before{content:"\e201"}.glyphicon-save-file:before{content:"\e202"}.glyphicon-open-file:before{content:"\e203"}.glyphicon-level-up:before{content:"\e204"}.glyphicon-copy:before{content:"\e205"}.glyphicon-paste:before{content:"\e206"}.glyphicon-alert:before{content:"\e209"}.glyphicon-equalizer:before{content:"\e210"}.glyphicon-king:before{content:"\e211"}.glyphicon-queen:before{content:"\e212"}.glyphicon-pawn:before{content:"\e213"}.glyphicon-bishop:before{content:"\e214"}.glyphicon-knight:before{content:"\e215"}.glyphicon-baby-formula:before{content:"\e216"}.glyphicon-tent:before{content:"\26fa"}.glyphicon-blackboard:before{content:"\e218"}.glyphicon-bed:before{content:"\e219"}.glyphicon-apple:before{content:"\f8ff"}.glyphicon-erase:before{content:"\e221"}.glyphicon-hourglass:before{content:"\231b"}.glyphicon-lamp:before{content:"\e223"}.glyphicon-duplicate:before{content:"\e224"}.glyphicon-piggy-bank:before{content:"\e225"}.glyphicon-scissors:before{content:"\e226"}.glyphicon-bitcoin:before{content:"\e227"}.glyphicon-btc:before{content:"\e227"}.glyphicon-xbt:before{content:"\e227"}.glyphicon-yen:before{content:"\00a5"}.glyphicon-jpy:before{content:"\00a5"}.glyphicon-ruble:before{content:"\20bd"}.glyphicon-rub:before{content:"\20bd"}.glyphicon-scale:before{content:"\e230"}.glyphicon-ice-lolly:before{content:"\e231"}.glyphicon-ice-lolly-tasted:before{content:"\e232"}.glyphicon-education:before{content:"\e233"}.glyphicon-option-horizontal:before{content:"\e234"}.glyphicon-option-vertical:before{content:"\e235"}.glyphicon-menu-hamburger:before{content:"\e236"}.glyphicon-modal-window:before{content:"\e237"}.glyphicon-oil:before{content:"\e238"}.glyphicon-grain:before{content:"\e239"}.glyphicon-sunglasses:before{content:"\e240"}.glyphicon-text-size:before{content:"\e241"}.glyphicon-text-color:before{content:"\e242"}.glyphicon-text-background:before{content:"\e243"}.glyphicon-object-align-top:before{content:"\e244"}.glyphicon-object-align-bottom:before{content:"\e245"}.glyphicon-object-align-horizontal:before{content:"\e246"}.glyphicon-object-align-left:before{content:"\e247"}.glyphicon-object-align-vertical:before{content:"\e248"}.glyphicon-object-align-right:before{content:"\e249"}.glyphicon-triangle-right:before{content:"\e250"}.glyphicon-triangle-left:before{content:"\e251"}.glyphicon-triangle-bottom:before{content:"\e252"}.glyphicon-triangle-top:before{content:"\e253"}.glyphicon-console:before{content:"\e254"}.glyphicon-superscript:before{content:"\e255"}.glyphicon-subscript:before{content:"\e256"}.glyphicon-menu-left:before{content:"\e257"}.glyphicon-menu-right:before{content:"\e258"}.glyphicon-menu-down:before{content:"\e259"}.glyphicon-menu-up:before{content:"\e260"}*{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}:after,:before{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}html{font-size:10px;-webkit-tap-highlight-color:rgba(0,0,0,0)}body{font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;font-size:14px;line-height:1.42857143;color:#333;background-color:#fff}button,input,select,textarea{font-family:inherit;font-size:inherit;line-height:inherit}a{color:#337ab7;text-decoration:none}a:focus,a:hover{color:#23527c;text-decoration:underline}a:focus{outline:5px auto -webkit-focus-ring-color;outline-offset:-2px}figure{margin:0}img{vertical-align:middle}.carousel-inner>.item>a>img,.carousel-inner>.item>img,.img-responsive,.thumbnail a>img,.thumbnail>img{display:block;max-width:100%;height:auto}.img-rounded{border-radius:6px}.img-thumbnail{padding:4px;line-height:1.42857143;background-color:#fff;border:1px solid #ddd;border-radius:4px;-webkit-transition:all .2s ease-in-out;-o-transition:all .2s ease-in-out;transition:all .2s ease-in-out;display:inline-block;max-width:100%;height:auto}.img-circle{border-radius:50%}hr{margin-top:20px;margin-bottom:20px;border:0;border-top:1px solid #eee}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);border:0}.sr-only-focusable:active,.sr-only-focusable:focus{position:static;width:auto;height:auto;margin:0;overflow:visible;clip:auto}[role=button]{cursor:pointer}.h1,.h2,.h3,.h4,.h5,.h6,h1,h2,h3,h4,h5,h6{font-family:inherit;font-weight:500;line-height:1.1;color:inherit}.h1 .small,.h1 small,.h2 .small,.h2 small,.h3 .small,.h3 small,.h4 .small,.h4 small,.h5 .small,.h5 small,.h6 .small,.h6 small,h1 .small,h1 small,h2 .small,h2 small,h3 .small,h3 small,h4 .small,h4 small,h5 .small,h5 small,h6 .small,h6 small{font-weight:400;line-height:1;color:#777}.h1,.h2,.h3,h1,h2,h3{margin-top:20px;margin-bottom:10px}.h1 .small,.h1 small,.h2 .small,.h2 small,.h3 .small,.h3 small,h1 .small,h1 small,h2 .small,h2 small,h3 .small,h3 small{font-size:65%}.h4,.h5,.h6,h4,h5,h6{margin-top:10px;margin-bottom:10px}.h4 .small,.h4 small,.h5 .small,.h5 small,.h6 .small,.h6 small,h4 .small,h4 small,h5 .small,h5 small,h6 .small,h6 small{font-size:75%}.h1,h1{font-size:36px}.h2,h2{font-size:30px}.h3,h3{font-size:24px}.h4,h4{font-size:18px}.h5,h5{font-size:14px}.h6,h6{font-size:12px}p{margin:0 0 10px}.lead{margin-bottom:20px;font-size:16px;font-weight:300;line-height:1.4}@media (min-width:768px){.lead{font-size:21px}}.small,small{font-size:85%}.mark,mark{padding:.2em;background-color:#fcf8e3}.text-left{text-align:left}.text-right{text-align:right}.text-center{text-align:center}.text-justify{text-align:justify}.text-nowrap{white-space:nowrap}.text-lowercase{text-transform:lowercase}.text-uppercase{text-transform:uppercase}.text-capitalize{text-transform:capitalize}.text-muted{color:#777}.text-primary{color:#337ab7}a.text-primary:focus,a.text-primary:hover{color:#286090}.text-success{color:#3c763d}a.text-success:focus,a.text-success:hover{color:#2b542c}.text-info{color:#31708f}a.text-info:focus,a.text-info:hover{color:#245269}.text-warning{color:#8a6d3b}a.text-warning:focus,a.text-warning:hover{color:#66512c}.text-danger{color:#a94442}a.text-danger:focus,a.text-danger:hover{color:#843534}.bg-primary{color:#fff;background-color:#337ab7}a.bg-primary:focus,a.bg-primary:hover{background-color:#286090}.bg-success{background-color:#dff0d8}a.bg-success:focus,a.bg-success:hover{background-color:#c1e2b3}.bg-info{background-color:#d9edf7}a.bg-info:focus,a.bg-info:hover{background-color:#afd9ee}.bg-warning{background-color:#fcf8e3}a.bg-warning:focus,a.bg-warning:hover{background-color:#f7ecb5}.bg-danger{background-color:#f2dede}a.bg-danger:focus,a.bg-danger:hover{background-color:#e4b9b9}.page-header{padding-bottom:9px;margin:40px 0 20px;border-bottom:1px solid #eee}ol,ul{margin-top:0;margin-bottom:10px}ol ol,ol ul,ul ol,ul ul{margin-bottom:0}.list-unstyled{padding-left:0;list-style:none}.list-inline{padding-left:0;list-style:none;margin-left:-5px}.list-inline>li{display:inline-block;padding-right:5px;padding-left:5px}dl{margin-top:0;margin-bottom:20px}dd,dt{line-height:1.42857143}dt{font-weight:700}dd{margin-left:0}@media (min-width:768px){.dl-horizontal dt{float:left;width:160px;clear:left;text-align:right;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.dl-horizontal dd{margin-left:180px}}abbr[data-original-title],abbr[title]{cursor:help}.initialism{font-size:90%;text-transform:uppercase}blockquote{padding:10px 20px;margin:0 0 20px;font-size:17.5px;border-left:5px solid #eee}blockquote ol:last-child,blockquote p:last-child,blockquote ul:last-child{margin-bottom:0}blockquote .small,blockquote footer,blockquote small{display:block;font-size:80%;line-height:1.42857143;color:#777}blockquote .small:before,blockquote footer:before,blockquote small:before{content:"\2014 \00A0"}.blockquote-reverse,blockquote.pull-right{padding-right:15px;padding-left:0;text-align:right;border-right:5px solid #eee;border-left:0}.blockquote-reverse .small:before,.blockquote-reverse footer:before,.blockquote-reverse small:before,blockquote.pull-right .small:before,blockquote.pull-right footer:before,blockquote.pull-right small:before{content:""}.blockquote-reverse .small:after,.blockquote-reverse footer:after,.blockquote-reverse small:after,blockquote.pull-right .small:after,blockquote.pull-right footer:after,blockquote.pull-right small:after{content:"\00A0 \2014"}address{margin-bottom:20px;font-style:normal;line-height:1.42857143}code,kbd,pre,samp{font-family:Menlo,Monaco,Consolas,"Courier New",monospace}code{padding:2px 4px;font-size:90%;color:#c7254e;background-color:#f9f2f4;border-radius:4px}kbd{padding:2px 4px;font-size:90%;color:#fff;background-color:#333;border-radius:3px;-webkit-box-shadow:inset 0 -1px 0 rgba(0,0,0,.25);box-shadow:inset 0 -1px 0 rgba(0,0,0,.25)}kbd kbd{padding:0;font-size:100%;font-weight:700;-webkit-box-shadow:none;box-shadow:none}pre{display:block;padding:9.5px;margin:0 0 10px;font-size:13px;line-height:1.42857143;color:#333;word-break:break-all;word-wrap:break-word;background-color:#f5f5f5;border:1px solid #ccc;border-radius:4px}pre code{padding:0;font-size:inherit;color:inherit;white-space:pre-wrap;background-color:transparent;border-radius:0}.pre-scrollable{max-height:340px;overflow-y:scroll}.container{padding-right:15px;padding-left:15px;margin-right:auto;margin-left:auto}@media (min-width:768px){.container{width:750px}}@media (min-width:992px){.container{width:970px}}@media (min-width:1200px){.container{width:1170px}}.container-fluid{padding-right:15px;padding-left:15px;margin-right:auto;margin-left:auto}.row{margin-right:-15px;margin-left:-15px}.row-no-gutters{margin-right:0;margin-left:0}.row-no-gutters [class*=col-]{padding-right:0;padding-left:0}.col-lg-1,.col-lg-10,.col-lg-11,.col-lg-12,.col-lg-2,.col-lg-3,.col-lg-4,.col-lg-5,.col-lg-6,.col-lg-7,.col-lg-8,.col-lg-9,.col-md-1,.col-md-10,.col-md-11,.col-md-12,.col-md-2,.col-md-3,.col-md-4,.col-md-5,.col-md-6,.col-md-7,.col-md-8,.col-md-9,.col-sm-1,.col-sm-10,.col-sm-11,.col-sm-12,.col-sm-2,.col-sm-3,.col-sm-4,.col-sm-5,.col-sm-6,.col-sm-7,.col-sm-8,.col-sm-9,.col-xs-1,.col-xs-10,.col-xs-11,.col-xs-12,.col-xs-2,.col-xs-3,.col-xs-4,.col-xs-5,.col-xs-6,.col-xs-7,.col-xs-8,.col-xs-9{position:relative;min-height:1px;padding-right:15px;padding-left:15px}.col-xs-1,.col-xs-10,.col-xs-11,.col-xs-12,.col-xs-2,.col-xs-3,.col-xs-4,.col-xs-5,.col-xs-6,.col-xs-7,.col-xs-8,.col-xs-9{float:left}.col-xs-12{width:100%}.col-xs-11{width:91.66666667%}.col-xs-10{width:83.33333333%}.col-xs-9{width:75%}.col-xs-8{width:66.66666667%}.col-xs-7{width:58.33333333%}.col-xs-6{width:50%}.col-xs-5{width:41.66666667%}.col-xs-4{width:33.33333333%}.col-xs-3{width:25%}.col-xs-2{width:16.66666667%}.col-xs-1{width:8.33333333%}.col-xs-pull-12{right:100%}.col-xs-pull-11{right:91.66666667%}.col-xs-pull-10{right:83.33333333%}.col-xs-pull-9{right:75%}.col-xs-pull-8{right:66.66666667%}.col-xs-pull-7{right:58.33333333%}.col-xs-pull-6{right:50%}.col-xs-pull-5{right:41.66666667%}.col-xs-pull-4{right:33.33333333%}.col-xs-pull-3{right:25%}.col-xs-pull-2{right:16.66666667%}.col-xs-pull-1{right:8.33333333%}.col-xs-pull-0{right:auto}.col-xs-push-12{left:100%}.col-xs-push-11{left:91.66666667%}.col-xs-push-10{left:83.33333333%}.col-xs-push-9{left:75%}.col-xs-push-8{left:66.66666667%}.col-xs-push-7{left:58.33333333%}.col-xs-push-6{left:50%}.col-xs-push-5{left:41.66666667%}.col-xs-push-4{left:33.33333333%}.col-xs-push-3{left:25%}.col-xs-push-2{left:16.66666667%}.col-xs-push-1{left:8.33333333%}.col-xs-push-0{left:auto}.col-xs-offset-12{margin-left:100%}.col-xs-offset-11{margin-left:91.66666667%}.col-xs-offset-10{margin-left:83.33333333%}.col-xs-offset-9{margin-left:75%}.col-xs-offset-8{margin-left:66.66666667%}.col-xs-offset-7{margin-left:58.33333333%}.col-xs-offset-6{margin-left:50%}.col-xs-offset-5{margin-left:41.66666667%}.col-xs-offset-4{margin-left:33.33333333%}.col-xs-offset-3{margin-left:25%}.col-xs-offset-2{margin-left:16.66666667%}.col-xs-offset-1{margin-left:8.33333333%}.col-xs-offset-0{margin-left:0}@media (min-width:768px){.col-sm-1,.col-sm-10,.col-sm-11,.col-sm-12,.col-sm-2,.col-sm-3,.col-sm-4,.col-sm-5,.col-sm-6,.col-sm-7,.col-sm-8,.col-sm-9{float:left}.col-sm-12{width:100%}.col-sm-11{width:91.66666667%}.col-sm-10{width:83.33333333%}.col-sm-9{width:75%}.col-sm-8{width:66.66666667%}.col-sm-7{width:58.33333333%}.col-sm-6{width:50%}.col-sm-5{width:41.66666667%}.col-sm-4{width:33.33333333%}.col-sm-3{width:25%}.col-sm-2{width:16.66666667%}.col-sm-1{width:8.33333333%}.col-sm-pull-12{right:100%}.col-sm-pull-11{right:91.66666667%}.col-sm-pull-10{right:83.33333333%}.col-sm-pull-9{right:75%}.col-sm-pull-8{right:66.66666667%}.col-sm-pull-7{right:58.33333333%}.col-sm-pull-6{right:50%}.col-sm-pull-5{right:41.66666667%}.col-sm-pull-4{right:33.33333333%}.col-sm-pull-3{right:25%}.col-sm-pull-2{right:16.66666667%}.col-sm-pull-1{right:8.33333333%}.col-sm-pull-0{right:auto}.col-sm-push-12{left:100%}.col-sm-push-11{left:91.66666667%}.col-sm-push-10{left:83.33333333%}.col-sm-push-9{left:75%}.col-sm-push-8{left:66.66666667%}.col-sm-push-7{left:58.33333333%}.col-sm-push-6{left:50%}.col-sm-push-5{left:41.66666667%}.col-sm-push-4{left:33.33333333%}.col-sm-push-3{left:25%}.col-sm-push-2{left:16.66666667%}.col-sm-push-1{left:8.33333333%}.col-sm-push-0{left:auto}.col-sm-offset-12{margin-left:100%}.col-sm-offset-11{margin-left:91.66666667%}.col-sm-offset-10{margin-left:83.33333333%}.col-sm-offset-9{margin-left:75%}.col-sm-offset-8{margin-left:66.66666667%}.col-sm-offset-7{margin-left:58.33333333%}.col-sm-offset-6{margin-left:50%}.col-sm-offset-5{margin-left:41.66666667%}.col-sm-offset-4{margin-left:33.33333333%}.col-sm-offset-3{margin-left:25%}.col-sm-offset-2{margin-left:16.66666667%}.col-sm-offset-1{margin-left:8.33333333%}.col-sm-offset-0{margin-left:0}}@media (min-width:992px){.col-md-1,.col-md-10,.col-md-11,.col-md-12,.col-md-2,.col-md-3,.col-md-4,.col-md-5,.col-md-6,.col-md-7,.col-md-8,.col-md-9{float:left}.col-md-12{width:100%}.col-md-11{width:91.66666667%}.col-md-10{width:83.33333333%}.col-md-9{width:75%}.col-md-8{width:66.66666667%}.col-md-7{width:58.33333333%}.col-md-6{width:50%}.col-md-5{width:41.66666667%}.col-md-4{width:33.33333333%}.col-md-3{width:25%}.col-md-2{width:16.66666667%}.col-md-1{width:8.33333333%}.col-md-pull-12{right:100%}.col-md-pull-11{right:91.66666667%}.col-md-pull-10{right:83.33333333%}.col-md-pull-9{right:75%}.col-md-pull-8{right:66.66666667%}.col-md-pull-7{right:58.33333333%}.col-md-pull-6{right:50%}.col-md-pull-5{right:41.66666667%}.col-md-pull-4{right:33.33333333%}.col-md-pull-3{right:25%}.col-md-pull-2{right:16.66666667%}.col-md-pull-1{right:8.33333333%}.col-md-pull-0{right:auto}.col-md-push-12{left:100%}.col-md-push-11{left:91.66666667%}.col-md-push-10{left:83.33333333%}.col-md-push-9{left:75%}.col-md-push-8{left:66.66666667%}.col-md-push-7{left:58.33333333%}.col-md-push-6{left:50%}.col-md-push-5{left:41.66666667%}.col-md-push-4{left:33.33333333%}.col-md-push-3{left:25%}.col-md-push-2{left:16.66666667%}.col-md-push-1{left:8.33333333%}.col-md-push-0{left:auto}.col-md-offset-12{margin-left:100%}.col-md-offset-11{margin-left:91.66666667%}.col-md-offset-10{margin-left:83.33333333%}.col-md-offset-9{margin-left:75%}.col-md-offset-8{margin-left:66.66666667%}.col-md-offset-7{margin-left:58.33333333%}.col-md-offset-6{margin-left:50%}.col-md-offset-5{margin-left:41.66666667%}.col-md-offset-4{margin-left:33.33333333%}.col-md-offset-3{margin-left:25%}.col-md-offset-2{margin-left:16.66666667%}.col-md-offset-1{margin-left:8.33333333%}.col-md-offset-0{margin-left:0}}@media (min-width:1200px){.col-lg-1,.col-lg-10,.col-lg-11,.col-lg-12,.col-lg-2,.col-lg-3,.col-lg-4,.col-lg-5,.col-lg-6,.col-lg-7,.col-lg-8,.col-lg-9{float:left}.col-lg-12{width:100%}.col-lg-11{width:91.66666667%}.col-lg-10{width:83.33333333%}.col-lg-9{width:75%}.col-lg-8{width:66.66666667%}.col-lg-7{width:58.33333333%}.col-lg-6{width:50%}.col-lg-5{width:41.66666667%}.col-lg-4{width:33.33333333%}.col-lg-3{width:25%}.col-lg-2{width:16.66666667%}.col-lg-1{width:8.33333333%}.col-lg-pull-12{right:100%}.col-lg-pull-11{right:91.66666667%}.col-lg-pull-10{right:83.33333333%}.col-lg-pull-9{right:75%}.col-lg-pull-8{right:66.66666667%}.col-lg-pull-7{right:58.33333333%}.col-lg-pull-6{right:50%}.col-lg-pull-5{right:41.66666667%}.col-lg-pull-4{right:33.33333333%}.col-lg-pull-3{right:25%}.col-lg-pull-2{right:16.66666667%}.col-lg-pull-1{right:8.33333333%}.col-lg-pull-0{right:auto}.col-lg-push-12{left:100%}.col-lg-push-11{left:91.66666667%}.col-lg-push-10{left:83.33333333%}.col-lg-push-9{left:75%}.col-lg-push-8{left:66.66666667%}.col-lg-push-7{left:58.33333333%}.col-lg-push-6{left:50%}.col-lg-push-5{left:41.66666667%}.col-lg-push-4{left:33.33333333%}.col-lg-push-3{left:25%}.col-lg-push-2{left:16.66666667%}.col-lg-push-1{left:8.33333333%}.col-lg-push-0{left:auto}.col-lg-offset-12{margin-left:100%}.col-lg-offset-11{margin-left:91.66666667%}.col-lg-offset-10{margin-left:83.33333333%}.col-lg-offset-9{margin-left:75%}.col-lg-offset-8{margin-left:66.66666667%}.col-lg-offset-7{margin-left:58.33333333%}.col-lg-offset-6{margin-left:50%}.col-lg-offset-5{margin-left:41.66666667%}.col-lg-offset-4{margin-left:33.33333333%}.col-lg-offset-3{margin-left:25%}.col-lg-offset-2{margin-left:16.66666667%}.col-lg-offset-1{margin-left:8.33333333%}.col-lg-offset-0{margin-left:0}}table{background-color:transparent}table col[class*=col-]{position:static;display:table-column;float:none}table td[class*=col-],table th[class*=col-]{position:static;display:table-cell;float:none}caption{padding-top:8px;padding-bottom:8px;color:#777;text-align:left}th{text-align:left}.table{width:100%;max-width:100%;margin-bottom:20px}.table>tbody>tr>td,.table>tbody>tr>th,.table>tfoot>tr>td,.table>tfoot>tr>th,.table>thead>tr>td,.table>thead>tr>th{padding:8px;line-height:1.42857143;vertical-align:top;border-top:1px solid #ddd}.table>thead>tr>th{vertical-align:bottom;border-bottom:2px solid #ddd}.table>caption+thead>tr:first-child>td,.table>caption+thead>tr:first-child>th,.table>colgroup+thead>tr:first-child>td,.table>colgroup+thead>tr:first-child>th,.table>thead:first-child>tr:first-child>td,.table>thead:first-child>tr:first-child>th{border-top:0}.table>tbody+tbody{border-top:2px solid #ddd}.table .table{background-color:#fff}.table-condensed>tbody>tr>td,.table-condensed>tbody>tr>th,.table-condensed>tfoot>tr>td,.table-condensed>tfoot>tr>th,.table-condensed>thead>tr>td,.table-condensed>thead>tr>th{padding:5px}.table-bordered{border:1px solid #ddd}.table-bordered>tbody>tr>td,.table-bordered>tbody>tr>th,.table-bordered>tfoot>tr>td,.table-bordered>tfoot>tr>th,.table-bordered>thead>tr>td,.table-bordered>thead>tr>th{border:1px solid #ddd}.table-bordered>thead>tr>td,.table-bordered>thead>tr>th{border-bottom-width:2px}.table-striped>tbody>tr:nth-of-type(odd){background-color:#f9f9f9}.table-hover>tbody>tr:hover{background-color:#f5f5f5}.table>tbody>tr.active>td,.table>tbody>tr.active>th,.table>tbody>tr>td.active,.table>tbody>tr>th.active,.table>tfoot>tr.active>td,.table>tfoot>tr.active>th,.table>tfoot>tr>td.active,.table>tfoot>tr>th.active,.table>thead>tr.active>td,.table>thead>tr.active>th,.table>thead>tr>td.active,.table>thead>tr>th.active{background-color:#f5f5f5}.table-hover>tbody>tr.active:hover>td,.table-hover>tbody>tr.active:hover>th,.table-hover>tbody>tr:hover>.active,.table-hover>tbody>tr>td.active:hover,.table-hover>tbody>tr>th.active:hover{background-color:#e8e8e8}.table>tbody>tr.success>td,.table>tbody>tr.success>th,.table>tbody>tr>td.success,.table>tbody>tr>th.success,.table>tfoot>tr.success>td,.table>tfoot>tr.success>th,.table>tfoot>tr>td.success,.table>tfoot>tr>th.success,.table>thead>tr.success>td,.table>thead>tr.success>th,.table>thead>tr>td.success,.table>thead>tr>th.success{background-color:#dff0d8}.table-hover>tbody>tr.success:hover>td,.table-hover>tbody>tr.success:hover>th,.table-hover>tbody>tr:hover>.success,.table-hover>tbody>tr>td.success:hover,.table-hover>tbody>tr>th.success:hover{background-color:#d0e9c6}.table>tbody>tr.info>td,.table>tbody>tr.info>th,.table>tbody>tr>td.info,.table>tbody>tr>th.info,.table>tfoot>tr.info>td,.table>tfoot>tr.info>th,.table>tfoot>tr>td.info,.table>tfoot>tr>th.info,.table>thead>tr.info>td,.table>thead>tr.info>th,.table>thead>tr>td.info,.table>thead>tr>th.info{background-color:#d9edf7}.table-hover>tbody>tr.info:hover>td,.table-hover>tbody>tr.info:hover>th,.table-hover>tbody>tr:hover>.info,.table-hover>tbody>tr>td.info:hover,.table-hover>tbody>tr>th.info:hover{background-color:#c4e3f3}.table>tbody>tr.warning>td,.table>tbody>tr.warning>th,.table>tbody>tr>td.warning,.table>tbody>tr>th.warning,.table>tfoot>tr.warning>td,.table>tfoot>tr.warning>th,.table>tfoot>tr>td.warning,.table>tfoot>tr>th.warning,.table>thead>tr.warning>td,.table>thead>tr.warning>th,.table>thead>tr>td.warning,.table>thead>tr>th.warning{background-color:#fcf8e3}.table-hover>tbody>tr.warning:hover>td,.table-hover>tbody>tr.warning:hover>th,.table-hover>tbody>tr:hover>.warning,.table-hover>tbody>tr>td.warning:hover,.table-hover>tbody>tr>th.warning:hover{background-color:#faf2cc}.table>tbody>tr.danger>td,.table>tbody>tr.danger>th,.table>tbody>tr>td.danger,.table>tbody>tr>th.danger,.table>tfoot>tr.danger>td,.table>tfoot>tr.danger>th,.table>tfoot>tr>td.danger,.table>tfoot>tr>th.danger,.table>thead>tr.danger>td,.table>thead>tr.danger>th,.table>thead>tr>td.danger,.table>thead>tr>th.danger{background-color:#f2dede}.table-hover>tbody>tr.danger:hover>td,.table-hover>tbody>tr.danger:hover>th,.table-hover>tbody>tr:hover>.danger,.table-hover>tbody>tr>td.danger:hover,.table-hover>tbody>tr>th.danger:hover{background-color:#ebcccc}.table-responsive{min-height:.01%;overflow-x:auto}@media screen and (max-width:767px){.table-responsive{width:100%;margin-bottom:15px;overflow-y:hidden;-ms-overflow-style:-ms-autohiding-scrollbar;border:1px solid #ddd}.table-responsive>.table{margin-bottom:0}.table-responsive>.table>tbody>tr>td,.table-responsive>.table>tbody>tr>th,.table-responsive>.table>tfoot>tr>td,.table-responsive>.table>tfoot>tr>th,.table-responsive>.table>thead>tr>td,.table-responsive>.table>thead>tr>th{white-space:nowrap}.table-responsive>.table-bordered{border:0}.table-responsive>.table-bordered>tbody>tr>td:first-child,.table-responsive>.table-bordered>tbody>tr>th:first-child,.table-responsive>.table-bordered>tfoot>tr>td:first-child,.table-responsive>.table-bordered>tfoot>tr>th:first-child,.table-responsive>.table-bordered>thead>tr>td:first-child,.table-responsive>.table-bordered>thead>tr>th:first-child{border-left:0}.table-responsive>.table-bordered>tbody>tr>td:last-child,.table-responsive>.table-bordered>tbody>tr>th:last-child,.table-responsive>.table-bordered>tfoot>tr>td:last-child,.table-responsive>.table-bordered>tfoot>tr>th:last-child,.table-responsive>.table-bordered>thead>tr>td:last-child,.table-responsive>.table-bordered>thead>tr>th:last-child{border-right:0}.table-responsive>.table-bordered>tbody>tr:last-child>td,.table-responsive>.table-bordered>tbody>tr:last-child>th,.table-responsive>.table-bordered>tfoot>tr:last-child>td,.table-responsive>.table-bordered>tfoot>tr:last-child>th{border-bottom:0}}fieldset{min-width:0;padding:0;margin:0;border:0}legend{display:block;width:100%;padding:0;margin-bottom:20px;font-size:21px;line-height:inherit;color:#333;border:0;border-bottom:1px solid #e5e5e5}label{display:inline-block;max-width:100%;margin-bottom:5px;font-weight:700}input[type=search]{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box;-webkit-appearance:none;-moz-appearance:none;appearance:none}input[type=checkbox],input[type=radio]{margin:4px 0 0;margin-top:1px\9;line-height:normal}fieldset[disabled] input[type=checkbox],fieldset[disabled] input[type=radio],input[type=checkbox].disabled,input[type=checkbox][disabled],input[type=radio].disabled,input[type=radio][disabled]{cursor:not-allowed}input[type=file]{display:block}input[type=range]{display:block;width:100%}select[multiple],select[size]{height:auto}input[type=checkbox]:focus,input[type=file]:focus,input[type=radio]:focus{outline:5px auto -webkit-focus-ring-color;outline-offset:-2px}output{display:block;padding-top:7px;font-size:14px;line-height:1.42857143;color:#555}.form-control{display:block;width:100%;height:34px;padding:6px 12px;font-size:14px;line-height:1.42857143;color:#555;background-color:#fff;background-image:none;border:1px solid #ccc;border-radius:4px;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075);box-shadow:inset 0 1px 1px rgba(0,0,0,.075);-webkit-transition:border-color ease-in-out .15s,box-shadow ease-in-out .15s;-o-transition:border-color ease-in-out .15s,box-shadow ease-in-out .15s;-webkit-transition:border-color ease-in-out .15s,-webkit-box-shadow ease-in-out .15s;transition:border-color ease-in-out .15s,-webkit-box-shadow ease-in-out .15s;transition:border-color ease-in-out .15s,box-shadow ease-in-out .15s;transition:border-color ease-in-out .15s,box-shadow ease-in-out .15s,-webkit-box-shadow ease-in-out .15s}.form-control:focus{border-color:#66afe9;outline:0;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 8px rgba(102,175,233,.6);box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 8px rgba(102,175,233,.6)}.form-control::-moz-placeholder{color:#999;opacity:1}.form-control:-ms-input-placeholder{color:#999}.form-control::-webkit-input-placeholder{color:#999}.form-control::-ms-expand{background-color:transparent;border:0}.form-control[disabled],.form-control[readonly],fieldset[disabled] .form-control{background-color:#eee;opacity:1}.form-control[disabled],fieldset[disabled] .form-control{cursor:not-allowed}textarea.form-control{height:auto}@media screen and (-webkit-min-device-pixel-ratio:0){input[type=date].form-control,input[type=datetime-local].form-control,input[type=month].form-control,input[type=time].form-control{line-height:34px}.input-group-sm input[type=date],.input-group-sm input[type=datetime-local],.input-group-sm input[type=month],.input-group-sm input[type=time],input[type=date].input-sm,input[type=datetime-local].input-sm,input[type=month].input-sm,input[type=time].input-sm{line-height:30px}.input-group-lg input[type=date],.input-group-lg input[type=datetime-local],.input-group-lg input[type=month],.input-group-lg input[type=time],input[type=date].input-lg,input[type=datetime-local].input-lg,input[type=month].input-lg,input[type=time].input-lg{line-height:46px}}.form-group{margin-bottom:15px}.checkbox,.radio{position:relative;display:block;margin-top:10px;margin-bottom:10px}.checkbox.disabled label,.radio.disabled label,fieldset[disabled] .checkbox label,fieldset[disabled] .radio label{cursor:not-allowed}.checkbox label,.radio label{min-height:20px;padding-left:20px;margin-bottom:0;font-weight:400;cursor:pointer}.checkbox input[type=checkbox],.checkbox-inline input[type=checkbox],.radio input[type=radio],.radio-inline input[type=radio]{position:absolute;margin-top:4px\9;margin-left:-20px}.checkbox+.checkbox,.radio+.radio{margin-top:-5px}.checkbox-inline,.radio-inline{position:relative;display:inline-block;padding-left:20px;margin-bottom:0;font-weight:400;vertical-align:middle;cursor:pointer}.checkbox-inline.disabled,.radio-inline.disabled,fieldset[disabled] .checkbox-inline,fieldset[disabled] .radio-inline{cursor:not-allowed}.checkbox-inline+.checkbox-inline,.radio-inline+.radio-inline{margin-top:0;margin-left:10px}.form-control-static{min-height:34px;padding-top:7px;padding-bottom:7px;margin-bottom:0}.form-control-static.input-lg,.form-control-static.input-sm{padding-right:0;padding-left:0}.input-sm{height:30px;padding:5px 10px;font-size:12px;line-height:1.5;border-radius:3px}select.input-sm{height:30px;line-height:30px}select[multiple].input-sm,textarea.input-sm{height:auto}.form-group-sm .form-control{height:30px;padding:5px 10px;font-size:12px;line-height:1.5;border-radius:3px}.form-group-sm select.form-control{height:30px;line-height:30px}.form-group-sm select[multiple].form-control,.form-group-sm textarea.form-control{height:auto}.form-group-sm .form-control-static{height:30px;min-height:32px;padding:6px 10px;font-size:12px;line-height:1.5}.input-lg{height:46px;padding:10px 16px;font-size:18px;line-height:1.3333333;border-radius:6px}select.input-lg{height:46px;line-height:46px}select[multiple].input-lg,textarea.input-lg{height:auto}.form-group-lg .form-control{height:46px;padding:10px 16px;font-size:18px;line-height:1.3333333;border-radius:6px}.form-group-lg select.form-control{height:46px;line-height:46px}.form-group-lg select[multiple].form-control,.form-group-lg textarea.form-control{height:auto}.form-group-lg .form-control-static{height:46px;min-height:38px;padding:11px 16px;font-size:18px;line-height:1.3333333}.has-feedback{position:relative}.has-feedback .form-control{padding-right:42.5px}.form-control-feedback{position:absolute;top:0;right:0;z-index:2;display:block;width:34px;height:34px;line-height:34px;text-align:center;pointer-events:none}.form-group-lg .form-control+.form-control-feedback,.input-group-lg+.form-control-feedback,.input-lg+.form-control-feedback{width:46px;height:46px;line-height:46px}.form-group-sm .form-control+.form-control-feedback,.input-group-sm+.form-control-feedback,.input-sm+.form-control-feedback{width:30px;height:30px;line-height:30px}.has-success .checkbox,.has-success .checkbox-inline,.has-success .control-label,.has-success .help-block,.has-success .radio,.has-success .radio-inline,.has-success.checkbox label,.has-success.checkbox-inline label,.has-success.radio label,.has-success.radio-inline label{color:#3c763d}.has-success .form-control{border-color:#3c763d;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075);box-shadow:inset 0 1px 1px rgba(0,0,0,.075)}.has-success .form-control:focus{border-color:#2b542c;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #67b168;box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #67b168}.has-success .input-group-addon{color:#3c763d;background-color:#dff0d8;border-color:#3c763d}.has-success .form-control-feedback{color:#3c763d}.has-warning .checkbox,.has-warning .checkbox-inline,.has-warning .control-label,.has-warning .help-block,.has-warning .radio,.has-warning .radio-inline,.has-warning.checkbox label,.has-warning.checkbox-inline label,.has-warning.radio label,.has-warning.radio-inline label{color:#8a6d3b}.has-warning .form-control{border-color:#8a6d3b;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075);box-shadow:inset 0 1px 1px rgba(0,0,0,.075)}.has-warning .form-control:focus{border-color:#66512c;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #c0a16b;box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #c0a16b}.has-warning .input-group-addon{color:#8a6d3b;background-color:#fcf8e3;border-color:#8a6d3b}.has-warning .form-control-feedback{color:#8a6d3b}.has-error .checkbox,.has-error .checkbox-inline,.has-error .control-label,.has-error .help-block,.has-error .radio,.has-error .radio-inline,.has-error.checkbox label,.has-error.checkbox-inline label,.has-error.radio label,.has-error.radio-inline label{color:#a94442}.has-error .form-control{border-color:#a94442;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075);box-shadow:inset 0 1px 1px rgba(0,0,0,.075)}.has-error .form-control:focus{border-color:#843534;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #ce8483;box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #ce8483}.has-error .input-group-addon{color:#a94442;background-color:#f2dede;border-color:#a94442}.has-error .form-control-feedback{color:#a94442}.has-feedback label~.form-control-feedback{top:25px}.has-feedback label.sr-only~.form-control-feedback{top:0}.help-block{display:block;margin-top:5px;margin-bottom:10px;color:#737373}@media (min-width:768px){.form-inline .form-group{display:inline-block;margin-bottom:0;vertical-align:middle}.form-inline .form-control{display:inline-block;width:auto;vertical-align:middle}.form-inline .form-control-static{display:inline-block}.form-inline .input-group{display:inline-table;vertical-align:middle}.form-inline .input-group .form-control,.form-inline .input-group .input-group-addon,.form-inline .input-group .input-group-btn{width:auto}.form-inline .input-group>.form-control{width:100%}.form-inline .control-label{margin-bottom:0;vertical-align:middle}.form-inline .checkbox,.form-inline .radio{display:inline-block;margin-top:0;margin-bottom:0;vertical-align:middle}.form-inline .checkbox label,.form-inline .radio label{padding-left:0}.form-inline .checkbox input[type=checkbox],.form-inline .radio input[type=radio]{position:relative;margin-left:0}.form-inline .has-feedback .form-control-feedback{top:0}}.form-horizontal .checkbox,.form-horizontal .checkbox-inline,.form-horizontal .radio,.form-horizontal .radio-inline{padding-top:7px;margin-top:0;margin-bottom:0}.form-horizontal .checkbox,.form-horizontal .radio{min-height:27px}.form-horizontal .form-group{margin-right:-15px;margin-left:-15px}@media (min-width:768px){.form-horizontal .control-label{padding-top:7px;margin-bottom:0;text-align:right}}.form-horizontal .has-feedback .form-control-feedback{right:15px}@media (min-width:768px){.form-horizontal .form-group-lg .control-label{padding-top:11px;font-size:18px}}@media (min-width:768px){.form-horizontal .form-group-sm .control-label{padding-top:6px;font-size:12px}}.btn{display:inline-block;margin-bottom:0;font-weight:400;text-align:center;white-space:nowrap;vertical-align:middle;-ms-touch-action:manipulation;touch-action:manipulation;cursor:pointer;background-image:none;border:1px solid transparent;padding:6px 12px;font-size:14px;line-height:1.42857143;border-radius:4px;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.btn.active.focus,.btn.active:focus,.btn.focus,.btn:active.focus,.btn:active:focus,.btn:focus{outline:5px auto -webkit-focus-ring-color;outline-offset:-2px}.btn.focus,.btn:focus,.btn:hover{color:#333;text-decoration:none}.btn.active,.btn:active{background-image:none;outline:0;-webkit-box-shadow:inset 0 3px 5px rgba(0,0,0,.125);box-shadow:inset 0 3px 5px rgba(0,0,0,.125)}.btn.disabled,.btn[disabled],fieldset[disabled] .btn{cursor:not-allowed;filter:alpha(opacity=65);opacity:.65;-webkit-box-shadow:none;box-shadow:none}a.btn.disabled,fieldset[disabled] a.btn{pointer-events:none}.btn-default{color:#333;background-color:#fff;border-color:#ccc}.btn-default.focus,.btn-default:focus{color:#333;background-color:#e6e6e6;border-color:#8c8c8c}.btn-default:hover{color:#333;background-color:#e6e6e6;border-color:#adadad}.btn-default.active,.btn-default:active,.open>.dropdown-toggle.btn-default{color:#333;background-color:#e6e6e6;background-image:none;border-color:#adadad}.btn-default.active.focus,.btn-default.active:focus,.btn-default.active:hover,.btn-default:active.focus,.btn-default:active:focus,.btn-default:active:hover,.open>.dropdown-toggle.btn-default.focus,.open>.dropdown-toggle.btn-default:focus,.open>.dropdown-toggle.btn-default:hover{color:#333;background-color:#d4d4d4;border-color:#8c8c8c}.btn-default.disabled.focus,.btn-default.disabled:focus,.btn-default.disabled:hover,.btn-default[disabled].focus,.btn-default[disabled]:focus,.btn-default[disabled]:hover,fieldset[disabled] .btn-default.focus,fieldset[disabled] .btn-default:focus,fieldset[disabled] .btn-default:hover{background-color:#fff;border-color:#ccc}.btn-default .badge{color:#fff;background-color:#333}.btn-primary{color:#fff;background-color:#337ab7;border-color:#2e6da4}.btn-primary.focus,.btn-primary:focus{color:#fff;background-color:#286090;border-color:#122b40}.btn-primary:hover{color:#fff;background-color:#286090;border-color:#204d74}.btn-primary.active,.btn-primary:active,.open>.dropdown-toggle.btn-primary{color:#fff;background-color:#286090;background-image:none;border-color:#204d74}.btn-primary.active.focus,.btn-primary.active:focus,.btn-primary.active:hover,.btn-primary:active.focus,.btn-primary:active:focus,.btn-primary:active:hover,.open>.dropdown-toggle.btn-primary.focus,.open>.dropdown-toggle.btn-primary:focus,.open>.dropdown-toggle.btn-primary:hover{color:#fff;background-color:#204d74;border-color:#122b40}.btn-primary.disabled.focus,.btn-primary.disabled:focus,.btn-primary.disabled:hover,.btn-primary[disabled].focus,.btn-primary[disabled]:focus,.btn-primary[disabled]:hover,fieldset[disabled] .btn-primary.focus,fieldset[disabled] .btn-primary:focus,fieldset[disabled] .btn-primary:hover{background-color:#337ab7;border-color:#2e6da4}.btn-primary .badge{color:#337ab7;background-color:#fff}.btn-success{color:#fff;background-color:#5cb85c;border-color:#4cae4c}.btn-success.focus,.btn-success:focus{color:#fff;background-color:#449d44;border-color:#255625}.btn-success:hover{color:#fff;background-color:#449d44;border-color:#398439}.btn-success.active,.btn-success:active,.open>.dropdown-toggle.btn-success{color:#fff;background-color:#449d44;background-image:none;border-color:#398439}.btn-success.active.focus,.btn-success.active:focus,.btn-success.active:hover,.btn-success:active.focus,.btn-success:active:focus,.btn-success:active:hover,.open>.dropdown-toggle.btn-success.focus,.open>.dropdown-toggle.btn-success:focus,.open>.dropdown-toggle.btn-success:hover{color:#fff;background-color:#398439;border-color:#255625}.btn-success.disabled.focus,.btn-success.disabled:focus,.btn-success.disabled:hover,.btn-success[disabled].focus,.btn-success[disabled]:focus,.btn-success[disabled]:hover,fieldset[disabled] .btn-success.focus,fieldset[disabled] .btn-success:focus,fieldset[disabled] .btn-success:hover{background-color:#5cb85c;border-color:#4cae4c}.btn-success .badge{color:#5cb85c;background-color:#fff}.btn-info{color:#fff;background-color:#5bc0de;border-color:#46b8da}.btn-info.focus,.btn-info:focus{color:#fff;background-color:#31b0d5;border-color:#1b6d85}.btn-info:hover{color:#fff;background-color:#31b0d5;border-color:#269abc}.btn-info.active,.btn-info:active,.open>.dropdown-toggle.btn-info{color:#fff;background-color:#31b0d5;background-image:none;border-color:#269abc}.btn-info.active.focus,.btn-info.active:focus,.btn-info.active:hover,.btn-info:active.focus,.btn-info:active:focus,.btn-info:active:hover,.open>.dropdown-toggle.btn-info.focus,.open>.dropdown-toggle.btn-info:focus,.open>.dropdown-toggle.btn-info:hover{color:#fff;background-color:#269abc;border-color:#1b6d85}.btn-info.disabled.focus,.btn-info.disabled:focus,.btn-info.disabled:hover,.btn-info[disabled].focus,.btn-info[disabled]:focus,.btn-info[disabled]:hover,fieldset[disabled] .btn-info.focus,fieldset[disabled] .btn-info:focus,fieldset[disabled] .btn-info:hover{background-color:#5bc0de;border-color:#46b8da}.btn-info .badge{color:#5bc0de;background-color:#fff}.btn-warning{color:#fff;background-color:#f0ad4e;border-color:#eea236}.btn-warning.focus,.btn-warning:focus{color:#fff;background-color:#ec971f;border-color:#985f0d}.btn-warning:hover{color:#fff;background-color:#ec971f;border-color:#d58512}.btn-warning.active,.btn-warning:active,.open>.dropdown-toggle.btn-warning{color:#fff;background-color:#ec971f;background-image:none;border-color:#d58512}.btn-warning.active.focus,.btn-warning.active:focus,.btn-warning.active:hover,.btn-warning:active.focus,.btn-warning:active:focus,.btn-warning:active:hover,.open>.dropdown-toggle.btn-warning.focus,.open>.dropdown-toggle.btn-warning:focus,.open>.dropdown-toggle.btn-warning:hover{color:#fff;background-color:#d58512;border-color:#985f0d}.btn-warning.disabled.focus,.btn-warning.disabled:focus,.btn-warning.disabled:hover,.btn-warning[disabled].focus,.btn-warning[disabled]:focus,.btn-warning[disabled]:hover,fieldset[disabled] .btn-warning.focus,fieldset[disabled] .btn-warning:focus,fieldset[disabled] .btn-warning:hover{background-color:#f0ad4e;border-color:#eea236}.btn-warning .badge{color:#f0ad4e;background-color:#fff}.btn-danger{color:#fff;background-color:#d9534f;border-color:#d43f3a}.btn-danger.focus,.btn-danger:focus{color:#fff;background-color:#c9302c;border-color:#761c19}.btn-danger:hover{color:#fff;background-color:#c9302c;border-color:#ac2925}.btn-danger.active,.btn-danger:active,.open>.dropdown-toggle.btn-danger{color:#fff;background-color:#c9302c;background-image:none;border-color:#ac2925}.btn-danger.active.focus,.btn-danger.active:focus,.btn-danger.active:hover,.btn-danger:active.focus,.btn-danger:active:focus,.btn-danger:active:hover,.open>.dropdown-toggle.btn-danger.focus,.open>.dropdown-toggle.btn-danger:focus,.open>.dropdown-toggle.btn-danger:hover{color:#fff;background-color:#ac2925;border-color:#761c19}.btn-danger.disabled.focus,.btn-danger.disabled:focus,.btn-danger.disabled:hover,.btn-danger[disabled].focus,.btn-danger[disabled]:focus,.btn-danger[disabled]:hover,fieldset[disabled] .btn-danger.focus,fieldset[disabled] .btn-danger:focus,fieldset[disabled] .btn-danger:hover{background-color:#d9534f;border-color:#d43f3a}.btn-danger .badge{color:#d9534f;background-color:#fff}.btn-link{font-weight:400;color:#337ab7;border-radius:0}.btn-link,.btn-link.active,.btn-link:active,.btn-link[disabled],fieldset[disabled] .btn-link{background-color:transparent;-webkit-box-shadow:none;box-shadow:none}.btn-link,.btn-link:active,.btn-link:focus,.btn-link:hover{border-color:transparent}.btn-link:focus,.btn-link:hover{color:#23527c;text-decoration:underline;background-color:transparent}.btn-link[disabled]:focus,.btn-link[disabled]:hover,fieldset[disabled] .btn-link:focus,fieldset[disabled] .btn-link:hover{color:#777;text-decoration:none}.btn-group-lg>.btn,.btn-lg{padding:10px 16px;font-size:18px;line-height:1.3333333;border-radius:6px}.btn-group-sm>.btn,.btn-sm{padding:5px 10px;font-size:12px;line-height:1.5;border-radius:3px}.btn-group-xs>.btn,.btn-xs{padding:1px 5px;font-size:12px;line-height:1.5;border-radius:3px}.btn-block{display:block;width:100%}.btn-block+.btn-block{margin-top:5px}input[type=button].btn-block,input[type=reset].btn-block,input[type=submit].btn-block{width:100%}.fade{opacity:0;-webkit-transition:opacity .15s linear;-o-transition:opacity .15s linear;transition:opacity .15s linear}.fade.in{opacity:1}.collapse{display:none}.collapse.in{display:block}tr.collapse.in{display:table-row}tbody.collapse.in{display:table-row-group}.collapsing{position:relative;height:0;overflow:hidden;-webkit-transition-property:height,visibility;-o-transition-property:height,visibility;transition-property:height,visibility;-webkit-transition-duration:.35s;-o-transition-duration:.35s;transition-duration:.35s;-webkit-transition-timing-function:ease;-o-transition-timing-function:ease;transition-timing-function:ease}.caret{display:inline-block;width:0;height:0;margin-left:2px;vertical-align:middle;border-top:4px dashed;border-top:4px solid\9;border-right:4px solid transparent;border-left:4px solid transparent}.dropdown,.dropup{position:relative}.dropdown-toggle:focus{outline:0}.dropdown-menu{position:absolute;top:100%;left:0;z-index:1000;display:none;float:left;min-width:160px;padding:5px 0;margin:2px 0 0;font-size:14px;text-align:left;list-style:none;background-color:#fff;background-clip:padding-box;border:1px solid #ccc;border:1px solid rgba(0,0,0,.15);border-radius:4px;-webkit-box-shadow:0 6px 12px rgba(0,0,0,.175);box-shadow:0 6px 12px rgba(0,0,0,.175)}.dropdown-menu.pull-right{right:0;left:auto}.dropdown-menu .divider{height:1px;margin:9px 0;overflow:hidden;background-color:#e5e5e5}.dropdown-menu>li>a{display:block;padding:3px 20px;clear:both;font-weight:400;line-height:1.42857143;color:#333;white-space:nowrap}.dropdown-menu>li>a:focus,.dropdown-menu>li>a:hover{color:#262626;text-decoration:none;background-color:#f5f5f5}.dropdown-menu>.active>a,.dropdown-menu>.active>a:focus,.dropdown-menu>.active>a:hover{color:#fff;text-decoration:none;background-color:#337ab7;outline:0}.dropdown-menu>.disabled>a,.dropdown-menu>.disabled>a:focus,.dropdown-menu>.disabled>a:hover{color:#777}.dropdown-menu>.disabled>a:focus,.dropdown-menu>.disabled>a:hover{text-decoration:none;cursor:not-allowed;background-color:transparent;background-image:none;filter:progid:DXImageTransform.Microsoft.gradient(enabled=false)}.open>.dropdown-menu{display:block}.open>a{outline:0}.dropdown-menu-right{right:0;left:auto}.dropdown-menu-left{right:auto;left:0}.dropdown-header{display:block;padding:3px 20px;font-size:12px;line-height:1.42857143;color:#777;white-space:nowrap}.dropdown-backdrop{position:fixed;top:0;right:0;bottom:0;left:0;z-index:990}.pull-right>.dropdown-menu{right:0;left:auto}.dropup .caret,.navbar-fixed-bottom .dropdown .caret{content:"";border-top:0;border-bottom:4px dashed;border-bottom:4px solid\9}.dropup .dropdown-menu,.navbar-fixed-bottom .dropdown .dropdown-menu{top:auto;bottom:100%;margin-bottom:2px}@media (min-width:768px){.navbar-right .dropdown-menu{right:0;left:auto}.navbar-right .dropdown-menu-left{right:auto;left:0}}.btn-group,.btn-group-vertical{position:relative;display:inline-block;vertical-align:middle}.btn-group-vertical>.btn,.btn-group>.btn{position:relative;float:left}.btn-group-vertical>.btn.active,.btn-group-vertical>.btn:active,.btn-group-vertical>.btn:focus,.btn-group-vertical>.btn:hover,.btn-group>.btn.active,.btn-group>.btn:active,.btn-group>.btn:focus,.btn-group>.btn:hover{z-index:2}.btn-group .btn+.btn,.btn-group .btn+.btn-group,.btn-group .btn-group+.btn,.btn-group .btn-group+.btn-group{margin-left:-1px}.btn-toolbar{margin-left:-5px}.btn-toolbar .btn,.btn-toolbar .btn-group,.btn-toolbar .input-group{float:left}.btn-toolbar>.btn,.btn-toolbar>.btn-group,.btn-toolbar>.input-group{margin-left:5px}.btn-group>.btn:not(:first-child):not(:last-child):not(.dropdown-toggle){border-radius:0}.btn-group>.btn:first-child{margin-left:0}.btn-group>.btn:first-child:not(:last-child):not(.dropdown-toggle){border-top-right-radius:0;border-bottom-right-radius:0}.btn-group>.btn:last-child:not(:first-child),.btn-group>.dropdown-toggle:not(:first-child){border-top-left-radius:0;border-bottom-left-radius:0}.btn-group>.btn-group{float:left}.btn-group>.btn-group:not(:first-child):not(:last-child)>.btn{border-radius:0}.btn-group>.btn-group:first-child:not(:last-child)>.btn:last-child,.btn-group>.btn-group:first-child:not(:last-child)>.dropdown-toggle{border-top-right-radius:0;border-bottom-right-radius:0}.btn-group>.btn-group:last-child:not(:first-child)>.btn:first-child{border-top-left-radius:0;border-bottom-left-radius:0}.btn-group .dropdown-toggle:active,.btn-group.open .dropdown-toggle{outline:0}.btn-group>.btn+.dropdown-toggle{padding-right:8px;padding-left:8px}.btn-group>.btn-lg+.dropdown-toggle{padding-right:12px;padding-left:12px}.btn-group.open .dropdown-toggle{-webkit-box-shadow:inset 0 3px 5px rgba(0,0,0,.125);box-shadow:inset 0 3px 5px rgba(0,0,0,.125)}.btn-group.open .dropdown-toggle.btn-link{-webkit-box-shadow:none;box-shadow:none}.btn .caret{margin-left:0}.btn-lg .caret{border-width:5px 5px 0;border-bottom-width:0}.dropup .btn-lg .caret{border-width:0 5px 5px}.btn-group-vertical>.btn,.btn-group-vertical>.btn-group,.btn-group-vertical>.btn-group>.btn{display:block;float:none;width:100%;max-width:100%}.btn-group-vertical>.btn-group>.btn{float:none}.btn-group-vertical>.btn+.btn,.btn-group-vertical>.btn+.btn-group,.btn-group-vertical>.btn-group+.btn,.btn-group-vertical>.btn-group+.btn-group{margin-top:-1px;margin-left:0}.btn-group-vertical>.btn:not(:first-child):not(:last-child){border-radius:0}.btn-group-vertical>.btn:first-child:not(:last-child){border-top-left-radius:4px;border-top-right-radius:4px;border-bottom-right-radius:0;border-bottom-left-radius:0}.btn-group-vertical>.btn:last-child:not(:first-child){border-top-left-radius:0;border-top-right-radius:0;border-bottom-right-radius:4px;border-bottom-left-radius:4px}.btn-group-vertical>.btn-group:not(:first-child):not(:last-child)>.btn{border-radius:0}.btn-group-vertical>.btn-group:first-child:not(:last-child)>.btn:last-child,.btn-group-vertical>.btn-group:first-child:not(:last-child)>.dropdown-toggle{border-bottom-right-radius:0;border-bottom-left-radius:0}.btn-group-vertical>.btn-group:last-child:not(:first-child)>.btn:first-child{border-top-left-radius:0;border-top-right-radius:0}.btn-group-justified{display:table;width:100%;table-layout:fixed;border-collapse:separate}.btn-group-justified>.btn,.btn-group-justified>.btn-group{display:table-cell;float:none;width:1%}.btn-group-justified>.btn-group .btn{width:100%}.btn-group-justified>.btn-group .dropdown-menu{left:auto}[data-toggle=buttons]>.btn input[type=checkbox],[data-toggle=buttons]>.btn input[type=radio],[data-toggle=buttons]>.btn-group>.btn input[type=checkbox],[data-toggle=buttons]>.btn-group>.btn input[type=radio]{position:absolute;clip:rect(0,0,0,0);pointer-events:none}.input-group{position:relative;display:table;border-collapse:separate}.input-group[class*=col-]{float:none;padding-right:0;padding-left:0}.input-group .form-control{position:relative;z-index:2;float:left;width:100%;margin-bottom:0}.input-group .form-control:focus{z-index:3}.input-group-lg>.form-control,.input-group-lg>.input-group-addon,.input-group-lg>.input-group-btn>.btn{height:46px;padding:10px 16px;font-size:18px;line-height:1.3333333;border-radius:6px}select.input-group-lg>.form-control,select.input-group-lg>.input-group-addon,select.input-group-lg>.input-group-btn>.btn{height:46px;line-height:46px}select[multiple].input-group-lg>.form-control,select[multiple].input-group-lg>.input-group-addon,select[multiple].input-group-lg>.input-group-btn>.btn,textarea.input-group-lg>.form-control,textarea.input-group-lg>.input-group-addon,textarea.input-group-lg>.input-group-btn>.btn{height:auto}.input-group-sm>.form-control,.input-group-sm>.input-group-addon,.input-group-sm>.input-group-btn>.btn{height:30px;padding:5px 10px;font-size:12px;line-height:1.5;border-radius:3px}select.input-group-sm>.form-control,select.input-group-sm>.input-group-addon,select.input-group-sm>.input-group-btn>.btn{height:30px;line-height:30px}select[multiple].input-group-sm>.form-control,select[multiple].input-group-sm>.input-group-addon,select[multiple].input-group-sm>.input-group-btn>.btn,textarea.input-group-sm>.form-control,textarea.input-group-sm>.input-group-addon,textarea.input-group-sm>.input-group-btn>.btn{height:auto}.input-group .form-control,.input-group-addon,.input-group-btn{display:table-cell}.input-group .form-control:not(:first-child):not(:last-child),.input-group-addon:not(:first-child):not(:last-child),.input-group-btn:not(:first-child):not(:last-child){border-radius:0}.input-group-addon,.input-group-btn{width:1%;white-space:nowrap;vertical-align:middle}.input-group-addon{padding:6px 12px;font-size:14px;font-weight:400;line-height:1;color:#555;text-align:center;background-color:#eee;border:1px solid #ccc;border-radius:4px}.input-group-addon.input-sm{padding:5px 10px;font-size:12px;border-radius:3px}.input-group-addon.input-lg{padding:10px 16px;font-size:18px;border-radius:6px}.input-group-addon input[type=checkbox],.input-group-addon input[type=radio]{margin-top:0}.input-group .form-control:first-child,.input-group-addon:first-child,.input-group-btn:first-child>.btn,.input-group-btn:first-child>.btn-group>.btn,.input-group-btn:first-child>.dropdown-toggle,.input-group-btn:last-child>.btn-group:not(:last-child)>.btn,.input-group-btn:last-child>.btn:not(:last-child):not(.dropdown-toggle){border-top-right-radius:0;border-bottom-right-radius:0}.input-group-addon:first-child{border-right:0}.input-group .form-control:last-child,.input-group-addon:last-child,.input-group-btn:first-child>.btn-group:not(:first-child)>.btn,.input-group-btn:first-child>.btn:not(:first-child),.input-group-btn:last-child>.btn,.input-group-btn:last-child>.btn-group>.btn,.input-group-btn:last-child>.dropdown-toggle{border-top-left-radius:0;border-bottom-left-radius:0}.input-group-addon:last-child{border-left:0}.input-group-btn{position:relative;font-size:0;white-space:nowrap}.input-group-btn>.btn{position:relative}.input-group-btn>.btn+.btn{margin-left:-1px}.input-group-btn>.btn:active,.input-group-btn>.btn:focus,.input-group-btn>.btn:hover{z-index:2}.input-group-btn:first-child>.btn,.input-group-btn:first-child>.btn-group{margin-right:-1px}.input-group-btn:last-child>.btn,.input-group-btn:last-child>.btn-group{z-index:2;margin-left:-1px}.nav{padding-left:0;margin-bottom:0;list-style:none}.nav>li{position:relative;display:block}.nav>li>a{position:relative;display:block;padding:10px 15px}.nav>li>a:focus,.nav>li>a:hover{text-decoration:none;background-color:#eee}.nav>li.disabled>a{color:#777}.nav>li.disabled>a:focus,.nav>li.disabled>a:hover{color:#777;text-decoration:none;cursor:not-allowed;background-color:transparent}.nav .open>a,.nav .open>a:focus,.nav .open>a:hover{background-color:#eee;border-color:#337ab7}.nav .nav-divider{height:1px;margin:9px 0;overflow:hidden;background-color:#e5e5e5}.nav>li>a>img{max-width:none}.nav-tabs{border-bottom:1px solid #ddd}.nav-tabs>li{float:left;margin-bottom:-1px}.nav-tabs>li>a{margin-right:2px;line-height:1.42857143;border:1px solid transparent;border-radius:4px 4px 0 0}.nav-tabs>li>a:hover{border-color:#eee #eee #ddd}.nav-tabs>li.active>a,.nav-tabs>li.active>a:focus,.nav-tabs>li.active>a:hover{color:#555;cursor:default;background-color:#fff;border:1px solid #ddd;border-bottom-color:transparent}.nav-tabs.nav-justified{width:100%;border-bottom:0}.nav-tabs.nav-justified>li{float:none}.nav-tabs.nav-justified>li>a{margin-bottom:5px;text-align:center}.nav-tabs.nav-justified>.dropdown .dropdown-menu{top:auto;left:auto}@media (min-width:768px){.nav-tabs.nav-justified>li{display:table-cell;width:1%}.nav-tabs.nav-justified>li>a{margin-bottom:0}}.nav-tabs.nav-justified>li>a{margin-right:0;border-radius:4px}.nav-tabs.nav-justified>.active>a,.nav-tabs.nav-justified>.active>a:focus,.nav-tabs.nav-justified>.active>a:hover{border:1px solid #ddd}@media (min-width:768px){.nav-tabs.nav-justified>li>a{border-bottom:1px solid #ddd;border-radius:4px 4px 0 0}.nav-tabs.nav-justified>.active>a,.nav-tabs.nav-justified>.active>a:focus,.nav-tabs.nav-justified>.active>a:hover{border-bottom-color:#fff}}.nav-pills>li{float:left}.nav-pills>li>a{border-radius:4px}.nav-pills>li+li{margin-left:2px}.nav-pills>li.active>a,.nav-pills>li.active>a:focus,.nav-pills>li.active>a:hover{color:#fff;background-color:#337ab7}.nav-stacked>li{float:none}.nav-stacked>li+li{margin-top:2px;margin-left:0}.nav-justified{width:100%}.nav-justified>li{float:none}.nav-justified>li>a{margin-bottom:5px;text-align:center}.nav-justified>.dropdown .dropdown-menu{top:auto;left:auto}@media (min-width:768px){.nav-justified>li{display:table-cell;width:1%}.nav-justified>li>a{margin-bottom:0}}.nav-tabs-justified{border-bottom:0}.nav-tabs-justified>li>a{margin-right:0;border-radius:4px}.nav-tabs-justified>.active>a,.nav-tabs-justified>.active>a:focus,.nav-tabs-justified>.active>a:hover{border:1px solid #ddd}@media (min-width:768px){.nav-tabs-justified>li>a{border-bottom:1px solid #ddd;border-radius:4px 4px 0 0}.nav-tabs-justified>.active>a,.nav-tabs-justified>.active>a:focus,.nav-tabs-justified>.active>a:hover{border-bottom-color:#fff}}.tab-content>.tab-pane{display:none}.tab-content>.active{display:block}.nav-tabs .dropdown-menu{margin-top:-1px;border-top-left-radius:0;border-top-right-radius:0}.navbar{position:relative;min-height:50px;margin-bottom:20px;border:1px solid transparent}@media (min-width:768px){.navbar{border-radius:4px}}@media (min-width:768px){.navbar-header{float:left}}.navbar-collapse{padding-right:15px;padding-left:15px;overflow-x:visible;border-top:1px solid transparent;-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,.1);box-shadow:inset 0 1px 0 rgba(255,255,255,.1);-webkit-overflow-scrolling:touch}.navbar-collapse.in{overflow-y:auto}@media (min-width:768px){.navbar-collapse{width:auto;border-top:0;-webkit-box-shadow:none;box-shadow:none}.navbar-collapse.collapse{display:block!important;height:auto!important;padding-bottom:0;overflow:visible!important}.navbar-collapse.in{overflow-y:visible}.navbar-fixed-bottom .navbar-collapse,.navbar-fixed-top .navbar-collapse,.navbar-static-top .navbar-collapse{padding-right:0;padding-left:0}}.navbar-fixed-bottom,.navbar-fixed-top{position:fixed;right:0;left:0;z-index:1030}.navbar-fixed-bottom .navbar-collapse,.navbar-fixed-top .navbar-collapse{max-height:340px}@media (max-device-width:480px) and (orientation:landscape){.navbar-fixed-bottom .navbar-collapse,.navbar-fixed-top .navbar-collapse{max-height:200px}}@media (min-width:768px){.navbar-fixed-bottom,.navbar-fixed-top{border-radius:0}}.navbar-fixed-top{top:0;border-width:0 0 1px}.navbar-fixed-bottom{bottom:0;margin-bottom:0;border-width:1px 0 0}.container-fluid>.navbar-collapse,.container-fluid>.navbar-header,.container>.navbar-collapse,.container>.navbar-header{margin-right:-15px;margin-left:-15px}@media (min-width:768px){.container-fluid>.navbar-collapse,.container-fluid>.navbar-header,.container>.navbar-collapse,.container>.navbar-header{margin-right:0;margin-left:0}}.navbar-static-top{z-index:1000;border-width:0 0 1px}@media (min-width:768px){.navbar-static-top{border-radius:0}}.navbar-brand{float:left;height:50px;padding:15px 15px;font-size:18px;line-height:20px}.navbar-brand:focus,.navbar-brand:hover{text-decoration:none}.navbar-brand>img{display:block}@media (min-width:768px){.navbar>.container .navbar-brand,.navbar>.container-fluid .navbar-brand{margin-left:-15px}}.navbar-toggle{position:relative;float:right;padding:9px 10px;margin-right:15px;margin-top:8px;margin-bottom:8px;background-color:transparent;background-image:none;border:1px solid transparent;border-radius:4px}.navbar-toggle:focus{outline:0}.navbar-toggle .icon-bar{display:block;width:22px;height:2px;border-radius:1px}.navbar-toggle .icon-bar+.icon-bar{margin-top:4px}@media (min-width:768px){.navbar-toggle{display:none}}.navbar-nav{margin:7.5px -15px}.navbar-nav>li>a{padding-top:10px;padding-bottom:10px;line-height:20px}@media (max-width:767px){.navbar-nav .open .dropdown-menu{position:static;float:none;width:auto;margin-top:0;background-color:transparent;border:0;-webkit-box-shadow:none;box-shadow:none}.navbar-nav .open .dropdown-menu .dropdown-header,.navbar-nav .open .dropdown-menu>li>a{padding:5px 15px 5px 25px}.navbar-nav .open .dropdown-menu>li>a{line-height:20px}.navbar-nav .open .dropdown-menu>li>a:focus,.navbar-nav .open .dropdown-menu>li>a:hover{background-image:none}}@media (min-width:768px){.navbar-nav{float:left;margin:0}.navbar-nav>li{float:left}.navbar-nav>li>a{padding-top:15px;padding-bottom:15px}}.navbar-form{padding:10px 15px;margin-right:-15px;margin-left:-15px;border-top:1px solid transparent;border-bottom:1px solid transparent;-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,.1),0 1px 0 rgba(255,255,255,.1);box-shadow:inset 0 1px 0 rgba(255,255,255,.1),0 1px 0 rgba(255,255,255,.1);margin-top:8px;margin-bottom:8px}@media (min-width:768px){.navbar-form .form-group{display:inline-block;margin-bottom:0;vertical-align:middle}.navbar-form .form-control{display:inline-block;width:auto;vertical-align:middle}.navbar-form .form-control-static{display:inline-block}.navbar-form .input-group{display:inline-table;vertical-align:middle}.navbar-form .input-group .form-control,.navbar-form .input-group .input-group-addon,.navbar-form .input-group .input-group-btn{width:auto}.navbar-form .input-group>.form-control{width:100%}.navbar-form .control-label{margin-bottom:0;vertical-align:middle}.navbar-form .checkbox,.navbar-form .radio{display:inline-block;margin-top:0;margin-bottom:0;vertical-align:middle}.navbar-form .checkbox label,.navbar-form .radio label{padding-left:0}.navbar-form .checkbox input[type=checkbox],.navbar-form .radio input[type=radio]{position:relative;margin-left:0}.navbar-form .has-feedback .form-control-feedback{top:0}}@media (max-width:767px){.navbar-form .form-group{margin-bottom:5px}.navbar-form .form-group:last-child{margin-bottom:0}}@media (min-width:768px){.navbar-form{width:auto;padding-top:0;padding-bottom:0;margin-right:0;margin-left:0;border:0;-webkit-box-shadow:none;box-shadow:none}}.navbar-nav>li>.dropdown-menu{margin-top:0;border-top-left-radius:0;border-top-right-radius:0}.navbar-fixed-bottom .navbar-nav>li>.dropdown-menu{margin-bottom:0;border-top-left-radius:4px;border-top-right-radius:4px;border-bottom-right-radius:0;border-bottom-left-radius:0}.navbar-btn{margin-top:8px;margin-bottom:8px}.navbar-btn.btn-sm{margin-top:10px;margin-bottom:10px}.navbar-btn.btn-xs{margin-top:14px;margin-bottom:14px}.navbar-text{margin-top:15px;margin-bottom:15px}@media (min-width:768px){.navbar-text{float:left;margin-right:15px;margin-left:15px}}@media (min-width:768px){.navbar-left{float:left!important}.navbar-right{float:right!important;margin-right:-15px}.navbar-right~.navbar-right{margin-right:0}}.navbar-default{background-color:#f8f8f8;border-color:#e7e7e7}.navbar-default .navbar-brand{color:#777}.navbar-default .navbar-brand:focus,.navbar-default .navbar-brand:hover{color:#5e5e5e;background-color:transparent}.navbar-default .navbar-text{color:#777}.navbar-default .navbar-nav>li>a{color:#777}.navbar-default .navbar-nav>li>a:focus,.navbar-default .navbar-nav>li>a:hover{color:#333;background-color:transparent}.navbar-default .navbar-nav>.active>a,.navbar-default .navbar-nav>.active>a:focus,.navbar-default .navbar-nav>.active>a:hover{color:#555;background-color:#e7e7e7}.navbar-default .navbar-nav>.disabled>a,.navbar-default .navbar-nav>.disabled>a:focus,.navbar-default .navbar-nav>.disabled>a:hover{color:#ccc;background-color:transparent}.navbar-default .navbar-nav>.open>a,.navbar-default .navbar-nav>.open>a:focus,.navbar-default .navbar-nav>.open>a:hover{color:#555;background-color:#e7e7e7}@media (max-width:767px){.navbar-default .navbar-nav .open .dropdown-menu>li>a{color:#777}.navbar-default .navbar-nav .open .dropdown-menu>li>a:focus,.navbar-default .navbar-nav .open .dropdown-menu>li>a:hover{color:#333;background-color:transparent}.navbar-default .navbar-nav .open .dropdown-menu>.active>a,.navbar-default .navbar-nav .open .dropdown-menu>.active>a:focus,.navbar-default .navbar-nav .open .dropdown-menu>.active>a:hover{color:#555;background-color:#e7e7e7}.navbar-default .navbar-nav .open .dropdown-menu>.disabled>a,.navbar-default .navbar-nav .open .dropdown-menu>.disabled>a:focus,.navbar-default .navbar-nav .open .dropdown-menu>.disabled>a:hover{color:#ccc;background-color:transparent}}.navbar-default .navbar-toggle{border-color:#ddd}.navbar-default .navbar-toggle:focus,.navbar-default .navbar-toggle:hover{background-color:#ddd}.navbar-default .navbar-toggle .icon-bar{background-color:#888}.navbar-default .navbar-collapse,.navbar-default .navbar-form{border-color:#e7e7e7}.navbar-default .navbar-link{color:#777}.navbar-default .navbar-link:hover{color:#333}.navbar-default .btn-link{color:#777}.navbar-default .btn-link:focus,.navbar-default .btn-link:hover{color:#333}.navbar-default .btn-link[disabled]:focus,.navbar-default .btn-link[disabled]:hover,fieldset[disabled] .navbar-default .btn-link:focus,fieldset[disabled] .navbar-default .btn-link:hover{color:#ccc}.navbar-inverse{background-color:#222;border-color:#080808}.navbar-inverse .navbar-brand{color:#9d9d9d}.navbar-inverse .navbar-brand:focus,.navbar-inverse .navbar-brand:hover{color:#fff;background-color:transparent}.navbar-inverse .navbar-text{color:#9d9d9d}.navbar-inverse .navbar-nav>li>a{color:#9d9d9d}.navbar-inverse .navbar-nav>li>a:focus,.navbar-inverse .navbar-nav>li>a:hover{color:#fff;background-color:transparent}.navbar-inverse .navbar-nav>.active>a,.navbar-inverse .navbar-nav>.active>a:focus,.navbar-inverse .navbar-nav>.active>a:hover{color:#fff;background-color:#080808}.navbar-inverse .navbar-nav>.disabled>a,.navbar-inverse .navbar-nav>.disabled>a:focus,.navbar-inverse .navbar-nav>.disabled>a:hover{color:#444;background-color:transparent}.navbar-inverse .navbar-nav>.open>a,.navbar-inverse .navbar-nav>.open>a:focus,.navbar-inverse .navbar-nav>.open>a:hover{color:#fff;background-color:#080808}@media (max-width:767px){.navbar-inverse .navbar-nav .open .dropdown-menu>.dropdown-header{border-color:#080808}.navbar-inverse .navbar-nav .open .dropdown-menu .divider{background-color:#080808}.navbar-inverse .navbar-nav .open .dropdown-menu>li>a{color:#9d9d9d}.navbar-inverse .navbar-nav .open .dropdown-menu>li>a:focus,.navbar-inverse .navbar-nav .open .dropdown-menu>li>a:hover{color:#fff;background-color:transparent}.navbar-inverse .navbar-nav .open .dropdown-menu>.active>a,.navbar-inverse .navbar-nav .open .dropdown-menu>.active>a:focus,.navbar-inverse .navbar-nav .open .dropdown-menu>.active>a:hover{color:#fff;background-color:#080808}.navbar-inverse .navbar-nav .open .dropdown-menu>.disabled>a,.navbar-inverse .navbar-nav .open .dropdown-menu>.disabled>a:focus,.navbar-inverse .navbar-nav .open .dropdown-menu>.disabled>a:hover{color:#444;background-color:transparent}}.navbar-inverse .navbar-toggle{border-color:#333}.navbar-inverse .navbar-toggle:focus,.navbar-inverse .navbar-toggle:hover{background-color:#333}.navbar-inverse .navbar-toggle .icon-bar{background-color:#fff}.navbar-inverse .navbar-collapse,.navbar-inverse .navbar-form{border-color:#101010}.navbar-inverse .navbar-link{color:#9d9d9d}.navbar-inverse .navbar-link:hover{color:#fff}.navbar-inverse .btn-link{color:#9d9d9d}.navbar-inverse .btn-link:focus,.navbar-inverse .btn-link:hover{color:#fff}.navbar-inverse .btn-link[disabled]:focus,.navbar-inverse .btn-link[disabled]:hover,fieldset[disabled] .navbar-inverse .btn-link:focus,fieldset[disabled] .navbar-inverse .btn-link:hover{color:#444}.breadcrumb{padding:8px 15px;margin-bottom:20px;list-style:none;background-color:#f5f5f5;border-radius:4px}.breadcrumb>li{display:inline-block}.breadcrumb>li+li:before{padding:0 5px;color:#ccc;content:"/\00a0"}.breadcrumb>.active{color:#777}.pagination{display:inline-block;padding-left:0;margin:20px 0;border-radius:4px}.pagination>li{display:inline}.pagination>li>a,.pagination>li>span{position:relative;float:left;padding:6px 12px;margin-left:-1px;line-height:1.42857143;color:#337ab7;text-decoration:none;background-color:#fff;border:1px solid #ddd}.pagination>li>a:focus,.pagination>li>a:hover,.pagination>li>span:focus,.pagination>li>span:hover{z-index:2;color:#23527c;background-color:#eee;border-color:#ddd}.pagination>li:first-child>a,.pagination>li:first-child>span{margin-left:0;border-top-left-radius:4px;border-bottom-left-radius:4px}.pagination>li:last-child>a,.pagination>li:last-child>span{border-top-right-radius:4px;border-bottom-right-radius:4px}.pagination>.active>a,.pagination>.active>a:focus,.pagination>.active>a:hover,.pagination>.active>span,.pagination>.active>span:focus,.pagination>.active>span:hover{z-index:3;color:#fff;cursor:default;background-color:#337ab7;border-color:#337ab7}.pagination>.disabled>a,.pagination>.disabled>a:focus,.pagination>.disabled>a:hover,.pagination>.disabled>span,.pagination>.disabled>span:focus,.pagination>.disabled>span:hover{color:#777;cursor:not-allowed;background-color:#fff;border-color:#ddd}.pagination-lg>li>a,.pagination-lg>li>span{padding:10px 16px;font-size:18px;line-height:1.3333333}.pagination-lg>li:first-child>a,.pagination-lg>li:first-child>span{border-top-left-radius:6px;border-bottom-left-radius:6px}.pagination-lg>li:last-child>a,.pagination-lg>li:last-child>span{border-top-right-radius:6px;border-bottom-right-radius:6px}.pagination-sm>li>a,.pagination-sm>li>span{padding:5px 10px;font-size:12px;line-height:1.5}.pagination-sm>li:first-child>a,.pagination-sm>li:first-child>span{border-top-left-radius:3px;border-bottom-left-radius:3px}.pagination-sm>li:last-child>a,.pagination-sm>li:last-child>span{border-top-right-radius:3px;border-bottom-right-radius:3px}.pager{padding-left:0;margin:20px 0;text-align:center;list-style:none}.pager li{display:inline}.pager li>a,.pager li>span{display:inline-block;padding:5px 14px;background-color:#fff;border:1px solid #ddd;border-radius:15px}.pager li>a:focus,.pager li>a:hover{text-decoration:none;background-color:#eee}.pager .next>a,.pager .next>span{float:right}.pager .previous>a,.pager .previous>span{float:left}.pager .disabled>a,.pager .disabled>a:focus,.pager .disabled>a:hover,.pager .disabled>span{color:#777;cursor:not-allowed;background-color:#fff}.label{display:inline;padding:.2em .6em .3em;font-size:75%;font-weight:700;line-height:1;color:#fff;text-align:center;white-space:nowrap;vertical-align:baseline;border-radius:.25em}a.label:focus,a.label:hover{color:#fff;text-decoration:none;cursor:pointer}.label:empty{display:none}.btn .label{position:relative;top:-1px}.label-default{background-color:#777}.label-default[href]:focus,.label-default[href]:hover{background-color:#5e5e5e}.label-primary{background-color:#337ab7}.label-primary[href]:focus,.label-primary[href]:hover{background-color:#286090}.label-success{background-color:#5cb85c}.label-success[href]:focus,.label-success[href]:hover{background-color:#449d44}.label-info{background-color:#5bc0de}.label-info[href]:focus,.label-info[href]:hover{background-color:#31b0d5}.label-warning{background-color:#f0ad4e}.label-warning[href]:focus,.label-warning[href]:hover{background-color:#ec971f}.label-danger{background-color:#d9534f}.label-danger[href]:focus,.label-danger[href]:hover{background-color:#c9302c}.badge{display:inline-block;min-width:10px;padding:3px 7px;font-size:12px;font-weight:700;line-height:1;color:#fff;text-align:center;white-space:nowrap;vertical-align:middle;background-color:#777;border-radius:10px}.badge:empty{display:none}.btn .badge{position:relative;top:-1px}.btn-group-xs>.btn .badge,.btn-xs .badge{top:0;padding:1px 5px}a.badge:focus,a.badge:hover{color:#fff;text-decoration:none;cursor:pointer}.list-group-item.active>.badge,.nav-pills>.active>a>.badge{color:#337ab7;background-color:#fff}.list-group-item>.badge{float:right}.list-group-item>.badge+.badge{margin-right:5px}.nav-pills>li>a>.badge{margin-left:3px}.jumbotron{padding-top:30px;padding-bottom:30px;margin-bottom:30px;color:inherit;background-color:#eee}.jumbotron .h1,.jumbotron h1{color:inherit}.jumbotron p{margin-bottom:15px;font-size:21px;font-weight:200}.jumbotron>hr{border-top-color:#d5d5d5}.container .jumbotron,.container-fluid .jumbotron{padding-right:15px;padding-left:15px;border-radius:6px}.jumbotron .container{max-width:100%}@media screen and (min-width:768px){.jumbotron{padding-top:48px;padding-bottom:48px}.container .jumbotron,.container-fluid .jumbotron{padding-right:60px;padding-left:60px}.jumbotron .h1,.jumbotron h1{font-size:63px}}.thumbnail{display:block;padding:4px;margin-bottom:20px;line-height:1.42857143;background-color:#fff;border:1px solid #ddd;border-radius:4px;-webkit-transition:border .2s ease-in-out;-o-transition:border .2s ease-in-out;transition:border .2s ease-in-out}.thumbnail a>img,.thumbnail>img{margin-right:auto;margin-left:auto}a.thumbnail.active,a.thumbnail:focus,a.thumbnail:hover{border-color:#337ab7}.thumbnail .caption{padding:9px;color:#333}.alert{padding:15px;margin-bottom:20px;border:1px solid transparent;border-radius:4px}.alert h4{margin-top:0;color:inherit}.alert .alert-link{font-weight:700}.alert>p,.alert>ul{margin-bottom:0}.alert>p+p{margin-top:5px}.alert-dismissable,.alert-dismissible{padding-right:35px}.alert-dismissable .close,.alert-dismissible .close{position:relative;top:-2px;right:-21px;color:inherit}.alert-success{color:#3c763d;background-color:#dff0d8;border-color:#d6e9c6}.alert-success hr{border-top-color:#c9e2b3}.alert-success .alert-link{color:#2b542c}.alert-info{color:#31708f;background-color:#d9edf7;border-color:#bce8f1}.alert-info hr{border-top-color:#a6e1ec}.alert-info .alert-link{color:#245269}.alert-warning{color:#8a6d3b;background-color:#fcf8e3;border-color:#faebcc}.alert-warning hr{border-top-color:#f7e1b5}.alert-warning .alert-link{color:#66512c}.alert-danger{color:#a94442;background-color:#f2dede;border-color:#ebccd1}.alert-danger hr{border-top-color:#e4b9c0}.alert-danger .alert-link{color:#843534}@-webkit-keyframes progress-bar-stripes{from{background-position:40px 0}to{background-position:0 0}}@-o-keyframes progress-bar-stripes{from{background-position:40px 0}to{background-position:0 0}}@keyframes progress-bar-stripes{from{background-position:40px 0}to{background-position:0 0}}.progress{height:20px;margin-bottom:20px;overflow:hidden;background-color:#f5f5f5;border-radius:4px;-webkit-box-shadow:inset 0 1px 2px rgba(0,0,0,.1);box-shadow:inset 0 1px 2px rgba(0,0,0,.1)}.progress-bar{float:left;width:0%;height:100%;font-size:12px;line-height:20px;color:#fff;text-align:center;background-color:#337ab7;-webkit-box-shadow:inset 0 -1px 0 rgba(0,0,0,.15);box-shadow:inset 0 -1px 0 rgba(0,0,0,.15);-webkit-transition:width .6s ease;-o-transition:width .6s ease;transition:width .6s ease}.progress-bar-striped,.progress-striped .progress-bar{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);-webkit-background-size:40px 40px;background-size:40px 40px}.progress-bar.active,.progress.active .progress-bar{-webkit-animation:progress-bar-stripes 2s linear infinite;-o-animation:progress-bar-stripes 2s linear infinite;animation:progress-bar-stripes 2s linear infinite}.progress-bar-success{background-color:#5cb85c}.progress-striped .progress-bar-success{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent)}.progress-bar-info{background-color:#5bc0de}.progress-striped .progress-bar-info{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent)}.progress-bar-warning{background-color:#f0ad4e}.progress-striped .progress-bar-warning{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent)}.progress-bar-danger{background-color:#d9534f}.progress-striped .progress-bar-danger{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent)}.media{margin-top:15px}.media:first-child{margin-top:0}.media,.media-body{overflow:hidden;zoom:1}.media-body{width:10000px}.media-object{display:block}.media-object.img-thumbnail{max-width:none}.media-right,.media>.pull-right{padding-left:10px}.media-left,.media>.pull-left{padding-right:10px}.media-body,.media-left,.media-right{display:table-cell;vertical-align:top}.media-middle{vertical-align:middle}.media-bottom{vertical-align:bottom}.media-heading{margin-top:0;margin-bottom:5px}.media-list{padding-left:0;list-style:none}.list-group{padding-left:0;margin-bottom:20px}.list-group-item{position:relative;display:block;padding:10px 15px;margin-bottom:-1px;background-color:#fff;border:1px solid #ddd}.list-group-item:first-child{border-top-left-radius:4px;border-top-right-radius:4px}.list-group-item:last-child{margin-bottom:0;border-bottom-right-radius:4px;border-bottom-left-radius:4px}.list-group-item.disabled,.list-group-item.disabled:focus,.list-group-item.disabled:hover{color:#777;cursor:not-allowed;background-color:#eee}.list-group-item.disabled .list-group-item-heading,.list-group-item.disabled:focus .list-group-item-heading,.list-group-item.disabled:hover .list-group-item-heading{color:inherit}.list-group-item.disabled .list-group-item-text,.list-group-item.disabled:focus .list-group-item-text,.list-group-item.disabled:hover .list-group-item-text{color:#777}.list-group-item.active,.list-group-item.active:focus,.list-group-item.active:hover{z-index:2;color:#fff;background-color:#337ab7;border-color:#337ab7}.list-group-item.active .list-group-item-heading,.list-group-item.active .list-group-item-heading>.small,.list-group-item.active .list-group-item-heading>small,.list-group-item.active:focus .list-group-item-heading,.list-group-item.active:focus .list-group-item-heading>.small,.list-group-item.active:focus .list-group-item-heading>small,.list-group-item.active:hover .list-group-item-heading,.list-group-item.active:hover .list-group-item-heading>.small,.list-group-item.active:hover .list-group-item-heading>small{color:inherit}.list-group-item.active .list-group-item-text,.list-group-item.active:focus .list-group-item-text,.list-group-item.active:hover .list-group-item-text{color:#c7ddef}a.list-group-item,button.list-group-item{color:#555}a.list-group-item .list-group-item-heading,button.list-group-item .list-group-item-heading{color:#333}a.list-group-item:focus,a.list-group-item:hover,button.list-group-item:focus,button.list-group-item:hover{color:#555;text-decoration:none;background-color:#f5f5f5}button.list-group-item{width:100%;text-align:left}.list-group-item-success{color:#3c763d;background-color:#dff0d8}a.list-group-item-success,button.list-group-item-success{color:#3c763d}a.list-group-item-success .list-group-item-heading,button.list-group-item-success .list-group-item-heading{color:inherit}a.list-group-item-success:focus,a.list-group-item-success:hover,button.list-group-item-success:focus,button.list-group-item-success:hover{color:#3c763d;background-color:#d0e9c6}a.list-group-item-success.active,a.list-group-item-success.active:focus,a.list-group-item-success.active:hover,button.list-group-item-success.active,button.list-group-item-success.active:focus,button.list-group-item-success.active:hover{color:#fff;background-color:#3c763d;border-color:#3c763d}.list-group-item-info{color:#31708f;background-color:#d9edf7}a.list-group-item-info,button.list-group-item-info{color:#31708f}a.list-group-item-info .list-group-item-heading,button.list-group-item-info .list-group-item-heading{color:inherit}a.list-group-item-info:focus,a.list-group-item-info:hover,button.list-group-item-info:focus,button.list-group-item-info:hover{color:#31708f;background-color:#c4e3f3}a.list-group-item-info.active,a.list-group-item-info.active:focus,a.list-group-item-info.active:hover,button.list-group-item-info.active,button.list-group-item-info.active:focus,button.list-group-item-info.active:hover{color:#fff;background-color:#31708f;border-color:#31708f}.list-group-item-warning{color:#8a6d3b;background-color:#fcf8e3}a.list-group-item-warning,button.list-group-item-warning{color:#8a6d3b}a.list-group-item-warning .list-group-item-heading,button.list-group-item-warning .list-group-item-heading{color:inherit}a.list-group-item-warning:focus,a.list-group-item-warning:hover,button.list-group-item-warning:focus,button.list-group-item-warning:hover{color:#8a6d3b;background-color:#faf2cc}a.list-group-item-warning.active,a.list-group-item-warning.active:focus,a.list-group-item-warning.active:hover,button.list-group-item-warning.active,button.list-group-item-warning.active:focus,button.list-group-item-warning.active:hover{color:#fff;background-color:#8a6d3b;border-color:#8a6d3b}.list-group-item-danger{color:#a94442;background-color:#f2dede}a.list-group-item-danger,button.list-group-item-danger{color:#a94442}a.list-group-item-danger .list-group-item-heading,button.list-group-item-danger .list-group-item-heading{color:inherit}a.list-group-item-danger:focus,a.list-group-item-danger:hover,button.list-group-item-danger:focus,button.list-group-item-danger:hover{color:#a94442;background-color:#ebcccc}a.list-group-item-danger.active,a.list-group-item-danger.active:focus,a.list-group-item-danger.active:hover,button.list-group-item-danger.active,button.list-group-item-danger.active:focus,button.list-group-item-danger.active:hover{color:#fff;background-color:#a94442;border-color:#a94442}.list-group-item-heading{margin-top:0;margin-bottom:5px}.list-group-item-text{margin-bottom:0;line-height:1.3}.panel{margin-bottom:20px;background-color:#fff;border:1px solid transparent;border-radius:4px;-webkit-box-shadow:0 1px 1px rgba(0,0,0,.05);box-shadow:0 1px 1px rgba(0,0,0,.05)}.panel-body{padding:15px}.panel-heading{padding:10px 15px;border-bottom:1px solid transparent;border-top-left-radius:3px;border-top-right-radius:3px}.panel-heading>.dropdown .dropdown-toggle{color:inherit}.panel-title{margin-top:0;margin-bottom:0;font-size:16px;color:inherit}.panel-title>.small,.panel-title>.small>a,.panel-title>a,.panel-title>small,.panel-title>small>a{color:inherit}.panel-footer{padding:10px 15px;background-color:#f5f5f5;border-top:1px solid #ddd;border-bottom-right-radius:3px;border-bottom-left-radius:3px}.panel>.list-group,.panel>.panel-collapse>.list-group{margin-bottom:0}.panel>.list-group .list-group-item,.panel>.panel-collapse>.list-group .list-group-item{border-width:1px 0;border-radius:0}.panel>.list-group:first-child .list-group-item:first-child,.panel>.panel-collapse>.list-group:first-child .list-group-item:first-child{border-top:0;border-top-left-radius:3px;border-top-right-radius:3px}.panel>.list-group:last-child .list-group-item:last-child,.panel>.panel-collapse>.list-group:last-child .list-group-item:last-child{border-bottom:0;border-bottom-right-radius:3px;border-bottom-left-radius:3px}.panel>.panel-heading+.panel-collapse>.list-group .list-group-item:first-child{border-top-left-radius:0;border-top-right-radius:0}.panel-heading+.list-group .list-group-item:first-child{border-top-width:0}.list-group+.panel-footer{border-top-width:0}.panel>.panel-collapse>.table,.panel>.table,.panel>.table-responsive>.table{margin-bottom:0}.panel>.panel-collapse>.table caption,.panel>.table caption,.panel>.table-responsive>.table caption{padding-right:15px;padding-left:15px}.panel>.table-responsive:first-child>.table:first-child,.panel>.table:first-child{border-top-left-radius:3px;border-top-right-radius:3px}.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child,.panel>.table:first-child>tbody:first-child>tr:first-child,.panel>.table:first-child>thead:first-child>tr:first-child{border-top-left-radius:3px;border-top-right-radius:3px}.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child td:first-child,.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child th:first-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child td:first-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child th:first-child,.panel>.table:first-child>tbody:first-child>tr:first-child td:first-child,.panel>.table:first-child>tbody:first-child>tr:first-child th:first-child,.panel>.table:first-child>thead:first-child>tr:first-child td:first-child,.panel>.table:first-child>thead:first-child>tr:first-child th:first-child{border-top-left-radius:3px}.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child td:last-child,.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child th:last-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child td:last-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child th:last-child,.panel>.table:first-child>tbody:first-child>tr:first-child td:last-child,.panel>.table:first-child>tbody:first-child>tr:first-child th:last-child,.panel>.table:first-child>thead:first-child>tr:first-child td:last-child,.panel>.table:first-child>thead:first-child>tr:first-child th:last-child{border-top-right-radius:3px}.panel>.table-responsive:last-child>.table:last-child,.panel>.table:last-child{border-bottom-right-radius:3px;border-bottom-left-radius:3px}.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child,.panel>.table:last-child>tbody:last-child>tr:last-child,.panel>.table:last-child>tfoot:last-child>tr:last-child{border-bottom-right-radius:3px;border-bottom-left-radius:3px}.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child td:first-child,.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child th:first-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child td:first-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child th:first-child,.panel>.table:last-child>tbody:last-child>tr:last-child td:first-child,.panel>.table:last-child>tbody:last-child>tr:last-child th:first-child,.panel>.table:last-child>tfoot:last-child>tr:last-child td:first-child,.panel>.table:last-child>tfoot:last-child>tr:last-child th:first-child{border-bottom-left-radius:3px}.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child td:last-child,.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child th:last-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child td:last-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child th:last-child,.panel>.table:last-child>tbody:last-child>tr:last-child td:last-child,.panel>.table:last-child>tbody:last-child>tr:last-child th:last-child,.panel>.table:last-child>tfoot:last-child>tr:last-child td:last-child,.panel>.table:last-child>tfoot:last-child>tr:last-child th:last-child{border-bottom-right-radius:3px}.panel>.panel-body+.table,.panel>.panel-body+.table-responsive,.panel>.table+.panel-body,.panel>.table-responsive+.panel-body{border-top:1px solid #ddd}.panel>.table>tbody:first-child>tr:first-child td,.panel>.table>tbody:first-child>tr:first-child th{border-top:0}.panel>.table-bordered,.panel>.table-responsive>.table-bordered{border:0}.panel>.table-bordered>tbody>tr>td:first-child,.panel>.table-bordered>tbody>tr>th:first-child,.panel>.table-bordered>tfoot>tr>td:first-child,.panel>.table-bordered>tfoot>tr>th:first-child,.panel>.table-bordered>thead>tr>td:first-child,.panel>.table-bordered>thead>tr>th:first-child,.panel>.table-responsive>.table-bordered>tbody>tr>td:first-child,.panel>.table-responsive>.table-bordered>tbody>tr>th:first-child,.panel>.table-responsive>.table-bordered>tfoot>tr>td:first-child,.panel>.table-responsive>.table-bordered>tfoot>tr>th:first-child,.panel>.table-responsive>.table-bordered>thead>tr>td:first-child,.panel>.table-responsive>.table-bordered>thead>tr>th:first-child{border-left:0}.panel>.table-bordered>tbody>tr>td:last-child,.panel>.table-bordered>tbody>tr>th:last-child,.panel>.table-bordered>tfoot>tr>td:last-child,.panel>.table-bordered>tfoot>tr>th:last-child,.panel>.table-bordered>thead>tr>td:last-child,.panel>.table-bordered>thead>tr>th:last-child,.panel>.table-responsive>.table-bordered>tbody>tr>td:last-child,.panel>.table-responsive>.table-bordered>tbody>tr>th:last-child,.panel>.table-responsive>.table-bordered>tfoot>tr>td:last-child,.panel>.table-responsive>.table-bordered>tfoot>tr>th:last-child,.panel>.table-responsive>.table-bordered>thead>tr>td:last-child,.panel>.table-responsive>.table-bordered>thead>tr>th:last-child{border-right:0}.panel>.table-bordered>tbody>tr:first-child>td,.panel>.table-bordered>tbody>tr:first-child>th,.panel>.table-bordered>thead>tr:first-child>td,.panel>.table-bordered>thead>tr:first-child>th,.panel>.table-responsive>.table-bordered>tbody>tr:first-child>td,.panel>.table-responsive>.table-bordered>tbody>tr:first-child>th,.panel>.table-responsive>.table-bordered>thead>tr:first-child>td,.panel>.table-responsive>.table-bordered>thead>tr:first-child>th{border-bottom:0}.panel>.table-bordered>tbody>tr:last-child>td,.panel>.table-bordered>tbody>tr:last-child>th,.panel>.table-bordered>tfoot>tr:last-child>td,.panel>.table-bordered>tfoot>tr:last-child>th,.panel>.table-responsive>.table-bordered>tbody>tr:last-child>td,.panel>.table-responsive>.table-bordered>tbody>tr:last-child>th,.panel>.table-responsive>.table-bordered>tfoot>tr:last-child>td,.panel>.table-responsive>.table-bordered>tfoot>tr:last-child>th{border-bottom:0}.panel>.table-responsive{margin-bottom:0;border:0}.panel-group{margin-bottom:20px}.panel-group .panel{margin-bottom:0;border-radius:4px}.panel-group .panel+.panel{margin-top:5px}.panel-group .panel-heading{border-bottom:0}.panel-group .panel-heading+.panel-collapse>.list-group,.panel-group .panel-heading+.panel-collapse>.panel-body{border-top:1px solid #ddd}.panel-group .panel-footer{border-top:0}.panel-group .panel-footer+.panel-collapse .panel-body{border-bottom:1px solid #ddd}.panel-default{border-color:#ddd}.panel-default>.panel-heading{color:#333;background-color:#f5f5f5;border-color:#ddd}.panel-default>.panel-heading+.panel-collapse>.panel-body{border-top-color:#ddd}.panel-default>.panel-heading .badge{color:#f5f5f5;background-color:#333}.panel-default>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#ddd}.panel-primary{border-color:#337ab7}.panel-primary>.panel-heading{color:#fff;background-color:#337ab7;border-color:#337ab7}.panel-primary>.panel-heading+.panel-collapse>.panel-body{border-top-color:#337ab7}.panel-primary>.panel-heading .badge{color:#337ab7;background-color:#fff}.panel-primary>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#337ab7}.panel-success{border-color:#d6e9c6}.panel-success>.panel-heading{color:#3c763d;background-color:#dff0d8;border-color:#d6e9c6}.panel-success>.panel-heading+.panel-collapse>.panel-body{border-top-color:#d6e9c6}.panel-success>.panel-heading .badge{color:#dff0d8;background-color:#3c763d}.panel-success>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#d6e9c6}.panel-info{border-color:#bce8f1}.panel-info>.panel-heading{color:#31708f;background-color:#d9edf7;border-color:#bce8f1}.panel-info>.panel-heading+.panel-collapse>.panel-body{border-top-color:#bce8f1}.panel-info>.panel-heading .badge{color:#d9edf7;background-color:#31708f}.panel-info>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#bce8f1}.panel-warning{border-color:#faebcc}.panel-warning>.panel-heading{color:#8a6d3b;background-color:#fcf8e3;border-color:#faebcc}.panel-warning>.panel-heading+.panel-collapse>.panel-body{border-top-color:#faebcc}.panel-warning>.panel-heading .badge{color:#fcf8e3;background-color:#8a6d3b}.panel-warning>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#faebcc}.panel-danger{border-color:#ebccd1}.panel-danger>.panel-heading{color:#a94442;background-color:#f2dede;border-color:#ebccd1}.panel-danger>.panel-heading+.panel-collapse>.panel-body{border-top-color:#ebccd1}.panel-danger>.panel-heading .badge{color:#f2dede;background-color:#a94442}.panel-danger>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#ebccd1}.embed-responsive{position:relative;display:block;height:0;padding:0;overflow:hidden}.embed-responsive .embed-responsive-item,.embed-responsive embed,.embed-responsive iframe,.embed-responsive object,.embed-responsive video{position:absolute;top:0;bottom:0;left:0;width:100%;height:100%;border:0}.embed-responsive-16by9{padding-bottom:56.25%}.embed-responsive-4by3{padding-bottom:75%}.well{min-height:20px;padding:19px;margin-bottom:20px;background-color:#f5f5f5;border:1px solid #e3e3e3;border-radius:4px;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.05);box-shadow:inset 0 1px 1px rgba(0,0,0,.05)}.well blockquote{border-color:#ddd;border-color:rgba(0,0,0,.15)}.well-lg{padding:24px;border-radius:6px}.well-sm{padding:9px;border-radius:3px}.close{float:right;font-size:21px;font-weight:700;line-height:1;color:#000;text-shadow:0 1px 0 #fff;filter:alpha(opacity=20);opacity:.2}.close:focus,.close:hover{color:#000;text-decoration:none;cursor:pointer;filter:alpha(opacity=50);opacity:.5}button.close{padding:0;cursor:pointer;background:0 0;border:0;-webkit-appearance:none;-moz-appearance:none;appearance:none}.modal-open{overflow:hidden}.modal{position:fixed;top:0;right:0;bottom:0;left:0;z-index:1050;display:none;overflow:hidden;-webkit-overflow-scrolling:touch;outline:0}.modal.fade .modal-dialog{-webkit-transform:translate(0,-25%);-ms-transform:translate(0,-25%);-o-transform:translate(0,-25%);transform:translate(0,-25%);-webkit-transition:-webkit-transform .3s ease-out;-o-transition:-o-transform .3s ease-out;transition:-webkit-transform .3s ease-out;transition:transform .3s ease-out;transition:transform .3s ease-out,-webkit-transform .3s ease-out,-o-transform .3s ease-out}.modal.in .modal-dialog{-webkit-transform:translate(0,0);-ms-transform:translate(0,0);-o-transform:translate(0,0);transform:translate(0,0)}.modal-open .modal{overflow-x:hidden;overflow-y:auto}.modal-dialog{position:relative;width:auto;margin:10px}.modal-content{position:relative;background-color:#fff;background-clip:padding-box;border:1px solid #999;border:1px solid rgba(0,0,0,.2);border-radius:6px;-webkit-box-shadow:0 3px 9px rgba(0,0,0,.5);box-shadow:0 3px 9px rgba(0,0,0,.5);outline:0}.modal-backdrop{position:fixed;top:0;right:0;bottom:0;left:0;z-index:1040;background-color:#000}.modal-backdrop.fade{filter:alpha(opacity=0);opacity:0}.modal-backdrop.in{filter:alpha(opacity=50);opacity:.5}.modal-header{padding:15px;border-bottom:1px solid #e5e5e5}.modal-header .close{margin-top:-2px}.modal-title{margin:0;line-height:1.42857143}.modal-body{position:relative;padding:15px}.modal-footer{padding:15px;text-align:right;border-top:1px solid #e5e5e5}.modal-footer .btn+.btn{margin-bottom:0;margin-left:5px}.modal-footer .btn-group .btn+.btn{margin-left:-1px}.modal-footer .btn-block+.btn-block{margin-left:0}.modal-scrollbar-measure{position:absolute;top:-9999px;width:50px;height:50px;overflow:scroll}@media (min-width:768px){.modal-dialog{width:600px;margin:30px auto}.modal-content{-webkit-box-shadow:0 5px 15px rgba(0,0,0,.5);box-shadow:0 5px 15px rgba(0,0,0,.5)}.modal-sm{width:300px}}@media (min-width:992px){.modal-lg{width:900px}}.tooltip{position:absolute;z-index:1070;display:block;font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;font-style:normal;font-weight:400;line-height:1.42857143;line-break:auto;text-align:left;text-align:start;text-decoration:none;text-shadow:none;text-transform:none;letter-spacing:normal;word-break:normal;word-spacing:normal;word-wrap:normal;white-space:normal;font-size:12px;filter:alpha(opacity=0);opacity:0}.tooltip.in{filter:alpha(opacity=90);opacity:.9}.tooltip.top{padding:5px 0;margin-top:-3px}.tooltip.right{padding:0 5px;margin-left:3px}.tooltip.bottom{padding:5px 0;margin-top:3px}.tooltip.left{padding:0 5px;margin-left:-3px}.tooltip.top .tooltip-arrow{bottom:0;left:50%;margin-left:-5px;border-width:5px 5px 0;border-top-color:#000}.tooltip.top-left .tooltip-arrow{right:5px;bottom:0;margin-bottom:-5px;border-width:5px 5px 0;border-top-color:#000}.tooltip.top-right .tooltip-arrow{bottom:0;left:5px;margin-bottom:-5px;border-width:5px 5px 0;border-top-color:#000}.tooltip.right .tooltip-arrow{top:50%;left:0;margin-top:-5px;border-width:5px 5px 5px 0;border-right-color:#000}.tooltip.left .tooltip-arrow{top:50%;right:0;margin-top:-5px;border-width:5px 0 5px 5px;border-left-color:#000}.tooltip.bottom .tooltip-arrow{top:0;left:50%;margin-left:-5px;border-width:0 5px 5px;border-bottom-color:#000}.tooltip.bottom-left .tooltip-arrow{top:0;right:5px;margin-top:-5px;border-width:0 5px 5px;border-bottom-color:#000}.tooltip.bottom-right .tooltip-arrow{top:0;left:5px;margin-top:-5px;border-width:0 5px 5px;border-bottom-color:#000}.tooltip-inner{max-width:200px;padding:3px 8px;color:#fff;text-align:center;background-color:#000;border-radius:4px}.tooltip-arrow{position:absolute;width:0;height:0;border-color:transparent;border-style:solid}.popover{position:absolute;top:0;left:0;z-index:1060;display:none;max-width:276px;padding:1px;font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;font-style:normal;font-weight:400;line-height:1.42857143;line-break:auto;text-align:left;text-align:start;text-decoration:none;text-shadow:none;text-transform:none;letter-spacing:normal;word-break:normal;word-spacing:normal;word-wrap:normal;white-space:normal;font-size:14px;background-color:#fff;background-clip:padding-box;border:1px solid #ccc;border:1px solid rgba(0,0,0,.2);border-radius:6px;-webkit-box-shadow:0 5px 10px rgba(0,0,0,.2);box-shadow:0 5px 10px rgba(0,0,0,.2)}.popover.top{margin-top:-10px}.popover.right{margin-left:10px}.popover.bottom{margin-top:10px}.popover.left{margin-left:-10px}.popover>.arrow{border-width:11px}.popover>.arrow,.popover>.arrow:after{position:absolute;display:block;width:0;height:0;border-color:transparent;border-style:solid}.popover>.arrow:after{content:"";border-width:10px}.popover.top>.arrow{bottom:-11px;left:50%;margin-left:-11px;border-top-color:#999;border-top-color:rgba(0,0,0,.25);border-bottom-width:0}.popover.top>.arrow:after{bottom:1px;margin-left:-10px;content:" ";border-top-color:#fff;border-bottom-width:0}.popover.right>.arrow{top:50%;left:-11px;margin-top:-11px;border-right-color:#999;border-right-color:rgba(0,0,0,.25);border-left-width:0}.popover.right>.arrow:after{bottom:-10px;left:1px;content:" ";border-right-color:#fff;border-left-width:0}.popover.bottom>.arrow{top:-11px;left:50%;margin-left:-11px;border-top-width:0;border-bottom-color:#999;border-bottom-color:rgba(0,0,0,.25)}.popover.bottom>.arrow:after{top:1px;margin-left:-10px;content:" ";border-top-width:0;border-bottom-color:#fff}.popover.left>.arrow{top:50%;right:-11px;margin-top:-11px;border-right-width:0;border-left-color:#999;border-left-color:rgba(0,0,0,.25)}.popover.left>.arrow:after{right:1px;bottom:-10px;content:" ";border-right-width:0;border-left-color:#fff}.popover-title{padding:8px 14px;margin:0;font-size:14px;background-color:#f7f7f7;border-bottom:1px solid #ebebeb;border-radius:5px 5px 0 0}.popover-content{padding:9px 14px}.carousel{position:relative}.carousel-inner{position:relative;width:100%;overflow:hidden}.carousel-inner>.item{position:relative;display:none;-webkit-transition:.6s ease-in-out left;-o-transition:.6s ease-in-out left;transition:.6s ease-in-out left}.carousel-inner>.item>a>img,.carousel-inner>.item>img{line-height:1}@media all and (transform-3d),(-webkit-transform-3d){.carousel-inner>.item{-webkit-transition:-webkit-transform .6s ease-in-out;-o-transition:-o-transform .6s ease-in-out;transition:-webkit-transform .6s ease-in-out;transition:transform .6s ease-in-out;transition:transform .6s ease-in-out,-webkit-transform .6s ease-in-out,-o-transform .6s ease-in-out;-webkit-backface-visibility:hidden;backface-visibility:hidden;-webkit-perspective:1000px;perspective:1000px}.carousel-inner>.item.active.right,.carousel-inner>.item.next{-webkit-transform:translate3d(100%,0,0);transform:translate3d(100%,0,0);left:0}.carousel-inner>.item.active.left,.carousel-inner>.item.prev{-webkit-transform:translate3d(-100%,0,0);transform:translate3d(-100%,0,0);left:0}.carousel-inner>.item.active,.carousel-inner>.item.next.left,.carousel-inner>.item.prev.right{-webkit-transform:translate3d(0,0,0);transform:translate3d(0,0,0);left:0}}.carousel-inner>.active,.carousel-inner>.next,.carousel-inner>.prev{display:block}.carousel-inner>.active{left:0}.carousel-inner>.next,.carousel-inner>.prev{position:absolute;top:0;width:100%}.carousel-inner>.next{left:100%}.carousel-inner>.prev{left:-100%}.carousel-inner>.next.left,.carousel-inner>.prev.right{left:0}.carousel-inner>.active.left{left:-100%}.carousel-inner>.active.right{left:100%}.carousel-control{position:absolute;top:0;bottom:0;left:0;width:15%;font-size:20px;color:#fff;text-align:center;text-shadow:0 1px 2px rgba(0,0,0,.6);background-color:rgba(0,0,0,0);filter:alpha(opacity=50);opacity:.5}.carousel-control.left{background-image:-webkit-linear-gradient(left,rgba(0,0,0,.5) 0,rgba(0,0,0,.0001) 100%);background-image:-o-linear-gradient(left,rgba(0,0,0,.5) 0,rgba(0,0,0,.0001) 100%);background-image:-webkit-gradient(linear,left top,right top,from(rgba(0,0,0,.5)),to(rgba(0,0,0,.0001)));background-image:linear-gradient(to right,rgba(0,0,0,.5) 0,rgba(0,0,0,.0001) 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#80000000', endColorstr='#00000000', GradientType=1);background-repeat:repeat-x}.carousel-control.right{right:0;left:auto;background-image:-webkit-linear-gradient(left,rgba(0,0,0,.0001) 0,rgba(0,0,0,.5) 100%);background-image:-o-linear-gradient(left,rgba(0,0,0,.0001) 0,rgba(0,0,0,.5) 100%);background-image:-webkit-gradient(linear,left top,right top,from(rgba(0,0,0,.0001)),to(rgba(0,0,0,.5)));background-image:linear-gradient(to right,rgba(0,0,0,.0001) 0,rgba(0,0,0,.5) 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#00000000', endColorstr='#80000000', GradientType=1);background-repeat:repeat-x}.carousel-control:focus,.carousel-control:hover{color:#fff;text-decoration:none;outline:0;filter:alpha(opacity=90);opacity:.9}.carousel-control .glyphicon-chevron-left,.carousel-control .glyphicon-chevron-right,.carousel-control .icon-next,.carousel-control .icon-prev{position:absolute;top:50%;z-index:5;display:inline-block;margin-top:-10px}.carousel-control .glyphicon-chevron-left,.carousel-control .icon-prev{left:50%;margin-left:-10px}.carousel-control .glyphicon-chevron-right,.carousel-control .icon-next{right:50%;margin-right:-10px}.carousel-control .icon-next,.carousel-control .icon-prev{width:20px;height:20px;font-family:serif;line-height:1}.carousel-control .icon-prev:before{content:"\2039"}.carousel-control .icon-next:before{content:"\203a"}.carousel-indicators{position:absolute;bottom:10px;left:50%;z-index:15;width:60%;padding-left:0;margin-left:-30%;text-align:center;list-style:none}.carousel-indicators li{display:inline-block;width:10px;height:10px;margin:1px;text-indent:-999px;cursor:pointer;background-color:#000\9;background-color:rgba(0,0,0,0);border:1px solid #fff;border-radius:10px}.carousel-indicators .active{width:12px;height:12px;margin:0;background-color:#fff}.carousel-caption{position:absolute;right:15%;bottom:20px;left:15%;z-index:10;padding-top:20px;padding-bottom:20px;color:#fff;text-align:center;text-shadow:0 1px 2px rgba(0,0,0,.6)}.carousel-caption .btn{text-shadow:none}@media screen and (min-width:768px){.carousel-control .glyphicon-chevron-left,.carousel-control .glyphicon-chevron-right,.carousel-control .icon-next,.carousel-control .icon-prev{width:30px;height:30px;margin-top:-10px;font-size:30px}.carousel-control .glyphicon-chevron-left,.carousel-control .icon-prev{margin-left:-10px}.carousel-control .glyphicon-chevron-right,.carousel-control .icon-next{margin-right:-10px}.carousel-caption{right:20%;left:20%;padding-bottom:30px}.carousel-indicators{bottom:20px}}.btn-group-vertical>.btn-group:after,.btn-group-vertical>.btn-group:before,.btn-toolbar:after,.btn-toolbar:before,.clearfix:after,.clearfix:before,.container-fluid:after,.container-fluid:before,.container:after,.container:before,.dl-horizontal dd:after,.dl-horizontal dd:before,.form-horizontal .form-group:after,.form-horizontal .form-group:before,.modal-footer:after,.modal-footer:before,.modal-header:after,.modal-header:before,.nav:after,.nav:before,.navbar-collapse:after,.navbar-collapse:before,.navbar-header:after,.navbar-header:before,.navbar:after,.navbar:before,.pager:after,.pager:before,.panel-body:after,.panel-body:before,.row:after,.row:before{display:table;content:" "}.btn-group-vertical>.btn-group:after,.btn-toolbar:after,.clearfix:after,.container-fluid:after,.container:after,.dl-horizontal dd:after,.form-horizontal .form-group:after,.modal-footer:after,.modal-header:after,.nav:after,.navbar-collapse:after,.navbar-header:after,.navbar:after,.pager:after,.panel-body:after,.row:after{clear:both}.center-block{display:block;margin-right:auto;margin-left:auto}.pull-right{float:right!important}.pull-left{float:left!important}.hide{display:none!important}.show{display:block!important}.invisible{visibility:hidden}.text-hide{font:0/0 a;color:transparent;text-shadow:none;background-color:transparent;border:0}.hidden{display:none!important}.affix{position:fixed}@-ms-viewport{width:device-width}.visible-lg,.visible-md,.visible-sm,.visible-xs{display:none!important}.visible-lg-block,.visible-lg-inline,.visible-lg-inline-block,.visible-md-block,.visible-md-inline,.visible-md-inline-block,.visible-sm-block,.visible-sm-inline,.visible-sm-inline-block,.visible-xs-block,.visible-xs-inline,.visible-xs-inline-block{display:none!important}@media (max-width:767px){.visible-xs{display:block!important}table.visible-xs{display:table!important}tr.visible-xs{display:table-row!important}td.visible-xs,th.visible-xs{display:table-cell!important}}@media (max-width:767px){.visible-xs-block{display:block!important}}@media (max-width:767px){.visible-xs-inline{display:inline!important}}@media (max-width:767px){.visible-xs-inline-block{display:inline-block!important}}@media (min-width:768px) and (max-width:991px){.visible-sm{display:block!important}table.visible-sm{display:table!important}tr.visible-sm{display:table-row!important}td.visible-sm,th.visible-sm{display:table-cell!important}}@media (min-width:768px) and (max-width:991px){.visible-sm-block{display:block!important}}@media (min-width:768px) and (max-width:991px){.visible-sm-inline{display:inline!important}}@media (min-width:768px) and (max-width:991px){.visible-sm-inline-block{display:inline-block!important}}@media (min-width:992px) and (max-width:1199px){.visible-md{display:block!important}table.visible-md{display:table!important}tr.visible-md{display:table-row!important}td.visible-md,th.visible-md{display:table-cell!important}}@media (min-width:992px) and (max-width:1199px){.visible-md-block{display:block!important}}@media (min-width:992px) and (max-width:1199px){.visible-md-inline{display:inline!important}}@media (min-width:992px) and (max-width:1199px){.visible-md-inline-block{display:inline-block!important}}@media (min-width:1200px){.visible-lg{display:block!important}table.visible-lg{display:table!important}tr.visible-lg{display:table-row!important}td.visible-lg,th.visible-lg{display:table-cell!important}}@media (min-width:1200px){.visible-lg-block{display:block!important}}@media (min-width:1200px){.visible-lg-inline{display:inline!important}}@media (min-width:1200px){.visible-lg-inline-block{display:inline-block!important}}@media (max-width:767px){.hidden-xs{display:none!important}}@media (min-width:768px) and (max-width:991px){.hidden-sm{display:none!important}}@media (min-width:992px) and (max-width:1199px){.hidden-md{display:none!important}}@media (min-width:1200px){.hidden-lg{display:none!important}}.visible-print{display:none!important}@media print{.visible-print{display:block!important}table.visible-print{display:table!important}tr.visible-print{display:table-row!important}td.visible-print,th.visible-print{display:table-cell!important}}.visible-print-block{display:none!important}@media print{.visible-print-block{display:block!important}}.visible-print-inline{display:none!important}@media print{.visible-print-inline{display:inline!important}}.visible-print-inline-block{display:none!important}@media print{.visible-print-inline-block{display:inline-block!important}}@media print{.hidden-print{display:none!important}} +/*# sourceMappingURL=bootstrap.min.css.map */ \ No newline at end of file diff --git a/src/websrc/3rdparty/css/footable.bootstrap-3.1.6.min.css b/src/websrc/3rdparty/css/footable.bootstrap-3.1.6.min.css deleted file mode 100644 index 87841b27a..000000000 --- a/src/websrc/3rdparty/css/footable.bootstrap-3.1.6.min.css +++ /dev/null @@ -1 +0,0 @@ -table.footable-details,table.footable>thead>tr.footable-filtering>th div.form-group{margin-bottom:0}table.footable,table.footable-details{position:relative;width:100%;border-spacing:0;border-collapse:collapse}table.footable-hide-fouc{display:none}table>tbody>tr>td>span.footable-toggle{margin-right:8px;opacity:.3}table>tbody>tr>td>span.footable-toggle.last-column{margin-left:8px;float:right}table.table-condensed>tbody>tr>td>span.footable-toggle{margin-right:5px}table.footable-details>tbody>tr>th:nth-child(1){min-width:40px;width:120px}table.footable-details>tbody>tr>td:nth-child(2){word-break:break-all}table.footable-details>tbody>tr:first-child>td,table.footable-details>tbody>tr:first-child>th,table.footable-details>tfoot>tr:first-child>td,table.footable-details>tfoot>tr:first-child>th,table.footable-details>thead>tr:first-child>td,table.footable-details>thead>tr:first-child>th{border-top-width:0}table.footable-details.table-bordered>tbody>tr:first-child>td,table.footable-details.table-bordered>tbody>tr:first-child>th,table.footable-details.table-bordered>tfoot>tr:first-child>td,table.footable-details.table-bordered>tfoot>tr:first-child>th,table.footable-details.table-bordered>thead>tr:first-child>td,table.footable-details.table-bordered>thead>tr:first-child>th{border-top-width:1px}div.footable-loader{vertical-align:middle;text-align:center;height:300px;position:relative}div.footable-loader>span.fooicon{display:inline-block;opacity:.3;font-size:30px;line-height:32px;width:32px;height:32px;margin-top:-16px;margin-left:-16px;position:absolute;top:50%;left:50%;-webkit-animation:fooicon-spin-r 2s infinite linear;animation:fooicon-spin-r 2s infinite linear}table.footable>tbody>tr.footable-empty>td{vertical-align:middle;text-align:center;font-size:30px}table.footable>tbody>tr>td,table.footable>tbody>tr>th{display:none}table.footable>tbody>tr.footable-detail-row>td,table.footable>tbody>tr.footable-detail-row>th,table.footable>tbody>tr.footable-empty>td,table.footable>tbody>tr.footable-empty>th{display:table-cell}@-webkit-keyframes fooicon-spin-r{0%{-webkit-transform:rotate(0);transform:rotate(0)}100%{-webkit-transform:rotate(359deg);transform:rotate(359deg)}}@keyframes fooicon-spin-r{0%{-webkit-transform:rotate(0);transform:rotate(0)}100%{-webkit-transform:rotate(359deg);transform:rotate(359deg)}}.fooicon{position:relative;top:1px;display:inline-block;font-family:'Glyphicons Halflings'!important;font-style:normal;font-weight:400;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.fooicon:after,.fooicon:before{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}.fooicon-loader:before{content:"\e030"}.fooicon-plus:before{content:"\2b"}.fooicon-minus:before{content:"\2212"}.fooicon-search:before{content:"\e003"}.fooicon-remove:before{content:"\e014"}.fooicon-sort:before{content:"\e150"}.fooicon-sort-asc:before{content:"\e155"}.fooicon-sort-desc:before{content:"\e156"}.fooicon-pencil:before{content:"\270f"}.fooicon-trash:before{content:"\e020"}.fooicon-eye-close:before{content:"\e106"}.fooicon-flash:before{content:"\e162"}.fooicon-cog:before{content:"\e019"}.fooicon-stats:before{content:"\e185"}table.footable>thead>tr.footable-filtering>th{border-bottom-width:1px;font-weight:400}.footable-filtering-external.footable-filtering-right,table.footable.footable-filtering-right>thead>tr.footable-filtering>th,table.footable>thead>tr.footable-filtering>th{text-align:right}.footable-filtering-external.footable-filtering-left,table.footable.footable-filtering-left>thead>tr.footable-filtering>th{text-align:left}.footable-filtering-external.footable-filtering-center,.footable-paging-external.footable-paging-center,table.footable-paging-center>tfoot>tr.footable-paging>td,table.footable.footable-filtering-center>thead>tr.footable-filtering>th,table.footable>tfoot>tr.footable-paging>td{text-align:center}table.footable>thead>tr.footable-filtering>th div.form-group+div.form-group{margin-top:5px}table.footable>thead>tr.footable-filtering>th div.input-group{width:100%}.footable-filtering-external ul.dropdown-menu>li>a.checkbox,table.footable>thead>tr.footable-filtering>th ul.dropdown-menu>li>a.checkbox{margin:0;display:block;position:relative}.footable-filtering-external ul.dropdown-menu>li>a.checkbox>label,table.footable>thead>tr.footable-filtering>th ul.dropdown-menu>li>a.checkbox>label{display:block;padding-left:20px}.footable-filtering-external ul.dropdown-menu>li>a.checkbox input[type=checkbox],table.footable>thead>tr.footable-filtering>th ul.dropdown-menu>li>a.checkbox input[type=checkbox]{position:absolute;margin-left:-20px}@media (min-width:768px){table.footable>thead>tr.footable-filtering>th div.input-group{width:auto}table.footable>thead>tr.footable-filtering>th div.form-group{margin-left:2px;margin-right:2px}table.footable>thead>tr.footable-filtering>th div.form-group+div.form-group{margin-top:0}}table.footable>tbody>tr>td.footable-sortable,table.footable>tbody>tr>th.footable-sortable,table.footable>tfoot>tr>td.footable-sortable,table.footable>tfoot>tr>th.footable-sortable,table.footable>thead>tr>td.footable-sortable,table.footable>thead>tr>th.footable-sortable{position:relative;padding-right:30px;cursor:pointer}td.footable-sortable>span.fooicon,th.footable-sortable>span.fooicon{position:absolute;right:6px;top:50%;margin-top:-7px;opacity:0;transition:opacity .3s ease-in}td.footable-sortable.footable-asc>span.fooicon,td.footable-sortable.footable-desc>span.fooicon,td.footable-sortable:hover>span.fooicon,th.footable-sortable.footable-asc>span.fooicon,th.footable-sortable.footable-desc>span.fooicon,th.footable-sortable:hover>span.fooicon{opacity:1}table.footable-sorting-disabled td.footable-sortable.footable-asc>span.fooicon,table.footable-sorting-disabled td.footable-sortable.footable-desc>span.fooicon,table.footable-sorting-disabled td.footable-sortable:hover>span.fooicon,table.footable-sorting-disabled th.footable-sortable.footable-asc>span.fooicon,table.footable-sorting-disabled th.footable-sortable.footable-desc>span.fooicon,table.footable-sorting-disabled th.footable-sortable:hover>span.fooicon{opacity:0;visibility:hidden}.footable-paging-external ul.pagination,table.footable>tfoot>tr.footable-paging>td>ul.pagination{margin:10px 0 0}.footable-paging-external span.label,table.footable>tfoot>tr.footable-paging>td>span.label{display:inline-block;margin:0 0 10px;padding:4px 10px}.footable-paging-external.footable-paging-left,table.footable-paging-left>tfoot>tr.footable-paging>td{text-align:left}.footable-paging-external.footable-paging-right,table.footable-editing-right td.footable-editing,table.footable-editing-right tr.footable-editing,table.footable-paging-right>tfoot>tr.footable-paging>td{text-align:right}ul.pagination>li.footable-page{display:none}ul.pagination>li.footable-page.visible{display:inline}td.footable-editing{width:90px;max-width:90px}table.footable-editing-no-delete td.footable-editing,table.footable-editing-no-edit td.footable-editing,table.footable-editing-no-view td.footable-editing{width:70px;max-width:70px}table.footable-editing-no-delete.footable-editing-no-view td.footable-editing,table.footable-editing-no-edit.footable-editing-no-delete td.footable-editing,table.footable-editing-no-edit.footable-editing-no-view td.footable-editing{width:50px;max-width:50px}table.footable-editing-no-edit.footable-editing-no-delete.footable-editing-no-view td.footable-editing,table.footable-editing-no-edit.footable-editing-no-delete.footable-editing-no-view th.footable-editing{width:0;max-width:0;display:none!important}table.footable-editing-left td.footable-editing,table.footable-editing-left tr.footable-editing{text-align:left}table.footable-editing button.footable-add,table.footable-editing button.footable-hide,table.footable-editing-show button.footable-show,table.footable-editing.footable-editing-always-show button.footable-hide,table.footable-editing.footable-editing-always-show button.footable-show,table.footable-editing.footable-editing-always-show.footable-editing-no-add tr.footable-editing{display:none}table.footable-editing.footable-editing-always-show button.footable-add,table.footable-editing.footable-editing-show button.footable-add,table.footable-editing.footable-editing-show button.footable-hide{display:inline-block} \ No newline at end of file diff --git a/src/websrc/3rdparty/js/bootstrap-3.3.7.min.js b/src/websrc/3rdparty/js/bootstrap-3.3.7.min.js deleted file mode 100644 index 9bcd2fcca..000000000 --- a/src/websrc/3rdparty/js/bootstrap-3.3.7.min.js +++ /dev/null @@ -1,7 +0,0 @@ -/*! - * Bootstrap v3.3.7 (http://getbootstrap.com) - * Copyright 2011-2016 Twitter, Inc. - * Licensed under the MIT license - */ -if("undefined"==typeof jQuery)throw new Error("Bootstrap's JavaScript requires jQuery");+function(a){"use strict";var b=a.fn.jquery.split(" ")[0].split(".");if(b[0]<2&&b[1]<9||1==b[0]&&9==b[1]&&b[2]<1||b[0]>3)throw new Error("Bootstrap's JavaScript requires jQuery version 1.9.1 or higher, but lower than version 4")}(jQuery),+function(a){"use strict";function b(){var a=document.createElement("bootstrap"),b={WebkitTransition:"webkitTransitionEnd",MozTransition:"transitionend",OTransition:"oTransitionEnd otransitionend",transition:"transitionend"};for(var c in b)if(void 0!==a.style[c])return{end:b[c]};return!1}a.fn.emulateTransitionEnd=function(b){var c=!1,d=this;a(this).one("bsTransitionEnd",function(){c=!0});var e=function(){c||a(d).trigger(a.support.transition.end)};return setTimeout(e,b),this},a(function(){a.support.transition=b(),a.support.transition&&(a.event.special.bsTransitionEnd={bindType:a.support.transition.end,delegateType:a.support.transition.end,handle:function(b){if(a(b.target).is(this))return b.handleObj.handler.apply(this,arguments)}})})}(jQuery),+function(a){"use strict";function b(b){return this.each(function(){var c=a(this),e=c.data("bs.alert");e||c.data("bs.alert",e=new d(this)),"string"==typeof b&&e[b].call(c)})}var c='[data-dismiss="alert"]',d=function(b){a(b).on("click",c,this.close)};d.VERSION="3.3.7",d.TRANSITION_DURATION=150,d.prototype.close=function(b){function c(){g.detach().trigger("closed.bs.alert").remove()}var e=a(this),f=e.attr("data-target");f||(f=e.attr("href"),f=f&&f.replace(/.*(?=#[^\s]*$)/,""));var g=a("#"===f?[]:f);b&&b.preventDefault(),g.length||(g=e.closest(".alert")),g.trigger(b=a.Event("close.bs.alert")),b.isDefaultPrevented()||(g.removeClass("in"),a.support.transition&&g.hasClass("fade")?g.one("bsTransitionEnd",c).emulateTransitionEnd(d.TRANSITION_DURATION):c())};var e=a.fn.alert;a.fn.alert=b,a.fn.alert.Constructor=d,a.fn.alert.noConflict=function(){return a.fn.alert=e,this},a(document).on("click.bs.alert.data-api",c,d.prototype.close)}(jQuery),+function(a){"use strict";function b(b){return this.each(function(){var d=a(this),e=d.data("bs.button"),f="object"==typeof b&&b;e||d.data("bs.button",e=new c(this,f)),"toggle"==b?e.toggle():b&&e.setState(b)})}var c=function(b,d){this.$element=a(b),this.options=a.extend({},c.DEFAULTS,d),this.isLoading=!1};c.VERSION="3.3.7",c.DEFAULTS={loadingText:"loading..."},c.prototype.setState=function(b){var c="disabled",d=this.$element,e=d.is("input")?"val":"html",f=d.data();b+="Text",null==f.resetText&&d.data("resetText",d[e]()),setTimeout(a.proxy(function(){d[e](null==f[b]?this.options[b]:f[b]),"loadingText"==b?(this.isLoading=!0,d.addClass(c).attr(c,c).prop(c,!0)):this.isLoading&&(this.isLoading=!1,d.removeClass(c).removeAttr(c).prop(c,!1))},this),0)},c.prototype.toggle=function(){var a=!0,b=this.$element.closest('[data-toggle="buttons"]');if(b.length){var c=this.$element.find("input");"radio"==c.prop("type")?(c.prop("checked")&&(a=!1),b.find(".active").removeClass("active"),this.$element.addClass("active")):"checkbox"==c.prop("type")&&(c.prop("checked")!==this.$element.hasClass("active")&&(a=!1),this.$element.toggleClass("active")),c.prop("checked",this.$element.hasClass("active")),a&&c.trigger("change")}else this.$element.attr("aria-pressed",!this.$element.hasClass("active")),this.$element.toggleClass("active")};var d=a.fn.button;a.fn.button=b,a.fn.button.Constructor=c,a.fn.button.noConflict=function(){return a.fn.button=d,this},a(document).on("click.bs.button.data-api",'[data-toggle^="button"]',function(c){var d=a(c.target).closest(".btn");b.call(d,"toggle"),a(c.target).is('input[type="radio"], input[type="checkbox"]')||(c.preventDefault(),d.is("input,button")?d.trigger("focus"):d.find("input:visible,button:visible").first().trigger("focus"))}).on("focus.bs.button.data-api blur.bs.button.data-api",'[data-toggle^="button"]',function(b){a(b.target).closest(".btn").toggleClass("focus",/^focus(in)?$/.test(b.type))})}(jQuery),+function(a){"use strict";function b(b){return this.each(function(){var d=a(this),e=d.data("bs.carousel"),f=a.extend({},c.DEFAULTS,d.data(),"object"==typeof b&&b),g="string"==typeof b?b:f.slide;e||d.data("bs.carousel",e=new c(this,f)),"number"==typeof b?e.to(b):g?e[g]():f.interval&&e.pause().cycle()})}var c=function(b,c){this.$element=a(b),this.$indicators=this.$element.find(".carousel-indicators"),this.options=c,this.paused=null,this.sliding=null,this.interval=null,this.$active=null,this.$items=null,this.options.keyboard&&this.$element.on("keydown.bs.carousel",a.proxy(this.keydown,this)),"hover"==this.options.pause&&!("ontouchstart"in document.documentElement)&&this.$element.on("mouseenter.bs.carousel",a.proxy(this.pause,this)).on("mouseleave.bs.carousel",a.proxy(this.cycle,this))};c.VERSION="3.3.7",c.TRANSITION_DURATION=600,c.DEFAULTS={interval:5e3,pause:"hover",wrap:!0,keyboard:!0},c.prototype.keydown=function(a){if(!/input|textarea/i.test(a.target.tagName)){switch(a.which){case 37:this.prev();break;case 39:this.next();break;default:return}a.preventDefault()}},c.prototype.cycle=function(b){return b||(this.paused=!1),this.interval&&clearInterval(this.interval),this.options.interval&&!this.paused&&(this.interval=setInterval(a.proxy(this.next,this),this.options.interval)),this},c.prototype.getItemIndex=function(a){return this.$items=a.parent().children(".item"),this.$items.index(a||this.$active)},c.prototype.getItemForDirection=function(a,b){var c=this.getItemIndex(b),d="prev"==a&&0===c||"next"==a&&c==this.$items.length-1;if(d&&!this.options.wrap)return b;var e="prev"==a?-1:1,f=(c+e)%this.$items.length;return this.$items.eq(f)},c.prototype.to=function(a){var b=this,c=this.getItemIndex(this.$active=this.$element.find(".item.active"));if(!(a>this.$items.length-1||a<0))return this.sliding?this.$element.one("slid.bs.carousel",function(){b.to(a)}):c==a?this.pause().cycle():this.slide(a>c?"next":"prev",this.$items.eq(a))},c.prototype.pause=function(b){return b||(this.paused=!0),this.$element.find(".next, .prev").length&&a.support.transition&&(this.$element.trigger(a.support.transition.end),this.cycle(!0)),this.interval=clearInterval(this.interval),this},c.prototype.next=function(){if(!this.sliding)return this.slide("next")},c.prototype.prev=function(){if(!this.sliding)return this.slide("prev")},c.prototype.slide=function(b,d){var e=this.$element.find(".item.active"),f=d||this.getItemForDirection(b,e),g=this.interval,h="next"==b?"left":"right",i=this;if(f.hasClass("active"))return this.sliding=!1;var j=f[0],k=a.Event("slide.bs.carousel",{relatedTarget:j,direction:h});if(this.$element.trigger(k),!k.isDefaultPrevented()){if(this.sliding=!0,g&&this.pause(),this.$indicators.length){this.$indicators.find(".active").removeClass("active");var l=a(this.$indicators.children()[this.getItemIndex(f)]);l&&l.addClass("active")}var m=a.Event("slid.bs.carousel",{relatedTarget:j,direction:h});return a.support.transition&&this.$element.hasClass("slide")?(f.addClass(b),f[0].offsetWidth,e.addClass(h),f.addClass(h),e.one("bsTransitionEnd",function(){f.removeClass([b,h].join(" ")).addClass("active"),e.removeClass(["active",h].join(" ")),i.sliding=!1,setTimeout(function(){i.$element.trigger(m)},0)}).emulateTransitionEnd(c.TRANSITION_DURATION)):(e.removeClass("active"),f.addClass("active"),this.sliding=!1,this.$element.trigger(m)),g&&this.cycle(),this}};var d=a.fn.carousel;a.fn.carousel=b,a.fn.carousel.Constructor=c,a.fn.carousel.noConflict=function(){return a.fn.carousel=d,this};var e=function(c){var d,e=a(this),f=a(e.attr("data-target")||(d=e.attr("href"))&&d.replace(/.*(?=#[^\s]+$)/,""));if(f.hasClass("carousel")){var g=a.extend({},f.data(),e.data()),h=e.attr("data-slide-to");h&&(g.interval=!1),b.call(f,g),h&&f.data("bs.carousel").to(h),c.preventDefault()}};a(document).on("click.bs.carousel.data-api","[data-slide]",e).on("click.bs.carousel.data-api","[data-slide-to]",e),a(window).on("load",function(){a('[data-ride="carousel"]').each(function(){var c=a(this);b.call(c,c.data())})})}(jQuery),+function(a){"use strict";function b(b){var c,d=b.attr("data-target")||(c=b.attr("href"))&&c.replace(/.*(?=#[^\s]+$)/,"");return a(d)}function c(b){return this.each(function(){var c=a(this),e=c.data("bs.collapse"),f=a.extend({},d.DEFAULTS,c.data(),"object"==typeof b&&b);!e&&f.toggle&&/show|hide/.test(b)&&(f.toggle=!1),e||c.data("bs.collapse",e=new d(this,f)),"string"==typeof b&&e[b]()})}var d=function(b,c){this.$element=a(b),this.options=a.extend({},d.DEFAULTS,c),this.$trigger=a('[data-toggle="collapse"][href="#'+b.id+'"],[data-toggle="collapse"][data-target="#'+b.id+'"]'),this.transitioning=null,this.options.parent?this.$parent=this.getParent():this.addAriaAndCollapsedClass(this.$element,this.$trigger),this.options.toggle&&this.toggle()};d.VERSION="3.3.7",d.TRANSITION_DURATION=350,d.DEFAULTS={toggle:!0},d.prototype.dimension=function(){var a=this.$element.hasClass("width");return a?"width":"height"},d.prototype.show=function(){if(!this.transitioning&&!this.$element.hasClass("in")){var b,e=this.$parent&&this.$parent.children(".panel").children(".in, .collapsing");if(!(e&&e.length&&(b=e.data("bs.collapse"),b&&b.transitioning))){var f=a.Event("show.bs.collapse");if(this.$element.trigger(f),!f.isDefaultPrevented()){e&&e.length&&(c.call(e,"hide"),b||e.data("bs.collapse",null));var g=this.dimension();this.$element.removeClass("collapse").addClass("collapsing")[g](0).attr("aria-expanded",!0),this.$trigger.removeClass("collapsed").attr("aria-expanded",!0),this.transitioning=1;var h=function(){this.$element.removeClass("collapsing").addClass("collapse in")[g](""),this.transitioning=0,this.$element.trigger("shown.bs.collapse")};if(!a.support.transition)return h.call(this);var i=a.camelCase(["scroll",g].join("-"));this.$element.one("bsTransitionEnd",a.proxy(h,this)).emulateTransitionEnd(d.TRANSITION_DURATION)[g](this.$element[0][i])}}}},d.prototype.hide=function(){if(!this.transitioning&&this.$element.hasClass("in")){var b=a.Event("hide.bs.collapse");if(this.$element.trigger(b),!b.isDefaultPrevented()){var c=this.dimension();this.$element[c](this.$element[c]())[0].offsetHeight,this.$element.addClass("collapsing").removeClass("collapse in").attr("aria-expanded",!1),this.$trigger.addClass("collapsed").attr("aria-expanded",!1),this.transitioning=1;var e=function(){this.transitioning=0,this.$element.removeClass("collapsing").addClass("collapse").trigger("hidden.bs.collapse")};return a.support.transition?void this.$element[c](0).one("bsTransitionEnd",a.proxy(e,this)).emulateTransitionEnd(d.TRANSITION_DURATION):e.call(this)}}},d.prototype.toggle=function(){this[this.$element.hasClass("in")?"hide":"show"]()},d.prototype.getParent=function(){return a(this.options.parent).find('[data-toggle="collapse"][data-parent="'+this.options.parent+'"]').each(a.proxy(function(c,d){var e=a(d);this.addAriaAndCollapsedClass(b(e),e)},this)).end()},d.prototype.addAriaAndCollapsedClass=function(a,b){var c=a.hasClass("in");a.attr("aria-expanded",c),b.toggleClass("collapsed",!c).attr("aria-expanded",c)};var e=a.fn.collapse;a.fn.collapse=c,a.fn.collapse.Constructor=d,a.fn.collapse.noConflict=function(){return a.fn.collapse=e,this},a(document).on("click.bs.collapse.data-api",'[data-toggle="collapse"]',function(d){var e=a(this);e.attr("data-target")||d.preventDefault();var f=b(e),g=f.data("bs.collapse"),h=g?"toggle":e.data();c.call(f,h)})}(jQuery),+function(a){"use strict";function b(b){var c=b.attr("data-target");c||(c=b.attr("href"),c=c&&/#[A-Za-z]/.test(c)&&c.replace(/.*(?=#[^\s]*$)/,""));var d=c&&a(c);return d&&d.length?d:b.parent()}function c(c){c&&3===c.which||(a(e).remove(),a(f).each(function(){var d=a(this),e=b(d),f={relatedTarget:this};e.hasClass("open")&&(c&&"click"==c.type&&/input|textarea/i.test(c.target.tagName)&&a.contains(e[0],c.target)||(e.trigger(c=a.Event("hide.bs.dropdown",f)),c.isDefaultPrevented()||(d.attr("aria-expanded","false"),e.removeClass("open").trigger(a.Event("hidden.bs.dropdown",f)))))}))}function d(b){return this.each(function(){var c=a(this),d=c.data("bs.dropdown");d||c.data("bs.dropdown",d=new g(this)),"string"==typeof b&&d[b].call(c)})}var e=".dropdown-backdrop",f='[data-toggle="dropdown"]',g=function(b){a(b).on("click.bs.dropdown",this.toggle)};g.VERSION="3.3.7",g.prototype.toggle=function(d){var e=a(this);if(!e.is(".disabled, :disabled")){var f=b(e),g=f.hasClass("open");if(c(),!g){"ontouchstart"in document.documentElement&&!f.closest(".navbar-nav").length&&a(document.createElement("div")).addClass("dropdown-backdrop").insertAfter(a(this)).on("click",c);var h={relatedTarget:this};if(f.trigger(d=a.Event("show.bs.dropdown",h)),d.isDefaultPrevented())return;e.trigger("focus").attr("aria-expanded","true"),f.toggleClass("open").trigger(a.Event("shown.bs.dropdown",h))}return!1}},g.prototype.keydown=function(c){if(/(38|40|27|32)/.test(c.which)&&!/input|textarea/i.test(c.target.tagName)){var d=a(this);if(c.preventDefault(),c.stopPropagation(),!d.is(".disabled, :disabled")){var e=b(d),g=e.hasClass("open");if(!g&&27!=c.which||g&&27==c.which)return 27==c.which&&e.find(f).trigger("focus"),d.trigger("click");var h=" li:not(.disabled):visible a",i=e.find(".dropdown-menu"+h);if(i.length){var j=i.index(c.target);38==c.which&&j>0&&j--,40==c.which&&jdocument.documentElement.clientHeight;this.$element.css({paddingLeft:!this.bodyIsOverflowing&&a?this.scrollbarWidth:"",paddingRight:this.bodyIsOverflowing&&!a?this.scrollbarWidth:""})},c.prototype.resetAdjustments=function(){this.$element.css({paddingLeft:"",paddingRight:""})},c.prototype.checkScrollbar=function(){var a=window.innerWidth;if(!a){var b=document.documentElement.getBoundingClientRect();a=b.right-Math.abs(b.left)}this.bodyIsOverflowing=document.body.clientWidth
',trigger:"hover focus",title:"",delay:0,html:!1,container:!1,viewport:{selector:"body",padding:0}},c.prototype.init=function(b,c,d){if(this.enabled=!0,this.type=b,this.$element=a(c),this.options=this.getOptions(d),this.$viewport=this.options.viewport&&a(a.isFunction(this.options.viewport)?this.options.viewport.call(this,this.$element):this.options.viewport.selector||this.options.viewport),this.inState={click:!1,hover:!1,focus:!1},this.$element[0]instanceof document.constructor&&!this.options.selector)throw new Error("`selector` option must be specified when initializing "+this.type+" on the window.document object!");for(var e=this.options.trigger.split(" "),f=e.length;f--;){var g=e[f];if("click"==g)this.$element.on("click."+this.type,this.options.selector,a.proxy(this.toggle,this));else if("manual"!=g){var h="hover"==g?"mouseenter":"focusin",i="hover"==g?"mouseleave":"focusout";this.$element.on(h+"."+this.type,this.options.selector,a.proxy(this.enter,this)),this.$element.on(i+"."+this.type,this.options.selector,a.proxy(this.leave,this))}}this.options.selector?this._options=a.extend({},this.options,{trigger:"manual",selector:""}):this.fixTitle()},c.prototype.getDefaults=function(){return c.DEFAULTS},c.prototype.getOptions=function(b){return b=a.extend({},this.getDefaults(),this.$element.data(),b),b.delay&&"number"==typeof b.delay&&(b.delay={show:b.delay,hide:b.delay}),b},c.prototype.getDelegateOptions=function(){var b={},c=this.getDefaults();return this._options&&a.each(this._options,function(a,d){c[a]!=d&&(b[a]=d)}),b},c.prototype.enter=function(b){var c=b instanceof this.constructor?b:a(b.currentTarget).data("bs."+this.type);return c||(c=new this.constructor(b.currentTarget,this.getDelegateOptions()),a(b.currentTarget).data("bs."+this.type,c)),b instanceof a.Event&&(c.inState["focusin"==b.type?"focus":"hover"]=!0),c.tip().hasClass("in")||"in"==c.hoverState?void(c.hoverState="in"):(clearTimeout(c.timeout),c.hoverState="in",c.options.delay&&c.options.delay.show?void(c.timeout=setTimeout(function(){"in"==c.hoverState&&c.show()},c.options.delay.show)):c.show())},c.prototype.isInStateTrue=function(){for(var a in this.inState)if(this.inState[a])return!0;return!1},c.prototype.leave=function(b){var c=b instanceof this.constructor?b:a(b.currentTarget).data("bs."+this.type);if(c||(c=new this.constructor(b.currentTarget,this.getDelegateOptions()),a(b.currentTarget).data("bs."+this.type,c)),b instanceof a.Event&&(c.inState["focusout"==b.type?"focus":"hover"]=!1),!c.isInStateTrue())return clearTimeout(c.timeout),c.hoverState="out",c.options.delay&&c.options.delay.hide?void(c.timeout=setTimeout(function(){"out"==c.hoverState&&c.hide()},c.options.delay.hide)):c.hide()},c.prototype.show=function(){var b=a.Event("show.bs."+this.type);if(this.hasContent()&&this.enabled){this.$element.trigger(b);var d=a.contains(this.$element[0].ownerDocument.documentElement,this.$element[0]);if(b.isDefaultPrevented()||!d)return;var e=this,f=this.tip(),g=this.getUID(this.type);this.setContent(),f.attr("id",g),this.$element.attr("aria-describedby",g),this.options.animation&&f.addClass("fade");var h="function"==typeof this.options.placement?this.options.placement.call(this,f[0],this.$element[0]):this.options.placement,i=/\s?auto?\s?/i,j=i.test(h);j&&(h=h.replace(i,"")||"top"),f.detach().css({top:0,left:0,display:"block"}).addClass(h).data("bs."+this.type,this),this.options.container?f.appendTo(this.options.container):f.insertAfter(this.$element),this.$element.trigger("inserted.bs."+this.type);var k=this.getPosition(),l=f[0].offsetWidth,m=f[0].offsetHeight;if(j){var n=h,o=this.getPosition(this.$viewport);h="bottom"==h&&k.bottom+m>o.bottom?"top":"top"==h&&k.top-mo.width?"left":"left"==h&&k.left-lg.top+g.height&&(e.top=g.top+g.height-i)}else{var j=b.left-f,k=b.left+f+c;jg.right&&(e.left=g.left+g.width-k)}return e},c.prototype.getTitle=function(){var a,b=this.$element,c=this.options;return a=b.attr("data-original-title")||("function"==typeof c.title?c.title.call(b[0]):c.title)},c.prototype.getUID=function(a){do a+=~~(1e6*Math.random());while(document.getElementById(a));return a},c.prototype.tip=function(){if(!this.$tip&&(this.$tip=a(this.options.template),1!=this.$tip.length))throw new Error(this.type+" `template` option must consist of exactly 1 top-level element!");return this.$tip},c.prototype.arrow=function(){return this.$arrow=this.$arrow||this.tip().find(".tooltip-arrow")},c.prototype.enable=function(){this.enabled=!0},c.prototype.disable=function(){this.enabled=!1},c.prototype.toggleEnabled=function(){this.enabled=!this.enabled},c.prototype.toggle=function(b){var c=this;b&&(c=a(b.currentTarget).data("bs."+this.type),c||(c=new this.constructor(b.currentTarget,this.getDelegateOptions()),a(b.currentTarget).data("bs."+this.type,c))),b?(c.inState.click=!c.inState.click,c.isInStateTrue()?c.enter(c):c.leave(c)):c.tip().hasClass("in")?c.leave(c):c.enter(c)},c.prototype.destroy=function(){var a=this;clearTimeout(this.timeout),this.hide(function(){a.$element.off("."+a.type).removeData("bs."+a.type),a.$tip&&a.$tip.detach(),a.$tip=null,a.$arrow=null,a.$viewport=null,a.$element=null})};var d=a.fn.tooltip;a.fn.tooltip=b,a.fn.tooltip.Constructor=c,a.fn.tooltip.noConflict=function(){return a.fn.tooltip=d,this}}(jQuery),+function(a){"use strict";function b(b){return this.each(function(){var d=a(this),e=d.data("bs.popover"),f="object"==typeof b&&b;!e&&/destroy|hide/.test(b)||(e||d.data("bs.popover",e=new c(this,f)),"string"==typeof b&&e[b]())})}var c=function(a,b){this.init("popover",a,b)};if(!a.fn.tooltip)throw new Error("Popover requires tooltip.js");c.VERSION="3.3.7",c.DEFAULTS=a.extend({},a.fn.tooltip.Constructor.DEFAULTS,{placement:"right",trigger:"click",content:"",template:''}),c.prototype=a.extend({},a.fn.tooltip.Constructor.prototype),c.prototype.constructor=c,c.prototype.getDefaults=function(){return c.DEFAULTS},c.prototype.setContent=function(){var a=this.tip(),b=this.getTitle(),c=this.getContent();a.find(".popover-title")[this.options.html?"html":"text"](b),a.find(".popover-content").children().detach().end()[this.options.html?"string"==typeof c?"html":"append":"text"](c),a.removeClass("fade top bottom left right in"),a.find(".popover-title").html()||a.find(".popover-title").hide()},c.prototype.hasContent=function(){return this.getTitle()||this.getContent()},c.prototype.getContent=function(){var a=this.$element,b=this.options;return a.attr("data-content")||("function"==typeof b.content?b.content.call(a[0]):b.content)},c.prototype.arrow=function(){return this.$arrow=this.$arrow||this.tip().find(".arrow")};var d=a.fn.popover;a.fn.popover=b,a.fn.popover.Constructor=c,a.fn.popover.noConflict=function(){return a.fn.popover=d,this}}(jQuery),+function(a){"use strict";function b(c,d){this.$body=a(document.body),this.$scrollElement=a(a(c).is(document.body)?window:c),this.options=a.extend({},b.DEFAULTS,d),this.selector=(this.options.target||"")+" .nav li > a",this.offsets=[],this.targets=[],this.activeTarget=null,this.scrollHeight=0,this.$scrollElement.on("scroll.bs.scrollspy",a.proxy(this.process,this)),this.refresh(),this.process()}function c(c){return this.each(function(){var d=a(this),e=d.data("bs.scrollspy"),f="object"==typeof c&&c;e||d.data("bs.scrollspy",e=new b(this,f)),"string"==typeof c&&e[c]()})}b.VERSION="3.3.7",b.DEFAULTS={offset:10},b.prototype.getScrollHeight=function(){return this.$scrollElement[0].scrollHeight||Math.max(this.$body[0].scrollHeight,document.documentElement.scrollHeight)},b.prototype.refresh=function(){var b=this,c="offset",d=0;this.offsets=[],this.targets=[],this.scrollHeight=this.getScrollHeight(),a.isWindow(this.$scrollElement[0])||(c="position",d=this.$scrollElement.scrollTop()),this.$body.find(this.selector).map(function(){var b=a(this),e=b.data("target")||b.attr("href"),f=/^#./.test(e)&&a(e);return f&&f.length&&f.is(":visible")&&[[f[c]().top+d,e]]||null}).sort(function(a,b){return a[0]-b[0]}).each(function(){b.offsets.push(this[0]),b.targets.push(this[1])})},b.prototype.process=function(){var a,b=this.$scrollElement.scrollTop()+this.options.offset,c=this.getScrollHeight(),d=this.options.offset+c-this.$scrollElement.height(),e=this.offsets,f=this.targets,g=this.activeTarget;if(this.scrollHeight!=c&&this.refresh(),b>=d)return g!=(a=f[f.length-1])&&this.activate(a);if(g&&b=e[a]&&(void 0===e[a+1]||b .dropdown-menu > .active").removeClass("active").end().find('[data-toggle="tab"]').attr("aria-expanded",!1),b.addClass("active").find('[data-toggle="tab"]').attr("aria-expanded",!0),h?(b[0].offsetWidth,b.addClass("in")):b.removeClass("fade"),b.parent(".dropdown-menu").length&&b.closest("li.dropdown").addClass("active").end().find('[data-toggle="tab"]').attr("aria-expanded",!0),e&&e()}var g=d.find("> .active"),h=e&&a.support.transition&&(g.length&&g.hasClass("fade")||!!d.find("> .fade").length);g.length&&h?g.one("bsTransitionEnd",f).emulateTransitionEnd(c.TRANSITION_DURATION):f(),g.removeClass("in")};var d=a.fn.tab;a.fn.tab=b,a.fn.tab.Constructor=c,a.fn.tab.noConflict=function(){return a.fn.tab=d,this};var e=function(c){c.preventDefault(),b.call(a(this),"show")};a(document).on("click.bs.tab.data-api",'[data-toggle="tab"]',e).on("click.bs.tab.data-api",'[data-toggle="pill"]',e)}(jQuery),+function(a){"use strict";function b(b){return this.each(function(){var d=a(this),e=d.data("bs.affix"),f="object"==typeof b&&b;e||d.data("bs.affix",e=new c(this,f)),"string"==typeof b&&e[b]()})}var c=function(b,d){this.options=a.extend({},c.DEFAULTS,d),this.$target=a(this.options.target).on("scroll.bs.affix.data-api",a.proxy(this.checkPosition,this)).on("click.bs.affix.data-api",a.proxy(this.checkPositionWithEventLoop,this)),this.$element=a(b),this.affixed=null,this.unpin=null,this.pinnedOffset=null,this.checkPosition()};c.VERSION="3.3.7",c.RESET="affix affix-top affix-bottom",c.DEFAULTS={offset:0,target:window},c.prototype.getState=function(a,b,c,d){var e=this.$target.scrollTop(),f=this.$element.offset(),g=this.$target.height();if(null!=c&&"top"==this.affixed)return e=a-d&&"bottom"},c.prototype.getPinnedOffset=function(){if(this.pinnedOffset)return this.pinnedOffset;this.$element.removeClass(c.RESET).addClass("affix");var a=this.$target.scrollTop(),b=this.$element.offset();return this.pinnedOffset=b.top-a},c.prototype.checkPositionWithEventLoop=function(){setTimeout(a.proxy(this.checkPosition,this),1)},c.prototype.checkPosition=function(){if(this.$element.is(":visible")){var b=this.$element.height(),d=this.options.offset,e=d.top,f=d.bottom,g=Math.max(a(document).height(),a(document.body).height());"object"!=typeof d&&(f=e=d),"function"==typeof e&&(e=d.top(this.$element)),"function"==typeof f&&(f=d.bottom(this.$element));var h=this.getState(g,b,e,f);if(this.affixed!=h){null!=this.unpin&&this.$element.css("top","");var i="affix"+(h?"-"+h:""),j=a.Event(i+".bs.affix");if(this.$element.trigger(j),j.isDefaultPrevented())return;this.affixed=h,this.unpin="bottom"==h?this.getPinnedOffset():null,this.$element.removeClass(c.RESET).addClass(i).trigger(i.replace("affix","affixed")+".bs.affix")}"bottom"==h&&this.$element.offset({top:g-b-f})}};var d=a.fn.affix;a.fn.affix=b,a.fn.affix.Constructor=c,a.fn.affix.noConflict=function(){return a.fn.affix=d,this},a(window).on("load",function(){a('[data-spy="affix"]').each(function(){var c=a(this),d=c.data();d.offset=d.offset||{},null!=d.offsetBottom&&(d.offset.bottom=d.offsetBottom),null!=d.offsetTop&&(d.offset.top=d.offsetTop),b.call(c,d)})})}(jQuery); \ No newline at end of file diff --git a/src/websrc/3rdparty/js/bootstrap-3.4.1.min.js b/src/websrc/3rdparty/js/bootstrap-3.4.1.min.js new file mode 100644 index 000000000..eb0a8b410 --- /dev/null +++ b/src/websrc/3rdparty/js/bootstrap-3.4.1.min.js @@ -0,0 +1,6 @@ +/*! + * Bootstrap v3.4.1 (https://getbootstrap.com/) + * Copyright 2011-2019 Twitter, Inc. + * Licensed under the MIT license + */ +if("undefined"==typeof jQuery)throw new Error("Bootstrap's JavaScript requires jQuery");!function(t){"use strict";var e=jQuery.fn.jquery.split(" ")[0].split(".");if(e[0]<2&&e[1]<9||1==e[0]&&9==e[1]&&e[2]<1||3this.$items.length-1||t<0))return this.sliding?this.$element.one("slid.bs.carousel",function(){e.to(t)}):i==t?this.pause().cycle():this.slide(idocument.documentElement.clientHeight;this.$element.css({paddingLeft:!this.bodyIsOverflowing&&t?this.scrollbarWidth:"",paddingRight:this.bodyIsOverflowing&&!t?this.scrollbarWidth:""})},s.prototype.resetAdjustments=function(){this.$element.css({paddingLeft:"",paddingRight:""})},s.prototype.checkScrollbar=function(){var t=window.innerWidth;if(!t){var e=document.documentElement.getBoundingClientRect();t=e.right-Math.abs(e.left)}this.bodyIsOverflowing=document.body.clientWidth
',trigger:"hover focus",title:"",delay:0,html:!1,container:!1,viewport:{selector:"body",padding:0},sanitize:!0,sanitizeFn:null,whiteList:t},m.prototype.init=function(t,e,i){if(this.enabled=!0,this.type=t,this.$element=g(e),this.options=this.getOptions(i),this.$viewport=this.options.viewport&&g(document).find(g.isFunction(this.options.viewport)?this.options.viewport.call(this,this.$element):this.options.viewport.selector||this.options.viewport),this.inState={click:!1,hover:!1,focus:!1},this.$element[0]instanceof document.constructor&&!this.options.selector)throw new Error("`selector` option must be specified when initializing "+this.type+" on the window.document object!");for(var o=this.options.trigger.split(" "),n=o.length;n--;){var s=o[n];if("click"==s)this.$element.on("click."+this.type,this.options.selector,g.proxy(this.toggle,this));else if("manual"!=s){var a="hover"==s?"mouseenter":"focusin",r="hover"==s?"mouseleave":"focusout";this.$element.on(a+"."+this.type,this.options.selector,g.proxy(this.enter,this)),this.$element.on(r+"."+this.type,this.options.selector,g.proxy(this.leave,this))}}this.options.selector?this._options=g.extend({},this.options,{trigger:"manual",selector:""}):this.fixTitle()},m.prototype.getDefaults=function(){return m.DEFAULTS},m.prototype.getOptions=function(t){var e=this.$element.data();for(var i in e)e.hasOwnProperty(i)&&-1!==g.inArray(i,o)&&delete e[i];return(t=g.extend({},this.getDefaults(),e,t)).delay&&"number"==typeof t.delay&&(t.delay={show:t.delay,hide:t.delay}),t.sanitize&&(t.template=n(t.template,t.whiteList,t.sanitizeFn)),t},m.prototype.getDelegateOptions=function(){var i={},o=this.getDefaults();return this._options&&g.each(this._options,function(t,e){o[t]!=e&&(i[t]=e)}),i},m.prototype.enter=function(t){var e=t instanceof this.constructor?t:g(t.currentTarget).data("bs."+this.type);if(e||(e=new this.constructor(t.currentTarget,this.getDelegateOptions()),g(t.currentTarget).data("bs."+this.type,e)),t instanceof g.Event&&(e.inState["focusin"==t.type?"focus":"hover"]=!0),e.tip().hasClass("in")||"in"==e.hoverState)e.hoverState="in";else{if(clearTimeout(e.timeout),e.hoverState="in",!e.options.delay||!e.options.delay.show)return e.show();e.timeout=setTimeout(function(){"in"==e.hoverState&&e.show()},e.options.delay.show)}},m.prototype.isInStateTrue=function(){for(var t in this.inState)if(this.inState[t])return!0;return!1},m.prototype.leave=function(t){var e=t instanceof this.constructor?t:g(t.currentTarget).data("bs."+this.type);if(e||(e=new this.constructor(t.currentTarget,this.getDelegateOptions()),g(t.currentTarget).data("bs."+this.type,e)),t instanceof g.Event&&(e.inState["focusout"==t.type?"focus":"hover"]=!1),!e.isInStateTrue()){if(clearTimeout(e.timeout),e.hoverState="out",!e.options.delay||!e.options.delay.hide)return e.hide();e.timeout=setTimeout(function(){"out"==e.hoverState&&e.hide()},e.options.delay.hide)}},m.prototype.show=function(){var t=g.Event("show.bs."+this.type);if(this.hasContent()&&this.enabled){this.$element.trigger(t);var e=g.contains(this.$element[0].ownerDocument.documentElement,this.$element[0]);if(t.isDefaultPrevented()||!e)return;var i=this,o=this.tip(),n=this.getUID(this.type);this.setContent(),o.attr("id",n),this.$element.attr("aria-describedby",n),this.options.animation&&o.addClass("fade");var s="function"==typeof this.options.placement?this.options.placement.call(this,o[0],this.$element[0]):this.options.placement,a=/\s?auto?\s?/i,r=a.test(s);r&&(s=s.replace(a,"")||"top"),o.detach().css({top:0,left:0,display:"block"}).addClass(s).data("bs."+this.type,this),this.options.container?o.appendTo(g(document).find(this.options.container)):o.insertAfter(this.$element),this.$element.trigger("inserted.bs."+this.type);var l=this.getPosition(),h=o[0].offsetWidth,d=o[0].offsetHeight;if(r){var p=s,c=this.getPosition(this.$viewport);s="bottom"==s&&l.bottom+d>c.bottom?"top":"top"==s&&l.top-dc.width?"left":"left"==s&&l.left-ha.top+a.height&&(n.top=a.top+a.height-l)}else{var h=e.left-s,d=e.left+s+i;ha.right&&(n.left=a.left+a.width-d)}return n},m.prototype.getTitle=function(){var t=this.$element,e=this.options;return t.attr("data-original-title")||("function"==typeof e.title?e.title.call(t[0]):e.title)},m.prototype.getUID=function(t){for(;t+=~~(1e6*Math.random()),document.getElementById(t););return t},m.prototype.tip=function(){if(!this.$tip&&(this.$tip=g(this.options.template),1!=this.$tip.length))throw new Error(this.type+" `template` option must consist of exactly 1 top-level element!");return this.$tip},m.prototype.arrow=function(){return this.$arrow=this.$arrow||this.tip().find(".tooltip-arrow")},m.prototype.enable=function(){this.enabled=!0},m.prototype.disable=function(){this.enabled=!1},m.prototype.toggleEnabled=function(){this.enabled=!this.enabled},m.prototype.toggle=function(t){var e=this;t&&((e=g(t.currentTarget).data("bs."+this.type))||(e=new this.constructor(t.currentTarget,this.getDelegateOptions()),g(t.currentTarget).data("bs."+this.type,e))),t?(e.inState.click=!e.inState.click,e.isInStateTrue()?e.enter(e):e.leave(e)):e.tip().hasClass("in")?e.leave(e):e.enter(e)},m.prototype.destroy=function(){var t=this;clearTimeout(this.timeout),this.hide(function(){t.$element.off("."+t.type).removeData("bs."+t.type),t.$tip&&t.$tip.detach(),t.$tip=null,t.$arrow=null,t.$viewport=null,t.$element=null})},m.prototype.sanitizeHtml=function(t){return n(t,this.options.whiteList,this.options.sanitizeFn)};var e=g.fn.tooltip;g.fn.tooltip=function i(o){return this.each(function(){var t=g(this),e=t.data("bs.tooltip"),i="object"==typeof o&&o;!e&&/destroy|hide/.test(o)||(e||t.data("bs.tooltip",e=new m(this,i)),"string"==typeof o&&e[o]())})},g.fn.tooltip.Constructor=m,g.fn.tooltip.noConflict=function(){return g.fn.tooltip=e,this}}(jQuery),function(n){"use strict";var s=function(t,e){this.init("popover",t,e)};if(!n.fn.tooltip)throw new Error("Popover requires tooltip.js");s.VERSION="3.4.1",s.DEFAULTS=n.extend({},n.fn.tooltip.Constructor.DEFAULTS,{placement:"right",trigger:"click",content:"",template:''}),((s.prototype=n.extend({},n.fn.tooltip.Constructor.prototype)).constructor=s).prototype.getDefaults=function(){return s.DEFAULTS},s.prototype.setContent=function(){var t=this.tip(),e=this.getTitle(),i=this.getContent();if(this.options.html){var o=typeof i;this.options.sanitize&&(e=this.sanitizeHtml(e),"string"===o&&(i=this.sanitizeHtml(i))),t.find(".popover-title").html(e),t.find(".popover-content").children().detach().end()["string"===o?"html":"append"](i)}else t.find(".popover-title").text(e),t.find(".popover-content").children().detach().end().text(i);t.removeClass("fade top bottom left right in"),t.find(".popover-title").html()||t.find(".popover-title").hide()},s.prototype.hasContent=function(){return this.getTitle()||this.getContent()},s.prototype.getContent=function(){var t=this.$element,e=this.options;return t.attr("data-content")||("function"==typeof e.content?e.content.call(t[0]):e.content)},s.prototype.arrow=function(){return this.$arrow=this.$arrow||this.tip().find(".arrow")};var t=n.fn.popover;n.fn.popover=function e(o){return this.each(function(){var t=n(this),e=t.data("bs.popover"),i="object"==typeof o&&o;!e&&/destroy|hide/.test(o)||(e||t.data("bs.popover",e=new s(this,i)),"string"==typeof o&&e[o]())})},n.fn.popover.Constructor=s,n.fn.popover.noConflict=function(){return n.fn.popover=t,this}}(jQuery),function(s){"use strict";function n(t,e){this.$body=s(document.body),this.$scrollElement=s(t).is(document.body)?s(window):s(t),this.options=s.extend({},n.DEFAULTS,e),this.selector=(this.options.target||"")+" .nav li > a",this.offsets=[],this.targets=[],this.activeTarget=null,this.scrollHeight=0,this.$scrollElement.on("scroll.bs.scrollspy",s.proxy(this.process,this)),this.refresh(),this.process()}function e(o){return this.each(function(){var t=s(this),e=t.data("bs.scrollspy"),i="object"==typeof o&&o;e||t.data("bs.scrollspy",e=new n(this,i)),"string"==typeof o&&e[o]()})}n.VERSION="3.4.1",n.DEFAULTS={offset:10},n.prototype.getScrollHeight=function(){return this.$scrollElement[0].scrollHeight||Math.max(this.$body[0].scrollHeight,document.documentElement.scrollHeight)},n.prototype.refresh=function(){var t=this,o="offset",n=0;this.offsets=[],this.targets=[],this.scrollHeight=this.getScrollHeight(),s.isWindow(this.$scrollElement[0])||(o="position",n=this.$scrollElement.scrollTop()),this.$body.find(this.selector).map(function(){var t=s(this),e=t.data("target")||t.attr("href"),i=/^#./.test(e)&&s(e);return i&&i.length&&i.is(":visible")&&[[i[o]().top+n,e]]||null}).sort(function(t,e){return t[0]-e[0]}).each(function(){t.offsets.push(this[0]),t.targets.push(this[1])})},n.prototype.process=function(){var t,e=this.$scrollElement.scrollTop()+this.options.offset,i=this.getScrollHeight(),o=this.options.offset+i-this.$scrollElement.height(),n=this.offsets,s=this.targets,a=this.activeTarget;if(this.scrollHeight!=i&&this.refresh(),o<=e)return a!=(t=s[s.length-1])&&this.activate(t);if(a&&e=n[t]&&(n[t+1]===undefined||e .active"),n=i&&r.support.transition&&(o.length&&o.hasClass("fade")||!!e.find("> .fade").length);function s(){o.removeClass("active").find("> .dropdown-menu > .active").removeClass("active").end().find('[data-toggle="tab"]').attr("aria-expanded",!1),t.addClass("active").find('[data-toggle="tab"]').attr("aria-expanded",!0),n?(t[0].offsetWidth,t.addClass("in")):t.removeClass("fade"),t.parent(".dropdown-menu").length&&t.closest("li.dropdown").addClass("active").end().find('[data-toggle="tab"]').attr("aria-expanded",!0),i&&i()}o.length&&n?o.one("bsTransitionEnd",s).emulateTransitionEnd(a.TRANSITION_DURATION):s(),o.removeClass("in")};var t=r.fn.tab;r.fn.tab=e,r.fn.tab.Constructor=a,r.fn.tab.noConflict=function(){return r.fn.tab=t,this};var i=function(t){t.preventDefault(),e.call(r(this),"show")};r(document).on("click.bs.tab.data-api",'[data-toggle="tab"]',i).on("click.bs.tab.data-api",'[data-toggle="pill"]',i)}(jQuery),function(l){"use strict";var h=function(t,e){this.options=l.extend({},h.DEFAULTS,e);var i=this.options.target===h.DEFAULTS.target?l(this.options.target):l(document).find(this.options.target);this.$target=i.on("scroll.bs.affix.data-api",l.proxy(this.checkPosition,this)).on("click.bs.affix.data-api",l.proxy(this.checkPositionWithEventLoop,this)),this.$element=l(t),this.affixed=null,this.unpin=null,this.pinnedOffset=null,this.checkPosition()};function i(o){return this.each(function(){var t=l(this),e=t.data("bs.affix"),i="object"==typeof o&&o;e||t.data("bs.affix",e=new h(this,i)),"string"==typeof o&&e[o]()})}h.VERSION="3.4.1",h.RESET="affix affix-top affix-bottom",h.DEFAULTS={offset:0,target:window},h.prototype.getState=function(t,e,i,o){var n=this.$target.scrollTop(),s=this.$element.offset(),a=this.$target.height();if(null!=i&&"top"==this.affixed)return nd&&c(b[d],d)!==!1;d++);},a.arr.get=function(b,c){var d=[];if(!a.is.array(b))return d;if(!a.is.fn(c))return b;for(var e=0,f=b.length;f>e;e++)c(b[e],e)&&d.push(b[e]);return d},a.arr.any=function(c,d){if(!a.is.array(c))return!1;d=a.is.fn(d)?d:b;for(var e=0,f=c.length;f>e;e++)if(d(c[e],e))return!0;return!1},a.arr.contains=function(b,c){if(!a.is.array(b)||a.is.undef(c))return!1;for(var d=0,e=b.length;e>d;d++)if(b[d]==c)return!0;return!1},a.arr.first=function(c,d){if(!a.is.array(c))return null;d=a.is.fn(d)?d:b;for(var e=0,f=c.length;f>e;e++)if(d(c[e],e))return c[e];return null},a.arr.map=function(b,c){var d=[],e=null;if(!a.is.array(b)||!a.is.fn(c))return d;for(var f=0,g=b.length;g>f;f++)null!=(e=c(b[f],f))&&d.push(e);return d},a.arr.remove=function(b,c){var d=[],e=[];if(!a.is.array(b)||!a.is.fn(c))return e;for(var f=0,g=b.length;g>f;f++)c(b[f],f,e)&&(d.push(f),e.push(b[f]));for(d.sort(function(a,b){return b-a}),f=0,g=d.length;g>f;f++){var h=d[f]-f;b.splice(h,1)}return e},a.arr["delete"]=function(b,c){var d=-1,e=null;if(!a.is.array(b)||a.is.undef(c))return e;for(var f=0,g=b.length;g>f;f++)if(b[f]==c){d=f,e=b[f];break}return-1!=d&&b.splice(d,1),e},a.arr.replace=function(a,b,c){var d=a.indexOf(b);-1!==d&&(a[d]=c)}}(FooTable),function(a){a.is={},a.is.type=function(a,b){return typeof a===b},a.is.defined=function(a){return"undefined"!=typeof a},a.is.undef=function(a){return"undefined"==typeof a},a.is.array=function(a){return"[object Array]"===Object.prototype.toString.call(a)},a.is.date=function(a){return"[object Date]"===Object.prototype.toString.call(a)&&!isNaN(a.getTime())},a.is["boolean"]=function(a){return"[object Boolean]"===Object.prototype.toString.call(a)},a.is.string=function(a){return"[object String]"===Object.prototype.toString.call(a)},a.is.number=function(a){return"[object Number]"===Object.prototype.toString.call(a)&&!isNaN(a)},a.is.fn=function(b){return a.is.defined(window)&&b===window.alert||"[object Function]"===Object.prototype.toString.call(b)},a.is.error=function(a){return"[object Error]"===Object.prototype.toString.call(a)},a.is.object=function(a){return"[object Object]"===Object.prototype.toString.call(a)},a.is.hash=function(b){return a.is.object(b)&&b.constructor===Object&&!b.nodeType&&!b.setInterval},a.is.element=function(a){return"object"==typeof HTMLElement?a instanceof HTMLElement:a&&"object"==typeof a&&null!==a&&1===a.nodeType&&"string"==typeof a.nodeName},a.is.promise=function(b){return a.is.object(b)&&a.is.fn(b.then)&&a.is.fn(b.promise)},a.is.jq=function(b){return a.is.defined(window.jQuery)&&b instanceof jQuery&&b.length>0},a.is.moment=function(b){return a.is.defined(window.moment)&&a.is.object(b)&&a.is["boolean"](b._isAMomentObject)},a.is.emptyObject=function(b){if(!a.is.hash(b))return!1;for(var c in b)if(b.hasOwnProperty(c))return!1;return!0},a.is.emptyArray=function(b){return a.is.array(b)?0===b.length:!0},a.is.emptyString=function(b){return a.is.string(b)?0===b.length:!0}}(FooTable),function(a){a.str={},a.str.contains=function(b,c,d){return a.is.emptyString(b)||a.is.emptyString(c)?!1:c.length<=b.length&&-1!==(d?b.toUpperCase().indexOf(c.toUpperCase()):b.indexOf(c))},a.str.containsExact=function(b,c,d){return a.is.emptyString(b)||a.is.emptyString(c)||c.length>b.length?!1:new RegExp("\\b"+a.str.escapeRegExp(c)+"\\b",d?"i":"").test(b)},a.str.containsWord=function(b,c,d){if(a.is.emptyString(b)||a.is.emptyString(c)||b.lengthf;f++)if(d?e[f].toUpperCase()==c.toUpperCase():e[f]==c)return!0;return!1},a.str.from=function(b,c){return a.is.emptyString(b)?b:a.str.contains(b,c)?b.substring(b.indexOf(c)+1):b},a.str.startsWith=function(b,c){return a.is.emptyString(b)?b==c:b.slice(0,c.length)==c},a.str.toCamelCase=function(b){return a.is.emptyString(b)?b:b.toUpperCase()===b?b.toLowerCase():b.replace(/^([A-Z])|[-\s_](\w)/g,function(b,c,d){return a.is.string(d)?d.toUpperCase():c.toLowerCase()})},a.str.random=function(b){return b=a.is.emptyString(b)?"":b,b+Math.random().toString(36).substr(2,9)},a.str.escapeRegExp=function(b){return a.is.emptyString(b)?b:b.replace(/[.*+?^${}()|[\]\\]/g,"\\$&")}}(FooTable),function(a){"use strict";function b(){}Object.create||(Object.create=function(){var b=function(){};return function(c){if(arguments.length>1)throw Error("Second argument not supported");if(!a.is.object(c))throw TypeError("Argument must be an object");b.prototype=c;var d=new b;return b.prototype=null,d}}());var c=/xyz/.test(function(){xyz})?/\b_super\b/:/.*/;b.__extend__=function(b,d,e,f){b[d]=a.is.fn(f)&&c.test(e)?function(a,b){return function(){var a,c;return a=this._super,this._super=f,c=b.apply(this,arguments),this._super=a,c}}(d,e):e},b.extend=function(d,e){function f(b,d,e,f){b[d]=a.is.fn(f)&&c.test(e)?function(a,b,c){return function(){var a,d;return a=this._super,this._super=c,d=b.apply(this,arguments),this._super=a,d}}(d,e,f):e}var g=Array.prototype.slice.call(arguments);if(d=g.shift(),e=g.shift(),a.is.hash(d)){var h=Object.create(this.prototype),i=this.prototype;for(var j in d)"__ctor__"!==j&&f(h,j,d[j],i[j]);var k=a.is.fn(h.__ctor__)?h.__ctor__:function(){if(!a.is.fn(this.construct))throw new SyntaxError('FooTable class objects must be constructed with the "new" keyword.');this.construct.apply(this,arguments)};return h.construct=a.is.fn(h.construct)?h.construct:function(){},k.prototype=h,h.constructor=k,k.extend=b.extend,k}a.is.string(d)&&a.is.fn(e)&&f(this.prototype,d,e,this.prototype[d])},a.Class=b,a.ClassFactory=a.Class.extend({construct:function(){this.registered={}},contains:function(b){return a.is.defined(this.registered[b])},names:function(){var a,b=[];for(a in this.registered)this.registered.hasOwnProperty(a)&&b.push(a);return b},register:function(b,c,d){if(a.is.string(b)&&a.is.fn(c)){var e=this.registered[b];this.registered[b]={name:b,klass:c,priority:a.is.number(d)?d:a.is.defined(e)?e.priority:0}}},load:function(b,c,d){var e,f,g=this,h=Array.prototype.slice.call(arguments),i=[],j=[];b=h.shift()||{};for(e in g.registered)if(g.registered.hasOwnProperty(e)){var k=g.registered[e];b.hasOwnProperty(e)&&(f=b[e],a.is.string(f)&&(f=a.getFnPointer(b[e])),a.is.fn(f)&&(k={name:e,klass:f,priority:g.registered[e].priority})),i.push(k)}for(e in b)b.hasOwnProperty(e)&&!g.registered.hasOwnProperty(e)&&(f=b[e],a.is.string(f)&&(f=a.getFnPointer(b[e])),a.is.fn(f)&&i.push({name:e,klass:f,priority:0}));return i.sort(function(a,b){return b.priority-a.priority}),a.arr.each(i,function(b){a.is.fn(b.klass)&&j.push(g._make(b.klass,h))}),j},make:function(b,c,d){var e,f=this,g=Array.prototype.slice.call(arguments);return b=g.shift(),e=f.registered[b],a.is.fn(e.klass)?f._make(e.klass,g):null},_make:function(a,b){function c(){return a.apply(this,b)}return c.prototype=a.prototype,new c}})}(FooTable),function(a,b){b.css2json=function(c){if(b.is.emptyString(c))return{};for(var d,e,f,g={},h=c.split(";"),i=0,j=h.length;j>i;i++)b.is.emptyString(h[i])||(d=h[i].split(":"),b.is.emptyString(d[0])||b.is.emptyString(d[1])||(e=b.str.toCamelCase(a.trim(d[0])),f=a.trim(d[1]),g[e]=f));return g},b.getFnPointer=function(a){if(b.is.emptyString(a))return null;var c=window,d=a.split(".");return b.arr.each(d,function(a){c[a]&&(c=c[a])}),b.is.fn(c)?c:null},b.checkFnValue=function(a,c,d){function e(a,c,d){return b.is.fn(c)?function(){return c.apply(a,arguments)}:d}return d=b.is.fn(d)?d:null,b.is.fn(c)?e(a,c,d):b.is.type(c,"string")?e(a,b.getFnPointer(c),d):d}}(jQuery,FooTable),function(a,b){b.Cell=b.Class.extend({construct:function(a,b,c,d){this.ft=a,this.row=b,this.column=c,this.created=!1,this.define(d)},define:function(c){this.$el=b.is.element(c)||b.is.jq(c)?a(c):null,this.$detail=null;var d=b.is.hash(c)&&b.is.hash(c.options)&&b.is.defined(c.value);this.value=this.column.parser.call(this.column,b.is.jq(this.$el)?this.$el:d?c.value:c,this.ft.o),this.o=a.extend(!0,{classes:null,style:null},d?c.options:{}),this.classes=b.is.jq(this.$el)&&this.$el.attr("class")?this.$el.attr("class").match(/\S+/g):b.is.array(this.o.classes)?this.o.classes:b.is.string(this.o.classes)?this.o.classes.match(/\S+/g):[],this.style=b.is.jq(this.$el)&&this.$el.attr("style")?b.css2json(this.$el.attr("style")):b.is.hash(this.o.style)?this.o.style:b.is.string(this.o.style)?b.css2json(this.o.style):{}},$create:function(){this.created||((this.$el=b.is.jq(this.$el)?this.$el:a("")).data("value",this.value).contents().detach().end().append(this.format(this.value)),this._setClasses(this.$el),this._setStyle(this.$el),this.$detail=a("").addClass(this.row.classes.join(" ")).data("__FooTableCell__",this).append(a("")).append(a("")),this.created=!0)},collapse:function(){this.created&&(this.$detail.children("th").html(this.column.title),this.$el.clone().attr("id",this.$el.attr("id")?this.$el.attr("id")+"-detail":void 0).css("display","table-cell").html("").append(this.$el.contents().detach()).replaceAll(this.$detail.children("td").first()),b.is.jq(this.$detail.parent())||this.$detail.appendTo(this.row.$details.find(".footable-details > tbody")))},restore:function(){if(this.created){if(b.is.jq(this.$detail.parent())){var a=this.$detail.children("td").first();this.$el.attr("class",a.attr("class")).attr("style",a.attr("style")).css("display",this.column.hidden||!this.column.visible?"none":"table-cell").append(a.contents().detach())}this.$detail.detach()}},parse:function(){return this.column.parser.call(this.column,this.$el,this.ft.o)},format:function(a){return this.column.formatter.call(this.column,a,this.ft.o,this.row.value)},val:function(c,d,e){if(b.is.undef(c))return this.value;var f=this,g=b.is.hash(c)&&b.is.hash(c.options)&&b.is.defined(c.value);if(this.o=a.extend(!0,{classes:f.classes,style:f.style},g?c.options:{}),this.value=g?c.value:c,this.classes=b.is.array(this.o.classes)?this.o.classes:b.is.string(this.o.classes)?this.o.classes.match(/\S+/g):[],this.style=b.is.hash(this.o.style)?this.o.style:b.is.string(this.o.style)?b.css2json(this.o.style):{},e=b.is["boolean"](e)?e:!0,this.created&&e){this.$el.data("value",this.value).empty();var h=this.$detail.children("td").first().empty(),i=b.is.jq(this.$detail.parent())?h:this.$el;i.append(this.format(this.value)),this._setClasses(i),this._setStyle(i),(b.is["boolean"](d)?d:!0)&&this.row.draw()}},_setClasses:function(a){var c=!b.is.emptyArray(this.column.classes),d=!b.is.emptyArray(this.classes),e=null;a.removeAttr("class"),(c||d)&&(c&&d?e=this.classes.concat(this.column.classes).join(" "):c?e=this.column.classes.join(" "):d&&(e=this.classes.join(" ")),b.is.emptyString(e)||a.addClass(e))},_setStyle:function(c){var d=!b.is.emptyObject(this.column.style),e=!b.is.emptyObject(this.style),f=null;c.removeAttr("style"),(d||e)&&(d&&e?f=a.extend({},this.column.style,this.style):d?f=this.column.style:e&&(f=this.style),b.is.hash(f)&&c.css(f))}})}(jQuery,FooTable),function(a,b){b.Column=b.Class.extend({construct:function(a,c,d){this.ft=a,this.type=b.is.emptyString(d)?"text":d,this.virtual=b.is["boolean"](c.virtual)?c.virtual:!1,this.$el=b.is.jq(c.$el)?c.$el:null,this.index=b.is.number(c.index)?c.index:-1,this.internal=!1,this.define(c),this.$create()},define:function(a){this.hidden=b.is["boolean"](a.hidden)?a.hidden:!1,this.visible=b.is["boolean"](a.visible)?a.visible:!0,this.name=b.is.string(a.name)?a.name:null,null==this.name&&(this.name="col"+(a.index+1)),this.title=b.is.string(a.title)?a.title:null,!this.virtual&&null==this.title&&b.is.jq(this.$el)&&(this.title=this.$el.html()),null==this.title&&(this.title="Column "+(a.index+1)),this.style=b.is.hash(a.style)?a.style:b.is.string(a.style)?b.css2json(a.style):{},this.classes=b.is.array(a.classes)?a.classes:b.is.string(a.classes)?a.classes.match(/\S+/g):[],this.parser=b.checkFnValue(this,a.parser,this.parser),this.formatter=b.checkFnValue(this,a.formatter,this.formatter)},$create:function(){(this.$el=!this.virtual&&b.is.jq(this.$el)?this.$el:a("")).html(this.title).addClass(this.classes.join(" ")).css(this.style)},parser:function(c){if(b.is.element(c)||b.is.jq(c)){var d=a(c).data("value");return b.is.defined(d)?d:a(c).html()}return b.is.defined(c)&&null!=c?c+"":null},formatter:function(a,b,c){return null==a?"":a},createCell:function(a){var c=b.is.jq(a.$el)?a.$el.children("td,th").get(this.index):null,d=b.is.hash(a.value)?a.value[this.name]:null;return new b.Cell(this.ft,a,this,c||d)}}),b.columns=new b.ClassFactory,b.columns.register("text",b.Column)}(jQuery,FooTable),function(a,b){b.Component=b.Class.extend({construct:function(a,c){if(!(a instanceof b.Table))throw new TypeError("The instance parameter must be an instance of FooTable.Table.");this.ft=a,this.enabled=b.is["boolean"](c)?c:!1},preinit:function(a){},init:function(){},destroy:function(){},predraw:function(){},draw:function(){},postdraw:function(){}}),b.components=new b.ClassFactory}(jQuery,FooTable),function(a,b){b.Defaults=function(){this.stopPropagation=!1,this.on=null},b.defaults=new b.Defaults}(jQuery,FooTable),function(a,b){b.Row=b.Class.extend({construct:function(a,b,c){this.ft=a,this.columns=b,this.created=!1,this.define(c)},define:function(c){this.$el=b.is.element(c)||b.is.jq(c)?a(c):null,this.$toggle=a("",{"class":"footable-toggle fooicon fooicon-plus"});var d=b.is.hash(c),e=d&&b.is.hash(c.options)&&b.is.hash(c.value);this.value=d?e?c.value:c:null,this.o=a.extend(!0,{expanded:!1,classes:null,style:null},e?c.options:{}),this.expanded=b.is.jq(this.$el)?this.$el.data("expanded")||this.o.expanded:this.o.expanded,this.classes=b.is.jq(this.$el)&&this.$el.attr("class")?this.$el.attr("class").match(/\S+/g):b.is.array(this.o.classes)?this.o.classes:b.is.string(this.o.classes)?this.o.classes.match(/\S+/g):[],this.style=b.is.jq(this.$el)&&this.$el.attr("style")?b.css2json(this.$el.attr("style")):b.is.hash(this.o.style)?this.o.style:b.is.string(this.o.style)?b.css2json(this.o.style):{},this.cells=this.createCells();var f=this;f.value={},b.arr.each(f.cells,function(a){f.value[a.column.name]=a.val()})},$create:function(){if(!this.created){(this.$el=b.is.jq(this.$el)?this.$el:a("")).data("__FooTableRow__",this),this._setClasses(this.$el),this._setStyle(this.$el),"last"==this.ft.rows.toggleColumn&&this.$toggle.addClass("last-column"),this.$details=a("",{"class":"footable-detail-row"}).append(a("",{colspan:this.ft.columns.visibleColspan}).append(a("",{"class":"footable-details "+this.ft.classes.join(" ")}).append("")));var c=this;b.arr.each(c.cells,function(a){a.created||a.$create(),c.$el.append(a.$el)}),c.$el.off("click.ft.row").on("click.ft.row",{self:c},c._onToggle),this.created=!0}},createCells:function(){var a=this;return b.arr.map(a.columns,function(b){return b.createCell(a)})},val:function(c,d,e){var f=this;if(!b.is.hash(c))return b.is.hash(this.value)&&!b.is.emptyObject(this.value)||(this.value={},b.arr.each(this.cells,function(a){a.column.internal||(f.value[a.column.name]=a.val())})),this.value;this.collapse(!1);var g=b.is.hash(c),h=g&&b.is.hash(c.options)&&b.is.hash(c.value);if(this.o=a.extend(!0,{expanded:f.expanded,classes:f.classes,style:f.style},h?c.options:{}),this.expanded=this.o.expanded,this.classes=b.is.array(this.o.classes)?this.o.classes:b.is.string(this.o.classes)?this.o.classes.match(/\S+/g):[],this.style=b.is.hash(this.o.style)?this.o.style:b.is.string(this.o.style)?b.css2json(this.o.style):{},g)if(h&&(c=c.value),b.is.hash(this.value))for(var i in c)c.hasOwnProperty(i)&&(this.value[i]=c[i]);else this.value=c;else this.value=null;e=b.is["boolean"](e)?e:!0,b.arr.each(this.cells,function(a){!a.column.internal&&b.is.defined(f.value[a.column.name])&&a.val(f.value[a.column.name],!1,e)}),this.created&&e&&(this._setClasses(this.$el),this._setStyle(this.$el),(b.is["boolean"](d)?d:!0)&&this.draw())},_setClasses:function(a){var c=!b.is.emptyArray(this.classes),d=null;a.removeAttr("class"),c&&(d=this.classes.join(" "),b.is.emptyString(d)||a.addClass(d))},_setStyle:function(a){var c=!b.is.emptyObject(this.style),d=null;a.removeAttr("style"),c&&(d=this.style,b.is.hash(d)&&a.css(d))},expand:function(){if(this.created){var a=this;a.ft.raise("expand.ft.row",[a]).then(function(){a.__hidden__=b.arr.map(a.cells,function(a){return a.column.hidden&&a.column.visible?a:null}),a.__hidden__.length>0&&(a.$details.insertAfter(a.$el).children("td").first().attr("colspan",a.ft.columns.visibleColspan),b.arr.each(a.__hidden__,function(a){a.collapse()})),a.$el.attr("data-expanded",!0),a.$toggle.removeClass("fooicon-plus").addClass("fooicon-minus"),a.expanded=!0,a.ft.raise("expanded.ft.row",[a])})}},collapse:function(a){if(this.created){var c=this;c.ft.raise("collapse.ft.row",[c]).then(function(){b.arr.each(c.__hidden__,function(a){a.restore()}),c.$details.detach(),c.$el.removeAttr("data-expanded"),c.$toggle.removeClass("fooicon-minus").addClass("fooicon-plus"),(b.is["boolean"](a)?a:!0)&&(c.expanded=!1),c.ft.raise("collapsed.ft.row",[c])})}},predraw:function(a){this.created&&(this.expanded&&this.collapse(!1),this.$toggle.detach(),a=b.is["boolean"](a)?a:!0,a&&this.$el.detach())},draw:function(a){this.created||this.$create(),b.is.jq(a)&&a.append(this.$el);var c=this;b.arr.each(c.cells,function(a){a.$el.css("display",a.column.hidden||!a.column.visible?"none":"table-cell"),c.ft.rows.showToggle&&c.ft.columns.hasHidden&&("first"==c.ft.rows.toggleColumn&&a.column.index==c.ft.columns.firstVisibleIndex||"last"==c.ft.rows.toggleColumn&&a.column.index==c.ft.columns.lastVisibleIndex)&&a.$el.prepend(c.$toggle),a.$el.add(a.column.$el).removeClass("footable-first-visible footable-last-visible"),a.column.index==c.ft.columns.firstVisibleIndex&&a.$el.add(a.column.$el).addClass("footable-first-visible"),a.column.index==c.ft.columns.lastVisibleIndex&&a.$el.add(a.column.$el).addClass("footable-last-visible")}),this.expanded&&this.expand()},toggle:function(){this.created&&this.ft.columns.hasHidden&&(this.expanded?this.collapse():this.expand())},_onToggle:function(b){var c=b.data.self;a(b.target).is(c.ft.rows.toggleSelector)&&c.toggle()}})}(jQuery,FooTable),function(a,b){b.instances=[],b.Table=b.Class.extend({construct:function(c,d,e){this._resizeTimeout=null,this.id=b.instances.push(this),this.initialized=!1,this.$el=(b.is.jq(c)?c:a(c)).first(),this.$loader=a("
",{"class":"footable-loader"}).append(a("",{"class":"fooicon fooicon-loader"})),this.o=a.extend(!0,{},b.defaults,d),this.data=this.$el.data()||{},this.classes=[],this.components=b.components.load(b.is.hash(this.data.components)?this.data.components:this.o.components,this),this.breakpoints=this.use(FooTable.Breakpoints),this.columns=this.use(FooTable.Columns),this.rows=this.use(FooTable.Rows),this._construct(e)},_construct:function(a){var c=this;return this._preinit().then(function(){return c._init().then(function(){return c.raise("ready.ft.table").then(function(){b.is.fn(a)&&a.call(c,c)})})}).always(function(a){c.$el.show(),b.is.error(a)&&console.error("FooTable: unhandled error thrown during initialization.",a)})},_preinit:function(){var a=this;return this.raise("preinit.ft.table",[a.data]).then(function(){var c=(a.$el.attr("class")||"").match(/\S+/g)||[];a.o.ajax=b.checkFnValue(a,a.data.ajax,a.o.ajax),a.o.stopPropagation=b.is["boolean"](a.data.stopPropagation)?a.data.stopPropagation:a.o.stopPropagation;for(var d=0,e=c.length;e>d;d++)b.str.startsWith(c[d],"footable")||a.classes.push(c[d]);return a.$el.hide().after(a.$loader),a.execute(!1,!1,"preinit",a.data)})},_init:function(){var c=this;return c.raise("init.ft.table").then(function(){var d=c.$el.children("thead"),e=c.$el.children("tbody"),f=c.$el.children("tfoot");return c.$el.addClass("footable footable-"+c.id),b.is.hash(c.o.on)&&c.$el.on(c.o.on),0==f.length&&c.$el.append(f=a("
")),0==e.length&&c.$el.append(""),0==d.length&&c.$el.prepend(d=a("")),c.execute(!1,!0,"init").then(function(){return c.$el.data("__FooTable__",c),0==f.children("tr").length&&f.remove(),0==d.children("tr").length&&d.remove(),c.raise("postinit.ft.table").then(function(){return c.draw()}).always(function(){a(window).off("resize.ft"+c.id,c._onWindowResize).on("resize.ft"+c.id,{self:c},c._onWindowResize),c.initialized=!0})})})},destroy:function(){var c=this;return c.raise("destroy.ft.table").then(function(){return c.execute(!0,!0,"destroy").then(function(){c.$el.removeData("__FooTable__").removeClass("footable-"+c.id),b.is.hash(c.o.on)&&c.$el.off(c.o.on),a(window).off("resize.ft"+c.id,c._onWindowResize),c.initialized=!1,b.instances[c.id]=null})}).fail(function(a){b.is.error(a)&&console.error("FooTable: unhandled error thrown while destroying the plugin.",a)})},raise:function(c,d){var e=this,f=b.__debug__&&(b.is.emptyArray(b.__debug_options__.events)||b.arr.any(b.__debug_options__.events,function(a){return b.str.contains(c,a)}));return d=d||[],d.unshift(this),a.Deferred(function(b){var g=a.Event(c);1==e.o.stopPropagation&&e.$el.one(c,function(a){a.stopPropagation()}),f&&console.log("FooTable:"+c+": ",d),e.$el.trigger(g,d),g.isDefaultPrevented()?(f&&console.log('FooTable: default prevented for the "'+c+'" event.'),b.reject(g)):b.resolve(g)})},use:function(a){for(var b=0,c=this.components.length;c>b;b++)if(this.components[b]instanceof a)return this.components[b];return null},draw:function(){var a=this,c=a.$el.clone().insertBefore(a.$el);return a.$el.detach(),a.execute(!1,!0,"predraw").then(function(){return a.raise("predraw.ft.table").then(function(){return a.execute(!1,!0,"draw").then(function(){return a.raise("draw.ft.table").then(function(){return a.execute(!1,!0,"postdraw").then(function(){return a.raise("postdraw.ft.table")})})})})}).fail(function(a){b.is.error(a)&&console.error("FooTable: unhandled error thrown during a draw operation.",a)}).always(function(){c.replaceWith(a.$el),a.$loader.remove()})},execute:function(a,c,d,e,f){var g=this,h=Array.prototype.slice.call(arguments);a=h.shift(),c=h.shift();var i=c?b.arr.get(g.components,function(a){return a.enabled}):g.components.slice(0);return h.unshift(a?i.reverse():i),g._execute.apply(g,h)},_execute:function(c,d,e,f){if(!c||!c.length)return a.when();var g,h=this,i=Array.prototype.slice.call(arguments);return c=i.shift(),d=i.shift(),g=c.shift(),b.is.fn(g[d])?a.Deferred(function(a){try{var c=g[d].apply(g,i);if(b.is.promise(c))return c.then(a.resolve,a.reject);a.resolve(c)}catch(e){a.reject(e)}}).then(function(){return h._execute.apply(h,[c,d].concat(i))}):h._execute.apply(h,[c,d].concat(i))},_onWindowResize:function(a){var b=a.data.self;null!=b._resizeTimeout&&clearTimeout(b._resizeTimeout),b._resizeTimeout=setTimeout(function(){b._resizeTimeout=null,b.raise("resize.ft.table").then(function(){b.breakpoints.check()})},300)}})}(jQuery,FooTable),function(a,b){b.ArrayColumn=b.Column.extend({construct:function(a,b){this._super(a,b,"array")},parser:function(c){if(b.is.element(c)||b.is.jq(c)){var d=a(c),e=d.data("value");if(b.is.array(e))return e;e=d.html();try{e=JSON.parse(e)}catch(f){e=null}return b.is.array(e)?e:null}return b.is.array(c)?c:null},formatter:function(a,c,d){return b.is.array(a)?JSON.stringify(a):""}}),b.columns.register("array",b.ArrayColumn)}(jQuery,FooTable),function(a,b){b.is.undef(window.moment)||(b.DateColumn=b.Column.extend({construct:function(a,c){this._super(a,c,"date"),this.formatString=b.is.string(c.formatString)?c.formatString:"MM-DD-YYYY"},parser:function(c){if(b.is.element(c)||b.is.jq(c)){var d=a(c).data("value");c=b.is.defined(d)?d:a(c).text(),b.is.string(c)&&(c=isNaN(c)?c:+c)}if(b.is.date(c))return moment(c);if(b.is.object(c)&&b.is["boolean"](c._isAMomentObject))return c;if(b.is.string(c)){if(isNaN(c))return moment(c,this.formatString);c=+c}return b.is.number(c)?moment(c):null},formatter:function(a,c,d){return b.is.object(a)&&b.is["boolean"](a._isAMomentObject)&&a.isValid()?a.format(this.formatString):""},filterValue:function(c){if((b.is.element(c)||b.is.jq(c))&&(c=a(c).data("filterValue")||a(c).text()),b.is.hash(c)&&b.is.hash(c.options)&&(b.is.string(c.options.filterValue)&&(c=c.options.filterValue),b.is.defined(c.value)&&(c=c.value)),b.is.object(c)&&b.is["boolean"](c._isAMomentObject))return c.format(this.formatString);if(b.is.string(c)){if(isNaN(c))return c;c=+c}return b.is.number(c)||b.is.date(c)?moment(c).format(this.formatString):b.is.defined(c)&&null!=c?c+"":""}}),b.columns.register("date",b.DateColumn))}(jQuery,FooTable),function(a,b){b.HTMLColumn=b.Column.extend({construct:function(a,b){this._super(a,b,"html")},parser:function(c){if(b.is.string(c)&&(c=a(a.trim(c))),b.is.element(c)&&(c=a(c)),b.is.jq(c)){var d=c.prop("tagName").toLowerCase();if("td"==d||"th"==d){var e=c.data("value");return b.is.defined(e)?e:c.contents()}return c}return null}}),b.columns.register("html",b.HTMLColumn)}(jQuery,FooTable),function(a,b){b.NumberColumn=b.Column.extend({construct:function(a,c){this._super(a,c,"number"),this.decimalSeparator=b.is.string(c.decimalSeparator)?c.decimalSeparator:".",this.thousandSeparator=b.is.string(c.thousandSeparator)?c.thousandSeparator:",",this.decimalSeparatorRegex=new RegExp(b.str.escapeRegExp(this.decimalSeparator),"g"),this.thousandSeparatorRegex=new RegExp(b.str.escapeRegExp(this.thousandSeparator),"g"),this.cleanRegex=new RegExp("[^-0-9"+b.str.escapeRegExp(this.decimalSeparator)+"]","g")},parser:function(c){if(b.is.element(c)||b.is.jq(c)){var d=a(c).data("value");c=b.is.defined(d)?d:a(c).text().replace(this.cleanRegex,"")}return b.is.string(c)&&(c=c.replace(this.thousandSeparatorRegex,"").replace(this.decimalSeparatorRegex,"."),c=parseFloat(c)),b.is.number(c)?c:null},formatter:function(a,b,c){if(null==a)return"";var d=(a+"").split(".");return 2==d.length&&d[0].length>3&&(d[0]=d[0].replace(/\B(?=(?:\d{3})+(?!\d))/g,this.thousandSeparator)),d.join(this.decimalSeparator)}}),b.columns.register("number",b.NumberColumn)}(jQuery,FooTable),function(a,b){b.ObjectColumn=b.Column.extend({construct:function(a,b){this._super(a,b,"object")},parser:function(c){if(b.is.element(c)||b.is.jq(c)){var d=a(c),e=d.data("value");if(b.is.object(e))return e;e=d.html();try{e=JSON.parse(e)}catch(f){e=null}return b.is.object(e)?e:null}return b.is.object(c)?c:null},formatter:function(a,c,d){return b.is.object(a)?JSON.stringify(a):""}}),b.columns.register("object",b.ObjectColumn)}(jQuery,FooTable),function(a,b){b.Breakpoint=b.Class.extend({construct:function(a,b){this.name=a,this.width=b}})}(jQuery,FooTable),function(a,b){b.Breakpoints=b.Component.extend({construct:function(a){this._super(a,!0),this.o=a.o,this.current=null,this.array=[],this.cascade=this.o.cascade,this.useParentWidth=this.o.useParentWidth,this.hidden=null,this._classNames="",this.getWidth=b.checkFnValue(this,this.o.getWidth,this.getWidth)},preinit:function(a){var c=this;return this.ft.raise("preinit.ft.breakpoints",[a]).then(function(){c.cascade=b.is["boolean"](a.cascade)?a.cascade:c.cascade,c.o.breakpoints=b.is.hash(a.breakpoints)?a.breakpoints:c.o.breakpoints,c.getWidth=b.checkFnValue(c,a.getWidth,c.getWidth),null==c.o.breakpoints&&(c.o.breakpoints={xs:480,sm:768,md:992,lg:1200});for(var d in c.o.breakpoints)c.o.breakpoints.hasOwnProperty(d)&&(c.array.push(new b.Breakpoint(d,c.o.breakpoints[d])),c._classNames+="breakpoint-"+d+" ");c.array.sort(function(a,b){return b.width-a.width})})},init:function(){var a=this;return this.ft.raise("init.ft.breakpoints").then(function(){a.current=a.get()})},draw:function(){this.ft.$el.removeClass(this._classNames).addClass("breakpoint-"+this.current.name)},calculate:function(){for(var a,c=this,d=null,e=[],f=null,g=c.getWidth(),h=0,i=c.array.length;i>h;h++)a=c.array[h],(!d&&h==i-1||g>=a.width&&(f instanceof b.Breakpoint?gd;d++)if(this.cascade?b.str.containsWord(this.hidden,c[d]):c[d]==this.current.name)return!1;return!0},check:function(){var a=this,c=a.get();c instanceof b.Breakpoint&&c!=a.current&&a.ft.raise("before.ft.breakpoints",[a.current,c]).then(function(){var b=a.current;return a.current=c,a.ft.draw().then(function(){a.ft.raise("after.ft.breakpoints",[a.current,b])})})},get:function(a){return b.is.undef(a)?this.calculate():a instanceof b.Breakpoint?a:b.is.string(a)?b.arr.first(this.array,function(b){return b.name==a}):b.is.number(a)&&a>=0&&af&&(f=a.index)}),f++;for(var g,h,i=0;f>i;i++)g={},b.arr.each(c,function(a){return a.index==i?(g=a,!1):void 0}),h={},b.arr.each(d,function(a){return a.index==i?(h=a,!1):void 0}),e.push(a.extend(!0,{},g,h))}return e}var f,g,h=[],i=[],j=d.ft.$el.find("tr.footable-header, thead > tr:last:has([data-breakpoints]), tbody > tr:first:has([data-breakpoints]), thead > tr:last, tbody > tr:first").first();if(j.length>0){var k=j.parent().is("tbody")&&j.children().length==j.children("td").length;k||(d.$header=j.addClass("footable-header")),j.children("td,th").each(function(b,c){f=a(c),g=f.data(),g.index=b,g.$el=f,g.virtual=k,i.push(g)}),k&&(d.showHeader=!1)}b.is.array(d.o.columns)&&!b.is.emptyArray(d.o.columns)?(b.arr.each(d.o.columns,function(a,b){a.index=b,h.push(a)}),d.parseFinalize(c,e(h,i))):b.is.promise(d.o.columns)?d.o.columns.then(function(a){b.arr.each(a,function(a,b){a.index=b,h.push(a)}),d.parseFinalize(c,e(h,i))},function(a){c.reject(Error("Columns ajax request error: "+a.status+" ("+a.statusText+")"))}):d.parseFinalize(c,e(h,i))})},parseFinalize:function(a,c){var d,e=this,f=[];b.arr.each(c,function(a){(d=b.columns.contains(a.type)?b.columns.make(a.type,e.ft,a):new b.Column(e.ft,a))&&f.push(d)}),b.is.emptyArray(f)?a.reject(Error("No columns supplied.")):(f.sort(function(a,b){ -return a.index-b.index}),a.resolve(f))},preinit:function(a){var c=this;return c.ft.raise("preinit.ft.columns",[a]).then(function(){return c.parse(a).then(function(d){c.array=d,c.showHeader=b.is["boolean"](a.showHeader)?a.showHeader:c.showHeader})})},init:function(){var a=this;return this.ft.raise("init.ft.columns",[a.array]).then(function(){a.$create()})},destroy:function(){var a=this;this.ft.raise("destroy.ft.columns").then(function(){a._fromHTML||a.$header.remove()})},predraw:function(){var a=this,c=!0;a.visibleColspan=0,a.firstVisibleIndex=0,a.lastVisibleIndex=0,a.hasHidden=!1,b.arr.each(a.array,function(b){b.hidden=!a.ft.breakpoints.visible(b.breakpoints),!b.hidden&&b.visible&&(c&&(a.firstVisibleIndex=b.index,c=!1),a.lastVisibleIndex=b.index,a.visibleColspan++),b.hidden&&(a.hasHidden=!0)}),a.ft.$el.toggleClass("breakpoint",a.hasHidden)},draw:function(){b.arr.each(this.array,function(a){a.$el.css("display",a.hidden||!a.visible?"none":"table-cell")}),!this.showHeader&&b.is.jq(this.$header.parent())&&this.$header.detach()},$create:function(){var c=this;c.$header=b.is.jq(c.$header)?c.$header:a("",{"class":"footable-header"}),c.$header.children("th,td").detach(),b.arr.each(c.array,function(a){c.$header.append(a.$el)}),c.showHeader&&!b.is.jq(c.$header.parent())&&c.ft.$el.children("thead").append(c.$header)},get:function(a){return a instanceof b.Column?a:b.is.string(a)?b.arr.first(this.array,function(b){return b.name==a}):b.is.number(a)?b.arr.first(this.array,function(b){return b.index==a}):b.is.fn(a)?b.arr.get(this.array,a):null},ensure:function(a){var c=this,d=[];return b.is.array(a)?(b.arr.each(a,function(a){d.push(c.get(a))}),d):d}}),b.components.register("columns",b.Columns,900)}(jQuery,FooTable),function(a){a.Defaults.prototype.columns=[],a.Defaults.prototype.showHeader=!0}(FooTable),function(a,b){b.Rows=b.Component.extend({construct:function(a){this._super(a,!0),this.o=a.o,this.array=[],this.all=[],this.showToggle=a.o.showToggle,this.toggleSelector=a.o.toggleSelector,this.toggleColumn=a.o.toggleColumn,this.emptyString=a.o.empty,this.expandFirst=a.o.expandFirst,this.expandAll=a.o.expandAll,this.$empty=null,this._fromHTML=b.is.emptyArray(a.o.rows)&&!b.is.promise(a.o.rows)},parse:function(){var c=this;return a.Deferred(function(a){var d=c.ft.$el.children("tbody").children("tr");b.is.array(c.o.rows)&&c.o.rows.length>0?c.parseFinalize(a,c.o.rows):b.is.promise(c.o.rows)?c.o.rows.then(function(b){c.parseFinalize(a,b)},function(b){a.reject(Error("Rows ajax request error: "+b.status+" ("+b.statusText+")"))}):b.is.jq(d)?(c.parseFinalize(a,d),d.detach()):c.parseFinalize(a,[])})},parseFinalize:function(c,d){var e=this,f=a.map(d,function(a){return new b.Row(e.ft,e.ft.columns.array,a)});c.resolve(f)},preinit:function(a){var c=this;return c.ft.raise("preinit.ft.rows",[a]).then(function(){return c.parse().then(function(d){c.all=d,c.array=c.all.slice(0),c.showToggle=b.is["boolean"](a.showToggle)?a.showToggle:c.showToggle,c.toggleSelector=b.is.string(a.toggleSelector)?a.toggleSelector:c.toggleSelector,c.toggleColumn=b.is.string(a.toggleColumn)?a.toggleColumn:c.toggleColumn,"first"!=c.toggleColumn&&"last"!=c.toggleColumn&&(c.toggleColumn="first"),c.emptyString=b.is.string(a.empty)?a.empty:c.emptyString,c.expandFirst=b.is["boolean"](a.expandFirst)?a.expandFirst:c.expandFirst,c.expandAll=b.is["boolean"](a.expandAll)?a.expandAll:c.expandAll})})},init:function(){var a=this;return a.ft.raise("init.ft.rows",[a.all]).then(function(){a.$create()})},destroy:function(){var a=this;this.ft.raise("destroy.ft.rows").then(function(){b.arr.each(a.array,function(b){b.predraw(!a._fromHTML)}),a.all=a.array=[]})},predraw:function(){b.arr.each(this.array,function(a){a.predraw()}),this.array=this.all.slice(0)},$create:function(){this.$empty=a("",{"class":"footable-empty"}).append(a("",{"class":"footable-filtering"}).prependTo(d.ft.$el.children("thead")),d.$cell=a(""),this.ft.$el.append(c)),this.$row.appendTo(c)}else this.$wrapper.appendTo(this.$container);this.detached=!1}b.is.jq(this.$cell)&&this.$cell.attr("colspan",this.ft.columns.visibleColspan),this._createLinks(),this._setVisible(this.current,this.current>this.previous),this._setNavigation(!0),this.$count.text(this.formattedCount)}},$create:function(){this._createdLinks=0;var c="footable-paging-center";switch(this.position){case"left":c="footable-paging-left";break;case"right":c="footable-paging-right"}if(this.ft.$el.addClass("footable-paging").addClass(c),this.$container=null===this.container?null:a(this.container).first(),b.is.jq(this.$container))this.$container.addClass("footable-paging-external").addClass(c);else{var d=this.ft.$el.children("tfoot");0==d.length&&(d=a(""),this.ft.$el.append(d)),this.$row=a("",{"class":"footable-paging"}).prependTo(d),this.$container=this.$cell=a(""),b.ft.$el.append(d)),b.$row=a("",{"class":"footable-editing"}).append(b.$cell).appendTo(d)},$buttonShow:function(){return'"},$buttonHide:function(){return'"},$buttonAdd:function(){return' "},$buttonEdit:function(){return' "},$buttonDelete:function(){return'"},$buttonView:function(){return' "},$rowButtons:function(){return b.is.jq(this._$buttons)?this._$buttons.clone():(this._$buttons=a('
'),this.allowView&&this._$buttons.append(this.$buttonView()),this.allowEdit&&this._$buttons.append(this.$buttonEdit()),this.allowDelete&&this._$buttons.append(this.$buttonDelete()),this._$buttons)},draw:function(){this.$cell.attr("colspan",this.ft.columns.visibleColspan)},_onEditClick:function(c){c.preventDefault();var d=c.data.self,e=a(this).closest("tr").data("__FooTableRow__");e instanceof b.Row&&d.ft.raise("edit.ft.editing",[e]).then(function(){d.callbacks.editRow.call(d.ft,e)})},_onDeleteClick:function(c){c.preventDefault();var d=c.data.self,e=a(this).closest("tr").data("__FooTableRow__");e instanceof b.Row&&d.ft.raise("delete.ft.editing",[e]).then(function(){d.callbacks.deleteRow.call(d.ft,e)})},_onViewClick:function(c){c.preventDefault();var d=c.data.self,e=a(this).closest("tr").data("__FooTableRow__");e instanceof b.Row&&d.ft.raise("view.ft.editing",[e]).then(function(){d.callbacks.viewRow.call(d.ft,e)})},_onAddClick:function(a){a.preventDefault();var b=a.data.self;b.ft.raise("add.ft.editing").then(function(){b.callbacks.addRow.call(b.ft)})},_onShowClick:function(a){a.preventDefault();var b=a.data.self;b.ft.raise("show.ft.editing").then(function(){b.ft.$el.addClass("footable-editing-show"),b.column.visible=!0,b.ft.draw()})},_onHideClick:function(a){a.preventDefault();var b=a.data.self;b.ft.raise("hide.ft.editing").then(function(){b.ft.$el.removeClass("footable-editing-show"),b.column.visible=!1,b.ft.draw()})}}),b.components.register("editing",b.Editing,850)}(jQuery,FooTable),function(a,b){b.EditingColumn=b.Column.extend({construct:function(a,b,c){this._super(a,c,"editing"),this.editing=b,this.internal=!0},$create:function(){(this.$el=!this.virtual&&b.is.jq(this.$el)?this.$el:a("
").text(this.emptyString))},draw:function(){var a=this,c=a.ft.$el.children("tbody"),d=!0;a.array.length>0?(a.$empty.detach(),b.arr.each(a.array,function(b){(a.expandFirst&&d||a.expandAll)&&(b.expanded=!0,d=!1),b.draw(c)})):(a.$empty.children("td").attr("colspan",a.ft.columns.visibleColspan),c.append(a.$empty))},load:function(c,d){var e=this,f=a.map(c,function(a){return new b.Row(e.ft,e.ft.columns.array,a)});b.arr.each(this.array,function(a){a.predraw()}),this.all=(b.is["boolean"](d)?d:!1)?this.all.concat(f):f,this.array=this.all.slice(0),this.ft.draw()},expand:function(){b.arr.each(this.array,function(a){a.expand()})},collapse:function(){b.arr.each(this.array,function(a){a.collapse()})}}),b.components.register("rows",b.Rows,800)}(jQuery,FooTable),function(a){a.Defaults.prototype.rows=[],a.Defaults.prototype.empty="No results",a.Defaults.prototype.showToggle=!0,a.Defaults.prototype.toggleSelector="tr,td,.footable-toggle",a.Defaults.prototype.toggleColumn="first",a.Defaults.prototype.expandFirst=!1,a.Defaults.prototype.expandAll=!1}(FooTable),function(a){a.Table.prototype.loadRows=function(a,b){this.rows.load(a,b)}}(FooTable),function(a){a.Filter=a.Class.extend({construct:function(b,c,d,e,f,g,h){this.name=b,this.space=!a.is.string(e)||"OR"!=e&&"AND"!=e?"AND":e,this.connectors=a.is["boolean"](f)?f:!0,this.ignoreCase=a.is["boolean"](g)?g:!0,this.hidden=a.is["boolean"](h)?h:!1,this.query=c instanceof a.Query?c:new a.Query(c,this.space,this.connectors,this.ignoreCase),this.columns=d},match:function(b){return a.is.string(b)?(a.is.string(this.query)&&(this.query=new a.Query(this.query,this.space,this.connectors,this.ignoreCase)),this.query instanceof a.Query?this.query.match(b):!1):!1},matchRow:function(b){var c=this,d=a.arr.map(b.cells,function(b){return a.arr.contains(c.columns,b.column)?b.filterValue:null}).join(" ");return c.match(d)}})}(FooTable),function(a,b){b.Filtering=b.Component.extend({construct:function(a){this._super(a,a.o.filtering.enabled),this.filters=a.o.filtering.filters,this.delay=a.o.filtering.delay,this.min=a.o.filtering.min,this.space=a.o.filtering.space,this.connectors=a.o.filtering.connectors,this.ignoreCase=a.o.filtering.ignoreCase,this.exactMatch=a.o.filtering.exactMatch,this.placeholder=a.o.filtering.placeholder,this.dropdownTitle=a.o.filtering.dropdownTitle,this.position=a.o.filtering.position,this.focus=a.o.filtering.focus,this.container=a.o.filtering.container,this.$container=null,this.$row=null,this.$cell=null,this.$form=null,this.$dropdown=null,this.$input=null,this.$button=null,this._filterTimeout=null,this._exactRegExp=/^"(.*?)"$/},preinit:function(a){var c=this;return c.ft.raise("preinit.ft.filtering").then(function(){c.ft.$el.hasClass("footable-filtering")&&(c.enabled=!0),c.enabled=b.is["boolean"](a.filtering)?a.filtering:c.enabled,c.enabled&&(c.space=b.is.string(a.filterSpace)?a.filterSpace:c.space,c.min=b.is.number(a.filterMin)?a.filterMin:c.min,c.connectors=b.is["boolean"](a.filterConnectors)?a.filterConnectors:c.connectors,c.ignoreCase=b.is["boolean"](a.filterIgnoreCase)?a.filterIgnoreCase:c.ignoreCase,c.exactMatch=b.is["boolean"](a.filterExactMatch)?a.filterExactMatch:c.exactMatch,c.focus=b.is["boolean"](a.filterFocus)?a.filterFocus:c.focus,c.delay=b.is.number(a.filterDelay)?a.filterDelay:c.delay,c.placeholder=b.is.string(a.filterPlaceholder)?a.filterPlaceholder:c.placeholder,c.dropdownTitle=b.is.string(a.filterDropdownTitle)?a.filterDropdownTitle:c.dropdownTitle,c.container=b.is.string(a.filterContainer)?a.filterContainer:c.container,c.filters=b.is.array(a.filterFilters)?c.ensure(a.filterFilters):c.ensure(c.filters),c.ft.$el.hasClass("footable-filtering-left")&&(c.position="left"),c.ft.$el.hasClass("footable-filtering-center")&&(c.position="center"),c.ft.$el.hasClass("footable-filtering-right")&&(c.position="right"),c.position=b.is.string(a.filterPosition)?a.filterPosition:c.position)},function(){c.enabled=!1})},init:function(){var a=this;return a.ft.raise("init.ft.filtering").then(function(){a.$create()},function(){a.enabled=!1})},destroy:function(){var a=this;return a.ft.raise("destroy.ft.filtering").then(function(){a.ft.$el.removeClass("footable-filtering").find("thead > tr.footable-filtering").remove()})},$create:function(){var c,d=this,e=a("
",{"class":"form-group footable-filtering-search"}).append(a("
").attr("colspan",d.ft.columns.visibleColspan).appendTo(d.$row),d.$container=d.$cell),d.$form=a("
",{"class":"form-inline"}).append(e).appendTo(d.$container),d.$input=a("",{type:"text","class":"form-control",placeholder:d.placeholder}),d.$button=a("
").attr("colspan",this.ft.columns.visibleColspan).appendTo(this.$row)}this.$wrapper=a("
",{"class":"footable-pagination-wrapper"}).appendTo(this.$container),this.$pagination=a("
").attr("colspan",b.ft.columns.visibleColspan).append(b.$buttonShow()),b.allowAdd&&b.$cell.append(b.$buttonAdd()),b.$cell.append(b.$buttonHide()),b.alwaysShow&&b.ft.$el.addClass("footable-editing-always-show"),b.allowAdd||b.ft.$el.addClass("footable-editing-no-add"),b.allowEdit||b.ft.$el.addClass("footable-editing-no-edit"),b.allowDelete||b.ft.$el.addClass("footable-editing-no-delete"),b.allowView||b.ft.$el.addClass("footable-editing-no-view");var d=b.ft.$el.children("tfoot");0==d.length&&(d=a("
",{"class":"footable-editing"})).html(this.title)},parser:function(c){if(b.is.string(c)&&(c=a(a.trim(c))),b.is.element(c)&&(c=a(c)),b.is.jq(c)){var d=c.prop("tagName").toLowerCase();return"td"==d||"th"==d?c.data("value")||c.contents():c}return null},createCell:function(c){var d=this.editing.$rowButtons(),e=a("").append(d);return b.is.jq(c.$el)&&(0===this.index?e.prependTo(c.$el):e.insertAfter(c.$el.children().eq(this.index-1))),new b.Cell(this.ft,c,this,e||e.html())}}),b.columns.register("editing",b.EditingColumn)}(jQuery,FooTable),function(a,b){b.Defaults.prototype.editing={enabled:!1,pageToNew:!0,position:"right",alwaysShow:!1,addRow:function(){},editRow:function(a){},deleteRow:function(a){},viewRow:function(a){},showText:' Edit rows',hideText:"Cancel",addText:"New row",editText:'',deleteText:'',viewText:'',allowAdd:!0,allowEdit:!0,allowDelete:!0,allowView:!1,column:{classes:"footable-editing",name:"editing",title:"",filterable:!1,sortable:!1}}}(jQuery,FooTable),function(a,b){b.is.defined(b.Paging)&&(b.Paging.prototype.unpaged=[],b.Paging.extend("predraw",function(){this.unpaged=this.ft.rows.array.slice(0),this._super()}))}(jQuery,FooTable),function(a,b){b.Row.prototype.add=function(c){c=b.is["boolean"](c)?c:!0;var d=this;return a.Deferred(function(a){var b=d.ft.rows.all.push(d)-1;return c?d.ft.draw().then(function(){a.resolve(b)}):void a.resolve(b)})},b.Row.prototype["delete"]=function(c){c=b.is["boolean"](c)?c:!0;var d=this;return a.Deferred(function(a){var e=d.ft.rows.all.indexOf(d);return b.is.number(e)&&e>=0&&e=0&&e>b&&(f=this.ft.rows.all[b]),f instanceof FooTable.Row&&a.is.hash(c)&&f.val(c,d)},a.Rows.prototype["delete"]=function(b,c){var d=this.ft.rows.all.length,e=b;a.is.number(b)&&b>=0&&d>b&&(e=this.ft.rows.all[b]),e instanceof FooTable.Row&&e["delete"](c)}}(FooTable),function(a,b){var c=0,d=function(a){var b,c,d=2166136261;for(b=0,c=a.length;c>b;b++)d^=a.charCodeAt(b),d+=(d<<1)+(d<<4)+(d<<7)+(d<<8)+(d<<24);return d>>>0}(location.origin+location.pathname);b.State=b.Component.extend({construct:function(a){this._super(a,a.o.state.enabled),this._key="1",this.key=this._key+(b.is.string(a.o.state.key)?a.o.state.key:this._uid()),this.filtering=b.is["boolean"](a.o.state.filtering)?a.o.state.filtering:!0,this.paging=b.is["boolean"](a.o.state.paging)?a.o.state.paging:!0,this.sorting=b.is["boolean"](a.o.state.sorting)?a.o.state.sorting:!0},preinit:function(a){var c=this;this.ft.raise("preinit.ft.state",[a]).then(function(){c.enabled=b.is["boolean"](a.state)?a.state:c.enabled,c.enabled&&(c.key=c._key+(b.is.string(a.stateKey)?a.stateKey:c.key),c.filtering=b.is["boolean"](a.stateFiltering)?a.stateFiltering:c.filtering,c.paging=b.is["boolean"](a.statePaging)?a.statePaging:c.paging,c.sorting=b.is["boolean"](a.stateSorting)?a.stateSorting:c.sorting)},function(){c.enabled=!1})},get:function(a){return JSON.parse(localStorage.getItem(this.key+":"+a))},set:function(a,b){localStorage.setItem(this.key+":"+a,JSON.stringify(b))},remove:function(a){localStorage.removeItem(this.key+":"+a)},read:function(){this.ft.execute(!1,!0,"readState")},write:function(){this.ft.execute(!1,!0,"writeState")},clear:function(){this.ft.execute(!1,!0,"clearState")},_uid:function(){var a=this.ft.$el.attr("id");return d+"_"+(b.is.string(a)?a:++c)}}),b.components.register("state",b.State,700)}(jQuery,FooTable),function(a){a.Component.prototype.readState=function(){},a.Component.prototype.writeState=function(){},a.Component.prototype.clearState=function(){}}(FooTable),function(a){a.Defaults.prototype.state={enabled:!1,filtering:!0,paging:!0,sorting:!0,key:null}}(FooTable),function(a){a.Filtering&&(a.Filtering.prototype.readState=function(){if(this.ft.state.filtering){var b=this.ft.state.get("filtering");a.is.hash(b)&&!a.is.emptyArray(b.filters)&&(this.filters=this.ensure(b.filters))}},a.Filtering.prototype.writeState=function(){if(this.ft.state.filtering){var b=a.arr.map(this.filters,function(b){return{name:b.name,query:b.query instanceof a.Query?b.query.val():b.query,columns:a.arr.map(b.columns,function(a){return a.name}),hidden:b.hidden,space:b.space,connectors:b.connectors,ignoreCase:b.ignoreCase}});this.ft.state.set("filtering",{filters:b})}},a.Filtering.prototype.clearState=function(){this.ft.state.filtering&&this.ft.state.remove("filtering")})}(FooTable),function(a){a.Paging&&(a.Paging.prototype.readState=function(){if(this.ft.state.paging){var b=this.ft.state.get("paging");a.is.hash(b)&&(this.current=b.current,this.size=b.size)}},a.Paging.prototype.writeState=function(){this.ft.state.paging&&this.ft.state.set("paging",{current:this.current,size:this.size})},a.Paging.prototype.clearState=function(){this.ft.state.paging&&this.ft.state.remove("paging")})}(FooTable),function(a){a.Sorting&&(a.Sorting.prototype.readState=function(){if(this.ft.state.sorting){var b=this.ft.state.get("sorting");if(a.is.hash(b)){var c=this.ft.columns.get(b.column);c instanceof a.Column&&(this.column=c,this.column.direction=b.direction)}}},a.Sorting.prototype.writeState=function(){this.ft.state.sorting&&this.column instanceof a.Column&&this.ft.state.set("sorting",{column:this.column.name,direction:this.column.direction})},a.Sorting.prototype.clearState=function(){this.ft.state.sorting&&this.ft.state.remove("sorting")})}(FooTable),function(a){a.Table.extend("_construct",function(a){return this.state=this.use(FooTable.State),this._super(a)}),a.Table.extend("_preinit",function(){var a=this;return a._super().then(function(){a.state.enabled&&a.state.read()})}),a.Table.extend("draw",function(){var a=this;return a._super().then(function(){a.state.enabled&&a.state.write()})})}(FooTable),function(a,b){b.Export=b.Component.extend({construct:function(a){this._super(a,!0),this.snapshot=[]},predraw:function(){this.snapshot=this.ft.rows.array.slice(0)},columns:function(){var a=[];return b.arr.each(this.ft.columns.array,function(b){b.internal||a.push({type:b.type,name:b.name,title:b.title,visible:b.visible,hidden:b.hidden,classes:b.classes,style:b.style})}),a},rows:function(a){a=b.is["boolean"](a)?a:!1;var c=a?this.ft.rows.all:this.snapshot,d=[];return b.arr.each(c,function(a){d.push(a.val())}),d},json:function(a){return JSON.parse(JSON.stringify({columns:this.columns(),rows:this.rows(a)}))},csv:function(a){var c,d,e="",f=this.columns();b.arr.each(f,function(a,b){d='"'+a.title.replace(/"/g,'""')+'"',e+=0===b?d:","+d}),e+="\n";var g=a?this.ft.rows.all:this.snapshot;return b.arr.each(g,function(a){b.arr.each(a.cells,function(a,b){a.column.internal||(c=a.column.stringify.call(a.column,a.value,a.ft.o,a.row.value),d='"'+c.replace(/"/g,'""')+'"',e+=0===b?d:","+d)}),e+="\n"}),e}}),b.components.register("export",b.Export,490)}(jQuery,FooTable),function(a){a.Column.prototype.__export_define__=function(b){this.stringify=a.checkFnValue(this,b.stringify,this.stringify)},a.Column.extend("define",function(a){this._super(a),this.__export_define__(a)}),a.Column.prototype.stringify=function(a,b,c){return a+""},a.is.defined(a.DateColumn)&&(a.DateColumn.prototype.stringify=function(b,c,d){return a.is.object(b)&&a.is["boolean"](b._isAMomentObject)&&b.isValid()?b.format(this.formatString):""}),a.ObjectColumn.prototype.stringify=function(b,c,d){return a.is.object(b)?JSON.stringify(b):""},a.ArrayColumn.prototype.stringify=function(b,c,d){return a.is.array(b)?JSON.stringify(b):""}}(FooTable),function(a){a.Table.prototype.toJSON=function(b){return this.use(a.Export).json(b)},a.Table.prototype.toCSV=function(b){return this.use(a.Export).csv(b)}}(FooTable); \ No newline at end of file diff --git a/src/websrc/index.html b/src/websrc/index.html index 944620a64..e5391c8f5 100644 --- a/src/websrc/index.html +++ b/src/websrc/index.html @@ -52,9 +52,6 @@ -
  • - Event Log -
  • Backup & Restore
  • @@ -194,7 +191,10 @@ -
    - +
    + +
    + + + + +
    +
    @@ -477,26 +503,22 @@
    -
    - - - - - - -
    MQTT
    -
    -
    -
    -
    -
    -
    -
    -
    -
    -
    +
    + + + + + + + + + + +
    MQTT  
     
    +
    TimeTopicPayload
    diff --git a/src/websrc/myesp.js b/src/websrc/myesp.js index fcf04cb19..04a07161a 100644 --- a/src/websrc/myesp.js +++ b/src/websrc/myesp.js @@ -2,78 +2,35 @@ var version = ""; var websock = null; var wsUri = "ws://" + window.location.host + "/ws"; -var utcSeconds; -var data = []; +var ntpSeconds; var ajaxobj; var custom_config = {}; -var config = { - "command": "configfile", - "network": { - "ssid": "", - "wmode": 1, - "password": "" - }, - "general": { - "hostname": "", - "serial": false, - "password": "admin", - "log_events": true - }, - "mqtt": { - "enabled": false, - "ip": "", - "port": 1883, - "qos": 1, - "keepalive": 60, - "retain": true, - "base": "", - "user": "", - "password": "", - "heartbeat": false - }, - "ntp": { - "server": "pool.ntp.org", - "interval": 30, - "enabled": true - } -}; +var xDown = null; +var yDown = null; -var page = 1; -var haspages; var file = {}; var backupstarted = false; var updateurl = ""; +var updateurl_dev = ""; + +var use_beta_firmware = false; var myespcontent; -function browserTime() { - var d = new Date(0); - var c = new Date(); - var timestamp = Math.floor((c.getTime() / 1000) + ((c.getTimezoneOffset() * 60) * -1)); - d.setUTCSeconds(timestamp); - document.getElementById("rtc").innerHTML = d.toUTCString().slice(0, -3); -} +var formData = new FormData(); + +var nextIsNotJson = false; + +var config = {}; function deviceTime() { - var c = new Date(); var t = new Date(0); // The 0 there is the key, which sets the date to the epoch - var devTime = Math.floor(utcSeconds + ((c.getTimezoneOffset() * 60) * -1)); - t.setUTCSeconds(devTime); + t.setUTCSeconds(ntpSeconds); document.getElementById("utc").innerHTML = t.toUTCString().slice(0, -3); } -function syncBrowserTime() { - var d = new Date(); - var timestamp = Math.floor((d.getTime() / 1000)); - var datatosend = {}; - datatosend.command = "settime"; - datatosend.epoch = timestamp; - websock.send(JSON.stringify(datatosend)); - $("#ntp").click(); -} - function handleNTPON() { document.getElementById("forcentp").style.display = "block"; } @@ -86,7 +43,8 @@ function listntp() { websock.send("{\"command\":\"gettime\"}"); document.getElementById("ntpserver").value = config.ntp.server; - document.getElementById("intervals").value = config.ntp.interval; + document.getElementById("interval").value = config.ntp.interval; + document.getElementById("timezone").value = config.ntp.timezone; if (config.ntp.enabled) { $("input[name=\"ntpenabled\"][value=\"1\"]").prop("checked", true); @@ -127,7 +85,8 @@ function custom_saveconfig() { function saventp() { config.ntp.server = document.getElementById("ntpserver").value; - config.ntp.interval = parseInt(document.getElementById("intervals").value); + config.ntp.interval = parseInt(document.getElementById("interval").value); + config.ntp.timezone = parseInt(document.getElementById("timezone").value); config.ntp.enabled = false; if (parseInt($("input[name=\"ntpenabled\"]:checked").val()) === 1) { @@ -160,6 +119,8 @@ function savegeneral() { config.general.log_events = true; } + config.general.log_ip = document.getElementById("log_ip").value; + saveconfig(); } @@ -205,12 +166,14 @@ function savenetwork() { config.network.wmode = wmode; config.network.password = document.getElementById("wifipass").value; + config.network.staticip = document.getElementById("staticip").value; + config.network.gatewayip = document.getElementById("gatewayip").value; + config.network.nmask = document.getElementById("nmask").value; + config.network.dnsip = document.getElementById("dnsip").value; saveconfig(); } -var formData = new FormData(); - function inProgress(callback) { $("body").load("myesp.html #progresscontent", function (responseTxt, statusTxt, xhr) { if (statusTxt === "success") { @@ -283,15 +246,13 @@ function inProgressUpload() { function handleSTA() { document.getElementById("scanb").style.display = "block"; - document.getElementById("hidessid").style.display = "block"; - document.getElementById("hidepasswd").style.display = "block"; + document.getElementById("hideclient").style.display = "block"; } function handleAP() { document.getElementById("ssid").style.display = "none"; document.getElementById("scanb").style.display = "none"; - document.getElementById("hidessid").style.display = "none"; - document.getElementById("hidepasswd").style.display = "none"; + document.getElementById("hideclient").style.display = "none"; document.getElementById("inputtohide").style.display = "block"; } @@ -307,6 +268,11 @@ function listnetwork() { handleSTA(); } + document.getElementById("staticip").value = config.network.staticip; + document.getElementById("gatewayip").value = config.network.gatewayip; + document.getElementById("nmask").value = config.network.nmask; + document.getElementById("dnsip").value = config.network.dnsip; + } function listgeneral() { @@ -320,6 +286,9 @@ function listgeneral() { if (config.general.log_events) { $("input[name=\"logeventsenabled\"][value=\"1\"]").prop("checked", true); } + + document.getElementById("log_ip").value = config.general.log_ip; + } function listmqtt() { @@ -375,32 +344,10 @@ function scanWifi() { } } -function getEvents() { - websock.send("{\"command\":\"geteventlog\", \"page\":" + page + "}"); -} - function isVisible(e) { return !!(e.offsetWidth || e.offsetHeight || e.getClientRects().length); } -function getnextpage(mode) { - if (!backupstarted) { - document.getElementById("loadpages").innerHTML = "Loading " + page + "/" + haspages; - } - - if (page < haspages) { - page = page + 1; - var commandtosend = {}; - commandtosend.command = mode; - commandtosend.page = page; - websock.send(JSON.stringify(commandtosend)); - } -} - -function builddata(obj) { - data = data.concat(obj.list); -} - function colorStatusbar(ref) { var percentage = ref.style.width.slice(0, -1); if (percentage > 50) { ref.className = "progress-bar progress-bar-success"; } else if (percentage > 25) { ref.className = "progress-bar progress-bar-warning"; } else { ref.class = "progress-bar progress-bar-danger"; } @@ -444,8 +391,40 @@ function listStats() { document.getElementById("mqttheartbeat").className = "label label-primary"; } - document.getElementById("mqttloghdr").innerHTML = "MQTT Publish Log: (topics are prefixed with " + ajaxobj.mqttloghdr + ")"; + document.getElementById("mqttloghdr").setAttribute('data-content', "Topics are prefixed with " + ajaxobj.mqttloghdr); + var mtable = document.getElementById("mqttlog"); + var obj = ajaxobj.mqttlog; + var tr, td; + + for (var i = 0; i < obj.length; i++) { + tr = document.createElement("tr"); + + td = document.createElement("td"); + + if (obj[i].time < 1563300000) { + td.innerHTML = "(" + obj[i].time + ")"; + } else { + var vuepoch = new Date(obj[i].time * 1000); + td.innerHTML = vuepoch.getUTCFullYear() + + "-" + twoDigits(vuepoch.getUTCMonth() + 1) + + "-" + twoDigits(vuepoch.getUTCDate()) + + " " + twoDigits(vuepoch.getUTCHours()) + + ":" + twoDigits(vuepoch.getUTCMinutes()) + + ":" + twoDigits(vuepoch.getUTCSeconds()); + } + tr.appendChild(td); + + td = document.createElement("td"); + td.innerHTML = obj[i].topic + tr.appendChild(td); + + td = document.createElement("td"); + td.innerHTML = obj[i].payload + tr.appendChild(td); + + mtable.appendChild(tr); + } } function getContent(contentname) { @@ -471,19 +450,6 @@ function getContent(contentname) { case "#networkcontent": listnetwork(); break; - case "#eventcontent": - page = 1; - data = []; - getEvents(); - - if (config.general.log_events) { - document.getElementById("logevents").style.display = "none"; - } else { - document.getElementById("logevents").style.display = "block"; - } - - break; - case "#customcontent": listcustom(); break; @@ -510,6 +476,7 @@ function getContent(contentname) { $("#appurl2").text(ajaxobj.appurl); updateurl = ajaxobj.updateurl; + updateurl_dev = ajaxobj.updateurl_dev; listCustomStats(); break; default: @@ -602,147 +569,12 @@ function twoDigits(value) { return value; } -function initEventTable() { - var newlist = []; - for (var i = 0; i < data.length; i++) { - newlist[i] = {}; - newlist[i].options = {}; - newlist[i].value = {}; - try { - var dup = JSON.parse(data[i]); - dup.uid = i + 1; - } catch (e) { - var dup = { "uid": i + 1, "type": "ERRO", "src": "SYS", "desc": "Error in log file", "data": data[i], "time": 1 } - } - newlist[i].value = dup; - - var c = dup.type; - switch (c) { - case "WARN": - newlist[i].options.classes = "warning"; - break; - case "INFO": - newlist[i].options.classes = "info"; - break; - case "ERRO": - newlist[i].options.classes = "danger"; - break; - default: - break; - } - } - jQuery(function ($) { - window.FooTable.init("#eventtable", { - columns: [{ - "name": "uid", - "title": "ID", - "type": "text", - "sorted": true, - "direction": "DESC" - }, - { - "name": "type", - "title": "Event Type", - "type": "text" - }, - { - "name": "src", - "title": "Source" - }, - { - "name": "desc", - "title": "Description" - }, - { - "name": "data", - "title": "Additional Data", - "breakpoints": "xs sm" - }, - { - "name": "time", - "title": "Date/Time", - "parser": function (value) { - if (value < 1563300000) { - return "(" + value + ")"; - } else { - var comp = new Date(); - value = Math.floor(value + ((comp.getTimezoneOffset() * 60) * -1)); - var vuepoch = new Date(value * 1000); - var formatted = vuepoch.getUTCFullYear() + - "-" + twoDigits(vuepoch.getUTCMonth() + 1) + - "-" + twoDigits(vuepoch.getUTCDate()) + - " " + twoDigits(vuepoch.getUTCHours()) + - ":" + twoDigits(vuepoch.getUTCMinutes()) + - ":" + twoDigits(vuepoch.getUTCSeconds()); - return formatted; - } - }, - "breakpoints": "xs sm" - } - ], - rows: newlist - }); - }); -} - -function initMQTTLogTable() { - var newlist = []; - for (var i = 0; i < ajaxobj.mqttlog.length; i++) { - var data = JSON.stringify(ajaxobj.mqttlog[i]); - newlist[i] = {}; - newlist[i].options = {}; - newlist[i].value = {}; - newlist[i].value = JSON.parse(data); - newlist[i].options.classes = "warning"; - newlist[i].options.style = "color: blue"; - } - jQuery(function ($) { - window.FooTable.init("#mqttlogtable", { - columns: [{ - "name": "time", - "title": "Last Published", - "style": { "min-width": "160px" }, - "parser": function (value) { - if (value < 1563300000) { - return "(" + value + ")"; - } else { - var comp = new Date(); - value = Math.floor(value + ((comp.getTimezoneOffset() * 60) * -1)); - var vuepoch = new Date(value * 1000); - var formatted = vuepoch.getUTCFullYear() + - "-" + twoDigits(vuepoch.getUTCMonth() + 1) + - "-" + twoDigits(vuepoch.getUTCDate()) + - " " + twoDigits(vuepoch.getUTCHours()) + - ":" + twoDigits(vuepoch.getUTCMinutes()) + - ":" + twoDigits(vuepoch.getUTCSeconds()); - return formatted; - } - }, - "breakpoints": "xs sm" - }, - { - "name": "topic", - "title": "Topic", - }, - { - "name": "payload", - "title": "Payload", - }, - ], - rows: newlist - }); - }); -} - -var nextIsNotJson = false; - function socketMessageListener(evt) { var obj = JSON.parse(evt.data); if (obj.hasOwnProperty("command")) { switch (obj.command) { case "status": ajaxobj = obj; - initMQTTLogTable(); getContent("#statuscontent"); break; case "custom_settings": @@ -752,17 +584,8 @@ function socketMessageListener(evt) { ajaxobj = obj; getContent("#custom_statuscontent"); break; - case "eventlist": - haspages = obj.haspages; - if (haspages === 0) { - document.getElementById("loading-img").style.display = "none"; - initEventTable(); - break; - } - builddata(obj); - break; case "gettime": - utcSeconds = obj.epoch; + ntpSeconds = obj.epoch; deviceTime(); break; case "ssidlist": @@ -778,26 +601,6 @@ function socketMessageListener(evt) { break; } } - - if (obj.hasOwnProperty("resultof")) { - switch (obj.resultof) { - case "eventlist": - if (page < haspages && obj.result === true) { - getnextpage("geteventlog"); - } else if (page === haspages) { - initEventTable(); - document.getElementById("loading-img").style.display = "none"; - } - break; - default: - break; - } - } -} - -function clearevent() { - websock.send("{\"command\":\"clearevent\"}"); - $("#eventlog").click(); } function compareDestroy() { @@ -814,46 +617,6 @@ function restart() { inProgress("restart"); } -$("#dismiss, .overlay").on("click", function () { - $("#sidebar").removeClass("active"); - $(".overlay").fadeOut(); -}); - -$("#sidebarCollapse").on("click", function () { - $("#sidebar").addClass("active"); - $(".overlay").fadeIn(); - $(".collapse.in").toggleClass("in"); - $("a[aria-expanded=true]").attr("aria-expanded", "false"); -}); - -$("#custom_status").click(function () { - websock.send("{\"command\":\"custom_status\"}"); - return false; -}); - -$("#status").click(function () { - websock.send("{\"command\":\"status\"}"); - return false; -}); - -$("#custom").click(function () { getContent("#customcontent"); return false; }); - -$("#network").on("click", (function () { getContent("#networkcontent"); return false; })); -$("#general").click(function () { getContent("#generalcontent"); return false; }); -$("#mqtt").click(function () { getContent("#mqttcontent"); return false; }); -$("#ntp").click(function () { getContent("#ntpcontent"); return false; }); -$("#backup").click(function () { getContent("#backupcontent"); return false; }); -$("#reset").click(function () { $("#destroy").modal("show"); return false; }); -$("#restart").click(function () { $("#reboot").modal("show"); return false; }); -$("#eventlog").click(function () { getContent("#eventcontent"); return false; }); - -$(".noimp").on("click", function () { - $("#noimp").modal("show"); -}); - -var xDown = null; -var yDown = null; - function handleTouchStart(evt) { xDown = evt.touches[0].clientX; yDown = evt.touches[0].clientY; @@ -953,8 +716,26 @@ function login() { } } +function getfirmware() { + if (use_beta_firmware) { + use_beta_firmware = false; + document.getElementById("updateb").innerHTML = "Switch to Development build"; + } else { + use_beta_firmware = true; + document.getElementById("updateb").innerHTML = "Switch to Stable release"; + } + getLatestReleaseInfo(); +} + function getLatestReleaseInfo() { - $.getJSON(updateurl).done(function (release) { + + if (use_beta_firmware) { + var url = updateurl_dev; + } else { + var url = updateurl; + } + + $.getJSON(url).done(function (release) { var asset = release.assets[0]; var downloadCount = 0; for (var i = 0; i < release.assets.length; i++) { @@ -978,10 +759,6 @@ function getLatestReleaseInfo() { }).error(function () { $("#onlineupdate").html("
    Couldn't get release details. Make sure there is an Internet connection.
    "); }); } -$("#update").on("shown.bs.modal", function (e) { - getLatestReleaseInfo(); -}); - function allowUpload() { $("#upbtn").prop("disabled", false); } @@ -1006,7 +783,7 @@ function start() { }); } -function refreshEMS() { +function refreshCustomStatus() { websock.send("{\"command\":\"custom_status\"}"); } @@ -1014,5 +791,30 @@ function refreshStatus() { websock.send("{\"command\":\"status\"}"); } +$("#dismiss, .overlay").on("click", function () { + $("#sidebar").removeClass("active"); + $(".overlay").fadeOut(); +}); + +$("#sidebarCollapse").on("click", function () { + $("#sidebar").addClass("active"); + $(".overlay").fadeIn(); + $(".collapse.in").toggleClass("in"); + $("a[aria-expanded=true]").attr("aria-expanded", "false"); +}); + +$("#custom_status").click(function () { websock.send("{\"command\":\"custom_status\"}"); return false; }); +$("#status").click(function () { websock.send("{\"command\":\"status\"}"); return false; }); +$("#custom").click(function () { getContent("#customcontent"); return false; }); +$("#network").on("click", (function () { getContent("#networkcontent"); return false; })); +$("#general").click(function () { getContent("#generalcontent"); return false; }); +$("#mqtt").click(function () { getContent("#mqttcontent"); return false; }); +$("#ntp").click(function () { getContent("#ntpcontent"); return false; }); +$("#backup").click(function () { getContent("#backupcontent"); return false; }); +$("#reset").click(function () { $("#destroy").modal("show"); return false; }); +$("#restart").click(function () { $("#reboot").modal("show"); return false; }); +$(".noimp").on("click", function () { $("#noimp").modal("show"); }); +$("#update").on("shown.bs.modal", function (e) { getfirmware(); }); + document.addEventListener("touchstart", handleTouchStart, false); document.addEventListener("touchmove", handleTouchMove, false); diff --git a/tools/webfilesbuilder/gulpfile.js b/tools/webfilesbuilder/gulpfile.js index 24212b30a..974e741b5 100644 --- a/tools/webfilesbuilder/gulpfile.js +++ b/tools/webfilesbuilder/gulpfile.js @@ -100,7 +100,7 @@ gulp.task('myespjs', function () { }); gulp.task('requiredjs', function () { - return gulp.src(['../../src/websrc/3rdparty/js/jquery-1.12.4.min.js', '../../src/websrc/3rdparty/js/bootstrap-3.3.7.min.js', '../../src/websrc/3rdparty/js/footable-3.1.6.min.js']) + return gulp.src(['../../src/websrc/3rdparty/js/jquery-1.12.4.min.js', '../../src/websrc/3rdparty/js/bootstrap-3.4.1.min.js']) .pipe(concat({ path: 'required.js', stat: { @@ -117,7 +117,7 @@ gulp.task('requiredjs', function () { gulp.task('requiredcss', function () { - return gulp.src(['../../src/websrc/3rdparty/css/bootstrap-3.3.7.min.css', '../../src/websrc/3rdparty/css/footable.bootstrap-3.1.6.min.css', '../../src/websrc/3rdparty/css/sidebar.css']) + return gulp.src(['../../src/websrc/3rdparty/css/bootstrap-3.4.1.min.css', '../../src/websrc/3rdparty/css/sidebar.css']) .pipe(concat({ path: 'required.css', stat: { diff --git a/tools/wsemulator/run.ps1 b/tools/wsemulator/run.ps1 old mode 100644 new mode 100755 diff --git a/tools/wsemulator/run.sh b/tools/wsemulator/run.sh old mode 100644 new mode 100755 index 4895c6245..03ac3be32 --- a/tools/wsemulator/run.sh +++ b/tools/wsemulator/run.sh @@ -1,6 +1,6 @@ #!/bin/sh -node $PWD/../webfilesbuilder/node_modules/gulp/bin/gulp.js --cwd $PWD/../webfilesbuilder +node $PWD/../webfilesbuilder/node_modules/gulp/bin/gulp.js --cwd $PWD/../webfilesbuilder open -na Google\ Chrome --args --disable-web-security --remote-debugging-port=9222 --user-data-dir="/tmp/chrome_dev" $PWD/../../src/websrc/temp/index.html diff --git a/tools/wsemulator/wserver.js b/tools/wsemulator/wserver.js index 1ae6c2be7..8f5353f82 100644 --- a/tools/wsemulator/wserver.js +++ b/tools/wsemulator/wserver.js @@ -47,30 +47,25 @@ var networks = { ] } -var eventlog = { - "command": "eventlist", - "page": 1, - "haspages": 1, - "list": [ - "{\"type\":\"WARN\",\"src\":\"system\",\"desc\":\"test data\",\"data\":\"Record #1\",\"time\": 1563371160}", - "{\"type\":\"WARN\",\"src\":\"system\",\"desc\":\"test data\",\"data\":\"Record #2\",\"time\":0}", - "{\"type\":\"INFO\",\"src\":\"system\",\"desc\":\"System booted\",\"data\":\"\",\"time\":1568660479}", - "{\"type\":\"WARN\",\"src\":\"system\",\"desc\":\"test data\",\"data\":\"Record #3\",\"time\":0}" - ] -} - var configfile = { "command": "configfile", "network": { "ssid": "myssid", "wmode": 0, - "password": "password" + "password": "password", + "password": "", + "staticip": "", + "gatewayip": "", + "nmask": "", + "dnsip": "" }, "general": { - "hostname": "myesp", + "hostname": "ems-esp", "password": "admin", "serial": true, - "log_events": true + "version": "1.0.0", + "log_events": false, + "log_ip": "10.11.12.13" }, "mqtt": { "enabled": false, @@ -86,8 +81,9 @@ var configfile = { }, "ntp": { "server": "pool.ntp.org", - "interval": "30", - "enabled": false + "interval": 720, + "timezone": 2, + "enabled": true } }; @@ -101,21 +97,11 @@ var custom_configfile = { "listen_mode": false, "shower_timer": true, "shower_alert": false, - "publish_time": 120, + "publish_time": 0, "tx_mode": 1 } }; -function sendEventLog() { - wss.broadcast(eventlog); - var res = { - "command": "result", - "resultof": "eventlist", - "result": true - }; - wss.broadcast(res); -} - function sendStatus() { var stats = { "command": "status", @@ -127,7 +113,7 @@ function sendStatus() { "availsize": 2469, "ip": "10.10.10.198", "ssid": "my_ssid", - "mac": "DC:4F:11:22:93:06", + "mac": "DC:4F:12:22:13:06", "signalstr": 62, "systemload": 0, "mqttconnected": true, @@ -155,6 +141,7 @@ function sendCustomStatus() { "customname": "EMS-ESP", "appurl": "https://github.com/proddy/EMS-ESP", "updateurl": "https://api.github.com/repos/proddy/EMS-ESP/releases/latest", + "updateurl_dev": "https://api.github.com/repos/proddy/EMS-ESP/releases/tags/travis-dev-build", "emsbus": { "ok": true, @@ -241,28 +228,13 @@ wss.on('connection', function connection(ws) { console.log("[INFO] Sending time"); var res = {}; res.command = "gettime"; - res.epoch = Math.floor((new Date).getTime() / 1000); - //res.epoch = 1567107755; - wss.broadcast(res); - break; - case "settime": - console.log("[INFO] Setting time (fake)"); - var res = {}; - res.command = "gettime"; - res.epoch = Math.floor((new Date).getTime() / 1000); + res.epoch = 1572613374; // this is 13:02:54 CET wss.broadcast(res); break; case "getconf": console.log("[INFO] Sending system configuration file (if set any)"); wss.broadcast(configfile); break; - case "geteventlog": - console.log("[INFO] Sending eventlog"); - sendEventLog(); - break; - case "clearevent": - console.log("[INFO] Clearing eventlog"); - break; case "restart": console.log("[INFO] Restart"); break;