mirror of
https://github.com/home-assistant/frontend.git
synced 2025-07-13 04:16:34 +00:00
Reorg root (#1559)
* Extract element from entrypoint * Reorg root * Extract more * Lint * Extract connection * Extract notification * Lint * Also split out more info dialog * Consolidate dynamic element creation
This commit is contained in:
parent
1a31855fc8
commit
1b2b62f04c
@ -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));
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
|
@ -1,439 +1,24 @@
|
|||||||
/* eslint-disable import/first */
|
|
||||||
// Load polyfill first so HTML imports start resolving
|
// Load polyfill first so HTML imports start resolving
|
||||||
|
/* eslint-disable import/first */
|
||||||
import '../resources/html-import/polyfill.js';
|
import '../resources/html-import/polyfill.js';
|
||||||
import '@polymer/app-route/app-location.js';
|
import '@polymer/app-route/app-location.js';
|
||||||
import '@polymer/app-route/app-route.js';
|
import '@polymer/app-route/app-route.js';
|
||||||
import '@polymer/iron-flex-layout/iron-flex-layout-classes.js';
|
import '@polymer/iron-flex-layout/iron-flex-layout-classes.js';
|
||||||
import '@polymer/paper-styles/typography.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 { 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 '../util/legacy-support';
|
||||||
import '../resources/roboto.js';
|
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
|
// 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.
|
// properly into iron-meta, which is used to transfer iconsets to iron-icon.
|
||||||
import '../components/ha-iconset-svg.js';
|
import '../components/ha-iconset-svg.js';
|
||||||
|
|
||||||
|
import '../layouts/app/home-assistant.js';
|
||||||
|
|
||||||
/* polyfill for paper-dropdown */
|
/* polyfill for paper-dropdown */
|
||||||
import(/* webpackChunkName: "polyfill-web-animations-next" */ 'web-animations-js/web-animations-next-lite.min.js');
|
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);
|
setPassiveTouchGestures(true);
|
||||||
/* LastPass createElement workaround. See #428 */
|
/* LastPass createElement workaround. See #428 */
|
||||||
document.createElement = Document.prototype.createElement;
|
document.createElement = Document.prototype.createElement;
|
||||||
|
|
||||||
class HomeAssistant extends LocalizeMixin(PolymerElement) {
|
|
||||||
static get template() {
|
|
||||||
return html`
|
|
||||||
<ha-pref-storage hass="[[hass]]" id="storage"></ha-pref-storage>
|
|
||||||
<notification-manager id="notifications" hass="[[hass]]"></notification-manager>
|
|
||||||
<app-location route="{{route}}"></app-location>
|
|
||||||
<app-route route="{{route}}" pattern="/:panel" data="{{routeData}}"></app-route>
|
|
||||||
<template is="dom-if" if="[[showMain]]" restamp="">
|
|
||||||
<home-assistant-main on-hass-more-info="handleMoreInfo" on-hass-dock-sidebar="handleDockSidebar" on-hass-notification="handleNotification" on-hass-logout="handleLogout" hass="[[hass]]" route="{{route}}"></home-assistant-main>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<template is="dom-if" if="[[!showMain]]" restamp="">
|
|
||||||
<login-form hass="[[hass]]" connection-promise="{{connectionPromise}}" show-loading="[[computeShowLoading(connectionPromise, hass)]]">
|
|
||||||
</login-form>
|
|
||||||
</template>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
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);
|
|
||||||
|
@ -37,7 +37,7 @@ function redirectLogin() {
|
|||||||
document.location = `${__PUBLIC_PATH__}authorize.html?response_type=code&client_id=${encodeURIComponent(clientId())}&redirect_uri=${encodeURIComponent(location.toString())}`;
|
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) => {
|
refreshToken_(clientId(), window.tokens.refresh_token).then((accessTokenResp) => {
|
||||||
window.tokens = Object.assign({}, window.tokens, accessTokenResp);
|
window.tokens = Object.assign({}, window.tokens, accessTokenResp);
|
||||||
localStorage.tokens = JSON.stringify(window.tokens);
|
localStorage.tokens = JSON.stringify(window.tokens);
|
||||||
@ -45,7 +45,7 @@ window.refreshToken = () =>
|
|||||||
access_token: accessTokenResp.access_token,
|
access_token: accessTokenResp.access_token,
|
||||||
expires: window.tokens.expires
|
expires: window.tokens.expires
|
||||||
};
|
};
|
||||||
}, () => redirectLogin());
|
}, () => redirectLogin()) : redirectLogin());
|
||||||
|
|
||||||
function resolveCode(code) {
|
function resolveCode(code) {
|
||||||
fetchToken(clientId(), code).then((tokens) => {
|
fetchToken(clientId(), code).then((tokens) => {
|
||||||
|
25
src/layouts/app/auth-mixin.js
Normal file
25
src/layouts/app/auth-mixin.js
Normal file
@ -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 = '/';
|
||||||
|
}
|
||||||
|
};
|
199
src/layouts/app/connection-mixin.js
Normal file
199
src/layouts/app/connection-mixin.js
Normal file
@ -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 });
|
||||||
|
}
|
||||||
|
};
|
23
src/layouts/app/dialog-manager-mixin.js
Normal file
23
src/layouts/app/dialog-manager-mixin.js
Normal file
@ -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));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
35
src/layouts/app/hass-base-mixin.js
Normal file
35
src/layouts/app/hass-base-mixin.js
Normal file
@ -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);
|
||||||
|
}
|
||||||
|
};
|
111
src/layouts/app/home-assistant.js
Normal file
111
src/layouts/app/home-assistant.js
Normal file
@ -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`
|
||||||
|
<app-location route="{{route}}"></app-location>
|
||||||
|
<app-route
|
||||||
|
route="{{route}}"
|
||||||
|
pattern="/:panel"
|
||||||
|
data="{{routeData}}"
|
||||||
|
></app-route>
|
||||||
|
<template is="dom-if" if="[[showMain]]" restamp>
|
||||||
|
<home-assistant-main
|
||||||
|
hass="[[hass]]"
|
||||||
|
route="{{route}}"
|
||||||
|
></home-assistant-main>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template is="dom-if" if="[[!showMain]]" restamp>
|
||||||
|
<login-form
|
||||||
|
hass="[[hass]]"
|
||||||
|
connection-promise="[[connectionPromise]]"
|
||||||
|
show-loading="[[computeShowLoading(connectionPromise, hass)]]"
|
||||||
|
></login-form>
|
||||||
|
</template>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
22
src/layouts/app/more-info-mixin.js
Normal file
22
src/layouts/app/more-info-mixin.js
Normal file
@ -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 });
|
||||||
|
}
|
||||||
|
};
|
11
src/layouts/app/notification-mixin.js
Normal file
11
src/layouts/app/notification-mixin.js
Normal file
@ -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'),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
15
src/layouts/app/sidebar-mixin.js
Normal file
15
src/layouts/app/sidebar-mixin.js
Normal file
@ -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);
|
||||||
|
}
|
||||||
|
};
|
46
src/layouts/app/themes-mixin.js
Normal file
46
src/layouts/app/themes-mixin.js
Normal file
@ -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);
|
||||||
|
}
|
||||||
|
};
|
80
src/layouts/app/translations-mixin.js
Normal file
80
src/layouts/app/translations-mixin.js
Normal file
@ -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);
|
||||||
|
}
|
||||||
|
};
|
@ -14,7 +14,6 @@ import EventsMixin from '../mixins/events-mixin.js';
|
|||||||
import NavigateMixin from '../mixins/navigate-mixin.js';
|
import NavigateMixin from '../mixins/navigate-mixin.js';
|
||||||
|
|
||||||
import(/* webpackChunkName: "ha-sidebar" */ '../components/ha-sidebar.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');
|
import(/* webpackChunkName: "voice-command-dialog" */ '../dialogs/ha-voice-command-dialog.js');
|
||||||
|
|
||||||
const NON_SWIPABLE_PANELS = ['kiosk', 'map'];
|
const NON_SWIPABLE_PANELS = ['kiosk', 'map'];
|
||||||
@ -36,7 +35,6 @@ class HomeAssistantMain extends NavigateMixin(EventsMixin(PolymerElement)) {
|
|||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
<ha-more-info-dialog hass="[[hass]]"></ha-more-info-dialog>
|
|
||||||
<ha-url-sync hass="[[hass]]"></ha-url-sync>
|
<ha-url-sync hass="[[hass]]"></ha-url-sync>
|
||||||
<app-route route="{{route}}" pattern="/states" tail="{{statesRouteTail}}"></app-route>
|
<app-route route="{{route}}" pattern="/states" tail="{{statesRouteTail}}"></app-route>
|
||||||
<ha-voice-command-dialog hass="[[hass]]" id="voiceDialog"></ha-voice-command-dialog>
|
<ha-voice-command-dialog hass="[[hass]]" id="voiceDialog"></ha-voice-command-dialog>
|
||||||
|
@ -9,11 +9,12 @@ import { ERR_CANNOT_CONNECT, ERR_INVALID_AUTH } from 'home-assistant-js-websocke
|
|||||||
|
|
||||||
|
|
||||||
import LocalizeMixin from '../mixins/localize-mixin.js';
|
import LocalizeMixin from '../mixins/localize-mixin.js';
|
||||||
|
import EventsMixin from '../mixins/events-mixin.js';
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* @appliesMixin LocalizeMixin
|
* @appliesMixin LocalizeMixin
|
||||||
*/
|
*/
|
||||||
class LoginForm extends LocalizeMixin(PolymerElement) {
|
class LoginForm extends EventsMixin(LocalizeMixin(PolymerElement)) {
|
||||||
static get template() {
|
static get template() {
|
||||||
return html`
|
return html`
|
||||||
<style include="iron-flex iron-positioning"></style>
|
<style include="iron-flex iron-positioning"></style>
|
||||||
@ -114,10 +115,6 @@ class LoginForm extends LocalizeMixin(PolymerElement) {
|
|||||||
this.addEventListener('keydown', ev => this.passwordKeyDown(ev));
|
this.addEventListener('keydown', ev => this.passwordKeyDown(ev));
|
||||||
}
|
}
|
||||||
|
|
||||||
connectedCallback() {
|
|
||||||
super.connectedCallback();
|
|
||||||
}
|
|
||||||
|
|
||||||
computeLoadingMsg(isValidating) {
|
computeLoadingMsg(isValidating) {
|
||||||
return isValidating ? 'Connecting' : 'Loading data';
|
return isValidating ? 'Connecting' : 'Loading data';
|
||||||
}
|
}
|
||||||
@ -150,10 +147,11 @@ class LoginForm extends LocalizeMixin(PolymerElement) {
|
|||||||
validatePassword() {
|
validatePassword() {
|
||||||
var auth = this.password;
|
var auth = this.password;
|
||||||
this.$.hideKeyboardOnFocus.focus();
|
this.$.hideKeyboardOnFocus.focus();
|
||||||
this.connectionPromise = window.createHassConnection(auth);
|
const connProm = window.createHassConnection(auth);
|
||||||
|
this.fire('try-connection', { connProm });
|
||||||
|
|
||||||
if (this.$.rememberLogin.checked) {
|
if (this.$.rememberLogin.checked) {
|
||||||
this.connectionPromise.then(function () {
|
connProm.then(function () {
|
||||||
localStorage.authToken = auth;
|
localStorage.authToken = auth;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -84,7 +84,7 @@ class NotificationManager extends LocalizeMixin(PolymerElement) {
|
|||||||
this.$.connToast.classList.toggle('fit-bottom', ev.matches);
|
this.$.connToast.classList.toggle('fit-bottom', ev.matches);
|
||||||
}
|
}
|
||||||
|
|
||||||
showNotification(message) {
|
showDialog({ message }) {
|
||||||
this.$.toast.show(message);
|
this.$.toast.show(message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,47 +1,32 @@
|
|||||||
import { PolymerElement } from '@polymer/polymer/polymer-element.js';
|
const STORED_STATE = ['dockedSidebar', 'selectedTheme', 'selectedLanguage'];
|
||||||
|
const STORAGE = window.localStorage || {};
|
||||||
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;
|
|
||||||
|
|
||||||
|
export function storeState(hass) {
|
||||||
try {
|
try {
|
||||||
for (var i = 0; i < STORED_STATE.length; i++) {
|
for (var i = 0; i < STORED_STATE.length; i++) {
|
||||||
var key = STORED_STATE[i];
|
var key = STORED_STATE[i];
|
||||||
var value = this.hass[key];
|
var value = hass[key];
|
||||||
this.storage[key] = JSON.stringify(value === undefined ? null : value);
|
STORAGE[key] = JSON.stringify(value === undefined ? null : value);
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
// Safari throws exception in private mode
|
// Safari throws exception in private mode
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
getStoredState() {
|
export function getState() {
|
||||||
var state = {};
|
var state = {};
|
||||||
|
|
||||||
for (var i = 0; i < STORED_STATE.length; i++) {
|
for (var i = 0; i < STORED_STATE.length; i++) {
|
||||||
var key = STORED_STATE[i];
|
var key = STORED_STATE[i];
|
||||||
if (key in this.storage) {
|
if (key in STORAGE) {
|
||||||
state[key] = JSON.parse(this.storage[key]);
|
state[key] = JSON.parse(STORAGE[key]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function clearState() {
|
||||||
|
// STORAGE is an object if localStorage not available.
|
||||||
|
if (STORAGE.clear) STORAGE.clear();
|
||||||
}
|
}
|
||||||
customElements.define('ha-pref-storage', HaPrefStorage);
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user