├── .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 |
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 |
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 |
5 |
6 |
7 |
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 | #
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 |
--------------------------------------------------------------------------------
![]()
4 | 5 |