diff --git a/.gitignore b/.gitignore index 07cb21044..7f4e4b98c 100644 --- a/.gitignore +++ b/.gitignore @@ -1,11 +1,50 @@ -.pio* +# vscode +.vscode +.vscode/.browse.c_cpp.db* +.vscode/c_cpp_properties.json +.vscode/launch.json +.vscode/* + +# platformio +.pio .clang_complete .gcc-flags.json -.vscode -.env -.DS_Store platformio.ini lib/readme.txt .travis.yml + +# web stuff compiled +src/websrc/css/required.css +src/websrc/js/required.js +src/websrc/fonts +src/websrc/gzipped +src/websrc/temp +*.gz.h + +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# Bower dependency directory (https://bower.io/) +bower_components + +# Dependency directories +node_modules/ +jspm_packages/ + +# Optional npm cache directory +.npm + +# Output of 'npm pack' +*.tgz + +# dotenv environment variables file +.env + +# project specfic +.DS_Store scripts/stackdmp.txt *.bin diff --git a/CHANGELOG.md b/CHANGELOG.md index 128d1c5b0..29a747b4d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,20 +5,22 @@ 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.8.3] 2019-08-12 +## [1.9.0] 2019-09-01 + +### Changed + +- New web interface with more features showing Boiler, Thermostat, Solar Module and Heat Pump. See https://github.com/proddy/EMS-ESP/wiki/Running-and-Monitoring +- Merged with @susisstrolch's TxMode2 branch for improved support for sending EMS packages. This is the default tx mode. +- Upgraded MyESP library optimizations for WiFi, AP and error handling +- `reboot` command renamed to `restart` to keep consistent with web interface +- Renamed `heartbeat` to `mqtt_heartbeat` in config settings +- Renamed MQTT topic "wwactivated" to "boiler_cmd_wwactivated" ### Fixed -- Added write support for RC3000 +- Handle Read and Write to EMS+ device logic changed, tested with RC3000 -## [1.8.2] 2019-08-10 - -### Fixed - -- Show correct temperatures for FW120. [Issue 166](https://github.com/proddy/EMS-ESP/pull/166) -- LED off works after reboot [Issue 167](https://github.com/proddy/EMS-ESP/issues/167) - -## [1.8.1] 2019-07-29 +## [1.8.1] 2019-07-27 ### Added diff --git a/README.md b/README.md index 2119311fc..310069472 100644 --- a/README.md +++ b/README.md @@ -2,29 +2,31 @@ EMS-ESP is a open-source system to communicate with **EMS** (Energy Management System) based boilers, thermostats and other modules from manufacturers like Bosch, Buderus, Nefit, Junkers and Sieger. -The code is writen for the Espressif **ESP8266** microcontroller and supports a telnet console for real-time monitoring and configuration and customizable MQTT support for publishing the information to a home automation system such as Home Assistant or Domoticz. +The code is written for the Espressif **ESP8266** microcontroller and supports a telnet console for real-time monitoring and configuration and customizable MQTT support for publishing the information to a home automation system such as Home Assistant or Domoticz. -### Please reference the [Wiki](https://github.com/proddy/EMS-ESP/wiki) for further details and instructions on how to build and configure the firmware. +#### Please reference the [Wiki](https://github.com/proddy/EMS-ESP/wiki) for further details and instructions on how to build and configure the firmware. --- -**An example of the Home Assistant integration:** +## Features + +#### A web interface for easy configuration and real-time monitoring of the EMS bus + +| ![web menu](https://github.com/proddy/EMS-ESP/raw/master/doc/web/system_status.PNG) | ![web menu](https://github.com/proddy/EMS-ESP/raw/master/doc/web/ems_dashboard.PNG) | +| -------------------------------------------------------------------------------- | -------------------------------------------------------------------------------- | + +#### MQTT support for Home Assistant and Domoticz ![ha](https://github.com/proddy/EMS-ESP/raw/master/doc/home_assistant/ha.png) -**Using BBQKees' [EMS Gateway](https://shop.hotgoodies.nl/ems/) circuit:** - -| ![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) | -| - | - | - | - -**Example of the EMS-ESP's telnet console:** +#### Telnet for advanced configuration and verbose traffic logging | ![telnet menu](https://github.com/proddy/EMS-ESP/raw/master/doc/telnet/telnet_menu.jpg) | ![telnet menu](https://github.com/proddy/EMS-ESP/raw/master/doc/telnet/telnet_stats.PNG) | -| - | - | +| --------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------- | --- -## The latest list of support EMS devices +## Current list of supported EMS devices ### Thermostats: @@ -34,7 +36,7 @@ The code is writen for the Espressif **ESP8266** microcontroller and supports a * RC20F * RC30/Nefit Moduline 400 * RC35 (only a single HC) -* RC300/RC310/Nefit Moduline 3000 +* RC300/RC310/RC3000 * Nefit Moduline 1010 * Junkers FR10 * TC100/Nefit Easy (read-only) @@ -72,3 +74,11 @@ The code is writen for the Espressif **ESP8266** microcontroller and supports a * EMS-OT OpenTherm converter * Web Gateway KM200 * HeatPump Module + +## Compatible with EMS Gateway + +Using BBQKees' [EMS Gateway](https://shop.hotgoodies.nl/ems/) board with integrated Wemos D1: + +| ![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) | +| ------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------- | + diff --git a/doc/Domoticz/nefit/mqtt.py b/doc/Domoticz/nefit/mqtt.py deleted file mode 100644 index 21ddc42a5..000000000 --- a/doc/Domoticz/nefit/mqtt.py +++ /dev/null @@ -1,113 +0,0 @@ -# Based on https://github.com/emontnemery/domoticz_mqtt_discovery -import Domoticz -import time - -class MqttClient: - Address = "" - Port = "" - mqttConn = None - isConnected = False - mqttConnectedCb = None - mqttDisconnectedCb = None - mqttPublishCb = None - - def __init__(self, destination, port, mqttConnectedCb, mqttDisconnectedCb, mqttPublishCb, mqttSubackCb): - Domoticz.Debug("MqttClient::__init__") - self.Address = destination - self.Port = port - self.mqttConnectedCb = mqttConnectedCb - self.mqttDisconnectedCb = mqttDisconnectedCb - self.mqttPublishCb = mqttPublishCb - self.mqttSubackCb = mqttSubackCb - self.Open() - - def __str__(self): - Domoticz.Debug("MqttClient::__str__") - if (self.mqttConn != None): - return str(self.mqttConn) - else: - return "None" - - def Open(self): - Domoticz.Debug("MqttClient::Open") - if (self.mqttConn != None): - self.Close() - self.isConnected = False - self.mqttConn = Domoticz.Connection(Name=self.Address, Transport="TCP/IP", Protocol="MQTT", Address=self.Address, Port=self.Port) - self.mqttConn.Connect() - - def Connect(self): - Domoticz.Debug("MqttClient::Connect") - if (self.mqttConn == None): - self.Open() - else: - ID = 'Domoticz_'+str(int(time.time())) - Domoticz.Log("MQTT CONNECT ID: '" + ID + "'") - self.mqttConn.Send({'Verb': 'CONNECT', 'ID': ID}) - - def Ping(self): - Domoticz.Debug("MqttClient::Ping") - if (self.mqttConn == None or not self.isConnected): - self.Open() - else: - self.mqttConn.Send({'Verb': 'PING'}) - - def Publish(self, topic, payload, retain = 0): - Domoticz.Log("MqttClient::Publish " + topic + " (" + payload + ")") - if (self.mqttConn == None or not self.isConnected): - self.Open() - else: - self.mqttConn.Send({'Verb': 'PUBLISH', 'Topic': topic, 'Payload': bytearray(payload, 'utf-8'), 'Retain': retain}) - - def Subscribe(self, topics): - Domoticz.Debug("MqttClient::Subscribe") - subscriptionlist = [] - for topic in topics: - subscriptionlist.append({'Topic':topic, 'QoS':0}) - if (self.mqttConn == None or not self.isConnected): - self.Open() - else: - self.mqttConn.Send({'Verb': 'SUBSCRIBE', 'Topics': subscriptionlist}) - - def Close(self): - Domoticz.Log("MqttClient::Close") - #TODO: Disconnect from server - self.mqttConn = None - self.isConnected = False - - def onConnect(self, Connection, Status, Description): - Domoticz.Debug("MqttClient::onConnect") - if (Status == 0): - Domoticz.Log("Successful connect to: "+Connection.Address+":"+Connection.Port) - self.Connect() - else: - Domoticz.Log("Failed to connect to: "+Connection.Address+":"+Connection.Port+", Description: "+Description) - - def onDisconnect(self, Connection): - Domoticz.Log("MqttClient::onDisonnect Disconnected from: "+Connection.Address+":"+Connection.Port) - self.Close() - # TODO: Reconnect? - if self.mqttDisconnectedCb != None: - self.mqttDisconnectedCb() - - def onMessage(self, Connection, Data): - topic = '' - if 'Topic' in Data: - topic = Data['Topic'] - payloadStr = '' - if 'Payload' in Data: - payloadStr = Data['Payload'].decode('utf8','replace') - payloadStr = str(payloadStr.encode('unicode_escape')) - - if Data['Verb'] == "CONNACK": - self.isConnected = True - if self.mqttConnectedCb != None: - self.mqttConnectedCb() - - if Data['Verb'] == "SUBACK": - if self.mqttSubackCb != None: - self.mqttSubackCb() - - if Data['Verb'] == "PUBLISH": - if self.mqttPublishCb != None: - self.mqttPublishCb(topic, Data['Payload']) \ No newline at end of file diff --git a/doc/Domoticz/nefit/plugin.py b/doc/Domoticz/nefit/plugin.py deleted file mode 100644 index abcf78c18..000000000 --- a/doc/Domoticz/nefit/plugin.py +++ /dev/null @@ -1,165 +0,0 @@ -""" - - - Plugin to control Nefit EMS-ESP with ' Proddy' firmware
-
- Automatically creates Domoticz devices for connected device.
- Do not forget to "Accept new Hardware Devices" on first run
-
- - - - - - - - -
-""" - -import Domoticz -import json -import time -from mqtt import MqttClient - -class Thermostat: - def checkDevices(self): - if 1 not in Devices: - Domoticz.Debug("Create Temperature Device") - Domoticz.Device(Name="Woonkamer", Unit=1, Type=80, Subtype=5).Create() - if 2 not in Devices: - Domoticz.Debug("Create System Pressure Device") - Domoticz.Device(Name="System Pressure", Unit=2, Type=243, Subtype=9).Create() - if 3 not in Devices: - Domoticz.Debug("Create Thermostat Device") - Domoticz.Device(Name="Nefit", Unit=3, Type=242, Subtype=1).Create() - - def onMqttMessage(self, topic, payload): - if "thermostat_currtemp" in payload: - temp=round(float(payload["thermostat_currtemp"]),1) - Domoticz.Debug("Current temp: {}".format(temp)) - if Devices[1].sValue != temp: - Devices[1].Update(nValue=1, sValue=str(temp)) - if "sysPress" in payload: - pressure=payload["sysPress"] - Domoticz.Debug("System Pressure: {}".format(pressure)) - if Devices[2].sValue != pressure: - Devices[2].Update(nValue=1, sValue=str(pressure)) - if "thermostat_seltemp" in payload: - temp=payload["thermostat_seltemp"] - Domoticz.Debug("Temp setting: {}".format(temp)) - if Devices[3].sValue != temp: - Devices[3].Update(nValue=1, sValue=str(temp)) - - def onCommand(self, mqttClient, unit, command, level, color): - topic = "home/ems-esp/thermostat_cmd_temp" - if (command == "Set Level"): - mqttClient.Publish(topic, str(level)) - -class BasePlugin: - mqttClient = None - - def onStart(self): - self.debugging = Parameters["Mode6"] - - if self.debugging == "Verbose+": - Domoticz.Debugging(2+4+8+16+64) - if self.debugging == "Verbose": - Domoticz.Debugging(2+4+8+16+64) - if self.debugging == "Debug": - Domoticz.Debugging(2+4+8) - - self.controller = Thermostat() - - self.controller.checkDevices() - - self.topics = list(["home/ems-esp/thermostat_data", "home/ems-esp/boiler_data", "home/ems-esp/STATE"]) - self.mqttserveraddress = Parameters["Address"].replace(" ", "") - self.mqttserverport = Parameters["Port"].replace(" ", "") - self.mqttClient = MqttClient(self.mqttserveraddress, self.mqttserverport, self.onMQTTConnected, self.onMQTTDisconnected, self.onMQTTPublish, self.onMQTTSubscribed) - - def checkDevices(self): - Domoticz.Log("checkDevices called") - - def onStop(self): - Domoticz.Log("onStop called") - - def onCommand(self, Unit, Command, Level, Color): - Domoticz.Debug("Command: " + Command + " (" + str(Level)) - self.controller.onCommand(self.mqttClient, Unit, Command, Level, Color) - - def onConnect(self, Connection, Status, Description): - self.mqttClient.onConnect(Connection, Status, Description) - - def onDisconnect(self, Connection): - self.mqttClient.onDisconnect(Connection) - - def onMessage(self, Connection, Data): - self.mqttClient.onMessage(Connection, Data) - - def onHeartbeat(self): - Domoticz.Debug("Heartbeating...") - - # Reconnect if connection has dropped - if self.mqttClient.mqttConn is None or (not self.mqttClient.mqttConn.Connecting() and not self.mqttClient.mqttConn.Connected() or not self.mqttClient.isConnected): - Domoticz.Debug("Reconnecting") - self.mqttClient.Open() - else: - self.mqttClient.Ping() - - def onMQTTConnected(self): - Domoticz.Debug("onMQTTConnected") - self.mqttClient.Subscribe(self.topics) - - def onMQTTDisconnected(self): - Domoticz.Debug("onMQTTDisconnected") - - def onMQTTSubscribed(self): - Domoticz.Debug("onMQTTSubscribed") - - def onMQTTPublish(self, topic, rawmessage): - Domoticz.Debug("MQTT message: " + topic + " " + str(rawmessage)) - - message = "" - try: - message = json.loads(rawmessage.decode('utf8')) - except ValueError: - message = rawmessage.decode('utf8') - - if (topic in self.topics): - self.controller.onMqttMessage(topic, message) - -global _plugin -_plugin = BasePlugin() - -def onStart(): - global _plugin - _plugin.onStart() - -def onStop(): - global _plugin - _plugin.onStop() - -def onConnect(Connection, Status, Description): - global _plugin - _plugin.onConnect(Connection, Status, Description) - -def onDisconnect(Connection): - global _plugin - _plugin.onDisconnect(Connection) - -def onMessage(Connection, Data): - global _plugin - _plugin.onMessage(Connection, Data) - -def onCommand(Unit, Command, Level, Color): - global _plugin - _plugin.onCommand(Unit, Command, Level, Color) - -def onHeartbeat(): - global _plugin - _plugin.onHeartbeat() diff --git a/doc/Domoticz/readme.txt b/doc/Domoticz/readme.txt deleted file mode 100644 index 0f39859be..000000000 --- a/doc/Domoticz/readme.txt +++ /dev/null @@ -1,10 +0,0 @@ -to install the plugin: -- copy the directory 'nefit' to the domoticz/plugins directory -- make sure that 'Accept new Hardware Devices' is enabeled in settings/sysem -- create new hardware with type 'Nefit EMS-ESP with Proddy firmware' -- set MQTT server and port - -The plugin crrently creates 3 devices: -- a room temperature meter -- a system pressure meter -- a thermostat setpoint control \ No newline at end of file diff --git a/doc/HT3-Bus_Telegramme.html b/doc/HT3-Bus_Telegramme.html new file mode 100644 index 000000000..1a019762c --- /dev/null +++ b/doc/HT3-Bus_Telegramme.html @@ -0,0 +1,7862 @@ + + + + + + + + + + + + + + + + + +
+

+

Übersicht

+ Telegramm Übersicht
+ ID 2
+ ID 7
+ ID 6
+ ID 190
+ ID 24
+ ID 25
+ ID 188
+ ID 27
+ ID 51
+ ID 52
+ ID 467...468
+ ID 26
+ ID 268
+ ID 296
+ ID 357...366
+ ID 367...376
+ ID 377...386
+ ID 677...684
+ ID 259
+ ID 260
+ ID 866
+ ID 868
+ ID 873
+ ID 874
+ ID 910
+ ID 913
+ ID 357_366_14_Modem
+ ID 377_387_4_Modem
+ ID 357...366_1x_Modem
+ ID 377...386_x_Modem
+ +

+
+

Tabelle 1: Telegramm Übersicht

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
HT Bus-Telegramme







Datum:14.10.2016


Version:0.2.0







Message-IDTelegramm(hex)BeschreibungSource-Werte (hex)Bemerkung



(SO)
2SO TT 02 xySoftware-Version / Busteilnehmer88TT = <Target-/Token-Nr>
7SO 00 07 xySteuerung: EMS Token Status88
6SO 00 06 xyDatum / Zeit90 | 98Mit 14 und 17 Bytes Länge
190TT 00 BE xyErrorCode / DisplayCode von Target
TT = <Target-/Token-Nr>
24SO 00 18 xyHeizgerät: Kesseldaten88Mit 31 und 33 Bytes Länge
25SO 00 19 xyHeizgerät: Heizungsdaten88
188SO 00 BC xyHeizgerät: Hybrid (Wärmepumpe)

27SO 00 1B xySollwert Warmwasser90
51SO 00 33 xyWarmwasser: Daten von Steuerung88
52SO 00 34 xyWarmwasser: Daten von Steuerung | IPM88 | Ax (x:=0...7)Mit 22,23 und 25 Bytes Länge
467...468SO 00 FF xy 00 D3...D4Betriebsart WW-System90
26SO 08 1A xyHeizkreis: Systemwerte90
268SO 00 FF xy 00 0CHeizkreis: von IPM1/IPM2 für MischerAx (x:=0...7)
296SO 00 FF xy 00 28Heizkreis: Fehlermeldungen90
357...366SO 00 FF xy 00 65...6EHeizkreis: Bauart190
367...376SO 00 FF xy 00 6F...78Heizkreis: Temperaturniveau90 | 9x (x:=8...F)
377...386SO 00 FF xy 00 79...82Heizkreis: Bauart290
677...684SO 00 FF xy 01 A5...ACHeizkreis: Systemwerte90 | 98Cxyz-Controller (z.B. CW100)
259SO 00 FF xy 00 03Solar: Solardaten von ISM1B0
260SO 00 FF xy 00 04Solar: Solardaten von ISM2B0Mit 24 und 35 Bytes Länge
866SO 00 FF xy 02 62Solar: Solardaten von MS100B0EMS2-Bus
868SO 00 FF xy 02 64Solar: Solardaten von MS100B0EMS2-Bus
873SO 00 FF xy 02 69Solar: Solardaten von MS100B0EMS2-Bus
874SO 00 FF xy 02 6ASolar: Solardaten von MS100B0EMS2-Bus
910SO 00 FF xy 02 8ESolar: Solardaten von MS100B0EMS2-Bus
913SO 00 FF xy 02 91Solar: Solardaten von MS100B0EMS2-Bus
357_14...366_14SO TA FF 0E 00 65...6EModem-CMD: Betriebsart setzen8D | C8TA = <Target-Nr>
377_4 ...386_4SO TA FF 04 00 79...82Modem-CMD: Betriebsart setzen8D | C8TA = <Target-Nr>
357_17...366_17SO TA FF 11 00 65...6EModem-CMD: Temp-Niveau setzen8D | C8TA = <Target-Nr>
377_7 ...386_7SO TA FF 07 00 79...82Modem-CMD: Temp-Niveau setzen8D | C8TA = <Target-Nr>





1: ( Hi-Byte * 256 + Lo-Byte ) / 10
Calculation-Type: 1
2: ( Byte3 * 65536 + Byte2 * 256 + Byte1 )
Calculation-Type: 2
3: ( Byte4 * 1048576 + Byte3 * 65536 + Byte2 * 256 + Byte1 )
Calculation-Type: 3
4: ( Type 3 ) / 10
Calculation-Type: 4
5: ( Type 3 ) / 1000
Calculation-Type: 5
+ +
+

Tabelle 2: ID 2

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
HT Bus-Telegramme



Message-ID: 2_x_0

ByteWerte (Hex)BemerkungBedeutung / IDBeispiel (Hex)

16Byte




Telegramm: Software-Version / Busteilnehmer

0SO
Source88
1TT<Token-/Target-Nr> (Geräteadr. Ungleich 0)Target18
202
2_x_002
3xy Telegramm-Offset (hier 0...9).
00
4xyErste Erkennung Busteilnehmer2_0_05F


- 00 = Variantenerkennung in Betrieb oder fehlerhaft



…..



- 5F = Heatronic III



- 64 = Schaltmodul IPM1



- 65 = Solarmodul ISM1



- 66 = Schaltmodul IPM2



- 67 = Solarmodul ISM2



- 67 = Solarmodul ISM2



- 69 = Witterungsgeführter Regler FW100



- 6A = Witterungsgeführter Regler FW200



- 6B = Raumtemperaturregler FR100



- 6C = Raumtemperaturregler FR110



- 6D = Fernbedienung FB 10



- 6E = Fernbedienung FB100



- 6F = Raumtemperaturregler FR10



…..



- BD = KM200



- BF = Raumtemperaturregler FR120



- C0 = Witterungsgefuehrter Regler FW120



…..

5xySoftware-Familie2_1_022
6xyVersion der Softwarefamilie2_2_004
7xyZweite Erkennung Busteilnehmern2_3_000
8xyKennzahl f. Grosse Änderung in HW- und SW2_4_000
9xyKennzahl f. Kleine Änderung in HW- und SW2_5_000
10xyDritte Erkennung Busteilnehmern2_6_000
11xyKennzahl f. Kleine Änderung in HW- und SW2_7_000
12xyKennzahl f. Grosse Änderung in HW- und SW2_8_000
13xyMarkenidentifizierung2_9_000


- 00 = keine Markenerkennung



- 01 = Bosch



- 02 = Junkers



- 03 = Buderus



…..

14<CRC>CRC
63
15<Ende>Ende
00
+ +
+

Tabelle 3: ID 7

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
HT Bus-Telegramme


Message-ID: 7_x_y
ByteWerte (Hex)BemerkungBedeutung / ID

21Byte



EMS Token Status
0SO
Source
100
Target
207
7_x_y
3xy Telegramm-Offset (hier 0...14).
4Bit0...Bit7EMS Token Status 8: EMS Master 7_0_0 bis


- EMS Token Status 9 … 157_0_7
5Bit0...Bit7EMS Token Status 16 … 23 7_1_0 bis



7_1_7
6Bit0...Bit7Busadresse 24 vorhanden7_2_0 bis


- EMS Token Status 25 … 31 7_2_7
7Bit0...Bit7Busadresse 32:Mischerstellmotor im HK1 vorhanden7_3_0 bis


- EMS Token Status 33 … 397_3_7
8Bit0...Bit7Busadresse 40:Warmwassersystem im HK1 vorhanden7_4_0 bis


- EMS Token Status 41 … 477_4_7
9Bit0...Bit7Busadresse 48:Solarmodul vorhanden7_5_0 bis


- EMS Token Status 49 … 557_5_7
10Bit0...Bit7Busadresse 56:Fernbedienung f. HK1 vorhanden7_6_0 bis


- EMS Token Status 57 … 637_6_7
11Bit0...Bit7Busadresse 64:Temperaturfühler im HK1 vorhanden7_7_0 bis


- EMS Token Status 65 … 717_7_7
12Bit0...Bit7Status für Busadresse 72...797_8_0 bis



7_8_7
13Bit0...Bit7EMS Token Status 80 … 877_9_0 bis



7_9_7
14Bit0...Bit7EMS Token Status 88 … 957_10_0 bis



7_10_7
15Bit0...Bit7EMS Token Status 96 … 1037_11_0 bis



7_11_7
16Bit0...Bit7EMS Token Status 104 … 1117_12_0 bis



7_12_7
17Bit0...Bit7EMS Token Status 112 … 119 (Cascaded EMS)7_13_0 bis



7_13_7
18Bit0...Bit7EMS Token Status 120 … 127 (Cascaded EMS)7_14_0 bis



7_14_7
19<CRC>CRC
20<Ende>Ende Marker
+ +
+

Tabelle 4: ID 6

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
HT Bus-Telegramme



Message-ID:6_x_y
ByteWerte (Hex)BemerkungBedeutung / ID

14Byte 17Byte




Datum/Zeit – Telegramm
090 | 9890 | 98Source :=90h oder :=98hSource
10000
Target
20606
6_x_y
3xyxy Telegramm-Offset (hier 0...6|10).
4xyxyJahr (Wert + 2000)dez.6_0_0
5xyxyMonat (01 … 12)dez.6_1_0
6xyxyStunden (00 … 23)dez.6_2_0
7xyxyTag (01 … 31)dez.6_3_0
8xyxyMinute (00 … 59)dez.6_4_0
9xyxySekunde (00 … 59)dez.6_5_0
10xyxyWochentag6_6_0



01=Montag; 02=Dienstag;... für Fxyz – Regler



00=Montag; 01=Dienstag;... für Cxyz – Regler
11Bit0...Bit7Bit0...Bit7Uhrstatus

Bit0Bit0- Sommerzeit6_7_0

Bit1Bit1- Funkempfang vorhanden6_7_1

Bit2Bit2- Funksignal vorhanden6_7_2

Bit3...Bit7Bit3...Bit7- Immer 0
12<CRC>xyToken-Adresse des aktuellen RTC-Owner6_8_0
13<Ende>xyAutomatische Sommer/Winter Umschaltung6_9_0
14
xyRTC Benutzer Kalibierungswert6_10_0
15
<CRC>

16
<Ende>

+ +
+

Tabelle 5: ID 190

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
HT Bus-Telegramme


Message-ID: 190_x_0
ByteWerte (Hex)BemerkungBedeutung / ID

11Byte



ErrorCode von Target-/Token
0TTTarget-/Token NummerSource
100- 00 = An AlleTarget
2BE
190_x_0
300Immer 0
4xyBus-Adresse des Fehlercodes190_0_0
5Hi-ByteDisplaycode190_1_0
6Lo-Byte
7Hi-ByteCause Code190_3_0
8Lo-Byte
9<CRC>CRC
10<Ende>Ende Marker
+ +
+

Tabelle 6: ID 24

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
HT Bus-Telegramme



Message-ID:24_x_y
ByteWerte (Hex)BemerkungBedeutung / ID

31Byte 33Byte




Kessel-Telegramm: Heizgerät
08888
Source
10000
Target
21818
24_x_y
3xyxy Telegramm-Offset (hier 0...25).
4xyxyVorlauf Soll-Temperatur24_0_0
5Hi-ByteHi-ByteVorlauf Ist-Temperatur24_1_0
6Lo-ByteLo-Byte
7xyxyKessel maximale Leistung (76/84/100; 100)%24_3_0
80-1000-100Aktuelle Brennerleistung in %24_4_0
9Bit0...Bit7BitfeldBetriebsmode

Bit0Bit1- Heizungs-Mode24_5_0

Bit1Bit2- Warmwasser-Mode24_5_1

Bit2Bit3:=0- Status Servicebetrieb24_5_2

Bit3Bit4- Brennerflamme an24_5_3

Bit4Bit5:=0- Aufheizphase des Wärmeerzeugers24_5_4

Bit5Bit6:=0- Verriegelnder Fehler24_5_5

Bit6Bit7:=0- Blockierender Fehler24_5_6

Bit7Bit8:=0- Status Wartungsanforderung24_5_7
10Bit0...Bit7Bit0...Bit7Status Heizbetrieb

Bit0Bit0- Heizbetrieb im Bussystem24_6_0

Bit1Bit1- Wärmeanforderung (durch Schalter)24_6_1

Bit2Bit2- Wärmeanforderung bei Betriebsart: Frost24_6_2

Bit3Bit3- Wärmeanforderung im WW-Betrieb bei Betriebsart: Frost24_6_3

Bit4Bit4- Interne Wärmeanforderung bei WW24_6_4

Bit5Bit5- Wärmeanforderung f. WW-Erkennung im Bussystem24_6_5

Bit6Bit6- Wärmeanforderung24_6_6

Bit7Bit7- Wärmeanforderung im Testmodus24_6_7
11Bit0...Bit7Bit0...Bit7Betriebs-Status

Bit0Bit0- Brenner an (Relais-Signal erste Brennstufe)24_7_0

Bit1Bit1- Brenner an (Relais-Signal zweite Brennstufe)24_7_1

Bit2Bit2- Lüfter an (Relais-Signal f. Lüfter)24_7_2

Bit3Bit3- Zündung an (Relais-Signal f. Zündung)24_7_3

Bit4Bit4- Ölvorwärmer an (Relais-Signal f. Ölvorwärmer)24_7_4

Bit5Bit5- Heizungspumpe an (Relais-Signal f. HP)24_7_5

Bit6Bit6- 3-Wege-Ventil auf Speicherladung24_7_6

Bit7Bit7- Zirkulationspumpe an (Relais-Signal f. ZP)24_7_7
12Bit0...Bit7Bit0...Bit7Status 1

Bit0Bit0- Meldesignal Abgasklappe f. Freigabe Ölbrenner24_8_0

Bit1Bit1- Signal vom Luftdruckschalter24_8_1

Bit2Bit2- Signal vom Flüssiggasbrenner24_8_2

Bit3Bit3- Signal vom Gasdruckwächter24_8_3

Bit4Bit4- Signal vom externen Ein-/Aus-Schalter24_8_4

Bit5Bit5- Digitales Eingangssignal24_8_5

Bit6Bit6- Signal vom Sicherheitstemperaturbegrenzer (TB)24_8_6

Bit7Bit7- Signal vom Raumthermostat24_8_7
13Hi-ByteHi-ByteWW-Temperatur Speicherfühler124_9_0
14Lo-ByteLo-Byte- (0x8300 := Nicht vorhanden)
15Hi-ByteHi-ByteWW-Temperatur Speicherfühler224_11_0
16Lo-ByteLo-Byte- (0x8000 | 0x7D00 := Nicht vorhanden)
17Hi-ByteHi-ByteTemperatur Kessel-Rücklauf24_13_0
18Lo-ByteLo-Byte- (0x8000 | 0x7D00 := Nicht vorhanden)
19Hi-ByteHi-ByteIonisationsstrom24_15_0
20Lo-ByteLo-Byte
21FFFFAnlagendruck am Wärmeerzeuger24_17_0



- (FF := ungültig)
22Hi-ByteHi-ByteDisplaycode24_18_0
23Lo-ByteLo-Byte
24Hi-ByteHi-ByteCause Code24_20_0
25Lo-ByteLo-Byte
2600FFWarmwasserdurchfluss-Menge24_22_0



- (FF := ungültig)
27Bit0...Bit7Bit0...Bit7Status 2

Bit0Bit0- Status Speicherlade-Pumpe (SP)24_23_0

Bit1Bit1- Flüssiggasventil an24_23_1

Bit2Bit2- Status Gaswärmepumpe24_23_2

Bit3Bit3- Status d. Relais im Schaltmodul UM1024_23_3

Bit4Bit4- Zirkulationspumpe an (Relais-Signal f. ZP)24_23_4

Bit5Bit5- Status Brenner Relais24_23_5

Bit6Bit6- FB reservierte Bit24_23_6

Bit7Bit7- FB reservierte Bit24_23_7
28Bit0...Bit7Bit0...Bit7Status 3

Bit0Bit0- Status der Füllfunktion 24_24_0

Bit1Bit1- Status Schaltmodul UM1024_24_1

Bit2Bit2- UM10 Signal für Brenner-Blockierung24_24_2

Bit3Bit3- Brennerfreigabe durch Schaltmodul24_24_3

Bit4Bit4- Status Brenneranlauf im Schaltmodul24_24_4

Bit5Bit5- Heizbetrieb blockiert bei Heatronic III24_24_5

Bit6Bit6- STB – Test aktiv24_24_6

Bit7Bit7- Tastensperre ein24_24_7
29<CRC>Hi-ByteCRC | Hi-Byte - Ansauglufttemperaturxy | 24_25_0
30<Ende>Lo-ByteEnde | Lo-Byte - Ansauglufttemperatur
31<CRC> -– | CRC
32<Ende> –- | Ende
+ +
+

Tabelle 7: ID 25

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
HT Bus-Telegramme


Message-ID:25_x_0
ByteWerte (Hex)BemerkungBedeutung / ID

33Byte



Kessel-Telegramm: Heizgerät
088
Source
100
Target
219
25_x_0
3xy Telegramm-Offset (hier 0...25).
4Hi-ByteAußentemperatur25_0_0
5Lo-Byte
6Hi-ByteMaximale Temperatur25_2_0
7Lo-Byte- 0x8000 = Sensorunterbrechung / Fühler nicht vorhanden


- 0x7FFF = Sensorkurzschluss
8Hi-ByteAbgastemperatur25_4_0
9Lo-Byte- 0x8000 = Sensorunterbrechung / Fühler nicht vorhanden


- 0x7FFF = Sensorkurzschluss
10Hi-ByteGasdruck / Luftdruck25_6_0
11Lo-Byte- 0xFFFF = Sensorunterbrechung / Fühler nicht vorhanden
12xyTaktsperre im Zweipunkt Betrieb25_8_0
13xyModulationsbereich Heizungspumpe (HP)25_9_0
14Byte 3Brennerstarts Total (für Warmwasser und Heizung)25_10_0
15Byte 2 „ ( Calculation-Type: 2 )
16Byte 1
17Byte 3Betriebsminuten Brenner Total (für Warmwasser und Heizung)25_13_0
18Byte 2 „ ( Calculation-Type: 2 )
19Byte 1
20Byte 3Betriebszeit f. Zweite Brennerstufe25_16_0
21Byte 2 „ ( Calculation-Type: 2 )
22Byte 1
23Byte 3Betriebsminuten Brenner (nur Heizung)25_19_0
24Byte 2 „ ( Calculation-Type: 2 )
25Byte 1
26Byte 3Brennerstarts (nur Heizung)25_22_0
27Byte 2 „ ( Calculation-Type: 2 )
28Byte 1
29Hi-ByteTemperatur an hydraulischer Weiche25_25_0
30Lo-Byte- 0x8000 = Sensorunterbrechung / Fühler nicht vorhanden


- 0x7FFF = Sensorkurzschluss
31<CRC>CRC
32<Ende>Ende Marker
+ +
+

Tabelle 8: ID 188

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
HT Bus-Telegramme


Message-ID:188_x_y
ByteWerte (Hex)BemerkungBedeutung / ID






Kessel-Telegramm: Heizgerät
088
Source
100
Target
2BC
188_x_y
3xy Telegramm-Offset (hier 0...13).
4Hi-ByteTemperatur Puffer-Speicher oben188_0_0
5Lo-Byte
6Hi-ByteTemperatur Puffer-Speicher unten188_2_0
7Lo-Byte
8Hi-ByteTemperatur Vorlauf Verflüssiger188_4_0
9Lo-Byte
10Hi-ByteTemperatur Rücklauf Verflüssiger188_6_0
11Lo-Byte
12Bit0...Bit7Betriebs-Status1

Bit0- Wärmepumpe188_8_0

Bit1
188_8_1

Bit2
188_8_2

Bit3
188_8_3

Bit4- Status Abtaumöglichkeit an W-Pumpe188_8_4

Bit5
188_8_5

Bit6
188_8_6

Bit7
188_8_7
13Bit0...Bit7Betriebs-Status2

Bit0- Abtaufunktion an W-Pumpe188_9_0

Bit1- Status Verdichter188_9_1

Bit2- Fehlerstatus Wärmepumpe188_9_2

Bit3
188_9_3

Bit4
188_9_4

Bit5
188_9_5

Bit6
188_9_6

Bit7
188_9_7
14<CRC>CRC
15<Ende>Ende
+ +
+

Tabelle 9: ID 27

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
HT Bus-Telegramme


Message-ID:27_0_0
ByteWerte (Hex)BemerkungBedeutung / ID

7Byte



Telegramm: Solltemperatur WW-System
090 Source
100
Target
21B
27_x_0
300Immer 00
432Sollwert Warmwasser-Temperatur27_0_0
5<CRC>CRC
6<Ende>Ende Marker
+ +
+

Tabelle 10: ID 51

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
HT Bus-Telegramme


Message-ID: 51_x_y
ByteWerte (Hex)BemerkungBedeutung / ID






Kessel-Telegramm: Warmwasser
088
Source
100
Target
233
51_x_y
3xy Telegramm-Offset (hier 0...12).
4xySoll-Temperatur Warmwasser51_0_0
5xy
51_1_0
6xySoll-Temperatur Warmwasser51_2_0
7xyTemperaturhysterese bei T-Soll51_3_0
8xy
51_4_0
9xy
51_5_0
10xy
51_6_0
11xy
51_7_0
12xy
51_8_0
13xy
51_9_0
14xy
51_10_0
15xy
51_11_0
16xy
51_12_0
17<CRC>

18<Ende>

+ +
+

Tabelle 11: ID 52

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
HT Bus-Telegramme






Message-ID: 52_x_y
ByteWerte (Hex)
BemerkungBedeutung / ID

23Byte 22Byte 25Byte





Kessel-Telegramm: Warmwasser
0888888
Source
1000000
Target
2343434
52_x_y
3xyxyxy Telegramm-Offset (hier 0...17).
4xyxyxySoll-Temperatur Warmwasser52_0_0
5Hi-ByteHi-ByteHi-ByteIst-Temperatur Warmwasser52_1_0
6Lo-ByteLo-ByteLo-Byte- 0x8000 = Sensorunterbrechung / Fühler nicht vorhanden




- 0x7FFF = Sensorkurzschluss
7Hi-ByteHi-ByteHi-ByteIst-Temperatur im Warmwasser - Speicher52_3_0
8Lo-ByteLo-ByteLo-Byte- 0x8000 = Sensorunterbrechung / Fühler nicht vorhanden




- 0x7FFF = Sensorkurzschluss
9Bit0...Bit7Bit0...Bit7Bit0...Bit7Warmwasser-Status

Bit0Bit0Bit0- WW-Bereitung im Normalbetrieb52_5_0

Bit1Bit1Bit1- Einmalige Speicher-Ladung52_5_1

Bit2Bit2Bit2- Thermische Desinfektion52_5_2

Bit3Bit3Bit3- Speicherladung im WW-System52_5_3

Bit4Bit4Bit4- Speicherladung im Nachwärmsystem52_5_4

Bit5Bit5Bit5- Erreichter Sollwert Warmwasser-Temperatur52_5_5

Bit6Bit6Bit6- Warmwasserbetrieb52_5_6

Bit7Bit7Bit7- Status f. Art der Warmwasserbereitung52_5_7




-- 0 = Warmwasserteilvorrang




-- 1 = Warmwasservorrang
10Bit0...Bit7Bit0...Bit7Bit0...Bit7WW-Fehlersignale

Bit0Bit0Bit0- WW-Temperaturfühler 1 defekt52_6_0

Bit1Bit1Bit1- WW-Temperaturfühler 2 defekt52_6_1

Bit2Bit2Bit2- WW-System wird nicht aufgeheizt52_6_2

Bit3Bit3Bit3- Thermische Desinfektion ist nicht in Betrieb52_6_3

Bit4Bit4Bit4- WW ist nicht blockiert52_6_4

Bit5...Bit7Bit5...Bit7Bit5...Bit7- Immer 052_6_5 bis





52_6_7
11Bit0...Bit7Bit0...Bit7Bit0...Bit7Zirkulationspumpen-Status

Bit0Bit0Bit0- Zirkulationspumpe (ZP) im Normalbetrieb52_7_0

Bit1Bit1Bit1- Zirkulationspumpe (ZP) an bei einmaliger Speicherladung52_7_1

Bit2Bit2Bit2- Zirkulationspumpe (ZP) an52_7_2

Bit3Bit3Bit3- Ansteuersignal f. Zirkulationspumpe (ZP)52_7_3

Bit4...Bit7Bit4...Bit7Bit4...Bit7- Immer 0
120...40...40...4Bauart des Warmwassersystems52_8_0




- 0 = ohne Warmwasserbereitung




- 1 = nach Durchlaufprinzip




- 2 = Druckloser Speicher




- 3 = Warmwasser-Speicherprinzip




- 4 = Schichtlade-Speicher
13xyxyxyAktuelle Wasserduchflussmenge52_9_0
14Byte 3Byte 3Byte 3Betriebszeit Warmwasser-Erzeugung (Minuten)52_10_0
15Byte 2Byte 2Byte 2
16Byte 1Byte 1Byte 1
17Byte 3Byte 3Byte 3Anzahl Brennerstarts für Warmwassererzeugung52_13_0
18Byte 2Byte 2Byte 2
19Byte 1Byte 1Byte 1
20xy<CRC>xyModulationsbereich ZP im WW-System 152_16_0
21<CRC><Ende>Hi-ByteHi-Byte Warmwasser Eingangstemperatur52_17_0
22<Ende>--Lo-ByteLo-Byte Warmwasser Eingangstemperatur




- 0x8000 = Sensorunterbrechung / Fühler nicht vorhanden




- 0x7FFF = Sensorkurzschluss
23

<CRC>

24

<Ende>

+ +
+

Tabelle 12: ID 467...468

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
HT Bus-Telegramme


Message-ID:467_x_0 bis 468_x_0
ByteWerte (Hex)BemerkungBedeutung / ID

11Byte



Telegramm: Betriebsart WW-System
090 Source
100
Target
2FF
EMS Marker
3xy
EMS Offset
400Immer 00EMS Type(H)
5D3 / D4WW-System


- D3=WW-System1467_0_0


- D4=WW-System2468_0_0
6xyBetriebsart Warmwasser-System


- 0=Automatikbetrieb f. WW-Speicher


- 1=Automatikbetrieb b. Kombigerät aktiv


- 2=Automatikbetrieb b. Kombigerät ausgeschaltet


- 3=Automatikbetrieb i. Urlaubsmodus f. WW-Speicher


- 4=Urlaubsfunktion eingeschaltet a. Kombigerät


- 5=Urlaubsfunktion ausgeschaltet a. Kombigerät


- 6=Fest eingestellte Speichertemperatur im Urlaubsprogramm


- 7=Thermische Desinfektion f. WW-Speicher


- 8=Warmwasser sofort


- 9=Estrichtrocknung in Betrieb oder angehalten
7xyWert f. Temperaturreduzierung bei solarer Unterstuetzung467_1_0 bis 468_1_0
8xyStatus der letzten thermischen Desinfektion467_2_0 bis 468_2_0


- 0=Abgeschlossen


- 1=In Betrieb


- 2=Abgebrochen
9<CRC>CRC
10<Ende>Ende Marker
+ +
+

Tabelle 13: ID 26

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
HT Bus-Telegramme



Message-ID:26_x_0

ByteWerte (Hex)BemerkungBedeutung / IDBeispiel (Hex)

11Byte




Telegramm: Heizkreis Systemwerte

090
Source
108Target = SteuerungTarget
21AImmer 1A26_x_0
3xy Telegramm-Offset (hier 0...4).

426Sollwert f. Vorlauftemperatur im Heizkreis26_0_0
564Maximale Leistung des Wärmeerzeugers26_1_0
664Sollwert f. Drehzahl der Umwälzpumpe26_2_0
70 / FFStatus f. Aufheizen mit hohem Wirkungsgrad26_3_0
83Betriebsart f. Umwälzpumpe im Energiesparmodus26_4_0
9<CRC>CRC

10<Ende>Ende Marker

+ +
+

Tabelle 14: ID 268

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
HT Bus-Telegramme


Message-ID:268_x_0
ByteWerte (Hex)BemerkungBedeutung / ID

14Byte



IPM – Telegramm (Schaltmodul)
0A0...A7
Source
100
Target
2FF
EMS Marker
3xy
EMS Offset
400Immer 00EMS Type(H)
50CImmer 0CEMS Type(L)
60...2Bauart des Heizkreises (Mischer ja/nein)


- 0=Nicht vorhanden


- 1=Ungemischter Heizkreis268_0_0


- 2=Gemischter Heizkreis268_0_1
70...1Status Heizungspumpe im Heizkreis268_1_0


- 0=Pumpe aus


- 1=Pumpe Ein
8xyMischer Position (Prozentwert)268_2_0
9Hi-ByteVorlauftemperatur 'Ist' für gemischten Heizkreis268_3_0
10Lo-Byte
11xySollwert Vorlauftemperatur (Grad)268_5_0
12<CRC>CRC
13<Ende>Ende Marker
+ +
+

Tabelle 15: ID 296

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
HT Bus-Telegramme


Message-ID:296_x_0
ByteWerte (Hex)BemerkungBedeutung / ID

32Byte



Telegramm: Heizkreis Fehlermeldungen
090
Source
100
Target
2FF
EMS Marker
3xyOffset auf nächste FehlermeldungEMS Offset
400Immer 00EMS Type(H)
528FehlerEMS Type(L)
6xy1Fehler1: Display-Code1296_0_0
7xy2Fehler1: Display-Code2296_1_0
8Hi-ByteFehler1: Fehlercode296_2_0
9Lo-Byte

10xyFehler1: Jahr (+2000)296_4_0
11xyFehler1: Monat296_5_0
12xyFehler1: Stunde296_6_0
13xyFehler1: Tag296_7_0
14xyFehler1: Minute296_8_0
15Hi-ByteFehler1: Minute (Reserviert)296_9_0
16Lo-Byte

17xyFehler1: Busadresse296_11_0
18xy1Fehler2: Display-Code1296_12_0
19xy2Fehler2: Display-Code2296_13_0
20Hi-ByteFehler2: Fehlercode296_14_0
21Lo-Byte

22xyFehler2: Jahr (+2000)296_16_0
23xyFehler2: Monat296_17_0
24xyFehler2: Stunde296_18_0
25xyFehler2: Tag296_19_0
26xyFehler2: Minute296_20_0
27Hi-ByteFehler2: Minute (Reserviert)296_21_0
28Lo-Byte

29xyFehler2: Busadresse296_23_0
30<CRC>CRC
31<Ende>Ende Marker
+ +
+

Tabelle 16: ID 357...366

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
HT Bus-Telegramme


Message-ID:357_x_0 bis 366_x_0
ByteWerte (Hex)BemerkungBedeutung / ID

29Byte



Telegramm: Heizkreis Steuerung


(Bauart des Heizkreises)
090
Source
100
Target
2FF
EMS Marker
3xy
EMS Offset
400Immer 00EMS Type(H)
565...6EHeizkreis xEMS Type(L)

65 65=Heizkreis1357_0_0

66 66=Heizkreis2358_0_0

67 67=Heizkreis3359_0_0

68 68=Heizkreis4360_0_0

69 69=Heizkreis5361_0_0

6A 6A=Heizkreis6362_0_0

6B 6B=Heizkreis7363_0_0

6C 6C=Heizkreis8364_0_0

6D 6D=Heizkreis9365_0_0

6E 6E=Heizkreis10366_0_0
60...3Bauart-Werte357_1_0 bis


- 0=Nicht vorhanden366_1_0


- 1=Ungemischter Heizkreis ohne Schaltmodul IPM


- 2=Ungemischter Heizkreis mit Schaltmodul IPM


- 3=Gemischter Heizkreis
70...2Fernbedienung für Heizkreis x


- 0=Nicht vorhanden


- 1=Fernbedienung FB 10


- 2=Fernbedienung FB100
80...4Bauart des Heizkreis x


- 0=nicht definiert357_2_0 bis


- 1=Fußpunkt/Endpunkt366_2_0


- 2=Radiatoren


- 3=Konvektoren


- 4=Fußbodenheizung
9z.B. 19Fußpunkt für Heizkurve (in Grad)357_3_0 bis



366_3_0
10z.B. 30Endpunkt für Heizkurve (in Grad)357_4_0 bis



366_4_0
11z.B. 50Maximale Vorlauftemperatur (in Grad) für Heizkreis x357_5_0 bis



366_5_0
12
Raumeinfluss-Faktor (%) im Heizkreis x357_6_0 bis



366_6_0
130...2Raumeinfluss im Heizkreis x bei Betriebsart357_7_0 bis


- 0=nicht definiert366_7_0


- 1=Normalbetrieb / Sparbetrieb / Frostschutzbetrieb


- 2=Sparbetrieb / Frostschutzbetrieb
14
Einstellung dauerhafte Raumtemperatur-Korrektur im Heizkreis x357_8_0 bis



366_8_0
150...3Betriebsart Raumtemperaturfühler für Heizkreis x357_9_0 bis


- 0=nicht definiert366_9_0


- 1=Externer Temperaturfühler


- 2=Interner Temperaturfühler


- 3=Temperatur im Sparmodus




160/FFStatus für Temperaturniveau Frost357_10_0 bis


- 0 = Aus366_10_0


- FF = Ein
17z.B. 2BAbschaltung (Außentemperaturgesteuert) von Heizkreis x357_11_0 bis


- (in 0.5 Grad Schritten)366_11_0
18
Frostgrenztemperatur für Heizkreis x357_12_0 bis


- (in 0.5 Grad Schritten)366_12_0
190...6Aktives Heizprogramm im Heizkreis x357_13_0 bis


- 0=nicht definiert366_13_0


- 1-6=Nummer des aktiven Heizprogramms


- (1:A; 2:=B;3:=C; …)
200....4Betriebsart für den Heizkreis x357_14_0 bis


- 0=nicht definiert366_14_0


- 1=Betrieb im Frostschutzmodus


- 2=Betrieb im Sparmodus


- 3=Betrieb im Normalmodus


- 4=Automatikbetrieb
21z.B. 14Temperaturniveau für Betriebsart Frost im Heizkreis x357_15_0 bis


- (in 0.5 Grad Schritten)366_15_0
22z.B. 28Temperaturniveau für Betriebsart Sparen im Heizkreis x357_16_0 bis


- (in 0.5 Grad Schritten)366_16_0
23z.B. 2BTemperaturniveau für Betriebsart Normal im Heizkreis x357_17_0 bis


- (in 0.5 Grad Schritten)366_17_0
240...3Aufheizgeschwindigkeit für Heizkreis x357_18_0 bis


- 0=nicht definiert366_18_0


- 1=Langsam


- 2=Normal


- 3=Schnell
250...4Urlaubsprogramm Betriebsart für Heizkreis x357_19_0 bis


- 0=nicht definiert366_19_0


- 1=Betrieb im Frostschutzmodus


- 2=Betrieb im Sparmodus


- 3=Betrieb im Normalmodus


- 4=Automatikbetrieb
26
Optimierungseinfluss für solare Unterstützung im Heizkreis x357_20_0 bis


- (in 1 Grad Schritten)366_20_0
27<CRC>CRC
28<Ende>Ende Marker
+ +
+

Tabelle 17: ID 367...376

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
HT Bus-Telegramme






Message-ID:367_x_0 bis 376_x_0
ByteWerte (Hex)BemerkungBedeutung / ID

17Byte 14Byte 9Byte





Telegramm: Heizkreis Steuerung




(Temperaturniveau für den Heizkreis)
090 | 9x90 | 9x90 | 9x (wobei: x:= 8...F)Source
1000000
Target
2FFFFFF
EMS Marker
3xyxyxy
EMS Offset
4000000Immer 00EMS Type(H)
56F...786F...786F...78Heizkreis-ZuordnungEMS Type(L)




6F=Heizkreis1367_0_0




70=Heizkreis2368_0_0




71=Heizkreis3369_0_0




72=Heizkreis4370_0_0




73=Heizkreis5371_0_0




74=Heizkreis6372_0_0




75=Heizkreis7373_0_0




76=Heizkreis8374_0_0




77=Heizkreis9375_0_0




78=Heizkreis10376_0_0
60...30...30...3Betriebsart Heizung:367_0_0 bis




- 0=nicht definiert376_0_0




- 1=Frost




- 2=Sparen




- 3=Heizen
70...50...5<CRC>Betriebsart Heizkreis | | CRC367_1_0 bis




- 0=nicht definiert376_1_0



- 1=dauernd



- 2=Automatikbetrieb



- 3=Urlaub



- 4=Estrichtrocknung im StandbyModus



- 5=Estrichtrocknung in Betrieb
8Hi-ByteHi-Byte<Ende>Soll-Temperatur (HK1 bis HK10) | | Ende Marker367_2_0 bis
9Lo-ByteLo-Byte376_2_0
10Hi-ByteHi-ByteIst-Temperatur (HK1 bis HK10 vom Regler)367_4_0 bis
11Lo-ByteLo-Byte376_4_0
12Hi-Byte<CRC>T-Raum FB10x | CRC367_6_0
13Lo-Byte<Ende>T-Raum FB10x | Ende Marker
1400 … 07Temperaturwert für solare Unterstützung der Vorlauftemperatur367_8_0
15<CRC>CRC
16<Ende>Ende Marker
+ +
+

Tabelle 18: ID 377...386

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
HT Bus-Telegramme


Message-ID:377_x_0 bis 386_x_0
ByteWerte (Hex)BemerkungBedeutung / ID

19Byte



Telegramm: Heizkreis Steuerung
090 (Bauart des Heizkreises)Source
100
Target
2FF
EMS Marker
3xy
EMS Offset
400Immer 00EMS Type(H)
579...82Heizkreis x KennungEMS Type(L)


79=Heizkreis1377_0_0


7A=Heizkreis2378_0_0


7B=Heizkreis3379_0_0


7C=Heizkreis4380_0_0


7D=Heizkreis5381_0_0


7E=Heizkreis6382_0_0


7F=Heizkreis7383_0_0


80=Heizkreis8384_0_0


81=Heizkreis9385_0_0


82=Heizkreis10386_0_0
60...3Bauart-Werte377_0_0 bis


- 0=Nicht vorhanden386_0_0


- 1=Ungemischter Heizkreis ohne Schaltmodul IPM


- 2=Ungemischter Heizkreis mit Schaltmodul IPM


- 3=Gemischter Heizkreis
7
Anpassungsfaktor im Heizkreis x377_1_0 bis



386_1_0
8
Verstärkungsfaktor im Heizkreis x377_2_0 bis



386_2_0
9
Maximale Vorlauftemperatur im Heizkreis x377_3_0 bis



386_3_0
100...4Betriebsart für Heizkreis x377_4_0 bis


- 0=nicht definiert386_4_0


- 1=Betrieb im Frostschutzmodus


- 2=Betrieb im Sparmodus


- 3=Betrieb im Normalmodus


- 4=Automatikbetrieb
11
Temperaturniveau bei Betriebsart Frost377_5_0 bis


- (in 0.5 Grad Schritten)386_5_0
12
Temperaturniveau bei Betriebsart Sparen377_6_0 bis


- (in 0.5 Grad Schritten)386_6_0
13
Temperaturniveau bei Betriebsart Normal377_7_0 bis


- (in 0.5 Grad Schritten)386_7_0
14
Urlaubsprogramm Betriebsart für Heizkreis x377_8_0 bis


- ( Werte wie bei Byte:10 Betriebsart Heizkreis)386_8_0
150/FFStatus Optimierungsfunktion im Heizkreis x377_9_0 bis


- 0 = Aus386_9_0


- FF = Ein
160...6Aktiviertes Heizprogramm377_10_0 bis


- 0=Nicht definiert386_10_0


- 1-6=Nummer des aktiven Heizprogramms


- (1=A; 2=B; 3=C; …)
17<CRC>CRC
18<Ende>Ende Marker
+ +
+

Tabelle 19: ID 677...684

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
HT Bus-Telegramme


Message-ID:677_x_0 bis 684_x_0
Byte
BemerkungBedeutung / ID






Telegramm: Heizkreis Steuerung


(Temperaturniveau für den Heizkreis)
090
Source
100
Target
2FF
EMS Marker
3xy
EMS Offset
401Immer 01EMS Type(H)
5A5...ACHeizkreis-ZuordnungEMS Type(L)


A5=Heizkreis1677_0_0


A6=Heizkreis2678_0_0


A7=Heizkreis3679_0_0


A8=Heizkreis4680_0_0


A9=Heizkreis5681_0_0


AA=Heizkreis6682_0_0


AB=Heizkreis7683_0_0


AC=Heizkreis8684_0_0
6Hi-ByteIst-Raumtemperatur (HK1 bis HK8)677_0_0 bis
7Lo-Byte684_0_0
8xyStatus Heizkreis6xy_2_0
9xy
6xy_3_0
10xy
6xy_4_0
11xy
6xy_5_0
12xySoll-Raumtemperatur (HK1 bis HK8)6xy_6_0
13xy
6xy_7_0
14Hi-Byte
6xy_8_0
15Lo-Byte
16xy
6xy_10_0
17xyTemperatur-Niveau6xy_11_0
18xy
6xy_12_0
19Hi-Byte
6xy_13_0
20Lo-Byte
21Hi-Byte
6xy_15_0
22Lo-Byte
23xy
6xy_17_0
24xy
6xy_18_0
25xy
6xy_19_0
26xy
6xy_20_0
27xyBetriebsstatus (HK1 bis HK8) {Auto / Manuell}6xy_21_0
28Hi-Byte
6xy_22_0
29Lo-Byte
30xy
6xy_24_0
31xy
6xy_25_0
32xy
6xy_26_0
33Hi-Byte
6xy_27_0
34Lo-Byte
35xy
6xy_29_0
36xy
6xy_30_0
37<CRC>CRC
38<Ende>Ende Marker
+ +
+

Tabelle 20: ID 259

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
HT Bus-Telegramme


Message-ID:259_x_0
ByteWerte (Hex)BemerkungBedeutung / ID

21Byte



ISM Solar-Telegramm
0B0
Source
100
Target
2FF
EMS Marker
3xy
EMS Offset
400Immer 00EMS Type(H)
503Immer 03EMS Type(L)
6xyOptimierungsfaktor WW mit solarer Unterstützung259_0_0
7xyOptimierungsfaktor Heiz. mit solarer Unterstützung259_1_0
8Hi-ByteSolarertrag in der letzten Stunde (Wh)259_2_0
9Lo-Byte
10Hi-ByteSolarkollektor1 Temperatur T1259_4_0
11Lo-Byte
12Hi-ByteSolarspeicher Temperatur T2259_6_0
13Lo-Byte
14Bit0...Bit7Betriebsart Solarpumpe (1. Kollektorfeld)

Bit0- Solarpumpe (SP); 0=aus; 1=ein259_8_0

Bit1- Relaysignal Umwälzpumpe(PE) bei thermischer Desinfektion259_8_1

Bit2..Bit7- Immer 0
15Bit0...Bit7Solar Systemstatus

Bit0- Abschaltung 1.Kollektorfeld bei Stagnation259_9_0


-- 0 =Nein


-- 1 =Ja (5 Grad Hysterese)

Bit1- Status Temperatur bei thermischer Desinfektion259_9_1

Bit2- Status Solarspeicher259_9_2


-- 0 =Nicht voll geladen


-- 1 =Voll geladen (2 Grad Hysterese)

Bit3-8 Immer 0
16Byte 3Laufzeit Solarpumpe (Minuten)
17Byte 2 „ ( Calculation-Type: 2 )259_10_0
18Byte 1
19<CRC>CRC
20<Ende>Ende Marker
+ +
+

Tabelle 21: ID 260

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
HT Bus-Telegramme



Message-ID:260_x_y
ByteWerte (Hex)
BemerkungBedeutung / ID

24Byte 35Byte




ISM Solar-Telegramm
0B0B0
Source
10000
Target
2FFFF
EMS Marker
3xyxy
EMS Offset
40000Immer 00EMS Type(H)
50404Immer 04EMS Type(L)
6Hi-Byte T3Hi-Byte T3Temperatur T3 im Pufferspeicher f. Rücklaufanhebung260_0_0
7Lo-Byte T3Lo-Byte T3
8Hi-ByteHi-ByteHeizungsrücklauftemperatur260_2_0
9Lo-ByteLo-Byte
10Hi-Byte T5Hi-Byte T5Temperatur T5 im Pufferspeicher (oben)260_4_0
11Lo-Byte T5Lo-Byte T5
12Hi-Byte T6Hi-Byte T6Temperatur T6 im Bereitschaftsspeicher (unten)260_6_0
13Lo-Byte T6Lo-Byte T6
14Hi-ByteHi-ByteTemperatur 2. Kollektorfeld260_8_0
15Lo-ByteLo-Byte
16Hi-ByteHi-ByteTemperatur TB im Pufferspeicher (oben)260_10_0
17Lo-ByteLo-Byte
18Hi-ByteHi-ByteTemperatur TC im Vorrang-/Nachrangspeicher260_12_0
19Lo-ByteLo-Byte
20Hi-ByteHi-ByteTemperatur am externen Wärmetauscher f. Solarsystem260_14_0
21Lo-ByteLo-Byte
22<CRC>Bit0...Bit7Status 1


Bit0- Betriebsart Ventil (DWU) f. Rücklaufanhebung260_16_0


Bit1- Relaisansteuerung f. Umwälzpumpe Umladesystem260_16_1


Bit2- Umwälzpumpe (PA) im 2. Kollektorfeld260_16_2


Bit3- Relaisansteuerung f. Umwälzpumpe (PB) Umladesystem260_16_3


Bit4- Betriebsart Umwälzpumpe (PC)/Umschaltventil260_16_4


Bit5- Betriebsart Umwälzpumpe (PD) im Sekundärkreis260_16_5


Bit6- Relaissignal bei Option F260_16_6


Bit7- unbenutzt260_16_7
23<Ende>Bit0...Bit7Status 2


Bit0- Ansteuerung Ventil DWU1 f. Rücklaufanhebung260_17_0


Bit1- Status maximale Temperatur im Umladespeicher260_17_1


Bit2- Status Umwälzpumpe (PA) im 2.Kollektorfeld (Stagnation)260_17_2


Bit3- Maximaltemperatur erreicht im WW-Speicher B260_17_3


Bit4- WW-Speicher geladen260_17_4


Bit5- Testmodus (Speicherladung Vorrangspeicher)260_17_5


Bit6- Maximaltemperatur erreicht im WW-Speicher C260_17_6


Bit7- Testmodus260_17_7
24
Byte 3Betriebszeit f. Solarmumpe (PA) im zweiten Kollektorfeld260_18_0
25
Byte 2
26
Byte 1
27
Hi-ByteZeitintervall f. Überprüfung ob Speicher C geladen wird260_21_0
28
Lo-Byte

29
Hi-ByteTemperatur TF 1 in Wärmequelle260_23_0
30
Lo-Byte

31
Hi-ByteTemperatur TF 2 in Wärmesenke260_25_0
32
Lo-Byte

33
<CRC>

34
<Ende>

+ +
+

Tabelle 22: ID 866

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
HT Bus-Telegramme


Message-ID:866
Byte
BemerkungBedeutung / ID






MS100 Solar-Telegramm
0B0
Source
100
Target
2FF
EMS Marker
3xy
EMS Offset
402Immer 02EMS Type(H)
562Immer 62EMS Type(L)
6Hi-ByteSolarkollektor1 Temperatur866_0_0
7Lo-Byte
8Hi-ByteSolarspeicher Temperatur unten866_2_0
9Lo-Byte
10Hi-ByteSolarspeicher Temperatur mittlerer Sensor866_4_0
11Lo-Byte
12Hi-ByteSolarkollektor2 Temperatur866_6_0
13Lo-Byte
14Hi-ByteSolarspeicher Beipass Temperatur866_8_0
15Lo-Byte
16Hi-ByteSolarspeicher Beipass Return-Temperatur866_10_0
17Lo-Byte
18Hi-Byte
866_12_0
19Lo-Byte
20Hi-Byte
866_14_0
21Lo-Byte
22Hi-Byte
866_16_0
23Lo-Byte
24Hi-Byte
866_18_0
25Lo-Byte
26Hi-Byte
866_20_0
27Lo-Byte
28Hi-Byte
866_22_0
29Lo-Byte
30Hi-Byte
866_24_0
31Lo-Byte
32<CRC>

33<Ende>

+ +
+

Tabelle 23: ID 868

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
HT Bus-Telegramme


Message-ID:868_x_y
Byte
BemerkungBedeutung / ID






MS100 Solar-Telegramm
0B0
Source
100
Target
2FF
EMS Marker
3xy
EMS Offset
402Immer 02EMS Type(H)
564Immer 64EMS Type(L)
6xy
868_0_0
7xy
868_1_0
8Bit0...Bit7
868_2_x

Bit0
868_2_0

Bit1
868_2_1

Bit2
868_2_2

Bit3
868_2_3

Bit4
868_2_4

Bit5
868_2_5

Bit6
868_2_6

Bit7
868_2_7
9Bit0...Bit7Solar Systemstatus868_3_x

Bit0- Abschaltung 1.Kollektorfeld bei Stagnation868_3_0


-- 0 =Nein


-- 1 =Ja

Bit1-- 1 =Solarspeicher maximale Temperatur erreicht868_3_1

Bit2-- 1 =Solarspeicher minimale Temperatur erreicht868_3_2

Bit3
868_3_3

Bit4
868_3_4

Bit5
868_3_5

Bit6
868_3_6

Bit7
868_3_7
10xy
868_4_0
11xy
868_5_0
12xy
868_6_0
13xy
868_7_0
14xy
868_8_0
15xyAktuelle Solarpumpen – Leistung868_9_0
16xy
868_10_0
17xy868_11_0
18xy
868_12_0
19xy868_13_0
20xy
868_14_0
21xy868_15_0
22<CRC>

23<Ende>

+ +
+

Tabelle 24: ID 873

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
HT Bus-Telegramme


Message-ID:873_x_0
Byte
BemerkungBedeutung / ID






MS100 Solar-Telegramm
0B0
Source
100
Target
2FF
EMS Marker
3xy
EMS Offset
402Immer 02EMS Type(H)
569Immer 69EMS Type(L)
6Byte 4Solarertrag letzte Stunde873_0_0
7Byte 3
8Byte 2 „ ( Calculation-Type: 4 )
9Byte 1
10Byte 4Solarertrag aktueller Tag873_4_0
11Byte 3
12Byte 2
13Byte 1
14Byte 4Solarertrag Summe873_8_0
15Byte 3
16Byte 2
17Byte 1
18<CRC>

19<Ende>

+ +
+

Tabelle 25: ID 874

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
HT Bus-Telegramme


Message-ID:874_x_0
Byte
BemerkungBedeutung / ID






MS100 Solar-Telegramm
0B0
Source
100
Target
2FF
EMS Marker
3xy
EMS Offset
402Immer 02EMS Type(H)
56AImmer 6AEMS Type(L)
6xy
874_0_0
7xy
874_1_0
8xy
874_2_0
9xy
874_3_0
10xy
874_4_0
11xy
874_5_0
12xy
874_6_0
13xy
874_7_0
14xy
874_8_0
15xy
874_9_0
16Bit0...Bit7
874_10_x

Bit0
874_10_0

Bit1
874_10_1

Bit2- Solarpumpe (SP); 0=aus; 1=ein874_10_2

Bit3
874_10_3

Bit4
874_10_4

Bit5
874_10_5

Bit6
874_10_6

Bit7
874_10_7
17xy
874_11_0
18xy
874_12_0
19xy874_13_0
20xy
874_14_0
21xy874_15_0
22<CRC>

23<Ende>

+ +
+

Tabelle 26: ID 910

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
HT Bus-Telegramme


Message-ID:910_x_0
Byte
BemerkungBedeutung / ID






MS100 Solar-Telegramm
0B0
Source
100
Target
2FF
EMS Marker
3xy
EMS Offset
402Immer 02EMS Type(H)
58EImmer 8EEMS Type(L)
6Byte 4Solarertrag letzte Stunde910_0_0
7Byte 3
8Byte 2 „ ( Calculation-Type: 4 )
9Byte 1
10Byte 4Solarertrag aktueller Tag910_4_0
11Byte 3
12Byte 2 „ ( Calculation-Type: 5 )
13Byte 1
14Byte 4Solarertrag Summe910_8_0
15Byte 3
16Byte 2 „ ( Calculation-Type: 4 )
17Byte 1
18<CRC>

19<Ende>

+ +
+

Tabelle 27: ID 913

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
HT Bus-Telegramme


Message-ID:913_x_0
Byte
BemerkungBedeutung / ID






MS100 Solar-Telegramm
0B0
Source
100
Target
2FF
EMS Marker
3xy
EMS Offset
402Immer 02EMS Type(H)
591Immer 91EMS Type(L)
6Byte 4Laufzeit Solarpumpe (Minuten)913_0_0
7Byte 3
8Byte 2 „ ( Calculation-Type: 2 )
9Byte 1
10xy
913_4_0
11xy
913_5_0
12xy
913_6_0
13xy
913_7_0
14xy
913_8_0
15xy
913_9_0
16xy
913_10_0
17xy
913_11_0
18<CRC>

19<Ende>

+ +
+

Tabelle 28: ID 357_366_14_Modem

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
HT Bus-Telegramme


Message-ID:357_14_0 bis 366_14_0
ByteWerte (Hex)BemerkungBedeutung / ID

9Byte



Modem-CMD:: Betriebsart setzen
08DSource: ModemSource
110TargetTarget
2FFEMS-TypeEMS Marker
30E
EMS Offset
400
EMS Type(H)
565...6EHeizkreis xEMS Type(L)


65=Heizkreis1


66=Heizkreis2


67=Heizkreis3


68=Heizkreis4


69=Heizkreis5


6A=Heizkreis6


6B=Heizkreis7


6C=Heizkreis8


6D=Heizkreis9


6E=Heizkreis10
60...4Heizkreisbetriebsart-Werte357_14_0 bis


- 0=Nicht definiert366_14_0


- 1=Betrieb im Frostschutzmodus


- 2=Betrieb im Sparmodus


- 3=Betrieb im Normalmodus


- 4=Automatikbetrieb
7<CRC>CRC
8<Ende>Ende Marker
+ +
+

Tabelle 29: ID 377_387_4_Modem

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
HT Bus-Telegramme


Message-ID:377_4_0 bis 386_4_0
ByteWerte (Hex)BemerkungBedeutung / ID

9Byte



Modem-CMD: Betriebsart setzen
08DSource: ModemSource
110TargetTarget
2FFEMS-TypeEMS Marker
304
EMS Offset
400
EMS Type(H)
579...82Heizkreis x KennungEMS Type(L)


79=Heizkreis1


7A=Heizkreis2


7B=Heizkreis3


7C=Heizkreis4


7D=Heizkreis5


7E=Heizkreis6


7F=Heizkreis7


80=Heizkreis8


81=Heizkreis9


82=Heizkreis10
60...4Heizkreisbetriebsart-Werte357_4_0 bis


- 0=Nicht definiert366_4_0


- 1=Betrieb im Frostschutzmodus


- 2=Betrieb im Sparmodus


- 3=Betrieb im Normalmodus


- 4=Automatikbetrieb
7<CRC>CRC
8<Ende>Ende Marker
+ +
+

Tabelle 30: ID 357...366_1x_Modem

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
HT Bus-Telegramme




Message-ID:357_1x_0 bis 366_1x_0


ByteWerte (Hex)BemerkungBedeutung / ID


9Byte
Betriebsart



Modem-CMD: Temperatur-Niveau setzen (Betriebsart Normal/Sparen/Frost) NormalSparenFrost
08DSource: ModemSourceSourceSource
110TargetTargetTargetTarget
2FFEMS-TypeEMS MarkerEMS MarkerEMS Marker
311/10/0FEMS-Offset 11 (hex)10 (hex) F (hex)
400
EMS Type(H)EMS Type(H)EMS Type(H)
565...6EHeizkreis xEMS Type(L)EMS Type(L)EMS Type(L)


65=Heizkreis1




66=Heizkreis2




67=Heizkreis3




68=Heizkreis4




69=Heizkreis5




6A=Heizkreis6




6B=Heizkreis7




6C=Heizkreis8




6D=Heizkreis9




6E=Heizkreis10


6
Temperaturniveau für Betriebsart: y im Heizkreis x357_17_0 bis357_16_0 bis357_15_0 bis


- (in 0.5 Grad Schritten)366_17_0366_16_0366_15_0
7<CRC>CRC


8<Ende>Ende Marker


+ +
+

Tabelle 31: ID 377...386_x_Modem

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
HT Bus-Telegramme




Message-ID:377_x_0 bis 386_x_0


ByteWerte (Hex)BemerkungBedeutung / ID


9Byte





Modem-CMD: Temperatur-Niveau setzen (Betriebsart Normal/Sparen/Frost) NormalSparenFrost
08DSource: ModemSourceSourceSource
110TargetTargetTargetTarget
2FFEMS-TypeEMS MarkerEMS MarkerEMS Marker
307/06/05EMS-Offset 7 (hex) 6 (hex) 5 (hex)
400
EMS Type(H)EMS Type(H)EMS Type(H)
579...82Heizkreis x KennungEMS Type(L)EMS Type(L)EMS Type(L)


79=Heizkreis1




7A=Heizkreis2




7B=Heizkreis3




7C=Heizkreis4




7D=Heizkreis5




7E=Heizkreis6




7F=Heizkreis7




80=Heizkreis8




81=Heizkreis9




82=Heizkreis10


60...4Temperaturniveau für Betriebsart: y im Heizkreis x377_7_0 bis377_6_0 bis377_5_0 bis


- (in 0.5 Grad Schritten)386_7_0386_6_0386_5_0
7<CRC>CRC


8<Ende>Ende Marker


+ + + + diff --git a/doc/home_assistant/ha.png b/doc/home_assistant/ha.png index ce0bc1b8a..e941ea86c 100644 Binary files a/doc/home_assistant/ha.png and b/doc/home_assistant/ha.png differ diff --git a/doc/telnet/telnet_menu.jpg b/doc/telnet/telnet_menu.jpg index fd00d890d..45c7bf872 100644 Binary files a/doc/telnet/telnet_menu.jpg and b/doc/telnet/telnet_menu.jpg differ diff --git a/doc/telnet/telnet_stats.PNG b/doc/telnet/telnet_stats.PNG index 54df0a35c..295d7c4de 100644 Binary files a/doc/telnet/telnet_stats.PNG and b/doc/telnet/telnet_stats.PNG differ diff --git a/doc/web/ems_dashboard.PNG b/doc/web/ems_dashboard.PNG new file mode 100644 index 000000000..168c8395c Binary files /dev/null and b/doc/web/ems_dashboard.PNG differ diff --git a/doc/web/system_status.PNG b/doc/web/system_status.PNG new file mode 100644 index 000000000..41ba11a2a Binary files /dev/null and b/doc/web/system_status.PNG differ diff --git a/lib/MyESP/MyESP.cpp b/lib/MyESP/MyESP.cpp deleted file mode 100644 index ebe180f21..000000000 --- a/lib/MyESP/MyESP.cpp +++ /dev/null @@ -1,2173 +0,0 @@ -/* - * MyESP - my ESP helper class to handle WiFi, MQTT and Telnet - * - * Paul Derbyshire - first revision: December 2018 - * - * Ideas borrowed from Espurna https://github.com/xoseperez/espurna - */ - -#include "MyESP.h" - -#ifdef CRASH -EEPROM_Rotate EEPROMr; -#endif - -union system_rtcmem_t { - struct { - uint8_t stability_counter; - uint8_t reset_reason; - uint8_t boot_status; - uint8_t _reserved_; - } parts; - uint32_t value; -}; - -uint8_t RtcmemSize = (sizeof(RtcmemData) / 4u); -auto Rtcmem = reinterpret_cast(RTCMEM_ADDR); - -// constructor -MyESP::MyESP() { - _app_hostname = strdup("MyESP"); - _app_name = strdup("MyESP"); - _app_version = strdup(MYESP_VERSION); - - _boottime = NULL; - _load_average = 100; // calculated load average - - _telnetcommand_callback = NULL; - _telnet_callback = NULL; - - _command[0] = '\0'; - - _fs_callback = NULL; - _fs_settings_callback = NULL; - - _web_callback = NULL; - - _serial = false; - - _heartbeat = false; - _mqtt_host = NULL; - _mqtt_password = NULL; - _mqtt_username = NULL; - _mqtt_retain = false; - _mqtt_keepalive = 300; - _mqtt_will_topic = NULL; - _mqtt_will_online_payload = NULL; - _mqtt_will_offline_payload = NULL; - _mqtt_base = NULL; - _mqtt_topic = NULL; - _mqtt_qos = 0; - _mqtt_reconnect_delay = MQTT_RECONNECT_DELAY_MIN; - _mqtt_last_connection = 0; - _mqtt_connecting = false; - - _firstInstall = false; - - _wifi_password = NULL; - _wifi_ssid = NULL; - _wifi_callback = NULL; - _wifi_connected = false; - - _ota_pre_callback = NULL; - _ota_post_callback = NULL; - _ota_doing_update = false; - - _suspendOutput = false; - - _rtcmem_status = false; - _systemStable = true; -} - -MyESP::~MyESP() { - end(); -} - -// end -void MyESP::end() { - SerialAndTelnet.end(); - jw.disconnect(); -} - -// general debug to the telnet or serial channels -void MyESP::myDebug(const char * format, ...) { - if (_suspendOutput) - return; - - va_list args; - va_start(args, format); - char test[1]; - - int len = ets_vsnprintf(test, 1, format, args) + 1; - - char * buffer = new char[len]; - ets_vsnprintf(buffer, len, format, args); - va_end(args); - - SerialAndTelnet.println(buffer); - - delete[] buffer; -} - -// for flashmemory. Must use PSTR() -void MyESP::myDebug_P(PGM_P format_P, ...) { - if (_suspendOutput) - return; - - char format[strlen_P(format_P) + 1]; - memcpy_P(format, format_P, sizeof(format)); - - va_list args; - va_start(args, format_P); - char test[1]; - int len = ets_vsnprintf(test, 1, format, args) + 1; - - char * buffer = new char[len]; - ets_vsnprintf(buffer, len, format, args); - - va_end(args); - -#ifdef MYESP_TIMESTAMP - // capture & print timestamp - char timestamp[10] = {0}; - snprintf_P(timestamp, sizeof(timestamp), PSTR("[%06lu] "), millis() % 1000000); - SerialAndTelnet.print(timestamp); -#endif - - SerialAndTelnet.println(buffer); - - delete[] buffer; -} - -// use Serial? -bool MyESP::getUseSerial() { - return (_serial); -} - -// heartbeat -bool MyESP::getHeartbeat() { - return (_heartbeat); -} - -// init heap ram -uint32_t MyESP::_getInitialFreeHeap() { - static uint32_t _heap = 0; - - if (0 == _heap) { - _heap = ESP.getFreeHeap(); - } - - return _heap; -} - -// used heap mem -// note calls to getFreeHeap sometimes causes some ESPs to crash -uint32_t MyESP::_getUsedHeap() { - return _getInitialFreeHeap() - ESP.getFreeHeap(); -} - -// 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(ARDUINO_ARCH_ESP32) - String hostname = String(WiFi.getHostname()); -#else - String hostname = WiFi.hostname(); -#endif - - myDebug_P(PSTR("[WIFI] SSID %s"), WiFi.SSID().c_str()); - myDebug_P(PSTR("[WIFI] CH %d"), WiFi.channel()); - myDebug_P(PSTR("[WIFI] RSSI %d"), WiFi.RSSI()); - myDebug_P(PSTR("[WIFI] IP %s"), WiFi.localIP().toString().c_str()); - myDebug_P(PSTR("[WIFI] MAC %s"), WiFi.macAddress().c_str()); - myDebug_P(PSTR("[WIFI] GW %s"), WiFi.gatewayIP().toString().c_str()); - myDebug_P(PSTR("[WIFI] MASK %s"), WiFi.subnetMask().toString().c_str()); - myDebug_P(PSTR("[WIFI] DNS %s"), WiFi.dnsIP().toString().c_str()); - myDebug_P(PSTR("[WIFI] HOST %s"), hostname.c_str()); - - // start OTA - ArduinoOTA.begin(); // moved to support esp32 - myDebug_P(PSTR("[OTA] listening to %s.local:%u"), ArduinoOTA.getHostname().c_str(), OTA_PORT); - - // MQTT Setup - _mqtt_setup(); - - _wifi_connected = true; - - // finally if we don't want Serial anymore, turn it off - if (!_serial) { - myDebug_P(PSTR("[SYSTEM] Disabling serial port communication.")); - SerialAndTelnet.flush(); // flush so all buffer is printed to serial - SerialAndTelnet.setSerial(NULL); - } - - // call any final custom settings - if (_wifi_callback) { - _wifi_callback(); - } - - jw.enableAPFallback(false); // Disable AP mode after initial connect was successful - } - - if (code == MESSAGE_ACCESSPOINT_CREATED) { - _wifi_connected = true; - - myDebug_P(PSTR("[WIFI] MODE AP")); - myDebug_P(PSTR("[WIFI] SSID %s"), jw.getAPSSID().c_str()); - myDebug_P(PSTR("[WIFI] IP %s"), WiFi.softAPIP().toString().c_str()); - myDebug_P(PSTR("[WIFI] MAC %s"), WiFi.softAPmacAddress().c_str()); - - // finally if we don't want Serial anymore, turn it off - if (!_serial) { - myDebug_P(PSTR("[SYSTEM] Disabling serial port communication.")); - SerialAndTelnet.flush(); // flush so all buffer is printed to serial - SerialAndTelnet.setSerial(NULL); - } - - // call any final custom settings - if (_wifi_callback) { - _wifi_callback(); - } - } - - if (code == MESSAGE_CONNECTING) { - myDebug_P(PSTR("[WIFI] Connecting to %s"), parameter); - _wifi_connected = false; - } - - if (code == MESSAGE_CONNECT_FAILED) { - myDebug_P(PSTR("[WIFI] Could not connect to %s"), parameter); - _wifi_connected = false; - } - - if (code == MESSAGE_DISCONNECTED) { - myDebug_P(PSTR("[WIFI] Disconnected")); - _wifi_connected = false; - } - - if (code == MESSAGE_SCANNING) { - myDebug_P(PSTR("[WIFI] Scanning")); - } - - if (code == MESSAGE_SCAN_FAILED) { - myDebug_P(PSTR("[WIFI] Scan failed")); - } - - if (code == MESSAGE_NO_NETWORKS) { - myDebug_P(PSTR("[WIFI] No networks found")); - } - - if (code == MESSAGE_NO_KNOWN_NETWORKS) { - myDebug_P(PSTR("[WIFI] No known networks found")); - } - - if (code == MESSAGE_FOUND_NETWORK) { - myDebug_P(PSTR("[WIFI] %s"), parameter); - } - - if (code == MESSAGE_CONNECT_WAITING) { - // too much noise - } - - if (code == MESSAGE_ACCESSPOINT_CREATING) { - myDebug_P(PSTR("[WIFI] Creating access point")); - } - - if (code == MESSAGE_ACCESSPOINT_FAILED) { - myDebug_P(PSTR("[WIFI] Could not create access point")); - } -} - -// return true if in WiFi AP mode -// does not work after wifi reset on ESP32 yet. See https://github.com/espressif/arduino-esp32/issues/1306 -bool MyESP::isAPmode() { - return (WiFi.getMode() & WIFI_AP); -} - -// received MQTT message -// we send this to the call back function. Important to parse are the event strings such as MQTT_MESSAGE_EVENT and MQTT_CONNECT_EVENT -void MyESP::_mqttOnMessage(char * topic, char * payload, size_t len) { - if (len == 0) - return; - - char message[len + 1]; - strlcpy(message, (char *)payload, len + 1); - - // myDebug_P(PSTR("[MQTT] Received %s => %s"), topic, message); // enable for debugging - - // topics are in format MQTT_BASE/HOSTNAME/TOPIC - char * topic_magnitude = strrchr(topic, '/'); // strip out everything until last / - if (topic_magnitude != nullptr) { - topic = topic_magnitude + 1; - } - - // check for standard messages - // Restart the device - if (strcmp(topic, MQTT_TOPIC_RESTART) == 0) { - myDebug_P(PSTR("[MQTT] Received restart command"), message); - resetESP(); - return; - } - - // handle response from a start message - // for example with HA it sends the system time from the server - if (strcmp(topic, MQTT_TOPIC_START) == 0) { - myDebug_P(PSTR("[MQTT] Received boottime: %s"), message); - setBoottime(message); - return; - } - - // Send message event to custom service - (_mqtt_callback)(MQTT_MESSAGE_EVENT, topic, message); -} - -// MQTT subscribe -// to MQTT_BASE/app_hostname/topic -void MyESP::mqttSubscribe(const char * topic) { - if (mqttClient.connected() && (strlen(topic) > 0)) { - unsigned int packetId = mqttClient.subscribe(_mqttTopic(topic), _mqtt_qos); - myDebug_P(PSTR("[MQTT] Subscribing to %s (PID %d)"), _mqttTopic(topic), packetId); - } -} - -// MQTT unsubscribe -// to MQTT_BASE/app_hostname/topic -void MyESP::mqttUnsubscribe(const char * topic) { - if (mqttClient.connected() && (strlen(topic) > 0)) { - unsigned int packetId = mqttClient.unsubscribe(_mqttTopic(topic)); - myDebug_P(PSTR("[MQTT] Unsubscribing to %s (PID %d)"), _mqttTopic(topic), packetId); - } -} - -// MQTT Publish -void MyESP::mqttPublish(const char * topic, const char * payload) { - // myDebug_P(PSTR("[MQTT] Sending pubish to %s with payload %s"), _mqttTopic(topic), payload); - mqttClient.publish(_mqttTopic(topic), _mqtt_qos, _mqtt_retain, payload); -} - -// MQTT onConnect - when a connect is established -void MyESP::_mqttOnConnect() { - myDebug_P(PSTR("[MQTT] Connected")); - _mqtt_reconnect_delay = MQTT_RECONNECT_DELAY_MIN; - - _mqtt_last_connection = millis(); - - // say we're alive to the Last Will topic - mqttClient.publish(_mqttTopic(_mqtt_will_topic), 1, true, _mqtt_will_online_payload); - - // subscribe to general subs - mqttSubscribe(MQTT_TOPIC_RESTART); - - // subscribe to a start message and send the first publish - myESP.mqttSubscribe(MQTT_TOPIC_START); - myESP.mqttPublish(MQTT_TOPIC_START, MQTT_TOPIC_START_PAYLOAD); - - // call custom function to handle mqtt receives - (_mqtt_callback)(MQTT_CONNECT_EVENT, NULL, NULL); -} - -// MQTT setup -void MyESP::_mqtt_setup() { - if (!_mqtt_host) { - 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")); - (_mqtt_callback)(MQTT_DISCONNECT_EVENT, NULL, NULL); // call callback with disconnect - } - if (reason == AsyncMqttClientDisconnectReason::MQTT_IDENTIFIER_REJECTED) { - myDebug_P(PSTR("[MQTT] Identifier Rejected")); - } - if (reason == AsyncMqttClientDisconnectReason::MQTT_SERVER_UNAVAILABLE) { - myDebug_P(PSTR("[MQTT] Server unavailable")); - } - if (reason == AsyncMqttClientDisconnectReason::MQTT_MALFORMED_CREDENTIALS) { - myDebug_P(PSTR("[MQTT] Malformed credentials")); - } - if (reason == AsyncMqttClientDisconnectReason::MQTT_NOT_AUTHORIZED) { - myDebug_P(PSTR("[MQTT] Not authorized")); - } - - // Reset reconnection delay - _mqtt_last_connection = millis(); - _mqtt_connecting = false; - }); - - //mqttClient.onSubscribe([this](uint16_t packetId, uint8_t qos) { myDebug_P(PSTR("[MQTT] Subscribe ACK for PID %d"), packetId); }); - - //mqttClient.onPublish([this](uint16_t packetId) { myDebug_P(PSTR("[MQTT] Publish ACK for PID %d"), packetId); }); - - mqttClient.onMessage([this](char * topic, char * payload, AsyncMqttClientMessageProperties properties, size_t len, size_t index, size_t total) { - _mqttOnMessage(topic, payload, len); - }); -} - -// WiFI setup -void MyESP::_wifi_setup() { - jw.setHostname(_app_hostname); // Set WIFI hostname - jw.subscribe([this](justwifi_messages_t code, char * parameter) { _wifiCallback(code, parameter); }); - jw.enableAP(false); - jw.setConnectTimeout(WIFI_CONNECT_TIMEOUT); - jw.setReconnectTimeout(WIFI_RECONNECT_INTERVAL); - jw.enableAPFallback(true); // AP mode only as fallback - jw.enableSTA(true); // Enable STA mode (connecting to a router) - jw.enableScan(false); // Configure it to scan available networks and connect in order of dBm - jw.cleanNetworks(); // Clean existing network configuration - jw.addNetwork(_wifi_ssid, _wifi_password); // Add a network - -#if defined(ESP8266) - WiFi.setSleepMode(WIFI_NONE_SLEEP); // added to possibly fix wifi dropouts in arduino core 2.5.0 -#endif -} - -// set the callback function for the OTA onstart -void MyESP::setOTA(ota_callback_f OTACallback_pre, ota_callback_f OTACallback_post) { - _ota_pre_callback = OTACallback_pre; - _ota_post_callback = OTACallback_post; -} - -// OTA callback when the upload process starts -void MyESP::_OTACallback() { - myDebug_P(PSTR("[OTA] Start")); - -#ifdef CRASH - // If we are not specifically reserving the sectors we are using as - // EEPROM in the memory layout then any OTA upgrade will overwrite - // all but the last one. - // Calling rotate(false) disables rotation so all writes will be done - // to the last sector. It also sets the dirty flag to true so the next commit() - // will actually persist current configuration to that last sector. - // Calling rotate(false) will also prevent any other EEPROM write - // to overwrite the OTA image. - // In case the OTA process fails, reenable rotation. - // See onError callback below. - EEPROMr.rotate(false); - EEPROMr.commit(); -#endif - - // stop the web server - webServer.close(); - - _ota_doing_update = true; - - if (_ota_pre_callback) { - (_ota_pre_callback)(); // call custom function - } -} - -// OTA Setup -void MyESP::_ota_setup() { - if (!_wifi_ssid) { - return; - } - - ArduinoOTA.setPort(OTA_PORT); - ArduinoOTA.setHostname(_app_hostname); - - ArduinoOTA.onStart([this]() { _OTACallback(); }); - ArduinoOTA.onEnd([this]() { - myDebug_P(PSTR("[OTA] Done, restarting...")); - _ota_doing_update = false; - _deferredReset(500, CUSTOM_RESET_OTA); - }); - - ArduinoOTA.onProgress([this](unsigned int progress, unsigned int total) { - static unsigned int _progOld; - unsigned int _prog = (progress / (total / 100)); - if (_prog != _progOld) { - myDebug_P(PSTR("[OTA] Progress: %u%%\r"), _prog); - _progOld = _prog; - } - }); - - ArduinoOTA.onError([this](ota_error_t error) { - if (error == OTA_AUTH_ERROR) - myDebug_P(PSTR("[OTA] Auth Failed")); - else if (error == OTA_BEGIN_ERROR) - myDebug_P(PSTR("[OTA] Begin Failed")); - else if (error == OTA_CONNECT_ERROR) - myDebug_P(PSTR("[OTA] Connect Failed")); - else if (error == OTA_RECEIVE_ERROR) - myDebug_P(PSTR("[OTA] Receive Failed")); - else if (error == OTA_END_ERROR) - myDebug_P(PSTR("[OTA] End Failed")); - -#ifdef CRASH - // There's been an error, reenable eeprom rotation - EEPROMr.rotate(true); -#endif - }); -} - -// sets boottime -void MyESP::setBoottime(const char * boottime) { - if (_boottime) { - free(_boottime); - } - _boottime = strdup(boottime); -} - -// eeprom -void MyESP::_eeprom_setup() { -#ifdef CRASH - EEPROMr.size(4); - EEPROMr.begin(SPI_FLASH_SEC_SIZE); -#endif -} - -// Set callback of sketch function to process project messages -void MyESP::setTelnet(telnetcommand_callback_f callback_cmd, telnet_callback_f callback) { - _telnetcommand_callback = callback_cmd; // external function to handle commands - _telnet_callback = callback; -} - -void MyESP::_telnetConnected() { - myDebug_P(PSTR("[TELNET] Telnet connection established")); - _consoleShowHelp(); // Show the initial message - -#ifdef CRASH - // show crash dump if just restarted after a fatal crash - uint32_t crash_time; - EEPROMr.get(SAVE_CRASH_EEPROM_OFFSET + SAVE_CRASH_CRASH_TIME, crash_time); - if ((crash_time != 0) && (crash_time != 0xFFFFFFFF)) { - myDebug_P(PSTR("[SYSTEM] There is stack data available from the last system crash. Use 'crash dump' to view and 'crash clear' to reset")); - } -#endif - - // call callback - if (_telnet_callback) { - (_telnet_callback)(TELNET_EVENT_CONNECT); - } -} - -void MyESP::_telnetDisconnected() { - myDebug_P(PSTR("[TELNET] Telnet connection closed")); - if (_telnet_callback) { - (_telnet_callback)(TELNET_EVENT_DISCONNECT); // call callback - } -} - -// Initialize the telnet server -void MyESP::_telnet_setup() { - SerialAndTelnet.setWelcomeMsg(""); - SerialAndTelnet.setCallbackOnConnect([this]() { _telnetConnected(); }); - SerialAndTelnet.setCallbackOnDisconnect([this]() { _telnetDisconnected(); }); - SerialAndTelnet.setDebugOutput(false); - SerialAndTelnet.begin(TELNET_SERIAL_BAUD); // default baud is 115200 - - // init command buffer for console commands - memset(_command, 0, TELNET_MAX_COMMAND_LENGTH); -} - -// Show help of commands -void MyESP::_consoleShowHelp() { - myDebug_P(PSTR("")); - myDebug_P(PSTR("* Connected to: %s version %s"), _app_name, _app_version); - - if (isAPmode()) { - myDebug_P(PSTR("* Device is in AP mode with SSID %s"), jw.getAPSSID().c_str()); - } else { - myDebug_P(PSTR("* Hostname: %s (%s)"), _getESPhostname().c_str(), WiFi.localIP().toString().c_str()); - myDebug_P(PSTR("* WiFi SSID: %s (signal %d%%)"), WiFi.SSID().c_str(), getWifiQuality()); - if (isMQTTConnected()) { - myDebug_P(PSTR("* MQTT connected (heartbeat %s)"), getHeartbeat() ? "enabled" : "disabled"); - } else { - myDebug_P(PSTR("* MQTT disconnected")); - } - } - - myDebug_P(PSTR("*")); - myDebug_P(PSTR("* Commands:")); - myDebug_P(PSTR("* ?=help, CTRL-D/quit=exit telnet session")); - myDebug_P(PSTR("* set, system, reboot")); -#ifdef CRASH - myDebug_P(PSTR("* crash ")); -#endif - - // call callback function - if (_telnet_callback) { - (_telnet_callback)(TELNET_EVENT_SHOWCMD); - } - - myDebug_P(PSTR("")); // newline -} - -// print all set commands and current values -void MyESP::_printSetCommands() { - myDebug_P(PSTR("")); // newline - myDebug_P(PSTR("The following set commands are available:")); - myDebug_P(PSTR("")); // newline - myDebug_P(PSTR(" set erase")); - myDebug_P(PSTR(" set [value]")); - myDebug_P(PSTR(" set [value]")); - myDebug_P(PSTR(" set serial ")); - - // call callback function - if (_telnet_callback) { - (_telnet_callback)(TELNET_EVENT_SHOWSET); - } - - myDebug_P(PSTR("")); // newline - myDebug_P(PSTR("Stored settings:")); - myDebug_P(PSTR("")); // newline - myDebug_P(PSTR(" wifi_ssid=%s "), (!_wifi_ssid) ? "" : _wifi_ssid); - SerialAndTelnet.print(FPSTR(" wifi_password=")); - if (!_wifi_password) { - SerialAndTelnet.print(FPSTR("")); - } else { - for (uint8_t i = 0; i < strlen(_wifi_password); i++) { - SerialAndTelnet.print(FPSTR("*")); - } - } - myDebug_P(PSTR("")); // newline - myDebug_P(PSTR(" mqtt_host=%s"), (!_mqtt_host) ? "" : _mqtt_host); - myDebug_P(PSTR(" mqtt_username=%s"), (!_mqtt_username) ? "" : _mqtt_username); - SerialAndTelnet.print(FPSTR(" mqtt_password=")); - if (!_mqtt_password) { - SerialAndTelnet.print(FPSTR("")); - } else { - for (uint8_t i = 0; i < strlen(_mqtt_password); i++) { - SerialAndTelnet.print(FPSTR("*")); - } - } - - myDebug_P(PSTR("")); // newline - myDebug_P(PSTR(" serial=%s"), (_serial) ? "on" : "off"); - myDebug_P(PSTR(" heartbeat=%s"), (_heartbeat) ? "on" : "off"); - - // print any custom settings - (_fs_settings_callback)(MYESP_FSACTION_LIST, 0, NULL, NULL); - - myDebug_P(PSTR("")); // newline -} - -// reset / restart -void MyESP::resetESP() { - myDebug_P(PSTR("* Reboot ESP...")); - _deferredReset(500, CUSTOM_RESET_TERMINAL); - end(); -#if defined(ARDUINO_ARCH_ESP32) - ESP.restart(); -#else - ESP.restart(); -#endif -} - -// read next word from string buffer -// if parameter true then a word is only terminated by a newline -char * MyESP::_telnet_readWord(bool allow_all_chars) { - if (allow_all_chars) { - return (strtok(NULL, "\n")); // allow only newline - } else { - return (strtok(NULL, ", \n")); // allow space and comma - } -} - -// change settings - always as strings -// messy code but effective since we don't have too many settings -// wc is word count, number of parameters after the 'set' command -bool MyESP::_changeSetting(uint8_t wc, const char * setting, const char * value) { - bool ok = false; - - // check for our internal commands first - if (strcmp(setting, "erase") == 0) { - _fs_eraseConfig(); - return true; - - } else if (strcmp(setting, "wifi_ssid") == 0) { - if (_wifi_ssid) - free(_wifi_ssid); - _wifi_ssid = NULL; // just to be sure - if (value) { - _wifi_ssid = strdup(value); - } - ok = true; - jw.enableSTA(false); - myDebug_P(PSTR("Note: please 'reboot' ESP to apply new WiFi settings")); - } else if (strcmp(setting, "wifi_password") == 0) { - if (_wifi_password) - free(_wifi_password); - _wifi_password = NULL; // just to be sure - if (value) { - _wifi_password = strdup(value); - } - ok = true; - jw.enableSTA(false); - myDebug_P(PSTR("Note: please 'reboot' ESP to apply new WiFi settings")); - - } else if (strcmp(setting, "mqtt_host") == 0) { - if (_mqtt_host) - free(_mqtt_host); - _mqtt_host = NULL; // just to be sure - if (value) { - _mqtt_host = strdup(value); - } - ok = true; - } else if (strcmp(setting, "mqtt_username") == 0) { - if (_mqtt_username) - free(_mqtt_username); - _mqtt_username = NULL; // just to be sure - if (value) { - _mqtt_username = strdup(value); - } - ok = true; - } else if (strcmp(setting, "mqtt_password") == 0) { - if (_mqtt_password) - free(_mqtt_password); - _mqtt_password = NULL; // just to be sure - if (value) { - _mqtt_password = strdup(value); - } - ok = true; - - } else if (strcmp(setting, "serial") == 0) { - ok = true; - _serial = false; - if (value) { - if (strcmp(value, "on") == 0) { - _serial = true; - ok = true; - myDebug_P(PSTR("Reboot ESP to activate Serial mode.")); - } else if (strcmp(value, "off") == 0) { - _serial = false; - ok = true; - myDebug_P(PSTR("Reboot ESP to deactivate Serial mode.")); - } else { - ok = false; - } - } - - } else if (strcmp(setting, "heartbeat") == 0) { - ok = true; - _heartbeat = false; - if (value) { - if (strcmp(value, "on") == 0) { - _heartbeat = true; - ok = true; - myDebug_P(PSTR("Heartbeat on")); - } else if (strcmp(value, "off") == 0) { - _heartbeat = false; - ok = true; - myDebug_P(PSTR("Heartbeat off")); - } else { - ok = false; - } - } - } else { - // finally check for any custom commands - ok = (_fs_settings_callback)(MYESP_FSACTION_SET, wc, setting, value); - } - - // if we were able to recognize the set command, continue - if (ok) { - // check for 2 params - if (value == nullptr) { - myDebug_P(PSTR("%s setting reset to its default value."), setting); - } else { - // must be 3 params - myDebug_P(PSTR("%s changed."), setting); - } - - myDebug_P(PSTR("")); // newline - - (void)fs_saveConfig(); // always save the values - } - - return ok; -} - -// force the serial on/off -void MyESP::setUseSerial(bool b) { - _serial = b; - SerialAndTelnet.setSerial(b ? &Serial : NULL); -} - -void MyESP::_telnetCommand(char * commandLine) { - char * str = commandLine; - bool state = false; - - if (strlen(commandLine) == 0) - return; - - // count the number of arguments - unsigned wc = 0; - while (*str) { - if (*str == ' ' || *str == '\n' || *str == '\t') { - state = false; - } else if (state == false) { - state = true; - ++wc; - } - ++str; - } - - // check first for reserved commands - char * temp = strdup(commandLine); // because strotok kills original string buffer - char * ptrToCommandName = strtok((char *)temp, " \n"); // space and newline - - // set command - if (strcmp(ptrToCommandName, "set") == 0) { - bool ok = false; - if (wc == 1) { - _printSetCommands(); - ok = true; - } else if (wc == 2) { // set - char * setting = _telnet_readWord(false); - ok = _changeSetting(wc - 1, setting, NULL); - } else { // set - char * setting = _telnet_readWord(false); - char * value = _telnet_readWord(true); // allow strange characters - ok = _changeSetting(wc - 1, setting, value); - } - - if (!ok) { - myDebug_P(PSTR("\nInvalid parameter for set command.")); - } - - return; - } - - // reboot command - if ((strcmp(ptrToCommandName, "reboot") == 0) && (wc == 1)) { - resetESP(); - } - - // show system stats - if ((strcmp(ptrToCommandName, "system") == 0) && (wc == 1)) { - showSystemStats(); - return; - } - - // show system stats - if ((strcmp(ptrToCommandName, "quit") == 0) && (wc == 1)) { - myDebug_P(PSTR("[TELNET] exiting telnet session")); - SerialAndTelnet.disconnectClient(); - return; - } - -#ifdef CRASH - // crash command - if ((strcmp(ptrToCommandName, "crash") == 0) && (wc >= 2)) { - char * cmd = _telnet_readWord(false); - if (strcmp(cmd, "dump") == 0) { - crashDump(); - } else if (strcmp(cmd, "clear") == 0) { - crashClear(); - } else if ((strcmp(cmd, "test") == 0) && (wc == 3)) { - char * value = _telnet_readWord(false); - crashTest(atoi(value)); - } else { - myDebug_P(PSTR("Error. Usage: crash ")); - } - return; // don't call custom command line callback - } -#endif - - // call callback function - if (_telnetcommand_callback) { - (_telnetcommand_callback)(wc, commandLine); - } -} - -// returns WiFi hostname as a String object -String MyESP::_getESPhostname() { - String hostname; - -#if defined(ARDUINO_ARCH_ESP32) - hostname = String(WiFi.getHostname()); -#else - hostname = WiFi.hostname(); -#endif - - return (hostname); -} - -// returns build time as a String - copied for espurna. see (c) -// takes the time from the gcc during compilation -String MyESP::_buildTime() { - const char time_now[] = __TIME__; // hh:mm:ss - unsigned int hour = atoi(&time_now[0]); - unsigned int minute = atoi(&time_now[3]); - unsigned int second = atoi(&time_now[6]); - - const char date_now[] = __DATE__; // Mmm dd yyyy - const char * months[] = {"Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"}; - unsigned int month = 0; - for (int i = 0; i < 12; i++) { - if (strncmp(date_now, months[i], 3) == 0) { - month = i + 1; - break; - } - } - unsigned int day = atoi(&date_now[3]); - unsigned int year = atoi(&date_now[7]); - - char buffer[20]; - snprintf_P(buffer, sizeof(buffer), PSTR("%04d-%02d-%02d %02d:%02d:%02d"), year, month, day, hour, minute, second); - - return String(buffer); -} - -// returns system uptime in seconds - copied for espurna. see (c) -unsigned long MyESP::_getUptime() { - static uint32_t last_uptime = 0; - static uint8_t uptime_overflows = 0; - - if (millis() < last_uptime) { - ++uptime_overflows; - } - last_uptime = millis(); - uint32_t uptime_seconds = uptime_overflows * (UPTIME_OVERFLOW / 1000) + (last_uptime / 1000); - - return uptime_seconds; -} - -// init RTC mem -void MyESP::_rtcmemInit() { - memset((uint32_t *)RTCMEM_ADDR, 0, sizeof(uint32_t) * RTCMEM_BLOCKS); - Rtcmem->magic = RTCMEM_MAGIC; -} - -uint8_t MyESP::getSystemBootStatus() { - system_rtcmem_t data; - data.value = Rtcmem->sys; - return data.parts.boot_status; -} - -void MyESP::_setSystemBootStatus(uint8_t status) { - system_rtcmem_t data; - 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() { - system_rtcmem_t data; - data.value = Rtcmem->sys; - return data.parts.stability_counter; -} - -void MyESP::_setSystemStabilityCounter(uint8_t counter) { - system_rtcmem_t data; - data.value = Rtcmem->sys; - data.parts.stability_counter = counter; - Rtcmem->sys = data.value; -} - -uint8_t MyESP::_getSystemResetReason() { - system_rtcmem_t data; - data.value = Rtcmem->sys; - return data.parts.reset_reason; -} - -void MyESP::_setSystemResetReason(uint8_t reason) { - system_rtcmem_t data; - data.value = Rtcmem->sys; - data.parts.reset_reason = reason; - Rtcmem->sys = data.value; -} - -// system_get_rst_info() result is cached by the Core init for internal use -uint32_t MyESP::getSystemResetReason() { - return resetInfo.reason; -} - -void MyESP::_rtcmemSetup() { - _rtcmem_status = _rtcmemStatus(); - if (!_rtcmem_status) { - _rtcmemInit(); - } -} - -void MyESP::_setCustomResetReason(uint8_t reason) { - _setSystemResetReason(reason); -} - -// returns false if not set and needs to be intialized, causing all rtcmem data to be wiped -bool MyESP::_rtcmemStatus() { - bool readable; - - uint32_t reason = getSystemResetReason(); - - // the last reset could have been caused by manually pressing the reset button - // so before wiping, capture the boot sequence - if (reason == REASON_EXT_SYS_RST) { // external system reset - if (getSystemBootStatus() == MYESP_BOOTSTATUS_BOOTING) { - _setSystemBootStatus(MYESP_BOOTSTATUS_RESETNEEDED); - } else { - _setSystemBootStatus(MYESP_BOOTSTATUS_POWERON); - } - } - - switch (reason) { - //case REASON_EXT_SYS_RST: // external system reset - case REASON_WDT_RST: // hardware watch dog reset - case REASON_DEFAULT_RST: // normal startup by power on - readable = false; - break; - default: - readable = true; - } - - readable = readable and (RTCMEM_MAGIC == Rtcmem->magic); - - return readable; -} - -bool MyESP::_getRtcmemStatus() { - return _rtcmem_status; -} - -uint8_t MyESP::_getCustomResetReason() { - static uint8_t status = 255; - if (status == 255) { - if (_rtcmemStatus()) - status = _getSystemResetReason(); - if (status > 0) - _setCustomResetReason(0); - if (status > CUSTOM_RESET_MAX) - status = 0; - } - return status; -} - -void MyESP::_deferredReset(unsigned long delaytime, uint8_t reason) { - _setSystemBootStatus(MYESP_BOOTSTATUS_POWERON); - _setCustomResetReason(reason); - delay(delaytime); -} - -// Call this method on boot with stable=true to reset the crash counter -// Each call increments the counter -// If the counter reaches SYSTEM_CHECK_MAX then the system is flagged as unstable -void MyESP::_setSystemCheck(bool stable) { - uint8_t value = 0; - - if (stable) { - value = 0; // system is ok - } else { - if (!_getRtcmemStatus()) { - _setSystemStabilityCounter(1); - return; - } - - value = _getSystemStabilityCounter(); - - if (++value > SYSTEM_CHECK_MAX) { - _systemStable = false; - value = 0; // system is unstable - myDebug_P(PSTR("[SYSTEM] Warning, system UNSTABLE.")); - - /* - // enable Serial again - if (!_serial) { - SerialAndTelnet.setSerial(&Serial); - _serial = true; - } - */ - } - } - - _setSystemStabilityCounter(value); -} - -// return if system is stable (false=bad) -bool MyESP::_getSystemCheck() { - return _systemStable; -} - -// periodically check if system is stable -void MyESP::_systemCheckLoop() { - static bool checked = false; - if (!checked && (millis() > SYSTEM_CHECK_TIME)) { - _setSystemCheck(true); // Flag system as stable - checked = true; - } -} - - -// print out ESP system stats -// for battery power is ESP.getVcc() -void MyESP::showSystemStats() { -#if defined(ESP8266) - myDebug_P(PSTR("%sESP8266 System stats:%s"), COLOR_BOLD_ON, COLOR_BOLD_OFF); -#else - myDebug_P(PSTR("ESP32 System stats:")); -#endif - myDebug_P(PSTR("")); - - myDebug_P(PSTR(" [APP] %s version: %s"), _app_name, _app_version); - myDebug_P(PSTR(" [APP] MyESP version: %s"), MYESP_VERSION); - myDebug_P(PSTR(" [APP] Build timestamp: %s"), _buildTime().c_str()); - if (_boottime != NULL) { - myDebug_P(PSTR(" [APP] Boot time: %s"), _boottime); - } - - // uptime - uint32_t t = _getUptime(); // seconds - - uint32_t d = t / 86400L; - uint32_t h = ((t % 86400L) / 3600L) % 60; - uint32_t rem = t % 3600L; - uint8_t m = rem / 60; - uint8_t s = rem % 60; - myDebug_P(PSTR(" [APP] Uptime: %d days %d hours %d minutes %d seconds"), d, h, m, s); - - myDebug_P(PSTR(" [APP] System Load: %d%%"), getSystemLoadAverage()); - - if (!_getSystemCheck()) { - myDebug_P(PSTR(" [SYSTEM] Device is in SAFE MODE")); - } - - if (isAPmode()) { - myDebug_P(PSTR(" [WIFI] Device is in AP mode with SSID %s"), jw.getAPSSID().c_str()); - } else { - myDebug_P(PSTR(" [WIFI] WiFi Hostname: %s"), _getESPhostname().c_str()); - myDebug_P(PSTR(" [WIFI] WiFi IP: %s"), WiFi.localIP().toString().c_str()); - myDebug_P(PSTR(" [WIFI] WiFi signal strength: %d%%"), getWifiQuality()); - } - - myDebug_P(PSTR(" [WIFI] WiFi MAC: %s"), WiFi.macAddress().c_str()); - - if (isMQTTConnected()) { - myDebug_P(PSTR(" [MQTT] is connected (with heartbeat %s)"), getHeartbeat() ? "enabled" : "disabled"); - } else { - myDebug_P(PSTR(" [MQTT] is disconnected")); - } - -#ifdef CRASH - char output_str[80] = {0}; - char buffer[16] = {0}; - myDebug_P(PSTR(" [EEPROM] EEPROM size: %u"), EEPROMr.reserved() * SPI_FLASH_SEC_SIZE); - strlcpy(output_str, " [EEPROM] EEPROM Sector pool size is ", sizeof(output_str)); - strlcat(output_str, itoa(EEPROMr.size(), buffer, 10), sizeof(output_str)); - strlcat(output_str, ", and in use are: ", sizeof(output_str)); - for (uint32_t i = 0; i < EEPROMr.size(); i++) { - strlcat(output_str, itoa(EEPROMr.base() - i, buffer, 10), sizeof(output_str)); - strlcat(output_str, " ", sizeof(output_str)); - } - myDebug(output_str); -#endif - -#ifdef ARDUINO_BOARD - myDebug_P(PSTR(" [SYSTEM] Board: %s"), ARDUINO_BOARD); -#endif - - myDebug_P(PSTR(" [SYSTEM] CPU frequency: %u MHz"), ESP.getCpuFreqMHz()); - myDebug_P(PSTR(" [SYSTEM] SDK version: %s"), ESP.getSdkVersion()); - -#if defined(ESP8266) - myDebug_P(PSTR(" [SYSTEM] CPU chip ID: 0x%06X"), ESP.getChipId()); - myDebug_P(PSTR(" [SYSTEM] Core version: %s"), ESP.getCoreVersion().c_str()); - myDebug_P(PSTR(" [SYSTEM] Boot version: %d"), ESP.getBootVersion()); - myDebug_P(PSTR(" [SYSTEM] Boot mode: %d"), ESP.getBootMode()); - unsigned char reason = _getCustomResetReason(); - if (reason > 0) { - char buffer[32]; - strcpy_P(buffer, custom_reset_string[reason - 1]); - myDebug_P(PSTR(" [SYSTEM] Last reset reason: %s"), buffer); - } else { - myDebug_P(PSTR(" [SYSTEM] Last reset reason: %s"), (char *)ESP.getResetReason().c_str()); - myDebug_P(PSTR(" [SYSTEM] Last reset info: %s"), (char *)ESP.getResetInfo().c_str()); - } - myDebug_P(PSTR(" [SYSTEM] Restart count: %d"), _getSystemStabilityCounter()); - - myDebug_P(PSTR(" [SYSTEM] rtcmem status: blocks:%u addr:0x%p"), RtcmemSize, Rtcmem); - for (uint8_t block = 0; block < RtcmemSize; ++block) { - myDebug_P(PSTR(" [SYSTEM] rtcmem %02u: %u"), block, reinterpret_cast(RTCMEM_ADDR)[block]); - } -#endif - - FlashMode_t mode = ESP.getFlashChipMode(); -#if defined(ESP8266) - myDebug_P(PSTR(" [FLASH] Flash chip ID: 0x%06X"), ESP.getFlashChipId()); -#endif - myDebug_P(PSTR(" [FLASH] Flash speed: %u Hz"), ESP.getFlashChipSpeed()); - myDebug_P(PSTR(" [FLASH] Flash mode: %s"), mode == FM_QIO ? "QIO" : mode == FM_QOUT ? "QOUT" : mode == FM_DIO ? "DIO" : mode == FM_DOUT ? "DOUT" : "UNKNOWN"); -#if defined(ESP8266) - myDebug_P(PSTR(" [FLASH] Flash size (CHIP): %d"), ESP.getFlashChipRealSize()); -#endif - myDebug_P(PSTR(" [FLASH] Flash size (SDK): %d"), ESP.getFlashChipSize()); - myDebug_P(PSTR(" [FLASH] Flash Reserved: %d"), 1 * SPI_FLASH_SEC_SIZE); - 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); - - myDebug_P(PSTR("")); -} - -/* - * Send heartbeat via MQTT with all system data - */ -void MyESP::_heartbeatCheck(bool force = false) { - static uint32_t last_heartbeat = 0; - - if ((millis() - last_heartbeat > HEARTBEAT_INTERVAL) || force) { - last_heartbeat = millis(); - - if (!isMQTTConnected() || !(_heartbeat)) { - return; - } - - uint32_t total_memory = _getInitialFreeHeap(); - uint32_t free_memory = ESP.getFreeHeap(); - uint8_t mem_available = 100 * free_memory / total_memory; // as a % - - char payload[300] = {0}; - char s[10]; - strlcpy(payload, "version=", sizeof(payload)); - strlcat(payload, _app_version, sizeof(payload)); // version - strlcat(payload, ", IP=", sizeof(payload)); - strlcat(payload, WiFi.localIP().toString().c_str(), sizeof(payload)); // IP address - strlcat(payload, ", rssid=", sizeof(payload)); - strlcat(payload, itoa(getWifiQuality(), s, 10), sizeof(payload)); // rssi % - strlcat(payload, "%, load=", sizeof(payload)); - strlcat(payload, ltoa(getSystemLoadAverage(), s, 10), sizeof(payload)); // load - strlcat(payload, "%, uptime=", sizeof(payload)); - strlcat(payload, ltoa(_getUptime(), s, 10), sizeof(payload)); // uptime in secs - strlcat(payload, "secs, freemem=", sizeof(payload)); - strlcat(payload, itoa(mem_available, s, 10), sizeof(payload)); // free mem as a % - strlcat(payload, "%", sizeof(payload)); - - // send to MQTT - myESP.mqttPublish(MQTT_TOPIC_HEARTBEAT, payload); - } -} - -// 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(); - - if (c == 0) - return; - - SerialAndTelnet.serialPrint(c); // echo to Serial (if connected) - - switch (c) { - case '\r': // likely have full command in buffer now, commands are terminated by CR and/or LF - case '\n': - _command[charsRead] = '\0'; // null terminate our command char array - - if (charsRead > 0) { - charsRead = 0; // is static, so have to reset - _suspendOutput = false; - if (_serial) { - SerialAndTelnet.serialPrint('\n'); // force newline if in Serial - } - _telnetCommand(_command); - } - break; - - case '\b': // (^H) - case 0x7F: // (^?) - if (charsRead > 0) { - _command[--charsRead] = '\0'; - - SerialAndTelnet.write(' '); - SerialAndTelnet.write('\b'); - } - - break; - - case '?': - if (!_suspendOutput) { - _consoleShowHelp(); - } else { - _command[charsRead++] = c; // add it to buffer as its part of the string entered - } - break; - case 0x04: // EOT, CTRL-D - myDebug_P(PSTR("[TELNET] exiting telnet session")); - SerialAndTelnet.disconnectClient(); - break; - default: - _suspendOutput = true; - if (charsRead < TELNET_MAX_COMMAND_LENGTH) { - _command[charsRead++] = c; - } - _command[charsRead] = '\0'; // just in case - break; - } - } -} - -// make sure we have a connection to MQTT broker -void MyESP::_mqttConnect() { - if (!_mqtt_host) - return; // MQTT not enabled - - // Do not connect if already connected or still trying to connect - if (mqttClient.connected() || _mqtt_connecting || (WiFi.status() != WL_CONNECTED)) { - return; - } - - // Check reconnect interval - if (millis() - _mqtt_last_connection < _mqtt_reconnect_delay) { - return; - } - - _mqtt_connecting = true; // we're doing a connection - - // Increase the reconnect delay - _mqtt_reconnect_delay += MQTT_RECONNECT_DELAY_STEP; - if (_mqtt_reconnect_delay > MQTT_RECONNECT_DELAY_MAX) { - _mqtt_reconnect_delay = MQTT_RECONNECT_DELAY_MAX; - } - - mqttClient.setServer(_mqtt_host, MQTT_PORT); - mqttClient.setClientId(_app_hostname); - mqttClient.setKeepAlive(_mqtt_keepalive); - mqttClient.setCleanSession(false); - - // last will - if (_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 (_mqtt_username && _mqtt_password) { - myDebug_P(PSTR("[MQTT] Connecting to MQTT using user %s..."), _mqtt_username); - mqttClient.setCredentials(_mqtt_username, _mqtt_password); - } else { - myDebug_P(PSTR("[MQTT] Connecting to MQTT...")); - } - - // Connect to the MQTT broker - mqttClient.connect(); -} - -// Setup everything we need -void MyESP::setWIFI(const char * wifi_ssid, const char * wifi_password, wifi_callback_f callback) { - // Check SSID too long or missing - if (!wifi_ssid || *wifi_ssid == 0x00 || strlen(wifi_ssid) > MAX_SSID_LEN) { - _wifi_ssid = NULL; - } else { - _wifi_ssid = strdup(wifi_ssid); - } - - // Check PASS too long - if (!wifi_password || *wifi_ssid == 0x00 || strlen(wifi_password) > MAX_PWD_LEN) { - _wifi_password = NULL; - } else { - _wifi_password = strdup(wifi_password); - } - - // callback - _wifi_callback = callback; -} - -// init MQTT settings -void MyESP::setMQTT(const char * mqtt_host, - const char * mqtt_username, - const char * mqtt_password, - const char * mqtt_base, - unsigned long mqtt_keepalive, - unsigned char mqtt_qos, - bool mqtt_retain, - const char * mqtt_will_topic, - const char * mqtt_will_online_payload, - const char * mqtt_will_offline_payload, - mqtt_callback_f callback) { - // can be empty - if (!mqtt_host || *mqtt_host == 0x00) { - _mqtt_host = NULL; - } else { - _mqtt_host = strdup(mqtt_host); - } - - // mqtt username and password can be empty - if (!mqtt_username || *mqtt_username == 0x00) { - _mqtt_username = NULL; - } else { - _mqtt_username = strdup(mqtt_username); - } - - // can be empty - if (!mqtt_password || *mqtt_password == 0x00) { - _mqtt_password = NULL; - } else { - _mqtt_password = strdup(mqtt_password); - } - - // base - if (_mqtt_base) { - free(_mqtt_base); - } - _mqtt_base = strdup(mqtt_base); - - // callback - _mqtt_callback = callback; - - // various mqtt settings - _mqtt_keepalive = mqtt_keepalive; - _mqtt_qos = mqtt_qos; - _mqtt_retain = mqtt_retain; - - // last will - if (!mqtt_will_topic || *mqtt_will_topic == 0x00) { - _mqtt_will_topic = NULL; - } else { - _mqtt_will_topic = strdup(mqtt_will_topic); - } - - if (!mqtt_will_online_payload || *mqtt_will_online_payload == 0x00) { - _mqtt_will_online_payload = NULL; - } else { - _mqtt_will_online_payload = strdup(mqtt_will_online_payload); - } - - if (!mqtt_will_offline_payload || *mqtt_will_offline_payload == 0x00) { - _mqtt_will_offline_payload = NULL; - } else { - _mqtt_will_offline_payload = strdup(mqtt_will_offline_payload); - } -} - -// builds up a topic by prefixing the base and hostname -char * MyESP::_mqttTopic(const char * topic) { - char buffer[MQTT_MAX_TOPIC_SIZE] = {0}; - - strlcpy(buffer, _mqtt_base, sizeof(buffer)); - strlcat(buffer, "/", sizeof(buffer)); - strlcat(buffer, _app_hostname, sizeof(buffer)); - strlcat(buffer, "/", sizeof(buffer)); - strlcat(buffer, topic, sizeof(buffer)); - - if (_mqtt_topic) { - free(_mqtt_topic); - } - _mqtt_topic = strdup(buffer); - - return _mqtt_topic; -} - -// print contents of file -// assumes Serial is open -void MyESP::_fs_printConfig() { - myDebug_P(PSTR("[FS] Contents:")); - - File configFile = SPIFFS.open(MYEMS_CONFIG_FILE, "r"); - if (!configFile) { - myDebug_P(PSTR("[FS] Failed to read file for printing")); - return; - } - - while (configFile.available()) { - SerialAndTelnet.print((char)configFile.read()); - } - myDebug_P(PSTR("")); // newline - - configFile.close(); -} - -// format File System -void MyESP::_fs_eraseConfig() { - myDebug_P(PSTR("[FS] Erasing all settings, please wait a few seconds. ESP will " - "automatically restart when finished.")); - - if (SPIFFS.remove(MYEMS_CONFIG_FILE)) { - delay(1000); // wait 1 second - SerialAndTelnet.flush(); - resetESP(); // hard reset - } -} - -// custom callback for web info -void MyESP::setWeb(web_callback_f callback_web) { - _web_callback = callback_web; -} - -void MyESP::setSettings(fs_callback_f callback_fs, fs_settings_callback_f callback_settings_fs) { - _fs_callback = callback_fs; - _fs_settings_callback = callback_settings_fs; -} - -// load from spiffs -bool MyESP::_fs_loadConfig() { - File configFile = SPIFFS.open(MYEMS_CONFIG_FILE, "r"); - - size_t size = configFile.size(); - if (size > 1024) { - myDebug_P(PSTR("[FS] Config file size is too large")); - return false; - } else if (size == 0) { - return false; - } - - StaticJsonDocument doc; - JsonObject json = doc.to(); - - // Deserialize the JSON document - DeserializationError error = deserializeJson(doc, configFile); - if (error) { - myDebug_P(PSTR("[FS] Failed to read config file. Error %s"), error.c_str()); - return false; - } - - const char * value; - - // fetch the standard system parameters - value = json["wifi_ssid"]; - _wifi_ssid = (value) ? strdup(value) : NULL; - - value = json["wifi_password"]; - _wifi_password = (value) ? strdup(value) : NULL; - - value = json["mqtt_host"]; - _mqtt_host = (value) ? strdup(value) : NULL; - - value = json["mqtt_username"]; - _mqtt_username = (value) ? strdup(value) : NULL; - - value = json["mqtt_password"]; - _mqtt_password = (value) ? strdup(value) : NULL; - - _heartbeat = (bool)json["heartbeat"]; // defaults to off - -// serial is only on when booting -#ifdef FORCE_SERIAL - _serial = true; -#else - _serial = json["serial"]; -#endif - - // callback for loading custom settings - // ok is false if there's a problem loading a custom setting (e.g. does not exist) - bool ok = (_fs_callback)(MYESP_FSACTION_LOAD, json); - - configFile.close(); - - return ok; -} - -// save settings to spiffs -bool MyESP::fs_saveConfig() { - bool ok = true; - - // call any custom functions before handling SPIFFS - if (_ota_pre_callback) { - (_ota_pre_callback)(); - } - - StaticJsonDocument doc; - JsonObject json = doc.to(); - - json["app_version"] = _app_version; - json["wifi_ssid"] = _wifi_ssid; - json["wifi_password"] = _wifi_password; - json["mqtt_host"] = _mqtt_host; - json["mqtt_username"] = _mqtt_username; - json["mqtt_password"] = _mqtt_password; - json["serial"] = _serial; - json["heartbeat"] = _heartbeat; - - // callback for saving custom settings - (void)(_fs_callback)(MYESP_FSACTION_SAVE, json); - - // if file exists, remove it just to be safe - if (SPIFFS.exists(MYEMS_CONFIG_FILE)) { - SPIFFS.remove(MYEMS_CONFIG_FILE); - } - - // open for writing - File configFile = SPIFFS.open(MYEMS_CONFIG_FILE, "w"); - if (!configFile) { - myDebug_P(PSTR("[FS] Failed to open config file for writing")); - return false; - } - - // Serialize JSON to file - if (serializeJson(json, configFile) == 0) { - myDebug_P(PSTR("[FS] Failed to write config file")); - ok = false; - } - - configFile.close(); - - // call any custom functions before handling SPIFFS - if (_ota_post_callback) { - (_ota_post_callback)(); - } - - return ok; // it worked -} - -// init the SPIFF file system and load the config -// if it doesn't exist try and create it -void MyESP::_fs_setup() { - if (!SPIFFS.begin()) { - myDebug_P(PSTR("[FS] Failed to mount the file system. Erasing...")); - _fs_eraseConfig(); // fix for ESP32 - return; - } - - // if its flagged as a first install, re-create the initial config file and quit function - if (_firstInstall) { - myDebug_P(PSTR("[FS] Re-creating config file for initial install")); - fs_saveConfig(); - return; - } - - // load the config file. if it doesn't exist (function returns false) create it - if (!_fs_loadConfig()) { - myDebug_P(PSTR("[FS] Re-creating config file")); - fs_saveConfig(); - _firstInstall = true; // flag as a first install - } - - // assume if the wifi ssid is empty, its a fresh install too - if ((_wifi_ssid == NULL)) { - _firstInstall = true; // flag as a first install - } - - myDebug_P(PSTR("[FS] Settings loaded from SPIFFS")); - - // _fs_printConfig(); // enable for debugging -} - -uint32_t MyESP::getSystemLoadAverage() { - return _load_average; -} - -// calculate load average -void MyESP::_calculateLoad() { - static uint32_t last_loadcheck = 0; - static uint32_t load_counter_temp = 0; - load_counter_temp++; - - if (millis() - last_loadcheck > LOADAVG_INTERVAL) { - static uint32_t load_counter = 0; - static uint32_t load_counter_max = 1; - - load_counter = load_counter_temp; - load_counter_temp = 0; - if (load_counter > load_counter_max) { - load_counter_max = load_counter; - } - _load_average = 100 - (100 * load_counter / load_counter_max); - last_loadcheck = millis(); - } -} - -// returns true is MQTT is alive -bool MyESP::isMQTTConnected() { - return mqttClient.connected(); -} - -// return true if wifi is connected -bool MyESP::isWifiConnected() { - return (_wifi_connected); -} - -/* - Return the quality (Received Signal Strength Indicator) - of the WiFi network. - Returns a number between 0 and 100 if WiFi is connected. - Returns -1 if WiFi is disconnected. - - High quality: 90% ~= -55dBm - Medium quality: 50% ~= -75dBm - Low quality: 30% ~= -85dBm - Unusable quality: 8% ~= -96dBm -*/ -int MyESP::getWifiQuality() { - if (WiFi.status() != WL_CONNECTED) - return -1; - int dBm = WiFi.RSSI(); - if (dBm <= -100) - return 0; - if (dBm >= -50) - return 100; - return 2 * (dBm + 100); -} - -#ifdef CRASH -/** - * Save crash information in EEPROM - * This function is called automatically if ESP8266 suffers an exception - * It should be kept quick / consise to be able to execute before hardware wdt may kick in - */ -extern "C" void custom_crash_callback(struct rst_info * rst_info, uint32_t stack_start, uint32_t stack_end) { - // write crash time to EEPROM - uint32_t crash_time = millis(); - EEPROMr.put(SAVE_CRASH_EEPROM_OFFSET + SAVE_CRASH_CRASH_TIME, crash_time); - - // write reset info to EEPROM - EEPROMr.write(SAVE_CRASH_EEPROM_OFFSET + SAVE_CRASH_RESTART_REASON, rst_info->reason); - EEPROMr.write(SAVE_CRASH_EEPROM_OFFSET + SAVE_CRASH_EXCEPTION_CAUSE, rst_info->exccause); - - // write epc1, epc2, epc3, excvaddr and depc to EEPROM - EEPROMr.put(SAVE_CRASH_EEPROM_OFFSET + SAVE_CRASH_EPC1, rst_info->epc1); - EEPROMr.put(SAVE_CRASH_EEPROM_OFFSET + SAVE_CRASH_EPC2, rst_info->epc2); - EEPROMr.put(SAVE_CRASH_EEPROM_OFFSET + SAVE_CRASH_EPC3, rst_info->epc3); - EEPROMr.put(SAVE_CRASH_EEPROM_OFFSET + SAVE_CRASH_EXCVADDR, rst_info->excvaddr); - EEPROMr.put(SAVE_CRASH_EEPROM_OFFSET + SAVE_CRASH_DEPC, rst_info->depc); - - // write stack start and end address to EEPROM - EEPROMr.put(SAVE_CRASH_EEPROM_OFFSET + SAVE_CRASH_STACK_START, stack_start); - EEPROMr.put(SAVE_CRASH_EEPROM_OFFSET + SAVE_CRASH_STACK_END, stack_end); - - // write stack trace to EEPROM and avoid overwriting settings - int16_t current_address = SAVE_CRASH_EEPROM_OFFSET + SAVE_CRASH_STACK_TRACE; - for (uint32_t i = stack_start; i < stack_end; i++) { - byte * byteValue = (byte *)i; - EEPROMr.write(current_address++, *byteValue); - } - - EEPROMr.commit(); -} - -/** - * Clears crash info - */ -void MyESP::crashClear() { - myDebug_P(PSTR("[CRASH] Clearing crash dump")); - uint32_t crash_time = 0xFFFFFFFF; - EEPROMr.put(SAVE_CRASH_EEPROM_OFFSET + SAVE_CRASH_CRASH_TIME, crash_time); - EEPROMr.commit(); -} - -/** - * Print out crash information that has been previously saved in EEPROM - * Copied from https://github.com/krzychb/EspSaveCrash - */ -void MyESP::crashDump() { - uint32_t crash_time; - EEPROMr.get(SAVE_CRASH_EEPROM_OFFSET + SAVE_CRASH_CRASH_TIME, crash_time); - if ((crash_time == 0) || (crash_time == 0xFFFFFFFF)) { - myDebug_P(PSTR("[CRASH] No crash data captured.")); - return; - } - - uint32_t t = crash_time / 1000; // convert to seconds - uint32_t d = t / 86400L; - uint32_t h = (t / 3600L) % 60; - uint32_t rem = t % 3600L; - uint8_t m = rem / 60; - uint8_t s = rem % 60; - myDebug_P(PSTR("[CRASH] Last crash was %d days %d hours %d minutes %d seconds since boot time"), d, h, m, s); - - // get reason and exception - // https://www.espressif.com/sites/default/files/documentation/esp8266_reset_causes_and_common_fatal_exception_causes_en.pdf - char buffer[80] = {0}; - char ss[16] = {0}; - strlcpy(buffer, "[CRASH] Reason of restart: ", sizeof(buffer)); - - uint8_t reason = EEPROMr.read(SAVE_CRASH_EEPROM_OFFSET + SAVE_CRASH_RESTART_REASON); - switch (reason) { - case REASON_WDT_RST: - strlcat(buffer, "1 - Hardware WDT reset", sizeof(buffer)); - break; - case REASON_EXCEPTION_RST: - strlcat(buffer, "2 - Fatal exception", sizeof(buffer)); - break; - case REASON_SOFT_WDT_RST: - strlcat(buffer, "3 - Software watchdog reset", sizeof(buffer)); - break; - case REASON_EXT_SYS_RST: - strlcat(buffer, "6 - Hardware reset", sizeof(buffer)); - break; - case REASON_SOFT_RESTART: - strlcat(buffer, "4 - Software reset", sizeof(buffer)); - break; - default: - strlcat(buffer, itoa(reason, ss, 10), sizeof(buffer)); - } - myDebug(buffer); - - // check for exception - // see https://github.com/esp8266/Arduino/blob/master/doc/exception_causes.rst - if (reason == REASON_EXCEPTION_RST) { - // get exception cause - uint8_t cause = EEPROMr.read(SAVE_CRASH_EEPROM_OFFSET + SAVE_CRASH_EXCEPTION_CAUSE); - strlcpy(buffer, "[CRASH] Exception cause: ", sizeof(buffer)); - if (cause == 0) { - strlcat(buffer, "0 - IllegalInstructionCause", sizeof(buffer)); - } else if (cause == 3) { - strlcat(buffer, "3 - LoadStoreErrorCause", sizeof(buffer)); - } else if (cause == 6) { - strlcat(buffer, "6 - IntegerDivideByZeroCause", sizeof(buffer)); - } else if (cause == 9) { - strlcat(buffer, "9 - LoadStoreAlignmentCause", sizeof(buffer)); - } else { - strlcat(buffer, itoa(cause, ss, 10), sizeof(buffer)); - } - } - myDebug(buffer); - - uint32_t epc1, epc2, epc3, excvaddr, depc; - EEPROMr.get(SAVE_CRASH_EEPROM_OFFSET + SAVE_CRASH_EPC1, epc1); - EEPROMr.get(SAVE_CRASH_EEPROM_OFFSET + SAVE_CRASH_EPC2, epc2); - EEPROMr.get(SAVE_CRASH_EEPROM_OFFSET + SAVE_CRASH_EPC3, epc3); - EEPROMr.get(SAVE_CRASH_EEPROM_OFFSET + SAVE_CRASH_EXCVADDR, excvaddr); - EEPROMr.get(SAVE_CRASH_EEPROM_OFFSET + SAVE_CRASH_DEPC, depc); - - myDebug_P(PSTR("[CRASH] epc1=0x%08x epc2=0x%08x epc3=0x%08x"), epc1, epc2, epc3); - myDebug_P(PSTR("[CRASH] excvaddr=0x%08x depc=0x%08x"), excvaddr, depc); - - uint32_t stack_start, stack_end; - EEPROMr.get(SAVE_CRASH_EEPROM_OFFSET + SAVE_CRASH_STACK_START, stack_start); - EEPROMr.get(SAVE_CRASH_EEPROM_OFFSET + SAVE_CRASH_STACK_END, stack_end); - - myDebug_P(PSTR("[CRASH] sp=0x%08x end=0x%08x"), stack_start, stack_end); - - int16_t current_address = SAVE_CRASH_EEPROM_OFFSET + SAVE_CRASH_STACK_TRACE; - int16_t stack_len = stack_end - stack_start; - - uint32_t stack_trace; - - myDebug_P(PSTR(">>>stack>>>")); - - for (int16_t i = 0; i < stack_len; i += 0x10) { - SerialAndTelnet.printf("%08x: ", stack_start + i); - for (byte j = 0; j < 4; j++) { - EEPROMr.get(current_address, stack_trace); - SerialAndTelnet.printf("%08x ", stack_trace); - current_address += 4; - } - SerialAndTelnet.println(); - } - myDebug_P(PSTR("<<", sizeof(s)); - strlcat(s, _app_name, sizeof(s)); - strlcat(s, " version ", sizeof(s)); - strlcat(s, _app_version, sizeof(s)); - strlcat(s, "", sizeof(s)); - - strlcat(s, "

System stats:
", sizeof(s)); - - if (isAPmode()) { - strlcat(s, " Device is in Wifi Access Point mode with SSID ", sizeof(s)); - strlcat(s, jw.getAPSSID().c_str(), sizeof(s)); - strlcat(s, "", sizeof(s)); - } else { - char buf[4]; - strlcat(s, " Connected to wireless network ", sizeof(s)); - strlcat(s, _getESPhostname().c_str(), sizeof(s)); - strlcat(s, " with signal strength ", sizeof(s)); - strlcat(s, itoa(getWifiQuality(), buf, 10), sizeof(s)); - strlcat(s, "%", sizeof(s)); - } - - strlcat(s, isMQTTConnected() ? "
MQTT is connected\n" : " MQTT is disconnected\n", sizeof(s)); - strlcat(s, "
", sizeof(s)); - - // uptime - char buffer[200]; - uint32_t t = _getUptime(); // seconds - uint32_t d = t / 86400L; - uint32_t h = ((t % 86400L) / 3600L) % 60; - uint32_t rem = t % 3600L; - uint8_t m = rem / 60; - uint8_t sec = rem % 60; - sprintf(buffer, " System uptime: %d days %d hours %d minutes %d seconds", d, h, m, sec); - strlcat(s, buffer, sizeof(s)); - - // memory - //uint32_t total_memory = _getInitialFreeHeap(); - //uint32_t free_memory = ESP.getFreeHeap(); - //sprintf(buffer, " Memory: %d bytes free (%2u%%)
", free_memory, 100 * free_memory / total_memory); - //strlcat(s, buffer, sizeof(s)); - - strlcat(s, "

", sizeof(s)); - if (_web_callback) { - char custom[MYESP_MAXCHARBUFFER]; - (_web_callback)(custom); - strlcat(s, custom, sizeof(s)); - } - strlcat(s, "


", sizeof(s)); - - // check why we're here - if ((_firstInstall) || (_wifi_ssid == NULL)) { - strlcat(s, "

Looks like a first install! Go here to connect the System to your network.

", sizeof(s)); - } else { - strlcat(s, "

Go here to connect the System to your wireless network.

", sizeof(s)); - } - - strlcat(s, webCommonPage_end, sizeof(s)); - webServer.sendHeader("Content-Length", String(strlen(s))); - webServer.send(200, "text/html", s); -} - -// Creates a webpage that allows the user to change the SSID and Password from the browser -void MyESP::_webResetPage() { - char s[1000] = {0}; - - strlcpy(s, webCommonPage_start, sizeof(s)); - strlcat(s, webCommonPage_start_body, sizeof(s)); - - strlcat(s, "

", sizeof(s)); - strlcat(s, _app_name, sizeof(s)); - strlcat(s, " version ", sizeof(s)); - strlcat(s, _app_version, sizeof(s)); - strlcat(s, "

", sizeof(s)); - - // Check to see if we've been sent any arguments and instantly return if not - if (webServer.args() == 0) { - strlcat(s, "

", sizeof(s)); - - if (_wifi_ssid != NULL) { - strlcat(s, "Current wifi SSID is ", sizeof(s)); - strlcat(s, _wifi_ssid, sizeof(s)); - strlcat(s, ".
", sizeof(s)); - } - - strlcat(s, "
Please enter your new wifi credentials below.

", sizeof(s)); - - strlcat(s, webResetPage_form, sizeof(s)); - strlcat(s, webCommonPage_end, sizeof(s)); - webServer.sendHeader("Content-Length", String(strlen(s))); - webServer.send(200, "text/html", s); - - } else { - // Create a string containing all the arguments - // Check to see if there are new values (also doubles to check the length of the new value is long enough) - if (webServer.arg("newssid").length() <= MAX_SSID_LEN) { - if (webServer.arg("newssid").length() == 0) { - _wifi_ssid = NULL; - } else { - _wifi_ssid = strdup(webServer.arg("newssid").c_str()); - } - } - - if (webServer.arg("newpassword").length() <= MAX_PWD_LEN) { - if (webServer.arg("newpassword").length() == 0) { - _wifi_password = NULL; - } else { - _wifi_password = strdup(webServer.arg("newpassword").c_str()); - } - } - - // Store the new settings - fs_saveConfig(); - - // Reply with a web page to indicate success or failure - strlcat(s, webResetPage_post, sizeof(s)); - strlcat(s, webCommonPage_end, sizeof(s)); - webServer.sendHeader("Content-Length", String(strlen(s))); - webServer.send(200, "text/html", s); - - delay(500); - resetESP(); - } -} - -// reset all settings -void MyESP::_webResetAllPage() { - char s[1000] = {0}; - - strlcpy(s, webCommonPage_start, sizeof(s)); - strlcat(s, webCommonPage_start_body, sizeof(s)); - - strlcat(s, "

", sizeof(s)); - strlcat(s, _app_name, sizeof(s)); - strlcat(s, " version ", sizeof(s)); - strlcat(s, _app_version, sizeof(s)); - strlcat(s, "

", sizeof(s)); - - // Check to see if we've been sent any arguments and instantly return if not - if (webServer.args() == 0) { - strlcat(s, - "

Are you absolutely sure you want to erase all settings?
Typing 'yes' will restart the System and you'll need to reconnect to the wifi " - "Access Point called ems-esp.

", - sizeof(s)); - - strlcat(s, webResetAllPage_form, sizeof(s)); - strlcat(s, webCommonPage_end, sizeof(s)); - webServer.sendHeader("Content-Length", String(strlen(s))); - webServer.send(200, "text/html", s); - } else { - // delete all settings - if (webServer.arg("confirm") == "yes") { - _fs_eraseConfig(); - delay(1000); // wait 1 sec - resetESP(); - } - } -} - -// set up web server -void MyESP::_webserver_setup() { - webServer.on("/", [this]() { _webRootPage(); }); - webServer.on("/reset", [this]() { _webResetPage(); }); - webServer.on("/resetall", [this]() { _webResetAllPage(); }); - - webServer.begin(); - - myDebug_P(PSTR("[WEB] Web server started")); -} - -// bootup sequence -// quickly flash LED until we get a Wifi connection, or AP established -// fast way is to use WRITE_PERI_REG(PERIPHS_GPIO_BASEADDR + (state ? 4 : 8), (1 << EMSESP_Status.led_gpio)); // 4 is on, 8 is off -void MyESP::_bootupSequence() { - uint8_t boot_status = getSystemBootStatus(); - - if ((boot_status == MYESP_BOOTSTATUS_BOOTED) || (millis() <= MYESP_BOOTUP_DELAY)) { - return; // already booted, or still starting up - } - - // only kick in after a few seconds - if (boot_status == MYESP_BOOTSTATUS_POWERON) { - _setSystemBootStatus(MYESP_BOOTSTATUS_BOOTING); - } - - static uint32_t last_bootupflash = 0; - - // flash LED quickly - if ((millis() - last_bootupflash > MYESP_BOOTUP_FLASHDELAY)) { - last_bootupflash = millis(); - int state = digitalRead(LED_BUILTIN); - digitalWrite(LED_BUILTIN, !state); - } - - if (isWifiConnected()) { - _setSystemBootStatus(MYESP_BOOTSTATUS_BOOTED); // completed, reset flag - digitalWrite(LED_BUILTIN, HIGH); // turn off LED - } -} - -// setup MyESP -void MyESP::begin(const char * app_hostname, const char * app_name, const char * app_version) { - _app_hostname = strdup(app_hostname); - _app_name = strdup(app_name); - _app_version = strdup(app_version); - - _telnet_setup(); // Telnet setup, called first to set Serial - - // print a welcome message - myDebug_P(PSTR("\n\n* %s version %s"), _app_name, _app_version); - - // set up onboard LED - pinMode(LED_BUILTIN, OUTPUT); - digitalWrite(LED_BUILTIN, HIGH); - - _getInitialFreeHeap(); // get initial free mem - _rtcmemSetup(); // rtc internal mem setup - - if (getSystemBootStatus() == MYESP_BOOTSTATUS_RESETNEEDED) { - myDebug_P(PSTR("** resetting all settings")); - _firstInstall = true; // flag as an initial install so the config file will be recreated - } - - _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 - _ota_setup(); // init OTA - _webserver_setup(); // init web server - - _setSystemCheck(false); // reset system check - _heartbeatCheck(true); // force heartbeat check (not the MQTT one) - - SerialAndTelnet.flush(); -} - -/* - * Loop. This is called as often as possible and it handles wifi, telnet, mqtt etc - */ -void MyESP::loop() { - jw.loop(); // WiFi - ArduinoOTA.handle(); // OTA - - if (_ota_doing_update) { - return; // quit if in the middle of an OTA update - } - - _calculateLoad(); - _systemCheckLoop(); - _heartbeatCheck(); - _bootupSequence(); - webServer.handleClient(); - _telnetHandle(); - _mqttConnect(); - - yield(); // ...and breath -} - -MyESP myESP; // create instance diff --git a/platformio.ini-example b/platformio.ini-example index fec449cab..40424e9d5 100644 --- a/platformio.ini-example +++ b/platformio.ini-example @@ -8,40 +8,69 @@ default_envs = release [common] ; -DMYESP_TIMESTAMP -DTESTS -DCRASH -DFORCE_SERIAL -DNO_GLOBAL_EEPROM -DLOGICANALYZER -extra_flags = -DNO_GLOBAL_EEPROM +;general_flags = -Wl,-Teagle.flash.4m2m.ld +general_flags = [env] +;board = esp12e board = d1_mini -; board = nodemcuv2 -; board = d1_mini_pro framework = arduino platform = espressif8266 lib_deps = CRC32 CircularBuffer + OneWire JustWifi AsyncMqttClient ArduinoJson - OneWire EEPROM_rotate + ESP Async WebServer + ESPAsyncTCP + ESPAsyncUDP upload_speed = 921600 monitor_speed = 115200 + +; example ports for OSX +;upload_port = /dev/cu.wchusbserial14403 +;upload_port = /dev/cu.usbserial-1440 + ; uncomment next 2 lines for OTA ;upload_protocol = espota ;upload_port = ems-esp.local +[env:buildweb] +extra_scripts = pre:scripts/buildweb.py + +[env:test] +build_type = debug +build_flags = ${common.general_flags} -DTESTS +extra_scripts = + pre:scripts/rename_fw.py + pre:scripts/buildweb.py + +[env:crash] +build_type = debug +build_flags = ${common.general_flags} -DNO_GLOBAL_EEPROM -DCRASH +extra_scripts = + pre:scripts/rename_fw.py + pre:scripts/buildweb.py + [env:debug] build_type = debug -build_flags = ${common.extra_flags} -DCRASH -extra_scripts = pre:scripts/rename_fw.py +build_flags = ${common.general_flags} +extra_scripts = + pre:scripts/rename_fw.py + pre:scripts/buildweb.py [env:clean] extra_scripts = pre:scripts/clean_fw.py [env:release] -build_flags = ${common.extra_flags} +build_flags = ${common.general_flags} extra_scripts = pre:scripts/rename_fw.py [env:checkcode] -build_flags = ${common.extra_flags} +build_type = debug +build_flags = ${common.general_flags} -Wall extra_scripts = scripts/checkcode.py + diff --git a/scripts/buildweb.py b/scripts/buildweb.py new file mode 100644 index 000000000..93a75510c --- /dev/null +++ b/scripts/buildweb.py @@ -0,0 +1,3 @@ +Import("env") + +env.Execute("node ./tools/webfilesbuilder/node_modules/gulp/bin/gulp.js --cwd ./tools/webfilesbuilder") diff --git a/scripts/rename_fw.py b/scripts/rename_fw.py index 42a11881a..7d2d1845e 100755 --- a/scripts/rename_fw.py +++ b/scripts/rename_fw.py @@ -1,7 +1,13 @@ #!/usr/bin/env python +from subprocess import call +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"]) + bag = {} exprs = [ (re.compile(r'^#define APP_VERSION\s+"(\S+)"'), 'app_version'), @@ -22,5 +28,9 @@ 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(".", "_")) + diff --git a/src/MyESP.cpp b/src/MyESP.cpp new file mode 100644 index 000000000..961828966 --- /dev/null +++ b/src/MyESP.cpp @@ -0,0 +1,2826 @@ +/* + * MyESP - my ESP helper class to handle WiFi, MQTT, Telnet, Web and other utils + * + * Paul Derbyshire - first revision: December 2018 + * + * with ideas borrowed from Espurna https://github.com/xoseperez/espurna + * and web from https://github.com/esprfid/esp-rfid + */ + +#include "MyESP.h" + +#ifdef CRASH +EEPROM_Rotate EEPROMr; +#endif + +union system_rtcmem_t { + struct { + uint8_t stability_counter; + uint8_t reset_reason; + uint8_t boot_status; + uint8_t _reserved_; + } parts; + uint32_t value; +}; + +// nasty global variables that are called from internal ws functions +static char * _general_password = nullptr; +static bool _shouldRestart = false; + +uint8_t RtcmemSize = (sizeof(RtcmemData) / 4u); +auto Rtcmem = reinterpret_cast(RTCMEM_ADDR); + +// constructor +MyESP::MyESP() { + _general_hostname = strdup("myesp"); + _app_name = strdup("MyESP"); + _app_version = strdup(MYESP_VERSION); + _app_url = nullptr; + _app_updateurl = nullptr; + + // general + _timerequest = false; + _formatreq = false; + _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 + _have_ntp_time = false; + + // telnet + _command[0] = '\0'; + _telnetcommand_callback_f = nullptr; + _telnet_callback_f = nullptr; + + // fs + _fs_loadsave_callback_f = nullptr; + _fs_setlist_callback_f = nullptr; + + // mqtt + _mqtt_ip = nullptr; + _mqtt_password = nullptr; + _mqtt_user = nullptr; + _mqtt_port = MQTT_PORT; + _mqtt_base = nullptr; + _mqtt_reconnect_delay = MQTT_RECONNECT_DELAY_MIN; + _mqtt_last_connection = 0; + _mqtt_connecting = false; + _mqtt_enabled = false; + _mqtt_heartbeat = false; + _mqtt_keepalive = MQTT_KEEPALIVE; + _mqtt_qos = MQTT_QOS; + _mqtt_retain = MQTT_RETAIN; + _mqtt_will_topic = strdup(MQTT_WILL_TOPIC); + _mqtt_will_online_payload = strdup(MQTT_WILL_ONLINE_PAYLOAD); + _mqtt_will_offline_payload = strdup(MQTT_WILL_OFFLINE_PAYLOAD); + + // network + _network_password = nullptr; + _network_ssid = nullptr; + _network_wmode = 1; // default AP + + _wifi_callback_f = nullptr; + _wifi_connected = false; + + // web + _web_callback_f = nullptr; + _webServer = new AsyncWebServer(80); + _ws = new AsyncWebSocket("/ws"); + _http_username = strdup(MYESP_HTTP_PASSWORD); + _general_password = strdup(MYESP_HTTP_PASSWORD); + + // system + _rtcmem_status = false; + _systemStable = true; + + // ntp + _ntp_server = strdup(MYESP_NTP_SERVER); + ; + _ntp_interval = 60; + _ntp_enabled = false; + + // get the build time + _buildTime = _getBuildTime(); + + // MQTT log + for (uint8_t i = 0; i < MYESP_MQTTLOG_MAX; i++) { + MQTT_log[i].timestamp = 0; + MQTT_log[i].topic = nullptr; + MQTT_log[i].payload = nullptr; + } +} + +MyESP::~MyESP() { + end(); +} + +// end +void MyESP::end() { + SPIFFS.end(); + _ws->enable(false); + delete _webServer; + delete _ws; + SerialAndTelnet.end(); + jw.disconnect(); +} + +// general debug to the telnet or serial channels +void MyESP::myDebug(const char * format, ...) { + if (_suspendOutput) + return; + + va_list args; + va_start(args, format); + char test[1]; + + int len = ets_vsnprintf(test, 1, format, args) + 1; + + char * buffer = new char[len]; + ets_vsnprintf(buffer, len, format, args); + va_end(args); + + SerialAndTelnet.println(buffer); + + delete[] buffer; +} + +// for flashmemory. Must use PSTR() +void MyESP::myDebug_P(PGM_P format_P, ...) { + if (_suspendOutput) + return; + + char format[strlen_P(format_P) + 1]; + memcpy_P(format, format_P, sizeof(format)); + + va_list args; + va_start(args, format_P); + char test[1]; + int len = ets_vsnprintf(test, 1, format, args) + 1; + + char * buffer = new char[len]; + ets_vsnprintf(buffer, len, format, args); + + va_end(args); + +#ifdef MYESP_TIMESTAMP + // capture & print timestamp + char timestamp[10] = {0}; + snprintf_P(timestamp, sizeof(timestamp), PSTR("[%06lu] "), millis() % 1000000); + SerialAndTelnet.print(timestamp); +#endif + + SerialAndTelnet.println(buffer); + + delete[] buffer; +} + +// use Serial? +bool MyESP::getUseSerial() { + return (_general_serial); +} + +// heartbeat +bool MyESP::getHeartbeat() { + return (_mqtt_heartbeat); +} + +// init heap ram +uint32_t MyESP::_getInitialFreeHeap() { + static uint32_t _heap = 0; + + if (0 == _heap) { + _heap = ESP.getFreeHeap(); + } + + return _heap; +} + +// called when WiFi is connected, and used to start OTA, MQTT +void MyESP::_wifiCallback(justwifi_messages_t code, char * parameter) { + if ((code == MESSAGE_CONNECTED)) { + myDebug_P(PSTR("[WIFI] SSID %s"), WiFi.SSID().c_str()); + myDebug_P(PSTR("[WIFI] CH %d"), WiFi.channel()); + myDebug_P(PSTR("[WIFI] RSSI %d"), WiFi.RSSI()); + myDebug_P(PSTR("[WIFI] IP %s"), WiFi.localIP().toString().c_str()); + myDebug_P(PSTR("[WIFI] MAC %s"), WiFi.macAddress().c_str()); + myDebug_P(PSTR("[WIFI] GW %s"), WiFi.gatewayIP().toString().c_str()); + myDebug_P(PSTR("[WIFI] MASK %s"), WiFi.subnetMask().toString().c_str()); + myDebug_P(PSTR("[WIFI] DNS %s"), WiFi.dnsIP().toString().c_str()); + myDebug_P(PSTR("[WIFI] HOST %s"), _getESPhostname().c_str()); + + // start OTA + ArduinoOTA.begin(); // moved to support esp32 + myDebug_P(PSTR("[OTA] listening to %s.local:%u"), ArduinoOTA.getHostname().c_str(), OTA_PORT); + + + myDebug_P(PSTR("[SYSTEM] Last reset info: %s"), (char *)ESP.getResetInfo().c_str()); // unconditionally show the last reset reason + + // 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.")); + SerialAndTelnet.flush(); // flush so all buffer is printed to serial + setUseSerial(false); + } else { + myDebug_P(PSTR("[SYSTEM] Serial port communication is enabled.")); + } + + _wifi_connected = true; + + // 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); + } + + // call any final custom stuff + if (_wifi_callback_f) { + _wifi_callback_f(); + } + + jw.enableAPFallback(false); // Disable AP mode after initial connect was successful + } + + if (code == MESSAGE_ACCESSPOINT_CREATED) { + myDebug_P(PSTR("[WIFI] MODE AP")); + myDebug_P(PSTR("[WIFI] SSID %s"), jw.getAPSSID().c_str()); + myDebug_P(PSTR("[WIFI] IP %s"), WiFi.softAPIP().toString().c_str()); + myDebug_P(PSTR("[WIFI] MAC %s"), WiFi.softAPmacAddress().c_str()); + + // if we don't want Serial anymore, turn it off + if (!_general_serial) { + myDebug_P(PSTR("[SYSTEM] Disabling serial port communication.")); + SerialAndTelnet.flush(); // flush so all buffer is printed to serial + setUseSerial(false); + } else { + myDebug_P(PSTR("[SYSTEM] Serial port communication is enabled.")); + } + + _wifi_connected = true; + + // call any final custom stuff + if (_wifi_callback_f) { + _wifi_callback_f(); + } + } + + if (code == MESSAGE_CONNECTING) { + myDebug_P(PSTR("[WIFI] Connecting to %s"), parameter); + _wifi_connected = false; + } + + if (code == MESSAGE_CONNECT_FAILED) { + myDebug_P(PSTR("[WIFI] Could not connect to %s"), parameter); + _wifi_connected = false; + } + + if (code == MESSAGE_DISCONNECTED) { + myDebug_P(PSTR("[WIFI] Disconnected")); + _wifi_connected = false; + } + + if (code == MESSAGE_SCANNING) { + myDebug_P(PSTR("[WIFI] Scanning")); + } + + if (code == MESSAGE_SCAN_FAILED) { + myDebug_P(PSTR("[WIFI] Scan failed")); + } + + if (code == MESSAGE_NO_NETWORKS) { + myDebug_P(PSTR("[WIFI] No networks found")); + } + + if (code == MESSAGE_NO_KNOWN_NETWORKS) { + myDebug_P(PSTR("[WIFI] No known networks found")); + } + + if (code == MESSAGE_FOUND_NETWORK) { + myDebug_P(PSTR("[WIFI] %s"), parameter); + } + + if (code == MESSAGE_CONNECT_WAITING) { + // too much noise + } + + if (code == MESSAGE_ACCESSPOINT_CREATING) { + myDebug_P(PSTR("[WIFI] Creating access point")); + } + + if (code == MESSAGE_ACCESSPOINT_FAILED) { + myDebug_P(PSTR("[WIFI] Could not create access point")); + } +} + +// return true if in WiFi AP mode +// does not work after wifi reset on ESP32 yet. See https://github.com/espressif/arduino-esp32/issues/1306 +bool MyESP::isAPmode() { + return (WiFi.getMode() & WIFI_AP); +} + +// received MQTT message +// we send this to the call back function. Important to parse are the event strings such as MQTT_MESSAGE_EVENT and MQTT_CONNECT_EVENT +void MyESP::_mqttOnMessage(char * topic, char * payload, size_t len) { + if (len == 0) + return; + + char message[len + 1]; + strlcpy(message, (char *)payload, len + 1); + + // myDebug_P(PSTR("[MQTT] Received %s => %s"), topic, message); // enable for debugging + + // topics are in format MQTT_BASE/HOSTNAME/TOPIC + char * topic_magnitude = strrchr(topic, '/'); // strip out everything until last / + if (topic_magnitude != nullptr) { + topic = topic_magnitude + 1; + } + + // check for standard messages + // Restart the device + if (strcmp(topic, MQTT_TOPIC_RESTART) == 0) { + myDebug_P(PSTR("[MQTT] Received restart command"), message); + resetESP(); + return; + } + + // Send message event to custom service + (_mqtt_callback_f)(MQTT_MESSAGE_EVENT, topic, message); +} + +// MQTT subscribe +void MyESP::mqttSubscribe(const char * topic) { + if (mqttClient.connected() && (strlen(topic) > 0)) { + unsigned int packetId = mqttClient.subscribe(_mqttTopic(topic), _mqtt_qos); + myDebug_P(PSTR("[MQTT] Subscribing to %s (PID %d)"), _mqttTopic(topic), packetId); + } +} + +// MQTT unsubscribe +void MyESP::mqttUnsubscribe(const char * topic) { + if (mqttClient.connected() && (strlen(topic) > 0)) { + unsigned int packetId = mqttClient.unsubscribe(_mqttTopic(topic)); + myDebug_P(PSTR("[MQTT] Unsubscribing to %s (PID %d)"), _mqttTopic(topic), packetId); + } +} + +// MQTT Publish +void MyESP::mqttPublish(const char * topic, const char * payload) { + // myDebug_P(PSTR("[MQTT] Sending pubish to %s with payload %s"), _mqttTopic(topic), payload); // for debugging + mqttClient.publish(_mqttTopic(topic), _mqtt_qos, _mqtt_retain, payload); + + _addMQTTLog(topic, payload); // add to the log +} + +// MQTT onConnect - when a connect is established +void MyESP::_mqttOnConnect() { + myDebug_P(PSTR("[MQTT] is connected")); + _mqtt_reconnect_delay = MQTT_RECONNECT_DELAY_MIN; + + _mqtt_last_connection = millis(); + + // say we're alive to the Last Will topic + mqttClient.publish(_mqttTopic(_mqtt_will_topic), 1, true, _mqtt_will_online_payload); // qos=1, retain=true + + // subscribe to general subs + mqttSubscribe(MQTT_TOPIC_RESTART); + + // subscribe to a start message and send the first publish + mqttSubscribe(MQTT_TOPIC_START); + mqttPublish(MQTT_TOPIC_START, MQTT_TOPIC_START_PAYLOAD); + + // send heartbeat if enabled + _heartbeatCheck(true); + + // call custom function to handle mqtt receives + (_mqtt_callback_f)(MQTT_CONNECT_EVENT, nullptr, nullptr); +} + +// 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")); + (_mqtt_callback_f)(MQTT_DISCONNECT_EVENT, nullptr, nullptr); // call callback with disconnect + } + if (reason == AsyncMqttClientDisconnectReason::MQTT_IDENTIFIER_REJECTED) { + myDebug_P(PSTR("[MQTT] Identifier Rejected")); + } + if (reason == AsyncMqttClientDisconnectReason::MQTT_SERVER_UNAVAILABLE) { + myDebug_P(PSTR("[MQTT] Server unavailable")); + } + if (reason == AsyncMqttClientDisconnectReason::MQTT_MALFORMED_CREDENTIALS) { + myDebug_P(PSTR("[MQTT] Malformed credentials")); + } + if (reason == AsyncMqttClientDisconnectReason::MQTT_NOT_AUTHORIZED) { + myDebug_P(PSTR("[MQTT] Not authorized")); + } + + // Reset reconnection delay + _mqtt_last_connection = millis(); + _mqtt_connecting = false; + }); + + //mqttClient.onSubscribe([this](uint16_t packetId, uint8_t qos) { myDebug_P(PSTR("[MQTT] Subscribe ACK for PID %d"), packetId); }); + //mqttClient.onPublish([this](uint16_t packetId) { myDebug_P(PSTR("[MQTT] Publish ACK for PID %d"), packetId); }); + + mqttClient.onMessage([this](char * topic, char * payload, AsyncMqttClientMessageProperties properties, size_t len, size_t index, size_t total) { + _mqttOnMessage(topic, payload, len); + }); +} + +// 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); + jw.enableAPFallback(true); // AP mode only as fallback + jw.enableSTA(true); // Enable STA mode (connecting to a router) + jw.enableScan(false); // Configure it to scan available networks and connect in order of dBm + jw.cleanNetworks(); // Clean existing network configuration + + /// wmode 1 is AP, 0 is client + if (_network_wmode == 1) { + jw.enableAP(true); + } else { + jw.enableAP(false); + jw.addNetwork(_network_ssid, _network_password); // Add a network + } + +#if defined(ESP8266) + WiFi.setSleepMode(WIFI_NONE_SLEEP); // added to possibly fix wifi dropouts in arduino core 2.5.0 +#endif +} + +// set the callback function for the OTA onstart +void MyESP::setOTA(ota_callback_f OTACallback_pre, ota_callback_f OTACallback_post) { + _ota_pre_callback_f = OTACallback_pre; + _ota_post_callback_f = OTACallback_post; +} + +// OTA callback when the upload process starts +void MyESP::_OTACallback() { + myDebug_P(PSTR("[OTA] Start")); + +#ifdef CRASH + // If we are not specifically reserving the sectors we are using as + // EEPROM in the memory layout then any OTA upgrade will overwrite + // all but the last one. + // Calling rotate(false) disables rotation so all writes will be done + // to the last sector. It also sets the dirty flag to true so the next commit() + // will actually persist current configuration to that last sector. + // Calling rotate(false) will also prevent any other EEPROM write + // to overwrite the OTA image. + // In case the OTA process fails, reenable rotation. + // See onError callback below. + EEPROMr.rotate(false); + EEPROMr.commit(); +#endif + + // stop the web server + _ws->enable(false); + + if (_ota_pre_callback_f) { + (_ota_pre_callback_f)(); // call custom function + } +} + +// OTA Setup +void MyESP::_ota_setup() { + if (!_network_ssid) { + return; + } + + ArduinoOTA.setPort(OTA_PORT); + ArduinoOTA.setHostname(_general_hostname); + + ArduinoOTA.onStart([this]() { _OTACallback(); }); + ArduinoOTA.onEnd([this]() { + myDebug_P(PSTR("[OTA] Done, restarting...")); + _deferredReset(500, CUSTOM_RESET_OTA); + }); + + ArduinoOTA.onProgress([this](unsigned int progress, unsigned int total) { + static unsigned int _progOld; + unsigned int _prog = (progress / (total / 100)); + if (_prog != _progOld) { + myDebug_P(PSTR("[OTA] Progress: %u%%"), _prog); + _progOld = _prog; + } + }); + + ArduinoOTA.onError([this](ota_error_t error) { + if (error == OTA_AUTH_ERROR) + myDebug_P(PSTR("[OTA] Auth Failed")); + else if (error == OTA_BEGIN_ERROR) + myDebug_P(PSTR("[OTA] Begin Failed")); + else if (error == OTA_CONNECT_ERROR) + myDebug_P(PSTR("[OTA] Connect Failed")); + else if (error == OTA_RECEIVE_ERROR) + myDebug_P(PSTR("[OTA] Receive Failed")); + else if (error == OTA_END_ERROR) + myDebug_P(PSTR("[OTA] End Failed")); + +#ifdef CRASH + // There's been an error, reenable eeprom rotation + EEPROMr.rotate(true); +#endif + }); +} + +// eeprom +void MyESP::_eeprom_setup() { +#ifdef CRASH + EEPROMr.size(4); + EEPROMr.begin(SPI_FLASH_SEC_SIZE); +#endif +} + +// Set callback of sketch function to process project messages +void MyESP::setTelnet(telnetcommand_callback_f callback_cmd, telnet_callback_f callback) { + _telnetcommand_callback_f = callback_cmd; // external function to handle commands + _telnet_callback_f = callback; +} + +void MyESP::_telnetConnected() { + myDebug_P(PSTR("[TELNET] Telnet connection established")); + _consoleShowHelp(); // Show the initial message + +#ifdef CRASH + // show crash dump if just restarted after a fatal crash + uint32_t crash_time; + EEPROMr.get(SAVE_CRASH_EEPROM_OFFSET + SAVE_CRASH_CRASH_TIME, crash_time); + if ((crash_time != 0) && (crash_time != 0xFFFFFFFF)) { + myDebug_P(PSTR("[SYSTEM] There is stack data available from the last system crash. Use 'crash dump' to view and 'crash clear' to reset")); + } +#endif + + // call callback + if (_telnet_callback_f) { + (_telnet_callback_f)(TELNET_EVENT_CONNECT); + } +} + +void MyESP::_telnetDisconnected() { + myDebug_P(PSTR("[TELNET] Telnet connection closed")); + if (_telnet_callback_f) { + (_telnet_callback_f)(TELNET_EVENT_DISCONNECT); // call callback + } +} + +// Initialize the telnet server +void MyESP::_telnet_setup() { + SerialAndTelnet.setWelcomeMsg(""); + SerialAndTelnet.setCallbackOnConnect([this]() { _telnetConnected(); }); + SerialAndTelnet.setCallbackOnDisconnect([this]() { _telnetDisconnected(); }); + SerialAndTelnet.setDebugOutput(false); + SerialAndTelnet.setPingTime(0); // default is 1500ms (1.5 seconds) + SerialAndTelnet.begin(TELNET_SERIAL_BAUD); // default baud is 115200 + + // init command buffer for console commands + memset(_command, 0, TELNET_MAX_COMMAND_LENGTH); +} + +// Show help of commands +void MyESP::_consoleShowHelp() { + myDebug_P(PSTR("")); + myDebug_P(PSTR("* Connected to: %s version %s"), _app_name, _app_version); + + if (isAPmode()) { + myDebug_P(PSTR("* Device is in AP mode with SSID %s"), jw.getAPSSID().c_str()); + } else { + myDebug_P(PSTR("* Hostname: %s (%s)"), _getESPhostname().c_str(), WiFi.localIP().toString().c_str()); + myDebug_P(PSTR("* WiFi SSID: %s (signal %d%%)"), WiFi.SSID().c_str(), getWifiQuality()); + if (isMQTTConnected()) { + myDebug_P(PSTR("* MQTT connected (heartbeat %s)"), getHeartbeat() ? "enabled" : "disabled"); + } else { + myDebug_P(PSTR("* MQTT disconnected")); + } + } + + myDebug_P(PSTR("*")); + myDebug_P(PSTR("* Commands:")); + myDebug_P(PSTR("* ?=help, CTRL-D/quit=exit telnet session")); + myDebug_P(PSTR("* set, system, restart, mqttlog")); +#ifdef CRASH + myDebug_P(PSTR("* crash ")); +#endif + + // call callback function + if (_telnet_callback_f) { + (_telnet_callback_f)(TELNET_EVENT_SHOWCMD); + } + + myDebug_P(PSTR("")); // newline +} + +// see if a char * string is empty. It could not be initialized yet. +// return true if there is a value +bool MyESP::_hasValue(char * s) { + if (s == nullptr) { + return false; + } + return (s[0] != '\0'); +} + +// print all set commands and current values +void MyESP::_printSetCommands() { + myDebug_P(PSTR("\nThe following set commands are available:\n")); + myDebug_P(PSTR(" set erase")); + myDebug_P(PSTR(" set ")); + 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_base [value]")); + myDebug_P(PSTR(" set mqtt_port [value]")); + myDebug_P(PSTR(" set ntp_enabled ")); + myDebug_P(PSTR(" set serial ")); + + // call callback function + if (_telnet_callback_f) { + (_telnet_callback_f)(TELNET_EVENT_SHOWSET); + } + + myDebug_P(PSTR("\nStored settings:\n")); + + if (_network_wmode == 0) { + myDebug_P(PSTR(" wifi_mode=client")); + } else { + myDebug_P(PSTR(" wifi_mode=ap")); + } + + if (_hasValue(_network_ssid)) { + myDebug_P(PSTR(" wifi_ssid=%s"), _network_ssid); + } else { + myDebug_P(PSTR(" wifi_ssid=")); + } + SerialAndTelnet.print(FPSTR(" wifi_password=")); + if (_hasValue(_network_password)) { + for (uint8_t i = 0; i < strlen(_network_password); i++) { + SerialAndTelnet.print(FPSTR("*")); + } + } + myDebug_P(PSTR("")); + myDebug_P(PSTR(" mqtt_enabled=%s"), (_mqtt_enabled) ? "on" : "off"); + if (_hasValue(_mqtt_ip)) { + myDebug_P(PSTR(" mqtt_ip=%s"), _mqtt_ip); + } else { + myDebug_P(PSTR(" mqtt_ip=")); + } + if (_hasValue(_mqtt_user)) { + myDebug_P(PSTR(" mqtt_username=%s"), _mqtt_user); + } else { + myDebug_P(PSTR(" mqtt_username=")); + } + SerialAndTelnet.print(FPSTR(" mqtt_password=")); + if (_hasValue(_mqtt_password)) { + for (uint8_t i = 0; i < strlen(_mqtt_password); i++) { + SerialAndTelnet.print(FPSTR("*")); + } + } + myDebug_P(PSTR("")); + if (_hasValue(_mqtt_base)) { + myDebug_P(PSTR(" mqtt_base=%s"), _mqtt_base); + } else { + myDebug_P(PSTR(" mqtt_base=")); + } + myDebug_P(PSTR(" mqtt_port=%d"), _mqtt_port); + myDebug_P(PSTR(" mqtt_heartbeat=%s"), (_mqtt_heartbeat) ? "on" : "off"); + + myDebug_P(PSTR(" serial=%s"), (_general_serial) ? "on" : "off"); + myDebug_P(PSTR(" ntp_enabled=%s"), (_ntp_enabled) ? "on" : "off"); + + // print any custom settings + if (_fs_setlist_callback_f) { + (_fs_setlist_callback_f)(MYESP_FSACTION_LIST, 0, nullptr, nullptr); + } + + myDebug_P(PSTR("")); // newline +} + +// reset / restart +void MyESP::resetESP() { + myDebug_P(PSTR("* Restart ESP...")); + _deferredReset(500, CUSTOM_RESET_TERMINAL); + end(); +#if defined(ARDUINO_ARCH_ESP32) + ESP.restart(); +#else + ESP.restart(); +#endif +} + +// read next word from string buffer +// if parameter true then a word is only terminated by a newline +char * MyESP::_telnet_readWord(bool allow_all_chars) { + if (allow_all_chars) { + return (strtok(nullptr, "\n")); // allow only newline + } else { + return (strtok(nullptr, ", \n")); // allow space and comma + } +} + +// change settings - always as strings +// messy code but effective since we don't have too many settings +// wc is word count, number of parameters after the 'set' command +bool MyESP::_changeSetting(uint8_t wc, const char * setting, const char * value) { + bool save_config = false; + bool save_custom_config = false; + + // check for our internal commands first + if (strcmp(setting, "erase") == 0) { + _fs_eraseConfig(); + return true; + + } else if (strcmp(setting, "wifi_ssid") == 0) { + if (value) { + free(_network_ssid); + _network_ssid = strdup(value); + } + save_config = true; + //jw.enableSTA(false); + myDebug_P(PSTR("Note: please 'restart' to apply new WiFi settings")); + } else if (strcmp(setting, "wifi_password") == 0) { + if (value) { + free(_network_password); + _network_password = strdup(value); + } + save_config = true; + //jw.enableSTA(false); + myDebug_P(PSTR("Note: please 'restart' to apply new WiFi settings")); + + } else if (strcmp(setting, "wifi_mode") == 0) { + if (value) { + if (strcmp(value, "ap") == 0) { + _network_wmode = 1; + save_config = true; + myDebug_P(PSTR("Note: please 'restart' to apply new WiFi settings")); + } else if (strcmp(value, "client") == 0) { + _network_wmode = 0; + save_config = true; + myDebug_P(PSTR("Note: please 'restart' to apply new WiFi settings")); + } else { + save_config = false; + } + } + + } else if (strcmp(setting, "mqtt_ip") == 0) { + if (value) { + free(_mqtt_ip); + _mqtt_ip = strdup(value); + } + save_config = true; + } else if (strcmp(setting, "mqtt_username") == 0) { + if (value) { + free(_mqtt_user); + _mqtt_user = strdup(value); + } + save_config = true; + } else if (strcmp(setting, "mqtt_password") == 0) { + if (value) { + free(_mqtt_password); + _mqtt_password = strdup(value); + } + save_config = true; + } else if (strcmp(setting, "mqtt_base") == 0) { + if (value) { + free(_mqtt_base); + _mqtt_base = strdup(value); + } + save_config = true; + } else if (strcmp(setting, "mqtt_port") == 0) { + if (value) { + _mqtt_port = atoi(value); + } + save_config = true; + } else if (strcmp(setting, "mqtt_enabled") == 0) { + save_config = true; + if (value) { + if (strcmp(value, "on") == 0) { + _mqtt_enabled = true; + save_config = true; + } else if (strcmp(value, "off") == 0) { + _mqtt_enabled = false; + save_config = true; + } else { + save_config = false; + } + } + + } else if (strcmp(setting, "serial") == 0) { + save_config = true; + if (value) { + if (strcmp(value, "on") == 0) { + _general_serial = true; + save_config = true; + myDebug_P(PSTR("Do a 'restart' to activate Serial mode.")); + } else if (strcmp(value, "off") == 0) { + _general_serial = false; + save_config = true; + myDebug_P(PSTR("Do a 'restart' to deactivate Serial mode.")); + } else { + save_config = false; + } + } + + } else if (strcmp(setting, "mqtt_heartbeat") == 0) { + save_config = true; + if (value) { + if (strcmp(value, "on") == 0) { + _mqtt_heartbeat = true; + save_config = true; + myDebug_P(PSTR("Heartbeat on")); + } else if (strcmp(value, "off") == 0) { + _mqtt_heartbeat = false; + save_config = true; + myDebug_P(PSTR("Heartbeat off")); + } else { + save_config = false; + } + } + } else if (strcmp(setting, "ntp_enabled") == 0) { + save_config = true; + if (value) { + if (strcmp(value, "on") == 0) { + _ntp_enabled = true; + save_config = true; + myDebug_P(PSTR("NTP on")); + } else if (strcmp(value, "off") == 0) { + _ntp_enabled = false; + save_config = true; + myDebug_P(PSTR("NTP off")); + } else { + save_config = false; + } + } + } else { + // finally check for any custom commands + if (_fs_setlist_callback_f) { + save_custom_config = (_fs_setlist_callback_f)(MYESP_FSACTION_SET, wc, setting, value); + } + } + + bool ok = false; + + // if we were able to recognize the set command, continue + if ((save_config || save_custom_config)) { + // check for 2 params + if (value == nullptr) { + myDebug_P(PSTR("%s setting reset to its default value."), setting); + } else { + // must be 3 params + myDebug_P(PSTR("%s changed."), setting); + } + } + + // now do the saving for system config if something has changed + if (save_config) { + ok = _fs_writeConfig(); + } + + // and see if we need to also save for custom config + if (save_custom_config) { + ok = _fs_createCustomConfig(); + } + + return ok; +} + +// force the serial on/off +void MyESP::setUseSerial(bool b) { + _general_serial = b; + SerialAndTelnet.setSerial(b ? &Serial : nullptr); +} + +void MyESP::_telnetCommand(char * commandLine) { + char * str = commandLine; + bool state = false; + + if (strlen(commandLine) == 0) + return; + + // count the number of arguments + unsigned wc = 0; + while (*str) { + if (*str == ' ' || *str == '\n' || *str == '\t') { + state = false; + } else if (state == false) { + state = true; + ++wc; + } + ++str; + } + + // check first for reserved commands + char * temp = strdup(commandLine); // because strotok kills original string buffer + char * ptrToCommandName = strtok((char *)temp, " \n"); // space and newline + + // set command + if (strcmp(ptrToCommandName, "set") == 0) { + bool ok = false; + if (wc == 1) { + _printSetCommands(); + ok = true; + } else if (wc == 2) { // set + char * setting = _telnet_readWord(false); + ok = _changeSetting(wc - 1, setting, nullptr); + } else { // set + char * setting = _telnet_readWord(false); + char * value = _telnet_readWord(true); // allow strange characters + ok = _changeSetting(wc - 1, setting, value); + } + + if (!ok) { + myDebug_P(PSTR("\nInvalid parameter for set command.")); + } + + return; + } + + // restart command + if ((strcmp(ptrToCommandName, "restart") == 0) && (wc == 1)) { + resetESP(); + return; + } + + // print mqtt log command + if ((strcmp(ptrToCommandName, "mqttlog") == 0) && (wc == 1)) { + _printMQTTLog(); + return; + } + + // show system stats + if ((strcmp(ptrToCommandName, "system") == 0) && (wc == 1)) { + showSystemStats(); + return; + } + + // show system stats + if ((strcmp(ptrToCommandName, "quit") == 0) && (wc == 1)) { + myDebug_P(PSTR("[TELNET] exiting telnet session")); + SerialAndTelnet.disconnectClient(); + return; + } + +#ifdef CRASH + // crash command + if ((strcmp(ptrToCommandName, "crash") == 0) && (wc >= 2)) { + char * cmd = _telnet_readWord(false); + if (strcmp(cmd, "dump") == 0) { + crashDump(); + } else if (strcmp(cmd, "clear") == 0) { + crashClear(); + } else if ((strcmp(cmd, "test") == 0) && (wc == 3)) { + char * value = _telnet_readWord(false); + crashTest(atoi(value)); + } else { + myDebug_P(PSTR("Error. Usage: crash ")); + } + return; // don't call custom command line callback + } +#endif + + // call callback function + if (_telnetcommand_callback_f) { + (_telnetcommand_callback_f)(wc, commandLine); + } +} + +// returns WiFi hostname as a String object +String MyESP::_getESPhostname() { + String hostname; + +#if defined(ARDUINO_ARCH_ESP32) + hostname = String(WiFi.getHostname()); +#else + hostname = WiFi.hostname(); +#endif + + return (hostname); +} + +// takes the time from the gcc during compilation +char * MyESP::_getBuildTime() { + const char time_now[] = __TIME__; // hh:mm:ss + uint8_t hour = atoi(&time_now[0]); + uint8_t minute = atoi(&time_now[3]); + uint8_t second = atoi(&time_now[6]); + + const char date_now[] = __DATE__; // Mmm dd yyyy + const char * months[] = {"Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"}; + uint8_t month = 0; + for (int i = 0; i < 12; i++) { + if (strncmp(date_now, months[i], 3) == 0) { + month = i + 1; + break; + } + } + uint8_t day = atoi(&date_now[3]); + uint16_t year = atoi(&date_now[7]); + + char buffer[30]; + snprintf_P(buffer, sizeof(buffer), PSTR("%04d-%02d-%02d %02d:%02d:%02d"), year, month, day, hour, minute, second); + + return (strdup(buffer)); +} + +// returns system uptime in seconds +unsigned long MyESP::_getUptime() { + static uint32_t last_uptime = 0; + static uint8_t uptime_overflows = 0; + + if (millis() < last_uptime) { + ++uptime_overflows; + } + last_uptime = millis(); + uint32_t uptime_seconds = uptime_overflows * (MYESP_UPTIME_OVERFLOW / 1000) + (last_uptime / 1000); + + return uptime_seconds; +} + +// init RTC mem +void MyESP::_rtcmemInit() { + memset((uint32_t *)RTCMEM_ADDR, 0, sizeof(uint32_t) * RTCMEM_BLOCKS); + Rtcmem->magic = RTCMEM_MAGIC; +} + +uint8_t MyESP::getSystemBootStatus() { + system_rtcmem_t data; + data.value = Rtcmem->sys; + return data.parts.boot_status; +} + +void MyESP::_setSystemBootStatus(uint8_t status) { + system_rtcmem_t data; + 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() { + system_rtcmem_t data; + data.value = Rtcmem->sys; + return data.parts.stability_counter; +} + +void MyESP::_setSystemStabilityCounter(uint8_t counter) { + system_rtcmem_t data; + data.value = Rtcmem->sys; + data.parts.stability_counter = counter; + Rtcmem->sys = data.value; +} + +uint8_t MyESP::_getSystemResetReason() { + system_rtcmem_t data; + data.value = Rtcmem->sys; + return data.parts.reset_reason; +} + +void MyESP::_setSystemResetReason(uint8_t reason) { + system_rtcmem_t data; + data.value = Rtcmem->sys; + data.parts.reset_reason = reason; + Rtcmem->sys = data.value; +} + +// system_get_rst_info() result is cached by the Core init for internal use +uint32_t MyESP::getSystemResetReason() { + return resetInfo.reason; +} + +void MyESP::_rtcmemSetup() { + _rtcmem_status = _rtcmemStatus(); + if (!_rtcmem_status) { + _rtcmemInit(); + } +} + +void MyESP::_setCustomResetReason(uint8_t reason) { + _setSystemResetReason(reason); +} + +// returns false if not set and needs to be intialized, causing all rtcmem data to be wiped +bool MyESP::_rtcmemStatus() { + bool readable; + + uint32_t reason = getSystemResetReason(); + + // the last reset could have been caused by manually pressing the reset button + // so before wiping, capture the boot sequence + if (reason == REASON_EXT_SYS_RST) { // external system reset + if (getSystemBootStatus() == MYESP_BOOTSTATUS_BOOTING) { + _setSystemBootStatus(MYESP_BOOTSTATUS_RESETNEEDED); + // _formatreq = true; // do a wipe next in the loop() - commented out for now because we use the web + } else { + _setSystemBootStatus(MYESP_BOOTSTATUS_POWERON); + } + } + + switch (reason) { + //case REASON_EXT_SYS_RST: // external system reset + case REASON_WDT_RST: // hardware watch dog reset + case REASON_DEFAULT_RST: // normal startup by power on + case REASON_SOFT_WDT_RST: // Software watchdog + readable = false; + break; + default: + readable = true; + } + + readable = readable and (RTCMEM_MAGIC == Rtcmem->magic); + + return readable; +} + +bool MyESP::_getRtcmemStatus() { + return _rtcmem_status; +} + +uint8_t MyESP::_getCustomResetReason() { + static uint8_t status = 255; + if (status == 255) { + if (_rtcmemStatus()) + status = _getSystemResetReason(); + if (status > 0) + _setCustomResetReason(0); + if (status > CUSTOM_RESET_MAX) + status = 0; + } + return status; +} + +void MyESP::_deferredReset(unsigned long delaytime, uint8_t reason) { + _setSystemBootStatus(MYESP_BOOTSTATUS_POWERON); + _setCustomResetReason(reason); + delay(delaytime); +} + +// Call this method on boot with stable=true to reset the crash counter +// Each call increments the counter +// If the counter reaches MYESP_SYSTEM_CHECK_MAX then the system is flagged as unstable +void MyESP::_setSystemCheck(bool stable) { + uint8_t value = 0; + + if (stable) { + value = 0; // system is ok + } else { + if (!_getRtcmemStatus()) { + _setSystemStabilityCounter(1); + return; + } + + value = _getSystemStabilityCounter(); + + if (++value > MYESP_SYSTEM_CHECK_MAX) { + _systemStable = false; + value = 0; // system is unstable + myDebug_P(PSTR("[SYSTEM] Warning, system UNSTABLE. Serial mode is enabled.")); + + // enable Serial again + if (!_general_serial) { + SerialAndTelnet.setSerial(&Serial); + _general_serial = true; + } + } + } + + _setSystemStabilityCounter(value); +} + +// return if system is stable (false=bad) +bool MyESP::_getSystemCheck() { + return _systemStable; +} + +// periodically check if system is stable +void MyESP::_systemCheckLoop() { + static bool checked = false; + if (!checked && (millis() > MYESP_SYSTEM_CHECK_TIME)) { + _setSystemCheck(true); // Flag system as stable + checked = true; + } +} + +// print out ESP system stats +// for battery power is ESP.getVcc() +void MyESP::showSystemStats() { +#if defined(ESP8266) + myDebug_P(PSTR("%sESP8266 System stats:%s"), COLOR_BOLD_ON, COLOR_BOLD_OFF); +#else + myDebug_P(PSTR("ESP32 System stats:")); +#endif + myDebug_P(PSTR("")); + + myDebug_P(PSTR(" [APP] %s version: %s"), _app_name, _app_version); + myDebug_P(PSTR(" [APP] MyESP version: %s"), MYESP_VERSION); + myDebug_P(PSTR(" [APP] Build timestamp: %s"), _buildTime); + + // uptime + uint32_t t = _getUptime(); // seconds + + uint32_t d = t / 86400L; + uint32_t h = ((t % 86400L) / 3600L) % 60; + uint32_t rem = t % 3600L; + uint8_t m = rem / 60; + uint8_t s = rem % 60; + myDebug_P(PSTR(" [APP] Uptime: %d day%s %d hour%s %d minute%s %d second%s"), + d, + (d == 1) ? "" : "s", + h, + (h == 1) ? "" : "s", + m, + (m == 1) ? "" : "s", + s, + (s == 1) ? "" : "s"); + + myDebug_P(PSTR(" [APP] System Load: %d%%"), getSystemLoadAverage()); + + if (!_getSystemCheck()) { + myDebug_P(PSTR(" [SYSTEM] Device is in SAFE MODE")); + } + + if (isAPmode()) { + myDebug_P(PSTR(" [WIFI] Device is in AP mode with SSID %s"), jw.getAPSSID().c_str()); + } else { + myDebug_P(PSTR(" [WIFI] WiFi Hostname: %s"), _getESPhostname().c_str()); + myDebug_P(PSTR(" [WIFI] WiFi IP: %s"), WiFi.localIP().toString().c_str()); + myDebug_P(PSTR(" [WIFI] WiFi signal strength: %d%%"), getWifiQuality()); + } + + myDebug_P(PSTR(" [WIFI] WiFi MAC: %s"), WiFi.macAddress().c_str()); + + if (isMQTTConnected()) { + myDebug_P(PSTR(" [MQTT] is connected (heartbeat %s)"), getHeartbeat() ? "enabled" : "disabled"); + } else { + myDebug_P(PSTR(" [MQTT] is disconnected")); + } + + if (_ntp_enabled) { + myDebug_P(PSTR(" [NTP] Time in UTC is %02d:%02d:%02d"), hour(now()), minute(now()), second(now())); + } + +#ifdef CRASH + char output_str[80] = {0}; + char buffer[16] = {0}; + myDebug_P(PSTR(" [EEPROM] EEPROM size: %u"), EEPROMr.reserved() * SPI_FLASH_SEC_SIZE); + strlcpy(output_str, " [EEPROM] EEPROM Sector pool size is ", sizeof(output_str)); + strlcat(output_str, itoa(EEPROMr.size(), buffer, 10), sizeof(output_str)); + strlcat(output_str, ", and in use are: ", sizeof(output_str)); + for (uint32_t i = 0; i < EEPROMr.size(); i++) { + strlcat(output_str, itoa(EEPROMr.base() - i, buffer, 10), sizeof(output_str)); + strlcat(output_str, " ", sizeof(output_str)); + } + myDebug(output_str); +#endif + + myDebug_P(PSTR(" [SYSTEM] System is %s"), _getSystemCheck() ? "Stable" : "Unstable!"); + +#ifdef ARDUINO_BOARD + myDebug_P(PSTR(" [SYSTEM] Board: %s"), ARDUINO_BOARD); +#endif + + myDebug_P(PSTR(" [SYSTEM] CPU frequency: %u MHz"), ESP.getCpuFreqMHz()); + myDebug_P(PSTR(" [SYSTEM] SDK version: %s"), ESP.getSdkVersion()); + +#if defined(ESP8266) + myDebug_P(PSTR(" [SYSTEM] CPU chip ID: 0x%06X"), ESP.getChipId()); + myDebug_P(PSTR(" [SYSTEM] Core version: %s"), ESP.getCoreVersion().c_str()); + myDebug_P(PSTR(" [SYSTEM] Boot version: %d"), ESP.getBootVersion()); + myDebug_P(PSTR(" [SYSTEM] Boot mode: %d"), ESP.getBootMode()); + unsigned char reason = _getCustomResetReason(); + if (reason > 0) { + char buffer[32]; + strcpy_P(buffer, custom_reset_string[reason - 1]); + myDebug_P(PSTR(" [SYSTEM] Last reset reason: %s"), buffer); + } else { + myDebug_P(PSTR(" [SYSTEM] Last reset reason: %s"), (char *)ESP.getResetReason().c_str()); + myDebug_P(PSTR(" [SYSTEM] Last reset info: %s"), (char *)ESP.getResetInfo().c_str()); + } + myDebug_P(PSTR(" [SYSTEM] Restart count: %d"), _getSystemStabilityCounter()); + + myDebug_P(PSTR(" [SYSTEM] rtcmem status: blocks:%u addr:0x%p"), RtcmemSize, Rtcmem); + for (uint8_t block = 0; block < RtcmemSize; ++block) { + myDebug_P(PSTR(" [SYSTEM] rtcmem %02u: %u"), block, reinterpret_cast(RTCMEM_ADDR)[block]); + } +#endif + + FlashMode_t mode = ESP.getFlashChipMode(); +#if defined(ESP8266) + myDebug_P(PSTR(" [FLASH] Flash chip ID: 0x%06X"), ESP.getFlashChipId()); +#endif + myDebug_P(PSTR(" [FLASH] Flash speed: %u Hz"), ESP.getFlashChipSpeed()); + myDebug_P(PSTR(" [FLASH] Flash mode: %s"), mode == FM_QIO ? "QIO" : mode == FM_QOUT ? "QOUT" : mode == FM_DIO ? "DIO" : mode == FM_DOUT ? "DOUT" : "UNKNOWN"); +#if defined(ESP8266) + myDebug_P(PSTR(" [FLASH] Flash size (CHIP): %d"), ESP.getFlashChipRealSize()); +#endif + myDebug_P(PSTR(" [FLASH] Flash size (SDK): %d"), ESP.getFlashChipSize()); + myDebug_P(PSTR(" [FLASH] Flash Reserved: %d"), 1 * SPI_FLASH_SEC_SIZE); + 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); + + myDebug_P(PSTR("")); +} + +/* + * Send heartbeat via MQTT with all system data + */ +void MyESP::_heartbeatCheck(bool force = false) { + static uint32_t last_heartbeat = 0; + + if ((millis() - last_heartbeat > MYESP_HEARTBEAT_INTERVAL) || force) { + last_heartbeat = millis(); + + // _printHeap("Heartbeat"); // for heartbeat debugging + + if (!isMQTTConnected() || !(_mqtt_heartbeat)) { + return; + } + + uint32_t total_memory = _getInitialFreeHeap(); + uint32_t free_memory = ESP.getFreeHeap(); + uint8_t mem_available = 100 * free_memory / total_memory; // as a % + + char payload[300] = {0}; + char s[10]; + strlcpy(payload, "version=", sizeof(payload)); + strlcat(payload, _app_version, sizeof(payload)); // version + strlcat(payload, ", IP=", sizeof(payload)); + strlcat(payload, WiFi.localIP().toString().c_str(), sizeof(payload)); // IP address + strlcat(payload, ", rssid=", sizeof(payload)); + strlcat(payload, itoa(getWifiQuality(), s, 10), sizeof(payload)); // rssi % + strlcat(payload, "%, load=", sizeof(payload)); + strlcat(payload, ltoa(getSystemLoadAverage(), s, 10), sizeof(payload)); // load + strlcat(payload, "%, uptime=", sizeof(payload)); + strlcat(payload, ltoa(_getUptime(), s, 10), sizeof(payload)); // uptime in secs + strlcat(payload, "secs, freemem=", sizeof(payload)); + strlcat(payload, itoa(mem_available, s, 10), sizeof(payload)); // free mem as a % + strlcat(payload, "%", sizeof(payload)); + + mqttPublish(MQTT_TOPIC_HEARTBEAT, payload); // send to MQTT + } +} + +// 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(); + + if (c == 0) + return; + + SerialAndTelnet.serialPrint(c); // echo to Serial (if connected) + + switch (c) { + case '\r': // likely have full command in buffer now, commands are terminated by CR and/or LF + case '\n': + _command[charsRead] = '\0'; // null terminate our command char array + + if (charsRead > 0) { + charsRead = 0; // is static, so have to reset + _suspendOutput = false; + if (_general_serial) { + SerialAndTelnet.serialPrint('\n'); // force newline if in Serial + } + _telnetCommand(_command); + } + break; + case '\b': // (^H) + case 0x7F: // (^?) + if (charsRead > 0) { + _command[--charsRead] = '\0'; + + SerialAndTelnet.write(' '); + SerialAndTelnet.write('\b'); + } + break; + case '?': + if (!_suspendOutput) { + _consoleShowHelp(); + } else { + _command[charsRead++] = c; // add it to buffer as its part of the string entered + } + break; + case 0x04: // EOT, CTRL-D + myDebug_P(PSTR("[TELNET] exiting telnet session")); + SerialAndTelnet.disconnectClient(); + break; + default: + _suspendOutput = true; + if (charsRead < TELNET_MAX_COMMAND_LENGTH) { + _command[charsRead++] = c; + } + _command[charsRead] = '\0'; // just in case + break; + } + } +} + +// make sure we have a connection to MQTT broker and the MQTT IP is set +void MyESP::_mqttConnect() { + if ((!_mqtt_enabled) || (!_hasValue(_mqtt_ip))) { + return; // MQTT not enabled + } + + // Do not connect if already connected or still trying to connect + if (mqttClient.connected() || _mqtt_connecting || (WiFi.status() != WL_CONNECTED)) { + return; + } + + // Check reconnect interval + if (millis() - _mqtt_last_connection < _mqtt_reconnect_delay) { + return; + } + + _mqtt_connecting = true; // we're doing a connection + + // Increase the reconnect delay + _mqtt_reconnect_delay += MQTT_RECONNECT_DELAY_STEP; + if (_mqtt_reconnect_delay > MQTT_RECONNECT_DELAY_MAX) { + _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 + mqttClient.connect(); +} + +// Setup everything we need +void MyESP::setWIFI(wifi_callback_f callback) { + // callback + _wifi_callback_f = callback; +} + +// init MQTT settings +void MyESP::setMQTT(mqtt_callback_f callback) { + _mqtt_callback_f = callback; // callback +} + +// builds up a topic by prefixing the base and hostname +char * MyESP::_mqttTopic(const char * topic) { + static char buffer[MQTT_MAX_TOPIC_SIZE] = {0}; + + if (_hasValue(_mqtt_base)) { + strlcpy(buffer, _mqtt_base, sizeof(buffer)); + strlcat(buffer, "/", sizeof(buffer)); + strlcat(buffer, _general_hostname, sizeof(buffer)); + } else { + strlcpy(buffer, _general_hostname, sizeof(buffer)); + } + + strlcat(buffer, "/", sizeof(buffer)); + strlcat(buffer, topic, sizeof(buffer)); + + return buffer; +} + +// print contents of file +// assumes Serial is open +void MyESP::_fs_printFile(const char * file) { + File configFile = SPIFFS.open(file, "r"); + if (!configFile) { + myDebug_P(PSTR("[FS] Failed to read file %s for printing"), file); + return; + } + + myDebug_P(PSTR("[FS] File: %s, Size: %d"), file, configFile.size()); + + while (configFile.available()) { + SerialAndTelnet.print((char)configFile.read()); + } + + myDebug_P(PSTR("[FS] end")); // newline + + configFile.close(); +} + +// format File System +void MyESP::_fs_eraseConfig() { + myDebug_P(PSTR("[FS] Doing a factory reset.")); + _formatreq = true; +} + +// custom callback for web info +void MyESP::setWeb(web_callback_f callback_web) { + _web_callback_f = callback_web; +} + +void MyESP::setSettings(fs_loadsave_callback_f loadsave, fs_setlist_callback_f setlist, bool useSerial) { + _fs_loadsave_callback_f = loadsave; + _fs_setlist_callback_f = setlist; + _general_serial = useSerial; +} + +// 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")); + } + + File configFile = SPIFFS.open(MYESP_CONFIG_FILE, "r"); + if (!configFile) { + configFile.close(); + myDebug_P(PSTR("[FS] No system config found")); + return false; + } + + // check size + size_t size = configFile.size(); + if (size > MYESP_SPIFFS_MAXSIZE) { + configFile.close(); + myDebug_P(PSTR("[FS] System config size is too large")); + return false; + } else if (size == 0) { + configFile.close(); + myDebug_P(PSTR("[FS] Corrupted system config")); + return false; + } + + // read file from SPIFFS into a char array + char json[MYESP_SPIFFS_MAXSIZE] = {0}; + if (configFile.readBytes(json, size) != size) { + configFile.close(); + myDebug_P(PSTR("[FS] Error, file sizes don't match with system config")); + return false; + } + configFile.close(); + + StaticJsonDocument doc; + + DeserializationError error = deserializeJson(doc, json); // Deserialize the JSON document + if (error) { + myDebug_P(PSTR("[FS] Failed to deserialize json, error %s"), error.c_str()); + configFile.close(); + return false; + } + + JsonObject network = doc["network"]; + _network_ssid = strdup(network["ssid"] | ""); + _network_password = strdup(network["password"] | ""); + _network_wmode = network["wmode"]; // 0 is client, 1 is AP + + JsonObject general = doc["general"]; + + _general_password = strdup(general["password"] | MYESP_HTTP_PASSWORD); + _ws->setAuthentication("admin", _general_password); + _general_hostname = strdup(general["hostname"]); + + // serial is only on when booting +#ifdef FORCE_SERIAL + myDebug_P(PSTR("[FS] Serial is forced")); + _general_serial = true; +#else + _general_serial = general["serial"]; +#endif + + JsonObject mqtt = doc["mqtt"]; + _mqtt_enabled = mqtt["enabled"]; + _mqtt_heartbeat = mqtt["heartbeat"]; + _mqtt_ip = strdup(mqtt["ip"] | ""); + _mqtt_user = strdup(mqtt["user"] | ""); + _mqtt_port = mqtt["port"] | MQTT_PORT; + _mqtt_password = strdup(mqtt["password"] | ""); + _mqtt_base = strdup(mqtt["base"] | MQTT_BASE_DEFAULT); + + JsonObject ntp = doc["ntp"]; + _ntp_server = strdup(ntp["server"] | ""); + _ntp_interval = ntp["interval"] | 60; + if (_ntp_interval == 0) + _ntp_interval = 60; + _ntp_enabled = ntp["enabled"]; + + myDebug_P(PSTR("[FS] System settings loaded")); + // serializeJsonPretty(doc, Serial); // turn on for debugging + + return true; +} + +// load custom settings +bool MyESP::_fs_loadCustomConfig() { + File configFile = SPIFFS.open(MYESP_CUSTOMCONFIG_FILE, "r"); + if (!configFile) { + myDebug_P(PSTR("[FS] No custom config found")); + return false; + } + + // check size + size_t size = configFile.size(); + if (size > MYESP_SPIFFS_MAXSIZE) { + configFile.close(); + myDebug_P(PSTR("[FS] Custom config size is too large")); + return false; + } else if (size == 0) { + configFile.close(); + myDebug_P(PSTR("[FS] Corrupted custom config")); + return false; + } + + // read file from SPIFFS into a char array + char data[MYESP_SPIFFS_MAXSIZE] = {0}; + if (configFile.readBytes(data, size) != size) { + myDebug_P(PSTR("[FS] File sizes don't match with custom config")); + configFile.close(); + return false; + } + configFile.close(); + + // create the JSON doc and pass it back to the callback function + StaticJsonDocument doc; + JsonObject json = doc.to(); // create empty object + + DeserializationError error = deserializeJson(doc, data); // Deserialize the JSON document + + if (error) { + myDebug_P(PSTR("[FS] Failed to deserialize json for custom config, error %s"), error.c_str()); + configFile.close(); + return false; + } + + if (_fs_loadsave_callback_f) { + if (!(_fs_loadsave_callback_f)(MYESP_FSACTION_LOAD, json)) { + myDebug_P(PSTR("[FS] Error reading custom config")); + return false; + } else { + myDebug_P(PSTR("[FS] Custom config loaded")); + //serializeJsonPretty(doc, Serial); // added for debugging + } + } + + return true; +} + +// save custom config to spiffs +bool MyESP::fs_saveCustomConfig(JsonObject root) { + bool ok = false; + + // open for writing + File configFile = SPIFFS.open(MYESP_CUSTOMCONFIG_FILE, "w"); + if (!configFile) { + myDebug_P(PSTR("[FS] Failed to open custom config for writing")); + return false; + } + + // call any custom functions before handling SPIFFS + if (_ota_pre_callback_f) { + (_ota_pre_callback_f)(); + } + + // Serialize JSON to file + size_t n = serializeJson(root, configFile); + configFile.close(); + + if (n) { + // reload the settings + if (_fs_loadsave_callback_f) { + if (!(_fs_loadsave_callback_f)(MYESP_FSACTION_LOAD, root)) { + myDebug_P(PSTR("[FS] Error parsing custom config json")); + } + } + + _writeEvent("INFO", "system", "Custom config stored in the SPIFFS", ""); + myDebug_P(PSTR("[FS] custom config saved")); + ok = true; + } + + if (_ota_pre_callback_f) { + (_ota_pre_callback_f)(); + } + + return ok; +} + +// save system config to spiffs +bool MyESP::fs_saveConfig(JsonObject root) { + 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")); + return false; + } + + // call any custom functions before handling SPIFFS + if (_ota_pre_callback_f) { + (_ota_pre_callback_f)(); + } + + // Serialize JSON to file + size_t n = serializeJson(root, configFile); + configFile.close(); + + if (n) { + _writeEvent("INFO", "system", "System config stored in the SPIFFS", ""); + myDebug_P(PSTR("[FS] system config saved")); + ok = true; + } + + // serializeJsonPretty(root, Serial); // for debugging + + if (_ota_pre_callback_f) { + (_ota_pre_callback_f)(); + } + + return ok; +} + +// create an initial system config file using default settings +bool MyESP::_fs_writeConfig() { + StaticJsonDocument doc; + JsonObject root = doc.to(); + + root["command"] = "configfile"; // header, important! + + JsonObject network = doc.createNestedObject("network"); + network["ssid"] = _network_ssid; + network["password"] = _network_password; + network["wmode"] = _network_wmode; + + JsonObject general = doc.createNestedObject("general"); + general["password"] = _general_password; + general["serial"] = _general_serial; + general["hostname"] = _general_hostname; + + JsonObject mqtt = doc.createNestedObject("mqtt"); + mqtt["enabled"] = _mqtt_enabled; + mqtt["heartbeat"] = _mqtt_heartbeat; + mqtt["ip"] = _mqtt_ip; + mqtt["user"] = _mqtt_user; + mqtt["port"] = _mqtt_port; + 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; + + bool ok = fs_saveConfig(root); // save it + + return ok; +} + +// create an empty json doc for the custom config and call callback to populate it +bool MyESP::_fs_createCustomConfig() { + StaticJsonDocument doc; + JsonObject json = doc.to(); + + json["command"] = "custom_configfile"; // header, important! + + if (_fs_loadsave_callback_f) { + if (!(_fs_loadsave_callback_f)(MYESP_FSACTION_SAVE, json)) { + myDebug_P(PSTR("[FS] Error building custom config json")); + } + } else { + myDebug_P(PSTR("[FS] Created custom config")); + } + + bool ok = fs_saveCustomConfig(json); + + return ok; +} + + +// init the SPIFF file system and load the config +// if it doesn't exist try and create it +void MyESP::_fs_setup() { + if (!SPIFFS.begin()) { + myDebug_P(PSTR("[FS] Formatting filesystem...")); + if (SPIFFS.format()) { + _writeEvent("WARN", "system", "File system formatted", ""); + } else { + myDebug_P(PSTR("[FS] Failed to format file system")); + } + } + + // 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 + } + + // load system and custom config + if (!_fs_loadCustomConfig()) { + _fs_createCustomConfig(); // create the initial config file + } +} + +uint32_t MyESP::getSystemLoadAverage() { + return _load_average; +} + +// calculate load average +void MyESP::_calculateLoad() { + static uint32_t last_loadcheck = 0; + static uint32_t load_counter_temp = 0; + load_counter_temp++; + + if (millis() - last_loadcheck > MYESP_LOADAVG_INTERVAL) { + static uint32_t load_counter = 0; + static uint32_t load_counter_max = 1; + + load_counter = load_counter_temp; + load_counter_temp = 0; + if (load_counter > load_counter_max) { + load_counter_max = load_counter; + } + _load_average = 100 - (100 * load_counter / load_counter_max); + last_loadcheck = millis(); + } +} + +// returns true is MQTT is alive +bool MyESP::isMQTTConnected() { + return mqttClient.connected(); +} + +// return true if wifi is connected (client or AP mode) +bool MyESP::isWifiConnected() { + return (_wifi_connected); +} + +/* + Return the quality (Received Signal Strength Indicator) + of the WiFi network. + Returns a number between 0 and 100 if WiFi is connected. + Returns -1 if WiFi is disconnected. + + High quality: 90% ~= -55dBm + Medium quality: 50% ~= -75dBm + Low quality: 30% ~= -85dBm + Unusable quality: 8% ~= -96dBm +*/ +int MyESP::getWifiQuality() { + if (WiFi.status() != WL_CONNECTED) + return -1; + int dBm = WiFi.RSSI(); + if (dBm <= -100) + return 0; + if (dBm >= -50) + return 100; + return 2 * (dBm + 100); +} + +#ifdef CRASH +/** + * Save crash information in EEPROM + * This function is called automatically if ESP8266 suffers an exception + * It should be kept quick / consise to be able to execute before hardware wdt may kick in + */ +extern "C" void custom_crash_callback(struct rst_info * rst_info, uint32_t stack_start, uint32_t stack_end) { + // write crash time to EEPROM + uint32_t crash_time = millis(); + EEPROMr.put(SAVE_CRASH_EEPROM_OFFSET + SAVE_CRASH_CRASH_TIME, crash_time); + + // write reset info to EEPROM + EEPROMr.write(SAVE_CRASH_EEPROM_OFFSET + SAVE_CRASH_RESTART_REASON, rst_info->reason); + EEPROMr.write(SAVE_CRASH_EEPROM_OFFSET + SAVE_CRASH_EXCEPTION_CAUSE, rst_info->exccause); + + // write epc1, epc2, epc3, excvaddr and depc to EEPROM + EEPROMr.put(SAVE_CRASH_EEPROM_OFFSET + SAVE_CRASH_EPC1, rst_info->epc1); + EEPROMr.put(SAVE_CRASH_EEPROM_OFFSET + SAVE_CRASH_EPC2, rst_info->epc2); + EEPROMr.put(SAVE_CRASH_EEPROM_OFFSET + SAVE_CRASH_EPC3, rst_info->epc3); + EEPROMr.put(SAVE_CRASH_EEPROM_OFFSET + SAVE_CRASH_EXCVADDR, rst_info->excvaddr); + EEPROMr.put(SAVE_CRASH_EEPROM_OFFSET + SAVE_CRASH_DEPC, rst_info->depc); + + // write stack start and end address to EEPROM + EEPROMr.put(SAVE_CRASH_EEPROM_OFFSET + SAVE_CRASH_STACK_START, stack_start); + EEPROMr.put(SAVE_CRASH_EEPROM_OFFSET + SAVE_CRASH_STACK_END, stack_end); + + // write stack trace to EEPROM and avoid overwriting settings + int16_t current_address = SAVE_CRASH_EEPROM_OFFSET + SAVE_CRASH_STACK_TRACE; + for (uint32_t i = stack_start; i < stack_end; i++) { + byte * byteValue = (byte *)i; + EEPROMr.write(current_address++, *byteValue); + } + + EEPROMr.commit(); +} + +/** + * Clears crash info + */ +void MyESP::crashClear() { + myDebug_P(PSTR("[CRASH] Clearing crash dump")); + uint32_t crash_time = 0xFFFFFFFF; + EEPROMr.put(SAVE_CRASH_EEPROM_OFFSET + SAVE_CRASH_CRASH_TIME, crash_time); + EEPROMr.commit(); +} + +/** + * Print out crash information that has been previously saved in EEPROM + * Copied from https://github.com/krzychb/EspSaveCrash + */ +void MyESP::crashDump() { + uint32_t crash_time; + EEPROMr.get(SAVE_CRASH_EEPROM_OFFSET + SAVE_CRASH_CRASH_TIME, crash_time); + if ((crash_time == 0) || (crash_time == 0xFFFFFFFF)) { + myDebug_P(PSTR("[CRASH] No crash data captured.")); + return; + } + + uint32_t t = crash_time / 1000; // convert to seconds + uint32_t d = t / 86400L; + uint32_t h = (t / 3600L) % 60; + uint32_t rem = t % 3600L; + uint8_t m = rem / 60; + uint8_t s = rem % 60; + myDebug_P(PSTR("[CRASH] Last crash was %d days %d hours %d minutes %d seconds since boot time"), d, h, m, s); + + // get reason and exception + // https://www.espressif.com/sites/default/files/documentation/esp8266_reset_causes_and_common_fatal_exception_causes_en.pdf + char buffer[80] = {0}; + char ss[16] = {0}; + strlcpy(buffer, "[CRASH] Reason of restart: ", sizeof(buffer)); + + uint8_t reason = EEPROMr.read(SAVE_CRASH_EEPROM_OFFSET + SAVE_CRASH_RESTART_REASON); + switch (reason) { + case REASON_WDT_RST: + strlcat(buffer, "1 - Hardware WDT reset", sizeof(buffer)); + break; + case REASON_EXCEPTION_RST: + strlcat(buffer, "2 - Fatal exception", sizeof(buffer)); + break; + case REASON_SOFT_WDT_RST: + strlcat(buffer, "3 - Software watchdog reset", sizeof(buffer)); + break; + case REASON_EXT_SYS_RST: + strlcat(buffer, "6 - Hardware reset", sizeof(buffer)); + break; + case REASON_SOFT_RESTART: + strlcat(buffer, "4 - Software reset", sizeof(buffer)); + break; + default: + strlcat(buffer, itoa(reason, ss, 10), sizeof(buffer)); + } + myDebug(buffer); + + // check for exception + // see https://github.com/esp8266/Arduino/blob/master/doc/exception_causes.rst + if (reason == REASON_EXCEPTION_RST) { + // get exception cause + uint8_t cause = EEPROMr.read(SAVE_CRASH_EEPROM_OFFSET + SAVE_CRASH_EXCEPTION_CAUSE); + strlcpy(buffer, "[CRASH] Exception cause: ", sizeof(buffer)); + if (cause == 0) { + strlcat(buffer, "0 - IllegalInstructionCause", sizeof(buffer)); + } else if (cause == 3) { + strlcat(buffer, "3 - LoadStoreErrorCause", sizeof(buffer)); + } else if (cause == 6) { + strlcat(buffer, "6 - IntegerDivideByZeroCause", sizeof(buffer)); + } else if (cause == 9) { + strlcat(buffer, "9 - LoadStoreAlignmentCause", sizeof(buffer)); + } else { + strlcat(buffer, itoa(cause, ss, 10), sizeof(buffer)); + } + } + myDebug(buffer); + + uint32_t epc1, epc2, epc3, excvaddr, depc; + EEPROMr.get(SAVE_CRASH_EEPROM_OFFSET + SAVE_CRASH_EPC1, epc1); + EEPROMr.get(SAVE_CRASH_EEPROM_OFFSET + SAVE_CRASH_EPC2, epc2); + EEPROMr.get(SAVE_CRASH_EEPROM_OFFSET + SAVE_CRASH_EPC3, epc3); + EEPROMr.get(SAVE_CRASH_EEPROM_OFFSET + SAVE_CRASH_EXCVADDR, excvaddr); + EEPROMr.get(SAVE_CRASH_EEPROM_OFFSET + SAVE_CRASH_DEPC, depc); + + myDebug_P(PSTR("[CRASH] epc1=0x%08x epc2=0x%08x epc3=0x%08x"), epc1, epc2, epc3); + myDebug_P(PSTR("[CRASH] excvaddr=0x%08x depc=0x%08x"), excvaddr, depc); + + uint32_t stack_start, stack_end; + EEPROMr.get(SAVE_CRASH_EEPROM_OFFSET + SAVE_CRASH_STACK_START, stack_start); + EEPROMr.get(SAVE_CRASH_EEPROM_OFFSET + SAVE_CRASH_STACK_END, stack_end); + + myDebug_P(PSTR("[CRASH] sp=0x%08x end=0x%08x"), stack_start, stack_end); + + int16_t current_address = SAVE_CRASH_EEPROM_OFFSET + SAVE_CRASH_STACK_TRACE; + int16_t stack_len = stack_end - stack_start; + + uint32_t stack_trace; + + myDebug_P(PSTR(">>>stack>>>")); + + for (int16_t i = 0; i < stack_len; i += 0x10) { + SerialAndTelnet.printf("%08x: ", stack_start + i); + for (byte j = 0; j < 4; j++) { + EEPROMr.get(current_address, stack_trace); + SerialAndTelnet.printf("%08x ", stack_trace); + current_address += 4; + } + SerialAndTelnet.println(); + } + myDebug_P(PSTR("<< root; + root["type"] = type; + root["src"] = src; + root["desc"] = desc; + root["data"] = data; + root["time"] = now(); // is relative if we're not using NTP + + // 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 + } + + eventlog.close(); +} + +// send a paged list (10 items) to the ws +void MyESP::_sendEventLog(uint8_t page) { + File eventlog = SPIFFS.open(MYESP_EVENTLOG_FILE, "r"); + if (!eventlog) { + eventlog.close(); + myDebug_P(PSTR("[WEB] Event log is missing")); + if (_ota_post_callback_f) { + (_ota_post_callback_f)(); // call custom function + } + return; // file can't be opened + } + + if (_ota_pre_callback_f) { + (_ota_pre_callback_f)(); // call custom function + } + + // the size of the json will be quite big so best not to use stack (StaticJsonDocument) + DynamicJsonDocument doc(MYESP_JSON_MAXSIZE); + JsonObject root = doc.to(); + root["command"] = "eventlist"; + root["page"] = page; + + JsonArray list = doc.createNestedArray("list"); + + uint8_t first = ((page - 1) * 10) + 1; + uint8_t last = page * 10; + uint8_t char_count = 0; + uint8_t line_count = 0; + uint16_t read_count = 0; + bool abort = false; + char char_buffer[MYESP_JSON_LOG_MAXSIZE]; + + // if at start, start immediately recording + bool record = (first == 1) ? true : false; + + // 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've overrun, which means corrupt so ignore rest + if (read_count++ > MYESP_JSON_LOG_MAXSIZE - 1) { + abort = true; + } + + // see if we have reached the end of the string + if (c == '\0' || c == '\n') { + line_count++; + + // save line + if (record) { + char_buffer[char_count] = '\0'; + list.add(char_buffer); + } + + char_count = 0; + read_count = 0; + if (line_count == first - 1) { // have we come to the start position, start recording + record = true; + } else if (line_count == last) { // finish recording and exit loop + record = false; + } + } else { + // add the char to the buffer if recording + if (record && (char_count < MYESP_JSON_LOG_MAXSIZE)) { + char_buffer[char_count++] = c; + } + } + } + eventlog.close(); // close SPIFFS + + float pages = line_count / 10.0; + root["haspages"] = ceil(pages); + + char buffer[MYESP_JSON_MAXSIZE]; + size_t len = serializeJson(root, buffer); + + //Serial.printf("\nEVENTLOG: page %d\n", page); // 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 + } +} + +// Handles WebSocket Events +void MyESP::_onWsEvent(AsyncWebSocket * server, AsyncWebSocketClient * client, AwsEventType type, void * arg, uint8_t * data, size_t len) { + if (type == WS_EVT_ERROR) { + myDebug("[WEB] WebSocket[%s][%u] error(%u): %s\r\n", server->url(), client->id(), *((uint16_t *)arg), (char *)data); + } else if (type == WS_EVT_DATA) { + AwsFrameInfo * info = (AwsFrameInfo *)arg; + uint64_t index = info->index; + uint64_t infolen = info->len; + if (info->final && info->index == 0 && infolen == len) { + // the whole message is in a single frame and we got all of it's data + client->_tempObject = malloc(len); + if (client->_tempObject != NULL) { + memcpy((uint8_t *)(client->_tempObject), data, len); + } + _procMsg(client, infolen); + } else { + // message is comprised of multiple frames or the frame is split into multiple packets + if (index == 0) { + if (info->num == 0 && client->_tempObject == NULL) { + client->_tempObject = malloc(infolen); + } + } + if (client->_tempObject != NULL) { + memcpy((uint8_t *)(client->_tempObject) + index, data, len); + } + if ((index + len) == infolen) { + if (info->final) { + _procMsg(client, infolen); + } + } + } + } +} + +// 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]; + memcpy(json, (char *)(client->_tempObject), sz); + json[sz] = '\0'; + + JsonObject root = doc.to(); // create empty object + DeserializationError error = deserializeJson(doc, json); // Deserialize the JSON document + if (error) { + myDebug_P(PSTR("[WEB] Couldn't parse WebSocket message, error %s"), error.c_str()); + free(client->_tempObject); + client->_tempObject = NULL; + return; + } + + const char * command = doc["command"]; + // Serial.printf("*** Got command: %s\n", command); // turn on for debugging + + // Check whatever the command is and act accordingly + if (strcmp(command, "configfile") == 0) { + if (_ota_pre_callback_f) { + (_ota_pre_callback_f)(); + } + _shouldRestart = fs_saveConfig(root); + if (_ota_post_callback_f) { + (_ota_post_callback_f)(); + } + } else if (strcmp(command, "custom_configfile") == 0) { + if (_ota_pre_callback_f) { + (_ota_pre_callback_f)(); + } + (void)fs_saveCustomConfig(root); + if (_ota_post_callback_f) { + (_ota_post_callback_f)(); + } + } else if (strcmp(command, "status") == 0) { + _sendStatus(); + } else if (strcmp(command, "custom_status") == 0) { + _sendCustomStatus(); + } else if (strcmp(command, "restart") == 0) { + _shouldRestart = true; + } else if (strcmp(command, "destroy") == 0) { + _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 (SPIFFS.remove(MYESP_EVENTLOG_FILE)) { + _writeEvent("WARN", "system", "Event log cleared", ""); + } else { + myDebug_P(PSTR("[WEB] Couldn't clear log file")); + } + } 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(); + } + + free(client->_tempObject); + client->_tempObject = NULL; +} + +// read both system config and the custom config and send as json to web socket +bool MyESP::_fs_sendConfig() { + File configFile; + size_t size; + char json[MYESP_SPIFFS_MAXSIZE] = {0}; + + configFile = SPIFFS.open(MYESP_CONFIG_FILE, "r"); + if (!configFile) { + myDebug_P(PSTR("[FS] No system config found to load")); + return false; + } + size = configFile.size(); + + // read file from SPIFFS into a char array + if (configFile.readBytes(json, size) != size) { + configFile.close(); + return false; + } + configFile.close(); + + //Serial.printf("_fs_sendConfig() sending system (%d): %s\n", size, json); // turn on for debugging + _ws->textAll(json, size); + + configFile = SPIFFS.open(MYESP_CUSTOMCONFIG_FILE, "r"); + if (!configFile) { + myDebug_P(PSTR("[FS] No custom config found to load")); + return false; + } + size = configFile.size(); + + // read file from SPIFFS into the same char array + memset(json, 0, MYESP_SPIFFS_MAXSIZE); + if (configFile.readBytes(json, size) != size) { + configFile.close(); + return false; + } + configFile.close(); + + //Serial.printf("_fs_sendConfig() sending custom (%d): %s\n", size, json); // turn on for debugging + _ws->textAll(json, size); + + return true; +} + +// send custom status via ws +void MyESP::_sendCustomStatus() { + // StaticJsonDocument<300> doc; + DynamicJsonDocument doc(MYESP_JSON_MAXSIZE); + + JsonObject root = doc.to(); + + root["command"] = "custom_status"; + root["version"] = _app_version; + root["customname"] = _app_name; + root["appurl"] = _app_url; + root["updateurl"] = _app_updateurl; + + // add specific custom stuff + if (_web_callback_f) { + (_web_callback_f)(root); + } + + char buffer[MYESP_JSON_MAXSIZE]; + size_t len = serializeJson(root, buffer); + // Serial.printf("_sendCustomStatus() sending: %s\n", buffer); // turn on for debugging + + _ws->textAll(buffer, len); +} + +// send system status via ws +void MyESP::_sendStatus() { + // capture memory before we stick in a huge json buffer on the heap! + uint32_t total_memory = _getInitialFreeHeap(); + uint32_t free_memory = ESP.getFreeHeap(); + + DynamicJsonDocument doc(MQTT_MAX_PAYLOAD_SIZE_LARGE); + JsonObject root = doc.to(); + root["command"] = "status"; + + FSInfo fsinfo; + if (!SPIFFS.info(fsinfo)) { + myDebug("[SYSTEM] Error getting info on SPIFFS"); + } else { + root["availspiffs"] = (fsinfo.totalBytes - fsinfo.usedBytes) / 1000; + root["spiffssize"] = (fsinfo.totalBytes / 1000); + } + + // all sizes in bytes converted to KB + root["initheap"] = total_memory; + root["heap"] = free_memory; + root["sketchsize"] = ESP.getSketchSize() / 1000; + root["availsize"] = ESP.getFreeSketchSpace() / 1000; + + if (isAPmode()) { + root["ip"] = WiFi.softAPIP().toString(); + root["ssid"] = jw.getAPSSID(); + root["mac"] = WiFi.softAPmacAddress(); + } else { + root["ip"] = WiFi.localIP().toString(); + root["ssid"] = WiFi.SSID(); + root["mac"] = WiFi.macAddress(); + } + + root["signalstr"] = getWifiQuality(); + root["systemload"] = getSystemLoadAverage(); + + root["mqttconnected"] = isMQTTConnected(); + root["mqttheartbeat"] = getHeartbeat(); + + char uptime[200]; + uint32_t t = _getUptime(); // seconds + uint8_t d = t / 86400L; + uint8_t h = ((t % 86400L) / 3600L) % 60; + uint32_t rem = t % 3600L; + uint8_t m = rem / 60; + uint8_t sec = rem % 60; + sprintf(uptime, "%d day%s %d hour%s %d minute%s %d second%s", d, (d == 1) ? "" : "s", h, (h == 1) ? "" : "s", m, (m == 1) ? "" : "s", sec, (sec == 1) ? "" : "s"); + root["uptime"] = uptime; + + char topic_s[MQTT_MAX_TOPIC_SIZE] = {0}; + if (_hasValue(_mqtt_base)) { + strlcpy(topic_s, _mqtt_base, sizeof(topic_s)); + strlcat(topic_s, "/", sizeof(topic_s)); + strlcat(topic_s, _general_hostname, sizeof(topic_s)); + } else { + strlcpy(topic_s, _general_hostname, sizeof(topic_s)); + } + strlcat(topic_s, "/", sizeof(topic_s)); + root["mqttloghdr"] = topic_s; + + // create MQTT log + JsonArray list = root.createNestedArray("mqttlog"); + + for (uint8_t i = 0; i < MYESP_MQTTLOG_MAX; i++) { + if (MQTT_log[i].topic != nullptr) { + JsonObject item = list.createNestedObject(); + item["topic"] = MQTT_log[i].topic; + item["payload"] = MQTT_log[i].payload; + item["time"] = MQTT_log[i].timestamp; + } + } + + char buffer[MQTT_MAX_PAYLOAD_SIZE_LARGE]; + size_t len = serializeJson(root, buffer); + + _ws->textAll(buffer, len); +} + +// print top5 wifi +void MyESP::_printScanResult(int networksFound) { + int n = networksFound; + int indices[n]; + for (int i = 0; i < networksFound; i++) { + indices[i] = i; + } + + // sort by RSSI + int skip[n]; + for (int i = 0; i < networksFound; i++) { + for (int j = i + 1; j < networksFound; j++) { + if (WiFi.RSSI(indices[j]) > WiFi.RSSI(indices[i])) { + std::swap(indices[i], indices[j]); + std::swap(skip[i], skip[j]); + } + } + } + + StaticJsonDocument<400> doc; + JsonObject root = doc.to(); + root["command"] = "ssidlist"; + + JsonArray list = doc.createNestedArray("list"); + for (int i = 0; i <= 5 && i < networksFound; ++i) { + JsonObject item = list.createNestedObject(); + item["ssid"] = WiFi.SSID(indices[i]); + item["bssid"] = WiFi.BSSIDstr(indices[i]); + item["rssi"] = WiFi.RSSI(indices[i]); + } + + char buffer[400]; + size_t len = serializeJson(root, buffer); + _ws->textAll(buffer, len); +} + +// set up web server +void MyESP::_webserver_setup() { + _ws->onEvent(std::bind(&MyESP::_onWsEvent, + this, + std::placeholders::_1, + std::placeholders::_2, + std::placeholders::_3, + std::placeholders::_4, + std::placeholders::_5, + std::placeholders::_6)); + _webServer->addHandler(_ws); + + _webServer->onNotFound([](AsyncWebServerRequest * request) { + AsyncWebServerResponse * response = request->beginResponse(404, "text/plain", "Not found"); + request->send(response); + }); + + _webServer->on("/update", + HTTP_POST, + [](AsyncWebServerRequest * request) { + AsyncWebServerResponse * response = request->beginResponse(200, "text/plain", _shouldRestart ? "OK" : "FAIL"); + response->addHeader("Connection", "close"); + request->send(response); + }, + [](AsyncWebServerRequest * request, String filename, size_t index, uint8_t * data, size_t len, bool final) { + if (!request->authenticate(MYESP_HTTP_USERNAME, _general_password)) { + return; + } + if (!index) { + _writeEvent("INFO", "system", "Firmware update started", ""); + //Serial.printf("[SYSTEM] Firmware update started: %s\n", filename.c_str()); // enable for debugging + Update.runAsync(true); + if (!Update.begin((ESP.getFreeSketchSpace() - 0x1000) & 0xFFFFF000)) { + _writeEvent("ERRO", "system", "Not enough space to update", ""); + //Update.printError(Serial); // enable for debugging + } + } + if (!Update.hasError()) { + if (Update.write(data, len) != len) { + _writeEvent("ERRO", "system", "Writing to flash failed", ""); + //Update.printError(Serial); // enable for debugging + } + } + if (final) { + if (Update.end(true)) { + _writeEvent("INFO", "system", "Firmware update finished", ""); + //Serial.printf("[SYSTEM] Firmware update finished: %uB\n", index + len); // enable for debugging + _shouldRestart = !Update.hasError(); + } else { + _writeEvent("ERRO", "system", "Firmware update failed", ""); + //Update.printError(Serial); // enable for debugging + } + } + }); + + _webServer->on("/fonts/glyphicons-halflings-regular.woff", HTTP_GET, [](AsyncWebServerRequest * request) { + AsyncWebServerResponse * response = + request->beginResponse_P(200, "font/woff", glyphicons_halflings_regular_woff_gz, glyphicons_halflings_regular_woff_gz_len); + response->addHeader("Content-Encoding", "gzip"); + request->send(response); + }); + _webServer->on("/css/required.css", HTTP_GET, [](AsyncWebServerRequest * request) { + AsyncWebServerResponse * response = request->beginResponse_P(200, "text/css", required_css_gz, required_css_gz_len); + response->addHeader("Content-Encoding", "gzip"); + request->send(response); + }); + _webServer->on("/js/required.js", HTTP_GET, [](AsyncWebServerRequest * request) { + AsyncWebServerResponse * response = request->beginResponse_P(200, "text/javascript", required_js_gz, required_js_gz_len); + response->addHeader("Content-Encoding", "gzip"); + request->send(response); + }); + _webServer->on("/js/myesp.js", HTTP_GET, [](AsyncWebServerRequest * request) { + AsyncWebServerResponse * response = request->beginResponse_P(200, "text/javascript", myesp_js_gz, myesp_js_gz_len); + response->addHeader("Content-Encoding", "gzip"); + request->send(response); + }); + + _webServer->on("/index.html", HTTP_GET, [](AsyncWebServerRequest * request) { + AsyncWebServerResponse * response = request->beginResponse_P(200, "text/html", index_html_gz, index_html_gz_len); + response->addHeader("Content-Encoding", "gzip"); + request->send(response); + }); + + _webServer->on("/myesp.html", HTTP_GET, [](AsyncWebServerRequest * request) { + AsyncWebServerResponse * response = request->beginResponse_P(200, "text/html", myesp_html_gz, myesp_html_gz_len); + response->addHeader("Content-Encoding", "gzip"); + request->send(response); + }); + + _webServer->on("/login", HTTP_GET, [](AsyncWebServerRequest * request) { + //IPAddress address = request->client()->remoteIP(); + //static String remoteIP = (String)address[0] + "." + (String)address[1] + "." + (String)address[2] + "." + (String)address[3]; + + if (!request->authenticate(MYESP_HTTP_USERNAME, _general_password)) { + //_writeEvent("WARN", "system", "New login attempt", remoteIP); + return request->requestAuthentication(); + } + request->send(200, "text/plain", "Success"); + // _writeEvent("INFO", "system", "Login successful", remoteIP); + }); + + _webServer->rewrite("/", "/index.html"); + _webServer->begin(); + + myDebug_P(PSTR("[WEB] Web server started")); +} + +// print memory +void MyESP::_printHeap(const char * s) { + 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, + total_memory, + total_memory - free_memory, + 100 * (total_memory - free_memory) / total_memory, + free_memory, + 100 * free_memory / total_memory); +} + +// print MQTT log - everything that was published last per topic +void MyESP::_printMQTTLog() { + myDebug_P(PSTR("MQTT publish log:")); + + for (uint8_t i = 0; i < MYESP_MQTTLOG_MAX; i++) { + if (MQTT_log[i].topic != nullptr) { + myDebug_P(PSTR("(%d) [%lu] Topic:%s Payload:%s"), i, MQTT_log[i].timestamp, MQTT_log[i].topic, MQTT_log[i].payload); + } + } + + myDebug_P(PSTR("")); // newline +} + +// add an MQTT log entry to our buffer +void MyESP::_addMQTTLog(const char * topic, const char * payload) { + static uint8_t logCount = 0; + uint8_t logPointer = 0; + bool found = false; + + // myDebug("Publish [#%d] %s (%d) %s (%d)", logCount, topic, strlen(topic), payload, strlen(payload)); // for debugging + + // find the topic + while ((_hasValue(MQTT_log[logPointer].topic) && logPointer < MYESP_MQTTLOG_MAX)) { + if (strcmp(MQTT_log[logPointer].topic, topic) == 0) { + found = true; + break; + } + logPointer++; + } + + // if not found add it and increment next free space pointer + if (!found) { + logPointer = logCount; + if (++logCount == MYESP_MQTTLOG_MAX) { + logCount = 0; // rotate + } + } + + // delete old record + if (MQTT_log[logPointer].topic) { + free(MQTT_log[logPointer].topic); + } + + if (MQTT_log[logPointer].payload) { + free(MQTT_log[logPointer].payload); + } + + // add new record + MQTT_log[logPointer].topic = strdup(topic); + MQTT_log[logPointer].payload = strdup(payload); + MQTT_log[logPointer].timestamp = now(); +} + +// send UTC time via ws +void MyESP::_sendTime() { + StaticJsonDocument<100> doc; + JsonObject root = doc.to(); + root["command"] = "gettime"; + root["epoch"] = now(); + + char buffer[100]; + size_t len = serializeJson(root, buffer); + _ws->textAll(buffer, len); +} + +// bootup sequence +// quickly flash LED until we get a Wifi connection, or AP established +// fast way is to use WRITE_PERI_REG(PERIPHS_GPIO_BASEADDR + (state ? 4 : 8), (1 << EMSESP_Status.led_gpio)); // 4 is on, 8 is off +void MyESP::_bootupSequence() { + uint8_t boot_status = getSystemBootStatus(); + + // check if its booted + if (boot_status == MYESP_BOOTSTATUS_BOOTED) { + if ((_ntp_enabled) && (now() > 10000) && !_have_ntp_time) { + _have_ntp_time = true; + _writeEvent("INFO", "system", "System booted", ""); + } + return; + } + + // still starting up + if (millis() <= MYESP_BOOTUP_DELAY) { + return; + } + + // only kick in after a few seconds + if (boot_status == MYESP_BOOTSTATUS_POWERON) { + _setSystemBootStatus(MYESP_BOOTSTATUS_BOOTING); + } + + static uint32_t last_bootupflash = 0; + + // flash LED quickly + if ((millis() - last_bootupflash > MYESP_BOOTUP_FLASHDELAY)) { + last_bootupflash = millis(); + int state = digitalRead(LED_BUILTIN); + digitalWrite(LED_BUILTIN, !state); + } + + if (isWifiConnected()) { + _setSystemBootStatus(MYESP_BOOTSTATUS_BOOTED); // completed, reset flag + digitalWrite(LED_BUILTIN, HIGH); // turn off LED, 1=OFF with LED_BULLETIN + + // write a log message if we're not using NTP, otherwise wait for the internet time to arrive + if (!_ntp_enabled) { + _writeEvent("INFO", "system", "System booted", ""); + } + } +} + +// 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) { + _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); + + _telnet_setup(); // Telnet setup, called first to set Serial + + // _fs_printFile(MYESP_CONFIG_FILE); // for debugging + // _fs_printFile(MYESP_CUSTOMCONFIG_FILE); // for debugging + // _fs_printFile(MYESP_EVENTLOG_FILE); // for debugging + + // print a welcome message + myDebug_P(PSTR("\n\n* %s version %s"), _app_name, _app_version); + + // set up onboard LED + pinMode(LED_BUILTIN, OUTPUT); + digitalWrite(LED_BUILTIN, HIGH); + + _getInitialFreeHeap(); // get initial free mem + _rtcmemSetup(); // rtc internal mem setup + _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 + _ota_setup(); // init OTA + _webserver_setup(); // init web server + + _setSystemCheck(false); // reset system check + _heartbeatCheck(true); // force heartbeat + + SerialAndTelnet.flush(); +} + +/* + * Loop. This is called as often as possible and it handles wifi, telnet, mqtt etc + */ +void MyESP::loop() { + _calculateLoad(); + _systemCheckLoop(); + _heartbeatCheck(); + _bootupSequence(); // see if a reset was pressed during bootup + + jw.loop(); // WiFi + ArduinoOTA.handle(); // OTA + + ESP.wdtFeed(); // feed the watchdog... + _telnetHandle(); // telnet + ESP.wdtFeed(); // feed the watchdog... + + _mqttConnect(); // MQTT + + if (_timerequest) { + _timerequest = false; + _sendTime(); + } + + if (_formatreq) { + myDebug("[SYSTEM] Factory reset initiated. Please wait. System will automatically restart when complete..."); + SPIFFS.end(); + _ws->enable(false); + SPIFFS.format(); + _deferredReset(500, CUSTOM_RESET_FACTORY); + ESP.restart(); + } + + if (_shouldRestart) { + _writeEvent("INFO", "system", "System is restarting", ""); + myDebug("[SYSTEM] Restarting..."); + _deferredReset(500, CUSTOM_RESET_TERMINAL); + ESP.restart(); + } + + yield(); // ... and breath. +} + +MyESP myESP; diff --git a/lib/MyESP/MyESP.h b/src/MyESP.h similarity index 59% rename from lib/MyESP/MyESP.h rename to src/MyESP.h index 74dcc2a4a..eaf3d3993 100644 --- a/lib/MyESP/MyESP.h +++ b/src/MyESP.h @@ -1,7 +1,7 @@ /* * MyESP.h * - * Paul Derbyshire - December 2018 + * Paul Derbyshire - first version December 2018 */ #pragma once @@ -9,15 +9,18 @@ #ifndef MyESP_h #define MyESP_h -#define MYESP_VERSION "1.1.24" +#define MYESP_VERSION "1.2.0" #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 // modified from https://github.com/yasheena/telnetspy +#include // https://github.com/xoseperez/justwifi + +#include "Ntp.h" +#include "TelnetSpy.h" // modified from https://github.com/yasheena/telnetspy #ifdef CRASH #include @@ -30,7 +33,6 @@ extern struct rst_info resetInfo; } #if defined(ARDUINO_ARCH_ESP32) -//#include #include // added for ESP32 #define ets_vsnprintf vsnprintf // added for ESP32 #define OTA_PORT 3232 @@ -39,24 +41,54 @@ extern struct rst_info resetInfo; #define OTA_PORT 8266 #endif -#define MYEMS_CONFIG_FILE "/config.json" +// web files +// reference libs +#include "webh/glyphicons-halflings-regular.woff.gz.h" +#include "webh/required.css.gz.h" +#include "webh/required.js.gz.h" -#define LOADAVG_INTERVAL 30000 // Interval between calculating load average (in ms) = 30 seconds +// custom stuff +#include "webh/index.html.gz.h" +#include "webh/myesp.html.gz.h" +#include "webh/myesp.js.gz.h" + +#define MYESP_CONFIG_FILE "/myesp.json" +#define MYESP_CUSTOMCONFIG_FILE "/customconfig.json" +#define MYESP_EVENTLOG_FILE "/eventlog.json" + +#define MYESP_HTTP_USERNAME "admin" // HTTP username +#define MYESP_HTTP_PASSWORD "admin" // default password + +#define MYESP_NTP_SERVER "pool.ntp.org" // default ntp server + +#define MYESP_LOADAVG_INTERVAL 30000 // Interval between calculating load average (in ms) = 30 seconds // WIFI -#define WIFI_CONNECT_TIMEOUT 10000 // Connecting timeout for WIFI in ms (10 seconds) -#define WIFI_RECONNECT_INTERVAL 600000 // If could not connect to WIFI, retry after this time in ms. 10 minutes +#define MYESP_WIFI_CONNECT_TIMEOUT 10000 // Connecting timeout for WIFI in ms (10 seconds) +#define MYESP_WIFI_RECONNECT_INTERVAL 600000 // If could not connect to WIFI, retry after this time in ms. 10 minutes // MQTT #define MQTT_PORT 1883 // MQTT port #define MQTT_RECONNECT_DELAY_MIN 2000 // Try to reconnect in 3 seconds upon disconnection #define MQTT_RECONNECT_DELAY_STEP 3000 // Increase the reconnect delay in 3 seconds after each failed attempt #define MQTT_RECONNECT_DELAY_MAX 120000 // Set reconnect time to 2 minutes at most -#define MQTT_MAX_TOPIC_SIZE 50 // max length of MQTT topic #define MQTT_TOPIC_START "start" #define MQTT_TOPIC_HEARTBEAT "heartbeat" #define MQTT_TOPIC_START_PAYLOAD "start" #define MQTT_TOPIC_RESTART "restart" +#define MQTT_WILL_ONLINE_PAYLOAD "online" // for last will & testament payload +#define MQTT_WILL_OFFLINE_PAYLOAD "offline" // for last will & testament payload +#define MQTT_BASE_DEFAULT "home" // default MQTT prefix to topics +#define MQTT_RETAIN false +#define MQTT_KEEPALIVE 60 // 1 minute +#define MQTT_QOS 1 +#define MQTT_WILL_TOPIC "status" // for last will & testament topic name +#define MQTT_MAX_TOPIC_SIZE 50 // max length of MQTT topic +#define MQTT_MAX_PAYLOAD_SIZE 500 // max size of a JSON object. See https://arduinojson.org/v6/assistant/ +#define MQTT_MAX_PAYLOAD_SIZE_LARGE 2000 // max size of a large JSON object, like for sending MQTT log +#define MYESP_JSON_MAXSIZE 2000 // for large Dynamic json files +#define MYESP_MQTTLOG_MAX 20 // max number of log entries for MQTT publishes +#define MYESP_JSON_LOG_MAXSIZE 300 // max size of an JSON log entry // Internal MQTT events #define MQTT_CONNECT_EVENT 0 @@ -94,18 +126,20 @@ extern struct rst_info resetInfo; // reset reason codes PROGMEM const char custom_reset_hardware[] = "Hardware button"; -PROGMEM const char custom_reset_terminal[] = "Reboot from terminal"; -PROGMEM const char custom_reset_mqtt[] = "Reboot from MQTT"; -PROGMEM const char custom_reset_ota[] = "Reboot after successful OTA update"; -PROGMEM const char * const custom_reset_string[] = {custom_reset_hardware, custom_reset_terminal, custom_reset_mqtt, custom_reset_ota}; +PROGMEM const char custom_reset_terminal[] = "Restart from terminal"; +PROGMEM const char custom_reset_mqtt[] = "Restart from MQTT"; +PROGMEM const char custom_reset_ota[] = "Restart after successful OTA update"; +PROGMEM const char custom_reset_factory[] = "Factory reset"; +PROGMEM const char * const custom_reset_string[] = {custom_reset_hardware, custom_reset_terminal, custom_reset_mqtt, custom_reset_ota, custom_reset_factory}; #define CUSTOM_RESET_HARDWARE 1 // Reset from hardware button #define CUSTOM_RESET_TERMINAL 2 // Reset from terminal #define CUSTOM_RESET_MQTT 3 // Reset via MQTT #define CUSTOM_RESET_OTA 4 // Reset after successful OTA update -#define CUSTOM_RESET_MAX 4 +#define CUSTOM_RESET_FACTORY 5 // Factory reset +#define CUSTOM_RESET_MAX 5 // SPIFFS -#define SPIFFS_MAXSIZE 800 // https://arduinojson.org/v6/assistant/ +#define MYESP_SPIFFS_MAXSIZE 800 // https://arduinojson.org/v6/assistant/ // CRASH /** @@ -157,9 +191,9 @@ struct RtcmemData { static_assert(sizeof(RtcmemData) <= (RTCMEM_BLOCKS * 4u), "RTCMEM struct is too big"); -#define SYSTEM_CHECK_TIME 60000 // The system is considered stable after these many millis (1 minute) -#define SYSTEM_CHECK_MAX 5 // After this many crashes on boot -#define HEARTBEAT_INTERVAL 120000 // in milliseconds, how often the MQTT heartbeat is sent (2 mins) +#define MYESP_SYSTEM_CHECK_TIME 60000 // The system is considered stable after these many millis (1 minute) +#define MYESP_SYSTEM_CHECK_MAX 10 // After this many crashes on boot +#define MYESP_HEARTBEAT_INTERVAL 120000 // in milliseconds, how often the MQTT heartbeat is sent (2 mins) typedef struct { bool set; // is it a set command @@ -176,14 +210,21 @@ typedef enum { MYESP_BOOTSTATUS_RESETNEEDED = 3 } MYESP_BOOTSTATUS; // boot messages +// for storing all MQTT publish messages +typedef struct { + char * topic; + char * payload; + time_t timestamp; +} _MQTT_Log; + 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_callback_f; -typedef std::function fs_settings_callback_f; -typedef std::function web_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 @@ -191,58 +232,34 @@ constexpr size_t ArraySize(T (&)[N]) { return N; } -template -void PROGMEM_readAnything(const T * sce, T & dest) { - memcpy_P(&dest, sce, sizeof(T)); -} - -#define UPTIME_OVERFLOW 4294967295 // Uptime overflow value +#define MYESP_UPTIME_OVERFLOW 4294967295 // Uptime overflow value // web min and max length of wifi ssid and password -#define MAX_SSID_LEN 32 -#define MAX_PWD_LEN 64 +#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 -// max size of char buffer for storing web page -#define MYESP_MAXCHARBUFFER 800 - -// Holds the admin webpage in the program memory -const char webCommonPage_start[] = "" - "" - ""; - -const char webCommonPage_start_body[] = ""; - -const char webCommonPage_end[] = ""; - -const char webResetPage_form[] = "
" - "" - "" - "" - "
"; - -const char webResetPage_post[] = - "

New wifi credentials set. System is now rebooting. Please wait a few seconds and then reconnect via telnet or browser to its new IP given address.

"; - -const char webResetAllPage_form[] = "
" - "" - "" - "
"; - // class definition class MyESP { + protected: + // webserver + AsyncWebServer * _webServer; + AsyncWebSocket * _ws; + + // NTP + NtpClient NTP; + public: MyESP(); ~MyESP(); - ESP8266WebServer webServer; // Web server on port 80 + // 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(const char * wifi_ssid, const char * wifi_password, wifi_callback_f callback); + void setWIFI(wifi_callback_f callback); bool isWifiConnected(); bool isAPmode(); @@ -251,17 +268,7 @@ class MyESP { void mqttSubscribe(const char * topic); void mqttUnsubscribe(const char * topic); void mqttPublish(const char * topic, const char * payload); - void setMQTT(const char * mqtt_host, - const char * mqtt_username, - const char * mqtt_password, - const char * mqtt_base, - unsigned long mqtt_keepalive, - unsigned char mqtt_qos, - bool mqtt_retain, - const char * mqtt_will_topic, - const char * mqtt_will_online_payload, - const char * mqtt_will_offline_payload, - mqtt_callback_f callback); + void setMQTT(mqtt_callback_f callback); // OTA void setOTA(ota_callback_f OTACallback_pre, ota_callback_f OTACallback_post); @@ -274,8 +281,9 @@ class MyESP { void setUseSerial(bool toggle); // FS - void setSettings(fs_callback_f callback, fs_settings_callback_f fs_settings_callback); - bool fs_saveConfig(); + void setSettings(fs_loadsave_callback_f loadsave, fs_setlist_callback_f setlist, bool useSerial = true); + bool fs_saveConfig(JsonObject root); + bool fs_saveCustomConfig(JsonObject root); // Web void setWeb(web_callback_f callback_web); @@ -289,8 +297,7 @@ class MyESP { // general void end(); void loop(); - void begin(const char * app_hostname, const char * app_name, const char * app_version); - void setBoottime(const char * boottime); + 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(); @@ -298,48 +305,56 @@ class MyESP { uint32_t getSystemLoadAverage(); uint32_t getSystemResetReason(); uint8_t getSystemBootStatus(); + bool _have_ntp_time; private: // mqtt - AsyncMqttClient mqttClient; - unsigned long _mqtt_reconnect_delay; - void _mqttOnMessage(char * topic, char * payload, size_t len); - void _mqttConnect(); - void _mqtt_setup(); - mqtt_callback_f _mqtt_callback; - void _mqttOnConnect(); - void _sendStart(); - char * _mqttTopic(const char * topic); - char * _mqtt_host; - char * _mqtt_username; + void _mqttOnMessage(char * topic, char * payload, size_t len); + void _mqttConnect(); + void _mqtt_setup(); + void _mqttOnConnect(); + void _sendStart(); + char * _mqttTopic(const char * topic); + + // mqtt log + _MQTT_Log MQTT_log[MYESP_MQTTLOG_MAX]; // log for publish messages + void _printMQTTLog(); + void _addMQTTLog(const char * topic, const char * payload); + + AsyncMqttClient mqttClient; // the MQTT class + uint32_t _mqtt_reconnect_delay; + mqtt_callback_f _mqtt_callback_f; + char * _mqtt_ip; + char * _mqtt_user; char * _mqtt_password; + int _mqtt_port; char * _mqtt_base; - unsigned long _mqtt_keepalive; - unsigned char _mqtt_qos; + bool _mqtt_enabled; + uint32_t _mqtt_keepalive; + uint8_t _mqtt_qos; bool _mqtt_retain; char * _mqtt_will_topic; char * _mqtt_will_online_payload; char * _mqtt_will_offline_payload; - char * _mqtt_topic; - unsigned long _mqtt_last_connection; + uint32_t _mqtt_last_connection; bool _mqtt_connecting; - bool _rtcmem_status; + bool _mqtt_heartbeat; // wifi void _wifiCallback(justwifi_messages_t code, char * parameter); void _wifi_setup(); - wifi_callback_f _wifi_callback; - char * _wifi_ssid; - char * _wifi_password; + wifi_callback_f _wifi_callback_f; + char * _network_ssid; + char * _network_password; + uint8_t _network_wmode; bool _wifi_connected; String _getESPhostname(); // ota - ota_callback_f _ota_pre_callback; - ota_callback_f _ota_post_callback; + ota_callback_f _ota_pre_callback_f; + ota_callback_f _ota_post_callback_f; void _ota_setup(); void _OTACallback(); - bool _ota_doing_update; // crash void _eeprom_setup(); @@ -354,37 +369,42 @@ class MyESP { void _telnet_setup(); char _command[TELNET_MAX_COMMAND_LENGTH]; // the input command from either Serial or Telnet void _consoleShowHelp(); - telnetcommand_callback_f _telnetcommand_callback; // Callable for projects commands - telnet_callback_f _telnet_callback; // callback for connect/disconnect + telnetcommand_callback_f _telnetcommand_callback_f; // Callable for projects commands + telnet_callback_f _telnet_callback_f; // callback for connect/disconnect bool _changeSetting(uint8_t wc, const char * setting, const char * value); - // fs - void _fs_setup(); - bool _fs_loadConfig(); - void _fs_printConfig(); - void _fs_eraseConfig(); + // fs and settings + void _fs_setup(); + bool _fs_loadConfig(); + bool _fs_loadCustomConfig(); + void _fs_printFile(const char * file); + void _fs_eraseConfig(); + bool _fs_writeConfig(); + bool _fs_createCustomConfig(); + bool _fs_sendConfig(); + fs_loadsave_callback_f _fs_loadsave_callback_f; + fs_setlist_callback_f _fs_setlist_callback_f; - // settings - fs_callback_f _fs_callback; - fs_settings_callback_f _fs_settings_callback; - void _printSetCommands(); - - // web - web_callback_f _web_callback; + void _printSetCommands(); // general - char * _app_hostname; + char * _general_hostname; char * _app_name; char * _app_version; - char * _boottime; + char * _app_url; + char * _app_updateurl; bool _suspendOutput; - bool _serial; - bool _heartbeat; + bool _general_serial; unsigned long _getUptime(); - String _buildTime(); - bool _firstInstall; + char * _getBuildTime(); + char * _buildTime; + bool _timerequest; + bool _formatreq; + bool _hasValue(char * s); + void _printHeap(const char * s); // reset reason and rtcmem + bool _rtcmem_status; bool _rtcmemStatus(); bool _getRtcmemStatus(); @@ -418,11 +438,29 @@ class MyESP { // heartbeat void _heartbeatCheck(bool force); - // webserver + // 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); + void _sendStatus(); + void _sendCustomStatus(); + 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; }; extern MyESP myESP; diff --git a/src/Ntp.cpp b/src/Ntp.cpp new file mode 100644 index 000000000..679bbae7b --- /dev/null +++ b/src/Ntp.cpp @@ -0,0 +1,46 @@ +/* + * Ntp.cpp + */ + +#include "Ntp.h" + +char * NtpClient::TimeServerName; +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) { + TimeServerName = strdup(server); + syncInterval = syncMins * 60; // convert to seconds + WiFi.hostByName(TimeServerName, timeServer); + setSyncProvider(getNtpTime); + setSyncInterval(syncInterval); +} + +ICACHE_FLASH_ATTR NtpClient::~NtpClient() { + udpListener.close(); +} + +// send an NTP request to the time server at the given address +time_t ICACHE_FLASH_ATTR NtpClient::getNtpTime() { + memset(NTPpacket, 0, sizeof(NTPpacket)); + NTPpacket[0] = 0b11100011; + NTPpacket[1] = 0; + NTPpacket[2] = 6; + NTPpacket[3] = 0xEC; + NTPpacket[12] = 49; + NTPpacket[13] = 0x4E; + NTPpacket[14] = 49; + 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); + }); + } + udpListener.write(NTPpacket, sizeof(NTPpacket)); + return 0; +} diff --git a/src/Ntp.h b/src/Ntp.h new file mode 100644 index 000000000..7eca60b9e --- /dev/null +++ b/src/Ntp.h @@ -0,0 +1,33 @@ +/* + * Ntp.h + * + */ +#pragma once + +#ifndef NTP_H_ +#define NTP_H_ + +#include +#include + +#include "TimeLib.h" // customized version of the time library + +#define NTP_PACKET_SIZE 48 // NTP time is in the first 48 bytes of message + +class NtpClient { + public: + void ICACHE_FLASH_ATTR Ntp(const char * server, time_t syncMins); + ICACHE_FLASH_ATTR virtual ~NtpClient(); + + static char * TimeServerName; + static IPAddress timeServer; + static time_t syncInterval; + + static AsyncUDP udpListener; + + static byte NTPpacket[NTP_PACKET_SIZE]; + + static ICACHE_FLASH_ATTR time_t getNtpTime(); +}; + +#endif diff --git a/lib/TelnetSpy/TelnetSpy.cpp b/src/TelnetSpy.cpp similarity index 99% rename from lib/TelnetSpy/TelnetSpy.cpp rename to src/TelnetSpy.cpp index 85716a0d8..6a44f83c0 100644 --- a/lib/TelnetSpy/TelnetSpy.cpp +++ b/src/TelnetSpy.cpp @@ -303,7 +303,7 @@ int TelnetSpy::available(void) { } int TelnetSpy::read(void) { - int val; + int val = 0; if (usedSer) { val = usedSer->read(); if (val != -1) { @@ -319,7 +319,7 @@ int TelnetSpy::read(void) { } int TelnetSpy::peek(void) { - int val; + int val = 0; if (usedSer) { val = usedSer->peek(); if (val != -1) { diff --git a/lib/TelnetSpy/TelnetSpy.h b/src/TelnetSpy.h similarity index 100% rename from lib/TelnetSpy/TelnetSpy.h rename to src/TelnetSpy.h diff --git a/src/TimeLib.cpp b/src/TimeLib.cpp new file mode 100644 index 000000000..e56de4097 --- /dev/null +++ b/src/TimeLib.cpp @@ -0,0 +1,143 @@ +#include "TimeLib.h" + +static tmElements_t tm; // a cache of time elements +static time_t cacheTime; // the time the cache was updated +static uint32_t syncInterval = 300; // time sync will be attempted after this many seconds +static uint32_t sysTime = 0; +static uint32_t prevMillis = 0; +static uint32_t nextSyncTime = 0; +static timeStatus_t Status = timeNotSet; +getExternalTime getTimePtr; // pointer to external sync function + +#define LEAP_YEAR(Y) (((1970 + (Y)) > 0) && !((1970 + (Y)) % 4) && (((1970 + (Y)) % 100) || !((1970 + (Y)) % 400))) +static const uint8_t monthDays[] = {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}; // API starts months from 1, this array starts from 0 + +time_t now() { + // calculate number of seconds passed since last call to now() + while (millis() - prevMillis >= 1000) { + // millis() and prevMillis are both unsigned ints thus the subtraction will always be the absolute value of the difference + sysTime++; + prevMillis += 1000; + } + if (nextSyncTime <= sysTime) { + if (getTimePtr != 0) { + time_t t = getTimePtr(); + if (t != 0) { + setTime(t); + } else { + nextSyncTime = sysTime + syncInterval; + Status = (Status == timeNotSet) ? timeNotSet : timeNeedsSync; + } + } + } + return (time_t)sysTime; +} + +void setSyncProvider(getExternalTime getTimeFunction) { + getTimePtr = getTimeFunction; + nextSyncTime = sysTime; + now(); // this will sync the clock +} + +void setSyncInterval(time_t interval) { // set the number of seconds between re-sync + syncInterval = (uint32_t)interval; + nextSyncTime = sysTime + syncInterval; +} + +void breakTime(time_t timeInput, tmElements_t & tm) { + // break the given time_t into time components + // this is a more compact version of the C library localtime function + // note that year is offset from 1970 !!! + + uint8_t year; + uint8_t month, monthLength; + uint32_t time; + unsigned long days; + + time = (uint32_t)timeInput; + tm.Second = time % 60; + time /= 60; // now it is minutes + tm.Minute = time % 60; + time /= 60; // now it is hours + tm.Hour = time % 24; + time /= 24; // now it is days + tm.Wday = ((time + 4) % 7) + 1; // Sunday is day 1 + + year = 0; + days = 0; + while ((unsigned)(days += (LEAP_YEAR(year) ? 366 : 365)) <= time) { + year++; + } + tm.Year = year; // year is offset from 1970 + + days -= LEAP_YEAR(year) ? 366 : 365; + time -= days; // now it is days in this year, starting at 0 + + days = 0; + month = 0; + monthLength = 0; + for (month = 0; month < 12; month++) { + if (month == 1) { // february + if (LEAP_YEAR(year)) { + monthLength = 29; + } else { + monthLength = 28; + } + } else { + monthLength = monthDays[month]; + } + + if (time >= monthLength) { + time -= monthLength; + } else { + break; + } + } + tm.Month = month + 1; // jan is month 1 + tm.Day = time + 1; // day of month +} + +void refreshCache(time_t t) { + if (t != cacheTime) { + breakTime(t, tm); + cacheTime = t; + } +} + +int day(time_t t) { // the day for the given time (0-6) + refreshCache(t); + return tm.Day; +} + +int month(time_t t) { // the month for the given time + refreshCache(t); + return tm.Month; +} + +int second(time_t t) { // the second for the given time + refreshCache(t); + return tm.Second; +} + +int minute(time_t t) { // the minute for the given time + refreshCache(t); + return tm.Minute; +} + +int hour(time_t t) { // the hour for the given time + refreshCache(t); + return tm.Hour; +} + +int year(time_t t) { // the year for the given time + refreshCache(t); + return tmYearToCalendar(tm.Year); +} + +void setTime(time_t t) { + sysTime = (uint32_t)t; + nextSyncTime = (uint32_t)t + syncInterval; + Status = timeSet; + prevMillis = millis(); // restart counting from now (thanks to Korman for this fix) +} + diff --git a/src/TimeLib.h b/src/TimeLib.h new file mode 100644 index 000000000..a827e38f0 --- /dev/null +++ b/src/TimeLib.h @@ -0,0 +1,49 @@ +#ifndef _Time_h +#define _Time_h + +#include + +#define SECS_PER_MIN ((time_t)(60UL)) +#define SECS_PER_HOUR ((time_t)(3600UL)) +#define SECS_PER_DAY ((time_t)(SECS_PER_HOUR * 24UL)) +#define tmYearToCalendar(Y) ((Y) + 1970) // full four digit year + +// This ugly hack allows us to define C++ overloaded functions, when included +// from within an extern "C", as newlib's sys/stat.h does. Actually it is +// intended to include "time.h" from the C library (on ARM, but AVR does not +// have that file at all). On Mac and Windows, the compiler will find this +// "Time.h" instead of the C library "time.h", so we may cause other weird +// and unpredictable effects by conflicting with the C library header "time.h", +// but at least this hack lets us define C++ functions as intended. Hopefully +// nothing too terrible will result from overriding the C library header?! +extern "C++" { +typedef enum { timeNotSet, timeNeedsSync, timeSet } timeStatus_t; + +typedef struct { + uint8_t Second; + uint8_t Minute; + uint8_t Hour; + uint8_t Wday; // day of week, sunday is day 1 + uint8_t Day; + uint8_t Month; + uint8_t Year; // offset from 1970; +} tmElements_t, TimeElements, *tmElementsPtr_t; + +typedef time_t (*getExternalTime)(); + +time_t now(); // return the current time as seconds since Jan 1 1970 +void setTime(time_t t); +timeStatus_t timeStatus(); // indicates if time has been set and recently synchronized +void setSyncProvider(getExternalTime getTimeFunction); // identify the external time provider +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 + +int hour(time_t t); // the hour for the given time +int minute(time_t t); // the minute for the given time +int second(time_t t); // the second for the given time +int day(time_t t); // the day for the given time +int month(time_t t); // the month for the given time +int weekday(time_t t); // the weekday for the given time +int year(time_t t); // the year for the given time +} +#endif diff --git a/src/custom.htm b/src/custom.htm new file mode 100644 index 000000000..01ddd4aed --- /dev/null +++ b/src/custom.htm @@ -0,0 +1,275 @@ +
+
+ Custom Settings +
Please refer to the Help for configuration options
+
+ +
+ +
+
+ + +
+
+
+ +
+ + + + +
+ +
+ +
+
+ + +
+
+
+ +
+ + + + +
+ +
+ +
+
+ + +
+
+
+ +
+ +
+
+ + +
+
+
+ +
+ +
+
+ + +
+
+
+ +
+ + + + +
+
+ +
+ + + + +
+ +
+ + + + +
+ +
+
+
+ +
+
+ +
Note: any setting marked with a requires a system restart after saving. +
+
+ +
+
+
+
+

EMS Dashboard

+
Real-time values from the EMS-ESP device are shown here
+
+
+
+ + + + + + + + + +
EMS Bus Status
+ + + +
Discovered Devices: +
    +
    +
+
+
+
+
Boiler
+ + + + + + + + + + + + + + + + + + + +
Hot Tap Water:Central Heating:
Selected Flow Temperature:Current Flow Temperature:
Boiler Temperature:Return Temperature:
+
+ +
+
Thermostat
+ + + + + + + + + + + +
Setpoint Temperature:Current Temperature:
Mode:
+
+ +
+
Solar Module
+ + + + + + + + + + + + + + + + + + + + + +
Colector Temperature:Bottom Temperature:
Pump Modulation:Pump Active:
Energy Last Hour:Energy Today:Energy Total:
+
+ +
+
Heat Pump
+ + + + + + + +
Pump Modulation:Pump Speed:
+
+ +
+
+
+ +
+
\ No newline at end of file diff --git a/src/custom.js b/src/custom.js new file mode 100644 index 000000000..0c1bd2e8f --- /dev/null +++ b/src/custom.js @@ -0,0 +1,169 @@ +var custom_config = { + "command": "custom_configfile", + "settings": { + "led": true, + "led_gpio": 2, + "dallas_gpio": 14, + "dallas_parasite": false, + "listen_mode": false, + "shower_timer": false, + "shower_alert": false, + "publish_time": 120, + "heating_circuit": 1 + } +} + +function custom_commit() { + websock.send(JSON.stringify(custom_config)); +} + +function listcustom() { + + document.getElementById("led_gpio").value = custom_config.settings.led_gpio; + document.getElementById("dallas_gpio").value = custom_config.settings.dallas_gpio; + document.getElementById("publish_time").value = custom_config.settings.publish_time; + document.getElementById("heating_circuit").value = custom_config.settings.heating_circuit; + document.getElementById("tx_mode").value = custom_config.settings.tx_mode; + + 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); + } +} + +function savecustom() { + custom_config.settings.led_gpio = parseInt(document.getElementById("led_gpio").value); + custom_config.settings.dallas_gpio = parseInt(document.getElementById("dallas_gpio").value); + + custom_config.settings.dallas_parasite = false; + if (parseInt($("input[name=\"dallas_parasite\"]:checked").val()) === 1) { + custom_config.settings.dallas_parasite = true; + } + + custom_config.settings.listen_mode = false; + if (parseInt($("input[name=\"listen_mode\"]:checked").val()) === 1) { + custom_config.settings.listen_mode = true; + } + + custom_config.settings.shower_timer = false; + if (parseInt($("input[name=\"shower_timer\"]:checked").val()) === 1) { + custom_config.settings.shower_timer = true; + } + + custom_config.settings.shower_alert = false; + if (parseInt($("input[name=\"shower_alert\"]:checked").val()) === 1) { + custom_config.settings.shower_alert = true; + } + + custom_config.settings.led = false; + if (parseInt($("input[name=\"led\"]:checked").val()) === 1) { + custom_config.settings.led = true; + } + + custom_config.settings.publish_time = parseInt(document.getElementById("publish_time").value); + custom_config.settings.heating_circuit = parseInt(document.getElementById("heating_circuit").value); + custom_config.settings.tx_mode = parseInt(document.getElementById("tx_mode").value); + + custom_uncommited(); +} + +function listCustomStats() { + document.getElementById("msg").innerHTML = ajaxobj.emsbus.msg; + if (ajaxobj.emsbus.ok) { + document.getElementById("msg").className = "alert alert-success"; + } else { + document.getElementById("msg").className = "alert alert-danger"; + document.getElementById("devicesshow").style.display = "none"; + document.getElementById("thermostat_show").style.display = "none"; + document.getElementById("boiler_show").style.display = "none"; + return; + } + + var list = document.getElementById("devices"); + var obj = ajaxobj.emsbus.devices; + + document.getElementById("devicesshow").style.display = "block"; + + for (var i = 0; i < obj.length; i++) { + var l = document.createElement("li"); + var type = obj[i].type; + if (type == 1) { + var color = "list-group-item-success"; + } else if (type == 2) { + var color = "list-group-item-info"; + } else if (type == 3) { + var color = "list-group-item-warning"; + } else if (type == 4) { + var color = "list-group-item-success"; + } else { + var color = ""; + } + l.innerHTML = obj[i].model + " (Version:" + obj[i].version + " ProductID:" + obj[i].productid + " DeviceID:0x" + obj[i].deviceid + ")"; + l.className = "list-group-item " + color; + list.appendChild(l); + } + + if (ajaxobj.boiler.ok) { + document.getElementById("boiler_show").style.display = "block"; + + document.getElementById("bm").innerHTML = ajaxobj.boiler.bm; + document.getElementById("b1").innerHTML = ajaxobj.boiler.b1; + document.getElementById("b2").innerHTML = ajaxobj.boiler.b2; + document.getElementById("b3").innerHTML = ajaxobj.boiler.b3 + " ℃"; + document.getElementById("b4").innerHTML = ajaxobj.boiler.b4 + " ℃"; + document.getElementById("b5").innerHTML = ajaxobj.boiler.b5 + " ℃"; + document.getElementById("b6").innerHTML = ajaxobj.boiler.b6 + " ℃"; + } else { + document.getElementById("boiler_show").style.display = "none"; + } + + if (ajaxobj.thermostat.ok) { + document.getElementById("thermostat_show").style.display = "block"; + + document.getElementById("tm").innerHTML = ajaxobj.thermostat.tm; + document.getElementById("ts").innerHTML = ajaxobj.thermostat.ts + " ℃"; + document.getElementById("tc").innerHTML = ajaxobj.thermostat.tc + " ℃"; + document.getElementById("tmode").innerHTML = ajaxobj.thermostat.tmode; + } else { + document.getElementById("thermostat_show").style.display = "none"; + } + + if (ajaxobj.sm.ok) { + document.getElementById("sm_show").style.display = "block"; + + document.getElementById("sm").innerHTML = ajaxobj.sm.sm; + document.getElementById("sm1").innerHTML = ajaxobj.sm.sm1 + " ℃"; + document.getElementById("sm2").innerHTML = ajaxobj.sm.sm2 + " ℃"; + document.getElementById("sm3").innerHTML = ajaxobj.sm.sm3 + " %"; + document.getElementById("sm4").innerHTML = ajaxobj.sm.sm4; + document.getElementById("sm5").innerHTML = ajaxobj.sm.sm5 + " Wh"; + document.getElementById("sm6").innerHTML = ajaxobj.sm.sm6 + " Wh"; + document.getElementById("sm7").innerHTML = ajaxobj.sm.sm7 + " KWh"; + } else { + document.getElementById("sm_show").style.display = "none"; + } + + if (ajaxobj.hp.ok) { + document.getElementById("hp_show").style.display = "block"; + + document.getElementById("hm").innerHTML = ajaxobj.hp.hm; + document.getElementById("hp1").innerHTML = ajaxobj.hp.hp1 + " %"; + document.getElementById("hp2").innerHTML = ajaxobj.hp.hp2 + " %"; + } else { + document.getElementById("hp_show").style.display = "none"; + } + + +} + diff --git a/src/ems-esp.cpp b/src/ems-esp.cpp index adc3aca2e..5c46350c7 100644 --- a/src/ems-esp.cpp +++ b/src/ems-esp.cpp @@ -4,11 +4,11 @@ * Paul Derbyshire - https://github.com/proddy/EMS-ESP * * See ChangeLog.md for history - * See README.md for Acknowledgments + * See wiki at https://github.com/proddy/EMS-ESP/Wiki for Acknowledgments */ // local libraries -#include "ds18.h" +#include "MyESP.h" #include "ems.h" #include "ems_devices.h" #include "emsuart.h" @@ -16,11 +16,9 @@ #include "version.h" // Dallas external temp sensors +#include "ds18.h" DS18 ds18; -// shared libraries -#include - // public libraries #include // https://github.com/bblanchon/ArduinoJson #include // https://github.com/bakercp/CRC32 @@ -28,11 +26,18 @@ DS18 ds18; // standard arduino libs #include // https://github.com/esp8266/Arduino/tree/master/libraries/Ticker +// default APP params +#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" + +// macros for easy debugging #define myDebug(...) myESP.myDebug(__VA_ARGS__) #define myDebug_P(...) myESP.myDebug_P(__VA_ARGS__) // set to value >0 if the ESP is overheating or there are timing issues. Recommend a value of 1. -#define EMSESP_DELAY 1 // initially set to 0 for no delay +#define EMSESP_DELAY 0 // initially set to 0 for no delay. Change to 1 if getting WDT resets from wifi #define DEFAULT_HEATINGCIRCUIT 1 // default to HC1 for thermostats that support multiple heating circuits like the RC35 @@ -69,6 +74,21 @@ Ticker showerColdShotStopTimer; #define SHOWER_COLDSHOT_DURATION 10 // in seconds. 10 seconds for cold water before turning back hot water #define SHOWER_MAX_DURATION 420000 // in ms. 7 minutes, before trigger a shot of cold water +#ifdef LOGICANALYZER +#define EMSESP_DALLAS_GPIO D1 +#define EMSESP_DALLAS_PARASITE false +#else +// 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 +#endif + +// Set LED pin used for showing the EMS bus connection status. Solid means EMS bus working, flashing is an error +// can be either the onboard LED on the ESP8266 (LED_BULLETIN) or external via an external pull-up LED (e.g. D1 on a bbqkees' board) +// can be enabled and disabled via the 'set led' command and pin set by 'set led_gpio' +#define EMSESP_LED_GPIO LED_BUILTIN + typedef struct { uint32_t timestamp; // for internal timings, via millis() uint8_t dallas_sensors; // count of dallas sensors @@ -83,6 +103,7 @@ typedef struct { uint8_t dallas_gpio; // pin for attaching external dallas temperature sensors bool dallas_parasite; // on/off is using parasite uint8_t heating_circuit; // number of heating circuit, 1 or 2 + uint8_t tx_mode; // TX mode 1,2 or 3 } _EMSESP_Status; typedef struct { @@ -98,15 +119,13 @@ static const command_t project_cmds[] PROGMEM = { {true, "led ", "toggle status LED on/off"}, {true, "led_gpio ", "set the LED pin. Default is the onboard LED 2. For external D1 use 5"}, {true, "dallas_gpio ", "set the external Dallas temperature sensors pin. Default is 14 for D5"}, - {true, "dallas_parasite ", "set to on if powering Dallas sesnsors via parasite power"}, - {true, "thermostat_type ", "set the thermostat type ID (e.g. 10 for 0x10)"}, - {true, "boiler_type ", "set the boiler type ID (e.g. 8 for 0x08)"}, + {true, "dallas_parasite ", "set to on if powering Dallas sensors via parasite power"}, {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, "heating_circuit <1 | 2>", "set the main thermostat HC to work with (if using multiple heating circuits)"}, - {true, "tx_mode ", "changes Tx logic. 0=ems 1.0, 1=ems+, 2=generic (experimental!), 3=HT3"}, + {true, "tx_mode ", "changes Tx logic. 1=ems generic, 2=ems+, 3=Junkers HT3"}, {false, "info", "show current captured on the devices"}, {false, "log ", "set logging mode to none, basic, thermostat only, raw or verbose"}, @@ -419,6 +438,8 @@ void showInfo() { myDebug_P(PSTR(" System logging set to Thermostat only")); } else if (sysLog == EMS_SYS_LOGGING_SOLARMODULE) { 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 { myDebug_P(PSTR(" System logging set to None")); } @@ -509,7 +530,7 @@ void showInfo() { _renderIntValue("Burner current power", "%", EMS_Boiler.curBurnPow); _renderShortValue("Flame current", "uA", EMS_Boiler.flameCurr); _renderIntValue("System pressure", "bar", EMS_Boiler.sysPress, 10); - if (EMS_Boiler.serviceCode == EMS_VALUE_SHORT_NOTSET) { + if (EMS_Boiler.serviceCode == EMS_VALUE_USHORT_NOTSET) { myDebug_P(PSTR(" System service code: %s"), EMS_Boiler.serviceCodeChar); } else { myDebug_P(PSTR(" System service code: %s (%d)"), EMS_Boiler.serviceCodeChar, EMS_Boiler.serviceCode); @@ -550,7 +571,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_getSolarModuleDescription(buffer_type)); _renderShortValue("Collector temperature", "C", EMS_SolarModule.collectorTemp); _renderShortValue("Bottom temperature", "C", EMS_SolarModule.bottomTemp); _renderIntValue("Pump modulation", "%", EMS_SolarModule.pumpModulation); @@ -561,16 +582,16 @@ void showInfo() { (EMS_SolarModule.pumpWorkMin % 1440) / 60, EMS_SolarModule.pumpWorkMin % 60); } - _renderUShortValue("Energy Last Hour", "Wh", EMS_SolarModule.EnergyLastHour, 1); // *10 - _renderUShortValue("Energy Today", "Wh", EMS_SolarModule.EnergyToday, 0); - _renderUShortValue("Energy Total", "kWH", EMS_SolarModule.EnergyTotal, 1); // *10 + _renderUShortValue("Energy last hour", "Wh", EMS_SolarModule.EnergyLastHour, 1); // *10 + _renderUShortValue("Energy today", "Wh", EMS_SolarModule.EnergyToday, 0); + _renderUShortValue("Energy total", "kWh", EMS_SolarModule.EnergyTotal, 1); // *10 } // For HeatPumps if (ems_getHeatPumpEnabled()) { myDebug_P(PSTR("")); // newline myDebug_P(PSTR("%sHeat Pump stats:%s"), COLOR_BOLD_ON, COLOR_BOLD_OFF); - myDebug_P(PSTR(" Solar Module: %s"), ems_getHeatPumpDescription(buffer_type)); + myDebug_P(PSTR(" Heat Pump module: %s"), ems_getHeatPumpDescription(buffer_type)); _renderIntValue("Pump modulation", "%", EMS_HeatPump.HPModulation); _renderIntValue("Pump speed", "%", EMS_HeatPump.HPSpeed); } @@ -579,7 +600,7 @@ 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)); + myDebug_P(PSTR(" Thermostat: %s"), ems_getThermostatDescription(buffer_type, false)); // Render Current & Setpoint Room Temperature if (ems_getThermostatModel() == EMS_MODEL_EASY) { @@ -684,23 +705,22 @@ void publishSensorValues() { } } - - // 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 connected + // don't send if MQTT is not connected if (!myESP.isMQTTConnected()) { return; } - char s[20] = {0}; // for formatting strings - StaticJsonDocument doc; - char data[MQTT_MAX_SIZE] = {0}; - CRC32 crc; - uint32_t fchecksum; + 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 @@ -772,22 +792,28 @@ void publishValues(bool force) { if (abs(EMS_Boiler.heatWorkMin) != EMS_VALUE_LONG_NOTSET) rootBoiler["heatWorkMin"] = (double)EMS_Boiler.heatWorkMin; - rootBoiler["ServiceCode"] = EMS_Boiler.serviceCodeChar; - rootBoiler["ServiceCodeNumber"] = EMS_Boiler.serviceCode; + if (EMS_Boiler.serviceCode != EMS_VALUE_USHORT_NOTSET) { + rootBoiler["ServiceCode"] = EMS_Boiler.serviceCodeChar; + rootBoiler["ServiceCodeNumber"] = EMS_Boiler.serviceCode; + } serializeJson(doc, data, sizeof(data)); - // calculate hash and send values if something has changed, to save unnecessary wifi traffic - for (size_t i = 0; i < measureJson(doc) - 1; i++) { - crc.update(data[i]); - } - fchecksum = crc.finalize(); - if ((previousBoilerPublishCRC != fchecksum) || force) { - previousBoilerPublishCRC = fchecksum; - myDebugLog("Publishing boiler data via MQTT"); + // 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]); + } + 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); + // send values via MQTT + myESP.mqttPublish(TOPIC_BOILER_DATA, data); + } } // see if the heating or hot tap water has changed, if so send @@ -859,18 +885,22 @@ void publishValues(bool force) { data[0] = '\0'; // reset data for next package serializeJson(doc, data, sizeof(data)); - // calculate new CRC - crc.reset(); - for (size_t i = 0; i < measureJson(doc) - 1; i++) { - crc.update(data[i]); - } - fchecksum = crc.finalize(); - if ((previousThermostatPublishCRC != fchecksum) || force) { - previousThermostatPublishCRC = fchecksum; - myDebugLog("Publishing thermostat data via MQTT"); + // 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"); - // send values via MQTT - myESP.mqttPublish(TOPIC_THERMOSTAT_DATA, data); + // send values via MQTT + myESP.mqttPublish(TOPIC_THERMOSTAT_DATA, data); + } } } @@ -909,18 +939,22 @@ void publishValues(bool force) { data[0] = '\0'; // reset data for next package serializeJson(doc, data, sizeof(data)); - // calculate new CRC - crc.reset(); - for (size_t i = 0; i < measureJson(doc) - 1; i++) { - crc.update(data[i]); - } - fchecksum = crc.finalize(); - if ((previousSMPublishCRC != fchecksum) || force) { - previousSMPublishCRC = fchecksum; - myDebugLog("Publishing SM data via MQTT"); + // 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); + // send values via MQTT + myESP.mqttPublish(TOPIC_SM_DATA, data); + } } } @@ -1045,8 +1079,6 @@ void do_regularUpdates() { ems_getThermostatValues(); ems_getBoilerValues(); ems_getSolarModuleValues(); - } else { - myDebugLog("System is either not connect to the EMS bus or listen_mode is enabled"); } } @@ -1133,44 +1165,43 @@ void runUnitTest(uint8_t test_num) { } // callback for loading/saving settings to the file system (SPIFFS) -bool FSCallback(MYESP_FSACTION action, const JsonObject json) { +bool LoadSaveCallback(MYESP_FSACTION action, JsonObject json) { if (action == MYESP_FSACTION_LOAD) { - EMSESP_Status.led = json["led"]; - EMSESP_Status.led_gpio = json["led_gpio"] | EMSESP_LED_GPIO; - EMSESP_Status.dallas_gpio = json["dallas_gpio"] | EMSESP_DALLAS_GPIO; - EMSESP_Status.dallas_parasite = json["dallas_parasite"] | EMSESP_DALLAS_PARASITE; + const JsonObject & settings = json["settings"]; - EMS_Thermostat.device_id = json["thermostat_type"] | EMSESP_THERMOSTAT_TYPE; - EMS_Boiler.device_id = json["boiler_type"] | EMSESP_BOILER_TYPE; + EMSESP_Status.led = settings["led"]; + EMSESP_Status.led_gpio = settings["led_gpio"] | EMSESP_LED_GPIO; + EMSESP_Status.dallas_gpio = settings["dallas_gpio"] | EMSESP_DALLAS_GPIO; + EMSESP_Status.dallas_parasite = settings["dallas_parasite"] | EMSESP_DALLAS_PARASITE; + EMSESP_Status.shower_timer = settings["shower_timer"]; + EMSESP_Status.shower_alert = settings["shower_alert"]; + EMSESP_Status.publish_time = settings["publish_time"] | DEFAULT_PUBLISHTIME; - EMSESP_Status.shower_timer = json["shower_timer"]; - EMSESP_Status.shower_alert = json["shower_alert"]; - EMSESP_Status.publish_time = json["publish_time"] | DEFAULT_PUBLISHTIME; - - ems_setTxMode(json["tx_mode"]); - - EMSESP_Status.listen_mode = json["listen_mode"]; + EMSESP_Status.listen_mode = settings["listen_mode"]; ems_setTxDisabled(EMSESP_Status.listen_mode); - EMSESP_Status.heating_circuit = json["heating_circuit"] | DEFAULT_HEATINGCIRCUIT; + EMSESP_Status.heating_circuit = settings["heating_circuit"] | DEFAULT_HEATINGCIRCUIT; ems_setThermostatHC(EMSESP_Status.heating_circuit); - return true; // return false if some settings are missing and we need to rebuild the file + EMSESP_Status.tx_mode = settings["tx_mode"] | 1; // default to 1 (generic) + ems_setTxMode(EMSESP_Status.tx_mode); + + return true; } if (action == MYESP_FSACTION_SAVE) { - json["thermostat_type"] = EMS_Thermostat.device_id; - json["boiler_type"] = EMS_Boiler.device_id; - json["led"] = EMSESP_Status.led; - json["led_gpio"] = EMSESP_Status.led_gpio; - json["dallas_gpio"] = EMSESP_Status.dallas_gpio; - json["dallas_parasite"] = EMSESP_Status.dallas_parasite; - json["listen_mode"] = EMSESP_Status.listen_mode; - json["shower_timer"] = EMSESP_Status.shower_timer; - json["shower_alert"] = EMSESP_Status.shower_alert; - json["publish_time"] = EMSESP_Status.publish_time; - json["heating_circuit"] = EMSESP_Status.heating_circuit; - json["tx_mode"] = ems_getTxMode(); + JsonObject settings = json.createNestedObject("settings"); + + settings["led"] = EMSESP_Status.led; + settings["led_gpio"] = EMSESP_Status.led_gpio; + settings["dallas_gpio"] = EMSESP_Status.dallas_gpio; + settings["dallas_parasite"] = EMSESP_Status.dallas_parasite; + settings["listen_mode"] = EMSESP_Status.listen_mode; + settings["shower_timer"] = EMSESP_Status.shower_timer; + settings["shower_alert"] = EMSESP_Status.shower_alert; + settings["publish_time"] = EMSESP_Status.publish_time; + settings["heating_circuit"] = EMSESP_Status.heating_circuit; + settings["tx_mode"] = EMSESP_Status.tx_mode; return true; } @@ -1181,7 +1212,7 @@ bool FSCallback(MYESP_FSACTION action, const JsonObject json) { // 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 SettingsCallback(MYESP_FSACTION action, uint8_t wc, const char * setting, const char * value) { +bool SetListCallback(MYESP_FSACTION action, uint8_t wc, const char * setting, const char * value) { bool ok = false; if (action == MYESP_FSACTION_SET) { @@ -1245,18 +1276,6 @@ bool SettingsCallback(MYESP_FSACTION action, uint8_t wc, const char * setting, c } } - // thermostat_type - if (strcmp(setting, "thermostat_type") == 0) { - EMS_Thermostat.device_id = ((wc == 2) ? (uint8_t)strtol(value, 0, 16) : EMS_ID_NONE); - ok = true; - } - - // boiler_type - if (strcmp(setting, "boiler_type") == 0) { - EMS_Boiler.device_id = ((wc == 2) ? (uint8_t)strtol(value, 0, 16) : EMS_ID_NONE); - ok = true; - } - // shower timer if ((strcmp(setting, "shower_timer") == 0) && (wc == 2)) { if (strcmp(value, "on") == 0) { @@ -1301,10 +1320,16 @@ bool SettingsCallback(MYESP_FSACTION action, uint8_t wc, const char * setting, c } } - // tx delay/ tx mode - if (((strcmp(setting, "tx_mode") == 0) || (strcmp(setting, "tx_delay") == 0)) && (wc == 2)) { - ems_setTxMode(atoi(value)); - ok = true; + // tx_mode + if ((strcmp(setting, "tx_mode") == 0) && (wc == 2)) { + uint8_t mode = atoi(value); + if ((mode >= 1) && (mode <= 3)) { + EMSESP_Status.tx_mode = mode; + ems_setTxMode(mode); + ok = true; + } else { + myDebug_P(PSTR("Error. Usage: set tx_mode <1 | 2 | 3>")); + } } } @@ -1313,26 +1338,12 @@ bool SettingsCallback(MYESP_FSACTION action, uint8_t wc, const char * setting, c myDebug_P(PSTR(" led_gpio=%d"), EMSESP_Status.led_gpio); myDebug_P(PSTR(" dallas_gpio=%d"), EMSESP_Status.dallas_gpio); myDebug_P(PSTR(" dallas_parasite=%s"), EMSESP_Status.dallas_parasite ? "on" : "off"); - - if (EMS_Thermostat.device_id == EMS_ID_NONE) { - myDebug_P(PSTR(" thermostat_type=")); - } else { - myDebug_P(PSTR(" thermostat_type=%02X"), EMS_Thermostat.device_id); - } - myDebug_P(PSTR(" heating_circuit=%d"), EMSESP_Status.heating_circuit); - - if (EMS_Boiler.device_id == EMS_ID_NONE) { - myDebug_P(PSTR(" boiler_type=")); - } else { - myDebug_P(PSTR(" boiler_type=%02X"), EMS_Boiler.device_id); - } - + myDebug_P(PSTR(" tx_mode=%d"), EMSESP_Status.tx_mode); myDebug_P(PSTR(" listen_mode=%s"), EMSESP_Status.listen_mode ? "on" : "off"); myDebug_P(PSTR(" shower_timer=%s"), EMSESP_Status.shower_timer ? "on" : "off"); myDebug_P(PSTR(" shower_alert=%s"), EMSESP_Status.shower_alert ? "on" : "off"); myDebug_P(PSTR(" publish_time=%d"), EMSESP_Status.publish_time); - myDebug_P(PSTR(" tx_mode=%d"), ems_getTxMode()); } return ok; @@ -1469,6 +1480,9 @@ void TelnetCommandCallback(uint8_t wc, const char * commandLine) { } else if (strcmp(second_cmd, "n") == 0) { ems_setLogging(EMS_SYS_LOGGING_NONE); ok = true; + } else if (strcmp(second_cmd, "j") == 0) { + ems_setLogging(EMS_SYS_LOGGING_JABBER); + ok = true; } } @@ -1562,18 +1576,20 @@ void MQTTCallback(unsigned int type, const char * topic, const char * message) { if (type == MQTT_CONNECT_EVENT) { myESP.mqttSubscribe(TOPIC_THERMOSTAT_CMD_TEMP); myESP.mqttSubscribe(TOPIC_THERMOSTAT_CMD_MODE); - myESP.mqttSubscribe(TOPIC_BOILER_WWACTIVATED); - myESP.mqttSubscribe(TOPIC_BOILER_CMD_WWTEMP); - myESP.mqttSubscribe(TOPIC_BOILER_CMD_COMFORT); - myESP.mqttSubscribe(TOPIC_BOILER_CMD_FLOWTEMP); - myESP.mqttSubscribe(TOPIC_SHOWER_TIMER); - myESP.mqttSubscribe(TOPIC_SHOWER_ALERT); - myESP.mqttSubscribe(TOPIC_SHOWER_COLDSHOT); myESP.mqttSubscribe(TOPIC_THERMOSTAT_CMD_HC); myESP.mqttSubscribe(TOPIC_THERMOSTAT_CMD_DAYTEMP); myESP.mqttSubscribe(TOPIC_THERMOSTAT_CMD_NIGHTTEMP); myESP.mqttSubscribe(TOPIC_THERMOSTAT_CMD_HOLIDAYTEMP); + myESP.mqttSubscribe(TOPIC_BOILER_CMD_WWACTIVATED); + myESP.mqttSubscribe(TOPIC_BOILER_CMD_WWTEMP); + myESP.mqttSubscribe(TOPIC_BOILER_CMD_COMFORT); + myESP.mqttSubscribe(TOPIC_BOILER_CMD_FLOWTEMP); + + myESP.mqttSubscribe(TOPIC_SHOWER_TIMER); + myESP.mqttSubscribe(TOPIC_SHOWER_ALERT); + myESP.mqttSubscribe(TOPIC_SHOWER_COLDSHOT); + // publish the status of the Shower parameters myESP.mqttPublish(TOPIC_SHOWER_TIMER, EMSESP_Status.shower_timer ? "1" : "0"); myESP.mqttPublish(TOPIC_SHOWER_ALERT, EMSESP_Status.shower_alert ? "1" : "0"); @@ -1637,7 +1653,7 @@ void MQTTCallback(unsigned int type, const char * topic, const char * message) { } // wwActivated - if (strcmp(topic, TOPIC_BOILER_WWACTIVATED) == 0) { + if (strcmp(topic, TOPIC_BOILER_CMD_WWACTIVATED) == 0) { if ((message[0] == '1' || strcmp(message, "on") == 0) || (strcmp(message, "auto") == 0)) { ems_setWarmWaterActivated(true); } else if (message[0] == '0' || strcmp(message, "off") == 0) { @@ -1699,58 +1715,175 @@ void MQTTCallback(unsigned int type, const char * topic, const char * message) { } } -// web information for diagnostics -void WebCallback(char * body) { - strlcpy(body, "EMS stats:
", MYESP_MAXCHARBUFFER); - - if (ems_getBusConnected()) { - char s[10]; - strlcat(body, "EMS Bus is connected
", MYESP_MAXCHARBUFFER); - strlcat(body, "Rx: # successful read requests=", MYESP_MAXCHARBUFFER); - strlcat(body, itoa(EMS_Sys_Status.emsRxPgks, s, 10), MYESP_MAXCHARBUFFER); - strlcat(body, ", # CRC errors=", MYESP_MAXCHARBUFFER); - strlcat(body, itoa(EMS_Sys_Status.emxCrcErr, s, 10), MYESP_MAXCHARBUFFER); - if (ems_getTxCapable()) { - strlcat(body, "
Tx: # successful write requests=", MYESP_MAXCHARBUFFER); - strlcat(body, itoa(EMS_Sys_Status.emsTxPkgs, s, 10), MYESP_MAXCHARBUFFER); - } else { - strlcat(body, "
Tx: no signal

", MYESP_MAXCHARBUFFER); - } - - // show device list - strlcpy(body, "EMS devices found:
", MYESP_MAXCHARBUFFER); - - char buffer[MYESP_MAXCHARBUFFER] = {0}; - uint8_t num_devices = ems_printDevices_s(buffer, MYESP_MAXCHARBUFFER); - if (num_devices == 0) { - strlcat(body, "(any detected and compatible EMS devices will show up here)", MYESP_MAXCHARBUFFER); - } else { - strlcat(body, buffer, MYESP_MAXCHARBUFFER); - } - - } else { - strlcat(body, "Unable to establish a connection to the EMS Bus.", MYESP_MAXCHARBUFFER); - } -} - // 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 // This is done after we have a WiFi signal to avoid any resource conflicts - system_uart_swap(); // TODO check + // TODO see if EMS bus is blocked during startup and whether we still need to delay the UART with the swap below? + // system_uart_swap(); +} + +// web information for diagnostics +void WebCallback(JsonObject root) { + JsonObject emsbus = root.createNestedObject("emsbus"); - /* if (myESP.getUseSerial()) { - myDebug_P(PSTR("Warning! EMS bus communication disabled when Serial mode enabled. Use 'set serial off' to start communication.")); + emsbus["ok"] = false; + emsbus["msg"] = "EMS Bus is disabled when in Serial mode. Check Settings->General Settings->Serial Port"; } else { - emsuart_init(); - myDebug_P(PSTR("[UART] Opened Rx/Tx connection")); - if (!EMSESP_Status.listen_mode) { - // go and find the boiler and thermostat types, if not in listen mode - ems_discoverModels(); + if (ems_getBusConnected()) { + if (ems_getTxDisabled()) { + emsbus["ok"] = false; + emsbus["msg"] = "EMS Bus Connected with Rx active but Tx has been disabled (in listen only mode)."; + } else if (ems_getTxCapable()) { + emsbus["ok"] = true; + emsbus["msg"] = "EMS Bus Connected with both Rx and Tx active."; + } else { + emsbus["ok"] = false; + emsbus["msg"] = "EMS Bus Connected but Tx is not working."; + } + } else { + emsbus["ok"] = false; + emsbus["msg"] = "EMS Bus is not connected. Check event logs for errors."; } } - */ + + JsonArray list = emsbus.createNestedArray("devices"); + + 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 s[10]; + itoa((it)->device_id, s, 16); + item["deviceid"] = s; // convert to hex + } + + JsonObject thermostat = root.createNestedObject("thermostat"); + + if (ems_getThermostatEnabled()) { + thermostat["ok"] = true; + + char buffer[200]; + thermostat["tm"] = ems_getThermostatDescription(buffer, true); + + // Render Current & Setpoint Room Temperature + if (ems_getThermostatModel() == EMS_MODEL_EASY) { + if (EMS_Thermostat.setpoint_roomTemp != EMS_VALUE_SHORT_NOTSET) + thermostat["ts"] = (double)EMS_Thermostat.setpoint_roomTemp / 100; + if (EMS_Thermostat.curr_roomTemp != EMS_VALUE_SHORT_NOTSET) + thermostat["tc"] = (double)EMS_Thermostat.curr_roomTemp / 100; + } else if ((ems_getThermostatModel() == EMS_MODEL_FR10) || (ems_getThermostatModel() == EMS_MODEL_FW100) + || (ems_getThermostatModel() == EMS_MODEL_FW120)) { + if (EMS_Thermostat.setpoint_roomTemp != EMS_VALUE_SHORT_NOTSET) + thermostat["ts"] = (double)EMS_Thermostat.setpoint_roomTemp / 10; + if (EMS_Thermostat.curr_roomTemp != EMS_VALUE_SHORT_NOTSET) + thermostat["tc"] = (double)EMS_Thermostat.curr_roomTemp / 10; + } else { + if (EMS_Thermostat.setpoint_roomTemp != EMS_VALUE_SHORT_NOTSET) + thermostat["ts"] = (double)EMS_Thermostat.setpoint_roomTemp / 2; + if (EMS_Thermostat.curr_roomTemp != EMS_VALUE_SHORT_NOTSET) + thermostat["tc"] = (double)EMS_Thermostat.curr_roomTemp / 10; + } + + // Render Termostat Mode, if we have a mode + uint8_t thermoMode = _getThermostatMode(); // 0xFF=unknown, 0=low, 1=manual, 2=auto, 3=night, 4=day + if (thermoMode == 0) { + thermostat["tmode"] = "low"; + } else if (thermoMode == 1) { + thermostat["tmode"] = "manual"; + } else if (thermoMode == 2) { + thermostat["tmode"] = "auto"; + } else if (thermoMode == 3) { + thermostat["tmode"] = "night"; + } else if (thermoMode == 4) { + thermostat["tmode"] = "day"; + } + } else { + thermostat["ok"] = false; + } + + JsonObject boiler = root.createNestedObject("boiler"); + if (ems_getBoilerEnabled()) { + boiler["ok"] = true; + + char buffer[200]; + boiler["bm"] = ems_getBoilerDescription(buffer, true); + + boiler["b1"] = (EMS_Boiler.tapwaterActive ? "running" : "off"); + boiler["b2"] = (EMS_Boiler.heatingActive ? "active" : "off"); + + if (EMS_Boiler.selFlowTemp != EMS_VALUE_INT_NOTSET) + boiler["b3"] = EMS_Boiler.selFlowTemp; + + if (EMS_Boiler.curFlowTemp != EMS_VALUE_INT_NOTSET) + boiler["b4"] = EMS_Boiler.curFlowTemp / 10; + + if (EMS_Boiler.boilTemp != EMS_VALUE_USHORT_NOTSET) + boiler["b5"] = (double)EMS_Boiler.boilTemp / 10; + + if (EMS_Boiler.retTemp != EMS_VALUE_USHORT_NOTSET) + boiler["b6"] = (double)EMS_Boiler.retTemp / 10; + + } else { + boiler["ok"] = false; + } + + // For SM10/SM100 Solar Module + JsonObject sm = root.createNestedObject("sm"); + if (ems_getSolarModuleEnabled()) { + sm["ok"] = true; + + char buffer[200]; + sm["sm"] = ems_getSolarModuleDescription(buffer, true); + + if (EMS_SolarModule.collectorTemp != EMS_VALUE_SHORT_NOTSET) + sm["sm1"] = (double)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 + + if (EMS_SolarModule.pumpModulation != EMS_VALUE_INT_NOTSET) + sm["sm3"] = EMS_SolarModule.pumpModulation; // Pump modulation % + + if (EMS_SolarModule.pump != EMS_VALUE_INT_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 + + 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; + } else { + sm["ok"] = false; + } + + // For HeatPumps + JsonObject hp = root.createNestedObject("hp"); + if (ems_getHeatPumpEnabled()) { + hp["ok"] = true; + char buffer[200]; + hp["hm"] = ems_getHeatPumpDescription(buffer, true); + + if (EMS_HeatPump.HPModulation != EMS_VALUE_INT_NOTSET) + hp["hp1"] = EMS_HeatPump.HPModulation; // Pump modulation % + + if (EMS_HeatPump.HPSpeed != EMS_VALUE_INT_NOTSET) + hp["hp2"] = EMS_HeatPump.HPSpeed; // Pump speed % + } else { + hp["ok"] = false; + } + + + // serializeJsonPretty(root, Serial); // turn on for debugging } // Initialize the boiler settings and shower settings @@ -1864,25 +1997,14 @@ void setup() { systemCheckTimer.attach(SYSTEMCHECK_TIME, do_systemCheck); // check if EMS is reachable - // set up myESP for Wifi, MQTT, MDNS and Telnet - myESP.setTelnet(TelnetCommandCallback, TelnetCallback); // set up Telnet commands - myESP.setWIFI(NULL, NULL, WIFICallback); // empty ssid and password as we take this from the config file - - // MQTT host, username and password taken from the SPIFFS settings - myESP.setMQTT( - NULL, NULL, NULL, MQTT_BASE, MQTT_KEEPALIVE, MQTT_QOS, MQTT_RETAIN, MQTT_WILL_TOPIC, MQTT_WILL_ONLINE_PAYLOAD, MQTT_WILL_OFFLINE_PAYLOAD, MQTTCallback); - - // OTA callback which is called when OTA is starting and stopping - myESP.setOTA(OTACallback_pre, OTACallback_post); - - // custom settings in SPIFFS - myESP.setSettings(FSCallback, SettingsCallback); - - // web custom settings - myESP.setWeb(WebCallback); - - // start up all the services - myESP.begin(APP_HOSTNAME, APP_NAME, APP_VERSION); + // set up myESP for Wifi, MQTT, MDNS and Telnet callbacks + myESP.setTelnet(TelnetCommandCallback, TelnetCallback); // set up Telnet commands + myESP.setWIFI(WIFICallback); // wifi callback + myESP.setMQTT(MQTTCallback); // MQTT ip, username and password taken from the SPIFFS settings + 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); // at this point we have all the settings from our internall SPIFFS config file // fire up the UART now @@ -1891,7 +2013,8 @@ void setup() { } else { Serial.println("Note: Serial output will now be disabled. Please use Telnet."); Serial.flush(); - emsuart_init(); + myESP.setUseSerial(false); + emsuart_init(); // start EMS bus transmissions myDebug_P(PSTR("[UART] Opened Rx/Tx connection")); if (!EMSESP_Status.listen_mode) { // go and find the boiler and thermostat types, if not in listen mode diff --git a/src/ems.cpp b/src/ems.cpp index b33c6c597..2493fb495 100644 --- a/src/ems.cpp +++ b/src/ems.cpp @@ -7,19 +7,17 @@ */ #include "ems.h" +#include "MyESP.h" #include "ems_devices.h" #include "emsuart.h" -#include #include // https://github.com/rlogiacco/CircularBuffer -#include -#include // std::list #ifdef TESTS #include "test_data.h" uint8_t _TEST_DATA_max = ArraySize(TEST_DATA); #endif -// myESP for logging to telnet and serial +// MyESP class for logging to telnet and serial #define myDebug(...) myESP.myDebug(__VA_ARGS__) #define myDebug_P(...) myESP.myDebug_P(__VA_ARGS__) @@ -28,7 +26,7 @@ _EMS_Sys_Status EMS_Sys_Status; // EMS Status 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_Type> Devices; +std::list<_Generic_Device> Devices; // macros used in the _process* functions #define _toByte(i) (EMS_RxTelegram->data[i]) @@ -95,8 +93,6 @@ 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_RCPLUSStatusHeating(_EMS_RxTelegram * EMS_RxTelegram); -void _process_RCPLUSStatusHeating(_EMS_RxTelegram * EMS_RxTelegram); void _process_RCPLUSStatusMode(_EMS_RxTelegram * EMS_RxTelegram); // Junkers FR10 & FW100 @@ -176,7 +172,6 @@ const _EMS_Type EMS_Types[] = { // Nefit 1010, RC300, RC310 (EMS Plus) {EMS_MODEL_ALL, EMS_TYPE_RCPLUSStatusMessage, "RCPLUSStatusMessage", _process_RCPLUSStatusMessage}, {EMS_MODEL_ALL, EMS_TYPE_RCPLUSSet, "RCPLUSSetMessage", _process_RCPLUSSetMessage}, - {EMS_MODEL_ALL, EMS_TYPE_RCPLUSStatusHeating, "RCPLUSStatusHeating", _process_RCPLUSStatusHeating}, {EMS_MODEL_ALL, EMS_TYPE_RCPLUSStatusMode, "RCPLUSStatusMode", _process_RCPLUSStatusMode}, // Junkers FR10 @@ -186,12 +181,12 @@ const _EMS_Type EMS_Types[] = { }; // calculate sizes of arrays at compile -uint8_t _EMS_Types_max = ArraySize(EMS_Types); // number of defined types -uint8_t _Boiler_Types_max = ArraySize(Boiler_Types); // number of boiler models -uint8_t _Solar_Module_Types_max = ArraySize(SolarModule_Types); // number of solar module types -uint8_t _Other_Types_max = ArraySize(Other_Types); // number of other ems devices -uint8_t _Thermostat_Types_max = ArraySize(Thermostat_Types); // number of defined thermostat types -uint8_t _HeatPump_Types_max = ArraySize(HeatPump_Types); // number of defined heatpuimp types +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_Types_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 heatpuimp types // these structs contain the data we store from the specific EMS devices _EMS_Boiler EMS_Boiler; // for boiler @@ -226,7 +221,7 @@ void ems_init() { EMS_Sys_Status.emsTxPkgs = 0; EMS_Sys_Status.emxCrcErr = 0; EMS_Sys_Status.emsRxStatus = EMS_RX_STATUS_IDLE; - EMS_Sys_Status.emsTxStatus = EMS_TX_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; @@ -235,8 +230,8 @@ void ems_init() { EMS_Sys_Status.emsTxDisabled = false; EMS_Sys_Status.emsPollFrequency = 0; EMS_Sys_Status.txRetryCount = 0; - EMS_Sys_Status.emsReverse = false; - EMS_Sys_Status.emsTxMode = 0; + EMS_Sys_Status.emsIDMask = 0x00; + EMS_Sys_Status.emsPollAck[0] = EMS_ID_ME; // thermostat EMS_Thermostat.setpoint_roomTemp = EMS_VALUE_SHORT_NOTSET; @@ -278,10 +273,10 @@ void ems_init() { 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.flameCurr = EMS_VALUE_SHORT_NOTSET; // Flame current in micro amps + 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)); - EMS_Boiler.serviceCode = EMS_VALUE_SHORT_NOTSET; + EMS_Boiler.serviceCode = EMS_VALUE_USHORT_NOTSET; // UBAMonitorSlow EMS_Boiler.extTemp = EMS_VALUE_SHORT_NOTSET; // Outside temperature @@ -340,7 +335,6 @@ void ems_init() { EMS_Thermostat.product_id = EMS_ID_NONE; strlcpy(EMS_Thermostat.version, "?", sizeof(EMS_Thermostat.version)); - // default logging is none ems_setLogging(EMS_SYS_LOGGING_DEFAULT); } @@ -355,23 +349,6 @@ bool ems_getPoll() { return EMS_Sys_Status.emsPollEnabled; } -void ems_setTxMode(uint8_t mode) { - EMS_Sys_Status.emsTxMode = mode; - - // special case for Junkers. If tx_mode is 3 then set the reverse poll flag - // https://github.com/proddy/EMS-ESP/issues/103#issuecomment-495945850 - if (mode == 3) { - EMS_Sys_Status.emsReverse = true; - myDebug_P(PSTR("Forcing emsReverse for Junkers")); - } else { - EMS_Sys_Status.emsReverse = false; - } -} - -uint8_t ems_getTxMode() { - return EMS_Sys_Status.emsTxMode; -} - bool ems_getEmsRefreshed() { return EMS_Sys_Status.emsRefreshed; } @@ -439,7 +416,7 @@ _EMS_SYS_LOGGING ems_getLogging() { } void ems_setLogging(_EMS_SYS_LOGGING loglevel) { - if (loglevel <= EMS_SYS_LOGGING_VERBOSE) { + if (loglevel <= EMS_SYS_LOGGING_JABBER) { EMS_Sys_Status.emsLogging = loglevel; if (loglevel == EMS_SYS_LOGGING_NONE) { myDebug_P(PSTR("System Logging set to None")); @@ -453,10 +430,19 @@ void ems_setLogging(_EMS_SYS_LOGGING loglevel) { myDebug_P(PSTR("System Logging set to Solar Module only")); } else if (loglevel == EMS_SYS_LOGGING_RAW) { 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")); } } } +/** + * send a poll acknowledge + */ +void ems_tx_pollAck() { + emsuart_tx_buffer(&EMS_Sys_Status.emsPollAck[0], 1); +} + /** * Calculate CRC checksum using lookup table for speed * len is length of all the data in bytes (including the header & CRC byte at end) @@ -522,6 +508,11 @@ int _ems_findType(uint16_t type) { return (typeFound ? i : -1); } +void ems_setTxMode(uint8_t mode) { + EMS_Sys_Status.emsTxMode = mode; +} + + /** * debug print a telegram to telnet/serial including the CRC */ @@ -556,18 +547,14 @@ void _debugPrintTelegram(const char * prefix, _EMS_RxTelegram * EMS_RxTelegram, strlcat(output_str, " ", sizeof(output_str)); // add space } - if (raw) { - strlcat(output_str, _hextoa(data[length - 1], buffer), sizeof(output_str)); - } else { - strlcat(output_str, "(CRC=", sizeof(output_str)); - strlcat(output_str, _hextoa(data[length - 1], buffer), sizeof(output_str)); - strlcat(output_str, ")", sizeof(output_str)); + 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)); @@ -586,7 +573,7 @@ void _ems_sendTelegram() { } // if we're preventing all outbound traffic, quit - if (EMS_Sys_Status.emsTxDisabled) { + if (ems_getTxDisabled()) { EMS_TxQueue.shift(); // remove from queue if (ems_getLogging() != EMS_SYS_LOGGING_NONE) { myDebug_P(PSTR("in Listen Mode. All Tx is disabled.")); @@ -606,6 +593,8 @@ void _ems_sendTelegram() { // 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 + if (EMS_Sys_Status.emsLogging != EMS_SYS_LOGGING_NONE) { _EMS_RxTelegram EMS_RxTelegram; // create new Rx object EMS_RxTelegram.length = EMS_TxTelegram.length; // full length of telegram @@ -614,32 +603,46 @@ void _ems_sendTelegram() { _debugPrintTelegram("Sending raw: ", &EMS_RxTelegram, COLOR_CYAN, true); } - EMS_TxTelegram.data[EMS_TxTelegram.length - 1] = _crcCalculator(EMS_TxTelegram.data, EMS_TxTelegram.length); // add the CRC - emsuart_tx_buffer(EMS_TxTelegram.data, EMS_TxTelegram.length); // send the telegram to the UART Tx - EMS_TxQueue.shift(); // and remove from queue + _EMS_TX_STATUS _txStatus = emsuart_tx_buffer(EMS_TxTelegram.data, EMS_TxTelegram.length); // send the telegram to the UART Tx + if (EMS_TX_BRK_DETECT == _txStatus || EMS_TX_WTD_TIMEOUT == _txStatus) { + // Tx Error! + if (EMS_Sys_Status.emsLogging == EMS_SYS_LOGGING_VERBOSE) { + myDebug_P(PSTR("** error sending buffer: %s"), _txStatus == EMS_TX_BRK_DETECT ? "BRK" : "WDTO"); + } + // EMS_Sys_Status.emsTxStatus = EMS_TX_STATUS_IDLE; + } + EMS_TxQueue.shift(); // and remove from queue return; } // create the header - EMS_TxTelegram.data[0] = (EMS_Sys_Status.emsReverse) ? EMS_ID_ME | 0x80 : EMS_ID_ME; // src + EMS_TxTelegram.data[0] = EMS_ID_ME ^ EMS_Sys_Status.emsIDMask; // src // dest if (EMS_TxTelegram.action == EMS_TX_TELEGRAM_WRITE) { - EMS_TxTelegram.data[1] = EMS_TxTelegram.dest; + EMS_TxTelegram.data[1] = EMS_TxTelegram.dest ^ EMS_Sys_Status.emsIDMask; } else { // for a READ or VALIDATE - EMS_TxTelegram.data[1] = EMS_TxTelegram.dest | 0x80; // read has 8th bit set + EMS_TxTelegram.data[1] = (EMS_TxTelegram.dest ^ 0x80 ^ EMS_Sys_Status.emsIDMask); // read has 8th bit set } // complete the rest of the header depending on EMS or EMS+ if (EMS_TxTelegram.type > 0xFF) { - // EMS 2.0 / emsplus + // EMS 2.0 / EMS+ EMS_TxTelegram.data[2] = 0xFF; // fixed value indicating an extended message EMS_TxTelegram.data[3] = EMS_TxTelegram.offset; - EMS_TxTelegram.data[4] = EMS_TxTelegram.dataValue; // for read its #bytes to return, for write it the value to set - EMS_TxTelegram.data[5] = EMS_TxTelegram.type >> 8; // type, 1st byte - EMS_TxTelegram.data[6] = EMS_TxTelegram.type & 0xFF; // type, 2nd byte - EMS_TxTelegram.length += 2; // add 2 bytes to length to compensate the extra FF and byte for the type + EMS_TxTelegram.length += 2; // add 2 bytes to length to compensate the extra FF and byte for the type + + // EMS+ has different format for read and write. See https://github.com/proddy/EMS-ESP/wiki/RC3xx-Thermostats + if ((EMS_TxTelegram.action == EMS_TX_TELEGRAM_READ) || (EMS_TxTelegram.action == EMS_TX_TELEGRAM_VALIDATE)) { + EMS_TxTelegram.data[4] = EMS_TxTelegram.dataValue; // for read its #bytes to return + EMS_TxTelegram.data[5] = EMS_TxTelegram.type >> 8; // type, 1st byte + EMS_TxTelegram.data[6] = EMS_TxTelegram.type & 0xFF; // type, 2nd byte + } else if (EMS_TxTelegram.action == EMS_TX_TELEGRAM_WRITE) { + EMS_TxTelegram.data[4] = EMS_TxTelegram.type >> 8; // type, 1st byte + EMS_TxTelegram.data[5] = EMS_TxTelegram.type & 0xFF; // type, 2nd byte + EMS_TxTelegram.data[6] = EMS_TxTelegram.dataValue; // for write it the value to set + } } else { // EMS 1.0 EMS_TxTelegram.data[2] = EMS_TxTelegram.type; // type @@ -656,30 +659,38 @@ void _ems_sendTelegram() { if (EMS_Sys_Status.emsLogging == EMS_SYS_LOGGING_VERBOSE) { char s[64] = {0}; if (EMS_TxTelegram.action == EMS_TX_TELEGRAM_WRITE) { - snprintf(s, sizeof(s), "Sending write of type 0x%02X to 0x%02X:", EMS_TxTelegram.type, EMS_TxTelegram.dest & 0x7F); + snprintf(s, sizeof(s), "Sending write of type 0x%02X to 0x%02X, ", EMS_TxTelegram.type, EMS_TxTelegram.dest & 0x7F); } else if (EMS_TxTelegram.action == EMS_TX_TELEGRAM_READ) { - snprintf(s, sizeof(s), "Sending read of type 0x%02X to 0x%02X:", EMS_TxTelegram.type, EMS_TxTelegram.dest & 0x7F); + snprintf(s, sizeof(s), "Sending read of type 0x%02X to 0x%02X, ", EMS_TxTelegram.type, EMS_TxTelegram.dest & 0x7F); } else if (EMS_TxTelegram.action == EMS_TX_TELEGRAM_VALIDATE) { - snprintf(s, sizeof(s), "Sending validate of type 0x%02X to 0x%02X:", EMS_TxTelegram.type, EMS_TxTelegram.dest & 0x7F); + snprintf(s, sizeof(s), "Sending validate of type 0x%02X to 0x%02X, ", EMS_TxTelegram.type, EMS_TxTelegram.dest & 0x7F); } _EMS_RxTelegram EMS_RxTelegram; - EMS_RxTelegram.length = EMS_TxTelegram.length; // complete length of telegram - EMS_RxTelegram.telegram = EMS_TxTelegram.data; - EMS_RxTelegram.timestamp = millis(); // now + 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 _debugPrintTelegram(s, &EMS_RxTelegram, COLOR_CYAN); } // send the telegram to the UART Tx - emsuart_tx_buffer(EMS_TxTelegram.data, EMS_TxTelegram.length); - - EMS_Sys_Status.emsTxStatus = EMS_TX_STATUS_WAIT; + _EMS_TX_STATUS _txStatus = emsuart_tx_buffer(EMS_TxTelegram.data, EMS_TxTelegram.length); // send the telegram to the UART Tx + if (EMS_TX_STATUS_OK == _txStatus || EMS_TX_STATUS_IDLE == _txStatus) + EMS_Sys_Status.emsTxStatus = EMS_TX_STATUS_WAIT; + else { + // Tx Error! + if (EMS_Sys_Status.emsLogging == EMS_SYS_LOGGING_VERBOSE) { + myDebug_P(PSTR("** error sending buffer: %s"), _txStatus == EMS_TX_BRK_DETECT ? "BRK" : "WDTO"); + } + EMS_Sys_Status.emsTxStatus = EMS_TX_STATUS_IDLE; + } } /** * Takes the last write command and turns into a validate request - * placing it on the queue + * placing it on the Tx queue */ void _createValidate() { if (EMS_TxQueue.isEmpty()) { @@ -703,9 +714,10 @@ void _createValidate() { new_EMS_TxTelegram.action = EMS_TX_TELEGRAM_VALIDATE; // copy old Write record - new_EMS_TxTelegram.type_validate = EMS_TxTelegram.type_validate; + 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.type = EMS_TxTelegram.type; new_EMS_TxTelegram.comparisonValue = EMS_TxTelegram.comparisonValue; new_EMS_TxTelegram.comparisonPostRead = EMS_TxTelegram.comparisonPostRead; new_EMS_TxTelegram.comparisonOffset = EMS_TxTelegram.comparisonOffset; @@ -720,6 +732,57 @@ void _createValidate() { EMS_TxQueue.unshift(new_EMS_TxTelegram); // add back to queue making it first to be picked up next (FIFO) } +/** + * dump a UART Tx or Rx buffer to console... + */ +void ems_dumpBuffer(const char * prefix, uint8_t * telegram, uint8_t length) { + uint32_t timestamp = millis(); + 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)); + strlcat(output_str, ":", sizeof(output_str)); + strlcat(output_str, _smallitoa((uint8_t)((timestamp / 60000) % 60), buffer), sizeof(output_str)); + strlcat(output_str, ":", sizeof(output_str)); + strlcat(output_str, _smallitoa((uint8_t)((timestamp / 1000) % 60), buffer), sizeof(output_str)); + strlcat(output_str, ".", sizeof(output_str)); + strlcat(output_str, _smallitoa3(timestamp % 1000, buffer), sizeof(output_str)); + strlcat(output_str, COLOR_RESET, sizeof(output_str)); + strlcat(output_str, ") ", sizeof(output_str)); + + strlcat(output_str, COLOR_YELLOW, sizeof(output_str)); + strlcat(output_str, prefix, sizeof(output_str)); + + // show some EMS_Sys_Status entries + strlcat(output_str, _hextoa(EMS_Sys_Status.emsRxStatus, buffer), sizeof(output_str)); + strlcat(output_str, " ", sizeof(output_str)); + 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)); + strlcat(output_str, " ", sizeof(output_str)); + } + + strlcat(output_str, COLOR_RESET, sizeof(output_str)); + + myDebug(output_str); +} + /** * Entry point triggered by an interrupt in emsuart.cpp * length is the number of all the telegram bytes up to and including the CRC at the end @@ -727,46 +790,61 @@ void _createValidate() { * When a telegram is processed we forcefully erase it from the stack to prevent overflow */ void ems_parseTelegram(uint8_t * telegram, uint8_t length) { - static uint32_t _last_emsPollFrequency = 0; + if (EMS_Sys_Status.emsLogging == EMS_SYS_LOGGING_JABBER) { + ems_dumpBuffer("ems_parseTelegram: ", telegram, length); + } + + /* + * Detect the EMS bus type - Buderus or Junkers - and set emsIDMask accordingly. + * we wait for the first valid telegram and look at the SourceID. + * If Bit 7 is set we have a Buderus, otherwise a Junkers + */ + if (EMS_Sys_Status.emsTxStatus == EMS_TX_REV_DETECT) { + if ((length >= 5) && (telegram[length - 1] == _crcCalculator(telegram, length))) { + EMS_Sys_Status.emsTxStatus = EMS_TX_STATUS_IDLE; + EMS_Sys_Status.emsIDMask = telegram[0] & 0x80; + EMS_Sys_Status.emsPollAck[0] = EMS_ID_ME ^ EMS_Sys_Status.emsIDMask; + } else + return; // ignore the whole telegram Rx Telegram while in DETECT mode + } + + /* + * It may happen that we where 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) { + if (EMS_Sys_Status.emsLogging > EMS_SYS_LOGGING_NONE) { + myDebug_P(PSTR("** [DEBUG MODE] Warning, we missed the bus - Rx non-idle!")); + } + return; + } /* * check if we just received a single 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 - * Roger Wilco: we have different types here: - * EMS_ID_ME && length == 1 && EMS_TX_STATUS_IDLE && EMS_RX_STATUS_IDLE: polling request - * EMS_ID_ME && length > 1 && EMS_TX_STATUS_IDLE && EMS_RX_STATUS_IDLE: direct telegram - * (EMS_TX_SUCCESS || EMS_TX_ERROR) && EMS_TX_STATUS_WAIT: response, free the EMS bus - * - * In addition, it may happen that we where interrupted (f.e. 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) { - if (EMS_Sys_Status.emsLogging > EMS_SYS_LOGGING_NONE) { - myDebug_P(PSTR("** [DEBUG MODE] We missed the bus - Rx non-idle!")); //TODO tidy up error logging - } - return; - } - if (length == 1) { - uint8_t value = telegram[0]; // 1st byte of data package + uint8_t value = telegram[0]; // 1st byte of data package + static uint32_t _last_emsPollFrequency = 0; + static uint8_t delay_tx = 0; // TODO remove, experimental // check first for a Poll for us - // the poll has the MSB set - seems to work on both EMS and Junkers - if ((value & 0x7F) == EMS_ID_ME) { - EMS_Sys_Status.emsTxCapable = true; + if ((value ^ 0x80 ^ EMS_Sys_Status.emsIDMask) == EMS_ID_ME) { uint32_t timenow_microsecs = micros(); EMS_Sys_Status.emsPollFrequency = (timenow_microsecs - _last_emsPollFrequency); _last_emsPollFrequency = timenow_microsecs; // do we have something to send thats waiting in the Tx queue? // if so send it if the Queue is not in a wait state - if ((!EMS_TxQueue.isEmpty()) && (EMS_Sys_Status.emsTxStatus == EMS_TX_STATUS_IDLE)) { + // TODO: but only do this at defined rate otherwise it'll be too quick, so skip a few polls + if ((!EMS_TxQueue.isEmpty()) && (EMS_Sys_Status.emsTxStatus == EMS_TX_STATUS_IDLE) && (++delay_tx == 2)) { + delay_tx = 0; _ems_sendTelegram(); // perform the read/write command immediately } else { // nothing to send so just send a poll acknowledgement back if (EMS_Sys_Status.emsPollEnabled) { - emsuart_tx_poll(); + ems_tx_pollAck(); } } } else if (EMS_Sys_Status.emsTxStatus == EMS_TX_STATUS_WAIT) { @@ -774,15 +852,15 @@ void ems_parseTelegram(uint8_t * telegram, uint8_t length) { if (value == EMS_TX_SUCCESS) { EMS_Sys_Status.emsTxPkgs++; // got a success 01. Send a validate to check the value of the last write - emsuart_tx_poll(); // send a poll to free the EMS bus + ems_tx_pollAck(); // send a poll to free the EMS bus _createValidate(); // create a validate Tx request (if needed) } else if (value == EMS_TX_ERROR) { // last write failed (04), delete it from queue and dont bother to retry if (EMS_Sys_Status.emsLogging == EMS_SYS_LOGGING_VERBOSE) { - myDebug_P(PSTR("** Write command failed from host")); + myDebug_P(PSTR("-> Error: Write command failed from host")); } - emsuart_tx_poll(); // send a poll to free the EMS bus - _removeTxQueue(); // remove from queue + ems_tx_pollAck(); // send a poll to free the EMS bus + _removeTxQueue(); // remove from queue } } @@ -790,9 +868,9 @@ void ems_parseTelegram(uint8_t * telegram, uint8_t length) { } // ignore anything that doesn't resemble a proper telegram package - // minimal is 5 bytes, excluding CRC at the end + // minimal is 5 bytes, excluding CRC at the end (for EMS1.0) if (length <= 4) { - //_debugPrintTelegram("Noisy data:", &EMS_RxTelegram COLOR_RED); + // _debugPrintTelegram("Noisy data:", &EMS_RxTelegram, COLOR_RED); return; } @@ -805,12 +883,13 @@ void ems_parseTelegram(uint8_t * telegram, uint8_t length) { EMS_RxTelegram.dest = telegram[1] & 0x7F; // remove 8th bit (don't care if read or write) EMS_RxTelegram.offset = telegram[3]; // offset is always 4th byte - // determing if its normal ems or ems plus + // determing if its normal ems or ems plus, check for marker if (telegram[2] >= 0xF0) { // its EMS plus / EMS 2.0 - EMS_RxTelegram.emsplus = true; + EMS_RxTelegram.emsplus = true; + EMS_RxTelegram.emsplus_type = telegram[2]; // 0xFF, 0xF7 or 0xF9 - if (telegram[2] == 0xFF) { + if (EMS_RxTelegram.emsplus_type == 0xFF) { EMS_RxTelegram.type = (telegram[4] << 8) + telegram[5]; // is a long in bytes 5 & 6 EMS_RxTelegram.data = telegram + 6; @@ -840,7 +919,7 @@ 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 // but still continue to process it - if (EMS_Sys_Status.emsLogging == EMS_SYS_LOGGING_RAW) { + if ((EMS_Sys_Status.emsLogging == EMS_SYS_LOGGING_RAW)) { _debugPrintTelegram("", &EMS_RxTelegram, COLOR_WHITE, true); } @@ -1026,7 +1105,7 @@ void _removeTxQueue() { } /** - * deciphers the telegram packet, which has already been checked for valid CRC and has a complete header (min of 5 bytes) + * 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 */ @@ -1035,7 +1114,8 @@ void _processType(_EMS_RxTelegram * EMS_RxTelegram) { // if its an echo of ourselves from the master UBA, ignore. This should never happen mind you if (EMS_RxTelegram->src == EMS_ID_ME) { - // _debugPrintTelegram("echo:", EMS_RxTelegram, COLOR_WHITE); + if (EMS_Sys_Status.emsLogging == EMS_SYS_LOGGING_JABBER) + _debugPrintTelegram("echo:", EMS_RxTelegram, COLOR_WHITE); return; } @@ -1048,8 +1128,8 @@ void _processType(_EMS_RxTelegram * EMS_RxTelegram) { // release the lock on the TxQueue EMS_Sys_Status.emsTxStatus = EMS_TX_STATUS_IDLE; - // at this point we can assume Txstatus is EMS_TX_STATUS_WAIT so we just sent a read/write/validate - // for READ, WRITE or VALIDATE the dest (telegram[1]) is always us, so check for this + // at this point we can assume TxStatus is EMS_TX_STATUS_WAIT as we just sent a read or validate + // 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(); @@ -1057,7 +1137,7 @@ void _processType(_EMS_RxTelegram * EMS_RxTelegram) { return; } - // first double check we actually have something in the queue + // 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; @@ -1067,7 +1147,7 @@ void _processType(_EMS_RxTelegram * EMS_RxTelegram) { _EMS_TxTelegram EMS_TxTelegram = EMS_TxQueue.first(); // check action - // if READ, match the current inbound telegram to what we sent + // 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) { @@ -1076,10 +1156,10 @@ void _processType(_EMS_RxTelegram * EMS_RxTelegram) { // 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 - + // 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(); @@ -1089,12 +1169,12 @@ void _processType(_EMS_RxTelegram * EMS_RxTelegram) { // 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, removing from queue")); + 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 attempt %d/%d..."), EMS_Sys_Status.txRetryCount, TX_WRITE_TIMEOUT_COUNT); + myDebug_P(PSTR("-> Read failed. Retrying (%d/%d)..."), EMS_Sys_Status.txRetryCount, TX_WRITE_TIMEOUT_COUNT); } } } @@ -1104,49 +1184,53 @@ void _processType(_EMS_RxTelegram * EMS_RxTelegram) { 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("** Error! Write - should not be here")); + 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 - uint8_t * data = telegram + 4; // data block starts at position 5 - uint8_t dataReceived = data[0]; // only a single byte is returned after a read + + // 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("Write to 0x%02X was successful"), EMS_TxTelegram.dest); + 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("Last write failed. Compared set value 0x%02X with received value 0x%02X"), EMS_TxTelegram.comparisonValue, dataReceived); + 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")); + 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("...Retrying write. Attempt %d/%d..."), EMS_Sys_Status.txRetryCount, TX_WRITE_TIMEOUT_COUNT); + 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_TxQueue.shift(); // remove validate from queue - EMS_TxQueue.unshift(EMS_TxTelegram); // add back to queue making it next in line + 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 } } } - emsuart_tx_poll(); // send Acknowledgement back to free the EMS bus since we have the telegram + 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 @@ -1306,7 +1390,7 @@ void _process_RC35StatusMessage(_EMS_RxTelegram * EMS_RxTelegram) { } else { EMS_Thermostat.curr_roomTemp = _toShort(EMS_OFFSET_RC35StatusMessage_curr); } - EMS_Thermostat.day_mode = _bitRead(EMS_OFFSET_RC35Get_mode_day, 1); // get day mode flag + EMS_Thermostat.day_mode = _bitRead(EMS_OFFSET_RC35StatusMessage_mode, 1); // get day mode flag EMS_Thermostat.circuitcalctemp = _toByte(EMS_OFFSET_RC35Set_circuitcalctemp); // 0x48 calculated temperature @@ -1325,48 +1409,41 @@ void _process_EasyStatusMessage(_EMS_RxTelegram * EMS_RxTelegram) { } /** - * type 0x01A5 - data from the Nefit RC1010 thermostat (0x18) and RC300/310s on 0x10 + * type 0x01A5 - data from the Nefit RC1010/3000 thermostat (0x18) and RC300/310s on 0x10 * EMS+ messages may come in with different offsets so handle them here */ void _process_RCPLUSStatusMessage(_EMS_RxTelegram * EMS_RxTelegram) { - if (EMS_RxTelegram->offset == 0) { + // handle single data values + if (EMS_RxTelegram->data_length == 1) { + switch (EMS_RxTelegram->offset) { + case EMS_OFFSET_RCPLUSStatusMessage_curr: // setpoint target temp + EMS_Thermostat.curr_roomTemp = _toShort(0); // value is * 10 + break; + case EMS_OFFSET_RCPLUSStatusMessage_setpoint: // current target temp + EMS_Thermostat.setpoint_roomTemp = _toByte(0); // 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.setpoint_roomTemp = _toByte(0); // 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.mode = _bitRead(0, 0); // bit 1, mode (auto=1 or manual=0) + EMS_Thermostat.day_mode = _bitRead(0, 1); // get day mode flag + + break; + } + } else if (EMS_RxTelegram->data_length > 20) { // 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.curr_roomTemp = _toShort(EMS_OFFSET_RCPLUSStatusMessage_curr); // value is * 10 EMS_Thermostat.setpoint_roomTemp = _toByte(EMS_OFFSET_RCPLUSStatusMessage_setpoint); // value is * 2 - - EMS_Thermostat.day_mode = _bitRead(EMS_OFFSET_RCPLUSGet_mode_day, 1); // get day mode flag - - EMS_Thermostat.mode = _bitRead(EMS_OFFSET_RCPLUSStatusMessage_mode, 0); // bit 1, mode (auto=1 or manual=0) + EMS_Thermostat.day_mode = _bitRead(EMS_OFFSET_RCPLUSStatusMessage_mode, 1); // get day mode flag + EMS_Thermostat.mode = _bitRead(EMS_OFFSET_RCPLUSStatusMessage_mode, 0); // bit 1, mode (auto=1 or manual=0) } - // actual set point - // e.g. Thermostat -> all, telegram: 10 00 FF 07 01 A5 32 - if (EMS_RxTelegram->offset == 7) { - // to add... - } - - // next set point - // e.g. Thermostat -> all, telegram: 18 00 FF 06 01 A5 22 - if (EMS_RxTelegram->offset == 6) { - // to add... - } - - // thermostat mode auto/manual, examples: - // manual : 10 00 FF 0A 01 A5 02 (CRC=16) #data=1 - // auto : Thermostat -> all, type 0x01A5 telegram: 10 00 FF 0A 01 A5 03 (CRC=17) #data=1 - if (EMS_RxTelegram->offset == EMS_OFFSET_RCPLUSStatusMessage_mode) { - EMS_Thermostat.mode = _bitRead(0, 0); // bit 0 - } -} - -/** - * type 0x01B9 - heating data from the Nefit RC1010 thermostat (0x18) and RC300/310s on 0x10 - */ -void _process_RCPLUSStatusHeating(_EMS_RxTelegram * EMS_RxTelegram) { - // see wiki - // operation mode, comfort levels 1,2,3, eco level + EMS_Sys_Status.emsRefreshed = true; // triggers a send the values back via MQTT } /** @@ -1389,10 +1466,39 @@ void _process_JunkersStatusMessage(_EMS_RxTelegram * EMS_RxTelegram) { } /** - * to complete.... + * type 0x01B9 EMS+ for reading the mode from RC300/RC310 thermostat */ void _process_RCPLUSSetMessage(_EMS_RxTelegram * EMS_RxTelegram) { - // to complete + // ignore F7 and F9 + if (EMS_RxTelegram->emsplus_type != 0xFF) { + return; + } + + // check for single values + // 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->offset == EMS_OFFSET_RCPLUSSet_temp_setpoint) || (EMS_RxTelegram->offset == EMS_OFFSET_RCPLUSSet_manual_setpoint)) { + EMS_Thermostat.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 + } else if (EMS_RxTelegram->offset == EMS_OFFSET_RCPLUSSet_mode) { + EMS_Thermostat.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 + } + + return; // quit + } + + // check for long broadcasts + if (EMS_RxTelegram->offset == 0) { + EMS_Thermostat.mode = _toByte(EMS_OFFSET_RCPLUSSet_mode); + EMS_Thermostat.daytemp = _toByte(EMS_OFFSET_RCPLUSSet_temp_comfort2); // is * 2 + EMS_Thermostat.nighttemp = _toByte(EMS_OFFSET_RCPLUSSet_temp_eco); // is * 2 + EMS_Sys_Status.emsRefreshed = true; // triggers a send the values back via MQTT + } } /** @@ -1617,17 +1723,19 @@ void ems_clearDeviceList() { /* * add an EMS device to our list of detected devices + * model_type : 1=boiler, 2=thermostat, 3=sm, 4=other, 5=unknown */ -void _addDevice(uint8_t product_id, uint8_t device_id, char * version, const char * model_string) { - _Generic_Type device; +void _addDevice(uint8_t model_type, uint8_t product_id, uint8_t device_id, char * version, const char * model_string) { + _Generic_Device device; // if its a duplicate don't add bool found = false; - for (std::list<_Generic_Type>::iterator it = Devices.begin(); it != Devices.end(); it++) { + for (std::list<_Generic_Device>::iterator it = Devices.begin(); it != Devices.end(); it++) { if (((it)->product_id == product_id) && ((it)->device_id == device_id)) { found = true; } } if (!found) { + device.model_type = model_type; device.product_id = product_id; device.device_id = device_id; strlcpy(device.version, version, sizeof(device.version)); @@ -1654,8 +1762,8 @@ void _process_Version(_EMS_RxTelegram * EMS_RxTelegram) { // see if its a known boiler int i = 0; bool typeFound = false; - while (i < _Boiler_Types_max) { - if ((Boiler_Types[i].product_id == product_id) && ((EMS_RxTelegram->src & 0x7F) == EMS_ID_BOILER)) { + while (i < _Boiler_Devices_max) { + if ((Boiler_Devices[i].product_id == product_id) && ((EMS_RxTelegram->src & 0x7F) == EMS_ID_BOILER)) { typeFound = true; // we have a matching product id. i is the index. break; } @@ -1664,31 +1772,30 @@ void _process_Version(_EMS_RxTelegram * EMS_RxTelegram) { if (typeFound) { // its a boiler - myDebug_P(PSTR("Boiler found: %s (DeviceID:0x%02X ProductID:%d Version:%s)"), Boiler_Types[i].model_string, EMS_ID_BOILER, product_id, version); + myDebug_P(PSTR("Boiler found: %s (DeviceID:0x%02X ProductID:%d Version:%s)"), Boiler_Devices[i].model_string, EMS_ID_BOILER, product_id, version); // add to list - _addDevice(product_id, EMS_ID_BOILER, version, Boiler_Types[i].model_string); + _addDevice(EMS_MODELTYPE_BOILER, product_id, EMS_ID_BOILER, version, Boiler_Devices[i].model_string); // 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_Types[i].model_string, + Boiler_Devices[i].model_string, EMS_ID_BOILER, product_id, version); EMS_Boiler.device_id = EMS_ID_BOILER; - EMS_Boiler.product_id = Boiler_Types[i].product_id; + EMS_Boiler.product_id = Boiler_Devices[i].product_id; strlcpy(EMS_Boiler.version, version, sizeof(EMS_Boiler.version)); // check to see if its a Junkers Heatronic3, which has a different poll'ing logic if (EMS_Boiler.product_id == EMS_PRODUCTID_HEATRONICS) { - EMS_Sys_Status.emsReverse = true; + EMS_Sys_Status.emsIDMask = 0x80; + EMS_Sys_Status.emsPollAck[0] = EMS_ID_ME ^ EMS_Sys_Status.emsIDMask; } - myESP.fs_saveConfig(); // save config to SPIFFS - ems_getBoilerValues(); // get Boiler values that we would usually have to wait for } return; @@ -1696,8 +1803,8 @@ void _process_Version(_EMS_RxTelegram * EMS_RxTelegram) { // its not a boiler, maybe its a known thermostat? i = 0; - while (i < _Thermostat_Types_max) { - if (Thermostat_Types[i].product_id == product_id) { + 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; } @@ -1708,33 +1815,31 @@ void _process_Version(_EMS_RxTelegram * EMS_RxTelegram) { // its a known thermostat if (EMS_Sys_Status.emsLogging >= EMS_SYS_LOGGING_BASIC) { myDebug_P(PSTR("Thermostat found: %s (DeviceID:0x%02X ProductID:%d Version:%s)"), - Thermostat_Types[i].model_string, - Thermostat_Types[i].device_id, + Thermostat_Devices[i].model_string, + Thermostat_Devices[i].device_id, product_id, version); } // add to list - _addDevice(product_id, Thermostat_Types[i].device_id, version, Thermostat_Types[i].model_string); + _addDevice(EMS_MODELTYPE_THERMOSTAT, product_id, Thermostat_Devices[i].device_id, version, Thermostat_Devices[i].model_string); // type 2 = thermostat // if we don't have a thermostat set, use this one if (((EMS_Thermostat.device_id == EMS_ID_NONE) || (EMS_Thermostat.model_id == EMS_MODEL_NONE) - || (EMS_Thermostat.device_id == Thermostat_Types[i].device_id)) + || (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_Types[i].model_string, - Thermostat_Types[i].device_id, + Thermostat_Devices[i].model_string, + Thermostat_Devices[i].device_id, product_id, version); - EMS_Thermostat.model_id = Thermostat_Types[i].model_id; - EMS_Thermostat.device_id = Thermostat_Types[i].device_id; - EMS_Thermostat.write_supported = Thermostat_Types[i].write_supported; + EMS_Thermostat.model_id = Thermostat_Devices[i].model_id; + EMS_Thermostat.device_id = Thermostat_Devices[i].device_id; + EMS_Thermostat.write_supported = Thermostat_Devices[i].write_supported; EMS_Thermostat.product_id = product_id; strlcpy(EMS_Thermostat.version, version, sizeof(EMS_Thermostat.version)); - myESP.fs_saveConfig(); // save config to SPIFFS - // get Thermostat values (if supported) ems_getThermostatValues(); } @@ -1744,8 +1849,8 @@ void _process_Version(_EMS_RxTelegram * EMS_RxTelegram) { // look for Solar Modules i = 0; - while (i < _Solar_Module_Types_max) { - if (SolarModule_Types[i].product_id == product_id) { + while (i < _SolarModule_Types_max) { + if (SolarModule_Devices[i].product_id == product_id) { typeFound = true; // we have a matching product id. i is the index. break; } @@ -1754,16 +1859,16 @@ void _process_Version(_EMS_RxTelegram * EMS_RxTelegram) { if (typeFound) { myDebug_P(PSTR("Solar Module found: %s (DeviceID:0x%02X ProductID:%d Version:%s)"), - SolarModule_Types[i].model_string, - SolarModule_Types[i].device_id, + SolarModule_Devices[i].model_string, + SolarModule_Devices[i].device_id, product_id, version); // add to list - _addDevice(product_id, SolarModule_Types[i].device_id, version, SolarModule_Types[i].model_string); + _addDevice(EMS_MODELTYPE_SM, product_id, SolarModule_Devices[i].device_id, version, SolarModule_Devices[i].model_string); // type 3 = other myDebug_P(PSTR("Solar Module support enabled.")); - EMS_SolarModule.device_id = SolarModule_Types[i].device_id; + EMS_SolarModule.device_id = SolarModule_Devices[i].device_id; EMS_SolarModule.product_id = product_id; strlcpy(EMS_SolarModule.version, version, sizeof(EMS_SolarModule.version)); @@ -1774,8 +1879,8 @@ void _process_Version(_EMS_RxTelegram * EMS_RxTelegram) { // look for heatpumps i = 0; - while (i < _HeatPump_Types_max) { - if (HeatPump_Types[i].product_id == product_id) { + 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; } @@ -1784,16 +1889,16 @@ void _process_Version(_EMS_RxTelegram * EMS_RxTelegram) { if (typeFound) { myDebug_P(PSTR("Heat Pump found: %s (DeviceID:0x%02X ProductID:%d Version:%s)"), - HeatPump_Types[i].model_string, - HeatPump_Types[i].device_id, + HeatPump_Devices[i].model_string, + HeatPump_Devices[i].device_id, product_id, version); // add to list - _addDevice(product_id, HeatPump_Types[i].device_id, version, HeatPump_Types[i].model_string); + _addDevice(EMS_MODELTYPE_HP, product_id, HeatPump_Devices[i].device_id, version, HeatPump_Devices[i].model_string); // type 3 = other myDebug_P(PSTR("Heat Pump support enabled.")); - EMS_HeatPump.device_id = SolarModule_Types[i].device_id; + EMS_HeatPump.device_id = SolarModule_Devices[i].device_id; EMS_HeatPump.product_id = product_id; strlcpy(EMS_HeatPump.version, version, sizeof(EMS_HeatPump.version)); return; @@ -1801,8 +1906,8 @@ void _process_Version(_EMS_RxTelegram * EMS_RxTelegram) { // finally look for the other EMS devices i = 0; - while (i < _Other_Types_max) { - if (Other_Types[i].product_id == product_id) { + 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; } @@ -1810,15 +1915,15 @@ void _process_Version(_EMS_RxTelegram * EMS_RxTelegram) { } if (typeFound) { - myDebug_P(PSTR("Device found: %s (DeviceID:0x%02X ProductID:%d Version:%s)"), Other_Types[i].model_string, Other_Types[i].device_id, product_id, version); + myDebug_P(PSTR("Device found: %s (DeviceID:0x%02X ProductID:%d Version:%s)"), Other_Devices[i].model_string, Other_Devices[i].device_id, product_id, version); // add to list - _addDevice(product_id, Other_Types[i].device_id, version, Other_Types[i].model_string); + _addDevice(EMS_MODELTYPE_OTHER, product_id, Other_Devices[i].device_id, version, Other_Devices[i].model_string); // type 3 = other return; } else { myDebug_P(PSTR("Unrecognized device found: %s (DeviceID:0x%02X ProductID:%d Version:%s)"), EMS_RxTelegram->src, product_id, version); // add to list - _addDevice(product_id, EMS_RxTelegram->src, version, "unknown?"); + _addDevice(EMS_MODELTYPE_OTHER, product_id, EMS_RxTelegram->src, version, "unknown?"); // type 4 = unknown } } @@ -1827,9 +1932,11 @@ void _process_Version(_EMS_RxTelegram * EMS_RxTelegram) { * Do a read command for the version with the src having the MSB set */ void _ems_detectJunkers() { +#ifdef JUNKERS_DETECT char s[30] = {0}; snprintf(s, sizeof(s), "%02X %02X %02X 00 %02X", (EMS_ID_ME | 0x80), (EMS_ID_BOILER | 0x080), EMS_TYPE_Version, EMS_MAX_TELEGRAM_LENGTH); ems_sendRawTelegram(s); +#endif } /* @@ -1921,39 +2028,39 @@ void ems_getThermostatValues() { return; } - uint8_t model_id = EMS_Thermostat.model_id; - uint8_t type = EMS_Thermostat.device_id; - uint8_t hc = EMS_Thermostat.hc; + uint8_t model_id = EMS_Thermostat.model_id; + uint8_t device_id = EMS_Thermostat.device_id; + uint8_t hc = EMS_Thermostat.hc; switch (model_id) { case EMS_MODEL_RC20: - ems_doReadCommand(EMS_TYPE_RC20StatusMessage, type); // to get the temps - ems_doReadCommand(EMS_TYPE_RC20Set, type); // to get the mode + 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: - ems_doReadCommand(EMS_TYPE_RC30StatusMessage, type); // to get the temps - ems_doReadCommand(EMS_TYPE_RC30Set, type); // to get the mode + 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: - ems_doReadCommand(EMS_TYPE_EasyStatusMessage, type); + ems_doReadCommand(EMS_TYPE_EasyStatusMessage, device_id); break; case EMS_MODEL_RC35: case EMS_MODEL_ES73: if (hc == 1) { - ems_doReadCommand(EMS_TYPE_RC35StatusMessage_HC1, type); // to get the temps - ems_doReadCommand(EMS_TYPE_RC35Set_HC1, type); // to get the mode + ems_doReadCommand(EMS_TYPE_RC35StatusMessage_HC1, device_id); // to get the temps + ems_doReadCommand(EMS_TYPE_RC35Set_HC1, device_id); // to get the mode } else if (hc == 2) { - ems_doReadCommand(EMS_TYPE_RC35StatusMessage_HC2, type); // to get the temps - ems_doReadCommand(EMS_TYPE_RC35Set_HC2, type); // to get the mode + ems_doReadCommand(EMS_TYPE_RC35StatusMessage_HC2, device_id); // to get the temps + ems_doReadCommand(EMS_TYPE_RC35Set_HC2, device_id); // to get the mode } break; case EMS_MODEL_RC300: - ems_doReadCommand(EMS_TYPE_RCPLUSStatusMessage, type); + ems_doReadCommand(EMS_TYPE_RCPLUSStatusMessage, device_id); default: break; } - ems_doReadCommand(EMS_TYPE_RCTime, type); // get Thermostat time + ems_doReadCommand(EMS_TYPE_RCTime, device_id); // get Thermostat time } /** @@ -1986,7 +2093,7 @@ void ems_getSolarModuleValues() { * returns current thermostat type as a string * by looking up the product_id */ -char * ems_getThermostatDescription(char * buffer) { +char * ems_getThermostatDescription(char * buffer, bool name_only) { uint8_t size = 128; if (!ems_getThermostatEnabled()) { strlcpy(buffer, "", size); @@ -1996,8 +2103,8 @@ char * ems_getThermostatDescription(char * buffer) { char tmp[6] = {0}; // scan through known ID types - while (i < _Thermostat_Types_max) { - if (Thermostat_Types[i].product_id == EMS_Thermostat.product_id) { + while (i < _Thermostat_Devices_max) { + if (Thermostat_Devices[i].product_id == EMS_Thermostat.product_id) { found = true; // we have a match break; } @@ -2005,7 +2112,10 @@ char * ems_getThermostatDescription(char * buffer) { } if (found) { - strlcpy(buffer, Thermostat_Types[i].model_string, size); + 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); @@ -2028,7 +2138,7 @@ char * ems_getThermostatDescription(char * buffer) { /** * returns current boiler type as a string */ -char * ems_getBoilerDescription(char * buffer) { +char * ems_getBoilerDescription(char * buffer, bool name_only) { uint8_t size = 128; if (!ems_getBoilerEnabled()) { strlcpy(buffer, "", size); @@ -2038,15 +2148,18 @@ char * ems_getBoilerDescription(char * buffer) { char tmp[6] = {0}; // scan through known ID types - while (i < _Boiler_Types_max) { - if (Boiler_Types[i].product_id == EMS_Boiler.product_id) { + 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_Types[i].model_string, size); + 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); @@ -2069,7 +2182,7 @@ char * ems_getBoilerDescription(char * buffer) { /** * returns current Solar Module type as a string */ -char * ems_getSolarModuleDescription(char * buffer) { +char * ems_getSolarModuleDescription(char * buffer, bool name_only) { uint8_t size = 128; if (!ems_getSolarModuleEnabled()) { strlcpy(buffer, "", size); @@ -2079,15 +2192,18 @@ char * ems_getSolarModuleDescription(char * buffer) { char tmp[6] = {0}; // scan through known ID types - while (i < _Solar_Module_Types_max) { - if (SolarModule_Types[i].product_id == EMS_SolarModule.product_id) { + while (i < _SolarModule_Types_max) { + if (SolarModule_Devices[i].product_id == EMS_SolarModule.product_id) { found = true; // we have a match break; } i++; } if (found) { - strlcpy(buffer, SolarModule_Types[i].model_string, size); + 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); @@ -2110,7 +2226,7 @@ char * ems_getSolarModuleDescription(char * buffer) { /** * returns current Heat Pump type as a string */ -char * ems_getHeatPumpDescription(char * buffer) { +char * ems_getHeatPumpDescription(char * buffer, bool name_only) { uint8_t size = 128; if (!ems_getHeatPumpEnabled()) { strlcpy(buffer, "", size); @@ -2120,15 +2236,18 @@ char * ems_getHeatPumpDescription(char * buffer) { char tmp[6] = {0}; // scan through known ID types - while (i < _HeatPump_Types_max) { - if (HeatPump_Types[i].product_id == EMS_HeatPump.product_id) { + 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_Types[i].model_string, size); + 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); @@ -2160,17 +2279,17 @@ void ems_scanDevices() { Device_Ids.push_back(EMS_ID_BOILER); // copy over thermostats - for (_Thermostat_Type tt : Thermostat_Types) { + for (_Thermostat_Device tt : Thermostat_Devices) { Device_Ids.push_back(tt.device_id); } // copy over solar modules - for (_SolarModule_Type sm : SolarModule_Types) { + for (_SolarModule_Device sm : SolarModule_Devices) { Device_Ids.push_back(sm.device_id); } // copy over others - for (_Other_Type ot : Other_Types) { + for (_Other_Device ot : Other_Devices) { Device_Ids.push_back(ot.device_id); } @@ -2194,34 +2313,34 @@ void ems_scanDevices() { void ems_printAllDevices() { uint8_t i; - myDebug_P(PSTR("\nThese %d devices are supported as boiler units:"), _Boiler_Types_max); - for (i = 0; i < _Boiler_Types_max; 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_Types[i].model_string, + Boiler_Devices[i].model_string, COLOR_BOLD_OFF, EMS_ID_BOILER, - Boiler_Types[i].product_id); + Boiler_Devices[i].product_id); } - myDebug_P(PSTR("\nThese %d devices are supported as solar module devices:"), _Solar_Module_Types_max); - for (i = 0; i < _Solar_Module_Types_max; i++) { + myDebug_P(PSTR("\nThese %d devices are supported as solar module devices:"), _SolarModule_Types_max); + for (i = 0; i < _SolarModule_Types_max; i++) { myDebug_P(PSTR(" %s%s%s (DeviceID:0x%02X ProductID:%d)"), COLOR_BOLD_ON, - SolarModule_Types[i].model_string, + SolarModule_Devices[i].model_string, COLOR_BOLD_OFF, - SolarModule_Types[i].device_id, - SolarModule_Types[i].product_id); + SolarModule_Devices[i].device_id, + SolarModule_Devices[i].product_id); } - myDebug_P(PSTR("\nThese %d devices are supported as other known EMS devices:"), _Other_Types_max); - for (i = 0; i < _Other_Types_max; i++) { + myDebug_P(PSTR("\nThese %d devices are supported as other known 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_Types[i].model_string, + Other_Devices[i].model_string, COLOR_BOLD_OFF, - Other_Types[i].device_id, - Other_Types[i].product_id); + Other_Devices[i].device_id, + Other_Devices[i].product_id); } myDebug_P(PSTR("\nThe following telegram type IDs are supported:")); @@ -2231,15 +2350,15 @@ void ems_printAllDevices() { } } - myDebug_P(PSTR("\nThese %d thermostat devices are supported:"), _Thermostat_Types_max); - for (i = 0; i < _Thermostat_Types_max; i++) { + 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_Types[i].model_string, + Thermostat_Devices[i].model_string, COLOR_BOLD_OFF, - Thermostat_Types[i].device_id, - Thermostat_Types[i].product_id, - (Thermostat_Types[i].write_supported) ? 'y' : 'n'); + Thermostat_Devices[i].device_id, + Thermostat_Devices[i].product_id, + (Thermostat_Devices[i].write_supported) ? 'y' : 'n'); } // print out known devices @@ -2254,7 +2373,7 @@ void ems_printAllDevices() { void ems_printDevices() { if (Devices.size() != 0) { myDebug_P(PSTR("\nThese %d EMS devices were detected:"), Devices.size()); - for (std::list<_Generic_Type>::iterator it = Devices.begin(); it != Devices.end(); it++) { + for (std::list<_Generic_Device>::iterator it = Devices.begin(); it != Devices.end(); it++) { myDebug_P(PSTR(" %s%s%s (DeviceID:0x%02X ProductID:%d Version:%s)"), COLOR_BOLD_ON, (it)->model_string, @@ -2269,23 +2388,6 @@ void ems_printDevices() { } } -/* - * prints the device list to a string for html parsing - */ -uint8_t ems_printDevices_s(char * buffer, uint16_t len) { - if (Devices.size() == 0) { - return 0; - } - - char s[100]; - for (std::list<_Generic_Type>::iterator it = Devices.begin(); it != Devices.end(); it++) { - sprintf(s, "%s (DeviceID:0x%02X ProductID:%d Version:%s)
", (it)->model_string, (it)->device_id, (it)->product_id, (it)->version); - strlcat(buffer, s, len); - } - - return Devices.size(); -} - /** * 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) @@ -2342,10 +2444,6 @@ void ems_sendRawTelegram(char * telegram) { char * p; char value[10] = {0}; - if (EMS_Sys_Status.emsTxDisabled) { - return; // user has disabled all Tx - } - _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 @@ -2375,7 +2473,6 @@ void ems_sendRawTelegram(char * telegram) { return; // nothing to send } - // calculate length including header and CRC EMS_TxTelegram.length = count + 2; EMS_TxTelegram.type_validate = EMS_ID_NONE; EMS_TxTelegram.action = EMS_TX_TELEGRAM_RAW; @@ -2394,7 +2491,7 @@ void ems_setThermostatTemp(float temperature, uint8_t temptype) { } if (!EMS_Thermostat.write_supported) { - myDebug_P(PSTR("Write not supported for this model Thermostat")); + myDebug_P(PSTR("Write not supported for this Thermostat model")); return; } @@ -2402,29 +2499,47 @@ void ems_setThermostatTemp(float temperature, 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 type = EMS_Thermostat.device_id; - uint8_t hc = EMS_Thermostat.hc; // heating circuit + uint8_t model_id = EMS_Thermostat.model_id; + uint8_t device_id = EMS_Thermostat.device_id; + uint8_t hc = EMS_Thermostat.hc; // heating circuit EMS_TxTelegram.action = EMS_TX_TELEGRAM_WRITE; - EMS_TxTelegram.dest = type; + EMS_TxTelegram.dest = device_id; myDebug_P(PSTR("Setting new thermostat temperature")); - // when doing a comparison to validate the new temperature we call a different type - if (model_id == EMS_MODEL_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) { 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) { 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 + if (EMS_Thermostat.mode == 1) { // auto + EMS_TxTelegram.offset = 0x08; // auto offset + } else if (EMS_Thermostat.mode == 0) { // manuaL + EMS_TxTelegram.offset = 0x0A; // manual offset + } + // TODO: commented out because it's too fast + // EMS_TxTelegram.type_validate = EMS_TYPE_RCPLUSStatusMessage; // validate by reading from a different telegram + EMS_TxTelegram.type_validate = EMS_ID_NONE; // validate by reading from a different telegram + + EMS_TxTelegram.comparisonPostRead = EMS_TYPE_RCPLUSStatusMessage; // after write, do a full fetch of all values + } else if ((model_id == EMS_MODEL_RC35) || (model_id == EMS_MODEL_ES73)) { switch (temptype) { case 1: // change the night temp @@ -2445,21 +2560,21 @@ void ems_setThermostatTemp(float temperature, uint8_t temptype) { } break; } - if (hc == 1) { // heating circuit 1 EMS_TxTelegram.type = EMS_TYPE_RC35Set_HC1; EMS_TxTelegram.comparisonPostRead = EMS_TYPE_RC35StatusMessage_HC1; + EMS_TxTelegram.type_validate = EMS_TxTelegram.type; } else { // heating circuit 2 EMS_TxTelegram.type = EMS_TYPE_RC35Set_HC2; EMS_TxTelegram.comparisonPostRead = EMS_TYPE_RC35StatusMessage_HC2; + EMS_TxTelegram.type_validate = EMS_TxTelegram.type; } } EMS_TxTelegram.length = EMS_MIN_TELEGRAM_LENGTH; - EMS_TxTelegram.dataValue = (uint8_t)((float)temperature * (float)2); // value - EMS_TxTelegram.type_validate = EMS_TxTelegram.type; + EMS_TxTelegram.dataValue = (uint8_t)((float)temperature * (float)2); // value * 2 EMS_TxTelegram.comparisonOffset = EMS_TxTelegram.offset; EMS_TxTelegram.comparisonValue = EMS_TxTelegram.dataValue; @@ -2468,8 +2583,9 @@ void ems_setThermostatTemp(float temperature, uint8_t temptype) { } /** - * Set the thermostat working mode (0=low/night, 1=manual/day, 2=auto/clock) - * 0xA8 on a RC20 and 0xA7 on RC30 + * Set the thermostat working mode + * (0=low/night, 1=manual/day, 2=auto/clock), 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 */ void ems_setThermostatMode(uint8_t mode) { if (!ems_getThermostatEnabled()) { @@ -2481,9 +2597,18 @@ void ems_setThermostatMode(uint8_t mode) { return; } - uint8_t model_id = EMS_Thermostat.model_id; - uint8_t type = EMS_Thermostat.device_id; - uint8_t hc = EMS_Thermostat.hc; + uint8_t model_id = EMS_Thermostat.model_id; + uint8_t device_id = EMS_Thermostat.device_id; + uint8_t hc = EMS_Thermostat.hc; + + // RC300/1000/3000 have different settings + if (model_id == EMS_MODEL_RC300) { + if (mode == 1) { + mode = 0; // manual + } else { + mode = 0xFF; // auto + } + } myDebug_P(PSTR("Setting thermostat mode to %d"), mode); @@ -2492,27 +2617,46 @@ void ems_setThermostatMode(uint8_t mode) { EMS_Sys_Status.txRetryCount = 0; // reset retry counter EMS_TxTelegram.action = EMS_TX_TELEGRAM_WRITE; - EMS_TxTelegram.dest = type; + EMS_TxTelegram.dest = device_id; EMS_TxTelegram.length = EMS_MIN_TELEGRAM_LENGTH; EMS_TxTelegram.dataValue = mode; // handle different thermostat types if (model_id == EMS_MODEL_RC20) { - EMS_TxTelegram.type = EMS_TYPE_RC20Set; - EMS_TxTelegram.offset = EMS_OFFSET_RC20Set_mode; + EMS_TxTelegram.type = EMS_TYPE_RC20Set; + EMS_TxTelegram.offset = EMS_OFFSET_RC20Set_mode; + EMS_TxTelegram.type_validate = EMS_TxTelegram.type; + EMS_TxTelegram.comparisonPostRead = EMS_TYPE_RC20StatusMessage; + } else if (model_id == EMS_MODEL_RC30) { - EMS_TxTelegram.type = EMS_TYPE_RC30Set; - EMS_TxTelegram.offset = EMS_OFFSET_RC30Set_mode; + EMS_TxTelegram.type = EMS_TYPE_RC30Set; + EMS_TxTelegram.offset = EMS_OFFSET_RC30Set_mode; + EMS_TxTelegram.type_validate = EMS_TxTelegram.type; + EMS_TxTelegram.comparisonPostRead = EMS_TYPE_RC30StatusMessage; + } else if ((model_id == EMS_MODEL_RC35) || (model_id == EMS_MODEL_ES73)) { - EMS_TxTelegram.type = (hc == 2) ? EMS_TYPE_RC35Set_HC2 : EMS_TYPE_RC35Set_HC1; - EMS_TxTelegram.offset = EMS_OFFSET_RC35Set_mode; + EMS_TxTelegram.type = (hc == 2) ? EMS_TYPE_RC35Set_HC2 : EMS_TYPE_RC35Set_HC1; + EMS_TxTelegram.offset = EMS_OFFSET_RC35Set_mode; + EMS_TxTelegram.type_validate = EMS_TxTelegram.type; + if (hc == 1) { + EMS_TxTelegram.comparisonPostRead = EMS_TYPE_RC35StatusMessage_HC1; + } else { + EMS_TxTelegram.comparisonPostRead = EMS_TYPE_RC35StatusMessage_HC2; + } + + } 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 + EMS_TxTelegram.offset = EMS_OFFSET_RCPLUSSet_mode; + + // TODO: commented out because it's too fast + // EMS_TxTelegram.type_validate = EMS_TYPE_RCPLUSStatusMessage; // 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; // after write, do a full fetch of all values } - EMS_TxTelegram.type_validate = EMS_TxTelegram.type; // callback to EMS_TYPE_RC30Temperature to fetch temps - EMS_TxTelegram.comparisonOffset = EMS_TxTelegram.offset; - EMS_TxTelegram.comparisonValue = EMS_TxTelegram.dataValue; - EMS_TxTelegram.comparisonPostRead = EMS_TxTelegram.type; - EMS_TxTelegram.forceRefresh = false; // send to MQTT is done automatically in 0xA8 process + 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); } @@ -2684,10 +2828,10 @@ void ems_setWarmTapWaterActivated(bool activated) { /** * Start up sequence for UBA Master, hopefully to initialize a handshake - * Still experimental + * Still experimental and not used yet! */ void ems_startupTelegrams() { - if ((EMS_Sys_Status.emsTxDisabled) || (!EMS_Sys_Status.emsBusConnected)) { + if ((ems_getTxDisabled()) || (!ems_getBusConnected())) { myDebug_P(PSTR("Unable to send startup sequence when in listen mode or the bus is disabled")); } diff --git a/src/ems.h b/src/ems.h index 800577200..d1e5a12a0 100644 --- a/src/ems.h +++ b/src/ems.h @@ -11,12 +11,14 @@ #pragma once #include +#include // std::list /* debug helper for logic analyzer * create marker puls on GPIOx * ° for Rx, we use GPIO14 * ° for Tx, we use GPIO12 */ + // clang-format off #ifdef LOGICANALYZER #define RX_MARK_PIN 14 @@ -29,39 +31,39 @@ #define GPIO_H(mask) (GPIO_REG_WRITE(GPIO_OUT_W1TS_ADDRESS, (mask))) #define GPIO_L(mask) (GPIO_REG_WRITE(GPIO_OUT_W1TC_ADDRESS, (mask))) -#define RX_PULSE(pulse) \ - do { \ - GPIO_H(RX_MARK_MASK); \ - delayMicroseconds(pulse); \ - GPIO_L(RX_MARK_MASK); \ +#define RX_PULSE(pulse) \ + do { \ + GPIO_H(RX_MARK_MASK); \ + delayMicroseconds(pulse); \ + GPIO_L(RX_MARK_MASK); \ } while (0) -#define TX_PULSE(pulse) \ - do { \ - GPIO_H(TX_MARK_MASK); \ - delayMicroseconds(pulse); \ - GPIO_L(TX_MARK_MASK); \ +#define TX_PULSE(pulse) \ + do { \ + GPIO_H(TX_MARK_MASK); \ + delayMicroseconds(pulse); \ + GPIO_L(TX_MARK_MASK); \ } while (0) -#define LA_PULSE(pulse) \ - do { \ - GPIO_H(MARKERS_MASK); \ - delayMicroseconds(pulse); \ - GPIO_L(MARKERS_MASK); \ +#define LA_PULSE(pulse) \ + do { \ + GPIO_H(MARKERS_MASK); \ + delayMicroseconds(pulse); \ + GPIO_L(MARKERS_MASK); \ } while (0) -#define INIT_MARKERS(void) \ - do { \ - pinMode(RX_MARK_PIN, OUTPUT); \ - pinMode(TX_MARK_PIN, OUTPUT); \ - GPIO_L(MARKERS_MASK); \ +#define INIT_MARKERS(void) \ + do { \ + pinMode(RX_MARK_PIN, OUTPUT); \ + pinMode(TX_MARK_PIN, OUTPUT); \ + GPIO_L(MARKERS_MASK); \ } while (0) #else -#define RX_PULSE(pulse) \ +#define RX_PULSE(pulse) \ {} -#define TX_PULSE(pulse) \ +#define TX_PULSE(pulse) \ {} -#define LA_PULSE(pulse) \ +#define LA_PULSE(pulse) \ {} -#define INIT_MARKERS(void) \ +#define INIT_MARKERS(void) \ {} #define RX_MARK_MASK #define TX_MARK_MASK @@ -113,6 +115,13 @@ //#define EMS_SYS_LOGGING_DEFAULT EMS_SYS_LOGGING_VERBOSE #define EMS_SYS_LOGGING_DEFAULT EMS_SYS_LOGGING_NONE +// define the model types which get rendered to html colors in the web interface +#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 + /* EMS UART transfer status */ typedef enum { EMS_RX_STATUS_IDLE, @@ -124,7 +133,8 @@ typedef enum { EMS_TX_STATUS_IDLE, // ready EMS_TX_STATUS_WAIT, // waiting for response from last Tx EMS_TX_WTD_TIMEOUT, // watchdog timeout during send - EMS_TX_BRK_DETECT // incoming BRK during Tx + EMS_TX_BRK_DETECT, // incoming BRK during Tx + EMS_TX_REV_DETECT // waiting to detect reverse bit } _EMS_TX_STATUS; #define EMS_TX_SUCCESS 0x01 // EMS single byte after a Tx Write indicating a success @@ -145,15 +155,16 @@ typedef enum { 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 - EMS_SYS_LOGGING_VERBOSE // everything + EMS_SYS_LOGGING_VERBOSE, // everything + EMS_SYS_LOGGING_JABBER // lots of debug output... } _EMS_SYS_LOGGING; // status/counters since last power on typedef struct { _EMS_RX_STATUS emsRxStatus; _EMS_TX_STATUS emsTxStatus; - uint16_t emsRxPgks; // received - uint16_t emsTxPkgs; // sent + uint16_t emsRxPgks; // # successfull received + uint16_t emsTxPkgs; // # successfull sent uint16_t emxCrcErr; // CRC errors bool emsPollEnabled; // flag enable the response to poll messages _EMS_SYS_LOGGING emsLogging; // logging @@ -164,8 +175,9 @@ typedef struct { bool emsTxCapable; // able to send via Tx bool emsTxDisabled; // true to prevent all Tx uint8_t txRetryCount; // # times the last Tx was re-sent - bool emsReverse; // if true, poll logic is reversed - uint8_t emsTxMode; // handles Tx logic + uint8_t emsIDMask; // Buderus: 0x00, Junkers: 0x80 + uint8_t emsPollAck[1]; // acknowledge buffer for Poll + uint8_t emsTxMode; // Tx mode 1, 2 or 3 } _EMS_Sys_Status; // The Tx send package @@ -174,12 +186,12 @@ typedef struct { uint8_t dest; uint16_t type; uint8_t offset; - uint8_t length; // full length of complete telegram + uint8_t length; // full length of complete telegram, including CRC uint8_t dataValue; // value to validate against uint16_t type_validate; // type to call after a successful Write command - uint8_t comparisonValue; // value to compare against during a validate - uint8_t comparisonOffset; // offset of where the byte is we want to compare too later - uint16_t comparisonPostRead; // after a successful write call this to read from this type ID + 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 uint8_t data[EMS_MAX_TELEGRAM_LENGTH]; @@ -187,19 +199,20 @@ typedef struct { // 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 + 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 } _EMS_RxTelegram; -// default empty Tx +// default empty Tx, must match struct const _EMS_TxTelegram EMS_TX_TELEGRAM_NEW = { EMS_TX_TELEGRAM_INIT, // action EMS_ID_NONE, // dest @@ -220,25 +233,25 @@ const _EMS_TxTelegram EMS_TX_TELEGRAM_NEW = { typedef struct { uint8_t product_id; char model_string[50]; -} _Boiler_Type; +} _Boiler_Device; typedef struct { uint8_t product_id; uint8_t device_id; char model_string[50]; -} _SolarModule_Type; +} _SolarModule_Device; typedef struct { uint8_t product_id; uint8_t device_id; char model_string[50]; -} _Other_Type; +} _Other_Device; typedef struct { uint8_t product_id; uint8_t device_id; char model_string[50]; -} _HeatPump_Type; +} _HeatPump_Device; typedef struct { uint8_t model_id; @@ -246,15 +259,16 @@ typedef struct { uint8_t device_id; char model_string[50]; bool write_supported; -} _Thermostat_Type; +} _Thermostat_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_Type; +} _Generic_Device; /* * Telegram package defintions @@ -390,6 +404,7 @@ typedef struct { } _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); @@ -403,28 +418,27 @@ void ems_testTelegram(uint8_t test_num); void ems_startupTelegrams(); bool ems_checkEMSBUSAlive(); void ems_clearDeviceList(); -void ems_setTxMode(uint8_t mode); -void ems_setThermostatTemp(float temperature, uint8_t temptype = 0); -void ems_setThermostatMode(uint8_t mode); -void ems_setThermostatHC(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); -void ems_setEmsRefreshed(bool b); -void ems_setWarmWaterModeComfort(uint8_t comfort); -void ems_setModels(); -void ems_setTxDisabled(bool b); -bool ems_getTxDisabled(); -uint8_t ems_getTxMode(); +void ems_setThermostatTemp(float temperature, uint8_t temptype = 0); +void ems_setThermostatMode(uint8_t mode); +void ems_setThermostatHC(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); +void ems_setEmsRefreshed(bool b); +void ems_setWarmWaterModeComfort(uint8_t comfort); +void ems_setModels(); +void ems_setTxDisabled(bool b); +bool ems_getTxDisabled(); +void ems_setTxMode(uint8_t mode); -char * ems_getThermostatDescription(char * buffer); -char * ems_getBoilerDescription(char * buffer); -char * ems_getSolarModuleDescription(char * buffer); -char * ems_getHeatPumpDescription(char * buffer); +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_getThermostatValues(); void ems_getBoilerValues(); void ems_getSolarModuleValues(); @@ -457,3 +471,5 @@ extern _EMS_Thermostat EMS_Thermostat; extern _EMS_SolarModule EMS_SolarModule; extern _EMS_HeatPump EMS_HeatPump; extern _EMS_Other EMS_Other; + +extern std::list<_Generic_Device> Devices; diff --git a/src/ems_devices.h b/src/ems_devices.h index 40c1d8a07..85273de02 100644 --- a/src/ems_devices.h +++ b/src/ems_devices.h @@ -13,12 +13,12 @@ #include "ems.h" /* - * Common + * Common Type */ #define EMS_TYPE_Version 0x02 /* - * Boiler... + * Boiler Telegram Types... */ #define EMS_TYPE_UBAMonitorFast 0x18 // is an automatic monitor broadcast #define EMS_TYPE_UBAMonitorSlow 0x19 // is an automatic monitor broadcast @@ -40,7 +40,7 @@ #define EMS_OFFSET_UBASetPoints_flowtemp 0 // flow temp -// Other +// SM and HP Types #define EMS_TYPE_SM10Monitor 0x97 // SM10Monitor #define EMS_TYPE_SM100Monitor 0x0262 // SM100Monitor #define EMS_TYPE_SM100Status 0x0264 // SM100Status @@ -54,7 +54,7 @@ #define EMS_OFFSET_ISM1Set_MaxBoilerTemp 6 // position of max boiler temp e.g. 50 in the following example: 90 30 FF 06 00 01 50 (CRC=2C) /* - * Thermostats... + * Thermostat Types */ // Common for all thermostats @@ -63,41 +63,46 @@ // RC10 specific #define EMS_TYPE_RC10StatusMessage 0xB1 // is an automatic thermostat broadcast giving us temps -#define EMS_TYPE_RC10Set 0xB0 // for setting values like temp and mode -#define EMS_OFFSET_RC10Set_temp 4 // position of thermostat setpoint temperature #define EMS_OFFSET_RC10StatusMessage_setpoint 1 // setpoint temp #define EMS_OFFSET_RC10StatusMessage_curr 2 // current temp +#define EMS_TYPE_RC10Set 0xB0 // for setting values like temp and mode +#define EMS_OFFSET_RC10Set_temp 4 // position of thermostat setpoint temperature + // RC20 specific #define EMS_TYPE_RC20StatusMessage 0x91 // is an automatic thermostat broadcast giving us temps -#define EMS_TYPE_RC20Set 0xA8 // for setting values like temp and mode -#define EMS_OFFSET_RC20Set_mode 23 // position of thermostat mode -#define EMS_OFFSET_RC20Set_temp 28 // position of thermostat setpoint temperature #define EMS_OFFSET_RC20StatusMessage_setpoint 1 // setpoint temp #define EMS_OFFSET_RC20StatusMessage_curr 2 // current temp +#define EMS_TYPE_RC20Set 0xA8 // for setting values like temp and mode +#define EMS_OFFSET_RC20Set_mode 23 // position of thermostat mode +#define EMS_OFFSET_RC20Set_temp 28 // position of thermostat setpoint temperature + // RC30 specific #define EMS_TYPE_RC30StatusMessage 0x41 // is an automatic thermostat broadcast giving us temps -#define EMS_TYPE_RC30Set 0xA7 // for setting values like temp and mode -#define EMS_OFFSET_RC30Set_mode 23 // position of thermostat mode -#define EMS_OFFSET_RC30Set_temp 28 // position of thermostat setpoint temperature #define EMS_OFFSET_RC30StatusMessage_setpoint 1 // setpoint temp #define EMS_OFFSET_RC30StatusMessage_curr 2 // current temp +#define EMS_TYPE_RC30Set 0xA7 // for setting values like temp and mode +#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 -#define EMS_TYPE_RC35Set_HC1 0x3D // for setting values like temp and mode (Working mode HC1) -#define EMS_TYPE_RC35Set_HC2 0x47 // for setting values like temp and mode (Working mode HC2) #define EMS_OFFSET_RC35StatusMessage_setpoint 2 // desired temp #define EMS_OFFSET_RC35StatusMessage_curr 3 // current temp -#define EMS_OFFSET_RC35Set_mode 7 // position of thermostat mode -#define EMS_OFFSET_RC35Set_temp_day 2 // position of thermostat setpoint temperature for day time -#define EMS_OFFSET_RC35Set_temp_night 1 // position of thermostat setpoint temperature for night time -#define EMS_OFFSET_RC35Get_mode_day 1 // position of thermostat day mode -#define EMS_OFFSET_RC35Set_temp_holiday 3 // temp during holiday 0x47 -#define EMS_OFFSET_RC35Set_heatingtype 0 // floor heating = 3 0x47 -#define EMS_OFFSET_RC35Set_circuitcalctemp 14 // calculated circuit temperature 0x48 +#define EMS_OFFSET_RC35StatusMessage_mode 1 //day mode + +#define EMS_TYPE_RC35Set_HC1 0x3D // for setting values like temp and mode (Working mode HC1) +#define EMS_TYPE_RC35Set_HC2 0x47 // for setting values like temp and mode (Working mode HC2) +#define EMS_OFFSET_RC35Set_mode 7 // position of thermostat mode +#define EMS_OFFSET_RC35Set_temp_day 2 // position of thermostat setpoint temperature for day time +#define EMS_OFFSET_RC35Set_temp_night 1 // position of thermostat setpoint temperature for night time +#define EMS_OFFSET_RC35Set_temp_holiday 3 // temp during holiday 0x47 +#define EMS_OFFSET_RC35Set_heatingtype 0 // floor heating = 3 0x47 +#define EMS_OFFSET_RC35Set_circuitcalctemp 14 // calculated circuit temperature 0x48 // Easy specific #define EMS_TYPE_EasyStatusMessage 0x0A // reading values on an Easy Thermostat @@ -105,14 +110,21 @@ #define EMS_OFFSET_EasyStatusMessage_curr 8 // current temp // RC1010, RC310 and RC300 specific (EMS Plus) -#define EMS_TYPE_RCPLUSStatusMessage 0x01A5 // is an automatic thermostat broadcast giving us temps -#define EMS_TYPE_RCPLUSStatusHeating 0x01B9 // heating mode -#define EMS_TYPE_RCPLUSStatusMode 0x1AF // summer/winter mode -#define EMS_TYPE_RCPLUSSet 0x03 // setpoint temp message -#define EMS_OFFSET_RCPLUSStatusMessage_setpoint 3 // setpoint temp -#define EMS_OFFSET_RCPLUSStatusMessage_curr 0 // current temp -#define EMS_OFFSET_RCPLUSGet_mode_day 8 // day/night mode -#define EMS_OFFSET_RCPLUSStatusMessage_mode 0x0A // thermostat mode (auto, manual) +#define EMS_TYPE_RCPLUSStatusMessage 0x01A5 // is an automatic thermostat broadcast giving us temps, also reading +#define EMS_TYPE_RCPLUSStatusMode 0x1AF // summer/winter mode +#define EMS_OFFSET_RCPLUSStatusMessage_mode 10 // thermostat mode (auto, manual) +#define EMS_OFFSET_RCPLUSStatusMessage_setpoint 3 // setpoint temp +#define EMS_OFFSET_RCPLUSStatusMessage_curr 0 // current temp +#define EMS_OFFSET_RCPLUSStatusMessage_currsetpoint 6 // target setpoint temp + +#define EMS_TYPE_RCPLUSSet 0x01B9 // setpoint temp message and mode +#define EMS_OFFSET_RCPLUSSet_mode 0 // operation mode(Auto=0xFF, Manual=0x00) +#define EMS_OFFSET_RCPLUSSet_temp_comfort3 1 // comfort3 level +#define EMS_OFFSET_RCPLUSSet_temp_comfort2 2 // comfort2 level +#define EMS_OFFSET_RCPLUSSet_temp_comfort1 3 // comfort1 level +#define EMS_OFFSET_RCPLUSSet_temp_eco 4 // eco level +#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) #define EMS_TYPE_JunkersStatusMessage 0x6F // is an automatic thermostat broadcast giving us temps @@ -120,7 +132,7 @@ #define EMS_OFFSET_JunkersStatusMessage_curr 4 // current temp -// Known EMS types +// Known EMS devices typedef enum { EMS_MODEL_NONE, // unset EMS_MODEL_ALL, // common for all devices @@ -157,7 +169,7 @@ typedef enum { // 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_Type Boiler_Types[] = { +const _Boiler_Device Boiler_Devices[] = { {72, "MC10 Module"}, {123, "Buderus GB172/Nefit Trendline/Junkers Cerapur"}, @@ -175,7 +187,7 @@ const _Boiler_Type Boiler_Types[] = { * Known Solar Module types * format is PRODUCT ID, DEVICE ID, DESCRIPTION */ -const _SolarModule_Type SolarModule_Types[] = { +const _SolarModule_Device SolarModule_Devices[] = { {EMS_PRODUCTID_SM10, EMS_ID_SM, "SM10 Solar Module"}, {EMS_PRODUCTID_SM100, EMS_ID_SM, "SM100 Solar Module"}, @@ -185,7 +197,7 @@ const _SolarModule_Type SolarModule_Types[] = { // Other EMS devices which are not considered boilers, thermostats or solar modules // format is PRODUCT ID, DEVICE ID, DESCRIPTION -const _Other_Type Other_Types[] = { +const _Other_Device Other_Devices[] = { {69, 0x21, "MM10 Mixer Module"}, {71, 0x11, "WM10 Switch Module"}, @@ -207,13 +219,13 @@ const _Other_Type Other_Types[] = { // heatpump // format is PRODUCT ID, DEVICE ID, DESCRIPTION -const _HeatPump_Type HeatPump_Types[] = {{252, EMS_ID_HP, "HeatPump Module"}}; +const _HeatPump_Device HeatPump_Devices[] = {{252, EMS_ID_HP, "HeatPump Module"}}; /* * Known thermostat types and their capabilities * format is MODEL_ID, PRODUCT ID, DEVICE ID, DESCRIPTION */ -const _Thermostat_Type Thermostat_Types[] = { +const _Thermostat_Device Thermostat_Devices[] = { // Easy devices - not currently supporting write operations {EMS_MODEL_EASY, 202, 0x18, "Logamatic TC100/Nefit Moduline Easy", EMS_THERMOSTAT_WRITE_NO}, diff --git a/src/emsuart.cpp b/src/emsuart.cpp index a701d25c3..7ecfe6351 100644 --- a/src/emsuart.cpp +++ b/src/emsuart.cpp @@ -7,12 +7,12 @@ #include "emsuart.h" #include "ems.h" -#include #include _EMSRxBuf * pEMSRxBuf; _EMSRxBuf * paEMSRxBuf[EMS_MAXBUFFERS]; -uint8_t emsRxBufIdx = 0; +uint8_t emsRxBufIdx = 0; +uint8_t phantomBreak = 0; os_event_t recvTaskQueue[EMSUART_recvTaskQueueLen]; // our Rx queue @@ -22,7 +22,7 @@ os_event_t recvTaskQueue[EMSUART_recvTaskQueueLen]; // our Rx queue // static void emsuart_rx_intr_handler(void * para) { static uint8_t length; - static uint8_t uart_buffer[EMS_MAXBUFFERSIZE]; + static uint8_t uart_buffer[EMS_MAXBUFFERSIZE + 2]; // is a new buffer? if so init the thing for a new telegram if (EMS_Sys_Status.emsRxStatus == EMS_RX_STATUS_IDLE) { @@ -33,7 +33,9 @@ static void emsuart_rx_intr_handler(void * para) { // fill IRQ buffer, by emptying Rx FIFO if (USIS(EMSUART_UART) & ((1 << UIFF) | (1 << UITO) | (1 << UIBD))) { while ((USS(EMSUART_UART) >> USRXC) & 0xFF) { - uart_buffer[length++] = USF(EMSUART_UART); + uint8_t rx = USF(EMSUART_UART); + if (length < EMS_MAXBUFFERSIZE) + uart_buffer[length++] = rx; } // clear Rx FIFO full and Rx FIFO timeout interrupts @@ -46,10 +48,11 @@ static void emsuart_rx_intr_handler(void * para) { ETS_UART_INTR_DISABLE(); // disable all interrupts and clear them USIC(EMSUART_UART) = (1 << UIBD); // INT clear the BREAK detect interrupt - pEMSRxBuf->length = length; - os_memcpy((void *)pEMSRxBuf->buffer, (void *)&uart_buffer, length); // copy data into transfer buffer, including the BRK 0x00 at the end - EMS_Sys_Status.emsRxStatus = EMS_RX_STATUS_IDLE; // set the status flag stating BRK has been received and we can start a new package - ETS_UART_INTR_ENABLE(); // re-enable UART interrupts + pEMSRxBuf->length = (length > EMS_MAXBUFFERSIZE) ? EMS_MAXBUFFERSIZE : length; + os_memcpy((void *)pEMSRxBuf->buffer, (void *)&uart_buffer, pEMSRxBuf->length); // copy data into transfer buffer, including the BRK 0x00 at the end + length = 0; + EMS_Sys_Status.emsRxStatus = EMS_RX_STATUS_IDLE; // set the status flag stating BRK has been received and we can start a new package + ETS_UART_INTR_ENABLE(); // re-enable UART interrupts system_os_post(EMSUART_recvTaskPrio, 0, 0); // call emsuart_recvTask() at next opportunity RX_PULSE(EMSUART_BIT_TIME / 2); @@ -67,18 +70,21 @@ static void ICACHE_FLASH_ATTR emsuart_recvTask(os_event_t * events) { uint8_t length = pCurrent->length; // number of bytes including the BRK at the end pCurrent->length = 0; - // validate and transmit the EMS buffer, excluding the BRK + if (phantomBreak) { + phantomBreak = 0; + length--; // remove phantom break from Rx buffer + } + if (length == 2) { RX_PULSE(20); // it's a poll or status code, single byte and ok to send on ems_parseTelegram((uint8_t *)pCurrent->buffer, 1); - } else if ((length > 4) && (length <= EMS_MAXBUFFERSIZE + 1) && (pCurrent->buffer[length - 2] != 0x00)) { + } else if ((length > 4) && (length <= EMS_MAXBUFFERSIZE + 1)) { // ignore double BRK at the end, possibly from the Tx loopback // also telegrams with no data value RX_PULSE(40); ems_parseTelegram((uint8_t *)pCurrent->buffer, length - 1); // transmit EMS buffer, excluding the BRK } - // memset(pCurrent->buffer, 0x00, EMS_MAXBUFFERSIZE); // wipe memory just to be safe } /* @@ -122,10 +128,10 @@ void ICACHE_FLASH_ATTR emsuart_init() { // UCFFT = RX FIFO Full Threshold (7 bit) = want this to be 31 for 32 bytes of buffer (default was 127) // see https://www.espressif.com/sites/default/files/documentation/esp8266-technical_reference_en.pdf // - // change: we set UCFFT to 1 to get an immediate indicator about incoming trafffic. + // change: we set UCFFT to 1 to get an immediate indicator about incoming traffic. // Otherwise, we're only noticed by UCTOT or RxBRK! USC1(EMSUART_UART) = 0; // reset config first - USC1(EMSUART_UART) = (0x01 << UCFFT) | (0x01 << UCTOT) | (1 << UCTOE); // enable interupts + USC1(EMSUART_UART) = (0x01 << UCFFT) | (0x01 << UCTOT) | (0 << UCTOE); // enable interupts // set interrupts for triggers USIC(EMSUART_UART) = 0xFFFF; // clear all interupts @@ -133,7 +139,8 @@ void ICACHE_FLASH_ATTR emsuart_init() { // enable rx break, fifo full and timeout. // but not frame error UIFR (because they are too frequent) or overflow UIOF because our buffer is only max 32 bytes - USIE(EMSUART_UART) = (1 << UIBD) | (1 << UIFF) | (1 << UITO); + // change: we don't care about Rx Timeout - it may lead to wrong readouts + USIE(EMSUART_UART) = (1 << UIBD) | (1 << UIFF) | (0 << UITO); // set up interrupt callbacks for Rx system_os_task(emsuart_recvTask, EMSUART_recvTaskPrio, recvTaskQueue, EMSUART_recvTaskQueueLen); @@ -142,7 +149,7 @@ void ICACHE_FLASH_ATTR emsuart_init() { system_set_os_print(0); // swap Rx and Tx pins to use GPIO13 (D7) and GPIO15 (D8) respectively - //system_uart_swap(); + system_uart_swap(); ETS_UART_INTR_ATTACH(emsuart_rx_intr_handler, nullptr); ETS_UART_INTR_ENABLE(); @@ -199,16 +206,15 @@ void ICACHE_FLASH_ATTR emsuart_tx_brk() { */ _EMS_TX_STATUS ICACHE_FLASH_ATTR emsuart_tx_buffer(uint8_t * buf, uint8_t len) { _EMS_TX_STATUS result = EMS_TX_STATUS_OK; + + if (EMS_Sys_Status.emsLogging == EMS_SYS_LOGGING_JABBER) { + ems_dumpBuffer("emsuart_tx_buffer: ", buf, len); // validate and transmit the EMS buffer, excluding the BRK + } + if (len) { LA_PULSE(50); - // temp code until we get mode 2 working without resets - if (EMS_Sys_Status.emsTxMode == 0) { // classic mode logic - for (uint8_t i = 0; i < len; i++) { - TX_PULSE(EMSUART_BIT_TIME / 4); - USF(EMSUART_UART) = buf[i]; - } - emsuart_tx_brk(); // send - } else if (EMS_Sys_Status.emsTxMode == 1) { // With extra tx delay for EMS+ + + if (EMS_Sys_Status.emsTxMode == 2) { // With extra tx delay for EMS+ for (uint8_t i = 0; i < len; i++) { TX_PULSE(EMSUART_BIT_TIME / 4); USF(EMSUART_UART) = buf[i]; @@ -228,9 +234,8 @@ _EMS_TX_STATUS ICACHE_FLASH_ATTR emsuart_tx_buffer(uint8_t * buf, uint8_t len) { delayMicroseconds(EMSUART_TX_WAIT_BYTE - EMSUART_TX_LAG + EMSUART_TX_WAIT_GAP); } emsuart_tx_brk(); // send - } else if (EMS_Sys_Status.emsTxMode == 2) { + } else if (EMS_Sys_Status.emsTxMode == 1) { /* - * * based on code from https://github.com/proddy/EMS-ESP/issues/103 by @susisstrolch * we emit the whole telegram, with Rx interrupt disabled, collecting busmaster response in FIFO. * after sending the last char we poll the Rx status until either @@ -254,9 +259,9 @@ _EMS_TX_STATUS ICACHE_FLASH_ATTR emsuart_tx_buffer(uint8_t * buf, uint8_t len) { // shorter busy poll... #define EMSUART_BUSY_WAIT (EMSUART_BIT_TIME / 8) -#define EMS_TX_TO_COUNT ((20 + 10000 / EMSUART_BIT_TIME) * 8) +#define EMS_TX_TO_CHARS (2 + 20) +#define EMS_TX_TO_COUNT ((EMS_TX_TO_CHARS)*10 * 8) uint16_t wdc = EMS_TX_TO_COUNT; - ETS_UART_INTR_DISABLE(); // disable rx interrupt // clear Rx status register @@ -272,7 +277,6 @@ _EMS_TX_STATUS ICACHE_FLASH_ATTR emsuart_tx_buffer(uint8_t * buf, uint8_t len) { USF(EMSUART_UART) = buf[i++]; // send each Tx byte // wait for echo from busmaster GPIO_L(TX_MARK_MASK); - while (((USS(EMSUART_UART) >> USRXC) & 0xFF) == _usrxc) { delayMicroseconds(EMSUART_BUSY_WAIT); // burn CPU cycles... if (--wdc == 0) { @@ -301,11 +305,13 @@ _EMS_TX_STATUS ICACHE_FLASH_ATTR emsuart_tx_buffer(uint8_t * buf, uint8_t len) { // wait until BRK detected... while (!(USIR(EMSUART_UART) & (1 << UIBD))) { - delayMicroseconds(EMSUART_BUSY_WAIT); + // delayMicroseconds(EMSUART_BUSY_WAIT); + delayMicroseconds(EMSUART_BIT_TIME); } USC0(EMSUART_UART) &= ~((1 << UCBRK) | (1 << UCLBE)); // disable loopback & clear USIC(EMSUART_UART) = (1 << UIBD); // clear BRK detect IRQ + phantomBreak = 1; } GPIO_L(TX_MARK_MASK); } @@ -314,16 +320,3 @@ _EMS_TX_STATUS ICACHE_FLASH_ATTR emsuart_tx_buffer(uint8_t * buf, uint8_t len) { } return result; } - -/* - * Send the Poll (our own ID) to Tx as a single byte and end with a - */ -void ICACHE_FLASH_ATTR emsuart_tx_poll() { - static uint8_t buf[1]; - if (EMS_Sys_Status.emsReverse) { - buf[0] = {EMS_ID_ME | 0x80}; - } else { - buf[0] = {EMS_ID_ME}; - } - emsuart_tx_buffer(buf, 1); -} diff --git a/src/emsuart.h b/src/emsuart.h index 0777dcfd5..0859aaf2f 100644 --- a/src/emsuart.h +++ b/src/emsuart.h @@ -7,7 +7,6 @@ */ #pragma once -#include #include #define EMSUART_UART 0 // UART 0 @@ -37,4 +36,3 @@ void ICACHE_FLASH_ATTR emsuart_init(); void ICACHE_FLASH_ATTR emsuart_stop(); void ICACHE_FLASH_ATTR emsuart_start(); _EMS_TX_STATUS ICACHE_FLASH_ATTR emsuart_tx_buffer(uint8_t * buf, uint8_t len); -void ICACHE_FLASH_ATTR emsuart_tx_poll(); diff --git a/src/my_config.h b/src/my_config.h index 1dc9c16b4..15f06b4d2 100644 --- a/src/my_config.h +++ b/src/my_config.h @@ -10,26 +10,14 @@ #include "ems.h" -// MQTT base name -#define MQTT_BASE "home" // all MQTT topics are prefix with this string, in the format // - -// MQTT general settings -#define MQTT_WILL_TOPIC "status" // for last will & testament topic name -#define MQTT_WILL_ONLINE_PAYLOAD "online" // for last will & testament payload -#define MQTT_WILL_OFFLINE_PAYLOAD "offline" // for last will & testament payload -#define MQTT_RETAIN false -#define MQTT_KEEPALIVE 120 // 2 minutes -#define MQTT_QOS 1 -#define MQTT_MAX_SIZE 700 // max size of a JSON object. See https://arduinojson.org/v6/assistant/ - // MQTT for thermostat #define TOPIC_THERMOSTAT_DATA "thermostat_data" // for sending thermostat values to MQTT #define TOPIC_THERMOSTAT_CMD_TEMP "thermostat_cmd_temp" // for received thermostat temp changes via MQTT #define TOPIC_THERMOSTAT_CMD_MODE "thermostat_cmd_mode" // for received thermostat mode changes via MQTT #define TOPIC_THERMOSTAT_CMD_HC "thermostat_cmd_hc" // for received thermostat hc number changes via MQTT -#define TOPIC_THERMOSTAT_CMD_DAYTEMP "thermostat_daytemp" // RC35 specific -#define TOPIC_THERMOSTAT_CMD_NIGHTTEMP "thermostat_nighttemp" // RC35 specific -#define TOPIC_THERMOSTAT_CMD_HOLIDAYTEMP "thermostat_holidayttemp" // RC35 specific +#define TOPIC_THERMOSTAT_CMD_DAYTEMP "thermostat_daytemp" // for received thermostat day temp (RC35 specific) +#define TOPIC_THERMOSTAT_CMD_NIGHTTEMP "thermostat_nighttemp" // for received thermostat night temp (RC35 specific) +#define TOPIC_THERMOSTAT_CMD_HOLIDAYTEMP "thermostat_holidayttemp" // for received thermostat holiday temp (RC35 specific) #define THERMOSTAT_CURRTEMP "thermostat_currtemp" // current temperature #define THERMOSTAT_SELTEMP "thermostat_seltemp" // selected temperature #define THERMOSTAT_HC "thermostat_hc" // which heating circuit number @@ -41,13 +29,13 @@ #define THERMOSTAT_CIRCUITCALCTEMP "thermostat_circuitcalctemp" // RC35 specific // MQTT for boiler -#define TOPIC_BOILER_DATA "boiler_data" // for sending boiler values to MQTT -#define TOPIC_BOILER_TAPWATER_ACTIVE "tapwater_active" // if hot tap water is running -#define TOPIC_BOILER_HEATING_ACTIVE "heating_active" // if heating is on -#define TOPIC_BOILER_WWACTIVATED "wwactivated" // for receiving MQTT message to change water on/off -#define TOPIC_BOILER_CMD_WWTEMP "boiler_cmd_wwtemp" // for received boiler wwtemp changes via MQTT -#define TOPIC_BOILER_CMD_COMFORT "boiler_cmd_comfort" // for received boiler ww comfort setting via MQTT -#define TOPIC_BOILER_CMD_FLOWTEMP "boiler_cmd_flowtemp" // for received boiler flowtemp value via MQTT +#define TOPIC_BOILER_DATA "boiler_data" // for sending boiler values to MQTT +#define TOPIC_BOILER_TAPWATER_ACTIVE "tapwater_active" // if hot tap water is running +#define TOPIC_BOILER_HEATING_ACTIVE "heating_active" // if heating is on +#define TOPIC_BOILER_CMD_WWACTIVATED "boiler_cmd_wwactivated" // for received message to change water on/off +#define TOPIC_BOILER_CMD_WWTEMP "boiler_cmd_wwtemp" // for received boiler wwtemp changes via MQTT +#define TOPIC_BOILER_CMD_COMFORT "boiler_cmd_comfort" // for received boiler ww comfort setting via MQTT +#define TOPIC_BOILER_CMD_FLOWTEMP "boiler_cmd_flowtemp" // for received boiler flowtemp value via MQTT // MQTT for SM10/SM100 Solar Module #define TOPIC_SM_DATA "sm_data" // topic name @@ -74,28 +62,3 @@ // MQTT for EXTERNAL SENSORS #define TOPIC_EXTERNAL_SENSORS "sensors" // for sending sensor values to MQTT #define PAYLOAD_EXTERNAL_SENSORS "temp_%d" // for formatting the payload for each external dallas sensor - - -//////////////////////////////////////////////////////////////////////////////////////////////////// -// THESE DEFAULT VALUES CAN ALSO BE SET AND STORED WITHTIN THE APPLICATION (see 'set' command) // -//////////////////////////////////////////////////////////////////////////////////////////////////// - -// Set LED pin used for showing the EMS bus connection status. Solid means EMS bus working, flashing is an error -// can be either the onboard LED on the ESP8266 (LED_BULLETIN) or external via an external pull-up LED (e.g. D1 on a bbqkees' board) -// can be enabled and disabled via the 'set led' command and pin set by 'set led_gpio' -#define EMSESP_LED_GPIO LED_BUILTIN - -#ifdef LOGICANALYZER -#define EMSESP_DALLAS_GPIO D1 -#define EMSESP_DALLAS_PARASITE false -#else -// 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 -#endif - -// By default the EMS bus will be scanned for known devices based on the product ids in ems_devices.h -// You can override the Thermostat and Boiler types here -#define EMSESP_BOILER_TYPE EMS_ID_NONE -#define EMSESP_THERMOSTAT_TYPE EMS_ID_NONE diff --git a/src/test_data.h b/src/test_data.h index 0f9edf3b7..c47805977 100644 --- a/src/test_data.h +++ b/src/test_data.h @@ -52,7 +52,9 @@ static const char * TEST_DATA[] = { "38 10 FF 00 03 2B 00 C7 07 C3 01", // test 47 - heatpump Enviline "08 0B 19 00 00 F7 80 00 80 00 00 00 00 00 03 58 97 0C 7B 1F 00 00 00 06 C4 DF 02 64 48 80 00", // test 48 - outdoor temp check "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 + "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 }; diff --git a/src/version.h b/src/version.h index 8163ddfed..64e2e8623 100644 --- a/src/version.h +++ b/src/version.h @@ -1,10 +1 @@ -/** - * - * Paul Derbyshire - https://github.com/proddy/EMS-ESP - */ - -#pragma once - -#define APP_NAME "EMS-ESP" -#define APP_VERSION "1.8.3" -#define APP_HOSTNAME "ems-esp" +#define APP_VERSION "1.9.0" diff --git a/src/webh/.gitkeep b/src/webh/.gitkeep new file mode 100644 index 000000000..e69de29bb diff --git a/src/websrc/3rdparty/css/bootstrap-3.3.7.min.css b/src/websrc/3rdparty/css/bootstrap-3.3.7.min.css new file mode 100644 index 000000000..ed3905e0e --- /dev/null +++ b/src/websrc/3rdparty/css/bootstrap-3.3.7.min.css @@ -0,0 +1,6 @@ +/*! + * 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/footable.bootstrap-3.1.6.min.css b/src/websrc/3rdparty/css/footable.bootstrap-3.1.6.min.css new file mode 100644 index 000000000..87841b27a --- /dev/null +++ b/src/websrc/3rdparty/css/footable.bootstrap-3.1.6.min.css @@ -0,0 +1 @@ +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/css/sidebar.css b/src/websrc/3rdparty/css/sidebar.css new file mode 100644 index 000000000..6a2f2345f --- /dev/null +++ b/src/websrc/3rdparty/css/sidebar.css @@ -0,0 +1 @@ +html {position: relative;overflow: scroll;overflow-x: hidden;min-height: 100% }::-webkit-scrollbar {width: 0px;background: transparent;}::-webkit-scrollbar-thumb {background: #e8e8e8;}body {background: #f1f3f6;margin-bottom: 60px }p {font-size: 1.1em;font-weight: 300;line-height: 1.7em;color: #999 }a, a:focus, a:hover {color: inherit;text-decoration: none;transition: all .3s }.navbar {padding: 15px 10px;background: #fff;border: none;border-radius: 0;margin-bottom: 40px;box-shadow: 1px 1px 3px rgba(0, 0, 0, .1) }#dismiss, #sidebar {background: #337ab7 }#content.navbar-btn {box-shadow: none;outline: 0;border: none }.line {width: 100%;height: 1px;border-bottom: 1px dashed #ddd;margin: 40px 0 }#sidebar {width: 250px;position: fixed;top: 0;left: -250px;height: 100vh;z-index: 999;color: #fff;transition: all .3s;overflow-y: auto;box-shadow: 3px 3px 3px rgba(0, 0, 0, .2) }@media screen and (min-width:768px) {#sidebar {left: 0 }.footer {margin-left: 250px }#ajaxcontent {margin-left: 250px }#dismiss, .navbar-btn {display: none }}#sidebar.active {left: 0 }#dismiss {width: 35px;height: 35px;line-height: 35px;text-align: center;position: absolute;top: 10px;right: 10px;cursor: pointer;-webkit-transition: all .3s;-o-transition: all .3s;transition: all .3s }#dismiss:hover {background: #fff;color: #337ab7 }.overlay {position: fixed;width: 100vw;height: 100vh;background: rgba(0, 0, 0, .7);z-index: 998;display: none }#sidebar .sidebar-header {padding: 20px;background: #337ab7 }#sidebar ul.components {padding: 20px 0;border-bottom: 1px solid #47748b }#content, ul.CTAs {padding: 20px }#sidebar ul p {color: #fff;padding: 10px }#sidebar ul li a {padding: 10px;font-size: 1.1em;display: block }#sidebar ul li a:hover {color: #337ab7;background: #fff }#sidebar ul li.active>a, a[aria-expanded=true] {color: #fff;background: #2e6da4 }a[data-toggle=collapse] {position: relative }a[aria-expanded=false]::before, a[aria-expanded=true]::before {content: '\e259';display: block;position: absolute;right: 20px;font-family: 'Glyphicons Halflings';font-size: .6em }#sidebar ul ul a, ul.CTAs a {font-size: .9em }a[aria-expanded=true]::before {content: '\e260' }#sidebar ul ul a {padding-left: 30px;background: #2e6da4 }ul.CTAs a {text-align: center;display: block;border-radius: 5px;margin-bottom: 5px }a.download {background: #fff;color: #337ab7 }#sidebar a.article, a.article:hover {background: #2e6da4;color: #fff }#content {width: 100%;min-height: 100vh;transition: all .3s;position: absolute;top: 0;right: 0 }.footer {position: fixed;bottom: 0;width: 100%;height: 45px;background-color: #f1f3f6 }i {margin-right: 1em } \ No newline at end of file diff --git a/src/websrc/3rdparty/fonts/glyphicons-halflings-regular.woff b/src/websrc/3rdparty/fonts/glyphicons-halflings-regular.woff new file mode 100644 index 000000000..9e612858f Binary files /dev/null and b/src/websrc/3rdparty/fonts/glyphicons-halflings-regular.woff differ diff --git a/src/websrc/3rdparty/js/bootstrap-3.3.7.min.js b/src/websrc/3rdparty/js/bootstrap-3.3.7.min.js new file mode 100644 index 000000000..9bcd2fcca --- /dev/null +++ b/src/websrc/3rdparty/js/bootstrap-3.3.7.min.js @@ -0,0 +1,7 @@ +/*! + * 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/footable-3.1.6.min.js b/src/websrc/3rdparty/js/footable-3.1.6.min.js new file mode 100644 index 000000000..53398d182 --- /dev/null +++ b/src/websrc/3rdparty/js/footable-3.1.6.min.js @@ -0,0 +1,10 @@ +/* +* FooTable v3 - FooTable is a jQuery plugin that aims to make HTML tables on smaller devices look awesome. +* @version 3.1.6 +* @link http://fooplugins.com +* @copyright Steven Usher & Brad Vincent 2015 +* @license Released under the GPLv3 license. +*/ +!function(a,b){window.console=window.console||{log:function(){},error:function(){}},a.fn.footable=function(a,c){return a=a||{},this.filter("table").each(function(d,e){b.init(e,a,c)})};var c={events:[]};b.__debug__=JSON.parse(localStorage.getItem("footable_debug"))||!1,b.__debug_options__=JSON.parse(localStorage.getItem("footable_debug_options"))||c,b.debug=function(d,e){return b.is["boolean"](d)?(b.__debug__=d,void(b.__debug__?(localStorage.setItem("footable_debug",JSON.stringify(b.__debug__)),b.__debug_options__=a.extend(!0,{},c,e||{}),b.is.hash(e)&&localStorage.setItem("footable_debug_options",JSON.stringify(b.__debug_options__))):(localStorage.removeItem("footable_debug"),localStorage.removeItem("footable_debug_options")))):b.__debug__},b.get=function(b){return a(b).first().data("__FooTable__")},b.init=function(a,c,d){var e=b.get(a);return e instanceof b.Table&&e.destroy(),new b.Table(a,c,d)},b.getRow=function(b){var c=a(b).closest("tr");return c.hasClass("footable-detail-row")&&(c=c.prev()),c.data("__FooTableRow__")}}(jQuery,FooTable=window.FooTable||{}),function(a){var b=function(){return!0};a.arr={},a.arr.each=function(b,c){if(a.is.array(b)&&a.is.fn(c))for(var d=0,e=b.length;e>d&&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("r;r++)if(g=a[r],g||0===g)if("object"===n.type(g))n.merge(q,g.nodeType?[g]:g);else if(ga.test(g)){i=i||p.appendChild(b.createElement("div")),j=($.exec(g)||["",""])[1].toLowerCase(),m=da[j]||da._default,i.innerHTML=m[1]+n.htmlPrefilter(g)+m[2],f=m[0];while(f--)i=i.lastChild;if(!l.leadingWhitespace&&aa.test(g)&&q.push(b.createTextNode(aa.exec(g)[0])),!l.tbody){g="table"!==j||ha.test(g)?"
").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/3rdparty/js/jquery-1.12.4.min.js b/src/websrc/3rdparty/js/jquery-1.12.4.min.js new file mode 100644 index 000000000..e83647587 --- /dev/null +++ b/src/websrc/3rdparty/js/jquery-1.12.4.min.js @@ -0,0 +1,5 @@ +/*! jQuery v1.12.4 | (c) jQuery Foundation | jquery.org/license */ +!function(a,b){"object"==typeof module&&"object"==typeof module.exports?module.exports=a.document?b(a,!0):function(a){if(!a.document)throw new Error("jQuery requires a window with a document");return b(a)}:b(a)}("undefined"!=typeof window?window:this,function(a,b){var c=[],d=a.document,e=c.slice,f=c.concat,g=c.push,h=c.indexOf,i={},j=i.toString,k=i.hasOwnProperty,l={},m="1.12.4",n=function(a,b){return new n.fn.init(a,b)},o=/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g,p=/^-ms-/,q=/-([\da-z])/gi,r=function(a,b){return b.toUpperCase()};n.fn=n.prototype={jquery:m,constructor:n,selector:"",length:0,toArray:function(){return e.call(this)},get:function(a){return null!=a?0>a?this[a+this.length]:this[a]:e.call(this)},pushStack:function(a){var b=n.merge(this.constructor(),a);return b.prevObject=this,b.context=this.context,b},each:function(a){return n.each(this,a)},map:function(a){return this.pushStack(n.map(this,function(b,c){return a.call(b,c,b)}))},slice:function(){return this.pushStack(e.apply(this,arguments))},first:function(){return this.eq(0)},last:function(){return this.eq(-1)},eq:function(a){var b=this.length,c=+a+(0>a?b:0);return this.pushStack(c>=0&&b>c?[this[c]]:[])},end:function(){return this.prevObject||this.constructor()},push:g,sort:c.sort,splice:c.splice},n.extend=n.fn.extend=function(){var a,b,c,d,e,f,g=arguments[0]||{},h=1,i=arguments.length,j=!1;for("boolean"==typeof g&&(j=g,g=arguments[h]||{},h++),"object"==typeof g||n.isFunction(g)||(g={}),h===i&&(g=this,h--);i>h;h++)if(null!=(e=arguments[h]))for(d in e)a=g[d],c=e[d],g!==c&&(j&&c&&(n.isPlainObject(c)||(b=n.isArray(c)))?(b?(b=!1,f=a&&n.isArray(a)?a:[]):f=a&&n.isPlainObject(a)?a:{},g[d]=n.extend(j,f,c)):void 0!==c&&(g[d]=c));return g},n.extend({expando:"jQuery"+(m+Math.random()).replace(/\D/g,""),isReady:!0,error:function(a){throw new Error(a)},noop:function(){},isFunction:function(a){return"function"===n.type(a)},isArray:Array.isArray||function(a){return"array"===n.type(a)},isWindow:function(a){return null!=a&&a==a.window},isNumeric:function(a){var b=a&&a.toString();return!n.isArray(a)&&b-parseFloat(b)+1>=0},isEmptyObject:function(a){var b;for(b in a)return!1;return!0},isPlainObject:function(a){var b;if(!a||"object"!==n.type(a)||a.nodeType||n.isWindow(a))return!1;try{if(a.constructor&&!k.call(a,"constructor")&&!k.call(a.constructor.prototype,"isPrototypeOf"))return!1}catch(c){return!1}if(!l.ownFirst)for(b in a)return k.call(a,b);for(b in a);return void 0===b||k.call(a,b)},type:function(a){return null==a?a+"":"object"==typeof a||"function"==typeof a?i[j.call(a)]||"object":typeof a},globalEval:function(b){b&&n.trim(b)&&(a.execScript||function(b){a.eval.call(a,b)})(b)},camelCase:function(a){return a.replace(p,"ms-").replace(q,r)},nodeName:function(a,b){return a.nodeName&&a.nodeName.toLowerCase()===b.toLowerCase()},each:function(a,b){var c,d=0;if(s(a)){for(c=a.length;c>d;d++)if(b.call(a[d],d,a[d])===!1)break}else for(d in a)if(b.call(a[d],d,a[d])===!1)break;return a},trim:function(a){return null==a?"":(a+"").replace(o,"")},makeArray:function(a,b){var c=b||[];return null!=a&&(s(Object(a))?n.merge(c,"string"==typeof a?[a]:a):g.call(c,a)),c},inArray:function(a,b,c){var d;if(b){if(h)return h.call(b,a,c);for(d=b.length,c=c?0>c?Math.max(0,d+c):c:0;d>c;c++)if(c in b&&b[c]===a)return c}return-1},merge:function(a,b){var c=+b.length,d=0,e=a.length;while(c>d)a[e++]=b[d++];if(c!==c)while(void 0!==b[d])a[e++]=b[d++];return a.length=e,a},grep:function(a,b,c){for(var d,e=[],f=0,g=a.length,h=!c;g>f;f++)d=!b(a[f],f),d!==h&&e.push(a[f]);return e},map:function(a,b,c){var d,e,g=0,h=[];if(s(a))for(d=a.length;d>g;g++)e=b(a[g],g,c),null!=e&&h.push(e);else for(g in a)e=b(a[g],g,c),null!=e&&h.push(e);return f.apply([],h)},guid:1,proxy:function(a,b){var c,d,f;return"string"==typeof b&&(f=a[b],b=a,a=f),n.isFunction(a)?(c=e.call(arguments,2),d=function(){return a.apply(b||this,c.concat(e.call(arguments)))},d.guid=a.guid=a.guid||n.guid++,d):void 0},now:function(){return+new Date},support:l}),"function"==typeof Symbol&&(n.fn[Symbol.iterator]=c[Symbol.iterator]),n.each("Boolean Number String Function Array Date RegExp Object Error Symbol".split(" "),function(a,b){i["[object "+b+"]"]=b.toLowerCase()});function s(a){var b=!!a&&"length"in a&&a.length,c=n.type(a);return"function"===c||n.isWindow(a)?!1:"array"===c||0===b||"number"==typeof b&&b>0&&b-1 in a}var t=function(a){var b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q,r,s,t,u="sizzle"+1*new Date,v=a.document,w=0,x=0,y=ga(),z=ga(),A=ga(),B=function(a,b){return a===b&&(l=!0),0},C=1<<31,D={}.hasOwnProperty,E=[],F=E.pop,G=E.push,H=E.push,I=E.slice,J=function(a,b){for(var c=0,d=a.length;d>c;c++)if(a[c]===b)return c;return-1},K="checked|selected|async|autofocus|autoplay|controls|defer|disabled|hidden|ismap|loop|multiple|open|readonly|required|scoped",L="[\\x20\\t\\r\\n\\f]",M="(?:\\\\.|[\\w-]|[^\\x00-\\xa0])+",N="\\["+L+"*("+M+")(?:"+L+"*([*^$|!~]?=)"+L+"*(?:'((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\"|("+M+"))|)"+L+"*\\]",O=":("+M+")(?:\\((('((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\")|((?:\\\\.|[^\\\\()[\\]]|"+N+")*)|.*)\\)|)",P=new RegExp(L+"+","g"),Q=new RegExp("^"+L+"+|((?:^|[^\\\\])(?:\\\\.)*)"+L+"+$","g"),R=new RegExp("^"+L+"*,"+L+"*"),S=new RegExp("^"+L+"*([>+~]|"+L+")"+L+"*"),T=new RegExp("="+L+"*([^\\]'\"]*?)"+L+"*\\]","g"),U=new RegExp(O),V=new RegExp("^"+M+"$"),W={ID:new RegExp("^#("+M+")"),CLASS:new RegExp("^\\.("+M+")"),TAG:new RegExp("^("+M+"|[*])"),ATTR:new RegExp("^"+N),PSEUDO:new RegExp("^"+O),CHILD:new RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+L+"*(even|odd|(([+-]|)(\\d*)n|)"+L+"*(?:([+-]|)"+L+"*(\\d+)|))"+L+"*\\)|)","i"),bool:new RegExp("^(?:"+K+")$","i"),needsContext:new RegExp("^"+L+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+L+"*((?:-\\d)?\\d*)"+L+"*\\)|)(?=[^-]|$)","i")},X=/^(?:input|select|textarea|button)$/i,Y=/^h\d$/i,Z=/^[^{]+\{\s*\[native \w/,$=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,_=/[+~]/,aa=/'|\\/g,ba=new RegExp("\\\\([\\da-f]{1,6}"+L+"?|("+L+")|.)","ig"),ca=function(a,b,c){var d="0x"+b-65536;return d!==d||c?b:0>d?String.fromCharCode(d+65536):String.fromCharCode(d>>10|55296,1023&d|56320)},da=function(){m()};try{H.apply(E=I.call(v.childNodes),v.childNodes),E[v.childNodes.length].nodeType}catch(ea){H={apply:E.length?function(a,b){G.apply(a,I.call(b))}:function(a,b){var c=a.length,d=0;while(a[c++]=b[d++]);a.length=c-1}}}function fa(a,b,d,e){var f,h,j,k,l,o,r,s,w=b&&b.ownerDocument,x=b?b.nodeType:9;if(d=d||[],"string"!=typeof a||!a||1!==x&&9!==x&&11!==x)return d;if(!e&&((b?b.ownerDocument||b:v)!==n&&m(b),b=b||n,p)){if(11!==x&&(o=$.exec(a)))if(f=o[1]){if(9===x){if(!(j=b.getElementById(f)))return d;if(j.id===f)return d.push(j),d}else if(w&&(j=w.getElementById(f))&&t(b,j)&&j.id===f)return d.push(j),d}else{if(o[2])return H.apply(d,b.getElementsByTagName(a)),d;if((f=o[3])&&c.getElementsByClassName&&b.getElementsByClassName)return H.apply(d,b.getElementsByClassName(f)),d}if(c.qsa&&!A[a+" "]&&(!q||!q.test(a))){if(1!==x)w=b,s=a;else if("object"!==b.nodeName.toLowerCase()){(k=b.getAttribute("id"))?k=k.replace(aa,"\\$&"):b.setAttribute("id",k=u),r=g(a),h=r.length,l=V.test(k)?"#"+k:"[id='"+k+"']";while(h--)r[h]=l+" "+qa(r[h]);s=r.join(","),w=_.test(a)&&oa(b.parentNode)||b}if(s)try{return H.apply(d,w.querySelectorAll(s)),d}catch(y){}finally{k===u&&b.removeAttribute("id")}}}return i(a.replace(Q,"$1"),b,d,e)}function ga(){var a=[];function b(c,e){return a.push(c+" ")>d.cacheLength&&delete b[a.shift()],b[c+" "]=e}return b}function ha(a){return a[u]=!0,a}function ia(a){var b=n.createElement("div");try{return!!a(b)}catch(c){return!1}finally{b.parentNode&&b.parentNode.removeChild(b),b=null}}function ja(a,b){var c=a.split("|"),e=c.length;while(e--)d.attrHandle[c[e]]=b}function ka(a,b){var c=b&&a,d=c&&1===a.nodeType&&1===b.nodeType&&(~b.sourceIndex||C)-(~a.sourceIndex||C);if(d)return d;if(c)while(c=c.nextSibling)if(c===b)return-1;return a?1:-1}function la(a){return function(b){var c=b.nodeName.toLowerCase();return"input"===c&&b.type===a}}function ma(a){return function(b){var c=b.nodeName.toLowerCase();return("input"===c||"button"===c)&&b.type===a}}function na(a){return ha(function(b){return b=+b,ha(function(c,d){var e,f=a([],c.length,b),g=f.length;while(g--)c[e=f[g]]&&(c[e]=!(d[e]=c[e]))})})}function oa(a){return a&&"undefined"!=typeof a.getElementsByTagName&&a}c=fa.support={},f=fa.isXML=function(a){var b=a&&(a.ownerDocument||a).documentElement;return b?"HTML"!==b.nodeName:!1},m=fa.setDocument=function(a){var b,e,g=a?a.ownerDocument||a:v;return g!==n&&9===g.nodeType&&g.documentElement?(n=g,o=n.documentElement,p=!f(n),(e=n.defaultView)&&e.top!==e&&(e.addEventListener?e.addEventListener("unload",da,!1):e.attachEvent&&e.attachEvent("onunload",da)),c.attributes=ia(function(a){return a.className="i",!a.getAttribute("className")}),c.getElementsByTagName=ia(function(a){return a.appendChild(n.createComment("")),!a.getElementsByTagName("*").length}),c.getElementsByClassName=Z.test(n.getElementsByClassName),c.getById=ia(function(a){return o.appendChild(a).id=u,!n.getElementsByName||!n.getElementsByName(u).length}),c.getById?(d.find.ID=function(a,b){if("undefined"!=typeof b.getElementById&&p){var c=b.getElementById(a);return c?[c]:[]}},d.filter.ID=function(a){var b=a.replace(ba,ca);return function(a){return a.getAttribute("id")===b}}):(delete d.find.ID,d.filter.ID=function(a){var b=a.replace(ba,ca);return function(a){var c="undefined"!=typeof a.getAttributeNode&&a.getAttributeNode("id");return c&&c.value===b}}),d.find.TAG=c.getElementsByTagName?function(a,b){return"undefined"!=typeof b.getElementsByTagName?b.getElementsByTagName(a):c.qsa?b.querySelectorAll(a):void 0}:function(a,b){var c,d=[],e=0,f=b.getElementsByTagName(a);if("*"===a){while(c=f[e++])1===c.nodeType&&d.push(c);return d}return f},d.find.CLASS=c.getElementsByClassName&&function(a,b){return"undefined"!=typeof b.getElementsByClassName&&p?b.getElementsByClassName(a):void 0},r=[],q=[],(c.qsa=Z.test(n.querySelectorAll))&&(ia(function(a){o.appendChild(a).innerHTML="",a.querySelectorAll("[msallowcapture^='']").length&&q.push("[*^$]="+L+"*(?:''|\"\")"),a.querySelectorAll("[selected]").length||q.push("\\["+L+"*(?:value|"+K+")"),a.querySelectorAll("[id~="+u+"-]").length||q.push("~="),a.querySelectorAll(":checked").length||q.push(":checked"),a.querySelectorAll("a#"+u+"+*").length||q.push(".#.+[+~]")}),ia(function(a){var b=n.createElement("input");b.setAttribute("type","hidden"),a.appendChild(b).setAttribute("name","D"),a.querySelectorAll("[name=d]").length&&q.push("name"+L+"*[*^$|!~]?="),a.querySelectorAll(":enabled").length||q.push(":enabled",":disabled"),a.querySelectorAll("*,:x"),q.push(",.*:")})),(c.matchesSelector=Z.test(s=o.matches||o.webkitMatchesSelector||o.mozMatchesSelector||o.oMatchesSelector||o.msMatchesSelector))&&ia(function(a){c.disconnectedMatch=s.call(a,"div"),s.call(a,"[s!='']:x"),r.push("!=",O)}),q=q.length&&new RegExp(q.join("|")),r=r.length&&new RegExp(r.join("|")),b=Z.test(o.compareDocumentPosition),t=b||Z.test(o.contains)?function(a,b){var c=9===a.nodeType?a.documentElement:a,d=b&&b.parentNode;return a===d||!(!d||1!==d.nodeType||!(c.contains?c.contains(d):a.compareDocumentPosition&&16&a.compareDocumentPosition(d)))}:function(a,b){if(b)while(b=b.parentNode)if(b===a)return!0;return!1},B=b?function(a,b){if(a===b)return l=!0,0;var d=!a.compareDocumentPosition-!b.compareDocumentPosition;return d?d:(d=(a.ownerDocument||a)===(b.ownerDocument||b)?a.compareDocumentPosition(b):1,1&d||!c.sortDetached&&b.compareDocumentPosition(a)===d?a===n||a.ownerDocument===v&&t(v,a)?-1:b===n||b.ownerDocument===v&&t(v,b)?1:k?J(k,a)-J(k,b):0:4&d?-1:1)}:function(a,b){if(a===b)return l=!0,0;var c,d=0,e=a.parentNode,f=b.parentNode,g=[a],h=[b];if(!e||!f)return a===n?-1:b===n?1:e?-1:f?1:k?J(k,a)-J(k,b):0;if(e===f)return ka(a,b);c=a;while(c=c.parentNode)g.unshift(c);c=b;while(c=c.parentNode)h.unshift(c);while(g[d]===h[d])d++;return d?ka(g[d],h[d]):g[d]===v?-1:h[d]===v?1:0},n):n},fa.matches=function(a,b){return fa(a,null,null,b)},fa.matchesSelector=function(a,b){if((a.ownerDocument||a)!==n&&m(a),b=b.replace(T,"='$1']"),c.matchesSelector&&p&&!A[b+" "]&&(!r||!r.test(b))&&(!q||!q.test(b)))try{var d=s.call(a,b);if(d||c.disconnectedMatch||a.document&&11!==a.document.nodeType)return d}catch(e){}return fa(b,n,null,[a]).length>0},fa.contains=function(a,b){return(a.ownerDocument||a)!==n&&m(a),t(a,b)},fa.attr=function(a,b){(a.ownerDocument||a)!==n&&m(a);var e=d.attrHandle[b.toLowerCase()],f=e&&D.call(d.attrHandle,b.toLowerCase())?e(a,b,!p):void 0;return void 0!==f?f:c.attributes||!p?a.getAttribute(b):(f=a.getAttributeNode(b))&&f.specified?f.value:null},fa.error=function(a){throw new Error("Syntax error, unrecognized expression: "+a)},fa.uniqueSort=function(a){var b,d=[],e=0,f=0;if(l=!c.detectDuplicates,k=!c.sortStable&&a.slice(0),a.sort(B),l){while(b=a[f++])b===a[f]&&(e=d.push(f));while(e--)a.splice(d[e],1)}return k=null,a},e=fa.getText=function(a){var b,c="",d=0,f=a.nodeType;if(f){if(1===f||9===f||11===f){if("string"==typeof a.textContent)return a.textContent;for(a=a.firstChild;a;a=a.nextSibling)c+=e(a)}else if(3===f||4===f)return a.nodeValue}else while(b=a[d++])c+=e(b);return c},d=fa.selectors={cacheLength:50,createPseudo:ha,match:W,attrHandle:{},find:{},relative:{">":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(a){return a[1]=a[1].replace(ba,ca),a[3]=(a[3]||a[4]||a[5]||"").replace(ba,ca),"~="===a[2]&&(a[3]=" "+a[3]+" "),a.slice(0,4)},CHILD:function(a){return a[1]=a[1].toLowerCase(),"nth"===a[1].slice(0,3)?(a[3]||fa.error(a[0]),a[4]=+(a[4]?a[5]+(a[6]||1):2*("even"===a[3]||"odd"===a[3])),a[5]=+(a[7]+a[8]||"odd"===a[3])):a[3]&&fa.error(a[0]),a},PSEUDO:function(a){var b,c=!a[6]&&a[2];return W.CHILD.test(a[0])?null:(a[3]?a[2]=a[4]||a[5]||"":c&&U.test(c)&&(b=g(c,!0))&&(b=c.indexOf(")",c.length-b)-c.length)&&(a[0]=a[0].slice(0,b),a[2]=c.slice(0,b)),a.slice(0,3))}},filter:{TAG:function(a){var b=a.replace(ba,ca).toLowerCase();return"*"===a?function(){return!0}:function(a){return a.nodeName&&a.nodeName.toLowerCase()===b}},CLASS:function(a){var b=y[a+" "];return b||(b=new RegExp("(^|"+L+")"+a+"("+L+"|$)"))&&y(a,function(a){return b.test("string"==typeof a.className&&a.className||"undefined"!=typeof a.getAttribute&&a.getAttribute("class")||"")})},ATTR:function(a,b,c){return function(d){var e=fa.attr(d,a);return null==e?"!="===b:b?(e+="","="===b?e===c:"!="===b?e!==c:"^="===b?c&&0===e.indexOf(c):"*="===b?c&&e.indexOf(c)>-1:"$="===b?c&&e.slice(-c.length)===c:"~="===b?(" "+e.replace(P," ")+" ").indexOf(c)>-1:"|="===b?e===c||e.slice(0,c.length+1)===c+"-":!1):!0}},CHILD:function(a,b,c,d,e){var f="nth"!==a.slice(0,3),g="last"!==a.slice(-4),h="of-type"===b;return 1===d&&0===e?function(a){return!!a.parentNode}:function(b,c,i){var j,k,l,m,n,o,p=f!==g?"nextSibling":"previousSibling",q=b.parentNode,r=h&&b.nodeName.toLowerCase(),s=!i&&!h,t=!1;if(q){if(f){while(p){m=b;while(m=m[p])if(h?m.nodeName.toLowerCase()===r:1===m.nodeType)return!1;o=p="only"===a&&!o&&"nextSibling"}return!0}if(o=[g?q.firstChild:q.lastChild],g&&s){m=q,l=m[u]||(m[u]={}),k=l[m.uniqueID]||(l[m.uniqueID]={}),j=k[a]||[],n=j[0]===w&&j[1],t=n&&j[2],m=n&&q.childNodes[n];while(m=++n&&m&&m[p]||(t=n=0)||o.pop())if(1===m.nodeType&&++t&&m===b){k[a]=[w,n,t];break}}else if(s&&(m=b,l=m[u]||(m[u]={}),k=l[m.uniqueID]||(l[m.uniqueID]={}),j=k[a]||[],n=j[0]===w&&j[1],t=n),t===!1)while(m=++n&&m&&m[p]||(t=n=0)||o.pop())if((h?m.nodeName.toLowerCase()===r:1===m.nodeType)&&++t&&(s&&(l=m[u]||(m[u]={}),k=l[m.uniqueID]||(l[m.uniqueID]={}),k[a]=[w,t]),m===b))break;return t-=e,t===d||t%d===0&&t/d>=0}}},PSEUDO:function(a,b){var c,e=d.pseudos[a]||d.setFilters[a.toLowerCase()]||fa.error("unsupported pseudo: "+a);return e[u]?e(b):e.length>1?(c=[a,a,"",b],d.setFilters.hasOwnProperty(a.toLowerCase())?ha(function(a,c){var d,f=e(a,b),g=f.length;while(g--)d=J(a,f[g]),a[d]=!(c[d]=f[g])}):function(a){return e(a,0,c)}):e}},pseudos:{not:ha(function(a){var b=[],c=[],d=h(a.replace(Q,"$1"));return d[u]?ha(function(a,b,c,e){var f,g=d(a,null,e,[]),h=a.length;while(h--)(f=g[h])&&(a[h]=!(b[h]=f))}):function(a,e,f){return b[0]=a,d(b,null,f,c),b[0]=null,!c.pop()}}),has:ha(function(a){return function(b){return fa(a,b).length>0}}),contains:ha(function(a){return a=a.replace(ba,ca),function(b){return(b.textContent||b.innerText||e(b)).indexOf(a)>-1}}),lang:ha(function(a){return V.test(a||"")||fa.error("unsupported lang: "+a),a=a.replace(ba,ca).toLowerCase(),function(b){var c;do if(c=p?b.lang:b.getAttribute("xml:lang")||b.getAttribute("lang"))return c=c.toLowerCase(),c===a||0===c.indexOf(a+"-");while((b=b.parentNode)&&1===b.nodeType);return!1}}),target:function(b){var c=a.location&&a.location.hash;return c&&c.slice(1)===b.id},root:function(a){return a===o},focus:function(a){return a===n.activeElement&&(!n.hasFocus||n.hasFocus())&&!!(a.type||a.href||~a.tabIndex)},enabled:function(a){return a.disabled===!1},disabled:function(a){return a.disabled===!0},checked:function(a){var b=a.nodeName.toLowerCase();return"input"===b&&!!a.checked||"option"===b&&!!a.selected},selected:function(a){return a.parentNode&&a.parentNode.selectedIndex,a.selected===!0},empty:function(a){for(a=a.firstChild;a;a=a.nextSibling)if(a.nodeType<6)return!1;return!0},parent:function(a){return!d.pseudos.empty(a)},header:function(a){return Y.test(a.nodeName)},input:function(a){return X.test(a.nodeName)},button:function(a){var b=a.nodeName.toLowerCase();return"input"===b&&"button"===a.type||"button"===b},text:function(a){var b;return"input"===a.nodeName.toLowerCase()&&"text"===a.type&&(null==(b=a.getAttribute("type"))||"text"===b.toLowerCase())},first:na(function(){return[0]}),last:na(function(a,b){return[b-1]}),eq:na(function(a,b,c){return[0>c?c+b:c]}),even:na(function(a,b){for(var c=0;b>c;c+=2)a.push(c);return a}),odd:na(function(a,b){for(var c=1;b>c;c+=2)a.push(c);return a}),lt:na(function(a,b,c){for(var d=0>c?c+b:c;--d>=0;)a.push(d);return a}),gt:na(function(a,b,c){for(var d=0>c?c+b:c;++db;b++)d+=a[b].value;return d}function ra(a,b,c){var d=b.dir,e=c&&"parentNode"===d,f=x++;return b.first?function(b,c,f){while(b=b[d])if(1===b.nodeType||e)return a(b,c,f)}:function(b,c,g){var h,i,j,k=[w,f];if(g){while(b=b[d])if((1===b.nodeType||e)&&a(b,c,g))return!0}else while(b=b[d])if(1===b.nodeType||e){if(j=b[u]||(b[u]={}),i=j[b.uniqueID]||(j[b.uniqueID]={}),(h=i[d])&&h[0]===w&&h[1]===f)return k[2]=h[2];if(i[d]=k,k[2]=a(b,c,g))return!0}}}function sa(a){return a.length>1?function(b,c,d){var e=a.length;while(e--)if(!a[e](b,c,d))return!1;return!0}:a[0]}function ta(a,b,c){for(var d=0,e=b.length;e>d;d++)fa(a,b[d],c);return c}function ua(a,b,c,d,e){for(var f,g=[],h=0,i=a.length,j=null!=b;i>h;h++)(f=a[h])&&(c&&!c(f,d,e)||(g.push(f),j&&b.push(h)));return g}function va(a,b,c,d,e,f){return d&&!d[u]&&(d=va(d)),e&&!e[u]&&(e=va(e,f)),ha(function(f,g,h,i){var j,k,l,m=[],n=[],o=g.length,p=f||ta(b||"*",h.nodeType?[h]:h,[]),q=!a||!f&&b?p:ua(p,m,a,h,i),r=c?e||(f?a:o||d)?[]:g:q;if(c&&c(q,r,h,i),d){j=ua(r,n),d(j,[],h,i),k=j.length;while(k--)(l=j[k])&&(r[n[k]]=!(q[n[k]]=l))}if(f){if(e||a){if(e){j=[],k=r.length;while(k--)(l=r[k])&&j.push(q[k]=l);e(null,r=[],j,i)}k=r.length;while(k--)(l=r[k])&&(j=e?J(f,l):m[k])>-1&&(f[j]=!(g[j]=l))}}else r=ua(r===g?r.splice(o,r.length):r),e?e(null,g,r,i):H.apply(g,r)})}function wa(a){for(var b,c,e,f=a.length,g=d.relative[a[0].type],h=g||d.relative[" "],i=g?1:0,k=ra(function(a){return a===b},h,!0),l=ra(function(a){return J(b,a)>-1},h,!0),m=[function(a,c,d){var e=!g&&(d||c!==j)||((b=c).nodeType?k(a,c,d):l(a,c,d));return b=null,e}];f>i;i++)if(c=d.relative[a[i].type])m=[ra(sa(m),c)];else{if(c=d.filter[a[i].type].apply(null,a[i].matches),c[u]){for(e=++i;f>e;e++)if(d.relative[a[e].type])break;return va(i>1&&sa(m),i>1&&qa(a.slice(0,i-1).concat({value:" "===a[i-2].type?"*":""})).replace(Q,"$1"),c,e>i&&wa(a.slice(i,e)),f>e&&wa(a=a.slice(e)),f>e&&qa(a))}m.push(c)}return sa(m)}function xa(a,b){var c=b.length>0,e=a.length>0,f=function(f,g,h,i,k){var l,o,q,r=0,s="0",t=f&&[],u=[],v=j,x=f||e&&d.find.TAG("*",k),y=w+=null==v?1:Math.random()||.1,z=x.length;for(k&&(j=g===n||g||k);s!==z&&null!=(l=x[s]);s++){if(e&&l){o=0,g||l.ownerDocument===n||(m(l),h=!p);while(q=a[o++])if(q(l,g||n,h)){i.push(l);break}k&&(w=y)}c&&((l=!q&&l)&&r--,f&&t.push(l))}if(r+=s,c&&s!==r){o=0;while(q=b[o++])q(t,u,g,h);if(f){if(r>0)while(s--)t[s]||u[s]||(u[s]=F.call(i));u=ua(u)}H.apply(i,u),k&&!f&&u.length>0&&r+b.length>1&&fa.uniqueSort(i)}return k&&(w=y,j=v),t};return c?ha(f):f}return h=fa.compile=function(a,b){var c,d=[],e=[],f=A[a+" "];if(!f){b||(b=g(a)),c=b.length;while(c--)f=wa(b[c]),f[u]?d.push(f):e.push(f);f=A(a,xa(e,d)),f.selector=a}return f},i=fa.select=function(a,b,e,f){var i,j,k,l,m,n="function"==typeof a&&a,o=!f&&g(a=n.selector||a);if(e=e||[],1===o.length){if(j=o[0]=o[0].slice(0),j.length>2&&"ID"===(k=j[0]).type&&c.getById&&9===b.nodeType&&p&&d.relative[j[1].type]){if(b=(d.find.ID(k.matches[0].replace(ba,ca),b)||[])[0],!b)return e;n&&(b=b.parentNode),a=a.slice(j.shift().value.length)}i=W.needsContext.test(a)?0:j.length;while(i--){if(k=j[i],d.relative[l=k.type])break;if((m=d.find[l])&&(f=m(k.matches[0].replace(ba,ca),_.test(j[0].type)&&oa(b.parentNode)||b))){if(j.splice(i,1),a=f.length&&qa(j),!a)return H.apply(e,f),e;break}}}return(n||h(a,o))(f,b,!p,e,!b||_.test(a)&&oa(b.parentNode)||b),e},c.sortStable=u.split("").sort(B).join("")===u,c.detectDuplicates=!!l,m(),c.sortDetached=ia(function(a){return 1&a.compareDocumentPosition(n.createElement("div"))}),ia(function(a){return a.innerHTML="","#"===a.firstChild.getAttribute("href")})||ja("type|href|height|width",function(a,b,c){return c?void 0:a.getAttribute(b,"type"===b.toLowerCase()?1:2)}),c.attributes&&ia(function(a){return a.innerHTML="",a.firstChild.setAttribute("value",""),""===a.firstChild.getAttribute("value")})||ja("value",function(a,b,c){return c||"input"!==a.nodeName.toLowerCase()?void 0:a.defaultValue}),ia(function(a){return null==a.getAttribute("disabled")})||ja(K,function(a,b,c){var d;return c?void 0:a[b]===!0?b.toLowerCase():(d=a.getAttributeNode(b))&&d.specified?d.value:null}),fa}(a);n.find=t,n.expr=t.selectors,n.expr[":"]=n.expr.pseudos,n.uniqueSort=n.unique=t.uniqueSort,n.text=t.getText,n.isXMLDoc=t.isXML,n.contains=t.contains;var u=function(a,b,c){var d=[],e=void 0!==c;while((a=a[b])&&9!==a.nodeType)if(1===a.nodeType){if(e&&n(a).is(c))break;d.push(a)}return d},v=function(a,b){for(var c=[];a;a=a.nextSibling)1===a.nodeType&&a!==b&&c.push(a);return c},w=n.expr.match.needsContext,x=/^<([\w-]+)\s*\/?>(?:<\/\1>|)$/,y=/^.[^:#\[\.,]*$/;function z(a,b,c){if(n.isFunction(b))return n.grep(a,function(a,d){return!!b.call(a,d,a)!==c});if(b.nodeType)return n.grep(a,function(a){return a===b!==c});if("string"==typeof b){if(y.test(b))return n.filter(b,a,c);b=n.filter(b,a)}return n.grep(a,function(a){return n.inArray(a,b)>-1!==c})}n.filter=function(a,b,c){var d=b[0];return c&&(a=":not("+a+")"),1===b.length&&1===d.nodeType?n.find.matchesSelector(d,a)?[d]:[]:n.find.matches(a,n.grep(b,function(a){return 1===a.nodeType}))},n.fn.extend({find:function(a){var b,c=[],d=this,e=d.length;if("string"!=typeof a)return this.pushStack(n(a).filter(function(){for(b=0;e>b;b++)if(n.contains(d[b],this))return!0}));for(b=0;e>b;b++)n.find(a,d[b],c);return c=this.pushStack(e>1?n.unique(c):c),c.selector=this.selector?this.selector+" "+a:a,c},filter:function(a){return this.pushStack(z(this,a||[],!1))},not:function(a){return this.pushStack(z(this,a||[],!0))},is:function(a){return!!z(this,"string"==typeof a&&w.test(a)?n(a):a||[],!1).length}});var A,B=/^(?:\s*(<[\w\W]+>)[^>]*|#([\w-]*))$/,C=n.fn.init=function(a,b,c){var e,f;if(!a)return this;if(c=c||A,"string"==typeof a){if(e="<"===a.charAt(0)&&">"===a.charAt(a.length-1)&&a.length>=3?[null,a,null]:B.exec(a),!e||!e[1]&&b)return!b||b.jquery?(b||c).find(a):this.constructor(b).find(a);if(e[1]){if(b=b instanceof n?b[0]:b,n.merge(this,n.parseHTML(e[1],b&&b.nodeType?b.ownerDocument||b:d,!0)),x.test(e[1])&&n.isPlainObject(b))for(e in b)n.isFunction(this[e])?this[e](b[e]):this.attr(e,b[e]);return this}if(f=d.getElementById(e[2]),f&&f.parentNode){if(f.id!==e[2])return A.find(a);this.length=1,this[0]=f}return this.context=d,this.selector=a,this}return a.nodeType?(this.context=this[0]=a,this.length=1,this):n.isFunction(a)?"undefined"!=typeof c.ready?c.ready(a):a(n):(void 0!==a.selector&&(this.selector=a.selector,this.context=a.context),n.makeArray(a,this))};C.prototype=n.fn,A=n(d);var D=/^(?:parents|prev(?:Until|All))/,E={children:!0,contents:!0,next:!0,prev:!0};n.fn.extend({has:function(a){var b,c=n(a,this),d=c.length;return this.filter(function(){for(b=0;d>b;b++)if(n.contains(this,c[b]))return!0})},closest:function(a,b){for(var c,d=0,e=this.length,f=[],g=w.test(a)||"string"!=typeof a?n(a,b||this.context):0;e>d;d++)for(c=this[d];c&&c!==b;c=c.parentNode)if(c.nodeType<11&&(g?g.index(c)>-1:1===c.nodeType&&n.find.matchesSelector(c,a))){f.push(c);break}return this.pushStack(f.length>1?n.uniqueSort(f):f)},index:function(a){return a?"string"==typeof a?n.inArray(this[0],n(a)):n.inArray(a.jquery?a[0]:a,this):this[0]&&this[0].parentNode?this.first().prevAll().length:-1},add:function(a,b){return this.pushStack(n.uniqueSort(n.merge(this.get(),n(a,b))))},addBack:function(a){return this.add(null==a?this.prevObject:this.prevObject.filter(a))}});function F(a,b){do a=a[b];while(a&&1!==a.nodeType);return a}n.each({parent:function(a){var b=a.parentNode;return b&&11!==b.nodeType?b:null},parents:function(a){return u(a,"parentNode")},parentsUntil:function(a,b,c){return u(a,"parentNode",c)},next:function(a){return F(a,"nextSibling")},prev:function(a){return F(a,"previousSibling")},nextAll:function(a){return u(a,"nextSibling")},prevAll:function(a){return u(a,"previousSibling")},nextUntil:function(a,b,c){return u(a,"nextSibling",c)},prevUntil:function(a,b,c){return u(a,"previousSibling",c)},siblings:function(a){return v((a.parentNode||{}).firstChild,a)},children:function(a){return v(a.firstChild)},contents:function(a){return n.nodeName(a,"iframe")?a.contentDocument||a.contentWindow.document:n.merge([],a.childNodes)}},function(a,b){n.fn[a]=function(c,d){var e=n.map(this,b,c);return"Until"!==a.slice(-5)&&(d=c),d&&"string"==typeof d&&(e=n.filter(d,e)),this.length>1&&(E[a]||(e=n.uniqueSort(e)),D.test(a)&&(e=e.reverse())),this.pushStack(e)}});var G=/\S+/g;function H(a){var b={};return n.each(a.match(G)||[],function(a,c){b[c]=!0}),b}n.Callbacks=function(a){a="string"==typeof a?H(a):n.extend({},a);var b,c,d,e,f=[],g=[],h=-1,i=function(){for(e=a.once,d=b=!0;g.length;h=-1){c=g.shift();while(++h-1)f.splice(c,1),h>=c&&h--}),this},has:function(a){return a?n.inArray(a,f)>-1:f.length>0},empty:function(){return f&&(f=[]),this},disable:function(){return e=g=[],f=c="",this},disabled:function(){return!f},lock:function(){return e=!0,c||j.disable(),this},locked:function(){return!!e},fireWith:function(a,c){return e||(c=c||[],c=[a,c.slice?c.slice():c],g.push(c),b||i()),this},fire:function(){return j.fireWith(this,arguments),this},fired:function(){return!!d}};return j},n.extend({Deferred:function(a){var b=[["resolve","done",n.Callbacks("once memory"),"resolved"],["reject","fail",n.Callbacks("once memory"),"rejected"],["notify","progress",n.Callbacks("memory")]],c="pending",d={state:function(){return c},always:function(){return e.done(arguments).fail(arguments),this},then:function(){var a=arguments;return n.Deferred(function(c){n.each(b,function(b,f){var g=n.isFunction(a[b])&&a[b];e[f[1]](function(){var a=g&&g.apply(this,arguments);a&&n.isFunction(a.promise)?a.promise().progress(c.notify).done(c.resolve).fail(c.reject):c[f[0]+"With"](this===d?c.promise():this,g?[a]:arguments)})}),a=null}).promise()},promise:function(a){return null!=a?n.extend(a,d):d}},e={};return d.pipe=d.then,n.each(b,function(a,f){var g=f[2],h=f[3];d[f[1]]=g.add,h&&g.add(function(){c=h},b[1^a][2].disable,b[2][2].lock),e[f[0]]=function(){return e[f[0]+"With"](this===e?d:this,arguments),this},e[f[0]+"With"]=g.fireWith}),d.promise(e),a&&a.call(e,e),e},when:function(a){var b=0,c=e.call(arguments),d=c.length,f=1!==d||a&&n.isFunction(a.promise)?d:0,g=1===f?a:n.Deferred(),h=function(a,b,c){return function(d){b[a]=this,c[a]=arguments.length>1?e.call(arguments):d,c===i?g.notifyWith(b,c):--f||g.resolveWith(b,c)}},i,j,k;if(d>1)for(i=new Array(d),j=new Array(d),k=new Array(d);d>b;b++)c[b]&&n.isFunction(c[b].promise)?c[b].promise().progress(h(b,j,i)).done(h(b,k,c)).fail(g.reject):--f;return f||g.resolveWith(k,c),g.promise()}});var I;n.fn.ready=function(a){return n.ready.promise().done(a),this},n.extend({isReady:!1,readyWait:1,holdReady:function(a){a?n.readyWait++:n.ready(!0)},ready:function(a){(a===!0?--n.readyWait:n.isReady)||(n.isReady=!0,a!==!0&&--n.readyWait>0||(I.resolveWith(d,[n]),n.fn.triggerHandler&&(n(d).triggerHandler("ready"),n(d).off("ready"))))}});function J(){d.addEventListener?(d.removeEventListener("DOMContentLoaded",K),a.removeEventListener("load",K)):(d.detachEvent("onreadystatechange",K),a.detachEvent("onload",K))}function K(){(d.addEventListener||"load"===a.event.type||"complete"===d.readyState)&&(J(),n.ready())}n.ready.promise=function(b){if(!I)if(I=n.Deferred(),"complete"===d.readyState||"loading"!==d.readyState&&!d.documentElement.doScroll)a.setTimeout(n.ready);else if(d.addEventListener)d.addEventListener("DOMContentLoaded",K),a.addEventListener("load",K);else{d.attachEvent("onreadystatechange",K),a.attachEvent("onload",K);var c=!1;try{c=null==a.frameElement&&d.documentElement}catch(e){}c&&c.doScroll&&!function f(){if(!n.isReady){try{c.doScroll("left")}catch(b){return a.setTimeout(f,50)}J(),n.ready()}}()}return I.promise(b)},n.ready.promise();var L;for(L in n(l))break;l.ownFirst="0"===L,l.inlineBlockNeedsLayout=!1,n(function(){var a,b,c,e;c=d.getElementsByTagName("body")[0],c&&c.style&&(b=d.createElement("div"),e=d.createElement("div"),e.style.cssText="position:absolute;border:0;width:0;height:0;top:0;left:-9999px",c.appendChild(e).appendChild(b),"undefined"!=typeof b.style.zoom&&(b.style.cssText="display:inline;margin:0;border:0;padding:1px;width:1px;zoom:1",l.inlineBlockNeedsLayout=a=3===b.offsetWidth,a&&(c.style.zoom=1)),c.removeChild(e))}),function(){var a=d.createElement("div");l.deleteExpando=!0;try{delete a.test}catch(b){l.deleteExpando=!1}a=null}();var M=function(a){var b=n.noData[(a.nodeName+" ").toLowerCase()],c=+a.nodeType||1;return 1!==c&&9!==c?!1:!b||b!==!0&&a.getAttribute("classid")===b},N=/^(?:\{[\w\W]*\}|\[[\w\W]*\])$/,O=/([A-Z])/g;function P(a,b,c){if(void 0===c&&1===a.nodeType){var d="data-"+b.replace(O,"-$1").toLowerCase();if(c=a.getAttribute(d),"string"==typeof c){try{c="true"===c?!0:"false"===c?!1:"null"===c?null:+c+""===c?+c:N.test(c)?n.parseJSON(c):c}catch(e){}n.data(a,b,c)}else c=void 0; +}return c}function Q(a){var b;for(b in a)if(("data"!==b||!n.isEmptyObject(a[b]))&&"toJSON"!==b)return!1;return!0}function R(a,b,d,e){if(M(a)){var f,g,h=n.expando,i=a.nodeType,j=i?n.cache:a,k=i?a[h]:a[h]&&h;if(k&&j[k]&&(e||j[k].data)||void 0!==d||"string"!=typeof b)return k||(k=i?a[h]=c.pop()||n.guid++:h),j[k]||(j[k]=i?{}:{toJSON:n.noop}),"object"!=typeof b&&"function"!=typeof b||(e?j[k]=n.extend(j[k],b):j[k].data=n.extend(j[k].data,b)),g=j[k],e||(g.data||(g.data={}),g=g.data),void 0!==d&&(g[n.camelCase(b)]=d),"string"==typeof b?(f=g[b],null==f&&(f=g[n.camelCase(b)])):f=g,f}}function S(a,b,c){if(M(a)){var d,e,f=a.nodeType,g=f?n.cache:a,h=f?a[n.expando]:n.expando;if(g[h]){if(b&&(d=c?g[h]:g[h].data)){n.isArray(b)?b=b.concat(n.map(b,n.camelCase)):b in d?b=[b]:(b=n.camelCase(b),b=b in d?[b]:b.split(" ")),e=b.length;while(e--)delete d[b[e]];if(c?!Q(d):!n.isEmptyObject(d))return}(c||(delete g[h].data,Q(g[h])))&&(f?n.cleanData([a],!0):l.deleteExpando||g!=g.window?delete g[h]:g[h]=void 0)}}}n.extend({cache:{},noData:{"applet ":!0,"embed ":!0,"object ":"clsid:D27CDB6E-AE6D-11cf-96B8-444553540000"},hasData:function(a){return a=a.nodeType?n.cache[a[n.expando]]:a[n.expando],!!a&&!Q(a)},data:function(a,b,c){return R(a,b,c)},removeData:function(a,b){return S(a,b)},_data:function(a,b,c){return R(a,b,c,!0)},_removeData:function(a,b){return S(a,b,!0)}}),n.fn.extend({data:function(a,b){var c,d,e,f=this[0],g=f&&f.attributes;if(void 0===a){if(this.length&&(e=n.data(f),1===f.nodeType&&!n._data(f,"parsedAttrs"))){c=g.length;while(c--)g[c]&&(d=g[c].name,0===d.indexOf("data-")&&(d=n.camelCase(d.slice(5)),P(f,d,e[d])));n._data(f,"parsedAttrs",!0)}return e}return"object"==typeof a?this.each(function(){n.data(this,a)}):arguments.length>1?this.each(function(){n.data(this,a,b)}):f?P(f,a,n.data(f,a)):void 0},removeData:function(a){return this.each(function(){n.removeData(this,a)})}}),n.extend({queue:function(a,b,c){var d;return a?(b=(b||"fx")+"queue",d=n._data(a,b),c&&(!d||n.isArray(c)?d=n._data(a,b,n.makeArray(c)):d.push(c)),d||[]):void 0},dequeue:function(a,b){b=b||"fx";var c=n.queue(a,b),d=c.length,e=c.shift(),f=n._queueHooks(a,b),g=function(){n.dequeue(a,b)};"inprogress"===e&&(e=c.shift(),d--),e&&("fx"===b&&c.unshift("inprogress"),delete f.stop,e.call(a,g,f)),!d&&f&&f.empty.fire()},_queueHooks:function(a,b){var c=b+"queueHooks";return n._data(a,c)||n._data(a,c,{empty:n.Callbacks("once memory").add(function(){n._removeData(a,b+"queue"),n._removeData(a,c)})})}}),n.fn.extend({queue:function(a,b){var c=2;return"string"!=typeof a&&(b=a,a="fx",c--),arguments.lengthh;h++)b(a[h],c,g?d:d.call(a[h],h,b(a[h],c)));return e?a:j?b.call(a):i?b(a[0],c):f},Z=/^(?:checkbox|radio)$/i,$=/<([\w:-]+)/,_=/^$|\/(?:java|ecma)script/i,aa=/^\s+/,ba="abbr|article|aside|audio|bdi|canvas|data|datalist|details|dialog|figcaption|figure|footer|header|hgroup|main|mark|meter|nav|output|picture|progress|section|summary|template|time|video";function ca(a){var b=ba.split("|"),c=a.createDocumentFragment();if(c.createElement)while(b.length)c.createElement(b.pop());return c}!function(){var a=d.createElement("div"),b=d.createDocumentFragment(),c=d.createElement("input");a.innerHTML="
a",l.leadingWhitespace=3===a.firstChild.nodeType,l.tbody=!a.getElementsByTagName("tbody").length,l.htmlSerialize=!!a.getElementsByTagName("link").length,l.html5Clone="<:nav>"!==d.createElement("nav").cloneNode(!0).outerHTML,c.type="checkbox",c.checked=!0,b.appendChild(c),l.appendChecked=c.checked,a.innerHTML="",l.noCloneChecked=!!a.cloneNode(!0).lastChild.defaultValue,b.appendChild(a),c=d.createElement("input"),c.setAttribute("type","radio"),c.setAttribute("checked","checked"),c.setAttribute("name","t"),a.appendChild(c),l.checkClone=a.cloneNode(!0).cloneNode(!0).lastChild.checked,l.noCloneEvent=!!a.addEventListener,a[n.expando]=1,l.attributes=!a.getAttribute(n.expando)}();var da={option:[1,""],legend:[1,"
","
"],area:[1,"",""],param:[1,"",""],thead:[1,"","
"],tr:[2,"","
"],col:[2,"","
"],td:[3,"","
"],_default:l.htmlSerialize?[0,"",""]:[1,"X
","
"]};da.optgroup=da.option,da.tbody=da.tfoot=da.colgroup=da.caption=da.thead,da.th=da.td;function ea(a,b){var c,d,e=0,f="undefined"!=typeof a.getElementsByTagName?a.getElementsByTagName(b||"*"):"undefined"!=typeof a.querySelectorAll?a.querySelectorAll(b||"*"):void 0;if(!f)for(f=[],c=a.childNodes||a;null!=(d=c[e]);e++)!b||n.nodeName(d,b)?f.push(d):n.merge(f,ea(d,b));return void 0===b||b&&n.nodeName(a,b)?n.merge([a],f):f}function fa(a,b){for(var c,d=0;null!=(c=a[d]);d++)n._data(c,"globalEval",!b||n._data(b[d],"globalEval"))}var ga=/<|&#?\w+;/,ha=/
"!==m[1]||ha.test(g)?0:i:i.firstChild,f=g&&g.childNodes.length;while(f--)n.nodeName(k=g.childNodes[f],"tbody")&&!k.childNodes.length&&g.removeChild(k)}n.merge(q,i.childNodes),i.textContent="";while(i.firstChild)i.removeChild(i.firstChild);i=p.lastChild}else q.push(b.createTextNode(g));i&&p.removeChild(i),l.appendChecked||n.grep(ea(q,"input"),ia),r=0;while(g=q[r++])if(d&&n.inArray(g,d)>-1)e&&e.push(g);else if(h=n.contains(g.ownerDocument,g),i=ea(p.appendChild(g),"script"),h&&fa(i),c){f=0;while(g=i[f++])_.test(g.type||"")&&c.push(g)}return i=null,p}!function(){var b,c,e=d.createElement("div");for(b in{submit:!0,change:!0,focusin:!0})c="on"+b,(l[b]=c in a)||(e.setAttribute(c,"t"),l[b]=e.attributes[c].expando===!1);e=null}();var ka=/^(?:input|select|textarea)$/i,la=/^key/,ma=/^(?:mouse|pointer|contextmenu|drag|drop)|click/,na=/^(?:focusinfocus|focusoutblur)$/,oa=/^([^.]*)(?:\.(.+)|)/;function pa(){return!0}function qa(){return!1}function ra(){try{return d.activeElement}catch(a){}}function sa(a,b,c,d,e,f){var g,h;if("object"==typeof b){"string"!=typeof c&&(d=d||c,c=void 0);for(h in b)sa(a,h,c,d,b[h],f);return a}if(null==d&&null==e?(e=c,d=c=void 0):null==e&&("string"==typeof c?(e=d,d=void 0):(e=d,d=c,c=void 0)),e===!1)e=qa;else if(!e)return a;return 1===f&&(g=e,e=function(a){return n().off(a),g.apply(this,arguments)},e.guid=g.guid||(g.guid=n.guid++)),a.each(function(){n.event.add(this,b,e,d,c)})}n.event={global:{},add:function(a,b,c,d,e){var f,g,h,i,j,k,l,m,o,p,q,r=n._data(a);if(r){c.handler&&(i=c,c=i.handler,e=i.selector),c.guid||(c.guid=n.guid++),(g=r.events)||(g=r.events={}),(k=r.handle)||(k=r.handle=function(a){return"undefined"==typeof n||a&&n.event.triggered===a.type?void 0:n.event.dispatch.apply(k.elem,arguments)},k.elem=a),b=(b||"").match(G)||[""],h=b.length;while(h--)f=oa.exec(b[h])||[],o=q=f[1],p=(f[2]||"").split(".").sort(),o&&(j=n.event.special[o]||{},o=(e?j.delegateType:j.bindType)||o,j=n.event.special[o]||{},l=n.extend({type:o,origType:q,data:d,handler:c,guid:c.guid,selector:e,needsContext:e&&n.expr.match.needsContext.test(e),namespace:p.join(".")},i),(m=g[o])||(m=g[o]=[],m.delegateCount=0,j.setup&&j.setup.call(a,d,p,k)!==!1||(a.addEventListener?a.addEventListener(o,k,!1):a.attachEvent&&a.attachEvent("on"+o,k))),j.add&&(j.add.call(a,l),l.handler.guid||(l.handler.guid=c.guid)),e?m.splice(m.delegateCount++,0,l):m.push(l),n.event.global[o]=!0);a=null}},remove:function(a,b,c,d,e){var f,g,h,i,j,k,l,m,o,p,q,r=n.hasData(a)&&n._data(a);if(r&&(k=r.events)){b=(b||"").match(G)||[""],j=b.length;while(j--)if(h=oa.exec(b[j])||[],o=q=h[1],p=(h[2]||"").split(".").sort(),o){l=n.event.special[o]||{},o=(d?l.delegateType:l.bindType)||o,m=k[o]||[],h=h[2]&&new RegExp("(^|\\.)"+p.join("\\.(?:.*\\.|)")+"(\\.|$)"),i=f=m.length;while(f--)g=m[f],!e&&q!==g.origType||c&&c.guid!==g.guid||h&&!h.test(g.namespace)||d&&d!==g.selector&&("**"!==d||!g.selector)||(m.splice(f,1),g.selector&&m.delegateCount--,l.remove&&l.remove.call(a,g));i&&!m.length&&(l.teardown&&l.teardown.call(a,p,r.handle)!==!1||n.removeEvent(a,o,r.handle),delete k[o])}else for(o in k)n.event.remove(a,o+b[j],c,d,!0);n.isEmptyObject(k)&&(delete r.handle,n._removeData(a,"events"))}},trigger:function(b,c,e,f){var g,h,i,j,l,m,o,p=[e||d],q=k.call(b,"type")?b.type:b,r=k.call(b,"namespace")?b.namespace.split("."):[];if(i=m=e=e||d,3!==e.nodeType&&8!==e.nodeType&&!na.test(q+n.event.triggered)&&(q.indexOf(".")>-1&&(r=q.split("."),q=r.shift(),r.sort()),h=q.indexOf(":")<0&&"on"+q,b=b[n.expando]?b:new n.Event(q,"object"==typeof b&&b),b.isTrigger=f?2:3,b.namespace=r.join("."),b.rnamespace=b.namespace?new RegExp("(^|\\.)"+r.join("\\.(?:.*\\.|)")+"(\\.|$)"):null,b.result=void 0,b.target||(b.target=e),c=null==c?[b]:n.makeArray(c,[b]),l=n.event.special[q]||{},f||!l.trigger||l.trigger.apply(e,c)!==!1)){if(!f&&!l.noBubble&&!n.isWindow(e)){for(j=l.delegateType||q,na.test(j+q)||(i=i.parentNode);i;i=i.parentNode)p.push(i),m=i;m===(e.ownerDocument||d)&&p.push(m.defaultView||m.parentWindow||a)}o=0;while((i=p[o++])&&!b.isPropagationStopped())b.type=o>1?j:l.bindType||q,g=(n._data(i,"events")||{})[b.type]&&n._data(i,"handle"),g&&g.apply(i,c),g=h&&i[h],g&&g.apply&&M(i)&&(b.result=g.apply(i,c),b.result===!1&&b.preventDefault());if(b.type=q,!f&&!b.isDefaultPrevented()&&(!l._default||l._default.apply(p.pop(),c)===!1)&&M(e)&&h&&e[q]&&!n.isWindow(e)){m=e[h],m&&(e[h]=null),n.event.triggered=q;try{e[q]()}catch(s){}n.event.triggered=void 0,m&&(e[h]=m)}return b.result}},dispatch:function(a){a=n.event.fix(a);var b,c,d,f,g,h=[],i=e.call(arguments),j=(n._data(this,"events")||{})[a.type]||[],k=n.event.special[a.type]||{};if(i[0]=a,a.delegateTarget=this,!k.preDispatch||k.preDispatch.call(this,a)!==!1){h=n.event.handlers.call(this,a,j),b=0;while((f=h[b++])&&!a.isPropagationStopped()){a.currentTarget=f.elem,c=0;while((g=f.handlers[c++])&&!a.isImmediatePropagationStopped())a.rnamespace&&!a.rnamespace.test(g.namespace)||(a.handleObj=g,a.data=g.data,d=((n.event.special[g.origType]||{}).handle||g.handler).apply(f.elem,i),void 0!==d&&(a.result=d)===!1&&(a.preventDefault(),a.stopPropagation()))}return k.postDispatch&&k.postDispatch.call(this,a),a.result}},handlers:function(a,b){var c,d,e,f,g=[],h=b.delegateCount,i=a.target;if(h&&i.nodeType&&("click"!==a.type||isNaN(a.button)||a.button<1))for(;i!=this;i=i.parentNode||this)if(1===i.nodeType&&(i.disabled!==!0||"click"!==a.type)){for(d=[],c=0;h>c;c++)f=b[c],e=f.selector+" ",void 0===d[e]&&(d[e]=f.needsContext?n(e,this).index(i)>-1:n.find(e,this,null,[i]).length),d[e]&&d.push(f);d.length&&g.push({elem:i,handlers:d})}return h]","i"),va=/<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:-]+)[^>]*)\/>/gi,wa=/\s*$/g,Aa=ca(d),Ba=Aa.appendChild(d.createElement("div"));function Ca(a,b){return n.nodeName(a,"table")&&n.nodeName(11!==b.nodeType?b:b.firstChild,"tr")?a.getElementsByTagName("tbody")[0]||a.appendChild(a.ownerDocument.createElement("tbody")):a}function Da(a){return a.type=(null!==n.find.attr(a,"type"))+"/"+a.type,a}function Ea(a){var b=ya.exec(a.type);return b?a.type=b[1]:a.removeAttribute("type"),a}function Fa(a,b){if(1===b.nodeType&&n.hasData(a)){var c,d,e,f=n._data(a),g=n._data(b,f),h=f.events;if(h){delete g.handle,g.events={};for(c in h)for(d=0,e=h[c].length;e>d;d++)n.event.add(b,c,h[c][d])}g.data&&(g.data=n.extend({},g.data))}}function Ga(a,b){var c,d,e;if(1===b.nodeType){if(c=b.nodeName.toLowerCase(),!l.noCloneEvent&&b[n.expando]){e=n._data(b);for(d in e.events)n.removeEvent(b,d,e.handle);b.removeAttribute(n.expando)}"script"===c&&b.text!==a.text?(Da(b).text=a.text,Ea(b)):"object"===c?(b.parentNode&&(b.outerHTML=a.outerHTML),l.html5Clone&&a.innerHTML&&!n.trim(b.innerHTML)&&(b.innerHTML=a.innerHTML)):"input"===c&&Z.test(a.type)?(b.defaultChecked=b.checked=a.checked,b.value!==a.value&&(b.value=a.value)):"option"===c?b.defaultSelected=b.selected=a.defaultSelected:"input"!==c&&"textarea"!==c||(b.defaultValue=a.defaultValue)}}function Ha(a,b,c,d){b=f.apply([],b);var e,g,h,i,j,k,m=0,o=a.length,p=o-1,q=b[0],r=n.isFunction(q);if(r||o>1&&"string"==typeof q&&!l.checkClone&&xa.test(q))return a.each(function(e){var f=a.eq(e);r&&(b[0]=q.call(this,e,f.html())),Ha(f,b,c,d)});if(o&&(k=ja(b,a[0].ownerDocument,!1,a,d),e=k.firstChild,1===k.childNodes.length&&(k=e),e||d)){for(i=n.map(ea(k,"script"),Da),h=i.length;o>m;m++)g=k,m!==p&&(g=n.clone(g,!0,!0),h&&n.merge(i,ea(g,"script"))),c.call(a[m],g,m);if(h)for(j=i[i.length-1].ownerDocument,n.map(i,Ea),m=0;h>m;m++)g=i[m],_.test(g.type||"")&&!n._data(g,"globalEval")&&n.contains(j,g)&&(g.src?n._evalUrl&&n._evalUrl(g.src):n.globalEval((g.text||g.textContent||g.innerHTML||"").replace(za,"")));k=e=null}return a}function Ia(a,b,c){for(var d,e=b?n.filter(b,a):a,f=0;null!=(d=e[f]);f++)c||1!==d.nodeType||n.cleanData(ea(d)),d.parentNode&&(c&&n.contains(d.ownerDocument,d)&&fa(ea(d,"script")),d.parentNode.removeChild(d));return a}n.extend({htmlPrefilter:function(a){return a.replace(va,"<$1>")},clone:function(a,b,c){var d,e,f,g,h,i=n.contains(a.ownerDocument,a);if(l.html5Clone||n.isXMLDoc(a)||!ua.test("<"+a.nodeName+">")?f=a.cloneNode(!0):(Ba.innerHTML=a.outerHTML,Ba.removeChild(f=Ba.firstChild)),!(l.noCloneEvent&&l.noCloneChecked||1!==a.nodeType&&11!==a.nodeType||n.isXMLDoc(a)))for(d=ea(f),h=ea(a),g=0;null!=(e=h[g]);++g)d[g]&&Ga(e,d[g]);if(b)if(c)for(h=h||ea(a),d=d||ea(f),g=0;null!=(e=h[g]);g++)Fa(e,d[g]);else Fa(a,f);return d=ea(f,"script"),d.length>0&&fa(d,!i&&ea(a,"script")),d=h=e=null,f},cleanData:function(a,b){for(var d,e,f,g,h=0,i=n.expando,j=n.cache,k=l.attributes,m=n.event.special;null!=(d=a[h]);h++)if((b||M(d))&&(f=d[i],g=f&&j[f])){if(g.events)for(e in g.events)m[e]?n.event.remove(d,e):n.removeEvent(d,e,g.handle);j[f]&&(delete j[f],k||"undefined"==typeof d.removeAttribute?d[i]=void 0:d.removeAttribute(i),c.push(f))}}}),n.fn.extend({domManip:Ha,detach:function(a){return Ia(this,a,!0)},remove:function(a){return Ia(this,a)},text:function(a){return Y(this,function(a){return void 0===a?n.text(this):this.empty().append((this[0]&&this[0].ownerDocument||d).createTextNode(a))},null,a,arguments.length)},append:function(){return Ha(this,arguments,function(a){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var b=Ca(this,a);b.appendChild(a)}})},prepend:function(){return Ha(this,arguments,function(a){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var b=Ca(this,a);b.insertBefore(a,b.firstChild)}})},before:function(){return Ha(this,arguments,function(a){this.parentNode&&this.parentNode.insertBefore(a,this)})},after:function(){return Ha(this,arguments,function(a){this.parentNode&&this.parentNode.insertBefore(a,this.nextSibling)})},empty:function(){for(var a,b=0;null!=(a=this[b]);b++){1===a.nodeType&&n.cleanData(ea(a,!1));while(a.firstChild)a.removeChild(a.firstChild);a.options&&n.nodeName(a,"select")&&(a.options.length=0)}return this},clone:function(a,b){return a=null==a?!1:a,b=null==b?a:b,this.map(function(){return n.clone(this,a,b)})},html:function(a){return Y(this,function(a){var b=this[0]||{},c=0,d=this.length;if(void 0===a)return 1===b.nodeType?b.innerHTML.replace(ta,""):void 0;if("string"==typeof a&&!wa.test(a)&&(l.htmlSerialize||!ua.test(a))&&(l.leadingWhitespace||!aa.test(a))&&!da[($.exec(a)||["",""])[1].toLowerCase()]){a=n.htmlPrefilter(a);try{for(;d>c;c++)b=this[c]||{},1===b.nodeType&&(n.cleanData(ea(b,!1)),b.innerHTML=a);b=0}catch(e){}}b&&this.empty().append(a)},null,a,arguments.length)},replaceWith:function(){var a=[];return Ha(this,arguments,function(b){var c=this.parentNode;n.inArray(this,a)<0&&(n.cleanData(ea(this)),c&&c.replaceChild(b,this))},a)}}),n.each({appendTo:"append",prependTo:"prepend",insertBefore:"before",insertAfter:"after",replaceAll:"replaceWith"},function(a,b){n.fn[a]=function(a){for(var c,d=0,e=[],f=n(a),h=f.length-1;h>=d;d++)c=d===h?this:this.clone(!0),n(f[d])[b](c),g.apply(e,c.get());return this.pushStack(e)}});var Ja,Ka={HTML:"block",BODY:"block"};function La(a,b){var c=n(b.createElement(a)).appendTo(b.body),d=n.css(c[0],"display");return c.detach(),d}function Ma(a){var b=d,c=Ka[a];return c||(c=La(a,b),"none"!==c&&c||(Ja=(Ja||n("