Update target humidity control for climate more info (#17531)

This commit is contained in:
Paul Bottein 2023-08-09 14:35:00 +02:00 committed by GitHub
parent 782e41dcda
commit ade430f326
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 430 additions and 96 deletions

View File

@ -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;
}
}

View File

@ -1,4 +1,5 @@
import "@material/mwc-list/mwc-list-item"; import "@material/mwc-list/mwc-list-item";
import { mdiThermometer, mdiWaterPercent } from "@mdi/js";
import { import {
CSSResultGroup, CSSResultGroup,
LitElement, LitElement,
@ -7,7 +8,7 @@ import {
html, html,
nothing, nothing,
} from "lit"; } from "lit";
import { property } from "lit/decorators"; import { property, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map"; import { classMap } from "lit/directives/class-map";
import { fireEvent } from "../../../common/dom/fire_event"; import { fireEvent } from "../../../common/dom/fire_event";
import { stopPropagation } from "../../../common/dom/stop_propagation"; 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 { supportsFeature } from "../../../common/entity/supports-feature";
import { formatNumber } from "../../../common/number/format_number"; import { formatNumber } from "../../../common/number/format_number";
import { blankBeforePercent } from "../../../common/translations/blank_before_percent"; 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-select";
import "../../../components/ha-slider";
import "../../../components/ha-switch"; import "../../../components/ha-switch";
import { import {
ClimateEntity, ClimateEntity,
ClimateEntityFeature, ClimateEntityFeature,
compareClimateHvacModes, compareClimateHvacModes,
} from "../../../data/climate"; } from "../../../data/climate";
import { UNAVAILABLE } from "../../../data/entity";
import { HomeAssistant } from "../../../types"; import { HomeAssistant } from "../../../types";
import "../components/climate/ha-more-info-climate-humidity";
import "../components/climate/ha-more-info-climate-temperature"; import "../components/climate/ha-more-info-climate-temperature";
import { moreInfoControlStyle } from "../components/ha-more-info-control-style"; import { moreInfoControlStyle } from "../components/ha-more-info-control-style";
type MainControl = "temperature" | "humidity";
class MoreInfoClimate extends LitElement { class MoreInfoClimate extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant; @property({ attribute: false }) public hass!: HomeAssistant;
@property() public stateObj?: ClimateEntity; @property() public stateObj?: ClimateEntity;
@state() private _mainControl: MainControl = "temperature";
private _resizeDebounce?: number; private _resizeDebounce?: number;
protected render() { protected render() {
@ -79,56 +86,92 @@ class MoreInfoClimate extends LitElement {
const currentTemperature = this.stateObj.attributes.current_temperature; const currentTemperature = this.stateObj.attributes.current_temperature;
const currentHumidity = this.stateObj.attributes.current_humidity; const currentHumidity = this.stateObj.attributes.current_humidity;
const rtlDirection = computeRTLDirection(hass);
return html` return html`
${currentTemperature || currentHumidity <div class="current">
? html`<div class="current"> ${currentTemperature != null
${currentTemperature != null ? html`
? html` <div>
<div> <p class="label">
<p class="label"> ${computeAttributeNameDisplay(
${computeAttributeNameDisplay( this.hass.localize,
this.hass.localize, this.stateObj,
this.stateObj, this.hass.entities,
this.hass.entities, "current_temperature"
"current_temperature" )}
)} </p>
</p> <p class="value">
<p class="value"> ${formatNumber(currentTemperature, this.hass.locale)}
${formatNumber(currentTemperature, this.hass.locale)} ${this.hass.config.unit_system.temperature}
${this.hass.config.unit_system.temperature} </p>
</p> </div>
</div> `
` : nothing}
: nothing} ${currentHumidity != null
${currentHumidity != null ? html`
? html` <div>
<div> <p class="label">
<p class="label"> ${computeAttributeNameDisplay(
${computeAttributeNameDisplay( this.hass.localize,
this.hass.localize, this.stateObj,
this.stateObj, this.hass.entities,
this.hass.entities, "current_humidity"
"current_humidity" )}
)} </p>
</p> <p class="value">
<p class="value"> ${formatNumber(
${formatNumber( currentHumidity,
currentHumidity, this.hass.locale
this.hass.locale )}${blankBeforePercent(this.hass.locale)}%
)}${blankBeforePercent(this.hass.locale)}% </p>
</p> </div>
</div> `
` : nothing}
: nothing} </div>
</div>`
: nothing}
<div class="controls"> <div class="controls">
<ha-more-info-climate-temperature ${this._mainControl === "temperature"
.hass=${this.hass} ? html`
.stateObj=${this.stateObj} <ha-more-info-climate-temperature
></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>
<div <div
class=${classMap({ class=${classMap({
@ -144,37 +187,6 @@ class MoreInfoClimate extends LitElement {
"has-preset_mode": supportPresetMode, "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"> <div class="container-hvac_modes">
<ha-select <ha-select
.label=${hass.localize("ui.card.climate.operation")} .label=${hass.localize("ui.card.climate.operation")}
@ -348,14 +360,9 @@ class MoreInfoClimate extends LitElement {
}, 500); }, 500);
} }
private _targetHumiditySliderChanged(ev) { private _setMainControl(ev: any) {
const newVal = ev.target.value; ev.stopPropagation();
this._callServiceHelper( this._mainControl = ev.currentTarget.control;
this.stateObj!.attributes.humidity,
newVal,
"set_humidity",
{ humidity: newVal }
);
} }
private _auxToggleChanged(ev) { private _auxToggleChanged(ev) {
@ -495,10 +502,6 @@ class MoreInfoClimate extends LitElement {
margin-top: 8px; margin-top: 8px;
} }
ha-slider {
width: 100%;
}
.container-humidity .single-row { .container-humidity .single-row {
display: flex; display: flex;
height: 50px; height: 50px;

View File

@ -28,7 +28,6 @@ import "../../../components/ha-button-menu";
import "../../../components/ha-icon-button-group"; import "../../../components/ha-icon-button-group";
import "../../../components/ha-icon-button-toggle"; import "../../../components/ha-icon-button-toggle";
import "../../../components/ha-outlined-button"; import "../../../components/ha-outlined-button";
import "../../../components/ha-outlined-icon-button";
import "../../../components/ha-select"; import "../../../components/ha-select";
import { UNAVAILABLE } from "../../../data/entity"; import { UNAVAILABLE } from "../../../data/entity";
import { ExtEntityRegistryEntry } from "../../../data/entity_registry"; import { ExtEntityRegistryEntry } from "../../../data/entity_registry";

View File

@ -999,7 +999,8 @@
}, },
"climate": { "climate": {
"target_label": "{action} to target", "target_label": "{action} to target",
"target": "Target" "target": "Target",
"humidity_target": "Humidity target"
}, },
"humidifier": { "humidifier": {
"target_label": "[%key:ui::dialogs::more_info_control::climate::target_label%]", "target_label": "[%key:ui::dialogs::more_info_control::climate::target_label%]",