├── .gitignore ├── ext ├── favicon.ico ├── 32_home_lock_open.png ├── 64_home_lock_open.png ├── apple-touch-icon.png ├── glyphicons-halflings.png ├── apple-touch-icon-57x57.png ├── apple-touch-icon-60x60.png ├── apple-touch-icon-72x72.png ├── apple-touch-icon-76x76.png ├── apple-touch-icon-114x114.png ├── apple-touch-icon-120x120.png ├── apple-touch-icon-144x144.png ├── apple-touch-icon-152x152.png ├── glyphicons-halflings-white.png ├── livestamp.min.js ├── sco.message.js ├── alertify.core.css ├── alertify.bootstrap.css ├── alarmserver.js ├── alertify.min.js ├── fastclick.min.js ├── index.html ├── bootstrap-responsive.min.css ├── moment.min.js └── bootstrap.min.js ├── ubuntu-initscript ├── server.key ├── server.crt ├── alarmserver-example.cfg ├── README.md ├── HTTPChannel.py ├── Envisalink.py ├── envisalinkdefs.py └── alarmserver.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | 3 | alarmserver.cfg 4 | .idea 5 | -------------------------------------------------------------------------------- /ext/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bs/AlarmServer/master/ext/favicon.ico -------------------------------------------------------------------------------- /ext/32_home_lock_open.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bs/AlarmServer/master/ext/32_home_lock_open.png -------------------------------------------------------------------------------- /ext/64_home_lock_open.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bs/AlarmServer/master/ext/64_home_lock_open.png -------------------------------------------------------------------------------- /ext/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bs/AlarmServer/master/ext/apple-touch-icon.png -------------------------------------------------------------------------------- /ext/glyphicons-halflings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bs/AlarmServer/master/ext/glyphicons-halflings.png -------------------------------------------------------------------------------- /ext/apple-touch-icon-57x57.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bs/AlarmServer/master/ext/apple-touch-icon-57x57.png -------------------------------------------------------------------------------- /ext/apple-touch-icon-60x60.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bs/AlarmServer/master/ext/apple-touch-icon-60x60.png -------------------------------------------------------------------------------- /ext/apple-touch-icon-72x72.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bs/AlarmServer/master/ext/apple-touch-icon-72x72.png -------------------------------------------------------------------------------- /ext/apple-touch-icon-76x76.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bs/AlarmServer/master/ext/apple-touch-icon-76x76.png -------------------------------------------------------------------------------- /ext/apple-touch-icon-114x114.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bs/AlarmServer/master/ext/apple-touch-icon-114x114.png -------------------------------------------------------------------------------- /ext/apple-touch-icon-120x120.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bs/AlarmServer/master/ext/apple-touch-icon-120x120.png -------------------------------------------------------------------------------- /ext/apple-touch-icon-144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bs/AlarmServer/master/ext/apple-touch-icon-144x144.png -------------------------------------------------------------------------------- /ext/apple-touch-icon-152x152.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bs/AlarmServer/master/ext/apple-touch-icon-152x152.png -------------------------------------------------------------------------------- /ext/glyphicons-halflings-white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bs/AlarmServer/master/ext/glyphicons-halflings-white.png -------------------------------------------------------------------------------- /ubuntu-initscript: -------------------------------------------------------------------------------- 1 | version "1.0" 2 | author "Daniel Leaberry" 3 | 4 | start on filesystem and net-device-up IFACE!=lo 5 | stop on stopping network-interface INTERFACE=eth0 6 | stop on starting shutdown 7 | 8 | respawn 9 | console output 10 | exec /usr/bin/python /path/to/alarmserver.py -c /path/to/alarmserver.cfg 11 | -------------------------------------------------------------------------------- /server.key: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIICXQIBAAKBgQDQ4xHHvPjESHOPpUTkTw/azfeC3CYz/oET9I58ffFxglQfWgHf 3 | 0EDMb77OYp01WIZ6DCGeOC1lhDdPSbxnkEz5V3EC0+xGnh5WB9X2T5Gekc6fDwgh 4 | 7pvgwCG0AFVqqzUVLwB1kCy5S4RLOVnyDdFnv5cnCP00oGmmh6aD3UnuTwIDAQAB 5 | AoGBAM9BTUXyAJ24CTjwUQ96Ro/hpoAncJxMG8Qx9SIeT+5A0rictJld5r0w7o+W 6 | Fsd0Q1FbMgvrT1eXPM2lqpLTARrjtgQglUHjwYC0wrFWpFgXBrIV/BSQYxK20iga 7 | TCDPjI2aANZpho2gglzO8/EMOYVQoYdCvnAn6ksqzT2wnE3BAkEA/gnjF56RdrI+ 8 | jbTElM/GuT4pt8XWV1vUXVVqdXnfOjbwWf8tv2+QG4ebOjYk5sIZEpeGQaQuvH5C 9 | vj957VKAbwJBANJ/8G5HlHMg0MGzqP16V2PIkTo+NeN2ig2/pmS+O9NlM2SDS+yT 10 | w2bAAo+hd/7pmd7xeqT0rK+BUPjAJE2+oCECQBCr+3BYYrmEdyB7pY8Sl7sefkRm 11 | QmvXRfeeHG97QRAj/OAbJBh1LOLxollOpltSj6ytrwztxnduXdj4d3sAuBcCQAJy 12 | jl8Z4fX8ubCm4B4iYAW+/UFKG+JLLIvAYLTnKVbp5FEU3bsgdLMrJFx7KiQCn1Fi 13 | SWTFm4Rm4oQh58onn0ECQQDm7dsB6rx4T37lThWE4ZVDpYZ2X2xpQcp0sjCiMXnX 14 | U1KeT0WmaUTTI2iMGKtdablPfnLfFBlQe5eUgO6Fo4KZ 15 | -----END RSA PRIVATE KEY----- 16 | -------------------------------------------------------------------------------- /server.crt: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIICcTCCAdoCCQDKpfJYG8ha3zANBgkqhkiG9w0BAQUFADB9MQswCQYDVQQGEwJD 3 | QTEQMA4GA1UECBMHT250YXJpbzEPMA0GA1UEBxMGT3R0YXdhMRQwEgYDVQQKEwtB 4 | bGFybVNlcnZlcjEUMBIGA1UEAxMLQWxhcm1TZXJ2ZXIxHzAdBgkqhkiG9w0BCQEW 5 | EGRvbm55a0BnbWFpbC5jb20wHhcNMTIxMjE1MDU1MTQxWhcNMTMxMjE1MDU1MTQx 6 | WjB9MQswCQYDVQQGEwJDQTEQMA4GA1UECBMHT250YXJpbzEPMA0GA1UEBxMGT3R0 7 | YXdhMRQwEgYDVQQKEwtBbGFybVNlcnZlcjEUMBIGA1UEAxMLQWxhcm1TZXJ2ZXIx 8 | HzAdBgkqhkiG9w0BCQEWEGRvbm55a0BnbWFpbC5jb20wgZ8wDQYJKoZIhvcNAQEB 9 | BQADgY0AMIGJAoGBANDjEce8+MRIc4+lRORPD9rN94LcJjP+gRP0jnx98XGCVB9a 10 | Ad/QQMxvvs5inTVYhnoMIZ44LWWEN09JvGeQTPlXcQLT7EaeHlYH1fZPkZ6Rzp8P 11 | CCHum+DAIbQAVWqrNRUvAHWQLLlLhEs5WfIN0We/lycI/TSgaaaHpoPdSe5PAgMB 12 | AAEwDQYJKoZIhvcNAQEFBQADgYEASqwreYMWeBt/QMWQwGqWT59KuqACTHvY5d8l 13 | cItR8tDXAm19x4tiBxMOKuvzq0CQ9mQXk5Zbq2A1joFby2pfDYHerABhVBG9Xzar 14 | bDb4ARbKJIpcr/UtEu1E1cw7/NL2ruo+B4savunCiNgs9cDyFhmwxc6pz7zGFj1x 15 | v9XYFUs= 16 | -----END CERTIFICATE----- 17 | -------------------------------------------------------------------------------- /ext/livestamp.min.js: -------------------------------------------------------------------------------- 1 | // Livestamp.js / v1.1.2 / (c) 2012 Matt Bradley / MIT License 2 | (function(d,g){var h=1E3,i=!1,e=d([]),j=function(b,a){var c=b.data("livestampdata");"number"==typeof a&&(a*=1E3);b.removeAttr("data-livestamp").removeData("livestamp");a=g(a);g.isMoment(a)&&!isNaN(+a)&&(c=d.extend({},{original:b.contents()},c),c.moment=g(a),b.data("livestampdata",c).empty(),e.push(b[0]))},k=function(){i||(f.update(),setTimeout(k,h))},f={update:function(){d("[data-livestamp]").each(function(){var a=d(this);j(a,a.data("livestamp"))});var b=[];e.each(function(){var a=d(this),c=a.data("livestampdata"); 3 | if(void 0===c)b.push(this);else if(g.isMoment(c.moment)){var e=a.html(),c=c.moment.fromNow();if(e!=c){var f=d.Event("change.livestamp");a.trigger(f,[e,c]);f.isDefaultPrevented()||a.html(c)}}});e=e.not(b)},pause:function(){i=!0},resume:function(){i=!1;k()},interval:function(b){if(void 0===b)return h;h=b}},l={add:function(b,a){"number"==typeof a&&(a*=1E3);a=g(a);g.isMoment(a)&&!isNaN(+a)&&(b.each(function(){j(d(this),a)}),f.update());return b},destroy:function(b){e=e.not(b);b.each(function(){var a= 4 | d(this),c=a.data("livestampdata");if(void 0===c)return b;a.html(c.original?c.original:"").removeData("livestampdata")});return b},isLivestamp:function(b){return void 0!==b.data("livestampdata")}};d.livestamp=f;d(function(){f.resume()});d.fn.livestamp=function(b,a){l[b]||(a=b,b="add");return l[b](this,a)}})(jQuery,moment); 5 | -------------------------------------------------------------------------------- /alarmserver-example.cfg: -------------------------------------------------------------------------------- 1 | [alarmserver] 2 | ## If a filename is given all output will be logged to the filename. 3 | ## If left blank output will all be on the console 4 | #logfile=/full/path/to/output.log 5 | logfile= 6 | 7 | ## Log URL requests 8 | ## By default all the url requests are logged. These happen every 5 9 | ## seconds with the web ui's. To disable all these set this to False 10 | logurlrequests=True 11 | 12 | 13 | ## The server runs with SSL. You need a certificate and key 14 | ## server.crt and server.key are included but you should 15 | ## generate your own. 16 | ## If left blank the default included cert/key will be used 17 | #certfile=/etc/apache2/ssl/server.crt 18 | #keyfile=/etc/apache2/ssl/server.key 19 | certfile= 20 | keyfile= 21 | 22 | ## Maximum number of events to display for each zone 23 | maxevents=10 24 | 25 | ## Total number of events to show for all the zones combined 26 | maxallevents=100 27 | 28 | ## Port to run the server on 29 | httpsport=8111 30 | 31 | ## Use a fuzzy time algorithm for displaying dates and times 32 | ## True means times will be "4 minutes ago", "3 days ago" 33 | ## False means times will be exact "Jun 21st 2013 08:00:00" 34 | eventtimeago=True 35 | 36 | ## Name of your parition(s) 37 | partition1=Home 38 | 39 | ## Zone names. Delete the zones you're not using to have them hidden. 40 | ## Add more zoneXX if you need more zones 41 | zone1=A 42 | zone2=B 43 | zone3=C 44 | zone4=D 45 | zone5=E 46 | zone6=F 47 | zone7=G 48 | zone8=H 49 | zone9=I 50 | zone10=J 51 | zone11=K 52 | zone12=L 53 | zone13=M 54 | zone14=N 55 | zone15=O 56 | zone16=P 57 | 58 | ## Pretty names for the user ids that arm/disarm alarm. 59 | user1=MyUser1 60 | user2=MyUser2 61 | user3=MyUser3 62 | 63 | [pushover] 64 | enable=False 65 | usertoken=tokengoeshere 66 | 67 | [envisalink] 68 | ## Connection credentials to talk to the Envisalink device 69 | host=envisalink 70 | port=4025 71 | pass=user 72 | 73 | ## Run a proxy for the Envisalink device to get around the 1 connection limit 74 | enableproxy=True 75 | proxyport=4025 76 | proxypass=user 77 | 78 | ## Alarm code: If defined you can disarm the alarm without having to 79 | ## enter a code. 80 | alarmcode=1111 81 | -------------------------------------------------------------------------------- /ext/sco.message.js: -------------------------------------------------------------------------------- 1 | /* ========================================================== 2 | * sco.message.js 3 | * http://github.com/terebentina/sco.js 4 | * ========================================================== 5 | * Copyright 2013 Dan Caragea. 6 | * 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | * ========================================================== */ 19 | 20 | /*jshint laxcomma:true, sub:true, browser:true, jquery:true, eqeqeq: false */ 21 | 22 | ;(function($, undefined) { 23 | "use strict"; 24 | 25 | var pluginName = 'scojs_message'; 26 | 27 | $[pluginName] = function(message, type) { 28 | clearTimeout($[pluginName].timeout); 29 | var $selector = $('#' + $[pluginName].options.id); 30 | if (!$selector.length) { 31 | $selector = $('
', {id: $[pluginName].options.id}).appendTo($[pluginName].options.appendTo); 32 | } 33 | $selector.html(message); 34 | if (type === undefined || type == $[pluginName].TYPE_ERROR) { 35 | $selector.removeClass($[pluginName].options.okClass).addClass($[pluginName].options.errClass); 36 | } else if (type == $[pluginName].TYPE_OK) { 37 | $selector.removeClass($[pluginName].options.errClass).addClass($[pluginName].options.okClass); 38 | } 39 | $selector.slideDown('fast', function() { 40 | $[pluginName].timeout = setTimeout(function() { $selector.slideUp('fast'); }, $[pluginName].options.delay); 41 | }); 42 | }; 43 | 44 | 45 | $.extend($[pluginName], { 46 | options: { 47 | id: 'page_message' 48 | ,okClass: 'page_mess_ok' 49 | ,errClass: 'page_mess_error' 50 | ,delay: 4000 51 | ,appendTo: 'body' // where should the modal be appended to (default to document.body). Added for unit tests, not really needed in real life. 52 | }, 53 | 54 | TYPE_ERROR: 1, 55 | TYPE_OK: 2 56 | }); 57 | })(jQuery); 58 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | This is still beta software. 2 | 3 | The ssl certificates that are provided are intended for demo purposes only. 4 | Please use openssl to generate your own. A quick HOWTO is below. 5 | 6 | As with any project documentation is key, there is plenty more to go in here and 7 | it will hopefully be soon! 8 | 9 | Config: 10 | Please see the alarmserver-example.cfg and rename to alarmserver.cfg and 11 | customize to requirements. 12 | 13 | 14 | Web Interface 15 | ------------- 16 | The web interface uses a responsive design which limits the scrolling on both desktop and mobile. 17 | 18 | ### Desktop ### 19 | ![Desktop](http://gschrader.github.io/Alarm-Server-Launcher/desktop.png) 20 | 21 | ### Mobile ### 22 | ![Mobile](http://gschrader.github.io/Alarm-Server-Launcher/mobile.png) 23 | 24 | 25 | OpenSSL Certificate Howto 26 | ------------------- 27 | 28 | To generate a self signed cert issue the following in a command prompt: 29 | `openssl req -x509 -nodes -days 3650 -newkey rsa:2048 -keyout server.key -out server.crt` 30 | 31 | Openssl will ask you some questions. The only semi-important one is the 'common name' field. 32 | You want this set to your servers fqdn. IE alarmserver.example.com. 33 | 34 | If you have a real ssl cert from a certificate authority and it has intermediate certs then you'll need to bundle them all up or the webbrowser will complain about it not being a valid cert. To bundle the certs use cat to include your cert, then the intermediates (ie cat mycert.crt > combined.crt; cat intermediates.crt >> combined.crt) 35 | 36 | 37 | Dependencies: 38 | ------------- 39 | 40 | On windows, pyOpenSSL is required. 41 | http://pypi.python.org/pypi/pyOpenSSL 42 | 43 | 44 | Launchers 45 | --------- 46 | * [MacOSX](https://github.com/gschrader/Alarm-Server-Launcher) 47 | 48 | REST API Info 49 | ------------- 50 | 51 | */api* 52 | 53 | * Returns a JSON dump of all currently known states 54 | 55 | */api/alarm/arm* 56 | 57 | * Quick arm 58 | 59 | */api/alarm/armwithcode?alarmcode=1111* 60 | 61 | * Arm with a code 62 | * Required param = **alarmcode** 63 | 64 | */api/alarm/stayarm* 65 | 66 | * Stay arm, no code needed 67 | 68 | */api/alarm/disarm* 69 | 70 | * Disarm system 71 | * Optional param = **alarmcode** 72 | * If alarmcode param is missing the config file value is used instead 73 | 74 | */api/pgm* 75 | 76 | * Activate a PGM output: 77 | * Required param = **pgmnum** 78 | * Required param = **alarmcode** 79 | 80 | */api/refresh* 81 | 82 | * Refresh data from alarm panel 83 | 84 | */api/config/eventtimeago* 85 | 86 | * Returns status of eventtimeago from the config file 87 | 88 | -------------------------------------------------------------------------------- /HTTPChannel.py: -------------------------------------------------------------------------------- 1 | import asyncore, asynchat 2 | import StringIO, mimetools 3 | import string 4 | import os, sys 5 | import datetime 6 | 7 | class push_FileProducer: 8 | # a producer which reads data from a file object 9 | 10 | def __init__(self, file): 11 | self.file = open(file, "rb") 12 | 13 | def more(self): 14 | if self.file: 15 | data = self.file.read(2048) 16 | if data: 17 | return data 18 | self.file = None 19 | return "" 20 | 21 | class HTTPChannel(asynchat.async_chat): 22 | def __init__(self, server, sock, addr): 23 | asynchat.async_chat.__init__(self, sock) 24 | self.server = server 25 | self.set_terminator("\r\n\r\n") 26 | self.header = None 27 | self.data = "" 28 | self.shutdown = 0 29 | 30 | def collect_incoming_data(self, data): 31 | self.data = self.data + data 32 | if len(self.data) > 16384: 33 | # limit the header size to prevent attacks 34 | self.shutdown = 1 35 | 36 | def found_terminator(self): 37 | if not self.header: 38 | # parse http header 39 | fp = StringIO.StringIO(self.data) 40 | request = string.split(fp.readline(), None, 2) 41 | if len(request) != 3: 42 | # badly formed request; just shut down 43 | self.shutdown = 1 44 | else: 45 | # parse message header 46 | self.header = mimetools.Message(fp) 47 | self.set_terminator("\r\n") 48 | self.server.handle_request( 49 | self, request[0], request[1], self.header 50 | ) 51 | self.close_when_done() 52 | self.data = "" 53 | else: 54 | pass # ignore body data, for now 55 | 56 | def pushstatus(self, status, explanation="OK"): 57 | self.push("HTTP/1.0 %d %s\r\n" % (status, explanation)) 58 | 59 | def pushok(self, content): 60 | self.pushstatus(200, "OK") 61 | self.push('Content-type: application/json\r\n') 62 | self.push('Expires: Sat, 26 Jul 1997 05:00:00 GMT\r\n') 63 | self.push('Last-Modified: '+ datetime.datetime.now().strftime("%d/%m/%Y %H:%M:%S")+' GMT\r\n') 64 | self.push('Cache-Control: no-store, no-cache, must-revalidate\r\n' ) 65 | self.push('Cache-Control: post-check=0, pre-check=0\r\n') 66 | self.push('Pragma: no-cache\r\n' ) 67 | self.push('\r\n') 68 | self.push(content) 69 | 70 | def pushfile(self, file): 71 | self.pushstatus(200, "OK") 72 | extension = os.path.splitext(file)[1] 73 | if extension == ".html": 74 | self.push("Content-type: text/html\r\n") 75 | elif extension == ".js": 76 | self.push("Content-type: text/javascript\r\n") 77 | elif extension == ".png": 78 | self.push("Content-type: image/png\r\n") 79 | elif extension == ".css": 80 | self.push("Content-type: text/css\r\n") 81 | self.push("\r\n") 82 | self.push_with_producer(push_FileProducer(sys.path[0] + os.sep + 'ext' + os.sep + file)) 83 | 84 | -------------------------------------------------------------------------------- /ext/alertify.core.css: -------------------------------------------------------------------------------- 1 | .alertify, 2 | .alertify-show, 3 | .alertify-log { 4 | -webkit-transition: all 500ms cubic-bezier(0.175, 0.885, 0.320, 1.275); 5 | -moz-transition: all 500ms cubic-bezier(0.175, 0.885, 0.320, 1.275); 6 | -ms-transition: all 500ms cubic-bezier(0.175, 0.885, 0.320, 1.275); 7 | -o-transition: all 500ms cubic-bezier(0.175, 0.885, 0.320, 1.275); 8 | transition: all 500ms cubic-bezier(0.175, 0.885, 0.320, 1.275); /* easeOutBack */ 9 | } 10 | .alertify-hide { 11 | -webkit-transition: all 250ms cubic-bezier(0.600, -0.280, 0.735, 0.045); 12 | -moz-transition: all 250ms cubic-bezier(0.600, -0.280, 0.735, 0.045); 13 | -ms-transition: all 250ms cubic-bezier(0.600, -0.280, 0.735, 0.045); 14 | -o-transition: all 250ms cubic-bezier(0.600, -0.280, 0.735, 0.045); 15 | transition: all 250ms cubic-bezier(0.600, -0.280, 0.735, 0.045); /* easeInBack */ 16 | } 17 | .alertify-log-hide { 18 | -webkit-transition: all 500ms cubic-bezier(0.600, -0.280, 0.735, 0.045); 19 | -moz-transition: all 500ms cubic-bezier(0.600, -0.280, 0.735, 0.045); 20 | -ms-transition: all 500ms cubic-bezier(0.600, -0.280, 0.735, 0.045); 21 | -o-transition: all 500ms cubic-bezier(0.600, -0.280, 0.735, 0.045); 22 | transition: all 500ms cubic-bezier(0.600, -0.280, 0.735, 0.045); /* easeInBack */ 23 | } 24 | .alertify-cover { 25 | position: fixed; z-index: 99999; 26 | top: 0; right: 0; bottom: 0; left: 0; 27 | background-color:white; 28 | filter:alpha(opacity=0); 29 | opacity:0; 30 | } 31 | .alertify-cover-hidden { 32 | display: none; 33 | } 34 | .alertify { 35 | position: fixed; z-index: 99999; 36 | top: 50px; left: 50%; 37 | width: 550px; 38 | margin-left: -275px; 39 | opacity: 1; 40 | } 41 | .alertify-hidden { 42 | -webkit-transform: translate(0,-150px); 43 | -moz-transform: translate(0,-150px); 44 | -ms-transform: translate(0,-150px); 45 | -o-transform: translate(0,-150px); 46 | transform: translate(0,-150px); 47 | opacity: 0; 48 | display: none; 49 | } 50 | /* overwrite display: none; for everything except IE6-8 */ 51 | :root *> .alertify-hidden { 52 | display: block; 53 | visibility: hidden; 54 | } 55 | .alertify-logs { 56 | position: fixed; 57 | z-index: 5000; 58 | bottom: 10px; 59 | right: 10px; 60 | width: 300px; 61 | } 62 | .alertify-logs-hidden { 63 | display: none; 64 | } 65 | .alertify-log { 66 | display: block; 67 | margin-top: 10px; 68 | position: relative; 69 | right: -300px; 70 | opacity: 0; 71 | } 72 | .alertify-log-show { 73 | right: 0; 74 | opacity: 1; 75 | } 76 | .alertify-log-hide { 77 | -webkit-transform: translate(300px, 0); 78 | -moz-transform: translate(300px, 0); 79 | -ms-transform: translate(300px, 0); 80 | -o-transform: translate(300px, 0); 81 | transform: translate(300px, 0); 82 | opacity: 0; 83 | } 84 | .alertify-dialog { 85 | padding: 25px; 86 | } 87 | .alertify-resetFocus { 88 | border: 0; 89 | clip: rect(0 0 0 0); 90 | height: 1px; 91 | margin: -1px; 92 | overflow: hidden; 93 | padding: 0; 94 | position: absolute; 95 | width: 1px; 96 | } 97 | .alertify-inner { 98 | text-align: center; 99 | } 100 | .alertify-text { 101 | margin-bottom: 15px; 102 | width: 100%; 103 | -webkit-box-sizing: border-box; 104 | -moz-box-sizing: border-box; 105 | box-sizing: border-box; 106 | font-size: 100%; 107 | } 108 | .alertify-buttons { 109 | } 110 | .alertify-button, 111 | .alertify-button:hover, 112 | .alertify-button:active, 113 | .alertify-button:visited { 114 | background: none; 115 | text-decoration: none; 116 | border: none; 117 | /* line-height and font-size for input button */ 118 | line-height: 1.5; 119 | font-size: 100%; 120 | display: inline-block; 121 | cursor: pointer; 122 | margin-left: 5px; 123 | } 124 | 125 | @media only screen and (max-width: 680px) { 126 | .alertify, 127 | .alertify-logs { 128 | width: 90%; 129 | -webkit-box-sizing: border-box; 130 | -moz-box-sizing: border-box; 131 | box-sizing: border-box; 132 | } 133 | .alertify { 134 | left: 5%; 135 | margin: 0; 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /ext/alertify.bootstrap.css: -------------------------------------------------------------------------------- 1 | /** 2 | * Twitter Bootstrap Look and Feel 3 | * Based on http://twitter.github.com/bootstrap/ 4 | */ 5 | .alertify, 6 | .alertify-log { 7 | font-family: sans-serif; 8 | } 9 | .alertify { 10 | background: #FFF; 11 | border: 1px solid #8E8E8E; /* browsers that don't support rgba */ 12 | border: 1px solid rgba(0,0,0,.3); 13 | border-radius: 6px; 14 | box-shadow: 0 3px 7px rgba(0,0,0,.3); 15 | -webkit-background-clip: padding; /* Safari 4? Chrome 6? */ 16 | -moz-background-clip: padding; /* Firefox 3.6 */ 17 | background-clip: padding-box; /* Firefox 4, Safari 5, Opera 10, IE 9 */ 18 | } 19 | .alertify-dialog { 20 | padding: 0; 21 | } 22 | .alertify-inner { 23 | text-align: left; 24 | } 25 | .alertify-message { 26 | padding: 15px; 27 | margin: 0; 28 | } 29 | .alertify-text-wrapper { 30 | padding: 0 15px; 31 | } 32 | .alertify-text { 33 | color: #555; 34 | border-radius: 4px; 35 | padding: 8px; 36 | background-color: #FFF; 37 | border: 1px solid #CCC; 38 | box-shadow: inset 0 1px 1px rgba(0,0,0,.075); 39 | } 40 | .alertify-text:focus { 41 | border-color: rgba(82,168,236,.8); 42 | outline: 0; 43 | box-shadow: inset 0 1px 1px rgba(0,0,0,.075), 0 0 8px rgba(82,168,236,.6); 44 | } 45 | 46 | .alertify-buttons { 47 | padding: 14px 15px 15px; 48 | background: #F5F5F5; 49 | border-top: 1px solid #DDD; 50 | border-radius: 0 0 6px 6px; 51 | box-shadow: inset 0 1px 0 #FFF; 52 | text-align: right; 53 | } 54 | .alertify-button, 55 | .alertify-button:hover, 56 | .alertify-button:focus, 57 | .alertify-button:active { 58 | margin-left: 10px; 59 | border-radius: 4px; 60 | font-weight: normal; 61 | padding: 4px 12px; 62 | text-decoration: none; 63 | box-shadow: inset 0 1px 0 rgba(255, 255, 255, .2), 0 1px 2px rgba(0, 0, 0, .05); 64 | background-image: -webkit-linear-gradient(top, rgba(255,255,255,.3), rgba(255,255,255,0)); 65 | background-image: -moz-linear-gradient(top, rgba(255,255,255,.3), rgba(255,255,255,0)); 66 | background-image: -ms-linear-gradient(top, rgba(255,255,255,.3), rgba(255,255,255,0)); 67 | background-image: -o-linear-gradient(top, rgba(255,255,255,.3), rgba(255,255,255,0)); 68 | background-image: linear-gradient(top, rgba(255,255,255,.3), rgba(255,255,255,0)); 69 | } 70 | .alertify-button:focus { 71 | outline: none; 72 | box-shadow: 0 0 5px #2B72D5; 73 | } 74 | .alertify-button:active { 75 | position: relative; 76 | box-shadow: inset 0 2px 4px rgba(0,0,0,.15), 0 1px 2px rgba(0,0,0,.05); 77 | } 78 | .alertify-button-cancel, 79 | .alertify-button-cancel:hover, 80 | .alertify-button-cancel:focus, 81 | .alertify-button-cancel:active { 82 | text-shadow: 0 -1px 0 rgba(255,255,255,.75); 83 | background-color: #E6E6E6; 84 | border: 1px solid #BBB; 85 | color: #333; 86 | background-image: -webkit-linear-gradient(top, #FFF, #E6E6E6); 87 | background-image: -moz-linear-gradient(top, #FFF, #E6E6E6); 88 | background-image: -ms-linear-gradient(top, #FFF, #E6E6E6); 89 | background-image: -o-linear-gradient(top, #FFF, #E6E6E6); 90 | background-image: linear-gradient(top, #FFF, #E6E6E6); 91 | } 92 | .alertify-button-cancel:hover, 93 | .alertify-button-cancel:focus, 94 | .alertify-button-cancel:active { 95 | background: #E6E6E6; 96 | } 97 | .alertify-button-ok, 98 | .alertify-button-ok:hover, 99 | .alertify-button-ok:focus, 100 | .alertify-button-ok:active { 101 | text-shadow: 0 -1px 0 rgba(0,0,0,.25); 102 | background-color: #04C; 103 | border: 1px solid #04C; 104 | border-color: #04C #04C #002A80; 105 | border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25); 106 | color: #FFF; 107 | } 108 | .alertify-button-ok:hover, 109 | .alertify-button-ok:focus, 110 | .alertify-button-ok:active { 111 | background: #04C; 112 | } 113 | 114 | .alertify-log { 115 | background: #D9EDF7; 116 | padding: 8px 14px; 117 | border-radius: 4px; 118 | color: #3A8ABF; 119 | text-shadow: 0 1px 0 rgba(255,255,255,.5); 120 | border: 1px solid #BCE8F1; 121 | } 122 | .alertify-log-error { 123 | color: #B94A48; 124 | background: #F2DEDE; 125 | border: 1px solid #EED3D7; 126 | } 127 | .alertify-log-success { 128 | color: #468847; 129 | background: #DFF0D8; 130 | border: 1px solid #D6E9C6; 131 | } -------------------------------------------------------------------------------- /ext/alarmserver.js: -------------------------------------------------------------------------------- 1 | var activeTab = null; 2 | var activeCollapse = null; 3 | var timeago = true; 4 | var autorefresh = true; 5 | var cache = {}; 6 | 7 | window.matchMediaPhone = function () { 8 | return matchMedia('(max-width: 767px)').matches; 9 | } 10 | window.matchMediaTablet = function () { 11 | return matchMedia('(min-width: 768px) and (max-width: 979px)').matches; 12 | } 13 | window.matchMediaDesktop = function () { 14 | return matchMedia('(min-width: 979px)').matches; 15 | } 16 | 17 | $.ajax({ 18 | type: "GET", 19 | url: "/api/config/eventtimeago", 20 | contentType: "application/json; charset=utf-8", 21 | dataType: "json", 22 | data: "{}", 23 | success: function (res) { 24 | timeago = res.eventtimeago.toLowerCase() == "true"; 25 | } 26 | }); 27 | 28 | function createEvents(list) { 29 | var source = $("#events-template").html(); 30 | var template = Handlebars.compile(source); 31 | 32 | list.reverse().forEach(function (ev, i) { 33 | ev.time = moment(ev.datetime).calendar(); 34 | }); 35 | 36 | return template({events: list, timeago: timeago}); 37 | } 38 | 39 | function details(obj, templateId) { 40 | var source = $(templateId).html(); 41 | var template = Handlebars.compile(source); 42 | 43 | var zones = []; 44 | for (var i = 1; i < 65; i++) { 45 | var zone = obj.zone[i + '']; 46 | if (zone && zone.name) { 47 | zone.id = i; 48 | zone.class = zone.status.open ? 'badge-important' : 'badge-success'; 49 | zone.icon = !zone.status.open ? 'icon-ok-sign' : 'icon-minus-sign'; 50 | zone.events = createEvents(zone.lastevents); 51 | zone.selected = ""; 52 | if (activeCollapse == "collapseZone" + i) { 53 | zone.selected = "in"; 54 | } 55 | zones.push(zone); 56 | } 57 | } 58 | 59 | var partitions = []; 60 | for (var i = 1; i < 65; i++) { 61 | var partition = obj.partition[i + '']; 62 | if (partition && partition.name) { 63 | partition.id = i; 64 | partition.class = partition.status.ready ? 'badge-success' : 'badge-important'; 65 | partition.icon = partition.status.ready ? 'icon-ok-sign' : 'icon-minus-sign'; 66 | partition.events = createEvents(partition.lastevents); 67 | partition.selected = ""; 68 | if (activeCollapse == "collapsePart" + i) { 69 | partition.selected = "in"; 70 | } 71 | partitions.push(partition); 72 | } 73 | } 74 | 75 | return template({zones: zones, 76 | zoneAllEvents: createEvents(obj.zone.lastevents), 77 | partitions: partitions, 78 | partitionAllEvents: createEvents(obj.partition.lastevents), 79 | zoneAllSelected: activeCollapse == "collapseZoneAll" ? "in" : "", 80 | partitionAllSelected: activeCollapse == "collapsePartAll" ? "in" : "", 81 | }); 82 | } 83 | 84 | function actions(obj) { 85 | var source = $("#actions-template").html(); 86 | var template = Handlebars.compile(source); 87 | 88 | return template({ 89 | arm: !obj.partition["1"].status.armed && !obj.partition["1"].status.exit_delay, 90 | disarm: obj.partition["1"].status.armed, 91 | cancel: obj.partition["1"].status.exit_delay, 92 | pgm: obj.partition["1"].status.pgm_output 93 | }); 94 | } 95 | 96 | function disarm() { 97 | alertify.prompt("What is your code?", function (e, code) { 98 | if (e) { 99 | doAction("/api/alarm/disarm?alarmcode=" + code); 100 | } 101 | }); 102 | } 103 | 104 | function pgm() { 105 | alertify.prompt("Enter PGM # to trigger", function (e, pgmnum) { 106 | if (e) { 107 | alertify.prompt("What is your code?", function (e1, code) { 108 | if (e1) { 109 | doAction("/api/pgm?pgmnum=" + pgmnum + "&alarmcode=" + code); 110 | } 111 | }); 112 | } 113 | }); 114 | } 115 | 116 | function armwithcode() { 117 | alertify.prompt("What is your code?", function (e, code) { 118 | if (e) { 119 | console.log(code); 120 | doAction("/api/alarm/armwithcode?alarmcode=" + code); 121 | } else { 122 | alertify.error("not armed") 123 | } 124 | }); 125 | } 126 | 127 | function doAction(action) { 128 | console.log(action); 129 | $.ajax({ 130 | type: "GET", 131 | url: action, 132 | contentType: "application/json; charset=utf-8", 133 | dataType: "json", 134 | success: function (res) { 135 | console.log(res.response); 136 | alertify.success(res.response); 137 | }, 138 | error: function () { 139 | alertify.error("error performing action"); 140 | } 141 | }); 142 | } 143 | 144 | function message(obj) { 145 | var str = ''; 146 | if (obj.partition["1"].status.entry_delay) { 147 | str += 'Entry delay'; 148 | } 149 | 150 | if (obj.partition["1"].status.exit_delay) { 151 | str += 'Exit delay'; 152 | } 153 | 154 | if (obj.partition["1"].status.alarm) { 155 | str += 'Alarm'; 156 | } 157 | 158 | if (obj.partition["1"].status.trouble) { 159 | str += 'Trouble'; 160 | } 161 | 162 | if (obj.partition["1"].status.tamper) { 163 | str += 'Tamper'; 164 | } 165 | 166 | if (obj.partition["1"].status.pgm_output) { 167 | str += 'PGM Output in progress'; 168 | } 169 | 170 | $('#message').html(str).fadeIn(); 171 | } 172 | 173 | function update(id, value, force) { 174 | var old = cache[id]; 175 | if (old != value || force) { 176 | $(id).html(value).fadeIn(); 177 | cache[id] = value; 178 | return true; 179 | } 180 | return false; 181 | } 182 | 183 | function refresh(force) { 184 | $.ajax({ 185 | type: "GET", 186 | url: "/api", 187 | contentType: "application/json; charset=utf-8", 188 | dataType: "json", 189 | data: "{}", 190 | success: function (res) { 191 | if (matchMediaPhone()) { 192 | update('#mobile-details', details(res, "#mobile-template"), force); 193 | } else { 194 | if (update('#details', details(res, "#template")), force) { 195 | if (activeTab) { 196 | $('#tabs a[href="' + activeTab + '"]').tab('show'); 197 | } 198 | } 199 | } 200 | update('#actions', actions(res), force); 201 | $('#tabs').tab(); 202 | 203 | $('#tabs a[data-toggle="tab"]').on('shown', function (e) { 204 | activeTab = e.target.hash; 205 | }); 206 | $('.accordion-body').on('show',function () { 207 | activeCollapse = this.id; 208 | }).on('hide', function () { 209 | activeCollapse = null; 210 | }); 211 | 212 | if (autorefresh) { 213 | $("#autorefresh").addClass('active'); 214 | } 215 | 216 | if (timeago) { 217 | $("#timeago").addClass('active'); 218 | } 219 | 220 | message(res); 221 | } 222 | }); 223 | } 224 | 225 | $(document).ready(function () { 226 | refresh(); 227 | FastClick.attach(document.body); 228 | }); 229 | 230 | 231 | setInterval(function () { 232 | if (autorefresh) { 233 | refresh(false); 234 | } 235 | }, 5000); 236 | -------------------------------------------------------------------------------- /ext/alertify.min.js: -------------------------------------------------------------------------------- 1 | /*! alertify - v0.3.11 - 2013-10-08 */ 2 | !function(a,b){"use strict";var c,d=a.document;c=function(){var c,e,f,g,h,i,j,k,l,m,n,o,p,q={},r={},s=!1,t={ENTER:13,ESC:27,SPACE:32},u=[];return r={buttons:{holder:'',submit:'',ok:'',cancel:''},input:'
',message:'

{{message}}

',log:'
{{message}}
'},p=function(){var a,c,e=!1,f=d.createElement("fakeelement"),g={WebkitTransition:"webkitTransitionEnd",MozTransition:"transitionend",OTransition:"otransitionend",transition:"transitionend"};for(a in g)if(f.style[a]!==b){c=g[a],e=!0;break}return{type:c,supported:e}},c=function(a){return d.getElementById(a)},q={labels:{ok:"OK",cancel:"Cancel"},delay:5e3,buttonReverse:!1,buttonFocus:"ok",transition:b,addListeners:function(a){var b,c,i,j,k,l="undefined"!=typeof f,m="undefined"!=typeof e,n="undefined"!=typeof o,p="",q=this;b=function(b){return"undefined"!=typeof b.preventDefault&&b.preventDefault(),i(b),"undefined"!=typeof o&&(p=o.value),"function"==typeof a&&("undefined"!=typeof o?a(!0,p):a(!0)),!1},c=function(b){return"undefined"!=typeof b.preventDefault&&b.preventDefault(),i(b),"function"==typeof a&&a(!1),!1},i=function(){q.hide(),q.unbind(d.body,"keyup",j),q.unbind(g,"focus",k),l&&q.unbind(f,"click",b),m&&q.unbind(e,"click",c)},j=function(a){var d=a.keyCode;(d===t.SPACE&&!n||n&&d===t.ENTER)&&b(a),d===t.ESC&&m&&c(a)},k=function(){n?o.focus():!m||q.buttonReverse?f.focus():e.focus()},this.bind(g,"focus",k),this.bind(h,"focus",k),l&&this.bind(f,"click",b),m&&this.bind(e,"click",c),this.bind(d.body,"keyup",j),this.transition.supported||this.setFocus()},bind:function(a,b,c){"function"==typeof a.addEventListener?a.addEventListener(b,c,!1):a.attachEvent&&a.attachEvent("on"+b,c)},handleErrors:function(){if("undefined"!=typeof a.onerror){var b=this;return a.onerror=function(a,c,d){b.error("["+a+" on line "+d+" of "+c+"]",0)},!0}return!1},appendButtons:function(a,b){return this.buttonReverse?b+a:a+b},build:function(a){var b="",c=a.type,d=a.message,e=a.cssClass||"";switch(b+='
',b+='Reset Focus',"none"===q.buttonFocus&&(b+=''),"prompt"===c&&(b+='
'),b+='
',b+=r.message.replace("{{message}}",d),"prompt"===c&&(b+=r.input),b+=r.buttons.holder,b+="
","prompt"===c&&(b+="
"),b+='Reset Focus',b+="
",c){case"confirm":b=b.replace("{{buttons}}",this.appendButtons(r.buttons.cancel,r.buttons.ok)),b=b.replace("{{ok}}",this.labels.ok).replace("{{cancel}}",this.labels.cancel);break;case"prompt":b=b.replace("{{buttons}}",this.appendButtons(r.buttons.cancel,r.buttons.submit)),b=b.replace("{{ok}}",this.labels.ok).replace("{{cancel}}",this.labels.cancel);break;case"alert":b=b.replace("{{buttons}}",r.buttons.ok),b=b.replace("{{ok}}",this.labels.ok)}return l.className="alertify alertify-"+c+" "+e,k.className="alertify-cover",b},close:function(a,b){var c,d,e=b&&!isNaN(b)?+b:this.delay,f=this;this.bind(a,"click",function(){c(a)}),d=function(a){a.stopPropagation(),f.unbind(this,f.transition.type,d),m.removeChild(this),m.hasChildNodes()||(m.className+=" alertify-logs-hidden")},c=function(a){"undefined"!=typeof a&&a.parentNode===m&&(f.transition.supported?(f.bind(a,f.transition.type,d),a.className+=" alertify-log-hide"):(m.removeChild(a),m.hasChildNodes()||(m.className+=" alertify-logs-hidden")))},0!==b&&setTimeout(function(){c(a)},e)},dialog:function(a,b,c,e,f){j=d.activeElement;var g=function(){m&&null!==m.scrollTop&&k&&null!==k.scrollTop||g()};if("string"!=typeof a)throw new Error("message must be a string");if("string"!=typeof b)throw new Error("type must be a string");if("undefined"!=typeof c&&"function"!=typeof c)throw new Error("fn must be a function");return this.init(),g(),u.push({type:b,message:a,callback:c,placeholder:e,cssClass:f}),s||this.setup(),this},extend:function(a){if("string"!=typeof a)throw new Error("extend method must have exactly one paramter");return function(b,c){return this.log(b,a,c),this}},hide:function(){var a,b=this;u.splice(0,1),u.length>0?this.setup(!0):(s=!1,a=function(c){c.stopPropagation(),b.unbind(l,b.transition.type,a)},this.transition.supported?(this.bind(l,this.transition.type,a),l.className="alertify alertify-hide alertify-hidden"):l.className="alertify alertify-hide alertify-hidden alertify-isHidden",k.className="alertify-cover alertify-cover-hidden",j.focus())},init:function(){d.createElement("nav"),d.createElement("article"),d.createElement("section"),null==c("alertify-cover")&&(k=d.createElement("div"),k.setAttribute("id","alertify-cover"),k.className="alertify-cover alertify-cover-hidden",d.body.appendChild(k)),null==c("alertify")&&(s=!1,u=[],l=d.createElement("section"),l.setAttribute("id","alertify"),l.className="alertify alertify-hidden",d.body.appendChild(l)),null==c("alertify-logs")&&(m=d.createElement("section"),m.setAttribute("id","alertify-logs"),m.className="alertify-logs alertify-logs-hidden",d.body.appendChild(m)),d.body.setAttribute("tabindex","0"),this.transition=p()},log:function(a,b,c){var d=function(){m&&null!==m.scrollTop||d()};return this.init(),d(),m.className="alertify-logs",this.notify(a,b,c),this},notify:function(a,b,c){var e=d.createElement("article");e.className="alertify-log"+("string"==typeof b&&""!==b?" alertify-log-"+b:""),e.innerHTML=a,m.appendChild(e),setTimeout(function(){e.className=e.className+" alertify-log-show"},50),this.close(e,c)},set:function(a){var b;if("object"!=typeof a&&a instanceof Array)throw new Error("args must be an object");for(b in a)a.hasOwnProperty(b)&&(this[b]=a[b])},setFocus:function(){o?(o.focus(),o.select()):i.focus()},setup:function(a){var d,j=u[0],k=this;s=!0,d=function(a){a.stopPropagation(),k.setFocus(),k.unbind(l,k.transition.type,d)},this.transition.supported&&!a&&this.bind(l,this.transition.type,d),l.innerHTML=this.build(j),g=c("alertify-resetFocus"),h=c("alertify-resetFocusBack"),f=c("alertify-ok")||b,e=c("alertify-cancel")||b,i="cancel"===q.buttonFocus?e:"none"===q.buttonFocus?c("alertify-noneFocus"):f,o=c("alertify-text")||b,n=c("alertify-form")||b,"string"==typeof j.placeholder&&""!==j.placeholder&&(o.value=j.placeholder),a&&this.setFocus(),this.addListeners(j.callback)},unbind:function(a,b,c){"function"==typeof a.removeEventListener?a.removeEventListener(b,c,!1):a.detachEvent&&a.detachEvent("on"+b,c)}},{alert:function(a,b,c){return q.dialog(a,"alert",b,"",c),this},confirm:function(a,b,c){return q.dialog(a,"confirm",b,"",c),this},extend:q.extend,init:q.init,log:function(a,b,c){return q.log(a,b,c),this},prompt:function(a,b,c,d){return q.dialog(a,"prompt",b,c,d),this},success:function(a,b){return q.log(a,"success",b),this},error:function(a,b){return q.log(a,"error",b),this},set:function(a){q.set(a)},labels:q.labels,debug:q.handleErrors}},"function"==typeof define?define([],function(){return new c}):"undefined"==typeof a.alertify&&(a.alertify=new c)}(this); -------------------------------------------------------------------------------- /ext/fastclick.min.js: -------------------------------------------------------------------------------- 1 | /* 2 | FastClick: polyfill to remove click delays on browsers with touch UIs. 3 | 4 | @version 0.6.8 5 | @codingstandard ftlabs-jsv2 6 | @copyright The Financial Times Limited [All Rights Reserved] 7 | @license MIT License (see LICENSE.txt) 8 | */ 9 | function FastClick(a){var b,c=this;this.trackingClick=!1;this.trackingClickStart=0;this.targetElement=null;this.lastTouchIdentifier=this.touchStartY=this.touchStartX=0;this.touchBoundary=10;this.layer=a;if(!a||!a.nodeType)throw new TypeError("Layer must be a document node");this.onClick=function(){return FastClick.prototype.onClick.apply(c,arguments)};this.onMouse=function(){return FastClick.prototype.onMouse.apply(c,arguments)};this.onTouchStart=function(){return FastClick.prototype.onTouchStart.apply(c, 10 | arguments)};this.onTouchEnd=function(){return FastClick.prototype.onTouchEnd.apply(c,arguments)};this.onTouchCancel=function(){return FastClick.prototype.onTouchCancel.apply(c,arguments)};FastClick.notNeeded(a)||(this.deviceIsAndroid&&(a.addEventListener("mouseover",this.onMouse,!0),a.addEventListener("mousedown",this.onMouse,!0),a.addEventListener("mouseup",this.onMouse,!0)),a.addEventListener("click",this.onClick,!0),a.addEventListener("touchstart",this.onTouchStart,!1),a.addEventListener("touchend", 11 | this.onTouchEnd,!1),a.addEventListener("touchcancel",this.onTouchCancel,!1),Event.prototype.stopImmediatePropagation||(a.removeEventListener=function(b,c,e){var f=Node.prototype.removeEventListener;"click"===b?f.call(a,b,c.hijacked||c,e):f.call(a,b,c,e)},a.addEventListener=function(b,c,e){var f=Node.prototype.addEventListener;"click"===b?f.call(a,b,c.hijacked||(c.hijacked=function(a){a.propagationStopped||c(a)}),e):f.call(a,b,c,e)}),"function"===typeof a.onclick&&(b=a.onclick,a.addEventListener("click", 12 | function(a){b(a)},!1),a.onclick=null))}FastClick.prototype.deviceIsAndroid=0c.offsetHeight){b=c;a.fastClickScrollParent=c;break}c=c.parentElement}while(c)}b&&(b.fastClickLastScrollTop=b.scrollTop)};FastClick.prototype.getTargetElementFromEventTarget=function(a){return a.nodeType===Node.TEXT_NODE?a.parentNode:a}; 17 | FastClick.prototype.onTouchStart=function(a){var b,c,d;if(1a.timeStamp-this.lastClickTime&&a.preventDefault();return!0};FastClick.prototype.touchHasMoved=function(a){a=a.changedTouches[0];var b=this.touchBoundary;return Math.abs(a.pageX-this.touchStartX)>b||Math.abs(a.pageY-this.touchStartY)>b?!0:!1};FastClick.prototype.findControl=function(a){return void 0!==a.control?a.control:a.htmlFor?document.getElementById(a.htmlFor):a.querySelector("button, input:not([type=hidden]), keygen, meter, output, progress, select, textarea")}; 19 | FastClick.prototype.onTouchEnd=function(a){var b,c,d=this.targetElement;this.touchHasMoved(a)&&(this.trackingClick=!1,this.targetElement=null);if(!this.trackingClick)return!0;if(200>a.timeStamp-this.lastClickTime)return this.cancelNextClick=!0;this.lastClickTime=a.timeStamp;b=this.trackingClickStart;this.trackingClick=!1;this.trackingClickStart=0;this.deviceIsIOSWithBadTarget&&(c=a.changedTouches[0],d=document.elementFromPoint(c.pageX-window.pageXOffset,c.pageY-window.pageYOffset)||d,d.fastClickScrollParent= 20 | this.targetElement.fastClickScrollParent);c=d.tagName.toLowerCase();if("label"===c){if(b=this.findControl(d)){this.focus(d);if(this.deviceIsAndroid)return!1;d=b}}else if(this.needsFocus(d)){if(100 2 | 3 | 4 | 5 | 6 | Alarm Server 7 | 8 | 9 | 10 | 11 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 88 | 89 | 90 | 115 | 116 | 161 | 162 | 222 | 223 | 243 | 244 |
245 | 246 |
247 |
248 |
249 |
250 |
251 |
252 |
253 |
254 |
255 |
256 |
257 | 258 |
259 |
260 | 261 |
262 | 263 | 264 | -------------------------------------------------------------------------------- /Envisalink.py: -------------------------------------------------------------------------------- 1 | import asyncore, asynchat 2 | import socket 3 | import logging 4 | import time 5 | import datetime 6 | 7 | from envisalinkdefs import evl_ResponseTypes 8 | from envisalinkdefs import evl_Defaults 9 | from envisalinkdefs import evl_ArmModes 10 | 11 | ALARMSTATE={'version' : 0.2} 12 | 13 | def dict_merge(a, b): 14 | c = a.copy() 15 | c.update(b) 16 | return c 17 | 18 | def getMessageType(code): 19 | return evl_ResponseTypes[code] 20 | 21 | def to_chars(string): 22 | chars = [] 23 | for char in string: 24 | chars.append(ord(char)) 25 | return chars 26 | 27 | def get_checksum(code, data): 28 | return ("%02X" % sum(to_chars(code)+to_chars(data)))[-2:] 29 | 30 | class Client(asynchat.async_chat): 31 | def __init__(self, config, proxyclients): 32 | 33 | self.logger = logging.getLogger('alarmserver.EnvisalinkClient') 34 | 35 | self.logger.debug('Staring Envisalink Client') 36 | # Call parent class's __init__ method 37 | asynchat.async_chat.__init__(self) 38 | 39 | # save dict reference to connected clients 40 | self._proxyclients = proxyclients 41 | 42 | # alarm sate 43 | self._alarmstate = ALARMSTATE 44 | 45 | # Define some private instance variables 46 | self._buffer = [] 47 | 48 | # Are we logged in? 49 | self._loggedin = False 50 | 51 | # Set our terminator to \n 52 | self.set_terminator("\r\n") 53 | 54 | # Set config 55 | self._config = config 56 | 57 | # Reconnect delay 58 | self._retrydelay = 10 59 | 60 | self.do_connect() 61 | 62 | def do_connect(self, reconnect = False): 63 | # Create the socket and connect to the server 64 | if reconnect == True: 65 | self.logger.warning('Connection failed, retrying in '+str(self._retrydelay)+ ' seconds') 66 | for i in range(0, self._retrydelay): 67 | time.sleep(1) 68 | 69 | self.create_socket(socket.AF_INET, socket.SOCK_STREAM) 70 | 71 | self.logger.debug('Connecting to {}:{}'.format(self._config.ENVISALINKHOST, self._config.ENVISALINKPORT)) 72 | self.connect((self._config.ENVISALINKHOST, self._config.ENVISALINKPORT)) 73 | 74 | def collect_incoming_data(self, data): 75 | # Append incoming data to the buffer 76 | self._buffer.append(data) 77 | 78 | def found_terminator(self): 79 | line = "".join(self._buffer) 80 | self.handle_line(line) 81 | self._buffer = [] 82 | 83 | def handle_connect(self): 84 | self.logger.info("Connected to %s:%i" % (self._config.ENVISALINKHOST, self._config.ENVISALINKPORT)) 85 | pass 86 | 87 | def handle_close(self): 88 | self._loggedin = False 89 | self.close() 90 | self.logger.info("Disconnected from %s:%i" % (self._config.ENVISALINKHOST, self._config.ENVISALINKPORT)) 91 | self.do_connect(True) 92 | 93 | def handle_error(self): 94 | self._loggedin = False 95 | self.close() 96 | self.logger.error("Disconnected from %s:%i" % (self._config.ENVISALINKHOST, self._config.ENVISALINKPORT)) 97 | self.do_connect(True) 98 | 99 | def send_command(self, code, data, checksum = True): 100 | if checksum == True: 101 | to_send = code+data+get_checksum(code,data)+'\r\n' 102 | else: 103 | to_send = code+data+'\r\n' 104 | 105 | self.logger.debug('TX > '+to_send[:-1]) 106 | self.push(to_send) 107 | 108 | def handle_line(self, input): 109 | if input != '': 110 | for client in self._proxyclients: 111 | self._proxyclients[client].send_command(input, False) 112 | 113 | code=int(input[:3]) 114 | parameters=input[3:][:-2] 115 | event = getMessageType(int(code)) 116 | message = self.format_event(event, parameters) 117 | self.logger.debug('RX < ' +str(code)+' - '+message) 118 | 119 | try: 120 | handler = "handle_%s" % evl_ResponseTypes[code]['handler'] 121 | except KeyError: 122 | #call general event handler 123 | self.handle_event(code, parameters, event, message) 124 | return 125 | 126 | try: 127 | func = getattr(self, handler) 128 | except AttributeError: 129 | raise CodeError("Handler function doesn't exist") 130 | 131 | func(code, parameters, event, message) 132 | 133 | def format_event(self, event, parameters): 134 | if 'type' in event: 135 | if event['type'] in ('partition', 'zone'): 136 | if event['type'] == 'partition': 137 | # If parameters includes extra digits then this next line would fail 138 | # without looking at just the first digit which is the partition number 139 | if int(parameters[0]) in self._config.PARTITIONNAMES: 140 | # After partition number can be either a usercode 141 | # or for event 652 a type of arm mode (single digit) 142 | # Usercode is always 4 digits padded with zeros 143 | if len(str(parameters)) == 5: 144 | # We have a usercode 145 | try: 146 | usercode = int(parameters[1:5]) 147 | except: 148 | usercode = 0 149 | if int(usercode) in self._config.ALARMUSERNAMES: 150 | alarmusername = self._config.ALARMUSERNAMES[int(usercode)] 151 | else: 152 | # Didn't find a username, use the code instead 153 | alarmusername = usercode 154 | return event['name'].format(str(self._config.PARTITIONNAMES[int(parameters[0])]), str(alarmusername)) 155 | elif len(parameters) == 2: 156 | # We have an arm mode instead, get it's friendly name 157 | armmode = evl_ArmModes[int(parameters[1])] 158 | return event['name'].format(str(self._config.PARTITIONNAMES[int(parameters[0])]), str(armmode)) 159 | else: 160 | return event['name'].format(str(self._config.PARTITIONNAMES[int(parameters)])) 161 | elif event['type'] == 'zone': 162 | if int(parameters) in self._config.ZONENAMES: 163 | if self._config.ZONENAMES[int(parameters)]!=False: 164 | return event['name'].format(str(self._config.ZONENAMES[int(parameters)])) 165 | 166 | return event['name'].format(str(parameters)) 167 | 168 | #envisalink event handlers, some events are unhandeled. 169 | def handle_login(self, code, parameters, event, message): 170 | if parameters == '3': 171 | self._loggedin = True 172 | self.send_command('005', self._config.ENVISALINKPASS) 173 | if parameters == '1': 174 | self.send_command('001', '') 175 | if parameters == '0': 176 | self.logger.warning('Incorrect envisalink password') 177 | sys.exit(0) 178 | 179 | def handle_event(self, code, parameters, event, message): 180 | # only handle events with a 'type' defined 181 | if not 'type' in event: 182 | return 183 | 184 | if not event['type'] in self._alarmstate: 185 | self._alarmstate[event['type']]={'lastevents' : []} 186 | 187 | # save event in alarm state depending on 188 | # the type of event 189 | 190 | parameters = int(parameters) 191 | 192 | # if zone event 193 | if event['type'] == 'zone': 194 | zone = parameters 195 | # if the zone is named in the config file save info in self._alarmstate 196 | if zone in self._config.ZONENAMES: 197 | # save zone if not already there 198 | if not zone in self._alarmstate['zone']: 199 | self._alarmstate['zone'][zone] = {'name' : self._config.ZONENAMES[zone]} 200 | else: 201 | self.logger.debug('Ignoring unnamed zone {}'.format(zone)) 202 | 203 | # if partition event 204 | elif event['type'] == 'partition': 205 | partition = parameters 206 | if partition in self._config.PARTITIONNAMES: 207 | # save partition name in alarmstate 208 | if not partition in self._alarmstate['partition']: 209 | self._alarmstate['partition'][partition] = {'name' : self._config.PARTITIONNAMES[partition]} 210 | else: 211 | self.logger.debug('Ignoring unnamed partition {}'.format(partition)) 212 | else: 213 | if not parameters in self._alarmstate[event['type']]: 214 | self._alarmstate[event['type']][partition] = {} 215 | 216 | # shorthand to event state 217 | eventstate = self._alarmstate[event['type']] 218 | 219 | # return if the parameters isn't in the alarm event state 220 | # i.e. if the current event type is in zone 1 (event[type]:zone, param:1) 221 | # then, if there isn't an alaramstate['zone'][2], return 222 | if not parameters in eventstate: 223 | return 224 | 225 | # populate status with defaults if there isn't already a status 226 | if not 'status' in eventstate[parameters]: 227 | eventstate[parameters]['status'] = evl_Defaults[event['type']] 228 | 229 | prev_state = eventstate[parameters]['status'] 230 | # save event status 231 | if 'status' in event: 232 | eventstate[parameters]['status']=dict_merge(eventstate[parameters]['status'], event['status']) 233 | 234 | # append event to lastevents, crete list if it doesn't exist 235 | if not 'lastevents' in eventstate[parameters]: 236 | eventstate[parameters]['lastevents'] = [] 237 | 238 | # if the state of the alarm (i.e., zone, partition, etc) remains 239 | # unchanged after event['status'] has been merged, return and 240 | # do not store in history 241 | if prev_state == eventstate[parameters]['status']: 242 | self.logger.debug('Discarded event. State not changed. ({} {})'.format(event['type'], parameters)) 243 | return 244 | 245 | # if lastevents is a list of non-zero length 246 | if eventstate[parameters]['lastevents']: 247 | # if this event is the same as previous discard it 248 | # except if lastevents is empty, then we get an IndexError exception 249 | if eventstate[parameters]['lastevents'][-1]['code'] == code: 250 | self.logger.debug('{}:{} ({}) discarded duplicate event'.format(event['type'], parameters, code)) 251 | return 252 | 253 | # append this event to lastevents 254 | eventstate[parameters]['lastevents'].append({ 255 | 'datetime' : str(datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")), 256 | 'code' : code, 257 | 'message' : message}) 258 | 259 | # manage last events list if it's > MAXEVENTS 260 | if len(eventstate[parameters]['lastevents']) > self._config.MAXEVENTS: 261 | eventstate[parameters]['lastevents'].pop(0) 262 | 263 | 264 | eventstate['lastevents'].append({'datetime' : str(datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")), 'message' : message}) 265 | if len(eventstate['lastevents']) > self._config.MAXALLEVENTS: 266 | eventstate['lastevents'].pop(0) 267 | 268 | 269 | def handle_zone(self, code, parameters, event, message): 270 | self.handle_event(code, parameters[1:], event, message) 271 | 272 | def handle_partition(self, code, parameters, event, message): 273 | self.handle_event(code, parameters[0], event, message) 274 | 275 | class Proxy(asyncore.dispatcher): 276 | def __init__(self, config, server): 277 | 278 | self.logger = logging.getLogger('alarmserver.Proxy') 279 | 280 | self._config = config 281 | if self._config.ENABLEPROXY == False: 282 | return 283 | 284 | asyncore.dispatcher.__init__(self) 285 | self.create_socket(socket.AF_INET, socket.SOCK_STREAM) 286 | self.set_reuse_addr() 287 | self.logger.info('Envisalink Proxy Started') 288 | 289 | self.bind(("", self._config.ENVISALINKPROXYPORT)) 290 | self.listen(5) 291 | 292 | def handle_accept(self): 293 | pair = self.accept() 294 | if pair is None: 295 | pass 296 | else: 297 | sock, addr = pair 298 | self.logger.info('Incoming proxy connection from %s' % repr(addr)) 299 | handler = ProxyChannel(server, self._config.ENVISALINKPROXYPASS, sock, addr) 300 | 301 | -------------------------------------------------------------------------------- /envisalinkdefs.py: -------------------------------------------------------------------------------- 1 | ## Alarm Server 2 | ## Supporting Envisalink 2DS/3 3 | ## Written by donnyk+envisalink@gmail.com 4 | ## 5 | ## This code is under the terms of the GPL v3 license. 6 | 7 | evl_Defaults = { 8 | 'zone' : {'open' : False, 'fault' : False, 'alarm' : False, 'tamper' : False}, 9 | 'partition' : {'ready' : False, 'trouble' : False, 'exit_delay' : False, 'entry_delay' : False, 'armed' : False, 'armed_bypass' : False, 'alarm' : False, 'tamper' : False, 'chime' : False, 'trouble_led' : False}, 10 | 'system' : {'fire_key_alarm' : False, 'aux_key_alarm' : False, 'panic_key_alarm' : False, '2wire_alarm' : False, 'battery_trouble' : False, 'ac_trouble' : False, 'system_bell_trouble' : False, 'system_tamper' : False, 'fire_trouble' : False} 11 | } 12 | 13 | evl_ArmModes = { 14 | 0 : 'Away', 15 | 1 : 'Stay', 16 | 2 : 'Zero Entry Away', 17 | 3 : 'Zero Entry Stay' 18 | } 19 | 20 | evl_ResponseTypes = { 21 | 500 : {'name' : 'Command Acknowledge', 'description' : 'A command has been received successfully.'}, 22 | 501 : {'name' : 'Command Error', 'description' : 'A command has been received with a bad checksum.'}, 23 | 502 : {'name' : 'System Error {0}', 'description' : 'An error has been detected.'}, 24 | 505 : {'name' : 'Login Interaction', 'description' : 'Sent During Session Login Only.', 'handler' : 'login'}, 25 | 510 : {'name' : 'Keypad Led State - Partition 1', 'description' : 'Outputted when the TPI has deceted a change of state in the Partition 1 keypad LEDs.'}, 26 | 511 : {'name' : 'Keypad Led Flash State - Partition 1', 'description' : 'Outputed when the TPI has detected a change of state in the Partition 1 keypad LEDs as to whether to flash or not. Overrides 510. That is, if 511 says the PROGRAM LED is flashing, then it doesn''t matter what 510 says.'}, 27 | 550 : {'name' : 'Time/Date Broadcast', 'description' : 'Outputs the current security system time.'}, 28 | 560 : {'name' : 'Ring Detected', 'description' : 'The Panel has detected a ring on the telephone line. Note: This command will only be issued if an ESCORT 5580xx module is present.'}, 29 | 561 : {'name' : 'Indoor Temperature Broadcast', 'description' : 'If an ESCORT 5580TC is installed, and at least one ENERSTAT thermostat, this command displays the interior temperature and the thermostat number.'}, 30 | 562 : {'name' : 'Outdoor Temperature Broadcast', 'description' : 'If an ESCORT 5580TC is installed, and at least one ENERSTAT thermostat, this command displays the exterior temperature and the thermostat number.'}, 31 | 601 : {'type' : 'zone', 'name' : 'Partition {0[0]} Zone {0[1]}{0[2]}{0[3]} Alarm', 'description' : 'A zone has gone into alarm.', 'handler' : 'zone', 'status' : {'alarm' : True}}, 32 | 602 : {'type' : 'zone', 'name' : 'Partition {0[0]} Zone {0[1]}{0[2]}{0[3]} Alarm Restore', 'description' : 'A zone alarm has been restored.', 'handler' : 'zone', 'status' : {'alarm' : False}}, 33 | 603 : {'type' : 'zone', 'name' : 'Partition {0[0]} Zone {0[1]}{0[2]}{0[3]} Tamper', 'description' : 'A zone has a tamper condition.', 'handler' : 'zone', 'status' : {'tamper' : True}}, 34 | 604 : {'type' : 'zone', 'name' : 'Partition {0[0]} Zone {0[1]}{0[2]}{0[3]} Tamper Restore', 'description' : 'A zone tamper condition has been restored.', 'handler' : 'zone', 'status' : {'tamper' : False}}, 35 | 605 : {'type' : 'zone', 'name' : 'Zone {0} Fault', 'description' : 'A zone has a fault condition.', 'status' : {'fault' : True}}, 36 | 606 : {'type' : 'zone', 'name' : 'Zone {0} Fault Restore', 'description' : 'A zone fault condition has been restored.', 'status' : {'fault' : False}}, 37 | 609 : {'type' : 'zone', 'name' : 'Zone {0} Open', 'description' : 'General status of the zone.', 'status' : {'open' : True}}, 38 | 610 : {'type' : 'zone', 'name' : 'Zone {0} Restored', 'description' : 'General status of the zone.', 'status' : {'open' : False}}, 39 | 615 : {'name' : 'Envisalink Zone Timer Dump', 'description' : 'This command contains the raw zone timers used inside the Envisalink. The dump is a 256 character packed HEX string representing 64 UINT16 (little endian) zone timers. Zone timers count down from 0xFFFF (zone is open) to 0x0000 (zone is closed too long ago to remember). Each ''tick'' of the zone time is actually 5 seconds so a zone timer of 0xFFFE means ''5 seconds ago''. Remember, the zone timers are LITTLE ENDIAN so the above example would be transmitted as FEFF.'}, 40 | 620 : {'name' : 'Duress Alarm', 'description' : 'A duress code has been entered on a system keypad.'}, 41 | 621 : {'type' : 'system', 'name' : '[F] Key Alarm', 'description' : 'A Fire key alarm has been detected.', 'status' : {'fire_key_alarm' : True}}, 42 | 622 : {'type' : 'system', 'name' : '[F] Key Alarm', 'description' : 'A Fire key alarm has been restored (sent automatically).', 'status' : {'fire_key_alarm' : False}}, 43 | 623 : {'type' : 'system', 'name' : '[A] Key Alarm', 'description' : 'A Auxillary key alarm has been detected.', 'status' : {'aux_key_alarm' : True}}, 44 | 624 : {'type' : 'system', 'name' : '[A] Key Alarm', 'description' : 'A Auxillary key alarm has been restored (sent automatically).', 'status' : {'aux_key_alarm' : False}}, 45 | 625 : {'type' : 'system', 'name' : '[P] Key Alarm', 'description' : 'A Panic key alarm has been detected.', 'status' : {'panic_key_alarm' : True}}, 46 | 626 : {'type' : 'system', 'name' : '[P] Key Alarm', 'description' : 'A Panic key alarm has been restored (sent automatically).', 'status' : {'panic_key_alarm' : False}}, 47 | 631 : {'type' : 'system', 'name' : '2-Wire Smoke/Aux Alarm', 'description' : 'A 2-wire smoke/Auxiliary alarm has been activated.', 'status' : {'2wire_alarm' : True}}, 48 | 632 : {'type' : 'system', 'name' : '2-Wire Smoke/Aux Restore', 'description' : 'A 2-wire smoke/Auxiliary alarm has been restored.', 'status' : {'2wire_alarm' : False}}, 49 | 650 : {'type' : 'partition', 'name' : 'Partition {0} Ready', 'description' : 'Partition can now be armed (all zones restored, no troubles, etc). Also issued at the end of Bell Timeout if the partition was READY when an alarm occurred.', 'status' : {'ready' : True, 'pgm_output' : False}}, 50 | 651 : {'type' : 'partition', 'name' : 'Partition {0} Not Ready', 'description' : 'Partition cannot be armed (zones open, trouble present, etc).', 'status' : {'ready' : False}}, 51 | 652 : {'type' : 'partition', 'name' : 'Partition {0} Armed Mode {1}', 'description' : 'Partition has been armed - sent at the end of exit delay Also sent after an alarm if the Bell Cutoff Timer expires Mode is appended to indicate whether the partition is armed AWAY, STAY, ZERO-ENTRY-AWAY, or ZERO-ENTRY-STAY.', 'handler' : 'partition', 'status' : {'armed' : True, 'exit_delay' : False}}, 52 | 653 : {'type' : 'partition', 'name' : 'Partition {0} Ready - Force Arming Enabled', 'description' : 'Partition can now be armed (all zones restored, no troubles, etc). Also issued at the end of Bell Timeout if the partition was READY when an alarm occurred.', 'status' : {'ready' : True}}, 53 | 654 : {'type' : 'partition', 'name' : 'Partition {0} In Alarm', 'description' : 'A partition is in alarm.', 'status' : {'alarm' : True}}, 54 | 655 : {'type' : 'partition', 'name' : 'Partition {0} Disarmed', 'description' : 'A partition has been disarmed.', 'status' : {'alarm' : False, 'armed' : False, 'exit_delay' : False, 'entry_delay' : False}}, 55 | 656 : {'type' : 'partition', 'name' : 'Partition {0} Exit Delay in Progress', 'description' : 'A partition is in Exit Delay.', 'status' : {'exit_delay' : True}}, 56 | 657 : {'type' : 'partition', 'name' : 'Partition {0} Entry Delay in Progress', 'description' : 'A partition is in Entry Delay.', 'status' : {'entry_delay' : True}}, 57 | 658 : {'type' : 'partition', 'name' : 'Partition {0} Keypad Lock-out', 'description' : 'A partition is in Keypad Lockout due to too many failed user code attempts.'}, 58 | 659 : {'type' : 'partition', 'name' : 'Partition {0} Failed to Arm', 'description' : 'An attempt to arm the partition has failed.'}, 59 | 660 : {'type' : 'partition', 'name' : 'Partition {0} PGM Output is in Progress', 'description' : '*71, *72, *73, or *74 has been pressed.', 'status': {'pgm_output' : True}}, 60 | 663 : {'type' : 'partition', 'name' : 'Partition {0} Chime Enabled', 'description' : 'The door chime feature has been enabled.', 'status' : {'chime' : True}}, 61 | 664 : {'type' : 'partition', 'name' : 'Partition {0} Chime Disabled', 'description' : 'The door chime feature has been disabled.', 'status' : {'chime' : False}}, 62 | 670 : {'type' : 'partition', 'name' : 'Partition {0} Invalid Access Code', 'description' : 'Invalid Access Code.'}, 63 | 671 : {'type' : 'partition', 'name' : 'Partition {0} Function Not Available', 'description' : 'A partition is in Entry delay.'}, 64 | 672 : {'type' : 'partition', 'name' : 'Partition {0} Failure to Arm', 'description' : 'An attempt was made to arm the partition and it failed.'}, 65 | 673 : {'type' : 'partition', 'name' : 'Partition {0} is Busy', 'description' : 'The partition is busy (another keypad is programming or an installer is programming).'}, 66 | 674 : {'type' : 'partition', 'name' : 'Partition {0} System Arming in Progress', 'description' : 'This system is auto-arming and is in arm warning delay.'}, 67 | 680 : {'name' : 'System in installers mode', 'description' : 'System has entered installers mode'}, 68 | 700 : {'type' : 'partition', 'name' : 'Partition {0} User {1} Closing', 'description' : 'A partition has been armed by a user - sent at the end of exit delay.', 'handler' : 'partition', 'status' : {'armed' : True, 'exit_delay' : False}}, 69 | 701 : {'type' : 'partition', 'name' : 'Partition {0} Special Closing', 'description' : 'A partition has been armed by one of the following methods: Quick Arm, Auto Arm, Keyswitch, DLS software, Wireless Key.', 'status' : {'armed' : True, 'exit_delay' : False}}, 70 | 702 : {'type' : 'partition', 'name' : 'Partition {0} Partial Closing', 'description' : 'A partition has been armed but one or more zones have been bypassed.', 'status' : {'armed' : True, 'exit_delay' : False}}, 71 | 750 : {'type' : 'partition', 'name' : 'Partition {0} User {1} Opening', 'description' : 'A partition has been disarmed by a user.', 'handler' : 'partition', 'status' : {'armed' : False, 'entry_delay' : False}}, 72 | 751 : {'type' : 'partition', 'name' : 'Partition {0} Special Opening', 'description' : 'A partition has been disarmed by one of the following methods: Keyswitch, DLS software, Wireless Key.', 'status' : {'armed' : False, 'entry_delay' : False}}, 73 | 800 : {'type' : 'system', 'name' : 'Panel Battery Trouble', 'description' : 'The panel has a low battery.', 'status' : {'battery_trouble' : True}}, 74 | 801 : {'type' : 'system', 'name' : 'Panel Battery Trouble Restore', 'description' : 'The panel''s low battery has been restored.', 'status' : {'battery_trouble' : False}}, 75 | 802 : {'type' : 'system', 'name' : 'Panel AC Trouble', 'description' : 'AC power to the panel has been removed.', 'status' : {'ac_trouble' : True}}, 76 | 803 : {'type' : 'system', 'name' : 'Panel AC Restore', 'description' : 'AC power to the panel has been restored.', 'status' : {'ac_trouble' : False}}, 77 | 806 : {'type' : 'system', 'name' : 'System Bell Trouble', 'description' : 'An open circuit has been detected across the bell terminals.', 'status' : {'system_bell_trouble' : True}}, 78 | 807 : {'type' : 'system', 'name' : 'System Bell Trouble Restoral', 'description' : 'The bell trouble has been restored.', 'status' : {'system_bell_trouble' : False}}, 79 | 814 : {'name' : 'FTC Trouble', 'description' : 'The panel has failed to communicate successfully to the monitoring.'}, 80 | 816 : {'name' : 'Buffer Near Full', 'description' : 'Sent when the panel''s Event Buffer is 75% full from when it was last uploaded to DLS.'}, 81 | 829 : {'type' : 'system', 'name' : 'General System Tamper', 'description' : 'A tamper has occurred with one of the following modules: Zone Expander, PC5132, PC5204, PC5208, PC5400, PC59XX, LINKS 2X50, PC5108L, PC5100, PC5200.', 'status' : {'system_tamper' : True}}, 82 | 830 : {'type' : 'system', 'name' : 'General System Tamper Restore', 'description' : 'A general system Tamper has been restored.', 'status' : {'system_tamper' : False}}, 83 | 840 : {'type' : 'partition', 'name' : 'Partition {0} Trouble LED ON', 'description' : 'This command shows the general trouble status that the trouble LED on a keypad normally shows. When ON, it means there is a trouble on this partition. This command when the LED transitions from OFF, to ON.', 'status' : {'trouble' : True}}, 84 | 841 : {'type' : 'partition', 'name' : 'Partition {0} Trouble LED OFF', 'description' : 'This command shows the general trouble status that the trouble LED on a keypad normally shows. When the LED is OFF, this usually means there are no troubles present on this partition but certain modes will blank this LED even in the presence of a partition trouble. This command when the LED transitions from ON, to OFF.', 'status' : {'trouble' : False}}, 85 | 842 : {'type' : 'system', 'name' : 'Fire Trouble Alarm', 'description' : 'Fire Trouble Alarm', 'status' : {'fire_trouble' : True}}, 86 | 843 : {'type' : 'system', 'name' : 'Fire Trouble Alarm Restore', 'description' : 'Fire Trouble Alarm Restore', 'status' : {'fire_trouble' : False}}, 87 | 849 : {'name' : 'Verbose Trouble Status', 'description' : 'This command is issued when a trouble appears on the system and roughly every 5 minutes until the trouble is cleared. The two characters are a bitfield (similar to 510,511). The meaning of each bit is the same as what you see on an LED keypad (see the user manual).'}, 88 | 900 : {'name' : 'Code Required', 'description' : 'This command will tell the API to enter an access code. Once entered, the 200 command will be sent to perform the required action. The code should be entered within the window time of the panel.'}, 89 | 912 : {'name' : 'Command Output Pressed', 'description' : 'This command will tell the API to enter an access code. Once entered, the 200 command will be sent to perform the required action. The code should be entered within the window time of the panel.'}, 90 | 921 : {'name' : 'Master Code Required', 'description' : 'This command will tell the API to enter a master access code. Once entered, the 200 command will be sent to perform the required action. The code should be entered within the window time of the panel.'}, 91 | 922 : {'name' : 'Installers Code Required', 'description' : 'This command will tell the API to enter an installers access code. Once entered, the 200 command will be sent to perform the required action. The code should be entered within the window time of the panel.'}, 92 | } 93 | -------------------------------------------------------------------------------- /alarmserver.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | ## Alarm Server 3 | ## Supporting Envisalink 2DS/3 4 | ## Contributors: https://github.com/juggie/AlarmServer/graphs/contributors 5 | ## Compatibility: https://github.com/juggie/AlarmServer/wiki/Compatibility 6 | ## 7 | ## This code is under the terms of the GPL v3 license. 8 | 9 | 10 | import asyncore, asynchat 11 | import ConfigParser 12 | import datetime 13 | import os, socket, string, sys, httplib, urllib, urlparse, ssl 14 | import json 15 | import hashlib 16 | import time 17 | import getopt 18 | import logging 19 | 20 | logger = logging.getLogger('alarmserver') 21 | logger.setLevel(logging.DEBUG) 22 | 23 | # Console handler 24 | # Prints all messages (debug level) 25 | ch = logging.StreamHandler(); 26 | ch.setLevel(logging.DEBUG) 27 | # create formatter 28 | formatter = logging.Formatter( 29 | fmt='%(asctime)s %(name)s %(levelname)s: %(message)s', 30 | datefmt='%b %d %H:%M:%S') 31 | ch.setFormatter(formatter); 32 | # add handlers to logger 33 | logger.addHandler(ch) 34 | 35 | import HTTPChannel 36 | import Envisalink 37 | 38 | LOGTOFILE = False 39 | 40 | class CodeError(Exception): pass 41 | 42 | MAXPARTITIONS=16 43 | MAXZONES=128 44 | MAXALARMUSERS=47 45 | CONNECTEDCLIENTS={} 46 | 47 | def to_chars(string): 48 | chars = [] 49 | for char in string: 50 | chars.append(ord(char)) 51 | return chars 52 | 53 | def get_checksum(code, data): 54 | return ("%02X" % sum(to_chars(code)+to_chars(data)))[-2:] 55 | 56 | #currently supports pushover notifications, more to be added 57 | #including email, text, etc. 58 | #to be fixed! 59 | def send_notification(config, message): 60 | if config.PUSHOVER_ENABLE == True: 61 | conn = httplib.HTTPSConnection("api.pushover.net:443") 62 | conn.request("POST", "/1/messages.json", 63 | urllib.urlencode({ 64 | "token": "qo0nwMNdX56KJl0Avd4NHE2onO4Xff", 65 | "user": config.PUSHOVER_USERTOKEN, 66 | "message": str(message), 67 | }), { "Content-type": "application/x-www-form-urlencoded" }) 68 | 69 | class AlarmServerConfig(): 70 | def __init__(self, configfile): 71 | 72 | self._config = ConfigParser.ConfigParser() 73 | self._config.read(configfile) 74 | 75 | self.LOGURLREQUESTS = self.read_config_var('alarmserver', 'logurlrequests', True, 'bool') 76 | self.HTTPSPORT = self.read_config_var('alarmserver', 'httpsport', 8111, 'int') 77 | self.CERTFILE = self.read_config_var('alarmserver', 'certfile', 'server.crt', 'str') 78 | self.KEYFILE = self.read_config_var('alarmserver', 'keyfile', 'server.key', 'str') 79 | self.MAXEVENTS = self.read_config_var('alarmserver', 'maxevents', 10, 'int') 80 | self.MAXALLEVENTS = self.read_config_var('alarmserver', 'maxallevents', 100, 'int') 81 | self.ENVISALINKHOST = self.read_config_var('envisalink', 'host', 'envisalink', 'str') 82 | self.ENVISALINKPORT = self.read_config_var('envisalink', 'port', 4025, 'int') 83 | self.ENVISALINKPASS = self.read_config_var('envisalink', 'pass', 'user', 'str') 84 | self.ENABLEPROXY = self.read_config_var('envisalink', 'enableproxy', True, 'bool') 85 | self.ENVISALINKPROXYPORT = self.read_config_var('envisalink', 'proxyport', self.ENVISALINKPORT, 'int') 86 | self.ENVISALINKPROXYPASS = self.read_config_var('envisalink', 'proxypass', self.ENVISALINKPASS, 'str') 87 | self.PUSHOVER_ENABLE = self.read_config_var('pushover', 'enable', False, 'bool') 88 | self.PUSHOVER_USERTOKEN = self.read_config_var('pushover', 'enable', False, 'bool') 89 | self.ALARMCODE = self.read_config_var('envisalink', 'alarmcode', 1111, 'int') 90 | self.EVENTTIMEAGO = self.read_config_var('alarmserver', 'eventtimeago', True, 'bool') 91 | self.LOGFILE = self.read_config_var('alarmserver', 'logfile', '', 'str') 92 | global LOGTOFILE 93 | if self.LOGFILE == '': 94 | LOGTOFILE = False 95 | else: 96 | LOGTOFILE = True 97 | 98 | self.PARTITIONNAMES={} 99 | for i in range(1, MAXPARTITIONS+1): 100 | partition = self.read_config_var('alarmserver', 'partition'+str(i), False, 'str', True) 101 | if partition: self.PARTITIONNAMES[i] = partition 102 | 103 | self.ZONENAMES={} 104 | for i in range(1, MAXZONES+1): 105 | zone = self.read_config_var('alarmserver', 'zone'+str(i), False, 'str', True) 106 | if zone: self.ZONENAMES[i] = zone 107 | 108 | self.ALARMUSERNAMES={} 109 | for i in range(1, MAXALARMUSERS+1): 110 | user = self.read_config_var('alarmserver', 'user'+str(i), False, 'str', True) 111 | if user: self.ALARMUSERNAMES[i] = user 112 | 113 | if self.PUSHOVER_USERTOKEN == False and self.PUSHOVER_ENABLE == True: self.PUSHOVER_ENABLE = False 114 | 115 | def defaulting(self, section, variable, default, quiet = False): 116 | if quiet == False: 117 | print('Config option '+ str(variable) + ' not set in ['+str(section)+'] defaulting to: \''+str(default)+'\'') 118 | 119 | def read_config_var(self, section, variable, default, type = 'str', quiet = False): 120 | try: 121 | if type == 'str': 122 | return self._config.get(section,variable) 123 | elif type == 'bool': 124 | return self._config.getboolean(section,variable) 125 | elif type == 'int': 126 | return int(self._config.get(section,variable)) 127 | except (ConfigParser.NoSectionError, ConfigParser.NoOptionError): 128 | self.defaulting(section, variable, default, quiet) 129 | return default 130 | 131 | class AlarmServer(asyncore.dispatcher): 132 | def __init__(self, config): 133 | # Call parent class's __init__ method 134 | asyncore.dispatcher.__init__(self) 135 | 136 | # Create Envisalink client object 137 | self._envisalinkclient = Envisalink.Client(config, CONNECTEDCLIENTS) 138 | 139 | #Store config 140 | self._config = config 141 | 142 | logger.info('AlarmServer on HTTPS port {}'.format(config.HTTPSPORT)) 143 | 144 | # Create socket and listen on it 145 | self.create_socket(socket.AF_INET, socket.SOCK_STREAM) 146 | self.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) 147 | self.bind(("", config.HTTPSPORT)) 148 | self.listen(5) 149 | 150 | def handle_accept(self): 151 | # Accept the connection 152 | conn, addr = self.accept() 153 | if (config.LOGURLREQUESTS): 154 | logger.debug('Incoming web connection from %s' % repr(addr)) 155 | 156 | try: 157 | HTTPChannel.HTTPChannel(self, ssl.wrap_socket(conn, server_side=True, certfile=config.CERTFILE, keyfile=config.KEYFILE, ssl_version=ssl.PROTOCOL_TLSv1), addr) 158 | except ssl.SSLError: 159 | logger.warning('Failed https connection, attempted with http') 160 | return 161 | 162 | def handle_request(self, channel, method, request, header): 163 | if (config.LOGURLREQUESTS): 164 | logger.debug('Web request: '+str(method)+' '+str(request)) 165 | 166 | query = urlparse.urlparse(request) 167 | query_array = urlparse.parse_qs(query.query, True) 168 | 169 | if query.path == '/': 170 | channel.pushfile('index.html'); 171 | elif query.path == '/api': 172 | channel.pushok(json.dumps(self._envisalinkclient._alarmstate)) 173 | elif query.path == '/api/alarm/arm': 174 | channel.pushok(json.dumps({'response' : 'Request to arm received'})) 175 | self._envisalinkclient.send_command('030', '1') 176 | elif query.path == '/api/alarm/stayarm': 177 | channel.pushok(json.dumps({'response' : 'Request to arm in stay received'})) 178 | self._envisalinkclient.send_command('031', '1') 179 | elif query.path == '/api/alarm/armwithcode': 180 | channel.pushok(json.dumps({'response' : 'Request to arm with code received'})) 181 | self._envisalinkclient.send_command('033', '1' + str(query_array['alarmcode'][0])) 182 | elif query.path == '/api/pgm': 183 | channel.pushok(json.dumps({'response' : 'Request to trigger PGM'})) 184 | #self._envisalinkclient.send_command('020', '1' + str(query_array['pgmnum'][0])) 185 | self._envisalinkclient.send_command('071', '1' + "*7" + str(query_array['pgmnum'][0])) 186 | time.sleep(1) 187 | self._envisalinkclient.send_command('071', '1' + str(query_array['alarmcode'][0])) 188 | elif query.path == '/api/alarm/disarm': 189 | channel.pushok(json.dumps({'response' : 'Request to disarm received'})) 190 | if 'alarmcode' in query_array: 191 | self._envisalinkclient.send_command('040', '1' + str(query_array['alarmcode'][0])) 192 | else: 193 | self._envisalinkclient.send_command('040', '1' + str(self._config.ALARMCODE)) 194 | elif query.path == '/api/refresh': 195 | channel.pushok(json.dumps({'response' : 'Request to refresh data received'})) 196 | self._envisalinkclient.send_command('001', '') 197 | elif query.path == '/api/config/eventtimeago': 198 | channel.pushok(json.dumps({'eventtimeago' : str(self._config.EVENTTIMEAGO)})) 199 | elif query.path == '/img/glyphicons-halflings.png': 200 | channel.pushfile('glyphicons-halflings.png') 201 | elif query.path == '/img/glyphicons-halflings-white.png': 202 | channel.pushfile('glyphicons-halflings-white.png') 203 | elif query.path == '/favicon.ico': 204 | channel.pushfile('favicon.ico') 205 | else: 206 | if len(query.path.split('/')) == 2: 207 | try: 208 | with open(sys.path[0] + os.sep + 'ext' + os.sep + query.path.split('/')[1]) as f: 209 | f.close() 210 | channel.pushfile(query.path.split('/')[1]) 211 | except IOError as e: 212 | print "I/O error({0}): {1}".format(e.errno, e.strerror) 213 | channel.pushstatus(404, "Not found") 214 | channel.push("Content-type: text/html\r\n") 215 | channel.push("File not found") 216 | channel.push("\r\n") 217 | else: 218 | if (config.LOGURLREQUESTS): 219 | logger.warning("Invalid file requested") 220 | 221 | channel.pushstatus(404, "Not found") 222 | channel.push("Content-type: text/html\r\n") 223 | channel.push("\r\n") 224 | 225 | def handle_error(self): 226 | logger.exception('AlarmServer exception') 227 | 228 | class ProxyChannel(asynchat.async_chat): 229 | def __init__(self, server, proxypass, sock, addr): 230 | asynchat.async_chat.__init__(self, sock) 231 | self.server = server 232 | self.set_terminator("\r\n") 233 | self._buffer = [] 234 | self._server = server 235 | self._clientMD5 = hashlib.md5(str(addr)).hexdigest() 236 | self._straddr = str(addr) 237 | self._proxypass = proxypass 238 | self._authenticated = False 239 | 240 | self.send_command('5053') 241 | 242 | def collect_incoming_data(self, data): 243 | # Append incoming data to the buffer 244 | self._buffer.append(data) 245 | 246 | def found_terminator(self): 247 | line = "".join(self._buffer) 248 | self._buffer = [] 249 | self.handle_line(line) 250 | 251 | def handle_line(self, line): 252 | logger.info('PROXY REQ < '+line) 253 | if self._authenticated == True: 254 | self._server._envisalinkclient.send_command(line, '', False) 255 | else: 256 | self.send_command('500005') 257 | expectedstring = '005' + self._proxypass + get_checksum('005', self._proxypass) 258 | if line == ('005' + self._proxypass + get_checksum('005', self._proxypass)): 259 | logger.info('Proxy User Authenticated') 260 | CONNECTEDCLIENTS[self._straddr]=self 261 | self._authenticated = True 262 | self.send_command('5051') 263 | else: 264 | logger.warning('Proxy User Authentication failed') 265 | self.send_command('5050') 266 | self.close() 267 | 268 | def send_command(self, data, checksum = True): 269 | if checksum == True: 270 | to_send = data+get_checksum(data, '')+'\r\n' 271 | else: 272 | to_send = data+'\r\n' 273 | 274 | self.push(to_send) 275 | 276 | def handle_close(self): 277 | logger.info('Proxy connection from %s closed' % self._straddr) 278 | if self._straddr in CONNECTEDCLIENTS: del CONNECTEDCLIENTS[self._straddr] 279 | self.close() 280 | 281 | def handle_error(self): 282 | logger.exception('Proxy connection from %s errored' % self._straddr) 283 | if self._straddr in CONNECTEDCLIENTS: del CONNECTEDCLIENTS[self._straddr] 284 | self.close() 285 | 286 | def usage(): 287 | print 'Usage: '+sys.argv[0]+' -c ' 288 | 289 | def main(argv): 290 | try: 291 | opts, args = getopt.getopt(argv, "hc:", ["help", "config="]) 292 | except getopt.GetoptError: 293 | usage() 294 | sys.exit(2) 295 | for opt, arg in opts: 296 | if opt in ("-h", "--help"): 297 | usage() 298 | sys.exit() 299 | elif opt in ("-c", "--config"): 300 | global conffile 301 | conffile = arg 302 | 303 | 304 | if __name__=="__main__": 305 | conffile='alarmserver.cfg' 306 | main(sys.argv[1:]) 307 | logger.info('Using configuration file %s' % conffile) 308 | config = AlarmServerConfig(conffile) 309 | if LOGTOFILE: 310 | # File handler. Only print INFO and above 311 | fh = logging.FileHandler(config.LOGFILE) 312 | fh.setLevel(logging.INFO) 313 | fh.setFormatter(formatter) 314 | logger.addHandler(fh) 315 | 316 | logger.info('-'*30) 317 | logger.info('Alarm Server Starting') 318 | 319 | server = AlarmServer(config) 320 | proxy = Envisalink.Proxy(config, server) 321 | 322 | try: 323 | while True: 324 | asyncore.loop(timeout=2, count=1) 325 | # insert scheduling code here. 326 | except KeyboardInterrupt: 327 | print "Crtl+C pressed. Shutting down." 328 | logger.info('Shutting down from Ctrl+C') 329 | 330 | server.shutdown(socket.SHUT_RDWR) 331 | server.close() 332 | sys.exit() 333 | -------------------------------------------------------------------------------- /ext/bootstrap-responsive.min.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * Bootstrap Responsive v2.3.2 3 | * 4 | * Copyright 2012 Twitter, Inc 5 | * Licensed under the Apache License v2.0 6 | * http://www.apache.org/licenses/LICENSE-2.0 7 | * 8 | * Designed and built with all the love in the world @twitter by @mdo and @fat. 9 | */.clearfix{*zoom:1}.clearfix:before,.clearfix:after{display:table;line-height:0;content:""}.clearfix:after{clear:both}.hide-text{font:0/0 a;color:transparent;text-shadow:none;background-color:transparent;border:0}.input-block-level{display:block;width:100%;min-height:30px;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}@-ms-viewport{width:device-width}.hidden{display:none;visibility:hidden}.visible-phone{display:none!important}.visible-tablet{display:none!important}.hidden-desktop{display:none!important}.visible-desktop{display:inherit!important}@media(min-width:768px) and (max-width:979px){.hidden-desktop{display:inherit!important}.visible-desktop{display:none!important}.visible-tablet{display:inherit!important}.hidden-tablet{display:none!important}}@media(max-width:767px){.hidden-desktop{display:inherit!important}.visible-desktop{display:none!important}.visible-phone{display:inherit!important}.hidden-phone{display:none!important}}.visible-print{display:none!important}@media print{.visible-print{display:inherit!important}.hidden-print{display:none!important}}@media(min-width:1200px){.row{margin-left:-30px;*zoom:1}.row:before,.row:after{display:table;line-height:0;content:""}.row:after{clear:both}[class*="span"]{float:left;min-height:1px;margin-left:30px}.container,.navbar-static-top .container,.navbar-fixed-top .container,.navbar-fixed-bottom .container{width:1170px}.span12{width:1170px}.span11{width:1070px}.span10{width:970px}.span9{width:870px}.span8{width:770px}.span7{width:670px}.span6{width:570px}.span5{width:470px}.span4{width:370px}.span3{width:270px}.span2{width:170px}.span1{width:70px}.offset12{margin-left:1230px}.offset11{margin-left:1130px}.offset10{margin-left:1030px}.offset9{margin-left:930px}.offset8{margin-left:830px}.offset7{margin-left:730px}.offset6{margin-left:630px}.offset5{margin-left:530px}.offset4{margin-left:430px}.offset3{margin-left:330px}.offset2{margin-left:230px}.offset1{margin-left:130px}.row-fluid{width:100%;*zoom:1}.row-fluid:before,.row-fluid:after{display:table;line-height:0;content:""}.row-fluid:after{clear:both}.row-fluid [class*="span"]{display:block;float:left;width:100%;min-height:30px;margin-left:2.564102564102564%;*margin-left:2.5109110747408616%;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}.row-fluid [class*="span"]:first-child{margin-left:0}.row-fluid .controls-row [class*="span"]+[class*="span"]{margin-left:2.564102564102564%}.row-fluid .span12{width:100%;*width:99.94680851063829%}.row-fluid .span11{width:91.45299145299145%;*width:91.39979996362975%}.row-fluid .span10{width:82.90598290598291%;*width:82.8527914166212%}.row-fluid .span9{width:74.35897435897436%;*width:74.30578286961266%}.row-fluid .span8{width:65.81196581196582%;*width:65.75877432260411%}.row-fluid .span7{width:57.26495726495726%;*width:57.21176577559556%}.row-fluid .span6{width:48.717948717948715%;*width:48.664757228587014%}.row-fluid .span5{width:40.17094017094017%;*width:40.11774868157847%}.row-fluid .span4{width:31.623931623931625%;*width:31.570740134569924%}.row-fluid .span3{width:23.076923076923077%;*width:23.023731587561375%}.row-fluid .span2{width:14.52991452991453%;*width:14.476723040552828%}.row-fluid .span1{width:5.982905982905983%;*width:5.929714493544281%}.row-fluid .offset12{margin-left:105.12820512820512%;*margin-left:105.02182214948171%}.row-fluid .offset12:first-child{margin-left:102.56410256410257%;*margin-left:102.45771958537915%}.row-fluid .offset11{margin-left:96.58119658119658%;*margin-left:96.47481360247316%}.row-fluid .offset11:first-child{margin-left:94.01709401709402%;*margin-left:93.91071103837061%}.row-fluid .offset10{margin-left:88.03418803418803%;*margin-left:87.92780505546462%}.row-fluid .offset10:first-child{margin-left:85.47008547008548%;*margin-left:85.36370249136206%}.row-fluid .offset9{margin-left:79.48717948717949%;*margin-left:79.38079650845607%}.row-fluid .offset9:first-child{margin-left:76.92307692307693%;*margin-left:76.81669394435352%}.row-fluid .offset8{margin-left:70.94017094017094%;*margin-left:70.83378796144753%}.row-fluid .offset8:first-child{margin-left:68.37606837606839%;*margin-left:68.26968539734497%}.row-fluid .offset7{margin-left:62.393162393162385%;*margin-left:62.28677941443899%}.row-fluid .offset7:first-child{margin-left:59.82905982905982%;*margin-left:59.72267685033642%}.row-fluid .offset6{margin-left:53.84615384615384%;*margin-left:53.739770867430444%}.row-fluid .offset6:first-child{margin-left:51.28205128205128%;*margin-left:51.175668303327875%}.row-fluid .offset5{margin-left:45.299145299145295%;*margin-left:45.1927623204219%}.row-fluid .offset5:first-child{margin-left:42.73504273504273%;*margin-left:42.62865975631933%}.row-fluid .offset4{margin-left:36.75213675213675%;*margin-left:36.645753773413354%}.row-fluid .offset4:first-child{margin-left:34.18803418803419%;*margin-left:34.081651209310785%}.row-fluid .offset3{margin-left:28.205128205128204%;*margin-left:28.0987452264048%}.row-fluid .offset3:first-child{margin-left:25.641025641025642%;*margin-left:25.53464266230224%}.row-fluid .offset2{margin-left:19.65811965811966%;*margin-left:19.551736679396257%}.row-fluid .offset2:first-child{margin-left:17.094017094017094%;*margin-left:16.98763411529369%}.row-fluid .offset1{margin-left:11.11111111111111%;*margin-left:11.004728132387708%}.row-fluid .offset1:first-child{margin-left:8.547008547008547%;*margin-left:8.440625568285142%}input,textarea,.uneditable-input{margin-left:0}.controls-row [class*="span"]+[class*="span"]{margin-left:30px}input.span12,textarea.span12,.uneditable-input.span12{width:1156px}input.span11,textarea.span11,.uneditable-input.span11{width:1056px}input.span10,textarea.span10,.uneditable-input.span10{width:956px}input.span9,textarea.span9,.uneditable-input.span9{width:856px}input.span8,textarea.span8,.uneditable-input.span8{width:756px}input.span7,textarea.span7,.uneditable-input.span7{width:656px}input.span6,textarea.span6,.uneditable-input.span6{width:556px}input.span5,textarea.span5,.uneditable-input.span5{width:456px}input.span4,textarea.span4,.uneditable-input.span4{width:356px}input.span3,textarea.span3,.uneditable-input.span3{width:256px}input.span2,textarea.span2,.uneditable-input.span2{width:156px}input.span1,textarea.span1,.uneditable-input.span1{width:56px}.thumbnails{margin-left:-30px}.thumbnails>li{margin-left:30px}.row-fluid .thumbnails{margin-left:0}}@media(min-width:768px) and (max-width:979px){.row{margin-left:-20px;*zoom:1}.row:before,.row:after{display:table;line-height:0;content:""}.row:after{clear:both}[class*="span"]{float:left;min-height:1px;margin-left:20px}.container,.navbar-static-top .container,.navbar-fixed-top .container,.navbar-fixed-bottom .container{width:724px}.span12{width:724px}.span11{width:662px}.span10{width:600px}.span9{width:538px}.span8{width:476px}.span7{width:414px}.span6{width:352px}.span5{width:290px}.span4{width:228px}.span3{width:166px}.span2{width:104px}.span1{width:42px}.offset12{margin-left:764px}.offset11{margin-left:702px}.offset10{margin-left:640px}.offset9{margin-left:578px}.offset8{margin-left:516px}.offset7{margin-left:454px}.offset6{margin-left:392px}.offset5{margin-left:330px}.offset4{margin-left:268px}.offset3{margin-left:206px}.offset2{margin-left:144px}.offset1{margin-left:82px}.row-fluid{width:100%;*zoom:1}.row-fluid:before,.row-fluid:after{display:table;line-height:0;content:""}.row-fluid:after{clear:both}.row-fluid [class*="span"]{display:block;float:left;width:100%;min-height:30px;margin-left:2.7624309392265194%;*margin-left:2.709239449864817%;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}.row-fluid [class*="span"]:first-child{margin-left:0}.row-fluid .controls-row [class*="span"]+[class*="span"]{margin-left:2.7624309392265194%}.row-fluid .span12{width:100%;*width:99.94680851063829%}.row-fluid .span11{width:91.43646408839778%;*width:91.38327259903608%}.row-fluid .span10{width:82.87292817679558%;*width:82.81973668743387%}.row-fluid .span9{width:74.30939226519337%;*width:74.25620077583166%}.row-fluid .span8{width:65.74585635359117%;*width:65.69266486422946%}.row-fluid .span7{width:57.18232044198895%;*width:57.12912895262725%}.row-fluid .span6{width:48.61878453038674%;*width:48.56559304102504%}.row-fluid .span5{width:40.05524861878453%;*width:40.00205712942283%}.row-fluid .span4{width:31.491712707182323%;*width:31.43852121782062%}.row-fluid .span3{width:22.92817679558011%;*width:22.87498530621841%}.row-fluid .span2{width:14.3646408839779%;*width:14.311449394616199%}.row-fluid .span1{width:5.801104972375691%;*width:5.747913483013988%}.row-fluid .offset12{margin-left:105.52486187845304%;*margin-left:105.41847889972962%}.row-fluid .offset12:first-child{margin-left:102.76243093922652%;*margin-left:102.6560479605031%}.row-fluid .offset11{margin-left:96.96132596685082%;*margin-left:96.8549429881274%}.row-fluid .offset11:first-child{margin-left:94.1988950276243%;*margin-left:94.09251204890089%}.row-fluid .offset10{margin-left:88.39779005524862%;*margin-left:88.2914070765252%}.row-fluid .offset10:first-child{margin-left:85.6353591160221%;*margin-left:85.52897613729868%}.row-fluid .offset9{margin-left:79.8342541436464%;*margin-left:79.72787116492299%}.row-fluid .offset9:first-child{margin-left:77.07182320441989%;*margin-left:76.96544022569647%}.row-fluid .offset8{margin-left:71.2707182320442%;*margin-left:71.16433525332079%}.row-fluid .offset8:first-child{margin-left:68.50828729281768%;*margin-left:68.40190431409427%}.row-fluid .offset7{margin-left:62.70718232044199%;*margin-left:62.600799341718584%}.row-fluid .offset7:first-child{margin-left:59.94475138121547%;*margin-left:59.838368402492065%}.row-fluid .offset6{margin-left:54.14364640883978%;*margin-left:54.037263430116376%}.row-fluid .offset6:first-child{margin-left:51.38121546961326%;*margin-left:51.27483249088986%}.row-fluid .offset5{margin-left:45.58011049723757%;*margin-left:45.47372751851417%}.row-fluid .offset5:first-child{margin-left:42.81767955801105%;*margin-left:42.71129657928765%}.row-fluid .offset4{margin-left:37.01657458563536%;*margin-left:36.91019160691196%}.row-fluid .offset4:first-child{margin-left:34.25414364640884%;*margin-left:34.14776066768544%}.row-fluid .offset3{margin-left:28.45303867403315%;*margin-left:28.346655695309746%}.row-fluid .offset3:first-child{margin-left:25.69060773480663%;*margin-left:25.584224756083227%}.row-fluid .offset2{margin-left:19.88950276243094%;*margin-left:19.783119783707537%}.row-fluid .offset2:first-child{margin-left:17.12707182320442%;*margin-left:17.02068884448102%}.row-fluid .offset1{margin-left:11.32596685082873%;*margin-left:11.219583872105325%}.row-fluid .offset1:first-child{margin-left:8.56353591160221%;*margin-left:8.457152932878806%}input,textarea,.uneditable-input{margin-left:0}.controls-row [class*="span"]+[class*="span"]{margin-left:20px}input.span12,textarea.span12,.uneditable-input.span12{width:710px}input.span11,textarea.span11,.uneditable-input.span11{width:648px}input.span10,textarea.span10,.uneditable-input.span10{width:586px}input.span9,textarea.span9,.uneditable-input.span9{width:524px}input.span8,textarea.span8,.uneditable-input.span8{width:462px}input.span7,textarea.span7,.uneditable-input.span7{width:400px}input.span6,textarea.span6,.uneditable-input.span6{width:338px}input.span5,textarea.span5,.uneditable-input.span5{width:276px}input.span4,textarea.span4,.uneditable-input.span4{width:214px}input.span3,textarea.span3,.uneditable-input.span3{width:152px}input.span2,textarea.span2,.uneditable-input.span2{width:90px}input.span1,textarea.span1,.uneditable-input.span1{width:28px}}@media(max-width:767px){body{padding-right:20px;padding-left:20px}.navbar-fixed-top,.navbar-fixed-bottom,.navbar-static-top{margin-right:-20px;margin-left:-20px}.container-fluid{padding:0}.dl-horizontal dt{float:none;width:auto;clear:none;text-align:left}.dl-horizontal dd{margin-left:0}.container{width:auto}.row-fluid{width:100%}.row,.thumbnails{margin-left:0}.thumbnails>li{float:none;margin-left:0}[class*="span"],.uneditable-input[class*="span"],.row-fluid [class*="span"]{display:block;float:none;width:100%;margin-left:0;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}.span12,.row-fluid .span12{width:100%;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}.row-fluid [class*="offset"]:first-child{margin-left:0}.input-large,.input-xlarge,.input-xxlarge,input[class*="span"],select[class*="span"],textarea[class*="span"],.uneditable-input{display:block;width:100%;min-height:30px;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}.input-prepend input,.input-append input,.input-prepend input[class*="span"],.input-append input[class*="span"]{display:inline-block;width:auto}.controls-row [class*="span"]+[class*="span"]{margin-left:0}.modal{position:fixed;top:20px;right:20px;left:20px;width:auto;margin:0}.modal.fade{top:-100px}.modal.fade.in{top:20px}}@media(max-width:480px){.nav-collapse{-webkit-transform:translate3d(0,0,0)}.page-header h1 small{display:block;line-height:20px}input[type="checkbox"],input[type="radio"]{border:1px solid #ccc}.form-horizontal .control-label{float:none;width:auto;padding-top:0;text-align:left}.form-horizontal .controls{margin-left:0}.form-horizontal .control-list{padding-top:0}.form-horizontal .form-actions{padding-right:10px;padding-left:10px}.media .pull-left,.media .pull-right{display:block;float:none;margin-bottom:10px}.media-object{margin-right:0;margin-left:0}.modal{top:10px;right:10px;left:10px}.modal-header .close{padding:10px;margin:-10px}.carousel-caption{position:static}}@media(max-width:979px){body{padding-top:0}.navbar-fixed-top,.navbar-fixed-bottom{position:static}.navbar-fixed-top{margin-bottom:20px}.navbar-fixed-bottom{margin-top:20px}.navbar-fixed-top .navbar-inner,.navbar-fixed-bottom .navbar-inner{padding:5px}.navbar .container{width:auto;padding:0}.navbar .brand{padding-right:10px;padding-left:10px;margin:0 0 0 -5px}.nav-collapse{clear:both}.nav-collapse .nav{float:none;margin:0 0 10px}.nav-collapse .nav>li{float:none}.nav-collapse .nav>li>a{margin-bottom:2px}.nav-collapse .nav>.divider-vertical{display:none}.nav-collapse .nav .nav-header{color:#777;text-shadow:none}.nav-collapse .nav>li>a,.nav-collapse .dropdown-menu a{padding:9px 15px;font-weight:bold;color:#777;-webkit-border-radius:3px;-moz-border-radius:3px;border-radius:3px}.nav-collapse .btn{padding:4px 10px 4px;font-weight:normal;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px}.nav-collapse .dropdown-menu li+li a{margin-bottom:2px}.nav-collapse .nav>li>a:hover,.nav-collapse .nav>li>a:focus,.nav-collapse .dropdown-menu a:hover,.nav-collapse .dropdown-menu a:focus{background-color:#f2f2f2}.navbar-inverse .nav-collapse .nav>li>a,.navbar-inverse .nav-collapse .dropdown-menu a{color:#999}.navbar-inverse .nav-collapse .nav>li>a:hover,.navbar-inverse .nav-collapse .nav>li>a:focus,.navbar-inverse .nav-collapse .dropdown-menu a:hover,.navbar-inverse .nav-collapse .dropdown-menu a:focus{background-color:#111}.nav-collapse.in .btn-group{padding:0;margin-top:5px}.nav-collapse .dropdown-menu{position:static;top:auto;left:auto;display:none;float:none;max-width:none;padding:0;margin:0 15px;background-color:transparent;border:0;-webkit-border-radius:0;-moz-border-radius:0;border-radius:0;-webkit-box-shadow:none;-moz-box-shadow:none;box-shadow:none}.nav-collapse .open>.dropdown-menu{display:block}.nav-collapse .dropdown-menu:before,.nav-collapse .dropdown-menu:after{display:none}.nav-collapse .dropdown-menu .divider{display:none}.nav-collapse .nav>li>.dropdown-menu:before,.nav-collapse .nav>li>.dropdown-menu:after{display:none}.nav-collapse .navbar-form,.nav-collapse .navbar-search{float:none;padding:10px 15px;margin:10px 0;border-top:1px solid #f2f2f2;border-bottom:1px solid #f2f2f2;-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,0.1),0 1px 0 rgba(255,255,255,0.1);-moz-box-shadow:inset 0 1px 0 rgba(255,255,255,0.1),0 1px 0 rgba(255,255,255,0.1);box-shadow:inset 0 1px 0 rgba(255,255,255,0.1),0 1px 0 rgba(255,255,255,0.1)}.navbar-inverse .nav-collapse .navbar-form,.navbar-inverse .nav-collapse .navbar-search{border-top-color:#111;border-bottom-color:#111}.navbar .nav-collapse .nav.pull-right{float:none;margin-left:0}.nav-collapse,.nav-collapse.collapse{height:0;overflow:hidden}.navbar .btn-navbar{display:block}.navbar-static .navbar-inner{padding-right:10px;padding-left:10px}}@media(min-width:980px){.nav-collapse.collapse{height:auto!important;overflow:visible!important}} 10 | -------------------------------------------------------------------------------- /ext/moment.min.js: -------------------------------------------------------------------------------- 1 | // moment.js 2 | // version : 2.1.0 3 | // author : Tim Wood 4 | // license : MIT 5 | // momentjs.com 6 | !function(t){function e(t,e){return function(n){return u(t.call(this,n),e)}}function n(t,e){return function(n){return this.lang().ordinal(t.call(this,n),e)}}function s(){}function i(t){a(this,t)}function r(t){var e=t.years||t.year||t.y||0,n=t.months||t.month||t.M||0,s=t.weeks||t.week||t.w||0,i=t.days||t.day||t.d||0,r=t.hours||t.hour||t.h||0,a=t.minutes||t.minute||t.m||0,o=t.seconds||t.second||t.s||0,u=t.milliseconds||t.millisecond||t.ms||0;this._input=t,this._milliseconds=u+1e3*o+6e4*a+36e5*r,this._days=i+7*s,this._months=n+12*e,this._data={},this._bubble()}function a(t,e){for(var n in e)e.hasOwnProperty(n)&&(t[n]=e[n]);return t}function o(t){return 0>t?Math.ceil(t):Math.floor(t)}function u(t,e){for(var n=t+"";n.lengthn;n++)~~t[n]!==~~e[n]&&r++;return r+i}function f(t){return t?ie[t]||t.toLowerCase().replace(/(.)s$/,"$1"):t}function l(t,e){return e.abbr=t,x[t]||(x[t]=new s),x[t].set(e),x[t]}function _(t){if(!t)return H.fn._lang;if(!x[t]&&A)try{require("./lang/"+t)}catch(e){return H.fn._lang}return x[t]}function m(t){return t.match(/\[.*\]/)?t.replace(/^\[|\]$/g,""):t.replace(/\\/g,"")}function y(t){var e,n,s=t.match(E);for(e=0,n=s.length;n>e;e++)s[e]=ue[s[e]]?ue[s[e]]:m(s[e]);return function(i){var r="";for(e=0;n>e;e++)r+=s[e]instanceof Function?s[e].call(i,t):s[e];return r}}function M(t,e){function n(e){return t.lang().longDateFormat(e)||e}for(var s=5;s--&&N.test(e);)e=e.replace(N,n);return re[e]||(re[e]=y(e)),re[e](t)}function g(t,e){switch(t){case"DDDD":return V;case"YYYY":return X;case"YYYYY":return $;case"S":case"SS":case"SSS":case"DDD":return I;case"MMM":case"MMMM":case"dd":case"ddd":case"dddd":return R;case"a":case"A":return _(e._l)._meridiemParse;case"X":return B;case"Z":case"ZZ":return j;case"T":return q;case"MM":case"DD":case"YY":case"HH":case"hh":case"mm":case"ss":case"M":case"D":case"d":case"H":case"h":case"m":case"s":return J;default:return new RegExp(t.replace("\\",""))}}function p(t){var e=(j.exec(t)||[])[0],n=(e+"").match(ee)||["-",0,0],s=+(60*n[1])+~~n[2];return"+"===n[0]?-s:s}function D(t,e,n){var s,i=n._a;switch(t){case"M":case"MM":i[1]=null==e?0:~~e-1;break;case"MMM":case"MMMM":s=_(n._l).monthsParse(e),null!=s?i[1]=s:n._isValid=!1;break;case"D":case"DD":case"DDD":case"DDDD":null!=e&&(i[2]=~~e);break;case"YY":i[0]=~~e+(~~e>68?1900:2e3);break;case"YYYY":case"YYYYY":i[0]=~~e;break;case"a":case"A":n._isPm=_(n._l).isPM(e);break;case"H":case"HH":case"h":case"hh":i[3]=~~e;break;case"m":case"mm":i[4]=~~e;break;case"s":case"ss":i[5]=~~e;break;case"S":case"SS":case"SSS":i[6]=~~(1e3*("0."+e));break;case"X":n._d=new Date(1e3*parseFloat(e));break;case"Z":case"ZZ":n._useUTC=!0,n._tzm=p(e)}null==e&&(n._isValid=!1)}function Y(t){var e,n,s=[];if(!t._d){for(e=0;7>e;e++)t._a[e]=s[e]=null==t._a[e]?2===e?1:0:t._a[e];s[3]+=~~((t._tzm||0)/60),s[4]+=~~((t._tzm||0)%60),n=new Date(0),t._useUTC?(n.setUTCFullYear(s[0],s[1],s[2]),n.setUTCHours(s[3],s[4],s[5],s[6])):(n.setFullYear(s[0],s[1],s[2]),n.setHours(s[3],s[4],s[5],s[6])),t._d=n}}function w(t){var e,n,s=t._f.match(E),i=t._i;for(t._a=[],e=0;eo&&(u=o,s=n);a(t,s)}function v(t){var e,n=t._i,s=K.exec(n);if(s){for(t._f="YYYY-MM-DD"+(s[2]||" "),e=0;4>e;e++)if(te[e][1].exec(n)){t._f+=te[e][0];break}j.exec(n)&&(t._f+=" Z"),w(t)}else t._d=new Date(n)}function T(e){var n=e._i,s=G.exec(n);n===t?e._d=new Date:s?e._d=new Date(+s[1]):"string"==typeof n?v(e):d(n)?(e._a=n.slice(0),Y(e)):e._d=n instanceof Date?new Date(+n):new Date(n)}function b(t,e,n,s,i){return i.relativeTime(e||1,!!n,t,s)}function S(t,e,n){var s=W(Math.abs(t)/1e3),i=W(s/60),r=W(i/60),a=W(r/24),o=W(a/365),u=45>s&&["s",s]||1===i&&["m"]||45>i&&["mm",i]||1===r&&["h"]||22>r&&["hh",r]||1===a&&["d"]||25>=a&&["dd",a]||45>=a&&["M"]||345>a&&["MM",W(a/30)]||1===o&&["y"]||["yy",o];return u[2]=e,u[3]=t>0,u[4]=n,b.apply({},u)}function F(t,e,n){var s,i=n-e,r=n-t.day();return r>i&&(r-=7),i-7>r&&(r+=7),s=H(t).add("d",r),{week:Math.ceil(s.dayOfYear()/7),year:s.year()}}function O(t){var e=t._i,n=t._f;return null===e||""===e?null:("string"==typeof e&&(t._i=e=_().preparse(e)),H.isMoment(e)?(t=a({},e),t._d=new Date(+e._d)):n?d(n)?k(t):w(t):T(t),new i(t))}function z(t,e){H.fn[t]=H.fn[t+"s"]=function(t){var n=this._isUTC?"UTC":"";return null!=t?(this._d["set"+n+e](t),H.updateOffset(this),this):this._d["get"+n+e]()}}function C(t){H.duration.fn[t]=function(){return this._data[t]}}function L(t,e){H.duration.fn["as"+t]=function(){return+this/e}}for(var H,P,U="2.1.0",W=Math.round,x={},A="undefined"!=typeof module&&module.exports,G=/^\/?Date\((\-?\d+)/i,Z=/(\-)?(\d*)?\.?(\d+)\:(\d+)\:(\d+)\.?(\d{3})?/,E=/(\[[^\[]*\])|(\\)?(Mo|MM?M?M?|Do|DDDo|DD?D?D?|ddd?d?|do?|w[o|w]?|W[o|W]?|YYYYY|YYYY|YY|gg(ggg?)?|GG(GGG?)?|e|E|a|A|hh?|HH?|mm?|ss?|SS?S?|X|zz?|ZZ?|.)/g,N=/(\[[^\[]*\])|(\\)?(LT|LL?L?L?|l{1,4})/g,J=/\d\d?/,I=/\d{1,3}/,V=/\d{3}/,X=/\d{1,4}/,$=/[+\-]?\d{1,6}/,R=/[0-9]*['a-z\u00A0-\u05FF\u0700-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]+|[\u0600-\u06FF\/]+(\s*?[\u0600-\u06FF]+){1,2}/i,j=/Z|[\+\-]\d\d:?\d\d/i,q=/T/i,B=/[\+\-]?\d+(\.\d{1,3})?/,K=/^\s*\d{4}-\d\d-\d\d((T| )(\d\d(:\d\d(:\d\d(\.\d\d?\d?)?)?)?)?([\+\-]\d\d:?\d\d)?)?/,Q="YYYY-MM-DDTHH:mm:ssZ",te=[["HH:mm:ss.S",/(T| )\d\d:\d\d:\d\d\.\d{1,3}/],["HH:mm:ss",/(T| )\d\d:\d\d:\d\d/],["HH:mm",/(T| )\d\d:\d\d/],["HH",/(T| )\d\d/]],ee=/([\+\-]|\d\d)/gi,ne="Date|Hours|Minutes|Seconds|Milliseconds".split("|"),se={Milliseconds:1,Seconds:1e3,Minutes:6e4,Hours:36e5,Days:864e5,Months:2592e6,Years:31536e6},ie={ms:"millisecond",s:"second",m:"minute",h:"hour",d:"day",w:"week",M:"month",y:"year"},re={},ae="DDD w W M D d".split(" "),oe="M D H h m s w W".split(" "),ue={M:function(){return this.month()+1},MMM:function(t){return this.lang().monthsShort(this,t)},MMMM:function(t){return this.lang().months(this,t)},D:function(){return this.date()},DDD:function(){return this.dayOfYear()},d:function(){return this.day()},dd:function(t){return this.lang().weekdaysMin(this,t)},ddd:function(t){return this.lang().weekdaysShort(this,t)},dddd:function(t){return this.lang().weekdays(this,t)},w:function(){return this.week()},W:function(){return this.isoWeek()},YY:function(){return u(this.year()%100,2)},YYYY:function(){return u(this.year(),4)},YYYYY:function(){return u(this.year(),5)},gg:function(){return u(this.weekYear()%100,2)},gggg:function(){return this.weekYear()},ggggg:function(){return u(this.weekYear(),5)},GG:function(){return u(this.isoWeekYear()%100,2)},GGGG:function(){return this.isoWeekYear()},GGGGG:function(){return u(this.isoWeekYear(),5)},e:function(){return this.weekday()},E:function(){return this.isoWeekday()},a:function(){return this.lang().meridiem(this.hours(),this.minutes(),!0)},A:function(){return this.lang().meridiem(this.hours(),this.minutes(),!1)},H:function(){return this.hours()},h:function(){return this.hours()%12||12},m:function(){return this.minutes()},s:function(){return this.seconds()},S:function(){return~~(this.milliseconds()/100)},SS:function(){return u(~~(this.milliseconds()/10),2)},SSS:function(){return u(this.milliseconds(),3)},Z:function(){var t=-this.zone(),e="+";return 0>t&&(t=-t,e="-"),e+u(~~(t/60),2)+":"+u(~~t%60,2)},ZZ:function(){var t=-this.zone(),e="+";return 0>t&&(t=-t,e="-"),e+u(~~(10*t/6),4)},z:function(){return this.zoneAbbr()},zz:function(){return this.zoneName()},X:function(){return this.unix()}};ae.length;)P=ae.pop(),ue[P+"o"]=n(ue[P],P);for(;oe.length;)P=oe.pop(),ue[P+P]=e(ue[P],2);for(ue.DDDD=e(ue.DDD,3),s.prototype={set:function(t){var e,n;for(n in t)e=t[n],"function"==typeof e?this[n]=e:this["_"+n]=e},_months:"January_February_March_April_May_June_July_August_September_October_November_December".split("_"),months:function(t){return this._months[t.month()]},_monthsShort:"Jan_Feb_Mar_Apr_May_Jun_Jul_Aug_Sep_Oct_Nov_Dec".split("_"),monthsShort:function(t){return this._monthsShort[t.month()]},monthsParse:function(t){var e,n,s;for(this._monthsParse||(this._monthsParse=[]),e=0;12>e;e++)if(this._monthsParse[e]||(n=H([2e3,e]),s="^"+this.months(n,"")+"|^"+this.monthsShort(n,""),this._monthsParse[e]=new RegExp(s.replace(".",""),"i")),this._monthsParse[e].test(t))return e},_weekdays:"Sunday_Monday_Tuesday_Wednesday_Thursday_Friday_Saturday".split("_"),weekdays:function(t){return this._weekdays[t.day()]},_weekdaysShort:"Sun_Mon_Tue_Wed_Thu_Fri_Sat".split("_"),weekdaysShort:function(t){return this._weekdaysShort[t.day()]},_weekdaysMin:"Su_Mo_Tu_We_Th_Fr_Sa".split("_"),weekdaysMin:function(t){return this._weekdaysMin[t.day()]},weekdaysParse:function(t){var e,n,s;for(this._weekdaysParse||(this._weekdaysParse=[]),e=0;7>e;e++)if(this._weekdaysParse[e]||(n=H([2e3,1]).day(e),s="^"+this.weekdays(n,"")+"|^"+this.weekdaysShort(n,"")+"|^"+this.weekdaysMin(n,""),this._weekdaysParse[e]=new RegExp(s.replace(".",""),"i")),this._weekdaysParse[e].test(t))return e},_longDateFormat:{LT:"h:mm A",L:"MM/DD/YYYY",LL:"MMMM D YYYY",LLL:"MMMM D YYYY LT",LLLL:"dddd, MMMM D YYYY LT"},longDateFormat:function(t){var e=this._longDateFormat[t];return!e&&this._longDateFormat[t.toUpperCase()]&&(e=this._longDateFormat[t.toUpperCase()].replace(/MMMM|MM|DD|dddd/g,function(t){return t.slice(1)}),this._longDateFormat[t]=e),e},isPM:function(t){return"p"===(t+"").toLowerCase()[0]},_meridiemParse:/[ap]\.?m?\.?/i,meridiem:function(t,e,n){return t>11?n?"pm":"PM":n?"am":"AM"},_calendar:{sameDay:"[Today at] LT",nextDay:"[Tomorrow at] LT",nextWeek:"dddd [at] LT",lastDay:"[Yesterday at] LT",lastWeek:"[Last] dddd [at] LT",sameElse:"L"},calendar:function(t,e){var n=this._calendar[t];return"function"==typeof n?n.apply(e):n},_relativeTime:{future:"in %s",past:"%s ago",s:"a few seconds",m:"a minute",mm:"%d minutes",h:"an hour",hh:"%d hours",d:"a day",dd:"%d days",M:"a month",MM:"%d months",y:"a year",yy:"%d years"},relativeTime:function(t,e,n,s){var i=this._relativeTime[n];return"function"==typeof i?i(t,e,n,s):i.replace(/%d/i,t)},pastFuture:function(t,e){var n=this._relativeTime[t>0?"future":"past"];return"function"==typeof n?n(e):n.replace(/%s/i,e)},ordinal:function(t){return this._ordinal.replace("%d",t)},_ordinal:"%d",preparse:function(t){return t},postformat:function(t){return t},week:function(t){return F(t,this._week.dow,this._week.doy).week},_week:{dow:0,doy:6}},H=function(t,e,n){return O({_i:t,_f:e,_l:n,_isUTC:!1})},H.utc=function(t,e,n){return O({_useUTC:!0,_isUTC:!0,_l:n,_i:t,_f:e})},H.unix=function(t){return H(1e3*t)},H.duration=function(t,e){var n,s,i=H.isDuration(t),a="number"==typeof t,o=i?t._input:a?{}:t,u=Z.exec(t);return a?e?o[e]=t:o.milliseconds=t:u&&(n="-"===u[1]?-1:1,o={y:0,d:~~u[2]*n,h:~~u[3]*n,m:~~u[4]*n,s:~~u[5]*n,ms:~~u[6]*n}),s=new r(o),i&&t.hasOwnProperty("_lang")&&(s._lang=t._lang),s},H.version=U,H.defaultFormat=Q,H.updateOffset=function(){},H.lang=function(t,e){return t?(e?l(t,e):x[t]||_(t),H.duration.fn._lang=H.fn._lang=_(t),void 0):H.fn._lang._abbr},H.langData=function(t){return t&&t._lang&&t._lang._abbr&&(t=t._lang._abbr),_(t)},H.isMoment=function(t){return t instanceof i},H.isDuration=function(t){return t instanceof r},H.fn=i.prototype={clone:function(){return H(this)},valueOf:function(){return+this._d+6e4*(this._offset||0)},unix:function(){return Math.floor(+this/1e3)},toString:function(){return this.format("ddd MMM DD YYYY HH:mm:ss [GMT]ZZ")},toDate:function(){return this._offset?new Date(+this):this._d},toISOString:function(){return M(H(this).utc(),"YYYY-MM-DD[T]HH:mm:ss.SSS[Z]")},toArray:function(){var t=this;return[t.year(),t.month(),t.date(),t.hours(),t.minutes(),t.seconds(),t.milliseconds()]},isValid:function(){return null==this._isValid&&(this._isValid=this._a?!c(this._a,(this._isUTC?H.utc(this._a):H(this._a)).toArray()):!isNaN(this._d.getTime())),!!this._isValid},utc:function(){return this.zone(0)},local:function(){return this.zone(0),this._isUTC=!1,this},format:function(t){var e=M(this,t||H.defaultFormat);return this.lang().postformat(e)},add:function(t,e){var n;return n="string"==typeof t?H.duration(+e,t):H.duration(t,e),h(this,n,1),this},subtract:function(t,e){var n;return n="string"==typeof t?H.duration(+e,t):H.duration(t,e),h(this,n,-1),this},diff:function(t,e,n){var s,i,r=this._isUTC?H(t).zone(this._offset||0):H(t).local(),a=6e4*(this.zone()-r.zone());return e=f(e),"year"===e||"month"===e?(s=432e5*(this.daysInMonth()+r.daysInMonth()),i=12*(this.year()-r.year())+(this.month()-r.month()),i+=(this-H(this).startOf("month")-(r-H(r).startOf("month")))/s,i-=6e4*(this.zone()-H(this).startOf("month").zone()-(r.zone()-H(r).startOf("month").zone()))/s,"year"===e&&(i/=12)):(s=this-r,i="second"===e?s/1e3:"minute"===e?s/6e4:"hour"===e?s/36e5:"day"===e?(s-a)/864e5:"week"===e?(s-a)/6048e5:s),n?i:o(i)},from:function(t,e){return H.duration(this.diff(t)).lang(this.lang()._abbr).humanize(!e)},fromNow:function(t){return this.from(H(),t)},calendar:function(){var t=this.diff(H().startOf("day"),"days",!0),e=-6>t?"sameElse":-1>t?"lastWeek":0>t?"lastDay":1>t?"sameDay":2>t?"nextDay":7>t?"nextWeek":"sameElse";return this.format(this.lang().calendar(e,this))},isLeapYear:function(){var t=this.year();return 0===t%4&&0!==t%100||0===t%400},isDST:function(){return this.zone()+H(t).startOf(e)},isBefore:function(t,e){return e="undefined"!=typeof e?e:"millisecond",+this.clone().startOf(e)<+H(t).startOf(e)},isSame:function(t,e){return e="undefined"!=typeof e?e:"millisecond",+this.clone().startOf(e)===+H(t).startOf(e)},min:function(t){return t=H.apply(null,arguments),this>t?this:t},max:function(t){return t=H.apply(null,arguments),t>this?this:t},zone:function(t){var e=this._offset||0;return null==t?this._isUTC?e:this._d.getTimezoneOffset():("string"==typeof t&&(t=p(t)),Math.abs(t)<16&&(t=60*t),this._offset=t,this._isUTC=!0,e!==t&&h(this,H.duration(e-t,"m"),1,!0),this)},zoneAbbr:function(){return this._isUTC?"UTC":""},zoneName:function(){return this._isUTC?"Coordinated Universal Time":""},daysInMonth:function(){return H.utc([this.year(),this.month()+1,0]).date()},dayOfYear:function(t){var e=W((H(this).startOf("day")-H(this).startOf("year"))/864e5)+1;return null==t?e:this.add("d",t-e)},weekYear:function(t){var e=F(this,this.lang()._week.dow,this.lang()._week.doy).year;return null==t?e:this.add("y",t-e)},isoWeekYear:function(t){var e=F(this,1,4).year;return null==t?e:this.add("y",t-e)},week:function(t){var e=this.lang().week(this);return null==t?e:this.add("d",7*(t-e))},isoWeek:function(t){var e=F(this,1,4).week;return null==t?e:this.add("d",7*(t-e))},weekday:function(t){var e=(this._d.getDay()+7-this.lang()._week.dow)%7;return null==t?e:this.add("d",t-e)},isoWeekday:function(t){return null==t?this.day()||7:this.day(this.day()%7?t:t-7)},lang:function(e){return e===t?this._lang:(this._lang=_(e),this)}},P=0;Pthis.$items.length-1||t<0)return;return this.sliding?this.$element.one("slid",function(){r.to(t)}):n==t?this.pause().cycle():this.slide(t>n?"next":"prev",e(this.$items[t]))},pause:function(t){return t||(this.paused=!0),this.$element.find(".next, .prev").length&&e.support.transition.end&&(this.$element.trigger(e.support.transition.end),this.cycle(!0)),clearInterval(this.interval),this.interval=null,this},next:function(){if(this.sliding)return;return this.slide("next")},prev:function(){if(this.sliding)return;return this.slide("prev")},slide:function(t,n){var r=this.$element.find(".item.active"),i=n||r[t](),s=this.interval,o=t=="next"?"left":"right",u=t=="next"?"first":"last",a=this,f;this.sliding=!0,s&&this.pause(),i=i.length?i:this.$element.find(".item")[u](),f=e.Event("slide",{relatedTarget:i[0],direction:o});if(i.hasClass("active"))return;this.$indicators.length&&(this.$indicators.find(".active").removeClass("active"),this.$element.one("slid",function(){var t=e(a.$indicators.children()[a.getActiveIndex()]);t&&t.addClass("active")}));if(e.support.transition&&this.$element.hasClass("slide")){this.$element.trigger(f);if(f.isDefaultPrevented())return;i.addClass(t),i[0].offsetWidth,r.addClass(o),i.addClass(o),this.$element.one(e.support.transition.end,function(){i.removeClass([t,o].join(" ")).addClass("active"),r.removeClass(["active",o].join(" ")),a.sliding=!1,setTimeout(function(){a.$element.trigger("slid")},0)})}else{this.$element.trigger(f);if(f.isDefaultPrevented())return;r.removeClass("active"),i.addClass("active"),this.sliding=!1,this.$element.trigger("slid")}return s&&this.cycle(),this}};var n=e.fn.carousel;e.fn.carousel=function(n){return this.each(function(){var r=e(this),i=r.data("carousel"),s=e.extend({},e.fn.carousel.defaults,typeof n=="object"&&n),o=typeof n=="string"?n:s.slide;i||r.data("carousel",i=new t(this,s)),typeof n=="number"?i.to(n):o?i[o]():s.interval&&i.pause().cycle()})},e.fn.carousel.defaults={interval:5e3,pause:"hover"},e.fn.carousel.Constructor=t,e.fn.carousel.noConflict=function(){return e.fn.carousel=n,this},e(document).on("click.carousel.data-api","[data-slide], [data-slide-to]",function(t){var n=e(this),r,i=e(n.attr("data-target")||(r=n.attr("href"))&&r.replace(/.*(?=#[^\s]+$)/,"")),s=e.extend({},i.data(),n.data()),o;i.carousel(s),(o=n.attr("data-slide-to"))&&i.data("carousel").pause().to(o).cycle(),t.preventDefault()})}(window.jQuery),!function(e){"use strict";var t=function(t,n){this.$element=e(t),this.options=e.extend({},e.fn.collapse.defaults,n),this.options.parent&&(this.$parent=e(this.options.parent)),this.options.toggle&&this.toggle()};t.prototype={constructor:t,dimension:function(){var e=this.$element.hasClass("width");return e?"width":"height"},show:function(){var t,n,r,i;if(this.transitioning||this.$element.hasClass("in"))return;t=this.dimension(),n=e.camelCase(["scroll",t].join("-")),r=this.$parent&&this.$parent.find("> .accordion-group > .in");if(r&&r.length){i=r.data("collapse");if(i&&i.transitioning)return;r.collapse("hide"),i||r.data("collapse",null)}this.$element[t](0),this.transition("addClass",e.Event("show"),"shown"),e.support.transition&&this.$element[t](this.$element[0][n])},hide:function(){var t;if(this.transitioning||!this.$element.hasClass("in"))return;t=this.dimension(),this.reset(this.$element[t]()),this.transition("removeClass",e.Event("hide"),"hidden"),this.$element[t](0)},reset:function(e){var t=this.dimension();return this.$element.removeClass("collapse")[t](e||"auto")[0].offsetWidth,this.$element[e!==null?"addClass":"removeClass"]("collapse"),this},transition:function(t,n,r){var i=this,s=function(){n.type=="show"&&i.reset(),i.transitioning=0,i.$element.trigger(r)};this.$element.trigger(n);if(n.isDefaultPrevented())return;this.transitioning=1,this.$element[t]("in"),e.support.transition&&this.$element.hasClass("collapse")?this.$element.one(e.support.transition.end,s):s()},toggle:function(){this[this.$element.hasClass("in")?"hide":"show"]()}};var n=e.fn.collapse;e.fn.collapse=function(n){return this.each(function(){var r=e(this),i=r.data("collapse"),s=e.extend({},e.fn.collapse.defaults,r.data(),typeof n=="object"&&n);i||r.data("collapse",i=new t(this,s)),typeof n=="string"&&i[n]()})},e.fn.collapse.defaults={toggle:!0},e.fn.collapse.Constructor=t,e.fn.collapse.noConflict=function(){return e.fn.collapse=n,this},e(document).on("click.collapse.data-api","[data-toggle=collapse]",function(t){var n=e(this),r,i=n.attr("data-target")||t.preventDefault()||(r=n.attr("href"))&&r.replace(/.*(?=#[^\s]+$)/,""),s=e(i).data("collapse")?"toggle":n.data();n[e(i).hasClass("in")?"addClass":"removeClass"]("collapsed"),e(i).collapse(s)})}(window.jQuery),!function(e){"use strict";function r(){e(".dropdown-backdrop").remove(),e(t).each(function(){i(e(this)).removeClass("open")})}function i(t){var n=t.attr("data-target"),r;n||(n=t.attr("href"),n=n&&/#/.test(n)&&n.replace(/.*(?=#[^\s]*$)/,"")),r=n&&e(n);if(!r||!r.length)r=t.parent();return r}var t="[data-toggle=dropdown]",n=function(t){var n=e(t).on("click.dropdown.data-api",this.toggle);e("html").on("click.dropdown.data-api",function(){n.parent().removeClass("open")})};n.prototype={constructor:n,toggle:function(t){var n=e(this),s,o;if(n.is(".disabled, :disabled"))return;return s=i(n),o=s.hasClass("open"),r(),o||("ontouchstart"in document.documentElement&&e('