mirror of
https://github.com/home-assistant/frontend.git
synced 2026-04-02 00:27:49 +01:00
Allow specific entity controls in Area card (#29025)
Co-authored-by: Wendelin <12148533+wendevlin@users.noreply.github.com>
This commit is contained in:
@@ -5,6 +5,7 @@ import { classMap } from "lit/directives/class-map";
|
||||
import { styleMap } from "lit/directives/style-map";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { ensureArray } from "../../../common/array/ensure-array";
|
||||
import { computeDomain } from "../../../common/entity/compute_domain";
|
||||
import { generateEntityFilter } from "../../../common/entity/entity_filter";
|
||||
import {
|
||||
computeGroupEntitiesState,
|
||||
@@ -15,6 +16,7 @@ import { domainColorProperties } from "../../../common/entity/state_color";
|
||||
import "../../../components/ha-control-button";
|
||||
import "../../../components/ha-control-button-group";
|
||||
import "../../../components/ha-domain-icon";
|
||||
import "../../../components/ha-state-icon";
|
||||
import "../../../components/ha-svg-icon";
|
||||
import type { AreaRegistryEntry } from "../../../data/area/area_registry";
|
||||
import { forwardHaptic } from "../../../data/haptics";
|
||||
@@ -23,13 +25,14 @@ import type { HomeAssistant } from "../../../types";
|
||||
import type { AreaCardFeatureContext } from "../cards/hui-area-card";
|
||||
import type { LovelaceCardFeature, LovelaceCardFeatureEditor } from "../types";
|
||||
import { cardFeatureStyles } from "./common/card-feature-styles";
|
||||
import type {
|
||||
AreaControl,
|
||||
AreaControlsCardFeatureConfig,
|
||||
LovelaceCardFeatureContext,
|
||||
LovelaceCardFeaturePosition,
|
||||
import {
|
||||
AREA_CONTROL_DOMAINS,
|
||||
type AreaControl,
|
||||
type AreaControlDomain,
|
||||
type AreaControlsCardFeatureConfig,
|
||||
type LovelaceCardFeatureContext,
|
||||
type LovelaceCardFeaturePosition,
|
||||
} from "./types";
|
||||
import { AREA_CONTROLS } from "./types";
|
||||
|
||||
interface AreaControlsButton {
|
||||
filter: {
|
||||
@@ -38,6 +41,14 @@ interface AreaControlsButton {
|
||||
};
|
||||
}
|
||||
|
||||
type NormalizedControl =
|
||||
| { type: "domain"; value: AreaControlDomain }
|
||||
| { type: "entity"; value: string };
|
||||
|
||||
interface ControlButtonElement extends HTMLElement {
|
||||
control: NormalizedControl;
|
||||
}
|
||||
|
||||
const coverButton = (deviceClass: string) => ({
|
||||
filter: {
|
||||
domain: "cover",
|
||||
@@ -45,7 +56,10 @@ const coverButton = (deviceClass: string) => ({
|
||||
},
|
||||
});
|
||||
|
||||
export const AREA_CONTROLS_BUTTONS: Record<AreaControl, AreaControlsButton> = {
|
||||
export const AREA_CONTROLS_BUTTONS: Record<
|
||||
AreaControlDomain,
|
||||
AreaControlsButton
|
||||
> = {
|
||||
light: {
|
||||
filter: {
|
||||
domain: "light",
|
||||
@@ -82,11 +96,11 @@ export const supportsAreaControlsCardFeature = (
|
||||
};
|
||||
|
||||
export const getAreaControlEntities = (
|
||||
controls: AreaControl[],
|
||||
controls: AreaControlDomain[],
|
||||
areaId: string,
|
||||
excludeEntities: string[] | undefined,
|
||||
hass: HomeAssistant
|
||||
): Record<AreaControl, string[]> =>
|
||||
): Record<AreaControlDomain, string[]> =>
|
||||
controls.reduce(
|
||||
(acc, control) => {
|
||||
const controlButton = AREA_CONTROLS_BUTTONS[control];
|
||||
@@ -101,7 +115,7 @@ export const getAreaControlEntities = (
|
||||
);
|
||||
return acc;
|
||||
},
|
||||
{} as Record<AreaControl, string[]>
|
||||
{} as Record<AreaControlDomain, string[]>
|
||||
);
|
||||
|
||||
export const MAX_DEFAULT_AREA_CONTROLS = 4;
|
||||
@@ -129,10 +143,23 @@ class HuiAreaControlsCardFeature
|
||||
| undefined;
|
||||
}
|
||||
|
||||
private get _controls() {
|
||||
return (
|
||||
this._config?.controls || (AREA_CONTROLS as unknown as AreaControl[])
|
||||
);
|
||||
private get _controls(): AreaControl[] {
|
||||
return this._config?.controls || [...AREA_CONTROL_DOMAINS];
|
||||
}
|
||||
|
||||
private _normalizeControl(control: AreaControl): NormalizedControl {
|
||||
// Handle explicit entity format
|
||||
if (typeof control === "object" && "entity_id" in control) {
|
||||
return { type: "entity", value: control.entity_id };
|
||||
}
|
||||
|
||||
// String format: domain control (if valid) or invalid
|
||||
if (AREA_CONTROL_DOMAINS.includes(control as AreaControlDomain)) {
|
||||
return { type: "domain", value: control as AreaControlDomain };
|
||||
}
|
||||
|
||||
// Invalid domain string - treat as entity
|
||||
return { type: "entity", value: control };
|
||||
}
|
||||
|
||||
static getStubConfig(): AreaControlsCardFeatureConfig {
|
||||
@@ -156,22 +183,37 @@ class HuiAreaControlsCardFeature
|
||||
private _handleButtonTap(ev: MouseEvent) {
|
||||
ev.stopPropagation();
|
||||
|
||||
if (!this.context?.area_id || !this.hass || !this._config) {
|
||||
if (!this.hass || !this._config) {
|
||||
return;
|
||||
}
|
||||
const control = (ev.currentTarget as any).control as AreaControl;
|
||||
|
||||
const normalized = (ev.currentTarget as ControlButtonElement).control;
|
||||
|
||||
if (normalized.type === "entity") {
|
||||
const entity = this.hass.states[normalized.value];
|
||||
if (entity) {
|
||||
forwardHaptic(this, "light");
|
||||
toggleGroupEntities(this.hass, [entity]);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.context?.area_id) {
|
||||
return;
|
||||
}
|
||||
|
||||
const domainControls = this._domainControls(this._controls);
|
||||
|
||||
const controlEntities = this._controlEntities(
|
||||
this._controls,
|
||||
domainControls,
|
||||
this.context.area_id,
|
||||
this.context.exclude_entities,
|
||||
this.hass!.entities,
|
||||
this.hass!.devices,
|
||||
this.hass!.areas
|
||||
this.hass.entities,
|
||||
this.hass.devices,
|
||||
this.hass.areas
|
||||
);
|
||||
const entitiesIds = controlEntities[control];
|
||||
|
||||
const entities = entitiesIds
|
||||
const entities = controlEntities[normalized.value]
|
||||
.map((entityId) => this.hass!.states[entityId] as HassEntity | undefined)
|
||||
.filter((v): v is HassEntity => Boolean(v));
|
||||
|
||||
@@ -179,9 +221,19 @@ class HuiAreaControlsCardFeature
|
||||
toggleGroupEntities(this.hass, entities);
|
||||
}
|
||||
|
||||
private _domainControls = memoizeOne((controls: AreaControl[]) =>
|
||||
controls
|
||||
.map((c) => this._normalizeControl(c))
|
||||
.filter(
|
||||
(n): n is { type: "domain"; value: AreaControlDomain } =>
|
||||
n.type === "domain"
|
||||
)
|
||||
.map((n) => n.value)
|
||||
);
|
||||
|
||||
private _controlEntities = memoizeOne(
|
||||
(
|
||||
controls: AreaControl[],
|
||||
controls: AreaControlDomain[],
|
||||
areaId: string,
|
||||
excludeEntities: string[] | undefined,
|
||||
// needed to update memoized function when entities, devices or areas change
|
||||
@@ -202,8 +254,15 @@ class HuiAreaControlsCardFeature
|
||||
return nothing;
|
||||
}
|
||||
|
||||
const normalizedControls = this._controls.map((c) =>
|
||||
this._normalizeControl(c)
|
||||
);
|
||||
|
||||
// Get domain controls for entity lookup
|
||||
const domainControls = this._domainControls(this._controls);
|
||||
|
||||
const controlEntities = this._controlEntities(
|
||||
this._controls,
|
||||
domainControls,
|
||||
this.context.area_id!,
|
||||
this.context.exclude_entities,
|
||||
this.hass!.entities,
|
||||
@@ -211,13 +270,17 @@ class HuiAreaControlsCardFeature
|
||||
this.hass!.areas
|
||||
);
|
||||
|
||||
const supportedControls = this._controls.filter(
|
||||
(control) => controlEntities[control].length > 0
|
||||
// Filter controls while preserving original order
|
||||
const allControls = normalizedControls.filter((n) =>
|
||||
n.type === "domain"
|
||||
? controlEntities[n.value].length > 0
|
||||
: this.hass!.states[n.value] &&
|
||||
!this.context?.exclude_entities?.includes(n.value)
|
||||
);
|
||||
|
||||
const displayControls = this._config.controls
|
||||
? supportedControls
|
||||
: supportedControls.slice(0, MAX_DEFAULT_AREA_CONTROLS); // Limit to max if using default controls
|
||||
? allControls
|
||||
: allControls.slice(0, MAX_DEFAULT_AREA_CONTROLS); // Limit to max if using default controls
|
||||
|
||||
if (!displayControls.length) {
|
||||
return nothing;
|
||||
@@ -229,35 +292,54 @@ class HuiAreaControlsCardFeature
|
||||
"no-stretch": this.position === "inline",
|
||||
})}
|
||||
>
|
||||
${displayControls.map((control) => {
|
||||
const button = AREA_CONTROLS_BUTTONS[control];
|
||||
${displayControls.map((normalized) => {
|
||||
let active: boolean;
|
||||
let label: string;
|
||||
let domain: string;
|
||||
let deviceClass: string | undefined;
|
||||
let entityState: string;
|
||||
let entity: HassEntity | undefined;
|
||||
|
||||
const entityIds = controlEntities[control];
|
||||
if (normalized.type === "domain") {
|
||||
const button = AREA_CONTROLS_BUTTONS[normalized.value];
|
||||
const controlEntityIds = controlEntities[normalized.value];
|
||||
|
||||
const entities = entityIds
|
||||
.map(
|
||||
(entityId) =>
|
||||
this.hass!.states[entityId] as HassEntity | undefined
|
||||
)
|
||||
.filter((v): v is HassEntity => Boolean(v));
|
||||
const entities = controlEntityIds
|
||||
.map(
|
||||
(entityId) =>
|
||||
this.hass!.states[entityId] as HassEntity | undefined
|
||||
)
|
||||
.filter((v): v is HassEntity => Boolean(v));
|
||||
|
||||
const groupState = computeGroupEntitiesState(entities);
|
||||
const groupState = computeGroupEntitiesState(entities);
|
||||
|
||||
const active = entities[0]
|
||||
? stateActive(entities[0], groupState)
|
||||
: false;
|
||||
active = entities[0] ? stateActive(entities[0], groupState) : false;
|
||||
label = this.hass!.localize(
|
||||
`ui.card_features.area_controls.${normalized.value}.${active ? "off" : "on"}`
|
||||
);
|
||||
domain = button.filter.domain;
|
||||
deviceClass = button.filter.device_class
|
||||
? ensureArray(button.filter.device_class)[0]
|
||||
: undefined;
|
||||
entityState = groupState;
|
||||
} else if (normalized.type === "entity") {
|
||||
entity = this.hass!.states[normalized.value];
|
||||
if (!entity) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
const label = this.hass!.localize(
|
||||
`ui.card_features.area_controls.${control}.${active ? "off" : "on"}`
|
||||
);
|
||||
|
||||
const domain = button.filter.domain;
|
||||
const deviceClass = button.filter.device_class
|
||||
? ensureArray(button.filter.device_class)[0]
|
||||
: undefined;
|
||||
active = stateActive(entity);
|
||||
label = this.hass!.localize(
|
||||
`ui.card.common.turn_${active ? "off" : "on"}`
|
||||
);
|
||||
domain = computeDomain(entity.entity_id);
|
||||
entityState = entity.state;
|
||||
} else {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
const activeColor = computeCssVariable(
|
||||
domainColorProperties(domain, deviceClass, groupState, true)
|
||||
domainColorProperties(domain, deviceClass, entityState, true)
|
||||
);
|
||||
|
||||
return html`
|
||||
@@ -268,15 +350,20 @@ class HuiAreaControlsCardFeature
|
||||
.title=${label}
|
||||
aria-label=${label}
|
||||
class=${active ? "active" : ""}
|
||||
.control=${control}
|
||||
.control=${normalized}
|
||||
@click=${this._handleButtonTap}
|
||||
>
|
||||
<ha-domain-icon
|
||||
.hass=${this.hass}
|
||||
.domain=${domain}
|
||||
.deviceClass=${deviceClass}
|
||||
.state=${groupState}
|
||||
></ha-domain-icon>
|
||||
${normalized.type === "domain"
|
||||
? html`<ha-domain-icon
|
||||
.hass=${this.hass}
|
||||
.domain=${domain}
|
||||
.deviceClass=${deviceClass}
|
||||
.state=${entityState}
|
||||
></ha-domain-icon>`
|
||||
: html`<ha-state-icon
|
||||
.hass=${this.hass}
|
||||
.stateObj=${entity}
|
||||
></ha-state-icon>`}
|
||||
</ha-control-button>
|
||||
`;
|
||||
})}
|
||||
|
||||
@@ -202,7 +202,7 @@ export interface TrendGraphCardFeatureConfig {
|
||||
detail?: boolean;
|
||||
}
|
||||
|
||||
export const AREA_CONTROLS = [
|
||||
export const AREA_CONTROL_DOMAINS = [
|
||||
"light",
|
||||
"fan",
|
||||
"cover-shutter",
|
||||
@@ -218,7 +218,9 @@ export const AREA_CONTROLS = [
|
||||
"switch",
|
||||
] as const;
|
||||
|
||||
export type AreaControl = (typeof AREA_CONTROLS)[number];
|
||||
export type AreaControlDomain = (typeof AREA_CONTROL_DOMAINS)[number];
|
||||
|
||||
export type AreaControl = AreaControlDomain | { entity_id: string };
|
||||
|
||||
export interface AreaControlsCardFeatureConfig {
|
||||
type: "area-controls";
|
||||
|
||||
@@ -1,26 +1,54 @@
|
||||
import { html, LitElement, nothing } from "lit";
|
||||
import type { HassEntity } from "home-assistant-js-websocket";
|
||||
import { mdiDragHorizontalVariant } from "@mdi/js";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { repeat } from "lit/directives/repeat";
|
||||
import Fuse from "fuse.js";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import { computeEntityNameList } from "../../../../common/entity/compute_entity_name_display";
|
||||
import { computeStateName } from "../../../../common/entity/compute_state_name";
|
||||
import { computeRTL } from "../../../../common/util/compute_rtl";
|
||||
import type { LocalizeFunc } from "../../../../common/translations/localize";
|
||||
import {
|
||||
multiTermSortedSearch,
|
||||
type FuseWeightedKey,
|
||||
} from "../../../../resources/fuseMultiTerm";
|
||||
import "../../../../components/ha-combo-box-item";
|
||||
import "../../../../components/ha-domain-icon";
|
||||
import "../../../../components/ha-form/ha-form";
|
||||
import type {
|
||||
HaFormSchema,
|
||||
SchemaUnion,
|
||||
} from "../../../../components/ha-form/types";
|
||||
import "../../../../components/ha-generic-picker";
|
||||
import type { PickerComboBoxItem } from "../../../../components/ha-picker-combo-box";
|
||||
import "../../../../components/chips/ha-input-chip";
|
||||
import "../../../../components/ha-sortable";
|
||||
import "../../../../components/ha-state-icon";
|
||||
import "../../../../components/ha-svg-icon";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
import {
|
||||
AREA_CONTROLS_BUTTONS,
|
||||
getAreaControlEntities,
|
||||
MAX_DEFAULT_AREA_CONTROLS,
|
||||
} from "../../card-features/hui-area-controls-card-feature";
|
||||
import {
|
||||
AREA_CONTROLS,
|
||||
AREA_CONTROL_DOMAINS,
|
||||
type AreaControl,
|
||||
type AreaControlDomain,
|
||||
type AreaControlsCardFeatureConfig,
|
||||
} from "../../card-features/types";
|
||||
import type { AreaCardFeatureContext } from "../../cards/hui-area-card";
|
||||
import type { LovelaceCardFeatureEditor } from "../../types";
|
||||
|
||||
interface AreaControlPickerItem extends PickerComboBoxItem {
|
||||
type?: "domain" | "entity";
|
||||
stateObj?: HassEntity;
|
||||
domain?: string;
|
||||
deviceClass?: string;
|
||||
}
|
||||
|
||||
type AreaControlsCardFeatureData = AreaControlsCardFeatureConfig & {
|
||||
customize_controls: boolean;
|
||||
};
|
||||
@@ -40,40 +68,14 @@ export class HuiAreaControlsCardFeatureEditor
|
||||
this._config = config;
|
||||
}
|
||||
|
||||
private _schema = memoizeOne(
|
||||
(
|
||||
localize: LocalizeFunc,
|
||||
customizeControls: boolean,
|
||||
compatibleControls: AreaControl[]
|
||||
) =>
|
||||
[
|
||||
{
|
||||
name: "customize_controls",
|
||||
selector: {
|
||||
boolean: {},
|
||||
},
|
||||
},
|
||||
...(customizeControls
|
||||
? ([
|
||||
{
|
||||
name: "controls",
|
||||
selector: {
|
||||
select: {
|
||||
reorder: true,
|
||||
multiple: true,
|
||||
options: compatibleControls.map((control) => ({
|
||||
value: control,
|
||||
label: localize(
|
||||
`ui.panel.lovelace.editor.features.types.area-controls.controls_options.${control}`
|
||||
),
|
||||
})),
|
||||
},
|
||||
},
|
||||
},
|
||||
] as const satisfies readonly HaFormSchema[])
|
||||
: []),
|
||||
] as const satisfies readonly HaFormSchema[]
|
||||
);
|
||||
private _schema = [
|
||||
{
|
||||
name: "customize_controls",
|
||||
selector: {
|
||||
boolean: {},
|
||||
},
|
||||
},
|
||||
] as const satisfies readonly HaFormSchema[];
|
||||
|
||||
private _supportedControls = memoizeOne(
|
||||
(
|
||||
@@ -88,7 +90,7 @@ export class HuiAreaControlsCardFeatureEditor
|
||||
return [];
|
||||
}
|
||||
const controlEntities = getAreaControlEntities(
|
||||
AREA_CONTROLS as unknown as AreaControl[],
|
||||
AREA_CONTROL_DOMAINS as unknown as AreaControlDomain[],
|
||||
areaId,
|
||||
excludeEntities,
|
||||
this.hass!
|
||||
@@ -99,6 +101,191 @@ export class HuiAreaControlsCardFeatureEditor
|
||||
}
|
||||
);
|
||||
|
||||
private _domainSearchKeys: FuseWeightedKey[] = [
|
||||
{
|
||||
name: "primary",
|
||||
weight: 10,
|
||||
},
|
||||
];
|
||||
|
||||
private _entitySearchKeys: FuseWeightedKey[] = [
|
||||
{
|
||||
name: "primary",
|
||||
weight: 10,
|
||||
},
|
||||
{
|
||||
name: "secondary",
|
||||
weight: 5,
|
||||
},
|
||||
{
|
||||
name: "id",
|
||||
weight: 3,
|
||||
},
|
||||
];
|
||||
|
||||
private _createFuseIndex = (
|
||||
items: AreaControlPickerItem[],
|
||||
keys: FuseWeightedKey[]
|
||||
) => Fuse.createIndex(keys, items);
|
||||
|
||||
private _domainFuseIndex = memoizeOne((items: AreaControlPickerItem[]) =>
|
||||
this._createFuseIndex(items, this._domainSearchKeys)
|
||||
);
|
||||
|
||||
private _entityFuseIndex = memoizeOne((items: AreaControlPickerItem[]) =>
|
||||
this._createFuseIndex(items, this._entitySearchKeys)
|
||||
);
|
||||
|
||||
private _getItems = memoizeOne(
|
||||
(
|
||||
areaId: string,
|
||||
excludeEntities: string[] | undefined,
|
||||
currentValue: AreaControl[],
|
||||
localize: LocalizeFunc,
|
||||
_entities: HomeAssistant["entities"],
|
||||
_devices: HomeAssistant["devices"],
|
||||
_areas: HomeAssistant["areas"]
|
||||
): ((
|
||||
searchString?: string,
|
||||
section?: string
|
||||
) => (AreaControlPickerItem | string)[]) =>
|
||||
(searchString?: string, section?: string) => {
|
||||
if (!this.hass) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const isSelected = (id: string): boolean =>
|
||||
currentValue.some((item) =>
|
||||
typeof item === "string" ? item === id : item.entity_id === id
|
||||
);
|
||||
|
||||
const controlEntities = getAreaControlEntities(
|
||||
AREA_CONTROL_DOMAINS as unknown as AreaControlDomain[],
|
||||
areaId,
|
||||
excludeEntities,
|
||||
this.hass
|
||||
);
|
||||
|
||||
const items: (AreaControlPickerItem | string)[] = [];
|
||||
let domainItems: AreaControlPickerItem[] = [];
|
||||
let entityItems: AreaControlPickerItem[] = [];
|
||||
|
||||
if (!section || section === "domain") {
|
||||
const supportedControls = (
|
||||
Object.keys(controlEntities) as (keyof typeof controlEntities)[]
|
||||
).filter((control) => controlEntities[control].length > 0);
|
||||
|
||||
supportedControls.forEach((control) => {
|
||||
if (isSelected(control)) {
|
||||
return;
|
||||
}
|
||||
const label = localize(
|
||||
`ui.panel.lovelace.editor.features.types.area-controls.controls_options.${control}`
|
||||
);
|
||||
const button = AREA_CONTROLS_BUTTONS[control];
|
||||
const deviceClass = button.filter.device_class
|
||||
? Array.isArray(button.filter.device_class)
|
||||
? button.filter.device_class[0]
|
||||
: button.filter.device_class
|
||||
: undefined;
|
||||
|
||||
domainItems.push({
|
||||
type: "domain",
|
||||
id: control,
|
||||
primary: label,
|
||||
domain: button.filter.domain,
|
||||
deviceClass,
|
||||
});
|
||||
});
|
||||
|
||||
if (searchString) {
|
||||
const fuseIndex = this._domainFuseIndex(domainItems);
|
||||
domainItems = multiTermSortedSearch(
|
||||
domainItems,
|
||||
searchString,
|
||||
this._domainSearchKeys,
|
||||
(item) => item.id,
|
||||
fuseIndex
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (!section || section === "entity") {
|
||||
const allEntityIds = Object.values(controlEntities).flat();
|
||||
const uniqueEntityIds = Array.from(new Set(allEntityIds));
|
||||
|
||||
const isRTL = computeRTL(this.hass);
|
||||
|
||||
uniqueEntityIds.forEach((entityId) => {
|
||||
if (isSelected(entityId)) {
|
||||
return;
|
||||
}
|
||||
const stateObj = this.hass!.states[entityId];
|
||||
if (!stateObj) {
|
||||
return;
|
||||
}
|
||||
|
||||
const [entityName, deviceName, areaName] = computeEntityNameList(
|
||||
stateObj,
|
||||
[{ type: "entity" }, { type: "device" }, { type: "area" }],
|
||||
this.hass!.entities,
|
||||
this.hass!.devices,
|
||||
this.hass!.areas,
|
||||
this.hass!.floors
|
||||
);
|
||||
|
||||
const primary = entityName || deviceName || entityId;
|
||||
const secondary = [areaName, entityName ? deviceName : undefined]
|
||||
.filter(Boolean)
|
||||
.join(isRTL ? " ◂ " : " ▸ ");
|
||||
|
||||
entityItems.push({
|
||||
type: "entity",
|
||||
id: entityId,
|
||||
primary,
|
||||
secondary,
|
||||
stateObj,
|
||||
});
|
||||
});
|
||||
|
||||
if (searchString) {
|
||||
const fuseIndex = this._entityFuseIndex(entityItems);
|
||||
entityItems = multiTermSortedSearch(
|
||||
entityItems,
|
||||
searchString,
|
||||
this._entitySearchKeys,
|
||||
(item) => item.id,
|
||||
fuseIndex
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Only add section headers if there are items in that section
|
||||
if (!section) {
|
||||
if (domainItems.length > 0) {
|
||||
items.push(
|
||||
localize(
|
||||
"ui.panel.lovelace.editor.features.types.area-controls.sections.domain"
|
||||
)
|
||||
);
|
||||
items.push(...domainItems);
|
||||
}
|
||||
if (entityItems.length > 0) {
|
||||
items.push(
|
||||
localize(
|
||||
"ui.panel.lovelace.editor.features.types.area-controls.sections.entity"
|
||||
)
|
||||
);
|
||||
items.push(...entityItems);
|
||||
}
|
||||
} else {
|
||||
items.push(...domainItems, ...entityItems);
|
||||
}
|
||||
|
||||
return items;
|
||||
}
|
||||
);
|
||||
|
||||
protected render() {
|
||||
if (!this.hass || !this._config || !this.context?.area_id) {
|
||||
return nothing;
|
||||
@@ -127,23 +314,179 @@ export class HuiAreaControlsCardFeatureEditor
|
||||
customize_controls: this._config.controls !== undefined,
|
||||
};
|
||||
|
||||
const schema = this._schema(
|
||||
this.hass.localize,
|
||||
data.customize_controls,
|
||||
supportedControls
|
||||
);
|
||||
const value = this._config.controls || [];
|
||||
|
||||
return html`
|
||||
<ha-form
|
||||
.hass=${this.hass}
|
||||
.data=${data}
|
||||
.schema=${schema}
|
||||
.schema=${this._schema}
|
||||
.computeLabel=${this._computeLabelCallback}
|
||||
@value-changed=${this._valueChanged}
|
||||
></ha-form>
|
||||
${data.customize_controls
|
||||
? html`
|
||||
${value.length
|
||||
? html`
|
||||
<ha-sortable
|
||||
no-style
|
||||
@item-moved=${this._itemMoved}
|
||||
handle-selector="button.primary.action"
|
||||
>
|
||||
<ha-chip-set>
|
||||
${repeat(
|
||||
value,
|
||||
(item) =>
|
||||
typeof item === "string" ? item : item.entity_id,
|
||||
(item, idx) => {
|
||||
const label = this._getItemLabel(item);
|
||||
return html`
|
||||
<ha-input-chip
|
||||
.idx=${idx}
|
||||
@remove=${this._removeItem}
|
||||
.label=${label}
|
||||
selected
|
||||
>
|
||||
<ha-svg-icon
|
||||
slot="icon"
|
||||
.path=${mdiDragHorizontalVariant}
|
||||
></ha-svg-icon>
|
||||
${label}
|
||||
</ha-input-chip>
|
||||
`;
|
||||
}
|
||||
)}
|
||||
</ha-chip-set>
|
||||
</ha-sortable>
|
||||
`
|
||||
: nothing}
|
||||
<ha-generic-picker
|
||||
.hass=${this.hass}
|
||||
.value=${""}
|
||||
.addButtonLabel=${this.hass.localize(
|
||||
"ui.panel.lovelace.editor.features.types.area-controls.controls"
|
||||
)}
|
||||
.getItems=${this._getItems(
|
||||
this.context.area_id,
|
||||
this.context.exclude_entities,
|
||||
value,
|
||||
this.hass.localize,
|
||||
this.hass.entities,
|
||||
this.hass.devices,
|
||||
this.hass.areas
|
||||
)}
|
||||
.rowRenderer=${this._rowRenderer as any}
|
||||
.sections=${[
|
||||
{
|
||||
id: "domain",
|
||||
label: this.hass.localize(
|
||||
"ui.panel.lovelace.editor.features.types.area-controls.sections.domain"
|
||||
),
|
||||
},
|
||||
{
|
||||
id: "entity",
|
||||
label: this.hass.localize(
|
||||
"ui.panel.lovelace.editor.features.types.area-controls.sections.entity"
|
||||
),
|
||||
},
|
||||
]}
|
||||
@value-changed=${this._controlChanged}
|
||||
></ha-generic-picker>
|
||||
`
|
||||
: nothing}
|
||||
`;
|
||||
}
|
||||
|
||||
private _rowRenderer = (item: AreaControlPickerItem) => html`
|
||||
<ha-combo-box-item type="button" compact>
|
||||
${item.type === "entity" && item.stateObj
|
||||
? html`<ha-state-icon
|
||||
slot="start"
|
||||
.hass=${this.hass}
|
||||
.stateObj=${item.stateObj}
|
||||
></ha-state-icon>`
|
||||
: item.domain
|
||||
? html`<ha-domain-icon
|
||||
slot="start"
|
||||
.hass=${this.hass}
|
||||
.domain=${item.domain}
|
||||
.deviceClass=${item.deviceClass}
|
||||
></ha-domain-icon>`
|
||||
: nothing}
|
||||
<span slot="headline">${item.primary}</span>
|
||||
${item.secondary
|
||||
? html`<span slot="supporting-text">${item.secondary}</span>`
|
||||
: nothing}
|
||||
${item.type === "entity" && item.stateObj
|
||||
? html`<span slot="supporting-text" class="code">
|
||||
${item.stateObj.entity_id}
|
||||
</span>`
|
||||
: nothing}
|
||||
</ha-combo-box-item>
|
||||
`;
|
||||
|
||||
private _getItemLabel(item: AreaControl): string {
|
||||
if (!this.hass) {
|
||||
return typeof item === "string" ? item : JSON.stringify(item);
|
||||
}
|
||||
|
||||
if (typeof item === "string") {
|
||||
if (AREA_CONTROL_DOMAINS.includes(item as AreaControlDomain)) {
|
||||
return this.hass.localize(
|
||||
`ui.panel.lovelace.editor.features.types.area-controls.controls_options.${item}`
|
||||
);
|
||||
}
|
||||
// Invalid/unknown domain string
|
||||
return item;
|
||||
}
|
||||
|
||||
if ("entity_id" in item) {
|
||||
const entityState = this.hass.states[item.entity_id];
|
||||
if (entityState) {
|
||||
return computeStateName(entityState);
|
||||
}
|
||||
return item.entity_id;
|
||||
}
|
||||
|
||||
return JSON.stringify(item);
|
||||
}
|
||||
|
||||
private _itemMoved(ev: CustomEvent): void {
|
||||
ev.stopPropagation();
|
||||
const { oldIndex, newIndex } = ev.detail;
|
||||
const controls = [...(this._config!.controls || [])];
|
||||
const item = controls.splice(oldIndex, 1)[0];
|
||||
controls.splice(newIndex, 0, item);
|
||||
this._updateControls(controls);
|
||||
}
|
||||
|
||||
private _removeItem(ev: CustomEvent): void {
|
||||
const index = (ev.currentTarget as any).idx;
|
||||
const controls = [...(this._config!.controls || [])];
|
||||
controls.splice(index, 1);
|
||||
this._updateControls(controls);
|
||||
}
|
||||
|
||||
private _controlChanged(ev: CustomEvent): void {
|
||||
ev.stopPropagation();
|
||||
const value = ev.detail.value;
|
||||
if (!value) {
|
||||
return;
|
||||
}
|
||||
// If it's a domain control (in AREA_CONTROL_DOMAINS), save as string for backwards compatibility
|
||||
// If it's an entity, save in explicit format
|
||||
const control = AREA_CONTROL_DOMAINS.includes(value as AreaControlDomain)
|
||||
? value
|
||||
: { entity_id: value };
|
||||
const controls = [...(this._config!.controls || []), control];
|
||||
this._updateControls(controls);
|
||||
}
|
||||
|
||||
private _updateControls(controls: AreaControl[]): void {
|
||||
const config = { ...this._config!, controls };
|
||||
fireEvent(this, "config-changed", { config });
|
||||
}
|
||||
|
||||
private _valueChanged(ev: CustomEvent): void {
|
||||
const { customize_controls, ...config } = ev.detail
|
||||
.value as AreaControlsCardFeatureData;
|
||||
@@ -166,10 +509,9 @@ export class HuiAreaControlsCardFeatureEditor
|
||||
}
|
||||
|
||||
private _computeLabelCallback = (
|
||||
schema: SchemaUnion<ReturnType<typeof this._schema>>
|
||||
schema: SchemaUnion<typeof this._schema>
|
||||
) => {
|
||||
switch (schema.name) {
|
||||
case "controls":
|
||||
case "customize_controls":
|
||||
return this.hass!.localize(
|
||||
`ui.panel.lovelace.editor.features.types.area-controls.${schema.name}`
|
||||
@@ -178,6 +520,19 @@ export class HuiAreaControlsCardFeatureEditor
|
||||
return "";
|
||||
}
|
||||
};
|
||||
|
||||
static styles = css`
|
||||
ha-sortable {
|
||||
display: block;
|
||||
margin-bottom: var(--ha-space-2);
|
||||
}
|
||||
ha-chip-set {
|
||||
margin-bottom: var(--ha-space-2);
|
||||
}
|
||||
.code {
|
||||
font-family: var(--ha-font-family-code);
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
declare global {
|
||||
|
||||
@@ -5,7 +5,10 @@ import type { LovelaceSectionConfig } from "../../../../data/lovelace/config/sec
|
||||
import type { LovelaceViewConfig } from "../../../../data/lovelace/config/view";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
import { getAreaControlEntities } from "../../card-features/hui-area-controls-card-feature";
|
||||
import { AREA_CONTROLS, type AreaControl } from "../../card-features/types";
|
||||
import {
|
||||
AREA_CONTROL_DOMAINS,
|
||||
type AreaControlDomain,
|
||||
} from "../../card-features/types";
|
||||
import type { AreaCardConfig, HeadingCardConfig } from "../../cards/types";
|
||||
import type { EntitiesDisplay } from "./area-view-strategy";
|
||||
import {
|
||||
@@ -76,7 +79,7 @@ export class AreasOverviewViewStrategy extends ReactiveElement {
|
||||
.map((display) => display.hidden || [])
|
||||
.flat();
|
||||
|
||||
const controls: AreaControl[] = AREA_CONTROLS.filter(
|
||||
const controls: AreaControlDomain[] = AREA_CONTROL_DOMAINS.filter(
|
||||
(a) => a !== "switch" // Exclude switches control for areas as we don't know what the switches control
|
||||
);
|
||||
const controlEntities = getAreaControlEntities(
|
||||
|
||||
@@ -8976,6 +8976,10 @@
|
||||
"label": "Area controls",
|
||||
"customize_controls": "Customize controls",
|
||||
"controls": "Controls",
|
||||
"sections": {
|
||||
"domain": "Domains",
|
||||
"entity": "Entities"
|
||||
},
|
||||
"controls_options": {
|
||||
"light": "Lights",
|
||||
"fan": "Fans",
|
||||
|
||||
Reference in New Issue
Block a user