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:
parent
ab19dbc35e
commit
45cdb5a3e4
|
@ -0,0 +1 @@
|
|||
hassio-icons.html
|
|
@ -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",
|
||||
|
|
|
@ -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] || '';
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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());
|
||||
}
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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') {
|
||||
|
|
|
@ -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;
|
||||
}
|
|
@ -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} %`;
|
||||
|
|
|
@ -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 '';
|
||||
|
|
|
@ -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]}`);
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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
|
||||
);
|
|
@ -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
|
||||
);
|
|
@ -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
|
||||
);
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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] || '';
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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');
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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 });
|
||||
}
|
||||
};
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
};
|
||||
|
|
|
@ -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);
|
|
@ -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'>
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
|
@ -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',
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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"
|
||||
|
|
Loading…
Reference in New Issue