Add websocket helpers to polymer (#1187)
* Add websocket helpers to polymer * Lint * Upgrade to home-assistant-js-websocket@2.0.0
This commit is contained in:
parent
964ada87b7
commit
e57d9f7751
|
@ -69,7 +69,7 @@
|
|||
"chartjs-chart-timeline": "0.2.0",
|
||||
"es6-object-assign": "^1.1.0",
|
||||
"fecha": "^2.3.3",
|
||||
"home-assistant-js-websocket": "^1.2.1",
|
||||
"home-assistant-js-websocket": "2.0.0",
|
||||
"intl-messageformat": "^2.2.0",
|
||||
"leaflet": "^1.0.2",
|
||||
"marked": "^0.3.19",
|
||||
|
|
|
@ -40,3 +40,6 @@ export const STATES_OFF = [
|
|||
/** Temperature units. */
|
||||
export const UNIT_C = '°C';
|
||||
export const UNIT_F = '°F';
|
||||
|
||||
/** Entity ID of the default view. */
|
||||
export const DEFAULT_VIEW_ENTITY_ID = 'group.default_view';
|
||||
|
|
|
@ -0,0 +1,24 @@
|
|||
import { DEFAULT_VIEW_ENTITY_ID } from '../const.js';
|
||||
|
||||
// Return an ordered array of available views
|
||||
export default function extractViews(entities) {
|
||||
const views = [];
|
||||
|
||||
Object.keys(entities).forEach((entityId) => {
|
||||
const entity = entities[entityId];
|
||||
if (entity.attributes.view) {
|
||||
views.push(entity);
|
||||
}
|
||||
});
|
||||
|
||||
views.sort((view1, view2) => {
|
||||
if (view1.entity_id === DEFAULT_VIEW_ENTITY_ID) {
|
||||
return -1;
|
||||
} else if (view2.entity_id === DEFAULT_VIEW_ENTITY_ID) {
|
||||
return 1;
|
||||
}
|
||||
return view1.attributes.order - view2.attributes.order;
|
||||
});
|
||||
|
||||
return views;
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
export default function getGroupEntities(entities, group) {
|
||||
const result = {};
|
||||
|
||||
group.attributes.entity_id.forEach((entityId) => {
|
||||
const entity = entities[entityId];
|
||||
|
||||
if (entity) {
|
||||
result[entity.entity_id] = entity;
|
||||
}
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
|
@ -0,0 +1,30 @@
|
|||
import computeDomain from './compute_domain.js';
|
||||
import getGroupEntities from './get_group_entities.js';
|
||||
|
||||
// Return an object containing all entities that the view will show
|
||||
// including embedded groups.
|
||||
export default function getViewEntities(entities, view) {
|
||||
const viewEntities = {};
|
||||
|
||||
view.attributes.entity_id.forEach((entityId) => {
|
||||
const entity = entities[entityId];
|
||||
|
||||
if (entity && !entity.attributes.hidden) {
|
||||
viewEntities[entity.entity_id] = entity;
|
||||
|
||||
if (computeDomain(entity.entity_id) === 'group') {
|
||||
const groupEntities = getGroupEntities(entities, entity);
|
||||
|
||||
Object.keys(groupEntities).forEach((grEntityId) => {
|
||||
const grEntity = groupEntities[grEntityId];
|
||||
|
||||
if (!grEntity.attributes.hidden) {
|
||||
viewEntities[grEntityId] = grEntity;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return viewEntities;
|
||||
}
|
|
@ -0,0 +1,24 @@
|
|||
import computeDomain from './compute_domain.js';
|
||||
|
||||
// Split a collection into a list of groups and a 'rest' list of ungrouped
|
||||
// entities.
|
||||
// Returns { groups: [], ungrouped: {} }
|
||||
export default function splitByGroups(entities) {
|
||||
const groups = [];
|
||||
const ungrouped = {};
|
||||
|
||||
Object.keys(entities).forEach((entityId) => {
|
||||
const entity = entities[entityId];
|
||||
|
||||
if (computeDomain(entityId) === 'group') {
|
||||
groups.push(entity);
|
||||
} else {
|
||||
ungrouped[entityId] = entity;
|
||||
}
|
||||
});
|
||||
|
||||
groups.forEach(group =>
|
||||
group.attributes.entity_id.forEach((entityId) => { delete ungrouped[entityId]; }));
|
||||
|
||||
return { groups, ungrouped };
|
||||
}
|
|
@ -9,6 +9,8 @@ import '../cards/ha-card-chooser.js';
|
|||
import './ha-demo-badge.js';
|
||||
|
||||
import computeStateDomain from '../common/entity/compute_state_domain.js';
|
||||
import splitByGroups from '../common/entity/split_by_groups.js';
|
||||
import getGroupEntities from '../common/entity/get_group_entities.js';
|
||||
|
||||
{
|
||||
// mapping domain to size of the card.
|
||||
|
@ -281,7 +283,7 @@ import computeStateDomain from '../common/entity/compute_state_domain.js';
|
|||
});
|
||||
}
|
||||
|
||||
const splitted = window.HAWS.splitByGroups(states);
|
||||
const splitted = splitByGroups(states);
|
||||
if (orderedGroupEntities) {
|
||||
splitted.groups.sort((gr1, gr2) => orderedGroupEntities[gr1.entity_id] -
|
||||
orderedGroupEntities[gr2.entity_id]);
|
||||
|
@ -344,7 +346,7 @@ import computeStateDomain from '../common/entity/compute_state_domain.js';
|
|||
});
|
||||
|
||||
splitted.groups.forEach((groupState) => {
|
||||
const entities = window.HAWS.getGroupEntities(states, groupState);
|
||||
const entities = getGroupEntities(states, groupState);
|
||||
addEntitiesCard(
|
||||
groupState.entity_id,
|
||||
Object.keys(entities).map(key => entities[key]),
|
||||
|
|
|
@ -9,6 +9,12 @@ 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 {
|
||||
ERR_INVALID_AUTH,
|
||||
subscribeEntities,
|
||||
subscribeConfig,
|
||||
} from 'home-assistant-js-websocket';
|
||||
|
||||
import translationMetadata from '../../build-translations/translationMetadata.json';
|
||||
import '../layouts/home-assistant-main.js';
|
||||
import '../layouts/login-form.js';
|
||||
|
@ -221,7 +227,7 @@ class HomeAssistant extends PolymerElement {
|
|||
// If we reconnect after losing connection and access token is no longer
|
||||
// valid.
|
||||
conn.addEventListener('reconnect-error', (_conn, err) => {
|
||||
if (err !== window.HAWS.ERR_INVALID_AUTH) return;
|
||||
if (err !== ERR_INVALID_AUTH) return;
|
||||
disconnected();
|
||||
this.unsubConnection();
|
||||
window.refreshToken().then(accessToken =>
|
||||
|
@ -231,7 +237,7 @@ class HomeAssistant extends PolymerElement {
|
|||
|
||||
var unsubEntities;
|
||||
|
||||
window.HAWS.subscribeEntities(conn, (states) => {
|
||||
subscribeEntities(conn, (states) => {
|
||||
this._updateHass({ states: states });
|
||||
}).then(function (unsub) {
|
||||
unsubEntities = unsub;
|
||||
|
@ -239,7 +245,7 @@ class HomeAssistant extends PolymerElement {
|
|||
|
||||
var unsubConfig;
|
||||
|
||||
window.HAWS.subscribeConfig(conn, (config) => {
|
||||
subscribeConfig(conn, (config) => {
|
||||
this._updateHass({ config: config });
|
||||
}).then(function (unsub) {
|
||||
unsubConfig = unsub;
|
||||
|
|
|
@ -1,11 +1,14 @@
|
|||
import * as HAWS from 'home-assistant-js-websocket';
|
||||
import {
|
||||
ERR_INVALID_AUTH,
|
||||
createConnection,
|
||||
subscribeConfig,
|
||||
subscribeEntities,
|
||||
} from 'home-assistant-js-websocket';
|
||||
|
||||
import fetchToken from '../common/auth/fetch_token.js';
|
||||
import refreshToken_ from '../common/auth/refresh_token.js';
|
||||
import parseQuery from '../common/util/parse_query.js';
|
||||
|
||||
window.HAWS = HAWS;
|
||||
|
||||
const init = window.createHassConnection = function (password, accessToken) {
|
||||
const proto = window.location.protocol === 'https:' ? 'wss' : 'ws';
|
||||
const url = `${proto}://${window.location.host}/api/websocket?${__BUILD__}`;
|
||||
|
@ -17,10 +20,10 @@ const init = window.createHassConnection = function (password, accessToken) {
|
|||
} else if (accessToken) {
|
||||
options.accessToken = accessToken;
|
||||
}
|
||||
return HAWS.createConnection(url, options)
|
||||
return createConnection(url, options)
|
||||
.then(function (conn) {
|
||||
HAWS.subscribeEntities(conn);
|
||||
HAWS.subscribeConfig(conn);
|
||||
subscribeEntities(conn);
|
||||
subscribeConfig(conn);
|
||||
return conn;
|
||||
});
|
||||
};
|
||||
|
@ -61,7 +64,7 @@ function main() {
|
|||
if (localStorage.tokens) {
|
||||
window.tokens = JSON.parse(localStorage.tokens);
|
||||
window.hassConnection = init(null, window.tokens.access_token).catch((err) => {
|
||||
if (err !== HAWS.ERR_INVALID_AUTH) throw err;
|
||||
if (err !== ERR_INVALID_AUTH) throw err;
|
||||
|
||||
return window.refreshToken().then(accessToken => init(null, accessToken));
|
||||
});
|
||||
|
|
|
@ -5,6 +5,8 @@ import '@polymer/paper-input/paper-input.js';
|
|||
import '@polymer/paper-spinner/paper-spinner.js';
|
||||
import { html } from '@polymer/polymer/lib/utils/html-tag.js';
|
||||
import { PolymerElement } from '@polymer/polymer/polymer-element.js';
|
||||
import { ERR_CANNOT_CONNECT, ERR_INVALID_AUTH } from 'home-assistant-js-websocket';
|
||||
|
||||
|
||||
import LocalizeMixin from '../mixins/localize-mixin.js';
|
||||
|
||||
|
@ -172,9 +174,9 @@ class LoginForm extends LocalizeMixin(PolymerElement) {
|
|||
function (errCode) {
|
||||
el.isValidating = false;
|
||||
|
||||
if (errCode === window.HAWS.ERR_CANNOT_CONNECT) {
|
||||
if (errCode === ERR_CANNOT_CONNECT) {
|
||||
el.errorMessage = 'Unable to connect';
|
||||
} else if (errCode === window.HAWS.ERR_INVALID_AUTH) {
|
||||
} else if (errCode === ERR_INVALID_AUTH) {
|
||||
el.errorMessage = 'Invalid password';
|
||||
} else {
|
||||
el.errorMessage = 'Unknown error: ' + errCode;
|
||||
|
|
|
@ -16,6 +16,8 @@ import '../components/ha-start-voice-button.js';
|
|||
|
||||
import './ha-app-layout.js';
|
||||
|
||||
import extractViews from '../common/entity/extract_views.js';
|
||||
import getViewEntities from '../common/entity/get_view_entities.js';
|
||||
import computeStateName from '../common/entity/compute_state_name.js';
|
||||
import computeStateDomain from '../common/entity/compute_state_domain.js';
|
||||
import computeLocationName from '../common/config/location_name.js';
|
||||
|
@ -268,7 +270,7 @@ import EventsMixin from '../mixins/events-mixin.js';
|
|||
|
||||
hassChanged(hass) {
|
||||
if (!hass) return;
|
||||
const views = window.HAWS.extractViews(hass.states);
|
||||
const views = extractViews(hass.states);
|
||||
let defaultView = null;
|
||||
// If default view present, it's in first index.
|
||||
if (views.length > 0 && views[0].entity_id === DEFAULT_VIEW_ENTITY_ID) {
|
||||
|
@ -311,9 +313,9 @@ import EventsMixin from '../mixins/events-mixin.js';
|
|||
|
||||
let states;
|
||||
if (currentView) {
|
||||
states = window.HAWS.getViewEntities(hass.states, hass.states[currentView]);
|
||||
states = getViewEntities(hass.states, hass.states[currentView]);
|
||||
} else {
|
||||
states = window.HAWS.getViewEntities(hass.states, hass.states[DEFAULT_VIEW_ENTITY_ID]);
|
||||
states = getViewEntities(hass.states, hass.states[DEFAULT_VIEW_ENTITY_ID]);
|
||||
}
|
||||
|
||||
// Make sure certain domains are always shown.
|
||||
|
|
|
@ -0,0 +1,32 @@
|
|||
import assert from 'assert';
|
||||
|
||||
import extractViews from '../../../src/common/entity/extract_views.js';
|
||||
|
||||
import {
|
||||
createEntities,
|
||||
createView,
|
||||
} from './test_util';
|
||||
|
||||
describe('extractViews', () => {
|
||||
it('should work', () => {
|
||||
const entities = createEntities(10);
|
||||
const view1 = createView({ attributes: { order: 10 } });
|
||||
entities[view1.entity_id] = view1;
|
||||
|
||||
const view2 = createView({ attributes: { order: 2 } });
|
||||
entities[view2.entity_id] = view2;
|
||||
|
||||
const view3 = createView({
|
||||
entity_id: 'group.default_view',
|
||||
attributes: { order: 8 }
|
||||
});
|
||||
entities[view3.entity_id] = view3;
|
||||
|
||||
const view4 = createView({ attributes: { order: 4 } });
|
||||
entities[view4.entity_id] = view4;
|
||||
|
||||
const expected = [view3, view2, view4, view1];
|
||||
|
||||
assert.deepEqual(expected, extractViews(entities));
|
||||
});
|
||||
});
|
|
@ -0,0 +1,31 @@
|
|||
import assert from 'assert';
|
||||
|
||||
import getGroupEntities from '../../../src/common/entity/get_group_entities.js';
|
||||
|
||||
import { createEntities, createGroup, entityMap } from './test_util';
|
||||
|
||||
describe('getGroupEntities', () => {
|
||||
it('works if all entities exist', () => {
|
||||
const entities = createEntities(5);
|
||||
const entityIds = Object.keys(entities);
|
||||
|
||||
const group = createGroup({ attributes: { entity_id: entityIds.splice(0, 2) } });
|
||||
|
||||
const groupEntities = entityMap(group.attributes.entity_id.map(ent => entities[ent]));
|
||||
assert.deepEqual(groupEntities, getGroupEntities(entities, group));
|
||||
});
|
||||
|
||||
it("works if one entity doesn't exist", () => {
|
||||
const entities = createEntities(5);
|
||||
const entityIds = Object.keys(entities);
|
||||
|
||||
const groupEntities = entityMap([
|
||||
entities[entityIds[0]],
|
||||
entities[entityIds[1]],
|
||||
]);
|
||||
|
||||
const group = createGroup({ attributes: { entity_id: entityIds.splice(0, 2).concat('light.does_not_exist') } });
|
||||
|
||||
assert.deepEqual(groupEntities, getGroupEntities(entities, group));
|
||||
});
|
||||
});
|
|
@ -0,0 +1,68 @@
|
|||
import assert from 'assert';
|
||||
|
||||
import getViewEntities from '../../../src/common/entity/get_view_entities.js';
|
||||
|
||||
import {
|
||||
createEntities,
|
||||
createEntity,
|
||||
createGroup,
|
||||
createView,
|
||||
entityMap
|
||||
} from './test_util';
|
||||
|
||||
describe('getViewEntities', () => {
|
||||
it('should work', () => {
|
||||
const entities = createEntities(10);
|
||||
const entityIds = Object.keys(entities);
|
||||
|
||||
const group1 = createGroup({ attributes: { entity_id: entityIds.splice(0, 2) } });
|
||||
entities[group1.entity_id] = group1;
|
||||
|
||||
const group2 = createGroup({ attributes: { entity_id: entityIds.splice(0, 3) } });
|
||||
entities[group2.entity_id] = group2;
|
||||
|
||||
const view = createView({
|
||||
attributes: {
|
||||
entity_id: [group1.entity_id, group2.entity_id].concat(entityIds.splice(0, 2))
|
||||
}
|
||||
});
|
||||
|
||||
const expectedEntities = entityMap(view.attributes.entity_id.map(ent => entities[ent]));
|
||||
Object.assign(
|
||||
expectedEntities,
|
||||
entityMap(group1.attributes.entity_id.map(ent => entities[ent]))
|
||||
);
|
||||
Object.assign(
|
||||
expectedEntities,
|
||||
entityMap(group2.attributes.entity_id.map(ent => entities[ent]))
|
||||
);
|
||||
|
||||
assert.deepEqual(expectedEntities, getViewEntities(entities, view));
|
||||
});
|
||||
|
||||
it('should not include hidden entities inside groups', () => {
|
||||
const visibleEntity = createEntity({ attributes: { hidden: false } });
|
||||
const hiddenEntity = createEntity({ attributes: { hidden: true } });
|
||||
const group1 = createGroup({ attributes: { entity_id: [
|
||||
visibleEntity.entity_id, hiddenEntity.entity_id] } });
|
||||
|
||||
const entities = {
|
||||
[visibleEntity.entity_id]: visibleEntity,
|
||||
[hiddenEntity.entity_id]: hiddenEntity,
|
||||
[group1.entity_id]: group1,
|
||||
};
|
||||
|
||||
const view = createView({
|
||||
attributes: {
|
||||
entity_id: [group1.entity_id],
|
||||
},
|
||||
});
|
||||
|
||||
const expectedEntities = {
|
||||
[visibleEntity.entity_id]: visibleEntity,
|
||||
[group1.entity_id]: group1,
|
||||
};
|
||||
|
||||
assert.deepEqual(expectedEntities, getViewEntities(entities, view));
|
||||
});
|
||||
});
|
|
@ -0,0 +1,38 @@
|
|||
import assert from 'assert';
|
||||
|
||||
import splitByGroups from '../../../src/common/entity/split_by_groups.js';
|
||||
|
||||
import { createEntities, createGroup, entityMap } from './test_util';
|
||||
|
||||
describe('splitByGroups', () => {
|
||||
it('splitByGroups splits correctly', () => {
|
||||
const entities = createEntities(7);
|
||||
const entityIds = Object.keys(entities);
|
||||
|
||||
const group1 = createGroup({
|
||||
attributes: {
|
||||
entity_id: entityIds.splice(0, 2),
|
||||
order: 6,
|
||||
},
|
||||
});
|
||||
entities[group1.entity_id] = group1;
|
||||
|
||||
const group2 = createGroup({
|
||||
attributes: {
|
||||
entity_id: entityIds.splice(0, 3),
|
||||
order: 4,
|
||||
},
|
||||
});
|
||||
entities[group2.entity_id] = group2;
|
||||
|
||||
const result = splitByGroups(entities);
|
||||
result.groups.sort((gr1, gr2) => gr1.attributes.order - gr2.attributes.order);
|
||||
|
||||
const expected = {
|
||||
groups: [group2, group1],
|
||||
ungrouped: entityMap(entityIds.map(ent => entities[ent])),
|
||||
};
|
||||
|
||||
assert.deepEqual(expected, result);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,54 @@
|
|||
/* eslint-disable camelcase, no-param-reassign */
|
||||
let mockState = 1;
|
||||
|
||||
export function createEntity(entity) {
|
||||
mockState++;
|
||||
entity.entity_id = entity.entity_id || `test.test_${mockState}`;
|
||||
entity.last_changed = entity.last_changed || (new Date()).toISOString();
|
||||
entity.last_updated = entity.last_updated || entity.last_changed;
|
||||
entity.attributes = entity.attributes || {};
|
||||
return entity;
|
||||
}
|
||||
|
||||
export function createGroup(entity) {
|
||||
mockState++;
|
||||
entity.entity_id = entity.entity_id || `group.test_${mockState}`;
|
||||
entity.state = entity.state || 'on';
|
||||
entity.attributes = entity.attributes || {};
|
||||
if (!('order' in entity.attributes)) {
|
||||
entity.attributes.order = 0;
|
||||
}
|
||||
return createEntity(entity);
|
||||
}
|
||||
|
||||
export function createView(entity) {
|
||||
entity.attributes = entity.attributes || {};
|
||||
entity.attributes.view = true;
|
||||
return createGroup(entity);
|
||||
}
|
||||
|
||||
export function createLightEntity(isOn) {
|
||||
mockState++;
|
||||
if (isOn === undefined) {
|
||||
isOn = Math.random() > 0.5;
|
||||
}
|
||||
return createEntity({
|
||||
entity_id: `light.mock_${mockState}`,
|
||||
state: isOn ? 'on' : 'off',
|
||||
});
|
||||
}
|
||||
|
||||
export function createEntities(count) {
|
||||
const entities = {};
|
||||
for (let i = 0; i < count; i++) {
|
||||
const entity = createLightEntity();
|
||||
entities[entity.entity_id] = entity;
|
||||
}
|
||||
return entities;
|
||||
}
|
||||
|
||||
export function entityMap(entityList) {
|
||||
const entities = {};
|
||||
entityList.forEach((entity) => { entities[entity.entity_id] = entity; });
|
||||
return entities;
|
||||
}
|
|
@ -4,11 +4,6 @@
|
|||
<script src="../node_modules/@webcomponents/webcomponentsjs/webcomponents-bundle.js"></script>
|
||||
<script src="../node_modules/wct-browser-legacy/browser.js"></script>
|
||||
|
||||
<!--
|
||||
Temporarily load core.js here so window.HAWS is available. We can remove
|
||||
this once hass-util includes the helper function directly.
|
||||
-->
|
||||
<script src="../build/core.js"></script>
|
||||
<script type="module" src="../src/state-summary/state-card-display.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
|
|
|
@ -6709,9 +6709,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@^1.2.1:
|
||||
version "1.2.1"
|
||||
resolved "https://registry.yarnpkg.com/home-assistant-js-websocket/-/home-assistant-js-websocket-1.2.1.tgz#11229bed179da6b6e32e0c269793787a162748bf"
|
||||
home-assistant-js-websocket@2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/home-assistant-js-websocket/-/home-assistant-js-websocket-2.0.0.tgz#36dd29d6cf525efff7e463f0770c56c09536a829"
|
||||
|
||||
home-or-tmp@^2.0.0:
|
||||
version "2.0.0"
|
||||
|
|
Loading…
Reference in New Issue