mirror of
https://github.com/home-assistant/frontend.git
synced 2025-07-27 03:06:41 +00:00
Add cover controls to area card and improve areas dashboard (#25892)
This commit is contained in:
parent
5c1a8029bf
commit
174d54396f
68
src/common/entity/group_entities.ts
Normal file
68
src/common/entity/group_entities.ts
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
import { callService, type HassEntity } from "home-assistant-js-websocket";
|
||||||
|
import { computeStateDomain } from "./compute_state_domain";
|
||||||
|
import { isUnavailableState, UNAVAILABLE } from "../../data/entity";
|
||||||
|
import type { HomeAssistant } from "../../types";
|
||||||
|
|
||||||
|
export const computeGroupEntitiesState = (states: HassEntity[]): string => {
|
||||||
|
if (!states.length) {
|
||||||
|
return UNAVAILABLE;
|
||||||
|
}
|
||||||
|
|
||||||
|
const validState = states.filter((stateObj) => isUnavailableState(stateObj));
|
||||||
|
|
||||||
|
if (!validState) {
|
||||||
|
return UNAVAILABLE;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use the first state to determine the domain
|
||||||
|
// This assumes all states in the group have the same domain
|
||||||
|
const domain = computeStateDomain(states[0]);
|
||||||
|
|
||||||
|
if (domain === "cover") {
|
||||||
|
for (const s of ["opening", "closing", "open"]) {
|
||||||
|
if (states.some((stateObj) => stateObj.state === s)) {
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return "closed";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (states.some((stateObj) => stateObj.state === "on")) {
|
||||||
|
return "on";
|
||||||
|
}
|
||||||
|
return "off";
|
||||||
|
};
|
||||||
|
|
||||||
|
export const toggleGroupEntities = (
|
||||||
|
hass: HomeAssistant,
|
||||||
|
states: HassEntity[]
|
||||||
|
) => {
|
||||||
|
if (!states.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use the first state to determine the domain
|
||||||
|
// This assumes all states in the group have the same domain
|
||||||
|
const domain = computeStateDomain(states[0]);
|
||||||
|
|
||||||
|
const state = computeGroupEntitiesState(states);
|
||||||
|
|
||||||
|
const isOn = state === "on" || state === "open";
|
||||||
|
|
||||||
|
let service = isOn ? "turn_off" : "turn_on";
|
||||||
|
if (domain === "cover") {
|
||||||
|
if (state === "opening" || state === "closing") {
|
||||||
|
// If the cover is opening or closing, we toggle it to stop it
|
||||||
|
service = "stop_cover";
|
||||||
|
} else {
|
||||||
|
// For covers, we use the open/close service
|
||||||
|
service = isOn ? "close_cover" : "open_cover";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const entitiesIds = states.map((stateObj) => stateObj.entity_id);
|
||||||
|
|
||||||
|
callService(hass.connection, domain, service, {
|
||||||
|
entity_id: entitiesIds,
|
||||||
|
});
|
||||||
|
};
|
@ -64,15 +64,27 @@ export const domainStateColorProperties = (
|
|||||||
const compareState = state !== undefined ? state : stateObj.state;
|
const compareState = state !== undefined ? state : stateObj.state;
|
||||||
const active = stateActive(stateObj, state);
|
const active = stateActive(stateObj, state);
|
||||||
|
|
||||||
|
return domainColorProperties(
|
||||||
|
domain,
|
||||||
|
stateObj.attributes.device_class,
|
||||||
|
compareState,
|
||||||
|
active
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const domainColorProperties = (
|
||||||
|
domain: string,
|
||||||
|
deviceClass: string | undefined,
|
||||||
|
state: string,
|
||||||
|
active: boolean
|
||||||
|
) => {
|
||||||
const properties: string[] = [];
|
const properties: string[] = [];
|
||||||
|
|
||||||
const stateKey = slugify(compareState, "_");
|
const stateKey = slugify(state, "_");
|
||||||
const activeKey = active ? "active" : "inactive";
|
const activeKey = active ? "active" : "inactive";
|
||||||
|
|
||||||
const dc = stateObj.attributes.device_class;
|
if (deviceClass) {
|
||||||
|
properties.push(`--state-${domain}-${deviceClass}-${stateKey}-color`);
|
||||||
if (dc) {
|
|
||||||
properties.push(`--state-${domain}-${dc}-${stateKey}-color`);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
properties.push(
|
properties.push(
|
||||||
|
@ -26,6 +26,7 @@ export class HaControlButtonGroup extends LitElement {
|
|||||||
.container {
|
.container {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
|
justify-content: var(--control-button-group-alignment, start);
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
@ -18,6 +18,8 @@ export class HaDomainIcon extends LitElement {
|
|||||||
|
|
||||||
@property({ attribute: false }) public deviceClass?: string;
|
@property({ attribute: false }) public deviceClass?: string;
|
||||||
|
|
||||||
|
@property({ attribute: false }) public state?: string;
|
||||||
|
|
||||||
@property() public icon?: string;
|
@property() public icon?: string;
|
||||||
|
|
||||||
@property({ attribute: "brand-fallback", type: Boolean })
|
@property({ attribute: "brand-fallback", type: Boolean })
|
||||||
@ -36,14 +38,17 @@ export class HaDomainIcon extends LitElement {
|
|||||||
return this._renderFallback();
|
return this._renderFallback();
|
||||||
}
|
}
|
||||||
|
|
||||||
const icon = domainIcon(this.hass, this.domain, this.deviceClass).then(
|
const icon = domainIcon(
|
||||||
(icn) => {
|
this.hass,
|
||||||
|
this.domain,
|
||||||
|
this.deviceClass,
|
||||||
|
this.state
|
||||||
|
).then((icn) => {
|
||||||
if (icn) {
|
if (icn) {
|
||||||
return html`<ha-icon .icon=${icn}></ha-icon>`;
|
return html`<ha-icon .icon=${icn}></ha-icon>`;
|
||||||
}
|
}
|
||||||
return this._renderFallback();
|
return this._renderFallback();
|
||||||
}
|
});
|
||||||
);
|
|
||||||
|
|
||||||
return html`${until(icon)}`;
|
return html`${until(icon)}`;
|
||||||
}
|
}
|
||||||
|
@ -504,14 +504,25 @@ export const serviceSectionIcon = async (
|
|||||||
export const domainIcon = async (
|
export const domainIcon = async (
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
domain: string,
|
domain: string,
|
||||||
deviceClass?: string
|
deviceClass?: string,
|
||||||
|
state?: string
|
||||||
): Promise<string | undefined> => {
|
): Promise<string | undefined> => {
|
||||||
const entityComponentIcons = await getComponentIcons(hass, domain);
|
const entityComponentIcons = await getComponentIcons(hass, domain);
|
||||||
if (entityComponentIcons) {
|
if (entityComponentIcons) {
|
||||||
const translations =
|
const translations =
|
||||||
(deviceClass && entityComponentIcons[deviceClass]) ||
|
(deviceClass && entityComponentIcons[deviceClass]) ||
|
||||||
entityComponentIcons._;
|
entityComponentIcons._;
|
||||||
return translations?.default;
|
// First check for exact state match
|
||||||
|
if (state && translations.state?.[state]) {
|
||||||
|
return translations.state[state];
|
||||||
}
|
}
|
||||||
|
// Then check for range-based icons if we have a numeric state
|
||||||
|
if (state !== undefined && translations.range && !isNaN(Number(state))) {
|
||||||
|
return getIconFromRange(Number(state), translations.range);
|
||||||
|
}
|
||||||
|
// Fallback to default icon
|
||||||
|
return translations.default;
|
||||||
|
}
|
||||||
|
|
||||||
return undefined;
|
return undefined;
|
||||||
};
|
};
|
||||||
|
@ -25,6 +25,9 @@ export const cardFeatureStyles = css`
|
|||||||
flex-basis: 20px;
|
flex-basis: 20px;
|
||||||
--control-button-padding: 0px;
|
--control-button-padding: 0px;
|
||||||
}
|
}
|
||||||
|
ha-control-button-group[no-stretch] > ha-control-button {
|
||||||
|
max-width: 48px;
|
||||||
|
}
|
||||||
ha-control-button {
|
ha-control-button {
|
||||||
--control-button-focus-color: var(--feature-color);
|
--control-button-focus-color: var(--feature-color);
|
||||||
}
|
}
|
||||||
|
@ -1,17 +1,22 @@
|
|||||||
import { mdiFan, mdiLightbulb, mdiToggleSwitch } from "@mdi/js";
|
import type { HassEntity } from "home-assistant-js-websocket";
|
||||||
import { callService, type HassEntity } from "home-assistant-js-websocket";
|
import { css, html, LitElement, nothing } from "lit";
|
||||||
import { LitElement, css, html, nothing } from "lit";
|
|
||||||
import { customElement, property, state } from "lit/decorators";
|
import { customElement, property, state } from "lit/decorators";
|
||||||
|
import { styleMap } from "lit/directives/style-map";
|
||||||
import memoizeOne from "memoize-one";
|
import memoizeOne from "memoize-one";
|
||||||
|
import { ensureArray } from "../../../common/array/ensure-array";
|
||||||
|
import { generateEntityFilter } from "../../../common/entity/entity_filter";
|
||||||
import {
|
import {
|
||||||
generateEntityFilter,
|
computeGroupEntitiesState,
|
||||||
type EntityFilter,
|
toggleGroupEntities,
|
||||||
} from "../../../common/entity/entity_filter";
|
} from "../../../common/entity/group_entities";
|
||||||
import { stateActive } from "../../../common/entity/state_active";
|
import { stateActive } from "../../../common/entity/state_active";
|
||||||
|
import { domainColorProperties } from "../../../common/entity/state_color";
|
||||||
import "../../../components/ha-control-button";
|
import "../../../components/ha-control-button";
|
||||||
import "../../../components/ha-control-button-group";
|
import "../../../components/ha-control-button-group";
|
||||||
import "../../../components/ha-svg-icon";
|
import "../../../components/ha-svg-icon";
|
||||||
import type { AreaRegistryEntry } from "../../../data/area_registry";
|
import type { AreaRegistryEntry } from "../../../data/area_registry";
|
||||||
|
import { forwardHaptic } from "../../../data/haptics";
|
||||||
|
import { computeCssVariable } from "../../../resources/css-variables";
|
||||||
import type { HomeAssistant } from "../../../types";
|
import type { HomeAssistant } from "../../../types";
|
||||||
import type { LovelaceCardFeature, LovelaceCardFeatureEditor } from "../types";
|
import type { LovelaceCardFeature, LovelaceCardFeatureEditor } from "../types";
|
||||||
import { cardFeatureStyles } from "./common/card-feature-styles";
|
import { cardFeatureStyles } from "./common/card-feature-styles";
|
||||||
@ -19,41 +24,55 @@ import type {
|
|||||||
AreaControl,
|
AreaControl,
|
||||||
AreaControlsCardFeatureConfig,
|
AreaControlsCardFeatureConfig,
|
||||||
LovelaceCardFeatureContext,
|
LovelaceCardFeatureContext,
|
||||||
|
LovelaceCardFeaturePosition,
|
||||||
} from "./types";
|
} from "./types";
|
||||||
import { AREA_CONTROLS } from "./types";
|
import { AREA_CONTROLS } from "./types";
|
||||||
|
|
||||||
interface AreaControlsButton {
|
interface AreaControlsButton {
|
||||||
iconPath: string;
|
offIcon?: string;
|
||||||
onService: string;
|
onIcon?: string;
|
||||||
offService: string;
|
filter: {
|
||||||
filter: EntityFilter;
|
domain: string;
|
||||||
|
device_class?: string;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const coverButton = (deviceClass: string) => ({
|
||||||
|
filter: {
|
||||||
|
domain: "cover",
|
||||||
|
device_class: deviceClass,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
export const AREA_CONTROLS_BUTTONS: Record<AreaControl, AreaControlsButton> = {
|
export const AREA_CONTROLS_BUTTONS: Record<AreaControl, AreaControlsButton> = {
|
||||||
light: {
|
light: {
|
||||||
iconPath: mdiLightbulb,
|
// Overrides the icons for lights
|
||||||
|
offIcon: "mdi:lightbulb-off",
|
||||||
|
onIcon: "mdi:lightbulb",
|
||||||
filter: {
|
filter: {
|
||||||
domain: "light",
|
domain: "light",
|
||||||
},
|
},
|
||||||
onService: "light.turn_on",
|
|
||||||
offService: "light.turn_off",
|
|
||||||
},
|
},
|
||||||
fan: {
|
fan: {
|
||||||
iconPath: mdiFan,
|
|
||||||
filter: {
|
filter: {
|
||||||
domain: "fan",
|
domain: "fan",
|
||||||
},
|
},
|
||||||
onService: "fan.turn_on",
|
|
||||||
offService: "fan.turn_off",
|
|
||||||
},
|
},
|
||||||
switch: {
|
switch: {
|
||||||
iconPath: mdiToggleSwitch,
|
|
||||||
filter: {
|
filter: {
|
||||||
domain: "switch",
|
domain: "switch",
|
||||||
},
|
},
|
||||||
onService: "switch.turn_on",
|
|
||||||
offService: "switch.turn_off",
|
|
||||||
},
|
},
|
||||||
|
"cover-blind": coverButton("blind"),
|
||||||
|
"cover-curtain": coverButton("curtain"),
|
||||||
|
"cover-damper": coverButton("damper"),
|
||||||
|
"cover-awning": coverButton("awning"),
|
||||||
|
"cover-door": coverButton("door"),
|
||||||
|
"cover-garage": coverButton("garage"),
|
||||||
|
"cover-gate": coverButton("gate"),
|
||||||
|
"cover-shade": coverButton("shade"),
|
||||||
|
"cover-shutter": coverButton("shutter"),
|
||||||
|
"cover-window": coverButton("window"),
|
||||||
};
|
};
|
||||||
|
|
||||||
export const supportsAreaControlsCardFeature = (
|
export const supportsAreaControlsCardFeature = (
|
||||||
@ -87,6 +106,8 @@ export const getAreaControlEntities = (
|
|||||||
{} as Record<AreaControl, string[]>
|
{} as Record<AreaControl, string[]>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
export const MAX_DEFAULT_AREA_CONTROLS = 4;
|
||||||
|
|
||||||
@customElement("hui-area-controls-card-feature")
|
@customElement("hui-area-controls-card-feature")
|
||||||
class HuiAreaControlsCardFeature
|
class HuiAreaControlsCardFeature
|
||||||
extends LitElement
|
extends LitElement
|
||||||
@ -96,6 +117,9 @@ class HuiAreaControlsCardFeature
|
|||||||
|
|
||||||
@property({ attribute: false }) public context?: LovelaceCardFeatureContext;
|
@property({ attribute: false }) public context?: LovelaceCardFeatureContext;
|
||||||
|
|
||||||
|
@property({ attribute: false })
|
||||||
|
public position?: LovelaceCardFeaturePosition;
|
||||||
|
|
||||||
@state() private _config?: AreaControlsCardFeatureConfig;
|
@state() private _config?: AreaControlsCardFeatureConfig;
|
||||||
|
|
||||||
private get _area() {
|
private get _area() {
|
||||||
@ -151,17 +175,12 @@ class HuiAreaControlsCardFeature
|
|||||||
);
|
);
|
||||||
const entitiesIds = controlEntities[control];
|
const entitiesIds = controlEntities[control];
|
||||||
|
|
||||||
const { onService, offService } = AREA_CONTROLS_BUTTONS[control];
|
const entities = entitiesIds
|
||||||
|
.map((entityId) => this.hass!.states[entityId] as HassEntity | undefined)
|
||||||
|
.filter((v): v is HassEntity => Boolean(v));
|
||||||
|
|
||||||
const isOn = entitiesIds.some((entityId) =>
|
forwardHaptic("light");
|
||||||
stateActive(this.hass!.states[entityId] as HassEntity)
|
toggleGroupEntities(this.hass, entities);
|
||||||
);
|
|
||||||
|
|
||||||
const [domain, service] = (isOn ? offService : onService).split(".");
|
|
||||||
|
|
||||||
callService(this.hass!.connection, domain, service, {
|
|
||||||
entity_id: entitiesIds,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private _controlEntities = memoizeOne(
|
private _controlEntities = memoizeOne(
|
||||||
@ -200,33 +219,67 @@ class HuiAreaControlsCardFeature
|
|||||||
(control) => controlEntities[control].length > 0
|
(control) => controlEntities[control].length > 0
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!supportedControls.length) {
|
const displayControls = this._config.controls
|
||||||
|
? supportedControls
|
||||||
|
: supportedControls.slice(0, MAX_DEFAULT_AREA_CONTROLS); // Limit to max if using default controls
|
||||||
|
|
||||||
|
if (!displayControls.length) {
|
||||||
return nothing;
|
return nothing;
|
||||||
}
|
}
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
<ha-control-button-group>
|
<ha-control-button-group ?no-stretch=${this.position === "inline"}>
|
||||||
${supportedControls.map((control) => {
|
${displayControls.map((control) => {
|
||||||
const button = AREA_CONTROLS_BUTTONS[control];
|
const button = AREA_CONTROLS_BUTTONS[control];
|
||||||
|
|
||||||
const entities = controlEntities[control];
|
const entityIds = controlEntities[control];
|
||||||
const active = entities.some((entityId) => {
|
|
||||||
const stateObj = this.hass!.states[entityId] as
|
const entities = entityIds
|
||||||
| HassEntity
|
.map(
|
||||||
| undefined;
|
(entityId) =>
|
||||||
if (!stateObj) {
|
this.hass!.states[entityId] as HassEntity | undefined
|
||||||
return false;
|
)
|
||||||
}
|
.filter((v): v is HassEntity => Boolean(v));
|
||||||
return stateActive(stateObj);
|
|
||||||
});
|
const groupState = computeGroupEntitiesState(entities);
|
||||||
|
|
||||||
|
const active = entities[0]
|
||||||
|
? stateActive(entities[0], groupState)
|
||||||
|
: false;
|
||||||
|
|
||||||
|
const label = this.hass!.localize(
|
||||||
|
`ui.card_features.area_controls.${control}.${active ? "off" : "on"}`
|
||||||
|
);
|
||||||
|
|
||||||
|
const icon = active ? button.onIcon : button.offIcon;
|
||||||
|
|
||||||
|
const domain = button.filter.domain;
|
||||||
|
const deviceClass = button.filter.device_class
|
||||||
|
? ensureArray(button.filter.device_class)[0]
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
const activeColor = computeCssVariable(
|
||||||
|
domainColorProperties(domain, deviceClass, groupState, true)
|
||||||
|
);
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
<ha-control-button
|
<ha-control-button
|
||||||
|
style=${styleMap({
|
||||||
|
"--active-color": activeColor,
|
||||||
|
})}
|
||||||
|
.title=${label}
|
||||||
|
aria-label=${label}
|
||||||
class=${active ? "active" : ""}
|
class=${active ? "active" : ""}
|
||||||
.control=${control}
|
.control=${control}
|
||||||
@click=${this._handleButtonTap}
|
@click=${this._handleButtonTap}
|
||||||
>
|
>
|
||||||
<ha-svg-icon .path=${button.iconPath}></ha-svg-icon>
|
<ha-domain-icon
|
||||||
|
.hass=${this.hass}
|
||||||
|
.icon=${icon}
|
||||||
|
.domain=${domain}
|
||||||
|
.deviceClass=${deviceClass}
|
||||||
|
.state=${groupState}
|
||||||
|
></ha-domain-icon>
|
||||||
</ha-control-button>
|
</ha-control-button>
|
||||||
`;
|
`;
|
||||||
})}
|
})}
|
||||||
@ -238,6 +291,9 @@ class HuiAreaControlsCardFeature
|
|||||||
return [
|
return [
|
||||||
cardFeatureStyles,
|
cardFeatureStyles,
|
||||||
css`
|
css`
|
||||||
|
ha-control-button-group {
|
||||||
|
--control-button-group-alignment: flex-end;
|
||||||
|
}
|
||||||
ha-control-button {
|
ha-control-button {
|
||||||
--active-color: var(--state-active-color);
|
--active-color: var(--state-active-color);
|
||||||
--control-button-focus-color: var(--state-active-color);
|
--control-button-focus-color: var(--state-active-color);
|
||||||
|
@ -7,6 +7,7 @@ import type { LovelaceCardFeature } from "../types";
|
|||||||
import type {
|
import type {
|
||||||
LovelaceCardFeatureConfig,
|
LovelaceCardFeatureConfig,
|
||||||
LovelaceCardFeatureContext,
|
LovelaceCardFeatureContext,
|
||||||
|
LovelaceCardFeaturePosition,
|
||||||
} from "./types";
|
} from "./types";
|
||||||
|
|
||||||
@customElement("hui-card-feature")
|
@customElement("hui-card-feature")
|
||||||
@ -19,6 +20,9 @@ export class HuiCardFeature extends LitElement {
|
|||||||
|
|
||||||
@property({ attribute: false }) public color?: string;
|
@property({ attribute: false }) public color?: string;
|
||||||
|
|
||||||
|
@property({ attribute: false })
|
||||||
|
public position?: LovelaceCardFeaturePosition;
|
||||||
|
|
||||||
private _element?: LovelaceCardFeature | HuiErrorCard;
|
private _element?: LovelaceCardFeature | HuiErrorCard;
|
||||||
|
|
||||||
private _getFeatureElement(feature: LovelaceCardFeatureConfig) {
|
private _getFeatureElement(feature: LovelaceCardFeatureConfig) {
|
||||||
@ -41,6 +45,7 @@ export class HuiCardFeature extends LitElement {
|
|||||||
element.hass = this.hass;
|
element.hass = this.hass;
|
||||||
element.context = this.context;
|
element.context = this.context;
|
||||||
element.color = this.color;
|
element.color = this.color;
|
||||||
|
element.position = this.position;
|
||||||
// Backwards compatibility from custom card features
|
// Backwards compatibility from custom card features
|
||||||
if (this.context.entity_id) {
|
if (this.context.entity_id) {
|
||||||
const stateObj = this.hass.states[this.context.entity_id];
|
const stateObj = this.hass.states[this.context.entity_id];
|
||||||
|
@ -5,6 +5,7 @@ import "./hui-card-feature";
|
|||||||
import type {
|
import type {
|
||||||
LovelaceCardFeatureConfig,
|
LovelaceCardFeatureConfig,
|
||||||
LovelaceCardFeatureContext,
|
LovelaceCardFeatureContext,
|
||||||
|
LovelaceCardFeaturePosition,
|
||||||
} from "./types";
|
} from "./types";
|
||||||
|
|
||||||
@customElement("hui-card-features")
|
@customElement("hui-card-features")
|
||||||
@ -17,6 +18,9 @@ export class HuiCardFeatures extends LitElement {
|
|||||||
|
|
||||||
@property({ attribute: false }) public color?: string;
|
@property({ attribute: false }) public color?: string;
|
||||||
|
|
||||||
|
@property({ attribute: false })
|
||||||
|
public position?: LovelaceCardFeaturePosition;
|
||||||
|
|
||||||
protected render() {
|
protected render() {
|
||||||
if (!this.features) {
|
if (!this.features) {
|
||||||
return nothing;
|
return nothing;
|
||||||
@ -29,6 +33,7 @@ export class HuiCardFeatures extends LitElement {
|
|||||||
.context=${this.context}
|
.context=${this.context}
|
||||||
.color=${this.color}
|
.color=${this.color}
|
||||||
.feature=${feature}
|
.feature=${feature}
|
||||||
|
.position=${this.position}
|
||||||
></hui-card-feature>
|
></hui-card-feature>
|
||||||
`
|
`
|
||||||
)}
|
)}
|
||||||
|
@ -158,7 +158,21 @@ export interface UpdateActionsCardFeatureConfig {
|
|||||||
backup?: "yes" | "no" | "ask";
|
backup?: "yes" | "no" | "ask";
|
||||||
}
|
}
|
||||||
|
|
||||||
export const AREA_CONTROLS = ["light", "fan", "switch"] as const;
|
export const AREA_CONTROLS = [
|
||||||
|
"light",
|
||||||
|
"fan",
|
||||||
|
"cover-shutter",
|
||||||
|
"cover-blind",
|
||||||
|
"cover-curtain",
|
||||||
|
"cover-shade",
|
||||||
|
"cover-awning",
|
||||||
|
"cover-garage",
|
||||||
|
"cover-gate",
|
||||||
|
"cover-door",
|
||||||
|
"cover-window",
|
||||||
|
"cover-damper",
|
||||||
|
"switch",
|
||||||
|
] as const;
|
||||||
|
|
||||||
export type AreaControl = (typeof AREA_CONTROLS)[number];
|
export type AreaControl = (typeof AREA_CONTROLS)[number];
|
||||||
|
|
||||||
@ -168,6 +182,8 @@ export interface AreaControlsCardFeatureConfig {
|
|||||||
exclude_entities?: string[];
|
exclude_entities?: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type LovelaceCardFeaturePosition = "bottom" | "inline";
|
||||||
|
|
||||||
export type LovelaceCardFeatureConfig =
|
export type LovelaceCardFeatureConfig =
|
||||||
| AlarmModesCardFeatureConfig
|
| AlarmModesCardFeatureConfig
|
||||||
| ClimateFanModesCardFeatureConfig
|
| ClimateFanModesCardFeatureConfig
|
||||||
|
@ -514,6 +514,7 @@ export class HuiAreaCard extends LitElement implements LovelaceCard {
|
|||||||
.context=${this._featureContext}
|
.context=${this._featureContext}
|
||||||
.color=${this._config.color}
|
.color=${this._config.color}
|
||||||
.features=${features}
|
.features=${features}
|
||||||
|
.position=${featurePosition}
|
||||||
></hui-card-features>
|
></hui-card-features>
|
||||||
`
|
`
|
||||||
: nothing}
|
: nothing}
|
||||||
|
@ -9,7 +9,10 @@ import type {
|
|||||||
ThemeMode,
|
ThemeMode,
|
||||||
TranslationDict,
|
TranslationDict,
|
||||||
} from "../../../types";
|
} from "../../../types";
|
||||||
import type { LovelaceCardFeatureConfig } from "../card-features/types";
|
import type {
|
||||||
|
LovelaceCardFeatureConfig,
|
||||||
|
LovelaceCardFeaturePosition,
|
||||||
|
} from "../card-features/types";
|
||||||
import type { LegacyStateFilter } from "../common/evaluate-filter";
|
import type { LegacyStateFilter } from "../common/evaluate-filter";
|
||||||
import type { Condition, LegacyCondition } from "../common/validate-condition";
|
import type { Condition, LegacyCondition } from "../common/validate-condition";
|
||||||
import type { HuiImage } from "../components/hui-image";
|
import type { HuiImage } from "../components/hui-image";
|
||||||
@ -113,7 +116,7 @@ export interface AreaCardConfig extends LovelaceCardConfig {
|
|||||||
sensor_classes?: string[];
|
sensor_classes?: string[];
|
||||||
alert_classes?: string[];
|
alert_classes?: string[];
|
||||||
features?: LovelaceCardFeatureConfig[];
|
features?: LovelaceCardFeatureConfig[];
|
||||||
features_position?: "bottom" | "inline";
|
features_position?: LovelaceCardFeaturePosition;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ButtonCardConfig extends LovelaceCardConfig {
|
export interface ButtonCardConfig extends LovelaceCardConfig {
|
||||||
@ -564,7 +567,7 @@ export interface TileCardConfig extends LovelaceCardConfig {
|
|||||||
icon_hold_action?: ActionConfig;
|
icon_hold_action?: ActionConfig;
|
||||||
icon_double_tap_action?: ActionConfig;
|
icon_double_tap_action?: ActionConfig;
|
||||||
features?: LovelaceCardFeatureConfig[];
|
features?: LovelaceCardFeatureConfig[];
|
||||||
features_position?: "bottom" | "inline";
|
features_position?: LovelaceCardFeaturePosition;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface HeadingCardConfig extends LovelaceCardConfig {
|
export interface HeadingCardConfig extends LovelaceCardConfig {
|
||||||
|
@ -9,7 +9,10 @@ import type {
|
|||||||
SchemaUnion,
|
SchemaUnion,
|
||||||
} from "../../../../components/ha-form/types";
|
} from "../../../../components/ha-form/types";
|
||||||
import type { HomeAssistant } from "../../../../types";
|
import type { HomeAssistant } from "../../../../types";
|
||||||
import { getAreaControlEntities } from "../../card-features/hui-area-controls-card-feature";
|
import {
|
||||||
|
getAreaControlEntities,
|
||||||
|
MAX_DEFAULT_AREA_CONTROLS,
|
||||||
|
} from "../../card-features/hui-area-controls-card-feature";
|
||||||
import {
|
import {
|
||||||
AREA_CONTROLS,
|
AREA_CONTROLS,
|
||||||
type AreaControl,
|
type AreaControl,
|
||||||
@ -72,7 +75,7 @@ export class HuiAreaControlsCardFeatureEditor
|
|||||||
] as const satisfies readonly HaFormSchema[]
|
] as const satisfies readonly HaFormSchema[]
|
||||||
);
|
);
|
||||||
|
|
||||||
private _compatibleControls = memoizeOne(
|
private _supportedControls = memoizeOne(
|
||||||
(
|
(
|
||||||
areaId: string,
|
areaId: string,
|
||||||
// needed to update memoized function when entities, devices or areas change
|
// needed to update memoized function when entities, devices or areas change
|
||||||
@ -99,14 +102,14 @@ export class HuiAreaControlsCardFeatureEditor
|
|||||||
return nothing;
|
return nothing;
|
||||||
}
|
}
|
||||||
|
|
||||||
const compatibleControls = this._compatibleControls(
|
const supportedControls = this._supportedControls(
|
||||||
this.context.area_id,
|
this.context.area_id,
|
||||||
this.hass.entities,
|
this.hass.entities,
|
||||||
this.hass.devices,
|
this.hass.devices,
|
||||||
this.hass.areas
|
this.hass.areas
|
||||||
);
|
);
|
||||||
|
|
||||||
if (compatibleControls.length === 0) {
|
if (supportedControls.length === 0) {
|
||||||
return html`
|
return html`
|
||||||
<ha-alert alert-type="warning">
|
<ha-alert alert-type="warning">
|
||||||
${this.hass.localize(
|
${this.hass.localize(
|
||||||
@ -124,7 +127,7 @@ export class HuiAreaControlsCardFeatureEditor
|
|||||||
const schema = this._schema(
|
const schema = this._schema(
|
||||||
this.hass.localize,
|
this.hass.localize,
|
||||||
data.customize_controls,
|
data.customize_controls,
|
||||||
compatibleControls
|
supportedControls
|
||||||
);
|
);
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
@ -143,12 +146,12 @@ export class HuiAreaControlsCardFeatureEditor
|
|||||||
.value as AreaControlsCardFeatureData;
|
.value as AreaControlsCardFeatureData;
|
||||||
|
|
||||||
if (customize_controls && !config.controls) {
|
if (customize_controls && !config.controls) {
|
||||||
config.controls = this._compatibleControls(
|
config.controls = this._supportedControls(
|
||||||
this.context!.area_id!,
|
this.context!.area_id!,
|
||||||
this.hass!.entities,
|
this.hass!.entities,
|
||||||
this.hass!.devices,
|
this.hass!.devices,
|
||||||
this.hass!.areas
|
this.hass!.areas
|
||||||
).concat();
|
).slice(0, MAX_DEFAULT_AREA_CONTROLS); // Limit to max default controls
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!customize_controls && config.controls) {
|
if (!customize_controls && config.controls) {
|
||||||
|
@ -6,7 +6,7 @@ import type { LovelaceSectionConfig } from "../../../../data/lovelace/config/sec
|
|||||||
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 { getAreaControlEntities } from "../../card-features/hui-area-controls-card-feature";
|
import { getAreaControlEntities } from "../../card-features/hui-area-controls-card-feature";
|
||||||
import type { AreaControl } from "../../card-features/types";
|
import { AREA_CONTROLS, type AreaControl } from "../../card-features/types";
|
||||||
import type { AreaCardConfig, HeadingCardConfig } from "../../cards/types";
|
import type { AreaCardConfig, HeadingCardConfig } from "../../cards/types";
|
||||||
import type { EntitiesDisplay } from "./area-view-strategy";
|
import type { EntitiesDisplay } from "./area-view-strategy";
|
||||||
import { computeAreaPath, getAreas } from "./helpers/areas-strategy-helper";
|
import { computeAreaPath, getAreas } from "./helpers/areas-strategy-helper";
|
||||||
@ -77,7 +77,9 @@ export class AreasOverviewViewStrategy extends ReactiveElement {
|
|||||||
.map((display) => display.hidden || [])
|
.map((display) => display.hidden || [])
|
||||||
.flat();
|
.flat();
|
||||||
|
|
||||||
const controls: AreaControl[] = ["light", "fan"];
|
const controls: AreaControl[] = AREA_CONTROLS.filter(
|
||||||
|
(a) => a !== "switch" // Exclude switches control for areas as we don't know what the switches control
|
||||||
|
);
|
||||||
const controlEntities = getAreaControlEntities(
|
const controlEntities = getAreaControlEntities(
|
||||||
controls,
|
controls,
|
||||||
area.area_id,
|
area.area_id,
|
||||||
@ -112,6 +114,11 @@ export class AreasOverviewViewStrategy extends ReactiveElement {
|
|||||||
},
|
},
|
||||||
]
|
]
|
||||||
: [],
|
: [],
|
||||||
|
grid_options: {
|
||||||
|
rows: 1,
|
||||||
|
columns: 12,
|
||||||
|
},
|
||||||
|
features_position: "inline",
|
||||||
navigation_path: path,
|
navigation_path: path,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
@ -13,6 +13,7 @@ import type { Constructor, HomeAssistant } from "../../types";
|
|||||||
import type {
|
import type {
|
||||||
LovelaceCardFeatureConfig,
|
LovelaceCardFeatureConfig,
|
||||||
LovelaceCardFeatureContext,
|
LovelaceCardFeatureContext,
|
||||||
|
LovelaceCardFeaturePosition,
|
||||||
} from "./card-features/types";
|
} from "./card-features/types";
|
||||||
import type { LovelaceElement, LovelaceElementConfig } from "./elements/types";
|
import type { LovelaceElement, LovelaceElementConfig } from "./elements/types";
|
||||||
import type { LovelaceRow, LovelaceRowConfig } from "./entity-rows/types";
|
import type { LovelaceRow, LovelaceRowConfig } from "./entity-rows/types";
|
||||||
@ -179,6 +180,7 @@ export interface LovelaceCardFeature extends HTMLElement {
|
|||||||
context?: LovelaceCardFeatureContext;
|
context?: LovelaceCardFeatureContext;
|
||||||
setConfig(config: LovelaceCardFeatureConfig);
|
setConfig(config: LovelaceCardFeatureConfig);
|
||||||
color?: string;
|
color?: string;
|
||||||
|
position?: LovelaceCardFeaturePosition;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface LovelaceCardFeatureConstructor
|
export interface LovelaceCardFeatureConstructor
|
||||||
|
@ -325,6 +325,62 @@
|
|||||||
"low": "Low"
|
"low": "Low"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"card_features": {
|
||||||
|
"area_controls": {
|
||||||
|
"light": {
|
||||||
|
"on": "Turn on area lights",
|
||||||
|
"off": "Turn off area lights"
|
||||||
|
},
|
||||||
|
"fan": {
|
||||||
|
"on": "Turn on area fans",
|
||||||
|
"off": "Turn off area fans"
|
||||||
|
},
|
||||||
|
"switch": {
|
||||||
|
"on": "Turn on area switches",
|
||||||
|
"off": "Turn off area switches"
|
||||||
|
},
|
||||||
|
"cover-awning": {
|
||||||
|
"on": "Open area awnings",
|
||||||
|
"off": "Close area awnings"
|
||||||
|
},
|
||||||
|
"cover-blind": {
|
||||||
|
"on": "Open area blinds",
|
||||||
|
"off": "Close area blinds"
|
||||||
|
},
|
||||||
|
"cover-curtain": {
|
||||||
|
"on": "Open area curtains",
|
||||||
|
"off": "Close area curtains"
|
||||||
|
},
|
||||||
|
"cover-damper": {
|
||||||
|
"on": "Open area dampers",
|
||||||
|
"off": "Close area dampers"
|
||||||
|
},
|
||||||
|
"cover-door": {
|
||||||
|
"on": "Open area doors",
|
||||||
|
"off": "Close area doors"
|
||||||
|
},
|
||||||
|
"cover-garage": {
|
||||||
|
"on": "Open garage door",
|
||||||
|
"off": "Close garage door"
|
||||||
|
},
|
||||||
|
"cover-gate": {
|
||||||
|
"on": "Open area gates",
|
||||||
|
"off": "Close area gates"
|
||||||
|
},
|
||||||
|
"cover-shade": {
|
||||||
|
"on": "Open area shades",
|
||||||
|
"off": "Close area shades"
|
||||||
|
},
|
||||||
|
"cover-shutter": {
|
||||||
|
"on": "Open area shutters",
|
||||||
|
"off": "Close area shutters"
|
||||||
|
},
|
||||||
|
"cover-window": {
|
||||||
|
"on": "Open area windows",
|
||||||
|
"off": "Close area windows"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"common": {
|
"common": {
|
||||||
"and": "and",
|
"and": "and",
|
||||||
"continue": "Continue",
|
"continue": "Continue",
|
||||||
@ -383,6 +439,7 @@
|
|||||||
"markdown": "Markdown",
|
"markdown": "Markdown",
|
||||||
"suggest_ai": "Suggest with AI"
|
"suggest_ai": "Suggest with AI"
|
||||||
},
|
},
|
||||||
|
|
||||||
"components": {
|
"components": {
|
||||||
"selectors": {
|
"selectors": {
|
||||||
"media": {
|
"media": {
|
||||||
@ -7857,7 +7914,17 @@
|
|||||||
"controls_options": {
|
"controls_options": {
|
||||||
"light": "Lights",
|
"light": "Lights",
|
||||||
"fan": "Fans",
|
"fan": "Fans",
|
||||||
"switch": "Switches"
|
"switch": "Switches",
|
||||||
|
"cover-awning": "Awnings",
|
||||||
|
"cover-blind": "Blinds",
|
||||||
|
"cover-curtain": "Curtains",
|
||||||
|
"cover-damper": "Dampers",
|
||||||
|
"cover-door": "Doors",
|
||||||
|
"cover-garage": "Garage doors",
|
||||||
|
"cover-gate": "Gates",
|
||||||
|
"cover-shade": "Shades",
|
||||||
|
"cover-shutter": "Shutters",
|
||||||
|
"cover-window": "Windows"
|
||||||
},
|
},
|
||||||
"no_compatible_controls": "No compatible controls available for this area"
|
"no_compatible_controls": "No compatible controls available for this area"
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user