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:
Paulus Schoutsen 2018-05-18 13:25:01 -04:00 committed by GitHub
parent 964ada87b7
commit e57d9f7751
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 353 additions and 26 deletions

View File

@ -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",

View File

@ -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';

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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 };
}

View File

@ -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]),

View File

@ -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;

View File

@ -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));
});

View File

@ -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;

View File

@ -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.

View File

@ -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));
});
});

View File

@ -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));
});
});

View File

@ -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));
});
});

View File

@ -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);
});
});

View File

@ -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;
}

View File

@ -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>

View File

@ -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"