mirror of
https://github.com/home-assistant/frontend.git
synced 2025-07-15 13:26:34 +00:00
Redesign area card (#25802)
* Use entity filter to get device classes in editor * Add name and sensor states to area card * Fix area type * Add basic controls * Fix editor * Add image * Add image type * Add translation key for area controls * Improve editor * Fix unknown entity id in area * Fix default feature position * Add alert badge * Add helper * Display all alerts when using big card * Disable covers and re-enable switches * Filter compatible controls * Use state icon for alerts * Rename to display type * Delete deprecated show camera * Fix aspect ratio * Improve helper * Undo domain icon changes * Undo domain icon changes * Update types * Fix translation cases * Fix card size * Feedback * Don't fallback to compact * Use plural form * Refactor active color
This commit is contained in:
parent
d58186fec9
commit
f87e20cae9
61
src/components/ha-aspect-ratio.ts
Normal file
61
src/components/ha-aspect-ratio.ts
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
import { css, html, LitElement, type PropertyValues } from "lit";
|
||||||
|
import { customElement, property } from "lit/decorators";
|
||||||
|
import { styleMap } from "lit/directives/style-map";
|
||||||
|
import parseAspectRatio from "../common/util/parse-aspect-ratio";
|
||||||
|
|
||||||
|
const DEFAULT_ASPECT_RATIO = "16:9";
|
||||||
|
|
||||||
|
@customElement("ha-aspect-ratio")
|
||||||
|
export class HaAspectRatio extends LitElement {
|
||||||
|
@property({ type: String, attribute: "aspect-ratio" })
|
||||||
|
public aspectRatio?: string;
|
||||||
|
|
||||||
|
private _ratio: {
|
||||||
|
w: number;
|
||||||
|
h: number;
|
||||||
|
} | null = null;
|
||||||
|
|
||||||
|
public willUpdate(changedProps: PropertyValues) {
|
||||||
|
if (changedProps.has("aspect_ratio") || this._ratio === null) {
|
||||||
|
this._ratio = this.aspectRatio
|
||||||
|
? parseAspectRatio(this.aspectRatio)
|
||||||
|
: null;
|
||||||
|
|
||||||
|
if (this._ratio === null || this._ratio.w <= 0 || this._ratio.h <= 0) {
|
||||||
|
this._ratio = parseAspectRatio(DEFAULT_ASPECT_RATIO);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected render(): unknown {
|
||||||
|
if (!this.aspectRatio) {
|
||||||
|
return html`<slot></slot>`;
|
||||||
|
}
|
||||||
|
return html`
|
||||||
|
<div
|
||||||
|
class="ratio"
|
||||||
|
style=${styleMap({
|
||||||
|
paddingBottom: `${((100 * this._ratio!.h) / this._ratio!.w).toFixed(2)}%`,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<slot></slot>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
static styles = css`
|
||||||
|
.ratio ::slotted(*) {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
"ha-aspect-ratio": HaAspectRatio;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,258 @@
|
|||||||
|
import { mdiFan, mdiLightbulb, mdiToggleSwitch } from "@mdi/js";
|
||||||
|
import { callService, type HassEntity } from "home-assistant-js-websocket";
|
||||||
|
import { LitElement, css, html, nothing } from "lit";
|
||||||
|
import { customElement, property, state } from "lit/decorators";
|
||||||
|
import { styleMap } from "lit/directives/style-map";
|
||||||
|
import memoizeOne from "memoize-one";
|
||||||
|
import {
|
||||||
|
generateEntityFilter,
|
||||||
|
type EntityFilter,
|
||||||
|
} from "../../../common/entity/entity_filter";
|
||||||
|
import { stateActive } from "../../../common/entity/state_active";
|
||||||
|
import "../../../components/ha-control-button";
|
||||||
|
import "../../../components/ha-control-button-group";
|
||||||
|
import "../../../components/ha-svg-icon";
|
||||||
|
import type { AreaRegistryEntry } from "../../../data/area_registry";
|
||||||
|
import type { HomeAssistant } from "../../../types";
|
||||||
|
import type { LovelaceCardFeature, LovelaceCardFeatureEditor } from "../types";
|
||||||
|
import { cardFeatureStyles } from "./common/card-feature-styles";
|
||||||
|
import type {
|
||||||
|
AreaControl,
|
||||||
|
AreaControlsCardFeatureConfig,
|
||||||
|
LovelaceCardFeatureContext,
|
||||||
|
} from "./types";
|
||||||
|
import { AREA_CONTROLS } from "./types";
|
||||||
|
|
||||||
|
interface AreaControlsButton {
|
||||||
|
iconPath: string;
|
||||||
|
activeColor: string;
|
||||||
|
onService: string;
|
||||||
|
offService: string;
|
||||||
|
filter: EntityFilter;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const AREA_CONTROLS_BUTTONS: Record<AreaControl, AreaControlsButton> = {
|
||||||
|
light: {
|
||||||
|
iconPath: mdiLightbulb,
|
||||||
|
filter: {
|
||||||
|
domain: "light",
|
||||||
|
},
|
||||||
|
activeColor: "var(--state-light-active-color)",
|
||||||
|
onService: "light.turn_on",
|
||||||
|
offService: "light.turn_off",
|
||||||
|
},
|
||||||
|
fan: {
|
||||||
|
iconPath: mdiFan,
|
||||||
|
filter: {
|
||||||
|
domain: "fan",
|
||||||
|
},
|
||||||
|
activeColor: "var(--state-fan-active-color)",
|
||||||
|
onService: "fan.turn_on",
|
||||||
|
offService: "fan.turn_off",
|
||||||
|
},
|
||||||
|
switch: {
|
||||||
|
iconPath: mdiToggleSwitch,
|
||||||
|
filter: {
|
||||||
|
domain: "switch",
|
||||||
|
},
|
||||||
|
activeColor: "var(--state-switch-active-color)",
|
||||||
|
onService: "switch.turn_on",
|
||||||
|
offService: "switch.turn_off",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const supportsAreaControlsCardFeature = (
|
||||||
|
hass: HomeAssistant,
|
||||||
|
context: LovelaceCardFeatureContext
|
||||||
|
) => {
|
||||||
|
const area = context.area_id ? hass.areas[context.area_id] : undefined;
|
||||||
|
return !!area;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getAreaControlEntities = (
|
||||||
|
controls: AreaControl[],
|
||||||
|
areaId: string,
|
||||||
|
hass: HomeAssistant
|
||||||
|
): Record<AreaControl, string[]> =>
|
||||||
|
controls.reduce(
|
||||||
|
(acc, control) => {
|
||||||
|
const controlButton = AREA_CONTROLS_BUTTONS[control];
|
||||||
|
const filter = generateEntityFilter(hass, {
|
||||||
|
area: areaId,
|
||||||
|
...controlButton.filter,
|
||||||
|
});
|
||||||
|
|
||||||
|
acc[control] = Object.keys(hass.entities).filter((entityId) =>
|
||||||
|
filter(entityId)
|
||||||
|
);
|
||||||
|
return acc;
|
||||||
|
},
|
||||||
|
{} as Record<AreaControl, string[]>
|
||||||
|
);
|
||||||
|
|
||||||
|
@customElement("hui-area-controls-card-feature")
|
||||||
|
class HuiAreaControlsCardFeature
|
||||||
|
extends LitElement
|
||||||
|
implements LovelaceCardFeature
|
||||||
|
{
|
||||||
|
@property({ attribute: false }) public hass?: HomeAssistant;
|
||||||
|
|
||||||
|
@property({ attribute: false }) public context?: LovelaceCardFeatureContext;
|
||||||
|
|
||||||
|
@state() private _config?: AreaControlsCardFeatureConfig;
|
||||||
|
|
||||||
|
private get _area() {
|
||||||
|
if (!this.hass || !this.context || !this.context.area_id) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
return this.hass.areas[this.context.area_id!] as
|
||||||
|
| AreaRegistryEntry
|
||||||
|
| undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
private get _controls() {
|
||||||
|
return (
|
||||||
|
this._config?.controls || (AREA_CONTROLS as unknown as AreaControl[])
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
static getStubConfig(): AreaControlsCardFeatureConfig {
|
||||||
|
return {
|
||||||
|
type: "area-controls",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public static async getConfigElement(): Promise<LovelaceCardFeatureEditor> {
|
||||||
|
await import(
|
||||||
|
"../editor/config-elements/hui-area-controls-card-feature-editor"
|
||||||
|
);
|
||||||
|
return document.createElement("hui-area-controls-card-feature-editor");
|
||||||
|
}
|
||||||
|
|
||||||
|
public setConfig(config: AreaControlsCardFeatureConfig): void {
|
||||||
|
if (!config) {
|
||||||
|
throw new Error("Invalid configuration");
|
||||||
|
}
|
||||||
|
this._config = config;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _handleButtonTap(ev: MouseEvent) {
|
||||||
|
ev.stopPropagation();
|
||||||
|
|
||||||
|
if (!this.context?.area_id || !this.hass) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const control = (ev.currentTarget as any).control as AreaControl;
|
||||||
|
|
||||||
|
const controlEntities = this._controlEntities(
|
||||||
|
this._controls,
|
||||||
|
this.context.area_id,
|
||||||
|
this.hass!.entities,
|
||||||
|
this.hass!.devices,
|
||||||
|
this.hass!.areas
|
||||||
|
);
|
||||||
|
const entitiesIds = controlEntities[control];
|
||||||
|
|
||||||
|
const { onService, offService } = AREA_CONTROLS_BUTTONS[control];
|
||||||
|
|
||||||
|
const isOn = entitiesIds.some((entityId) =>
|
||||||
|
stateActive(this.hass!.states[entityId] as HassEntity)
|
||||||
|
);
|
||||||
|
|
||||||
|
const [domain, service] = (isOn ? offService : onService).split(".");
|
||||||
|
|
||||||
|
callService(this.hass!.connection, domain, service, {
|
||||||
|
entity_id: entitiesIds,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private _controlEntities = memoizeOne(
|
||||||
|
(
|
||||||
|
controls: AreaControl[],
|
||||||
|
areaId: string,
|
||||||
|
// needed to update memoized function when entities, devices or areas change
|
||||||
|
_entities: HomeAssistant["entities"],
|
||||||
|
_devices: HomeAssistant["devices"],
|
||||||
|
_areas: HomeAssistant["areas"]
|
||||||
|
) => getAreaControlEntities(controls, areaId, this.hass!)
|
||||||
|
);
|
||||||
|
|
||||||
|
protected render() {
|
||||||
|
if (
|
||||||
|
!this._config ||
|
||||||
|
!this.hass ||
|
||||||
|
!this.context ||
|
||||||
|
!this._area ||
|
||||||
|
!supportsAreaControlsCardFeature(this.hass, this.context)
|
||||||
|
) {
|
||||||
|
return nothing;
|
||||||
|
}
|
||||||
|
|
||||||
|
const controlEntities = this._controlEntities(
|
||||||
|
this._controls,
|
||||||
|
this.context.area_id!,
|
||||||
|
this.hass!.entities,
|
||||||
|
this.hass!.devices,
|
||||||
|
this.hass!.areas
|
||||||
|
);
|
||||||
|
|
||||||
|
const supportedControls = this._controls.filter(
|
||||||
|
(control) => controlEntities[control].length > 0
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!supportedControls.length) {
|
||||||
|
return nothing;
|
||||||
|
}
|
||||||
|
|
||||||
|
return html`
|
||||||
|
<ha-control-button-group>
|
||||||
|
${supportedControls.map((control) => {
|
||||||
|
const button = AREA_CONTROLS_BUTTONS[control];
|
||||||
|
|
||||||
|
const entities = controlEntities[control];
|
||||||
|
const active = entities.some((entityId) => {
|
||||||
|
const stateObj = this.hass!.states[entityId] as
|
||||||
|
| HassEntity
|
||||||
|
| undefined;
|
||||||
|
if (!stateObj) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return stateActive(stateObj);
|
||||||
|
});
|
||||||
|
|
||||||
|
return html`
|
||||||
|
<ha-control-button
|
||||||
|
class=${active ? "active" : ""}
|
||||||
|
style=${styleMap({ "--active-color": button.activeColor })}
|
||||||
|
.control=${control}
|
||||||
|
@click=${this._handleButtonTap}
|
||||||
|
>
|
||||||
|
<ha-svg-icon .path=${button.iconPath}></ha-svg-icon>
|
||||||
|
</ha-control-button>
|
||||||
|
`;
|
||||||
|
})}
|
||||||
|
</ha-control-button-group>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
static get styles() {
|
||||||
|
return [
|
||||||
|
cardFeatureStyles,
|
||||||
|
css`
|
||||||
|
ha-control-button {
|
||||||
|
--active-color: var(--primary-color);
|
||||||
|
}
|
||||||
|
ha-control-button.active {
|
||||||
|
--control-button-background-color: var(--active-color);
|
||||||
|
--control-button-icon-color: var(--active-color);
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
"hui-area-controls-card-feature": HuiAreaControlsCardFeature;
|
||||||
|
}
|
||||||
|
}
|
@ -158,6 +158,15 @@ export interface UpdateActionsCardFeatureConfig {
|
|||||||
backup?: "yes" | "no" | "ask";
|
backup?: "yes" | "no" | "ask";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const AREA_CONTROLS = ["light", "fan", "switch"] as const;
|
||||||
|
|
||||||
|
export type AreaControl = (typeof AREA_CONTROLS)[number];
|
||||||
|
|
||||||
|
export interface AreaControlsCardFeatureConfig {
|
||||||
|
type: "area-controls";
|
||||||
|
controls?: AreaControl[];
|
||||||
|
}
|
||||||
|
|
||||||
export type LovelaceCardFeatureConfig =
|
export type LovelaceCardFeatureConfig =
|
||||||
| AlarmModesCardFeatureConfig
|
| AlarmModesCardFeatureConfig
|
||||||
| ClimateFanModesCardFeatureConfig
|
| ClimateFanModesCardFeatureConfig
|
||||||
@ -187,8 +196,10 @@ export type LovelaceCardFeatureConfig =
|
|||||||
| ToggleCardFeatureConfig
|
| ToggleCardFeatureConfig
|
||||||
| UpdateActionsCardFeatureConfig
|
| UpdateActionsCardFeatureConfig
|
||||||
| VacuumCommandsCardFeatureConfig
|
| VacuumCommandsCardFeatureConfig
|
||||||
| WaterHeaterOperationModesCardFeatureConfig;
|
| WaterHeaterOperationModesCardFeatureConfig
|
||||||
|
| AreaControlsCardFeatureConfig;
|
||||||
|
|
||||||
export interface LovelaceCardFeatureContext {
|
export interface LovelaceCardFeatureContext {
|
||||||
entity_id?: string;
|
entity_id?: string;
|
||||||
|
area_id?: string;
|
||||||
}
|
}
|
||||||
|
File diff suppressed because it is too large
Load Diff
@ -101,11 +101,18 @@ export interface EntitiesCardConfig extends LovelaceCardConfig {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface AreaCardConfig extends LovelaceCardConfig {
|
export interface AreaCardConfig extends LovelaceCardConfig {
|
||||||
area: string;
|
area?: string;
|
||||||
|
name?: string;
|
||||||
navigation_path?: string;
|
navigation_path?: string;
|
||||||
|
display_type?: "compact" | "icon" | "picture" | "camera";
|
||||||
|
/** @deprecated Use `display_type` instead */
|
||||||
show_camera?: boolean;
|
show_camera?: boolean;
|
||||||
camera_view?: HuiImage["cameraView"];
|
camera_view?: HuiImage["cameraView"];
|
||||||
aspect_ratio?: string;
|
aspect_ratio?: string;
|
||||||
|
sensor_classes?: string[];
|
||||||
|
alert_classes?: string[];
|
||||||
|
features?: LovelaceCardFeatureConfig[];
|
||||||
|
features_position?: "bottom" | "inline";
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ButtonCardConfig extends LovelaceCardConfig {
|
export interface ButtonCardConfig extends LovelaceCardConfig {
|
||||||
|
@ -54,7 +54,10 @@ export class HuiImage extends LitElement {
|
|||||||
|
|
||||||
@property({ attribute: false }) public darkModeFilter?: string;
|
@property({ attribute: false }) public darkModeFilter?: string;
|
||||||
|
|
||||||
@property({ attribute: false }) public fitMode?: "cover" | "contain" | "fill";
|
@property({ attribute: "fit-mode", type: String }) public fitMode?:
|
||||||
|
| "cover"
|
||||||
|
| "contain"
|
||||||
|
| "fill";
|
||||||
|
|
||||||
@state() private _imageVisible? = false;
|
@state() private _imageVisible? = false;
|
||||||
|
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
import "../card-features/hui-alarm-modes-card-feature";
|
import "../card-features/hui-alarm-modes-card-feature";
|
||||||
import "../card-features/hui-climate-fan-modes-card-feature";
|
import "../card-features/hui-climate-fan-modes-card-feature";
|
||||||
import "../card-features/hui-climate-swing-modes-card-feature";
|
|
||||||
import "../card-features/hui-climate-swing-horizontal-modes-card-feature";
|
|
||||||
import "../card-features/hui-climate-hvac-modes-card-feature";
|
import "../card-features/hui-climate-hvac-modes-card-feature";
|
||||||
import "../card-features/hui-climate-preset-modes-card-feature";
|
import "../card-features/hui-climate-preset-modes-card-feature";
|
||||||
|
import "../card-features/hui-climate-swing-horizontal-modes-card-feature";
|
||||||
|
import "../card-features/hui-climate-swing-modes-card-feature";
|
||||||
import "../card-features/hui-counter-actions-card-feature";
|
import "../card-features/hui-counter-actions-card-feature";
|
||||||
import "../card-features/hui-cover-open-close-card-feature";
|
import "../card-features/hui-cover-open-close-card-feature";
|
||||||
import "../card-features/hui-cover-position-card-feature";
|
import "../card-features/hui-cover-position-card-feature";
|
||||||
@ -21,12 +21,13 @@ import "../card-features/hui-lock-open-door-card-feature";
|
|||||||
import "../card-features/hui-media-player-volume-slider-card-feature";
|
import "../card-features/hui-media-player-volume-slider-card-feature";
|
||||||
import "../card-features/hui-numeric-input-card-feature";
|
import "../card-features/hui-numeric-input-card-feature";
|
||||||
import "../card-features/hui-select-options-card-feature";
|
import "../card-features/hui-select-options-card-feature";
|
||||||
import "../card-features/hui-target-temperature-card-feature";
|
|
||||||
import "../card-features/hui-target-humidity-card-feature";
|
import "../card-features/hui-target-humidity-card-feature";
|
||||||
|
import "../card-features/hui-target-temperature-card-feature";
|
||||||
import "../card-features/hui-toggle-card-feature";
|
import "../card-features/hui-toggle-card-feature";
|
||||||
import "../card-features/hui-update-actions-card-feature";
|
import "../card-features/hui-update-actions-card-feature";
|
||||||
import "../card-features/hui-vacuum-commands-card-feature";
|
import "../card-features/hui-vacuum-commands-card-feature";
|
||||||
import "../card-features/hui-water-heater-operation-modes-card-feature";
|
import "../card-features/hui-water-heater-operation-modes-card-feature";
|
||||||
|
import "../card-features/hui-area-controls-card-feature";
|
||||||
|
|
||||||
import type { LovelaceCardFeatureConfig } from "../card-features/types";
|
import type { LovelaceCardFeatureConfig } from "../card-features/types";
|
||||||
import {
|
import {
|
||||||
@ -36,6 +37,7 @@ import {
|
|||||||
|
|
||||||
const TYPES = new Set<LovelaceCardFeatureConfig["type"]>([
|
const TYPES = new Set<LovelaceCardFeatureConfig["type"]>([
|
||||||
"alarm-modes",
|
"alarm-modes",
|
||||||
|
"area-controls",
|
||||||
"climate-fan-modes",
|
"climate-fan-modes",
|
||||||
"climate-swing-modes",
|
"climate-swing-modes",
|
||||||
"climate-swing-horizontal-modes",
|
"climate-swing-horizontal-modes",
|
||||||
|
@ -1,43 +1,59 @@
|
|||||||
import { html, LitElement, nothing } from "lit";
|
import { mdiGestureTap, mdiListBox, mdiTextShort } from "@mdi/js";
|
||||||
|
import { css, html, LitElement, nothing } from "lit";
|
||||||
import { customElement, property, state } from "lit/decorators";
|
import { customElement, property, state } from "lit/decorators";
|
||||||
import memoizeOne from "memoize-one";
|
import memoizeOne from "memoize-one";
|
||||||
import {
|
import {
|
||||||
assert,
|
any,
|
||||||
array,
|
array,
|
||||||
|
assert,
|
||||||
assign,
|
assign,
|
||||||
boolean,
|
boolean,
|
||||||
|
enums,
|
||||||
object,
|
object,
|
||||||
optional,
|
optional,
|
||||||
string,
|
string,
|
||||||
} from "superstruct";
|
} from "superstruct";
|
||||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
|
||||||
import "../../../../components/ha-form/ha-form";
|
|
||||||
import {
|
import {
|
||||||
DEFAULT_ASPECT_RATIO,
|
fireEvent,
|
||||||
DEVICE_CLASSES,
|
type HASSDomEvent,
|
||||||
} from "../../cards/hui-area-card";
|
} from "../../../../common/dom/fire_event";
|
||||||
import type { SchemaUnion } from "../../../../components/ha-form/types";
|
import { generateEntityFilter } from "../../../../common/entity/entity_filter";
|
||||||
|
import { caseInsensitiveStringCompare } from "../../../../common/string/compare";
|
||||||
|
import type { LocalizeFunc } from "../../../../common/translations/localize";
|
||||||
|
import "../../../../components/ha-form/ha-form";
|
||||||
|
import type {
|
||||||
|
HaFormSchema,
|
||||||
|
SchemaUnion,
|
||||||
|
} from "../../../../components/ha-form/types";
|
||||||
|
import type { SelectOption } from "../../../../data/selector";
|
||||||
|
import { getSensorNumericDeviceClasses } from "../../../../data/sensor";
|
||||||
import type { HomeAssistant } from "../../../../types";
|
import type { HomeAssistant } from "../../../../types";
|
||||||
|
import type {
|
||||||
|
LovelaceCardFeatureConfig,
|
||||||
|
LovelaceCardFeatureContext,
|
||||||
|
} from "../../card-features/types";
|
||||||
|
import { DEVICE_CLASSES } from "../../cards/hui-area-card";
|
||||||
import type { AreaCardConfig } from "../../cards/types";
|
import type { AreaCardConfig } from "../../cards/types";
|
||||||
import type { LovelaceCardEditor } from "../../types";
|
import type { LovelaceCardEditor } from "../../types";
|
||||||
import { baseLovelaceCardConfig } from "../structs/base-card-struct";
|
import { baseLovelaceCardConfig } from "../structs/base-card-struct";
|
||||||
import { computeDomain } from "../../../../common/entity/compute_domain";
|
import type { EditDetailElementEvent, EditSubElementEvent } from "../types";
|
||||||
import { caseInsensitiveStringCompare } from "../../../../common/string/compare";
|
import { configElementStyle } from "./config-elements-style";
|
||||||
import type { SelectOption } from "../../../../data/selector";
|
import { getSupportedFeaturesType } from "./hui-card-features-editor";
|
||||||
import { getSensorNumericDeviceClasses } from "../../../../data/sensor";
|
|
||||||
import type { LocalizeFunc } from "../../../../common/translations/localize";
|
|
||||||
|
|
||||||
const cardConfigStruct = assign(
|
const cardConfigStruct = assign(
|
||||||
baseLovelaceCardConfig,
|
baseLovelaceCardConfig,
|
||||||
object({
|
object({
|
||||||
area: optional(string()),
|
area: optional(string()),
|
||||||
|
name: optional(string()),
|
||||||
navigation_path: optional(string()),
|
navigation_path: optional(string()),
|
||||||
theme: optional(string()),
|
|
||||||
show_camera: optional(boolean()),
|
show_camera: optional(boolean()),
|
||||||
|
display_type: optional(enums(["compact", "icon", "picture", "camera"])),
|
||||||
camera_view: optional(string()),
|
camera_view: optional(string()),
|
||||||
aspect_ratio: optional(string()),
|
|
||||||
alert_classes: optional(array(string())),
|
alert_classes: optional(array(string())),
|
||||||
sensor_classes: optional(array(string())),
|
sensor_classes: optional(array(string())),
|
||||||
|
features: optional(array(any())),
|
||||||
|
features_position: optional(enums(["bottom", "inline"])),
|
||||||
|
aspect_ratio: optional(string()),
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -52,6 +68,12 @@ export class HuiAreaCardEditor
|
|||||||
|
|
||||||
@state() private _numericDeviceClasses?: string[];
|
@state() private _numericDeviceClasses?: string[];
|
||||||
|
|
||||||
|
private _featureContext = memoizeOne(
|
||||||
|
(areaId?: string): LovelaceCardFeatureContext => ({
|
||||||
|
area_id: areaId,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
private _schema = memoizeOne(
|
private _schema = memoizeOne(
|
||||||
(
|
(
|
||||||
localize: LocalizeFunc,
|
localize: LocalizeFunc,
|
||||||
@ -61,7 +83,40 @@ export class HuiAreaCardEditor
|
|||||||
) =>
|
) =>
|
||||||
[
|
[
|
||||||
{ name: "area", selector: { area: {} } },
|
{ name: "area", selector: { area: {} } },
|
||||||
{ name: "show_camera", required: false, selector: { boolean: {} } },
|
{
|
||||||
|
name: "content",
|
||||||
|
flatten: true,
|
||||||
|
type: "expandable",
|
||||||
|
iconPath: mdiTextShort,
|
||||||
|
schema: [
|
||||||
|
{
|
||||||
|
name: "",
|
||||||
|
type: "grid",
|
||||||
|
schema: [
|
||||||
|
{ name: "name", selector: { text: {} } },
|
||||||
|
{
|
||||||
|
name: "display_type",
|
||||||
|
required: true,
|
||||||
|
selector: {
|
||||||
|
select: {
|
||||||
|
options: ["compact", "icon", "picture", "camera"].map(
|
||||||
|
(value) => ({
|
||||||
|
value,
|
||||||
|
label: localize(
|
||||||
|
`ui.panel.lovelace.editor.card.area.display_type_options.${value}`
|
||||||
|
),
|
||||||
|
})
|
||||||
|
),
|
||||||
|
mode: "dropdown",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "",
|
||||||
|
type: "grid",
|
||||||
|
schema: [
|
||||||
...(showCamera
|
...(showCamera
|
||||||
? ([
|
? ([
|
||||||
{
|
{
|
||||||
@ -78,23 +133,8 @@ export class HuiAreaCardEditor
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
] as const)
|
] as const satisfies readonly HaFormSchema[])
|
||||||
: []),
|
: []),
|
||||||
{
|
|
||||||
name: "",
|
|
||||||
type: "grid",
|
|
||||||
schema: [
|
|
||||||
{
|
|
||||||
name: "navigation_path",
|
|
||||||
required: false,
|
|
||||||
selector: { navigation: {} },
|
|
||||||
},
|
|
||||||
{ name: "theme", required: false, selector: { theme: {} } },
|
|
||||||
{
|
|
||||||
name: "aspect_ratio",
|
|
||||||
default: DEFAULT_ASPECT_RATIO,
|
|
||||||
selector: { text: {} },
|
|
||||||
},
|
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -119,44 +159,66 @@ export class HuiAreaCardEditor
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
] as const
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "interactions",
|
||||||
|
type: "expandable",
|
||||||
|
flatten: true,
|
||||||
|
iconPath: mdiGestureTap,
|
||||||
|
schema: [
|
||||||
|
{
|
||||||
|
name: "navigation_path",
|
||||||
|
required: false,
|
||||||
|
selector: { navigation: {} },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
] as const satisfies readonly HaFormSchema[]
|
||||||
);
|
);
|
||||||
|
|
||||||
private _binaryClassesForArea = memoizeOne((area: string): string[] =>
|
private _binaryClassesForArea = memoizeOne(
|
||||||
this._classesForArea(area, "binary_sensor")
|
(area: string | undefined): string[] => {
|
||||||
);
|
if (!area) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
private _sensorClassesForArea = memoizeOne(
|
const binarySensorFilter = generateEntityFilter(this.hass!, {
|
||||||
(area: string, numericDeviceClasses?: string[]): string[] =>
|
domain: "binary_sensor",
|
||||||
this._classesForArea(area, "sensor", numericDeviceClasses)
|
area,
|
||||||
);
|
entity_category: "none",
|
||||||
|
});
|
||||||
|
|
||||||
private _classesForArea(
|
const classes = Object.keys(this.hass!.entities)
|
||||||
area: string,
|
.filter(binarySensorFilter)
|
||||||
domain: "sensor" | "binary_sensor",
|
.map((id) => this.hass!.states[id]?.attributes.device_class)
|
||||||
numericDeviceClasses?: string[] | undefined
|
.filter((c): c is string => Boolean(c));
|
||||||
): string[] {
|
|
||||||
const entities = Object.values(this.hass!.entities).filter(
|
|
||||||
(e) =>
|
|
||||||
computeDomain(e.entity_id) === domain &&
|
|
||||||
!e.entity_category &&
|
|
||||||
!e.hidden &&
|
|
||||||
(e.area_id === area ||
|
|
||||||
(e.device_id && this.hass!.devices[e.device_id]?.area_id === area))
|
|
||||||
);
|
|
||||||
|
|
||||||
const classes = entities
|
|
||||||
.map((e) => this.hass!.states[e.entity_id]?.attributes.device_class || "")
|
|
||||||
.filter(
|
|
||||||
(c) =>
|
|
||||||
c &&
|
|
||||||
(domain !== "sensor" ||
|
|
||||||
!numericDeviceClasses ||
|
|
||||||
numericDeviceClasses.includes(c))
|
|
||||||
);
|
|
||||||
|
|
||||||
return [...new Set(classes)];
|
return [...new Set(classes)];
|
||||||
}
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
private _sensorClassesForArea = memoizeOne(
|
||||||
|
(area: string | undefined, numericDeviceClasses?: string[]): string[] => {
|
||||||
|
if (!area) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const sensorFilter = generateEntityFilter(this.hass!, {
|
||||||
|
domain: "sensor",
|
||||||
|
area,
|
||||||
|
device_class: numericDeviceClasses,
|
||||||
|
entity_category: "none",
|
||||||
|
});
|
||||||
|
|
||||||
|
const classes = Object.keys(this.hass!.entities)
|
||||||
|
.filter(sensorFilter)
|
||||||
|
.map((id) => this.hass!.states[id]?.attributes.device_class)
|
||||||
|
.filter((c): c is string => Boolean(c));
|
||||||
|
|
||||||
|
return [...new Set(classes)];
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
private _buildBinaryOptions = memoizeOne(
|
private _buildBinaryOptions = memoizeOne(
|
||||||
(possibleClasses: string[], currentClasses: string[]): SelectOption[] =>
|
(possibleClasses: string[], currentClasses: string[]): SelectOption[] =>
|
||||||
@ -191,7 +253,14 @@ export class HuiAreaCardEditor
|
|||||||
|
|
||||||
public setConfig(config: AreaCardConfig): void {
|
public setConfig(config: AreaCardConfig): void {
|
||||||
assert(config, cardConfigStruct);
|
assert(config, cardConfigStruct);
|
||||||
this._config = config;
|
|
||||||
|
const displayType =
|
||||||
|
config.display_type || (config.show_camera ? "camera" : "picture");
|
||||||
|
this._config = {
|
||||||
|
...config,
|
||||||
|
display_type: displayType,
|
||||||
|
};
|
||||||
|
delete this._config.show_camera;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async updated() {
|
protected async updated() {
|
||||||
@ -202,16 +271,50 @@ export class HuiAreaCardEditor
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private _featuresSchema = memoizeOne(
|
||||||
|
(localize: LocalizeFunc) =>
|
||||||
|
[
|
||||||
|
{
|
||||||
|
name: "features_position",
|
||||||
|
required: true,
|
||||||
|
selector: {
|
||||||
|
select: {
|
||||||
|
mode: "box",
|
||||||
|
options: ["bottom", "inline"].map((value) => ({
|
||||||
|
label: localize(
|
||||||
|
`ui.panel.lovelace.editor.card.tile.features_position_options.${value}`
|
||||||
|
),
|
||||||
|
description: localize(
|
||||||
|
`ui.panel.lovelace.editor.card.tile.features_position_options.${value}_description`
|
||||||
|
),
|
||||||
|
value,
|
||||||
|
image: {
|
||||||
|
src: `/static/images/form/tile_features_position_${value}.svg`,
|
||||||
|
src_dark: `/static/images/form/tile_features_position_${value}_dark.svg`,
|
||||||
|
flip_rtl: true,
|
||||||
|
},
|
||||||
|
})),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
] as const satisfies readonly HaFormSchema[]
|
||||||
|
);
|
||||||
|
|
||||||
|
private _hasCompatibleFeatures = memoizeOne(
|
||||||
|
(context: LovelaceCardFeatureContext) =>
|
||||||
|
getSupportedFeaturesType(this.hass!, context).length > 0
|
||||||
|
);
|
||||||
|
|
||||||
protected render() {
|
protected render() {
|
||||||
if (!this.hass || !this._config) {
|
if (!this.hass || !this._config) {
|
||||||
return nothing;
|
return nothing;
|
||||||
}
|
}
|
||||||
|
|
||||||
const possibleBinaryClasses = this._binaryClassesForArea(
|
const areaId = this._config!.area;
|
||||||
this._config.area || ""
|
|
||||||
);
|
const possibleBinaryClasses = this._binaryClassesForArea(this._config.area);
|
||||||
const possibleSensorClasses = this._sensorClassesForArea(
|
const possibleSensorClasses = this._sensorClassesForArea(
|
||||||
this._config.area || "",
|
this._config.area,
|
||||||
this._numericDeviceClasses
|
this._numericDeviceClasses
|
||||||
);
|
);
|
||||||
const binarySelectOptions = this._buildBinaryOptions(
|
const binarySelectOptions = this._buildBinaryOptions(
|
||||||
@ -223,68 +326,196 @@ export class HuiAreaCardEditor
|
|||||||
this._config.sensor_classes || DEVICE_CLASSES.sensor
|
this._config.sensor_classes || DEVICE_CLASSES.sensor
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const showCamera = this._config.display_type === "camera";
|
||||||
|
|
||||||
|
const displayType =
|
||||||
|
this._config.display_type || this._config.show_camera
|
||||||
|
? "camera"
|
||||||
|
: "picture";
|
||||||
|
|
||||||
const schema = this._schema(
|
const schema = this._schema(
|
||||||
this.hass.localize,
|
this.hass.localize,
|
||||||
this._config.show_camera || false,
|
showCamera,
|
||||||
binarySelectOptions,
|
binarySelectOptions,
|
||||||
sensorSelectOptions
|
sensorSelectOptions
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const featuresSchema = this._featuresSchema(this.hass.localize);
|
||||||
|
|
||||||
const data = {
|
const data = {
|
||||||
camera_view: "auto",
|
camera_view: "auto",
|
||||||
alert_classes: DEVICE_CLASSES.binary_sensor,
|
alert_classes: DEVICE_CLASSES.binary_sensor,
|
||||||
sensor_classes: DEVICE_CLASSES.sensor,
|
sensor_classes: DEVICE_CLASSES.sensor,
|
||||||
|
features_position: "bottom",
|
||||||
|
display_type: displayType,
|
||||||
...this._config,
|
...this._config,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const featureContext = this._featureContext(areaId);
|
||||||
|
const hasCompatibleFeatures = this._hasCompatibleFeatures(featureContext);
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
<ha-form
|
<ha-form
|
||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
.data=${data}
|
.data=${data}
|
||||||
.schema=${schema}
|
.schema=${schema}
|
||||||
.computeLabel=${this._computeLabelCallback}
|
.computeLabel=${this._computeLabelCallback}
|
||||||
|
.computeHelper=${this._computeHelperCallback}
|
||||||
@value-changed=${this._valueChanged}
|
@value-changed=${this._valueChanged}
|
||||||
></ha-form>
|
></ha-form>
|
||||||
|
<ha-expansion-panel outlined>
|
||||||
|
<ha-svg-icon slot="leading-icon" .path=${mdiListBox}></ha-svg-icon>
|
||||||
|
<h3 slot="header">
|
||||||
|
${this.hass!.localize(
|
||||||
|
"ui.panel.lovelace.editor.card.generic.features"
|
||||||
|
)}
|
||||||
|
</h3>
|
||||||
|
<div class="content">
|
||||||
|
${hasCompatibleFeatures
|
||||||
|
? html`
|
||||||
|
<ha-form
|
||||||
|
class="features-form"
|
||||||
|
.hass=${this.hass}
|
||||||
|
.data=${data}
|
||||||
|
.schema=${featuresSchema}
|
||||||
|
.computeLabel=${this._computeLabelCallback}
|
||||||
|
@value-changed=${this._valueChanged}
|
||||||
|
></ha-form>
|
||||||
|
`
|
||||||
|
: nothing}
|
||||||
|
<hui-card-features-editor
|
||||||
|
.hass=${this.hass}
|
||||||
|
.context=${featureContext}
|
||||||
|
.features=${this._config!.features ?? []}
|
||||||
|
@features-changed=${this._featuresChanged}
|
||||||
|
@edit-detail-element=${this._editDetailElement}
|
||||||
|
></hui-card-features-editor>
|
||||||
|
</div>
|
||||||
|
</ha-expansion-panel>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
private _valueChanged(ev: CustomEvent): void {
|
private _valueChanged(ev: CustomEvent): void {
|
||||||
const config = ev.detail.value;
|
const newConfig = ev.detail.value as AreaCardConfig;
|
||||||
if (!config.show_camera) {
|
|
||||||
|
const config: AreaCardConfig = {
|
||||||
|
features: this._config!.features,
|
||||||
|
...newConfig,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (config.display_type !== "camera") {
|
||||||
delete config.camera_view;
|
delete config.camera_view;
|
||||||
}
|
}
|
||||||
|
|
||||||
fireEvent(this, "config-changed", { config });
|
fireEvent(this, "config-changed", { config });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private _featuresChanged(ev: CustomEvent) {
|
||||||
|
ev.stopPropagation();
|
||||||
|
if (!this._config || !this.hass) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const features = ev.detail.features as LovelaceCardFeatureConfig[];
|
||||||
|
const config: AreaCardConfig = {
|
||||||
|
...this._config,
|
||||||
|
features,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (features.length === 0) {
|
||||||
|
delete config.features;
|
||||||
|
}
|
||||||
|
|
||||||
|
fireEvent(this, "config-changed", { config });
|
||||||
|
}
|
||||||
|
|
||||||
|
private _editDetailElement(ev: HASSDomEvent<EditDetailElementEvent>): void {
|
||||||
|
const index = ev.detail.subElementConfig.index;
|
||||||
|
const config = this._config!.features![index!];
|
||||||
|
const featureContext = this._featureContext(this._config!.area);
|
||||||
|
|
||||||
|
fireEvent(this, "edit-sub-element", {
|
||||||
|
config: config,
|
||||||
|
saveConfig: (newConfig) => this._updateFeature(index!, newConfig),
|
||||||
|
context: featureContext,
|
||||||
|
type: "feature",
|
||||||
|
} as EditSubElementEvent<
|
||||||
|
LovelaceCardFeatureConfig,
|
||||||
|
LovelaceCardFeatureContext
|
||||||
|
>);
|
||||||
|
}
|
||||||
|
|
||||||
|
private _updateFeature(index: number, feature: LovelaceCardFeatureConfig) {
|
||||||
|
const features = this._config!.features!.concat();
|
||||||
|
features[index] = feature;
|
||||||
|
const config = { ...this._config!, features };
|
||||||
|
fireEvent(this, "config-changed", {
|
||||||
|
config: config,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private _computeHelperCallback = (
|
||||||
|
schema:
|
||||||
|
| SchemaUnion<ReturnType<typeof this._schema>>
|
||||||
|
| SchemaUnion<ReturnType<typeof this._featuresSchema>>
|
||||||
|
): string | undefined => {
|
||||||
|
switch (schema.name) {
|
||||||
|
case "alert_classes":
|
||||||
|
if (this._config?.display_type === "compact") {
|
||||||
|
return this.hass!.localize(
|
||||||
|
`ui.panel.lovelace.editor.card.area.alert_classes_helper`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
default:
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
private _computeLabelCallback = (
|
private _computeLabelCallback = (
|
||||||
schema: SchemaUnion<ReturnType<typeof this._schema>>
|
schema:
|
||||||
|
| SchemaUnion<ReturnType<typeof this._schema>>
|
||||||
|
| SchemaUnion<ReturnType<typeof this._featuresSchema>>
|
||||||
) => {
|
) => {
|
||||||
switch (schema.name) {
|
switch (schema.name) {
|
||||||
case "theme":
|
|
||||||
return `${this.hass!.localize(
|
|
||||||
"ui.panel.lovelace.editor.card.generic.theme"
|
|
||||||
)} (${this.hass!.localize(
|
|
||||||
"ui.panel.lovelace.editor.card.config.optional"
|
|
||||||
)})`;
|
|
||||||
case "area":
|
case "area":
|
||||||
return this.hass!.localize("ui.panel.lovelace.editor.card.area.name");
|
return this.hass!.localize("ui.panel.lovelace.editor.card.area.name");
|
||||||
|
|
||||||
|
case "name":
|
||||||
|
case "camera_view":
|
||||||
|
case "content":
|
||||||
|
return this.hass!.localize(
|
||||||
|
`ui.panel.lovelace.editor.card.generic.${schema.name}`
|
||||||
|
);
|
||||||
case "navigation_path":
|
case "navigation_path":
|
||||||
return this.hass!.localize(
|
return this.hass!.localize(
|
||||||
"ui.panel.lovelace.editor.action-editor.navigation_path"
|
"ui.panel.lovelace.editor.action-editor.navigation_path"
|
||||||
);
|
);
|
||||||
case "aspect_ratio":
|
case "interactions":
|
||||||
|
case "features_position":
|
||||||
return this.hass!.localize(
|
return this.hass!.localize(
|
||||||
"ui.panel.lovelace.editor.card.generic.aspect_ratio"
|
`ui.panel.lovelace.editor.card.tile.${schema.name}`
|
||||||
);
|
|
||||||
case "camera_view":
|
|
||||||
return this.hass!.localize(
|
|
||||||
"ui.panel.lovelace.editor.card.generic.camera_view"
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return this.hass!.localize(
|
return this.hass!.localize(
|
||||||
`ui.panel.lovelace.editor.card.area.${schema.name}`
|
`ui.panel.lovelace.editor.card.area.${schema.name}`
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
static get styles() {
|
||||||
|
return [
|
||||||
|
configElementStyle,
|
||||||
|
css`
|
||||||
|
ha-form {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
}
|
||||||
|
.features-form {
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
|
@ -0,0 +1,180 @@
|
|||||||
|
import { html, LitElement, nothing } from "lit";
|
||||||
|
import { customElement, property, state } from "lit/decorators";
|
||||||
|
import memoizeOne from "memoize-one";
|
||||||
|
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||||
|
import type { LocalizeFunc } from "../../../../common/translations/localize";
|
||||||
|
import "../../../../components/ha-form/ha-form";
|
||||||
|
import type {
|
||||||
|
HaFormSchema,
|
||||||
|
SchemaUnion,
|
||||||
|
} from "../../../../components/ha-form/types";
|
||||||
|
import type { HomeAssistant } from "../../../../types";
|
||||||
|
import { getAreaControlEntities } from "../../card-features/hui-area-controls-card-feature";
|
||||||
|
import {
|
||||||
|
AREA_CONTROLS,
|
||||||
|
type AreaControl,
|
||||||
|
type AreaControlsCardFeatureConfig,
|
||||||
|
type LovelaceCardFeatureContext,
|
||||||
|
} from "../../card-features/types";
|
||||||
|
import type { LovelaceCardFeatureEditor } from "../../types";
|
||||||
|
|
||||||
|
type AreaControlsCardFeatureData = AreaControlsCardFeatureConfig & {
|
||||||
|
customize_controls: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
@customElement("hui-area-controls-card-feature-editor")
|
||||||
|
export class HuiAreaControlsCardFeatureEditor
|
||||||
|
extends LitElement
|
||||||
|
implements LovelaceCardFeatureEditor
|
||||||
|
{
|
||||||
|
@property({ attribute: false }) public hass?: HomeAssistant;
|
||||||
|
|
||||||
|
@property({ attribute: false }) public context?: LovelaceCardFeatureContext;
|
||||||
|
|
||||||
|
@state() private _config?: AreaControlsCardFeatureConfig;
|
||||||
|
|
||||||
|
public setConfig(config: AreaControlsCardFeatureConfig): void {
|
||||||
|
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 _compatibleControls = memoizeOne(
|
||||||
|
(
|
||||||
|
areaId: string,
|
||||||
|
// needed to update memoized function when entities, devices or areas change
|
||||||
|
_entities: HomeAssistant["entities"],
|
||||||
|
_devices: HomeAssistant["devices"],
|
||||||
|
_areas: HomeAssistant["areas"]
|
||||||
|
) => {
|
||||||
|
if (!this.hass) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
const controlEntities = getAreaControlEntities(
|
||||||
|
AREA_CONTROLS as unknown as AreaControl[],
|
||||||
|
areaId,
|
||||||
|
this.hass!
|
||||||
|
);
|
||||||
|
return (
|
||||||
|
Object.keys(controlEntities) as (keyof typeof controlEntities)[]
|
||||||
|
).filter((control) => controlEntities[control].length > 0);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
protected render() {
|
||||||
|
if (!this.hass || !this._config || !this.context?.area_id) {
|
||||||
|
return nothing;
|
||||||
|
}
|
||||||
|
|
||||||
|
const compatibleControls = this._compatibleControls(
|
||||||
|
this.context.area_id,
|
||||||
|
this.hass.entities,
|
||||||
|
this.hass.devices,
|
||||||
|
this.hass.areas
|
||||||
|
);
|
||||||
|
|
||||||
|
if (compatibleControls.length === 0) {
|
||||||
|
return html`
|
||||||
|
<ha-alert alert-type="warning">
|
||||||
|
${this.hass.localize(
|
||||||
|
"ui.panel.lovelace.editor.features.types.area-controls.no_compatible_controls"
|
||||||
|
)}
|
||||||
|
</ha-alert>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const data: AreaControlsCardFeatureData = {
|
||||||
|
...this._config,
|
||||||
|
customize_controls: this._config.controls !== undefined,
|
||||||
|
};
|
||||||
|
|
||||||
|
const schema = this._schema(
|
||||||
|
this.hass.localize,
|
||||||
|
data.customize_controls,
|
||||||
|
compatibleControls
|
||||||
|
);
|
||||||
|
|
||||||
|
return html`
|
||||||
|
<ha-form
|
||||||
|
.hass=${this.hass}
|
||||||
|
.data=${data}
|
||||||
|
.schema=${schema}
|
||||||
|
.computeLabel=${this._computeLabelCallback}
|
||||||
|
@value-changed=${this._valueChanged}
|
||||||
|
></ha-form>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _valueChanged(ev: CustomEvent): void {
|
||||||
|
const { customize_controls, ...config } = ev.detail
|
||||||
|
.value as AreaControlsCardFeatureData;
|
||||||
|
|
||||||
|
if (customize_controls && !config.controls) {
|
||||||
|
config.controls = this._compatibleControls(
|
||||||
|
this.context!.area_id!,
|
||||||
|
this.hass!.entities,
|
||||||
|
this.hass!.devices,
|
||||||
|
this.hass!.areas
|
||||||
|
).concat();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!customize_controls && config.controls) {
|
||||||
|
delete config.controls;
|
||||||
|
}
|
||||||
|
|
||||||
|
fireEvent(this, "config-changed", { config: config });
|
||||||
|
}
|
||||||
|
|
||||||
|
private _computeLabelCallback = (
|
||||||
|
schema: SchemaUnion<ReturnType<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}`
|
||||||
|
);
|
||||||
|
default:
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
"hui-area-controls-card-feature-editor": HuiAreaControlsCardFeatureEditor;
|
||||||
|
}
|
||||||
|
}
|
@ -18,6 +18,7 @@ import {
|
|||||||
} from "../../../../data/lovelace_custom_cards";
|
} from "../../../../data/lovelace_custom_cards";
|
||||||
import type { HomeAssistant } from "../../../../types";
|
import type { HomeAssistant } from "../../../../types";
|
||||||
import { supportsAlarmModesCardFeature } from "../../card-features/hui-alarm-modes-card-feature";
|
import { supportsAlarmModesCardFeature } from "../../card-features/hui-alarm-modes-card-feature";
|
||||||
|
import { supportsAreaControlsCardFeature } from "../../card-features/hui-area-controls-card-feature";
|
||||||
import { supportsClimateFanModesCardFeature } from "../../card-features/hui-climate-fan-modes-card-feature";
|
import { supportsClimateFanModesCardFeature } from "../../card-features/hui-climate-fan-modes-card-feature";
|
||||||
import { supportsClimateHvacModesCardFeature } from "../../card-features/hui-climate-hvac-modes-card-feature";
|
import { supportsClimateHvacModesCardFeature } from "../../card-features/hui-climate-hvac-modes-card-feature";
|
||||||
import { supportsClimatePresetModesCardFeature } from "../../card-features/hui-climate-preset-modes-card-feature";
|
import { supportsClimatePresetModesCardFeature } from "../../card-features/hui-climate-preset-modes-card-feature";
|
||||||
@ -61,6 +62,7 @@ type SupportsFeature = (
|
|||||||
|
|
||||||
const UI_FEATURE_TYPES = [
|
const UI_FEATURE_TYPES = [
|
||||||
"alarm-modes",
|
"alarm-modes",
|
||||||
|
"area-controls",
|
||||||
"climate-fan-modes",
|
"climate-fan-modes",
|
||||||
"climate-hvac-modes",
|
"climate-hvac-modes",
|
||||||
"climate-preset-modes",
|
"climate-preset-modes",
|
||||||
@ -95,6 +97,7 @@ type UiFeatureTypes = (typeof UI_FEATURE_TYPES)[number];
|
|||||||
|
|
||||||
const EDITABLES_FEATURE_TYPES = new Set<UiFeatureTypes>([
|
const EDITABLES_FEATURE_TYPES = new Set<UiFeatureTypes>([
|
||||||
"alarm-modes",
|
"alarm-modes",
|
||||||
|
"area-controls",
|
||||||
"climate-fan-modes",
|
"climate-fan-modes",
|
||||||
"climate-hvac-modes",
|
"climate-hvac-modes",
|
||||||
"climate-preset-modes",
|
"climate-preset-modes",
|
||||||
@ -116,6 +119,7 @@ const SUPPORTS_FEATURE_TYPES: Record<
|
|||||||
SupportsFeature | undefined
|
SupportsFeature | undefined
|
||||||
> = {
|
> = {
|
||||||
"alarm-modes": supportsAlarmModesCardFeature,
|
"alarm-modes": supportsAlarmModesCardFeature,
|
||||||
|
"area-controls": supportsAreaControlsCardFeature,
|
||||||
"climate-fan-modes": supportsClimateFanModesCardFeature,
|
"climate-fan-modes": supportsClimateFanModesCardFeature,
|
||||||
"climate-swing-modes": supportsClimateSwingModesCardFeature,
|
"climate-swing-modes": supportsClimateSwingModesCardFeature,
|
||||||
"climate-swing-horizontal-modes":
|
"climate-swing-horizontal-modes":
|
||||||
|
@ -7172,10 +7172,17 @@
|
|||||||
},
|
},
|
||||||
"area": {
|
"area": {
|
||||||
"name": "Area",
|
"name": "Area",
|
||||||
"alert_classes": "Alert Classes",
|
"alert_classes": "Alert classes",
|
||||||
"sensor_classes": "Sensor Classes",
|
"alert_classes_helper": "In compact style, only the first one will be shown. Order alerts by priority.",
|
||||||
|
"sensor_classes": "Sensor classes",
|
||||||
"description": "The Area card automatically displays entities of a specific area.",
|
"description": "The Area card automatically displays entities of a specific area.",
|
||||||
"show_camera": "Show camera feed instead of area picture"
|
"display_type": "Display type",
|
||||||
|
"display_type_options": {
|
||||||
|
"compact": "Compact",
|
||||||
|
"icon": "Area icon",
|
||||||
|
"picture": "Area picture",
|
||||||
|
"camera": "Camera feed"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"calendar": {
|
"calendar": {
|
||||||
"name": "Calendar",
|
"name": "Calendar",
|
||||||
@ -7832,6 +7839,17 @@
|
|||||||
"ask": "Ask"
|
"ask": "Ask"
|
||||||
},
|
},
|
||||||
"backup_not_supported": "Backup is not supported."
|
"backup_not_supported": "Backup is not supported."
|
||||||
|
},
|
||||||
|
"area-controls": {
|
||||||
|
"label": "Area controls",
|
||||||
|
"customize_controls": "Customize controls",
|
||||||
|
"controls": "Controls",
|
||||||
|
"controls_options": {
|
||||||
|
"light": "Lights",
|
||||||
|
"fan": "Fans",
|
||||||
|
"switch": "Switches"
|
||||||
|
},
|
||||||
|
"no_compatible_controls": "No compatible controls available for this area"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
Loading…
x
Reference in New Issue
Block a user