mirror of
https://github.com/home-assistant/frontend.git
synced 2025-07-24 09:46:36 +00:00
Update target humidity control for climate more info (#17531)
This commit is contained in:
parent
782e41dcda
commit
ade430f326
@ -0,0 +1,331 @@
|
||||
import { mdiMinus, mdiPlus } from "@mdi/js";
|
||||
import { CSSResultGroup, LitElement, PropertyValues, css, html } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { styleMap } from "lit/directives/style-map";
|
||||
import { stateActive } from "../../../../common/entity/state_active";
|
||||
import { domainStateColorProperties } from "../../../../common/entity/state_color";
|
||||
import { supportsFeature } from "../../../../common/entity/supports-feature";
|
||||
import { clamp } from "../../../../common/number/clamp";
|
||||
import { formatNumber } from "../../../../common/number/format_number";
|
||||
import { blankBeforePercent } from "../../../../common/translations/blank_before_percent";
|
||||
import { debounce } from "../../../../common/util/debounce";
|
||||
import "../../../../components/ha-control-circular-slider";
|
||||
import "../../../../components/ha-outlined-icon-button";
|
||||
import "../../../../components/ha-svg-icon";
|
||||
import { ClimateEntity, ClimateEntityFeature } from "../../../../data/climate";
|
||||
import { UNAVAILABLE } from "../../../../data/entity";
|
||||
import { computeCssVariable } from "../../../../resources/css-variables";
|
||||
import { HomeAssistant } from "../../../../types";
|
||||
|
||||
@customElement("ha-more-info-climate-humidity")
|
||||
export class HaMoreInfoClimateHumidity extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public stateObj!: ClimateEntity;
|
||||
|
||||
@state() private _targetHumidity?: number;
|
||||
|
||||
protected willUpdate(changedProp: PropertyValues): void {
|
||||
super.willUpdate(changedProp);
|
||||
if (changedProp.has("stateObj")) {
|
||||
this._targetHumidity = this.stateObj.attributes.humidity;
|
||||
}
|
||||
}
|
||||
|
||||
private get _step() {
|
||||
return 1;
|
||||
}
|
||||
|
||||
private get _min() {
|
||||
return this.stateObj.attributes.min_humidity ?? 0;
|
||||
}
|
||||
|
||||
private get _max() {
|
||||
return this.stateObj.attributes.max_humidity ?? 100;
|
||||
}
|
||||
|
||||
private _valueChanged(ev: CustomEvent) {
|
||||
const value = (ev.detail as any).value;
|
||||
if (isNaN(value)) return;
|
||||
this._targetHumidity = value;
|
||||
this._callService();
|
||||
}
|
||||
|
||||
private _valueChanging(ev: CustomEvent) {
|
||||
const value = (ev.detail as any).value;
|
||||
if (isNaN(value)) return;
|
||||
this._targetHumidity = value;
|
||||
}
|
||||
|
||||
private _debouncedCallService = debounce(() => this._callService(), 1000);
|
||||
|
||||
private _callService() {
|
||||
this.hass.callService("climate", "set_humidity", {
|
||||
entity_id: this.stateObj!.entity_id,
|
||||
humidity: this._targetHumidity,
|
||||
});
|
||||
}
|
||||
|
||||
private _handleButton(ev) {
|
||||
const step = ev.currentTarget.step as number;
|
||||
|
||||
let humidity = this._targetHumidity ?? this._min;
|
||||
humidity += step;
|
||||
humidity = clamp(humidity, this._min, this._max);
|
||||
|
||||
this._targetHumidity = humidity;
|
||||
this._debouncedCallService();
|
||||
}
|
||||
|
||||
private _renderLabel() {
|
||||
return html`
|
||||
<p class="action">
|
||||
${this.hass.localize(
|
||||
"ui.dialogs.more_info_control.climate.humidity_target"
|
||||
)}
|
||||
</p>
|
||||
`;
|
||||
}
|
||||
|
||||
private _renderButtons() {
|
||||
return html`
|
||||
<div class="buttons">
|
||||
<ha-outlined-icon-button
|
||||
.step=${-this._step}
|
||||
@click=${this._handleButton}
|
||||
>
|
||||
<ha-svg-icon .path=${mdiMinus}></ha-svg-icon>
|
||||
</ha-outlined-icon-button>
|
||||
<ha-outlined-icon-button
|
||||
.step=${this._step}
|
||||
@click=${this._handleButton}
|
||||
>
|
||||
<ha-svg-icon .path=${mdiPlus}></ha-svg-icon>
|
||||
</ha-outlined-icon-button>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private _renderTarget(humidity: number) {
|
||||
const formatted = formatNumber(humidity, this.hass.locale, {
|
||||
maximumFractionDigits: 0,
|
||||
});
|
||||
|
||||
return html`
|
||||
<div class="target">
|
||||
<p class="value" aria-hidden="true">
|
||||
${formatted}<span class="unit">%</span>
|
||||
</p>
|
||||
<p class="visually-hidden">
|
||||
${formatted}${blankBeforePercent(this.hass.locale)}%
|
||||
</p>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
protected render() {
|
||||
const supportsTargetHumidity = supportsFeature(
|
||||
this.stateObj,
|
||||
ClimateEntityFeature.TARGET_HUMIDITY
|
||||
);
|
||||
const active = stateActive(this.stateObj);
|
||||
|
||||
// Use humidifier state color
|
||||
const mainColor = computeCssVariable(
|
||||
domainStateColorProperties(
|
||||
"humidifier",
|
||||
this.stateObj,
|
||||
active ? "on" : "off"
|
||||
)
|
||||
);
|
||||
|
||||
const targetHumidity = this._targetHumidity;
|
||||
const currentHumidity = this.stateObj.attributes.current_humidity;
|
||||
|
||||
if (supportsTargetHumidity && targetHumidity != null) {
|
||||
return html`
|
||||
<div
|
||||
class="container"
|
||||
style=${styleMap({
|
||||
"--main-color": mainColor,
|
||||
})}
|
||||
>
|
||||
<ha-control-circular-slider
|
||||
.value=${this._targetHumidity}
|
||||
.min=${this._min}
|
||||
.max=${this._max}
|
||||
.step=${this._step}
|
||||
.current=${currentHumidity}
|
||||
.disabled=${this.stateObj!.state === UNAVAILABLE}
|
||||
@value-changed=${this._valueChanged}
|
||||
@value-changing=${this._valueChanging}
|
||||
>
|
||||
</ha-control-circular-slider>
|
||||
<div class="info">
|
||||
<div class="action-container">${this._renderLabel()}</div>
|
||||
<div class="target-container">
|
||||
${this._renderTarget(targetHumidity)}
|
||||
</div>
|
||||
</div>
|
||||
${this._renderButtons()}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
return html`
|
||||
<div class="container">
|
||||
<ha-control-circular-slider
|
||||
.current=${this.stateObj.attributes.current_temperature}
|
||||
.min=${this._min}
|
||||
.max=${this._max}
|
||||
.step=${this._step}
|
||||
disabled
|
||||
>
|
||||
</ha-control-circular-slider>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return css`
|
||||
/* Layout */
|
||||
.container {
|
||||
position: relative;
|
||||
}
|
||||
.info {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
pointer-events: none;
|
||||
font-size: 16px;
|
||||
line-height: 24px;
|
||||
letter-spacing: 0.1px;
|
||||
}
|
||||
.info * {
|
||||
margin: 0;
|
||||
pointer-events: auto;
|
||||
}
|
||||
/* Elements */
|
||||
.target-container {
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
.target .value {
|
||||
font-size: 56px;
|
||||
line-height: 1;
|
||||
letter-spacing: -0.25px;
|
||||
}
|
||||
.target .value .unit {
|
||||
font-size: 0.4em;
|
||||
line-height: 1;
|
||||
margin-left: 2px;
|
||||
}
|
||||
.action-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 200px;
|
||||
height: 48px;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
.action {
|
||||
font-weight: 500;
|
||||
text-align: center;
|
||||
color: var(--action-color, inherit);
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
}
|
||||
.dual {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 24px;
|
||||
margin-bottom: 40px;
|
||||
}
|
||||
|
||||
.dual button {
|
||||
outline: none;
|
||||
background: none;
|
||||
color: inherit;
|
||||
font-family: inherit;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
border: none;
|
||||
opacity: 0.5;
|
||||
padding: 0;
|
||||
transition:
|
||||
opacity 180ms ease-in-out,
|
||||
transform 180ms ease-in-out;
|
||||
cursor: pointer;
|
||||
}
|
||||
.dual button:focus-visible {
|
||||
transform: scale(1.1);
|
||||
}
|
||||
.dual button.selected {
|
||||
opacity: 1;
|
||||
}
|
||||
.buttons {
|
||||
position: absolute;
|
||||
bottom: 10px;
|
||||
left: 0;
|
||||
right: 0;
|
||||
margin: 0 auto;
|
||||
width: 120px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
.buttons ha-outlined-icon-button {
|
||||
--md-outlined-icon-button-container-size: 48px;
|
||||
--md-outlined-icon-button-icon-size: 24px;
|
||||
}
|
||||
/* Accessibility */
|
||||
.visually-hidden {
|
||||
position: absolute;
|
||||
overflow: hidden;
|
||||
clip: rect(0 0 0 0);
|
||||
height: 1px;
|
||||
width: 1px;
|
||||
margin: -1px;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
}
|
||||
/* Slider */
|
||||
ha-control-circular-slider {
|
||||
--control-circular-slider-color: var(
|
||||
--main-color,
|
||||
var(--disabled-color)
|
||||
);
|
||||
}
|
||||
ha-control-circular-slider::after {
|
||||
display: block;
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: -10%;
|
||||
left: -10%;
|
||||
right: -10%;
|
||||
bottom: -10%;
|
||||
background: radial-gradient(
|
||||
50% 50% at 50% 50%,
|
||||
var(--action-color, transparent) 0%,
|
||||
transparent 100%
|
||||
);
|
||||
opacity: 0.15;
|
||||
pointer-events: none;
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-more-info-climate-humidity": HaMoreInfoClimateHumidity;
|
||||
}
|
||||
}
|
@ -1,4 +1,5 @@
|
||||
import "@material/mwc-list/mwc-list-item";
|
||||
import { mdiThermometer, mdiWaterPercent } from "@mdi/js";
|
||||
import {
|
||||
CSSResultGroup,
|
||||
LitElement,
|
||||
@ -7,7 +8,7 @@ import {
|
||||
html,
|
||||
nothing,
|
||||
} from "lit";
|
||||
import { property } from "lit/decorators";
|
||||
import { property, state } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import { fireEvent } from "../../../common/dom/fire_event";
|
||||
import { stopPropagation } from "../../../common/dom/stop_propagation";
|
||||
@ -19,24 +20,30 @@ import { computeStateDisplay } from "../../../common/entity/compute_state_displa
|
||||
import { supportsFeature } from "../../../common/entity/supports-feature";
|
||||
import { formatNumber } from "../../../common/number/format_number";
|
||||
import { blankBeforePercent } from "../../../common/translations/blank_before_percent";
|
||||
import { computeRTLDirection } from "../../../common/util/compute_rtl";
|
||||
import "../../../components/ha-icon-button-group";
|
||||
import "../../../components/ha-icon-button-toggle";
|
||||
import "../../../components/ha-select";
|
||||
import "../../../components/ha-slider";
|
||||
import "../../../components/ha-switch";
|
||||
import {
|
||||
ClimateEntity,
|
||||
ClimateEntityFeature,
|
||||
compareClimateHvacModes,
|
||||
} from "../../../data/climate";
|
||||
import { UNAVAILABLE } from "../../../data/entity";
|
||||
import { HomeAssistant } from "../../../types";
|
||||
import "../components/climate/ha-more-info-climate-humidity";
|
||||
import "../components/climate/ha-more-info-climate-temperature";
|
||||
import { moreInfoControlStyle } from "../components/ha-more-info-control-style";
|
||||
|
||||
type MainControl = "temperature" | "humidity";
|
||||
|
||||
class MoreInfoClimate extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property() public stateObj?: ClimateEntity;
|
||||
|
||||
@state() private _mainControl: MainControl = "temperature";
|
||||
|
||||
private _resizeDebounce?: number;
|
||||
|
||||
protected render() {
|
||||
@ -79,56 +86,92 @@ class MoreInfoClimate extends LitElement {
|
||||
const currentTemperature = this.stateObj.attributes.current_temperature;
|
||||
const currentHumidity = this.stateObj.attributes.current_humidity;
|
||||
|
||||
const rtlDirection = computeRTLDirection(hass);
|
||||
|
||||
return html`
|
||||
${currentTemperature || currentHumidity
|
||||
? html`<div class="current">
|
||||
${currentTemperature != null
|
||||
? html`
|
||||
<div>
|
||||
<p class="label">
|
||||
${computeAttributeNameDisplay(
|
||||
this.hass.localize,
|
||||
this.stateObj,
|
||||
this.hass.entities,
|
||||
"current_temperature"
|
||||
)}
|
||||
</p>
|
||||
<p class="value">
|
||||
${formatNumber(currentTemperature, this.hass.locale)}
|
||||
${this.hass.config.unit_system.temperature}
|
||||
</p>
|
||||
</div>
|
||||
`
|
||||
: nothing}
|
||||
${currentHumidity != null
|
||||
? html`
|
||||
<div>
|
||||
<p class="label">
|
||||
${computeAttributeNameDisplay(
|
||||
this.hass.localize,
|
||||
this.stateObj,
|
||||
this.hass.entities,
|
||||
"current_humidity"
|
||||
)}
|
||||
</p>
|
||||
<p class="value">
|
||||
${formatNumber(
|
||||
currentHumidity,
|
||||
this.hass.locale
|
||||
)}${blankBeforePercent(this.hass.locale)}%
|
||||
</p>
|
||||
</div>
|
||||
`
|
||||
: nothing}
|
||||
</div>`
|
||||
: nothing}
|
||||
<div class="current">
|
||||
${currentTemperature != null
|
||||
? html`
|
||||
<div>
|
||||
<p class="label">
|
||||
${computeAttributeNameDisplay(
|
||||
this.hass.localize,
|
||||
this.stateObj,
|
||||
this.hass.entities,
|
||||
"current_temperature"
|
||||
)}
|
||||
</p>
|
||||
<p class="value">
|
||||
${formatNumber(currentTemperature, this.hass.locale)}
|
||||
${this.hass.config.unit_system.temperature}
|
||||
</p>
|
||||
</div>
|
||||
`
|
||||
: nothing}
|
||||
${currentHumidity != null
|
||||
? html`
|
||||
<div>
|
||||
<p class="label">
|
||||
${computeAttributeNameDisplay(
|
||||
this.hass.localize,
|
||||
this.stateObj,
|
||||
this.hass.entities,
|
||||
"current_humidity"
|
||||
)}
|
||||
</p>
|
||||
<p class="value">
|
||||
${formatNumber(
|
||||
currentHumidity,
|
||||
this.hass.locale
|
||||
)}${blankBeforePercent(this.hass.locale)}%
|
||||
</p>
|
||||
</div>
|
||||
`
|
||||
: nothing}
|
||||
</div>
|
||||
<div class="controls">
|
||||
<ha-more-info-climate-temperature
|
||||
.hass=${this.hass}
|
||||
.stateObj=${this.stateObj}
|
||||
></ha-more-info-climate-temperature>
|
||||
${this._mainControl === "temperature"
|
||||
? html`
|
||||
<ha-more-info-climate-temperature
|
||||
.hass=${this.hass}
|
||||
.stateObj=${this.stateObj}
|
||||
></ha-more-info-climate-temperature>
|
||||
`
|
||||
: nothing}
|
||||
${this._mainControl === "humidity"
|
||||
? html`
|
||||
<ha-more-info-climate-humidity
|
||||
.hass=${this.hass}
|
||||
.stateObj=${this.stateObj}
|
||||
></ha-more-info-climate-humidity>
|
||||
`
|
||||
: nothing}
|
||||
${supportTargetHumidity
|
||||
? html`
|
||||
<ha-icon-button-group>
|
||||
<ha-icon-button-toggle
|
||||
.selected=${this._mainControl === "temperature"}
|
||||
.disabled=${this.stateObj!.state === UNAVAILABLE}
|
||||
.label=${this.hass.localize(
|
||||
"ui.dialogs.more_info_control.light.color"
|
||||
)}
|
||||
.control=${"temperature"}
|
||||
@click=${this._setMainControl}
|
||||
>
|
||||
<ha-svg-icon .path=${mdiThermometer}></ha-svg-icon>
|
||||
</ha-icon-button-toggle>
|
||||
<ha-icon-button-toggle
|
||||
.selected=${this._mainControl === "humidity"}
|
||||
.disabled=${this.stateObj!.state === UNAVAILABLE}
|
||||
.label=${this.hass.localize(
|
||||
"ui.dialogs.more_info_control.light.color_temp"
|
||||
)}
|
||||
.control=${"humidity"}
|
||||
@click=${this._setMainControl}
|
||||
>
|
||||
<ha-svg-icon .path=${mdiWaterPercent}></ha-svg-icon>
|
||||
</ha-icon-button-toggle>
|
||||
</ha-icon-button-group>
|
||||
`
|
||||
: nothing}
|
||||
</div>
|
||||
<div
|
||||
class=${classMap({
|
||||
@ -144,37 +187,6 @@ class MoreInfoClimate extends LitElement {
|
||||
"has-preset_mode": supportPresetMode,
|
||||
})}
|
||||
>
|
||||
${supportTargetHumidity
|
||||
? html`
|
||||
<div class="container-humidity">
|
||||
<div>
|
||||
${computeAttributeNameDisplay(
|
||||
hass.localize,
|
||||
stateObj,
|
||||
hass.entities,
|
||||
"humidity"
|
||||
)}
|
||||
</div>
|
||||
<div class="single-row">
|
||||
<div class="target-humidity">
|
||||
${stateObj.attributes.humidity} %
|
||||
</div>
|
||||
<ha-slider
|
||||
step="1"
|
||||
pin
|
||||
ignore-bar-touch
|
||||
dir=${rtlDirection}
|
||||
.min=${stateObj.attributes.min_humidity}
|
||||
.max=${stateObj.attributes.max_humidity}
|
||||
.value=${stateObj.attributes.humidity}
|
||||
@change=${this._targetHumiditySliderChanged}
|
||||
>
|
||||
</ha-slider>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
: ""}
|
||||
|
||||
<div class="container-hvac_modes">
|
||||
<ha-select
|
||||
.label=${hass.localize("ui.card.climate.operation")}
|
||||
@ -348,14 +360,9 @@ class MoreInfoClimate extends LitElement {
|
||||
}, 500);
|
||||
}
|
||||
|
||||
private _targetHumiditySliderChanged(ev) {
|
||||
const newVal = ev.target.value;
|
||||
this._callServiceHelper(
|
||||
this.stateObj!.attributes.humidity,
|
||||
newVal,
|
||||
"set_humidity",
|
||||
{ humidity: newVal }
|
||||
);
|
||||
private _setMainControl(ev: any) {
|
||||
ev.stopPropagation();
|
||||
this._mainControl = ev.currentTarget.control;
|
||||
}
|
||||
|
||||
private _auxToggleChanged(ev) {
|
||||
@ -495,10 +502,6 @@ class MoreInfoClimate extends LitElement {
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
ha-slider {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.container-humidity .single-row {
|
||||
display: flex;
|
||||
height: 50px;
|
||||
|
@ -28,7 +28,6 @@ import "../../../components/ha-button-menu";
|
||||
import "../../../components/ha-icon-button-group";
|
||||
import "../../../components/ha-icon-button-toggle";
|
||||
import "../../../components/ha-outlined-button";
|
||||
import "../../../components/ha-outlined-icon-button";
|
||||
import "../../../components/ha-select";
|
||||
import { UNAVAILABLE } from "../../../data/entity";
|
||||
import { ExtEntityRegistryEntry } from "../../../data/entity_registry";
|
||||
|
@ -999,7 +999,8 @@
|
||||
},
|
||||
"climate": {
|
||||
"target_label": "{action} to target",
|
||||
"target": "Target"
|
||||
"target": "Target",
|
||||
"humidity_target": "Humidity target"
|
||||
},
|
||||
"humidifier": {
|
||||
"target_label": "[%key:ui::dialogs::more_info_control::climate::target_label%]",
|
||||
|
Loading…
x
Reference in New Issue
Block a user