├── .gitignore ├── widgets ├── text │ ├── text.coffee │ ├── text.html │ └── text.scss ├── iframe │ ├── iframe.html │ ├── iframe.scss │ └── iframe.coffee ├── clock │ ├── clock.html │ ├── clock.scss │ └── clock.coffee ├── image │ ├── image.html │ ├── image.coffee │ └── image.scss ├── reload │ ├── reload.html │ ├── reload.coffee │ └── reload.scss ├── stmode │ ├── stmode.html │ ├── stmode.scss │ └── stmode.coffee ├── changepage │ ├── changepage.html │ ├── changepage.coffee │ └── changepage.scss ├── stswitch │ ├── stswitch.html │ ├── stswitch.scss │ └── stswitch.coffee ├── stlock │ ├── stlock.html │ ├── stlock.scss │ └── stlock.coffee ├── stmotion │ ├── stmotion.html │ ├── stmotion.scss │ └── stmotion.coffee ├── sttemp │ ├── sttemp.html │ ├── sttemp.coffee │ └── sttemp.scss ├── stcontact │ ├── stcontact.html │ ├── stcontact.scss │ └── stcontact.coffee ├── sthumidity │ ├── sthumidity.html │ ├── sthumidity.coffee │ └── sthumidity.scss ├── stpresence │ ├── stpresence.html │ ├── stpresence.scss │ └── stpresence.coffee ├── list │ ├── list.coffee │ ├── list.html │ └── list.scss ├── graph │ ├── graph.html │ ├── graph.coffee │ └── graph.scss ├── stmeter │ ├── stmeter.html │ ├── stmeter.scss │ └── stmeter.coffee ├── comments │ ├── comments.html │ ├── comments.coffee │ └── comments.scss ├── stmodechange │ ├── stmodechange.html │ ├── stmodechange.scss │ └── stmodechange.coffee ├── meter │ ├── meter.html │ ├── meter.coffee │ └── meter.scss ├── number │ ├── number.html │ ├── number.scss │ └── number.coffee ├── stdimmer │ ├── stdimmer.html │ ├── stdimmer.scss │ └── stdimmer.coffee └── stweather │ ├── stweather.coffee │ ├── stweather.html │ └── stweather.scss ├── public ├── favicon.ico └── 404.html ├── assets ├── images │ └── logo.png ├── fonts │ ├── climacons-webfont.eot │ ├── climacons-webfont.ttf │ ├── climacons-webfont.woff │ ├── fontawesome-webfont.eot │ ├── fontawesome-webfont.ttf │ └── fontawesome-webfont.woff ├── stylesheets │ ├── climacons-font.css │ ├── jquery.gridster.css │ ├── application.scss │ └── font-awesome.css └── javascripts │ ├── clickablewidget.coffee │ ├── gridster │ └── jquery.leanModal.min.js │ ├── application.coffee │ ├── dashing.gridster.coffee │ ├── eventsource.min.js │ ├── cycleDashboards.coffee │ └── jquery.knob.js ├── jobs ├── heartbeat.rb └── smartthings.rb ├── Gemfile ├── lib ├── settings.rb └── stapp.rb ├── dashboards ├── layout.erb └── main.erb ├── config.ru ├── hadashboard ├── Gemfile.lock ├── README.md └── smartapps └── DashingAccess.groovy /.gitignore: -------------------------------------------------------------------------------- 1 | *DS_STORE 2 | history.yml 3 | persistent.db 4 | -------------------------------------------------------------------------------- /widgets/text/text.coffee: -------------------------------------------------------------------------------- 1 | class Dashing.Text extends Dashing.Widget 2 | -------------------------------------------------------------------------------- /widgets/iframe/iframe.html: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FlorianZ/hadashboard/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /widgets/clock/clock.html: -------------------------------------------------------------------------------- 1 |

2 |

-------------------------------------------------------------------------------- /assets/images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FlorianZ/hadashboard/HEAD/assets/images/logo.png -------------------------------------------------------------------------------- /widgets/image/image.html: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /widgets/reload/reload.html: -------------------------------------------------------------------------------- 1 |

Reload

2 | 3 |

-------------------------------------------------------------------------------- /widgets/stmode/stmode.html: -------------------------------------------------------------------------------- 1 |

2 | 3 |

4 | -------------------------------------------------------------------------------- /assets/fonts/climacons-webfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FlorianZ/hadashboard/HEAD/assets/fonts/climacons-webfont.eot -------------------------------------------------------------------------------- /assets/fonts/climacons-webfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FlorianZ/hadashboard/HEAD/assets/fonts/climacons-webfont.ttf -------------------------------------------------------------------------------- /assets/fonts/climacons-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FlorianZ/hadashboard/HEAD/assets/fonts/climacons-webfont.woff -------------------------------------------------------------------------------- /assets/fonts/fontawesome-webfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FlorianZ/hadashboard/HEAD/assets/fonts/fontawesome-webfont.eot -------------------------------------------------------------------------------- /assets/fonts/fontawesome-webfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FlorianZ/hadashboard/HEAD/assets/fonts/fontawesome-webfont.ttf -------------------------------------------------------------------------------- /assets/fonts/fontawesome-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FlorianZ/hadashboard/HEAD/assets/fonts/fontawesome-webfont.woff -------------------------------------------------------------------------------- /widgets/changepage/changepage.html: -------------------------------------------------------------------------------- 1 |

2 | 3 |

-------------------------------------------------------------------------------- /widgets/iframe/iframe.scss: -------------------------------------------------------------------------------- 1 | .widget-iframe { 2 | padding: 3px 0px 0px 0px !important; 3 | 4 | iframe { 5 | width: 100%; 6 | height: 100%; 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /widgets/stswitch/stswitch.html: -------------------------------------------------------------------------------- 1 |

2 | 3 |

-------------------------------------------------------------------------------- /widgets/stlock/stlock.html: -------------------------------------------------------------------------------- 1 |

2 | 3 |

4 | -------------------------------------------------------------------------------- /widgets/stmotion/stmotion.html: -------------------------------------------------------------------------------- 1 |

2 | 3 |

4 | -------------------------------------------------------------------------------- /widgets/sttemp/sttemp.html: -------------------------------------------------------------------------------- 1 |

2 | 3 |

4 |

°F

5 | -------------------------------------------------------------------------------- /widgets/stcontact/stcontact.html: -------------------------------------------------------------------------------- 1 |

2 | 3 |

4 | -------------------------------------------------------------------------------- /widgets/sthumidity/sthumidity.html: -------------------------------------------------------------------------------- 1 |

2 | 3 |

4 |

%

5 | -------------------------------------------------------------------------------- /widgets/stpresence/stpresence.html: -------------------------------------------------------------------------------- 1 |

2 | 3 |

4 | -------------------------------------------------------------------------------- /widgets/reload/reload.coffee: -------------------------------------------------------------------------------- 1 | class Dashing.Reload extends Dashing.ClickableWidget 2 | 3 | ready: -> 4 | 5 | onData: (data) -> 6 | 7 | onClick: (event) -> 8 | Dashing.fire 'reload' 9 | -------------------------------------------------------------------------------- /widgets/list/list.coffee: -------------------------------------------------------------------------------- 1 | class Dashing.List extends Dashing.Widget 2 | ready: -> 3 | if @get('unordered') 4 | $(@node).find('ol').remove() 5 | else 6 | $(@node).find('ul').remove() 7 | -------------------------------------------------------------------------------- /widgets/graph/graph.html: -------------------------------------------------------------------------------- 1 |

2 | 3 |

4 | 5 |

6 | -------------------------------------------------------------------------------- /widgets/text/text.html: -------------------------------------------------------------------------------- 1 |

2 | 3 |

4 | 5 |

6 | 7 |

8 | -------------------------------------------------------------------------------- /jobs/heartbeat.rb: -------------------------------------------------------------------------------- 1 | # Send a heartbeat message to keep the event stream open 2 | SCHEDULER.every '15s', :first_in => 0 do |job| 3 | event = ":heartbeat #{Time.now.to_i}\n\n" 4 | Sinatra::Application.settings.connections.each { |out| out << event } 5 | end 6 | -------------------------------------------------------------------------------- /widgets/stmeter/stmeter.html: -------------------------------------------------------------------------------- 1 |

2 | 3 | 4 | -------------------------------------------------------------------------------- /widgets/comments/comments.html: -------------------------------------------------------------------------------- 1 |

2 |
3 |

4 |

5 |
6 | 7 |

8 | -------------------------------------------------------------------------------- /widgets/reload/reload.scss: -------------------------------------------------------------------------------- 1 | // ---------------------------------------------------------------------------- 2 | // Widget styles 3 | // ---------------------------------------------------------------------------- 4 | .widget-reload { 5 | 6 | background-color: #444 !important; 7 | 8 | .title { 9 | color: #fff; 10 | } 11 | 12 | } -------------------------------------------------------------------------------- /widgets/stmode/stmode.scss: -------------------------------------------------------------------------------- 1 | // ---------------------------------------------------------------------------- 2 | // Widget-stmode styles 3 | // ---------------------------------------------------------------------------- 4 | .widget-stmode { 5 | 6 | .title { 7 | color: #fff; 8 | } 9 | 10 | .mode { 11 | color: #fff; 12 | } 13 | 14 | } 15 | -------------------------------------------------------------------------------- /widgets/stmodechange/stmodechange.html: -------------------------------------------------------------------------------- 1 |

2 | 3 |
4 |
5 |

6 |
7 |
8 |

9 |
10 |
11 | -------------------------------------------------------------------------------- /widgets/image/image.coffee: -------------------------------------------------------------------------------- 1 | class Dashing.Image extends Dashing.Widget 2 | 3 | ready: -> 4 | # This is fired when the widget is done being rendered 5 | 6 | onData: (data) -> 7 | # Handle incoming data 8 | # You can access the html node of this widget with `@node` 9 | # Example: $(@node).fadeOut().fadeIn() will make the node flash each time data comes in. 10 | -------------------------------------------------------------------------------- /widgets/iframe/iframe.coffee: -------------------------------------------------------------------------------- 1 | class Dashing.Iframe extends Dashing.Widget 2 | 3 | ready: -> 4 | # This is fired when the widget is done being rendered 5 | 6 | onData: (data) -> 7 | # Handle incoming data 8 | # You can access the html node of this widget with `@node` 9 | # Example: $(@node).fadeOut().fadeIn() will make the node flash each time data comes in. 10 | -------------------------------------------------------------------------------- /widgets/changepage/changepage.coffee: -------------------------------------------------------------------------------- 1 | class Dashing.Changepage extends Dashing.ClickableWidget 2 | 3 | ready: -> 4 | 5 | onData: (data) -> 6 | 7 | onClick: (node, event) -> 8 | Dashing.cycleDashboardsNow( 9 | boardnumber: @get('page'), 10 | stagger: @get('stagger'), 11 | fastTransition: @get('fasttransition'), 12 | transitiontype: @get('transitiontype')) 13 | -------------------------------------------------------------------------------- /widgets/changepage/changepage.scss: -------------------------------------------------------------------------------- 1 | // ---------------------------------------------------------------------------- 2 | // Widget-change-page styles 3 | // ---------------------------------------------------------------------------- 4 | .widget-change-page { 5 | 6 | background-color: #444 !important; 7 | 8 | .title { 9 | color: rgba(255, 255, 255, 0.7); 10 | } 11 | 12 | } 13 | -------------------------------------------------------------------------------- /widgets/meter/meter.html: -------------------------------------------------------------------------------- 1 |

2 | 3 | 4 | 5 |

6 | 7 |

8 | -------------------------------------------------------------------------------- /widgets/stmeter/stmeter.scss: -------------------------------------------------------------------------------- 1 | // ---------------------------------------------------------------------------- 2 | // Widget-stmeter styles 3 | // ---------------------------------------------------------------------------- 4 | .widget-stmeter { 5 | 6 | input.stmeter { 7 | background-color: #444; 8 | color: #aa00ff; 9 | } 10 | 11 | .title { 12 | color: #fff; 13 | } 14 | 15 | } 16 | -------------------------------------------------------------------------------- /widgets/number/number.html: -------------------------------------------------------------------------------- 1 |

2 | 3 |

4 | 5 |

6 | 7 |

8 | 9 |

10 | 11 |

12 | -------------------------------------------------------------------------------- /assets/stylesheets/climacons-font.css: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: 'Climacons-Font'; 3 | src:url('climacons-webfont.eot'); 4 | src:url('climacons-webfont.eot?#iefix') format('embedded-opentype'), 5 | url('climacons-webfont.svg#Climacons-Font') format('svg'), 6 | url('climacons-webfont.woff') format('woff'), 7 | url('climacons-webfont.ttf') format('truetype'); 8 | font-weight: normal; 9 | font-style: normal; 10 | } -------------------------------------------------------------------------------- /widgets/stcontact/stcontact.scss: -------------------------------------------------------------------------------- 1 | // ---------------------------------------------------------------------------- 2 | // Widget-stcontact styles 3 | // ---------------------------------------------------------------------------- 4 | .widget-stcontact { 5 | 6 | .title { 7 | color: #fff; 8 | } 9 | 10 | .icon-open { 11 | color: #ff00aa; 12 | } 13 | 14 | .icon-closed { 15 | color: #888888; 16 | } 17 | 18 | } 19 | -------------------------------------------------------------------------------- /widgets/stmotion/stmotion.scss: -------------------------------------------------------------------------------- 1 | // ---------------------------------------------------------------------------- 2 | // Widget-stmotion styles 3 | // ---------------------------------------------------------------------------- 4 | .widget-stmotion { 5 | 6 | .title { 7 | color: #fff; 8 | } 9 | 10 | .icon-active { 11 | color: #ff00aa; 12 | } 13 | 14 | .icon-inactive { 15 | color: #888888; 16 | } 17 | 18 | } 19 | -------------------------------------------------------------------------------- /widgets/stpresence/stpresence.scss: -------------------------------------------------------------------------------- 1 | // ---------------------------------------------------------------------------- 2 | // Widget-stpresence styles 3 | // ---------------------------------------------------------------------------- 4 | .widget-stpresence { 5 | 6 | .title { 7 | color: #fff; 8 | } 9 | 10 | .icon-present { 11 | color: #aaff00; 12 | } 13 | 14 | .icon-absent { 15 | color: #888888; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /widgets/stdimmer/stdimmer.html: -------------------------------------------------------------------------------- 1 |

2 |

3 | 4 |
5 | 50 6 |

%

7 | 8 |
9 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | ruby '2.3.3' 3 | 4 | gem 'dashing', '>=1.3.6' 5 | gem 'thor' 6 | 7 | # Authentication 8 | gem 'oa-openid' 9 | gem 'omniauth-heroku' 10 | gem 'oauth2' 11 | 12 | # JSON 13 | gem 'json' 14 | 15 | # Database 16 | gem 'data_mapper' 17 | 18 | # Development 19 | group :development do 20 | gem 'dm-sqlite-adapter' 21 | end 22 | 23 | # Production 24 | group :production do 25 | gem 'dm-postgres-adapter' 26 | end 27 | -------------------------------------------------------------------------------- /widgets/stlock/stlock.scss: -------------------------------------------------------------------------------- 1 | // ---------------------------------------------------------------------------- 2 | // Widget styles 3 | // ---------------------------------------------------------------------------- 4 | .widget-stlock { 5 | 6 | background-color: #444 !important; 7 | 8 | .title { 9 | color: #fff; 10 | } 11 | 12 | .icon-unlocked { 13 | color: #ff00aa; 14 | } 15 | 16 | .icon-locked { 17 | color: #fff; 18 | } 19 | 20 | } 21 | -------------------------------------------------------------------------------- /widgets/meter/meter.coffee: -------------------------------------------------------------------------------- 1 | class Dashing.Meter extends Dashing.Widget 2 | 3 | @accessor 'value', Dashing.AnimatedValue 4 | 5 | constructor: -> 6 | super 7 | @observe 'value', (value) -> 8 | $(@node).find(".meter").val(value).trigger('change') 9 | 10 | ready: -> 11 | meter = $(@node).find(".meter") 12 | meter.attr("data-bgcolor", meter.css("background-color")) 13 | meter.attr("data-fgcolor", meter.css("color")) 14 | meter.knob() 15 | -------------------------------------------------------------------------------- /widgets/clock/clock.scss: -------------------------------------------------------------------------------- 1 | // ---------------------------------------------------------------------------- 2 | // Sass declarations 3 | // ---------------------------------------------------------------------------- 4 | 5 | // ---------------------------------------------------------------------------- 6 | // Widget-clock styles 7 | // ---------------------------------------------------------------------------- 8 | .widget-clock { 9 | 10 | .time { 11 | color: #aa00ff; 12 | } 13 | 14 | } 15 | -------------------------------------------------------------------------------- /widgets/stmode/stmode.coffee: -------------------------------------------------------------------------------- 1 | class Dashing.Stmode extends Dashing.Widget 2 | constructor: -> 3 | super 4 | @queryState() 5 | 6 | @accessor 'mode', 7 | get: -> @_mode ? "Unknown" 8 | set: (key, value) -> @_mode = value 9 | 10 | queryState: -> 11 | $.get '/smartthings/dispatch', 12 | widgetId: @get('id'), 13 | deviceType: 'mode' 14 | (data) => 15 | json = JSON.parse data 16 | @set 'mode', json.mode 17 | 18 | ready: -> 19 | 20 | onData: (data) -> 21 | -------------------------------------------------------------------------------- /widgets/image/image.scss: -------------------------------------------------------------------------------- 1 | // ---------------------------------------------------------------------------- 2 | // Sass declarations 3 | // ---------------------------------------------------------------------------- 4 | $background-color: #4b4b4b; 5 | 6 | // ---------------------------------------------------------------------------- 7 | // Widget-image styles 8 | // ---------------------------------------------------------------------------- 9 | .widget-image { 10 | 11 | background-color: $background-color; 12 | 13 | } 14 | -------------------------------------------------------------------------------- /lib/settings.rb: -------------------------------------------------------------------------------- 1 | require 'data_mapper' 2 | 3 | # Initialize the DataMapper to use a database, if available. Fall back to an 4 | # sqlite file, if no database has been set up. 5 | DataMapper.setup(:default, ENV['DATABASE_URL'] || 'sqlite:persistent.db') 6 | 7 | # Set object model for settings 8 | class Setting 9 | include DataMapper::Resource 10 | 11 | property :name, String, :key => true 12 | property :value, Text 13 | end 14 | 15 | # Finalize all models 16 | DataMapper.finalize 17 | 18 | # Up-migrate the schema 19 | DataMapper.auto_upgrade! 20 | -------------------------------------------------------------------------------- /widgets/stswitch/stswitch.scss: -------------------------------------------------------------------------------- 1 | // ---------------------------------------------------------------------------- 2 | // Widget styles 3 | // ---------------------------------------------------------------------------- 4 | .widget-stswitch { 5 | 6 | background-color: #444 !important; 7 | 8 | .title { 9 | color: #fff; 10 | } 11 | 12 | .switch-icon-off .switch-icon-on { 13 | font-size: 150%; 14 | } 15 | 16 | .switch-icon-off { 17 | color: #888888; 18 | } 19 | 20 | .switch-icon-on { 21 | color: #aaff00; 22 | } 23 | 24 | } 25 | -------------------------------------------------------------------------------- /widgets/sttemp/sttemp.coffee: -------------------------------------------------------------------------------- 1 | class Dashing.Sttemp extends Dashing.Widget 2 | constructor: -> 3 | super 4 | @queryState() 5 | 6 | @accessor 'value', 7 | get: -> if @_value then Math.floor(@_value) else 0 8 | set: (key, value) -> @_value = value 9 | 10 | queryState: -> 11 | $.get '/smartthings/dispatch', 12 | widgetId: @get('id'), 13 | deviceType: 'temperature', 14 | deviceId: @get('device') 15 | (data) => 16 | json = JSON.parse data 17 | @set 'value', json.value 18 | 19 | ready: -> 20 | 21 | onData: (data) -> 22 | -------------------------------------------------------------------------------- /widgets/list/list.html: -------------------------------------------------------------------------------- 1 |

2 | 3 |
    4 |
  1. 5 | 6 | 7 |
  2. 8 |
9 | 10 | 16 | 17 |

18 |

19 | -------------------------------------------------------------------------------- /widgets/sthumidity/sthumidity.coffee: -------------------------------------------------------------------------------- 1 | class Dashing.Sthumidity extends Dashing.Widget 2 | constructor: -> 3 | super 4 | @queryState() 5 | 6 | @accessor 'value', 7 | get: -> if @_value then Math.floor(@_value) else 0 8 | set: (key, value) -> @_value = value 9 | 10 | queryState: -> 11 | $.get '/smartthings/dispatch', 12 | widgetId: @get('id'), 13 | deviceType: 'humidity', 14 | deviceId: @get('device') 15 | (data) => 16 | json = JSON.parse data 17 | @set 'value', json.value 18 | 19 | ready: -> 20 | 21 | onData: (data) -> 22 | -------------------------------------------------------------------------------- /widgets/sttemp/sttemp.scss: -------------------------------------------------------------------------------- 1 | // ---------------------------------------------------------------------------- 2 | // Widget-sttemp styles 3 | // ---------------------------------------------------------------------------- 4 | .widget-sttemp { 5 | 6 | .title { 7 | color: #fff; 8 | } 9 | 10 | .value { 11 | color: #00aaff; 12 | display: inline-block; 13 | vertical-align: middle; 14 | } 15 | 16 | .unit { 17 | color: #00aaff; 18 | font-size: 250%; 19 | font-weight: 400; 20 | display: inline-block; 21 | vertical-align: top; 22 | margin-left: 5px; 23 | margin-top: 5px; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /widgets/clock/clock.coffee: -------------------------------------------------------------------------------- 1 | class Dashing.Clock extends Dashing.Widget 2 | 3 | ready: -> 4 | setInterval(@startTime, 500) 5 | 6 | startTime: => 7 | today = new Date() 8 | 9 | h = today.getHours() 10 | m = today.getMinutes() 11 | m = @formatTime(m) 12 | @set('time', @formatHours(h) + ":" + m + " " + @formatAmPm(h)) 13 | @set('date', today.toLocaleDateString()) 14 | 15 | formatTime: (i) -> 16 | if i < 10 then "0" + i else i 17 | 18 | formatAmPm: (h) -> 19 | if h >= 12 then "PM" else "AM" 20 | 21 | formatHours: (h) -> 22 | if h > 12 23 | h - 12 24 | else if h == 0 25 | 12 26 | else 27 | h -------------------------------------------------------------------------------- /widgets/comments/comments.coffee: -------------------------------------------------------------------------------- 1 | class Dashing.Comments extends Dashing.Widget 2 | 3 | @accessor 'quote', -> 4 | "“#{@get('current_comment')?.body}”" 5 | 6 | ready: -> 7 | @currentIndex = 0 8 | @commentElem = $(@node).find('.comment-container') 9 | @nextComment() 10 | @startCarousel() 11 | 12 | onData: (data) -> 13 | @currentIndex = 0 14 | 15 | startCarousel: -> 16 | setInterval(@nextComment, 8000) 17 | 18 | nextComment: => 19 | comments = @get('comments') 20 | if comments 21 | @commentElem.fadeOut => 22 | @currentIndex = (@currentIndex + 1) % comments.length 23 | @set 'current_comment', comments[@currentIndex] 24 | @commentElem.fadeIn() 25 | -------------------------------------------------------------------------------- /widgets/stmeter/stmeter.coffee: -------------------------------------------------------------------------------- 1 | class Dashing.Stmeter extends Dashing.Widget 2 | constructor: -> 3 | super 4 | @queryState() 5 | @observe 'value', (value) -> 6 | $(@node).find(".stmeter").val(value).trigger('change') 7 | 8 | @accessor 'value', Dashing.AnimatedValue 9 | 10 | queryState: -> 11 | $.get '/smartthings/dispatch', 12 | widgetId: @get('id'), 13 | deviceType: 'power', 14 | deviceId: @get('device') 15 | (data) => 16 | json = JSON.parse data 17 | @set 'value', json.value 18 | 19 | ready: -> 20 | stmeter = $(@node).find(".stmeter") 21 | stmeter.attr("data-bgcolor", stmeter.css("background-color")) 22 | stmeter.attr("data-fgcolor", stmeter.css("color")) 23 | stmeter.knob() 24 | 25 | onData: (data) -> -------------------------------------------------------------------------------- /public/404.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | This Dashboard doesn't exist. 5 | 17 | 18 | 19 | 20 | 21 |
22 |

Drats! That Dashboard doesn't exist.

23 |

You may have mistyped the address or the page may have moved.

24 |
25 | 26 | -------------------------------------------------------------------------------- /widgets/stmodechange/stmodechange.scss: -------------------------------------------------------------------------------- 1 | // ---------------------------------------------------------------------------- 2 | // Widget-stmode styles 3 | // ---------------------------------------------------------------------------- 4 | .widget-stmodechange { 5 | 6 | background-color: #444 !important; 7 | 8 | .title { 9 | color: #fff; 10 | } 11 | 12 | i { 13 | font-style: normal; 14 | } 15 | 16 | .container { 17 | position: relative; 18 | height: 75px; 19 | } 20 | 21 | .icon, .timer { 22 | position: absolute; 23 | top: 0; 24 | left: 0; 25 | width: 100%; 26 | height: 100%; 27 | } 28 | 29 | .timer { 30 | z-index: 1; 31 | } 32 | 33 | .icon-inactive { 34 | color: #888888; 35 | } 36 | 37 | .icon-active { 38 | color: #aaff00; 39 | } 40 | 41 | } 42 | -------------------------------------------------------------------------------- /widgets/stcontact/stcontact.coffee: -------------------------------------------------------------------------------- 1 | class Dashing.Stcontact extends Dashing.Widget 2 | constructor: -> 3 | super 4 | @queryState() 5 | 6 | @accessor 'state', 7 | get: -> @_state ? "Unknown" 8 | set: (key, value) -> @_state = value 9 | 10 | @accessor 'icon', 11 | get: -> if @get('state') == 'open' then 'expand' else 'compress' 12 | set: Batman.Property.defaultAccessor.set 13 | 14 | @accessor 'icon-style', -> 15 | if @get('state') == 'open' then 'icon-open' else 'icon-closed' 16 | 17 | queryState: -> 18 | $.get '/smartthings/dispatch', 19 | widgetId: @get('id'), 20 | deviceType: 'contact', 21 | deviceId: @get('device') 22 | (data) => 23 | json = JSON.parse data 24 | @set 'state', json.state 25 | 26 | ready: -> 27 | 28 | onData: (data) -> 29 | -------------------------------------------------------------------------------- /widgets/stmotion/stmotion.coffee: -------------------------------------------------------------------------------- 1 | class Dashing.Stmotion extends Dashing.Widget 2 | constructor: -> 3 | super 4 | @queryState() 5 | 6 | @accessor 'state', 7 | get: -> @_state ? "Unknown" 8 | set: (key, value) -> @_state = value 9 | 10 | @accessor 'icon', 11 | get: -> if @get('state') == 'active' then 'exchange' else 'reorder' 12 | set: Batman.Property.defaultAccessor.set 13 | 14 | @accessor 'icon-style', -> 15 | if @get('state') == 'active' then 'icon-active' else 'icon-inactive' 16 | 17 | queryState: -> 18 | $.get '/smartthings/dispatch', 19 | widgetId: @get('id'), 20 | deviceType: 'motion', 21 | deviceId: @get('device') 22 | (data) => 23 | json = JSON.parse data 24 | @set 'state', json.state 25 | 26 | ready: -> 27 | 28 | onData: (data) -> 29 | -------------------------------------------------------------------------------- /widgets/stpresence/stpresence.coffee: -------------------------------------------------------------------------------- 1 | class Dashing.Stpresence extends Dashing.Widget 2 | constructor: -> 3 | super 4 | @queryState() 5 | 6 | @accessor 'state', 7 | get: -> @_state ? "Unknown" 8 | set: (key, value) -> @_state = value 9 | 10 | @accessor 'icon', 11 | get: -> if @get('state') == 'present' then 'user' else 'times' 12 | set: Batman.Property.defaultAccessor.set 13 | 14 | @accessor 'icon-style', -> 15 | if @get('state') == 'present' then 'icon-present' else 'icon-absent' 16 | 17 | queryState: -> 18 | $.get '/smartthings/dispatch', 19 | widgetId: @get('id'), 20 | deviceType: 'presence', 21 | deviceId: @get('device') 22 | (data) => 23 | json = JSON.parse data 24 | @set 'state', json.state 25 | 26 | ready: -> 27 | 28 | onData: (data) -> 29 | -------------------------------------------------------------------------------- /widgets/sthumidity/sthumidity.scss: -------------------------------------------------------------------------------- 1 | // ---------------------------------------------------------------------------- 2 | // Sass declarations 3 | // ---------------------------------------------------------------------------- 4 | 5 | // ---------------------------------------------------------------------------- 6 | // Widget-sthumidity styles 7 | // ---------------------------------------------------------------------------- 8 | .widget-sthumidity { 9 | 10 | .title { 11 | color: #fff 12 | } 13 | 14 | .value { 15 | color: #00aaff; 16 | display: inline-block; 17 | vertical-align: middle; 18 | } 19 | 20 | .unit { 21 | color: #00aaff; 22 | font-size: 250%; 23 | font-weight: 400; 24 | display: inline-block; 25 | vertical-align: top; 26 | margin-left: 5px; 27 | margin-top: 5px; 28 | } 29 | 30 | 31 | } 32 | -------------------------------------------------------------------------------- /widgets/text/text.scss: -------------------------------------------------------------------------------- 1 | // ---------------------------------------------------------------------------- 2 | // Sass declarations 3 | // ---------------------------------------------------------------------------- 4 | $background-color: #ec663c; 5 | 6 | $title-color: rgba(255, 255, 255, 0.7); 7 | $moreinfo-color: rgba(255, 255, 255, 0.7); 8 | 9 | // ---------------------------------------------------------------------------- 10 | // Widget-text styles 11 | // ---------------------------------------------------------------------------- 12 | .widget-text { 13 | 14 | background-color: $background-color; 15 | 16 | .title { 17 | color: $title-color; 18 | } 19 | 20 | .more-info { 21 | color: $moreinfo-color; 22 | } 23 | 24 | .updated-at { 25 | color: rgba(255, 255, 255, 0.7); 26 | } 27 | 28 | 29 | &.large h3 { 30 | font-size: 325%; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /widgets/comments/comments.scss: -------------------------------------------------------------------------------- 1 | // ---------------------------------------------------------------------------- 2 | // Sass declarations 3 | // ---------------------------------------------------------------------------- 4 | $background-color: #eb9c3c; 5 | 6 | $title-color: rgba(255, 255, 255, 0.7); 7 | $moreinfo-color: rgba(255, 255, 255, 0.7); 8 | 9 | // ---------------------------------------------------------------------------- 10 | // Widget-comment styles 11 | // ---------------------------------------------------------------------------- 12 | .widget-comments { 13 | 14 | background-color: $background-color; 15 | 16 | .title { 17 | color: $title-color; 18 | margin-bottom: 15px; 19 | } 20 | 21 | .name { 22 | padding-left: 5px; 23 | } 24 | 25 | .comment-container { 26 | display: none; 27 | } 28 | 29 | .more-info { 30 | color: $moreinfo-color; 31 | } 32 | 33 | } 34 | -------------------------------------------------------------------------------- /assets/javascripts/clickablewidget.coffee: -------------------------------------------------------------------------------- 1 | class Dashing.ClickableWidget extends Dashing.Widget 2 | constructor: -> 3 | super 4 | $(@node).on 'click', (evt) => @handleClick evt 5 | $(@node).on 'touchstart', (evt) => @handleTouchStart evt 6 | $(@node).on 'touchmove', (evt) => @handleTouchMove evt 7 | $(@node).on 'touchend', (evt) => @handleTouchEnd evt 8 | 9 | handleClick: (evt) -> 10 | @onClick evt 11 | 12 | handleTouchStart: (evt) -> 13 | evt.preventDefault() 14 | @onTouchStart evt 15 | 16 | handleTouchMove: (evt) -> 17 | @onTouchMove evt 18 | 19 | handleTouchEnd: (evt) -> 20 | @onTouchEnd evt 21 | @onClick evt 22 | 23 | onClick: (evt) -> 24 | # override for click events 25 | 26 | onTouchStart: (evt) -> 27 | # override for touchstart events 28 | 29 | onTouchMove: (evt) -> 30 | # override for touchmove events 31 | 32 | onTouchEnd: (evt) -> 33 | # override for touchend events 34 | -------------------------------------------------------------------------------- /widgets/meter/meter.scss: -------------------------------------------------------------------------------- 1 | // ---------------------------------------------------------------------------- 2 | // Sass declarations 3 | // ---------------------------------------------------------------------------- 4 | $background-color: #9c4274; 5 | 6 | $title-color: rgba(255, 255, 255, 0.7); 7 | $moreinfo-color: rgba(255, 255, 255, 0.3); 8 | 9 | $meter-background: darken($background-color, 15%); 10 | 11 | // ---------------------------------------------------------------------------- 12 | // Widget-meter styles 13 | // ---------------------------------------------------------------------------- 14 | .widget-meter { 15 | 16 | background-color: $background-color; 17 | 18 | input.meter { 19 | background-color: $meter-background; 20 | color: #fff; 21 | } 22 | 23 | .title { 24 | color: $title-color; 25 | } 26 | 27 | .more-info { 28 | color: $moreinfo-color; 29 | } 30 | 31 | .updated-at { 32 | color: rgba(0, 0, 0, 0.3); 33 | } 34 | 35 | } 36 | -------------------------------------------------------------------------------- /widgets/stweather/stweather.coffee: -------------------------------------------------------------------------------- 1 | class Dashing.Stweather extends Dashing.Widget 2 | constructor: -> 3 | super 4 | @_icons = 5 | chanceflurries: '', 6 | chancerain: '', 7 | chancesleet: '', 8 | chancesnow: '', 9 | chancetstorms: '', 10 | clear: '', 11 | cloudy: '', 12 | flurries: '', 13 | fog: '', 14 | hazy: '', 15 | mostlycloudy: '', 16 | mostlysunny: '', 17 | partlycloudy: '', 18 | partlysunny: '', 19 | sleet: '', 20 | rain: '', 21 | snow: '', 22 | sunny: '', 23 | tstorms: '' 24 | 25 | @accessor 'climacon', -> 26 | new Batman.TerminalAccessible (attr) => 27 | @_icons[attr] 28 | 29 | @accessor 'now_temp', 30 | get: -> if @_temp then Math.floor(@_temp) else 0 31 | set: (key, value) -> @_temp = value 32 | 33 | ready: -> 34 | 35 | onData: (data) -> 36 | -------------------------------------------------------------------------------- /widgets/number/number.scss: -------------------------------------------------------------------------------- 1 | // ---------------------------------------------------------------------------- 2 | // Sass declarations 3 | // ---------------------------------------------------------------------------- 4 | $background-color: #47bbb3; 5 | $value-color: #fff; 6 | 7 | $title-color: rgba(255, 255, 255, 0.7); 8 | $moreinfo-color: rgba(255, 255, 255, 0.7); 9 | 10 | // ---------------------------------------------------------------------------- 11 | // Widget-number styles 12 | // ---------------------------------------------------------------------------- 13 | .widget-number { 14 | 15 | background-color: $background-color; 16 | 17 | .title { 18 | color: $title-color; 19 | } 20 | 21 | .value { 22 | color: $value-color; 23 | } 24 | 25 | .change-rate { 26 | font-weight: 500; 27 | font-size: 150%; 28 | color: $value-color; 29 | } 30 | 31 | .more-info { 32 | color: $moreinfo-color; 33 | } 34 | 35 | .updated-at { 36 | color: rgba(0, 0, 0, 0.3); 37 | } 38 | 39 | } 40 | -------------------------------------------------------------------------------- /widgets/stweather/stweather.html: -------------------------------------------------------------------------------- 1 |

Today

2 |

3 | 4 |

5 |

°F

6 | 7 |
8 | 9 |

10 |

/

11 |

12 | 13 |

14 |

15 | 16 |
17 | 18 |

Tomorrow

19 |

20 | 21 |

22 |

/

23 |

24 | 25 |

26 |

27 | -------------------------------------------------------------------------------- /assets/javascripts/gridster/jquery.leanModal.min.js: -------------------------------------------------------------------------------- 1 | // leanModal v1.1 by Ray Stone - http://finelysliced.com.au 2 | // Dual licensed under the MIT and GPL 3 | 4 | (function($){$.fn.extend({leanModal:function(options){var defaults={top:100,overlay:0.5,closeButton:null};var overlay=$("
");$("body").append(overlay);options=$.extend(defaults,options);return this.each(function(){var o=options;$(this).click(function(e){var modal_id=$(this).attr("href");$("#lean_overlay").click(function(){close_modal(modal_id)});$(o.closeButton).click(function(){close_modal(modal_id)});var modal_height=$(modal_id).outerHeight();var modal_width=$(modal_id).outerWidth(); 5 | $("#lean_overlay").css({"display":"block",opacity:0});$("#lean_overlay").fadeTo(200,o.overlay);$(modal_id).css({"display":"block","position":"fixed","opacity":0,"z-index":11000,"left":50+"%","margin-left":-(modal_width/2)+"px","top":o.top+"px"});$(modal_id).fadeTo(200,1);e.preventDefault()})});function close_modal(modal_id){$("#lean_overlay").fadeOut(200);$(modal_id).css({"display":"none"})}}})})(jQuery); 6 | -------------------------------------------------------------------------------- /widgets/number/number.coffee: -------------------------------------------------------------------------------- 1 | class Dashing.Number extends Dashing.Widget 2 | @accessor 'current', Dashing.AnimatedValue 3 | 4 | @accessor 'difference', -> 5 | if @get('last') 6 | last = parseInt(@get('last')) 7 | current = parseInt(@get('current')) 8 | if last != 0 9 | diff = Math.abs(Math.round((current - last) / last * 100)) 10 | "#{diff}%" 11 | else 12 | "" 13 | 14 | @accessor 'arrow', -> 15 | if @get('last') 16 | if parseInt(@get('current')) > parseInt(@get('last')) then 'icon-arrow-up' else 'icon-arrow-down' 17 | 18 | onData: (data) -> 19 | if data.status 20 | # clear existing "status-*" classes 21 | $(@get('node')).attr 'class', (i,c) -> 22 | c.replace /\bstatus-\S+/g, '' 23 | # add new class 24 | $(@get('node')).addClass "status-#{data.status}" 25 | 26 | onClick: (node, event) -> 27 | $.post '/smartthings/dispatch', 28 | widgetId: @get('id'), 29 | deviceId: 'Console Lamp', 30 | command: 'toggle' 31 | (data) -> 32 | alert data 33 | -------------------------------------------------------------------------------- /widgets/graph/graph.coffee: -------------------------------------------------------------------------------- 1 | class Dashing.Graph extends Dashing.Widget 2 | 3 | @accessor 'current', -> 4 | return @get('displayedValue') if @get('displayedValue') 5 | points = @get('points') 6 | if points 7 | points[points.length - 1].y 8 | 9 | ready: -> 10 | container = $(@node).parent() 11 | # Gross hacks. Let's fix this. 12 | width = (Dashing.widget_base_dimensions[0] * container.data("sizex")) + Dashing.widget_margins[0] * 2 * (container.data("sizex") - 1) 13 | height = (Dashing.widget_base_dimensions[1] * container.data("sizey")) 14 | @graph = new Rickshaw.Graph( 15 | element: @node 16 | width: width 17 | height: height 18 | renderer: @get("graphtype") 19 | series: [ 20 | { 21 | color: "#fff", 22 | data: [{x:0, y:0}] 23 | } 24 | ] 25 | ) 26 | 27 | @graph.series[0].data = @get('points') if @get('points') 28 | 29 | x_axis = new Rickshaw.Graph.Axis.Time(graph: @graph) 30 | y_axis = new Rickshaw.Graph.Axis.Y(graph: @graph, tickFormat: Rickshaw.Fixtures.Number.formatKMBT) 31 | @graph.render() 32 | 33 | onData: (data) -> 34 | if @graph 35 | @graph.series[0].data = data.points 36 | @graph.render() 37 | -------------------------------------------------------------------------------- /widgets/stlock/stlock.coffee: -------------------------------------------------------------------------------- 1 | class Dashing.Stlock extends Dashing.ClickableWidget 2 | constructor: -> 3 | super 4 | @queryState() 5 | 6 | @accessor 'state', 7 | get: -> @_state ? 'unlocked' 8 | set: (key, value) -> @_state = value 9 | 10 | @accessor 'icon', 11 | get: -> if @get('state') == 'unlocked' then 'unlock-alt' else 'lock' 12 | set: Batman.Property.defaultAccessor.set 13 | 14 | @accessor 'icon-style', -> 15 | if @get('state') == 'locked' then 'icon-locked' else 'icon-unlocked' 16 | 17 | toggleState: -> 18 | newState = if @get('state') == 'locked' then 'unlock' else 'lock' 19 | @set 'state', newState 20 | return newState 21 | 22 | queryState: -> 23 | $.get '/smartthings/dispatch', 24 | widgetId: @get('id'), 25 | deviceType: 'lock', 26 | deviceId: @get('device') 27 | (data) => 28 | json = JSON.parse data 29 | @set 'state', json.state 30 | 31 | postState: -> 32 | newState = @toggleState() 33 | $.post '/smartthings/dispatch', 34 | deviceType: 'lock', 35 | deviceId: @get('device'), 36 | command: newState, 37 | (data) => 38 | json = JSON.parse data 39 | if json.error != 0 40 | @toggleState() 41 | 42 | ready: -> 43 | 44 | onData: (data) -> 45 | 46 | onClick: (event) -> 47 | @postState() 48 | -------------------------------------------------------------------------------- /assets/javascripts/application.coffee: -------------------------------------------------------------------------------- 1 | # Use the Yaffle EventSource polyfill to work around browser issues 2 | #= require eventsource.min.js 3 | 4 | # dashing.js is located in the dashing framework 5 | # It includes jquery & batman for you. 6 | #= require dashing.js 7 | 8 | #= require_directory . 9 | #= require_tree ../../widgets 10 | 11 | console.log("Yeah! The dashboard has started!") 12 | 13 | Dashing.on 'ready', -> 14 | Dashing.widget_margins ||= [5, 5] 15 | Dashing.widget_base_dimensions ||= [145, 145] 16 | Dashing.numColumns ||= 7 17 | Dashing.cycleDashboards({timeInSeconds: 0, stagger: true, page: 1}); 18 | 19 | contentWidth = (Dashing.widget_base_dimensions[0] + Dashing.widget_margins[0] * 2) * Dashing.numColumns 20 | 21 | Batman.setImmediate -> 22 | $('.gridster').width(contentWidth) 23 | $('.gridster > ul').gridster 24 | widget_margins: Dashing.widget_margins 25 | widget_base_dimensions: Dashing.widget_base_dimensions 26 | avoid_overlapped_widgets: !Dashing.customGridsterLayout 27 | draggable: 28 | stop: Dashing.showGridsterInstructions 29 | start: -> Dashing.currentWidgetPositions = Dashing.getWidgetPositions() 30 | if( /Android|webOS|iPhone|iPad|iPod|BlackBerry/i.test(navigator.userAgent) ) 31 | $('.gridster > ul').each -> 32 | $(@).gridster().data('gridster').draggable().disable() 33 | -------------------------------------------------------------------------------- /widgets/list/list.scss: -------------------------------------------------------------------------------- 1 | // ---------------------------------------------------------------------------- 2 | // Sass declarations 3 | // ---------------------------------------------------------------------------- 4 | $background-color: #12b0c5; 5 | $value-color: #fff; 6 | 7 | $title-color: rgba(255, 255, 255, 0.7); 8 | $label-color: rgba(255, 255, 255, 0.7); 9 | $moreinfo-color: rgba(255, 255, 255, 0.7); 10 | 11 | // ---------------------------------------------------------------------------- 12 | // Widget-list styles 13 | // ---------------------------------------------------------------------------- 14 | .widget-list { 15 | 16 | background-color: $background-color; 17 | vertical-align: top; 18 | 19 | .title { 20 | color: $title-color; 21 | } 22 | 23 | ol, ul { 24 | margin: 0 15px; 25 | text-align: left; 26 | color: $label-color; 27 | } 28 | 29 | ol { 30 | list-style-position: inside; 31 | } 32 | 33 | li { 34 | margin-bottom: 5px; 35 | } 36 | 37 | .list-nostyle { 38 | list-style: none; 39 | } 40 | 41 | .label { 42 | color: $label-color; 43 | } 44 | 45 | .value { 46 | float: right; 47 | margin-left: 12px; 48 | font-weight: 600; 49 | color: $value-color; 50 | } 51 | 52 | .updated-at { 53 | color: rgba(0, 0, 0, 0.3); 54 | } 55 | 56 | .more-info { 57 | color: $moreinfo-color; 58 | } 59 | 60 | } 61 | -------------------------------------------------------------------------------- /assets/javascripts/dashing.gridster.coffee: -------------------------------------------------------------------------------- 1 | #= require_directory ./gridster 2 | 3 | # This file enables gridster integration (http://gridster.net/) 4 | # Delete it if you'd rather handle the layout yourself. 5 | # You'll miss out on a lot if you do, but we won't hold it against you. 6 | 7 | Dashing.gridsterLayout = (positions) -> 8 | Dashing.customGridsterLayout = true 9 | positions = positions.replace(/^"|"$/g, '') 10 | positions = $.parseJSON(positions) 11 | widgets = $("[data-row^=]") 12 | for widget, index in widgets 13 | $(widget).attr('data-row', positions[index].row) 14 | $(widget).attr('data-col', positions[index].col) 15 | 16 | Dashing.getWidgetPositions = -> 17 | $(".gridster ul:first").gridster().data('gridster').serialize() 18 | 19 | Dashing.showGridsterInstructions = -> 20 | newWidgetPositions = Dashing.getWidgetPositions() 21 | 22 | unless JSON.stringify(newWidgetPositions) == JSON.stringify(Dashing.currentWidgetPositions) 23 | Dashing.currentWidgetPositions = newWidgetPositions 24 | $('#save-gridster').slideDown() 25 | $('#gridster-code').text(" 26 | 31 | ") 32 | 33 | $ -> 34 | $('#save-gridster').leanModal() 35 | 36 | $('#save-gridster').click -> 37 | $('#save-gridster').slideUp() 38 | -------------------------------------------------------------------------------- /widgets/graph/graph.scss: -------------------------------------------------------------------------------- 1 | // ---------------------------------------------------------------------------- 2 | // Sass declarations 3 | // ---------------------------------------------------------------------------- 4 | $background-color: #dc5945; 5 | 6 | $title-color: rgba(255, 255, 255, 0.7); 7 | $moreinfo-color: rgba(255, 255, 255, 0.3); 8 | $tick-color: rgba(0, 0, 0, 0.4); 9 | 10 | 11 | // ---------------------------------------------------------------------------- 12 | // Widget-graph styles 13 | // ---------------------------------------------------------------------------- 14 | .widget-graph { 15 | 16 | background-color: $background-color; 17 | position: relative; 18 | 19 | 20 | svg { 21 | position: absolute; 22 | opacity: 0.4; 23 | fill-opacity: 0.4; 24 | left: 0px; 25 | top: 0px; 26 | } 27 | 28 | .title, .value { 29 | position: relative; 30 | z-index: 99; 31 | } 32 | 33 | .title { 34 | color: $title-color; 35 | } 36 | 37 | .more-info { 38 | color: $moreinfo-color; 39 | font-weight: 600; 40 | font-size: 100%; 41 | margin-top: 0; 42 | } 43 | 44 | .x_tick { 45 | position: absolute; 46 | bottom: 0; 47 | .title { 48 | font-size: 100%; 49 | color: $tick-color; 50 | opacity: 0.5; 51 | padding-bottom: 3px; 52 | } 53 | } 54 | 55 | .y_ticks { 56 | font-size: 100%; 57 | fill: $tick-color; 58 | fill-opacity: 1; 59 | } 60 | 61 | .domain { 62 | display: none; 63 | } 64 | 65 | } 66 | -------------------------------------------------------------------------------- /dashboards/layout.erb: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | <%= yield_content(:title) %> 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 |
22 | <%= yield %> 23 |
24 | 25 | <% if development? %> 26 |
27 |

Paste the following at the top of <%= params[:dashboard] %>.erb

28 | 29 |
30 | Save this layout 31 | <% end %> 32 | 33 | 34 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /widgets/stswitch/stswitch.coffee: -------------------------------------------------------------------------------- 1 | class Dashing.Stswitch extends Dashing.ClickableWidget 2 | constructor: -> 3 | super 4 | @queryState() 5 | 6 | @accessor 'state', 7 | get: -> @_state ? 'off' 8 | set: (key, value) -> @_state = value 9 | 10 | @accessor 'icon', 11 | get: -> if @['icon'] then @['icon'] else 12 | if @get('state') == 'on' then @get('iconon') else @get('iconoff') 13 | set: Batman.Property.defaultAccessor.set 14 | 15 | @accessor 'iconon', 16 | get: -> @['iconon'] ? 'circle' 17 | set: Batman.Property.defaultAccessor.set 18 | 19 | @accessor 'iconoff', 20 | get: -> @['iconoff'] ? 'circle-thin' 21 | set: Batman.Property.defaultAccessor.set 22 | 23 | @accessor 'icon-style', -> 24 | if @get('state') == 'on' then 'switch-icon-on' else 'switch-icon-off' 25 | 26 | toggleState: -> 27 | newState = if @get('state') == 'on' then 'off' else 'on' 28 | @set 'state', newState 29 | return newState 30 | 31 | queryState: -> 32 | $.get '/smartthings/dispatch', 33 | widgetId: @get('id'), 34 | deviceType: 'switch', 35 | deviceId: @get('device') 36 | (data) => 37 | json = JSON.parse data 38 | @set 'state', json.switch 39 | 40 | postState: -> 41 | newState = @toggleState() 42 | $.post '/smartthings/dispatch', 43 | deviceType: 'switch', 44 | deviceId: @get('device'), 45 | command: newState, 46 | (data) => 47 | json = JSON.parse data 48 | if json.error != 0 49 | @toggleState() 50 | 51 | ready: -> 52 | 53 | onClick: (event) -> 54 | @postState() 55 | -------------------------------------------------------------------------------- /widgets/stdimmer/stdimmer.scss: -------------------------------------------------------------------------------- 1 | // ---------------------------------------------------------------------------- 2 | // Widget styles 3 | // ---------------------------------------------------------------------------- 4 | .widget-stdimmer { 5 | 6 | background-color: #444!important; 7 | position: relative; 8 | line-height: 100%; 9 | 10 | .title { 11 | color: #fff; 12 | margin: 14px 0 20px; 13 | } 14 | 15 | .switch-icon-off .switch-icon-on { 16 | font-size: 150%; 17 | } 18 | 19 | .switch-icon-off { 20 | color: #888888; 21 | } 22 | 23 | .switch-icon-on { 24 | color: #aaff00; 25 | } 26 | 27 | .toggle-area { 28 | z-index: 10; 29 | position: absolute; 30 | top: 0; 31 | left: 0; 32 | width: 100%; 33 | height: 110px; 34 | } 35 | 36 | #slider { 37 | margin-top: 5px; 38 | 39 | #dimmer { 40 | -webkit-appearance: none; 41 | background: #888888; 42 | height: 8px; 43 | } 44 | 45 | #dimmer::-webkit-slider-thumb { 46 | -webkit-appearance: none; 47 | z-index: 1; 48 | background: #888888; 49 | border: 1px solid #444; 50 | width: 11px; 51 | height: 24px; 52 | cursor: pointer; 53 | } 54 | 55 | #level { 56 | display: inline-block; 57 | position: absolute; 58 | top: 95px; 59 | right: 25px; 60 | font-weight: 600; 61 | color: #888888; 62 | } 63 | 64 | .unit { 65 | display: inline-block; 66 | position: absolute; 67 | top: 95px; 68 | right: 13px; 69 | font-size: 10px; 70 | color: #888888; 71 | } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /assets/stylesheets/jquery.gridster.css: -------------------------------------------------------------------------------- 1 | /*! gridster.js - v0.1.0 - 2012-08-14 2 | * http://gridster.net/ 3 | * Copyright (c) 2012 ducksboard; Licensed MIT */ 4 | 5 | .gridster { 6 | position:relative; 7 | } 8 | 9 | .gridster > * { 10 | margin: 0 auto; 11 | -webkit-transition: height .4s; 12 | -moz-transition: height .4s; 13 | -o-transition: height .4s; 14 | -ms-transition: height .4s; 15 | transition: height .4s; 16 | } 17 | 18 | .gridster .gs_w{ 19 | z-index: 2; 20 | position: absolute; 21 | } 22 | 23 | .ready .gs_w:not(.preview-holder) { 24 | -webkit-transition: opacity .3s, left .3s, top .3s; 25 | -moz-transition: opacity .3s, left .3s, top .3s; 26 | -o-transition: opacity .3s, left .3s, top .3s; 27 | transition: opacity .3s, left .3s, top .3s; 28 | } 29 | 30 | .gridster .preview-holder { 31 | z-index: 1; 32 | position: absolute; 33 | background-color: #fff; 34 | border-color: #fff; 35 | opacity: 0.3; 36 | } 37 | 38 | .gridster .player-revert { 39 | z-index: 10!important; 40 | -webkit-transition: left .3s, top .3s!important; 41 | -moz-transition: left .3s, top .3s!important; 42 | -o-transition: left .3s, top .3s!important; 43 | transition: left .3s, top .3s!important; 44 | } 45 | 46 | .gridster .dragging { 47 | z-index: 10!important; 48 | -webkit-transition: all 0s !important; 49 | -moz-transition: all 0s !important; 50 | -o-transition: all 0s !important; 51 | transition: all 0s !important; 52 | } 53 | 54 | /* Uncomment this if you set helper : "clone" in draggable options */ 55 | /*.gridster .player { 56 | opacity:0; 57 | }*/ -------------------------------------------------------------------------------- /widgets/stweather/stweather.scss: -------------------------------------------------------------------------------- 1 | // ---------------------------------------------------------------------------- 2 | // Widget-stmode styles 3 | // ---------------------------------------------------------------------------- 4 | .widget-stweather { 5 | 6 | h1 { 7 | margin-bottom: 0px; 8 | } 9 | 10 | .colored { 11 | color: #ffaa00 !important; 12 | } 13 | 14 | [class^="primary"] { 15 | display: inline-block; 16 | vertical-align: middle; 17 | padding: 0; 18 | margin-left: 10px; 19 | margin-right: 10px; 20 | margin-top: 0px; 21 | margin-bottom: 0px; 22 | } 23 | 24 | [class^="secondary"] { 25 | display: inline-block; 26 | vertical-align: middle; 27 | padding: 0; 28 | margin-left: 0px; 29 | margin-right: 0px; 30 | margin-top: 0px; 31 | margin-bottom: 0px; 32 | color: rgba(255, 255, 255, 0.6); 33 | } 34 | 35 | .primary-climacon { 36 | font-family: "Climacons-Font"; 37 | font-size: 90px; 38 | } 39 | 40 | .primary-info { 41 | font-size: 380%; 42 | font-weight: 400; 43 | } 44 | 45 | .primary-unit { 46 | font-size: 250%; 47 | font-weight: 400; 48 | margin-left: 0; 49 | margin-top: 20px; 50 | vertical-align: top; 51 | } 52 | 53 | .secondary-icon { 54 | font-family: "Climacons-Font"; 55 | font-size: 40px; 56 | margin-left: 10px; 57 | margin-right: 0px; 58 | } 59 | 60 | .secondary-climacon { 61 | font-family: "Climacons-Font"; 62 | font-size: 55px; 63 | margin-left: 10px; 64 | margin-right: 10px; 65 | } 66 | 67 | .secondary-info { 68 | font-size: 170%; 69 | font-weight: 200; 70 | } 71 | 72 | hr { 73 | width: 90%; 74 | border: 0; 75 | height: 1px; 76 | background: rgba(255, 255, 255, 0.25); 77 | margin-top: 15px; 78 | margin-bottom: 15px; 79 | } 80 | 81 | } 82 | -------------------------------------------------------------------------------- /widgets/stdimmer/stdimmer.coffee: -------------------------------------------------------------------------------- 1 | class Dashing.Stdimmer extends Dashing.ClickableWidget 2 | constructor: -> 3 | super 4 | @queryState() 5 | 6 | @accessor 'state', 7 | get: -> @_state ? 'off' 8 | set: (key, value) -> @_state = value 9 | 10 | @accessor 'level', 11 | get: -> @_level ? '50' 12 | set: (key, value) -> @_level = value 13 | 14 | @accessor 'icon', 15 | get: -> if @['icon'] then @['icon'] else 16 | if @get('state') == 'on' then @get('iconon') else @get('iconoff') 17 | set: Batman.Property.defaultAccessor.set 18 | 19 | @accessor 'iconon', 20 | get: -> @['iconon'] ? 'circle' 21 | set: Batman.Property.defaultAccessor.set 22 | 23 | @accessor 'iconoff', 24 | get: -> @['iconoff'] ? 'circle-thin' 25 | set: Batman.Property.defaultAccessor.set 26 | 27 | @accessor 'icon-style', -> 28 | if @get('state') == 'on' then 'switch-icon-on' else 'switch-icon-off' 29 | 30 | @accessor 'stateInverse', -> 31 | if @get('state') == 'on' then 'off' else 'on' 32 | 33 | setLevel: -> 34 | @_level = event.target.value 35 | $.post '/smartthings/dispatch', 36 | deviceType: 'dimmer/level', 37 | deviceId: @get('device'), 38 | command: @_level, 39 | (data) => 40 | json = JSON.parse data 41 | 42 | toggleState: -> 43 | newState = @get 'stateInverse' 44 | @set 'state', newState 45 | return newState 46 | 47 | queryState: -> 48 | $.get '/smartthings/dispatch', 49 | widgetId: @get('id'), 50 | deviceType: 'dimmer', 51 | deviceId: @get('device') 52 | (data) => 53 | json = JSON.parse data 54 | @set 'state', json.state 55 | @set 'level', json.level 56 | 57 | postState: -> 58 | newState = @toggleState() 59 | $.post '/smartthings/dispatch', 60 | deviceType: 'dimmer', 61 | deviceId: @get('device'), 62 | command: newState, 63 | (data) => 64 | json = JSON.parse data 65 | if json.error != 0 66 | @toggleState() 67 | 68 | ready: -> 69 | 70 | onData: (data) -> 71 | 72 | 73 | onClick: (event) -> 74 | if event.target.id == "dimmer" 75 | @setLevel(event) 76 | else if event.target.id == "switch" 77 | @postState() 78 | -------------------------------------------------------------------------------- /widgets/stmodechange/stmodechange.coffee: -------------------------------------------------------------------------------- 1 | class Dashing.Stmodechange extends Dashing.ClickableWidget 2 | constructor: -> 3 | super 4 | @queryState() 5 | 6 | @accessor 'icon', 7 | get: -> @['icon'] ? 'tag' 8 | set: Batman.Property.defaultAccessor.set 9 | 10 | @accessor 'icon-style', -> 11 | if @isModeSet() then 'icon-active' else 'icon-inactive' 12 | 13 | @accessor 'mode', 14 | get: -> @_mode ? 'Unknown' 15 | set: (key, value) -> @_mode = value 16 | 17 | @accessor 'countdown', 18 | get: -> @_countdown ? 0 19 | set: (key, value) -> @_countdown = value 20 | 21 | @accessor 'timer', 22 | get: -> @_timer ? 0 23 | set: (key, value) -> @_timer = value 24 | 25 | showTimer: -> 26 | $(@node).find('.icon').hide() 27 | $(@node).find('.timer').show() 28 | 29 | showIcon: -> 30 | $(@node).find('.timer').hide() 31 | $(@node).find('.icon').show() 32 | 33 | isModeSet: -> 34 | @get('mode') == @get('changemode') 35 | 36 | queryState: -> 37 | $.get '/smartthings/dispatch', 38 | widgetId: @get('id'), 39 | deviceType: 'mode' 40 | (data) => 41 | json = JSON.parse data 42 | @set 'mode', json.mode 43 | 44 | postModeState: -> 45 | oldMode = @get 'mode' 46 | @set 'mode', @get('changemode') 47 | $.post '/smartthings/dispatch', 48 | deviceType: 'mode', 49 | mode: @get('changemode'), 50 | (data) => 51 | json = JSON.parse data 52 | if json.error != 0 53 | @set 'mode', oldModeM 54 | 55 | postPhraseState: -> 56 | $.post '/smartthings/dispatch', 57 | deviceType: 'phrase', 58 | phrase: @get('phrase') 59 | (data) => 60 | @queryState() 61 | 62 | ready: -> 63 | @showIcon() 64 | 65 | onData: (data) -> 66 | 67 | changeModeDelayed: => 68 | if @get('timer') <= 0 69 | @showIcon() 70 | if @get('phrase') 71 | @postPhraseState() 72 | else 73 | @postModeState() 74 | @_timeout = null 75 | else 76 | @showTimer() 77 | @set 'timer', @get('timer') - 1 78 | @_timeout = setTimeout(@changeModeDelayed, 1000) 79 | 80 | onClick: (event) -> 81 | if not @_timeout and not @isModeSet() 82 | @set 'timer', @get('countdown') 83 | @changeModeDelayed() 84 | -------------------------------------------------------------------------------- /config.ru: -------------------------------------------------------------------------------- 1 | require 'omniauth-heroku' 2 | require 'dashing' 3 | 4 | configure do 5 | # The auth token used by external clients to get API access to the 6 | # dashing widgets. 7 | set :auth_token, ENV["DASHING_AUTH_TOKEN"] 8 | 9 | # Email used for signing up to Heroku. This is used for authentication. 10 | set :user, ENV["HEROKU_OAUTH_EMAIL"] 11 | 12 | helpers do 13 | # Protects access to pages and redirects to the autentication page 14 | # if not already authenticated. 15 | def protected! 16 | # Ignore authentication in development 17 | if not production? 18 | return 19 | end 20 | 21 | # Authenticate in production 22 | if settings.user 23 | redirect '/auth/heroku' unless session[:user_id] == settings.user 24 | else 25 | # The HEROKU_OAUTH_EMAIL env var has not been set! 26 | redirect '/auth/notset' 27 | end 28 | end 29 | end 30 | 31 | # Store the authenticated user name in session state 32 | use Rack::Session::Cookie, :secret => ENV["SESSION_SECRET"] 33 | 34 | # Authenticate with Heroku 35 | use OmniAuth::Builder do 36 | provider :heroku, 37 | ENV["HEROKU_OAUTH_ID"], 38 | ENV["HEROKU_OAUTH_SECRET"], 39 | fetch_info: true 40 | end 41 | 42 | # Heroku authentication callback. 43 | get '/auth/heroku/callback' do 44 | if auth = request.env['omniauth.auth'] 45 | if auth['info']['email'] == settings.user 46 | session[:user_id] = settings.user 47 | redirect '/' 48 | else 49 | redirect '/auth/bad' 50 | end 51 | else 52 | redirect '/auth/failure' 53 | end 54 | end 55 | 56 | # Authentication failure. Indicates a configuration problem. 57 | get '/auth/failure' do 58 | "Authentication failure." 59 | end 60 | 61 | # Bad credentials. Indicates that the username, password or two-factor 62 | # auth code (if enabled) is incorrect. 63 | get '/auth/bad' do 64 | "Access denied." 65 | end 66 | 67 | # No authentication credentials have been set. HEROKU_OAUTH_EMAIL set? 68 | get '/auth/notset' do 69 | "Credentials not set." 70 | end 71 | 72 | # Restore the event history on load 73 | savedHistory = Setting.get('history') 74 | if savedHistory 75 | set :history, JSON.parse(savedHistory.value) 76 | end 77 | 78 | # Upong exiting, write the event history to persistent storage 79 | at_exit do 80 | savedHistory = Setting.first_or_create(:name => 'history') 81 | savedHistory.value = JSON.generate(settings.history) 82 | savedHistory.save 83 | end 84 | end 85 | 86 | map Sinatra::Application.assets_prefix do 87 | run Sinatra::Application.sprockets 88 | end 89 | 90 | run Sinatra::Application -------------------------------------------------------------------------------- /jobs/smartthings.rb: -------------------------------------------------------------------------------- 1 | require 'json' 2 | 3 | # URI to the installed app root 4 | host_uri = ENV["DASHING_URI"] || 'http://localhost:3030' 5 | 6 | # SmartApp credentials 7 | client_id = ENV["ST_CLIENT_ID"] 8 | # Keeping ST_API_KEY for compatibility (was renamed to ST_CLIENT_SECRET) 9 | client_secret = ENV["ST_CLIENT_SECRET"] || ENV["ST_API_KEY"] 10 | 11 | # Create a new STApp instance for communication with the SmartApp 12 | app = STApp.new( 13 | client_id, client_secret, 14 | host_uri + '/smartthings/oauth/callback') 15 | 16 | # Must be called to authenticate with SmartThings, at least once 17 | get '/smartthings/authorize' do 18 | redirect app.authorize 19 | end 20 | 21 | # Authentication callback for SmartThings 22 | get '/smartthings/oauth/callback' do 23 | app.acquireToken(params[:code]) 24 | app.request(:post, 'config', { 25 | dashingURI: host_uri, 26 | dashingAuthToken: settings.auth_token}) 27 | redirect '/' 28 | end 29 | 30 | # Dispatch requests to the SmartApp endpoint 31 | get '/smartthings/dispatch' do 32 | app.request(:get, params['deviceType'], params) 33 | end 34 | 35 | post '/smartthings/dispatch' do 36 | app.request(:post, params['deviceType'], params) 37 | end 38 | 39 | # Update the weather ever so often 40 | SCHEDULER.every '15m', :first_in => 0 do |job| 41 | # Current weather 42 | weather = app.request(:get, 'weather', { feature: 'conditions' }) 43 | 44 | # Forecast (today & tomorrow) 45 | forecast = app.request(:get, 'weather', { feature: 'forecast' }) 46 | 47 | # Emit the event 48 | if weather and forecast 49 | data = JSON.parse(weather).merge JSON.parse(forecast) 50 | send_event('weather', { 51 | now_temp: data["current_observation"]["temp_f"], 52 | humidity: data["current_observation"]["relative_humidity"], 53 | wind_speed: data["current_observation"]["wind_mph"], 54 | wind_speed_gust: data["current_observation"]["wind_gust_mph"], 55 | wind_dir: data["current_observation"]["wind_dir"], 56 | temp_low: data["forecast"]["simpleforecast"]["forecastday"][0]["low"]["fahrenheit"], 57 | temp_high: data["forecast"]["simpleforecast"]["forecastday"][0]["high"]["fahrenheit"], 58 | icon: data["forecast"]["simpleforecast"]["forecastday"][0]["icon"], 59 | precip: data["forecast"]["simpleforecast"]["forecastday"][0]["pop"], 60 | tomorrow_temp_low: data["forecast"]["simpleforecast"]["forecastday"][1]["low"]["fahrenheit"], 61 | tomorrow_temp_high: data["forecast"]["simpleforecast"]["forecastday"][1]["high"]["fahrenheit"], 62 | tomorrow_icon: data["forecast"]["simpleforecast"]["forecastday"][1]["icon"], 63 | tomorrow_precip: data["forecast"]["simpleforecast"]["forecastday"][1]["pop"]}) 64 | end 65 | end -------------------------------------------------------------------------------- /lib/stapp.rb: -------------------------------------------------------------------------------- 1 | require 'oauth2' 2 | require 'json' 3 | 4 | # 5 | # Object grants REST-ful access to a ST SmartApp endpoint. This 6 | # object also handles authorization with SmartThings. 7 | # 8 | class STApp 9 | def initialize(client_id, client_secret, redirect_uri) 10 | @client = OAuth2::Client.new(client_id, client_secret, { 11 | site: 'https://graph.api.smartthings.com', 12 | authorize_url: '/oauth/authorize', 13 | token_url: '/oauth/token' 14 | }) 15 | 16 | @token = retrieveToken() 17 | @endpoint = getEndpoint(@token) 18 | @redirect_uri = redirect_uri 19 | end 20 | 21 | # Returns the url used for authorization 22 | def authorize() 23 | @client.auth_code.authorize_url(redirect_uri: @redirect_uri, scope: 'app') 24 | end 25 | 26 | # Given a previously acquired auth code, this will acquire the 27 | # authorization token for use with subsequent requests. 28 | def acquireToken(auth_code) 29 | @token = @client.auth_code.get_token( 30 | auth_code, 31 | redirect_uri: @redirect_uri, 32 | scope: 'app') 33 | storeToken(@token) 34 | 35 | @endpoint = getEndpoint(@token) 36 | end 37 | 38 | # Make a request to the SmartApp endpoint. The verb shall be set to 39 | # :get or :post. data shall be a dictionary, which will be converted 40 | # to a JSON object in the request. 41 | def request(verb, url, data) 42 | if not @token 43 | return 44 | end 45 | 46 | @token = refreshToken(@token) 47 | 48 | result = @token.request( 49 | verb, @endpoint + '/' + url, { 50 | body: JSON.generate(data), 51 | headers: {'Content-Type'=>"application/json"} }) 52 | result.body() 53 | end 54 | 55 | # Refresh the auth token, if it has expired. 56 | def refreshToken(token) 57 | if token and token.expired? 58 | token.refresh! 59 | else 60 | token 61 | end 62 | end 63 | 64 | # Retrieve the SmartApp endpoint 65 | def getEndpoint(token) 66 | if not token 67 | return nil 68 | end 69 | 70 | response = token.get('/api/smartapps/endpoints') 71 | response.parsed()[0]['url'] 72 | end 73 | 74 | # Retrieve an existing token from persistent storage 75 | def retrieveToken() 76 | s = Setting.get('st_token') 77 | if s 78 | token = OAuth2::AccessToken.from_hash( 79 | @client, 80 | JSON.parse(s.value)) 81 | refreshToken(token) 82 | end 83 | end 84 | 85 | # Store a token in persistant storage 86 | def storeToken(token) 87 | s = Setting.first_or_create(:name => 'st_token') 88 | s.value = JSON.generate(token.to_hash) 89 | s.save 90 | end 91 | 92 | private :refreshToken, :getEndpoint, :retrieveToken, :storeToken 93 | 94 | end -------------------------------------------------------------------------------- /hadashboard: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require 'thor' 4 | require 'securerandom' 5 | 6 | class HADashboardCLI < Thor 7 | include Thor::Actions 8 | 9 | desc "setup", "Sets up the Heroku app and its environment." 10 | def setup 11 | say "Authorizing with Heroku", :green 12 | say "NOTE: Make sure to upload a public key if prompted!", :green 13 | system "heroku login" 14 | username = system "heroku auth:whoami" 15 | say "" 16 | 17 | say "Creating New App", :green 18 | system "heroku create" 19 | output = system "heroku app:info -s | grep ^name=" 20 | app_info = shell_to_hash output 21 | domain_name = app_info["domain_name"] 22 | app_name = app_info["name"] 23 | if not app_name 24 | say "*** Could not retrieve app name.", :red 25 | return 26 | end 27 | say "" 28 | 29 | say "Adding PostgreSQL Add-on", :green 30 | system "heroku addons:add heroku-postgresql:hobby-dev" 31 | say "" 32 | 33 | say "Creating API Client", :green 34 | system "heroku plugins:install https://github.com/heroku/heroku-oauth" 35 | output = system "heroku clients:create -s \"hadashboard\""\ 36 | "https://#{app_name}.herokuapp.com/auth/heroku/callback" 37 | client_auth = shell_to_hash output 38 | say "" 39 | 40 | say "Requesting SmartApp Credentials", :green 41 | st_client_id = nil 42 | loop do 43 | st_client_id = ask "SmartApp OAuth Client ID" 44 | break if validate_uuid st_client_id 45 | say "*** Value entered is not a UUID. Typo?", :red 46 | end 47 | 48 | st_client_secret = nil 49 | loop do 50 | st_client_secret = ask "SmartApp OAuth Client Secret" 51 | break if validate_uuid st_client_secret 52 | say "*** Value entered is not a UUID. Typo?", :red 53 | end 54 | say "" 55 | 56 | say "Configuring Heroku Variables", :green 57 | cvars = "" 58 | cvars << "DASHING_AUTH_TOKEN=" << SecureRandom.uuid << " " 59 | cvars << "DASHING_URI=http://" << domain_name << " " 60 | cvars << "HEROKU_OAUTH_EMAIL=" << username << " " 61 | cvars << "HEROKU_OAUTH_ID=" << client_auth["HEROKU_OAUTH_ID"] << " " 62 | cvars << "HEROKU_OAUTH_SECRET=" << client_auth["HEROKU_OAUTH_SECRET"] << " " 63 | cvars << "SESSION_SECRET=" << SecureRandom.uuid << " " 64 | cvars << "ST_CLIENT_ID=" << st_client_id << " " 65 | cvars << "ST_CLIENT_SECRET=" << st_client_secret << " " 66 | system "heroku config:set #{cvars}" 67 | say "" 68 | 69 | say "Waiting for Database", :green 70 | say "... this may take up to 5 minutes!", :green 71 | system "heroku pg:wait" 72 | say "" 73 | 74 | say "Deploying App", :green 75 | system "git push heroku master" 76 | say "" 77 | 78 | say "Opening App", :green 79 | say "NOTE: Don't forget to authorize with the SmartApp at: "\ 80 | "#{domain_name}/smartthings/authorize", :green 81 | system "heroku open" 82 | say "" 83 | end 84 | 85 | private 86 | 87 | def shell_to_hash(shell_output) 88 | Hash[shell_output.each_line.map { |l| l.chomp.split "=", 2 }] 89 | end 90 | 91 | def validate_uuid(value) 92 | (value =~ /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/) == 0 93 | end 94 | 95 | end 96 | 97 | HADashboardCLI.start(ARGV) 98 | -------------------------------------------------------------------------------- /assets/javascripts/eventsource.min.js: -------------------------------------------------------------------------------- 1 | /** @license 2 | * eventsource.js 3 | * Available under MIT License (MIT) 4 | * https://github.com/Yaffle/EventSource/ 5 | */ 6 | !function(a){"use strict";function b(){this.data={}}function c(){this.listeners=new b}function d(a){setTimeout(function(){throw a},0)}function e(a){this.type=a,this.target=null}function f(a,b){e.call(this,a),this.data=b.data,this.lastEventId=b.lastEventId}function g(a,b){var c=Number(a)||b;return z>c?z:c>A?A:c}function h(a,b,c){try{"function"==typeof b&&b.call(a,c)}catch(e){d(e)}}function i(b,d){function i(){L=s,null!==H&&(H.abort(),H=null),0!==I&&(clearTimeout(I),I=0),0!==J&&(clearTimeout(J),J=0),E.readyState=s}function j(a){var b=L===r||L===q?H.responseText||"":"",c=null,d=!1;if(L===q){var i=0,j="",k="";if(n)try{i=Number(H.status||0),j=String(H.statusText||""),k=String(H.getResponseHeader("Content-Type")||"")}catch(l){i=0}else i=200,k=H.contentType;if(200===i&&y.test(k)){if(L=r,G=!0,F=B,E.readyState=r,c=new e("open"),E.dispatchEvent(c),h(E,E.onopen,c),L===s)return}else if(0!==i){var m="";m=200!==i?"EventSource's response has a status "+i+" "+j.replace(/\s+/g," ")+" that is not 200. Aborting the connection.":"EventSource's response has a Content-Type specifying an unsupported type: "+k.replace(/\s+/g," ")+". Aborting the connection.",setTimeout(function(){throw new Error(m)}),d=!0}}if(L===r){b.length>K&&(G=!0);for(var o=K-1,z=b.length,J="\n";++o1048576||0===I&&!G)?0===I&&(G=!1,I=setTimeout(P,C)):(L=p,H.abort(),0!==I&&(clearTimeout(I),I=0),F>16*B&&(F=16*B),F>A&&(F=A),I=setTimeout(P,F),F=2*F+1,E.readyState=q,c=new e("error"),E.dispatchEvent(c),h(E,E.onerror,c))}function k(){j(!1)}function l(){j(!0)}b=String(b);var z=Boolean(m&&d&&d.withCredentials),B=g(d?d.retry:0/0,1e3),C=g(d?d.heartbeatTimeout:0/0,45e3),D=d&&d.lastEventId&&String(d.lastEventId)||"",E=this,F=B,G=!1,H=new o,I=0,J=0,K=0,L=p,M=[],N="",O="",P=null,Q=u,R="",S="";d=null,n&&(J=setTimeout(function T(){3===H.readyState&&k(),J=setTimeout(T,500)},0)),P=function(){if(I=0,L!==p)return void j(!1);if(n&&(void 0!==H.sendAsBinary||void 0===H.onloadend)&&a.document&&a.document.readyState&&"complete"!==a.document.readyState)return void(I=setTimeout(P,4));H.onload=H.onerror=l,n&&(H.onabort=l,H.onreadystatechange=k),H.onprogress=k,G=!1,I=setTimeout(P,C),K=0,L=q,M.length=0,O="",N=D,S="",R="",Q=u;var c=b.slice(0,5);c="data:"!==c&&"blob:"!==c?b+((-1===b.indexOf("?",0)?"?":"&")+"lastEventId="+encodeURIComponent(D)+"&r="+String(Math.random()+1).slice(2)):b,H.open("GET",c,!0),n&&(H.withCredentials=z,H.responseType="text",H.setRequestHeader("Accept","text/event-stream")),H.send(null)},c.call(this),this.close=i,this.url=b,this.readyState=q,this.withCredentials=z,this.onopen=null,this.onmessage=null,this.onerror=null,P()}function j(){this.CONNECTING=q,this.OPEN=r,this.CLOSED=s}b.prototype={get:function(a){return this.data[a+"~"]},set:function(a,b){this.data[a+"~"]=b},"delete":function(a){delete this.data[a+"~"]}},c.prototype={dispatchEvent:function(a){a.target=this;var b=String(a.type),c=this.listeners,e=c.get(b);if(e)for(var f=e.length,g=-1,h=null;++g=0;)if(d[e]===b)return;d.push(b)},removeEventListener:function(a,b){a=String(a);var c=this.listeners,d=c.get(a);if(d){for(var e=d.length,f=[],g=-1;++g 2.0, >= 2.0.2) 6 | backports (3.7.0) 7 | bcrypt (3.1.11) 8 | bcrypt-ruby (3.1.5) 9 | bcrypt (>= 3.1.3) 10 | coffee-script (2.2.0) 11 | coffee-script-source 12 | execjs 13 | coffee-script-source (1.12.2) 14 | daemons (1.2.4) 15 | dashing (1.3.7) 16 | coffee-script (~> 2.2.0) 17 | execjs (~> 2.0.2) 18 | rack (~> 1.5.4) 19 | rufus-scheduler (~> 2.0.24) 20 | sass (~> 3.2.12) 21 | sinatra (~> 1.4.4) 22 | sinatra-contrib (~> 1.4.2) 23 | sprockets (~> 2.10.1) 24 | thin (~> 1.6.1) 25 | thor (> 0.18.1) 26 | data_mapper (1.2.0) 27 | dm-aggregates (~> 1.2.0) 28 | dm-constraints (~> 1.2.0) 29 | dm-core (~> 1.2.0) 30 | dm-migrations (~> 1.2.0) 31 | dm-serializer (~> 1.2.0) 32 | dm-timestamps (~> 1.2.0) 33 | dm-transactions (~> 1.2.0) 34 | dm-types (~> 1.2.0) 35 | dm-validations (~> 1.2.0) 36 | data_objects (0.10.17) 37 | addressable (~> 2.1) 38 | dm-aggregates (1.2.0) 39 | dm-core (~> 1.2.0) 40 | dm-constraints (1.2.0) 41 | dm-core (~> 1.2.0) 42 | dm-core (1.2.1) 43 | addressable (~> 2.3) 44 | dm-do-adapter (1.2.0) 45 | data_objects (~> 0.10.6) 46 | dm-core (~> 1.2.0) 47 | dm-migrations (1.2.0) 48 | dm-core (~> 1.2.0) 49 | dm-postgres-adapter (1.2.0) 50 | dm-do-adapter (~> 1.2.0) 51 | do_postgres (~> 0.10.6) 52 | dm-serializer (1.2.2) 53 | dm-core (~> 1.2.0) 54 | fastercsv (~> 1.5) 55 | json (~> 1.6) 56 | json_pure (~> 1.6) 57 | multi_json (~> 1.0) 58 | dm-sqlite-adapter (1.2.0) 59 | dm-do-adapter (~> 1.2.0) 60 | do_sqlite3 (~> 0.10.6) 61 | dm-timestamps (1.2.0) 62 | dm-core (~> 1.2.0) 63 | dm-transactions (1.2.0) 64 | dm-core (~> 1.2.0) 65 | dm-types (1.2.2) 66 | bcrypt-ruby (~> 3.0) 67 | dm-core (~> 1.2.0) 68 | fastercsv (~> 1.5) 69 | json (~> 1.6) 70 | multi_json (~> 1.0) 71 | stringex (~> 1.4) 72 | uuidtools (~> 2.1) 73 | dm-validations (1.2.0) 74 | dm-core (~> 1.2.0) 75 | do_postgres (0.10.17) 76 | data_objects (= 0.10.17) 77 | do_sqlite3 (0.10.17) 78 | data_objects (= 0.10.17) 79 | eventmachine (1.2.3) 80 | execjs (2.0.2) 81 | faraday (0.11.0) 82 | multipart-post (>= 1.2, < 3) 83 | fastercsv (1.5.5) 84 | hashie (3.5.5) 85 | hike (1.2.3) 86 | json (1.8.6) 87 | json_pure (1.8.6) 88 | jwt (1.5.6) 89 | multi_json (1.12.1) 90 | multi_xml (0.6.0) 91 | multipart-post (2.0.0) 92 | oa-core (0.3.2) 93 | oa-openid (0.3.2) 94 | oa-core (= 0.3.2) 95 | rack-openid (~> 1.3.1) 96 | ruby-openid-apps-discovery (~> 1.2.0) 97 | oauth2 (1.3.1) 98 | faraday (>= 0.8, < 0.12) 99 | jwt (~> 1.0) 100 | multi_json (~> 1.3) 101 | multi_xml (~> 0.5) 102 | rack (>= 1.2, < 3) 103 | omniauth (1.4.2) 104 | hashie (>= 1.2, < 4) 105 | rack (>= 1.0, < 3) 106 | omniauth-heroku (0.3.0) 107 | omniauth (~> 1.2) 108 | omniauth-oauth2 (~> 1.2) 109 | omniauth-oauth2 (1.4.0) 110 | oauth2 (~> 1.0) 111 | omniauth (~> 1.2) 112 | public_suffix (2.0.5) 113 | rack (1.5.5) 114 | rack-openid (1.3.1) 115 | rack (>= 1.1.0) 116 | ruby-openid (>= 2.1.8) 117 | rack-protection (1.5.3) 118 | rack 119 | rack-test (0.6.3) 120 | rack (>= 1.0) 121 | ruby-openid (2.7.0) 122 | ruby-openid-apps-discovery (1.2.0) 123 | ruby-openid (>= 2.1.7) 124 | rufus-scheduler (2.0.24) 125 | tzinfo (>= 0.3.22) 126 | sass (3.2.19) 127 | sinatra (1.4.8) 128 | rack (~> 1.5) 129 | rack-protection (~> 1.4) 130 | tilt (>= 1.3, < 3) 131 | sinatra-contrib (1.4.7) 132 | backports (>= 2.0) 133 | multi_json 134 | rack-protection 135 | rack-test 136 | sinatra (~> 1.4.0) 137 | tilt (>= 1.3, < 3) 138 | sprockets (2.10.2) 139 | hike (~> 1.2) 140 | multi_json (~> 1.0) 141 | rack (~> 1.0) 142 | tilt (~> 1.1, != 1.3.0) 143 | stringex (1.5.1) 144 | thin (1.6.4) 145 | daemons (~> 1.0, >= 1.0.9) 146 | eventmachine (~> 1.0, >= 1.0.4) 147 | rack (~> 1.0) 148 | thor (0.19.4) 149 | thread_safe (0.3.6) 150 | tilt (1.4.1) 151 | tzinfo (1.2.3) 152 | thread_safe (~> 0.1) 153 | uuidtools (2.1.5) 154 | 155 | PLATFORMS 156 | ruby 157 | 158 | DEPENDENCIES 159 | dashing (>= 1.3.6) 160 | data_mapper 161 | dm-postgres-adapter 162 | dm-sqlite-adapter 163 | json 164 | oa-openid 165 | oauth2 166 | omniauth-heroku 167 | thor 168 | 169 | RUBY VERSION 170 | ruby 2.3.3p222 171 | 172 | BUNDLED WITH 173 | 1.14.6 174 | -------------------------------------------------------------------------------- /dashboards/main.erb: -------------------------------------------------------------------------------- 1 | <% content_for :title do %>HADashboard<% end %> 2 | 3 |
4 |
    5 |
  • 6 |
    7 |
  • 8 | 9 |
  • 10 |
    11 |
  • 12 | 13 |
  • 14 |
    15 |
  • 16 | 17 |
  • 18 |
    19 |
  • 20 | 21 |
  • 22 |
    23 |
  • 24 | 25 |
  • 26 |
    27 |
  • 28 | 29 |
  • 30 |
    31 |
  • 32 | 33 |
  • 34 |
    35 |
  • 36 | 37 |
  • 38 |
    39 |
  • 40 | 41 |
  • 42 |
    43 |
  • 44 | 45 |
  • 46 |
    47 |
  • 48 | 49 |
  • 50 |
    51 |
  • 52 | 53 |
  • 54 |
    55 |
  • 56 | 57 |
  • 58 |
    59 |
  • 60 | 61 |
  • 62 |
    63 |
  • 64 | 65 |
  • 66 |
    67 |
  • 68 | 69 |
  • 70 |
    71 |
  • 72 | 73 |
  • 74 |
    75 |
  • 76 | 77 |
  • 78 |
    79 |
  • 80 | 81 |
  • 82 |
    83 |
  • 84 | 85 |
  • 86 |
    87 |
  • 88 | 89 |
  • 90 |
    91 |
  • 92 | 93 |
  • 94 |
    95 |
  • 96 | 97 |
98 |
99 | 100 |
101 |
    102 |
  • 103 |
    104 |
  • 105 |
  • 106 |
    107 |
  • 108 |
  • 109 |
    110 |
  • 111 |
112 |
113 | -------------------------------------------------------------------------------- /assets/stylesheets/application.scss: -------------------------------------------------------------------------------- 1 | /* 2 | //=require_directory . 3 | //=require_tree ../../widgets 4 | */ 5 | // ---------------------------------------------------------------------------- 6 | // Sass declarations 7 | // ---------------------------------------------------------------------------- 8 | $background-color: #222; 9 | $text-color: #fff; 10 | 11 | $background-warning-color-1: #e82711; 12 | $background-warning-color-2: #9b2d23; 13 | $text-warning-color: #fff; 14 | 15 | $background-danger-color-1: #eeae32; 16 | $background-danger-color-2: #ff9618; 17 | $text-danger-color: #fff; 18 | 19 | @-webkit-keyframes status-warning-background { 20 | 0% { background-color: $background-warning-color-1; } 21 | 50% { background-color: $background-warning-color-2; } 22 | 100% { background-color: $background-warning-color-1; } 23 | } 24 | @-webkit-keyframes status-danger-background { 25 | 0% { background-color: $background-danger-color-1; } 26 | 50% { background-color: $background-danger-color-2; } 27 | 100% { background-color: $background-danger-color-1; } 28 | } 29 | @mixin animation($animation-name, $duration, $function, $animation-iteration-count:""){ 30 | -webkit-animation: $animation-name $duration $function #{$animation-iteration-count}; 31 | -moz-animation: $animation-name $duration $function #{$animation-iteration-count}; 32 | -ms-animation: $animation-name $duration $function #{$animation-iteration-count}; 33 | } 34 | 35 | // ---------------------------------------------------------------------------- 36 | // Base styles 37 | // ---------------------------------------------------------------------------- 38 | html { 39 | font-size: 100%; 40 | -webkit-text-size-adjust: 100%; 41 | -ms-text-size-adjust: 100%; 42 | } 43 | 44 | body { 45 | margin: 0; 46 | background-color: $background-color; 47 | font-size: 15px; 48 | color: $text-color; 49 | font-family: 'Helvetica Neue', 'Helvetica', 'Open Sans', 'Arial' 50 | } 51 | 52 | b, strong { 53 | font-weight: bold; 54 | } 55 | 56 | a { 57 | text-decoration: none; 58 | color: inherit; 59 | } 60 | 61 | img { 62 | border: 0; 63 | -ms-interpolation-mode: bicubic; 64 | vertical-align: middle; 65 | } 66 | 67 | img, object { 68 | max-width: 100%; 69 | } 70 | 71 | iframe { 72 | max-width: 100%; 73 | } 74 | 75 | table { 76 | border-collapse: collapse; 77 | border-spacing: 0; 78 | width: 100%; 79 | } 80 | 81 | td { 82 | vertical-align: middle; 83 | } 84 | 85 | ul, ol { 86 | padding: 0; 87 | margin: 0; 88 | } 89 | 90 | h1, h2, h3, h4, h5, p { 91 | padding: 0; 92 | margin: 0; 93 | } 94 | h1 { 95 | margin-bottom: 6px; 96 | text-align: center; 97 | font-size: 150%; 98 | font-weight: 200; 99 | } 100 | h2 { 101 | text-transform: uppercase; 102 | font-size: 380%; 103 | font-weight: 400; 104 | color: $text-color; 105 | } 106 | h3 { 107 | font-size: 125%; 108 | font-weight: 300; 109 | color: $text-color; 110 | } 111 | 112 | // ---------------------------------------------------------------------------- 113 | // Base widget styles 114 | // ---------------------------------------------------------------------------- 115 | .gridster { 116 | margin: 0px auto; 117 | } 118 | 119 | .icon-background { 120 | pointer-events: none; 121 | width: 100%!important; 122 | height: 100%; 123 | position: absolute; 124 | left: 0; 125 | top: 0; 126 | opacity: 0.1; 127 | font-size: 1375%; 128 | text-align: center; 129 | margin-top: 82px; 130 | } 131 | 132 | .list-nostyle { 133 | list-style: none; 134 | } 135 | 136 | .gridster ul { 137 | list-style: none; 138 | } 139 | 140 | .gs_w { 141 | width: 100%; 142 | display: table; 143 | cursor: pointer; 144 | } 145 | 146 | .widget { 147 | padding: 0px 0px; 148 | text-align: center; 149 | width: 100%; 150 | display: table-cell; 151 | vertical-align: middle; 152 | background-color: #333; 153 | } 154 | 155 | .widget.status-warning { 156 | background-color: $background-warning-color-1; 157 | @include animation(status-warning-background, 2s, ease, infinite); 158 | 159 | .icon-warning-sign { 160 | display: inline-block; 161 | } 162 | 163 | .title, .more-info { 164 | color: $text-warning-color; 165 | } 166 | } 167 | 168 | .widget.status-danger { 169 | color: $text-danger-color; 170 | background-color: $background-danger-color-1; 171 | @include animation(status-danger-background, 2s, ease, infinite); 172 | 173 | .icon-warning-sign { 174 | display: inline-block; 175 | } 176 | 177 | .title, .more-info { 178 | color: $text-danger-color; 179 | } 180 | } 181 | 182 | .more-info { 183 | font-size: 75%; 184 | position: absolute; 185 | bottom: 16px; 186 | left: 0; 187 | right: 0; 188 | } 189 | 190 | .updated-at { 191 | font-size: 75%; 192 | position: absolute; 193 | bottom: 12px; 194 | left: 0; 195 | right: 0; 196 | } 197 | 198 | #save-gridster { 199 | display: none; 200 | position: fixed; 201 | top: 0; 202 | margin: 0px auto; 203 | left: 50%; 204 | z-index: 1000; 205 | background: black; 206 | width: 190px; 207 | text-align: center; 208 | border: 1px solid white; 209 | border-top: 0px; 210 | margin-left: -95px; 211 | padding: 15px; 212 | } 213 | 214 | #save-gridster:hover { 215 | padding-top: 25px; 216 | } 217 | 218 | #saving-instructions { 219 | display: none; 220 | padding: 10px; 221 | width: 500px; 222 | height: 122px; 223 | z-index: 1000; 224 | background: white; 225 | top: 100px; 226 | color: black; 227 | font-size: 75px; 228 | padding-bottom: 4px; 229 | 230 | textarea { 231 | white-space: nowrap; 232 | width: 494px; 233 | height: 80px; 234 | } 235 | } 236 | 237 | #lean_overlay { 238 | position: fixed; 239 | z-index:100; 240 | top: 0px; 241 | left: 0px; 242 | height:100%; 243 | width:100%; 244 | background: #000; 245 | display: none; 246 | } 247 | 248 | #container { 249 | padding-top: 5px; 250 | } 251 | 252 | 253 | // ---------------------------------------------------------------------------- 254 | // Clearfix 255 | // ---------------------------------------------------------------------------- 256 | .clearfix:before, .clearfix:after { content: "\0020"; display: block; height: 0; overflow: hidden; } 257 | .clearfix:after { clear: both; } 258 | .clearfix { zoom: 1; } 259 | 260 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Installation 2 | 3 | ## 1. Sign up for Heroku 4 | Navigate to https://www.heroku.com and sign up for a free account. No need to enter your credit card information: We will be using a free dyno. 5 | 6 | Download and install the [Heroku Toolbelt](https://toolbelt.heroku.com) on your local machine. The toolbelt includes the Heroku CLI (Command Line Interface), which we will be using to configure the web app on the Heroku dyno. Note, that you can complete the configuration without the CLI, by using the Heroku web interface. However, the process becomes a lot more streamlined with the toolbelt. 7 | 8 | 9 | ## 2. Clone the Repository 10 | Clone the **hadashboard** repository to the current local directory on your machine. 11 | 12 | ``` bash 13 | $ git clone https://github.com/FlorianZ/hadashboard.git 14 | ``` 15 | 16 | Change your working directory to the repository root. Moving forward, we will be working from this directory. 17 | 18 | ``` bash 19 | $ cd hadashboard 20 | ``` 21 | 22 | 23 | ## 3. Install the SmartApp 24 | Navigate to https://graph.api.smartthings.com and log in to your SmartThings IDE account. Select the **'My SmartApps'** tab, and click the **'+ New SmartApp'** button to create a new SmartApp. 25 | 26 | Fill in the required information. The **'Name'** and **'Description'** are both required fields, but their values are not important. 27 | 28 | Make sure to click the **'Enable OAuth in Smart App'** button to grant REST API access to the new SmartApp. Note the **'OAuth Client ID'** and **'OAuth Client Secret'**. Both will later be required by the Dashing backend to authenticate with the new SmartApp and talk to SmartThings. 29 | 30 | Hit the **'Create'** button to get to the code editor. Replace the content of the code editor with the content of the file at: `smartapps/DashingAccess.groovy` 31 | 32 | Click the **'Save'** button and then **'Publish -> For Me'**. 33 | 34 | ## 4. Configure the Web App 35 | Using the Heroku CLI, log in to your Heroku account with the email address and password used to create your Heroku account in step 1. You may be required to **generate a new public key** in the process. The Heroku CLI will take care of this and upload the key in the process. 36 | 37 | ``` bash 38 | $ heroku login 39 | Enter your Heroku credentials. 40 | Email: your@email.com 41 | Password: 42 | Could not find an existing public key. 43 | Would you like to generate one? [Yn] 44 | Generating new SSH public key. 45 | Uploading ssh public key /Users/user/.ssh/id_rsa.pub 46 | ``` 47 | 48 | Now, we will create a new Heroku app. Make sure you are still in the repository root directory (*hadashboard*) from step 2. 49 | 50 | ``` bash 51 | $ heroku create 52 | Creating your-app-name... done, stack is cedar 53 | http://your-app-name.herokuapp.com/ | git@heroku.com:your-app-name.git 54 | Git remote heroku added 55 | ``` 56 | 57 | This will create a new web app with a random name. The name will be output to the terminal. Take note of it. If you don't like the name, don't worry: You can [rename your app](https://devcenter.heroku.com/articles/renaming-apps) at any time using the Heroku CLI or web interface. 58 | 59 | Your new app will later be reachable at **http://*your-app-name*.herokuapp.com**. We have not deployed your app yet, and before we do we will need to make sure that only you have access to your dashboard, and set up a few **Config Variables**: 60 | 61 | To make sure that your dashboard is not publicly viewable, and that only you have access to it, the hadashboard code is set up to use your Heroku credentials (from step 1) for authentication. 62 | 63 | Next, create a client for hadashboard. Make sure to replace *your-app-name* with the name of your app, as returned by `heroku create` earlier. 64 | 65 | ``` bash 66 | $ heroku clients:create -s "hadashboard" https://your-app-name.herokuapp.com/auth/heroku/callback 67 | HEROKU_OAUTH_ID=some-random-uuid 68 | HEROKU_OAUTH_SECRET=some-random-uuid 69 | ``` 70 | 71 | The command will return two random uuids. The app id and secret. Note the two uuids, as we will set them in the app environment as **Config Variables** along with some other variables required by the hadashboard app. Before running this command, make sure to first **replace** with real values as described below. 72 | 73 | ``` bash 74 | $ heroku config:set \ 75 | DASHING_AUTH_TOKEN=some-random-uuid \ 76 | DASHING_URI=http://your-app-name.herokuapp.com \ 77 | HEROKU_OAUTH_EMAIL=your@email.com \ 78 | HEROKU_OAUTH_ID=uuid-from-above \ 79 | HEROKU_OAUTH_SECRET=uuid-from-above\ 80 | SESSION_SECRET=some-random-uuid \ 81 | ST_CLIENT_ID=from-step-3 \ 82 | ST_CLIENT_SECRET=from-step-3 83 | ``` 84 | 85 | - **DASHING_AUTH_TOKEN**: Set this to a new, random uuid. You may be able to generate a new uuid using the `uuidgen` command line tool, if it is installed on your local machine. You can also use one of the many [online tools](https://www.uuidgenerator.net) to generate uuids. This is used as a machine-to-machine password of sorts, so you could even make up your own. Just make sure it's long, and random, and comlicated in order to ensure it is as secure as possible. This uuid/password is used for the SmartApp to communicate with the Heroku app. 86 | - **DASHING_URI**: This is the URI to your heroku app. The `heroku create` command will return this value. It is usually **http://*your-app-name*.herokuapp.com**, but make sure to replace *your-app-name* with the actual app name. Also make sure there is no trailing */* character in this value. 87 | - **HEROKU_OAUTH_EMAIL**: The email address you used to sign up to Heroku (step 1). 88 | - **HEROKU_OAUTH_ID**: Set this to the **HEROKU_OAUTH_ID** uuid returned by the previous command (`heroku clients:create`). 89 | - **HEROKU_OAUTH_SECRET**: Set this to the **HEROKU_OAUTH_SECRET** uuid returned by the previous command (`heroku clients:create`). 90 | - **SESSION_SECRET**: Generate a new, random uuid for this, just like you did for **DASHING_AUTH_TOKEN**. This uuid/password is used to encrypt the session cookie in your browser. 91 | - **ST_CLIENT_ID**: Set this to the SmartApp **'OAuth Client ID'** from step 3. 92 | - **ST_CLIENT_SECRET**: Set this to the SmartApp **'OAuth Client Secret'** from step 3. 93 | 94 | Lastly, we will add a PostgreSQL database. Heroku makes this trivial by simple configuring an add-on: 95 | 96 | ``` bash 97 | $ heroku addons:add heroku-postgresql:hobby-dev 98 | Adding heroku-postgresql:hobby-dev to your-app-name... done, v69 (free) 99 | Attached as HEROKU_POSTGRESQL_GOLD 100 | Database has been created and is available 101 | ``` 102 | 103 | Heroku may require up to 5 minutes to set up the database for you. You can run the following command to make sure the database is ready before you move on to the next step. 104 | 105 | ``` bash 106 | $ heroku pg:wait 107 | Waiting for database HEROKU_POSTGRESQL_GOLD... done 108 | ``` 109 | 110 | ## 5. Deploy to Heroku 111 | To deploy the hadashboard app, all we need to do is push the git repository to Heroku. This will automatically install all the dependencies on the server, and restart the app. 112 | 113 | ``` bash 114 | $ git push heroku 115 | Initializing repository, done. 116 | Counting objects: 301, done. 117 | Delta compression using up to 8 threads. 118 | Compressing objects: 100% (171/171), done. 119 | Writing objects: 100% (301/301), 447.67 KiB | 537.00 KiB/s, done. 120 | Total 301 (delta 120), reused 301 (delta 120) 121 | 122 | -----> Ruby app detected 123 | -----> Compiling Ruby/Rack 124 | -----> Using Ruby version: ruby-2.0.0 125 | -----> Installing dependencies using 1.6.3 126 | ... 127 | 128 | -----> Discovering process types 129 | Procfile declares types -> (none) 130 | Default types for Ruby -> console, rake, web 131 | 132 | -----> Compressing... done, 18.5MB 133 | -----> Launching... done, v6 134 | http://your-app-name.herokuapp.com/ deployed to Heroku 135 | 136 | To git@heroku.com:your-app-name.git 137 | * [new branch] master -> master 138 | ``` 139 | 140 | To access the hadashboard app, navigate to **http://*your-app-name*.herokuapps.com**. You will be asked to grant the hadashboard app access to your Heroku account. Make sure to **Allow Access**. The hadashboard app needs access to your Heroku account in order to verify your identity. You may be asked to log in to your Heroku account, as well. Use the email address and password from step 1. 141 | 142 | You will see the default dashboard, but it will not yet have access to your SmartThings. 143 | 144 | ## 6. Authorize with SmartThings 145 | To grant the hadashboard access to SmartThings, you must first authorize with the SmartApp created in step 3. To do so, navigate to **http://*your-app-name*.herokuapps.com/smartthings/authorize**. Log in with your SmartThings credentials and allows access to all the devices you would like to be able to control from the hadashboard. 146 | 147 | After clicking **Authorize** you should be redirected back to the default dashboard, and you should now have access to your things. 148 | 149 | Note, that currently authorization only persists for the lifetime of the execution context of the hadashboard app. So, **whenever you restart the Heroku dyno** (such as after deploying changes - step 5) you will have to **repeat this step**. You may also have to authorize **after making changes to the SmartApp**, for the changes to take effect. 150 | 151 | 152 | # Changing Widgets 153 | The hadashboard is a Dashing app, so make sure to read all the instructions on http://dashing.io to learn how to add widgets to your dashboard, as well as how to create new widgets. 154 | 155 | Essentially, you will have to modify the `dashboards/main.erb` file. After modifying this file, you must commit the changes to the git repository: 156 | 157 | ``` bash 158 | $ git add . 159 | $ git commit -m "Made some changes to the main.erb layout file." 160 | $ git push heroku 161 | ``` 162 | 163 | This will re-deploy your application, so make sure to repeat installation step 6. 164 | 165 | The basic anatomy of a widget is this: 166 | ``` html 167 |
  • 168 |
    172 |
    173 |
  • 174 | ``` 175 | - **data-row**, **data-col**: The position of the widget in the grid. 176 | - **data-sizex**, **data-sizey**: The size of the widget in terms of grid tile. 177 | - **data-id**: The unique id of the widget. 178 | - **data-view**: The type of widget to be used (Stswitch, Sttemp, etc.) 179 | - **data-icon**: For Stswitch, the icon displayed on the tile. See http://fontawesome.io for an icon cheatsheet. 180 | - **data-title**: The title to be displayed on the tile. 181 | - **data-device**: This is the name of the device to be controlled by this Stswitch tile. Use the displayLabel as set in SmartThings. Also make sure that access has been granted to this device during authorization with SmartThings (installation step 6). You can always repeat step 6 to change access rights. 182 | - **data-changemode**: The mode to be "watched" by the Stmodechange widget. The widget will indicate if this mode has been set, as well as change to this mode if interacted with (touched, clicked). 183 | - **data-phrase**: (optional) The "Hello Home" phrase to execute when this Stmodechange widget is interacted with (touched, clicked). If this property is set, the phrase will be executed, instead of changing the mode set by **data-changemode**. 184 | - **data-countdown**: A delay in seconds used by the Stmodechange widget to delay the mode change / phrase execution. 185 | - **data-event-touchend**: Adds interactivity to this widget. Set this to **onClick** if you want the widget to react to interactions. Removing this property makes the widget static. Refers to interactivity on mobile devices. 186 | - **data-event-click**: Same as **data-event-touchend**, but for desktop browsers. 187 | 188 | Please, refer to the Dashing website for instructions on how to change the grid and tile size, as well as more general instructions about widgets, their properties, and how to create new widgets. 189 | 190 | 191 | # Changing Dashboards 192 | You can also have multiple dashboards, by simply adding a new .erb file to the dashboards directory and navigating to the dashboards via **http://*your-app-name*.herokuapps.com/*dashboard-file-name-without-extension*** 193 | 194 | For example, if you want to deploy multiple devices, you could have one dashboard per room and still only use one hadashboard app installation. 195 | 196 | Please refer to the Dashing website with regards to instructions on multiple dashboards, the default dashboard and cycling through dashboards. 197 | 198 | 199 | # Changing Theme 200 | In order to change the theme for your dashboard, you will need to update 1 line in assets/stylesheets/themes/current.scss 201 | 202 | Specifically, update the following to include the filename of your theme that exists in the same directory. 203 | 204 | ``` bash 205 | @import "grey-grey.scss" 206 | ``` 207 | 208 | # Troubleshooting 209 | ### My Dashboard is not updating or devices do not respond to interaction. 210 | If your dashboard is not updating or devices have stopped responding to interaction, first try to refresh your browser. If this did not help, navigate to **http://*your-app-name*.herokuapps.com/smartthings/authorize** and re-authorize with the SmartApp (installation step 6) 211 | You will have to re-authorize with the SmartApp whenever you make changes to backend files (usually .rb or .erb) or the SmartApp Groovy code. 212 | If your dashboard stops updating or devices have stopped responding without making any changes, make sure to file a bug using the issue tracker. 213 | 214 | ### I made changes to my dashboard/widgets but they don't show up in the browser. 215 | After making changes to any files (except the SmartApp Groovy code), you will have to commit your changes to the local git repository and then push these changes to Heroku. These commands from the local repository root should do the trick: 216 | 217 | ``` bash 218 | $ git add . 219 | $ git commit -m "Replace this text with a meaningful description of your changes." 220 | $ git push heroku 221 | ``` 222 | 223 | ### Can I test my changes locally? 224 | Yes! See the **Getting Started** section at http://dashing.io for more details. 225 | 226 | Essentially, you want to make sure that you have Ruby installed on your local machine. Then, install the Dashing gem: 227 | 228 | ``` bash 229 | $ gem install dashing 230 | ``` 231 | 232 | From your repository root, make sure that all dependencies are available. Note, that you will need to re-run the bundler whenever you modify the Gemfile. 233 | 234 | ``` bash 235 | $ bundle 236 | ``` 237 | 238 | You can start a local webserver like this: 239 | 240 | ``` bash 241 | $ dashing start 242 | ``` 243 | 244 | Point your browser to **http://localhost:3030** to access the hadashboard on your local machine. 245 | 246 | Note, that the SmartApp will not be able to communicate with the hadashboard running on your local machine, and that in order for the hadashboar to communicate with the SmartApp, the **ST_CLIENT_ID** and **ST_CLIENT_SECRET** variables must be set in your local environment. You can also hardcode these values at the top of the `jobs/smartthings.rb` file. 247 | -------------------------------------------------------------------------------- /assets/javascripts/cycleDashboards.coffee: -------------------------------------------------------------------------------- 1 | # Flying Widgets v0.1.1 2 | # 3 | # To use, put this file in assets/javascripts/cycleDashboard.coffee. Then find this line in 4 | # application.coffee: 5 | # 6 | # $('.gridster ul:first').gridster 7 | # 8 | # And change it to: 9 | # 10 | # $('.gridster > ul').gridster 11 | # 12 | # Finally, put multiple gridster divs in your dashboard, and add a call to Dashing.cycleDashboards() 13 | # to the javascript at the top of your dashboard: 14 | # 15 | # 22 | # 23 | # <% content_for :title do %>Loop Dashboard<% end %> 24 | # 25 | #
    26 | #
      27 | # 28 | #
    • 29 | #
      30 | #
    • 31 | # 32 | #
    33 | #
    34 | # 35 | #
    36 | #
      37 | # 38 | #
    39 | #
    40 | # 41 | 42 | # Some generic helper functions 43 | sleep = (timeInSeconds, fn) -> setTimeout fn, timeInSeconds * 1000 44 | isArray = (obj) -> Object.prototype.toString.call(obj) is '[object Array]' 45 | isString = (obj) -> Object.prototype.toString.call(obj) is '[object String]'; 46 | isFunction = (obj) -> obj && obj.constructor && obj.call && obj.apply 47 | 48 | #### Show/Hide functions. 49 | # 50 | # Every member of `showFunctions` and `hideFunctions` must be one of: 51 | # 52 | # * A `{start, end, transition}` object (transition defaults to 'all 1s'.) 53 | # * A `{transitionFunction}` object. 54 | # * A `fn($dashboard, widget, originalLocations)` which returns one of the above. 55 | # 56 | # The easiest way to define a transition is just to specify start and end CSS proprties for each 57 | # widget with a `{start, end}` object. The `fadeOut` and `fadeIn` are some of the simplest 58 | # examples below. Sometimes you might need slightly more control, in which case `start` and 59 | # `end` can each be functions of the form `($widget, index)`, where $widget is the jquery object 60 | # for the widget being transformed, and index is the index of the widget within the dashboard. 61 | # The function form is handy when you want to do something different for each widget, depending 62 | # on it's location. 63 | # 64 | # For even more control, you can specify a `fn($dashboard, widgets, originalLocations)` function 65 | # in place of the entire object. This is handy when you have some setup work to do for your 66 | # transition, such as detecting the width of the page so you can move all widgets off-screen. 67 | # 68 | # For the ultimate in control, you can specify a 69 | # `transitionFunction{$dashboard, options, done}` object. It will be up to you to 70 | # do whatever you need to do in order to hide or display the dashboard. The CSS of every widget 71 | # will be reset to something sane when the function completes, but otherwise it's entirely 72 | # up to you. Params are: 73 | # 74 | # * `$dashboard` - jquery object of the dashboard to show/hide. 75 | # * `options.stagger` - True if transition should be staggered. 76 | # * `options.widgets` - An array of all widgets in the dashboard. 77 | # * `options.originalLocations` - An array of CSS data about the location, opacity, etc... of 78 | # each widget. 79 | # * `done()` - Async callback. Make sure you call this! 80 | # 81 | 82 | hideFunctions = { 83 | # toRight: ($dashboard, widgets, originalLocations) -> 84 | # documentWidth = $(document).width() 85 | # return {end: (($widget) -> {left: documentWidth, opacity: 0})} 86 | 87 | # shrink: { 88 | # start: { 89 | # opacity: 1, 90 | # transform: 'scale(1,1)', 91 | # "-webkit-transform": 'scale(1,1)' 92 | # }, 93 | # end: { 94 | # transform: 'scale(0,0)', 95 | # "-webkit-transform": 'scale(0,0)', 96 | # opacity: 0 97 | # } 98 | # } 99 | 100 | fadeOut: { 101 | start: {opacity: 1} 102 | end: {opacity: 0} 103 | } 104 | 105 | # explode: { 106 | # start: { 107 | # opacity: 1 108 | # transform: 'scale(1,1)', 109 | # "-webkit-transform": 'scale(1,1)' 110 | # } 111 | # end: { 112 | # opacity: 0 113 | # transform: 'scale(2,2)', 114 | # "-webkit-transform": 'scale(2,2)' 115 | # } 116 | # } 117 | } 118 | 119 | # Handy function for reversing simple transitions 120 | reverseTransition = (obj) -> 121 | if isFunction(obj) or obj.transitionFunction? 122 | throw new Error("Can't reverse transition") 123 | return {start: obj.end, end: obj.start, transition: obj.transition} 124 | 125 | showFunctions = { 126 | # fromLeft: ($dashboard, widgets, originalLocations) -> 127 | # start: (($widget, index) -> {left: "#{-$widget.width() - $dashboard.width()}px", opacity: 0}), 128 | # end: (($widget, index) -> originalLocations[index]), 129 | 130 | # fromTop: ($dashboard, widgets, originalLocations) -> 131 | # start: (($widget, index) -> {top: "#{-$widget.height() - $dashboard.height()}px", opacity: 0}), 132 | # end: (($widget, index) -> return originalLocations[index]), 133 | 134 | # zoom: reverseTransition(hideFunctions.shrink) 135 | 136 | fadeIn: reverseTransition(hideFunctions.fadeOut) 137 | 138 | # implode: reverseTransition(hideFunctions.explode) 139 | 140 | } 141 | 142 | # Move an element from one place to another using a CSS3 transition. 143 | # 144 | # * `elements` - One or more elements to move, in an array. 145 | # * `transition` - The transition string to apply (e.g.: 'left 1s ease 0s') 146 | # * `start` - This can be an object (e.g. `{left: 0px}`) or a `fn($el, index)` 147 | # which returns such an object. This is the location the object will start at. 148 | # If start is omitted, then the current location of the object will be used 149 | # as the start. 150 | # * `end` - As with `start`, this can be an object or a function. `end` is required. 151 | # * `timeInSeconds` - The time required to complete the transition. This function will 152 | # wait this long before calling `done()`. 153 | # * `offset` is an offset for the index passed into `start()` and `end()`. Handy when 154 | # you want to split up an array of 155 | # * `done()` - Async callback. 156 | moveWithTransition = (elements, {transition, start, end, timeInSeconds, offset}, done) -> 157 | transition = transition or '' 158 | timeInSeconds = timeInSeconds or 0 159 | end = end or {} 160 | offset = offset or 0 161 | 162 | origTransitions = [] 163 | moveToStart = () -> 164 | for el, index in elements 165 | $el = $(el) 166 | origTransitions[index + offset] = $el.css 'transition' 167 | $el.css transition: 'left 0s ease 0s' 168 | $el.css(if isFunction start then start($el, index + offset) else start) 169 | 170 | moveToEnd = () -> 171 | for el, index in elements 172 | $el = $(el) 173 | $el.css transition: transition 174 | $el.css(if isFunction end then end($el, index + offset) else end) 175 | sleep Math.max(0, timeInSeconds), -> 176 | $el.css transition: origTransitions[index + offset] 177 | done? null 178 | 179 | if start 180 | moveToStart() 181 | sleep 0, -> moveToEnd() 182 | else 183 | moveToEnd() 184 | 185 | # Runs a function which shows or hides the dashboard. This function ensures that all the 186 | # dashboards widgets end up where they started. 187 | # 188 | # Transitions should be a `{start, end}` object suitable for passing to moveWithTransition, 189 | # or a `transitions($dashboard, widgets, originalLocations)` function which returns such an object. 190 | # 191 | showHideDashboard = (visible, stagger, $dashboard, transitions, done) -> 192 | $dashboard = $($dashboard) 193 | 194 | $ul = $dashboard.children('ul') 195 | $widgets = $ul.children('li') 196 | 197 | # Record the original location, opacity, other CSS attributes we might want to edit 198 | originalLocations = [] 199 | $widgets.each (index, widget) -> 200 | $widget = $(widget) 201 | originalLocations[index] = { 202 | left: $widget.css 'left' 203 | top: $widget.css 'top' 204 | width: $widget.css 'width' 205 | height: $widget.css 'height' 206 | opacity: $widget.css 'opacity' 207 | transform: $widget.css 'transform' 208 | "-webkit-transform": $widget.css '-webkit-transform' 209 | } 210 | 211 | widgets = $.makeArray($widgets) 212 | 213 | if isFunction transitions 214 | transitions = transitions($dashboard, widgets, originalLocations) 215 | 216 | 217 | origDone = done 218 | done = () -> 219 | sleep 0, () -> 220 | # Make sure the dashboard is in a sane state. 221 | $dashboard.toggle( visible ) 222 | 223 | sleep 0, () -> 224 | # Clear any styles we've set on the widgets. 225 | # 226 | # TODO: It would be nice to record the styles before we start, and then restore them 227 | # here, but I've found that if my laptop goes to sleep, when it wakes up, when 228 | # displaying the dashboard on Chrome, it sometimes picks up bad values for 229 | # `originalLocations`. By always forcing the style to a sane known value, we know 230 | # everything will work out in the end. 231 | # 232 | $dashboard.children('ul').children('li').attr 'style', 'position: absolute' 233 | 234 | origDone?() 235 | 236 | 237 | transitionString = "all 1s" 238 | 239 | if transitions.transitionFunction 240 | # Show/hide the dashboard with a custom function 241 | transitionFunction = transitions.transitionFunction 242 | 243 | else if !stagger 244 | transitionFunction = ($dashboard, {widgets, originalLocations}, fnDone) -> 245 | moveWithTransition widgets, { 246 | end: transitions.start 247 | }, -> sleep 0, -> 248 | if visible then $dashboard.show() 249 | moveWithTransition widgets, { 250 | start: transitions.start, 251 | end: transitions.end, 252 | transition: transitions.transition or transitionString, 253 | timeInSeconds: 1 254 | }, fnDone 255 | 256 | else 257 | transitionFunction = ($dashboard, {widgets, originalLocations}, fnDone) -> 258 | singleWidgetFn = (widget, index) -> 259 | moveWithTransition [widget], { 260 | end: transitions.start, 261 | offset: index 262 | }, -> sleep 0, -> 263 | if visible then $dashboard.show() 264 | sleep (Math.random()/2), () -> 265 | moveWithTransition [widget], { 266 | start: transitions.start, 267 | end: transitions.end, 268 | transition: transitions.transition or transitionString, 269 | timeInSeconds: 1, 270 | offset: index 271 | }, -> 272 | for widget, index in widgets 273 | singleWidgetFn(widget, index) 274 | 275 | sleep 1.5, fnDone 276 | 277 | # Show or hide the dashboard 278 | transitionFunction $dashboard, {stagger, widgets, originalLocations}, done 279 | 280 | # Select a member at random from an object. 281 | # 282 | # If 'allowedMembers' is an array of strings, then only the corresponding members will be 283 | # considered for selection. 284 | # 285 | # Returns a "{key, value}" object. 286 | pickMember = (object, allowedMembers=null) -> 287 | answer = null 288 | functionArray = [] 289 | if allowedMembers? 290 | if not isArray allowedMembers then allowedMembers = [allowedMembers] 291 | for memberName in allowedMembers 292 | if memberName of object then functionArray.push {key: memberName, value: object[memberName]} 293 | else 294 | for memberName, member of object 295 | functionArray.push {key: memberName, value: member} 296 | 297 | if functionArray.length > 0 298 | index = Math.floor(Math.random()*functionArray.length); 299 | answer = functionArray[index] 300 | 301 | return answer 302 | 303 | # Cycle the dashboard to the next dashboard. 304 | # 305 | # If a transition is already in progress, this function does nothing. 306 | Dashing.cycleDashboardsNow = do () -> 307 | transitionInProgress = false 308 | visibleIndex = 0 309 | (options = {}) -> 310 | return if transitionInProgress 311 | transitionInProgress = true 312 | 313 | {stagger, fastTransition, boardnumber, transitiontype} = options 314 | stagger = !!stagger 315 | fastTransition = !!fastTransition 316 | 317 | $dashboards = $('.gridster') 318 | 319 | # Work out which dashboard to show 320 | oldVisibleIndex = visibleIndex 321 | if boardnumber? 322 | visibleIndex = boardnumber - 1 323 | else 324 | visibleIndex++ 325 | if visibleIndex >= $dashboards.length 326 | visibleIndex = 0 327 | 328 | if oldVisibleIndex == visibleIndex 329 | # Only one dashboard. Disable fast transitions 330 | fastTransition = false 331 | 332 | doneCount = 0 333 | doneFn = () -> 334 | doneCount++ 335 | # Only set transitionInProgress to false when both the show and the hide functions 336 | # are finished. 337 | if doneCount is 2 338 | transitionInProgress = false 339 | 340 | # Hide the old dashboard 341 | hideFunction = pickMember hideFunctions 342 | 343 | showNewDashboard = () -> 344 | options.onTransition?($($dashboards[visibleIndex])) 345 | showFunction = null 346 | chainsTo = hideFunction.value.chainsTo 347 | if isString chainsTo 348 | showFunction = showFunctions[chainsTo] 349 | else if chainsTo? 350 | showFunction = {key: "chainsTo", value: chainsTo} 351 | 352 | if !showFunction 353 | showFunction = pickMember showFunctions 354 | 355 | # console.log "Showing dashboard #{visibleIndex} #{showFunction.key}" 356 | showHideDashboard true, stagger, $dashboards[visibleIndex], showFunction.value, () -> 357 | doneFn() 358 | 359 | # console.log "Hiding dashboard #{oldVisibleIndex} #{hideFunction.key}" 360 | showHideDashboard false, stagger, $dashboards[oldVisibleIndex], hideFunction.value, () -> 361 | if !fastTransition 362 | showNewDashboard() 363 | doneFn() 364 | 365 | # If fast transitions are enabled, then don't wait for the hiding animation to complete 366 | # before showing the new dashboard. 367 | if fastTransition then showNewDashboard() 368 | 369 | return null 370 | 371 | # Adapted from http://stackoverflow.com/questions/1403888/get-url-parameter-with-javascript-or-jquery 372 | getURLParameter = (name) -> 373 | encodedParameter = (RegExp(name + '=' + '(.+?)(&|$)').exec(location.search)||[null,null])[1] 374 | return if encodedParameter? then decodeURI(encodedParameter) else null 375 | 376 | # Cause dashing to cycle from one dashboard to the next. 377 | # 378 | # Dashboard cycling can be bypassed by passing a "page" parameter in the url. For example, 379 | # going to http://dashboardserver/mydashboard?page=2 will show the second dashboard in the list 380 | # and will not cycle. 381 | # 382 | # Options: 383 | # * `timeInSeconds` - The time to display each dashboard, in seconds. If 0, then dashboards will 384 | # not automatically cycle, but can be cycled manually by calling `cycleDashboardsNow()`. 385 | # * `stagger` - If this is true, each widget will be transitioned individually at slightly 386 | # randomized times. This gives a more random look. If false, then all wigets will be moved 387 | # at the same time. Note if `timeInSeconds` is 0, then this option is ignored (but can, instead, 388 | # be passed to `cycleDashboardsNow()`.) 389 | # * `fastTransition` - If true, then we will run the show and hide transitions simultaneously. 390 | # This gets your new dashboard up onto the screen faster. 391 | # * `onTransition($newDashboard)` - A function to call before a dashboard is displayed. 392 | # 393 | Dashing.cycleDashboards = (options) -> 394 | timeInSeconds = if options.timeInSeconds? then options.timeInSeconds else 20 395 | 396 | $dashboards = $('.gridster') 397 | 398 | startDashboard = if options.page? then options.page else 1 399 | startDashboard = Math.max startDashboard, 1 400 | startDashboard = Math.min startDashboard, $dashboards.length 401 | 402 | $dashboards.each (dashboardIndex, dashboard) -> 403 | # Hide all but the first dashboard. 404 | $(dashboard).toggle(dashboardIndex is (startDashboard - 1)) 405 | 406 | # Set all dashboards to position: absolute so they stack one on top of the other 407 | $(dashboard).css "position": "absolute" 408 | 409 | # If the user specified a dashboard, then don't cycle from one dashboard to the next. 410 | if !startDashboardParam? and (timeInSeconds > 0) 411 | cycleFn = () -> Dashing.cycleDashboardsNow(options) 412 | setInterval cycleFn, timeInSeconds * 1000 413 | 414 | $(document).keypress (event) -> 415 | # Cycle to next dashboard on space 416 | if event.keyCode is 32 then Dashing.cycleDashboardsNow(options) 417 | return true 418 | 419 | # Customized version of `Dashing.gridsterLayout()` which supports multiple dashboards. 420 | Dashing.cycleGridsterLayout = (positions) -> 421 | #positions = positions.replace(/^"|"$/g, '') # ?? 422 | positions = JSON.parse(positions) 423 | $dashboards = $(".gridster > ul") 424 | if isArray(positions) and ($dashboards.length == positions.length) 425 | Dashing.customGridsterLayout = true 426 | for position, index in positions 427 | $dashboard = $($dashboards[index]) 428 | widgets = $dashboard.children("[data-row^=]") 429 | for widget, index in widgets 430 | $(widget).attr('data-row', position[index].row) 431 | $(widget).attr('data-col', position[index].col) 432 | else 433 | console.log "Warning: Could not apply custom layout!" 434 | 435 | # Redefine functions for saving layout 436 | sleep 0.1, () -> 437 | Dashing.getWidgetPositions = -> 438 | dashboardPositions = [] 439 | for dashboard in $(".gridster > ul") 440 | dashboardPositions.push $(dashboard).gridster().data('gridster').serialize() 441 | return dashboardPositions 442 | 443 | Dashing.showGridsterInstructions = -> 444 | newWidgetPositions = Dashing.getWidgetPositions() 445 | 446 | if !isArray(newWidgetPositions[0]) 447 | $('#save-gridster').slideDown() 448 | $('#gridster-code').text(" 449 | Something went wrong - reload the page and try again. 450 | ") 451 | else 452 | unless JSON.stringify(newWidgetPositions) == JSON.stringify(Dashing.currentWidgetPositions) 453 | Dashing.currentWidgetPositions = newWidgetPositions 454 | $('#save-gridster').slideDown() 455 | $('#gridster-code').text(" 456 | 461 | ") -------------------------------------------------------------------------------- /smartapps/DashingAccess.groovy: -------------------------------------------------------------------------------- 1 | /** 2 | * Dashing Access 3 | * 4 | * Copyright 2014 florianz 5 | * 6 | * Author: florianz 7 | * Contributor: bmmiller, Dianoga, mattjfrank, ronnycarr 8 | * 9 | */ 10 | 11 | 12 | // 13 | // Definition 14 | // 15 | definition( 16 | name: "Dashing Access", 17 | namespace: "florianz", 18 | author: "florianz", 19 | description: "API access for Dashing dashboards.", 20 | category: "Convenience", 21 | iconUrl: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png", 22 | iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience@2x.png", 23 | oauth: true) { 24 | } 25 | 26 | 27 | // 28 | // Preferences 29 | // 30 | preferences { 31 | section("Allow access to the following things...") { 32 | input "contacts", "capability.contactSensor", title: "Which contact sensors?", multiple: true, required: false 33 | input "locks", "capability.lock", title: "Which Locks?", multiple: true, required: false 34 | input "meters", "capability.powerMeter", title: "Which meters?", multiple: true, required: false 35 | input "motions", "capability.motionSensor", title: "Which motion sensors?", multiple: true, required: false 36 | input "presences", "capability.presenceSensor", title: "Which presence sensors?", multiple: true, required: false 37 | input "dimmers", "capability.switchLevel", title: "Which dimmers?", multiple: true, required: false 38 | input "switches", "capability.switch", title: "Which switches?", multiple: true, required: false 39 | input "temperatures", "capability.temperatureMeasurement", title: "Which temperature sensors?", multiple: true, required: false 40 | input "humidities", "capability.relativeHumidityMeasurement", title: "Which humidity sensors?", multiple: true, required: false 41 | 42 | } 43 | } 44 | 45 | 46 | // 47 | // Mappings 48 | // 49 | mappings { 50 | path("/config") { 51 | action: [ 52 | GET: "getConfig", 53 | POST: "postConfig" 54 | ] 55 | } 56 | path("/contact") { 57 | action: [ 58 | GET: "getContact" 59 | ] 60 | } 61 | path("/lock") { 62 | action: [ 63 | GET: "getLock", 64 | POST: "postLock" 65 | ] 66 | } 67 | path("/mode") { 68 | action: [ 69 | GET: "getMode", 70 | POST: "postMode" 71 | ] 72 | } 73 | path("/motion") { 74 | action: [ 75 | GET: "getMotion" 76 | ] 77 | } 78 | path("/phrase") { 79 | action: [ 80 | POST: "postPhrase" 81 | ] 82 | } 83 | path("/power") { 84 | action: [ 85 | GET: "getPower" 86 | ] 87 | } 88 | path("/presence") { 89 | action: [ 90 | GET: "getPresence" 91 | ] 92 | } 93 | path("/dimmer") { 94 | action: [ 95 | GET: "getDimmer", 96 | POST: "postDimmer" 97 | ] 98 | } 99 | path("/dimmer/level") { 100 | action: [ 101 | POST: "dimmerLevel" 102 | ] 103 | } 104 | path("/switch") { 105 | action: [ 106 | GET: "getSwitch", 107 | POST: "postSwitch" 108 | ] 109 | } 110 | path("/temperature") { 111 | action: [ 112 | GET: "getTemperature" 113 | ] 114 | } 115 | path("/humidity") { 116 | action: [ 117 | GET: "getHumidity" 118 | ] 119 | } 120 | path("/weather") { 121 | action: [ 122 | GET: "getWeather" 123 | ] 124 | } 125 | } 126 | 127 | 128 | // 129 | // Installation 130 | // 131 | def installed() { 132 | initialize() 133 | } 134 | 135 | def updated() { 136 | unsubscribe() 137 | initialize() 138 | } 139 | 140 | def initialize() { 141 | state.dashingURI = "" 142 | state.dashingAuthToken = "" 143 | state.widgets = [ 144 | "contact": [:], 145 | "lock": [:], 146 | "mode": [], 147 | "motion": [:], 148 | "power": [:], 149 | "presence": [:], 150 | "dimmer": [:], 151 | "switch": [:], 152 | "temperature": [:], 153 | "humidity": [:], 154 | 155 | ] 156 | 157 | subscribe(contacts, "contact", contactHandler) 158 | subscribe(location, locationHandler) 159 | subscribe(locks, "lock", lockHandler) 160 | subscribe(motions, "motion", motionHandler) 161 | subscribe(meters, "power", meterHandler) 162 | subscribe(presences, "presence", presenceHandler) 163 | subscribe(dimmers, "switch", dimmerSwitch) 164 | subscribe(dimmers, "level", dimmerHandler) 165 | subscribe(switches, "switch", switchHandler) 166 | subscribe(temperatures, "temperature", temperatureHandler) 167 | subscribe(humidities, "humidity", humidityHandler) 168 | 169 | } 170 | 171 | 172 | // 173 | // Config 174 | // 175 | def getConfig() { 176 | ["dashingURI": state.dashingURI, "dashingAuthToken": state.dashingAuthToken] 177 | } 178 | 179 | def postConfig() { 180 | state.dashingURI = request.JSON?.dashingURI 181 | state.dashingAuthToken = request.JSON?.dashingAuthToken 182 | respondWithSuccess() 183 | } 184 | 185 | // 186 | // Contacts 187 | // 188 | def getContact() { 189 | def deviceId = request.JSON?.deviceId 190 | log.debug "getContact ${deviceId}" 191 | 192 | if (deviceId) { 193 | registerWidget("contact", deviceId, request.JSON?.widgetId) 194 | 195 | def whichContact = contacts.find { it.displayName == deviceId } 196 | if (!whichContact) { 197 | return respondWithStatus(404, "Device '${deviceId}' not found.") 198 | } else { 199 | return [ 200 | "deviceId": deviceId, 201 | "state": whichContact.currentContact] 202 | } 203 | } 204 | 205 | def result = [:] 206 | contacts.each { 207 | result[it.displayName] = [ 208 | "state": it.currentContact, 209 | "widgetId": state.widgets.contact[it.displayName]]} 210 | 211 | return result 212 | } 213 | 214 | def contactHandler(evt) { 215 | def widgetId = state.widgets.contact[evt.displayName] 216 | notifyWidget(widgetId, ["state": evt.value]) 217 | } 218 | 219 | // 220 | // Locks 221 | // 222 | def getLock() { 223 | def deviceId = request.JSON?.deviceId 224 | log.debug "getLock ${deviceId}" 225 | 226 | if (deviceId) { 227 | registerWidget("lock", deviceId, request.JSON?.widgetId) 228 | 229 | def whichLock = locks.find { it.displayName == deviceId } 230 | if (!whichLock) { 231 | return respondWithStatus(404, "Device '${deviceId}' not found.") 232 | } else { 233 | return [ 234 | "deviceId": deviceId, 235 | "state": whichLock.currentLock] 236 | } 237 | } 238 | 239 | def result = [:] 240 | locks.each { 241 | result[it.displayName] = [ 242 | "state": it.currentLock, 243 | "widgetId": state.widgets.lock[it.displayName]]} 244 | 245 | return result 246 | } 247 | 248 | def postLock() { 249 | def command = request.JSON?.command 250 | def deviceId = request.JSON?.deviceId 251 | log.debug "postLock ${deviceId}, ${command}" 252 | 253 | if (command && deviceId) { 254 | def whichLock = locks.find { it.displayName == deviceId } 255 | if (!whichLock) { 256 | return respondWithStatus(404, "Device '${deviceId}' not found.") 257 | } else { 258 | whichLock."$command"() 259 | } 260 | } 261 | return respondWithSuccess() 262 | } 263 | 264 | def lockHandler(evt) { 265 | def widgetId = state.widgets.lock[evt.displayName] 266 | notifyWidget(widgetId, ["state": evt.value]) 267 | } 268 | 269 | // 270 | // Meters 271 | // 272 | def getPower() { 273 | def deviceId = request.JSON?.deviceId 274 | log.debug "getPower ${deviceId}" 275 | 276 | if (deviceId) { 277 | registerWidget("power", deviceId, request.JSON?.widgetId) 278 | 279 | def whichMeter = meters.find { it.displayName == deviceId } 280 | if (!whichMeter) { 281 | return respondWithStatus(404, "Device '${deviceId}' not found.") 282 | } else { 283 | return [ 284 | "deviceId": deviceId, 285 | "value": whichMeter.currentValue("power")] 286 | } 287 | } 288 | 289 | def result = [:] 290 | meters.each { 291 | result[it.displayName] = [ 292 | "value": it.currentValue("power"), 293 | "widgetId": state.widgets.power[it.displayName]]} 294 | 295 | return result 296 | } 297 | 298 | def meterHandler(evt) { 299 | def widgetId = state.widgets.power[evt.displayName] 300 | notifyWidget(widgetId, ["value": evt.value]) 301 | } 302 | 303 | // 304 | // Modes 305 | // 306 | def getMode() { 307 | def widgetId = request.JSON?.widgetId 308 | if (widgetId) { 309 | if (!state['widgets']['mode'].contains(widgetId)) { 310 | state['widgets']['mode'].add(widgetId) 311 | log.debug "registerWidget for mode: ${widgetId}" 312 | } 313 | } 314 | 315 | log.debug "getMode" 316 | return ["mode": location.mode] 317 | } 318 | 319 | def postMode() { 320 | def mode = request.JSON?.mode 321 | log.debug "postMode ${mode}" 322 | 323 | if (mode) { 324 | setLocationMode(mode) 325 | } 326 | 327 | if (location.mode != mode) { 328 | return respondWithStatus(404, "Mode not found.") 329 | } 330 | return respondWithSuccess() 331 | } 332 | 333 | def locationHandler(evt) { 334 | for (i in state['widgets']['mode']) { 335 | notifyWidget(i, ["mode": evt.value]) 336 | } 337 | } 338 | 339 | // 340 | // Motions 341 | // 342 | def getMotion() { 343 | def deviceId = request.JSON?.deviceId 344 | log.debug "getMotion ${deviceId}" 345 | 346 | if (deviceId) { 347 | registerWidget("motion", deviceId, request.JSON?.widgetId) 348 | 349 | def whichMotion = motions.find { it.displayName == deviceId } 350 | if (!whichMotion) { 351 | return respondWithStatus(404, "Device '${deviceId}' not found.") 352 | } else { 353 | return [ 354 | "deviceId": deviceId, 355 | "state": whichMotion.currentMotion] 356 | } 357 | } 358 | 359 | def result = [:] 360 | motionss.each { 361 | result[it.displayName] = [ 362 | "state": it.currentMotion, 363 | "widgetId": state.widgets.motion[it.displayName]]} 364 | 365 | return result 366 | } 367 | 368 | def motionHandler(evt) { 369 | def widgetId = state.widgets.motion[evt.displayName] 370 | notifyWidget(widgetId, ["state": evt.value]) 371 | } 372 | 373 | // 374 | // Phrases 375 | // 376 | def postPhrase() { 377 | def phrase = request.JSON?.phrase 378 | log.debug "postPhrase ${phrase}" 379 | 380 | if (!phrase) { 381 | respondWithStatus(404, "Phrase not specified.") 382 | } 383 | 384 | location.helloHome.execute(phrase) 385 | 386 | return respondWithSuccess() 387 | 388 | } 389 | 390 | // 391 | // Presences 392 | // 393 | def getPresence() { 394 | def deviceId = request.JSON?.deviceId 395 | log.debug "getPresence ${deviceId}" 396 | 397 | if (deviceId) { 398 | registerWidget("presence", deviceId, request.JSON?.widgetId) 399 | 400 | def whichPresence = presences.find { it.displayName == deviceId } 401 | if (!whichPresence) { 402 | return respondWithStatus(404, "Device '${deviceId}' not found.") 403 | } else { 404 | return [ 405 | "deviceId": deviceId, 406 | "state": whichPresence.currentPresence] 407 | } 408 | } 409 | 410 | def result = [:] 411 | presences.each { 412 | result[it.displayName] = [ 413 | "state": it.currentPresence, 414 | "widgetId": state.widgets.presence[it.displayName]]} 415 | 416 | return result 417 | } 418 | 419 | def presenceHandler(evt) { 420 | def widgetId = state.widgets.presence[evt.displayName] 421 | notifyWidget(widgetId, ["state": evt.value]) 422 | } 423 | 424 | // 425 | // Dimmers 426 | // 427 | def getDimmer() { 428 | def deviceId = request.JSON?.deviceId 429 | log.debug "getDimmer ${deviceId}" 430 | 431 | if (deviceId) { 432 | registerWidget("dimmer", deviceId, request.JSON?.widgetId) 433 | 434 | def whichDimmer = dimmers.find { it.displayName == deviceId } 435 | if (!whichDimmer) { 436 | return respondWithStatus(404, "Device '${deviceId}' not found.") 437 | } else { 438 | return [ 439 | "deviceId": deviceId, 440 | "level": whichDimmer.currentValue("level"), 441 | "state": whichDimmer.currentValue("switch") 442 | ] 443 | } 444 | } 445 | 446 | def result = [:] 447 | dimmers.each { 448 | result[it.displayName] = [ 449 | "state": it.currentValue("switch"), 450 | "level": it.currentValue("level"), 451 | "widgetId": state.widgets.dimmer[it.displayName]]} 452 | 453 | return result 454 | } 455 | 456 | def postDimmer() { 457 | def command = request.JSON?.command 458 | def deviceId = request.JSON?.deviceId 459 | log.debug "postDimmer ${deviceId}, ${command}" 460 | 461 | if (command && deviceId) { 462 | def whichDimmer = dimmers.find { it.displayName == deviceId } 463 | if (!whichDimmer) { 464 | return respondWithStatus(404, "Device '${deviceId}' not found.") 465 | } else { 466 | whichDimmer."$command"() 467 | } 468 | } 469 | return respondWithSuccess() 470 | } 471 | 472 | def dimmerLevel() { 473 | def command = request.JSON?.command 474 | def deviceId = request.JSON?.deviceId 475 | log.debug "dimmerLevel ${deviceId}, ${command}" 476 | command = command.toInteger() 477 | if (command && deviceId) { 478 | def whichDimmer = dimmers.find { it.displayName == deviceId } 479 | if (!whichDimmer) { 480 | return respondWithStatus(404, "Device '${deviceId}' not found.") 481 | } else { 482 | whichDimmer.setLevel(command) 483 | } 484 | } 485 | return respondWithSuccess() 486 | } 487 | 488 | def dimmerHandler(evt) { 489 | def widgetId = state.widgets.dimmer[evt.displayName] 490 | pause(1000) 491 | notifyWidget(widgetId, ["level": evt.value]) 492 | } 493 | 494 | def dimmerSwitch(evt) { 495 | def whichDimmer = dimmers.find { it.displayName == evt.displayName } 496 | def widgetId = state.widgets.dimmer[evt.displayName] 497 | notifyWidget(widgetId, ["state": evt.value]) 498 | } 499 | 500 | // 501 | // Switches 502 | // 503 | def getSwitch() { 504 | def deviceId = request.JSON?.deviceId 505 | log.debug "getSwitch ${deviceId}" 506 | 507 | if (deviceId) { 508 | registerWidget("switch", deviceId, request.JSON?.widgetId) 509 | 510 | def whichSwitch = switches.find { it.displayName == deviceId } 511 | if (!whichSwitch) { 512 | return respondWithStatus(404, "Device '${deviceId}' not found.") 513 | } else { 514 | return [ 515 | "deviceId": deviceId, 516 | "switch": whichSwitch.currentSwitch] 517 | } 518 | } 519 | 520 | def result = [:] 521 | switches.each { 522 | result[it.displayName] = [ 523 | "state": it.currentSwitch, 524 | "widgetId": state.widgets.switch[it.displayName]]} 525 | 526 | return result 527 | } 528 | 529 | def postSwitch() { 530 | def command = request.JSON?.command 531 | def deviceId = request.JSON?.deviceId 532 | log.debug "postSwitch ${deviceId}, ${command}" 533 | 534 | if (command && deviceId) { 535 | def whichSwitch = switches.find { it.displayName == deviceId } 536 | if (!whichSwitch) { 537 | return respondWithStatus(404, "Device '${deviceId}' not found.") 538 | } else { 539 | whichSwitch."$command"() 540 | } 541 | } 542 | return respondWithSuccess() 543 | } 544 | 545 | def switchHandler(evt) { 546 | def widgetId = state.widgets.switch[evt.displayName] 547 | notifyWidget(widgetId, ["state": evt.value]) 548 | } 549 | 550 | // 551 | // Temperatures 552 | // 553 | def getTemperature() { 554 | def deviceId = request.JSON?.deviceId 555 | log.debug "getTemperature ${deviceId}" 556 | 557 | if (deviceId) { 558 | registerWidget("temperature", deviceId, request.JSON?.widgetId) 559 | 560 | def whichTemperature = temperatures.find { it.displayName == deviceId } 561 | if (!whichTemperature) { 562 | return respondWithStatus(404, "Device '${deviceId}' not found.") 563 | } else { 564 | return [ 565 | "deviceId": deviceId, 566 | "value": whichTemperature.currentTemperature] 567 | } 568 | } 569 | 570 | def result = [:] 571 | temperatures.each { 572 | result[it.displayName] = [ 573 | "value": it.currentTemperature, 574 | "widgetId": state.widgets.temperature[it.displayName]]} 575 | 576 | return result 577 | } 578 | 579 | def temperatureHandler(evt) { 580 | def widgetId = state.widgets.temperature[evt.displayName] 581 | notifyWidget(widgetId, ["value": evt.value]) 582 | } 583 | 584 | // 585 | // Humidities 586 | // 587 | def getHumidity() { 588 | def deviceId = request.JSON?.deviceId 589 | log.debug "getHumidity ${deviceId}" 590 | 591 | if (deviceId) { 592 | registerWidget("humidity", deviceId, request.JSON?.widgetId) 593 | 594 | def whichHumidity = humidities.find { it.displayName == deviceId } 595 | if (!whichHumidity) { 596 | return respondWithStatus(404, "Device '${deviceId}' not found.") 597 | } else { 598 | return [ 599 | "deviceId": deviceId, 600 | "value": whichHumidity.currentHumidity] 601 | } 602 | } 603 | 604 | def result = [:] 605 | humidities.each { 606 | result[it.displayName] = [ 607 | "value": it.currentHumidity, 608 | "widgetId": state.widgets.humidity[it.displayName]]} 609 | 610 | return result 611 | } 612 | 613 | def humidityHandler(evt) { 614 | def widgetId = state.widgets.humidity[evt.displayName] 615 | notifyWidget(widgetId, ["value": evt.value]) 616 | } 617 | 618 | // 619 | // Weather 620 | // 621 | def getWeather() { 622 | def feature = request.JSON?.feature 623 | if (!feature) { 624 | feature = "conditions" 625 | } 626 | return getWeatherFeature(feature) 627 | } 628 | 629 | 630 | // 631 | // Widget Helpers 632 | // 633 | private registerWidget(deviceType, deviceId, widgetId) { 634 | if (deviceType && deviceId && widgetId) { 635 | state['widgets'][deviceType][deviceId] = widgetId 636 | log.debug "registerWidget ${deviceType}:${deviceId}@${widgetId}" 637 | } 638 | } 639 | 640 | private notifyWidget(widgetId, data) { 641 | if (widgetId && state.dashingAuthToken) { 642 | def uri = getWidgetURI(widgetId) 643 | data["auth_token"] = state.dashingAuthToken 644 | log.debug "notifyWidget ${uri} ${data}" 645 | httpPostJson(uri, data) 646 | } 647 | } 648 | 649 | private getWidgetURI(widgetId) { 650 | state.dashingURI + "/widgets/${widgetId}" 651 | } 652 | 653 | 654 | // 655 | // Response Helpers 656 | // 657 | private respondWithStatus(status, details = null) { 658 | def response = ["error": status as Integer] 659 | if (details) { 660 | response["details"] = details as String 661 | } 662 | return response 663 | } 664 | 665 | private respondWithSuccess() { 666 | return respondWithStatus(0) 667 | } 668 | -------------------------------------------------------------------------------- /assets/javascripts/jquery.knob.js: -------------------------------------------------------------------------------- 1 | /*!jQuery Knob*/ 2 | /** 3 | * Downward compatible, touchable dial 4 | * 5 | * Version: 1.2.0 (15/07/2012) 6 | * Requires: jQuery v1.7+ 7 | * 8 | * Copyright (c) 2012 Anthony Terrien 9 | * Under MIT and GPL licenses: 10 | * http://www.opensource.org/licenses/mit-license.php 11 | * http://www.gnu.org/licenses/gpl.html 12 | * 13 | * Thanks to vor, eskimoblood, spiffistan, FabrizioC 14 | */ 15 | $(function () { 16 | 17 | /** 18 | * Kontrol library 19 | */ 20 | "use strict"; 21 | 22 | /** 23 | * Definition of globals and core 24 | */ 25 | var k = {}, // kontrol 26 | max = Math.max, 27 | min = Math.min; 28 | 29 | k.c = {}; 30 | k.c.d = $(document); 31 | k.c.t = function (e) { 32 | return e.originalEvent.touches.length - 1; 33 | }; 34 | 35 | /** 36 | * Kontrol Object 37 | * 38 | * Definition of an abstract UI control 39 | * 40 | * Each concrete component must call this one. 41 | * 42 | * k.o.call(this); 43 | * 44 | */ 45 | k.o = function () { 46 | var s = this; 47 | 48 | this.o = null; // array of options 49 | this.$ = null; // jQuery wrapped element 50 | this.i = null; // mixed HTMLInputElement or array of HTMLInputElement 51 | this.g = null; // 2D graphics context for 'pre-rendering' 52 | this.v = null; // value ; mixed array or integer 53 | this.cv = null; // change value ; not commited value 54 | this.x = 0; // canvas x position 55 | this.y = 0; // canvas y position 56 | this.$c = null; // jQuery canvas element 57 | this.c = null; // rendered canvas context 58 | this.t = 0; // touches index 59 | this.isInit = false; 60 | this.fgColor = null; // main color 61 | this.pColor = null; // previous color 62 | this.dH = null; // draw hook 63 | this.cH = null; // change hook 64 | this.eH = null; // cancel hook 65 | this.rH = null; // release hook 66 | 67 | this.run = function () { 68 | var cf = function (e, conf) { 69 | var k; 70 | for (k in conf) { 71 | s.o[k] = conf[k]; 72 | } 73 | s.init(); 74 | s._configure() 75 | ._draw(); 76 | }; 77 | 78 | if(this.$.data('kontroled')) return; 79 | this.$.data('kontroled', true); 80 | 81 | this.extend(); 82 | this.o = $.extend( 83 | { 84 | // Config 85 | min : this.$.data('min') || 0, 86 | max : this.$.data('max') || 100, 87 | stopper : true, 88 | readOnly : this.$.data('readonly'), 89 | 90 | // UI 91 | cursor : (this.$.data('cursor') === true && 30) 92 | || this.$.data('cursor') 93 | || 0, 94 | thickness : this.$.data('thickness') || 0.35, 95 | width : this.$.data('width') || 200, 96 | height : this.$.data('height') || 200, 97 | displayInput : this.$.data('displayinput') == null || this.$.data('displayinput'), 98 | displayPrevious : this.$.data('displayprevious'), 99 | fgColor : this.$.data('fgcolor') || '#87CEEB', 100 | inline : false, 101 | 102 | // Hooks 103 | draw : null, // function () {} 104 | change : null, // function (value) {} 105 | cancel : null, // function () {} 106 | release : null // function (value) {} 107 | }, this.o 108 | ); 109 | 110 | // routing value 111 | if(this.$.is('fieldset')) { 112 | 113 | // fieldset = array of integer 114 | this.v = {}; 115 | this.i = this.$.find('input') 116 | this.i.each(function(k) { 117 | var $this = $(this); 118 | s.i[k] = $this; 119 | s.v[k] = $this.val(); 120 | 121 | $this.bind( 122 | 'change' 123 | , function () { 124 | var val = {}; 125 | val[k] = $this.val(); 126 | s.val(val); 127 | } 128 | ); 129 | }); 130 | this.$.find('legend').remove(); 131 | 132 | } else { 133 | // input = integer 134 | this.i = this.$; 135 | this.v = this.$.val(); 136 | (this.v == '') && (this.v = this.o.min); 137 | 138 | this.$.bind( 139 | 'change' 140 | , function () { 141 | s.val(s.$.val()); 142 | } 143 | ); 144 | } 145 | 146 | (!this.o.displayInput) && this.$.hide(); 147 | 148 | this.$c = $(''); 151 | this.c = this.$c[0].getContext("2d"); 152 | 153 | this.$ 154 | .wrap($('
    ')) 157 | .before(this.$c); 158 | 159 | if (this.v instanceof Object) { 160 | this.cv = {}; 161 | this.copy(this.v, this.cv); 162 | } else { 163 | this.cv = this.v; 164 | } 165 | 166 | this.$ 167 | .bind("configure", cf) 168 | .parent() 169 | .bind("configure", cf); 170 | 171 | this._listen() 172 | ._configure() 173 | ._xy() 174 | .init(); 175 | 176 | this.isInit = true; 177 | 178 | this._draw(); 179 | 180 | return this; 181 | }; 182 | 183 | this._draw = function () { 184 | 185 | // canvas pre-rendering 186 | var d = true, 187 | c = document.createElement('canvas'); 188 | 189 | c.width = s.o.width; 190 | c.height = s.o.height; 191 | s.g = c.getContext('2d'); 192 | 193 | s.clear(); 194 | 195 | s.dH 196 | && (d = s.dH()); 197 | 198 | (d !== false) && s.draw(); 199 | 200 | s.c.drawImage(c, 0, 0); 201 | c = null; 202 | }; 203 | 204 | this._touch = function (e) { 205 | 206 | var touchMove = function (e) { 207 | 208 | var v = s.xy2val( 209 | e.originalEvent.touches[s.t].pageX, 210 | e.originalEvent.touches[s.t].pageY 211 | ); 212 | 213 | if (v == s.cv) return; 214 | 215 | if ( 216 | s.cH 217 | && (s.cH(v) === false) 218 | ) return; 219 | 220 | 221 | s.change(v); 222 | s._draw(); 223 | }; 224 | 225 | // get touches index 226 | this.t = k.c.t(e); 227 | 228 | // First touch 229 | touchMove(e); 230 | 231 | // Touch events listeners 232 | k.c.d 233 | .bind("touchmove.k", touchMove) 234 | .bind( 235 | "touchend.k" 236 | , function () { 237 | k.c.d.unbind('touchmove.k touchend.k'); 238 | 239 | if ( 240 | s.rH 241 | && (s.rH(s.cv) === false) 242 | ) return; 243 | 244 | s.val(s.cv); 245 | } 246 | ); 247 | 248 | return this; 249 | }; 250 | 251 | this._mouse = function (e) { 252 | 253 | var mouseMove = function (e) { 254 | var v = s.xy2val(e.pageX, e.pageY); 255 | if (v == s.cv) return; 256 | 257 | if ( 258 | s.cH 259 | && (s.cH(v) === false) 260 | ) return; 261 | 262 | s.change(v); 263 | s._draw(); 264 | }; 265 | 266 | // First click 267 | mouseMove(e); 268 | 269 | // Mouse events listeners 270 | k.c.d 271 | .bind("mousemove.k", mouseMove) 272 | .bind( 273 | // Escape key cancel current change 274 | "keyup.k" 275 | , function (e) { 276 | if (e.keyCode === 27) { 277 | k.c.d.unbind("mouseup.k mousemove.k keyup.k"); 278 | 279 | if ( 280 | s.eH 281 | && (s.eH() === false) 282 | ) return; 283 | 284 | s.cancel(); 285 | } 286 | } 287 | ) 288 | .bind( 289 | "mouseup.k" 290 | , function (e) { 291 | k.c.d.unbind('mousemove.k mouseup.k keyup.k'); 292 | 293 | if ( 294 | s.rH 295 | && (s.rH(s.cv) === false) 296 | ) return; 297 | 298 | s.val(s.cv); 299 | } 300 | ); 301 | 302 | return this; 303 | }; 304 | 305 | this._xy = function () { 306 | var o = this.$c.offset(); 307 | this.x = o.left; 308 | this.y = o.top; 309 | return this; 310 | }; 311 | 312 | this._listen = function () { 313 | 314 | if (!this.o.readOnly) { 315 | this.$c 316 | .bind( 317 | "mousedown" 318 | , function (e) { 319 | e.preventDefault(); 320 | s._xy()._mouse(e); 321 | } 322 | ) 323 | .bind( 324 | "touchstart" 325 | , function (e) { 326 | e.preventDefault(); 327 | s._xy()._touch(e); 328 | } 329 | ); 330 | this.listen(); 331 | } else { 332 | this.$.attr('readonly', 'readonly'); 333 | } 334 | 335 | return this; 336 | }; 337 | 338 | this._configure = function () { 339 | 340 | // Hooks 341 | if (this.o.draw) this.dH = this.o.draw; 342 | if (this.o.change) this.cH = this.o.change; 343 | if (this.o.cancel) this.eH = this.o.cancel; 344 | if (this.o.release) this.rH = this.o.release; 345 | 346 | if (this.o.displayPrevious) { 347 | this.pColor = this.h2rgba(this.o.fgColor, "0.4"); 348 | this.fgColor = this.h2rgba(this.o.fgColor, "0.6"); 349 | } else { 350 | this.fgColor = this.o.fgColor; 351 | } 352 | 353 | return this; 354 | }; 355 | 356 | this._clear = function () { 357 | this.$c[0].width = this.$c[0].width; 358 | }; 359 | 360 | // Abstract methods 361 | this.listen = function () {}; // on start, one time 362 | this.extend = function () {}; // each time configure triggered 363 | this.init = function () {}; // each time configure triggered 364 | this.change = function (v) {}; // on change 365 | this.val = function (v) {}; // on release 366 | this.xy2val = function (x, y) {}; // 367 | this.draw = function () {}; // on change / on release 368 | this.clear = function () { this._clear(); }; 369 | 370 | // Utils 371 | this.h2rgba = function (h, a) { 372 | var rgb; 373 | h = h.substring(1,7) 374 | rgb = [parseInt(h.substring(0,2),16) 375 | ,parseInt(h.substring(2,4),16) 376 | ,parseInt(h.substring(4,6),16)]; 377 | return "rgba(" + rgb[0] + "," + rgb[1] + "," + rgb[2] + "," + a + ")"; 378 | }; 379 | 380 | this.copy = function (f, t) { 381 | for (var i in f) { t[i] = f[i]; } 382 | }; 383 | }; 384 | 385 | 386 | /** 387 | * k.Dial 388 | */ 389 | k.Dial = function () { 390 | k.o.call(this); 391 | 392 | this.startAngle = null; 393 | this.xy = null; 394 | this.radius = null; 395 | this.lineWidth = null; 396 | this.cursorExt = null; 397 | this.w2 = null; 398 | this.PI2 = 2*Math.PI; 399 | 400 | this.extend = function () { 401 | this.o = $.extend( 402 | { 403 | bgColor : this.$.data('bgcolor') || '#EEEEEE', 404 | angleOffset : this.$.data('angleoffset') || 0, 405 | angleArc : this.$.data('anglearc') || 360, 406 | inline : true 407 | }, this.o 408 | ); 409 | }; 410 | 411 | this.val = function (v) { 412 | if (null != v) { 413 | this.cv = this.o.stopper ? max(min(v, this.o.max), this.o.min) : v; 414 | this.v = this.cv; 415 | this.$.val(this.v); 416 | this._draw(); 417 | } else { 418 | return this.v; 419 | } 420 | }; 421 | 422 | this.xy2val = function (x, y) { 423 | var a, ret; 424 | 425 | a = Math.atan2( 426 | x - (this.x + this.w2) 427 | , - (y - this.y - this.w2) 428 | ) - this.angleOffset; 429 | 430 | if(this.angleArc != this.PI2 && (a < 0) && (a > -0.5)) { 431 | // if isset angleArc option, set to min if .5 under min 432 | a = 0; 433 | } else if (a < 0) { 434 | a += this.PI2; 435 | } 436 | 437 | ret = ~~ (0.5 + (a * (this.o.max - this.o.min) / this.angleArc)) 438 | + this.o.min; 439 | 440 | this.o.stopper 441 | && (ret = max(min(ret, this.o.max), this.o.min)); 442 | 443 | return ret; 444 | }; 445 | 446 | this.listen = function () { 447 | // bind MouseWheel 448 | var s = this, 449 | mw = function (e) { 450 | e.preventDefault(); 451 | 452 | var ori = e.originalEvent 453 | ,deltaX = ori.detail || ori.wheelDeltaX 454 | ,deltaY = ori.detail || ori.wheelDeltaY 455 | ,v = parseInt(s.$.val()) + (deltaX>0 || deltaY>0 ? 1 : deltaX<0 || deltaY<0 ? -1 : 0); 456 | 457 | if ( 458 | s.cH 459 | && (s.cH(v) === false) 460 | ) return; 461 | 462 | s.val(v); 463 | } 464 | , kval, to, m = 1, kv = {37:-1, 38:1, 39:1, 40:-1}; 465 | 466 | this.$ 467 | .bind( 468 | "keydown" 469 | ,function (e) { 470 | var kc = e.keyCode; 471 | kval = parseInt(String.fromCharCode(kc)); 472 | 473 | if (isNaN(kval)) { 474 | 475 | (kc !== 13) // enter 476 | && (kc !== 8) // bs 477 | && (kc !== 9) // tab 478 | && (kc !== 189) // - 479 | && e.preventDefault(); 480 | 481 | // arrows 482 | if ($.inArray(kc,[37,38,39,40]) > -1) { 483 | e.preventDefault(); 484 | 485 | var v = parseInt(s.$.val()) + kv[kc] * m; 486 | 487 | s.o.stopper 488 | && (v = max(min(v, s.o.max), s.o.min)); 489 | 490 | s.change(v); 491 | s._draw(); 492 | 493 | // long time keydown speed-up 494 | to = window.setTimeout( 495 | function () { m*=2; } 496 | ,30 497 | ); 498 | } 499 | } 500 | } 501 | ) 502 | .bind( 503 | "keyup" 504 | ,function (e) { 505 | if (isNaN(kval)) { 506 | if (to) { 507 | window.clearTimeout(to); 508 | to = null; 509 | m = 1; 510 | s.val(s.$.val()); 511 | } 512 | } else { 513 | // kval postcond 514 | (s.$.val() > s.o.max && s.$.val(s.o.max)) 515 | || (s.$.val() < s.o.min && s.$.val(s.o.min)); 516 | } 517 | 518 | } 519 | ); 520 | 521 | this.$c.bind("mousewheel DOMMouseScroll", mw); 522 | this.$.bind("mousewheel DOMMouseScroll", mw) 523 | }; 524 | 525 | this.init = function () { 526 | 527 | if ( 528 | this.v < this.o.min 529 | || this.v > this.o.max 530 | ) this.v = this.o.min; 531 | 532 | this.$.val(this.v); 533 | this.w2 = this.o.width / 2; 534 | this.cursorExt = this.o.cursor / 100; 535 | this.xy = this.w2; 536 | this.lineWidth = this.xy * this.o.thickness; 537 | this.radius = this.xy - this.lineWidth / 2; 538 | 539 | this.o.angleOffset 540 | && (this.o.angleOffset = isNaN(this.o.angleOffset) ? 0 : this.o.angleOffset); 541 | 542 | this.o.angleArc 543 | && (this.o.angleArc = isNaN(this.o.angleArc) ? this.PI2 : this.o.angleArc); 544 | 545 | // deg to rad 546 | this.angleOffset = this.o.angleOffset * Math.PI / 180; 547 | this.angleArc = this.o.angleArc * Math.PI / 180; 548 | 549 | // compute start and end angles 550 | this.startAngle = 1.5 * Math.PI + this.angleOffset; 551 | this.endAngle = 1.5 * Math.PI + this.angleOffset + this.angleArc; 552 | 553 | var s = max( 554 | String(Math.abs(this.o.max)).length 555 | , String(Math.abs(this.o.min)).length 556 | , 2 557 | ) + 2; 558 | 559 | this.o.displayInput 560 | && this.i.css({ 561 | 'width' : ((this.o.width / 2 + 4) >> 0) + 'px' 562 | ,'height' : ((this.o.width / 3) >> 0) + 'px' 563 | ,'position' : 'absolute' 564 | ,'vertical-align' : 'middle' 565 | ,'margin-top' : ((this.o.width / 3) >> 0) + 'px' 566 | ,'margin-left' : '-' + ((this.o.width * 3 / 4 + 2) >> 0) + 'px' 567 | ,'border' : 0 568 | ,'background' : 'none' 569 | ,'font' : 'bold ' + ((this.o.width / s) >> 0) + 'px Arial' 570 | ,'text-align' : 'center' 571 | ,'color' : this.o.fgColor 572 | ,'padding' : '0px' 573 | ,'-webkit-appearance': 'none' 574 | }) 575 | || this.i.css({ 576 | 'width' : '0px' 577 | ,'visibility' : 'hidden' 578 | }); 579 | }; 580 | 581 | this.change = function (v) { 582 | this.cv = v; 583 | this.$.val(v); 584 | }; 585 | 586 | this.angle = function (v) { 587 | return (v - this.o.min) * this.angleArc / (this.o.max - this.o.min); 588 | }; 589 | 590 | this.draw = function () { 591 | 592 | var c = this.g, // context 593 | a = this.angle(this.cv) // Angle 594 | , sat = this.startAngle // Start angle 595 | , eat = sat + a // End angle 596 | , sa, ea // Previous angles 597 | , r = 1; 598 | 599 | c.lineWidth = this.lineWidth; 600 | 601 | this.o.cursor 602 | && (sat = eat - this.cursorExt) 603 | && (eat = eat + this.cursorExt); 604 | 605 | c.beginPath(); 606 | c.strokeStyle = this.o.bgColor; 607 | c.arc(this.xy, this.xy, this.radius, this.endAngle, this.startAngle, true); 608 | c.stroke(); 609 | 610 | if (this.o.displayPrevious) { 611 | ea = this.startAngle + this.angle(this.v); 612 | sa = this.startAngle; 613 | this.o.cursor 614 | && (sa = ea - this.cursorExt) 615 | && (ea = ea + this.cursorExt); 616 | 617 | c.beginPath(); 618 | c.strokeStyle = this.pColor; 619 | c.arc(this.xy, this.xy, this.radius, sa, ea, false); 620 | c.stroke(); 621 | r = (this.cv == this.v); 622 | } 623 | 624 | c.beginPath(); 625 | c.strokeStyle = r ? this.o.fgColor : this.fgColor ; 626 | c.arc(this.xy, this.xy, this.radius, sat, eat, false); 627 | c.stroke(); 628 | }; 629 | 630 | this.cancel = function () { 631 | this.val(this.v); 632 | }; 633 | }; 634 | 635 | $.fn.dial = $.fn.knob = function (o) { 636 | return this.each( 637 | function () { 638 | var d = new k.Dial(); 639 | d.o = o; 640 | d.$ = $(this); 641 | d.run(); 642 | } 643 | ).parent(); 644 | }; 645 | 646 | }); -------------------------------------------------------------------------------- /assets/stylesheets/font-awesome.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * Font Awesome 4.2.0 by @davegandy - http://fontawesome.io - @fontawesome 3 | * License - http://fontawesome.io/license (Font: SIL OFL 1.1, CSS: MIT License) 4 | */ 5 | /* FONT PATH 6 | * -------------------------- */ 7 | @font-face { 8 | font-family: 'FontAwesome'; 9 | src: url('../assets/fontawesome-webfont.eot?v=4.2.0'); 10 | src: url('../assets/fontawesome-webfont.eot?#iefix&v=4.2.0') format('embedded-opentype'), url('../assets/fontawesome-webfont.woff?v=4.2.0') format('woff'), url('../assets/fontawesome-webfont.ttf?v=4.2.0') format('truetype'), url('../fonts/fontawesome-webfont.svg?v=4.2.0#fontawesomeregular') format('svg'); 11 | font-weight: normal; 12 | font-style: normal; 13 | } 14 | .fa { 15 | display: inline-block; 16 | font: normal normal normal 14px/1 FontAwesome; 17 | font-size: inherit; 18 | text-rendering: auto; 19 | -webkit-font-smoothing: antialiased; 20 | -moz-osx-font-smoothing: grayscale; 21 | } 22 | /* makes the font 33% larger relative to the icon container */ 23 | .fa-lg { 24 | font-size: 1.33333333em; 25 | line-height: 0.75em; 26 | vertical-align: -15%; 27 | } 28 | .fa-2x { 29 | font-size: 2em; 30 | } 31 | .fa-3x { 32 | font-size: 3em; 33 | } 34 | .fa-4x { 35 | font-size: 4em; 36 | } 37 | .fa-5x { 38 | font-size: 5em; 39 | } 40 | .fa-fw { 41 | width: 1.28571429em; 42 | text-align: center; 43 | } 44 | .fa-ul { 45 | padding-left: 0; 46 | margin-left: 2.14285714em; 47 | list-style-type: none; 48 | } 49 | .fa-ul > li { 50 | position: relative; 51 | } 52 | .fa-li { 53 | position: absolute; 54 | left: -2.14285714em; 55 | width: 2.14285714em; 56 | top: 0.14285714em; 57 | text-align: center; 58 | } 59 | .fa-li.fa-lg { 60 | left: -1.85714286em; 61 | } 62 | .fa-border { 63 | padding: .2em .25em .15em; 64 | border: solid 0.08em #eeeeee; 65 | border-radius: .1em; 66 | } 67 | .pull-right { 68 | float: right; 69 | } 70 | .pull-left { 71 | float: left; 72 | } 73 | .fa.pull-left { 74 | margin-right: .3em; 75 | } 76 | .fa.pull-right { 77 | margin-left: .3em; 78 | } 79 | .fa-spin { 80 | -webkit-animation: fa-spin 2s infinite linear; 81 | animation: fa-spin 2s infinite linear; 82 | } 83 | @-webkit-keyframes fa-spin { 84 | 0% { 85 | -webkit-transform: rotate(0deg); 86 | transform: rotate(0deg); 87 | } 88 | 100% { 89 | -webkit-transform: rotate(359deg); 90 | transform: rotate(359deg); 91 | } 92 | } 93 | @keyframes fa-spin { 94 | 0% { 95 | -webkit-transform: rotate(0deg); 96 | transform: rotate(0deg); 97 | } 98 | 100% { 99 | -webkit-transform: rotate(359deg); 100 | transform: rotate(359deg); 101 | } 102 | } 103 | .fa-rotate-90 { 104 | filter: progid:DXImageTransform.Microsoft.BasicImage(rotation=1); 105 | -webkit-transform: rotate(90deg); 106 | -ms-transform: rotate(90deg); 107 | transform: rotate(90deg); 108 | } 109 | .fa-rotate-180 { 110 | filter: progid:DXImageTransform.Microsoft.BasicImage(rotation=2); 111 | -webkit-transform: rotate(180deg); 112 | -ms-transform: rotate(180deg); 113 | transform: rotate(180deg); 114 | } 115 | .fa-rotate-270 { 116 | filter: progid:DXImageTransform.Microsoft.BasicImage(rotation=3); 117 | -webkit-transform: rotate(270deg); 118 | -ms-transform: rotate(270deg); 119 | transform: rotate(270deg); 120 | } 121 | .fa-flip-horizontal { 122 | filter: progid:DXImageTransform.Microsoft.BasicImage(rotation=0, mirror=1); 123 | -webkit-transform: scale(-1, 1); 124 | -ms-transform: scale(-1, 1); 125 | transform: scale(-1, 1); 126 | } 127 | .fa-flip-vertical { 128 | filter: progid:DXImageTransform.Microsoft.BasicImage(rotation=2, mirror=1); 129 | -webkit-transform: scale(1, -1); 130 | -ms-transform: scale(1, -1); 131 | transform: scale(1, -1); 132 | } 133 | :root .fa-rotate-90, 134 | :root .fa-rotate-180, 135 | :root .fa-rotate-270, 136 | :root .fa-flip-horizontal, 137 | :root .fa-flip-vertical { 138 | filter: none; 139 | } 140 | .fa-stack { 141 | position: relative; 142 | display: inline-block; 143 | width: 2em; 144 | height: 2em; 145 | line-height: 2em; 146 | vertical-align: middle; 147 | } 148 | .fa-stack-1x, 149 | .fa-stack-2x { 150 | position: absolute; 151 | left: 0; 152 | width: 100%; 153 | text-align: center; 154 | } 155 | .fa-stack-1x { 156 | line-height: inherit; 157 | } 158 | .fa-stack-2x { 159 | font-size: 2em; 160 | } 161 | .fa-inverse { 162 | color: #ffffff; 163 | } 164 | /* Font Awesome uses the Unicode Private Use Area (PUA) to ensure screen 165 | readers do not read off random characters that represent icons */ 166 | .fa-glass:before { 167 | content: "\f000"; 168 | } 169 | .fa-music:before { 170 | content: "\f001"; 171 | } 172 | .fa-search:before { 173 | content: "\f002"; 174 | } 175 | .fa-envelope-o:before { 176 | content: "\f003"; 177 | } 178 | .fa-heart:before { 179 | content: "\f004"; 180 | } 181 | .fa-star:before { 182 | content: "\f005"; 183 | } 184 | .fa-star-o:before { 185 | content: "\f006"; 186 | } 187 | .fa-user:before { 188 | content: "\f007"; 189 | } 190 | .fa-film:before { 191 | content: "\f008"; 192 | } 193 | .fa-th-large:before { 194 | content: "\f009"; 195 | } 196 | .fa-th:before { 197 | content: "\f00a"; 198 | } 199 | .fa-th-list:before { 200 | content: "\f00b"; 201 | } 202 | .fa-check:before { 203 | content: "\f00c"; 204 | } 205 | .fa-remove:before, 206 | .fa-close:before, 207 | .fa-times:before { 208 | content: "\f00d"; 209 | } 210 | .fa-search-plus:before { 211 | content: "\f00e"; 212 | } 213 | .fa-search-minus:before { 214 | content: "\f010"; 215 | } 216 | .fa-power-off:before { 217 | content: "\f011"; 218 | } 219 | .fa-signal:before { 220 | content: "\f012"; 221 | } 222 | .fa-gear:before, 223 | .fa-cog:before { 224 | content: "\f013"; 225 | } 226 | .fa-trash-o:before { 227 | content: "\f014"; 228 | } 229 | .fa-home:before { 230 | content: "\f015"; 231 | } 232 | .fa-file-o:before { 233 | content: "\f016"; 234 | } 235 | .fa-clock-o:before { 236 | content: "\f017"; 237 | } 238 | .fa-road:before { 239 | content: "\f018"; 240 | } 241 | .fa-download:before { 242 | content: "\f019"; 243 | } 244 | .fa-arrow-circle-o-down:before { 245 | content: "\f01a"; 246 | } 247 | .fa-arrow-circle-o-up:before { 248 | content: "\f01b"; 249 | } 250 | .fa-inbox:before { 251 | content: "\f01c"; 252 | } 253 | .fa-play-circle-o:before { 254 | content: "\f01d"; 255 | } 256 | .fa-rotate-right:before, 257 | .fa-repeat:before { 258 | content: "\f01e"; 259 | } 260 | .fa-refresh:before { 261 | content: "\f021"; 262 | } 263 | .fa-list-alt:before { 264 | content: "\f022"; 265 | } 266 | .fa-lock:before { 267 | content: "\f023"; 268 | } 269 | .fa-flag:before { 270 | content: "\f024"; 271 | } 272 | .fa-headphones:before { 273 | content: "\f025"; 274 | } 275 | .fa-volume-off:before { 276 | content: "\f026"; 277 | } 278 | .fa-volume-down:before { 279 | content: "\f027"; 280 | } 281 | .fa-volume-up:before { 282 | content: "\f028"; 283 | } 284 | .fa-qrcode:before { 285 | content: "\f029"; 286 | } 287 | .fa-barcode:before { 288 | content: "\f02a"; 289 | } 290 | .fa-tag:before { 291 | content: "\f02b"; 292 | } 293 | .fa-tags:before { 294 | content: "\f02c"; 295 | } 296 | .fa-book:before { 297 | content: "\f02d"; 298 | } 299 | .fa-bookmark:before { 300 | content: "\f02e"; 301 | } 302 | .fa-print:before { 303 | content: "\f02f"; 304 | } 305 | .fa-camera:before { 306 | content: "\f030"; 307 | } 308 | .fa-font:before { 309 | content: "\f031"; 310 | } 311 | .fa-bold:before { 312 | content: "\f032"; 313 | } 314 | .fa-italic:before { 315 | content: "\f033"; 316 | } 317 | .fa-text-height:before { 318 | content: "\f034"; 319 | } 320 | .fa-text-width:before { 321 | content: "\f035"; 322 | } 323 | .fa-align-left:before { 324 | content: "\f036"; 325 | } 326 | .fa-align-center:before { 327 | content: "\f037"; 328 | } 329 | .fa-align-right:before { 330 | content: "\f038"; 331 | } 332 | .fa-align-justify:before { 333 | content: "\f039"; 334 | } 335 | .fa-list:before { 336 | content: "\f03a"; 337 | } 338 | .fa-dedent:before, 339 | .fa-outdent:before { 340 | content: "\f03b"; 341 | } 342 | .fa-indent:before { 343 | content: "\f03c"; 344 | } 345 | .fa-video-camera:before { 346 | content: "\f03d"; 347 | } 348 | .fa-photo:before, 349 | .fa-image:before, 350 | .fa-picture-o:before { 351 | content: "\f03e"; 352 | } 353 | .fa-pencil:before { 354 | content: "\f040"; 355 | } 356 | .fa-map-marker:before { 357 | content: "\f041"; 358 | } 359 | .fa-adjust:before { 360 | content: "\f042"; 361 | } 362 | .fa-tint:before { 363 | content: "\f043"; 364 | } 365 | .fa-edit:before, 366 | .fa-pencil-square-o:before { 367 | content: "\f044"; 368 | } 369 | .fa-share-square-o:before { 370 | content: "\f045"; 371 | } 372 | .fa-check-square-o:before { 373 | content: "\f046"; 374 | } 375 | .fa-arrows:before { 376 | content: "\f047"; 377 | } 378 | .fa-step-backward:before { 379 | content: "\f048"; 380 | } 381 | .fa-fast-backward:before { 382 | content: "\f049"; 383 | } 384 | .fa-backward:before { 385 | content: "\f04a"; 386 | } 387 | .fa-play:before { 388 | content: "\f04b"; 389 | } 390 | .fa-pause:before { 391 | content: "\f04c"; 392 | } 393 | .fa-stop:before { 394 | content: "\f04d"; 395 | } 396 | .fa-forward:before { 397 | content: "\f04e"; 398 | } 399 | .fa-fast-forward:before { 400 | content: "\f050"; 401 | } 402 | .fa-step-forward:before { 403 | content: "\f051"; 404 | } 405 | .fa-eject:before { 406 | content: "\f052"; 407 | } 408 | .fa-chevron-left:before { 409 | content: "\f053"; 410 | } 411 | .fa-chevron-right:before { 412 | content: "\f054"; 413 | } 414 | .fa-plus-circle:before { 415 | content: "\f055"; 416 | } 417 | .fa-minus-circle:before { 418 | content: "\f056"; 419 | } 420 | .fa-times-circle:before { 421 | content: "\f057"; 422 | } 423 | .fa-check-circle:before { 424 | content: "\f058"; 425 | } 426 | .fa-question-circle:before { 427 | content: "\f059"; 428 | } 429 | .fa-info-circle:before { 430 | content: "\f05a"; 431 | } 432 | .fa-crosshairs:before { 433 | content: "\f05b"; 434 | } 435 | .fa-times-circle-o:before { 436 | content: "\f05c"; 437 | } 438 | .fa-check-circle-o:before { 439 | content: "\f05d"; 440 | } 441 | .fa-ban:before { 442 | content: "\f05e"; 443 | } 444 | .fa-arrow-left:before { 445 | content: "\f060"; 446 | } 447 | .fa-arrow-right:before { 448 | content: "\f061"; 449 | } 450 | .fa-arrow-up:before { 451 | content: "\f062"; 452 | } 453 | .fa-arrow-down:before { 454 | content: "\f063"; 455 | } 456 | .fa-mail-forward:before, 457 | .fa-share:before { 458 | content: "\f064"; 459 | } 460 | .fa-expand:before { 461 | content: "\f065"; 462 | } 463 | .fa-compress:before { 464 | content: "\f066"; 465 | } 466 | .fa-plus:before { 467 | content: "\f067"; 468 | } 469 | .fa-minus:before { 470 | content: "\f068"; 471 | } 472 | .fa-asterisk:before { 473 | content: "\f069"; 474 | } 475 | .fa-exclamation-circle:before { 476 | content: "\f06a"; 477 | } 478 | .fa-gift:before { 479 | content: "\f06b"; 480 | } 481 | .fa-leaf:before { 482 | content: "\f06c"; 483 | } 484 | .fa-fire:before { 485 | content: "\f06d"; 486 | } 487 | .fa-eye:before { 488 | content: "\f06e"; 489 | } 490 | .fa-eye-slash:before { 491 | content: "\f070"; 492 | } 493 | .fa-warning:before, 494 | .fa-exclamation-triangle:before { 495 | content: "\f071"; 496 | } 497 | .fa-plane:before { 498 | content: "\f072"; 499 | } 500 | .fa-calendar:before { 501 | content: "\f073"; 502 | } 503 | .fa-random:before { 504 | content: "\f074"; 505 | } 506 | .fa-comment:before { 507 | content: "\f075"; 508 | } 509 | .fa-magnet:before { 510 | content: "\f076"; 511 | } 512 | .fa-chevron-up:before { 513 | content: "\f077"; 514 | } 515 | .fa-chevron-down:before { 516 | content: "\f078"; 517 | } 518 | .fa-retweet:before { 519 | content: "\f079"; 520 | } 521 | .fa-shopping-cart:before { 522 | content: "\f07a"; 523 | } 524 | .fa-folder:before { 525 | content: "\f07b"; 526 | } 527 | .fa-folder-open:before { 528 | content: "\f07c"; 529 | } 530 | .fa-arrows-v:before { 531 | content: "\f07d"; 532 | } 533 | .fa-arrows-h:before { 534 | content: "\f07e"; 535 | } 536 | .fa-bar-chart-o:before, 537 | .fa-bar-chart:before { 538 | content: "\f080"; 539 | } 540 | .fa-twitter-square:before { 541 | content: "\f081"; 542 | } 543 | .fa-facebook-square:before { 544 | content: "\f082"; 545 | } 546 | .fa-camera-retro:before { 547 | content: "\f083"; 548 | } 549 | .fa-key:before { 550 | content: "\f084"; 551 | } 552 | .fa-gears:before, 553 | .fa-cogs:before { 554 | content: "\f085"; 555 | } 556 | .fa-comments:before { 557 | content: "\f086"; 558 | } 559 | .fa-thumbs-o-up:before { 560 | content: "\f087"; 561 | } 562 | .fa-thumbs-o-down:before { 563 | content: "\f088"; 564 | } 565 | .fa-star-half:before { 566 | content: "\f089"; 567 | } 568 | .fa-heart-o:before { 569 | content: "\f08a"; 570 | } 571 | .fa-sign-out:before { 572 | content: "\f08b"; 573 | } 574 | .fa-linkedin-square:before { 575 | content: "\f08c"; 576 | } 577 | .fa-thumb-tack:before { 578 | content: "\f08d"; 579 | } 580 | .fa-external-link:before { 581 | content: "\f08e"; 582 | } 583 | .fa-sign-in:before { 584 | content: "\f090"; 585 | } 586 | .fa-trophy:before { 587 | content: "\f091"; 588 | } 589 | .fa-github-square:before { 590 | content: "\f092"; 591 | } 592 | .fa-upload:before { 593 | content: "\f093"; 594 | } 595 | .fa-lemon-o:before { 596 | content: "\f094"; 597 | } 598 | .fa-phone:before { 599 | content: "\f095"; 600 | } 601 | .fa-square-o:before { 602 | content: "\f096"; 603 | } 604 | .fa-bookmark-o:before { 605 | content: "\f097"; 606 | } 607 | .fa-phone-square:before { 608 | content: "\f098"; 609 | } 610 | .fa-twitter:before { 611 | content: "\f099"; 612 | } 613 | .fa-facebook:before { 614 | content: "\f09a"; 615 | } 616 | .fa-github:before { 617 | content: "\f09b"; 618 | } 619 | .fa-unlock:before { 620 | content: "\f09c"; 621 | } 622 | .fa-credit-card:before { 623 | content: "\f09d"; 624 | } 625 | .fa-rss:before { 626 | content: "\f09e"; 627 | } 628 | .fa-hdd-o:before { 629 | content: "\f0a0"; 630 | } 631 | .fa-bullhorn:before { 632 | content: "\f0a1"; 633 | } 634 | .fa-bell:before { 635 | content: "\f0f3"; 636 | } 637 | .fa-certificate:before { 638 | content: "\f0a3"; 639 | } 640 | .fa-hand-o-right:before { 641 | content: "\f0a4"; 642 | } 643 | .fa-hand-o-left:before { 644 | content: "\f0a5"; 645 | } 646 | .fa-hand-o-up:before { 647 | content: "\f0a6"; 648 | } 649 | .fa-hand-o-down:before { 650 | content: "\f0a7"; 651 | } 652 | .fa-arrow-circle-left:before { 653 | content: "\f0a8"; 654 | } 655 | .fa-arrow-circle-right:before { 656 | content: "\f0a9"; 657 | } 658 | .fa-arrow-circle-up:before { 659 | content: "\f0aa"; 660 | } 661 | .fa-arrow-circle-down:before { 662 | content: "\f0ab"; 663 | } 664 | .fa-globe:before { 665 | content: "\f0ac"; 666 | } 667 | .fa-wrench:before { 668 | content: "\f0ad"; 669 | } 670 | .fa-tasks:before { 671 | content: "\f0ae"; 672 | } 673 | .fa-filter:before { 674 | content: "\f0b0"; 675 | } 676 | .fa-briefcase:before { 677 | content: "\f0b1"; 678 | } 679 | .fa-arrows-alt:before { 680 | content: "\f0b2"; 681 | } 682 | .fa-group:before, 683 | .fa-users:before { 684 | content: "\f0c0"; 685 | } 686 | .fa-chain:before, 687 | .fa-link:before { 688 | content: "\f0c1"; 689 | } 690 | .fa-cloud:before { 691 | content: "\f0c2"; 692 | } 693 | .fa-flask:before { 694 | content: "\f0c3"; 695 | } 696 | .fa-cut:before, 697 | .fa-scissors:before { 698 | content: "\f0c4"; 699 | } 700 | .fa-copy:before, 701 | .fa-files-o:before { 702 | content: "\f0c5"; 703 | } 704 | .fa-paperclip:before { 705 | content: "\f0c6"; 706 | } 707 | .fa-save:before, 708 | .fa-floppy-o:before { 709 | content: "\f0c7"; 710 | } 711 | .fa-square:before { 712 | content: "\f0c8"; 713 | } 714 | .fa-navicon:before, 715 | .fa-reorder:before, 716 | .fa-bars:before { 717 | content: "\f0c9"; 718 | } 719 | .fa-list-ul:before { 720 | content: "\f0ca"; 721 | } 722 | .fa-list-ol:before { 723 | content: "\f0cb"; 724 | } 725 | .fa-strikethrough:before { 726 | content: "\f0cc"; 727 | } 728 | .fa-underline:before { 729 | content: "\f0cd"; 730 | } 731 | .fa-table:before { 732 | content: "\f0ce"; 733 | } 734 | .fa-magic:before { 735 | content: "\f0d0"; 736 | } 737 | .fa-truck:before { 738 | content: "\f0d1"; 739 | } 740 | .fa-pinterest:before { 741 | content: "\f0d2"; 742 | } 743 | .fa-pinterest-square:before { 744 | content: "\f0d3"; 745 | } 746 | .fa-google-plus-square:before { 747 | content: "\f0d4"; 748 | } 749 | .fa-google-plus:before { 750 | content: "\f0d5"; 751 | } 752 | .fa-money:before { 753 | content: "\f0d6"; 754 | } 755 | .fa-caret-down:before { 756 | content: "\f0d7"; 757 | } 758 | .fa-caret-up:before { 759 | content: "\f0d8"; 760 | } 761 | .fa-caret-left:before { 762 | content: "\f0d9"; 763 | } 764 | .fa-caret-right:before { 765 | content: "\f0da"; 766 | } 767 | .fa-columns:before { 768 | content: "\f0db"; 769 | } 770 | .fa-unsorted:before, 771 | .fa-sort:before { 772 | content: "\f0dc"; 773 | } 774 | .fa-sort-down:before, 775 | .fa-sort-desc:before { 776 | content: "\f0dd"; 777 | } 778 | .fa-sort-up:before, 779 | .fa-sort-asc:before { 780 | content: "\f0de"; 781 | } 782 | .fa-envelope:before { 783 | content: "\f0e0"; 784 | } 785 | .fa-linkedin:before { 786 | content: "\f0e1"; 787 | } 788 | .fa-rotate-left:before, 789 | .fa-undo:before { 790 | content: "\f0e2"; 791 | } 792 | .fa-legal:before, 793 | .fa-gavel:before { 794 | content: "\f0e3"; 795 | } 796 | .fa-dashboard:before, 797 | .fa-tachometer:before { 798 | content: "\f0e4"; 799 | } 800 | .fa-comment-o:before { 801 | content: "\f0e5"; 802 | } 803 | .fa-comments-o:before { 804 | content: "\f0e6"; 805 | } 806 | .fa-flash:before, 807 | .fa-bolt:before { 808 | content: "\f0e7"; 809 | } 810 | .fa-sitemap:before { 811 | content: "\f0e8"; 812 | } 813 | .fa-umbrella:before { 814 | content: "\f0e9"; 815 | } 816 | .fa-paste:before, 817 | .fa-clipboard:before { 818 | content: "\f0ea"; 819 | } 820 | .fa-lightbulb-o:before { 821 | content: "\f0eb"; 822 | } 823 | .fa-exchange:before { 824 | content: "\f0ec"; 825 | } 826 | .fa-cloud-download:before { 827 | content: "\f0ed"; 828 | } 829 | .fa-cloud-upload:before { 830 | content: "\f0ee"; 831 | } 832 | .fa-user-md:before { 833 | content: "\f0f0"; 834 | } 835 | .fa-stethoscope:before { 836 | content: "\f0f1"; 837 | } 838 | .fa-suitcase:before { 839 | content: "\f0f2"; 840 | } 841 | .fa-bell-o:before { 842 | content: "\f0a2"; 843 | } 844 | .fa-coffee:before { 845 | content: "\f0f4"; 846 | } 847 | .fa-cutlery:before { 848 | content: "\f0f5"; 849 | } 850 | .fa-file-text-o:before { 851 | content: "\f0f6"; 852 | } 853 | .fa-building-o:before { 854 | content: "\f0f7"; 855 | } 856 | .fa-hospital-o:before { 857 | content: "\f0f8"; 858 | } 859 | .fa-ambulance:before { 860 | content: "\f0f9"; 861 | } 862 | .fa-medkit:before { 863 | content: "\f0fa"; 864 | } 865 | .fa-fighter-jet:before { 866 | content: "\f0fb"; 867 | } 868 | .fa-beer:before { 869 | content: "\f0fc"; 870 | } 871 | .fa-h-square:before { 872 | content: "\f0fd"; 873 | } 874 | .fa-plus-square:before { 875 | content: "\f0fe"; 876 | } 877 | .fa-angle-double-left:before { 878 | content: "\f100"; 879 | } 880 | .fa-angle-double-right:before { 881 | content: "\f101"; 882 | } 883 | .fa-angle-double-up:before { 884 | content: "\f102"; 885 | } 886 | .fa-angle-double-down:before { 887 | content: "\f103"; 888 | } 889 | .fa-angle-left:before { 890 | content: "\f104"; 891 | } 892 | .fa-angle-right:before { 893 | content: "\f105"; 894 | } 895 | .fa-angle-up:before { 896 | content: "\f106"; 897 | } 898 | .fa-angle-down:before { 899 | content: "\f107"; 900 | } 901 | .fa-desktop:before { 902 | content: "\f108"; 903 | } 904 | .fa-laptop:before { 905 | content: "\f109"; 906 | } 907 | .fa-tablet:before { 908 | content: "\f10a"; 909 | } 910 | .fa-mobile-phone:before, 911 | .fa-mobile:before { 912 | content: "\f10b"; 913 | } 914 | .fa-circle-o:before { 915 | content: "\f10c"; 916 | } 917 | .fa-quote-left:before { 918 | content: "\f10d"; 919 | } 920 | .fa-quote-right:before { 921 | content: "\f10e"; 922 | } 923 | .fa-spinner:before { 924 | content: "\f110"; 925 | } 926 | .fa-circle:before { 927 | content: "\f111"; 928 | } 929 | .fa-mail-reply:before, 930 | .fa-reply:before { 931 | content: "\f112"; 932 | } 933 | .fa-github-alt:before { 934 | content: "\f113"; 935 | } 936 | .fa-folder-o:before { 937 | content: "\f114"; 938 | } 939 | .fa-folder-open-o:before { 940 | content: "\f115"; 941 | } 942 | .fa-smile-o:before { 943 | content: "\f118"; 944 | } 945 | .fa-frown-o:before { 946 | content: "\f119"; 947 | } 948 | .fa-meh-o:before { 949 | content: "\f11a"; 950 | } 951 | .fa-gamepad:before { 952 | content: "\f11b"; 953 | } 954 | .fa-keyboard-o:before { 955 | content: "\f11c"; 956 | } 957 | .fa-flag-o:before { 958 | content: "\f11d"; 959 | } 960 | .fa-flag-checkered:before { 961 | content: "\f11e"; 962 | } 963 | .fa-terminal:before { 964 | content: "\f120"; 965 | } 966 | .fa-code:before { 967 | content: "\f121"; 968 | } 969 | .fa-mail-reply-all:before, 970 | .fa-reply-all:before { 971 | content: "\f122"; 972 | } 973 | .fa-star-half-empty:before, 974 | .fa-star-half-full:before, 975 | .fa-star-half-o:before { 976 | content: "\f123"; 977 | } 978 | .fa-location-arrow:before { 979 | content: "\f124"; 980 | } 981 | .fa-crop:before { 982 | content: "\f125"; 983 | } 984 | .fa-code-fork:before { 985 | content: "\f126"; 986 | } 987 | .fa-unlink:before, 988 | .fa-chain-broken:before { 989 | content: "\f127"; 990 | } 991 | .fa-question:before { 992 | content: "\f128"; 993 | } 994 | .fa-info:before { 995 | content: "\f129"; 996 | } 997 | .fa-exclamation:before { 998 | content: "\f12a"; 999 | } 1000 | .fa-superscript:before { 1001 | content: "\f12b"; 1002 | } 1003 | .fa-subscript:before { 1004 | content: "\f12c"; 1005 | } 1006 | .fa-eraser:before { 1007 | content: "\f12d"; 1008 | } 1009 | .fa-puzzle-piece:before { 1010 | content: "\f12e"; 1011 | } 1012 | .fa-microphone:before { 1013 | content: "\f130"; 1014 | } 1015 | .fa-microphone-slash:before { 1016 | content: "\f131"; 1017 | } 1018 | .fa-shield:before { 1019 | content: "\f132"; 1020 | } 1021 | .fa-calendar-o:before { 1022 | content: "\f133"; 1023 | } 1024 | .fa-fire-extinguisher:before { 1025 | content: "\f134"; 1026 | } 1027 | .fa-rocket:before { 1028 | content: "\f135"; 1029 | } 1030 | .fa-maxcdn:before { 1031 | content: "\f136"; 1032 | } 1033 | .fa-chevron-circle-left:before { 1034 | content: "\f137"; 1035 | } 1036 | .fa-chevron-circle-right:before { 1037 | content: "\f138"; 1038 | } 1039 | .fa-chevron-circle-up:before { 1040 | content: "\f139"; 1041 | } 1042 | .fa-chevron-circle-down:before { 1043 | content: "\f13a"; 1044 | } 1045 | .fa-html5:before { 1046 | content: "\f13b"; 1047 | } 1048 | .fa-css3:before { 1049 | content: "\f13c"; 1050 | } 1051 | .fa-anchor:before { 1052 | content: "\f13d"; 1053 | } 1054 | .fa-unlock-alt:before { 1055 | content: "\f13e"; 1056 | } 1057 | .fa-bullseye:before { 1058 | content: "\f140"; 1059 | } 1060 | .fa-ellipsis-h:before { 1061 | content: "\f141"; 1062 | } 1063 | .fa-ellipsis-v:before { 1064 | content: "\f142"; 1065 | } 1066 | .fa-rss-square:before { 1067 | content: "\f143"; 1068 | } 1069 | .fa-play-circle:before { 1070 | content: "\f144"; 1071 | } 1072 | .fa-ticket:before { 1073 | content: "\f145"; 1074 | } 1075 | .fa-minus-square:before { 1076 | content: "\f146"; 1077 | } 1078 | .fa-minus-square-o:before { 1079 | content: "\f147"; 1080 | } 1081 | .fa-level-up:before { 1082 | content: "\f148"; 1083 | } 1084 | .fa-level-down:before { 1085 | content: "\f149"; 1086 | } 1087 | .fa-check-square:before { 1088 | content: "\f14a"; 1089 | } 1090 | .fa-pencil-square:before { 1091 | content: "\f14b"; 1092 | } 1093 | .fa-external-link-square:before { 1094 | content: "\f14c"; 1095 | } 1096 | .fa-share-square:before { 1097 | content: "\f14d"; 1098 | } 1099 | .fa-compass:before { 1100 | content: "\f14e"; 1101 | } 1102 | .fa-toggle-down:before, 1103 | .fa-caret-square-o-down:before { 1104 | content: "\f150"; 1105 | } 1106 | .fa-toggle-up:before, 1107 | .fa-caret-square-o-up:before { 1108 | content: "\f151"; 1109 | } 1110 | .fa-toggle-right:before, 1111 | .fa-caret-square-o-right:before { 1112 | content: "\f152"; 1113 | } 1114 | .fa-euro:before, 1115 | .fa-eur:before { 1116 | content: "\f153"; 1117 | } 1118 | .fa-gbp:before { 1119 | content: "\f154"; 1120 | } 1121 | .fa-dollar:before, 1122 | .fa-usd:before { 1123 | content: "\f155"; 1124 | } 1125 | .fa-rupee:before, 1126 | .fa-inr:before { 1127 | content: "\f156"; 1128 | } 1129 | .fa-cny:before, 1130 | .fa-rmb:before, 1131 | .fa-yen:before, 1132 | .fa-jpy:before { 1133 | content: "\f157"; 1134 | } 1135 | .fa-ruble:before, 1136 | .fa-rouble:before, 1137 | .fa-rub:before { 1138 | content: "\f158"; 1139 | } 1140 | .fa-won:before, 1141 | .fa-krw:before { 1142 | content: "\f159"; 1143 | } 1144 | .fa-bitcoin:before, 1145 | .fa-btc:before { 1146 | content: "\f15a"; 1147 | } 1148 | .fa-file:before { 1149 | content: "\f15b"; 1150 | } 1151 | .fa-file-text:before { 1152 | content: "\f15c"; 1153 | } 1154 | .fa-sort-alpha-asc:before { 1155 | content: "\f15d"; 1156 | } 1157 | .fa-sort-alpha-desc:before { 1158 | content: "\f15e"; 1159 | } 1160 | .fa-sort-amount-asc:before { 1161 | content: "\f160"; 1162 | } 1163 | .fa-sort-amount-desc:before { 1164 | content: "\f161"; 1165 | } 1166 | .fa-sort-numeric-asc:before { 1167 | content: "\f162"; 1168 | } 1169 | .fa-sort-numeric-desc:before { 1170 | content: "\f163"; 1171 | } 1172 | .fa-thumbs-up:before { 1173 | content: "\f164"; 1174 | } 1175 | .fa-thumbs-down:before { 1176 | content: "\f165"; 1177 | } 1178 | .fa-youtube-square:before { 1179 | content: "\f166"; 1180 | } 1181 | .fa-youtube:before { 1182 | content: "\f167"; 1183 | } 1184 | .fa-xing:before { 1185 | content: "\f168"; 1186 | } 1187 | .fa-xing-square:before { 1188 | content: "\f169"; 1189 | } 1190 | .fa-youtube-play:before { 1191 | content: "\f16a"; 1192 | } 1193 | .fa-dropbox:before { 1194 | content: "\f16b"; 1195 | } 1196 | .fa-stack-overflow:before { 1197 | content: "\f16c"; 1198 | } 1199 | .fa-instagram:before { 1200 | content: "\f16d"; 1201 | } 1202 | .fa-flickr:before { 1203 | content: "\f16e"; 1204 | } 1205 | .fa-adn:before { 1206 | content: "\f170"; 1207 | } 1208 | .fa-bitbucket:before { 1209 | content: "\f171"; 1210 | } 1211 | .fa-bitbucket-square:before { 1212 | content: "\f172"; 1213 | } 1214 | .fa-tumblr:before { 1215 | content: "\f173"; 1216 | } 1217 | .fa-tumblr-square:before { 1218 | content: "\f174"; 1219 | } 1220 | .fa-long-arrow-down:before { 1221 | content: "\f175"; 1222 | } 1223 | .fa-long-arrow-up:before { 1224 | content: "\f176"; 1225 | } 1226 | .fa-long-arrow-left:before { 1227 | content: "\f177"; 1228 | } 1229 | .fa-long-arrow-right:before { 1230 | content: "\f178"; 1231 | } 1232 | .fa-apple:before { 1233 | content: "\f179"; 1234 | } 1235 | .fa-windows:before { 1236 | content: "\f17a"; 1237 | } 1238 | .fa-android:before { 1239 | content: "\f17b"; 1240 | } 1241 | .fa-linux:before { 1242 | content: "\f17c"; 1243 | } 1244 | .fa-dribbble:before { 1245 | content: "\f17d"; 1246 | } 1247 | .fa-skype:before { 1248 | content: "\f17e"; 1249 | } 1250 | .fa-foursquare:before { 1251 | content: "\f180"; 1252 | } 1253 | .fa-trello:before { 1254 | content: "\f181"; 1255 | } 1256 | .fa-female:before { 1257 | content: "\f182"; 1258 | } 1259 | .fa-male:before { 1260 | content: "\f183"; 1261 | } 1262 | .fa-gittip:before { 1263 | content: "\f184"; 1264 | } 1265 | .fa-sun-o:before { 1266 | content: "\f185"; 1267 | } 1268 | .fa-moon-o:before { 1269 | content: "\f186"; 1270 | } 1271 | .fa-archive:before { 1272 | content: "\f187"; 1273 | } 1274 | .fa-bug:before { 1275 | content: "\f188"; 1276 | } 1277 | .fa-vk:before { 1278 | content: "\f189"; 1279 | } 1280 | .fa-weibo:before { 1281 | content: "\f18a"; 1282 | } 1283 | .fa-renren:before { 1284 | content: "\f18b"; 1285 | } 1286 | .fa-pagelines:before { 1287 | content: "\f18c"; 1288 | } 1289 | .fa-stack-exchange:before { 1290 | content: "\f18d"; 1291 | } 1292 | .fa-arrow-circle-o-right:before { 1293 | content: "\f18e"; 1294 | } 1295 | .fa-arrow-circle-o-left:before { 1296 | content: "\f190"; 1297 | } 1298 | .fa-toggle-left:before, 1299 | .fa-caret-square-o-left:before { 1300 | content: "\f191"; 1301 | } 1302 | .fa-dot-circle-o:before { 1303 | content: "\f192"; 1304 | } 1305 | .fa-wheelchair:before { 1306 | content: "\f193"; 1307 | } 1308 | .fa-vimeo-square:before { 1309 | content: "\f194"; 1310 | } 1311 | .fa-turkish-lira:before, 1312 | .fa-try:before { 1313 | content: "\f195"; 1314 | } 1315 | .fa-plus-square-o:before { 1316 | content: "\f196"; 1317 | } 1318 | .fa-space-shuttle:before { 1319 | content: "\f197"; 1320 | } 1321 | .fa-slack:before { 1322 | content: "\f198"; 1323 | } 1324 | .fa-envelope-square:before { 1325 | content: "\f199"; 1326 | } 1327 | .fa-wordpress:before { 1328 | content: "\f19a"; 1329 | } 1330 | .fa-openid:before { 1331 | content: "\f19b"; 1332 | } 1333 | .fa-institution:before, 1334 | .fa-bank:before, 1335 | .fa-university:before { 1336 | content: "\f19c"; 1337 | } 1338 | .fa-mortar-board:before, 1339 | .fa-graduation-cap:before { 1340 | content: "\f19d"; 1341 | } 1342 | .fa-yahoo:before { 1343 | content: "\f19e"; 1344 | } 1345 | .fa-google:before { 1346 | content: "\f1a0"; 1347 | } 1348 | .fa-reddit:before { 1349 | content: "\f1a1"; 1350 | } 1351 | .fa-reddit-square:before { 1352 | content: "\f1a2"; 1353 | } 1354 | .fa-stumbleupon-circle:before { 1355 | content: "\f1a3"; 1356 | } 1357 | .fa-stumbleupon:before { 1358 | content: "\f1a4"; 1359 | } 1360 | .fa-delicious:before { 1361 | content: "\f1a5"; 1362 | } 1363 | .fa-digg:before { 1364 | content: "\f1a6"; 1365 | } 1366 | .fa-pied-piper:before { 1367 | content: "\f1a7"; 1368 | } 1369 | .fa-pied-piper-alt:before { 1370 | content: "\f1a8"; 1371 | } 1372 | .fa-drupal:before { 1373 | content: "\f1a9"; 1374 | } 1375 | .fa-joomla:before { 1376 | content: "\f1aa"; 1377 | } 1378 | .fa-language:before { 1379 | content: "\f1ab"; 1380 | } 1381 | .fa-fax:before { 1382 | content: "\f1ac"; 1383 | } 1384 | .fa-building:before { 1385 | content: "\f1ad"; 1386 | } 1387 | .fa-child:before { 1388 | content: "\f1ae"; 1389 | } 1390 | .fa-paw:before { 1391 | content: "\f1b0"; 1392 | } 1393 | .fa-spoon:before { 1394 | content: "\f1b1"; 1395 | } 1396 | .fa-cube:before { 1397 | content: "\f1b2"; 1398 | } 1399 | .fa-cubes:before { 1400 | content: "\f1b3"; 1401 | } 1402 | .fa-behance:before { 1403 | content: "\f1b4"; 1404 | } 1405 | .fa-behance-square:before { 1406 | content: "\f1b5"; 1407 | } 1408 | .fa-steam:before { 1409 | content: "\f1b6"; 1410 | } 1411 | .fa-steam-square:before { 1412 | content: "\f1b7"; 1413 | } 1414 | .fa-recycle:before { 1415 | content: "\f1b8"; 1416 | } 1417 | .fa-automobile:before, 1418 | .fa-car:before { 1419 | content: "\f1b9"; 1420 | } 1421 | .fa-cab:before, 1422 | .fa-taxi:before { 1423 | content: "\f1ba"; 1424 | } 1425 | .fa-tree:before { 1426 | content: "\f1bb"; 1427 | } 1428 | .fa-spotify:before { 1429 | content: "\f1bc"; 1430 | } 1431 | .fa-deviantart:before { 1432 | content: "\f1bd"; 1433 | } 1434 | .fa-soundcloud:before { 1435 | content: "\f1be"; 1436 | } 1437 | .fa-database:before { 1438 | content: "\f1c0"; 1439 | } 1440 | .fa-file-pdf-o:before { 1441 | content: "\f1c1"; 1442 | } 1443 | .fa-file-word-o:before { 1444 | content: "\f1c2"; 1445 | } 1446 | .fa-file-excel-o:before { 1447 | content: "\f1c3"; 1448 | } 1449 | .fa-file-powerpoint-o:before { 1450 | content: "\f1c4"; 1451 | } 1452 | .fa-file-photo-o:before, 1453 | .fa-file-picture-o:before, 1454 | .fa-file-image-o:before { 1455 | content: "\f1c5"; 1456 | } 1457 | .fa-file-zip-o:before, 1458 | .fa-file-archive-o:before { 1459 | content: "\f1c6"; 1460 | } 1461 | .fa-file-sound-o:before, 1462 | .fa-file-audio-o:before { 1463 | content: "\f1c7"; 1464 | } 1465 | .fa-file-movie-o:before, 1466 | .fa-file-video-o:before { 1467 | content: "\f1c8"; 1468 | } 1469 | .fa-file-code-o:before { 1470 | content: "\f1c9"; 1471 | } 1472 | .fa-vine:before { 1473 | content: "\f1ca"; 1474 | } 1475 | .fa-codepen:before { 1476 | content: "\f1cb"; 1477 | } 1478 | .fa-jsfiddle:before { 1479 | content: "\f1cc"; 1480 | } 1481 | .fa-life-bouy:before, 1482 | .fa-life-buoy:before, 1483 | .fa-life-saver:before, 1484 | .fa-support:before, 1485 | .fa-life-ring:before { 1486 | content: "\f1cd"; 1487 | } 1488 | .fa-circle-o-notch:before { 1489 | content: "\f1ce"; 1490 | } 1491 | .fa-ra:before, 1492 | .fa-rebel:before { 1493 | content: "\f1d0"; 1494 | } 1495 | .fa-ge:before, 1496 | .fa-empire:before { 1497 | content: "\f1d1"; 1498 | } 1499 | .fa-git-square:before { 1500 | content: "\f1d2"; 1501 | } 1502 | .fa-git:before { 1503 | content: "\f1d3"; 1504 | } 1505 | .fa-hacker-news:before { 1506 | content: "\f1d4"; 1507 | } 1508 | .fa-tencent-weibo:before { 1509 | content: "\f1d5"; 1510 | } 1511 | .fa-qq:before { 1512 | content: "\f1d6"; 1513 | } 1514 | .fa-wechat:before, 1515 | .fa-weixin:before { 1516 | content: "\f1d7"; 1517 | } 1518 | .fa-send:before, 1519 | .fa-paper-plane:before { 1520 | content: "\f1d8"; 1521 | } 1522 | .fa-send-o:before, 1523 | .fa-paper-plane-o:before { 1524 | content: "\f1d9"; 1525 | } 1526 | .fa-history:before { 1527 | content: "\f1da"; 1528 | } 1529 | .fa-circle-thin:before { 1530 | content: "\f1db"; 1531 | } 1532 | .fa-header:before { 1533 | content: "\f1dc"; 1534 | } 1535 | .fa-paragraph:before { 1536 | content: "\f1dd"; 1537 | } 1538 | .fa-sliders:before { 1539 | content: "\f1de"; 1540 | } 1541 | .fa-share-alt:before { 1542 | content: "\f1e0"; 1543 | } 1544 | .fa-share-alt-square:before { 1545 | content: "\f1e1"; 1546 | } 1547 | .fa-bomb:before { 1548 | content: "\f1e2"; 1549 | } 1550 | .fa-soccer-ball-o:before, 1551 | .fa-futbol-o:before { 1552 | content: "\f1e3"; 1553 | } 1554 | .fa-tty:before { 1555 | content: "\f1e4"; 1556 | } 1557 | .fa-binoculars:before { 1558 | content: "\f1e5"; 1559 | } 1560 | .fa-plug:before { 1561 | content: "\f1e6"; 1562 | } 1563 | .fa-slideshare:before { 1564 | content: "\f1e7"; 1565 | } 1566 | .fa-twitch:before { 1567 | content: "\f1e8"; 1568 | } 1569 | .fa-yelp:before { 1570 | content: "\f1e9"; 1571 | } 1572 | .fa-newspaper-o:before { 1573 | content: "\f1ea"; 1574 | } 1575 | .fa-wifi:before { 1576 | content: "\f1eb"; 1577 | } 1578 | .fa-calculator:before { 1579 | content: "\f1ec"; 1580 | } 1581 | .fa-paypal:before { 1582 | content: "\f1ed"; 1583 | } 1584 | .fa-google-wallet:before { 1585 | content: "\f1ee"; 1586 | } 1587 | .fa-cc-visa:before { 1588 | content: "\f1f0"; 1589 | } 1590 | .fa-cc-mastercard:before { 1591 | content: "\f1f1"; 1592 | } 1593 | .fa-cc-discover:before { 1594 | content: "\f1f2"; 1595 | } 1596 | .fa-cc-amex:before { 1597 | content: "\f1f3"; 1598 | } 1599 | .fa-cc-paypal:before { 1600 | content: "\f1f4"; 1601 | } 1602 | .fa-cc-stripe:before { 1603 | content: "\f1f5"; 1604 | } 1605 | .fa-bell-slash:before { 1606 | content: "\f1f6"; 1607 | } 1608 | .fa-bell-slash-o:before { 1609 | content: "\f1f7"; 1610 | } 1611 | .fa-trash:before { 1612 | content: "\f1f8"; 1613 | } 1614 | .fa-copyright:before { 1615 | content: "\f1f9"; 1616 | } 1617 | .fa-at:before { 1618 | content: "\f1fa"; 1619 | } 1620 | .fa-eyedropper:before { 1621 | content: "\f1fb"; 1622 | } 1623 | .fa-paint-brush:before { 1624 | content: "\f1fc"; 1625 | } 1626 | .fa-birthday-cake:before { 1627 | content: "\f1fd"; 1628 | } 1629 | .fa-area-chart:before { 1630 | content: "\f1fe"; 1631 | } 1632 | .fa-pie-chart:before { 1633 | content: "\f200"; 1634 | } 1635 | .fa-line-chart:before { 1636 | content: "\f201"; 1637 | } 1638 | .fa-lastfm:before { 1639 | content: "\f202"; 1640 | } 1641 | .fa-lastfm-square:before { 1642 | content: "\f203"; 1643 | } 1644 | .fa-toggle-off:before { 1645 | content: "\f204"; 1646 | } 1647 | .fa-toggle-on:before { 1648 | content: "\f205"; 1649 | } 1650 | .fa-bicycle:before { 1651 | content: "\f206"; 1652 | } 1653 | .fa-bus:before { 1654 | content: "\f207"; 1655 | } 1656 | .fa-ioxhost:before { 1657 | content: "\f208"; 1658 | } 1659 | .fa-angellist:before { 1660 | content: "\f209"; 1661 | } 1662 | .fa-cc:before { 1663 | content: "\f20a"; 1664 | } 1665 | .fa-shekel:before, 1666 | .fa-sheqel:before, 1667 | .fa-ils:before { 1668 | content: "\f20b"; 1669 | } 1670 | .fa-meanpath:before { 1671 | content: "\f20c"; 1672 | } 1673 | --------------------------------------------------------------------------------