diff --git a/src/common/entity/extract_views.ts b/src/common/entity/extract_views.ts index bcc7084646..d5679e5518 100644 --- a/src/common/entity/extract_views.ts +++ b/src/common/entity/extract_views.ts @@ -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); } }); diff --git a/src/common/entity/get_view_entities.ts b/src/common/entity/get_view_entities.ts index 62aa7e5d89..c3d249cfcd 100644 --- a/src/common/entity/get_view_entities.ts +++ b/src/common/entity/get_view_entities.ts @@ -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) => { diff --git a/src/common/entity/split_by_groups.ts b/src/common/entity/split_by_groups.ts index a894472d8c..aed1ab7f0e 100644 --- a/src/common/entity/split_by_groups.ts +++ b/src/common/entity/split_by_groups.ts @@ -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; } diff --git a/src/panels/lovelace/common/generate-lovelace-config.ts b/src/panels/lovelace/common/generate-lovelace-config.ts new file mode 100644 index 0000000000..ca8b4fd0e9 --- /dev/null +++ b/src/panels/lovelace/common/generate-lovelace-config.ts @@ -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, + }; +}; diff --git a/src/panels/lovelace/ha-panel-lovelace.js b/src/panels/lovelace/ha-panel-lovelace.js index a866a9eda9..f69434e714 100644 --- a/src/panels/lovelace/ha-panel-lovelace.js +++ b/src/panels/lovelace/ha-panel-lovelace.js @@ -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`