mirror of
https://github.com/home-assistant/frontend.git
synced 2025-07-24 09:46:36 +00:00
Add area dashboard settings (#24619)
* Add hidden and order settings * Share path logic * Add editor to sort and filter areas * Remove unused form * Add areas strategy in the dashboard picker * Move display editor * Fix min width * Add leading icon slot to expansion panel * Fix left chevron icon with dynamic property * Use area display in original state strategy * Rename selector * Rename to area_display * Remove ha-expansion-panel changes * Remove expanded * Fix rebase * Display all entities in the areas strategy overview (#24663) * Don't use subgroup * Add all entities in the overview * Add tile card features for area view
This commit is contained in:
parent
7aaea37db7
commit
64b9104199
30
src/common/entity/context/get_area_context.ts
Normal file
30
src/common/entity/context/get_area_context.ts
Normal file
@ -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,
|
||||
};
|
||||
};
|
@ -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,
|
||||
|
@ -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`
|
||||
<ha-list-item
|
||||
tabindex="0"
|
||||
role="button"
|
||||
hasMeta
|
||||
twoline
|
||||
graphic="icon"
|
||||
@click=${this._edit}
|
||||
@keydown=${this._edit}
|
||||
.disabled=${this.disabled}
|
||||
>
|
||||
<ha-svg-icon slot="graphic" .path=${mdiTextureBox}></ha-svg-icon>
|
||||
<span>${this.label}</span>
|
||||
<span slot="secondary">${description}</span>
|
||||
<ha-icon-next
|
||||
slot="meta"
|
||||
.label=${this.hass.localize("ui.common.edit")}
|
||||
></ha-icon-next>
|
||||
</ha-list-item>
|
||||
`;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
98
src/components/ha-areas-display-editor.ts
Normal file
98
src/components/ha-areas-display-editor.ts
Normal file
@ -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`
|
||||
<ha-expansion-panel
|
||||
outlined
|
||||
.header=${this.label}
|
||||
.expanded=${this.expanded}
|
||||
>
|
||||
<ha-svg-icon slot="leading-icon" .path=${mdiTextureBox}></ha-svg-icon>
|
||||
<ha-items-display-editor
|
||||
.hass=${this.hass}
|
||||
.items=${items}
|
||||
.value=${value}
|
||||
@value-changed=${this._areaDisplayChanged}
|
||||
></ha-items-display-editor>
|
||||
</ha-expansion-panel>
|
||||
`;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
241
src/components/ha-items-display-editor.ts
Normal file
241
src/components/ha-items-display-editor.ts
Normal file
@ -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`
|
||||
<ha-sortable
|
||||
draggable-selector=".draggable"
|
||||
handle-selector=".handle"
|
||||
@item-moved=${this._itemMoved}
|
||||
>
|
||||
<ha-md-list>
|
||||
${repeat(
|
||||
allItems,
|
||||
(item) => item.value,
|
||||
(item, _idx) => {
|
||||
const isVisible = !this.value.hidden.includes(item.value);
|
||||
const { label, value, description, icon, iconPath } = item;
|
||||
return html`
|
||||
<ha-md-list-item
|
||||
class=${classMap({
|
||||
hidden: !isVisible,
|
||||
draggable: isVisible,
|
||||
})}
|
||||
>
|
||||
<span slot="headline">${label}</span>
|
||||
${description
|
||||
? html`<span slot="supporting-text">${description}</span>`
|
||||
: nothing}
|
||||
${isVisible
|
||||
? html`
|
||||
<ha-svg-icon
|
||||
class="handle"
|
||||
.path=${mdiDrag}
|
||||
slot="start"
|
||||
></ha-svg-icon>
|
||||
`
|
||||
: html`<ha-svg-icon slot="start"></ha-svg-icon>`}
|
||||
${!showIcon
|
||||
? nothing
|
||||
: icon
|
||||
? html`
|
||||
<ha-icon
|
||||
class="icon"
|
||||
.icon=${icon}
|
||||
slot="start"
|
||||
></ha-icon>
|
||||
`
|
||||
: iconPath
|
||||
? html`
|
||||
<ha-svg-icon
|
||||
class="icon"
|
||||
.path=${iconPath}
|
||||
slot="start"
|
||||
></ha-svg-icon>
|
||||
`
|
||||
: nothing}
|
||||
<ha-icon-button
|
||||
.path=${isVisible ? mdiEye : mdiEyeOff}
|
||||
slot="end"
|
||||
.label=${this.hass.localize(
|
||||
`ui.components.items-display-editor.${isVisible ? "hide" : "show"}`,
|
||||
{
|
||||
label: label,
|
||||
}
|
||||
)}
|
||||
.value=${value}
|
||||
@click=${this._toggle}
|
||||
></ha-icon-button>
|
||||
${this.showNavigationButton
|
||||
? html`
|
||||
<ha-icon-button-next
|
||||
slot="end"
|
||||
.value=${value}
|
||||
@click=${this._navigate}
|
||||
></ha-icon-button-next>
|
||||
`
|
||||
: nothing}
|
||||
</ha-md-list-item>
|
||||
`;
|
||||
}
|
||||
)}
|
||||
</ha-md-list>
|
||||
</ha-sortable>
|
||||
`;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
@ -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`
|
||||
<ha-area-filter
|
||||
<ha-areas-display-editor
|
||||
.hass=${this.hass}
|
||||
.value=${this.value}
|
||||
.label=${this.label}
|
||||
.helper=${this.helper}
|
||||
.disabled=${this.disabled}
|
||||
.required=${this.required}
|
||||
></ha-area-filter>
|
||||
></ha-areas-display-editor>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-selector-area_filter": HaAreaFilterSelector;
|
||||
"ha-selector-areas_display": HaAreasDisplaySelector;
|
||||
}
|
||||
}
|
@ -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"),
|
||||
|
@ -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 {
|
||||
|
@ -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<AreaFilterDialogParams>
|
||||
{
|
||||
@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`
|
||||
<ha-dialog
|
||||
open
|
||||
@closed=${this._cancel}
|
||||
.heading=${this._dialogParams.title ??
|
||||
this.hass.localize("ui.components.area-filter.title")}
|
||||
>
|
||||
<ha-sortable
|
||||
draggable-selector=".draggable"
|
||||
handle-selector=".handle"
|
||||
@item-moved=${this._areaMoved}
|
||||
>
|
||||
<mwc-list class="areas">
|
||||
${repeat(
|
||||
allAreas,
|
||||
(area) => area,
|
||||
(area, _idx) => {
|
||||
const isVisible = !this._hidden.includes(area);
|
||||
const name = this.hass!.areas[area]?.name || area;
|
||||
return html`
|
||||
<ha-list-item
|
||||
class=${classMap({
|
||||
hidden: !isVisible,
|
||||
draggable: isVisible,
|
||||
})}
|
||||
hasMeta
|
||||
graphic="icon"
|
||||
noninteractive
|
||||
>
|
||||
${isVisible
|
||||
? html`<ha-svg-icon
|
||||
class="handle"
|
||||
.path=${mdiDrag}
|
||||
slot="graphic"
|
||||
></ha-svg-icon>`
|
||||
: nothing}
|
||||
${name}
|
||||
<ha-icon-button
|
||||
tabindex="0"
|
||||
class="action"
|
||||
.path=${isVisible ? mdiEye : mdiEyeOff}
|
||||
slot="meta"
|
||||
.label=${this.hass!.localize(
|
||||
`ui.components.area-filter.${
|
||||
isVisible ? "hide" : "show"
|
||||
}`,
|
||||
{ area: name }
|
||||
)}
|
||||
.area=${area}
|
||||
@click=${this._toggle}
|
||||
></ha-icon-button>
|
||||
</ha-list-item>
|
||||
`;
|
||||
}
|
||||
)}
|
||||
</mwc-list>
|
||||
</ha-sortable>
|
||||
<ha-button slot="secondaryAction" dialogAction="cancel">
|
||||
${this.hass.localize("ui.common.cancel")}
|
||||
</ha-button>
|
||||
<ha-button @click=${this._submit} slot="primaryAction">
|
||||
${this.hass.localize("ui.common.submit")}
|
||||
</ha-button>
|
||||
</ha-dialog>
|
||||
`;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
@ -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<AreaFilterValue | null>((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);
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
@ -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")
|
||||
|
@ -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 => {
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
@ -14,7 +14,7 @@ const SCHEMA = [
|
||||
{
|
||||
name: "areas",
|
||||
selector: {
|
||||
area_filter: {},
|
||||
areas_display: {},
|
||||
},
|
||||
},
|
||||
{
|
||||
|
@ -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<Group, string[]>;
|
||||
|
||||
type AreaFilteredByGroup = Record<Group, EntityFilterFunc[]>;
|
||||
|
||||
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<string[]>(
|
||||
(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,
|
||||
|
@ -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<LovelaceConfig> {
|
||||
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<LovelaceViewRawConfig>((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<LovelaceViewRawConfig>((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<LovelaceStrategyEditor> {
|
||||
await import("./editor/hui-areas-dashboard-strategy-editor");
|
||||
return document.createElement("hui-areas-dashboard-strategy-editor");
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
|
@ -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<LovelaceViewConfig> {
|
||||
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<LovelaceSectionConfig>((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<LovelaceSectionConfig | undefined>((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",
|
||||
|
@ -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`
|
||||
<ha-areas-display-editor
|
||||
.hass=${this.hass}
|
||||
.value=${value}
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.lovelace.editor.strategy.areas.areas_display"
|
||||
)}
|
||||
@value-changed=${this._areaDisplayChanged}
|
||||
expanded
|
||||
></ha-areas-display-editor>
|
||||
`;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
@ -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}`;
|
@ -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;
|
||||
}
|
||||
|
@ -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": {
|
||||
|
Loading…
x
Reference in New Issue
Block a user