From 8e143c2e4461771a51cdead553b7fa9e5bbdf772 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 12 Jul 2015 16:57:15 -0700 Subject: [PATCH] ES6 Polymer version --- .eslintrc | 11 + .gitignore | 3 + bower.json | 44 ++++ package.json | 29 +++ scripts/minify.js | 20 ++ src/cards/state-card-configurator.html | 15 ++ src/cards/state-card-configurator.js | 14 ++ src/cards/state-card-content.html | 8 + src/cards/state-card-content.js | 46 +++++ src/cards/state-card-display.html | 22 ++ src/cards/state-card-display.js | 13 ++ src/cards/state-card-media_player.html | 41 ++++ src/cards/state-card-media_player.js | 50 +++++ src/cards/state-card-scene.html | 15 ++ src/cards/state-card-scene.js | 24 +++ src/cards/state-card-thermostat.html | 43 ++++ src/cards/state-card-thermostat.js | 13 ++ src/cards/state-card-toggle.html | 24 +++ src/cards/state-card-toggle.js | 70 +++++++ src/cards/state-card.html | 29 +++ src/cards/state-card.js | 25 +++ src/components/display-time.html | 5 + src/components/display-time.js | 17 ++ src/components/domain-icon.html | 11 + src/components/domain-icon.js | 23 +++ src/components/entity-list.html | 27 +++ src/components/entity-list.js | 29 +++ src/components/events-list.html | 30 +++ src/components/events-list.js | 29 +++ src/components/ha-color-picker.html | 12 ++ src/components/ha-color-picker.js | 151 ++++++++++++++ src/components/ha-logbook.html | 20 ++ src/components/ha-logbook.js | 16 ++ src/components/ha-sidebar.html | 128 ++++++++++++ src/components/ha-sidebar.js | 121 +++++++++++ src/components/ha-voice-command-progress.html | 32 +++ src/components/ha-voice-command-progress.js | 27 +++ src/components/loading-box.html | 19 ++ src/components/loading-box.js | 5 + src/components/logbook-entry.html | 53 +++++ src/components/logbook-entry.js | 12 ++ src/components/relative-ha-datetime.html | 7 + src/components/relative-ha-datetime.js | 61 ++++++ src/components/services-list.html | 35 ++++ src/components/services-list.js | 37 ++++ src/components/state-badge.html | 52 +++++ src/components/state-badge.js | 34 +++ src/components/state-cards.html | 61 ++++++ src/components/state-cards.js | 18 ++ src/components/state-history-chart-line.html | 1 + src/components/state-history-chart-line.js | 194 ++++++++++++++++++ .../state-history-chart-timeline.html | 10 + .../state-history-chart-timeline.js | 107 ++++++++++ src/components/state-history-charts.html | 49 +++++ src/components/state-history-charts.js | 110 ++++++++++ src/components/state-info.html | 53 +++++ src/components/state-info.js | 14 ++ src/components/stream-status.html | 23 +++ src/components/stream-status.js | 30 +++ src/dialogs/more-info-dialog.html | 48 +++++ src/dialogs/more-info-dialog.js | 105 ++++++++++ src/home-assistant.html | 32 +++ src/home-assistant.js | 46 +++++ src/layouts/home-assistant-main.html | 46 +++++ src/layouts/home-assistant-main.js | 89 ++++++++ src/layouts/login-form.html | 83 ++++++++ src/layouts/login-form.js | 69 +++++++ src/layouts/partial-base.html | 29 +++ src/layouts/partial-base.js | 16 ++ src/layouts/partial-dev-call-service.html | 47 +++++ src/layouts/partial-dev-call-service.js | 54 +++++ src/layouts/partial-dev-fire-event.html | 48 +++++ src/layouts/partial-dev-fire-event.js | 43 ++++ src/layouts/partial-dev-set-state.html | 49 +++++ src/layouts/partial-dev-set-state.js | 64 ++++++ src/layouts/partial-history.html | 45 ++++ src/layouts/partial-history.js | 75 +++++++ src/layouts/partial-logbook.html | 41 ++++ src/layouts/partial-logbook.js | 77 +++++++ src/layouts/partial-states.html | 76 +++++++ src/layouts/partial-states.js | 112 ++++++++++ src/managers/notification-manager.html | 14 ++ src/managers/notification-manager.js | 24 +++ src/more-infos/more-info-camera.html | 18 ++ src/more-infos/more-info-camera.js | 29 +++ src/more-infos/more-info-configurator.html | 51 +++++ src/more-infos/more-info-configurator.js | 76 +++++++ src/more-infos/more-info-content.html | 19 ++ src/more-infos/more-info-content.js | 68 ++++++ src/more-infos/more-info-default.html | 19 ++ src/more-infos/more-info-default.js | 27 +++ src/more-infos/more-info-group.html | 22 ++ src/more-infos/more-info-group.js | 43 ++++ src/more-infos/more-info-light.html | 48 +++++ src/more-infos/more-info-light.js | 63 ++++++ src/more-infos/more-info-media_player.html | 56 +++++ src/more-infos/more-info-media_player.js | 152 ++++++++++++++ src/more-infos/more-info-script.html | 12 ++ src/more-infos/more-info-script.js | 11 + src/more-infos/more-info-sun.html | 21 ++ src/more-infos/more-info-sun.js | 48 +++++ src/more-infos/more-info-thermostat.html | 40 ++++ src/more-infos/more-info-thermostat.js | 87 ++++++++ src/polymer.js | 1 + src/resources/home-assistant-icons.html | 31 +++ src/resources/home-assistant-style.html | 48 +++++ src/resources/pikaday-js.html | 2 + src/util/attribute-class-names.js | 6 + src/util/bound-nuclear-behavior.js | 5 + src/util/domain-icon.js | 57 +++++ src/util/format-date-time.js | 5 + src/util/format-date.js | 5 + src/util/format-time.js | 5 + src/util/nuclear-behavior.js | 35 ++++ src/util/state-card-type.js | 14 ++ src/util/state-more-info-type.js | 11 + src/util/validate-auth.js | 8 + src/util/xybri-to-rgb.js | 21 ++ webpack.config.js | 29 +++ 119 files changed, 4747 insertions(+) create mode 100644 .eslintrc create mode 100644 .gitignore create mode 100644 bower.json create mode 100644 package.json create mode 100755 scripts/minify.js create mode 100644 src/cards/state-card-configurator.html create mode 100644 src/cards/state-card-configurator.js create mode 100644 src/cards/state-card-content.html create mode 100644 src/cards/state-card-content.js create mode 100755 src/cards/state-card-display.html create mode 100644 src/cards/state-card-display.js create mode 100644 src/cards/state-card-media_player.html create mode 100644 src/cards/state-card-media_player.js create mode 100644 src/cards/state-card-scene.html create mode 100644 src/cards/state-card-scene.js create mode 100644 src/cards/state-card-thermostat.html create mode 100644 src/cards/state-card-thermostat.js create mode 100755 src/cards/state-card-toggle.html create mode 100644 src/cards/state-card-toggle.js create mode 100644 src/cards/state-card.html create mode 100644 src/cards/state-card.js create mode 100644 src/components/display-time.html create mode 100644 src/components/display-time.js create mode 100644 src/components/domain-icon.html create mode 100644 src/components/domain-icon.js create mode 100644 src/components/entity-list.html create mode 100644 src/components/entity-list.js create mode 100644 src/components/events-list.html create mode 100644 src/components/events-list.js create mode 100644 src/components/ha-color-picker.html create mode 100644 src/components/ha-color-picker.js create mode 100644 src/components/ha-logbook.html create mode 100644 src/components/ha-logbook.js create mode 100644 src/components/ha-sidebar.html create mode 100644 src/components/ha-sidebar.js create mode 100644 src/components/ha-voice-command-progress.html create mode 100644 src/components/ha-voice-command-progress.js create mode 100644 src/components/loading-box.html create mode 100644 src/components/loading-box.js create mode 100644 src/components/logbook-entry.html create mode 100644 src/components/logbook-entry.js create mode 100644 src/components/relative-ha-datetime.html create mode 100644 src/components/relative-ha-datetime.js create mode 100644 src/components/services-list.html create mode 100644 src/components/services-list.js create mode 100644 src/components/state-badge.html create mode 100644 src/components/state-badge.js create mode 100644 src/components/state-cards.html create mode 100644 src/components/state-cards.js create mode 100644 src/components/state-history-chart-line.html create mode 100644 src/components/state-history-chart-line.js create mode 100644 src/components/state-history-chart-timeline.html create mode 100644 src/components/state-history-chart-timeline.js create mode 100644 src/components/state-history-charts.html create mode 100644 src/components/state-history-charts.js create mode 100644 src/components/state-info.html create mode 100644 src/components/state-info.js create mode 100644 src/components/stream-status.html create mode 100644 src/components/stream-status.js create mode 100644 src/dialogs/more-info-dialog.html create mode 100644 src/dialogs/more-info-dialog.js create mode 100644 src/home-assistant.html create mode 100644 src/home-assistant.js create mode 100644 src/layouts/home-assistant-main.html create mode 100644 src/layouts/home-assistant-main.js create mode 100644 src/layouts/login-form.html create mode 100644 src/layouts/login-form.js create mode 100644 src/layouts/partial-base.html create mode 100644 src/layouts/partial-base.js create mode 100644 src/layouts/partial-dev-call-service.html create mode 100644 src/layouts/partial-dev-call-service.js create mode 100644 src/layouts/partial-dev-fire-event.html create mode 100644 src/layouts/partial-dev-fire-event.js create mode 100644 src/layouts/partial-dev-set-state.html create mode 100644 src/layouts/partial-dev-set-state.js create mode 100644 src/layouts/partial-history.html create mode 100644 src/layouts/partial-history.js create mode 100644 src/layouts/partial-logbook.html create mode 100644 src/layouts/partial-logbook.js create mode 100644 src/layouts/partial-states.html create mode 100644 src/layouts/partial-states.js create mode 100644 src/managers/notification-manager.html create mode 100644 src/managers/notification-manager.js create mode 100644 src/more-infos/more-info-camera.html create mode 100644 src/more-infos/more-info-camera.js create mode 100644 src/more-infos/more-info-configurator.html create mode 100644 src/more-infos/more-info-configurator.js create mode 100644 src/more-infos/more-info-content.html create mode 100644 src/more-infos/more-info-content.js create mode 100644 src/more-infos/more-info-default.html create mode 100644 src/more-infos/more-info-default.js create mode 100644 src/more-infos/more-info-group.html create mode 100644 src/more-infos/more-info-group.js create mode 100644 src/more-infos/more-info-light.html create mode 100644 src/more-infos/more-info-light.js create mode 100644 src/more-infos/more-info-media_player.html create mode 100644 src/more-infos/more-info-media_player.js create mode 100644 src/more-infos/more-info-script.html create mode 100644 src/more-infos/more-info-script.js create mode 100644 src/more-infos/more-info-sun.html create mode 100644 src/more-infos/more-info-sun.js create mode 100644 src/more-infos/more-info-thermostat.html create mode 100644 src/more-infos/more-info-thermostat.js create mode 100644 src/polymer.js create mode 100644 src/resources/home-assistant-icons.html create mode 100644 src/resources/home-assistant-style.html create mode 100644 src/resources/pikaday-js.html create mode 100644 src/util/attribute-class-names.js create mode 100644 src/util/bound-nuclear-behavior.js create mode 100644 src/util/domain-icon.js create mode 100644 src/util/format-date-time.js create mode 100644 src/util/format-date.js create mode 100644 src/util/format-time.js create mode 100644 src/util/nuclear-behavior.js create mode 100644 src/util/state-card-type.js create mode 100644 src/util/state-more-info-type.js create mode 100644 src/util/validate-auth.js create mode 100644 src/util/xybri-to-rgb.js create mode 100644 webpack.config.js diff --git a/.eslintrc b/.eslintrc new file mode 100644 index 0000000000..e9267ed37a --- /dev/null +++ b/.eslintrc @@ -0,0 +1,11 @@ +{ + "extends": "eslint-config-airbnb", + "globals": { + "__DEV__": false, + "__DEMO__": false + }, + "rules": { + "comma-dangle": [2, "always-multiline"], + "no-underscore-dangle": false + } +} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000000..ab3fcd61e1 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +build/* +node_modules/* +bower_components/* diff --git a/bower.json b/bower.json new file mode 100644 index 0000000000..ee24baa81c --- /dev/null +++ b/bower.json @@ -0,0 +1,44 @@ +{ + "name": "Home Assistant", + "version": "0.1.0", + "authors": [ + "Paulus Schoutsen " + ], + "main": "splash-login.html", + "license": "MIT", + "private": true, + "ignore": [ + "bower_components" + ], + "devDependencies": { + "polymer": "Polymer/polymer#^1.0.0", + "webcomponentsjs": "Polymer/webcomponentsjs#^0.7", + "paper-header-panel": "PolymerElements/paper-header-panel#^1.0.0", + "paper-toolbar": "PolymerElements/paper-toolbar#^1.0.0", + "paper-menu": "PolymerElements/paper-menu#^1.0.0", + "iron-input": "PolymerElements/iron-input#^1.0.0", + "iron-icons": "PolymerElements/iron-icons#^1.0.0", + "iron-image": "PolymerElements/iron-image#^1.0.0", + "paper-toast": "PolymerElements/paper-toast#^1.0.0", + "paper-dialog": "PolymerElements/paper-dialog#^1.0.0", + "paper-dialog-scrollable": "polymerelements/paper-dialog-scrollable#^1.0.0", + "paper-spinner": "PolymerElements/paper-spinner#^1.0.0", + "paper-button": "PolymerElements/paper-button#^1.0.0", + "paper-input": "PolymerElements/paper-input#^1.0.0", + "paper-toggle-button": "PolymerElements/paper-toggle-button#^1.0.0", + "paper-icon-button": "PolymerElements/paper-icon-button#^1.0.0", + "paper-item": "PolymerElements/paper-item#^1.0.0", + "paper-slider": "PolymerElements/paper-slider#^1.0.0", + "paper-checkbox": "PolymerElements/paper-checkbox#^1.0.0", + "paper-drawer-panel": "PolymerElements/paper-drawer-panel#^1.0.0", + "paper-scroll-header-panel": "polymerelements/paper-scroll-header-panel#^1.0.0", + "google-apis": "GoogleWebComponents/google-apis#0.8-preview", + "layout": "Polymer/layout", + "paper-styles": "polymerelements/paper-styles#^1.0.0", + "pikaday": "~1.3.2" + }, + "resolutions": { + "polymer": "^1.0.0", + "webcomponentsjs": "^0.7.0" + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000000..da931bc0df --- /dev/null +++ b/package.json @@ -0,0 +1,29 @@ +{ + "name": "home-assistant-polymer", + "version": "1.0.0", + "description": "A frontend for Home Assistant using the Polymer framework", + "scripts": { + "js_dev": "webpack --colors --progress -d --watch", + "js_dev_demo": "BUILD_DEMO=1 webpack --colors --progress -d --watch", + "js_prod": "BUILD_DEV=0 webpack --colors --progress -p -d", + "js_demo": "BUILD_DEV=0 BUILD_DEMO=1 webpack --colors --progress -p -d", + "frontend_html": "vulcanize --inline-css --inline-scripts --strip-comments src/home-assistant.html > build/frontend.vulcan.html", + "frontend_minify": "node scripts/minify.js", + "frontend_prod": "npm run js_prod && bower install && npm run frontend_html && npm run frontend_minify", + "frontend_demo": "npm run js_demo && bower install && npm run frontend_html && npm run frontend_minify" + }, + "author": "Paulus Schoutsen (http://paulusschoutsen.nl)", + "license": "MIT", + "devDependencies": { + "babel-core": "^5.6.18", + "babel-loader": "^5.3.1", + "bower": "^1.4.1", + "eslint-config-airbnb": "0.0.6", + "home-assistant-js": "git+https://github.com/balloob/home-assistant-js.git#d6159b2654e070e13017bc8f99418246c5112d9a", + "html-minifier": "^0.7.2", + "lodash": "^3.10.0", + "node-libs-browser": "^0.5.2", + "vulcanize": "^1.10.1", + "webpack": "^1.10.1" + } +} diff --git a/scripts/minify.js b/scripts/minify.js new file mode 100755 index 0000000000..c04bb50bfd --- /dev/null +++ b/scripts/minify.js @@ -0,0 +1,20 @@ +var minify = require('html-minifier'); +var fs = require('fs'); + +var html = fs.readFileSync('build/frontend.vulcan.html').toString(); + + // removeComments: true, + // collapseWhitespace: true, +var minifiedHtml = minify.minify(html, { + customAttrAssign: [/\$=/], + "removeComments": true, + "removeCommentsFromCDATA": true, + "removeCDATASectionsFromCDATA": true, + "collapseWhitespace": true, + "collapseBooleanAttributes": true, + "removeScriptTypeAttributes": true, + "removeStyleLinkTypeAttributes": true, + "minifyJS": true, +}); + +fs.writeFileSync('build/frontend.html', minifiedHtml); diff --git a/src/cards/state-card-configurator.html b/src/cards/state-card-configurator.html new file mode 100644 index 0000000000..cb6ebd9705 --- /dev/null +++ b/src/cards/state-card-configurator.html @@ -0,0 +1,15 @@ + + + + + + + + diff --git a/src/cards/state-card-configurator.js b/src/cards/state-card-configurator.js new file mode 100644 index 0000000000..3b7194efd3 --- /dev/null +++ b/src/cards/state-card-configurator.js @@ -0,0 +1,14 @@ +import Polymer from '../polymer'; + +require('../components/state-info'); +require('./state-card-display'); + +export default Polymer({ + is: 'state-card-configurator', + + properties: { + stateObj: { + type: Object, + }, + }, +}); \ No newline at end of file diff --git a/src/cards/state-card-content.html b/src/cards/state-card-content.html new file mode 100644 index 0000000000..e489684e83 --- /dev/null +++ b/src/cards/state-card-content.html @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/src/cards/state-card-content.js b/src/cards/state-card-content.js new file mode 100644 index 0000000000..a331c47e03 --- /dev/null +++ b/src/cards/state-card-content.js @@ -0,0 +1,46 @@ +import Polymer from '../polymer'; + +import stateCardType from '../util/state-card-type'; + +require('./state-card-display'); +require('./state-card-toggle'); +require('./state-card-thermostat'); +require('./state-card-configurator'); +require('./state-card-scene'); +require('./state-card-media_player'); + +export default Polymer({ + is: 'state-card-content', + + properties: { + stateObj: { + type: Object, + observer: 'stateObjChanged', + } + }, + + stateObjChanged: function(newVal, oldVal) { + var root = Polymer.dom(this); + + if (!newVal) { + if (root.lastChild) { + root.removeChild(root.lastChild); + } + return; + } + + var newCardType = stateCardType(newVal); + + if (!oldVal || stateCardType(oldVal) != newCardType) { + if (root.lastChild) { + root.removeChild(root.lastChild); + } + + var stateCard = document.createElement("state-card-" + newCardType); + stateCard.stateObj = newVal; + root.appendChild(stateCard); + } else { + root.lastChild.stateObj = newVal; + } + }, +}); diff --git a/src/cards/state-card-display.html b/src/cards/state-card-display.html new file mode 100755 index 0000000000..52976475f4 --- /dev/null +++ b/src/cards/state-card-display.html @@ -0,0 +1,22 @@ + + + + + + + + + diff --git a/src/cards/state-card-display.js b/src/cards/state-card-display.js new file mode 100644 index 0000000000..1f8d5cda5e --- /dev/null +++ b/src/cards/state-card-display.js @@ -0,0 +1,13 @@ +import Polymer from '../polymer'; + +require('../components/state-info'); + +export default Polymer({ + is: 'state-card-display', + + properties: { + stateObj: { + type: Object, + }, + }, +}); \ No newline at end of file diff --git a/src/cards/state-card-media_player.html b/src/cards/state-card-media_player.html new file mode 100644 index 0000000000..015170ed33 --- /dev/null +++ b/src/cards/state-card-media_player.html @@ -0,0 +1,41 @@ + + + + + + + + diff --git a/src/cards/state-card-media_player.js b/src/cards/state-card-media_player.js new file mode 100644 index 0000000000..6c7f04ee3e --- /dev/null +++ b/src/cards/state-card-media_player.js @@ -0,0 +1,50 @@ +import Polymer from '../polymer'; + +require('../components/state-info'); + +const PLAYING_STATES = ['playing', 'paused']; + +export default Polymer({ + is: 'state-card-media_player', + + properties: { + stateObj: { + type: Object, + }, + + isPlaying: { + type: Boolean, + computed: 'computeIsPlaying(stateObj)', + }, + }, + + computeIsPlaying: function(stateObj) { + return PLAYING_STATES.indexOf(stateObj.state) !== -1; + }, + + computePrimaryText: function(stateObj, isPlaying) { + return isPlaying ? stateObj.attributes.media_title : stateObj.stateDisplay; + }, + + computeSecondaryText: function(stateObj, isPlaying) { + var text; + + if (stateObj.attributes.media_content_type == 'music') { + return stateObj.attributes.media_artist; + + } else if (stateObj.attributes.media_content_type == 'tvshow') { + text = stateObj.attributes.media_series_title; + + if (stateObj.attributes.media_season && stateObj.attributes.media_episode) { + text += ` S${stateObj.attributes.media_season}E${stateObj.attributes.media_episode}`; + } + return text; + + } else if (stateObj.attributes.app_name) { + return stateObj.attributes.app_name; + + } else { + return ''; + } + }, +}); diff --git a/src/cards/state-card-scene.html b/src/cards/state-card-scene.html new file mode 100644 index 0000000000..593e05032f --- /dev/null +++ b/src/cards/state-card-scene.html @@ -0,0 +1,15 @@ + + + + + + + + diff --git a/src/cards/state-card-scene.js b/src/cards/state-card-scene.js new file mode 100644 index 0000000000..a07643578a --- /dev/null +++ b/src/cards/state-card-scene.js @@ -0,0 +1,24 @@ +import Polymer from '../polymer'; + +require('./state-card-display'); +require('./state-card-toggle'); + +export default Polymer({ + is: 'state-card-scene', + + properties: { + stateObj: { + type: Object, + }, + + allowToggle: { + type: Boolean, + value: false, + computed: 'computeAllowToggle(stateObj)', + }, + }, + + computeAllowToggle: function(stateObj) { + return stateObj.state === 'off' || stateObj.attributes.active_requested; + }, +}); diff --git a/src/cards/state-card-thermostat.html b/src/cards/state-card-thermostat.html new file mode 100644 index 0000000000..0a062e5756 --- /dev/null +++ b/src/cards/state-card-thermostat.html @@ -0,0 +1,43 @@ + + + + + + + + diff --git a/src/cards/state-card-thermostat.js b/src/cards/state-card-thermostat.js new file mode 100644 index 0000000000..89abd3cfd9 --- /dev/null +++ b/src/cards/state-card-thermostat.js @@ -0,0 +1,13 @@ +import Polymer from '../polymer'; + +require('../components/state-info'); + +export default Polymer({ + is: 'state-card-thermostat', + + properties: { + stateObj: { + type: Object, + }, + }, +}); diff --git a/src/cards/state-card-toggle.html b/src/cards/state-card-toggle.html new file mode 100755 index 0000000000..c3021cbe9f --- /dev/null +++ b/src/cards/state-card-toggle.html @@ -0,0 +1,24 @@ + + + + + + + + + diff --git a/src/cards/state-card-toggle.js b/src/cards/state-card-toggle.js new file mode 100644 index 0000000000..57bb6b46d9 --- /dev/null +++ b/src/cards/state-card-toggle.js @@ -0,0 +1,70 @@ +import { serviceActions } from 'home-assistant-js'; + +import Polymer from '../polymer'; + +require('../components/state-info'); + +export default Polymer({ + is: 'state-card-toggle', + + properties: { + stateObj: { + type: Object, + observer: 'stateObjChanged', + }, + + toggleChecked: { + type: Boolean, + value: false, + }, + }, + + ready() { + this.forceStateChange = this.forceStateChange.bind(this); + this.forceStateChange(); + }, + + toggleTapped(ev) { + ev.stopPropagation(); + }, + + toggleChanged(ev) { + var newVal = ev.target.checked; + + if(newVal && this.stateObj.state === "off") { + this.turn_on(); + } else if(!newVal && this.stateObj.state === "on") { + this.turn_off(); + } + }, + + stateObjChanged(newVal) { + if (newVal) { + this.updateToggle(newVal); + } + }, + + updateToggle(stateObj) { + this.toggleChecked = stateObj && stateObj.state === "on"; + }, + + forceStateChange() { + this.updateToggle(this.stateObj); + }, + + turn_on() { + // We call updateToggle after a successful call to re-sync the toggle + // with the state. It will be out of sync if our service call did not + // result in the entity to be turned on. Since the state is not changing, + // the resync is not called automatic. + serviceActions.callTurnOn(this.stateObj.entityId).then(this.forceStateChange); + }, + + turn_off() { + // We call updateToggle after a successful call to re-sync the toggle + // with the state. It will be out of sync if our service call did not + // result in the entity to be turned on. Since the state is not changing, + // the resync is not called automatic. + serviceActions.callTurnOff(this.stateObj.entityId).then(this.forceStateChange); + }, +}); diff --git a/src/cards/state-card.html b/src/cards/state-card.html new file mode 100644 index 0000000000..523b9f2221 --- /dev/null +++ b/src/cards/state-card.html @@ -0,0 +1,29 @@ + + + + + + + + + diff --git a/src/cards/state-card.js b/src/cards/state-card.js new file mode 100644 index 0000000000..7f99f2c01e --- /dev/null +++ b/src/cards/state-card.js @@ -0,0 +1,25 @@ +import { moreInfoActions } from 'home-assistant-js'; + +import Polymer from '../polymer'; + +require('./state-card-content'); + +export default Polymer({ + is: 'state-card', + + properties: { + stateObj: { + type: Object, + }, + }, + + listeners: { + 'tap': 'cardTapped', + }, + + cardTapped(ev) { + ev.stopPropagation(); + this.async(moreInfoActions.selectEntity.bind( + this, this.stateObj.entityId), 100); + }, +}); diff --git a/src/components/display-time.html b/src/components/display-time.html new file mode 100644 index 0000000000..4c31e6bdfe --- /dev/null +++ b/src/components/display-time.html @@ -0,0 +1,5 @@ + + + + + diff --git a/src/components/display-time.js b/src/components/display-time.js new file mode 100644 index 0000000000..79b9fe0732 --- /dev/null +++ b/src/components/display-time.js @@ -0,0 +1,17 @@ +import Polymer from '../polymer'; + +import formatTime from '../util/format-time'; + +export default Polymer({ + is: 'display-time', + + properties: { + dateObj: { + type: Object, + }, + }, + + computeTime: function(dateObj) { + return dateObj ? formatTime(dateObj) : ''; + }, +}); diff --git a/src/components/domain-icon.html b/src/components/domain-icon.html new file mode 100644 index 0000000000..05f01c6dcf --- /dev/null +++ b/src/components/domain-icon.html @@ -0,0 +1,11 @@ + + + + + + + + + diff --git a/src/components/domain-icon.js b/src/components/domain-icon.js new file mode 100644 index 0000000000..819f4abee2 --- /dev/null +++ b/src/components/domain-icon.js @@ -0,0 +1,23 @@ +import Polymer from '../polymer'; + +import domainIcon from '../util/domain-icon'; + +export default Polymer({ + is: 'domain-icon', + + properties: { + domain: { + type: String, + value: '', + }, + + state: { + type: String, + value: '', + }, + }, + + computeIcon(domain, state) { + return domainIcon(domain, state); + }, +}); diff --git a/src/components/entity-list.html b/src/components/entity-list.html new file mode 100644 index 0000000000..ce7f95ba5f --- /dev/null +++ b/src/components/entity-list.html @@ -0,0 +1,27 @@ + + + + + + + diff --git a/src/components/entity-list.js b/src/components/entity-list.js new file mode 100644 index 0000000000..ad00894ee9 --- /dev/null +++ b/src/components/entity-list.js @@ -0,0 +1,29 @@ +import { entityGetters } from 'home-assistant-js'; + +import Polymer from '../polymer'; +import nuclearObserver from '../util/bound-nuclear-behavior'; + +export default Polymer({ + is: 'entity-list', + + behaviors: [nuclearObserver], + + properties: { + entities: { + type: Array, + bindNuclear: [ + entityGetters.entityMap, + function(map) { + return map.valueSeq(). + sortBy(function(entity) { return entity.entityId; }) + .toArray(); + }, + ], + }, + }, + + entitySelected: function(ev) { + ev.preventDefault(); + this.fire('entity-selected', {entityId: ev.model.entity.entityId}); + }, +}); diff --git a/src/components/events-list.html b/src/components/events-list.html new file mode 100644 index 0000000000..5eb3eba917 --- /dev/null +++ b/src/components/events-list.html @@ -0,0 +1,30 @@ + + + + + + + diff --git a/src/components/events-list.js b/src/components/events-list.js new file mode 100644 index 0000000000..b73eb613d7 --- /dev/null +++ b/src/components/events-list.js @@ -0,0 +1,29 @@ +import { eventGetters } from 'home-assistant-js'; + +import Polymer from '../polymer'; +import nuclearObserver from '../util/bound-nuclear-behavior'; + +export default Polymer({ + is: 'events-list', + + behaviors: [nuclearObserver], + + properties: { + events: { + type: Array, + bindNuclear: [ + eventGetters.entityMap, + function(map) { + return map.valueSeq() + .sortBy(function(event) { return event.event; }) + .toArray(); + } + ], + }, + }, + + eventSelected(ev) { + ev.preventDefault(); + this.fire('event-selected', {eventType: ev.model.event.event}); + }, +}); diff --git a/src/components/ha-color-picker.html b/src/components/ha-color-picker.html new file mode 100644 index 0000000000..1bc8574d45 --- /dev/null +++ b/src/components/ha-color-picker.html @@ -0,0 +1,12 @@ + + + + + + diff --git a/src/components/ha-color-picker.js b/src/components/ha-color-picker.js new file mode 100644 index 0000000000..ecff57e7c2 --- /dev/null +++ b/src/components/ha-color-picker.js @@ -0,0 +1,151 @@ +/** + * Color-picker custom element + * Originally created by bbrewer97202 (Ben Brewer). MIT Licensed. + * https://github.com/bbrewer97202/color-picker-element + * + * Adapted to work with Polymer. + */ +import Polymer from '../polymer'; + +/** + * given red, green, blue values, return the equivalent hexidecimal value + * base source: http://stackoverflow.com/a/5624139 + */ +var componentToHex = function(c) { + var hex = c.toString(16); + return hex.length === 1 ? "0" + hex : hex; +}; + +var rgbToHex = function(color) { + return "#" + componentToHex(color.r) + componentToHex(color.g) + + componentToHex(color.b); +}; + +export default Polymer({ + is: 'ha-color-picker', + + properties: { + width: { + type: Number, + value: 300, + }, + height: { + type: Number, + value: 300, + }, + color: { + type: Object, + }, + }, + + listeners: { + 'mousedown': 'onMouseDown', + 'mouseup': 'onMouseUp', + 'touchstart': 'onTouchStart', + 'touchend': 'onTouchEnd', + 'tap': 'onTap', + }, + + onMouseDown(e) { + this.onMouseMove(e); + this.addEventListener('mousemove', this.onMouseMove); + }, + + onMouseUp(e) { + this.removeEventListener('mousemove', this.onMouseMove); + }, + + onTouchStart(e) { + this.onTouchMove(e); + this.addEventListener('touchmove', this.onTouchMove); + }, + + onTouchEnd(e) { + this.removeEventListener('touchmove', this.onTouchMove); + }, + + onTap(e) { + e.stopPropagation(); + }, + + onTouchMove(e) { + var touch = e.touches[0]; + this.onColorSelect(e, {x: touch.clientX, y: touch.clientY}); + }, + + onMouseMove(e) { + e.preventDefault(); + if (this.mouseMoveIsThrottled) { + this.mouseMoveIsThrottled = false; + this.onColorSelect(e); + this.async( + function() { this.mouseMoveIsThrottled = true; }.bind(this), 100); + } + }, + + onColorSelect(e, coords) { + if (this.context) { + coords = coords || this.relativeMouseCoordinates(e); + var data = this.context.getImageData(coords.x, coords.y, 1, 1).data; + + this.setColor({r: data[0], g: data[1], b: data[2]}); + } + }, + + setColor(rgb) { + //save calculated color + this.color = {hex: rgbToHex(rgb), rgb: rgb}; + + this.fire('colorselected', { + rgb: this.color.rgb, + hex: this.color.hex + }); + }, + + /** + * given a mouse click event, return x,y coordinates relative to the clicked target + * @returns object with x, y values + */ + relativeMouseCoordinates(e) { + var x = 0, y = 0; + + if (this.canvas) { + var rect = this.canvas.getBoundingClientRect(); + x = e.clientX - rect.left; + y = e.clientY - rect.top; + } + + return { + x: x, + y: y + }; + }, + + ready() { + this.setColor = this.setColor.bind(this); + this.mouseMoveIsThrottled = true; + this.canvas = this.children[0]; + this.context = this.canvas.getContext('2d'); + + var colorGradient = this.context.createLinearGradient(0, 0, this.width, 0); + colorGradient.addColorStop(0, "rgb(255,0,0)"); + colorGradient.addColorStop(0.16, "rgb(255,0,255)"); + colorGradient.addColorStop(0.32, "rgb(0,0,255)"); + colorGradient.addColorStop(0.48, "rgb(0,255,255)"); + colorGradient.addColorStop(0.64, "rgb(0,255,0)"); + colorGradient.addColorStop(0.80, "rgb(255,255,0)"); + colorGradient.addColorStop(1, "rgb(255,0,0)"); + this.context.fillStyle = colorGradient; + this.context.fillRect(0, 0, this.width, this.height); + + var bwGradient = this.context.createLinearGradient(0, 0, 0, this.height); + bwGradient.addColorStop(0, "rgba(255,255,255,1)"); + bwGradient.addColorStop(0.5, "rgba(255,255,255,0)"); + bwGradient.addColorStop(0.5, "rgba(0,0,0,0)"); + bwGradient.addColorStop(1, "rgba(0,0,0,1)"); + + this.context.fillStyle = bwGradient; + this.context.fillRect(0, 0, this.width, this.height); + }, + +}); diff --git a/src/components/ha-logbook.html b/src/components/ha-logbook.html new file mode 100644 index 0000000000..c5b7da5e26 --- /dev/null +++ b/src/components/ha-logbook.html @@ -0,0 +1,20 @@ + + + + + + + + diff --git a/src/components/ha-logbook.js b/src/components/ha-logbook.js new file mode 100644 index 0000000000..4d58911890 --- /dev/null +++ b/src/components/ha-logbook.js @@ -0,0 +1,16 @@ +import Polymer from '../polymer'; + +Polymer({ + is: 'ha-logbook', + + properties: { + entries: { + type: Object, + value: [], + }, + }, + + noEntries: function(entries) { + return !entries.length; + } +}); diff --git a/src/components/ha-sidebar.html b/src/components/ha-sidebar.html new file mode 100644 index 0000000000..acf8a13b58 --- /dev/null +++ b/src/components/ha-sidebar.html @@ -0,0 +1,128 @@ + + + + + + + + + + + + + + + + + + diff --git a/src/components/ha-sidebar.js b/src/components/ha-sidebar.js new file mode 100644 index 0000000000..6c21780f12 --- /dev/null +++ b/src/components/ha-sidebar.js @@ -0,0 +1,121 @@ +import { + configGetters, + navigationGetters, + authActions, + navigationActions, + util +} from 'home-assistant-js'; + +import Polymer from '../polymer'; +import nuclearObserver from '../util/bound-nuclear-behavior'; +import domainIcon from '../util/domain-icon'; + +require('./stream-status'); + +const { entityDomainFilters } = util; + +Polymer({ + is: 'ha-sidebar', + + behaviors: [nuclearObserver], + + properties: { + menuSelected: { + type: String, + // observer: 'menuSelectedChanged', + }, + + selected: { + type: String, + bindNuclear: navigationGetters.activePage, + observer: 'selectedChanged', + }, + + possibleFilters: { + type: Array, + value: [], + bindNuclear: [ + navigationGetters.possibleEntityDomainFilters, + function(domains) { return domains.toArray(); } + ], + }, + + hasHistoryComponent: { + type: Boolean, + bindNuclear: configGetters.isComponentLoaded('history'), + }, + + hasLogbookComponent: { + type: Boolean, + bindNuclear: configGetters.isComponentLoaded('logbook'), + }, + }, + + // menuSelectedChanged: function(newVal) { + // if (this.selected !== newVal) { + // this.selectPanel(newVal); + // } + // }, + + selectedChanged(newVal) { + // if (this.menuSelected !== newVal) { + // this.menuSelected = newVal; + // } + var menuItems = this.querySelectorAll('.menu [data-panel]'); + + for (var i = 0; i < menuItems.length; i++) { + if(menuItems[i].dataset.panel === newVal) { + menuItems[i].classList.add('selected'); + } else { + menuItems[i].classList.remove('selected'); + } + } + }, + + menuClicked(ev) { + var target = ev.target; + var checks = 5; + + // find panel to select + while(checks && !target.dataset.panel) { + target = target.parentElement; + checks--; + } + + if (checks) { + this.selectPanel(target.dataset.panel); + } + }, + + handleDevClick(ev) { + // prevent it from highlighting first menu item + document.activeElement.blur(); + this.menuClicked(ev); + }, + + selectPanel(newChoice) { + if(newChoice === this.selected) { + return; + } else if (newChoice == 'logout') { + this.handleLogOut(); + return; + } + navigationActions.navigate.apply(null, newChoice.split('/')); + }, + + handleLogOut() { + authActions.logOut(); + }, + + filterIcon(filter) { + return domainIcon(filter); + }, + + filterName(filter) { + return entityDomainFilters[filter]; + }, + + filterType(filter) { + return 'states/' + filter; + }, +}); diff --git a/src/components/ha-voice-command-progress.html b/src/components/ha-voice-command-progress.html new file mode 100644 index 0000000000..368b2c0e55 --- /dev/null +++ b/src/components/ha-voice-command-progress.html @@ -0,0 +1,32 @@ + + + + + + + + + + + diff --git a/src/components/ha-voice-command-progress.js b/src/components/ha-voice-command-progress.js new file mode 100644 index 0000000000..8e2f93ebcc --- /dev/null +++ b/src/components/ha-voice-command-progress.js @@ -0,0 +1,27 @@ +import { voiceGetters } from 'home-assistant-js'; + +import Polymer from '../polymer'; +import nuclearObserver from '../util/bound-nuclear-behavior'; + +export default Polymer({ + is: 'ha-voice-command-progress', + + behaviors: [nuclearObserver], + + properties: { + isTransmitting: { + type: Boolean, + bindNuclear: voiceGetters.isTransmitting, + }, + + interimTranscript: { + type: String, + bindNuclear: voiceGetters.extraInterimTranscript, + }, + + finalTranscript: { + type: String, + bindNuclear: voiceGetters.finalTranscript, + }, + }, +}); diff --git a/src/components/loading-box.html b/src/components/loading-box.html new file mode 100644 index 0000000000..acb36ded2a --- /dev/null +++ b/src/components/loading-box.html @@ -0,0 +1,19 @@ + + + + + + + diff --git a/src/components/loading-box.js b/src/components/loading-box.js new file mode 100644 index 0000000000..f5d858914d --- /dev/null +++ b/src/components/loading-box.js @@ -0,0 +1,5 @@ +import Polymer from '../polymer'; + +export default Polymer({ + is: 'loading-box', +}); diff --git a/src/components/logbook-entry.html b/src/components/logbook-entry.html new file mode 100644 index 0000000000..5b2e89ad3b --- /dev/null +++ b/src/components/logbook-entry.html @@ -0,0 +1,53 @@ + + + + + + + + + + diff --git a/src/components/logbook-entry.js b/src/components/logbook-entry.js new file mode 100644 index 0000000000..81b6adb01b --- /dev/null +++ b/src/components/logbook-entry.js @@ -0,0 +1,12 @@ +import { moreInfoActions } from 'home-assistant-js'; + +import Polymer from '../polymer'; + +export default Polymer({ + is: 'logbook-entry', + + entityClicked: function(ev) { + ev.preventDefault(); + moreInfoActions.selectEntity(this.entryObj.entityId); + } +}); diff --git a/src/components/relative-ha-datetime.html b/src/components/relative-ha-datetime.html new file mode 100644 index 0000000000..25317437f1 --- /dev/null +++ b/src/components/relative-ha-datetime.html @@ -0,0 +1,7 @@ + + + + + diff --git a/src/components/relative-ha-datetime.js b/src/components/relative-ha-datetime.js new file mode 100644 index 0000000000..353cb66521 --- /dev/null +++ b/src/components/relative-ha-datetime.js @@ -0,0 +1,61 @@ +import moment from 'moment'; +import { util } from 'home-assistant-js'; +import Polymer from '../polymer'; + +const UPDATE_INTERVAL = 60000; // 60 seconds + +const { parseDateTime } = util; + +export default Polymer({ + is: 'relative-ha-datetime', + + properties: { + datetime: { + type: String, + observer: 'datetimeChanged', + }, + + datetimeObj: { + type: Object, + observer: 'datetimeObjChanged', + }, + + parsedDateTime: { + type: Object, + }, + + relativeTime: { + type: String, + value: 'not set', + }, + }, + + created() { + this.updateRelative = this.updateRelative.bind(this); + }, + + attached() { + this._interval = setInterval(this.updateRelative, UPDATE_INTERVAL); + }, + + detached() { + clearInterval(this._interval); + }, + + datetimeChanged(newVal) { + this.parsedDateTime = newVal ? parseDateTime(newVal) : null; + + this.updateRelative(); + }, + + datetimeObjChanged(newVal) { + this.parsedDateTime = newVal; + + this.updateRelative(); + }, + + updateRelative() { + this.relativeTime = this.parsedDateTime ? + moment(this.parsedDateTime).fromNow() : ""; + }, +}); diff --git a/src/components/services-list.html b/src/components/services-list.html new file mode 100644 index 0000000000..832ef4851e --- /dev/null +++ b/src/components/services-list.html @@ -0,0 +1,35 @@ + + + + + + + + + + + diff --git a/src/components/services-list.js b/src/components/services-list.js new file mode 100644 index 0000000000..26f89f8a2e --- /dev/null +++ b/src/components/services-list.js @@ -0,0 +1,37 @@ +import { serviceGetters } from 'home-assistant-js'; + +import Polymer from '../polymer'; +import nuclearObserver from '../util/bound-nuclear-behavior'; + +require('./domain-icon'); + +export default Polymer({ + is: 'services-list', + + behaviors: [nuclearObserver], + + properties: { + serviceDomains: { + type: Array, + bindNuclear: [ + serviceGetters.entityMap, + function(map) { + return map.valueSeq() + .sortBy(function(domain) { return domain.domain; }) + .toJS(); + }, + ], + }, + }, + + computeServices(domain) { + return this.services.get(domain).toArray(); + }, + + serviceClicked(ev) { + ev.preventDefault(); + this.fire( + 'service-selected', {domain: ev.model.domain.domain, + service: ev.model.service}); + }, +}); diff --git a/src/components/state-badge.html b/src/components/state-badge.html new file mode 100644 index 0000000000..d97ab76b65 --- /dev/null +++ b/src/components/state-badge.html @@ -0,0 +1,52 @@ + + + + + + + + + + diff --git a/src/components/state-badge.js b/src/components/state-badge.js new file mode 100644 index 0000000000..d44e90df86 --- /dev/null +++ b/src/components/state-badge.js @@ -0,0 +1,34 @@ +import Polymer from '../polymer'; + +import xyBriToRgb from '../util/xybri-to-rgb'; + +require('./domain-icon'); + +export default Polymer({ + is: 'state-badge', + + properties: { + stateObj: { + type: Object, + observer: 'updateIconColor', + }, + }, + + /** + * Called when an attribute changes that influences the color of the icon. + */ + updateIconColor(newVal) { + // for domain light, set color of icon to light color if available + if(newVal.domain == "light" && newVal.state == "on" && + newVal.attributes.brightness && newVal.attributes.xy_color) { + + var rgb = xyBriToRgb(newVal.attributes.xy_color[0], + newVal.attributes.xy_color[1], + newVal.attributes.brightness); + this.$.icon.style.color = "rgb(" + rgb.map(Math.floor).join(",") + ")"; + } else { + this.$.icon.style.color = null; + } + }, + +}); diff --git a/src/components/state-cards.html b/src/components/state-cards.html new file mode 100644 index 0000000000..fa379740e6 --- /dev/null +++ b/src/components/state-cards.html @@ -0,0 +1,61 @@ + + + + + + + + + diff --git a/src/components/state-cards.js b/src/components/state-cards.js new file mode 100644 index 0000000000..c151d707e9 --- /dev/null +++ b/src/components/state-cards.js @@ -0,0 +1,18 @@ +import Polymer from '../polymer'; + +require('../cards/state-card'); + +Polymer({ + is: 'state-cards', + + properties: { + states: { + type: Array, + value: [], + }, + }, + + computeEmptyStates(states) { + return states.length === 0; + }, +}); diff --git a/src/components/state-history-chart-line.html b/src/components/state-history-chart-line.html new file mode 100644 index 0000000000..de3742e6d3 --- /dev/null +++ b/src/components/state-history-chart-line.html @@ -0,0 +1 @@ + diff --git a/src/components/state-history-chart-line.js b/src/components/state-history-chart-line.js new file mode 100644 index 0000000000..a6e96d88b8 --- /dev/null +++ b/src/components/state-history-chart-line.js @@ -0,0 +1,194 @@ +import pluck from 'lodash/collection/pluck'; +import flatten from 'lodash/array/flatten'; +import uniq from 'lodash/array/uniq'; +import sortBy from 'lodash/collection/sortBy'; + +import Polymer from '../polymer'; + +export default Polymer({ + is: 'state-history-chart-line', + + properties: { + data: { + type: Object, + observer: 'dataChanged', + }, + + unit: { + type: String, + }, + + isSingleDevice: { + type: Boolean, + value: false, + }, + + isAttached: { + type: Boolean, + value: false, + observer: 'dataChanged', + }, + }, + + created: function() { + this.style.display = 'block'; + }, + + attached: function() { + this.isAttached = true; + }, + + dataChanged: function() { + this.drawChart(); + }, + + /************************************************** + The following code gererates line graphs for devices with continuous + values(which are devices that have a unit_of_measurement values defined). + On each graph the devices are grouped by their unit of measurement, eg. all + sensors measuring MB will be a separate line on single graph. The google + chart API takes data as a 2 dimensional array in the format: + + DateTime, device1, device2, device3 + 2015-04-01, 1, 2, 0 + 2015-04-01, 0, 1, 0 + 2015-04-01, 2, 1, 1 + + NOTE: the first column is a javascript date objects. + + The first thing we do is build up the data with rows for each time of a state + change and initialise the values to 0. THen we loop through each device and + fill in its data. + + **************************************************/ + drawChart() { + if (!this.isAttached) { + return; + } + + var root = Polymer.dom(this); + var unit = this.unit; + var deviceStates = this.data; + + while (root.lastChild) { + root.removeChild(root.lastChild); + } + + if (deviceStates.length === 0) { + return; + } + + var chart = new google.visualization.LineChart(this); + var dataTable = new google.visualization.DataTable(); + + dataTable.addColumn({ type: 'datetime', id: 'Time' }); + + var options = { + legend: { position: 'top' }, + titlePosition: 'none', + vAxes: { + // Adds units to the left hand side of the graph + 0: {title: unit} + }, + hAxis: { + format: 'H:mm' + }, + lineWidth: 1, + chartArea:{left:'60',width:"95%"}, + explorer: { + actions: ['dragToZoom', 'rightClickToReset', 'dragToPan'], + keepInBounds: true, + axis: 'horizontal', + maxZoomIn: 0.1 + } + }; + + if(this.isSingleDevice) { + options.legend.position = 'none'; + options.vAxes[0].title = null; + options.chartArea.left = 40; + options.chartArea.height = '80%'; + options.chartArea.top = 5; + options.enableInteractivity = false; + } + + // Get a unique list of times of state changes for all the device + // for a particular unit of measureent. + var times = pluck(flatten(deviceStates), "lastChangedAsDate"); + times = uniq(times, function(e) { + return e.getTime(); + }); + + times = sortBy(times, function(o) { return o; }); + + var data = []; + var empty = new Array(deviceStates.length); + for(var i = 0; i < empty.length; i++) { + empty[i] = 0; + } + + var timeIndex = 1; + var endDate = new Date(); + var prevDate = times[0]; + + for(i = 0; i < times.length; i++) { + // because we only have state changes we add an extra point at the same time + // that holds the previous state which makes the line display correctly + var beforePoint = new Date(times[i]); + data.push([beforePoint].concat(empty)); + + data.push([times[i]].concat(empty)); + prevDate = times[i]; + timeIndex++; + } + data.push([endDate].concat(empty)); + + + var deviceCount = 0; + deviceStates.forEach(function(device) { + var attributes = device[device.length - 1].attributes; + dataTable.addColumn('number', attributes.friendly_name); + + var currentState = 0; + var previousState = 0; + var lastIndex = 0; + var count = 0; + var prevTime = data[0][0]; + device.forEach(function(state) { + + currentState = state.state; + var start = state.lastChangedAsDate; + if(state.state == 'None') { + currentState = previousState; + } + for(var i = lastIndex; i < data.length; i++) { + data[i][1 + deviceCount] = parseFloat(previousState); + // this is where data gets filled in for each time for the particular device + // because for each time two entries were create we fill the first one with the + // previous value and the second one with the new value + if(prevTime.getTime() == data[i][0].getTime() && data[i][0].getTime() == start.getTime()) { + data[i][1 + deviceCount] = parseFloat(currentState); + lastIndex = i; + prevTime = data[i][0]; + break; + } + prevTime = data[i][0]; + } + + previousState = currentState; + + count++; + }.bind(this)); + + //fill in the rest of the Array + for(var i = lastIndex; i < data.length; i++) { + data[i][1 + deviceCount] = parseFloat(previousState); + } + + deviceCount++; + }.bind(this)); + + dataTable.addRows(data); + chart.draw(dataTable, options); + }, +}); diff --git a/src/components/state-history-chart-timeline.html b/src/components/state-history-chart-timeline.html new file mode 100644 index 0000000000..2627eaba11 --- /dev/null +++ b/src/components/state-history-chart-timeline.html @@ -0,0 +1,10 @@ + + + + + + diff --git a/src/components/state-history-chart-timeline.js b/src/components/state-history-chart-timeline.js new file mode 100644 index 0000000000..64cf0031b3 --- /dev/null +++ b/src/components/state-history-chart-timeline.js @@ -0,0 +1,107 @@ +import Polymer from '../polymer'; + +export default Polymer({ + is: 'state-history-chart-timeline', + + properties: { + data: { + type: Object, + observer: 'dataChanged', + }, + + isAttached: { + type: Boolean, + value: false, + observer: 'dataChanged', + }, + }, + + attached() { + this.isAttached = true; + }, + + dataChanged() { + this.drawChart(); + }, + + drawChart() { + if (!this.isAttached) { + return; + } + var root = Polymer.dom(this); + var stateHistory = this.data; + + while (root.node.lastChild) { + root.node.removeChild(root.node.lastChild); + } + + if (!stateHistory || stateHistory.length === 0) { + return; + } + + var chart = new google.visualization.Timeline(this); + var dataTable = new google.visualization.DataTable(); + + dataTable.addColumn({ type: 'string', id: 'Entity' }); + dataTable.addColumn({ type: 'string', id: 'State' }); + dataTable.addColumn({ type: 'date', id: 'Start' }); + dataTable.addColumn({ type: 'date', id: 'End' }); + + var addRow = function(entityDisplay, stateStr, start, end) { + stateStr = stateStr.replace(/_/g, ' '); + dataTable.addRow([entityDisplay, stateStr, start, end]); + }; + + var startTime = new Date( + stateHistory.reduce(function(minTime, stateInfo) { + return Math.min( + minTime, stateInfo[0].lastChangedAsDate); + }, new Date()) + ); + + // end time is Math.min(curTime, start time + 1 day) + var endTime = new Date(startTime); + endTime.setDate(endTime.getDate()+1); + if (endTime > new Date()) { + endTime = new Date(); + } + + var numTimelines = 0; + // stateHistory is a list of lists of sorted state objects + stateHistory.forEach(function(stateInfo) { + if(stateInfo.length === 0) return; + + var entityDisplay = stateInfo[0].entityDisplay; + var newLastChanged, prevState = null, prevLastChanged = null; + + stateInfo.forEach(function(state) { + if (prevState !== null && state.state !== prevState) { + newLastChanged = state.lastChangedAsDate; + + addRow(entityDisplay, prevState, prevLastChanged, newLastChanged); + + prevState = state.state; + prevLastChanged = newLastChanged; + } else if (prevState === null) { + prevState = state.state; + prevLastChanged = state.lastChangedAsDate; + } + }); + + addRow(entityDisplay, prevState, prevLastChanged, endTime); + numTimelines++; + }.bind(this)); + + chart.draw(dataTable, { + height: 55 + numTimelines * 42, + + timeline: { + showRowLabels: stateHistory.length > 1 + }, + + hAxis: { + format: 'H:mm' + }, + }); + }, +}); diff --git a/src/components/state-history-charts.html b/src/components/state-history-charts.html new file mode 100644 index 0000000000..2976bbe40f --- /dev/null +++ b/src/components/state-history-charts.html @@ -0,0 +1,49 @@ + + + + + + + + + + + + diff --git a/src/components/state-history-charts.js b/src/components/state-history-charts.js new file mode 100644 index 0000000000..83d9934991 --- /dev/null +++ b/src/components/state-history-charts.js @@ -0,0 +1,110 @@ +import Polymer from '../polymer'; + +require('./loading-box'); +require('./state-history-chart-timeline'); +require('./state-history-chart-line'); + +export default Polymer({ + is: 'state-history-charts', + + properties: { + stateHistory: { + type: Object, + }, + + isLoadingData: { + type: Boolean, + value: false, + }, + + apiLoaded: { + type: Boolean, + value: false, + }, + + isLoading: { + type: Boolean, + computed: 'computeIsLoading(isLoadingData, apiLoaded)', + }, + + groupedStateHistory: { + type: Object, + computed: 'computeGroupedStateHistory(isLoading, stateHistory)', + }, + + isSingleDevice: { + type: Boolean, + computed: 'computeIsSingleDevice(stateHistory)', + }, + }, + + computeIsSingleDevice(stateHistory) { + return stateHistory && stateHistory.size == 1; + }, + + computeGroupedStateHistory(isLoading, stateHistory) { + if (isLoading || !stateHistory) { + return {line: [], timeline: []}; + } + + var lineChartDevices = {}; + var timelineDevices = []; + + stateHistory.forEach(function(stateInfo) { + if (!stateInfo || stateInfo.size === 0) { + return; + } + + var stateWithUnit = stateInfo.find(function(state) { + return 'unit_of_measurement' in state.attributes; + }); + + var unit = stateWithUnit ? + stateWithUnit.attributes.unit_of_measurement : false; + + if (!unit) { + timelineDevices.push(stateInfo.toArray()); + } else if(unit in lineChartDevices) { + lineChartDevices[unit].push(stateInfo.toArray()); + } else { + lineChartDevices[unit] = [stateInfo.toArray()]; + } + }); + + timelineDevices = timelineDevices.length > 0 && timelineDevices; + + var unitStates = Object.keys(lineChartDevices).map(function(unit) { + return [unit, lineChartDevices[unit]]; }); + + return {line: unitStates, timeline: timelineDevices}; + }, + + googleApiLoaded() { + google.load("visualization", "1", { + packages: ["timeline", "corechart"], + callback: function() { + this.apiLoaded = true; + }.bind(this) + }); + }, + + computeContentClasses(isLoading) { + return isLoading ? 'loading' : ''; + }, + + computeIsLoading(isLoadingData, apiLoaded) { + return isLoadingData || !apiLoaded; + }, + + computeIsEmpty(stateHistory) { + return stateHistory && stateHistory.size === 0; + }, + + extractUnit(arr) { + return arr[0]; + }, + + extractData(arr) { + return arr[1]; + }, +}); diff --git a/src/components/state-info.html b/src/components/state-info.html new file mode 100644 index 0000000000..638ba0dbe4 --- /dev/null +++ b/src/components/state-info.html @@ -0,0 +1,53 @@ + + + + + + + + + + diff --git a/src/components/state-info.js b/src/components/state-info.js new file mode 100644 index 0000000000..29e874bdc3 --- /dev/null +++ b/src/components/state-info.js @@ -0,0 +1,14 @@ +import Polymer from '../polymer'; + +require('./state-badge'); +require('./relative-ha-datetime'); + +export default Polymer({ + is: 'state-info', + + properties: { + stateObj: { + type: Object, + }, + }, +}); diff --git a/src/components/stream-status.html b/src/components/stream-status.html new file mode 100644 index 0000000000..c9ee7645a8 --- /dev/null +++ b/src/components/stream-status.html @@ -0,0 +1,23 @@ + + + + + + + + + + + diff --git a/src/components/stream-status.js b/src/components/stream-status.js new file mode 100644 index 0000000000..7fd8e3e3dc --- /dev/null +++ b/src/components/stream-status.js @@ -0,0 +1,30 @@ +import { streamGetters, streamActions } from 'home-assistant-js'; + +import Polymer from '../polymer'; +import nuclearObserver from '../util/bound-nuclear-behavior'; + +export default Polymer({ + is: 'stream-status', + + behaviors: [nuclearObserver], + + properties: { + isStreaming: { + type: Boolean, + bindNuclear: streamGetters.isStreamingEvents, + }, + + hasError: { + type: Boolean, + bindNuclear: streamGetters.hasStreamingEventsError, + }, + }, + + toggleChanged: function() { + if (this.isStreaming) { + streamActions.stop(); + } else { + streamActions.start(); + } + }, +}); diff --git a/src/dialogs/more-info-dialog.html b/src/dialogs/more-info-dialog.html new file mode 100644 index 0000000000..e0d31c6898 --- /dev/null +++ b/src/dialogs/more-info-dialog.html @@ -0,0 +1,48 @@ + + + + + + + + + + + + + diff --git a/src/dialogs/more-info-dialog.js b/src/dialogs/more-info-dialog.js new file mode 100644 index 0000000000..bd18aa77f3 --- /dev/null +++ b/src/dialogs/more-info-dialog.js @@ -0,0 +1,105 @@ +import { + configGetters, + entityHistoryGetters, + entityHistoryActions, + moreInfoGetters, + moreInfoActions +} from 'home-assistant-js'; + +import Polymer from '../polymer'; +import nuclearObserver from '../util/bound-nuclear-behavior'; + +require('../cards/state-card-content'); +require('../components/state-history-charts'); +require('../more-infos/more-info-content'); + +// if you don't want the history component to show add the domain to this array +const DOMAINS_WITH_NO_HISTORY = ['camera']; + +export default Polymer({ + is: 'more-info-dialog', + + behaviors: [nuclearObserver], + + properties: { + stateObj: { + type: Object, + bindNuclear: moreInfoGetters.currentEntity, + observer: 'stateObjChanged', + }, + + stateHistory: { + type: Object, + bindNuclear: [ + moreInfoGetters.currentEntityHistory, + function(history) { + return history ? [history] : false; + }, + ], + }, + + isLoadingHistoryData: { + type: Boolean, + bindNuclear: entityHistoryGetters.isLoadingEntityHistory, + }, + + hasHistoryComponent: { + type: Boolean, + bindNuclear: configGetters.isComponentLoaded('history'), + observer: 'fetchHistoryData', + }, + + shouldFetchHistory: { + type: Boolean, + bindNuclear: moreInfoGetters.isCurrentEntityHistoryStale, + observer: 'fetchHistoryData', + }, + + showHistoryComponent: { + type: Boolean, + value: false, + }, + + dialogOpen: { + type: Boolean, + value: false, + observer: 'dialogOpenChanged', + }, + }, + + fetchHistoryData() { + if (this.stateObj && this.hasHistoryComponent && + this.shouldFetchHistory) { + entityHistoryActions.fetchRecent(this.stateObj.entityId); + } + if(this.stateObj) { + if(DOMAINS_WITH_NO_HISTORY.indexOf(this.stateObj.domain) !== -1) { + this.showHistoryComponent = false; + } + else { + this.showHistoryComponent = this.hasHistoryComponent; + } + } + }, + + stateObjChanged(newVal) { + if (!newVal) { + this.dialogOpen = false; + return; + } + + this.fetchHistoryData(); + + // allow dialog to render content before showing it so it is + // positioned correctly. + this.async(function() { + this.dialogOpen = true; + }.bind(this), 10); + }, + + dialogOpenChanged(newVal) { + if (!newVal) { + moreInfoActions.deselectEntity(); + } + }, +}); diff --git a/src/home-assistant.html b/src/home-assistant.html new file mode 100644 index 0000000000..07f22515bf --- /dev/null +++ b/src/home-assistant.html @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + diff --git a/src/home-assistant.js b/src/home-assistant.js new file mode 100644 index 0000000000..368a217404 --- /dev/null +++ b/src/home-assistant.js @@ -0,0 +1,46 @@ +import Polymer from './polymer'; + +import { + syncGetters, + localStoragePreferences +} from 'home-assistant-js'; + +import nuclearObserver from './util/bound-nuclear-behavior'; +import validateAuth from './util/validate-auth'; + +require('./layouts/login-form'); +require('./layouts/home-assistant-main'); + +export default Polymer({ + is: 'home-assistant', + + hostAttributes: { + auth: null, + }, + + behaviors: [nuclearObserver], + + properties: { + auth: { + type: String, + }, + loaded: { + type: Boolean, + bindNuclear: syncGetters.isDataLoaded, + }, + }, + + ready() { + // remove the HTML init message + document.getElementById('init').remove(); + + // if auth was given, tell the backend + if(this.auth) { + validateAuth(this.auth, false); + } else if (localStoragePreferences.authToken) { + validateAuth(localStoragePreferences.authToken, true); + } + + localStoragePreferences.startSync(); + }, +}); diff --git a/src/layouts/home-assistant-main.html b/src/layouts/home-assistant-main.html new file mode 100644 index 0000000000..0175b03bba --- /dev/null +++ b/src/layouts/home-assistant-main.html @@ -0,0 +1,46 @@ + + + + + + + + + + + + + + + + + + diff --git a/src/layouts/home-assistant-main.js b/src/layouts/home-assistant-main.js new file mode 100644 index 0000000000..a03fc2ccba --- /dev/null +++ b/src/layouts/home-assistant-main.js @@ -0,0 +1,89 @@ +import { + configGetters, + entityGetters, + navigationGetters, + authActions, + navigationActions, + urlSync, + util +} from 'home-assistant-js'; + +import nuclearObserver from '../util/bound-nuclear-behavior'; + +require('../components/ha-sidebar'); +require('../layouts/partial-states'); +require('../layouts/partial-logbook'); +require('../layouts/partial-history'); +require('../layouts/partial-dev-call-service'); +require('../layouts/partial-dev-fire-event'); +require('../layouts/partial-dev-set-state'); +require('../managers/notification-manager'); +require('../dialogs/more-info-dialog'); + +export default Polymer({ + is: 'home-assistant-main', + + behaviors: [nuclearObserver], + + properties: { + narrow: { + type: Boolean, + }, + + activePage: { + type: String, + bindNuclear: navigationGetters.activePage, + observer: 'activePageChanged', + }, + + isSelectedStates: { + type: Boolean, + bindNuclear: navigationGetters.isActivePane('states'), + }, + + isSelectedHistory: { + type: Boolean, + bindNuclear: navigationGetters.isActivePane('history'), + }, + + isSelectedLogbook: { + type: Boolean, + bindNuclear: navigationGetters.isActivePane('logbook'), + }, + + isSelectedDevEvent: { + type: Boolean, + bindNuclear: navigationGetters.isActivePane('devEvent'), + }, + + isSelectedDevState: { + type: Boolean, + bindNuclear: navigationGetters.isActivePane('devState'), + }, + + isSelectedDevService: { + type: Boolean, + bindNuclear: navigationGetters.isActivePane('devService'), + }, + }, + + listeners: { + 'open-menu': 'openDrawer', + }, + + openDrawer: function() { + this.$.drawer.openDrawer(); + }, + + activePageChanged: function() { + this.$.drawer.closeDrawer(); + }, + + attached: function() { + urlSync.startSync(); + }, + + detached: function() { + urlSync.stopSync(); + }, +}); diff --git a/src/layouts/login-form.html b/src/layouts/login-form.html new file mode 100644 index 0000000000..2fdc252419 --- /dev/null +++ b/src/layouts/login-form.html @@ -0,0 +1,83 @@ + + + + + + + + + + + + + + + diff --git a/src/layouts/login-form.js b/src/layouts/login-form.js new file mode 100644 index 0000000000..b8a8e62088 --- /dev/null +++ b/src/layouts/login-form.js @@ -0,0 +1,69 @@ +import Polymer from '../polymer'; + +import { authGetters } from 'home-assistant-js'; + +import nuclearObserver from '../util/bound-nuclear-behavior'; +import validateAuth from '../util/validate-auth'; + +export default Polymer({ + is: 'login-form', + + behaviors: [nuclearObserver], + + properties: { + isValidating: { + type: Boolean, + observer: 'isValidatingChanged', + bindNuclear: authGetters.isValidating, + }, + + isInvalid: { + type: Boolean, + bindNuclear: authGetters.isInvalidAttempt, + }, + + errorMessage: { + type: String, + bindNuclear: authGetters.attemptErrorMessage, + }, + }, + + listeners: { + 'keydown': 'passwordKeyDown', + 'loginButton.click': 'validatePassword', + }, + + observers: [ + 'validatingChanged(isValidating, isInvalid)', + ], + + validatingChanged: function(isValidating, isInvalid) { + if (!isValidating && !isInvalid) { + this.$.passwordInput.value = ''; + } + }, + + isValidatingChanged: function(newVal) { + if (!newVal) { + this.async(function() { this.$.passwordInput.focus(); }.bind(this), 10); + } + }, + + passwordKeyDown: function(ev) { + // validate on enter + if(ev.keyCode === 13) { + this.validatePassword(); + ev.preventDefault(); + + // clear error after we start typing again + } else if(this.isInvalid) { + this.isInvalid = false; + } + }, + + validatePassword: function() { + this.$.hideKeyboardOnFocus.focus(); + + validateAuth(this.$.passwordInput.value, this.$.rememberLogin.checked); + }, +}); diff --git a/src/layouts/partial-base.html b/src/layouts/partial-base.html new file mode 100644 index 0000000000..95de732421 --- /dev/null +++ b/src/layouts/partial-base.html @@ -0,0 +1,29 @@ + + + + + + + + + + + diff --git a/src/layouts/partial-base.js b/src/layouts/partial-base.js new file mode 100644 index 0000000000..fa785abcf1 --- /dev/null +++ b/src/layouts/partial-base.js @@ -0,0 +1,16 @@ +import Polymer from '../polymer'; + +export default Polymer({ + is: 'partial-base', + + properties: { + narrow: { + type: Boolean, + value: false, + }, + }, + + toggleMenu: function() { + this.fire('open-menu'); + }, +}); diff --git a/src/layouts/partial-dev-call-service.html b/src/layouts/partial-dev-call-service.html new file mode 100644 index 0000000000..9a91a3b8e5 --- /dev/null +++ b/src/layouts/partial-dev-call-service.html @@ -0,0 +1,47 @@ + + + + + + + + + + + + + + diff --git a/src/layouts/partial-dev-call-service.js b/src/layouts/partial-dev-call-service.js new file mode 100644 index 0000000000..f486e8062a --- /dev/null +++ b/src/layouts/partial-dev-call-service.js @@ -0,0 +1,54 @@ +import { serviceActions } from 'home-assistant-js'; + +import Polymer from '../polymer'; + +require('./partial-base'); +require('../components/services-list'); + +export default Polymer({ + is: 'partial-dev-call-service', + + properties: { + narrow: { + type: Boolean, + value: false, + }, + + domain: { + type: String, + value: '', + }, + + service: { + type: String, + value: '', + }, + + serviceData: { + type: String, + value: '', + }, + }, + + serviceSelected(ev) { + this.domain = ev.detail.domain; + this.service = ev.detail.service; + }, + + callService() { + var serviceData; + + try { + serviceData = this.serviceData ? JSON.parse(this.serviceData): {}; + } catch (err) { + alert("Error parsing JSON: " + err); + return; + } + + serviceActions.callService(this.domain, this.service, serviceData); + }, + + computeFormClasses(narrow) { + return 'layout ' + (narrow ? 'vertical' : 'horizontal'); + }, +}); diff --git a/src/layouts/partial-dev-fire-event.html b/src/layouts/partial-dev-fire-event.html new file mode 100644 index 0000000000..d5f3dadbfa --- /dev/null +++ b/src/layouts/partial-dev-fire-event.html @@ -0,0 +1,48 @@ + + + + + + + + + + + + + + + diff --git a/src/layouts/partial-dev-fire-event.js b/src/layouts/partial-dev-fire-event.js new file mode 100644 index 0000000000..159a8dce18 --- /dev/null +++ b/src/layouts/partial-dev-fire-event.js @@ -0,0 +1,43 @@ +import { eventActions } from 'home-assistant-js'; + +import Polymer from '../polymer'; + +require('./partial-base'); +require('../components/events-list'); + +export default Polymer({ + is: 'partial-dev-fire-event', + + properties: { + eventType: { + type: String, + value: '', + }, + + eventData: { + type: String, + value: '', + }, + }, + + eventSelected(ev) { + this.eventType = ev.detail.eventType; + }, + + fireEvent() { + var eventData; + + try { + eventData = this.eventData ? JSON.parse(this.eventData) : {}; + } catch (err) { + alert("Error parsing JSON: " + err); + return; + } + + eventActions.fireEvent(this.eventType, eventData); + }, + + computeFormClasses(narrow) { + return 'layout ' + (narrow ? 'vertical' : 'horizontal'); + }, +}); diff --git a/src/layouts/partial-dev-set-state.html b/src/layouts/partial-dev-set-state.html new file mode 100644 index 0000000000..c1a9e11fa4 --- /dev/null +++ b/src/layouts/partial-dev-set-state.html @@ -0,0 +1,49 @@ + + + + + + + + + + + + + + + diff --git a/src/layouts/partial-dev-set-state.js b/src/layouts/partial-dev-set-state.js new file mode 100644 index 0000000000..6270dcbf2d --- /dev/null +++ b/src/layouts/partial-dev-set-state.js @@ -0,0 +1,64 @@ +import { reactor, entityGetters, entityActions } from 'home-assistant-js'; + +import Polymer from '../polymer'; + +require('./partial-base'); +require('../components/entity-list'); + +export default Polymer({ + is: 'partial-dev-set-state', + + properties: { + entityId: { + type: String, + value: '', + }, + + state: { + type: String, + value: '', + }, + + stateAttributes: { + type: String, + value: '', + }, + }, + + setStateData(stateData) { + var value = stateData ? JSON.stringify(stateData, null, ' ') : ""; + + this.$.inputData.value = value; + + // not according to the spec but it works... + this.$.inputDataWrapper.update(this.$.inputData); + }, + + entitySelected(ev) { + var state = reactor.evaluate(entityGetters.byId(ev.detail.entityId)); + + this.entityId = state.entityId; + this.state = state.state; + this.stateAttributes = JSON.stringify(state.attributes, null, ' '); + }, + + handleSetState() { + var attr; + try { + attr = this.stateAttributes ? JSON.parse(this.stateAttributes) : {}; + } catch (err) { + alert("Error parsing JSON: " + err); + return; + } + + entityActions.save({ + entityId: this.entityId, + state: this.state, + attributes: attr, + }); + }, + + computeFormClasses(narrow) { + return 'layout ' + (narrow ? 'vertical' : 'horizontal'); + }, +}); diff --git a/src/layouts/partial-history.html b/src/layouts/partial-history.html new file mode 100644 index 0000000000..d5a251b012 --- /dev/null +++ b/src/layouts/partial-history.html @@ -0,0 +1,45 @@ + + + + + + + + + + + + + + diff --git a/src/layouts/partial-history.js b/src/layouts/partial-history.js new file mode 100644 index 0000000000..44b695ee63 --- /dev/null +++ b/src/layouts/partial-history.js @@ -0,0 +1,75 @@ +import { + uiActions, + entityHistoryGetters, + entityHistoryActions +} from 'home-assistant-js'; + +import Polymer from '../polymer'; +import nuclearObserver from '../util/bound-nuclear-behavior'; + +require('./partial-base'); +require('../components/state-history-charts'); + +export default Polymer({ + is: 'partial-history', + + behaviors: [nuclearObserver], + + properties: { + narrow: { + type: Boolean, + }, + + isDataLoaded: { + type: Boolean, + bindNuclear: entityHistoryGetters.hasDataForCurrentDate, + observer: 'isDataLoadedChanged', + }, + + stateHistory: { + type: Object, + bindNuclear: entityHistoryGetters.entityHistoryForCurrentDate, + }, + + isLoadingData: { + type: Boolean, + bindNuclear: entityHistoryGetters.isLoadingEntityHistory, + }, + + selectedDate: { + type: String, + value: null, + bindNuclear: entityHistoryGetters.currentDate, + }, + }, + + isDataLoadedChanged(newVal) { + if (!newVal) { + entityHistoryActions.fetchSelectedDate(); + } + }, + + handleRefreshClick() { + entityHistoryActions.fetchSelectedDate(); + }, + + datepickerFocus() { + this.datePicker.adjustPosition(); + this.datePicker.gotoDate(moment('2015-06-30').toDate()); + }, + + attached() { + this.datePicker = new Pikaday({ + field: this.$.datePicker.inputElement, + onSelect: entityHistoryActions.changeCurrentDate, + }); + }, + + detached() { + this.datePicker.destroy(); + }, + + computeContentClasses(narrow) { + return 'flex content ' + (narrow ? 'narrow' : 'wide'); + }, +}); \ No newline at end of file diff --git a/src/layouts/partial-logbook.html b/src/layouts/partial-logbook.html new file mode 100644 index 0000000000..c312574bab --- /dev/null +++ b/src/layouts/partial-logbook.html @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + + diff --git a/src/layouts/partial-logbook.js b/src/layouts/partial-logbook.js new file mode 100644 index 0000000000..01a1235520 --- /dev/null +++ b/src/layouts/partial-logbook.js @@ -0,0 +1,77 @@ +import { logbookGetters, logbookActions } from 'home-assistant-js'; + +import Polymer from '../polymer'; +import nuclearObserver from '../util/bound-nuclear-behavior'; + +require('./partial-base'); +require('../components/ha-logbook'); +require('../components/loading-box'); + +export default Polymer({ + is: 'partial-logbook', + + behaviors: [nuclearObserver], + + properties: { + narrow: { + type: Boolean, + value: false, + }, + + selectedDate: { + type: String, + bindNuclear: logbookGetters.currentDate, + }, + + isLoading: { + type: Boolean, + bindNuclear: logbookGetters.isLoadingEntries, + }, + + isStale: { + type: Boolean, + bindNuclear: logbookGetters.isCurrentStale, + observer: 'isStaleChanged', + }, + + entries: { + type: Array, + bindNuclear: [ + logbookGetters.currentEntries, + function(entries) { return entries.toArray(); }, + ], + }, + + datePicker: { + type: Object, + }, + }, + + isStaleChanged(newVal) { + if (newVal) { + // isLoading wouldn't update without async <_< + this.async( + function() { logbookActions.fetchDate(this.selectedDate); }, 10); + } + }, + + handleRefresh() { + logbookActions.fetchDate(this.selectedDate); + }, + + datepickerFocus() { + this.datePicker.adjustPosition(); + this.datePicker.gotoDate(moment('2015-06-30').toDate()); + }, + + attached() { + this.datePicker = new Pikaday({ + field: this.$.datePicker.inputElement, + onSelect: logbookActions.changeCurrentDate, + }); + }, + + detached() { + this.datePicker.destroy(); + }, +}); diff --git a/src/layouts/partial-states.html b/src/layouts/partial-states.html new file mode 100644 index 0000000000..d30894c1c4 --- /dev/null +++ b/src/layouts/partial-states.html @@ -0,0 +1,76 @@ + + + + + + + + + + + + + + diff --git a/src/layouts/partial-states.js b/src/layouts/partial-states.js new file mode 100644 index 0000000000..40d66bb52a --- /dev/null +++ b/src/layouts/partial-states.js @@ -0,0 +1,112 @@ +import { + configGetters, + navigationGetters, + voiceGetters, + streamGetters, + serviceGetters, + syncGetters, + syncActions, + voiceActions, + util +} from 'home-assistant-js'; + +import Polymer from '../polymer'; +import nuclearObserver from '../util/bound-nuclear-behavior'; + +const { entityDomainFilters } = util; + +require('./partial-base'); +require('../components/state-cards'); +require('../components/ha-voice-command-progress'); + +export default Polymer({ + is: 'partial-states', + + behaviors: [nuclearObserver], + + properties: { + narrow: { + type: Boolean, + value: false, + }, + + filter: { + type: String, + bindNuclear: navigationGetters.activeFilter, + }, + + isFetching: { + type: Boolean, + bindNuclear: syncGetters.isFetching, + }, + + isStreaming: { + type: Boolean, + bindNuclear: streamGetters.isStreamingEvents, + }, + + canListen: { + type: Boolean, + bindNuclear: [ + voiceGetters.isVoiceSupported, + configGetters.isComponentLoaded('conversation'), + function(isVoiceSupported, componentLoaded) { + return isVoiceSupported && componentLoaded; + } + ] + }, + + isListening: { + type: Boolean, + bindNuclear: voiceGetters.isListening, + }, + + showListenInterface: { + type: Boolean, + bindNuclear: [ + voiceGetters.isListening, + voiceGetters.isTransmitting, + function(isListening, isTransmitting) { + return isListening || isTransmitting; + }, + ], + }, + + states: { + type: Array, + bindNuclear: [ + navigationGetters.filteredStates, + // are here so a change to services causes a re-render. + // we need this to decide if we show toggles for states. + serviceGetters.entityMap, + function(states) { return states.toArray(); }, + ], + }, + }, + + handleRefresh() { + syncActions.fetchAll(); + }, + + handleListenClick() { + if (this.isListening) { + voiceActions.stop(); + } else { + voiceActions.listen(); + } + }, + + computeHeaderTitle(filter) { + return filter ? entityDomainFilters[filter] : 'States'; + }, + + computeListenButtonIcon(isListening) { + return isListening ? 'av:mic-off' : 'av:mic'; + }, + + computeRefreshButtonClass(isFetching) { + if (isFetching) { + return 'ha-spin'; + } + }, +}); diff --git a/src/managers/notification-manager.html b/src/managers/notification-manager.html new file mode 100644 index 0000000000..3d09a58d23 --- /dev/null +++ b/src/managers/notification-manager.html @@ -0,0 +1,14 @@ + + + + + + + + diff --git a/src/managers/notification-manager.js b/src/managers/notification-manager.js new file mode 100644 index 0000000000..d89b0cdb42 --- /dev/null +++ b/src/managers/notification-manager.js @@ -0,0 +1,24 @@ +import { notificationGetters } from 'home-assistant-js'; + +import Polymer from '../polymer'; +import nuclearObserver from '../util/bound-nuclear-behavior'; + +export default Polymer({ + is: 'notification-manager', + + behaviors: [nuclearObserver], + + properties: { + text: { + type: String, + bindNuclear: notificationGetters.lastNotificationMessage, + observer: 'showNotification', + }, + }, + + showNotification(newText) { + if (newText) { + this.$.toast.show(); + } + } +}); diff --git a/src/more-infos/more-info-camera.html b/src/more-infos/more-info-camera.html new file mode 100644 index 0000000000..97496abff9 --- /dev/null +++ b/src/more-infos/more-info-camera.html @@ -0,0 +1,18 @@ + + + + + + + diff --git a/src/more-infos/more-info-camera.js b/src/more-infos/more-info-camera.js new file mode 100644 index 0000000000..ce0951ca23 --- /dev/null +++ b/src/more-infos/more-info-camera.js @@ -0,0 +1,29 @@ +import Polymer from '../polymer'; + +export default Polymer({ + is: 'more-info-camera', + + properties: { + stateObj: { + type: Object, + }, + dialogOpen: { + type: Boolean, + }, + }, + + imageLoaded() { + this.fire('iron-resize'); + }, + + computeCameraImageUrl(dialogOpen) { + if (__DEMO__) { + return 'http://194.218.96.92/jpg/image.jpg'; + } else if (dialogOpen) { + return '/api/camera_proxy_stream/' + this.stateObj.entityId; + } else { + // Return an empty image if dialog is not open + return 'data:image/gif;base64,R0lGODlhAQABAAAAACw='; + } + }, +}); diff --git a/src/more-infos/more-info-configurator.html b/src/more-infos/more-info-configurator.html new file mode 100644 index 0000000000..9e6165ff21 --- /dev/null +++ b/src/more-infos/more-info-configurator.html @@ -0,0 +1,51 @@ + + + + + + + + + diff --git a/src/more-infos/more-info-configurator.js b/src/more-infos/more-info-configurator.js new file mode 100644 index 0000000000..ad674b356b --- /dev/null +++ b/src/more-infos/more-info-configurator.js @@ -0,0 +1,76 @@ +import { + streamGetters, + syncActions, + serviceActions +} from 'home-assistant-js'; + +import Polymer from '../polymer'; +import nuclearObserver from '../util/bound-nuclear-behavior'; + +require('../components/loading-box'); + +export default Polymer({ + is: 'more-info-configurator', + + behaviors: [nuclearObserver], + + properties: { + stateObj: { + type: Object, + }, + + action: { + type: String, + value: 'display', + }, + + isStreaming: { + type: Boolean, + bindNuclear: streamGetters.isStreamingEvents, + }, + + isConfigurable: { + type: Boolean, + computed: 'computeIsConfigurable(stateObj)', + }, + + isConfiguring: { + type: Boolean, + value: false, + }, + + submitCaption: { + type: String, + computed: 'computeSubmitCaption(stateObj)', + }, + }, + + computeIsConfigurable(stateObj) { + return stateObj.state == 'configure'; + }, + + computeSubmitCaption(stateObj) { + return stateObj.attributes.submit_caption || 'Set configuration'; + }, + + submitClicked() { + this.isConfiguring = true; + + var data = { + configure_id: this.stateObj.attributes.configure_id + }; + + serviceActions.callService('configurator', 'configure', data).then( + function() { + this.isConfiguring = false; + + if (!this.isStreaming) { + syncActions.fetchAll(); + } + }.bind(this), + + function() { + this.isConfiguring = false; + }.bind(this)); + }, +}); diff --git a/src/more-infos/more-info-content.html b/src/more-infos/more-info-content.html new file mode 100644 index 0000000000..1c9dc8b2fe --- /dev/null +++ b/src/more-infos/more-info-content.html @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + diff --git a/src/more-infos/more-info-content.js b/src/more-infos/more-info-content.js new file mode 100644 index 0000000000..b4da8a73f2 --- /dev/null +++ b/src/more-infos/more-info-content.js @@ -0,0 +1,68 @@ +import Polymer from '../polymer'; +import stateMoreInfoType from '../util/state-more-info-type'; + +require('./more-info-default'); +require('./more-info-group'); +require('./more-info-sun'); +require('./more-info-configurator'); +require('./more-info-thermostat'); +require('./more-info-script'); +require('./more-info-light'); +require('./more-info-media_player'); +require('./more-info-camera'); + +export default Polymer({ + is: 'more-info-content', + + properties: { + stateObj: { + type: Object, + observer: 'stateObjChanged', + }, + + dialogOpen: { + type: Boolean, + value: false, + observer: 'dialogOpenChanged', + }, + }, + + dialogOpenChanged(newVal, oldVal) { + var root = Polymer.dom(this); + + if (root.lastChild) { + root.lastChild.dialogOpen = newVal; + } + }, + + stateObjChanged(newVal, oldVal) { + var root = Polymer.dom(this); + + if (!newVal) { + if (root.lastChild) { + root.removeChild(root.lastChild); + } + return; + } + + var newMoreInfoType = stateMoreInfoType(newVal); + + if (!oldVal || stateMoreInfoType(oldVal) != newMoreInfoType) { + + if (root.lastChild) { + root.removeChild(root.lastChild); + } + + var moreInfo = document.createElement('more-info-' + newMoreInfoType); + moreInfo.stateObj = newVal; + moreInfo.dialogOpen = this.dialogOpen; + root.appendChild(moreInfo); + + } else { + + root.lastChild.dialogOpen = this.dialogOpen; + root.lastChild.stateObj = newVal; + + } + }, +}); diff --git a/src/more-infos/more-info-default.html b/src/more-infos/more-info-default.html new file mode 100644 index 0000000000..54269409e8 --- /dev/null +++ b/src/more-infos/more-info-default.html @@ -0,0 +1,19 @@ + + + + + + diff --git a/src/more-infos/more-info-default.js b/src/more-infos/more-info-default.js new file mode 100644 index 0000000000..a2c4ebf086 --- /dev/null +++ b/src/more-infos/more-info-default.js @@ -0,0 +1,27 @@ +import Polymer from '../polymer'; + +const FILTER_KEYS = ['entity_picture', 'friendly_name', 'unit_of_measurement']; + +export default Polymer({ + is: 'more-info-default', + + properties: { + stateObj: { + type: Object, + }, + }, + + computeDisplayAttributes(stateObj) { + if (!stateObj) { + return []; + } + + return Object.keys(stateObj.attributes).filter(function(key) { + return FILTER_KEYS.indexOf(key) === -1; + }); + }, + + getAttributeValue(stateObj, attribute) { + return stateObj.attributes[attribute]; + }, +}); diff --git a/src/more-infos/more-info-group.html b/src/more-infos/more-info-group.html new file mode 100644 index 0000000000..aee8974235 --- /dev/null +++ b/src/more-infos/more-info-group.html @@ -0,0 +1,22 @@ + + + + + + + + diff --git a/src/more-infos/more-info-group.js b/src/more-infos/more-info-group.js new file mode 100644 index 0000000000..72cc7f3ce2 --- /dev/null +++ b/src/more-infos/more-info-group.js @@ -0,0 +1,43 @@ +import { + entityGetters, + moreInfoGetters +} from 'home-assistant-js'; + +import Polymer from '../polymer'; +import nuclearObserver from '../util/bound-nuclear-behavior'; + +require('../cards/state-card-content'); + +export default Polymer({ + is: 'more-info-group', + + behaviors: [nuclearObserver], + + properties: { + stateObj: { + type: Object, + }, + + states: { + type: Array, + bindNuclear: [ + moreInfoGetters.currentEntity, + entityGetters.entityMap, + function(currentEntity, entities) { + // weird bug?? + if (!currentEntity) { + return; + } + return currentEntity.attributes.entity_id.map( + entities.get.bind(entities)); + }, + ], + }, + }, + + + updateStates() { + this.states = this.stateObj && this.stateObj.attributes.entity_id ? + stateStore.gets(this.stateObj.attributes.entity_id).toArray() : []; + }, +}); diff --git a/src/more-infos/more-info-light.html b/src/more-infos/more-info-light.html new file mode 100644 index 0000000000..123eeb5c3f --- /dev/null +++ b/src/more-infos/more-info-light.html @@ -0,0 +1,48 @@ + + + + + + + + + diff --git a/src/more-infos/more-info-light.js b/src/more-infos/more-info-light.js new file mode 100644 index 0000000000..a1aa846d3e --- /dev/null +++ b/src/more-infos/more-info-light.js @@ -0,0 +1,63 @@ +import { serviceActions } from 'home-assistant-js'; + +import Polymer from '../polymer'; +import attributeClassNames from '../util/attribute-class-names'; + +require('../components/ha-color-picker'); + +const ATTRIBUTE_CLASSES = ['brightness', 'xy_color']; + +export default Polymer({ + is: 'more-info-light', + + properties: { + stateObj: { + type: Object, + observer: 'stateObjChanged', + }, + + brightnessSliderValue: { + type: Number, + value: 0, + } + }, + + stateObjChanged(newVal, oldVal) { + if (newVal && newVal.state === 'on') { + this.brightnessSliderValue = newVal.attributes.brightness; + } + + this.async(function() { + this.fire('iron-resize'); + }.bind(this), 500); + }, + + computeClassNames(stateObj) { + return attributeClassNames(stateObj, ATTRIBUTE_CLASSES); + }, + + brightnessSliderChanged(ev) { + var bri = parseInt(ev.target.value); + + if(isNaN(bri)) return; + + if(bri === 0) { + serviceActions.callTurnOff(this.stateObj.entityId); + } else { + serviceActions.callService('light', 'turn_on', { + entity_id: this.stateObj.entityId, + brightness: bri + }); + } + }, + + colorPicked(ev) { + var color = ev.detail.rgb; + + serviceActions.callService('light', 'turn_on', { + entity_id: this.stateObj.entityId, + rgb_color: [color.r, color.g, color.b] + }); + } + +}); diff --git a/src/more-infos/more-info-media_player.html b/src/more-infos/more-info-media_player.html new file mode 100644 index 0000000000..e6e12a9841 --- /dev/null +++ b/src/more-infos/more-info-media_player.html @@ -0,0 +1,56 @@ + + + + + + + + diff --git a/src/more-infos/more-info-media_player.js b/src/more-infos/more-info-media_player.js new file mode 100644 index 0000000000..0bf5a414cc --- /dev/null +++ b/src/more-infos/more-info-media_player.js @@ -0,0 +1,152 @@ +import { serviceActions } from 'home-assistant-js'; + +import Polymer from '../polymer'; +import attributeClassNames from '../util/attribute-class-names'; + +const ATTRIBUTE_CLASSES = ['volume_level']; + +export default Polymer({ + is: 'more-info-media_player', + + properties: { + stateObj: { + type: Object, + observer: 'stateObjChanged', + }, + + isOff: { + type: Boolean, + value: false, + }, + + isPlaying: { + type: Boolean, + value: false, + }, + + isMuted: { + type: Boolean, + value: false + }, + + volumeSliderValue: { + type: Number, + value: 0, + }, + + supportsPause: { + type: Boolean, + value: false, + }, + + supportsVolumeSet: { + type: Boolean, + value: false, + }, + + supportsVolumeMute: { + type: Boolean, + value: false, + }, + + supportsPreviousTrack: { + type: Boolean, + value: false, + }, + + supportsNextTrack: { + type: Boolean, + value: false, + }, + + supportsTurnOn: { + type: Boolean, + value: false, + }, + + supportsTurnOff: { + type: Boolean, + value: false, + }, + + }, + + stateObjChanged(newVal) { + if (newVal) { + this.isOff = newVal.state == 'off'; + this.isPlaying = newVal.state == 'playing'; + this.volumeSliderValue = newVal.attributes.volume_level * 100; + this.isMuted = newVal.attributes.is_volume_muted; + this.supportsPause = (newVal.attributes.supported_media_commands & 1) !== 0; + this.supportsVolumeSet = (newVal.attributes.supported_media_commands & 4) !== 0; + this.supportsVolumeMute = (newVal.attributes.supported_media_commands & 8) !== 0; + this.supportsPreviousTrack = (newVal.attributes.supported_media_commands & 16) !== 0; + this.supportsNextTrack = (newVal.attributes.supported_media_commands & 32) !== 0; + this.supportsTurnOn = (newVal.attributes.supported_media_commands & 128) !== 0; + this.supportsTurnOff = (newVal.attributes.supported_media_commands & 256) !== 0; + } + + this.async(function() { this.fire('iron-resize'); }.bind(this), 500); + }, + + computeClassNames(stateObj) { + return attributeClassNames(stateObj, ATTRIBUTE_CLASSES); + }, + + computeIsOff(stateObj) { + return stateObj.state == 'off'; + }, + + computeMuteVolumeIcon(isMuted) { + return isMuted ? 'av:volume-off' : 'av:volume-up'; + }, + + computePlaybackControlIcon(stateObj) { + if (this.isPlaying) { + return this.supportsPause ? 'av:pause' : 'av:stop'; + } + return 'av:play-arrow'; + }, + + computeHidePowerButton(isOff, supportsTurnOn, supportsTurnOff) { + return isOff ? !supportsTurnOn : !supportsTurnOff; + }, + + handleTogglePower() { + this.callService(this.isOff ? 'turn_on' : 'turn_off'); + }, + + handlePrevious() { + this.callService('media_previous_track'); + }, + + handlePlaybackControl() { + if (this.isPlaying && !this.supportsPause) { + alert('This case is not supported yet'); + } + this.callService('media_play_pause'); + }, + + handleNext() { + this.callService('media_next_track'); + }, + + handleVolumeTap() { + if (!this.supportsVolumeMute) { + return; + } + this.callService('volume_mute', { is_volume_muted: !this.isMuted }); + }, + + volumeSliderChanged(ev) { + var volPercentage = parseFloat(ev.target.value); + var vol = volPercentage > 0 ? volPercentage / 100 : 0; + this.callService('volume_set', { volume_level: vol }); + }, + + callService(service, data) { + data = data || {}; + data.entity_id = this.stateObj.entityId; + serviceActions.callService('media_player', service, data); + }, +}); diff --git a/src/more-infos/more-info-script.html b/src/more-infos/more-info-script.html new file mode 100644 index 0000000000..9310d4b2ae --- /dev/null +++ b/src/more-infos/more-info-script.html @@ -0,0 +1,12 @@ + + + + + diff --git a/src/more-infos/more-info-script.js b/src/more-infos/more-info-script.js new file mode 100644 index 0000000000..8359d7270e --- /dev/null +++ b/src/more-infos/more-info-script.js @@ -0,0 +1,11 @@ +import Polymer from '../polymer'; + +export default Polymer({ + is: 'more-info-script', + + properties: { + stateObj: { + type: Object, + }, + }, +}); diff --git a/src/more-infos/more-info-sun.html b/src/more-infos/more-info-sun.html new file mode 100644 index 0000000000..dae4f817da --- /dev/null +++ b/src/more-infos/more-info-sun.html @@ -0,0 +1,21 @@ + + + + + + + diff --git a/src/more-infos/more-info-sun.js b/src/more-infos/more-info-sun.js new file mode 100644 index 0000000000..a37347d4bd --- /dev/null +++ b/src/more-infos/more-info-sun.js @@ -0,0 +1,48 @@ +import { util } from 'home-assistant-js'; + +import formatTime from '../util/format-time'; + +const { parseDateTime } = util; + +export default Polymer({ + is: 'more-info-sun', + + properties: { + stateObj: { + type: Object, + observer: 'stateObjChanged', + }, + + risingDate: { + type: Object, + }, + + settingDate: { + type: Object, + }, + + risingTime: { + type: String, + }, + + settingTime: { + type: String, + }, + }, + + stateObjChanged() { + this.risingDate = parseDateTime(this.stateObj.attributes.next_rising); + this.risingTime = formatTime(this.risingDate); + + this.settingDate = parseDateTime(this.stateObj.attributes.next_setting); + this.settingTime = formatTime(this.settingDate); + + var root = Polymer.dom(this); + + if(self.risingDate > self.settingDate) { + root.appendChild(this.$.rising); + } else { + root.appendChild(this.$.setting); + } + } +}); diff --git a/src/more-infos/more-info-thermostat.html b/src/more-infos/more-info-thermostat.html new file mode 100644 index 0000000000..21da6548e5 --- /dev/null +++ b/src/more-infos/more-info-thermostat.html @@ -0,0 +1,40 @@ + + + + + + + + diff --git a/src/more-infos/more-info-thermostat.js b/src/more-infos/more-info-thermostat.js new file mode 100644 index 0000000000..235f84ded0 --- /dev/null +++ b/src/more-infos/more-info-thermostat.js @@ -0,0 +1,87 @@ +import { util, serviceActions } from 'home-assistant-js'; + +import Polymer from '../polymer'; +import attributeClassNames from '../util/attribute-class-names'; + +const { temperatureUnits } = util; +const ATTRIBUTE_CLASSES = ['away_mode']; + +export default Polymer({ + is: 'more-info-thermostat', + + properties: { + stateObj: { + type: Object, + observer: 'stateObjChanged', + }, + + tempMin: { + type: Number, + }, + + tempMax: { + type: Number, + }, + + targetTemperatureSliderValue: { + type: Number, + }, + + awayToggleChecked: { + type: Boolean, + }, + }, + + stateObjChanged(newVal, oldVal) { + this.targetTemperatureSliderValue = this.stateObj.state; + this.awayToggleChecked = this.stateObj.attributes.away_mode == 'on'; + + if (this.stateObj.attributes.unit_of_measurement === + temperatureUnits.UNIT_TEMP_F) { + this.tempMin = 45; + this.tempMax = 95; + } else { + this.tempMin = 7; + this.tempMax = 35; + } + }, + + computeClassNames(stateObj) { + return attributeClassNames(stateObj, ATTRIBUTE_CLASSES); + }, + + targetTemperatureSliderChanged(ev) { + var temp = parseInt(ev.target.value); + + if(isNaN(temp)) return; + + serviceActions.callService('thermostat', 'set_temperature', { + entity_id: this.stateObj.entityId, + temperature: temp + }); + }, + + toggleChanged(ev) { + var newVal = ev.target.checked; + + if(newVal && this.stateObj.attributes.away_mode === 'off') { + this.service_set_away(true); + } else if(!newVal && this.stateObj.attributes.away_mode === 'on') { + this.service_set_away(false); + } + }, + + service_set_away(away_mode) { + // We call stateChanged after a successful call to re-sync the toggle + // with the state. It will be out of sync if our service call did not + // result in the entity to be turned on. Since the state is not changing, + // the resync is not called automatic. + serviceActions.callService( + 'thermostat', 'set_away_mode', + {entity_id: this.stateObj.entityId, away_mode: away_mode}) + + .then(function() { + this.stateObjChanged(this.stateObj); + }.bind(this)); + }, +}); diff --git a/src/polymer.js b/src/polymer.js new file mode 100644 index 0000000000..858ee90e4f --- /dev/null +++ b/src/polymer.js @@ -0,0 +1 @@ +export default window.Polymer; diff --git a/src/resources/home-assistant-icons.html b/src/resources/home-assistant-icons.html new file mode 100644 index 0000000000..71d000b97d --- /dev/null +++ b/src/resources/home-assistant-icons.html @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/src/resources/home-assistant-style.html b/src/resources/home-assistant-style.html new file mode 100644 index 0000000000..cb7685568e --- /dev/null +++ b/src/resources/home-assistant-style.html @@ -0,0 +1,48 @@ + + + diff --git a/src/resources/pikaday-js.html b/src/resources/pikaday-js.html new file mode 100644 index 0000000000..258ba8631c --- /dev/null +++ b/src/resources/pikaday-js.html @@ -0,0 +1,2 @@ + + diff --git a/src/util/attribute-class-names.js b/src/util/attribute-class-names.js new file mode 100644 index 0000000000..ef9456be78 --- /dev/null +++ b/src/util/attribute-class-names.js @@ -0,0 +1,6 @@ +export default function attributeClassNames(stateObj, attributes) { + if (!stateObj) return ''; + return attributes.map(function(attribute) { + return attribute in stateObj.attributes ? 'has-' + attribute : ''; + }).join(' '); +} diff --git a/src/util/bound-nuclear-behavior.js b/src/util/bound-nuclear-behavior.js new file mode 100644 index 0000000000..454aeae809 --- /dev/null +++ b/src/util/bound-nuclear-behavior.js @@ -0,0 +1,5 @@ +import { reactor } from 'home-assistant-js'; + +import NuclearObserver from './nuclear-behavior'; + +export default NuclearObserver(reactor); diff --git a/src/util/domain-icon.js b/src/util/domain-icon.js new file mode 100644 index 0000000000..312ce1b500 --- /dev/null +++ b/src/util/domain-icon.js @@ -0,0 +1,57 @@ +export default function domainIcon(domain, state) { + switch(domain) { + case "homeassistant": + return "home"; + + case "group": + return "homeassistant-24:group"; + + case "device_tracker": + return "social:person"; + + case "switch": + return "image:flash-on"; + + case "media_player": + var icon = "hardware:cast"; + + if (state && state !== "off" && state !== 'idle') { + icon += "-connected"; + } + + return icon; + + case "sun": + return "image:wb-sunny"; + + case "light": + return "image:wb-incandescent"; + + case "simple_alarm": + return "social:notifications"; + + case "notify": + return "announcement"; + + case "thermostat": + return "homeassistant-100:thermostat"; + + case "sensor": + return "visibility"; + + case "configurator": + return "settings"; + + case "conversation": + return "av:hearing"; + + case "script": + return "description"; + + case 'scene': + return 'social:pages'; + + default: + return "bookmark"; + } +}; diff --git a/src/util/format-date-time.js b/src/util/format-date-time.js new file mode 100644 index 0000000000..6748bb835c --- /dev/null +++ b/src/util/format-date-time.js @@ -0,0 +1,5 @@ +import moment from 'moment'; + +export default function formatDateTime(dateObj) { + return moment(dateObj).format('lll'); +}; diff --git a/src/util/format-date.js b/src/util/format-date.js new file mode 100644 index 0000000000..8f2a68c795 --- /dev/null +++ b/src/util/format-date.js @@ -0,0 +1,5 @@ +import moment from 'moment'; + +export default function formatDate(dateObj) { + return moment(dateObj).format('ll'); +}; \ No newline at end of file diff --git a/src/util/format-time.js b/src/util/format-time.js new file mode 100644 index 0000000000..af6b9cf551 --- /dev/null +++ b/src/util/format-time.js @@ -0,0 +1,5 @@ +import moment from 'moment'; + +export default function formatTime(dateObj) { + return moment(dateObj).format('LT'); +}; diff --git a/src/util/nuclear-behavior.js b/src/util/nuclear-behavior.js new file mode 100644 index 0000000000..537cfee8fa --- /dev/null +++ b/src/util/nuclear-behavior.js @@ -0,0 +1,35 @@ +export default function NuclearObserver(reactor) { + return { + + attached: function() { + var component = this; + this.__unwatchFns = Object.keys(component.properties).reduce( + function(unwatchFns, key) { + if (!('bindNuclear' in component.properties[key])) { + return unwatchFns; + } + var getter = component.properties[key].bindNuclear; + + if (!getter) { + throw 'Undefined getter specified for key ' + key; + } + + component[key] = reactor.evaluate(getter); + + return unwatchFns.concat(reactor.observe(getter, function(val) { + if (__DEV__) { + console.log(component, key, val); + } + component[key] = val; + })); + }, []); + }, + + detached: function() { + while (this.__unwatchFns.length) { + this.__unwatchFns.shift()(); + } + }, + + }; +}; diff --git a/src/util/state-card-type.js b/src/util/state-card-type.js new file mode 100644 index 0000000000..e6c3f268cb --- /dev/null +++ b/src/util/state-card-type.js @@ -0,0 +1,14 @@ +import { reactor, serviceGetters } from 'home-assistant-js'; + +const DOMAINS_WITH_CARD = [ + 'thermostat', 'configurator', 'scene', 'media_player']; + +export default function stateCardType(state) { + if(DOMAINS_WITH_CARD.indexOf(state.domain) !== -1) { + return state.domain; + } else if(reactor.evaluate(serviceGetters.canToggle(state.entityId))) { + return "toggle"; + } else { + return "display"; + } +} diff --git a/src/util/state-more-info-type.js b/src/util/state-more-info-type.js new file mode 100644 index 0000000000..6cf865e02a --- /dev/null +++ b/src/util/state-more-info-type.js @@ -0,0 +1,11 @@ +const DOMAINS_WITH_MORE_INFO = [ + 'light', 'group', 'sun', 'configurator', 'thermostat', 'script', 'media_player', 'camera' +]; + +export default function stateMoreInfoType(state) { + if(DOMAINS_WITH_MORE_INFO.indexOf(state.domain) !== -1) { + return state.domain; + } else { + return 'default'; + } +} diff --git a/src/util/validate-auth.js b/src/util/validate-auth.js new file mode 100644 index 0000000000..ca19a73b94 --- /dev/null +++ b/src/util/validate-auth.js @@ -0,0 +1,8 @@ +import { authActions, localStoragePreferences } from 'home-assistant-js'; + +export default function(authToken, rememberAuth) { + authActions.validate(authToken, { + rememberAuth, + useStreaming: localStoragePreferences.useStreaming, + }); +} diff --git a/src/util/xybri-to-rgb.js b/src/util/xybri-to-rgb.js new file mode 100644 index 0000000000..92baba082f --- /dev/null +++ b/src/util/xybri-to-rgb.js @@ -0,0 +1,21 @@ +// from http://stackoverflow.com/questions/22894498/philips-hue-convert-xy-from-api-to-hex-or-rgb +export default function xyBriToRgb(x, y, bri) { + const z = 1.0 - x - y; + const Y = bri / 255.0; // Brightness of lamp + const X = (Y / y) * x; + const Z = (Y / y) * z; + let r = X * 1.612 - Y * 0.203 - Z * 0.302; + let g = -X * 0.509 + Y * 1.412 + Z * 0.066; + let b = X * 0.026 - Y * 0.072 + Z * 0.962; + r = r <= 0.0031308 ? 12.92 * r : (1.0 + 0.055) * Math.pow(r, (1.0 / 2.4)) - 0.055; + g = g <= 0.0031308 ? 12.92 * g : (1.0 + 0.055) * Math.pow(g, (1.0 / 2.4)) - 0.055; + b = b <= 0.0031308 ? 12.92 * b : (1.0 + 0.055) * Math.pow(b, (1.0 / 2.4)) - 0.055; + const maxValue = Math.max(r, g, b); + r /= maxValue; + g /= maxValue; + b /= maxValue; + r = r * 255; if (r < 0) { r = 255; } + g = g * 255; if (g < 0) { g = 255; } + b = b * 255; if (b < 0) { b = 255; } + return [r, g, b]; +} diff --git a/webpack.config.js b/webpack.config.js new file mode 100644 index 0000000000..76c9b25508 --- /dev/null +++ b/webpack.config.js @@ -0,0 +1,29 @@ +'use strict'; + +var webpack = require("webpack"); + +var definePlugin = new webpack.DefinePlugin({ + __DEV__: JSON.stringify(JSON.parse(process.env.BUILD_DEV || 'true')), + __DEMO__: JSON.stringify(JSON.parse(process.env.BUILD_DEMO || 'false')), +}); + +module.exports = { + entry: "./src/home-assistant.js", + output: { + path: 'build', + filename: "_app_compiled.js" + }, + module: { + loaders: [ + { + loader: "babel-loader", + test: /.js$/, + exclude: /node_modules\/(^home-assistant-js)/ + } + ] + }, + plugins: [ + definePlugin, + new webpack.ContextReplacementPlugin(/moment[\/\\]locale$/, /no-other-locales-for-now/) + ] +};