Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 5 additions & 4 deletions src/common/entity/extract_views.ts
Original file line number Diff line number Diff line change
@@ -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);
}
});

Expand Down
2 changes: 1 addition & 1 deletion src/common/entity/get_view_entities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) => {
Expand Down
7 changes: 4 additions & 3 deletions src/common/entity/split_by_groups.ts
Original file line number Diff line number Diff line change
@@ -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;
}
Expand Down
236 changes: 236 additions & 0 deletions src/panels/lovelace/common/generate-lovelace-config.ts
Original file line number Diff line number Diff line change
@@ -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") {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not use switch?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤷‍♂️ no real reason.

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,
};
};
21 changes: 16 additions & 5 deletions src/panels/lovelace/ha-panel-lovelace.js
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand Down Expand Up @@ -116,10 +117,20 @@ class Lovelace extends PolymerElement {
_state: "loaded",
});
} catch (err) {
this.setProperties({
_state: "error",
_errorMsg: err.message,
});
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,
});
}
}
}

Expand Down
4 changes: 4 additions & 0 deletions src/panels/lovelace/hui-root.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand Down