mirror of
https://github.com/home-assistant/frontend.git
synced 2025-07-25 18:26:35 +00:00
Add entities filtering and reordering for areas strategy dashboard (#24677)
* Add entities editor * Save entities per domain and area * Use hidden and reorder logic in dashboard * Add overview hidden logic * Don't use icon for nav * Remove overview hidden * Change default text * Fix icons * Rename config properties
This commit is contained in:
parent
0e8be25a60
commit
2c0c48106d
@ -45,3 +45,22 @@ export const caseInsensitiveStringCompare = (
|
|||||||
|
|
||||||
return fallbackStringCompare(a.toLowerCase(), b.toLowerCase());
|
return fallbackStringCompare(a.toLowerCase(), b.toLowerCase());
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const orderCompare = (order: string[]) => (a: string, b: string) => {
|
||||||
|
const idxA = order.indexOf(a);
|
||||||
|
const idxB = order.indexOf(b);
|
||||||
|
|
||||||
|
if (idxA === idxB) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (idxA === -1) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (idxB === -1) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return idxA - idxB;
|
||||||
|
};
|
||||||
|
@ -33,8 +33,11 @@ export class HaAreasDisplayEditor extends LitElement {
|
|||||||
|
|
||||||
@property({ type: Boolean }) public required = false;
|
@property({ type: Boolean }) public required = false;
|
||||||
|
|
||||||
|
@property({ type: Boolean, attribute: "show-navigation-button" })
|
||||||
|
public showNavigationButton = false;
|
||||||
|
|
||||||
protected render(): TemplateResult {
|
protected render(): TemplateResult {
|
||||||
const compare = areaCompare(this.hass.areas, this.value?.order);
|
const compare = areaCompare(this.hass.areas);
|
||||||
|
|
||||||
const areas = Object.values(this.hass.areas).sort((areaA, areaB) =>
|
const areas = Object.values(this.hass.areas).sort((areaA, areaB) =>
|
||||||
compare(areaA.area_id, areaB.area_id)
|
compare(areaA.area_id, areaB.area_id)
|
||||||
@ -68,6 +71,7 @@ export class HaAreasDisplayEditor extends LitElement {
|
|||||||
.items=${items}
|
.items=${items}
|
||||||
.value=${value}
|
.value=${value}
|
||||||
@value-changed=${this._areaDisplayChanged}
|
@value-changed=${this._areaDisplayChanged}
|
||||||
|
.showNavigationButton=${this.showNavigationButton}
|
||||||
></ha-items-display-editor>
|
></ha-items-display-editor>
|
||||||
</ha-expansion-panel>
|
</ha-expansion-panel>
|
||||||
`;
|
`;
|
||||||
|
81
src/components/ha-entities-display-editor.ts
Normal file
81
src/components/ha-entities-display-editor.ts
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
import type { TemplateResult } from "lit";
|
||||||
|
import { LitElement, html } from "lit";
|
||||||
|
import { customElement, property } from "lit/decorators";
|
||||||
|
import { fireEvent } from "../common/dom/fire_event";
|
||||||
|
import { computeStateName } from "../common/entity/compute_state_name";
|
||||||
|
import { entityIcon } from "../data/icons";
|
||||||
|
import type { HomeAssistant } from "../types";
|
||||||
|
import "./ha-items-display-editor";
|
||||||
|
import type { DisplayItem, DisplayValue } from "./ha-items-display-editor";
|
||||||
|
|
||||||
|
export interface EntitiesDisplayValue {
|
||||||
|
hidden?: string[];
|
||||||
|
order?: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
@customElement("ha-entities-display-editor")
|
||||||
|
export class HaEntitiesDisplayEditor extends LitElement {
|
||||||
|
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||||
|
|
||||||
|
@property() public label?: string;
|
||||||
|
|
||||||
|
@property({ attribute: false }) public value?: EntitiesDisplayValue;
|
||||||
|
|
||||||
|
@property({ attribute: false }) public entitiesIds: string[] = [];
|
||||||
|
|
||||||
|
@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 entities = this.entitiesIds
|
||||||
|
.map((entityId) => this.hass.states[entityId])
|
||||||
|
.filter(Boolean);
|
||||||
|
|
||||||
|
const items: DisplayItem[] = entities.map((entity) => ({
|
||||||
|
value: entity.entity_id,
|
||||||
|
label: computeStateName(entity),
|
||||||
|
icon: entityIcon(this.hass, entity),
|
||||||
|
}));
|
||||||
|
|
||||||
|
const value: DisplayValue = {
|
||||||
|
order: this.value?.order ?? [],
|
||||||
|
hidden: this.value?.hidden ?? [],
|
||||||
|
};
|
||||||
|
|
||||||
|
return html`
|
||||||
|
<ha-items-display-editor
|
||||||
|
.hass=${this.hass}
|
||||||
|
.items=${items}
|
||||||
|
.value=${value}
|
||||||
|
@value-changed=${this._itemDisplayChanged}
|
||||||
|
></ha-items-display-editor>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _itemDisplayChanged(ev) {
|
||||||
|
ev.stopPropagation();
|
||||||
|
const value = ev.detail.value as DisplayValue;
|
||||||
|
const newValue: EntitiesDisplayValue = {
|
||||||
|
...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-entities-display-editor": HaEntitiesDisplayEditor;
|
||||||
|
}
|
||||||
|
}
|
@ -1,22 +1,26 @@
|
|||||||
import { ResizeController } from "@lit-labs/observers/resize-controller";
|
import { ResizeController } from "@lit-labs/observers/resize-controller";
|
||||||
import { mdiDrag, mdiEye, mdiEyeOff } from "@mdi/js";
|
import { mdiDrag, mdiEye, mdiEyeOff } from "@mdi/js";
|
||||||
|
import type { TemplateResult } from "lit";
|
||||||
import { LitElement, css, html, nothing } from "lit";
|
import { LitElement, css, html, nothing } from "lit";
|
||||||
import { customElement, property } from "lit/decorators";
|
import { customElement, property } from "lit/decorators";
|
||||||
import { classMap } from "lit/directives/class-map";
|
import { classMap } from "lit/directives/class-map";
|
||||||
|
import { ifDefined } from "lit/directives/if-defined";
|
||||||
import { repeat } from "lit/directives/repeat";
|
import { repeat } from "lit/directives/repeat";
|
||||||
|
import { until } from "lit/directives/until";
|
||||||
import memoizeOne from "memoize-one";
|
import memoizeOne from "memoize-one";
|
||||||
import { fireEvent } from "../common/dom/fire_event";
|
import { fireEvent } from "../common/dom/fire_event";
|
||||||
|
import { orderCompare } from "../common/string/compare";
|
||||||
import type { HomeAssistant } from "../types";
|
import type { HomeAssistant } from "../types";
|
||||||
import "./ha-icon";
|
import "./ha-icon";
|
||||||
import "./ha-icon-button";
|
import "./ha-icon-button";
|
||||||
import "./ha-icon-button-next";
|
import "./ha-icon-next";
|
||||||
import "./ha-md-list";
|
import "./ha-md-list";
|
||||||
import "./ha-md-list-item";
|
import "./ha-md-list-item";
|
||||||
import "./ha-sortable";
|
import "./ha-sortable";
|
||||||
import "./ha-svg-icon";
|
import "./ha-svg-icon";
|
||||||
|
|
||||||
export interface DisplayItem {
|
export interface DisplayItem {
|
||||||
icon?: string;
|
icon?: string | Promise<string | undefined>;
|
||||||
iconPath?: string;
|
iconPath?: string;
|
||||||
value: string;
|
value: string;
|
||||||
label: string;
|
label: string;
|
||||||
@ -52,6 +56,10 @@ export class HaItemDisplayEditor extends LitElement {
|
|||||||
hidden: [],
|
hidden: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@property({ attribute: false }) public actionsRenderer?: (
|
||||||
|
item: DisplayItem
|
||||||
|
) => TemplateResult<1> | typeof nothing;
|
||||||
|
|
||||||
private _showIcon = new ResizeController(this, {
|
private _showIcon = new ResizeController(this, {
|
||||||
callback: (entries) => entries[0]?.contentRect.width > 450,
|
callback: (entries) => entries[0]?.contentRect.width > 450,
|
||||||
});
|
});
|
||||||
@ -70,7 +78,11 @@ export class HaItemDisplayEditor extends LitElement {
|
|||||||
newHidden.push(value);
|
newHidden.push(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
const newVisibleItems = this._visibleItems(this.items, newHidden);
|
const newVisibleItems = this._visibleItems(
|
||||||
|
this.items,
|
||||||
|
newHidden,
|
||||||
|
this.value.order
|
||||||
|
);
|
||||||
const newOrder = newVisibleItems.map((a) => a.value);
|
const newOrder = newVisibleItems.map((a) => a.value);
|
||||||
|
|
||||||
this.value = {
|
this.value = {
|
||||||
@ -84,7 +96,11 @@ export class HaItemDisplayEditor extends LitElement {
|
|||||||
ev.stopPropagation();
|
ev.stopPropagation();
|
||||||
const { oldIndex, newIndex } = ev.detail;
|
const { oldIndex, newIndex } = ev.detail;
|
||||||
|
|
||||||
const visibleItems = this._visibleItems(this.items, this.value.hidden);
|
const visibleItems = this._visibleItems(
|
||||||
|
this.items,
|
||||||
|
this.value.hidden,
|
||||||
|
this.value.order
|
||||||
|
);
|
||||||
const newOrder = visibleItems.map((item) => item.value);
|
const newOrder = visibleItems.map((item) => item.value);
|
||||||
|
|
||||||
const movedItem = newOrder.splice(oldIndex, 1)[0];
|
const movedItem = newOrder.splice(oldIndex, 1)[0];
|
||||||
@ -103,8 +119,21 @@ export class HaItemDisplayEditor extends LitElement {
|
|||||||
ev.stopPropagation();
|
ev.stopPropagation();
|
||||||
}
|
}
|
||||||
|
|
||||||
private _visibleItems = memoizeOne((items: DisplayItem[], hidden: string[]) =>
|
private _visibleItems = memoizeOne(
|
||||||
items.filter((item) => !hidden.includes(item.value))
|
(items: DisplayItem[], hidden: string[], order: string[]) => {
|
||||||
|
const compare = orderCompare(order);
|
||||||
|
return items
|
||||||
|
.filter((item) => !hidden.includes(item.value))
|
||||||
|
.sort((a, b) => compare(a.value, b.value));
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
private _allItems = memoizeOne(
|
||||||
|
(items: DisplayItem[], hidden: string[], order: string[]) => {
|
||||||
|
const visibleItems = this._visibleItems(items, hidden, order);
|
||||||
|
const hiddenItems = this._hiddenItems(items, hidden);
|
||||||
|
return [...visibleItems, ...hiddenItems];
|
||||||
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
private _hiddenItems = memoizeOne((items: DisplayItem[], hidden: string[]) =>
|
private _hiddenItems = memoizeOne((items: DisplayItem[], hidden: string[]) =>
|
||||||
@ -112,10 +141,11 @@ export class HaItemDisplayEditor extends LitElement {
|
|||||||
);
|
);
|
||||||
|
|
||||||
protected render() {
|
protected render() {
|
||||||
const allItems = [
|
const allItems = this._allItems(
|
||||||
...this._visibleItems(this.items, this.value.hidden),
|
this.items,
|
||||||
...this._hiddenItems(this.items, this.value.hidden),
|
this.value.hidden,
|
||||||
];
|
this.value.order
|
||||||
|
);
|
||||||
|
|
||||||
const showIcon = this._showIcon.value;
|
const showIcon = this._showIcon.value;
|
||||||
return html`
|
return html`
|
||||||
@ -128,11 +158,18 @@ export class HaItemDisplayEditor extends LitElement {
|
|||||||
${repeat(
|
${repeat(
|
||||||
allItems,
|
allItems,
|
||||||
(item) => item.value,
|
(item) => item.value,
|
||||||
(item, _idx) => {
|
(item: DisplayItem, _idx) => {
|
||||||
const isVisible = !this.value.hidden.includes(item.value);
|
const isVisible = !this.value.hidden.includes(item.value);
|
||||||
const { label, value, description, icon, iconPath } = item;
|
const { label, value, description, icon, iconPath } = item;
|
||||||
return html`
|
return html`
|
||||||
<ha-md-list-item
|
<ha-md-list-item
|
||||||
|
type=${ifDefined(
|
||||||
|
this.showNavigationButton ? "button" : undefined
|
||||||
|
)}
|
||||||
|
@click=${this.showNavigationButton
|
||||||
|
? this._navigate
|
||||||
|
: undefined}
|
||||||
|
.value=${value}
|
||||||
class=${classMap({
|
class=${classMap({
|
||||||
hidden: !isVisible,
|
hidden: !isVisible,
|
||||||
draggable: isVisible,
|
draggable: isVisible,
|
||||||
@ -157,7 +194,7 @@ export class HaItemDisplayEditor extends LitElement {
|
|||||||
? html`
|
? html`
|
||||||
<ha-icon
|
<ha-icon
|
||||||
class="icon"
|
class="icon"
|
||||||
.icon=${icon}
|
.icon=${until(icon, "")}
|
||||||
slot="start"
|
slot="start"
|
||||||
></ha-icon>
|
></ha-icon>
|
||||||
`
|
`
|
||||||
@ -170,6 +207,11 @@ export class HaItemDisplayEditor extends LitElement {
|
|||||||
></ha-svg-icon>
|
></ha-svg-icon>
|
||||||
`
|
`
|
||||||
: nothing}
|
: nothing}
|
||||||
|
${this.actionsRenderer
|
||||||
|
? html`
|
||||||
|
<span slot="end"> ${this.actionsRenderer(item)} </span>
|
||||||
|
`
|
||||||
|
: nothing}
|
||||||
<ha-icon-button
|
<ha-icon-button
|
||||||
.path=${isVisible ? mdiEye : mdiEyeOff}
|
.path=${isVisible ? mdiEye : mdiEyeOff}
|
||||||
slot="end"
|
slot="end"
|
||||||
@ -183,13 +225,7 @@ export class HaItemDisplayEditor extends LitElement {
|
|||||||
@click=${this._toggle}
|
@click=${this._toggle}
|
||||||
></ha-icon-button>
|
></ha-icon-button>
|
||||||
${this.showNavigationButton
|
${this.showNavigationButton
|
||||||
? html`
|
? html` <ha-icon-next slot="end"></ha-icon-next> `
|
||||||
<ha-icon-button-next
|
|
||||||
slot="end"
|
|
||||||
.value=${value}
|
|
||||||
@click=${this._navigate}
|
|
||||||
></ha-icon-button-next>
|
|
||||||
`
|
|
||||||
: nothing}
|
: nothing}
|
||||||
</ha-md-list-item>
|
</ha-md-list-item>
|
||||||
`;
|
`;
|
||||||
|
@ -1,7 +1,5 @@
|
|||||||
import { ReactiveElement } from "lit";
|
import { ReactiveElement } from "lit";
|
||||||
import { customElement } from "lit/decorators";
|
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 { LovelaceBadgeConfig } from "../../../../data/lovelace/config/badge";
|
||||||
import type { LovelaceCardConfig } from "../../../../data/lovelace/config/card";
|
import type { LovelaceCardConfig } from "../../../../data/lovelace/config/card";
|
||||||
import type { LovelaceSectionRawConfig } from "../../../../data/lovelace/config/section";
|
import type { LovelaceSectionRawConfig } from "../../../../data/lovelace/config/section";
|
||||||
@ -13,118 +11,21 @@ import { supportsLightBrightnessCardFeature } from "../../card-features/hui-ligh
|
|||||||
import { supportsLockCommandsCardFeature } from "../../card-features/hui-lock-commands-card-feature";
|
import { supportsLockCommandsCardFeature } from "../../card-features/hui-lock-commands-card-feature";
|
||||||
import { supportsTargetTemperatureCardFeature } from "../../card-features/hui-target-temperature-card-feature";
|
import { supportsTargetTemperatureCardFeature } from "../../card-features/hui-target-temperature-card-feature";
|
||||||
import type { LovelaceCardFeatureConfig } from "../../card-features/types";
|
import type { LovelaceCardFeatureConfig } from "../../card-features/types";
|
||||||
|
import {
|
||||||
|
AREA_STRATEGY_GROUP_ICONS,
|
||||||
|
AREA_STRATEGY_GROUP_LABELS,
|
||||||
|
getAreaGroupedEntities,
|
||||||
|
} from "./helpers/area-strategy-helper";
|
||||||
|
|
||||||
type Group = "lights" | "climate" | "media_players" | "security";
|
export interface EntitiesDisplay {
|
||||||
|
hidden?: string[];
|
||||||
type AreaEntitiesByGroup = Record<Group, string[]>;
|
order?: 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 {
|
export interface AreaViewStrategyConfig {
|
||||||
type: "area";
|
type: "area";
|
||||||
area?: string;
|
area?: string;
|
||||||
|
groups_options?: Record<string, EntitiesDisplay>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const computeTileCardConfig =
|
const computeTileCardConfig =
|
||||||
@ -207,21 +108,24 @@ export class AreaViewStrategy extends ReactiveElement {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const groupedEntities = getAreaGroupedEntities(config.area, hass);
|
const groupedEntities = getAreaGroupedEntities(
|
||||||
|
config.area,
|
||||||
|
hass,
|
||||||
|
config.groups_options
|
||||||
|
);
|
||||||
|
|
||||||
const computeTileCard = computeTileCardConfig(hass);
|
const computeTileCard = computeTileCardConfig(hass);
|
||||||
|
|
||||||
const {
|
const { lights, climate, media_players, security } = groupedEntities;
|
||||||
lights,
|
|
||||||
climate,
|
|
||||||
media_players: mediaPlayers,
|
|
||||||
security,
|
|
||||||
} = groupedEntities;
|
|
||||||
if (lights.length > 0) {
|
if (lights.length > 0) {
|
||||||
sections.push({
|
sections.push({
|
||||||
type: "grid",
|
type: "grid",
|
||||||
cards: [
|
cards: [
|
||||||
computeHeadingCard("Lights", "mdi:lightbulb"),
|
computeHeadingCard(
|
||||||
|
AREA_STRATEGY_GROUP_LABELS.lights,
|
||||||
|
AREA_STRATEGY_GROUP_ICONS.lights
|
||||||
|
),
|
||||||
...lights.map(computeTileCard),
|
...lights.map(computeTileCard),
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
@ -231,18 +135,24 @@ export class AreaViewStrategy extends ReactiveElement {
|
|||||||
sections.push({
|
sections.push({
|
||||||
type: "grid",
|
type: "grid",
|
||||||
cards: [
|
cards: [
|
||||||
computeHeadingCard("Climate", "mdi:home-thermometer"),
|
computeHeadingCard(
|
||||||
|
AREA_STRATEGY_GROUP_LABELS.climate,
|
||||||
|
AREA_STRATEGY_GROUP_ICONS.climate
|
||||||
|
),
|
||||||
...climate.map(computeTileCard),
|
...climate.map(computeTileCard),
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (mediaPlayers.length > 0) {
|
if (media_players.length > 0) {
|
||||||
sections.push({
|
sections.push({
|
||||||
type: "grid",
|
type: "grid",
|
||||||
cards: [
|
cards: [
|
||||||
computeHeadingCard("Entertainment", "mdi:multimedia"),
|
computeHeadingCard(
|
||||||
...mediaPlayers.map(computeTileCard),
|
AREA_STRATEGY_GROUP_LABELS.media_players,
|
||||||
|
AREA_STRATEGY_GROUP_ICONS.media_players
|
||||||
|
),
|
||||||
|
...media_players.map(computeTileCard),
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -251,7 +161,10 @@ export class AreaViewStrategy extends ReactiveElement {
|
|||||||
sections.push({
|
sections.push({
|
||||||
type: "grid",
|
type: "grid",
|
||||||
cards: [
|
cards: [
|
||||||
computeHeadingCard("Security", "mdi:security"),
|
computeHeadingCard(
|
||||||
|
AREA_STRATEGY_GROUP_LABELS.security,
|
||||||
|
AREA_STRATEGY_GROUP_ICONS.security
|
||||||
|
),
|
||||||
...security.map(computeTileCard),
|
...security.map(computeTileCard),
|
||||||
],
|
],
|
||||||
});
|
});
|
@ -3,17 +3,25 @@ import { customElement } from "lit/decorators";
|
|||||||
import type { LovelaceConfig } from "../../../../data/lovelace/config/types";
|
import type { LovelaceConfig } from "../../../../data/lovelace/config/types";
|
||||||
import type { LovelaceViewRawConfig } from "../../../../data/lovelace/config/view";
|
import type { LovelaceViewRawConfig } from "../../../../data/lovelace/config/view";
|
||||||
import type { HomeAssistant } from "../../../../types";
|
import type { HomeAssistant } from "../../../../types";
|
||||||
import type { AreaViewStrategyConfig } from "../area/area-view-strategy";
|
import type {
|
||||||
|
AreaViewStrategyConfig,
|
||||||
|
EntitiesDisplay,
|
||||||
|
} from "./area-view-strategy";
|
||||||
import type { LovelaceStrategyEditor } from "../types";
|
import type { LovelaceStrategyEditor } from "../types";
|
||||||
import type { AreasViewStrategyConfig } from "./areas-view-strategy";
|
import type { AreasViewStrategyConfig } from "./areas-view-strategy";
|
||||||
import { computeAreaPath, getAreas } from "./helpers/areas-strategy-helpers";
|
import { computeAreaPath, getAreas } from "./helpers/areas-strategy-helpers";
|
||||||
|
|
||||||
|
interface AreaOptions {
|
||||||
|
groups_options?: Record<string, EntitiesDisplay>;
|
||||||
|
}
|
||||||
|
|
||||||
export interface AreasDashboardStrategyConfig {
|
export interface AreasDashboardStrategyConfig {
|
||||||
type: "areas";
|
type: "areas";
|
||||||
areas_display?: {
|
areas_display?: {
|
||||||
hidden?: string[];
|
hidden?: string[];
|
||||||
order?: string[];
|
order?: string[];
|
||||||
};
|
};
|
||||||
|
areas_options?: Record<string, AreaOptions>;
|
||||||
}
|
}
|
||||||
|
|
||||||
@customElement("areas-dashboard-strategy")
|
@customElement("areas-dashboard-strategy")
|
||||||
@ -30,13 +38,15 @@ export class AreasDashboardStrategy extends ReactiveElement {
|
|||||||
|
|
||||||
const areaViews = areas.map<LovelaceViewRawConfig>((area) => {
|
const areaViews = areas.map<LovelaceViewRawConfig>((area) => {
|
||||||
const path = computeAreaPath(area.area_id);
|
const path = computeAreaPath(area.area_id);
|
||||||
|
const areaConfig = config.areas_options?.[area.area_id];
|
||||||
|
|
||||||
return {
|
return {
|
||||||
title: area.name,
|
title: area.name,
|
||||||
icon: area.icon || undefined,
|
|
||||||
path: path,
|
path: path,
|
||||||
strategy: {
|
strategy: {
|
||||||
type: "area",
|
type: "area",
|
||||||
area: area.area_id,
|
area: area.area_id,
|
||||||
|
groups_options: areaConfig?.groups_options,
|
||||||
} satisfies AreaViewStrategyConfig,
|
} satisfies AreaViewStrategyConfig,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
@ -50,6 +60,7 @@ export class AreasDashboardStrategy extends ReactiveElement {
|
|||||||
strategy: {
|
strategy: {
|
||||||
type: "areas",
|
type: "areas",
|
||||||
areas_display: config.areas_display,
|
areas_display: config.areas_display,
|
||||||
|
areas_options: config.areas_options,
|
||||||
} satisfies AreasViewStrategyConfig,
|
} satisfies AreasViewStrategyConfig,
|
||||||
},
|
},
|
||||||
...areaViews,
|
...areaViews,
|
||||||
|
@ -3,8 +3,13 @@ import { customElement } from "lit/decorators";
|
|||||||
import type { LovelaceSectionConfig } from "../../../../data/lovelace/config/section";
|
import type { LovelaceSectionConfig } from "../../../../data/lovelace/config/section";
|
||||||
import type { LovelaceViewConfig } from "../../../../data/lovelace/config/view";
|
import type { LovelaceViewConfig } from "../../../../data/lovelace/config/view";
|
||||||
import type { HomeAssistant } from "../../../../types";
|
import type { HomeAssistant } from "../../../../types";
|
||||||
import { getAreaGroupedEntities } from "../area/area-view-strategy";
|
import { getAreaGroupedEntities } from "./helpers/area-strategy-helper";
|
||||||
import { computeAreaPath, getAreas } from "./helpers/areas-strategy-helpers";
|
import { computeAreaPath, getAreas } from "./helpers/areas-strategy-helpers";
|
||||||
|
import type { EntitiesDisplay } from "./area-view-strategy";
|
||||||
|
|
||||||
|
interface AreaOptions {
|
||||||
|
groups_options?: Record<string, EntitiesDisplay>;
|
||||||
|
}
|
||||||
|
|
||||||
export interface AreasViewStrategyConfig {
|
export interface AreasViewStrategyConfig {
|
||||||
type: "areas";
|
type: "areas";
|
||||||
@ -12,6 +17,7 @@ export interface AreasViewStrategyConfig {
|
|||||||
hidden?: string[];
|
hidden?: string[];
|
||||||
order?: string[];
|
order?: string[];
|
||||||
};
|
};
|
||||||
|
areas_options?: Record<string, AreaOptions>;
|
||||||
}
|
}
|
||||||
|
|
||||||
@customElement("areas-view-strategy")
|
@customElement("areas-view-strategy")
|
||||||
@ -30,7 +36,13 @@ export class AreasViewStrategy extends ReactiveElement {
|
|||||||
.map<LovelaceSectionConfig | undefined>((area) => {
|
.map<LovelaceSectionConfig | undefined>((area) => {
|
||||||
const path = computeAreaPath(area.area_id);
|
const path = computeAreaPath(area.area_id);
|
||||||
|
|
||||||
const groups = getAreaGroupedEntities(area.area_id, hass, true);
|
const areaConfig = config.areas_options?.[area.area_id];
|
||||||
|
|
||||||
|
const groups = getAreaGroupedEntities(
|
||||||
|
area.area_id,
|
||||||
|
hass,
|
||||||
|
areaConfig?.groups_options
|
||||||
|
);
|
||||||
|
|
||||||
const entities = [
|
const entities = [
|
||||||
...groups.lights,
|
...groups.lights,
|
||||||
@ -67,7 +79,7 @@ export class AreasViewStrategy extends ReactiveElement {
|
|||||||
: [
|
: [
|
||||||
{
|
{
|
||||||
type: "markdown",
|
type: "markdown",
|
||||||
content: "No controllable devices in this area.",
|
content: "No entities in this area.",
|
||||||
},
|
},
|
||||||
]),
|
]),
|
||||||
],
|
],
|
||||||
|
@ -1,9 +1,20 @@
|
|||||||
import { html, LitElement, nothing } from "lit";
|
import { css, html, LitElement, nothing } from "lit";
|
||||||
import { customElement, property, state } from "lit/decorators";
|
import { customElement, property, state } from "lit/decorators";
|
||||||
import { fireEvent } from "../../../../../common/dom/fire_event";
|
import { fireEvent } from "../../../../../common/dom/fire_event";
|
||||||
import "../../../../../components/ha-areas-display-editor";
|
import "../../../../../components/ha-areas-display-editor";
|
||||||
import type { AreasDisplayValue } from "../../../../../components/ha-areas-display-editor";
|
import type { AreasDisplayValue } from "../../../../../components/ha-areas-display-editor";
|
||||||
|
import "../../../../../components/ha-entities-display-editor";
|
||||||
|
import "../../../../../components/ha-icon-button";
|
||||||
|
import "../../../../../components/ha-icon-button-prev";
|
||||||
|
import "../../../../../components/ha-icon";
|
||||||
import type { HomeAssistant } from "../../../../../types";
|
import type { HomeAssistant } from "../../../../../types";
|
||||||
|
import type { AreaStrategyGroup } from "../helpers/area-strategy-helper";
|
||||||
|
import {
|
||||||
|
AREA_STRATEGY_GROUP_ICONS,
|
||||||
|
AREA_STRATEGY_GROUPS,
|
||||||
|
AREA_STRATEGY_GROUP_LABELS,
|
||||||
|
getAreaGroupedEntities,
|
||||||
|
} from "../helpers/area-strategy-helper";
|
||||||
import type { LovelaceStrategyEditor } from "../../types";
|
import type { LovelaceStrategyEditor } from "../../types";
|
||||||
import type { AreasDashboardStrategyConfig } from "../areas-dashboard-strategy";
|
import type { AreasDashboardStrategyConfig } from "../areas-dashboard-strategy";
|
||||||
|
|
||||||
@ -21,11 +32,62 @@ export class HuiAreasDashboardStrategyEditor
|
|||||||
this._config = config;
|
this._config = config;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@state()
|
||||||
|
private _area?: string;
|
||||||
|
|
||||||
protected render() {
|
protected render() {
|
||||||
if (!this.hass || !this._config) {
|
if (!this.hass || !this._config) {
|
||||||
return nothing;
|
return nothing;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this._area) {
|
||||||
|
const groups = getAreaGroupedEntities(this._area, this.hass);
|
||||||
|
|
||||||
|
const area = this.hass.areas[this._area];
|
||||||
|
|
||||||
|
return html`
|
||||||
|
<div class="toolbar">
|
||||||
|
<ha-icon-button-prev @click=${this._back}></ha-icon-button-prev>
|
||||||
|
<p>${area.name}</p>
|
||||||
|
</div>
|
||||||
|
${AREA_STRATEGY_GROUPS.map((group) => {
|
||||||
|
const entities = groups[group] || [];
|
||||||
|
const value =
|
||||||
|
this._config!.areas_options?.[this._area!]?.groups_options?.[group];
|
||||||
|
|
||||||
|
return html`
|
||||||
|
<ha-expansion-panel
|
||||||
|
header=${AREA_STRATEGY_GROUP_LABELS[group]}
|
||||||
|
expanded
|
||||||
|
outlined
|
||||||
|
>
|
||||||
|
<ha-icon
|
||||||
|
slot="leading-icon"
|
||||||
|
.icon=${AREA_STRATEGY_GROUP_ICONS[group]}
|
||||||
|
></ha-icon>
|
||||||
|
${entities.length
|
||||||
|
? html`
|
||||||
|
<ha-entities-display-editor
|
||||||
|
.hass=${this.hass}
|
||||||
|
.value=${value}
|
||||||
|
.label=${group}
|
||||||
|
@value-changed=${this._entitiesDisplayChanged}
|
||||||
|
.group=${group}
|
||||||
|
.area=${this._area}
|
||||||
|
.entitiesIds=${entities}
|
||||||
|
></ha-entities-display-editor>
|
||||||
|
`
|
||||||
|
: html`
|
||||||
|
<p>
|
||||||
|
No entities in this section, it will not be displayed.
|
||||||
|
</p>
|
||||||
|
`}
|
||||||
|
</ha-expansion-panel>
|
||||||
|
`;
|
||||||
|
})}
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
const value = this._config.areas_display;
|
const value = this._config.areas_display;
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
@ -35,13 +97,25 @@ export class HuiAreasDashboardStrategyEditor
|
|||||||
.label=${this.hass.localize(
|
.label=${this.hass.localize(
|
||||||
"ui.panel.lovelace.editor.strategy.areas.areas_display"
|
"ui.panel.lovelace.editor.strategy.areas.areas_display"
|
||||||
)}
|
)}
|
||||||
@value-changed=${this._areaDisplayChanged}
|
@value-changed=${this._areasDisplayChanged}
|
||||||
expanded
|
expanded
|
||||||
|
show-navigation-button
|
||||||
|
@item-display-navigate-clicked=${this._handleAreaNavigate}
|
||||||
></ha-areas-display-editor>
|
></ha-areas-display-editor>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
private _areaDisplayChanged(ev: CustomEvent): void {
|
private _back(): void {
|
||||||
|
if (this._area) {
|
||||||
|
this._area = undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private _handleAreaNavigate(ev: CustomEvent): void {
|
||||||
|
this._area = ev.detail.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _areasDisplayChanged(ev: CustomEvent): void {
|
||||||
const value = ev.detail.value as AreasDisplayValue;
|
const value = ev.detail.value as AreasDisplayValue;
|
||||||
const newConfig: AreasDashboardStrategyConfig = {
|
const newConfig: AreasDashboardStrategyConfig = {
|
||||||
...this._config!,
|
...this._config!,
|
||||||
@ -50,6 +124,45 @@ export class HuiAreasDashboardStrategyEditor
|
|||||||
|
|
||||||
fireEvent(this, "config-changed", { config: newConfig });
|
fireEvent(this, "config-changed", { config: newConfig });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private _entitiesDisplayChanged(ev: CustomEvent): void {
|
||||||
|
const value = ev.detail.value as AreasDisplayValue;
|
||||||
|
|
||||||
|
const { group, area } = ev.currentTarget as unknown as {
|
||||||
|
group: AreaStrategyGroup;
|
||||||
|
area: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const newConfig: AreasDashboardStrategyConfig = {
|
||||||
|
...this._config!,
|
||||||
|
areas_options: {
|
||||||
|
...this._config!.areas_options,
|
||||||
|
[area]: {
|
||||||
|
...this._config!.areas_options?.[area],
|
||||||
|
groups_options: {
|
||||||
|
...this._config!.areas_options?.[area]?.groups_options,
|
||||||
|
[group]: value,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
fireEvent(this, "config-changed", { config: newConfig });
|
||||||
|
}
|
||||||
|
|
||||||
|
static get styles() {
|
||||||
|
return [
|
||||||
|
css`
|
||||||
|
.toolbar {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
ha-expansion-panel {
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
|
@ -0,0 +1,149 @@
|
|||||||
|
import type { EntityFilterFunc } from "../../../../../common/entity/entity_filter";
|
||||||
|
import { generateEntityFilter } from "../../../../../common/entity/entity_filter";
|
||||||
|
import { orderCompare } from "../../../../../common/string/compare";
|
||||||
|
import type { HomeAssistant } from "../../../../../types";
|
||||||
|
|
||||||
|
export const AREA_STRATEGY_GROUPS = [
|
||||||
|
"lights",
|
||||||
|
"climate",
|
||||||
|
"media_players",
|
||||||
|
"security",
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
export const AREA_STRATEGY_GROUP_ICONS = {
|
||||||
|
lights: "mdi:lightbulb",
|
||||||
|
climate: "mdi:home-thermometer",
|
||||||
|
media_players: "mdi:multimedia",
|
||||||
|
security: "mdi:security",
|
||||||
|
};
|
||||||
|
|
||||||
|
// Todo be replace by translation when validated
|
||||||
|
export const AREA_STRATEGY_GROUP_LABELS = {
|
||||||
|
lights: "Lights",
|
||||||
|
climate: "Climate",
|
||||||
|
media_players: "Entertainment",
|
||||||
|
security: "Security",
|
||||||
|
};
|
||||||
|
|
||||||
|
export type AreaStrategyGroup = (typeof AREA_STRATEGY_GROUPS)[number];
|
||||||
|
|
||||||
|
type AreaEntitiesByGroup = Record<AreaStrategyGroup, string[]>;
|
||||||
|
|
||||||
|
type AreaFilteredByGroup = Record<AreaStrategyGroup, EntityFilterFunc[]>;
|
||||||
|
|
||||||
|
interface DisplayOptions {
|
||||||
|
hidden?: string[];
|
||||||
|
order?: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
type AreaGroupsDisplayOptions = Record<string, DisplayOptions>;
|
||||||
|
|
||||||
|
export const getAreaGroupedEntities = (
|
||||||
|
area: string,
|
||||||
|
hass: HomeAssistant,
|
||||||
|
displayOptions?: AreaGroupsDisplayOptions
|
||||||
|
): 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",
|
||||||
|
}),
|
||||||
|
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",
|
||||||
|
}),
|
||||||
|
generateEntityFilter(hass, {
|
||||||
|
domain: "binary_sensor",
|
||||||
|
device_class: ["door", "garage_door"],
|
||||||
|
area: area,
|
||||||
|
entity_category: "none",
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
return Object.fromEntries(
|
||||||
|
Object.entries(groupedFilters).map(([group, filters]) => {
|
||||||
|
const entities = filters.reduce<string[]>(
|
||||||
|
(acc, filter) => [
|
||||||
|
...acc,
|
||||||
|
...allEntities.filter((entity) => filter(entity)),
|
||||||
|
],
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
|
const hidden = displayOptions?.[group]?.hidden
|
||||||
|
? new Set(displayOptions[group].hidden)
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
const order = displayOptions?.[group]?.order;
|
||||||
|
|
||||||
|
let filteredEntities = entities;
|
||||||
|
if (hidden) {
|
||||||
|
filteredEntities = entities.filter(
|
||||||
|
(entity: string) => !hidden.has(entity)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (order) {
|
||||||
|
filteredEntities = filteredEntities.concat().sort(orderCompare(order));
|
||||||
|
}
|
||||||
|
return [group, filteredEntities];
|
||||||
|
})
|
||||||
|
) as AreaEntitiesByGroup;
|
||||||
|
};
|
@ -32,7 +32,7 @@ const STRATEGIES: Record<LovelaceStrategyConfigType, Record<string, any>> = {
|
|||||||
energy: () => import("../../energy/strategies/energy-view-strategy"),
|
energy: () => import("../../energy/strategies/energy-view-strategy"),
|
||||||
map: () => import("./map/map-view-strategy"),
|
map: () => import("./map/map-view-strategy"),
|
||||||
iframe: () => import("./iframe/iframe-view-strategy"),
|
iframe: () => import("./iframe/iframe-view-strategy"),
|
||||||
area: () => import("./area/area-view-strategy"),
|
area: () => import("./areas/area-view-strategy"),
|
||||||
areas: () => import("./areas/areas-view-strategy"),
|
areas: () => import("./areas/areas-view-strategy"),
|
||||||
},
|
},
|
||||||
section: {},
|
section: {},
|
||||||
|
Loading…
x
Reference in New Issue
Block a user