diff --git a/src/common/entity/context/get_area_context.ts b/src/common/entity/context/get_area_context.ts
new file mode 100644
index 0000000000..6b05bf5d63
--- /dev/null
+++ b/src/common/entity/context/get_area_context.ts
@@ -0,0 +1,30 @@
+import type { AreaRegistryEntry } from "../../../data/area_registry";
+import type { FloorRegistryEntry } from "../../../data/floor_registry";
+import type { HomeAssistant } from "../../../types";
+
+interface AreaContext {
+ area: AreaRegistryEntry | null;
+ floor: FloorRegistryEntry | null;
+}
+
+export const getAreaContext = (
+ areaId: string,
+ hass: HomeAssistant
+): AreaContext => {
+ const area = (hass.areas[areaId] as AreaRegistryEntry | undefined) || null;
+
+ if (!area) {
+ return {
+ area: null,
+ floor: null,
+ };
+ }
+
+ const floorId = area?.floor_id;
+ const floor = floorId ? hass.floors[floorId] : null;
+
+ return {
+ area: area,
+ floor: floor,
+ };
+};
diff --git a/src/common/entity/entity_filter.ts b/src/common/entity/entity_filter.ts
index 53558e5298..072520d8a0 100644
--- a/src/common/entity/entity_filter.ts
+++ b/src/common/entity/entity_filter.ts
@@ -17,7 +17,7 @@ export interface EntityFilter {
hidden_platform?: string | string[];
}
-type EntityFilterFunc = (entityId: string) => boolean;
+export type EntityFilterFunc = (entityId: string) => boolean;
export const generateEntityFilter = (
hass: HomeAssistant,
diff --git a/src/components/ha-area-filter.ts b/src/components/ha-area-filter.ts
deleted file mode 100644
index b34af2bd51..0000000000
--- a/src/components/ha-area-filter.ts
+++ /dev/null
@@ -1,95 +0,0 @@
-import { mdiTextureBox } from "@mdi/js";
-import type { TemplateResult } from "lit";
-import { LitElement, css, html } from "lit";
-import { customElement, property } from "lit/decorators";
-import { fireEvent } from "../common/dom/fire_event";
-import { showAreaFilterDialog } from "../dialogs/area-filter/show-area-filter-dialog";
-import type { HomeAssistant } from "../types";
-import "./ha-icon-next";
-import "./ha-svg-icon";
-import "./ha-textfield";
-
-export interface AreaFilterValue {
- hidden?: string[];
- order?: string[];
-}
-
-@customElement("ha-area-filter")
-export class HaAreaPicker extends LitElement {
- @property({ attribute: false }) public hass!: HomeAssistant;
-
- @property() public label?: string;
-
- @property({ attribute: false }) public value?: AreaFilterValue;
-
- @property() public helper?: string;
-
- @property({ type: Boolean }) public disabled = false;
-
- @property({ type: Boolean }) public required = false;
-
- protected render(): TemplateResult {
- const allAreasCount = Object.keys(this.hass.areas).length;
- const hiddenAreasCount = this.value?.hidden?.length ?? 0;
-
- const description =
- hiddenAreasCount === 0
- ? this.hass.localize("ui.components.area-filter.all_areas")
- : allAreasCount === hiddenAreasCount
- ? this.hass.localize("ui.components.area-filter.no_areas")
- : this.hass.localize("ui.components.area-filter.area_count", {
- count: allAreasCount - hiddenAreasCount,
- });
-
- return html`
-
-
- ${this.label}
- ${description}
-
-
- `;
- }
-
- private async _edit(ev) {
- if (ev.defaultPrevented) {
- return;
- }
- if (ev.type === "keydown" && ev.key !== "Enter" && ev.key !== " ") {
- return;
- }
- ev.preventDefault();
- ev.stopPropagation();
- const value = await showAreaFilterDialog(this, {
- title: this.label,
- initialValue: this.value,
- });
- if (!value) return;
- fireEvent(this, "value-changed", { value });
- }
-
- static styles = css`
- ha-list-item {
- --mdc-list-side-padding-left: 8px;
- --mdc-list-side-padding-right: 8px;
- }
- `;
-}
-
-declare global {
- interface HTMLElementTagNameMap {
- "ha-area-filter": HaAreaPicker;
- }
-}
diff --git a/src/components/ha-areas-display-editor.ts b/src/components/ha-areas-display-editor.ts
new file mode 100644
index 0000000000..340ae2126f
--- /dev/null
+++ b/src/components/ha-areas-display-editor.ts
@@ -0,0 +1,98 @@
+import { mdiTextureBox } from "@mdi/js";
+import type { TemplateResult } from "lit";
+import { LitElement, html } from "lit";
+import { customElement, property } from "lit/decorators";
+import { fireEvent } from "../common/dom/fire_event";
+import { getAreaContext } from "../common/entity/context/get_area_context";
+import { areaCompare } from "../data/area_registry";
+import type { HomeAssistant } from "../types";
+import "./ha-expansion-panel";
+import "./ha-items-display-editor";
+import type { DisplayItem, DisplayValue } from "./ha-items-display-editor";
+import "./ha-svg-icon";
+import "./ha-textfield";
+
+export interface AreasDisplayValue {
+ hidden?: string[];
+ order?: string[];
+}
+
+@customElement("ha-areas-display-editor")
+export class HaAreasDisplayEditor extends LitElement {
+ @property({ attribute: false }) public hass!: HomeAssistant;
+
+ @property() public label?: string;
+
+ @property({ attribute: false }) public value?: AreasDisplayValue;
+
+ @property() public helper?: string;
+
+ @property({ type: Boolean }) public expanded = false;
+
+ @property({ type: Boolean }) public disabled = false;
+
+ @property({ type: Boolean }) public required = false;
+
+ protected render(): TemplateResult {
+ const compare = areaCompare(this.hass.areas, this.value?.order);
+
+ const areas = Object.values(this.hass.areas).sort((areaA, areaB) =>
+ compare(areaA.area_id, areaB.area_id)
+ );
+
+ const items: DisplayItem[] = areas.map((area) => {
+ const { floor } = getAreaContext(area.area_id, this.hass!);
+ return {
+ value: area.area_id,
+ label: area.name,
+ icon: area.icon ?? undefined,
+ iconPath: mdiTextureBox,
+ description: floor?.name,
+ };
+ });
+
+ const value: DisplayValue = {
+ order: this.value?.order ?? [],
+ hidden: this.value?.hidden ?? [],
+ };
+
+ return html`
+
+
+
+
+ `;
+ }
+
+ private async _areaDisplayChanged(ev) {
+ ev.stopPropagation();
+ const value = ev.detail.value as DisplayValue;
+ const newValue: AreasDisplayValue = {
+ ...this.value,
+ ...value,
+ };
+ if (newValue.hidden?.length === 0) {
+ delete newValue.hidden;
+ }
+ if (newValue.order?.length === 0) {
+ delete newValue.order;
+ }
+
+ fireEvent(this, "value-changed", { value: newValue });
+ }
+}
+
+declare global {
+ interface HTMLElementTagNameMap {
+ "ha-areas-display-editor": HaAreasDisplayEditor;
+ }
+}
diff --git a/src/components/ha-items-display-editor.ts b/src/components/ha-items-display-editor.ts
new file mode 100644
index 0000000000..6feb75c207
--- /dev/null
+++ b/src/components/ha-items-display-editor.ts
@@ -0,0 +1,241 @@
+import { ResizeController } from "@lit-labs/observers/resize-controller";
+import { mdiDrag, mdiEye, mdiEyeOff } from "@mdi/js";
+import { LitElement, css, html, nothing } from "lit";
+import { customElement, property } from "lit/decorators";
+import { classMap } from "lit/directives/class-map";
+import { repeat } from "lit/directives/repeat";
+import memoizeOne from "memoize-one";
+import { fireEvent } from "../common/dom/fire_event";
+import type { HomeAssistant } from "../types";
+import "./ha-icon";
+import "./ha-icon-button";
+import "./ha-icon-button-next";
+import "./ha-md-list";
+import "./ha-md-list-item";
+import "./ha-sortable";
+import "./ha-svg-icon";
+
+export interface DisplayItem {
+ icon?: string;
+ iconPath?: string;
+ value: string;
+ label: string;
+ description?: string;
+}
+
+export interface DisplayValue {
+ order: string[];
+ hidden: string[];
+}
+
+declare global {
+ interface HTMLElementTagNameMap {
+ "ha-items-display-editor": HaItemDisplayEditor;
+ }
+ interface HASSDomEvents {
+ "item-display-navigate-clicked": { value: string };
+ }
+}
+
+@customElement("ha-items-display-editor")
+export class HaItemDisplayEditor extends LitElement {
+ @property({ attribute: false }) public hass!: HomeAssistant;
+
+ @property({ attribute: false }) public items: DisplayItem[] = [];
+
+ @property({ type: Boolean, attribute: "show-navigation-button" })
+ public showNavigationButton = false;
+
+ @property({ attribute: false })
+ public value: DisplayValue = {
+ order: [],
+ hidden: [],
+ };
+
+ private _showIcon = new ResizeController(this, {
+ callback: (entries) => entries[0]?.contentRect.width > 450,
+ });
+
+ private _toggle(ev) {
+ ev.stopPropagation();
+ const value = ev.currentTarget.value;
+
+ const hiddenItems = this._hiddenItems(this.items, this.value.hidden);
+
+ const newHidden = hiddenItems.map((item) => item.value);
+
+ if (newHidden.includes(value)) {
+ newHidden.splice(newHidden.indexOf(value), 1);
+ } else {
+ newHidden.push(value);
+ }
+
+ const newVisibleItems = this._visibleItems(this.items, newHidden);
+ const newOrder = newVisibleItems.map((a) => a.value);
+
+ this.value = {
+ hidden: newHidden,
+ order: newOrder,
+ };
+ fireEvent(this, "value-changed", { value: this.value });
+ }
+
+ private _itemMoved(ev: CustomEvent): void {
+ ev.stopPropagation();
+ const { oldIndex, newIndex } = ev.detail;
+
+ const visibleItems = this._visibleItems(this.items, this.value.hidden);
+ const newOrder = visibleItems.map((item) => item.value);
+
+ const movedItem = newOrder.splice(oldIndex, 1)[0];
+ newOrder.splice(newIndex, 0, movedItem);
+
+ this.value = {
+ ...this.value,
+ order: newOrder,
+ };
+ fireEvent(this, "value-changed", { value: this.value });
+ }
+
+ private _navigate(ev) {
+ const value = ev.currentTarget.value;
+ fireEvent(this, "item-display-navigate-clicked", { value });
+ ev.stopPropagation();
+ }
+
+ private _visibleItems = memoizeOne((items: DisplayItem[], hidden: string[]) =>
+ items.filter((item) => !hidden.includes(item.value))
+ );
+
+ private _hiddenItems = memoizeOne((items: DisplayItem[], hidden: string[]) =>
+ items.filter((item) => hidden.includes(item.value))
+ );
+
+ protected render() {
+ const allItems = [
+ ...this._visibleItems(this.items, this.value.hidden),
+ ...this._hiddenItems(this.items, this.value.hidden),
+ ];
+
+ const showIcon = this._showIcon.value;
+ return html`
+
+
+ ${repeat(
+ allItems,
+ (item) => item.value,
+ (item, _idx) => {
+ const isVisible = !this.value.hidden.includes(item.value);
+ const { label, value, description, icon, iconPath } = item;
+ return html`
+
+ ${label}
+ ${description
+ ? html`${description}`
+ : nothing}
+ ${isVisible
+ ? html`
+
+ `
+ : html``}
+ ${!showIcon
+ ? nothing
+ : icon
+ ? html`
+
+ `
+ : iconPath
+ ? html`
+
+ `
+ : nothing}
+
+ ${this.showNavigationButton
+ ? html`
+
+ `
+ : nothing}
+
+ `;
+ }
+ )}
+
+
+ `;
+ }
+
+ static styles = css`
+ :host {
+ display: block;
+ }
+ .handle {
+ cursor: move;
+ padding: 8px;
+ margin: -8px;
+ }
+ ha-md-list {
+ padding: 0;
+ }
+ ha-md-list-item {
+ --md-list-item-top-space: 0;
+ --md-list-item-bottom-space: 0;
+ --md-list-item-leading-space: 8px;
+ --md-list-item-trailing-space: 8px;
+ --md-list-item-two-line-container-height: 48px;
+ --md-list-item-one-line-container-height: 48px;
+ }
+ ha-md-list-item ha-icon-button {
+ margin-left: -12px;
+ margin-right: -12px;
+ }
+ ha-md-list-item.hidden {
+ --md-list-item-label-text-color: var(--disabled-text-color);
+ --md-list-item-supporting-text-color: var(--disabled-text-color);
+ }
+ ha-md-list-item.hidden .icon {
+ color: var(--disabled-text-color);
+ }
+ `;
+}
+
+declare global {
+ interface HTMLElementTagNameMap {
+ "ha-items-display-editor": HaItemDisplayEditor;
+ }
+}
diff --git a/src/components/ha-selector/ha-selector-area-filter.ts b/src/components/ha-selector/ha-selector-areas-display.ts
similarity index 64%
rename from src/components/ha-selector/ha-selector-area-filter.ts
rename to src/components/ha-selector/ha-selector-areas-display.ts
index 6237aec0ef..d6b02593a3 100644
--- a/src/components/ha-selector/ha-selector-area-filter.ts
+++ b/src/components/ha-selector/ha-selector-areas-display.ts
@@ -1,14 +1,14 @@
import { LitElement, html } from "lit";
import { customElement, property } from "lit/decorators";
-import type { AreaFilterSelector } from "../../data/selector";
+import type { AreasDisplaySelector } from "../../data/selector";
import type { HomeAssistant } from "../../types";
-import "../ha-area-filter";
+import "../ha-areas-display-editor";
-@customElement("ha-selector-area_filter")
-export class HaAreaFilterSelector extends LitElement {
+@customElement("ha-selector-areas_display")
+export class HaAreasDisplaySelector extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
- @property({ attribute: false }) public selector!: AreaFilterSelector;
+ @property({ attribute: false }) public selector!: AreasDisplaySelector;
@property() public value?: any;
@@ -22,20 +22,20 @@ export class HaAreaFilterSelector extends LitElement {
protected render() {
return html`
-
+ >
`;
}
}
declare global {
interface HTMLElementTagNameMap {
- "ha-selector-area_filter": HaAreaFilterSelector;
+ "ha-selector-areas_display": HaAreasDisplaySelector;
}
}
diff --git a/src/components/ha-selector/ha-selector.ts b/src/components/ha-selector/ha-selector.ts
index dd45f13d8d..69a5b59a25 100644
--- a/src/components/ha-selector/ha-selector.ts
+++ b/src/components/ha-selector/ha-selector.ts
@@ -14,7 +14,7 @@ const LOAD_ELEMENTS = {
action: () => import("./ha-selector-action"),
addon: () => import("./ha-selector-addon"),
area: () => import("./ha-selector-area"),
- area_filter: () => import("./ha-selector-area-filter"),
+ areas_display: () => import("./ha-selector-areas-display"),
attribute: () => import("./ha-selector-attribute"),
assist_pipeline: () => import("./ha-selector-assist-pipeline"),
boolean: () => import("./ha-selector-boolean"),
diff --git a/src/data/selector.ts b/src/data/selector.ts
index ba422c6833..8993e0e5c3 100644
--- a/src/data/selector.ts
+++ b/src/data/selector.ts
@@ -23,7 +23,7 @@ export type Selector =
| ActionSelector
| AddonSelector
| AreaSelector
- | AreaFilterSelector
+ | AreasDisplaySelector
| AttributeSelector
| BooleanSelector
| ButtonToggleSelector
@@ -92,8 +92,8 @@ export interface AreaSelector {
} | null;
}
-export interface AreaFilterSelector {
- area_filter: {} | null;
+export interface AreasDisplaySelector {
+ areas_display: {} | null;
}
export interface AttributeSelector {
diff --git a/src/dialogs/area-filter/area-filter-dialog.ts b/src/dialogs/area-filter/area-filter-dialog.ts
deleted file mode 100644
index 56fce665b3..0000000000
--- a/src/dialogs/area-filter/area-filter-dialog.ts
+++ /dev/null
@@ -1,203 +0,0 @@
-import "@material/mwc-list/mwc-list";
-import { mdiDrag, mdiEye, mdiEyeOff } from "@mdi/js";
-import type { CSSResultGroup } from "lit";
-import { LitElement, css, html, nothing } from "lit";
-import { customElement, property, state } from "lit/decorators";
-import { classMap } from "lit/directives/class-map";
-import { repeat } from "lit/directives/repeat";
-import { fireEvent } from "../../common/dom/fire_event";
-import type { AreaFilterValue } from "../../components/ha-area-filter";
-import "../../components/ha-button";
-import "../../components/ha-dialog";
-import "../../components/ha-icon-button";
-import "../../components/ha-list-item";
-import "../../components/ha-sortable";
-import { areaCompare } from "../../data/area_registry";
-import { haStyleDialog } from "../../resources/styles";
-import type { HomeAssistant } from "../../types";
-import type { HassDialog } from "../make-dialog-manager";
-import type { AreaFilterDialogParams } from "./show-area-filter-dialog";
-
-@customElement("dialog-area-filter")
-export class DialogAreaFilter
- extends LitElement
- implements HassDialog
-{
- @property({ attribute: false }) public hass?: HomeAssistant;
-
- @state() private _dialogParams?: AreaFilterDialogParams;
-
- @state() private _hidden: string[] = [];
-
- @state() private _areas: string[] = [];
-
- public showDialog(dialogParams: AreaFilterDialogParams): void {
- this._dialogParams = dialogParams;
- this._hidden = dialogParams.initialValue?.hidden ?? [];
- const order = dialogParams.initialValue?.order ?? [];
- const allAreas = Object.keys(this.hass!.areas);
- this._areas = allAreas.concat().sort(areaCompare(this.hass!.areas, order));
- }
-
- public closeDialog() {
- this._dialogParams = undefined;
- this._hidden = [];
- this._areas = [];
- fireEvent(this, "dialog-closed", { dialog: this.localName });
- return true;
- }
-
- private _submit(): void {
- const order = this._areas.filter((area) => !this._hidden.includes(area));
- const value: AreaFilterValue = {
- hidden: this._hidden,
- order,
- };
- this._dialogParams?.submit?.(value);
- this.closeDialog();
- }
-
- private _cancel(): void {
- this._dialogParams?.cancel?.();
- this.closeDialog();
- }
-
- private _areaMoved(ev: CustomEvent): void {
- ev.stopPropagation();
- const { oldIndex, newIndex } = ev.detail;
-
- const areas = this._areas.concat();
-
- const option = areas.splice(oldIndex, 1)[0];
- areas.splice(newIndex, 0, option);
-
- this._areas = areas;
- }
-
- protected render() {
- if (!this._dialogParams || !this.hass) {
- return nothing;
- }
-
- const allAreas = this._areas;
-
- return html`
-
-
-
- ${repeat(
- allAreas,
- (area) => area,
- (area, _idx) => {
- const isVisible = !this._hidden.includes(area);
- const name = this.hass!.areas[area]?.name || area;
- return html`
-
- ${isVisible
- ? html``
- : nothing}
- ${name}
-
-
- `;
- }
- )}
-
-
-
- ${this.hass.localize("ui.common.cancel")}
-
-
- ${this.hass.localize("ui.common.submit")}
-
-
- `;
- }
-
- private _toggle(ev) {
- const area = ev.target.area;
- const hidden = [...(this._hidden ?? [])];
- if (hidden.includes(area)) {
- hidden.splice(hidden.indexOf(area), 1);
- } else {
- hidden.push(area);
- }
- this._hidden = hidden;
- const nonHiddenAreas = this._areas.filter(
- (ar) => !this._hidden.includes(ar)
- );
- const hiddenAreas = this._areas.filter((ar) => this._hidden.includes(ar));
- this._areas = [...nonHiddenAreas, ...hiddenAreas];
- }
-
- static get styles(): CSSResultGroup {
- return [
- haStyleDialog,
- css`
- ha-dialog {
- /* Place above other dialogs */
- --dialog-z-index: 104;
- --dialog-content-padding: 0;
- }
- ha-list-item {
- overflow: visible;
- }
- .hidden {
- color: var(--disabled-text-color);
- }
- .handle {
- cursor: move; /* fallback if grab cursor is unsupported */
- cursor: grab;
- }
- .actions {
- display: flex;
- flex-direction: row;
- }
- ha-icon-button {
- display: block;
- margin: -12px;
- }
- `,
- ];
- }
-}
-
-declare global {
- interface HTMLElementTagNameMap {
- "dialog-area-filter": DialogAreaFilter;
- }
-}
diff --git a/src/dialogs/area-filter/show-area-filter-dialog.ts b/src/dialogs/area-filter/show-area-filter-dialog.ts
deleted file mode 100644
index 148db2d8bc..0000000000
--- a/src/dialogs/area-filter/show-area-filter-dialog.ts
+++ /dev/null
@@ -1,38 +0,0 @@
-import { fireEvent } from "../../common/dom/fire_event";
-import type { AreaFilterValue } from "../../components/ha-area-filter";
-
-export interface AreaFilterDialogParams {
- title?: string;
- initialValue?: AreaFilterValue;
- submit?: (value?: AreaFilterValue) => void;
- cancel?: () => void;
-}
-
-export const showAreaFilterDialog = (
- element: HTMLElement,
- dialogParams: AreaFilterDialogParams
-) =>
- new Promise((resolve) => {
- const origCancel = dialogParams.cancel;
- const origSubmit = dialogParams.submit;
-
- fireEvent(element, "show-dialog", {
- dialogTag: "dialog-area-filter",
- dialogImport: () => import("./area-filter-dialog"),
- dialogParams: {
- ...dialogParams,
- cancel: () => {
- resolve(null);
- if (origCancel) {
- origCancel();
- }
- },
- submit: (code: AreaFilterValue) => {
- resolve(code);
- if (origSubmit) {
- origSubmit(code);
- }
- },
- },
- });
- });
diff --git a/src/panels/config/dashboard/dialog-new-dashboard.ts b/src/panels/config/dashboard/dialog-new-dashboard.ts
index 6f9ff9a34d..33040d8f2c 100644
--- a/src/panels/config/dashboard/dialog-new-dashboard.ts
+++ b/src/panels/config/dashboard/dialog-new-dashboard.ts
@@ -1,5 +1,5 @@
import "@material/mwc-list/mwc-list";
-import { mdiMap, mdiPencilOutline, mdiShape, mdiWeb } from "@mdi/js";
+import { mdiHome, mdiMap, mdiPencilOutline, mdiShape, mdiWeb } from "@mdi/js";
import type { CSSResultGroup } from "lit";
import { LitElement, css, html, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
@@ -30,6 +30,10 @@ const STRATEGIES = [
type: "iframe",
iconPath: mdiWeb,
},
+ {
+ type: "areas",
+ iconPath: mdiHome,
+ },
] as const satisfies Strategy[];
@customElement("ha-dialog-new-dashboard")
diff --git a/src/panels/lovelace/common/generate-lovelace-config.ts b/src/panels/lovelace/common/generate-lovelace-config.ts
index 2ac9cb0dbe..b1ef399835 100644
--- a/src/panels/lovelace/common/generate-lovelace-config.ts
+++ b/src/panels/lovelace/common/generate-lovelace-config.ts
@@ -7,7 +7,7 @@ import { splitByGroups } from "../../../common/entity/split_by_groups";
import { stripPrefixFromEntityName } from "../../../common/entity/strip_prefix_from_entity_name";
import { stringCompare } from "../../../common/string/compare";
import type { LocalizeFunc } from "../../../common/translations/localize";
-import type { AreaFilterValue } from "../../../components/ha-area-filter";
+import type { AreasDisplayValue } from "../../../components/ha-areas-display-editor";
import { areaCompare } from "../../../data/area_registry";
import type {
EnergyPreferences,
@@ -503,7 +503,7 @@ export const generateDefaultViewConfig = (
entities: HassEntities,
localize: LocalizeFunc,
energyPrefs?: EnergyPreferences,
- areasPrefs?: AreaFilterValue,
+ areasPrefs?: AreasDisplayValue,
hideEntitiesWithoutAreas?: boolean,
hideEnergy?: boolean
): LovelaceViewConfig => {
diff --git a/src/panels/lovelace/editor/dashboard-strategy-editor/dialogs/dialog-dashboard-strategy-editor.ts b/src/panels/lovelace/editor/dashboard-strategy-editor/dialogs/dialog-dashboard-strategy-editor.ts
index ced78a0833..92d192ee25 100644
--- a/src/panels/lovelace/editor/dashboard-strategy-editor/dialogs/dialog-dashboard-strategy-editor.ts
+++ b/src/panels/lovelace/editor/dashboard-strategy-editor/dialogs/dialog-dashboard-strategy-editor.ts
@@ -191,8 +191,24 @@ class DialogDashboardStrategyEditor extends LitElement {
haStyleDialog,
css`
ha-dialog {
- --mdc-dialog-max-width: 800px;
--dialog-content-padding: 0 24px;
+ --dialog-surface-position: fixed;
+ --dialog-surface-top: 40px;
+ --mdc-dialog-min-width: min(600px, calc(100% - 32px));
+ --mdc-dialog-max-width: calc(100% - 32px);
+ --mdc-dialog-max-height: calc(100% - 80px);
+ }
+
+ @media all and (max-width: 450px), all and (max-height: 500px) {
+ /* overrule the ha-style-dialog max-height on small screens */
+ ha-dialog {
+ height: 100%;
+ --dialog-surface-top: 0px;
+ --mdc-dialog-min-width: 100%;
+ --mdc-dialog-max-width: 100%;
+ --mdc-dialog-max-height: 100%;
+ --dialog-content-padding: 8px;
+ }
}
`,
];
diff --git a/src/panels/lovelace/editor/dashboard-strategy-editor/hui-original-states-dashboard-strategy-editor.ts b/src/panels/lovelace/editor/dashboard-strategy-editor/hui-original-states-dashboard-strategy-editor.ts
index 2d085b5c2c..291be48a1f 100644
--- a/src/panels/lovelace/editor/dashboard-strategy-editor/hui-original-states-dashboard-strategy-editor.ts
+++ b/src/panels/lovelace/editor/dashboard-strategy-editor/hui-original-states-dashboard-strategy-editor.ts
@@ -14,7 +14,7 @@ const SCHEMA = [
{
name: "areas",
selector: {
- area_filter: {},
+ areas_display: {},
},
},
{
diff --git a/src/panels/lovelace/strategies/area/area-view-strategy.ts b/src/panels/lovelace/strategies/area/area-view-strategy.ts
index 53716164e8..03bd7d6dfc 100644
--- a/src/panels/lovelace/strategies/area/area-view-strategy.ts
+++ b/src/panels/lovelace/strategies/area/area-view-strategy.ts
@@ -1,30 +1,173 @@
import { ReactiveElement } from "lit";
import { customElement } from "lit/decorators";
+import type { EntityFilterFunc } from "../../../../common/entity/entity_filter";
import { generateEntityFilter } from "../../../../common/entity/entity_filter";
import type { LovelaceBadgeConfig } from "../../../../data/lovelace/config/badge";
import type { LovelaceCardConfig } from "../../../../data/lovelace/config/card";
import type { LovelaceSectionRawConfig } from "../../../../data/lovelace/config/section";
import type { LovelaceViewConfig } from "../../../../data/lovelace/config/view";
import type { HomeAssistant } from "../../../../types";
+import { supportsAlarmModesCardFeature } from "../../card-features/hui-alarm-modes-card-feature";
+import { supportsCoverOpenCloseCardFeature } from "../../card-features/hui-cover-open-close-card-feature";
+import { supportsLightBrightnessCardFeature } from "../../card-features/hui-light-brightness-card-feature";
+import { supportsLockCommandsCardFeature } from "../../card-features/hui-lock-commands-card-feature";
+import { supportsTargetTemperatureCardFeature } from "../../card-features/hui-target-temperature-card-feature";
+import type { LovelaceCardFeatureConfig } from "../../card-features/types";
+
+type Group = "lights" | "climate" | "media_players" | "security";
+
+type AreaEntitiesByGroup = Record;
+
+type AreaFilteredByGroup = Record;
+
+export const getAreaGroupedEntities = (
+ area: string,
+ hass: HomeAssistant,
+ controlOnly = false
+): AreaEntitiesByGroup => {
+ const allEntities = Object.keys(hass.states);
+
+ const groupedFilters: AreaFilteredByGroup = {
+ lights: [
+ generateEntityFilter(hass, {
+ domain: "light",
+ area: area,
+ entity_category: "none",
+ }),
+ ],
+ climate: [
+ generateEntityFilter(hass, {
+ domain: "climate",
+ area: area,
+ entity_category: "none",
+ }),
+ generateEntityFilter(hass, {
+ domain: "humidifier",
+ area: area,
+ entity_category: "none",
+ }),
+ generateEntityFilter(hass, {
+ domain: "cover",
+ area: area,
+ device_class: [
+ "shutter",
+ "awning",
+ "blind",
+ "curtain",
+ "shade",
+ "shutter",
+ "window",
+ ],
+ entity_category: "none",
+ }),
+ ...(controlOnly
+ ? []
+ : [
+ generateEntityFilter(hass, {
+ domain: "binary_sensor",
+ area: area,
+ device_class: "window",
+ entity_category: "none",
+ }),
+ ]),
+ ],
+ media_players: [
+ generateEntityFilter(hass, {
+ domain: "media_player",
+ area: area,
+ entity_category: "none",
+ }),
+ ],
+ security: [
+ generateEntityFilter(hass, {
+ domain: "alarm_control_panel",
+ area: area,
+ entity_category: "none",
+ }),
+ generateEntityFilter(hass, {
+ domain: "lock",
+ area: area,
+ entity_category: "none",
+ }),
+ generateEntityFilter(hass, {
+ domain: "cover",
+ device_class: ["door", "garage", "gate"],
+ area: area,
+ entity_category: "none",
+ }),
+ ...(controlOnly
+ ? []
+ : [
+ generateEntityFilter(hass, {
+ domain: "binary_sensor",
+ device_class: ["door", "garage_door"],
+ area: area,
+ entity_category: "none",
+ }),
+ ]),
+ ],
+ };
+
+ return Object.fromEntries(
+ Object.entries(groupedFilters).map(([group, filters]) => [
+ group,
+ filters.reduce(
+ (acc, filter) => [
+ ...acc,
+ ...allEntities.filter((entity) => filter(entity)),
+ ],
+ []
+ ),
+ ])
+ ) as AreaEntitiesByGroup;
+};
export interface AreaViewStrategyConfig {
type: "area";
area?: string;
}
-const computeTileCard = (entity: string): LovelaceCardConfig => ({
- type: "tile",
- entity: entity,
-});
+const computeTileCardConfig =
+ (hass: HomeAssistant) =>
+ (entity: string): LovelaceCardConfig => {
+ const stateObj = hass.states[entity];
+
+ let feature: LovelaceCardFeatureConfig | undefined;
+ if (supportsLightBrightnessCardFeature(stateObj)) {
+ feature = {
+ type: "light-brightness",
+ };
+ } else if (supportsCoverOpenCloseCardFeature(stateObj)) {
+ feature = {
+ type: "cover-open-close",
+ };
+ } else if (supportsTargetTemperatureCardFeature(stateObj)) {
+ feature = {
+ type: "target-temperature",
+ };
+ } else if (supportsAlarmModesCardFeature(stateObj)) {
+ feature = {
+ type: "alarm-modes",
+ };
+ } else if (supportsLockCommandsCardFeature(stateObj)) {
+ feature = {
+ type: "lock-commands",
+ };
+ }
+
+ return {
+ type: "tile",
+ entity: entity,
+ features: feature ? [feature] : undefined,
+ };
+ };
const computeHeadingCard = (
heading: string,
- icon: string,
- style: "title" | "subtitle" = "title"
+ icon: string
): LovelaceCardConfig => ({
type: "heading",
heading: heading,
- heading_style: style,
icon: icon,
});
@@ -64,134 +207,36 @@ export class AreaViewStrategy extends ReactiveElement {
});
}
- const allEntities = Object.keys(hass.states);
+ const groupedEntities = getAreaGroupedEntities(config.area, hass);
- // Lights
- const lights = allEntities.filter(
- generateEntityFilter(hass, {
- domain: "light",
- area: config.area,
- entity_category: "none",
- })
- );
+ const computeTileCard = computeTileCardConfig(hass);
- if (lights.length) {
+ const {
+ lights,
+ climate,
+ media_players: mediaPlayers,
+ security,
+ } = groupedEntities;
+ if (lights.length > 0) {
sections.push({
type: "grid",
cards: [
- {
- type: "heading",
- heading: "Lights",
- icon: "mdi:lamps",
- },
- ...lights.map((entity) => ({
- type: "tile",
- entity: entity,
- })),
+ computeHeadingCard("Lights", "mdi:lightbulb"),
+ ...lights.map(computeTileCard),
],
});
}
- // Climate
- const thermostats = allEntities.filter(
- generateEntityFilter(hass, {
- domain: "climate",
- area: config.area,
- entity_category: "none",
- })
- );
-
- const humidifiers = allEntities.filter(
- generateEntityFilter(hass, {
- domain: "humidifier",
- area: config.area,
- entity_category: "none",
- })
- );
-
- const shutters = allEntities.filter(
- generateEntityFilter(hass, {
- domain: "cover",
- area: config.area,
- device_class: [
- "shutter",
- "awning",
- "blind",
- "curtain",
- "shade",
- "shutter",
- "window",
- ],
- entity_category: "none",
- })
- );
-
- const climateSensor = allEntities.filter(
- generateEntityFilter(hass, {
- domain: "binary_sensor",
- area: config.area,
- device_class: "window",
- entity_category: "none",
- })
- );
-
- const climateSectionCards: LovelaceCardConfig[] = [];
-
- if (
- thermostats.length ||
- humidifiers.length ||
- shutters.length ||
- climateSensor.length
- ) {
- climateSectionCards.push(
- computeHeadingCard("Climate", "mdi:home-thermometer")
- );
- }
-
- if (thermostats.length > 0 || humidifiers.length > 0) {
- const title =
- thermostats.length > 0 && humidifiers.length
- ? "Thermostats and humidifiers"
- : thermostats.length
- ? "Thermostats"
- : "Humidifiers";
- climateSectionCards.push(
- computeHeadingCard(title, "mdi:thermostat", "subtitle"),
- ...thermostats.map(computeTileCard),
- ...humidifiers.map(computeTileCard)
- );
- }
-
- if (shutters.length > 0) {
- climateSectionCards.push(
- computeHeadingCard("Shutters", "mdi:window-shutter", "subtitle"),
- ...shutters.map(computeTileCard)
- );
- }
-
- if (climateSensor.length > 0) {
- climateSectionCards.push(
- computeHeadingCard("Sensors", "mdi:window-open", "subtitle"),
- ...climateSensor.map(computeTileCard)
- );
- }
-
- if (climateSectionCards.length > 0) {
+ if (climate.length > 0) {
sections.push({
type: "grid",
- cards: climateSectionCards,
+ cards: [
+ computeHeadingCard("Climate", "mdi:home-thermometer"),
+ ...climate.map(computeTileCard),
+ ],
});
}
- // Media players
- const mediaPlayers = allEntities.filter(
- generateEntityFilter(hass, {
- domain: "media_player",
- area: config.area,
- entity_category: "none",
- })
- );
-
if (mediaPlayers.length > 0) {
sections.push({
type: "grid",
@@ -202,77 +247,27 @@ export class AreaViewStrategy extends ReactiveElement {
});
}
- // Security
- const alarms = allEntities.filter(
- generateEntityFilter(hass, {
- domain: "alarm_control_panel",
- area: config.area,
- entity_category: "none",
- })
- );
- const locks = allEntities.filter(
- generateEntityFilter(hass, {
- domain: "lock",
- area: config.area,
- entity_category: "none",
- })
- );
- const doors = allEntities.filter(
- generateEntityFilter(hass, {
- domain: "cover",
- device_class: ["door", "garage", "gate"],
- area: config.area,
- entity_category: "none",
- })
- );
- const securitySensors = allEntities.filter(
- generateEntityFilter(hass, {
- domain: "binary_sensor",
- device_class: ["door", "garage_door"],
- area: config.area,
- entity_category: "none",
- })
- );
-
- const securitySectionCards: LovelaceCardConfig[] = [];
-
- if (alarms.length > 0 || locks.length > 0) {
- const title =
- alarms.length > 0 && locks.length
- ? "Alarms and locks"
- : alarms.length
- ? "Alarms"
- : "Locks";
- securitySectionCards.push(
- computeHeadingCard(title, "mdi:shield", "subtitle"),
- ...alarms.map(computeTileCard),
- ...locks.map(computeTileCard)
- );
- }
-
- if (doors.length > 0) {
- securitySectionCards.push(
- computeHeadingCard("Doors", "mdi:door", "subtitle"),
- ...doors.map(computeTileCard)
- );
- }
-
- if (securitySensors.length > 0) {
- securitySectionCards.push(
- computeHeadingCard("Sensors", "mdi:wifi", "subtitle"),
- ...securitySensors.map(computeTileCard)
- );
- }
-
- if (securitySectionCards.length > 0) {
+ if (security.length > 0) {
sections.push({
type: "grid",
- cards: securitySectionCards,
+ cards: [
+ computeHeadingCard("Security", "mdi:security"),
+ ...security.map(computeTileCard),
+ ],
});
}
return {
type: "sections",
+ header: {
+ badges_position: "bottom",
+ layout: "responsive",
+ card: {
+ type: "markdown",
+ text_only: true,
+ content: `## ${area.name}`,
+ },
+ },
max_columns: 2,
sections: sections,
badges: badges,
diff --git a/src/panels/lovelace/strategies/areas/areas-dashboard-strategy.ts b/src/panels/lovelace/strategies/areas/areas-dashboard-strategy.ts
index 6de6bc7704..4773488762 100644
--- a/src/panels/lovelace/strategies/areas/areas-dashboard-strategy.ts
+++ b/src/panels/lovelace/strategies/areas/areas-dashboard-strategy.ts
@@ -1,34 +1,45 @@
import { ReactiveElement } from "lit";
import { customElement } from "lit/decorators";
-import { areaCompare } from "../../../../data/area_registry";
import type { LovelaceConfig } from "../../../../data/lovelace/config/types";
import type { LovelaceViewRawConfig } from "../../../../data/lovelace/config/view";
import type { HomeAssistant } from "../../../../types";
import type { AreaViewStrategyConfig } from "../area/area-view-strategy";
+import type { LovelaceStrategyEditor } from "../types";
+import type { AreasViewStrategyConfig } from "./areas-view-strategy";
+import { computeAreaPath, getAreas } from "./helpers/areas-strategy-helpers";
-export interface AreasDashboardStrategyConfig {}
+export interface AreasDashboardStrategyConfig {
+ type: "areas";
+ areas_display?: {
+ hidden?: string[];
+ order?: string[];
+ };
+}
@customElement("areas-dashboard-strategy")
export class AreasDashboardStrategy extends ReactiveElement {
static async generate(
- _config: AreasDashboardStrategyConfig,
+ config: AreasDashboardStrategyConfig,
hass: HomeAssistant
): Promise {
- const compare = areaCompare(hass.areas);
- const areas = Object.values(hass.areas).sort((a, b) =>
- compare(a.area_id, b.area_id)
+ const areas = getAreas(
+ hass.areas,
+ config.areas_display?.hidden,
+ config.areas_display?.order
);
- const areaViews = areas.map((area) => ({
- title: area.name,
- icon: area.icon || undefined,
- path: `areas-${area.area_id}`,
- subview: true,
- strategy: {
- type: "area",
- area: area.area_id,
- } satisfies AreaViewStrategyConfig,
- }));
+ const areaViews = areas.map((area) => {
+ const path = computeAreaPath(area.area_id);
+ return {
+ title: area.name,
+ icon: area.icon || undefined,
+ path: path,
+ strategy: {
+ type: "area",
+ area: area.area_id,
+ } satisfies AreaViewStrategyConfig,
+ };
+ });
return {
views: [
@@ -38,12 +49,18 @@ export class AreasDashboardStrategy extends ReactiveElement {
path: "home",
strategy: {
type: "areas",
- },
+ areas_display: config.areas_display,
+ } satisfies AreasViewStrategyConfig,
},
...areaViews,
],
};
}
+
+ public static async getConfigElement(): Promise {
+ await import("./editor/hui-areas-dashboard-strategy-editor");
+ return document.createElement("hui-areas-dashboard-strategy-editor");
+ }
}
declare global {
diff --git a/src/panels/lovelace/strategies/areas/areas-view-strategy.ts b/src/panels/lovelace/strategies/areas/areas-view-strategy.ts
index 502aebf4ac..c6069f9b1a 100644
--- a/src/panels/lovelace/strategies/areas/areas-view-strategy.ts
+++ b/src/panels/lovelace/strategies/areas/areas-view-strategy.ts
@@ -1,57 +1,81 @@
import { ReactiveElement } from "lit";
import { customElement } from "lit/decorators";
-import { areaCompare } from "../../../../data/area_registry";
import type { LovelaceSectionConfig } from "../../../../data/lovelace/config/section";
import type { LovelaceViewConfig } from "../../../../data/lovelace/config/view";
import type { HomeAssistant } from "../../../../types";
+import { getAreaGroupedEntities } from "../area/area-view-strategy";
+import { computeAreaPath, getAreas } from "./helpers/areas-strategy-helpers";
export interface AreasViewStrategyConfig {
type: "areas";
+ areas_display?: {
+ hidden?: string[];
+ order?: string[];
+ };
}
@customElement("areas-view-strategy")
export class AreasViewStrategy extends ReactiveElement {
static async generate(
- _config: AreasViewStrategyConfig,
+ config: AreasViewStrategyConfig,
hass: HomeAssistant
): Promise {
- const compare = areaCompare(hass.areas);
- const areas = Object.values(hass.areas).sort((a, b) =>
- compare(a.area_id, b.area_id)
+ const areas = getAreas(
+ hass.areas,
+ config.areas_display?.hidden,
+ config.areas_display?.order
);
- const areaSections = areas.map((area) => {
- const areaPath = `areas-${area.area_id}`;
- return {
- type: "grid",
- cards: [
- {
- type: "heading",
- heading: area.name,
- icon: area.icon || undefined,
- badges: [
- ...(area.temperature_entity_id
- ? [{ entity: area.temperature_entity_id }]
- : []),
- ...(area.humidity_entity_id
- ? [{ entity: area.humidity_entity_id }]
- : []),
- ],
- tap_action: {
- action: "navigate",
- navigation_path: areaPath,
+ const areaSections = areas
+ .map((area) => {
+ const path = computeAreaPath(area.area_id);
+
+ const groups = getAreaGroupedEntities(area.area_id, hass, true);
+
+ const entities = [
+ ...groups.lights,
+ ...groups.climate,
+ ...groups.media_players,
+ ...groups.security,
+ ];
+
+ return {
+ type: "grid",
+ cards: [
+ {
+ type: "heading",
+ heading: area.name,
+ icon: area.icon || undefined,
+ badges: [
+ ...(area.temperature_entity_id
+ ? [{ entity: area.temperature_entity_id }]
+ : []),
+ ...(area.humidity_entity_id
+ ? [{ entity: area.humidity_entity_id }]
+ : []),
+ ],
+ tap_action: {
+ action: "navigate",
+ navigation_path: path,
+ },
},
- },
- {
- type: "area",
- area: area.area_id,
- navigation_path: areaPath,
- alert_classes: [],
- sensor_classes: [],
- },
- ],
- };
- });
+ ...(entities.length
+ ? entities.map((entity) => ({
+ type: "tile",
+ entity: entity,
+ }))
+ : [
+ {
+ type: "markdown",
+ content: "No controllable devices in this area.",
+ },
+ ]),
+ ],
+ };
+ })
+ .filter(
+ (section): section is LovelaceSectionConfig => section !== undefined
+ );
return {
type: "sections",
diff --git a/src/panels/lovelace/strategies/areas/editor/hui-areas-dashboard-strategy-editor.ts b/src/panels/lovelace/strategies/areas/editor/hui-areas-dashboard-strategy-editor.ts
new file mode 100644
index 0000000000..c1a9586b50
--- /dev/null
+++ b/src/panels/lovelace/strategies/areas/editor/hui-areas-dashboard-strategy-editor.ts
@@ -0,0 +1,59 @@
+import { html, LitElement, nothing } from "lit";
+import { customElement, property, state } from "lit/decorators";
+import { fireEvent } from "../../../../../common/dom/fire_event";
+import "../../../../../components/ha-areas-display-editor";
+import type { AreasDisplayValue } from "../../../../../components/ha-areas-display-editor";
+import type { HomeAssistant } from "../../../../../types";
+import type { LovelaceStrategyEditor } from "../../types";
+import type { AreasDashboardStrategyConfig } from "../areas-dashboard-strategy";
+
+@customElement("hui-areas-dashboard-strategy-editor")
+export class HuiAreasDashboardStrategyEditor
+ extends LitElement
+ implements LovelaceStrategyEditor
+{
+ @property({ attribute: false }) public hass?: HomeAssistant;
+
+ @state()
+ private _config?: AreasDashboardStrategyConfig;
+
+ public setConfig(config: AreasDashboardStrategyConfig): void {
+ this._config = config;
+ }
+
+ protected render() {
+ if (!this.hass || !this._config) {
+ return nothing;
+ }
+
+ const value = this._config.areas_display;
+
+ return html`
+
+ `;
+ }
+
+ private _areaDisplayChanged(ev: CustomEvent): void {
+ const value = ev.detail.value as AreasDisplayValue;
+ const newConfig: AreasDashboardStrategyConfig = {
+ ...this._config!,
+ areas_display: value,
+ };
+
+ fireEvent(this, "config-changed", { config: newConfig });
+ }
+}
+
+declare global {
+ interface HTMLElementTagNameMap {
+ "hui-areas-dashboard-strategy-editor": HuiAreasDashboardStrategyEditor;
+ }
+}
diff --git a/src/panels/lovelace/strategies/areas/helpers/areas-strategy-helpers.ts b/src/panels/lovelace/strategies/areas/helpers/areas-strategy-helpers.ts
new file mode 100644
index 0000000000..3dde5af439
--- /dev/null
+++ b/src/panels/lovelace/strategies/areas/helpers/areas-strategy-helpers.ts
@@ -0,0 +1,25 @@
+import type { AreaRegistryEntry } from "../../../../../data/area_registry";
+import { areaCompare } from "../../../../../data/area_registry";
+import type { HomeAssistant } from "../../../../../types";
+
+export const getAreas = (
+ entries: HomeAssistant["areas"],
+ hiddenAreas?: string[],
+ areasOrder?: string[]
+): AreaRegistryEntry[] => {
+ const areas = Object.values(entries);
+
+ const filteredAreas = hiddenAreas
+ ? areas.filter((area) => !hiddenAreas!.includes(area.area_id))
+ : areas.concat();
+
+ const compare = areaCompare(entries, areasOrder);
+
+ const sortedAreas = filteredAreas.sort((areaA, areaB) =>
+ compare(areaA.area_id, areaB.area_id)
+ );
+
+ return sortedAreas;
+};
+
+export const computeAreaPath = (areaId: string): string => `areas-${areaId}`;
diff --git a/src/panels/lovelace/strategies/original-states/original-states-view-strategy.ts b/src/panels/lovelace/strategies/original-states/original-states-view-strategy.ts
index a128f79730..6336cc2611 100644
--- a/src/panels/lovelace/strategies/original-states/original-states-view-strategy.ts
+++ b/src/panels/lovelace/strategies/original-states/original-states-view-strategy.ts
@@ -2,7 +2,7 @@ import { STATE_NOT_RUNNING } from "home-assistant-js-websocket";
import { ReactiveElement } from "lit";
import { customElement } from "lit/decorators";
import { isComponentLoaded } from "../../../../common/config/is_component_loaded";
-import type { AreaFilterValue } from "../../../../components/ha-area-filter";
+import type { AreasDisplayValue } from "../../../../components/ha-areas-display-editor";
import { getEnergyPreferences } from "../../../../data/energy";
import type { LovelaceViewConfig } from "../../../../data/lovelace/config/view";
import type { HomeAssistant } from "../../../../types";
@@ -10,7 +10,7 @@ import { generateDefaultViewConfig } from "../../common/generate-lovelace-config
export interface OriginalStatesViewStrategyConfig {
type: "original-states";
- areas?: AreaFilterValue;
+ areas?: AreasDisplayValue;
hide_entities_without_area?: boolean;
hide_energy?: boolean;
}
diff --git a/src/translations/en.json b/src/translations/en.json
index ec0b89902e..b9700992b3 100644
--- a/src/translations/en.json
+++ b/src/translations/en.json
@@ -1127,6 +1127,10 @@
},
"multi-textfield": {
"add_item": "Add {item}"
+ },
+ "items-display-editor": {
+ "show": "Show {label}",
+ "hide": "Hide {label}"
}
},
"dialogs": {
@@ -3121,6 +3125,10 @@
"iframe": {
"title": "Webpage",
"description": "Integrate a webpage as a dashboard"
+ },
+ "areas": {
+ "title": "Areas (experimental)",
+ "description": "Display your devices with a view for each area"
}
}
},
@@ -7456,12 +7464,15 @@
},
"strategy": {
"original-states": {
- "areas": "Areas",
+ "areas": "Areas to display",
"hide_entities_without_area": "Hide entities without area",
"hide_energy": "Hide energy"
},
"iframe": {
"url": "URL"
+ },
+ "areas": {
+ "areas_display": "Areas to display"
}
},
"view": {