Add valve entity (#19024)

* Add valve entity

* Update icon based on device class

* Check assumed state first

* Reset mode if entity id changes
This commit is contained in:
Paul Bottein 2023-12-18 13:53:52 +01:00 committed by GitHub
parent ad543dbffb
commit 3e7fa66790
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 898 additions and 5 deletions

View File

@ -28,10 +28,12 @@ import {
mdiLockAlert,
mdiLockClock,
mdiLockOpen,
mdiMeterGas,
mdiMotionSensor,
mdiPackage,
mdiPackageDown,
mdiPackageUp,
mdiPipeValve,
mdiPowerPlug,
mdiPowerPlugOff,
mdiRestart,
@ -274,6 +276,16 @@ export const domainIconWithoutDefault = (
: mdiPackageUp
: mdiPackage;
case "valve":
switch (stateObj?.attributes.device_class) {
case "water":
return mdiPipeValve;
case "gas":
return mdiMeterGas;
default:
return mdiPipeValve;
}
case "water_heater":
return compareState === "off" ? mdiWaterBoilerOff : mdiWaterBoiler;

View File

@ -42,6 +42,8 @@ export function stateActive(stateObj: HassEntity, state?: string): boolean {
return compareState !== "standby";
case "vacuum":
return !["idle", "docked", "paused"].includes(compareState);
case "valve":
return compareState !== "closed";
case "plant":
return compareState === "problem";
case "group":

View File

@ -37,6 +37,7 @@ const STATE_COLORED_DOMAIN = new Set([
"timer",
"update",
"vacuum",
"valve",
"water_heater",
]);

View File

@ -0,0 +1,102 @@
import { mdiStop, mdiValveClosed, mdiValveOpen } from "@mdi/js";
import { CSSResultGroup, LitElement, html, css, nothing } from "lit";
import { customElement, property } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import { supportsFeature } from "../common/entity/supports-feature";
import {
ValveEntity,
ValveEntityFeature,
canClose,
canOpen,
canStop,
} from "../data/valve";
import type { HomeAssistant } from "../types";
import "./ha-icon-button";
@customElement("ha-valve-controls")
class HaValveControls extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public stateObj!: ValveEntity;
protected render() {
if (!this.stateObj) {
return nothing;
}
return html`
<div class="state">
<ha-icon-button
class=${classMap({
hidden: !supportsFeature(this.stateObj, ValveEntityFeature.OPEN),
})}
.label=${this.hass.localize("ui.card.valve.open_valve")}
@click=${this._onOpenTap}
.disabled=${!canOpen(this.stateObj)}
.path=${mdiValveOpen}
>
</ha-icon-button>
<ha-icon-button
class=${classMap({
hidden: !supportsFeature(this.stateObj, ValveEntityFeature.STOP),
})}
.label=${this.hass.localize("ui.card.valve.stop_valve")}
@click=${this._onStopTap}
.disabled=${!canStop(this.stateObj)}
.path=${mdiStop}
></ha-icon-button>
<ha-icon-button
class=${classMap({
hidden: !supportsFeature(this.stateObj, ValveEntityFeature.CLOSE),
})}
.label=${this.hass.localize("ui.card.valve.close_valve")}
@click=${this._onCloseTap}
.disabled=${!canClose(this.stateObj)}
.path=${mdiValveClosed}
>
</ha-icon-button>
</div>
`;
}
private _onOpenTap(ev): void {
ev.stopPropagation();
this.hass.callService("valve", "open_valve", {
entity_id: this.stateObj.entity_id,
});
}
private _onCloseTap(ev): void {
ev.stopPropagation();
this.hass.callService("valve", "close_valve", {
entity_id: this.stateObj.entity_id,
});
}
private _onStopTap(ev): void {
ev.stopPropagation();
this.hass.callService("valve", "stop_valve", {
entity_id: this.stateObj.entity_id,
});
}
static get styles(): CSSResultGroup {
return css`
:host {
display: block;
}
.state {
white-space: nowrap;
}
.hidden {
visibility: hidden !important;
}
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-valve-controls": HaValveControls;
}
}

View File

@ -65,7 +65,7 @@ export function canOpen(stateObj: CoverEntity) {
return false;
}
const assumedState = stateObj.attributes.assumed_state === true;
return (!isFullyOpen(stateObj) && !isOpening(stateObj)) || assumedState;
return assumedState || (!isFullyOpen(stateObj) && !isOpening(stateObj));
}
export function canClose(stateObj: CoverEntity): boolean {
@ -73,7 +73,7 @@ export function canClose(stateObj: CoverEntity): boolean {
return false;
}
const assumedState = stateObj.attributes.assumed_state === true;
return (!isFullyClosed(stateObj) && !isClosing(stateObj)) || assumedState;
return assumedState || (!isFullyClosed(stateObj) && !isClosing(stateObj));
}
export function canStop(stateObj: CoverEntity): boolean {
@ -85,7 +85,7 @@ export function canOpenTilt(stateObj: CoverEntity): boolean {
return false;
}
const assumedState = stateObj.attributes.assumed_state === true;
return !isFullyOpenTilt(stateObj) || assumedState;
return assumedState || !isFullyOpenTilt(stateObj);
}
export function canCloseTilt(stateObj: CoverEntity): boolean {
@ -93,7 +93,7 @@ export function canCloseTilt(stateObj: CoverEntity): boolean {
return false;
}
const assumedState = stateObj.attributes.assumed_state === true;
return !isFullyClosedTilt(stateObj) || assumedState;
return assumedState || !isFullyClosedTilt(stateObj);
}
export function canStopTilt(stateObj: CoverEntity): boolean {

View File

@ -75,6 +75,9 @@ export const DOMAIN_ATTRIBUTES_UNITS = {
vacuum: {
battery_level: "%",
},
valve: {
current_position: "%",
},
sensor: {
battery_level: "%",
},

85
src/data/valve.ts Normal file
View File

@ -0,0 +1,85 @@
import {
HassEntityAttributeBase,
HassEntityBase,
} from "home-assistant-js-websocket";
import { UNAVAILABLE } from "./entity";
import { stateActive } from "../common/entity/state_active";
import { HomeAssistant } from "../types";
export const enum ValveEntityFeature {
OPEN = 1,
CLOSE = 2,
SET_POSITION = 4,
STOP = 8,
}
export function isFullyOpen(stateObj: ValveEntity) {
if (stateObj.attributes.current_position !== undefined) {
return stateObj.attributes.current_position === 100;
}
return stateObj.state === "open";
}
export function isFullyClosed(stateObj: ValveEntity) {
if (stateObj.attributes.current_position !== undefined) {
return stateObj.attributes.current_position === 0;
}
return stateObj.state === "closed";
}
export function isOpening(stateObj: ValveEntity) {
return stateObj.state === "opening";
}
export function isClosing(stateObj: ValveEntity) {
return stateObj.state === "closing";
}
export function canOpen(stateObj: ValveEntity) {
if (stateObj.state === UNAVAILABLE) {
return false;
}
const assumedState = stateObj.attributes.assumed_state === true;
return assumedState || (!isFullyOpen(stateObj) && !isOpening(stateObj));
}
export function canClose(stateObj: ValveEntity): boolean {
if (stateObj.state === UNAVAILABLE) {
return false;
}
const assumedState = stateObj.attributes.assumed_state === true;
return assumedState || (!isFullyClosed(stateObj) && !isClosing(stateObj));
}
export function canStop(stateObj: ValveEntity): boolean {
return stateObj.state !== UNAVAILABLE;
}
interface ValveEntityAttributes extends HassEntityAttributeBase {
current_position?: number;
position?: number;
}
export interface ValveEntity extends HassEntityBase {
attributes: ValveEntityAttributes;
}
export function computeValvePositionStateDisplay(
stateObj: ValveEntity,
hass: HomeAssistant,
position?: number
) {
const statePosition = stateActive(stateObj)
? stateObj.attributes.current_position
: undefined;
const currentPosition = position ?? statePosition;
return currentPosition && currentPosition !== 100
? hass.formatEntityAttributeValue(
stateObj,
"current_position",
Math.round(currentPosition)
)
: "";
}

View File

@ -27,6 +27,7 @@ export const DOMAINS_WITH_NEW_MORE_INFO = [
"lock",
"siren",
"switch",
"valve",
"water_heater",
];
/** Domains with separate more info dialog. */
@ -61,6 +62,7 @@ export const DOMAINS_WITH_MORE_INFO = [
"timer",
"update",
"vacuum",
"valve",
"water_heater",
"weather",
];

View File

@ -42,7 +42,9 @@ class MoreInfoCover extends LitElement {
protected willUpdate(changedProps: PropertyValues): void {
super.willUpdate(changedProps);
if (changedProps.has("stateObj") && this.stateObj) {
if (!this._mode) {
const entityId = this.stateObj.entity_id;
const oldEntityId = changedProps.get("stateObj")?.entity_id;
if (!this._mode || entityId !== oldEntityId) {
this._mode =
supportsFeature(this.stateObj, CoverEntityFeature.SET_POSITION) ||
supportsFeature(this.stateObj, CoverEntityFeature.SET_TILT_POSITION)

View File

@ -0,0 +1,192 @@
import { mdiMenu, mdiSwapVertical } from "@mdi/js";
import {
CSSResultGroup,
LitElement,
PropertyValues,
css,
html,
nothing,
} from "lit";
import { customElement, property, state } from "lit/decorators";
import { supportsFeature } from "../../../common/entity/supports-feature";
import "../../../components/ha-attributes";
import "../../../components/ha-icon-button-group";
import "../../../components/ha-icon-button-toggle";
import {
ValveEntity,
ValveEntityFeature,
computeValvePositionStateDisplay,
} from "../../../data/valve";
import "../../../state-control/valve/ha-state-control-valve-buttons";
import "../../../state-control/valve/ha-state-control-valve-position";
import "../../../state-control/valve/ha-state-control-valve-toggle";
import type { HomeAssistant } from "../../../types";
import "../components/ha-more-info-state-header";
import { moreInfoControlStyle } from "../components/more-info-control-style";
type Mode = "position" | "button";
@customElement("more-info-valve")
class MoreInfoValve extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public stateObj?: ValveEntity;
@state() private _mode?: Mode;
private _setMode(ev) {
this._mode = ev.currentTarget.mode;
}
protected willUpdate(changedProps: PropertyValues): void {
super.willUpdate(changedProps);
if (changedProps.has("stateObj") && this.stateObj) {
const entityId = this.stateObj.entity_id;
const oldEntityId = changedProps.get("stateObj")?.entity_id;
if (!this._mode || entityId !== oldEntityId) {
this._mode = supportsFeature(
this.stateObj,
ValveEntityFeature.SET_POSITION
)
? "position"
: "button";
}
}
}
private get _stateOverride() {
const stateDisplay = this.hass.formatEntityState(this.stateObj!);
const positionStateDisplay = computeValvePositionStateDisplay(
this.stateObj!,
this.hass
);
if (positionStateDisplay) {
return `${stateDisplay}${positionStateDisplay}`;
}
return stateDisplay;
}
protected render() {
if (!this.hass || !this.stateObj) {
return nothing;
}
const supportsPosition = supportsFeature(
this.stateObj,
ValveEntityFeature.SET_POSITION
);
const supportsOpenClose =
supportsFeature(this.stateObj, ValveEntityFeature.OPEN) ||
supportsFeature(this.stateObj, ValveEntityFeature.CLOSE) ||
supportsFeature(this.stateObj, ValveEntityFeature.STOP);
const supportsOpenCloseWithoutStop =
supportsFeature(this.stateObj, ValveEntityFeature.OPEN) &&
supportsFeature(this.stateObj, ValveEntityFeature.CLOSE) &&
!supportsFeature(this.stateObj, ValveEntityFeature.STOP);
return html`
<ha-more-info-state-header
.hass=${this.hass}
.stateObj=${this.stateObj}
.stateOverride=${this._stateOverride}
></ha-more-info-state-header>
<div class="controls">
<div class="main-control">
${
this._mode === "position"
? html`
${supportsPosition
? html`
<ha-state-control-valve-position
.stateObj=${this.stateObj}
.hass=${this.hass}
></ha-state-control-valve-position>
`
: nothing}
`
: nothing
}
${
this._mode === "button"
? html`
${supportsOpenCloseWithoutStop
? html`
<ha-state-control-valve-toggle
.stateObj=${this.stateObj}
.hass=${this.hass}
></ha-state-control-valve-toggle>
`
: supportsOpenClose
? html`
<ha-state-control-valve-buttons
.stateObj=${this.stateObj}
.hass=${this.hass}
></ha-state-control-valve-buttons>
`
: nothing}
`
: nothing
}
</div>
${
supportsPosition && supportsOpenClose
? html`
<ha-icon-button-group>
<ha-icon-button-toggle
.label=${this.hass.localize(
`ui.dialogs.more_info_control.valve.switch_mode.position`
)}
.selected=${this._mode === "position"}
.path=${mdiMenu}
.mode=${"position"}
@click=${this._setMode}
></ha-icon-button-toggle>
<ha-icon-button-toggle
.label=${this.hass.localize(
`ui.dialogs.more_info_control.valve.switch_mode.button`
)}
.selected=${this._mode === "button"}
.path=${mdiSwapVertical}
.mode=${"button"}
@click=${this._setMode}
></ha-icon-button-toggle>
</ha-icon-button-group>
`
: nothing
}
</div>
</div>
<ha-attributes
.hass=${this.hass}
.stateObj=${this.stateObj}
extra-filters="current_position,current_tilt_position"
></ha-attributes>
`;
}
static get styles(): CSSResultGroup {
return [
moreInfoControlStyle,
css`
.main-control {
display: flex;
flex-direction: row;
align-items: center;
}
.main-control > * {
margin: 0 8px;
}
`,
];
}
}
declare global {
interface HTMLElementTagNameMap {
"more-info-valve": MoreInfoValve;
}
}

View File

@ -35,6 +35,7 @@ const LAZY_LOADED_MORE_INFO_CONTROL = {
timer: () => import("./controls/more-info-timer"),
update: () => import("./controls/more-info-update"),
vacuum: () => import("./controls/more-info-vacuum"),
valve: () => import("./controls/more-info-valve"),
water_heater: () => import("./controls/more-info-water_heater"),
weather: () => import("./controls/more-info-weather"),
};

View File

@ -251,6 +251,10 @@ export class HuiTileCard extends LitElement implements LovelaceCard {
return this._renderStateContent(stateObj, ["state", "current_position"]);
}
if (domain === "valve" && active) {
return this._renderStateContent(stateObj, ["state", "current_position"]);
}
if (domain === "humidifier") {
return this._renderStateContent(stateObj, ["state", "current_humidity"]);
}

View File

@ -49,6 +49,7 @@ const LAZY_LOAD_TYPES = {
"time-entity": () => import("../entity-rows/hui-time-entity-row"),
"timer-entity": () => import("../entity-rows/hui-timer-entity-row"),
"update-entity": () => import("../entity-rows/hui-update-entity-row"),
"valve-entity": () => import("../entity-rows/hui-valve-entity-row"),
conditional: () => import("../special-rows/hui-conditional-row"),
"weather-entity": () => import("../entity-rows/hui-weather-entity-row"),
divider: () => import("../special-rows/hui-divider-row"),
@ -94,6 +95,7 @@ const DOMAIN_TO_ELEMENT_TYPE = {
timer: "timer",
update: "update",
vacuum: "toggle",
valve: "valve",
// Temporary. Once climate is rewritten,
// water heater should get its own row.
water_heater: "climate",

View File

@ -0,0 +1,73 @@
import {
css,
CSSResultGroup,
html,
LitElement,
PropertyValues,
nothing,
} from "lit";
import { customElement, property, state } from "lit/decorators";
import "../../../components/ha-valve-controls";
import { ValveEntity } from "../../../data/valve";
import { HomeAssistant } from "../../../types";
import { hasConfigOrEntityChanged } from "../common/has-changed";
import "../components/hui-generic-entity-row";
import { createEntityNotFoundWarning } from "../components/hui-warning";
import { EntityConfig, LovelaceRow } from "./types";
@customElement("hui-valve-entity-row")
class HuiValveEntityRow extends LitElement implements LovelaceRow {
@property({ attribute: false }) public hass?: HomeAssistant;
@state() private _config?: EntityConfig;
public setConfig(config: EntityConfig): void {
if (!config) {
throw new Error("Invalid configuration");
}
this._config = config;
}
protected shouldUpdate(changedProps: PropertyValues): boolean {
return hasConfigOrEntityChanged(this, changedProps);
}
protected render() {
if (!this._config || !this.hass) {
return nothing;
}
const stateObj = this.hass.states[this._config.entity] as ValveEntity;
if (!stateObj) {
return html`
<hui-warning>
${createEntityNotFoundWarning(this.hass, this._config.entity)}
</hui-warning>
`;
}
return html`
<hui-generic-entity-row .hass=${this.hass} .config=${this._config}>
<ha-valve-controls
.hass=${this.hass}
.stateObj=${stateObj}
></ha-valve-controls>
</hui-generic-entity-row>
`;
}
static get styles(): CSSResultGroup {
return css`
ha-valve-controls {
margin-right: -0.57em;
}
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"hui-valve-entity-row": HuiValveEntityRow;
}
}

View File

@ -174,6 +174,7 @@ const mainStyles = css`
--state-switch-active-color: var(--amber-color);
--state-update-active-color: var(--orange-color);
--state-vacuum-active-color: var(--teal-color);
--state-valve-active-color: var(--blue-color);
--state-sensor-battery-high-color: var(--green-color);
--state-sensor-battery-low-color: var(--red-color);
--state-sensor-battery-medium-color: var(--orange-color);

View File

@ -0,0 +1,145 @@
import { mdiStop, mdiValveClosed, mdiValveOpen } from "@mdi/js";
import {
CSSResultGroup,
LitElement,
TemplateResult,
css,
html,
nothing,
} from "lit";
import { customElement, property } from "lit/decorators";
import { repeat } from "lit/directives/repeat";
import memoizeOne from "memoize-one";
import { supportsFeature } from "../../common/entity/supports-feature";
import "../../components/ha-control-button";
import "../../components/ha-control-button-group";
import "../../components/ha-control-slider";
import "../../components/ha-svg-icon";
import {
ValveEntity,
ValveEntityFeature,
canClose,
canOpen,
canStop,
} from "../../data/valve";
import { HomeAssistant } from "../../types";
type ValveButton = "open" | "close" | "stop" | "none";
export const getValveButtons = memoizeOne(
(stateObj: ValveEntity): ValveButton[] => {
const supportsOpen = supportsFeature(stateObj, ValveEntityFeature.OPEN);
const supportsClose = supportsFeature(stateObj, ValveEntityFeature.CLOSE);
const supportsStop = supportsFeature(stateObj, ValveEntityFeature.STOP);
const buttons: ValveButton[] = [];
if (supportsOpen) buttons.push("open");
if (supportsStop) buttons.push("stop");
if (supportsClose) buttons.push("close");
return buttons;
}
);
@customElement("ha-state-control-valve-buttons")
export class HaStateControlValveButtons extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public stateObj!: ValveEntity;
private _onOpenTap(ev): void {
ev.stopPropagation();
this.hass!.callService("valve", "open_valve", {
entity_id: this.stateObj!.entity_id,
});
}
private _onCloseTap(ev): void {
ev.stopPropagation();
this.hass!.callService("valve", "close_valve", {
entity_id: this.stateObj!.entity_id,
});
}
private _onStopTap(ev): void {
ev.stopPropagation();
this.hass!.callService("valve", "stop_valve", {
entity_id: this.stateObj!.entity_id,
});
}
protected renderButton(button: ValveButton | undefined) {
if (button === "open") {
return html`
<ha-control-button
.label=${this.hass.localize("ui.card.valve.open_valve")}
@click=${this._onOpenTap}
.disabled=${!canOpen(this.stateObj)}
data-button="open"
>
<ha-svg-icon .path=${mdiValveOpen}></ha-svg-icon>
</ha-control-button>
`;
}
if (button === "close") {
return html`
<ha-control-button
.label=${this.hass.localize("ui.card.valve.close_valve")}
@click=${this._onCloseTap}
.disabled=${!canClose(this.stateObj)}
data-button="close"
>
<ha-svg-icon .path=${mdiValveClosed}></ha-svg-icon>
</ha-control-button>
`;
}
if (button === "stop") {
return html`
<ha-control-button
.label=${this.hass.localize("ui.card.valve.stop_valve")}
@click=${this._onStopTap}
.disabled=${!canStop(this.stateObj)}
data-button="stop"
>
<ha-svg-icon .path=${mdiStop}></ha-svg-icon>
</ha-control-button>
`;
}
return nothing;
}
protected render(): TemplateResult {
const buttons = getValveButtons(this.stateObj);
return html`
<ha-control-button-group vertical>
${repeat(
buttons,
(button) => button,
(button) => this.renderButton(button)
)}
</ha-control-button-group>
`;
}
static get styles(): CSSResultGroup {
return css`
ha-control-button-group {
height: 45vh;
max-height: 320px;
min-height: 200px;
--control-button-group-spacing: 6px;
--control-button-group-thickness: 100px;
}
ha-control-button {
--control-button-border-radius: 18px;
--mdc-icon-size: 24px;
}
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-state-control-valve-buttons": HaStateControlValveButtons;
}
}

View File

@ -0,0 +1,88 @@
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property, state } from "lit/decorators";
import { styleMap } from "lit/directives/style-map";
import { computeAttributeNameDisplay } from "../../common/entity/compute_attribute_display";
import { stateColorCss } from "../../common/entity/state_color";
import "../../components/ha-control-slider";
import { CoverEntity } from "../../data/cover";
import { UNAVAILABLE } from "../../data/entity";
import { DOMAIN_ATTRIBUTES_UNITS } from "../../data/entity_attributes";
import { HomeAssistant } from "../../types";
@customElement("ha-state-control-valve-position")
export class HaStateControlValvePosition extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public stateObj!: CoverEntity;
@state() value?: number;
protected updated(changedProp: Map<string | number | symbol, unknown>): void {
if (changedProp.has("stateObj")) {
const currentPosition = this.stateObj?.attributes.current_position;
this.value =
currentPosition != null ? Math.round(currentPosition) : undefined;
}
}
private _valueChanged(ev: CustomEvent) {
const value = (ev.detail as any).value;
if (isNaN(value)) return;
this.hass.callService("valve", "set_valve_position", {
entity_id: this.stateObj!.entity_id,
position: value,
});
}
protected render(): TemplateResult {
const color = stateColorCss(this.stateObj);
return html`
<ha-control-slider
vertical
.value=${this.value}
min="0"
max="100"
show-handle
@value-changed=${this._valueChanged}
.ariaLabel=${computeAttributeNameDisplay(
this.hass.localize,
this.stateObj,
this.hass.entities,
"current_position"
)}
style=${styleMap({
"--control-slider-color": color,
"--control-slider-background": color,
})}
.disabled=${this.stateObj.state === UNAVAILABLE}
.unit=${DOMAIN_ATTRIBUTES_UNITS.valve.current_position}
.locale=${this.hass.locale}
>
</ha-control-slider>
`;
}
static get styles(): CSSResultGroup {
return css`
ha-control-slider {
height: 45vh;
max-height: 320px;
min-height: 200px;
--control-slider-thickness: 100px;
--control-slider-border-radius: 24px;
--control-slider-color: var(--primary-color);
--control-slider-background: var(--disabled-color);
--control-slider-background-opacity: 0.2;
--control-slider-tooltip-font-size: 20px;
}
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-state-control-valve-position": HaStateControlValvePosition;
}
}

View File

@ -0,0 +1,167 @@
import { HassEntity } from "home-assistant-js-websocket";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import { styleMap } from "lit/directives/style-map";
import { domainIcon } from "../../common/entity/domain_icon";
import { stateColorCss } from "../../common/entity/state_color";
import "../../components/ha-control-button";
import "../../components/ha-control-switch";
import { UNAVAILABLE, UNKNOWN } from "../../data/entity";
import { forwardHaptic } from "../../data/haptics";
import { HomeAssistant } from "../../types";
@customElement("ha-state-control-valve-toggle")
export class HaStateControlValveToggle extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public stateObj!: HassEntity;
private _valueChanged(ev) {
const checked = ev.target.checked as boolean;
if (checked) {
this._turnOn();
} else {
this._turnOff();
}
}
private _turnOn() {
this._callService(true);
}
private _turnOff() {
this._callService(false);
}
private async _callService(turnOn): Promise<void> {
if (!this.hass || !this.stateObj) {
return;
}
forwardHaptic("light");
await this.hass.callService(
"valve",
turnOn ? "open_valve" : "close_valve",
{
entity_id: this.stateObj.entity_id,
}
);
}
protected render(): TemplateResult {
const onColor = stateColorCss(this.stateObj, "open");
const offColor = stateColorCss(this.stateObj, "closed");
const isOn =
this.stateObj.state === "open" ||
this.stateObj.state === "closing" ||
this.stateObj.state === "opening";
const isOff = this.stateObj.state === "closed";
if (
this.stateObj.attributes.assumed_state ||
this.stateObj.state === UNKNOWN
) {
return html`
<div class="buttons">
<ha-control-button
.label=${this.hass.localize("ui.card.valve.open_valve")}
@click=${this._turnOn}
.disabled=${this.stateObj.state === UNAVAILABLE}
class=${classMap({
active: isOn,
})}
style=${styleMap({
"--color": onColor,
})}
>
<ha-svg-icon
.path=${domainIcon("valve", this.stateObj, "open")}
></ha-svg-icon>
</ha-control-button>
<ha-control-button
.label=${this.hass.localize("ui.card.valve.close_valve")}
@click=${this._turnOff}
.disabled=${this.stateObj.state === UNAVAILABLE}
class=${classMap({
active: isOff,
})}
style=${styleMap({
"--color": offColor,
})}
>
<ha-svg-icon
.path=${domainIcon("valve", this.stateObj, "closed")}
></ha-svg-icon>
</ha-control-button>
</div>
`;
}
return html`
<ha-control-switch
.pathOn=${domainIcon("valve", this.stateObj, "open")}
.pathOff=${domainIcon("valve", this.stateObj, "closed")}
vertical
reversed
.checked=${isOn}
@change=${this._valueChanged}
.ariaLabel=${isOn
? this.hass.localize("ui.card.valve.close_valve")
: this.hass.localize("ui.card.valve.open_valve")}
style=${styleMap({
"--control-switch-on-color": onColor,
"--control-switch-off-color": offColor,
})}
.disabled=${this.stateObj.state === UNAVAILABLE}
>
</ha-control-switch>
`;
}
static get styles(): CSSResultGroup {
return css`
ha-control-switch {
height: 45vh;
max-height: 320px;
min-height: 200px;
--control-switch-thickness: 100px;
--control-switch-border-radius: 24px;
--control-switch-padding: 6px;
--mdc-icon-size: 24px;
}
.buttons {
display: flex;
flex-direction: column;
width: 100px;
height: 45vh;
max-height: 320px;
min-height: 200px;
padding: 6px;
box-sizing: border-box;
}
ha-control-button {
flex: 1;
width: 100%;
--control-button-border-radius: 18px;
--mdc-icon-size: 24px;
}
ha-control-button.active {
--control-button-icon-color: white;
--control-button-background-color: var(--color);
--control-button-background-opacity: 1;
}
ha-control-button:not(:last-child) {
margin-bottom: 6px;
}
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-state-control-valve-toggle": HaStateControlValveToggle;
}
}

View File

@ -255,6 +255,11 @@
"turn_off": "Turn off"
}
},
"valve": {
"open_valve": "Open valve",
"close_valve": "Close valve",
"stop_valve": "Stop valve"
},
"water_heater": {
"currently": "[%key:ui::card::climate::currently%]",
"on_off": "On / off",
@ -1094,6 +1099,12 @@
"start_mowing": "Start mowing",
"pause": "Pause",
"dock": "Return to dock"
},
"valve": {
"switch_mode": {
"button": "[%key:ui::dialogs::more_info_control::valve::switch_mode::button%]",
"position": "[%key:ui::dialogs::more_info_control::valve::switch_mode::position%]"
}
}
},
"entity_registry": {