mirror of
https://github.com/home-assistant/frontend.git
synced 2025-07-09 02:16:35 +00:00
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:
parent
785ed6f9db
commit
d41a4cf78b
@ -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 { DEFAULT_VIEW_ENTITY_ID } from "../const";
|
||||||
|
import { GroupEntity } from "../../types";
|
||||||
|
|
||||||
// Return an ordered array of available views
|
// Return an ordered array of available views
|
||||||
export default function extractViews(entities: HassEntities): HassEntity[] {
|
export default function extractViews(entities: HassEntities): GroupEntity[] {
|
||||||
const views: HassEntity[] = [];
|
const views: GroupEntity[] = [];
|
||||||
|
|
||||||
Object.keys(entities).forEach((entityId) => {
|
Object.keys(entities).forEach((entityId) => {
|
||||||
const entity = entities[entityId];
|
const entity = entities[entityId];
|
||||||
if (entity.attributes.view) {
|
if (entity.attributes.view) {
|
||||||
views.push(entity);
|
views.push(entity as GroupEntity);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -8,7 +8,7 @@ import { GroupEntity } from "../../types";
|
|||||||
export default function getViewEntities(
|
export default function getViewEntities(
|
||||||
entities: HassEntities,
|
entities: HassEntities,
|
||||||
view: GroupEntity
|
view: GroupEntity
|
||||||
) {
|
): HassEntities {
|
||||||
const viewEntities = {};
|
const viewEntities = {};
|
||||||
|
|
||||||
view.attributes.entity_id.forEach((entityId) => {
|
view.attributes.entity_id.forEach((entityId) => {
|
||||||
|
@ -1,18 +1,19 @@
|
|||||||
import computeDomain from "./compute_domain";
|
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
|
// Split a collection into a list of groups and a 'rest' list of ungrouped
|
||||||
// entities.
|
// entities.
|
||||||
// Returns { groups: [], ungrouped: {} }
|
// Returns { groups: [], ungrouped: {} }
|
||||||
export default function splitByGroups(entities: HassEntities) {
|
export default function splitByGroups(entities: HassEntities) {
|
||||||
const groups: HassEntity[] = [];
|
const groups: GroupEntity[] = [];
|
||||||
const ungrouped: HassEntities = {};
|
const ungrouped: HassEntities = {};
|
||||||
|
|
||||||
Object.keys(entities).forEach((entityId) => {
|
Object.keys(entities).forEach((entityId) => {
|
||||||
const entity = entities[entityId];
|
const entity = entities[entityId];
|
||||||
|
|
||||||
if (computeDomain(entityId) === "group") {
|
if (computeDomain(entityId) === "group") {
|
||||||
groups.push(entity);
|
groups.push(entity as GroupEntity);
|
||||||
} else {
|
} else {
|
||||||
ungrouped[entityId] = entity;
|
ungrouped[entityId] = entity;
|
||||||
}
|
}
|
||||||
|
236
src/panels/lovelace/common/generate-lovelace-config.ts
Normal file
236
src/panels/lovelace/common/generate-lovelace-config.ts
Normal 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,
|
||||||
|
};
|
||||||
|
};
|
@ -5,8 +5,9 @@ import "@polymer/paper-button/paper-button";
|
|||||||
import "../../layouts/hass-loading-screen";
|
import "../../layouts/hass-loading-screen";
|
||||||
import "../../layouts/hass-error-screen";
|
import "../../layouts/hass-error-screen";
|
||||||
import "./hui-root";
|
import "./hui-root";
|
||||||
|
import localizeMixin from "../../mixins/localize-mixin";
|
||||||
|
|
||||||
class Lovelace extends PolymerElement {
|
class Lovelace extends localizeMixin(PolymerElement) {
|
||||||
static get template() {
|
static get template() {
|
||||||
return html`
|
return html`
|
||||||
<style>
|
<style>
|
||||||
@ -116,10 +117,20 @@ class Lovelace extends PolymerElement {
|
|||||||
_state: "loaded",
|
_state: "loaded",
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
this.setProperties({
|
if (err.code === "file_not_found") {
|
||||||
_state: "error",
|
const {
|
||||||
_errorMsg: err.message,
|
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,
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -274,6 +274,10 @@ class HUIRoot extends NavigateMixin(EventsMixin(PolymerElement)) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
_editModeEnable() {
|
_editModeEnable() {
|
||||||
|
if (this.config._frontendAuto) {
|
||||||
|
alert("Unable to edit automatic generated UI yet.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
this._editMode = true;
|
this._editMode = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user