Add valve open/close card feature (#26488)

* Add valve open/close card feature

* Toggle button UI if no assumed state
This commit is contained in:
Aidan Timson
2025-08-12 13:26:12 +01:00
committed by GitHub
parent a7eef81272
commit 539e89e7b5
5 changed files with 238 additions and 0 deletions

View File

@@ -0,0 +1,225 @@
import { mdiStop, mdiValveClosed, mdiValveOpen } from "@mdi/js";
import { html, LitElement, nothing, css } from "lit";
import { customElement, property, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import { styleMap } from "lit/directives/style-map";
import { computeDomain } from "../../../common/entity/compute_domain";
import { supportsFeature } from "../../../common/entity/supports-feature";
import { stateColorCss } from "../../../common/entity/state_color";
import "../../../components/ha-control-button";
import "../../../components/ha-control-button-group";
import "../../../components/ha-svg-icon";
import {
canClose,
canOpen,
canStop,
ValveEntityFeature,
type ValveEntity,
} from "../../../data/valve";
import { UNAVAILABLE, UNKNOWN } from "../../../data/entity";
import type { HomeAssistant } from "../../../types";
import type { LovelaceCardFeature } from "../types";
import { cardFeatureStyles } from "./common/card-feature-styles";
import type {
ValveOpenCloseCardFeatureConfig,
LovelaceCardFeatureContext,
} from "./types";
import "../../../components/ha-control-switch";
export const supportsValveOpenCloseCardFeature = (
hass: HomeAssistant,
context: LovelaceCardFeatureContext
) => {
const stateObj = context.entity_id
? hass.states[context.entity_id]
: undefined;
if (!stateObj) return false;
const domain = computeDomain(stateObj.entity_id);
return (
domain === "valve" &&
(supportsFeature(stateObj, ValveEntityFeature.OPEN) ||
supportsFeature(stateObj, ValveEntityFeature.CLOSE))
);
};
@customElement("hui-valve-open-close-card-feature")
class HuiValveOpenCloseCardFeature
extends LitElement
implements LovelaceCardFeature
{
@property({ attribute: false }) public hass?: HomeAssistant;
@property({ attribute: false }) public context?: LovelaceCardFeatureContext;
@state() private _config?: ValveOpenCloseCardFeatureConfig;
private get _stateObj() {
if (!this.hass || !this.context || !this.context.entity_id) {
return undefined;
}
return this.hass.states[this.context.entity_id!] as ValveEntity | undefined;
}
static getStubConfig(): ValveOpenCloseCardFeatureConfig {
return {
type: "valve-open-close",
};
}
public setConfig(config: ValveOpenCloseCardFeatureConfig): void {
if (!config) {
throw new Error("Invalid configuration");
}
this._config = config;
}
private _onOpenValve(): void {
this.hass!.callService("valve", "open_valve", {
entity_id: this._stateObj!.entity_id,
});
}
private _onCloseValve(): void {
this.hass!.callService("valve", "close_valve", {
entity_id: this._stateObj!.entity_id,
});
}
private _onOpenTap(ev): void {
ev.stopPropagation();
this._onOpenValve();
}
private _onCloseTap(ev): void {
ev.stopPropagation();
this._onCloseValve();
}
private _onStopTap(ev): void {
ev.stopPropagation();
this.hass!.callService("valve", "stop_valve", {
entity_id: this._stateObj!.entity_id,
});
}
private _valueChanged(ev): void {
ev.stopPropagation();
const checked = ev.target.checked as boolean;
if (checked) {
this._onOpenValve();
} else {
this._onCloseValve();
}
}
protected render() {
if (
!this._config ||
!this.hass ||
!this.context ||
!this._stateObj ||
!supportsValveOpenCloseCardFeature(this.hass, this.context)
) {
return nothing;
}
// Determine colors and active states for toggle-style UI
const openColor = stateColorCss(this._stateObj, "open");
const closedColor = stateColorCss(this._stateObj, "closed");
const openIcon = mdiValveOpen;
const closedIcon = mdiValveClosed;
const isOpen =
this._stateObj.state === "open" ||
this._stateObj.state === "closing" ||
this._stateObj.state === "opening";
const isClosed = this._stateObj.state === "closed";
if (
this._stateObj.attributes.assumed_state ||
this._stateObj.state === UNKNOWN
) {
return html`
<ha-control-button-group>
${supportsFeature(this._stateObj, ValveEntityFeature.CLOSE)
? html`
<ha-control-button
.label=${this.hass.localize("ui.card.valve.close_valve")}
@click=${this._onCloseTap}
.disabled=${!canClose(this._stateObj)}
class=${classMap({
active: isClosed,
})}
style=${styleMap({
"--color": closedColor,
})}
>
<ha-svg-icon .path=${mdiValveClosed}></ha-svg-icon>
</ha-control-button>
`
: nothing}
${supportsFeature(this._stateObj, ValveEntityFeature.STOP)
? html`
<ha-control-button
.label=${this.hass.localize("ui.card.valve.stop_valve")}
@click=${this._onStopTap}
.disabled=${!canStop(this._stateObj)}
>
<ha-svg-icon .path=${mdiStop}></ha-svg-icon>
</ha-control-button>
`
: nothing}
${supportsFeature(this._stateObj, ValveEntityFeature.OPEN)
? html`
<ha-control-button
.label=${this.hass.localize("ui.card.valve.open_valve")}
@click=${this._onOpenTap}
.disabled=${!canOpen(this._stateObj)}
class=${classMap({
active: isOpen,
})}
style=${styleMap({
"--color": openColor,
})}
>
<ha-svg-icon .path=${mdiValveOpen}></ha-svg-icon>
</ha-control-button>
`
: nothing}
</ha-control-button-group>
`;
}
return html`
<ha-control-switch
.pathOn=${openIcon}
.pathOff=${closedIcon}
.checked=${isOpen}
@change=${this._valueChanged}
.label=${this.hass.localize("ui.card.common.toggle")}
.disabled=${this._stateObj.state === UNAVAILABLE}
>
</ha-control-switch>
`;
}
static get styles() {
return [
cardFeatureStyles,
css`
ha-control-button.active {
--control-button-icon-color: white;
--control-button-background-color: var(--color);
--control-button-background-opacity: 1;
}
`,
];
}
}
declare global {
interface HTMLElementTagNameMap {
"hui-valve-open-close-card-feature": HuiValveOpenCloseCardFeature;
}
}

View File

@@ -153,6 +153,10 @@ export interface VacuumCommandsCardFeatureConfig {
commands?: VacuumCommand[];
}
export interface ValveOpenCloseCardFeatureConfig {
type: "valve-open-close";
}
export const LAWN_MOWER_COMMANDS = ["start_pause", "dock"] as const;
export type LawnMowerCommand = (typeof LAWN_MOWER_COMMANDS)[number];
@@ -223,6 +227,7 @@ export type LovelaceCardFeatureConfig =
| ToggleCardFeatureConfig
| UpdateActionsCardFeatureConfig
| VacuumCommandsCardFeatureConfig
| ValveOpenCloseCardFeatureConfig
| WaterHeaterOperationModesCardFeatureConfig
| AreaControlsCardFeatureConfig;

View File

@@ -28,6 +28,7 @@ import "../card-features/hui-target-temperature-card-feature";
import "../card-features/hui-toggle-card-feature";
import "../card-features/hui-update-actions-card-feature";
import "../card-features/hui-vacuum-commands-card-feature";
import "../card-features/hui-valve-open-close-card-feature";
import "../card-features/hui-water-heater-operation-modes-card-feature";
import "../card-features/hui-area-controls-card-feature";
@@ -69,6 +70,7 @@ const TYPES = new Set<LovelaceCardFeatureConfig["type"]>([
"toggle",
"update-actions",
"vacuum-commands",
"valve-open-close",
"water-heater-operation-modes",
]);

View File

@@ -48,6 +48,7 @@ import { supportsTargetTemperatureCardFeature } from "../../card-features/hui-ta
import { supportsToggleCardFeature } from "../../card-features/hui-toggle-card-feature";
import { supportsUpdateActionsCardFeature } from "../../card-features/hui-update-actions-card-feature";
import { supportsVacuumCommandsCardFeature } from "../../card-features/hui-vacuum-commands-card-feature";
import { supportsValveOpenCloseCardFeature } from "../../card-features/hui-valve-open-close-card-feature";
import { supportsWaterHeaterOperationModesCardFeature } from "../../card-features/hui-water-heater-operation-modes-card-feature";
import type {
LovelaceCardFeatureConfig,
@@ -94,6 +95,7 @@ const UI_FEATURE_TYPES = [
"toggle",
"update-actions",
"vacuum-commands",
"valve-open-close",
"water-heater-operation-modes",
] as const satisfies readonly FeatureType[];
@@ -155,6 +157,7 @@ const SUPPORTS_FEATURE_TYPES: Record<
toggle: supportsToggleCardFeature,
"update-actions": supportsUpdateActionsCardFeature,
"vacuum-commands": supportsVacuumCommandsCardFeature,
"valve-open-close": supportsValveOpenCloseCardFeature,
"water-heater-operation-modes": supportsWaterHeaterOperationModesCardFeature,
};

View File

@@ -7885,6 +7885,9 @@
"return_home": "[%key:ui::dialogs::more_info_control::vacuum::return_home%]"
}
},
"valve-open-close": {
"label": "Valve open/close"
},
"climate-fan-modes": {
"label": "Climate fan modes",
"style": "[%key:ui::panel::lovelace::editor::features::types::climate-preset-modes::style%]",