From 8a8097af99dd971d4e698fcf2d7986e972c8a7ea Mon Sep 17 00:00:00 2001
From: Paulus Schoutsen <Paulus@PaulusSchoutsen.nl>
Date: Wed, 22 Oct 2014 00:02:18 -0700
Subject: [PATCH] Initial commit Polymer interface

---
 .../http/www_static/polymer/bower.json        |  30 ++
 .../http/www_static/polymer/entity-list.html  |  55 ++++
 .../www_static/polymer/event-fire-dialog.html |  71 +++++
 .../http/www_static/polymer/events-list.html  |  58 ++++
 .../polymer/home-assistant-api.html           | 270 ++++++++++++++++++
 .../polymer/home-assistant-main.html          |  79 +++++
 .../polymer/service-call-dialog.html          |  69 +++++
 .../www_static/polymer/services-list.html     |  74 +++++
 .../http/www_static/polymer/state-card.html   | 139 +++++++++
 .../www_static/polymer/state-set-dialog.html  |  84 ++++++
 .../http/www_static/polymer/states-cards.html |  64 +++++
 11 files changed, 993 insertions(+)
 create mode 100644 homeassistant/components/http/www_static/polymer/bower.json
 create mode 100644 homeassistant/components/http/www_static/polymer/entity-list.html
 create mode 100644 homeassistant/components/http/www_static/polymer/event-fire-dialog.html
 create mode 100644 homeassistant/components/http/www_static/polymer/events-list.html
 create mode 100644 homeassistant/components/http/www_static/polymer/home-assistant-api.html
 create mode 100644 homeassistant/components/http/www_static/polymer/home-assistant-main.html
 create mode 100644 homeassistant/components/http/www_static/polymer/service-call-dialog.html
 create mode 100644 homeassistant/components/http/www_static/polymer/services-list.html
 create mode 100755 homeassistant/components/http/www_static/polymer/state-card.html
 create mode 100644 homeassistant/components/http/www_static/polymer/state-set-dialog.html
 create mode 100755 homeassistant/components/http/www_static/polymer/states-cards.html

diff --git a/homeassistant/components/http/www_static/polymer/bower.json b/homeassistant/components/http/www_static/polymer/bower.json
new file mode 100644
index 00000000000..f83f815face
--- /dev/null
+++ b/homeassistant/components/http/www_static/polymer/bower.json
@@ -0,0 +1,30 @@
+{
+  "name": "Home Assistant",
+  "version": "0.1.0",
+  "authors": [
+    "Paulus Schoutsen <Paulus@PaulusSchoutsen.nl>"
+  ],
+  "main": "index.htm",
+  "license": "MIT",
+  "private": true,
+  "ignore": [
+    "**/.*",
+    "node_modules",
+    "bower_components",
+    "test",
+    "tests"
+  ],
+  "dependencies": {
+    "polymer": "Polymer/polymer#~0.4.2",
+    "font-roboto": "Polymer/font-roboto#~0.4.2",
+    "core-header-panel": "Polymer/core-header-panel#~0.4.2",
+    "core-toolbar": "Polymer/core-toolbar#~0.4.2",
+    "core-icon-button": "Polymer/core-icon-button#~0.4.2",
+    "paper-fab": "Polymer/paper-fab#~0.4.2",
+    "core-ajax": "Polymer/core-ajax#~0.4.2",
+    "paper-toast": "Polymer/paper-toast#~0.4.2",
+    "paper-dialog": "Polymer/paper-dialog#~0.4.2",
+    "paper-button": "Polymer/paper-button#~0.4.2",
+    "core-tooltip": "Polymer/core-tooltip#~0.4.2"
+  }
+}
diff --git a/homeassistant/components/http/www_static/polymer/entity-list.html b/homeassistant/components/http/www_static/polymer/entity-list.html
new file mode 100644
index 00000000000..a76d6a82432
--- /dev/null
+++ b/homeassistant/components/http/www_static/polymer/entity-list.html
@@ -0,0 +1,55 @@
+<link rel="import" href="bower_components/polymer/polymer.html">
+
+<polymer-element name="entity-list" attributes="api cbEntityClicked">
+  <template>
+    <style>
+    :host {
+      display: block;
+    }
+
+    .entityContainer {
+      font-size: 1rem;
+    }
+    </style>
+
+    <template if={{cbEntityClicked}}>
+      <style>
+      a {
+        text-decoration: underline;
+        cursor: pointer;
+      }
+      </style>
+    </template>
+
+    <div>
+      <template repeat="{{state in states}}">
+        <div class='eventContainer'>
+          <a on-click={{handleClick}}>{{state.entity_id}}</a>
+        </div>
+      </template>
+
+    </div>
+  </template>
+  <script>
+  Polymer({
+    cbEventClicked: null,
+    states: [],
+
+    domReady: function() {
+      this.api.addEventListener('states-updated', this.statesUpdated.bind(this))
+      this.statesUpdated()
+    },
+
+    statesUpdated: function() {
+      this.states = this.api.states;
+    },
+
+    handleClick: function(ev) {
+      if(this.cbEntityClicked) {
+        this.cbEntityClicked(ev.path[0].innerHTML);
+      }
+    },
+
+  });
+  </script>
+</polymer-element>
diff --git a/homeassistant/components/http/www_static/polymer/event-fire-dialog.html b/homeassistant/components/http/www_static/polymer/event-fire-dialog.html
new file mode 100644
index 00000000000..9a132769329
--- /dev/null
+++ b/homeassistant/components/http/www_static/polymer/event-fire-dialog.html
@@ -0,0 +1,71 @@
+<link rel="import" href="bower_components/polymer/polymer.html">
+<link rel="import" href="bower_components/paper-dialog/paper-dialog.html">
+<link rel="import" href="bower_components/paper-dialog/paper-dialog-transition.html">
+<link rel="import" href="bower_components/paper-button/paper-button.html">
+<link rel="import" href="bower_components/paper-input/paper-input.html">
+
+<link rel="import" href="events-list.html">
+
+<polymer-element name="event-fire-dialog" attributes="api">
+  <template>
+    <style>
+    paper-input:first-child {
+      padding-top: 0;
+    }
+
+    .eventContainer {
+      margin-left: 30px;
+    }
+    </style>
+
+  <paper-dialog id="dialog" heading="Fire Event" transition="paper-dialog-transition-bottom" backdrop="true">
+    <div layout horizontal>
+      <div>
+        <paper-input id="inputType" label="Event Type" floatingLabel="true" autofocus required></paper-input>
+        <paper-input id="inputData" label="Event Data (JSON, optional)" floatingLabel="true" multiline></paper-input>
+      </div>
+      <div class='eventContainer'>
+        <b>Available events:</b>
+        <events-list api={{api}} cbEventClicked={{eventSelected}}></event-list>        
+      </div>
+    </div>
+    <paper-button dismissive>Cancel</paper-button>
+    <paper-button affirmative on-click={{clickFireEvent}}>Fire Event</paper-button>
+  </paper-dialog>
+
+  </template>
+  <script>
+  Polymer({
+    ready: function() {
+      // to ensure callback methods work..
+      this.eventSelected = this.eventSelected.bind(this)
+    },
+
+    show: function(eventType, eventData) {
+      this.setEventType(eventType);
+      this.setEventData(eventData);
+
+      this.$.dialog.toggle();
+    },
+
+    setEventType: function(eventType) {
+      this.$.inputType.value = eventType;
+    },
+
+    setEventData: function(eventData) {
+      this.$.inputData.value = eventData;      
+    },
+
+    eventSelected: function(eventType) {
+      this.setEventType(eventType);
+    },
+
+    clickFireEvent: function() {
+      this.api.fire_event(
+        this.$.inputType.value,
+        this.$.inputData.value
+        )
+    }
+  });
+  </script>
+</polymer-element>
diff --git a/homeassistant/components/http/www_static/polymer/events-list.html b/homeassistant/components/http/www_static/polymer/events-list.html
new file mode 100644
index 00000000000..bd08b5e3003
--- /dev/null
+++ b/homeassistant/components/http/www_static/polymer/events-list.html
@@ -0,0 +1,58 @@
+<link rel="import" href="bower_components/polymer/polymer.html">
+
+<polymer-element name="events-list" attributes="api cbEventClicked">
+  <template>
+    <style>
+    :host {
+      display: block;
+    }
+
+    .eventContainer {
+      font-size: 1rem;
+    }
+
+    </style>
+
+    <template if={{cbEventClicked}}>
+      <style>
+      a {
+        text-decoration: underline;
+        cursor: pointer;
+      }
+      </style>
+    </template>
+
+    <div>
+      <template repeat="{{event in events}}">
+        <div class='eventContainer'>
+          <a on-click={{handleClick}}>{{event.event}}</a>
+          ({{event.listener_count}} listeners)
+        </div>
+      </template>
+
+    </div>
+  </template>
+  <script>
+  Polymer({
+    cbEventClicked: null,
+    events: [],
+
+    domReady: function() {
+      this.events = this.api.events
+
+      this.api.addEventListener('events-updated', this.eventsUpdated.bind(this))
+    },
+
+    eventsUpdated: function() {
+      this.events = this.api.events;
+    },
+
+    handleClick: function(ev) {
+      if(this.cbEventClicked) {
+        this.cbEventClicked(ev.path[0].innerHTML);
+      }
+    },
+
+  });
+  </script>
+</polymer-element>
diff --git a/homeassistant/components/http/www_static/polymer/home-assistant-api.html b/homeassistant/components/http/www_static/polymer/home-assistant-api.html
new file mode 100644
index 00000000000..8bad3463f9e
--- /dev/null
+++ b/homeassistant/components/http/www_static/polymer/home-assistant-api.html
@@ -0,0 +1,270 @@
+<link rel="import" href="bower_components/polymer/polymer.html">
+<link rel="import" href="bower_components/core-ajax/core-ajax.html">
+<link rel="import" href="bower_components/paper-toast/paper-toast.html">
+
+<link rel="import" href="event-fire-dialog.html">
+<link rel="import" href="service-call-dialog.html">
+<link rel="import" href="state-set-dialog.html">
+
+<polymer-element name="home-assistant-api" attributes="auth">
+  <template>
+    <style>
+    core-ajax {
+      display: none;
+    }
+    </style>
+
+    <paper-toast id="toast" role="alert" text=""></paper-toast>
+    <event-fire-dialog id="eventDialog" api={{api}}></event-fire-dialog>
+    <service-call-dialog id="serviceDialog" api={{api}}></service-call-dialog>
+    <state-set-dialog id="stateDialog" api={{api}}></state-set-dialog>
+
+    <core-ajax id="statesAjax"
+      auto
+      method="GET"
+      url="/api/states"
+      headers='{"HA-access": "{{auth}}"}'
+      on-core-response="{{statesLoaded}}"
+      handleAs="json">
+    </core-ajax>
+
+    <core-ajax id="eventsAjax"
+      auto
+      method="GET"
+      url="/api/events"
+      headers='{"HA-access": "{{auth}}"}'
+      on-core-response="{{eventsLoaded}}"
+      handleAs="json">
+    </core-ajax>
+
+    <core-ajax id="servicesAjax"
+      auto
+      method="GET"
+      url="/api/services"
+      headers='{"HA-access": "{{auth}}"}'
+      on-core-response="{{servicesLoaded}}"
+      handleAs="json">
+    </core-ajax>
+
+  </template>
+  <script>
+  Polymer({
+    auth: "",
+    states: [],
+    services: {},
+    events: {},
+    stateUpdateTimeout: null,
+
+    created: function() {
+      this.api = this;
+
+      // so we can pass these methods safely as callbacks
+      this.turn_on = this.turn_on.bind(this);
+      this.turn_off = this.turn_off.bind(this);
+    },
+
+    _laterFetchStates: function() {
+      if(this.stateUpdateTimeout) {
+        clearTimeout(this.stateUpdateTimeout);
+      }
+
+      // update states in 60 seconds
+      this.stateUpdateTimeout = setTimeout(this.fetchStates.bind(this), 60000);
+    },
+
+    _sortStates: function(states) {
+      return states.sort(function(one, two) {
+        if (one.entity_id > two.entity_id) {
+          return 1;
+        } else if (one.entity_id < two.entity_id) {
+          return -1;
+        } else {
+          return 0;
+        }
+      })
+    },
+
+    statesLoaded: function() {
+      // Make a copy of the loaded data
+      this.states = this._sortStates(this.$.statesAjax.response.slice(0));
+
+      this.fire('states-updated')
+
+      this._laterFetchStates();
+    },
+
+    eventsLoaded: function() {
+      // Make a copy of the loaded data
+      this.events = this.$.eventsAjax.response;
+
+      this.fire('events-updated')
+    },
+
+    servicesLoaded: function() {
+      // Make a copy of the loaded data
+      this.services = this.$.servicesAjax.response;
+
+      this.fire('services-updated')
+    },
+
+    _pushNewState: function(new_state) {
+      var state;
+      var stateFound = false;
+
+      for(var i = 0; i < this.states.length; i++) {
+        if(this.states[i].entity_id == new_state.entity_id) {
+          state = this.states[i];
+          state.attributes = new_state.attributes;
+          state.last_changed = new_state.last_changed;
+          state.state = new_state.state;
+
+          stateFound = true;
+          break;
+        }
+      }
+
+      if(!stateFound) {
+        this.states.push(new_state);
+        this._sortStates(this.states);
+      }
+    },
+
+    fetchState: function(entity_id) {
+      var successStateUpdate = function(new_state) {
+        this._pushNewState(new_state);
+      }
+
+      this.call_api("GET", "states/" + entity_id, null, successStateUpdate.bind(this));
+    },
+
+    fetchStates: function() {
+      this.$.statesAjax.go();
+    },
+
+    getState: function(entityId) {
+      for(var i = 0; i < this.states.length; i++) {
+        if(this.states[i].entity_id == entityId) {
+          return this.states[i];
+        }
+      }
+    },
+
+    turn_on: function(entity_id) {
+      this.call_service("homeassistant", "turn_on", {entity_id: entity_id});
+    },
+
+    turn_off: function(entity_id) {
+      this.call_service("homeassistant", "turn_off", {entity_id: entity_id})
+    },
+
+    set_state: function(entity_id, state, attributes) {
+      var payload = {state: state}
+
+      if(attributes) {
+        payload.attributes = attributes;
+      }
+
+      var successToast = function(new_state) {
+        this.showToast("State of "+entity_id+" successful set to "+state+".");
+        this._pushNewState(new_state);
+      }
+
+      this.call_api("POST", "states/" + entity_id,
+                    payload, successToast.bind(this));
+    },
+
+    call_service: function(domain, service, parameters) {
+      var successToast = function() {
+        this.showToast("Service "+domain+"/"+service+" successful called.");
+      }
+
+      this.call_api("POST", "services/" + domain + "/" + service,
+                    parameters, successToast.bind(this));
+    },
+
+    fire_event: function(eventType, eventData) {
+      eventData = eventData ? JSON.parse(eventData) : "";
+
+      var successToast = function() {
+        this.showToast("Event "+eventType+" successful fired.");
+      }
+
+      this.call_api("POST", "events/" + eventType,
+                    eventData, successToast.bind(this));
+    },
+
+    call_api: function(method, path, parameters, callback) {
+      var req = new XMLHttpRequest();
+      req.open(method, "/api/" + path, true)
+      req.setRequestHeader("HA-access", this.auth);
+
+      req.onreadystatechange = function() {
+
+        if(req.readyState == 4 && req.status > 199 && req.status < 300) {
+
+          if(callback) {
+            callback(JSON.parse(req.responseText))
+          }
+          // if we targetted an entity id, update state after 2 seconds
+          if(parameters && parameters.entity_id) {
+            var updateCallback;
+
+            // if a string, update just that entity, otherwise update all
+            if(typeof(parameters.entity_id) == "string") {
+              updateCallback = function() {
+                this.fetchState(parameters.entity_id);
+              }
+
+            } else {
+              updateCallback = this.fetchStates();
+            }
+
+            setTimeout(updateCallback.bind(this), 2000);
+          }
+        }
+      }.bind(this)
+
+      if(parameters) {
+        req.send(JSON.stringify(parameters))
+      } else {
+        req.send()
+      }
+    },
+
+    showEditStateDialog: function(entityId) {
+      var state = this.getState(entityId);
+
+      this.showSetStateDialog(entityId, state.state, state.attributes)
+    },
+
+    showSetStateDialog: function(entityId, state, stateAttributes) {
+      entityId = entityId || "";
+      state = state || "";
+      stateAttributes = stateAttributes || null;
+
+      this.$.stateDialog.show(entityId, state, stateAttributes);
+    },
+
+    showFireEventDialog: function(eventType, eventData) {
+      eventType = eventType || "";
+      eventData = eventData || "";
+
+      this.$.eventDialog.show(eventType, eventData)
+    },
+
+    showCallServiceDialog: function(domain, service, serviceData) {
+      domain = domain || "";
+      service = service || "";
+      serviceData = serviceData || "";
+
+      this.$.serviceDialog.show(domain, service, serviceData);
+    },
+
+    showToast: function(message) {
+      this.$.toast.text = message;
+      this.$.toast.show();
+    }
+
+  });
+  </script>
+</polymer-element>
diff --git a/homeassistant/components/http/www_static/polymer/home-assistant-main.html b/homeassistant/components/http/www_static/polymer/home-assistant-main.html
new file mode 100644
index 00000000000..1f068b9eb93
--- /dev/null
+++ b/homeassistant/components/http/www_static/polymer/home-assistant-main.html
@@ -0,0 +1,79 @@
+<link rel="import" href="bower_components/font-roboto/roboto.html">
+<link rel="import" href="bower_components/core-header-panel/core-header-panel.html">
+<link rel="import" href="bower_components/core-toolbar/core-toolbar.html">
+<link rel="import" href="bower_components/core-icon-button/core-icon-button.html">
+<link rel="import" href="bower_components/paper-fab/paper-fab.html">
+
+<link rel="import" href="home-assistant-api.html">
+<link rel="import" href="states-cards.html">
+
+<polymer-element name="home-assistant-main" attributes="auth">
+  <template>
+    <style type="text/css">
+
+      :host {
+        font-family: 'RobotoDraft', sans-serif;
+      }
+
+      core-header-panel {
+        height: 100%;
+        overflow: auto;
+        -webkit-overflow-scrolling: touch;
+      }
+
+      core-toolbar {
+        background: #03a9f4;
+        font-size: 1.5rem;
+        color: white;
+      }
+
+      paper-fab {
+        position: absolute;
+        bottom: 10px;
+        right: 10px;
+      }
+
+    </style>
+
+    <home-assistant-api auth="{{auth}}" id="api"></home-assistant-api>
+
+    <core-header-panel layout>
+
+      <core-toolbar>
+        <div flex>
+          Home Assistant
+        </div>
+        <core-icon-button icon="developer-mode-tv" on-click="{{handleEventClick}}"></core-icon-button>
+        <core-icon-button icon="settings-remote" on-click="{{handleServiceClick}}"></core-icon-button>
+      </core-toolbar>
+
+      <div class="content" flex>
+        <states-cards api="{{api}}"></states-cards>
+        <paper-fab icon="add" on-click={{handleAddStateClick}}></paper-fab>
+      </div>
+
+    </core-header-panel>
+
+  </template>
+  <script>
+  Polymer({
+
+    ready: function() {
+      this.api = this.$.api;
+    },
+
+    handleEventClick: function() {
+      this.api.showFireEventDialog();
+    },
+
+    handleServiceClick: function() {
+      this.api.showCallServiceDialog();
+    },
+
+    handleAddStateClick: function() {
+      this.api.showSetStateDialog();
+    }
+
+  });
+  </script>  
+</polymer-element>
diff --git a/homeassistant/components/http/www_static/polymer/service-call-dialog.html b/homeassistant/components/http/www_static/polymer/service-call-dialog.html
new file mode 100644
index 00000000000..4429623081e
--- /dev/null
+++ b/homeassistant/components/http/www_static/polymer/service-call-dialog.html
@@ -0,0 +1,69 @@
+<link rel="import" href="bower_components/polymer/polymer.html">
+<link rel="import" href="bower_components/paper-dialog/paper-dialog.html">
+<link rel="import" href="bower_components/paper-dialog/paper-dialog-transition.html">
+<link rel="import" href="bower_components/paper-button/paper-button.html">
+<link rel="import" href="bower_components/paper-input/paper-input.html">
+
+<link rel="import" href="services-list.html">
+
+<polymer-element name="service-call-dialog" attributes="api">
+  <template>
+    <style>
+    paper-input:first-child {
+      padding-top: 0;
+    }
+
+    .serviceContainer {
+      margin-left: 30px;
+    }
+    </style>
+
+  <paper-dialog id="dialog" heading="Call Service" transition="paper-dialog-transition-bottom" backdrop="true">
+    <div layout horizontal>
+      <div>
+        <paper-input id="inputDomain" label="Domain" floatingLabel="true" autofocus required></paper-input>
+        <paper-input id="inputService" label="Service" floatingLabel="true" required></paper-input>
+        <paper-input id="inputData" label="Service Data (JSON, optional)" floatingLabel="true" multiline></paper-input>
+      </div>
+      <div class='serviceContainer'>
+        <b>Available services:</b>
+        <services-list api={{api}} cbServiceClicked={{serviceSelected}}></event-list>        
+      </div>
+    </div>
+    <paper-button dismissive>Cancel</paper-button>
+    <paper-button affirmative on-click={{clickCallService}}>Call Service</paper-button>
+  </paper-dialog>
+
+  </template>
+  <script>
+  Polymer({
+    ready: function() {
+      // to ensure callback methods work..
+      this.serviceSelected = this.serviceSelected.bind(this)
+    },
+
+    show: function(domain, service, serviceData) {
+      this.setService(domain, service);
+      this.$.inputData.value = serviceData;
+      this.$.dialog.toggle();
+    },
+
+    setService: function(domain, service) {
+      this.$.inputDomain.value = domain;
+      this.$.inputService.value = service;
+    },
+
+    serviceSelected: function(domain, service) {
+      this.setService(domain, service);
+    },
+
+    clickCallService: function() {
+      this.api.call_service(
+        this.$.inputDomain.value,
+        this.$.inputService.value,
+        this.$.inputData.value
+        )
+    }
+  });
+  </script>
+</polymer-element>
diff --git a/homeassistant/components/http/www_static/polymer/services-list.html b/homeassistant/components/http/www_static/polymer/services-list.html
new file mode 100644
index 00000000000..715e5b6a9bf
--- /dev/null
+++ b/homeassistant/components/http/www_static/polymer/services-list.html
@@ -0,0 +1,74 @@
+<link rel="import" href="bower_components/polymer/polymer.html">
+<link rel="import" href="bower_components/core-menu/core-menu.html">
+<link rel="import" href="bower_components/core-menu/core-submenu.html">
+<link rel="import" href="bower_components/core-item/core-item.html">
+
+<polymer-element name="services-list" attributes="api cbServiceClicked">
+  <template>
+    <style>
+    :host {
+      display: block;
+    }
+
+    core-menu {
+      margin-top: 0;
+      font-size: 1rem;
+    }
+
+    a {
+      display: block;
+    }
+    </style>
+
+    <template if={{cbServiceClicked}}>
+      <style>
+      a {
+        text-decoration: underline;
+        cursor: pointer;
+      }
+      </style>
+    </template>
+
+    <div>
+      <core-menu selected="0">
+      
+        <template repeat="{{serv in services}}">
+          <core-submenu icon="settings" label="{{serv.domain}}">
+            <template repeat="{{service in serv.services}}">
+              <a on-click={{serviceClicked}} data-domain={{serv.domain}}>{{service}}</a>
+            </template>
+          </core-submenu>
+        </template>
+        
+      </core-menu>
+
+    </div>
+  </template>
+  <script>
+  Polymer({
+    services: [],
+    cbServiceClicked: null,
+
+    domReady: function() {
+      this.services = this.api.services
+
+      this.api.addEventListener('services-updated', this.servicesUpdated.bind(this))
+    },
+
+    servicesUpdated: function() {
+      this.services = this.api.services;
+    },
+
+    serviceClicked: function(ev) {
+      if(this.cbServiceClicked) {
+        var target = ev.path[0];
+        var domain = target.getAttributeNode("data-domain").value;
+        var service = target.innerHTML;
+
+        this.cbServiceClicked(domain, service);
+      }
+    }
+
+  });
+  </script>
+</polymer-element>
diff --git a/homeassistant/components/http/www_static/polymer/state-card.html b/homeassistant/components/http/www_static/polymer/state-card.html
new file mode 100755
index 00000000000..ad618a7372d
--- /dev/null
+++ b/homeassistant/components/http/www_static/polymer/state-card.html
@@ -0,0 +1,139 @@
+<script src="bower_components/moment/moment.js"></script>
+<link rel="import" href="bower_components/polymer/polymer.html">
+<link rel="import" href="bower_components/core-tooltip/core-tooltip.html">
+<link rel="import" href="bower_components/paper-button/paper-button.html">
+
+<polymer-element name="state-card"
+  attributes="entity state last_changed state_attr cb_turn_on, cb_turn_off cb_edit">
+  <template>
+    <style>
+    :host {
+      position: relative;
+      background-color: white;
+      padding: 20px 20px 50px 20px;
+      width: 100%;
+      font-weight: 300;
+      border-radius: 2px;
+    }
+
+    .header {
+      text-transform: capitalize;
+      
+      font-size: 1.5rem;
+    }
+
+    .subheader {
+      margin-top: -5px;
+      color: darkgrey;
+    }
+
+    .content {
+      margin-top: 10px;
+    }
+
+    .actions {
+      position: absolute;
+      bottom: 10px;
+      left: 20px;
+      right: 20px;
+
+      text-align: right;
+    }
+
+    paper-button.toggle {
+      color: #03a9f4;
+    }
+
+    </style>
+
+    <div class="header" horizontal justified layout>
+      <span class="entity_id">{{entity_id | makeReadable}}</span>
+      <span class='state'>{{state | makeReadable}}</span>
+    </div>
+
+    <div class="subheader" horizontal justified layout>
+      <span class="domain">{{domain}}</span>
+      <core-tooltip label="{{last_changed}}" position="bottom">
+        <span class="last_changed_from_now">{{last_changed_from_now}}</span>
+      </core-tooltip>
+    </div>
+    
+
+    <div class="content">
+      <template repeat="{{key in objectKeys(state_attr)}}">
+        <div>{{key | makeReadable}}: {{state_attr[key]}}</div>
+      </template>
+    </div>
+
+    <div class="actions">
+      <paper-button class='edit' on-click="{{editClicked}}">EDIT</paper-button>
+
+      <template if="{{state == 'on'}}">
+        <paper-button class="toggle" on-click="{{turn_off}}">TURN OFF</paper-button>
+      </template>
+      <template if="{{state == 'off'}}">
+        <paper-button class="toggle" on-click="{{turn_on}}">TURN ON</paper-button>
+      </template>
+    </div>
+
+  </template>
+  <script>
+  Polymer({
+    // attributes
+    entity: "",
+    state: "",
+    last_changed: "never",
+    state_attr: {},
+    cb_turn_on: null,
+    cb_turn_off: null,
+    cb_edit: null,
+
+    // computed
+    domain: "",
+    entity_id: "",
+
+    entityChanged: function(oldVal, newVal) {
+      var parts = newVal.split(".")
+
+      if(parts.length == 1) {
+        this.domain = ""
+        this.entity_id = parts[0]
+      } else {
+        this.domain = parts[0]
+        this.entity_id = parts.slice(1).join('.')
+      }
+    },
+
+    last_changedChanged: function(oldVal, newVal) {
+      this.last_changed_from_now = moment(this.last_changed, "HH:mm:ss DD-MM-YYYY").fromNow()
+    },
+
+    turn_on: function() {
+      if(this.cb_turn_on) {
+        this.cb_turn_on(this.entity);
+      }
+    },
+
+    turn_off: function() {
+      if(this.cb_turn_off) {
+        this.cb_turn_off(this.entity);
+      }
+    },
+
+    editClicked: function() {
+      if(this.cb_edit) {
+        this.cb_edit(this.entity);
+      }
+    },
+
+    // used as filter
+    makeReadable: function(value) {
+      return value.replace("_", " ")
+    },
+
+    objectKeys: function(obj) {
+      return obj ? Object.keys(obj) : [];
+    }
+  });
+  </script>
+</polymer-element>
diff --git a/homeassistant/components/http/www_static/polymer/state-set-dialog.html b/homeassistant/components/http/www_static/polymer/state-set-dialog.html
new file mode 100644
index 00000000000..acdac30b3b8
--- /dev/null
+++ b/homeassistant/components/http/www_static/polymer/state-set-dialog.html
@@ -0,0 +1,84 @@
+<link rel="import" href="bower_components/polymer/polymer.html">
+<link rel="import" href="bower_components/paper-dialog/paper-dialog.html">
+<link rel="import" href="bower_components/paper-dialog/paper-dialog-transition.html">
+<link rel="import" href="bower_components/paper-button/paper-button.html">
+<link rel="import" href="bower_components/paper-input/paper-input.html">
+
+<link rel="import" href="entity-list.html">
+
+<polymer-element name="state-set-dialog" attributes="api">
+  <template>
+    <style>
+    paper-input:first-child {
+      padding-top: 0;
+    }
+
+    .stateContainer {
+      margin-left: 30px;
+    }
+    </style>
+
+  <paper-dialog id="dialog" heading="Set State" transition="paper-dialog-transition-center" backdrop="true">
+    <div layout horizontal>
+      <div>
+        <paper-input id="inputEntityID" label="Entity ID" floatingLabel="true" autofocus required></paper-input>
+        <paper-input id="inputState" label="State" floatingLabel="true" required></paper-input>
+        <paper-input id="inputData" label="State attributes (JSON, optional)" floatingLabel="true" multiline></paper-input>
+      </div>
+      <div class='stateContainer'>
+        <b>Current entities:</b>
+        <entity-list api={{api}} cbEntityClicked={{entitySelected}}></entity-list>
+      </div>
+    </div>
+    <paper-button dismissive>Cancel</paper-button>
+    <paper-button affirmative on-click={{clickSetState}}>Set State</paper-button>
+  </paper-dialog>
+
+  </template>
+  <script>
+  Polymer({
+    ready: function() {
+      // to ensure callback methods work..
+      this.entitySelected = this.entitySelected.bind(this)
+    },
+
+    show: function(entityId, state, stateData) {
+      this.setEntityId(entityId);
+      this.setState(state);
+      this.setStateData(stateData);
+
+      this.$.dialog.toggle();
+    },
+
+    setEntityId: function(entityId) {
+      this.$.inputEntityID.value = entityId;      
+    },
+
+    setState: function(state) {
+      this.$.inputState.value = state;      
+    },
+
+    setStateData: function(stateData) {
+      var value = stateData ? JSON.stringify(stateData, null, '  ') : "";
+
+      this.$.inputData.value = value;
+    },
+
+    entitySelected: function(entityId) {
+      this.setEntityId(entityId);
+
+      var state = this.api.getState(entityId);
+      this.setState(state.state);
+      this.setStateData(state.attributes);
+    },
+
+    clickSetState: function() {
+      this.api.set_state(
+        this.$.inputEntityID.value,
+        this.$.inputState.value,
+        JSON.parse(this.$.inputData.value)
+        )
+    }
+  });
+  </script>
+</polymer-element>
diff --git a/homeassistant/components/http/www_static/polymer/states-cards.html b/homeassistant/components/http/www_static/polymer/states-cards.html
new file mode 100755
index 00000000000..d650aae0671
--- /dev/null
+++ b/homeassistant/components/http/www_static/polymer/states-cards.html
@@ -0,0 +1,64 @@
+<link rel="import" href="bower_components/polymer/polymer.html">
+<link rel="import" href="state-card.html">
+
+<polymer-element name="states-cards" attributes="api">
+  <template>
+    <style>
+    :host {
+      display: block;
+      width: 100%;
+    }
+
+    state-card, state-add-card {
+      display: inline-block;
+      width: 350px;
+      margin: 10px 0 0 10px;      
+    }
+
+    state-add-card {
+      cursor: pointer;
+    }
+
+    </style>
+
+    <div horizontal layout wrap>
+
+      <template repeat="{{state in states}}">
+        <state-card
+          entity="{{state.entity_id}}"
+          state="{{state.state}}"
+          last_changed="{{state.last_changed}}"
+          state_attr="{{state.attributes}}"
+          cb_turn_on="{{api.turn_on}}"
+          cb_turn_off="{{api.turn_off}}"
+          cb_edit={{editCallback}}>
+        </state-card>
+      </template>
+
+    </div>
+  </template>
+  <script>
+  Polymer({
+    states: [],
+
+    ready: function() {
+      this.editCallback = this.editCallback.bind(this);
+    },
+
+    domReady: function() {
+      this.states = this.api.states
+
+      this.api.addEventListener('states-updated', this.statesUpdated.bind(this))
+    },
+
+    statesUpdated: function() {
+      this.states = this.api.states;
+    },
+
+    editCallback: function(entityId) {
+      this.api.showEditStateDialog(entityId);
+    },
+
+  });
+  </script>
+</polymer-element>
-- 
GitLab