Use new version of HAWS (#1612)

* Use new version of HAWS

* Fix init page

* Lint

* Fix tests

* Update gitignore

* Clear old tokens, use new key to store
This commit is contained in:
Paulus Schoutsen 2018-08-31 09:45:58 +02:00 committed by GitHub
parent ab19dbc35e
commit 45cdb5a3e4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
35 changed files with 176 additions and 456 deletions

1
hassio/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
hassio-icons.html

View File

@ -68,7 +68,7 @@
"es6-object-assign": "^1.1.0",
"eslint-import-resolver-webpack": "^0.10.0",
"fecha": "^2.3.3",
"home-assistant-js-websocket": "^2.1.0",
"home-assistant-js-websocket": "^3.0.0",
"intl-messageformat": "^2.2.0",
"js-yaml": "^3.12.0",
"leaflet": "^1.3.1",

View File

@ -203,7 +203,7 @@ class HaWeatherCard extends
}
getUnit(measure) {
const lengthUnit = this.hass.config.core.unit_system.length || '';
const lengthUnit = this.hass.config.unit_system.length || '';
switch (measure) {
case 'air_pressure':
return lengthUnit === 'km' ? 'hPa' : 'inHg';
@ -212,7 +212,7 @@ class HaWeatherCard extends
case 'precipitation':
return lengthUnit === 'km' ? 'mm' : 'in';
default:
return this.hass.config.core.unit_system[measure] || '';
return this.hass.config.unit_system[measure] || '';
}
}

View File

@ -1,76 +0,0 @@
import { storeTokens, loadTokens } from './token_storage.js';
function genClientId() {
return `${location.protocol}//${location.host}/`;
}
export function redirectLogin() {
document.location.href = `/auth/authorize?response_type=code&client_id=${encodeURIComponent(genClientId())}&redirect_uri=${encodeURIComponent(location.toString())}`;
return new Promise((() => {}));
}
function fetchTokenRequest(code) {
const data = new FormData();
data.append('client_id', genClientId());
data.append('grant_type', 'authorization_code');
data.append('code', code);
return fetch('/auth/token', {
credentials: 'same-origin',
method: 'POST',
body: data,
}).then((resp) => {
if (!resp.ok) throw new Error('Unable to fetch tokens');
return resp.json().then((tokens) => {
tokens.expires = (tokens.expires_in * 1000) + Date.now();
return tokens;
});
});
}
function refreshTokenRequest(tokens) {
const data = new FormData();
data.append('client_id', genClientId());
data.append('grant_type', 'refresh_token');
data.append('refresh_token', tokens.refresh_token);
return fetch('/auth/token', {
credentials: 'same-origin',
method: 'POST',
body: data,
}).then((resp) => {
if (!resp.ok) throw new Error('Unable to fetch tokens');
return resp.json().then((newTokens) => {
newTokens.expires = (newTokens.expires_in * 1000) + Date.now();
return newTokens;
});
});
}
export function resolveCode(code) {
return fetchTokenRequest(code).then((tokens) => {
storeTokens(tokens);
history.replaceState(null, null, location.pathname);
return tokens;
}, (err) => {
// eslint-disable-next-line
console.error('Resolve token failed', err);
alert('Unable to fetch tokens');
redirectLogin();
});
}
export function refreshToken() {
const tokens = loadTokens();
if (tokens === null) {
return redirectLogin();
}
return refreshTokenRequest(tokens).then((accessTokenResp) => {
const newTokens = Object.assign({}, tokens, accessTokenResp);
storeTokens(newTokens);
return newTokens;
}, () => redirectLogin());
}

View File

@ -13,24 +13,26 @@ export function askWrite() {
return tokenCache.tokens !== undefined && tokenCache.writeEnabled === undefined;
}
export function storeTokens(tokens) {
export function saveTokens(tokens) {
tokenCache.tokens = tokens;
if (tokenCache.writeEnabled) {
try {
storage.tokens = JSON.stringify(tokens);
storage.hassTokens = JSON.stringify(tokens);
} catch (err) {} // eslint-disable-line
}
}
export function enableWrite() {
tokenCache.writeEnabled = true;
storeTokens(tokenCache.tokens);
saveTokens(tokenCache.tokens);
}
export function loadTokens() {
if (tokenCache.tokens === undefined) {
try {
const tokens = storage.tokens;
// Delete the old token cache.
delete storage.tokens;
const tokens = storage.hassTokens;
if (tokens) {
tokenCache.tokens = JSON.parse(tokens);
tokenCache.writeEnabled = true;

View File

@ -1,4 +1,4 @@
/** Return if a component is loaded. */
export default function isComponentLoaded(hass, component) {
return hass && hass.config.core.components.indexOf(component) !== -1;
return hass && hass.config.components.indexOf(component) !== -1;
}

View File

@ -1,4 +1,4 @@
/** Get the location name from a hass object. */
export default function computeLocationName(hass) {
return hass && hass.config.core.location_name;
return hass && hass.config.location_name;
}

View File

@ -1,5 +1,5 @@
export default function canToggleDomain(hass, domain) {
const services = hass.config.services[domain];
const services = hass.services[domain];
if (!services) { return false; }
if (domain === 'lock') {

View File

@ -1,11 +0,0 @@
export default function parseQuery(queryString) {
const query = {};
const items = queryString.split('&');
for (let i = 0; i < items.length; i++) {
const item = items[i].split('=');
const key = decodeURIComponent(item[0]);
const value = item.length > 1 ? decodeURIComponent(item[1]) : undefined;
query[key] = value;
}
return query;
}

View File

@ -60,7 +60,7 @@ class HaClimateState extends LocalizeMixin(PolymerElement) {
computeCurrentStatus(hass, stateObj) {
if (!hass || !stateObj) return null;
if (stateObj.attributes.current_temperature != null) {
return `${stateObj.attributes.current_temperature} ${hass.config.core.unit_system.temperature}`;
return `${stateObj.attributes.current_temperature} ${hass.config.unit_system.temperature}`;
}
if (stateObj.attributes.current_humidity != null) {
return `${stateObj.attributes.current_humidity} %`;
@ -73,9 +73,9 @@ class HaClimateState extends LocalizeMixin(PolymerElement) {
// We're using "!= null" on purpose so that we match both null and undefined.
if (stateObj.attributes.target_temp_low != null &&
stateObj.attributes.target_temp_high != null) {
return `${stateObj.attributes.target_temp_low} - ${stateObj.attributes.target_temp_high} ${hass.config.core.unit_system.temperature}`;
return `${stateObj.attributes.target_temp_low} - ${stateObj.attributes.target_temp_high} ${hass.config.unit_system.temperature}`;
} else if (stateObj.attributes.temperature != null) {
return `${stateObj.attributes.temperature} ${hass.config.core.unit_system.temperature}`;
return `${stateObj.attributes.temperature} ${hass.config.unit_system.temperature}`;
} else if (stateObj.attributes.target_humidity_low != null &&
stateObj.attributes.target_humidity_high != null) {
return `${stateObj.attributes.target_humidity_low} - ${stateObj.attributes.target_humidity_high} %`;

View File

@ -17,7 +17,7 @@ class HaServiceDescription extends PolymerElement {
}
_getDescription(hass, domain, service) {
var domainServices = hass.config.services[domain];
var domainServices = hass.services[domain];
if (!domainServices) return '';
var serviceObject = domainServices[service];
if (!serviceObject) return '';

View File

@ -34,13 +34,13 @@ class HaServicePicker extends LocalizeMixin(PolymerElement) {
if (!hass) {
this._services = [];
return;
} else if (oldHass && hass.config.services === oldHass.config.services) {
} else if (oldHass && hass.services === oldHass.services) {
return;
}
const result = [];
Object.keys(hass.config.services).sort().forEach((domain) => {
const services = Object.keys(hass.config.services[domain]).sort();
Object.keys(hass.services).sort().forEach((domain) => {
const services = Object.keys(hass.services[domain]).sort();
for (let i = 0; i < services.length; i++) {
result.push(`${domain}.${services[i]}`);

View File

@ -10,6 +10,7 @@ import '../components/ha-icon.js';
import '../util/hass-translation.js';
import LocalizeMixin from '../mixins/localize-mixin.js';
import isComponentLoaded from '../common/config/is_component_loaded.js';
/*
* @appliesMixin LocalizeMixin
@ -250,7 +251,7 @@ class HaSidebar extends LocalizeMixin(PolymerElement) {
}
_mqttLoaded(hass) {
return hass.config.core.components.indexOf('mqtt') !== -1;
return isComponentLoaded(hass, 'mqtt');
}
_computeUserName(user) {

10
src/data/ws-panels.js Normal file
View File

@ -0,0 +1,10 @@
import { createCollection } from 'home-assistant-js-websocket';
export const subscribePanels = (conn, onChange) =>
createCollection(
'_pnl',
conn_ => conn_.sendMessagePromise({ type: 'get_panels' }),
null,
conn,
onChange
);

20
src/data/ws-themes.js Normal file
View File

@ -0,0 +1,20 @@
import { createCollection } from 'home-assistant-js-websocket';
const fetchThemes = conn => conn.sendMessagePromise({
type: 'frontend/get_themes'
});
const subscribeUpdates = (conn, store) =>
conn.subscribeEvents(
event => store.setState(event.data, true),
'themes_updated'
);
export const subscribeThemes = (conn, onChange) =>
createCollection(
'_thm',
fetchThemes,
subscribeUpdates,
conn,
onChange
);

10
src/data/ws-user.js Normal file
View File

@ -0,0 +1,10 @@
import { createCollection, getUser } from 'home-assistant-js-websocket';
export const subscribeUser = (conn, onChange) =>
createCollection(
'_usr',
conn_ => getUser(conn_),
null,
conn,
onChange
);

View File

@ -125,13 +125,13 @@ class MoreInfoClimate extends LocalizeMixin(EventsMixin(PolymerElement)) {
<div class$="[[stateObj.attributes.operation_mode]]">
<div hidden$="[[!supportsTemperatureControls(stateObj)]]">[[localize('ui.card.climate.target_temperature')]]</div>
<template is="dom-if" if="[[supportsTemperature(stateObj)]]">
<ha-climate-control value="[[stateObj.attributes.temperature]]" units="[[hass.config.core.unit_system.temperature]]" step="[[computeTemperatureStepSize(hass, stateObj)]]" min="[[stateObj.attributes.min_temp]]" max="[[stateObj.attributes.max_temp]]" on-change="targetTemperatureChanged">
<ha-climate-control value="[[stateObj.attributes.temperature]]" units="[[hass.config.unit_system.temperature]]" step="[[computeTemperatureStepSize(hass, stateObj)]]" min="[[stateObj.attributes.min_temp]]" max="[[stateObj.attributes.max_temp]]" on-change="targetTemperatureChanged">
</ha-climate-control>
</template>
<template is="dom-if" if="[[supportsTemperatureRange(stateObj)]]">
<ha-climate-control value="[[stateObj.attributes.target_temp_low]]" units="[[hass.config.core.unit_system.temperature]]" step="[[computeTemperatureStepSize(hass, stateObj)]]" min="[[stateObj.attributes.min_temp]]" max="[[stateObj.attributes.target_temp_high]]" class="range-control-left" on-change="targetTemperatureLowChanged">
<ha-climate-control value="[[stateObj.attributes.target_temp_low]]" units="[[hass.config.unit_system.temperature]]" step="[[computeTemperatureStepSize(hass, stateObj)]]" min="[[stateObj.attributes.min_temp]]" max="[[stateObj.attributes.target_temp_high]]" class="range-control-left" on-change="targetTemperatureLowChanged">
</ha-climate-control>
<ha-climate-control value="[[stateObj.attributes.target_temp_high]]" units="[[hass.config.core.unit_system.temperature]]" step="[[computeTemperatureStepSize(hass, stateObj)]]" min="[[stateObj.attributes.target_temp_low]]" max="[[stateObj.attributes.max_temp]]" class="range-control-right" on-change="targetTemperatureHighChanged">
<ha-climate-control value="[[stateObj.attributes.target_temp_high]]" units="[[hass.config.unit_system.temperature]]" step="[[computeTemperatureStepSize(hass, stateObj)]]" min="[[stateObj.attributes.target_temp_low]]" max="[[stateObj.attributes.max_temp]]" class="range-control-right" on-change="targetTemperatureHighChanged">
</ha-climate-control>
</template>
</div>
@ -293,7 +293,7 @@ class MoreInfoClimate extends LocalizeMixin(EventsMixin(PolymerElement)) {
computeTemperatureStepSize(hass, stateObj) {
if (stateObj.attributes.target_temp_step) {
return stateObj.attributes.target_temp_step;
} else if (hass.config.core.unit_system.temperature.indexOf('F') !== -1) {
} else if (hass.config.unit_system.temperature.indexOf('F') !== -1) {
return 1;
}
return 0.5;

View File

@ -304,7 +304,7 @@ class MoreInfoMediaPlayer extends LocalizeMixin(EventsMixin(PolymerElement)) {
}
sendTTS() {
const services = this.hass.config.services.tts;
const services = this.hass.services.tts;
const serviceKeys = Object.keys(services).sort();
let service;
let i;

View File

@ -156,7 +156,7 @@ class MoreInfoWeather extends LocalizeMixin(PolymerElement) {
}
getUnit(measure) {
const lengthUnit = this.hass.config.core.unit_system.length || '';
const lengthUnit = this.hass.config.unit_system.length || '';
switch (measure) {
case 'air_pressure':
return lengthUnit === 'km' ? 'hPa' : 'inHg';
@ -165,7 +165,7 @@ class MoreInfoWeather extends LocalizeMixin(PolymerElement) {
case 'precipitation':
return lengthUnit === 'km' ? 'mm' : 'in';
default:
return this.hass.config.core.unit_system[measure] || '';
return this.hass.config.unit_system[measure] || '';
}
}

View File

@ -1,78 +1,39 @@
import {
ERR_INVALID_AUTH,
getAuth,
createConnection,
subscribeConfig,
subscribeEntities,
subscribeServices,
} from 'home-assistant-js-websocket';
import { redirectLogin, resolveCode, refreshToken } from '../common/auth/token.js';
// import refreshToken_ from '../common/auth/refresh_token.js';
import parseQuery from '../common/util/parse_query.js';
import { loadTokens } from '../common/auth/token_storage.js';
import { loadTokens, saveTokens } from '../common/auth/token_storage.js';
import { subscribePanels } from '../data/ws-panels.js';
import { subscribeThemes } from '../data/ws-themes.js';
import { subscribeUser } from '../data/ws-user.js';
const init = window.createHassConnection = function (password, accessToken) {
const proto = window.location.protocol === 'https:' ? 'wss' : 'ws';
const url = `${proto}://${window.location.host}/api/websocket?${__BUILD__}`;
const options = {
setupRetry: 10,
};
if (password) {
options.authToken = password;
} else if (accessToken) {
options.accessToken = accessToken.access_token;
options.expires = accessToken.expires;
window.hassAuth = getAuth({
hassUrl: `${location.protocol}//${location.host}`,
saveTokens,
loadTokens: () => Promise.resolve(loadTokens()),
});
window.hassConnection = window.hassAuth.then((auth) => {
if (location.search.includes('auth_callback=1')) {
history.replaceState(null, null, location.pathname);
}
return createConnection(url, options)
.then(function (conn) {
subscribeEntities(conn);
subscribeConfig(conn);
return conn;
});
};
return createConnection({ auth });
});
function main() {
if (location.search) {
const query = parseQuery(location.search.substr(1));
if (query.code) {
window.hassConnection = resolveCode(query.code).then(newTokens => init(null, newTokens));
return;
}
}
const tokens = loadTokens();
if (tokens == null) {
redirectLogin();
return;
}
if (Date.now() + 30000 > tokens.expires) {
// refresh access token if it will expire in 30 seconds to avoid invalid auth event
window.hassConnection = refreshToken().then(newTokens => init(null, newTokens));
return;
}
window.hassConnection = init(null, tokens).catch((err) => {
if (err !== ERR_INVALID_AUTH) throw err;
return refreshToken().then(newTokens => init(null, newTokens));
});
}
function mainLegacy() {
if (window.noAuth === '1') {
window.hassConnection = init();
} else if (window.localStorage.authToken) {
window.hassConnection = init(window.localStorage.authToken);
} else {
window.hassConnection = null;
}
}
if (window.useOAuth === '1') {
main();
} else {
mainLegacy();
}
// Start fetching some of the data that we will need.
window.hassConnection.then((conn) => {
const noop = () => {};
subscribeEntities(conn, noop);
subscribeConfig(conn, noop);
subscribeServices(conn, noop);
subscribePanels(conn, noop);
subscribeThemes(conn, noop);
subscribeUser(conn, noop);
});
window.addEventListener('error', (e) => {
const homeAssistant = document.querySelector('home-assistant');

View File

@ -1,6 +1,7 @@
import { afterNextRender } from '@polymer/polymer/lib/utils/render-status.js';
import { clearState } from '../../util/ha-pref-storage.js';
import { askWrite } from '../../common/auth/token_storage.js';
import { subscribeUser } from '../../data/ws-user.js';
export default superClass => class extends superClass {
ready() {
@ -20,17 +21,7 @@ export default superClass => class extends superClass {
hassConnected() {
super.hassConnected();
this._getCurrentUser();
}
_getCurrentUser() {
// only for new auth
if (this.hass.connection.options.accessToken) {
this.hass.callWS({
type: 'auth/current_user',
}).then(user => this._updateHass({ user }), () => {});
}
subscribeUser(this.hass.connection, user => this._updateHass({ user }));
}
_handleLogout() {

View File

@ -2,6 +2,8 @@ import {
ERR_INVALID_AUTH,
subscribeEntities,
subscribeConfig,
subscribeServices,
callService,
} from 'home-assistant-js-websocket';
import translationMetadata from '../../../build-translations/translationMetadata.json';
@ -9,44 +11,24 @@ import translationMetadata from '../../../build-translations/translationMetadata
import LocalizeMixin from '../../mixins/localize-mixin.js';
import EventsMixin from '../../mixins/events-mixin.js';
import { refreshToken } from '../../common/auth/token.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';
import { subscribePanels } from '../../data/ws-panels';
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);
}
this._handleConnProm();
}
async _handleNewConnProm(connProm) {
this.connectionPromise = connProm;
async _handleConnProm() {
const [auth, conn] = await Promise.all([window.hassAuth, window.hassConnection]);
let conn;
try {
conn = await connProm;
} catch (err) {
this.connectionPromise = null;
return;
}
this._setConnection(conn);
}
_setConnection(conn) {
this.hass = Object.assign({
auth,
connection: conn,
connected: true,
states: null,
@ -64,7 +46,7 @@ export default superClass =>
moreInfoEntityId: null,
callService: async (domain, service, serviceData = {}) => {
try {
await conn.callService(domain, service, serviceData);
await callService(conn, domain, service, serviceData);
let message;
let name;
@ -100,24 +82,20 @@ export default superClass =>
},
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 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 refreshToken();
conn.options.accessToken = accessToken.access_token;
conn.options.expires = accessToken.expires;
return await hassCallApi(host, auth, method, path, parameters);
try {
if (auth.expired) await auth.refreshAccessToken();
} catch (err) {
if (err === ERR_INVALID_AUTH) {
// Trigger auth flow
location.reload();
// ensure further JS is not executed
await new Promise(() => {});
}
throw err;
}
return await hassCallApi(host, auth, method, path, parameters);
},
// For messages that do not get a response
sendWS: (msg) => {
@ -138,9 +116,7 @@ export default superClass =>
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);
return resp;
},
}, getState());
@ -152,56 +128,26 @@ export default superClass =>
const conn = this.hass.connection;
const reconnected = () => this.hassReconnected();
const disconnected = () => this.hassDisconnected();
const reconnectError = async (_conn, err) => {
if (err !== ERR_INVALID_AUTH) return;
while (this.unsubFuncs.length) {
this.unsubFuncs.pop()();
}
const accessToken = await refreshToken();
const newConn = window.createHassConnection(null, accessToken);
newConn.then(() => this.hassReconnected());
this._handleNewConnProm(newConn);
};
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);
conn.addEventListener('ready', () => this.hassReconnected());
conn.addEventListener('disconnected', () => this.hassDisconnected());
// If we reconnect after losing connection and auth is no longer valid.
conn.addEventListener('reconnect-error', (_conn, err) => {
if (err === ERR_INVALID_AUTH) location.reload();
});
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();
subscribeEntities(conn, states => this._updateHass({ states }));
subscribeConfig(conn, config => this._updateHass({ config }));
subscribeServices(conn, services => this._updateHass({ services }));
subscribePanels(conn, panels => this._updateHass({ panels }));
}
hassReconnected() {
super.hassReconnected();
this._updateHass({ connected: true });
this._loadPanels();
}
hassDisconnected() {
super.hassDisconnected();
this._updateHass({ connected: false });
}
async _loadPanels() {
const panels = await this.hass.callWS({
type: 'get_panels'
});
this._updateHass({ panels });
}
};

View File

@ -6,6 +6,7 @@ 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 '../../layouts/ha-init-page.js';
import '../../resources/ha-style.js';
import registerServiceWorker from '../../util/register-service-worker.js';
@ -20,8 +21,6 @@ import ConnectionMixin from './connection-mixin.js';
import NotificationMixin from './notification-mixin.js';
import DisconnectToastMixin from './disconnect-toast-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, [
@ -52,21 +51,13 @@ class HomeAssistant extends ext(PolymerElement, [
</template>
<template is="dom-if" if="[[!showMain]]" restamp>
<login-form
hass="[[hass]]"
connection-promise="[[connectionPromise]]"
show-loading="[[computeShowLoading(connectionPromise, hass)]]"
></login-form>
<ha-init-page></ha-init-page>
</template>
`;
}
static get properties() {
return {
connectionPromise: {
type: Object,
value: null,
},
hass: {
type: Object,
value: null,
@ -91,13 +82,7 @@ class HomeAssistant extends ext(PolymerElement, [
}
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)));
return hass && hass.states && hass.config && hass.panels && hass.services;
}
computePanelUrl(routeData) {

View File

@ -1,46 +1,33 @@
import applyThemesOnElement from '../../common/dom/apply_themes_on_element.js';
import { storeState } from '../../util/ha-pref-storage.js';
import { subscribeThemes } from '../../data/ws-themes.js';
export default superClass => class extends superClass {
ready() {
super.ready();
this.addEventListener('settheme', e => this._setTheme(e));
this.addEventListener('settheme', (ev) => {
this._updateHass({ selectedTheme: ev.detail });
this._applyTheme();
storeState(this.hass);
});
}
hassConnected() {
super.hassConnected();
this.hass.callWS({
type: 'frontend/get_themes',
}).then((themes) => {
subscribeThemes(this.hass.connection, (themes) => {
this._updateHass({ themes });
applyThemesOnElement(
document.documentElement,
themes,
this.hass.selectedTheme,
true
);
this._applyTheme();
});
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 });
_applyTheme() {
applyThemesOnElement(
document.documentElement,
this.hass.themes,
this.hass.selectedTheme,
true
);
storeState(this.hass);
}
};

View File

@ -14,102 +14,24 @@ import EventsMixin from '../mixins/events-mixin.js';
/*
* @appliesMixin LocalizeMixin
*/
class LoginForm extends EventsMixin(LocalizeMixin(PolymerElement)) {
class HaInitPage extends EventsMixin(LocalizeMixin(PolymerElement)) {
static get template() {
return html`
<style include="iron-flex iron-positioning"></style>
<style>
:host {
white-space: nowrap;
}
paper-input {
display: block;
margin-bottom: 16px;
}
paper-checkbox {
margin-right: 8px;
}
paper-button {
margin-left: 72px;
}
.interact {
height: 125px;
}
#validatebox {
margin-top: 16px;
text-align: center;
}
.validatemessage {
margin-top: 10px;
paper-spinner {
margin-bottom: 10px;
}
</style>
<div class="layout vertical center center-center fit">
<img src="/static/icons/favicon-192x192.png" height="192">
<a href="#" id="hideKeyboardOnFocus"></a>
<div class="interact">
<div id="loginform" hidden$="[[showSpinner]]">
<paper-input id="passwordInput" label="[[localize('ui.login-form.password')]]" type="password" autofocus="" invalid="[[errorMessage]]" error-message="[[errorMessage]]" value="{{password}}"></paper-input>
<div class="layout horizontal center">
<paper-checkbox for="" id="rememberLogin">[[localize('ui.login-form.remember')]]</paper-checkbox>
<paper-button on-click="validatePassword">[[localize('ui.login-form.log_in')]]</paper-button>
</div>
</div>
<div id="validatebox" hidden$="[[!showSpinner]]">
<paper-spinner active="true"></paper-spinner><br>
<div class="validatemessage">[[computeLoadingMsg(isValidating)]]</div>
</div>
</div>
<paper-spinner active="true"></paper-spinner>
Loading data
</div>
`;
}
static get properties() {
return {
hass: {
type: Object,
},
connectionPromise: {
type: Object,
notify: true,
observer: 'handleConnectionPromiseChanged',
},
errorMessage: {
type: String,
value: '',
},
isValidating: {
type: Boolean,
observer: 'isValidatingChanged',
value: false,
},
showLoading: {
type: Boolean,
value: false,
},
showSpinner: {
type: Boolean,
computed: 'computeShowSpinner(showLoading, isValidating)',
},
password: {
type: String,
value: '',
},
};
}
ready() {
super.ready();
this.addEventListener('keydown', ev => this.passwordKeyDown(ev));
@ -183,4 +105,4 @@ class LoginForm extends EventsMixin(LocalizeMixin(PolymerElement)) {
}
}
customElements.define('login-form', LoginForm);
customElements.define('ha-init-page', HaInitPage);

View File

@ -122,10 +122,10 @@ class HaPanelDevInfo extends PolymerElement {
<p class='version'>
<a href='https://www.home-assistant.io'><img src="/static/icons/favicon-192x192.png" height="192" /></a><br />
Home Assistant<br />
[[hass.config.core.version]]
[[hass.config.version]]
</p>
<p>
Path to configuration.yaml: [[hass.config.core.config_dir]]
Path to configuration.yaml: [[hass.config.config_dir]]
</p>
<p class='develop'>
<a href='https://www.home-assistant.io/developers/credits/' target='_blank'>

View File

@ -230,7 +230,7 @@ class HaPanelDevService extends PolymerElement {
}
_computeAttributesArray(hass, domain, service) {
const serviceDomains = hass.config.services;
const serviceDomains = hass.services;
if (!(domain in serviceDomains)) return [];
if (!(service in serviceDomains[domain])) return [];
@ -241,7 +241,7 @@ class HaPanelDevService extends PolymerElement {
}
_computeDescription(hass, domain, service) {
const serviceDomains = hass.config.services;
const serviceDomains = hass.services;
if (!(domain in serviceDomains)) return undefined;
if (!(service in serviceDomains[domain])) return undefined;
return serviceDomains[domain][service].description;

View File

@ -162,7 +162,7 @@ class HuiMapCard extends PolymerElement {
const zoom = this._config.default_zoom;
if (this._mapItems.length === 0) {
this._map.setView(
new Leaflet.LatLng(this.hass.config.core.latitude, this.hass.config.core.longitude),
new Leaflet.LatLng(this.hass.config.latitude, this.hass.config.longitude),
zoom || 14
);
return;

View File

@ -79,7 +79,7 @@ class HaPanelMap extends LocalizeMixin(PolymerElement) {
if (this._mapItems.length === 0) {
this._map.setView(
new Leaflet.LatLng(this.hass.config.core.latitude, this.hass.config.core.longitude),
new Leaflet.LatLng(this.hass.config.latitude, this.hass.config.longitude),
14
);
} else {

View File

@ -4,12 +4,7 @@ export default function hassCallApi(host, auth, method, path, parameters) {
return new Promise(function (resolve, reject) {
var req = new XMLHttpRequest();
req.open(method, url, true);
if (auth.authToken) {
req.setRequestHeader('X-HA-access', auth.authToken);
} else if (auth.accessToken) {
req.setRequestHeader('authorization', `Bearer ${auth.accessToken}`);
}
req.setRequestHeader('authorization', `Bearer ${auth.accessToken}`);
req.onload = function () {
let body = req.responseText;

View File

@ -4,19 +4,17 @@ import canToggleDomain from '../../../src/common/entity/can_toggle_domain';
describe('canToggleDomain', () => {
const hass = {
config: {
services: {
light: {
turn_on: null, // Service keys only need to be present for test
turn_off: null,
},
lock: {
lock: null,
unlock: null,
},
sensor: {
custom_service: null,
},
services: {
light: {
turn_on: null, // Service keys only need to be present for test
turn_off: null,
},
lock: {
lock: null,
unlock: null,
},
sensor: {
custom_service: null,
},
},
};

View File

@ -4,12 +4,10 @@ import canToggleState from '../../../src/common/entity/can_toggle_state';
describe('canToggleState', () => {
const hass = {
config: {
services: {
light: {
turn_on: null, // Service keys only need to be present for test
turn_off: null,
},
services: {
light: {
turn_on: null, // Service keys only need to be present for test
turn_off: null,
},
},
};

View File

@ -4,12 +4,10 @@ import stateCardType from '../../../src/common/entity/state_card_type.js';
describe('stateCardType', () => {
const hass = {
config: {
services: {
light: {
turn_on: null, // Service keys only need to be present for test
turn_off: null,
},
services: {
light: {
turn_on: null, // Service keys only need to be present for test
turn_off: null,
},
},
};

View File

@ -1,18 +0,0 @@
import { assert } from 'chai';
import parseQuery from '../../../src/common/util/parse_query.js';
describe('parseQuery', () => {
it('works', () => {
assert.deepEqual(parseQuery('hello=world'), { hello: 'world' });
assert.deepEqual(parseQuery('hello=world&drink=soda'), {
hello: 'world',
drink: 'soda',
});
assert.deepEqual(parseQuery('hello=world&no_value&drink=soda'), {
hello: 'world',
no_value: undefined,
drink: 'soda',
});
});
});

View File

@ -6513,9 +6513,9 @@ hoek@4.x.x:
version "4.2.1"
resolved "https://registry.yarnpkg.com/hoek/-/hoek-4.2.1.tgz#9634502aa12c445dd5a7c5734b572bb8738aacbb"
home-assistant-js-websocket@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/home-assistant-js-websocket/-/home-assistant-js-websocket-2.1.0.tgz#192f4e8cef248882bc62b70d56a12e8113d41c3b"
home-assistant-js-websocket@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/home-assistant-js-websocket/-/home-assistant-js-websocket-3.0.0.tgz#498828a29827bdd1f3e99cf3b5e152694cededbf"
home-or-tmp@^2.0.0:
version "2.0.0"