Generate Lovelace config on the fly (#2091)

* Generate Lovelace config on the fly

* Disable editing

* Fix domain name title rendering
This commit is contained in:
Paulus Schoutsen 2018-11-23 17:39:50 +01:00 committed by GitHub
parent 785ed6f9db
commit d41a4cf78b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 266 additions and 13 deletions

View File

@ -1,14 +1,15 @@
import { HassEntities, HassEntity } from "home-assistant-js-websocket";
import { HassEntities } from "home-assistant-js-websocket";
import { DEFAULT_VIEW_ENTITY_ID } from "../const";
import { GroupEntity } from "../../types";
// Return an ordered array of available views
export default function extractViews(entities: HassEntities): HassEntity[] {
const views: HassEntity[] = [];
export default function extractViews(entities: HassEntities): GroupEntity[] {
const views: GroupEntity[] = [];
Object.keys(entities).forEach((entityId) => {
const entity = entities[entityId];
if (entity.attributes.view) {
views.push(entity);
views.push(entity as GroupEntity);
}
});

View File

@ -8,7 +8,7 @@ import { GroupEntity } from "../../types";
export default function getViewEntities(
entities: HassEntities,
view: GroupEntity
) {
): HassEntities {
const viewEntities = {};
view.attributes.entity_id.forEach((entityId) => {

View File

@ -1,18 +1,19 @@
import computeDomain from "./compute_domain";
import { HassEntity, HassEntities } from "home-assistant-js-websocket";
import { HassEntities } from "home-assistant-js-websocket";
import { GroupEntity } from "../../types";
// Split a collection into a list of groups and a 'rest' list of ungrouped
// entities.
// Returns { groups: [], ungrouped: {} }
export default function splitByGroups(entities: HassEntities) {
const groups: HassEntity[] = [];
const groups: GroupEntity[] = [];
const ungrouped: HassEntities = {};
Object.keys(entities).forEach((entityId) => {
const entity = entities[entityId];
if (computeDomain(entityId) === "group") {
groups.push(entity);
groups.push(entity as GroupEntity);
} else {
ungrouped[entityId] = entity;
}

View File

@ -0,0 +1,236 @@
import { HomeAssistant, GroupEntity } from "../../../types";
import { HassEntity, HassEntities } from "home-assistant-js-websocket";
import extractViews from "../../../common/entity/extract_views";
import getViewEntities from "../../../common/entity/get_view_entities";
import computeStateName from "../../../common/entity/compute_state_name";
import splitByGroups from "../../../common/entity/split_by_groups";
import computeObjectId from "../../../common/entity/compute_object_id";
import computeStateDomain from "../../../common/entity/compute_state_domain";
import { LocalizeFunc } from "../../../mixins/localize-base-mixin";
interface CardConfig {
id?: string;
type: string;
[key: string]: any;
}
interface ViewConfig {
title?: string;
badges?: string[];
cards?: CardConfig[];
id?: string;
icon?: string;
}
interface LovelaceConfig {
_frontendAuto: boolean;
title?: string;
views: ViewConfig[];
}
const DEFAULT_VIEW_ENTITY_ID = "group.default_view";
const DOMAINS_BADGES = [
"binary_sensor",
"device_tracker",
"mailbox",
"sensor",
"sun",
"timer",
];
const HIDE_DOMAIN = new Set(["persistent_notification", "configurator"]);
const computeCards = (title: string, states: HassEntity[]): CardConfig[] => {
const cards: CardConfig[] = [];
// For entity card
const entities: string[] = [];
states.forEach((stateObj) => {
const domain = computeStateDomain(stateObj);
if (domain === "alarm_control_panel") {
cards.push({
type: "alarm-panel",
entity: stateObj.entity_id,
});
} else if (domain === "climate") {
cards.push({
type: "thermostat",
entity: stateObj.entity_id,
});
} else if (domain === "media_player") {
cards.push({
type: "media-control",
entity: stateObj.entity_id,
});
} else if (domain === "weather") {
cards.push({
type: "weather-forecast",
entity: stateObj.entity_id,
});
} else {
entities.push(stateObj.entity_id);
}
});
if (entities.length > 0) {
cards.unshift({
title,
type: "entities",
entities,
});
}
return cards;
};
const computeDefaultViewStates = (hass: HomeAssistant): HassEntities => {
const states = {};
Object.keys(hass.states).forEach((entityId) => {
const stateObj = hass.states[entityId];
if (
!stateObj.attributes.hidden &&
!HIDE_DOMAIN.has(computeStateDomain(stateObj))
) {
states[entityId] = hass.states[entityId];
}
});
return states;
};
const generateViewConfig = (
localize: LocalizeFunc,
id: string,
title: string | undefined,
icon: string | undefined,
entities: HassEntities,
groupOrders: { [entityId: string]: number }
): ViewConfig => {
const splitted = splitByGroups(entities);
splitted.groups.sort(
(gr1, gr2) => groupOrders[gr1.entity_id] - groupOrders[gr2.entity_id]
);
const badgeEntities: { [domain: string]: string[] } = {};
const ungroupedEntitites: { [domain: string]: string[] } = {};
// Organize ungrouped entities in badges/ungrouped things
Object.keys(splitted.ungrouped).forEach((entityId) => {
const state = splitted.ungrouped[entityId];
const domain = computeStateDomain(state);
const coll = DOMAINS_BADGES.includes(domain)
? badgeEntities
: ungroupedEntitites;
if (!(domain in coll)) {
coll[domain] = [];
}
coll[domain].push(state.entity_id);
});
let badges: string[] = [];
DOMAINS_BADGES.forEach((domain) => {
if (domain in badgeEntities) {
badges = badges.concat(badgeEntities[domain]);
}
});
let cards: CardConfig[] = [];
splitted.groups.forEach((groupEntity) => {
cards = cards.concat(
computeCards(
computeStateName(groupEntity),
groupEntity.attributes.entity_id.map((entityId) => entities[entityId])
)
);
});
Object.keys(ungroupedEntitites)
.sort()
.forEach((domain) => {
cards = cards.concat(
computeCards(
localize(`domain.${domain}`),
ungroupedEntitites[domain].map((entityId) => entities[entityId])
)
);
});
return {
id,
title,
icon,
badges,
cards,
};
};
export const generateLovelaceConfig = (
hass: HomeAssistant,
localize: LocalizeFunc
): LovelaceConfig => {
const viewEntities = extractViews(hass.states);
const views = viewEntities.map((viewEntity: GroupEntity) => {
const states = getViewEntities(hass.states, viewEntity);
// In the case of a normal view, we use group order as specified in view
const groupOrders = {};
Object.keys(states).forEach((entityId, idx) => {
groupOrders[entityId] = idx;
});
return generateViewConfig(
localize,
computeObjectId(viewEntity.entity_id),
computeStateName(viewEntity),
viewEntity.attributes.icon,
states,
groupOrders
);
});
let title = hass.config.location_name;
// User can override default view. If they didn't, we will add one
// that contains all entities.
if (
viewEntities.length === 0 ||
viewEntities[0].entity_id !== DEFAULT_VIEW_ENTITY_ID
) {
const states = computeDefaultViewStates(hass);
// In the case of a default view, we want to use the group order attribute
const groupOrders = {};
Object.keys(states).forEach((entityId) => {
const stateObj = states[entityId];
if (stateObj.attributes.order) {
groupOrders[entityId] = stateObj.attributes.order;
}
});
views.unshift(
generateViewConfig(
localize,
"default_view",
"Home",
undefined,
states,
groupOrders
)
);
// Make sure we don't have Home as title and first tab.
if (views.length > 1 && title === "Home") {
title = "Home Assistant";
}
}
return {
_frontendAuto: true,
title,
views,
};
};

View File

@ -5,8 +5,9 @@ import "@polymer/paper-button/paper-button";
import "../../layouts/hass-loading-screen";
import "../../layouts/hass-error-screen";
import "./hui-root";
import localizeMixin from "../../mixins/localize-mixin";
class Lovelace extends PolymerElement {
class Lovelace extends localizeMixin(PolymerElement) {
static get template() {
return html`
<style>
@ -116,12 +117,22 @@ class Lovelace extends PolymerElement {
_state: "loaded",
});
} catch (err) {
if (err.code === "file_not_found") {
const {
generateLovelaceConfig,
} = await import("./common/generate-lovelace-config");
this.setProperties({
_config: generateLovelaceConfig(this.hass, this.localize),
_state: "loaded",
});
} else {
this.setProperties({
_state: "error",
_errorMsg: err.message,
});
}
}
}
_equal(a, b) {
return a === b;

View File

@ -274,6 +274,10 @@ class HUIRoot extends NavigateMixin(EventsMixin(PolymerElement)) {
}
_editModeEnable() {
if (this.config._frontendAuto) {
alert("Unable to edit automatic generated UI yet.");
return;
}
this._editMode = true;
}