Add new humidity control in humidifier more info (#17011)

* Add new humidity control in humidifier more info

* Fix humidifier card

* Some adjustments

* Add current label

* Some adjustments

* Clean code

* Remove unused code

* Fix merge

* Add current to main screen

* Remove toggle

* Add action

* Update buttons

* Add gallery

* Some fixes

* Add overflow

* Update src/dialogs/more-info/components/humidifier/ha-more-info-humidifier-humidity.ts

* Update src/dialogs/more-info/controls/more-info-humidifier.ts

Co-authored-by: Bram Kragten <mail@bramkragten.nl>

* Use climate translation

---------

Co-authored-by: Bram Kragten <mail@bramkragten.nl>
This commit is contained in:
Paul Bottein 2023-08-08 13:07:46 +02:00 committed by GitHub
parent 89e96e4681
commit 4986b013a2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 605 additions and 125 deletions

View File

@ -0,0 +1,3 @@
---
title: Humidifier
---

View File

@ -0,0 +1,57 @@
import { html, LitElement, PropertyValues, TemplateResult } from "lit";
import { customElement, property, query } from "lit/decorators";
import "../../../../src/components/ha-card";
import "../../../../src/dialogs/more-info/more-info-content";
import { getEntity } from "../../../../src/fake_data/entity";
import {
MockHomeAssistant,
provideHass,
} from "../../../../src/fake_data/provide_hass";
import "../../components/demo-more-infos";
const ENTITIES = [
getEntity("humidifier", "humidifier", "on", {
friendly_name: "Humidifier",
device_class: "humidifier",
current_humidity: 50,
humidity: 30,
}),
getEntity("humidifier", "dehumidifier", "on", {
friendly_name: "Dehumidifier",
device_class: "dehumidifier",
current_humidity: 50,
humidity: 30,
}),
getEntity("humidifier", "unavailable", "unavailable", {
friendly_name: "Unavailable humidifier",
}),
];
@customElement("demo-more-info-humidifier")
class DemoMoreInfoHumidifier extends LitElement {
@property() public hass!: MockHomeAssistant;
@query("demo-more-infos") private _demoRoot!: HTMLElement;
protected render(): TemplateResult {
return html`
<demo-more-infos
.hass=${this.hass}
.entities=${ENTITIES.map((ent) => ent.entityId)}
></demo-more-infos>
`;
}
protected firstUpdated(changedProperties: PropertyValues) {
super.firstUpdated(changedProperties);
const hass = provideHass(this._demoRoot);
hass.updateTranslations(null, "en");
hass.addEntities(ENTITIES);
}
}
declare global {
interface HTMLElementTagNameMap {
"demo-more-info-humidifier": DemoMoreInfoHumidifier;
}
}

View File

@ -1,9 +1,25 @@
import {
mdiAccountArrowRight,
mdiAirHumidifier,
mdiArrowDownBold,
mdiArrowUpBold,
mdiBabyCarriage,
mdiClockOutline,
mdiHome,
mdiLeaf,
mdiPower,
mdiPowerSleep,
mdiRefreshAuto,
mdiRocketLaunch,
mdiSofa,
mdiWaterPercent,
} from "@mdi/js";
import {
HassEntityAttributeBase,
HassEntityBase,
} from "home-assistant-js-websocket";
export type HumidifierState = "on" | "off";
export type HumidifierState = "off" | "on";
export type HumidifierAction = "off" | "idle" | "humidifying" | "drying";
@ -19,7 +35,52 @@ export type HumidifierEntity = HassEntityBase & {
};
};
export const HUMIDIFIER_SUPPORT_MODES = 1;
export const enum HumidifierEntityFeature {
MODES = 1,
}
export const HUMIDIFIER_DEVICE_CLASS_HUMIDIFIER = "humidifier";
export const HUMIDIFIER_DEVICE_CLASS_DEHUMIDIFIER = "dehumidifier";
export const enum HumidifierEntityDeviceClass {
HUMIDIFIER = "humidifier",
DEHUMIDIFIER = "dehumidifier",
}
type HumidifierBuiltInMode =
| "normal"
| "eco"
| "away"
| "boost"
| "comfort"
| "home"
| "sleep"
| "auto"
| "baby";
export const HUMIDIFIER_MODE_ICONS: Record<HumidifierBuiltInMode, string> = {
auto: mdiRefreshAuto,
away: mdiAccountArrowRight,
baby: mdiBabyCarriage,
boost: mdiRocketLaunch,
comfort: mdiSofa,
eco: mdiLeaf,
home: mdiHome,
normal: mdiWaterPercent,
sleep: mdiPowerSleep,
};
export const computeHumidiferModeIcon = (mode?: string) =>
HUMIDIFIER_MODE_ICONS[mode as HumidifierBuiltInMode] ?? mdiAirHumidifier;
export const HUMIDIFIER_ACTION_ICONS: Record<HumidifierAction, string> = {
drying: mdiArrowDownBold,
humidifying: mdiArrowUpBold,
idle: mdiClockOutline,
off: mdiPower,
};
export const HUMIDIFIER_ACTION_MODE: Record<HumidifierAction, HumidifierState> =
{
drying: "on",
humidifying: "on",
idle: "off",
off: "off",
};

View File

@ -0,0 +1,334 @@
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 { computeAttributeValueDisplay } from "../../../../common/entity/compute_attribute_display";
import { stateActive } from "../../../../common/entity/state_active";
import { stateColorCss } from "../../../../common/entity/state_color";
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 { UNAVAILABLE } from "../../../../data/entity";
import {
HUMIDIFIER_ACTION_MODE,
HumidifierEntity,
HumidifierEntityDeviceClass,
} from "../../../../data/humidifier";
import { HomeAssistant } from "../../../../types";
@customElement("ha-more-info-humidifier-humidity")
export class HaMoreInfoHumidifierHumidity extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public stateObj!: HumidifierEntity;
@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("humidifier", "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 _renderAction() {
const action = this.stateObj.attributes.action;
const actionLabel = computeAttributeValueDisplay(
this.hass.localize,
this.stateObj,
this.hass.locale,
this.hass.config,
this.hass.entities,
"action"
) as string;
return html`
<p class="action">
${action && ["drying", "humidifying"].includes(action)
? this.hass.localize(
"ui.dialogs.more_info_control.humidifier.target_label",
{ action: actionLabel }
)
: action && action !== "off" && action !== "idle"
? actionLabel
: this.hass.localize(
"ui.dialogs.more_info_control.humidifier.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 mainColor = stateColorCss(this.stateObj);
const active = stateActive(this.stateObj);
const action = this.stateObj.attributes.action;
let actionColor: string | undefined;
if (action && action !== "idle" && action !== "off" && active) {
actionColor = stateColorCss(
this.stateObj,
HUMIDIFIER_ACTION_MODE[action]
);
}
const targetHumidity = this._targetHumidity;
const currentHumidity = this.stateObj.attributes.current_humidity;
if (targetHumidity != null) {
const inverted =
this.stateObj.attributes.device_class ===
HumidifierEntityDeviceClass.DEHUMIDIFIER;
return html`
<div
class="container"
style=${styleMap({
"--main-color": mainColor,
"--action-color": actionColor,
})}
>
<ha-control-circular-slider
.inverted=${inverted}
.value=${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._renderAction()}</div>
<div class="target-container">
${this._renderTarget(targetHumidity)}
</div>
</div>
${this._renderButtons()}
</div>
`;
}
return html`
<div
class="container"
style=${styleMap({
"--action-color": actionColor,
})}
>
<ha-control-circular-slider
.current=${currentHumidity}
.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;
}
.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-humidifier-humidity": HaMoreInfoHumidifierHumidity;
}
}

View File

@ -21,6 +21,7 @@ export const DOMAINS_WITH_NEW_MORE_INFO = [
"cover",
"climate",
"fan",
"humidifier",
"input_boolean",
"light",
"lock",

View File

@ -1,37 +1,45 @@
import "@material/mwc-list/mwc-list-item";
import {
css,
CSSResultGroup,
html,
LitElement,
PropertyValues,
css,
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";
import {
computeAttributeNameDisplay,
computeAttributeValueDisplay,
} from "../../../common/entity/compute_attribute_display";
import { stopPropagation } from "../../../common/dom/stop_propagation";
import { computeStateDisplay } from "../../../common/entity/compute_state_display";
import { supportsFeature } from "../../../common/entity/supports-feature";
import { computeRTLDirection } from "../../../common/util/compute_rtl";
import "../../../components/ha-select";
import "../../../components/ha-slider";
import "../../../components/ha-switch";
import { formatNumber } from "../../../common/number/format_number";
import { blankBeforePercent } from "../../../common/translations/blank_before_percent";
import {
HumidifierEntity,
HUMIDIFIER_SUPPORT_MODES,
HumidifierEntityFeature,
} from "../../../data/humidifier";
import { HomeAssistant } from "../../../types";
import { computeStateDisplay } from "../../../common/entity/compute_state_display";
import { moreInfoControlStyle } from "../components/ha-more-info-control-style";
import "../components/humidifier/ha-more-info-humidifier-humidity";
class MoreInfoHumidifier extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property() public stateObj?: HumidifierEntity;
@state() public _mode?: string;
protected willUpdate(changedProps: PropertyValues): void {
super.willUpdate(changedProps);
if (changedProps.has("stateObj")) {
this._mode = this.stateObj?.attributes.mode;
}
}
private _resizeDebounce?: number;
protected render() {
@ -42,43 +50,52 @@ class MoreInfoHumidifier extends LitElement {
const hass = this.hass;
const stateObj = this.stateObj;
const supportModes = supportsFeature(stateObj, HUMIDIFIER_SUPPORT_MODES);
const supportModes = supportsFeature(
stateObj,
HumidifierEntityFeature.MODES
);
const rtlDirection = computeRTLDirection(hass);
const currentHumidity = this.stateObj.attributes.current_humidity;
return html`
${currentHumidity
? html`<div class="current">
${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="controls">
<ha-more-info-humidifier-humidity
.hass=${this.hass}
.stateObj=${this.stateObj}
></ha-more-info-humidifier-humidity>
</div>
<div
class=${classMap({
"has-modes": supportModes,
})}
>
<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>
<ha-select
.label=${hass.localize("ui.card.humidifier.state")}
.value=${stateObj.state}
.label=${this.hass.localize("ui.card.humidifier.state")}
.value=${this.stateObj.state}
fixedMenuPosition
naturalMenuWidth
@selected=${this._handleStateChanged}
@ -86,21 +103,21 @@ class MoreInfoHumidifier extends LitElement {
>
<mwc-list-item value="off">
${computeStateDisplay(
hass.localize,
stateObj,
hass.locale,
this.hass.localize,
this.stateObj,
this.hass.locale,
this.hass.config,
hass.entities,
this.hass.entities,
"off"
)}
</mwc-list-item>
<mwc-list-item value="on">
${computeStateDisplay(
hass.localize,
stateObj,
hass.locale,
this.hass.localize,
this.stateObj,
this.hass.locale,
this.hass.config,
hass.entities,
this.hass.entities,
"on"
)}
</mwc-list-item>
@ -114,6 +131,7 @@ class MoreInfoHumidifier extends LitElement {
fixedMenuPosition
naturalMenuWidth
@selected=${this._handleModeChanged}
@action=${this._handleModeChanged}
@closed=${stopPropagation}
>
${stateObj.attributes.available_modes!.map(
@ -121,9 +139,9 @@ class MoreInfoHumidifier extends LitElement {
<mwc-list-item .value=${mode}>
${computeAttributeValueDisplay(
hass.localize,
stateObj,
stateObj!,
hass.locale,
this.hass.config,
hass.config,
hass.entities,
"mode",
mode
@ -133,7 +151,7 @@ class MoreInfoHumidifier extends LitElement {
)}
</ha-select>
`
: ""}
: nothing}
</div>
`;
}
@ -153,16 +171,6 @@ class MoreInfoHumidifier extends LitElement {
}, 500);
}
private _targetHumiditySliderChanged(ev) {
const newVal = ev.target.value;
this._callServiceHelper(
this.stateObj!.attributes.humidity,
newVal,
"set_humidity",
{ humidity: newVal }
);
}
private _handleStateChanged(ev) {
const newVal = ev.target.value || null;
this._callServiceHelper(
@ -173,16 +181,6 @@ class MoreInfoHumidifier extends LitElement {
);
}
private _handleModeChanged(ev) {
const newVal = ev.target.value || null;
this._callServiceHelper(
this.stateObj!.attributes.mode,
newVal,
"set_mode",
{ mode: newVal }
);
}
private async _callServiceHelper(
oldVal: unknown,
newVal: unknown,
@ -221,37 +219,71 @@ class MoreInfoHumidifier extends LitElement {
}
}
private _handleModeChanged(ev) {
ev.stopPropagation();
ev.preventDefault();
const index = ev.detail.index;
const newVal = this.stateObj!.attributes.available_modes![index];
const oldVal = this._mode;
if (!newVal || oldVal === newVal) return;
this._mode = newVal;
this.hass.callService("humidifier", "set_mode", {
entity_id: this.stateObj!.entity_id,
mode: newVal,
});
}
static get styles(): CSSResultGroup {
return css`
return [
moreInfoControlStyle,
css`
:host {
color: var(--primary-text-color);
}
.current {
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
text-align: center;
margin-bottom: 40px;
}
.current div {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
text-align: center;
flex: 1;
}
.current p {
margin: 0;
text-align: center;
color: var(--primary-text-color);
}
.current .label {
opacity: 0.8;
font-size: 14px;
line-height: 16px;
letter-spacing: 0.4px;
margin-bottom: 4px;
}
.current .value {
font-size: 22px;
font-weight: 500;
line-height: 28px;
}
ha-select {
width: 100%;
margin-top: 8px;
}
ha-slider {
width: 100%;
}
.container-humidity .single-row {
display: flex;
height: 50px;
}
.target-humidity {
width: 90px;
font-size: 200%;
margin: auto;
direction: ltr;
}
.single-row {
padding: 8px 0;
}
`;
`,
];
}
}

View File

@ -225,6 +225,15 @@ export class HuiTileCard extends LitElement implements LovelaceCard {
}
}
if (domain === "humidifier" && stateActive(stateObj)) {
const humidity = (stateObj as HumidifierEntity).attributes.humidity;
if (humidity) {
return `${Math.round(humidity)}${blankBeforePercent(
this.hass!.locale
)}%`;
}
}
const stateDisplay = computeStateDisplay(
this.hass!.localize,
stateObj,

View File

@ -1,32 +1,11 @@
import {
mdiArrowDownBold,
mdiArrowUpBold,
mdiClockOutline,
mdiPower,
} from "@mdi/js";
import { stateColorCss } from "../../../../../common/entity/state_color";
import {
HumidifierAction,
HUMIDIFIER_ACTION_ICONS,
HUMIDIFIER_ACTION_MODE,
HumidifierEntity,
HumidifierState,
} from "../../../../../data/humidifier";
import { ComputeBadgeFunction } from "./tile-badge";
export const HUMIDIFIER_ACTION_ICONS: Record<HumidifierAction, string> = {
drying: mdiArrowDownBold,
humidifying: mdiArrowUpBold,
idle: mdiClockOutline,
off: mdiPower,
};
export const HUMIDIFIER_ACTION_MODE: Record<HumidifierAction, HumidifierState> =
{
drying: "on",
humidifying: "on",
idle: "off",
off: "off",
};
export const computeHumidifierBadge: ComputeBadgeFunction = (stateObj) => {
const hvacAction = (stateObj as HumidifierEntity).attributes.action;

View File

@ -1000,6 +1000,10 @@
"climate": {
"target_label": "{action} to target",
"target": "Target"
},
"humidifier": {
"target_label": "[%key:ui::dialogs::more_info_control::climate::target_label%]",
"target": "[%key:ui::dialogs::more_info_control::climate::target%]"
}
},
"entity_registry": {