diff --git a/src/dialogs/dialog-manager.js b/src/dialogs/dialog-manager.js
deleted file mode 100644
index e26f0ddea7..0000000000
--- a/src/dialogs/dialog-manager.js
+++ /dev/null
@@ -1,23 +0,0 @@
-// Allows registering dialogs and makes sure they are appended to the root element.
-export default (root) => {
- root.addEventListener('register-dialog', (regEv) => {
- let loaded = null;
-
- const {
- dialogShowEvent,
- dialogTag,
- dialogImport,
- } = regEv.detail;
-
- root.addEventListener(dialogShowEvent, (showEv) => {
- if (!loaded) {
- loaded = dialogImport().then(() => {
- const dialogEl = document.createElement(dialogTag);
- root.shadowRoot.appendChild(dialogEl);
- return dialogEl;
- });
- }
- loaded.then(dialogEl => dialogEl.showDialog(showEv.detail));
- });
- });
-};
diff --git a/src/entrypoints/app.js b/src/entrypoints/app.js
index a8de26ce7a..4cf50a382a 100644
--- a/src/entrypoints/app.js
+++ b/src/entrypoints/app.js
@@ -1,439 +1,24 @@
-/* eslint-disable import/first */
// Load polyfill first so HTML imports start resolving
+/* eslint-disable import/first */
import '../resources/html-import/polyfill.js';
import '@polymer/app-route/app-location.js';
import '@polymer/app-route/app-route.js';
import '@polymer/iron-flex-layout/iron-flex-layout-classes.js';
import '@polymer/paper-styles/typography.js';
-import { html } from '@polymer/polymer/lib/utils/html-tag.js';
import { setPassiveTouchGestures } from '@polymer/polymer/lib/utils/settings.js';
-import { PolymerElement } from '@polymer/polymer/polymer-element.js';
-import { afterNextRender } from '@polymer/polymer/lib/utils/render-status.js';
-import LocalizeMixin from '../mixins/localize-mixin.js';
-
-import {
- ERR_INVALID_AUTH,
- subscribeEntities,
- subscribeConfig,
-} from 'home-assistant-js-websocket';
-
-import translationMetadata from '../../build-translations/translationMetadata.json';
-import '../layouts/home-assistant-main.js';
-import '../resources/ha-style.js';
-import '../util/ha-pref-storage.js';
-import { getActiveTranslation, getTranslation } from '../util/hass-translation.js';
import '../util/legacy-support';
import '../resources/roboto.js';
-import hassCallApi from '../util/hass-call-api.js';
-import makeDialogManager from '../dialogs/dialog-manager.js';
-import registerServiceWorker from '../util/register-service-worker.js';
-import computeStateName from '../common/entity/compute_state_name.js';
-import applyThemesOnElement from '../common/dom/apply_themes_on_element.js';
// For MDI icons. Needs to be part of main bundle or else it won't hook
// properly into iron-meta, which is used to transfer iconsets to iron-icon.
import '../components/ha-iconset-svg.js';
+import '../layouts/app/home-assistant.js';
+
/* polyfill for paper-dropdown */
import(/* webpackChunkName: "polyfill-web-animations-next" */ 'web-animations-js/web-animations-next-lite.min.js');
-import(/* webpackChunkName: "login-form" */ '../layouts/login-form.js');
-import(/* webpackChunkName: "notification-manager" */ '../managers/notification-manager.js');
-
setPassiveTouchGestures(true);
/* LastPass createElement workaround. See #428 */
document.createElement = Document.prototype.createElement;
-
-class HomeAssistant extends LocalizeMixin(PolymerElement) {
- static get template() {
- return html`
-
-
-
-
-
-
-
-
-
-
-
-
-`;
- }
-
- static get properties() {
- return {
- connectionPromise: {
- type: Object,
- value: window.hassConnection || null,
- observer: 'handleConnectionPromise',
- },
- connection: {
- type: Object,
- value: null,
- observer: 'connectionChanged',
- },
- hass: {
- type: Object,
- value: null,
- },
- showMain: {
- type: Boolean,
- computed: 'computeShowMain(hass)',
- },
- route: Object,
- routeData: Object,
- panelUrl: {
- type: String,
- computed: 'computePanelUrl(routeData)',
- observer: 'panelUrlChanged',
- },
- };
- }
-
- constructor() {
- super();
- makeDialogManager(this);
- }
-
- ready() {
- super.ready();
- this.addEventListener('settheme', e => this.setTheme(e));
- this.addEventListener('hass-language-select', e => this.selectLanguage(e));
- this.loadResources();
- afterNextRender(null, registerServiceWorker);
- }
-
- computeShowMain(hass) {
- return hass && hass.states && hass.config && hass.panels;
- }
-
- computeShowLoading(connectionPromise, hass) {
- // Show loading when connecting or when connected but not all pieces loaded yet
- return (connectionPromise != null
- || (hass && hass.connection && (!hass.states || !hass.config)));
- }
-
- async loadResources(fragment) {
- const result = await getTranslation(fragment);
- this._updateResources(result.language, result.data);
- }
-
- async loadBackendTranslations() {
- if (!this.hass.language) return;
-
- const language = this.hass.selectedLanguage || this.hass.language;
-
- const { resources } = await this.hass.callWS({
- type: 'frontend/get_translations',
- language,
- });
-
- // If we've switched selected languages just ignore this response
- if ((this.hass.selectedLanguage || this.hass.language) !== language) return;
-
- this._updateResources(language, resources);
- }
-
- _updateResources(language, data) {
- // Update the language in hass, and update the resources with the newly
- // loaded resources. This merges the new data on top of the old data for
- // this language, so that the full translation set can be loaded across
- // multiple fragments.
- this._updateHass({
- language: language,
- resources: {
- [language]: Object.assign({}, this.hass
- && this.hass.resources && this.hass.resources[language], data),
- },
- });
- }
-
- connectionChanged(conn, oldConn) {
- if (oldConn) {
- this.unsubConnection();
- this.unsubConnection = null;
- }
- if (!conn) {
- this._updateHass({
- connection: null,
- connected: false,
- states: null,
- config: null,
- themes: null,
- dockedSidebar: false,
- moreInfoEntityId: null,
- callService: null,
- callApi: null,
- sendWS: null,
- callWS: null,
- user: null,
- });
- return;
- }
- var notifications = this.$.notifications;
- this.hass = Object.assign({
- connection: conn,
- connected: true,
- states: null,
- config: null,
- themes: null,
- panels: null,
- panelUrl: this.panelUrl,
-
- language: getActiveTranslation(),
- // If resources are already loaded, don't discard them
- resources: (this.hass && this.hass.resources) || null,
-
- translationMetadata: translationMetadata,
- dockedSidebar: false,
- moreInfoEntityId: null,
- callService: async (domain, service, serviceData) => {
- try {
- await conn.callService(domain, service, serviceData || {});
-
- let message;
- let name;
- if (serviceData.entity_id && this.hass.states &&
- this.hass.states[serviceData.entity_id]) {
- name = computeStateName(this.hass.states[serviceData.entity_id]);
- }
- if (service === 'turn_on' && serviceData.entity_id) {
- message = this.localize(
- 'ui.notification_toast.entity_turned_on',
- 'entity', name || serviceData.entity_id
- );
- } else if (service === 'turn_off' && serviceData.entity_id) {
- message = this.localize(
- 'ui.notification_toast.entity_turned_off',
- 'entity', name || serviceData.entity_id
- );
- } else {
- message = this.localize(
- 'ui.notification_toast.service_called',
- 'service', `${domain}/${service}`
- );
- }
- notifications.showNotification(message);
- } catch (err) {
- const msg = this.localize(
- 'ui.notification_toast.service_call_failed',
- 'service', `${domain}/${service}`
- );
- notifications.showNotification(msg);
- throw err;
- }
- },
- callApi: async (method, path, parameters) => {
- const host = window.location.protocol + '//' + window.location.host;
- const auth = conn.options;
- try {
- // Refresh token if it will expire in 30 seconds
- if (auth.accessToken && Date.now() + 30000 > auth.expires) {
- const accessToken = await window.refreshToken();
- conn.options.accessToken = accessToken.access_token;
- conn.options.expires = accessToken.expires;
- }
- return await hassCallApi(host, auth, method, path, parameters);
- } catch (err) {
- if (!err || err.status_code !== 401 || !auth.accessToken) throw err;
-
- // If we connect with access token and get 401, refresh token and try again
- const accessToken = await window.refreshToken();
- conn.options.accessToken = accessToken.access_token;
- conn.options.expires = accessToken.expires;
- return await hassCallApi(host, auth, method, path, parameters);
- }
- },
- // For messages that do not get a response
- sendWS: (msg) => {
- // eslint-disable-next-line
- if (__DEV__) console.log('Sending', msg);
- conn.sendMessage(msg);
- },
- // For messages that expect a response
- callWS: (msg) => {
- /* eslint-disable no-console */
- if (__DEV__) console.log('Sending', msg);
-
- const resp = conn.sendMessagePromise(msg);
-
- if (__DEV__) {
- resp.then(
- result => console.log('Received', result),
- err => console.log('Error', err),
- );
- }
- // In the future we'll do this as a breaking change
- // inside home-assistant-js-websocket
- return resp.then(result => result.result);
- },
- }, this.$.storage.getStoredState());
-
- var reconnected = () => {
- this._updateHass({ connected: true });
- this.loadBackendTranslations();
- this._loadPanels();
- };
-
- const disconnected = () => {
- this._updateHass({ connected: false });
- };
-
- conn.addEventListener('ready', reconnected);
-
- // If we reconnect after losing connection and access token is no longer
- // valid.
- conn.addEventListener('reconnect-error', async (_conn, err) => {
- if (err !== ERR_INVALID_AUTH) return;
- disconnected();
- this.unsubConnection();
- const accessToken = await window.refreshToken();
- this.handleConnectionPromise(window.createHassConnection(null, accessToken));
- });
- conn.addEventListener('disconnected', disconnected);
-
- let unsubEntities;
-
- subscribeEntities(conn, (states) => {
- this._updateHass({ states: states });
- }).then(function (unsub) {
- unsubEntities = unsub;
- });
-
- let unsubConfig;
-
- subscribeConfig(conn, (config) => {
- this._updateHass({ config: config });
- }).then(function (unsub) {
- unsubConfig = unsub;
- });
-
- this._loadPanels();
-
- let unsubThemes;
-
- this.hass.callWS({
- type: 'frontend/get_themes',
- }).then((themes) => {
- this._updateHass({ themes });
- applyThemesOnElement(
- document.documentElement,
- themes,
- this.hass.selectedTheme,
- true
- );
- });
-
- // only for new auth
- if (conn.options.accessToken) {
- this.hass.callWS({
- type: 'auth/current_user',
- }).then(user => this._updateHass({ user }), () => {});
- }
-
- conn.subscribeEvents((event) => {
- this._updateHass({ themes: event.data });
- applyThemesOnElement(
- document.documentElement,
- event.data,
- this.hass.selectedTheme,
- true
- );
- }, 'themes_updated').then(function (unsub) {
- unsubThemes = unsub;
- });
-
- this.loadBackendTranslations();
-
- this.unsubConnection = function () {
- conn.removeEventListener('ready', reconnected);
- conn.removeEventListener('disconnected', disconnected);
- unsubEntities();
- unsubConfig();
- unsubThemes();
- };
- }
-
- computePanelUrl(routeData) {
- return (routeData && routeData.panel) || 'states';
- }
-
- panelUrlChanged(newPanelUrl) {
- this._updateHass({ panelUrl: newPanelUrl });
- this.loadTranslationFragment(newPanelUrl);
- }
-
- async handleConnectionPromise(prom) {
- if (!prom) return;
-
- try {
- this.connection = await prom;
- } catch (err) {
- this.connectionPromise = null;
- }
- }
-
- handleMoreInfo(ev) {
- ev.stopPropagation();
-
- this._updateHass({ moreInfoEntityId: ev.detail.entityId });
- }
-
- handleDockSidebar(ev) {
- ev.stopPropagation();
- this._updateHass({ dockedSidebar: ev.detail.dock });
- this.$.storage.storeState();
- }
-
- handleNotification(ev) {
- this.$.notifications.showNotification(ev.detail.message);
- }
-
- handleLogout() {
- this.connection.close();
- localStorage.clear();
- document.location = '/';
- }
-
- setTheme(event) {
- this._updateHass({ selectedTheme: event.detail });
- applyThemesOnElement(
- document.documentElement,
- this.hass.themes,
- this.hass.selectedTheme,
- true
- );
- this.$.storage.storeState();
- }
-
- selectLanguage(event) {
- this._updateHass({ selectedLanguage: event.detail.language });
- this.$.storage.storeState();
- this.loadResources();
- this.loadBackendTranslations();
- this.loadTranslationFragment(this.panelUrl);
- }
-
- loadTranslationFragment(panelUrl) {
- if (translationMetadata.fragments.includes(panelUrl)) {
- this.loadResources(panelUrl);
- }
- }
-
- async _loadPanels() {
- const panels = await this.hass.callWS({
- type: 'get_panels'
- });
- this._updateHass({ panels });
- }
-
-
- _updateHass(obj) {
- this.hass = Object.assign({}, this.hass, obj);
- }
-}
-
-customElements.define('home-assistant', HomeAssistant);
diff --git a/src/entrypoints/core.js b/src/entrypoints/core.js
index 2c1adfbf40..32f963b90b 100644
--- a/src/entrypoints/core.js
+++ b/src/entrypoints/core.js
@@ -37,7 +37,7 @@ function redirectLogin() {
document.location = `${__PUBLIC_PATH__}authorize.html?response_type=code&client_id=${encodeURIComponent(clientId())}&redirect_uri=${encodeURIComponent(location.toString())}`;
}
-window.refreshToken = () =>
+window.refreshToken = () => (window.tokens ?
refreshToken_(clientId(), window.tokens.refresh_token).then((accessTokenResp) => {
window.tokens = Object.assign({}, window.tokens, accessTokenResp);
localStorage.tokens = JSON.stringify(window.tokens);
@@ -45,7 +45,7 @@ window.refreshToken = () =>
access_token: accessTokenResp.access_token,
expires: window.tokens.expires
};
- }, () => redirectLogin());
+ }, () => redirectLogin()) : redirectLogin());
function resolveCode(code) {
fetchToken(clientId(), code).then((tokens) => {
diff --git a/src/layouts/app/auth-mixin.js b/src/layouts/app/auth-mixin.js
new file mode 100644
index 0000000000..eafa3e35ea
--- /dev/null
+++ b/src/layouts/app/auth-mixin.js
@@ -0,0 +1,25 @@
+import { clearState } from '../../util/ha-pref-storage.js';
+
+export default superClass => class extends superClass {
+ ready() {
+ super.ready();
+ this.addEventListener('hass-logout', () => this._handleLogout());
+ }
+
+ hassConnected() {
+ super.hassConnected();
+
+ // only for new auth
+ if (this.hass.connection.options.accessToken) {
+ this.hass.callWS({
+ type: 'auth/current_user',
+ }).then(user => this._updateHass({ user }), () => {});
+ }
+ }
+
+ _handleLogout() {
+ this.hass.connection.close();
+ clearState();
+ document.location.href = '/';
+ }
+};
diff --git a/src/layouts/app/connection-mixin.js b/src/layouts/app/connection-mixin.js
new file mode 100644
index 0000000000..69dba37d6e
--- /dev/null
+++ b/src/layouts/app/connection-mixin.js
@@ -0,0 +1,199 @@
+import {
+ ERR_INVALID_AUTH,
+ subscribeEntities,
+ subscribeConfig,
+} from 'home-assistant-js-websocket';
+
+import translationMetadata from '../../../build-translations/translationMetadata.json';
+
+import LocalizeMixin from '../../mixins/localize-mixin.js';
+import EventsMixin from '../../mixins/events-mixin.js';
+
+import { getState } from '../../util/ha-pref-storage.js';
+import { getActiveTranslation } from '../../util/hass-translation.js';
+import hassCallApi from '../../util/hass-call-api.js';
+import computeStateName from '../../common/entity/compute_state_name.js';
+
+export default superClass =>
+ class extends EventsMixin(LocalizeMixin(superClass)) {
+ constructor() {
+ super();
+ this.unsubFuncs = [];
+ }
+
+ ready() {
+ super.ready();
+ this.addEventListener('try-connection', e =>
+ this._handleNewConnProm(e.detail.connProm));
+ if (window.hassConnection) {
+ this._handleNewConnProm(window.hassConnection);
+ }
+ }
+
+ async _handleNewConnProm(connProm) {
+ this.connectionPromise = connProm;
+
+ let conn;
+
+ try {
+ conn = await connProm;
+ } catch (err) {
+ this.connectionPromise = null;
+ return;
+ }
+ this._setConnection(conn);
+ }
+
+ _setConnection(conn) {
+ this.hass = Object.assign({
+ connection: conn,
+ connected: true,
+ states: null,
+ config: null,
+ themes: null,
+ panels: null,
+ panelUrl: this.panelUrl,
+
+ language: getActiveTranslation(),
+ // If resources are already loaded, don't discard them
+ resources: (this.hass && this.hass.resources) || null,
+
+ translationMetadata: translationMetadata,
+ dockedSidebar: false,
+ moreInfoEntityId: null,
+ callService: async (domain, service, serviceData = {}) => {
+ try {
+ await conn.callService(domain, service, serviceData);
+
+ let message;
+ let name;
+ if (serviceData.entity_id && this.hass.states &&
+ this.hass.states[serviceData.entity_id]) {
+ name = computeStateName(this.hass.states[serviceData.entity_id]);
+ }
+ if (service === 'turn_on' && serviceData.entity_id) {
+ message = this.localize(
+ 'ui.notification_toast.entity_turned_on',
+ 'entity', name || serviceData.entity_id
+ );
+ } else if (service === 'turn_off' && serviceData.entity_id) {
+ message = this.localize(
+ 'ui.notification_toast.entity_turned_off',
+ 'entity', name || serviceData.entity_id
+ );
+ } else {
+ message = this.localize(
+ 'ui.notification_toast.service_called',
+ 'service', `${domain}/${service}`
+ );
+ }
+ this.fire('hass-notification', { message });
+ } catch (err) {
+ const message = this.localize(
+ 'ui.notification_toast.service_call_failed',
+ 'service', `${domain}/${service}`
+ );
+ this.fire('hass-notification', { message });
+ throw err;
+ }
+ },
+ callApi: async (method, path, parameters) => {
+ const host = window.location.protocol + '//' + window.location.host;
+ const auth = conn.options;
+ try {
+ // Refresh token if it will expire in 30 seconds
+ if (auth.accessToken && Date.now() + 30000 > auth.expires) {
+ const accessToken = await window.refreshToken();
+ conn.options.accessToken = accessToken.access_token;
+ conn.options.expires = accessToken.expires;
+ }
+ return await hassCallApi(host, auth, method, path, parameters);
+ } catch (err) {
+ if (!err || err.status_code !== 401 || !auth.accessToken) throw err;
+
+ // If we connect with access token and get 401, refresh token and try again
+ const accessToken = await window.refreshToken();
+ conn.options.accessToken = accessToken.access_token;
+ conn.options.expires = accessToken.expires;
+ return await hassCallApi(host, auth, method, path, parameters);
+ }
+ },
+ // For messages that do not get a response
+ sendWS: (msg) => {
+ // eslint-disable-next-line
+ if (__DEV__) console.log('Sending', msg);
+ conn.sendMessage(msg);
+ },
+ // For messages that expect a response
+ callWS: (msg) => {
+ /* eslint-disable no-console */
+ if (__DEV__) console.log('Sending', msg);
+
+ const resp = conn.sendMessagePromise(msg);
+
+ if (__DEV__) {
+ resp.then(
+ result => console.log('Received', result),
+ err => console.log('Error', err),
+ );
+ }
+ // In the future we'll do this as a breaking change
+ // inside home-assistant-js-websocket
+ return resp.then(result => result.result);
+ },
+ }, getState());
+
+ this.hassConnected();
+ }
+
+ hassConnected() {
+ super.hassConnected();
+
+ const conn = this.hass.connection;
+
+ const reconnected = () => this.hassReconnected();
+ const disconnected = () => this._updateHass({ connected: false });
+ const reconnectError = async (_conn, err) => {
+ if (err !== ERR_INVALID_AUTH) return;
+ disconnected();
+ while (this.unsubFuncs.length) {
+ this.unsubFuncs.pop()();
+ }
+ const accessToken = await window.refreshToken();
+ this._handleNewConnProm(window.createHassConnection(null, accessToken));
+ };
+
+ conn.addEventListener('ready', reconnected);
+ conn.addEventListener('disconnected', disconnected);
+ // If we reconnect after losing connection and access token is no longer
+ // valid.
+ conn.addEventListener('reconnect-error', reconnectError);
+
+ this.unsubFuncs.push(() => {
+ conn.removeEventListener('ready', reconnected);
+ conn.removeEventListener('disconnected', disconnected);
+ conn.removeEventListener('reconnect-error', reconnectError);
+ });
+
+ subscribeEntities(conn, states => this._updateHass({ states }))
+ .then(unsub => this.unsubFuncs.push(unsub));
+
+ subscribeConfig(conn, config => this._updateHass({ config }))
+ .then(unsub => this.unsubFuncs.push(unsub));
+
+ this._loadPanels();
+ }
+
+ hassReconnected() {
+ super.hassReconnected();
+ this._updateHass({ connected: true });
+ this._loadPanels();
+ }
+
+ async _loadPanels() {
+ const panels = await this.hass.callWS({
+ type: 'get_panels'
+ });
+ this._updateHass({ panels });
+ }
+ };
diff --git a/src/layouts/app/dialog-manager-mixin.js b/src/layouts/app/dialog-manager-mixin.js
new file mode 100644
index 0000000000..a99a5aa37f
--- /dev/null
+++ b/src/layouts/app/dialog-manager-mixin.js
@@ -0,0 +1,23 @@
+export default superClass =>
+ class extends superClass {
+ ready() {
+ super.ready();
+ this.addEventListener('register-dialog', e => this.registerDialog(e.detail));
+ }
+
+ registerDialog({ dialogShowEvent, dialogTag, dialogImport }) {
+ let loaded = null;
+
+ this.addEventListener(dialogShowEvent, (showEv) => {
+ if (!loaded) {
+ loaded = dialogImport().then(() => {
+ const dialogEl = document.createElement(dialogTag);
+ this.shadowRoot.appendChild(dialogEl);
+ this.provideHass(dialogEl);
+ return dialogEl;
+ });
+ }
+ loaded.then(dialogEl => dialogEl.showDialog(showEv.detail));
+ });
+ }
+ };
diff --git a/src/layouts/app/hass-base-mixin.js b/src/layouts/app/hass-base-mixin.js
new file mode 100644
index 0000000000..7572491b57
--- /dev/null
+++ b/src/layouts/app/hass-base-mixin.js
@@ -0,0 +1,35 @@
+/* eslint-disable no-unused-vars */
+export default superClass => class extends superClass {
+ constructor() {
+ super();
+ this.__pendingHass = false;
+ this.__provideHass = [];
+ }
+
+ // Exists so all methods can safely call super method
+ hassConnected() {}
+ hassReconnected() {}
+ panelUrlChanged(newPanelUrl) {}
+ hassChanged(hass, oldHass) {
+ this.__provideHass.forEach((el) => {
+ el.hass = hass;
+ });
+ }
+
+ provideHass(el) {
+ this.__provideHass.push(el);
+ }
+
+ async _updateHass(obj) {
+ const oldHass = this.hass;
+ this.hass = Object.assign({}, this.hass, obj);
+ this.__pendingHass = true;
+
+ await 0;
+
+ if (!this.__pendingHass) return;
+
+ this.__pendingHass = false;
+ this.hassChanged(this.hass, oldHass);
+ }
+};
diff --git a/src/layouts/app/home-assistant.js b/src/layouts/app/home-assistant.js
new file mode 100644
index 0000000000..716b67edeb
--- /dev/null
+++ b/src/layouts/app/home-assistant.js
@@ -0,0 +1,111 @@
+import '@polymer/app-route/app-location.js';
+import '@polymer/app-route/app-route.js';
+import '@polymer/iron-flex-layout/iron-flex-layout-classes.js';
+import { html } from '@polymer/polymer/lib/utils/html-tag.js';
+import { PolymerElement } from '@polymer/polymer/polymer-element.js';
+import { afterNextRender } from '@polymer/polymer/lib/utils/render-status.js';
+
+import '../../layouts/home-assistant-main.js';
+import '../../resources/ha-style.js';
+import registerServiceWorker from '../../util/register-service-worker.js';
+
+import HassBaseMixin from './hass-base-mixin.js';
+import AuthMixin from './auth-mixin.js';
+import TranslationsMixin from './translations-mixin.js';
+import ThemesMixin from './themes-mixin.js';
+import MoreInfoMixin from './more-info-mixin.js';
+import SidebarMixin from './sidebar-mixin.js';
+import DialogManagerMixin from './dialog-manager-mixin.js';
+import ConnectionMixin from './connection-mixin.js';
+import NotificationMixin from './notification-mixin.js';
+
+import(/* webpackChunkName: "login-form" */ '../../layouts/login-form.js');
+
+const ext = (baseClass, mixins) => mixins.reduceRight((base, mixin) => mixin(base), baseClass);
+
+class HomeAssistant extends ext(PolymerElement, [
+ AuthMixin,
+ ThemesMixin,
+ TranslationsMixin,
+ MoreInfoMixin,
+ SidebarMixin,
+ ConnectionMixin,
+ NotificationMixin,
+ DialogManagerMixin,
+ HassBaseMixin
+]) {
+ static get template() {
+ return html`
+
+
+
+
+
+
+
+
+
+`;
+ }
+
+ static get properties() {
+ return {
+ connectionPromise: {
+ type: Object,
+ value: null,
+ },
+ hass: {
+ type: Object,
+ value: null,
+ },
+ showMain: {
+ type: Boolean,
+ computed: 'computeShowMain(hass)',
+ },
+ route: Object,
+ routeData: Object,
+ panelUrl: {
+ type: String,
+ computed: 'computePanelUrl(routeData)',
+ observer: 'panelUrlChanged',
+ },
+ };
+ }
+
+ ready() {
+ super.ready();
+ afterNextRender(null, registerServiceWorker);
+ }
+
+ computeShowMain(hass) {
+ return hass && hass.states && hass.config && hass.panels;
+ }
+
+ computeShowLoading(connectionPromise, hass) {
+ // Show loading when connecting or when connected but not all pieces loaded yet
+ return (connectionPromise != null
+ || (hass && hass.connection && (!hass.states || !hass.config)));
+ }
+
+ computePanelUrl(routeData) {
+ return (routeData && routeData.panel) || 'states';
+ }
+
+ panelUrlChanged(newPanelUrl) {
+ super.panelUrlChanged(newPanelUrl);
+ this._updateHass({ panelUrl: newPanelUrl });
+ }
+}
+
+customElements.define('home-assistant', HomeAssistant);
diff --git a/src/layouts/app/more-info-mixin.js b/src/layouts/app/more-info-mixin.js
new file mode 100644
index 0000000000..821e42b65b
--- /dev/null
+++ b/src/layouts/app/more-info-mixin.js
@@ -0,0 +1,22 @@
+import { afterNextRender } from '@polymer/polymer/lib/utils/render-status.js';
+
+export default superClass =>
+ class extends superClass {
+ ready() {
+ super.ready();
+ this.addEventListener('hass-more-info', e => this._handleMoreInfo(e));
+
+ // Load it once we are having the initial rendering done.
+ afterNextRender(null, () =>
+ import(/* webpackChunkName: "more-info-dialog" */ '../../dialogs/ha-more-info-dialog.js'));
+ }
+
+ async _handleMoreInfo(ev) {
+ if (!this.__moreInfoEl) {
+ this.__moreInfoEl = document.createElement('ha-more-info-dialog');
+ this.shadowRoot.appendChild(this.__moreInfoEl);
+ this.provideHass(this.__moreInfoEl);
+ }
+ this._updateHass({ moreInfoEntityId: ev.detail.entityId });
+ }
+ };
diff --git a/src/layouts/app/notification-mixin.js b/src/layouts/app/notification-mixin.js
new file mode 100644
index 0000000000..ac32352722
--- /dev/null
+++ b/src/layouts/app/notification-mixin.js
@@ -0,0 +1,11 @@
+export default superClass =>
+ class extends superClass {
+ ready() {
+ super.ready();
+ this.registerDialog({
+ dialogShowEvent: 'hass-notification',
+ dialogTag: 'notification-manager',
+ dialogImport: () => import(/* webpackChunkName: "notification-manager" */ '../../managers/notification-manager.js'),
+ });
+ }
+ };
diff --git a/src/layouts/app/sidebar-mixin.js b/src/layouts/app/sidebar-mixin.js
new file mode 100644
index 0000000000..55cda0bffc
--- /dev/null
+++ b/src/layouts/app/sidebar-mixin.js
@@ -0,0 +1,15 @@
+import { storeState } from '../../util/ha-pref-storage.js';
+
+export default superClass =>
+ class extends superClass {
+ ready() {
+ super.ready();
+ this.addEventListener('hass-dock-sidebar', e =>
+ this._handleDockSidebar(e));
+ }
+
+ _handleDockSidebar(ev) {
+ this._updateHass({ dockedSidebar: ev.detail.dock });
+ storeState(this.hass);
+ }
+ };
diff --git a/src/layouts/app/themes-mixin.js b/src/layouts/app/themes-mixin.js
new file mode 100644
index 0000000000..c35a1ff717
--- /dev/null
+++ b/src/layouts/app/themes-mixin.js
@@ -0,0 +1,46 @@
+import applyThemesOnElement from '../../common/dom/apply_themes_on_element.js';
+import { storeState } from '../../util/ha-pref-storage.js';
+
+export default superClass => class extends superClass {
+ ready() {
+ super.ready();
+ this.addEventListener('settheme', e => this._setTheme(e));
+ }
+
+ hassConnected() {
+ super.hassConnected();
+
+ this.hass.callWS({
+ type: 'frontend/get_themes',
+ }).then((themes) => {
+ this._updateHass({ themes });
+ applyThemesOnElement(
+ document.documentElement,
+ themes,
+ this.hass.selectedTheme,
+ true
+ );
+ });
+
+ this.hass.connection.subscribeEvents((event) => {
+ this._updateHass({ themes: event.data });
+ applyThemesOnElement(
+ document.documentElement,
+ event.data,
+ this.hass.selectedTheme,
+ true
+ );
+ }, 'themes_updated').then(unsub => this.unsubFuncs.push(unsub));
+ }
+
+ _setTheme(event) {
+ this._updateHass({ selectedTheme: event.detail });
+ applyThemesOnElement(
+ document.documentElement,
+ this.hass.themes,
+ this.hass.selectedTheme,
+ true
+ );
+ storeState(this.hass);
+ }
+};
diff --git a/src/layouts/app/translations-mixin.js b/src/layouts/app/translations-mixin.js
new file mode 100644
index 0000000000..140c592120
--- /dev/null
+++ b/src/layouts/app/translations-mixin.js
@@ -0,0 +1,80 @@
+import translationMetadata from '../../../build-translations/translationMetadata.json';
+import { getTranslation } from '../../util/hass-translation.js';
+
+import { storeState } from '../../util/ha-pref-storage.js';
+
+/*
+ * superClass needs to contain `this.hass` and `this._updateHass`.
+ */
+
+export default superClass => class extends superClass {
+ ready() {
+ super.ready();
+ this.addEventListener('hass-language-select', e => this._selectLanguage(e));
+ this._loadResources();
+ }
+
+ hassConnected() {
+ super.hassConnected();
+ this._loadBackendTranslations();
+ }
+
+ hassReconnected() {
+ super.hassReconnected();
+ this._loadBackendTranslations();
+ }
+
+ panelUrlChanged(newPanelUrl) {
+ super.panelUrlChanged(newPanelUrl);
+ this._loadTranslationFragment(newPanelUrl);
+ }
+
+ async _loadBackendTranslations() {
+ if (!this.hass.language) return;
+
+ const language = this.hass.selectedLanguage || this.hass.language;
+
+ const { resources } = await this.hass.callWS({
+ type: 'frontend/get_translations',
+ language,
+ });
+
+ // If we've switched selected languages just ignore this response
+ if ((this.hass.selectedLanguage || this.hass.language) !== language) return;
+
+ this._updateResources(language, resources);
+ }
+
+ _loadTranslationFragment(panelUrl) {
+ if (translationMetadata.fragments.includes(panelUrl)) {
+ this._loadResources(panelUrl);
+ }
+ }
+
+ async _loadResources(fragment) {
+ const result = await getTranslation(fragment);
+ this._updateResources(result.language, result.data);
+ }
+
+ _updateResources(language, data) {
+ // Update the language in hass, and update the resources with the newly
+ // loaded resources. This merges the new data on top of the old data for
+ // this language, so that the full translation set can be loaded across
+ // multiple fragments.
+ this._updateHass({
+ language: language,
+ resources: {
+ [language]: Object.assign({}, this.hass
+ && this.hass.resources && this.hass.resources[language], data),
+ },
+ });
+ }
+
+ _selectLanguage(event) {
+ this._updateHass({ selectedLanguage: event.detail.language });
+ storeState(this.hass);
+ this._loadResources();
+ this._loadBackendTranslations();
+ this._loadTranslationFragment(this.panelUrl);
+ }
+};
diff --git a/src/layouts/home-assistant-main.js b/src/layouts/home-assistant-main.js
index d6f6752443..6f40ba42fa 100644
--- a/src/layouts/home-assistant-main.js
+++ b/src/layouts/home-assistant-main.js
@@ -14,7 +14,6 @@ import EventsMixin from '../mixins/events-mixin.js';
import NavigateMixin from '../mixins/navigate-mixin.js';
import(/* webpackChunkName: "ha-sidebar" */ '../components/ha-sidebar.js');
-import(/* webpackChunkName: "more-info-dialog" */ '../dialogs/ha-more-info-dialog.js');
import(/* webpackChunkName: "voice-command-dialog" */ '../dialogs/ha-voice-command-dialog.js');
const NON_SWIPABLE_PANELS = ['kiosk', 'map'];
@@ -36,7 +35,6 @@ class HomeAssistantMain extends NavigateMixin(EventsMixin(PolymerElement)) {
height: 100%;
}
-
diff --git a/src/layouts/login-form.js b/src/layouts/login-form.js
index 1ac2b15c05..ddaefd3a41 100644
--- a/src/layouts/login-form.js
+++ b/src/layouts/login-form.js
@@ -9,11 +9,12 @@ import { ERR_CANNOT_CONNECT, ERR_INVALID_AUTH } from 'home-assistant-js-websocke
import LocalizeMixin from '../mixins/localize-mixin.js';
+import EventsMixin from '../mixins/events-mixin.js';
/*
* @appliesMixin LocalizeMixin
*/
-class LoginForm extends LocalizeMixin(PolymerElement) {
+class LoginForm extends EventsMixin(LocalizeMixin(PolymerElement)) {
static get template() {
return html`
@@ -114,10 +115,6 @@ class LoginForm extends LocalizeMixin(PolymerElement) {
this.addEventListener('keydown', ev => this.passwordKeyDown(ev));
}
- connectedCallback() {
- super.connectedCallback();
- }
-
computeLoadingMsg(isValidating) {
return isValidating ? 'Connecting' : 'Loading data';
}
@@ -150,10 +147,11 @@ class LoginForm extends LocalizeMixin(PolymerElement) {
validatePassword() {
var auth = this.password;
this.$.hideKeyboardOnFocus.focus();
- this.connectionPromise = window.createHassConnection(auth);
+ const connProm = window.createHassConnection(auth);
+ this.fire('try-connection', { connProm });
if (this.$.rememberLogin.checked) {
- this.connectionPromise.then(function () {
+ connProm.then(function () {
localStorage.authToken = auth;
});
}
diff --git a/src/managers/notification-manager.js b/src/managers/notification-manager.js
index a3820862df..99b754a959 100644
--- a/src/managers/notification-manager.js
+++ b/src/managers/notification-manager.js
@@ -84,7 +84,7 @@ class NotificationManager extends LocalizeMixin(PolymerElement) {
this.$.connToast.classList.toggle('fit-bottom', ev.matches);
}
- showNotification(message) {
+ showDialog({ message }) {
this.$.toast.show(message);
}
}
diff --git a/src/util/ha-pref-storage.js b/src/util/ha-pref-storage.js
index 8e103eeb57..caf23fb0fe 100644
--- a/src/util/ha-pref-storage.js
+++ b/src/util/ha-pref-storage.js
@@ -1,47 +1,32 @@
-import { PolymerElement } from '@polymer/polymer/polymer-element.js';
-
-const STORED_STATE = [
- 'dockedSidebar',
- 'selectedTheme',
- 'selectedLanguage',
-];
-
-class HaPrefStorage extends PolymerElement {
- static get properties() {
- return {
- hass: Object,
- storage: {
- type: Object,
- value: window.localStorage || {},
- },
- };
- }
-
- storeState() {
- if (!this.hass) return;
-
- try {
- for (var i = 0; i < STORED_STATE.length; i++) {
- var key = STORED_STATE[i];
- var value = this.hass[key];
- this.storage[key] = JSON.stringify(value === undefined ? null : value);
- }
- } catch (err) {
- // Safari throws exception in private mode
- }
- }
-
- getStoredState() {
- var state = {};
+const STORED_STATE = ['dockedSidebar', 'selectedTheme', 'selectedLanguage'];
+const STORAGE = window.localStorage || {};
+export function storeState(hass) {
+ try {
for (var i = 0; i < STORED_STATE.length; i++) {
var key = STORED_STATE[i];
- if (key in this.storage) {
- state[key] = JSON.parse(this.storage[key]);
- }
+ var value = hass[key];
+ STORAGE[key] = JSON.stringify(value === undefined ? null : value);
}
-
- return state;
+ } catch (err) {
+ // Safari throws exception in private mode
}
}
-customElements.define('ha-pref-storage', HaPrefStorage);
+
+export function getState() {
+ var state = {};
+
+ for (var i = 0; i < STORED_STATE.length; i++) {
+ var key = STORED_STATE[i];
+ if (key in STORAGE) {
+ state[key] = JSON.parse(STORAGE[key]);
+ }
+ }
+
+ return state;
+}
+
+export function clearState() {
+ // STORAGE is an object if localStorage not available.
+ if (STORAGE.clear) STORAGE.clear();
+}