From 7ff8cd834dd9219bbbb238cef37c9525ffee2eba Mon Sep 17 00:00:00 2001 From: Paul Derbyshire Date: Mon, 29 Jul 2019 13:54:02 +0200 Subject: [PATCH 01/74] bump to 1.9.0 --- src/version.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/version.h b/src/version.h index 742cca2da..7e23fdfc1 100644 --- a/src/version.h +++ b/src/version.h @@ -6,5 +6,5 @@ #pragma once #define APP_NAME "EMS-ESP" -#define APP_VERSION "1.8.1b24" +#define APP_VERSION "1.9.0" #define APP_HOSTNAME "ems-esp" From dab66ce9181888852817abe78e062e3923c75503 Mon Sep 17 00:00:00 2001 From: Susis Strolch Date: Mon, 22 Jul 2019 10:39:47 +0200 Subject: [PATCH 02/74] =?UTF-8?q?fixing=20tx=5Fmode=202=20=C2=B0=20stabili?= =?UTF-8?q?ze=20emsuart=5Frx...=20=20=20We=20can=20get=20more=20than=2032?= =?UTF-8?q?=20bytes=20because=20of=20the=20trailing=20BRK.=20=20=20So=20th?= =?UTF-8?q?e=20buffersize=20for=20Rx=20interrupt=20is=20(for=20safety)=20i?= =?UTF-8?q?ncreased=20to=2036=20bytes.=20=20=20If=20length=20exceeds=2036?= =?UTF-8?q?=20bytes=20we=20dump=20them=20to=20/dev/null?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ° reintroduced the phantomBreak flag again We _must_ signal to Rx that we have a double break, otherwise we get problems in emsuart_recvTask... ° add ems_dumpBuffer which shows TxBuffer before send and RxBuffer after receive and applying phantomBreak. The dump is activated in "log j" mode and used to debug the protocol problems. ° change handling of ID bit 7 on system start we listen for telegram until we receive a valid one, larger than 5 byte. Depending on the bit7 of the source address we decide if we have a Buderus EMS or a Junkers EMS bus. This decision is used to set the variables emsIDMask (0x00 for Buderus, 0x80 for Junkers) and the emsPollAck buffer, used to send the propper acknowledge, depending on EMS type. ° move poll acknowledge function (emsuart_tx_poll) from emsuart.cpp to ems.cpp and rename to ems_pollAck ° add EMS_TX_REV_DETECT status for detecting the SourceID.7 bit and setting emsIDMask and emsPollAck buffer accordingly ° set initial emsTxStatus to EMS_TX_REV_DETECT ° add 'log j' - jabber - for more extensive debug logs --- src/ems-esp.cpp | 3 ++ src/ems.cpp | 139 +++++++++++++++++++++++++++++++++++++++--------- src/ems.h | 58 ++++++++++---------- src/emsuart.cpp | 49 +++++++++-------- src/emsuart.h | 1 - 5 files changed, 174 insertions(+), 76 deletions(-) diff --git a/src/ems-esp.cpp b/src/ems-esp.cpp index 5733a8367..3632f6644 100644 --- a/src/ems-esp.cpp +++ b/src/ems-esp.cpp @@ -1467,6 +1467,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_NONE); + ok = true; } } diff --git a/src/ems.cpp b/src/ems.cpp index b33c6c597..6bc10ddf9 100644 --- a/src/ems.cpp +++ b/src/ems.cpp @@ -226,7 +226,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 +235,9 @@ 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; @@ -361,11 +362,12 @@ void ems_setTxMode(uint8_t 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; + EMS_Sys_Status.emsIDMask = 0x80; myDebug_P(PSTR("Forcing emsReverse for Junkers")); } else { - EMS_Sys_Status.emsReverse = false; + EMS_Sys_Status.emsIDMask = 0x00; } + EMS_Sys_Status.emsPollAck[0] = EMS_ID_ME ^ EMS_Sys_Status.emsIDMask; } uint8_t ems_getTxMode() { @@ -453,10 +455,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) @@ -615,20 +626,26 @@ void _ems_sendTelegram() { } 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! + 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; } 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+ @@ -671,9 +688,17 @@ void _ems_sendTelegram() { } // send the telegram to the UART Tx - emsuart_tx_buffer(EMS_TxTelegram.data, EMS_TxTelegram.length); + _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! + // Tx Error! + myDebug_P(PSTR("** error sending buffer: %s"),_txStatus == EMS_TX_BRK_DETECT ? + "BRK" : "WDTO"); + EMS_Sys_Status.emsTxStatus = EMS_TX_STATUS_IDLE; + } - EMS_Sys_Status.emsTxStatus = EMS_TX_STATUS_WAIT; } @@ -720,6 +745,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}; + + if (EMS_Sys_Status.emsLogging != EMS_SYS_LOGGING_JABBER) + return; + + // 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 @@ -729,16 +805,28 @@ void _createValidate() { void ems_parseTelegram(uint8_t * telegram, uint8_t length) { static uint32_t _last_emsPollFrequency = 0; + ems_dumpBuffer("** [DEBUG MODE] ems_parseTelegram: ", telegram, length); /* * 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 + */ + + /* + * 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 (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) { @@ -752,8 +840,7 @@ void ems_parseTelegram(uint8_t * telegram, uint8_t length) { uint8_t value = telegram[0]; // 1st byte of data package // 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) { + if ((value ^ 0x80 ^ EMS_Sys_Status.emsIDMask) == EMS_ID_ME) { EMS_Sys_Status.emsTxCapable = true; uint32_t timenow_microsecs = micros(); EMS_Sys_Status.emsPollFrequency = (timenow_microsecs - _last_emsPollFrequency); @@ -766,7 +853,7 @@ void ems_parseTelegram(uint8_t * telegram, uint8_t length) { } 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,14 +861,14 @@ 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")); } - emsuart_tx_poll(); // send a poll to free the EMS bus + ems_tx_pollAck(); // send a poll to free the EMS bus _removeTxQueue(); // remove from queue } } @@ -792,7 +879,7 @@ 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 if (length <= 4) { - //_debugPrintTelegram("Noisy data:", &EMS_RxTelegram COLOR_RED); + _debugPrintTelegram("Noisy data:", &EMS_RxTelegram, COLOR_RED); return; } @@ -1035,7 +1122,7 @@ 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); + _debugPrintTelegram("echo:", EMS_RxTelegram, COLOR_WHITE); return; } @@ -1143,10 +1230,9 @@ void _processType(_EMS_RxTelegram * EMS_RxTelegram) { } } - 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 @@ -1684,7 +1770,8 @@ void _process_Version(_EMS_RxTelegram * EMS_RxTelegram) { // 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 diff --git a/src/ems.h b/src/ems.h index 800577200..d06ba7f19 100644 --- a/src/ems.h +++ b/src/ems.h @@ -29,39 +29,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 @@ -124,7 +124,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,7 +146,8 @@ 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 @@ -164,8 +166,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 } _EMS_Sys_Status; // The Tx send package @@ -390,6 +393,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); diff --git a/src/emsuart.cpp b/src/emsuart.cpp index a701d25c3..a0bb11411 100644 --- a/src/emsuart.cpp +++ b/src/emsuart.cpp @@ -13,6 +13,7 @@ _EMSRxBuf * pEMSRxBuf; _EMSRxBuf * paEMSRxBuf[EMS_MAXBUFFERS]; uint8_t emsRxBufIdx = 0; +uint8_t phantomBreak= 0; os_event_t recvTaskQueue[EMSUART_recvTaskQueueLen]; // our Rx queue @@ -22,7 +23,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 +34,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,8 +49,9 @@ 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 + 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 @@ -67,18 +71,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 +129,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 +140,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); @@ -199,6 +207,7 @@ 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; + 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 @@ -254,9 +263,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 +281,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 +309,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); } @@ -317,13 +327,8 @@ _EMS_TX_STATUS ICACHE_FLASH_ATTR emsuart_tx_buffer(uint8_t * buf, uint8_t len) { /* * Send the Poll (our own ID) to Tx as a single byte and end with a - */ + * *** moved to ems.cpp, renamed to ems_tx_pollAck 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); + static uint8_t buf[1] = {EMS_ID_ME ^ EMS_Sys_Status.emsIDMask}; } + */ \ No newline at end of file diff --git a/src/emsuart.h b/src/emsuart.h index 0777dcfd5..3a4f9da7e 100644 --- a/src/emsuart.h +++ b/src/emsuart.h @@ -37,4 +37,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(); From e680ae6066694cf566eba3d5d7deae13c21fa379 Mon Sep 17 00:00:00 2001 From: Susis Strolch Date: Mon, 29 Jul 2019 10:00:52 +0200 Subject: [PATCH 03/74] =?UTF-8?q?=C2=B0=20fix=20compile=20time=20error=20i?= =?UTF-8?q?n=20ems.cpp=20(missing=20comment)=20=C2=B0=20show=20'echo:teleg?= =?UTF-8?q?ram'=20msg=20only=20in=20jabber=20mode=20=C2=B0=20fix=20wrong?= =?UTF-8?q?=20value=20when=20applying=20'log=20j'?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/ems-esp.cpp | 2 +- src/ems.cpp | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/ems-esp.cpp b/src/ems-esp.cpp index 3632f6644..d3468c2e1 100644 --- a/src/ems-esp.cpp +++ b/src/ems-esp.cpp @@ -1468,7 +1468,7 @@ void TelnetCommandCallback(uint8_t wc, const char * commandLine) { ems_setLogging(EMS_SYS_LOGGING_NONE); ok = true; } else if (strcmp(second_cmd, "j") == 0) { - ems_setLogging(EMS_SYS_LOGGING_NONE); + ems_setLogging(EMS_SYS_LOGGING_JABBER); ok = true; } } diff --git a/src/ems.cpp b/src/ems.cpp index 6bc10ddf9..dbe01ac2d 100644 --- a/src/ems.cpp +++ b/src/ems.cpp @@ -879,7 +879,7 @@ 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 if (length <= 4) { - _debugPrintTelegram("Noisy data:", &EMS_RxTelegram, COLOR_RED); + // _debugPrintTelegram("Noisy data:", &EMS_RxTelegram, COLOR_RED); return; } @@ -1122,7 +1122,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; } From a32da99618bfec331b60f78ca48e9659019e10a5 Mon Sep 17 00:00:00 2001 From: Susis Strolch Date: Mon, 29 Jul 2019 18:12:35 +0200 Subject: [PATCH 04/74] txmode2 / Junkers autodetect * reduce logs in jabber mode * disable explicit test for Junkers * add HT3 document --- HT3-Bus_Telegramme.html | 7862 +++++++++++++++++++++++++++++++++++++++ src/ems-esp.cpp | 2 + src/ems.cpp | 13 +- 3 files changed, 7873 insertions(+), 4 deletions(-) create mode 100644 HT3-Bus_Telegramme.html diff --git a/HT3-Bus_Telegramme.html b/HT3-Bus_Telegramme.html new file mode 100644 index 000000000..1a019762c --- /dev/null +++ b/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/src/ems-esp.cpp b/src/ems-esp.cpp index d3468c2e1..b1e27b031 100644 --- a/src/ems-esp.cpp +++ b/src/ems-esp.cpp @@ -419,6 +419,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")); } diff --git a/src/ems.cpp b/src/ems.cpp index dbe01ac2d..56128df59 100644 --- a/src/ems.cpp +++ b/src/ems.cpp @@ -441,7 +441,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")); @@ -642,7 +642,7 @@ void _ems_sendTelegram() { // 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 ^ EMS_Sys_Status.emsIDMask); // read has 8th bit set @@ -756,6 +756,7 @@ void ems_dumpBuffer(const char *prefix, uint8_t *telegram, uint8_t length) { if (EMS_Sys_Status.emsLogging != EMS_SYS_LOGGING_JABBER) return; +/* // we only care about known devices if (length) { uint8_t dev = telegram[0] & 0x7F; @@ -763,6 +764,7 @@ void ems_dumpBuffer(const char *prefix, uint8_t *telegram, uint8_t length) { ||(dev == 0x01)||(dev == 0x0b)||(dev == 0x10))) return; } +*/ strlcpy(output_str, "(", sizeof(output_str)); strlcat(output_str, COLOR_CYAN, sizeof(output_str)); @@ -796,6 +798,7 @@ void ems_dumpBuffer(const char *prefix, uint8_t *telegram, uint8_t length) { 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 @@ -805,7 +808,7 @@ void ems_dumpBuffer(const char *prefix, uint8_t *telegram, uint8_t length) { void ems_parseTelegram(uint8_t * telegram, uint8_t length) { static uint32_t _last_emsPollFrequency = 0; - ems_dumpBuffer("** [DEBUG MODE] ems_parseTelegram: ", telegram, length); + ems_dumpBuffer("ems_parseTelegram: ", telegram, length); /* * 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) @@ -927,7 +930,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); } @@ -1915,9 +1918,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 } /* From 573ebcffeb910c2931870391ff1eb8d19515c4aa Mon Sep 17 00:00:00 2001 From: Susis Strolch Date: Mon, 29 Jul 2019 21:35:22 +0200 Subject: [PATCH 05/74] txstatus: remove all references and settings --- src/ems-esp.cpp | 11 -------- src/ems.cpp | 19 -------------- src/emsuart.cpp | 68 +------------------------------------------------ 3 files changed, 1 insertion(+), 97 deletions(-) diff --git a/src/ems-esp.cpp b/src/ems-esp.cpp index b1e27b031..0e0ae8c3c 100644 --- a/src/ems-esp.cpp +++ b/src/ems-esp.cpp @@ -1147,8 +1147,6 @@ bool FSCallback(MYESP_FSACTION action, const JsonObject json) { 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"]; ems_setTxDisabled(EMSESP_Status.listen_mode); @@ -1170,8 +1168,6 @@ bool FSCallback(MYESP_FSACTION action, const JsonObject json) { 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(); - return true; } @@ -1300,12 +1296,6 @@ bool SettingsCallback(MYESP_FSACTION action, uint8_t wc, const char * setting, c myDebug_P(PSTR("Error. Usage: set heating_circuit <1 | 2>")); } } - - // tx delay/ tx mode - if (((strcmp(setting, "tx_mode") == 0) || (strcmp(setting, "tx_delay") == 0)) && (wc == 2)) { - ems_setTxMode(atoi(value)); - ok = true; - } } if (action == MYESP_FSACTION_LIST) { @@ -1332,7 +1322,6 @@ bool SettingsCallback(MYESP_FSACTION action, uint8_t wc, const char * setting, c 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; diff --git a/src/ems.cpp b/src/ems.cpp index 56128df59..3b1422e4b 100644 --- a/src/ems.cpp +++ b/src/ems.cpp @@ -235,7 +235,6 @@ void ems_init() { EMS_Sys_Status.emsTxDisabled = false; EMS_Sys_Status.emsPollFrequency = 0; EMS_Sys_Status.txRetryCount = 0; - EMS_Sys_Status.emsTxMode = 0; EMS_Sys_Status.emsIDMask = 0x00; EMS_Sys_Status.emsPollAck[0] = EMS_ID_ME; @@ -356,24 +355,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.emsIDMask = 0x80; - myDebug_P(PSTR("Forcing emsReverse for Junkers")); - } else { - EMS_Sys_Status.emsIDMask = 0x00; - } - EMS_Sys_Status.emsPollAck[0] = EMS_ID_ME ^ EMS_Sys_Status.emsIDMask; -} - -uint8_t ems_getTxMode() { - return EMS_Sys_Status.emsTxMode; -} - bool ems_getEmsRefreshed() { return EMS_Sys_Status.emsRefreshed; } diff --git a/src/emsuart.cpp b/src/emsuart.cpp index a0bb11411..834718524 100644 --- a/src/emsuart.cpp +++ b/src/emsuart.cpp @@ -171,36 +171,6 @@ void ICACHE_FLASH_ATTR emsuart_start() { ETS_UART_INTR_ENABLE(); } -/* - * Send a BRK signal - * Which is a 11-bit set of zero's (11 cycles) - */ -void ICACHE_FLASH_ATTR emsuart_tx_brk() { - uint32_t tmp; - - // must make sure Tx FIFO is empty - while (((USS(EMSUART_UART) >> USTXC) & 0xFF) != 0) - ; - - tmp = ((1 << UCRXRST) | (1 << UCTXRST)); // bit mask - USC0(EMSUART_UART) |= (tmp); // set bits - USC0(EMSUART_UART) &= ~(tmp); // clear bits - - // To create a 11-bit we set TXD_BRK bit so the break signal will - // automatically be sent when the tx fifo is empty - tmp = (1 << UCBRK); - GPIO_H(TX_MARK_MASK); - USC0(EMSUART_UART) |= (tmp); // set bit - - if (EMS_Sys_Status.emsTxMode <= 1) { // classic mode and ems+ (0, 1) - delayMicroseconds(EMSUART_TX_BRK_WAIT); - } else if (EMS_Sys_Status.emsTxMode == 3) { // junkers mode - delayMicroseconds(EMSUART_TX_WAIT_BRK - EMSUART_TX_LAG); // 1144 (11 Bits) - } - - USC0(EMSUART_UART) &= ~(tmp); // clear bit - GPIO_L(TX_MARK_MASK); -} /* * Send to Tx, ending with a @@ -210,35 +180,7 @@ _EMS_TX_STATUS ICACHE_FLASH_ATTR emsuart_tx_buffer(uint8_t * buf, uint8_t len) { 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+ - for (uint8_t i = 0; i < len; i++) { - TX_PULSE(EMSUART_BIT_TIME / 4); - USF(EMSUART_UART) = buf[i]; - delayMicroseconds(EMSUART_TX_BRK_WAIT); // https://github.com/proddy/EMS-ESP/issues/23# - } - emsuart_tx_brk(); // send - } else if (EMS_Sys_Status.emsTxMode == 3) { // Junkers logic by @philrich - for (uint8_t i = 0; i < len; i++) { - TX_PULSE(EMSUART_BIT_TIME / 4); - USF(EMSUART_UART) = buf[i]; - - // just to be safe wait for tx fifo empty (needed?) - while (((USS(EMSUART_UART) >> USTXC) & 0xff) != 0) - ; - - // wait until bits are sent on wire - delayMicroseconds(EMSUART_TX_WAIT_BYTE - EMSUART_TX_LAG + EMSUART_TX_WAIT_GAP); - } - emsuart_tx_brk(); // send - } else if (EMS_Sys_Status.emsTxMode == 2) { - /* + /* * * 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. @@ -321,14 +263,6 @@ _EMS_TX_STATUS ICACHE_FLASH_ATTR emsuart_tx_buffer(uint8_t * buf, uint8_t len) { } ETS_UART_INTR_ENABLE(); // receive anything from FIFO... } - } return result; } -/* - * Send the Poll (our own ID) to Tx as a single byte and end with a - * *** moved to ems.cpp, renamed to ems_tx_pollAck -void ICACHE_FLASH_ATTR emsuart_tx_poll() { - static uint8_t buf[1] = {EMS_ID_ME ^ EMS_Sys_Status.emsIDMask}; -} - */ \ No newline at end of file From cb2427b75e7f0c79110553f21b3041ae56d55591 Mon Sep 17 00:00:00 2001 From: Paul Derbyshire Date: Mon, 29 Jul 2019 22:38:55 +0200 Subject: [PATCH 06/74] write to ems+ thermostats (https://github.com/proddy/EMS-ESP/issues/145) --- src/ems.cpp | 256 ++++++++++++++++++++++++---------------------- src/ems.h | 12 +-- src/ems_devices.h | 32 +++--- 3 files changed, 158 insertions(+), 142 deletions(-) diff --git a/src/ems.cpp b/src/ems.cpp index b33c6c597..404db9fc1 100644 --- a/src/ems.cpp +++ b/src/ems.cpp @@ -28,7 +28,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 +95,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 +174,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 +183,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 @@ -1325,7 +1322,7 @@ 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) { @@ -1361,14 +1358,6 @@ void _process_RCPLUSStatusMessage(_EMS_RxTelegram * EMS_RxTelegram) { } } -/** - * 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 -} - /** * type 0x01AF - summer/winter mode from the Nefit RC1010 thermostat (0x18) and RC300/310s on 0x10 */ @@ -1389,10 +1378,14 @@ 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 + 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 } /** @@ -1619,10 +1612,10 @@ void ems_clearDeviceList() { * add an EMS device to our list of detected devices */ void _addDevice(uint8_t product_id, uint8_t device_id, char * version, const char * model_string) { - _Generic_Type device; + _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; } @@ -1654,8 +1647,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,22 +1657,22 @@ 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(product_id, EMS_ID_BOILER, version, Boiler_Devices[i].model_string); // 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 @@ -1696,8 +1689,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,28 +1701,28 @@ 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(product_id, Thermostat_Devices[i].device_id, version, Thermostat_Devices[i].model_string); // 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)); @@ -1744,8 +1737,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 +1747,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(product_id, SolarModule_Devices[i].device_id, version, SolarModule_Devices[i].model_string); 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 +1767,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 +1777,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(product_id, HeatPump_Devices[i].device_id, version, HeatPump_Devices[i].model_string); 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 +1794,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,10 +1803,10 @@ 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(product_id, Other_Devices[i].device_id, version, Other_Devices[i].model_string); return; } else { myDebug_P(PSTR("Unrecognized device found: %s (DeviceID:0x%02X ProductID:%d Version:%s)"), EMS_RxTelegram->src, product_id, version); @@ -1921,39 +1914,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 } /** @@ -1996,8 +1989,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 +1998,7 @@ char * ems_getThermostatDescription(char * buffer) { } if (found) { - strlcpy(buffer, Thermostat_Types[i].model_string, size); + strlcpy(buffer, Thermostat_Devices[i].model_string, size); } else { strlcpy(buffer, "DeviceID: 0x", size); strlcat(buffer, _hextoa(EMS_Thermostat.device_id, tmp), size); @@ -2038,15 +2031,15 @@ 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); } else { strlcpy(buffer, "DeviceID: 0x", size); strlcat(buffer, _hextoa(EMS_Boiler.device_id, tmp), size); @@ -2079,15 +2072,15 @@ 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); } else { strlcpy(buffer, "DeviceID: 0x", size); strlcat(buffer, _hextoa(EMS_SolarModule.device_id, tmp), size); @@ -2120,15 +2113,15 @@ 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); } else { strlcpy(buffer, "DeviceID: 0x", size); strlcat(buffer, _hextoa(EMS_HeatPump.device_id, tmp), size); @@ -2160,17 +2153,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 +2187,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 +2224,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 +2247,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, @@ -2278,7 +2271,7 @@ uint8_t ems_printDevices_s(char * buffer, uint16_t len) { } char s[100]; - 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++) { 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); } @@ -2394,7 +2387,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,17 +2395,15 @@ 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; @@ -2425,6 +2416,15 @@ void ems_setThermostatTemp(float temperature, uint8_t temptype) { EMS_TxTelegram.type = EMS_TYPE_RC30Set; EMS_TxTelegram.offset = EMS_OFFSET_RC30Set_temp; EMS_TxTelegram.comparisonPostRead = EMS_TYPE_RC30StatusMessage; + + } 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 + } } else if ((model_id == EMS_MODEL_RC35) || (model_id == EMS_MODEL_ES73)) { switch (temptype) { case 1: // change the night temp @@ -2445,7 +2445,6 @@ void ems_setThermostatTemp(float temperature, uint8_t temptype) { } break; } - if (hc == 1) { // heating circuit 1 EMS_TxTelegram.type = EMS_TYPE_RC35Set_HC1; @@ -2458,7 +2457,7 @@ void ems_setThermostatTemp(float temperature, uint8_t temptype) { } EMS_TxTelegram.length = EMS_MIN_TELEGRAM_LENGTH; - EMS_TxTelegram.dataValue = (uint8_t)((float)temperature * (float)2); // value + EMS_TxTelegram.dataValue = (uint8_t)((float)temperature * (float)2); // value * 2 EMS_TxTelegram.type_validate = EMS_TxTelegram.type; EMS_TxTelegram.comparisonOffset = EMS_TxTelegram.offset; EMS_TxTelegram.comparisonValue = EMS_TxTelegram.dataValue; @@ -2468,8 +2467,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 +2481,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,7 +2501,7 @@ 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; @@ -2506,6 +2515,9 @@ void ems_setThermostatMode(uint8_t mode) { } 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; + } 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; } EMS_TxTelegram.type_validate = EMS_TxTelegram.type; // callback to EMS_TYPE_RC30Temperature to fetch temps diff --git a/src/ems.h b/src/ems.h index 800577200..fa21ba201 100644 --- a/src/ems.h +++ b/src/ems.h @@ -220,25 +220,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,7 +246,7 @@ typedef struct { uint8_t device_id; char model_string[50]; bool write_supported; -} _Thermostat_Type; +} _Thermostat_Device; // for consolidating all types typedef struct { @@ -254,7 +254,7 @@ typedef struct { uint8_t device_id; char version[10]; char model_string[50]; -} _Generic_Type; +} _Generic_Device; /* * Telegram package defintions diff --git a/src/ems_devices.h b/src/ems_devices.h index f33a1c88a..c581c0b16 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 @@ -106,13 +106,17 @@ // 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_TYPE_RCPLUSSet 0x01B9 // setpoint temp message and mode #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_RCPLUSGet_mode_day 10 // day/night mode +#define EMS_OFFSET_RCPLUSSet_mode 0 // operation mode (Auto=xFF, Manual=x00) #define EMS_OFFSET_RCPLUSStatusMessage_mode 0x0A // thermostat mode (auto, manual) +#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 // Junkers FR10, FW100 (EMS Plus) #define EMS_TYPE_JunkersStatusMessage 0x6F // is an automatic thermostat broadcast giving us temps @@ -120,7 +124,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 +161,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 +179,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 +189,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 +211,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}, @@ -226,7 +230,7 @@ const _Thermostat_Type Thermostat_Types[] = { {EMS_MODEL_RC20F, 93, 0x18, "RC20F", EMS_THERMOSTAT_WRITE_YES}, {EMS_MODEL_RC30, 78, 0x10, "RC30/Nefit Moduline 400", EMS_THERMOSTAT_WRITE_YES}, {EMS_MODEL_RC35, 86, 0x10, "RC35", EMS_THERMOSTAT_WRITE_YES}, - {EMS_MODEL_RC300, 158, 0x10, "RC300/RC310/Nefit Moduline 3000", EMS_THERMOSTAT_WRITE_NO}, + {EMS_MODEL_RC300, 158, 0x10, "RC300/RC310/Nefit Moduline 3000", EMS_THERMOSTAT_WRITE_YES}, {EMS_MODEL_1010, 165, 0x18, "Nefit Moduline 1010", EMS_THERMOSTAT_WRITE_NO}, // Sieger From 2a2a50b8c3ad8f009adde1661a4c6dc0065c8896 Mon Sep 17 00:00:00 2001 From: Paul Derbyshire Date: Mon, 29 Jul 2019 22:40:33 +0200 Subject: [PATCH 07/74] tidy up includes --- src/ems.cpp | 1 - src/emsuart.cpp | 1 - src/emsuart.h | 1 - 3 files changed, 3 deletions(-) diff --git a/src/ems.cpp b/src/ems.cpp index 404db9fc1..6c0318841 100644 --- a/src/ems.cpp +++ b/src/ems.cpp @@ -9,7 +9,6 @@ #include "ems.h" #include "ems_devices.h" #include "emsuart.h" -#include #include // https://github.com/rlogiacco/CircularBuffer #include #include // std::list diff --git a/src/emsuart.cpp b/src/emsuart.cpp index a701d25c3..0da551c42 100644 --- a/src/emsuart.cpp +++ b/src/emsuart.cpp @@ -7,7 +7,6 @@ #include "emsuart.h" #include "ems.h" -#include #include _EMSRxBuf * pEMSRxBuf; diff --git a/src/emsuart.h b/src/emsuart.h index 0777dcfd5..20d290f77 100644 --- a/src/emsuart.h +++ b/src/emsuart.h @@ -7,7 +7,6 @@ */ #pragma once -#include #include #define EMSUART_UART 0 // UART 0 From 53f3f44ae2dd97ff1ec87d9679802830d5357139 Mon Sep 17 00:00:00 2001 From: Susis Strolch Date: Wed, 31 Jul 2019 11:17:36 +0200 Subject: [PATCH 08/74] hunting the Software Watchdog restart MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ° unconditionaly show the last reset reason on telnet connect ° trying to enable stack trace also for Software watchdog events ° add wtdfeed() to MyESP loop --- lib/MyESP/MyESP.cpp | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/lib/MyESP/MyESP.cpp b/lib/MyESP/MyESP.cpp index 6d0077954..6d64cf284 100644 --- a/lib/MyESP/MyESP.cpp +++ b/lib/MyESP/MyESP.cpp @@ -188,6 +188,8 @@ void MyESP::_wifiCallback(justwifi_messages_t code, char * parameter) { // start OTA ArduinoOTA.begin(); // moved to support esp32 myDebug_P(PSTR("[OTA] listening to %s.local:%u"), ArduinoOTA.getHostname().c_str(), OTA_PORT); + // unconditionaly show the last reset reason + myDebug_P(PSTR("[SYSTEM] Last reset info: %s"), (char *)ESP.getResetInfo().c_str()); // MQTT Setup _mqtt_setup(); @@ -1005,9 +1007,10 @@ bool MyESP::_rtcmemStatus() { } 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_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: @@ -2159,12 +2162,14 @@ void MyESP::loop() { return; // quit if in the middle of an OTA update } + ESP.wdtFeed(); // feed the watchdog... _calculateLoad(); _systemCheckLoop(); _heartbeatCheck(); _bootupSequence(); webServer.handleClient(); _telnetHandle(); + ESP.wdtFeed(); // feed the watchdog... _mqttConnect(); yield(); // ...and breath From 89818b23bdc8e5fc532343adb99f617c4ee8526e Mon Sep 17 00:00:00 2001 From: Paul Date: Fri, 2 Aug 2019 09:25:15 +0200 Subject: [PATCH 09/74] 1.9.1 web --- CHANGELOG.md | 29 ++++++++++++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4e81e9f93..d6ae9ed36 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,34 @@ 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.1dev] 2019-07-27 +## [1.9.0b_web] 2019-07-31 + +### Breaking changes for first time use +- Make sure you update your local platformio.ini using the example one and set the target to 'debug' +- On first boot it will re-build the SPIFFS config file so all <1.9 settings will be lost. Connect to AP 'ems-esp' and use the web to enter your wifi settings. +- Default web admin password is 'admin' +- When building the firmware using the `debug` target the web front-end code is assembled and compressed. You'll need node and gulp for this. + - Download and install NodeJS from https://nodejs.org/en/download/ + - Navigate to the folder tools/webfilesbuilder + - `npm install --global gulp-cli` + - `npm install --save-dev gulp` + - `gulp` wil build the files. This is also done automatically in the platformio target + - Navigate to the folder tools/wsemulator + - `npm install ws` (from https://github.com/websockets/ws) + - ./run or ./run.sh will start the mock web server. Follow the instructions listed. + +### Added + +- New web code +- New MyESP code optimizations +- Magic Fairy dust + +### Know issues + +- SPIFFS event log may wear out ROM ? +- Timezone not working 100% + +## [1.8.1] 2019-07-27 ### Added From fc52f0545397db8f1e6531eb0166019440a86d21 Mon Sep 17 00:00:00 2001 From: Paul Date: Fri, 2 Aug 2019 09:26:13 +0200 Subject: [PATCH 10/74] 1.9.0 web - new implementation --- .gitignore | 47 +- platformio.ini-example | 36 +- scripts/buildweb.py | 3 + scripts/rename_fw.py | 10 + {lib/MyESP => src}/MyESP.cpp | 1707 +++++++---- {lib/MyESP => src}/MyESP.h | 230 +- src/Ntp.cpp | 49 + src/Ntp.h | 35 + {lib/TelnetSpy => src}/TelnetSpy.cpp | 4 +- {lib/TelnetSpy => src}/TelnetSpy.h | 0 src/TimeLib.cpp | 185 ++ src/TimeLib.h | 49 + src/custom.htm | 239 ++ src/custom.js | 141 + src/ems-esp.cpp | 276 +- src/ems.cpp | 52 +- src/ems.h | 8 +- src/emsuart.cpp | 4 +- src/my_config.h | 12 - src/version.h | 10 +- src/webh/.gitkeep | 0 .../3rdparty/css/bootstrap-3.3.7.min.css | 6 + .../css/footable.bootstrap-3.1.6.min.css | 1 + src/websrc/3rdparty/css/sidebar.css | 1 + .../fonts/glyphicons-halflings-regular.woff | Bin 0 -> 23424 bytes src/websrc/3rdparty/js/bootstrap-3.3.7.min.js | 7 + src/websrc/3rdparty/js/footable-3.1.6.min.js | 10 + src/websrc/3rdparty/js/jquery-1.12.4.min.js | 5 + src/websrc/index.html | 280 ++ src/websrc/myesp.htm | 484 +++ src/websrc/myesp.js | 934 ++++++ tools/webfilesbuilder/gulp.js | 19 + tools/webfilesbuilder/gulp.meta.js | 9 + tools/webfilesbuilder/gulpfile.js | 232 ++ tools/webfilesbuilder/package-lock.json | 2715 +++++++++++++++++ tools/webfilesbuilder/package.json | 24 + tools/wsemulator/package-lock.json | 27 + tools/wsemulator/package.json | 14 + tools/wsemulator/run.ps1 | 15 + tools/wsemulator/run.sh | 7 + tools/wsemulator/wserver.js | 250 ++ 41 files changed, 7206 insertions(+), 931 deletions(-) create mode 100644 scripts/buildweb.py rename {lib/MyESP => src}/MyESP.cpp (54%) rename {lib/MyESP => src}/MyESP.h (65%) create mode 100644 src/Ntp.cpp create mode 100644 src/Ntp.h rename {lib/TelnetSpy => src}/TelnetSpy.cpp (99%) rename {lib/TelnetSpy => src}/TelnetSpy.h (100%) create mode 100644 src/TimeLib.cpp create mode 100644 src/TimeLib.h create mode 100644 src/custom.htm create mode 100644 src/custom.js create mode 100644 src/webh/.gitkeep create mode 100644 src/websrc/3rdparty/css/bootstrap-3.3.7.min.css create mode 100644 src/websrc/3rdparty/css/footable.bootstrap-3.1.6.min.css create mode 100644 src/websrc/3rdparty/css/sidebar.css create mode 100644 src/websrc/3rdparty/fonts/glyphicons-halflings-regular.woff create mode 100644 src/websrc/3rdparty/js/bootstrap-3.3.7.min.js create mode 100644 src/websrc/3rdparty/js/footable-3.1.6.min.js create mode 100644 src/websrc/3rdparty/js/jquery-1.12.4.min.js create mode 100644 src/websrc/index.html create mode 100644 src/websrc/myesp.htm create mode 100644 src/websrc/myesp.js create mode 100644 tools/webfilesbuilder/gulp.js create mode 100644 tools/webfilesbuilder/gulp.meta.js create mode 100644 tools/webfilesbuilder/gulpfile.js create mode 100644 tools/webfilesbuilder/package-lock.json create mode 100644 tools/webfilesbuilder/package.json create mode 100644 tools/wsemulator/package-lock.json create mode 100644 tools/wsemulator/package.json create mode 100644 tools/wsemulator/run.ps1 create mode 100644 tools/wsemulator/run.sh create mode 100644 tools/wsemulator/wserver.js 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/platformio.ini-example b/platformio.ini-example index 3293cefdb..6c9c41794 100644 --- a/platformio.ini-example +++ b/platformio.ini-example @@ -3,45 +3,59 @@ ; [platformio] -;default_envs = release -default_envs = debug +default_envs = release +;default_envs = debug [common] ; -DMYESP_TIMESTAMP -DTESTS -DCRASH -DFORCE_SERIAL -DNO_GLOBAL_EEPROM -DLOGICANALYZER -extra_flags = -DNO_GLOBAL_EEPROM +general_flags = -DNO_GLOBAL_EEPROM [env] 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 +upload_protocol = espota +upload_port = ems-esp.local + +[env:buildweb] +extra_scripts = 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} -DCRASH -DTESTS +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 100644 --- 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/lib/MyESP/MyESP.cpp b/src/MyESP.cpp similarity index 54% rename from lib/MyESP/MyESP.cpp rename to src/MyESP.cpp index 6d0077954..971d86b07 100644 --- a/lib/MyESP/MyESP.cpp +++ b/src/MyESP.cpp @@ -1,9 +1,9 @@ /* - * MyESP - my ESP helper class to handle WiFi, MQTT and Telnet + * MyESP - my ESP helper class to handle WiFi, MQTT, Telnet, Web and other utils * * Paul Derbyshire - first revision: December 2018 * - * Ideas borrowed from Espurna https://github.com/xoseperez/espurna + * with ideas borrowed from Espurna https://github.com/xoseperez/espurna */ #include "MyESP.h" @@ -22,61 +22,85 @@ union system_rtcmem_t { uint32_t value; }; + +// nasty global variables that are called from internal ws functions +static char * _general_password = nullptr; +static bool _shouldReboot = false; + 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); + _general_hostname = strdup("myesp"); + _app_name = strdup("MyESP"); + _app_version = strdup(MYESP_VERSION); - _boottime = NULL; - _load_average = 100; // calculated load average + // 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; - _telnetcommand_callback = NULL; - _telnet_callback = NULL; + // telnet + _command[0] = '\0'; + _telnetcommand_callback_f = nullptr; + _telnet_callback_f = nullptr; - _command[0] = '\0'; + // fs + _fs_loadsave_callback_f = nullptr; + _fs_setlist_callback_f = nullptr; - _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 + _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); - _firstInstall = false; + // network + _network_password = nullptr; + _network_ssid = nullptr; + _network_wmode = 1; // default AP - _wifi_password = NULL; - _wifi_ssid = NULL; - _wifi_callback = NULL; - _wifi_connected = false; + _wifi_callback_f = nullptr; + _wifi_connected = false; - _ota_pre_callback = NULL; - _ota_post_callback = NULL; - _ota_doing_update = false; - - _suspendOutput = false; + // web + _web_callback_f = nullptr; + _webServer = new AsyncWebServer(80); + _ws = new AsyncWebSocket("/ws"); + _http_username = strdup("admin"); + _general_password = strdup(MYESP_HTTP_PASSWORD); // default naughty password + // system _rtcmem_status = false; _systemStable = true; + + // ntp + _ntp_server = strdup(MYESP_NTP_SERVER); + ; + _ntp_interval = 60; + _ntp_timezone = 1; + _ntp_enabled = false; + + // get the build time + _buildTime = _getBuildTime(); } MyESP::~MyESP() { @@ -85,6 +109,10 @@ MyESP::~MyESP() { // end void MyESP::end() { + SPIFFS.end(); + _ws->enable(false); + delete _webServer; + delete _ws; SerialAndTelnet.end(); jw.disconnect(); } @@ -141,12 +169,12 @@ void MyESP::myDebug_P(PGM_P format_P, ...) { // use Serial? bool MyESP::getUseSerial() { - return (_serial); + return (_general_serial); } // heartbeat bool MyESP::getHeartbeat() { - return (_heartbeat); + return (_mqtt_heartbeat); } // init heap ram @@ -169,12 +197,6 @@ uint32_t MyESP::_getUsedHeap() { // 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()); @@ -183,7 +205,7 @@ void MyESP::_wifiCallback(justwifi_messages_t code, char * parameter) { 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()); + myDebug_P(PSTR("[WIFI] HOST %s"), _getESPhostname().c_str()); // start OTA ArduinoOTA.begin(); // moved to support esp32 @@ -192,41 +214,51 @@ void MyESP::_wifiCallback(justwifi_messages_t code, char * parameter) { // MQTT Setup _mqtt_setup(); - _wifi_connected = true; - - // finally if we don't want Serial anymore, turn it off - if (!_serial) { + // 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 - SerialAndTelnet.setSerial(NULL); + setUseSerial(false); + } else { + myDebug_P(PSTR("[SYSTEM] Serial port communication is enabled.")); } - // call any final custom settings - if (_wifi_callback) { - _wifi_callback(); + _wifi_connected = true; + + // NTP now that we have a WiFi connection + if (_ntp_enabled) { + NTP.Ntp(_ntp_server, _ntp_timezone, _ntp_interval * 60); // 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) { - _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) { + // 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 - SerialAndTelnet.setSerial(NULL); + setUseSerial(false); + } else { + myDebug_P(PSTR("[SYSTEM] Serial port communication is enabled.")); } - // call any final custom settings - if (_wifi_callback) { - _wifi_callback(); + _wifi_connected = true; + + // call any final custom stuff + if (_wifi_callback_f) { + _wifi_callback_f(); } } @@ -309,16 +341,8 @@ void MyESP::_mqttOnMessage(char * topic, char * payload, size_t len) { 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_callback_f)(MQTT_MESSAGE_EVENT, topic, message); } // MQTT subscribe @@ -331,7 +355,7 @@ void MyESP::mqttSubscribe(const char * topic) { } // MQTT unsubscribe -// to MQTT_BASE/app_hostname/topic +// to MQTT_BASE with app_hostname/topic void MyESP::mqttUnsubscribe(const char * topic) { if (mqttClient.connected() && (strlen(topic) > 0)) { unsigned int packetId = mqttClient.unsubscribe(_mqttTopic(topic)); @@ -341,13 +365,13 @@ void MyESP::mqttUnsubscribe(const char * topic) { // 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); + // 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); } // MQTT onConnect - when a connect is established void MyESP::_mqttOnConnect() { - myDebug_P(PSTR("[MQTT] Connected")); + myDebug_P(PSTR("[MQTT] is connected")); _mqtt_reconnect_delay = MQTT_RECONNECT_DELAY_MIN; _mqtt_last_connection = millis(); @@ -359,16 +383,16 @@ void MyESP::_mqttOnConnect() { 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); + mqttSubscribe(MQTT_TOPIC_START); + mqttPublish(MQTT_TOPIC_START, MQTT_TOPIC_START_PAYLOAD); // call custom function to handle mqtt receives - (_mqtt_callback)(MQTT_CONNECT_EVENT, NULL, NULL); + (_mqtt_callback_f)(MQTT_CONNECT_EVENT, nullptr, nullptr); } // MQTT setup void MyESP::_mqtt_setup() { - if (!_mqtt_host) { + if (!_mqtt_enabled) { myDebug_P(PSTR("[MQTT] is disabled")); } @@ -377,7 +401,7 @@ void MyESP::_mqtt_setup() { 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 + (_mqtt_callback_f)(MQTT_DISCONNECT_EVENT, nullptr, nullptr); // call callback with disconnect } if (reason == AsyncMqttClientDisconnectReason::MQTT_IDENTIFIER_REJECTED) { myDebug_P(PSTR("[MQTT] Identifier Rejected")); @@ -398,7 +422,6 @@ void MyESP::_mqtt_setup() { }); //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) { @@ -408,16 +431,22 @@ void MyESP::_mqtt_setup() { // WiFI setup void MyESP::_wifi_setup() { - jw.setHostname(_app_hostname); // Set WIFI hostname + jw.setHostname(_general_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 + 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 @@ -426,8 +455,8 @@ void MyESP::_wifi_setup() { // 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_pre_callback_f = OTACallback_pre; + _ota_post_callback_f = OTACallback_post; } // OTA callback when the upload process starts @@ -450,28 +479,27 @@ void MyESP::_OTACallback() { #endif // stop the web server - webServer.close(); + // TODO: check if we need to stop webserver on OTA + _ws->closeAll(); + _webServer->end(); - _ota_doing_update = true; - - if (_ota_pre_callback) { - (_ota_pre_callback)(); // call custom function + if (_ota_pre_callback_f) { + (_ota_pre_callback_f)(); // call custom function } } // OTA Setup void MyESP::_ota_setup() { - if (!_wifi_ssid) { + if (!_network_ssid) { return; } ArduinoOTA.setPort(OTA_PORT); - ArduinoOTA.setHostname(_app_hostname); + ArduinoOTA.setHostname(_general_hostname); ArduinoOTA.onStart([this]() { _OTACallback(); }); ArduinoOTA.onEnd([this]() { myDebug_P(PSTR("[OTA] Done, restarting...")); - _ota_doing_update = false; _deferredReset(500, CUSTOM_RESET_OTA); }); @@ -503,14 +531,6 @@ void MyESP::_ota_setup() { }); } -// sets boottime -void MyESP::setBoottime(const char * boottime) { - if (_boottime) { - free(_boottime); - } - _boottime = strdup(boottime); -} - // eeprom void MyESP::_eeprom_setup() { #ifdef CRASH @@ -521,8 +541,8 @@ void MyESP::_eeprom_setup() { // 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; + _telnetcommand_callback_f = callback_cmd; // external function to handle commands + _telnet_callback_f = callback; } void MyESP::_telnetConnected() { @@ -539,15 +559,15 @@ void MyESP::_telnetConnected() { #endif // call callback - if (_telnet_callback) { - (_telnet_callback)(TELNET_EVENT_CONNECT); + if (_telnet_callback_f) { + (_telnet_callback_f)(TELNET_EVENT_CONNECT); } } void MyESP::_telnetDisconnected() { myDebug_P(PSTR("[TELNET] Telnet connection closed")); - if (_telnet_callback) { - (_telnet_callback)(TELNET_EVENT_DISCONNECT); // call callback + if (_telnet_callback_f) { + (_telnet_callback_f)(TELNET_EVENT_DISCONNECT); // call callback } } @@ -589,58 +609,93 @@ void MyESP::_consoleShowHelp() { #endif // call callback function - if (_telnet_callback) { - (_telnet_callback)(TELNET_EVENT_SHOWCMD); + 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("")); // newline - myDebug_P(PSTR("The following set commands are available:")); - myDebug_P(PSTR("")); // newline + 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 [value]")); + myDebug_P(PSTR(" set mqtt_enabled ")); + myDebug_P(PSTR(" set [value]")); + 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) { - (_telnet_callback)(TELNET_EVENT_SHOWSET); + if (_telnet_callback_f) { + (_telnet_callback_f)(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("")); + myDebug_P(PSTR("\nStored settings:\n")); + + if (_network_wmode == 0) { + myDebug_P(PSTR(" wifi_mode=client")); } else { - for (uint8_t i = 0; i < strlen(_wifi_password); i++) { + 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("")); // 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("")); + 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("")); // newline - myDebug_P(PSTR(" serial=%s"), (_serial) ? "on" : "off"); - myDebug_P(PSTR(" heartbeat=%s"), (_heartbeat) ? "on" : "off"); + myDebug_P(PSTR(" serial=%s"), (_general_serial) ? "on" : "off"); + myDebug_P(PSTR(" heartbeat=%s"), (_mqtt_heartbeat) ? "on" : "off"); + myDebug_P(PSTR(" ntp_enabled=%s"), (_ntp_enabled) ? "on" : "off"); // print any custom settings - (_fs_settings_callback)(MYESP_FSACTION_LIST, 0, NULL, NULL); + if (_fs_setlist_callback_f) { + (_fs_setlist_callback_f)(MYESP_FSACTION_LIST, 0, nullptr, nullptr); + } myDebug_P(PSTR("")); // newline } @@ -661,9 +716,9 @@ void MyESP::resetESP() { // 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 + return (strtok(nullptr, "\n")); // allow only newline } else { - return (strtok(NULL, ", \n")); // allow space and comma + return (strtok(nullptr, ", \n")); // allow space and comma } } @@ -671,7 +726,8 @@ char * MyESP::_telnet_readWord(bool allow_all_chars) { // 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; + bool save_config = false; + bool save_custom_config = false; // check for our internal commands first if (strcmp(setting, "erase") == 0) { @@ -679,91 +735,137 @@ bool MyESP::_changeSetting(uint8_t wc, const char * setting, const char * value) 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); + free(_network_ssid); + _network_ssid = strdup(value); } - ok = true; - jw.enableSTA(false); + save_config = 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); + free(_network_password); + _network_password = strdup(value); } - ok = true; - jw.enableSTA(false); + save_config = 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 + } else if (strcmp(setting, "wifi_mode") == 0) { if (value) { - _mqtt_host = strdup(value); + if (strcmp(value, "ap") == 0) { + _network_wmode = 1; + save_config = true; + myDebug_P(PSTR("Note: please 'reboot' ESP to apply new WiFi settings")); + } else if (strcmp(value, "client") == 0) { + _network_wmode = 0; + save_config = true; + myDebug_P(PSTR("Note: please 'reboot' ESP to apply new WiFi settings")); + } else { + save_config = false; + } } - ok = true; + + } 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 (_mqtt_username) - free(_mqtt_username); - _mqtt_username = NULL; // just to be sure if (value) { - _mqtt_username = strdup(value); + free(_mqtt_user); + _mqtt_user = strdup(value); } - ok = true; + save_config = true; } else if (strcmp(setting, "mqtt_password") == 0) { - if (_mqtt_password) - free(_mqtt_password); - _mqtt_password = NULL; // just to be sure if (value) { + free(_mqtt_password); _mqtt_password = strdup(value); } - ok = true; - - } else if (strcmp(setting, "serial") == 0) { - ok = true; - _serial = false; + 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) { - _serial = true; - ok = true; + _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("Reboot ESP to activate Serial mode.")); } else if (strcmp(value, "off") == 0) { - _serial = false; - ok = true; + _general_serial = false; + save_config = true; myDebug_P(PSTR("Reboot ESP to deactivate Serial mode.")); } else { - ok = false; + save_config = false; } } } else if (strcmp(setting, "heartbeat") == 0) { - ok = true; - _heartbeat = false; + save_config = true; if (value) { if (strcmp(value, "on") == 0) { - _heartbeat = true; - ok = true; + _mqtt_heartbeat = true; + save_config = true; myDebug_P(PSTR("Heartbeat on")); } else if (strcmp(value, "off") == 0) { - _heartbeat = false; - ok = true; + _mqtt_heartbeat = false; + save_config = true; myDebug_P(PSTR("Heartbeat off")); } else { - ok = false; + 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 - ok = (_fs_settings_callback)(MYESP_FSACTION_SET, wc, setting, value); + 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 (ok) { + if ((save_config || save_custom_config)) { // check for 2 params if (value == nullptr) { myDebug_P(PSTR("%s setting reset to its default value."), setting); @@ -771,10 +873,16 @@ bool MyESP::_changeSetting(uint8_t wc, const char * setting, const char * value) // must be 3 params myDebug_P(PSTR("%s changed."), setting); } + } - myDebug_P(PSTR("")); // newline + // now do the saving for system config if something has changed + if (save_config) { + ok = _fs_writeConfig(); + } - (void)fs_saveConfig(); // always save the values + // and see if we need to also save for custom config + if (save_custom_config) { + ok = _fs_createCustomConfig(); } return ok; @@ -782,8 +890,8 @@ bool MyESP::_changeSetting(uint8_t wc, const char * setting, const char * value) // force the serial on/off void MyESP::setUseSerial(bool b) { - _serial = b; - SerialAndTelnet.setSerial(b ? &Serial : NULL); + _general_serial = b; + SerialAndTelnet.setSerial(b ? &Serial : nullptr); } void MyESP::_telnetCommand(char * commandLine) { @@ -815,10 +923,10 @@ void MyESP::_telnetCommand(char * commandLine) { if (wc == 1) { _printSetCommands(); ok = true; - } else if (wc == 2) { // set + } else if (wc == 2) { // set char * setting = _telnet_readWord(false); - ok = _changeSetting(wc - 1, setting, NULL); - } else { // set + 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); @@ -868,8 +976,8 @@ void MyESP::_telnetCommand(char * commandLine) { #endif // call callback function - if (_telnetcommand_callback) { - (_telnetcommand_callback)(wc, commandLine); + if (_telnetcommand_callback_f) { + (_telnetcommand_callback_f)(wc, commandLine); } } @@ -886,33 +994,32 @@ String MyESP::_getESPhostname() { 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]); +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"}; - unsigned int month = 0; + uint8_t 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]); + uint8_t day = atoi(&date_now[3]); + uint16_t year = atoi(&date_now[7]); - char buffer[20]; + char buffer[30]; snprintf_P(buffer, sizeof(buffer), PSTR("%04d-%02d-%02d %02d:%02d:%02d"), year, month, day, hour, minute, second); - return String(buffer); + return (strdup(buffer)); } -// returns system uptime in seconds - copied for espurna. see (c) +// returns system uptime in seconds unsigned long MyESP::_getUptime() { static uint32_t last_uptime = 0; static uint8_t uptime_overflows = 0; @@ -921,7 +1028,7 @@ unsigned long MyESP::_getUptime() { ++uptime_overflows; } last_uptime = millis(); - uint32_t uptime_seconds = uptime_overflows * (UPTIME_OVERFLOW / 1000) + (last_uptime / 1000); + uint32_t uptime_seconds = uptime_overflows * (MYESP_UPTIME_OVERFLOW / 1000) + (last_uptime / 1000); return uptime_seconds; } @@ -999,6 +1106,7 @@ bool MyESP::_rtcmemStatus() { 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() } else { _setSystemBootStatus(MYESP_BOOTSTATUS_POWERON); } @@ -1044,7 +1152,7 @@ void MyESP::_deferredReset(unsigned long delaytime, uint8_t reason) { // 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 +// If the counter reaches MYESP_SYSTEM_CHECK_MAX then the system is flagged as unstable void MyESP::_setSystemCheck(bool stable) { uint8_t value = 0; @@ -1058,18 +1166,16 @@ void MyESP::_setSystemCheck(bool stable) { value = _getSystemStabilityCounter(); - if (++value > SYSTEM_CHECK_MAX) { + if (++value > MYESP_SYSTEM_CHECK_MAX) { _systemStable = false; value = 0; // system is unstable - myDebug_P(PSTR("[SYSTEM] Warning, system UNSTABLE.")); + myDebug_P(PSTR("[SYSTEM] Warning, system UNSTABLE. Serial mode is enabled.")); - /* // enable Serial again - if (!_serial) { + if (!_general_serial) { SerialAndTelnet.setSerial(&Serial); - _serial = true; + _general_serial = true; } - */ } } @@ -1084,7 +1190,7 @@ bool MyESP::_getSystemCheck() { // periodically check if system is stable void MyESP::_systemCheckLoop() { static bool checked = false; - if (!checked && (millis() > SYSTEM_CHECK_TIME)) { + if (!checked && (millis() > MYESP_SYSTEM_CHECK_TIME)) { _setSystemCheck(true); // Flag system as stable checked = true; } @@ -1103,10 +1209,7 @@ void MyESP::showSystemStats() { 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); - } + myDebug_P(PSTR(" [APP] Build timestamp: %s"), _buildTime); // uptime uint32_t t = _getUptime(); // seconds @@ -1135,11 +1238,15 @@ void MyESP::showSystemStats() { 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"); + 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}; @@ -1154,6 +1261,8 @@ void MyESP::showSystemStats() { 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 @@ -1217,10 +1326,10 @@ void MyESP::showSystemStats() { void MyESP::_heartbeatCheck(bool force = false) { static uint32_t last_heartbeat = 0; - if ((millis() - last_heartbeat > HEARTBEAT_INTERVAL) || force) { + if ((millis() - last_heartbeat > MYESP_HEARTBEAT_INTERVAL) || force) { last_heartbeat = millis(); - if (!isMQTTConnected() || !(_heartbeat)) { + if (!isMQTTConnected() || !(_mqtt_heartbeat)) { return; } @@ -1244,8 +1353,7 @@ void MyESP::_heartbeatCheck(bool force = false) { 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); + mqttPublish(MQTT_TOPIC_HEARTBEAT, payload); // send to MQTT } } @@ -1271,13 +1379,12 @@ void MyESP::_telnetHandle() { if (charsRead > 0) { charsRead = 0; // is static, so have to reset _suspendOutput = false; - if (_serial) { + if (_general_serial) { SerialAndTelnet.serialPrint('\n'); // force newline if in Serial } _telnetCommand(_command); } break; - case '\b': // (^H) case 0x7F: // (^?) if (charsRead > 0) { @@ -1286,9 +1393,7 @@ void MyESP::_telnetHandle() { SerialAndTelnet.write(' '); SerialAndTelnet.write('\b'); } - break; - case '?': if (!_suspendOutput) { _consoleShowHelp(); @@ -1311,10 +1416,11 @@ void MyESP::_telnetHandle() { } } -// make sure we have a connection to MQTT broker +// make sure we have a connection to MQTT broker and the MQTT IP is set void MyESP::_mqttConnect() { - if (!_mqtt_host) + 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)) { @@ -1334,21 +1440,21 @@ void MyESP::_mqttConnect() { _mqtt_reconnect_delay = MQTT_RECONNECT_DELAY_MAX; } - mqttClient.setServer(_mqtt_host, MQTT_PORT); - mqttClient.setClientId(_app_hostname); + mqttClient.setServer(_mqtt_ip, _mqtt_port); + mqttClient.setClientId(_general_hostname); mqttClient.setKeepAlive(_mqtt_keepalive); mqttClient.setCleanSession(false); // last will - if (_mqtt_will_topic) { + 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 (_mqtt_username && _mqtt_password) { - myDebug_P(PSTR("[MQTT] Connecting to MQTT using user %s..."), _mqtt_username); - mqttClient.setCredentials(_mqtt_username, _mqtt_password); + 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...")); } @@ -1358,292 +1464,366 @@ void MyESP::_mqttConnect() { } // 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); - } - +void MyESP::setWIFI(wifi_callback_f callback) { // callback - _wifi_callback = callback; + _wifi_callback_f = 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); - } +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) { - char buffer[MQTT_MAX_TOPIC_SIZE] = {0}; + 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)); + } - 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; + return buffer; } // 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"); +void MyESP::_fs_printFile(const char * file) { + File configFile = SPIFFS.open(file, "r"); if (!configFile) { myDebug_P(PSTR("[FS] Failed to read file for printing")); return; } + myDebug_P(PSTR("[FS] File: %s, Size: %d"), file, configFile.size()); + while (configFile.available()) { SerialAndTelnet.print((char)configFile.read()); } - myDebug_P(PSTR("")); // newline + myDebug_P(PSTR("[FS] end")); // 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 - } + 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 = callback_web; + _web_callback_f = 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; +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 from spiffs +// load system config from SPIFFS +// returns false on error or the file needs to be recreated bool MyESP::_fs_loadConfig() { - File configFile = SPIFFS.open(MYEMS_CONFIG_FILE, "r"); + // see if old file exists and delete it + if (SPIFFS.exists("/config.json")) { + SPIFFS.remove("/config.json"); + myDebug_P(PSTR("[FS] Removed obsolete version")); + } + File configFile = SPIFFS.open(MYESP_CONFIG_FILE, "r"); + if (!configFile) { + configFile.close(); + myDebug_P(PSTR("[FS] No system config file")); + return false; + } + + // check size size_t size = configFile.size(); - if (size > 1024) { - myDebug_P(PSTR("[FS] Config file size is too large")); + if (size > MYESP_SPIFFS_MAXSIZE) { + configFile.close(); + myDebug_P(PSTR("[FS] System config file size is too large")); return false; } else if (size == 0) { + configFile.close(); + myDebug_P(PSTR("[FS] Corrupted system config file")); return false; } - StaticJsonDocument doc; - JsonObject json = doc.to(); + // 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] File sizes don't match with system config file")); + return false; + } + configFile.close(); - // Deserialize the JSON document - DeserializationError error = deserializeJson(doc, configFile); + StaticJsonDocument doc; + + DeserializationError error = deserializeJson(doc, json); // Deserialize the JSON document if (error) { - myDebug_P(PSTR("[FS] Failed to read config file. Error %s"), error.c_str()); + myDebug_P(PSTR("[FS] Failed to deserialize json, error %s"), error.c_str()); + configFile.close(); return false; } - const char * value; + JsonObject network = doc["network"]; + _network_ssid = strdup(network["ssid"] | ""); + _network_password = strdup(network["password"] | ""); + _network_wmode = network["wmode"]; // 0 is client, 1 is AP - // fetch the standard system parameters - value = json["wifi_ssid"]; - _wifi_ssid = (value) ? strdup(value) : NULL; + JsonObject general = doc["general"]; - value = json["wifi_password"]; - _wifi_password = (value) ? strdup(value) : NULL; + _general_password = strdup(general["password"] | MYESP_HTTP_PASSWORD); + _ws->setAuthentication("admin", _general_password); + _general_hostname = strdup(general["hostname"]); - 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 + // serial is only on when booting #ifdef FORCE_SERIAL - _serial = true; + _general_serial = true; #else - _serial = json["serial"]; + _general_serial = general["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); + 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"] | ""); + JsonObject ntp = doc["ntp"]; + _ntp_server = strdup(ntp["server"] | ""); + _ntp_interval = ntp["interval"] | 60; + if (_ntp_interval == 0) + _ntp_interval = 60; + _ntp_timezone = ntp["timezone"] | 0; + _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 file")); + return false; + } + + // check size + size_t size = configFile.size(); + if (size > MYESP_SPIFFS_MAXSIZE) { + configFile.close(); + myDebug_P(PSTR("[FS] Custom config file size is too large")); + return false; + } else if (size == 0) { + configFile.close(); + myDebug_P(PSTR("[FS] Corrupted custom config file")); + 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 file")); + 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 file 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", "sys", "Custom config stored in the SPIFFS", ""); + myDebug_P(PSTR("[FS] custom config file saved")); + ok = true; + } + + if (_ota_pre_callback_f) { + (_ota_pre_callback_f)(); + } + 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); - } +// save system config to spiffs +bool MyESP::fs_saveConfig(JsonObject root) { + bool ok = false; // open for writing - File configFile = SPIFFS.open(MYEMS_CONFIG_FILE, "w"); + File configFile = SPIFFS.open(MYESP_CONFIG_FILE, "w"); if (!configFile) { - myDebug_P(PSTR("[FS] Failed to open config file for writing")); + myDebug_P(PSTR("[FS] Failed to open system 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; + // 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(); - // call any custom functions before handling SPIFFS - if (_ota_post_callback) { - (_ota_post_callback)(); + if (n) { + _writeEvent("INFO", "sys", "System config stored in the SPIFFS", ""); + myDebug_P(PSTR("[FS] system config file saved")); + ok = true; } - return ok; // it worked + // 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["timezone"] = _ntp_timezone; + 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 file")); + } + + 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] Failed to mount the file system. Erasing...")); - _fs_eraseConfig(); // fix for ESP32 - return; + Serial.print(F("[WARN] Formatting filesystem...")); + if (SPIFFS.format()) { + _writeEvent("WARN", "sys", "File system formatted", ""); + } else { + myDebug_P(PSTR("[FS] Failed to format file system")); + } } - // 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 + // 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] Re-creating config file")); - fs_saveConfig(); - _firstInstall = true; // flag as a first install + myDebug_P(PSTR("[FS] Creating a new system config file")); + _fs_writeConfig(); // create the initial config file } - // assume if the wifi ssid is empty, its a fresh install too - if ((_wifi_ssid == NULL)) { - _firstInstall = true; // flag as a first install + // load system and custom config + if (!_fs_loadCustomConfig()) { + _fs_createCustomConfig(); // create the initial config file } - - myDebug_P(PSTR("[FS] Settings loaded from SPIFFS")); - - // _fs_printConfig(); // enable for debugging } uint32_t MyESP::getSystemLoadAverage() { @@ -1656,7 +1836,7 @@ void MyESP::_calculateLoad() { static uint32_t load_counter_temp = 0; load_counter_temp++; - if (millis() - last_loadcheck > LOADAVG_INTERVAL) { + if (millis() - last_loadcheck > MYESP_LOADAVG_INTERVAL) { static uint32_t load_counter = 0; static uint32_t load_counter_max = 1; @@ -1853,7 +2033,7 @@ void MyESP::crashDump() { } /* - * Force some crashes to test if stack collection and debugging works + * Force some crashes to test if stack collection works */ void MyESP::crashTest(uint8_t t) { if (t == 1) { @@ -1867,7 +2047,7 @@ void MyESP::crashTest(uint8_t t) { if (t == 2) { myDebug_P(PSTR("[CRASH] Attempting to read through a pointer to no object ...")); int * nullPointer; - nullPointer = NULL; + nullPointer = nullptr; // null pointer dereference - read // attempt to read a value through a null pointer Serial.println(*nullPointer); @@ -1906,190 +2086,459 @@ void MyESP::crashInfo() { } #endif -// default home web page -void MyESP::_webRootPage() { - char s[1000] = {0}; - - strlcpy(s, webCommonPage_start, sizeof(s)); - // strlcat(s, webCommonPage_start_refresh, 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)); - - 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)); +// write a log entry to SPIFFS +void MyESP::_writeEvent(const char * type, const char * src, const char * desc, const char * data) { + File eventlog = SPIFFS.open(MYESP_EVENTLOG_FILE, "a"); + if (!eventlog) { + return; } - strlcat(s, isMQTTConnected() ? "
MQTT is connected\n" : " MQTT is disconnected\n", sizeof(s)); - strlcat(s, "
", sizeof(s)); + DynamicJsonDocument doc(300); + JsonObject root = doc.to(); // TODO do I need to convert to root? read docs - // uptime - char buffer[200]; + 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); + + if (n) { + eventlog.print("\n"); // this indicates end of the entry + } + + 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(); + return; // file can't be opened + } + + // 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; + uint8_t last = page * 10; + uint8_t i = 0; + + while (eventlog.available()) { + String item = String(); // TODO replace String with char* + item = eventlog.readStringUntil('\n'); + if (i >= first && i < last) { + list.add(item); + } + i++; + } + eventlog.close(); + + float pages = i / 10.0; + root["haspages"] = ceil(pages); + + char buffer[MYESP_JSON_MAXSIZE]; + size_t len = serializeJson(root, buffer); + + _ws->textAll(buffer, len); + _ws->textAll("{\"command\":\"result\",\"resultof\":\"eventlist\",\"result\": true}"); +} + +// 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) { + fs_saveConfig(root); + _shouldReboot = true; + } else if (strcmp(command, "custom_configfile") == 0) { + fs_saveCustomConfig(root); + } else if (strcmp(command, "status") == 0) { + _sendStatus(); + } else if (strcmp(command, "custom_status") == 0) { + _sendCustomStatus(); + } else if (strcmp(command, "restart") == 0) { + _shouldReboot = true; + } else if (strcmp(command, "destroy") == 0) { + _formatreq = true; + } else if (strcmp(command, "geteventlog") == 0) { + uint8_t page = doc["page"]; + _sendEventLog(page); + } else if (strcmp(command, "clearevent") == 0) { + SPIFFS.remove(MYESP_EVENTLOG_FILE); + _writeEvent("WARN", "sys", "Event log cleared", ""); + } 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 file 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 file 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["helpurl"] = _app_helpurl; + 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() { + StaticJsonDocument<400> doc; + JsonObject root = doc.to(); + + FSInfo fsinfo; + if (!SPIFFS.info(fsinfo)) { + myDebug("[SYSTEM] Error getting info on SPIFFS"); + } + + root["command"] = "status"; + + // all sizes in bytes converted to KB + root["heap"] = ESP.getFreeHeap() / 1000; + root["sketchsize"] = ESP.getSketchSize() / 1000; + root["availsize"] = ESP.getFreeSketchSpace() / 1000; + root["availspiffs"] = (fsinfo.totalBytes - fsinfo.usedBytes) / 1000; + root["spiffssize"] = (fsinfo.totalBytes / 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 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)); + sprintf(uptime, "%d days %d hours %d minutes %d seconds", d, h, m, sec); + root["uptime"] = uptime; - // 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); + char buffer[400]; + size_t len = serializeJson(root, buffer); + _ws->textAll(buffer, len); } -// Creates a webpage that allows the user to change the SSID and Password from the browser -void MyESP::_webResetPage() { - char s[1000] = {0}; +// print top5 wifi +void MyESP::_printScanResult(int networksFound) { + int n = networksFound; + int indices[n]; + for (int i = 0; i < networksFound; i++) { + indices[i] = i; + } - 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()); + // 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]); } } - - 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}; + StaticJsonDocument<400> doc; + JsonObject root = doc.to(); + root["command"] = "ssidlist"; - 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(); - } + 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() { - webServer.on("/", [this]() { _webRootPage(); }); - webServer.on("/reset", [this]() { _webResetPage(); }); - webServer.on("/resetall", [this]() { _webResetAllPage(); }); + _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.begin(); + _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", _shouldReboot ? "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", "updt", "Firmware update started", ""); + //Serial.printf("[ UPDT ] Firmware update started: %s\n", filename.c_str()); // enable for debugging + Update.runAsync(true); + if (!Update.begin((ESP.getFreeSketchSpace() - 0x1000) & 0xFFFFF000)) { + _writeEvent("ERRO", "updt", "Not enough space to update", ""); + //Update.printError(Serial); // enable for debugging + } + } + if (!Update.hasError()) { + if (Update.write(data, len) != len) { + _writeEvent("ERRO", "updt", "Writing to flash failed", ""); + //Update.printError(Serial); // enable for debugging + } + } + if (final) { + if (Update.end(true)) { + _writeEvent("INFO", "updt", "Firmware update finished", ""); + //Serial.printf("[SYSTEM] Firmware update finished: %uB\n", index + len); // enable for debugging + _shouldReboot = !Update.hasError(); + } else { + _writeEvent("ERRO", "updt", "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", "websrv", "New login attempt", remoteIP); + return request->requestAuthentication(); + } + request->send(200, "text/plain", "Success"); + // _writeEvent("INFO", "websrv", "Login successful", remoteIP); + }); + + _webServer->rewrite("/", "/index.html"); + _webServer->begin(); myDebug_P(PSTR("[WEB] Web server started")); } + +// send UTC time via ws +void MyESP::_sendTime() { + StaticJsonDocument<100> doc; + JsonObject root = doc.to(); + root["command"] = "gettime"; + root["epoch"] = now(); + root["timezone"] = _ntp_timezone; + + 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(); - if ((boot_status == MYESP_BOOTSTATUS_BOOTED) || (millis() <= MYESP_BOOTUP_DELAY)) { + // check if its booted + if (boot_status == MYESP_BOOTSTATUS_BOOTED) { + if (!_ntp_enabled) { + _writeEvent("INFO", "sys", "System booted", "elapsed time"); + } else { + // wait until we have a real time, after 10 seconds + if ((!_have_ntp_time) && (now() > 10000)) { + _have_ntp_time = true; + _writeEvent("INFO", "sys", "System booted", ""); + } + return; + } + } + + if (millis() <= MYESP_BOOTUP_DELAY) { return; // already booted, or still starting up } @@ -2114,10 +2563,12 @@ void MyESP::_bootupSequence() { } // 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); +void MyESP::begin(const char * app_hostname, const char * app_name, const char * app_version, const char * app_helpurl, const char * app_updateurl) { + _general_hostname = strdup(app_hostname); + _app_name = strdup(app_name); + _app_version = strdup(app_version); + _app_helpurl = strdup(app_helpurl); + _app_updateurl = strdup(app_updateurl); _telnet_setup(); // Telnet setup, called first to set Serial @@ -2130,44 +2581,60 @@ void MyESP::begin(const char * app_hostname, const char * app_name, const char * _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 + _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) + _heartbeatCheck(true); // force heartbeat SerialAndTelnet.flush(); + + //_fs_printFile(MYESP_EVENTLOG_FILE); // for debugging + //_fs_printFile(MYESP_EVENTLOG_FILE); // for debugging + //_fs_printFile(MYESP_CUSTOMCONFIG_FILE); // for debugging } /* * 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(); + _bootupSequence(); // see if a reset was pressed during bootup + _telnetHandle(); // telnet - yield(); // ...and breath + ESP.wdtFeed(); // TODO see if this helps with WDT resets + + jw.loop(); // WiFi + ArduinoOTA.handle(); // OTA + _mqttConnect(); // MQTT + + if (_timerequest) { + _timerequest = false; + _sendTime(); + } + + if (_formatreq) { + myDebug("[SYSTEM] Factory reset initiated"); + SPIFFS.end(); + _ws->enable(false); + SPIFFS.format(); + _deferredReset(500, CUSTOM_RESET_FACTORY); + ESP.restart(); + } + + if (_shouldReboot) { + _writeEvent("INFO", "sys", "System is rebooting", ""); + myDebug("[SYSTEM] Rebooting..."); + _deferredReset(500, CUSTOM_RESET_TERMINAL); + ESP.restart(); + } + + yield(); // ... and breath. } -MyESP myESP; // create instance +MyESP myESP; diff --git a/lib/MyESP/MyESP.h b/src/MyESP.h similarity index 65% rename from lib/MyESP/MyESP.h rename to src/MyESP.h index 74dcc2a4a..b189f7bfa 100644 --- a/lib/MyESP/MyESP.h +++ b/src/MyESP.h @@ -9,15 +9,18 @@ #ifndef MyESP_h #define MyESP_h -#define MYESP_VERSION "1.1.24" +#define MYESP_VERSION "1.2" #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,13 +41,31 @@ 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 @@ -57,6 +77,12 @@ extern struct rst_info resetInfo; #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_RETAIN false +#define MQTT_KEEPALIVE 60 // 1 minute +#define MQTT_QOS 1 +#define MQTT_WILL_TOPIC "status" // for last will & testament topic name // Internal MQTT events #define MQTT_CONNECT_EVENT 0 @@ -94,18 +120,21 @@ 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/ +#define MYESP_JSON_MAXSIZE 2000 // for large Dynamic json files // CRASH /** @@ -157,9 +186,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 @@ -181,9 +210,9 @@ typedef std::function 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 +220,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 +256,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 +269,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 +285,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_helpurl, const char * app_updateurl); void resetESP(); int getWifiQuality(); void showSystemStats(); @@ -298,6 +293,7 @@ class MyESP { uint32_t getSystemLoadAverage(); uint32_t getSystemResetReason(); uint8_t getSystemBootStatus(); + bool _have_ntp_time; private: // mqtt @@ -306,40 +302,41 @@ class MyESP { void _mqttOnMessage(char * topic, char * payload, size_t len); void _mqttConnect(); void _mqtt_setup(); - mqtt_callback_f _mqtt_callback; + mqtt_callback_f _mqtt_callback_f; void _mqttOnConnect(); void _sendStart(); char * _mqttTopic(const char * topic); - char * _mqtt_host; - char * _mqtt_username; + char * _mqtt_ip; + char * _mqtt_user; char * _mqtt_password; + int _mqtt_port; char * _mqtt_base; + bool _mqtt_enabled; unsigned long _mqtt_keepalive; unsigned char _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; 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 +351,41 @@ 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_helpurl; + 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); // reset reason and rtcmem + bool _rtcmem_status; bool _rtcmemStatus(); bool _getRtcmemStatus(); @@ -418,11 +419,30 @@ 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 + uint8_t _ntp_timezone; + 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..e44563b61 --- /dev/null +++ b/src/Ntp.cpp @@ -0,0 +1,49 @@ +/* + * Ntp.cpp + */ + +#include "Ntp.h" + +char * NtpClient::TimeServerName; +int8_t NtpClient::timezone; +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, int8_t tz, time_t syncSecs) { + TimeServerName = strdup(server); + timezone = tz; + syncInterval = syncSecs; + 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..a13fd9da6 --- /dev/null +++ b/src/Ntp.h @@ -0,0 +1,35 @@ +/* + * 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, int8_t tz, time_t syncSecs); + ICACHE_FLASH_ATTR virtual ~NtpClient(); + + static char * TimeServerName; + static IPAddress timeServer; + static int8_t timezone; + static time_t syncInterval; + + static AsyncUDP udpListener; + + static byte NTPpacket[NTP_PACKET_SIZE]; + + private: + 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..d2335c7e0 --- /dev/null +++ b/src/TimeLib.cpp @@ -0,0 +1,185 @@ +#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; +} + +// indicates if time has been set and recently synchronized +timeStatus_t timeStatus() { + now(); // required to actually update the status + return Status; +} + +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 +} + +time_t makeTime(const tmElements_t & tm) { + // assemble time elements into time_t + // note year argument is offset from 1970 (see macros in time.h to convert to other formats) + // previous version used full four digit year (or digits since 2000),i.e. 2009 was 2009 or 9 + + int i; + uint32_t seconds; + + // seconds from 1970 till 1 jan 00:00:00 of the given year + seconds = tm.Year * (SECS_PER_DAY * 365); + for (i = 0; i < tm.Year; i++) { + if (LEAP_YEAR(i)) { + seconds += SECS_PER_DAY; // add extra days for leap years + } + } + + // add days for this year, months start from 1 + for (i = 1; i < tm.Month; i++) { + if ((i == 2) && LEAP_YEAR(tm.Year)) { + seconds += SECS_PER_DAY * 29; + } else { + seconds += SECS_PER_DAY * monthDays[i - 1]; //monthDay array starts from 0 + } + } + seconds += (tm.Day - 1) * SECS_PER_DAY; + seconds += tm.Hour * SECS_PER_HOUR; + seconds += tm.Minute * SECS_PER_MIN; + seconds += tm.Second; + return (time_t)seconds; +} + +void refreshCache(time_t t) { + if (t != cacheTime) { + breakTime(t, tm); + 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 weekday(time_t t) { + refreshCache(t); + return tm.Wday; +} + +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..4ad1215c0 --- /dev/null +++ b/src/custom.htm @@ -0,0 +1,239 @@ +
+
+
+ Custom Settings +
Please refer to the Help for configuration options.
+
+ +
+ +
+
+ + +
+
+
+ +
+ + + + +
+ +
+ +
+
+ + +
+
+
+ +
+ + + + +
+ +
+ +
+
+ + +
+
+
+ +
+ +
+
+ + +
+
+
+ +
+ +
+
+ + +
+
+
+ +
+ + + + +
+
+ +
+ + + + +
+ +
+ + + + +
+ +
+
+
+ +
+
+ +
Note: any setting marked with a requires a restart after saving.
+
+ +
+
+
+
+

Dashboard

+

Real-time values from the EMS-ESP device are shown here.

+
+
+
+ + + + + + + + + +
EMS Bus stats
+

+
Detected Devices: +
    +
    +
+
+
+
+
Boiler
+ + + + + + + + + + + + + + + + +
Model:
Hot Tap Water:Central Heating:
Selected Flow Temperature:Boiler Temperature:
+
+ +
+
Thermostat
+ + + + + + + + + + + + + + + +
Model:
Setpoint Temperature:Current Temperature:
Mode:
+
+ +
+
+
+
+
+ +
+
+
+
\ No newline at end of file diff --git a/src/custom.js b/src/custom.js new file mode 100644 index 000000000..0d76f10e6 --- /dev/null +++ b/src/custom.js @@ -0,0 +1,141 @@ +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, + "tx_mode": 0 + } +} + +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 = "label label-success"; + } else { + document.getElementById("msg").className = "label label-danger"; + 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; + for (var i = 0; i < obj.length; i++) { + var l = document.createElement("li"); + var type = obj[i].type; + if (type == 1) { + var color = "info"; + } else if (type == 2) { + var color = "success"; + } else if (type == 3) { + var color = "warning"; + } else if (type == 4) { + var color = "danger"; + } else { + var color = ""; + } + l.innerHTML = "Model:" + obj[i].model + ", Version:" + obj[i].version + ", ProductID:" + obj[i].productid + ", DeviceID:" + obj[i].deviceid; + l.className = "list-group-item 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 + " ℃"; + } 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"; + + } + + + +} + diff --git a/src/ems-esp.cpp b/src/ems-esp.cpp index 5733a8367..08477068d 100644 --- a/src/ems-esp.cpp +++ b/src/ems-esp.cpp @@ -8,19 +8,17 @@ */ // local libraries -#include "ds18.h" #include "ems.h" #include "ems_devices.h" #include "emsuart.h" #include "my_config.h" #include "version.h" +#include // 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 @@ -32,7 +30,7 @@ DS18 ds18; #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 1 // initially set to 0 for no delay // TODO change delay to 0? #define DEFAULT_HEATINGCIRCUIT 1 // default to HC1 for thermostats that support multiple heating circuits like the RC35 @@ -69,6 +67,8 @@ 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 +#define MQTT_MAX_SIZE 700 // max size of a JSON object. See https://arduinojson.org/v6/assistant/ + typedef struct { uint32_t timestamp; // for internal timings, via millis() uint8_t dallas_sensors; // count of dallas sensors @@ -98,9 +98,7 @@ 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"}, @@ -550,7 +548,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 +559,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 +577,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) { @@ -683,8 +681,6 @@ 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 @@ -1131,44 +1127,42 @@ 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(settings["tx_mode"]); - 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 + 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"] = ems_getTxMode(); return true; } @@ -1179,7 +1173,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) { @@ -1243,18 +1237,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) { @@ -1311,21 +1293,7 @@ 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(" 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"); @@ -1697,58 +1665,110 @@ 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 + // system_uart_swap(); // TODO see if this still blocks the EMS lines +} + +// 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"; } 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_getTxCapable()) { + emsbus["ok"] = true; + emsbus["msg"] = "EMS Bus Connected, Rx and Tx active"; + } else { + emsbus["ok"] = false; + emsbus["msg"] = "EMS Bus Connected, Tx is failing"; + } + } 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["deviceid"] = (it)->device_id; + item["version"] = (it)->version; + item["productid"] = (it)->product_id; + } + + 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)) { + 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.boilTemp != EMS_VALUE_USHORT_NOTSET) + boiler["b4"] = (double)EMS_Boiler.boilTemp / 10; + + } else { + boiler["ok"] = false; + } + + // serializeJsonPretty(root, Serial); // turn on for debugging } // Initialize the boiler settings and shower settings @@ -1862,25 +1882,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, true); // 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_HELPURL, APP_UPDATEURL); // at this point we have all the settings from our internall SPIFFS config file // fire up the UART now @@ -1889,7 +1898,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 6c0318841..2ce9cc647 100644 --- a/src/ems.cpp +++ b/src/ems.cpp @@ -11,7 +11,6 @@ #include "emsuart.h" #include // https://github.com/rlogiacco/CircularBuffer #include -#include // std::list #ifdef TESTS #include "test_data.h" @@ -739,7 +738,7 @@ void ems_parseTelegram(uint8_t * telegram, uint8_t length) { */ 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 + myDebug_P(PSTR("** [DEBUG MODE] Warning, we missed the bus - Rx non-idle!")); // TODO tidy up error logging } return; } @@ -1609,8 +1608,9 @@ void ems_clearDeviceList() { /* * add an EMS device to our list of detected devices + * model_type = 1=info=boiler, 2=success=thermostat, 3=warning=sm, 4=danger=other, 5=none=unknown */ -void _addDevice(uint8_t product_id, uint8_t device_id, char * version, const char * model_string) { +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; @@ -1620,6 +1620,7 @@ void _addDevice(uint8_t product_id, uint8_t device_id, char * version, const cha } } if (!found) { + device.model_type = model_type; device.product_id = product_id; device.device_id = device_id; strlcpy(device.version, version, sizeof(device.version)); @@ -1659,7 +1660,7 @@ void _process_Version(_EMS_RxTelegram * EMS_RxTelegram) { 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_Devices[i].model_string); + _addDevice(1, 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 @@ -1679,8 +1680,6 @@ void _process_Version(_EMS_RxTelegram * EMS_RxTelegram) { EMS_Sys_Status.emsReverse = true; } - myESP.fs_saveConfig(); // save config to SPIFFS - ems_getBoilerValues(); // get Boiler values that we would usually have to wait for } return; @@ -1707,7 +1706,7 @@ void _process_Version(_EMS_RxTelegram * EMS_RxTelegram) { } // add to list - _addDevice(product_id, Thermostat_Devices[i].device_id, version, Thermostat_Devices[i].model_string); + _addDevice(2, 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) @@ -1725,8 +1724,6 @@ void _process_Version(_EMS_RxTelegram * EMS_RxTelegram) { 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(); } @@ -1752,7 +1749,7 @@ void _process_Version(_EMS_RxTelegram * EMS_RxTelegram) { version); // add to list - _addDevice(product_id, SolarModule_Devices[i].device_id, version, SolarModule_Devices[i].model_string); + _addDevice(3, 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_Devices[i].device_id; @@ -1782,7 +1779,7 @@ void _process_Version(_EMS_RxTelegram * EMS_RxTelegram) { version); // add to list - _addDevice(product_id, HeatPump_Devices[i].device_id, version, HeatPump_Devices[i].model_string); + _addDevice(3, 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_Devices[i].device_id; @@ -1805,12 +1802,12 @@ void _process_Version(_EMS_RxTelegram * EMS_RxTelegram) { 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_Devices[i].device_id, version, Other_Devices[i].model_string); + _addDevice(4, 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(5, product_id, EMS_RxTelegram->src, version, "unknown?"); // type 4 = unknown } } @@ -1978,7 +1975,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); @@ -1998,6 +1995,9 @@ char * ems_getThermostatDescription(char * buffer) { if (found) { strlcpy(buffer, Thermostat_Devices[i].model_string, size); + if (name_only) { + return buffer; // only interested in the model name + } } else { strlcpy(buffer, "DeviceID: 0x", size); strlcat(buffer, _hextoa(EMS_Thermostat.device_id, tmp), size); @@ -2020,7 +2020,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); @@ -2039,6 +2039,9 @@ char * ems_getBoilerDescription(char * buffer) { } if (found) { strlcpy(buffer, Boiler_Devices[i].model_string, size); + if (name_only) { + return buffer; // only interested in the model name + } } else { strlcpy(buffer, "DeviceID: 0x", size); strlcat(buffer, _hextoa(EMS_Boiler.device_id, tmp), size); @@ -2261,23 +2264,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_Device>::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) @@ -2695,7 +2681,7 @@ 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)) { diff --git a/src/ems.h b/src/ems.h index fa21ba201..6c84cc4dd 100644 --- a/src/ems.h +++ b/src/ems.h @@ -11,6 +11,7 @@ #pragma once #include +#include // std::list /* debug helper for logic analyzer * create marker puls on GPIOx @@ -250,6 +251,7 @@ typedef struct { // for consolidating all types typedef struct { + uint8_t model_type; // 1=info=boiler, 2=success=thermostat, 3=warning=sm, 4=danger=other, 5=none=unknown uint8_t product_id; uint8_t device_id; char version[10]; @@ -421,8 +423,8 @@ void ems_setTxDisabled(bool b); bool ems_getTxDisabled(); uint8_t ems_getTxMode(); -char * ems_getThermostatDescription(char * buffer); -char * ems_getBoilerDescription(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); char * ems_getHeatPumpDescription(char * buffer); void ems_getThermostatValues(); @@ -457,3 +459,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/emsuart.cpp b/src/emsuart.cpp index 0da551c42..3e72bbc32 100644 --- a/src/emsuart.cpp +++ b/src/emsuart.cpp @@ -121,7 +121,7 @@ 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 @@ -141,7 +141,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(); diff --git a/src/my_config.h b/src/my_config.h index 1dc9c16b4..dc5c1c877 100644 --- a/src/my_config.h +++ b/src/my_config.h @@ -10,18 +10,6 @@ #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 diff --git a/src/version.h b/src/version.h index 7e23fdfc1..2d63c81c4 100644 --- a/src/version.h +++ b/src/version.h @@ -1,10 +1,6 @@ -/** - * - * Paul Derbyshire - https://github.com/proddy/EMS-ESP - */ - -#pragma once #define APP_NAME "EMS-ESP" -#define APP_VERSION "1.9.0" +#define APP_VERSION "1.9.0b_web" #define APP_HOSTNAME "ems-esp" +#define APP_HELPURL "https://github.com/proddy/EMS-ESP/wiki" +#define APP_UPDATEURL "https://api.github.com/repos/proddy/EMS-ESP/releases/latest" 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 0000000000000000000000000000000000000000..9e612858f802245ddcbf59788a0db942224bab35 GIT binary patch literal 23424 zcmY&eV{m0%u#Iioo_J#0nb?@vwry)-+qNe*Z>))v8{5gt_uj9!t5)^yb-JtjRGrhi zYInOUNJxNyf_yKX01)K=WP|Si>HqEj|B{eUl?MR<)%<1&{(~)D+NPwKxWqT-@~snp zg9KCz1VTZDiS?UH`PRk1VPM{29cgT9=D?!Wc_@}qzggFv;gb@2cJQAYWWtpEZ7?y@jSVqjx${B5UV@SO|wH<<0; z{><1KdVI%Ki}>~<`46C0AggwUwx-|QcU;iiZ{NZu`ur>hd*|Hb(|6veERqxu=b@5Bab=rqptGxd{QJg!4*-i_$sES~)AB46}Fjg|ea#e@?J}z%CUJ zOsLWRQR1#ng^sD)A4FDuY!iUhzlgfJh(J@BRqd&P#v2B`+saBx>m+M&q7vk-75$NH%T5pi%m z5FX?`2-5l53=a&GkC9^NZCLpN5(DMKMwwab$FDIs?q>4!!xBS}75gX_5;(luk;3Vl zLCLd5a_8`Iyz}K}+#RMwu6DVk3O_-}n>aE!4NaD*sQn`GxY?cHe!Bl9n?u&g6?aKm z-P8z&;Q3gr;h`YIxX%z^o&GZZg1=>_+hP2$$-DnL_?7?3^!WAsY4I7|@K;aL<>OTK zByfjl2PA$T83*LM9(;espx-qB%wv7H2i6CFsfAg<9V>Pj*OpwX)l?^mQfr$*OPPS$ z=`mzTYs{*(UW^ij1U8UfXjNoY7GK*+YHht(2oKE&tfZuvAyoN(;_OF>-J6AMmS5fB z^sY6wea&&${+!}@R1f$5oC-2J>J-A${@r(dRzc`wnK>a7~8{Y-scc|ETOI8 zjtNY%Y2!PI;8-@a=O}+{ap1Ewk0@T`C`q!|=KceX9gK8wtOtIC96}-^7)v23Mu;MH zhKyLGOQMujfRG$p(s`(2*nP4EH7*J57^=|%t(#PwCcW7U%e=8Jb>p6~>RAlY4a*ts=pl}_J{->@kKzxH|8XQ5{t=E zV&o`$D#ZHdv&iZWFa)(~oBh-Osl{~CS0hfM7?PyWUWsr5oYlsyC1cwULoQ4|Y5RHA2*rN+EnFPnu z`Y_&Yz*#550YJwDy@brZU>0pWV^RxRjL221@2ABq)AtA%Cz?+FG(}Yh?^v)1Lnh%D zeM{{3&-4#F9rZhS@DT0E(WRkrG!jC#5?OFjZv*xQjUP~XsaxL2rqRKvPW$zHqHr8Urp2Z)L z+)EvQeoeJ8c6A#Iy9>3lxiH3=@86uiTbnnJJJoypZ7gco_*HvKOH97B? zWiwp>+r}*Zf9b3ImxwvjL~h~j<<3shN8$k-$V1p|96I!=N6VBqmb==Bec|*;HUg?) z4!5#R*(#Fe)w%+RH#y{8&%%!|fQ5JcFzUE;-yVYR^&Ek55AXb{^w|@j|&G z|6C-+*On%j;W|f8mj?;679?!qY86c{(s1-PI2Wahoclf%1*8%JAvRh1(0)5Vu37Iz z`JY?RW@qKr+FMmBC{TC7k@}fv-k8t6iO}4K-i3WkF!Lc=D`nuD)v#Na zA|R*no51fkUN3^rmI;tty#IK284*2Zu!kG13!$OlxJAt@zLU`kvsazO25TpJLbK&;M8kw*0)*14kpf*)3;GiDh;C(F}$- z1;!=OBkW#ctacN=je*Pr)lnGzX=OwgNZjTpVbFxqb;8kTc@X&L2XR0A7oc!Mf2?u9 zcctQLCCr+tYipa_k=;1ETIpHt!Jeo;iy^xqBES^Ct6-+wHi%2g&)?7N^Yy zUrMIu){Jk)luDa@7We5U!$$3XFNbyRT!YPIbMKj5$IEpTX1IOtVP~(UPO2-+9ZFi6 z-$3<|{Xb#@tABt0M0s1TVCWKwveDy^S!!@4$s|DAqhsEv--Z}Dl)t%0G>U#ycJ7cy z^8%;|pg32=7~MJmqlC-x07Sd!2YX^|2D`?y;-$a!rZ3R5ia{v1QI_^>gi(HSS_e%2 zUbdg^zjMBBiLr8eSI^BqXM6HKKg#@-w`a**w(}RMe%XWl3MipvBODo*hi?+ykYq)z ziqy4goZw0@VIUY65+L7DaM5q=KWFd$;W3S!Zi>sOzpEF#(*3V-27N;^pDRoMh~(ZD zJLZXIam0lM7U#)119Hm947W)p3$%V`0Tv+*n=&ybF&}h~FA}7hEpA&1Y!BiYIb~~D z$TSo9#3ee02e^%*@4|*+=Nq6&JG5>zX4k5f?)z*#pI-G(+j|jye%13CUdcSP;rNlY z#Q!X%zHf|V)GWIcEz-=fW6AahfxI~y7w7i|PK6H@@twdgH>D_R@>&OtKl}%MuAQ7I zcpFmV^~w~8$4@zzh~P~+?B~%L@EM3x(^KXJSgc6I=;)B6 zpRco2LKIlURPE*XUmZ^|1vb?w*ZfF}EXvY13I4af+()bAI5V?BRbFp`Sb{8GRJHd* z4S2s%4A)6Uc=PK%4@PbJ<{1R6+2THMk0c+kif**#ZGE)w6WsqH z`r^DL&r8|OEAumm^qyrryd(HQ9olv$ltnVGB{aY?_76Uk%6p;e)2DTvF(;t=Q+|8b zqfT(u5@BP);6;jmRAEV057E*2d^wx@*aL1GqWU|$6h5%O@cQtVtC^isd%gD7PZ_Io z_BDP5w(2*)Mu&JxS@X%%ByH_@+l>y07jIc~!@;Raw)q_;9oy@*U#mCnc7%t85qa4? z%_Vr5tkN^}(^>`EFhag;!MpRh!&bKnveQZAJ4)gEJo1@wHtT$Gs6IpznN$Lk-$NcM z3ReVC&qcXvfGX$I0nfkS$a|Pm%x+lq{WweNc;K>a1M@EAVWs2IBcQPiEJNt}+Ea8~WiapASoMvo(&PdUO}AfC~>ZGzqWjd)4no( ziLi#e3lOU~sI*XPH&n&J0cWfoh*}eWEEZW%vX?YK!$?w}htY|GALx3;YZoo=JCF4@ zdiaA-uq!*L5;Yg)z-_`MciiIwDAAR3-snC4V+KA>&V%Ak;p{1u>{Lw$NFj)Yn0Ms2*kxUZ)OTddbiJM}PK!DM}Ot zczn?EZXhx3wyu6i{QMz_Ht%b?K&-@5r;8b076YDir`KXF0&2i9NQ~#JYaq*}Ylb}^ z<{{6xy&;dQ;|@k_(31PDr!}}W$zF7Jv@f%um0M$#=8ygpu%j(VU-d5JtQwT714#f0z+Cm$F9JjGr_G!~NS@L9P;C1? z;Ij2YVYuv}tzU+HugU=f9b1Wbx3418+xj$RKD;$gf$0j_A&c;-OhoF*z@DhEW@d9o zbQBjqEQnn2aG?N9{bmD^A#Um6SDKsm0g{g_<4^dJjg_l_HXdDMk!p`oFv8+@_v_9> zq;#WkQ!GNGfLT7f8m60H@$tu?p;o_It#TApmE`xnZr|_|cb3XXE)N^buLE`9R=Qbg zXJu}6r07me2HU<)S7m?@GzrQDTE3UH?FXM7V+-lT#l}P(U>Fvnyw8T7RTeP`R579m zj=Y>qDw1h-;|mX-)cSXCc$?hr;43LQt)7z$1QG^pyclQ1Bd!jbzsVEgIg~u9b38;> zfsRa%U`l%did6HzPRd;TK{_EW;n^Ivp-%pu0%9G-z@Au{Ry+EqEcqW=z-#6;-!{WA z;l+xC6Zke>dl+(R1q7B^Hu~HmrG~Kt575mzve>x*cL-shl+zqp6yuGX)DDGm`cid! znlnZY=+a5*xQ=$qM}5$N+o!^(TqTFHDdyCcL8NM4VY@2gnNXF|D?5a558Lb*Yfm4) z_;0%2EF7k{)i(tTvS`l5he^KvW%l&-suPwpIlWB_Za1Hfa$@J!emrcyPpTKKM@NqL z?X_SqHt#DucWm<3Lp}W|&YyQE27zbGP55=HtZmB(k*WZA79f##?TweCt{%5yuc+Kx zgfSrIZI*Y57FOD9l@H0nzqOu|Bhrm&^m_RK6^Z<^N($=DDxyyPLA z+J)E(gs9AfaO`5qk$IGGY+_*tEk0n_wrM}n4G#So>8Dw6#K7tx@g;U`8hN_R;^Uw9JLRUgOQ?PTMr4YD5H7=ryv)bPtl=<&4&% z*w6k|D-%Tg*F~sh0Ns(h&mOQ_Qf{`#_XU44(VDY8b})RFpLykg10uxUztD>gswTH} z&&xgt>zc(+=GdM2gIQ%3V4AGxPFW0*l0YsbA|nFZpN~ih4u-P!{39d@_MN)DC%d1w z7>SaUs-g@Hp7xqZ3Tn)e z7x^sC`xJ{V<3YrmbB{h9i5rdancCEyL=9ZOJXoVHo@$$-%ZaNm-75Z-Ry9Z%!^+STWyv~To>{^T&MW0-;$3yc9L2mhq z;ZbQ5LGNM+aN628)Cs16>p55^T^*8$Dw&ss_~4G5Go63gW^CY+0+Z07f2WB4Dh0^q z-|6QgV8__5>~&z1gq0FxDWr`OzmR}3aJmCA^d_eufde7;d|OCrKdnaM>4(M%4V`PxpCJc~UhEuddx9)@)9qe_|i z)0EA%&P@_&9&o#9eqZCUCbh?`j!zgih5sJ%c4(7_#|Xt#r7MVL&Q+^PQEg3MBW;4T zG^4-*8L%s|A}R%*eGdx&i}B1He(mLygTmIAc^G(9Si zK7e{Ngoq>r-r-zhyygK)*9cj8_%g z)`>ANlipCdzw(raeqP-+ldhyUv_VOht+!w*>Sh+Z7(7(l=9~_Vk ztsM|g1xW`?)?|@m2jyAgC_IB`Mtz(O`mwgP15`lPb2V+VihV#29>y=H6ujE#rdnK` zH`EaHzABs~teIrh`ScxMz}FC**_Ii?^EbL(n90b(F0r0PMQ70UkL}tv;*4~bKCiYm zqngRuGy`^c_*M6{*_~%7FmOMquOEZXAg1^kM`)0ZrFqgC>C%RJvQSo_OAA(WF3{euE}GaeA?tu5kF@#62mM$a051I zNhE>u>!gFE8g#Jj95BqHQS%|>DOj71MZ?EYfM+MiJcX?>*}vKfGaBfQFZ3f^Q-R1# znhyK1*RvO@nHb|^i4Ep_0s{lZwCNa;Ix<{E5cUReguJf+72QRZIc%`9-Vy)D zWKhb?FbluyDTgT^naN%l2|rm}oO6D0=3kfXO2L{tqj(kDqjbl(pYz9DykeZlk4iW5 zER`)vqJxx(NOa;so@buE!389-YLbEi@6rZG0#GBsC+Z0fzT6+d7deYVU;dy!rPXiE zmu73@Jr&~K{-9MVQD}&`)e>yLNWr>Yh8CXae9XqfvVQ&eC_;#zpoaMxZ0GpZz7xjx z`t_Q-F?u=vrRPaj3r<9&t6K=+egimiJ8D4gh-rUYvaVy zG($v+3zk5sMuOhjxkH7bQ}(5{PD3Mg?!@8PkK&w>n7tO8FmAmoF30_#^B~c(Q_`4L zYWOoDVSnK|1=p{+@`Fk^Qb81Xf89_S`RSTzv(a4ID%71nll%{Wad$!CKfeTKkyC?n zCkMKHU#*nz_(tO$M)UP&ZfJ#*q(0Gr!E(l5(ce<3xut+_i8XrK8?Xr7_oeHz(bZ?~8q5q~$Rah{5@@7SMN zx9PnJ-5?^xeW2m?yC_7A#WK*B@oIy*Y@iC1n7lYKj&m7vV;KP4TVll=II)$39dOJ^czLRU>L> z68P*PFMN+WXxdAu=Hyt3g$l(GTeTVOZYw3KY|W0Fk-$S_`@9`K=60)bEy?Z%tT+Iq z7f>%M9P)FGg3EY$ood+v$pdsXvG? zd2q3abeu-}LfAQWY@=*+#`CX8RChoA`=1!hS1x5dOF)rGjX4KFg!iPHZE2E=rv|A} zro(8h38LLFljl^>?nJkc+wdY&MOOlVa@6>vBki#gKhNVv+%Add{g6#-@Z$k*ps}0Y zQ=8$)+Nm||)mVz^aa4b-Vpg=1daRaOU)8@BY4jS>=5n#6abG@(F2`=k-eQ9@u# zxfNFHv=z2w@{p1dzSOgHokX1AUGT0DY4jQI@YMw)EWQ~q5wmR$KQ}Y;(HPMSQCwzu zdli|G?bj(>++CP)yQ4s6YfpDc3KqPmquQSxg%*EnTWumWugbDW5ef%8j-rT#3rJu? z)5n;4b2c*;2LIW%LmvUu6t1~di~}0&Svy}QX#ER|hDFZwl!~zUP&}B1oKAxIzt~so zb!GaJYOb#&qRUjEI1xe_`@7qv_-LggQ$JE8+{ryT4%ldwC5ete+{G3C#g@^oxfY3#F zcLlj(l2G8>tC<5XWV|6_DZQZ7ow?MD8EZ9mM2oV~WoV-uoExmbwpzc6eMV}%J_{3l zW(4t2a-o}XRlU|NSiYn!*nR(Sc>*@TuU*(S77gfCi7+WR%2b;4#RiyxWR3(u5BIdf zo@#g4wQjtG3T$PqdX$2z8Zi|QP~I^*9iC+(!;?qkyk&Q7v>DLJGjS44q|%yBz}}>i z&Ve%^6>xY<=Pi9WlwpWB%K10Iz`*#gS^YqMeV9$4qFchMFO}(%y}xs2Hn_E}s4=*3 z+lAeCKtS}9E{l(P=PBI;rsYVG-gw}-_x;KwUefIB@V%RLA&}WU2XCL_?hZHoR<7ED zY}4#P_MmX(_G_lqfp=+iX|!*)RdLCr-1w`4rB_@bI&Uz# z!>9C3&LdoB$r+O#n);WTPi;V52OhNeKfW6_NLnw zpFTuLC^@aPy~ZGUPZr;)=-p|b$-R8htO)JXy{ecE5a|b{{&0O%H2rN&9(VHxmvNly zbY?sVk}@^{aw)%#J}|UW=ucLWs%%j)^n7S%8D1Woi$UT}VuU6@Sd6zc2+t_2IMBxd zb4R#ykMr8s5gKy=v+opw6;4R&&46$V+OOpDZwp3iR0Osqpjx))joB*iX+diVl?E~Q zc|$qmb#T#7Kcal042LUNAoPTPUxF-iGFw>ZFnUqU@y$&s8%h-HGD`EoNBbe#S>Y-4 zlkeAP>62k~-N zHQqXXyN67hGD6CxQIq_zoepU&j0 zYO&}<4cS^2sp!;5))(aAD!KmUED#QGr48DVlwbyft31WlS2yU<1>#VMp?>D1BCFfB z_JJ-kxTB{OLI}5XcPHXUo}x~->VP%of!G_N-(3Snvq`*gX3u0GR&}*fFwHo3-vIw0 zeiWskq3ZT9hTg^je{sC^@+z3FAd}KNhbpE5RO+lsLgv$;1igG7pRwI|;BO7o($2>mS(E z$CO@qYf5i=Zh6-xB=U8@mR7Yjk%OUp;_MMBfe_v1A(Hqk6!D})x%JNl838^ZA13Xu zz}LyD@X2;5o1P61Rc$%jcUnJ>`;6r{h5yrEbnbM$$ntA@P2IS1PyW^RyG0$S2tUlh z8?E(McS?7}X3nAAJs2u_n{^05)*D7 zW{Y>o99!I9&KQdzgtG(k@BT|J*;{Pt*b|?A_})e98pXCbMWbhBZ$t&YbNQOwN^=F) z_yIb_az2Pyya2530n@Y@s>s>n?L79;U-O9oPY$==~f1gXro5Y z*3~JaenSl_I}1*&dpYD?i8s<7w%~sEojqq~iFnaYyLgM#so%_ZZ^WTV0`R*H@{m2+ zja4MX^|#>xS9YQo{@F1I)!%RhM{4ZUapHTKgLZLcn$ehRq(emb8 z9<&Nx*RLcS#)SdTxcURrJhxPM2IBP%I zf1bWu&uRf{60-?Gclb5(IFI*!%tU*7d`i!l@>TaHzYQqH4_Y*6!Wy0d-B#Lz7Rg3l zqKsvXUk9@6iKV6#!bDy5n&j9MYpcKm!vG7z*2&4G*Yl}iccl*@WqKZWQSJCgQSj+d ze&}E1mAs^hP}>`{BJ6lv*>0-ft<;P@`u&VFI~P3qRtufE11+|#Y6|RJccqo27Wzr}Tp|DH z`G4^v)_8}R24X3}=6X&@Uqu;hKEQV^-)VKnBzI*|Iskecw~l?+R|WKO*~(1LrpdJ? z0!JKnCe<|m*WR>m+Qm+NKNH<_yefIml z+x32qzkNRrhR^IhT#yCiYU{3oq196nC3ePkB)f%7X1G^Ibog$ZnYu4(HyHUiFB`6x zo$ty-8pknmO|B9|(5TzoHG|%>s#7)CM(i=M7Nl=@GyDi-*ng6ahK(&-_4h(lyUN-oOa$` zo+P;C4d@m^p9J4c~rbi$rq9nhGxayFjhg+Rqa{l#`Y z!(P6K7fK3T;y!VZhGiC#)|pl$QX?a)a9$(4l(usVSH>2&5pIu5ALn*CqBt)9$yAl; z-{fOmgu><7YJ5k>*0Q~>lq72!XFX6P5Z{vW&zLsraKq5H%Z26}$OKDMv=sim;K?vsoVs(JNbgTU8-M%+ zN(+7Xl}`BDl=KDkUHM9fLlV)gN&PqbyX)$86!Wv!y+r*~kAyjFUKPDWL3A)m$@ir9 zjJ;uQV9#3$*`Dqo1Cy5*;^8DQcid^Td=CivAP+D;gl4b7*xa9IQ-R|lY5tIpiM~9- z%Hm9*vDV@_1FfiR|Kqh_5Ml0sm?abD>@peo(cnhiSWs$uy&$RYcd+m`6%X9FN%?w}s~Q=3!pJzbN~iJ}bbM*PPi@!E0eN zhKcuT=kAsz8TQo76CMO+FW#hr6da({mqpGK2K4T|xv9SNIXZ}a=4_K5pbz1HE6T}9 zbApW~m0C`q)S^F}B9Kw5!eT)Bj_h9vlCX8%VRvMOg8PJ*>PU>%yt-hyGOhjg!2pZR4{ z=VR_*?Hw|aai##~+^H>3p$W@6Zi`o4^iO2Iy=FPdEAI58Ebc~*%1#sh8KzUKOVHs( z<3$LMSCFP|!>fmF^oESZR|c|2JI3|gucuLq4R(||_!8L@gHU8hUQZKn2S#z@EVf3? zTroZd&}JK(mJLe>#x8xL)jfx$6`okcHP?8i%dW?F%nZh=VJ)32CmY;^y5C1^?V0;M z<3!e8GZcPej-h&-Osc>6PU2f4x=XhA*<_K*D6U6R)4xbEx~{3*ldB#N+7QEXD^v=I z+i^L+V7_2ld}O2b-(#bmv*PyZI4|U#Q5|22a(-VLOTZc3!9ns1RI-? zA<~h|tPH0y*bO1#EMrsWN>4yJM7vqFZr?uw$H8*PhiHRQg1U9YoscX-G|gck+SSRX!(e7@~eeUEw+POsT;=W9J&=EV`cUc{PIg_#TQVGnZsQbCs7#Q-)v#BicxLw#Fb?#)8TYbu zN)5R=MI1i7FHhF|X}xEl=sW~`-kf;fOR^h1yjthSw?%#F{HqrY2$q>7!nbw~nZ8q9 zh{vY! z%i=H!!P&wh z7_E%pB7l5)*VU>_O-S~d5Z!+;f{pQ4e86*&);?G<9*Q$JEJ!ZxY;Oj5&@^eg0Zs!iLCAR`2K?MSFzjX;kHD6)^`&=EZOIdW>L#O`J zf~$M4}JiV}v6B-e{NUBGFgj-*H%NG zfY0X(@|S8?V)drF;2OQcpDl2LV=~=%gGx?_$fbSsi@%J~taHcMTLLpjNF8FkjnjyM zW;4sSf6RHaa~LijL#EJ0W2m!BmQP(f=%Km_N@hsBFw%q#7{Er?y1V~UEPEih87B`~ zv$jE%>Ug9&=o+sZVZL7^+sp)PSrS;ZIJac4S-M>#V;T--4FXZ*>CI7w%583<{>tb6 zOZ8gZ#B0jplyTbzto2VOs)s9U%trre`m=RlKf{I_Nwdxn(xNG%zaVNurEYiMV3*g| z``3;{j7`UyfFrjlEbIJN{0db|r>|LA@=vX9CHFZYiexnkn$b%8Rvw0TZOQIXa;oTI zv@j;ZP+#~|!J(aBz9S{wL7W%Dr1H)G-XUNt9-lP?ijJ-XEj1e*CI~-Xz@4(Xg;UoG z{uzBf-U+(SHe}6oG%;A*93Zb=oE>uTb^%qsL>|bQf?7_6=KIiPU`I|r;YcZ!YG7y~ zQu@UldAwz$^|uoz3mz1;An-WVBtefSh-pv<`n&TU3oM!hrEI?l@v8A4#^$4t&~T32 zl*J=1q~h+60sNc43>0aVvhzyfjshgPYZoQ(OOh>LbUIoblb@1z~zp?))n?^)q6WGuDh}gMUaA9|X z3qq-XlcNldy5==T4rq*~g@XVY!9sYZjo#R7 zr{n)r5^S{9+$+8l7IVB*3_k5%-TBY@C%`P@&tZf>82sm#nfw7L%92>nN$663yW!yt zhS>EfLcE_Z)gv-Y^h1;xj(<4nD4GY{C-nWUgQc9cMmH{qpa!uEznrGF^?bbJHApScQ$j>$JZHAX80DdXu z--AMgrA0$Otdd#N9#!cg2Z~N8&lj1d+wDh+^ZObWJ$J)_h(&2#msu>q0B$DEERy{1 zCJN{7M@%#E@8pda`@u!v@{gcT3bA*>g*xYLXlbb&o@1vX*x+l}Voys6o~^_7>#GB| z*r!R%kA9k%J`?m>1tMHB9x$ZRe0$r~ui}X}jOC)9LH=Po*2SLdtf3^4?VKnu2ox&mV~0oDgi` z;9d}P$g~9%ThTK8s}5ow2V4?(-lU*ed8ro|}mU}pk% z;bqB0bx3AOk<0Joeh}Vl@_7Po&C`Cg>>gff>e7fu41U3Ic{JQu1W%+!Gvz3GDO2ixKd;KF6UEw8F_cDAh08gB>@ zaRH2Q96sBJ>`4aXvrF0xPtIWoA1pPsRQtU~xDtnEfTJnl{A9u5pR^K8=UdNq%T8F$)FbN> zgK+_(BF#D>R>kK!M#OT~=@@}3yAYqm33?{Bv?2iBr|-aRK0@uapzuXI)wE0=R@m^7 zQ`wLBn(M*wg!mgmQT1d!@3<2z>~rmDW)KG0*B4>_R6LjiI0^9QT8gtDDT|Lclxppm z+OeL6H3QpearJAB%1ellZ6d*)wBQ(hPbE=%?y6i^uf%`RXm*JW*WQ%>&J+=V(=qf{ zri~yItvTZbII+7S0>4Q0U9@>HnMP$X>8TqAfD(vAh};2P{QK)ik`a6$W$nG<{bR2Ufd!^iE z#1K58$gW!xpeYHeehuhQCXZ9p%N8m zB+l~T_u-Ycr!U>!?xu!!*6rNxq37{`DhMMfY6NpD3Jw zkYQDstvt30Hc_SaZuuMP2YrdW@HsPMbf^Y9lI<9$bnMil2X7`Ba-DGLbzgqP>mxwe zf1&JkDH54D3nLar2KjJ3z`*R+rUABq4;>>4Kjc2iQEj7pVLcZYZ~pteAG4rm1{>PQy=!QiV5G|tVk)53 zP?Azw+N)Yq3zZ`dW7Q9Bq@Y*jSK0<1f`HM;_>GH57pf_S%Ounz_yhTY8lplQSM`xx zU{r-Deqs+*I~sLI$Oq`>i`J1kJ(+yNOYy$_>R3Jfi680<|^u#J@aY%Q>O zqfI~sCbk#3--^zMkV&Yj0D(R^rK}+_npgPr_4^kYuG=pO%$C_7v{s@-{M-P@RL3^<`kO@b=YdKMuccfO1ZW# zeRYE%D~CMAgPlo?T!O6?b|pOZv{iMWb;sN=jF%=?$Iz_5zH?K;aFGU^8l7u%zHgiy z%)~y|k;Es-7YX69AMj^epGX#&^c@pp+lc}kKc`5CjPN4Z$$e58$Yn*J?81%`0~A)D zPg-db*pj-t4-G9>ImW4IMi*v#9z^9VD9h@9t;3jMAUVxt=oor+16yHf{lT|G4 zya6{4#BxFw!!~UTRwXXawKU4iz$$GMY6=Z8VM{2@0{=5A0+A#p6$aT3ubRyWMWPq9 zCEH5(Il0v4e4=Yxg(tDglfYAy!UpC>&^4=x7#6_S&Ktds)a8^`^tp6RnRd{KImB^o z2n=t#>iKx<*evmvoE{+fH#@WXGWs$)Uxrtf?r>AaxV0?kf0o@oDboJ6z0cgP@A$;k>SK1UqC?Q_ zk_I?j74;}uNXhOf_5ZxQSgB4otDEb9JJrX1kq`-o%T>g%M5~xXf!2_4P~K64tKgXq z&KHZ0@!cPvUJG4kw-0;tPo$zJrU-Nop>Uo65Pm|yaNvKjhi7V1g98;^N1~V3% zTR>yWa+X2FJ_wpPwz3i^6AGwOa_VMS-&`*KoKgF2&oR10Jn6{!pvVG@n=Jk@vjNuY zL~P7aDGhg~O9G^!bHi$8?G9v9Gp0cmekYkK;(q=47;~gI>h-kx-ceM{ml$#8KI$4ltyjaqP zki^cyDERloAb)dcDBU4na9C(pfD{P@eBGA}0|Rb)p{ISqi60=^FUEdF!ok{Gs;vb) zfj9(#1QA64w*ud^YsN5&PeiI>c`VioE8h)e}W%S9NMA55Gs zrWL6l+@3CKd@8(UQLTwe12SGWMqRn+j)QZRj*g)Xua)%ayzpqs{pD(WWESJYL3{M$ z%qkpM`jFoqLYVv6{IbCkL?fEiJj$VG=$taup&RL9e{s(Sgse2xVJlw0h74EXJKt2eX|dxz{->0)3W`JN7Bv!rLvRZc z0tAOZ2yVe4g9iq826qXAg`f!*+}(o1;1FDb>kKexumFS40KvK0yH1_@Z=LgWZ+}(Y zwYsa;OLz6tTA%gS=>8$=Z7pLh>|K2QElL)E=Q*(n*H`8R`8={-@4mTD-SWBOYRxV? zmF(-rJB8^Wlp?319rTrh^?QEP?|Msxrv?WbJ-+id+V#F2Y4(JPJ6U9bv+U1cIIH^W z)lg$_=g^Ma>2~Pyd_YOAv29Cb-U6DJO?NxnW7~QP*SmYi*vdUVuW#LWQ_u0`hymZi zaQS3Nb^4`ro$>0G%zbXmr5|D|iq0R<;S@?kr0j5Ruq87-Z1>crx%EzVZ9#U;{?}ti zW2W%*9MQg3Nbh%Ti6LhDd|-aFSgXoPG`mHlUU1iCHr>ru>DX?W_#13(`u*!Plu2OP z6jk=2>BC0l)aw;HCmxoYD1i4b%m$1`DYC_^L~ zIEAnFcHvad=-aO3(_MI=9#`z6-9*_!&$?<%meb5;jGd5Qp=MGf z6BD{%`L#TAOq%z%@*ib95Ey7NbUF=BlszVk3Iu3imD&*91N-ij%hW?W@~2TtdHTfP z#n0@Xd7X8Dyu36n{k#PwQ~T~X7mAO^cNV+z<HO@3X-# z_@rAn$k~(l@kciCC;&Qd*fWRI>=;fL{UPlciNDWyj$bX<#r^(r;EE8wwUVQm&7~QY zCXRj!**r^xybAEPq>h3W$uvI1j=yNIyzkE_D7fpGw)OV{U*Uwm{xB;mEg2(|y|ICd zMdQVqzMb-=XM6|E-a9kNh)^9lY`-DjhhHD1w5lufRcy+QLgJ47!fFne86#F; zX{ufroVBEZJOY?rDo!;Te6aOZ^1SO!dYRxQ*2njyA~dCWawn)>!*k7~>8Ikt&e*0>>V5ZbO|*1+2LFOqVe zXHb!aMk03^h%&9L8GMy7UDI2Kev>V@(R}*Iu6x+!Hn4~D@wj`P%#Hdbf(lK{+DD7f zJ&(v*mhn_e(R$^5L#bM^^Q@-!*b!l|+Xrb(q*MRFJYnrE7*xko!SJOy9LngR2|q5k zY`Ioiu+YBfzF{Labszk-E#*BYQk>$()=xWEGZRKwY)*UxP}0dGuPLZOkNJDI9Hy zFjfwiK6RjhH#rHW#B0(MW}i%V`943<6@Z*Nd^JEP5uZonXm=u%AM>{H^U@&Jy*i0s za_Da^xI6pMtXzHc{e~_ZcnKP*;=YL2Z^RmzDl{dJTk7*}E_h*NvgnhnxVKB59Duh~ zqouS_WoOR*{UvUw_K#OWz;gMracr%8>QQ&V*jv!8)ho;U8}9~8EU{N<=Z_gR%IpMT zbkePUG_afm=#|iIfFmdqkpLMGxY5D$`?I}&T7>TexU@v zkBx09kG)O;09ckj#(_Uov6vv{{HOcr-%H#DUQ@*GzF8Zh{iSM13%fuB%>wjdU@3Nf zlnYE!GTyNrqes|;nLFXfWU*Wg-9wmr=NBd$nCk+H?iwNvcd0Wab^3CT9a`>3V~oWI z9=_H+N-Q=MQ(io4u4mpdQ;k&5FXnKV5M7R`@WJ9h(GrAirO#XXOU{qQpk^B^Vd=Dt{wiqT zg-#j9J~@o%H2;W9mg)o6@*Vo;BSs2*4HAHpDk02mndAsov08R_48zJZ@J)s7+hyCo zy*0L#y)?AqZt-wX%+_Vx`8*A95OLHvs1$k~{h-_N_vov_gHJE=`X>L?5K+ zD?u59=mjtImMvd1GsDytuYp{IyUkW&?h zF>$#`n$~bZ)KN0B$XGeMYh&`;g8 zo_2-koaO6+8O!+L>SpIQbG(i;QW9UJi{Ecewlo?s&D!^>i$|#jaW}#HJuxt|W48=? zb^Y&O$a1s5ddr8DIt!sD!t=y1g(d4GR(s;s-HfV$GXl&m;+sAAxB^rk(3_NjE$p#L z*t4em?tA0d+XwRxN^OQwzbDZMuSE0J1)Ky{mq)^t4bnSl*)s>zNM@mMdtd78&ebHN z`!(|lE5q-p+TsRaNnMXwALaN5QIZ2IUi^Z22tsN5>nvIO+YU}Q*xh6}ee6@rR~<&1 z(PB4z>9ZBUMXZwSMmd9-aKKsmJeJq^G|#JclOh*xf0?^e0(`40nsg1z)(48;4}B_( zGwPI)yo|{oX{dVDL-5-aMGr;~vU1cPtJP5JM(sswz&Q`e<@0?y{YhsO9YK8EYJA;L z>7oG_Mts+(wCBC*Md82#XdKw&J*IizR?9k^rf1r{Ot-&>V^ke{9nI9zavlcNkIJtN z7T>?o|4rENk-?|lewZ(EfdR;%BUrzKJ^UkCpsM)EA9QHBVV8trT&*O(9?FO{MLTFL z=5P0H+T6C^jAuX0k4U;~GM!x`!X2N~3_n?qXY$HI>x@(DHEy&Q3ucT1R6fj28wX!I zC=&d$@bJ_v^%?W2Ngl}e8ww`b%BrN-PzGH;$@B2Ky1?%GMkm#~Okj(-Admyy;qya| zOi73kr_pwt?5Nj3p=&H>81!w#>Agj z(QXx{j0r=pTl>micAI_5vUw<3`Sht?Z}-j2Wx~F8DKCUQrsXl2?W8hur42(F_ zsSJ)_36&x6A|YkY6c<2a94SXbv~d>4CC4nkDPvf9Z5Fys^6^5r0j5=E>Cgy_Dk@tS z%?c}9!qB?t6t8(XMH%le8UeNWp@Nsma~Ql+^3Bo%_npMryeQJz4V=BAqE~T?dejng z3ge{fjCHoNAfYBvsfq;G%VL|j7t z`X0sy1EEgpyD;)tS1x+fnv-?C@glP0{RCW}Ma?3qpoq_&IJAYOy3G#s`rsh5=3>`K zkj``=;|*x5HSjZC zXNvPLh372q;=+6ja|SC!R-`JcL}}wwskajjTUGTpL(1zkN-p?BA2lmf+J3WsB7!k`0Brx8^cLTF9h)r+LZ$vsZo}`OpOs)?c6$hclR!R#MAeh|_DY|9r zy+_3c%IO9h9X?ksp?an&>Lw;QeQ`T-Ku6HaK~H?E9-Z5$cZu{YU;1+-6B$|JD;%!^ zt(4l>F8}a-UkC4YtOxFHckhl4VKr6P$P_O*U!)IDory%}Wz`YeFx6TO{y2Y${SBm?H9cTWV=WWJ z`_*CGso!ZN>l@~_jkeXtV}fczfA{TUkyeD>)i3|NFGcCsBmK3HXp&ol_@GVs7PIpfULy!hi zs+%KYgS%(n7_z_}6)hblk~W#LZ@&2)fwm6xkFP%&Ju|MFWbNiTwy{{g-pV1RK`L&=RE2D z4|g;~vd8xd|teYS%w!IlT4W$&FTrk-hcTADX!P?*f1YWEIRwq$Ys%^(Z9w&HT$>} zsMD#6Df=uJrX!JHP7<>Or;e_Cf=}`!`qR=i8fBj)$6Lxx{HRzd8Tnzd0p>kSps{OG zKJkml>bUj8$u|F=``l(-aMxWBC@CGZ#FXClQZ<4|&%jN}Tkg#q8z)=>Ly{$i0`rjU zvt|QddO&i=91e?h3>s~i;+6{ z8X4i6a1wDLrSuE#W(zhan+U*Zq+8p3a))JFVF4ffaV51K^YgTso~3;Y*NmM; zx8T?y-N0uyWY(8=me-HUC9xtABvX5~%yg+Cp&XF$Bq=OcK6T*D7eZ2EmIoCFWm{$S z1PNw8HDpe5hHeCusN8kdeb&f2#=3M^A~7YwJ7FRrhq*)PG9x?JIAaC{MV}5}g#7R$-Ly%)4=IUkRCGOR|XTMjn&okRmFjaO^YF5^* z@)#MCBOBezD)*xQNxydlUyN?dW{fS(s-T`gv*0BEnk}`BdmrbmPO8q8y(X$AA}*RH%I7Av!~84pudHb&%Q5-j zt?=6x(iR?<^_7X0v6Ys#VAL}dKk^hcjI=|EY;kPcZ_w<*H`_*|N7SacaM1ERD@6ab zg`!iTm7$URV+lpW_{V$ruR&A>jrX68k4x2wo$45}&wf7o<|o(@B!u-L@bKyQBAGwy z4#}UrRAu>^>Vb6k2-th^>WjvP;Nl|i3WrjWv3ISkj{m{eAcQIW^_ndxSX@|8T(ASJ z?_$fcP2u*6uOBk-{d>^ z0vWlfGQMvysI%R=iE|A+!!Nw?C917EU*_$`;;)px?s83CRd3i_jBN)k#nR5t$dJ(+ z_sP;wG@Ad)^(3LRj7q}0b2O(b`|i0~5SYb%Sjk^*5ISZ-Ab+}DGu$-X1n^TF1Ndw_ zF|e*1)cI2%`TR&AW~XpqpFb!=3cHbS>np9hYD_Mr5}y5Y`SY^r7isA2Q4(z zazRQEqWDKT2zIEbjSYdCPi1ZOGz80Nsl}gxO^DWMY0AV<2K&OL{&^6#@L1?lXu#6xSMh%3^5c*}oM6DQGY#(a^@z<&D zF(43I9e&5`h|A$5!+UFuOH0>F3$shBV4`0#M4RSB8=6F0ZgIbq<2LQ$Hh^(kAJu=! zt8ZGXTacD{(3W{V1$j_{Jc)Ka7t6u}ho`4kF+4@t_0!mCBn z)}o%eA}L)_L?=jw6BIfll7tb3n}?*yLt&XADa=rW>qz=_6s9ziOd5sXjil>FVFx3r zf>Feewk0v#W9>Gp4GacTRr>Sd2T6dWi-{YX`v!D)kCWzG5xQB=?es5ON(%nkwUhNl zV>@xkWWWv*N+{e$(SrExvN6BXzU(Hxlx27{VYHf+LpIbTO+Yu(ltMk<;)3A(LU@ytVYFkYvTa79idMtUFhfxx?P!)2F`prNWW#Fub#l>N2s@nh&n_ zA4{#}|AIs9|A4P0ZF%fy=hDN!t#ifH<)4u2kirK~JUpjQ-J+~cXOZI&dIts;P}UeXslP6zKvpEKSN-$y>kJ^nw2tC9bv zo(|lT@?vZ!{_l|d^8Yh)eEBh*5ABh+Lzjw+?V)o z#P-W7361>E(Y4;@`sv;VKn G`u_lkUM?>H literal 0 HcmV?d00001 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("