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
|
||||
/* 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`
|
||||
<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())}`;
|
||||
}
|
||||
|
||||
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) => {
|
||||
|
|
|
@ -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 = '/';
|
||||
}
|
||||
};
|
|
@ -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 });
|
||||
}
|
||||
};
|
|
@ -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));
|
||||
});
|
||||
}
|
||||
};
|
|
@ -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);
|
||||
}
|
||||
};
|
|
@ -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);
|
|
@ -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 });
|
||||
}
|
||||
};
|
|
@ -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'),
|
||||
});
|
||||
}
|
||||
};
|
|
@ -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);
|
||||
}
|
||||
};
|
|
@ -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);
|
||||
}
|
||||
};
|
|
@ -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(/* 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%;
|
||||
}
|
||||
</style>
|
||||
<ha-more-info-dialog hass="[[hass]]"></ha-more-info-dialog>
|
||||
<ha-url-sync hass="[[hass]]"></ha-url-sync>
|
||||
<app-route route="{{route}}" pattern="/states" tail="{{statesRouteTail}}"></app-route>
|
||||
<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 EventsMixin from '../mixins/events-mixin.js';
|
||||
|
||||
/*
|
||||
* @appliesMixin LocalizeMixin
|
||||
*/
|
||||
class LoginForm extends LocalizeMixin(PolymerElement) {
|
||||
class LoginForm extends EventsMixin(LocalizeMixin(PolymerElement)) {
|
||||
static get template() {
|
||||
return html`
|
||||
<style include="iron-flex iron-positioning"></style>
|
||||
|
@ -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;
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue